Files
goodbuddy/BUILD.md
T
mesalogo 55efca3ee1 feat: connect local Obsidian vaults to conversations
Users can now read, search, and update existing local Obsidian notes through
built-in tools without configuring an external MCP server or Obsidian plugin.
Add vault discovery, an optional folder scope, and independent test, save,
enablement, and runtime assignment controls in MCP settings.

Bundle MCPVault 0.16.0 and its runtime dependencies, exposing 19 shared tools
to direct models and managed local OpenCode and Continue conversations.
Obsidian is disabled by default. Ask remains read-only; Execute permits
writes. Scope and assignment changes revoke existing Obsidian grants.
DeepSeek Harness and remote execution remain unsupported.

Validate the isolated staged snapshot with its own dependency installation:
typecheck, lint, production build, and 540 tests across 14 files passed.
No paid model calls were repeated. Physical-offline, installer, and other
platform acceptance remain pending as documented in the feature record.

Release note: Connect local Obsidian vaults in MCP settings to find notes and
write conversation results back to them. Test all registered vaults or a
chosen folder before enabling access; no Obsidian plugin is required.
2026-09-27 00:37:46 +08:00

35 KiB
Raw Blame History

GoodBuddy 开发与构建

环境要求

  • Node.js 24,或当前锁定依赖明确支持的 Node.js 版本
  • npm
  • Windows、Linux 或 macOS

安装锁定依赖:

npm ci

不要将 API Key、访问令牌、私有模型地址或本地数据库提交到仓库。

本地开发

启动开发环境:

npm run dev

预览已经生成的生产构建:

npm run build
npm start

质量验证

跨功能测试方法见质量验证索引。桌面界面的测试分层、 原生键盘、截图与计算样式取证、隔离启动和脱敏报告遵循 Electron UI 自动化测试规范。

提交或打包前运行:

npm test
npm run typecheck
npm run lint

Vitest runs two ordered projects from vitest.config.ts. The unit project runs first with up to four workers, bounded by Node's available CPU parallelism. Once it finishes, the integration project runs with one worker. The serial group contains all test files under tests/ (including *.electron.test.ts), src/main/agent/**/*runtime*.test.ts, and src/agent-daemon/private-endpoint.test.ts. Other tests remain in the unit group. Both projects inherit the React plugin, jsdom environment, renderer setup, and existing exclusions; test-file isolation remains enabled.

This preserves serialization for the process/socket tests that previously timed out on Linux with two workers (commit 93721d6). sequence.groupOrder keeps the two phases from overlapping. Existing file filters still work, for example npm test -- src/main/agent/opencode-runtime.test.ts. To select a whole phase, use npm test -- --project unit or npm test -- --project integration. To inspect file selection without running tests, use npx vitest list --filesOnly.

For activity-history persistence, run npx vitest run src/main/assistant/activity-history-io.test.ts. This checks both WAL growth and repeated full-history reads: consecutive saves compare against the last committed in-memory records, invalidated on close, data clearing or another SQLite connection's commit. It also checks rollback and retry. The cache does not change the database schema or the full-snapshot IPC contract. WAL size and SQL scan counts do not measure physical disk throughput or conversation message persistence; validate those separately when investigating live I/O.

监听模式:

npm run test:watch

真实 Runtime 端到端测试默认关闭。配置兼容模型凭据后,显式启用:

GOODBUDDY_RUN_RUNTIME_E2E=1 npm test -- src/main/agent/runtime-e2e.manual.test.ts

OpenCode/Continue 用例默认读取 dist/harness-package-probe/win-unpacked;也可用 GOODBUDDY_E2E_PACKAGED_ROOT 指定其他已解包应用目录。该测试可能发起真实外部模型 调用。测试不会输出 API Key,文件操作在临时工作区中执行。文件包含经 Main 回环 broker 调用已分配自定义 MCP 的真实 OpenCode 和 Continue 用例。

不使用外部模型的 Runtime 原生交互回归位于 src/main/agent/opencode-runtime-permissions.test.ts、 src/main/agent/continue-runtime-permissions.test.ts 和 src/main/agent/deepseek-harness-acp-e2e.test.ts。它们用本地确定性模型驱动真实 Runtime 或固定 Host,覆盖工作区外访问、原生问答及 Ask 边界。OpenCode 用例默认使用开发缓存 二进制;可通过 GOODBUDDY_TEST_OPENCODE_BINARY 指定已经安装的候选二进制,避免把 仍在运行的旧缓存版本当作候选版本验证。这不替代当前源码 Agent 的 Linux Host 验收。

