dsh-deepseek-web-login 0.6.37 → 0.6.38

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,70 @@
2
2
 
3
3
  本项目大致遵循语义化版本;日期为本地时间。
4
4
 
5
+ ## 0.6.38 — 2026-10-03
6
+
7
+ 工具调用协议从**裸 JSON** 改成**围栏包裹**(` ```dsh-tool `),根治"标记漏到正文"这一类问题。
8
+
9
+ ### 起因:用户拿 cuckoo 的分享页来对比
10
+
11
+ 用户问「为什么 cuckoo 不会出现 DSML 等内容,他不也是工具调用」。取它的分享页逐项统计:
12
+
13
+ | | cuckoo | 我们(改之前) |
14
+ |---|---|---|
15
+ | 调用格式 | ` ```cuckoo ` **围栏代码块** | `{"tool_calls":[…]}` **裸 JSON** |
16
+ | 页面里 `DSML` | **0** | 出现过 |
17
+ | 页面里 `tool_calls` | **0** | 出现过 |
18
+ | 页面里 `invoke` / `parameters` | **0** | 出现过 |
19
+
20
+ **根因就在格式本身**:围栏**自带语法边界**,模型即使在围栏外多写一句废话也进不了正文;
21
+ 裸 JSON 没有边界 —— 模型在思考里写 JSON、或者在 JSON 前后多写一个字,整段直接成为正文。
22
+ 这也是为什么我们的规则 6 早就写着"禁止 XML 标记"却仍然漏:**禁令能约束内容,约束不了边界。**
23
+
24
+ DSH 那侧**不用改**:我们把调用转成 `tool-call` 事件交给它,它只看事件、不看文本。
25
+
26
+ ### 改动
27
+
28
+ **提示词**(两份,`TOOL_PROTOCOL_INSTRUCTIONS` 与串行版必须逐字一致):
29
+
30
+ - 示例与开头句改成 ` ```dsh-tool ` 包裹
31
+ - rule 6 改为「必须用围栏包裹;不包裹的 JSON 与 XML 标记都会泄漏」
32
+
33
+ **解析器**(`protocol.ts`):
34
+
35
+ - `stripCallFence()`:调用块前后**无条件**剥掉所有围栏(含 ```json 这类模型自选语言名)。
36
+ 进入该函数的文本都已确定属于调用块,所以放宽是安全的。
37
+ - `CALL_FENCE_OPEN_RE` + `sawCallFence`:在 `drain()` 里对**普通正文**判围栏。
38
+ ⚠️ 只认带 `dsh-tool` 的开栏,且**不**加"后面不是闭栏"的负向断言 ——
39
+ 逐字符分块时开栏先到(JSON 还没来),那条断言会把它误判成闭栏而放行出去。
40
+ - 闭栏只在本流见过开栏时才剥(`sawCallFence` 门控)。
41
+
42
+ **没有动**:`extractBalancedJson` 要求 `{` 在开头这件事(靠捕获时先剥 head 解决)、
43
+ DSH 侧的调用解析、裸 JSON 的支持路径(模型不听话时的兜底必须留着)。
44
+
45
+ ### 期间踩到的两个坑
46
+
47
+ **① 放宽判据吃掉正常内容(真实回归)。** 一度无条件剥所有 ``` ⇒ `check-auto-continue` N03
48
+ (正文里的 ```xml 示例)与 `check-tools-section` 各变红。曾试图用"独占一行的裸闭栏"补救,
49
+ 逐字符分块时闭栏还没成行、照样漏 user 的代码块。最终形态:**普通正文只认带 `dsh-tool` 的开栏**,
50
+ 闭栏靠 `sawCallFence` 配对。
51
+
52
+ **② 围栏示例有体积成本。** `maxChars=5000` 的极小预算下,协议头占 3304/5000(66%),
53
+ 新增的围栏示例把工具定义挤掉了(`check-tools-section` 变红)。压掉 rule 6 与 rule 8 的冗余措辞后
54
+ 协议头 **2869 字符**(比改动前只多 55),工具定义恢复。`logic-test` 里加了一条上限断言
55
+ (`< 3000`)防它悄悄涨回去。
56
+
57
+ ### 验证
58
+
59
+ - `logic-test` 62 → **67 项**(5 条新围栏用例:整块/逐字符/按行三种分块 × 带散文、
60
+ 正常代码块不许被剥、裸 JSON 兜底仍可用、协议指令必须要求围栏 + 体积上限)
61
+ - `dev/probe-fenced-protocol.mjs`:7 种形态全部"调用提取成功 + 零泄漏"
62
+ - DSML 矩阵 144/144 仍全拦得住
63
+ - **四处变异手工确认打红**(脚本里的 `execFileSync` 在本机不可靠,变异写不进去 ⇒ 假"全绿"):
64
+ 开栏不扣留 → 2 红、flush 不剥闭栏 → 2 红、head 不剥围栏 → 1 红、`stripCallFence` 恒等 → 2 红
65
+ - 又抓到一次**虚守卫**:提示词退回裸 JSON 时用例仍全绿(只查了 `includes('dsh-tool')`,
66
+ 而围栏示例行里也有这个词)。补上"必须出现 `inside a fenced code block`"后变异正确打红。
67
+ - `tsc` / smoke / 产物 / **64/64** 全过,真实留痕未污染
68
+
5
69
  ## 0.6.37 — 2026-10-03
