dsh-tap 0.20.0

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.
Files changed (121) hide show
  1. package/AGENTS.md +99 -0
  2. package/CHANGELOG.md +751 -0
  3. package/LICENSE +21 -0
  4. package/README.en.md +59 -0
  5. package/README.md +256 -0
  6. package/cordis.patch.yml +385 -0
  7. package/core/bridge.js +698 -0
  8. package/core/json-store.js +91 -0
  9. package/core/rotation.js +108 -0
  10. package/core/usage-meter.js +176 -0
  11. package/docs/diagnosis-cache-quota.md +181 -0
  12. package/docs/diagnosis-qoder-flash.md +67 -0
  13. package/docs/diagnosis-trae-3003.md +302 -0
  14. package/docs/goals/bridge-port-host-split.md +91 -0
  15. package/docs/goals/desktop-adaptation.md +106 -0
  16. package/docs/goals/qoder-cn-provider-design.md +308 -0
  17. package/docs/goals/settings-card-ux-redesign-plan.md +1680 -0
  18. package/docs/goals/settings-card-ux-redesign.md +162 -0
  19. package/docs/goals/trae-agent-v3.md +41 -0
  20. package/docs/goals/trae-work-cn-repair.md +44 -0
  21. package/docs/goals/v0.8-/351/242/235/345/272/246/345/217/257/350/247/201-/346/250/241/345/236/213/345/212/250/346/200/201/345/214/226-/345/244/232/346/234/215/345/212/241/345/225/206.md +85 -0
  22. package/docs/pitfalls.md +105 -0
  23. package/docs/reverse/trae-cloud-api.md +218 -0
  24. package/docs/reverse/trae-model-catalog.md +129 -0
  25. package/docs/reverse/traework-cn.md +520 -0
  26. package/docs/rules/STATE.md +198 -0
  27. package/docs/rules/content-moderation.md +54 -0
  28. package/docs/rules/dev-role-boundary.md +94 -0
  29. package/docs/rules/extra-providers.md +42 -0
  30. package/docs/rules/gateway-facts.md +91 -0
  31. package/docs/rules/oauth-handshake.md +76 -0
  32. package/docs/rules/prompt-cache.md +93 -0
  33. package/docs/rules/quota-signals.md +125 -0
  34. package/docs/rules/routing.md +84 -0
  35. package/docs/rules/templates/oauth-reverse-checklist.md +42 -0
  36. package/docs/rules/trae-surface.md +242 -0
  37. package/docs/rules/ua-validation.md +81 -0
  38. package/host-config.js +282 -0
  39. package/index.js +2306 -0
  40. package/lib/client.js +2893 -0
  41. package/local-scan.js +104 -0
  42. package/package.json +82 -0
  43. package/providers/ark/index.js +11 -0
  44. package/providers/bailian/index.js +10 -0
  45. package/providers/bigmodel/index.js +11 -0
  46. package/providers/codebuddy/agenttool.js +122 -0
  47. package/providers/codebuddy/catalog.js +227 -0
  48. package/providers/codebuddy/errors.js +44 -0
  49. package/providers/codebuddy/headers.js +36 -0
  50. package/providers/codebuddy/images.js +125 -0
  51. package/providers/codebuddy/index.js +123 -0
  52. package/providers/codebuddy/oauth.js +279 -0
  53. package/providers/deepseek/index.js +11 -0
  54. package/providers/moonshot/index.js +11 -0
  55. package/providers/openai-compat.js +177 -0
  56. package/providers/openrouter/index.js +27 -0
  57. package/providers/qoder/catalog.js +145 -0
  58. package/providers/qoder/cosy.js +419 -0
  59. package/providers/qoder/gateway.js +563 -0
  60. package/providers/qoder/index.js +116 -0
  61. package/providers/qoder/oauth.js +364 -0
  62. package/providers/qoder/qoder_auth.wasm +0 -0
  63. package/providers/qoder/quota.js +56 -0
  64. package/providers/qwen/index.js +15 -0
  65. package/providers/tool-pairing.js +129 -0
  66. package/providers/trae/catalog.js +103 -0
  67. package/providers/trae/errors.js +85 -0
  68. package/providers/trae/gateway.js +853 -0
  69. package/providers/trae/index.js +126 -0
  70. package/providers/trae/oauth.js +443 -0
  71. package/providers/trae/quota.js +75 -0
  72. package/providers/trae/remote.js +365 -0
  73. package/scripts/capture-cache.mjs +65 -0
  74. package/scripts/capture-traffic.mjs +83 -0
  75. package/scripts/hermes-probe-dev-role.mjs +263 -0
  76. package/scripts/measure-latency.mjs +253 -0
  77. package/scripts/probe-ark-thinking.mjs +298 -0
  78. package/scripts/probe-cache-decline.mjs +292 -0
  79. package/scripts/probe-cache-ttl.mjs +221 -0
  80. package/scripts/probe-cache.mjs +156 -0
  81. package/scripts/probe-codebuddy-efforts.mjs +355 -0
  82. package/scripts/probe-codebuddy-tier-wiring.mjs +244 -0
  83. package/scripts/probe-effort-gaps.mjs +133 -0
  84. package/scripts/probe-media.mjs +115 -0
  85. package/scripts/probe-moderation.mjs +159 -0
  86. package/scripts/probe-oauth.mjs +617 -0
  87. package/scripts/probe-qoder-attribution-arm8.mjs +92 -0
  88. package/scripts/probe-qoder-attribution-arm9.mjs +102 -0
  89. package/scripts/probe-qoder-attribution.mjs +266 -0
  90. package/scripts/probe-qoder-flash-confirm.mjs +94 -0
  91. package/scripts/probe-qoder-live.mjs +344 -0
  92. package/scripts/probe-qoder-matrix.mjs +274 -0
  93. package/scripts/probe-qoder-null-content.mjs +153 -0
  94. package/scripts/probe-qoder-pairing.mjs +308 -0
  95. package/scripts/probe-qoder-quota.mjs +162 -0
  96. package/scripts/probe-qoder-thinking-config.mjs +52 -0
  97. package/scripts/probe-qoder-thinking-efforts.mjs +269 -0
  98. package/scripts/probe-quota.mjs +136 -0
  99. package/scripts/probe-quota2.mjs +144 -0
  100. package/scripts/probe-routing.mjs +226 -0
  101. package/scripts/probe-trae-3003-diagnosis.mjs +139 -0
  102. package/scripts/probe-trae-agent-v3.mjs +399 -0
  103. package/scripts/probe-trae-efforts.mjs +149 -0
  104. package/scripts/probe-trae-live.mjs +147 -0
  105. package/scripts/probe-trae-max-effort.mjs +179 -0
  106. package/scripts/probe-trae-model-routing.mjs +381 -0
  107. package/scripts/probe-trae-thinking-scene.mjs +238 -0
  108. package/scripts/probe-trae-transport-outage.mjs +176 -0
  109. package/scripts/probe-ua.mjs +508 -0
  110. package/scripts/trae-model-catalog.mjs +632 -0
  111. package/scripts/verify-agents-md.mjs +45 -0
  112. package/scripts/verify-bridge.mjs +1041 -0
  113. package/scripts/verify-core-generic.mjs +302 -0
  114. package/scripts/verify-desktop-acceptance.mjs +184 -0
  115. package/scripts/verify-host-config.mjs +265 -0
  116. package/scripts/verify-models.mjs +390 -0
  117. package/scripts/verify-providers.mjs +255 -0
  118. package/scripts/verify-qoder-provider.mjs +1020 -0
  119. package/scripts/verify-rotation.mjs +308 -0
  120. package/scripts/verify-trae-model-catalog.mjs +284 -0
  121. package/scripts/verify-trae-provider.mjs +1451 -0
