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,105 @@
1
+ # 踩坑记录全本(每条都付过学费)
2
+
3
+ > 本文是踩坑记录全本(2026-08-29 自 AGENTS.md 同名章节逐字迁入),编号 #1–#25 保持稳定——代码注释与 wiki 里的「踩坑 #N」引用即指向本文。AGENTS.md 只留「编号 + 标签」超短表;新坑追加在本文末尾(取新编号),并在 AGENTS.md 踩坑速查加一个 `#N` 标签——两侧编号集合的连续性由 `scripts/verify-agents-md.mjs` 对账锁定(见 #52)。
4
+
5
+ 1. **bundle 入口必须 `insert`**:dsh 对声明 `dsh.bundle` 的包只应用 patch、不加载 JS;必须在 cordis.patch.yml 里 `- insert: [{id, name}]` 才会执行 `apply()`。
6
+ 2. **双 settings 服务实例**:bundle 入口侧与 Web 客户端连接侧的 `settings` 服务互不相通,命名空间注册对设置页不可见——所以设置卡走自有 webServer 路由(dsh-html-visualizer 同模式)。别尝试改回官方 installSettingsSection。
7
+ 3. **客户端模块格式**:`window.__ModuleLoader__.load({id, factory})`,factory 内 `require('react')`;包需声明 `exports["./client"]` 与 `dsh.client.manifest`。手写 `React.createElement`(无构建步骤)。
8
+ 4. **React hooks 规则**:`useSyncExternalStore(scope.subscribe,…)` 必须传绑定包装(裸方法引用丢 `this`);hook 不能在条件分支后调用。改 UI 后必跑浏览器回归。
9
+ 5. **合成事件**:脚本派发的原生 blur 不触发 React onBlur,用真实 `input.blur()`;受控 checkbox 可能双 change,写操作加去抖。
10
+ 6. **模型同步的纯净态**:`llm-pi-ai.providers.codebuddy.models` 写入 `~/.dsh/settings.yaml` 即时生效(选择器实时刷新);但状态归零时必须**删除**该覆盖层,否则陈旧清单遮蔽插件更新的静态模型。禁用"目录新增"模型只删 extra、**不写 disabled**(否则永远非纯净)。**v0.8 G4 修订**:动态目录(`/v3/config` 启动同步)存活期间镜像**恒铺**——镜像内容每次启动随网关刷新,不属"陈旧遮蔽";仅当无动态目录且无 disabled/extra 时才删覆盖层(`syncModelsToDshSettings` 的 pristine 判定含 `dynamicCatalog == null`)。
11
+ 7. **错误提示要带原因**:catch 里只写"(网络)"曾把 `reload is not a function` 误导成网络问题排查了一圈。
12
+ 8. **dsh web 增删插件后必须重启**进程才会刷新启动清单(运行中的清单是内存缓存)。
13
+ 9. 本插件 JS 不能 import `@deepseek-ai/*`(除非装进自己的 node_modules——加载器按插件路径解析);provider 接口用鸭子类型零依赖实现,仅 `schemastery`/`yaml` 两个运行时依赖(锁 dsh 0.1.0-rc.6 线)。
14
+ 10. **验证队列/代理行为必须断言"响应完成"**:首字节/时间戳看起来都对、连接却永远不收尾——v0.5.5 的 `SessionLimiter.release()` 在计数归零且队列非空时直接 return,limit=1 下同会话第二个请求永久挂起(0.5.6 修复,回归锁在 verify-bridge.mjs)。同类教训:桥的出站头是**重建**的,"保留调用方已设头"若只跳过注入而不转发,等于静默丢弃(同为 0.5.6 修复,改逐头保留+补全)。
15
+ 11. **launch-environment 是启动时不可变快照**:插件运行时写 `process.env.X` 对 dsh 凭据解析**无效**(`createLaunchEnvironmentSnapshot` 冻结于任何 config entry 挂载前)。要让 pi-ai 在无 `apiKeyEnv` 时也发请求,正解是 patch 里放**静态哨兵 Authorization 头**:pi-ai `getClientApiKey` 见 authorization 头即放行(返回 "unused"),OpenAI SDK 的 `defaultHeaders` 合并顺序在 `authHeaders` 之后,哨兵因此真正上线,桥再逐请求替换。另注意 dsh-llm-pi-ai 的 `requestHeaders` 会剥掉与 attribution 冲突的头——静态 `User-Agent` 永远到不了网关(被 dsh 自己的 UA 替换),`/v2` 不校验 UA 才无感。
16
+ 12. **schemastery 不物化无默认值字段**:`Config({})` 的键集合不含 `activeApiKey` 这类无 default 的字段——曾用 `hasOwnProperty(Config({}), key)` 当写入白名单,切换活跃 Key 被静默丢弃。白名单一律查显式清单(`SETTINGS_FIELDS`),别查解析产物的键。
17
+ 13. **ctx.tools 注册的 schema 必须是最终 JSON Schema**:defineTool 的"简写→JSON Schema"转换器在宿主包内,插件 import 不到(见 #9);`parameters`/`output.schema` 直接手写完整 JSON Schema 即可正常注册。
18
+ 14. **provider 工厂之间传的是 settings 函数,不是解析结果**:makeSearchProvider/makeFetchProvider 曾把 `settings()` 的对象传给期望函数的 `callAgentTool`,每次搜索/抓取抛 `TypeError: settings is not a function`——对外就是无网关 code 的"模糊报错"。这类跨层签名漂移启动日志看不出来,只能靠端到端真实调用暴露。
19
+ 15. **UI 原生化的正确姿势**:宿主**没有全局可复用 class**(第一方与 dshmarket 全是 CSS Modules hash 类名)。复用 = require 平台 seed 模块 `@deepseek-ai/dsh-client-ui-primitives`(Button/Input/图标;try/catch 失败回落原生元素,卡片不白屏)+ 注入单个 `<style data-plugin="…" data-plugin-css="…">`(cbc- 前缀类,与第一方同协议,模块加载器可按插件归因/热清理)+ 全部颜色走 `--dsw-alias-*` tokens(深色主题靠 `body[data-ds-dark-theme]` 下的 alias 变量自动跟随,无需自己写媒体查询)。`--dsw-alias-accent` 和 `--dsw-alias-label-error` **不存在**——写了永远走 fallback,正确名是 `state-business-primary`/`state-error-primary`。组件外壳数值抄第一方 PluginCard:radius 12、border-l2、bg-layer-3→展开 bg-layer-2、padding 14/16。
20
+ 16. **puppeteer 回归三坑**(0.7.1 重建脚本时各踩一次):a) `page.evaluate` 无法序列化 DOM 元素——返回 Element 的表达式恒解析为 `undefined`,存在性断言的 `!!` 必须写在 evaluate **内部**(外层 `!!(await evaluate(el))` 永远 false,且毫无报错);b) `setInput`(native setter + input 事件)与 `blur()` 必须分两个任务——同一任务内 blur 的 commit 闭包读到的还是旧草稿,静默不保存;c) 涉及"重置"按钮的断言先 `normalizeField` 把字段归一到 schema 默认值——中断的 run 会留下文件层覆盖,"重置"回的是默认值而不是 run 起始值,基线错了断言必挂。
21
+ 17. **`server.listen` 不挂 error 监听 = 宿主进程炸弹**:桥绑 3901 遇 EADDRINUSE(第二个 dsh 实例——`dsh web --help` 都会加载插件抢绑)时 unhandled 'error' 事件直接崩掉整个 dsh。listen 前挂 `server.on('error')` 降级为告警 + 状态字段(`bridgeRuntime`),绝不抛出。同类教训:轮询型 UI 断言必须先等"正在读取"消失再读文本(step25 首跑 3 连挂就是首次 pull 未返回);轮询断言的基线计数器要和文本显示的口径一致——step25 曾拿全量 `totalRequests` 对比卡片"今日"计数,跨本地午夜后必然分叉、断言永不成立(文本基线应取 `usage.today.requests`)。
22
+ 18. **dsh rc.7 把 `settings.plugin.item` 槽位从 list 改成 keyed**(0.7.3 适配):tab 改为从 api-proxy `settings.describe` 读 Host 命名空间清单,按 `renderSlot(…, {entryKey: ns})` 逐个派发——**卡片想出现,必须同时满足**:宿主半 `ctx.inject(['settings'])` + `settings.register('dsh-tap', Config)` 注册命名空间(只作派发声明,读写仍走自有路由;注册是本 fiber 的 effect)+ 浏览器半注册带 `key: "dsh-tap"`。rc.6 的硬编码白名单 `WEB_SETTINGS_NAMESPACES` 与 `settings-not-exposed` 已删,第三方插件自曝配置面是官方落地的新路径。rc.6↔rc.7 兼容写法:注册项同时带 `key` 和 `id/order/label`——list 槽位只校验 `id`、keyed 只校验 `key`,多余字段都被忽略。踩坑 #2 因此**部分过时**:rc.7 起命名空间注册对设置页可见了(但官方 `installSettingsSection` 仍不是我们数据流的载体)。
23
+ 19. **"逐字节等价"若靠手写重建 = 自欺欺人**(0.7.4 破解 content_filter 事件的代价):pi-ai 会把推理模型的 system prompt 序列化成 `role:"developer"`,而此前所有"等价回放"都手写 `system`——差异字段被重建过程抹掉,导致把网关审核误判成"时变风控/按客户端形态"。正解是开 `CODEBUDDY_BRIDGE_DUMP` 抓真实请求体,再以 dump 为基准逐字段 bisect(一次翻转即定位 developer 角色)。网关对 developer/system 指令语义等价,桥直接重写即可。
24
+ 20. **共享层的运行状态必须是实例状态,不是模块状态**(0.7.5 拆 core/ 时保住的老语义):verify-rotation 靠 `import('index.js?case=A')`/`?case=B` 拿两个独立插件实例来隔离轮询游标/冷却表——若这些状态沉进 `core/rotation.js` 的模块全局,两个实例经相对路径 import 命中的仍是**同一个** core 模块(query 不传染给子导入),隔离即破。纪律:core/ 一律导出工厂/类(`new KeyRotator()`、`createUsageMeter()`、`createBridge()`),实例在 index.js 模块作用域各创建一次;providers/codebuddy/ 同理(`createCodeBuddyProvider` 闭包持有 refresh 单飞锁/pending 态/quota 缓存)。写跨层测试断言轮询顺序前先想清楚游标在第几个请求上(verify-core-generic R4 用全新 rotator 钉死游标)。
25
+ 21. **settings.yaml 用户层一个坏 provider 块 = 全层连坐**(G6 实测 2026-08-19):手写 `api: bogus` 的 provider 进 settings.yaml 后重启,**整个 llm-pi-ai 用户层被丢弃**(dsh-settings publish/解析 catch 后保持上一份好值/回退 base)——patch 层的 codebuddy 幸存,用户手写的 qianwenai/kimiclaw/kimi-coding 全灭。所以插件写 provider 块必须"本地校验(id 正则/api 枚举/URL)+ 实测 GET /models 后才落盘"。同机制其余事实:provider 块可纯 settings.yaml 覆盖层新增(`z.dict(profile)` 深合并、chokidar 热加载原地换路由、**免重启**);凭据只有 `apiKeyEnv` 一个字段(无字面量 apiKey),每请求活解析、来源序 = 启动环境快照 > `~/.dsh/.credentials.yaml`(chokidar 活层,**文件必须 0600** 否则凭据缝抛错)> .env(冻结);rc.7 的 Models 页自带 CustomProviderCard 就是这套(settings.mutate + credentials.set RPC,key ref 惯例 `<ROUTE>_API_KEY`)——插件因踩坑 #2 走自写文件,同一落点同一形状。
26
+ 22. **GitHub 逆向项目是"历史参照"不是"协议真相"**(v0.8.x 接 Trae 时验证):linqiu919/trae2api 给了完整旧协议(/api/ide/v1/chat + x-ide-token + 无 DeviceProof 的刷新),但 2026-08 客户端已换成 /api/agent/v3 任务制 + mchost 网关 + ECDSA 设备签名——**盲抄旧仓库必然失败**。正确姿势:GitHub 定方向(端点族/信封形态/历史语义)→ 本机二进制 strings 提取当前字段 → 无凭据在线探测校准(401/400 错误信封就是免费指纹,不需要任何真实账号)→ 带凭据联调只留一步(probe-trae-live.mjs)。
27
+ 23. **别找存量 token,没有**(v0.8.x 实测排除法):Trae 登录令牌不在 state.vscdb(无 `secret://` 键,只有无关的 mcpOAuth)、不在 Windows 凭据管理器(cmdkey 无条目)、harness 数据库 AES 加密(node:sqlite 直接 "file is not a database")——令牌只在 Electron 内存/加密存储。唯一正路是**自持设备密钥走完整设备流**:自己生成 P-256 密钥对、DeviceInfo.DevicePublicKey 上报注册,refresh 的 DeviceProof 自己签(traework-cn.md 判断 #5"仅拿 refresh token 不足以复刻"只否定偷 IDE token 的路线,不适用自注册)。
28
+ 24. **官方本地 harness 是懒启动的黑盒**:IDE 常驻(13 个进程)≠ harness 在线(:40005 无监听,AI 面板交互才拉起);启动器 x64/run_helper.exe 裸拉无参即退(参数/握手未知)。想"驱动官方 harness 免协议逆向"的路线卡在两处未知数;**直连云端**路线只卡"一次用户登录"——自主可推进性决定架构取舍。另外 harness 的 Rust axum 路由(/api/v1/chat/start_chat 等)与云端路由(/api/agent/v3/*)在 strings 里混在一起,提取时必须按命名空间分辨,别把本地 RPC 当云端端点。
29
+ 25. **第二上游接入的"路由存在性管理"模式**(0.8.7 / dsh 0.1.1-rc.2 终版;2026-08-23 初版"静态基线+空数组遮蔽"已被上游杀死):dsh-llm-pi-ai 适配层在 0.1.1-rc.2 重写后,**非目录路由的空 models 清单在 apply 时直接 throw**("resolves no models",连坐整棵 llm-pi-ai 纤维),热加载路径也被 onChange 拒绝并保持旧路由——"铺空数组禁用通道"彻底失效。正解:**patch 完全不带 trae 块**(不定义路由就没有校验),settings.yaml 镜像恒铺**整块**——启用+已同步铺完整块(displayName/api/baseURL/headers/models,baseURL 跟随 traeBridgePort),禁用/未同步/全部模型禁用**删 providers.trae 整块**(路由消失、选择器隐藏、免重启;无 patch 基线即无"删路径回落静态清单"问题)。codebuddy 通道同理加了全禁用防护(禁用最后一个有效模型被拒)。升级 dsh 到 0.1.1-rc.2 前必须先清理 ≤0.8.5 形态的 trae 残块(只带 models 路径缺 baseURL,llm-pi-ai 会先于插件 apply 炸掉);同类手写残块(仅 apiKeyEnv 无 models 的 provider 块)同样致命。历史版本:同一个 llm-pi-ai 行内两个 provider 并列(codebuddy+trae)曾是 patch 的正常写法,0.8.7 起 trae 移出 patch 后不存在此形态。
30
+ 26. **设置接口要按"响应整体"审脱敏,不能只盯主要字段**(2026-08-17 代码审查发现,0.8.7 已修复):`settingsView()` 的 `value.apiKeys` 一直只回 `{name, masked}`,但同一响应的 **`user` 字段直接透传 `readFileLayer()` 原始文件层**——`user.apiKeys[].key` 是明文 `ck_` Key,经 `GET /dsh-tap/settings` 下发浏览器,本机任何能访问 127.0.0.1 端口的页面/进程都能读走全部 Key(OAuth 令牌走独立文件本就不回传,漏的是 api-key 模式)。修复:`user.apiKeys[].key` 同样过 `maskKey`——前端 `overridden()` 只用 hasOwnProperty 判覆盖,不依赖明文。教训:给设置/状态接口新增回传字段时先问"这个字段里有没有 secret"。
31
+ 27. **React 组件函数体的普通局部变量不跨渲染;checkbox 去抖终版 = useRef 同值去重,不用时间窗**(2026-08-17 审查发现;2026-09-03 标签页重设计时修复并二次纠偏):client.js 模型勾选的双 change 去抖(踩坑 #5 的 guard)曾是组件体内的 `var lastToggle = {}`——**每次渲染都重建**,去抖从不生效(前半段教训:跨渲染易变状态用 `useRef`)。0.9.1 改 `useRef` + 400ms 时间窗后立刻翻出**后半段**:勾选往返(POST modelSetEnabled + reload GET)本机可以快过 400ms,第二次**真实**点击被去抖吞掉,step20 重勾往返超时。终版方案 = **同值去重**:`lastSentRef.current[id] === enabled` 时丢弃——双 change bug 的重复事件携带与上次已发送相同的目标态,而真实切换必然反值;无时间参数、无漏杀窗口。Trae 模型启停同构复用。
32
+ 28. **HTTP 请求体禁止 `string += buffer` 逐分片拼接**(2026-09-03 v4-flash"缓存命中率下降快"根因,完整证据链 docs/diagnosis-cache-decline.md;**0.9.2 已修复**——core/bridge.js / providers/trae/gateway.js / index.js 设置路由三处 listen 均改 `chunks.push` + `Buffer.concat` 一次解码,verify-bridge [10] 分片用例回归锁定案):`core/bridge.js` 的 `rawBody += c` 对每个 TCP 分片**独立隐式 utf8 解码**,跨分片的多字节中文字符被替换成 3×U+FFFD——分片边界逐请求随机 → 出站前缀逐请求漂移 → 网关内容寻址缓存只能命中到损坏点(hit 值与 dump 分叉字节数定量对应)。现场签名:连续请求共享前导消息骤减、消息体长度 ±2、FFFD 数=3;重释了存量"40k+ 保留不稳/命中波动/吸附快照"全部历史归因(都发生在经桥路径上;直连探测 24 发全 99.3%、TTL ≥600s)。三重教训:a) body 组装一律 `chunks.push(c)` + `Buffer.concat().toString('utf8')` 一次解码(同模式还有 trae/gateway.js 与 index.js 设置路由);b) **mock 回归用单块写 body 测不出这类 bug**——verify-bridge 漏网 8 个版本,必须加分片写入用例断言 bodySha 与整块一致;c) "命中率异常"先怀疑自己改没改字节(踩坑 #19 的镜像教训:那次是手写重建抹掉差异,这次是传输层制造差异),DUMP 抓真实字节对连续请求做前缀 diff 是第一动作。附带后果:模型收到的中文上下文本身带乱码(正确性问题,不只是计费)。
33
+ 29. **net.Socket 不挂 `data` 监听 = paused 流,服务端 FIN 后 `close` 永不派发**(2026-09-03 verify-bridge [10] 分片用例首跑挂死定位):测试用原生 socket 写分片请求体、只挂 `error`/`close` 等 close 再 resolve——套件在 [10] 处挂死,而桥取证日志(CODEBUDDY_BRIDGE_LOG)证明请求完整过桥、上游 200、out 记录落盘,**被测物无罪,测试自身挂起**。机制:无 `data` 监听的 socket 停在 paused 模式,响应字节无人消费,收到 FIN 也不派发 `end`/`close`;加一行 `sock.on('data', () => {})` 后 5ms 即 close(二分验证:唯一差异就是这行)。教训:a) 原生 socket 客户端**必须**消费响应(哪怕空监听器),否则 promise 靠 `close` resolve 的测试全部永久挂起且无报错;b) "套件挂死"先开取证日志区分被测物与测试自身——证据显示请求全链路完成时,嫌疑转向测试代码;c) 与踩坑 #10 同族("断言响应完成"的测试自身别制造永不完成的连接),但那次是产品 bug、这次是测试 bug,互为镜像。
34
+ 30. **dsh 0.1.5 三处破坏性变更**(2026-09-11 升级 0.1.1-rc.2 → 0.1.5-rc.1 适配实录):a) **web 入口 token 闸**——无 `?token=` 一律 401("dsh web authentication required"),token 只在 `dsh web` 启动行打印一次,`?token=` 换签名 cookie 后同源免带;浏览器回归经 `DSH_WEB_TOKEN` 环境变量传入(_helpers.js 的 `ENTRY`)。b) **`settings.plugin.item` 槽位改为设置页运行时声明**(注册条目的 children 链:settings.section → settings.plugins.tab → settings.plugin.item)——apply 时直接 `slots.register` 抢跑在声明之前,卡片**静默不出现、无任何报错**;正解 = 官方内置卡/dshmarket 同形态 `slots.inject("settings.plugin.item", () => slots.register({name, key, inject}, Card))` 等声明落地再注册,keyed 槽只校验 `key`(id/order/label 是 rc.6 列表槽遗产,0.1.5 路径已去)。诊断路径:`settings/describe` RPC 看 Host 服务命名空间 ∩ 槽位注册两侧账本。c) **RPC `llm.providers` 改名 `llm/listProviders`**——payload 必须 `{args:{}}`("exactly one plain-object args field"),返回 `[{id,name}]` 只含 active。附带:清 3080 占用时 `pkill -f 'dsh web'` 会连自己的 shell 一起杀(命令行自匹配),用 `pkill -f 'bin/dsh [w]eb'`。
35
+ 31. **状态文件落盘必须 tmp+rename 原子写**(2026-09-11 深度审计发现,0.9.5 当轮修复):全部落盘曾用 `writeFileSync` 原地 O_TRUNC 直写——崩溃 mid-write 后 JSON 侧 `readJson` 静默回 `{}`(apiKeys/OAuth 令牌/ECDSA 设备私钥无声丢失);YAML 侧更阴险:**截断的 settings.yaml 往往仍是合法 YAML**——dsh 以残缺配置静默启动(模型清单/路由整块消失),比响亮失败更难排查。修复:core/json-store.js 新增 `writeTextAtomic`(`${path}.${pid}.tmp` + `renameSync`,失败扫尾 unlink),`writeJson` 与 index.js 全部 8 处 settings.yaml/.credentials.yaml 写入点改走原子写;0600 随新 inode 天然生效——旧纪律"mode 只在创建时适用、既有文件要 chmod 补断言"随之消解(原子写每次落新 inode)。
36
+ 32. **同值去重表必须失败销账**(2026-09-11 深度审计发现,0.9.5 当轮修复;踩坑 #27 的配套纪律):#27 的 useRef 同值去重在 POST **发出前**记录目标态,而失败路径(!res.ok 与 catch)只 setErr 不回滚——失败后受控 checkbox 随未变的服务端真值弹回原态,用户再次点击产生与已记录相同的目标态 → 被去重吞掉,该模型在此面板生命周期内("隐藏不卸载"从不重建)**同向操作永久失效**,只能刷新页面。修复:4 个失败分支(CodeBuddy 模型行 + Trae 模型行各 2)各加 `delete lastSentRef.current[id]`。通式教训:凡"发送前记账、靠记账挡重复"的去重/防抖机制,失败分支必须销账。
37
+ 33. **两条异步生命周期纪律**(2026-09-11 深度审计发现,0.9.5 当轮修复):a) **fire-and-forget promise 必须 `.catch` 落地**——codebuddy OAuth 轮询 `poll()` 裸奔,轮询体内 `writeAuth`(json-store)同步抛(ENOSPC/EACCES/EROFS)穿透 `finally` 成 unhandledRejection,Node ≥15 默认 throw 直接崩整个 dsh 进程;修复 `poll().catch` 落 `oauthPending.error`。b) **生命周期"已在目标态"早退条件必须计入失败态**——syncBridge/syncTraeBridge 的 `stopBridge && runningPort === port` 早退在 listen 失败(停-起竞态 EADDRINUSE)后恒真,桥/网关**永久 wedge 到重启**(主聊天路径静默中断);早退加 `!runtime.lastError` 后失败态放行重试即自愈。
38
+
39
+ 34. **dsh 0.1.6 拆除 `settings.plugin.item` 槽,`slots.inject` 对未声明槽静默等待 → 升级后卡片"消失且零报错"**(2026-09-19 升级 0.1.5-rc.2 → 0.1.6-alpha.2 实录;dshmarket 同受害):插件配置 UI 从设置页搬进新的 Plugin Manager 面板(侧栏「插件」→ `main` keyed 面板),槽位换成 `plugins.item`(+ `plugins.bundle.config` / `plugins.row.config`),boot 即声明、外部 `slots.inject('plugins.item', cb)` 立即回调。旧槽名在全仓 260 个包里一处不剩——#30 的"inject 等声明再注册"写法因此对旧槽永远等待,无超时无报错。适配(0.9.7):**双槽注册**(`plugins.item` 新槽 + `settings.plugin.item` 旧槽回退,哪个声明走哪个);新槽 owner props 是 `{view:'summary'|'page'}`——summary = 标题下一行简介、page = 完整表单(页面自带标题/图标/返回 crumb),卡片新增 `embedded` 模式(常开、不画自有折叠头部);register 字段 `id` 必填、`label` 会被页面用作卡片标题(`key` 多余但无害)。注册处:`dsh-client-ui-plugin-manager/lib/client.js` ItemCard/ItemDetail(`renderSlot({view})` 派发)。附带:0.1.6 的 `dsh.client.inject` 只做到达排序、未知名称静默跳过——`@deepseek-ai/dsh-client-runtime` 已不存在(从 package.json 删掉);`DSH_WEB_TOKEN` 环境变量闸移除(URL `?token=` 仍在)。教训:宿主槽位是**无版本契约的运行时表面**,升级 dsh 后第一动作 = grep 新安装产物里的槽名清单(`CLIENT_SLOT_API` keys)对账注册侧。
40
+
41
+ 35. **Windows Git Bash 的 `curl -d` 中文按 ANSI(GBK) 编码发出**(2026-09-20 qoder 网关联调假象):curl.exe 是原生 Windows 程序,argv 过 CRT 时落系统代码页——`-d '{"content":"中文"}'` 实际发出 GBK 字节,服务端按 UTF-8 解出乱码(模型自述输入是 mojibake),差点误判成网关编码 bug;同一中文经 Node fetch 走 UTF-8 完全正常。教训:Windows 上含非 ASCII 的 HTTP 测试一律用 Node fetch(或写文件 `--data-binary @f`),不用 curl -d 内联中文;看到"中文变乱码"先怀疑测试工具链,不是被测物(踩坑 #19/#28 同族:先怀疑自己这侧的字节)。
42
+ 36. **wasm-bindgen 的 `RequestResult.headers` 是 JS Map,`{...map}` 展开得空对象**(2026-09-20 手写胶水联调):Qoder COSY wasm 的签名结果 headers 由 wasm 侧 `new Map()` + set 组装——直接展开/赋值给 fetch headers 等于**一个头都不带**(无 Authorization 无 Cosy-*),服务器直接断连且无错误响应(`UND_ERR_SOCKET other side closed`),表象像网络/TLS 问题。正解 `Object.fromEntries(map)`。同类:手写胶水时 `ptr >>> 0 + len` 因优先级变成 `ptr >>> len`——位运算永远最后写括号。
43
+ 37. **上游对未知/臆造模型 key 静默改派 auto,响应的 model 字段与计费是哨兵**(2026-09-22 qwen3.8flash 诊断实录):请求 `qmodel_38flash`(按命名规律猜的 key)返回 200、正文正常——乍看"Flash 可用",实际响应 chunk `model` 恒 `"auto"` 且 billable:false(上游把未知 key 改派 auto 节点);真 key `qfmodel` 当时走真实节点(该节点正挂着带内 400)。代价:误判方向,排查被带偏近一小时。教训:a) 探测模型可用性必须用**真实目录 key**(目录响应 key 字段 / settings.yaml 镜像块 id,不是 display_name、不是命名规律);b) 响应 model ≠ 请求 model = 改派信号,先核对再下结论(踩坑 #22 Trae 改派同族);c) 计费字段是辅证——真实节点 billable:true,auto 改派 billable:false。
44
+ 38. **测试 fixture 写死"未来日期" = 定时炸弹**(2026-09-22 verify-qoder 炸雷实录):真机捕获形态 mock 的 `expires_at: '2026-09-20T…'` 编写日(09-19)合法,断言"有效期落在未来"当日全绿;09-21 起 fixture 落在过去、断言必红,与代码质量无关,极易误判成回归(修好前 suite 红了一天多)。教训:凡时间断言的 fixture 一律相对当前时刻(`new Date(Date.now()+Δ).toISOString()`),禁写绝对日期;"昨天还绿的套件今天红"先查 fixture 里的时钟。
45
+ 39. **pi-ai 会丢弃 `stopReason=error/aborted` 的 assistant 消息、却保留它产出的 toolResult → 出站带孤儿 tool 消息,严格上游 400**(2026-09-22 Qoder 通道 `provider_error` 根因;插件侧已在网关出站口修复,verify-qoder [18] 锁定案):`dsh-llm-pi-ai` 背后的 `@earendil-works/pi-ai` `transform-messages.js` 第二遍处理里,`if (assistantMsg.stopReason === "error" || assistantMsg.stopReason === "aborted") continue` 整条删掉该 assistant(注释理由:中断轮次含半截推理/工具调用,重放会触发 API 错误),但它上一轮的 tool 结果消息**照旧进 params**;`openai-completions.js` 的 `convertMessages` 于是产出 `[system, user, tool, user]`——第三条 `role:"tool"` 没有前置 `assistant.tool_calls`。离线复现(`convertMessages` 直接喂 stopReason=error/aborted 的历史)100% 命中该形态。上游 OpenAI 兼容面严格校验即回 400(Qoder 包成 `provider_error` + `details` 内层文案,见 gateway-facts Qoder 节)。**触发场景 = 任何失败/中断的工具轮之后**:一次 400/5xx/中断把当轮 assistant 写成 `stopReason=error/aborted`(dsh-llm-pi-ai 的 `case "error"` → `mapStopReason(event.error)`),**该消息在历史里永久存在**,此后每次请求都带孤儿 tool 结果 → **自续循环**(这也解释了"报错后怎么点都报同一个错",直到插件侧修复兜住)。教训:a) 翻译网关/透传桥出站前要对 `messages` 做**不变量体检**(tool 必须有前置 tool_calls、tool_calls 必须有结果),不能默认宿主序列化器输出合法;b) 宿主的"容错删除"往往只删一半——跨包组合(pi-ai 删 assistant + 我们发 tool)出的错,报错文案却指向模型侧,定位必须回到"我们到底发了什么字节"(踩坑 #19/#28 同族);c) 上游容错面按模型家族分裂(auto 容忍孤儿 tool、dmodel/kmodel/mmodel 拒绝),**"换个模型能跑"不是协议没问题的证据**。**(09-22 当日修正:孤儿只是同一条报错的次要触发;主因是 #41 的 `content:null` 不可见——宿主对**每个**工具轮都这么发,且本条首版修复补的桩自己就是 `content:null`,于是"修完仍报同一条错"。定案见 #41。)**
46
+
47
+
48
+ 40. **客户端 transcript/日志里的 usage 是加工后的记录,不是线缆帧;计费归因判别必须让"实验量级 × 计数器分辨率"匹配**(2026-09-22 Qoder 用量统计课题实录,当日自我推翻):官方客户端 transcript 里的 usage 对象带 `request_id`/`speed`/`inference_geo`/`context_usage_ratio` 且 `billable:false`,而同一请求的线缆 SSE 帧只有 tokens/credits 且 `billable:true`——客户端落盘前 enrich 过,以 transcript 字段反推线缆形态必然走偏(本轮因此误判过一轮"request_id 回显差异")。同课题的判别方法论:a) "服务端按什么记账"不要猜——构造梯度臂(裸 body / +归因字段 / +business 块 / +finish 上报 / +tracking 上报),每臂前后拉计数器做差分;b) **实验用量必须大于计数器分辨率**——`addOnQuota.used` 只显整数,0.002 级小额探测被取整吞掉,据此判"裸 body 不记账"是假阴性(当日臂 9 大额双臂复测推翻:单发 ~1 credits,45s 内 197→199→202,裸 body 同样实时入账);高精度哨兵(`creditsSummary.totalCredits` 11 位小数)分辨率高但**量的是另一个池**(会话统计汇总,非配额扣减),选错哨兵同样误判;c) "能聊天"与"被记账"与"进统计视图"是三条独立链路——配额实时扣减 / 统计视图延迟批处理 / 明细记录由归因链驱动,缺一不可互证;d) 短时间窗(≤15 分钟)不动不能证伪"延迟批处理",长窗口复测要挂定时任务收尾。
49
+
50
+ 41. **严格上游把 `content` 为 `null`/缺键的消息当"不存在"→ 宿主每个工具轮都发的 `content:null` 让工具调用必 400,而"补桩"若也用 null,修复自身就是新坏体**(2026-09-22 Qoder `provider_error` 真根因,当日推翻 #39 的定案):单变量差分——同一条**结构合法**的工具环只切 `assistant.content` ∈ {`null`, 缺键, `""`, `"我查一下"`},其余逐字节相同:`null`/缺键在 **dmodel** 报 `Messages with role 'tool' must be a response to a preceding message with 'tool_calls'`(= 用户看到的原文)、**kmodel** 报 `Invalid request: tool_call_id is not found`、**mmodel** 报 `invalid params, tool result's tool id(call_a1) not found (2013)`;`""`/文本三家全绿。`role:"tool"` 自身 `content:null` 另报 `An assistant message with 'tool_calls' must be followed by tool messages responding to each 'tool_call_id'`。宿主 `pi-ai` `convertMessages`(openai-completions.js:961 `content: compat.requiresAssistantAfterToolResult ? "" : null`)在自定义 provider 的 detectCompat 默认(该标志 false)下,把**每个纯工具轮**都序列化成 `{role:'assistant',content:null,tool_calls:[…]}`(离线用真实 `convertMessages` 复现,出站恒为此形)⇒ dsh 在严格家族上**第一次调用工具就必炸**,与有没有发生过中断无关;#39 的孤儿只是同一条报错的第二个触发。首版修复(补孤儿插 assistant 桩)桩用 `content:null` ⇒ 桩在校验器眼里同样不存在 ⇒ **修完仍报同一条错**(实测 `B_orphan_stub_null` ❌ / `B_orphan_stub_empty` ✅;经生产网关 B1–B5 全红)。修复 = `sanitizeToolPairing` 增加**可见性归一**(assistant/tool 的 `null`/缺键 content → `''`,桩用 `''`,`repaired.invisible` 入取证),Qoder 网关另折叠 `developer`→`system`(同批实测 developer 在反序列化阶段整请求被拒:`Failed to deserialize the JSON body...`);修后用**当前代码起的临时网关**打真实 dmodel,`A_call_content_null`/`B_orphan_plain`/`B_orphan_stub_null`/`C_tool_content_null` 四形态全部 400 → 200 出正文。教训:a) 报错说"tool 没有前置 tool_calls"时,前置那条**可能在、只是被上游判为不可见**——先查"哪条被丢了"再查"哪条缺了";b) 替身/补齐消息要按**上游校验器的眼睛**构造,不能按 OpenAI 规范的字面合法性(`content:null` 规范合法,严格上游不认);c) **单变量差分**(只切一个字段、其余逐字节相同)是唯一能定死因果的实验形态——多变体一起改会把 null-content 与孤儿两个触发混成一个结论,#39 正是这么误判并据此写出无效修复的;d) **在容错家族上验证修复等于没验证**(qmodel/auto/gmodel 对坏体静默容忍、多半回空正文),首版修复的"实测通过"就是在 qmodel 上做的;e) 官方客户端为何从不触发:本机取证 105 份 transcript 孤儿 result 恒 0、879 个工具回合仅 2.73% 是裸 tool_use(93.3% 带 thinking、52.8% 带 text)、252 份 `qodercli.log` 里消息数涨到 457 条仍零条配对 400——差异全在客户端历史构造纪律,不在端点或服务端校验。
51
+
52
+ 42. **上游的"能力声明"字段不等于存在对应线值;档位拼写的接受面逐模型不一致**(2026-09-22 CodeBuddy 思考强度核查,`probe-codebuddy-efforts.mjs`):目录 `/v3/config` 新形态声明 `{supportedEfforts:["low","high","max"], canDisableThinking:true, defaultEffort:"high"}`——`supportedEfforts` 直接可用(照抄拼写即线值),但 `canDisableThinking:true` **找不到任何可靠的关思考线值**:两模型**省略参数照常思考**(reason≈1.2k,与 defaultEffort 一致),显式 `off`/`disabled`/`auto` 被 HTTP 200 接受却**推理量不变**(≈1.2–1.4k),只有 `minimal`/`none` 量级下降但**两模型不一致**(glm-5.3-flash ≈0 / kimi-k2.8-preview ≈140,基线≈1k)⇒ 声明只证明"有哪些档",任何"关/禁用"语义必须实测,否则选择器上的 `Off`(映射为"省略参数")就是**假开关**。同族两条:a) **误拼档位有专用码 11150 `invalid_reasoning_effort`,但触发面不一致**——`deepseek-v4-pro` 发 `off`/`disabled`/`auto` 全 11150,而**同一拼写在 glm-5.3-flash 上被静默接受**(200、行为同默认档);"A 模型拒绝这个拼写"不能外推成"网关拒绝",档位表只列实测可接受/目录声明过的拼写。b) **目录里有 ≠ `/v2` 可路由**:`glm-4.6v`/`kimi-k2-thinking`/`minimax-m2.5`/`hy4-preview-x` 四个条目在目录里字段齐全,六臂全部 `11102 service info not found`——照目录铺静态清单会造出永远失败的模型(routing.md R-R3 再证)。方法论:档位判据取"HTTP 200 + 无带内错误帧",`off` 判据取"**省略参数**时 reasoning_content 为 0"(不是"发 off 时"——off 的语义就是省略),推理长度只是弱信号(自适应非单调:glm-5.1 low 662 / medium 666 / high 1101 / max 338),单采样不可下结论(重复采样:minimal 在 glm-5.3-flash 上 0/11/0)。
53
+
54
+ 43. **宿主把"用户配置文档"整体换成"profile 的 volatile 表单"时,旧文档只被一次性导入且导入成败取决于插件在不在场;隔离实例的结论不可外推**(2026-09-23 dsh 0.1.6-alpha.2 → 0.1.7-alpha.2 升级实录):0.1.7 删除 `@deepseek-ai/dsh-settings-file`(npm 上该版本 404),`settings` 服务改由 `dsh-settings` 提供——`~/.dsh/settings.yaml` 降级为 legacy:Loader 稳定后**一次性导入** profile 的 `cordis.patch.yml`,随即改名 `settings.yaml.imported`(此后写它等于写死文件);`ctx.settings.register(ns, schema)` **消失**(调用即 `TypeError: not a function`),改为 `configure({auto}, fiber)` + `describe/update/replace/mutate(ns=profile entry id, …, expectedRevision)`,只有 `.volatile()` 字段可编辑(0.1.7 的 `llm-pi-ai.providers` 恰好是 volatile,模型镜像/路由存在性管理才能平移)。**最贵的一课**:先在隔离 DSH_HOME(无本插件)实测,`llm-pi-ai` 段被**整段拒绝导入**——因为段里的 `codebuddy: {models:…}` 只有 models、缺 `api`/`baseURL`,而基座路由由**本插件的 patch** 提供;同一份 settings.yaml 在真实 profile 下(插件在场)导入成功(patch 1.6KB → 12KB)。⇒ "段能否通过校验"依赖组合里有没有那个插件,**隔离实例的导入/校验结论不可外推**,必须带插件复测(踩坑 #21 同族:坏块毒化整层;#19 同族:别拿重建品当真相)。同批三个部署侧必修:a) `profiles/<p>/package.json` 的 `dsh.profile.bundles` 里**上游已移除的包**(本次 `dsh-experimental-agent-team-web-profile`)会让每次启动刷 `skipping profile bundle … cannot resolve`;b) 升级后 `profiles/node_modules` 链接农场留大量断链(本次 72 条含 `dsh-settings-file`)且缺新包链接(43 条含 `dsh-config-editor`/`dsh-client-ui-primitives`),要跑 `repair-profile-links.js`(heal 只补不删,踩坑同 2026-09-13 记录);c) `dsh-launcher/package.json` 声明的 `start.js`/`stop.js` **实际不存在**(编排者是 tray.js),"先停再升"必须按命令行杀进程——否则旧进程带着已被替换的 node_modules 继续跑 = **混版半态**(懒加载到的模块是新版、已在内存里的是旧版),故障会表现为无法复现的随机错。方法论:升级前 `npm pack` 逐包 diff 接触面(本次 17 包)判定"零破坏 vs 需适配",比读 release note 可靠;写入类改动一律配 `?probe=` 诊断端点,把"选路/可写性/写入目标/revision"变成可观测事实(本次 `mode=forms`、`documentPath=…\profiles\web\cordis.patch.yml`、`autoGenerate=false` 一次看穿)。
55
+
56
+ 44. **"写入即重载"的两条时序坑:服务实例会被替换、插件会 re-apply;宿主 UI 原语按名取用必须有候选表**(2026-09-23 同批升级实录,两条都是"套件全绿之后才发现"的隐性回归):dsh 0.1.7 每次写配置都会重载 profile patch,连带两个后果——①**Settings 服务实例可能被替换**(上游 README 明写 "a late-loading or replaced Settings service picks the policy up"),而 cordis 的 `ctx.<service>` 是**实时 getter**:把实例缓存进闭包,等于拿着陈旧 revision 反复撞 `SETTINGS_CONFLICT`(实测 stderr `expected revision 5, now 6`,三次重试全败——因为重试还在问同一个陈旧实例)。正解 = 只缓存**注入进来的子上下文**,每次操作现取 `sctx.settings`;重试要重新活取 + 重读 revision,服务消失要显式报错而不是静默(verify-host-config A21–A24 锁定案)。②**插件会 re-apply**:写入→重载→自己又被 apply 一遍,于是"启动期铺一次镜像"这类代码会跑第二次;如果那次写入用的是**尚未同步完成的状态**(`dynamicCatalog == null` ⇒ 有效清单只剩静态基线 16 条),就会把上一轮写好的完整清单(28 条)**覆盖回残缺态**,且终态稳定停在残缺那份(用户可见 = 选择器少 12 个网关目录模型,**零报错**)。正解 = 启动期不要用"未同步"的状态写权威配置层;既然后续同步必然会写,那次前置写入就是纯冗余,直接删(删后 `model catalog synced` 每启动 2→1 次、冲突归零、镜像启动即 28)。**判别手法**:把"写入落点文件里的实际条数"与"插件自报的有效条数"两个数字对账(16 vs 28 一眼看穿),比读日志可靠——日志里 `SETTINGS_CONFLICT` 只出现一次,极易误判成偶发抖动。③同批还踩了**宿主 UI 原语改名**:`dsh-client-ui-primitives` 把带尺寸数字后缀的图标名全废了(`IconChevronDownOutline14` 在 265 个导出里 0 命中,改成 `…Outline{Regular,Medium,Artwork}`,且基础名 `IconChevronDownOutline` 也不导出),按名取用静默拿到 `null` → 退原生兜底、图标降级、**零报错**。教训:按名取用宿主 UI 原语一律走**候选名列表**(旧名 → 新名各变体),别假设命名约定稳定;这类改动 `npm pack` 的**文件级 diff 看不出来**,要比"导出名清单"。
57
+
58
+ 45. **"写后立刻读"要区分写完与生效完:POST 响应同步返回,但网关 `running` 由 `'listening'` 事件异步翻转 ⇒ 紧随的 GET 读到陈旧 false,UI 无轮询时假「未监听」永久驻留**(2026-09-23 设置卡前端刷新的代码审查发现,0.9.11 修复):写路径是 `writeFileLayer(nextUser) → applyLive() → sendJSON(200)`(index.js:1656),而 `applyLive()` 里 `syncBridge/syncTraeBridge/syncQoderBridge` 调完 `server.listen(port)` 就返回,`runtime.running = true` 要等 `'listening'` 回调(providers/trae/gateway.js:756)——**响应发出时监听尚未成立**。卡片在 POST 的 `.then` 里立刻 `load()`,很容易采样到这个 pre-listening 窗口,于是刚勾选"启用"的通道显示 warn「网关未监听」;而主视图 GET 没有轮询(只有 usage 分区自己 10s 轮询),这枚假 warn 会一直挂到用户收起再展开或再保存一次。窗口很窄(`'listening'` 通常下一个 tick 就翻转,而 GET 要过一次网络往返 + React 调度),但**后果是常驻的**——窄竞态配无自愈路径 = 用户眼里的稳定 bug。修复 = 保存后若 GET 视图里仍有「未监听」芯片,按 1s/2s/4s 有限次退避补拉(`settleGateways`,卸载时清 timer);真失败(EADDRINUSE 等)用尽次数后停手,warn 如实留下。锁定案 = card-regression `[14]`:mock GET/POST(POST 后第 1 次 GET 仍 `running:false`、第 2 次起 true)断言芯片自愈,**零真实写入**(跑前跑后对 `~/.dsh/codebuddy-plugin.json` 取哈希对账)。通式教训:凡状态由异步事件(listening/close/ready/flush)翻转的,同步返回的写响应**不代表新状态可读**;读侧要么轮询到收敛、要么退避补拉,别把一次采样当终态(踩坑 #10 同族:验证异步行为必须断言"完成"而不是"首字节";#33b 同族:早退条件要计入失败态,否则 wedge 到重启)。**(09-23 随 0.10.0 手风琴重设计的指针更正:锁定案已随套件迁移——`card-regression.js` 同日退役删除,本条的回归锁现在是 `dsh-ui-test/card-accordion.js` 的 `[B4]`(after 断言另加固 `posts === prePosts + 1`,即补拉窗口内不得出现第二次保存);机制本身未变(`lib/client.js` 的 `settleGateways`/`GATEWAY_SETTLE_DELAYS`),只是"usage 分区自己 10s 轮询"的口径改成了"通用区块展开时才轮询"。)** **(0.10.0 跨分支终审补半句:本条当时只修了"采样时机"那一半——写后的读还必须能识别陈旧。`lib/client.js` 的 `load()` 现按 `useRef` 自增请求代次,回包时代次不是最新就整包丢弃(**失败分支同受此门约束**:陈旧请求的报错也不许盖住新态);否则两次 POST 落在同一个 GET 往返窗口(实测 ~1.0–1.3s)内,先发后回的旧响应会覆掉新响应,UI 停在旧值而主视图无轮询自救——"四区块可同时展开 + 每分区各持一个 `saveIn(block)`"让这种并发成为正常路径。通式补完:写后的读既不能当终态(要补拉到收敛),也不能让乱序的读落地(要带代次丢弃)。)** **(0.10.0 复审补第三半:`load()` 一旦会返回 `null`,所有"拿返回值决定下一步"的站点都不能再读那个返回值。**补拉链的两个判定点(`save` 的 `.then` 启动点、`settleGateways` 的退避续跑点)必须同形地读 `dataRef.current`(= 最新被采纳的那份),只改一处等于把本条的自愈保护削掉一半**——复审实测:回退启动点 ⇒ 保存的 GET 往返窗口内只要有一发非保存路径的 `load()`(卡片重开 / OAuth poll / 同步目录)抢先被采纳,链条一次都不会启动、假「未监听」常驻到用户收起再展开,症状与本条原描述逐字相同;而这一半是"加代次门"那次改动**自己引入**的行为回退(改前成功路径永远返回 `d`,该站点不可能停摆),不是既有缺陷。同族的形状教训:给一个函数新增"什么都不返回"的分支时,要把所有消费返回值/回调参数的站点一次改完,改两处对称的点漏一处比全用旧写法更难看出来。锁 = `[B4]`(续跑自愈)+ `[B5]`(启动点在竞争包作废时照旧启动;`[B5]` 同时是 A2 代次门的第一枚时序用例——迟到包独带的水印 `模型 4242 个` 不得出现在 DOM)。)**
59
+
60
+ 46. **键盘可达性断言必须用「可信按键」:`dispatchEvent(new KeyboardEvent(...))` 不触发原生 `<button>` 的默认激活行为 ⇒ 测出来的是 harness 伪缺陷,不是产品缺陷**(2026-09-23 设置卡手风琴重设计 Task 7,代价 = 一轮产品代码改动 + 一次 revert):新套件的 `[G1]` 照 brief 逐字实现(`el.dispatchEvent(new KeyboardEvent("keydown",{key:"Enter",bubbles:true}))` 后断言 `aria-expanded` 翻转),run1 全套件唯一红点恰是它——`FAIL [G1] aria-controls 指向真实存在的 body 且已翻转 — {"id":null,"exists":false,"expanded":"false"}`。当时的结论是"区块头按钮键盘不可用",于是给 `lib/client.js` 的 `BlockHead` 展开按钮补了 7 行 `onKeyDown`(Enter ⇒ `preventDefault()` + `onToggle()`,注释还论证了"原生 button 只对受信任按键执行激活"——提交 `48329a7`)。**真相是浏览器本来就对真键盘执行该默认动作**,脚本派发的合成事件 `isTrusted === false` 才是唯一不翻转的那一个:产品侧"修复"之后 `4dd9f52` 把它整块撤回,回归锁改用 `page.keyboard.press("Enter")`(CDP 受信任输入,`card-accordion.js:983`),当轮即 **115/115 全绿**(Task 7 fix round 口径;Task 8 追加两条锁后为 117,本轮 Task 9 复跑仍 117/117;0.10.0 的跨分支终审轮 → 118、残余修复轮 → 119、复审修复轮 → **129**(`[B5]` 十断言),算式见 `AGENTS.md` 的套件行——这些数字都是**各轮当时的实况**,别拿某一轮的去改别处的口径)——即"缺陷"从来不存在,`onKeyDown` 反而是把浏览器默认行为复制进 React 处理器(一旦哪天浏览器行为与它不一致,就是双翻转的源头)。教训:a) **合成事件 ≠ 用户输入**,这条在本文已有两次前科——#5(脚本派发的 blur 不触发 React onBlur)与 #16b(setInput 与 blur 必须分任务)是同一族,凡"点/按/聚焦之后行为没发生"的断言,先问事件可不可信(`isTrusted`),再怀疑被测物;b) 判别手法 = **同一断言换驱动方式再跑一次**:`dispatchEvent` 红、`page.keyboard.press` 绿 ⇒ 差异全在驱动侧,产品代码无罪(本轮还顺手得到一个真结论:区块头本来就键盘可达);c) 回归锁"红"之后不要立刻写产品代码——先复现出"最小可信路径",本轮的代价就是那个来回:一轮 fix 的学费 + 历史上一次假的缺陷修复记录(`48329a7`→`4dd9f52` 两次提交只为撤销同一件事)。
61
+
62
+ 47. **宿主主题由 `body[data-ds-dark-theme]`(属性)驱动,`prefers-color-scheme` 媒体仿真对本宿主零效果**(2026-09-23 Task 7/8 实测):⇒ 浅色不变式要走"摘属性"路径;且**原生控件配色必须显式绑该属性**——`color-scheme` **不继承**,其 used value 由 html/body 向上传播给原生控件面板,插件不写就跟着宿主恒深色,浅色下未勾选 checkbox 呈**深色实心块**(看起来像已开启)。实测三件事(一次性探针脚本 `tmp-h6-probe.js` 已随该轮删除,物证留 `shots/tmp-h6-{default,dark,light}.png`):① `prefers-color-scheme` 的三种仿真(不仿真=headless 默认 light / 强仿 light / 强仿 dark)下**三张截图 md5 互等**(本轮复量:`e849eb9170aa1a43dae6e7ef0ac7a879` ×3),body 计算亮度恒 21.7、`bodyBg=rgb(21,21,23)`——headless 默认即 light,所以"仿真成 light 后断言变浅"的守卫是**死断言**(`matchMedia` 报 light 而页面仍是暗色,两半都恒真);② `localStorage` 无 theme 键 ⇒ 暗色来源在宿主 profile 层写死的属性;③ `document.body.removeAttribute("data-ds-dark-theme")` 之后 `bodyBg` 立刻变 `rgb(255,255,255)`、2.5s 后复查**未被宿主 observer 加回**、回设属性即回暗 ⇒ 摘属性是活的通路(`[H6]` 于是按"暗色基线断言 + 摘属性断言翻浅"两条互照,`card-accordion.js:910`/`:935`)。**缺陷怎么发现的**:不是断言抓到的——是 Task 8 Step 4 的**逐张看图**核验浅色基线时报为缺陷(`probe-head-check-unchecked-light.png` 里未勾选态是"近黑实心圆角方块 + 浅灰描边、没有空心内底",与已勾选的蓝底白勾**形状完全相同、只差填充色** ⇒ 判"不可读"),`a421d35` 才修。当时的量化根因:浅色态(`darkAttr:false`、body 计算亮度 255)下 `html`/`body`/`.cbc-card` 三处 `getComputedStyle().colorScheme` **全为 `dark`**,而插件全文件 `grep color-scheme` 零命中、宿主 `input`/`checkbox` 规则逐条 `matches` 都不落在这枚 `.cbc-check` 上 ⇒ 面板色由宿主根节点无条件带下来。影响面也比区块头大:卡内**所有**未勾选原生 checkbox/radio 同此渲染(模型行未勾的 9 个、生图开关、桥/会话头开关、API Key radio),区块头只是 Task 3 把通道开关上移后**最扎眼的那一处**(收起态第一眼位置)。修复 = `lib/client.js:127-128` 两条规则显式钉面板色来源(`body[data-ds-dark-theme] .cbc-card{color-scheme:dark}` / `body:not([data-ds-dark-theme]) .cbc-card{color-scheme:light}`)——不能用 `@media (prefers-color-scheme)`,因为①;挂 `.cbc-card` 是因为它是两种视图的唯一共同根(page 视图 `div.cbc-card` / 旧槽 `li.cbc-card`,summary 视图只有一行 span、不渲染控件)。回归锁 = `[H6]` 第 6 条(`:946`)+ 第 7 条"两主题裁剪物证均已出图"(`:954`,防物证静默停产);判别力**只在 light 半边**(dark 半边读到的值是宿主传播态的 parity 观察,别写成"两向同验"——同批实测 RED 输出正是 `{"dark":{"cardScheme":"dark"},"light":{"cardScheme":"dark"}}`)。物证:`shots/probe-head-check-unchecked-light.png` 由深色实心块(md5 `0262bc70…`)变浅底空心框(`17e11816…`),`-dark.png`(`b1792d3d…`)**修复前后逐字节相同** ⇒ 暗色零影响。教训:a) 主题不变式要按**宿主的实际驱动方式**选路,不能按 Web 标准直觉(这里 `matchMedia` 与 CSS 媒体查询都不参与);b) `color-scheme` 这类"影响原生控件面板"的属性属**零声明即跟随祖先**,插件卡必须在自己的根上显式表态;c) 截图基线的价值在"逐张看图"这一步——机器核对全绿(尺寸/在位/无报错)时,人眼仍能看到"空心 vs 实心块"这种语义反转的缺陷。
63
+
64
+ 48. **`page.screenshot({fullPage:true})` 在本宿主是空操作**(2026-09-23 Task 8 Important 1;同类:"报告声称的适配在产物里零命中"与"两向同验"过誉):产出恒为视口尺寸 1440×900——**"尺寸对 ≠ 内容在"**。机制:卡片渲染在宿主 page 视图的内层 overflow 容器(`SECTION.*_page`)里,`document.scrollHeight === 视口高`,`fullPage` 没有可扩展的文档高度。旧批"完整纵向基线"6 张(`shots/acc-00-overview.png`…`acc-04-general.png` + `acc-dark-active-tab.png`,本轮读 PNG 头复核)**全是 1440×900 视口图**,而 `shots-baseline.js` 当时的头注释自称"出完整纵向视图"——说法是假的。改走元素句柄 `handle.screenshot()` 后**又踩一层**:900px 视口下 `acc-light-01-codebuddy.png` 出图 926×1371(高于视口)**而下半截约 470px 是空白**——元素级截图只保证按元素盒取像素,容器不绘视口外的内容;当时脚本的"图高 == 区块盒高"机器核对**照样通过**,是**逐张看图**才抓到的(修法:视口拉到 1440×3000,`shots-baseline.js:73-74` + 新增 fit 核对"最近滚动祖先的 client 区是否整个包住该元素",`:153-181`,不成立先 `scrollIntoView` 复量、仍不成立记 FAIL)。现行基线 10 张(明/暗 × 总览+四区块)实测尺寸 960×275 / 926×1371 / 926×423 / 926×622 / 926×1157,每行日志带 `完整包含=true`。同条目的**文档纪律**部分:本轮两次"文档自述跑在产物前面"——① Task 8 盘点表把 `qoder-prefs-check.js` 的适配面写成含"连接域名/同步目录适配"两条,实际产物零命中(该脚本压根不碰这两处),判为 Important 2 后在报告就地更正;② 报告 §10.3 把 `[H6]` 配色锁写成"两向同验(改反了也必红)",实际判别力只在 light 半边(见 #47),§11.8 更正、Task 9 把正文两处措辞改齐(含 `a421d35` 提交信息里同一句——提交不可改写,以本文与 §11.8 为准)。教训:a) 截图类断言的"过了"要能翻译成"内容在画面里",否则只是像素数对得上(与 #10/#29 同族:断言完成而不是断言首字节/尺寸);b) **文档自述与产物同批更新**——报告/注释/提交信息里的"已做 X"必须能在被指认的文件里 grep 到,写完就核对一次,别让下一轮照着它抄(CHANGELOG 尤其吃这个亏)。
65
+
66
+ 49. **上游给 provider 模块改名/拆包 ⇒ profile patch 里按旧模块名写的条目被整条静默跳过,而"包还在、版本也对、退出码 0"三样全都看着正常**(2026-09-26 dsh 0.1.7-rc.1 → rc.2 升级实录):rc.2 把 id `llm-deepseek` 的承载模块从 `@deepseek-ai/dsh-llm-deepseek` 改名为 `@deepseek-ai/dsh-llm-deepseek-api-key`,并另拆出 `dsh-llm-deepseek-account` 管账户鉴权/发现(旧包 `dsh-llm-deepseek` 仍在 node_modules 里、版本同为 rc.2)。于是 `~/.dsh/profiles/web/cordis.patch.yml` 里那条 `- id: llm-deepseek / name: "@deepseek-ai/dsh-llm-deepseek"` 被判 `name mismatch` 并 **整条 config 不生效**(`deepseek-flash` 的 name/contextWindow/inputModalities 覆盖无声消失),唯一信号是启动 stderr 的一行 `patch: name mismatch for "llm-deepseek" (expected "@deepseek-ai/dsh-llm-deepseek-api-key", got "@deepseek-ai/dsh-llm-deepseek"), skipping`,`--dump-config` 照旧 exit 0、stdout 照旧上千行。修复 = 条目 `name` 改成新承载模块(并留注释说明"写旧名等于整条不生效")。教训:a) 升级后必须 `--dump-config` **把 stderr 单独收下来逐行读**(`2>err`),只看退出码与 stdout 行数等于没验;b) id→模块名的**权威映射看 dump 输出里的 bundle 清单段**(那里就写着 `id: llm-deepseek / name: '@…-api-key'`),不要按"旧包还在不在"推断;这是 #43① 的同族但更阴的一档——那边是 `cannot resolve` 的响亮跳过,这边是"包在、名字不对"的静默失配;c) `repair-profile-links.js --dry-run` 的"补缺链"清单是依赖闭包变动的免费信号:本轮新增 6 条里就含 `dsh-llm-deepseek-api-key`/`-account` 两个改名/拆分线索,升级前先跑一次 dry-run,就能预判要逐条核对哪些 patch 条目,而不是等用户发现"模型窗口又变回默认了"。
67
+
68
+ 50. **「命名空间/清单存在性」预言机只对"健康时必在场"的条目有效——拿它罩不在其注册面的条目 = 常驻假 warn,比没有 warn 更糟**(2026-09-26 P1 宿主实况对账首版误报,基线截图逐张看图抓到):对账功能用 dsh 0.1.7 settings `describe` 的 `allNamespaces` 判定 cordis.patch.yml 三个 patch 条目(llm-pi-ai / agent-default-model / web)是否被宿主加载,实拍图立即出现「缺失:web」黄点——而此刻 web 钉选完全健康(搜索/抓取一直走 codebuddy)。机制:`allNamespaces` 只覆盖**注册了 settings 命名空间**的 entry(volatile 表单可见的那类),`web` 行的 patch(`searchProvider`/`fetchProvider` 钉选)从不注册 settings 命名空间 ⇒ 健康态也永远缺席 ⇒ 存在性预言机对它恒报缺失。常驻假 warn 的代价不是噪音本身,是**训练用户忽略 warn**——对账功能的全部价值随之归零(`[R1]` 的"warn 数 === drift 数"不变量只罩通道行,罩不住 entries 行,正是漏网路径;160 断言全绿时假 warn 已在画面上)。正解 = **按条目性质选预言机**:settings 条目查 allNamespaces 存在性,非 settings 条目做**效果级直查**——dsh-web 的 WebRuntime 构造时把 config 钉选落实例字段(`searchProviderId`/`fetchProviderId` 公开可读),patch 行被跳过则字段 undefined,直查运行时实例比"命名空间在场"更接近用户可感知事实。教训:a) 任何"期望集合 vs 实际集合"的对账,先验证实际侧数据源对**每一类**期望条目健康时的在场性——预言机覆盖率不是 100% 时,缺的那些条目要显式标"口径外"或换预言机,不能默认缺席=异常;b) 新 UI 行的"无 warn 态"也是不变量——本机健康实拍必须全绿点,首拍就逐张看(#47 同族:机器核对全绿 ≠ 语义正确);c) 效果级信号(运行时实例字段/真实行为)优先于元数据信号(清单/命名空间),后者只是前者的代理。
69
+
70
+ 51. **CDP `overridePermissions` 授 `clipboard-read` 会把同族 `clipboard-write` 一并显式 deny,之后连可信点击的瞬时激活都救不回 `writeText`**(2026-09-26 P2-3「复制诊断」回归锁 [I1] 定位):套件本想给剪贴板断言铺路,对 origin 调 `browserContext.overridePermissions` 授 clipboard 读权限——结果产品代码的真实用户路径(可信点击 → `navigator.clipboard.writeText`)恒 `NotAllowedError`,而**同一按钮人工点击完全正常**。实测结论:**完全不授权反而正常**(可信点击 ⇒ transient activation ⇒ writeText 成功);一旦 overridePermissions 碰过 clipboard 族,write 侧即被置 deny,transient activation 也不再放行。正解 = **零授权 + 可信点击 + 间谍读回**:不碰 overridePermissions,按钮用 `page.click`(CDP 受信任输入,踩坑 #46 同族)触发 writeText;断言侧不读真剪贴板,而是前置注入 writeText 间谍(`window.__clip` 记文本,写被拒记 `__err__ <name>`)读回——既绕开权限面,payload 又逐字节可核(锁定注释在 `card-accordion.js` [I1] 段,注明"别改回 overridePermissions")。教训:a) 测试环境给浏览器"开权限"不是纯增益——授权动作本身可能改变同族其它权限的判定,**人工能点、脚本不能点时先查脚本对环境的改动**(授权/仿真/注入),再怀疑产品(#46 同族:驱动侧伪缺陷);b) 剪贴板这类权限敏感面,断言走间谍注入比走真实系统 API 更稳也更精确。
71
+
72
+ 52. **AGENTS.md 无锁增长 = 每会话常驻 token 的无上限税;靠"记得保持薄"的手工纪律必然漂移,要结构性分流 + linter 硬闸门**(2026-09-26 治理实录):AGENTS.md 涨到 184 行 / 34,323 字节(≈21.8k 字符,中文为主 ⇒ 每会话 15k+ token 常驻),而头行还写着"踩坑全本(#1–#48)"——列表实际已到 #51,手工口径漂移实锤。机制:速查节 O(N) 增长无上限,每加一条坑/事实都"顺手加一行",没有任何闸门说不。外部依据:Anthropic 官方结论 "bloated CLAUDE.md files cause Claude to ignore your actual instructions" + 逐行删除测试法("删掉这行 agent 会犯错吗?");HumanLayer 研究称指令遵从墙在 150–200 条;社区预算锚点 ≤150 行 / 12KB。宿主约束:ZCode 不支持 @import、路径域规则、嵌套子目录 AGENTS.md ⇒ 社区方案的这些路线全部不可用,可用机制 = 项目级 skill(`.agents/skills/`,agentskills.io 开放标准)+ 现成 linter。修复四件套:①任务态知识(浏览器回归全套)分流到 `.agents/skills/dsh-ui-regression/`;②速查节压成"编号+标签"超短表,使用纪律改为动手前 grep 本文对应编号;③体积与引用完整性进 CI 硬闸门(`agents-md lint --check --threshold 0 --max-lines 150 --max-bytes 12000 --fail-on-placeholder` + `asamarts/alint` 的 `agent-context@v1` 规则集 + 本地 `scripts/verify-agents-md.mjs` 含编号连续性对账);④新坑/新事实/新命令的落点写进 AGENTS.md 维护纪律。教训:a) 凡靠"记得保持 X"的纪律早晚漂移——不变量要交给断言(#38 同族:那边是 fixture 时钟,这边是"头行口径 vs 列表长度"没人对账);b) 分层文档齐备时,索引文件复述下层内容是纯税负——只留指针;c) 硬编码会增长的口径(如"#1–#48")必然过期——要么不写数字,要么让 linter 对账。
73
+
74
+ 53. **Chrome 原生 checkbox 的命中面扩不走 input 自身 padding,要走 label 包裹 + 负 margin**(2026-09-27 设置卡 WCAG 2.2 SC 2.5.8 收敛实录,`dsh-ui-test/tmp-geom-probe.js` 证伪/证实各一轮):区块头启用开关 `.cbc-check` 命中就是 16×16,SC 2.5.8 要求 ≥24×24。给 input 打 `padding:4px;margin:-4px` 无效——`appearance:checkbox` 的命中区不吃 padding(仍 16×16),盒模型还把框挪 7px(padOnInput 变体实测 cbX 1385.5→1392.5)。正解 = `<label class="cbc-checkhit">` 包裹(`display:inline-flex;align-items:center;padding:4px;margin:-4px`):负 margin 让 label 在 flex 布局里仍按 16px 占位——开关 x 与行高逐像素不变,命中面实测 31×30(padOnLabel 变体)。label 对落在控件自身的 click 不再转发、只对 label 空白处派发一次合成 click ⇒ 不引入 #27 的双 change([C3]「恰 1 次 POST」在跑)。教训:a) 扩原生控件命中面先想"包一层能落点击的元素",别在控件自身盒模型上使劲;b) "DOM 加包装但布局逐字节不变"靠负 margin 对冲 padding 达成;离线几何探针(复现同套 CSS,一次跑四方案 × 新旧子序 × 确认态有无)是同时钉住命中面与坐标的唯一手段——元素级 click 测试对跳位与命中面**全盲**,套件侧只补了一条量 hit 矩形的锁([C3]「命中面 ≥24×24」,退回裸 checkbox 即红;坐标锁反而锁不住这条回归,裸 checkbox 坐标与包裹态逐像素相同)。
75
+
76
+ 54. **桌面宿主(0.2.0-rc.2)的组合树里 bundle patch 的 codebuddy 块不生效,镜像只写 models 子路径会被 llm-pi-ai 校验拒掉——desktop profile 接入必须手放完整 provider 块**(2026-10-03 desktop 适配 G4 实测):dsh-tap 经 desktop CLI 以 link bundle 装入(package.json 两落点自动写好)后,`?probe=host-config` 报 `providerIds=[volces,qoder]` + `lastError=ERR: llm-pi-ai: provider "codebuddy" model "default" needs an api`——bundle 层(仓库 cordis.patch.yml)的 codebuddy 完整块没有出现在组合结果里(web 侧用户层 patch 历史手放过同形块,所以 web 一直免疫、从未暴露),而插件的模型镜像按设计只 set `providers.codebuddy.models` 子路径(index.js syncModelsToDshSettings,纯净态还会 unset)⇒ 合并树里 codebuddy 路由没有 api/baseURL,0.2.0-rc.2 的 llm-pi-ai 校验器(`api = request.api ?? base?.api ?? routeApi`,asar 实读)判 `needs an api` ⇒ **mutate 整体回滚**(用户层文件无 codebuddy 键、lastError 留痕)。修复 = 往 desktop patch 的 llm-pi-ai.providers 手放完整块(displayName/api/baseURL/headers/compat/models,从 web 用户层拷贝同形——web 正是这么接入的),重启后 lastError=null、providerIds 三键齐。教训:a) **"bundle patch 会被组合器合并"不能凭 package.json 声明想当然**——同一声明在 web 生效不代表在桌面宿主生效(也可能两处都没生效而 web 靠用户层块活着);b) 镜像的"子路径覆盖"设计以"基线块在组合树里存在"为前提,前提塌了校验就拒;c) 判定手法 = probe 的 lastError + providerIds 与文件层实况三方对账,applyOps 的错误格式 `ERR: <宿主校验原文>` 直接给出根因;d) 附带实证:0.2.0-rc.2 的 settings seam 与 0.1.7 同代(forms/mutate/writable 全在,host-config 零改动),且其 schema 对未知文件层键不抛错(HEAD 版 Config 实测)——共存的 web 实例(旧内存代码)读新键无害。
77
+
78
+ 55. **cordis 给 apply 的 entry config 恒含 schema 默认值——把 entry 层当"用户显式设置"判定时,schema 默认会压过运行时计算值,静默失效**(2026-10-03 桥端口宿主分流 G3 实测):分流设计 `resolveBridgePorts(entry, file, profileDir)`——entry 或文件层给了合法端口就当"显式"采信,否则按宿主信号分流(默认 profile 3902/3903,非默认 +10)。活 desktop 实测 `dir=desktop` 但端口恒 3902/3903 不分流:entryPortKeys 实报 `[bridgePort,traeBridgePort,traeChatTransport,qoderBridgePort]`——cordis 把 `Config({})` 的 schema 默认(含三端口 3901/3902/3903)作为 entry config 传入,"entry 显式优先"把这些默认值当成用户显式,分流偏移永远轮不到。**cordis 语义里 entry ≠ 用户显式**——它是 schema 解析后的默认值载体;真正的用户显式只在文件层(设置卡 commit 才落键)。修复 = 「显式」判定拆两层:entry 端口值**偏离 schema 默认**才算显式(verify-* 套件传随机空闲口 ≠ 默认 → 采信,否则桥会绑 3901 撞真实实例);文件层显式恒采信(含等于默认的值——那是用户明确要的);cordis 的默认 entry 落回分流/文件层。教训:a) **宿主传给 apply 的第二参不是"用户配了什么",是"schema 解析出什么"**——要区分"用户显式"必须与 schema 默认比对,不能看键在不在(它永远在);b) 这类"值对但来源错"的缺陷单测/离线全绿(套件 entry 是随机口 ≠ 默认,恰好绕过),只有活实例的默认 entry 才暴露——分流/默认值覆盖类逻辑的验收必须有活实例探针,不能止于离线套件;c) 指纹探针(`?probe=...` 回显 entryPortKeys/文件层键/解析结果旁证)是把"跑的是哪份代码 + 哪层供的值"一次钉死的最低成本手段。
79
+
80
+
81
+ 56. **应用壳转发会剥掉 Origin 头——「curl 模拟过门 ≠ 壳内真实请求过门」,安全门的同源判定必须给无-Origin 形态留语义**(2026-10-03 桥端口分流 GUI 实测,computer-use 首轮真实壳内操作爆出):desktop 壳(Electron 0.2.0-rc.2)的 `forwardWebRequest`(asar main.js 实读)把页面请求转发到本机 Host 前删掉 `origin`/`host`/`cookie`/`sec-fetch-site` 四个头(壳侧已按自有名单 `dsh-app://app` 验过 Origin)⇒ 到达插件的 POST **恒无 Origin**;而 `sameOrigin` 对 `origin===undefined` 恒拒 ⇒ **壳内所有写操作(保存/重试监听/登录)自 Origin 门上线起全是 403**,用户在壳内点「重试监听」才炸出来。历史盲区成因:0.15.0 的 Origin 门验收是 curl 模拟(`-H "Origin: dsh-app://app"` 直打,带 Origin 头)——三连 200/200/403 全绿但**从未代表真壳形态**;STATE 未验证项里写着「壳内是否透传 Origin 需壳内 DevTools」一直没人做。修复 = `sameOrigin` 放行无 Origin 的 POST(威胁模型:浏览器跨站 POST 恒带 Origin 不受影响;无 Origin = 壳转发/非浏览器,回环 Host 门兜底;非浏览器本可伪造 Origin,此门对其无约束力)。教训:a) **带自定义 Origin 头的 curl 是「模拟壳」不是「壳」**——壳会改写/剥掉请求头,转发链上的门判定要按转发后的真实形态设计;b) 本地特权面的同源语义要与 GET 侧门(`localGuardFailure` 无-Origin-放行)对齐,否则 GET 通 POST 堵这种半态最难排查;c) 403 响应体不带拒绝原因(`{ok:false}` vs guardFail 带文案)让客户端横幅只剩 HTTP 码——安全门拒人时把「收到了什么」带回响应/日志,是排查这类问题的最短路径。
82
+
83
+ 57. **wasm-bindgen retptr-first 导出的栈槽协议错一位就静默全废——胶水按「习惯」手写而不对 wasm 类型段实测,decrypt 全程没解密**(2026-10-03 风险清单实锤):cosy.js 的 `decrypt(text)` 写成 `wasm.decrypt_server_response(passString(text), LEN)`(2 参,省掉 retptr),而 wasm 类型段实报 `(i32,i32,i32)→()`(retptr-first 三参,与同文件 `generate_runtime_auth_fields`/`qodercontext_*` 同型)——栈槽布局 `[resultPtr, resultLen, errPtr, errLen]`。后果链:wasm 把第一参当栈指针 ⇒ 结果槽写进输入串的线性内存(实测输入区被覆写成 `1,0,0,0…`)、ret 恒 `undefined` ⇒ 真 imports 下内部抛错被 `catch {}` 吞掉 ⇒ `rt.decrypt(任意输入)` 恒原样返回,**catalog.js 的「Encode=1 密文兜底」是死路**——上游一旦回密文,`JSON.parse(decrypt(text))` 直接 SyntaxError,Qoder 目录同步静默失败。修法 = 栈槽协议三件套:`sp=__wbindgen_add_to_stack_pointer(-16); decrypt_server_response(sp, ptr, len);` 读槽 `[rptr,rlen,eptr,elen]`,errLen 非零则 `throw takeObject(eptr)`,成功 `getString(rptr,rlen)` 后 `__wbindgen_export4(rptr,rlen,1)` 还内存。教训:a) **wasm 导出的参数个数以类型段为准,不以「同文件别的导出长什么样」为据**——`__wbg_*_free` 是 2 参、`requestresult_body` 是 2 参、`decrypt_server_response` 是 3 参,同模块内形态不一,必须逐个 `WebAssembly.Module.exports` + 类型段解析实锤;b) `catch { return text }` 这种「兜底原样返回」会把 ABI 错误**藏成功能正常**——解密从未发生但调用方拿到的恰是输入,离线自测全绿;兜底分支必须断言「兜底被走过」(如计数器或日志),否则死路永远没人发现。
84
+
85
+ 58. **wasm 堆对象不受 JS GC 管理——导出表里的 `__wbg_*_free` 不主动调就是无界泄漏,`global.gc()` 也救不回来**(2026-10-03 与 #57 同批):wasm 导出表明明有 `__wbg_requestresult_free`/`__wbg_qodercontext_free`,cosy.js 胶水全文从未调用——每次 `prepareChat/prepareGet/prepareSigned` 泄漏一个 RequestResult(含 url/headers Map/body,≈3KB/次),凭据轮换还泄漏旧 QoderContext;实测 8 万次签名 +245MB 且 `global.gc()` 不回收(这些字节在 wasm 线性内存里,V8 GC 管不着)。修法 = 双 free 纪律:①RequestResult 访问器取值后立即 `free()`(幂等:`if (ptr) __wbg_*_free(ptr); ptr=0`),三个签名出口统一经 `drain()` 消费后即释;②`ensureContext` 凭据轮换时先建后 free 旧上下文(失败保留旧上下文,不炸当前链)。教训:a) **wasm-bindgen 的 JS 侧包装对象只是「句柄」,真正的内存在 wasm 线性区**——句柄被 GC 不代表底层被释放,`__wbg_*_free` 是唯一回收通道;b) 长跑网关的内存曲线是这类泄漏的唯一可信探针——单测/短跑全绿,8 万次级压测才现形;c) free 必须幂等(重复 free 同一 ptr 是 UB),`ptr=0` 置位是标配。
86
+
87
+ 59. **会话并发闸的 release 只能在 finally 一处——错误路径顺手 release 再 return,并发上限被击穿**(2026-10-03):qoder/gateway.js 流内错误帧路径先显式 `release()` 再 `return`,finally 里又 release 一次。SessionLimiter 的契约是「一次 acquire 对应恰好一次 release」(core/bridge.js:139-155)——双 release 在有排队时多唤醒一个等待者(limit=4 实际并发变 6),无排队时 inflight 计数永久漂移。教训:a) acquire 返回的 release 是**一次性凭证**,任何「顺手释放」都是 bug——提前 return 的分支让 finally 统一兜,别在分支里重复;b) 这类缺陷单测全绿(并发压力不到阈值不现形),断言要直接对 limiter 的 inflight 计数做终态校验。
88
+
89
+ 60. **SSE 重试回退不能只看「writeHead 没发」——已下发的角色 chunk/排队提示/文本帧全是用户可见内容,重发即重复**(2026-10-03):trae/gateway.js 遇 3003 返回 `'fallback'` 换 chat_v3 在同一 HTTP 响应上从头再发,代码只挡了 writeHead 重(`if (!res.headersSent)`)——首 attempt 已发出的角色 chunk、排队提示 chunk(是 `delta.content`,进用户可见答案)、文本/推理 chunk 全部重复下发。修法 = 「可见内容离手」信号跨 attempt 维护(`streamStarted`),首个可见帧(角色/排队/文本/工具)离手后置真,此后 3003 按终局错误下发而不再回退;角色 chunk 延迟到首个可见帧时随头发出(不能 attempt 开头无条件预发,否则信号立即为真、回退永远走不到)。教训:a) **「能不能重试」的判据是「用户看到了什么」不是「协议头发了没」**——writeHead 是传输层,角色/排队/文本是应用层,两层要分开判;b) 响应头与角色 chunk 也要分步:头可以提前发(不算可见内容),但 `res.write` 一行注释/帧都会把头发出去——`Cannot write headers after they are sent` 就是注释行抢在 writeHead 前落地炸出来的;c) 附带修复:单事件多 tool_calls 循环内反复赋 `out.toolCall` 只下发最后一个(并行调用静默丢帧)——改数组逐个产出。
90
+
91
+ 61. **回环网关只验 Host 不验 Origin,恶意网页 `sendBeacon` 免预检直打就能烧额度——Host 门防的是「连不连得上」,Origin 门防的才是「浏览器借不借得出」**(2026-10-03):qoder/trae/bridge 三网关只校验 Host 头回环——恶意网页 `navigator.sendBeacon('http://127.0.0.1:3902/v1/chat/completions', json)`(text/plain 免 CORS 预检、浏览器自动带正确 Host)即可驱动网关消耗用户额度,响应读不到没关系,副作用已发生。修法 = 补 Origin 门(index.js `localGuardFailure` 同口径):浏览器跨站请求恒带 Origin,其 host:port 必须与 Host 完全一致才放行;**无 Origin 放行**(本机 fetch/curl 与剥 Origin 的壳转发均不带该头,Host 门仍把守回环——与 #56 同语义)。教训:a) **Host 门与 Origin 门是两个威胁模型**:前者防 DNS rebinding/LAN 直连(连不连得上),后者防跨站借浏览器(借不借得出)——只设一个等于只关一扇门;b) 设置卡路由(index.js:1219)早已有同款 Origin 门,网关没跟上——特权面的安全门要全链路对齐,不能有「这边设防那边裸奔」的断点。
92
+
93
+ 62. **令牌刷新落盘是读-改-写,与 logout 的整体覆写竞态——在飞刷新会把已登出的令牌写回,登出失效**(2026-10-03):三个 OAuth 模块同病,刷新落盘 `writeAuth({...readAuth(), auth: next})`,与 logout 的整体覆写竞态——刷新在飞时用户点登出,刷新完成把令牌写回。修法 = 存储代际守卫:模块级 `storeGeneration`,logout 时递增,refresh 启动时记代际、落盘前比对,过期即丢弃结果(实测内存模型:代际守卫后 refresh 返 undefined、logout 后 store 保持空)。教训:a) **「读-改-写」与「整体覆写」天然竞态**——读-改-写方必须带代际/版本戳,落盘前校验,否则慢的一方必然覆盖快的一方;b) 这类竞态概率低但后果明确(用户以为登出了实际没登出),属于「低概率高后果」必须修的一类。
94
+
95
+ 63. **同值去重表/首字节护栏这类「单侧行为」要全链路对齐——设置卡有 Origin 门而网关没有、inline 有首字节护栏而 remote 没有、quota 有 memoize 而无单飞,断点即缺陷**(2026-10-03 一批同型):a) trae remote.js `openRemoteEvents` 的 fetch 无 signal(createSession 有 20s、stop 有 10s,唯独它没有)——边缘「收下不回应」时请求永久挂起、并发槽不释放,inline 面有护栏(gateway.js:561)remote 面缺失;b) trae quota 60s memoize 但无单飞——并发 snapshot 各发一遍重复请求,memoize 只挡「第二次以后」挡不住「同时在飞」;c) qoder `syncCatalog` 注释自称幂等可重入故无单飞,codebuddy 侧反而在组合根单飞——口径不一,并发时互相完整覆盖 catalogState(不损坏但浪费请求);d) rotation.js 见 `AbortError` 就不冷却不故障转移——上游「连上不回应」的首字节超时也是 AbortError,该冷却换 key 的场景没换(bridge 有 `clientDisconnected` 标记却没用它区分)。教训:**同类防护在不同子面的覆盖要成对审计**——「A 面有 B 面没有」不是「B 面不需要」,十有八九是漏了;护栏/单飞/去重表这类「看着像优化」的机制,实际是正确性的一部分。
96
+
97
+ 64. **上游能力声明在目录里、投影层丢掉,宿主就永远不出档位入口——「目录有声明」≠「宿主有入口」**(2026-10-04,用户报「输入框里有些模型没有推理等级」):宿主 Model/Effort 选择器的「推理等级」只由模型条目的 `reasoningEfforts` 决定(pi-ai `resolveModelReasoning` → `reasoning:true` + `thinkingLevelMap` → `reasoningInfo` → 选择器出该项,`getSupportedThinkingLevels` 再按 map 过滤)。dsh-tap 的 Qoder 目录**逐模型声明**了 `thinking_config: {disabled, enabled:{efforts:{...}}}`(2026-10-04 实测 14 模型,9 个带命名档位),而 `projectQoderModel` 只投影 id/name/contextWindow/maxTokens/input——声明在投影层被丢掉 ⇒ 镜像进 `llm-pi-ai.providers.qoder.models` 的条目没有档位表 ⇒ **14 个 Qoder 模型在宿主选择器里一个档位都不出**(设置卡里却有逐模型 effort select,两边口径不一致)。同族两条:a) **只补档位表还不够,路由必须声明 `compat.supportsReasoningEffort`**——pi-ai 的 openai-completions 出站只在 compat 为真时才把选中档位写成 `reasoning_effort`(qoder 镜像块原先无 compat ⇒ 出档但请求里没这个键);b) **anthropic 方言的 off 线值是「发 `thinking:{type:disabled}`」不是省略参数**(`anthropic-messages.js:902`:`thinkingEnabled === false && thinkingLevelMap.off !== null`),所以**上游拒绝 disabled 的模型绝不能声明 off 档**——Ark `glm-5.3`/`glm-5.3-flash` 实测 400 `InvalidParameter`("thinking.type `disabled` is not supported by this model"),声明了就是摆一个必然 400 的档位;反之 `off` 键缺省 = map.off 为 null = 不发 disabled = 上游默认(照常思考),安全。教训:**能力声明要从上游目录一路投影到宿主条目(声明 → 投影 → 镜像 → compat),任何一层丢掉都表现为「UI 里没入口」且零报错**;档位拼写与 off 语义一律取上游实测,不臆造(#42 同纪律)。**同族第三条(0.19.0 补):设置卡里的固定档位表是同一个坑的另一面**——卡里写死 `off/low/medium/high/max` 时,目录声明的 `xhigh`(Qoder)/`extra_high`(Trae)/`light`(Trae)这类上游专有拼写就**选不到**,而"选了但上游不认"更糟;档位选项必须与宿主**同一真源**(服务端把目录声明原样发给 UI,`GET qoder.models.efforts`),写入校验也按该模型声明收口(允许集 = 声明 ∪ 存量值,避免全量期望态重发时误判旧拼写非法);收尾眼 = `node scripts/probe-effort-gaps.mjs` 把「声明有没有到路由条目 + 路由有没有 compat」变成可跑的对账(发版清单项)。
98
+
99
+
100
+ 65. **确定性安全扫描器按「通用 Web 服务」建模,本地回环网关的每条产品主干在它眼里都是污点链——Mimosa git 门把「设置→fetch=SSRF」「handler→res.end=XSS」模式匹配出 32 个高危,且会话级 hook 拦截 push/commit 后无任何本地绕过口**(2026-10-04,0.19.0 发版被拦三轮实测):dsh-tap 本质就是「在 127.0.0.1 起翻译网关、向上游发 fetch」的程序,而 Mimosa L3 的 semgrep 规则不识别三件事——(a) taint 源(用户自己的设置文件/硬编码厂商 baseURL)属本地信任边界、(b) 三个监听面已回环绑定 + Host/Origin 双门、(c) 响应体是静态字面量。结果:全仓 24 个 fetch 汇点 + 15 个 res.end 汇点经「入口/1 跳/2 跳」展开膨胀成 32 high;最讽刺的是被标 XSS 的三行(trae/gateway.js 的 403、qoder/gateway.js 的 403、core/bridge.js 的 403)**恰恰是 #61 Origin 门本身的实现**——安全门被当成 XSS 汇点。处置面更棘手:`MIMOSA_GIT_GATE_MODE=warn` 与 `MIMOSA_NO_GIT_GATE=1`(插件 README 写明的两个官方开关)经 PreToolUse hook 链**都不生效**(hook 逻辑在 protected-loader 加密的 .mimosa 资产里,判定写死,`--no-verify`/env 均绕不过)——唯一出路是宿主的插件/会话配置,bash 单侧无解。教训:a) **接确定性安全门前先校准威胁模型**——「发 fetch 即 SSRF」「写响应即 XSS」对回环代理/网关类项目必然系统性误报,规则需要喂「回环绑定 + 已有 Origin 门 + 字面量响应体」这类上下文抑制,否则门会把项目本职全部拦死;b) **hook 拦截面要有逃生口**——一个连官方 env 开关都不认的强制门,等于把「安全建议」升级成「安全锁死」,发版这类正当动作必须有可审计的放行通道(例如按 finding hash 的豁免清单,或 warn 模式真正生效);c) 历史对照——同插件的**深扫**在本仓库有过真价值(#57–#63 八项真阳性全修),问题不在扫描器有无用,而在**门控粒度**:写入门拦「AI 新写的高危」合理,git 门把「全仓固有模式」当 push 阻断则是误用。
101
+ 66. **宿主 schema 对 `models[].reasoningEfforts` 的键有固定枚举(off|minimal|low|medium|high|xhigh|max)——上游专有拼写原样做键,forms seam 整块拒收且静默**(2026-10-04,goal trae-work-cn-repair G3):Trae 目录声明 `light`/`extra_high`,投影成 `{light:"light",…}` 后镜像写入被宿主校验整段拒绝(`$.providers.trae.models[1].reasoningEfforts expected false | {…enum…}`)——**连带该路由块的全部后续镜像写入一起失败**(启停模型都不再生效),而错误只落在 host-config 的 `lastError`(`sync*ToDshSettings` 的 catch 只写 stderr,设置卡零信号),表象 = 「同步返回 ok:true 但宿主配置层万年不更新」。修法 = 键做宿主词汇映射(light→low/extra_high→xhigh,保序),值恒为目录声明拼写(线值不臆造,#42);未映射拼写不进宿主表(设置卡侧仍按声明拼写出控件/校验/注入)。教训:a) **出档链路要防「写不进」不只防「丢了」**——probe-effort-gaps 三层对账(#64)能发现投影缺失,发现不了 schema 拒收;镜像写入的验收眼 = 写后读回(或直接看 `?probe=host-config` 的 lastError);b) 上游词汇 ≠ 宿主词汇时映射层必须显式成表(`TRAE_EFFORT_TO_HOST_KEY`),别「先直抄试试」——schema 拒收是静默的,不靠对账根本不会浮现。
102
+
103
+ 67. **环境探测路径按开发机形态写死(WSL 的 `/mnt/c/Users`),另一个平台(Windows 原生进程)静默扫空——「扫不到」返回 [] 不报错,故障被放大成功能整体失声**(2026-10-04,goal trae-work-cn-repair G1 根因):`discoverStateDbs()` 写死 WSL 路径,桌面 dsh(win32 原生)里 `/mnt/c` 不存在 → readdirSync throw 被 catch 成 [] → 目录同步永远「未发现 state.vscdb」,而 UI 只有泛化「未同步」四个字,用户与排查者都看不到真因。修法 = 探测按 `process.platform` 分支(win32 直查 `os.homedir()/AppData/Roaming/…`,WSL 保持逐用户扫描),平台/ home 做成可注入参数(`{platform, home}`)让两条路都能离线夹具测试。教训:a) **任何「读本机环境」的代码都要问一句"作者机器之外的形态跑过吗"**——路径/根目录/分隔符按平台分支是底线;b) **发现类函数的空结果 ≠ 没问题**——空态必须配套可见的原因出口(本仓 G2 的 syncView.error),否则下游只能猜;c) 验收要在目标平台真跑——WSL 里全绿的发现逻辑在 Windows 原生进程里是死的,离线 fixture 证明不了平台分支。
104
+
105
+ 68. **同一上游端点的不同「面」(function/scene 参数)可以各有各的出站方言——同一套翻译代码换 function 名复用,形态差一位即 proto 层静默全废**(2026-10-05,Trae agent 面接入实测):llm_utils_chat 的 inline_chat 面历史 assistant.tool_calls 用 OpenAI 同构的 `function` 键透传(2026-08-24 校准),而 solo_work_lite 面同字段必须改名 `function_call`——用错形态不报「未知字段」,报 proto 反序列化错 `*idecopilot.ToolCall read field 4 'FunctionCall' error: required field Name is not set`(上游把 function_call.Name 映射到 proto 必填字段,function 键被丢弃后 Name 为空)。教训:a) **同一端点换 function 参数 ≠ 同一协议**——出站形态(含历史消息内嵌结构的键名)要按面逐一实测锁定,不能从「同端点同信封」推断「同方言」;b) **proto 层校验失败的报文念的是 proto 字段名不是线缆字段名**——看到 'FunctionCall'/'Name' 这类大驼峰 proto 名,要反推到线缆层的 function_call.name 而不是去 body 顶层找;c) 该面 SSE 出站的 tool_calls 键(function_call)与入站历史消息要求的键一致——**入站出站同构是该面协议的对称性证据**,探针抓到出站形态后应立即回头验证入站同键(本 goal 的 A3 臂就是靠这条假设一次修好)。
@@ -0,0 +1,218 @@
1
+ # Trae 云端 API 实测档案(v0.8.x 接入依据)
2
+
3
+ > 日期:2026-08-23
4
+ > 方法:GitHub 社区逆向(linqiu919/trae2api,2025-06 停更)提供历史参照 → 本机
5
+ > 二进制 strings 提取当前协议面 → **无凭据在线探测**校准错误信封与端点存活。
6
+ > §1–§4 全程未使用任何真实凭据;带凭据联调由 `scripts/probe-trae-live.mjs` 承担
7
+ > ——**2026-08-23/24 已执行完毕**(§2 已校准段、§5 对照表;生产级交叉参照
8
+ > github.com/autumnsentiment/Trae2api-cn 的 raw client)。
9
+
10
+ ## 0. 一句话结论
11
+
12
+ TraeWork CN 的聊天面是**任务制私有 RPC**(`/api/agent/v3/*`,SSE),网关在
13
+ `trae-api-cn.mchost.guru`;OAuth 换/刷令牌在 `api.trae.cn`
14
+ (`/trae/api/v3/oauth/ExchangeToken`,火山系 ResponseMetadata 信封)。
15
+ 插件侧用**自持 ECDSA P-256 设备密钥**走完整设备流,refresh 的 DeviceProof
16
+ 由自己签名——不依赖、不提取官方 IDE 的任何凭据。
17
+
18
+ ## 1. 无凭据在线探测(2026-08-23,curl 直打)
19
+
20
+ | 端点 | 结果 | 结论 |
21
+ |---|---|---|
22
+ | `POST api.trae.cn/trae/api/v3/oauth/ExchangeToken`(假 ClientID) | 400 `ResponseMetadata.Error{Code:"10101", Message:"Invalid client.", StandardCode:"040004"}` | 端点存活;client 校验层 |
23
+ | 同上(真实 ClientID `en1oxy7wnw8j9n` + 假 AuthCode) | 400 `Code:"10101"`, `Message:"无效参数:{__Message.field}."` | client 过了校验层;线上模板变量未渲染(可当指纹) |
24
+ | `POST api.trae.cn/cloudide/api/v3/trae/oauth/ExchangeToken`(旧路径) | 400 同上 | **旧路径仍存活**(trae2api 时代的刷新路径没死,但当前客户端走新路径) |
25
+ | `POST api.trae.cn/cloudide/api/v3/trae/GetUserInfo`(无 token) | 401 `Code:"20310"`, `Message:"The user is not logged in,"` | cloudide 面信封确认 |
26
+ | `POST trae-api-cn.mchost.guru/api/agent/v3/create_agent_task`(无 token) | **401** `{"code":1001,"message":"We're sorry, but we are not able to authenticate you…"}` | **聊天网关在 mchost**;1001 = 统一未认证码 |
27
+ | `POST trae-api-cn.mchost.guru/api/agent/v3/llm_utils_chat`(无 token) | 401 同上 | 工具型一次性聊天端点存活 |
28
+ | `POST api.trae.cn/api/agent/v3/*`、`api.trae.com.cn/api/agent/v3/*` | 404(TLB nginx) | agent 面不在 api.trae.cn |
29
+
30
+ 两种错误信封并存:mchost 面 `{code, message}`;api.trae.cn 面火山系
31
+ `ResponseMetadata.Error`。`providers/trae/errors.js` 的 normalizeTraeError
32
+ 两者都吃 + 裸非 JSON 容忍。
33
+
34
+ ## 2. 二进制协议面提取(harness.dll / ai_agent.dll strings)
35
+
36
+ - 云端路由族(harness.dll,`TTNetConfig` 邻域):
37
+ `/api/agent/v3/`:`create_agent_task`、`commit_toolcall_result`、`interrupt`、
38
+ `resume_agent_task`、`get_resume_agent_task_status`、`query_history_state`、
39
+ `sync_history_state`、`compact`、`llm_utils_chat`、`workflow/start`、
40
+ `workflow/commit_toolcall`、`use_fast_request`、`generate_summary`、
41
+ `dsl/logs/subscribe`、`dsl/templates`、`dsl/render/resources`。
42
+ → 官方 SOLO 的 agent 环 = create_task → SSE 事件 → commit_toolcall 循环;
43
+ **`llm_utils_chat` 是工具型一次性聊天**(title.rs / video.rs / telemetry 用它),
44
+ 是 dsh provider 场景(纯 LLM 调用)的正确目标。
45
+ - llm_utils_chat 请求信封(**2026-08-23/24 带凭据联调已校准**,证据
46
+ docs/probes/trae-chat-live-*.json):
47
+ - 体:`{messages, model, function, request_id, session_id, stream:true}`;
48
+ `messages[].content` 必须是 `{type:"text",text}` 块数组(字符串 → 400/4001
49
+ "cannot unmarshal string …LLMRawMessageContent");`function` 必填——缺则
50
+ SSE error 2001 "function is empty, cannot resolve model by usage=",
51
+ 实用值 `inline_chat`;`scene_params` 若带必须是 string(内嵌 JSON)。
52
+ - 头(绑定层逐项实测收敛):`x-app-id` = product.json 固定 appId
53
+ `6eefa01c-1036-4c7e-9ca5-d891f63bfcd8`(**≠ OAuth client_id**;缺省 4001
54
+ "expr_path=app_id");version-code 类头必须是**数字串**('0.1.52' 判 missing,
55
+ 实测 `20260401` 过);三头同 JWT(`Authorization: Cloud-IDE-JWT` +
56
+ `X-Cloudide-Token` + `x-ide-token`)+ 设备指纹头组 + `x-request-id`。
57
+ - **`model` 字段不被路由**:inline_chat 函数位由服务端解析到账户当前默认
58
+ 模型(两次实测 `provider_model_name` 均为 `kimi-k2.6`,与请求的
59
+ glm-5.3 / DeepSeek-V4-Pro 无关)——dsh 侧多模型清单实为同一出口。
60
+ - SSE 事件语法(实测):`metadata` / `timing_cost`(含 provider_model_name)
61
+ / `output`(data.response、data.reasoning_content)/ `token_usage`
62
+ (计数在 data 顶层:prompt/completion/total + cache_read_input_tokens)
63
+ / `done`(finish_reason);错误走 `event:error` data{code,message}。
64
+ 2026-08-23 实测 response 为逐段增量;解析器按 Trae2api-cn 生产参照做
65
+ 累计快照前缀差分(两形态兼容),单点 createTraeStreamParser。
66
+ - 工具调用(2026-08-24 实测,trae-chat-live-tools-*.json):请求侧
67
+ `tools[].function.parameters` 必须**序列化为字符串**(Go string 型,
68
+ 对象直发 4001 "cannot unmarshal object …parameters of type string");
69
+ 响应侧 `output.tool_calls[i]` 的键是 **`function_call`**(非 function),
70
+ `arguments` 为**增量片段**、续片 id/name 为空按 `index` 归属拼接;
71
+ 带工具调用时 `done` 仍发 `finish_reason:"stop"`——网关映射为 OpenAI
72
+ 语义的 `tool_calls`,否则客户端不触发工具循环。
73
+ - 认证头两套并存(harness.dll):`authorization` + `x-ide-token`(ide_token 语义)
74
+ 与 `x-cloudide-token`;设备头组:`x-app-id`、`x-ide-version-code`、
75
+ `x-app-version-code`、`x-user-region`、`x-tt-env`、`x-use-ppe`、`x-env-lane`、
76
+ `request-traffic-type`、`x-device-id`、`x-machine-id`、`x-os-version`、
77
+ `x-device-cpu`、`x-device-brand`、`x-web-id`。插件侧先发双头 + 已知设备头,
78
+ 联调后按 401 形态收敛。
79
+ - 自定义 provider 直连:二进制里有 `deepseek/anthropic/openrouter/gemini/aws/xai`
80
+ 前缀 + `/v1/chat/completions` 组合 + `ak/sk` 字段——BYOK 条目由 harness 直连
81
+ 第三方(与 state.vscdb 目录里 `deepseek//…` 条目带 ak/sk 互证),
82
+ 这就是目录映射时排除 BYOK 条目的原因。
83
+
84
+ ## 3. 本地 harness(备选架构,存档未采用)
85
+
86
+ - 官方 Rust harness 是**本地服务器**:`resources/app/modules/ai-agent`,
87
+ meta.json 固定 `socket.port: 40005`;axum 路由 `/api/v1/chat/{initialize,
88
+ start_chat,subscribe_events,append_msg,…}`(harness\server\src\modules 证据),
89
+ Electron 侧驱动。
90
+ - **懒启动**:IDE 常驻不等于 harness 常驻(2026-08-23 实测 IDE 13 进程、
91
+ 40005 无监听);启动器是 `x64/run_helper.exe`(目录里没有 start.bat 引用的
92
+ ai-agent.exe),裸拉无参即退——参数/握手未知。
93
+ - harness 数据库 `ModularData/ai-agent/database.db` **加密**(harness\storage-db
94
+ \src\connection\encryption.rs,node:sqlite 报 "file is not a database"),
95
+ 令牌不落 state.vscdb(只有 `mcpOAuth` 一个无关键),Windows 凭据管理器无
96
+ Trae 条目 → 令牌只在内存/加密存储。
97
+ - 结论:harness 驱动路线需要本地 RPC schema + 认证注入方式两个未知数,
98
+ 且无法自主触发懒启动;**直连云端路线(已采用)只需要一次用户登录**。
99
+
100
+ ## 4. 历史参照(GitHub:linqiu919/trae2api,2025-06 停更)
101
+
102
+ - 旧协议:`POST {base}/api/ide/v1/chat` + TraeRequest 信封(user_input/
103
+ intent_name="general_qa_intent"/variables/context_resolvers/chat_history/
104
+ session_id/conversation_id/current_turn/valid_turns/multi_media/model_name/
105
+ last_llm_response_info/is_preset/provider)+ `x-ide-token` 头。
106
+ - 旧刷新:`POST /cloudide/api/v3/trae/oauth/ExchangeToken`
107
+ `{ClientID, RefreshToken, ClientSecret:"-", UserID}`(**无 DeviceProof**)。
108
+ 2026-08 客户端已演进为 DeviceProof 签名(traework-cn.md §6);旧路径虽
109
+ 存活(§1),请求体已不满足当前客户端形态。
110
+ - 旧域名 `a0ai-api-sg.byteintlapi.com` 是国际版;CN 走 mchost/api.trae.cn。
111
+ - 旧 TraeRequest 信封字段与当前 llm_utils_chat 信封部分重合(messages/
112
+ session_id/conversation_id/model_name),是 buildChatRequest 的交叉证据。
113
+
114
+ ## 5. 插件实现 ↔ 证据对照
115
+
116
+ | 实现点 | 证据来源 | 置信度 |
117
+ |---|---|---|
118
+ | OAuth 设备流全流程 | traework-cn.md §3–§6(静态逆向原文) | 高 |
119
+ | PKCE/设备密钥自持 | 同上("私钥为首次登录/设备注册时生成"→ 自注册同理) | 高 |
120
+ | ExchangeToken 端点/信封 | §1 无凭据实测(10101 两层) | 高 |
121
+ | 聊天网关域名+端点 | §1 401/1001 实测 + 二进制路由族 | 高 |
122
+ | llm_utils_chat 请求信封 | §2 带凭据联调(4001/2001 逐字段收敛)+ Trae2api-cn 生产参照 | **高(已联调)** |
123
+ | SSE 事件语法 | §2 实测事件流(metadata/timing_cost/output/token_usage/done) | **高(已联调)** |
124
+ | 双认证头形态 | traework-cn.md §7 + 二进制双头组 + §2 三头实测 | 高 |
125
+
126
+ 联调状态:**2026-08-23/24 已完成**(`--chat` 真实对话成功,证据
127
+ docs/probes/trae-chat-live-*.json;verify-trae-provider 50 断言锁形态)。
128
+ 再校准路径(上游改协议时重跑):
129
+ ```sh
130
+ node scripts/probe-trae-live.mjs --login # 浏览器登录 + DeviceProof 刷新自证
131
+ node scripts/probe-trae-live.mjs --chat "你好" # 真实对话,原始证据落 docs/probes/
132
+ ```
133
+ 若 401:先试 `--sig raw`(DER→IEEE-P1363);信封/事件语法改动单点在
134
+ providers/trae/gateway.js(buildChatRequest / createTraeStreamParser)。
135
+
136
+ ### 5.1 模型路由与限制(2026-08-24 带凭据实测,重要)
137
+
138
+ - **raw 面(llm_utils_chat)的模型路由被 function 位钉死,model 字段不被路由**。
139
+ 2026-08-24 终局探测矩阵(证据 docs/probes/trae-model-routing[234]-*.json):
140
+ - `function=inline_chat`:只服务**账户默认模型**(本账号=kimi-k2.6);任何
141
+ 其他 model 名(kimi-k3 / glm-5.3 / DeepSeek-V4-Flash-Official)一律 SSE
142
+ error `3003 "all models failed"`;附加 custom_model 对象无效(同样 3003)。
143
+ 早间该面曾对非默认模型静默改派 kimi-k2.6(200 成功),当日下午起变为
144
+ 硬错误 3003——**服务端行为有时变性**,两态都要兼容。
145
+ **08-24 当日再恶化(~09:39 UTC 起)**:3003 扩大到一切 model 名(含默认
146
+ kimi-k2.6、含不带 model 字段),失败流无 timing_cost——服务端未走到选模
147
+ 成功一步;同信封同凭据 chat_v3 正常对话,额度池充足 → 判定 inline 面模型
148
+ 解析层服务端故障。完整证据链与对照实验见 **docs/diagnosis-trae-3003.md**
149
+ (证据 docs/probes/trae-3003-diagnosis-*.json)。
150
+ - `function=chat_v3` / `solo_agent_lite`:任意 model 名(包括
151
+ `"not-a-model"`)都 200,但 timing_cost 证实恒为
152
+ `seed-code-lite-dev-0602-v1-part1`;`solo_work_lite` 恒 `glm-5.2`。
153
+ - 真值源 = timing_cost 事件的 `provider_model_name`。网关对策:解析
154
+ timing_cost,改派时以 SSE 注释行 `: trae-reroute requested=… actual=…`
155
+ 告知(OpenAI 解析器忽略、不污染调用方会话历史),计量/日志记真实模型。
156
+ - **官方客户端同窗口的网络日志(round 9 取证,2026-08-24)**:
157
+ 官方 IDE 的 llm_utils_chat 请求**不直连 trae-api-cn 而是 307 内部重定向到
158
+ `api5-normal.mchost.guru`**(当日全天 586 次 307 全部落到该域;TTNet 域名
159
+ 调度 x-net-sdk-domain-dispatch)。但实测两域名同信封同 3003 —— **域名非解药**。
160
+ 官方请求头组含插件未发的指纹头(version-code 用当日构建号 `20260811`、
161
+ `x-app-version:"default"`、`x-bridge-transport:"aha"`、`x-request-pin`、
162
+ `x-requested-at`、`request-traffic-type:"prod"`、`user-agent:"TraeClient/TTNet"`、
163
+ `x-lgw-req-sdk-type:"3"`);逐头二分加回均无行为差异 → 头组差异非 3003 根因。
164
+ 官方聊天主通道走 remote(当日 `chat_sessions` 提及 576 次 vs `llm_utils_chat`
165
+ 20 次),官方自己在事故期**也不依赖 inline 面** —— 与插件建议"切 remote"一致。
166
+ ⚠️ 官方日志的 307 记录显示 `status:"SUCCESS"`/`code:200` 是**链路层成功**(响应体
167
+ 被 TTNet 吞掉、未落盘),不可作为"官方绕过 3003"的证明;客户端实际体验是否撞
168
+ 3003 无法从该日志判定(chat_sessions 才走官方主 UI)。
169
+ - **临时波动警示**:同信封同 token 同内容,chat_v3 曾短暂返回 1005 套餐门
170
+ (`extra:{"plan":1}`,kimi/glm/DeepSeek 三模型全中),数分钟内自愈回 200 ——
171
+ 1005 不只出现在 remote 面 kimi-k3,raw 面也曾全模型闪过;**单次 1005 不能
172
+ 当作账号级套餐判定**,需重试/交叉验证(round 9 实测,见 diagnosis-trae-3003.md §10)。
173
+
174
+ - **唯一真实的模型选择机制 = remote 会话协议**(已落地为网关 remote 传输,
175
+ providers/trae/remote.js):`POST {base}/api/remote/v1/chat_sessions`
176
+ (initial_message.`model_name` + `model_selection_strategy:"manual"` +
177
+ `agent_type:"solo_agent_remote"`,content 空数组、历史扁平化进 query)
178
+ → `GET /chat_sessions/{id}/events?reply_to_message_id=…` SSE → stop。
179
+ `model_config` 事件与 done 的 `user_message_context.model_info.config_name`
180
+ 双重证实路由到请求模型(glm-5.3 / kimi-k2.6 实测),且模型自报一致。
181
+ - 事件语法:plan_item(thought=可见文本、reasoning_content=思考,**累计
182
+ 快照**按 id 分槽前缀差分;tool_call_info.name==="finish" 的 params.summary
183
+ 为最终答复)/ model_config / token_usage / done;heartbeat、
184
+ status_changed、platform_timing、timing_events、session_* 忽略;
185
+ queuing/notification = 排队(并发受套餐 solo_agent_parallel_limit 限)。
186
+ - 代价:每请求起云端沙箱 agent(约 25k 系统提示,跨会话前缀缓存命中
187
+ 25.4k/25.4k),**消耗 work 额度池**;OpenAI tools 无法传递(远端 agent
188
+ 自持工具),网关对带 tools 的请求返回 400 remote-no-tools。
189
+ - 套餐门:error 事件 `1005`(message 空、data.plan 携带档位)——Free 账号
190
+ 请求 kimi-k3 命中(model_config 显示路由成功但 LLM 调用被拒);glm-5.3 /
191
+ kimi-k2.6 / DeepSeek 系可服务。
192
+ - 并发门:业务 429 `991502 reason:solo_agent_parallel_limit`——只创建会话不
193
+ 消费事件流的僵尸会话同样占位;stop 端点对未运行会话回 409 "chat session
194
+ is not running",只能等沙箱 TTL 自灭(08-24 实测,diagnosis-trae-3003.md §3)。
195
+ - **边缘层指纹(08-24 故障取证)**:TLB/nginx 节点路由漂移时 create_session
196
+ 返回**裸文本** `404 Not Found`(text/plain;WAF 拦截则为空体 403),而
197
+ 业务级拒绝恒为 JSON 信封、无凭据探测同路径稳定 401 JSON `{code:1001}`。
198
+ remote.js 已对裸 404/403 自动重试一次。详见 docs/diagnosis-trae-3003.md §3。
199
+ - **额度双池实测**(2026-08-24,docs/probes/trae-credits-*.json):
200
+ `POST api.trae.cn/trae/api/v2/pay/ide_user_ent_usage`(body
201
+ `{"require_usage":true,"req_source":0|1|2}`,Cloud-IDE-JWT + x-device-*
202
+ 头组)→ `user_entitlement_pack_list[]`,按
203
+ `entitlement_base_info.available_endpoint` 分池:**0=IDE 通用池(raw
204
+ inline_chat 消耗)、1=work 池(remote chat_sessions 消耗)**;限额在
205
+ `quota.credits_limit`,用量在 `usage.credits_amount`。实测对照:3 次 remote
206
+ 会话后 work 池 +9.4 credits(1764.2→1773.6),IDE 池不动(0.58)。
207
+ - **限流 4011 很紧**(raw 面):短时连续探测即触发("requests have exceeded
208
+ the rate limit");联调时请求间隔 ≥20s。remote 面无 4011,但有排队。
209
+ - **/api/ide/v1/chat(老端点)存活但拒现代模型**:老 TraeRequest 信封被接受
210
+ (user_input/intent_name/model_name…),但 model_name 校验 4023 "the model is
211
+ unknown"(glm-5.3 / glm-5.2 均拒)——本账号该端点注册表不含现代 preset 名,
212
+ 不作为模型选择通道。`/api/ide/v1/get_model_list` 两域名均 404(2026-08-24)。
213
+ - GetUserInfo 的昵称字段是 **ScreenName**(Name/Nickname 均为空)。
214
+
215
+ ## 6. 免责声明
216
+
217
+ 同 traework-cn.md:仅用于本机软件互操作研究与调试;接口属厂商未公开协议,
218
+ 可能随时变更;不得绕过鉴权、滥用配额或访问未授权数据。