生产构建

生成 Electron Main、Preload 和 Renderer 生产文件:

npm run build

中间构建输出位于 out。该目录为生成内容,应修改源文件后重新构建,不要直接编辑。

Obsidian 接入的构建钩子将锁定的 npm @bitbonsai/mcpvault@0.16.0 及递归运行依赖、 许可证和原生辅助资源复制到 out/main/obsidian-mcpvault,桌面打包时解包到 app.asar.unpacked。运行时复用现有工具执行环境的 Node,不执行在线 npm 安装。 生产组件自动化、Electron 托管 Node、Windows 设置与聊天 UI,以及受限网络下的 Windows x64 解包应用已验证;物理断网、安装器及其他系统验收待完成。 验证记录见 Obsidian 进度。

图标生成

应用图标源文件位于 icons。修改源图或图标处理逻辑后运行:

npm run icons

脚本会精确裁出亮色和深色圆角卡片,清理圆角外侧背景并使用适合主题的 边缘颜色生成透明抗锯齿,避免缩放后出现白边。它会统一更新 build 中的 PNG / ICO、Renderer 图标,以及官网使用的亮色和深色品牌图标。任务栏和 托盘图标保持透明背景。

平台打包

当前平台默认包

npm run dist

Windows

生成 Windows NSIS 安装包:

npm run dist:win

生成用于本机调试的 Windows 便携目录:

npm run portable

macOS

生成 x64 和 arm64 DMG:

npm run dist:mac

Linux

同时生成 Linux x64 和 arm64 包:

npm run dist:linux

只生成指定架构:

npm run dist:linux:x64
npm run dist:linux:arm64

每个 Linux 架构生成:

  • deb:适用于麒麟、统信 UOS 等 Debian 系桌面。
  • rpm:适用于 Fedora、RHEL、Rocky Linux、openEuler 等 RPM 系桌面。
  • AppImage:适用于免安装验证和便携运行。

打包产物位于 dist。

所有桌面打包入口通过同一个 electron-builder 启动脚本运行。正常日志会隐藏 duplicate dependency references 信息摘要,因为它只表示同一已解析依赖被多条 生产依赖路径共享,并不要求构建操作者处理;调试日志仍会保留该摘要。Renderer 中 已由 Vite 打包的 @antv/g6 归类为构建期依赖,不会再作为独立 Node 模块复制进桌面 安装包。

以上 dist* 与 portable 命令用于普通本地桌面打包。它们会携带 agent-runtime-lock.json、remote-runtime-lock.json 与公开的 agent-release-keys.json,但所有桌面构建路径(包括本地 portable)都不会嵌入 可安装的远端 Agent 或远端 Runtime payload。没有使用托管 SSH 时,无需下载任何额外 组件,缺少远端包也不能阻塞普通桌面打包或发布。

“设置 > 平台功能 > 远程项目”列出用户数据目录中已经下载或导入的 Linux x64、Linux arm64 和 macOS arm64 复合 Agent 包。首次打开和手动刷新会从“关于与更新”所选来源读取 小型签名目录,显示本地版本、在线最新兼容版本以及 有更新 / 已是最新 / 未下载 / 校验失败;此检查不会下载 .gbagent。只有用户点击“下载”或“更新到”后才传输包, 用户也可导入或导出 .gbagent 离线包。 Main 在写入缓存前校验签名目录、目录签名、大小、SHA-256、桌面最低版本、Agent 协议、 内部 Agent/Runtime 签名和完整 payload。缓存位于 Electron userData 下的 remote-components/agent-packages,不暴露给 Renderer。

公钥 registry 的信任来自严格 schema、唯一 key ID、规范 Base64、有效 Ed25519 key、 production/test 环境和撤销列表,不来自 JSON 空白或换行。读取桌面资源、复合包和 Host 已有 registry 时接受等价 UTF-8 JSON 格式;向 Host 写入前统一重新序列化为确定性 LF JSON。签名目录、包描述符、manifest、签名和 payload 的原始字节校验保持不变,包下载 继续流式计算 SHA-256。

