@eddyskywalker/dsh-chatgpt-subscription 0.10.10 → 0.10.12

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 CHANGED
@@ -2,6 +2,37 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ - **修复所有订阅线路共享号池的读-改-写竞争**:原来只分别串行化读和写,令牌刷新或设置更新会把整份旧快照写回,覆盖其他请求的新令牌、账号与冷却状态——共享内核 `account-pool.ts` 的注释曾声称不会发生。现在账号增删、备注、主账号、轮换策略、冷却和认证标记都在同一锁内读取最新文档并修改;指向同一文件的多个号池实例共享锁与身份版本号。
6
+ - **刷新不占用整池锁**:模型请求、工具凭据、设置卡片与 Codex 强制续期共用按账号/凭据代际的单飞。读取当前凭据与登记刷新任务是一个短事务,网络请求在锁外发出,锁内不等待任何 flight(否则它自己的提交会死锁)。
7
+ - **条件写与过期保护**:轮换结果和外部权威凭据都按“发起时的凭据”写回,不匹配就返回最新行而不覆盖——晚到的刷新不会盖掉重新登录的新令牌,也不会复活已删除的账号;旧刷新抛出的终态失败也不会把刚登录的账号标记失效。刷新期间账号被删除时,模型请求会切到其他可用账号。
8
+ - **严格落盘与可选记账分离**:轮换出的新令牌必须写入并读回验证成功;最近使用时间等记账失败仍不阻断正常请求,且不会回滚期间发生的删除、重登录或主账号切换。主账号镜像按提交顺序更新,显式空池不再因镜像清理失败而从旧投影复活账号。
9
+ - **消除一次调用两次轮换**:`OAuthService` 原先先取凭据(可能已按到期刷新)再强制续期,会在几秒内连续消费两个刷新令牌;现在强制续期由号池一次完成。
10
+ - **迁移线路特有路径**:Claude 的身份索引、导入、合并与删除改用事务,凭据文档只在号池提交成功后同步(此前满池或落盘失败会留下孤儿凭据),非订阅凭据仍在事务内被拒绝;MiniMax 自愈与 WorkBuddy 桌面账号同步改用定向原子补丁与条件写。
11
+ - **确定性回归测试**:`test/account-pool-concurrency.test.ts` 覆盖跨实例并发添加与设置、不同账号交错刷新、同账号只轮换一次、刷新期间删除/重新登录、过时认证失败、镜像顺序与落盘失败,并在未修复代码上实测失败。协调范围是同一个宿主进程;不声称提供跨进程文件锁,也无法阻止外部应用自行轮换令牌。
12
+
13
+ - **Claude 线路审计修复(均有回归测试,且在修复前代码上实测失败)**:
14
+ - **按账号「重新登录」修不好那个账号**(`routes.ts` `loginStore`)。重新登录经 `pool.credentialStoreFor()` 只写进旧的单账号凭据文档,而请求路由读的是**号池行**;随后又清掉失效标记,于是该行带着**已失效的旧令牌**重新进入轮换,下一次请求再次失败——浏览器登录成功了,账号却立刻又要求重新登录。现在一律经 `pool.addAccount()` 写入(按账号身份就地更新同一行并同步文档),且只有新凭据确实落在用户点的那个账号上时才清除失效标记(浏览器里登成别的账号时,会新增那个账号,而不会把死行放回轮换)。
15
+ - **刷新令牌在「2xx 但响应体解析失败」后被重发**(`oauth.ts` `refreshAccessToken`)。重试循环只对 `ClaudeUnauthorizedError` 提前停止,而 `parseTokenResponse` 抛的是普通 Error;一次 200 意味着轮换型 refresh token 已被消费,重发同一个必得 `invalid_grant`,账号随即被判失效。现只重试 `ClaudeRetryableError`。
16
+ - **登录卡片轮询在 `exchanging` 状态永久停止**(`ClaudeSection.tsx`)。主机兑换授权码期间报告 `exchanging`,若某次轮询恰好读到它,轮询即停,卡片永远停在「正在交换令牌」,而主机早已登录完成。现在 `pending` 与 `exchanging` 都持续轮询。
17
+ - **仅含图片的工具结果发送空 text 块**(`mapper.ts`)。API 要求 text 块非空,截图 / read_image 这类结果会让该轮以及之后每一轮回放它的请求都失败。现在以 `(see attached image)` 占位(同参照实现),并丢弃结果中的空文本块。
18
+ - **出站文本未清除孤立代理项(unpaired surrogate)**。按字节截断的工具输出可能在 emoji 中间断开,`JSON.stringify` 会写出孤立的 `\uD83D` 转义,上游拒收,且因留在历史里而每轮复现。`sanitizeText` 现同时去除 NUL 与孤立代理项(同参照实现 `sanitizeSurrogates`),成对的 emoji 不受影响。
19
+ - **403 `permission_error` 被当成凭据失效**(`client.ts` `classifyFailure`)。官方定义它是「无权使用该资源」——例如选了订阅不含的模型——重新登录无法改变。原先会把正常账号标记失效、移出号池轮换,并提示用户重新登录。现归类为请求问题(`PROVIDER_ERROR`);没有类型化 body 的 403 仍按凭据失败处理(保守)。
20
+ - **模型列表只读第一页**(`client.ts` `performCatalogLoad`)。`GET /v1/models` 默认每页 20 条,而本线路把该列表当作选择器的权威;现发送文档允许的最大值 `limit=1000`。
21
+ - **`refusal` / `sensitive` 被当成正常结束,与 tool_use 同时出现时还会执行被拒那一轮的工具调用**;**`model_context_window_exceeded` 被当成正常结束**(官方:应视为截断)。现在前者报告为 `error`(附 `stop_details.explanation`),后者映射为 `max-tokens`。
22
+
23
+ - **修复 Claude 线路输出速度(TPS)与首字时间(TTFT)统计严重偏高**(用户报告)。DSH 的 TPS = `usage.outputTokens ÷(完成时刻 − 首个非空 delta 时刻)`,而 `outputTokens` 包含全部思考 token。`mapper.ts` 却把 `thinking_delta` **缓冲到 `content_block_stop` 才一次性发出**,于是首字时钟在思考**结束后**才开始:全部思考 token 被除以「回答阶段」的时间(例:思考 60 秒约 5000 token、回答 5 秒约 500 token → 显示约 1100 tok/s,实际约 85 tok/s;以工具调用结尾的步骤更离谱),TTFT 则吞掉整个思考阶段,界面在思考期间也一片空白。
24
+ - **修法**:思考块在 `content_block_start` 即发 `block-start`,每个 `thinking_delta` 到达即发 `reasoning-delta`;只有回放条目(需要最后才到的 signature)等到块结束写入。原注释给出的缓冲理由(「不能回放的思考不该让调用方看到」)与代码行为本就不符——无签名块一直照常展示。
25
+ - **顺带修正思考 token 数**:原先 `reasoningTokens` 用「返回文本长度 ÷ 4」估算,而 Claude 4+ 返回的是**摘要**思考,远少于实际计费的思考量。现改用服务端在最终 `message_delta` 给出的 `usage.output_tokens_details.thinking_tokens`(缺失时才回落到估算),并封顶于 `outputTokens`——DSH 会整条丢弃 `reasoningTokens > outputTokens` 的用量记录。
26
+ - **测试**:新增用例逐事件断言「delta 到达即发出」,并在 HEAD 版 mapper 上实测失败(`expected [] to deeply equal [ block-start … ]`);另有 `thinking_tokens` 优先与封顶两条。
27
+ - **修复 Claude Opus 5.5 在新账号上可能「一次前缀变化后每轮都 400」**。官方 preserved-thinking 文档列明 Fable 5.1、**Opus 5.5**、Sonnet 5.5 会做思考块前缀校验,2026-08-31 起创建的账号默认强制:`system` / `tools` / 更早消息一旦变化(DSH 压缩、工具列表变化、本插件的图片卸载),回放的思考块即 400,且重发同一请求永远失败,除非设 `block_binding.prefix_mismatch_behavior: 'drop_block'`。Opus 5.5 走 adaptive 分支从不发送它。现能力表新增 `bindsThinkingToPrefix`,adaptive 分支对这类模型附带 `block_binding`(配合上一条的 beta 头),**不**改变其 effort 语义(仍不强制 high)。
28
+
29
+ - **修复 Claude 线路无法使用 Claude Sonnet 5.5(`claude-sonnet-5-5`)**(用户报告:设置卡片上该模型显示「该模型不接受思考档位」)。
30
+ - **根因 ①:能力表没有这一行**。服务端 `GET /v1/models` 已列出它,但本地表里查不到,于是落到保守桩:无档位、不支持图片、200K 窗口、`thinkingMode: 'none'`,且**声称支持 temperature**——而官方文档写明该模型收到非默认 temperature 即 400。现新增为**本地策展行**(快照 `pi-ai@0.85.1` 早于该模型):1M 上下文 / 128K 输出 / 支持图片 / 档位 `low`–`max` / 不支持 temperature / 思考不可关闭(`disabled` 与 budget 形式均为 400)。取值来自官方 overview 与 what's-new 页面,并与更新的参照 `pi-ai@0.99.1` 逐项一致。
31
+ - **思考形态取 `mid-convo`**:官方默认档位是 `high`(与该分支未指定时强制的 `high` 一致,不会像 Opus 5.5 那样被静默抬档);且其思考块与会话前缀绑定——2026-08-31 之后创建的账号,前缀一变(system、tools、更早消息)回放就 400,除非请求带 `block_binding.prefix_mismatch_behavior: 'drop_block'`。`test/claude-model-catalog.test.ts` 的 mid-convo 名单相应加入该 id。
32
+ - **根因 ②:`mid-convo` 请求缺少授权 `block_binding` 的 beta**。官方文档明确:带 `block_binding` 却不带 `anthropic-beta: thinking-binding-controls-2026-08-01` 会返回 400 `block_binding: Extra inputs are not permitted`;参照实现对这类模型一直发送该 beta,本插件此前没有。现在请求头**从已构建的请求体读出**是否带 `block_binding`(`claudeBodyBindsThinking`),带则追加该 beta。这同时影响已有的 `claude-fable-5-1` 与 `claude-opus-5`。
33
+ - **申报版本 2.1.283 → 2.1.285**(npm `latest`)。Sonnet 5.5 由 Claude Code **2.1.284** 首次提供,而上游此前正是按「首次提供该模型的版本」为 Opus 5.5 设门槛;未观测到上游报错数字,因此**不记录** `minCliVersion`,只加一条测试锁「申报默认版本 ≥ 2.1.284」。若你按旧说明设了 `DSH_CLAUDE_CLI_VERSION=2.1.283`,请删除或改为 ≥ 2.1.284。
34
+ - **验证**:新增/修改的 10 条用例在**回退 `src/` 后全部失败**、恢复后通过;以 `pi-ai@0.85.1` 快照跑保真锁 206 条 Claude 用例全绿;`npm run typecheck` 0 错误;全量 1777 passed / 10 skipped,仅 `web-provider-lifecycle` 3 条失败——本机 DNS 把公网域名解析到 fake-IP,**干净工作树上同样失败**。未在真实订阅账号上端到端验证。
35
+
5
36
  - **新增 Codex 模型 GPT-6.1 Sol(`gpt-6.1-sol`)**。官方文档(`developers.openai.com/api/docs/models/gpt-6.1-sol`)明确它是 GPT-6 家族里「平衡速度、成本与智能」的一档:near-Astra 性能、更低价格,且在 **ChatGPT Work 与 Codex 中可用**。
