dsh-deepseek-web-login 0.6.26 → 0.6.27

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,54 @@
2
2
 
3
3
  本项目大致遵循语义化版本;日期为本地时间。
4
4
 
5
+ ## 0.6.27 — 2026-10-02
6
+
7
+ **修复:早先发过的图片被反复重挂在后面的每条消息上("我这轮没发图,网页端却又有图")。**
8
+
9
+ ### 取证过程(这次没有猜)
10
+
11
+ 1. 用户截图:那条 `哈哈哈哈` 消息上挂着两个图片框,但 DSH 里它**没有**图。
12
+ 2. 先确认"图是不是真实存在":写工具解 DSH 会话日志(`dev/scan-session-images.mjs`)——
13
+ `session.v4.jsonl.zstd` 是 **zstd 多帧**(一个 70KB 文件 34 帧),而 `zstdDecompressSync()`
14
+ **只解第一帧**,只拿到 session header ⇒ "含 image 的行数"永远是 0(**假阴性**)。
15
+ 逐帧解压后确认:会话里确实有 **2 条带图的用户消息**("还不错哎"、"这个能看见了吧")。
16
+ 3. 既然图是真的,问题就变成"**为什么它出现在不含图的那一轮**"—— 于是回到代码。
17
+
18
+ ### 根因
19
+
20
+ `sentRefIds`("服务端已见过哪些图"的账本)原本是**一个全局 Set + 一个 `sentRefIdsSession` 变量**:
21
+
22
+ ```js
23
+ if (sentRefIdsSession !== sessionId) { sentRefIds = new Set(); sentRefIdsSession = sessionId }
24
+ ```
25
+
26
+ 两个窗口交错时它们会互相覆盖:
27
+
28
+ 1. 窗口 A 的请求进来 → 记成 A
29
+ 2. 窗口 B 的请求进来(A 还没返回)→ **清空**,记成 B(A 的账没了)
30
+ 3. A 的下一轮进来 → 发现 `sentRefIdsSession` 不是自己 → **再清一次**
31
+
32
+ ⇒ A 里**早就发出去过**的图,每轮都被当成"没发过"⇒ 反复重发 ⇒ 网页端把它们挂到了后面每条消息上。
33
+
34
+ ### 修法
35
+
36
+ 账本改成 **`Map<sessionId, Set<key>>`**:每个会话各记各的,并发的窗口互不影响。
37
+ 会话退役(`retireSession`)与全量清理(`disposeSessionReuse`)时销掉对应条目,避免 Map 跟着开过的窗口一直涨。
38
+
39
+ ### 验证
40
+
41
+ - 新增用例:**两个窗口交错**(A 首轮 → B 首轮 → A 续轮 → B 续轮),
42
+ 断言各自的图都只发一次、续轮不再重发
43
+ - 变异反向验证:把账本改回"会话一变就清空" ⇒ 用例变红、`rc=1`
44
+ - 另修两条产物断言(它们引用的是已被替换的旧变量形态;意图不变)
45
+ - `tsc --noEmit`、`npm run test`(60/60)、`check-smoke`、`check-bundle` 全通过
46
+
47
+ ### 新增诊断工具
48
+
49
+ - `dev/scan-session-images.mjs`:扫一个会话日志,报"历史里到底有没有图片"
50
+ - `dev/dump-image-messages.mjs`:把带图的消息连同文本一起打出来(判断图该挂在哪一轮)
51
+ - `dev/trace-session-images.mjs`:列出图片出现在日志的第几行
52
+
5
53
  ## 0.6.26 — 2026-10-01
6
54
 
7
55
  **修复:新开一个窗口、只发一句话,网页端却显示「修改」+ `2 / 2`。**
package/lib/index.js CHANGED
@@ -5117,13 +5117,26 @@ let lastRetireReason;
5117
5117
  * 服务端**已知**的图片 file_id(0.1.83):本会话内已经随请求发出去过的那批。
5118
5118
  *
5119
5119
  * 只用于链式投喂:走 `chained`(有父链)时服务端会回溯历史、那张图已经在它的上下文里,
5120
- * 于是每轮重带整批是纯冗余 —— 真机实测见 tests/probe-image-chain.mjs(两张不同布局的图、
5121
- * 第二轮都不带 `ref_file_ids`,仍都答对四个角)。
5120
+ * 于是每轮重带整批是纯冗余 —— 真机实测见 tests/probe-image-chain.mjs。
5122
5121
  * 会话换掉就整批作废:新会话没有那份历史。
