# 更新日志（Changelog）

本文件只记**公开行为变化**（新增能力、破坏性改动、重要修复）。实现细节、重构与测试加固请直接看 `git log`。

## [0.11.0] — 2026-10-16

本版两条主线：新能力**对话框用量胶囊**（输入框右侧的第二条读数，按 provider 门控），与**面板操作体感**批次（ADR-014）落地。

### 对话框用量胶囊：输入框右侧的第二条读数（两个上游、一个座席，provider 门控）

- **新：注册 `conversation.input.right` composer 座席并挂载 `ComposerUsagePill`**——读数按当前会话模型的 provider 门控：`agnes-token-plan` 模型画控制台 5 小时 / 每周请求窗口的使用百分比，`agnescode` 模型画桌面端积分余额（画存量、不造百分比：网关只发积分不给上限，把余额换算成百分比需要发明一个它占谁的份额）；**其它 provider 的会话一律不挂载**（无读取、无定时器、无读数）——别的插件的会话绝不该带这些数字，`agnescode` 的额度宇宙也是桌面网关自己的，不能给它画控制台的窗口。
- **门控坐在组件边界而非 hook 之后**：外层组件只决定挂哪个内层组件，轮询 hook 全在内层里——先调 hook 再早退会违反 Rules of Hooks，React 会把组件丢弃到会话结束（姊妹插件有专门钉住这条的回归 scenario）。
- **读数与面板同源、不造第二把尺**：百分比走 `windowPercent`（平台自己的 `usagePct` 优先、`used/limit` 只作兜底、**从不钳制**——超额窗口报 >100 是事实不是渲染 bug），严重度点走 `usageTier`（90 / 70，与面板进度条同源）；AgnesCode 侧 `0` 是「耗尽」读数、字段缺席是「读不到」（破折号），不是零。
- **软失败（§5 不变量②）**：读取失败保留最后读数并标记过期，不清空、不抛错；注册失败面板照常工作、该模块缺席。
- 影响面：`src/client/{composer-usage-pill,const,apply,cards,agnescode-tab,i18n,styles,index}.ts` + `client.js`（随 src 同 commit 重建）；`test/composer-slot-gate.mjs`（新增：钉住座席 / 渲染器 / projection / 物化契约与两个 provider id 的双侧拼写）、`test/dom-render.test.mjs`（胶囊真 DOM scenario）、`test/render.test.mjs` / `test/contract.test.mjs`（§11c）；文档与 CI 门禁同步。

### 面板操作体感：快照取数并行 + 保存回显不再拖全量快照（ADR-014）

- **可感变化（账号失效 / 控制台降级场景）**：① 快照五源取数全部并行（原 `overview` 先行再批量；失效账号下两条 15 s 超时链叠加，单次快照最坏 30 s 起，现压到 ~15 s；死令牌锁号不重新引入——`getToken` 单飞 + `rejected` 集 + 逐请求 401 处置，测试 B 组按新形状重钉：每源至多一次出示 / 每 poll 一次登录）；② 接入 API tab 的 provider 开关 / 模型清单 / 出图·视频工具卡的「保存后全量快照 reload」改为把 POST 响应体就地并入快照 `llm` 块——关模型、切开关的反馈不再等下一轮慢轮询；③ AgnesCode 三动作（开关 / 检测本机登录态 / 保存模型清单）直接应用 POST 的完整响应，harvest 成功即时翻出已关联卡片（此前要再排一次最长 30 s 的 GET）；④ `/agnescode` 路由的 balance / catalog 两个 30 s 上游读改并行（最坏 60 s → 30 s）。工具卡「实际生效模型」徽标与额度数字仍随 30 s 轮询更新（Host 计算值，本地不造数）。
- **归位说明**：此批次（①–④）在 0.10.2 发布（2026-10-10）之后的补记提交里被记在 `[0.10.2]` 节下，但已发布的 0.10.2 tarball 里**没有其中任何一条**（0.10.2 的 `overview` 仍先行单独取数、客户端半边尚无 `LlmEchoPatch`）；`[0.10.2]` 下的「面板操作体感」节移入本节订正。
- 影响面：`src/host/{snapshot-aggregate,routes/agnescode}.ts`、`src/client/{panel-page,provider-controls,model-picker,wire,agnescode-tab}.ts`、`test/routes.test.mjs`、`docs/`（ADR-014、ARCHITECTURE §3、PROVIDER-HOT-RELOAD、AGNES-API、PITFALLS）、`lib/` + `client.js`（随 src 同 commit 重建）。

## [0.10.2] — 2026-10-10

### 修复：视频状态轮询断点续查、额度渲染与模型花名册的几处真实缺陷 + 凭据隔离加固

- **修（视频工具）：异步视频轮询被拒时原本不带 `video_id` 且只走「超时 / 超轮次」两路径**，agent 首次查询拿不到 task id 就无法续查，任务卡在「查不到」。`video-client` 现改为携带 `video_id` 并有限重试（`queryRetries=2` → 初查 + 2 次重试 = 3 次调用），与超时 / 超轮次两条路径对齐。新增 `video.test.mjs` 覆盖耗尽路径与 `queryRetries=0` 边界。
- **修（额度渲染）：`usagePct` 有值而 `used` 缺失时，`count(null)` 被渲染成 `0`，面板会打印「已用 0 / N」**（F1）。`UsageTotals` 四个主计数另加 `typeof` 门：缺席字段不再画成「0」——真实数据流下此门不触发（`countOf` 已把缺席平台字段压成数字 0、`overview` 失败时 `quota.totals:null` 见红线⑥ / ADR-007），属渲染层自防御，防未来 partial 对象被构造出来。
- **修（AgnesCode 花名册）：两处目录正确性缺陷**。（F5）排除别名 `id === EXCLUDED_IDS[0]` 改为 `.includes(id)`，第二个别名不再静默失效、继续混进花名册；（F10）`provider-controls` 的 `idList` 用 `String(id)` 强转，`null` 会变成字符串 `"null"` 这个假模型 id 出现在候选列表里，改用 `models.ts` 的 `rosterIds`（只收非空字符串）。
- **修（凭据隔离）：AgnesCode 的 DPAPI PowerShell 子进程不再继承含凭据的父进程 env**，新增 `dpapiChildEnv` 剥除四枚凭据名（`AGNES_PASSWORD` / `AGNES_USERNAME` / `AGNES_TOKEN_PLAN_API_KEY` / `AGNESCODE_CREDENTIAL`），与全仓凭据 REF 集合逐字一致——降低解密第三方桌面端会话文件那一步的凭据泄漏半径（红线①）。`harvest attempts[].detail` 与 `state().error` 出口统一过 `redactSecrets`。
- **内部加固 / 清理（不属公开行为变化，详见 `git log`）**：收敛三轮四域代码审查的约 22 项缺陷与计数器 / 方法归一化 / 单飞泄漏 / JSDoc 归位等低危项；删除 25 个零读者 style token（`details*` / `grant*` / `models*` / `trend*` / `poolsGrid` / `primaryHover`）与全仓零引用的孤儿 `jsqr` devDependency；统一 `countOf` 与 `Number(x)||0` 两处数值惯用法、`requestMethod` 六路由共用、BFF 门控头收敛为单一 `AGNESCODE_BASE_HEADERS`；CI `build-gate` 确保暂存目录存在再 `mkdtemp`。上述均以新增断言钉住，无破坏性改动。
- 影响面：`src/{host,client}/`（多文件）、`lib/`、`client.js`（随 src 同 commit 重建，构建零漂移）、`test/`（多套件 + 断言）、`package.json`（版本 + 去孤儿依赖）。`npm test` 全绿：28 套件 + tsc / build / dup / e2e 四门禁通过，e2e 66/66 PASSED。

## [0.10.1] — 2026-10-06

### 修复：AgnesCode 截图里的真实账号昵称已脱敏（该图曾随 0.10.0 发布）

- **修（资产泄漏）：`assets/panel-AgnesCode.png` 此前把面板上的真实关联账号昵称原样截进了市场页截图**——面板那一行渲染的是登录账号的昵称字段（`i18n` 的 `agnescode.loggedIn` 槽位 `已关联：{nick}`），截图取景时该值未做任何遮挡。昵称指向的是一个真实账号，**这不是示例数据**，因此 0.10.0 的 tarball 里那张图等同于把该账号名随包分发。图已重截，该行改为中性占位文案，账号名不再出现在任何像素里。
- **这是 0.10.1 头号动机，优先级高于本节其余条目**：0.10.0 已发布且**不可覆盖**（npm 同版本号唯一，`unpublish` 后同号也不可复用），所以修复只能随新版本到达用户——这也是本版应当尽快发布的原因。
- **处置边界要说清，避免误判风险面**：泄漏面**仅此一个二进制资源**。已核对 0.10.0 tarball 内全部文本类文件（`.md` / `.json` / `.yml`），账号名**无任何文本残留**；其余两张截图不含账号名。本仓红线条目（凭据不入库、错误文本脱敏）针对的是凭据与令牌，昵称属个人信息、不在凭据服务内，故**不触发凭据层面的处置流程**，但仍按泄漏修掉。
- **`screenshots.json` 与文件名未变**：只换了图片内容，未增删文件，因此市场页的清单与 `files` 白名单不需跟着改（`files` 含 `assets`，重截自动随包发布）。
- 影响面：`assets/panel-AgnesCode.png`（仅此一个文件，无代码改动）。

### 登录表单服从 Host 下发的节流窗口：刷新页面不再让按钮在锁定期内可点

- **修：跨进程 / 跨 profile 的锁定期内在面板上按一次就可能撞锁**。登录节流写在插件自己的 `throttle.json` 里，**机器级共享**（desktop profile 吃到的 429，在 web profile 的 Host 上同样真实），而 Host 一直把「剩余窗口」与「已停车」两个事实下发到快照的 `auth` 块。面板此前**不读它们**：倒计时从组件本地 state 起步，只有本进程那次登录失败才会被填上。刷新页面、换 profile、换 Host 进程这三种再正常不过的操作，都会让提交按钮在一个 Host 仍在服务的窗口里显示为可用——而 `POST /account` 是**刻意**清节流的（理由是「用户是在按面板的指示操作」），那次点击于是不止白费：它抹掉另一个进程正在遵守的记录，并把一次真实尝试打进可能已锁的账号。倒计时现由 `servedWaitMs` 判定、`servedWaitUntil` 换算，表单用前者播种并单调吸收后续窗口（只延后不缩短，避免把读者已在等的窗口提前解锁）。
- **新增：已停车（`needsUserAction`）时给一句话而不是倒计时**。错密码、需人工验证、自造退避用尽这三类拒绝**没有**可等待的期限——倒计时数到零不会有任何东西重试，所以那个数字是撒谎。面板现在明说「已停止自动重试，请核对账号与密码后手动登录」并保持提交按钮可用（重新输入才是出路），与 `auth.locked`（平台说账号本身被锁）分作两句。
- **修：验证码类拒绝会同时渲染两句矛盾的话**。`POST /account` 的失败体在「平台给了 `Retry-After`」时会同时带一个整段窗口与 `needsUserAction: true`，于是旧渲染路径上「已停止自动重试」与一个两小时倒计时同时出现。`submit` 现在让 parked 优先于窗口——`AccountForm` 与 `snapshot.ts` 的规则同源，此前只有后者。
- **`API.md` 补上 `auth` 块两个节流字段的契约**（单位是**剩余**毫秒不是整段窗口；parked 时不许倒计时；面板必须服从它们而非只认自己那次失败），`AUTH.md` §7 与 `ARCHITECTURE.md` §3 数据流图同步接入。字段此前只有两个 JSON 值、没有任何语义说明，按字面读会把「parked 也倒计时」这类 bug 写出来。
- 影响面：`src/client/{snapshot,account-form,i18n}.ts`、`test/{panel.test.mjs,panel-decision.js}`、`docs/{API,AUTH,ARCHITECTURE,ADR}.md`、`docs/PITFALLS.md`（新增 §52–§54）、`docs/ADR.md`（新增 ADR-010）、`client.js`（重建产物）。`lib/` 未改——本次全部改动在 client 半边。

### 插件卡补上图标：Plugins 页不再裸显示包名 + 一段英文

- **新增 `icon.svg`**（`icon: "./icon.svg"`）：Plugins 页的插件卡此前只有文字，卡片图标由 DSH Host 读 `package.json` 的 `icon` 字段渲染——不提供就缺一块。沿用面板的「积分币」母题（渐变圆盘 + 白色记数笔画），配色对齐本插件自己的品牌蓝 `--agnes-brand`（#1E40AF），与面板开关 / tab 下边框 / 额度进度条同一套 token，不跟 DSH 的中性色走。几何与兄弟插件 `dsh-connect-sensenova-token-plan` 保持一致，避免同系列插件卡片形态各异。
- **读取端是硬校验，不是降级**：`icon` 必须是相对路径、扩展名限 SVG/PNG/JPEG/WebP、文件必须落在 `files` 白名单目录内、体积 ≤256 KiB——任一不满足会让整条插件元数据报错（该经验的出处是 `dsh-connect-sensenova-token-plan` CHANGELOG 0.4.4，已写进 `docs/PITFALLS.md` §27）。因此 `icon.svg` 同时进 `files` 白名单，`exports` 无需新增子路径（图标由 Host 直接按仓库路径读，不走包导出）。
- 影响面：`icon.svg`、`package.json`、`docs/DSH-PLUGIN.md`（教学快照同步 `icon` 字段与 `files` 数组）。

### 评审整改：红线④文档措辞更正 + 新增 ADR-012「大统一」爆炸半径权衡

- **修（文档 / 红线④）：`AGENTS.md` 红线④称「`AUTH_DEFAULTS.consoleOrigin` 是唯一出处」与代码不符**。代码里后端源站字面量实际有两处——`agnes-auth.ts:47` 的 `AUTH_DEFAULTS.consoleOrigin` 与 `host-config.ts:41` 的 `CONFIG_DEFAULTS.consoleBase`——两者值一致，且由 `host-config.ts:323`（`resolveAuthOverrides` 在 `:384` 以 `consoleOrigin: consoleBase` 汇流）合并为单一 `consoleOrigin`，运行时是一条源、非漂移 bug。红线④现改为「两处字面量一致、配置边界汇流为单一 `consoleOrigin`，别写第三处也别让两处分叉」。
- **新增（决策账本）：`ADR-012` 把「大统一」的结构性爆炸半径列为已知权衡**。ADR-001 把额度面板升级为商汤全过程集成单点入口；实测同进程现承载 LLM provider 注册、出图执行、视频异步状态机，以及解密第三方桌面端 AgnesCode 的 os_crypt/DPAPI 会话文件（攻击面最敏感的一块）。隔离纪律真实（agnescode 与主线 publisher 共用 `publish-core.ts` 机制、不共用状态；`kind:"grant"` 单点构造/判定；throttle 与凭据 grant 刻意共享不统一），但结构性爆炸半径被重新集中——这正是对 PITFALLS §6「私有状态误进凭据服务会炸整台机器」那次教训的现代重演形态。判定：属设计张力非缺陷；主动缓解是 §5 不变量（五源软失败 / 一个模块缺席不埋别的模块）与 ADR-005 准入门槛 + 拆分蓝图把结构债从事后锐评变事前决策点。
- 影响面：`AGENTS.md`（红线④措辞）、`docs/ADR.md`（新增 ADR-012）、本文件。