@@ -0,0 +1,242 @@
1
+ # TRAE API 面 / OAuth / 插件 侦察规则文档
2
+
3
+ > 日期:2026-08-19
4
+ > 方法:dsh-tap 同款逆向方法论 —— 现象 → 假设 → 探测 → 规则 → 预测验证
5
+ > 目标:把 TRAE(字节火山系)API 面、OAuth、插件体系摸成白箱,供未来做 provider/自动化接入
6
+ > 主线来源:TRAE 官方日志 + 本地配置 + 实际 HTTP 探测
7
+
8
+ ---
9
+
10
+ ## 0. 一句话结论
11
+
12
+ TRAE(TRAE SOLO CN / trae.cn)是字节火山系的 Electron IDE,与 CodeBuddy(腾讯 copilot.tencent.com)不是同一套后端:
13
+
14
+ - 认证:字节账号体系(手机号登录,内部用 Supabase OAuth 服务)
15
+ - API 面:api.trae.cn + api.trae.com.cn + trae-api-cn.mchost.guru 三个域名,专用协议(非 OpenAI 兼容)
16
+ - 插件:trae-remote-official registry 的 16 个官方插件,全部本地 ~/.trae-cn/plugins/
17
+ - MCP:支持本地集成模式(solo_design_lite / dev_agent / browser_use),无远程 HTTP MCP
18
+ - Skills:本地 skills + builtin skills + 远端 skill market
19
+
20
+ CodeBuddy 那套规则(UA/缓存/路由/额度)对 TRAE 不直接适用,但方法论完全可迁移——本文档就是 TRAE 版的第一份侦察基线。
21
+
22
+ ---
23
+
24
+ ## 1. 主机事实(侦察基线)
25
+
26
+ ### 1.1 安装与数据目录
27
+
28
+ | 项 | 路径 | 说明 |
29
+ |---|---|---|
30
+ | 可执行文件 | AppData\Local\Programs\TRAE SOLO CN\TRAE SOLO CN.exe | Electron,214MB |
31
+ | 资源 | ...\resources\app | 未打包 asar(只有 node_modules.asar),源码可读 |
32
+ | 数据目录 | AppData\Roaming\TRAE SOLO CN\ | 配置、日志、会话存储 |
33
+ | 用户数据 | %USERPROFILE%\.trae-cn\ | 插件、skills、mcps、extensions、plugin-config 都在这 |
34
+ | 日志 | AppData\Roaming\TRAE SOLO CN\logs\<timestamp>\main.log | 主日志,含全部 API 调用 |
35
+ | 构建 | version 2.3.71801 / app 0.1.51 / vscode 1.107.1 | 2026-08-14 构建 |
36
+
37
+ ### 1.2 本地服务端口(运行时)
38
+
39
+ | 端口 | 用途 |
40
+ |---|---|
41
+ | 17788 | Supabase OAuth 本地服务(回调 /callback) |
42
+ | 51000 | ckg 本地嵌入服务(local_embedding) |
43
+ | 8644 / 9097 / 9900 | 其他本地网络服务 |
44
+
45
+ ### 1.3 关键事实
46
+
47
+ - TRAE 使用 ttnet(火山网络库)做 HTTP,带 ttnet fetch 日志前缀
48
+ - 用户已登录(手机号 <redacted:phone>,用户 ID <redacted:userId>,LastLoginType=sms,注册 2025-08-09)
49
+ - token 有效期:expiredAt=2026-08-29、refreshExpiredAt=2027-02-11、tokenReleaseAt=2026-08-15,JWT refresh 被禁用
50
+
51
+ ---
52
+
53
+ ## 2. API 面(已实测)
54
+
55
+ ### 2.1 域名三件套
56
+
57
+ | 域名 | 归属 | 典型端点 |
58
+ |---|---|---|
59
+ | api.trae.cn | 主 API | pay、ug、GetThirdPartyToken、cloudide GetUserInfo |
60
+ | api.trae.com.cn | 扩展/icube | extensions skill/list、icube user/native/config/notifications |
61
+ | trae-api-cn.mchost.guru | 远端技能 market | /api/remote/v1/skills |
62
+
63
+ ### 2.2 已抓到的完整端点清单(启动时自动调用)
64
+
65
+ | 端点 | 需要认证? | 响应 | 说明 |
66
+ |---|---|---|---|
67
+ | api.trae.cn/ | 否 | 404 | 根路径无内容 |
68
+ | api.trae.cn/trae/api/v2/ug/checkin_credits/status | 是 | 200 JSON,未认证时 code:1001 | 签到积分状态 |
69
+ | api.trae.cn/trae/api/v2/pay/ide_user_pay_status | 是 | 200 JSON(code 1001 变体) | 套餐/付费状态 |
70
+ | api.trae.cn/trae/api/v2/pay/ide_user_ent_usage | 是 | 200 | 企业用量 |
71
+ | api.trae.cn/trae/api/v3/GetThirdPartyToken | 是 | 404 未带参数;正常调用 3 次(feishu/lark) | 三方 token 获取 |
72
+ | api.trae.cn/cloudide/api/v3/trae/GetUserInfo | 是(x-cloudide-token) | ResponseMetadata+Result | 用户信息 |
73
+ | api.trae.com.cn/extensions/api/-/skill/list | 否 | 200 JSON | 技能市场列表(公开) |
74
+ | trae-api-cn.mchost.guru/api/remote/v1/skills | 是 | 401 未认证 | 远端技能 |
75
+ | api.trae.com.cn/icube/api/v1/user | 未知 | 404 | icube 用户 |
76
+ | api.trae.com.cn/icube/api/v1/native/config/query | 混合 | 带 mid/did/uid 参数 | 动态配置 |
77
+ | api.trae.com.cn/icube/api/v1/notifications/count | 混合 | 200 | 通知数 |
78
+ | api.trae.com.cn/icube/api/v1/package/check_update | 否 | — | 更新检查 |
79
+
80
+ > 错误码规律(已实测):未认证错误统一为 code: 1001 + 英文 apologize 文案,与 CodeBuddy 的 12403/11101/11102/14407 完全不同。
81
+
82
+ ### 2.3 认证头
83
+
84
+ - x-cloudide-token: <token> 用于 cloudide API(日志确认)
85
+ - 其他端点带 x-cloudide-token / 可能还有 ttnet 特有头
86
+ - GetUserInfo 请求头:Content-Type: application/json + x-cloudide-token
87
+
88
+ ---
89
+
90
+ ## 3. OAuth(Supabase 服务,已实测触发)
91
+
92
+ ### 3.1 架构
93
+
94
+ 用户点击登录/扫码
95
+ -> TRAE 启动本地 SupabaseOAuthLocalServer (127.0.0.1:17788)
96
+ -> 浏览器/回调端点 http://127.0.0.1:17788/callback?code=XXX&state=YYY
97
+ -> 服务解析 code/state -> 换 token -> 写文件
98
+
99
+ ### 3.2 已实测行为
100
+
101
+ | 探测 | 结果 | 结论 |
102
+ |---|---|---|
103
+ | GET /callback(无参数) | 400 | 端点存在,缺参数 |
104
+ | GET /callback?code=test123&state=xyz | 500 + Failed to write supabase token to file | 完整链路被触发:接受参数->尝试换 token->写盘失败(假 code) |
105
+ | GET /oauth /auth /health | 404 | 只有 /callback 一个公开端点 |
106
+ | token 保存路径 | AppData\Roaming\TRAE SOLO CN\User\supabase-token.json | 日志明确,当前文件不存在(token 存在别处/unset) |
107
+
108
+ ### 3.3 规则(已确立)
109
+
110
+ 1. 本地 OAuth 服务端口固定 17788(可被占用则换端口,日志会打 Found available port)
111
+ 2. /callback 是唯一公开暴露端点,接受 code + state 参数
112
+ 3. 认证 token 主存储不在 supabase-token.json(文件不存在但应用正常工作)-> 存在 Windows 凭据或内存
113
+ 4. JWT refresh 禁用 -> token 到期(8-29)后需重新登录,refreshToken 无自动续期
114
+ 5. GetThirdPartyToken 启动时 fetch 3 次(feishu/lark),响应非数组则告警 Result is not an array(不影响主功能)
115
+
116
+ > 未解(需真登录一次才可测):token 在 GetUserInfo/其他 API 间的复用、refresh 轮换的真实条件、state 生命周期真实 TTL。
117
+
118
+ ---
119
+
120
+ ## 4. 插件体系(已完整侦察)
121
+
122
+ ### 4.1 插件来源
123
+
124
+ - 只装官方 registry:trae-remote-official(display: Remote Official Plugins)
125
+ - 安装清单:~/.trae-cn/installed-plugins.json(含 marketplace、page、plugin 详情)
126
+ - 启用配置:~/.trae-cn/plugin-config.json
127
+
128
+ ### 4.2 16 个官方插件(全清单,本地实装)
129
+
130
+ | 插件 | 版本 | 用户启用 | 说明 |
131
+ |---|---|---|---|
132
+ | browser | 1.0.3 | 是 | 浏览器控制 |
133
+ | build-web-apps | 0.1.2 | 是 | Web 应用构建 |
134
+ | build-web-data-visualization | 0.1.21 | 否 | 数据可视化 |
135
+ | frontend-design | 0.0.0 | 否 | 前端设计 |
136
+ | github | 0.1.2 | 是 | GitHub |
137
+ | ip-strategy | 0.1.2 | 否 | IP 策略 |
138
+ | lark | 1.0.3 | 否 | 飞书 |
139
+ | personal-workbench | 0.1.0 | 否 | 个人工作台 |
140
+ | product-lifecycle-workbench | 0.2.1 | 否 | 产品生命周期 |
141
+ | seedance | 1.0.1 | 是 | 即梦视频 |
142
+ | seedream | 1.0.1 | 是 | 即梦图片 |
143
+ | staff-engineer-mode | 2.1.0 | 否 | 员工工程师模式 |
144
+ | stark | 0.7.2 | 是 | 智能体协调 |
145
+ | story-craft | 0.1.1 | 否 | 故事创作 |
146
+ | web-app-development | 0.1.1 | 是 | Web 应用开发 |
147
+ | whiteboard | 0.2.0 | 是 | 白板 |
148
+
149
+ ### 4.3 插件安装位置
150
+
151
+ ~/.trae-cn/plugins/trae-remote-official/<name>/<version>/
152
+
153
+ 每个插件含 manifest_json(name/version/i18n/interface/capabilities)、connector_json、mcp_servers_json、skills_json。
154
+
155
+ ### 4.4 MCP 系统(已实测)
156
+
157
+ TRAE 的 MCP 是本地集成模式,三个 profile:
158
+
159
+ | profile | 来源 | 发现 |
160
+ |---|---|---|
161
+ | solo_design_lite | dsh-tap 项目 | integrated_code_mode(V8 沙箱 Exec) + GitHub server |
162
+ | dev_agent | 规则动乱项目 | mcp_Blender(Blender 3D/混元 3D/Hyper3D 全套工具) |
163
+ | browser_use | 习惯项目 | GitHub server |
164
+
165
+ - 每个 MCP server 有 SERVER_METADATA.json(只含 server_name)+ tools/*.json(完整工具 schema)
166
+ - 工具 JSON 只有 schema 无实际端点 -> 本地集成 MCP,不带网络 API
167
+ - integrated_code_mode 的 Exec 是核心:V8 沙箱,无 console/fetch/require/process/fs/network,只有 tools/text/exit/ALL_TOOLS
168
+
169
+ ### 4.5 Skills
170
+
171
+ | 类别 | 位置 | 内容 |
172
+ |---|---|---|
173
+ | 本地 skills | ~/.trae-cn/skills/ | algorithmic-art、defuddle、flux-best-practices、json-canvas、obsidian-*、prompt-images、shadcn、study-habits、write-xiaohongshu、xiaohongshu-note-analyzer、zhihu-publisher |
174
+ | builtin skills | ~/.trae-cn/builtin_skills/ | TRAE-dynamic-ui、TRAE-security-review、_shared |
175
+ | 远端 skill market | api.trae.com.cn/extensions/api/-/skill/list | 未认证 200,公开 |
176
+
177
+ skill 开关:~/.trae-cn/skill-config.json(disabledSkills 60+ 个,含 lark-* 全家、visualization 全家)
178
+
179
+ ---
180
+
181
+ ## 5. 与 CodeBuddy 规则的关系(方法论迁移)
182
+
183
+ | CodeBuddy 课题 | TRAE 对应 | 结论 |
184
+ |---|---|---|
185
+ | UA 校验(/v3/config) | 无 UA 门 | 不适用 |
186
+ | 提示缓存(按内容寻址) | 未测(TRAE 有自己的 ttnet 缓存体系) | 待探索 |
187
+ | 路由配置(14407) | 错误码完全不同(1001 vs 14407) | 重做 |
188
+ | 额度信号(pay/usage) | 有 pay/usage 端点(ide_user_pay_status / ent_usage) | 高度相关,下一步重点 |
189
+ | OAuth 握手 | Supabase 本地服务 + /callback,与 CodeBuddy 网页授权不同 | 方法论同,实现重做 |
190
+ | 内容审核 | 未测 | 待探索 |
191
+
192
+ ### 迁移规则(铁律)
193
+
194
+ 1. 先把 CodeBuddy 的 docs/rules/ 视为方法论模板,不是数据源
195
+ 2. 对 TRAE 所有"必须长这样"的结论,必须重新验证(每个端点真实探测)
196
+ 3. 错误码/头名/域名不通用,逐项重测
197
+ 4. OAuth 研究注意:只理解不绕过(沿用红线)
198
+
199
+ ---
200
+
201
+ ## 5.5 agent 面实测事实(function=solo_work_lite,2026-10-05 探针落地)
202
+
203
+ 同一聊天端点 `POST trae-api-cn.mchost.guru/api/agent/v3/llm_utils_chat` 的「agent 用法」由
204
+ `function` 参数切换——**solo_work_lite 面**(证据 docs/probes/trae-agent-v3-*.jsonl,
205
+ 探针 scripts/probe-trae-agent-v3.mjs,8 臂 + function 名扫描 ×2;社区参照
206
+ dsh-connect-trae SOLO_ROUTE_DECISION / Trae2api-cn build_llm_chat_body):
207
+
208
+ - **能力面(实测证实)**:接受 OpenAI 风格 tools(parameters 必须序列化为字符串,同
209
+ inline 面 Go string 型约束)+ tool_choice=auto;返回结构化 tool_calls(SSE
210
+ output 事件的 tool_calls[i] 键为 **function_call**,arguments 增量片段按 index
211
+ 拼接);**并行调用**(A4 臂单事件双 tool_calls);**role:tool 回传闭环**——
212
+ 第二轮历史携带 assistant.tool_calls + role:tool 结果后产出最终回答(A3 臂)。
213
+ - **方言陷阱(踩坑 #68)**:历史 assistant.tool_calls 的出站键必须是
214
+ **function_call**——OpenAI 同构的 function 键被 proto 层拒:
215
+ 2001 `*idecopilot.ToolCall read field 4 'FunctionCall' error: required field
216
+ Name is not set`。inline_chat 面则用 function 键透传(2026-08-24 校准)——
217
+ **同端点不同 function 面各有方言**,不能互推。
218
+ - **function 名注册表(扫描实测)**:agent_chat / agent_v3 / solo_agent_chat /
219
+ agent / assistant_chat 均未注册(2001 "no function config found for appId=…");
220
+ chat 已注册但模型名不被路由(4023 "the model is unknown"——进入模型路由层才报);
221
+ solo_agent 被 dsh-connect-trae 标注为 Remote-agent 花名册函数(用它调任何模型
222
+ 回 4011)。
223
+ - **模型路由钉死**:任何 model 名都 200,但 timing_cost.provider_model_name 恒为
224
+ glm-5.2(A6 臂 kimi-k2.6 / DeepSeek-V4-Flash 实测)——改派由网关 SSE 注释行
225
+ / message.note 诚实披露,计量记真实模型。
226
+ - **reasoning_effort 被忽略**(A5 臂与 A2 同形态,不报错也不生效)——档位注入
227
+ 仅保留在 remote 面;agent 面照发不虚标(踩坑 #42 纪律)。
228
+ - **宽容面**:错模型名(E1)与坏参数形态(E2,temperature 字符串)均静默容忍
229
+ 返回 200——与 inline 面 3003 硬错相反。
230
+ - **额度**:token_usage 事件携带 prompt/completion/total/reasoning_tokens/
231
+ cache_read_input_tokens 全字段;消耗 IDE 池(available_endpoint=0,同 raw 面)。
232
+ - 落地:`traeChatTransport:'agent'`(第三档,设置卡可选,逐请求热读取免重启)。
233
+
234
+ ## 6. 未解清单(下一步探测计划)
235
+
236
+ - [ ] api.trae.cn 各端点的完整认证方案(x-cloudide-token 从哪来、哪些端点共用)
237
+ - [ ] pay/usage 端点的认证响应展开(额度信号的迁移重点)
238
+ - [ ] TRAE 的 ttnet 缓存体系(是否有类似按内容寻址)
239
+ - [ ] Supabase OAuth 的真实 code 交换(需一次真实登录)
240
+ - [ ] token 的 refresh 轮换真实行为(JWT refresh disabled 是否恒真)
241
+ - [ ] 插件 market API:api.trae.com.cn/extensions/api/-/skill/list 的完整清单(未认证可取)
242
+ - [ ] installed-plugins.json 的 marketplace 分页(page_size 16,下一页在哪)
@@ -0,0 +1,81 @@
1
+ # 规则 01:UA 校验(12403)
2
+
3
+ > 状态:**规则成立**(主路径);宽松路径的出现条件**未解**(见 §6)
4
+ > 证据:`docs/probes/ua-2026-08-19.jsonl`(74 条探测记录,含每轮预注册预测与 HIT/MISS 判定)
5
+ > 脚本:`scripts/probe-ua.mjs`(`--set matrix|predict…predict12` 可复跑全部轮次)
6
+
7
+ ## 1. 现象(上游咒语原文)
8
+
9
+ - AGENTS.md:22 —「`/agenttool/v1/search`、`/agenttool/v1/webfetch`:UA 必须是 CLI 形态(如 `CLI/unknown CodeBuddy/2.136.0`,`CodeBuddyCode/1.0` 被拒 12403)」
10
+ - README.md:159 —「`GET /v3/config`…UA 必须形如 `CLI/unknown CodeBuddy/2.136.0`,否则报 12403」
11
+ - index.js:551 —「/agenttool rejected the /v2 client UA with error 12403 ("check ua")」
12
+ - cordis.patch.yml:29 —「e.g. "CLI/unknown CodeBuddy/2.136.0" — the /v2 client UA is rejected with error 12403」
13
+
14
+ 咒语形态:一个**必须逐字长这样的字符串**。本课题把它拆成了可证伪的字段规则。
15
+
16
+ ## 2. 规则(每条附验证过程)
17
+
18
+ ### R-A:/v3/config 的 UA 门 = 子串正则,不是全串比对
19
+
20
+ **规则**:UA 通过 ⟺ 其中包含匹配 `/codebuddy\/[^a-z\s]*\./i` 的子串——即 `codebuddy` 产品 token(**大小写不敏感**)后紧跟一段**不含 ASCII 字母、不含空格**且**至少含一个字面点号**的"版本"前缀;点号之后是什么不再检查;token 在 UA 中的位置不限。
21
+
22
+ **验证链**(全部实测,括号内为该轮 HIT/MISS):
23
+
24
+ | 规则迭代 | 内容 | 结局 |
25
+ |---|---|---|
26
+ | H1 | 需要 `CodeBuddy/<semver>` token,其余装饰 | 被 `codebuddy/2.136.0`(小写通过)推翻 |
27
+ | R1 | 不区分大小写子串 `codebuddy/` | 被 `X/y CODEBUDDY/1`(12403)推翻 |
28
+ | R1′ | 白名单两种大小写 | 被 `Codebuddy/1.0`、`codeBuddy/1.0`、`CODEBUDDY/2.136.0` 全通过推翻 |
29
+ | R2/R3 | 版本长度 ≥2 / ≥3 | 被 `CodeBuddy/111`(3 字符仍 12403)、`CodeBuddy/abcd`(4 字符仍 12403)推翻 |
30
+ | R6 | 双正则析取(CLI 形 ∨ semver token) | 被 `CLI/unknown CodeBuddy/abcd`(12403)推翻 |
31
+ | R7/R8 | `/codebuddy\/\d+\.\d+/i` → `/codebuddy\/\d+\./i` | 被 `CodeBuddy/.5`(点前无数字仍通过)推翻 |
32
+ | R9/R10 | 首字符类 `[\d.]` / `[\d.-]` | 被 `-.5`、`+.5`、`_.5` 通过推翻 |
33
+ | **R11→R-A** | `/codebuddy\/[^a-z\s]*\./i` | **12 条预注册预测 11 条 HIT**(见下) |
34
+
35
+ R-A 的预注册预测命中记录(未观测行为 → 实测):
36
+ - `CodeBuddy/.` → 200 **HIT**;`CodeBuddy/..` → 200 **HIT**(孤立点号即合法)
37
+ - `CodeBuddy/x.` → 12403 **HIT**(字母打头、点后无内容才构成拒因)
38
+ - `CodeBuddy/汉.5` → 不可测(HTTP 头为 ByteString,非 ASCII 无法发送,证据记 n/a)
39
+ - `CodeBuddy/ .` → 12403 **HIT**(空格断版本段)
40
+ - `CodeBuddy/ab.1`、`CodeBuddy/a1.2` → 12403 **HIT**(点前含字母即拒)
41
+ - `CodeBuddy/1.0` 控制 → 200 **HIT**
42
+ - `CodeBuddy/v1.2`、`CodeBuddy/1a.2`、`CodeBuddy/x.5`、`CodeBuddy/Z.9` → 12403(实测,反向锁定"点前禁字母")
43
+
44
+ ### R-B:触发 12403 的最小变异
45
+
46
+ **规则**:从现网 UA `CLI/unknown CodeBuddy/2.136.0` 出发,**单字符替换**第一个版本数字为任意 ASCII 字母(如 `CodeBuddy/x.136.0`)即触发 12403;没有任何单字符删除能触发(`2.136.0` 删首字符得 `.136.0` 仍合法,删一个点仍有另一个点)。校验器唯一检查的字段是 UA 中的 `codebuddy/<非字母非空格且含点>` token;`CLI/unknown` 前缀、`X-IDE-*`、`X-Product`、`X-Requested-With` 均不参与校验(`no-xproduct` 200;`ide-other`、`case-cli-lower`、`suffix-added` 全 200)。
47
+
48
+ 拒绝面:`HTTP 400` + `{"code":12403,"msg":"check ua, get coding copilot version error"}`。
49
+
50
+ ### R-C:端点作用域 —— 咒语的作用域是错的
51
+
52
+ - **`/v3/config`:有 UA 门**(本规则全部证据所在)。
53
+ - **`/v2/chat/completions`:无 UA 门**。真实字节证据:`docs/probes/bridge-flash-ab-2026-08-18.jsonl` 中线上桥以 `user-agent: deepseek-harness/0.1.0-rc.6` 成功;探测补证 `Garbage/0.0` + `stream:true` → 200 SSE 流(`predict-chat-stream-garbage-ua` HIT)。
54
+ - **`/agenttool/v1/search`:当前无 UA 门**(api-key 认证下)。咒语称 `CodeBuddyCode/1.0` 在此被 12403,实测 `CodeBuddyCode/1.0`、`Garbage/0.0`、`CodeBuddy/ab` **全部 200 并返回搜索结果**(`predict-agenttool-*` 三条)。咒语描述的是历史行为或 OAuth 路径行为——本插件使用的 api-key 直调路径上该门不存在。
55
+
56
+ ## 3. 对 CLIENT_HEADERS 的裁判(重构依据)
57
+
58
+ | 字段 | 判定 | 依据 |
59
+ |---|---|---|
60
+ | `User-Agent: CLI/unknown CodeBuddy/2.136.0` | **部分是规则**:仅需含 `codebuddy/` + 含点的非字母版本段;`CLI/unknown` 前缀与具体版本号**是迷信** | R-A 全部证据 |
61
+ | `X-IDE-Type/Name/Version`、`X-Product-Version` | **对校验是迷信**(是否参与遥测/路由 = 课题 3 待查) | `no-xproduct`、矩阵伴随头组 |
62
+ | `X-Product: SaaS` | 同上,/v3/config 不查 | `no-xproduct` 200 |
63
+ | `X-Requested-With` | /v3/config、/agenttool 均不查 | 矩阵未携带仍 200(探测脚本只发了 Accept/Auth/x-api-key/UA) |
64
+
65
+ ## 4. 红线条目核对
66
+
67
+ - 咒语未入库:每条结论上方附验证链;"宽松路径"问不动 → §6 标未解。
68
+ - 真实字节基准:/v2 无 UA 门的结论以 `bridge-flash-ab-2026-08-18.jsonl` 抓包为基准;探测变异本身是受控实验而非对照基准。
69
+ - 低速率:74 次请求,间隔 1.5–2s,单账号,全部只读(chat 臂 `max_tokens=1`,search 臂 `max_results:1`)。
70
+
71
+ ## 5. 现象 → 规则的可复跑路径
72
+
73
+ ```bash
74
+ node scripts/probe-ua.mjs --set matrix # 17 变异 + 2 chat 臂
75
+ node scripts/probe-ua.mjs --set predictN # N=1..12,逐轮预注册预测
76
+ ```
77
+
78
+ ## 6. 未解
79
+
80
+ - **宽松路径(flapping)**:严格规则之外,少数请求间歇放行非标版本(`CLI/unknown CodeBuddy/abc`、`CodeBuddy/a.1`、`CodeBuddy/abc` 各通过一次;`a.1` 复测 4/4 拒、`abc` 复测 2 拒 1 放)。放行条件(节点哈希 / 灰度 / 时间窗)未能从黑盒确定。工程结论:**按严格规则构造 UA,不把宽松路径当能力依赖**。
81
+ - 字母打头版本中 `a.1` 曾通过一次:若未来复现稳定,需重开本课题(脚本预测集可直接扩展)。
package/host-config.js ADDED
@@ -0,0 +1,282 @@
1
+ /**
2
+ * host-config.js — dsh 宿主配置层的读写通道(按宿主能力选路)。
3
+ *
4
+ * 为什么需要这一层(dsh 0.1.7-alpha.2 实测,2026-09-23):
5
+ * 上游把「用户配置文档」整体换成了「profile 的 volatile 配置表单」——
6
+ * - `@deepseek-ai/dsh-settings-file` 被删除(npm 上 0.1.7-alpha.2 已 404),
7
+ * `settings` 服务改由 `@deepseek-ai/dsh-settings` 提供;
8
+ * - `~/.dsh/settings.yaml` 降级为 legacy:Loader 稳定后**一次性导入** profile
9
+ * patch,随即改名 `settings.yaml.imported`(实测真实 web profile 下
10
+ * `llm-pi-ai` 段导入成功,profile patch 1.6KB → 12KB);
11
+ * - `ctx.settings.register(ns, schema)` **已不存在**(调用即 TypeError),
12
+ * 取而代之的是 `configure({auto}, fiber)`(页面策略)+
13
+ * `describe/update/replace/mutate(ns=profile entry id, …, expectedRevision)`;
14
+ * - 写入由 `dsh-config-editor` 落到 profile 的 `cordis.patch.yml`,立即生效;
15
+ * - 只有 `.volatile()` 字段可表单编辑——`llm-pi-ai` 的 `providers` 在 0.1.7
16
+ * 正是 volatile,所以本插件的模型镜像/路由存在性管理能平移过去。
17
+ *
18
+ * 本模块把两代宿主的差异收在一处:上层只产出 ops(set/unset 一条路径),
19
+ * 由这里决定走新 seam 还是旧的 settings.yaml 文档编辑。写入**永不 reject**
20
+ * (踩坑 #33:fire-and-forget 必须落地)——失败记在 lastError 并回传结果对象。
21
+ */
22
+
23
+ import { readFileSync } from 'node:fs'
24
+ import YAML from 'yaml'
25
+ import { writeTextAtomic } from './core/json-store.js'
26
+
27
+ /** llm-pi-ai 在 profile 里的 entry id(也是新 seam 的命名空间)。 */
28
+ const NS = 'llm-pi-ai'
29
+
30
+ /** 按路径取值;任一段缺失返回 undefined(用于"该路径当前有没有东西"的判定)。 */
31
+ function getPath(root, path) {
32
+ let cur = root
33
+ for (const seg of path) {
34
+ if (cur == null || typeof cur !== 'object') return undefined
35
+ cur = cur[seg]
36
+ }
37
+ return cur
38
+ }
39
+
40
+ /** 稳定序列化:键排序后 JSON,用于同值比对(避免每次重铺都判为"变了")。 */
41
+ function stable(value) {
42
+ try {
43
+ return JSON.stringify(value, (_k, v) => (v && typeof v === 'object' && !Array.isArray(v)
44
+ ? Object.keys(v).sort().reduce((o, k) => { o[k] = v[k]; return o }, {})
45
+ : v)) ?? 'null'
46
+ } catch {
47
+ return String(value)
48
+ }
49
+ }
50
+
51
+ export function createHostConfigLayer({ settingsPath, schema, legacyNamespace = 'dsh-tap', log = () => {} }) {
52
+ let settingsCtx = null
53
+ let service = null
54
+ let fiber = null
55
+ let disposeConfigure = null
56
+ let revision = null
57
+ let lastError = null
58
+ let attachError = null
59
+
60
+ /**
61
+ * 活取 settings 服务。宿主每次写入都会重载 profile patch,Settings 服务实例
62
+ * **可能被替换**(上游 README:a late-loading or replaced Settings service
63
+ * picks the policy up)——缓存的实例引用会带着陈旧 revision,写必撞
64
+ * SETTINGS_CONFLICT 且重试无效(实测:expected revision 5, now 6,三次全败)。
65
+ * cordis 的 ctx.<service> 是实时 getter,故一律经子上下文现取。
66
+ */
67
+ function svc() {
68
+ const live = settingsCtx ? settingsCtx.settings : null
69
+ return live ?? service
70
+ }
71
+
72
+ /** 旧宿主路径:注释保留的 settings.yaml 文档编辑。 */
73
+ function legacyDoc() {
74
+ try {
75
+ return YAML.parseDocument(readFileSync(settingsPath, 'utf8'))
76
+ } catch {
77
+ return new YAML.Document()
78
+ }
79
+ }
80
+
81
+ function legacyApply(ops) {
82
+ const doc = legacyDoc()
83
+ let changed = false
84
+ for (const op of ops) {
85
+ const path = [NS, ...op.path]
86
+ if (op.op === 'unset') {
87
+ if (doc.getIn(path)) { doc.deleteIn(path); changed = true }
88
+ } else {
89
+ const next = YAML.parse(YAML.stringify(op.value ?? null))
90
+ if (YAML.stringify(doc.getIn(path) ?? null) !== YAML.stringify(next)) {
91
+ doc.setIn(path, next)
92
+ changed = true
93
+ }
94
+ }
95
+ }
96
+ if (changed) writeTextAtomic(settingsPath, String(doc))
97
+ return { ok: true, mode: 'legacy', changed }
98
+ }
99
+
100
+ /** 新宿主路径:Settings forms seam(写落 profile patch,立即生效)。 */
101
+ function formsAvailable() {
102
+ const s = svc()
103
+ return !!s && typeof s.mutate === 'function'
104
+ }
105
+
106
+ function describeEntry() {
107
+ const s = svc()
108
+ if (!s || typeof s.describe !== 'function') return null
109
+ try {
110
+ const list = s.describe() ?? []
111
+ const hit = list.find((d) => d && d.ns === NS) ?? null
112
+ if (hit) revision = hit.revision
113
+ return hit
114
+ } catch (err) {
115
+ attachError = `describe failed: ${err?.message ?? err}`
116
+ return null
117
+ }
118
+ }
119
+
120
+ async function formsApply(ops) {
121
+ const s0 = svc()
122
+ if (s0 && s0.writable === false) {
123
+ return { ok: false, mode: 'forms', error: 'SETTINGS_NOT_WRITABLE(当前 profile 不接受表单写入)' }
124
+ }
125
+ for (let attempt = 0; attempt < 4; attempt++) {
126
+ // 每轮都重新活取服务 + 重读 revision:宿主可能在我们两次调用之间
127
+ // 重载 profile 并替换 Settings 实例(见 svc() 注释)。
128
+ const s = svc()
129
+ if (!s || typeof s.mutate !== 'function') {
130
+ return { ok: false, mode: 'forms', error: 'SETTINGS_SERVICE_GONE(宿主重载后 settings 服务不可用)' }
131
+ }
132
+ const entry = describeEntry()
133
+ const expected = entry?.revision ?? revision ?? undefined
134
+ // 写前比对:同值不写。重复写会白涨 revision 并触发一次 profile 重载,
135
+ // 目录轮询/启动重铺这类高频路径上会抖成 reload 风暴。
136
+ const pending = ops.filter((op) => {
137
+ const cur = getPath(entry?.value, op.path)
138
+ if (op.op === 'unset') return cur !== undefined
139
+ return stable(cur) !== stable(op.value ?? null)
140
+ })
141
+ if (pending.length === 0) return { ok: true, mode: 'forms', changed: false, revision: entry?.revision ?? null }
142
+ try {
143
+ await s.mutate(NS, pending, expected)
144
+ const after = describeEntry()
145
+ return { ok: true, mode: 'forms', changed: true, revision: after?.revision ?? null }
146
+ } catch (err) {
147
+ const code = err?.code
148
+ if (code === 'SETTINGS_CONFLICT' && attempt < 3) continue // revision 过期 → 重取服务重读重试
149
+ return { ok: false, mode: 'forms', error: `${code ?? 'ERR'}: ${err?.message ?? err}` }
150
+ }
151
+ }
152
+ return { ok: false, mode: 'forms', error: 'SETTINGS_CONFLICT: 四次重试仍未拿到最新 revision' }
153
+ }
154
+
155
+ return {
156
+ /**
157
+ * 挂到宿主:注入 settings 服务,并按能力声明页面策略。
158
+ * 0.1.7+:configure({auto:false}) —— 本插件自带设置卡,不要让宿主按
159
+ * Config schema 自动生成一个页面(pi-ai 自己也这么做)。
160
+ * ≤0.1.6:register(ns, schema) —— 旧 `settings.plugin.item` 卡片派发靠这个
161
+ * 命名空间声明(宿主 api-proxy `settings.describe` 出清单)。
162
+ */
163
+ attach(ctx) {
164
+ ctx.inject(['settings'], (sctx) => {
165
+ settingsCtx = sctx ?? null
166
+ service = sctx?.settings ?? null
167
+ fiber = ctx.fiber ?? null
168
+ const s = svc()
169
+ if (!s) { attachError = 'settings service unavailable'; return }
170
+ if (typeof s.configure === 'function') {
171
+ try {
172
+ disposeConfigure = s.configure({ auto: false }, fiber) ?? null
173
+ } catch (err) {
174
+ attachError = `configure failed: ${err?.message ?? err}`
175
+ log(`[dsh-tap] settings configure(auto:false) failed: ${err?.message ?? err}`)
176
+ }
177
+ } else if (typeof s.register === 'function' && schema) {
178
+ try {
179
+ s.register(legacyNamespace, schema)
180
+ } catch (err) {
181
+ attachError = `register failed: ${err?.message ?? err}`
182
+ log(`[dsh-tap] settings namespace register failed: ${err?.message ?? err}`)
183
+ }
184
+ } else {
185
+ attachError = 'settings service exposes neither configure() nor register()'
186
+ }
187
+ })
188
+ },
189
+
190
+ dispose() {
191
+ if (disposeConfigure) { try { disposeConfigure() } catch {} disposeConfigure = null }
192
+ settingsCtx = null
193
+ service = null
194
+ },
195
+
196
+ /** 当前选路:'forms'(dsh 0.1.7+)/ 'legacy'(≤0.1.6 或无 settings 服务)。 */
197
+ mode() {
198
+ return formsAvailable() ? 'forms' : 'legacy'
199
+ },
200
+
201
+ /** 宿主 settings 服务是否挂上(与 mode 分开报:无服务时仍走 legacy 文件层)。 */
202
+ servicePresent() {
203
+ return !!svc()
204
+ },
205
+
206
+ get lastError() { return lastError },
207
+ get attachError() { return attachError },
208
+
209
+ /** 诊断:新 seam 到底给了什么(升级排查用,经设置路由 ?probe=host-config 暴露)。 */
210
+ probe() {
211
+ const s = svc()
212
+ const entry = describeEntry()
213
+ const api = s ? Object.getOwnPropertyNames(Object.getPrototypeOf(s)) : []
214
+ return {
215
+ mode: this.mode(),
216
+ servicePresent: !!s,
217
+ formsWritable: formsAvailable(),
218
+ api: api.filter((n) => !n.startsWith('_') && n !== 'constructor'),
219
+ writable: s?.writable ?? null,
220
+ documentPath: (() => { try { return s?.documentPath ?? null } catch { return null } })(),
221
+ entryFound: !!entry,
222
+ entryNs: entry?.ns ?? null,
223
+ entryRevision: entry?.revision ?? null,
224
+ autoGenerate: entry?.autoGenerate ?? null,
225
+ applies: entry?.applies ?? null,
226
+ valueKeys: entry && entry.value && typeof entry.value === 'object' ? Object.keys(entry.value) : null,
227
+ providerIds: entry?.value?.providers && typeof entry.value.providers === 'object'
228
+ ? Object.keys(entry.value.providers)
229
+ : null,
230
+ schemaKeys: entry?.schema && typeof entry.schema === 'object' ? Object.keys(entry.schema).slice(0, 12) : null,
231
+ describeCount: (() => { try { return (s?.describe?.() ?? []).length } catch { return null } })(),
232
+ allNamespaces: (() => {
233
+ try { return (s?.describe?.() ?? []).map((d) => d?.ns).filter(Boolean) } catch { return null }
234
+ })(),
235
+ attachError,
236
+ lastError,
237
+ legacySettingsPath: settingsPath,
238
+ legacyExists: (() => { try { readFileSync(settingsPath, 'utf8'); return true } catch { return false } })(),
239
+ }
240
+ },
241
+
242
+ /**
243
+ * 有效 providers 视图(对账/去重判定用)。
244
+ * forms:读 describe 的 live value(含 patch 层与 profile 覆盖的合成结果);
245
+ * legacy:解析 settings.yaml 的 llm-pi-ai.providers。
246
+ */
247
+ readProviders() {
248
+ if (formsAvailable()) {
249
+ const entry = describeEntry()
250
+ const p = entry?.value?.providers
251
+ if (p && typeof p === 'object') return p
252
+ return {}
253
+ }
254
+ try {
255
+ return YAML.parse(readFileSync(settingsPath, 'utf8'))?.[NS]?.providers ?? {}
256
+ } catch {
257
+ return {}
258
+ }
259
+ },
260
+
261
+ /**
262
+ * 应用一组 ops(path 相对 llm-pi-ai 配置根,例如 ['providers','trae'])。
263
+ * 永不 reject:失败落 lastError 并在结果对象里带 error。
264
+ */
265
+ async applyOps(ops) {
266
+ if (!Array.isArray(ops) || ops.length === 0) return { ok: true, mode: this.mode(), changed: false }
267
+ let res
268
+ try {
269
+ res = formsAvailable() ? await formsApply(ops) : legacyApply(ops)
270
+ } catch (err) {
271
+ res = { ok: false, mode: this.mode(), error: `${err?.message ?? err}` }
272
+ }
273
+ if (!res.ok) {
274
+ lastError = res.error
275
+ log(`[dsh-tap] host config write failed (${res.mode}): ${res.error}`)
276
+ } else if (lastError) {
277
+ lastError = null
278
+ }
279
+ return res
280
+ },
281
+ }
282
+ }