“SSH 主机 > 远程运行环境”只有一个按版本事实显示“安装远程环境”“更新远程环境”或 “重新安装”的主按钮。次级 SegmentedControl 选择默认且不持久化的“自动”、“Host 下载” 或“GoodBuddy 传输”:

  • “Host 下载”使用签名目录中的固定 GitHub/北京镜像 URL、大小、SHA-256、 有界重定向链和固定 SSH prepare channel。GoodBuddy 桌面控制面通过该通道发送有界 one-shot installer;Host 校验目录绑定的归档大小和 SHA-256 后,由包内固定 Node 解码 并运行 installer。
  • 显式“GoodBuddy 传输”在在线目录不可用,或本地已验证包版本不低于在线候选时优先使用 本地包,包括版本更高的离线导入;线上存在更新且可用时仍取得线上新包。需要下载时, 同一次用户操作会校验 SHA-256 与签名、缓存并取得 lease,再以有界流式 SFTP 上传一个 compound archive 和归档内已验证的 bootstrap Node;约 294 MiB 的整包不会一次读入 Main Buffer,Host 仍再次校验。
  • “自动”只在 operation/prepare 前执行 Host capability probe;直连明确可用才选择 Host 下载,否则选择 GoodBuddy 传输。显式选择保持有效,prepare、commit 或 adoption 失败后不跨 acquisition 自动 fallback。

两种 acquisition 都把同一签名 compound .gbagent 交付到固定 operation staging,随后 共用 control-plane prepare → commit → Agent activate/health → Runtime activate → finalize → cleanup。Main 只持久化清理 operation staging 所需的 operation ID,不保存 Agent/Runtime metadata 副本。操作中断或 adoption 失败后,下一次更新尽力清理旧暂存并 重新 prepare,不恢复或续写旧操作。确认 adoption 后显式 cleanup。Host 在 prepare 解包时完成一次完整 payload 校验;commit 只复核签名 metadata 并原子发布,Agent/Runtime 注册后的 health、capabilities 和 prompt 启动不再遍历或哈希 完整 payload。cleanup 失败不回滚已健康环境,也不阻塞下一次 fresh prepare。

进入“主机与远程执行”设置页只读取本地 Host 列表,不逐台连接。用户点击某台 Host 的 “刷新版本”后才执行远端版本和直连准备能力探测。项目切换只更新本地选择,不建立 SSH 连接;首次实际使用远程 Workspace 或 Runtime 时才建立连接。当前进程会复用该 Host 已确认 的 Agent/Runtime identity 和 Agent 连接,Host 编辑或环境更新会定向清空它们。 直连安装本身只依赖 SSH,不依赖已有 Agent。显式选择 Host 下载时由实际安装流程重新探测 并报告工具、权限、空间或来源不可达等原因;本次操作不会自动改用 GoodBuddy 传输。 新建远程项目弹窗也只读本地 Host 验证记录;目录浏览或保存才连接所选 Host。Host 卡片 在版本相同时仍允许显式重装;损坏的 GoodBuddy-owned 同 digest 目录会先隔离、替换并在 失败时恢复。直连 bootstrap 使用固定短命令 exec sh -s,脚本和有界输入经 stdin 发送, 避免 SSH command 长度限制。

新增 Host 只保存并探测,不提供自动安装开关;只有用户明确点击主按钮后才传输完整包。现有 package format v1 归档已经包含固定 Agent 和 Node,可直接用于 Host 直连;归档无需携带 agent/lib/package-installer.cjs,签名目录不提供也不检查额外的 bootstrap 能力元数据。 首次 bootstrap 不依赖已安装 Agent daemon。目标契约见 SSH Host 远程环境准备与直连下载设计。

删除 Host 或远程项目属于纯本地管理操作。Host 确认框列出全部关联项目记录并级联清理 本地数据库,但不连接 Host、不检查远端目录,也不删除远端文件;Host 设置文件和项目事务 的普通失败会回滚已写入的一侧。

Runtime 资源

桌面包会携带经过版本与完整性校验的 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。
  • DSH 插件安装使用精确锁定并从 app.asar 解包的 npm CLI,通过当前 Electron 的 Node 模式运行;最终用户不需要另装 Node.js 或 npm。
  • DSH 图片输入使用精确锁定的 @napi-rs/canvas 完整解码 JPEG/PNG。通用包与目标平台、目标架构的 Skia 原生包必须从 app.asar 解包;当打包 Runner 的架构与目标架构不同时,发布脚本会根据 lockfile 的精确版本、下载地址和 integrity 临时暂存目标原生包,完成后清理。发布校验会检查版本、目标架构和 MIT 许可证。
  • 打包钩子位于 build/runtime-hooks.cjs。

跨架构打包前,确认目标架构的 OpenCode 资源已经准备完成。不要用其他架构的二进制替代目标资源。