### 评审整改（续）：ADR-012 债务控制阀落地为可执行术前自查

- **把 ADR-012 的「债务控制阀」从账本里的已知权衡变成吸收决策的实际闸门**。原条目只说「后续新能力吸收先对照这条」，现在钉死成 ADR-005「申报容器」步骤里必填的一张**爆炸半径对照表**——四问：① 承重面归类（落在四类既有承重面之一，还是引入第五类共享承重面 → 自动触发拆分蓝图）；② 新增共享状态（写盘 / 凭据引用 / 解密密钥访问是否复用既有机制，不得新开 state 窝点又不走蓝图）；③ 失败域边界（会不会撞 ADR-004/007「一个模块缺席不许埋掉别的模块」）；④ 基线可冻结（行为基线能否写进 store/routes 测试）。四问任一命中「引入新承重面 / 新 state 窝点 / 失败域外溢 / 基线不可冻」，先回 ADR-005 拆分蓝图或降级改造，而非带债落地。
- 影响面：`docs/ADR.md`（ADR-012 债务控制阀段扩充）、本文件。

## [0.10.0] — 2026-10-04

### AgnesCode 目录改读 `/v2/models`：模型名带上平台真实的积分倍率与促销标

- **目录端点从 `/v1/models` 换到 `{origin}/v2/models`**：两者是同一 host 上的**两条不同目录**，不是同一份数据——v1 只有 8 行且**不带任何按模型价格**，v2 有 9 行且其中 8 行带平台自己的 `points_cost_multiplier`。桌面端一直读 v2，这就是它的选择器能显示 `1.20x` / `1.85x` 而本插件此前什么都不显示的原因。此前的结论「AgnesCode 不存在按模型倍率」**是错的**，根因是端点选错（详见 `docs/ROADMAP.md` §6.3.1「倍率字段正记」）。
- **模型显示名带上倍率 `Name · xNN.NN`**：pi-ai 的 descriptor 没有承载计费的字段（`cost` 是每 token 美元，本网关是积分制），所以倍率走**显示名**——这是 DSH 选择器唯一会渲染的表面。拼写沿用兄弟插件 `dsh-connect-sensenova-token-plan` 与 WorkBuddy 自有选择器既有的 `· x` 形态，**不引入第二套约定**。
- **`0` 是已公布的价格（免费），不是缺失读数**：`×0.00` 与「未声明」是两件事，前者照常显示、后者才不画。为此新增 `numZeroOk()`——仓库既有的 `num()` / `numOrNull()` 都以 `> 0` 为准，直接复用会把「免费」静默变成「没读到」。
- **面板花名册新增促销标芯片**（`display_label`，如 `限时七折`）：用桌面端同款蓝底配色，与中性灰底的「会员」徽章刻意区分——一个是**平台定价事实**，一个是**账号状态**。平台在**每一行**都放这个键、未促销的值为空串，因此判据是「非空才画」，而不是「键存在就画」。
- **`auto` 行排除**：v2 多出的 `auto` 是路由别名而非模型（无价格、无厂商），按 id 显式排除；判据不取「没有倍率」，以免平台给它定价后它悄悄回到花名册。
- **会员门槛字段随端点迁移**（`is_member_only` 布尔 → `allowed_subscription` 列表）：映射规则为「列表非空即会员」，已对 v1 逐行核对 **8/8 一致**，属**重新编码**而非重新解释。
- **修：改价不触发重注册**（`catalogSignature` 现纳入倍率）。签名原先只编码 `id:vision`，而倍率已进显示名——`glm-5.2` 从 `1.85` 改到 `9.99` 时签名**完全相同**，选择器会一直显示旧倍率。平台在促销边界上确实会改价（`glm-5.2` 当前带 `限时七折`），所以这是会真实发生的路径。**操作者手填的伪倍率仍不触发重注册**（它在签名之后才挂上），原裁定不变。
- **修：AgnesCode tab 在文档隐藏时停止轮询**（与 `PanelPage` 对齐），不再在后台空转。
- **修：`render.test.mjs` 的午夜 flake**：`now + 1h` 在 23:00 之后跨到次日，而断言仍要求「同日」的裸时钟——每天 23:00–00:00 之间该门禁固定转红 1 项。改为按构造恒为当天的时刻。详见 `docs/PITFALLS.md` §48（与 §41「护栏的存活守卫自己漏了平台假设」同族，这次漏的是**时间**假设）。
- **文档**：`docs/AGNES-API.md` §7.3 记入「单次 chat 请求图像上限 4 张」的 400 实测（官方文档未公示该上限）；`docs/PITFALLS.md` 新增 §48（午夜 flake）与 §49（`num()` 把「免费」塌成「未声明」）。
- 影响面：`src/host/{agnescode.ts, agnescode-models.ts, provider-publish.ts, util.ts, routes/agnescode.ts}`、`src/client/{agnescode-tab.ts, wire.ts, styles.ts, i18n.ts}`、`test/{agnescode, publish-core, render, live-agnescode}`、`docs/{ROADMAP, AGNES-API, PITFALLS, TESTING}`。

## [0.9.0] — 2026-10-03

### UI：开关统一为项目品牌色，积分额度拆成三张卡片

- **开关改用 `--agnes-brand`（#1E40AF 科技蓝）**，不再跟着 DSH 的中性色走：DSH 的 `--dsw-alias-brand-primary` 在暗色模式下解析为近黑 `#0f1115`，开关 ON 态看起来像个空白白色胶囊（2026-10-03 实测截图确认）。项目自己的蓝色已在 tab 激活下边框、额度进度条、用量进度条、模型 checkbox accent 中使用，开关纳入同一套 token。几何仍沿用 DSH 原生 Switch（36×20 capsule、2px padding、16×16 thumb、`translateX(16px)` 运动、120ms ease），颜色用项目自己的。
- **新增共享 `Switch` 组件**（`src/client/switch.ts`）：提供方开关、出图/视频工具开关、AgnesCode 开关统一走这一个组件，消除三份重复的胶囊样式。原生 `<input type="checkbox">` + `appearance:none`，浏览器自动暴露 `aria-checked` 与焦点环，键盘可达性免费。
- **积分额度从一张宽卡片拆成三张小卡片**：原卡片在 861px 面板宽度内只占左边约 200px（标题「积分余额」+ 大数字 1200 + 小字「时效 1200 · 永久 0」），右侧约 660px 空白。现拆为「积分余额 / 时效 / 永久」三张 min-width 卡片（`flex: 1 1 160px` + `minWidth: 120` + `flexWrap: wrap`），面板宽时一排、窄时换行堆叠。字典 key `agnescode.balanceSplit` 退役，新增 `agnescode.balanceTimeLabel` / `agnescode.balancePermanentLabel`。
- **模型清单头部重排**：原来一行塞了三种不同性质的信息（标题 + 机制说明 + 实时默认档位）。现标题与默认档位一行（左右分布），机制说明移到卡片底部 footnote。
- **移除「解除关联」**：它的两个副作用（删除本地 JWT + 注销 provider）已分别由「检测本机登录态」（重新采集覆盖 JWT）与「注册 AgnesCode 提供方」开关（关闭即注销）覆盖，按钮没有独立能力，纯增加误触风险。`POST /agnescode` 的 `logout` action、host route handler、字典 key、面板按钮一并删除。
- 影响面：`src/client/{switch.ts, provider-controls.ts, model-picker.ts, agnescode-tab.ts, i18n.ts}`、`src/host/routes/agnescode.ts`、`docs/{API.md, ARCHITECTURE.md}`、`test/{render, agnescode}.test.mjs`。

## [0.8.1] — 2026-10-03

### 修复：AgnesCode provider 补全思考契约——档位可用，`off` 诚实不提供

- **背景**：AgnesCode 是桌面端上游，v1 一直按「wire 通道未验证」把 `reasoning` 钉成 `false`，DSH 里看不到思考档位。2026-10-03 对真 BFF（`agnes-3.0-flash`）补探 7 发，推翻两条假设。
- **实测**：`{bffBase}/v1/chat/completions` 在**不发任何思考字段**时仍返回 `message.reasoning_content`（406 字 / 320 reasoning_tokens）——默认即思考开；`reasoning_effort` 的 `none`/`low`/`medium`/`high` 全被接受（都 200）。
- **关键发现**：`reasoning_effort:"none"` **关不掉思考**（仍回 257 字 / 188 reasoning_tokens）。桌面端真正关思考的开关是 `request_params.agnes_thinking_enabled=false`（或 `thinking_effort:"off"`），而 pi-ai 的档位映射是字符串值、表达不了它。
- **落地**：descriptor 翻 `reasoning:true` + `agnescodeThinkingLevelMap()`（`off:null` 诚实不提供，避免「点关思考实际还开着」的静默谎言；`low/medium/high` 开；`xhigh/max` 关，BFF 未实测）；profile 钉 `reasoning: DEFAULT_REASONING_EFFORT`（high），与 Token Plan 侧对齐。
- 影响面：`src/host/{agnescode-models, agnescode-llm-adapter}.ts`、`test/agnescode.test.mjs`、`docs/ROADMAP.md` §6.3、`lib/`（按 ADR-008 重建入库）。

## [0.8.0] — 2026-10-02

### 修复：AgnesCode provider 必须进 tab 才启用（mount seed 竞态）

- **现象**：插件更新后重启 DSH，AgnesCode provider 不注册；凭据没变、桌面 App 没开过，但只要**进一次插件页的 AgnesCode tab** 就自动完成启用 + 热重载。
- **根因**：mount seed 只跑一次。它需要两个可能**晚于本插件**注册的服务——读凭据的 `credentials` 服务、注册用的 `llm` 服务。Host 启动早期两者未就位时，seed 撞上 `not_configured`（凭据读不到）或 `no llm registration service`，随即静默放弃，没有重试；唯一能救它的是 tab GET 的自愈，而 GET 只在打开 tab 时才发——于是「进 tab 才启用」。
- **修法**（对齐小浣熊线 `seedRaccoonOnMount` 的既有做法）：
  - `util.ts` 移植 `retryBounded`（线性退避）。
  - `agnescode-lifecycle.ts` 的 mount seed 改为有界重试（6 次 × 300ms，`AGNESCODE_SEED_ATTEMPTS`/`AGNESCODE_SEED_DELAY_MS`）：每轮重读开关 + 凭据 + 注册状态，`registered===true` 或 `isDisposed()` 即停；开关每轮重读，并发关掉也被尊重。
  - `routes/agnescode.ts` 的 GET 自愈从「只修 `not_configured`」放宽为「开关 on + 已关联 + 任何未注册错误」——覆盖 seed 窗口耗尽但 llm 仍未就位的残局。
  - `lifecycle.ts` 的 draw/video 工具读 `tools` 服务改为 `resolveServiceWithRetry`（与工具线同款防护：只重试读、绝不重试注册）。
- 影响面：`src/host/{util, agnescode-lifecycle, lifecycle, routes/agnescode}.ts`、`test/agnescode.test.mjs`。

### 破坏性改动：AgnesCode provider id 改为裸产品名 `agnescode`

- **改动**：provider id `agnes-agnescode` → `agnescode`，显示名 `Agnes AgnesCode` → `AgnesCode`。
- **为什么**：`agnes-agnescode` 读起来是结巴——供应商前缀 `agnes-` 重复了产品名里本就有的 "Agnes"（显示名 `Agnes AgnesCode` 同样）。兄弟插件 `dsh-connect-workbuddy` 的 id 就是裸产品名（`workbuddy` / `workbuddy-global`），本线对齐这个粒度。**撞名论证不依赖前缀**：裸 `agnescode` 与 `sensenova-*` 命名空间仍不相交，`dsh-connect-sensenova-token-plan` 的 `DUPLICATE_ADAPTER` 风险不变；`test/agnescode.test.mjs` 的钉字面量从「在 agnes- 命名空间」改为「是裸产品名、既非 sensenova- 也非 agnes-agnescode」。
- **迁移**：**不需要重新勾选**——开关文件 `agnescode-provider.json`、模型勾选 `agnescode-models.json`、凭据引用 `AGNESCODE_CREDENTIAL` 的名称都没变，只有注册用的 id 变。Host 重启后 mount seed 会在新 id 下重新注册（上一节修好的有界重试正是这条路径的兜底），旧 id 的注册随进程结束自然消失。（历史上 `sensenova-agnescode` → `agnes-agnescode` 那次需要重勾，是因为当时 seed 是单次、可能撞上服务晚到；这次 seed 已收敛，无此要求。）
- **影响面**：`src/host/{agnescode-models, agnescode-llm-adapter}.ts`、`test/agnescode.test.mjs`、`README.md`、`AGENTS.md`。

## [0.7.0] — 2026-10-02

**AgnesCode 面板从「只读花名册」升级为「可勾选的模型推送给」**：每个模型一行勾选框，决定哪些模型注册进 DSH 模型列表，改动即时生效；配套搜索 + 全部勾选/取消 + 已勾选计数，与 Token Plan 那套同一形状。勾选存在独立的 profile 级状态文件，**空 = 不筛选 = 全推**，所以没勾过的安装行为完全不变。面板信息架构另有一刀（额度 / 累计 / 套餐拆成三个 section）。另修掉一个「删注释时误删三元条件行、构建失败、插件无法加载」的问题。

### 评审整改：API.md §1 补上 `/agnescode` 的 `saveModels` 动作与 `enabledModelIds` 字段

- **`/agnescode` 契约文档漏记已上线能力**：代码（`routes/agnescode.ts:248`、`lib/index.js`、`client.js`、`agnescode-tab.ts`、`agnescode.test.mjs`）早已实现第三个 POST 动作 `saveModels`（持久化花名册勾选并即时重发过滤后 roster），且 GET 响应含 `enabledModelIds` 推送允许清单，但 `docs/API.md` §1 仍写「POST 动作两种」、GET 形状也未列 `enabledModelIds`。本次把 POST 改为三种动作、GET 形状补 `enabledModelIds`，与 CHANGELOG「AgnesCode：模型勾选」那条及路由代码对齐。
- 影响面：`docs/API.md`（§1 agnescode 块）、本文件。

