dsh-deepseek-web-login 0.6.2 → 0.6.3
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 +30 -0
- package/lib/index.js +116 -16
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
本项目遵循大致语义化版本;日期为本地时间。
|
|
4
4
|
|
|
5
|
+
## 0.6.3 — 2026-09-27
|
|
6
|
+
|
|
7
|
+
> 限流之后**任务自己接下去**,不用再手点「继续」。
|
|
8
|
+
|
|
9
|
+
### 修复
|
|
10
|
+
|
|
11
|
+
- **限流不再让整轮直接结束**:适配器现在**显式声明重试策略**
|
|
12
|
+
(`providerRetryPolicy`,上限 `maxDelayMs` 与我们的最大限流退避对齐)。
|
|
13
|
+
此前返回 `undefined` ⇒ 落到 dsh-llm 的默认上限 **10 秒**,而我们的限流退避是 **40~117 秒**
|
|
14
|
+
⇒ 策略判定"提供方要的等太久"**直接放弃重试**,`turn/end` 变成 error、界面显示
|
|
15
|
+
「本轮运行失败」,必须用户手点「继续」。现场证据(会话日志原文):
|
|
16
|
+
`failure:{code:"RATE_LIMIT", providerRetryAfterMs:49208}`。
|
|
17
|
+
- 🔴 **SSE 路径现在也会问「还能不能换号」**:此前只有 HTTP 路径走 `canFailover`,
|
|
18
|
+
而**节流最常见的形态正是 SSE**(`event: error` / `event: toast`)⇒ "能换号就给 2s 短退避、
|
|
19
|
+
让重试立刻发生并由检查点换号"这套逻辑在真实场景里从未生效过。
|
|
20
|
+
- `canFailover` 提成**单一来源**(`makeCanFailover`):HTTP 与 SSE 两条路径必须同答,
|
|
21
|
+
否则会出现"有时自己接下去、有时必须手点"。
|
|
22
|
+
|
|
23
|
+
### 修正
|
|
24
|
+
|
|
25
|
+
- `throttleBackoffMs` 的注释与代码不一致:多处写"20s 起",而实现是**先推档再计算**
|
|
26
|
+
⇒ 实际首档 **40s**。按实测值(49208ms = 40000 + 23% 抖动)校正注释,**不动行为**。
|
|
27
|
+
|
|
28
|
+
### 说明
|
|
29
|
+
|
|
30
|
+
- 重试是**有界**的(normal 模式、最多 5 次),避免账号真被封时无限打请求;
|
|
31
|
+
想要"绝不放弃"可把 `RETRY_POLICY.maxRetries` 调大或改 `mode: 'always'`(默认不开)。
|
|
32
|
+
- 封禁(`user is muted`)不受影响:解除时间远大于上限时仍按"放弃重试 + 显示解除时间"处理;
|
|
33
|
+
但**短封禁**(< 上限)现在也能自己等过去。
|
|
34
|
+
|
|
5
35
|
## 0.6.2 — 2026-09-27
|
|
6
36
|
|
|
7
37
|
> 长尾工具压成**一行**:`` `签名` — 首句 ``。目录再瘦一圈,能力依然一个不少。
|
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
|
-
/**
|
|
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(
|
|
1561
|
-
return base + Math.round(base *
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
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
|
-
/**
|
|
5735
|
-
|
|
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.
|
|
8997
|
+
const FALLBACK_VERSION = "0.6.3";
|
|
8898
8998
|
let cached;
|
|
8899
8999
|
/** 本插件版本(如 `0.1.26`)。 */
|
|
8900
9000
|
function pluginVersion() {
|