GoodBuddy 管理的本机 OpenCode 启动后会轮询已认证的 loopback health 接口,最多等待 30 秒。该窗口用于兼容 Linux ARM 等较慢环境;不要改回依赖 stdout 文案或 10 秒固定上限。 聊天中的 Runtime 切换只执行路径和模型凭据就绪检查,不为状态显示启动临时 Server。 设置中的显式连接测试、原生能力清单读取和首次实际请求仍会启动 OpenCode,并继续使用 上述完整健康检查。

GoodBuddy Agent 工件

面向用户发布的是按远端 Host 架构区分的复合 .gbagent,每个包同时包含签名 Agent、固定 Node 和当前桌面源码维护并适配的签名 OpenCode Runtime。Runtime 不建立 独立的用户版本流。远程直连所需的有界 one-shot installer 由桌面控制面打包并通过固定 SSH prepare channel 发送,不是 .gbagent 的必需条目。Host 按签名目录校验完整归档 大小和 SHA-256 后,包内固定 Node 解码并运行 installer。两种 acquisition 都在固定 operation staging 中执行同一 prepare/commit;本地 pending record 只保存清理暂存所需的 operation ID。失败或中断后的下一次更新尽力清理旧暂存并重新 prepare,不保存或恢复 Agent/Runtime metadata 副本。生产构建命令为:

node build/agent-package.cjs build \
  --arch <x64|arm64> \
  --minimum-desktop-version <desktop-version> \
  --node-archive <locked-node-runtime.tar.gz> \
  --runtime-archive <locked-opencode-package.tgz> \
  --output goodbuddy-agent-<agent-version>-linux-<arch>.gbagent

底层 agent:build/import/verify 与 remote-runtime:build/import/verify 命令继续用于 开发、签名和复合包组装验证;其 .agent-resources 与 .remote-runtime-resources 输出都是不提交的生成内容,也不会被任何桌面打包命令发现 或嵌入。

Agent 原生 CI 策略

Agent 源码、共享协议、lock、bundle 工具和测试与桌面应用保持在同一仓库、同一 commit; 不维护长期分叉的 Agent 源码分支。.github/workflows/agents.yml 在相关 Pull Request、 main push 和手工触发时,分别使用 ubuntu-24.04 与 ubuntu-24.04-arm 原生构建 Linux x64/arm64 复合包,并使用 macos-15 原生构建 Darwin arm64 复合包。所有 job 从 agent-runtime-lock.json 解析官方 Node HTTPS 地址与 digest,并从 remote-runtime-lock.json 解析固定 OpenCode npm 包与 integrity; 下载后再次校验,随后使用仅存在于进程内的临时 Ed25519 测试 key 构建两次,完成内外层 测试签名验证、Agent CLI 加载 smoke 和确定性 .gbagent 对比。所有目标还验证 Koffi 原生模块加载与 OpenCode --version;Linux job 继续运行 Agent 原生回归。

Darwin arm64 的 Node 和 OpenCode 固定输入分别由同一份 agent-runtime-lock.json 与 remote-runtime-lock.json 提供,不再维护 CI 专用副本。 组包器区分 ELF 与 Mach-O,Darwin CI 产物命名为 goodbuddy-agent-<version>-darwin-arm64.gbagent,不构建 Intel Mac 目标。 macOS Host 继续通过 SSH 按需启动 detached Agent,不注册 launchd 或开机服务。 进程身份使用 macOS 原生接口,Unix Socket 使用 getpeereid;Host 下载兼容系统自带 shasum -a 256,不要求安装 GNU coreutils。真实 Host 验证记录见 远程主机技术设计。

三个目标共用 agent-catalog.json 和现有签名格式,条目按平台与架构区分。旧版 Desktop 只接受 Linux catalog 条目;首次发布包含 Darwin 的累计 catalog 前必须先发布新版 Desktop,并在发布说明中要求用户升级 Desktop。旧版的在线 Agent 目录检查会失败, 本地已验证包不被删除。不得为此改写历史包或复用发布标签。

该验证 workflow 不读取 production signing secret,不修改公开 key registry,也不上传 可安装 Agent 工件。.github/workflows/agent-release.yml 是唯一 production Agent 发布路径:只接受指向受保护 main 历史的 annotated agent-v${agent-runtime-lock.agentVersion} 标签,在 agent-signing Environment 中 原生构建 Linux x64、Linux arm64、macOS arm64 三个目标,生成并签名累计目录,然后同步到非 Latest 的 GitHub Agent Release 与北京 OSS。该 Environment 只保护一组 GoodBuddy 通用发布变量与 Secret:

  • GOODBUDDY_SIGNING_KEY_ID / GOODBUDDY_SIGNING_PRIVATE_KEY