### AgnesCode：模型勾选决定哪些模型推送进 DSH

- **能力**：AgnesCode tab 的模型花名册每行加勾选框（未勾选行降低透明度），决定哪些模型注册进 DSH 模型列表；保存后**立刻**重发一次过滤后的 roster，不必等下一次轮询。
- **完整版交互**：搜索框（`autocomplete="off"`，避免浏览器密码管理器把控制台账号填进这个 tab 唯一的文本输入）+ 全部勾选/全部取消（作用于可见行）+ 已勾选/共 N 计数；搜索只过滤「渲染什么」，不改变「保存什么」。
- **持久化**：新增 profile 级状态文件 `agnescode-models.json`（空列表 = 不筛选 = 全推，与 Token Plan 侧同一约定）；`/agnescode` 路由新增 `saveModels` 动作。
- **隔离**：独立的 store 与过滤点（`agnescode-models-store.ts` + `filterAgnescodeRows`），只在发布前裁剪 roster，不与 Token Plan 的 `enabledModelIds` 或 publisher 状态串味。
- 影响面：`src/host/{agnescode-models-store, agnescode-models, agnescode-lifecycle, index, routes/agnescode, types}.ts`、`src/client/{agnescode-tab, i18n}.ts`、`test/agnescode.test.mjs`。

### 修复：删除注释时误删三元条件行，导致构建失败、插件无法加载

- **现象**：插件报 `Cannot find module 'lib/agnescode-llm-adapter-DkN4N4JD.js'`，Host 起不来。
- **根因**：一个临时脚本按正则删「冗余注释」，误删了 `src/client/account-form.ts` 里三元表达式的条件行（`cooling`），rolldown 解析失败、整次构建中断，`lib/` 从未重建——而 `index.js` 还引用着旧的 chunk。
- **修法**：补回条件行；修复另两处被删掉句子主语/替换成重复 `@param` 的注释；仓库换行统一回 LF；删除临时脚本。

### 面板信息架构：额度 / 累计 / 套餐拆成三个 section

- **问题**：`积分额度` tab 原来只有两个 section——「额度上限」里塞了当前套餐 + 四个窗口池 + 账号累计用量，「用量统计（近 30 天）」里只有柱图；套餐目录（catalogue）折叠在 PlanCard 内部。这导致三件彼此无关的事（我的窗口额度 / 我的历史累计 / 平台卖的所有套餐）共享同一个 section 标题「额度上限」，名字只覆盖其中一件；账号累计（无上限）挤在「额度上限」section 里，而真正的用量数据（柱图）在另一个 section，读者要把两个 section 的数字相减才能算「剩余」——这个减法没有意义（窗口周期 ≠ 账号累计周期）。
- **修法**：拆成三个 section：
  - **「我的额度」**（`section.quota`）：当前套餐身份 + 四个窗口池 + vision 模型行。只讲「我当前能用多少」。
  - **「套餐对比（{count} 档）」**（`section.catalogue`）：`CatalogueCard`（从 `PlanCard` 剥离），独立 section，默认折叠。未登录时它是唯一的额度信息源；已登录时它回答「升级值不值」，不再与读者自己的数字挤在同一张卡里读起来像推销。
  - **「我的用量」**（`section.usage`）：`windowNote`（「不能相减」的诚实声明）+ 账号累计用量 + 近 N 天柱图。窗口在上一 section，累计在本 section，声明放在两者之间。
- **`windowNote` 跟着累计走**：「下面的上限 / 下方的账号累计」这段诚实声明原来夹在 PlanCard 与 UsageTotals 之间（同一个 section 内），现在随累计移入「我的用量」section 开头——读者进入这个 section 先看到声明、再看到累计数字。
- **`section.usage` 不再含 `{days}` 占位符**：柱图的日期范围由 x 轴自带，section 标题「我的用量」是稳定标签；「近 {days} 天」作为 chart 上方的小标题（`usage.period`），与累计用量分开。
- **`PlanCard` 不再吃 `quota.plans`**：它的空态判定从「plan null + windows 空 + catalogue 空」收窄为「plan null + windows 空」；`CatalogueCard` 在 `plans` 为空时返回 `null`，section 整块隐藏（不画空卡）。
- 影响面：`src/client/{i18n, cards, index, panel-page}.ts`、`test/{render.test.mjs, panel-render.js}`；`quota.catalogue` 键退役（`section.catalogue` 接管，带 count），`usage.period` 键新增。

### AgnesCode：存储格式漂移哨子——桌面端升级不再冒充「文件不存在」

- **问题**：AgnesCode 线吃的是从桌面 App 逆推的私有格式。App 升级改了会话文件形状（如 `code-auth-session.cn.v2`）时，严格模式全 miss，面板只给出与「没登录」**同款**的「文件不存在」档；而此前采集的 JWT 还能用约 28 天——没人会注意，直到凭据死掉、重登重采也无济于事的那天。
- **修法**：检测步进新增第 9 档 `format_drift`——App 目录存在、里面有「像会话文件族」却不认识形状的文件时，第一档就说**「存储格式已变，升级本插件后再点检测」**，不再把人往「重新登录」上引。同时 `doctor` 新增机器级**只读盘点**：`%APPDATA%/AgnesCode` 的会话文件名清单与漂移点名——「没装 App / 装了没登录 / 登录了但格式变了」在命令行即可分辨；盘点只看文件名，**永不读文件字节、永不碰解密密钥**。
- **诚实性**：有可读的 `.v1` 时漂移声明**不成立**（同目录的未知兄弟可能是 App 自留备份）；平台没有已知 App 布局时如实报「未盘点」，不报零。

### 修复：插件卡在中文界面显示英文文案

- **现象**：Plugins 页卡片标题是裸包名、描述是一段英文——全程无报错（`docs/PITFALLS.md` §33）。
- **根因**：卡片文案由 Host 的 `readPluginMeta()` 读**包元数据**（`locale/*.json` 的 `meta.title`/`meta.description`），不经插件 bundle 渲染。本插件没有 `locale/` 目录、`exports` 也没有该资源子路径——解析抛 `ERR_PACKAGE_PATH_NOT_EXPORTED` 被读取端当「无元数据」吞掉，落回 `package.json.description` 的英文兜底（`displayName` 这条链路根本不读）。
- **修法**：新增 `locale/en.json` + `locale/zh.json`（标题「Agnes Token Plan 接入全家桶」与中文描述），`exports` 加 `./locale/*.json` 子路径，`files` 白名单加 `locale`（三者缺一即哑，见 PITFALLS §33）；`package.test.mjs` 新增检查 7 把这条链钉死。

### 破坏性改动：AgnesCode provider id 改入 `agnes-` 命名空间（避免与兄弟插件撞名）

- **改动**：provider id `sensenova-agnescode` → `agnes-agnescode`，显示名 `SenseNova AgnesCode` → `Agnes AgnesCode`。
- **为什么**：兄弟插件 `dsh-connect-sensenova-token-plan` 注册 `sensenova-*` id；重复 id 会被 `registerAdapter` 以 `DUPLICATE_ADAPTER` 拒绝，其中一个 provider 会从选择器**静默消失**（`docs/ARCHITECTURE.md` §5）。本插件走 Agnes 体系，id 统一 `agnes-*`，与 `sensenova-*` 命名空间不相交，撞名不可能；`test/agnescode.test.mjs` 把 `agnes-agnescode` 字面量钉死，防止有人改回去。
- **对已有安装的影响**：曾启用 AgnesCode 提供方的安装需**重新勾选一次**（provider id 变了，旧注册不再认领）。凭据引用 `AGNESCODE_CREDENTIAL`、switch-store、publisher 全部不变，**无需重新采集登录态**。
- **一并**：`llm-error-fix.ts` 的 429 分诊注释把「商汤方言」定位改为「判据方言无关 + Agnes 未实测但有兜底」（判定逻辑未动——它本就只读 `type` 字段名与消息词，未硬编码商汤码值）；`test/error-fix.test.mjs` 补一组「无结构化 `type` 的 Agnes 兜底」夹具。

### 修复：做过 profile 分段迁移的机器上，draw / video 开关重启后自己关掉

- **现象**：面板打开「出图」或「视频」开关后，**下一次 Host 启动它自己回到配置默认（关）**——无报错、无告警，`doctor` 也读不出异常。只在经历过 §23 profile 分段迁移的机器上出现，且**只有 draw / video 中招**，provider 与 AgnesCode 正常。
- **根因**：四个开关商店是逐字互抄的四份代码。§23 的「继承旧共享目录的值」回调在布尔-only 的两份里收到布尔、在带偏好的两份里收到 `{enabled, modelId}` 对象，而写回体照抄了布尔版——把整个对象塞进 `enabled` 字段。进程内的读缓存掩盖了它，所以只有重开 Host 从磁盘重读才腐烂（`docs/PITFALLS.md` §34）。
- **修法**：四个开关商店收敛到一个共用构造（`src/host/switch-store.ts`），各自只声明不同的事实（文件名 / 形状版本号 / 偏好 wire key / 报错措辞）；`doctor` 的三份解析器改为复用商店自己的那份。新增 `test/switch-store.test.mjs` 对**四个商店跑同一组断言**（含「继承后重开仍读得到」），此类复制型漂移从此会红。

### doctor 新增一行：不带 Origin 的写请求留痕

- **改动**：`npm run doctor` 的报告多出一行 `admission: N state-changing request(s) admitted with no Origin (last: <方法> @ <时间>)`；`--json` 报告新增 `admission` 字段。没有记录过就不显示这一行。
- **为什么**：同源闸在请求未声明 `Origin` 时放行，这是**必须**的（浏览器的同站 GET 不发 `Origin`；跨站 POST 必发、发了就被比对拦掉），所以那条分支上过的只可能是非浏览器客户端。它本身不是漏洞，但此前完全不可见——`doctor` 给不出"有没有发生过"。
- **不是加固**：放行逻辑**一字未动**（`routes` 190 / `wiring` 44 / `doctor` 39 零回归）。记录只含次数、时间与方法名，**不含** Host / Origin / 路径 / 头值 / 凭据（`docs/PITFALLS.md` §35）。

### 可读性：11 处手抄的「面板值 > 配置默认」收敛到唯一裁决模块

- **现象**：「面板存过的值胜过补丁声明的默认」这一条开关优先级规则，被手抄在 11 处调用点，且有两种长得几乎一样、语义却不同的方言——有默认（`panel ?? settings.x === true`，用于 provider / draw / video）与无默认（`panel === true`，仅 AgnesCode：它根本没有配置默认，未设置必须落到 `off`）。两者靠肉眼分不出来。
- **风险（不仅有可读性）**：① 把无默认型误读成有默认型，会让 AgnesCode 在补丁某天多出一个默认值时自行注册起来；② `provider-publish.ts` 里 `panelValue ?? effectiveSettings.registerProvider === true` 实际按 `panelValue ?? (x === true)` 解析——结果正确但**读起来像** `(panelValue ?? x) === true`，诱导出错误编辑；③ `snapshot-aggregate.ts` 里「值」与「来源」各读一次开关，两次读取之间一翻（或第二次失败）就会输出 `true` 却标注来源为 `config`；④ 旧写法 `(store ? store.enabled() : null).catch(...)` 在 store 缺席时是对 `null` 调 `.catch`，抛 `TypeError` 而非回答 `null`，只是当前调用方都恰好传了 store 才没炸。
- **修法**：新增 `src/host/switch-precedence.ts`，三个函数（`resolveSwitchEnabled` / `resolveSwitchValue` / `readPanelValue`）承载全部判定，值与来源一起返回；11 处调用点改为调用它（AgnesCode 三处严格按「无配置默认」解析）。`readPanelValue` 安全接受缺席的 store（旧陷阱被消除）。新增 `test/switch-precedence.test.mjs`（23 条）钉住两种语义分界与上面四类缺陷，并带「接线钉」：任何文件仍自己拼装优先级、或 AgnesCode 三处传了配置默认，即红。重构前后 `routes` 190 / `provider` 204 / `agnescode` 119 / `doctor` 39 / `switch-store` 84 / `admission-audit` 27 逐项计数一致零漂移。

## [0.6.0] — 2026-10-01

**面板信息架构再收一刀**：顶栏从「页面标题栏」降格为「当前 tab 的状态与刷新出口」——撤销假的全局标题，「更新于」/令牌状态/刷新按 tab 归属；额度窗口卡片改以**百分比为主视**并按职责分池；三个 tab 补上常驻的官网入口。**AgnesCode 这条线修掉两个叠加的旧病**：GET 路由的 `lastHarvest` 落在暂时性死区，让「检测本机登录态」点了没反应；以及「开关先开、令牌后到」留下的 `not_configured` 被当作红色错误报警。推理侧把单次输出上限从兜底的 32768 提到实测平台上限 65536，契约基线 seed 一并退役、换真机全阶梯证据。

### 修复：AgnesCode tab 开关提供方时闪现红色报错「not_configured」

- **自愈通道（Host）**：GET 路由原来只读不修——「开关先开、令牌后到」留下的 `not_configured` 会一直挂着，直到用户手动点击什么。现在 GET 发现「开关开 + 已关联 + providerError 为 not_configured」这一陈旧组合时，自动补一次发布（每 60s 至多一次，避免持续失败的发布被面板轮询反复重建适配器），下一拍轮询即可看到修复后的状态。手动采集保持手动——解锁桌面 App 的加密会话文件是隐私敏感操作，只在用户点击时执行；采集之后的一切（启动 seed、采集后发布、GET 自愈）全自动。
- **现象**：勾选「启用 AgnesCode 提供方」的瞬间，面板弹出一行红色 `not_configured`，检测到登录态后又自己消失——看起来像一次自愈的故障。
- **根因**：`not_configured` 是发布门的**预期中间态**（「开关已开、令牌还没有」），Host 语义无误；错在客户端把它当原始错误文本、以 alert 样式直渲染。检测/关联完成后发布门清掉错误，于是「又消失」。
- **修法（客户端）**：未关联时不再渲染它——「已启用——检测到本机登录态后即可注册模型」一行已经说了同一件事；已关联时降级为一句普通状态文字「凭据未就绪——点『检测本机登录态』重新采集后再试」，不再红色报警。其余 providerError 仍按错误渲染。

### AgnesCode tab：凭据卡片一行式 + 「已注册」pill

- **AgnesCode tab 向小浣（商汤）参考件对齐排版**：凭据卡片改为**一行式**——「已关联：昵称」居左，「检测本机登录态」「解除关联」两个按钮居右（原先三行状态加两行按钮，声明一个「你已登录」要花五行）；积分余额与时效/永久拆分合并为**一行**小字（`agnescode.balanceLine`，凭据有效期不并入——凭据卡里已经有一遍，重复的日期读起来像两个截止时间）；「已注册」从一句普通文字改为模型清单头部的**绿色 pill**，与「模型（N）」同排——注册状态骑在它描述的清单上，而不是在清单上方独自成句。

