1. OpenCode 流式响应与进度回调为什么你的终端总在“干等”如果你用 OpenCode 跑过稍微复杂一点的任务比如让它“分析这个项目的代码结构并给出重构建议”大概率经历过这种场景敲下回车终端光标一闪一闪然后……就没有然后了。你不知道它是在读文件、在检索依赖、在思考还是已经卡死了。这种“信息黑洞”体验本质上是流式响应和进度回调没有配置到位。OpenCode 本身是支持流式输出的它通过 SSEServer-Sent Events把模型生成的每一个 token 推送到 TUI 界面。但插件层拿不到原始 SSE 字节流你能用的是event钩子订阅part.updated事件用tool.execute.before/after监听工具执行用experimental.chat.system.transform往系统提示里注入进度上下文。这套机制组合起来就能把“干等”变成“透明过程”。这篇内容适合已经装好 OpenCode、想给终端加一个“进度仪表盘”的开发者。我会从模型通道准备讲起把 Base URL 填到 TaoToken 的 API 地址然后一步步实现流式日志、工具进度、旋转动画和进度上下文注入。全程可复制踩过的坑我也会标出来。2. 前置准备把模型通道接到 TaoTokenOpenCode 要跑起来首先得有一个能用的模型通道。这一步不涉及任何插件逻辑只是把 Key 和 Base URL 配好。你可以先去 TaoToken 官网 注册账号然后在控制台创建一个 API Key。拿到 Key 之后打开 OpenCode 的模型配置文件把 Base URL 填成https://taotoken.net/api注意两点不要加/v1也不要带任何 UTM 参数。TaoToken 在这里只提供 Key 和 Base URL它不参与part.updated事件、工具钩子或进度仪表盘的逻辑。换句话说模型通道是“路”进度回调是“仪表盘”两者各管各的。配置写完后先别急着写插件。你可以先发一条简单请求确认模型通道是通的。比如在 OpenCode 里输入“分析这个项目的代码结构并给出重构建议”观察终端里有没有出现“正在生成”“调用工具”“思考中”这类状态。如果能看到这些说明模型通道已经通了可以继续往下做流式响应和进度回调。3. 可复制配置从 stream-logger.ts 开始3.1 理解 OpenCode 的流式响应机制在 OpenCode 里流式响应不是简单的“打字机效果”。从你按下回车到 AI 输出完整回复中间会产生大量内部事件思考开始/结束、文本块流、工具调用开始/结束、工具执行结果、对话完成。这些事件通过 SSE 推送到 TUI但插件层拿不到原始字节流。那插件怎么知道 AI 在输出什么答案是订阅part.updated事件。每次有新的文本块、工具调用或思维链被保存到数据库时这个事件就会触发。你可以在事件回调里拿到part.type区分是text、tool_call、tool_result还是thinking。3.2 写一个流式日志插件在.opencode/plugins/stream-logger.ts里写入以下代码import type { Plugin } from opencode-ai/plugin export const StreamLoggerPlugin: Plugin async (ctx) { const sessionOutputs new Mapstring, { charCount: number, toolCalls: number, thinking: boolean }() return { event: async ({ event }) { if (event.type part.updated) { const part event.properties.part const sessionId event.properties.sessionID if (!sessionOutputs.has(sessionId)) { sessionOutputs.set(sessionId, { charCount: 0, toolCalls: 0, thinking: false }) } const state sessionOutputs.get(sessionId)! if (part.type text) { const text part.text || state.charCount text.length if (state.charCount % 50 text.length) { console.log(正在生成... 已输出 ${state.charCount} 字符) } } else if (part.type tool_call) { state.toolCalls 1 console.log(调用工具: ${part.name || unknown} (第 ${state.toolCalls} 个工具)) } else if (part.type tool_result) { console.log(工具执行完成: ${part.name || unknown}) } else if (part.type thinking) { if (!state.thinking) { state.thinking true console.log(AI 开始思考...) } const thinkingText part.text || if (thinkingText.length 0) { console.log(思考中... (${thinkingText.length} 字符)) } } } if (event.type session.idle) { const sessionId event.properties.sessionID const state sessionOutputs.get(sessionId) if (state) { console.log(\n会话完成统计:) console.log( - 总输出字符: ${state.charCount}) console.log( - 工具调用次数: ${state.toolCalls}) console.log( - 是否使用了思维链: ${state.thinking ? 是 : 否}) } } } } }保存文件后重启 OpenCode再发一次“分析这个项目的代码结构并给出重构建议”。你应该能在终端里看到“正在生成”“调用工具”“思考中”这些实时进度信息。这一步验证的是模型通道和事件订阅是否都通了。4. 验证请求工具进度与旋转动画4.1 用 tool.execute.before/after 显示工具执行进度part.updated能告诉你“工具被调用了”但如果你想在工具执行前后插入自定义逻辑比如显示“正在读取文件...”和“文件读取完成”就需要用tool.execute.before和tool.execute.after钩子。在.opencode/plugins/tool-progress.ts中写入import type { Plugin } from opencode-ai/plugin export const ToolProgressPlugin: Plugin async (ctx) { const { client } ctx return { tool.execute.before: async (input, output) { const toolName input.tool let message 正在执行: ${toolName} if (toolName read) { const filePath output.args?.filePath || 未知文件 message 正在读取文件: ${filePath} } else if (toolName edit || toolName write) { const filePath output.args?.filePath || output.args?.path || 未知文件 message 正在写入文件: ${filePath} } else if (toolName bash) { const cmd output.args?.command || 未知命令 message 正在执行命令: ${cmd.substring(0, 50)}${cmd.length 50 ? ... : } } else if (toolName grep || toolName glob) { const pattern output.args?.pattern || output.args?.query || 未知模式 message 正在搜索: ${pattern} } await client.tui.notify({ message: message, level: info }) return output }, tool.execute.after: async (input, output) { const toolName input.tool const duration output.duration || 0 const durationStr duration 1000 ? ${(duration / 1000).toFixed(1)}s : ${duration}ms await client.tui.notify({ message: ${toolName} 完成 (耗时 ${durationStr}), level: success }) return output } } }保存后重启 OpenCode发一个需要读文件或执行命令的请求。你应该能在 TUI 的通知区域看到“正在读取文件: xxx”和“read 完成 (耗时 15ms)”这样的通知。4.2 设计自定义进度指示器如果你觉得文字通知不够直观可以加一个旋转动画。在.opencode/plugins/spinner-progress.ts中写入import type { Plugin } from opencode-ai/plugin export const SpinnerProgressPlugin: Plugin async (ctx) { const SPINNER_FRAMES [⠋, ⠙, ⠹, ⠸, ⠼, ⠴, ⠦, ⠧, ⠇, ⠏] let spinnerIndex 0 let isRunning false let intervalId: Timer | null null const updateSpinner (message: string) { const frame SPINNER_FRAMES[spinnerIndex % SPINNER_FRAMES.length] process.stdout.write(\r${frame} ${message}${ .repeat(10)}) spinnerIndex } return { event: async ({ event }) { if (event.type part.updated !isRunning) { const part event.properties.part if (part.type text || part.type thinking) { isRunning true let message AI 正在生成... if (intervalId) clearInterval(intervalId) intervalId setInterval(() { updateSpinner(message) }, 100) } } if (event.type session.idle) { if (intervalId) { clearInterval(intervalId) intervalId null } isRunning false process.stdout.write(\r .repeat(50) \r) console.log(生成完成) } } } }这个插件会在终端里显示一个旋转的动画字符随着 AI 输出动态更新完成时自动清除。5. 本篇常见错排查5.1 client.tui.notify 方法不存在或报错如果你看到Error: client.tui.notify is not a function先确认 OpenCode 版本是否在 v0.2.0 以上。较早版本可能没有开放这个 API。替代方案是用console.log输出进度信息虽然不会显示在 TUI 界面里但能在终端日志中看到。升级命令npm update -g opencode-ai5.2 part.updated 事件触发过于频繁导致性能问题流式输出会触发大量part.updated事件如果插件在事件处理中做大量计算或 IO 操作会导致 CPU 占用高。解决方案是在事件处理中加节流let lastUpdate 0 if (Date.now() - lastUpdate 200) return lastUpdate Date.now()另外日志输出不要每个字符都打每 50 个字符输出一次就够了。5.3 experimental.chat.system.transform 钩子不触发这个钩子是实验性的某些版本可能还没稳定开放。先确认opencode.json中已启用{ experimental: { enableSystemTransform: true } }如果还是不触发可以改用experimental.chat.messages.transform作为替代在消息列表中注入进度信息。同时检查插件文件是否被正确加载可以在插件初始化时打日志确认。6. 继续深入进度上下文注入与仪表盘整合前面几步做完你已经有了流式日志、工具进度和旋转动画。还有一个更高级的玩法用experimental.chat.system.transform把进度信息注入到 AI 的上下文中让 AI 自己知道“已经做到哪一步了”。这样它在接近完成时可能会输出更精炼的内容而不是一直啰嗦。在.opencode/plugins/progress-context.ts中写入import type { Plugin } from opencode-ai/plugin export const ProgressContextPlugin: Plugin async (ctx) { const progressMap new Mapstring, { step: number, total: number, description: string }() return { experimental.chat.system.transform: async (input, output) { const sessionId input.sessionID const progress progressMap.get(sessionId) if (!progress) return output const progressText \n【任务进度】\n- 当前步骤: ${progress.step}/${progress.total}\n- 步骤描述: ${progress.description}\n- 已完成: ${(progress.step / progress.total * 100).toFixed(0)}%\n output.system (output.system || ) progressText return output }, event: async ({ event }) { if (event.type part.updated) { const part event.properties.part const sessionId event.properties.sessionID if (part.type tool_call) { const current progressMap.get(sessionId) if (current) { progressMap.set(sessionId, { ...current, step: current.step 1, description: 执行工具: ${part.name || unknown} }) } } if (part.type text) { const text part.text || const current progressMap.get(sessionId) if (current) { if (text.includes(分析完成)) { progressMap.set(sessionId, { ...current, description: 分析完成开始生成 }) } else if (text.includes(正在生成)) { progressMap.set(sessionId, { ...current, description: 正在生成代码... }) } } } } if (event.type session.created) { const sessionId event.properties.sessionID progressMap.set(sessionId, { step: 0, total: 10, description: 开始任务 }) } if (event.type session.idle) { const sessionId event.properties.sessionID progressMap.delete(sessionId) } } } }这个插件的核心思路是让 AI 感知自己的进度从而调整输出节奏。当 AI 知道自己已经完成了 80% 的工作时它可能会在后续输出中更简洁。最后你可以把前面所有插件整合成一个完整的仪表盘插件在 TUI 通知区域实时显示 AI 的当前阶段、输出字符数、正在执行的工具和进度百分比。整合后的效果是从“思考中”开始经过“执行工具 (read, grep)”到“生成中... (450 字符)”最后显示“完成 (共 450 字符, 3 个工具) [100%]”。如果你在实现过程中遇到进度条外观定制、更复杂的进度信息展示需求或者想讨论 OpenCode 插件的其他进阶玩法欢迎在评论区留言。后续我还会继续更新 OpenCode 工具函数封装和装饰器库的内容把学到的技巧沉淀成可复用的工具库。
