- 人工智能
- AI Agent
- AI 应用
- 移动开发
- CLI
- 后端
【免费下载链接】happy
Mobile and Web client for Codex and Claude Code, with realtime voice, encryption and fully featured
本文是 Happy 项目针对 OpenCode 做的一轮协议层运行时追踪:从源码启动 OpenCode 服务、复制全局认证到隔离临时根目录、驱动真实样例项目,只相信「发给 OpenCode 的请求体、OpenCode 返回的 JSON、/event的原始 SSE 日志、以及 OpenCode 源码」四类证据,从而回答一个核心问题——OpenCode 的实时同步与转写模型到底是什么样,Happy 的转写与同步层又该从中借鉴什么。读完本文,你将掌握 OpenCode 目录路由、权限询问、媒体输入、子会话四类真实协议的完整载荷与事件序列,并理解docs/plans/provider-envelope-redesign.md中「消息 + 部件」改造方案的证据来源。
一、追踪背景与证据边界
这份追踪记录撰写于 2026-03-21,是较早一轮竞品分析缺失的协议级补充。其方法非常直接:运行 OpenCode 源码,把全局安装里的认证复制进一个隔离的临时根目录,驱动一个真实的小项目,并且只信任以下证据:
- 发送给 OpenCode 的精确请求体;
- OpenCode 各端点返回的精确 JSON 响应;
- OpenCode
/event的原始 SSE 日志; - OpenCode 源码本身。
在 Happy 一侧,这份文档只信任 Happy 仓库内的代码。换言之,这是一份「用真实运行轨迹说话」的对照文档,而不是纸面推测。
1.1 实际使用的环境
| 项 | 值 |
|---|---|
| OpenCode 源码检出 | ../happy-adjacent/research/opencode(作者本地外部检出目录) |
| 检出版本 | commit2e0d5d230893dbddcefb35a02f53ff2e7a58e5d0 |
| 样例项目 | 仓库内的 environments/lab-rat-todo-project(纯前端静态小项目:index.html、app.js、styles.css、README.md) |
| 隔离运行时根 | /tmp/opencode-trace-dev.ptZAVJ |
| 认证来源 | ~/.local/share/opencode/auth.json,复制到/tmp/opencode-trace-dev.ptZAVJ/share/opencode/auth.json,复制后的认证文件中仅保留了openai一个 provider key |
1.2 服务端启动命令
XDG_DATA_HOME=/tmp/opencode-trace-dev.ptZAVJ/share \ XDG_CACHE_HOME=/tmp/opencode-trace-dev.ptZAVJ/cache \ XDG_CONFIG_HOME=/tmp/opencode-trace-dev.ptZAVJ/config \ XDG_STATE_HOME=/tmp/opencode-trace-dev.ptZAVJ/state \ OPENCODE_CONFIG_DIR=/tmp/opencode-trace-dev.ptZAVJ/profile \ OPENCODE_DB=/tmp/opencode-trace-dev.ptZAVJ/share/opencode/opencode.db \ bun run --cwd packages/opencode --conditions=browser src/index.ts \ serve --hostname 127.0.0.1 --port 4098 --print-logs --log-level DEBUG这套命令的要点在于:通过XDG_*系列环境变量把 OpenCode 的数据、缓存、配置、状态全部隔离到/tmp/opencode-trace-dev.ptZAVJ下,OPENCODE_CONFIG_DIR指定独立配置目录,OPENCODE_DB显式指定 SQLite 数据库路径;服务监听127.0.0.1:4098,并以 DEBUG 级别打印日志。这样既能复用全局认证(只含 openai key),又不会污染真实环境。
支撑这套实测的关键 OpenCode 源码文件(作者本地检出,路径从../happy-adjacent/research/opencode起):
packages/opencode/src/auth/index.tspackages/opencode/src/server/server.tspackages/opencode/src/server/routes/experimental.tspackages/opencode/src/session/message-v2.tspackages/opencode/src/session/prompt.tspackages/opencode/src/tool/task.tspackages/opencode/src/permission/index.ts
二、什么是「真实」:证据分散在四个面
追踪得出的第一个重要结论是:OpenCode 有用的协议证据并不是一个大的隐藏 RPC 信封,而是分散在四个面上:
- 发送到
POST /session/:id/prompt_async的请求体; - 从
GET /session/:id/message读到的持久化消息行; - 从
GET /event获得的实时补丁流; - 控制面端点如
/path、/permission、/session/:id/children、/experimental/worktree、/experimental/workspace的响应。
这个区分对 Happy 至关重要。OpenCode 并没有一条「一次性提供给 UI 全部所需」的 append-only 转写流,而是由四类机制协同:
- 带类型部件的稳定消息行(stable message rows with typed parts);
- 针对这些行的实时补丁事件(live patch events);
- 权限与会话状态的一等旁路事件(first-class side-channel events);
- 转写之外的独立 workspace / worktree 路由。
三、Flow 0:目录路由、worktree、workspace 与「sandbox」的真实含义
在触碰任何 prompt 之前,先验证服务端如何限定请求范围。关键路由输入是请求头:
x-opencode-directory: /Users/kirilldubovitskiy/projects/happy/environments/lab-rat-todo-project真实的GET /path响应:
{ "home": "/Users/kirilldubovitskiy", "state": "/tmp/opencode-trace-dev.ptZAVJ/state/opencode", "config": "/tmp/opencode-trace-dev.ptZAVJ/config/opencode", "worktree": "/Users/kirilldubovitskiy/projects/happy", "directory": "/Users/kirilldubovitskiy/projects/happy/environments/lab-rat-todo-project" }当前项目对应的真实空列表:
GET /experimental/worktree -> [] GET /experimental/workspace -> []这意味着:
- 项目限定是请求路由问题,不是转写问题;
- 当前项目位于一个更宽的
worktree根目录之下,以及一个更窄的directory之内; - worktree 与 workspace 是显式的控制面资源;
- 这些内容不会以
type: "sandbox"或type: "workspace"之类的转写部件出现。
所以当 OpenCode 的产品语言说「sandbox」时,落在具体实现上主要是三件事:目录限定、可选的 workspace 路由、可选的 git worktree 管理。它不是Happy 已经拥有代码的那种 OS/文件系统/网络沙箱策略——后者可以参考 packages/happy-cli/src/sandbox/config.ts 中的buildSandboxRuntimeConfig,它按sessionIsolation(strict/workspace/custom)构造文件系统allowWrite/denyRead/denyWrite白名单,并按networkMode(blocked/allowed/custom)配置allowedDomains/deniedDomains/allowLocalBinding等网络策略。这是两种完全不同的「沙箱」语义。
四、Flow 1:围绕apply_patch的权限询问——最完整的一条真实链路
这是整个追踪中最干净的一条链路,因为它一次跑通了:用户 prompt 创建 → 助手步骤生命周期 → reasoning → 工具调用 → 权限请求 → 权限回复 → 文件编辑副作用 → 助手最终跟进。
4.1 会话创建(强制询问的编辑权限规则)
POST /session { "title": "trace permission ask", "permission": [ { "permission": "edit", "pattern": "*", "action": "ask" } ] }会话创建时显式带上权限规则,把edit的*模式设为ask,从而强制触发权限询问。
4.2 实际发送的 Prompt 请求体
POST /session/{sessionID}/prompt_async { "agent": "build", "model": { "providerID": "openai", "modelID": "gpt-5.4-mini" }, "parts": [ { "type": "text", "text": "Create a new file named TRACE_PERMISSION.md in the current directory with exactly one line: rat permission trace. Then reply with one short sentence." } ] }注意:prompt 本身就是「带类型部件的数组」,agent与model在消息外层显式声明。
4.3 用户消息被持久化
{ "info": { "role": "user", "id": "msg_d0f8263b50016s8bKlZ36Te52c", "sessionID": "ses_2f07d9c71ffeikGiLoOKqF2Evb" }, "parts": [ { "type": "text", "text": "Create a new file named TRACE_PERMISSION.md in the current directory with exactly one line: rat permission trace. Then reply with one short sentence." } ] }消息被持久化为info(角色、id、会话 id)+parts(有序类型部件)两层结构,与请求体形态一致。
4.4 实时权限事件(SSE)
真实 SSE 事件:
{ "type": "permission.asked", "properties": { "id": "per_d0f826ef60011FReJ16cM2d0MK", "sessionID": "ses_2f07d9c71ffeikGiLoOKqF2Evb", "permission": "edit", "patterns": [ "environments/lab-rat-todo-project/TRACE_PERMISSION.md" ], "always": ["*"], "tool": { "messageID": "msg_d0f8263b9001UcnDrNrnbyYeyT", "callID": "call_uJj6gIQfIPpSoBV9oOWBT7cF" }, "metadata": { "filepath": "environments/lab-rat-todo-project/TRACE_PERMISSION.md", "files": [ { "relativePath": "environments/lab-rat-todo-project/TRACE_PERMISSION.md", "type": "add", "after": "rat permission trace\n", "additions": 1, "deletions": 0 } ] } } }这是追踪里最重要的发现之一:OpenCode 的权限不是简单的「工具 X 想要批准」。这个请求携带了:
- 权限种类(
edit); - 精确路径模式(
patterns); - 稳定的请求 id(
per_...); - 与工具调用的回链(
tool.messageID+tool.callID); - 一份可直接渲染的 diff 载荷(
metadata.files带type、after、additions、deletions)。
也就是说,权限事件本身就为 UI 准备好了审批界面的全部素材。
4.5 工具调用前后的助手消息
批准后持久化的助手消息:
{ "info": { "role": "assistant", "finish": "tool-calls", "id": "msg_d0f8263b9001UcnDrNrnbyYeyT" }, "parts": [ { "type": "step-start", "snapshot": "dfd3f0873ec51c2ddbf0b6b79acc154e5ab15c5d" }, { "type": "reasoning", "text": "**Creating a file**\n\nI need to create a file...", "metadata": { "openai": { "itemId": "rs_...", "reasoningEncryptedContent": "gAAAAA..." } } }, { "type": "tool", "callID": "call_uJj6gIQfIPpSoBV9oOWBT7cF", "tool": "apply_patch", "state": { "status": "completed", "input": { "patchText": "*** Begin Patch\n*** Add File: TRACE_PERMISSION.md\n+rat permission trace\n*** End Patch" }, "output": "Success. Updated the following files:\nA environments/lab-rat-todo-project/TRACE_PERMISSION.md" } }, { "type": "step-finish", "reason": "tool-calls" } ] }随后 OpenCode 又发出第二条助手消息承载最终可见文本:
{ "info": { "role": "assistant", "finish": "stop" }, "parts": [ { "type": "step-start" }, { "type": "text", "text": "Done." }, { "type": "step-finish", "reason": "stop" } ] }关键点:工具调用携带自己的状态机(state.status:pending → running → completed),且step-start/step-finish是独立部件,reasoning 也是独立部件,最终文本又落在单独的 assistant 消息里。整条链在持久化层是自解释的。
4.6 实际看到的实时事件序列
原始 SSE 流的顺序(18 步):
session.createdmessage.updated(用户消息)message.part.updated(用户text部件)session.status→busymessage.updated(助手消息壳)message.part.updated→step-startmessage.part.updated→reasoning- 大量
message.part.delta块流式推送 reasoning 文本 message.part.updated→ 工具部件status: "pending"permission.askedpermission.repliedfile.editedfile.watcher.updatedmessage.part.updated→ 工具部件status: "running"message.part.updated→ 工具部件status: "completed"message.part.updated→step-finish- 第二条助手消息携带最终
text session.status→idle
文件确实在磁盘上被创建:
TRACE_PERMISSION.md: rat permission trace五、Flow 2:媒体输入失败路径——provider 拒绝前的诚实存储
失败场景很有价值,因为它展示了 provider 拒绝请求之前 OpenCode 到底存了什么。
5.1 Prompt 请求体
{ "agent": "build", "model": { "providerID": "openai", "modelID": "gpt-5.4-mini" }, "parts": [ { "type": "text", "text": "Describe the attached image in one short sentence. Do not use any tools." }, { "type": "file", "mime": "image/png", "filename": "tiny.png", "url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO7Z0XQAAAAASUVORK5CYII=" } ] }5.2 用户消息被持久化(原样)
{ "info": { "role": "user" }, "parts": [ { "type": "text", "text": "Describe the attached image in one short sentence. Do not use any tools." }, { "type": "file", "mime": "image/png", "filename": "tiny.png", "url": "data:image/png;base64,iVBORw0K..." } ] }5.3 助手错误被持久化
{ "info": { "role": "assistant", "error": { "name": "APIError", "data": { "message": "The image data you provided does not represent a valid image. Please check your input and try again.", "statusCode": 400, "isRetryable": false, "metadata": { "url": "https://api.openai.com/v1/responses" } } } }, "parts": [] }实时流同时发出了session.error。这是一个 OpenCode「诚实」的好例子:用户侧的file部件被原样存储,失败则成为助手/会话的错误状态,而没有被「规范化」掉。对 Happy 而言,这意味着消息层必须能表达失败,而不是把失败折叠进某个模糊的文本里。
六、Flow 3:媒体输入成功路径——本地文件被 OpenCode 自行解析
成功路径完全不同,因为输入 URL 是本地file://...,由 OpenCode 自己解析。
6.1 Prompt 请求体
{ "agent": "build", "model": { "providerID": "openai", "modelID": "gpt-5.4-mini" }, "parts": [ { "type": "text", "text": "Describe the attached image in one short sentence. Do not use any tools." }, { "type": "file", "mime": "image/png", "filename": "logo.png", "url": "file:///Users/kirilldubovitskiy/projects/happy/logo.png" } ] }6.2 规范化后的用户消息
{ "info": { "role": "user" }, "parts": [ { "type": "text", "text": "Describe the attached image in one short sentence. Do not use any tools." }, { "type": "text", "synthetic": true, "text": "Called the Read tool with the following input: {\"filePath\":\"/Users/kirilldubovitskiy/projects/happy/logo.png\"}" }, { "type": "file", "mime": "image/png", "filename": "logo.png", "url": "data:image/png;base64,iVBORw0K..." } ] }这是session/prompt.ts的真实行为(原文档明确标注,非猜测):OpenCode 注入一条synthetic: true的文本部件描述这次读取,再把媒体本身作为file部件存储,且本地文件被解析成具体的data:URL。
6.3 助手响应被持久化
{ "info": { "role": "assistant", "finish": "stop" }, "parts": [ { "type": "step-start" }, { "type": "reasoning", "text": "", "metadata": { "openai": { "itemId": "rs_...", "reasoningEncryptedContent": "gAAAAA..." } } }, { "type": "text", "text": "A cute cartoon otter is lounging in water while using a laptop." }, { "type": "step-finish", "reason": "stop" } ] }6.4 实时流证明了什么
/event日志显示:message.part.updated(合成读取文本)→message.part.updated(file部件)→ reasoning 创建 → 流式message.part.delta(助手文本)→session.status回到idle。
所以真实的媒体故事是三段式:
- 用户侧 prompt 部件:
type: "file"; - 内部转写展开:
synthetic text+ 具体file; - 助手侧回答:普通文本输出。
七、Flow 4:子任务 / 子会话 / 权限约束——与 Happy 对照最重要的一环
这是四类追踪里与 Happy 做并排对照最重要的一条。
7.1 Prompt 请求体(含 agent 部件)
{ "agent": "build", "model": { "providerID": "openai", "modelID": "gpt-5.4-mini" }, "parts": [ { "type": "text", "text": "Find the main files in this tiny project and report back briefly." }, { "type": "agent", "name": "explore" } ] }7.2 用户消息被改写
OpenCode没有只存原始的agent部件,而是把用户消息重写为:
{ "info": { "role": "user" }, "parts": [ { "type": "text", "text": "Find the main files in this tiny project and report back briefly." }, { "type": "agent", "name": "explore" }, { "type": "text", "synthetic": true, "text": " Use the above message and context to generate a prompt and call the task tool with subagent: explore" } ] }同样是session/prompt.ts的真实行为:追加一条合成文本,把「派生子代理」这件事显式写进转写。
7.3 父会话的助手消息与task工具部件
{ "info": { "role": "assistant", "finish": "tool-calls", "id": "msg_d0f855de2001b0RbgA3JGA5lzk" }, "parts": [ { "type": "step-start" }, { "type": "reasoning", "text": "**Generating a task prompt**\n\nI need to call the task tool with the subagent explore..." }, { "type": "tool", "callID": "call_OqUEr7ccnf3zEb2rgLYDp5uR", "tool": "task", "state": { "status": "completed", "input": { "description": "Find main project files", "prompt": "Inspect the repository and identify the main files in this tiny project. Focus on the key entry points, config files, and any top-level files that define how the project runs. Return a brief list of the most important files with one short note each about what they appear to do. Keep it concise and do not modify anything.", "subagent_type": "explore" }, "output": "task_id: ses_2f07a8dd6ffeRc23sIIgM4ZpMT (for resuming to continue this task if needed)\n\n<task_result>\nMain files:\n\n- `.../index.html` ...\n- `.../app.js` ...\n- `.../styles.css` ...\n- `.../README.md` ...\n\nNo build/config files are present; it looks like a simple frontend-only static app.\n</task_result>", "metadata": { "sessionId": "ses_2f07a8dd6ffeRc23sIIgM4ZpMT", "model": { "modelID": "gpt-5.4-mini", "providerID": "openai" } } } }, { "type": "step-finish", "reason": "tool-calls" } ] }两个关键点:
- 父转写存储
task工具调用及其结果; - 可恢复性由子会话 id 提供,以
task_id形式返回(ses_2f07a8dd6ffeRc23sIIgM4ZpMT即子会话 id,可用于续跑该任务)。
7.4 子会话真的被创建了
真实GET /session/{parentID}/children响应:
[ { "id": "ses_2f07a8dd6ffeRc23sIIgM4ZpMT", "parentID": "ses_2f07aa25affeqiZHSnBiN8pSyG", "title": "Find main project files (@explore subagent)", "directory": "/Users/kirilldubovitskiy/projects/happy/environments/lab-rat-todo-project", "permission": [ { "permission": "todowrite", "pattern": "*", "action": "deny" }, { "permission": "todoread", "pattern": "*", "action": "deny" }, { "permission": "task", "pattern": "*", "action": "deny" } ] } ]这是「OpenCode 子代理就是子会话」最干净的证明:子会话拥有自己的 id、自己的parentID指向父会话、自己的标题、继承的目录,以及自己的权限规则(这里显式 deny 了 todo 写入/读取与再派生子任务,防止无限递归)。
7.5 跨父子的实时流
原始/event流:
- 父会话用户消息创建
- 父会话助手
step-start - 父会话 reasoning 增量
- 父会话
task工具部件 →pending - 子会话
session.created - 父会话工具部件 →
running - 子会话内出现子会话用户消息
- 子会话助手消息开始
- 子会话 reasoning 增量流式推送
- 子会话达到
idle - 父会话工具部件 →
completed
结论:OpenCode 并没有在一条扁平消息泳道里「假装」子代理,而是使用:父会话转写 + 子会话转写 + 父工具元数据(链接到子会话 id)。
八、与 Happy 当前代码的逐项对照
以下对照只使用 Happy 代码(不做 Happy 运行时追踪),表中 OpenCode 侧结论由日志/源码证明:
| 主题 | OpenCode(日志/代码证明) | Happy(代码证明) |
|---|---|---|
| 外层信封 | 消息行已有顶层info+ 有序类型化parts | packages/happy-wire/src/messages.ts 仍把较新格式包装为role: "session"+ 内层content: sessionEnvelope |
| 事件判别 | 部件用顶层type(如text、reasoning、tool、file、agent、subtask、step-start) | packages/happy-wire/src/sessionProtocol.ts 仍把事件类型嵌套在ev.t之下(sessionEventSchema是t判别联合) |
| 权限 | 实时permission.asked/permission.replied事件携带工具回链与 diff 元数据 | packages/happy-app/sources/sync/reducer/reducer.ts 仍需通过合并类转写消息与加密agentState来重建权限状态 |
| 子代理 | 带parentID的真实子会话;task_id是可恢复的子会话 id | packages/happy-wire/src/sessionProtocol.ts 的信封只有可选的subagent字段(cuid2 校验),没有子会话身份 + 转写级链接 |
| 媒体 | 用户file部件 + 合成辅助text;成功的本地文件变成具体data:URL | packages/happy-wire/src/sessionProtocol.ts 只有一种file事件形态;计划文档提出直接采用photo/video/file变体 |
| 沙箱 / 隔离 | 路由靠 directory/workspace 与可选 worktree;「sandbox」大多是 worktree/workspace 语言 | packages/happy-cli/src/sandbox/config.ts 已有具体的文件系统 allow/deny 规则与网络模式 |
| 客户端复杂度 | OpenCode 的 reducer 把实时补丁合并进已类型化的消息行 | packages/happy-app/sources/sync/typesRaw.ts 与 packages/happy-app/sources/sync/reducer/reducer.ts 仍保留多个 legacy 载荷族系(agentEvent、session 事件、tool-call 等)及复杂的重建逻辑 |
对照结论非常直白:
- OpenCode 拥有更干净的转写形态;
- Happy 拥有更强的真实沙箱配置;
- Happy 当前的 reducer 复杂度,是「不再保留多个明文载荷族系」的最强论据。
关于沙箱这一点可以进一步说明:Happy 的buildSandboxRuntimeConfig在sessionIsolation: 'strict'时只允许写入会话目录 + 额外写入路径 + 共享 agent 状态路径(~/.codex、~/.claude),在workspace模式放宽到 workspace 根;网络侧blocked模式把allowedDomains与deniedDomains都清空,allowed模式则放行。这套文件系统 + 网络的策略能力,是 OpenCode 目录路由式「沙箱」所不具备的。
九、对provider-envelope-redesign.md的启示
当前规划上下文来自 docs/plans/provider-envelope-redesign.md(DRAFT v2,OpenCode 衍生),现有判断仍然成立:
- p6 信封重设计工作位于 dirty worktree,尚未进入已提交的分支历史;
- 该工作已经验证了若干有价值的清理动作:
type放顶层、去掉外层role: "session"、引入parentId/agentId、转写级权限、直接媒体变体; - 该计划文档中的现有提案仍是「记录的方案」(plan of record);
- OpenCode 的原始协议形态,仍是锁定 Happy 新稳态 schema 前最值得评估的外部参照;
- Claude 更旧的类转写格式,如果最终证明最简单稳定的模型更接近那段历史,仍是合理的回退选项。
原文档特别强调:OpenCode并不主张照抄 ACP 包装器行为,而是主张照抄原始转写形态:
- 稳定的消息行(stable message rows);
- 类型化部件(typed parts);
- 显式的权限对象(explicit permission objects);
- 显式的子会话身份(explicit child-session identity);
- 转写状态与实时补丁传输的清晰分离。
这与provider-envelope-redesign.md的取舍一致:该计划采纳 message+parts 形态,但拒绝OpenCode「权限/问题走旁路 SSE 事件」的做法——计划在工具部件状态机中加入显式blocked状态,让权限请求与决策永久落在工具部件上(block字段 +decision: "once" | "always" | "reject"),并拒绝以原始message.part.delta回放作为持久化同步模型,改为可打补丁的规范消息(pending → blocked → running → completed 原地演进,同步发完整更新消息,refetch 拿最新状态)。
十、Happy 最难的部分:加密存储下的三个落地方案
这是 OpenCode 与 Happy 分歧最大的地方。
OpenCode 之所以能长期维持规范消息行 + 部件补丁,是因为它的存储层能看到明文会话状态(SQLite 明文行,可随意打补丁)。而Happy 存储的是不透明加密 blob。因此直接照抄 OpenCode 就必然要做一个存储决策。以下是三个候选方案。
方案 A:append-only 规范转写事件
存储已规范化的、自身可持久化的加密记录。
示例心智模型:
{ "kind": "agent-event", "type": "tool-start", ... } { "kind": "agent-event", "type": "permission-request", ... } { "kind": "agent-event", "type": "tool-end", ... }优点:存储不可变;refetch 简单;匹配 Happy 当前的传输假设;避免重放原始 delta 来重建可用转写。缺点:不是对 OpenCode 补丁模型的字面照搬;要么 start/end 事件永远分离,要么客户端必须为 UI 便利推导「最新状态」视图。
方案 B:给规范加密消息行打补丁
保留稳定加密消息 id,但当部件获得新状态时重写加密载荷,使 refetch 返回最新的规范快照。
示例心智模型(初始):
{ "messageId": "msg_123", "parts": [ { "type": "tool", "state": { "status": "pending" } } ] }之后被重写为:
{ "messageId": "msg_123", "parts": [ { "type": "tool", "state": { "status": "completed", "input": { ... }, "output": "..." } } ] }优点:最接近 OpenCode 的服务端模型;refetch 直接拿到最新规范状态;客户端重建问题更少。缺点:加密消息行变得可变;同步/版本控制更微妙;除非同时保留影子事件日志,否则失去纯 append-only 历史。
方案 C:追加原始补丁事件,客户端重建
存储原始流,客户端重建消息状态。
示例心智模型:
{ "type": "message.updated", ... } { "type": "message.part.updated", ... } { "type": "message.part.delta", ... } { "type": "permission.asked", ... }优点:最接近 OpenCode 实时流;只要每个补丁都被追加就是完全不可变的。缺点:这恰恰是最可能复刻 Happy 当前 reducer 之痛的路线;refetch 需要回放/物化;加密存储 + 遗留格式支持使其成为复杂度最高的选项。
推荐结论
如果 Happy 要向 OpenCode 借鉴,应该借形态(shape),而不是整套持久化策略。最强的两个选项是:
- append-only规范事件(方案 A);
- 可打补丁的规范消息快照(方案 B)。
最弱的选项是:把原始补丁流重建作为主要持久化格式(方案 C)——那会保留太多我们正试图消除的复杂度。方案 B 也正是docs/plans/provider-envelope-redesign.md选定的方向:消息 id 稳定、工具部件状态演进时整体重加密并同步完整消息、refetch 直接拿最新状态、不以 append-only 事件日志作为主存储;而 DB 行、seq排序、localId、v3 HTTP 消息 API、Socket.IO 失效通知与加密 blob 格式均保持不变。
小结
以真实运行轨迹为唯一证据,可以确认 OpenCode 的协议核心是「稳定消息行 + 类型化部件 + 显式权限对象 + 显式子会话身份 + 转写与实时补丁分离」。它对 Happy 的最大价值不是可照抄的传输层,而是可作为稳态 schema 的参照形态;而 Happy 真正的差异化优势(文件系统/网络级沙箱策略、端到端加密存储)决定了它必须把 OpenCode 的形态改造进自己的加密存储约束之内——这正是docs/plans/provider-envelope-redesign.md正在做的事。建议继续阅读同一研究系列中的 docs/competition/opencode/message-protocol.md 与 docs/competition/opencode/sources.md,以及 docs/plans/provider-envelope-redesign.md 的完整方案。
- 人工智能
- AI Agent
- AI 应用
- 移动开发
- CLI
- 后端
【免费下载链接】happy
Mobile and Web client for Codex and Claude Code, with realtime voice, encryption and fully featured
相关推荐
Happy Provider Envelope 重构设计:以 OpenCode message+parts 模型统一多 Provider 会话协议
Happy Provider Envelope 重构设计:以 OpenCode message+parts 模型统一多 Provider 会话协议 本文基于 d
人工智能AI AgentAI 应用移动开发CLI后端OpenCode 消息协议深度剖析:happy 项目借鉴的 Envelope + Typed Parts 转写模型
OpenCode 消息协议深度剖析:happy 项目借鉴的 Envelope + Typed Parts 转写模型 导读 本文基于 happy 仓库 docs/
人工智能AI AgentAI 应用移动开发CLI后端Happy 竞品协议矩阵解析:OpenCode、Codex、Claude、Superset 的传输、转录、权限与沙箱设计对照
Happy 竞品协议矩阵解析:OpenCode、Codex、Claude、Superset 的传输、转录、权限与沙箱设计对照 本文基于 docs/competi
人工智能AI AgentAI 应用移动开发CLI后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考