@eddyskywalker/dsh-chatgpt-subscription 0.13.2 → 0.14.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 CHANGED
@@ -2,6 +2,36 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.14.0 - 2026-10-08
6
+
7
+ - **[Claude] 支持 Claude Haiku 5.5(`claude-haiku-5-5`),并把申报的客户端版本抬到 2.1.293**
8
+ - **能力表新增一行**(`src/host/claude/model-catalog.ts`):官方 2026-10-07 发布,1M 上下文 / 128K 输出 / 支持图片,档位 `low`–`max`。**固定 id,没有日期后缀也没有别名**,所以不像 4.5 那样成对出现。
9
+ - **每个字段来自官方文档,不是从模型名推的**:这一行与邻居在三个字段上直接冲突——`claude-haiku-4-5` 是 `budget`、可关思考、支持 temperature,5.5 三条全反(自适应思考、档位控制深度、拒绝非默认采样参数)。能力表的地基就是「名字不决定任何字段」,照抄邻居就是这条地基要防的那种错。
10
+ - **不能用 `budget`**:官方迁移指南写明手动预算形式 `{type:"enabled",budget_tokens:N}` 在 5.5 上是 400。照抄 4.5 那一行会发出模型直接拒绝的请求。
11
+ - **也不进 `mid-convo` 分支**:官方默认档位是 `medium`,而 `mid-convo` 在调用方未指定档位时会强制 `effort: high`——等于每次请求都替用户多花一档、多付一档的钱。这与 Opus 5.5 是同一个坑、同一份理由,因此 Haiku 5.5 同样靠 `bindsThinkingToPrefix` 拿到 `block_binding`,而**不**接受强制 effort。测试里钉死 mid-convo 白名单的那条断言也没有收录它。
12
+ - **`canDisableThinking: true`——本表第一个为真的本地新增行**:官方明确 `thinking:{type:"disabled"}` 在 `low`/`medium`/`high` 被接受、在 `xhigh`/`max` 是 400。这条不对称能安全记成一个布尔值,靠的是本线路**发这个 form 的时机**:只有调用方明确要求关闭思考时才发,且此时**不申报任何档位**,请求因此跑在官方默认 `medium` 上——落在被接受的一侧;想用 `xhigh`/`max` 的人一定开着思考,走的是 adaptive form。反过来把「xhigh 以上 400」读成「不能关闭」才是错的——那会把低档位本来可用的开关一起拿走。
13
+ - **与号池相关的一条用户可见后果**:官方写明 Haiku 5.5 的思考块只在产出它的账号(或其关联账号)里有效,换账号回放会被**静默丢弃**——请求成功,模型看不到那段推理。多账号轮换因此表现为「同一段对话里答案质量忽高忽低」而**不报错**。这一条记在能力表的行注释与 README 里,因为它是号池的行为,不是模型的性质。
14
+ - **申报版本 2.1.285 → 2.1.293**:Haiku 5.5 首发于 Claude Code 2.1.293,而 `types.ts` 早就写着「默认版本必须 ≥ 首次提供每个模型的那个发行版」。这不是假设:加这一行时 npm 的 `stable` 标签**仍停在 2.1.285**(插件当时申报的值),`latest` 已经是 2.1.293——不抬版本的结果是照常广告一个每次请求都会被 `claude_code_version_too_old` 拒掉的模型,而本地预检看不出来。该行**没有** `minCliVersion`:没见过上游真的拒它,就不臆造门槛数字(Sonnet 5.5 也是这么处理的)。版本锁另加一条断言,把「≥ 2.1.293」写进测试而不只写进注释。
15
+ - **保真锁同步跟着加**:本机参照目录(pi-ai 0.87.1)里没有这个模型,所以它进 `LOCALLY_CURATED_MODEL_IDS`,并在 `test/claude-model-catalog.test.ts` 里逐字段断言官方文档的值;该文件的三处说明文字(「今天两行」「快照之外还差几行」「两行的理由」)一并改成三行,否则文档会比锁先过期。
16
+ - **新装默认勾选里的 Haiku 槽位换成 5.5**:默认列表每族只放一个当前代模型,所以 `claude-haiku-4-5` 被 `claude-haiku-5-5` 顶掉,长度不变。这是安装期的明确决策而不是静默追加:「未编辑过默认列表」按成员比对,老用户存的旧默认仍按原样生效,不会被这次改动悄悄改写。
17
+ - 验证:`npm run typecheck` 0 错误;`test/claude-model-catalog.test.ts` **26 passed / 0 failed**(本机装有参照目录,15 行转录保真断言与「快照之外恰为这两行」都真的跑了,不是 skip)、`test/claude-mapper.test.ts` 91、`test/claude-adapter.test.ts` 43、`test/claude-routes.test.ts` 21、`test/claude-token-store.test.ts` 45、`test/provider-output-reservation.test.ts` 181 全通过;全量 `npm test` **2826 passed / 11 failed / 7 skipped**,11 个失败全部是 `test/claude-oauth.test.ts` 的 `No bindable loopback port in the probe range.`——本机 bind `127.0.0.1` 直接 `EACCES`(已单独探测确认),与本次改动无关。
18
+ - **[Codex] 在 ChatGPT 卡片里导入本机 Codex CLI 的登录(可选)**
19
+ - **重复登录是纯粹的摩擦**:已经在官方 CLI 登录过的用户,登录的正是这个 Provider 服务的那份订阅,却还要再走一次完整的 OAuth。新增 `POST /adopt`,让它点一次就少一整轮登录。
20
+ - **导入得到的是快照,本插件永不再刷新它**:ChatGPT 的 refresh token 会轮换,而 CLI 会在**原地刷新自己的文件**——两个进程刷同一份授权会互相作废,输的一方握着服务端已经作废的令牌,结果是用户**被本插件的好意踢出 Codex CLI**。进程内的单飞解决不了,因为竞态在**进程之间**:两个进程唯一共享的就是那个文件,而按只读规则本插件不能写它。因此快照只在 CLI 还为它作保时可用,**过期是收编这件事的一部分而不是缺陷**。
21
+ - **只读是结构性的,不是承诺**:`codex-adopt.ts` 只按名字从 `node:fs/promises` 导入 `readFile` 与 `stat`,加一次写操作必须先改这行 import(而 import 是评审第一眼看的地方);测试断言一次读取前后凭据文件的**字节、大小与 mtime 完全不变**。
22
+ - **存在性与内容是两个函数**:`codexCliCredentialPresence()` 只 `stat`,开机即可跑;`readCodexCliCredentials()` 只在你点了导入之后才调用。合成一个「顺手先读一下以防万一」的启动路径,正是会把 opt-in 变成装饰的那种做法。
23
+ - **每条线路只在自己的页面里回答自己的问题**:一开始把这三处做成了总览页上一次跨线路的统一只读扫描(一条统一扫描路由加一个扫描模块),结果那一块替 Claude Code 与 MiniMax Code 回答了它们自己已经回答过的问题,只能挂一句「该供应商在自己的设置页里已有导入入口,请到那里导入」——而用户就在离那一页一步之遥的地方。现在没有跨线路扫描了:Codex CLI 的答案由 `GET /status` 上的 `codexCliSignInAvailable` 给出,Claude 与 MiniMax 继续用各自的既有读取器与各自的导入入口。**「文件在哪」只有一份答案,因为只有各自的读取器拥有它。**
24
+ - **宿主顺带报出它 stat 过的那个路径**:`GET /status` 多带一个 `codexCliSignInPath`(`codexCliCredentialPaths()` 的唯一候选,纯函数、不碰文件系统),有了它「没找到」才可追问「你到底找哪儿了」。**卡片本身不显示它**——卡片上除了按钮不加文字,本条只保证答案在接口上随时可取。
25
+ - **「已检测到」不等于可用**:判断可用必须读取文件,那正是 opt-in 之后才做的事。未识别的形状(文件不存在、不是 JSON、`auth_mode` 不是 `chatgpt`、`tokens` 不是对象、缺 access/refresh token、access token 不是带可用 `exp` 的 JWT)一律给出 `undefined` 而不是异常,卡片报「格式无法识别」;邮箱与套餐来自 id token 自己的声明,没有就不填。`exp` 由秒换算成毫秒——错向时是静默的,而错向另一边会让一枚凭据看起来几个世纪都不过期。
26
+ - **不猜 Kimi Code**:本机 Kimi Code 的凭据文件没有区域 / `oauthHost` / `baseUrl`,插件宁可拒绝也不猜,因此该行不出现,而不是显示成一个「装上就能收编」的承诺。
27
+ - **卡片与号池共同钉住「只借 access token」**:`codexNeedsRefresh` 对 adopted 凭据**恒答否**,刷新钩子本身也直接拒绝(双重挡住是为了让将来漏掉前者的调用者大声失败,而不是悄悄花掉一枚不属于本插件的授权);过期即退出调度且**不产生任何写入**,摘要标 `removable: false`,删除只能走显式的 `removeImportedAccount`。池文件往返保存该标记,于是重启后也不会把快照当成自持凭据。
28
+ - **导入只是多一行**:不触碰当前存储的登录态;去重键 `accountId ?? email` 让同一账号重复导入时**原地更新**而不是产生重复账号。
29
+ - 客户端只多一个按钮,且只在 **ChatGPT 卡片**里:账号池卡片头部那行「登录」**旁边**的「导入本机 Codex CLI 登录」,用共享卡片的 `renderLoginActions` 插槽,和 Claude 页面那个收编按钮同一套布局——登录保留主按钮样式,导入是次要的那个。这个按钮**不受检测结果影响,永远都在**:一个会自己消失的按钮回答不了「我本机到底登录了没有」,而没有登录时点它也并非无声无息,宿主会回一句明说缘由的错误,并显示在该卡片已有的错误条里。**卡片上不添加任何说明文字**:按钮已经说明了自己的动作,而「有没有」「找到了吗」「去哪儿找的」三段散文正是用户要求移除的「无关文字」(Claude 页面上同样的三段一并移除)。
30
+ - **导入必须能撤销,而撤销的形状由路由决定**:`/adopt/disable` 刻意不接受 `accountId`(池对 adopted 行的删除拒绝是一条**安全**规则,逐条撤销的入口一旦能抵达自持登录就会打破它),于是撤销做成了 ChatGPT 账号池卡片下方的一个**池级**按钮「停止导入全部本机登录」,文案与提示都写明「全部」。导入行被标为 `removable: false`,共享卡片据此隐去 Delete;没有这个按钮时导入就是**单向门**,只能手改 pool 文件才能收回——一个只进不出的功能不算做完。
31
+ - 路由:`POST /adopt`(body `{source:'codex'}`,未知来源返回一句指向真正拥有它的供应商的话,而不是静默无操作)、`POST /adopt/disable`;`GET /status` 另带 `codexCliSignInAvailable` 与 `codexCliSignInPath`。
32
+ - 测试:新增 `test/codex-adopt.test.ts`(19 例)与 `test/codex-local-signin-ui.test.tsx`(6 例),并扩充 `test/codex-account-pool.test.ts`(27 例)与 `test/routes.test.ts`(10 例)。
33
+ - 验证:`npm run typecheck` 与 `npm run build` 均 0 错误;`test/client-registration.test.ts` **16 passed / 0 failed**、`test/codex-import-undo-ui.test.tsx` 6、`test/codex-local-signin-ui.test.tsx` 6、`test/routes.test.ts` 10、`test/codex-account-pool.test.ts` 27、`test/codex-adopt.test.ts` 19 全通过;全量 `npm test` **2823 passed / 11 failed / 7 skipped**,11 个失败全部是 `test/claude-oauth.test.ts` 的 `No bindable loopback port in the probe range.`,在**未改动的工作树上同样复现**,与本功能无关。
34
+
5
35
  ## 0.13.2 - 2026-10-08