6
37
  - **规格**(同一份官方文档):`reasoning.effort` 支持 `low` / `medium`(默认)/ `high` / `xhigh` / `max`,**不支持 `none` 与 `minimal`**;1,050,000 context window、128,000 max output tokens、输入模态 text + image、Apr 30 2026 知识截止。
7
38
  - **归入现有 GPT-6 profile**(`reasoningProfile: 'gpt-6'`):本插件早前的 GPT-6 档已经是不含 `none`/`minimal` 的 `low/medium/high/xhigh/max` 且默认 `medium`,与官方对 6.1 Sol 的描述**逐项一致**,因此直接复用该 profile 而不是新造一个;输出上限 128K 与 384K 起始上下文也沿用 GPT-6 家族常量,并进入 `DEFAULT_VISIBLE_CODEX_MODEL_IDS`(紧邻 Astra)。
package/README.md CHANGED
@@ -112,7 +112,7 @@
112
112
  - **一次授权只兑换一次**:浏览器回调与手动粘贴可能同时到达,用 compare-and-set 保证只有一方发起兑换(另一方得到「正在处理中」,已结算的流程得到 410 且**不做任何兑换**)。错误的 `state` **只拒绝那一个请求**,不会终止正在进行的合法登录。回调服务器只绑 loopback、只应答 `/callback`、只接受本机来源;
