
1. opencode 注入用户提示词到底在解决什么问题如果你在用 opencode 做自动化编码或者 Agent 编排迟早会碰到一个需求在请求真正发出去之前动态往会话里塞一段用户提示词。比如根据当前任务状态补一句「继续按上一步的方案实现」或者根据外部事件注入一段上下文。opencode 本身不允许插件改写已有的会话历史消息但它留了两个钩子口子其中一个就是用户侧的client.session.promptAsync通过它可以把一段文本作为新的用户消息注入会话。这个能力听起来简单实际落地时有三个坑注入什么内容、注入给谁agent 和 variant 怎么读、注入失败或超时怎么办。我试过在请求链路里直接拼字符串结果冷启动时把用户选的思考深度降级成了默认值排查了半天才发现是 variant 没实时读取。所以这篇不讲概念直接给可复制的config.toml和settings.json骨架、钩子注册方式、超时参数配置再附一次注入生效的验证动作目标是在超时可控的前提下稳定完成提示词注入同时把 Key 和 API 通道统一走 TaoToken。适合谁看需要在 opencode 请求链路里动态改写提示词、并且希望所有模型调用走统一 Key/API 通道的开发者。如果你只是想让 opencode 跑起来这篇可能偏重了但如果你要做 Agent 续推、任务编排、或者多会话管理这里的钩子和超时语义就是绕不开的。2. 接入前的准备TaoToken 通道与 Key 获取opencode 的模型调用最终要落到一个 API 端点上。把端点统一到 TaoToken 的好处是一个 Key 覆盖多家模型注入逻辑不用为每个 provider 写一套鉴权分支超时和重试策略也能集中配置。TaoToken 的 API 地址是https://taotoken.net/api官网入口在https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content。先拿 Key。打开控制台创建 API Key建议按项目分 Key方便后面排查是哪个会话在超时。创建入口在 console 页面拿到形如sk-开头的字符串后先存到环境变量里不要硬编码进config.toml否则提交到仓库就泄露了。export TAOTOKEN_API_KEYsk-你的key如果你用的是 Claude Code 那套 Anthropic 兼容协议TaoToken 也提供了对应的接入文档opencode 这边我们走标准 OpenAI 兼容的 chat 接口即可。Key 拿到后先别急着配 opencode用一条 curl 确认通道是通的curl -s https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d { model: gpt-4o-mini, messages: [{role: user, content: ping}], max_tokens: 8 }返回里有choices字段就说明 Key 和通道都没问题。这一步别跳过后面注入超时排查时你需要先排除「是不是 Key 本身就不通」这个变量。3. 可复制配置config.toml 与 settings.json 骨架opencode 的配置分两层config.toml管 provider 和模型端点settings.json管插件和钩子行为。先看config.toml核心是把 base_url 指向 TaoToken并把超时参数显式写出来不要依赖默认值。# ~/.config/opencode/config.toml [provider.taotoken] name TaoToken base_url https://taotoken.net/api/v1 api_key_env TAOTOKEN_API_KEY default_model gpt-4o-mini [provider.taotoken.options] timeout_ms 30000 max_retries 2 [model.gpt-4o-mini] provider taotoken context_window 128000timeout_ms这里给 30000比 SDK 钩子层的 10000 大原因是钩子层的超时是「发后即忘」的判定上限而 provider 层的超时是真实网络请求的上限。两层要拉开差距否则钩子层刚判定超时底层请求其实还在飞容易造成半发送。再看settings.json这里注册钩子并配置注入行为{ plugins: { prompt-injector: { enabled: true, hooks: { chat.message: { mode: readonly, record_last_agent: true, stamp_event: true }, session.promptAsync: { timeout_ms: 10000, timeout_env: GOAL_SDK_TIMEOUT_MS, retry_on_timeout: true } }, injection: { max_text_length: 512, include_dynamic_notes: false, read_variant_before_send: true } } } }几个参数值得单独说。chat.message的mode设成readonly意思是用户消息进来时钩子只读不改写它的职责限定在激活会话、记录 last_agent、刷新滑动窗口、标记活动信号这四件事。为什么不改写用户原文一是静态前缀缓存会被动态注记击穿二是改写会破坏 opencode 的撤销和重放语义。read_variant_before_send打开后每次发送前实时调用session.get读当前 variant避免冷启动把用户选的思考深度降级成默认值读取失败时回退到内存缓存不抛错中断注入。4. 钩子注册与超时参数注入链路怎么串起来注入的调用形态是promptAsync核心调用长这样await promptFn.call(session, { path: { id: session_id }, body: { ...(agent ? { agent } : {}), ...(variant ? { variant } : {}), parts: [{ type: text, text }], }, })agent从会话当前状态读plan 代理不参与续推守卫放在门链层。variant发送前实时读读不到就用缓存。parts里只放一段最小驱动文本由continuation_prompt生成不含动态注记。超时这块是重点。promptAsync以SDK_CALL_TIMEOUT_MS默认 10000可用环境变量GOAL_SDK_TIMEOUT_MS覆盖为上限实现方式是手动时钟标记定时器触发时只把timed_out置为 true不取消底层 promise。为什么不取消因为 SDK 超时和成功可能同时返回 undefinedwith_timeout的 fallback 分不清是哪种而且取消底层 promise 可能造成半发送副作用不幂等。这个注入的副作用是追加一条用户消息本身可重试重复送达由会话自身语义消解。调用方按返回值决策send_prompt返回是否在时限内完成。TICK 续推忽略这个结果下一 tick 自然重试manage_subagent必须报告SEND_TIMEOUT由主代理决定重试。你可以这样在环境里覆盖超时export GOAL_SDK_TIMEOUT_MS15000调大这个值适合网络抖动明显的场景调小适合需要快速失败的编排。但别把它设得比 provider 层的timeout_ms还大否则钩子层永远等不到超时判定重试逻辑就失效了。5. 验证注入生效一次可复现的请求配置写完要验证。最直接的方式是跑一个集成测试脚本断言注入后会话确实收到了续推文本。下面这个最小验证脚本可以直接改改用// verify-inject.mjs import { createClient } from opencode/sdk const client createClient({ baseUrl: https://taotoken.net/api/v1, apiKey: process.env.TAOTOKEN_API_KEY, }) const session await client.session.create({ agent: build }) const result await client.session.promptAsync({ path: { id: session.id }, body: { parts: [{ type: text, text: 继续按上一步方案实现 }], }, }) console.log(inject result:, result) console.log(timed_out:, result undefined) const history await client.session.get({ path: { id: session.id } }) const lastMsg history.messages.at(-1) console.log(last role:, lastMsg.role) console.log(last text:, lastMsg.parts[0].text)跑之前确认GOAL_SDK_TIMEOUT_MS已设置然后执行GOAL_SDK_TIMEOUT_MS10000 node verify-inject.mjs预期输出里last role是userlast text就是你注入的那段文本。如果timed_out打印 true 但历史里又有这条消息说明底层请求其实成功了只是钩子层时钟先到了——这正是「发后即忘」设计要处理的场景调用方按返回值决策即可TICK 会自然重试。故障注入也建议跑一遍把promptAsync改成抛错确认目标会话保持 active、循环不死。这一步能验证你的失败路径是否真的可重试。6. 常见报错与排查清单注入后会话没收到消息但也没报错。先看agent是不是 plan 代理plan 不参与续推守卫在门链层直接拦掉了。再看variant读取是否抛错中断了注入正常情况下session.get异常要捕获并回退缓存不能往外抛。超时误报明明成功了却返回 undefined。这是 SDK 超时和成功同时返回 undefined 的经典情况。检查你的调用方是不是把 undefined 当成了失败。TICK 续推忽略返回值是对的manage_subagent才需要报告SEND_TIMEOUT。冷启动后思考深度被降级。说明read_variant_before_send没开或者读取失败后没回退缓存。打开这个开关并确认缓存session_variant在读取失败时被采用。用户原文被改写导致撤销失效。检查chat.message钩子的mode是不是被改成了可写。这个钩子只该做四件事激活会话、记录 last_agent、调用stamp_event刷新滑动窗口、调用note_user_activity标记活动信号。任何写操作都该落到持久化状态不碰用户原文。Key 不通导致的假超时。回到第 2 节的 curl 先确认通道。如果 curl 都超时那问题在 Key 或网络不在注入逻辑。这时候去 API Keys 页面重新生成一个 Key 试试或者换模型对话页面手动发一条消息确认账号状态。排查顺序建议先 curl 确认通道再确认 agent/variant 读取最后看超时参数两层是否拉开差距。大部分「注入不生效」最后都落在 agent 守卫或 variant 读取上。7. 把注入链路和统一通道固定下来注入用户提示词这件事正确定义是发送最小驱动文本不修改已存在消息agent、variant、超时这些量都显式读取并给出失败路径记录与注入分离注入与取消分离。违背任何一条注入会污染缓存、破坏撤销或卡死循环。通道这边把 base_url 固定到https://taotoken.net/apiKey 走环境变量超时两层配置拉开差距这套骨架就能复用到多个项目。如果你后面要做长期编码或 Agent 编排可以考虑 Coding Plan 把配额和模型调度统一管起来日常验证模型行为用模型对话页面就够了接入细节和参数说明都在接入文档里。先把这篇的config.toml和settings.json跑通再按自己的续推逻辑改continuation_prompt比一上来就堆功能稳得多。