6
36
 
7
37
  - **[Kimi Code] 读图请求按「实际发送体积」计量,超限时回收而非直接拒绝**
package/README.md CHANGED
@@ -17,6 +17,7 @@
17
17
  - [Claude(订阅)线路](#claude订阅线路)
18
18
  - [子代理模型授权](#子代理模型授权0215-起032-起强制指定模型)
19
19
  - [随包分发的 Agent Preset](#随包分发的-agent-preset)
20
+ - [本机登录](#本机登录)
20
21
  - [安全边界](#安全边界)
21
22
  - [插件路由](#插件路由)
22
23
  - [开发与验证](#开发与验证)
@@ -28,7 +29,7 @@
28
29
 
29
30
  | Provider ID | 订阅 | 协议 | 登录方式 | 模型目录来源 |
30
31
  | --- | --- | --- | --- | --- |
31
- | `codex-chatgpt` | ChatGPT(Plus / Pro / Business…) | Responses | 浏览器 OAuth(localhost:1455 回调) | **实时** `/backend-api/codex/models` |
32
+ | `codex-chatgpt` | ChatGPT(Plus / Pro / Business…) | Responses | 浏览器 OAuth(localhost:1455 回调),或导入本机 Codex CLI 登录 | **实时** `/backend-api/codex/models` |
32
33
  | `kimi-code` | Kimi 会员 | Anthropic Messages | **设备码**(RFC 8628) | 实时 `/v1/models` + 本地兜底 |
33
34
  | `command-code` | Command Code | Anthropic / OpenAI 双轨 | 浏览器 OAuth 或粘贴 API Key | 实时 `/provider/v1/models` |
34
35
  | `workbuddy-subscription` | 腾讯 WorkBuddy / CodeBuddy | OpenAI 兼容(仅流式) | 扫描桌面端凭据或官方浏览器授权 | 实时 `/v3/config` |
@@ -49,6 +50,7 @@
49
50
  - 支持 token 刷新、登录取消与账号注销;
50
51
  - Windows 使用 CurrentUser DPAPI 加密存储 token;Linux 使用当前用户独占的 `0600` 文件存储;明文不发送给 Client;
51
52
  - 设置页会明确显示当前存储类型,并在 Linux 上提示文件存储未额外加密。
53
+ - **本机已经登录过 Codex CLI 的用户可以不再走一次 OAuth**(详见「本机登录」):**ChatGPT 卡片**的账号池里,「登录」按钮**旁边**还有一个「导入本机 Codex CLI 登录」按钮——两者是同一件事的两种做法,布局与 Claude 页面上那个收编按钮一致。它**只看凭据文件是否存在、从不读取令牌**,且要你显式点那个导入按钮之后才会读取。导入得到的是一份**快照**,**永不由本插件刷新**——ChatGPT 的 refresh token 会轮换,而 CLI 会在原地刷新自己的文件,两个进程各自刷新会互相作废,最后是你被本插件的好意踢出 Codex CLI,而进程内的单飞解决不了这个**跨进程**竞态。快照过期后请**回 Codex CLI 重新登录后再导入**;本插件**绝不写入或删除 Codex CLI 的任何文件**。
52
54
 
53
55
  ### 模型接入
54
56
 
@@ -363,6 +365,8 @@ DSH 模型选择器应显示 **“Codex(ChatGPT 订阅)”**。GPT-6 系列
363
365
 
364
366
  **设置 → Codex 订阅 → 网络代理** 同时控制 GPT 与 Antigravity(Gemini)的 Host 请求,可选择系统代理(自动检测)、自定义代理或直连。Gemini 模型生成、网页登录后的令牌交换、令牌刷新、账号信息、项目发现、配额与模型目录查询均使用此设置;修改后对后续请求生效,无需重启 DSH。浏览器中的 Google 授权页面使用浏览器自己的网络设置。
365
367
 
368
+ 已经用官方 Codex CLI 登录过的用户可以跳过第 3 步的 OAuth:**设置 → 订阅服务 → ChatGPT**,在账号池卡片里点「登录」**旁边**的「导入本机 Codex CLI 登录」即可(见「本机登录」)。
369
+
366
370
  ### Command Code
367
371
 
368
372
  1. 重启 `dsh web`;
@@ -653,10 +657,12 @@ DSH 设置页的「Subagent」卡片会把勾选的模型写成会话级的允
653
657
  档位的模型生效。**档位原样透传**(`ReasoningEffortId` 在 harness 里是无约束 brand,本仓库多条线路都直接使用
654
658
  `xhigh`),不做任何收敛,以免把用户选的档位静默降级。
655
659
 
656
- **模型表**共 16 条:14 条转录自本机随 harness 安装的参照目录,另加 2 条**本地新增**:**Claude Opus 5.5**
657
- (`claude-opus-5-5`,官方 2026-09-22 发布,1M 上下文 / 128K 输出 / 支持图片,档位 `low`–`max`),以及
658
- **Claude Sonnet 5.5**(`claude-sonnet-5-5`,官方 2026-09-28 发布,规格同上;不支持 temperature、思考不可关闭;
659
- 官方默认档位为 `high`,且思考块与会话前缀绑定,因此走 `mid-convo` 分支并带 `block_binding`)。以下说明针对 Opus 5.5。
660
+ **模型表**共 17 条:15 条转录自本机随 harness 安装的参照目录(**Claude Opus 5.5** 已在参照目录中,只是
661
+ `thinkingMode` 一项按官方文档取值,见下),另加 2 条**本地新增**:**Claude Sonnet 5.5**
662
+ (`claude-sonnet-5-5`,官方 2026-09-28 发布,1M 上下文 / 128K 输出 / 支持图片;不支持 temperature、思考不可关闭;
663
+ 官方默认档位为 `high`,且思考块与会话前缀绑定,因此走 `mid-convo` 分支并带 `block_binding`),以及
664
+ **Claude Haiku 5.5**(`claude-haiku-5-5`,官方 2026-10-07 发布,规格同上;**固定 id,无日期后缀也没有别名**)。
665
+ 以下说明针对 Opus 5.5。
660
666
  它有一条**与其它模型不同的硬约束**:**思考永远开启、不能关闭**——官方文档明确 `thinking:{type:"disabled"}`
661
667
  与 `{type:"enabled",budget_tokens:N}` **都会返回 400**,因此能力表把它标为不可关闭思考,档位表是控制思考深度的
662
668
  唯一手段。它的**默认档位是 `medium`**(其余带 effort 的模型默认 `high`),这一点被刻意保留:本仓库有两类模型走
@@ -666,11 +672,26 @@ DSH 设置页的「Subagent」卡片会把勾选的模型写成会话级的允
666
672
  adaptive 分支对这类模型额外发送 `block_binding: drop_block`(**不**强制 effort)。
667
673
 
668
674
  它还有一条**版本门槛**:上游要求申报的客户端版本 **≥ 2.1.280** 才为该模型提供服务。能力表用 `minCliVersion`
669
- 记录这一点(**只有这一行有门槛**,其余 15 行缺省——缺省表示「未知」而不是「无门槛」,不臆造数字)。插件申报的
670
- 默认版本为 **2.1.285**(不低于首次提供 Sonnet 5.5 的 Claude Code 2.1.284),并且**在发出请求之前就本地校验**「申报版本 vs 该模型门槛」,不满足时直接给出模型、当前
675
+ 记录这一点(**只有这一行有门槛**,其余 16 行缺省——缺省表示「未知」而不是「无门槛」,不臆造数字)。插件申报的
676
+ 默认版本为 **2.1.293**(不低于首次提供 Sonnet 5.5 的 Claude Code 2.1.284,也不低于首次提供 **Haiku 5.5 的 2.1.293**——
677
+ 加这一行时 npm 的 `stable` 标签仍停在 2.1.285,所以这里必须读 `latest` 而不是 `stable`),并且**在发出请求之前就本地校验**「申报版本 vs 该模型门槛」,不满足时直接给出模型、当前
671
678
  版本与要求版本,**而不是白花一个往返让上游返回 400**。测试另有一条不变量:**申报的默认版本必须 ≥ 表中每个模型的
672
679
  门槛**——这条锁让「加了高门槛模型却忘了抬版本」无法通过 CI。
673
680
 
681
+ **Claude Haiku 5.5** 有三条与上面两个 5.5 不同的地方,都是照抄官方文档、不是从模型名推的:
682
+
683
+ - **它可以关闭思考,但只在低档位**。官方明确 `thinking:{type:"disabled"}` 在 `low`/`medium`/`high` 被接受,在
684
+ `xhigh`/`max` 是 400——因此能力表把它标为**可关闭**(`canDisableThinking: true`),这是本表里唯一一个为真的
685
+ 本地新增行。这条线路只在调用方明确要求「关闭思考」时才发这个 form,且此时**不申报任何档位**,请求因此跑在
686
+ 官方默认 `medium` 上——正落在被接受的一侧;而想用 `xhigh`/`max` 的人一定开着思考,走的是 adaptive form。
687
+ - **它不能沿用 Haiku 4.5 的 `budget` 表**。官方迁移指南写明手动预算形式
688
+ `{type:"enabled",budget_tokens:N}` 在 5.5 上是 400,所以这一行改走 `adaptive`;又因为官方默认档位是
689
+ `medium`(与 Opus 5.5 同列),它同样**不进 `mid-convo` 分支**——那一档会强制 `effort: high`,等于每次请求
690
+ 都替用户多花一档。它同样是官方文档列明的**思考块前缀校验模型**,所以 `bindsThinkingToPrefix` 为真。
691
+ - **它的思考块只在产出它的账号里有效**。官方写明换账号回放会被**静默丢弃**(请求成功,模型看不到那段推理)。
692
+ Claude 号池会在多个账号之间轮换,所以长会话中途换号会表现为「同一段对话里答案质量忽高忽低」而**不报错**——
693
+ 这是换号的行为,不是模型不稳定。另外 5.5 与 4.7 之后的模型用同一个新 tokenizer,同样的文本约多 30% token。
694
+
674
695
  ### 收编本机已有的 Claude Code 登录(可选,默认关闭)
675
696
 
676
697
  如果你本机已经登录过 Claude Code,可以点「收编本机登录」把它作为一条账号导入,省去再次登录。
@@ -735,6 +756,47 @@ access token / refresh token / 授权码 / PKCE verifier**;诊断里提到某
735
756
  **SHA-256 指纹前缀**(`sha256:.../len:...`),**不是**令牌的任何一段原文——早前版本会打印前 6 个字符,
736
757
  那本身就是一次凭据泄露,因为同一个字符串会被渲染到设置卡片并写进 Host 日志(有测试锁定)。
737
758
 
759
+ ## 本机登录
760
+
761
+ **每条线路只在自己的详情页里回答关于本机的问题**。ChatGPT(Codex)线路的 ChatGPT 卡片里,「登录」按钮**旁边**就是「导入本机 Codex CLI 登录」——与 Claude 页面上那个收编按钮同样的位置、同样的次要样式;点它可以省掉再走一次 ChatGPT 授权。Claude Code 与 MiniMax Code 各自的设置页里本来就各有一个导入入口,不在这里重复。
762
+
763
+ ### 只看文件在不在,从不读令牌
764
+
765
+ 打开 ChatGPT 卡片时,宿主用一次 `stat` 回答这件事,**没有任何文件被打开读取**:
766
+
767
+ | 线路 | 查过的路径 | 导入入口在哪 |
768
+ | --- | --- | --- |
769
+ | Codex CLI | `$CODEX_HOME/auth.json`,未设置该变量时为 `~/.codex/auth.json` | ChatGPT 卡片账号池里「登录」旁边的那个按钮 |
770
+ | Claude Code | `~/.claude/.credentials.json`(目录可用 `CLAUDE_CONFIG_DIR` 改,见「Claude(订阅)线路」) | Claude 线路自己的设置页 |
771
+ | MiniMax Code | `~/.minimax/auth/<buildEnv>/<region>/mcode-public/auth.json`,国区与国际区各一(见「MiniMax Code(编程订阅)线路」) | 不需要按钮:它的令牌库直接采用本机那份凭据 |
772
+
773
+ - **为什么不放在总览页**:总览页上一块跨线路的扫描,替别的线路回答问题,最后只能写一句「该供应商在自己的设置页里已有导入入口,请到那里导入」——而用户就在离那一页一步之遥的地方,那句话既答错了地方,本身也自相矛盾。信号归各自的线路,位置也就归各自的线路。
774
+ - **Kimi Code 不在这里**:本机 Kimi Code 的凭据文件**不带**本插件需要的区域、`oauthHost` 与 `baseUrl`,而插件**拒绝猜**这三个值,因此既不扫描也不显示——那一行若是显示成「未检测到」,看起来就像装个插件就能收编。
775
+
776
+ 理由不是谨慎,而是这一块的位置:它要渲染的是一份**邀请**(一个可以点的按钮),而此时你还没有同意任何事。一个会读文件的扫描器会让后面那个 opt-in 变成装饰,同时为一页只需要「有 / 没有」的信息在内存里握着别家订阅的令牌。所以这一块**不显示**任何账号、套餐、邮箱或到期时间:这些答案要导入之后才存在,导入前显示它们等于提前读取。
777
+
778
+ 因此**「已检测到」只表示那个文件在**,不表示它可用——判断可用必须读取文件,而那是 opt-in 之后的事。文件在却形状不认识时(不是 JSON、`auth_mode` 不是 `chatgpt`、`tokens` 不是对象、缺 access 或 refresh token、access token 不是一个带可用 `exp` 的 JWT),导入会明确报出「格式无法识别」而不是抛错或静默成功,也不会把默认值当成事实;邮箱与套餐则来自 id token 自己的声明,没有就不填。查过的路径**两种状态都写出来**,包括没找到的时候——「你到底去哪儿找了」问的往往正是它,而一个只说「没找到」却不给路径的答案没法据此行动。
779
+
780
+ ### 导入 Codex CLI 登录(可选)
781
+
782
+ 在 ChatGPT 卡片账号池里点「登录」**旁边**的「导入本机 Codex CLI 登录」。三点与 Claude Code 的收编同源:
783
+
784
+ - 导入的是**快照**,**本插件永不再刷新它**;
785
+ - 快照过期后请**回 Codex CLI 重新登录、再导入一次**,本插件里没有这条登录路径;
786
+ - 本插件**绝不写入、不创建、不移动、不删除** Codex CLI 的任何文件。
787
+
788
+ 前两条的理由值得写全,因为这是整个功能最容易被误解的地方:ChatGPT 的 refresh token 会轮换,而 CLI 会在**原地刷新自己的文件**。两个进程刷同一份授权会互相作废,输的一方握着服务端已经作废的令牌,结果是**你被本插件的好意踢出 Codex CLI**。进程内的单飞解决不了它——这是**跨进程**竞态,两个进程之间唯一共享的就是那个文件,而按上一条本插件不能写它。所以这份快照只在 CLI 自己还为它作保的期间可用,**过期是收编这件事的一部分,而不是一个待修的缺陷**。
789
+
790
+ 「绝不写入」是**结构性**的保证而不是承诺:`codex-adopt.ts` 只按名字从 `node:fs/promises` 导入 `readFile` 与 `stat`,加一次写操作必须先改这行 import,而 import 正是评审第一眼看的地方;测试还断言一次读取前后该凭据文件的**字节、大小与 mtime 完全不变**。
791
+
792
+ ### 导入之后
793
+
794
+ - **只是号池里多一行**,不替换你当前的登录:已存储的登录态原样保留,两者在同一个池里参与调度;
795
+ - **同一个账号导入两次会原地更新**,不会多出一个重复账号:号池的去重键是 `accountId ?? email`;
796
+ - **导入的行与插件自持的登录语义不同**:它**永不被刷新**——`needsRefresh` 钩子对 adopted 凭据恒答否,刷新钩子本身也直接拒绝,两者都挡住是为了让将来漏掉前者的调用者大声失败而不是悄悄花掉一枚不属于本插件的授权;过期即**退出调度**且**不产生任何写入**,摘要标为**不可删除**,删除只能走显式的 `removeImportedAccount`(`POST /adopt/disable` 清掉插件这边记下的快照)。因此你在本插件里自己签入的账号不会被这个动作顺手清掉,而 CLI 自己的文件更不会被删除或改写;
797
+ - **过期不是失败**:卡片与号池会说明这份快照已过期、该回 Codex CLI 里重新登录后再导入一次;这一行随之退出调度,而不是拿一枚作废的令牌去试一次请求。
798
+ - **导入可以撤销,但撤销是「全部」而非「某一行」**:借来的凭据在账号池里被标为**不可删除**(这正是「过期不产生任何写入」与「插件自持的登录不会被顺手清掉」两条规则的代价),因此删除入口不在每一行上,而在 ChatGPT 标签页账号池卡片下方的一个按钮:**停止导入全部本机登录**。点它走 `POST /adopt/disable`,清掉插件这边记下的**所有**导入快照;你在本插件里自己签入的账号不在其中,**Codex CLI 自己的登录文件也不会被删除或改写**——这一步只是让本插件忘记它拷贝的那一份。按池级语义设计而不是行级,是因为该路由刻意不接受 `accountId`:一个摆在某一行上、却会连带清空邻居的按钮,比没有按钮更糟,所以按钮文案与提示里都写明「全部」,将来若改成逐行语义能被看见。
799
+
738
800
  ## 安全边界
739
801
 
740
802
  Antigravity 的 access token / refresh token 使用独立的系统凭据存储:Windows 使用 CurrentUser DPAPI(`$DSH_HOME/storages/antigravity-oauth.json.dpapi`),macOS 使用登录钥匙串,Linux 使用 Secret Service。macOS / Linux 的服务名为 `dsh-antigravity`,账号键按旧凭据文件的绝对路径生成,隔离不同的 `DSH_HOME`。
@@ -747,6 +809,8 @@ Antigravity 的 access token / refresh token 使用独立的系统凭据存储
747
809
 
748
810
  **收编(adopt)是只读的**:本插件从不创建、修改、移动或删除 Claude Code 的任何文件;它只读取。收编得到的快照**永不由本插件刷新**(原因见上文)。
749
811
 
812
+ **Codex CLI 的导入(adopt)同样是只读的**:本插件从不创建、修改、移动或删除 Codex CLI 的任何文件。存在性与内容是两个函数——`/status` 里的 `codexCliSignInAvailable` 由一次 `stat` 回答,**在你点击导入之前没有任何令牌被读取**,而这一个字段也不含任何凭据内容(旁边的 `codexCliSignInPath` 只是被查看的那个文件路径)。导入得到的快照**永不由本插件刷新**;过期后该行退出调度,且**不产生任何写入**(原因见「本机登录」)。
813
+
750
814
  macOS 钥匙串服务名为 `Claude Code-credentials`(**未核实**,本仓库不读取它:一份第三方指南与真实客户端对该名称说法不一致,因此它被明确列为非目标)。
751
815
 
752
816
  升级后首次访问 Antigravity 凭据时,会读取旧 `storages/antigravity-oauth.json`,加密保存并读回校验;成功后删除旧 JSON,通常无需重新登录。失败会保留旧文件并报告错误,不会回退到明文存储。注销同时清理旧文件和新凭据。Linux 需要 `secret-tool`(libsecret 工具包)及可用、已解锁的 Secret Service 钥匙环;无桌面服务的主机也需要配置该服务。系统凭据存储保护落盘数据,不防御当前用户下已获权限的进程。
@@ -772,7 +836,7 @@ Antigravity 的 Gemini 用量以流结束时的上游累计计数为准,缓存
772
836
 
773
837
  | 方法 | 路径 | 说明 |
774
838
  | --- | --- | --- |
775
- | GET | `/status` | 查询账号、连接状态与额度 |
839
+ | GET | `/status` | 查询账号、连接状态与额度;另带 `codexCliSignInAvailable`(本机是否有可导入的 Codex CLI 登录,**只 `stat` 不读取**)与 `codexCliSignInPath`(为此查看过的那个文件路径) |
776
840
  | POST | `/login/start` | 开始 OAuth 登录 |
777
841
  | GET | `/login/events?loginId=...` | SSE 订阅登录进度 |
778
842
  | POST | `/login/cancel` | 取消登录任务 |
@@ -781,6 +845,8 @@ Antigravity 的 Gemini 用量以流结束时的上游累计计数为准,缓存
781
845
  | POST | `/quota/refresh` | 刷新额度 |
782
846
  | POST | `/connection/test` | 测试连接 |
783
847
  | POST | `/preferences/update` | 更新搜索来源和 composer 快捷用量偏好 |
848
+ | POST | `/adopt` | 导入本机登录(body `{source:'codex'}`),**只读**:快照作为一行加入 ChatGPT 号池,不触碰当前登录,也不触碰 Codex CLI 的文件 |
849
+ | POST | `/adopt/disable` | 停止导入:清掉插件这边记录的导入快照;**不删除也不改写 Codex CLI 的文件** |
784
850
 
785
851
  状态响应只包含脱敏 email、套餐、账号 ID 后四位、token 到期时间和额度 DTO。
786
852
 
@@ -842,6 +908,7 @@ MiniMax、Kimi、Codex、Claude、Command Code、WorkBuddy 和 Antigravity 的
842
908
  | 模型请求报持续 400 / 「You're out of extra usage」 | 可能是身份或版本门槛:订阅令牌要求 Claude Code 身份头与 `system` 首块,且服务端会校验你申报的客户端版本。此类错误被归类为**请求问题而非凭据问题**,不会把你登出 |
843
909
  | 额度显示为空 / 某项显示「—」 | 额度接口的键集随账号类型变化,未提供的窗口会显示为未知而**不会伪造 0**。注意 **`utilization` 是已用百分比,0% 表示尚未使用(正常态)**,不是额度耗尽;真正的耗尽会以 429 与响应头状态呈现。接口被限流时卡片保留上次成功快照并标注时间 |
844
910
  | 收编后提示「本机 Claude Code 登录已过期」 | 这是设计如此:收编的是**快照**,插件永不刷新它(Claude Code 刷新同一枚轮换令牌,两个进程各自刷新会互相作废)。回 Claude Code 重新登录后**再收编一次**即可 |
911
+ | 导入后提示「从 Codex CLI 导入的登录已过期」 | 同样如此:导入的是**快照**,插件永不刷新它(ChatGPT 的 refresh token 会轮换,而 CLI 会原地刷新自己的文件,两个进程各自刷新会互相作废,最后是你被本插件踢出 Codex CLI)。回 **Codex CLI** 重新登录后**再导入一次**即可——本插件里没有这条登录路径 |
845
912
  | 账号被标为「无法识别账号身份」 | 服务端这个账号既没返回 uuid 也没返回 email,插件无法在再次登录时自动认出它。用卡片上的**手动合并**把它并入既有账号;这是已知限制,插件不会用假 id 掩盖它 |
846
913
  | 模型不出现在选择器里 | 依次检查:卡片是否显示「模型路由已被其他 Provider 占用」(另一适配器持有该 id 时本插件会如实报告冲突而非抛错);以及模型是否已勾选 |
847
914
  | Kimi Code 发送视频却没有画面 | `k3-256k` 不支持视频,切到 `k3` 或 `kimi-for-coding`;容器须在白名单内(mp4/mpeg/mov/avi/x-flv/mpg/webm/wmv/3gpp);若该模型走的是 Anthropic 线路,视频会降级为文字(该协议没有文档化的视频块)。以上情况模型都会收到明确的文字说明,据此向你说明而不是凭空回答 |
package/lib/client.js CHANGED
@@ -685,6 +685,22 @@ window.__ModuleLoader__.load({
685
685
  ...body
686
686
  });
687
687
  }
688
+ /**
689
+ * Import this machine's Codex CLI sign-in, answering with the refreshed status.
690
+ *
691
+ * The body names the source rather than the URL carrying it, so the host can
692
+ * refuse a sign-in it does not import with a sentence instead of a silent
693
+ * no-op. The type is 'codex' alone on purpose: Claude Code and MiniMax Code are
694
+ * imported from their own provider settings, and a union here would invite a
695
+ * call the host always answers with a 400.
696
+ */
697
+ adoptLocalLogin(source) {
698
+ return post$2(`${ROUTE_PREFIX}/adopt`, { source });
699
+ }
700
+ /** Forget every imported snapshot. The CLIs' own files are never touched. */
701
+ disableAdoptedLocalLogins() {
702
+ return post$2(`${ROUTE_PREFIX}/adopt/disable`, {});
703
+ }
688
704
  refresh() {
689
705
  return post$2(`${ROUTE_PREFIX}/token/refresh`, {});
690
706
  }
@@ -1436,6 +1452,38 @@ window.__ModuleLoader__.load({
1436
1452
  setStatus(next);
1437
1453
  });
1438
1454
  };
1455
+ /**
1456
+ * Forget every imported snapshot: the undo for the local sign-in import below.
1457
+ *
1458
+ * ONE control for the whole pool rather than one per row, because the host's
1459
+ * route deliberately ignores an accountId and clears every imported row at once.
1460
+ * That is not an oversight to route around: the pool refuses to delete an
1461
+ * adopted credential, so a per-row control here could only ever be a button that
1462
+ * silently clears its neighbours — a worse bug than offering no button at all.
1463
+ * The Codex CLI's own file is not deleted, rewritten or moved by this; it only
1464
+ * makes this plugin forget the copy it took.
1465
+ */
1466
+ const stopImporting = async () => {
1467
+ await run("adopt-disable", async () => {
1468
+ setStatus(await apiRef.current.disableAdoptedLocalLogins());
1469
+ });
1470
+ };
1471
+ /**
1472
+ * Copy this machine's Codex CLI sign-in into the pool as a snapshot.
1473
+ *
1474
+ * The status is re-read rather than taken from the route's answer: the route
1475
+ * answers with the pool's refreshed status, which knows nothing about the
1476
+ * CLI file, so adopting from its value would flip the block to "not detected"
1477
+ * one click after the import succeeded. The file is still there — the import
1478
+ * copies it — so /status is the answer that tells the truth, and the row the
1479
+ * account just created comes back with it.
1480
+ */
1481
+ const importLocalSignIn = async () => {
1482
+ await run("adopt", async () => {
1483
+ await apiRef.current.adoptLocalLogin("codex");
1484
+ await load(true);
1485
+ });
1486
+ };
1439
1487
  const setRotationStrategy = async (strategy) => {
1440
1488
  await run("strategy", async () => {
1441
1489
  const next = await apiRef.current.accountAction("strategy", { strategy });
@@ -1472,6 +1520,7 @@ window.__ModuleLoader__.load({
1472
1520
  setBusy(null);
1473
1521
  }
1474
1522
  };
1523
+ const importedAccounts = (status?.accounts ?? []).filter((account) => account.removable === false);
1475
1524
  status?.account;
1476
1525
  const preferences = status?.preferences;
1477
1526
  const quota = status?.quota;
@@ -1515,6 +1564,21 @@ window.__ModuleLoader__.load({
1515
1564
  onRelogin: (accountId) => relogin(accountId),
1516
1565
  onSetStrategy: (strategy) => void setRotationStrategy(strategy),
1517
1566
  showQuota: true,
1567
+ renderLoginActions: () => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1568
+ className: "dsha-account-add-actions",
1569
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1570
+ className: "dsha-btn dsha-btn-primary",
1571
+ disabled: busy !== null,
1572
+ onClick: () => void startLogin(),
1573
+ children: login?.active ? t("pending") : t("addAccount")
1574
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1575
+ className: "dsha-btn",
1576
+ disabled: busy !== null,
1577
+ "aria-label": t("localCodexLoginImport"),
1578
+ onClick: () => void importLocalSignIn(),
1579
+ children: busy === "adopt" ? t("localCodexLoginImporting") : t("localCodexLoginImport")
1580
+ })]
1581
+ }),
1518
1582
  renderDetails: (entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: entry.planLabel && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
1519
1583
  t("plan"),
1520
1584
  ": ",
@@ -1548,7 +1612,21 @@ window.__ModuleLoader__.load({
1548
1612
  onClick: refreshToken,
1549
1613
  children: t("refreshToken")
1550
1614
  }) : null]
1551
- })
1615
+ }),
1616
+ importedAccounts.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1617
+ className: "dsha-muted",
1618
+ children: formatPoolLabel(t("stopImportingHint"), { count: importedAccounts.length })
1619
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1620
+ className: "dsha-actions",
1621
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1622
+ className: "dsha-btn",
1623
+ type: "button",
1624
+ disabled: busy !== null,
1625
+ "aria-label": formatPoolLabel(t("stopImportingLabel"), { count: importedAccounts.length }),
1626
+ onClick: () => void stopImporting(),
1627
+ children: busy === "adopt-disable" ? t("stopImportingBusy") : t("stopImporting")
1628
+ })
1629
+ })] }) : null
1552
1630
  ]
