dsh-deepseek-web-login 0.6.28 → 0.6.29

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
  本项目大致遵循语义化版本;日期为本地时间。
4
4
 
5
+ ## 0.6.29 — 2026-10-02
6
+
7
+ **修复:登录态过期(或账号被限流)之后,重新登录会"又新建一个网页端会话"、整段历史全量重发。**
8
+
9
+ 现场:用户在旧窗口发了一条消息,网页端凭空多出一个会话;DSH 日志里那一轮是
10
+ `API 密钥无效`(AUTH)→ 重试 1795ms。他随后重新登录了**同一个账号**,但会话没能接回去。
11
+ `feed-decisions.jsonl` 对上了:那次是 `new-session`,而 `account` 前后**完全一样** ——
12
+ 账号没变,是**会话槽被退役了**。
13
+
14
+ **根因**:`retireSession()` 被当成"失败即弃"用在了三处失败分支(`webapi.ts`):
15
+ 建连抛错、`!resp.ok`、业务错误信封。401/403 说的是**令牌**过期、429 说的是**账号级**限流
16
+ —— 这些跟"这个会话"毫无关系,而且**换一个新会话账号照样被限**:退役它没有任何好处,
17
+ 唯一的效果是把用户整段对话丢掉(重登后只能新建会话 + 把历史全量重发一遍)。
18
+
19
+ **改法**:只有**服务端明说"这个会话废了"**(业务码 `invalid chat session id`)才退役;
20
+ 认证与限流这类**账号级**失败改为记进 `keepIds`,由收尾逻辑保住。
21
+
22
+ - 判定发生在 `openCompletion`(只有它看得见错误码/业务码),消费发生在
23
+ `streamWebCompletion` 的 finally —— 因为建连阶段失败时外层 `sessionId` **还是 undefined**
24
+ (它在 `await` 之后才赋值),`id === sessionId` 恒假,靠错误码在外层判会连"本轮真正在用的
25
+ 那个会话"一起退役。
26
+ - ⚠️ 5xx / 网络失败 / 取消 / 空响应**不在**保留之列:那些情况下服务端可能已经开始生成、
27
+ 会话停在半路,照旧退役(N04 的意图不削弱)。原「失败即弃」用例改用 500 并改名为
28
+ 「服务端 5xx(会话可能停在半路)⇒ 不复用那个会话」,意图不变、范围收窄。
29
+
30
+ **新增 5 条用例**:401 / 429 / `user is muted` / `throttled` 四种账号级失败后必须**接回原会话**、
31
+ 且不得把会话交给宿主去删;外加"新窗口第一轮就 401"(走的是收尾逻辑那条路径 ——
32
+ 复用来的会话不在 `owned` 里,两条路径必须各守一条)。
33
+ 四处变异全部确认变红:内联跳过、信封分支、收尾闸门,以及重建后的产物断言。
34
+
5
35
  ## 0.6.28 — 2026-10-02
6
36
 
7
37
  **修复:思考通道的垃圾(DSML 等工具调用标记)会显示在网页端、并且一直留在转写里。**
package/lib/index.js CHANGED
@@ -5395,7 +5395,7 @@ function makeCanFailover(params) {
5395
5395
  }
5396
5396
  };
5397
5397
  }
