diff --git a/.github/workflows/packages.yml b/.github/workflows/packages.yml index 0499b32a..0d9f48e3 100644 --- a/.github/workflows/packages.yml +++ b/.github/workflows/packages.yml @@ -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 diff --git a/BUILD.md b/BUILD.md index 26a6454a..298a7556 100644 --- a/BUILD.md +++ b/BUILD.md @@ -219,6 +219,7 @@ package format v1 归档已经包含固定 Agent 和 Node,可直接用于 Host 桌面包会携带经过版本与完整性校验的 OpenCode、Continue 和 DSH 插件安装 Runtime: - OpenCode 平台二进制来自 `.runtime-resources/`。 +- 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 --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 编码后的内容。 diff --git a/FEATURES.md b/FEATURES.md index 17a4c6dc..c7f4a34e 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -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 diff --git a/FEATURES.zh-CN.md b/FEATURES.zh-CN.md index 38269481..d1f375e0 100644 --- a/FEATURES.zh-CN.md +++ b/FEATURES.zh-CN.md @@ -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 更新检查及所有旧版镜像更新检查需要从下载页手动升级。 ### 开放接口、团队协作与远程执行 diff --git a/build/aggregate-release.cjs b/build/aggregate-release.cjs index a3e95943..7e3ab747 100644 --- a/build/aggregate-release.cjs +++ b/build/aggregate-release.cjs @@ -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'] } ] diff --git a/build/build-release.cjs b/build/build-release.cjs index 2a88cc87..efaef320 100644 --- a/build/build-release.cjs +++ b/build/build-release.cjs @@ -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') diff --git a/build/run-packaged-deepseek-harness-smoke.cjs b/build/run-packaged-deepseek-harness-smoke.cjs index e4e808eb..4918bd7d 100644 --- a/build/run-packaged-deepseek-harness-smoke.cjs +++ b/build/run-packaged-deepseek-harness-smoke.cjs @@ -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'), diff --git a/build/verify-desktop-runtimes.cjs b/build/verify-desktop-runtimes.cjs new file mode 100644 index 00000000..71361336 --- /dev/null +++ b/build/verify-desktop-runtimes.cjs @@ -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 } diff --git a/docs/development/release-runbook.md b/docs/development/release-runbook.md index 2e261db1..8f43ea66 100644 --- a/docs/development/release-runbook.md +++ b/docs/development/release-runbook.md @@ -38,9 +38,19 @@ below. `npm run release:package -- --platform --arch `. It only packages for the native host and writes to `dist/release/-`. -- 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//` prefix first. Verify all 14 installer URLs publicly before + `releases//` 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//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. diff --git a/docs/features/assistant-workbar/progress.md b/docs/features/assistant-workbar/progress.md index 4293d693..1e9e25c9 100644 --- a/docs/features/assistant-workbar/progress.md +++ b/docs/features/assistant-workbar/progress.md @@ -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 会话规则并 diff --git a/docs/features/deepseek-harness/technical-design.md b/docs/features/deepseek-harness/technical-design.md index deb7a7da..db7bf77d 100644 --- a/docs/features/deepseek-harness/technical-design.md +++ b/docs/features/deepseek-harness/technical-design.md @@ -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. 总体架构 diff --git a/docs/features/remote-host/technical-design.md b/docs/features/remote-host/technical-design.md index 45e209d6..bd007c85 100644 --- a/docs/features/remote-host/technical-design.md +++ b/docs/features/remote-host/technical-design.md @@ -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` 保持不可变且未发布。 diff --git a/electron.vite.config.ts b/electron.vite.config.ts index e0ceb5d4..f1671cdd 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -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', diff --git a/package-lock.json b/package-lock.json index 19aa6f9a..d0fa83f6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index a4814704..7df38048 100644 --- a/package.json +++ b/package.json @@ -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{,/**/*}" ] }, { diff --git a/resources/release-notes.json b/resources/release-notes.json index 7978bd45..6e888779 100644 --- a/resources/release-notes.json +++ b/resources/release-notes.json @@ -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", diff --git a/sites/README.md b/sites/README.md index 873957f4..cd854e91 100644 --- a/sites/README.md +++ b/sites/README.md @@ -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` 实验预览;选中后安装包类型固定为 diff --git a/sites/en.html b/sites/en.html index bff02182..d7159e4e 100644 --- a/sites/en.html +++ b/sites/en.html @@ -261,7 +261,7 @@ -

macOS

Apple silicon / Intel · DMG / ZIP

+

macOS

Apple silicon / Intel · DMG

-

macOS

Apple 芯片 / Intel · DMG / ZIP

+

macOS

Apple 芯片 / Intel · DMG

diff --git a/sites/release-index.js b/sites/release-index.js index ab34f5ee..f9b1acbc 100644 --- a/sites/release-index.js +++ b/sites/release-index.js @@ -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"]), diff --git a/sites/scripts/release-index.test.mjs b/sites/scripts/release-index.test.mjs index c906395b..21e1dbd1 100644 --- a/sites/scripts/release-index.test.mjs +++ b/sites/scripts/release-index.test.mjs @@ -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) => { diff --git a/sites/scripts/validate.mjs b/sites/scripts/validate.mjs index 2c35848b..bcb3084f 100644 --- a/sites/scripts/validate.mjs +++ b/sites/scripts/validate.mjs @@ -538,7 +538,7 @@ const expectedDownloadOptions = { }, macos: { arches: ["arm64", "x64"], - formats: ["dmg", "zip"], + formats: ["dmg"], }, linux: { arches: ["x64", "arm64", "loong64"], diff --git a/src/main/version-checker.test.ts b/src/main/version-checker.test.ts index e373241f..30044187 100644 --- a/src/main/version-checker.test.ts +++ b/src/main/version-checker.test.ts @@ -99,6 +99,46 @@ function successfulFetch(): ReturnType> { }) } +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(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 diff --git a/src/main/version-checker.ts b/src/main/version-checker.ts index 726bbffa..2da7901d 100644 --- a/src/main/version-checker.ts +++ b/src/main/version-checker.ts @@ -336,10 +336,16 @@ function isCanonicalReleaseAssetApiUrl(value: string): boolean { const expectedFormats: Record = { 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) || diff --git a/tests/aggregate-release.test.ts b/tests/aggregate-release.test.ts index d784e24a..97eb8019 100644 --- a/tests/aggregate-release.test.ts +++ b/tests/aggregate-release.test.ts @@ -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' ) diff --git a/tests/build-release.test.ts b/tests/build-release.test.ts index ca7e1967..c690e85e 100644 --- a/tests/build-release.test.ts +++ b/tests/build-release.test.ts @@ -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', diff --git a/tests/desktop-runtime-packaging.test.ts b/tests/desktop-runtime-packaging.test.ts new file mode 100644 index 00000000..77759c39 --- /dev/null +++ b/tests/desktop-runtime-packaging.test.ts @@ -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 + 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() + }) +}) diff --git a/tests/packages-workflow.test.ts b/tests/packages-workflow.test.ts index 1b8a9260..8f7e7b37 100644 --- a/tests/packages-workflow.test.ts +++ b/tests/packages-workflow.test.ts @@ -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([ diff --git a/tests/site-release.test.ts b/tests/site-release.test.ts index dcea02f4..eb131481 100644 --- a/tests/site-release.test.ts +++ b/tests/site-release.test.ts @@ -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) }) })