1. OpenCode Agent 项目里 Catalogs 到底解决什么问题
如果你正在用 OpenCode 这类 Agent 框架做多包仓库开发,大概率会遇到一个很烦的场景:根目录一个package.json,packages/下面十几个子包各自还有一份package.json,每个子包里都写着"zod": "4.1.8"、"typescript": "5.8.2"。某天要统一升一个版本,你得挨个文件改,漏掉一个,包管理器就给你多装一份旧版本,磁盘里躺着两份甚至三份同一个库。
Catalogs(目录)就是干这个的。它只出现在根目录的package.json里,通过workspaces.catalog字段集中声明整个仓库所有公共依赖的精确版本号。子包不再写具体版本,只写"catalog:"这个占位符,包管理器解析时自动去根目录的字典里查。你可以把它理解成一份“依赖菜单”:根目录负责定标准,子包负责点菜。
这套机制对 OpenCode Agent 项目尤其重要,因为 Agent 项目通常会把模型调用、工具执行、UI 渲染拆成多个子包,每个子包都可能依赖同一批基础库。如果版本不统一,运行时很容易出现“子包 A 用 TS 5.0 编译、子包 B 用 TS 4.9 编译”的诡异冲突。而当我们把统一 Key、统一 API 通道的接入位置也纳入 Catalogs 管理时,整个仓库的依赖和配置就真正做到了“一处改、处处生效”。
这篇就围绕package.json与workspaces两个切入点,把 OpenCode Agent 项目在 Catalogs 配置环节的实操讲清楚:目录结构怎么摆、字段怎么写、统一 API 通道放在哪、怎么用一条命令验证配置生效且请求走通。适合正在搭 Monorepo 的 Agent 开发者、需要统一管理多子包依赖的团队,以及想把模型调用配置收敛到根目录的同学。
2. 接入前的准备:TaoToken 通道与项目结构梳理
在动package.json之前,先把两件事理清楚:一是统一 API 通道从哪来,二是 OpenCode 项目的 workspaces 目录长什么样。
统一 API 通道我用的是 TaoToken,它的 API 地址是https://taotoken.net/api,兼容 OpenAI 风格的请求格式,所以 OpenCode Agent 里那些走baseURL的模型客户端可以直接对接。你需要先在控制台创建一个 API Key,这个 Key 后面会作为环境变量注入到各个子包,而不是硬编码在代码里。控制台入口在https://taotoken.net/console,创建 Key 的页面在https://taotoken.net/api-keys。如果你还没决定用哪个模型,可以先去模型对话页面https://taotoken.net/model-chat试一下请求格式,确认返回结构符合预期再往下配。
然后是项目结构。OpenCode 这类 Agent 项目的典型 workspaces 布局是这样的:
opencode-agent/ ├── package.json # 根目录,含 workspaces + catalog ├── pnpm-workspace.yaml # 如果用 pnpm,工作区声明在这里 ├── packages/ │ ├── core/ # Agent 核心逻辑 │ │ └── package.json │ ├── tools/ # 工具执行层 │ │ └── package.json │ ├── console/ # 控制台/UI │ │ └── package.json │ └── shared/ # 公共工具 │ └── package.json └── .env # 统一环境变量(不提交)根目录的package.json里,workspaces字段告诉包管理器“仓库里有这些子包”,catalog字段则集中声明公共依赖版本。子包的package.json里,依赖版本写成"catalog:",内部包引用写成"workspace:*"。这里要区分两个词:根目录的workspaces(带 s)是物理集合,代表工作区结构;子包里的workspace(不带 s)是本地链接协议,通知包管理器走内部通道拿包,不用上外网。
统一 API 通道的接入位置,我建议放在根目录的.env加一个共享配置包packages/shared。.env里放TAOTOKEN_API_KEY和TAOTOKEN_BASE_URL,packages/shared导出一个读取配置的函数,其他子包通过"@opencode/shared": "workspace:*"引用它。这样 Key 只在一个地方维护,子包不碰敏感信息。
3. 可复制的 package.json 与 workspaces 配置片段
这一节直接给可复制的配置。先看根目录package.json,重点是workspaces和catalog两个字段:
{ "name": "opencode-agent", "private": true, "packageManager": "pnpm@9.0.0", "workspaces": { "packages": [ "packages/*" ], "catalog": { "typescript": "5.8.2", "zod": "4.1.8", "hono": "4.6.3", "solid-js": "1.9.3", "@solidjs/router": "0.15.1", "openai": "4.77.0", "dotenv": "16.4.7" } }, "scripts": { "verify:catalog": "node scripts/verify-catalog.mjs" } }注意catalog是写在workspaces对象内部的,不是和workspaces平级。这是很多同学第一次配容易写错的地方。openai这个包之所以放进 catalog,是因为 OpenCode Agent 里多个子包都要用它构造模型客户端,统一版本能避免请求格式不一致。
如果你用的是 pnpm,工作区声明通常在pnpm-workspace.yaml,但 catalog 依然可以放在根package.json的workspaces.catalog里,两者不冲突:
packages: - "packages/*"再看子包packages/core/package.json,依赖版本全部用catalog:占位:
{ "name": "@opencode/core", "version": "0.1.0", "type": "module", "dependencies": { "@opencode/shared": "workspace:*", "openai": "catalog:", "zod": "catalog:", "dotenv": "catalog:" }, "devDependencies": { "typescript": "catalog:" } }@opencode/shared用workspace:*,表示走本地软链接;openai、zod用catalog:,表示去根目录字典查版本。子包完全不需要知道openai是 4.77.0 还是别的,升级时只改根目录一处。
接着是统一 API 通道的配置包packages/shared/src/config.ts:
import 'dotenv/config'; export interface TaoTokenConfig { apiKey: string; baseURL: string; defaultModel: string; } export function loadTaoTokenConfig(): TaoTokenConfig { const apiKey = process.env.TAOTOKEN_API_KEY; if (!apiKey) { throw new Error('TAOTOKEN_API_KEY 未设置,请检查根目录 .env'); } return { apiKey, baseURL: process.env.TAOTOKEN_BASE_URL ?? 'https://taotoken.net/api', defaultModel: process.env.TAOTOKEN_MODEL ?? 'gpt-4o-mini', }; }根目录.env内容:
TAOTOKEN_API_KEY=sk-你的Key TAOTOKEN_BASE_URL=https://taotoken.net/api TAOTOKEN_MODEL=gpt-4o-mini这样packages/core里构造客户端时,直接import { loadTaoTokenConfig } from '@opencode/shared',拿到的就是统一通道。Key 只在.env里出现一次,子包代码里没有任何硬编码。
4. 验证配置生效:一条命令确认请求走通
配置写完,怎么确认 catalog 真的生效、请求真的走通?分两步。
第一步,验证 catalog 解析。在根目录执行:
pnpm install --frozen-lockfile如果 catalog 配置有误,比如子包写了"catalog:"但根目录字典里没有对应条目,pnpm 会直接报错,类似No catalog entry found for "xxx"。安装成功后,检查node_modules/.pnpm里对应包的版本,应该和根目录 catalog 声明的一致。你也可以用一条更直接的命令:
pnpm why openai输出会显示openai被哪些子包引用、解析到哪个版本。如果多个子包都引用它,版本应该只有一个,这就是 catalog 起作用的证据。
第二步,验证 API 请求走通。在packages/core里写一个最小验证脚本scripts/verify-request.mjs:
import OpenAI from 'openai'; import { loadTaoTokenConfig } from '@opencode/shared'; const config = loadTaoTokenConfig(); const client = new OpenAI({ apiKey: config.apiKey, baseURL: config.baseURL, }); const res = await client.chat.completions.create({ model: config.defaultModel, messages: [{ role: 'user', content: '回复 OK 两个字母即可' }], }); console.log('baseURL:', config.baseURL); console.log('model:', res.model); console.log('content:', res.choices[0].message.content);在根目录执行:
node packages/core/scripts/verify-request.mjs预期输出类似:
baseURL: https://taotoken.net/api model: gpt-4o-mini content: OK看到content: OK,说明三件事都对了:catalog 把openai版本统一解析了、@opencode/shared的 workspace 软链接生效了、API 请求通过统一通道走通了。如果baseURL打印出来是undefined或者默认值不对,回去检查.env是否被dotenv正确加载,以及packages/shared是否在子包依赖里声明了workspace:*。
5. 常见报错排查:401、local proxy failed、reading choices
配置过程中最容易撞上的几个报错,我按实际遇到的频率排一下。
401 Unauthorized。这个最常见,通常是TAOTOKEN_API_KEY没读到或者 Key 失效。先确认.env在根目录而不是子包目录,dotenv/config默认从当前工作目录找.env。如果你在子包目录里直接跑脚本,工作目录不对,.env就加载不到。解决办法是在根目录跑,或者在脚本里显式指定路径dotenv.config({ path: '../../.env' })。另外确认 Key 是从https://taotoken.net/api-keys创建的,没有多余空格。
local proxy failed。这个报错一般出现在请求根本没发出去的时候,比如baseURL写成了https://taotoken.net(少了/api),或者环境变量里混入了其他代理配置。检查TAOTOKEN_BASE_URL是否精确等于https://taotoken.net/api,以及 shell 里有没有残留的HTTP_PROXY之类变量干扰。清掉后重跑验证脚本。
reading 'choices'。典型报错是Cannot read properties of undefined (reading 'choices'),说明res是 undefined,请求返回了非预期结构。常见原因是模型名写错,或者请求体格式不对。先打印完整响应console.log(JSON.stringify(res, null, 2)),看返回里有没有error字段。如果模型名不在可用列表里,换成TAOTOKEN_MODEL里确认过的值。还有一种情况是openai包版本和请求格式不匹配,这时候回去检查 catalog 里openai的版本是否被正确解析,用pnpm why openai确认。
OAuth 相关报错。如果你在 OpenCode 里用了需要 OAuth 的模型客户端,报错可能提示 token 过期或回调失败。这类问题通常和 catalog 无关,而是认证流程本身。确认你用的是 API Key 模式而不是 OAuth 模式,loadTaoTokenConfig返回的是apiKey字段,直接传给new OpenAI({ apiKey })即可,不需要走 OAuth 回调。
排查时记住一个原则:先确认配置读到了(打印config),再确认请求发出去了(打印baseURL),最后确认响应结构对(打印完整res)。三步定位,基本不会卡太久。
6. 把统一通道固化进工作流
配置跑通之后,建议把验证脚本挂到 CI 或者 pre-commit 钩子里。根目录package.json里已经加了verify:catalog脚本,你可以再补一个verify:request,在每次改完 catalog 或.env结构后跑一遍。这样团队里任何人升级依赖版本,都不会悄悄破坏 API 通道。
长期做 Agent 编码和工具链开发的话,可以考虑把模型调用收敛到packages/shared里统一封装,子包只调用封装后的函数,不直接碰openai客户端。这样以后换模型、换通道,只改一个文件。如果你需要更稳定的长期编码额度,可以了解一下 Coding Plan:https://taotoken.net/coding-plan。接入文档在https://taotoken.net/doc,里面有针对不同语言和框架的示例,配合这篇的 catalog 配置一起看,基本能把 OpenCode Agent 项目的依赖和通道一次性理顺。