### 面板内常驻官网入口：「接入 API」tab 的额度管理链接与 AgnesCode tab 的客户端下载

- API Key 卡片原来只在**未配置** Key 时显示「前往官网免费获取 →」；现在配置了 Key 也会常驻一条「前往官网管理额度 →」，两条文案按状态切换、指向同一官网地址。与「积分额度」tab 有意重复——离开面板去找控制台不应要求记住网址。
- **AgnesCode tab 底部常驻客户端下载入口**：`agnes-ai.cn/agnescode`（产品页）以「下载 AgnesCode 桌面客户端，领取限时积分 →」一行小字钉在 tab 卡片底部，**未关联与已关联都显示**——已关联的账号同样要靠重开桌面 App 续期凭据（JWT 无刷新端点），且新用户安装可领限时积分。这是三个 tab 里唯一要求用户机器上装有额外软件的流程，此前面板里没有任何入口。

### 额度窗口卡片改为百分比主视；移除静态套餐文案「1500 次模型请求 / 5 小时」

- **窗口卡片以百分比为主视**：平台在 `subscription.usage` 里报出某窗口的 `used` 时，卡片的大字是该窗口的消耗百分比（转写平台自己的 `used / limit`），`已用 X / Y` 的原始计数降为下方小字。原来的「上限作大字」在窗口耗尽时会读起来像还有量。
- **移除 `usageLimitText`**：套餐目录的 `usage_limit_text`（「1500 次模型请求 / 5 小时」）是**不随用量变化的静态文案**，额度用光时它照样显示 1500——误导来源，整条拔掉（解析 / 投影 / wire / 渲染）。5 小时窗口形态由 `concurrency_window_h` 字段本身给出，不依赖它。
- **窗口卡片按职责分池**：四个窗口不再平铺一行「模型请求 / 每周请求 / 生图 / 视频」，改为**两张池卡片**——「模型请求」池内含「5 小时」「每周」两张子卡（卡片头直接写窗口），「多媒体」池内含「生图」「视频」两张子卡（每日作小字角标）。池卡片共享一张 auto-fit 网格：宽屏下两池左右并排铺满一行，窄屏上下堆叠，消灭「每周请求 / 每周」这种名词与周期互相重复的卡片头，也消灭右侧留白。未知维度的窗口落进无标题的池，不会被分组吞掉。
- **卡片底部合并为一行**：`已用 X / Y` 与重置时间共用底部同一行（左计数、右重置），不再上下两行占两倍纵向空间。零上限窗口没有分数可画，也不显示「已用 0 / 0」。
- 没有报 `used` 的窗口（今天的 video）维持上限作主视、不画进度条，行为不变。

### 面板顶栏降格为「按 tab 的 shell」：撤掉全局标题与全局刷新

- **撤掉假全局标题**：顶栏原来固定渲染「积分面板」，而它只是三个 tab 里第一个的名字；Plugins 页在这个卡片**上方**已经渲染了插件自己的名称与描述，所以那是第二遍、且口径更窄的标题。顶栏不再有标题（`panel.title` 随之从字典删除）。
- **状态与刷新按 tab 归属**（新决策函数 `barPlan`，纯函数、随 `panel` 测试面导出，由 Node 套件直接驱动）：
  - 「积分额度」tab：快照的「更新于」+ 令牌 chip + 陈旧数据告警 + 刷新快照；
  - 「接入 API」tab：同一份快照（它渲染的就是快照里的 `llm` 块），因此同样的「更新于」与刷新，但**不再挂令牌 chip**——那枚令牌是控制台凭据，这个 tab 用的是已保存的 API Key；
  - 「AgnesCode」tab：它读自己的路由、按 60s 自己的节奏轮询，所以由该 tab **自己发布**「更新于」与刷新。此前顶栏在这一 tab 上引用的是快照的时间戳，刷新按钮也只重载快照——两者都指向**另一个 tab** 的数据。
- **顺带修掉一个看不见的失败**：AgnesCode tab 的路由读取失败时 `error` 只被 set、从不渲染，于是「Host 没回话」显示成「未关联——请先在桌面端登录」，把原因甩给了用户的桌面 App。现在失败会明说，且这一刻不再声称「未关联」（面板没有这个证据）；顶栏刷新按钮在该 tab 尚未发布 loader 时置灰，而不是静默重载另一个 tab。
- 契约：`barPlan` 的决策由 `test/render.test.mjs` H3 驱动，接线与「失败必须渲染」由 `test/panel.test.mjs` F6 钉住；`docs.test.mjs` 检查 `README_TABS` 依赖 `activeTab` 的字面联合类型，源码注释已写明不要改成类型别名。

### 修复：AgnesCode tab 的「检测本机登录态」点了没反应（GET 路由每次都抛错）

- **现象**：桌面端已登录、点「检测本机登录态」也回「已读取本机登录态。」，但卡片仍然停在「未关联」——凭据其实早已写进 DSH 凭据服务，面板只是永远看不到它。
- **根因（Host 侧，`routes.ts`）**：AgnesCode 路由把「上一次探测记录」`lastHarvest` 声明在**处理函数内部、GET 分支之后**。GET 分支先 `return`，于是 `agnescodeState()` 读到的是处在**暂时性死区**的 `let` 绑定，整个 handler 抛 `ReferenceError: Cannot access 'lastHarvest' before initialization`，Host 把它变成**无 body 的 400**。POST（harvest/switch）在该声明之后执行，所以按钮全好——只有 tab 进场的 GET 是坏的。面板因此永远拿不到 `loggedIn`，5 分钟轮询也修不回来。
- **修法**：`lastHarvest` 与 `agnescodeHarvestInFlight` 上移到**路由注册作用域**（每次注册一份，跨请求共享）。这同时兑现了原注释里「GET 也带上次探测记录」「并发 harvest 合并成一次 walk」两句话——在此之前两者都只在单个请求内成立。
- **客户端加固**：`load()` 此前 `if (!response.ok) return;` 静默返回，`state === null` 且 `error === null`，于是渲染出「未关联——请先在桌面端登录」——把 Host 的故障说成用户的桌面 App 没登录。现在非 2xx 会把状态码写进 `error`，由「失败必须渲染」那条路径明说。
- 契约：`test/agnescode.test.mjs` 新增「route surface」段——用假 ctx 驱动真实 `registerRoutes` 并对 `/agnescode` 发 GET，钉住 200、`loggedIn` 与「payload 不带 token」。

### 推理通道：单次输出上限从 32768 提到平台上限 65536

- Token Plan 推理线的每个请求此前实际携带 `max_tokens = 32768`。这不是平台限制，而是 `dsh-llm-pi-ai` 注册兜底（`DEFAULT_MAX_TOKENS = 32768`，其校验强制要求正整数）——descriptor「不声明值防截断」的旧决策因此失效：未声明≠无上限，而是被砍半，思考（默认 high 档）与回答挤在 32k 预算里，长回合先截思考。
- descriptor 现声明实测平台上限 `maxTokens: 65_536`（`PROBED_MAX_TOKENS`）。真机探针（2026-10-01，`api.agnes-ai.cn/v1`）：32768 / 65536 / 缺省不发均 200（65536 在 agnes-2.5-flash 与 agnes-2.0-flash 复测通过），131072 → 400 平台原文「max_tokens 不能超过 65536」。每轮实际上限为 `min(65536, contextWindow − prompt − 4096)`。
- 契约钉点同步：`test/provider.test.mjs` / `test/contract.test.mjs` 翻正为钉 65536；`docs/AGNES-API.md` §7.3、`docs/ARCHITECTURE.md` §5.2 同步。

### 契约基线 seed 退役：真机全阶梯回填（选择器档位面变化）

- `test/baselines/agnes-contract.json` 从单条 SenseNova 时代 seed（`deepseek-v4.1-flash`，全 pending）回填为 2026-10-01 真机实测：live 目录 7 条（`agnes-2.5-pro` / `-alpha` / `-beta` 当天内消失，已记入漂移），三个 chat 模型的 `reasoning_effort` 全阶梯 + thinking 字段拼写（统一 `message.reasoning_content`）逐项取证，全部附平台响应原文于 `driftLog`。
- `PROBED_EFFORT` 随之换血（Agnes id + 实测 cell），选择器档位面变化：**`max` 档在全部三个 Agnes chat 模型上开放**（SenseNova 时代 max 是 glm 独占），`xhigh` 仅 `agnes-3.0-flash` 开放（2.0/2.5 被平台校验器 400，其自报合法集 `'none', 'low', 'medium', 'high' or 'max'` 不含 xhigh）；`off/low/medium/high` 面不变。
- `live-contract` 新增 §2c `max_tokens` 上限双向护栏（上限须仍 200、两倍须仍 400），基线 `meta.maxTokensCap` 固化该事实；`docs/AGNES-API.md` §7.1/§7.2 的 ⏳ 待实测注记了结。

## [0.5.0] — 2026-10-01

**独立仓库首次发布**：新包名 `dsh-connect-agnes-token-plan` 首次上 npm。全面更名并切换登录线（Agnes 一跳登录），额度模型改为账号级四窗口，新增视频生成 agent 工具与第三上游 AgnesCode。

> **版本号说明**：0.4.4 从未发布到 npm（原 0.4.4 章节内容并入本节）；旧包名 `dsh-connect-sensenova-token-plan` 由兄弟插件独立维护，已发至 0.4.5。

### 全面更名 + 登录线切换（迁移主线）

- **包名 / provider / 工具 / Key 引用全切 Agnes**：`dsh-connect-agnes-token-plan`、provider `agnes-token-plan`、agent 工具 `agnes_draw_image`、Key 引用 `AGNES_TOKEN_PLAN_API_KEY`、控制台 `platform-backend.agnes-ai.cn` / 推理 `api.agnes-ai.cn`。
- **登录改为一跳邮箱+密码**：`POST {consoleBase}/api/user/login`——Agnes 无 OIDC / 无 refresh token，令牌失效即**重登一次**（重登是唯一的续期路径）；密码明文 JSON 仅走 TLS、不落盘，`AGNES_PASSWORD` 是唯一持久来源。
- **登录节流按平台声明窗口照单全收**：`Retry-After` 头与消息里的时间文案（中英文）都解析为等待窗口，绝不自行截短；平台未声明才回退本地指数退避。
- 随迁移退役的 OIDC 模块与测试已删除（`sensenova-auth.ts` / `sensenova-crypto.ts` / `test/auth.test.mjs`）。

### 额度模型：账号级四窗口（不再计算「剩余」）

- Agnes 不发布 per-pool 余额，只在滑动窗口上封顶四个维度（5h 请求 / 周请求 / 日图 / 日视频），控制台只给上限与**累计**用量——`limit − total` 无人能负责，所以面板头条从「剩余」换成「限额」，累计用量单独一行（宁少一个数，也不发明一个）。
- 控制台未连接时不再整页只剩登录表单；额度正常显示时不再误挂「需要重新登录」。

### 窗口内已用量（上线实测推翻旧结论）

- **结论更正**：原来认为「控制台不提供窗口内的已用量」，实测 `/api/cn/user/subscription` 的 `usage` 块**就是**控制台「当前用量」那一屏的数据源，每窗口带 `used` / `limit` / `time_range_start` / `time_range_end` / `reset_at` / `reset_in_seconds` / `usage_pct`。旧结论让面板只显示上限、进度条永远缺席——数据一直在已下载的订阅响应里，只是没人解析。
- **新增 `parseSubscriptionUsage`**（`parsers.ts`）：把 `usage` 归一成面板的四个窗口 key（`text_generation.windowed`→`requests5h`、`.weekly`→`requestsWeekly`、`image_generation.daily`→`imagesDaily`、`video_generation.daily`→`videoDaily`），时间戳按平台声明的 Asia/Shanghai 解析（UTC+8），缺字段读作 `null` 而不是 0。
- **`quota.windows[]` 并入平台给的 `used` / 窗口起止 / 重置时间**；客户端 `QuotaWindowCard` 在 `used` 存在时画进度条并显示「已用 x / limit · y%」与「重置 时间」。**仍然不做减法**：窗口用量是平台逐字转写的，账号累计是另一段周期，`limit − total` 依旧被拒绝。
- **契约与文案同步**：`EXPECTED_SHAPES.subscription` 由空数组填成真实身份键（`usage` 故意不要求，避免把「从未消费」误报成形状漂移）；`quota.windowNote` 中英文案、`docs/AGNES-API.md` §4 / §6 / 端点表、`docs/API.md` 相应更正。

### 视频生成 agent 工具（可选，默认关）

与出图对称新增 `agnes_video_generate`：面板开关 + 「建任务→轮询→取 url」状态机（`VIDEO_MAX_POLLS` 防呆、无 30s 冷却门）。

- **V2.0 参数体系**：`width`/`height`/`num_frames`(8n+1)/`frame_rate`；2.5 家族进 `video25ModelIds` 单独上报、不进对话选择器。
- **2.5 系列补齐（秒数制）**：`buildVideoBody25`（`seconds` 4–12 / `size` 720P·960P·2K / `aspect_ratio` 白名单 + `image`/`keyframes` → media、flash 收敛仅 720P 且 reference ≤5）双家族分派，互斥字段永不同时发给同一模型；`pickVideoModel` 认全量视频模型、目录无 V2.0 时回落到第一个 2.5 模型；帧数字段自动换算（`121@24fps → 5s`）、`negative_prompt` 不转发给 2.5；面板候选含 2.5、文案改为「已支持、自动换算」。
- **双向混发防护 + 错误点名实际模型**：`buildVideoBody` 只解构 V2.0 字段，所以 2.5 专有字段（`seconds`/`size`/`aspect_ratio`/`mode`/`keyframes`）发到 V2.0 模型原先是**静默丢弃**——agent 以为拿到 10 秒 2K、实际拿到 5 秒 720P 且无任何信号。现在 `defineVideoTool` 在分派前拒掉这个组合（`VIDEO25_ONLY_FIELDS`），报错逐字段点名并给出修法；反向（V2.0 帧数字段 → 2.5 模型）仍走换算，保留「不判断家族也能出片」的退路。2.5 家族的每个校验错误经 `video25ErrorContext()` 带上**实际解析到的模型 id 与所属体系**（`模型 agnes-video-2.5-flash（2.5 秒数制）：无效的 seconds 3：须为 4–12 的整数秒`），此前只说「2.5 系列」，得回读 catalog 才知道打到的是哪个模型。
- **工具描述与文档修正**：描述里不完整的 id `2.5-flash` 补全为 `agnes-video-2.5-flash`（避免 agent 照抄后白跑一次 400）并写明反向混发会被拒；README 与 `AGNES-API.md` 头部「只覆盖 V2.0 参数体系、2.5 不选」的过期表述改为双体系都覆盖；`API.md`「认不出的 id 退回自动选择」与实现不符（显式 `model` 原样直传、不查目录，只面板偏好 `videoModelId` 才查目录后回落），已改正并注明两条 id 的校验强度差异。

