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_ping,model 传 gpt-4o-mini如果通道走通,你会看到类似这样的返回:
status=200 body={"id":"chatcmpl-...","object":"chat.completion","choices":[{"index":0,"message":{"role":"assistant","content":"pong"}...status=200说明鉴权通过,choices数组里有内容说明模型正常响应。如果返回status=401,说明 Key 不对或没读到;如果返回status=404,多半是 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.ok:
if (!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,遇到不确定的参数可以直接对照。