6
70
 
7
71
  修 DSML 标记泄漏:`dsml-` 连字符变体**带空格**时整块原样上屏(用户分享页现场)。
package/lib/index.js CHANGED
@@ -1449,22 +1449,24 @@ const HEAD_RATIO = .62;
1449
1449
  const PROTOCOL_SLACK_CHARS = 96;
1450
1450
  const TOOL_PROTOCOL_INSTRUCTIONS = `# Tool Calling Protocol
1451
1451
 
1452
- You can call tools to complete the user's task. When you need a tool, output ONLY a single JSON object, with no other text before or after it:
1452
+ You can call tools to complete the user's task. When you need a tool, output a single JSON object inside a fenced code block, with no other text before or after it:
1453
1453
 
1454
+ \`\`\`dsh-tool
1454
1455
  {"tool_calls":[{"name":"<tool-name>","arguments":{<json-arguments>}}]}
1456
+ \`\`\`
1455
1457
 
1456
1458
  Rules:
1457
1459
  1. Put every tool you want to run in the "tool_calls" array (usually exactly one; a batch is allowed).
1458
- 2. Stop immediately after that JSON object. The runner executes the call(s) and returns the results to you as the next message.
1460
+ 2. Stop immediately after the closing fence. The runner executes the call(s) and returns the results to you as the next message.
1459
1461
  3. Never fabricate, guess, or simulate tool output — always wait for the real result.
1460
1462
  4. When no tool is needed, answer normally in plain text and do NOT emit that JSON.
1461
1463
  5. "arguments" must be valid JSON (double-quoted strings, no trailing commas). When a value is a Windows path, escape backslashes as \\\\ (e.g. "C:\\\\Users\\\\me"); an unescaped single backslash makes the whole object unparsable. Close every brace: the call object and its "arguments" object each need their OWN closing "}" — one missing "}" makes the whole batch unparsable and the call will be discarded.
1462
1464
  5b. Two things break the JSON most often — check them before you emit:
1463
1465
  (a) QUOTES INSIDE A VALUE. A shell/PowerShell command very often contains double quotes, e.g. Get-ChildItem "$env:USERPROFILE\\.dsh". Every such inner double quote MUST be escaped as \\" inside the JSON string. An unescaped one ends the string early and discards the whole call.
1464
1466
  (b) LINE BREAKS INSIDE A VALUE. Never put a real line break inside a string; write \\n instead. When a command needs several statements, join them with ";" on ONE line, or use \\n escapes — do not paste them as actual newlines. Prefer single quotes inside commands to reduce escaping.
1465
- 6. Do NOT use XML/HTML-like markup for tool calls: no angle-bracket wrapper tags (no <tool_calls>, <invoke>, <parameter>), and none of the private delimiter-prefixed variants some DeepSeek surfaces use. The JSON object above is the ONLY accepted format. Markup is not just ignored — it leaks into the visible transcript (and into the web conversation) as broken output.
1467
+ 6. Always wrap the JSON in a \`\`\`dsh-tool fence (see the example above). Do NOT use XML/HTML-like markup instead: no angle-bracket wrapper tags (no <tool_calls>, <invoke>, <parameter>), nor the private delimiter-prefixed variants some DeepSeek surfaces use. An unfenced object or any such markup leaks into the visible transcript and into the web conversation.
1466
1468
  7. Always answer in the same language the user writes in (these instructions are English only for precision; the JSON itself is language-neutral).
1467
- 8. NEVER reproduce the transcript. Do not restate previous turns, "[Tool Result …]" blocks, tool output, or the current prompt. Emit ONLY the calls you want to run right now. A payload that replays earlier calls or embeds tool results is discarded and costs a retry — measured case: a model emitted 15 replayed calls inside one 8152-char payload, and every one of them had to be thrown away.
1469
+ 8. NEVER reproduce the transcript. Do not restate previous turns, "[Tool Result …]" blocks, tool output, or the current prompt. Emit ONLY the calls you want to run right now. A payload that replays earlier calls or embeds tool results is discarded and costs a retry.
1468
1470
  9. Keep each batch SMALL — at most 3 calls, and prefer exactly 1. If you need more, send them in successive steps. Long payloads are the ones that most often come out malformed.
1469
1471
  10. Each call must be able to run on its own: no shared shell variables across calls, no dependence on another call in the same batch.`;
