@dmh2002/dsh-connect-qoder 0.0.0-stage → 0.4.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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,428 @@
1
+ # 更新日志(Changelog)
2
+
3
+ 格式参照 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
4
+
5
+ ## 0.4.0 (2026-10-13)
6
+
7
+ 0.3.0 发布后挖出的四条修复,外加一条有意的旧行为移除。四条里两条是 0.3.0 自己带出来的伤:**npm 上 0.3.0 的 peer 范围装不进当前 DSH**(装上即 `ERESOLVE`,必须尽快覆盖掉),以及**国际版需要的第二族令牌**;另两条是用户实拍回来的(思考强度间歇性消失、签到倒计时)。其中思考强度那条经历了一次返工 —— 第一版修复在生产路径上是**死代码**,险些带着假说明发布,经过与返工都记在条目里。
8
+
9
+ ### Fixed
10
+
11
+ - **国际版积分与签到恒为 `credential-rejected` —— 国际版需要【两族并存】的令牌,而插件只有一族**(用户实拍;cn 侧完全正常,所以这个问题只在国际版显形)。
12
+ - **实测(本机两个真实 PAT,只读;唯一变动的是「怎么换令牌」)**:
13
+
14
+ | 面 | 认哪一族 | 国际版 + 旧令牌 | 国际版 + 新令牌 |
15
+ | --- | --- | --- | --- |
16
+ | chat / cosy(模型对话) | `/algo/api/v3/user/jobToken` 的 `securityOauthToken` | ✅ got-delta | ❌ 403 code 105 登录态失效 |
17
+ | sash / 额度(积分、签到) | `/api/v1/jobToken/exchange` 的 `token` | ❌ 401 TOKEN_EXPIRE | ✅ 200 |
18
+
19
+ 国内版两族**等价**,所以这个差异在国内版永远看不出来 —— 这就是它反复查不出来的原因,也是「模型能用、只有积分读不到」这个奇怪组合的来源。
20
+ - **更早的两处误判**(记在这里,因为它们塑造了排查路径):先把 401 归因为「PAT 失效」,再归因为「国际版 sash 不开放该账号」。两者都被矩阵证伪 —— 截图显示网页端有 300 套餐 + 100 资源包积分,且换一枚新令牌后 sash 全线 200。
21
+ - **修法**:`QoderSessionHolder` 增加 `sashToken` / `sashExpireAtMs`;`exchangeJobToken` 里**额外**换一次 `/api/v1/jobToken/exchange`,`jobTokenGet`(用量)与 `sashRequest`(签到/活动)的 Authorization 改用它,chat / cosy 那一族原样不动。
22
+ - **必须在交换处 `await`**:放后台的话,紧接着的第一次读积分会带着空令牌去,拿到一个**与「真被拒」无法区分**的 401(实测:第一次 401、第二次 200)。这个坑本轮真实踩到了。
23
+ - **body 键是 `personal_token`**:`personal_access_token`(eghrhegpe/dsh-connect-qoder 用的拼写)在当前上游回 **400 BadRequest**,两个区域都是。
24
+ - **sash 交换失败不抛**:只让「积分读不到」,不让 chat / 对话跟着挂 —— 那是两条独立链路。
25
+ - **测试**:新增 `tests/live-intl-session.spec.ts`(**打真实上游,默认跳过**,`QODER_LIVE_TESTS=1` 打开)钉住 7 条事实:两区域各自的积分、签到、对话,外加两个区域不共用 baseUrl。改动前它精确失败在「global 积分 / 签到」两条而 chat 两条仍绿;改动后 7 条全绿。
26
+ - 另有 6 条桩测固定契约(两族都换、sash 用新令牌、失败不抛、body 键、以及**第一发就带对令牌**的竞态守卫),全部做过变异测试:回落旧令牌 / 换回 fire-and-forget 各自被接住。
27
+ - 参考实现:Liki4/qodercli2api、shuishuipingan/qoder2api-hub、eghrhegpe/dsh-connect-qoder(三者都走 `/api/v1/jobToken/exchange`)。
28
+
29
+ - **peer 范围静默排除整条 `0.2.0-rc.*` 宿主线 —— 从 npm 装到当前 DSH 就会 `ERESOLVE`。**
30
+ - **现象**:13 条 `@deepseek-ai/dsh-*` peer 写的是 `>=0.1.7-rc.1 <0.3.0-0`。这条范围**看着很宽,却匹配不到任何 `0.2.0-rc.*`**:node-semver 只有当范围里*某个*比较符与该版本的 `major.minor.patch` 元组完全一致、且自身也带预发布标签时,才放行预发布版本;`0.1.7-rc.1` 与 `0.3.0-0` 都不落在 `0.2.0` 元组上,于是 `0.2.0-rc.1` / `0.2.0-rc.2` 被**静默**排除。而 `@deepseek-ai/dsh` 的 `latest` **正是 `0.2.0-rc.2`** —— 从 npm 安装本插件的用户会撞上 `ERESOLVE`,还得自己手工绕过。
31
+ - **为什么本地与 CI 全绿**:本机 profile 是 `link:` 安装,**完全不走 peer 解析**;而 `tests/dsh-line.spec.ts` 当时自带一个只认「两个比较符」的简易实现,它对预发布版本的判断与 node-semver **相反** —— 一条**假绿**测试,而且它断言的恰好是错误的那一侧(用例写着「0.2.x 线仍放行」,实际并不放行)。
32
+ - **修法**:范围改为**两条分支** `>=0.1.7-rc.1 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0`(peer 与 dev 共 26 处一并改),`tests/dsh-line.spec.ts` 的比较器换成**真实的 node-semver**(`semver` 提为显式 devDependency —— 此前只作为传递依赖存在),删掉已无用的手写比较器;新增两条用例:已发布的 `0.2.0-rc.*` 必须被放行(回归)、有意排除的 `0.1.7-alpha.*` / `0.2.1-alpha.*` / `0.3.0-rc.1` 仍须被挡在外;并让 peer 与 dev 的口径一致。两条 README 的「受支持的宿主范围」表与 `RELEASING.md` 的发布核对清单同步更新。
33
+ - **依据**:`awesome-dsh-plugin/awesome-dsh-plugin` 的 `contributing.md`「A peer range without an explicit prerelease branch silently excludes every prerelease build of the harness」。
34
+ - **思考强度「有时候有、有时候没有」—— 目录只在启动时拉一次,而保存的快照全是空档位**(用户实拍)。
35
+ - **实测本机状态**(只读检查,未发起任何领取):保存的 `regions.cn.lastCatalog` 里 **14 个模型的 `thinkingEfforts` 全部是 `[]`**,`defaultThinkingEffort` 一个都没有;而同一时刻上游实时目录里 **9 个模型明确声明了档位**(`qmodel_38max` / `qfmodel` → `low, medium, xhigh` 默认 medium;`dmodel` → `high, max` 默认 max;`dfmodel` / `gmodel` / `kmodel` / `gfmodel` / `gm51model` / `kmodel_latest` 各自不同)。`[]` 是**旧 schema 把档位字段剥掉时**留下的产物 —— 也就是说这份快照根本不是「上游没声明」,而是「它来自一个没拉到档位的时刻」。
36
+ - **为什么是间歇性的**:`fullCatalog`(唯一带档位的目录)是**纯内存**的,而目录只在**启动时**拉一次。于是:点一次「从 Qoder 刷新」→ 实时目录装上 → 档位出现;重启 / 重新读取 → 实时目录空 → 退回那份全空快照 → 档位消失。「有没有点过刷新、进程活了多久」就是「有时候有、有时候没有」的全部来源。
37
+ - **修法**:`displayModels`(卡片 `/usage` 路由读的就是它)读到「本进程还没成功拉过实时目录 + 手上的目录一个档位都没声明」时,触发一次后台重拉(fire-and-forget + 在飞标记去重,**不阻塞**这次读 —— `displayModels` 是被 `/usage` 路由同步调用的读路径,在里面 await 网络会让每次读卡片都等一次上游),**继续服务手上的目录**:模型不能从卡片上消失,缺的只是档位,拉回来之后的下一次读取自然补上。「本进程还没拉到实时目录」这个守卫不可省 —— 上游**真的**没声明档位是合法状态,去掉守卫后每 60 秒一次的卡片轮询都会拖一发模型列表请求,永不停。
38
+ - **返工记录(第一版修复是死代码,险些带着假说明发布)**:第一版把重拉挂在 `displayModels` 的「运行时目录为空」分支上,三层缺陷叠加,使它在生产路径上**一次都不会执行**:
39
+ 1. 调用处 `if (!serve.models) return serve.fallback` —— `serve.models` 恒为数组,而 `[]` 是 truthy,这个分支永远不走;
40
+ 2. `serveSavedCatalog` 的 stale 路径返回 `fallback: []`,兜底目录从未被填上(与它自己的文档注释「返回 `{ models: [], fallback: [...] }`」直接矛盾);
41
+ 3. 根因:运行时目录 `catalog` 构造时就种了 9 个静态兜底模型、`set()` 拒绝空数组,所以 `live.length > 0` 恒成立、永远提前返回 —— 挂在它后面的整个 lastCatalog 分支(`serveSavedCatalog` 唯一的调用处)不可达。
42
+ 当时的集成测试只断言 `ctx.llm.listModels`(适配器路径,读运行时目录),恰好绕开 `displayModels` —— 于是测试全绿、修复无效,CHANGELOG 差一点写下一个没发生的修复。这轮把空档位判据挪到**真正被服务的 live 列表**上、删掉不可达分支,并把测试改成走**真实的 `/usage` 路由**(假 webServer 捕获路由,见「签到积分不刷新」那条)。
43
+ - **判据用 `length > 0` 而不是「字段在不在」**:旧 schema 留下的是**空数组**而不是缺席,所以「字段在」恰恰不能作为「声明过」的证据。两种「没有档位」(旧快照的空数组 vs 上游真的没声明)从数据上分不开,**要不要重拉由目录来源决定**(见修法里的守卫);判据与动作继续打包在 `serveSavedCatalog(models, onStale)` 里 —— 拆开接线时,把调用处变异掉单测照样全绿。
44
+ - **测试**(`tests/index.spec.ts`,+2 走真实 `/usage` 路由):一条钉**自愈** —— 启动拉取失败后,第一次读卡片**继续服务旧快照里的模型**(模型不消失、无档位),同时触发一次重拉;落地后下一次读,档位来自实时目录。另一条钉**守卫** —— 上游诚实声明「无档位」时,实时目录落地后的卡片轮询不再打模型列表端点。三组变异测试:删掉触发(复现原死代码)/ 去掉守卫(读路径变打网络循环)/ 恢复 `models: []` 的旧契约 —— 各自被对应测试接住。
45
+ - **每日签到:领取之后不说「还有多久刷新」—— 而「已领取」这句话离开它是残缺的**(用户实拍官网页面上的 `09:22:23` 倒计时,并指出领取成功要判断是否有倒计时)。
46
+ - **先去看接口,而不是猜**:用本地那个能用的 PAT 只读拉了一次 `GET /sash/api/v1/me/campaigns`(**没有领取任何东西**)。实测活动行是:
47
+ ```json
48
+ { "campaignId": "01a12102-…", "campaignKey": "act-20261009-057",
49
+ "actionType": "CLAIM_BENEFIT", "claimStatus": "CLAIMED",
50
+ "startAt": 1791770400, "endAt": 1791856740,
51
+ "benefit": { "kind": "CREDITS", "amount": 100,
52
+ "validity": { "mode": "RELATIVE_DAYS", "days": 30 } } }
53
+ ```
54
+ 换算(UTC+8):`startAt` = 当天 **10:00:00**,`endAt` = 次日 **09:59:00** —— 与活动文案「每日 10:00(UTC+8)刷新」一致。所以页面上那个 `09:22:23` 就是 **`endAt − 当前时间`**。
55
+ - **领取不是「今天到此为止」**:领取只是把这一窗从 `CLAIMABLE` 变成 `CLAIMED`,而下一窗什么时候开,上游早就写在 `endAt` 里了。此前插件只回一个 `already-claimed` / `claimed`,UI 只能说「已签到」—— 用户会以为要等到第二天零点。
56
+ - **修法**(`endAt` 是 epoch **秒**,转毫秒只发生在它变成时间戳的那一刻;解析层保持秒,否则 `startAt`/`endAt` 单位会不一致):
57
+ - `QoderCampaign` 解析 `startAt` / `endAt`;`QoderCheckinOutcome` 在 `claimed` **与 `already-claimed` 两条路**(含 409 幂等)都带上 `nextRefreshAtMs`;
58
+ - 宿主快照从 `Record<id, boolean>` 升级为 `{ claimed, refreshAtMs? }`,写进成员行的 `checkinRefreshAtMs`;
59
+ - 客户端新增 `countdownText()`(`HH:MM:SS`,秒级向上取整,小时不封顶),由既有的 20 秒 `nowMs` 心跳重绘;样式用等宽数字(tabular-nums),否则位数一变整格宽度就抖、表格列跟着晃。
60
+ - **上游没给 `endAt` 就不显示倒计时**,绝不用本地零点或「上次 +24h」编一个。
61
+ - **顺带修掉一个真缺陷**:客户端最初把「已领取但上游没给时间」与「倒计时走完了」都表达成 `countdownText(...) === ''`,于是前者被渲染成「现在可以领取」—— 把「不知道下一窗什么时候开」说成了「已经可以领了」。改成四态(`off` / `unknown` / `ticking` / `due`)。测试接住了它:先写成两态时那条断言立刻失败。
62
+ - 测试 +17(`tests/checkin-countdown.spec.ts` 新建 7 条、`tests/upstream.spec.ts` +5、`tests/pool-render.spec.tsx` +5),**用实测值锚定**(`endAt = 1791856740`,倒计时断言精确到 `09:22:23`)。四组变异测试:`already-claimed` 丢掉刷新时刻 / `unknown` 塌成 `due` / 宿主快照丢掉 `refreshAtMs` / 上游缺字段时仍然编一个 —— 各自被对应测试接住。
63
+
64
+ ### Changed
65
+
66
+ - **回复流不再注入「将由账号 … 回答本次问题」。** 账号池成员接管请求时,此前会在回复正文最前面带一行这个提示(`poolServingNotice`,账号标识打码)。宿主侧不再提供该注入点,回复正文从模型自己的输出开始;shim 的注入能力仍保留,只是本插件不再用它。
67
+
68
+ ## 0.3.0 (2026-10-12)
69
+
70
+ 三条主线:**每日签到真的接通了**(我早期把它判成「Qoder 没有签到」,那是没实测就下的结论)、**与 dsh-connect-workbuddy 的逐模块对等审计**,以及收尾的**国际版(global)对等审计**(结论见 [`docs/audit/AUDIT-REPORT.md`](docs/audit/AUDIT-REPORT.md))。对等审计的每条都以 workbuddy 的已验证行为为基准,并对照 pi-ai / DSH 宿主源码逐条核实。
71
+
72
+ 国际版审计最重要的结论**不是**国际版缺代码:一是它此前从未被执行过(`~/.dsh` 下没有 `.qoder-accounts.global.json` / `.qoder-pool.global.json`,CLI `doctor` 实测 `global: vault=0`),二是审计顺带挖出两个**双区域都受影响**的功能性 bug(账号转移的导出/导入往返、CLI 子命令分发)——见下面的 Fixed。
73
+
74
+ ### Features
75
+
76
+ 本版新增的用户可见能力(详细经过记在下面的 Fixed / Changed 里):
77
+
78
+ - **每日签到(CN + 国际版)** —— 「一键签到所有账号」+ 池表格的「签到」列;先查状态再领取(幂等),国际版按区域读取本机 Qoder 客户端的 UMID 设备身份组件产出风控头,取不到时如实报错。
79
+ - **思考强度可选且真的发得出去** —— 卡片按模型只列上游声明过的档位,选定即保存、每个请求带上(顶层 `reasoningEffort` + `parameters.reasoning_effort` 双写,与官方客户端一致)。
80
+ - **「在对话框显示积分」开关移进账号池标题行**,少占一整行。
81
+
82
+ ### Fixed
83
+
84
+ - **国际版 tab「还是缺少账号池这些功能」—— 同一处区域分发上的两个独立缺陷**(用户实拍,接在「国际版看起来就是空的」那轮之后)。
85
+ - **为什么「cn 一切正常」查不出来**:两个缺陷都只伤**非 cn** 的那个 tab,cn 自己完全不受影响。上一轮修的是「signed-out 时账号池不渲染」(`renderPool` 读 `signIn`),那个确实修好了;但只要 usage 文档**根本没落地**,卡片仍然什么都不渲染 —— 症状看起来一样,根因完全不同。
86
+ - **缺陷 1:挂载时只读 `cn`。** `useEffect` 里只有 `void refresh('cn')`,另一个区域要等 **60 秒**轮询才第一次有数据。切到国际版就是一片空白(连账号池与「添加 PAT」一起),要自己等一分钟才长出来。现在挂载即读**两个**区域,并在切 tab 时若该区域还没读过就立刻补读一次(不依赖轮询时机)。
87
+ - **缺陷 2:两个区域共用一个 `AbortController`。** `refresh()` 第一行 `controllers.current?.abort()` 取消的是「上一次发起的那个请求」,而那个请求可能属于**另一个区域**:轮询里 `for (const region of QODER_REGIONS) void refresh(region)` 背靠背连发两次,cn 的请求刚建好就被 global 那次取消(反之亦然)。**同一时刻只有一个区域的 usage 能落地。** 现在每区域一个 controller:取消只用于「同区域连续刷新只认最后一次」,跨区域互不干涉。
88
+ - **测试**(`tests/card-render.spec.tsx`,+4 条):用**同一份 signed-in 文档**渲染两个 tab 并逐项比对池/成员行/模型行/签到/测试/添加 PAT/导出导入/签到列;挂载时两个区域都被读到;**推进 60 秒轮询断言两区刷新互不取消**;两 tab 渲染清单完全相等。
89
+ - 每条都做过变异测试:还原「只读 cn」/「共享 controller」/「`renderPool` 读 `signIn`」各自被对应测试接住。其中共享 controller 那条**只在轮询路径上可测** —— 挂载读修好之后它被掩盖了,第一版挂在挂载路径上,变异后仍然通过(说明那条断言是空的),已改成推进轮询后配对「被取消的请求之后是否还有同区域的请求」。
90
+ - **领了签到积分,卡片上的积分不刷新**(用户实拍:「我领了积分,但是积分没刷新」)。
91
+ - **根因**:`runCheckinBatch` 收尾只 `delete usageCache[region]` —— **作废**了缓存,却没有任何人去重新填充它。于是积分停在签到前的数字,直到某个**与签到无关**的动作(改配置 / 点「从 Qoder 刷新」)顺带触发 `refreshUsage`。等于把「读到新值」推给了一个不会到来的下一次无关动作。
92
+ - **修法**:签到后直接 `await refreshUsage(region)`。
93
+ - **必须 await 而不是 fire-and-forget**:客户端收到签到响应后**立刻**调 `onChanged()` 重新读一次 usage(`AccountPool.runCheckinBatch` 末尾)。宿主若不等待,这次重读会发生在上游返回之前、读到刚被作废的空缓存 —— 那是竞态,不是修复。
94
+ - 测试(`tests/index.spec.ts`)挂一个假 `webServer` 把路由抓下来,**真的调那条 checkin 路由**,断言签到之后 usage **被再读了一次**。断言基线取在挂载期的读取**全部落定之后** —— 第一版取早了,一次迟到的挂载期读取混进来,断言就假通过了(这正是它一度在 bug 还在时仍然通过的原因)。变异测试确认:换回 `delete usageCache` 立刻失败。
95
+ - **账号池标题行整体对齐 workbuddy**(用户要求:「这部分区域的样式整体对齐 workbuddy」):做了一次机械对比,而不是逐处猜。结论是**样式层已经高度一致**——表格 21 条同名规则逐字相同、DOM 类名顺序也完全一致;账号工具框与设置区 9 条同样一致。本轮真正修掉的是标题行:
96
+ - `.dsm-qoder-pool-head`:`gap` 12px → **10px**、去掉 `justify-content:space-between`(workbuddy 同规)。
97
+ - 积分开关改为 workbuddy 的 `pool-head-extra` 形态:**紧挨标题**的直接子项,而不是我上一轮自创的 `.dsm-qoder-pool-head-right` 分组(那多包一层、还改了换行与间距行为)。那个自创类已删除。
98
+ - 「当前使用」芯片改用 `margin-left:auto` 靠右(workbuddy 同规):开关文案长度随区域变化时芯片不再左右晃。
99
+ - `.dsm-qoder-credits-switch`:`display:flex` → `inline-flex`(它现在嵌在标题行里)。
100
+ - 保留三处**有意保留**的差异(都是前几轮用户明确要求的,不回退):账号池 `padding-top:8px`(要求收紧空档)、保存行去掉 `flex-wrap`、按钮组去掉 `flex:none`(要求两条保存行样式统一)。
101
+ - **tab 栏不再顶着上面的分隔线**(用户实拍:「国际版 国内版 不要顶着上面这条线,最基本的统一样式」):`.dsm-plugin-card-body` 这条家族共享规则只有 `padding:0 0 8px`——**上内边距为 0**,所以正文的**首个元素必须自带顶部间距**。workbuddy 的 tab 栏正是被 `.dsm-workbuddy-usage`(`padding:16px 0 4px`)托住的;qoder 这边在上一轮删掉积分块时,把那层容器一并拆掉了,于是 tab 栏直接贴在分隔线上。现在用同一条规则 `.dsm-qoder-usage`(本就存在)恢复同样的 16px 呼吸空间——不是新加特例,是恢复与同族插件一致的结构。
102
+ - **「在对话框显示积分」开关移到账号池标题行**(用户要求:「放到账号池这个标题旁边吧,节约点空间」):它此前在卡片正文里独占一行,约 30px 高却只有一个勾选框。现在渲染在 `pool-head` 右端、排在「当前使用」芯片**之前**(可操作的控件在状态之前)。卡片仍然持有写权限与区域,所以由卡片构造这个节点、经 `creditsSwitch` prop 交给账号池安置(与 `accountTools` 同规);原来那个只为它存在的 `.dsm-qoder-credits-row` 外壳随之删除。语义也更顺:它决定「积分怎么显示」,而积分明细正是下面那张表的内容。
103
+ - **「在对话框显示积分」那行上移**(用户实拍:「红色的部分向上靠拢一些」):额度明细移除后,那一行仍套着为明细列表准备的 `.dsm-qoder-usage`(`padding:16px 0 4px` + `gap:16px`),连同 `.dsm-qoder-credits-switch` 自己的 `margin:8px 0 2px` 与账号池区块的 `padding-top:16px`,三处叠加出约 19px 的空档。现在:这一行换成自己的外壳 `.dsm-qoder-credits-row`(`padding:12px 0 2px`)、开关的 margin 归零(间距由外壳统一负责)、账号池的 `padding-top` 收到 8px。分隔线之上只剩约 11px。
104
+ - **国际版签到接通 —— 两个区域能力对齐**(用户要求:「国际版也照着国内版做」)。
105
+ - 盘点后发现**只有一处**不对称:`checkinSupported: reg => reg === 'cn'`。网关、模型目录、对话、PAT、账号池、积分都已按 region 对称(区域配置表里两边字段一致)。
106
+ - 国际版的差别只在认证细节:它的签到活动**只下发给带设备风控头的请求**(`Cosy-MachineToken` / `Cosy-MachineCode` / `Cosy-MachineType`)。国内版实测不需要,多发反而是噪音。
107
+ - 新增 `src/device-risk.ts`:按区域读取本机 Qoder 客户端自带的 UMID 组件(`umid/runtime-info`),它直接产出这三件套。国内版优先 `Qoder CN.app`、国际版优先 `Qoder.app` —— 设备身份与区域绑定,用错一侧会被风控拒绝。结果按 50 分钟进程内缓存(参考实现同量级),避免每次签到都 fork 进程。
108
+ - 组件不存在时**如实报错**,而不是静默降级成「无头请求」—— 那样只会拿到空活动列表,用户看到「今天没有活动」而真实原因是取不到设备身份。可用 `QODER_UMID_PATH` 显式指定路径(该值是**优先**而非唯一来源:指错了路只要本机还有客户端就继续用)。
109
+ - `QoderRegionConfig` 新增 `requiresDeviceRisk`,由 `sashRequest` 按区域条件附加请求头。
110
+ - 实测:本机两个 App 都在,各自命中自己的组件;CN 请求只带 `cosy-clienttype`,国际版带齐三个 machine 头。
111
+ - **「添加 PAT」那一行补上就地说明**(用户要求:「添加 PAT 后添加文本(登录网页 qoder.com / qoder.com.cn → 服务集成 → 新建令牌),国际国内域名不同」):
112
+ - 文案放在**入口这一行**(标签与按钮之间),而不是只留在对话框里的文档链接 —— 用户看到这个入口的瞬间,最想知道的就是「去哪弄一个令牌」。
113
+ - 域名逐区域不同,由 `region` 决定:国内 `qoder.com.cn`、国际 `qoder.com`。
114
+ - 新增 `QODER_CONSOLE_DOMAIN` 常量,与已有的 `knownQoderRegionOfDomain`(域名反查区域)**放在一起维护** —— 同一组事实的两种读法,分开放就很容易漂移而不被发现。
115
+ - 注意国际版用的是**控制台域名 `qoder.com`**,不是网关用的 `qoder.sh`:用户在浏览器里看到的是前者。
116
+ - `.dsm-qoder-bar-hint` 用 `flex:1 1 auto` + `min-width:0`:吃掉标签与按钮之间的余量,窄卡片上折行到按钮上方,而不是把按钮挤出可视区。
117
+ - 新增 4 条渲染测试(两种区域的域名、同一行的顺序、域名表本身),防止两处域名漂移。
118
+ - **修掉 400 `developer is not one of [...]`:必须显式关闭 pi-ai 的 developer 角色**(用户实拍上游 400)。
119
+ - pi-ai 的 openai-completions API 这样决定系统提示词的角色:
120
+ ```js
121
+ const useDeveloperRole = model.reasoning && compat.supportsDeveloperRole
122
+ const role = useDeveloperRole ? 'developer' : 'system'
123
+ ```
124
+ Qoder 上游只接受 `system` / `assistant` / `user` / `tool` / `function`,收到 `developer` 直接 400。
125
+ - **这个分支只在同时满足「reasoning 为真」与「compat 未显式关闭」时才走。** 我让档位真正生效后 `reasoning` 从 false 变 true,把第一个条件打开了;`supportsDeveloperRole` 的默认值是**开启**,于是第二个条件也成立 —— 两个改动叠加才暴露出来(这也是为什么它此前一直没出现)。
126
+ - 修法:`compat` 显式写 `supportsDeveloperRole: false`,系统提示词恒以 `system` 角色发出。`supportsReasoningEffort` 保持不变(档位功能不受影响)。
127
+ - 新增两条回归测试:从真实适配器取出 pi-ai provider,断言推理模型 `reasoning === true` **且** `compat.supportsDeveloperRole === false`;非推理模型同样关闭。
128
+ - **「Provider is not configured」的真正原因:`auth.apiKey.resolve` 返回了 `undefined`**(用户实拍,并再次提示我去查参考项目 —— 两次都对)。
129
+ - **错误文案的真实来源**(不在我们的代码里,在 vendored 的 pi-ai 里):
130
+ ```js
131
+ // pi-ai dist/models.js → applyAuth
132
+ const resolution = await this.getAuth(model, {...})
133
+ if (!resolution) throw new ModelsError('auth',
134
+ `Provider is not configured: ${model.provider}`) // ← 用户看到的那句
135
+ ```
136
+ 而 `resolve.js` 在「没有存储凭据」时会退回 ambient,调用 provider 自己的 `auth.apiKey.resolve({ credential: undefined })`。
137
+ - **我们的 resolver 无条件 `return undefined`** → `getAuth` 返回 undefined → 直接抛错。**问题不在档位、不在目录、不在区域注册,而在这一行的返回值形状。**
138
+ - **正确形状**(`dsh-llm-pi-ai` 自己的 `harnessApiKeyAuth()` 就是这样):
139
+ ```js
140
+ resolve: ({ credential }) => Promise.resolve({
141
+ auth: credential?.key === void 0 ? {} : { apiKey: credential.key },
142
+ source: name,
143
+ })
144
+ ```
145
+ 关键:**即使没有 key 也必须返回一个对象**(`auth: {}`),把「是否真的需要 key」交给协议层判断;返回 `undefined` 才等于宣称「这个 provider 根本没配置」。
146
+ - 本 route 的真实凭据不从这里来 —— 它靠 shim 的进程内 secret 认证,secret 由 `PiAiAdapter` 的 `resolveApiKey` 逐请求提供。所以 resolver 报「没有 key」是诚实的、也是正确的,只要它是一个对象。
147
+ - 测试:新增两条,**走真实的 `createQoderAdapter`**(从 `piProvider.auth.apiKey.resolve` 取 resolver 调用,而不是复制一份实现),并做了**变异验证**:退回旧实现后两条立刻失败。
148
+ - 前面两轮(档位 map 全 null 会作废整条路由)也是真实缺陷、也修掉了 —— 但它不是「not configured」的原因;我当时把两个不同层级的失败混为一谈了。
149
+ - **修掉「Provider is not configured: qoder」的真正根因**(用户实拍,并提示「之前参考的项目都是怎么做的」——对照 trae/workbuddy 的 `modelErrors`/`defaultContextWindow` 注释后,去 DSH 内核源码里找到了硬校验)。
150
+ - **根因**:DSH 内核(`dsh-llm`)对适配器返回的推理元数据是**硬校验**的:
151
+ ```js
152
+ if (reasoning.efforts.length === 0)
153
+ throw new LlmError('adapter returned invalid reasoning metadata …', 'INVALID_MODEL_REASONING')
154
+ ```
155
+ 而 `dsh-llm-pi-ai` 的 `getSupportedThinkingLevels()` 把 `thinkingLevelMap[level] === null` 判为「不支持」,并把 `xhigh`/`max` 额外要求 `!== undefined`。于是**「全 null 的 map」解析出零个档位**。
156
+ - 我此前在「上游没声明档位」时**仍然**返回全 null 的 map。上游的 `auto` 与 `qmodel_latest` 恰恰不声明 `thinking_config.efforts` → 零档位 → **整条 provider 路由被 DSH 作废**。用户看到的是「Provider is not configured: qoder」,而不是「这个模型没有档位」。
157
+ - **修法**:`qoderThinkingLevelMap` 在「没有一个真实档位」时返回 `undefined`(而不是全 null 的 map)。DSH 读作「该模型不提供推理」→ 不渲染档位选择器(正确的降级),路由保持健康。
158
+ - 新增两条回归测试:一条复刻 DSH 的 `getSupportedThinkingLevels()`,对九种上游形状断言「map 为 undefined,或至少解析出一个真实档位」——即**任何模型都不会让整条路由作废**。
159
+ - 教训:`provider 整条路由」与「单模型能力」是两个层级。单模型的元数据写错,代价是整条 provider 消失,所以「降级」必须比「声明」更保守。
160
+ - **思考档位改为只映射上游**真正声明的档位**(用户要求:「学习 workbuddy 一样,显示可以选择的档位,然后保存到 DSH 自己的模型设置里」)。
161
+ - 对照 workbuddy 发现两处关键差异:
162
+ 1. **workbuddy 的档位是「只显示」的纯文本**(`model.reasoning.supportedEfforts.join(' / ')`),它不保存用户选择;用户要的是**可选 + 存到 DSH**,所以保留 qoder 已有的可保存选择,只对齐它「显示哪些档位」的判断。
163
+ 2. **workbuddy 的 `workBuddyThinkingLevelMap` 只映射上游声明过的档位**(`supported.includes(level) ? level : null`)——DSH 的模型设置里因此只出现真实存在的档位。而 qoder 此前把六个档位**全部**置 `null`,只在 `off` 上有效:我当时的结论是「Qoder 的线是布尔 `is_reasoning`,表达不了半档」。**那个结论是错的** —— `thinking_config.enabled.efforts` 逐模型声明了真实档位,且不统一(Qwen 系 `xhigh/low/medium`,DeepSeek/GLM 系 `high/low/max`)。
164
+ - 修法:`qoderThinkingLevelMap` 按声明映射**同名**档位,未声明的置 `null`;上游声明可关闭时额外给 `off`。上游没声明任何档位时仍给出 map(全 `null`),因为模型确实是推理模型 —— DSH 据此知道它支持推理,只是没有可选项。
165
+ - 请求线不变:shim 侧仍按 `reasoning_effort` → 顶层 `reasoningEffort` + `parameters.reasoning_effort` 发送。
166
+ - 实测效果:DSH 模型设置里 `Qwen3.8-Flash` 出现 low/medium/xhigh,`DeepSeek-V4-Pro` 出现 high/max(+可关闭)。
167
+ - **卡片改读实时目录,思考档位这才真正出得来**(用户实拍「还是没有思考强度」——即使刚点过「从 Qoder 刷新」)。
168
+ - 上一个提交只修了 schema(字段不再被剥掉),但**没有解决「卡片读的是哪份目录」**。
169
+ - `displayModels()` 的优先序是「保存的 lastCatalog → 实时目录 → 静态兜底」,而卡片读的就是它。`lastCatalog` 是**上一次保存时的快照**:上游后来声明的档位(`thinking_config`)只存在于实时目录里,快照里没有 → 卡片永远不渲染档位选择器。更糟的是旧 schema 已把档位从快照里剥掉,于是那份快照**永远不会**再有档位。
170
+ - 修法:
171
+ - 新增 `fullCatalog[region]` 保存**未过滤的实时目录**(`refreshRegionCatalog` 拉到的完整列表,含上游声明的档位);
172
+ - `displayModels()` 改为**实时目录优先**:反映上游当前声明了什么,保存快照退居兜底。
173
+ - 静态兜底列表(`QODER_FALLBACK_MODELS`)**没有**档位是刻意的 —— 它是猜的,不该编造上游没声明的档位名;此时卡片不渲染档位选择器,等真实目录到达后自动出现。
174
+ - 验证:实测上游确实返回档位(`qfmodel` → `['xhigh','low','medium']`,默认 `medium`;`dmodel` → `['high','max']`),且 `parseUpstreamModel` 解析正确。
175
+ - **修掉「拉取的思考模式又不见了」:保存的模型目录丢字段**(用户实拍)。
176
+ - 客户端**确实**把 `thinkingEfforts` / `defaultThinkingEffort` / `contextTiers` 写进了 payload(`buildModelsPayload` 里三个字段都在)。
177
+ - 但宿主的 `modelConfig` **没有声明**它们。schemastery 解析保存的 `lastCatalog` 时会**静默剥掉未声明的字段** —— 于是磁盘上根本没有这些字段,重启后 `thinkingEfforts === undefined`,卡片按「缺失就不渲染」的规则把整个档位选择器隐藏掉。
178
+ - `modelFromStored` 同样没有把这两项读回来。
179
+ - 修法:schema 里补上三个字段(`required(false)`,保持「缺失即不干预」),`modelFromStored` 把它们读回。
180
+ - 新增回归测试:用真实的 region schema 验证「保存 → 读回」三个字段都不丢。
181
+ - **教训**:这是同一类 bug 的第三例——「客户端写了,但读回时被丢弃」。写字段与读字段必须成对,且要有 round-trip 测试兜底。
182
+ - **修掉「保存模型保存不了」:`__save` 路由没有按区域合并**(用户报告「现在保存模型保存不了是什么原因」)。
183
+ - **客户端**只发它要写的那**一个**区域(`{ [region]: value }`)——它读不到另一侧的当前状态,无法自己做合并。
184
+ - **宿主路由此前**把这个 body **整个**写进 settings(`deps.settings.set(field, body.value)`),于是**保存 cn 模型的这一次操作会静默删掉整个 global 槽位**。
185
+ - 后果已经发生:磁盘 `cordis.patch.yml` 里 `regions` 只剩 `cn`,`global` 的全部配置被抹平。用户看到的现象是「保存不了」,而实际被抹掉的是另一侧的一切。
186
+ - 修法:宿主有 `settings.describe()` 能读到现值,所以合并放在服务端做(`mergeSlotWise`):请求里出现的区域**整份替换**,未出现的区域**原样保留**;顶层其他字段一并保留。槽位内部的字段级合并仍由客户端的 `nextRegionModels` 负责(它从现槽位起 merge)。
187
+ - 新增回归测试,并做了**变异验证**:把合并去掉,该测试立刻失败(防止写成永远绿的断言)。
188
+ - **修掉真正的根因:`<p>` 的浏览器默认边距**(用户实拍「还是没有改善」)。前两次我都在调容器的 `gap`/`padding`,但那根本不是空白的来源 —— `.dsm-qoder-usage-hint` 渲染成 `<p>`,而**浏览器默认给 `p` 加 `margin:1em 0`(上下各 16px)**。本文件里其他每一条渲染成 `<p>` 的规则都写了 `margin:0`,**唯独它和 `.dsm-qoder-hint` 漏了**。于是那 16px 一直顶在那里,改容器当然「没有改善」。
189
+ - 修法:两条规则都显式 `margin:0`,上下间距真正交还给容器控制(`usage-hint` 用 `padding:6px 0 6px 19px`)。
190
+ - workbuddy 的同名规则有同样的遗漏(所以那边同样偏松),但此处按用户的判断修,不做「保持一致」。
191
+ - 新增一条**机制性**测试:凡渲染成 `<p>` 的类,其规则必须含 `margin:`。这类 bug 靠看截图很难定位(浏览器默认样式不出现在任何 CSS 文件里),但可以机械拦住。
192
+ - 同时修掉了测试辅助函数 `ruleOf` 的正则未转义类名里的点 —— 它此前会把 `.dsm-qoder-hint` 当成通配模式匹配,可能命中别的规则或匹配不到(假绿灯)。
193
+ - **「保存的模型」终于真的生效了(第 2 次修,这次找到了架构层面的根因)**(用户实拍:「模型的保存还是不对」)。我上一次只改 `deriveCatalog` 本身,那是治标。真正的根因是**两个模型视图被我混成了一层** —— workbuddy 分得很清楚:
194
+ | 视图 | 数据源 | 是否按勾选过滤 | 谁在读 |
195
+ |---|---|---|---|
196
+ | 运行时目录 | `stacks[region].catalog` | **✅ 过滤** | 适配器 `getModels()` → **DSH 内置选择器** |
197
+ | 卡片显示 | `displayModels()` | ❌ 不过滤 | 卡片的模型列表(要能勾上未勾的) |
198
+ - 我此前把过滤放在 `registerModelDiscovery` 回调里,而 **DSH 的选择器读的是运行时目录**(经适配器 `getModels()`)—— 过滤根本没作用到用户看到的那份列表上。
199
+ - 更要命的是 `applySelection()`:它在**每次配置写入时**跑,把完整目录重新装回运行时目录,直接覆盖掉刷新时的过滤结果。这就是「保存了还是一整份」的直接原因。
200
+ - 现在:运行时目录在两处(刷新、配置写入)都按勾选过滤;卡片改走新增的 `displayModels()`(上次保存 → 实时目录 → 静态兜底,workbuddy 同规),所以未勾选的模型仍在卡片里可见、可勾。
201
+ - **DSH 选择器里的模型名补上倍率**(用户实拍:「模型没有倍数」)。卡片自己的列表早已把倍率画在名字旁,但适配器交给 DSH 的 `name` 是裸名字(`info.displayName`),于是同一个模型在两处显示不一致。现在与 workbuddy 同规,`toPiModel` 用 `modelDisplayName(info)`,选择器里显示成 `Qwen3.7-Max · x1.00`。纯显示改动:DSH 侧所有关联都按模型 id 做(选择器当前项、会话事件、agent 默认模型、请求线上的 `model`)。
202
+ - **接通每日签到(Qoder 确实有,此前是我误判为「没有」)**(用户指出「qoder有签到啊」——用户是对的)。我早期把 `checkinSupported` 写死为 false,注释还写着「网关没有签到端点」,那是**没实测就下的结论**:真实路径叫 `campaigns`(活动/权益),名字里没有「签到」二字,所以照着 checkin / signin / reward 猜必然 404(实测猜了 13 个)。以「我猜不到」论证「不存在」是方法论上的错误。
203
+ - **协议**(社区参考实现 L0NE-6/Qoder-Daily,已在本机 CN 账号实测通过):`GET {openapi}/sash/api/v1/me/campaigns?forceRefresh=true` 拉活动,`POST …/{campaignId}/claim` 领取;认证用与用量查询同一套 `jt-` 令牌,另需 `cosy-clienttype:10` 与 `user-agent:Qoder`。只有 `actionType=CLAIM_BENEFIT` 且 `claimStatus=CLAIMABLE` 的可领;重复领取(`CLAIMED` / 409 / `ALREADY_CLAIMED` / `replayed`)是幂等的「今日已领」,**不算失败**。实测你的账号返回 100 Credits / 30 天有效、状态 `CLAIMED`,与桌面端截图一致。
204
+ - **`checkinSupported` 改为真实检测**:CN 为 true、国际版暂为 false(国际版同样有该活动,但需要本机 UMID 组件生成的设备风控头 `Cosy-MachineToken/MachineCode/MachineType`,未接通——字段保留,卡片按它隐藏整列)。
205
+ - **UI 对齐 workbuddy**:账号池表格补回「签到」列(5 列 grid,规则本就存在)、批量动作里补回「一键签到所有账号」(primary、排最前,签到是唯一带来收益的动作),以及签到日志(逐账号三态:已领取 N Credits / 今天已领过 / 失败原因)。签到单元格是**三态**而不是两态——还没读到的账号说「未知」,不说「未签到」,否则会与用户刚亲眼看到的领取结果矛盾。
206
+ - 签到会改变积分余额,因此批次结束后作废该区域的用量缓存,卡片不会继续显示领取前的数字。
207
+ - **卡片正文不再重复额度明细**(用户实拍:「红色框出来的部分,也不需要了,因为下面已经显示了」):那行「附加额度 · 92 / 100 (12/31, 08:00 到期)」与账号池表格的「积分」列是同一组数字(连日期的分钟都一致),而池里是**逐账号**显示、粒度更细。成功状态下这一块不再渲染;`renderCredits` / `bucketLabel` / `formatDate` 三个随之失去引用的函数一并删除。
208
+ - **但「读不到」的情况保留**:池行只能显示一个「—」,说不出原因,而 `creditsError`(凭据被拒 / 上游未响应)只存在于这份 usage 文档里——池行拿不到。所以现在只渲染失败文案,成功时什么都不显示。这一条同样有测试守着(做过变异验证:把失败文案也删掉,测试会失败)。
209
+ - **幽灵成员(凭据已删除的池成员)改为自动清理,不再提示用户**(用户实拍:「这个我之前移除了 2 个 PAT,你应该直接就清理掉,而不是再给我这么一个提示,而且我测试的时候这两个也会涵盖进去」)。三处一起改:
210
+ - **客户端自愈**:`memberAccountIds` 里留有已删除凭据的 id 时,静默把名单收敛为实际存在的成员并写回(`pruningRef` 防重入;只在不脏、不忙、可写时动手——正在编辑草稿时偷改会覆盖用户的选择)。原先那句「有 N 个已保存的账号当前没有可用的 PAT……**再保存一次即可移除它们**」把插件的内部记账问题推给了用户:他删完 PAT 还得回来再点一次保存,而他本来就不想要这些账号。文案键 `poolGhostMembers` 与 `ghostMemberIds` helper 一并删除。
211
+ - **测试路径不再遍历空壳**(用户指出的「测试的时候这两个也会涵盖进去」):批量测试此前对 `memberAccountIds` **原样**建目标,于是每个幽灵 id 都被「测」一遍,只得到一条假的 `credential-rejected`(`no stored credential for this account`),白跑一轮还往池里写脏数据。现在先 `credentialFor` 过滤,只测真的有凭据的成员。
212
+ - 残留的探针记录无害、也不再增长:failover 排名在 `credentialFor === undefined` 时就 `continue`,压根不会读到它们的探针;而新的批量测试不会再为幽灵 id 写记录。
213
+ - **账号行删掉「重新读取账号」与「移除」两个按钮**(用户实拍:「现在都不用授权文件了,哪里还有什么重新读取账号」):
214
+ - 「重新读取账号」做的事(重读快照 + 刷新)现在由账号池的测试/添加等操作自动完成,而「重新读取**授权文件**」这个语义本身早已过时——凭据走 PAT,不再依赖授权文件。用户说得对,这个按钮不该存在。
215
+ - 「移除 · <账号>」与账号池表格的「移除」是**两个不同操作**(前者删保险库里的 PAT 记录,后者只把账号移出池成员),摆在一起只会让人分不清删的是哪一个。保留池里的那个;真要清理凭据用 `dsh-connect-qoder logout`。
216
+ - 连带清理:`rescanAccounts` / `removeVaultAccount` 两个函数、`confirmRemoveId` state、三个只被它们用到的导入(`QODER_ACCOUNTS_PATH` / `QODER_ACCOUNTS_REFRESH_PATH` / `withQoderRouteAction`)、以及 8 个随之失去引用的文案键。账号选择器(多账号时手动指定)保留——它仍是账号池不可用时唯一的指定入口。
217
+ - **积分区不再把同一组数字说两遍**(用户实拍圈出要求清理):顶部那行「92 Token 余额 · 最近积分包 12/31 过期」是**合计**,紧接着的「附加额度 · 92 / 100 (12/31 到期)」是同一组数字的**明细**(还带进度条)。合计行删除,只留明细;`usageToken` / `usageNearestExpiry` / `usageExpiringSoon` 三个随之失去引用的文案键一并清掉。「即将过期」的信息没有丢——账号池的每个账号行各自标 `pool-soon` / `pool-soon-plain`,那里的粒度更有用(哪个账号要过期)。顺带删掉 `renderCredits` 里一个算出来从没用过的 `pct` 变量。
218
+ - **积分(用量)查询一直 401 —— 用错了凭据**(用户实拍「没有积分」,并指出令牌明明是新的)。`fetchUsage` 打 `{openapi}/sash/api/v2/me/usage` 时带的是 **COSY 签名 bearer**(和聊天/模型列表同一套),而该端点不认它。实测三种凭据(2026-10,CN,同一张有效令牌):
219
+
220
+ | 凭据 | 结果 |
221
+ | --- | --- |
222
+ | 裸 PAT `pt-…` | 401 `TOKEN_EXPIRE / token is not active` |
223
+ | COSY 签名 bearer | 401 `TOKEN_INVALID / invalid token` |
224
+ | jobToken 的 `securityOauthToken`(`jt-…`) | **200 + 完整 `qoderUsage`** |
225
+
226
+ `sash` 是**浏览器会话面**的接口:缺 Authorization 时它回 `UNAUTHORIZED / missing cookie header`,可见它本为 cookie 会话设计,既不收 PAT 也不认 cosy 协议头。`TOKEN_EXPIRE / token is not active` 这条消息误导性极强——它让人以为令牌失效,实际只是「这种凭据不适用于这个端点」。新增 `jobTokenGet()`(只换 Authorization,不带 cosy-* 头)专供 `sash` 系列取数;`fetchUsage` 改用它,PAT 直连降为兜底。测试锁住「带的必须是 jt- 令牌」。
227
+ - 顺带修正 `docs/PAT.md`:原文据此把「积分读不到」写成了「令牌可能坏了」,已改为「令牌有效性不能只看单一端点」并附上面这张实测表。
228
+ - **限流(429)对宿主的 code 改写为 `quota_exceeded`**(HTTP 状态仍 429,上游消息原文透传)。原 `rate_limit_exceeded` 会被宿主归入 `RATE_LIMIT` **原地重试集**(默认 5 次指数退避),把重试预算烧在小时级冷却上——正是 workbuddy 用 `KIND_HOST_CODE` 修掉的同一个缺陷形状。
229
+ - **账号池接管通知改走首个内容帧**(原为 `choices: []` 的自定义 `notice` 帧):pi-ai 的 chunk 循环对无 choice 的帧直接 `continue`,换号通知此前**用户完全不可见**。现在以引用块前缀写进 `choices[0].delta.content`。
230
+ - **换号重试前增加 2 秒可中断间隔**(对齐 workbuddy 的 `FAILOVER_ACCOUNT_GAP_MS`):上游限流按请求体积触发,零间隔连环重试会让每个候选账号撞上同一堵墙。
231
+ - **策略层重复返回已试账号时 shim 侧自行刹车**(`tried` 检查不再只依赖策略层)。
232
+ - **客户端中途断开即中止上游请求**(`res` 的 close → AbortController),且断开前的失败不再记成账号池测量(断开不是账号的错;workbuddy 同规)。
233
+ - **补回 Origin 回环校验**(插件注释自称「Host / Origin / Content-Type 三重校验」,实际漏了 Origin)与**请求体 64MB 上限**(超界回 413,先排干上传再回写,避免 socket 被拆后客户端连状态码都看不到);新增 `/healthz`。
234
+ - **`tool_choice:"none"` 落实为不带 tools**:cosy 模板没有 `tool_choice` 字段,其他形态无法透传,但「宿主禁止工具」必须生效(workbuddy `prepareChatBody` 同义)。
235
+ - **模型行的「免费」标记不再误用「总量」文案键**:新增 `row.modelFree`(免费 / Free)。
236
+ - **模型行的模型名恢复显示 `displayName`**:原误渲染为 `row.usageTotal` 文案(每行模型名都显示成「总量 / total」),与免费标记是同一个占位键漏改的两个实例。
237
+ - **区域 tab 栏恢复 workbuddy 同构的两层结构**:原实现把按钮与开关的嵌套写反,并把 `dsm-qoder-tab-dot`(7px 状态圆点的样式类)拼到了 tab **容器**上——圆点规则会把承载元素缩成 7px 圆,整条 tab 栏塌成两个被裁切的小药丸。现为 `tab-cell > button.tab + label.tab-switch`,激活态与停用态(`tab-active` / `tab-off`)落在按钮上,并补 `role="tablist"` / `role="tab"` / `aria-selected`。
238
+ - **设置页上卡片默认展开**:`open` 初始值由 `view === 'summary'` 改为 `view === 'page'`(与 workbuddy 同规);此前 bundle 页打开时卡片是收起的。
239
+ - **卡片按钮字号归一到 13px**:「重新读取账号」(原带 `pool-small-btn`,12px)、「从 Qoder 刷新」(原误带行内探测类的 `model-probe`,11px)、账号行的删除与两步确认按钮(原 12px)全部去掉小尺寸覆盖类;`model-probe`(11px)只保留给模型行内的「测一测」,`pool-small-btn`(12px)只保留给账号池表格内的行按钮——与 workbuddy 的应用位置逐一对应。
240
+ - **卡片页脚恢复家族结构**:原 `<footer class="dsm-qoder-foot">` 的类**从未有对应 CSS 规则**——链接渲染成浏览器默认蓝、`margin-left:auto` 因无 flex 容器失效,两段文字挤作一团。现与 workbuddy 同构:仓库链接以 `usage-cheer`(灰色 13px + ★)放入保存行左侧,「放弃修改」按钮恒在(未改动时禁用,不再条件渲染),保存错误改为按存在条件渲染(原实现恒渲染一个空 `div`,平白吃掉一段垂直间距)。
241
+ - **删除页脚常驻的「如何创建 PAT?」提示行**(用户实拍要求):workbuddy 的 `usage-hint` 只在有话要说时出现(需要重新登录 / 选择丢失),从不作常驻页脚。该指南入口移进「添加 PAT」弹窗——用户打开那个弹窗的时刻正是需要它的时刻;卡片页脚自此不再有任何常驻行。
242
+ - **账号选择器改为按需出现**(`accounts.length > 1` 才渲染):零账号时该行只剩一个不可操作的下拉,选项文本是「自动 —— 设置 → 环境变量 → 保险库」,对没有账号的用户是一句无法解读的话;单账号时「自动」等于唯一候选,切换是空操作。两种情况都不再画这一行。
243
+ - **模型行的名称与倍率对齐 workbuddy 渲染**:倍率由本地化句子 `积分 0.79x` 改为 workbuddy 的 `(0.79x)` 括号形态(`toFixed(2)` 固定两位,免费模型显示 `(0.00x)`);模型名下方那行模型 id 不再渲染(workbuddy 的行从不显示 id),id 降级为名称的 `title` 悬停提示。随之删除失去引用的 `row.modelRate` 文案键与 `.dsm-qoder-model-id` 样式。
244
+ - **账号池「目标测试模型」下拉显示模型名与倍率**(原直接渲染模型 **id**,如 `qmodel_latest`):改为 workbuddy 的 `名字 (x0.79)` 形态。为此 `RenderSettings` 的 `catalog` 由 id 数组改为模型对象数组。
245
+ - **账号池目录补回真实倍率**:路由层投影(`poolModelOf` 与 pool 视图的 `targetModels`)此前只对免费模型写 `creditMultiplier: 0`,**付费模型一个数字都没有**——选项读起来像免费。现改为透传上游 `priceFactor`;未声明倍率的仍保持缺省(不编造数字)。
246
+ - **区块顺序对齐 workbuddy**:账号池管理(账号池表格与设置)从模型列表**之后**移到**之前**(账号行 → 账号池 → 模型列表)。原顺序要求用户先翻过整张模型表才够得到池设置;workbuddy 的 `AccountPool` 就在模型区之前。
247
+ - **模型行体验对齐 workbuddy(四项)**:
248
+ - 测试按钮从「每行独占一个 details 行」挪进**头部行右端**,说明文字只作按钮悬停提示(`title`)——此前未测试的每个模型行都铺着一整段相同的「对该模型发一条真实聊天请求……」说明;探测结果行改为仅在有结果时渲染(workbuddy 的 `result === undefined → null` 同规),并改为单段 `<p role="status">`(结果 + ` · 原因`,全文进 `title`)。
249
+ - 删除「推理强度:」空尾巴标签:它来自 `t('row.modelReasoning', { efforts: '' }).replace(': ', '')` 的 hack——全角冒号根本匹配不到 ASCII `': '`,且 Qoder 上游不给 effort 列表,这个标签永远只渲染半个句子。随之删除失去引用的 `row.modelReasoning` 文案键。
250
+ - 上下文档位选择器从 details 行挪进头部行(workbuddy 的 context-budget fieldset 同位)。
251
+ - 列表尾部补上能力声明注记(`row.modelCapabilityPending`,「仅展示 Qoder 接口明确公布的模型能力。」——locale 键早已存在但此前从未渲染),并补 `.dsm-qoder-model-capability-note` 样式。
252
+ - **倍率为 1 的模型也显示 `(1.00x)`**:`webModelOf` 曾把 `priceFactor === 1` 当「无倍率」丢掉(兜底目录全部中招,卡片上一个倍率都看不到);`modelDisplayName` 同样在 1 时省略后缀。两边都改为一律透传/照写,与 workbuddy 卡片、DSH 选择器及 Qoder 自家菜单(`· x1.0`)一致。
253
+
254
+
255
+ - **恢复建议从未实测,却在断言(用户实拍)**:卡片上的「本区域还有其他账号,但刚才都没能通过上游校验」是**猜**出来的——`web-status` 写的是 `reloginRequired: accounts.length <= 1`,只要存在第二条记录就宣称「可以切换一个试试」,而那条记录可能同样过期(用户实拍:同一个邮箱的两条记录,一条已过期,卡片却让它去切换)。`credential-recovery.ts` 里早就写好了一个会**逐个探测**其余账号、只把真正答过话的那个承诺给用户的实现,但**从未被任何地方调用**。现在接线为 `credentialRecovery` dep:切换建议基于实测,「其他账号也没通过」也基于实测。
256
+ - **「移除账号」按钮渲染了整句说明**:按钮文字用的是 `row.accountRemoveHint`——那是一整句解释(「把这个账号的 PAT 从插件保险库移除。设置中的 PAT、环境变量 PAT 与 Qoder 官网本身的登录不受影响。」),按钮因此撑成一大块;两个同名账号的按钮挤在一起,完全读不出是干什么的。新增短标签 `row.accountRemoveShort`(移除 / Remove),整句说明留在 `title` 上继续可达。
257
+ - **同名账号无法区分**:同一个邮箱重新签发 PAT 会得到**两条**记录(id 由 PAT 推导,是不同的),两条都叫同一个名字——下拉与删除按钮上只显示名字,「移除哪一个」「当前在用哪一个」都看不出答案。现在重名时补 id 尾号(末 4 位),正在计费的那条带「使用中」标记;下拉与删除按钮共用同一套标签,避免「在下拉里选中一个、到按钮上又认不出」。
258
+ - **添加 PAT 时填的显示名没有任何地方使用**(用户实拍指出):客户端一直会把这个名字随请求发给宿主,但路由里的 `add` 分支**完全忽略它**——它只调 `validateForRegion` 拿上游解析出的名字并写库,于是用户给 PAT 起的名字(「公司号」「备用」)填了等于没填,列表里显示的永远是上游那个标识。现在**用户填的名字优先**,没填才退回上游解析出的名字(两者都空则写空串,卡片渲染「未命名」);顺带让命中已存在 id 的添加也按本次输入改名。成功提示「已添加「X»。在上方选中它即可使用」也随之改掉——账号选择器只在多个账号时才出现,单账号下那句话没有对应控件。
259
+ - **why it existed**: workbuddy 走 OAuth,昵称由上游直接给出,没有「手动填名字」这一步,所以对等审计时这一条没有可对照的实现——它是 qoder 特有的 PAT 模式才需要的功能,只做了前端、没接到存储。
260
+ - **凭据被上游拒时静默成空白(「积分看不到」的真正原因)**:实测过期 PAT 打上游返回 `401 TOKEN_EXPIRE / token is not active`,而 `fetchUsage` 把**所有**错误一律吞成 `undefined`,宿主于是走成功分支写入 `{usage: undefined}`——卡片既没有积分、也没有任何错误提示,只剩一片空白,用户无从判断发生了什么。现在 `fetchUsage` 区分两类失败:**凭据被拒(401/403)抛出**(两条回退路径都拒绝才抛,保留原始错误),其他错误(超时、5xx、网关抖动)仍返回 `undefined`(那是「这次没读到」,报成凭据被拒会把用户引去换一个没问题的 PAT);宿主把 `kind === 'auth'` 记为 `credential-rejected`,卡片随之渲染「凭据被拒」区块与切换/添加 PAT 的出路。
261
+ - 同一处静默还导致 `credential-recovery` 的凭据可用性判定**永远为真**:它靠 `try { await fetchUsage } catch { usable = false }` 判断,而原实现正常返回 `undefined`,`await` 不抛错——过期凭据因此被判定为可用,于是「切换到另一个账号」的恢复建议建立在错误的可用性判断上。
262
+ - **思考强度:从「一行只读文字」做成真正可用的设置**(用户指出「没有地方可以选择」)。上游模型条目带 `thinking_config.enabled.efforts`(实测形状 `{xhigh:{}, low:{}, medium:{is_default:true}}`,键即档位名);上一轮只在模型行渲染了一行只读文字,这一轮补上真正的选择器与**发送链路**:逐模型保存的 `thinkingEfforts`(一项设置——选好保存后每个请求自动带上,与 Qoder 客户端的 `reasoning-effort-changed` 同形态),卡片上是带「跟随上游默认」选项的下拉。
263
+ - **档位确实发得出去**(修正上一轮的错误判断):我此前查了 `model_config` 只有四个字段就断言「发不了档位」——查得不够。档位走的是**另一个字段**:反编译 Qoder 官方客户端(`app.asar.unpacked` 里的 agent SDK)可见请求体为 `{..., reasoningEffort:"none", parameters:{...A.parameters, reasoning_effort:"none"}}`,即顶层 `reasoningEffort` 加 `parameters.reasoning_effort` 双写。现在两个位置都写,与官方客户端一致。
264
+ - **选中档位即开启思考**:档位与布尔 `is_reasoning` 是同一件事的两个粒度,所以保存了档位就按开启处理;宿主明确要求 `reasoning_effort: "off"` 时让位(那一刻的意图优先于设置)。
265
+ - **键序不可信**:上游给的顺序是 `xhigh, low, medium`,直接显示会让用户以为 xhigh 是最弱档;解析按强度语义序表(`minimal<low<medium<high<xhigh<max`)排序,未知档位名排在已知档之后以保证顺序稳定。
266
+ - **不是每个模型都有档位**:`auto`、`qmodel_latest`、`mmodel` 等实测 `efforts` 缺失,此时字段缺席、卡片不渲染控件(空控件会让人以为「支持思考却没有强度可选」);档位名也不统一(Qwen 系 `low/medium/xhigh`、DeepSeek/GLM 系 `low/high/max`),直接显示上游档位名、不做本地化与跨模型比较。
267
+
268
+ - **积分与目录的成败解耦**:`refreshUsage` 此前链在目录刷新成功的分支里——动态目录一挂(启动时网络抖动、网关超时……),积分就永远不来,卡片上「看不到积分」。现在目录失败只落兜底目录,积分照常拉取。
269
+ - **账号池投影的倍率规则统一**:`index.ts` 的 `poolModelsOf` 是第三处「只对免费模型写 0、付费模型没数字」的投影(前两处在 web-status,上一轮已修)。三处现统一为「免费折叠为 0(`resolveTargetModel` 只认 `=== 0` 的契约),付费透传真实 `priceFactor`」。
270
+ - **卡片头部补 `aria-label`**(「展开/收起:Qoder Connect」):图标按钮此前对读屏没有名字(workbuddy 同规)。
271
+ - **模型行的图片/关闭思考复选框恒渲染**:是否可用只看写权限,不再按上游字段有无隐藏控件(workbuddy 同规)——隐藏会让能力未知的模型行缺控件,行与行控件数量不一、视觉抖动。
272
+ - **区域 error 分支不再渲染空壳**:usage 拉取失败时此前因 `regionOn` 兜底为 true 而继续渲染账号行/账号池/模型列表的空架子;现在与 workbuddy 同规,只留错误行。
273
+ - **「凭据被拒」区块重构为 workbuddy 同构**:`usage-error` 容器(`role="alert"`)+ `strong`/`p` 结构;「可切换 / 无号可切 / 有号但都失败」三态说明互斥,切换动作是独立按钮(busy 时显示「正在读取…」)。新增 `row.credentialRejectedSwitchAction` 文案键。
274
+ - **模型保存/区域开关/积分开关与账号池保存互斥**:账号池经新增的 `onBusyChange` 上报保存窗口,池保存期间卡片侧三类写入一律禁用(两边写同一个 region slot,并发会以旧快照互相回滚);模型「保存」按钮同时补上「至少启用一个模型」的门栏(workbuddy 同规)。
275
+ - **账号池活动日志跨折叠/跨 tab 留存**:日志从组件 state 迁入模块级 `logStore`(每区域一份,上限 50 条,「清空」同时清 store)——此前折叠卡片或切换 tab 就清空历史,看起来像操作没发生(workbuddy 同规)。
276
+ - **账号池两张表合并回一张成员表(workbuddy 同构)**:成员复选框、状态点(排除=红/琥珀、未测=灰、测过=绿/琥珀)、账号名 +「使用中」标签 + 成员标签 + 测试时间/排除原因、积分(总数 + 临期高亮/最近到期)、探针(结果 + 来源/时长)、行操作同处一行;原「只读状态表 + 独立勾选表」双表拆除。成员表头带选中计数与全选/清空;区块顺序重排为 表 → 目标告警 → 设置(目标模型带来源一句话「当前:xx(你指定/自动挑选的免费模型)」)→ 区域说明 → 日志;陈旧目标在 select 中回退显示原值(`targetModelId || staleTargetModelId`),保存/行删除期间盖 BusyOverlay。
277
+ - **账号池成员行的积分列接通了数据**:此前 `buildPoolView` 从不填 `credits`/`expiringSoon`/`nearestExpiryMs`,积分列恒为「—」。新增 `memberCredits` dep(按成员逐账号读 usage,单账号失败只让该行显示「—」),并把读数写入 10 分钟快照供请求路径的 failover 排名使用(`rankPool` 的积分打分项此前永远按 0 排;workbuddy 的 `creditsSnapshot` 同规)。
278
+ - **账号池设置区补回外壳与保存行细节(workbuddy 同构)**:设置区自己的小标题(「账号池设置」)+ 脏标记 + 说明行(「下面是偏好设置:点保存才生效……」——locale 键早已存在但从未渲染);保存行左端补仓库链接(★ 鼓励一下),「放弃修改」改为恒渲染(未改动时禁用,不再条件渲染导致这一行左右跳动),并去掉保存行里与标题重复的脏标记。
279
+ - **账号池保存行挪到设置卡片之外**(用户实拍:「还是不对」——上一轮我只改了 CSS 规则,没找到真正原因):
280
+ - **真正的根因是嵌套**:`pool-save-bar` 被放在了 `pool-settings`(那张 `overflow:hidden` 的带边框圆角卡片)**内部**,于是左端的仓库链接紧贴卡片边框,而上方的「启用账号池」内缩 14px——同一条左对齐线断成两截。workbuddy 里它是那张卡片的**兄弟**。现在移出卡片,它自带的 `border-top` 也正是这一行与卡片之间的分隔。
281
+ - 顺带修掉两条规则的措辞差异(池那条多写了 `flex-wrap:wrap`、按钮组多写 `flex:none`)——同一个「保存行」不该有两套写法。加了 `tests/styles.spec.ts`(纯文本 CSS 断言,不需要 DOM)与 `pool-render` 的嵌套断言把两件事都钉住。
282
+ - **删掉账号池尾部重复的区域说明**(用户实拍:「这行字不要,都重复了」):`poolRegionNote`(「国内版与国际版是完全独立的两个账号池:账号、开关、目标模型互不影响」)与 tab 栏下方的 `tabHint`(「每个 tab 是一个独立供应商……一侧的改动不影响另一侧」)说的是同一件事。删掉池区那条渲染与两份文案键。同时清掉两个本轮失去引用的文案键(`modelReasoning` / `modelReasoningDefault`——思考强度从只读文字改成选择器后不再使用)。
283
+ - **探针「来源」药丸接上未知态样式**:`.dsm-qoder-pool-prov-unknown`(带描边)规则早已存在但 JSX 从未使用——来源未知的旧记录此前与「测试写入」渲染得一模一样。同时把失败原因从独立药丸改为并入探针主行(`结果 · 原因`,workbuddy 的 `probeLine` 同规):该药丸原被硬编码成琥珀色的「live-request」样式,而原因可来自任何失败结果,颜色在说谎;全文仍在行 `title` 中。
284
+ - **账号工具移到账号池设置区顶部的虚线框**(用户实拍对照):「添加 PAT / 导出导入账号」两行此前直接堆在账号行末尾、没有外框;现在与 workbuddy 同构——`pool-tools` 虚线框落在「账号池设置」标题之下、设置说明行之上(两个工具立即生效,说明行讲的是「下面这些偏好要保存」,顺序颠倒会让读者以为工具也要保存)。为此 `AddAccount`/`AccountTransfer` 由卡片注入账号池(`accountTools` prop),因为它们需要卡片的刷新与区域。同时删掉两行常驻说明(`.dsm-qoder-bar-hint` 用法):workbuddy 的对应行只在等待/完成/出错时出现状态文本,常驻说明把每行撑成三行高,而它讲的「本区域生效的账号」「导出为 JSON…」分别是账号行与弹窗里的话题。导出按钮补上 `accounts.length === 0` 门栏。
285
+ - **「正在计费」的行补回左侧竖条**:`.dsm-qoder-pool-row-current` 此前只有 7% 透明的背景色,暗色主题下几乎看不出哪一行在用;现补上 workbuddy 同款的 `inset 3px 0 0` 品牌色竖条(同一个类名的第二条规则)。
286
+ - **上下文档位:去掉标签,并修掉重复档位**(用户实拍):
287
+ - **200K 出现两次**——为了「显式回落原生值」额外前置了一个原生窗口选项,而上游的 `contextTiers` 本身就以原生窗口开头(实测 `[200000, 400000, 1000000]`),于是同一个值渲染了两遍。现在选项**只**来自上游声明的档位。宿主侧 `contextTiersOf` 本来就用 `new Set` 去过重,重复纯粹是客户端多加的那一项。
288
+ - 标签「上下文」也一并去掉(上一轮刚从「DSH 上下文预算」缩短而来):它紧挨着模型自己的上下文信息(`最大上下文 200,000`),下拉里的 200K / 400K / 1M 已经说明在选什么。可见文字去掉,无障碍名 `aria-label` 保留——读屏用户不能因此失去这一行的含义。
289
+ - **字体样式突兀**(用户实拍):两个选择器此前复用了账号池表格的 `dsm-qoder-pool-num`——13px、74px 宽、8px 圆角的**实心方块**,放进 11px 的模型行(meta 行与头部控件行)里格外扎眼。改为新的 `.dsm-qoder-inline-select`:无边框、无底色、字号与所在行一致(11px / 16px 行高),读起来像这一行的文字而不是表单控件;箭头由外层 `.dsm-qoder-inline` 的 `::after` 绘制(原生箭头在暗色主题下不可控),hover 才提亮、focus 才出现焦点环。选项列表单独给背景色——系统绘制的下拉不继承行内样式,不加会渲染成一列透明文字。
290
+
291
+ - **国际版(零凭据区域)整块空白,连「添加 PAT」入口都没有**(用户实拍:「国际版看起来就是空的」)。
292
+ - **根因**:`QoderCard.renderPool()` 第一行是 `if (signIn?.pool === undefined) return <span />`,而 `signIn` 只在 `status === 'signed-in'` 时存在。一个 PAT 都没有的区域**永远**拿到 `signed-out`,于是账号池整块不渲染 —— 连同由它渲染的「添加 PAT / 导出 / 导入账号」一起消失。**最需要添加第一个账号的页面,恰恰没有任何添加账号的入口。**
293
+ - **为什么之前没查出来**:上一轮修的是同一件事的**宿主那一半**(`buildWebUsage` 的 `signed-out` 支不再提前返回,现在也带上 `pool`),宿主侧单测也确实过了。但客户端还锁在 `signIn` 上 —— 两侧必须同时改,零凭据的区域才会既能看到「这里还没有账号」的说法,又拿得到添加入口。这是**一次只修一半**留下的坑。
294
+ - **修法**:`renderPool` 改读 `usage`(`status === 'error'` 仍排除:那份文档没读成,它的 `pool` 字段根本不存在,渲染空壳只会把错误行挤到下面,与 workbuddy 同规)。`AccountTransfer` 的账号列表同步改读 `doc.accounts`。
295
+ - 新增 4 条卡片级回归测试(`tests/card-render.spec.tsx`):零凭据时账号池渲染 / 「添加 PAT」与导出导入入口都在 / 空池说「还没有账号」而非「成员限流」/ `status=error` 时不渲染空壳。**做过变异测试**:把 `renderPool` 改回读 `signIn`(即还原这个 bug),3 条立刻失败。
296
+ - 审计报告 `docs/audit/AUDIT-REPORT.md` 的结论「国际版代码无缺失、只是从未配置」需要修正为:**代码曾经缺失一个客户端渲染门槛**,零凭据区域因此不可配置。本条即其修复。
297
+ - **行内那个「移除」根本不是删除 —— 移出池与彻底删除此前是同一颗按钮**(用户实拍:「我国内版本,删除唯一的号,但是号并没有删除干净」)。
298
+ - **根因**:`POST /pool?action=remove` 这一支只做了 `updateMembership(...)`,也就是**把 id 从 `memberAccountIds` 里剔掉**——凭据(`~/.dsh/.qoder-accounts.<region>.json`)一行都没动。UI 上按钮却写着「移除」,和账号行里另一个已删掉的「移除 · <账号>」(那个是真删 vault 的)共用同一套文案,读起来就是删除。实测磁盘:点完之后 vault 里那条记录(`pat-5d76517ac7699f87`)原封不动,配置里 `accountId` 也还指着它;下一次「全选」又能勾回来。这就是「删不掉」的完整机制。
299
+ - **修法**:按 workbuddy 的划分把两件事彻底分开 ——
300
+ - **移出池 = 取消勾选并保存**。行内不再有任何「移出池」按钮:勾选框已经在表达「是不是成员」,再给一个只改同一件事的控件,两处迟早会不一致(这次的 bug 就是这么来的)。
301
+ - **彻底删除 = 行内「删除」按钮 + 两步内联二次确认**。宿主侧删掉 vault 条目,并**连带收掉配置里指向它的两处残骸**:区域的 `accountId`(否则卡片会一直说「所选账号已不存在」)与 `memberAccountIds`(否则留下一条勾不上的幽灵 id)。
302
+ - **settings / env 的 PAT 明确拒绝**(409 + `account-not-deletable`)而不是回一个「其实没删掉什么」的 200:那类凭据由插件之外管理,删 vault 条目对它无效;卡片据 `source` 直接不给那些行显示删除按钮。id 先按 `isQoderAccountId` 的形状校验(400),不让畸形值走到宿主内部。
303
+ - **危险动作才配危险样式**:静止态的「删除」保持中性 outline(它和「测试」并排,每次看都要重新判断哪个会丢数据);红色留给确认条里那颗「彻底删除」,也就是用户已经明确表达意图的那一刻。确认条就地展开在那一行的操作格里,不弹窗——确认的是什么必须和被确认的行在同一视线内。
304
+ - 新增 11 条测试:宿主侧 6 条(真删 vault 条目、配置残骸两处都收、删 A 不动 B 的选择、settings/env 被拒、畸形 id 400、缺 id 400),客户端侧 5 条(没有「移除」按钮、点第一下不发请求、确认才发且带对路由与 id、「保留」能撤掉、settings/env 行无删除按钮、失败时翻出结构化文案)。全部做过变异测试:去掉真删 / 去掉残留清理 / 去掉确认闸门 / 无视 `source` 四种改法各自被对应的测试接住。
305
+ - **一个账号都没有时,账号池整段不渲染 —— 于是没有地方添加账号**(用户实拍:「没有账号的时候显示的不对,账号池这个功能也要有,否则没地方添加账号」)。
306
+ - **根因**:`buildWebUsage` 在 `serving === undefined`(没有任何可用凭据)时**提前返回**,那一支刻意不带 `pool` 字段,理由是「不出现 pool 段就等于没有任何账号正在使用」。而客户端的 `renderPool()` 在 `pool === undefined` 时直接返回空 `<span/>` —— **「添加 PAT / 导入账号」这两件工具恰好由账号池区渲染**。于是「一个账号都没有」这个最需要添加入口的状态,反而完全没有添加入口。原来的单元测试还把这条行为写成了断言(`signed-out 时不出现 pool 段`),所以改动前一直没人发现。
307
+ - **修法**:signed-out 支也构建并带上账号池视图(`servingAccountId` 传 `undefined`,`current` 自然全为 false)。「当前没有正在计费的账号」改由**每一行的 `current`** 表达,不再靠整段缺席来暗示。`QoderWebUsage` 的 signed-out 变体相应加上可选 `pool`。
308
+ - 顺手修掉一处**误导文案**:`poolNoCandidate`(「成员要么限流、要么积分耗尽……」)此前不判断有没有账号,空池也会显示,读起来像账号出了问题而不是还没添加。现在只在**真的有账号但都不可用**时才说,空池改为新的空状态 `row.poolEmptyHint`(中英双语,指向「添加 PAT」与「导出 / 导入账号」)。
309
+ - 新增 4 条测试:宿主侧「零账号仍然给出 pool 视图、且没有任何一行 `current`」与反向的「没有 pool dep 时不凭空造池」;客户端侧「零账号仍渲染账号池 + 空状态 + 添加账号入口,且不说 poolNoCandidate」与「有账号但都不可用才显示警告」。宿主侧那条做过变异测试(去掉 pool 即失败)。
310
+ - **客户端 bundle 的模块 id 必须等于包名 —— 改包名把插件整个搞挂过。** 包名从 `dsh-connect-qoder` 改成带 scope 的 `@dmh2002/dsh-connect-qoder` 后,插件在设置页报「启用失败 … failed to import」,渲染进程控制台的真实错误是:
311
+ `client-modules: duplicate factory registration for "dsh-connect-qoder" (bundle executed twice without invalidate?)`。
312
+ - **根因**:`tsdown.config.ts` 里 `PLUGIN_ID` 是**硬编码**的旧名,而 shell 的客户端模块花名册是按**包名**给每一行建键的(`resolveMeta` → `packageName`,取自 package.json 的 `name`);模块加载器又靠「bundle 声明的 id ↔ 花名册行的 id」配对来记录 revision 并在重建时 invalidate(`graphRows` / `reloadTargets` 都以它建键)。两者一旦不等,注册就永远挂不到自己那一行上、**永远不会被 invalidate**,bundle 第二次执行即抛 duplicate。
313
+ - **修法**:`PLUGIN_ID` 改为从 `package.json` 的 `name` **推导**(与 `PACKAGE_VERSION` 同源),从此改包名不可能再把 id 落下。已核对同 profile 里两个正常工作的 scoped 插件(`@linxin666/dsh-client-ui-git-graph`、`@changfenhuang/dsh-genui`)——它们的 bundle id 都等于自己的 scoped 包名,本项目此前是唯一的例外。
314
+ - 新增 `tests/plugin-id.spec.ts` 守卫(含一条「旧写法必然失败」的回归断言,并校验 `lib/client.js` 里真的声明了这个 id):单元测试此前完全看不见这个 bug(全绿),只有真机激活才暴露,所以用构建配置层的断言钉住它。
315
+ - 注意 `cordis.patch.yml` 的 entry `id`(`dsh-connect-qoder`)**故意保持不带 scope**:它承担设置命名空间与 `/plugins/<id>/…` 路由,改名会孤儿化老配置。那是**另一个**标识,不是这个模块 id。
316
+ - **另外还需要在 profile 侧修一处**(不随包发布,故不在本仓库改动范围内):本地是 `"dsh-connect-qoder": "link:…"`(unscoped key),`node_modules` 下只有 unscoped 软链;而 `cordis.patch.yml` 的 `name` 是 scoped 的,`@dmh2002/dsh-connect-qoder` 因此 `ERR_MODULE_NOT_FOUND`。需要把 profile 的依赖键与 `dsh.profile.bundles` 一并对齐成 scoped 名并重装(或改用插件管理器的「安装本地 bundle」重新登记),详见本次排障结论。
317
+ - **配置卡片正文整片消失(用户报「之前做的功能菜单都没了」)—— 槽位 key 同样按包名建,这是同一身份漂移的第二处。**
318
+ 改包名后,插件管理页里插件仍显示**运行中**、图标 / 描述 / 组件行都在,但**配置卡片正文什么都不渲染**(国内版/国际版 tab、模型管理、账号池、积分整片空白),而且**没有任何报错**。
319
+ - **根因**:DSH 的插件配置页把槽位按**包标识**建键 —— `plugins.bundle.config` 用**裸包名**,`plugins.row.config` 用 **`<包名>#<行 id>`**(行 id 就是 bundle patch 里声明的 entry `id`)。而 `src/client/index.tsx` 把这两个 key **硬编码**成旧的 unscoped 名(`dsh-connect-qoder` / `dsh-connect-qoder#dsh-connect-qoder`)。包名改成 `@dmh2002/dsh-connect-qoder` 后 key 对不上,槽位注册**静默失配** —— 它包在 try/catch 里,所以连 console 都不会红。
320
+ - **修法**:包名改为**构建期注入**(`__DSH_QODER_PACKAGE_NAME__`,与 `__DSH_QODER_VERSION__` 同源、都取自 package.json);新增 `src/client/slot-keys.ts`(无 React 依赖的纯函数 `qoderCardSlotKeys(packageName, rowId)`)供卡片注册使用;`vitest.config.ts` 镜像同一组 define,让测试看到真实包名。
321
+ - 新增 5 条测试(`tests/plugin-id.spec.ts`):key 格式、bundle key 等于真实包名、row key 等于 `<包名>#<entry id>`、**「旧硬编码 key 必然失配」的回归断言**,以及「构建产物真的用这组 key 注册」的产物断言。该产物断言在 `lib/` 不存在时**跳过而非抛错**(`lib/` 是 gitignore 的,且 `pnpm run check` 先跑测试、后跑构建)。
322
+ - 已做变异测试:把 `index.tsx` 改回硬编码旧 key,产物断言即失败。
323
+ - **账号转移的导出/导入往返彻底修好(双区域都坏过)**:`exportAccounts` 此前直接吐 vault 内部字段 `{ pat, accountName, id }`——**既不调用 `transferRecordOf`,也就没有 `domain` 字段,字段名也不是 snake_case**。而导入侧的区域门只认 `domain`,于是用户导出的文件再导入时**全判 skipped(导出即导入 = 0 条成功)**,功能等于不可用。
324
+ - 修法:导出改走 `transferRecordsFor(region, accounts)`(新增于 `src/transfer.ts`,`access_token` / `uid` / `nickname` / `domain` 四字段齐全,domain 由 `QODER_REGION_TRANSFER_DOMAIN` 按区域给出)。
325
+ - `QoderTransferDeps.exportAccounts` 的返回类型从「vault 形状」收紧为 `QoderTransferRecord[]`,**从类型上**挡住这种形状回退——原类型本身就是这个 bug 的共谋。
326
+ - `QODER_REGION_TRANSFER_DOMAIN` 由既有的 `knownQoderRegionOfDomain` **推导**而来(而不是手抄第二份域名表),两处不可能再漂移;漏登记某区域会在模块加载即抛错。
327
+ - **补了往返回归测试**(`tests/transfer.spec.ts` 的 `export → import round trip`):导出 → 原样喂回导入 → 断言 `imported === 导出条数`,cn 与 global 各一条,外加交叉导入必须跳过。
328
+ - 这个 bug 之前能存活,是因为两侧的单元测试各自都是绿的:`transferRecordOf` 单独测(对)、区域门单独测(对),但**没有任何测试把真实导出路径接到导入路径上**。往返测试就是为堵住这条接缝。
329
+ - **CLI 三条子命令从未真正跑起来**:`main()` 过滤 argv 的末项写成了 `REGION_KEYS.includes(arg)`(只保留 `cn` / `global`),于是**命令词自己被过滤掉**,`command` 恒为 `undefined`,`doctor` / `status` / `logout` 全部只会打印 usage 后退出。`src/bin.ts` 此前没有任何测试文件,所以一路无人发现。
330
+ - 修法:语义改正为「保留**不是** flag、**不是**区域名的实参」,并抽成纯函数 `commandArgOf()`(分发规则的唯一实现)。
331
+ - 新增 `tests/bin.spec.ts`:覆盖命令词、`--json` 任意位置、`--region` 及其实参、纯 flag 输入,以及一条**回归断言**(旧写法的等价实现对 `['doctor']` 返回 `undefined`,新实现返回 `'doctor'`)。
332
+ - 实测(`node lib/bin.js doctor --json`)现在真的输出两区域状态,其中 `global: vault=0` —— 这条输出也正是「国际版从未被执行」的直接证据。
333
+ - **国际版的用量/积分查询补发设备风控头**:`requiresDeviceRisk` 的语义是「该区域的 sash 请求都要带 `Cosy-MachineToken/Code/Type`」,但它只被签到路径(`sashRequest`)消费;用量走的 `jobTokenGet` 与直连回退 `fetchUsageDirect` 都只发 `accept` + `authorization`。已用注入 fetch 实测证实:修复前 global 的 `/sash/api/v2/me/usage` 请求**零个** machine 头。现在两条路径都按区域附加(国内版仍不带,不多发噪音)。
334
+ - 补 3 条回归测试:国际版用量带齐三件套、国内版不带、**回退路径也带**(迫使主路径 401 走回退后断言两次请求都带头)。
335
+ - 这三条都做了变异测试:单独还原任一处的修复,对应测试即失败。
336
+
337
+ ### Changed
338
+
339
+ - **插件图标改回 LaoDing 插件家族通用的 LD 品牌标**:`icons/dsh-connect-qoder-{128,64}.png` 与卡片头部内联 data URI 均与 dsh-connect-trae / dsh-connect-workbuddy 字节一致(家族约定:图标标识「同一家族的插件」,产品区分由标题与描述承载)。取代 0.2.0 的 Qoder 品牌紫白 Q 磁贴。
340
+ - `src/upstream.ts` 文件头注释此前称国际版的 models 走 `api2.qoder.sh`、jobToken 走 `center.qoder.sh`,而代码三个端点都指向 `api1.qoder.sh`。实测(2026-10)三者对本插件用到的端点**完全等效**(对同一假 PAT 都回 `401 personal token is invalid` 而非 `Signature invalid`),故统一为单网关主机并写进注释;将来若要按端点分流,应改 `QODER_REGIONS` 配置表而非只改注释。
341
+ - `src/index.ts` 的 `registration.cn.directory` 单句柄(一次 `registerConfigurableProviders` 登记两侧)补了说明注释,避免读成「global 漏了 directory 句柄」。
342
+ - **npm 包名改为 `@dmh2002/dsh-connect-qoder`** —— 无作用域的 `dsh-connect-qoder` 已被他人占用(发布者 `gghskl`,仓库 `hdhgsysh/dsh-connect-qoder`,实为 `eghrhegpe` 那份「读本机登录、零配置」的实现,已在注册表内),本项目的 PAT 路线发不到那个名字下。**插件在 DSH 内的标识不变**:`cordis.patch.yml` 的 entry `id`、设置命名空间、`/plugins/dsh-connect-qoder/…` 路由与 CLI 程序名仍是 `dsh-connect-qoder`,老用户升级后配置照旧生效;随包改变的只有 `name` 与安装命令。
343
+
344
+ ### Docs
345
+
346
+ - 新增 [`docs/PAT.md`](docs/PAT.md):PAT 是什么、从控制台哪里签发(国内版/国际版各自入口)、卡片粘贴与环境变量两条配置路径、优先级与常见问题;README 的 PAT 段落改为指向它并补上发证入口。`package.json` 的 `files` 由 `docs/assets` 改为 `docs`,使 `docs/*.md` 随包发布(此前 README 与包内的 DESIGN 文档链接在 npm 页面上是死链)。
347
+ - **README 双语全面重写并配图** —— 顶部改为设置卡片实机截图,功能特性区补入模型选择器、模型管理、composer 积分面板三张图;新增「账号池」章节(自动换号规则、那个开关到底控制什么、排序规则、两个批量动作)与「市场收录与展示」「已知限制」「免责声明」三节。同时修掉一批过时描述:Qoder **有**每日签到(此前写「没有」)、按钮叫「测试」而非「测一测」、积分逐账号显示在账号池里、倍率的 `(0.50x)` 形态与模型名示例。
348
+ - `screenshots.json` 与 `docs/assets/` 落地四张实机截图(设置卡片 / 模型管理 / composer 积分 / 模型选择器),供 GitHub 展示与插件市场审核读取。
349
+ - `THIRD_PARTY_NOTICES.md` 补 `L0NE-6/Qoder-Daily`(MIT)条目:每日签到的协议参照,并写明本项目独立实现的两点差异(逐账号「先查后领」的幂等、取不到设备身份时如实报错而不是静默降级)。
350
+ - `docs/PAT.md` 补「每日签到(实测协议)」一节,并修掉一处标题粘连导致的排版错误。
351
+ - `docs/DESIGN.md` 与 `src/*/` 头部注释里「Qoder 没有每日签到」的过时结论全部改正(`status-paths.ts` / `account-pool.ts` / `web-status.ts` / `client/AccountPool.tsx` / `client/locales.ts` / `client/QoderCard.tsx`)。
352
+ - **`RELEASING.md` 与 `package.json` 的包名一并对齐到作用域名**:验证命令、`npm view` / registry 直连核验、打包内容清单与暂存发布判据里的包名全部改为 `@dmh2002/dsh-connect-qoder`(registry 路径用 `%2f` 编码作用域斜杠),并新增一步「发布后核对 profile 侧的 scoped 依赖键」——本轮改名的两处静默故障(bundle 模块 id、配置槽位 key)都源于「包名改了但别处没跟上」,流程里必须留一道闸。
353
+ - 收录草稿 `awesome-dsh-plugin-submission/`:描述按现行能力重写(补多账号池 / 签到 / 自动换号),并记下与注册表内同名条目(`eghrhegpe/dsh-connect-qoder`,读本机登录路线)的差异,提 PR 时直接引用;同时改正两处过时前置说明(现行规则**没有**提交数门槛;npm 映射由 registry 自动采集,**条目里手写 `npm:` 会被拒**)。
354
+ - **命令行诊断的调用方式改写为实测结果**:README 双语原先写 `dsh plugin --profile desktop exec …`,本机跑不通的原因是 PATH 里的 `dsh` 包装脚本仍指向一个已不存在的 app 路径(与插件无关);改为主张「进入 profile 目录用 `pnpm exec dsh-connect-qoder …`」,并写明两条实测边界——裸路径 `node node_modules/dsh-connect-qoder/lib/bin.js` 在改名后必然 `MODULE_NOT_FOUND`(软链已搬到 `@dmh2002/` 下,缺的是入口文件不是 peer),以及 CLI 所需的宿主 peer(`dsh-atomic-write` / `dsh-home-paths`)由 pnpm 随包装进 profile、`auto-install-peers` 关掉时才会缺。「从 npm 装的副本在宿主之外跑 CLI 必然失败」这一原先写进 README 的结论**经实测不成立**,已删除;「已知限制」同步改写(英文版补上对应条目,此前中英不对称)。
355
+ - `THIRD_PARTY_NOTICES.md` 的图标一节改对了:0.3.0 起图标是**本作者原创**的家族 LD 标,不再随包分发任何第三方美术作品(0.2.0 曾用过 Qoder 客户端图标本体)。
356
+
357
+ ### Tests
358
+
359
+ - `tests/shim.spec.ts` 从 10 用例扩到 20 用例:非 loopback Origin 拒绝 / 回环 Origin 放行、请求体上限 413、`/healthz` 鉴权、首字节前失败回 JSON 且 code 翻译、流中失败仍走 SSE 错误帧、`tool_choice:"none"` 丢 tools、换号可见通知、耗尽时每个失败账号都记测量且消息注明尝试数、重复候选刹车、换号间隔生效、400 不换号。
360
+ - 新增首个**客户端真实渲染**测试 `tests/card-render.spec.tsx`(jsdom 挂载真组件,key-preserving 翻译):断言 tab 栏结构(圆点只在按钮内、激活/停用类落在按钮上)与模型行文案(displayName 与 `row.modelFree`,而不是键字面量)。本轮三个视觉缺陷全部来自「363 个 host 测试从不执行组件」这一盲区,此文件补上这一层。
361
+
362
+ ## 0.2.0 (2026-01-01)
363
+
364
+ 与同族插件 `dsh-connect-workbuddy`(参考实现)对齐外观与能力,同时贴合 Qoder 的 PAT 上游现实。
365
+
366
+ ### Features
367
+
368
+ - **多账号池(PAT)** —— 每区域一个 `$DSH_HOME/.qoder-accounts.<region>.json` 本地 vault(0600),叠加设置 PAT 与环境变量,三者按 settings → env → vault 去重合并;卡片可新增 / 移除 / 改名账号,也可显式选择某账号服务(`regions.<region>.accountId`,留空自动)。
369
+ - **账号池按实测排序与故障转移** —— 「测一测」批量对每个成员发真实请求,结果落 `$DSH_HOME/.qoder-pool.<region>.json`;聊天流在首字节前按 `rankPool`(可用性 → 额度从高到低 → 到期最近 → id)自动换账号,失败回写 `live-request` 探测记录。Qoder **没有每日签到**,故 `checkinSupported` 恒为 `false`、卡片不出现签到列。
370
+ - **账号转移(导入 / 导出)** —— 按区域导出 vault 账号为传输文件,预览后按索引导入;跨区域域名(`qoder.com.cn` / `qoder.sh`)正向匹配,不匹配的记录计入 `skipped`。
371
+ - **PAT 校验与增删** —— 新增 PAT 时经 Qoder `JobToken` 交换校验并取回显示名,失败即拒绝;移除只删 vault 条目,不碰设置 PAT / 环境变量。
372
+ - **上下文预算(context tiers)** —— 从上游 `context_config` 解析模型声明的档位,卡片按档位选择上下文预算,仅在已声明档位内生效(否则回落原生值)。
373
+ - **积分读数进 composer** —— `conversation.input.left` 插槽(order 4)按区域显示 `QD CN` / `QD AI` 与当前账号余额,受每区域 `showCreditsInMainUi`(默认开,opt-out)控制;每账号明细走 `account-credits` 路由。
374
+ - **CLI 增强** —— `doctor` 增报 pi-ai 代际、宿主心跳存活、vault 账号计数;`status` 增报凭据来源与当前剩余积分;新增 `logout [--region cn|global]` 清除本区域 vault 账号与账号池测量副本。
375
+ - **回环路由扩展** —— 新增 `usage`(每区域快照)、`account-credits`、`accounts add/remove`、`pool test/remove`、`transfer export/preview/import`、`__save`(两段式合并,防部分写入误删兄弟区域)。
376
+
377
+ ### Changed
378
+
379
+ - **客户端对齐同族 `dsm-*` 视觉语言** —— `dsm-qoder-*` 组件类 + 与 workbuddy 逐字一致的共享 `.dsm-plugin-card*` / `.dsm-btn*`;文案迁移到 `row.*` / `composer.*` 双语键;新增账号池表、账号转移面板、PAT 新增表单、composer 积分读数与忙碌遮罩等组件。
380
+ - **PI 代际兼容桥** —— 适配器按 `piAiRuntimeInfo()` 代际门控:legacy pi-ai 把 0.87 形态 transcript 折回 0.85 形态(system 消息与 tools 折叠到 `systemPrompt`/`tools`),modern 代际原样透传;Qoder 上游接受 user-first 会话,故不注入兜底 system prompt。
381
+ - **推理档位** —— Qoder 推理线上量是布尔 `model_config.is_reasoning`(非 effort 档),故 `qoderThinkingLevelMap` 只表达 `off`(仅 `canDisableThinking` 模型提供),各 effort 档位映射 `null`。
382
+ - **插件图标** —— 改用 Qoder 桌面版**应用图标本体**(`icon.icns` 解出的白底黑 Q),替换此前那张与深色卡片几乎融为一体的黑底磁贴;共享 `.dsm-plugin-card-icon`(32×32、radius 7px)与 workbuddy 逐字节一致。
383
+ - **探针结果文案统一** —— 抽出 `client/probe-label.ts` 作为**唯一**渲染规则:模型行与账号池表此前各写一套(耗时一处毫秒一处秒、恢复时刻一处只有时:分:秒一处是日期+时间),同一份测量两种说法;并补回缺失的耗时(`probeOkMs`)与上游给出的恢复时刻(`probeRateLimitedAt` / `probeOutOfCreditAt` + 相对时长)。
384
+
385
+ ### Fixed
386
+
387
+ - **图片输入的默认值不再采信平台标记** —— 上游 `is_vl` 是**平台**声明而非模型原生能力(内置目录里几乎全是 `true`,连纯文本模型也是),据此默认勾选会在纯文本模型上开出图片输入、一发图即 400。改为按精确 model id 键控的人工核对表(`native-modality.ts`),只有文档确认 `multimodal` 才预勾选,未核实一律不勾。
388
+ - **图片勾选改为独立持久集合** `regions.<region>.imageModelKeys`(原与 `lastCatalog` 快照耦合,目录重写可能连带改变图像答案),运行时只从该集合派生图像能力。
389
+ - **「可关闭思考」增加校正机制** —— 上游声明的 `canDisableThinking` 不可全信(workbuddy 实测有模型对 `reasoning_effort:"off"` 直接 400,且失败会在此后每次请求重演)。新增 `off-thinking.ts`:拒绝集 + 首读种子 + 运行时盖印。本插件暂无实测样本,拒绝集留空而非虚构(行为不倒退,有样本时加一行即可生效)。
390
+ - **脱敏缺陷** —— `redactUpstreamText` 原实现 `'$1[redacted]'` 把含密钥的捕获组原样保留,等于没有脱敏;改为 `key=` 保留键名抹值、裸 `pt-` token 整段替换。
391
+ - **账号删除改为二次确认** —— vault 是这些账号的**唯一**存储位置,硬删无法找回(workbuddy 的删除是可找回的「暂时隐藏」,故它不需要确认);两步内联确认替代一键删除。
392
+ - **探针结果按区域分别存放** —— 原为扁平命名空间,切换 tab 会串台;改为与 workbuddy 同构的按区域键控,刷新目录不再丢失已测结果。
393
+ - **账号池目标下拉补失效项** —— 已保存但已下架的 `targetModelId` 缺少自己的 option 时,受控 select 会退回显示第一项而值仍是旧 id(显示与取值分叉,下次保存又把旧 id 写回);补一个 disabled 的失效项。
394
+
395
+ ### Docs
396
+
397
+ - README / README.en.md(双语)更新为 0.2.0 的多账号池、转移、上下文预算与 composer 积分能力。
398
+ - 新增 [`docs/DESIGN.md`](docs/DESIGN.md):模块结构、凭据与账号池、以及从 workbuddy 继承的三条「已修缺陷」教训与取舍依据。
399
+ - 新增 `scripts/probe-cosy.mjs`(不依赖宿主直连 cosy 链路验证)与 `scripts/dump-vault.mjs`(只读 dump vault / 账号池测量),并加 `pnpm run probe:cosy` / `probe:vault` 入口。
400
+
401
+ ### Tests
402
+
403
+ - 测试覆盖从 9 个 spec / 82 用例扩到 **21 个 spec / 349 用例**:为此前零测试的 host 模块补齐规格(account-pool 三件套、accounts vault、credentials、transfer、probe、credential-recovery、web-status 全路由、adapter、native-modality、off-thinking,并扩展 catalog)。
404
+ - 测试基建对齐 workbuddy:per-file 隔离 `DSH_HOME`(`tests/setup-per-file-home.ts` + `vitest.config.ts` 的 env 隔离),避免 vault/账号池/心跳文件在用例间互相污染。
405
+
406
+ ## 0.1.0 (2026-01-01)
407
+
408
+ 首个公开发布版本。
409
+
410
+ ### Features
411
+
412
+ - **双区域并行接入** —— 国内版注册为 `qoder` provider(`gateway.qoder.com.cn`),国际版注册为 `qoder-global` provider(`api1.qoder.sh`);有 PAT 的区域其模型出现在 DSH 模型选择器里,两个 provider 可同时使用。
413
+ - **PAT 凭据** —— 设置卡片内粘贴(`regions.<region>.pat`)或环境变量(`QODER_PERSONAL_ACCESS_TOKEN` / `QODERCN_PERSONAL_ACCESS_TOKEN`),设置项优先;凭据只写入本地插件配置,从不进入网络请求。
414
+ - **模型目录与倍率** —— 从 cosy 网关实时拉取模型清单,价格因子以内嵌 `· xN.NN` 显示;逐项勾选启用(`enabledModelKeys`),保存立即生效;上游不可达时回退内置静态目录。
415
+ - **用量概览** —— 每区域只读展示基础额度 / 附加额度 / 组织额度的已用、总量、剩余、占比与到期时间。
416
+ - **模型可用性测试** —— 卡片内对单个模型「测一测」,直达上游确认可用性,限流 / 凭据无效 / 内容审核分别报因。
417
+ - **区域开关(opt-out)** —— 每个区域右侧勾选框,显式取消勾选即把该 provider 从选择器整体撤下;账号配置全部保留,重新勾选即恢复。
418
+ - **安全 loopback shim** —— 每区域一个随机端口 + 进程内随机 secret,真实 PAT 不交给 pi-ai。
419
+ - **命令行诊断** —— `dsh-connect-qoder doctor|status [--json]`,无需浏览器即可确认 PAT 来源、指纹派生与上游可达性。
420
+
421
+ ### Docs
422
+
423
+ - README / README.en.md(双语)、CHANGELOG、THIRD_PARTY_NOTICES、RELEASING(发布流程)。
424
+ - 收录草稿:`awesome-dsh-plugin-submission/`(Awesome DSH Plugin 注册表)。
425
+
426
+ ### 说明
427
+
428
+ - 首次发布按 `RELEASING.md` 流程执行;`npm publish` 若走暂存发布(staged publishing),需 2FA 批准后再核对 registry。
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 LaoDing (dingminhua)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.