该 GoodBuddy 身份统一签署内部 Agent、固定 Runtime manifest、外层 .gbagent 描述符和累计目录,各层通过既有签名域区分用途,不再要求内部 Runtime 单独配置生产 身份。私钥只注入签名步骤;预检只读取 key ID 并确认公钥已在 resources/agent-release-keys.json 注册为 production 且未撤销。

累计目录使用 agent-catalog.json 与 agent-catalog.sig。每个条目绑定 Agent 版本、 最低桌面版本、Agent 协议、远端 OpenCode 版本/digest、架构、文件名、大小、SHA-256 以及固定 GitHub/OSS URL。同一版本和架构的字节不可改变;目录最多保留 200 个条目。 OSS 先写 agent-releases/v<version>/ 不可变对象,GitHub Release 公开且验证后才更新 单一 agent-releases/latest.json 指针。该指针只引用同一不可变版本目录中的 agent-catalog.json 与 agent-catalog.sig,避免客户端观察到跨版本组合。Agent Release 必须使用 --latest=false,不得改变桌面 Release 的 Latest 标记。

远程 Runtime 工件

remote-runtime-lock.json 独立锁定首个远程 Runtime:OpenCode 1.18.29 的 Linux x64 baseline 与 arm64 官方包 integrity、bin/opencode 入口和固定 acp 参数。它不同于 agent-runtime-lock.json,后者锁定 Agent 自带 Node 以及用于 Linux SO_PEERCRED 的 Koffi 版本。Agent bundle 将 Koffi loader 作为 external module,并只携带目标 Linux 架构的 glibc/musl 原生 binding;构建与导入都会校验其 ELF 架构和 MIT 许可证。

lock 同时固定 @continuedev/cli@1.5.47 的 npm integrity。CN 工件使用 lib/continue/dist/cn.js,由 Agent 中的 HTTP-to-ACP helper 启动,不能传入原生 acp 参数。remote-runtime:build 与 build/agent-package.cjs build 接受 --runtime-id continue;默认 Agent 组包通过 --continue-archive 同时携带 OC/CN。 CN 的 JS payload 通过文件摘要与签名校验,执行它的 Node 仍来自 Agent 固定包。 默认 workflow 已接入联合组包;开发包与完整桌面到 Linux 的实测见 CN 部署记录。

远程 Runtime 工件由以下命令独立管理:

# 在对应 Linux 原生 Runner 上从 lock 指定的 npm 归档构建并签名
npm run remote-runtime:build -- --arch <x64|arm64> \
  --runtime-archive <locked-opencode-package.tgz>

# 为复合 Agent 包组装导入并完整验证签名归档
npm run remote-runtime:import -- --arch x64 --archive <runtime-x64.tar>
npm run remote-runtime:import -- --arch arm64 --archive <runtime-arm64.tar>

默认生成目录为 .remote-runtime-resources/linux-<arch>/opencode/<digest>, 不提交到仓库。生产 workflow 从 remote-runtime-lock.json 解析固定包名、版本和 integrity,以 --ignore-scripts 从官方 npm registry 获取归档;bundle 工具在签名前 再次验证归档 SHA-512、包名、版本和 ELF 架构。

源码已经实现 Runtime bundle build/import/verify、Daemon 侧 manifest/Ed25519/payload/ELF/lock 校验、digest registry、ACP v3、直接进程 ownership 和 runtime/model-bridge v1。Agent 通过私有 Unix socket 按需 detached 启动,不依赖 systemd、D-Bus、Linger 或 bubblewrap。Ask 与 Execute 都直接启动签名 Runtime;两者的 差异由 OpenCode Ask 权限配置和 Agent 工具权限分发边界共同执行,Execute 不注入该 权限覆盖。

OpenCode 通过已签名 Agent helper 和每次 Prompt 的私有 Unix socket 使用 Main-only 模型 网关;Provider URL、API Key 和真实 Provider 认证头不进入远端。helper 的 loopback HTTP 入口使用每个进程随机生成的路径 capability,Anthropic/OpenAI SDK 需要的固定非秘密 本地兼容标记由 helper 核对后丢弃。GoodBuddy 自己维护会话标题,所以生成的 OpenCode 配置必须禁用 title Agent;工具循环只使用 build Agent 的模型轮次。