### 第三个上游：AgnesCode tab（可选，默认关）

- **本机登录态采集**（workbuddy 族先例）：微信登录发生在 AgnesCode 桌面 App 里，插件只读 App 的 os_crypt 加密会话文件（DPAPI + AES-256-GCM，注入式可离线测），探测失败按五档逐文件分诊（`file_missing` / `no_key` / `decrypt_failed` / `no_token` / `untrusted_base`）。
- 独立 provider `sensenova-agnescode`：接口 base **按账号跟随**会话文件且钉死在 Agnes 域名族；无刷新端点（JWT 约 28 天），续期 = 重开桌面 App 后「检测本机登录态」。
- 独立凭据引用 `AGNESCODE_CREDENTIAL`（引用值而非私有 kind）、独立 switch-store / publisher，与主注册零耦合；`reasoning:false`（thinking 线路未证实）、memberOnly 标记不隐藏（红线 7 精神）。
- 契约探针（ROADMAP §6.3）：CN BFF OpenAI 兼容、28 天 JWT、独立积分池、本机会话文件解密链已验证。

### 出图与其它修复

- 出图 `n` 硬限为 1、补全 `ratio`/`image`/`return_base64`（live probe 确认 size 2K/4K 常量与 extra_body 全接受）；出图模型识别补齐名称兜底（结构化模态判定 + 名称双链统一）；出图描述与成功 hint 修复。
- 浏览器不再把保存的控制台账号自动填进模型搜索框；sk-（免费版）与 cpk-（Token Plan）密钥前缀提示区分。
- 渐进式 checkJs 类型校验（激活既有 JSDoc 防静默漂移）。
- e2e 扩至 59 项实跑；全量离线门禁 + build-gate + e2e 绿。

## [0.4.3] — 2026-10-01

第二个上游（小浣熊）、面板从侧边栏归位到 Plugins 页、模型花名册重排，外加一批「状态翻成某个值后操作入口跟着消失」的可见性修复。

> **版本号说明**：0.4.1 与 0.4.2 都**未发布到 npm**（registry 上仍是 0.4.0）。本版把自 0.4.1 以来的全部改动一次收敛为 0.4.3，0.4.2 不再单独发布。

### 第二个上游：小浣熊 tab（微信扫码登录 + 独立 provider）

`ROADMAP §6.1` 的「第二上游」落地：接入 `xiaohuanxiong.com` 网关，与 Token Plan 积分池**相互独立**，全程 opt-in。

- **面板第三个 tab「小浣熊」**（`src/client/raccoon-tab.ts`）：开关、微信扫码登录、积分余额、模型花名册四块。文案中英双语（`raccoon.*` 17 键）。
- **Host 半边 peer-free 协议层**（`src/host/raccoon.ts`）：扫码登录走查、信封解析（成功判据是 `code === 0`，用普通 `Number` 比较——共享的 `num` 助手会把 `0` 当假值拒掉）、一次性 refresh-token 轮换、余额与目录读取、两态思考档位映射。
- **凭据走 DSH 凭据服务**（`src/host/raccoon-store.ts`）：存的是凭据服务引用而非明文；轮换后的新 pair 在续期时**回写**——这正是与「读桌面端文件」路线相比唯一能站住的地方。
- **开关按 profile 分段**（`src/host/raccoon-switch-store.ts`）：与 provider/draw 同一套文件-backed 纪律。
- **第二个独立 publisher**（`src/host/raccoon-publish.ts` + `raccoon-llm-adapter.ts`）：与 Token Plan 的注册完全隔离，小浣熊侧怎么折腾都churn 不到主注册。
- **自包含 QR 编码器**（`src/client/qr.ts`，455 行，零外部依赖）：byte mode、纠错等级 M、v1–10，登录 URL 直接在面板内编码成图。
- **测试**：`test/raccoon.test.mjs`（离线，覆盖协议、两个 store、描述符映射、publisher 状态机、QR 编码器、开关 store），已进 `npm test` 与 CI offline 档。

**小浣熊上线后连修四轮**（每条都是「面板看起来正常、实际不可用」）：

- **QR 编码器产出的矩阵没有任何解码器读得出来**：用 jsQR（`qrcode-decoder` 同款纯 JS 引擎）做解码验证时发现，首版从 v1 到 v10 全部解码失败——尽管套件里每一条结构断言都是绿的。五个缺陷全是绘制顺序或码表错误，最典型的是**定位图形（timing）画在三个 finder 之后**，把 finder 内部的第 6 行/列擦成黑白条纹；timing 必须先画，finder 后画才能覆盖。
- **扫码确认时「正在等待」不可见**：进行中的那次扫码对 tab 不可见。
- **QR 要等下一轮 60s 轮询才出现**：登录入口像没反应。
- **`fontSize` / `marginTop` 泄漏成 DOM 属性而非样式**：样式静默失效。
- **扫码登录拿到的昵称没存**：面板无法称呼用户，补存后正常问候。
- **小浣熊的积分倍率没进模型选择器**：面板花名册显示的 ×0.75 / free / ×0.2，在选择器里看不到（pi-ai 没有计费元数据通道，选择器只渲染名字）。倍率改挂进显示名——`GLM-5.3（×0.75）`、`SenseNova 6.8 Flash（free）`，倍率 1 保持裸名。两处同源，不会打架。

### 面板从侧边栏归位到 Plugins 页

- **去掉 `sidebar.panellist` 与 `main` 槽位注册**，只留 `plugins.bundle.config`：面板改为 Plugins 页内的**内联卡片**，不再占侧边栏一行（`src/client/panel-page.ts`、`src/client/index.ts`）。同时摘掉 layout 服务依赖与 onClose 按钮（Plugins 页自己管导航），`PanelPage` 的 `onClose` 改为按传入与否决定渲染。
- 这解决了「装了 5 个 connect 插件 = 侧边栏 5 行噪音」的问题，与 workbuddy 的形态对齐。

### 小浣熊模型列表：行形态与 Token Plan 花名册对齐

原来每个模型只有「名字 + 一个 English 徽章」，参数行整块缺席，读起来像个半成品。数据其实**早就在手上**——网关的目录行带 `context_window` / `max_output_tokens`，`fetchRaccoonCatalog` 自首个版本就归一化了这两个字段，`raccoonRoster` 也一路保留着，只有这个渲染组件把它们丢了：

- **补上参数行**：「256K 上下文 · 最大输出 64K」，复用 Token Plan 花名册的 `S.modelMeta` 与同两条文案模板（`llm.contextBadge` / `llm.metaOutput`）。目录没声明该字段时**不画这一段**（`num()` 对缺失值返回 `undefined`，不兜底编数）——面板只引述平台真给的东西。
- **倍率改为独立 chip**（`S.modelRate`，与 Token Plan 同款），`0` 显示 `free` 而不是 `×0`（零倍率是对模型的事实陈述，不是一个为零的费率）；新增 `raccoon.free` 键补齐 i18n。
- **倍率 tooltip 不复用 `llm.rosterRateTitle`**：那句话写的是「积分消耗**伪**倍率（自定义对比用，**非官方**）」，因为 Token Plan 的倍率确实是操作者在插件里配的；而小浣熊的倍率是**网关目录直接声明的字段**，借那句 tooltip 等于给真数据贴上"编的"标签。新增 `raccoon.rateTitle`「网关目录声明的积分倍率（0 为免费）」，并加断言钉住别再用错。
- **`· vision` 硬编码英文改为 `llm.rosterVision`「可看图」徽章**，靠右对齐——原先那个 `" · vision"` 是拼在倍率徽章里的中文字典里的英文裸串，既没走 i18n 也没走统一徽章样式。
- **给列表一个框**（新增 `S.modelPanel`）：与同 tab 里「已登录」块（`S.card`）同级的容器。行的分隔线样式**不变**——仓库既有原则是「section card 持有唯一的框、行不重画框」，缺的是列表这一层的容器，不是给每行描边。

`test/render.test.mjs` 补 5 条断言（150 → 155）：参数行在有字段时出现、在没字段时不出现为空行、费率 chip 渲染、free 不带 `×0`，以及**思考档位必须缺席**——该 provider 注册的是 `reasoning: false`（pi-ai 发不出网关唯一有效的 `extra_body.thinking`），照抄 Token Plan 那行「思考 关闭/低/中/高」等于承诺一个 DSH 选择器永远不会给的档位。均做过「删参数行 / 填入思考档位 → 红 → 还原 → 绿」验证。

### 「接入 API」tab：卡片改名、重排、默认展开

按「你为什么来这」而不是按依赖排序，三张卡全部改名 / 换位置（`src/client/panel-page.ts`、`src/client/i18n.ts`）：

- **「提供方注册与模型推送」改名「语言模型」**（`llm.providerTitle`，en: `Language models`），并移到**第一位**、默认展开。
- **「出图工具」第二位**，默认展开。两张功能卡都是用户进这个 tab 的理由；收起的卡把自己的开关藏起来，读起来像「这个功能没反应」。
- **「API Key」移到最后**并保持默认收起——它是前两张卡的**前置条件**，由它们指回来，而不是被配置的对象；卡内是密钥输入框，也不该是默认铺开的那一屏。

同时修掉一处被这次重排坑到的文案漂移：`llm.rosterEmpty` 原写「在**上方**保存一次 API Key」，而它所指引的 API Key 卡已经换到**下方**——方向词在换序后必然指错，改为直接点名「API Key」卡片（中英两侧同步；`draw.off`、`llm.noKey` 那两处「上方」指的是**同卡片内**的开关与输入框，不跨卡，保持不变）。

`test/panel.test.mjs` 补三条断言把这次的决定钉住：三张卡的渲染顺序、默认展开集合、跨卡指引必须点卡片名而非方向词（均做过「故意改回旧序 / 改回 false → 红 → 还原 → 绿」验证）。

### 截图与市场清单对齐（`docs` 门禁新增检查 `SCREENSHOTS`）

重截截图时文件名从 `panel-credit-pools.png` / `panel-provider-setup.png` 换了新名，磁盘与 git 都已同步，**唯独 `screenshots.json` 仍指着两个已删掉的旧文件**——工作树干净、构建通过、其余检查全绿，**没有任何东西在报错**，而它是市场页取图的唯一依据，推上去就是图裂。

- **清单对齐当前两张**：`assets/panel-credit.png`（「积分额度」tab）、`assets/panel-API-provider.png`（「接入 API」tab）；「小浣熊」那张待补。
- **`docs.test.mjs` 新增检查 `SCREENSHOTS`**：清单声明的每条路径必须**真实存在于磁盘**、是图片扩展名、条目数 1–8、且为仓库根相对路径。三种破坏实测红过再还原复验绿（换回旧名 / `..` 逃逸 / 拿 `README.md` 当条目）。
- **`PITFALLS` 新增 §26**（条目数 25 → 26，三处引用由门禁当场抓出并同步）：**清单类文件（`.json` / `.yml` / `.toml`）不是文档，但同样是契约**——改资产名前先问「还有谁按名字引用它」；本仓库按名字引用 `assets/` 的机器消费方只有 `screenshots.json` 一处，正是「只有一处 + 又不叫 `.md`」让它成了盲区。
- **`CONTRIBUTING` §8** 那句「重截并推送后截图自动生效（**文件名不变**）」已修正：它藏了个未言明的前提——**改名就必须同步清单**，而这次恰恰是换了名。

### 模型花名册重排（WorkBuddy 形态）

- **行去卡内框、改用分隔线**：`modelRow` 由横向 flex 行改为纵向列（头行 + 缩进的参数行），新增 `modelRowHead` 包裹头行；徽章只标 notable 态（删「纯文本」徽章、补「额度耗尽」徽章）。工具行计数成组右置，批量按钮 32px 与搜索框等高。
- **每行只留会变的 per-model 事实**：参数段从「上下文 · 最大输出 · 默认思考强度」改为**本模型实际可选档位**——「思考 关闭/低/中/高/极高[/最高]」，由 Host 新导出的 `supportedThinkingLevels` 按 pi-ai `getSupportedThinkingLevels` 同一规则过滤（与 DSH 选择器同源），glm-5.2 独显「最高」。provider 级常数（默认思考强度）收进花名册头部只说一次（`llm.rosterThinkingDefault`），**删掉逐行重复的恒定默认档**——用户锐评「恒定默认档逐行重复=噪音」，该反模式记于 `docs/ARCHIVE-IMPROVEMENTS-2026-09.md` §7（2026-10-05 归档）。
- **`tokenSize` 修正 1049k → 1M**：千整走十进制、纯二进制走 1024、≥1M 归 M。Host 侧新增 `maxOutputLength` 投影（`0` = 未声明则整段不画，绝不猜）。
- **`matchMultiplier` 单一匹配器**：趋势行 ×N 与花名册 ×N 同源同值，两处显示不可能不一致。

### 出图行的渲染回归修复

- **出图行把名字与徽章摞成了竖排**：`modelRow` 在花名册重排时改为纵向列并新增 `modelRowHead`，但当时只迁移了 `model-picker.ts`；`provider-controls.ts` 的 `DrawSwitch` 与 `raccoon-tab.ts` 仍是旧标记——`label` 与徽章成了纵向列的直接子元素，于是堆叠，且 `modelName` 的 `flex: 0 1 auto` 被压到近乎零宽。现两处都补上 `modelRowHead` 包裹（与 `ModelRoster` 同一契约）。
- **回归守卫**：`test/render.test.mjs` G6 组改为真正挂载 `DrawSwitch` 并断言行是列形态、没有任何一行把名字/徽章留在 `modelRowHead` 之外、每行恰好 3 个头行；`ModelRoster` 与 `RaccoonRoster` 共用同一组断言并带**非空性检查**（此前「0 行也算通过」的真空通过正是这次回归溜走的原因）。为让渲染套件能挂载，`raccoon-tab.ts` 把内联花名册抽成无 hook 的导出组件 `RaccoonRoster`。
- **出图选择器的两处显示修复**：勾选态不再叠在开关上；目录为空时说明原因（而不是留白）；自动选择且未钉模型时徽章点名实际生效的模型；出图块在自动选择时也下发（此前只在钉死模型时下发）。

### 面板注册引导双向化