113
113
  - **刷新是单飞的**:并发请求共享同一次刷新。否则到期瞬间的一批请求会各自轮换刷新令牌,除第一个之外全部作废(上游会给出终局判定)。刷新令牌轮换后**先读回校验再落盘**;
114
114
  - **模型能力逐模型查表**(`src/host/claude/model-catalog.ts`),**不从模型名推断**。该表转录自本机随 harness 安装的参照目录,**只证明抄录忠实,不证明服务端提供这些模型**——服务端自己的 `GET /v1/models` 才是权威,且它**只覆盖上下文窗口**,能力字段不被改写;
115
- - **思考形态有四类,顺序决定成败**(`thinkingMode`):`mid-convo` → `{type:'adaptive', block_binding:{prefix_mismatch_behavior:'drop_block'}}` 外加 `output_config.effort`;`adaptive` → `{type:'adaptive'}`;`budget` → `{type:'enabled', budget_tokens}`(预算算术按参照实现转录,思考预算计入 `max_tokens`,**必须为回答留出至少 1024 token**);`none` → 不发思考字段。**`mid-convo` 排在最前且无条件**——`claude-fable-5-1` 与 `claude-opus-5` 同时带 `forceAdaptiveThinking`,把它们当成普通 `adaptive` 会**静默丢掉 `block_binding` 与 `output_config`**,而参照实现自己的注释写明该缺失会导致**持续 400**;
115
+ - **思考形态有四类,顺序决定成败**(`thinkingMode`):`mid-convo` → `{type:'adaptive', block_binding:{prefix_mismatch_behavior:'drop_block'}}` 外加 `output_config.effort`;`adaptive` → `{type:'adaptive'}`;`budget` → `{type:'enabled', budget_tokens}`(预算算术按参照实现转录,思考预算计入 `max_tokens`,**必须为回答留出至少 1024 token**);`none` → 不发思考字段。**`mid-convo` 排在最前且无条件**——`claude-fable-5-1` 与 `claude-opus-5` 同时带 `forceAdaptiveThinking`,把它们当成普通 `adaptive` 会**静默丢掉 `block_binding` 与 `output_config`**,而参照实现自己的注释写明该缺失会导致**持续 400**。反过来,**`block_binding` 本身必须由 `anthropic-beta: thinking-binding-controls-2026-08-01` 授权**(官方文档:缺该 beta 时返回 400 `block_binding: Extra inputs are not permitted`),因此请求头从**已构建的请求体**读出是否带 `block_binding`,带则追加该 beta;
116
116
  - **思考块带签名则原样回放**(这是思考模式下多轮工具调用的前提;`redacted_thinking` 同样回放),**无签名则丢弃**。工具名在出站时按 Claude Code 规范大小写归一化、入站时按大小写无关匹配回用户工具名;若两个工具归一化后碰撞,则**放弃归一化**原样发送,避免把结果投给错误的工具;
