dsh-deepseek-web-login 0.6.36 → 0.6.37

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,74 @@
2
2
 
3
3
  本项目大致遵循语义化版本;日期为本地时间。
4
4
 
5
+ ## 0.6.37 — 2026-10-03
6
+
7
+ 修 DSML 标记泄漏:`dsml-` 连字符变体**带空格**时整块原样上屏(用户分享页现场)。
8
+
9
+ ### 现场
10
+
11
+ 用户对比 cuckoo(干净)与本插件的分享页,报告 DSML 漏到网页端。分享页原文:
12
+
13
+ ```
14
+ <||DSML|| calls>
15
+ <||DSML|| invoke name="pwsh">
16
+ <||DSML|| parameter name="command" string="true">Get-Date -Format …</||DSML|| parameter>
17
+ </||DSML|| invoke>
18
+ </||DSML|| calls>
19
+ ```
20
+
21
+ 同时工具调用**确实执行了**(下一轮有 `[Tool Result for call_68d163…]`)—— 同一段标记
22
+ 既被执行、又漏到正文,所以不是"认不出",是"没走进捕获态"。
23
+
24
+ ### 根因
25
+
26
+ `DSML_PREFIX`(竖线段,可选)与 `(?:dsml-)?`(连字符段,可选)是**两个各自独立**的可选片段。
27
+ `<dsml- calls>`(连字符后带一个空格)两边都匹配不上 ⇒ 不进捕获态 ⇒ 整块当正文。
28
+
29
+ 144 组合矩阵(前缀 6 种 × 形态 4 种 × 分块 3 种 × 通道 2 条)实测:修复前 **54 个组合泄漏**,
30
+ 其中 `dsml-` 两个变体**全部 24 个组合**整块上屏,正文与思考通道都一样。
31
+
32
+ ### 为什么一直没暴露
33
+
34
+ 现有用例只写了**无空格**形态(`check-dsml-stray.mjs` / `logic-test.mjs` 都是 `<dsml-calls>`)——
35
+ **测的形态和现场形态不是同一个**。与「0.6.28 思考分支一句 `continue` 跳过全部四道网」同类:
36
+ 判据只覆盖了一种规范写法,现场是另一种。
37
+
38
+ ### 修法
39
+
40
+ - 前缀两种变体合并成**一个可选组** `DSML_PREFIX_BODY`,`dsml-` 后面**允许空白**。
41
+ - `normalizeDsml` 的两个 `dsml-` 替换同样吃空白(否则归一化后仍剩 `< calls>`,后面全失败)。
42
+ - `XML_MARKER_STARTERS` 补 `dsml- tool_calls` / `dsml- invoke` / `dsml- calls`
43
+ (hold-back 判据是逐字符比对 starter 前缀,少列一种跨包就判不出)。
44
+ - `findXmlToolCallEnd` 里那条闭合正则**只认半角单竖线** `(?:\|\s*DSML\s*\|\s*)?`,
45
+ 而现场是全角 `||` ⇒ 裸 invoke 块的收尾会落到"当正文吐出"分支。改用共用片段。
46
+ - `stripStrayToolMarkup` 的包裹标签前缀改为**可选**(裸 `<calls>` / `</calls>` 此前只剥闭合,
47
+ 开启标签整行残留)。
48
+
49
+ ### 踩到并修掉的第二个坑:可选性不能写在片段里
50
+
51
+ 修完第一处,`logic-test` 立刻红:`在 HTML 里 <invoke> 不是一个标准标签` 被吞。
52
+
53
+ 原因是我把"前缀可选"实现成 `(?:${DSML_PREFIX})?${DSML_HYPHEN}` —— 两个**各自**可选的片段串起来,
54
+ 整组仍可为空 ⇒ "无前缀也匹配 invoke" ⇒ 正文里正常讨论的标签被吞。**连踩两次**才想清楚:
55
+ 可选性只能写在**整个片段的外层**,两个变体必须放进同一个交替里。
56
+
57
+ 所以引入了 `DSML_PREFIX_ONLY`(不带 `?`)专供 `invoke` 使用:包裹标签是 DSML 私有标记,
58
+ 剥它零风险;`invoke` 是通用 XML 标签,正文里真的会被讨论,必须要求带前缀。
59
+
60
+ ### 用例
61
+
62
+ - `check-dsml-stray` 18 → **40 项**:前缀矩阵(7 种前缀 × 配平/截断/逐字符)+ 一条**反向**用例
63
+ (正文里正常讨论 `<invoke>` 不被误吞)。
64
+ - `dev/probe-dsml-matrix.mjs`:144 组合探针,判为泄漏时**打印上屏原文**。
65
+ ⚠️ 写探针时我连栽两次假结论(把 `ReasoningSanitizer.push` 的字符串返回值当对象用;
66
+ 模板拼接出错造出双份后缀),两次都是"只报结论不打证据"。**探针必须能自证。**
67
+ - 变异验证:①去掉 `dsml-` 的空白容忍 → 7 条红;②`invoke` 前缀改回可选 → 新用例 +
68
+ `logic-test` 各 1 条红。两个方向都有守卫。
69
+ - 产物断言修了一条:原正则绑死了旧的双可选结构(正是本次修掉的错误结构),改成守意图。
70
+
71
+ `tsc` / smoke / 产物 / **64/64** 全过,矩阵 144 组合全拦得住,真实留痕未污染。
72
+
5
73
  ## 0.6.36 — 2026-10-02