1553
1631
  }),
1554
1632
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Section, {
@@ -7143,9 +7221,6 @@ window.__ModuleLoader__.load({
7143
7221
  importClaudeCode: "导入本机 Claude Code 登录态",
7144
7222
  importing: "导入中...",
7145
7223
  stopImporting: "停止导入",
7146
- importHint: "读取本机 Claude Code 的登录凭据,作为只读快照加入账号池。不会修改、移动或删除 Claude Code 自己的文件,停止导入只是让本插件忘记这份快照。",
7147
- importUnavailable: "未找到可导入的本机 Claude Code 登录态。",
7148
- importSearched: "查找位置:{paths}",
7149
7224
  connection: "连接",
7150
7225
  testConnection: "测试连接",
7151
7226
  testingConnection: "测试中...",
@@ -7263,9 +7338,6 @@ window.__ModuleLoader__.load({
7263
7338
  importClaudeCode: "Import the local Claude Code sign-in",
7264
7339
  importing: "Importing...",
7265
7340
  stopImporting: "Stop importing",
7266
- importHint: "Reads the local Claude Code credential and adds it to the account pool as a read-only snapshot. Claude Code's own file is never modified, moved or deleted; stopping the import only makes this plugin forget the snapshot.",
7267
- importUnavailable: "No usable local Claude Code sign-in was found.",
7268
- importSearched: "Searched: {paths}",
7269
7341
  connection: "Connection",
7270
7342
  testConnection: "Test connection",
7271
7343
  testingConnection: "Testing...",
@@ -7919,118 +7991,102 @@ window.__ModuleLoader__.load({
7919
7991
  entry.source === "claude-code" ? t.sourceClaudeCode : t.sourceManaged
7920
7992
  ] })
7921
7993
  ] }),
7922
- children: [
7923
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7924
- className: "dsha-muted",
7925
- style: { paddingTop: 12 },
7926
- children: t.importHint
7927
- }),
7928
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7929
- className: "dsha-muted",
7930
- children: status?.claudeCodeSignInAvailable === true ? t.importClaudeCode : t.importUnavailable
7931
- }),
7932
- status?.claudeCodePaths !== void 0 && status.claudeCodePaths.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7933
- className: "dsha-muted dshcl-mono",
7934
- children: t.importSearched.replace("{paths}", status.claudeCodePaths.join(" · "))
7935
- }),
7936
- hasAdopted && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7937
- className: "dsha-actions",
7938
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7939
- className: "dsha-btn",
7940
- disabled: busy !== null,
7941
- "aria-label": t.stopImporting,
7942
- onClick: () => void stopImporting(),
7943
- children: busy === "adopt-disable" ? t.importing : t.stopImporting
7944
- })
7945
- }),
7946
- flow !== null && flow.status !== "idle" && flow.status !== "complete" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7947
- className: "dshcl-flow",
7948
- children: [
7949
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: t.signInSection }),
7950
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7951
- className: "dsha-muted",
7952
- children: flow?.status === "error" ? `${t.loginFailed}: ${flow.error ?? ""}` : flow?.status === "exchanging" ? t.loginExchanging : flow?.mode === "manual" ? t.loginModeManual : t.loginModeLoopback
7953
- }),
7954
- flow?.hint !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7955
- className: "dsha-muted",
7956
- children: flow.hint
7994
+ children: [hasAdopted && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7995
+ className: "dsha-actions",
7996
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7997
+ className: "dsha-btn",
7998
+ disabled: busy !== null,
7999
+ "aria-label": t.stopImporting,
8000
+ onClick: () => void stopImporting(),
8001
+ children: busy === "adopt-disable" ? t.importing : t.stopImporting
8002
+ })
8003
+ }), flow !== null && flow.status !== "idle" && flow.status !== "complete" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8004
+ className: "dshcl-flow",
8005
+ children: [
8006
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: t.signInSection }),
8007
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
8008
+ className: "dsha-muted",
8009
+ children: flow?.status === "error" ? `${t.loginFailed}: ${flow.error ?? ""}` : flow?.status === "exchanging" ? t.loginExchanging : flow?.mode === "manual" ? t.loginModeManual : t.loginModeLoopback
8010
+ }),
8011
+ flow?.hint !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
8012
+ className: "dsha-muted",
8013
+ children: flow.hint
8014
+ }),
8015
+ flow?.fallbackReason !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
8016
+ className: "dsha-notice",
8017
+ children: t.loginFallbackReason.replace("{detail}", flow.fallbackReason)
8018
+ }),
8019
+ flow?.authUrl !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
8020
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8021
+ className: "dsha-label",
8022
+ children: t.authorizeUrl
7957
8023
  }),