- 账号表单标题下与 API Key 表单底部的官网链接**两种状态都显示**：无账号时「前往官网注册」，已有账号时「前往官网管理额度 / 获取 API Key」（新键 `auth.portalHint`，中英双语；URL 入 `const.ts`）。

### 思考档位收敛到「实测过 200 才画」（面板不再过度承诺 low/medium/xhigh）

面板花名册每行原来把 `low/medium/xhigh` 对**所有**商汤模型都画出来，但冻结契约表
`test/baselines/sensenova-contract.json` 里 `xhigh` 只实测过 deepseek-v4-flash（200）、
`max` 只实测过 glm-5.2（200），`low`/`medium` 对任何模型都**从未实测**（代码依据只是
平台 400 报错文案里出现过这两个词，而那串列表是**并集**、各模型支持面不同）——用户点了某家不支持的档就可能 400。
现把 `thinkingLevelMapFor` 改为**逐模型门控**（新常量 `PROBED_EFFORT`，数据抄自同一张
冻结契约，覆盖 low/medium/high/xhigh/max 五格）：`high`（平台默认，全家族实测过）保持
开，`xhigh` 只对 deepseek-v4-flash 开，`max` 只对 glm-5.2 开；未收录模型默认全关。

**2026-09-30 探针补测后**：`test/live-contract.mjs` 扩了 `reasoning_effort:low/medium`
探针（2s 退避；429 记 INDEFINITE 不当判读——限流是节奏问题不是参数拒绝，只有 4xx 才
算"不支持"的负证据）。首跑实锤 200 的格子：sensenova-6.8-flash-lite（low+medium）、
**deepseek-v4-flash（low+medium，你原问的那行——低中档平台真支持）**、glm-5.2（low+medium）、
deepseek-flash（medium）、kimi-k3（medium）→ 这些格已翻 `true`，对应面板行放出低/中档。
仍 INDEFINITE 的（429 rpm 窗口，非 400，保持关、重跑再翻）：deepseek-v4-pro 的 low+medium、
deepseek-flash 的 low、kimi-k3 的 low。

测试同步：`contract` §2 加逐模型 low/medium 断言（读契约表，`true`→开、`indefinite`/`false`→关）、
`provider` §1（v4-flash/glm-5.2/flash-lite 的 low+medium 开、INDEFINITE 格关）、`retry` §5、
`routes` Q2、`render` G4（v4-flash 那行 = 关闭/低/中/高/极高）。文档同步：`API.md` 样例
行与字段注记、`IMPROVEMENTS.md` §7 修订、本条。

### 可见性修复三连：登录态常显 + provider 注册 pending 态 + 小浣熊错误不再被开关藏起

同族问题——「状态翻成某个值后，操作入口/错误行跟着消失，用户拿不到重入路」：

- **登录态与账号编辑器无条件可见**（中间态不再是死路）：此前「连接商汤控制台」区卡只在 `hasAccount || needsAccount` 时出现——中间态（已点过「清除账号」、grant 仍在静默续期）下整卡消失，要等 refresh_token 彻底失效才重新拿到重输入口。现改为**只要快照携带 auth 块就常显**，积分额度页底部永远有该卡（默认折叠、一键展开）：随时可核对登录态、重输账号改指仍有效的 grant、或再清一次账号。头部「需要重新登录」chip 的 tooltip 在 Host 没给 reason 时回落 guidance 文案，不再留空白。
- **provider 注册 pending 态**：开关已开、服务在、无 error，但注册没落地时，原文案谎报「未注册——勾选上方开关」（指向已勾选的开关）；新增 `llm.registeredPending` 直说 pending。同时修分支序：具体失败（providerError）优先于能力缺失（noService），两者同时成立时不再吞错误。
- **小浣熊 tab 注册状态行不再被开关门控**：providerError 原来只在 `enabled === true` 时渲染，开关一关报错就消失；现 `state !== null` 即渲染，失败行与开关解耦。
- **构建修复**：`qr.ts` 内 8 处 `x?.y = v`（可选链左值赋值）是解析错误，tsdown 直接挂掉——改为先索引后赋值。
- **测试**：`test/panel.test.mjs` B2 组翻转（中间态/死 grant 态都钉 `canManageAccount === true`）；`panel-decision.js` 镜像同步去门控；`render.test.mjs` 新增 pending 态与 error 优先序 2 项 pin。

### 修复：全新安装时面板只剩「连接 Agnes 控制台」，连 tab 栏都不渲染

上报现象：全新装好插件打开面板，看不到三个 tab，整页只有一个登录表单——**API Key tab 和小浣熊 tab 也一起消失了**，尽管它们一个根本不读控制台、一个连的是另一个上游。

根因是**结构性设计缺陷，不是移植失误**（源版同样存在）：控制台探针 `overview` 被当作**唯一致命源**，失败即整个快照走 `ok:false`；客户端又把 tab 栏放在 `data` 分支里，于是「控制台没登录」这一件事把三个互相独立的模块全埋了。这正是 `AGENTS.md` 模块独立性不变量禁止的形状。

两层修复 + 一条诚实性补丁：

- **Host（`snapshot-aggregate.ts`）**：`overview` 从致命源降级为五个可降级源之一（`soft()`），新增 `quota.consoleConnected`；失败原因（`not_configured` / `auth_error` / `console_error`）走 `quota.error.code`。它仍**串行先取**——401 时先换新令牌，后面批量才拿活令牌出门（`test/routes.test.mjs` B 组的「死令牌只被出示一次」仍是绿的）。
- **客户端（`panel-page.ts`）**：tab 栏从 `data` 分支提到顶层，**第一帧就渲染**；`data === null` 时登录表单变成额度 tab 的**内容**而不是整页。控制台未连接时额度 tab 内点名原因，并一次性自动展开账号卡（用 `useRef` + `useEffect`，因为 `open` 是受控 prop，写成 `open: openSections.account || needsSetup` 会让卡片再也收不起来）。
- **诚实性补丁**：`quota.totals` 在读不到时是 **`null`，不是零值块**。`parseUsageOverview(null)` 的每个计数器都经 `countOf` 落到 `0`，直接渲染会把一次读取失败伪装成「0 次请求 / 0 token」——把失败变成测量。面板本就有 `null` → 「暂未读到」的分支，所以这一条只改 Host 侧。
- **顺带暴露的副作用**：未登录的 Host 现在也会去取**公开套餐目录**（它是控制台静默时唯一还剩的额度内容），`test/wiring.test.mjs` 此前从未 stub 过它，已补。
- **测试**：`routes` +8、`wiring` +2、`e2e` +2、`render` +2（含「第一帧就带三个 tab」的回归）；`docs/API.md` / `ARCHITECTURE.md` §3 §5 同步。客户端两个 tab 的行为未变，只是重新可达。

**上线实测又挖出一个缺口（同日补）**：重启 web 后快照确实变成 `ok:true` / `consoleConnected:false` / `totals:null`，但 `quota.error.code` 是 **`login_rejected`**——而 `GUIDANCE_BY_CODE` 只覆盖了 5 个码。根因是这次降级的连带效应：`AUTH_FAILURE_CODES` 里 13 个码现在**都能**以 `quota.error.code` 抵达面板（以前它们只会变成整份 `ok:false`，面板只读顶层 `code`），表却没跟着长。后果是面板退化成泛泛的「控制台未连接」，丢掉「账号需要重新输入」这个唯一有用的信息。

- `GUIDANCE_BY_CODE` 补齐到 `AUTH_FAILURE_CODES ∪ NO_LOGIN_CODES` 全覆盖：令牌侧（`auth_error` / `jwt_expired` / `no_refresh_token` / `refresh_rejected` + 四个已死的 OIDC 期码）统一给「令牌失效，重登一次」；账号侧 `not_configured` / `missing_credentials` 给「还没配账号」；四种凭据拒绝**复用表单自己的文案**（`auth.badCredentials` / `auth.locked` / `auth.rateLimited` / `auth.verification`）——它们的下一步动作各不相同（重输 / 等解锁 / 等限频 / 完成人工验证），塌成一句「登录失败」会对其中三种给出错误建议。
- **补上反向检查**（`test/panel.test.mjs`）：此前只断言「表里的 key 都是真码」，现在同时断言「能抵达面板的码都有文案」。缺的正是这一半，而且是被线上抓到的，不是被测试抓到的。

### 修复：浏览器把保存的商汤账号填进模型搜索框

上报现象：密码管理器把 SenseNova 控制台的账号自动填进插件的输入框，**连「接入 API」tab 的模型搜索框也被塞了账号**，看着像浏览器按关键词猜中了什么。

**不是关键词，是我们主动声明的，而且同 origin 上还有第二个一模一样的表单**：

- 两个插件（`dsh-connect-sensenova-token-plan` 与 `dsh-connect-agnes-token-plan`）同时装在 `profiles/web` 的 bundles 里，注册进同一个 slot、由同一个 `http://127.0.0.1:3080` 提供——浏览器按 origin 存密码，分不清谁是谁。
- 两个插件的表单语义逐行相同（`<form>` + `autocomplete="username"` + `type="password"` + `current-password`），正是 Chromium 文档里的标准登录表单形状。
- API Key 那个 `<form>` 只有密码字段、**没有用户名字段**。Chromium 自己的文档要求这种拆分布局必须在密码表单里放一个含用户名的字段（可 CSS 隐藏），否则它会自己去找——它挑中了同页唯一既无 `autocomplete` 又无 `name` 的文本输入框，也就是模型搜索框。

修法：给模型搜索框加 `autoComplete="off"` 与 `name="model-search"`。**账号表单的 `username`/`current-password` 语义刻意保留**（浏览器记住 Agnes 账号是想要的能力），本次只堵「去表单外找用户名」这条路径。

- `render.test.mjs` 新增 I2 组：渲染全部 7 个可能含输入框的组件，断言每个可填文本输入框要么 `autoComplete="off"`、要么声明凭据角色，并把「唯一声明凭据角色的是账号表单」钉死。该检查实测会红再复绿。
- 机制与证据写进 `docs/PITFALLS.md` §27（同 origin 多插件会把这一点放大成「凭据互相串门」）。

### 修复：额度已经正常显示，头部却一直挂「需要重新登录」

上线后看到的第二处「状态不落地」：面板头部 chip 说「需要重新登录」，下面额度、用量、套餐**全都正常**——数据是真的，chip 是假的，而且不会自愈，要重启 Host 才消失。

- **根因**：`state.lastError` 由 `getToken()` 的 `acquire()` 路径写入、也由那条路径清除，但 `getToken()` 在缓存令牌仍然新鲜时会**提前 return**，根本不进 `acquire()`；而面板上的手动登录是**直接存令牌**的，从不经过 `acquire()`。一次失败的自动重登写上这个字符串，用户手动登录成功后节流记录被清了、**这个字符串没清**，新令牌又足够新鲜——清除路径再也不会跑到。
- **修法**：不逐个写入点补清除，而是把上报钉在结果上——`state()` 里 `error: isFresh(stored) ? null : …`。`auth.error` 的契约是「面板该向用户要账号」，持有一个控制台没拒绝过的令牌时就不存在当前错误。
- **同族**：`AccountForm` 的「已保存并登录，正在读取额度…」也从不落地（`saved` 只被「清除账号」复位），改由父组件传 `hasSnapshot` 门控——独立表单照旧显示，面板内嵌不再显示。**这条没有测试覆盖**（测试替身的 `useState` 只返回初值，进不去 `saved`），是记录在案的缺口。
- **测试**：`store.test.mjs` 8c 组补两处断言（parked 期间必须报错、改正之后必须不报），实测会红并精确报出用户看到的那句原文，再还原复绿；冻结基线零漂移。机制写进 `docs/PITFALLS.md` §28。

### 运维诊断 doctor（PITFALLS §22 的欠账）

回答「这台机器的 provider / 出图开关到底开没开」——此前唯一答案在一个 JSON 状态文件里，不在任何配置文件、任何路由、任何 CLI。

- **新增 `src/host/doctor.ts`（peer-free 纯读层）+ `tools/doctor.mjs` CLI**：读 `$DSH_HOME/state/<name>/` 与每个 `<profile>/<name>/` 下的 `provider.json` / `draw.json` / `catalog.json`，报「面板保存值 > 部署默认值」的生效开关、模型允许清单、以及「文件存在但读不成」时**点名**是哪个文件（损坏/外来版本不会静默变空）。`npm run doctor`（人读）/ `npm run doctor:json`（机器读）。
- 与「大统一」定位对齐：单点入口必须可查，doctor 是第一条查询通道；它不写任何文件、不碰凭据，只在磁盘上读，Host 没起也能跑。
- **测试**：`test/doctor.test.mjs`（23 项，离线门禁）——三套 payload 解析器的「损坏/外来版本读作未设」方向、profile 分段与 shared 布局互不误认、干净机器/损坏文件的人读文案；进 `npm test` 与 CI offline 档。

### 边界裁定：第二上游正式纳入界内（不变量 3 修订）

Token Plan 与小浣熊同为商汤旗下产品线，`sensenova` 这个名头名副其实，第二上游不再是「待定方向的破例」。

- **`ARCHITECTURE.md` §5 不变量 3 修订**：划线依据从「只吸收与商汤 **Key/账号线**强相关的能力」改为「只吸收与**商汤（SenseTime）产品线**强相关的能力」。原划法按 Key 域名 / 认证域划线，会把同一厂商的姐妹产品线误判到界外——第二上游由此从「撞边界、须先定方向」变为**界内**。另两条不变量（opt-in 默认关、凭据红线）不变。
- **新增 §5.5** 记录裁定本身，**三条独立举证**而非印象：中证网 2026-07-19（U1 Pro 能力「在**商汤旗下的**产业级 AI『小浣熊』及 Seko 中已得到深度验证」）、商汤官方稿件 2026-09-21（「**商汤小浣熊 Raccoon Work**」由商汤科技打造）、以及本仓库既有措辞（ROADMAP §6.1 早写着「接入**商汤小浣熊**桌面 App 模型」——边界这次才承认，事实一直在那儿）。
- **它与 Token Plan 的确切关系**（后来人据此判断还能不能再放宽）：**同一厂商、不同产品线、认证域互不相通、积分口径互不算**。所以实现上必须继续是独立凭据 / 独立 publisher / 独立 provider id——共享任何一样都等于把两条产品线焊死。它是**第二上游**，与 §6.1.1 判死的**第二登录路径**不是同一件事。
- **防止口子被开成无限大**：本次放宽只覆盖「同一厂商下的产品线」，**跨厂商聚合**（codearts 那类一套形态融 N 家）依旧在界外。以后每纳入一条新产品线必须同时落三条——① 外部可核的厂商举证；② 与 Token Plan 的具体关系，尤其是积分与认证域是否通用；③ 齐全后才允许以 opt-in 默认关的形态进入树干。缺一条就退回 publisher 隔离那条路。
- **同步**：`ROADMAP.md` §6.1.1 的「须先定方向——不默认吸收」改为**方向已定**；`AGENTS.md` 第三条事实改为「同属商汤旗下，但认证域不相通」。§6.1.1 判死第二**登录路径**的两次复测结论**不变**——它与「第二上游属不属界内」不是同一件事，别混为一谈。