6
74
 
7
75
  **把"提示词里到底有什么"变成可查的事实(诊断),并顺带给出这次现场的完整核对结论。**
package/lib/index.js CHANGED
@@ -1984,9 +1984,28 @@ const MARKER_RE = /\{\s*"tool_calls?"\s*:/;
1984
1984
  * 却撞上第二个 `|` → 整个标记认不出来 → 不进捕获态 → 原样进正文 → GUI 渲染成乱码。
1985
1985
  * 现在竖线按 `+` 容忍(含全角/半角混用),并把 `calls` 也列入包裹标签名。
1986
1986
  */
1987
- const DSML_PREFIX = "(?:[||]+\\s*DSML\\s*[||]+\\s*)?";
1988
1987
  const WRAPPER_NAMES = "tool_calls|tool_call|function_calls|calls";
1989
- const XML_STARTER_RE = new RegExp(`<\\s*${DSML_PREFIX}(?:dsml-)?(${WRAPPER_NAMES}|invoke)\\b`, "i");
1988
+ /**
1989
+ * DSML 前缀(竖线段**或** `dsml-` 连字符段),**整组可选**。
1990
+ *
1991
+ * 🔴 2026-10-03 修掉一个连踩两次的坑:此前是两个**各自独立**的可选片段
1992
+ * (`DSML_PREFIX` 自带 `?`,再串一个 `(?:dsml-)?`)。写成 `A? B? invoke` 时,
1993
+ * `A?` 里的 `?` 会让**整组都能为空** ⇒ "无前缀也匹配 invoke" ⇒ 正文里正常讨论的
1994
+ * `<invoke>` 被吞(logic-test 守着这条)。**可选性只能写在整个片段的外层**,
1995
+ * 两个变体必须放进同一个交替里。现在 `stripStrayToolMarkup` 里 `invoke` 用的就是这个
1996
+ * 片段的**非可选**写法 `DSML_PREFIX_ONLY`。
1997
+ *
1998
+ * 另修:`dsml-` 与标签名之间**允许空白**(`<dsml- calls>`)。旧写法只认 `<dsml-calls>`,
1999
+ * 而模型两种都吐 ⇒ 带空格那种整块原样上屏(144 组合矩阵里 24 个泄漏)。
2000
+ * ⚠️ 之所以一直没暴露:现有用例只写了无空格形态(`check-dsml-stray.mjs` / `logic-test.mjs`)——
2001
+ * **测的形态和现场形态不是同一个**。与「0.6.28 思考分支跳过全部四道网」同类。
2002
+ */
2003
+ const DSML_PREFIX_BODY = "[||]+\\s*DSML\\s*[||]+\\s*|\\s*dsml-\\s*";
2004
+ /** 整组可选(前缀可有可无)。 */
2005
+ const DSML_PREFIX = `(?:${DSML_PREFIX_BODY})?`;
2006
+ /** 整组**必需**(给"必须有前缀才剥"的场合用,如 `invoke`)。 */
2007
+ const DSML_PREFIX_ONLY = `(?:${DSML_PREFIX_BODY})`;
2008
+ const XML_STARTER_RE = new RegExp(`<\\s*${DSML_PREFIX}(${WRAPPER_NAMES}|invoke)\\b`, "i");
1990
2009
  /** 代码围栏收尾(模型常把调用块放进 ``` 里)。 */
1991
2010
  const FENCE_TAIL_RE = /\n?[ \t]*```[a-zA-Z0-9]*[ \t]*\n?$/;
1992
2011
  const FENCE_HEAD_RE = /^[ \t]*\n?```[ \t]*\n?/;
@@ -1995,8 +2014,8 @@ const FENCE_HEAD_RE = /^[ \t]*\n?```[ \t]*\n?/;
1995
2014
  * 「findXmlToolCallEnd 认得出收尾、parseXmlToolCalls 认不出 invoke」→ 整块被降级成正文泄漏。
1996
2015
  * 覆盖:`< invoke`(标签名带空白)、单/重复竖线的 DSML 前缀(含全角)、`<dsml-invoke>`。
1997
2016
  */
1998
- const TAG_OPEN_PREFIX = `<\\s*${DSML_PREFIX}(?:dsml-)?`;
1999
- const TAG_CLOSE_PREFIX = `<\\/\\s*${DSML_PREFIX}(?:dsml-)?`;
2017
+ const TAG_OPEN_PREFIX = `<\\s*${DSML_PREFIX}`;
2018
+ const TAG_CLOSE_PREFIX = `<\\/\\s*${DSML_PREFIX}`;
2000
2019
  const XML_CLOSE_NAMES = `parameter|invoke|${WRAPPER_NAMES}`;
2001
2020
  /**
2002
2021
  * 归一化 DSML 噪声 → 标准标签。
@@ -2004,7 +2023,7 @@ const XML_CLOSE_NAMES = `parameter|invoke|${WRAPPER_NAMES}`;
2004
2023
  * 让标签名紧跟在 `<` 之后(`<` + 前缀 + ` ` + `invoke` → `<invoke`)。
2005
2024
  */
2006
2025
  function normalizeDsml(text) {
2007
- return text.replace(new RegExp(`<(/?)${DSML_PREFIX}`, "gi"), "<$1").replace(/<\s*dsml-/gi, "<").replace(/<\/\s*dsml-/gi, "</");
2026
+ return text.replace(new RegExp(`<(/?)${DSML_PREFIX}`, "gi"), "<$1").replace(/<\s*dsml-\s*/gi, "<").replace(/<\/\s*dsml-\s*/gi, "</");
2008
2027
  }
2009
2028
  /**
2010
2029
  * JSON 调用标记前缀(用于跨包 hold-back 判断)。
@@ -2014,7 +2033,13 @@ function normalizeDsml(text) {
2014
2033
  * → 整个 JSON 泄漏成正文。修复见 partialMarkerSuffixLength。
2015
2034
  */
2016
2035
  const JSON_MARKER_STARTERS = ["{\"tool_calls\"", "{\"tool_call\""];
2017
- /** XML 标记前缀(用于跨包 hold-back 判断)。`calls` 是实测出现的退化包裹名。 */
2036
+ /**
2037
+ * XML 标记前缀(用于跨包 hold-back 判断)。`calls` 是实测出现的退化包裹名。
2038
+ *
2039
+ * ⚠️ `dsml-` 连字符条目必须同时列**带空格**形态(`<dsml- calls>`):hold-back 判据是
2040
+ * "逐字符比对 starter 前缀",少列一种 ⇒ 跨包切分时判不出前缀 ⇒ 半截标记当正文吐出。
2041
+ * 这条与 dsml- 前缀的修复是同一根因(2026-10-03)。
2042
+ */
2018
2043
  const XML_MARKER_STARTERS = [
2019
2044
  "<tool_calls",
2020
2045
  "<tool_call",
@@ -2022,7 +2047,10 @@ const XML_MARKER_STARTERS = [
2022
2047
  "<calls",
2023
2048
  "<invoke",
2024
2049
  "<dsml-tool_calls",
2025
- "<dsml-invoke"
2050
+ "<dsml-invoke",
2051
+ "<dsml- tool_calls",
2052
+ "<dsml- invoke",
2053
+ "<dsml- calls"
2026
2054
  ];
2027
2055
  /**
2028
2056
  * 判断 text 末尾是否是(可能的)标记前缀 —— 决定是否 hold back。
@@ -2615,12 +2643,12 @@ function parseSalvagedToolCallJson(buffer) {
2615
2643
  */
2616
2644
  function findXmlToolCallEnd(buffer) {
2617
2645
  const text = buffer;
2618
- const wrapper = new RegExp(`<\\s*${DSML_PREFIX}(?:dsml-)?(${WRAPPER_NAMES})\\b`, "i").exec(text);
2646
+ const wrapper = new RegExp(`<\\s*${DSML_PREFIX}(${WRAPPER_NAMES})\\b`, "i").exec(text);
2619
2647
  const startsWithWrapper = wrapper !== null && wrapper.index === 0;
2620
- const isInvokeStart = (value) => new RegExp(`^\\s*<\\s*${DSML_PREFIX}(?:dsml-)?invoke\\b`, "i").test(value);
2648
+ const isInvokeStart = (value) => new RegExp(`^\\s*<\\s*${DSML_PREFIX}invoke\\b`, "i").test(value);
2621
2649
  if (startsWithWrapper) {
2622
2650
  const tag = wrapper[1].toLowerCase();
2623
- const match = new RegExp(`<\\/\\s*${DSML_PREFIX}(?:dsml-)?${tag}\\s*>`, "i").exec(text);
2651
+ const match = new RegExp(`<\\/\\s*${DSML_PREFIX}${tag}\\s*>`, "i").exec(text);
2624
2652
  return match ? match.index + match[0].length : -1;
2625
2653
  }
2626
2654
  if (!isInvokeStart(text)) return -1;
@@ -2628,12 +2656,12 @@ function findXmlToolCallEnd(buffer) {
2628
2656
  for (;;) {
2629
2657
  const slice = text.slice(cursor);
2630
2658
  if (!isInvokeStart(slice)) return cursor > 0 ? cursor : -1;
2631
- const match = /<\/\s*(?:\|\s*DSML\s*\|\s*)?(?:dsml-)?invoke\s*>/i.exec(slice);
2659
+ const match = new RegExp(`<\\/\\s*${DSML_PREFIX}invoke\\s*>`, "i").exec(slice);
2632
2660
  if (!match) return -1;
2633
2661
  cursor += match.index + match[0].length;
2634
2662
  const rest = text.slice(cursor);
2635
2663
  if (isInvokeStart(rest)) continue;
2636
- const stray = new RegExp(`^\\s*<\\/\\s*${DSML_PREFIX}(?:dsml-)?(?:${WRAPPER_NAMES})\\s*>`, "i").exec(rest);
2664
+ const stray = new RegExp(`^\\s*<\\/\\s*${DSML_PREFIX}(?:${WRAPPER_NAMES})\\s*>`, "i").exec(rest);
2637
2665
  if (stray) return cursor + stray[0].length;
2638
2666
  const tail = rest.trim();
2639
2667
  if (tail.startsWith("<") && /^<\/?\s*[||]?\s*[A-Za-z]{0,14}$/.test(tail)) return -1;
@@ -2660,7 +2688,7 @@ function findXmlToolCallEnd(buffer) {
2660
2688
  function stripStrayToolMarkup(text) {
2661
2689
  if (!text) return text;
2662
2690
  if (!/voke\s*>|calls?\s*>|tool_calls?\s*>|function_calls?\s*>|DSML/i.test(text)) return text;
2663
- return text.replace(new RegExp(`<\\/?\\s*(?:(?:[||]+\\s*DSML\\s*[||]+\\s*)|dsml-)(?:dsml-)?(?:${WRAPPER_NAMES}|invoke)\\s*>`, "gi"), "").replace(new RegExp(`<\\/\\s*(?:[||]+\\s*DSML\\s*[||]+\\s*)?(?:dsml-)?(?:${WRAPPER_NAMES}|invoke)\\s*>`, "gi"), "").replace(/<\/\s+(?:tool_calls?|function_calls|calls|invoke)\s*>/gi, "").replace(/(^|\n)[ \t]*(?:in)?voke\s*>\s*(?=\n|$)/gi, "$1");
2691
+ return text.replace(new RegExp(`<\\/?\\s*(?:${DSML_PREFIX_ONLY})?(?:${WRAPPER_NAMES})\\s*>|<\\/?\\s*${DSML_PREFIX_ONLY}invoke\\s*>`, "gi"), "").replace(new RegExp(`(^|\\n)[ \\t]*<\\/?[ \\t]+(?:${WRAPPER_NAMES})[ \\t]*>[ \\t]*(?=\\n|$)`, "gi"), "$1").replace(/(^|\n)[ \t]*(?:in)?voke\s*>\s*(?=\n|$)/gi, "$1");
2664
2692
  }
2665
2693
  /**
2666
2694
  * 流式工具调用过滤器。
@@ -10454,7 +10482,7 @@ async function checkForUpdate(current, fetchImpl) {
10454
10482
  * 兜底常量与 package.json 的一致性由 `tests/check-smoke.mjs` 守着,不会漂。
10455
10483
  */
10456
10484
  /** 与 package.json 保持一致的兜底版本(由测试保证不会漂)。 */
10457
- const FALLBACK_VERSION = "0.6.36";
10485
+ const FALLBACK_VERSION = "0.6.37";
10458
10486
  let cached;
10459
10487
  /** 本插件版本(如 `0.1.26`)。 */
10460
10488
  function pluginVersion() {