Ask 模式的 ACP 权限中介只可为原生 read 选择 allow_once;其他工具种类和 allow_always 请求必须拒绝。Unix 模型桥 broker 必须按 socket 当前已缓冲字节增量读取长度帧,不能等待 read(remaining) 一次返回 完整大响应。Agent 传输或权限实现变化后,除了普通测试,还要在原生 Linux 上运行包含 至少 256 KiB 响应的模型桥回归。

agent-runtime-lock.json 维护独立于桌面应用的 Agent 版本, remote-runtime-lock.json 维护 OpenCode Runtime 版本。Agent 内容变化时必须更新 agentVersion;Runtime 更新时使用 Runtime 自身版本。签名 manifest 或 bundle digest 唯一标识对应版本的精确工件。变更后必须重新构建、签名并导入 Linux x64/arm64 两套工件; 旧版本或旧 digest 会按设计校验失败。不要手工把本机 .runtime-resources 当作远程 bundle。

桌面应用更新后不会在项目切换时下载或安装 Agent。用户先在 SSH Host 的远程运行环境 卡片显式选择直连下载或 GoodBuddy 传输。托管 SSH 项目只保存 Host ID、远端路径、 Runtime 选择和默认模式;创建或保存时验证 Host 当前环境,普通项目切换只读取这些本地 配置。首次实际使用 Workspace 或 Runtime 时解析 Host current identity;同一进程内后续 项目复用已确认的 Agent/Runtime identity 和 Agent 连接。该流程不取得安装包、不重读完整 payload,也不发布组件。

两条手动准备路径都支持阶段进度和取消;成功后定向失效该 Host 的连接与 Runtime 缓存, 引用项目下一次打开自然使用新的 current 环境。验收应覆盖两种准备方式与项目当前环境 验证入口,并确认失败或取消不会删除 Host 配置、凭据、项目设置、Workspace 文件、全局 可信元数据或仍可用的旧安装。

跨平台 CI 与 GitHub Release

.github/workflows/packages.yml 是统一发布工作流。它先验证并生成一次 out 生产 bundle,再在六个原生 Runner 上分别打包 Windows、macOS 和 Linux 的 x64、arm64 版本。生产 bundle 仅作为短期 Actions artifact 供打包任务复用,不会上传到 GitHub Release。该工作流不构建、下载、签名或嵌入 远端 Agent/OpenCode 复合包,也不依赖 agent-signing Environment;远端包缺失或 Agent 发布失败不会阻塞任何桌面目标。

本地构建单个平台目标:

npm run release:package -- --platform <windows|macos|linux> --arch <x64|arm64>

release:package 仅构建桌面产品。基础配置统一携带 agent-release-keys.json、agent-runtime-lock.json 与 remote-runtime-lock.json 供 Main 校验独立下载的复合包,但 release 配置不增加 .agent-resources 或 .remote-runtime-resources。发布校验继续检查桌面自身的 OpenCode、Continue 与 DeepSeek Harness Runtime、app.asar、目标架构和安装包签名。

在 macOS 上明确生成未签名、未公证的开发验证包:

npm run release:package -- --platform macos --arch <x64|arm64> --unsigned

默认发布产物为 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 的 release-manifest.json。

推送 main 时只运行源码验证和 production bundle 构建,不运行六平台 打包矩阵,避免随后推送版本标签时对同一提交重复完整打包。手动触发会运行 验证和六平台打包,并保留 30 天 Actions artifacts,但不会创建 Release。

Desktop 发布顺序(强制)

Desktop 正式发布必须把候选分支推送与标签推送拆成两个阶段,不能在同一批操作中先后 推送 main 和标签:

  1. 完成源码、版本、双语 Release Notes 与相关文档修改,运行 Release Notes 校验、 npm test、npm run typecheck 和 npm run lint。常规发布准备不在本机运行 production build、打包或安装包探针。
  2. 提交候选,确认用户批准的准确 commit SHA 与双语 Release Notes,fetch 两个远端后, 将该候选分别推送到 origin/main 和 github/main。
  3. 等待这个准确 SHA 的 main 分支 Cross-platform packages workflow 完成。只有 Run validators 和 Build production bundle 都成功,候选才具备打标签条件。
  4. 如果 main workflow 失败,不得创建标签。修复必须形成新的 commit,重新完成本地 校验、双远端 main 推送,并等待新 SHA 的 main workflow 成功;旧 SHA 的成功结果 不能替代新 SHA 的验证。
  5. main 成功后再次 fetch 两个远端,确认工作区干净、两个远端 main 都等于获批 SHA、 目标标签在本地和远端均未使用。此时才可在该 SHA 创建 annotated tag,并把标签分别 推送到两个远端,随后等待六平台打包和正式发布完成。

