fix: restore packaged runtimes and ship macOS DMG only
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
Deploy website to GitHub Pages / Deploy static website (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s

Desktop 0.12.7 omitted OpenCode's offline node_modules because the resource
copier excludes node_modules at its source root. Copy from the parent and
verify every prepared configuration file in the final package. Bundle DSH
session projection with the unpacked Host instead of leaving an ASAR-only
external import, and reject external DeepSeek imports in shipped chunks.

Run packaged OpenCode plugin import and the real DSH UtilityProcess handshake
in Windows x64 CI before removing unpacked output. Add regressions using the
actual electron-builder copier and Vite externalization configuration.

Prepare approved Desktop 0.12.8 notes and change macOS defaults and CI to DMG
only. Update aggregation, website downloads and update readers for 12
installers and 20 assets. Current readers retain historical DMG/ZIP support;
the user accepts that older macOS GitHub readers and all older mirror readers
need manual upgrades. Agent remains 0.11.21, with no new Agent or LoongArch tag.

Validation: 3608 tests passed, 0 failed, 61 skipped across 339 files;
typecheck, lint, release-note validation, website static checks and 18 website
tests passed. No local production build or packaged-app launch was performed
at the user's request; actual packaged startup validation runs in CI.

Release note: 修复安装后 OpenCode 离线依赖缺失和 DeepSeek Harness Host 启动退出;macOS 改为仅提供 DMG,受影响旧版用户需从下载页手动升级。
This commit is contained in:
mesalogo
2026-09-08 14:17:38 +08:00
parent 451b30d434
commit f95acdc000
29 changed files with 371 additions and 53 deletions
+2 -2
View File
@@ -248,7 +248,7 @@ jobs:
- name: Build, sign and notarize macOS release packages
if: matrix.platform == 'macos' && steps.macos-signing.outputs.enabled == 'true'
run: npm run release:package -- --platform ${{ matrix.platform }} --arch ${{ matrix.arch }} --skip-build
run: npm run release:package -- --platform ${{ matrix.platform }} --arch ${{ matrix.arch }} --format dmg --skip-build
env:
ELECTRON_CACHE: ${{ runner.temp }}/electron
ELECTRON_BUILDER_CACHE: ${{ runner.temp }}/electron-builder
@@ -261,7 +261,7 @@ jobs:
- name: Build unsigned macOS release packages
if: matrix.platform == 'macos' && steps.macos-signing.outputs.enabled == 'false'
run: npm run release:package -- --platform ${{ matrix.platform }} --arch ${{ matrix.arch }} --skip-build --unsigned
run: npm run release:package -- --platform ${{ matrix.platform }} --arch ${{ matrix.arch }} --format dmg --skip-build --unsigned
env:
ELECTRON_CACHE: ${{ runner.temp }}/electron
ELECTRON_BUILDER_CACHE: ${{ runner.temp }}/electron-builder
+6 -5
View File
@@ -219,6 +219,7 @@ package format v1 归档已经包含固定 Agent 和 Node,可直接用于 Host
桌面包会携带经过版本与完整性校验的 OpenCode、Continue 和 DSH 插件安装 Runtime:
- OpenCode 平台二进制来自 `.runtime-resources/<arch>`。
- Desktop 的离线配置复制以 `.runtime-resources` 为源、`opencode-config{,/**/*}` 为过滤器,避免 electron-builder 对复制源根目录下 `node_modules` 的固定排除。最终包逐文件核对离线配置与依赖;DSH bootstrap 和 chunk 不得残留外部 `@deepseek-ai/*` 导入。Windows x64 CI 在删除解包目录前实际导入包内 OpenCode 插件并运行隔离的 DSH UtilityProcess 握手与 npm 探针,失败即停止发布。
- OpenCode 离线配置位于 `.runtime-resources/opencode-config`,构建时按已安装 `@opencode-ai/plugin/package.json` 的版本复用,不依赖根 lockfile 或自定义 ready/integrity 标记。缺失、不可读或版本不匹配时,在 staging 中执行精确版本 npm 安装,成功后替换缓存;安装失败保留旧缓存并清理 staging。本机共享配置同样比较随包插件与缓存插件的版本,版本不同时通过 staging 替换。
- 离线依赖安装禁用 npm bin links;远程 Runtime 组包排除不使用的 `.bin` 命令链接,并把复制后的普通依赖文件统一为 `0644`,OpenCode 入口仍为 `0755`。目录元数据、每个依赖文件及许可证继续纳入签名校验。
- Continue Runtime 来自锁定版本的 `@continuedev/cli`。
@@ -397,8 +398,8 @@ DeepSeek Harness Runtime、`app.asar`、目标架构和安装包签名。
npm run release:package -- --platform macos --arch <x64|arm64> --unsigned
```
默认发布产物为 Windows 的 NSIS 安装包与 portable ZIP、macOS 的 DMG 与
ZIP,以及 Linux 的 AppImage、DEB 与 RPM。Linux 原生 Runner 必须安装
默认发布产物为 Windows 的 NSIS 安装包与 portable ZIP、macOS 的 DMG,
以及 Linux 的 AppImage、DEB 与 RPM。Linux 原生 Runner 必须安装
`rpm`/`rpmbuild` 工具后再调用 electron-builder。Windows portable ZIP 解压后可直接
运行 `GoodBuddy.exe`,并包含启用便携数据目录的
`.goodbuddy-portable.json`。每个目标目录都包含带文件大小和 SHA-256 的
@@ -434,8 +435,8 @@ Desktop 正式发布必须把候选分支推送与标签推送拆成两个阶段
全部目标成功后,才会严格校验并聚合所有平台产物,生成按平台重命名的
manifests、总 `release-manifest.json` 和 `SHA256SUMS`。随后工作流通过
GitHub OIDC 获取短期 STS 凭据,将发布资产和 `site-release.json` 上传到
北京 OSS 的不可变版本目录,并公开校验 14 个安装包。验证通过后才创建或
更新 draft GitHub Release、上传 22 个 Release 资产并正式发布,最后原子
北京 OSS 的不可变版本目录,并公开校验 12 个安装包。验证通过后才创建或
更新 draft GitHub Release、上传 20 个 Release 资产并正式发布,最后原子
切换官网 `latest.json`。任一步失败都不会提前切换官网最新版本。
同一标签重跑时,工作流会根据 `resources/release-notes.json` 重新生成并
@@ -482,7 +483,7 @@ git push github "$tag"
macOS 发布 job 会先原子判断 Apple 凭据状态:以下五项 Actions Secrets 全部
存在时,使用 Developer ID Application 证书签名,并通过 App Store Connect
API Key 提交 Apple notarization;五项全部缺失时,明确生成未签名、未公证的
DMG 和 ZIP,并在 Actions 日志与摘要中警告 Gatekeeper 限制;只配置一部分时
DMG,并在 Actions 日志与摘要中警告 Gatekeeper 限制;只配置一部分时
任务失败,不能静默降级为未签名包。
- `MACOS_CERTIFICATE_BASE64`:包含证书及私钥的 `.p12` 文件经 Base64 编码后的内容。
+13 -3
View File
@@ -21,7 +21,7 @@ otherwise.
packages target compatible x64 and arm64 environments, including UOS, Kylin,
Hygon, Zhaoxin, Kunpeng, and Phytium systems. This is not vendor certification.
LoongArch has a separate experimental loong64 preview outside standard
releases and automatic updates; no preview is planned for 0.12.7. See the
releases and automatic updates; no preview is planned for 0.12.8. See the
[preview boundaries](./docs/development/loongarch-preview-build.md).
- [x] **Configurable global shortcut**: Enable, disable, or record an Electron
accelerator under Platform Features / General. The default remains
@@ -174,7 +174,7 @@ otherwise.
failure, Agent `SIGKILL`/restart, and recovery from a reopened Desktop SQLite
database. Successful tool START/END events appear exactly once, with no
Prompt, provider, or tool replay observed. The current Agent source lock is
`0.11.21`, while the current Desktop release candidate is `0.12.7`; formal
`0.11.21`, while the current Desktop release candidate is `0.12.8`; formal
publication status follows the separate Agent and Desktop
release channels. Current macOS source has passed native package installation,
detached lifecycle, Attach, real Ask/Execute, and cancellation of tools in
@@ -567,7 +567,17 @@ otherwise.
build commands.
- [x] **Six-target native release matrix**: Native runners build Windows,
macOS, and Linux `x64` and `arm64` targets with release manifests and SHA-256
hashes. Linux also produces AppImage, DEB, and RPM.
hashes. Windows provides NSIS and portable ZIP, macOS DMG only, and Linux
AppImage, DEB, and RPM, for 12 installers and 20 published assets.
- [x] **Desktop Runtime package verification**: Offline OpenCode dependencies
are copied without electron-builder's root node_modules exclusion; DSH
session projection is bundled into the unpacked Host. Final package checks
compare the offline dependency tree and reject external DeepSeek imports.
Windows x64 CI additionally imports the packaged OpenCode plugin and checks
the real DSH UtilityProcess handshake before publishing.
- [x] **DMG-only update manifests**: Current Desktop and website readers accept
DMG-only and historical DMG/ZIP manifests. Older macOS GitHub readers and all
older mirror readers require a manual upgrade from the download page.
### Open interfaces, team collaboration, and remote execution
+5 -3
View File
@@ -14,7 +14,7 @@
### 桌面基础、工作空间与上下文
- [x] **跨平台桌面应用**:支持 Windows、macOS、Linux,以及 `x64`、`arm64` 发布目标。
- [x] **信创软硬件覆盖**:正式 Linux 安装包覆盖国产 `x64` 与 `arm64` 架构,可用于统信 UOS、银河麒麟及海光、兆芯、鲲鹏、飞腾等兼容环境。龙芯 LoongArch 另行提供 `loong64` 实验预览版,不属于正式发布矩阵,也未纳入自动更新;`0.12.7` 不构建该预览。这里的覆盖范围不等同于厂商或整机兼容认证,具体状态与限制见[龙芯预览版构建说明](./docs/development/loongarch-preview-build.md)。
- [x] **信创软硬件覆盖**:正式 Linux 安装包覆盖国产 `x64` 与 `arm64` 架构,可用于统信 UOS、银河麒麟及海光、兆芯、鲲鹏、飞腾等兼容环境。龙芯 LoongArch 另行提供 `loong64` 实验预览版,不属于正式发布矩阵,也未纳入自动更新;`0.12.8` 不构建该预览。这里的覆盖范围不等同于厂商或整机兼容认证,具体状态与限制见[龙芯预览版构建说明](./docs/development/loongarch-preview-build.md)。
- [x] **可配置全局快捷唤起**:在“平台功能 / 通用设置”中启停或录制 Electron accelerator;默认保留 `CommandOrControl+Shift+Space`,冲突或保存失败时继续使用上一组已注册快捷键,并显示可处理的状态。
- [x] **Projects、独立对话与会话分支**:按项目隔离上下文,管理会话、附件和 Git 工作区变更;本地会话可在稳定状态下复制当前聊天内容到独立分支,分支持续显示来源徽标且不复制 Task、队列或成果归属;项目选择器区分本地、托管 SSH 与远程消息通道项目,托管 SSH 项目按 Host 分组并在 Host 标题显示真实 Agent 连接状态,项目行只保留远端路径。
- [x] **文件、截图、窗口、剪贴板上下文**:用户明确选择后才加入模型上下文。
@@ -104,10 +104,12 @@
### 开源、构建与发布
- 当前源码候选为 Desktop `0.12.7`、Agent `0.11.21`,OpenCode 固定为 `1.18.29`;正式发布状态以 Desktop 与 Agent 的独立发布渠道为准。
- 当前源码候选为 Desktop `0.12.8`、Agent `0.11.21`,OpenCode 固定为 `1.18.29`;正式发布状态以 Desktop 与 Agent 的独立发布渠道为准。
- [x] **0BSD 开源许可**:原创代码可自由使用、复制、修改、分发和商用;第三方组件和资源仍遵循各自许可证。
- [x] **可复现依赖安装与源码构建**:使用锁定依赖、Node.js 24 和统一的测试、类型检查、Lint、生产构建命令。
- [x] **六平台原生发布矩阵**:Windows、macOS、Linux 的 `x64`、`arm64` 目标由原生 Runner 构建,并提供发布清单和 SHA-256 哈希;Linux 同时生成 AppImage、DEB 和 RPM。
- [x] **六平台原生发布矩阵**:Windows、macOS、Linux 的 `x64`、`arm64` 目标由原生 Runner 构建,并提供发布清单和 SHA-256 哈希。Windows 提供 NSIS 与 portable ZIP,macOS 仅 DMG,Linux 提供 AppImage、DEB 和 RPM,共 12 个安装包、20 个发布资产。
- [x] **桌面 Runtime 包校验**:OpenCode 离线依赖复制避开 electron-builder 对源根目录 node_modules 的排除,DSH 会话投影组件打入解包后的 Host。最终包逐文件核对离线依赖并拒绝外部 DeepSeek 导入;Windows x64 CI 还会导入包内 OpenCode 插件、验证真实 DSH UtilityProcess 握手,通过后才发布。
- [x] **DMG-only 更新清单**:当前 Desktop 和官网读取器同时接受仅 DMG 与历史 DMG/ZIP 清单。旧版 macOS 的 GitHub 更新检查及所有旧版镜像更新检查需要从下载页手动升级。
### 开放接口、团队协作与远程执行
+2 -2
View File
@@ -19,8 +19,8 @@ const manifestName = 'release-manifest.json'
const targetDefinitions = [
{ platform: 'windows', arch: 'x64', formats: ['nsis', 'portable'] },
{ platform: 'windows', arch: 'arm64', formats: ['nsis', 'portable'] },
{ platform: 'macos', arch: 'x64', formats: ['dmg', 'zip'] },
{ platform: 'macos', arch: 'arm64', formats: ['dmg', 'zip'] },
{ platform: 'macos', arch: 'x64', formats: ['dmg'] },
{ platform: 'macos', arch: 'arm64', formats: ['dmg'] },
{ platform: 'linux', arch: 'x64', formats: ['AppImage', 'deb', 'rpm'] },
{ platform: 'linux', arch: 'arm64', formats: ['AppImage', 'deb', 'rpm'] }
]
+30 -2
View File
@@ -42,6 +42,10 @@ const {
const {
readZipCentralDirectory
} = require('./zip-central-directory.cjs')
const {
verifyOpenCodeConfig,
verifyHarnessBundleImports
} = require('./verify-desktop-runtimes.cjs')
const root = join(__dirname, '..')
const packageJson = JSON.parse(
@@ -90,6 +94,8 @@ const portableRequiredFiles = [
'resources/tray-icon.png',
'resources/tool-environment/managed-python-artifacts.json',
'resources/runtimes/opencode/opencode.exe',
'resources/runtimes/opencode-config/node_modules/@opencode-ai/plugin/package.json',
'resources/runtimes/opencode-config/node_modules/@opencode-ai/plugin/dist/index.js',
'resources/runtimes/continue/package.json',
'resources/runtimes/npm/bin/npm-cli.js',
'resources/runtimes/npm/bin/npx-cli.js',
@@ -126,7 +132,7 @@ const platformDefinitions = {
},
macos: {
builderFlag: '--mac',
defaultFormats: ['dmg', 'zip'],
defaultFormats: ['dmg'],
supportedFormats: ['dmg', 'zip'],
unpackedPattern: /^mac(?:-.+)?$/u,
executable: [
@@ -820,6 +826,10 @@ function verifyHarnessPackage(
join(unpackedRoot, ...harnessHostEntry.split('/')),
'DeepSeek Harness 可执行 Host'
)
verifyHarnessBundleImports(
readFileSync(join(unpackedRoot, ...harnessHostEntry.split('/')), 'utf8'),
harnessHostEntry
)
const harnessLlmChunk = [...entries]
.map((entry) => entry.slice(1).split(sep).join('/'))
.find((entry) =>
@@ -868,6 +878,10 @@ function verifyHarnessPackage(
join(unpackedRoot, ...chunkPath.split('/')),
'DeepSeek Harness 可执行 module chunk'
)
verifyHarnessBundleImports(
readFileSync(join(unpackedRoot, ...chunkPath.split('/')), 'utf8'),
chunkPath
)
}
for (const [packageName, expectedVersion] of Object.entries(
harnessPackageVersions
@@ -1080,6 +1094,10 @@ function verifyUnpackedOutput(directory, options) {
'Remote Runtime 锁定清单'
)
assertFile(runtimeExecutable, 'OpenCode Runtime')
verifyOpenCodeConfig(
resources,
join(root, '.runtime-resources', 'opencode-config')
)
assertFile(
join(resources, 'runtimes', 'continue', 'dist', 'index.js'),
'Continue Runtime'
@@ -1567,7 +1585,7 @@ function printHelp() {
默认格式:
windows: nsis, portable (ZIP)
macos: dmg, zip
macos: dmg
linux: AppImage, deb, rpm`)
}
@@ -1629,6 +1647,16 @@ async function main(argv = process.argv.slice(2)) {
stagingDirectory,
options
)
if (
process.env.GITHUB_ACTIONS === 'true' &&
options.platform === 'windows' &&
options.arch === 'x64'
) {
await run(process.execPath, [
join(root, 'build', 'run-packaged-deepseek-harness-smoke.cjs'),
unpackedDirectory
])
}
if (
options.platform === 'windows' &&
options.formats.includes('portable')
@@ -13,6 +13,7 @@ const {
const { statSync } = require('node:fs')
const { tmpdir } = require('node:os')
const { delimiter, join, resolve } = require('node:path')
const { pathToFileURL } = require('node:url')
const unpackedPath = process.argv[2]
? resolve(process.argv[2])
@@ -109,6 +110,20 @@ async function main() {
const npmProject = join(root, 'npm-project')
const npmFixture = join(root, 'npm-fixture')
await mkdir(project, { recursive: true })
const pluginUrl = pathToFileURL(join(
unpackedPath, 'resources', 'runtimes', 'opencode-config',
'node_modules', '@opencode-ai', 'plugin', 'dist', 'index.js'
)).href
const importedPlugin = await run(
executable,
['--input-type=module', '--eval', `await import(${JSON.stringify(pluginUrl)})`],
{ ...process.env, ELECTRON_RUN_AS_NODE: '1' },
root
)
if (importedPlugin.exitCode !== 0 || importedPlugin.signal) {
throw new Error(`Packaged OpenCode plugin import failed: ${importedPlugin.output.trim()}`)
}
console.log('Packaged OpenCode offline plugin import: ready')
await copyFile(
resolve('build/deepseek-harness-utility-smoke.cjs'),
+39
View File
@@ -0,0 +1,39 @@
const { readdirSync, readFileSync, statSync } = require('node:fs')
const { join, relative } = require('node:path')
const { opencodeVersion } = require('./opencode-config.cjs')
function verifyOpenCodeConfig(resources, source) {
const destination = join(resources, 'runtimes', 'opencode-config')
const manifestPath = join(destination, 'node_modules', '@opencode-ai', 'plugin', 'package.json')
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
if (manifest.name !== '@opencode-ai/plugin' || manifest.version !== opencodeVersion) {
throw new Error('Packaged OpenCode plugin version is invalid')
}
const pending = [source]
let files = 0
while (pending.length > 0) {
const directory = pending.pop()
for (const entry of readdirSync(directory, { withFileTypes: true })) {
const file = join(directory, entry.name)
if (entry.isDirectory()) {
pending.push(file)
} else if (entry.isFile()) {
const target = join(destination, relative(source, file))
if (!statSync(target, { throwIfNoEntry: false })?.isFile() ||
!readFileSync(target).equals(readFileSync(file))) {
throw new Error(`Packaged OpenCode dependency is missing or changed: ${relative(source, file)}`)
}
files++
}
}
}
return files
}
function verifyHarnessBundleImports(source, name) {
if (/(?:\bfrom\s*|\bimport\s*(?:\(\s*)?|\brequire\s*\(\s*)["']@deepseek-ai\//u.test(source)) {
throw new Error(`DeepSeek Harness bundle contains an external DeepSeek dependency: ${name}`)
}
}
module.exports = { verifyOpenCodeConfig, verifyHarnessBundleImports }
+17 -7
View File
@@ -38,9 +38,19 @@ below.
`npm run release:package -- --platform <platform> --arch <arch>`. It only
packages for the native host and writes to
`dist/release/<platform>-<arch>`.
- Default deliverables are NSIS and portable ZIP for Windows, DMG and ZIP for
- Default deliverables are NSIS and portable ZIP for Windows, DMG for
macOS, and AppImage, DEB, and RPM for Linux. Every target includes
`release-manifest.json` with SHA-256 hashes.
- Starting with Desktop 0.12.8, macOS releases contain DMG only. Older macOS
GitHub update readers and all older mirror readers require ZIP and cannot
validate this new matrix; affected users must upgrade manually from the
download page. The user approved this immediate format change. Current
readers accept both the historical DMG/ZIP matrix and the DMG-only matrix.
- Final Desktop package verification compares every bundled OpenCode
configuration file with the prepared dependency tree and rejects external
DeepSeek imports in the unpacked Host and chunks. Windows x64 CI also
imports the packaged OpenCode plugin and starts an isolated packaged
UtilityProcess before publishing; source-only tests do not replace it.
- `build/build-release.cjs` verifies the unpacked application, `app.asar`,
bundled Continue and OpenCode runtimes, executable architecture, and package
signatures before atomically replacing a release directory.
@@ -211,12 +221,12 @@ surface together before a new release.
It must be able to write immutable version objects and the final latest
pointer without granting unrelated administration privileges.
- Upload release assets and `site-release.json` under the immutable
`releases/<tag>/` prefix first. Verify all 14 installer URLs publicly before
`releases/<tag>/` prefix first. Verify all 12 installer URLs publicly before
creating or publishing the GitHub Release. Update
`releases/latest.json` only after the GitHub Release is public and all prior
checks succeeded.
- The expected GitHub Release contains 22 assets: 14 installers (two formats
for each Windows/macOS target and three formats for each Linux target), six
- The expected GitHub Release contains 20 assets: 12 installers (two formats
for each Windows target, DMG for each macOS target, and three formats for each Linux target), six
renamed target manifests, one aggregate `release-manifest.json`, and one `SHA256SUMS`.
`site-release.json` is an OSS publication artifact, not a GitHub Release
asset.
@@ -269,17 +279,17 @@ Do not report a release complete until all of the following are verified:
2. The public GitHub Release is non-draft, non-prerelease, marked Latest, and
uses the expected tag and title. Its body must exactly match the Markdown
generated from the approved packaged bilingual notes.
3. GitHub release metadata contains exactly the expected 22 uploaded asset
3. GitHub release metadata contains exactly the expected 20 uploaded asset
names, including the expected Windows x64 and arm64 assets. Do not download,
hash, launch, or send individual network probes to published installers
unless the user explicitly requests that additional validation.
4. The Beijing `releases/latest.json` returns HTTP 200, has the expected stable
version, exact six targets and 14 installer entries, the trusted Beijing
version, exact six targets and 12 installer entries, the trusted Beijing
URLs, and the GitHub fallback URL. It must match the immutable
`releases/<tag>/site-release.json`. Fetching and comparing these small JSON
metadata files is sufficient; rely on the successful publication job for
its per-asset public checks.
5. The live website successfully fetches the index and produces the 14 correct
5. The live website successfully fetches the index and produces the 12 correct
platform/architecture/format links from that metadata.
6. Both remote `main` refs and both peeled tag refs still equal the approved
release commit, and the local working tree is clean.
@@ -1,5 +1,18 @@
# 工作栏实现与验证进度
## 2026-09-08 Desktop 0.12.8 安装包修复
- 真实 `0.12.7` 安装目录确认 OpenCode 配置缺少整个 `node_modules`。新增测试调用实际
electron-builder 资源复制器,复现相同 ENOENT;把复制源上移到 `.runtime-resources`
后,插件及传递依赖逐文件复制验证通过。
- 真实安装的 DSH bootstrap 存在 `dsh-session-projection` 外部导入,而 Host 解包目录没有
对应依赖。将其加入 Vite bundling,配置回归从失败转为通过;最终包会拒绝同类外部导入。
- Windows x64 CI 已接入包内 OpenCode 插件导入、真实 DSH UtilityProcess 握手及 npm
探针,位于解包目录清理和发布之前。用户要求不做本地打包;CI 探针结果尚待本候选执行,
不能把当前源码回归通过当作真实发布包已通过。
- 本次不改变 Agent `0.11.21` 源码或包。macOS 改为 DMG-only,用户明确接受旧版客户端
手动升级影响;格式与发布规则见[发布手册](../../development/release-runbook.md)。
## 2026-09-08 Execute 目录权限等待修复
- 本机 OpenCode 的内部配置显式允许默认工具权限;每次 Ask 仍设置 deny-all 会话规则并
@@ -141,6 +141,9 @@ Harness 子进程内控制面不能取代 Main 控制面,Main 控制面也不
Harness Session 只在当前 Runtime 进程生命周期内存在。释放 GoodBuddy 会话时必须同步释放对应 Harness Agent。
固定 Host 显式加载 `dsh-session-projection` 的内存 SessionProjectionRegistry,这是
`0.1.2-rc.1` AgentLoop 的必需依赖;不加载 Session persistence,也不新增磁盘会话副本。
该依赖与其余 DSH 依赖一起由 Vite 打入 Host,不能从 `app.asar.unpacked` 外部导入
仅存在于 `app.asar` 的 npm 包。最终打包检查及 Windows CI 的真实 UtilityProcess
启动探针见 [Runtime 资源与打包](../../../BUILD.md#runtime-资源)。
## 6. 总体架构
@@ -12,7 +12,7 @@ Linux x64/arm64、取消和离线 GoodBuddy 传输的真实 Host 验收。
Windows 到 Linux x64 的安装、Agent-owned Prompt、Agent 本地模型 gateway、断线恢复、
同一 OpenCode Session 续接、取消和终态清理已经使用真实模型与工具验证。Agent
`0.11.14` 已通过独立 workflow 发布 Linux x64/arm64 复合包和签名累计目录;当前未发布
Agent 源码 lock 为 `0.11.21`,Desktop 候选为 `0.12.7`,等待各自独立发布审批。
Agent 源码 lock 为已发布的 `0.11.21`,Desktop 候选为 `0.12.8`;本次桌面打包修复不重发 Agent。
现有源码显示本地与远端 OpenCode 原生 Task,并取消 GoodBuddy 对生产 Prompt 的
固定墙钟总时限。失败的 `agent-v0.11.3` 保持不可变且未发布。
+1
View File
@@ -90,6 +90,7 @@ export default defineConfig({
'@deepseek-ai/dsh-sandbox',
'@deepseek-ai/dsh-sandbox-policy',
'@deepseek-ai/dsh-session',
'@deepseek-ai/dsh-session-projection',
'@deepseek-ai/dsh-shell-env',
'@deepseek-ai/dsh-skill',
'@deepseek-ai/dsh-subprocess-local',
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "goodbuddy",
"version": "0.12.7",
"version": "0.12.8",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "goodbuddy",
"version": "0.12.7",
"version": "0.12.8",
"license": "0BSD",
"dependencies": {
"@agentclientprotocol/sdk": "0.25.1",
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "goodbuddy",
"version": "0.12.7",
"version": "0.12.8",
"private": true,
"description": "Secure desktop AI workspace with controlled Agent Runtimes",
"desktopName": "GoodBuddy",
@@ -128,10 +128,10 @@
]
},
{
"from": ".runtime-resources/opencode-config",
"to": "runtimes/opencode-config",
"from": ".runtime-resources",
"to": "runtimes",
"filter": [
"**/*"
"opencode-config{,/**/*}"
]
},
{
+32
View File
@@ -1,6 +1,38 @@
{
"formatVersion": 1,
"releases": [
{
"version": "0.12.8",
"releasedAt": "2026-09-08",
"notes": {
"zh-CN": {
"highlights": [],
"features": [],
"fixes": [
"**修复 OpenCode 启动失败。** 安装包补齐离线配置依赖,避免因缺少 `@opencode-ai/plugin/package.json` 无法启动。升级后无需手动安装 Node.js 或 npm 依赖。",
"**修复 DeepSeek Harness Host 启动退出。** 将必需的会话组件完整打入 Host,避免安装后提示“Host 启动前退出(code 1)”。现有模型配置保持不变。",
"**macOS 仅提供 DMG。** Intel 与 Apple 芯片版本不再构建或发布 ZIP。Windows 和 Linux 的安装包格式保持不变。"
],
"notices": [
"旧版 macOS 的 GitHub 更新检查,以及所有平台的旧版镜像更新检查可能无法识别新清单,请从官方下载页手动升级至 `0.12.8`。",
"Agent 继续使用 `0.11.21`,本次不重发 Agent,也不构建 LoongArch 实验预览。"
]
},
"en-US": {
"highlights": [],
"features": [],
"fixes": [
"**Fixed OpenCode startup failures.** Desktop packages now include the offline configuration dependencies, avoiding startup failures caused by a missing `@opencode-ai/plugin/package.json`. No manual Node.js or npm dependency installation is needed after upgrading.",
"**Fixed DeepSeek Harness Host startup exits.** Required session components are bundled into the Host, preventing the “Host exited before startup (code 1)” error after installation. Existing model settings remain unchanged.",
"**DMG-only macOS releases.** Intel and Apple silicon versions no longer build or publish ZIP packages. Windows and Linux package formats are unchanged."
],
"notices": [
"Older macOS GitHub update checks and older mirror update checks on all platforms may reject the new manifest. Upgrade manually to `0.12.8` from the official download page.",
"Agent remains at `0.11.21`; this release does not republish the Agent or include a LoongArch preview."
]
}
}
},
{
"version": "0.12.7",
"releasedAt": "2026-09-08",
+2 -2
View File
@@ -81,7 +81,7 @@ https://github.com/mesalogo/goodbuddy/releases/latest
校验规则与桌面更新检查保持一致:索引只能指向稳定 SemVer 版本,必须恰好
包含 Windows、macOS、Linux 的 x64 / arm64 六个匹配目标;每个目标必须提供
准确的两种格式和扩展名、正的安全整数大小、64 字符小写十六进制 SHA-256、
准确的格式和扩展名(Windows 两种、macOS 仅 DMG、Linux 三种),正的安全整数大小、64 字符小写十六进制 SHA-256、
全局唯一文件名,以及位于
`https://goodbuddy.oss-cn-beijing.aliyuncs.com/releases/v${version}/`
下、对文件名进行 URL 编码的精确地址。校验清单和 GitHub 回退地址也必须
@@ -90,7 +90,7 @@ https://github.com/mesalogo/goodbuddy/releases/latest
安装包文件名必须与发布生成器完全一致:Windows 使用
`GoodBuddy-${version}-windows-${arch}-setup.exe` 与
`GoodBuddy-${version}-windows-${arch}-portable.zip`;macOS 使用
`GoodBuddy-${version}-mac-${arch}.dmg|zip`;Linux x64 的 AppImage、DEB 与
`GoodBuddy-${version}-mac-${arch}.dmg`;读取器仍接受历史 DMG/ZIP 清单,但当前页面不提供 macOS ZIP 选项。Linux x64 的 AppImage、DEB 与
RPM 分别使用 electron-builder 的 `x86_64`、`amd64` 与 `x86_64` 架构名。
Linux arm64 的 AppImage/DEB 使用 `arm64`,RPM 使用 `aarch64`。
Linux 处理器选择器另外提供龙芯 `loong64` 实验预览;选中后安装包类型固定为
+1 -1
View File
@@ -261,7 +261,7 @@
<path d="M16.8 12.7c0-2.7 2.2-4 2.3-4.1A5 5 0 0 0 15.2 6c-1.7-.2-3.2 1-4.1 1-.9 0-2.2-1-3.6-1-1.8 0-3.5 1.1-4.5 2.7-2 3.5-.5 8.7 1.4 11.5.9 1.4 2 2.8 3.5 2.7 1.4 0 1.9-.9 3.7-.9 1.7 0 2.2.9 3.7.9s2.5-1.4 3.4-2.7a10 10 0 0 0 1.6-3.3 4.6 4.6 0 0 1-3.5-4.2ZM14.1 4.3A4.7 4.7 0 0 0 15.2 1a4.8 4.8 0 0 0-3.1 1.6A4.4 4.4 0 0 0 11 5.8c1.2.1 2.3-.5 3.1-1.5Z" />
</svg>
</div>
<div><h3>macOS</h3><p>Apple silicon / Intel · DMG / ZIP</p></div>
<div><h3>macOS</h3><p>Apple silicon / Intel · DMG</p></div>
<a
class="button button--download"
href="https://github.com/mesalogo/goodbuddy/releases/latest"
+1 -2
View File
@@ -285,7 +285,7 @@
<path d="M16.8 12.7c0-2.7 2.2-4 2.3-4.1A5 5 0 0 0 15.2 6c-1.7-.2-3.2 1-4.1 1-.9 0-2.2-1-3.6-1-1.8 0-3.5 1.1-4.5 2.7-2 3.5-.5 8.7 1.4 11.5.9 1.4 2 2.8 3.5 2.7 1.4 0 1.9-.9 3.7-.9 1.7 0 2.2.9 3.7.9s2.5-1.4 3.4-2.7a10 10 0 0 0 1.6-3.3 4.6 4.6 0 0 1-3.5-4.2ZM14.1 4.3A4.7 4.7 0 0 0 15.2 1a4.8 4.8 0 0 0-3.1 1.6A4.4 4.4 0 0 0 11 5.8c1.2.1 2.3-.5 3.1-1.5Z" />
</svg>
</div>
<div><h3>macOS</h3><p>Apple 芯片 / Intel · DMG / ZIP</p></div>
<div><h3>macOS</h3><p>Apple 芯片 / Intel · DMG</p></div>
<div class="download-options" data-download-card="macos">
<label>
<span>处理器</span>
@@ -298,7 +298,6 @@
<span>类型</span>
<select data-download-format aria-label="macOS 安装包类型">
<option value="dmg">DMG(推荐)</option>
<option value="zip">ZIP</option>
</select>
</label>
</div>
+7 -4
View File
@@ -27,12 +27,12 @@
"macos-x64": Object.freeze({
platform: "macos",
arch: "x64",
formats: Object.freeze(["dmg", "zip"]),
formats: Object.freeze(["dmg"]),
}),
"macos-arm64": Object.freeze({
platform: "macos",
arch: "arm64",
formats: Object.freeze(["dmg", "zip"]),
formats: Object.freeze(["dmg"]),
}),
"linux-x64": Object.freeze({
platform: "linux",
@@ -143,6 +143,9 @@
for (const key of targetKeys) {
const definition = targetDefinitions[key];
const target = index.targets[key];
const formats = definition.platform === "macos" && target?.files?.zip
? ["dmg", "zip"]
: definition.formats;
assert(
hasExactKeys(target, ["platform", "arch", "files"]) &&
target.platform === definition.platform &&
@@ -150,11 +153,11 @@
`发布目标与键不匹配:${key}`,
);
assert(
hasExactKeys(target.files, definition.formats),
hasExactKeys(target.files, formats),
`发布目标文件格式无效:${key}`,
);
for (const format of definition.formats) {
for (const format of formats) {
const file = target.files[format];
assert(
hasExactKeys(file, ["name", "size", "sha256", "url"]),
+8
View File
@@ -128,6 +128,14 @@ test("accepts the canonical stable six-target release index", () => {
assert.equal(validateReleaseIndex(index), index);
});
test("accepts DMG-only releases with twelve installer entries", () => {
const index = validIndex();
delete index.targets["macos-x64"].files.zip;
delete index.targets["macos-arm64"].files.zip;
assert.equal(validateReleaseIndex(index), index);
assert.equal(Object.values(index.targets).flatMap((target) => Object.values(target.files)).length, 12);
});
test("rejects unstable or non-strict versions and extra top-level fields", () => {
for (const version of ["v1.2.3", "01.2.3", "1.2", "1.2.3-rc.1"]) {
expectRejected((index) => {
+1 -1
View File
@@ -538,7 +538,7 @@ const expectedDownloadOptions = {
},
macos: {
arches: ["arm64", "x64"],
formats: ["dmg", "zip"],
formats: ["dmg"],
},
linux: {
arches: ["x64", "arm64", "loong64"],
+40
View File
@@ -99,6 +99,46 @@ function successfulFetch(): ReturnType<typeof vi.fn<typeof fetch>> {
})
}
describe('DMG-only release updates', () => {
it.each([false, true])('accepts macOS GitHub releases (legacy ZIP: %s)', async (legacyZip) => {
const formats = legacyZip ? ['dmg', 'zip'] : ['dmg']
const macFiles = formats.map((format, index) => ({
name: `GoodBuddy-${latestVersion}-mac-arm64.${format}`,
size: 100 + index,
sha256: 'a'.repeat(64)
}))
const manifest = manifestPayload({
targets: [{
platform: 'macos', arch: 'arm64', formats,
manifest: 'release-manifest-macos-arm64.json', files: macFiles
}],
files: macFiles.map((file) => ({ ...file, platform: 'macos', arch: 'arm64' }))
})
const fetch = vi.fn<typeof globalThis.fetch>(async (input) =>
jsonResponse(String(input) === GOODBUDDY_LATEST_RELEASE_API_URL
? releasePayload() : manifest)
)
const result = await checkForUpdates({
currentVersion: '1.0.0', platform: 'darwin', arch: 'arm64', fetch
})
expect(result.target.formats).toEqual(formats)
})
it('accepts a DMG-only mirror for Windows and macOS clients', async () => {
const index = mirrorIndexPayload()
delete index.targets['macos-x64']!.files.zip
delete index.targets['macos-arm64']!.files.zip
for (const platform of ['win32', 'darwin'] as const) {
const result = await checkMirrorForUpdates({
currentVersion: '1.0.0', platform, arch: 'x64',
fetch: vi.fn(async () => jsonResponse(index))
})
expect(result.updateAvailable).toBe(true)
if (platform === 'darwin') expect(result.target.formats).toEqual(['dmg'])
}
})
})
type MirrorTestFile = {
name: string
size: number
+11 -5
View File
@@ -336,10 +336,16 @@ function isCanonicalReleaseAssetApiUrl(value: string): boolean {
const expectedFormats: Record<ReleasePlatform, string[]> = {
windows: ['nsis', 'portable'],
macos: ['dmg', 'zip'],
macos: ['dmg'],
linux: ['AppImage', 'deb', 'rpm']
}
function formatsForTarget(platform: ReleasePlatform, includesZip: boolean): string[] {
return platform === 'macos' && includesZip
? ['dmg', 'zip']
: expectedFormats[platform]
}
function hasExpectedFileFormats(
platform: ReleasePlatform,
files: ReleaseFile[]
@@ -352,7 +358,7 @@ function hasExpectedFileFormats(
}
const extensions =
platform === 'macos'
? ['.dmg', '.zip']
? files.length === 2 ? ['.dmg', '.zip'] : ['.dmg']
: ['.AppImage', '.deb', '.rpm']
return extensions.every(
(extension) =>
@@ -418,7 +424,7 @@ function validateMirrorIndex(
if (!target || `${target.platform}-${target.arch}` !== key) {
throw new Error(`Mirror release target is invalid: ${key}`)
}
const formats = expectedFormats[target.platform]
const formats = formatsForTarget(target.platform, Boolean(target.files.zip))
if (
Object.keys(target.files).length !== formats.length ||
formats.some((format) => !target.files[format])
@@ -454,7 +460,7 @@ function validateMirrorIndex(
if (!target) {
throw new Error(`Mirror release target is missing: ${platform}/${arch}`)
}
const formats = expectedFormats[platform]
const formats = formatsForTarget(platform, Boolean(target.files.zip))
return {
platform,
arch,
@@ -487,7 +493,7 @@ function validateCurrentTarget(
if (!target) {
throw new Error('Release manifest target is missing')
}
const formats = expectedFormats[platform]
const formats = formatsForTarget(platform, target.formats.includes('zip'))
if (
target.formats.length !== formats.length ||
!formats.every((format, index) => target.formats[index] === format) ||
+2 -2
View File
@@ -156,7 +156,7 @@ describe('release asset aggregation', () => {
])
const outputNames = readdirSync(output)
expect(outputNames).toHaveLength(22)
expect(outputNames).toHaveLength(20)
expect(outputNames).toContain('release-manifest.json')
expect(outputNames).toContain('SHA256SUMS')
expect(outputNames).not.toContain('builder-debug.yml')
@@ -180,7 +180,7 @@ describe('release asset aggregation', () => {
join(output, 'SHA256SUMS'),
'utf8'
)
expect(sums.trim().split('\n')).toHaveLength(21)
expect(sums.trim().split('\n')).toHaveLength(19)
expect(sums).toContain(
'release-manifest-windows-x64.json'
)
+5 -1
View File
@@ -235,6 +235,10 @@ function machO(cpuType: number): Buffer {
function portableDirectory(parent: string): string {
const directory = join(parent, 'portable')
const plugin = join(directory, 'resources', 'runtimes', 'opencode-config', 'node_modules', '@opencode-ai', 'plugin')
mkdirSync(join(plugin, 'dist'), { recursive: true })
writeFileSync(join(plugin, 'package.json'), '{"name":"@opencode-ai/plugin","version":"1.18.29"}')
writeFileSync(join(plugin, 'dist', 'index.js'), 'export const tool = true')
mkdirSync(
join(directory, 'resources', 'runtimes', 'opencode'),
{ recursive: true }
@@ -319,7 +323,7 @@ function endOfCentralDirectory(
describe('release build arguments', () => {
it.each([
['win32', 'x64', 'windows', ['nsis', 'portable']],
['darwin', 'arm64', 'macos', ['dmg', 'zip']],
['darwin', 'arm64', 'macos', ['dmg']],
['linux', 'x64', 'linux', ['AppImage', 'deb', 'rpm']]
])(
'uses %s host defaults',
+92
View File
@@ -0,0 +1,92 @@
// @vitest-environment node
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { createRequire } from 'node:module'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import { describe, expect, it } from 'vitest'
import type { Plugin, UserConfig } from 'vite'
import config from '../electron.vite.config'
const require = createRequire(import.meta.url)
const packageJson = require('../package.json')
const { getFileMatchers, copyFiles } = require('app-builder-lib/out/fileMatcher')
const { verifyOpenCodeConfig, verifyHarnessBundleImports } = require('../build/verify-desktop-runtimes.cjs')
describe('desktop Runtime packaging', () => {
it('runs packaged Windows Runtime checks in CI before removing the unpacked output', () => {
const release = readFileSync(join(process.cwd(), 'build', 'build-release.cjs'), 'utf8')
const smoke = readFileSync(join(process.cwd(), 'build', 'run-packaged-deepseek-harness-smoke.cjs'), 'utf8')
expect(release).toContain("process.env.GITHUB_ACTIONS === 'true'")
expect(release.indexOf('run-packaged-deepseek-harness-smoke.cjs'))
.toBeLessThan(release.indexOf('rmSync(unpackedDirectory'))
expect(smoke).toContain('Packaged OpenCode offline plugin import: ready')
expect(smoke).toContain("result.status !== 'ready'")
})
it('copies offline OpenCode dependencies through the real electron-builder resource copier', async () => {
const root = mkdtempSync(join(tmpdir(), 'goodbuddy-desktop-runtime-copy-'))
const output = join(root, 'output', 'resources')
const source = join(root, '.runtime-resources', 'opencode-config')
const files = {
'package.json': '{"private":true}',
'.gitignore': 'node_modules\n',
'node_modules/@opencode-ai/plugin/package.json': '{"name":"@opencode-ai/plugin","version":"1.18.29"}',
'node_modules/@opencode-ai/plugin/dist/index.js': 'export const tool = true\n',
'node_modules/zod/package.json': '{"name":"zod","version":"4.1.8"}',
'node_modules/zod/index.js': 'export const z = {}\n'
}
try {
for (const [name, content] of Object.entries(files)) {
const file = join(source, name)
mkdirSync(dirname(file), { recursive: true })
writeFileSync(file, content)
}
const resource = packageJson.build.extraResources.find(
(entry: { from: string }) => entry.from.includes('.runtime-resources') &&
JSON.stringify(entry).includes('opencode-config')
)
expect(resource).toBeDefined()
const matchers = getFileMatchers(
{ extraResources: [resource] }, 'extraResources', output,
{ defaultSrc: root, globalOutDir: join(root, 'output'), customBuildOptions: {}, macroExpander: (value: string) => value }
)
await copyFiles(matchers)
for (const [name, content] of Object.entries(files)) {
expect(readFileSync(join(output, 'runtimes', 'opencode-config', name), 'utf8')).toBe(content)
}
expect(verifyOpenCodeConfig(output, source)).toBe(Object.keys(files).length)
rmSync(join(output, 'runtimes', 'opencode-config', 'node_modules', 'zod', 'index.js'))
expect(() => verifyOpenCodeConfig(output, source)).toThrow('dependency is missing or changed')
} finally {
rmSync(root, { recursive: true, force: true })
}
})
it('bundles every direct DeepSeek dependency into the unpacked Host instead of leaving ASAR-only imports', () => {
const plugin = (config.main?.plugins as Plugin[]).find(
(entry) => entry.name === 'vite:externalize-deps'
)
expect(plugin).toBeDefined()
const buildConfig: UserConfig = {}
const hook = plugin!.config
if (typeof hook !== 'function') throw new Error('Expected externalization config hook')
Reflect.apply(hook, {}, [buildConfig, { command: 'build', mode: 'production' }])
const external = buildConfig.build?.rollupOptions?.external as Array<string | RegExp>
for (const dependency of Object.keys(packageJson.dependencies).filter(
(name) => name.startsWith('@deepseek-ai/')
)) {
expect(external.some((entry) => typeof entry === 'string'
? entry === dependency
: entry.test(dependency)), dependency).toBe(false)
}
})
it.each([
'import Registry from "@deepseek-ai/dsh-session-projection";',
'export { Registry } from "@deepseek-ai/dsh-session-projection";',
'await import("@deepseek-ai/dsh-session-projection");'
])('rejects an external DeepSeek dependency in a shipped bootstrap or chunk', (source) => {
expect(() => verifyHarnessBundleImports(source, 'host.js')).toThrow('external DeepSeek dependency')
expect(() => verifyHarnessBundleImports('import { x } from "./chunks/host.js";', 'host.js')).not.toThrow()
})
})
+12
View File
@@ -50,6 +50,18 @@ function jobNeeds(name: string): string[] {
}
describe('desktop packages workflow', () => {
it('builds only DMG for both signed and unsigned macOS jobs', () => {
const steps = parsed.jobs.package?.steps?.filter(
(step) => step.name?.endsWith('macOS release packages') &&
!step.name.includes('non-macOS')
)
expect(steps).toHaveLength(2)
for (const step of steps ?? []) {
expect(step.run).toContain('--format dmg')
expect(step.run).not.toContain('--format dmg,zip')
}
})
it('uses valid YAML, pinned action majors, and only desktop release jobs', () => {
expect(() => parse(workflow)).not.toThrow()
expect(Object.keys(parsed.jobs)).toEqual([
+4 -4
View File
@@ -46,8 +46,8 @@ const verifier = require(
const targetDefinitions = [
['windows', 'x64', ['nsis', 'portable']],
['windows', 'arm64', ['nsis', 'portable']],
['macos', 'x64', ['dmg', 'zip']],
['macos', 'arm64', ['dmg', 'zip']],
['macos', 'x64', ['dmg']],
['macos', 'arm64', ['dmg']],
['linux', 'x64', ['AppImage', 'deb', 'rpm']],
['linux', 'arm64', ['AppImage', 'deb', 'rpm']]
] as const
@@ -145,7 +145,7 @@ describe('site release manifest', () => {
await expect(
verifier.verifySiteRelease(manifest, request)
).resolves.toBe(14)
expect(request).toHaveBeenCalledTimes(14)
).resolves.toBe(12)
expect(request).toHaveBeenCalledTimes(12)
})
})