dsh-deepseek-web-login 0.6.2 → 0.6.4

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,57 @@
2
2
 
3
3
  本项目遵循大致语义化版本;日期为本地时间。
4
4
 
5
+ ## 0.6.4 — 2026-09-27
6
+
7
+ > 文档版本:把「封号归因」那一节带到 npm 包页面上(纯文档,**无代码改动**)。
8
+
9
+ ### 文档
10
+
11
+ - README(中/英)新增 **「关于『装了这个插件之后封号了』」**,放在「免责声明」之前:
12
+ 把上下文里各部分的量级摆出来,再给自查顺序。
13
+
14
+ | 来源 | 量级 | 由谁决定 |
15
+ |---|---|---|
16
+ | 本插件协议指令 | **3,045 字符 ≈ 950 token** | 本插件(多个版本未变) |
17
+ | 工具目录 | 0.6.2 起长尾压成一行,同口径 **-50%** | 你装了多少工具插件 |
18
+ | `~/.dsh/prompt-inject.md` + `AGENTS.md` | **49,185 字节 ≈ 1.2 万 token(协议的 16 倍)** | 你的全局注入 |
19
+
20
+ 另附维护者实测:连续 5 天无封禁;单日 178 次请求中 6 次限流、全部在退避后自动恢复。
21
+
22
+ ### 说明
23
+
24
+ - 仅为让 npm 包页面的 README 与仓库同步;代码与产物逻辑与 0.6.3 一致。
25
+
26
+ ## 0.6.3 — 2026-09-27
27
+
28
+ > 限流之后**任务自己接下去**,不用再手点「继续」。
29
+
30
+ ### 修复
31
+
32
+ - **限流不再让整轮直接结束**:适配器现在**显式声明重试策略**
33
+ (`providerRetryPolicy`,上限 `maxDelayMs` 与我们的最大限流退避对齐)。
34
+ 此前返回 `undefined` ⇒ 落到 dsh-llm 的默认上限 **10 秒**,而我们的限流退避是 **40~117 秒**
35
+ ⇒ 策略判定"提供方要的等太久"**直接放弃重试**,`turn/end` 变成 error、界面显示
36
+ 「本轮运行失败」,必须用户手点「继续」。现场证据(会话日志原文):
37
+ `failure:{code:"RATE_LIMIT", providerRetryAfterMs:49208}`。
38
+ - 🔴 **SSE 路径现在也会问「还能不能换号」**:此前只有 HTTP 路径走 `canFailover`,
39
+ 而**节流最常见的形态正是 SSE**(`event: error` / `event: toast`)⇒ "能换号就给 2s 短退避、
40
+ 让重试立刻发生并由检查点换号"这套逻辑在真实场景里从未生效过。
41
+ - `canFailover` 提成**单一来源**(`makeCanFailover`):HTTP 与 SSE 两条路径必须同答,
42
+ 否则会出现"有时自己接下去、有时必须手点"。
43
+
44
+ ### 修正
45
+
46
+ - `throttleBackoffMs` 的注释与代码不一致:多处写"20s 起",而实现是**先推档再计算**
47
+ ⇒ 实际首档 **40s**。按实测值(49208ms = 40000 + 23% 抖动)校正注释,**不动行为**。
48
+
49
+ ### 说明
50
+
51
+ - 重试是**有界**的(normal 模式、最多 5 次),避免账号真被封时无限打请求;
52
+ 想要"绝不放弃"可把 `RETRY_POLICY.maxRetries` 调大或改 `mode: 'always'`(默认不开)。
53
+ - 封禁(`user is muted`)不受影响:解除时间远大于上限时仍按"放弃重试 + 显示解除时间"处理;
54
+ 但**短封禁**(< 上限)现在也能自己等过去。
55
+
5
56
  ## 0.6.2 — 2026-09-27
6
57
 
7
58
  > 长尾工具压成**一行**:`` `签名` — 首句 ``。目录再瘦一圈,能力依然一个不少。
