dsh-llm-adapters 0.0.0-stage → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DEVELOPMENT.md +475 -0
- package/FAQ.md +292 -0
- package/LICENSE +21 -0
- package/README.md +173 -3
- package/client.js +1822 -0
- package/cordis.patch.yml +6 -0
- package/lib/index.js +8968 -0
- package/lib/types/account-profile.d.ts +18 -0
- package/lib/types/account-store.d.ts +135 -0
- package/lib/types/adapter.d.ts +142 -0
- package/lib/types/api.d.ts +42 -0
- package/lib/types/auth.d.ts +126 -0
- package/lib/types/brand.d.ts +36 -0
- package/lib/types/catalog.d.ts +141 -0
- package/lib/types/codearts-endpoints.d.ts +108 -0
- package/lib/types/codearts-login.d.ts +62 -0
- package/lib/types/codearts-oauth.d.ts +88 -0
- package/lib/types/codearts-sign.d.ts +30 -0
- package/lib/types/codearts-window.d.ts +65 -0
- package/lib/types/codearts.d.ts +208 -0
- package/lib/types/config.d.ts +165 -0
- package/lib/types/http.d.ts +14 -0
- package/lib/types/index.d.ts +158 -0
- package/lib/types/live.d.ts +12 -0
- package/lib/types/login-page.d.ts +66 -0
- package/lib/types/loomy-login.d.ts +89 -0
- package/lib/types/loomy.d.ts +72 -0
- package/lib/types/model-name.d.ts +35 -0
- package/lib/types/openrouter.d.ts +106 -0
- package/lib/types/qr.d.ts +60 -0
- package/lib/types/raccoon-login.d.ts +77 -0
- package/lib/types/raccoon.d.ts +87 -0
- package/lib/types/trae-client.d.ts +52 -0
- package/lib/types/trae-endpoints.d.ts +56 -0
- package/lib/types/trae-login.d.ts +108 -0
- package/lib/types/trae.d.ts +234 -0
- package/lib/types/types.d.ts +84 -0
- package/lib/types/wire.d.ts +101 -0
- package/package.json +105 -6
- package/src/account-profile.ts +49 -0
- package/src/account-store.ts +343 -0
- package/src/adapter.ts +383 -0
- package/src/api.ts +297 -0
- package/src/auth.ts +582 -0
- package/src/brand.ts +70 -0
- package/src/catalog.ts +428 -0
- package/src/codearts-endpoints.ts +139 -0
- package/src/codearts-login.ts +289 -0
- package/src/codearts-oauth.ts +304 -0
- package/src/codearts-sign.ts +95 -0
- package/src/codearts-window.ts +136 -0
- package/src/codearts.ts +1161 -0
- package/src/config.ts +264 -0
- package/src/http.ts +66 -0
- package/src/index.ts +425 -0
- package/src/live.ts +20 -0
- package/src/login-page.ts +559 -0
- package/src/loomy-login.ts +451 -0
- package/src/loomy.ts +317 -0
- package/src/model-name.ts +90 -0
- package/src/openrouter.ts +392 -0
- package/src/qr.ts +563 -0
- package/src/raccoon-login.ts +383 -0
- package/src/raccoon.ts +485 -0
- package/src/trae-client.ts +312 -0
- package/src/trae-endpoints.ts +91 -0
- package/src/trae-login.ts +784 -0
- package/src/trae.ts +1457 -0
- package/src/types.ts +91 -0
- package/src/wire.ts +564 -0
package/DEVELOPMENT.md
ADDED
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
# 开发文档
|
|
2
|
+
|
|
3
|
+
实现要点与改这个仓库前必须知道的约束。返回 [README](./README.md)。
|
|
4
|
+
|
|
5
|
+
## 目录结构
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
src/
|
|
9
|
+
index.ts 注册 loomy / raccoon / trae / codearts 路由、设置接口与配置目录
|
|
10
|
+
adapter.ts 把 DSH 请求接到 provider client,模型解析与上下文窗口
|
|
11
|
+
api.ts /models、/status、/account/browser、/account
|
|
12
|
+
auth.ts 插件凭据状态、JWT 到期判定、退出登录
|
|
13
|
+
account-store.ts 插件自己的凭据文件(原子写、0600、串行读改写)
|
|
14
|
+
login-page.ts 本地登录页:随机端口/路径、二维码下发、状态轮询、可选绑手机
|
|
15
|
+
qr.ts 零依赖二维码(byte 模式、纠错 M、版本 1–10)
|
|
16
|
+
loomy-login.ts Loomy 微信扫码:uuid、长轮询、讯飞账号 HMAC 接口
|
|
17
|
+
raccoon-login.ts 小浣熊扫码:服务端签发二维码、轮询、业务会话确认
|
|
18
|
+
trae-login.ts Trae 网页登录:独立浏览器配置、CDP 读 cookie 与设备指纹
|
|
19
|
+
trae-client.ts Trae 桌面客户端登录态:只读 storage.json、本机解密、设备指纹
|
|
20
|
+
trae-endpoints.ts 两个区域的站点 / 网关 / 计费基址
|
|
21
|
+
account-profile.ts 小浣熊账号展示字段
|
|
22
|
+
loomy.ts Loomy 目录 / 积分 / 流式调用
|
|
23
|
+
raccoon.ts 小浣熊目录 / 余额 / 流式调用
|
|
24
|
+
trae.ts Trae 目录合并 / SOLO 流式调用 / 额度 / 客户端签到
|
|
25
|
+
codearts-login.ts CodeArts 浏览器授权:本地回调、PKCE/DPoP、ticket 回退
|
|
26
|
+
codearts-oauth.ts STS 令牌换取与续期(PKCE + DPoP,零依赖 JWS)
|
|
27
|
+
codearts-sign.ts 华为 SDK-HMAC-SHA256 请求签名
|
|
28
|
+
codearts-endpoints.ts 网关基址、窗口静态表、benefit 与 VL 清单
|
|
29
|
+
codearts.ts CodeArts 双目录合并 / 签名流式调用 / 额度 / 签到
|
|
30
|
+
codearts-window.ts CodeArts 会话预算:每 60 秒 3 次的排队窗口
|
|
31
|
+
catalog.ts 目录解析、模型缓存、能力来源合并
|
|
32
|
+
model-name.ts 公共名称规则:括号标签、倍率、连字符归一化、匹配 key
|
|
33
|
+
openrouter.ts OpenRouter 模型表:原子同步、磁盘缓存、通配匹配档位与模态
|
|
34
|
+
wire.ts OpenAI 兼容 SSE、工具调用、usage
|
|
35
|
+
config.ts volatile 配置与 live 读取
|
|
36
|
+
client.js 设置页(ModuleLoader bundle,无构建步骤)
|
|
37
|
+
test/ vitest,不访问真实上游
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## 扫码登录
|
|
41
|
+
|
|
42
|
+
两条链路都**没有回调地址**,也不读取日常浏览器的 Cookie。宿主在 `127.0.0.1` 起一个临时
|
|
43
|
+
登录页,页面只显示二维码与状态;凭据由宿主侧轮询拿到,不经过页面脚本。
|
|
44
|
+
|
|
45
|
+
### 本地登录页(`login-page.ts`)
|
|
46
|
+
|
|
47
|
+
`startLoginPage()` 在 `127.0.0.1:0` 上监听,路径带 32 位随机前缀,**先返回 URL**,二维码在
|
|
48
|
+
后台准备(Loomy 需要先拉微信授权页与图片)。返回后:
|
|
49
|
+
|
|
50
|
+
- `GET /<nonce>/login` 返回单文件页面;二维码可能在准备中,页面每 1.2 秒轮询状态。
|
|
51
|
+
- `GET /<nonce>/status` 触发一次 provider 轮询。同一时刻只允许一个轮询在飞;Loomy 的长轮询
|
|
52
|
+
可能占用 40 秒,第二个请求直接返回当前状态。二维码只下发一次,之后页面自己保留。
|
|
53
|
+
- 可选绑手机:`POST /<nonce>/send`、`POST /<nonce>/verify`。手机号与验证码只在宿主内存里,
|
|
54
|
+
页面拿不到 `rcode` / `msgid` / `session`。
|
|
55
|
+
- 5 分钟超时;成功后页面保持 60 秒再关服务器,用户可以读到「登录成功」。
|
|
56
|
+
- `close()` 关闭监听并 `closeAllConnections()`,不会因为一个挂起的长轮询卡住取消。
|
|
57
|
+
|
|
58
|
+
`POST /account` 的 `browser` 动作只负责启动页面并返回 URL;设置页在点击手势里
|
|
59
|
+
`window.open(url)`,被拦截时卡片上的「重新打开登录页」仍可用。设置页轮询
|
|
60
|
+
`GET /account/browser?brand=...`,收到 `running:false, signedIn:true` 后重读 `/status`。
|
|
61
|
+
|
|
62
|
+
### Loomy(`loomy-login.ts`)
|
|
63
|
+
|
|
64
|
+
1. 生成 `state`,请求 `open.weixin.qq.com/connect/qrconnect`(`appid` / 官方
|
|
65
|
+
`redirect_uri` / `scope=snsapi_login`),从 HTML 里提取二维码 `uuid`。
|
|
66
|
+
2. 下载 `connect/qrcode/<uuid>`(JPEG)并内联成 data URL。
|
|
67
|
+
3. 长轮询 `long.open.weixin.qq.com/connect/l/qrconnect`:`408` 待扫码、`404` 已扫码待确认、
|
|
68
|
+
`405` 已确认(这一帧带 `wx_code`)、`403` 取消、`402` 失效。
|
|
69
|
+
4. `wx_code` → `/login/thirdAccount/bind/auth` 换 `rcode` 与 `bind`。`bind=1` 走
|
|
70
|
+
`/bind/skip` 直接换 `session`;`bind=0` 显示绑手机表单,走 `/bind/sendMsg` +
|
|
71
|
+
`/bind/checkCode`。
|
|
72
|
+
5. 讯飞账号接口用 HMAC-SHA1 的 9 段签名,认证头是 `account {ak}:{sig}`。请求体**只序列化
|
|
73
|
+
一次**,签名与发送共用同一字符串;`base.ua` 固定为 `Loomy|Desktop|Electron|macOS`。
|
|
74
|
+
|
|
75
|
+
`redirect_uri` 指向的官方回调页本身是 404,但链路不加载它:`code` 来自长轮询。不要把它改成
|
|
76
|
+
`127.0.0.1`,微信会校验域名白名单。
|
|
77
|
+
|
|
78
|
+
### 小浣熊(`raccoon-login.ts`)
|
|
79
|
+
|
|
80
|
+
1. `POST /api/iam/v1/auth/mp/qrcode`(空对象)取服务端签发的二维码:
|
|
81
|
+
`qrcode_code`(形如 `qr-…`)、`scan_path`、`expires_in`、`poll_interval`。
|
|
82
|
+
2. 二维码内容由 `scan_path` 与 `https://xiaohuanxiong.com` 拼成,并附加 `code`、
|
|
83
|
+
`appname=商汤小浣熊`、`source=platform` —— 与官网登录页实际生成的值一致。
|
|
84
|
+
3. 按 `poll_interval`(通常 2 秒)`POST /api/iam/v1/auth/mp/qrcode/login`(`qrcode_code`);
|
|
85
|
+
`pending` / `logging` / `canceled` / `success`。`AUTH_QRCODE_EXPIRED` 视为过期,网络抖动仍
|
|
86
|
+
继续轮询。
|
|
87
|
+
4. 成功后先用 `POST /api/web/auth/v1/iam/confirm` 建立业务会话(失败不致命),再向
|
|
88
|
+
`model_catalog` 校验令牌;只有网关接受的令牌才写入账号存储。扫码登录没有刷新流程,到期后
|
|
89
|
+
重新扫码。
|
|
90
|
+
|
|
91
|
+
⚠️ 不能像旧版那样在本地随机生成 `code`:轮询会返回 `pending`,但微信扫到的落地页会把该码判为
|
|
92
|
+
“过期或已被使用”。码必须来自第 1 步。
|
|
93
|
+
|
|
94
|
+
不走官方桌面端的 `office-raccoon://auth/callback`,也不改网页里写死的回调地址。
|
|
95
|
+
|
|
96
|
+
### Trae(`trae-login.ts` + `trae.ts`)
|
|
97
|
+
|
|
98
|
+
Trae 没有可以自建的二维码链路:设备指纹必须来自一次真实的网页登录。因此这里不用本地登录页,
|
|
99
|
+
而是**自己启动一个浏览器**:
|
|
100
|
+
|
|
101
|
+
1. 找一个本机 Chromium 系浏览器(`DSH_TRAE_BROWSER` 可指定),用
|
|
102
|
+
`--remote-debugging-port=0 --user-data-dir=<插件数据目录>/trae-browser/<region>` 启动,
|
|
103
|
+
打开 `www.trae.cn` 或 `www.trae.ai`。配置目录与用户自己的浏览器隔离,但**跨登录保留**,
|
|
104
|
+
让同一区域始终复用同一设备指纹;启动参数还关掉了 `AutomationControlled` 特征。
|
|
105
|
+
2. 读 `DevToolsActivePort` 得到调试端口,`/json/list` 找到页面,用 WebSocket 连 CDP。
|
|
106
|
+
被强杀的浏览器会把端口文件留在配置目录里:启动前先探测该端口,能应答就复用那个窗口
|
|
107
|
+
(只新开一个标签页,结束时也只关标签页),不能应答则删掉文件再启动,且只接受本次启动
|
|
108
|
+
写入的文件,避免连到死端口后把刚打开的窗口当成失败关掉。
|
|
109
|
+
3. 每 2 秒 `Network.getAllCookies` 拼 `Cookie` 头,回放给 `CheckLogin`;登录后
|
|
110
|
+
`common/GetUserToken` 换 `Cloud-IDE-JWT`,`trae/GetUserInfo` 取账号信息。
|
|
111
|
+
4. 设备指纹取自该会话的 `localStorage.__msuuid__`(没有时 `msid`,再没有就生成一个并留在
|
|
112
|
+
页面里);`machineId = sha256("trae-web-machine\0" + deviceId)`。这两项、JWT、网页
|
|
113
|
+
cookie 与网页会话到期时间一起写入账号记录。JWT 约 8 小时,网页会话约 14 天。
|
|
114
|
+
5. 10 分钟超时;成功后关闭窗口但保留配置目录。状态检查连续失败不会立刻结束流程(只有 CDP
|
|
115
|
+
断线或浏览器真的退出才结束),避免登录重定向中的网络抖动关掉用户正在操作的窗口。取消、
|
|
116
|
+
失败与插件卸载同样走清理路径(Windows 上用 `taskkill /T /F` 杀掉进程树);复用的窗口
|
|
117
|
+
只关标签页。退出登录会删除该区域的配置目录,避免下次登录被浏览器 cookie 自动接回。
|
|
118
|
+
|
|
119
|
+
两条目录来源合并成一份模型表(`mergeTraeModels`):**只考虑客户端主通道**
|
|
120
|
+
(`TRAE_PRIMARY_FUNCTIONS` = `solo_work_remote` / `solo_work_lite`)里的远端行,并且每一行都
|
|
121
|
+
必须在 `get_detail_param` 里找到 wire config(chat 只接受 `config_name`)。因此
|
|
122
|
+
`Qwen3.8-Flash` / `Kimi-K2.8-Preview`(只在远端 `chat_v3`,任何 wire roster 都没有 config)
|
|
123
|
+
不会出现;只在 `chat_v3` / `solo_coder` 里仍可调用、但客户端已下架的老模型(GLM-4.6/4.7、
|
|
124
|
+
MiniMax-M2、Kimi-K2.0905、Qwen3-Coder、普通版 DeepSeek-V4-Flash/Pro)也不会出现。只在 wire
|
|
125
|
+
里出现、没有主通道目录行的配置不会被提升。远端显示名归一化后相同的两行
|
|
126
|
+
(`DeepSeek-V4-Flash` 与 `-Official`)只保留窗口更大的一行,不向名称里拼接 id 或括号。
|
|
127
|
+
Trae 与另外两个供应商共用 `resolveCapabilities`:OpenRouter 的实时表优先(`match` 用同一套
|
|
128
|
+
名称归一化),匹配不到回退目录,两边都没有就开放全部档位与模态。因此 OpenRouter 命中时
|
|
129
|
+
DeepSeek V4.1 Flash 是 None/Low/High/Max,未命中时才是目录的 light/high/extra_high。
|
|
130
|
+
|
|
131
|
+
聊天走 `POST {chatBase}/api/agent/v3/llm_utils_chat`,需要 `x-device-id` / `x-machine-id` /
|
|
132
|
+
`x-uid` 与 **`x-app-version-code` / `x-ide-version-code`** 等网页头。后两个是数字,缺失时上游
|
|
133
|
+
返回 `4001 binding: expr_path=app_version_code, cause=missing required parameter`(聊天)或
|
|
134
|
+
`config_info_list: []`(`get_detail_param`,静默少一半目录);网页登录拿不到桌面构建号,因此用
|
|
135
|
+
固定的数字 `TRAE_VERSION_CODE`,只规范格式、不冒充设备。消息 content 一律是 block 数组,
|
|
136
|
+
assistant 的工具调用放在 `function_call`,工具的 `parameters` 是 JSON **字符串**。上游错误帧
|
|
137
|
+
按码映射:`1005` / `4222` → 402,`4008` / `4011` → 429,`4001` / `4023` → 400,其余 502。
|
|
138
|
+
|
|
139
|
+
目录里的 `features` 是 **JSON 字符串**(不是对象),倍率在
|
|
140
|
+
`features.consumption_rate.data.rate`;不解析字符串时所有模型都会丢掉倍率。同一模型会出现在
|
|
141
|
+
多个分组,容量取最强行(Max 优先、其次更宽的 dev 窗口),但**调用函数以 wire 目录为准**:
|
|
142
|
+
远端分组可能把模型标到一个实际会回 `4001` 的函数上(实测 `solo_agent_remote` 拒绝
|
|
143
|
+
`deepseek-v4.1-flash`,`solo_work_remote` 正常)。
|
|
144
|
+
|
|
145
|
+
思考档位走公共规则,不按 Trae 单独裁剪。实测(同一提示、每档 3 次)`reasoning_tokens` 在
|
|
146
|
+
`none/low/high/xhigh/extra_high/max` 之间随机波动(38–109),非法值 `banana` / `999` / `MAX`
|
|
147
|
+
同样返回 200,说明 SOLO 通道**不校验** `reasoning_effort`——也正因如此,发 OpenRouter 的档位
|
|
148
|
+
不会触发上游错误。宿主按 `resolveModel().reasoning.efforts` 做请求前校验;只有当 OpenRouter
|
|
149
|
+
快照为空、列表回退到目录三档时,旧会话里的 `max` 才需要重选。
|
|
150
|
+
|
|
151
|
+
上下文窗口同时保留上游的 dev 与 Max 两档:模型行默认用 dev,下拉里额外提供它声明的
|
|
152
|
+
`maxContextWindow`(例如 1M),不再只显示 200K。
|
|
153
|
+
|
|
154
|
+
宽窗模型还多一个 500K 中间档(与 `dsh-connect-trae` 一致):只有当 Max ≥ 500K、且 500K 确实
|
|
155
|
+
介于默认窗口与 Max 之间时才出现,落盘值不是该模型可选窗口时会被忽略,不会把模型撑到它没有
|
|
156
|
+
的窗口。
|
|
157
|
+
|
|
158
|
+
工具调用的流有三条必须遵守的细节,都是实测出来的:
|
|
159
|
+
|
|
160
|
+
1. **id 只在首帧出现。** 参数分片的 `tool_calls[]` 会重复 `index`,但 `id` 与 `type` 是空串。
|
|
161
|
+
把空串当成值写入会把真实 id 覆盖掉,宿主随后就会报
|
|
162
|
+
`tool/result ... requires toolCallId matching its tool source`。因此空 id / 空 name 一律忽略,
|
|
163
|
+
并在发出前再兜一次:缺 id 的调用生成新 id,重复 id 重新生成,保证每次调用一个唯一非空 id。
|
|
164
|
+
2. **工具结果必须是 block 数组。** `content` 传字符串会被上游拒绝:`cannot unmarshal string into
|
|
165
|
+
... messages.content of type []*idecopilot.LLMRawMessageContent`;空数组则报
|
|
166
|
+
`param is invalid`。工具没有文本时补一个空 text block。
|
|
167
|
+
3. **一次响应可以带多个工具调用**,`index` 0/1/… 各自独立;`finish_reason` 仍是 `stop`,
|
|
168
|
+
所以有工具调用时按 `tool-calls` 收尾,否则宿主不会执行它们。
|
|
169
|
+
|
|
170
|
+
`max_tokens` 会被透传(上游接受);图片走 `image_url` 的 data URL,实测被网关正常识别。
|
|
171
|
+
高级设置里的 `traeVisionModelIds` / `traeTextOnlyModelIds` 在目录合并后生效,`textOnly` 优先。
|
|
172
|
+
|
|
173
|
+
模型 id 与显示名都必须唯一:id 按小写去重(`custom_model_gemini` 这种跨分组重复);两条
|
|
174
|
+
不同 id 归一到同一显示名时(`DeepSeek-V4-Flash` 与 `DeepSeek-V4-Flash-Official`)只保留窗口
|
|
175
|
+
更宽的一行,不合成 `(id)` 后缀——共享命名规则只提取上游括号内容,不新增括号。
|
|
176
|
+
|
|
177
|
+
`Qwen3.8-Flash` / `Kimi-K2.8-Preview` 是客户端 TraeCode 通道的模型:5 个聊天函数和 12 个目录
|
|
178
|
+
函数的 `get_detail_param` 都不返回它们的 config,网页令牌调 `/api/ide/v2/llm_raw_chat` 也会对
|
|
179
|
+
包括普通模型在内的所有 config 回 `4001`,所以网页登录无法调用;插件把它们隐藏,而不是提供
|
|
180
|
+
一个必然失败的入口。若将来要支持,需要另一套桌面端 TraeCode 凭据,不能复用本插件的网页令牌。
|
|
181
|
+
|
|
182
|
+
模型 id 必须唯一:宿主对重复元数据直接拒绝整条 provider(“invalid or duplicate model
|
|
183
|
+
metadata”)。远端目录在多个分组里用不同大小写/显示名重复列出同一 config,所以
|
|
184
|
+
`parseTraeRemoteModels` 按小写 id 合并,`mergeTraeModels` 再用 `usedIds` 拦住只在 wire 里
|
|
185
|
+
重复出现的 config,`LocalAdapter.listModels` 最后按 id 去重兜底——这一层对所有供应商生效。
|
|
186
|
+
|
|
187
|
+
额度按区域走不同接口:国内版 `web_user_ent_usage` v2(已用 / 总额 + 资源包,按最早到期排序),
|
|
188
|
+
国际版 `web_user_ent_usage` v1(套餐、`basic_usage_limit` / `basic_usage_amount` 美元用量与
|
|
189
|
+
`end_time` 重置时间)。国际版的 `ide_user_pay_status` 只有套餐名、没有任何金额,官网的
|
|
190
|
+
“Dollar Usage”面板读的也是 v1 这份 roster。两条路线的选择由区域档案的 `usageV2` 决定。
|
|
191
|
+
|
|
192
|
+
### Trae 客户端登录与签到(`trae-client.ts`)
|
|
193
|
+
|
|
194
|
+
Trae 的每日签到只在“复用本机客户端登录态”时开放。网页登录派生的设备号在 `claim` 上会被上游
|
|
195
|
+
回 `9074`,因此网页登录不显示签到入口。`trae-client.ts` 只读客户端
|
|
196
|
+
`User/globalStorage/storage.json` 里的 `iCubeAuthInfo://icube.cloudide`:密文头部是 6 字节
|
|
197
|
+
magic + 32 字节密钥材料,AES-128-CBC 的 key/IV 由两级 SHA-512 与固定 salt 派生,明文是
|
|
198
|
+
SHA-512 摘要 + JSON。插件从解密结果取 `Cloud-IDE-JWT`,从
|
|
199
|
+
`iCubeAuthInfo://icube-dc:<16 位设备号>` 取设备号,从 `telemetry.machineId` 取机器码;
|
|
200
|
+
只读、不回写,客户端退出或令牌过期后重新复用。`checkinStatus` / `claimCheckin` 只在该来源下
|
|
201
|
+
调用 `/trae/api/v2/ug/checkin_credits/{status,claim}`,body 为 `{ req_source: 1 }`,并直接
|
|
202
|
+
使用客户端设备号(不做派生)。数据目录按区域默认取 `%APPDATA%\Trae CN` / `%APPDATA%\Trae`
|
|
203
|
+
(macOS 为 `Application Support`,Linux 为 XDG 配置目录);`DSH_TRAE_CLIENT_DIR`、
|
|
204
|
+
`DSH_TRAE_CN_CLIENT_DIR` 与 `DSH_TRAE_GLOBAL_CLIENT_DIR` 可覆盖它,测试与便携安装都走这组
|
|
205
|
+
环境变量。
|
|
206
|
+
|
|
207
|
+
### 小浣熊的每日积分(`raccoon.ts`)
|
|
208
|
+
|
|
209
|
+
没有独立的签到端点:积分由服务端在当天首次网页访问时发放,`setting_info` 的
|
|
210
|
+
`point_grant_popups` 只是通知,`/points/v2/bills` 里 `biz_type=daily_grant` 的记录才是可核对的
|
|
211
|
+
凭据。插件只把 `grant_at` 属于今天(北京时间)的记录算作今日签到,并把 `created_at` 作为到账时间
|
|
212
|
+
显示;`ensureDailyCheckin()` 先读流水,未到账才以 web 身份调用 `setting_info`。宿主按
|
|
213
|
+
`raccoonCheckinMinutes`(0 关闭)定时跑这一遍,卡片上的网页入口始终保留。
|
|
214
|
+
|
|
215
|
+
### CodeArts(`codearts-login.ts` + `codearts.ts`)
|
|
216
|
+
|
|
217
|
+
CodeArts 是唯一不用 Bearer 令牌的供应商:华为 API 网关校验 `SDK-HMAC-SHA256`,所以每次请求都要
|
|
218
|
+
用 AK/SK + STS 安全令牌对**确切的请求体**签名。
|
|
219
|
+
|
|
220
|
+
登录分两段。宿主在 `127.0.0.1` 起一个 ≥10000 端口的回调服务器(portal 拒绝更低的端口),把
|
|
221
|
+
`portal/authorize` 地址交给设置页打开;华为回调带 `code` 时用 PKCE(S256)+ DPoP(ES256)换 STS
|
|
222
|
+
凭据,带 `secret` 时退回旧的 ticket 轮询(只有 AK/SK,没有续期令牌)。两条都算成功——把旧路径
|
|
223
|
+
判成失败会让用户永远停在「等待授权」。PKCE verifier 与 DPoP 私钥随凭据保存:续期必须复用同一对,
|
|
224
|
+
换一对会被 STS 拒绝。续期令牌的 `exp` 固定为登录 +30 天且续期不延长,所以判据只看
|
|
225
|
+
`invalid_grant` / `ExpiredRefreshToken`;`InvalidDPoPHeader` 属于单次请求失败(时钟偏差、重放窗口),
|
|
226
|
+
把它当终态会把材料完好的账号一步标死。
|
|
227
|
+
|
|
228
|
+
签名有三处细节,任一错都只回 `APIG.0301 verify ak sk signature fail`:路径必须以 `/` 结尾;`host`
|
|
229
|
+
参与签名但不发送(由 fetch 生成);`Agent-Type` / `X-Language` 必须在**签名之后**追加(进 canonical
|
|
230
|
+
会验签失败)。`maas_type: benefit` 相反——它**必须**参与签名,缺失时免费额度模型回
|
|
231
|
+
`InferHub.002002009.404`。
|
|
232
|
+
|
|
233
|
+
目录来自两个端点:`opengw gateway/config` 给 benefit 模型(`result.models`),
|
|
234
|
+
`snap-access/v1/model/builtin` 给常规模型(`builtinModels`)。两处都**不下发上下文窗口**,窗口取自
|
|
235
|
+
IDE 自身 provider 配置的 `limit.context`(`codearts-endpoints.ts` 的静态表,来源与日期写在注释
|
|
236
|
+
里);表里没有的模型落到 `CODEARTS_FALLBACK_CONTEXT_WINDOW`(当前目录最小的真实窗口)——偏小只会
|
|
237
|
+
提前压缩,偏大可能让上游拒绝一轮对话。
|
|
238
|
+
|
|
239
|
+
图片能力分两类:`kimi-k2.6-vl` 与 `Qwen3-VL-235B`(`builtinModels` 里 `supports_images: true`,
|
|
240
|
+
类别「图片理解」,上游 `display_enabled: false` 但 `enable: true`)能**直接收图**,实测还能照常发
|
|
241
|
+
工具调用;其余模型带图一律回 `InferHub.001001020.406`——包括 `deepseek-v4.1-flash`,尽管 DeepSeek
|
|
242
|
+
官方 API 接受图片(data URL、外部 https、`image_url` 字符串、Anthropic `image/source`、
|
|
243
|
+
`input_image`、1017 变体共 8 种写法实测全被拒,同一张图给 VL 模型立即返回)。所以目录里**每一行都
|
|
244
|
+
标 `vision`**——那是适配器「可以接收图片」的判据,文本模型的图片随后由转述处理。设置页只看
|
|
245
|
+
`visionRelay`:为真的一行不显示「读图」标签,改在列表下方用一句提示说明转述与缓存,这样标签只表达
|
|
246
|
+
模型的原生能力。窗口表里 VL 模型没有 `limit.context`,按模型卡各取 256K;网关名册没有
|
|
247
|
+
`supports_images` 字段,那部分仍靠 `CODEARTS_VISION_MODELS` 兜底。
|
|
248
|
+
|
|
249
|
+
### 图片转述(`relayImages` / `describeImage`)
|
|
250
|
+
|
|
251
|
+
`buildMessages` 先把附件读成 data URL,之后按目标模型分流:
|
|
252
|
+
|
|
253
|
+
1. 目标是 VL 模型 → 原样发送,不做中转;
|
|
254
|
+
2. 目标是文本模型且消息里有图片 → `relayImages` 逐张取图,先用 `CODEARTS_VISION_MODEL`
|
|
255
|
+
(`kimi-k2.6-vl`,IDE 内核日志恒定选它)识图,把该 part 换成
|
|
256
|
+
`[图片内容(由 kimi-k2.6-vl 识别)]…` 的文本;
|
|
257
|
+
3. 转述失败(网关拒绝、空回答)→ 换成 `[图片识别失败:原因]`,主请求照常发,不让失败伪装成图片内容;
|
|
258
|
+
4. 转述结果按 `sha256(data URL)` 缓存到 `$DSH_HOME/llm-adapters/codearts-image-cache.json`(最多
|
|
259
|
+
64 条,`tmp → rename` 原子写)。同一张图在后续轮次、甚至重启后都直接复用,不再占会话额度。
|
|
260
|
+
|
|
261
|
+
每张**新**图是一次独立的 chat 请求,和主请求共用 `CodeArtsSessionWindow` 的 3 次/分钟额度,所以
|
|
262
|
+
`describeImage` 也排队、也会在 `TM.00001041` 时退名额重试。`hasImageParts` 先做一次纯内存检查,
|
|
263
|
+
没有图片的请求完全不碰缓存文件。
|
|
264
|
+
|
|
265
|
+
思考只有两档:`off` / `on`。网关切的是**顶层** `thinking: { type: 'disabled' }`,不是
|
|
266
|
+
`reasoning_effort`;`on` 与不传等价,所以只有关闭时才发该字段。`reasoning_effort` 不是「更细的
|
|
267
|
+
档位」:网关会**按模型**校验它(`deepseek-v4.1-flash` 收 `none|low|medium|high|xhigh|max`,
|
|
268
|
+
`glm-5.3` 直接拒绝 `none`),但实测不影响思考长度——同一道证明题上 `deepseek-v4.1-flash` 的
|
|
269
|
+
`reasoning_tokens` 是 `none` 388 / `low` 480 / `max` 515 / 不传 595,`glm-5.3` 是 `max` 709 /
|
|
270
|
+
不传 670,噪声大于档位差;只有 `thinking: disabled` 能把它压到 0。`max_tokens` 钳到 65536(更大的值
|
|
271
|
+
是硬 4xx)。聊天带 `tool_stream: true`(超大工具参数分片下发,否则大文件写入会被网关截断)与
|
|
272
|
+
`prompt_cache_key`(否则 `cached_tokens` 恒为 0)。
|
|
273
|
+
|
|
274
|
+
额度来自 `snap-manager/v1/statistics/plugin`:`usageTotalPackageCredit` 是汇总行,**不能**再和三个
|
|
275
|
+
分类一起当资源包求和(会翻倍);只有基础包能拿到真实到期(套餐 `end_date`),赠送包与按需包如实
|
|
276
|
+
留空。汇总行同时给出 `package_credit_amount` / `package_credit_used` / `package_credit_remain`,
|
|
277
|
+
前两项是卡片进度条唯一的「总额 + 已用」来源(缺汇总行时由分类行相加补出)。签到是三步(账户类型 →
|
|
278
|
+
活动列表 → claim + confirm),只在积分计费账户上可用,`claimable=false` 且状态属已领取时才算「今天已领」。
|
|
279
|
+
|
|
280
|
+
思考档位是 `off|on` 加上「网关接受的档位」与 OpenRouter `supported_efforts` 的交集,顺序固定为
|
|
281
|
+
`off, on, low, medium, high, xhigh, max`。`off` 映射到 `thinking: disabled`
|
|
282
|
+
(唯一实测能把 `reasoning_tokens` 压到 0 的开关);`on` 是旧默认,两个字段都不发;其余档位原样写进
|
|
283
|
+
`reasoning_effort`。网关按模型校验取值(`glm-5.3` 不接受 `none`),所以 `none` 只作为 `off` 的
|
|
284
|
+
兼容别名,不作为独立档位暴露,`minimal` 这类编辑器专属档位同样被交集挡掉。匹配不到 OpenRouter 条目
|
|
285
|
+
(或快照还不存在)时保留全部档位——上游明明接受却不给控制,比多给一档更糟。实测
|
|
286
|
+
`deepseek-v4.1-flash` → `off/on/low/high/max`,`GLM-5.2` → `off/on/high/xhigh`,
|
|
287
|
+
`openpangu-2.0-pro` 全开。
|
|
288
|
+
|
|
289
|
+
账号名不在 STS 响应里(那里只有 AK/SK/安全令牌),而在**续期令牌**的 `user_profile` 声明中:再套
|
|
290
|
+
一层 base64 的 JSON,含 `account_name` / `principal_id` / `account_id`。`readUserProfile` 解它,
|
|
291
|
+
`CodeArtsAuth.status()` 在记录里没有名字时当场解一次(老账号无需重新登录),续期时再写回记录。
|
|
292
|
+
`?secret=` 票据登录拿不到续期令牌,那条路径没有名字。
|
|
293
|
+
|
|
294
|
+
`wire.ts` 的 SSE 消费被拆成 `consumeOpenAiStream`:CodeArts 要自己签名 fetch,但帧处理与
|
|
295
|
+
`openAiStream` 完全相同;`onFrame` 钩子用来抓「HTTP 200 + SSE 内嵌 `error_code`」的排队/额度错误。
|
|
296
|
+
|
|
297
|
+
### CodeArts 的会话预算(`codearts-window.ts`)
|
|
298
|
+
|
|
299
|
+
体验版按账号限制会话:**每滚动 60 秒 3 次**,全模型共用,与并发无关,换 `session-id` 不加额度。
|
|
300
|
+
实网连续 6 次串行请求的结果是 `OK OK OK` 后整批被 `TM.00001041` 拒绝,约 61 秒后一起恢复;被拒的
|
|
301
|
+
请求**不**延长锁定(第 4 次在 72.9s 被拒、93.0s 与 113.2s 再被拒,133.4s 就恢复了,而不是
|
|
302
|
+
113.2 + 60)。反过来说,`InferHub.4004.200`(缺 benefit 包)是**会**占额度的,所以那条路径靠
|
|
303
|
+
`noBenefit` 记住结论,而不是每次重猜。
|
|
304
|
+
|
|
305
|
+
`CodeArtsSessionWindow` 因此做两件事:发请求前排队拿名额(满了就等最老的滑出窗口,`MARGIN_MS`
|
|
306
|
+
留 1.5 秒吸收「本地开始 → 网关计数」之间的网络时延),以及在被 `TM.00001041` 明确拒绝时把名额
|
|
307
|
+
**退回**(`slot.release()`)——占着拒绝请求的名额,会把下一分钟拖成每 20 秒一次,而网关根本没记这
|
|
308
|
+
一笔。窗口是进程内单例:热重载重置计数只会让下一次请求撞上网关还记得的额度。
|
|
309
|
+
|
|
310
|
+
### 公共的名称与能力规则
|
|
311
|
+
|
|
312
|
+
这些规则在 `src/model-name.ts` 与 `src/openrouter.ts`,Loomy、Raccoon 以及以后新增的供应商
|
|
313
|
+
都走同一套,**不按模型 id 硬编码**。
|
|
314
|
+
|
|
315
|
+
1. **名称归一化**(`parseModelName`):提取 `()` / `()` 里的内容;纯倍率(`x0.8`、`12倍`)
|
|
316
|
+
作为 `priceFactor` 并从句子里删掉;其他括号内容变成 `labels`;数字之间的 `-` 变 `.`
|
|
317
|
+
(`GLM-5-3` → `GLM 5.3`),其余 `-` 变空格(`Kimi-K3` → `Kimi K3`)。
|
|
318
|
+
2. **OpenRouter 匹配**(`OpenRouterIndex.match`):`modelKey` 去掉空格、点、横线等,把
|
|
319
|
+
`sn-kimi-k3`、`Kimi K3`、`moonshotai/kimi-k3` 归一到同一 key;先查精确 key,再用双向
|
|
320
|
+
包含做通配匹配(上游少一个版本后缀、或多一个后缀都能命中),同分且有歧义时放弃匹配。
|
|
321
|
+
3. **能力来源**(`resolveCapabilities`):OpenRouter 有 `supported_efforts` /
|
|
322
|
+
`input_modalities` 就用它;否则回退上游的 `reasoning_efforts`、`input_modalities`、
|
|
323
|
+
`capabilities.vision` 或 `tags`;两边都没有时开放全部档位与 text/image 两种模态。
|
|
324
|
+
4. **默认档**取列表里**最强**的一档(强度顺序 `none < minimal < low < medium <
|
|
325
|
+
high < xhigh < max`),由 `defaultEffortOf` 统一计算:有 `max` 就用 `max`;两态列表
|
|
326
|
+
(`off` / `on`)没有强度排名,取最后一项 `on`。只有一个档位时就取它。
|
|
327
|
+
5. **档位名只做大写化**:`none` → `None`、`low` → `Low`、`max` → `Max`,`xhigh` 保持原样;
|
|
328
|
+
不做中文翻译。
|
|
329
|
+
6. **同步即更新**:OpenRouter 表由插件页的「OpenRouter 模型同步」按配置的间隔自动同步,也可
|
|
330
|
+
手动触发;刷新模型列表**不会**清空它。同步成功后内存快照与磁盘文件一起原子替换,失败则两者
|
|
331
|
+
都保留上一次成功的数据。请求带浏览器 UA 与 `X-Title`,单次 30 秒,429/5xx/网络错误重试一次,
|
|
332
|
+
连续失败后退避 5 分钟再试,避免每 30 秒一次把账号打到限流。
|
|
333
|
+
目录解析不再等待 OpenRouter(`readOpenRouter` 只读内存快照):没有快照时按「两边都没说」处理,
|
|
334
|
+
目录读取不会被一次 800 KB 下载拖住。
|
|
335
|
+
|
|
336
|
+
Loomy 的 `GET /models` 自带 `reasoning_efforts` 与 `default_reasoning_effort`,但为了统一
|
|
337
|
+
规则,它也会先查 OpenRouter(命中时以实时表为准),匹配不上才用上游的档位列表;无论档位来自
|
|
338
|
+
哪一边,默认档都按最强计算。它的请求体直接透传 `reasoning_effort`。
|
|
339
|
+
|
|
340
|
+
### 模型目录缓存(`catalog.ts`)
|
|
341
|
+
|
|
342
|
+
`ModelCatalog` 是惰性缓存:TTL(`modelCacheTtlSeconds`,默认 120 秒)只是过期标记,过期后
|
|
343
|
+
不会自己刷新,要等下一次读取(切模型、打开菜单、设置页)才在后台刷新;有快照时那次读取立即
|
|
344
|
+
返回旧数据,没有快照时才会等待。登录成功、退出登录、网关 401/403,以及 `fresh=1` 的读取会
|
|
345
|
+
清空快照。Trae 与 CodeArts 只在登录无法续期时才通过 `onInvalidate` 清空,不再每轮对话后
|
|
346
|
+
清空——否则刚聊完再切模型又要重新读一遍目录,正是缓存要避免的卡顿。
|
|
347
|
+
|
|
348
|
+
### OpenRouter 快照(`openrouter.ts`)
|
|
349
|
+
|
|
350
|
+
一份快照由四个供应商共享,由 `index.ts` 创建并注入。`models()` 只读内存(缓存为空时才触发
|
|
351
|
+
一次同步),刷新供应商目录不会清空它;`sync()` 是唯一会发网络请求的入口,由 30 秒 tick 按
|
|
352
|
+
`openRouterSyncMinutes` 判断是否到期,设置页的「立即同步」也调用它。
|
|
353
|
+
|
|
354
|
+
原子性分两层:内存中只有完整解析并建好索引后才替换 `snapshot`;磁盘先写同目录临时文件,
|
|
355
|
+
再 `rename` 覆盖 `$DSH_HOME/llm-adapters/openrouter-models.json`。任一步失败都不覆盖旧数据,
|
|
356
|
+
并把原因放进 `status().error`。并发调用共享同一个 in-flight Promise,所以定时同步、手动同步
|
|
357
|
+
和模型读取不会打出多个请求。
|
|
358
|
+
|
|
359
|
+
Raccoon 的思考控制有两条通道,都要发:`extra_body.thinking.type = enabled | disabled`
|
|
360
|
+
(真正的开关,`disabled` 实测能把 `reasoning_tokens` 压到 0)与 `reasoning_effort`
|
|
361
|
+
(强度档,网关确实生效;Kimi-K3 上 `low` 约 2.7k、`max` 约 8.4k completion token)。
|
|
362
|
+
`raccoon.ts` 只在 DSH 传下 `reasoningEffort` 时才写这两个字段;DSH 会用适配器声明的
|
|
363
|
+
`defaultReasoningEffort` 物化它。
|
|
364
|
+
|
|
365
|
+
额度接口是 `GET /points/v1/balance`,核心字段为 `available_points`(另有 `reward_points`、
|
|
366
|
+
`daily_points`、`topup_points`)。没有 `available_points` 时按旧字段回退,绝不编造数字。
|
|
367
|
+
|
|
368
|
+
### 失败与清理
|
|
369
|
+
|
|
370
|
+
- 超时、二维码失效、用户取消都会终止页面轮询并把状态写进 `LoginStatus`;设置页显示原因。
|
|
371
|
+
- 取消/插件卸载时 `close()` 关闭服务器;正在进行的 provider 轮询即使稍后返回,也不会再写入
|
|
372
|
+
凭据(`advance()` 在落定前检查 `terminal`)。
|
|
373
|
+
- 登录页的 QR 与状态接口不带任何令牌;账号写入仍走 `AccountStore`。
|
|
374
|
+
|
|
375
|
+
## 凭据
|
|
376
|
+
|
|
377
|
+
凭据只有一个来源:`AccountStore`(`$DSH_HOME/llm-adapters/accounts.json`)。插件不发现、不
|
|
378
|
+
读取、也不回写任何客户端登录文件,也没有客户端路径配置项。
|
|
379
|
+
|
|
380
|
+
`AccountStore` 的读-改-写全部串行在同一条 Promise 链里;并发登录与退出不会互相覆盖。
|
|
381
|
+
文件权限 0600;目录由 `DSH_LLM_ADAPTERS_HOME` 或 `$DSH_HOME/llm-adapters` 决定。
|
|
382
|
+
|
|
383
|
+
- Loomy:登录时声明 14 天有效期并把到期时间存进记录;过期后状态变为未登录,提示重新扫码。
|
|
384
|
+
- 小浣熊:扫码得到的 access token 若是 JWT,按 `exp` 判断;没有 JWT 声明的不透明令牌按
|
|
385
|
+
已登录处理,等网关 401 再提示重新扫码。两者都没有刷新流程。
|
|
386
|
+
- Trae:一个区域一条记录(`trae.cn` / `trae.global`),带令牌、设备指纹、登录时间、登录时
|
|
387
|
+
捕获的网页 cookie 与网页会话到期时间。JWT 的 `exp` 约 8 小时,到期前 5 分钟用 cookie 调
|
|
388
|
+
`common/GetUserToken` 自动续期;续期失败才提示重新登录。退出登录清当前区域并删除该区域的
|
|
389
|
+
浏览器配置目录。
|
|
390
|
+
- CodeArts:AK/SK + STS 安全令牌,外加续期所需的 refresh token、PKCE verifier 与 DPoP 私钥。
|
|
391
|
+
到期前 5 分钟自动续期,续期结果写回同一条记录;续期令牌失效才需要重新授权。
|
|
392
|
+
|
|
393
|
+
`RaccoonAuth.accessToken(true)` 只用于网关明确拒绝令牌的路径,它会直接报“登录已失效”,
|
|
394
|
+
不再尝试刷新或回读任何文件。
|
|
395
|
+
|
|
396
|
+
## 设置接口
|
|
397
|
+
|
|
398
|
+
| 路由 | 用途 |
|
|
399
|
+
| --- | --- |
|
|
400
|
+
| `GET /api/llm-adapters/models?brand=` | 实时目录 + 开关 / 窗口,`fresh=1` 清缓存 |
|
|
401
|
+
| `GET /api/llm-adapters/status?brand=` | 登录状态、登录来源、本机客户端检测、到期时间、额度 |
|
|
402
|
+
| `GET /api/llm-adapters/account/browser?brand=` | 登录进度(本地登录页 URL、Trae 的浏览器状态或 CodeArts 的授权页地址) |
|
|
403
|
+
| `POST /api/llm-adapters/account` | `browser` / `browser-cancel` / `client-login` / `checkin` / `logout` |
|
|
404
|
+
|
|
405
|
+
`checkin` 由 Trae(仅国内版且客户端登录态)与 CodeArts 实现,其余供应商会明确拒绝;`browser`
|
|
406
|
+
对 CodeArts 返回华为云授权页地址,页面在新标签页打开它,之后仍由宿主轮询 `/account/browser`
|
|
407
|
+
接回。`client-login` 只对 Trae 生效:服务端读取本机客户端登录态后返回普通账户读数,响应里
|
|
408
|
+
没有令牌。`GET /status` 的 `clientLogin` 字段只暴露客户端是否安装、是否已登录与账号名,
|
|
409
|
+
同样不含令牌。
|
|
410
|
+
|
|
411
|
+
写操作在 body 里带 `brand`,URL 不承载写目标。`browser` 还可以带 `region`(`cn` / `global`),
|
|
412
|
+
只有 Trae 使用它。配置写入仍走宿主 `configForms` 的修订栅栏,不经过插件自己的写接口。设置页
|
|
413
|
+
轮询进度时不会把令牌带给浏览器;`checkin` 只返回签到状态,不返回令牌。
|
|
414
|
+
|
|
415
|
+
Trae 的区域是配置项 `traeRegion`,不是第二个 brand:设置页切区域时写这个字段,随后按
|
|
416
|
+
`brand:region` 的 key 重读状态与目录快照,两个区域的账号与模型选择互不覆盖。
|
|
417
|
+
|
|
418
|
+
## 传输层
|
|
419
|
+
|
|
420
|
+
`wire.ts` 把 DSH 的 content blocks 投影成 OpenAI 消息:文本、图片(attachment store → data
|
|
421
|
+
URL + 路径句柄)、文件(占位文本)、assistant 工具调用、tool 结果。SSE 解析支持 CRLF 跨块;工具分片
|
|
422
|
+
先缓存到流尾再一次性发出,保证 id 与参数完整。
|
|
423
|
+
|
|
424
|
+
**图片除了像素还要给路径。** 像素让模型「看见」图片,但用户说「取反色后保存」时它需要知道文件在哪。
|
|
425
|
+
DSH 的原生适配器会在图片旁附上 `requestImageHandleText()`:附件名、像素尺寸、**只读归一化副本的
|
|
426
|
+
宿主路径**,以及「编辑前先复制成可写 `.png`」的指示;只发像素时模型只能去工作区里瞎找(实测回
|
|
427
|
+
「图片没有保存在工作目录中」)。现在 `ReadImage` 的返回值带上 `path`(`attachments.imageHostPath()`
|
|
428
|
+
取宿主路径,再经 `ctx.fs.processPathFromHostPath()` 映射到工具执行世界),`userContent()` 在
|
|
429
|
+
`image_url` 前插入 `imageRefText()` 生成的同一句文案;被请求限额丢弃的图片走
|
|
430
|
+
`offloadedImageText()`。适配器还会记住每个模型上一次验证过的 `vision` 能力:目录某次读不到时不再
|
|
431
|
+
谎报 text-only——那会让 DSH 在适配器之前就把像素换成占位符。
|
|
432
|
+
|
|
433
|
+
- 上游只给 `[DONE]`、不给 `finish_reason` 时(Loomy 会这样截断),标记为 `max-tokens`。
|
|
434
|
+
- usage 全部向上取整并保证 `reasoningTokens <= outputTokens`;DSH 的持久化校验不接受小数。
|
|
435
|
+
- OpenAI 规范的流会在中间帧带 `"usage": null`(CodeArts 的 VL 模型就是这样),`usageOf` 必须同时
|
|
436
|
+
容忍 `null` 与 `undefined`——只判后者会在首帧后抛 `Cannot read properties of null`,整条流作废。
|
|
437
|
+
- 小浣熊在 401 时不再重放请求:扫码登录没有刷新流程,直接提示重新扫码,避免重复回答。
|
|
438
|
+
|
|
439
|
+
## 测试与构建
|
|
440
|
+
|
|
441
|
+
```sh
|
|
442
|
+
npm install
|
|
443
|
+
npm run typecheck
|
|
444
|
+
npm test
|
|
445
|
+
npm run build
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
`lib/` 随源码一起提交:从 GitHub 直装(`github:k1-h2/dsh-llm-adapters`)时 pnpm 不会执行构建,
|
|
449
|
+
没有 `lib/index.js` 的包会在启用时报 `ERR_MODULE_NOT_FOUND`。改完 `src/` 必须重新构建并提交产物。
|
|
450
|
+
|
|
451
|
+
`vitest` 覆盖目录解析、登录页状态机、Loomy 长轮询与扫码绑定、小浣熊扫码轮询、Trae 两份目录
|
|
452
|
+
的合并与档位映射、Trae 客户端登录态的解密 / 设备号 / 过期与区域判定、Trae 客户端签到、
|
|
453
|
+
CodeArts 的签名 / PKCE-DPoP / 双目录 / 倍率时段 / 额度 / 签到、SSE 与工具调用、凭据存储与并发、
|
|
454
|
+
过期判定、设置 API。登录相关的测试全部注入假 `fetch`,不访问真实上游,
|
|
455
|
+
也不启动浏览器。需要真机验证时,手动点一次「打开扫码登录」或「打开浏览器登录」,确认接回、
|
|
456
|
+
模型列表与一次对话。
|
|
457
|
+
|
|
458
|
+
## 易变点
|
|
459
|
+
|
|
460
|
+
- Loomy 依赖微信授权页的 HTML 里仍内嵌 `/connect/qrcode/<uuid>`,以及讯飞账号端点的签名规则
|
|
461
|
+
和 `accessKeyId` / `accessKeySecret` / `appId` 常量。页面改版先看 `extractWechatUuid()`。
|
|
462
|
+
- 小浣熊依赖 `/auth/mp/qrcode` 的签发字段、`scan_path` 形态与 `auth/mp/qrcode/login` 的
|
|
463
|
+
状态值;扫码页或状态值改名会使二维码扫不出结果。
|
|
464
|
+
- 目录字段:Loomy 的倍率在展示名称的括号里,小浣熊的 `billing_*` / `params.*` 可能改名;
|
|
465
|
+
名称归一化与 OpenRouter 匹配都依赖这些字段的语义,改版时先看 `parseModelName` /
|
|
466
|
+
`buildOpenRouterIndex` 的测试。
|
|
467
|
+
- OpenRouter 表本身会变,所以每次「刷新状态与模型」都重拉,不写死任何模型清单;匹配不到时
|
|
468
|
+
回退上游,再没有就全部开放,任何新供应商都自动适用这套规则。
|
|
469
|
+
- Trae 依赖 `DevToolsActivePort` 与 CDP 的 `Network.getAllCookies`、以及页面里的
|
|
470
|
+
`localStorage.__msuuid__`;浏览器或站点改版后先看 `trae-login.ts` 的 `DEVICE_EXPRESSION`
|
|
471
|
+
与 `authPost` 路径。两个区域的基址集中在 `trae-endpoints.ts`,站点换域名只需改那一处。
|
|
472
|
+
- CodeArts 依赖华为云的 `portal/authorize` 参数拼写(`code_challenge_method` 必须是 `SHA-256`
|
|
473
|
+
而不是 `S256`,否则 portal 退回 ticket 流程)、`sts…/v1/oauth2/tokens` 的 DPoP 校验,以及
|
|
474
|
+
两个目录端点的字段名(`model_id` / `builtinModels` / `result.models`)。窗口静态表与 benefit
|
|
475
|
+
清单都在 `codearts-endpoints.ts`;上游调整窗口或新增 benefit 模型时改那一处。
|