117
117
  - **多账号号池,与其它线路同源**:走共享内核 `AccountPoolCore` 并复用同一张设置卡片,具备顺序耗尽 / 轮询调度 / 粘性会话、429 冷却换号、账号级失效保留、设为主账号与备注。**账号身份是两层**:不可变的 `internalId` 是唯一路由键,`identityKeys`(uuid / email / 派生 seed)是**只增不换**的别名集,同一账号再次登录会**合并**而不是产生幽灵账号。**唯一例外的诚实说明**:当服务端既没返回 uuid 也没返回 email 时无法自动识别同一账号,卡片会把该账号标注出来并提供**手动合并**;
118
118
  - **可选择性收编本机已有的 Claude Code 登录(默认关闭)**:开启前只做一次「文件是否存在」的探测,**不读取内容**;只有你显式开启后才读取。收编得到的是一份**快照**,**永不由本插件刷新**——Claude Code 刷新的是同一枚轮换令牌,两个进程各自刷新会互相作废,而进程内的单飞解决不了跨进程竞态。快照过期后卡片会提示你**回 Claude Code 重新登录后再收编**,并且**绝不会写入或删除 Claude Code 的任何文件**;
@@ -474,17 +474,21 @@ DSH 设置页的「Subagent」卡片会把勾选的模型写成会话级的允
474
474
  档位的模型生效。**档位原样透传**(`ReasoningEffortId` 在 harness 里是无约束 brand,本仓库多条线路都直接使用
475
475
  `xhigh`),不做任何收敛,以免把用户选的档位静默降级。