5122
+ *
5123
+ * 🔴 0.6.27:改成**按会话分别记账**(原来是「一个全局 Set + 一个 sentRefIdsSession 变量」)。
5124
+ *
5125
+ * 旧写法在**多窗口并发**下会互相覆盖,链条是:
5126
+ * ① 窗口 A 的请求进来 → `sentRefIdsSession !== A` ⇒ `sentRefIds` **清空**、记成 A
5127
+ * ② 窗口 B 的请求进来(A 还没回来)→ `sentRefIdsSession !== B` ⇒ **又清空**(A 的账没了)
5128
+ * ③ A 的下一轮进来 → `sentRefIdsSession` 是 B ⇒ **再清一次**
5129
+ * ⇒ A 里"早就发出去过"的图每轮都被当成没发过 ⇒ **反复重发**,
5130
+ * 网页端于是把那些图挂到了**后面的每一条消息**上。
5131
+ *
5132
+ * 用户现场(2026-10-02):「我这轮没发图片,网页端却又有图片了」——
5133
+ * 那张图确实是他早先发的,但它被重复挂到了不含图的那几条消息上。
5123
5134
  */
5124
- let sentRefIds = /* @__PURE__ */ new Set();
5125
- /** `sentRefIds` 归属的会话 id(见上)。 */
5126
- let sentRefIdsSession;
5135
+ const sentRefIdsBySession = /* @__PURE__ */ new Map();
5136
+ /** 会话退役时连它的图片账一起销掉(否则 Map 会跟着开过的窗口一直涨)。 */
5137
+ function dropSentRefIds(sessionId) {
5138
+ sentRefIdsBySession.delete(sessionId);
5139
+ }
5127
5140
  /**
5128
5141
  * 上一次上报过的决策原因(0.1.63)。链式投喂的决策每轮都在做,
5129
5142
  * 但"原因"通常连续几百轮都不变 —— 只在**变化时**上报,日志才不会被刷满,
@@ -5258,6 +5271,7 @@ function evictIdleSlots() {
5258
5271
  for (const slot of idle.slice(0, reuseSlots.size - 6)) {
5259
5272
  reuseSlots.delete(slot.key);
5260
5273
  contextChains.delete(slot.key);
5274
+ dropSentRefIds(slot.sessionId);
5261
5275
  if (lastChainKey === slot.key) lastChainKey = void 0;
5262
5276
  }
5263
5277
  }
@@ -5266,9 +5280,11 @@ function retireSession(sessionId) {
5266
5280
  if (!sessionId) {
5267
5281
  reuseSlots.clear();
5268
5282
  contextChains.clear();
5283
+ sentRefIdsBySession.clear();
5269
5284
  lastChainKey = void 0;
5270
5285
  return;
5271
5286
  }
5287
+ dropSentRefIds(sessionId);
5272
5288
  for (const [key, slot] of [...reuseSlots]) if (slot.sessionId === sessionId) reuseSlots.delete(key);
5273
5289
  for (const [key, chain] of [...contextChains]) if (chain.sessionId === sessionId) contextChains.delete(key);
5274
5290
  if (lastChainKey !== void 0 && !contextChains.has(lastChainKey)) lastChainKey = void 0;
@@ -5289,8 +5305,7 @@ function disposeSessionReuse() {
5289
5305
  reuseSlots.clear();
5290
5306
  contextChains.clear();
5291
5307
  lastChainKey = void 0;
5292
- sentRefIds = /* @__PURE__ */ new Set();
5293
- sentRefIdsSession = void 0;
5308
+ sentRefIdsBySession.clear();
5294
5309
  if (slots.length === 0) return void 0;
5295
5310
  for (const slot of slots) try {
5296
5311
  slot.cleanup?.(slot.sessionId);
@@ -5379,10 +5394,7 @@ async function openCompletion(auth, params, signal, transport) {
5379
5394
  echoDropped: feed.echoDropped ?? 0
5380
5395
  });
5381
5396
  }
5382
- if (sentRefIdsSession !== sessionId) {
5383
- sentRefIds = /* @__PURE__ */ new Set();
5384
- sentRefIdsSession = sessionId;
5385
- }
5397
+ const sentRefIds = sentRefIdsBySession.get(sessionId) ?? /* @__PURE__ */ new Set();
5386
5398
  const askedRefItems = (params.refFileIds ?? []).map((id, index) => ({
5387
5399
  id,
5388
5400
  key: params.refKeys?.[index] ?? id
@@ -5440,6 +5452,7 @@ async function openCompletion(auth, params, signal, transport) {
5440
5452
  const contentType = String(resp.headers.get("content-type") ?? "");
5441
5453
  if (contentType.includes("text/event-stream")) {
5442
5454
  for (const item of refItemsToSend) sentRefIds.add(item.key);
5455
+ sentRefIdsBySession.set(sessionId, sentRefIds);
5443
5456
  return {
5444
5457
  sessionId,
5445
5458
  resp,
@@ -10139,7 +10152,7 @@ async function checkForUpdate(current, fetchImpl) {
10139
10152
  * 兜底常量与 package.json 的一致性由 `tests/check-smoke.mjs` 守着,不会漂。
10140
10153
  */
10141
10154
  /** 与 package.json 保持一致的兜底版本(由测试保证不会漂)。 */
10142
- const FALLBACK_VERSION = "0.6.26";
10155
+ const FALLBACK_VERSION = "0.6.27";
10143
10156
  let cached;
10144
10157
  /** 本插件版本(如 `0.1.26`)。 */
10145
10158
  function pluginVersion() {