5398
- async function openCompletion(auth, params, signal, transport) {
5398
+ async function openCompletion(auth, params, signal, transport, keepIds) {
5399
5399
  let lastFailure;
5400
5400
  const canFailover = makeCanFailover(params);
5401
5401
  const slotKey = requestSlotKey(auth, params);
@@ -5477,8 +5477,11 @@ async function openCompletion(auth, params, signal, transport) {
5477
5477
  signal
5478
5478
  });
5479
5479
  } catch (error) {
5480
- retireSession(sessionId);
5481
- params.onDeleteSession?.(sessionId);
5480
+ if (error instanceof AdapterLlmError && (error.code === "AUTH" || error.code === "RATE_LIMIT")) keepIds?.add(sessionId);
5481
+ else {
5482
+ retireSession(sessionId);
5483
+ params.onDeleteSession?.(sessionId);
5484
+ }
5482
5485
  if (error instanceof AdapterLlmError) throw error;
5483
5486
  if (params.signal?.aborted) throw new AdapterLlmError("DeepSeek web request aborted by caller", "ABORTED", { cause: error });
5484
5487
  throw new AdapterLlmError(`DeepSeek web request failed: ${error?.message ?? error}`, "TRANSPORT", { cause: error });
@@ -5488,8 +5491,11 @@ async function openCompletion(auth, params, signal, transport) {
5488
5491
  const code = httpErrorCode(resp.status);
5489
5492
  const retryAfter = parseRetryAfterMs(resp.headers.get("retry-after"));
5490
5493
  const hint = code === "AUTH" ? " —— 网页登录态可能已过期,请到「设置 → DeepSeek 网页登录」重新登录" : code === "RATE_LIMIT" ? " —— 网页版限流" : "";
5491
- retireSession(sessionId);
5492
- params.onDeleteSession?.(sessionId);
5494
+ if (code === "AUTH" || code === "RATE_LIMIT") keepIds?.add(sessionId);
5495
+ else {
5496
+ retireSession(sessionId);
5497
+ params.onDeleteSession?.(sessionId);
5498
+ }
5493
5499
  const authRetryAfterMs = code === "AUTH" ? canFailover("auth") ? AUTH_FAILOVER_RETRY_MS : AUTH_GIVEUP_RETRY_MS : void 0;
5494
5500
  throw new AdapterLlmError(`DeepSeek web completion failed (HTTP ${resp.status})${text ? `: ${text.slice(0, 200)}` : ""}${hint}`, code, {
5495
5501
  status: resp.status,
@@ -5534,9 +5540,14 @@ async function openCompletion(auth, params, signal, transport) {
5534
5540
  providerRetryAfterMs: canFailover("throttled") ? FAILOVER_RETRY_MS : THROTTLE_RETRY_MS
5535
5541
  } : {}
5536
5542
  }) : new AdapterLlmError(`DeepSeek 网页端返回了非流式响应(content-type: ${contentType || "unknown"}):${text.slice(0, 200)}`, "MALFORMED_RESPONSE", { status: resp.status });
5537
- retireSession(sessionId);
5538
- params.onDeleteSession?.(sessionId);
5539
- if (attempt === 0 && biz && isInvalidSessionError(biz)) {
5543
+ const accountLevel = !!biz && (muted || busy || throttled || failureCode === "AUTH");
5544
+ const ruined = !!biz && isInvalidSessionError(biz);
5545
+ if (accountLevel) keepIds?.add(sessionId);
5546
+ else {
5547
+ retireSession(sessionId);
5548
+ params.onDeleteSession?.(sessionId);
5549
+ }
5550
+ if (attempt === 0 && ruined) {
5540
5551
  lastFailure = failure;
5541
5552
  continue;
5542
5553
  }
@@ -5629,6 +5640,26 @@ async function* streamWebCompletion(auth, params, transport = defaultTransport)
5629
5640
  * 所以必须在这里兜住它后续才返回的那些会话。
5630
5641
  */
5631
5642
  const owned = /* @__PURE__ */ new Set();
5643
+ /**
5644
+ * 本轮**账号级失败**(认证/限流)时不该被退役的会话(0.6.29)。
5645
+ *
5646
+ * 为什么不能放在 `finally` 里靠错误码判:建连阶段失败时外层 `sessionId` **还是 undefined**
5647
+ * (它在 `await openCompletion(...)` 之后才赋值),`id === sessionId` 恒假 ⇒ 会连
5648
+ * "本轮真正在用的那个会话"一起退役。必须先把它记下来 —— 只有 `openCompletion` 知道。
5649
+ *
5650
+ * 判据(在那里判,这里只消费):`AUTH`(HTTP 401/403、信封 40003/40001)与
5651
+ * `RATE_LIMIT`(HTTP 429、`user is muted`、`busy generating`、`throttled` 文案族)
5652
+ * 说的都是**账号级**状态 —— 令牌过期、账号被限到某时刻、账号同时在生成。
5653
+ * 换一个新网页端会话一点用都没有(账号照样被限),唯一的效果是把用户的对话丢掉:
5654
+ * 他重新登录后只能新建会话、把整段历史全量重发一遍。
5655
+ * 2026-10-02 现场:登录态过期 ⇒ 401 ⇒ 会话被弃 ⇒ `feed-decisions` 里同账号冒出
5656
+ * `new-session`、网页端"又新建一个窗口"、29 条历史全量重发。
5657
+ *
5658
+ * ⚠️ 5xx / 网络失败 / 取消 / 空响应 **不在此列** —— 那些情况下服务端可能已经开始生成、
5659
+ * 会话停在半路,照旧退役(N04 的意图不能削弱);会话真的废了则由业务码
5660
+ * `invalid chat session id` 显式告知。
5661
+ */
5662
+ const keepIds = /* @__PURE__ */ new Set();
5632
5663
  let finalized = false;
5633
5664
  const tracked = {
5634
5665
  ...transport,
@@ -5657,7 +5688,7 @@ async function* streamWebCompletion(auth, params, transport = defaultTransport)
5657
5688
  ...params,
5658
5689
  sessionReuseTurns: limit,
5659
5690
  onDeleteSession: cleanup
5660
- }, signal, tracked));
5691
+ }, signal, tracked, keepIds));
5661
5692
  sessionId = opened.sessionId;
5662
5693
  sentFeed = opened.feed;
5663
5694
  sentChainKey = slotKey;
@@ -5698,6 +5729,7 @@ async function* streamWebCompletion(auth, params, transport = defaultTransport)
5698
5729
  const roundOk = complete || sawTerminal;
5699
5730
  lastRetireReason = void 0;
5700
5731
  for (const id of owned) {
5732
+ if (limit > 0 && keepIds.has(id) && !poisoned) continue;
5701
5733
  if (id === sessionId && roundOk && !poisoned && limit > 0) continue;
5702
5734
  lastRetireReason = id !== sessionId ? "extra-session" : poisoned ? "poisoned" : !roundOk ? "not-finished" : "rotated";
5703
5735
  retireSession(id);
@@ -10227,7 +10259,7 @@ async function checkForUpdate(current, fetchImpl) {
10227
10259
  * 兜底常量与 package.json 的一致性由 `tests/check-smoke.mjs` 守着,不会漂。
10228
10260
  */
10229
10261
  /** 与 package.json 保持一致的兜底版本(由测试保证不会漂)。 */
10230
- const FALLBACK_VERSION = "0.6.28";
10262
+ const FALLBACK_VERSION = "0.6.29";
10231
10263
  let cached;
10232
10264
  /** 本插件版本(如 `0.1.26`)。 */
10233
10265
  function pluginVersion() {