trae-cn-provider
Manifest validTrae CN as a native DeepSeek Harness LLM provider: browser OAuth login by default, an opt-in local-install credential source, and an OpenAI-compatible surface on the host web server.
trae-cn-provider
把 Trae CN 作为一个原生 LLM provider 接入 DeepSeek Harness(DSH)。 安装后 Trae 的模型会直接出现在 DSH 的模型选择器里,也可以额外开出一个 OpenAI 兼容接口给 Cline / Claude Code 之类的客户端用。
macOS 优先、源码可审计、凭据不出本机。
⚠️ 先读这一段:风险声明
- 本项目违反 Trae(字节跳动)的使用条款。 它把 Trae 客户端内部的接口当作通用 LLM API 使用,这不是官方支持的用法。
- 你的 Trae 账号有被封禁的风险。 请自行判断是否接受。
- 消耗的是你自己账号的额度/积分。本项目不提供、不转让、不代充任何额度。
- 本项目与 Trae、字节跳动没有任何关系,未获其授权或背书。
- 请不要把本插件连同你的凭据一起分发。凭据只应存在于你自己的机器上。
- 作者不对因使用本项目产生的任何后果负责(包括但不限于账号封禁、额度损失、数据问题)。如果你不接受以上任何一条,请不要安装。
它做什么
- 以 DSH 原生
LlmAdapter的形式注册一个 provider route:trae-cn - 模型自动出现在模型选择器(从 Trae 的模型目录实时拉取)
- 支持流式正文、思考链(reasoning)、原生 function calling、上下文窗口、思考档位
- 支持图片输入(多模态模型):DSH 的图片块经 attachments 缝内联为 data URL;OpenAI 层也接受
image_url - 可选:在宿主已有的 web server 上挂一个 OpenAI 兼容面(
/v1/models、/v1/chat/completions),不自己占用端口 - 同时提供一个独立 CLI,可脱离 DSH 单独当本地代理跑
它不做什么
- 不代理、不中转、不上传任何凭据到第三方。代码里出网的域名只有 Trae 官方域名(见「出网清单」)。
- 不共享额度给别人,不做多账号池,不做额度转卖。
- 不修改 Trae 客户端,不 hook 它的进程。
安装
# 从 npm 安装到 web profile(安装即激活:会自动追加进 dsh.profile.bundles)
dsh plugin --profile web add trae-cn-provider
# 桌面版:需要先完全退出 DeepSeek Harness 桌面应用
dsh plugin --profile desktop add trae-cn-provider
桌面 profile 的坑:
dsh plugin --profile desktop要求桌面应用完全退出后再执行 (否则报Open DeepSeek Harness Desktop once to initialize its profile, then fully quit it)。 卸载:dsh plugin --profile <p> remove trae-cn-provider。
安装后重启对应 profile 即可。模型选择器里会出现 Trae CN 分组。
登录与凭据来源
插件有三档凭据来源,通过 credentialSource 配置:
| 值 | 行为 | 需要浏览器 | 包内是否存在解密凭据的代码路径 |
|---|---|---|---|
oauth(默认) | 只在 DSH 里走一次浏览器登录,凭据存本机 | 是(一次) | 否 |
local | 直接读本机已登录的 Trae 客户端凭据 | 否 | 是(显式开启才生效) |
auto | 先看已保存的 OAuth 凭据,没有再看本机客户端 | 视情况 | 是(同上) |
oauth(默认,推荐)
在 DSH 里执行登录命令(默认 /trae-login),会打开浏览器到 Trae 官方授权页:
https://www.trae.cn/authorization?...&auth_callback_url=http://127.0.0.1:<port>/authorize
实测发现:Trae 的回调不是下发授权码,而是直接把凭据塞进回调参数(userJwt 是 URL 编码的 JSON,含 Token / RefreshToken / TokenExpireAt;userInfo 含 UserID / Name)。所以不存在「换码」这一步。回调只在 127.0.0.1 上临时监听,拿完即关。
凭据默认落在 ~/.trae-proxy/credentials.json(权限 0600)。若 DSH 的 credentials 服务可用,会优先存进那里。
目录可以用环境变量 TRAE_PROXY_CRED_DIR 覆盖(测试用它做隔离;也可以在你想把凭据放到别处时用)。
local(可选,零摩擦)
直接读本机 Trae 客户端的登录态,不需要浏览器。支持两种存储格式,运行时自动嗅探:
| edition | 存储格式 | 上游 |
|---|---|---|
cn Trae CN | tc 加密 | trae-api-cn.mchost.guru |
solo TRAE SOLO CN | tc 加密 | trae-api-cn.mchost.guru |
sg Trae 国际版 | 明文 JSON | a0ai-api-sg.byteintlapi.com |
solo-sg TRAE SOLO | tc 加密 | a0ai-api-sg.byteintlapi.com |
路径:macOS ~/Library/Application Support/<App>/User/globalStorage/storage.json,Windows %APPDATA%\<App>\User\globalStorage\storage.json。
必须知道的取舍:
tc格式的密钥材料完全内嵌在密文自身(第 6–38 字节派生),不需要 macOS Keychain,也不受它保护 —— 这意味着本机任何能读到那个storage.json的进程都能解出你的 Trae token。这是 Trae 自身格式的性质,不是本插件引入的。选择
local会让包内存在这条解密路径。如果你关心「发布包里有没有解密凭据的代码」,请保持默认的oauth。
local随时可能失效,这是正常现象。storage.json归 Trae 客户端所有,它会自己改写这个文件 —— 退出登录、重置登录态、原地迁移都会把iCubeAuthInfo://icube.cloudide这个键删掉(本机实测发生过:该键在客户端运行期间被移除,只留下设备密钥和 usertag)。所以不要把
local当成长期可靠的来源:
- 想省事又要稳,用
auto(已保存的浏览器登录优先,本机凭据兜底)。- 键消失时插件会返回 502 并说明是哪个 edition 缺什么,不会崩溃;按提示在客户端重新登录,或换回
oauth。edition: auto会按cn → solo → sg → solo-sg顺序挑第一个能读的,但不校验它的过期时间,可能选中一个很久以前就过期的凭据(本机sg就是 2 月的)。这种时候上游会返回认证错误,看错误里的 edition 就知道换哪个了。
配置
通过 profile 的 cordis.patch.yml 覆盖:
- id: trae-cn
config:
credentialSource: oauth # oauth | local | auto
edition: cn # local/auto 时读哪个 edition
requestStyle: chat_v3 # chat_v3(实测可用)| inline_chat(实验,实测失败)
appVersionCode: 20260901 # 必须是纯整数,见下方说明
ideVersion: '' # 留空 = 从本机 Trae 安装读取
displayName: Trae CN
loginCommand: trae-login
mountOpenAIEndpoint: true
openAIPath: /trae-cn # 挂载前缀(不是 base URL):对外接口为 /trae-cn/v1/...
allowBrowserOrigins: [] # 默认 []:带 Origin 头的浏览器请求一律 403
allowBrowserOrigins 是安全默认:该端点花的是你自己账号的 Trae 额度,而任何网页都能向 127.0.0.1
发请求,所以默认拒绝所有带 Origin 头的请求。Hermes、openai-python、curl 这类服务端客户端不发
Origin,不受影响;确实需要浏览器直连时,把来源(例如 https://my-tool.local)加进这个列表。
appVersionCode 会作为 x-app-version-code / x-ide-version-code 发送。它必须是纯整数:带点的值(例如 2.3.90452)会被上游以
bad request: binding: expr_path=app_version_code, cause=parameter type does not match binding data 拒绝。
而且网关按该值的数值大小做灰度,值越小返回的 function 配置越少。实测阈值:
| 取值 | 目录条目 | 过滤后可聊天模型 |
|---|---|---|
20260401 | 42 | 7 |
20260701 | 46 | — |
>= 20260901 | 57 | 20(完整目录) |
默认值已取 20260901,无需手工配置即可看到全部模型。本机 product.json 的 traeVersionCode(20250325)偏旧,detectEnv() 只会取「不比基准旧」的值。
OpenAI 兼容接口
mountOpenAIEndpoint: true(默认)时,挂在宿主 web server 的 openAIPath 下:
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /trae-cn/v1/models | 模型列表 |
| POST | /trae-cn/v1/chat/completions | 对话(流式 / 非流式,含 reasoning_content 与 tool calls) |
| GET | /trae-cn/health | 健康检查 |
独立 CLI 也可以把同样的接口跑在自有端口上。注意独立进程的路径没有 /trae-cn 前缀,直接就是 /v1/...:
node src/cli.js login # 浏览器登录
node src/cli.js models # 列出模型
node src/cli.js serve --port 8317 [--api-key <key>] # → http://127.0.0.1:8317/v1
node src/cli.js chat "你好"
浏览器来源默认被拒绝。 以前这里无条件回
Access-Control-Allow-Origin: *,意味着你浏览器里 任意一个页面都能对127.0.0.1上的这个端点发请求,用你的 Trae 登录态和额度跑模型。从 0.1.6 起, 带Origin头的请求直接回403,除非来源被加进allowBrowserOrigins。服务端客户端(Hermes、openai-python、curl)不发Origin,行为不变。
可用模型
模型目录是从上游实时拉取的,不在代码里硬编码。本机实测(2026-10)过滤后 20 个用户可选模型:
| config_name | 显示名 | 思考档位 |
|---|---|---|
Doubao-Seed-Evolving | Seed-Evolving | yes |
Doubao-Seed-2.1-Pro | Seed-2.1-Pro-0915 | light / high |
Doubao-Seed-2.1-Turbo | Seed-2.1-Turbo | light / high |
Doubao-Seed-Code | Seed-Code | light / high |
mimo-v2.6-flash | MiMo-V2.6-Flash | yes |
mimo-v2.6-pro | MiMo-V2.6-Pro | yes |
step-5-preview | Step-5-Preview | light / high |
glm-5.3-flashx | GLM-5.3-FlashX | light / high / extra_high |
glm-5.3-flash | GLM-5.3-Flash | light / high / extra_high |
glm-5.3 | GLM-5.3 | light / high / extra_high |
glm-5.2 | GLM-5.2 | high / extra_high |
deepseek-v4.1-flash | DeepSeek-V4-Flash 正式版 | light / high / extra_high |
DeepSeek-V4-Flash-Official | DeepSeek-V4-Flash | light / high / extra_high |
DeepSeek-V4-Pro-Official | DeepSeek-V4-Pro | light / high / extra_high |
kimi-k3 | Kimi-K3 | light / high / extra_high |
kimi-k2.8-preview | Kimi-K2.8-Preview | light / high / extra_high |
minimax-m3 | MiniMax-M3 | — |
qwen3.8-flash | Qwen3.8-Flash | light / high / extra_high |
qwen3.8-max | Qwen3.8-Max | light / high / extra_high |
qwen-3.7-plus | Qwen3.7-Plus | — |
目录共 57 条,其余条目(custom_model_* 占位、内部工具模型、is_invisible_to_user 的旧条目)不是可用的聊天模型,已按上游标记过滤掉 —— 放宽过滤不会多出可用模型,只会多出噪音。
两个决定目录大小的独立维度(缺一不可,只改一个会静默停在旧目录):
appVersionCode的数值大小(灰度阈值,见上文)。- 登录设备的
DeviceName—— 留成TraeProxy会被上游识别为非真客户端,只回 2 条 trial 兜底目录。现已默认取本机短主机名,可用TRAE_PROXY_DEVICE_NAME覆盖。
与同类项目的差异
生态里已有几个同类项目,写清楚差异免得你重复踩坑:
| 项目 | 形态 | 与我们不同的地方 |
|---|---|---|
dsh-trae-api | DSH 插件 + 独立 HTTP 服务 | 它的目录路径写死 %APPDATA%,正主在 macOS 上 100% 不可用;且把 token 明文写进包目录下的 .env;默认 API Key 硬编码;1.0.0/1.0.1 曾绑 0.0.0.0 且无鉴权(1.0.2 才修) |
@casually/dsh-trae-api | 同上(fork) | 修了 macOS 路径,只做 HTTP 服务,不是 DSH 原生 provider |
@hajilongshi/dsh-trae-api | 同上(fork) | 功能最全,但声明的 GitHub 仓库 404、维护者 GitHub 账号不存在,溯源无法核验 |
| Trae-Relay | Windows 单 exe(C++) | 仅 Windows;用它做协议参考(本项目的协议事实来源之一) |
| Trae-Proxy | 反方向 | 是「把自己的 key 塞进 Trae」,与本项目方向相反 |
本项目的做法不同:
- DSH 原生
LlmAdapter,不是外部 HTTP 服务的壳 —— 工具调用、思考档位、usage 走 DSH 原生类型,不经过有损的 OpenAI 转换 - 不自己 listen 端口,OpenAI 兼容面挂在宿主 web server 上
- 企业版账号的刷新按账号自身的 host 走(见下),这是同类项目普遍没处理的
- 凭据落在用户目录的
0600文件,不落在包目录 - 全部出网域名可枚举、有完整性校验、凭据不进日志
实测验证状态
下面这些是在真机上用真实账号跑过的,不是推断:
| 能力 | 结果 |
|---|---|
模型目录 get_detail_param | 200,57 条 config → 过滤后 20 个用户模型(appVersionCode >= 20260901) |
流式对话 llm_utils_chat | 正文 + reasoning_content 增量正常 |
| usage | 平铺字段正常,prompt cache 生效(实测 cached_tokens: 128) |
| 原生 function calling | 完整 tool_calls:get_weather {"city":"北京"} |
finish 语义 | 上游在带工具调用时也回 finish_reason: "stop",已按实测改成 {kind:'tool-calls'} |
| OpenAI 兼容层 | /v1/models、/v1/chat/completions(流式 + 非流式) |
| 多模态图片输入 | 96×96 纯红 PNG 走两条路径都实测通过:DSH {type:'image'} 块 + attachments 缝,以及 OpenAI 层的 image_url data URL —— 模型均回答「红色」。脚本 test/live-image.js |
本机凭据 tc 解密 | cn(加密)/ sg(明文)两种格式均解出并调通上游 |
| token 刷新 | OAuth 与 local 两条来源都曾刷新成功(走账号自己的 host、双信封解析)。注意 refresh token 会被作废,见下方「refresh token 会失效」 |
| 过期触发刷新链路 | test/live-refresh.js 用隔离的凭据副本把 expiresAt 强制到过去,验证「过期 → 自动刷新 → 请求成功 → 新 token 落盘」;当前该凭据的 refresh token 已失效,需要重新登录后本项才能通过(脚本会明确诊断,不会让你误以为插件坏了) |
| adapter 端到端 | listModels 20 个 / resolveModel 上下文正确 / stream 正文+思考+usage+工具调用 |
| 跨 DSH 版本 | 在 0.1.2-rc.1、0.2.0-rc.2、0.2.1-alpha.2 三个宿主上分别启动并验证:provider 注册、resolveModelInfo、挂载接口(health / models / 对话)全部通过。后两个分别对应桌面应用打包的版本与 npm 上的最新版 |
| 别的框架直连(Hermes) | 独立进程 serve --port 8317 下 20 个模型全部可用;Hermes 侧流式 / 非流式 / function calling 均通过,-z 一轮问答正常返回,会话的 billing_provider 落在 trae。排错细节见「让别的 agent 框架也用上」 |
企业版账号的两个坑(同类项目基本没处理,会导致刷新永远失败):
- 刷新主机不能写死
api.trae.cn。企业版要打账号自己的 host(account.saasBootConfig.apiHost,或网页登录回调的host参数),否则返回20101 refresh token is invalid。 - 企业版网关用
{code, message, Data}信封,不是{ResponseMetadata, Result},两者都要兼容。
refresh token 会失效,而且网关会把它说成「网络异常」
这是个运维事实,不是插件缺陷,但很容易误判:
- 网关对一枚失效的 refresh token 回的是自己的业务码
30022 网络异常,看上去像瞬时网络故障,实际是鉴权失败,唯一的恢复方式是重新登录。 - refresh token 会被作废:Trae 客户端自己那枚轮换后,你手上的副本就死了;任何一次刷新如果结果被丢弃(例如调试脚本干过一次),旧值也就没用了。
- 因此:access token 的有效期就是「不用重新登录」的上限(实测
TokenExpireDuration是 14 天)。
插件对此的处理:
/trae-login的返回值必须符合宿主的CommandResult联合类型。@deepseek-ai/dsh-commands会在注册边界校验它(其lib/index.js的normalizeResult),不合规就抛command "<名字>" returned unknown result kind "<kind>"。它只接受两种形状:{ kind: 'success', text?: string }与{ kind: 'error', text: string }(字段名是text,错误文本且必须非空)。这条以前写成{ kind: 'text' },于是每次/trae-login都会先把登录做完、再在返回的那一刻抛错。离线套件当时只验证了命令「已注册」而没验证它的返回值,所以没抓到;现在由lib/support.js的commandSuccess/commandFailure统一产出,并有测试对着上面这条契约守着。prepareCall().stream()必须直接返回一个 AsyncIterable,不能返回「解析成 AsyncIterable 的 Promise」。dsh-llm@0.2.0-rc.2的adapterStream对dispatch(...)的返回值同步解引用[Symbol.asyncIterator](),拿到 Promise 就直接抛dispatch(...)[Symbol.asyncIterator] is not a function—— 于是每一次 Trae 模型调用都是立刻失败。这条以前把stream对准了 async 的streamWithInfo(它await之后才返回 chunk 生成器,等于返回 Promise)。现在由prepareCall()返回stream: (options) => this.streamPrepared(...),streamPrepared是一个 async generator,yield* await this.streamWithInfo(...)把「先用 Promise 完成建连、再把 chunk 流扇出」的既有时序原样保留;test/smoke-plugin.js里有对着同步契约(typeof dispatched[Symbol.asyncIterator] === 'function')的回归测试守着。- 企业版/租户主机上会在两个刷新端点之间自动降级。需要设备签名的
/trae/api/v3/oauth/ExchangeToken在console.enterprise.trae.cn这类租户主机上根本不存在(网关回 404 + HTML),而免签名的/cloudide/api/v3/trae/oauth/ExchangeToken存在。所以刷新会先试签名那条,只有在端点确实缺失(404/405)时才退到免签名那条;如果服务器是主动拒绝签名,则原样抛出,绝不悄悄降级到更弱的流程。 - access token 还没真过期时,刷新失败不致命。
needsRefresh()提前 30 分钟触发,所以这半小时内一次短暂刷新失败会降级为警告 + 继续用现有凭据,而不是把本来还能成功的请求打掉。只有 token 真的过期了才硬失败。 - 硬失败时不再原样抛出「网络异常」,而是抛
LlmError('REFRESH_FAILED'),消息里说明「refresh token 已失效」并给出重登命令,同时保留原始上游原因便于排查。 - 401 之后的重试是真的会刷新。这里曾经有个隐蔽 bug:旧代码靠写入
expiresAt: 0来表示「强制刷新」,但needsRefresh()把 falsy 的过期时间当成「未知,不主动刷」,于是那次「重试」其实用的是同一枚刚被拒的 token。现在改成显式的ensureFresh(force = true)。 credentialSource: local读到的凭据刷新后只留在内存,绝不写回 OAuth 凭据文件 —— 否则两个来源会静默错位。- 上面每一条都有离线测试锁住(见「开发」一节),
test/live-refresh.js则把两种失效情况区分开并明确诊断。
出网清单
代码里出现的全部外部域名:
| 域名 | 用途 |
|---|---|
trae-api-cn.mchost.guru | CN 版模型目录 + 聊天 |
a0ai-api-sg.byteintlapi.com | 国际版模型目录 + 聊天 |
api.trae.cn | 令牌刷新(默认 host) |
console.enterprise.trae.cn 等 | 企业版账号自己的刷新 host(由凭据决定,不写死) |
www.trae.cn | 浏览器授权页(仅 oauth 来源,由浏览器打开) |
127.0.0.1 | 本地回调监听(仅 oauth 登录期间,临时端口,用完即关) |
没有任何第三方域名,没有任何把 token 发往非 Trae 域名的代码路径。
工作原理(协议要点)
完整逆向记录见 PROTOCOL.md(400+ 行)。关键点:
POST https://trae-api-cn.mchost.guru/api/agent/v3/llm_utils_chat
Authorization: Cloud-IDE-JWT <token>
X-Cloudide-Token / X-Ide-Token / X-Uid / x-app-id / x-app-version-code / x-ide-version ...
{ "messages": [{ "role":"user", "content":[{"type":"text","text":"..."}] }],
"function": "chat_v3",
"stream": true,
"config_name": "glm-5.2",
"model": "glm-5.2__dev",
"model_name": "glm-5.2__dev",
"config_source": 1,
"is_custom_model": false,
"provider": "" }
messages[].content必须是 parts 数组,传字符串会被拒(cannot unmarshal string into ... content of type []*...)model/model_name要用目录里model_detail_list[].model_name(带__dev后缀)tools[].function.parameters上游要求 JSON 字符串(不是对象)- SSE 事件:
output(content/response/reasoning/reasoning_content都是增量)、token_usage、done、error、[DONE]
目录结构
lib/index.js 插件入口:{ name, inject, Config, apply },注册 provider 与 adapter
lib/adapter.js LlmAdapter 子类:目录 → 模型信息,Trae 事件 → StreamChunk
lib/chat-body.js 请求体构造(chat_v3 / inline_chat 两种形态)
lib/messages.js DSH block 消息 / legacy OpenAI 消息 → Trae parts
lib/credentials.js 凭据存储与刷新(oauth / local / auto 三档)
lib/attribution.js DSH 强制的 attributionHeaders 接入
src/trae-client.js 上游客户端:目录 + SSE 解析 + 工具调用累积
src/trae-auth.js OAuth 登录、回调解析、令牌刷新(两种信封)
src/trae-local-credentials.js 本机凭据读取(tc 解密 + 明文)
src/openai-server.js OpenAI 兼容层
src/cli.js 独立 CLI(login / models / serve / chat)
开发
npm run check # 语法 + YAML + 清单一致性 + Config schema + 四套 smoke 测试(全部离线,mock fetch)
npm test # 只跑四套 smoke 测试
smoke 测试全部用 stub fetch 拦截上游,不需要凭据、不联网。四套分别覆盖:
smoke-adapter.js—LlmAdapter契约:目录映射、chunk 序列、思考档位、工具调用、usage 映射,以及凭据刷新的三条不变量(并发调用只刷新一次、401 只重试一次不会无限循环、失效 refresh token 报可操作的REFRESH_FAILED)smoke-client.js— 上游客户端与凭据:企业版刷新主机、双响应信封、回调解析、tc 解密smoke-plugin.js— 插件入口apply():Config 是否是可被 cordis 校验的 Standard Schema、provider/adapter 是否按契约注册、挂载的 OpenAI 接口是否真的能应答,以及prepareCall().stream()同步返回 AsyncIterable(防[Symbol.asyncIterator] is not a function回归)smoke-login.js—/trae-login命令全链路:命令注册 →webLogin→ 授权链接 → 合成 Trae 的真实回调 → 凭据落盘。它把openInApp换成桩(否则会真的弹出浏览器),并用TRAE_PROXY_CRED_DIR隔离凭据文件,不会碰你真实的登录态
另有两个需要联网的手动脚本(不在 npm run check 里):
node test/live-refresh.js # 用隔离副本强制凭据过期,验证「过期 → 自动刷新 → 请求成功 → 新 token 落盘」
node test/live-image.js # 现场生成纯红 PNG,验证两条图片路径都真的被模型看见(回答「红色」)
live-refresh 会断言刷新确实打到了账号自己的 host、没有回落到 api.trae.cn、新 token 已落盘,并校验你真实的凭据文件未被触碰(比对 SHA-256)。凭据的 refresh token 已失效时它会明确诊断而不是让你以为插件坏了。
live-image 自己生成图片(不依赖任何 fixture),分别走 DSH 的 {type:'image'} 块 + attachments 缝、以及 OpenAI 层的 image_url data URL —— 离线测试只能断言请求体里有 data URL,证明不了上游真的接受、模型真的看得见,这个脚本补的就是这一步。
第三套是必要的:上面两个真实崩溃(Config 不是 Standard Schema、把
http.Server当请求处理器)都发生在这条路径上,而它一度是零覆盖。
验证插件是否真的注册进了运行中的 DSH
apply() 不抛错不等于注册成功。要拿到直接证据,用只用于验证的探针覆盖层从宿主进程内部读注册状态:
# 注意 --patch 是 dsh 层选项,必须放在 --profile 之前
dsh --patch test/probe-patch.yml --profile web --no-open --port 18318
启动日志会打印(同时写入 /tmp/trae-probe-result.json):
[trae-probe] providers=[{"id":"deepseek-official",...},{"id":"trae-cn","name":"Trae CN"}]
[trae-probe] trae-cn models (7)=["Doubao-Seed-Code","glm-5.2",...]
[trae-probe] resolved first={"id":"Doubao-Seed-Code","contextWindow":256000,"efforts":["light","high"]}
探针文件(test/probe-catalog.mjs、test/probe-patch.yml)不在 files[] 里,不会进发布包。
验证发布产物本身
link: 安装不会装被链接包的 dependencies,所以它不能用来验证发布形态。要验证真实产物:
npm pack
dsh plugin --profile web remove trae-cn-provider
dsh plugin --profile web add ./trae-cn-provider-<version>.tgz
从 tarball 安装时 @deepseek-ai/schemastery 会被真正装上(走真实 schema 路径,而不是内部的兜底实现),这也是发布前应当跑一遍的检查。
在另一个 DSH 版本上验证(重要)
~/.local/bin/dsh 与桌面应用打包的 DSH 是不同版本(本机实测分别是 0.1.2-rc.1 和 0.2.0-rc.2)。只在一个版本上验证是不够的 —— 桌面才装了 @deepseek-ai/* 的会话校验。DSH 的 API 面在两个版本间确有差异(dsh-llm 0.2.0-rc.2 比 0.1.2-rc.1 多出 20+ 个导出,例如 assembleAssistantStream、AssistantStreamAccumulator、createSystemMessage)。
桌面把整个 DSH 打进 app.asar,可以直接抽出来当独立 CLI 跑(test/asar-read.mjs 就是个最小 asar 读取器,不依赖 npx asar):
RES="/Applications/DeepSeek Harness.app/Contents/Resources"
rm -rf /tmp/dsh020 && mkdir -p /tmp/dsh020
node test/asar-read.mjs "$RES/app.asar" extract dsh /tmp/dsh020
# 归档里的 .node 原生绑定是 unpacked 条目,必须从旁边补回来,否则启动会报
# "No usable native binding found for node-addon-require-builtin-darwin-arm64"
cp -R "$RES/app.asar.unpacked/dsh/." /tmp/dsh020/
node /tmp/dsh020/node_modules/@deepseek-ai/dsh/lib/bin.js --version # 打印 0.2.0-rc.2
node /tmp/dsh020/node_modules/@deepseek-ai/dsh/lib/bin.js \
--patch test/probe-patch.yml --profile web --no-open --port 18320
用 web profile 是对的选择:desktop profile 被正在运行的桌面应用占着,dsh 会拒绝操作,而且装进去需要先退出应用。
发布
先说一个容易误解的点:DSH 没有官方插件市场。 在 DSH 应用里搜不到任何 dsh-market 组件,dsh plugin 本身只是 pnpm 的透传壳:
dsh plugin --profile <p> <pnpm 参数...> # 在 $DSH_HOME/profiles/<p>/ 里执行 pnpm
所以「上市场」的落点就是发布到 npm:
npm run check # 先跑一遍离线检查
npm pack # 可选:先验证产物内容
npm publish --access public # 需要你自己的 npm 账号
用户侧安装(add 之后 reconcile() 会自动把包追加进 dsh.profile.bundles,安装即激活,无需手工改配置):
dsh plugin --profile web add trae-cn-provider
dsh plugin --profile desktop add trae-cn-provider # 需先完全退出桌面应用
社区里存在若干「DSH 市场」类的第三方索引/UI 项目,它们与本仓库无关;本插件不依赖任何索引即可安装。
两个非显然的打包决定
1. 所有 @deepseek-ai/* peer 都标了 optional: true,尽管它们在运行时是必需的。
因为它们是宿主进程提供的(DSH 把插件加载进自己的进程,凭 ctx 拿到 llm / credentials 等服务),不该从 npm 装进插件自己的 node_modules。
注意这些范围本身是能从 npm 解析的(>=0.1.2-rc.1 → 0.1.2-rc.1;npm view <pkg> version 只显示 latest 标签,预发布版不会被打 latest,别被它误导)。问题不在装不上,而在于多装:声明为 required peer 时,包管理器会把 DSH 核心的一份副本拉进用户目录 —— 实测同一个 tarball:
| 声明方式 | npm install 结果 |
|---|---|
| required peer | added 14 packages |
| optional peer | added 4 packages |
多出来的 10 个包是宿主已经提供的 DSH 核心,纯属冗余,还可能造成同一模块存在两份实例。生态里的 @furongjun1999/dsh-memory 面对同样处境,做法也是全部标 optional。
2. prepublishOnly 会先跑完 npm run check。
所以 npm publish 不可能发出没通过检查的构建(语法、YAML、清单一致性、Config schema、四套 smoke)。
上架 dsh.so(社区插件市场)
dsh.so 是社区维护的 DSH 插件市场(收录 1.7 万+ 条目,带 L1–L5 验证等级)。先发布到 npm,再去提交 —— 因为它的检查对象恒为已发布的 tarball,不是仓库。
要到 https://www.dsh.so/zh/submit/ 粘贴仓库地址或 npm 包名,检查器会拉取、校验格式、跑安全扫描,通过后生成一份条目供审核。
它的接受标准,逐条对照本包:
| 标准 | 本包 |
|---|---|
DSH 兼容信号(dsh 字段 / cordis manifest / dsh-plugin 话题) | ✅ dsh.bundle.patch + dsh.compatibility,仓库已打 dsh-plugin 话题 |
| README 带安装说明 | ✅ 「安装」小节 |
| SPDX 可识别的开源许可证 | ✅ MIT + LICENSE |
插件 id 符合 ^[a-z0-9]+(-[a-z0-9]+)*$ | ✅ trae-cn-provider |
| 仓库含可扫描源码 | ✅ 15 个 JS 文件 |
| 安全扫描无阻断级发现 | ⚠️ 见下 |
关于安全扫描的风险,先说清楚:它针对 JS/TS 做 AST 分析,找的是硬编码密钥、数据外传端点、破坏性操作、挖矿特征。而 dsh.so 页面上标注的扫描技术提供商是 dsh-plugin-vet(即 @jieai/dsh-plugin-vet),它本身是可公开获取的,所以提交前可以先自己用同一把尺子量一遍:
npx -p @jieai/dsh-plugin-vet vet-gate --package node_modules/trae-cn-provider
0.1.0 的实测结果:
"blocked": false
"verdict": "clean"
32 findings — all severity: info
R3 x26 只读 process 成员(能力触达面)
R15 x2 fetch 目标由运行时构造,静态不可审计(启发式)
R6/R9/R10 各 1
即无 medium/high/critical 发现。两个值得留意的点仍在,供人工复核时对照:
credentialSource: local会解密本机 Trae 的凭据存储。默认值是oauth(不碰任何解密),但代码路径确实在包里。详见 SECURITY.md。test/里的测试语料含有形如 token 的字符串(都是fake-前缀的假值)。社区里有先例:扫描器曾把插件的测试语料当成插件的危险行为(deepseek-harness#2818)。
检查器在提交之前就会给出报告,所以先跑检查、看结论,再决定要不要提交。
收录后兼容性只会标记为 Declared(作者声明);L5「运行验证通过」需要 dsh.so 自己针对具体 DSH 版本实测。你也可以按它给的模板到官方 Discussions 发兼容性报告,帮助把条目升到 Verified。
发布前检查清单:
npm run check全绿npm pack的产物里包含cordis.patch.yml(否则装上也白装)- 从 tarball 安装一次并启动确认(见「开发」一节的验证方法)
- 版本号已更新;
private不是true - 包元数据(
author/repository/homepage/bugs)已填写 —— 别人评估「这个包可不可信」时最先看的就是它们。
让别的 agent 框架也用上(以 Hermes 为例)
插件对外提供的是标准 OpenAI 兼容端点(/v1/models + /v1/chat/completions),任何支持自定义 OpenAI 端点的框架都能直接接入,不需要改插件、也不需要第二次登录。
它有两种暴露方式,端口和路径都不一样,接错是最常见的失败原因:
| 方式 | Base URL | 端口稳定性 |
|---|---|---|
DSH 宿主挂载(mountOpenAIEndpoint: true,默认) | http://127.0.0.1:<DSH Web 端口>/trae-cn/v1 | ❌ DSH 桌面端每次启动都换端口 |
独立进程 node src/cli.js serve --port 8317 | http://127.0.0.1:8317/v1 | ✅ 端口固定 |
⚠️ 不要照抄具体端口号。 DSH 的 Web 端口是每次启动重新分配的(本机实测:
19387→54865→50455,每次重启都换一个,旧端口上不再有进程监听)。同时独立进程的路径里没有/trae-cn前缀。写死错的组合时表现就是:Hermes 能启动、模型列表也能看到,但一发消息就失败。
推荐的接法:用独立进程 + 固定端口。 它不依赖 DSH 是否在跑,DSH 重启也不受影响:
node src/cli.js serve --port 8317 # 常驻,打印 http://127.0.0.1:8317/v1
curl -s http://127.0.0.1:8317/v1/models | head -c 200 # 自检:应返回 20 个模型
然后改 ~/.hermes/config.yaml:
model:
provider: trae
base_url: "http://127.0.0.1:8317/v1"
api_key: "trae-local" # 端点仅监听 127.0.0.1 且无鉴权,非空即可
api_mode: chat_completions
default: glm-5.3
providers:
trae:
base_url: "http://127.0.0.1:8317/v1"
api_key: "trae-local"
default_model: "glm-5.3"
实测:Hermes 把它解析为自定义端点(api_mode: chat_completions),20 个模型全部可见,流式 / 非流式 / function calling 均可用。
两个前提:
- 凭据只有一份。 消费方(Hermes)只是 HTTP 客户端,不持有 Trae 凭据;登录与刷新全部由本插件负责。DSH 里
/trae-login一次,两边同时可用。 - 端点要常驻。 想脱离 DSH,就用上面的独立进程,并配 LaunchAgent(见本节末尾)。
报错对照
| 现象 | 含义 | 处理 |
|---|---|---|
HTTP 429: Insufficient balance or no resource package,且 provider: zai、base_url: https://api.z.ai/... | 请求根本没走 Trae,打到了 Z.AI 官方接口(GLM_API_KEY 余额不足) | 确认 model.provider 是 trae;已有会话会保留自己的 provider/model,需要新开一个对话,或在输入框的模型选择器里选 trae 分组下的模型 |
ECONNREFUSED 127.0.0.1:<port>,Hermes 日志出现 probe-down | base_url 的端口没有进程监听 | 按上表重新确认端口;DSH 重启后必须换成新端口,或改用固定端口的独立进程 |
模型列表里只有 zai 的模型、看不到 trae | providers.trae 没被读入,或 base_url 连不上导致模型发现失败 | hermes config get providers.trae;先 curl 自检端点 |
模型名会撞车。 Trae 和 Z.AI 都有
glm-5.2/glm-5.3。只写模型名时,Hermes 可能把glm-*解析回 Z.AI 的zaiprovider,于是又变成余额报错。选模型时要选 trae 分组下的那个,而不是zai分组下的同名项。
两个容易漏的 Hermes 细节
1. 桌面端输入框的模型选择是「粘性」的,会盖掉配置文件。 Hermes 把这个选择存在 renderer 的 localStorage(hermes.desktop.composer.model / hermes.desktop.composer.provider),每次新对话通过 session.create 的 client override 发上来,优先级高于 model: 段。所以只改 config.yaml 不会改变已经在用 zai 的那个输入框 —— 日志会写得很清楚:
session.create 20261019_195633_28685e: model=glm-5.3 provider=zai source=client override (profile default: glm-5.3)
处理:在输入框的模型选择器里点一次 trae 分组下的模型即可(选择器里 trae 那一组的 is_current 为 true,api_url 就是本文档的 base_url)。
2. fallback_providers 必须是 YAML 列表,不能是 JSON 字符串。 写成
fallback_providers: '[{"provider": "trae", "model": "glm-5.3", "base_url": "http://127.0.0.1:8317/v1"}]'
会被静默忽略(hermes_cli/fallback_config.py 的 _iter_fallback_entries 只认 dict/list),日志表现为 no fallback_chain / fallback_providers is configured。正确写法:
fallback_providers:
- provider: trae
model: glm-5.3
base_url: http://127.0.0.1:8317/v1
api_key: trae-local
- provider: deepseek
model: deepseek-v4-flash
base_url: https://api.deepseek.com/v1
billing 类错误(余额不足的 429)在分类器里带 should_fallback=True,所以配好这条链之后,即使某个对话还钉在 zai 上,也会在真正发请求前切到 trae。
常驻(macOS LaunchAgent)
~/Library/LaunchAgents/local.trae-cn-provider.plist(node 换成 command -v node 的绝对路径):
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key><string>local.trae-cn-provider</string>
<key>ProgramArguments</key>
<array>
<string>/opt/homebrew/bin/node</string>
<string>/path/to/trae-cn-provider/src/cli.js</string>
<string>serve</string>
<string>--port</string>
<string>8317</string>
</array>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
<key>StandardOutPath</key><string>/tmp/trae-cn-provider.log</string>
<key>StandardErrorPath</key><string>/tmp/trae-cn-provider.err.log</string>
</dict>
</plist>
launchctl load -w ~/Library/LaunchAgents/local.trae-cn-provider.plist
launchctl print gui/$(id -u)/local.trae-cn-provider | head -20 # 确认 state = running
顺带说明:别指望直接读 Trae 客户端的登录态(
credentialSource: local)。新版 Trae CN 已把storage.json里的键名改为动态拼接的iCubeAuthInfo://icube-dc:<authProviderId>,且该键解出来只有设备密钥对、没有 token —— 登录态迁到了state.vscdb,那里也不存 token。local因此在新版 CN 上读不到东西,属正常现象,用默认的oauth。
本仓库当前的完成度(自查用)
工程部分全部完成并已在真机验证:
| 目标项 | 状态 |
|---|---|
macOS 本机凭据提取(tc 加密 + 明文,4 个 edition) | ✅ 实测解出并调通上游 |
Trae 上游协议适配(llm_utils_chat SSE) | ✅ 正文 / 思考 / 工具调用 / usage / 图片全部实测 |
OpenAI 兼容 /v1/models + /v1/chat/completions | ✅ 流式 + 非流式 + 工具 + 图片实测 |
| 打包成 DSH bundle 插件 | ✅ 从 tarball 安装并启动验证 |
| 在本机验证可用 | ✅ 在 DSH 0.1.2-rc.1 与 0.2.0-rc.2 上分别验证 provider 注册与调用 |
prepareCall().stream() 适配 0.2.0-rc.2 的同步解引用 | ✅ streamPrepared async generator + 回归测试(0.1.3) |
| 完整模型目录(20 个而非 7 个) | ✅ 三处版本码统一到 20260901 + DeviceName 对齐真客户端(0.1.4) |
| 走通发布流程 | ✅ 0.1.0–0.1.5 已发布到 npm;dsh.so 收录见上节 |
因此剩余步骤只有两个,且都必须由账号持有者本人操作:
npm login后执行npm publish --access public- 装进日常使用的 profile:先完全退出 DeepSeek Harness 桌面应用,再执行
dsh plugin --profile desktop add trae-cn-provider
更新日志
| 版本 | 变更 |
|---|---|
| 0.1.6 | 安全加固,来自 dsh.so 动态审计的两条发现:① /v1 默认不再下发 Access-Control-Allow-Origin: *,带 Origin 头的请求一律 403(新增 allowBrowserOrigins 白名单)—— 以前任意网页都能用你的 Trae 额度;② login-debug.log 落盘前对回调参数脱敏,userJwt / refreshToken / userInfo 等敏感值替换成 <redacted N> —— 以前令牌会被复制到凭据文件之外。两条都有回归测试锁住 |
| 0.1.5 | 文档为主:新增「让别的 agent 框架也用上(以 Hermes 为例)」的完整接法与排错 —— DSH 宿主端口每次启动都会变、桌面端输入框的模型选择是粘性的、fallback_providers 必须是 YAML 列表(写成 JSON 字符串会被静默忽略);补 LaunchAgent 常驻写法。运行时行为与 0.1.4 一致 |
| 0.1.4 | 模型目录 7 → 20:三处版本码统一到 20260901,DeviceName 对齐真客户端 |
| 0.1.3 | prepareCall().stream() 改为同步返回 AsyncIterable(修 0.2.0-rc.2 上每次调用即失败) |
| 0.1.2 | /trae-login 的返回值改成宿主 CommandResult 接受的形状(以前登录成功却在返回时抛错) |
| 0.1.1 | 租户主机上刷新失败时回退到 cloudide 交换端点 |
| 0.1.0 | 首个发布版 |
许可
MIT,见 LICENSE。
再次提醒:使用本项目违反 Trae 使用条款,且可能导致账号被封。请自行判断。
Comments
Loading…