feat: expand multi-runtime workflows for 0.10.0

GoodBuddy previously exposed Runtime capabilities, MCP assignments, plugin controls, context compaction, usage reporting, and task-completion behavior through incomplete or inconsistent paths. This release unifies managed OpenCode, Continue, and DeepSeek Harness controls; adds DSH plugin and image workflows; strengthens MCP and Runtime lifecycle bounds; fixes Windows notification activation; validates cross-architecture packages; and presents the approved bilingual four-section release notes.

The DSH plugin marketplace remains a default-off preview whose trusted third-party code runs with current-user permissions. Ask remains read-only, Execute keeps approval controls, and context compaction may use the selected model without deleting GoodBuddy chat history.

Release note: GoodBuddy 0.10.0 重点完善多 Runtime 工作流,统一 OpenCode、Continue 与 DeepSeek Harness 的能力、MCP、插件和上下文管理,并提升长对话、多会话与任务通知的连贯性。
This commit is contained in:
mesalogo
2026-08-17 12:57:12 +08:00
parent f5ee9b2198
commit cbbe30896e
50 changed files with 4077 additions and 605 deletions
+197 -36
View File
@@ -33,6 +33,7 @@ const MAXIMUM_CATALOG_ENTRIES = 1_000
const DEFAULT_REQUEST_TIMEOUT_MS = 15_000
const DEFAULT_INSTALL_TIMEOUT_MS = 5 * 60_000
const MAXIMUM_PROCESS_OUTPUT_CHARACTERS = 64 * 1024
const MAXIMUM_PACKUMENT_VERSIONS = 20_000
const npmSearchPackageSchema = z
.object({
@@ -93,11 +94,59 @@ const npmVersionManifestSchema = npmInstalledManifestSchema.extend({
dist: npmDistributionSchema
})
const npmPackumentSchema = z
.object({
versions: z.record(z.string(), npmVersionManifestSchema)
function isPlainObject(
value: unknown
): value is Record<string, unknown> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return false
}
const prototype = Object.getPrototypeOf(value)
return prototype === Object.prototype || prototype === null
}
const npmPackumentVersionsSchema = z
.custom<Record<string, unknown>>(isPlainObject, {
message: 'npm packument versions must be a plain object'
})
.passthrough()
.superRefine((versions, context) => {
const keys = Object.keys(versions)
if (keys.length > MAXIMUM_PACKUMENT_VERSIONS) {
context.addIssue({
code: 'too_big',
origin: 'object',
maximum: MAXIMUM_PACKUMENT_VERSIONS,
inclusive: true,
path: [],
message: 'npm packument contains too many versions'
})
}
if (
keys.some(
(key) =>
key === '__proto__' ||
key === 'prototype' ||
key === 'constructor'
)
) {
context.addIssue({
code: 'custom',
path: [],
message: 'npm packument contains an unsafe version key'
})
}
})
const npmPackumentSchema = z
.custom<Record<string, unknown>>(isPlainObject, {
message: 'npm packument must be a plain object'
})
.pipe(
z
.object({
versions: npmPackumentVersionsSchema
})
.passthrough()
)
type NpmVersionManifest = z.infer<typeof npmVersionManifestSchema>
@@ -114,6 +163,7 @@ export type PackageManagerRunner = (
cwd: string
env: NodeJS.ProcessEnv
timeoutMs: number
signal?: AbortSignal
}
) => Promise<PackageManagerRunResult>
@@ -124,11 +174,13 @@ function waitForProcessClose(
return Promise.resolve()
}
return new Promise((resolve) => {
const timer = setTimeout(resolve, 5_000)
child.once('close', () => {
const finish = (): void => {
clearTimeout(timer)
child.removeListener('close', finish)
resolve()
})
}
const timer = setTimeout(finish, 5_000)
child.once('close', finish)
})
}
@@ -147,11 +199,13 @@ async function terminatePackageManager(
}
)
await new Promise<void>((resolve) => {
const timer = setTimeout(resolve, 5_000)
const finish = (): void => {
clearTimeout(timer)
killer.removeListener('close', finish)
killer.removeListener('error', finish)
resolve()
}
const timer = setTimeout(finish, 5_000)
killer.once('close', finish)
killer.once('error', finish)
})
@@ -183,6 +237,14 @@ export const runPackageManager: PackageManagerRunner = (
options
) =>
new Promise((resolve, reject) => {
if (options.signal?.aborted) {
reject(
options.signal.reason instanceof Error
? options.signal.reason
: new Error('DSH 插件安装已取消')
)
return
}
const child = spawn(command, [...args], {
cwd: options.cwd,
env: options.env,
@@ -194,42 +256,79 @@ export const runPackageManager: PackageManagerRunner = (
let stdout = ''
let stderr = ''
let settled = false
const timer = setTimeout(() => {
if (settled) {
return
}
settled = true
void terminatePackageManager(child).then(
() => reject(new Error('DSH 插件安装超时')),
() => reject(new Error('DSH 插件安装超时'))
)
}, options.timeoutMs)
child.stdout?.on('data', (chunk) => {
let terminating = false
const onStdout = (chunk: unknown): void => {
stdout = boundedAppend(stdout, chunk)
})
child.stderr?.on('data', (chunk) => {
}
const onStderr = (chunk: unknown): void => {
stderr = boundedAppend(stderr, chunk)
})
child.once('error', (error) => {
}
const cleanup = (): void => {
clearTimeout(timer)
options.signal?.removeEventListener('abort', onAbort)
child.stdout?.removeListener('data', onStdout)
child.stderr?.removeListener('data', onStderr)
child.removeListener('error', onError)
child.removeListener('close', onClose)
}
const settleRejected = (error: Error): void => {
if (settled) {
return
}
settled = true
clearTimeout(timer)
cleanup()
reject(error)
})
child.once('close', (code) => {
if (settled) {
}
const terminateAndReject = (error: Error): void => {
if (settled || terminating) {
return
}
terminating = true
void terminatePackageManager(child).then(
() => settleRejected(error),
() => settleRejected(error)
)
}
const onAbort = (): void => {
terminateAndReject(
options.signal?.reason instanceof Error
? options.signal.reason
: new Error('DSH 插件安装已取消')
)
}
const onError = (error: Error): void => {
if (terminating) {
return
}
settleRejected(error)
}
const onClose = (code: number | null): void => {
if (settled || terminating) {
return
}
settled = true
clearTimeout(timer)
cleanup()
resolve({
exitCode: code ?? 1,
stdout,
stderr
})
}
const timer = setTimeout(
() =>
terminateAndReject(new Error('DSH 插件安装超时')),
options.timeoutMs
)
child.stdout?.on('data', onStdout)
child.stderr?.on('data', onStderr)
child.once('error', onError)
child.once('close', onClose)
options.signal?.addEventListener('abort', onAbort, {
once: true
})
if (options.signal?.aborted) {
onAbort()
}
})
function publicHttpUrl(value: string | undefined): string | undefined {
@@ -292,14 +391,18 @@ function catalogEntry(
async function fetchJson(
fetcher: typeof fetch,
url: URL,
timeoutMs: number
timeoutMs: number,
signal?: AbortSignal
): Promise<unknown> {
const timeoutSignal = AbortSignal.timeout(timeoutMs)
const response = await fetcher(url, {
headers: {
accept: 'application/json',
'user-agent': 'GoodBuddy-DSH-Marketplace/1'
},
signal: AbortSignal.timeout(timeoutMs)
signal: signal
? AbortSignal.any([signal, timeoutSignal])
: timeoutSignal
})
if (!response.ok) {
throw new Error(`DSH 插件市场请求失败(HTTP ${response.status}`)
@@ -520,6 +623,15 @@ async function prepareNodeCommand(
}
export class DshNpmExtensionInstaller {
private disposed = false
private readonly activeInstalls = new Map<
Promise<{
entrypoint: string
integrity?: string
}>,
AbortController
>()
constructor(
private readonly options: {
dshHome: string
@@ -534,7 +646,7 @@ export class DshNpmExtensionInstaller {
}
) {}
async install(
install(
input: Parameters<
RuntimeExtensionStoreDependencies['install']
>[0]
@@ -542,7 +654,45 @@ export class DshNpmExtensionInstaller {
entrypoint: string
integrity?: string
}> {
const manifest = await this.resolveManifest(input.entry)
if (this.disposed) {
return Promise.reject(new Error('DSH 插件安装器正在关闭'))
}
const controller = new AbortController()
const operation = this.performInstall(input, controller.signal)
this.activeInstalls.set(operation, controller)
void operation.then(
() => {
this.activeInstalls.delete(operation)
},
() => {
this.activeInstalls.delete(operation)
}
)
return operation
}
async dispose(): Promise<void> {
this.disposed = true
const active = [...this.activeInstalls.entries()]
for (const [, controller] of active) {
controller.abort(new Error('应用退出,DSH 插件安装已取消'))
}
await Promise.allSettled(
active.map(([operation]) => operation)
)
}
private async performInstall(
input: Parameters<
RuntimeExtensionStoreDependencies['install']
>[0],
signal: AbortSignal
): Promise<{
entrypoint: string
integrity?: string
}> {
const manifest = await this.resolveManifest(input.entry, signal)
signal.throwIfAborted()
await writeFile(
join(input.destinationDirectory, 'package.json'),
`${JSON.stringify(
@@ -608,6 +758,7 @@ export class DshNpmExtensionInstaller {
],
{
cwd: input.destinationDirectory,
signal,
env: {
...packageManagerEnvironment,
npm_config_audit: 'false',
@@ -625,6 +776,7 @@ export class DshNpmExtensionInstaller {
} catch (error) {
throw packageManagerError(error)
}
signal.throwIfAborted()
if (result.exitCode !== 0) {
const detail =
result.stderr.trim() ||
@@ -686,7 +838,8 @@ export class DshNpmExtensionInstaller {
}
private async resolveManifest(
entry: RuntimeExtensionCatalogEntry
entry: RuntimeExtensionCatalogEntry,
signal: AbortSignal
): Promise<NpmVersionManifest> {
const registryUrl = (
this.options.registryUrl ?? NPM_REGISTRY_URL
@@ -699,13 +852,21 @@ export class DshNpmExtensionInstaller {
this.options.fetcher ?? fetch,
url,
this.options.requestTimeoutMs ??
DEFAULT_REQUEST_TIMEOUT_MS
DEFAULT_REQUEST_TIMEOUT_MS,
signal
)
)
const manifest = packument.versions[entry.package.version]
if (!manifest) {
if (
!Object.prototype.hasOwnProperty.call(
packument.versions,
entry.package.version
)
) {
throw new Error('DSH 插件精确版本未发布')
}
const manifest = npmVersionManifestSchema.parse(
packument.versions[entry.package.version]
)
if (
manifest.name !== entry.package.name ||
manifest.version !== entry.package.version