不可移动、删除、重建或复用失败的发布标签。完整审批、恢复和发布后检查以 发布 Runbook 为准。

推送 v${package.version} 标签时,工作流运行验证和六平台打包。只有在 全部目标成功后,才会严格校验并聚合所有平台产物,生成按平台重命名的 manifests、总 release-manifest.json 和 SHA256SUMS。随后工作流通过 GitHub OIDC 获取短期 STS 凭据,将发布资产和 site-release.json 上传到 北京 OSS 的不可变版本目录,并公开校验 12 个安装包。验证通过后才创建或 更新 draft GitHub Release、上传 20 个 Release 资产并正式发布,最后原子 切换官网 latest.json。任一步失败都不会提前切换官网最新版本。

同一标签重跑时,工作流会根据 resources/release-notes.json 重新生成并 覆盖 GitHub Release 正文,以 --clobber 更新已知发布资产,同时保留未知 附件。若源码与发布元数据未变化,应修正外部配置后重跑同一不可变标签; 只有必须修改代码或元数据时才递增版本并创建新标签。

Agent 发布使用另一套标签和 workflow:

agent_tag="agent-v$(node -p "require('./agent-runtime-lock.json').agentVersion")"
git tag -a "$agent_tag" -m "GoodBuddy Agent $(node -p "require('./agent-runtime-lock.json').agentVersion")"

实际创建或推送前同样必须获得用户确认。.github/workflows/agent-release.yml 原生构建两个 .gbagent、签名累计目录、上传 agent-releases/v<agent-version>/,创建 --latest=false GitHub Release,再切换 OSS 签名目录指针。生产发布需要 agent-signing 与 aliyun-oss-release 两个受保护 Environment;普通分支/PR 的 agents.yml 不使用其中任何 Secret。

Agent 标签也必须遵循与 Desktop 相同的 pre-tag 顺序。更新 Agent 生产源码或 agentVersion 后,先把候选提交推送到两个 main,等待该准确 SHA 的 GoodBuddy Agents main workflow 全部成功,再次 fetch 并确认两个远端 main、干净 工作区和未使用标签,最后才可创建和推送 annotated agent-v<version>。不得把 Agent 标签与候选 main 放在同一批推送中;任何修正 commit 都必须重新等待新 SHA 的 Agent main workflow,旧 SHA 的成功结果不能复用。

中英文发布说明统一维护在 resources/release-notes.json。新版本按“本次 亮点 / Highlights”“功能更新 / Features”“问题修复 / Bug Fixes”“使用前 请留意 / Before You Start”四段组织,应用首次启动弹窗与 GitHub Release 正文共用该来源。旧版两段式记录会兼容读取,无需改写。提交发布候选前运行 npm run release:notes:verify 校验版本、双语条目数量并生成 Markdown。

发布标签必须与 package.json 版本完全一致。实际推送标签和触发发布前仍 需人工确认,例如当前版本应使用:

tag="v$(node -p "require('./package.json').version")"
git tag -a "$tag" -m "GoodBuddy $(node -p "require('./package.json').version")"
git push origin "$tag"
git push github "$tag"

macOS 发布 job 会先原子判断 Apple 凭据状态:以下五项 Actions Secrets 全部 存在时,使用 Developer ID Application 证书签名,并通过 App Store Connect API Key 提交 Apple notarization;五项全部缺失时,明确生成未签名、未公证的 DMG,并在 Actions 日志与摘要中警告 Gatekeeper 限制;只配置一部分时 任务失败,不能静默降级为未签名包。

  • MACOS_CERTIFICATE_BASE64:包含证书及私钥的 .p12 文件经 Base64 编码后的内容。
  • MACOS_CERTIFICATE_PASSWORD:导出 .p12 时设置的密码。
  • APPLE_API_KEY_BASE64:App Store Connect API Key 的 .p8 文件经 Base64 编码后的内容。
  • APPLE_API_KEY_ID:App Store Connect API Key 的 Key ID。
  • APPLE_API_ISSUER:App Store Connect API Key 的 Issuer ID。

在 macOS 上生成适合 Secrets 的单行 Base64 内容:

base64 -i DeveloperIDApplication.p12 | tr -d '\n'
base64 -i AuthKey_XXXXXXXXXX.p8 | tr -d '\n'

证书必须是 Apple Developer 后台创建的 Developer ID Application,并在导出 .p12 的 Mac 钥匙串中同时包含对应私钥。API Key 建议使用团队级 App Store Connect Key;.p8 只能下载一次。签名材料只放入 GitHub Secrets,不提交到仓库。

