@eddyskywalker/dsh-chatgpt-subscription 0.19.1 → 0.21.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.
Files changed (42) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/lib/client.js +291 -20
  3. package/lib/client.js.map +1 -1
  4. package/lib/index.js +585 -177
  5. package/lib/types/client/minimax-code/MinimaxCodeSection.d.ts.map +1 -1
  6. package/lib/types/client/minimax-code/api.d.ts +30 -0
  7. package/lib/types/client/minimax-code/api.d.ts.map +1 -1
  8. package/lib/types/client/minimax-code/locales.d.ts +62 -2
  9. package/lib/types/client/minimax-code/locales.d.ts.map +1 -1
  10. package/lib/types/client/minimax-code/styles.d.ts.map +1 -1
  11. package/lib/types/client/minimax-code/useMinimaxCodeSection.d.ts +50 -1
  12. package/lib/types/client/minimax-code/useMinimaxCodeSection.d.ts.map +1 -1
  13. package/lib/types/client/ollama/OllamaSection.d.ts.map +1 -1
  14. package/lib/types/client/ollama/api.d.ts +2 -2
  15. package/lib/types/client/ollama/api.d.ts.map +1 -1
  16. package/lib/types/host/antigravity/adapter.d.ts.map +1 -1
  17. package/lib/types/host/claude/adapter.d.ts.map +1 -1
  18. package/lib/types/host/command-code/adapter.d.ts.map +1 -1
  19. package/lib/types/host/common/idle-watchdog.d.ts +13 -1
  20. package/lib/types/host/common/idle-watchdog.d.ts.map +1 -1
  21. package/lib/types/host/glm-coding-plan/adapter.d.ts.map +1 -1
  22. package/lib/types/host/kimi-code/adapter.d.ts.map +1 -1
  23. package/lib/types/host/minimax-code/adapter.d.ts.map +1 -1
  24. package/lib/types/host/minimax-code/reset-card.d.ts +136 -0
  25. package/lib/types/host/minimax-code/reset-card.d.ts.map +1 -0
  26. package/lib/types/host/minimax-code/routes.d.ts.map +1 -1
  27. package/lib/types/host/ollama/adapter.d.ts.map +1 -1
  28. package/lib/types/host/ollama/client.d.ts +8 -1
  29. package/lib/types/host/ollama/client.d.ts.map +1 -1
  30. package/lib/types/host/ollama/routes.d.ts.map +1 -1
  31. package/lib/types/host/ollama/token-store.d.ts +8 -3
  32. package/lib/types/host/ollama/token-store.d.ts.map +1 -1
  33. package/lib/types/host/responses-client.d.ts +11 -1
  34. package/lib/types/host/responses-client.d.ts.map +1 -1
  35. package/lib/types/host/workbuddy/adapter.d.ts.map +1 -1
  36. package/lib/types/index.d.ts +8 -0
  37. package/lib/types/index.d.ts.map +1 -1
  38. package/lib/types/shared/minimax-code-contracts.d.ts +35 -0
  39. package/lib/types/shared/minimax-code-contracts.d.ts.map +1 -1
  40. package/lib/types/shared/ollama-contracts.d.ts +8 -1
  41. package/lib/types/shared/ollama-contracts.d.ts.map +1 -1
  42. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,41 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.21.0 - 2026-10-12