7958
- flow?.fallbackReason !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7959
- className: "dsha-notice",
7960
- children: t.loginFallbackReason.replace("{detail}", flow.fallbackReason)
8024
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
8025
+ className: "dshcl-flow-url",
8026
+ children: flow.authUrl
7961
8027
  }),
7962
- flow?.authUrl !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
7963
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7964
- className: "dsha-label",
7965
- children: t.authorizeUrl
7966
- }),
7967
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
7968
- className: "dshcl-flow-url",
7969
- children: flow.authUrl
7970
- }),
7971
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7972
- className: "dsha-actions",
7973
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
7974
- className: "dsha-btn",
7975
- href: flow.authUrl,
7976
- target: "_blank",
7977
- rel: "noreferrer noopener",
7978
- children: t.openAuthorizeUrl
7979
- })
8028
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
8029
+ className: "dsha-actions",
8030
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
8031
+ className: "dsha-btn",
8032
+ href: flow.authUrl,
8033
+ target: "_blank",
8034
+ rel: "noreferrer noopener",
8035
+ children: t.openAuthorizeUrl
7980
8036
  })
7981
- ] }),
7982
- flow?.redirectUri !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7983
- className: "dsha-row",
7984
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7985
- className: "dsha-label",
7986
- children: t.redirectUri
7987
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7988
- className: "dsha-value dshcl-mono",
7989
- children: flow.redirectUri
7990
- })]
7991
- }),
7992
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7993
- className: "dshcl-paste",
7994
- children: [
7995
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: t.manualPasteSection }),
7996
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7997
- className: "dsha-muted",
7998
- children: t.manualPasteHint
7999
- }),
8000
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8001
- className: "dshcl-paste-row",
8002
- children: [
8003
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
8004
- type: "text",
8005
- className: "dsha-select",
8006
- "aria-label": t.manualPasteSection,
8007
- placeholder: t.manualPastePlaceholder,
8008
- value: pasteValue,
8009
- disabled: busy !== null,
8010
- onChange: (event) => setPasteValue(event.currentTarget.value),
8011
- onKeyDown: (event) => {
8012
- if (event.key === "Enter") submitPaste();
8013
- }
8014
- }),
8015
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
8016
- className: "dsha-btn dsha-btn-primary",
8017
- disabled: busy !== null || pasteValue.trim() === "",
8018
- onClick: () => void submitPaste(),
8019
- children: busy === "login-input" ? t.manualPasteSubmitting : t.manualPasteSubmit
8020
- }),
8021
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
8022
- className: "dsha-btn",
8023
- disabled: busy !== null,
8024
- onClick: () => void cancelLogin(),
8025
- children: t.cancelSignIn
8026
- })
8027
- ]
8028
- })
8029
- ]
8030
8037
  })
