docs: organize product design by feature

Define repository and feature-scoped documentation rules for PRDs, User Stories, logic, UI, technical design, and progress evidence.

Move feature-owned documents under docs/features, add one entry point per feature, remove the redundant docs index, and update all affected cross-references. Expand the local tool environment design with explicit product, story, logic, UI, technical, and progress documents.
This commit is contained in:
mesalogo
2026-09-01 14:52:22 +08:00
parent dba3380a06
commit 2f1881a414
56 changed files with 1763 additions and 765 deletions
@@ -9,7 +9,7 @@
| 日期 | 2026-08-19 |
| 适用产品 | GoodBuddy 桌面端 |
| 文档角色 | Task/Job、调度、目标、并行实验、会话监督、分区记忆与持续学习的总纲 |
| 领域模型 | [Task 与 Job 统一领域模型](../prd/task-and-job/task-and-job-model.md) |
| 领域模型 | [Task 与 Job 统一领域模型](../features/task-and-job/task-and-job-model.md) |
## 1. 背景
@@ -445,7 +445,7 @@ Renderer 只通过显式 IPC 列出、加入、删除、提升、释放和接收
监督统一进入应用级助手工作栏中固定且始终可访问的“监督”栏目,不再保留“独立可折叠右栏”
和“动态新增页签”两种实现。栏目默认跟随当前上下文,用户可以固定到其他 Conversation、
Task 或实验对象;详细范围与交互契约见
[通用助手工作栏与执行空间 PRD](../prd/assistant-experience/assistant-workbar-and-execution-spaces-prd.md)。
[通用助手工作栏与执行空间 PRD](../features/assistant-workbar/prd.md)。
## 13. 安全与隐私
@@ -555,15 +555,15 @@ Conversation 关联,旧 child-task 字段可兼容映射到 Job/Subjob;心
## 17. 相关文档
- [Task 与 Job 统一领域模型](../prd/task-and-job/task-and-job-model.md)
- [Task Center PRD](../prd/task-and-job/task-center-prd.md)
- [Scheduled Task PRD](../prd/task-and-job/scheduled-task-prd.md)
- [Job 与 Subjob PRD](../prd/task-and-job/job-and-subjob-prd.md)
- [智能心跳 PRD](../prd/smart-heartbeat/smart-heartbeat-prd.md)
- [并行实验工作台 PRD](../prd/experiments/parallel-experiments-prd.md)
- [会话监督 PRD](../prd/supervision/conversation-supervision-prd.md)
- [分区记忆 PRD](../prd/memory/partitioned-memory-prd.md)
- [持续学习与评估门 PRD](../prd/learning/continuous-learning-prd.md)
- [Task 与 Job 统一领域模型](../features/task-and-job/task-and-job-model.md)
- [Task Center PRD](../features/task-and-job/task-center-prd.md)
- [Scheduled Task PRD](../features/task-and-job/scheduled-task-prd.md)
- [Job 与 Subjob PRD](../features/task-and-job/job-and-subjob-prd.md)
- [智能心跳 PRD](../features/smart-heartbeat/prd.md)
- [并行实验工作台 PRD](../features/parallel-experiments/prd.md)
- [会话监督 PRD](../features/conversation-supervision/prd.md)
- [分区记忆 PRD](../features/memory/prd.md)
- [持续学习与评估门 PRD](../features/continuous-learning/prd.md)
- [GoodBuddy 长期助手功能规划](../roadmap/long-term-assistant-roadmap.md)
- [GoodBuddy 统一界面设计系统](../../UI-DESIGN.md)
@@ -1,778 +0,0 @@
# GoodBuddy 自维护 DeepSeek Harness Runtime 设计
## 1. 文档信息
| 项目 | 内容 |
| --- | --- |
| 文档状态 | 实现与发布验收基线 |
| 设计目标 | 将 DeepSeek Harness 作为 GoodBuddy 的第三个 Agent Runtime |
| Runtime 标识 | `deepseek-harness` |
| 当前依赖基线 | 实际使用的 `@deepseek-ai/dsh-*` 底层库,精确锁定 `0.1.0-rc.8` |
| 上游状态 | Developer Preview,允许出现破坏性变更 |
| 上游许可证 | MIT |
| GoodBuddy 目标平台 | Windows、macOS、Linux,x64 与 arm64 |
| 本文性质 | 设计与发布验收约定 |
本文定义 DeepSeek Harness 在 GoodBuddy 中的架构边界、协议、执行策略、插件市场、界面、打包和验收要求。实现必须继续遵守 GoodBuddy 已有的 Main 进程安全边界、Ask/Execute 语义、取消、超时、有界输出和资源回收约定。
## 2. 摘要
DeepSeek Harness 的底层库使用 Cordis 组合服务。GoodBuddy 不采用官方产品 profile,也不允许用户配置覆盖内部 Host 或控制服务;GoodBuddy 自行维护 Host、控制协议和生命周期,同时提供一个由 Main 管理、默认关闭的 npm 插件市场。用户显式开启后,市场只搜索带精确 `dsh-plugin` 关键字的公共 npm 包,不代表 GoodBuddy 审核、推荐或承诺兼容这些包。
用户明确安装并启用的插件以当前用户权限运行。Ask/Execute 只控制模型经过 `tools/execute` 发起的工具调用:Ask 只允许 Host 中真实注册的 `read`、`skill` 和 Main 管理的 Web Search/Fetch 代理,Execute 放行 Host 中全部已注册工具。插件不能用同名工具冒充 Ask 允许项。插件安装脚本和初始化代码不属于模型工具调用,不能由 Ask 限制,因此界面在安装前必须明确确认这一边界。
整体分成两个互相约束的部分:
1. **GoodBuddy Main Control Plane**
- 运行在 Electron Main 进程。
- 持有加密设置、模型连接选择、Ask 只读策略、Runtime 生命周期、插件市场状态和审计归属。
- 通过 Electron `utilityProcess` 启动受控 Harness 子进程。
- 对环境、输入、输出、超时、取消和进程树执行强制限制,并只把已启用插件的受管入口传给 Host。
2. **GoodBuddy Harness Control Plane**
- 运行在 Harness 子进程内,是 Host 私有的内部控制组件,不导出 Cordis 插件入口。
- 使用 ACP 兼容的 JSON-RPC stdio 作为基础控制面。
- 增加 GoodBuddy 所需的能力握手、每轮权限准备、会话释放、工具事件、推理、用量和安全凭据请求扩展。
- 与 GoodBuddy Host 一起维护、构建和发布,不设计为独立 npm 包或市场插件;第三方插件只作为显式配置加载。
DeepSeek Harness 不替换 OpenCode、Continue 或直连模型 Runtime。用户可以按全局、项目、会话或消息通道继续选择现有 Runtime。
## 3. 背景与上游能力
### 3.1 已确认的官方能力
- `@deepseek-ai/dsh` 是官方 profile 启动器。
- Harness 插件是导出 `apply(ctx, config)` 的 Cordis 模块。
- npm 包可通过 `dsh.bundle` 声明配置补丁,再通过 `dsh plugin --profile <name> add <package>` 安装。
- ACP 支持:
- 初始化。
- 创建多个会话。
- 发送 Prompt。
- 按会话取消。
- 一次性权限选择。
- 已提交的助手文本。
- 官方本地沙箱支持:
- Linux:Bubblewrap,或 Landlock 降级。
- macOS:Seatbelt。
- Windows:ACL 受限令牌,官方明确标记为部分强制执行。
GoodBuddy 不组合上述 Runtime OS 沙箱。当前产品选择 DSH 本地 Shell 与
Filesystem Provider,以 GoodBuddy 客户端进程的当前用户权限运行工具。
### 3.2 官方通道的缺口
官方 ACP 插件有意只输出已提交文本,不输出推理、工具进度、计划、标题和用量。它也没有标准的会话关闭方法。SDK JSON-RPC 的展示事件更完整,但缺少 GoodBuddy 需要的单轮取消和权限回传。
因此,首版不单独选用其中一个官方通道作为完整实现。GoodBuddy Harness Control Plane 以 ACP 语义为基础,补充有命名空间的扩展方法和事件。
### 3.3 自维护边界
GoodBuddy 自己的 Runtime 和控制面不包装成标准 DSH 插件,也不加载用户 profile 或自定义 Host。只有 GoodBuddy Main 可以启动内部 Host、选择受管插件入口并处理启动失败。公共 npm 市场是第三方扩展来源,不改变 GoodBuddy 对内部 Host、Ask/Execute 语义和协议版本的控制。
## 4. 目标与非目标
### 4.1 首版目标
- 增加 `deepseek-harness` Runtime,并在设置、聊天和消息通道中可选择。
- 使用 GoodBuddy 管理的模型连接,不在 Renderer 或持久化 Harness 配置中写入 API Key。
- 当所选模型连接明确声明支持图像输入时,允许向 DeepSeek Harness 发送有界的 JPEG/PNG;文本模型在启动 Host 或调用模型前拒绝图片。
- Ask 模式在 Runtime 工具分发边界强制只读,阻止 Shell、写入和编辑工具。
- 在 Web Search 能力启用时,通过 Main 代理向 Ask 与 Execute 提供有界的 `web_search` 和 `web_fetch`,Harness Utility 不持有服务凭据。
- Execute 模式使用 DSH 本地 Provider,以当前用户权限执行文件与命令工具;工作区是默认工作目录,不是 OS 权限边界。
- 提供默认关闭的公共 npm DSH 插件市场总开关;用户显式开启后可搜索、查看详情、精确版本安装、启用、停用、配置和移除,首次安装前明确确认当前用户权限。
- 只加载 Main 明确传入的已启用插件;单个插件启动失败不得阻止 Host,并自动停用失败插件。
- 允许 Skills 和自定义 MCP 显式分配给 DeepSeek Harness;自定义 MCP 只在 Execute 中通过 Main 代理。
- 设置页可读取有界的 Host/插件原生 Tool 与 Skill 清单;Tool 元数据显示类型、来源及 Ask/Execute 可用性,并明确排除 GoodBuddy 分配的 Skills、Web/MCP 请求代理。
- 支持多会话、同会话串行、跨会话并行。
- 支持按请求取消、超时、会话释放和应用退出时完整回收。
- 输出文本、推理、工具参数、工具结果、stderr 和协议队列全部有界。
- 使用真实 OpenAI 兼容 Chat Completions 模型验证调用,而不在日志、测试产物或提交中暴露凭据。
- 保留 Windows、macOS、Linux 的 x64 和 arm64 发布能力。
### 4.2 首版非目标
- 不替换 OpenCode、Continue 或直连模型 Runtime。
- 不开放用户 Cordis profile、cordis.patch.yml 或 $DSH_HOME 全局补丁覆盖。
- 不提供外部 Host、自定义 Harness Control Plane、任意本地模块路径或用户 profile 插件目录。
- 不加载 Harness Web UI、HMR、遥测、自动更新或目录选择器。
- 不提供 Runtime OS 沙箱模式或相关持久设置。
- 不向 Utility 暴露 MCP 凭据或建立直连 MCP Client。只有用户明确分配给 Harness 的 MCP 工具可以通过 Main 代理调用。
- 不在首版向 Harness 暴露 GoodBuddy 浏览器控制、知识库或 Magic Notes。
- 不在首版支持会话恢复、Harness Subagent、后台 Job、Hook、浏览器控制或 Workflow;Web Search/Fetch 只通过 Main 代理提供,不加载 Harness 自有网页服务。上述长生命周期能力未来统一进入右侧 Runtime 监督栏,不进入 Composer 工具栏。
- 不发布独立 npm 包,也不创建上游 PR。
- 不为第三方插件增加权限矩阵、风险等级、逐工具审批、沙箱档位、回滚代际或兼容性背书。
## 5. 核心设计决策
### 5.1 第三个独立 Runtime
`deepseek-harness` 是明确的 Runtime 类型,不伪装成 `model`、`opencode` 或 `continue`。共享契约、设置迁移、Runtime 选择、检测、聊天标签、消息通道和模型用量都使用同一个稳定标识。
### 5.2 受控组合,不启动用户 profile
GoodBuddy 使用自己固定的 Harness Host 入口和只读组合模板,不调用 `dsh web`,也不启动用户已有 profile。运行时禁止以下来源参与组合:
- 当前工作目录的 `.env`。
- 用户 Harness Home 的 `.env`。
- `$DSH_HOME/cordis.patch.yml`。
- 用户 profile 的 `cordis.patch.yml`。
- 任意 `--patch`。
- HMR 和 profile 驱动的动态插件安装。
模型名称、服务地址、工作区、Skills、MCP schema 和已启用插件的规范化入口通过严格校验的 Main 配置传给 Host。API Key 只通过受控凭据通道按需提供,不写入 YAML、命令行、Renderer 或日志。插件配置只来自 GoodBuddy 受管状态,不合并用户 profile 或全局补丁。
### 5.3 双层内部控制面
Harness 子进程内控制面不能取代 Main 控制面,Main 控制面也不能代替进程内的 Session/Tool 适配层:
- Harness Control Plane 最接近 Session、Agent、Tool 和 Usage seam,适合做内部协议转换与 Ask 工具拦截。
- Main 控制面是可信安全边界,适合持有模式授权策略、加密设置、进程控制和 IPC。
任何一侧缺失能力握手时,Runtime 必须报告不可用,不能降级为不受控执行。
### 5.4 GoodBuddy 继续拥有持久会话
首版不启用 Harness JSONL 会话持久化和 SQLite 会话索引。原因如下:
- GoodBuddy 已经持久化对话、消息、活动、工具事件和用量。
- 再写一份 Harness 日志会扩大敏感数据副本和清理范围。
- GoodBuddy 在 Runtime 重启后可以用现有的有界历史创建新 Harness Session。
Harness Session 只在当前 Runtime 进程生命周期内存在。释放 GoodBuddy 会话时必须同步释放对应 Harness Agent。
## 6. 总体架构
```text
Renderer
│ 显式、经 schema 验证的 preload API
▼
Electron Main
├─ RuntimeSettingsStore
├─ RuntimeExtensionStore / npm Marketplace
├─ AgentRuntimeController
├─ RuntimeAuthorizer(Ask 拒绝 / Main 代理工具授权)
└─ DeepSeekHarnessRuntime / Main Control Plane
│ ACP + goodbuddy/* 扩展,stdin/stdout
▼
Electron utilityProcess
└─ GoodBuddy Harness Host
├─ 固定 Cordis 组合
├─ GoodBuddy Harness Control Plane(内部组件)
├─ DSH Agent 与 LLM seam
├─ 按模型能力挂载的有界内存图片存储
├─ 本地 Shell / Filesystem Provider
├─ 最小工具集与 Main 代理 MCP
└─ Main 明确启用的第三方 Cordis 插件
│ HTTP/HTTPS
▼
用户选择的 OpenAI 兼容模型连接
```
### 6.1 信任边界
| 区域 | 信任级别 | 允许持有的内容 |
| --- | --- | --- |
| Renderer | 不可信展示层 | 脱敏设置、状态、用户可见事件 |
| Preload | 窄桥 | 明确方法和共享 schema |
| Electron Main | 可信控制面 | 加密设置、模式授权策略、Runtime 生命周期 |
| npm 安装子进程 | 第三方执行面 | 受管暂存目录、去除模型凭据的有界环境和当前用户权限 |
| Harness utilityProcess | 不可信执行面 | 当前请求、临时凭据、受控工具、第三方插件代码和当前用户权限 |
| Harness 工具子进程 | 最低信任 | 单次命令所需的最小环境和当前用户权限 |
Harness 子进程崩溃、输出异常、拒绝协议或加载错误时,Main 必须失败关闭。
## 7. GoodBuddy Harness Control Plane
### 7.1 内部组件职责
控制面负责:
- 启动 ACP 兼容的 JSON-RPC stdio 服务。
- 创建、查找和释放 Harness Agent。
- 在 Prompt 前应用 GoodBuddy 指定的 Ask/Execute 权限。
- 将 DSH Session 事件转换为有界的 GoodBuddy 事件。
- 将 LLM 用量转换为稳定的模型用量事件。
- 根据 Main 传入的模型能力声明 ACP 图片能力,验证内联图片并转换为 DSH 的不可变 Attachment 引用。
- 在 dispose 时先取消 Agent,再等待子 Agent 和工具清理。
- 保证 stdout 只包含协议帧,诊断只写 stderr。
控制面不负责:
- 保存 GoodBuddy 设置。
- 持久保存 API Key。
- 决定 Main 的模式授权结果。
- 直接访问 Renderer 或 Electron API。
- 接受任意路径、外部 Host 或 profile 覆盖;插件入口只能来自 Main 的受管清单。
- 自行上传遥测。
### 7.2 第三方插件加载
GoodBuddy 控制面自身不导出 `apply(ctx, config)`,也不提供默认 stdin/stdout 入口或可安装 manifest。第三方插件由 Main 在启动配置中逐项指定:
- Main 只传递受管 Store 中已启用插件的稳定 ID、规范化入口文件和 JSON 配置。
- Launcher 与 Host 对消息结构和绝对入口路径执行严格校验;Host 解析真实路径并要求入口是普通文件。
- Host 动态加载 Cordis 插件并等待激活,每个插件有独立的 5 秒激活超时,完整插件序列最多占用 90 秒。
- Main 的 Host 启动预算使用 10 秒基础预算,加上每个已启用插件 5 秒激活与最多 1 秒失败清理、且整个插件序列最多占用 91 秒,再预留 2 秒保存失败插件状态;显式测试超时仍作为调用方指定的硬上限。
- 插件按清单依次加载;导入、导出形态或激活失败只记录该插件,不阻止其他插件和 Host 启动。失败 Fiber 的清理同样有界。
- 有限但超过预算的同步导入或同步 `apply` 在返回后按超时失败并继续加载后续插件;JavaScript 不能在同一事件循环内抢占永不返回的同步第三方代码,此时由 Main 的独立启动截止时间终止整个 Utility。
- 失败 ID 在 ready 握手中返回 Main;Main 原子写入停用状态和启动错误。
- 插件成功激活后可注册工具或后台生命周期逻辑。Ask 只能拦截模型工具调用,不能撤销初始化阶段已经发生的副作用。
GoodBuddy 不扫描任意目录、不读取用户 profile 插件清单,也不接受 Renderer 直接提供文件路径。
插件安装、升级和移除在目录重命名前写入受管变更日志。Main 下次初始化时以持久
Store 是否已经提交为准,确定性完成新目录或恢复旧目录,并在处理前重新验证受管
目录、入口真实路径、符号链接和根目录包含关系。旧版 `store.json` 继续原地迁移,
不要求用户重新安装插件。
## 8. 协议设计
### 8.1 传输
- stdin/stdout 使用换行分隔 JSON-RPC。
- stdout 不得出现日志、Banner、进度条或调试输出。
- stderr 只允许有界诊断,不得包含 Prompt、工具完整输出或凭据。
- 每一帧、每一字段和每个请求累计输出都必须在解析前或接收时限流。
- 图片只允许作为 ACP 内联 base64 内容传入;拒绝远程 URI,Host 不替用户获取图片 URL。
### 8.2 标准 ACP 方法
首版保留 ACP 的初始化、`session/new`、`session/prompt` 和 `session/cancel` 语义。`promptCapabilities.image` 必须与所选模型连接的 `supportsImageInput` 完全一致,不能仅根据 Provider 或模型名称猜测。标准 ACP 客户端可以使用只读默认行为,但只有完成 GoodBuddy 能力握手的客户端才能启用 Execute。
### 8.3 GoodBuddy 扩展
扩展统一使用 `goodbuddy/` 命名空间:
| 方法或事件 | 方向 | 用途 |
| --- | --- | --- |
| `goodbuddy/handshake` | Main → Control Plane | 交换控制协议、Harness、ACP 版本和能力 |
| `goodbuddy/session/prepare` | Main → Control Plane | 在下一次 Prompt 前设置工作模式和请求标识 |
| `goodbuddy/session/release` | Main → Control Plane | 取消并释放指定 Session |
| `goodbuddy/session/event` | Control Plane → Main | 文本、推理、工具、状态和用量事件 |
| `goodbuddy/credential/resolve` | Control Plane → Main | 按已登记引用请求当前 Runtime 的临时凭据 |
| `goodbuddy/tools/list` | Control Plane → Main | 取得 Main 管理的有界 Web 工具与当前 Execute 请求的 MCP 工具 schema |
| `goodbuddy/tools/call` | Control Plane → Main | 校验活动请求、工作模式、参数和精确注册代理身份后调用 Main Web/MCP 工具 |
| `goodbuddy/native/snapshot` | Main → Control Plane | 从无 Agent scope 的 Host Registry 读取有界的原生 Tool/Skill 元数据,排除 GoodBuddy 分配项与请求级代理 |
| `goodbuddy/shutdown` | Main → Control Plane | 停止接收新请求并有序清理 |
Utility 启动控制协议使用版本 2,严格携带 `supportsImageInput` 与固定 8 MiB 帧上限;版本 1 或缺少该字段的启动消息失败关闭,不能让 Host 自行猜测模型能力。扩展版本独立于 ACP 版本。握手响应至少包含:
```ts
type GoodBuddyHarnessCapabilities = {
controlProtocolVersion: 1
harnessVersion: string
acpProtocolVersion: number
supports: {
cancellation: true
sessionRelease: true
reasoningEvents: boolean
toolEvents: boolean
usageEvents: boolean
credentialResolution: true
}
execution: {
mode: 'host'
}
}
```
版本不兼容、必需能力缺失或 `execution.mode` 不是 `host` 时,Main 不得开始模型请求。
### 8.4 每轮权限准备
GoodBuddy 的工作模式属于每个请求,不属于 Runtime 进程全局状态。同一对话可以在 Ask 和 Execute 之间切换。因此:
1. `session/new` 后默认是 Ask。
2. 每个 Prompt 前,Main 发送一次 `goodbuddy/session/prepare`。
3. Harness Control Plane 将准备状态绑定到 `sessionId + requestId`。
4. `session/prompt` 只能消费匹配且尚未使用的准备状态。
5. 缺少准备状态、重复使用、请求标识不匹配时,Control Plane 直接拒绝请求。
6. 同一 Session 只允许一个 Prompt 在途。
### 8.5 事件模型
Harness Control Plane 只发送 GoodBuddy 能稳定解释的字段:
- `status`:简短运行状态。
- `text`:已提交的助手文本分片。
- `reasoning`:可选的有界推理摘要分片。
- `tool`:工具 ID、名称、状态和有界输入/输出摘要。
- `model-usage`:模型、提供方、输入、输出和缓存 Token。
- `done`:停止原因和 Session ID。
禁止发送原始 Cordis Context、完整环境、内部对象、堆栈中的凭据或无界 Session 日志。
## 9. Runtime 生命周期
### 9.1 进程模型
- 每个活动的 DeepSeek Harness Runtime 实例拥有一个 `utilityProcess`。
- 一个进程可以承载多个 Harness Session。
- 同一 GoodBuddy 对话的 Prompt 串行执行。
- 不同对话可以并行,但受全局并发上限控制。
- Runtime 设置变化时创建新实例,旧实例等待在途请求结束或在宽限期后被取消。
### 9.2 会话映射
Main 保存内存映射:
```text
GoodBuddy conversationId -> Harness sessionId + process generation
```
- 首次请求创建 Session。
- 已有 Session 只发送当前 Prompt。
- 进程重启或映射失效时,创建新 Session,并只在这一次加入 GoodBuddy 提供的有界历史。
- 历史以明确的“不可信会话数据”结构传入,不能拼接成系统指令。
- 用户分配的 Skill 只通过 Main 校验的包路径进入 Host,并在 Agent scope 注册;不得把 Skill 内容伪装成用户 Prompt。
### 9.3 取消与有界控制操作
- 用户取消时立即发送 `session/cancel`。
- 取消等待有界,超时后关闭连接并终止整个 Harness 进程。
- 初始化、握手、请求准备、权限回传和关闭分别使用独立控制超时。
- 生产 Prompt 不设置固定墙钟总时限,由 Harness 自身协议终态或用户取消结束。测试可以
显式注入短 Prompt 时限验证取消传播,超时错误与用户取消仍不能被宽泛 catch 抹平。
- 取消后仍可接收并丢弃该请求的最终协议结算帧,但不得写入下一请求。
### 9.4 释放与退出
- 原生能力清单通过一次性 Runtime 探测,取得有界快照后立即 dispose,不得因浏览设置或切换项目把 Host 缓存在执行 Runtime 池中。
- 删除或释放对话时调用 `goodbuddy/session/release`。
- Runtime dispose 时先拒绝新请求,再取消所有 Session。
- Harness Control Plane 完成 Agent、工具和会话清理,Host 完成 Cordis Fiber 与子进程的反向清理。
- Main 在宽限期内等待正常退出。
- 超时后终止 utilityProcess,并在平台允许时清理完整进程树。
- 应用退出时中止正在运行的 npm 插件安装并终止其完整进程树,不能让 lifecycle script 在 GoodBuddy 退出后继续运行。
- 应用退出不得因 Harness 清理无限阻塞。
## 10. 权限与主机执行
### 10.1 模式映射
| GoodBuddy 模式 | Host 内置与插件工具 | Main Web Search/Fetch | GoodBuddy 自定义 MCP | 行为 |
| --- | --- | --- | --- | --- |
| Ask | 只允许 Host Registry 中真实注册的 `read` 与 `skill`;其他 Host/插件工具一律拒绝 | 能力启用时注册 Main 的精确代理对象,无逐次审批 | 不注册 | 模型工具调用保持只读 |
| Execute | 放行 Host 中全部已注册的内置与插件工具 | 能力启用时注册 Main 代理 | 按分配注册并经过既有 RuntimeAuthorizer | 不增加插件权限层,以当前用户权限运行 |
### 10.2 Ask 模式
- Harness Control Plane 在 `tools/execute` 分发边界识别当前 Session 和在途请求。
- 采用所有权感知的只读允许列表,只接受 Registry 中真实的 `read`、`skill` 和 Main 注册的 Web 代理对象;`write`、`edit`、Shell、MCP 及任意新插件工具默认拒绝。只比较工具名不足以授权,插件注册同名工具仍会被拒绝。
- Ask 不注册 Main 代理的 MCP 工具。
- Web Search/Fetch 的凭据、传输与结果限制保留在 Main;Utility 只看到有界 schema 和结果。
- 只读不等于无限输出,读取仍受字节和工具结果上限控制。
- 插件安装脚本和 Cordis 初始化生命周期不经过 `tools/execute`。Ask 不能把已启用第三方代码变成沙箱,也不能保证第三方代码没有启动副作用。
### 10.3 Execute 模式
- 工作区来自 Session 创建时的规范化绝对路径,并作为文件与命令工具的默认工作目录。
- DSH 本地 Filesystem、Bash 或 PowerShell Provider 直接使用 GoodBuddy 客户端当前用户的 OS 权限。
- 工作区不是 containment 边界;绝对路径和命令可访问当前用户本来有权访问的主机资源。
- 已启用插件注册的工具与内置工具使用同一分发路径;GoodBuddy 不增加插件权限矩阵或逐工具确认。
- Main 代理的 MCP 工具继续执行分配、schema、活动请求、模式和 RuntimeAuthorizer 校验。
- 所有工具调用仍作为活动事件记录;Ask 和 delegation 路径继续固定拒绝。
### 10.4 Runtime OS 沙箱
- GoodBuddy 不加载 DSH 平台 Sandbox Provider,也不执行启动沙箱探测。
- “安全与数据”不提供 Runtime OS 沙箱开关。
- 握手明确报告 `execution.mode = 'host'`,状态文案明确说明工具使用当前用户权限。
- Electron Renderer、Preload、Browser Session 等应用安全沙箱不在本设计变更范围内。
### 10.5 环境与凭据
- 使用环境变量白名单构造 utilityProcess 环境。
- 不继承 `NODE_OPTIONS`、调试端口、任意 npm 配置、用户 `DSH_*` 覆盖或白名单之外的凭据。
- `DSH_TELEMETRY_DISABLED=1` 必须固定设置。
- Harness Home 指向 GoodBuddy 管理的隔离目录。
- 不调用官方 `loadEnv` 或 `loadLayeredEnv`。
- API Key 由 Main 从加密设置中解析。
- Harness Control Plane 只能用已握手登记的引用通过 `goodbuddy/credential/resolve` 请求当前 Runtime 的凭据。
- 凭据只在模型请求所需的子进程内存中短暂存在,不写磁盘、不进入工具环境、不打印。
- npm 安装使用同一环境白名单并移除模型 Provider 凭据;安装脚本仍拥有当前用户的文件、进程和网络权限。
## 11. 受控 Harness 组合
首版只加载完成文本对话、受控代码操作和用户明确分配能力所需的固定服务:
- Agent、Session、LLM 和 Tool Registry 基础服务。
- GoodBuddy Harness Control Plane。
- OpenAI 兼容 Chat Completions LLM 适配器。
- 仅在所选模型声明图片能力时挂载的进程内 Attachment Store;它完整解码图片、校验格式/尺寸/摘要,以内容寻址引用保存,并随 Session 或 Host 释放。
- DSH 本地 Subprocess、Filesystem 和平台 Shell Provider。
- Token Meter 和必要的上下文压缩。
- 有界的读取、写入、编辑和 Shell 工具。
- Agent scope 的 Skill Registry 与 `skill` 工具。Skill 目录由 Main 选择并在 Launcher 和 Host 两次规范化、校验。
- Main 代理的 Web 与 MCP schema 工具。Utility 不持有 Web/MCP URL、凭据或 Transport。
- Main 明确传入的第三方 Cordis 插件及其 JSON 配置。
首版明确不加载:
- Web UI、HMR、Host API 和目录选择器。
- Harness 遥测。
- Settings File 和 Local Credentials。
- 用户 profile 与全局补丁。
- Harness 自有 Web Search/Fetch、Utility 直连 Web/MCP、Hooks。
- Subagent、Workflow、Ralph、后台 Job。
- JSONL Session Persistence 和 SQLite Session Query。
- 自动技能发现、任意目录扫描和 profile 市场状态。
如果某个首版工具依赖被排除服务,启动审计必须失败,而不是自动加载更大的默认 bundle。
## 12. 模型配置
### 12.1 配置来源
DeepSeek Harness 首版只使用符合下列边界的 GoodBuddy 模型连接:
- 协议必须是 `openai-chat-completions`。
- 认证必须是 API Key。
- 服务地址支持 HTTP 和 HTTPS,不根据主机位置、端口、凭据、查询参数或片段增加额外限制。
- 模型名称不限制为 DeepSeek 品牌,由所选 OpenAI 兼容服务决定。
- 模型名称和服务地址由 Main 传入受控 Host。
- 图片能力只读取所选 GoodBuddy 模型连接的 `supportsImageInput`;Main、Utility 启动配置、ACP 能力和 Pi-AI 模型输入模态必须使用同一个布尔值。
- API Key 继续保存在 GoodBuddy 加密设置中。
- 启动环境提供的部署连接只由 Main 自动解析,不在 Renderer 中显示为可选来源。
不允许选择 Harness 自有的用户配置文件或自定义 Host。Runtime 始终使用随当前 GoodBuddy 版本发布的内置 Host,并通过完整内部能力握手。
### 12.2 设置变化
模型、凭据、Skill、MCP 分配或插件安装、启停、配置、移除变化时,GoodBuddy 创建新 Runtime 实例。Harness Host 路径始终由当前 GoodBuddy 构建提供,不能由设置或环境变量替换。旧实例按现有 Runtime Controller 语义退役,不在一个活动进程内热替换配置。
### 12.3 输入限制
- 文本始终可用;图片是否可用完全取决于所选模型连接是否显式声明 `supportsImageInput: true`。
- 文本模型收到图片时必须在启动 Host 或发起模型网络调用前返回明确错误,不能静默丢弃图片。
- 图片模型只接受内联 JPEG/PNG,不接受 URL、文件路径、ACP `uri` 或其他媒体类型。
- Main 已通过 `nativeImage` 解码用户选择的图片并生成有界模型输入;Utility 仍须独立执行严格 base64、签名、容器结构、CRC(PNG)、完整解码、尺寸和摘要校验,不能把 Main 校验当作跨进程信任替代。
- 每条消息最多 8 张图,单图编码后最多 1 MiB,图片合计最多 2 MiB,单边最长 8,192 px,单图最多 1,600 万像素,累计解码像素最多 3,200 万(重复引用也计入预算)。进程内 Store 另设 32 MiB、256 个唯一对象的总上限。
- Attachment Store 只服务当前非持久 Harness Session;引用按 Session 释放,Host 退出时清空,不写入磁盘或 GoodBuddy 第二份会话日志。
- GoodBuddy 历史、Prompt、系统指令分别保持不同信任层。
- 任何用户文本都不能进入 Cordis 配置表达式或模块名。
## 13. 输出和资源边界
建议首版默认限制:
| 项目 | 默认上限 |
| --- | --- |
| 单个 JSON-RPC 帧 | 8 MiB |
| 单图 / 单条消息图片 | 1 MiB / 8 张且合计 2 MiB |
| 单图尺寸 / 单条消息解码像素 | 单边最长 8,192 px 且 1,600 万像素 / 3,200 万像素 |
| Host 临时图片存储 | 32 MiB 且最多 256 个唯一对象 |
| 单个文本或推理事件 | 64 KiB |
| 单次请求累计协议输出 | 4 MiB |
| 工具输入摘要 | 4,000 字符 |
| 工具输出摘要 | 4,000 字符 |
| 待处理事件数 | 1,000 |
| stderr 累计 | 64 KiB |
| Host 启动 | 10 秒基础预算 + 每插件 5 秒激活与最多 1 秒失败清理,插件序列最多 91 秒;Main 另预留 2 秒持久化失败状态 |
| ACP 初始化与内部握手 | 每阶段 10 秒 |
| 单次 Prompt | 无固定墙钟总时限,由 Harness 终态或用户取消结束 |
| 有序关闭宽限期 | 2 秒 |
超过限制时应取消当前请求。协议帧、队列或 stderr 持续异常时,应终止 Runtime 进程,避免继续信任已失控的通道。
## 14. Runtime 检测与状态
### 14.1 检测
检测只验证:
- 内置 Host 路径是规范化文件。
- 版本可读取且在支持范围内。
- 内部控制面能力握手成功。
检测不得调用付费模型,也不得读取或输出 API Key。真实模型测试是单独的显式操作。
### 14.2 设置界面
Agent Runtime 使用共享 `SegmentedControl` 展示 OpenCode、Continue 和 DeepSeek Harness。DeepSeek Harness 必须标记为“开发者预览”,并说明上游 RC 可能发生破坏性变更。
Runtime 的概览、模型配置和检测信息放在同一张详情卡中。当前单独显示的一行“已就绪”应移入卡片,与路径、版本号归为同一组:
```text
Runtime: GoodBuddy 内置 DeepSeek Harness
模型配置: 跟随 GoodBuddy · 企业网关(qwen-plus)
状态: 已就绪
路径: <受控 Host 路径>
版本: 0.1.0-rc.8
执行权限: 当前用户权限
Host 始终由当前 GoodBuddy 版本提供,不存在自定义 Host 入口。
```
界面要求:
- 不再在卡片外重复一行检测结果。
- 使用语义化键值结构,路径允许换行,不截断关键信息。
- 状态不能只依靠绿色表达,必须同时有文字。
- 检测中和不可用分别显示明确文案。
- 高级设置默认收起。
- DSH 插件市场提供共享 Switch 样式的总开关并默认关闭。关闭时不请求公共 npm 目录且隐藏市场管理界面,但不修改已有插件的逐项启停状态;因此已启用插件继续随 Host 加载,重新开启后恢复原有管理状态。
- 同一 Runtime 页面提供紧凑的 DSH 插件市场:客户端筛选名称、包名、描述和许可证,已安装插件优先显示。
- 安装前使用一个明确 Checkbox 确认 npm 安装脚本、插件初始化和 Execute 工具均使用当前用户权限;不展示权限矩阵或逐工具审批。
- 已安装插件使用共享 Switch 启停,并提供 JSON 配置、明确移除确认和启动失败信息。
- npm 目录离线时仍显示并允许管理已安装插件;目录错误就地显示并可重试。
- 安装、启停、配置和移除的短期结果通过应用通知显示,不重复保留页内成功提示。
- DSH 不提供独立的“允许图片”开关。Runtime 连接选择只引用“模型连接”中维护的图片能力声明,避免同一模型出现两份冲突配置。
聊天顶栏只显示简短 Runtime 状态,不显示文件路径和版本。完整诊断只在设置页展示。
### 14.3 Agent Runtime 交互表面归属
OpenCode、Continue 和 DeepSeek Harness 的后续能力按操作生命周期放置,不按上游产品分别堆叠入口:
| 表面 | 负责内容 | 不负责内容 |
| --- | --- | --- |
| Composer 通用行 | 附件、语音、知识范围、专家、Ask/Execute、Runtime 和发送 | Session 监督、后台进度、历史任务管理 |
| Composer Runtime 专属行 | 仅对当前消息生效且需要高频选择的 Agent、预设、Prompt/Command 快捷操作 | Task 级委派、后台执行、Workflow/Hook 生命周期 |
| 助手工作栏固定“Runtime”栏目 | 用户所选 Conversation 或 Task 的 Runtime 状态、Task 级委派与取消、后台执行进度/结果、Workflow/Hook 运行、长任务暂停/恢复/终止和会话监督;不显示 Job/Run 树 | 持久模型、程序路径、默认 Agent/预设配置 |
| 设置 > Agent Runtime | 持久 Runtime 配置、默认值、插件管理、能力清单和连接诊断 | 某次活动会话的实时控制 |
Runtime 栏目入口始终存在,并采用统一监督模型;内部再按用户所选目标及其 Runtime 的真实能力
显示 OpenCode、Continue 或 DSH 的具体区域。未支持能力不渲染空卡片或一排禁用按钮,而是
在用户需要理解缺口时显示原因和可执行入口。跟随模式切换 Runtime、Conversation 或 Task
时必须清理上一归属的聚合执行状态,固定目标则保持不变。完整工作栏契约见
[通用助手工作栏与执行空间 PRD](../prd/assistant-experience/assistant-workbar-and-execution-spaces-prd.md)。
所有未来的 Subagent、Job、Workflow、Hook 和会话操作仍须经过 Main 的 Runtime 边界,保留取消、超时、权限、Task/Job/Subjob 层级、用量和活动审计。高风险动作在侧栏就地确认,运行结果进入活动与成果记录,不以 Composer 按钮代替监督面板。DeepSeek Harness 首版仍不加载这些服务,本节只确定未来跨 Runtime 的产品位置和协议归属。
## 15. IPC 与共享契约
共享 schema 需要覆盖:
- `deepseek-harness` provider 和 Runtime ID。
- Runtime 选择中的 `deepseekHarness` 分支。
- 检测结果中的路径、版本、详情和主机执行模式。
- GoodBuddy 模型连接选择。
- 从所选模型连接解析并传到 Host 的 `supportsImageInput`,以及 ACP 图片能力的一致性。
- DeepSeek Harness 模型用量归属。
- Skill 与 MCP 对 `deepseek-harness` 的显式分配。
- 插件市场总开关、目录、已安装状态、启停状态、JSON 配置和有界启动错误。
- 插件 `set-marketplace-enabled`、`install`、`set-enabled`、`configure`、`remove` 五类严格 action。
Renderer 只接收公开 npm 元数据和受管插件状态。任何凭据、完整环境、npm 子进程参数、内部 Host 配置或任意插件文件路径都不能由 Renderer 提交;安装 action 只能引用当前目录中的精确包名与版本。
已有设置迁移必须:
- 对没有新字段的用户使用安全默认值。
- 新建或没有已安装插件的旧 Store 将市场迁移为关闭;已有安装记录的旧 Store 保持开启,避免升级后隐藏用户正在管理的插件。
- 保留 OpenCode、Continue 和模型连接选择。
- 修复失效的 DeepSeek Harness 模型引用时给出可报告的迁移警告。
- 不把旧 Runtime 自动迁移为 DeepSeek Harness。
## 16. 打包与供应链
### 16.1 版本策略
- 官方 RC 包全部精确锁定,不使用 `^` 或 `~`。
- 同一 Harness 核心包族必须保持同一 RC 版本。
- 升级前检查 release diff、协议 diff、工具执行语义和依赖闭包。
- 内部握手同时检查锁定的 Harness 基线和 GoodBuddy 控制协议版本。
### 16.2 插件市场安装
- 目录来自 npm 公共搜索 API,只保留包含精确 `dsh-plugin` 关键字的包,最多读取 1,000 项并短期缓存。
- 安装时再次读取精确版本 packument,不信任搜索结果替代版本清单。
- GoodBuddy 精确锁定并随发布包携带 npm `11.19.0`;Electron 以 `ELECTRON_RUN_AS_NODE=1` 启动该 CLI 和受管 `node` shim,不要求用户另装 Node.js 或 npm。
- npm 使用普通依赖解析并运行包及依赖声明的 lifecycle scripts。安装确认必须准确说明这些脚本以当前用户权限运行。
- 安装在 Store 的暂存目录中完成,校验包名、精确版本、`dsh.bundle` 声明、入口文件和 lockfile integrity 后才原子替换当前版本。
- 市场关闭时拒绝新安装且不请求目录,但 `getEnabledExtensions()` 继续按逐项启停状态返回已安装插件。
- 首次安装默认启用。更新保留既有启停状态和 JSON 配置;失败更新保留原安装。
- 每个插件只有一个受管目录和一条状态记录;Store 同时持久化市场总开关。状态写入原子化且 mutation 串行。
- JSON 配置限制为对象根、64 KiB、16 层、每个容器 256 项和 4,096 个节点,避免 IPC、持久化和 Host 启动载荷无界增长。
- Renderer 不接收受管入口路径;Main 只接受目录中的插件 ID 与精确包版本,不能由 IPC 指定 tarball URL、文件路径或命令。
### 16.3 原生依赖
受控组合可能需要:
- `node-pty`,用于受管理的工具子进程。
- `koffi`,用于本地 Filesystem 在 Windows 上保持文件 ACL 和原子替换。
- `@napi-rs/canvas` 及当前平台二进制,用于在 Utility 内完整解码并复核 JPEG/PNG;原生模块必须从 ASAR 解包并通过目标架构校验。
构建 GoodBuddy 自身时不得广泛批准依赖安装脚本;只允许生产组合实际需要、来源已审查、版本已锁定的脚本。这与用户确认后由市场插件执行自身 lifecycle scripts 是两个不同阶段。六个平台的构建必须验证:
- 对应架构的原生文件存在。
- Electron Utility Process 可加载原生模块。
- spawn helper 的权限正确。
- 包中没有混入其他平台不需要的可执行内容,除非上游包无法拆分且已记录。
### 16.4 生产闭包
发布包包含受控 Host、锁定的 npm 安装 Runtime 和许可证。应尽量避免把 Harness Web profile、HMR 和其他未加载产品面带入生产闭包。若 npm 依赖结构无法拆分,必须:
- 确认这些模块不会被加载。
- 评估它们带来的 audit 和体积风险。
- 在后续上游版本允许时改为最小包族。
- 确认 `tests/fixtures` 以及 Web3D 测试 Skill/MCP 不进入正式发布资源。
### 16.5 漏洞门禁
当前安装后的 `npm audit` 报告不能直接用 `npm audit fix --force` 处理。每项漏洞需要区分:
- GoodBuddy 既有依赖。
- Harness 新增生产依赖。
- 仅开发或打包依赖。
- 未加载但被带入的 Web 依赖。
进入 Harness 执行路径且有可利用条件的高危问题必须在发布前修复、替换或移出生产闭包。
### 16.6 发布验证
`build/build-release.cjs` 需要验证:
- Harness Host 和受控配置存在。
- GoodBuddy Host、内部控制协议与 Harness 依赖版本清单存在。
- 平台原生 PTY/Koffi 依赖架构正确。
- Harness、ACP SDK 和其他新增第三方许可证已打包。
- `app.asar` 外需要执行或动态加载的资源位于预期目录。
- 独立的 npm Runtime 及其捆绑依赖闭包存在,并可通过当前 Electron Node Runtime 启动和执行生命周期脚本。
- Web3D Skill/MCP 等测试 fixture 不在 `app.asar` 或 `extraResources` 中。
## 17. 测试策略
### 17.1 单元测试
- Runtime 选择、设置迁移和失效引用修复。
- 二进制检测、版本解析和路径规范化。
- ACP 握手、事件转换和请求关联。
- 每个会话单请求、跨会话并行。
- Ask 在工具分发边界只允许真实注册的 `read`、`skill` 与 Main Web 代理,并拒绝同名冒充和任意新插件工具;Execute 放行插件工具。
- 握手只接受明确的 `execution.mode = 'host'`。
- 未分配 Skill/MCP 不可见;分配后的 Skill catalog 可调用 `skill` 加载。
- 原生能力快照只包含 Host/插件原生 Skills,不包含 GoodBuddy 分配的 Skills 或 MCP。
- Ask 不注册 MCP 工具;Web 代理可用于 Ask 与 Execute;Execute 每轮刷新有界 MCP schema,并在调用前再次校验活动请求、模式、参数和 RuntimeAuthorizer 结果。
- MCP URL、启动命令和凭据不进入 Utility 启动配置或协议结果。
- 未知授权结果失败关闭。
- 超时、取消、迟到帧和进程意外退出。
- 协议帧、事件队列、工具摘要和 stderr 上限。
- 文本模型在 Host 启动前拒绝图片;图片模型的能力声明、ACP 图片块、Pi-AI 模态和 Attachment Store 保持一致。
- 图片 base64、格式签名、PNG CRC、完整解码、尺寸、单图/单消息/Store 上限、内容摘要、Session 释放和 Host 清空。
- release 和 dispose 的幂等性。
- 状态卡中的状态、路径、版本和当前用户执行权限。
- 插件 action 与目录 schema 接受严格的市场总开关并拒绝权限、回滚、任意路径和非精确版本等未支持字段。
- Store 的原子安装、失败更新保留、串行 mutation、离线管理、配置、移除和启动失败停用。
- npm 分页、精确关键字、捆绑 CLI 调用、lifecycle 参数、包身份、入口和 integrity 校验。
- Renderer 的搜索、权限确认、Switch、JSON 配置、移除确认、离线目录和通知反馈。
### 17.2 本地集成测试
使用无网络的假控制面/模型验证:
- utilityProcess 管道。
- 多 Session。
- Session 释放。
- Runtime 替换。
- 进程树回收。
- 本地 Filesystem 与 Shell Provider 使用规范化工作区作为默认工作目录,且不报告沙箱强制模式。
- 受控配置不会读取工作区 `.env` 和用户 DSH 配置。
- 插件导入或激活失败相互隔离,成功插件继续加载,失败 ID 返回 Main。
- IPC 只接受严格插件 action,可信 Renderer 操作后触发 Runtime 重建。
### 17.3 真实模型测试
真实测试已经获得用户授权,但必须由显式环境门禁启用。Web3D Skill 和 MCP 仅作为 `tests/fixtures` 下的测试资产使用,不属于内置发布能力。至少验证:
1. 文本问答成功,并记录正确 Runtime 和模型用量。
2. Ask 可以读取工作区,但写入被拒绝,且不会弹出权限对话框。
3. 启用 Web Search 后,Ask 可以调用 Main 管理的 `web_search` 与 `web_fetch`,插件同名工具仍被拒绝。
4. Execute 可以在工作区创建测试文件。
5. Execute 工具确实以当前用户权限运行,且状态和握手不宣称 OS 隔离。
6. Ask、delegation 和无活动请求不能绕过工具分发检查。
7. 取消长请求后不再产生文本,并可继续使用其他 Session。
8. 两个 Session 可并行,事件不会串线。
9. 释放会话和关闭应用后没有残留 Harness 或工具进程。
10. 从全新用户设置流程启用一个 3D 游戏 Skill 和实际本地或开放 MCP,工具事件能够证明二者确实被调用。
11. Harness 生成的 3D 游戏项目可以安装、启动和实际游玩,包含 3D 渲染、玩家控制、目标和反馈,浏览器无关键错误。
12. 使用公共 npm 搜索,通过 GoodBuddy 捆绑的 npm 安装经审查的最小第三方插件,Host 成功加载并执行其真实工具。
13. 实际 ACP 路径中 Ask 拒绝该插件工具,Execute 允许该工具,不出现 GoodBuddy 逐工具确认。
测试不得打印、快照或提交 API Key。测试创建的文件只能位于专用临时工作区,并在确认可再现后清理。
### 17.4 项目验证
源码完成后必须运行:
```text
npm test
npm run typecheck
npm run lint
npm run build
```
涉及发布资源后,还要按可用原生平台运行聚焦的 `release:package` 验证。无法在当前主机执行的目标必须由六平台 CI 验证。
## 18. 验收标准
功能只有同时满足以下条件才算完成:
- `deepseek-harness` 可被保存、选择、检测和显示。
- Runtime 详情卡内显示状态、路径、版本和当前用户执行权限。
- Skills 与自定义 MCP 设置可把能力分配给 DeepSeek Harness;请求级 GoodBuddy 内置 MCP 当前不支持分配,并在内置 MCP 卡片中以置灰、未选择状态明确显示。布局、键盘语义、文案和保存回显通过真机检查。
- DSH 市场初始关闭且不加载 npm 目录;显式开启后可以搜索、安装、启停、配置和移除插件,安装前只出现一次准确的当前用户权限确认。关闭市场后已有启用插件继续运行,重新开启后管理状态不变。
- Ask 写入测试在 Runtime 边界失败。
- Ask 可使用已启用的 Main Web Search/Fetch,且插件无法通过同名工具绕过所有权校验。
- Ask 拒绝任意插件工具,Execute 可调用全部已启用插件工具。
- Runtime 原生清单只显示 Host/插件原生 Skills,不显示 GoodBuddy 分配项。
- Execute 工作区内写入成功。
- Runtime OS 沙箱设置、平台 Runner、启动探测和原生沙箱打包产物均不存在。
- 取消、超时、切换 Runtime 和退出应用均能回收进程。
- 多会话不串流、不串权限请求、不串用量。
- 用户 DSH 配置、`.env`、遥测和 Web UI 未被加载。
- 一个插件启动失败时 Host 仍可用,失败插件自动停用并在设置中显示。
- 发布包携带可执行的锁定 npm CLI,安装插件不依赖系统 Node.js/npm。
- API Key 不进入 Renderer、配置文件、日志、错误文本或测试产物。
- 全量测试、类型检查、Lint 和生产构建通过。
- 真实 OpenAI 兼容 Chat Completions 请求成功。
- 真实请求调用已分配 Skill 和 MCP,并生成、启动和实际游玩一个可用的 3D 游戏项目。
- 新增第三方许可证和发布校验完整。
## 19. 已知限制
- DeepSeek Harness 底层库当前是 RC,但 GoodBuddy 不自动跟随升级;每次升级都可能要求同步修改内部控制面。
- Harness 文件和命令工具没有 Runtime OS 隔离,会继承 GoodBuddy 客户端当前用户能够访问的主机资源。
- 首版不恢复 Harness 原生 Session,Runtime 重启后由 GoodBuddy 历史重建。
- 图片输入仅在所选模型连接明确声明支持时可用;首版仍不支持知识库、浏览器控制和 Harness Subagent。Web Search/Fetch 仅使用 Main 代理,MCP 仅支持用户分配、Main 代理和 Execute 自动单次授权路径。
- Harness Subagent、后台 Job、Workflow、Hook 和原生会话监督尚未实现;未来按 Task 聚合到右侧 Runtime 监督栏,不扩张 Composer 工具栏或暴露 Job/Run 层级。
- 推理、工具和用量扩展属于 GoodBuddy 协议,不是标准 ACP 保证。
- 市场来自公共 npm 关键字搜索,不是精选目录;包的质量、兼容性和维护状态由发布者负责。
- 插件安装、初始化、后台生命周期和 Execute 工具使用当前用户权限,不受 Runtime OS 沙箱保护;Ask 只控制模型工具调用。
- 不支持用户 profile、自定义 Host、任意本地插件路径或 profile patch。
## 20. 自维护与升级策略
GoodBuddy 对该 Runtime 采用内部维护策略:
1. 当前通过验证的 Host、控制协议和依赖锁定随 GoodBuddy 一起版本化。
2. 不自动跟随 DSH RC、插件 ABI、profile 格式或市场元数据变化;目录只反映 npm 当前精确版本。
3. 升级前审查实际用户收益、上游 diff、主机工具语义、协议行为、依赖闭包和许可证。
4. 六个平台的单元、假模型、UtilityProcess、主机执行和真实模型门禁全部通过后才能更新基线。
5. 若上游方向不再满足 GoodBuddy 用户需求或安全边界,允许维护兼容补丁、替换单个底层包,或逐步移除 DSH 依赖;`goodbuddy/*` 内部协议保持由 GoodBuddy 控制。
6. GoodBuddy 自身不以进入官方插件目录或服务非 GoodBuddy 客户端为目标;第三方市场兼容仅限当前受测 Cordis 导出和 `dsh.bundle` 声明。
## 21. 备选方案记录
### 21.1 每次调用 `dsh --profile headless`
未采用。它适合一次性任务,但不能满足流式事件、多会话、细粒度取消、权限回传和低延迟复用。
### 21.2 只使用官方 ACP 插件
未采用。取消和一次性权限选择符合需求,但缺少工具、推理、用量和会话释放事件。
### 21.3 只使用官方 SDK JSON-RPC
未采用。事件更完整,但单轮取消和权限回传能力不足。
### 21.4 把全部安全逻辑放进 Harness 子进程
未采用。Harness 子进程属于不可信执行面,不能拥有最终模式授权策略、加密设置和进程回收权限。
### 21.5 把全部控制适配放在 Main
未采用。Main 无法可靠观察 Cordis 内部 Session、Tool、Usage 和权限 seam,只能得到不完整的外部进程行为。
当前选择让双层内部控制面保持 GoodBuddy 私有,同时允许 Main 从受管 Store 向固定 Host 注入标准 Cordis 插件;插件扩展面不会取代 GoodBuddy 的可信 Main 控制权。
@@ -1,944 +0,0 @@
# GoodBuddy 全双工实时语音交互设计
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 文档类型 | 跨功能技术与产品架构 |
| 状态 | 设计中 |
| 版本 | 0.1 |
| 日期 | 2026-08-19 |
| 适用产品 | GoodBuddy 桌面端 |
| 目标平台 | Windows、macOS、Linux,x64 与 arm64 |
| 相关基线 | [跨平台助手产品设计](../design/cross-platform-assistant-product-design.md)、[长期助手路线图](../roadmap/long-term-assistant-roadmap.md)、[统一界面设计系统](../../UI-DESIGN.md) |
本文定义 GoodBuddy 中类似自然通话的全双工实时语音能力,包括本地与云端语音引擎、
音频平面、会话状态、打断语义、工具审批、数据留存、失败恢复、跨平台交付和验收指标。
本文所称“支持本地与云端”是指用户可以显式配置并选择不同语音引擎,不代表系统可以在
它们之间自动切换。**GoodBuddy 不设计静默降级。**
---
## 1. 摘要与核心决策
1. 实时语音是独立的 `VoiceSession`,不把现有一次性语音听写改名后直接复用。
2. 系统支持三种显式引擎:
- 本地模块化全双工:本地流式 ASR、所选 Agent Runtime、本地流式 TTS。
- 本地原生全双工:一个本地端到端语音模型同时听、想和说。
- 云端原生全双工:通过供应商 Realtime/Live API 进行双向流式音频交互。
3. 用户开始会话时冻结引擎、Provider、模型、版本、地域、数据位置、声音、能力和
Turn Detection 配置。会话过程中不得静默替换。
4. 同一目标内允许有界重试、网络抖动恢复和语义等价的内部执行优化;任何会改变
Provider、模型、数据位置、成本、隐私、能力、质量或可感知延迟的替代路径都必须显式。
5. 所选引擎不可用时,会话明确进入 `blocked` 或 `failed`,保留可恢复上下文,并提供
“重试当前引擎”或“结束后选择其他引擎”。不自动切换本地/云端,不退回听写、纯文本或
非全双工模式。
6. Renderer 负责麦克风采集、回声消除、低延迟播放和即时打断;Main 负责凭据、会话
控制、Provider Adapter、工具权限、持久化和资源回收。
7. 音频帧不进入普通 `AgentEvent` 和聊天消息持久化通道。默认只保存最终文本、会话状态和
有界诊断,不保存原始录音。
8. 语音不能成为新的授权通道。Ask 继续只读,Execute 的工具调用继续经过现有审批控件。
---
## 2. 背景与当前基础
GoodBuddy 当前已经具备:
- Renderer 中的麦克风入口、录音状态和取消操作。
- `getUserMedia` 的单声道采集、回声消除和噪声抑制。
- 将完整录音重采样为 16 kHz PCM 的能力。
- 基于 `sherpa-onnx` 的本地离线识别、模型下载、ZIP 迁移、选择和删除。
- Main 中受信任发送者校验、Zod IPC 输入校验、超时、取消和应用关闭回收。
- `AgentRuntime.run()` 的流式文本、工具事件、审批、取消和会话持久化。
- Renderer 中的流式聊天时间线、全局通知和可访问的输入控件。
当前链路仍是:
```text
点击麦克风
→ 最多录音 20 秒
→ 停止并一次性发送完整 PCM
→ 本地离线转写
→ 把文本插入输入框
→ 用户再次确认发送
```
该链路适合听写,但不具备:
- 连续流式识别和临时转写。
- 同时采集与播放。
- 自动轮次检测。
- 助手语音输出。
- 用户抢话和响应截断。
- 音频队列、背压和时钟同步。
- 实时语音 Provider 抽象。
- 语音会话快照和诊断。
因此实时语音必须新增会话层,而不是在现有 `SpeechTranscriptionService` 后面简单追加 TTS。
---
## 3. 目标
### 3.1 用户目标
- 用户可以像通话一样持续说话,不需要每轮点击开始和停止。
- 助手可以边生成边说,并显示与实际播放进度一致的文本。
- 用户开口时可以自然打断,助手在很短时间内停止出声并开始听取新内容。
- 用户始终知道当前使用本地还是云端、具体引擎是什么、音频或转写文本会去哪里。
- 本地或云端引擎失败时,用户能看到准确状态并决定下一步,不被系统暗中换模型。
- 语音对话继续拥有文本聊天中的项目、知识库、角色、Ask/Execute、工具审批和历史能力。
### 3.2 产品目标
- 在六个平台/架构目标上提供统一的上层会话契约。
- 先以现有 `sherpa-onnx` 和 Agent Runtime 构建可跨平台交付的本地模块化引擎。
- 允许云端 Provider 使用 WebRTC 或 WebSocket,但不把供应商协议泄漏到通用 UI。
- 允许高性能设备安装本地原生全双工模型,但按真实能力检测决定是否可选。
- 保持 Main-only 凭据、上下文隔离、沙箱、取消、超时、有界输出和关机回收。
- 为延迟、打断、回声、音频中断、Provider 错误和成本提供可诊断指标。
---
## 4. 非目标
首期不包含:
- 唤醒词、后台常驻监听或应用退出后的麦克风采集。
- 根据网络、负载、价格或“智能判断”自动选择语音引擎。
- 在一个会话内自动从云端切到本地,或从本地切到云端。
- 从原生全双工自动退到 ASR → LLM → TTS,或反向切换。
- 在实时语音失败后自动改成一次性听写、纯文本发送或系统 TTS。
- 默认保存、上传或训练用户原始音频。
- 声音克隆、未成年人声音模仿、电话呼入或多人会议。
- 使用口头“同意”替代工具审批按钮或键盘确认。
- 绕过当前 Agent Runtime 和权限边界的 Provider 直连工具、MCP 或 Connector。
- 保证所有本地原生语音模型都能在 CPU 或全部六个发布目标上运行。
---
## 5. 术语与全双工范围
| 术语 | 定义 |
| --- | --- |
| `VoiceEngineProfile` | 用户保存的语音引擎配置,包含类型、Provider、模型、地域、声音和能力 |
| `VoiceSession` | 一次从用户显式开始到结束的连续实时语音会话 |
| `VoiceTurn` | 用户输入和助手响应形成的一次可持久化对话轮次 |
| 系统级全双工 | 麦克风在助手播放期间继续采集,用户可以随时打断 |
| 原生模型全双工 | 同一个模型联合处理持续输入、轮次判断和持续语音输出 |
| 模块化全双工 | ASR、Agent Runtime 和 TTS 分离,但系统保持同时听说与可打断 |
| 临时文本 | 尚未确认的 ASR 或尚未实际播放的助手文本,不写入长期历史 |
| 已提交文本 | 用户轮次已确认,或助手对应音频已实际播放的文本 |
| Barge-in | 用户在助手说话时开口,触发立即静音、取消和上下文截断 |
| 引擎快照 | 会话开始时冻结的完整、无凭据配置及能力声明 |
“模块化”不等同于“回退”。当用户明确选择模块化本地引擎时,它就是该会话的唯一正式
执行路径。原生模型和模块化引擎之间没有隐式优先级。
---
## 6. 不静默降级产品契约
### 6.1 必须显式的变化
以下变化不得在活动会话中静默发生:
- 本地与云端之间切换。
- Provider、Endpoint、地域或账号切换。
- 模型 ID、模型版本、量化档位或语音角色切换。
- 原生全双工与模块化全双工之间切换。
- ASR、LLM 或 TTS 组件切换。
- 从音频输入改成仅文本输入,或从语音输出改成仅文本输出。
- 禁用原本声明可用的工具、知识库、角色或 Execute 能力后继续运行。
- 把原始音频改为上传,或改变云端数据地域和保留策略。
- 采用明显更慢、更低质量或成本不同的路径。
### 6.2 可自动进行的恢复
以下操作可以自动执行,但必须保持同一引擎快照:
- 同一连接内的丢包恢复、抖动缓冲和音频重排。
- 同一 Provider、模型、地域和配置的有限重连。
- 同一本地模型进程的有限重启。
- 不改变语义、隐私、成本和已声明性能级别的算子或执行 Provider 优化。
恢复在用户可感知前完成时可不打断界面;持续超过 500 ms、导致音频停顿或创建新远端
会话时,必须显示“正在重新连接当前引擎”。所有恢复都进入有界诊断记录。
### 6.3 失败后的用户决策
恢复预算耗尽后:
1. 停止采集上传和音频播放。
2. 将临时文本标记为未提交,不伪装成完整轮次。
3. 保存已提交文本和脱敏错误。
4. 显示当前失败的引擎、影响和建议。
5. 提供“重试当前引擎”和“结束语音会话”。
6. 用户结束后可以显式选择其他引擎并开始新会话。
首期不提供自动 Failover 列表。未来即使允许用户预先配置替代引擎,也必须在切换前获得
明确确认,并在会话中持续显示新的活动引擎。
### 6.4 产品级适用范围
本契约不仅适用于语音。GoodBuddy 中 Provider、模型、Runtime、数据处理位置、工作模式、
权限范围和质量档位等影响隐私、成本或能力的用户选择,都不得被静默替换。
用户明确选择名为“自动”的策略时,系统可以在该策略事先声明的范围内选择,但实际结果和
任何能力退化必须可见、可诊断,不能把空结果或不完整结果表示为正常成功。
---
## 7. 用户体验
### 7.1 入口
现有麦克风入口继续表示“语音输入/听写”,转写进入可编辑输入框,不自动发送。
实时语音使用独立的“开始语音对话”入口,避免用户误以为点击一次听写会开启持续监听。
入口只在以下条件满足时可用:
- 已选择并验证一个全双工语音引擎。
- 当前平台满足该引擎能力要求。
- 麦克风权限可申请。
- 当前 Conversation 没有冲突的活动请求。
- 当前工作模式和引擎能力兼容。
### 7.2 会话界面
活动会话显示一个持续可见的语音控制区:
- 当前状态:准备中、正在听、用户说话、正在思考、助手说话、正在打断、等待审批、
正在重连、失败。
- 本地/云端徽标、引擎名称和数据去向。
- 实时用户转写和与播放同步的助手文本。
- 麦克风静音、结束会话和必要的设备入口。
- 输入音量与助手播放状态,但不得只用颜色表达。
- 云端会话的使用量或成本提示入口。
“结束语音会话”是活动状态下的唯一主操作。波形和头像动效遵守
`prefers-reduced-motion`,关闭动效后仍使用文字和图标表达状态。
### 7.3 打断
助手说话期间检测到用户有效语音:
1. 在 Renderer 立即对当前音频执行 20–40 ms 淡出。
2. 清空尚未播放的音频队列。
3. 向 Main 发送包含播放位置的 `interrupt`。
4. Main 取消当前 Agent/TTS 响应或向 Provider 发送截断事件。
5. 尚未播放的助手文本保持临时状态并从会话上下文中移除。
6. 输入状态切到用户说话,继续采集,不重新建立会话。
键盘点击“停止说话”与语音 Barge-in 使用相同取消和提交语义。
### 7.4 工具和审批
- Ask 模式继续在 Runtime 边界保持只读。
- Execute 模式的工具调用进入现有 Approval Broker。
- 等待审批时暂停新的助手音频,可播放一次确定性的短提示,例如“需要你确认一个操作”。
- 工具参数、风险、范围和确认操作使用现有可访问审批控件。
- 麦克风中的“同意”“确认”或相似内容只作为普通用户文本,不构成授权。
- 用户拒绝或取消后,结果作为结构化工具事件返回当前引擎,不私自换模型继续。
### 7.5 设置结构
长期设置结构使用一级“语音”分类,并以 `PageTabs` 组织:
1. **实时对话**:语音引擎列表、默认引擎、能力、数据位置、地域、声音和真实连接测试。
2. **语音输入**:现有本地 ASR 模型、一次性听写和麦克风设置。
3. **语音输出**:本地 TTS 模型、声音、语速和试听。
当前“模型连接”中的“语音输入”可在迁移阶段保留,之后移动现有模型管理组件时必须保存
已安装模型和选择,不创建第二份设置。
模型类型选择器当前已经包含四项,不增加第五个分段项来承载实时语音,以免违反
`SegmentedControl` 的 2–4 项约束。
语音引擎卡片必须持续显示:
- 本地或云端。
- Provider、模型和版本。
- 支持的语言。
- 系统级或原生模型全双工。
- 是否支持工具、图像和当前 Ask/Execute 模式。
- 所需硬件或云端地域。
- 音频和文本的数据去向。
- 安装、已验证、不可用或需要凭据状态。
实际生成能力只能通过一次真实、有界、由用户触发的会话测试确认。配置保存成功或只完成
握手不能证明麦克风输入、语音输出和打断均可工作。
---
## 8. 总体架构
```text
┌──────────────────────── Renderer ────────────────────────┐
│ VoiceSession UI │
│ getUserMedia → AudioWorklet Capture → Fast VAD │
│ AudioWorklet Playback ← Jitter/Playback Queue │
└────────────── control IPC ─────── media MessagePort ───────┘
│
┌────────────────────────── Main ────────────────────────────┐
│ VoiceSessionController │
│ ├─ Session snapshot and state │
│ ├─ Turn coordinator and interruption │
│ ├─ Tool/approval bridge │
│ ├─ Transcript/message persistence │
│ ├─ Credential and provider policy │
│ └─ VoiceEngineAdapter │
│ ├─ LocalModularAdapter │
│ ├─ LocalNativeDuplexAdapter │
│ └─ CloudRealtimeAdapter │
└───────────────┬──────────────────────┬─────────────────────┘
│ │
Local managed sidecar Cloud Realtime API
or bounded worker WebRTC / WebSocket
```
### 8.1 Renderer 音频平面
Renderer 负责需要接近音频设备的低延迟操作:
- 在用户操作后调用 `getUserMedia`。
- 请求单声道、回声消除、噪声抑制和受支持时的自动增益。
- 使用 `AudioWorklet`,不继续扩展 `ScriptProcessorNode`。
- 将音频切成 10–20 ms 有序帧,并按引擎格式重采样。
- 执行快速本地 VAD,用于 Barge-in,不独立提交最终轮次。
- 维护有界播放和抖动缓冲,记录实际播放采样位置。
- 在打断、设备变化、休眠或窗口销毁时立即静音和释放资源。
Renderer 不持有长期 API Key、不创建本地模型目录、不决定工具权限,也不持久化原始音频。
### 8.2 Preload 与 IPC
控制面使用显式、类型化的 preload 方法:
- `voice.getSnapshot()`
- `voice.startSession(input)`
- `voice.stopSession(sessionId)`
- `voice.setMuted(input)`
- `voice.interrupt(input)`
- `voice.respondApproval(...)` 继续复用现有审批接口
- `voice.onEvent(listener)`
音频帧不使用逐帧 `ipcRenderer.invoke`、JSON 或 Base64。Main 通过
`MessageChannelMain` 向可信主 Frame 传递专用 `MessagePort`,使用可转移
`ArrayBuffer` 和严格的帧头。控制事件和媒体帧分别限速、限长和验证。
### 8.3 Main 控制面
`VoiceSessionController` 负责:
- 每个窗口最多一个活动语音会话。
- 解析并冻结 `VoiceSessionSnapshot`。
- 建立所选 Adapter,不执行自动 Adapter 选择。
- 维护输入、输出和生命周期状态。
- 将 Barge-in 传播到 Provider、Agent Runtime、TTS 和播放队列。
- 桥接工具调用、审批、问题和取消。
- 只提交已经确认或实际播放的文本。
- 处理超时、重连预算、应用退出、系统休眠和窗口销毁。
- 对错误和诊断执行脱敏与边界限制。
### 8.4 本地进程边界
轻量 ONNX 能力可以运行在受控 Worker。需要 Python、CUDA、Metal/MLX 或独立依赖树的
原生模型运行在 GoodBuddy 管理的 Sidecar:
- 只绑定 loopback,不监听外部网卡。
- 使用随机端口和每次启动的短期认证值。
- 环境变量使用最小 allowlist。
- 不继承云端 Provider 密钥。
- 模型路径由 Main 从受管目录解析,不接受任意相对路径。
- 启动、健康检查、并发、输出、内存、超时和进程树有界。
- 应用退出时终止完整进程树。
Sidecar 不因本地模型启动失败而自行连接云端。
### 8.5 云端连接边界
供应商支持 WebRTC 时优先使用其媒体传输、编解码和抖动能力:
- Main 使用长期凭据创建受限、短时的会话描述或临时凭据。
- Renderer 只接收当前会话需要的短期材料。
- 工具和业务事件优先由 Main sideband 连接处理。
- Provider 不支持 sideband 时,由 Main 拥有 WebSocket,并通过媒体 `MessagePort`
与 Renderer 交换音频。
长期凭据永不进入 Renderer、日志、诊断或会话快照。云端 Profile 必须固定可信 Endpoint、
地域和数据说明,不跟随重定向切换到未声明的主机。
---
## 9. 共享契约
建议新增 `src/shared/voice-contracts.ts`,核心结构如下:
```ts
type VoiceEngineKind =
| 'local-modular'
| 'local-native-duplex'
| 'cloud-native-duplex'
type VoiceComponentRef = {
providerId: string
modelId: string
modelVersion?: string
endpoint?: string
region?: string
accountRef?: string
credentialRef?: string
}
type VoiceEngineProfile = {
id: string
name: string
kind: VoiceEngineKind
locality: 'local' | 'cloud'
voiceId: string
components: {
asr?: VoiceComponentRef
tts?: VoiceComponentRef
nativeDuplex?: VoiceComponentRef
}
dataPath: {
audioDestination:
| { kind: 'device' }
| {
kind: 'provider'
providerId: string
endpoint: string
region?: string
}
transcriptDestination:
| { kind: 'device' }
| {
kind: 'provider'
providerId: string
endpoint: string
region?: string
}
retentionPolicyId?: string
}
capabilities: {
nativeDuplex: boolean
supportsTools: boolean
supportsAsk: boolean
supportsExecute: boolean
inputLanguages: string[]
outputLanguages: string[]
}
}
type VoiceRuntimeSnapshot = {
selection: Exclude<AgentRuntimeSelection, { provider: 'auto' }>
profileRevision?: string
configurationDigest: string
workspacePath: string
}
type VoiceSessionSnapshot = {
sessionId: string
conversationId: string
profile: VoiceEngineProfile
profileRevision: string
engineConfigurationDigest: string
runtime?: VoiceRuntimeSnapshot
workMode: 'ask' | 'execute'
inputFormat: VoiceAudioFormat
outputFormat: VoiceAudioFormat
turnDetection: VoiceTurnDetectionConfig
startedAt: string
}
```
`credentialRef` 和 `accountRef` 是不含凭据正文的稳定引用。Endpoint 写入 Profile 或快照前
必须规范化并删除用户名、密码、查询参数和 Fragment;供应商部署路径仍应保留,以便检测
Endpoint 是否发生变化。Profile 持久化时只引用 Main 加密设置,快照不包含长期或临时
Token。
模块化引擎分别记录 ASR 和 TTS 组件,原生引擎记录 `nativeDuplex` 组件;不能用一个
`modelId` 代表多组件链路。文本 Agent 使用独立 `VoiceRuntimeSnapshot`,记录已解析的明确
Runtime、模型 Profile 修订、配置摘要和工作区。`dataPath` 分别说明原始音频和转写文本
留在设备还是发送到哪个供应商。
### 9.1 Runtime Lease
语音会话不能在每轮请求时重新读取可变的全局 Runtime 设置。启动时必须:
1. 将 `auto` 解析为明确的 Runtime 和模型 Profile,并在会话界面显示实际结果。
2. 根据已解析配置创建或取得一个不可变的 `VoiceRuntimeLease`。
3. Lease 在整个 Voice Session 内引用同一个 Runtime 实例和配置摘要。
4. 全局设置变化只为新请求和新 Voice Session 创建 Runtime,不替换活动 Lease。
5. 用户删除或修改活动 Profile 时,界面说明“下次语音会话生效”;当前 Lease 继续运行。
6. 固定实例无法继续时,当前语音会话明确失败,不能取得新的全局 Runtime 继续。
现有 `AgentRuntimeController` 的可变 `current` Slot 会在 `replace()` 后中断活动请求,因此
不能直接作为长期 Voice Session Lease。实现前必须增加引用计数式 Pin/Lease,或由
`SelectedRuntimeManager` 为会话持有独立 Runtime Slot;会话结束后再
`releaseConversation()` 并释放 Lease。
### 9.2 事件
控制事件至少包括:
- `session-preparing`
- `session-ready`
- `session-reconnecting`
- `input-speech-started`
- `input-transcript-delta`
- `input-transcript-committed`
- `response-started`
- `response-transcript-delta`
- `response-audio-started`
- `response-interrupted`
- `response-completed`
- `approval-required`
- `tool-state`
- `usage`
- `error`
- `session-ended`
音频帧使用独立二进制协议,包含:
- `sessionId`
- `generationId`
- `sequence`
- `timestampSamples`
- `sampleRate`
- `channels`
- `encoding`
- `payload`
帧乱序、重复、跨会话或超过大小上限时直接拒绝,不尝试解释为其他格式。
---
## 10. 状态模型
全双工不能只用一个“正在听/正在说”枚举描述。会话使用三个正交状态:
```text
Lifecycle:
idle → preparing → active ↔ reconnecting → ended
└──────────────→ failed
Input:
muted ↔ listening ↔ speech
Output:
idle → generating → playing → interrupting → idle
```
用户可见状态由三个状态组合得出。合法示例:
- `input=listening + output=playing`:助手说话,同时继续监听。
- `input=speech + output=interrupting`:用户抢话,助手正在停止。
- `lifecycle=reconnecting + input=muted + output=idle`:当前引擎重连,停止上传。
`awaiting-approval` 是运行阻塞原因,不关闭会话;此时输入可以继续听取取消或补充文本,
但不能把口头内容解释成授权。
---
## 11. Turn、文本与播放提交
### 11.1 用户输入
- 流式 ASR Delta 只用于界面。
- Endpoint Detector 确认轮次后产生 committed transcript。
- 空白、纯噪音和低置信度片段不创建用户消息。
- 用户可在提交前通过键盘修正;修正结果而非原始猜测进入 Agent Runtime。
### 11.2 助手输出
模块化 TTS 可能落后于文本生成,因此助手文本分为:
- `generated`:模型已生成,尚未安排播放。
- `queued`:已生成音频,尚未播放。
- `played`:对应音频已从播放时钟确认输出。
助手消息需要区分“用户可见历史”和“下一轮模型上下文”:
- 所有已展示的有界文本和结构化内容都写入可见消息历史。
- 可朗读文本记录 `generated`、`queued`、`played` 边界;中断后的消息标记为
`interrupted`,并保留用户已经看见的内容及已播放边界。
- 下一轮模型上下文只包含 `played` 可朗读文本,以及已经展示的 `visual-only` 内容。
- 尚未播放的可朗读尾部即使曾临时显示,也不回送模型,并在历史中显示“未播完”状态。
代码块、表格、URL、引用和工具结果等不适合逐字朗读的内容使用 `visual-only` Block。它们
一旦完整展示即可进入可见历史和下一轮上下文,不受语音播放边界裁切。这样既不会丢失用户
已经看到的详细成果,也不会让模型误以为用户听到了被打断的语音尾部。
云端 Provider 支持会话截断时,Main 使用实际播放位置截断远端 Conversation Item;
不支持时由 GoodBuddy 在下一轮上下文中只组装 `played` 和已展示的 `visual-only` 部分。
现有消息契约与上下文组装器需要增加对应 Block 状态,不能用删掉完整助手消息来模拟截断。
### 11.3 文本转语音规划
模块化引擎从流式文本中产生可取消的短语块:
- 优先在中文标点、英文句界和自然从句边界提交。
- 首个短语不等待完整回答,以降低首音频延迟。
- URL、Markdown 标记、代码块、表格、引用编号和工具 JSON 不逐字符朗读。
- 不能可靠口述的内容在界面展示,并使用确定性短提示说明“详细内容已显示在对话中”。
- 不调用第二个未选择的模型生成“语音摘要”。
---
## 12. 引擎设计
### 12.1 本地模块化全双工
首个跨平台本地基线复用现有 `sherpa-onnx`:
```text
AudioWorklet
→ Silero/TEN VAD
→ sherpa-onnx OnlineRecognizer
→ selected AgentRuntime
→ deterministic speech text planner
→ sherpa-onnx TTS callback
→ AudioWorklet playback
```
现有 `sherpa-onnx` Node Addon 已提供在线识别、VAD、本地 TTS 和 TTS 音频回调。当前已安装
的 SenseVoice、Paraformer 和 Whisper 目录主要用于离线识别;实时模式需要独立的在线
模型目录和能力声明,不能把离线模型误标成流式模型。
“本地模块化”只保证音频采集、ASR 和 TTS 在本机。中间 Agent Runtime 是否本地取决于
用户明确选择的模型连接:
- 连接到 loopback 本地模型时,完整链路可离线。
- 连接到云端文本模型时,原始音频留在本地,但最终转写文本和 Agent 上下文会发送到
该模型。界面必须明确显示这一数据路径。
不得因当前文本 Runtime 不可用而替换为另一模型连接。
### 12.2 本地原生全双工
本地原生 Adapter 面向 MiniCPM-o、Moshi/PersonaPlex、BayLing-Duplex 等能够持续接收并
生成音频的模型。具体模型接入前必须逐个验证:
- 中文和目标语言质量。
- 真正的持续输入、Barge-in 和 Backchannel,而不只是流式输出。
- 首音频延迟和长期运行内存。
- Windows、macOS、Linux 及 x64/arm64 Runtime 可用性。
- NVIDIA CUDA、Apple Silicon 或 CPU 的真实硬件要求。
- 工具调用、系统指令、上下文长度和取消支持。
- 模型、声音、训练数据与商业分发许可。
- 权重下载、ZIP 迁移、校验和、磁盘占用和卸载。
本地原生模型不作为六平台默认能力。只有能力检测和一次真实会话测试通过后才允许选择。
缺少结构化工具能力的模型可以声明为 Ask-only;Execute 入口必须阻塞并说明原因,不能暗中
调用另一个文本模型补齐工具。
### 12.3 云端原生全双工
云端 Adapter 可以面向 OpenAI Realtime、Gemini Live、Qwen Realtime、Azure Voice Live
等正式配置。每个 Adapter 必须显式声明:
- WebRTC 或 WebSocket 传输。
- 输入输出音频格式。
- VAD、Semantic Turn Detection 和手动提交能力。
- 响应取消、音频截断和实际播放对齐能力。
- 输入与输出转写能力。
- 工具调用和 sideband 控制能力。
- 会话时长、上下文、速率限制和费用。
- 可用地域、数据处理与保留说明。
Provider 配置不使用泛化“OpenAI compatible”推断 Realtime 能力。普通 Chat Completions
Endpoint 不能因为 URL 相似就被标记为实时语音。
---
## 13. 音频处理
### 13.1 采集
- 浏览器设备通常以 44.1 或 48 kHz 采集,不能假定请求值就是实际值。
- 使用 `MediaStreamTrack.getSettings()` 记录实际声道、采样率和回声消除状态。
- AudioWorklet 以原始设备时钟采集,再按引擎要求转换为 16/24/48 kHz。
- 默认单声道 Float32 内部格式,边界处转换为 PCM16、Opus 或 Provider 指定格式。
- 每帧 10–20 ms,带序号和采样时间,不使用墙钟猜测播放位置。
### 13.2 回声与抢话
回声处理使用两层信号:
1. Chromium AEC/NS/AGC 处理后的麦克风流。
2. GoodBuddy 已知的播放活动、播放能量和 VAD 结果。
只有满足最短语音持续时间、能量和回声相关性条件时才触发 Barge-in。阈值必须可测试,
不能仅依赖一个 Provider 的 `speech_started` 事件。Provider 事件作为权威轮次信号之一,
本地快速 VAD 负责先静音。
### 13.3 播放与背压
- 每个响应使用独立 `generationId`,旧响应帧不得进入新队列。
- 播放队列按采样时钟排序,禁止无限积压。
- 达到高水位时对上游施加背压;无法背压的 Provider 丢弃会话并报告协议错误,不能持续
增长内存。
- 音频缺口使用短静音或 Provider 编解码恢复,不重复上一段语音。
- 切换输出设备、设备丢失或系统休眠时暂停提交时钟,避免把未播放文本标记为已听到。
---
## 14. 数据与持久化
### 14.1 默认保存
- Voice Session ID、Conversation ID 和时间。
- 无凭据的引擎快照及其摘要哈希。
- 最终用户文本、已展示的助手消息、`visual-only` Block、实际播放边界和中断状态。
- 中断、失败、取消和完成状态。
- 有界延迟、音频中断和用量指标。
- 工具与审批事件继续进入现有任务和活动记录。
### 14.2 默认不保存
- 原始麦克风音频。
- Provider 返回但尚未播放的音频。
- 临时 ASR Delta。
- 长期或临时 API Key、Cookie、会话 Token。
- Provider 原始错误正文和可能包含用户内容的网络帧。
- 回声参考信号、设备唯一标识和完整声学特征。
未来若提供录音留存,必须是独立、默认关闭的功能,说明保存位置、期限、大小、导出和删除,
并与“改进模型”授权分离。
### 14.3 崩溃恢复
应用启动时将未结束的 Voice Session 标记为 `interrupted`。恢复文本 Conversation,
但不自动重新打开麦克风、不自动连接 Provider,也不重播未完成音频。
---
## 15. 错误、重连与资源回收
| 场景 | 行为 |
| --- | --- |
| 麦克风权限拒绝 | 阻塞启动,保留引擎选择,提供系统权限说明 |
| 输入/输出设备消失 | 立即静音或暂停,要求用户处理设备,不改用未选择设备 |
| 本地模型缺失或损坏 | 阻塞启动,进入模型管理,不连接云端 |
| 本地 Runtime 启动失败 | 在有界预算内重启同一 Runtime,之后明确失败 |
| 云端认证或地域错误 | 明确失败,保留配置,不尝试其他 Provider/地域 |
| 短暂断网 | 同一引擎有界重连,超过 500 ms 显示状态 |
| Provider 限流或余额不足 | 结束生成并显示原因,不切本地模型 |
| Agent Runtime 失败 | 终止当前轮次,允许重试同一 Runtime,不换连接 |
| TTS 失败 | 当前轮次失败,不静默改成系统 TTS 或仅文本成功 |
| 工具等待审批 | 暂停响应,保留会话;拒绝后把结果返回当前引擎 |
| 应用退出/窗口销毁 | 取消请求、停止 Track、关闭 Port/PeerConnection、终止 Sidecar |
| 系统休眠/锁屏 | 停止采集和上传;恢复后要求用户显式继续 |
每个会话必须有最大时长、最大连续无声时间、最大媒体队列、最大临时文本、最大重连次数和
最大诊断大小。取消优先于重连和重试。
---
## 16. 安全与隐私
1. 只允许可信主窗口主 Frame 创建和控制 Voice Session。
2. 麦克风权限只放行音频,不因实时语音放开视频。
3. 任何音频采集都需要用户操作;活动期间持续显示应用内状态和系统麦克风指示。
4. 云端会话在开始前显示 Provider、地域、发送内容和可能费用。
5. API Key 只在 Main 的加密设置或受控环境变量中使用。
6. 临时 Provider 凭据具有最短可行期限、最小能力和单会话作用域。
7. Provider 工具调用必须回到 Main 的白名单、Schema、Ask/Execute 和审批边界。
8. 本地 Sidecar 只监听 loopback,使用短期认证,不开放外部端口。
9. 模型权重按受信任目录、固定来源、大小和 SHA-256 校验,导入 ZIP 防止路径穿越和压缩炸弹。
10. 日志只记录状态、耗时、错误分类和匿名引擎 ID,不记录语音正文和音频。
11. 窗口隐藏时若会话仍活动,托盘必须持续显示麦克风状态和停止入口;首期可以选择隐藏即
暂停,但不能隐藏后无提示继续采集。
12. Voice Session 不扩大项目、知识库、文件、浏览器或桌面控制范围。
---
## 17. 性能与质量指标
### 17.1 交互指标
| 指标 | 目标 |
| --- | --- |
| 用户开口到本地 VAD 检出 | P95 ≤ 100 ms |
| Barge-in 检出到扬声器静音 | P95 ≤ 150 ms |
| 播放队列常态深度 | 100–400 ms |
| 用户轮次结束到临时文本稳定 | P50 ≤ 300 ms |
| 用户轮次结束到首段助手音频 | 云端/原生引擎 P50 ≤ 800 ms;模块化引擎 P50 ≤ 1,200 ms |
| 已提交文本与实际播放偏差 | ≤ 100 ms 或一个最小短语块 |
| 连续 30 分钟会话 | 无未界定内存增长、重复播放或资源泄漏 |
本地指标必须注明测试硬件,不能把高端 GPU 结果宣传为 CPU 基线。未达到所选引擎声明的
实时系数时,能力检测应标记为不满足实时要求,而不是静默切到更小模型。
### 17.2 质量指标
- 中文普通话、英文和中英混合词的 ASR 错误率。
- 长停顿、语气词、短回答和自我修正的轮次准确率。
- 扬声器回声、键盘声、音乐和旁人说话下的误打断率。
- 真正用户抢话的漏检率和停止延迟。
- TTS 首段延迟、断句、数字、日期、英文缩写和代码术语可懂度。
- 中断后下一轮上下文不包含未播放内容。
- Provider、模型、数据位置和能力从不发生未声明变化。
---
## 18. 测试策略
### 18.1 自动化
- Voice Contract Schema、大小边界和迁移测试。
- 三组正交状态及非法状态组合测试。
- 有序、乱序、重复、迟到和跨 Session 音频帧测试。
- Barge-in 对播放、Provider、Agent、TTS 和持久化的取消传播测试。
- 临时文本、已提交文本和播放位置对齐测试。
- 同一引擎重连预算与超时测试。
- “禁止静默降级”矩阵测试:任何 Adapter、Provider、模型、地域或 Runtime 变化都必须失败。
- Ask 只读和 Execute 审批测试。
- 窗口销毁、应用退出、休眠和设备丢失的资源释放测试。
- 不持久化音频、临时 Token 和 Provider 原始正文的数据库测试。
### 18.2 模拟与声学测试
建立确定性 Fake Voice Engine,能够注入:
- 固定节奏的输入、文本和音频。
- 网络抖动、丢包、重复和断开。
- 超前文本、迟到音频和错误播放位置。
- 用户抢话、回声、短噪音和长停顿。
- 工具调用、审批、拒绝和取消。
真实声学测试使用预录双声道夹具,一路作为助手扬声器参考,一路作为用户麦克风输入。
不能只通过静态单段 WAV 验证全双工。
### 18.3 手动与外部调用
- 六个发布目标分别验证麦克风权限、采集、播放、设备拔插和应用退出。
- 本地模型在声明的最低硬件上完成 30 分钟稳定性和实时系数测试。
- 云端 Provider 测试会产生外部调用和费用,只在明确授权的 gated 测试中运行。
- 每个云端 Adapter 至少验证一次真实音频输入、真实音频输出、打断和工具审批。
- 真实测试失败时不使用配置握手成功替代生成验证。
---
## 19. 跨平台交付
### 19.1 基线
- 本地模块化引擎作为 Windows、macOS、Linux x64/arm64 的统一功能基线。
- 在线 ASR、VAD 和轻量 TTS 权重不内置,继续使用按需下载和 ZIP 离线迁移。
- GoodBuddy 托管模型的下载遵守
[平台功能页签与模型下载源设计](./model-download-source-design.md),使用用户显式选择的
ModelScope 或 Hugging Face,失败时不切换来源。
- 云端 Adapter 在六个平台复用同一契约,并分别验证 Electron WebRTC/WebSocket 行为。
- 本地原生引擎按 Adapter 声明平台与硬件,不伪装成全平台能力。
### 19.2 硬件能力等级
| 等级 | 目标 |
| --- | --- |
| CPU 基线 | 本地模块化 ASR/TTS;文本 Runtime 可以本地或云端 |
| Apple Silicon | 可增加 MLX/Metal 本地原生 Adapter,必须单独验证 |
| NVIDIA GPU | 可增加 CUDA 本地原生 Adapter,按显存和驱动验证 |
| 不满足要求 | 引擎卡片显示不可用与原因,不自动选择其他引擎 |
安装包继续保持轻量。大模型权重、CUDA Runtime 和独立 Python 环境不得无条件加入全部
发布包。
---
## 20. 分阶段实施
### 阶段 0:契约与模拟器
- 新增 Voice Contracts、状态机和 Fake Voice Engine。
- 建立禁止静默降级测试矩阵。
- 建立会话快照、事件和诊断结构。
### 阶段 1:Renderer 音频平面
- AudioWorklet 采集与播放。
- 媒体 `MessagePort`、背压和播放时钟。
- 快速 VAD、回声关联、Barge-in 和设备生命周期。
- 实时语音控制区和可访问状态。
### 阶段 2:本地模块化基线
- 在线 ASR 和 VAD 模型管理。
- Agent Runtime 流式文本桥。
- 本地 TTS 模型管理、短语规划、音频回调和取消。
- 最终文本持久化与工具审批。
- 六个平台/架构验证。
### 阶段 3:首个云端原生 Adapter
- Main-only 凭据和引擎 Profile。
- WebRTC 或 WebSocket 会话。
- 转写、音频、截断、用量和 Provider 错误。
- sideband 工具与审批。
- 真实有费用的 gated 验证。
### 阶段 4:本地原生全双工 Adapter
- 选择一个中文质量、许可和硬件要求已验证的模型。
- 建立受管 Sidecar、能力检测和真实会话测试。
- 验证原生 Barge-in、文本提交、工具能力和长期稳定性。
### 阶段 5:扩展与质量
- 增加经过验证的云端和本地 Adapter。
- 输出设备选择和企业语音策略。
- 声学基准、延迟仪表盘和成本诊断。
- 评估是否允许用户预配置仍需确认的显式替代策略。
---
## 21. 首个垂直切片
首个可合并实现应使用 Fake Voice Engine,不立即绑定某个云端 Provider:
1. 用户显式开始会话。
2. AudioWorklet 持续采集和播放模拟流。
3. Fake Engine 产生临时转写、助手文本和音频。
4. 用户开口触发 150 ms 内静音和响应取消。
5. 持久化已提交用户文本、已展示助手内容、`visual-only` Block 和实际播放边界;下一轮
模型上下文只使用已播放文本与完整显示的 `visual-only` Block。
6. 模拟工具审批时暂停语音,拒绝口头授权。
7. 注入 Adapter 失败后明确结束,不切换任何引擎。
8. 关闭窗口后所有 Track、Port、计时器和模拟任务归零。
该切片先验证最难改变的会话、音频、提交和安全契约,再分别接入本地和云端实现。
---
## 22. 验收标准
- 用户可以明确选择本地模块化、本地原生或云端原生引擎,界面持续显示当前选择。
- 会话快照冻结 Provider、模型、地域、声音、数据位置和能力。
- 任何引擎、Provider、模型、地域、Runtime 或模式变化都不能在测试中静默发生。
- 助手播放期间继续采集麦克风,用户可在 P95 150 ms 内打断。
- 中断后未播放音频与文本不进入下一轮上下文。
- Ask 和 Execute 在语音中与文本中使用同一权限和审批边界。
- 语音口令不能批准工具。
- 云端长期凭据不进入 Renderer,本地 Sidecar 不监听外部地址。
- 默认数据库、日志和 Artifact 中没有原始音频。
- 本地引擎失败不连接云端,云端引擎失败不启动本地模型。
- 重连只针对同一引擎快照,并在可感知时显示状态。
- 六个平台目标完成各自声明能力的真实采集、播放、取消和资源回收验证。
- `npm test`、`npm run typecheck`、`npm run lint` 和生产构建全部通过。
---
## 23. 参考
- [sherpa-onnx](https://github.com/k2-fsa/sherpa-onnx):本地在线/离线 ASR、VAD 与 TTS。
- [OpenAI Realtime](https://developers.openai.com/api/docs/guides/realtime):云端实时音频会话与 WebRTC/WebSocket。
- [Gemini Live API](https://ai.google.dev/gemini-api/docs/live-api):云端双向实时音频与多模态会话。
- [Qwen Realtime](https://help.aliyun.com/zh/model-studio/realtime):云端实时音视频输入与音频/文本输出。
- [MiniCPM-o](https://github.com/OpenBMB/MiniCPM-V):本地端到端多模态与全双工候选。
- [Moshi](https://github.com/kyutai-labs/moshi):本地原生全双工语音模型框架。
- [PersonaPlex](https://github.com/NVIDIA/personaplex):本地可控角色与声音的全双工候选。
- [AudioWorklet](https://developer.mozilla.org/en-US/docs/Web/API/AudioWorklet):Renderer 低延迟音频处理基础。
@@ -1,710 +0,0 @@
# GoodBuddy 应用内反馈系统设计与对接
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 文档类型 | 跨功能产品与技术架构 |
| 状态 | 已实施并完成生产联调,待发布 |
| 版本 | 1.0 |
| 日期 | 2026-08-25 |
| 适用产品 | GoodBuddy 桌面端 |
| 目标平台 | Windows、macOS、Linux,x64 与 arm64 |
| 相关基线 | [统一界面设计系统](../../UI-DESIGN.md)、[GoodBuddy 文档导航](../README.md) |
本文定义 GoodBuddy 桌面端的应用内反馈入口、数据边界、Renderer / Preload /
Main 进程职责、公共 API 对接、失败处理和验收方法。反馈平台服务端、管理后台、
数据库、附件存储、部署和运营流程由独立项目维护,不在本文复制其内部设计。
本文是 **GoodBuddy 客户端实现的权威设计**。服务端字段、状态码和限制以反馈平台
发布的版本化公共 API 契约为准;两边的同名契约必须通过兼容性测试保持一致,不能各自
扩展后继续沿用相同的 `schemaVersion`。
---
## 1. 摘要与核心决策
1. 用户在“设置 → 关于与更新”内直接提交问题、功能建议、体验意见或其他反馈,不再跳转
GitHub Issue 页面。
2. Renderer 只负责表单、截图预览和状态展示。所有网络请求、安装标识、应用版本和系统
环境组装都在 Main 中完成。
3. Preload 只暴露用途明确的 `feedback.submit()` 方法,不暴露任意 URL、通用
`fetch`、Electron API、文件路径或服务端管理能力。
4. 公共请求固定使用版本化接口 `POST /api/v1/feedback`,GoodBuddy 的
`productKey` 固定为 `goodbuddy`。
5. 反馈服务的生产 HTTPS Origin 是客户端源代码中的受信产品常量,不是用户设置,不从
Renderer、项目、Runtime、环境变量、远程配置或服务端重定向中获取。
6. GoodBuddy 当前的全局 TLS 策略允许模型和内网服务使用自签名或过期证书。反馈可能包含
用户输入和截图,因此 **反馈客户端不得复用全局不校验证书的 Dispatcher 或
`globalThis.fetch`**,必须使用独立的 Undici Dispatcher 并显式启用正常证书校验。
7. 首版允许一张可选截图。截图只能由用户主动选择或粘贴,发送前必须预览并允许移除;
GoodBuddy 不自动截屏。原始输入和 PNG 标准化结果都不能超过 5 MiB。
8. Main 在发送前解码并重编码截图为 PNG,确保原始 EXIF/XMP 等元数据不会离开设备;
服务端仍独立执行自己的图片验证和标准化。
9. 一次新反馈生成一个 `clientRequestId`。网络失败后的手动重试必须复用原 ID,服务端
以此保证幂等;创建另一条反馈时才生成新 ID。
10. GoodBuddy 使用单独的随机安装 UUID 做服务端限流。该 UUID 不得复用 Agent
installation ID、硬件 ID、机器名、用户名、项目 ID 或任何设备指纹。
11. 默认不发送对话、提示词、模型回复、诊断、日志、文件、项目或工作区路径、知识库、模型
配置、Runtime 配置、API Key、Token、Cookie、SSH 信息、剪贴板或屏幕内容。用户可通过
默认关闭的 Checkbox 主动附加有界的最近桌面诊断摘要。
12. 反馈提交失败时保留全部表单内容和截图,成功后显示可复制的服务端反馈编号。
13. 首版不提供反馈历史、状态查询、编辑、撤回、自动日志收集或服务端管理入口。
14. 生产接口固定为 `https://imp.mesalogo.com/api/v1/feedback`。域名健康检查、
TLS、v1 请求校验和客户端调用链必须在发布前一起通过。
---
## 2. 目标与非目标
### 2.1 用户目标
- 不离开 GoodBuddy 即可报告问题或提出建议。
- 在发送前明确知道哪些字段会离开设备。
- 只在主动选择后上传截图,并能检查、替换或移除截图。
- 网络失败后无需重新填写内容。
- 提交成功后获得稳定编号,便于后续沟通。
### 2.2 产品目标
- 让 GoodBuddy 成为独立多产品反馈平台的第一个客户端。
- 保持 Electron 上下文隔离和 Main-only 网络边界。
- 使用小而稳定的版本化契约,不把服务端工单、数据库和管理员概念泄漏到客户端。
- 对 Windows、macOS、Linux 的 x64 与 arm64 使用相同行为。
- 以数据最小化为默认,不把应用内反馈演变为隐式遥测或日志上传。
### 2.3 非目标
首版不包含:
- GitHub Issue 创建、GitHub Token 或 GitHub 网页附件上传。
- GoodBuddy 内的管理员后台、处理状态修改或内部备注。
- 用户账户、登录、反馈列表、远程状态查询或推送通知。
- 自动附加当前对话、桌面诊断、日志、错误堆栈、项目、文件、模型或 Runtime 信息。
- 远端 Agent 日志或通用日志、诊断包和附件框架。
- 自动截取全屏、当前窗口、浏览器、远程桌面或其他应用窗口。
- 多截图、视频、录屏、任意文件附件或压缩包。
- 离线队列、后台自动重试、指数退避、跨重启草稿或发送历史。
- 用户可编辑的反馈 Endpoint、证书豁免、代理凭据或服务端 API Key。
- 由 GoodBuddy 推断优先级、自动分类、自动生成描述或代表用户提交。
---
## 3. 用户体验
### 3.1 入口
入口放在现有“关于与更新”页面,不增加新的设置一级分类。
在当前版本与更新卡片之后增加“帮助改进 GoodBuddy”能力卡:
- 标题:`帮助改进 GoodBuddy`
- 说明:`报告问题、提出建议或反馈使用体验,我们会在独立反馈系统中处理。`
- 主操作:`提交反馈`
该卡片不显示反馈平台内部状态、管理员信息或 GitHub 链接。服务不可用时仍允许用户打开
表单和保留输入;提交失败后提供重试,而不是把用户重定向到另一个渠道。
### 3.2 反馈对话框
对话框字段按以下顺序排列:
1. **反馈类型**,必填:
- 问题:`bug`
- 功能建议:`feature`
- 使用体验:`experience`
- 其他:`other`
2. **标题**,必填,1 至 120 字符。
3. **详细描述**,必填,去除首尾空白后 10 至 5,000 字符;选择桌面诊断时为摘要预留空间,
用户描述上限为 3,398 字符。
4. **附加最近桌面诊断记录**,可选的原生 Checkbox,默认关闭。
5. **联系邮箱**,可选,最多 254 字符。
6. **截图**,可选,最多一张,支持选择 PNG/JPEG/WebP 或在对话框中粘贴图片。
7. **将发送的信息**,只读显示应用版本、系统、架构和界面语言。
8. **隐私说明**,紧邻提交操作。
隐私说明使用明确文案:
> 将发送反馈类型、标题、描述、可选邮箱、GoodBuddy 版本、操作系统、架构、界面语言,
> 以及你主动添加的截图。默认不上传桌面诊断;只有勾选后才会把有界诊断摘要追加到描述。
> 不会发送对话、Prompt、凭据、文件内容、路径、Provider 原始响应或远端 Agent 日志。
Checkbox 下方持续说明:诊断摘要只含时间、组件、阶段、稳定错误码、错误类型和固定短消息。
Renderer 不预览、不读取也不接收诊断内容或路径。勾选后详细描述上限为 3,398 字符,为附加
摘要预留 1,600 字符以及分隔换行;超长草稿保留在表单中并显示字段错误,不静默截断。
截图区域必须包含:
- 缩略预览。
- 尺寸和所选文件大小;Main 仍在发送前独立执行 PNG 标准化和大小检查。
- `替换截图` 与 `移除截图`。
- `截图可能包含画面中可见的个人信息,请在发送前检查。`
### 3.3 提交状态
| 状态 | 行为 |
| --- | --- |
| 未填写 | 字段保持可编辑,提交按钮按字段有效性启用 |
| 校验失败 | 在对应字段旁显示错误并把焦点移到第一个无效字段 |
| 提交中 | 禁止重复提交、关闭和修改字段,按钮显示“正在提交…” |
| 网络或服务失败 | 保留字段和截图,在表单内显示可操作错误及“重试” |
| 成功 | 对话框切换为确认面板,显示并允许复制反馈编号 |
提交成功属于必须保留编号的上下文结果,不再同时发送一条重复的全局成功通知。异步失败也
保留在对话框内,因为错误与当前草稿和重试操作直接关联。
### 3.4 可访问性
- 对话框使用 `role="dialog"`、`aria-modal="true"` 和可见标题。
- 复用 `activateModalFocus()` 与 `trapTabFocus()`,打开后聚焦反馈类型,关闭后恢复入口
按钮焦点。
- 非提交状态支持 Escape 关闭;提交中保持对话框打开,直到请求完成或到达有界超时。
- 字段使用原生 `label`、`select`、`input` 和 `textarea`。
- 图片预览提供说明性替代文本,删除和替换按钮有明确可访问名称。
- 加载、失败和成功状态使用文字,不依赖颜色。
- 浅色、深色和 `prefers-reduced-motion` 行为遵循 `UI-DESIGN.md`。
---
## 4. 数据与隐私边界
### 4.1 明确发送的数据
| 数据 | 来源 | 用途 |
| --- | --- | --- |
| `schemaVersion` | Main 常量 | 选择公共契约版本 |
| `productKey` | Main 常量 `goodbuddy` | 服务端产品路由 |
| `category` | 用户选择 | 分流反馈 |
| `title` | 用户输入 | 摘要 |
| `description` | 用户输入;勾选时由 Main 追加桌面诊断摘要 | 问题、建议和主动提供的有界诊断详情 |
| `contactEmail` | 用户可选输入 | 必要时联系用户 |
| `environment.appVersion` | `app.getVersion()` | 判断受影响版本 |
| `environment.platform` | `process.platform` 映射 | 判断受影响平台 |
| `environment.architecture` | `process.arch` 映射 | 判断受影响架构 |
| `environment.locale` | 当前 GoodBuddy UI 语言 | 理解用户语言环境 |
| `installationId` | Main 本地随机 UUID | 服务端安装实例限流 |
| `clientRequestId` | 当前草稿随机 UUID | 幂等和安全重试 |
| `screenshot` | 用户主动选择或粘贴 | 复现视觉问题 |
客户端不主动发送来源 IP;IP 是 HTTPS 请求到达服务端时产生的网络元数据。服务端如何保存、
散列和保留该元数据由独立反馈平台的隐私与运维策略负责。
用户填写联系邮箱后,邮箱会作为业务字段通过 HTTPS 发送并由反馈平台保存,不属于端到端
加密内容。用户不希望提供联系方式时应留空,GoodBuddy 不尝试从系统账户或其他设置推断
邮箱。
### 4.2 默认不发送的数据
以下数据不能因为“有助于复现”而隐式加入请求:
- 当前或历史对话、提示词、模型回复和 Supervisor 内容。
- GoodBuddy、Runtime、Agent、SSH、消息通道、浏览器或操作系统日志。
- 文件内容、附件、知识库、Magic Notes、成果和工作区改动。
- 文件名、项目名、本地路径、远程路径、主机名、用户名或 IP 配置。
- LLM、向量模型、语音模型、OCR、MCP、Skill、Runtime 或通道配置。
- API Key、Access Token、Cookie、SSH 密码、私钥、代理凭据和其他秘密。
- 剪贴板、屏幕、摄像头、麦克风或其他应用内容。
- Remote Agent installation ID、Runtime ID、Conversation ID、Task ID 或 Project ID。
除上述用户主动选择、追加到描述且有界的桌面诊断摘要外,未来若增加日志、诊断包或其他
附件,必须单独设计字段、预览、大小限制、用户选择和服务端保留策略,不能复用截图字段
静默上传。
### 4.3 本地数据生命周期
- 表单草稿、邮箱和截图只保存在当前 Renderer 内存中。
- 首版不把草稿、失败请求、成功响应或截图写入 GoodBuddy 数据库和设置文件。
- Main 只在请求期间持有标准化图片字节,请求结束后释放引用,不创建临时图片文件。
- 成功后关闭确认面板时清空草稿。
- `installationId` 单独保存在用户数据目录中的
`feedback-identity.json`,格式为版本化 JSON,文件权限沿用私有原子设置文件机制。
- “清除本地数据”必须中止进行中的反馈请求并删除该身份文件;下次提交会生成新 UUID。
---
## 5. 公共 API 对接
### 5.1 外部契约所有权
反馈平台负责维护公共 API 的权威定义。GoodBuddy 只固定客户端实际需要的版本 1 子集:
```text
POST https://imp.mesalogo.com/api/v1/feedback
```
当前生产 Endpoint 已作为 Main-only 产品常量固定。实现和发布验证必须持续保证:
- 使用 `https:`。
- URL 不含用户名、密码、查询参数或 fragment。
- Path 精确为 `/api/v1/feedback`。
- 对应产品 `goodbuddy` 已在服务端启用。
- 桌面端所在网络能够直接访问该 Origin。
Renderer、Preload、用户设置、环境变量和远程配置都不能覆盖该 Endpoint。域名迁移必须
同时修改客户端受信常量、测试、服务端 Origin 配置和部署文档。
### 5.2 请求 JSON
无截图时发送 `application/json`:
```json
{
"schemaVersion": 1,
"productKey": "goodbuddy",
"category": "bug",
"title": "魔法笔记页签不可见",
"description": "平台功能中的魔法笔记页签被布局压缩,无法看到或点击。",
"contactEmail": "user@example.com",
"environment": {
"appVersion": "0.11.0",
"platform": "windows",
"architecture": "x64",
"locale": "zh-CN"
},
"installationId": "00000000-0000-4000-8000-000000000101",
"clientRequestId": "00000000-0000-4000-8000-000000000102"
}
```
有截图时发送 `multipart/form-data`:
- `payload`:同一 JSON 的字符串形式。
- `screenshot`:一张标准化 PNG,文件名固定为 `feedback.png`。
`contactEmail` 为空时省略,不发送空字符串。请求对象不附加服务端未声明字段。
### 5.3 成功响应
HTTP 201 表示首次创建,HTTP 200 表示相同 `clientRequestId` 的幂等结果:
```json
{
"reference": "GOODBUDDY-000001",
"duplicate": false
}
```
客户端对两种成功状态使用相同确认界面。`duplicate: true` 不能显示为失败,它表示先前请求
已经成功落库。
### 5.4 错误映射
| HTTP / 错误 | 用户结果 |
| --- | --- |
| 400 | 提示提交内容无效,保留草稿;同时记录为客户端契约缺陷 |
| 403 | 提示客户端与反馈服务配置不匹配 |
| 404 | 提示反馈服务暂不可用 |
| 413 | 提示截图或请求过大,允许移除截图后重试 |
| 429 | 提示提交过于频繁,保留草稿并让用户稍后手动重试 |
| 500–599 | 提示服务暂时异常,保留草稿 |
| 超时 / DNS / TLS / 离线 | 提示无法连接反馈服务,保留草稿 |
| 响应格式错误 | 提示服务返回无效结果,不能猜测成功 |
| 本地诊断读取失败 | 不发送反馈,保留草稿、勾选状态和请求 ID,提示重试或取消勾选 |
首版不自动重试。自动重试容易让用户无法判断是否已经发送,也会在服务持续异常时制造额外
流量。用户点击“重试”时复用同一个 `clientRequestId`。
---
## 6. 进程与模块设计
### 6.1 调用链
```text
UpdateSettingsSection / FeedbackDialog
→ window.goodbuddy.feedback.submit(input)
→ preload: ipcRenderer.invoke(feedbackSubmit)
→ Main IPC: trusted sender + Zod parse
→ FeedbackService: identity + environment + screenshot normalization
→ StrictFeedbackHttpClient
→ POST /api/v1/feedback
→ response schema parse
→ Renderer confirmation or inline retry state
```
### 6.2 Shared
新增 `src/shared/feedback-contracts.ts`,包含:
- `feedbackCategorySchema`
- `feedbackDraftSchema`
- `feedbackScreenshotInputSchema`
- `feedbackSubmitInputSchema`
- `feedbackSubmitResultSchema`
- `feedbackPublicPayloadSchema`
- 公共字段长度和截图限制常量
推荐客户端 IPC 输入:
```ts
type FeedbackSubmitInput = {
category: 'bug' | 'feature' | 'experience' | 'other'
title: string
description: string
includeDiagnostics: boolean
contactEmail?: string
locale: 'zh-CN' | 'en-US'
clientRequestId: string
screenshot?: {
data: Uint8Array
mimeType: 'image/png' | 'image/jpeg' | 'image/webp'
}
}
```
Renderer 不能传入 `productKey`、`appVersion`、`platform`、`architecture` 或
`installationId`,也不能传入诊断内容或路径。这些字段由 Main 从受信运行环境生成。
### 6.3 IPC 与 Preload
新增:
```text
ipcChannels.feedbackSubmit = "feedback:submit"
```
`DesktopApi` 新增必选窄接口:
```ts
feedback: {
submit: (input: FeedbackSubmitInput) => Promise<FeedbackSubmitResult>
}
```
Main handler 必须:
1. 调用现有 `assertTrustedSender(event, window)`。
2. 使用 Shared Zod schema 解析 `unknown` 输入。
3. 不接收 URL、文件路径、Cookie、Header 或任意请求选项。
4. 由 `FeedbackService` 跟踪活动 Promise,应用关闭或清除数据时先取消再有界等待。
Preload 只转发已声明输入,不做网络请求或图片文件读取。
反馈 handler 独立放在 `feedback/feedback-ipc.ts`,复用共享的
`assertTrustedSender()`,避免继续扩展通用 `registerIpcHandlers()` 的位置参数列表。
### 6.4 FeedbackIdentityStore
新增 Main-only `FeedbackIdentityStore`:
- 文件:`<userData>/feedback-identity.json`
- Schema:
```json
{
"version": 1,
"installationId": "00000000-0000-4000-8000-000000000101"
}
```
- 缺失时使用 `crypto.randomUUID()` 创建。
- 使用 `writeJsonFileAtomically()` 写入 0600 临时文件并原子替换。
- 读取时使用严格 schema。
- 损坏时隔离原文件并生成新身份,不读取或复用 Remote Agent 身份。
- 并发首次读取必须合并为同一个 Promise,不能生成两个 UUID。
- `clear()` 删除文件和内存缓存,供“清除本地数据”调用。
该 UUID 是可重置的应用安装级随机标识,不声称代表真实设备或用户。
### 6.5 FeedbackService
Main-only `FeedbackService` 负责:
- 读取或创建安装 UUID。
- 映射应用版本、平台和架构。
- 验证并标准化可选截图。
- 组装严格的公共 payload。
- 仅在 `includeDiagnostics: true` 时通过注入的 DesktopDiagnostics provider 读取最多 20 条
最近记录;默认提交完全不调用诊断 provider。
- 将诊断格式化为带
`[GOODBUDDY_DESKTOP_DIAGNOSTICS_V1_BEGIN]` /
`[GOODBUDDY_DESKTOP_DIAGNOSTICS_V1_END]` 明确分隔、最多 1,600 字符且只保留完整 JSON
行的摘要。无记录时写入固定 `{"status":"no-recent-diagnostics"}` 事实。
- 调用 HTTP Client。
- 维持单次提交的 AbortController,并在 dispose / 清除本地数据时取消。
平台映射固定为:
| Node 值 | API 值 |
| --- | --- |
| `win32` | `windows` |
| `darwin` | `macos` |
| `linux` | `linux` |
| 其他 | `unknown` |
架构只保留 `x64` 和 `arm64`,其他值映射为 `unknown`。
公共 API 继续使用 `schemaVersion: 1` 和原有固定字段。`includeDiagnostics` 只属于客户端
IPC 输入,不进入公共 payload,也不新增 multipart 字段;Main 把摘要追加到
`payload.description`。默认未勾选时,公共 payload 与既有实现逐字段一致。诊断读取失败时
不得发送部分摘要或声称已附加,必须返回可本地化的 `diagnostics-unavailable` 错误。
### 6.6 严格 HTTPS Client
反馈客户端使用 `undici` 的独立 `Agent`:
```ts
new Agent({
connect: {
rejectUnauthorized: true
}
})
```
这是必要边界,不是额外配置。GoodBuddy 的 `GlobalTlsPolicy` 会给模型、MCP 和内网服务
安装不校验证书的全局 Dispatcher;反馈内容不能经过该 Dispatcher。
每次请求必须满足:
- 固定、预先校验的 HTTPS URL。
- 使用 Undici 原始 `request()`,不安装重定向处理器;任何 3xx 都按服务不可用处理。
- 不使用 Cookie Jar、缓存、Referrer 或浏览器凭据模式。
- `Accept: application/json`。
- 固定、非敏感 `User-Agent`,例如 `GoodBuddy-Feedback/<version>`。
- 不发送 `Authorization`、Cookie、API Key 或自定义代理凭据。
- 总超时建议 15 秒,最大不得超过 30 秒。
- 成功响应体最多 16 KiB,并检查流式实际字节数,不能只相信
`Content-Length`。
- 只接受 JSON `Content-Type` 和 JSON 响应体,并使用
`feedbackSubmitResultSchema` 解析。
- 日志只记录有界错误类别和 HTTP 状态,不记录请求正文、邮箱、截图、安装 UUID、
`clientRequestId` 或响应编号。
测试可以注入 Endpoint、Dispatcher 和 Request Transport。生产构造路径不得读取测试覆盖值或
环境变量。
---
## 7. 截图处理
### 7.1 Renderer
- 文件输入限制为 `.png,.jpg,.jpeg,.webp`。
- 原始文件上限固定为 5 MiB,超过后不读取完整内容或调用 IPC。
- 粘贴事件只处理图片,不拦截普通文本粘贴。
- 读取 `File.arrayBuffer()` 后先执行 Shared 字节上限校验。
- 使用临时 Object URL 预览,替换、移除或关闭对话框时立即
`URL.revokeObjectURL()`。
- 不调用 `webUtils.getPathForFile()`,不把本地文件路径传给 Main 或服务端。
### 7.2 Main
Main 不信任 Renderer 声明的 MIME:
1. 校验 PNG、JPEG、WebP 文件签名。
2. 使用 `nativeImage.createFromBuffer()` 实际解码。
3. 拒绝空图片、异常尺寸、超过 API 公开上限的像素数。
4. 使用 `toPNG()` 重编码,去除原始元数据。
5. 检查标准化后字节数不超过 5 MiB,最大边长不超过 8,192 像素,总像素数不超过
3,200 万。
6. 仅把标准化 PNG 放入当前网络请求,不写入磁盘。
客户端和服务端都执行图片验证属于有意的纵深防御:客户端保证原始元数据不离开设备,
服务端保证不能信任任意客户端。
---
## 8. Renderer 实施落点
### 8.1 组件
- `UpdateSettingsSection.tsx`
- 继续负责应用信息和更新卡片。
- 渲染反馈入口卡片。
- `FeedbackDialog.tsx`
- 独立管理草稿、预览、校验、提交、重试和成功编号。
- 关闭后销毁草稿状态。
对话框不与更新检查状态共用 `saving`、`checking` 或 `error`,避免一个功能失败覆盖另一个
功能的状态。
### 8.2 国际化
在 `settingsSections` 的 `zh-CN` 与 `en-US` 资源中增加同构键:
- 入口卡片标题、说明和按钮。
- 类型名称。
- 字段标签、占位、帮助和校验。
- 截图选择、替换、移除和隐私提醒。
- 提交中、失败、重试、成功和复制编号。
- 数据发送说明。
组件不得内嵌只存在于一种语言的产品文案。
### 8.3 样式
- 复用 `capability-card`、`field`、`primary-button`、`secondary-button` 和现有
对话框层级。
- 新样式使用 `UI-DESIGN.md` 的语义颜色、间距、圆角和动效令牌。
- 对话框桌面端最大宽度建议 600px;窄窗口占可用宽度并保持 16px 页面边距。
- 不为反馈入口创建新的一级页面、页签视觉或高饱和营销卡片。
---
## 9. 失败、生命周期与恢复
### 9.1 幂等
- 打开新草稿时生成 `clientRequestId`。
- 字段编辑、截图替换、网络失败和手动重试都不改变 ID。
- 成功或显式丢弃草稿后,下一条反馈生成新 ID。
- 同一 ID 返回原编号时按成功处理。
### 9.2 并发
- 对话框同一时间只允许一个提交。
- Main 同一 `clientRequestId` 只运行一个请求。
- Main 全局同一时间只发送一条反馈;相同 ID 的重复调用合并,不同 ID 在已有请求期间返回
有界 `busy` 结果,不能并行保留多份 5 MiB 截图。
- 读取截图使用选择代次;旧文件后完成时必须丢弃并释放 Object URL,不能覆盖较新的截图。
### 9.3 应用退出与清除数据
- 反馈请求有独立 AbortController。
- 应用退出时取消进行中的请求并关闭严格 TLS Dispatcher。
- “清除本地数据”先取消请求,再删除 `feedback-identity.json`。
- 失败或取消不写入本地“已发送”状态,服务端是否已接收由同 ID 重试确认。
### 9.4 服务不可用
反馈平台不可用时:
- 保留表单和截图。
- 显示明确网络或服务错误。
- 不静默丢弃、不自动切换 GitHub、不打开网页、不自动重试。
- 其他 GoodBuddy 功能继续可用。
---
## 10. 实施文件
当前实现涉及:
| 文件或目录 | 变更 |
| --- | --- |
| `src/shared/feedback-contracts.ts` | 输入、公共 payload、响应和限制 schema |
| `src/shared/ipc-channels.ts` | `feedbackSubmit` channel |
| `src/shared/contracts.ts` | `DesktopApi.feedback` |
| `src/main/feedback/feedback-identity-store.ts` | 随机安装 ID |
| `src/main/feedback/feedback-screenshot.ts` | 签名、解码和 PNG 标准化 |
| `src/main/feedback/feedback-http-client.ts` | 固定 Endpoint、严格 TLS、有界响应 |
| `src/main/feedback/feedback-service.ts` | payload 组装与生命周期 |
| `src/main/feedback/feedback-ipc.ts` | 独立 trusted sender、schema parse 和 submit handler |
| `src/main/trusted-ipc-sender.ts` | Main IPC 共享 sender / main Frame 校验 |
| `src/main/index.ts` | Main service 构造、清理和 IPC 注入 |
| `src/preload/index.ts` | 窄 `feedback.submit()` bridge |
| `src/renderer/src/FeedbackDialog.tsx` | 表单和成功/失败流程 |
| `src/renderer/src/UpdateSettingsSection.tsx` | About 入口卡片 |
| `src/renderer/src/i18n/locales/*/settingsSections.ts` | 中英文文案 |
| `src/renderer/src/styles.css` | 令牌化布局与响应式样式 |
每个生产文件应有相邻或现有测试文件覆盖。不要把 Endpoint、截图处理或网络逻辑塞进
`ipc.ts` 或 React 组件。
---
## 11. 验证
以下 Shared、Main、IPC、Preload 和 Renderer 行为均由相邻或现有自动化测试覆盖;生产
Endpoint 的健康检查和无副作用 schema 探针也必须在发布前通过。
### 11.1 Shared 合同
- 接受四种反馈类型和有效字段。
- 拒绝未知字段、空标题、短描述、超长描述、错误邮箱、非 UUID 和多截图。
- 对截图按字节限制,而不是字符串长度限制。
- 请求与反馈平台 `schemaVersion: 1` fixture 双向兼容。
### 11.2 Main 单元测试
- 安装 ID 首次创建、并发读取、持久化、损坏隔离和清除。
- 不复用 Agent、Runtime、Project 或硬件身份。
- 平台和架构映射。
- PNG/JPEG/WebP 签名、实际解码、尺寸限制、PNG 重编码和元数据不保留。
- 无截图 JSON、有截图 multipart、请求 Header 和固定 URL。
- 独立 Dispatcher 显式 `rejectUnauthorized: true`。
- 拒绝重定向、非 HTTPS、错误 Origin、超时、超大响应、非 JSON 和错误 response schema。
- 4xx、429、5xx 和网络错误映射。
- 手动重试复用 `clientRequestId`。
- 不同 ID 的并发请求被有界拒绝,清除本地数据期间不能开始新提交。
### 11.3 IPC 与 Preload
- 未知窗口和非主 Frame 被拒绝。
- Renderer 不能覆盖 `productKey`、版本、平台、架构、安装 ID、Endpoint 或 Header。
- Preload 只暴露 `feedback.submit()`。
- Sandbox 测试验证没有通用网络或文件路径能力泄漏。
### 11.4 Renderer
- About 页面展示反馈卡片。
- 表单字段、字符限制、中英文文案和隐私说明。
- 普通文本粘贴不被图片处理拦截。
- 截图选择、预览、替换、移除和 Object URL 清理。
- 并发截图读取按最新选择生效,关闭对话框后不会留下待处理 Object URL。
- 失败保留字段、诊断勾选状态,重试使用原 `clientRequestId`。
- 默认提交不读取诊断且公共 payload 不变;主动附加、描述预算、无记录和读取失败均有覆盖。
- 成功显示真实编号并清空后续新草稿。
- Dialog role、初始焦点、Tab 循环、Escape 和焦点恢复。
- 浅色、深色、窄窗口和 reduced motion。
### 11.5 真实集成
2026-08-25 已从隔离配置的真实 Electron 生产构建完成一次端到端合成提交:
- 标题:`[GOODBUDDY INTEGRATION TEST] 2026-08-25`
- 类型:`other`
- 联系邮箱:未填写
- 截图:GoodBuddy 公共应用图标,经 Main 重编码后以 PNG multipart 上传
- 服务端编号:`GOODBUDDY-000001`
该结果确认 About UI、Renderer、Preload、trusted IPC、安装身份、Main 图片处理、独立严格
TLS Dispatcher 和生产 v1 API 调用链可以共同工作。发布候选仍应按以下清单复验:
1. 从打包候选的 About 页面提交一条明确标记的合成反馈。
2. 可选添加一张不含个人数据的测试截图。
3. 确认客户端显示的编号与管理后台一致。
4. 确认版本、平台、架构和语言正确。
5. 确认服务端只保存标准化 PNG,不含原始图片元数据。
6. 断网后提交,确认草稿保留;恢复网络后以同一 ID 重试且只产生一条记录。
7. 使用无效证书的测试服务,确认反馈客户端拒绝连接,即使 GoodBuddy 全局内网 TLS
策略允许其他服务继续工作。
8. 检查 Renderer 和 Main 日志不含正文、邮箱、截图、安装 ID 或凭据。
最终仍需运行仓库统一验证:
```text
npm test
npm run typecheck
npm run lint
npm run build
```
---
## 12. 发布与兼容性
- `schemaVersion: 1` 与 `/api/v1/feedback` 是同一版本边界。
- 向请求增加必填字段、修改枚举或改变字段语义属于破坏性变更,必须发布新 schema 或新
API 版本。
- GoodBuddy 对响应执行严格解析。服务端需要扩展响应时,应先协调客户端兼容策略。
- Endpoint 域名变化属于客户端发布变更,不能依赖 301/302 迁移。
- 入口发布与 `goodbuddy` 产品启用状态、生产 TLS 和 v1 契约保持同一发布门槛。
- 此功能不迁移现有用户数据;只新增可重置的随机反馈安装身份。
- 发布说明应明确:用户现在可以在“关于与更新”内直接提交反馈,邮箱和截图可选;桌面诊断
默认不上传,只有用户勾选后才附加有界摘要,并且不会发送对话、Prompt、凭据、文件内容、
路径、Provider 原始响应或远端 Agent 日志。
---
## 13. 当前对接状态
截至 2026-08-25:
1. 生产 Origin 已确定为 `https://imp.mesalogo.com`,HTTPS、HSTS 和健康检查正常。
2. `/api/v1/feedback` 已返回符合 v1 契约的严格校验错误,无副作用探针未创建记录。
3. `goodbuddy` 产品已启用,实际提交返回 `GOODBUDDY-000001`。
4. GoodBuddy 客户端已完成 Shared、Main、IPC、Preload、About UI、双语文案、截图和
生命周期实现。
5. 服务端公开 schema、字段长度和 5 MiB 截图上限与客户端一致。
6. 隔离 Electron 实例已完成浅色、深色、600×800 窄窗口、焦点与背景隔离、截图预览和
真实生产提交验收。
File diff suppressed because it is too large Load Diff
@@ -1,647 +0,0 @@
# GoodBuddy 本机工具环境设计与实施计划
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 文档类型 | 跨功能技术与产品架构 |
| 状态 | 计划 |
| 版本 | 0.1 |
| 日期 | 2026-08-31 |
| 适用产品 | GoodBuddy 桌面端 |
| 目标平台 | Windows、macOS、Linux,x64 与 arm64 |
| 相关基线 | [DeepSeek Harness Runtime 设计](./deepseek-harness-runtime-design.md)、[SSH 远程主机与 GoodBuddy Agent 实现说明](./remote-host-and-goodbuddy-agent-design.md)、[统一界面设计系统](../../UI-DESIGN.md) |
本文定义 GoodBuddy 为本机 SKILL 脚本和本机 stdio MCP Server 提供 Node.js、Python
解释器的产品形态、设置模型、进程环境、安装生命周期和实施顺序。目标是让普通用户无需
预装开发环境即可使用依赖脚本的能力,同时让专业用户能够明确选择自己维护的解释器。
本文中的“本机工具环境”不等于 Agent Runtime,也不等于 SSH Host 上由 GoodBuddy Agent
管理的远程环境。
---
## 1. 摘要与核心决策
1. 本机工具环境首期只管理 `Node.js` 和 `Python`。
2. 每种工具只有两种互斥来源:
- **GoodBuddy 托管**:由 GoodBuddy 提供、验证和解析。
- **自定义环境**:用户从检测结果中选择,或指定其他可执行文件。
3. 不提供独立的“自动选择”。它会隐藏实际版本,并可能在系统环境变化后静默改变执行结果。
4. 不区分“系统安装”和“指定路径”。二者最终都是由用户明确选择的外部可执行文件,只是
发现方式不同。
5. Node.js 托管环境复用 Electron 内置 Node,通过受管 `node` shim 设置
`ELECTRON_RUN_AS_NODE=1`,不额外打包一份 Node。
6. Python 托管环境不无条件进入桌面安装包。用户首次需要时,GoodBuddy 按平台和架构下载、
校验并安装一份固定 Python 发行包。
7. `npm` 跟随 Node.js 来源,`pip` 跟随 Python 来源,不作为独立设置项。
8. 首期不实现每个 SKILL、项目或 Runtime 单独覆盖解释器,也不实现通用 Node/Python
版本管理器。
9. 环境只注入本机 Runtime 和本机 stdio MCP 启动链路,不修改系统 PATH,不写 Shell
Profile,不同步到远程主机。
10. Ask/Execute 的现有权限语义保持不变。提供解释器不代表 Ask 获得脚本执行权限。
11. 全新安装默认使用托管来源;升级用户当前可用的系统解释器会在一次性迁移中固化为
自定义绝对路径,避免升级后改变既有脚本运行环境。
12. 本机 stdio MCP 与 SKILL 使用同一套 Node.js/Python 选择。依赖 Python 或 Node.js
启动的 MCP 不再要求用户另外配置一套解释器。
---
## 2. 当前实现基线
GoodBuddy 当前已经具备:
- 桌面安装包内的 Electron Node Runtime。
- 随安装包交付的锁定 npm CLI。
- DeepSeek Harness 插件安装器使用的受管 `node.cmd`/`node` shim,可复用
`process.execPath + ELECTRON_RUN_AS_NODE=1`。
- OpenCode、Continue 和 DeepSeek Harness 的独立本机进程环境构建入口。
- SKILL 按 Runtime 目标分配和暂存的生产链路。
- stdio MCP 的 Main 进程启动链路。
- OpenCode 和 Continue 可执行文件的路径探测、版本读取和自定义路径设置模式。
- 模型及远程 Runtime 包的下载、校验、进度、取消和清理模式。
当前缺口:
- 安装包没有向 SKILL 或通用 stdio MCP 暴露可直接调用的 `node` 命令。
- 本机 Runtime 只继承经过筛选的系统 PATH;没有把 GoodBuddy 工具目录加入 PATH。
- Python 完全依赖用户自行安装。
- 设置页没有统一展示 Node.js、Python 的实际来源、版本、路径和可用状态。
- 用户配置 `node server.js` 或 `python script.py` 时,成功与否取决于 GoodBuddy 启动时
继承到的系统环境。
---
## 3. 范围
### 3.1 首期范围
- 本机 OpenCode、Continue、DeepSeek Harness 加载的 SKILL 脚本。
- 由 GoodBuddy Main 启动的本机 stdio MCP Server。
- Node.js 和 npm。
- Python 3、pip 和标准 `venv` 能力。
- 托管环境安装、删除、状态检查和真实解释器诊断。
- 系统解释器探测、自定义文件选择和明确版本验证。
- Windows、macOS、Linux 的 x64 与 arm64。
### 3.2 明确不作用的范围
- SSH Host 上的 GoodBuddy Agent、固定 Node 和签名 OpenCode Runtime。
- 远程项目的 PATH、Shell Profile、Python、Node 或系统包。
- HTTP/SSE MCP Server 的服务端运行环境。
- Renderer 进程和网页内容。
- GoodBuddy 自身构建环境。
### 3.3 非目标
首期不包含:
- 通用 `nvm`、`pyenv`、Conda、Poetry、uv 或操作系统包管理器替代品。
- 任意 Node.js/Python 版本的目录、升级矩阵或自动切换。
- 每个 SKILL 自动创建和维护独立虚拟环境。
- 自动读取并执行任意 SKILL 的安装脚本。
- 自动解析或合并多个 SKILL 的 `requirements.txt`、`package.json`。
- 修改用户的系统 PATH、注册表、Shell Profile 或全局 npm/pip 配置。
- 把 Python 无条件打入所有桌面安装包。
- 把本机工具环境上传、复制或映射到 SSH Host。
- 对用户自定义解释器进行安装、升级、修复或卸载。
---
## 4. 用户体验
### 4.1 设置位置
在 `Agent Runtime` 设置中增加“本机工具环境”区块。该区块位于默认工作区之后、
具体 Runtime 高级设置之前,避免让用户误以为它只属于某一个 Runtime。
区块顶部持续显示范围说明:
> 用于本机 SKILL 脚本和本机 stdio MCP;不会修改系统环境,也不会应用于远程主机。
设置使用普通卡片和原生单选语义。选项只有两项,不增加三级页签或页面专属切换控件。
### 4.2 Node.js 卡片
```text
Node.js
● GoodBuddy 托管(推荐)
Node.js vXX · 已就绪
○ 自定义环境
[Node.js v22.x · C:\Program Files\nodejs\node.exe ▼]
[选择其他可执行文件]
```
托管 Node 随应用可用,不显示安装按钮。版本来自一次真实
`node --version` 诊断,而不是仅根据 Electron 包版本推断。
### 4.3 Python 卡片
未安装托管 Python 时:
```text
Python
● GoodBuddy 托管(推荐)
尚未安装
[安装]
○ 自定义环境
[Python 3.13.x · C:\Python313\python.exe ▼]
[选择其他可执行文件]
```
安装后显示版本、架构、占用空间和“删除”操作。删除前只需要说明依赖托管 Python 的本机
SKILL 和 MCP 将暂时不可用;不引入额外确认文字输入。
### 4.4 自定义环境
- 下拉列表展示本次检测到且验证成功的候选项。
- 每项必须同时展示版本和规范化绝对路径。
- “选择其他可执行文件”使用 Main 管理的文件选择器。
- 保存前执行真实 `--version` 诊断。
- 已保存路径失效时保持该选择并显示“路径不可用”,不静默切回托管环境或其他候选。
- 用户可以显式改回 GoodBuddy 托管。
### 4.5 反馈
- 安装、删除和重新检测的瞬时成功信息使用应用通知。
- 当前来源、版本、路径、下载进度和阻塞错误保留在卡片内。
- 下载失败保留当前选择和已验证旧安装。
- 字段或路径错误就地展示,不与通知重复。
---
## 5. 设置与状态模型
### 5.1 持久设置
每种工具只保存用户意图:
```ts
type LocalToolEnvironmentSelection =
| { source: 'managed' }
| { source: 'custom'; executablePath: string }
type LocalToolEnvironmentSettings = {
node: LocalToolEnvironmentSelection
python: LocalToolEnvironmentSelection
}
```
不持久化以下可重新推导的信息:
- 检测候选列表。
- 当前版本。
- PATH。
- shim 路径。
- 诊断结果。
- 托管包的重复元数据副本。
托管 Python 的固定包清单属于随应用交付或从受信目录读取的产品元数据,不进入用户设置。
### 5.2 兼容迁移
- 全新用户的 Node.js、Python 默认来源均为 `managed`。
- 已发布版本升级且设置中没有本机工具环境字段时,Main 分别按升级前实际使用的 PATH
解析 `node` 和 Python 3。
- 找到并通过真实版本诊断后,迁移写入
`{ source: 'custom', executablePath: '<规范化绝对路径>' }`。
- 未找到有效解释器时,该工具迁移为 `{ source: 'managed' }`。
- 迁移只执行一次并持久化明确结果。后续 PATH 变化不会自动改变选择。
- 迁移不复制、修改或删除系统解释器及其已安装包。
这样保留现有用户脚本的解释器来源,同时不向产品增加长期存在的“自动选择”模式。
### 5.3 Renderer 状态
Renderer 只接收有界公开状态:
```ts
type LocalToolEnvironmentStatus = {
tool: 'node' | 'python'
selection: LocalToolEnvironmentSelection
active?: {
source: 'managed' | 'custom'
executablePath: string
version: string
}
candidates: Array<{
executablePath: string
version: string
}>
managed: {
state: 'ready' | 'not-installed' | 'installing' | 'error'
version?: string
installedBytes?: number
}
error?: string
}
```
Renderer 不能提交下载 URL、摘要、解压目录、命令参数或任意环境变量。
---
## 6. Main 进程架构
### 6.1 `LocalToolEnvironmentService`
Main 新增单一服务负责:
- 读取和保存工具来源设置。
- 探测系统候选。
- 验证自定义解释器。
- 解析当前有效解释器。
- 准备受管 shim 目录。
- 安装和删除托管 Python。
- 发布有界进度和状态。
- 应用退出时取消自身安装任务并清理本次暂存。
保持一个服务即可,不为 Node、Python、下载、shim 和探测分别建立持久 Manager 层。
### 6.2 解析结果
Runtime 和 MCP 不读取设置文件,而是请求一次解析结果:
```ts
type ResolvedLocalToolEnvironment = {
binDirectory: string
nodeExecutable?: string
pythonExecutable?: string
}
```
`binDirectory` 包含受管命令入口:
- Windows:`node.cmd`、`npm.cmd`、`python.cmd`、`python3.cmd`、`pip.cmd`。
- macOS/Linux:`node`、`npm`、`python`、`python3`、`pip`。
只有当前选择且验证成功的工具才生成入口。入口使用绝对路径,不依赖递归 PATH 查找。
### 6.3 环境注入
本机 Runtime 启动时把 `binDirectory` 放在其现有 PATH 前面:
```text
<GoodBuddy tool bin><delimiter><existing filtered PATH>
```
注入点包括:
- 内置 OpenCode Server 进程。
- Continue Utility Process。
- DeepSeek Harness Utility Process。
- Main 启动的 stdio MCP Server。
不注入:
- GoodBuddy Main 自身的 `process.env`。
- Renderer。
- SSH/Agent/远程 Runtime 请求。
- 用户打开的普通系统终端。
首期不修改助手工作栏终端的环境,避免与用户 Shell Profile 和系统命令预期发生隐式冲突。
如果未来提供“使用 GoodBuddy 工具环境的新终端”,必须作为用户显式选择的新终端类型设计。
### 6.4 运行时刷新
工具环境设置保存、托管 Python 安装或删除后:
- 新启动的 Runtime 和 MCP 使用新环境。
- 已运行进程不在中途替换 PATH。
- 需要重建的当前 Runtime 使用现有 Runtime 重建入口完成。
- 已运行的 stdio MCP 在下一次正常重连或显式测试时使用新环境;首期不增加独立热切换协议。
界面应准确说明“新启动的本机 Runtime 和 MCP 将使用此环境”。
---
## 7. Node.js 托管环境
### 7.1 执行方式
托管 `node` shim 固定调用当前 `process.execPath`:
```text
ELECTRON_RUN_AS_NODE=1 <GoodBuddy executable> <arguments...>
```
实现复用 DeepSeek Harness npm 安装器已经验证的转义和平台脚本模式,不复制另一套命令
拼接规则。
### 7.2 npm
- 托管 Node 使用安装包内锁定的 npm CLI。
- `npm` shim 调用托管 Node,并把 npm CLI 绝对路径作为第一个参数。
- `npm_node_execpath`、`npm_execpath` 只在 npm 子进程范围设置。
- 自定义 Node 优先使用同一目录或系统 PATH 中与其配套的 npm;找不到 npm 时 Node 仍可用,
卡片显示“npm 不可用”。
- 首期不混用“自定义 Node + GoodBuddy npm”,避免产生难以解释的版本组合。
### 7.3 约束
- `ELECTRON_RUN_AS_NODE` 不写入 Main 全局环境。
- shim 不放入系统 PATH。
- Electron 升级后版本变化必须在设置状态中可见。
- 普通 JavaScript 和 Node 内置模块属于支持路径。
- 原生 `.node` 扩展可能受 Electron ABI 影响,界面帮助文案应明确不保证普通 Node 原生
扩展兼容;N-API 兼容性仍以模块自身声明和真实加载为准。
---
## 8. Python 托管环境
### 8.1 包形态
- 使用可再分发、包含 `pip` 和 `venv` 的固定 CPython 独立发行包。
- 每个平台和架构只有一个 GoodBuddy 支持版本。
- 包目录包含版本、平台、架构、URL、字节数、SHA-256 和许可证信息。
- 下载源复用“关于与更新”中用户选择的官方 GitHub/镜像来源,不另建一套来源设置。
- 具体版本在实施时按六平台可用性和许可证复核后锁定,不在用户设置中自由选择。
- Python 标准库和许可证与托管包一起安装。
### 8.2 安装
```text
用户点击安装
→ Main 读取当前固定包元数据
→ 下载到 GoodBuddy 用户数据暂存目录
→ 校验大小和 SHA-256
→ 解压到新的版本目录
→ 运行 python --version 和最小标准库诊断
→ 发布为当前托管 Python
→ 删除本次暂存
```
失败时删除本次暂存并保留已验证旧安装。首期不建立快照、安装日志恢复器或多版本注册表;
下次安装从头重试即可。
### 8.3 Python 包
- 托管 Python 提供正常的 `python -m pip`。
- SKILL 仍显式声明和执行自身需要的依赖安装命令。
- 首期不自动扫描或安装 `requirements.txt`。
- 首期共用一个当前用户的托管 Python 包环境;若出现可复现的依赖冲突,再单独设计按
SKILL 隔离,不预先建设虚拟环境编排系统。
- 专业用户需要 Conda、uv、Poetry 或项目虚拟环境时,应选择对应环境中的 Python
可执行文件作为“自定义环境”。
### 8.4 删除与升级
- 删除只影响 GoodBuddy-owned Python 目录。
- 不删除用户工作区、系统 Python 或自定义环境。
- 正在被 GoodBuddy 子进程使用时,先通过现有 Runtime/MCP 生命周期停止相关进程,再删除。
- Python 版本随 GoodBuddy 发布清单更新,但不后台自动升级。用户在设置页看到“可更新”后
显式执行更新。
---
## 9. 自定义环境探测
### 9.1 Node.js
候选来源:
- 当前筛选后 PATH 中的 `node`/`node.exe`。
- Windows 常见 Node 安装目录和当前用户 npm 目录关联的 Node。
- macOS/Linux 常见系统安装位置。
- 已保存的自定义路径。
每个候选使用分离参数执行 `--version`,要求普通文件、目标平台可执行、成功退出且版本输出
可解析。候选按规范化真实路径去重。
### 9.2 Python
候选来源:
- Windows 的 `python.exe`。
- macOS/Linux 的 `python3`,其次是明确指向 Python 3 的 `python`。
- 常见系统和当前用户安装目录。
- 已保存的自定义路径。
验证命令必须同时确认 Python 3、平台架构和解释器绝对路径。Windows Store 的未安装
App Execution Alias 不能作为成功候选。
### 9.3 探测行为
- 打开设置页时读取最近状态,不阻塞整个设置中心。
- 用户点击“重新检测”时重新扫描。
- 探测不安装软件、不执行包管理器、不修改文件。
- 单候选和总探测均使用有界超时、输出上限和进程树清理。
- 路径或版本诊断失败只影响该候选。
---
## 10. SKILL 与 MCP 接入
### 10.1 SKILL
GoodBuddy 继续按当前方式把 SKILL 包暂存到目标 Runtime。工具环境只改变本机 Runtime
子进程看到的 PATH,因此 SKILL 可以使用稳定命令:
```text
node "<skill-dir>/scripts/example.js"
python "<skill-dir>/scripts/example.py"
python -m pip install -r "<skill-dir>/requirements.txt"
```
SKILL 不接收解释器真实安装目录,也不把路径写入自身内容。未准备的解释器被调用时,命令
明确失败,Runtime 应把原始“环境未就绪”上下文返回给用户。
### 10.2 stdio MCP
- `command: node`、`command: python` 和对应脚本参数通过同一个工具 bin 解析。
- `command: python3`、`command: npm`、`command: pip` 同样解析到当前明确选择的工具来源。
- 以 `#!/usr/bin/env python3` 或 `#!/usr/bin/env node` 启动且由 Runtime Shell 解析的
MCP 脚本,也会从注入的工具 bin 找到当前解释器。
- 例如 `python -m my_mcp_server`、`node server.js` 这类本机 MCP 启动命令,无论由 Main
直连模型工具链还是本机 OpenCode、Continue、DeepSeek Harness 发起,都使用相同来源。
- 绝对命令路径继续按用户配置执行,不被工具环境替换。
- HTTP/SSE MCP 不受影响。
- 自定义 MCP 凭据和工具环境状态保持分离。
- MCP 进程继续受现有取消、超时、关闭和输出上限控制。
- 工具环境只提供解释器和配套 npm/pip,不自动安装 MCP 自身的 Python/Node 依赖;这些
依赖仍由 MCP 的安装流程或用户明确执行的安装命令准备。
### 10.3 Ask/Execute
- Ask 的 Runtime 只读边界保持不变。
- 提供 Node/Python 不会给直连模型新增 Execute 工具。
- OpenCode、Continue、DeepSeek Harness 仍按各自 Ask 权限边界决定是否允许命令工具。
- Execute 继续表示用户对所选本机账号可用工具和路径的完整授权。
- stdio MCP 的启动行为沿用当前能力开关和分配规则,不新增第二套批准流程。
---
## 11. 远程主机边界
本机工具环境与远程托管环境严格分离:
- 不上传 shim、Python 包、pip 包或自定义解释器路径。
- 不修改 SSH Host PATH。
- 不把本机 SKILL 包同步到远程 OpenCode。
- 不把本机 stdio MCP Server 配置传给远程 ACP Session。
- 远程 Agent 的固定 Node 继续只服务 Agent、模型桥和签名 Runtime。
- 远程项目只能使用远端签名 Runtime 自身具备的能力和 SSH 账号已有系统环境。
远程主机设置页必须持续显示:
> 本机配置的 Skills 和 MCP 不会应用于远程主机。
---
## 12. IPC 与安全边界
- 所有设置和操作输入使用共享 Zod schema。
- Renderer 只提交工具类型、来源、已选择候选路径或文件选择动作。
- 自定义路径由 Main 重新规范化、检查和执行真实版本诊断。
- Renderer 不能提交 PATH、环境变量、下载 URL、安装目录或 shim 内容。
- Main 只在明确的本机 Runtime/MCP 子进程环境中注入工具 bin。
- API Key 和 MCP 凭据不进入解释器检测命令、安装日志或状态快照。
- 下载、解压、诊断和删除都限制在 GoodBuddy-owned 目录。
- 不删除或覆盖用户自定义解释器及其包。
- 继续保持 Context Isolation、Sandbox 和 Renderer 无 Node Integration。
---
## 13. 实施顺序
以下阶段是开发顺序,不是可以长期停留的残缺产品状态。合并该能力前应完成普通用户使用
托管 Node/Python 和专业用户使用自定义解释器的完整本机生产路径。
### 阶段 1:共享契约、设置与诊断
- 增加本机工具环境设置 schema、迁移默认值和 IPC 契约。
- 全新用户默认 Node.js、Python 均选择 `managed`。
- 升级用户把当前有效的系统解释器一次性固化为自定义绝对路径。
- 实现 Node/Python 候选探测和真实版本诊断。
- 增加设置页卡片、范围说明、来源选择、路径选择和状态展示。
- 保持旧用户现有 Runtime 设置不变。
### 阶段 2:托管 Node 与本机执行链路
- 提取并复用当前 DSH `node` shim 逻辑。
- 生成当前会话受管 tool bin。
- 为 OpenCode、Continue、DeepSeek Harness 注入工具 bin。
- 为本机 stdio MCP 注入同一工具环境。
- 验证 npm CLI 与 npm lifecycle 子进程使用正确 Node。
### 阶段 3:托管 Python
- 选定并锁定六平台 CPython 独立发行包。
- 增加包目录、许可证、下载、进度、取消、校验、安装、诊断和删除。
- 生成 `python`、`python3`、`pip` shim。
- 验证当前内置 Python SKILL 的真实执行路径。
### 阶段 4:端到端验收与文档收口
- 使用内置 Node 脚本 SKILL 验证无需系统 Node。
- 使用现有 Python SKILL 验证全新设备从安装托管 Python 到生成实际成果。
- 使用 Node 和 Python stdio MCP 分别验证启动和工具调用。
- 验证切换到系统/自定义解释器后的版本和路径。
- 验证 Ask/Execute、取消、退出和 Runtime 重建行为。
- 更新用户说明、架构文档、设置文案、发布打包校验和许可证清单。
---
## 14. 预计代码落点
| 层 | 预计改动 |
| --- | --- |
| Shared | 本机工具环境设置、状态、操作和 IPC schema |
| Main | `LocalToolEnvironmentService`、探测、shim、Python 包安装 |
| Runtime | `process-environment`、OpenCode、Continue、DSH 启动环境注入 |
| MCP | stdio transport 的受管 PATH 注入 |
| Preload | 状态读取、保存、检测、安装、取消、删除和进度订阅 |
| Renderer | Agent Runtime 设置中的本机工具环境卡片 |
| Build | Python 包目录/许可证校验,现有 npm 和 Electron Node 验证 |
| Docs | 用户范围、SKILL/MCP 使用和远程主机边界 |
实际实现应优先复用现有运行环境检测、模型下载、通知和进程树清理代码,不为了表格中的职责
机械新增同名文件或抽象层。
---
## 15. 测试策略
### 15.1 单元测试
- 设置迁移和严格 schema。
- 两种来源的解析,不存在第三种自动状态。
- Node/Python 候选发现、去重、超时和版本解析。
- Windows Store Python alias 拒绝。
- shim 的 Windows 与 POSIX 转义。
- `ELECTRON_RUN_AS_NODE` 仅存在于 Node 子进程。
- PATH 前缀顺序和原环境保留。
- 已保存自定义路径失效时不回退。
- Python 下载摘要、暂存清理、成功发布和删除范围。
### 15.2 本地集成测试
- 安装包中的 Electron Node 实际执行 JavaScript 文件。
- 安装包中的 npm CLI 实际运行并启动一个最小 lifecycle script。
- OpenCode、Continue、DeepSeek Harness 分别解析相同 `node`/`python` shim。
- 本机 stdio MCP 使用托管和自定义来源启动。
- 设置变化后新进程使用新环境,旧进程不被中途改写。
- 取消和应用退出没有残留安装任务或工具进程。
### 15.3 六平台打包验证
- tool bin 脚本存在且权限正确。
- Electron RunAsNode 可用。
- npm CLI 和许可证存在。
- Python 固定包目录覆盖目标平台/架构,下载工件和许可证可验证。
- Python 安装后能导入标准库、pip、venv。
- 安装包未无条件包含完整 Python 工件。
### 15.4 项目验证
源码完成后运行:
```text
npm test
npm run typecheck
npm run lint
npm run build
```
涉及生产打包资源时按发布流程运行对应平台的原生 CI 打包验证。该功能不改变远程 Agent
或桌面到 Agent 生产链路,因此不要求因本机工具环境本身修改共享 SSH Host;如果后续改动
触及远程协议、Agent 包或远程 Runtime,则必须按远程架构文档完成真实 Host 验证。
---
## 16. 验收标准
- 全新安装且系统没有 Node.js 时,本机 SKILL 可以通过 `node` 执行普通 JavaScript。
- 全新安装且系统没有 npm 时,托管 npm 可以执行受支持的安装命令。
- 系统没有 Python 时,用户可以从设置页安装托管 Python,并运行现有 Python SKILL 生成
实际成果。
- 用户可以选择检测到的系统 Node/Python,设置页显示实际版本和绝对路径。
- 用户可以选择未被自动发现的解释器文件,保存前完成真实诊断。
- 自定义路径失效时明确报错,不静默切换来源。
- Node.js 和 Python 各自只有“GoodBuddy 托管”和“自定义环境”两个来源。
- npm 跟随 Node,pip 跟随 Python,不出现重复来源设置。
- 本机 OpenCode、Continue、DeepSeek Harness 和 stdio MCP 使用一致的解释器选择。
- 依赖 Python 和依赖 Node.js 的本机 stdio MCP 均能使用托管环境完成真实启动与工具调用。
- 不修改系统 PATH、Shell Profile 或用户自定义解释器。
- Ask 不因安装解释器获得额外写入或命令权限。
- 取消、Runtime 重建和应用退出可以清理 GoodBuddy 自己启动的进程与暂存。
- 远程主机不接收本机 SKILL、MCP、shim、Python 包或自定义解释器路径。
- 中英文设置文案、键盘操作、可见焦点、状态和错误反馈符合统一界面设计系统。
- 全量测试、类型检查、Lint 和生产构建通过。
---
## 17. 已知限制
- Electron Node 与普通 Node 的原生模块 ABI 可能不同,首期不承诺普通 Node 原生扩展兼容。
- 托管 Python 首期只有一个 GoodBuddy 支持版本,不提供多版本并存和自动选择。
- 首期共用托管 Python 包环境,不能解决所有第三方包版本冲突;复杂项目应选择用户自己的
虚拟环境。
- 已运行的 Runtime 和 MCP 不热切换解释器,设置变化对新进程生效。
- 助手工作栏终端首期不自动继承本机工具环境。
- HTTP/SSE MCP 的远端服务依赖由服务部署方负责。
@@ -10,7 +10,7 @@
| 日期 | 2026-08-19 |
| 适用产品 | GoodBuddy 桌面端 |
| 目标平台 | Windows、macOS、Linux,x64 与 arm64 |
| 相关基线 | [统一界面设计系统](../../UI-DESIGN.md)、[文档解析与本地 OCR](../prd/document-processing/document-extraction-and-local-ocr.md)、[本地文本向量模型与连接设计](./local-text-embedding-model-design.md)、[全双工实时语音交互设计](./full-duplex-voice-design.md) |
| 相关基线 | [统一界面设计系统](../../UI-DESIGN.md)、[文档解析与本地 OCR](../features/document-processing/prd.md)、[本地文本向量模型与连接设计](../features/knowledge-base/local-embedding-technical-design.md)、[全双工实时语音交互设计](../features/full-duplex-voice/technical-design.md) |
本文定义“设置 → 平台功能”的二级页签结构,以及所有 GoodBuddy 托管本地模型共同使用的
“模型下载源”设置。首期下载源为 ModelScope 和 Hugging Face,默认使用 ModelScope。
@@ -1,502 +0,0 @@
# SSH 远程主机与 GoodBuddy Agent 实现说明
## 状态
本文记录截至 2026-08-31 的当前代码实现,不定义额外的信任框架。“新增 Host 只探测、Host 卡片手动准备
Agent/Runtime、Host 直接从 GitHub/北京镜像下载、项目始终使用 Host current 环境”已经完成源码接线,
详细事务与验收边界见
[SSH Host 远程环境准备与直连下载设计](./remote-host-environment-provisioning-design.md);
控制面直连源码可直接使用既有 package format v1 包,不等待携带 installer 的新 Agent
包,也不通过额外目录元数据判断 bootstrap 能力;公开能力仍等待 GitHub/北京镜像、
Linux x64/arm64、取消和离线 GoodBuddy 传输的真实 Host 验收。
Windows 到 Linux x64 的安装、Agent-owned Prompt、Agent 本地模型 gateway、断线恢复、
同一 OpenCode Session 续接、取消和终态清理已经使用真实模型与工具验证。Agent
`0.11.10` 已通过独立 workflow 发布 Linux x64/arm64 复合包和签名累计目录;当前未发布
Agent 源码 lock 为 `0.11.14`,桌面版本仍保持已发布的 `0.11.13`,等待发布审批后再选择
新版本。现有源码显示本地与远端 OpenCode 原生 Task,并取消 GoodBuddy 对生产 Prompt 的
固定墙钟总时限。失败的 `agent-v0.11.3` 保持不可变且未发布。
## 产品语义
远程 Host 与项目能力是默认关闭的技术预览。用户在“设置 > 平台功能”的独立
“远程项目(技术预览)”页签启用开关后,Renderer 才显示 SSH Host 管理、托管 SSH 项目创建和已保存
远程项目;Main 在 IPC 边界执行同一开关校验,避免隐藏界面被绕过。关闭开关不删除 Host、
项目或凭据;当前远程项目切回第一个普通本地项目。应用启动仍固定
选择普通本地项目,不会因为保存了远程项目而自动连接 Host。
远程项目只有两种工作模式:
- **Ask**:Runtime 在操作系统边界以只读方式访问项目 Workspace。
- **Execute**:用户已授权使用所选 SSH 账号的完整权限。Runtime 可以使用该账号可访问的文件、进程、网络和工具,不再要求额外 trust tier、consent checklist、逐工具审批或“受控执行”授权。
Execute 不获得 root 或 SSH 账号本身没有的权限。托管 SSH Prompt 会把当前选中的文本模型
profile 和凭据作为有界、逐 Prompt 的控制消息交给 Agent;该凭据不进入命令参数、环境变量、
语义日志或模型调用账本,并在 Prompt 终态清除。此行为只适用于用户已明确启用的可信组织
网络和已认证 SSH Host。
## 组件
```text
Renderer
-> 窄 Preload / Main IPC
Main
-> SSH Host 与 Host Key 管理
-> SSH connection pool
-> 签名 Agent / Runtime 安装
-> 远程项目当前环境准备与稳定配置 SQLite 事务
-> 永久项目、对话、Task 与已同步事件 SQLite
SSH attach
-> 远端私有 Unix socket
Detached GoodBuddy Agent
-> Workspace / Git 协议
-> Agent-owned OpenCode ACP Prompt Promise
-> Prompt-scoped Provider gateway / model-call ledger
-> 有界语义 transcript / page / ACK
-> 有界私有诊断记录
-> 直接拥有的 Runtime 进程
```
不提供把远端路径交给本机 Runtime 的伪本地模式,也不提供 SSH stdio Runtime fallback。
## SSH Host
- Renderer 只管理脱敏 Host 快照,不接触密码、密文、SSH Client 或任意命令接口。
- Main 在认证前取得 Host Key,展示算法与 SHA-256 指纹,并保存完整 key blob。
- 首次连接需要用户确认。Host Key 变化展示旧/新值并要求显式替换。
- 密码使用 Electron `safeStorage`;系统 SSH Agent 认证不启用 Agent forwarding。
- Host 地址、用户或 Host Key generation 变化时关闭旧连接,并定向退役依赖旧 Host identity 的 Workspace 和 Runtime 会话。下一次选择使用 Host 管理的当前连接记录解析 current Agent/Runtime,不读取项目中的旧 revision 或组件 identity;当前 registry、连接或 capability 无效时才要求显式修复。
## Agent 安装与生命周期
- Agent bundle 通过 manifest、Ed25519 签名、payload digest、平台和架构校验。
- 安装使用当前 SSH 用户目录中的 GoodBuddy-owned 路径和 side-by-side digest 目录。
- Agent 代码与固定 Node Runtime 一起位于签名 compound payload。显式安装或重装从本次
已完整校验的准备目录原子发布整个 Agent,不扫描旧安装、不创建跨版本 Node 硬链接;
Agent 不匹配不会阻止升级,也不会覆盖 GoodBuddy-owned 路径以外的内容。
- 桌面包只携带版本 lock 与 production 公钥,不携带远端 payload。Host 卡片只有一个按
版本事实显示“安装远程环境”“更新远程环境”或“重新安装”的主按钮;次级
SegmentedControl 选择“自动”“Host 下载”或“GoodBuddy 传输”,默认“自动”且不持久化。
每个复合包绑定 Agent、固定 Node 与桌面维护的 OpenCode Runtime 精确工件。添加或重新
验证 Host 只保存并探测,不自动传输完整包。
- Host 管理独占 Agent/Runtime 的包准备、更新和完整 payload 验证。已有项目的打开和切换
只更新本地项目选择。Workspace 和执行路径按需读取 Host current registry,
执行固定 `attach-or-bootstrap` 并复用当前 Agent 连接;同一进程内复用已确认 identity。
它们不取得安装包、不发布组件,也不通过 SFTP 重读或哈希 payload。
- Agent 的固定 attach/按需启动命令只读取 Host 管理已提交的 installation registry 和
与其 digest 匹配的有界 manifest 元数据,不在每次项目切换时重新验签或哈希完整 payload。
完整验签、payload 扫描和 registry 写入仍只发生在显式 Host 准备/更新流程。
- 项目切换不执行远程核对,立即选择本地项目配置。Workspace 或 Runtime 首次实际使用时
才通过当前 Agent 连接核对所需能力;失败只影响该操作。新建或显式保存项目仍执行完整
准备,并只事务写入 Host、路径、Runtime 选择和工作模式。
- SSH 连接先尝试 attach;Agent 不存在或未运行时执行幂等 bootstrap,然后重新 attach。
- Agent 是按需启动的 detached process,不注册开机服务,不依赖 systemd、D-Bus 或 Linger。
- 每个模型桥 helper 都为 loopback HTTP 入口生成一次性随机路径 capability;只有写入当前 OpenCode 子进程配置的 URL 可以访问该入口,其他本机用户即使发现临时端口也不能提交模型请求。
- Agent 监听当前用户拥有的私有 Unix socket。SSH 中断只关闭 relay,不拥有 Agent 和活动 Runtime 的生命周期。
- SSH Host 设置通过固定探针和有界 SFTP 显示 Host 已登记的 Agent/OpenCode 版本,并与当前
GoodBuddy 所需版本比较。进入或切回该设置页只读取本地 Host 列表,不连接任何 Host;
用户点击“刷新版本”后才探测对应 Host,探测不安装或切换远端组件。项目切换与此按钮
解耦且不建立连接;实际远程操作才建立或复用 SSH/Agent 连接。
- “平台功能 > 远程项目”中的本地 Agent 包清单与 Host 状态分离。Main 对用户数据目录中
Linux x64/arm64 `.gbagent` 分别执行外层可信签名、桌面/协议兼容性、内部 Agent/Runtime
签名、架构与完整 payload 校验;首次打开和手动刷新还会读取“关于与更新”所选来源的
小型签名目录。Renderer 只得到本地/在线版本、是否有更新、架构、远端 Runtime 版本、
协议和本地状态,不得到缓存路径、key ID 或 digest。目录检查不下载 `.gbagent`,在线
包下载只由用户点击带目标版本的操作触发;离线导入/导出通过 Main 管理的文件对话框完成。
- “自动”只在 operation/prepare 前用 Host capability probe 选择 acquisition:直连明确
可用才选 Host 下载,否则选 GoodBuddy 传输;显式选择不被改写。prepare、commit 或
adoption 失败后都不跨 acquisition 自动 fallback。
- Host 下载由桌面控制面通过固定 SSH prepare channel 和结构化 stdin 发送候选信息,
Host 取得 compound `.gbagent` 并核对大小与 SHA-256。显式 GoodBuddy 传输在在线目录
不可用,或本地已验证包版本不低于在线候选时优先租用本地包,包括更高版本离线导入;
线上存在更新且可用时仍取得线上新包。需要下载时,同一次操作会验 SHA-256 与签名、
缓存并取得 lease,再以有界流式 SFTP 上传一个 compound archive 和归档中已验证的
bootstrap Node;约 294 MiB 完整包不会一次读入 Main `Buffer`,Host 仍再次校验完整包。
- 两种 acquisition 交付到固定 operation staging 后,完全共用 control-plane
`prepare → commit → Agent activate/health → Runtime activate → finalize → cleanup`。
Main 只保存清理 operation staging 所需的 operation ID。中断或 adoption 失败后,下次
更新尽力清理旧暂存并重新 prepare,不保存或恢复 Agent/Runtime metadata 副本。确认
adoption 后才显式 cleanup;cleanup 失败不回滚健康环境,也不阻塞下一次 fresh prepare。
Host 在 prepare 解包时
完成一次完整 payload 校验,commit 和后续注册检查不再遍历包体。首次 bootstrap 不依赖
既有 Agent daemon,也不要求 format v1 归档携带 `agent/lib/package-installer.cjs`。
- 即使版本号均为当前版本,Host 卡片仍提供同版本“重新安装”,用于修复 registry、签名
或安装 identity 异常。GoodBuddy-owned 同 digest 目录损坏时先隔离后替换,并在发布或
原子 commit 失败时恢复;commit 成功后的激活失败保留新发布内容,下次点击重新安装时
清理旧暂存并重新 prepare/adopt。不要求删除 Host、凭据或引用项目。
- Host 卡片的“版本匹配”badge 只表达版本事实,不代表 Agent 正在运行或环境健康。重新安装
失败时明确显示本次操作未完成,并以随后重新检查的版本卡片表达当前事实;提交结果
不确定时不声称旧版本未被替换。
- 更新成功后才定向清理该 Host 的 Agent 连接和 Runtime 选择缓存。引用项目再次打开时
解析新的 current registry 并建立当前连接,无需刷新项目记录;当前环境无效时要求用户
显式修复,但不在项目切换中下载或安装。失败或取消保留 Host 配置、凭据、项目、
Workspace 和旧组件。
- 显式 stop、升级、身份冲突或进程退出时清理 GoodBuddy 自己的 socket、状态和子进程;不得删除或覆盖无关 Host 文件。
### Host 级环境生命周期
- Agent、Node 和 Runtime 是 Host/SSH 账号级共享环境。新增或重新验证 Host 后先保存 Host
并只读探测,不自动安装;用户随后在 Host 卡片明确选择安装或更新方式,失败保留 Host
并允许重试。
- Host 可以按签名目录固定的 URL、大小和 SHA-256 直接从当前 GitHub/北京镜像来源下载
完整 `.gbagent`,也可使用 GoodBuddy 本机下载、流式 SFTP 传输和离线导入。自动模式只在
操作开始时择一;执行失败不会在同一次操作中切换 acquisition。
- 项目只保存 Host ID、远端路径、Runtime 选择和默认模式。创建或保存时验证 Host current
环境;打开或切换只读取本地配置。Workspace/Runtime 实际使用时从 Host current registry
和当前 Agent 连接取得 live identity,不再扫描、下载、上传或发布 Agent/Runtime。完整
事务、兼容边界和验收要求以
[Host 环境准备设计](./remote-host-environment-provisioning-design.md) 为准。
- 打开新建/项目设置弹窗只读取本地 Host 验证记录,不并发检查所有 Host;目录浏览或保存
才连接所选 Host。
- 项目选择器中的管理操作默认浮动隐藏,在悬停、键盘焦点或触屏环境显示。管理任意已保存
项目不先激活项目,因此 Host 不可达或远端目录已不存在时仍可删除其本地记录。
- “远程项目”在选择器中按已保存 SSH Host 轻量分组。Host 标题显示 Main 已持有的真实
Agent 连接状态(未连接、连接中、就绪或异常),读取和展示状态本身不连接 Host;
项目副标题只显示远端路径,不重复“托管 SSH”类型。
- 删除 Host 时确认框列出所有引用它的本地项目记录,包括归档项目。确认后只删除本机
Host、凭据、项目及其关联记录,不连接 Host、不删除远端目录或内容;Host 设置写入或
项目事务失败时恢复另一侧,避免只删掉其中一类本地记录。
## Workspace
Main 传入已规范化的绝对 POSIX root。Agent 返回 Workspace identity、访问模式、Git 状态和有界能力。
- 创建项目时,用户可以直接输入远端工作目录,或通过输入框右侧的文件夹按钮浏览并选择目录。
- 项目保存前的目录浏览使用 Main 管理的只读、有界 SFTP,只返回目录;每次请求重新解析并校验当前 Host revision 与 Host Key generation,不安装 Agent、不暴露任意 shell/SFTP 接口。
- 浏览从 SSH 账号 Home 或当前有效绝对路径开始,限制扫描次数、返回条目和总时限;取消、Host 变化、超时或连接失败时关闭 SFTP 并保留用户原先输入。
- 选择目录只更新项目草稿,仍需通过正常的 Agent、Workspace、Runtime 验证和项目保存事务才会持久化。
- Ask Workspace handle 不暴露写入方法。
- Execute Workspace handle 可读写,但这不是 Execute 的唯一权限面;Execute Runtime 本身使用 SSH 账号的正常权限。
- 文件预览、搜索、Git diff、目录项和显式文件传输保持字节、条目数和路径长度上限。
## Runtime
首个远程 Runtime 固定为签名 OpenCode ACP bundle。
托管 SSH 项目的 Composer Runtime 菜单只显示当前配置的 OpenCode 和管理入口,不显示
直连模型、Continue 或 DeepSeek Harness。激活旧远程会话时,若其保存了其他 Runtime
selection,Renderer 会恢复为当前 OpenCode 配置;Main 的远程请求校验仍是最终边界。
本机配置的 SKILL 包和 stdio MCP Server 不上传或分配给远程 Runtime;远程 ACP Session
继续使用空 MCP Server 列表。Node.js、Python 等本机工具环境同样不修改或同步到 Host,
完整边界见[本机工具环境设计与实施计划](./local-tool-environment-design.md)。
### Ask
Ask 使用系统 `bwrap`:
- Workspace 和 Runtime bundle 只读 bind;
- 独立可写 scratch HOME、TMPDIR 和 XDG 目录;
- 固定环境名和固定 Runtime argv;
- 通过只读边界阻止 Workspace 修改。
- ACP 权限请求只有在工具种类为原生 `read` 且 Runtime 提供 `allow_once` 选项时才允许;
search、edit、execute、未知工具以及只提供持久授权的请求全部拒绝。
`allow_once` 只解决 OpenCode 发起原生读取前的 ACP 协商,真正的文件系统边界仍是只读
`bwrap` bind。Host 缺少 `bwrap` 时 Ask 不可用。
### Execute
Execute 不经过 Ask 的 bubblewrap profile:
- 直接启动已签名 Runtime entrypoint;
- 使用 Agent 本地模型 gateway 时,签名 Agent launcher 会 `exec` 候选 manifest 锁定的
Node Runtime;进程 owner 校验最终 Node executable,而不是已经被替换的 shell
launcher 路径;
- `cwd` 为项目 Workspace;
- 继承 SSH 账号的正常环境、文件系统、进程和网络能力;
- 不进行 T2/T3、confinement attestation、approval bridge 或逐工具批准。
两种模式都保留输入/输出字节上限、用户取消和进程组清理。生产 Prompt 不设置固定墙钟
总时限;只要 Runtime 尚未按自身协议结束,Main 和 Agent 就允许其持续运行。启动、握手、
单个控制 RPC、重连尝试和关闭清理仍使用独立的有界超时,测试可以显式注入短 Prompt
时限验证取消升级,但该覆盖值不是生产默认。无界 Prompt 继续复用已发布的必填
`deadlineAt` 字段,以 year-9999 哨兵表达;Main 与 Agent 必须同时理解该哨兵,Agent
`0.11.10` 及更早版本仍会按其 manifest 上限夹紧,因此桌面启用这一语义前必须配套更新
到 Agent `0.11.13` 或更高版本。
## ACP 与断线
- 管控面只负责有界 JSON-RPC:连接、查询、启动、取消、关闭和重连。数据面只负责
ACP/blob 数据与 Agent 本机 journal。二者共享同一个经过 Host Key 校验和认证的
SSH attach 字节流,但不共享生命周期:管控连接关闭不会隐式取消 Agent 已接受的
Runtime 动作。
- 帧只保留连接 identity、channel identity、单调 sequence、类型和长度。Main 与
Agent 都按调用顺序 FIFO 写入,输入队列达到固定字节或条目上限时暂停 SSH stream。
顺序、可靠传输、分包和网络背压全部交给 SSH;协议不实现 credit/window、优先级
调度、跨 channel 超车、批发送或第二套拥塞控制。
- Agent 在把 Main 输入交给 Runtime 前、以及把 Runtime 输出交给 Main 前,先把 ACP frame 写入本机 journal。ACK 只推进 cursor 并裁剪已确认 frame,不表示 channel 已终止。
- Main 持久化 binding identity 和单调 cursor;保留中的 connection lease 即使处于 offline/reconnecting,也可以先把新 cursor 落盘。重连提交不能覆盖 resume 过程中并发落盘的更新。
- Runtime 事件先经过单事件、单请求输出和每次 Prompt 最多 100 个不同工具调用的边界;
通过边界后,Renderer 和 Main 将已接受的工具详情保存到本地 SQLite,不再额外缩短其
输入、输出、错误或摘要。
- OpenCode 原生 Task 工具按 `subagent_type`、`description`、`prompt` 和稳定 tool call ID
解析为子 Agent 事件。本地 OpenCode SDK 与远端 ACP 增量工具事件复用同一转换;ACP
首帧缺少参数时可以先显示普通工具活动,后续参数确认其为 Task 后必须替换为子 Agent
状态卡,并持久化任务说明、终态与输出。
- 同一 detached Agent 存活时,短暂 SSH 断线依次执行 `controller/resume`、`runtime/resumeAcpChannel` 和 `runtime/replayAcpChannel`,从 Main 已确认的 cursor 后只重放 Agent 到 Main 的已记录输出。重复 frame 会被确认但不会再次交给上层。若上一代连接只留下没有活动请求的 detached binding,完成精确 controller takeover 后会先有界停止并核对遗留 Runtime process,再用新 channel epoch 重新打开同一 binding 并恢复已有 ACP session;其他 controller、未证明 takeover 或仍有活动请求的 binding 仍被拒绝。
- Main 到 Runtime 的 ACP 输入、模型请求、工具请求和 blob 不自动重放。
- 无法确认外部 Provider 是否已处理的模型调用保持结果未知,避免重复计费或重复副作用。
- 只有远端返回 identity 匹配且 `closed: true` 时,Main 才删除持久化 binding;传输失败或未确认 close 会保留 recovery identity。
- 终态 close 由 Agent 在一个事务中为两个方向记录 sequence high-water tombstone,删除剩余 frame 和 active channel,并归零对应 journal quota。tombstone 用于拒绝迟到的旧 epoch frame,因此当前不会按时间自动裁剪。
- 断线不主动终止正在运行的 Runtime。用户取消、显式关闭、输出边界或 Agent shutdown
才触发停止;连接恢复只受当前用户取消信号和单次重连控制超时约束,不把 Prompt 已运行
时长当作停止条件。
- Agent 失联期间继续把 Runtime 输出写入本机 journal;重新连接只校验 controller
identity、恢复 binding,并从 Main 最后确认的 cursor 后同步缺失数据。已接受的
指令和结果不确定的 Provider 请求都不自动重放。
### Agent-owned Prompt 与 Desktop 恢复
- 托管 SSH 的生产 Prompt 从一开始就由 Agent 持有 ACP `ClientSideConnection`、原始
Prompt Promise、Provider 轮次和 Runtime 进程。SSH relay 或 Desktop 进程不属于该
Promise 的生命周期,因此正常退出、强制结束 Desktop 或本机网络中断后,Host 仍能继续
后续模型/工具轮次。该路径要求 `runtime/acp` capability v4,即 Agent `0.11.14` 或更高。
- Agent 把 ACP session update、permission decision 和唯一 Prompt 终态写入有界语义
transcript。Main 不恢复旧 JSON-RPC Promise,也不重发 `session/prompt`;Fresh Desktop
只对同一 controller/binding/operation 执行 takeover、attach 和 ACK-exclusive page。
- 每个 Agent 语义 sequence 在 Main 映射为零到多个公开事件,最后追加内部 checkpoint。
Desktop 在同一 SQLite 事务中去重 provenance、归并原 assistant message,并在 checkpoint
时提交 Task 终态;事务成功后生成器才继续并向 Agent ACK。断电发生在同一 sequence
中间时会重读该 sequence,已写事件按 `(binding, operation, sequence, eventIndex)` 去重,
未写事件继续归并,不会重复 Provider 或工具执行。
- ACK 后 Agent 删除已确认事件,只保留小型操作终态与游标用于幂等核对;未 ACK 输出继续
保留。Main 按 transcript page 的最高连续 sequence ACK,终态仍立即 ACK。单事件、单页、
单 Prompt 总字节和事件数都有上限;Agent 始终为唯一终态预留一个事件和最大事件字节,
避免配额耗尽后无法记录终态或无法重启。已 ACK 终态操作和已交付模型 ledger 在 daemon
运行期间也按固定上限裁剪,不依赖重启。Agent 重启不能重建内存中的 Runtime/Prompt
Promise,因此启动时把遗留 nonterminal 记录变为 `outcome-unknown`,Desktop 只同步该
终态而不自动重放。
- prepare 已接受但 start RPC 未到达时,Agent 使用两分钟 prepare-to-start watchdog 清除
进程和 Prompt 凭据并记录 `outcome-unknown`,避免无界 Prompt 哨兵造成永久悬挂。
- Desktop SQLite 仍是永久可见项目/对话数据的权威;Agent transcript 只是尚未同步事件的
权威。启动后 Renderer 先订阅恢复进度,再触发每项目恢复。项目选择器依次显示网络、
Agent、Runtime、cursor、完成或失败/重试;只阻塞受影响项目的发送和队列,已有历史与
其他项目保持可用。
- 正常应用退出对已接受托管 SSH 请求执行 detach,不等待远端完成;本地请求和尚未接受的
请求仍按原行为取消。用户显式“停止”始终发送稳定 operation cancellation,并等待 Agent
语义终态。已进入 Agent-owned `run` 但 start/attach 响应仍不确定的请求继续保留为
`interrupted + remote_recoverable`,并自动走 exact attach;不会因为尚未收到第一个公开
事件而丢失恢复身份。
## Agent 本地模型 gateway
- Main 在 `runtime/preparePrompt` 中发送当前解析后的有界 profile;Agent 只在该 operation
的内存状态中持有 Provider URL、API Key 和认证信息。OpenCode 仍通过 Prompt 私有 Unix
socket 发出模型请求,但 HTTP Provider dispatch、响应交付和稳定 call ledger 全部位于
Agent,因此 Desktop 断电后可以继续下一轮。
- Agent 校验固定 model、协议和 API path,拒绝 background/store、provider web search、
provider MCP、持久 conversation、非文本 modality 和其他可独立计费能力;请求/响应 body
各限制 768 KiB,响应同时核对声明长度与短读。每个
`(binding, operation, roundIndex)` 只 dispatch 一次;已 dispatch 但未证明完整交付的调用
变为 `outcome-unknown`,不会自动重试。
- Prompt 最多 100 个模型调用,并受单次与累计输出 Token 上限保护;这些是资源边界,不是
Prompt 墙钟总时限。Provider 单次请求仍有独立超时。实际 usage 在语义 transcript 中同步
回 Desktop 的 usage/task 数据。Agent 在所有进程终止路径统一清除 Prompt token 计数。
- helper 可以接收同一 Prompt 内并发到达的模型桥请求;它在单一稳定模型桥上按到达
顺序等待并交付,不返回本地 `bridge-busy`,每个响应只有在 HTTP 完整 flush 后才
发送 delivery ACK。
- GoodBuddy 自己管理会话标题,因此传给 OpenCode 的配置禁用 title Agent;一次用户
Prompt 不会额外触发标题模型请求。
- Agent gateway 不再把 Provider 请求往返 Desktop;legacy blob bridge 只保留给旧的
非 Agent-owned 测试/兼容路径。凭据不进入 Renderer、SSH 命令参数或远端环境。
- Unix socket 接收端按 `readableLength` 中已经缓冲的字节增量排空一个声明长度的帧,
不会在部分大响应到达时反复请求尚未缓冲的完整剩余长度;短读、连接结束、错误和取消
都会使当前交换失败。原生 Linux 回归覆盖至少 256 KiB 的 broker 响应。
- Renderer 把相邻 text/reasoning delta 合并为消息 block 时始终替换最后一个 block,
不修改既有 React state 对象。这样开发环境 StrictMode 重复调用 state updater 时,
block metadata 与 canonical 消息正文保持一致。
## Agent 开发期间的真实 Host 验证
任何会改变已部署 GoodBuddy Agent 或桌面到 Agent 生产链路的源码改动,都必须在开发期间
使用共享 Linux x64 真实测试 Host 验证。适用范围包括 Agent daemon、Agent protocol
及共享 contract、Agent 组包与安装、attach/update、Runtime 启动与进程归属、Workspace、
model bridge,以及生命周期和恢复逻辑。单元测试、mock、fixture、CI 或正式发布前回归
都不能替代这一步;不得等到准备发布时才首次运行受影响的真实链路。
测试 Host 有两个网络入口:
- `192.168.0.23`:局域网入口,在本地网络可达时优先使用;
- `10.7.0.23`:VPN 入口,局域网入口不可达时使用。
两者指向同一台物理 Host,只代表两条访问路由,不能计为两个独立 Host、两种架构或两份
覆盖结果。测试只使用已固定的 Host identity 和 GoodBuddy 已有凭据存储;地址不得写入
产品行为或默认配置,用户名、密码、私钥等凭据不得写入源码、文档、命令、日志或测试输出。
开发时应在改动链路达到可运行状态后尽早执行真实 Host 验证,并在最终相关改动后重复受
影响的场景。必须使用当前源码构建或当前候选包,不能只验证 Host 上已经发布的旧 Agent。
按改动范围至少选择以下场景:
1. Agent 组包、安装或升级:验证当前候选的 attach-or-bootstrap、安装/更新、版本切换及
随后的连接。
2. Runtime profile、进程启动/归属或 Ask/Execute 边界:通过桌面生产入口启动真实 Runtime,
验证受影响模式;Ask 权限改动必须验证只读边界,Execute 权限改动必须验证所需写入、
进程和网络能力。
3. Agent protocol、RPC、stream、model bridge 或模型消息链路:执行最小有界真实模型调用,
禁用非必要工具和附件,并记录实际调用次数;大消息或工具链路改动还应覆盖对应的真实
payload/工具场景。
4. 会话、owner、取消、超时、断线或恢复逻辑:执行与改动直接相关的中断、取消、Agent/
Runtime 重启、SSH 断开或重连序列,并确认不会错误重放结果未知的操作。
5. Workspace 或 Host 文件路径:只在 GoodBuddy 专用测试目录操作,验证真实路径和权限,
不得读取、覆盖或删除无关 Host 文件。
停止或修改远端进程前必须先核对其启动身份、PID/进程组和 GoodBuddy 归属。若两个入口均
不可达,应立即把真实 Host 验证报告为开发阻塞,保持该验证未完成,不能把 Agent 改动表述
为已完成,也不能把首次真实验证推迟到发布阶段。
## 已完成的 E2E 验收记录
- 2026-08 的本地 fixture 完整验证 Linux x64 Agent `0.11.2-e2e.12`、Node `24.19.0`
和 Agent protocol `2.0`;当时没有 arm64 fixture,因此该记录不能作为当前独立发布
的双架构验收。Agent `0.11.10` 后续已由原生 workflow 发布并公开验证双架构工件;
当前源码 lock 固定为 `0.11.14`;正式发布状态以独立 Agent Release 与签名 catalog 为准。
- 2026-08-30 在共享 Linux x64 Host 的隔离测试 HOME 中验证当前 `0.11.13` 源码候选:
签名 Runtime 清单的测试墙钟上限为 1 秒,模型桥首轮故意延迟 2.515 秒后 Prompt 仍在
5.770 秒正常完成;随后 OpenCode 原生 Task 依次产生 `running`、`completed` 子 Agent
事件并完成父请求。测试结束后隔离 Agent、Runtime、socket 和 Workspace 已全部清理,
Host current Agent 仍为 `0.11.10`。
- 2026-08-31 使用隔离 Linux x64 Agent `0.11.14` 候选完成 remote-authoritative 矩阵:
15 秒短 smoke、2 分钟正常 detach、5 分钟本机 harness 强制结束、并发对话、SSH relay
丢失、显式取消、确定性 Provider 错误、2 秒 Provider 结果未知、Agent `SIGKILL`/重启,
以及 reopened Desktop SQLite 恢复。成功工具的 START/END sentinel 都各出现一次;
Provider 结果未知与 Agent 重启只提交 `outcome-unknown`,未重放 Prompt、模型调用或工具。
- 同一矩阵中的 Desktop schema v32 实际恢复保持原 project/conversation/request/
assistant-message 身份,在同一事务中提交语义 provenance、消息与 cursor;重新打开
`assistant.sqlite` 后消息和 Task 都完整,重复 provenance 返回 false,recoverable Task
归零。
- 最终源码复核后重新构建测试签名 package:
SHA-256 `39307fd1dccd638dcb723027591bcd83deab7057d550c99eff6f037e339829f4`,
installation `agent-2cad05acfebe853394807e3a11f7befcd9f47215c2cf3a33a03d5f644b45716e`,
Runtime `1.18.9` / bundle
`sha256:6c7fc975415ef2547d3729bc4772d3e78362c2e76c05d1fc24968a0468fcb7ba`。
最终 15 秒 detach/recovery 使用两个模型轮次、一个工具和一个终态,ledger 两轮均
`completed + delivered`,语义 `latest=ACK=41`,保留事件、Runtime owner、ACP channel/
frame 都为 0,START/END 各一次。最终取消另使用一个模型轮次、一个工具和一个 cancelled
终态,语义 `latest=ACK=29`,清理计数同样为 0。最终补充验证共 3 次真实文本模型调用。
采证后已按 PID/starttime/executable 停止精确隔离 daemon 并删除隔离 Host 测试根目录;
`/root/.goodbuddy` 下的共享 Agent 安装和进程未被修改。
- 2026-08-31 使用当前工作树短期测试签名 Linux x64 候选,在全新隔离 HOME 和 installation
中验证有界诊断:bootstrap、detached daemon、attach、relay 断开与同 controller 重连、
`controller/resume`、Runtime candidate 激活和 OpenCode ACP 进程启动均通过;新
`diagnostics --installation-id` 命令在 daemon 重启后读取到 23 条完整 JSONL。诊断目录
为 `0700`、文件为 `0600`,单文件 3719 bytes,未超过 64 KiB。六类测试哨兵覆盖 Prompt、
密钥、环境、SSH 参数、用户文件内容和原始错误消息,CLI 输出与磁盘均未出现哨兵。把隔离
diagnostics 目录临时设为不可写后,Attach 和 `agent/status` 仍正常;随后已恢复权限。
本次未注入模型凭据或执行 Prompt,真实文本模型调用 0 次;三文件轮转继续由聚焦测试覆盖。
测试结束前按 PID、UID、starttime、executable 和唯一 installation 核对并停止所有本次
daemon,本地及 Host 临时目录均已清理,共享 current Agent 和 `/root/.goodbuddy` 未修改。
- 正常 Host 更新路径把 Linux x64 Host 的 Agent 更新为 `0.11.2-e2e.12`,并确认
OpenCode Runtime 已安装版本与所需版本均为 `1.18.9`。
- 一条新的 Ask 用户操作只提交一次。OpenCode 先在 build 模型轮次请求一个原生
`read`,读取专用测试 Workspace 中的证据文件,再在第二个 build 模型轮次生成最终
回答;没有 title 模型轮次,也没有第二个工具调用。
- 两个模型轮次都记录已交付,远端 helper 保持休眠且 Unix socket 没有未读响应积压。
持久化的 canonical 助手正文精确为 `GOODBUDDY_REMOTE_TOOL_4C6B9D8A`。验收操作没有
重放任何结果未知或已经完成的 Provider 请求。
## 项目保存
远程项目只持久化稳定配置:
- Host ID;
- 规范远端工作目录;
- Runtime selection;
- `ask | execute` 默认模式。
Agent installation、Host revision/Host Key generation、Workspace identity、Runtime
bundle/adapter digest 和 capability generation 都属于 live connection/lease,不进入项目
domain object 或 SQLite。打开托管 SSH 项目不接受 Renderer 回传的旧项目草稿,而是从 Host
store 解析当前连接目标,通过 Agent/Runtime 安装管理器的 current/activate 读取路径取得
registry identity,再用同一个 Agent 连接验证 Workspace 路径和 Runtime capability。该流程
不会扫描完整 payload、取得安装包或发布组件。
实时 Runtime 创建只接受 OpenCode,使用当前解析后的模型 profile 准备 Agent 本地 gateway,
从当前 Agent 连接和 Runtime registry 取得会话 identity,并在该连接上打开 Workspace 和
ACP channel。Host 编辑或环境更新会定向失效 Agent 连接与 Runtime 缓存,下一次请求自然
重新取得 current 环境;无需修改项目。Ask/Execute 权限继续由每次 Prompt 的 ACP Runtime
边界执行。
Renderer 在用户主动打开已有托管 SSH 项目时立即切换,不显示远程激活状态。Host、Agent、
Workspace、Runtime 和 Saving 进度仅用于新建或显式保存项目;进行中可显式取消并禁用会
产生冲突的项目创建和设置入口。远端 RPC 拒绝只向
Renderer 暴露固定方法名、数字 RPC code 和有界 service code,不转发 Host 私有错误详情。
`runtime/preparePrompt` 在远端明确拒绝且尚未接受 Prompt 时会关闭对应 Main binding,
不会把确定性失败错误保留为 `outcome-unknown`。未确认终态的 Prompt 仍不会自动重放;
同一当前 Agent identity 上的 `prompt-running` binding 会阻止不同的新 operation,直到
原 operation 恢复或取消,避免遗留 Host 工作与新 Prompt 并行。只有 Host/Agent/Runtime
identity 已变化且旧 authority 无法恢复,或原 binding 已有 `outcome-unknown` 终态时,显式
新 Prompt 才替换旧 binding。新 ACP session 的首次 Prompt 会把 Desktop 已持久化的有界
会话历史作为不可信数据一并发送;成功加载或恢复原 ACP session 时不会重复注入历史。
Ask 模式的 Linux Runtime 进程由 `bwrap` 启动后 `exec` 到已签名 Runtime 或 Agent
model bridge helper;监督器只接受同一 PID/进程组从固定 `bwrap` 路径到预期最终可执行文件
的有界转换,再登记为运行中进程。
不持久化 T2/T3、consent、approval bridge、confinement 或组件验证结果。数据库 schema
v32 保留项目及关联用户数据,并加入 remote-recoverable Task/message/provenance/cursor;
执行空间表保持 `project_id/kind/root_path/ssh_host_id`
四列,并删除旧 Runtime 验证表。
## Agent 有界诊断
Detached Agent 不依赖 stdio 留存故障证据。每个 installation 在固定私有目录
`~/.goodbuddy/state/<installationId>/diagnostics/` 中维护
`agent-diagnostics.jsonl`、`.1` 和 `.2`,每个文件不超过 64 KiB,目录权限为 `0700`,
文件权限为 `0600`。记录覆盖 daemon 启停、detached 启动、连接、恢复和 Runtime
启动/退出;每条只包含固定事件、时间、PID、可选工作模式、固定原因和白名单错误码。
诊断写入使用最多 64 条的有界异步队列。连接、恢复和 Runtime 回调只把已经归一化的固定
记录入队,不等待磁盘;原始 `Error` 不进入队列。队列满、目录不可写或轮转失败只丢弃诊断,
不得改变 Attach、协议、Runtime 或退出结果。daemon 停止和 bootstrap 生命周期结束时执行
有界 flush。多个 Agent 进程通过 GoodBuddy 私有短期锁协调 append 和轮转,不把长期锁作为
运行前提。
已安装 Agent 提供只读固定命令:
```text
goodbuddy-agent diagnostics --installation-id <installationId>
```
该命令只能从 installation 对应的固定 state 目录读取上述有界文件,并逐行输出已验证 JSONL;
不接受任意路径,也不扩展 Agent protocol。诊断不得包含 accepted Prompt、模型输入输出、
模型密钥、完整配置、环境变量、SSH 参数、用户文件内容、路径或原始错误消息。应用内反馈的
“附加最近桌面诊断记录”只读取 Desktop 诊断,不会自动取得或上传这些远端 Agent 记录。
## 资源与发布
- 所有桌面构建(包括 `portable` 与 `release:package`)只携带
`agent-runtime-lock.json`、`remote-runtime-lock.json` 与公开的
`agent-release-keys.json`,不嵌入 `.agent-resources`、
`.remote-runtime-resources` 或任何可安装 Linux 远端 payload。
- `build/agent-package.cjs` 在对应原生 Linux 架构组装确定性 `.gbagent`,外层签名覆盖
描述符和每个内部文件身份;内部 Agent 与 Runtime 仍分别使用既有签名 manifest 和 lock
校验,但所有 production 层统一使用一组 GoodBuddy 通用发布身份并通过签名域区分用途,
不要求内部 Runtime 单独配置密钥。`build/agent-catalog.cjs` 为双架构包生成签名累计目录,
拒绝同一版本/架构改变字节。
- `.github/workflows/agents.yml` 只使用进程内临时测试 key 做分支/PR 原生验证,不发布。
`.github/workflows/agent-release.yml` 才可在 annotated `agent-v<version>` 标签和受保护
`agent-signing` Environment 中构建 production 包。Agent GitHub Release 必须
`--latest=false`;北京 OSS 先写 `agent-releases/v<version>/`,GitHub 公开后才切换
单一 `agent-releases/latest.json` 指针,使客户端始终从同一不可变版本目录读取目录与签名。
- Main 的本地缓存只发布完整验证成功的内容。在线目录和包都绑定固定 GitHub/OSS URL、
大小和 SHA-256;选取规则是桌面最低版本满足、Agent protocol major 相同且 minor 不高于
当前支持值的最高 SemVer。缺少某架构仅使该架构 Host 的托管 SSH 无法激活。
- `agent-release-keys.json` 读取时按 UTF-8 JSON 解析并严格验证 schema、唯一 key ID、
规范 Base64、有效 Ed25519 key、环境和撤销列表,不把 CRLF/LF 或普通 JSON 空白作为
信任条件。上传到 Host 前重新序列化为确定性 LF JSON。签名目录、描述符、manifest 和
payload 仍按原始签名字节及 SHA-256 验证。
## 发布前完整回归(不能替代开发验证)
以下清单只用于复核开发期间已经在真实 Host 上通过的链路,不得作为 Agent 改动的首次
真实 Host 验证:
1. `npm test`
2. `npm run typecheck`
3. `npm run lint`
4. `npm run build`
5. 用测试签名复合包验证在线下载源选择和离线导入/导出;正式 Agent 候选还需公开校验
双架构 production 包与签名目录。
6. 在已固定 Host Key 的 Linux x64 测试 Host 上验证 attach-or-bootstrap。
7. 在 GoodBuddy 专用测试目录验证 Ask 无法修改文件。
8. 验证 Execute 可以写文件、启动进程和访问网络,同时不触碰无关 Host 文件。
9. 运行一次有界的真实模型调用,确认凭据不进入 Renderer、SSH 参数、远端环境或磁盘,
只在当前 accepted operation 生命周期内进入 Agent 内存。
10. 中断并恢复 SSH,确认活动 Runtime 不被网络抖动终止。
@@ -1,329 +0,0 @@
# SSH Host 远程环境准备与直连下载设计
## 状态
本文记录 2026-08-28 确认的目标设计及当前实施状态。源码已经实现 Host 手动准备、
GoodBuddy 传输、Host 直连下载、control-plane one-shot installer、发布后健康 adoption,
以及项目路径只验证已安装环境。Host 负责 Agent/Runtime 的安装与更新生命周期,项目流程
只消费已经准备好的 Host 环境。
当前源码可以直接使用既有 package format v1 `.gbagent`:归档已经包含固定 Agent 和
Node,installer 由 GoodBuddy 桌面控制面打包并通过固定 SSH prepare channel 单次发送,
不要求归档包含 `agent/lib/package-installer.cjs`,签名目录也不提供或检查额外的
bootstrap 能力元数据。公开直连能力仍须完成本文末尾 GitHub/北京镜像、Linux x64/arm64、
取消和离线 GoodBuddy 传输的真实 Host 验收;本文
不表示这些验收或新的 Agent 发布已经发生。当前实现细节同时以
[SSH 远程主机与 GoodBuddy Agent 实现说明](./remote-host-and-goodbuddy-agent-design.md)
和源码为准。
## 设计结论
将安装和更新从项目切换移到 Host 管理是合理的,原因如下:
- Agent、固定 Node 和 OpenCode Runtime 安装在 SSH 账号 Home 下,由同一 Host/账号上的
多个项目共享,不属于某一个 Workspace。
- 大文件传输发生在项目切换时会阻塞导航,也把 Host 级故障误显示成项目故障。
- Host Key、认证、平台和架构本来就在新增 Host 时验证,此时继续只读探测并在 Host 卡片
提供手动准备入口最容易理解。
- 项目只保存 Host ID、远端路径、Runtime 选择和默认工作模式。Agent installation、
Runtime digest、Workspace identity 和 Host revision 都从当次连接取得,不写入项目;
“移出安装”不等于“跳过当前环境检查”。
目标职责是:
| 流程 | 职责 |
| --- | --- |
| 新增或重新验证 Host | 验证身份和认证,保存 Host 并探测环境,不传输安装包 |
| Host 管理 | 用户明确刷新目标 Host,并可选择安装、同版本重修、取消和重试 |
| 创建远程项目 | 只读本地 Host 验证记录,保存时连接所选 Host 并验证当前 Agent、Workspace、Runtime 和模型配置,只持久化稳定配置 |
| 切换远程项目 | 只切换本地项目配置,不连接 Host;实际使用 Workspace 或 Runtime 时按需复用 Host 当前环境 |
## 不变量
- Host 配置与远程环境准备是两个事务。SSH 验证成功后先原子保存 Host、凭据和完整 Host
Key,再开始可能耗时的下载和安装。
- 环境准备失败或取消时保留已保存 Host,显示“未就绪”并允许重试,不创建半完成项目。
- 不把“环境健康”保存为长期可信布尔值。设置页从 Host 的签名 registry 和当前 Host
identity 派生版本状态;准备完成和实际使用时再执行 bootstrap/health。合法但暂未运行的
detached Agent 仍可显示为已安装,打开设置不能为了刷新卡片隐式启动或更新它。
- 所有安装继续使用 GoodBuddy-owned staging 和 side-by-side digest 目录。失败不得覆盖
可用旧安装,也不得删除 Host 上无关文件。
- Renderer 不接收下载 URL、缓存路径、签名 key、digest、SSH 命令或凭据。Main 负责来源
选择、可信目录解析、操作编排和 IPC 输入校验。
- 产品不提供“自动安装远程环境”开关。保存、重新验证或打开 Host 都不能自动传输
`.gbagent`;只有用户点击 Host 卡片的主按钮后,Main 才开始准备。
- Host 卡片只有一个主按钮,按版本事实显示“安装远程环境”“更新远程环境”或“重新安装”。
次级 SegmentedControl 选择“自动”“Host 下载”或“GoodBuddy 传输”;默认“自动”,只影响
下一次操作,不持久化为 Host 配置。
- “自动”只在 operation/prepare 前执行一次 Host capability probe:直连明确可用时选择
Host 下载,否则选择 GoodBuddy 传输。用户显式选择的方式保持有效;一旦 acquisition
已选定,prepare、commit 或 adoption 的任何失败都停止本次操作,不跨 acquisition
自动回退。
- 直连安装只要求已验证 SSH 连接和远端系统工具,不要求现有 Agent daemon。轻量能力探测
返回确定性缺少工具、空间、权限或来源不可达时说明原因;探测本身异常时仍允许用户点击
直连安装,由实际安装流程重新解析签名候选并通过 SSH 重试。
- 项目路径不得为了“方便”回退到隐式安装。Host 未就绪时应引导到 Host 设置,不在项目
切换器中启动大文件传输。
## 新增 Host 流程
新增向导保持现有 Host Key 和认证顺序,并在系统探针之后显示一次只读环境结果:
1. 输入名称、地址、端口、用户名和认证方式。
2. 在发送认证材料前读取并确认 Host Key。
3. 完成 SSH 认证以及平台、架构、Shell、Home 有界探针。
4. 原子保存 Host、加密凭据和完整 Host Key。
5. 读取 Host 已安装的 Agent/Runtime registry,并执行一次轻量下载能力探测。
6. 如果已安装 identity 满足当前 GoodBuddy 要求,显示“版本匹配”并结束;该 badge 不代表
Agent 正在运行或环境健康。
7. 如果组件缺失或需要更新,显示目标 Agent、Node、Runtime、架构、来源和完整包大小,
告知用户保存后前往 Host 卡片安装,然后结束新增向导。
新增流程到此结束,不显示自动安装开关,也不自动开始环境安装。保存后的 Host 卡片提供
一个手动准备主按钮,并以次级 SegmentedControl 选择“自动”“Host 下载”或
“GoodBuddy 传输”。主按钮按版本事实显示“安装远程环境”“更新远程环境”或“重新安装”。
本地尚无精确包不要求用户先离开当前页面;选择 GoodBuddy 传输时,同一次操作会取得并
验证候选后继续上传。
探测不得自动下载完整 `.gbagent`。只有用户明确启动安装后才允许传输大包。
## Host 编辑
- 仅修改显示名称不要求重装远端环境。
- 地址、端口、用户名或 Host Key 变化时,旧 Host identity 立即失效,关闭旧连接和对应
Runtime 缓存。项目记录不含旧组件 identity;下一次打开直接解析新 Host 的当前环境。
保存新身份后只重新执行环境探测,所需安装由用户在 Host 卡片手动启动。
- 只替换同一目标的认证材料时仍重新认证并读取实际环境;如果签名 identity 和健康状态
未变化,不重复安装。
- 当前项目引用的 Host 被编辑时,Main 立即失效该 Host 的 identity 和连接缓存;下一次
Workspace/Runtime 操作按需使用新目标。环境未准备好时该操作失败,不回退旧连接。
## 下载能力探测
探测在 Host Key 已固定且 SSH 认证成功后执行,只检查当前选择的“关于与更新”来源:
- Host 是受支持的 Linux `x64` 或 `arm64`。
- SSH 账号 Home 可写,GoodBuddy-owned staging 可创建。
- 可用空间满足签名目录声明的包大小、解包上限和安全余量。
- Host 可以通过 HTTPS 访问所选 GitHub 或北京镜像 URL。
- 首次安装所需的下载、SHA-256 和运行归档内固定 Node 的能力可用。
- 已安装 Agent 是否报告未来的直连更新能力。
探测使用小型签名目录或有界请求,不以一次成功 `HEAD` 代替真实安装验证。结果只用于当次
展示,不持久化为永久能力;开始安装前重新检查,因为网络、代理和磁盘状态可能变化。
探测失败不能阻止保存 Host,也不能禁用显式 GoodBuddy 传输或离线导入路径。显式选择
Host 下载时由实际操作报告不可用原因;“自动”只把“直连明确可用”作为选择 Host 下载的
条件,其他结果选择 GoodBuddy 传输。
## 工件选择与来源
Main 继续读取并验证 `agent-catalog.json`、`agent-catalog.sig` 和 production 公钥
registry,按以下条件选择唯一候选:
- Host 平台和架构匹配;
- 最低桌面版本满足;
- Agent protocol major 相同,minor 不高于桌面支持值;
- 使用满足条件的最高 SemVer;
- URL、文件名、大小和 SHA-256 来自签名目录;
- 下载来源跟随“关于与更新”的 GitHub/镜像选择。
远程下载只优化大包的网络路径。Main 仍需先取得并验证小型目录,再通过经过 Host Key
验证的 SSH 控制面传递固定候选元数据。产品不接受 Renderer、项目、Host 文件或用户输入的
任意下载 URL。
## 获取与统一安装事务
首次安装不能假设 Host 已有 GoodBuddy Agent、installer 或系统 Node,也不由既有 Agent
daemon 参与。两种 acquisition 的差别只在于如何把同一个签名 compound `.gbagent` 交付到
固定 operation staging:
- **Host 下载**:控制面通过固定 SSH prepare channel 发送签名目录绑定的 URL、大小和
SHA-256;Host 有界下载并校验完整归档。
- **GoodBuddy 传输**:显式选择时,在线目录不可用或本地已验证包版本不低于在线候选,
则优先使用本地包,包括版本更高的离线导入;线上存在更新且可用时仍取得线上新包。
需要下载时,同一次用户操作会验证 SHA-256 和签名、写入缓存并取得 lease。随后以有界
流式 SFTP 上传一个 compound archive,以及从该归档中已验证的 bootstrap Node。约
294 MiB 的完整包不会一次读入 Main `Buffer`,Host 收到后仍再次校验完整包。
交付后两种 acquisition 完全共用一个 control-plane 事务:
1. Main 创建带 operation ID、总时限、输出上限和取消信号的准备操作,并通过固定
`prepare` channel 发送有界 one-shot installer 与结构化输入。
2. Host 在解包写入准备目录的同一遍处理中校验归档大小、SHA-256、外层及内层签名、
平台、架构、协议和每个 payload digest;不再先完整扫描 payload、再重复解包和哈希。
3. `commit` 只复核有界的签名 manifest、准备回执和入口 metadata,再以目录 rename 发布
已准备内容及包内认证的公钥 registry/Runtime lock;不会重读归档或遍历 payload。
4. commit 后在同一条已固定身份的 SSH lease 上执行 Agent bootstrap/readiness 和 Runtime
activate。新 Agent 最多完整验证一次,新 Runtime 最多完整验证一次;注册成功后的
health、capabilities 和 prompt 启动只读取 registry、签名 manifest 与入口 metadata。
5. Main 确认 adoption 后显式执行 cleanup。cleanup 失败不回滚已经健康的 Agent/Runtime,
也不阻塞下一次更新。
这不是两个并行安装管理器,也不存在把 Agent 与 Runtime payload 逐文件走 SFTP 安装的
第二条路径。两种 acquisition 共享相同的 prepare、commit、adoption、finalize 与 cleanup。
Host 缺少可靠的下载、SHA-256 或 ZIP 能力时,不自动安装系统软件,也不要求 root。显式
Host 下载返回明确失败;本次操作不自动切换 acquisition,用户可改选 GoodBuddy 传输后
重新点击主按钮。后续更新和同版本重装复用同一事务,不要求用户先通过项目切换完成
“过渡升级”。
## 本机传输与离线路径
现有 `.gbagent` 本地缓存、在线下载、离线导入和导出继续保留:
- GoodBuddy 路径在本地缺少精确候选时,于同一次操作完成下载、整包大小与 SHA-256、
全部签名和 payload 验证、缓存发布及 lease 获取。
- SFTP 有界流式上传单个 compound archive 与其中已验证的 bootstrap Node 到操作自己的
staging;Main 不把完整归档读入内存,Host 再次校验后进入统一 control-plane 事务。
- Host 无公网、代理/TLS 异常或缺少 bootstrap 工具时,用户可显式使用此路径;离线导入
后也走同一缓存 lease、上传和 Host 校验流程。
- 本地项目和普通桌面打包不依赖任何远端 Agent 工件。
## 完整包与发布
第一阶段继续发布和下载完整复合 `.gbagent`,不同时引入组件化增量发布:
- 远程直连和 GoodBuddy 本机下载都会传输完整包,其中包含 Agent、固定 Node 和 OpenCode
Runtime。
- 每次显式安装或重装都从本次已经校验并解包的准备目录原子替换对应 Agent 与 Runtime
digest 目录,不为节省本机磁盘写入而重新遍历旧目录,也不使用跨安装硬链接。
- 操作中断后不续写旧 operation。下一次更新尽力清理其 staging,并从签名候选重新
prepare;正常 cleanup 随后删除本次准备目录。
只有真实包体和发布频率证明网络成本仍不可接受时,才另行设计 Agent、Node 和 Runtime
独立签名组件目录;本功能不提前引入第二套发布协议。
## Host 准备状态
设置中的 Host 卡片分开表达版本事实和操作结果:
- **未验证**:Host Key、认证或系统探针尚未完成。
- **尚未安装**:Host 可连接,但 Agent 或 Runtime 缺失。
- **需要更新**:已安装 identity 与当前兼容候选不一致。
- **正在准备**:正在探测、下载、验签、安装或健康检查。
- **版本匹配**:版本 badge 只表示 Agent/Runtime identity 与当前兼容候选匹配,不等同于
进程正在运行或环境健康;设置页普通刷新不会启动已停止的 Agent。
- **准备失败**:保留 Host 和可用旧组件,展示有界错误与重试入口。
主按钮按版本事实显示“安装远程环境”“更新远程环境”或“重新安装”。重新安装失败后,
界面显示本次操作未完成,并以随后重新检查的版本卡片表达当前事实,不在提交结果不确定时
断言旧版本未被替换,也不得把“版本匹配”改称“环境健康”。统一进度以 `探测`、
`下载/传输`、`校验`、`应用环境`、`Agent 激活与健康检查`、`Runtime 激活` 和
`完成设置` 表达用户可理解的阶段。
同一 Host identity 同时只允许一个由用户启动、Main-owned 的准备操作。重复观察者加入
已有操作,不启动第二次发布;窗口关闭、显式取消、Host identity 改变或 Main 退出时执行
有界取消。Main 只为该 Host 持久化一个不含凭据的 version-1 pending operation ID,用于
下一次操作开始时尽力清理旧 staging。无论旧 cleanup 成功、目标已变化还是旧记录损坏,
都删除这条可丢弃记录并开始 fresh prepare;不保存远端 metadata 副本,不续写旧 commit,
也不跨重启恢复 adoption。当前操作中可确定的失败会立即尽力 cleanup。环境已健康后的
cleanup 失败不回滚环境,也不阻塞下一次更新。
## 项目创建与切换
- 新建托管 SSH 项目弹窗只读取本地 Host Key/连接验证记录,不逐台连接或刷新版本。只有
已完成本地验证的 Host 可选;浏览目录或保存时才连接所选 Host,并由保存流程完整检查
Agent、Workspace 和 Runtime,但不隐式安装。
- 项目保存验证 Host 当前 revision 与 Host Key generation、当前 Agent、Workspace、
Runtime、模型 profile 和 Ask/Execute 配置;SQLite 事务只写入 Host ID、规范远端路径、
Runtime 选择和默认工作模式。
- 打开或切换已有项目只提交本地项目选择,不执行 SSH、registry、Workspace 或 Runtime
检查。首次实际使用远程 Workspace 或 Runtime 时才解析 Host 管理的当前 Agent/Runtime,
并建立或复用当前 Agent 连接。已安装 Agent 暂未运行时,
固定 `attach-or-bootstrap` 命令幂等启动 registry 中的 current identity;这里的
bootstrap 只启动进程,不安装组件,也不重新读取、哈希或验签完整 payload。
Agent 进程读取 Host 管理已提交的 registry 和匹配 manifest 元数据,不重新验签或扫描
完整安装 payload。
- Workspace 和 Runtime 使用当前 Agent 连接产生的 live handle/lease。项目不保存或核对
历史 Workspace/Runtime identity;当前 registry、连接状态或 capability 无效时停止并
引导到 Host 管理,不在项目流程中下载或安装。
- 桌面更新导致 Host 组件过旧时,项目保持未切换并提示前往 Host 设置。应用启动仍进入
第一个普通本地项目,不自动连接或更新远端 Host。
- Host 环境更新成功后,Main 定向失效该 Host 的 Agent/Runtime identity、连接和 Runtime
会话缓存。所有项目在下一次实际操作时自然使用新的 current 环境;无需改写项目记录。
- 环境准备失败时不改变已有项目绑定。Host identity 编辑失败时则继续保持 fail-closed,
因为旧身份不能替代用户刚保存的新身份。
- 项目管理入口与项目选择分离。项目选择器中的浮动管理按钮直接打开目标项目设置;删除
不访问 Workspace、Agent 或 Host,因此远端目录已删除或 Host 不可达都不能阻止本地
项目记录清理。删除当前项目或 Host 级联删除当前项目时只回退到普通本地项目。
- 项目选择器把托管 SSH 项目按已保存 Host 分组,并只读取 Main 当前持有的 Agent 连接
状态显示“未连接 / 连接中 / 就绪 / 异常”;打开菜单或读取状态不得隐式连接 Host。
Host 已承担类型与连接语义,项目副标题只显示远端工作目录。
- Host 删除确认使用本地数据库列出全部引用项目,包括归档项目;确认后级联删除本地项目
及其会话、任务、成果、记忆、计划和心跳关联,再删除本地 Host 与凭据。整个流程不发起
SSH,也不删除远端内容;跨 Host 设置文件与 SQLite 的普通失败会恢复已写入的一侧。
项目切换不展示远程激活进度,也不阻塞项目导航。`Host`、`Agent`、`Workspace`、`Runtime`
和 `Saving` 进度只属于新建或显式保存项目;实际 Workspace/Runtime 操作使用正常的局部
加载和错误反馈。
## 实现结构
当前实现复用现有机制,没有新增平行的安装管理器:
- 扩展当前 Host 系统探针和远程环境 inspector,返回瞬时下载准备能力。
- 让当前 `RemoteEnvironmentUpdateService` 演进为 Host 环境准备编排器,统一用户手动
启动的首次安装和后续更新、自动 acquisition 选择、进度、取消和最终失效。
- Host 环境准备编排器直接完成一次 package prepare/commit 及 Agent/Runtime adoption。
Workspace 和执行路径只读取 current registry 并调用 Host 侧轻量 identity/health 检查,
在当前进程内复用已确认 identity 和 Agent 连接;Host 编辑或环境更新定向清空这些缓存。
这些路径不会取得安装包、发布组件或通过 SFTP 扫描 payload。
- Host 卡片即使显示相同版本也保留显式重装操作,用于修复 registry、签名或 payload
identity 异常;若同一 digest 的 GoodBuddy-owned 目录校验失败,重装会先隔离旧目录,
并在原子 commit 失败时恢复旧目录。commit 成功后的 adoption 失败保留新发布内容,
下次重装清理旧暂存并重新 prepare/adopt。重装不要求删除 Host、凭据或项目,也不会
处理所有者异常、符号链接或非 GoodBuddy 管理路径。
- 由桌面控制面打包有界 one-shot installer,并通过固定短命令 `exec sh -s` 和 stdin
发送脚本及结构化输入,避免 SSH command 长度限制;既有 package format v1 归档内固定
Node 负责解码和运行它。
- Preload 只暴露明确的 Host 探测、准备、取消和进度方法,不暴露 URL 或任意 SSH 命令。
- 现有 Agent/Runtime side-by-side、固定 SSH lease、Host 端一次完整验证、注册后轻量检查、
连接失效和项目稳定配置事务继续作为唯一实现。
## 迁移
- 已保存 Host 打开设置时只显示本地记录,不自动连接或检查版本;用户点击对应 Host 的
“刷新版本”后才执行只读状态探测。项目切换不依赖该按钮,也不建立连接。
- 数据库升级保留远程项目、Host ID、远端路径、Runtime 选择及关联会话/任务,并删除旧的
Agent/Runtime/Workspace/Host 验证列和 Runtime 验证表。下一次实际远程操作从 Host
current 环境重新取得所有 live identity。
- 如果 Host 未就绪或版本不兼容,项目仍可在本地选择,但实际远程操作失败;不再走旧的
隐式安装。
- 已下载或导入的本地 `.gbagent` 保持可用,不能因加入远程直连而迁移或删除。
- pending operation 保持 version 1,只记录 Host、目标 identity 与 operation ID,用于
尽力清理旧 staging;它不是远端环境状态或恢复日志。
- 远程直连继续使用现有 package format version 1,不要求归档携带 installer,也不要求
目录声明额外的 bootstrap 能力元数据。
## 验收标准
1. 新 Host 保存和探测完成后不会自动下载、上传或安装任何包,也不存在自动安装开关;
再次进入 Host 设置不会逐台连接,只有显式“刷新版本”才探测目标 Host。
2. Host 卡片只有一个按版本事实命名的主按钮;次级 SegmentedControl 提供默认且不持久化的
“自动”及显式“Host 下载”“GoodBuddy 传输”。没有预装 Agent、Node 或 Runtime 的 Host
可以完成准备;远程下载不可用时可手动改用 GoodBuddy 传输。
3. 探测不下载完整包,失败不阻止保存 Host;非确定性探测失败不禁用直连重试,安装失败
或取消后 Host 仍可重试。
4. 直接下载严格匹配签名目录中的 URL、大小和 SHA-256;GoodBuddy 传输可在同一次操作中
下载并验证缺失候选,以有界流式 SFTP 上传完整归档和已验证 bootstrap Node。Host 对
两者完成相同的外层与内层签名、路径、架构、协议和 payload 校验。
5. Host 对本次候选只完成一次完整 payload 校验;commit、adoption、health、capabilities
和 prompt 启动不得再次遍历或哈希完整 Agent/Runtime,显式重装仍能用本次准备内容修复
同 digest 的损坏目录。
6. 创建、打开和切换项目不会产生 `.gbagent` 下载、Agent payload SFTP 上传或安装目录发布。
7. 未就绪 Host 不能创建远程项目;已有项目仍可本地选择,但远程操作失败并引导到 Host 设置。
8. Host 编辑或更新后,引用项目使用 Host 管理的当前连接和 current registry,不读取项目
中的旧 Host/Agent/Runtime/Workspace identity;当前环境无效时才停止并要求在 Host 管理
中修复。
9. Host 地址、用户或 Host Key 在准备期间变化时停止候选发布,旧身份不会被重新启用。
10. GitHub、北京镜像、无公网 Host、取消、应用退出、磁盘不足、损坏下载和不支持架构均有
明确结果,且不触碰 GoodBuddy-owned 目录之外的文件。
11. Linux x64 和 arm64 均通过 GitHub/北京镜像、取消、离线 GoodBuddy 传输,以及项目使用
Host current 环境执行 Workspace/Runtime 操作的真实 Host 测试;这些真实 Host 验收当前
仍待完成。
12. Host 不可达或远端目录不存在时,用户仍可从项目浮动管理入口删除本地项目;删除 Host
会列出并本地清理全部关联项目,但不会连接 Host 或删除任何远端目录和内容。
13. 中断或 adoption 失败后的下一次更新只尽力清理旧 staging,然后 fresh prepare;
pending record 不包含远端 metadata 副本。已健康环境的 cleanup 失败不触发回滚,
也不阻塞下一次更新。