6
+ - **[Ollama] 修复添加 Key 后「订阅服务」整页空白;补齐线路启停开关;「全不选」与勾选状态实时同步到模型选择器**
7
+ - **问题 1(整页空白)**:`/ollama/api/accounts` 的 `add` 分支独自返回 `{ id }`,其余动作都返回完整 status。客户端把动作回包直接当作渲染状态,`status.models` 为 `undefined` 时渲染期 `TypeError` 把设置页整棵 React 子树打崩。修法:add 与其他动作一样回落到统一的完整 status 应答;客户端渲染路径对残缺 status 一律降级为空区块(`status?.models ?? []`),任何残缺载荷永不再崩页。
8
+ - **问题 2(缺开关)**:总览卡片里 Ollama 是唯一没有启停开关的线路(`canToggle: false` 硬编码)。适配器其实早已读取 `enabled` 旗标,缺的只是接线:新增 `POST /ollama/api/settings { enabled }`(与其余线路同形同义),hub 描述符接上开关,总览聚合回报真实旗标而非恒 `true`。
9
+ - **问题 3(「全不选」弹回全选)**:`enabledModelIds` 的「空数组 = 不过滤」语义使「全不选」写入 `[]` 后被读回成全选,长清单永远无法收窄。改为三态:`null` = 不过滤(全部,含日后新同步的模型)、数组 = 显式选择(`[]` = 全不选)。设置文件写入 `version: 2` 标记;旧文件的空数组只能解释为「未动过的默认」,读回 `null`,存量用户行为不变。手动勾满整张清单时客户端归一化回 `null`,「全部」继续跟随目录而非冻结在凑巧完整的那一刻。
10
+ - **问题 4(取消勾选后选择器仍显示)**:Ollama 是唯一不在设置变更后发 `llm/adapters-updated` 的线路,模型选择器只能等别的线路碰巧发事件才刷新。`/models` 与 `/settings` 的实际写入现在都发该事件,与其余线路逐字对齐。
11
+ - **测试**:路由层 8 例新增(add/delete 应答必须是完整 status、全不选持久化为 `[]`、`null` 恢复不过滤、version-2 前后文件各自读法、开关往返、只读不发事件);适配器层按三态改写 3 例;客户端层 3 例(残缺动作回包不崩页、全不选后全部不勾且计数 0/2、勾满归一化为 `null`)。反向验证:撤掉客户端加固后「残缺回包」用例以原样 `TypeError: Cannot read properties of undefined (reading 'length')` 失败,恢复后全绿。
12
+ - **验证:`npm run typecheck` 0 错误;全量 `npm test` 215 passed | 1 skipped(216 文件)、3494 passed | 7 skipped | 0 failed。**
13
+
14
+ ## 0.20.0 - 2026-10-12
15
+ - **[MiniMax Code] 新增「限额重置卡」:查看可用卡与近 30 天流水,选卡确认后重置套餐限额**
16
+ - **能力来源**:MiniMax 官方桌面端 v3.1.1(2026-10-04)起提供「限额重置」——订阅用户可在用量页查看可用重置卡与近 30 天获得/使用记录,**选择一张卡并确认后**重置对应限额。本插件复刻它,接同一组上游接口。
17
+ - **上游事实(用真实账号实测确证,非推测)**:`GET /backend/account/reset_card/list`(可用卡)、`GET /backend/account/reset_card/flows`(流水)、`POST /backend/account/reset_card/use`(消费)。主机是插件**已在用**的 `api.minimax.cn` / `api.minimaxi.com`(即 `QUOTA_HOST_CANDIDATES`),认证是**现有**的 `Authorization: Bearer`。不需要新主机、不需要签名头、不需要新认证方式。
18
+ - **三处踩过的坑,都写进了实现**:
19
+ - **`flows` 必带 `create_time`**(起始毫秒,取 30 天前),否则上游返回 `2013 invalid params`。这不是可选参数,是硬要求。
20
+ - **消费成功靠原样回传卡上的 `source_type` + `out_biz_no`,绝不自己生成 request id。** 服务端以 `out_biz_no` 去重;自造一个 id 等于递给服务端一个它没记录过的键,第二次点击就会**真的多消费一张卡**——正是本线路契约警告的那种伤害。
21
+ - **`outcome` 必须原样透传,不能塌缩成布尔值。** 六个值里 `REPLAYED` / `LOCK_BUSY` / `RETRYABLE_FAIL` 三个的含义是「可能已消费,请用**同一张卡**确认」,界面必须据此劝用户重试同一张卡而非换卡。官方客户端也是这么处理的。
22
+ - **幂等键不出浏览器**:`POST /reset-cards/use` 只接受 `{ cardId }`。host 侧先读一次面板,从**自己刚读到**的那份数据里查出该卡的 `source_type`/`out_biz_no` 再调上游——浏览器无法指定幂等键,也就无法消费一张它没读过的卡。同源检查照旧。
23
+ - **不确定态不刷新列表**:`LOCK_BUSY` / `RETRYABLE_FAIL` 之后**故意不重读面板、不刷新用量**,否则会把那一行「让用户用同一张卡重试」的提示直接从屏幕上刷掉。已决的四个 outcome 才刷新。
24
+ - **测试**:host 20 例 + 客户端 15 例,全部 mock fetch,**没有任何一次真实 `/use` 调用**。host 侧六组变异(删 `create_time`、自造 request id、不容忍 flows 失败、放行任意 outcome、只试首个主机、路由信任浏览器传来的 `out_biz_no`)各自被对应用例捕获;客户端侧四组变异(改错不确定态文案、删二次确认守卫、按钮不禁用、去掉使用后刷新)同样如期变红,恢复后全绿。
25
+ - **复审时独立重做变异**:只把 `out_biz_no` 换成自造值(一处改动),4 个用例立刻失败——含「重试必须用同一张卡」这条安全关键用例;恢复后 20/20。
26
+ - **验证:`npx tsc -b --force` 0 错误;`npx tsc -p test/tsconfig.json` 0 错误;全量 `npx vitest run` 215 passed | 1 skipped(216 文件)、3483 passed | 7 skipped | 0 failed。**
27
+ - **诚实声明:没有跑过真实消费。** 全部 `/use` 路径都是 mock 验收——真实消费会消耗用户真实的重置卡,本轮刻意不做。`list`/`flows` 的响应形状是用真实账号只读实测过的。
28
+ - **遗留项(既有问题,非本次引入)**:`QUOTA_HOST_CANDIDATES.global` 只有一个候选 `api.minimax.io`,而该主机对国区凭据返回 401。国际区账号的可用主机仍未实测——这与本线路既有的配额读取是同一个未解问题。
29
+
30
+ - **[九条线路] 空闲超时把「没有可展示内容」误判成「连接已死」,长时间思考或上游持续心跳时整轮被判 TIMEOUT**
31
+ - **问题(issue #52)**:流式线路的空闲看门狗只对「解析器 yield 出的 `StreamChunk`」复位。Codex 线路的 `wrapStreamWithWatchdog` 包在 `parseResponsesStream()` 外面,于是两类完全正常的流量会被判成 5 分钟静默:① 上游持续发 `response.in_progress` 这类状态帧——它们走完 mapper 后**不产出任何 chunk**;② reasoning summary 超过 12000 字符展示上限被截断后继续发送摘要——`visibleReasoningDelta()` 返回空文本,且不够 768 字符的 flush 水位。同一条误判存在于其余八条自建流的线路(antigravity / claude / command-code / minimax / glm / kimi / workbuddy / ollama),共九条。
32
+ - **修法:把计时判据从「展示层产出」下沉到「传输层收到数据」。** 共享 seam `wrapStreamWithWatchdog` 新增可选第二参数 `StreamActivity`(`pulse()`),内部接 `@deepseek-ai/dsh-timeout` 的 `idleWatchdog.pulse()`——它只在 `next()` 挂起期间重新计时,正是「读操作在等、字节在到」这个场景。因为 TypeScript 里参数更少的函数可以赋值给参数更多的签名,历史单参调用点继续编译通过,八条线路因此各自独立接线、互不阻塞。每条线路在**两处**上报:`reader.read()` 返回非空字节时一次,每个完整 SSE 帧一次。第二处刻意放在 `data:` 过滤**之前**,所以 `: ping` / `: keep-alive` 这类纯注释心跳(Codex 解析器原本在 `data === ''` 处整个丢掉)同样算活动。**摘要上限与展示缓冲逐字未动**:截断只影响展示,不再影响活动判定,也没有生成任何假的界面文字。
33
+ - **Codex 线路另加可配置超时**:插件 Config 新增 `codexStreamIdleTimeoutMs`,默认仍是 300000,`min(1)` 且上限 `MAX_TIMER_DELAY_MS`——**0 不是关闭开关**,这条线是截止时间不是功能开关。该常量与 `pulse()` 在 `@deepseek-ai/dsh-timeout` 里自 0.1.2-alpha.5 起就存在(已核对该 tag 的源码),所以没有新增跨代兼容 seam。
34
+ - **测试**:新增 5 个文件、8 条线路各一例,全部不 mock `idle-watchdog`(要观察的正是它本身不肯触发)。每条线路的用例都是同一个实验:假响应体连续发送纯注释心跳帧、撑过数倍于空闲窗口的时间且**期间不产生任何可展示 chunk**,最后正常收尾——修复后必须正常完成,修复前必然 `stream idle timeout`。另各配一条反向用例:真正一个字节都不发时仍然抛 `TIMEOUT`,证明反向用例不是空转。摘要截断那一条显式断言 block-end 的 reasoning 仍含 `Reasoning summary truncated` 且长度 < 12100,排除「靠去掉上限过关」这条路。
35
+ - **变异验证(复审时独立重做)**:只把共享 seam 里 `pulse` 的实现置空(一处改动,不动任何线路),8 条线路的 12 条用例**全部**以真实 TIMEOUT 失败,反向静默用例仍通过;恢复后全绿。子代理另各自做过一次「注释掉本线路 `activity?.pulse()`」的变异,结论一致。
36
+ - **验证:`npx tsc -b --force` 0 错误;`npx tsc -p test/tsconfig.json` 0 错误;全量 `npx vitest run` 213 passed | 1 skipped(214 文件)、3448 passed | 7 skipped | 0 failed。** 本轮未复现历史记录里的 Windows host 端口保留类失败。
37
+ - **诚实声明:本轮全部为 mock SSE 验收,没有真实 ChatGPT 订阅账号参与。** 心跳形态只实测了 SSE 注释帧;「合法但被 mapper 丢弃的 `data:` 状态帧」由代码路径推断同样会 pulse(pulse 挂在每一帧上),但未单独构造该形态实测。issue 里并列出现的 `NGHTTP2_INTERNAL_ERROR` 属于 HTTP/2 传输层异常,本报告不声称本次改动能解决它。
38
+
39
+
5
40
  ## 0.19.1 - 2026-10-11
6
41
  - **[GLM(ZCode 账号)] 账号管理一行的三个按钮排不下,第三个被挤到下一行**
7
42
  - **问题**:「账号管理(已登录 N 个账号)」与右侧按钮区共用一行(`.dsha-grouphead` 是 `justify-content:space-between`),而按钮容器带 `flex-wrap:wrap`。标题占掉宽度后,三个按钮(添加国区账号 / 添加国际区账号 / 导入本机 ZCode 登录态)里第三个必然折行。WorkBuddy 与 Claude 都只有两个按钮,所以这条线路是**唯一**有三个的,问题也只在它身上显形。
package/lib/client.js CHANGED
@@ -6290,6 +6290,26 @@ window.__ModuleLoader__.load({
6290
6290
  function messageOf$1(cause) {
6291
6291
  return cause instanceof Error ? cause.message : String(cause);
6292
6292
  }
6293
+ /**
6294
+ * The reset-card panel: the cards still held, plus the recent flow.
6295
+ *
6296
+ * Goes through the shared reader, so a host half that is older than this one
6297
+ * produces the reader's "restart DSH" sentence rather than a bare parse crash.
6298
+ */
6299
+ function fetchResetCards() {
6300
+ return get$1("/reset-cards");
6301
+ }
6302
+ /**
6303
+ * Spend one card.
6304
+ *
6305
+ * The body carries the `cardId` and nothing else — deliberately. The card's
6306
+ * idempotency key is a field the service de-duplicates on, so letting the
6307
+ * browser supply one would let a client spend a card it never read. The host
6308
+ * looks the card up and sends the key it found.
6309
+ */
6310
+ function useResetCard(cardId) {
6311
+ return post$2("/reset-cards/use", { cardId });
6312
+ }
6293
6313
  //#endregion
6294
6314
  //#region src/client/minimax-code/locales.ts
6295
6315
  const NS_MINIMAX_CODE = "dsh-minimax-code";
@@ -6359,6 +6379,26 @@ window.__ModuleLoader__.load({
6359
6379
  quotaUsedShare: "已用 {percent}%",
6360
6380
  quotaRefresh: "刷新用量",
6361
6381
  quotaRefreshing: "刷新中...",
6382
+ resetSection: "限额重置",
6383
+ resetHint: "订阅附带的限额重置卡可以立即重置用量限额。请先选择一张卡,再确认消费。",
6384
+ resetConfirm: "请选择一张卡并确认消费。完成后会重新读取用量,不会自动重发对话。",
6385
+ resetAvailable: "可用重置卡 {count} 张",
6386
+ resetNone: "当前没有可用的重置卡。",
6387
+ resetUnavailable: "暂时读不到重置卡(未登录,或该账号没有这个功能)。",
6388
+ resetExpires: "到期 {time}",
6389
+ resetUse: "使用",
6390
+ resetUsing: "重置中…",
6391
+ resetRefresh: "刷新重置卡",
6392
+ resetRefreshing: "读取中…",
6393
+ resetFlowTitle: "近 30 天记录",
6394
+ resetFlowEmpty: "近 30 天没有重置卡记录。",
6395
+ resetFlowGrant: "获得",
6396
+ resetFlowUse: "已使用",
6397
+ resetOutcomeSuccess: "重置成功。",
6398
+ resetOutcomeReplayed: "该卡已使用,限额状态已更新。",
6399
+ resetOutcomeUncertain: "结果不确定,请用同一张卡重试确认,不会重复扣卡——换一张卡会真的多消费一张。",
6400
+ resetOutcomeExpired: "该卡已过期,无法用于重置;列表已刷新。",
6401
+ resetOutcomeNotFound: "该卡不存在或已不可用;列表已刷新。",
6362
6402
  composerLabel: "额度",
6363
6403
  selectAll: "全选",
6364
6404
  unselectAll: "全不选",
@@ -6449,6 +6489,26 @@ window.__ModuleLoader__.load({
6449
6489
  quotaUsedShare: "{percent}% used",
6450
6490
  quotaRefresh: "Refresh usage",
6451
6491
  quotaRefreshing: "Refreshing...",
6492
+ resetSection: "Quota reset",
6493
+ resetHint: "Reset cards included with the subscription reset a usage limit immediately. Pick one card, then confirm spending it.",
6494
+ resetConfirm: "Select a card and confirm spending it. Usage is re-read afterwards; no conversation is resent.",
6495
+ resetAvailable: "{count} reset card(s) available",
6496
+ resetNone: "No reset card is available right now.",
6497
+ resetUnavailable: "Reset cards are unreadable right now (not signed in, or this account does not have the feature).",
6498
+ resetExpires: "Expires {time}",
6499
+ resetUse: "Use",
6500
+ resetUsing: "Resetting…",
6501
+ resetRefresh: "Refresh cards",
6502
+ resetRefreshing: "Reading...",
6503
+ resetFlowTitle: "Last 30 days",
6504
+ resetFlowEmpty: "No reset-card activity in the last 30 days.",
6505
+ resetFlowGrant: "Granted",
6506
+ resetFlowUse: "Used",
6507
+ resetOutcomeSuccess: "Reset succeeded.",
6508
+ resetOutcomeReplayed: "That card was already used; the limit state has been updated.",
6509
+ resetOutcomeUncertain: "The result is uncertain. Retry with the SAME card to confirm — retrying does not spend a second card, switching to another one does.",
6510
+ resetOutcomeExpired: "That card has expired and cannot reset the limit; the list has been refreshed.",
6511
+ resetOutcomeNotFound: "That card does not exist or is no longer usable; the list has been refreshed.",
6452
6512
  composerLabel: "Quota",
6453
6513
  selectAll: "Select all",
6454
6514
  unselectAll: "Unselect all",
@@ -6536,6 +6596,51 @@ window.__ModuleLoader__.load({
6536
6596
  composerLabel: t("composerLabel")
6537
6597
  };
6538
6598
  }
6599
+ /**
6600
+ * One reset-card panel read, defensively shaped.
6601
+ *
6602
+ * The route is new and the card renders whatever it is handed, so a half that
6603
+ * answers something else (a stale host, a route that 404s into an error body the
6604
+ * reader already raised) must not turn into a render crash on `cards.length`.
6605
+ * A missing list is an empty list, which is also the truthful reading: the user
6606
+ * can select nothing and the use button stays disabled.
6607
+ */
6608
+ function normalizeResetCards(value) {
6609
+ return {
6610
+ cards: Array.isArray(value?.cards) ? value.cards : [],
6611
+ flows: Array.isArray(value?.flows) ? value.flows : [],
6612
+ fetchedAtMs: typeof value?.fetchedAtMs === "number" && Number.isFinite(value.fetchedAtMs) ? value.fetchedAtMs : 0
6613
+ };
6614
+ }
6615
+ /**
6616
+ * What the card tells the user after a spend, one sentence per outcome.
6617
+ *
6618
+ * The six outcomes are not six flavours of the same answer. Two are certain
6619
+ * ("done"), two are refusals (the card is gone — re-read the list), and two are
6620
+ * `I did not find out`: the service de-duplicates on the card's own key, so
6621
+ * retrying THE SAME card costs nothing while picking another one really does
6622
+ * spend a second card. That is why the uncertain sentence names the same card.
6623
+ */
6624
+ function resetOutcomeNotice(outcome, t) {
6625
+ switch (outcome) {
6626
+ case "SUCCESS": return t("resetOutcomeSuccess");
6627
+ case "REPLAYED": return t("resetOutcomeReplayed");
6628
+ case "LOCK_BUSY":
6629
+ case "RETRYABLE_FAIL": return t("resetOutcomeUncertain");
6630
+ case "EXPIRED": return t("resetOutcomeExpired");
6631
+ case "NOT_FOUND": return t("resetOutcomeNotFound");
6632
+ }
6633
+ }
6634
+ /**
6635
+ * Whether an outcome leaves the spent card's fate decided.
6636
+ *
6637
+ * True for the four outcomes that will not change if the user waits: the card is
6638
+ * consumed or it is not usable. False for the two uncertain ones — refreshing
6639
+ * there would drop the very row the sentence tells the user to retry with.
6640
+ */
6641
+ function outcomeIsDecided(outcome) {
6642
+ return outcome !== "LOCK_BUSY" && outcome !== "RETRYABLE_FAIL";
6643
+ }
6539
6644
  /** Device-code poll cadence. The host owns the real expiry. */
6540
6645
  const LOGIN_POLL_INTERVAL_MS$1 = 2e3;
6541
6646
  function useMinimaxCodeSection({ onModelChange }) {
@@ -6550,6 +6655,9 @@ window.__ModuleLoader__.load({
6550
6655
  const [contextDrafts, setContextDrafts] = (0, react.useState)({});
6551
6656
  const [savingModel, setSavingModel] = (0, react.useState)(null);
6552
6657
  const [pool, setPool] = (0, react.useState)(void 0);
6658
+ const [resetCards, setResetCards] = (0, react.useState)(null);
6659
+ const [resetNotice, setResetNotice] = (0, react.useState)(null);
6660
+ const [selectedResetCardId, setSelectedResetCardId] = (0, react.useState)(null);
6553
6661
  const quotaFollowUp = (0, react.useRef)(void 0);
6554
6662
  const loadStatus = (0, react.useCallback)(async (quiet = false) => {
6555
6663
  if (!quiet) setError(null);
@@ -6636,6 +6744,64 @@ window.__ModuleLoader__.load({
6636
6744
  } catch {}
6637
6745
  })();
6638
6746
  }, [authenticated, status?.quota]);
6747
+ /**
6748
+ * Read the reset-card panel.
6749
+ *
6750
+ * Silent on failure, exactly like the pool read: this feature is absent on
6751
+ * accounts that never earned a card, and a settings card must not raise an
6752
+ * error banner for a route that has nothing to say.
6753
+ */
6754
+ const loadResetCards = (0, react.useCallback)(async () => {
6755
+ try {
6756
+ setResetCards(normalizeResetCards(await fetchResetCards()));
6757
+ } catch {
6758
+ setResetCards(null);
6759
+ }
6760
+ }, []);
6761
+ /**
6762
+ * The panel is read once the card knows there is an account to read it for.
6763
+ *
6764
+ * Reads before sign-in answer with a refusal, which is not a state the user
6765
+ * can act on, so the read waits for a signed-in account exactly as the usage
6766
+ * read below does.
6767
+ */
6768
+ (0, react.useEffect)(() => {
6769
+ if (!authenticated) return;
6770
+ loadResetCards();
6771
+ }, [authenticated, loadResetCards]);
6772
+ const refreshResetCards = async () => {
6773
+ await loadResetCards();
6774
+ };
6775
+ const selectResetCard = (cardId) => {
6776
+ setSelectedResetCardId(cardId);
6777
+ };
6778
+ /**
6779
+ * Spend one card, behind the confirmation the official client asks for.
6780
+ *
6781
+ * The confirmation lives here rather than in the view because "cancel" must
6782
+ * mean no request at all: the card may not even be sent when the user says no.
6783
+ * After a decided outcome the panel is re-read so the spent card leaves the
6784
+ * list; after an uncertain one it is deliberately left alone, because the row
6785
+ * the user is told to retry with is the one that would disappear.
6786
+ */
6787
+ const spendResetCard = async (cardId) => {
6788
+ if (typeof window !== "undefined" && !window.confirm(t("resetConfirm"))) return;
6789
+ try {
6790
+ setBusy("reset-card");
6791
+ setError(null);
6792
+ const result = await useResetCard(cardId);
6793
+ setResetCards(normalizeResetCards(result.cards));
6794
+ setResetNotice(resetOutcomeNotice(result.outcome, t));
6795
+ setSelectedResetCardId(null);
6796
+ if (!outcomeIsDecided(result.outcome)) return;
6797
+ await loadResetCards();
6798
+ await refreshQuota();
6799
+ } catch (cause) {
6800
+ setError(messageOf$1(cause));
6801
+ } finally {
6802
+ setBusy(null);
6803
+ }
6804
+ };
6639
6805
  const login = async () => {
6640
6806
  try {
6641
6807
  setBusy("login");
@@ -6901,7 +7067,10 @@ window.__ModuleLoader__.load({
6901
7067
  connectionNotice,
6902
7068
  contextDrafts,
6903
7069
  savingModel,
6904
- pool
7070
+ pool,
7071
+ resetCards,
7072
+ resetNotice,
7073
+ selectedResetCardId
6905
7074
  },
6906
7075
  derived: {
6907
7076
  authenticated,
@@ -6910,6 +7079,8 @@ window.__ModuleLoader__.load({
6910
7079
  models,
6911
7080
  contextModels,
6912
7081
  overrideCount,
7082
+ resetCardList: resetCards?.cards ?? [],
7083
+ resetCardFlows: resetCards?.flows ?? [],
6913
7084
  poolLabels: accountPoolLabelsFor$1(t)
6914
7085
  },
6915
7086
  actions: {
@@ -6928,6 +7099,9 @@ window.__ModuleLoader__.load({
6928
7099
  resetContextWindow,
6929
7100
  resetAllContextWindows,
6930
7101
  refreshQuota,
7102
+ refreshResetCards,
7103
+ selectResetCard,
7104
+ useResetCard: spendResetCard,
6931
7105
  testConnection,
6932
7106
  updateContextDraft
6933
7107
  }
@@ -6964,9 +7138,9 @@ window.__ModuleLoader__.load({
6964
7138
  }
6965
7139
  function MinimaxCodeSection({ onModelChange }) {
6966
7140
  const { state, derived, actions } = useMinimaxCodeSection({ onModelChange });
6967
- const { status, loading, busy, error, flow, copied, connectionNotice, contextDrafts, savingModel, pool } = state;
6968
- const { authenticated, account, quota, models, contextModels, overrideCount, poolLabels } = derived;
6969
- const { login, cancelLogin, copyCode, logout, poolAction, setStrategy, toggleModel, setAllModels, setCheckinEnabled, runCheckinNow, updateEffort, saveContextWindow, resetContextWindow, resetAllContextWindows, refreshQuota, testConnection, updateContextDraft } = actions;
7141
+ const { status, loading, busy, error, flow, copied, connectionNotice, contextDrafts, savingModel, pool, resetCards, resetNotice, selectedResetCardId } = state;
7142
+ const { authenticated, account, quota, models, contextModels, overrideCount, poolLabels, resetCardList, resetCardFlows } = derived;
7143
+ const { login, cancelLogin, copyCode, logout, poolAction, setStrategy, toggleModel, setAllModels, setCheckinEnabled, runCheckinNow, updateEffort, saveContextWindow, resetContextWindow, resetAllContextWindows, refreshQuota, refreshResetCards, selectResetCard, useResetCard, testConnection, updateContextDraft } = actions;
6970
7144
  const t = fallbackTranslate$1;
6971
7145
  if (loading) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6972
7146
  className: "dsha-page",
@@ -7350,6 +7524,98 @@ window.__ModuleLoader__.load({
7350
7524
  })
7351
7525
  ]
7352
7526
  }),
7527
+ resetCards !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
7528
+ className: "dsha-group",
7529
+ children: [
7530
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7531
+ className: "dsha-grouphead",
7532
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: t("resetSection") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7533
+ className: "dsha-btn",
7534
+ disabled: busy !== null,
7535
+ onClick: () => void refreshResetCards(),
7536
+ children: busy === "reset-card" ? t("resetRefreshing") : t("resetRefresh")
7537
+ })]
7538
+ }),
7539
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7540
+ className: "dsha-muted",
7541
+ children: t("resetHint")
7542
+ }),
7543
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7544
+ className: "dsha-row",
7545
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7546
+ className: "dsha-label",
7547
+ children: t("resetAvailable").replace("{count}", String(resetCardList.length))
7548
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7549
+ className: "dsha-value",
7550
+ children: formatDate(resetCards.fetchedAtMs)
7551
+ })]
7552
+ }),
7553
+ resetCardList.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7554
+ className: "dsha-empty",
7555
+ children: t("resetNone")
7556
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7557
+ className: "dshm-reset-list",
7558
+ children: resetCardList.map((card) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7559
+ className: "dsha-row dshm-reset-row",
7560
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7561
+ type: "button",
7562
+ className: "dsha-btn dshm-reset-pick",
7563
+ "aria-pressed": selectedResetCardId === card.cardId,
7564
+ disabled: busy !== null,
7565
+ onClick: () => selectResetCard(selectedResetCardId === card.cardId ? null : card.cardId),
7566
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7567
+ className: "dshm-mono",
7568
+ children: card.cardId
7569
+ })
7570
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7571
+ className: "dsha-value",
7572
+ children: t("resetExpires").replace("{time}", formatDate(card.expiresAtMs))
7573
+ })]
7574
+ }, card.cardId))
7575
+ }),
7576
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7577
+ className: "dsha-notice dshm-quota-note",
7578
+ children: t("resetConfirm")
7579
+ }),
7580
+ resetNotice !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7581
+ className: "dsha-notice dshm-quota-note",
7582
+ role: "status",
7583
+ children: resetNotice
7584
+ }),
7585
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7586
+ className: "dsha-actions",
7587
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7588
+ className: "dsha-btn dsha-btn-primary",
7589
+ disabled: busy !== null || selectedResetCardId === null,
7590
+ onClick: () => {
7591
+ if (selectedResetCardId !== null) useResetCard(selectedResetCardId);
7592
+ },
7593
+ children: busy === "reset-card" ? t("resetUsing") : t("resetUse")
7594
+ })
7595
+ }),
7596
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7597
+ className: "dsha-muted dshm-quota-note",
7598
+ children: t("resetFlowTitle")
7599
+ }),
7600
+ resetCardFlows.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7601
+ className: "dsha-empty",
7602
+ children: t("resetFlowEmpty")
7603
+ }) : resetCardFlows.map((entry, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7604
+ className: "dsha-row",
7605
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7606
+ className: "dsha-label",
7607
+ children: entry.eventType === "GRANT" ? t("resetFlowGrant") : t("resetFlowUse")
7608
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
7609
+ className: "dsha-value dshm-mono",
7610
+ children: [
7611
+ entry.cardId,
7612
+ " · ",
7613
+ formatDate(entry.createdAtMs)
7614
+ ]
7615
+ })]
7616
+ }, `${entry.cardId}:${entry.createdAtMs}:${index}`))
7617
+ ]
7618
+ }),
7353
7619
  error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7354