8031
- ]
8032
- })
8033
- ]
8038
+ ] }),
8039
+ flow?.redirectUri !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8040
+ className: "dsha-row",
8041
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8042
+ className: "dsha-label",
8043
+ children: t.redirectUri
8044
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8045
+ className: "dsha-value dshcl-mono",
8046
+ children: flow.redirectUri
8047
+ })]
8048
+ }),
8049
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8050
+ className: "dshcl-paste",
8051
+ children: [
8052
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: t.manualPasteSection }),
8053
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
8054
+ className: "dsha-muted",
8055
+ children: t.manualPasteHint
8056
+ }),
8057
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8058
+ className: "dshcl-paste-row",
8059
+ children: [
8060
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
8061
+ type: "text",
8062
+ className: "dsha-select",
8063
+ "aria-label": t.manualPasteSection,
8064
+ placeholder: t.manualPastePlaceholder,
8065
+ value: pasteValue,
8066
+ disabled: busy !== null,
8067
+ onChange: (event) => setPasteValue(event.currentTarget.value),
8068
+ onKeyDown: (event) => {
8069
+ if (event.key === "Enter") submitPaste();
8070
+ }
8071
+ }),
8072
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
8073
+ className: "dsha-btn dsha-btn-primary",
8074
+ disabled: busy !== null || pasteValue.trim() === "",
8075
+ onClick: () => void submitPaste(),
8076
+ children: busy === "login-input" ? t.manualPasteSubmitting : t.manualPasteSubmit
8077
+ }),
8078
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
8079
+ className: "dsha-btn",
8080
+ disabled: busy !== null,
8081
+ onClick: () => void cancelLogin(),
8082
+ children: t.cancelSignIn
8083
+ })
8084
+ ]
8085
+ })
8086
+ ]
8087
+ })
8088
+ ]
8089
+ })]
8034
8090
  }),