凭据完整时,macOS 打包完成后会挂载 DMG,并分别执行 codesign、Gatekeeper spctl 和 stapler 校验;签名无效或 notarization ticket 不存在时,发布矩阵 会在上传产物前失败。完全没有凭据时,六平台矩阵和标签发布仍可完成,但 macOS 产物没有 Developer ID 签名或 Apple 公证,Gatekeeper 可能阻止首次打开。发布 工作流必须在 Actions 日志与摘要中明确这一限制;获得完整凭据后应恢复签名 发布并重新验证。

Windows 代码签名仍未配置。对外分发前还应配置 Windows 签名凭据,并重新验证 安装、升级和系统安全提示。

发布前冒烟测试

建议在每个目标系统上至少验证:

  1. 安装、启动、升级和卸载。
  2. 中文输入法、窗口缩放和高分屏显示。
  3. 系统密钥环和模型连接。
  4. 本地知识库导入、检索和知识图谱。
  5. Ask、Execute 的权限边界与旧版 Plan 数据兼容。
  6. OpenCode 与 Continue 的权限边界、取消和超时。
  7. DeepSeek Harness Ask 拒绝写入和第三方插件工具,可调用 Main 管理的 Web Search/Fetch;Execute 可调用已启用插件工具。文本模型在网络调用前拒绝图片,声明图片能力的模型可以实际接收 JPEG/PNG。
  8. OpenCode Agent/Command、原生上下文 Compact,以及 Continue Rules/Prompt 预设、结构化提问和 GoodBuddy 手动摘要压缩。
  9. Runtime 原生清单把 Tools 与 Commands/LSP/Formatters 分开,显示来源及 Ask/Execute 可用性,不混入 GoodBuddy 分配的 Skills/MCP;外部 OpenCode 只报告连接状态,Continue 明确标记原生 Tools 静态发现不支持;内置 MCP 的启停与 Runtime 分配会持久化并限制后续请求,DeepSeek Harness 保持不可分配;MCP 测试只读取有界 Prompt/Resource 元数据,不读取 Resource 内容。
  10. DSH 市场可安装、停用、重新启用和移除插件;启动失败插件不会阻止 Host,并显示为自动停用。
  11. 智能心跳的创建、暂停、恢复和历史记录。
  12. 应用退出后无残留 Runtime 子进程。

DeepSeek Harness 的 Electron Utility Host 可单独执行无模型、无凭据冒烟测试:

npm run smoke:deepseek-harness

该命令先生成 production bundle,再从 CommonJS Electron 主入口启动实际 utilityProcess,等待固定 Host 完成本地主机执行器初始化与内部 ready 握手。它不会发起 模型请求,也不会读取或传递 API Key。

Windows x64 完整打包后的 Utility Host 与内置 npm 冒烟测试:

npm run build
node node_modules/electron-builder/cli.js --win dir --x64 --publish never --config.directories.output=dist/harness-package-probe
npm run smoke:deepseek-harness:packaged

该测试启动打包后的 Host,并使用打包资源中的准确 npm 版本安装一个本地临时包, 确认 npm 依赖闭包、Electron Node 模式、node shim 和生命周期脚本都可用。它不访问 npm registry,也不运行模型请求。

真实 DSH 市场测试会访问公共 npm、运行第三方安装与插件代码,因此只在已明确授权时启用:

GOODBUDDY_DSH_MARKETPLACE_E2E=1 npm test -- src/main/agent/dsh-extension-marketplace.e2e.test.ts

该测试使用临时用户目录,经捆绑 npm 路径安装已审查的最小测试插件,并验证 Host 加载和真实工具调用;测试结束后删除临时目录。

要让真实模型同时验证插件的 Ask 拒绝与 Execute 调用,可显式提供兼容的 OpenAI Chat Completions 配置:

GOODBUDDY_DSH_MODEL_E2E=1 \
GOODBUDDY_DSH_API_KEY=... \
GOODBUDDY_DSH_BASE_URL=https://api.deepseek.com \
GOODBUDDY_DSH_MODEL=deepseek-chat \
npm test -- src/main/agent/deepseek-harness-acp-e2e.test.ts -t "rejects a real npm plugin"

如需覆盖发布包内置 npm 路径,再设置 GOODBUDDY_DSH_NPM_CLI 与 GOODBUDDY_DSH_NODE_EXECUTABLE 指向已解包应用中的 npm CLI 和应用主程序。