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
+74 -23
View File
@@ -527,14 +527,39 @@ function targetHarnessPaths(options) {
function targetRuntimePackageNames(options) {
const target = targetHarnessPaths(options)
return [target.koffiPackage]
return [target.koffiPackage, target.canvasPackage]
}
function lockedTargetRuntimePackage(packageName) {
const expectedVersion =
packageJson.optionalDependencies?.[packageName]
function lockedTargetRuntimePackage(
packageName,
packageMetadata = packageJson,
lockMetadata = packageLock
) {
let expectedVersion =
packageMetadata.optionalDependencies?.[packageName]
if (
typeof expectedVersion !== 'string' &&
packageName.startsWith('@napi-rs/canvas-')
) {
const canvasPackageName = '@napi-rs/canvas'
const canvasVersion =
packageMetadata.dependencies?.[canvasPackageName]
const canvasLockEntry =
lockMetadata.packages?.[`node_modules/${canvasPackageName}`]
if (
typeof canvasVersion !== 'string' ||
canvasLockEntry?.version !== canvasVersion ||
canvasLockEntry.optionalDependencies?.[packageName] !==
canvasVersion
) {
throw new Error(
`目标 Runtime 依赖未完整锁定:${packageName}`
)
}
expectedVersion = canvasVersion
}
const lockEntry =
packageLock.packages?.[`node_modules/${packageName}`]
lockMetadata.packages?.[`node_modules/${packageName}`]
if (
typeof expectedVersion !== 'string' ||
lockEntry?.version !== expectedVersion ||
@@ -548,7 +573,6 @@ function lockedTargetRuntimePackage(packageName) {
return {
name: packageName,
version: expectedVersion,
resolved: lockEntry.resolved,
integrity: lockEntry.integrity
}
}
@@ -596,9 +620,13 @@ function verifyArchiveIntegrity(filePath, expectedIntegrity) {
}
}
function installedPackageMatches(packageName, expectedVersion) {
function installedPackageMatches(
packageName,
expectedVersion,
runtimeRoot = root
) {
const manifestPath = join(
root,
runtimeRoot,
'node_modules',
...packageName.split('/'),
'package.json'
@@ -618,14 +646,29 @@ function installedPackageMatches(packageName, expectedVersion) {
return true
}
async function stageTargetRuntimeDependencies(options) {
async function stageTargetRuntimeDependencies(
options,
dependencies = {}
) {
const runtimeRoot = dependencies.root ?? root
const runtimePackageJson =
dependencies.packageJson ?? packageJson
const runtimePackageLock =
dependencies.packageLock ?? packageLock
const missing = targetRuntimePackageNames(options)
.map(lockedTargetRuntimePackage)
.map((packageName) =>
lockedTargetRuntimePackage(
packageName,
runtimePackageJson,
runtimePackageLock
)
)
.filter(
(dependency) =>
!installedPackageMatches(
dependency.name,
dependency.version
dependency.version,
runtimeRoot
)
)
if (missing.length === 0) {
@@ -644,14 +687,27 @@ async function stageTargetRuntimeDependencies(options) {
}
try {
const npm = npmInvocation()
const npm = dependencies.npmInvocation?.() ?? npmInvocation()
const captureCommand =
dependencies.runCapture ?? runCapture
const extractArchive =
dependencies.extractArchive ??
((archivePath, destination) =>
run('tar', [
'-xzf',
archivePath,
'-C',
destination,
'--strip-components',
'1'
]))
for (const [index, dependency] of missing.entries()) {
const archiveDirectory = join(
stagingRoot,
`package-${index}`
)
mkdirSync(archiveDirectory, { recursive: true })
const output = await runCapture(npm.command, [
const output = await captureCommand(npm.command, [
...npm.prefixArgs,
'pack',
`${dependency.name}@${dependency.version}`,
@@ -671,7 +727,7 @@ async function stageTargetRuntimeDependencies(options) {
verifyArchiveIntegrity(archivePath, dependency.integrity)
const destination = join(
root,
runtimeRoot,
'node_modules',
...dependency.name.split('/')
)
@@ -682,18 +738,12 @@ async function stageTargetRuntimeDependencies(options) {
}
mkdirSync(destination, { recursive: true })
stagedDirectories.push(destination)
await run('tar', [
'-xzf',
archivePath,
'-C',
destination,
'--strip-components',
'1'
])
await extractArchive(archivePath, destination)
if (
!installedPackageMatches(
dependency.name,
dependency.version
dependency.version,
runtimeRoot
)
) {
throw new Error(
@@ -1619,6 +1669,7 @@ module.exports = {
parseArguments,
parsePackedPackageMetadata,
platformDefinitions,
lockedTargetRuntimePackage,
replaceOutput,
stageTargetRuntimeDependencies,
targetRuntimePackageNames,
+58 -19
View File
@@ -32,13 +32,21 @@ function validateItems(value, label) {
fail(`${label} contains a non-string item`)
}
const normalized = item.trim()
if (!normalized || normalized.length > 240) {
if (!normalized || normalized.length > 500) {
fail(`${label} contains an empty or oversized item`)
}
return normalized
})
}
const releaseNoteSections = [
'highlights',
'features',
'fixes',
'notices'
]
const legacyReleaseNoteSections = ['features', 'fixes']
function validateRelease(value, index) {
const label = `releases[${index}]`
if (!hasExactKeys(value, ['version', 'releasedAt', 'notes'])) {
@@ -63,28 +71,50 @@ function validateRelease(value, index) {
const notes = Object.fromEntries(
['zh-CN', 'en-US'].map((locale) => {
const localized = value.notes[locale]
if (!hasExactKeys(localized, ['features', 'fixes'])) {
const isCurrentFormat = hasExactKeys(
localized,
releaseNoteSections
)
const isLegacyFormat = hasExactKeys(
localized,
legacyReleaseNoteSections
)
if (!isCurrentFormat && !isLegacyFormat) {
fail(`${label}.notes.${locale} has invalid fields`)
}
const features = validateItems(
localized.features,
`${label}.notes.${locale}.features`
const normalized = Object.fromEntries(
releaseNoteSections.map((section) => [
section,
section in localized
? validateItems(
localized[section],
`${label}.notes.${locale}.${section}`
)
: []
])
)
const fixes = validateItems(
localized.fixes,
`${label}.notes.${locale}.fixes`
)
if (features.length + fixes.length === 0) {
if (normalized.highlights.length > 3) {
fail(
`${label}.notes.${locale}.highlights must contain no more than 3 items`
)
}
if (
releaseNoteSections.every(
(section) => normalized[section].length === 0
)
) {
fail(`${label}.notes.${locale} must not be empty`)
}
return [locale, { features, fixes }]
return [locale, normalized]
})
)
if (
notes['zh-CN'].features.length !== notes['en-US'].features.length ||
notes['zh-CN'].fixes.length !== notes['en-US'].fixes.length
) {
fail(`${label} localized section counts do not match`)
for (const section of releaseNoteSections) {
if (
notes['zh-CN'][section].length !==
notes['en-US'][section].length
) {
fail(`${label} localized ${section} counts do not match`)
}
}
return {
version: value.version,
@@ -126,14 +156,18 @@ const localizedDefinitions = [
{
locale: 'zh-CN',
title: `GoodBuddy ${release.version} 更新内容`,
highlights: '本次亮点',
features: '功能更新',
fixes: '问题修复'
fixes: '问题修复',
notices: '使用前请留意'
},
{
locale: 'en-US',
title: `What's New in GoodBuddy ${release.version}`,
highlights: 'Highlights',
features: 'Features',
fixes: 'Bug Fixes'
fixes: 'Bug Fixes',
notices: 'Before You Start'
}
]
@@ -151,8 +185,13 @@ const markdown = localizedDefinitions
...(index === 0 ? [] : ['---', '']),
`# ${definition.title}`,
'',
...markdownSection(
definition.highlights,
notes.highlights
),
...markdownSection(definition.features, notes.features),
...markdownSection(definition.fixes, notes.fixes)
...markdownSection(definition.fixes, notes.fixes),
...markdownSection(definition.notices, notes.notices)
]
})
.join('\n')