dsh-deepseek-web-login 0.6.36 → 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 +132 -0
- package/lib/index.js +105 -31
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,138 @@
|
|
|
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
|
+
|
|
69
|
+
## 0.6.37 — 2026-10-03
|
|
70
|
+
|
|
71
|
+
修 DSML 标记泄漏:`dsml-` 连字符变体**带空格**时整块原样上屏(用户分享页现场)。
|
|
72
|
+
|
|
73
|
+
### 现场
|
|
74
|
+
|
|
75
|
+
用户对比 cuckoo(干净)与本插件的分享页,报告 DSML 漏到网页端。分享页原文:
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
<||DSML|| calls>
|
|
79
|
+
<||DSML|| invoke name="pwsh">
|
|
80
|
+
<||DSML|| parameter name="command" string="true">Get-Date -Format …</||DSML|| parameter>
|
|
81
|
+
</||DSML|| invoke>
|
|
82
|
+
</||DSML|| calls>
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
同时工具调用**确实执行了**(下一轮有 `[Tool Result for call_68d163…]`)—— 同一段标记
|
|
86
|
+
既被执行、又漏到正文,所以不是"认不出",是"没走进捕获态"。
|
|
87
|
+
|
|
88
|
+
### 根因
|
|
89
|
+
|
|
90
|
+
`DSML_PREFIX`(竖线段,可选)与 `(?:dsml-)?`(连字符段,可选)是**两个各自独立**的可选片段。
|
|
91
|
+
`<dsml- calls>`(连字符后带一个空格)两边都匹配不上 ⇒ 不进捕获态 ⇒ 整块当正文。
|
|
92
|
+
|
|
93
|
+
144 组合矩阵(前缀 6 种 × 形态 4 种 × 分块 3 种 × 通道 2 条)实测:修复前 **54 个组合泄漏**,
|
|
94
|
+
其中 `dsml-` 两个变体**全部 24 个组合**整块上屏,正文与思考通道都一样。
|
|
95
|
+
|
|
96
|
+
### 为什么一直没暴露
|
|
97
|
+
|
|
98
|
+
现有用例只写了**无空格**形态(`check-dsml-stray.mjs` / `logic-test.mjs` 都是 `<dsml-calls>`)——
|
|
99
|
+
**测的形态和现场形态不是同一个**。与「0.6.28 思考分支一句 `continue` 跳过全部四道网」同类:
|
|
100
|
+
判据只覆盖了一种规范写法,现场是另一种。
|
|
101
|
+
|
|
102
|
+
### 修法
|
|
103
|
+
|
|
104
|
+
- 前缀两种变体合并成**一个可选组** `DSML_PREFIX_BODY`,`dsml-` 后面**允许空白**。
|
|
105
|
+
- `normalizeDsml` 的两个 `dsml-` 替换同样吃空白(否则归一化后仍剩 `< calls>`,后面全失败)。
|
|
106
|
+
- `XML_MARKER_STARTERS` 补 `dsml- tool_calls` / `dsml- invoke` / `dsml- calls`
|
|
107
|
+
(hold-back 判据是逐字符比对 starter 前缀,少列一种跨包就判不出)。
|
|
108
|
+
- `findXmlToolCallEnd` 里那条闭合正则**只认半角单竖线** `(?:\|\s*DSML\s*\|\s*)?`,
|
|
109
|
+
而现场是全角 `||` ⇒ 裸 invoke 块的收尾会落到"当正文吐出"分支。改用共用片段。
|
|
110
|
+
- `stripStrayToolMarkup` 的包裹标签前缀改为**可选**(裸 `<calls>` / `</calls>` 此前只剥闭合,
|
|
111
|
+
开启标签整行残留)。
|
|
112
|
+
|
|
113
|
+
### 踩到并修掉的第二个坑:可选性不能写在片段里
|
|
114
|
+
|
|
115
|
+
修完第一处,`logic-test` 立刻红:`在 HTML 里 <invoke> 不是一个标准标签` 被吞。
|
|
116
|
+
|
|
117
|
+
原因是我把"前缀可选"实现成 `(?:${DSML_PREFIX})?${DSML_HYPHEN}` —— 两个**各自**可选的片段串起来,
|
|
118
|
+
整组仍可为空 ⇒ "无前缀也匹配 invoke" ⇒ 正文里正常讨论的标签被吞。**连踩两次**才想清楚:
|
|
119
|
+
可选性只能写在**整个片段的外层**,两个变体必须放进同一个交替里。
|
|
120
|
+
|
|
121
|
+
所以引入了 `DSML_PREFIX_ONLY`(不带 `?`)专供 `invoke` 使用:包裹标签是 DSML 私有标记,
|
|
122
|
+
剥它零风险;`invoke` 是通用 XML 标签,正文里真的会被讨论,必须要求带前缀。
|
|
123
|
+
|
|
124
|
+
### 用例
|
|
125
|
+
|
|
126
|
+
- `check-dsml-stray` 18 → **40 项**:前缀矩阵(7 种前缀 × 配平/截断/逐字符)+ 一条**反向**用例
|
|
127
|
+
(正文里正常讨论 `<invoke>` 不被误吞)。
|
|
128
|
+
- `dev/probe-dsml-matrix.mjs`:144 组合探针,判为泄漏时**打印上屏原文**。
|
|
129
|
+
⚠️ 写探针时我连栽两次假结论(把 `ReasoningSanitizer.push` 的字符串返回值当对象用;
|
|
130
|
+
模板拼接出错造出双份后缀),两次都是"只报结论不打证据"。**探针必须能自证。**
|
|
131
|
+
- 变异验证:①去掉 `dsml-` 的空白容忍 → 7 条红;②`invoke` 前缀改回可选 → 新用例 +
|
|
132
|
+
`logic-test` 各 1 条红。两个方向都有守卫。
|
|
133
|
+
- 产物断言修了一条:原正则绑死了旧的双可选结构(正是本次修掉的错误结构),改成守意图。
|
|
134
|
+
|
|
135
|
+
`tsc` / smoke / 产物 / **64/64** 全过,矩阵 144 组合全拦得住,真实留痕未污染。
|
|
136
|
+
|
|
5
137
|
## 0.6.36 — 2026-10-02
|
|
6
138
|
|
|
7
139
|
**把"提示词里到底有什么"变成可查的事实(诊断),并顺带给出这次现场的完整核对结论。**
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
/**
|
|
@@ -1984,19 +1988,59 @@ const MARKER_RE = /\{\s*"tool_calls?"\s*:/;
|
|
|
1984
1988
|
* 却撞上第二个 `|` → 整个标记认不出来 → 不进捕获态 → 原样进正文 → GUI 渲染成乱码。
|
|
1985
1989
|
* 现在竖线按 `+` 容忍(含全角/半角混用),并把 `calls` 也列入包裹标签名。
|
|
1986
1990
|
*/
|
|
1987
|
-
const DSML_PREFIX = "(?:[||]+\\s*DSML\\s*[||]+\\s*)?";
|
|
1988
1991
|
const WRAPPER_NAMES = "tool_calls|tool_call|function_calls|calls";
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
+
/**
|
|
1993
|
+
* DSML 前缀(竖线段**或** `dsml-` 连字符段),**整组可选**。
|
|
1994
|
+
*
|
|
1995
|
+
* 🔴 2026-10-03 修掉一个连踩两次的坑:此前是两个**各自独立**的可选片段
|
|
1996
|
+
* (`DSML_PREFIX` 自带 `?`,再串一个 `(?:dsml-)?`)。写成 `A? B? invoke` 时,
|
|
1997
|
+
* `A?` 里的 `?` 会让**整组都能为空** ⇒ "无前缀也匹配 invoke" ⇒ 正文里正常讨论的
|
|
1998
|
+
* `<invoke>` 被吞(logic-test 守着这条)。**可选性只能写在整个片段的外层**,
|
|
1999
|
+
* 两个变体必须放进同一个交替里。现在 `stripStrayToolMarkup` 里 `invoke` 用的就是这个
|
|
2000
|
+
* 片段的**非可选**写法 `DSML_PREFIX_ONLY`。
|
|
2001
|
+
*
|
|
2002
|
+
* 另修:`dsml-` 与标签名之间**允许空白**(`<dsml- calls>`)。旧写法只认 `<dsml-calls>`,
|
|
2003
|
+
* 而模型两种都吐 ⇒ 带空格那种整块原样上屏(144 组合矩阵里 24 个泄漏)。
|
|
2004
|
+
* ⚠️ 之所以一直没暴露:现有用例只写了无空格形态(`check-dsml-stray.mjs` / `logic-test.mjs`)——
|
|
2005
|
+
* **测的形态和现场形态不是同一个**。与「0.6.28 思考分支跳过全部四道网」同类。
|
|
2006
|
+
*/
|
|
2007
|
+
const DSML_PREFIX_BODY = "[||]+\\s*DSML\\s*[||]+\\s*|\\s*dsml-\\s*";
|
|
2008
|
+
/** 整组可选(前缀可有可无)。 */
|
|
2009
|
+
const DSML_PREFIX = `(?:${DSML_PREFIX_BODY})?`;
|
|
2010
|
+
/** 整组**必需**(给"必须有前缀才剥"的场合用,如 `invoke`)。 */
|
|
2011
|
+
const DSML_PREFIX_ONLY = `(?:${DSML_PREFIX_BODY})`;
|
|
2012
|
+
const XML_STARTER_RE = new RegExp(`<\\s*${DSML_PREFIX}(${WRAPPER_NAMES}|invoke)\\b`, "i");
|
|
1992
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
|
+
}
|
|
1993
2037
|
/**
|
|
1994
2038
|
* 开/收标签前缀(宽容写法)。严格解析与宽容解析**必须共用同一套**,否则会出现
|
|
1995
2039
|
* 「findXmlToolCallEnd 认得出收尾、parseXmlToolCalls 认不出 invoke」→ 整块被降级成正文泄漏。
|
|
1996
2040
|
* 覆盖:`< invoke`(标签名带空白)、单/重复竖线的 DSML 前缀(含全角)、`<dsml-invoke>`。
|
|
1997
2041
|
*/
|
|
1998
|
-
const TAG_OPEN_PREFIX = `<\\s*${DSML_PREFIX}
|
|
1999
|
-
const TAG_CLOSE_PREFIX = `<\\/\\s*${DSML_PREFIX}
|
|
2042
|
+
const TAG_OPEN_PREFIX = `<\\s*${DSML_PREFIX}`;
|
|
2043
|
+
const TAG_CLOSE_PREFIX = `<\\/\\s*${DSML_PREFIX}`;
|
|
2000
2044
|
const XML_CLOSE_NAMES = `parameter|invoke|${WRAPPER_NAMES}`;
|
|
2001
2045
|
/**
|
|
2002
2046
|
* 归一化 DSML 噪声 → 标准标签。
|
|
@@ -2004,7 +2048,7 @@ const XML_CLOSE_NAMES = `parameter|invoke|${WRAPPER_NAMES}`;
|
|
|
2004
2048
|
* 让标签名紧跟在 `<` 之后(`<` + 前缀 + ` ` + `invoke` → `<invoke`)。
|
|
2005
2049
|
*/
|
|
2006
2050
|
function normalizeDsml(text) {
|
|
2007
|
-
return text.replace(new RegExp(`<(/?)${DSML_PREFIX}`, "gi"), "<$1").replace(/<\s*dsml
|
|
2051
|
+
return text.replace(new RegExp(`<(/?)${DSML_PREFIX}`, "gi"), "<$1").replace(/<\s*dsml-\s*/gi, "<").replace(/<\/\s*dsml-\s*/gi, "</");
|
|
2008
2052
|
}
|
|
2009
2053
|
/**
|
|
2010
2054
|
* JSON 调用标记前缀(用于跨包 hold-back 判断)。
|
|
@@ -2014,7 +2058,13 @@ function normalizeDsml(text) {
|
|
|
2014
2058
|
* → 整个 JSON 泄漏成正文。修复见 partialMarkerSuffixLength。
|
|
2015
2059
|
*/
|
|
2016
2060
|
const JSON_MARKER_STARTERS = ["{\"tool_calls\"", "{\"tool_call\""];
|
|
2017
|
-
/**
|
|
2061
|
+
/**
|
|
2062
|
+
* XML 标记前缀(用于跨包 hold-back 判断)。`calls` 是实测出现的退化包裹名。
|
|
2063
|
+
*
|
|
2064
|
+
* ⚠️ `dsml-` 连字符条目必须同时列**带空格**形态(`<dsml- calls>`):hold-back 判据是
|
|
2065
|
+
* "逐字符比对 starter 前缀",少列一种 ⇒ 跨包切分时判不出前缀 ⇒ 半截标记当正文吐出。
|
|
2066
|
+
* 这条与 dsml- 前缀的修复是同一根因(2026-10-03)。
|
|
2067
|
+
*/
|
|
2018
2068
|
const XML_MARKER_STARTERS = [
|
|
2019
2069
|
"<tool_calls",
|
|
2020
2070
|
"<tool_call",
|
|
@@ -2022,7 +2072,10 @@ const XML_MARKER_STARTERS = [
|
|
|
2022
2072
|
"<calls",
|
|
2023
2073
|
"<invoke",
|
|
2024
2074
|
"<dsml-tool_calls",
|
|
2025
|
-
"<dsml-invoke"
|
|
2075
|
+
"<dsml-invoke",
|
|
2076
|
+
"<dsml- tool_calls",
|
|
2077
|
+
"<dsml- invoke",
|
|
2078
|
+
"<dsml- calls"
|
|
2026
2079
|
];
|
|
2027
2080
|
/**
|
|
2028
2081
|
* 判断 text 末尾是否是(可能的)标记前缀 —— 决定是否 hold back。
|
|
@@ -2600,7 +2653,7 @@ function parseToolCallJson(json) {
|
|
|
2600
2653
|
* 注意:不配平的截断仍由 structuralRepairCandidates 的安全闸门拒绝(宁可不执行半条命令)。
|
|
2601
2654
|
*/
|
|
2602
2655
|
function parseSalvagedToolCallJson(buffer) {
|
|
2603
|
-
const text = buffer.replace(
|
|
2656
|
+
const text = buffer.replace(FENCE_ANY_HEAD_RE, "").replace(/^\s+/, "");
|
|
2604
2657
|
const direct = parseToolCallJson(text);
|
|
2605
2658
|
if (direct) return direct;
|
|
2606
2659
|
const balanced = extractBalancedJson(text);
|
|
@@ -2615,12 +2668,12 @@ function parseSalvagedToolCallJson(buffer) {
|
|
|
2615
2668
|
*/
|
|
2616
2669
|
function findXmlToolCallEnd(buffer) {
|
|
2617
2670
|
const text = buffer;
|
|
2618
|
-
const wrapper = new RegExp(`<\\s*${DSML_PREFIX}(
|
|
2671
|
+
const wrapper = new RegExp(`<\\s*${DSML_PREFIX}(${WRAPPER_NAMES})\\b`, "i").exec(text);
|
|
2619
2672
|
const startsWithWrapper = wrapper !== null && wrapper.index === 0;
|
|
2620
|
-
const isInvokeStart = (value) => new RegExp(`^\\s*<\\s*${DSML_PREFIX}
|
|
2673
|
+
const isInvokeStart = (value) => new RegExp(`^\\s*<\\s*${DSML_PREFIX}invoke\\b`, "i").test(value);
|
|
2621
2674
|
if (startsWithWrapper) {
|
|
2622
2675
|
const tag = wrapper[1].toLowerCase();
|
|
2623
|
-
const match = new RegExp(`<\\/\\s*${DSML_PREFIX}
|
|
2676
|
+
const match = new RegExp(`<\\/\\s*${DSML_PREFIX}${tag}\\s*>`, "i").exec(text);
|
|
2624
2677
|
return match ? match.index + match[0].length : -1;
|
|
2625
2678
|
}
|
|
2626
2679
|
if (!isInvokeStart(text)) return -1;
|
|
@@ -2628,12 +2681,12 @@ function findXmlToolCallEnd(buffer) {
|
|
|
2628
2681
|
for (;;) {
|
|
2629
2682
|
const slice = text.slice(cursor);
|
|
2630
2683
|
if (!isInvokeStart(slice)) return cursor > 0 ? cursor : -1;
|
|
2631
|
-
const match =
|
|
2684
|
+
const match = new RegExp(`<\\/\\s*${DSML_PREFIX}invoke\\s*>`, "i").exec(slice);
|
|
2632
2685
|
if (!match) return -1;
|
|
2633
2686
|
cursor += match.index + match[0].length;
|
|
2634
2687
|
const rest = text.slice(cursor);
|
|
2635
2688
|
if (isInvokeStart(rest)) continue;
|
|
2636
|
-
const stray = new RegExp(`^\\s*<\\/\\s*${DSML_PREFIX}(
|
|
2689
|
+
const stray = new RegExp(`^\\s*<\\/\\s*${DSML_PREFIX}(?:${WRAPPER_NAMES})\\s*>`, "i").exec(rest);
|
|
2637
2690
|
if (stray) return cursor + stray[0].length;
|
|
2638
2691
|
const tail = rest.trim();
|
|
2639
2692
|
if (tail.startsWith("<") && /^<\/?\s*[||]?\s*[A-Za-z]{0,14}$/.test(tail)) return -1;
|
|
@@ -2660,7 +2713,7 @@ function findXmlToolCallEnd(buffer) {
|
|
|
2660
2713
|
function stripStrayToolMarkup(text) {
|
|
2661
2714
|
if (!text) return text;
|
|
2662
2715
|
if (!/voke\s*>|calls?\s*>|tool_calls?\s*>|function_calls?\s*>|DSML/i.test(text)) return text;
|
|
2663
|
-
return text.replace(new RegExp(`<\\/?\\s*(
|
|
2716
|
+
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
2717
|
}
|
|
2665
2718
|
/**
|
|
2666
2719
|
* 流式工具调用过滤器。
|
|
@@ -2674,6 +2727,14 @@ var ToolCallStreamFilter = class {
|
|
|
2674
2727
|
capture = null;
|
|
2675
2728
|
abandoned = null;
|
|
2676
2729
|
knownTools;
|
|
2730
|
+
/**
|
|
2731
|
+
* 本次流里**我们剥掉过一个调用围栏的开栏**(0.6.38)。
|
|
2732
|
+
*
|
|
2733
|
+
* ⇒ 之后出现的闭栏必定是同一个调用块的收尾,可以安全剥掉。
|
|
2734
|
+
* ⚠️ 有了这个标记才敢剥闭栏:否则"正文里正常的代码块"会被误伤
|
|
2735
|
+
* (`check-auto-continue` N03 / `check-tools-section` 各有用例守着)。
|
|
2736
|
+
*/
|
|
2737
|
+
sawCallFence = false;
|
|
2677
2738
|
constructor(knownTools) {
|
|
2678
2739
|
this.knownTools = knownTools;
|
|
2679
2740
|
}
|
|
@@ -2708,7 +2769,8 @@ var ToolCallStreamFilter = class {
|
|
|
2708
2769
|
else out.text += stripStrayToolMarkup(captured.buffer);
|
|
2709
2770
|
this.capture = null;
|
|
2710
2771
|
}
|
|
2711
|
-
|
|
2772
|
+
const tailText = stripStrayToolMarkup(this.pending);
|
|
2773
|
+
out.text += this.sawCallFence ? tailText.replace(CALL_FENCE_TAIL_RE, "") : tailText;
|
|
2712
2774
|
this.pending = "";
|
|
2713
2775
|
if (this.abandoned) out.rejected = this.abandoned;
|
|
2714
2776
|
return out;
|
|
@@ -2742,7 +2804,7 @@ var ToolCallStreamFilter = class {
|
|
|
2742
2804
|
};
|
|
2743
2805
|
else out.text += stripStrayToolMarkup(block);
|
|
2744
2806
|
this.capture = null;
|
|
2745
|
-
this.pending = captured.buffer.slice(end)
|
|
2807
|
+
this.pending = stripCallFence(captured.buffer.slice(end)) + this.pending;
|
|
2746
2808
|
continue;
|
|
2747
2809
|
}
|
|
2748
2810
|
const balanced = extractBalancedJson(captured.buffer);
|
|
@@ -2762,7 +2824,7 @@ var ToolCallStreamFilter = class {
|
|
|
2762
2824
|
if (calls) {
|
|
2763
2825
|
out.calls.push(...calls);
|
|
2764
2826
|
this.capture = null;
|
|
2765
|
-
this.pending = captured.buffer.slice(balanced.end)
|
|
2827
|
+
this.pending = stripCallFence(captured.buffer.slice(balanced.end)) + this.pending;
|
|
2766
2828
|
continue;
|
|
2767
2829
|
}
|
|
2768
2830
|
const head = captured.buffer.slice(0, balanced.end);
|
|
@@ -2783,9 +2845,9 @@ var ToolCallStreamFilter = class {
|
|
|
2783
2845
|
const useXml = xmlIndex !== -1 && (jsonIndex === -1 || xmlIndex < jsonIndex);
|
|
2784
2846
|
const index = useXml ? xmlIndex : jsonIndex;
|
|
2785
2847
|
if (index !== -1) {
|
|
2786
|
-
|
|
2787
|
-
const
|
|
2788
|
-
if (
|
|
2848
|
+
const rawHead = this.pending.slice(0, index);
|
|
2849
|
+
const head = stripCallFence(rawHead);
|
|
2850
|
+
if (head !== rawHead) this.sawCallFence = true;
|
|
2789
2851
|
out.text += head;
|
|
2790
2852
|
this.capture = {
|
|
2791
2853
|
mode: useXml ? "xml" : "json",
|
|
@@ -2795,6 +2857,13 @@ var ToolCallStreamFilter = class {
|
|
|
2795
2857
|
continue;
|
|
2796
2858
|
}
|
|
2797
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
|
+
}
|
|
2798
2867
|
if (this.pending.length <= HOLD_BACK_CHARS) return;
|
|
2799
2868
|
const hold = partialMarkerSuffixLength(this.pending);
|
|
2800
2869
|
if (hold > 0) {
|
|
@@ -2802,6 +2871,11 @@ var ToolCallStreamFilter = class {
|
|
|
2802
2871
|
this.pending = this.pending.slice(this.pending.length - hold);
|
|
2803
2872
|
return;
|
|
2804
2873
|
}
|
|
2874
|
+
if (this.sawCallFence && this.pending.includes("```")) {
|
|
2875
|
+
out.text += this.pending.replace(CALL_FENCE_TAIL_RE, "");
|
|
2876
|
+
this.pending = "";
|
|
2877
|
+
return;
|
|
2878
|
+
}
|
|
2805
2879
|
out.text += this.pending;
|
|
2806
2880
|
this.pending = "";
|
|
2807
2881
|
return;
|
|
@@ -10454,7 +10528,7 @@ async function checkForUpdate(current, fetchImpl) {
|
|
|
10454
10528
|
* 兜底常量与 package.json 的一致性由 `tests/check-smoke.mjs` 守着,不会漂。
|
|
10455
10529
|
*/
|
|
10456
10530
|
/** 与 package.json 保持一致的兜底版本(由测试保证不会漂)。 */
|
|
10457
|
-
const FALLBACK_VERSION = "0.6.
|
|
10531
|
+
const FALLBACK_VERSION = "0.6.38";
|
|
10458
10532
|
let cached;
|
|
10459
10533
|
/** 本插件版本(如 `0.1.26`)。 */
|
|
10460
10534
|
function pluginVersion() {
|