8035
8091
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
8036
8092
  className: "dsha-group",
@@ -9334,6 +9390,12 @@ window.__ModuleLoader__.load({
9334
9390
  hubSwitchOn: "启用 {name}",
9335
9391
  hubSwitchOff: "停用 {name}",
9336
9392
  hubCardLabel: "{name},{annotation}",
9393
+ localCodexLoginImport: "导入本机 Codex CLI 登录",
9394
+ localCodexLoginImporting: "导入中…",
9395
+ stopImporting: "停止导入全部本机登录",
9396
+ stopImportingLabel: "停止导入全部本机登录(当前 {count} 个)",
9397
+ stopImportingBusy: "停止导入中…",
9398
+ stopImportingHint: "停止导入只会让本插件忘记本机 CLI 登录的副本;Codex CLI 自己的登录文件不会被删除或修改。账号池不允许逐条删除借来的凭据,所以这里一次会停止全部导入。",
9337
9399
  modelChecklistCount: "已启用 {count}/{total}"
9338
9400
  },
9339
9401
  en: {
@@ -9484,6 +9546,12 @@ window.__ModuleLoader__.load({
9484
9546
  hubSwitchOn: "Enable {name}",
9485
9547
  hubSwitchOff: "Disable {name}",
9486
9548
  hubCardLabel: "{name}, {annotation}",
9549
+ localCodexLoginImport: "Import the local Codex CLI sign-in",
9550
+ localCodexLoginImporting: "Importing…",
9551
+ stopImporting: "Stop importing all local sign-ins",
9552
+ stopImportingLabel: "Stop importing all local sign-ins ({count} currently imported)",
9553
+ stopImportingBusy: "Stopping the import…",
9554
+ stopImportingHint: "Stopping the import only makes this plugin forget its copy of the local CLI sign-in; the Codex CLI own sign-in file is never deleted or changed. The account pool does not allow deleting a borrowed credential one at a time, so this stops every import at once.",
9487
9555
  modelChecklistCount: "{count}/{total} enabled"
9488
9556
  }
9489
9557
  };