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:
+74
-23
@@ -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
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user