
1. 为什么要在 opencode 里写一个 TypeScript 插件opencode 是一个跑在终端里的 AI 编码代理它本身提供了插件机制允许你用 JavaScript 或 TypeScript 挂钩各种事件、注册自定义命令、甚至注入环境变量。但很多人第一次接触它时会卡在一个很实际的问题上插件写出来了请求却还是走默认通道怎么把 endpoint 和鉴权统一改到自己的 Key 通道这就是本篇要解决的问题。我会带你从零初始化一个 TypeScript 插件项目用 opencode SDK 注册一个自定义命令然后把插件发出的请求指向 TaoToken 的统一 Key 通道最后跑一次真实调用验证请求确实走通了。适合谁看已经用过 opencode、想扩展它行为的开发者手里有多个模型 Key、想统一管理入口的人以及想学 opencode 插件开发但不知道从哪下手的新手。你不需要精通 Bun 或 Zod只要会写基本的 TypeScript 就能跟上。先说清楚 opencode 插件的加载逻辑这决定了你把文件放哪里。它支持两种加载方式本地文件加载和 npm 包加载。本地文件放在.opencode/plugins/项目级或~/.config/opencode/plugins/全局启动时自动加载npm 包则在配置文件里用plugin数组声明启动时用 Bun 自动安装并缓存到~/.cache/opencode/node_modules/。加载顺序也有讲究全局配置 → 项目配置 → 全局插件目录 → 项目插件目录。同名同版本的 npm 包只加载一次但本地插件和名字相似的 npm 插件会分别独立加载。理解这一点后面排查“为什么我的插件没生效”会省很多时间。插件本身是一个模块导出一个或多个插件函数。每个函数接收上下文对象返回一个钩子对象。上下文里有project、client、$、directory、worktree这几个关键字段其中client就是用来和 AI 交互的 SDK 客户端也是我们后面改请求通道的入口。2. TaoToken 统一 Key 通道的前置准备在动手写插件之前先把 TaoToken 这边的准备工作做完。TaoToken 提供的是一个统一的 API 通道你可以把它理解成一个“请求中转站”插件不用关心底层是哪个模型厂商只要把 endpoint 指向它、带上统一的 Key就能调用。第一步是拿到 API Key。访问 https://taotoken.net/api-keys 创建你的密钥。这个 Key 是后续所有请求的鉴权凭证格式通常是一串以特定前缀开头的字符串。创建后先复制保存页面刷新后不一定能再看到完整值。第二步是确认 Base URL。TaoToken 的 API 入口是 https://taotoken.net/api注意这里不带任何查询参数。你的插件在拼接请求地址时应该以这个为前缀后面再接具体的路径比如/v1/chat/completions或 SDK 约定的端点。第三步是选模型 ID。TaoToken 支持多种模型具体可用列表可以在模型对话页面查看https://taotoken.net/models。选一个你常用的模型 ID 记下来后面写进插件配置。这里有个容易踩的坑很多人会把 Base URL 写成带/v1的完整路径结果插件再拼一次/v1变成/v1/v1/...请求直接 404。正确做法是 Base URL 只写到https://taotoken.net/api版本路径交给 SDK 或你自己在代码里补。如果你打算长期用 opencode 做编码和 Agent 任务可以考虑 Coding Plan它更适合高频调用场景https://taotoken.net/coding-plan。不过本篇的插件演示用普通 API Key 就够了。还有一点要提醒opencode 的 npm 插件在启动时会用 Bun 自动安装依赖所以你的插件如果依赖了外部包要么在配置目录里放一个package.json声明依赖要么把插件发布到 npm。本地插件想用外部包必须在配置目录创建package.json否则启动时会报模块找不到。3. 可复制的插件项目配置与入口文件现在进入实操。先建项目目录初始化 npm 和 TypeScript。mkdir opencode-taotoken-plugin cd opencode-taotoken-plugin npm init -y npm install -D typescript types/node npm install opencode-ai/pluginpackage.json需要补上类型声明和构建脚本。opencode 的插件包提供了Plugin类型导入它能让你的钩子获得类型检查。下面是一份可直接复制的package.json{ name: opencode-taotoken-plugin, version: 1.0.0, type: module, main: dist/index.js, scripts: { build: tsc, dev: tsc --watch }, dependencies: { opencode-ai/plugin: ^0.1.0 }, devDependencies: { typescript: ^5.4.0, types/node: ^20.11.0 } }配套的tsconfig.json{ compilerOptions: { target: ES2022, module: ESNext, moduleResolution: bundler, outDir: dist, rootDir: src, strict: true, esModuleInterop: true, skipLibCheck: true, declaration: true }, include: [src/**/*.ts] }接下来是插件入口文件src/index.ts。这个插件做两件事注册一个自定义命令以及在shell.env钩子里注入 TaoToken 的环境变量让所有 shell 执行都能拿到统一的 Key 和 Base URL。import type { Plugin } from opencode-ai/plugin; import { tool } from opencode-ai/plugin; const TAOTOKEN_BASE_URL https://taotoken.net/api; const TAOTOKEN_API_KEY process.env.TAOTOKEN_API_KEY ?? ; export const TaoTokenPlugin: Plugin async ({ project, client, $, directory, worktree }) { await client.app.log({ body: { service: taotoken-plugin, level: info, message: TaoToken plugin initialized, extra: { directory, worktree }, }, }); return { shell.env: async (input, output) { output.env.TAOTOKEN_BASE_URL TAOTOKEN_BASE_URL; output.env.TAOTOKEN_API_KEY TAOTOKEN_API_KEY; output.env.OPENAI_BASE_URL TAOTOKEN_BASE_URL; output.env.OPENAI_API_KEY TAOTOKEN_API_KEY; }, tool: { taotoken_ping: tool({ description: Ping TaoToken API through the unified key channel, args: { model: tool.schema.string().describe(Model ID to test), }, async execute(args, context) { const res await fetch(${TAOTOKEN_BASE_URL}/v1/chat/completions, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${TAOTOKEN_API_KEY}, }, body: JSON.stringify({ model: args.model, messages: [{ role: user, content: ping }], max_tokens: 8, }), }); const data await res.json(); return status${res.status} body${JSON.stringify(data).slice(0, 200)}; }, }), }, }; };这里有几个关键点。第一shell.env钩子会把OPENAI_BASE_URL和OPENAI_API_KEY注入到所有 shell 执行环境里这样即使某些工具默认读这两个变量也会自动走 TaoToken 通道。第二自定义工具taotoken_ping直接用fetch打 TaoToken 的/v1/chat/completions用来验证通道是否走通。第三client.app.log是结构化日志比console.log更适合排查问题。构建一下npm run build然后把dist/index.js复制到 opencode 的插件目录。项目级放.opencode/plugins/全局放~/.config/opencode/plugins/。如果你想让插件在多个项目里复用放全局目录更省事。mkdir -p ~/.config/opencode/plugins cp dist/index.js ~/.config/opencode/plugins/taotoken-plugin.js注意opencode 加载本地插件时如果插件里import了外部包需要在配置目录放package.json声明依赖。我们这个插件只依赖opencode-ai/plugin的类型构建后是纯 JS运行时不需要额外依赖所以直接复制即可。4. 验证请求确实走通 TaoToken 通道配置完成后启动 opencode观察日志里有没有TaoToken plugin initialized。如果看到了说明插件加载成功。接下来在 opencode 里调用我们注册的自定义工具。工具名是taotoken_ping参数是模型 ID。假设你选的模型 ID 是gpt-4o-mini在对话里让它执行调用 taotoken_pingmodel 传 gpt-4o-mini如果通道走通你会看到类似这样的返回status200 body{id:chatcmpl-...,object:chat.completion,choices:[{index:0,message:{role:assistant,content:pong}...status200说明鉴权通过choices数组里有内容说明模型正常响应。如果返回status401说明 Key 不对或没读到如果返回status404多半是 Base URL 拼接出了问题。再验证一下环境变量注入。在 opencode 的 shell 工具里执行echo $OPENAI_BASE_URL echo $OPENAI_API_KEY | head -c 8应该输出https://taotoken.net/api和你的 Key 前几位。这说明shell.env钩子生效了后续任何走 OpenAI 兼容协议的工具都会自动指向 TaoToken。如果你想更直观地确认请求确实到了 TaoToken可以在 TaoToken 的 console 里查看调用记录https://taotoken.net/console。每次taotoken_ping调用都会留下一条记录包含模型、时间、token 消耗。这是最直接的“请求走通”证据。实测下来整个链路是这样的opencode 启动 → 加载插件 → 注入环境变量 → 调用自定义工具 → fetch 打到 TaoToken → TaoToken 转发到模型 → 返回结果。任何一环断了都会在 status 或日志里体现。如果你用的是 Claude Code 类的接入场景TaoToken 也提供了对应的接入文档https://taotoken.net/doc。里面的配置方式和本篇插件思路一致都是改 Base URL 和 Key。5. 常见报错排查401、local proxy failed 与 reading choices插件跑不起来报错通常集中在几个地方。下面按真实遇到的错误逐个拆。401 Unauthorized。这是最常见的。原因一般是 Key 没读到或格式不对。先检查TAOTOKEN_API_KEY环境变量有没有在启动 opencode 的 shell 里导出。如果你是在插件里硬编码确认字符串没有多余空格。另外注意shell.env注入的变量只在 opencode 的 shell 执行环境里有效不会影响 opencode 进程本身读取process.env。所以插件里process.env.TAOTOKEN_API_KEY需要你在启动前就 export 好export TAOTOKEN_API_KEY你的Key opencodelocal proxy failed。这个报错通常出现在网络层意思是本地代理连接失败。检查你的 Base URL 是不是写成了http://localhost:xxxx之类的本地地址。TaoToken 的地址是https://taotoken.net/api不要加端口不要加/v1。如果你之前配过其他工具的代理设置确认没有残留的环境变量干扰比如HTTP_PROXY、HTTPS_PROXY。reading choices。这个报错说明代码在解析响应时data.choices是 undefined。原因通常是响应体不是预期的 JSON 结构可能是 401 或 404 的错误页被当成正常响应解析了。解决办法是在解析前先判断res.okif (!res.ok) { const text await res.text(); return error status${res.status} body${text.slice(0, 200)}; } const data await res.json(); return status${res.status} choices${data.choices?.length ?? 0};这样即使出错你也能看到真实的错误信息而不是一个模糊的 TypeError。OAuth 相关报错。如果你在插件里用了需要 OAuth 的 SDK 方法可能会遇到 token 过期或 scope 不足。opencode 的client对象本身走的是本地会话不涉及 OAuth但如果你在自定义工具里调用了外部 OAuth 服务需要单独处理刷新逻辑。建议把 OAuth 逻辑和 TaoToken 的 Key 鉴权分开不要混在一个工具里。插件没加载。检查文件是不是放在了正确的目录文件名是不是.js结尾opencode 加载本地插件时对扩展名有要求。另外如果你同时放了全局和项目级插件注意加载顺序项目级会覆盖全局的同名钩子。依赖找不到。本地插件如果 import 了外部包必须在配置目录放package.json并运行bun install。opencode 启动时会自动跑bun install但如果你的package.json路径不对或者依赖版本冲突就会报模块找不到。最稳妥的办法是把插件构建成零依赖的纯 JS就像本篇的做法。排查时记住一个原则先看 status code再看响应体最后看日志。client.app.log输出的结构化日志会带上 service 和 level比console.log更容易过滤。6. 把插件接入长期编码工作流插件跑通之后你可以把它扩展成更实用的形态。比如在tool.execute.before钩子里拦截所有 bash 命令自动注入 TaoToken 的环境变量或者在session.idle事件里发通知提醒你任务完成。如果你打算把 opencode 作为日常编码代理建议把 TaoToken 的 Key 和 Base URL 统一配在全局插件里这样所有项目都能复用。项目级的.opencode/plugins/则用来放项目特有的逻辑比如某个仓库专用的自定义工具。对于需要长期跑 Agent 任务的场景Coding Plan 比按量计费更划算https://taotoken.net/coding-plan。它适合高频调用、多轮对话、代码生成这类消耗 token 较多的任务。最后给一个实用技巧把taotoken_ping工具保留在插件里每次改完配置先 ping 一下确认通道正常再开始正式工作。这比等到任务跑到一半才发现 401 要省心得多。完整的 API 文档和接入示例在 https://taotoken.net/doc遇到不确定的参数可以直接对照。