7620
  className: "dsha-error",
7355
7621
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: error })
@@ -9974,13 +10240,13 @@ window.__ModuleLoader__.load({
9974
10240
  const [key, setKey] = (0, react.useState)("");
9975
10241
  const [alias, setAlias] = (0, react.useState)("");
9976
10242
  const [showKeyForm, setShowKeyForm] = (0, react.useState)(false);
9977
- /** Ids the user has switched on. Empty means 'no filter', i.e. all of them. */
9978
- const [enabled, setEnabled] = (0, react.useState)(/* @__PURE__ */ new Set());
10243
+ /** Ids the user has switched on. Null means 'no filter', i.e. all of them. */
10244
+ const [enabled, setEnabled] = (0, react.useState)(null);
9979
10245
  const reload = (0, react.useCallback)(async () => {
9980
10246
  try {
9981
10247
  const next = await API.status();
9982
10248
  setStatus(next);
9983
- setEnabled(new Set(next.enabledModelIds));
10249
+ setEnabled(next.enabledModelIds == null ? null : new Set(next.enabledModelIds));
9984
10250
  } catch (cause) {
9985
10251
  setError(cause instanceof Error ? cause.message : String(cause));
9986
10252
  }
@@ -10026,31 +10292,32 @@ window.__ModuleLoader__.load({
10026
10292
  });
10027
10293
  }, [run]);
10028
10294
  const isModelEnabled = (0, react.useCallback)((id) => {
10029
- return enabled.size === 0 || enabled.has(id);
10295
+ return enabled === null || enabled.has(id);
10030
10296
  }, [enabled]);
10031
10297
  const applyEnabled = (0, react.useCallback)((next) => {
10032
10298
  run("models", async () => {
10033
10299
  const result = await API.setEnabledModels(next);
10034
- setEnabled(new Set(result.enabledModelIds));
10300
+ setEnabled(result.enabledModelIds == null ? null : new Set(result.enabledModelIds));
10035
10301
  return API.status();
10036
10302
  });
10037
10303
  }, [run]);
10038
10304
  const toggleModel = (0, react.useCallback)((id, checked) => {
10039
10305
  if (status === null) return;
10040
- const current = status.models.filter((model) => isModelEnabled(model.id)).map((model) => model.id);
10306
+ const all = status.models ?? [];
10307
+ const current = all.filter((model) => isModelEnabled(model.id)).map((model) => model.id);
10041
10308
  const next = checked ? [.../* @__PURE__ */ new Set([...current, id])] : current.filter((entry) => entry !== id);
10042
- applyEnabled(next);
10309
+ applyEnabled(next.length === all.length ? null : next);
10043
10310
  }, [
10044
10311
  status,
10045
10312
  isModelEnabled,
10046
10313
  applyEnabled
10047
10314
  ]);
10048
10315
  const setAllModels = (0, react.useCallback)((selectAll) => {
10049
- if (status === null || status.models.length === 0) return;
10050
- applyEnabled(selectAll ? status.models.map((model) => model.id) : []);
10051
- }, [status, applyEnabled]);
10316
+ applyEnabled(selectAll ? null : []);
10317
+ }, [applyEnabled]);
10052
10318
  const t = zh$1;