### 第二上游补复测 + 文档追平（一次自审掀开的三处「文档与代码分头走路」）

- **README 不再说谎**（它进 npm 的 `files` 白名单，是用户装完后读到的唯一说明书）：此前 ① **零处**提到当期头条特性——第三个 tab「小浣熊」；② 三处仍写「侧边栏」，而代码早已迁到 Plugins 页，第 31 行「打开侧边栏「积分面板」」会让人**找不到入口**（操作级失效，不是文字洁癖）；③ 仍挂着「面板**只读**」的承诺，而它已经能注册推理通道、挂出图工具、写 DSH settings。三处全部改正，并补齐「出图工具」与「第二个上游：小浣熊」两个真正的能力小节（含 provider id `sensenova-raccoon`、工具挂载需重启的事实）。
- **`cordis.patch.yml` 头注释的同类漂移**：第 3 行同款地写着 "sidebar panel"，同样进 npm 包，一并改掉。
- **第二上游的网关契约补复测**（新增 `docs/ROADMAP.md` §6.1.2）：2026-10-01 的四次无凭据只读请求——本插件实际依赖的 `/model_catalog` 与 `/chat/completions` 均回 `401 authorization_empty_error`（已抵鉴权层，路由健在）；09-30 记录里那个 404 的是参考件 `dsh-raccoon-work` 的 `/models`，**本插件不使用**。此前据前缀相同就判「实现的常量正是被标注 404 的那份清单」属**误判，已更正**。附带教训收进 PITFALLS §24。
- **护栏：`docs.test.mjs` 新增两条语义检查**——原有的全是形式校验（链接 / 表格去重 / 行数上限），对面「文档断言的事是否与代码一致」毫无感知：
  - **检查 `README_TABS`**：README 必须覆盖面板的每一个 tab。tab 名单从 `panel-page.ts` 的 `activeTab` 联合类型派生、文案从 `i18n.ts` 的 `tab.<id>` 派生，**不写死任何名字**，于是「加 tab 忘了告诉用户」和「改 tab 名不跟」都必然红。
  - **检查 `SELF_DESCRIPTION`**：README 与 `cordis.patch.yml` 声称的 UI 位置必须与 `src/client/*.ts` 实际注册的槽位一致。
  - 两条都做过「故意破坏 → 必须红 → 还原 → 复绿」的双向验证（删光「小浣熊」精确报出 `raccoon（面板文案「小浣熊」）`；改回事故原文报「声称面板在侧边栏但没有 sidebar 槽位注册」）。
- **PITFALLS 新增 §24（GET 探活会误判）与 §25（形式门禁全绿 ≠ 文档说实话）**，总数 23 → 25；三处散落的过期引用（`README.md` / `AGENTS.md` / `docs/README.md`）同步——这批数字正是由新门禁当场揪出来的。

### 文档修正：撤销 ROADMAP §0 被证伪的前提

- [ROADMAP.md](docs/ROADMAP.md) §0 与 §2 引言原写「本插件已是双 profile 的 `agent-default-model`——即这台机器的**默认推理通道**，故障域已升级为推理可用性」。该论断 2026-09-29 已被研究原稿 §1.2 撤销（`agent-default-model` 是宿主的选择记录服务，原引用不可复现；撤销原文现见 [ARCHIVE-BOUNDARY-DECISIONS.md](docs/ARCHIVE-BOUNDARY-DECISIONS.md) A1），但 ROADMAP 未同步。现改为「**能力事实**：可注册 provider `sensenova-token-plan`；是否默认通道由 profile 与用户模型选择决定；一旦某 profile 选它作默认，故障域才从面板升级为推理可用性（条件性爆炸半径）」。两份文档不再正面矛盾。

### 文档与仓库纪律

- **消灭「离线 N 套件」数字漂移源**：文档不再背书套件数量与枚举（`AGENTS.md` 曾写十七套件、`DSH-PLUGIN.md` 曾列 17 项清单，raccoon/doctor 套件加入后双双过期）——套件清单与链的唯一事实源收敛到 `package.json scripts.test`（已有 `package.test.mjs` 钉磁盘 ↔ 链 ↔ CI 一致性），文档只指事实源不复制数字。
- **现状模块引用 `.js` → `.ts` 全量对齐**（64 处 / 9 文件）：只改「现状引用」，保留「历史动作 / 研究档案 / 产物 / 外部」四类原文。

### 仓库结构规范化：src/ 全源码，lib/ 纯产物

- 全部源码收敛到 `src/`（`src/host/*.ts` 27 个 Host 模块 + `src/client/*.ts` Client 半边）；`lib/` 与根 `client.js` 降为纯构建产物并加入 `.gitignore`——删掉后 `npm run build` 一条命令从源码完整重建。
- 对外行为无变化：`main`/`exports` 指向不变（`./lib/index.js`、`./client: ./client.js`），`files` 白名单同步，`npm publish` 经 `prepack` 自动构建；`exports` 收敛为 `.` 与 `./client` 两个子路径。

### 模态判定地基（modality.ts）：出图 / 视频模型识别统一为单一出处

出图工具认不出模型的根因是「模态判定」两路分家、又都没拿到字段：Agnes 网关的 `/v1/models` 条目只带 `id/object/created/owned_by/supported_endpoint_types`，**没有任何模态字段**（真机确证 2026-10-01）。同一缺失让两个判定朝相反方向失手——`isImageGenModel` 严格方向（缺字段 = 不是出图模型）导致 `agnes_draw_image` 选不出候选，面板报「暂无出图模型」；`isChatModel` 宽松方向（缺字段 = 对话）把 4 个 image/video 模型全挂进对话选择器，一选就 400「是 image 模型，请使用 /v1/images/generations」。

- **新增 `src/host/modality.ts` 作为模态判定的唯一出处**，三级解析：`output_modalities` 字段优先（平台补字段即自动生效）→ `agnes-image-*` / `agnes-video-*` 名称段兜底（只匹配完整段，不重蹈 `dsh-draw-router` 的 `/u1-fast/i` 子串漏判）→ 默认 `text`。`isImageGenModel` 与 `isChatModel` 改为共用这一个函数，矛盾由构造消除而非靠约定维持。
- **真实目录验证**：出图候选 0 → 2（自动选中 `agnes-image-2.1-flash`）；对话名单 7 → 3。出图端点本身无需改动——真机实测 `POST /v1/images/generations` 返回 `data[0].url`，与 `parseDrawResponse` 已对齐。
- **文档更正**：`AGNES-API.md` §7.1 原先把 SenseNova 字段名误列为 Agnes 字段；新增 §7.5 记录图片同步 / 视频异步端点形状（`POST /v1/video/generations` 建任务、`GET /v1/videos/generations` 查任务）。`ARCHITECTURE.md` §5.4 记录「结构化判定」前提在 Agnes 上的反转。
- **测试**：`test/draw.test.mjs` 58 → 67，`test/provider.test.mjs` 201 → 205。

### 出图工具面板开关（drawEnabled）

与 provider 开关同机制的「面板开关 + 立即生效」，出图吸收（§5.4 接法 B）不再需要改配置重启：

- **面板新增「出图工具」卡片**（`client.js` 的 `DrawSwitch` 控件）：勾选保存后写入插件私有状态文件 `$DSH_HOME/state/<plugin>/draw.json`，与 `provider-store.js` 走完全相同的完整性纪律。优先级：面板保存值 > `cordis.patch.yml` 的 `drawEnabled`；从未动过面板的部署，行为与 `false` 一致。
- **新增 `POST /api/<name>/draw` 路由**（`routes.js`）：与 `/provider` 同一信任形状（同源围栏 + 4 KB body 上限），`{ enabled: true|false }` 或 `{ forget: true }`。
- **快照 `llm.drawEnabled` / `llm.drawSource`**：`snapshot-aggregate.js` 在 llm 块里回显出图开关的生效值与来源，面板无需单独调 `/draw` 就能读到当前状态。
- **`lifecycle.js` 的 `registerDrawTool` 改读生效值**：不再直接读 `settings.drawEnabled`，而是「面板保存值 ?? 配置默认值」。工具的实际挂载/缺席发生在**下一个 Host 启动**时（agent tools 没有 unregister 语义），开关值本身是立即生效的。
- **测试**：`test/provider.test.mjs` 新增 draw-store 纯逻辑组（归一、读写、版本拒绝、损坏忽略、forget）；`test/routes.test.mjs` 新增 R 组（8 项，覆盖 GET/POST/forget/跨域围栏/跨 remount 持久化/配置回退）；`test/wiring.test.mjs` 路由计数从 5 更新为 6。

### 视频工具面板开关（videoEnabled）：V2.0 单体系，异步任务制

与出图对称，新增可选的视频生成 agent 工具（默认关），接入全链路与出图共用同一套纪律：

- **面板新增「视频工具」卡片**（`client.js` 的 `VideoSwitch` 控件）：与 `DrawSwitch` 共用同一私有 `ToolSwitch` 体，保证两个工具的降级阶梯与状态不可能分叉。勾选保存写入 `$DSH_HOME/state/<plugin>/video.json`，优先级：面板保存值 > `cordis.patch.yml` 的 `videoEnabled`。
- **新增 `POST /api/<name>/video` 路由**（`routes.js`）：与 `/draw` 共用同一处理器 `registerToolSwitchRoute`，区别只在闭包捕获的 store 与请求体键名（`drawModelId` / `videoModelId`）——键名承重，写错会让两工具静默共用一个模型；`test/routes.test.mjs` 组 S 专门钉隔离。
- **Host 半边 `src/host/video.ts`（V2.0 单体系，不无脑移植上游）**：走 `POST {apiBase}/videos` 建任务 → 轮询 `{host}/agnesapi` → 取 `url` 的完整状态机；`VIDEO_MAX_POLLS = 1000` 防时钟不前进时空转；**刻意不设 30s 冷却门**（协议往返延迟已远宽于冷却窗口，且一次视频尝试分钟级、共用视频限频池）。`buildVideoBody` 对非法显式值**抛错而非夹取**（夹取帧数会静默改变视频时长，分钟级生成后才发现）。
- **只覆盖 V2.0 参数体系**（`width`/`height`/`num_frames`(须 8n+1)/`frame_rate`/`seed`/`negative_prompt`/`image`），与 2.5 系列（`mode`/`seconds`/`size`/`aspect_ratio`）互斥；`pickVideoModel` 只在 V2.0 家族选，2.5 进 `video25ModelIds` 单独上报面板说明，不进对话选择器。
- **快照新增视频键**：`llm.videoEnabled` / `videoSource` / `videoModel` / `videoCandidateCount` / `videoCandidateIds` / `video25ModelIds`；`progress: 0` 走 `parseVideoQuery` 直接 `Number()` 读取（不能用坚持正值的 `num()`，否则合法 0% 被读成缺失）。
- **配套配置**：`cordis.patch.yml` 新增 `videoEnabled` / `videoModelId` / `videoTimeoutMs`(≥30000) / `videoWidth`(1152) / `videoHeight`(768) / `videoNumFrames`(121) / `videoFrameRate`(24) 七项；`src/host/types.ts` 的 `HostDeps` 补 `videoFetch`。
- **文档**：`AGNES-API.md` §7.5 整段重写为 V2.0 请求体字段与校验规则；`API.md` 补快照视频键与 `/video` 路由；`SETUP.md` 补 7 个视频配置字段；`PROVIDER-HOT-RELOAD.md` 新增 §8；`ARCHITECTURE.md` §5.4 补「视频吸收」；`README.md` 补视频工具段；`PITFALLS.md` 新增 §29（两站凭据不互通）/ §30（视频异步任务制）/ §31（`progress:0` 不能用 `num()`）。
- **测试**：`test/video.test.mjs`（89 项，peer-free）覆盖 V2.0 请求体校验、V2.0+2.5 混合目录的候选过滤与自动选型、开关隔离；`test/routes.test.mjs` 组 S（10 项）覆盖视频开关读写、跨工具键名隔离、各自 store 隔离；`test/render.test.mjs` 组 G10 覆盖 `VideoSwitch` 渲染；`test/doctor.test.mjs` 覆盖 `video.json` 解析与隔离；`test/wiring.test.mjs` 路由计数 7 → 8。

### 面板视觉打磨

几处「不报错、但会误导或压平层级」的显示。

- **每模型消耗柱状图补图例**：柱子按「最高消耗者」归一化，top 模型永远填满轨道——它回答的是「谁在烧积分」，但满格会被误读成「这个模型快触顶」。卡片底部补一行说明「柱长按最高消耗相对显示，非占总额度比例」，图表不再靠省略说谎。
- **返赠余额从药丸改为指标数字**：`返赠余额 327,904` 此前裹在与「通用池/专属池」同款的圆角药丸里，一个大数字被压成和静态类型标签同等权重的装饰。改为卡头右侧的 tabular 数字，读作可花余额指标。
- **注册成功不再常驻绿色**：「已向 DSH 注册提供方…」是静止常态，长期亮 `state-success-primary` 会让人误以为刚发生了好事。收敛为中性次要文字，绿色只留给「已保存」这类瞬时反馈。
- **去掉最内层子卡的冗余边框**：section 卡→池卡→双子窗三层等宽 `border-l1` 互相抵消、压平层级。最内层子窗改为纯靠更深的背景面（layer-2）浮起，边框只留两层。
- **API Key 字段标题去重**：「模型接入（API Key）」区块头与卡内输入框标签原本同名重复，标签改为「API Key」。
- **品牌色锚定商汤紫**：去掉 shell 跟随层，3 个品牌强调点直接锚定 SenseNova 紫；活动 tab 与非活动 tab 使用同一套基础样式。

## [0.4.1] — 2026-09-29

面板前端修复：几处「看起来对、实际错」的显示，加两处让操作直接失效的 bug。

