dsh-deepseek-web-login 0.6.32 → 0.6.34
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 +83 -0
- package/lib/index.js +75 -4
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,89 @@
|
|
|
2
2
|
|
|
3
3
|
本项目大致遵循语义化版本;日期为本地时间。
|
|
4
4
|
|
|
5
|
+
## 0.6.34 — 2026-10-02
|
|
6
|
+
|
|
7
|
+
**修复:工具返回被当成「用户说的话」发给模型 —— 所以它回「收到,…」而不是回答问题。**
|
|
8
|
+
|
|
9
|
+
用户现场(原话):「提示词还是把答案直接告诉模型了,难怪最后模型的回答有『收到,…』」
|
|
10
|
+
(附分享页 + 截图:问「你知道现在几点嘛」→ 答「**收到**,2026-10-02 星期五 18:54,傍晚快七点了。」)
|
|
11
|
+
|
|
12
|
+
先排除掉已修的那一半:0.6.33 确实在跑(日志已有 `firstDiff`),而且**增量已经正常** ——
|
|
13
|
+
`18:51:37 chars=17` / `18:54:09 chars=11` / `18:54:26 chars=14` / `18:54:30 chars=31`,
|
|
14
|
+
对比修之前的 **40193**。所以这次的「答案在提示词里」不是回声。
|
|
15
|
+
|
|
16
|
+
**证据链**(`dev/dump-turn-input.mjs` 读 DSH 自己的会话日志 + 分享页逐行核对):
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
turn 3 step 1: user 「你知道现在几点嘛」
|
|
20
|
+
assistant 「pwsh」 ← 工具调用
|
|
21
|
+
step 2: assistant 「收到,2026-10-02 星期五 18:54,傍晚快七点了。」
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**中间那条工具返回,在 DSH 的消息列表里根本不存在**(不是 `tool-result` 块)。
|
|
25
|
+
而分享页里渲染出来的是 `User: 2026-10-02 18:54:28 星期五` ——
|
|
26
|
+
**`User: ` 前缀只可能由我们的"纯文本分支"产生** ⇒ DSH 把 pwsh 的输出当成一条**普通 user 文本**交给我们。
|
|
27
|
+
|
|
28
|
+
于是模型读到的是"用户告诉了我时间"。它自己的思考原话(分享页里看得到):
|
|
29
|
+
|
|
30
|
+
> 「用户告诉我时间。不需要工具。」
|
|
31
|
+
> 「不过我应该核对一下——**我没拿到工具结果,用户直接给了时间**。那就接受。」
|
|
32
|
+
|
|
33
|
+
⇒ 回答从"回答几点"变成"收到,…"。**用户看到的"答案被喂进去"是真的,但它的根因不是重发,是错标。**
|
|
34
|
+
|
|
35
|
+
**修法**:`serializePromptParts` 认出这种形状 —— **assistant 发过工具调用 ⇒ 紧随其后的那条 user 文本
|
|
36
|
+
按构造只能是工具返回**(agent 循环里必须先有工具返回才轮到下一条用户输入),于是标成
|
|
37
|
+
`[Tool Result for <id>]`,不再渲染成 `User: …`。
|
|
38
|
+
一对多时逐个配对(一个调用一条返回);数量对不齐就合并成一条(宁可少一层对应关系,
|
|
39
|
+
也不能让它看起来像用户说的话);**带图片的 user 消息不误标**(那是用户真的发的)。
|
|
40
|
+
|
|
41
|
+
**新增 5 条用例**(`check-context-feed` 65 项):必须标成工具返回 / 没有工具调用时不许误标 /
|
|
42
|
+
多调用多返回逐个配对 / 数量不齐时合并 / 带图消息不误标。**一处变异确认变红**(关掉这条判据 → 3 红)。
|
|
43
|
+
|
|
44
|
+
## 0.6.33 — 2026-10-02
|
|
45
|
+
|
|
46
|
+
**修复:断链重发时把模型自己刚说的话又喂了回去 —— 一句话走掉 4 万字符。**
|
|
47
|
+
|
|
48
|
+
用户现场(原话):「我就说了个『哇哦帅气』,结果网页版发送的提示词怎么这么长???
|
|
49
|
+
以及我问他天气怎么样,结果我看提示词,相当于我发给网页版已知的答案,然后模型在回复我!」
|
|
50
|
+
|
|
51
|
+
日志一条就够(`feed-decisions.jsonl`):
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
18:33:09 not-appended chain=15 ent=15 tail=false chars=40193 head=14594
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**5 个字的输入,发出去 40193 字符。** 网页端的用户气泡里赫然是"上一句天气回答 + 哇哦帅气"。
|
|
58
|
+
|
|
59
|
+
**根因**:`replay`(链还在、但不能只发增量)发的是**整份历史**,而历史里全是 `Assistant: …` 条目
|
|
60
|
+
—— 也就是模型自己刚说过的那段回答。**增量路径早就有"剔掉模型回声"这条规则**(0.6.17 为同一个
|
|
61
|
+
投诉加的,原话也是"完全没必要"),**但 `replay` 这条路没走它** —— 当时注释写的是"只在增量里剔,
|
|
62
|
+
全量需要完整对话(从零重述)"。**这个理由在 replay 上不成立**:走到 replay 说明链还在
|
|
63
|
+
(同会话、同账号),那条会话里固定头和历史都还在,根本不是"从零重述"。
|
|
64
|
+
|
|
65
|
+
**改法**:`replay` 按**代价从小到大**挑,第一个可用的就用:
|
|
66
|
+
|
|
67
|
+
1. **从第一个分歧点起的条目**(剔回声)—— 通常就是这一句新消息;
|
|
68
|
+
2. 整份条目(剔回声)—— 分歧点算不出来时退一步;
|
|
69
|
+
3. `transcript`(0.6.32:至少省掉固定头);
|
|
70
|
+
4. 整份 `full` —— 什么都算不出来时的兜底。
|
|
71
|
+
|
|
72
|
+
每档都要求"非空且不超预算";头**变了**时只有第 4 档合法(新头从没发过)。
|
|
73
|
+
上方那次的 40193 字符,在新逻辑下是 **`User: 哇哦帅气`**。
|
|
74
|
+
|
|
75
|
+
**顺带把"为什么断链"变成一个可读的事实**:以前只有 `tailSame=false`,它只说"链尾变了",
|
|
76
|
+
不说**是哪一条、从哪儿变的** —— 于是每次排查都只能猜。现在每轮多记 **`firstDiff`**
|
|
77
|
+
(和链从第几个条目开始不一样)。这一轮假如早就有它,一眼就能看到是哪一条被改了。
|
|
78
|
+
|
|
79
|
+
**用例**:`check-context-feed` 60 项(新增 7 条:最小档 / 回声必须剔 / 头变了必须整份 /
|
|
80
|
+
退到整份剔回声 / 不许发空串 / 四档都不行时兜底 / 历史变短也走最小档),
|
|
81
|
+
`check-context-chain` 19 项(端到端断言"只发分歧点之后的条目、且**不许含固定头**",
|
|
82
|
+
并守 `firstDiff` / `promptChars` / `headChars` 三个留痕字段)。
|
|
83
|
+
**两处变异确认变红**:把 replay 改回总是发 full(9 红)、去掉回声过滤(4 红)。
|
|
84
|
+
|
|
85
|
+
**顺带修的**:三条**绑语法形态**的产物断言与两条老用例,在行为变更后失配 —— 按"改测同一意图"
|
|
86
|
+
重写(例如"历史被改写 ⇒ 退回全量"改成"⇒ 只发分歧点之后的条目"),没有把行为改回去。
|
|
87
|
+
|
|
5
88
|
## 0.6.32 — 2026-10-02
|
|
6
89
|
|
|
7
90
|
**修复:链式投喂"退回全量重发"时会重发整份固定头(约 6.35 万字符)—— 那一段是纯重复。**
|
package/lib/index.js
CHANGED
|
@@ -1892,6 +1892,19 @@ function serializePromptParts(options) {
|
|
|
1892
1892
|
const toolSection = buildToolSection(options.tools, toolBudget);
|
|
1893
1893
|
const protocol = toolSection ? `\n\n${protocolText}${toolSection}` : "";
|
|
1894
1894
|
const lines = [];
|
|
1895
|
+
/**
|
|
1896
|
+
* 上一条 assistant 消息里的工具调用 id —— 用来把**紧随其后的那条 user 文本**认成工具返回。
|
|
1897
|
+
*
|
|
1898
|
+
* 🔴 2026-10-02 现场(用户报「提示词把答案直接告诉模型了,所以它回『收到,…』」):
|
|
1899
|
+
* DSH 执行 `pwsh` 之后,把工具输出当成一条**普通的 user 文本消息**交给我们,于是我们照实
|
|
1900
|
+
* 渲染成 `User: 2026-10-02 18:54:28 星期五` —— 模型读到的是"用户告诉了我时间",
|
|
1901
|
+
* 它自己的思考原话就是:「**我没拿到工具结果,用户直接给了时间**。那就接受。」
|
|
1902
|
+
* 回答于是从"回答几点"变成"收到,…" —— 看着就像在复述一个已知答案。
|
|
1903
|
+
*
|
|
1904
|
+
* 判据为什么成立:在 agent 循环里,assistant 发出工具调用之后**必须先有工具返回**,
|
|
1905
|
+
* 才轮得到下一条用户输入。所以紧跟其后的那条 user 文本,按构造只能是工具返回。
|
|
1906
|
+
*/
|
|
1907
|
+
let pendingToolCallIds = [];
|
|
1895
1908
|
for (const message of options.messages ?? []) {
|
|
1896
1909
|
if (!message || typeof message !== "object") continue;
|
|
1897
1910
|
const blocks = Array.isArray(message.content) ? message.content : [];
|
|
@@ -1903,6 +1916,7 @@ function serializePromptParts(options) {
|
|
|
1903
1916
|
if (message.role === "assistant") {
|
|
1904
1917
|
const text = flattenText(blocks).join("");
|
|
1905
1918
|
const renderedCalls = renderToolCalls(blocks);
|
|
1919
|
+
pendingToolCallIds = renderedCalls ? blocks.filter((block) => block?.type === "tool-call").map((block) => String(block.toolCallId ?? block.id ?? "")) : [];
|
|
1906
1920
|
if (renderedCalls) lines.push(`Assistant: ${renderedCalls}`);
|
|
1907
1921
|
else if (text.trim()) lines.push(`Assistant: ${text}`);
|
|
1908
1922
|
continue;
|
|
@@ -1911,6 +1925,15 @@ function serializePromptParts(options) {
|
|
|
1911
1925
|
const text = flattenText(blocks.filter((block) => block?.type !== "tool-result")).join("");
|
|
1912
1926
|
const imageMarks = blockImageMarks(blocks, options.keptImageKeys);
|
|
1913
1927
|
const images = imageMarks.length;
|
|
1928
|
+
if (toolResults.length === 0 && pendingToolCallIds.length > 0 && images === 0 && text.trim()) {
|
|
1929
|
+
const textBlocks = blocks.filter((block) => block?.type === "text" && typeof block.text === "string" && block.text.trim()).map((block) => block.text);
|
|
1930
|
+
const ids = pendingToolCallIds;
|
|
1931
|
+
pendingToolCallIds = [];
|
|
1932
|
+
if (textBlocks.length === ids.length && ids.length > 0) for (let i = 0; i < ids.length; i += 1) lines.push(`[Tool Result for ${ids[i]}]\n${textBlocks[i]}`);
|
|
1933
|
+
else lines.push(`[Tool Result for ${ids.filter(Boolean).join(", ")}]\n${text}`);
|
|
1934
|
+
continue;
|
|
1935
|
+
}
|
|
1936
|
+
pendingToolCallIds = [];
|
|
1914
1937
|
if (text.trim() || toolResults.length === 0 && images === 0 || images > 0) {
|
|
1915
1938
|
const imageNote = imageMarks.length > 0 ? `\n${imageMarks.join(" ")}` : "";
|
|
1916
1939
|
lines.push(`User: ${text}${imageNote}`);
|
|
@@ -3432,6 +3455,16 @@ function isAssistantTranscriptEntry(entry) {
|
|
|
3432
3455
|
return typeof entry === "string" && /^\s*Assistant:/.test(entry);
|
|
3433
3456
|
}
|
|
3434
3457
|
/**
|
|
3458
|
+
* 两个条目序列**从哪个下标开始不一样**(完全相同则返回较短的公共长度)。
|
|
3459
|
+
*
|
|
3460
|
+
* 用途:链没续上时(`not-appended`)算"服务端还没见过的部分"从哪里开始。
|
|
3461
|
+
*/
|
|
3462
|
+
function firstDifference(prev, next) {
|
|
3463
|
+
const shared = Math.min(prev.length, next.length);
|
|
3464
|
+
for (let i = 0; i < shared; i += 1) if (prev[i] !== next[i]) return i;
|
|
3465
|
+
return shared;
|
|
3466
|
+
}
|
|
3467
|
+
/**
|
|
3435
3468
|
* 这一轮要不要**换一个干净会话**。
|
|
3436
3469
|
*
|
|
3437
3470
|
* 🔴 2026-09-30 实测的坑(用户报"换个窗口聊天就把上下文清理一次"):
|
|
@@ -3566,9 +3599,9 @@ function decideFeed(input) {
|
|
|
3566
3599
|
* 用户在网页端看到的现象(同一段 Tool Calling Protocol 出现两次)就是它。
|
|
3567
3600
|
* ⚠️ 只有 `head-changed` 必须重发头(新头没给过),所以那条路径**不**走这个优化。
|
|
3568
3601
|
*/
|
|
3569
|
-
const replay = (reason,
|
|
3602
|
+
const replay = (reason, options = {}) => {
|
|
3570
3603
|
return {
|
|
3571
|
-
prompt:
|
|
3604
|
+
prompt: replayPrompt(options),
|
|
3572
3605
|
parentMessageId: chain.parentId,
|
|
3573
3606
|
next: {
|
|
3574
3607
|
head,
|
|
@@ -3579,13 +3612,50 @@ function decideFeed(input) {
|
|
|
3579
3612
|
reason
|
|
3580
3613
|
};
|
|
3581
3614
|
};
|
|
3615
|
+
/**
|
|
3616
|
+
* 重发时到底发哪一段。**按代价从小到大试,第一个可用的就用**,全都不行才退回整份。
|
|
3617
|
+
*
|
|
3618
|
+
* 🔴 2026-10-02 第二轮的现场(用户报"我只说了『哇哦帅气』,发出去的提示词怎么这么长"):
|
|
3619
|
+
* `not-appended` 走这条路时发的是**整份历史**,而历史里全是 `Assistant: …` 条目 ——
|
|
3620
|
+
* 也就是**模型自己刚说过的那段回答**。于是:① 5 个字的输入发出去 40193 字符;
|
|
3621
|
+
* ② 网页端的用户气泡里赫然是"上一句回答 + 我的新消息",看着就像"我把答案喂给它让它复述"。
|
|
3622
|
+
* 增量路径**早就有**"剔掉模型回声"这条规则(0.6.17 为同样的投诉加的),**这条路没走它** ——
|
|
3623
|
+
* 当时注释还写着"只在增量里剔",理由站在"全量是从零重述"那边;但 replay 根本不是从零重述:
|
|
3624
|
+
* 链还在(同会话、同账号),该会话里什么都有。所以这里必须按**同一套网**来。
|
|
3625
|
+
*
|
|
3626
|
+
* 四档(每档都要求"非空且不超预算"):
|
|
3627
|
+
* ① 从**第一个分歧点**起的条目(剔回声)—— 最小,通常就是这一句新消息;
|
|
3628
|
+
* ② 整份条目(剔回声)—— 分歧点算不出来时退一步,至少不再把模型的话喂回去;
|
|
3629
|
+
* ③ `transcript`(0.6.32:省掉固定头);
|
|
3630
|
+
* ④ 整份 `full` —— 什么都算不出来时的兜底。
|
|
3631
|
+
* ⚠️ 头**变了**时只有 ④ 合法(新头从没发过)。
|
|
3632
|
+
*/
|
|
3633
|
+
const replayPrompt = ({ headUnchanged = false, from }) => {
|
|
3634
|
+
if (headUnchanged) {
|
|
3635
|
+
const budget = Number.isFinite(input.maxChars) ? input.maxChars : Number.POSITIVE_INFINITY;
|
|
3636
|
+
const usable = (text) => text.trim().length > 0 && text.length <= budget;
|
|
3637
|
+
if (from !== void 0 && from < entries.length) {
|
|
3638
|
+
const tail = entries.slice(from);
|
|
3639
|
+
const withoutEcho = tail.filter((line) => !isAssistantTranscriptEntry(line));
|
|
3640
|
+
const tailText = (withoutEcho.length > 0 ? withoutEcho : tail).join("\n\n");
|
|
3641
|
+
if (usable(tailText)) return tailText;
|
|
3642
|
+
}
|
|
3643
|
+
const noEcho = entries.filter((line) => !isAssistantTranscriptEntry(line)).join("\n\n");
|
|
3644
|
+
if (usable(noEcho)) return noEcho;
|
|
3645
|
+
if (typeof input.transcript === "string" && usable(input.transcript)) return input.transcript;
|
|
3646
|
+
}
|
|
3647
|
+
return full;
|
|
3648
|
+
};
|
|
3582
3649
|
if (!input.reused) return detach("new-session");
|
|
3583
3650
|
const chain = input.chain;
|
|
3584
3651
|
if (!chain) return detach("no-chain");
|
|
3585
3652
|
if (chain.sessionId !== input.sessionId) return detach("session-changed");
|
|
3586
3653
|
if (chain.accountKey !== input.accountKey) return detach("account-changed");
|
|
3587
3654
|
if (chain.head !== head) return replay("head-changed");
|
|
3588
|
-
if (!canExtendChain(chain.entries, entries)) return replay("not-appended",
|
|
3655
|
+
if (!canExtendChain(chain.entries, entries)) return replay("not-appended", {
|
|
3656
|
+
headUnchanged: true,
|
|
3657
|
+
from: firstDifference(chain.entries, entries)
|
|
3658
|
+
});
|
|
3589
3659
|
const appended = entries.slice(chain.entries.length);
|
|
3590
3660
|
const meaningful = appended.some((line) => isContinuationCue(line)) ? appended : appended.filter((line) => !isAssistantTranscriptEntry(line));
|
|
3591
3661
|
const echoDropped = meaningful.length > 0 ? appended.length - meaningful.length : 0;
|
|
@@ -5501,6 +5571,7 @@ async function openCompletion(auth, params, signal, transport, keepIds) {
|
|
|
5501
5571
|
entriesLen: currentEntries ? currentEntries.length : null,
|
|
5502
5572
|
promptChars: feed.prompt.length,
|
|
5503
5573
|
headChars: params.promptParts ? String(params.promptParts.head ?? "").length : null,
|
|
5574
|
+
firstDiff: !chainEntries || !currentEntries ? null : firstDifference(chainEntries, currentEntries),
|
|
5504
5575
|
tailSame: !chainEntries || !currentEntries ? null : currentEntries.length <= chainEntries.length ? false : currentEntries[chainEntries.length - 1] === chainEntries[chainEntries.length - 1]
|
|
5505
5576
|
});
|
|
5506
5577
|
}
|
|
@@ -10371,7 +10442,7 @@ async function checkForUpdate(current, fetchImpl) {
|
|
|
10371
10442
|
* 兜底常量与 package.json 的一致性由 `tests/check-smoke.mjs` 守着,不会漂。
|
|
10372
10443
|
*/
|
|
10373
10444
|
/** 与 package.json 保持一致的兜底版本(由测试保证不会漂)。 */
|
|
10374
|
-
const FALLBACK_VERSION = "0.6.
|
|
10445
|
+
const FALLBACK_VERSION = "0.6.34";
|
|
10375
10446
|
let cached;
|
|
10376
10447
|
/** 本插件版本(如 `0.1.26`)。 */
|
|
10377
10448
|
function pluginVersion() {
|