# GoodBuddy 开发与构建 ## 环境要求 - Node.js 24,或当前锁定依赖明确支持的 Node.js 版本 - npm - Windows、Linux 或 macOS 安装锁定依赖: ```bash npm ci ``` 不要将 API Key、访问令牌、私有模型地址或本地数据库提交到仓库。 ## 本地开发 启动开发环境: ```bash npm run dev ``` 预览已经生成的生产构建: ```bash npm run build npm start ``` ## 质量验证 提交或打包前运行: ```bash npm test npm run typecheck npm run lint ``` 监听模式: ```bash npm run test:watch ``` 真实 Runtime 端到端测试默认关闭。配置兼容模型凭据后,显式启用: ```bash 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 用例。 ## 生产构建 生成 Electron Main、Preload 和 Renderer 生产文件: ```bash npm run build ``` 中间构建输出位于 `out`。该目录为生成内容,应修改源文件后重新构建,不要直接编辑。 ## 图标生成 应用图标源文件位于 `icons`。修改源图或图标处理逻辑后运行: ```bash npm run icons ``` 脚本会精确裁出亮色和深色圆角卡片,清理圆角外侧背景并使用适合主题的 边缘颜色生成透明抗锯齿,避免缩放后出现白边。它会统一更新 `build` 中的 PNG / ICO、Renderer 图标,以及官网使用的亮色和深色品牌图标。任务栏和 托盘图标保持透明背景。 ## 平台打包 ### 当前平台默认包 ```bash npm run dist ``` ### Windows 生成 Windows NSIS 安装包: ```bash npm run dist:win ``` 生成用于本机调试的 Windows 便携目录: ```bash npm run portable ``` ### macOS 生成 `x64` 和 `arm64` DMG: ```bash npm run dist:mac ``` ### Linux 同时生成 Linux `x64` 和 `arm64` 包: ```bash npm run dist:linux ``` 只生成指定架构: ```bash npm run dist:linux:x64 npm run dist:linux:arm64 ``` 每个 Linux 架构生成: - `deb`:适用于麒麟、统信 UOS 等 Debian 系桌面。 - AppImage:适用于免安装验证和便携运行。 打包产物位于 `dist`。 以上 `dist*` 与 `portable` 命令用于普通本地桌面打包。它们会携带 `agent-runtime-lock.json`、`remote-runtime-lock.json` 与公开的 `agent-release-keys.json`。普通 `dist*` 仍为 metadata-only;本地 `portable` 若发现 `.agent-resources/linux-x64` 或 `linux-arm64`,会先按当前 lock 与公钥 registry 完整校验,再把已存在的 Agent 工件嵌入 Portable。设置 `GOODBUDDY_PORTABLE_REQUIRE_AGENT_ARCHS=x64`(或 `x64,arm64`)可要求指定架构 必须存在并成功嵌入,否则构建失败。没有本地工件时,Portable 仍可验证并复用 Host 上 与 lock 完全匹配的签名 Agent,但不能安装缺失或不匹配的 Agent。 ## Runtime 资源 桌面包会携带经过版本与完整性校验的 OpenCode、Continue 和 DSH 插件安装 Runtime: - OpenCode 平台二进制来自 `.runtime-resources/`。 - 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 Agent 工件 Linux Agent 工件与普通桌面构建相互独立,由专用命令管理: ```bash # 只能在对应 Linux 架构上使用锁定的 Node Runtime 原生构建并签名 npm run agent:build -- --arch --runtime-archive # 导入由原生 CI 生成的签名归档,导入时会完整校验 npm run agent:import -- --arch x64 --archive npm run agent:import -- --arch arm64 --archive # 独立重新校验本地工件 npm run agent:verify -- --arch x64 npm run agent:verify -- --arch arm64 ``` 默认位置为 `.agent-resources/linux-x64` 和 `.agent-resources/linux-arm64`,该目录是 生成内容,不提交到仓库。普通命令不会隐式构建或导入 Agent;`npm run portable` 只会校验并嵌入已经存在的本地 Agent 工件。 ### Agent 原生 CI 策略 Agent 的 Linux x64 与 arm64 构建应使用同一源码 commit/tag 上的原生 GitHub Actions job 或可复用 workflow,不维护长期分叉的 Agent 源码分支。这样桌面源码、协议、lock 文件和签名 manifest 的来源保持一致。若后续把 Agent 构建从统一发布 workflow 拆出, 应使用 `workflow_dispatch`、可复用 workflow 或 Agent tag 触发,明确 checkout 待发布 桌面的精确 commit,并输出供发布 workflow 下载、`agent:import` 和 `agent:verify` 的 两套签名归档;发布 job 仍必须在嵌入前后验证两种架构,不能信任分支名或未校验 artifact。 ### 远程 OpenCode Runtime 基础 `remote-runtime-lock.json` 独立锁定首个远程 Runtime:OpenCode 1.18.9 的 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 许可证。 源码已经实现 Runtime bundle build/import/verify、Daemon 侧 manifest/Ed25519/payload/ELF/lock 校验、digest registry、ACP v3、直接进程 ownership、 Ask 的固定 `bwrap` 只读 profile 和 `runtime/model-bridge` v1。Agent 通过私有 Unix socket 按需 detached 启动,不依赖 systemd、D-Bus 或 Linger。Execute 直接以所选 SSH 账号权限 启动签名 Runtime;Ask 才使用只读 bubblewrap。 OpenCode 通过已签名 Agent helper 和每次 Prompt 的私有 Unix socket 使用 Main-only 模型 网关;Provider URL、API Key 和真实 Provider 认证头不进入远端。Anthropic/OpenAI SDK 只使用 固定非秘密本地兼容标记,helper 核对后丢弃。 `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。 桌面应用更新后,用户下次打开托管 SSH 项目会使用该桌面版本内置的签名 Agent/Runtime 执行一次完整激活。所需版本健康且项目事务提交成功后才刷新项目绑定;失败不会覆盖旧安装或 把未提交的新 identity 暴露给 Workspace/Runtime。 ## 跨平台 CI 与 GitHub Release `.github/workflows/packages.yml` 是统一发布工作流。它先验证并生成一次 `out` 生产 bundle,再在六个原生 Runner 上分别打包 Windows、macOS 和 Linux 的 `x64`、`arm64` 版本。生产 bundle 仅作为短期 Actions artifact 供打包任务复用,不会上传到 GitHub Release。工作流另行在 Linux x64/arm64 Runner 上原生构建并签名 Agent,桌面矩阵下载、导入并校验两套归档后才调用 `release:package`;桌面 Runner 不重新构建 Agent。 本地构建单个平台目标: ```bash npm run release:package -- --platform --arch ``` `release:package` 是唯一会嵌入远程 Agent 和远程 OpenCode Runtime 的桌面打包入口。公开 签名 key registry 已由基础打包配置统一携带;运行前 必须已经导入 Linux x64 与 arm64 两套签名 Agent/Runtime 工件;脚本会在启动 Electron Builder 前验证完整矩阵, 使用 `build/electron-builder.release.cjs` 加入两套 bundle,并连同基础配置中的 `agent-release-keys.json`、`agent-runtime-lock.json` 与 `remote-runtime-lock.json` 在打包后再次验证 应用内副本。任一架构缺失、签名无效、组件版本、digest、ELF 架构、许可证或 lock 不匹配都会失败,不会生成不完整的发布包。当前仓库已包含 production 公钥 registry; production preflight 仍要求外部安全供应与当前源码、lock 完全匹配的 Linux x64/arm64 签名工件,缺少任一架构时会故意失败。 在 macOS 上明确生成未签名、未公证的开发验证包: ```bash npm run release:package -- --platform macos --arch --unsigned ``` 默认发布产物为 Windows 的 NSIS 安装包与 portable ZIP、macOS 的 DMG 与 ZIP,以及 Linux 的 AppImage 与 DEB。Windows portable ZIP 解压后可直接 运行 `GoodBuddy.exe`,并包含启用便携数据目录的 `.goodbuddy-portable.json`。每个目标目录都包含带文件大小和 SHA-256 的 `release-manifest.json`。 推送 `main` 时只运行源码验证和 production bundle 构建,不运行六平台 打包矩阵,避免随后推送版本标签时对同一提交重复完整打包。手动触发会运行 验证和六平台打包,并保留 30 天 Actions artifacts,但不会创建 Release。 推送 `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` 更新已知发布资产,同时保留未知 附件。若源码与发布元数据未变化,应修正外部配置后重跑同一不可变标签; 只有必须修改代码或元数据时才递增版本并创建新标签。 中英文发布说明统一维护在 `resources/release-notes.json`。新版本按“本次 亮点 / Highlights”“功能更新 / Features”“问题修复 / Bug Fixes”“使用前 请留意 / Before You Start”四段组织,应用首次启动弹窗与 GitHub Release 正文共用该来源。旧版两段式记录会兼容读取,无需改写。提交发布候选前运行 `npm run release:notes:verify` 校验版本、双语条目数量并生成 Markdown。 发布标签必须与 `package.json` 版本完全一致。实际推送标签和触发发布前仍 需人工确认,例如当前版本应使用: ```bash 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 和 ZIP,并在 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 内容: ```bash 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 可单独执行无模型、无凭据冒烟测试: ```bash npm run smoke:deepseek-harness ``` 该命令先生成 production bundle,再从 CommonJS Electron 主入口启动实际 `utilityProcess`,等待固定 Host 完成本地主机执行器初始化与内部 ready 握手。它不会发起 模型请求,也不会读取或传递 API Key。 Windows x64 完整打包后的 Utility Host 与内置 npm 冒烟测试: ```bash 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、运行第三方安装与插件代码,因此只在已明确授权时启用: ```bash GOODBUDDY_DSH_MARKETPLACE_E2E=1 npm test -- src/main/agent/dsh-extension-marketplace.e2e.test.ts ``` 该测试使用临时用户目录,经捆绑 npm 路径安装已审查的最小测试插件,并验证 Host 加载和真实工具调用;测试结束后删除临时目录。 要让真实模型同时验证插件的 Ask 拒绝与 Execute 调用,可显式提供兼容的 OpenAI Chat Completions 配置: ```bash 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 和应用主程序。 ## Renderer bundle 性能门禁 `electron-vite` 会在 `out/renderer/.vite` 生成 Vite manifest 和仅含构建模块 归属的 module manifest。`npm run build:bundle` 在 bundle 完成后自动运行 `build/check-renderer-bundle.cjs`,去重统计首屏同步闭包及 Knowledge、知识图谱、 Activity、Magic Notes、Settings 动态入口同步加载的 JS 与 CSS 合计 raw / gzip 大小并执行预算校验。 门禁同时验证以下结构约束: - Knowledge、Activity 与 G6 不得进入首屏同步闭包。 - `KnowledgeGraphChart` 与 G6 不得进入 Knowledge shell 的同步闭包。 - G6 必须由知识图谱动态入口同步拥有。 路径遍历使用 manifest 中的相对文件名并通过 Node `path.resolve` 读取,因此兼容 Windows 与 POSIX 构建输出。构建专用 module manifest 只记录项目相对路径、 `node_modules/` 相对路径或稳定的虚拟模块名,不记录 Runner 的盘符、主目录或 绝对路径;检查成功后会删除该诊断文件,检查失败时保留以便排查。标准 Vite manifest 会保留在输出中。预算以干净生产构建为基线并保留有限余量;若业务确需 提高预算,必须先检查 manifest 闭包和产物差异,不能只为通过 CI 调大数值。 Bundle 大小是发现意外依赖和持续膨胀的粗粒度工程护栏,不是用户体验指标,也不能替代 Electron 中的实际交互验证。Settings 的硬上限以 2026-08-23 验证构建 `648.78 kB raw / 97.79 kB gzip` 为基线,增加约 15% 评审余量后取整为 `750 kB raw / 115 kB gzip`。该上限用于拦截显著增长,不要求实现长期贴线运行。 调整 Renderer 拆包时遵守以下优先级: 1. 同级页签的交互一致性、首个绘制帧内反馈和输入保留优先于包体数字。 2. 不得仅为通过预算,把一个原本轻量的同级页签单独改成首次点击才加载。 3. 确需动态加载重型功能时,必须提供非空、无布局跳变的局部状态,并增加首次交互测试。 4. 修改预算必须记录干净生产构建基线、增量闭包差异和 Electron 实测结果。