1470
1472
  /**
@@ -1482,22 +1484,24 @@ Rules:
1482
1484
  */
1483
1485
  const SERIAL_TOOL_PROTOCOL_INSTRUCTIONS = `# Tool Calling Protocol
1484
1486
 
1485
- You can call tools to complete the user's task. When you need a tool, output ONLY a single JSON object, with no other text before or after it:
1487
+ You can call tools to complete the user's task. When you need a tool, output a single JSON object inside a fenced code block, with no other text before or after it:
1486
1488
 
1489
+ \`\`\`dsh-tool
1487
1490
  {"tool_calls":[{"name":"<tool-name>","arguments":{<json-arguments>}}]}
1491
+ \`\`\`
1488
1492
 
1489
1493
  Rules:
1490
1494
  1. Put exactly ONE tool in the "tool_calls" array — one call per message, never a batch.
1491
- 2. Stop immediately after that JSON object. The runner executes the call and returns the result to you as the next message.
1495
+ 2. Stop immediately after the closing fence. The runner executes the call and returns the result to you as the next message.
1492
1496
  3. Never fabricate, guess, or simulate tool output — always wait for the real result.
1493
1497
  4. When no tool is needed, answer normally in plain text and do NOT emit that JSON.
1494
1498
  5. "arguments" must be valid JSON (double-quoted strings, no trailing commas). When a value is a Windows path, escape backslashes as \\\\ (e.g. "C:\\\\Users\\\\me"); an unescaped single backslash makes the whole object unparsable. Close every brace: the call object and its "arguments" object each need their OWN closing "}" — one missing "}" makes the whole batch unparsable and the call will be discarded.
1495
1499
  5b. Two things break the JSON most often — check them before you emit:
1496
1500
  (a) QUOTES INSIDE A VALUE. A shell/PowerShell command very often contains double quotes, e.g. Get-ChildItem "$env:USERPROFILE\\.dsh". Every such inner double quote MUST be escaped as \\" inside the JSON string. An unescaped one ends the string early and discards the whole call.
1497
1501
  (b) LINE BREAKS INSIDE A VALUE. Never put a real line break inside a string; write \\n instead. When a command needs several statements, join them with ";" on ONE line, or use \\n escapes — do not paste them as actual newlines. Prefer single quotes inside commands to reduce escaping.
1498
- 6. Do NOT use XML/HTML-like markup for tool calls: no angle-bracket wrapper tags (no <tool_calls>, <invoke>, <parameter>), and none of the private delimiter-prefixed variants some DeepSeek surfaces use. The JSON object above is the ONLY accepted format. Markup is not just ignored — it leaks into the visible transcript (and into the web conversation) as broken output.
1502
+ 6. Always wrap the JSON in a \`\`\`dsh-tool fence (see the example above). Do NOT use XML/HTML-like markup instead: no angle-bracket wrapper tags (no <tool_calls>, <invoke>, <parameter>), nor the private delimiter-prefixed variants some DeepSeek surfaces use. An unfenced object or any such markup leaks into the visible transcript and into the web conversation.
1499
1503
  7. Always answer in the same language the user writes in (these instructions are English only for precision; the JSON itself is language-neutral).
1500
- 8. NEVER reproduce the transcript. Do not restate previous turns, "[Tool Result …]" blocks, tool output, or the current prompt. Emit ONLY the calls you want to run right now. A payload that replays earlier calls or embeds tool results is discarded and costs a retry — measured case: a model emitted 15 replayed calls inside one 8152-char payload, and every one of them had to be thrown away.
1504
+ 8. NEVER reproduce the transcript. Do not restate previous turns, "[Tool Result …]" blocks, tool output, or the current prompt. Emit ONLY the calls you want to run right now. A payload that replays earlier calls or embeds tool results is discarded and costs a retry.
1501
1505
  9. Do NOT batch. Emit one call, stop, and wait for its real result before you decide the next step. Needing several tools means several successive messages, one call each — the user has turned batching off for this session, so a multi-call array works against them.
1502
1506
  10. Unlike a batch, a call here MAY build on the previous step's result — read what came back and use it. That is the point of one-at-a-time. But never invent a result you have not received.`;