- **每周额度重置时间误显示为当天**：5 小时与每周两个窗口共用同一个时间格式化器，而它只吐 `HH:MM`——每周窗口的 `reset_at` 是几天后的绝对时刻，日期被吞掉，读作「重置 18:10」，像是当天 18 点发生。新增 `when()`：今天保持紧凑 `HH:MM`，跨天带 `MM-DD`。返赠到期一直用的是 `clockLong`（带日期），只有重置时间漏了。
- **模型选择器「全选」会清空已选模型**：`ModelPicker.bulk` 把 `{id, name, vision}` 行数组传给只接受字符串的 `allowListFor`，`rosterIds` 过滤后恒为空 → 无论点「全选」还是「全不选」都返回 `[HIDE_ALL_MODELS]`，保存即清空模型列表。逻辑提成纯函数 `bulkModelsIn`，调用点显式转 id。
- **选择器单个模型无法勾选**：`ModelRoster` 的 checkbox 只有 `checked` 没有 `onChange`，实际只读——配合上一条，整块选择器不可用。
- **轮询竞态**：前一轮请求未返回时定时器又触发一轮，慢的那次会后发先至、覆盖新数据，用量条肉眼可见地往回跳。每次 `load` 递增 generation，过期响应直接丢弃，并用 `AbortController` 取消被取代的请求（不只是忽略结果）。后台标签停掉轮询，切回时立即刷新一次。
- **控制台不可读时给出可行动的文案**：`console_error` 一直被挡在登录表单之外（`FORM_EXCLUDED_CODES`），但 `GUIDANCE_BY_CODE` 没有这个键，用户只看到裸错误串、暗示永久故障——现在提示「通常下一次自动刷新即可恢复」。HTTP 401/403 不再只显 `读取失败：HTTP 401`，改为按令牌失效处理并保持登录表单可达。
- **暗色主题状态色静默失效**：三处主题变量前缀写成 `--dsh-alias-*`（不存在的变量名），「已耗尽」红标与「已保存」绿字失效；官方 UI 一律 `--dsw-alias-*`。
- **可访问性**：错误类 7 处 `role="alert"`、状态类 5 处 `role="status"`（此前屏幕阅读器听不到任何失败原因）；QuotaCard 的 `aria-valuenow` 与可见百分比统一为一位小数；模型搜索框补 `aria-label`；两处 tooltip 不再裸露 `{balance}` / `{selected}` 占位符。

## [0.4.0] — 2026-09-29

出图吸收（大统一 §5.4 接法 B）：本插件可以给 agent 提供商汤出图能力了。

- **新增 agent 工具 `sensenova_draw_image`**（opt-in，配置 `drawEnabled: false` 默认关）：POST `{apiBase}/images/generations`，鉴权用面板「模型接入」保存的 `SENSENOVA_API_KEY` 引用（每次调用现取，轮换 Key 无需重启）。
- **出图模型识别用结构化字段，不用名字正则**：从 catalog 的 `output_modalities` 判定（与 chat 清单的排除逻辑互为反向，两份清单不可能矛盾）。社区同类 `dsh-draw-router` 的名字正则会漏掉 `sensenova-u1.5-lite`，本实现不会（对照见 `docs/ARCHITECTURE.md` §5.4）。
- **429 分诊与失败冷却**：出图失败时区分「配额不足（别盲重试）」与「限频（等再试）」；失败后 30s 冷却，防止 agent 在耗尽的共享池上打转。
- **修复 429 误判纠正（chat 路径）**：peer 的 `classifyPiAiError` 先跑 `isQuotaExceededError`，命中面过宽——商汤限频 429 体里带 `rate budget` / `credits` 字眼时会被抢判成 `QUOTA`，导致本应退避重试的限频被按"配额耗尽"快速失败、且模型被面板静默下线（呈现"额度已用尽"）。新增 `llm-error-fix.js`：在 `llm-adapter.js` 用 Proxy 包裹 `PiAiAdapter` 的流出口，把这类"误判的限频 QUOTA"在出流前纠正回 `RATE_LIMIT`（保留原 message），真配额耗尽与已限频原样放行。`test/error-fix.test.mjs`（24 项，peer-free）覆盖。详见 ROADMAP §3 的纠偏注记。
- **降级同型**：无 tools 服务的 Host、peer 加载失败、注册被拒——工具静默缺席，面板与 provider 不受影响；快照契约零改动（14 键不变）。
- 配套：`drawModelId`（首选模型）与 `drawTimeoutMs`（默认 120s）两个配置；`test/draw.test.mjs`（56 项，peer-free）。

## [0.3.4] — 2026-09-29

429 自愈与「清单自带识别」：Token Plan 池额度耗尽时，模型不再发出必失败的请求。

- **provider 级重试策略**（`llm-retry.js` → `resolveRetryPolicy` 显式配置）：配额耗尽（`QUOTA`/`ACCOUNT_QUOTA`）**不重试、快速失败**；限频（`RATE_LIMIT`）按退避重试。候选路由在共享额度池上空转只会延长冷却窗口，故刻意不对配额做重试（对应上文「不做多 Key 池」）。
- **清单自带识别**：某模型所属额度池耗尽（`remaining <= 0`）时，该模型从 DSH 模型选择器移除（不再发出必 429 的请求）；面板花名册则**保留**该模型并以 `available:false` / `quotaExhausted:true` 标记、灰色显示原因，用户可知「为什么这个模型不见了」。
- **额度跨越零点自动重注册**：快照用 `quotaSignature` 去抖，仅在额度状态变化时才触发一次 `publishProvider` 重建（受 `PiAiAdapter` 对 profiles Map 引用记忆化约束，这是唯一生效路径），无需重启 Host。
- **快照 `llm` 块新增** `quotaBlockedModelIds`；`models` 每行新增 `available` / `quotaExhausted` 字段，供面板渲染。

## [0.3.3] — 2026-09-29

**安全姿态收紧：控制台密码不再落盘**（破坏性改动——老用户升级后需重新登录一次的情况见下）。

- **密码默认不写入任何文件**：此前 `saveAccount` 会把面板输入的密码原样存进 DSH 凭据服务（`~/.dsh/.credentials.yaml`，owner-only 的明文 YAML）。本版改为只保存**账号 + access/refresh token**；密码仅在登录瞬间于内存中使用，用完即弃。
- **`SENSENOVA_PASSWORD` 环境变量是密码唯一的持久来源**（显式 opt-in）：放在环境里，refresh_token 失效后可自动重登，与旧行为一致；不放，则 refresh 失效时面板要求重新输入一次（README「登录失败怎么办」早已承诺此路径）。
- **旧版残留自动清除**：凭据服务里已存的 `SENSENOVA_PASSWORD` 引用在首次接触时被移除（一次性清扫），明文密码不会继续留在磁盘上。
- 行为影响：**升级后若当前 refresh_token 仍有效，完全无感**（续期照常）；仅在 refresh_token 已失效、且依赖"密码存在凭据服务里自动重登"的场景下，会多一次手动输入。
- `state().hasAccount` 语义微调：现在按"已保存的账号名"判定（不再要求密码可用），面板的「清除账号」入口在密码缺失时仍然可用。

## [0.3.2] — 2026-09-29

面板新增「模型允许清单」：可以**勾选具体哪些模型推送进 DSH 的模型列表**，不再只能全推或全不推。

- **面板新增模型选择器**（「模型接入（API Key）」区）：列出本 Key 目录下每个模型（带名称与可看图标记），逐条勾选；支持搜索过滤、可见项全选/全取消、实时计数，编辑先落草稿，点保存才写入。
- **新增路由** `POST /api/<name>/models`（同源围栏 + body 上限 + 500 项上限，与账号/api-key/provider 路由同一信任形状）：`POST { enabledModelIds: string[] }` 替换允许清单，**同一请求内**重新发布 provider，面板不用等下一次轮询。设计理由见 [docs/PROVIDER-HOT-RELOAD.md](docs/PROVIDER-HOT-RELOAD.md) §6。
- **清单语义**：`[]` = 不过滤，全部推送；非空 = 严格允许清单，只推列出的模型；`["__hide_all__"]` = 一个都不推送（表达「临时全部收起」，空数组已表示「未筛选」，需要独立写法）。
- **快照 `llm` 块新增两个字段**：`models`（整份可选目录：`id` / `name` / `vision`，**不受**过滤影响，面板据此画可勾选项）、`enabledModelIds`（当前生效的允许清单）。
- **`modelCount` / `visionCount` 改为按清单过滤后计数**——它们描述的是「实际注册了多少」，而不是目录有多大。0.3.1 及以前清单恒为空，数值不变。
- 清单存在与 catalog 同一份私有状态文件 `state/<name>/catalog.json` 的 `enabledModelIds` 字段（版本与原子写纪律不变），重启即恢复。
- `POST /api/<name>/api-key` 的 `forget` 语义不变，但它会连带清掉勾选记录：换一个 Key 就是一份新的、未勾选的目录。
- 补记：`package.json` 的 `version` 在 0.3.1 时漏改（一直是 0.3.0），本次一并补齐到 0.3.2。

## [0.3.1] — 2026-09-28

提供方注册开关热生效（[docs/PROVIDER-HOT-RELOAD.md](docs/PROVIDER-HOT-RELOAD.md)）：

- **面板新增真开关**：「模型接入（API Key）」区可直接勾选「向 DSH 注册 SenseNova 提供方」，保存到插件私有状态文件并在**同一请求内**重新发布 provider——立即生效，无需改配置、无需重启 Host。
- **新增路由** `GET/POST /api/<name>/provider`（同源围栏 + body 上限，与账号/api-key 路由同一信任形状）：`POST { enabled: boolean }` 切换开关，GET 回显生效值、来源与注册状态。
- **优先级**：面板保存过的值 > `cordis.patch.yml` 的 `registerProvider`（后者降级为部署默认）。未触碰面板的部署行为与 0.3.0 完全一致。
- **快照 `llm` 块新增 `registerSource`**（`"panel"` / `"config"`）；`registerProvider` 改为回显生效值。
- 开关状态存 `state/<name>/provider.json`（新模块 `provider-store.js`，版本化 + 原子写 + 损坏即读作未设置，与 `catalog-store`/`throttle-store` 同一完整性纪律）。

## [0.3.0] — 2026-09-28

第三步「一条龙」：本插件可**直接注册 SenseNova LLM provider**，不再需要手写 `llm-pi-ai` patch 行（opt-in，默认关闭）。

- **面板新增「模型接入（API Key）」区**：粘贴 `sk-` Key 即保存为 DSH 凭据服务引用 `SENSENOVA_API_KEY`（与手写 provider 读取同一引用名），支持显示/隐藏、保存、清除；环境变量 `SENSENOVA_API_KEY` 仍作兜底，清除面板引用不会动环境变量；任何接口响应只回「有无/来源」去密状态，不回显 Key。
- **新增配置 `registerProvider`（默认 `false`）**：开启后 Host 以 provider id `sensenova-token-plan` 直连 `apiBase`（默认 `https://token.sensenova.cn/v1`）注册 OpenAI 兼容适配器（`ctx.llm.registerAdapter` + `registerConfigurableProviders`），模型列表由 `/v1/models` catalog 自动构建并广播刷新；vision 模型自动带图片输入，无需手填 `imageModelIds`。
- **catalog 与模型勾选清单存插件私有状态文件** `$DSH_HOME/state/<name>/catalog.json`（原子写、损坏即忽略），不写 dsh 配置；重启后、首次轮询前即凭缓存完成注册。
- **快照新增 `llm` 块**：`hasApiKey` / `keySource` / `ephemeral` / `registerProvider` / `llmAvailable` / `providerRegistered` / `providerId` / `modelCount` / `visionCount`（成功响应顶层键 13 → 14）。
- **新增路由** `GET/POST /api/<name>/api-key`（同源围栏 + 4 KB body 上限，与账号路由一致）。
- 无 `llm` 服务或 peer 加载失败时面板与额度轮询照常工作，provider 静默缺席并在快照里带进去密错误原因。
- **peer 解析**：`@earendil-works/pi-ai` / `@deepseek-ai/dsh-llm` / `@deepseek-ai/dsh-llm-pi-ai` 进 `peerDependencies`。它们由 Host 发行，npm 装进 profile 的插件能顺着 `profiles/node_modules` 解析到；**开发期 symlink/junction 进 profile 的检出解析不到**（Node 会把链接解成 realpath），表现为面板一直说「provider 缺席」而离线套件全绿——修法与现象见 PITFALLS §16。
- e2e 现在会真开 `registerProvider` 并断言注册成功（含目录 / vision / 去密状态），并像离线套件一样从子进程环境里剥掉 `SENSENOVA_*`（PITFALLS §17）。
- Host 侧改动需**完全退出 DSH（含托盘）后重启**生效。

## [0.2.0] — 2026-09-28

文档与合规加固（无对外行为变化，纯质量与一致性工作）：

- **文档职责归位**：根 `README.md` 从 ~243 行瘦身到 ~128 行，只留索引与快速上手；重复的「认证」章节、配置/节流/测试表格、文件表已删除或下沉。
  - `docs/SETUP.md`：新增「环境隔离（web 优先，桌面端后置）」说明与「常见信号与处置」表。
  - `docs/TESTING.md`：吸收原 README 独占的开发轶事（test:live 边界、读写隔离、凭据双钉、store 守卫），并补 `docs.test.mjs` 一行职责；测试套件说明由「九套件」更正为「十套件」。
  - `docs/DSH-PLUGIN.md`：移除写死的测试计数，教学快照 caveat 与 `files` 数组同步到真实 `package.json`。
  - `docs/ARCHITECTURE.md` / `docs/CONTRIBUTING.md`：把 host 外路径（`~/.dsh/profiles/desktop/cordis.patch.yml`）标注为「仓库外」，示例提交信息改为中文。
- **文档一致性门禁**（新增 `test/docs.test.mjs`，已纳入 `npm test` 十套件与 CI）：内部链接可解析、同一张表格不跨文件重复、根 README 行数上限、DSH-PLUGIN 教学快照与 `package.json` 同步、`API.md` 快照示例与声明契约键集一致。
- **许可证与第三方合规**：新增 `LICENSE`（MIT，Copyright (c) 2026 eghrhegpe）与 `THIRD_PARTY_NOTICES.md`（致谢 dsh-connect-qoder 文档范式、connect 家族设计对齐、上游接口参考实现）；二者进入 `files` 打包清单。
- **上游关系澄清**：各文档补 `shaobingtongzhi/sensenova-usage-dashboard` GitHub 参考链接；如实记录本地 `upstream/` 副本当前不含 `.git`（设计态为独立仓库，恢复命令见 `docs/ARCHITECTURE.md` §1）。
- **首次发布 npm**：`dsh-connect-sensenova-token-plan@0.2.0`（无 scope、公开）；`package.json` 移除 `private: true` 并补 `repository` 字段指回 GitHub 仓，市场列表可自动关联下载量。

## [0.1.x] 及之前 — 未发布

早期提交（最新见 `git log`，本机无 git tag）：实现 OIDC+PKCE 登录、密码 JWE 封包、`refresh_token` 静默续期、登录节流分类、双窗口（5h/7d）用量面板、趋势与返赠明细、视觉第二步 opt-in 写入本插件 settings。具体条目以提交历史为准，本文件不再补列。