package/README.en.md CHANGED
@@ -472,6 +472,48 @@ but only when the array itself is closed (a truncated stream must never be repai
472
472
  and an unparsable protocol block is never emitted as answer text again: with no other text in the step it
473
473
  reports a retryable `EMPTY_RESPONSE`, otherwise it appends a one-line notice and logs the raw block.
474
474
 
475
+ ## On "your plugin got my account banned"
476
+
477
+ This attribution shows up from time to time. Before blaming the plugin, compare the **sizes** of what actually
478
+ goes into your context.
479
+
480
+ **What this plugin contributes**
481
+
482
+ | Source | Size | Decided by |
483
+ |---|---|---|
484
+ | Tool protocol instructions (text written by this plugin) | **3,045 chars ≈ 950 tokens** | this plugin; unchanged for many versions |
485
+ | Tool catalog (rendered by this plugin; content comes from the tools DSH hands over) | long-tail tools collapsed to one line since 0.6.2 — measured **-50%** like-for-like | **how many tool plugins you installed** |
486
+ | Above + system header, in a 61-tool setup | ~64k chars ≈ 16k tokens | the bulk is the tool catalog, not the protocol |
487
+
488
+ So the part that can fairly be blamed on this plugin is **those 3,000 characters**.
489
+
490
+ **What actually inflates the context**
491
+
492
+ - **MCP servers** — each one puts its whole tool schema into the context, on **every turn**
493
+ - **Skills** — the more you install, the thicker the system prompt
494
+ - **Jailbreak / "armor-breaking" prompts** — routinely tens of thousands of tokens, and they stay in context
495
+ - **Global injection files** — `~/.dsh/prompt-inject.md` (41 KB on our machine) + `~/.dsh/AGENTS.md` (7 KB),
496
+ sent **in full, every turn** ≈ 49 KB ≈ 12k tokens, i.e. **16×** this plugin's protocol text
497
+
498
+ A bigger context means heavier requests per turn, which makes volume/density heuristics more likely to trip.
499
+ **That part has nothing to do with this plugin, and this plugin cannot control it.**
500
+
501
+ **Maintainer's own experience (reference, not a guarantee)**
502
+
503
+ Five consecutive days of daily use (including tool-call-heavy sessions) with **no ban**; of 178 requests in one
504
+ day, 6 hit the web endpoint's "too frequent" throttle and all recovered after backoff.
505
+ The distinction matters: **throttling is routine and recovers; a ban is a different thing.**
506
+
507
+ **If you suspect you got hit, check in this order**
508
+
509
+ 1. Count your MCP servers and skills — every line is visible in `~/.dsh/profiles/desktop/cordis.patch.yml`
510
+ 2. `wc -c ~/.dsh/prompt-inject.md ~/.dsh/AGENTS.md` — see how large those two files are
511
+ 3. Open the "Token usage" tab and look at per-turn input (our median is around 100k tokens; the tool catalog is a small slice)
512
+ 4. Disable the MCP servers / plugin lines you do not use (`- id: xxx` + `disabled: true`, takes effect after restart)
513
+
514
+ Everything this plugin can do about the risk (gating request density, serializing tool calls, session cleanup,
515
+ slimming the tool catalog) is documented in the sections above. **The rest has to come from your own context.**
516
+
475
517
  ## Disclaimer
476
518
 
477
519
  > ⚠️ **Unofficial.** Not affiliated with, endorsed by, or sponsored by DeepSeek. "DeepSeek" is a trademark of its owner.
package/README.md CHANGED
@@ -643,6 +643,46 @@ DSH Desktop 用的是 `desktop` profile,而注入器的 junction 默认建在
643
643
 
644
644
  同时感谢 DeepSeek Harness 生态与 [dsh-super-injector](https://github.com/yjh051108/dsh-super-injector)(运行时注入 / 侧挂开发链路)。
645
645
 
646
+ ## 关于「装了这个插件之后封号了」
647
+
648
+ 这个归因在群里出现过几次。先把**上下文的各部分量级**摆出来,再判断该怪谁。
649
+
650
+ **本插件这一侧到底占多少**
651
+
652
+ | 来源 | 量级 | 由谁决定 |
653
+ |---|---|---|
654
+ | 工具协议指令(本插件自己写的文本) | **3,045 字符 ≈ 950 token** | 本插件,多个版本未变 |
655
+ | 工具目录(本插件负责渲染,内容来自 DSH 下发的工具) | 0.6.2 起长尾工具压成一行,同口径实测 **-50%** | **你装了多少工具插件** |
656
+ | 以上 + system 头合计(61 个工具的环境) | 约 6.4 万字符 ≈ 1.6 万 token | 大头是工具目录,不是协议 |
657
+
658
+ **能记在本插件账上的,只有那 3,000 字符。**
659
+
660
+ **真正把上下文撑大的,通常是别的东西**
661
+
662
+ - **MCP 服务器**:每接一个,它的工具 schema 就整份进上下文,而且是**每一轮**都在
663
+ - **Skills**:装得越多,system prompt 越厚
664
+ - **越狱 / 破甲类提示词**:这类文本动辄上万 token,且会一直留在上下文里
665
+ - **全局注入文件**:`~/.dsh/prompt-inject.md`(本机实测 41 KB)+ `~/.dsh/AGENTS.md`(7 KB)
666
+ —— 这两个文件是**每轮整份**进 system prompt 的,合计约 49 KB ≈ 1.2 万 token,
667
+ 是本插件协议文本的 **16 倍**
668
+
669
+ 上下文越大 ⇒ 每轮请求越重 ⇒ 越容易撞上风控的体量与密度判定。**这部分与本插件无关,本插件也管不到。**
670
+
671
+ **维护者本机实测(供参考,不是担保)**
672
+
673
+ 连续 5 天日常使用(含工具调用密集的会话)**未被封禁**;单日台账 178 次请求里有 6 次「网页版限流」,
674
+ 全部在退避后自动恢复。区别很重要:**限流是常态、可自动恢复;封禁是另一回事。**
675
+
676
+ **怀疑自己中招,按这个顺序自查**
677
+
678
+ 1. 数一下装了哪些 MCP 服务器、哪些 skill —— `~/.dsh/profiles/desktop/cordis.patch.yml` 里逐行可见
679
+ 2. `wc -c ~/.dsh/prompt-inject.md ~/.dsh/AGENTS.md` —— 看这两个文件的体积
680
+ 3. 打开设置页的「Token 统计」,看单轮输入量级(本机中位数在 10 万 token 上下,工具目录只占其中一小块)
681
+ 4. 把用不到的 MCP / 插件行禁用掉(`- id: xxx` + `disabled: true`,重启生效),再观察
682
+
683
+ 本插件能做的(压密度、串行工具调用、会话清理、长尾工具瘦身)都写在上面「请求节流」等章节里;
684
+ **剩下的大头,只能从你自己的上下文里省。**
685
+
646
686
  ## 免责声明
647
687
 
648
688
  > ⚠️ **非官方项目**:与 DeepSeek 无任何关联,未获其授权、认可或赞助。"DeepSeek" 为其权利人商标。
package/lib/index.js CHANGED
@@ -1479,15 +1479,20 @@ function muteUntilMs(json) {
1479
1479
  * 为什么是 2 秒而不是 0:让 dsh-llm-retry 立刻重发,但仍留一点余量 ——
1480
1480
  * 重发进入适配器时,"自动换号"的检查点要先跑完(可能含一次探活)。
1481
1481
  * 它**不是**"等对面恢复"的退避,而是"我马上换个号再试一次"的信号。
1482
+ *
1483
+ * 导出是为了让 `adapter.ts` 声明重试策略时拿它当 `initialDelayMs`(单一来源)。
1482
1484
  */
1483
1485
  const FAILOVER_RETRY_MS = 2e3;
1484
1486
  /**
1485
- * 限流但**没得换号**时的退避。
1487
+ * 限流但**没得换号**时的退避(HTTP 路径用的定值)。
1486
1488
  *
1487
1489
  * 为什么还留着 20 秒:限流是"发太快了"的反馈,同号立刻重试只会再撞一次。
1488
1490
  * ⚠️ 但它同时意味着"这一轮基本就断了" —— 台账实测限流后能自动重发的几次,间隔都落在
1489
1491
  * 27.8s ~ 60s 且那几次仍然失败。所以**能换号就一定走 `FAILOVER_RETRY_MS`**,
1490
1492
  * 别让用户在这里干等(那是"任务卡住、要手动重发"的根源)。
1493
+ *
1494
+ * ⚠️ 别拿它当"我们的最大退避":SSE 路径走的是 `throttleBackoffMs`(渐长、上限更高),
1495
+ * 见 `MAX_THROTTLE_RETRY_MS`。
1491
1496
  */
1492
1497
  const THROTTLE_RETRY_MS = 2e4;
1493
1498
  /**
@@ -1554,11 +1559,34 @@ function fetchImplKind() {
1554
1559
  }
1555
1560
  let throttleStreak = 0;
1556
1561
  let lastThrottleAt = 0;
1557
- /** 取下一次节流退避(ms):20s 起、每次翻倍、上限 90s,并加 0~30% 抖动。 */
1562
+ /** 限流退避:基数 20s、每次翻倍、封顶 90s,另加 0~30% 抖动。 */
1563
+ const THROTTLE_BASE_MS = 2e4;
1564
+ const THROTTLE_MAX_MS = 9e4;
1565
+ const THROTTLE_JITTER_RATIO = .3;
1566
+ /**
1567
+ * 我们**可能**上报给调用方(`dsh-llm-retry`)的**最大**限流退避。
1568
+ *
1569
+ * 🔴 它必须 ≤ 适配器声明的 `providerRetryPolicy().backoff.maxDelayMs`(见 `adapter.ts`):
1570
+ * dsh-llm 的重试策略在 normal 模式下,一旦「提供方要的延迟 > maxDelayMs」就**直接放弃重试**
1571
+ * (`return next()`),整轮随即以 error 结束 —— 表现就是"任务停在限流上,要用户手点继续"。
1572
+ *
1573
+ * 实测 2026-09-27 14:35:32(会话日志原文):
1574
+ * `failure: {code: "RATE_LIMIT", providerRetryAfterMs: 49208}` → 默认 maxDelayMs=10000
1575
+ * ⇒ 一次都没重试,`turn/end` 直接是 error。所以**两边的数字必须一起看**,别只改一边。
1576
+ */
1577
+ const MAX_THROTTLE_RETRY_MS = Math.round(THROTTLE_MAX_MS * 1.3);
1578
+ /**
1579
+ * 取下一次节流退避(ms)。
1580
+ *
1581
+ * ⚠️ 实际的**首档是 40s,不是 20s**:`noteThrottled` 先把 `throttleStreak` 加一,
1582
+ * 再调本函数算 `20s × 2^streak`。注释与代码曾不一致(多处写"20s 起"),
1583
+ * 2026-09-27 按实测值(49208ms = 40000 + 23% 抖动)校正为 40s 起。
1584
+ * 保序:40s → 80s → 90s(封顶),每次都加 0~30% 抖动。
1585
+ */
1558
1586
  function throttleBackoffMs(now = Date.now()) {
1559
1587
  if (now - lastThrottleAt > 3e5) throttleStreak = 0;
1560
- const base = Math.min(2e4 * 2 ** throttleStreak, 9e4);
1561
- return base + Math.round(base * .3 * Math.random());
1588
+ const base = Math.min(THROTTLE_BASE_MS * 2 ** throttleStreak, THROTTLE_MAX_MS);
1589
+ return base + Math.round(base * THROTTLE_JITTER_RATIO * Math.random());
1562
1590
  }
1563
1591
  /** 记录一次节流;返回本次应给的退避(ms)。 */
1564
1592
  function noteThrottled(now = Date.now()) {
@@ -1568,6 +1596,22 @@ function noteThrottled(now = Date.now()) {
1568
1596
  return throttleBackoffMs(now);
1569
1597
  }
1570
1598
  /**
1599
+ * 限流(SSE 路径)该向调用方要多久的退避。
1600
+ *
1601
+ * 能换号 ⇒ 短退避,让重试**立刻**发生(重发前的检查点会换上可用账号);
1602
+ * 不能 ⇒ 给足渐长退避。问不出来(没注入 / 抛错)时按"不能换号"处理:保守方向 ——
1603
+ * 宁可多等一会儿,也不要给一个它其实接不上的短退避(那只会更快地撞同一个限流)。
1604
+ *
1605
+ * ⚠️ `noteThrottled()` 有副作用(推高 streak),所以**只在真要长退避时才调它** ——
1606
+ * 能换号时不烧档位。
1607
+ */
1608
+ function throttleRetryAfterMs(canFailover) {
1609
+ try {
1610
+ if (canFailover?.("throttled") === true) return FAILOVER_RETRY_MS;
1611
+ } catch {}
1612
+ return noteThrottled();
1613
+ }
1614
+ /**
1571
1615
  * 账号级节流:「发得太频繁」。
1572
1616
  *
1573
1617
  * 实测 2026-09-11 16:11(SSE error 事件,不是 HTTP 429):
@@ -2571,7 +2615,7 @@ function createSseState(options = {}) {
2571
2615
  event.rateLimitKind = "concurrent";
2572
2616
  } else if (isThrottled(message)) {
2573
2617
  event.code = "RATE_LIMIT";
2574
- event.retryAfterMs = noteThrottled();
2618
+ event.retryAfterMs = throttleRetryAfterMs(options.canFailover);
2575
2619
  event.rateLimitKind = "throttled";
2576
2620
  }
2577
2621
  out.push(event);
@@ -2590,7 +2634,7 @@ function createSseState(options = {}) {
2590
2634
  event.rateLimitKind = "concurrent";
2591
2635
  } else if (isThrottled(full)) {
2592
2636
  event.code = "RATE_LIMIT";
2593
- event.retryAfterMs = noteThrottled();
2637
+ event.retryAfterMs = throttleRetryAfterMs(options.canFailover);
2594
2638
  event.rateLimitKind = "throttled";
2595
2639
  }
2596
2640
  out.push(event);
@@ -2900,19 +2944,28 @@ const defaultTransport = {
2900
2944
  * - 会话失效(invalid chat session id)→ **换一个新会话透明重试一次**(用户无感);
2901
2945
  * - 其它业务错误 → 按业务码抛出(AUTH / RATE_LIMIT / PROVIDER_ERROR…)。
2902
2946
  */
2903
- async function openCompletion(auth, params, signal, transport) {
2904
- let lastFailure;
2905
- /**
2906
- * 宿主给的「还能不能换号接着干」。问不出来(没注入 / 抛错)时按**不能**处理 ——
2907
- * 保守方向:宁可让用户点一次「继续」,也不要给一个它其实接不上的短退避。
2908
- */
2909
- const canFailover = (kind) => {
2947
+ /**
2948
+ * 宿主给的「当前账号被限时,还能不能换号接着干」。
2949
+ *
2950
+ * 🔴 **单一来源**:`openCompletion`(HTTP 路径)与 `streamWebCompletion`(SSE 路径)都必须走它 ——
2951
+ * 两条路径的答案不一致时会出现最难查的故障:SSE 给长退避放弃重试,而 HTTP 给短退避重发,
2952
+ * 用户看到的就是"有时自己接下去了、有时必须手点继续"。
2953
+ *
2954
+ * 问不出来(没注入 / 抛错)时按**不能**处理 —— 保守方向:宁可让用户多点一次「继续」,
2955
+ * 也不要给一个它其实接不上的短退避(那只会更快地撞同一个限流)。
2956
+ */
2957
+ function makeCanFailover(params) {
2958
+ return (kind) => {
2910
2959
  try {
2911
2960
  return params.canFailover?.(kind) === true;
2912
2961
  } catch {
2913
2962
  return false;
2914
2963
  }
2915
2964
  };
2965
+ }
2966
+ async function openCompletion(auth, params, signal, transport) {
2967
+ let lastFailure;
2968
+ const canFailover = makeCanFailover(params);
2916
2969
  for (let attempt = 0; attempt < 2; attempt++) {
2917
2970
  const lease = await leaseSession(auth, signal, transport, params.sessionReuseTurns ?? 20, params.onDeleteSession);
2918
2971
  const sessionId = lease.sessionId;
@@ -3050,6 +3103,7 @@ let reuseFlightTail = Promise.resolve();
3050
3103
  async function* streamWebCompletion(auth, params, transport = defaultTransport) {
3051
3104
  const controller = new AbortController();
3052
3105
  const signal = params.signal ? AbortSignal.any([params.signal, controller.signal]) : controller.signal;
3106
+ const canFailover = makeCanFailover(params);
3053
3107
  const rawLimit = params.sessionReuseTurns ?? 20;
3054
3108
  const limit = Number.isFinite(rawLimit) ? Math.max(0, Math.floor(rawLimit)) : 20;
3055
3109
  let release;
@@ -3141,6 +3195,7 @@ async function* streamWebCompletion(auth, params, transport = defaultTransport)
3141
3195
  if (timer) clearTimeout(timer);
3142
3196
  iterator = parseWebSse(body, {
3143
3197
  thinkingEnabled: params.thinkingEnabled,
3198
+ canFailover,
3144
3199
  onResponseMessageId: (id) => {
3145
3200
  responseMessageId = id;
3146
3201
  }
@@ -5714,6 +5769,45 @@ function looksMidSentence(text) {
5714
5769
  function allowsAutoContinue(purpose) {
5715
5770
  return purpose === void 0 || purpose === null || purpose === "" || purpose === "chat";
5716
5771
  }
5772
+ /**
5773
+ * 交给 dsh-llm 的**重试策略**(`LlmAdapter.providerRetryPolicy` 的返回值)。
5774
+ *
5775
+ * 🔴 为什么必须显式声明、**不能返回 undefined**:不声明就用 dsh-llm 的默认策略,其中
5776
+ * `maxDelayMs = 10_000`;而我们的限流退避是 **40~117s**。normal 模式下策略一旦发现
5777
+ * 「提供方要的延迟 > maxDelayMs」就**直接放弃重试**(`return next()`)⇒ 整轮以 error 结束、
5778
+ * 用户得手点「继续」。
5779
+ *
5780
+ * 实测 2026-09-27 14:35:32(会话日志原文):
5781
+ * `failure:{code:"RATE_LIMIT", providerRetryAfterMs:49208}` → 默认上限 10s
5782
+ * ⇒ **一次都没重试**,`turn/end` 直接是 error。这就是"自动换号了、但还得我发继续"的根因。
5783
+ *
5784
+ * 声明之后:提供方要的延迟 ≤ `maxDelayMs` 会被**照单等待并重试**(同一个打开的轮次里重跑
5785
+ * 失败步骤,历史保持一致),任务自己接下去 —— 重发时"自动换号"的检查点还会换上可用账号。
5786
+ *
5787
+ * 取值理由:
5788
+ * - `initialDelayMs` = `FAILOVER_RETRY_MS`:本地退避起点与"能换号"那条路一致;
5789
+ * - `maxDelayMs` = `MAX_THROTTLE_RETRY_MS`(117s):**必须 ≥ 我们可能上报的最大退避**,
5790
+ * 否则又回到"要等太久 ⇒ 放弃"。`tests/check-llm-retry.mjs` 守这个不变量;
5791
+ * - `maxRetries` = 5(与 dsh-llm 默认一致):**有界** —— 避免账号真被封时无限打请求。
5792
+ * 想更激进可以调大,或改 `mode: 'always'`(无尝试上限);那会在坏账号上一直重试,默认不开;
5793
+ * - `retryableCodes` 与 dsh-llm 默认一致(含 `RATE_LIMIT`)—— 我们只抛这几个码。
5794
+ */
5795
+ const RETRY_POLICY = Object.freeze({
5796
+ mode: "normal",
5797
+ maxRetries: 5,
5798
+ retryableCodes: Object.freeze([
5799
+ "EMPTY_RESPONSE",
5800
+ "RATE_LIMIT",
5801
+ "SERVER",
5802
+ "TIMEOUT",
5803
+ "TRANSPORT"
5804
+ ]),
5805
+ backoff: Object.freeze({
5806
+ initialDelayMs: FAILOVER_RETRY_MS,
5807
+ maxDelayMs: MAX_THROTTLE_RETRY_MS,
5808
+ jitterRatio: .2
5809
+ })
5810
+ });
5717
5811
  /** 构造 deepseek-web 适配器(鸭子类型满足 LlmAdapter 契约,无需继承)。 */
5718
5812
  function createAdapter(deps) {
5719
5813
  const logger = deps.config.logger;
@@ -5731,8 +5825,14 @@ function createAdapter(deps) {
5731
5825
  name: "DeepSeek 网页版(免费)"
5732
5826
  };
5733
5827
  },
5734
- /** 未配置策略 → 走 dsh-llm 默认重试码表(EMPTY_RESPONSE/RATE_LIMIT/SERVER/TIMEOUT/TRANSPORT)。 */
5735
- providerRetryPolicy(_provider) {},
5828
+ /**
5829
+ * 重试策略:**必须显式给**(理由见 `RETRY_POLICY` 的注释)——
5830
+ * 返回 `undefined` 会落到 dsh-llm 的默认上限 10s,而我们的限流退避是 40~117s,
5831
+ * 于是"要等太久 ⇒ 放弃重试 ⇒ 用户手点继续"。
5832
+ */
5833
+ providerRetryPolicy(_provider) {
5834
+ return RETRY_POLICY;
5835
+ },
5736
5836
  /**
5737
5837
  * 图片请求计价:本路由不声明 → undefined(消费者回落到自己的中性估算)。
5738
5838
  *
@@ -8894,7 +8994,7 @@ async function checkForUpdate(current, fetchImpl) {
8894
8994
  * 兜底常量与 package.json 的一致性由 `tests/check-smoke.mjs` 守着,不会漂。
8895
8995
  */
8896
8996
  /** 与 package.json 保持一致的兜底版本(由测试保证不会漂)。 */
8897
- const FALLBACK_VERSION = "0.6.2";
8997
+ const FALLBACK_VERSION = "0.6.4";
8898
8998
  let cached;
8899
8999
  /** 本插件版本(如 `0.1.26`)。 */
8900
9000
  function pluginVersion() {