1503
1507
  /**
@@ -2006,9 +2010,30 @@ const DSML_PREFIX = `(?:${DSML_PREFIX_BODY})?`;
2006
2010
  /** 整组**必需**(给"必须有前缀才剥"的场合用,如 `invoke`)。 */
2007
2011
  const DSML_PREFIX_ONLY = `(?:${DSML_PREFIX_BODY})`;
2008
2012
  const XML_STARTER_RE = new RegExp(`<\\s*${DSML_PREFIX}(${WRAPPER_NAMES}|invoke)\\b`, "i");
2009
- /** 代码围栏收尾(模型常把调用块放进 ``` 里)。 */
2010
- const FENCE_TAIL_RE = /\n?[ \t]*```[a-zA-Z0-9]*[ \t]*\n?$/;
2011
2013
  const FENCE_HEAD_RE = /^[ \t]*\n?```[ \t]*\n?/;
2014
+ /** 捕获态里出现的围栏(含语言名、不要求行首)。 */
2015
+ const FENCE_ANY_HEAD_RE = /\n?[ \t]*```[ \t]*[a-zA-Z0-9_-]*[ \t]*\n?/;
2016
+ const CALL_FENCE_OPEN_RE = new RegExp(`\`\`\`[ \\t]*dsh-tool[ \\t]*\\n?`, "i");
2017
+ /**
2018
+ * 闭栏(可能带语言名)。**只在 sawCallFence 为真时使用** ——
2019
+ * 那表示开栏已经被剥掉,这个闭栏必定是调用块的另一半。
2020
+ *
2021
+ * ⚠️ 闭栏**不能**在 `drain()` 里按"位置"裸判(`/\n?```…$/` 那种):用户正常回答里的
2022
+ * 代码块闭栏也会命中,逐字符分块时必现(`check-auto-continue` N03 抓到过)。
2023
+ */
2024
+ const CALL_FENCE_TAIL_RE = /\n?[ \t]*```[ \t]*(?:[a-zA-Z0-9_-]*)[ \t]*\n?$/;
2025
+ /**
2026
+ * 剥掉**属于调用块**的围栏(0.6.38)。
2027
+ *
2028
+ * ⚠️ 这里可以**无条件剥所有围栏**(包括裸 ``` 和 ```json):进入本函数的文本都已经
2029
+ * 确定属于一个调用块 —— 捕获态的 buffer,或紧跟其后残留的部分。
2030
+ * 那个"不要误伤用户代码块"的约束在**别处**(`drain()` 里对**普通正文**的判据),
2031
+ * 那里用 `CALL_FENCE_OPEN_RE` + `sawCallFence` 把关。
2032
+ * 模型也常用 ```json 包裹调用(`logic-test` 有用例守着),那些围栏同样该剥。
2033
+ */
2034
+ function stripCallFence(text) {
2035
+ return text.replace(/```[ \t]*[a-zA-Z0-9_-]*[ \t]*\n?/g, "");
2036
+ }
2012
2037
  /**
2013
2038
  * 开/收标签前缀(宽容写法)。严格解析与宽容解析**必须共用同一套**,否则会出现
2014
2039
  * 「findXmlToolCallEnd 认得出收尾、parseXmlToolCalls 认不出 invoke」→ 整块被降级成正文泄漏。
@@ -2628,7 +2653,7 @@ function parseToolCallJson(json) {
2628
2653
  * 注意:不配平的截断仍由 structuralRepairCandidates 的安全闸门拒绝(宁可不执行半条命令)。
2629
2654
  */
2630
2655
  function parseSalvagedToolCallJson(buffer) {
2631
- const text = buffer.replace(FENCE_HEAD_RE, "");
2656
+ const text = buffer.replace(FENCE_ANY_HEAD_RE, "").replace(/^\s+/, "");
2632
2657
  const direct = parseToolCallJson(text);
2633
2658
  if (direct) return direct;
2634
2659
  const balanced = extractBalancedJson(text);
@@ -2702,6 +2727,14 @@ var ToolCallStreamFilter = class {
2702
2727
  capture = null;
2703
2728
  abandoned = null;
2704
2729
  knownTools;
2730
+ /**
2731
+ * 本次流里**我们剥掉过一个调用围栏的开栏**(0.6.38)。
2732
+ *
2733
+ * ⇒ 之后出现的闭栏必定是同一个调用块的收尾,可以安全剥掉。
2734
+ * ⚠️ 有了这个标记才敢剥闭栏:否则"正文里正常的代码块"会被误伤
2735
+ * (`check-auto-continue` N03 / `check-tools-section` 各有用例守着)。
2736
+ */
2737
+ sawCallFence = false;
2705
2738
  constructor(knownTools) {
2706
2739
  this.knownTools = knownTools;
2707
2740
  }
@@ -2736,7 +2769,8 @@ var ToolCallStreamFilter = class {
2736
2769
  else out.text += stripStrayToolMarkup(captured.buffer);
2737
2770
  this.capture = null;
2738
2771
  }
2739
- out.text += stripStrayToolMarkup(this.pending);
2772
+ const tailText = stripStrayToolMarkup(this.pending);
2773
+ out.text += this.sawCallFence ? tailText.replace(CALL_FENCE_TAIL_RE, "") : tailText;
2740
2774
  this.pending = "";
2741
2775
  if (this.abandoned) out.rejected = this.abandoned;
2742
2776
  return out;
@@ -2770,7 +2804,7 @@ var ToolCallStreamFilter = class {
2770
2804
  };
2771
2805
  else out.text += stripStrayToolMarkup(block);
2772
2806
  this.capture = null;
2773
- this.pending = captured.buffer.slice(end).replace(FENCE_HEAD_RE, "") + this.pending;
2807
+ this.pending = stripCallFence(captured.buffer.slice(end)) + this.pending;
2774
2808
  continue;
2775
2809
  }
2776
2810
  const balanced = extractBalancedJson(captured.buffer);
@@ -2790,7 +2824,7 @@ var ToolCallStreamFilter = class {
2790
2824
  if (calls) {
2791
2825
  out.calls.push(...calls);
2792
2826
  this.capture = null;
2793
- this.pending = captured.buffer.slice(balanced.end).replace(FENCE_HEAD_RE, "") + this.pending;
2827
+ this.pending = stripCallFence(captured.buffer.slice(balanced.end)) + this.pending;
2794
2828
  continue;
2795
2829
  }
2796
2830
  const head = captured.buffer.slice(0, balanced.end);
@@ -2811,9 +2845,9 @@ var ToolCallStreamFilter = class {
2811
2845
  const useXml = xmlIndex !== -1 && (jsonIndex === -1 || xmlIndex < jsonIndex);
2812
2846
  const index = useXml ? xmlIndex : jsonIndex;
2813
2847
  if (index !== -1) {
2814
- let head = this.pending.slice(0, index);
2815
- const fence = FENCE_TAIL_RE.exec(head);
2816
- if (fence) head = head.slice(0, fence.index);
2848
+ const rawHead = this.pending.slice(0, index);
2849
+ const head = stripCallFence(rawHead);
2850
+ if (head !== rawHead) this.sawCallFence = true;
2817
2851
  out.text += head;
2818
2852
  this.capture = {
2819
2853
  mode: useXml ? "xml" : "json",
@@ -2823,6 +2857,13 @@ var ToolCallStreamFilter = class {
2823
2857
  continue;
2824
2858
  }
2825
2859
  this.pending = stripStrayToolMarkup(this.pending);
2860
+ const ownFence = CALL_FENCE_OPEN_RE.exec(this.pending);
2861
+ if (ownFence) {
2862
+ this.sawCallFence = true;
2863
+ out.text += this.pending.slice(0, ownFence.index);
2864
+ this.pending = this.pending.slice(ownFence.index);
2865
+ return;
2866
+ }
2826
2867
  if (this.pending.length <= HOLD_BACK_CHARS) return;
2827
2868
  const hold = partialMarkerSuffixLength(this.pending);
2828
2869
  if (hold > 0) {
@@ -2830,6 +2871,11 @@ var ToolCallStreamFilter = class {
2830
2871
  this.pending = this.pending.slice(this.pending.length - hold);
2831
2872
  return;
2832
2873
  }
2874
+ if (this.sawCallFence && this.pending.includes("```")) {
2875
+ out.text += this.pending.replace(CALL_FENCE_TAIL_RE, "");
2876
+ this.pending = "";
2877
+ return;
2878
+ }
2833
2879
  out.text += this.pending;
2834
2880
  this.pending = "";
2835
2881
  return;
@@ -10482,7 +10528,7 @@ async function checkForUpdate(current, fetchImpl) {
10482
10528
  * 兜底常量与 package.json 的一致性由 `tests/check-smoke.mjs` 守着,不会漂。
10483
10529
  */
10484
10530
  /** 与 package.json 保持一致的兜底版本(由测试保证不会漂)。 */
10485
- const FALLBACK_VERSION = "0.6.37";
10531
+ const FALLBACK_VERSION = "0.6.38";
10486
10532
  let cached;
10487
10533
  /** 本插件版本(如 `0.1.26`)。 */
10488
10534
  function pluginVersion() {