476
476
 
477
- **模型表**共 15 条:14 条转录自本机随 harness 安装的参照目录,另加 1 条**本地新增**的 **Claude Opus 5.5**
478
- (`claude-opus-5-5`,官方 2026-09-22 发布,1M 上下文 / 128K 输出 / 支持图片,档位 `low`–`max`)。
477
+ **模型表**共 16 条:14 条转录自本机随 harness 安装的参照目录,另加 2 条**本地新增**:**Claude Opus 5.5**
478
+ (`claude-opus-5-5`,官方 2026-09-22 发布,1M 上下文 / 128K 输出 / 支持图片,档位 `low`–`max`),以及
479
+ **Claude Sonnet 5.5**(`claude-sonnet-5-5`,官方 2026-09-28 发布,规格同上;不支持 temperature、思考不可关闭;
480
+ 官方默认档位为 `high`,且思考块与会话前缀绑定,因此走 `mid-convo` 分支并带 `block_binding`)。以下说明针对 Opus 5.5。
479
481
  它有一条**与其它模型不同的硬约束**:**思考永远开启、不能关闭**——官方文档明确 `thinking:{type:"disabled"}`
480
482
  与 `{type:"enabled",budget_tokens:N}` **都会返回 400**,因此能力表把它标为不可关闭思考,档位表是控制思考深度的
481
483
  唯一手段。它的**默认档位是 `medium`**(其余带 effort 的模型默认 `high`),这一点被刻意保留:本仓库有两类模型走
482
484
  「强制 effort」分支,若把 Opus 5.5 一并归入,就会**静默把每次请求抬高一档、花更多钱**,因此它走的是普通 adaptive
483
- 分支。
485
+ 分支。但它(与 Fable 5.1、Sonnet 5.5 一样)是官方文档列明的**思考块前缀校验模型**:2026-08-31 之后创建的账号,一旦
486
+ 前缀变化(压缩、工具列表变化、图片卸载)回放思考块就会**每次都 400**,因此能力表用 `bindsThinkingToPrefix` 标记它,
487
+ adaptive 分支对这类模型额外发送 `block_binding: drop_block`(**不**强制 effort)。
484
488
 
485
489
  它还有一条**版本门槛**:上游要求申报的客户端版本 **≥ 2.1.280** 才为该模型提供服务。能力表用 `minCliVersion`
486
- 记录这一点(**只有这一行有门槛**,其余 14 行缺省——缺省表示「未知」而不是「无门槛」,不臆造数字)。插件申报的
487
- 默认版本为 **2.1.283**,并且**在发出请求之前就本地校验**「申报版本 vs 该模型门槛」,不满足时直接给出模型、当前
490
+ 记录这一点(**只有这一行有门槛**,其余 15 行缺省——缺省表示「未知」而不是「无门槛」,不臆造数字)。插件申报的
491
+ 默认版本为 **2.1.285**(不低于首次提供 Sonnet 5.5 的 Claude Code 2.1.284),并且**在发出请求之前就本地校验**「申报版本 vs 该模型门槛」,不满足时直接给出模型、当前
488
492
  版本与要求版本,**而不是白花一个往返让上游返回 400**。测试另有一条不变量:**申报的默认版本必须 ≥ 表中每个模型的
489
493
  门槛**——这条锁让「加了高门槛模型却忘了抬版本」无法通过 CI。
490
494
 
package/lib/client.js CHANGED
@@ -3951,7 +3951,7 @@ window.__ModuleLoader__.load({
3951
3951
  };
3952
3952
  }, [loadStatus]);
3953
3953
  (0, react.useEffect)(() => {
3954
- if (flow?.status !== "pending") return;
3954
+ if (flow?.status !== "pending" && flow?.status !== "exchanging") return;
3955
3955
  const timer = window.setInterval(() => {
3956
3956
  (async () => {
3957
3957
  try {