# AGENTS.md — 给 AI 代理的必读须知 > 你是将要安装、维护或修改本插件的 AI 代理。在动手任何操作**之前**,先读完本文件。 > 这里记录了别人踩过的坑、数据准确性的红线、以及开源发布前的待办。违反了会出 bug、误导用户、或导致开源被喷。 --- ## 一、这是什么 `dsh-api-dashboard` 是 **DeepSeek Harness 专用** 的多平台 API 余额/用量看板插件。 深度绑定 DSHA Web 框架:`conversation.composer.dock` slot、`sessionProjections`、`webServer` 路由、`credentials` 凭证。 **仅能运行在 DeepSeek Harness(手机版 DSHA / 桌面版)里**,任何其他环境无法加载。 - 服务端:`src/index.js`(余额轮询 + HTTP 路由 `/api-dashboard/*` + 自动更新) - 客户端:`client/client.js`(UI,dsh bundle 启动时打包;改它后需**重启 web GUI** 才生效) ### 📚 文档地图(v1.4.1 起 README 已精简) | 文件 | 给谁看 | 内容 | |---|---|---| | `README.md` | **用户** | 简介、界面预览、功能、支持平台、安装、配置、安全。**保持精简**,别把排查过程/历史写回去。 | | `AGENTS.md`(本文件) | **代理 / 维护者** | 红线、踩坑、机制说明 —— 冗长的排查记录放这里。 | | `CHANGELOG.md` | 用户 / 维护者 | 完整版本历史(README 只留最近一版摘要)。 | | `docs/RELEASING.md` | 维护者 | npm 发布流程(OIDC)。 | | `docs/screenshots/` | — | README 用的真机截图,**不在 `package.json` 的 `files` 里,不进 npm 包**。 | --- ## 二、安装须知(AI 代理注意!) ### ✅ 唯一实测可用的安装方式(v1.4.1 校准) ```sh # 1. 下载 curl -L "https://codeload.github.com/133563825as-ai/dsh-api-dashboard/tar.gz/refs/heads/main" -o /tmp/dsh-api-dashboard.tar.gz # 2. 解压(必须带 --strip-components=1)。是 git 工作区就别这么干, 用 git pull。 mkdir -p /root/dsha-api-dashboard tar xzf /tmp/dsh-api-dashboard.tar.gz -C /root/dsha-api-dashboard --strip-components=1 # 3. ★ 关键一步: 建 node_modules 软链(插件的 peer 依赖由宿主 DSH 提供) ln -sfn /usr/local/lib/node_modules/@deepseek-ai/dsh/node_modules /root/dsha-api-dashboard/node_modules # 4. 用 link: 装进 profile(文件保持真实路径) dsh plugin --profile web add link:/root/dsha-api-dashboard # 5. 重启 dsh web ``` 仓库根目录 `install.sh` 把上面 5 步做完了(含旧目录改名回退、git 工作区保护)。 ### 安装姿势对照(2026-09-11 在 DSHA 上逐条实测) | 姿势 | 后果 | |------|------| | 源码 + `node_modules` 软链 + `dsh plugin add link:
/package.json` → 装前体检 → 装后体检,
并明确区分「装前就坏」和「装后才坏」,避免背锅。
- 抢救:`node tools/profile-doctor.mjs --profile web [--fix]`
- ⚠️ `--fix` **只摘 `dependencies` 里的条目**(与 DSH 自己的 `reconcilePlugins` 同规则);
`@deepseek-ai/dsh-base` / `dsh-web-app` 这类 in-box bundle 从 `$DSH_HOME/profiles/node_modules` 解析,
**绝不许从清单里摘** —— 摘了 DSH 直接哑掉。
- ⚠️ 判断 bundle 能否解析必须用 `createRequire( /node_modules`** —— 那会把 in-box bundle 全部误报成坏的。
`ERR_MODULE_NOT_FOUND` 对「没装」和「软链断掉」是**同一句话**,光看报错分不出来,必须实际解析一次。
## 三、数据准确性红线(改代码前必读)
以下平台余额解析有过「显示不准确」的历史问题。开源公布后会被用户/社区质疑,**务必如实处理,不许造假数字**。
### ⚠️ 已知准确性问题 / 待验证项
| 平台 | 当前状态 | 问题 | 你要注意 |
|------|----------|------|----------|
| 智谱 GLM | 已改(2026-08-30 真实key实测) + **账户分流(2026-09-04)** | 实测确认:智谱**无真实余额 API**,`limits[]` 按 unit 分维度返回 Coding Plan 配额——`unit=3`=5小时窗口、`unit=6`=周配额、`unit=5`=工具(月度);`remaining`=该维剩余积分,`percentage`=该维**填充度/已用**(100=用完)非"剩余%"。**且仅 Coding Plan 套餐可查**:按量付费账户调同一接口返回 `{"code":500,"msg":"当前用户不存在coding plan"}`,候选余额端点(`/api/monitor/account/balance`、`/api/paas/v4/dashboard/billing/*`、`/api/paas/v4/users/me`)**全部 404** | 已改**按 unit 分维度显示**(主显示 5小时窗口,顺带提示周配额/工具用完),绝不把"已用完"当"余额100%";**按量付费/套餐过期走 `classifyBizError` 返回中性 `no-balance-api`**(不标红),其余业务错误透传原始 `msg`。⚠️ 改这段务必先跑 `test-dshadb.mjs` 的 P1~P12——**套餐用户的配额解析绝不能被按量付费分支吞掉**(套餐用户 `parsed` 非空,根本走不到 `classifyBizError`) |
| DeepSeek | 已修 | 负余额曾显示「正常」 | 负数必须显示红(err) |
| OpenRouter | **待验证** | `total_credits-total_usage` 字段名存疑, 读错会 NaN→0 | 需要真实 key 实测 |
| siliconflow | 待验证 | `totalBalance` 字段可能不对 | 需实测 |
| Novita | 待验证 | `availableBalance÷10000` 单位存疑 | 需实测 |
| one-api quota | 待验证 | `quota÷500000` 系数存疑 | 需实测 |
| xAI | 待验证 | 无专属解析, 走 openai 分支 | 需实测 |
> **2026-08-30 已加「字段存在性校验」**:OpenRouter / siliconflow / stepfun / kimi / one-api 若**预期字段缺失**(接口改名/字段名不对),解析直接返回「无法解析/未开放」,**不再把无效值归 0 冒充「余额 0」**——这是对上面各行「字段名存疑」的防御性兜底。
> ⚠️ **字段值 / 单位换算仍需真实 key 实测后才能定论**(红线:别因有了兜底就跳过实测)——兜底解决的是"假 0",**不解决"值不对"**。
### ❗ 通用规则(改任何平台解析时)
1. **不要造假**:查不到准确余额,宁可显示「未开放/待确认」,不要硬塞一个数。
2. **负数/耗尽必须红**:余额 ≤0 或 percent 很低 → 红(err)。
3. **区分「真实 0」和「解析失败」**:`toAmount` 把无效值归 0,可能导致「余额0」假象——**2026-08-30 已在预设平台解析处加「字段存在性校验」兜底**(缺失字段→返回 null→前端显示「无法解析/未开放」)。此兜底只覆盖预设平台,改自定义中转/模型解析时仍需注意区分真实 0 与解析失败。
4. **限流配额 ≠ 余额**:很多平台返回「每 N 小时 xx token」的限流窗口,那不是账户余额,别当余额显示。
> **价格表(`MODEL_PRICES` / `V4_RATES`)来源与币种(2026-09-10 v1.4.0 重写)**:
> - **DeepSeek 走 `V4_RATES` 峰谷表**——已对照[官方定价页](https://api-docs.deepseek.com/zh-cn/quick_start/pricing)中英文**双页**核实(中文页给 CNY、英文页给官方 USD 直发价),值与时区窗口(北京时间周一至周五 9-12/14-18 高峰、空闲=半价)**完全正确**;`deepseek-v4-flash-vision-exp` 与 flash 同价,`deepseek-chat/reasoner/r1` 已 2026-07-24 退役(调用报错)。⚠️ 官方 USD 是**直发价**(口径约 1 USD ≈ 6.67 CNY),**不是** `USD_TO_CNY_RATE`(=7) 换算出来的,别拿汇率去"校正"它。
> - **通用 `MODEL_PRICES`(v1.4.0 起改为「原生币种」存储)**:现役主力来自各厂商**官方定价页原文**(2026-09-10 抓取核对:`api-docs.deepseek.com` 中英双页 / `platform.kimi.com` / `platform.minimaxi.com` / `platform.stepfun.com` / `docs.bigmodel.cn` / `help.aliyun.com` 百炼 / 小米 MiMo 官方降价公告);历史条目来自 NousResearch hermes-agent `usage_pricing.py` 与 [modelradar.cn](https://modelradar.cn/data/models.json)。
> 🔴 **规则:国内厂商官方页给 CNY → 表里直接写官方 CNY 原值;海外给 USD → 直接写官方 USD 原值。不要再做任何 ÷7!**
> 币种由 `modelRegion(model)` 判定(配套导出 `nativeCurrencyOf`),`resolveModelPrice` 只在「显示币种 ≠ 原生币种」时才用 `USD_TO_CNY_RATE`(近似值) 换算。默认配置 `currency='CNY'` + `overseasCurrency='USD'`(v1.4.0 起默认值从 `'follow'` 改成 `'USD'`)下两边同币种、**零换算**。
> **为什么改**:「统一 USD 基准」被同一类错误咬过两次 —— MiMo(¥1 被写成 0.020,等于又除了一次 7)与 `glm-4-plus`(¥2.5/¥5/¥5 被当 USD,显示 ¥17.5/¥35/¥35);更早还有**整个「历史/参考」段的国内条目**都填的是 CNY 原值却在旧口径下被又 ×7。原生币种存储让这类错误**无法表达** —— 改表时直接抄官方页数字即可。
> 一手价未取到的模型→落 `defaultPrices`(单位仍是 **USD**,本轮未改),**别乱填**;确实拿不到官方原文、只能保号迁移的条目,**必须在注释里标「未核实」**。旧模型(2025-08)条目标为"历史/参考"。仅估算用,实际以平台为准。
> - ⚠️ **第三方聚合源只作参考,与原表冲突时不要盲信**:modelradar 2026-09-03 快照里 GPT-5.6 系输出价全呈「输入×1.25」异常模式(疑似抓错列)、且不跟踪促销价(qwen3.7-max 报的是原价) → **这两类已故意未采纳**,注释中有标注,别当遗漏"修回去"。
> - 🔴 **`cacheHit` 缺官方佐证时别标「无缓存折扣」**(v1.2.3 血泪):`glm-5.3-flash` 曾被标 `cacheHit = cacheMiss`,长会话数百万缓存读 token 全按全价计 → 用户实际充值 5 元、面板显示消耗 ¥9.93。**v1.4.0 已拿到官方明文**([docs.bigmodel.cn 定价页](https://docs.bigmodel.cn/cn/guide/start/pricing)):GLM-5.3 `¥8/¥28/缓存 ¥2`、GLM-5.3-Flash `¥0.8/¥2.8/缓存 ¥0.23` —— 即**缓存读 ≈ 输入价的 25%~29%**,与当初「20%」的交叉推断同量级但以官方为准。已加回归断言「GLM 系 `cacheHit` 必须 < `cacheMiss`」+「全表 `cacheHit` ≤ `cacheMiss`」。**真的无折扣才写等值,不确定就按同厂同系比例推并注明依据。**
> ⚠️ **同族反面教材**:`minimax-m2.7` 曾因「中转站无缓存报价」直接写成 `cacheHit = cacheMiss`,而 MiniMax [官方页](https://platform.minimaxi.com/docs/guides/pricing-paygo) 明写缓存读 `¥0.42`(输入 ¥2.1 的 20%)→ 长会话高估 5 倍。**「接口没给」不等于「官方没有」**,先去官方定价页确认再决定写等值。
> - 💱 **v1.3.2 起币种不是全局唯一的**:`resolveModelPrice` 的币种由 `currencyForModel(config, model)` 决定——
> 海外模型(`modelRegion(model)==='海外'`)在 `overseasCurrency` 非 `'follow'` 时走它,其余跟 `currency`。
> **改价格解析时别再假设「一个会话只有一种货币」**:`makeCostProjection` 的视图给的是 `costByCurrency`(按币种分组),
> 混合会话客户端两段拼接显示,**绝不要为了凑成一个数字而按汇率折算合并**——那正是 v1.2.x 想摆脱的 ×7 误差来源。
> `modelRegion` 未命中的模型**不表态、走主货币**(保守),新增模型时若产地重要,去补 `OVERSEAS_MODEL_PREFIXES` /
> `DOMESTIC_MODEL_PREFIXES` 前缀表,别在别处硬编码判定。
> - ⏰ **促销价有时效,到期要更新**:`gemini-3.8/3.7/3.6-flash` 2026-12-31 到期后翻倍;`gpt-5.6-sol` 促销至少到 2026-11-21(列表价 $5/$30);`qwen3.7-plus` 限时 8 折;`qwen3.8-max` 90 天/100 万 token 免费额度。`qwen3.8-max` 夜间 22:00-08:00 五折**未实现**(峰谷引擎目前只服务 DeepSeek)。
> ⚠️ **v1.4.0 已作废两条促销记录**:`glm-5.3-flash` 促销(官方页现值就是 ¥0.8/¥2.8,无到期标记)与 `qwen3.7-max` 5 折(**官方页现为原价 ¥12/¥36,查无 5 折** —— 原值 0.83/2.48 来源不明,已删)。**「促销」必须有官方页原文或到期日期兜底,否则别写。**
---
## 四、维护铁律(改代码前必读)
1. **先备份再改**:改 `client/client.js` 或 `src/index.js` 前,先 `tar` 一份 / 存回退点。用户习惯 A/B 对比+回滚。
2. **推送前问维护者**:默认只做本地改动与本地测试。**别擅自推 GitHub / 发 npm**。
3. **测试方法**:`node --check <文件>` 只查语法;真正的客户端改动要**重启 dsh web GUI** 才进 bundle。运行在容器里时别贸然重启(会断会话)。
4. **改 UI 结构要克制**:UI 方向尚未定稿(半屏/三Tab/核心分组几种方案均已否决),已确认保留的是「**玻璃背景**」。别擅自大改 UI 结构。
5. **玻璃色经验**:浅色用纯白 rtgba(255,255,255,0.72),深色走 `@media(prefers-color-scheme:dark)`。**别用 CSS `color-mix` 跟 token 推玻璃色**——会发灰。
6. **版本闭环 + 版本号规则**:任何对已发布功能的改动,记得 bump `package.json`/`package-lock.json` 版本 + 更新 README changelog,推 GitHub 后老用户面板会提示更新。
**版本号 `X.Y.Z` 的含义(维护者 2026-09-10 明确):`Y` = 大版本更新(新功能 / 结构性改动),`Z` = 修补 bug。** 别把 bugfix 当大版本发,也别一个功能跳两个 `Y`。
⚠️ **一次连续的、尚未发布的开发要合并进同一个版本号**:`v1.4.0`(原生币种)/ `v1.5.0`(子代理可见)/ `v1.5.1`(子代理冷会话自测修复)本来是同一轮工作里连着的三个号,维护者反馈「版本太夸张,之前是 1.3 现在已经 1.5」—— **已全部并回 `v1.4.0`**(三段日志合成一条)。以后:**没发布过就别连着跳号**,更别为一个「上线前自测发现的缺陷」单开版本。
7. **npm 发布只走 Trusted Publishing (OIDC)**(完整流程见 [`docs/RELEASING.md`](docs/RELEASING.md)):`git tag v<版本> && git push origin v<版本>` 触发
`.github/workflows/publish.yml`,仓库内**不存任何 npm token**。
⚠️ **别再试 `npm publish` + token/OTP**:npm 已限制「绕过 2FA 的 token」用于直接发布
(https://gh.io/npm-gat-bypass2fa-deprecation),Granular / Automation token 加 `--otp` 在开启 2FA
的账号上均实测失败(web 登录成功后 publish 仍报 EOTP)。
8. **测试必须以退出码表达结果**:`test/*.mjs` 结尾都有 `if (fail > 0) process.exitCode = 1`。
新增测试脚本别忘了加,否则 CI 里断言失败也会被当成通过。
⚠️ **测试不许读运行机器上的私有文件**:`test-provider-kinds.mjs` 原来直读 `~/.dsh/settings.yaml`,
clone 到别处 / 在 CI 里一律 ENOENT 崩掉,断言里还会带上使用者真实的中转站名与域名。
已改为内联夹具(结构与真实文件逐项对齐,名称与域名用 `relay-one.example.com` 之类占位值)。
**别把真实 provider 名 / 中转域名写进测试**。
9. **provider 判定别改回硬编码名单**:官方/中转三层判定(用户显式名单 → settings.yaml 的 baseURL 域名白名单 → `-official` 后缀)是开源化改造的关键,**千万别把「官方 provider 名单」写回客户端硬编码**——别人的中转站叫什么猜不到。判定的服务端逻辑在 `src/index.js` 的 `parseProviderBaseURLs` / `computeProviderKinds` / `isOfficialHost`,客户端落地在 `client/client.js` 的 `isRelayProvider(provider, config)`。没写 baseURL 的 provider 是「不表态、交后缀兜底」,**别**去读 pi-ai 内置目录补官方域名(`xiaomi` 就是内置目录指向官方域名、但 key 实际来自中转站的反例)。
---
## 五、自动更新机制(v0.6.0+)
- `GET /api-dashboard/update`:对比 GitHub main 的 package.json version,5 分钟缓存。
- `POST /api-dashboard/update/install`:下载→校验→备份→原子替换→失败回滚,重启生效。
- 改代码时别破坏这两个端点;`applyUpdate` 有 `remoteVersion`/`localTarball` 测试注入口。
---
## 六、开源发布前待办(2026-09-10 v1.4.0 校准)
### 🔎 「自动判定」现状(2026-09-10 在本机实测;v1.4.0 补上最后一块短板,被问到时照这个答)
这个插件里叫「自动」的东西有好几套,**真假不一**,改代码前先看清动的是哪一套:
| 机制 | 位置 | 真的假的 | 实测 |
|---|---|---|---|
| provider 官方/中转判定 | `computeProviderKinds`(服务端读 `settings.yaml`)+ 客户端 `isRelayProvider` | ✅ **真**(域名白名单式) | 本机 8 个 provider:只有写了官方域名的 `zhipu` 判 `official`,其余按域名判 `relay` |
| API Key 发现 | `resolvePresetKey` / `resolveApiKeyRef` | ✅ **真**(三层兜底) | 环境变量 → DSH `credentials` 服务 → 直接解析 `~/.dsh/.credentials.yaml` |
| 自定义中转站端点探测 | `queryCustomRelay`(`queryType:'auto'`) | ✅ **真** | 依次试 `billing/subscription` → `api/user/self` → `credit_grants` |
| 自定义模型格式探测 | `queryCustomModel`(`queryType:'auto'`) | ✅ **真** | 对用户给的**那一个 URL** 试 4 种解析格式 |
| **DSH provider 自动入列** | `parseProviderEntries` + `selectDshProviders` + `listDshProviderRelays` | ✅ **真**(v1.4.0 新增) | 本机自动发现 5 条中转站(dshzuoxhe/jiyuan/jiyuanlvdong/mimov/new),`zhipu` 判 official 跳过、`xiaomi`/`opencode` 没写 baseURL 跳过 |
| **平台余额清单** | `PLATFORM_PRESETS` / `config.presets` | ⚠️ **半自动** | 预设清单本身仍是硬编码(官方平台就那几个,合理);但**用户自己的中转站现在会自动入列了** —— 见下条 |
> ⚠️ **两个必须记住的点**:
> 1. **v1.4.0 起插件会读 DSH 的 `settings.yaml` provider 列表**(`storages/settings.yaml` 的 `llm-pi-ai.providers`),自动合成中转站条目去查余额 —— 用户不必再手抄一遍 baseUrl + key。**改这块别退回「只查 `customRelays`」**,那是 v1.4.0 之前最大的体验断点。细节见下方「DSH provider 自动入列」小节。
> 2. **没写 `baseURL` 的 provider 一律「不表态」→ 按中转站显示「—」**(铁律 9 的**故意设计**,别改)。本机 `xiaomi` / `opencode` 没写 baseURL,所以状态条显示「—」。`xiaomi` 正是「内置目录指向官方域名、但 key 实际来自中转站」的反例。
>
> 另外:**`case 'auto': return null`**(`parseResponse`)是**故意的** —— 多格式探测逻辑在 `queryCustomModel` 里,别以为那是 bug。
### 仍未完成
- [ ] 推送前自检:文件树无 `.dsh/`、无本地状态文件、无任何 API key(**推送需仓库维护者授权,代理不得擅自推**)
- [ ] 验证 B 栏(OpenRouter/siliconflow/Novita/one-api/xAI)的真实字段,修正解析(**需真实 key**)
- [ ] `glm-5-turbo` model id 官方确认(`model_id_mapping.json` 标 `confirmed: false`)
- [ ] **重新取证「未核实」价格条目**(v1.4.0 逐条标了注释,均按 `×7` 保号迁移、显示值未跳变):豆包 Seed 2.0 全系(火山方舟官方页是 SPA,`.md` 出口返回壳页)、腾讯混元三条、`kimi-k2.5`、`moonshot-v1-*`、`step-1-*`、`deepseek-chat/reasoner/r1` 三条占位价(与 DeepSeek 官方历史价对不上)
- [ ] **海外三家官方定价页复核**:OpenAI / Anthropic / Gemini 在容器环境 403 或地域封锁,v1.4.0 未能取原文 —— `gpt-5.6-*` / `claude-*` / `gemini-3.*` 仍是 radar 二手源
- [ ] **分档价未实现**:GLM-5 系官方分 `[0,32K)` / `≥32K` 两档(本表按更贵的 ≥32K 保守入库);Qwen `qwen3.6-plus` 有 256K 档 `¥8/¥48`;`qwen3.8-max` 夜间 22:00-08:00 五折 —— 峰谷引擎目前只服务 DeepSeek
- [ ] `qwen3.8-max` / `qwen3.8-flash` 的 cacheHit 是官方**明文例外**(「不是标准输入的 10%,具体见百炼控制台」),现用中转站实测值(¥1.5 / ¥0.1),**有控制台截图请替换**
### 已完成(别重复做)
- [x] `toAmount` 归零问题 → 已加「字段存在性校验」兜底(缺字段→null→显示「未开放」,不冒充「余额 0」);v1.4.0 又补齐 `openrouter`(守卫 `&&`→`||`,原先只缺一个字段会伪造**负数余额**)与 `deepseek`(`total_balance` 缺失校验)
- [x] **v1.4.2 修复 DeepSeek「读错钱包」**(真机报告 + 复现):`/user/balance` 的 `balance_infos` 是**一个币种钱包一条**(官方文档 `currency` 取值 `CNY`/`USD`),且**数组顺序不保证** —— 实测同一 key 连打 5 次,第 4 次顺序翻成 `[USD=0.00, CNY=123.45]`(金额为占位)。旧代码 `const p = infos[0]` 盲取第一条 → 有余额的账户显示成 `$0.00 · 异常`,且**下面那道 `total_balance == null` 红线拦不住**(`"0.00"` 是合法字符串,守卫被绕过)。现改为「**主货币优先 → 余额 > 0 → 首条**」确定性挑选,与接口顺序无关。
⚠️ 改这块务必跑 `test-deepseek-multicurrency.mjs`(15 条,含顺序翻转 / 全 0 / 字段非法 / 空串 `Number("")===0` 四类);**该测试对旧代码会挂 8 条**,别把它当成"已通过"就删。
⚠️ 同类漏洞家族:`openai-credit-grants`(v1.4.0) / `openrouter`(v1.4.1) / `deepseek` 多币种(v1.4.2) —— 共性是「**选错一条记录 / 选错一个字段,就当真实数字渲染**」。新增任何多记录型接口解析时,先问一句「我挑的是哪一条,凭什么」。
- [x] ~~`git filter-repo` 清历史~~ → **不需要**:v1.1.3 时已重建全新 git 仓库,历史天生干净
- [x] provider 官方/中转三层判定(原唯一开源阻断项);v1.4.0 又**删掉了 `computeProviderKinds` 里「按 provider 名字猜官方」的兜底**(与铁律 9 冲突)
- [x] ~~价格表币种统一 USD 基准~~ → **v1.4.0 已改为「原生币种」存储**(见第三节;旧的 USD 基准口径是 MiMo / glm-4-plus / 整个历史段 ×7 错价的共同成因)
- [x] 安全审计(无高危)+ 4 项加固:状态文件强制 0600、请求体 256KB 上限、输入清洗、officialProviders 上限
- [x] 测试脚本入仓 `test/` 并改相对路径(clone 即可跑,**v1.4.0: 15 文件 456 断言**)
- [x] v1.4.0 又一并修掉三处交互问题(详见 ① 与 ⑤ 小节):**拖大肥鱼会被手机壳判成开侧边栏**(`.dshadb-whale-grab` 让路层)、
**台词气泡压在头顶**(`bottom:calc(100% + 6px)`)、**冷启动挂件硬跳 + 状态条随内容高 2px**
(真机 LayoutShift 埋点定位:0.01193 / 0.00094+0.00053)
- [x] v1.4.0 修复会话消耗**漏计 `assistant/attempt` 与重试累加**(旧代码读的 `assistant/chunk` 不在 `KNOWN_SESSION_EVENT_TYPES` 里,是死分支 —— 连测试夹具都用错了事件名,所以回归一直没拦住;已改真实事件并新增 `test-cost-projection.mjs`)
- [x] v1.4.0 修复会话消耗**前缀兜底吞模型**(`gpt-4.1` 被 `gpt-4` 吞掉,输出虚高约 37 倍)—— 只认「安全后缀」
- [x] v1.4.0 **子代理消耗可见**(见下方小节「子代理消耗是怎么算出来的」)
- [x] v1.4.0 **状态文件结构版本 `configVersion` + 迁移**:老状态文件里存的旧默认值会把新默认值钉死(`overseasCurrency: 'follow'` 就是这么坑了「原生币种」那版的默认值改动)。**以后只要改动已持久化字段的默认值, 必须 `CONFIG_VERSION +1` 并补迁移。**
- [x] v1.4.0 **设置界面控件统一 + 补深色模式**:`.dshadb_field_select` 去掉系统箭头、数字框去上下箭头、滑块自绘、设置面板/状态条/子代理胶囊补齐 `prefers-color-scheme: dark`(此前一律硬编码白底)。
⚠️ 中途曾把「币种」几个 `