OpenCode 实现流式响应与进度回调,Base URL 填 TaoToken 的 API 地址
2026/9/21 18:27:09 网站建设 项目流程

1. OpenCode 流式响应与进度回调:为什么你的终端总在“干等”

如果你用 OpenCode 跑过稍微复杂一点的任务,比如让它“分析这个项目的代码结构并给出重构建议”,大概率经历过这种场景:敲下回车,终端光标一闪一闪,然后……就没有然后了。你不知道它是在读文件、在检索依赖、在思考,还是已经卡死了。这种“信息黑洞”体验,本质上是流式响应和进度回调没有配置到位。

OpenCode 本身是支持流式输出的,它通过 SSE(Server-Sent Events)把模型生成的每一个 token 推送到 TUI 界面。但插件层拿不到原始 SSE 字节流,你能用的是event钩子订阅part.updated事件,用tool.execute.before/after监听工具执行,用experimental.chat.system.transform往系统提示里注入进度上下文。这套机制组合起来,就能把“干等”变成“透明过程”。

这篇内容适合已经装好 OpenCode、想给终端加一个“进度仪表盘”的开发者。我会从模型通道准备讲起,把 Base URL 填到 TaoToken 的 API 地址,然后一步步实现流式日志、工具进度、旋转动画和进度上下文注入。全程可复制,踩过的坑我也会标出来。

2. 前置准备:把模型通道接到 TaoToken

OpenCode 要跑起来,首先得有一个能用的模型通道。这一步不涉及任何插件逻辑,只是把 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,区分是texttool_calltool_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 Map<string, { 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.beforetool.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-ai

5.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 Map<string, { 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 工具函数封装和装饰器库的内容,把学到的技巧沉淀成可复用的工具库。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询