10053
10319
  const pool = status?.pool;
10320
+ const models = status?.models ?? [];
10054
10321
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
10055
10322
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
10056
10323
  className: "dsha-group",
@@ -10191,21 +10458,21 @@ window.__ModuleLoader__.load({
10191
10458
  style: { fontSize: 12 },
10192
10459
  children: t.wireNote
10193
10460
  }),
10194
- status !== null && status.models.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
10461
+ status !== null && models.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
10195
10462
  className: "dsha-empty",
10196
10463
  children: status.catalogSynced ? t.catalogEmpty : t.catalogNeverSynced
10197
10464
  }),
10198
- status !== null && status.models.length === 0 && !status.usable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
10465
+ status !== null && models.length === 0 && !status.usable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
10199
10466
  className: "dsha-muted",
10200
10467
  style: { fontSize: 12 },
10201
10468
  children: t.catalogNeedKey
10202
10469
  }),
10203
- status !== null && status.models.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
10470
+ status !== null && models.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
10204
10471
  className: "dsha-muted",
10205
10472
  style: { fontSize: 12 },
10206
- children: t.modelCount.replace("{count}", String(status.models.length))
10473
+ children: t.modelCount.replace("{count}", String(models.length))
10207
10474
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelChecklist, {
10208
- items: status.models.map((model) => ({
10475
+ items: models.map((model) => ({
10209
10476
  id: model.id,
10210
10477
  name: model.name ?? model.id,
10211
10478
  enabled: isModelEnabled(model.id)
@@ -10329,7 +10596,7 @@ window.__ModuleLoader__.load({
10329
10596
  {
10330
10597
  id: "ollama",
10331
10598
  name: "Ollama",
10332
- setEnabled: null,
10599
+ setEnabled: (enabled) => postLineSettings("/ollama/api", { enabled }),
10333
10600
  renderDetail: ({ onModelChange }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OllamaSection, { onModelChange })
10334
10601
  }
10335
10602
  ];
@@ -11777,6 +12044,10 @@ window.__ModuleLoader__.load({
11777
12044
  .dshm-model-list{display:flex;flex-wrap:wrap;gap:7px;padding-top:8px}
11778
12045
  .dshm-model-list code{background:var(--dsw-alias-bg-layer-2);border:0.5px solid var(--dsw-alias-border-l2);border-radius:6px;color:var(--dsw-alias-label-secondary);font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:11px;padding:5px 8px;white-space:nowrap}
11779
12046
  .dshm-quota-note{margin-top:12px}
12047
+ .dshm-reset-list{display:flex;flex-direction:column;gap:6px;padding-top:8px}
12048
+ .dshm-reset-row{align-items:center;justify-content:space-between}
12049
+ .dshm-reset-pick{background:none;border:0.5px solid var(--dsw-alias-border-l2);cursor:pointer;padding:4px 8px}
12050
+ .dshm-reset-pick[aria-pressed="true"]{border-color:var(--dsw-alias-border-focus,var(--dsw-alias-border-l3));background:var(--dsw-alias-bg-layer-2)}
11780
12051
  `, STYLE_ID$2);
11781
12052
  }
11782
12053
  //#endregion