ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

opencode hook 沉淀用户历史输入,作为永久项目记忆的配置实践

opencode hook 沉淀用户历史输入,作为永久项目记忆的配置实践 1. 为什么多轮 Agent 协作总在“失忆”用 OpenCode 跑长期项目的人大概率都遇到过这个场景昨天会话里刚敲定的表结构、业务边界、架构取舍今天新开一个会话模型又像第一次见到这个项目一样从头问“你想做什么”。这不是模型笨而是会话上下文天生是易失的——关掉进程记忆就没了。我试过把关键信息手动贴进新会话但项目一复杂就贴不动了字段几十个、决策十几条每次复制粘贴既费 token 又容易漏。更麻烦的是多人轮流接手同一个仓库时A 的会话记忆 B 完全看不到项目知识全散在各自的聊天记录里。OpenCode 的 plugin hook 机制正好能解决这个问题。它允许你在chat.message事件里拦截用户输入把原始提问落盘到项目目录下的.history/再定期压缩成一份人类和模型都能读的长期记忆文件。新会话启动时只要让 agent 读一下.history/history.txt项目上下文就自动继承了。这套方案的核心检索词就是opencode hook 沉淀用户历史输入用 hook 采集、用 plugin 落盘、用 skill 触发压缩最终形成项目级永久记忆。它适合三类人长期迭代同一个仓库的独立开发者、多人轮流接手项目的团队、以及想让后续模型快速理解项目背景而不是依赖旧会话的 agent 重度用户。下面我把整套配置拆成可复制的步骤包括插件脚本、skill 文件、目录结构和验证动作。你照着做重启会话后就能确认历史输入被读取。2. TaoToken 前置给 OpenCode 配一个稳定的模型入口在写 hook 之前得先保证 OpenCode 能正常调用模型。OpenCode 本身是客户端模型请求要发到某个兼容 OpenAI 协议的端点。这里用 TaoToken 作为统一入口它的 API 地址是https://taotoken.net/api兼容 OpenAI 的/v1/chat/completions格式OpenCode 的 provider 配置可以直接对接。为什么先讲这个因为 hook 压缩历史时插件内部会调用client.session.prompt发起一次模型请求把待压缩的用户提问整合成长期记忆。如果模型入口没配好压缩这一步会直接失败.history/history.txt永远是空的。所以模型接入是整套记忆方案的前置条件。配置方式是在 OpenCode 的 provider 配置里指定 Base URL 和 API Key。OpenCode 支持在~/.config/opencode/下放配置文件具体路径取决于你的版本常见的是opencode.json或config.json。核心字段是baseURL和apiKey模型 ID 填你实际要用的模型名。这里有个容易踩的坑Base URL 不要带/v1后缀OpenCode 会自己拼接路径。如果你填成https://taotoken.net/api/v1请求会变成/v1/v1/chat/completions直接 404。正确写法就是https://taotoken.net/api。API Key 的获取在控制台的 API Keys 页面生成后复制到配置里。如果你还没配过可以先到模型对话页面确认模型能正常响应再回来配 OpenCode。模型对话入口在https://taotoken.net/chatAPI Keys 在https://taotoken.net/console/api-keys。配好之后先用一个最小请求验证连通性。在终端里跑curl https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d { model: claude-sonnet-4-20250514, messages: [{role: user, content: ping}] }如果返回里有choices字段和正常的文本内容说明模型入口通了。这一步不通后面的 hook 压缩一定失败所以别跳过。对于长期跑 agent 协作的场景如果压缩频率高、会话多可以考虑用 Coding Plan 来降低单次调用成本入口在https://taotoken.net/coding-plan。不过对于个人项目按量调用通常够用先跑通再说。模型入口配好后OpenCode 启动时就能正常发起请求。接下来才是 hook 插件本身。记住顺序先通模型再装插件否则你会分不清是插件问题还是网络问题。3. 可复制配置插件脚本、skill 与目录结构这一节是整套方案的核心所有文件都可以直接复制。先建目录结构再放插件脚本最后放 skill 文件。3.1 目录结构OpenCode 的插件和 skill 都放在用户配置目录下。在 macOS/Linux 上路径是~/.config/opencode/。你需要创建两个目录mkdir -p ~/.config/opencode/plugins mkdir -p ~/.config/opencode/skills/project-history-compress插件脚本放在~/.config/opencode/plugins/project-history.jsskill 文件放在~/.config/opencode/skills/project-history-compress/SKILL.md。如果~/.config/opencode/package.json不存在需要创建一个声明插件依赖{ type: module, dependencies: { opencode-ai/plugin: 1.14.48 } }然后在~/.config/opencode/下执行npm install把opencode-ai/plugin装好。插件本体是 JSOpenCode 原生支持 JS/TS 插件不依赖 Python。3.2 插件脚本关键配置插件脚本比较长核心逻辑是拦截chat.message事件提取用户原始文本写入.history/pending.ndjson当满足压缩条件时调用模型把待压缩提问整合成.history/history.txt。脚本顶部有几个可调参数直接决定压缩行为const AUTO_COMPACT_MAX_AGE_MS 3 * 60 * 60 * 1000; const AUTO_COMPACT_MAX_PENDING_PROMPTS 100; const PENDING_MAX_TEXT_CHARS 10000; const PENDING_MAX_LINES 2000;AUTO_COMPACT_MAX_AGE_MS是最老待压缩提问的年龄阈值默认 3 小时。AUTO_COMPACT_MAX_PENDING_PROMPTS是待压缩提问数量阈值默认 100 条。满足任一条件就触发自动压缩。PENDING_MAX_TEXT_CHARS是单条提问的最大长度超出会截断并追加...(已截断)。PENDING_MAX_LINES是pending.ndjson的最大行数超出后删除最老记录。采集逻辑只保留用户原始文本不采集 assistant 回复、tool 输出、plan 文本和 reasoning。写入时按时间倒序文件顶部永远是最新记录。压缩完成后会在pending.ndjson顶部插入一个边界 marker{marker:compacted,compactedAt:2026-05-13T14:30:00.00008:00,reason:auto,processedPromptCount:12}这个 marker 的作用是标记“哪些记录已经被整理过”下次压缩时只处理 marker 之前的记录。3.3 项目目录下生成的文件当你在有效项目目录下启动 OpenCode插件会自动创建.history/目录和四个文件.history/history.txt # 长期项目记忆中文标题分类 .history/pending.ndjson # 待压缩的用户原始提问 .history/state.json # 最近压缩时间和统计信息 .history/.gitignore # 控制本地状态文件是否进版本管理.gitignore默认忽略pending.ndjson、state.json和lock但保留history.txt。这样长期记忆可以进版本库团队共享而本地状态文件不会污染提交。3.4 skill 文件skill 文件告诉 agent 什么时候调用压缩工具。内容如下--- name: project-history-compress description: 压缩当前项目 .history 目录中的待整理用户提问并更新长期项目历史总结文件 --- ## 背景 这个 skill 用来维护项目级的长期记忆避免 OpenCode 多轮会话里的项目背景、需求演化、关键决策只留在会话上下文中。 ## 目的 - 把当前项目里尚未整理的用户原始提问压缩成长期可读的项目总结。 - 让 .history/history.txt 成为后续人类和模型都能快速阅读的项目记忆入口。 ## 正确动作 直接调用 project_history_compact 工具参数使用 {force: true} ## 约束 - 不要自己在聊天里手工代替工具做总结优先调用工具。 - 不要把 tool 输出、plan 文本、assistant 回复复制到 .history/pending.ndjson。skill 的触发方式是手动当你说“压缩 history”或“刷新项目历史总结”时agent 会调用project_history_compact工具参数force: true表示立即压缩不等自动阈值。3.5 启动方式推荐用项目目录作为参数启动opencode /path/to/project或者先cd再启动cd /path/to/project opencode注意不要用-c当目录参数因为-c在 OpenCode 里是--continue含义完全不同。用错会导致插件把工作目录识别成错误路径.history/落到不该落的地方。插件是在 OpenCode 进程启动时加载的修改插件脚本后必须退出并重新进入 OpenCode 才能生效。这一点很关键很多人改完脚本发现没反应就是因为没重启进程。4. 验证请求重启会话后确认历史被读取配置放好后得验证整套链路真的通了。验证分三步确认文件生成、确认提问被采集、确认压缩后历史被读取。4.1 确认文件生成在项目目录下启动 OpenCodecd /path/to/your-project opencode启动后插件会异步初始化.history/。等几秒检查目录ls -la .history/你应该看到history.txt、pending.ndjson、state.json、.gitignore四个文件。如果没生成说明插件没加载成功检查~/.config/opencode/plugins/project-history.js是否存在、package.json依赖是否装好。history.txt初始内容类似项目历史自动维护 该文件由 OpenCode 项目历史插件自动维护。 最后更新时间2026-05-13T14:12:00.12308:00 # 项目概述 暂无项目历史摘要。4.2 确认提问被采集在 OpenCode 会话里输入一条有实际项目信息的提问比如这个项目用 PostgreSQL用户表叫 users主键是 id字段有 email、created_at、status。发送后检查pending.ndjsoncat .history/pending.ndjson你应该看到一条记录格式是{ts:2026-05-13T14:12:00.12308:00,text:这个项目用 PostgreSQL用户表叫 users主键是 id字段有 email、created_at、status。}如果文件是空的说明chat.messagehook 没触发。检查插件是否在进程启动时加载以及提问是否被shouldCaptureText过滤掉了——维护类提示比如包含project_history_compact会被跳过。4.3 确认压缩后历史被读取手动触发压缩。在会话里说压缩 historyagent 会调用project_history_compact工具参数force: true。压缩完成后检查history.txtcat .history/history.txt你应该看到类似内容项目历史自动维护 该文件由 OpenCode 项目历史插件自动维护。 最后更新时间2026-05-13T14:30:00.00008:00 # 项目概述 项目使用 PostgreSQL 数据库。 # 表 Schema - users 表主键 id字段 email、created_at、status同时pending.ndjson顶部会插入边界 marker{marker:compacted,compactedAt:2026-05-13T14:30:00.00008:00,reason:manual,processedPromptCount:1}state.json会更新lastCompactedAt和lastCompaction统计。4.4 新会话继承验证最关键的一步退出 OpenCode重新进入同一个项目目录开一个新会话。然后让 agent 读历史读一下 .history/history.txt告诉我这个项目的表结构。如果 agent 能准确说出users表的字段说明项目级永久记忆生效了。新会话没有旧会话的上下文但通过读取.history/history.txt它继承了项目知识。这一步验证通过整套方案就跑通了。后续每次会话的用户提问都会自动沉淀定期压缩成长期记忆新会话自动继承。5. 本篇常见错排查401、local proxy failed 与 reading choices配置过程中最容易卡在几个具体报错上。这一节按真实报错逐个排查。5.1 401 Unauthorized压缩时插件内部会调用模型如果 API Key 没配好会返回 401。典型报错Error: 401 Unauthorized {error:{message:Invalid API key,type:invalid_request_error}}排查顺序先确认~/.config/opencode/下的 provider 配置里apiKey字段填的是 TaoToken 控制台生成的 Key没有多余空格。再确认 Base URL 是https://taotoken.net/api不带/v1。最后用第 2 节的 curl 命令单独验证 Key 是否有效。如果 curl 通但 OpenCode 报 401说明 OpenCode 的配置没读到检查配置文件路径和格式。5.2 local proxy failed这个报错通常出现在网络层Error: local proxy failed: connect ECONNREFUSED 127.0.0.1:7890说明 OpenCode 尝试走本地代理端口但代理没启动。如果你没配代理检查环境变量HTTP_PROXY、HTTPS_PROXY是否被设置成了失效的地址。清掉这些变量再试unset HTTP_PROXY HTTPS_PROXY ALL_PROXY如果你确实需要网络配置确保代理进程在运行。但更推荐直接让 OpenCode 走直连避免代理层引入额外故障点。5.3 reading choices压缩响应解析失败时会出现TypeError: Cannot read properties of undefined (reading choices)这说明模型返回的 JSON 结构里没有choices字段。可能原因Base URL 配错导致请求打到了非兼容端点或者模型 ID 填错导致返回了错误结构。检查 provider 配置里的baseURL和model字段。用 curl 验证时确认返回体里有choices[0].message.content。另一个可能是压缩 prompt 返回的文本不是合法 JSON。插件里parseJson(promptText, null)解析失败会抛Compaction response text was not valid JSON。这时检查模型是否被 system prompt 约束住了——插件在压缩请求里加了Return only valid JSON matching the requested shape如果模型不遵守可以换一个指令遵循更强的模型。5.4 OAuth 相关报错如果你用的是需要 OAuth 的 provider可能出现Error: OAuth token expiredOpenCode 的某些 provider 走 OAuth 流程token 过期需要重新授权。但用 TaoToken 的 API Key 方式不涉及 OAuth直接填 Key 即可。如果你混用了两种认证方式确保 provider 配置里只保留一种。5.5 插件没生效改完插件脚本后没重启 OpenCode是最常见的“没反应”原因。插件在进程启动时加载运行中修改脚本不会热更新。退出进程重新opencode /path/to/project即可。另外检查package.json里的opencode-ai/plugin版本是否和 OpenCode 兼容。版本不匹配可能导致 hook 注册失败。如果chat.message一直不触发先确认插件导出的是ProjectHistoryPlugin且 OpenCode 能识别~/.config/opencode/plugins/下的文件。5.6 三件套检查清单无论哪种报错先核对三件套是否齐全配置项正确值常见错误Base URLhttps://taotoken.net/api多带/v1后缀API Key控制台生成的 Key复制时带空格或换行Model ID实际模型名填了不存在的模型这三项任意一项错压缩都会失败。排查时先用 curl 验证三件套再回到 OpenCode 里测。6. 把项目记忆接进你的日常 agent 工作流整套方案跑通后日常使用其实很简单正常在项目目录下启动 OpenCode正常提问插件在后台采集。你不需要每次手动做什么压缩会在满足阈值时自动触发或者你随时说“压缩 history”手动触发。有几个实用技巧值得记一下。第一.history/history.txt建议进版本库这样团队成员拉取代码后新会话直接读这份文件就能继承项目上下文不用每个人重新问一遍。.gitignore默认已经忽略了本地状态文件只保留history.txt这个默认行为是合理的。第二如果项目知识更新频繁可以把AUTO_COMPACT_MAX_PENDING_PROMPTS调小比如改成 50让压缩更及时。代价是模型调用次数增加。反过来如果项目迭代慢可以调大到 200减少压缩频率。第三切换 agent 工具时只要告诉新工具“读一下项目根目录的.history/history.txt”它就能快速获取项目记忆。这套方案不绑定 OpenCode记忆文件是纯文本任何能读文件的 agent 都能用。第四表 schema 类内容会被特殊保留。插件在压缩 prompt 里明确要求“尽量保留原始结构、字段顺序、缩进和换行只做轻量压缩”所以 DDL 和字段清单不会被打成一句抽象总结。这对需要精确字段信息的场景很重要。如果你想让压缩更稳定可以在 skill 里补充项目特有的分类要求比如“必须保留接口路径和参数说明”。skill 文件是纯 Markdown改完重启 OpenCode 生效。最后模型入口的稳定性直接决定压缩成功率。如果压缩经常失败先检查 TaoToken 的 API Key 和 Base URL再到模型对话页面确认模型可用。接入文档在https://taotoken.net/docAPI Keys 在https://taotoken.net/console/api-keys。长期跑 agent 协作的话Coding Plan 入口在https://taotoken.net/coding-plan按需选用。整套配置的核心就一句话用 hook 采集用户输入用 plugin 落盘用 skill 触发压缩让.history/history.txt成为项目级永久记忆。新会话读一次上下文就回来了。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进