dsh-plugin-om 0.0.20 → 0.0.22
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/README.md +69 -156
- package/dist/client/OmCompactionCard.d.ts +5 -11
- package/dist/client/OmCompactionCard.d.ts.map +1 -1
- package/dist/client/definition.d.ts +17 -19
- package/dist/client/definition.d.ts.map +1 -1
- package/dist/client/format.d.ts +2 -2
- package/dist/client/format.d.ts.map +1 -1
- package/dist/client/index.d.ts +5 -11
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/locales.d.ts +2 -4
- package/dist/client/locales.d.ts.map +1 -1
- package/dist/client.js +17 -26
- package/dist/client.js.map +1 -1
- package/dist/compress.d.ts +15 -41
- package/dist/compress.d.ts.map +1 -1
- package/dist/config.d.ts +13 -18
- package/dist/config.d.ts.map +1 -1
- package/dist/constants.d.ts +10 -10
- package/dist/constants.d.ts.map +1 -1
- package/dist/embedding.d.ts +19 -36
- package/dist/embedding.d.ts.map +1 -1
- package/dist/index.d.ts +1 -22
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +516 -712
- package/dist/json-schema.d.ts +9 -0
- package/dist/json-schema.d.ts.map +1 -0
- package/dist/log-index.d.ts +19 -12
- package/dist/log-index.d.ts.map +1 -1
- package/dist/logger.d.ts +3 -3
- package/dist/logger.d.ts.map +1 -1
- package/dist/model-download.d.ts +7 -11
- package/dist/model-download.d.ts.map +1 -1
- package/dist/rate-limit.d.ts +21 -0
- package/dist/rate-limit.d.ts.map +1 -0
- package/dist/recall-output.d.ts +37 -0
- package/dist/recall-output.d.ts.map +1 -0
- package/dist/recall.d.ts +7 -15
- package/dist/recall.d.ts.map +1 -1
- package/dist/semantic-recall.d.ts +10 -17
- package/dist/semantic-recall.d.ts.map +1 -1
- package/dist/summarize.d.ts +30 -43
- package/dist/summarize.d.ts.map +1 -1
- package/dist/types.d.ts +22 -35
- package/dist/types.d.ts.map +1 -1
- package/dist/utils.d.ts +9 -8
- package/dist/utils.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -3,40 +3,40 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { z } from "zod";
|
|
6
|
+
import { toJSONSchema } from "zod/v4/core";
|
|
6
7
|
//#region \0rolldown/runtime.js
|
|
7
8
|
var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
|
|
8
9
|
//#endregion
|
|
9
10
|
//#region src/constants.ts
|
|
10
11
|
/**
|
|
11
|
-
*
|
|
12
|
+
* 共享常量:插件级魔法字符串,集中定义避免散落各模块。
|
|
13
|
+
* 导出 PLUGIN_LABEL / HISTORY_TAG / HISTORY_TIP / COMPLETE_MESSAGE_DEFINITION /
|
|
14
|
+
* COMPACT_CHECKPOINT_PLUGIN / isPluginOwnedSource。
|
|
12
15
|
*/
|
|
13
|
-
/** 插件标识:压缩消息 source.plugin
|
|
16
|
+
/** 插件标识:压缩消息 source.plugin 取值与日志前缀。 */
|
|
14
17
|
const PLUGIN_LABEL = "dsh-plugin-om";
|
|
15
|
-
/** 压缩日志标签名:<history>...</history>
|
|
18
|
+
/** 压缩日志标签名:<history>...</history> 包裹观察/反思日志块。 */
|
|
16
19
|
const HISTORY_TAG = "history";
|
|
17
|
-
/**
|
|
20
|
+
/** 压缩日志块开标签的 tip 属性(对 AI 的提醒:本块是历史压缩产物,不要复述)。 */
|
|
18
21
|
const HISTORY_TIP = "当前块是历史消息的压缩产物,不要复述";
|
|
19
22
|
/**
|
|
20
|
-
* 「完整消息」定义(提示词 /
|
|
23
|
+
* 「完整消息」定义(提示词 / 工具描述 / 块顶注释共用)。
|
|
21
24
|
* 完整消息是摘要日志与 recall 共用的定位单位;首条 index 为 0,按会话顺序递增、全局稳定。
|
|
22
|
-
* 用户消息指 user_message 中 source.kind 为 user 的部分;其余部分(宿主注入的上下文等)
|
|
23
|
-
* 为系统消息,同样是一条完整消息。
|
|
24
25
|
*/
|
|
25
26
|
const COMPLETE_MESSAGE_DEFINITION = "`完整消息`被定义为`用户消息`,`系统消息`,`模型输出文本`和`具有result的toolcall`的集合。首条`完整消息`的index是0,后续的index递增。";
|
|
26
|
-
/** 判定 user/message 的 source
|
|
27
|
+
/** 判定 user/message 的 source 是否为本插件自产(压缩日志消息;含旧宿主 checkpoint 标记兼容)。这类消息不占完整消息 index。 */
|
|
27
28
|
function isPluginOwnedSource(source) {
|
|
28
29
|
if (source?.kind !== "plugin") return false;
|
|
29
30
|
return source.plugin === "dsh-plugin-om" || source.plugin === "compact";
|
|
30
31
|
}
|
|
31
32
|
//#endregion
|
|
32
33
|
//#region src/utils.ts
|
|
33
|
-
/**
|
|
34
|
+
/** 判断值是否为普通对象(非 null、非数组)。 */
|
|
34
35
|
function isRecord(value) {
|
|
35
36
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
36
37
|
}
|
|
37
|
-
/** 生成 uuid:优先 crypto.randomUUID
|
|
38
|
+
/** 生成 uuid:优先 crypto.randomUUID,回退为时间戳+随机串拼接。 */
|
|
38
39
|
function uuid() {
|
|
39
|
-
/** 全局 crypto 对象(提供 randomUUID 的现代环境才存在)。 */
|
|
40
40
|
const cryptoObj = globalThis.crypto;
|
|
41
41
|
if (cryptoObj && typeof cryptoObj.randomUUID === "function") return cryptoObj.randomUUID();
|
|
42
42
|
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 12)}`;
|
|
@@ -44,15 +44,13 @@ function uuid() {
|
|
|
44
44
|
/** 提取 content 数组中的纯文本块(type === 'text' 的 block.text 拼接)。 */
|
|
45
45
|
function blocksToText(blocks) {
|
|
46
46
|
if (!Array.isArray(blocks)) return "";
|
|
47
|
-
/** 拼接结果缓冲区。 */
|
|
48
47
|
const out = [];
|
|
49
48
|
for (const block of blocks) if (isRecord(block) && block.type === "text" && typeof block.text === "string") out.push(block.text);
|
|
50
49
|
return out.join("");
|
|
51
50
|
}
|
|
52
|
-
/** 消息内容的字符数:递归计入 text 块与 tool-result
|
|
51
|
+
/** 消息内容的字符数:递归计入 text 块与 tool-result 内嵌文本。 */
|
|
53
52
|
function textCharCount(message) {
|
|
54
53
|
if (!message || !Array.isArray(message.content)) return 0;
|
|
55
|
-
/** 累计字符数。 */
|
|
56
54
|
let total = 0;
|
|
57
55
|
for (const block of message.content) if (block.type === "text") total += block.text.length;
|
|
58
56
|
else if (block.type === "tool-result") total += blocksToText(block.content).length;
|
|
@@ -61,7 +59,6 @@ function textCharCount(message) {
|
|
|
61
59
|
/** 面向 recall 的完整消息呈现:text 原样;tool-call 展开(参数=代码);tool-result 取文本。 */
|
|
62
60
|
function renderMessageText(message) {
|
|
63
61
|
if (!message || !Array.isArray(message.content)) return "";
|
|
64
|
-
/** 各内容块呈现后的拼接缓冲区。 */
|
|
65
62
|
const parts = [];
|
|
66
63
|
for (const block of message.content) if (block.type === "text") parts.push(block.text);
|
|
67
64
|
else if (block.type === "tool-call") parts.push(`[tool-call ${block.name} id=${String(block.id)}]
|
|
@@ -69,7 +66,7 @@ ${safeJson(block.arguments)}`);
|
|
|
69
66
|
else if (block.type === "tool-result") parts.push(blocksToText(block.content));
|
|
70
67
|
return parts.join("\n");
|
|
71
68
|
}
|
|
72
|
-
/** 安全 JSON 序列化:序列化失败时退回 String
|
|
69
|
+
/** 安全 JSON 序列化:序列化失败时退回 String 呈现。 */
|
|
73
70
|
function safeJson(value) {
|
|
74
71
|
try {
|
|
75
72
|
return JSON.stringify(value, null, 2);
|
|
@@ -77,14 +74,13 @@ function safeJson(value) {
|
|
|
77
74
|
return String(value);
|
|
78
75
|
}
|
|
79
76
|
}
|
|
80
|
-
/** 主会话判定:subagent 会话 header.origin === 'subagent'
|
|
77
|
+
/** 主会话判定:subagent 会话 header.origin === 'subagent'。 */
|
|
81
78
|
function isMainSession(session) {
|
|
82
79
|
return session.header?.origin !== "subagent";
|
|
83
80
|
}
|
|
84
81
|
/** 解析会话路由目标(provider/model),未路由时返回 undefined。 */
|
|
85
82
|
function routedTarget(session) {
|
|
86
83
|
try {
|
|
87
|
-
/** 会话请求头中的路由配置。 */
|
|
88
84
|
const config = session.requestHeader()?.config;
|
|
89
85
|
if (config?.provider && config.model) return {
|
|
90
86
|
provider: config.provider,
|
|
@@ -95,39 +91,29 @@ function routedTarget(session) {
|
|
|
95
91
|
//#endregion
|
|
96
92
|
//#region src/log-index.ts
|
|
97
93
|
/**
|
|
98
|
-
*
|
|
99
|
-
*
|
|
94
|
+
* 会话日志索引:完整消息索引与渲染。
|
|
95
|
+
* 导出 indexCompleteMessages(完整消息四类折叠索引,recall 与摘要共用同一套编号)、
|
|
96
|
+
* indexMessages / surfaceIndexOf / messageIdOfEvent(消息级定位辅助)、
|
|
97
|
+
* collectImageRefs / renderCompleteMessageParts / renderCompleteMessage(完整消息渲染
|
|
98
|
+
* 与图片附件收集)。事件日志仅追加(被遮蔽的事件仍可读,recall 依赖此性质)。
|
|
100
99
|
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
* 占一条;压缩日志中以 <sys type="KIND" index="N"> 空块表示(内容不进入输入);
|
|
105
|
-
* - assistant:模型输出文本(assistant/message 中的文本块)占一条;
|
|
106
|
-
* - toolcall:单个工具调用及其结果占一条(同一条 AI 消息里的文本与工具调用拆开,
|
|
107
|
-
* tool/result 按 source.callId 匹配其 tool-call;未匹配的 result 独立成条,防御)。
|
|
108
|
-
* 本插件自产的压缩日志消息(source.kind === 'plugin' 且 plugin 为本插件标识)不占 index。
|
|
109
|
-
* index 从 0 起、按日志顺序递增、只追加不重排 → 会话内全局稳定,
|
|
110
|
-
* 压缩后旧摘要条目引用的 index 仍然有效。
|
|
100
|
+
* 完整消息分四类:user(用户消息)、sys(系统消息,压缩日志中以 <sys> 空块表示)、
|
|
101
|
+
* assistant(模型输出文本)、toolcall(单个工具调用及其结果,result 按 callId 匹配并入)。
|
|
102
|
+
* index 从 0 起、按日志顺序递增、只追加不重排(压缩后旧摘要条目引用的 index 仍然有效)。
|
|
111
103
|
*/
|
|
112
104
|
/**
|
|
113
105
|
* 完整消息索引:按日志顺序把消息事件折叠为完整消息序列(四类,见文件头)。
|
|
114
106
|
* 工具调用结果按 source.callId 匹配其 tool-call 并入该条;未匹配的 result 独立成条(防御)。
|
|
115
|
-
*
|
|
116
|
-
* tool-call;索引对未闭合调用不设特殊处理。
|
|
107
|
+
* 本插件自产消息不占位;压缩在 agent/pre-step 触发(call-result 完备),不存在未闭合调用。
|
|
117
108
|
*/
|
|
118
109
|
function indexCompleteMessages(session) {
|
|
119
|
-
/** 完整消息序列(index = 数组下标,0 起)。 */
|
|
120
110
|
const cms = [];
|
|
121
|
-
/** 等待结果的 toolcall 完整消息(callId → cm)。 */
|
|
122
111
|
const pending = /* @__PURE__ */ new Map();
|
|
123
|
-
/** 会话全部事件(仅追加)。 */
|
|
124
112
|
const events = session.events;
|
|
125
113
|
for (let seq = 0; seq < events.length; seq += 1) {
|
|
126
|
-
/** 当前待检查事件。 */
|
|
127
114
|
const event = events[seq];
|
|
128
115
|
if (!event) continue;
|
|
129
116
|
if (event.type === "user/message") {
|
|
130
|
-
/** 事件 source(区分用户消息与宿主注入/插件消息)。 */
|
|
131
117
|
const source = event.data.source;
|
|
132
118
|
if (isPluginOwnedSource(source)) continue;
|
|
133
119
|
if (source?.kind === "user") cms.push({
|
|
@@ -142,10 +128,8 @@ function indexCompleteMessages(session) {
|
|
|
142
128
|
...source?.kind === void 0 ? {} : { kind: source.kind }
|
|
143
129
|
});
|
|
144
130
|
} else if (event.type === "assistant/message") {
|
|
145
|
-
/** 助手消息(含文本与 tool-call 块)。 */
|
|
146
131
|
const message = event.data.message;
|
|
147
132
|
if (!message || !Array.isArray(message.content)) continue;
|
|
148
|
-
/** 是否存在文本块(有则先产出 assistant 条)。 */
|
|
149
133
|
let hasText = false;
|
|
150
134
|
for (const block of message.content) if (block.type === "text") {
|
|
151
135
|
hasText = true;
|
|
@@ -158,9 +142,7 @@ function indexCompleteMessages(session) {
|
|
|
158
142
|
});
|
|
159
143
|
for (const block of message.content) {
|
|
160
144
|
if (block.type !== "tool-call") continue;
|
|
161
|
-
/** 工具调用 id(匹配 result 用)。 */
|
|
162
145
|
const callId = String(block.id ?? "");
|
|
163
|
-
/** 本调用对应的完整消息(结果随后并入)。 */
|
|
164
146
|
const cm = {
|
|
165
147
|
index: cms.length,
|
|
166
148
|
type: "toolcall",
|
|
@@ -171,11 +153,8 @@ function indexCompleteMessages(session) {
|
|
|
171
153
|
if (callId !== "") pending.set(callId, cm);
|
|
172
154
|
}
|
|
173
155
|
} else if (event.type === "tool/result") {
|
|
174
|
-
/** 结果消息的 source(callId 关联调用)。 */
|
|
175
156
|
const source = event.data.message?.source;
|
|
176
|
-
/** 关联的调用 id。 */
|
|
177
157
|
const callId = String(source?.callId ?? "");
|
|
178
|
-
/** 匹配到的 toolcall 完整消息(无则独立成条)。 */
|
|
179
158
|
const cm = callId === "" ? void 0 : pending.get(callId);
|
|
180
159
|
if (cm) {
|
|
181
160
|
cm.seqs.push(seq);
|
|
@@ -191,42 +170,66 @@ function indexCompleteMessages(session) {
|
|
|
191
170
|
return cms;
|
|
192
171
|
}
|
|
193
172
|
/**
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
* - assistant:仅文本块;
|
|
197
|
-
* - toolcall:调用块(工具名 + 参数)+ 结果文本(pruner 裁剪超大结果)。
|
|
173
|
+
* 递归收集内容块中的图片附件元数据(recall 输出保留图片用):
|
|
174
|
+
* image 块按附件元数据收集(字段不全则忽略);tool-result 块递归收集其 content。
|
|
198
175
|
*/
|
|
199
|
-
function
|
|
176
|
+
function collectImageRefs(content, out) {
|
|
177
|
+
if (!Array.isArray(content)) return;
|
|
178
|
+
for (const block of content) {
|
|
179
|
+
if (!isRecord(block)) continue;
|
|
180
|
+
if (block.type === "image") {
|
|
181
|
+
const a = block.attachment;
|
|
182
|
+
if (isRecord(a) && typeof a.attachmentId === "string" && a.attachmentId !== "" && typeof a.mediaType === "string" && typeof a.bytes === "number" && Number.isFinite(a.bytes) && typeof a.width === "number" && Number.isFinite(a.width) && typeof a.height === "number" && Number.isFinite(a.height)) out.push({
|
|
183
|
+
attachmentId: a.attachmentId,
|
|
184
|
+
mediaType: a.mediaType,
|
|
185
|
+
bytes: a.bytes,
|
|
186
|
+
width: a.width,
|
|
187
|
+
height: a.height,
|
|
188
|
+
...typeof a.name === "string" && a.name !== "" ? { name: a.name } : {}
|
|
189
|
+
});
|
|
190
|
+
} else if (block.type === "tool-result") collectImageRefs(block.content, out);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* 渲染一条完整消息为「文本 + 图片」(recall / recall-semantic 输出用):
|
|
195
|
+
* user/sys 取消息原文,assistant 取文本块,toolcall 为调用块 + 结果文本
|
|
196
|
+
* (pruner 裁剪超大结果);同时收集该条完整消息携带的图片附件(含 tool-result 嵌套,
|
|
197
|
+
* pruner 裁剪掉的图片不收集)。
|
|
198
|
+
*/
|
|
199
|
+
function renderCompleteMessageParts(session, cm, pruner) {
|
|
200
|
+
const images = [];
|
|
200
201
|
if (cm.type === "user" || cm.type === "sys") {
|
|
201
|
-
/** 用户/系统消息事件(seq 缺失则无法渲染)。 */
|
|
202
202
|
const seq = cm.seqs[0];
|
|
203
203
|
const event = seq === void 0 ? void 0 : session.events[seq];
|
|
204
|
-
/** 派生的消息对象。 */
|
|
205
204
|
const message = event ? session.deriveEventMessage(event) : null;
|
|
206
|
-
|
|
205
|
+
if (message && Array.isArray(message.content)) collectImageRefs(message.content, images);
|
|
206
|
+
return {
|
|
207
|
+
text: message ? renderMessageText(message) : "",
|
|
208
|
+
images
|
|
209
|
+
};
|
|
207
210
|
}
|
|
208
211
|
if (cm.type === "assistant") {
|
|
209
|
-
/** 助手消息事件(seq 缺失则无法渲染)。 */
|
|
210
212
|
const seq = cm.seqs[0];
|
|
211
213
|
const event = seq === void 0 ? void 0 : session.events[seq];
|
|
212
|
-
/** 派生的消息对象。 */
|
|
213
214
|
const message = event ? session.deriveEventMessage(event) : null;
|
|
214
|
-
if (!message || !Array.isArray(message.content)) return
|
|
215
|
-
|
|
215
|
+
if (!message || !Array.isArray(message.content)) return {
|
|
216
|
+
text: "",
|
|
217
|
+
images
|
|
218
|
+
};
|
|
219
|
+
collectImageRefs(message.content, images);
|
|
216
220
|
const texts = [];
|
|
217
221
|
for (const block of message.content) if (block.type === "text") texts.push(String(block.text));
|
|
218
|
-
return
|
|
222
|
+
return {
|
|
223
|
+
text: texts.join("\n"),
|
|
224
|
+
images
|
|
225
|
+
};
|
|
219
226
|
}
|
|
220
|
-
/** 渲染缓冲。 */
|
|
221
227
|
const parts = [];
|
|
222
|
-
/** 承载调用块的 assistant 事件(seq 缺失则跳过调用块)。 */
|
|
223
228
|
const callSeq = cm.seqs[0];
|
|
224
229
|
const callEvent = callSeq === void 0 ? void 0 : session.events[callSeq];
|
|
225
230
|
if (callEvent?.type === "assistant/message") {
|
|
226
|
-
/** 助手消息对象(取对应调用块)。 */
|
|
227
231
|
const message = session.deriveEventMessage(callEvent);
|
|
228
232
|
if (message && Array.isArray(message.content)) {
|
|
229
|
-
/** 匹配的 tool-call 块。 */
|
|
230
233
|
let call;
|
|
231
234
|
for (const block of message.content) if (block.type === "tool-call" && String(block.id ?? "") === (cm.callId ?? "")) {
|
|
232
235
|
call = block;
|
|
@@ -235,33 +238,35 @@ function renderCompleteMessage(session, cm, pruner) {
|
|
|
235
238
|
if (call) parts.push(`[tool-call ${String(call.name ?? "")} id=${String(call.id ?? "")}]\n${safeJson(call.arguments)}`);
|
|
236
239
|
}
|
|
237
240
|
}
|
|
238
|
-
/** 结果事件 seq(无则调用未闭合)。 */
|
|
239
241
|
const resultSeq = cm.seqs[1];
|
|
240
242
|
const resultEvent = resultSeq === void 0 ? void 0 : session.events[resultSeq];
|
|
241
243
|
if (resultEvent?.type === "tool/result") {
|
|
242
|
-
/** 结果消息对象(pruner 裁剪超大内容后渲染)。 */
|
|
243
244
|
let message = session.deriveEventMessage(resultEvent);
|
|
244
245
|
if (message && pruner?.pruneContent) {
|
|
245
|
-
/** 裁剪后的内容块。 */
|
|
246
246
|
const pruned = pruner.pruneContent(message.content);
|
|
247
247
|
if (pruned) message = {
|
|
248
248
|
...message,
|
|
249
249
|
content: pruned
|
|
250
250
|
};
|
|
251
251
|
}
|
|
252
|
-
|
|
252
|
+
if (message && Array.isArray(message.content)) collectImageRefs(message.content, images);
|
|
253
253
|
const text = message ? renderMessageText(message) : "";
|
|
254
254
|
if (text.trim() !== "") parts.push(`[result]\n${text}`);
|
|
255
255
|
}
|
|
256
|
-
return
|
|
256
|
+
return {
|
|
257
|
+
text: parts.join("\n"),
|
|
258
|
+
images
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
/** 渲染一条完整消息的文本(压缩输入与语义嵌入共用):renderCompleteMessageParts 的纯文本投影。 */
|
|
262
|
+
function renderCompleteMessage(session, cm, pruner) {
|
|
263
|
+
return renderCompleteMessageParts(session, cm, pruner).text;
|
|
257
264
|
}
|
|
258
265
|
//#endregion
|
|
259
266
|
//#region src/logger.ts
|
|
260
267
|
/**
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
* 开关配置键 debug:true 强制开启、false 强制关闭,缺省按 NODE_ENV !== 'production' 判定
|
|
264
|
-
* (默认值由 resolveConfig 解析,见 config.ts)。
|
|
268
|
+
* 插件日志门面:step 为步骤级(debug)日志,按配置开关过滤;info/warn 始终输出。
|
|
269
|
+
* 导出 PluginLogger / makeLogger。
|
|
265
270
|
*/
|
|
266
271
|
/** 构建插件日志门面:统一加 PLUGIN_LABEL 前缀,step 按 debug 开关过滤。 */
|
|
267
272
|
function makeLogger(ctx, debug) {
|
|
@@ -7368,7 +7373,7 @@ var require_dom_parser = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
7368
7373
|
exports.onWarningStopParsing = onWarningStopParsing;
|
|
7369
7374
|
}));
|
|
7370
7375
|
//#endregion
|
|
7371
|
-
//#region src/
|
|
7376
|
+
//#region src/rate-limit.ts
|
|
7372
7377
|
var import_lib = (/* @__PURE__ */ __commonJSMin(((exports) => {
|
|
7373
7378
|
var conventions = require_conventions();
|
|
7374
7379
|
exports.assign = conventions.assign;
|
|
@@ -7409,14 +7414,66 @@ var import_lib = (/* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
7409
7414
|
exports.onWarningStopParsing = domParser.onWarningStopParsing;
|
|
7410
7415
|
})))();
|
|
7411
7416
|
/**
|
|
7417
|
+
* 全局限流门(插件进程级共享状态):任一摘要请求遇 429 后进入冷却期,
|
|
7418
|
+
* 此后所有摘要请求发出前先等待到「最近一次 429 + rateLimitWaitMs」之后。
|
|
7419
|
+
* 等待期间 signal 中止则立即放弃;等待期间的新 429 顺延冷却期。
|
|
7420
|
+
* 导出 isRateLimitError / noteRateLimit / gateRateLimit / RATE_LIMIT_WAIT_MS_DEFAULT /
|
|
7421
|
+
* resetRateLimitGate。
|
|
7422
|
+
*/
|
|
7423
|
+
/** 最近一次 429 限流的时间戳(ms;null = 未遇过限流,门直接放行)。 */
|
|
7424
|
+
let lastRateLimitAt = null;
|
|
7425
|
+
/** 判定错误信息是否为 429 限流(匹配 429 或 rate limit,大小写不敏感)。 */
|
|
7426
|
+
function isRateLimitError(message) {
|
|
7427
|
+
return /\b429\b|rate[\s_-]?limit/i.test(message);
|
|
7428
|
+
}
|
|
7429
|
+
/** 记录一次 429 限流:把冷却期起点更新为当前时间。 */
|
|
7430
|
+
function noteRateLimit() {
|
|
7431
|
+
lastRateLimitAt = Date.now();
|
|
7432
|
+
}
|
|
7433
|
+
/** 可中止延时:等待满 ms 返回 true;等待期间 signal 中止返回 false。 */
|
|
7434
|
+
function delay(ms, signal) {
|
|
7435
|
+
return new Promise((resolve) => {
|
|
7436
|
+
if (signal?.aborted) {
|
|
7437
|
+
resolve(false);
|
|
7438
|
+
return;
|
|
7439
|
+
}
|
|
7440
|
+
const timer = setTimeout(() => {
|
|
7441
|
+
cleanup();
|
|
7442
|
+
resolve(true);
|
|
7443
|
+
}, ms);
|
|
7444
|
+
const onAbort = () => {
|
|
7445
|
+
cleanup();
|
|
7446
|
+
resolve(false);
|
|
7447
|
+
};
|
|
7448
|
+
const cleanup = () => {
|
|
7449
|
+
clearTimeout(timer);
|
|
7450
|
+
signal?.removeEventListener("abort", onAbort);
|
|
7451
|
+
};
|
|
7452
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
7453
|
+
});
|
|
7454
|
+
}
|
|
7455
|
+
/**
|
|
7456
|
+
* 限流等待门:处于 429 冷却期时等待到期限再放行(返回 true);
|
|
7457
|
+
* 等待期间 signal 中止返回 false。未遇过限流或冷却期已过立即放行。waitMs ≤ 0 视为不限流。
|
|
7458
|
+
*/
|
|
7459
|
+
async function gateRateLimit(waitMs, signal) {
|
|
7460
|
+
if (waitMs <= 0) return true;
|
|
7461
|
+
while (lastRateLimitAt !== null) {
|
|
7462
|
+
const remaining = lastRateLimitAt + waitMs - Date.now();
|
|
7463
|
+
if (remaining <= 0) return true;
|
|
7464
|
+
if (!await delay(remaining, signal)) return false;
|
|
7465
|
+
}
|
|
7466
|
+
return true;
|
|
7467
|
+
}
|
|
7468
|
+
//#endregion
|
|
7469
|
+
//#region src/summarize.ts
|
|
7470
|
+
/**
|
|
7412
7471
|
* 共享压缩提示词(观察/反思同一套):定义 history 块(模型消息 + index 的表达形式)、
|
|
7413
|
-
*
|
|
7414
|
-
* index
|
|
7415
|
-
* mode 控制【摘要粒度】:无参为通用提示词(反思调用);'summary' 要求简单摘要(较早分块);
|
|
7416
|
-
* 'detailed' 要求越往后越细(最后一块保留细节)。观察分块时除最后一块外均用 'summary'。
|
|
7472
|
+
* 完整消息定义、压缩要求(完整保留用户/系统消息、reasoning 仅参考、关联 assistant
|
|
7473
|
+
* 合并为模块、index 连续)、输出格式与数据源说明。
|
|
7417
7474
|
*/
|
|
7418
|
-
function buildHistoryPrompt(
|
|
7419
|
-
|
|
7475
|
+
function buildHistoryPrompt() {
|
|
7476
|
+
return [
|
|
7420
7477
|
"把下方的 <history> 消息记录压缩为一份更紧凑的 <history> 压缩日志。不用工具、不展示思考、不输出多余文字。",
|
|
7421
7478
|
"",
|
|
7422
7479
|
"【history 块定义】",
|
|
@@ -7452,37 +7509,23 @@ function buildHistoryPrompt(mode) {
|
|
|
7452
7509
|
`</${HISTORY_TAG}>`,
|
|
7453
7510
|
"",
|
|
7454
7511
|
"【数据源】下方的 <history> 消息记录是本次要压缩的全部消息;压缩结果作为一个新的 <history> 块输出。"
|
|
7455
|
-
];
|
|
7456
|
-
if (mode !== void 0) {
|
|
7457
|
-
const granularity = mode === "summary" ? "【摘要粒度】本条为较早的消息,做简单摘要即可:概括要点,不必展开细节;但用户消息仍逐条保留原文,不概括、不省略。" : "【摘要粒度】本条为最近的消息,越往后越细:靠近末尾的完整消息保留更多细节(关键文件、改动与结论),前面的可适当从简;但用户消息始终逐条保留原文,不概括、不省略。";
|
|
7458
|
-
const marker = "【输出格式】只输出一个 <history> 包裹的合法 XML 日志块";
|
|
7459
|
-
const markerIndex = lines.findIndex((line) => line.includes(marker));
|
|
7460
|
-
if (markerIndex !== -1) lines.splice(markerIndex, 0, granularity);
|
|
7461
|
-
}
|
|
7462
|
-
return lines.join("\n");
|
|
7512
|
+
].join("\n");
|
|
7463
7513
|
}
|
|
7464
|
-
/** 渲染用户消息条目(DOM
|
|
7514
|
+
/** 渲染用户消息条目(DOM 元素):文本块原样;图片/文件等非文本块以注释补充。 */
|
|
7465
7515
|
function renderUserEntry(doc, session, cm) {
|
|
7466
|
-
/** 用户消息事件(seq 缺失则无法渲染)。 */
|
|
7467
7516
|
const seq = cm.seqs[0];
|
|
7468
7517
|
const event = seq === void 0 ? void 0 : session.events[seq];
|
|
7469
|
-
/** 派生的消息对象。 */
|
|
7470
7518
|
const message = event ? session.deriveEventMessage(event) : null;
|
|
7471
7519
|
if (!message || !Array.isArray(message.content)) return null;
|
|
7472
|
-
/** user_message 元素(文本 + 注释)。 */
|
|
7473
7520
|
const el = doc.createElement("user_message");
|
|
7474
7521
|
el.setAttribute("index", String(cm.index));
|
|
7475
|
-
/** 是否存在可输出内容(文本或注释)。 */
|
|
7476
7522
|
let hasContent = false;
|
|
7477
7523
|
for (const block of message.content) if (block.type === "text") {
|
|
7478
7524
|
el.appendChild(doc.createTextNode(String(block.text)));
|
|
7479
7525
|
hasContent = true;
|
|
7480
7526
|
} else if (block.type === "image") {
|
|
7481
|
-
/** 图片附件元数据(名称 / 媒体类型 / 尺寸 / 字节数)。 */
|
|
7482
7527
|
const ref = block.attachment;
|
|
7483
|
-
/** 显示名(有则附上)。 */
|
|
7484
7528
|
const name = ref?.name ? `:${ref.name}` : "";
|
|
7485
|
-
/** 元数据串。 */
|
|
7486
7529
|
const meta = ref ? `(${String(ref.mediaType ?? "")} ${String(ref.width ?? "")}×${String(ref.height ?? "")},${String(ref.bytes ?? "")} bytes)` : "";
|
|
7487
7530
|
el.appendChild(doc.createComment(` 图片附件${name}${meta} `));
|
|
7488
7531
|
hasContent = true;
|
|
@@ -7494,40 +7537,29 @@ function renderUserEntry(doc, session, cm) {
|
|
|
7494
7537
|
return el;
|
|
7495
7538
|
}
|
|
7496
7539
|
/**
|
|
7497
|
-
*
|
|
7498
|
-
*
|
|
7499
|
-
*
|
|
7500
|
-
*
|
|
7501
|
-
* - assistant / toolcall → <assistant index="N">(模型输出文本 / toolcall&result 原样)。
|
|
7502
|
-
* 文本经 XML 序列化自动转义(用户输入 / 文本 / reasoning 中的特殊字符不破坏 XML 合法性)。
|
|
7503
|
-
* 仅渲染 seqs 全部落在给定集合内的完整消息(本插件自产消息天然不占位)。
|
|
7540
|
+
* 渲染完整消息记录(观察输入):输出一个合法的 <history> 块——
|
|
7541
|
+
* user → <user_message>(文本原样、图片注释)、sys → <sys> 空块、assistant 的
|
|
7542
|
+
* reasoning → <reasoning>(参考条目)、assistant/toolcall → <assistant>(原样文本)。
|
|
7543
|
+
* 文本经 XML 序列化自动转义;仅渲染 seqs 全部落在给定集合内的完整消息。
|
|
7504
7544
|
*/
|
|
7505
7545
|
function renderMessages(session, seqs) {
|
|
7506
|
-
/** 遮蔽 seq 集合。 */
|
|
7507
7546
|
const shadowed = new Set(seqs);
|
|
7508
|
-
/** 各 assistant 消息的 reasoning 文本(seq → 文本列表,按日志顺序)。 */
|
|
7509
7547
|
const reasoningBySeq = /* @__PURE__ */ new Map();
|
|
7510
7548
|
for (const seq of seqs) {
|
|
7511
|
-
/** 当前 assistant 消息(非消息事件跳过)。 */
|
|
7512
7549
|
const event = session.events[seq];
|
|
7513
7550
|
if (event?.type !== "assistant/message") continue;
|
|
7514
7551
|
const message = event.data.message;
|
|
7515
7552
|
if (!message || !Array.isArray(message.content)) continue;
|
|
7516
|
-
/** 该消息的 reasoning 块文本。 */
|
|
7517
7553
|
const reasonings = [];
|
|
7518
7554
|
for (const block of message.content) if (block.type === "reasoning" && typeof block.text === "string") reasonings.push(block.text);
|
|
7519
7555
|
if (reasonings.length > 0) reasoningBySeq.set(seq, reasonings);
|
|
7520
7556
|
}
|
|
7521
|
-
/** 已输出 reasoning 的 assistant 消息 seq(每条消息的 reasoning 只输出一次)。 */
|
|
7522
7557
|
const emittedReasoning = /* @__PURE__ */ new Set();
|
|
7523
|
-
/** DOM 文档(构建条目元素)。 */
|
|
7524
7558
|
const doc = new import_lib.DOMParser().parseFromString(`<${HISTORY_TAG} />`, "text/xml");
|
|
7525
|
-
/** 顶层条目元素(按日志顺序)。 */
|
|
7526
7559
|
const entries = [];
|
|
7527
7560
|
for (const cm of indexCompleteMessages(session)) {
|
|
7528
7561
|
if (!cm.seqs.every((seq) => shadowed.has(seq))) continue;
|
|
7529
7562
|
if (cm.type === "sys") {
|
|
7530
|
-
/** sys 元素(type=source.kind,index=完整消息序号)。 */
|
|
7531
7563
|
const sysEl = doc.createElement("sys");
|
|
7532
7564
|
sysEl.setAttribute("type", cm.kind ?? "");
|
|
7533
7565
|
sysEl.setAttribute("index", String(cm.index));
|
|
@@ -7536,45 +7568,35 @@ function renderMessages(session, seqs) {
|
|
|
7536
7568
|
continue;
|
|
7537
7569
|
}
|
|
7538
7570
|
if (cm.type === "user") {
|
|
7539
|
-
/** 用户消息条目(无文本且无注释则跳过)。 */
|
|
7540
7571
|
const rendered = renderUserEntry(doc, session, cm);
|
|
7541
7572
|
if (rendered === null) continue;
|
|
7542
7573
|
entries.push(rendered);
|
|
7543
7574
|
} else {
|
|
7544
|
-
/** 承载该条完整消息的 assistant seq(缺失则无 reasoning 可输出)。 */
|
|
7545
7575
|
const callSeq = cm.seqs[0];
|
|
7546
|
-
/** 该消息的 reasoning(无则跳过)。 */
|
|
7547
7576
|
const reasonings = callSeq === void 0 ? void 0 : reasoningBySeq.get(callSeq);
|
|
7548
7577
|
if (callSeq !== void 0 && reasonings !== void 0 && !emittedReasoning.has(callSeq)) {
|
|
7549
7578
|
emittedReasoning.add(callSeq);
|
|
7550
7579
|
for (const text of reasonings) {
|
|
7551
|
-
/** reasoning 元素(文本自动转义)。 */
|
|
7552
7580
|
const re = doc.createElement("reasoning");
|
|
7553
7581
|
re.appendChild(doc.createTextNode(text));
|
|
7554
7582
|
entries.push(re);
|
|
7555
7583
|
}
|
|
7556
7584
|
}
|
|
7557
|
-
/** 该条完整消息的文本(assistant=文本;toolcall=调用参数+结果,原样)。 */
|
|
7558
7585
|
const text = renderCompleteMessage(session, cm);
|
|
7559
7586
|
if (text.trim() === "") continue;
|
|
7560
|
-
/** assistant 元素(文本自动转义)。 */
|
|
7561
7587
|
const el = doc.createElement("assistant");
|
|
7562
7588
|
el.setAttribute("index", String(cm.index));
|
|
7563
7589
|
el.appendChild(doc.createTextNode(text));
|
|
7564
7590
|
entries.push(el);
|
|
7565
7591
|
}
|
|
7566
7592
|
}
|
|
7567
|
-
/** XML 序列化器(文本/注释自动转义)。 */
|
|
7568
7593
|
const serializer = new import_lib.XMLSerializer();
|
|
7569
7594
|
return `<${HISTORY_TAG}>\n${entries.map((el) => serializer.serializeToString(el)).join("\n")}\n</${HISTORY_TAG}>`;
|
|
7570
7595
|
}
|
|
7571
|
-
/** 流收集器:提取文本输出 + usage + finish(不依赖宿主 BlockAssembler
|
|
7596
|
+
/** 流收集器:提取文本输出 + usage + finish(不依赖宿主 BlockAssembler)。 */
|
|
7572
7597
|
var StreamCollector = class {
|
|
7573
|
-
/** 文本输出缓冲(text-delta 拼接;reasoning 不计入)。 */
|
|
7574
7598
|
textBuf = "";
|
|
7575
|
-
/** usage chunk(无则 undefined)。 */
|
|
7576
7599
|
_usage;
|
|
7577
|
-
/** finish chunk(流结束仍无则视为 stop)。 */
|
|
7578
7600
|
_finish;
|
|
7579
7601
|
/** 喂入一个流 chunk(仅消费文本/usage/finish,其余忽略)。 */
|
|
7580
7602
|
push(chunk) {
|
|
@@ -7601,7 +7623,7 @@ var StreamCollector = class {
|
|
|
7601
7623
|
return this._finish ?? { kind: "stop" };
|
|
7602
7624
|
}
|
|
7603
7625
|
};
|
|
7604
|
-
/** 构造插件自产 user
|
|
7626
|
+
/** 构造插件自产 user 消息(摘要调用的输入消息;id 为品牌类型 MessageId)。 */
|
|
7605
7627
|
function makePluginUserMessage(text) {
|
|
7606
7628
|
return {
|
|
7607
7629
|
id: uuid(),
|
|
@@ -7617,8 +7639,8 @@ function makePluginUserMessage(text) {
|
|
|
7617
7639
|
};
|
|
7618
7640
|
}
|
|
7619
7641
|
/**
|
|
7620
|
-
*
|
|
7621
|
-
*
|
|
7642
|
+
* 构建摘要请求选项:指令作为 system,渲染输入作为唯一的 user 消息,
|
|
7643
|
+
* 不沿用主会话请求前缀(前缀复用需模型自行计数,导致索引异常)。
|
|
7622
7644
|
*/
|
|
7623
7645
|
function buildSummaryOptions(session, instruction, contextText, maxTokens, target, signal) {
|
|
7624
7646
|
return {
|
|
@@ -7636,7 +7658,6 @@ function buildSummaryOptions(session, instruction, contextText, maxTokens, targe
|
|
|
7636
7658
|
const HISTORY_FORMAT_NOTE = `<!-- 完整消息:${COMPLETE_MESSAGE_DEFINITION} <TAG index="N">表示单条完整消息,<TAG start="A" end="B"> 表示连续模块,start/end 是首尾完整消息的 index;<sys type="KIND" index="N"> 表示被压缩的系统消息,块中为空 -->`;
|
|
7637
7659
|
/** 读取元素整数属性(非负整数;缺失 / 非数字返回 undefined)。 */
|
|
7638
7660
|
function intAttr(el, name) {
|
|
7639
|
-
/** 属性原始值(缺失为 null)。 */
|
|
7640
7661
|
const raw = el.getAttribute(name);
|
|
7641
7662
|
if (raw === null || raw === "") return void 0;
|
|
7642
7663
|
if (!/^\d+$/.test(raw)) return void 0;
|
|
@@ -7644,43 +7665,31 @@ function intAttr(el, name) {
|
|
|
7644
7665
|
}
|
|
7645
7666
|
/**
|
|
7646
7667
|
* 用 XML 解析器解析一个 <history> 块(结构合法性校验):
|
|
7647
|
-
*
|
|
7648
|
-
*
|
|
7649
|
-
* - 顶层只允许 user_message / assistant / sys 条目元素(其余元素类型视为不合法);
|
|
7650
|
-
* - 出现 <reasoning> 元素时标记 hasReasoning(产物不允许)。
|
|
7651
|
-
* 条目按文档顺序提取(index / start / end 属性缺失或不合法 → 不合法)。
|
|
7668
|
+
* 非法 XML / 根非 <history> / 顶层出现未定义元素 / 条目属性缺失或非法 → null;
|
|
7669
|
+
* 出现 <reasoning> 元素时标记 hasReasoning(产物不允许)。
|
|
7652
7670
|
*/
|
|
7653
7671
|
function parseHistoryBlock(xml) {
|
|
7654
|
-
/** DOM 文档(解析失败抛错)。 */
|
|
7655
7672
|
let doc;
|
|
7656
7673
|
try {
|
|
7657
7674
|
doc = new import_lib.DOMParser().parseFromString(xml, "text/xml");
|
|
7658
7675
|
} catch {
|
|
7659
7676
|
return null;
|
|
7660
7677
|
}
|
|
7661
|
-
/** 根节点(必须为 <history>)。 */
|
|
7662
7678
|
const root = doc.documentElement;
|
|
7663
7679
|
if (!root || root.nodeName !== "history") return null;
|
|
7664
|
-
/** 条目缓冲区。 */
|
|
7665
7680
|
const entries = [];
|
|
7666
|
-
/** 是否含 reasoning 元素。 */
|
|
7667
7681
|
let hasReasoning = false;
|
|
7668
|
-
/** 根的子节点。 */
|
|
7669
7682
|
const children = root.childNodes;
|
|
7670
7683
|
for (let i = 0; i < children.length; i += 1) {
|
|
7671
|
-
/** 当前子节点(跳过文本与注释)。 */
|
|
7672
7684
|
const node = children[i];
|
|
7673
7685
|
if (node?.nodeType !== 1) continue;
|
|
7674
|
-
/** 子元素(节点类型收窄)。 */
|
|
7675
7686
|
const el = node;
|
|
7676
|
-
/** 元素标签名。 */
|
|
7677
7687
|
const tag = el.nodeName;
|
|
7678
7688
|
if (tag === "reasoning") {
|
|
7679
7689
|
hasReasoning = true;
|
|
7680
7690
|
continue;
|
|
7681
7691
|
}
|
|
7682
7692
|
if (tag === "user_message") {
|
|
7683
|
-
/** index 属性(缺失/非法 → 不合法)。 */
|
|
7684
7693
|
const index = intAttr(el, "index");
|
|
7685
7694
|
if (index === void 0) return null;
|
|
7686
7695
|
entries.push({
|
|
@@ -7688,7 +7697,6 @@ function parseHistoryBlock(xml) {
|
|
|
7688
7697
|
index
|
|
7689
7698
|
});
|
|
7690
7699
|
} else if (tag === "sys") {
|
|
7691
|
-
/** index 属性(缺失/非法 → 不合法)。 */
|
|
7692
7700
|
const index = intAttr(el, "index");
|
|
7693
7701
|
if (index === void 0) return null;
|
|
7694
7702
|
entries.push({
|
|
@@ -7696,7 +7704,6 @@ function parseHistoryBlock(xml) {
|
|
|
7696
7704
|
index
|
|
7697
7705
|
});
|
|
7698
7706
|
} else if (tag === "assistant") {
|
|
7699
|
-
/** 单条 index 或模块 start..end(二选一)。 */
|
|
7700
7707
|
const index = intAttr(el, "index");
|
|
7701
7708
|
if (index !== void 0) entries.push({
|
|
7702
7709
|
kind: "assistant",
|
|
@@ -7721,13 +7728,11 @@ function parseHistoryBlock(xml) {
|
|
|
7721
7728
|
}
|
|
7722
7729
|
/**
|
|
7723
7730
|
* 解析文本中全部 <history> 块内的条目(反思输入为多个块拼接:逐块解析提取)。
|
|
7724
|
-
*
|
|
7731
|
+
* 非法块跳过;仅提取不校验顺序(连续性由 historyContinuity 校验)。
|
|
7725
7732
|
*/
|
|
7726
7733
|
function parseHistoryEntries(text) {
|
|
7727
|
-
/** 条目缓冲区。 */
|
|
7728
7734
|
const out = [];
|
|
7729
7735
|
for (const m of text.matchAll(/<history[\s\S]*?<\/history>/g)) {
|
|
7730
|
-
/** 单块解析结果(非法块跳过)。 */
|
|
7731
7736
|
const parsed = parseHistoryBlock(m[0]);
|
|
7732
7737
|
if (parsed === null) continue;
|
|
7733
7738
|
out.push(...parsed.entries);
|
|
@@ -7735,16 +7740,13 @@ function parseHistoryEntries(text) {
|
|
|
7735
7740
|
return out;
|
|
7736
7741
|
}
|
|
7737
7742
|
/**
|
|
7738
|
-
* 校验条目 index/start/end
|
|
7739
|
-
*
|
|
7740
|
-
* 空条目 / 非法范围(start > end) / 跳号、重叠或乱序 → 返回 null。
|
|
7743
|
+
* 校验条目 index/start/end 连续性:每条给出覆盖区间,按出现顺序相邻条目必须首尾相接,
|
|
7744
|
+
* 返回整体覆盖区间;空条目 / 非法范围 / 跳号、重叠或乱序 → 返回 null。
|
|
7741
7745
|
*/
|
|
7742
7746
|
function historyContinuity(entries) {
|
|
7743
7747
|
if (entries.length === 0) return null;
|
|
7744
|
-
/** 各条目覆盖区间(保持块内出现顺序)。 */
|
|
7745
7748
|
const ranges = [];
|
|
7746
7749
|
for (const e of entries) {
|
|
7747
|
-
/** 模块(start/end)或单条(index 为自身)。 */
|
|
7748
7750
|
const lo = e.kind === "assistant" && e.start !== void 0 ? e.start : e.index ?? 0;
|
|
7749
7751
|
const hi = e.kind === "assistant" && e.end !== void 0 ? e.end : e.index ?? 0;
|
|
7750
7752
|
if (lo > hi) return null;
|
|
@@ -7754,13 +7756,11 @@ function historyContinuity(entries) {
|
|
|
7754
7756
|
});
|
|
7755
7757
|
}
|
|
7756
7758
|
for (let i = 1; i < ranges.length; i += 1) {
|
|
7757
|
-
/** 相邻两条(防御性判空)。 */
|
|
7758
7759
|
const prev = ranges[i - 1];
|
|
7759
7760
|
const curr = ranges[i];
|
|
7760
7761
|
if (prev === void 0 || curr === void 0) return null;
|
|
7761
7762
|
if (curr.lo !== prev.hi + 1) return null;
|
|
7762
7763
|
}
|
|
7763
|
-
/** 首尾条目(防御性判空)。 */
|
|
7764
7764
|
const first = ranges[0];
|
|
7765
7765
|
const last = ranges[ranges.length - 1];
|
|
7766
7766
|
if (first === void 0 || last === void 0) return null;
|
|
@@ -7769,83 +7769,141 @@ function historyContinuity(entries) {
|
|
|
7769
7769
|
end: last.hi
|
|
7770
7770
|
};
|
|
7771
7771
|
}
|
|
7772
|
+
/** <history> 开标签(允许携带属性):模糊定位输出中日志块起点。 */
|
|
7773
|
+
const HISTORY_OPEN_TAG_RE = /<history(\s[^>]*)?>/;
|
|
7774
|
+
/** history 块内条目标签的 token 正则(开 / 闭 / 自闭合):模糊提取按标签逐个扫描配对。 */
|
|
7775
|
+
const HISTORY_ENTRY_TOKEN_RE = /<(\/)?(user_message|sys|assistant|reasoning)\b([^>]*?)(\/)?>/g;
|
|
7776
|
+
/** 解析标签属性串为键值对(支持双引号 / 单引号 / 无引号取值;非法片段忽略)。 */
|
|
7777
|
+
function parseTagAttrs(raw) {
|
|
7778
|
+
const attrs = {};
|
|
7779
|
+
for (const m of raw.matchAll(/([^\s=/]+)\s*=\s*("([^"]*)"|'([^']*)'|([^\s"'>]+))/g)) {
|
|
7780
|
+
const key = m[1] ?? "";
|
|
7781
|
+
const value = m[3] ?? m[4] ?? m[5] ?? "";
|
|
7782
|
+
if (key !== "") attrs[key] = value;
|
|
7783
|
+
}
|
|
7784
|
+
return attrs;
|
|
7785
|
+
}
|
|
7786
|
+
/**
|
|
7787
|
+
* 解码 XML 预定义实体(& 最后解码,避免 &lt; 之类被二次解码)。
|
|
7788
|
+
* 模糊提取的条目文本解码后经 XML 序列化重新转义,已有转义形式不发生二次转义。
|
|
7789
|
+
*/
|
|
7790
|
+
function decodeXmlEntities(text) {
|
|
7791
|
+
return text.replace(/</g, "<").replace(/>/g, ">").replace(/"/g, "\"").replace(/'/g, "'").replace(/&/g, "&");
|
|
7792
|
+
}
|
|
7793
|
+
/**
|
|
7794
|
+
* 模糊重建 <history> 块(xmldom 原生无模糊匹配,按条目标签做字符串级扫描配对):
|
|
7795
|
+
* 依次提取 user_message / sys / assistant / reasoning 条目——开闭标签就近配对、
|
|
7796
|
+
* 自闭合直接成条、未闭合条目以文本末尾收口;未知元素与其间杂文忽略;条目文本
|
|
7797
|
+
* 解码后重新序列化为合法 XML。扫描不到任何条目返回 null。
|
|
7798
|
+
*/
|
|
7799
|
+
function rebuildHistoryBlock(inner) {
|
|
7800
|
+
const entries = [];
|
|
7801
|
+
const stack = [];
|
|
7802
|
+
for (const m of inner.matchAll(HISTORY_ENTRY_TOKEN_RE)) {
|
|
7803
|
+
const tag = m[2] ?? "";
|
|
7804
|
+
if (m[1] === "/") for (let i = stack.length - 1; i >= 0; i -= 1) {
|
|
7805
|
+
const open = stack[i];
|
|
7806
|
+
if (open?.tag === tag) {
|
|
7807
|
+
entries.push({
|
|
7808
|
+
tag,
|
|
7809
|
+
attrs: open.attrs,
|
|
7810
|
+
content: inner.slice(open.contentFrom, m.index)
|
|
7811
|
+
});
|
|
7812
|
+
stack.splice(i, 1);
|
|
7813
|
+
break;
|
|
7814
|
+
}
|
|
7815
|
+
}
|
|
7816
|
+
else if (m[4] === "/") entries.push({
|
|
7817
|
+
tag,
|
|
7818
|
+
attrs: parseTagAttrs(m[3] ?? ""),
|
|
7819
|
+
content: ""
|
|
7820
|
+
});
|
|
7821
|
+
else stack.push({
|
|
7822
|
+
tag,
|
|
7823
|
+
attrs: parseTagAttrs(m[3] ?? ""),
|
|
7824
|
+
contentFrom: m.index + m[0].length
|
|
7825
|
+
});
|
|
7826
|
+
}
|
|
7827
|
+
for (const open of stack) entries.push({
|
|
7828
|
+
tag: open.tag,
|
|
7829
|
+
attrs: open.attrs,
|
|
7830
|
+
content: inner.slice(open.contentFrom)
|
|
7831
|
+
});
|
|
7832
|
+
if (entries.length === 0) return null;
|
|
7833
|
+
const doc = new import_lib.DOMParser().parseFromString(`<${HISTORY_TAG} />`, "text/xml");
|
|
7834
|
+
const root = doc.documentElement;
|
|
7835
|
+
if (!root) return null;
|
|
7836
|
+
for (const e of entries) {
|
|
7837
|
+
const el = doc.createElement(e.tag);
|
|
7838
|
+
for (const [key, value] of Object.entries(e.attrs)) el.setAttribute(key, decodeXmlEntities(value));
|
|
7839
|
+
el.appendChild(doc.createTextNode(decodeXmlEntities(e.content)));
|
|
7840
|
+
root.appendChild(el);
|
|
7841
|
+
}
|
|
7842
|
+
return new import_lib.XMLSerializer().serializeToString(root);
|
|
7843
|
+
}
|
|
7772
7844
|
/**
|
|
7773
7845
|
* 从 AI 摘要输出中提取合法日志(不信任 AI 的总结结果):
|
|
7774
|
-
*
|
|
7775
|
-
*
|
|
7776
|
-
*
|
|
7777
|
-
*
|
|
7778
|
-
*
|
|
7779
|
-
* - index/start/end 连续性校验(historyContinuity + 与 expected 覆盖区间一致);
|
|
7780
|
-
* - 产出后把首个开标签改写为带 tip 属性的版本(对 AI 的提醒),并在其后插入
|
|
7781
|
-
* 格式说明注释(HISTORY_FORMAT_NOTE,块顶)。
|
|
7846
|
+
* 取首个 <history> 开标签(允许带属性)到最后一个 </history> 切为候选块,
|
|
7847
|
+
* 找不到或内容过短返回 null;候选块经 XML 结构校验,整块非法时按条目模糊
|
|
7848
|
+
* 提取重建为合法块(不要求模型输出整体合法 XML);随后统一校验:无 reasoning、
|
|
7849
|
+
* index 连续且与 expected 覆盖区间一致;通过后统一改写为带 tip 属性的开标签
|
|
7850
|
+
* 并在块顶插入格式说明注释。
|
|
7782
7851
|
*/
|
|
7783
7852
|
function extractSummaryLog(raw, expected) {
|
|
7784
|
-
/** 开标签(模型输出格式)。 */
|
|
7785
|
-
const openTag = `<${HISTORY_TAG}>`;
|
|
7786
|
-
/** 闭标签。 */
|
|
7787
7853
|
const closeTag = `</${HISTORY_TAG}>`;
|
|
7788
|
-
|
|
7789
|
-
const
|
|
7790
|
-
/** 首个开标签位置(无则 -1)。 */
|
|
7791
|
-
const open = raw.indexOf(openTag);
|
|
7792
|
-
/** 最后一个闭标签位置(无则 -1)。 */
|
|
7854
|
+
const openMatch = HISTORY_OPEN_TAG_RE.exec(raw);
|
|
7855
|
+
const open = openMatch?.index ?? -1;
|
|
7793
7856
|
const close = raw.lastIndexOf(closeTag);
|
|
7794
|
-
if (
|
|
7795
|
-
|
|
7796
|
-
|
|
7857
|
+
if (openMatch === null || close === -1 || close < open) return null;
|
|
7858
|
+
const inner = raw.slice(open + openMatch[0].length, close);
|
|
7859
|
+
if (inner.trim().length < 10) return null;
|
|
7797
7860
|
const block = raw.slice(open, close + closeTag.length);
|
|
7798
|
-
|
|
7799
|
-
|
|
7861
|
+
const candidate = parseHistoryBlock(block) === null ? rebuildHistoryBlock(inner) : block;
|
|
7862
|
+
if (candidate === null) return null;
|
|
7863
|
+
const parsed = parseHistoryBlock(candidate);
|
|
7800
7864
|
if (parsed === null) return null;
|
|
7801
7865
|
if (parsed.hasReasoning) return null;
|
|
7802
|
-
/** 条目覆盖区间。 */
|
|
7803
7866
|
const span = historyContinuity(parsed.entries);
|
|
7804
7867
|
if (span === null) return null;
|
|
7805
7868
|
if (expected?.start !== void 0 && span.start !== expected.start) return null;
|
|
7806
7869
|
if (expected?.end !== void 0 && span.end !== expected.end) return null;
|
|
7807
|
-
|
|
7808
|
-
return
|
|
7870
|
+
const candidateInner = candidate.slice(candidate.indexOf(">") + 1, candidate.length - closeTag.length).trim();
|
|
7871
|
+
return `<${HISTORY_TAG} tip="${HISTORY_TIP}">\n${HISTORY_FORMAT_NOTE}\n${candidateInner}\n</${HISTORY_TAG}>`;
|
|
7809
7872
|
}
|
|
7810
7873
|
/**
|
|
7811
7874
|
* 直连 LLM 执行一次摘要(观察或反思),返回文本与可选 token usage。
|
|
7812
|
-
* 失败(抛异常 / 空输出 / 非 stop 结束 /
|
|
7813
|
-
* options.maxAttempts
|
|
7814
|
-
*
|
|
7875
|
+
* 失败(抛异常 / 空输出 / 非 stop 结束 / 校验不通过)均记录日志并重试,最多尝试
|
|
7876
|
+
* options.maxAttempts 次;全部失败返回 null(不产生任何日志变更)。每次请求发出前
|
|
7877
|
+
* 先过全局限流等待门。
|
|
7815
7878
|
*/
|
|
7816
7879
|
async function runSummarySubagent(ctx, agent, instruction, contextText, maxTokens, target, debug, signal, options) {
|
|
7817
|
-
/** 当前会话。 */
|
|
7818
7880
|
const session = agent.session;
|
|
7819
|
-
/** 插件日志门面(失败日志始终输出)。 */
|
|
7820
7881
|
const logger = makeLogger(ctx, debug);
|
|
7821
|
-
/** 总尝试次数(首次 + 重试;缺省兜底)。 */
|
|
7822
7882
|
const maxAttempts = options?.maxAttempts ?? 11;
|
|
7823
|
-
/** 最后一次失败的原因(error=调用异常 / finish=未完成原因;最终失败日志使用)。 */
|
|
7824
7883
|
let lastFailure = {};
|
|
7825
7884
|
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
7826
7885
|
if (signal?.aborted) {
|
|
7827
7886
|
logger.warn(`摘要调用中止(第 ${attempt}/${maxAttempts} 次尝试前 signal 已中止),放弃本次摘要`);
|
|
7828
7887
|
return null;
|
|
7829
7888
|
}
|
|
7889
|
+
const rateLimitWaitMs = options?.rateLimitWaitMs ?? 6e4;
|
|
7890
|
+
if (!await gateRateLimit(rateLimitWaitMs, signal)) {
|
|
7891
|
+
logger.warn(`摘要调用中止(第 ${attempt}/${maxAttempts} 次尝试前限流等待被 signal 中止),放弃本次摘要`);
|
|
7892
|
+
return null;
|
|
7893
|
+
}
|
|
7830
7894
|
logger.step(`摘要调用开始(第 ${attempt}/${maxAttempts} 次,provider ${target.provider},model ${target.model},maxTokens ${maxTokens})`);
|
|
7831
7895
|
try {
|
|
7832
|
-
/** 摘要请求选项(new 方式组装)。 */
|
|
7833
7896
|
const requestOptions = buildSummaryOptions(session, instruction, contextText, maxTokens, target, signal);
|
|
7834
|
-
/** 流收集器(文本/usage/finish)。 */
|
|
7835
7897
|
const collector = new StreamCollector();
|
|
7836
7898
|
for await (const chunk of ctx.llm.stream(requestOptions)) collector.push(chunk);
|
|
7837
|
-
/** 提取合法日志(首个 <history> 到最后一个 </history>,无 reasoning、index 连续;不信任 AI 输出)。 */
|
|
7838
7899
|
const text = extractSummaryLog(collector.text, options?.expected);
|
|
7839
|
-
/** 终止原因(仅 stop 视为完成)。 */
|
|
7840
7900
|
const finish = collector.finish;
|
|
7841
7901
|
if (finish.kind !== "stop" || text === null) {
|
|
7842
|
-
/** 未完成原因(空输出 / 非法日志 / 校验不通过 / 非 stop 终止原因)。 */
|
|
7843
7902
|
const reason = finish.kind === "stop" ? collector.text.trim() === "" ? "无输出" : "缺少合法 <history> 块(含 reasoning 或 index 不连续)" : String(finish.kind);
|
|
7844
7903
|
lastFailure = { finish: reason };
|
|
7845
7904
|
logger.warn(`摘要未完成(第 ${attempt}/${maxAttempts} 次,${reason})` + (attempt < maxAttempts ? ",将重试" : ",重试耗尽,忽略本次摘要"));
|
|
7846
7905
|
continue;
|
|
7847
7906
|
}
|
|
7848
|
-
/** 摘要请求的 token usage(归入主会话记录;无则省略)。 */
|
|
7849
7907
|
const usage = collector.usage;
|
|
7850
7908
|
logger.step(`摘要调用成功(第 ${attempt}/${maxAttempts} 次,输出 ${text.length} 字符` + (usage === void 0 ? "" : `,input ${String(usage.inputTokens ?? "?")} / output ${String(usage.outputTokens ?? "?")} tokens`) + ")");
|
|
7851
7909
|
return {
|
|
@@ -7854,162 +7912,67 @@ async function runSummarySubagent(ctx, agent, instruction, contextText, maxToken
|
|
|
7854
7912
|
...usage === void 0 ? {} : { usage }
|
|
7855
7913
|
};
|
|
7856
7914
|
} catch (error) {
|
|
7857
|
-
/** 错误信息(统一为字符串)。 */
|
|
7858
7915
|
const message = error instanceof Error ? error.message : String(error);
|
|
7916
|
+
if (isRateLimitError(message)) {
|
|
7917
|
+
noteRateLimit();
|
|
7918
|
+
logger.warn(`摘要调用触发限流(429,第 ${attempt}/${maxAttempts} 次),下一次请求前至少等待 ${rateLimitWaitMs}ms`);
|
|
7919
|
+
}
|
|
7859
7920
|
lastFailure = { error: message };
|
|
7860
7921
|
logger.warn(`摘要调用失败(第 ${attempt}/${maxAttempts} 次,${message})` + (attempt < maxAttempts ? ",将重试" : ",重试耗尽,忽略本次摘要"));
|
|
7861
7922
|
}
|
|
7862
7923
|
}
|
|
7863
|
-
/** 全部尝试失败:记录最终失败日志(含最后原因,便于诊断)。 */
|
|
7864
7924
|
logger.warn(`摘要调用最终失败(已尝试 ${maxAttempts} 次` + (lastFailure.error !== void 0 ? `,最后错误:${lastFailure.error}` : "") + (lastFailure.finish !== void 0 ? `,最后结果:${lastFailure.finish}` : "") + "),忽略本次摘要");
|
|
7865
7925
|
return null;
|
|
7866
7926
|
}
|
|
7867
7927
|
//#endregion
|
|
7868
7928
|
//#region src/compress.ts
|
|
7869
7929
|
/**
|
|
7870
|
-
*
|
|
7871
|
-
*
|
|
7872
|
-
*
|
|
7873
|
-
* 压缩为观察日志,作为独立的新 <history> 块,只精确替换被压缩的新消息区间(旧块原地
|
|
7874
|
-
* 保留,多块并存按序排列;不再把旧+新合并进一条消息);
|
|
7875
|
-
* - 反思:全部 <history> 块 tokens 合计 ≥ reflectThresholdTokens(默认 30000)→
|
|
7876
|
-
* 同上摘要调用精简合并(输入为多个块拼接,共用同一套提示词),把整个块区段合并为一条。
|
|
7877
|
-
* 两级检查在 pre-step 阻塞串行执行(先反思后观察),避免压缩失败或重复压缩。
|
|
7878
|
-
* 自动压缩由配置键 omEnabled 开关(false 时关闭;recall 工具不受影响)。
|
|
7879
|
-
*
|
|
7880
|
-
* 压缩边界(historySection):消息列表中最后一个合法的 <history> 块(source 为插件)
|
|
7881
|
-
* 之后的消息视为未压缩;其前(含自身)视为已压缩,不重复压缩、不计入观察阈值。
|
|
7930
|
+
* 两级自动压缩(观察/反思)与 compaction 生命周期提交。
|
|
7931
|
+
* 导出 estimateTextTokens / isPairBalancedAfter / computeCompressRange / historySection /
|
|
7932
|
+
* reflectPass / observePass / maybeCompress。
|
|
7882
7933
|
*
|
|
7883
|
-
*
|
|
7884
|
-
*
|
|
7885
|
-
*
|
|
7886
|
-
*
|
|
7887
|
-
*
|
|
7888
|
-
*
|
|
7889
|
-
*
|
|
7890
|
-
*
|
|
7891
|
-
* 观察区间:pre-step 触发时日志 call-result 完备,区间不再受 turn/end 封顶——压缩边界 →
|
|
7892
|
-
* 表层长度-1-tailCount(尾部保留 config.tailMessageCount 条不压缩),当前 turn 中已
|
|
7893
|
-
* 完备的消息同样可压缩;区间终点回退到 tool-call/result 配对平衡点(不切段)。
|
|
7894
|
-
* 摘要输出的 <history> 块经连续性校验(无 reasoning、index/start/end 连续且覆盖预期
|
|
7895
|
-
* 区间),失败按 config.compressRetryCount 重试。仅主会话生效。
|
|
7934
|
+
* - 反思:全部 <history> 块 token 合计 ≥ reflectThresholdTokens 时,摘要合并为一条
|
|
7935
|
+
* - 观察:净压力(上下文压力 − 已压缩块 token 合计)≥ observeThresholdTokens 时,
|
|
7936
|
+
* 摘要未压缩消息为新 <history> 块并精确替换被压缩区间(旧块保留)
|
|
7937
|
+
* - 两级在 pre-step 阻塞串行执行(先反思后观察);仅主会话生效;omEnabled=false 关闭
|
|
7938
|
+
* - 压缩边界:最后一个合法 <history> 块之后的消息视为未压缩,其前不重复压缩
|
|
7939
|
+
* - 提交走宿主 compaction/* 生命周期事件(start 带 phase → summary → 替换消息 → end),
|
|
7940
|
+
* 失败补 end(error);替换消息 source 标记插件标识供 UI 认领
|
|
7896
7941
|
*/
|
|
7897
7942
|
/** 历史文本 token 估算:4 字符 ≈ 1 token(与宿主 dsh-token-meter 启发式一致)。 */
|
|
7898
7943
|
function estimateTextTokens(text) {
|
|
7899
7944
|
return Math.ceil(text.length / 4);
|
|
7900
7945
|
}
|
|
7901
7946
|
/**
|
|
7902
|
-
* 按 token 边界把完整消息序列分块(观察并行压缩用):完整消息不跨块——
|
|
7903
|
-
* 一条完整消息(含其 thinking/text/toolcall&result 全部 seqs)必然整体落在同一块,
|
|
7904
|
-
* 单条超界的消息独立成块。空输入返回空数组。
|
|
7905
|
-
*/
|
|
7906
|
-
function chunkCompleteMessages(session, cms, tokenBoundary, estimateMessage) {
|
|
7907
|
-
if (cms.length === 0) return [];
|
|
7908
|
-
/** 分块结果。 */
|
|
7909
|
-
const chunks = [];
|
|
7910
|
-
/** 当前块。 */
|
|
7911
|
-
let current = [];
|
|
7912
|
-
/** 当前块累计 token(按各完整消息 seqs 的派生消息估算合计)。 */
|
|
7913
|
-
let currentTokens = 0;
|
|
7914
|
-
for (const cm of cms) {
|
|
7915
|
-
/** 当前完整消息的 token 估算(其全部表层 seqs 的派生消息估算之和)。 */
|
|
7916
|
-
let cmTokens = 0;
|
|
7917
|
-
for (const seq of cm.seqs) {
|
|
7918
|
-
/** 表层事件(seq 缺失跳过)。 */
|
|
7919
|
-
const event = session.events[seq];
|
|
7920
|
-
/** 派生消息(用于 token 估算)。 */
|
|
7921
|
-
const message = event ? session.deriveEventMessage(event) : null;
|
|
7922
|
-
if (message) cmTokens += estimateMessage(message);
|
|
7923
|
-
}
|
|
7924
|
-
if (current.length > 0 && currentTokens + cmTokens > tokenBoundary) {
|
|
7925
|
-
chunks.push(current);
|
|
7926
|
-
current = [];
|
|
7927
|
-
currentTokens = 0;
|
|
7928
|
-
}
|
|
7929
|
-
current.push(cm);
|
|
7930
|
-
currentTokens += cmTokens;
|
|
7931
|
-
}
|
|
7932
|
-
if (current.length > 0) chunks.push(current);
|
|
7933
|
-
return chunks;
|
|
7934
|
-
}
|
|
7935
|
-
/**
|
|
7936
|
-
* 合并分块摘要为单个 <history> 块:剥离各块的 <history> 开/闭标签(保留块内全部内容),
|
|
7937
|
-
* 以统一的开标签(带 tip 属性)与闭标签包裹拼接。块内内容原样保留(含格式说明注释)。
|
|
7938
|
-
*/
|
|
7939
|
-
function mergeChunkReports(parts) {
|
|
7940
|
-
/** 各块内层内容(剥离外壳失败时原样保留该块文本)。 */
|
|
7941
|
-
const inners = [];
|
|
7942
|
-
for (const part of parts) {
|
|
7943
|
-
/** 首个 <history 开标签位置。 */
|
|
7944
|
-
const open = part.indexOf(`<${HISTORY_TAG}`);
|
|
7945
|
-
/** 开标签右括号位置。 */
|
|
7946
|
-
const gt = open === -1 ? -1 : part.indexOf(">", open);
|
|
7947
|
-
/** 最后一个 </history> 闭标签位置。 */
|
|
7948
|
-
const close = part.lastIndexOf(`</${HISTORY_TAG}>`);
|
|
7949
|
-
if (open === -1 || gt === -1 || close === -1 || close <= gt) {
|
|
7950
|
-
inners.push(part);
|
|
7951
|
-
continue;
|
|
7952
|
-
}
|
|
7953
|
-
inners.push(part.slice(gt + 1, close));
|
|
7954
|
-
}
|
|
7955
|
-
return `<${HISTORY_TAG} tip="${HISTORY_TIP}">\n${inners.join("\n").trim()}\n</${HISTORY_TAG}>`;
|
|
7956
|
-
}
|
|
7957
|
-
/** 合并多块摘要的 token usage(同名数字字段求和;全部为空返回 undefined)。 */
|
|
7958
|
-
function mergeUsage(usages) {
|
|
7959
|
-
if (usages.length === 0) return void 0;
|
|
7960
|
-
/** 合并结果(必填字段从 0 起累加)。 */
|
|
7961
|
-
const out = {
|
|
7962
|
-
inputTokens: 0,
|
|
7963
|
-
outputTokens: 0
|
|
7964
|
-
};
|
|
7965
|
-
for (const u of usages) {
|
|
7966
|
-
out.inputTokens += u.inputTokens;
|
|
7967
|
-
out.outputTokens += u.outputTokens;
|
|
7968
|
-
if (u.cacheReadTokens !== void 0) out.cacheReadTokens = (out.cacheReadTokens ?? 0) + u.cacheReadTokens;
|
|
7969
|
-
if (u.cacheWriteTokens !== void 0) out.cacheWriteTokens = (out.cacheWriteTokens ?? 0) + u.cacheWriteTokens;
|
|
7970
|
-
if (u.reasoningTokens !== void 0) out.reasoningTokens = (out.reasoningTokens ?? 0) + u.reasoningTokens;
|
|
7971
|
-
}
|
|
7972
|
-
return out;
|
|
7973
|
-
}
|
|
7974
|
-
/**
|
|
7975
7947
|
* 反思输入块引用的最大完整消息 index(解析全部条目取最大 end;无条目返回 -1)。
|
|
7976
7948
|
* 反思输出必须覆盖输入引用的完整 index 区间(0..max),连续性校验据此约束。
|
|
7977
7949
|
*/
|
|
7978
7950
|
function reflectExpectedEnd(contextText) {
|
|
7979
|
-
/** 最大 end(无条目保持 -1)。 */
|
|
7980
7951
|
let max = -1;
|
|
7981
7952
|
for (const e of parseHistoryEntries(contextText)) {
|
|
7982
|
-
/** 当前条目上界(模块 end 或单条 index)。 */
|
|
7983
7953
|
const hi = e.kind === "assistant" && e.end !== void 0 ? e.end : e.index ?? 0;
|
|
7984
7954
|
if (hi > max) max = hi;
|
|
7985
7955
|
}
|
|
7986
7956
|
return max;
|
|
7987
7957
|
}
|
|
7988
7958
|
/**
|
|
7989
|
-
*
|
|
7990
|
-
*
|
|
7991
|
-
*
|
|
7992
|
-
* 仅当消息文本含 <history> 开标签(含带 tip 属性版本)时视为压缩日志,返回整段文本;
|
|
7993
|
-
* 其余消息返回 undefined(旧格式 <om-history> 块按「不兼容,干净切换」视为普通消息)。
|
|
7959
|
+
* 判定消息是否为本插件的压缩日志消息并提取日志文本(按 source 标记判断,兼容旧宿主
|
|
7960
|
+
* checkpoint 标记 'compact')。仅当消息文本含 <history> 开标签时视为压缩日志,返回整段
|
|
7961
|
+
* 文本;其余返回 undefined。
|
|
7994
7962
|
*/
|
|
7995
7963
|
function historyTextOf(event) {
|
|
7996
7964
|
if (event?.type !== "user/message") return void 0;
|
|
7997
|
-
/** 消息 source(本插件自产消息的标记;含兼容旧宿主 checkpoint 标记)。 */
|
|
7998
7965
|
const source = event.data.source;
|
|
7999
7966
|
if (!isPluginOwnedSource(source)) return void 0;
|
|
8000
|
-
/** 消息纯文本。 */
|
|
8001
7967
|
const text = blocksToText(event.data.content);
|
|
8002
|
-
/** 首个 <history 出现位置(无则非压缩日志)。 */
|
|
8003
7968
|
const idx = text.indexOf(`<${HISTORY_TAG}`);
|
|
8004
7969
|
if (idx === -1) return void 0;
|
|
8005
|
-
/** 开标签校验:<history> 或 <history tip=…>(排除 </history> 等)。 */
|
|
8006
7970
|
const rest = text.slice(idx + 7 + 1);
|
|
8007
7971
|
if (!rest.startsWith(">") && !rest.startsWith(" tip=")) return void 0;
|
|
8008
7972
|
return text;
|
|
8009
7973
|
}
|
|
8010
7974
|
/** 提取 <history> 块内文(去掉开/闭标签与首尾空白;非块文本原样返回)。 */
|
|
8011
7975
|
function historyInnerText(text) {
|
|
8012
|
-
/** 闭标签。 */
|
|
8013
7976
|
const closeTag = `</${HISTORY_TAG}>`;
|
|
8014
7977
|
const close = text.lastIndexOf(closeTag);
|
|
8015
7978
|
if (close === -1) return text;
|
|
@@ -8020,44 +7983,12 @@ function historyInnerText(text) {
|
|
|
8020
7983
|
return text.slice(gt + 1, close).trim();
|
|
8021
7984
|
}
|
|
8022
7985
|
/**
|
|
8023
|
-
*
|
|
8024
|
-
*
|
|
8025
|
-
* 系统消息(user/message 中非 kind:user 的部分,如宿主注入的上下文)不计入阈值。
|
|
8026
|
-
*/
|
|
8027
|
-
function measureUncompressedTokens(session, meter) {
|
|
8028
|
-
/** 压缩边界(最后一个 history 块的 seq;无则 undefined)。 */
|
|
8029
|
-
const { boundarySeq } = historySection(session);
|
|
8030
|
-
/** token 合计。 */
|
|
8031
|
-
let total = 0;
|
|
8032
|
-
/** 是否已越过压缩边界(开始计入)。 */
|
|
8033
|
-
let measuring = boundarySeq === void 0;
|
|
8034
|
-
for (const seq of session.surface.nodes) {
|
|
8035
|
-
if (!measuring) {
|
|
8036
|
-
if (seq === boundarySeq) measuring = true;
|
|
8037
|
-
continue;
|
|
8038
|
-
}
|
|
8039
|
-
/** 事件对应的消息(用于 token 估算)。 */
|
|
8040
|
-
const event = session.events[seq];
|
|
8041
|
-
const message = event ? session.deriveEventMessage(event) : null;
|
|
8042
|
-
if (!message) continue;
|
|
8043
|
-
if (event?.type === "user/message") {
|
|
8044
|
-
if (event.data.source?.kind !== "user") continue;
|
|
8045
|
-
}
|
|
8046
|
-
total += meter.estimateMessage(message);
|
|
8047
|
-
}
|
|
8048
|
-
return total;
|
|
8049
|
-
}
|
|
8050
|
-
/**
|
|
8051
|
-
* 判定表层节点 seq 之后的切点是否 tool-call/result 配对平衡(与宿主
|
|
8052
|
-
* dsh-compaction 的 toolPairingBalancedAfter 同语义):按表层顺序折叠未闭合的
|
|
8053
|
-
* 工具调用数,处理到 seq 后计数为 0 即平衡。pre-step 时日志 call-result 完备,
|
|
8054
|
-
* 该检查作为区间边界的安全网(防止把助手 tool-call 与其结果切到两侧)。
|
|
7986
|
+
* 判定表层节点 seq 之后的切点是否 tool-call/result 配对平衡:按表层顺序折叠未闭合的
|
|
7987
|
+
* 工具调用数,处理到 seq 后计数为 0 即平衡(防止把 tool-call 与其结果切到两侧)。
|
|
8055
7988
|
*/
|
|
8056
7989
|
function isPairBalancedAfter(session, seq) {
|
|
8057
|
-
/** 未闭合工具调用计数。 */
|
|
8058
7990
|
let inProgress = 0;
|
|
8059
7991
|
for (const node of session.surface.nodes) {
|
|
8060
|
-
/** 当前表层事件。 */
|
|
8061
7992
|
const event = session.events[node];
|
|
8062
7993
|
if (event?.type === "assistant/message") inProgress += event.data.message.content.filter((block) => block.type === "tool-call").length;
|
|
8063
7994
|
else if (event?.type === "tool/result") inProgress -= 1;
|
|
@@ -8066,35 +7997,25 @@ function isPairBalancedAfter(session, seq) {
|
|
|
8066
7997
|
return false;
|
|
8067
7998
|
}
|
|
8068
7999
|
/**
|
|
8069
|
-
*
|
|
8070
|
-
*
|
|
8071
|
-
*
|
|
8000
|
+
* 观察压缩区间:压缩边界后的首个表层节点 → 表层长度-1-tailCount(尾部保留 tailCount
|
|
8001
|
+
* 条不压缩);区间终点回退到 tool-call/result 配对平衡点(不切段)。无可行区间返回
|
|
8002
|
+
* undefined。返回区间起止表层 seq 与被遮蔽 seq 列表。
|
|
8072
8003
|
*/
|
|
8073
8004
|
function computeCompressRange(session, tailCount) {
|
|
8074
|
-
/** 当前表层节点(按日志顺序)。 */
|
|
8075
8005
|
const surface = [...session.surface.nodes];
|
|
8076
8006
|
if (surface.length === 0) return void 0;
|
|
8077
|
-
/** 压缩边界(最后一个 <history> 块;其前含自身已压缩,起点在其后)。 */
|
|
8078
8007
|
const { boundarySeq } = historySection(session);
|
|
8079
|
-
/** 区间起点表层下标:无压缩日志时为 0;否则为压缩边界(最后一个 history 块)在表层
|
|
8080
|
-
* (消息日志顺序)中的后继第一条消息。注意不能按 seq 比较取「边界之后」——替换块是
|
|
8081
|
-
* 追加到日志末尾的,其 seq 大于被遮蔽的消息;表层顺序才是消息的逻辑日志顺序。 */
|
|
8082
8008
|
const startIdx = boundarySeq === void 0 ? 0 : surface.indexOf(boundarySeq) + 1;
|
|
8083
8009
|
if (startIdx >= surface.length) return void 0;
|
|
8084
|
-
/** 区间末表层节点下标(尾部保留 tailCount 条不压缩)。 */
|
|
8085
8010
|
let endIdx = surface.length - 1 - tailCount;
|
|
8086
8011
|
if (endIdx < startIdx) return void 0;
|
|
8087
|
-
/** 区间终点回退到配对平衡点(不切断 tool-call/result 配对)。 */
|
|
8088
8012
|
while (endIdx >= startIdx) {
|
|
8089
|
-
/** 当前候选终点节点(表层节点序列稠密,防御性判空)。 */
|
|
8090
8013
|
const node = surface[endIdx];
|
|
8091
8014
|
if (node === void 0 || isPairBalancedAfter(session, node)) break;
|
|
8092
8015
|
endIdx -= 1;
|
|
8093
8016
|
}
|
|
8094
8017
|
if (endIdx < startIdx) return void 0;
|
|
8095
|
-
/** 区间起点(压缩边界后的首个节点)。 */
|
|
8096
8018
|
const start = surface[startIdx];
|
|
8097
|
-
/** 区间终点(表层节点 seq)。 */
|
|
8098
8019
|
const end = surface[endIdx];
|
|
8099
8020
|
if (start === void 0 || end === void 0) return void 0;
|
|
8100
8021
|
return {
|
|
@@ -8105,17 +8026,14 @@ function computeCompressRange(session, tailCount) {
|
|
|
8105
8026
|
}
|
|
8106
8027
|
/**
|
|
8107
8028
|
* 压缩边界(historySection):扫描表层全部节点,收集所有合法 <history> 压缩日志块
|
|
8108
|
-
* (内文 + seq,按表层顺序),并以最后一个块的 seq
|
|
8109
|
-
*
|
|
8110
|
-
*
|
|
8029
|
+
* (内文 + seq,按表层顺序),并以最后一个块的 seq 作为压缩边界——边界之后的消息视为
|
|
8030
|
+
* 未压缩,其前(含边界自身)视为已压缩。无压缩日志时 blocks 为空、boundarySeq 为
|
|
8031
|
+
* undefined。
|
|
8111
8032
|
*/
|
|
8112
8033
|
function historySection(session) {
|
|
8113
|
-
/** 收集结果(按表层顺序)。 */
|
|
8114
8034
|
const blocks = [];
|
|
8115
|
-
/** 最后一个压缩日志块的 seq(无则 undefined)。 */
|
|
8116
8035
|
let boundarySeq;
|
|
8117
8036
|
for (const seq of session.surface.nodes) {
|
|
8118
|
-
/** <history> 内文(非压缩日志消息时为 undefined)。 */
|
|
8119
8037
|
const text = historyTextOf(session.events[seq]);
|
|
8120
8038
|
if (text === void 0) continue;
|
|
8121
8039
|
blocks.push({
|
|
@@ -8129,9 +8047,8 @@ function historySection(session) {
|
|
|
8129
8047
|
boundarySeq
|
|
8130
8048
|
};
|
|
8131
8049
|
}
|
|
8132
|
-
/** 当前打开中的 turn 号(最近 turn/start 且未被 turn/end 关闭);无则 null
|
|
8050
|
+
/** 当前打开中的 turn 号(最近 turn/start 且未被 turn/end 关闭);无则 null。 */
|
|
8133
8051
|
function openTurnOf(session) {
|
|
8134
|
-
/** 折叠结果(turn/start 打开、turn/end 关闭)。 */
|
|
8135
8052
|
let turn = null;
|
|
8136
8053
|
for (const event of session.events) if (event.type === "turn/start") turn = event.data.turn;
|
|
8137
8054
|
else if (event.type === "turn/end") turn = null;
|
|
@@ -8141,9 +8058,8 @@ function openTurnOf(session) {
|
|
|
8141
8058
|
function newCompactionId() {
|
|
8142
8059
|
return uuid();
|
|
8143
8060
|
}
|
|
8144
|
-
/** 追加 compaction/start(log-only
|
|
8061
|
+
/** 追加 compaction/start(log-only;载荷带 phase 区分观察/反思),返回事件 seq。 */
|
|
8145
8062
|
function appendCompactionStart(session, lifecycle, phase) {
|
|
8146
|
-
/** start 事件载荷(宿主类型 + 插件扩展 phase;宿主 append 不做 schema 剥离,扩展字段原样持久化)。 */
|
|
8147
8063
|
const payload = {
|
|
8148
8064
|
compactionId: lifecycle.compactionId,
|
|
8149
8065
|
turn: lifecycle.turn,
|
|
@@ -8156,7 +8072,6 @@ function appendCompactionStart(session, lifecycle, phase) {
|
|
|
8156
8072
|
* summary 为完整合并后的 <history> 内文;usage 由摘要调用提取(无则省略)。
|
|
8157
8073
|
*/
|
|
8158
8074
|
function appendCompactionSummary(session, data) {
|
|
8159
|
-
/** summary 事件载荷(宿主类型 + 插件扩展 shadowedCharCount/attemptCount;宿主 append 不做 schema 剥离,扩展字段原样持久化)。 */
|
|
8160
8075
|
const payload = {
|
|
8161
8076
|
compactionId: data.lifecycle.compactionId,
|
|
8162
8077
|
summary: [{
|
|
@@ -8183,10 +8098,8 @@ function appendCompactionEnd(session, lifecycle, error) {
|
|
|
8183
8098
|
...error === void 0 ? {} : { error }
|
|
8184
8099
|
}).seq;
|
|
8185
8100
|
}
|
|
8186
|
-
/** 追加 <history> 压缩日志消息(surfaceOp 替换遮蔽区间,source 标记插件自产 + compactionId
|
|
8101
|
+
/** 追加 <history> 压缩日志消息(surfaceOp 替换遮蔽区间,source 标记插件自产 + compactionId)。 */
|
|
8187
8102
|
function appendHistoryMessage(session, content, sourceEventSeqs, surfaceOp, compactionId) {
|
|
8188
|
-
/** 压缩替换消息(内容即 <history> 标签块,不再附加前缀句;对 AI 的提醒在块开标签 tip 属性上;
|
|
8189
|
-
* source 标记插件标识(plugin: 'dsh-plugin-om')+ compactionId 供 UI 关联)。 */
|
|
8190
8103
|
const message = {
|
|
8191
8104
|
id: uuid(),
|
|
8192
8105
|
role: "user",
|
|
@@ -8206,49 +8119,36 @@ function appendHistoryMessage(session, content, sourceEventSeqs, surfaceOp, comp
|
|
|
8206
8119
|
});
|
|
8207
8120
|
}
|
|
8208
8121
|
/**
|
|
8209
|
-
* 反思:全部 <history> 块
|
|
8210
|
-
*
|
|
8211
|
-
* 更紧凑的摘要。失败不产生部分替换。
|
|
8122
|
+
* 反思:全部 <history> 块 token 合计 ≥ reflectThresholdTokens 时,摘要调用把整个块
|
|
8123
|
+
* 区段合并替换为一条更紧凑的摘要。失败不产生部分替换。
|
|
8212
8124
|
*/
|
|
8213
8125
|
async function reflectPass(ctx, agent, config, target, signal) {
|
|
8214
|
-
/** 当前会话。 */
|
|
8215
8126
|
const session = agent.session;
|
|
8216
|
-
/** 插件日志门面。 */
|
|
8217
8127
|
const logger = makeLogger(ctx, config.debug);
|
|
8218
8128
|
logger.step(`反思检查(反思阈值 ${config.reflectThresholdTokens} tokens)`);
|
|
8219
|
-
/** 全部 <history> 压缩日志块(按表层顺序;无则跳过)。 */
|
|
8220
8129
|
const { blocks } = historySection(session);
|
|
8221
8130
|
if (blocks.length === 0) {
|
|
8222
8131
|
logger.step("反思:无 <history> 压缩日志,跳过");
|
|
8223
8132
|
return;
|
|
8224
8133
|
}
|
|
8225
|
-
/** 反思阈值(配置的绝对 token 数)。 */
|
|
8226
8134
|
const threshold = config.reflectThresholdTokens;
|
|
8227
|
-
/** 全部块 token 估算合计(摘要总长)。 */
|
|
8228
8135
|
const tokens = blocks.reduce((total, block) => total + estimateTextTokens(block.text), 0);
|
|
8229
|
-
/** 被压缩块区段的字符数合计(压缩前内文长度;UI 标题统计用,与观察路径一致只计内容不计标签)。 */
|
|
8230
8136
|
const shadowedCharCount = blocks.reduce((total, block) => total + historyInnerText(block.text).length, 0);
|
|
8231
8137
|
if (tokens < threshold) {
|
|
8232
8138
|
logger.step(`反思:摘要 ${tokens} tokens < 阈值 ${threshold},跳过`);
|
|
8233
8139
|
return;
|
|
8234
8140
|
}
|
|
8235
8141
|
logger.step(`反思:摘要 ${tokens} tokens ≥ 阈值 ${threshold},触发精简合并(${blocks.length} 个块)`);
|
|
8236
|
-
/** 块区段首尾(historySection 只收集表层节点,天然在表层)。 */
|
|
8237
8142
|
const first = blocks[0];
|
|
8238
8143
|
const last = blocks[blocks.length - 1];
|
|
8239
8144
|
if (first === void 0 || last === void 0) {
|
|
8240
8145
|
logger.step("反思:块区段缺失,跳过");
|
|
8241
8146
|
return;
|
|
8242
8147
|
}
|
|
8243
|
-
/** 被替换块区段的 seq 列表。 */
|
|
8244
8148
|
const blockSeqs = blocks.map((block) => block.seq);
|
|
8245
|
-
/** 共享提示词(观察/反思同一套)。 */
|
|
8246
8149
|
const instruction = buildHistoryPrompt();
|
|
8247
|
-
/** 渲染输入(全部 <history> 块拼接)。 */
|
|
8248
8150
|
const contextText = blocks.map((block) => block.text).join("\n");
|
|
8249
|
-
/** 反思输出应覆盖输入块引用的全部 index(解析输入条目取最大 end;无条目则只校验起点)。 */
|
|
8250
8151
|
const expectedEnd = reflectExpectedEnd(contextText);
|
|
8251
|
-
/** 本次压缩生命周期(compactionId + 当前轮次;start 在摘要调用前开启,UI 压缩中提示)。 */
|
|
8252
8152
|
const lifecycle = {
|
|
8253
8153
|
compactionId: newCompactionId(),
|
|
8254
8154
|
turn: openTurnOf(session)
|
|
@@ -8257,18 +8157,17 @@ async function reflectPass(ctx, agent, config, target, signal) {
|
|
|
8257
8157
|
logger.step("反思:追加 compaction/start(摘要调用前开启压缩中提示)");
|
|
8258
8158
|
appendCompactionStart(session, lifecycle, "reflect");
|
|
8259
8159
|
} catch (error) {
|
|
8260
|
-
/** 启动失败信息(统一字符串)。 */
|
|
8261
8160
|
const message = error instanceof Error ? error.message : String(error);
|
|
8262
8161
|
logger.warn(`反思压缩启动失败: ${message}`);
|
|
8263
8162
|
return;
|
|
8264
8163
|
}
|
|
8265
|
-
/** 反思摘要结果(null 表示失败/跳过)。 */
|
|
8266
8164
|
const summaryResult = await runSummarySubagent(ctx, agent, instruction, contextText, config.compressMaxTokens, target, config.debug, signal, {
|
|
8267
8165
|
maxAttempts: config.compressRetryCount + 1,
|
|
8268
8166
|
expected: expectedEnd < 0 ? { start: 0 } : {
|
|
8269
8167
|
start: 0,
|
|
8270
8168
|
end: expectedEnd
|
|
8271
|
-
}
|
|
8169
|
+
},
|
|
8170
|
+
rateLimitWaitMs: config.rateLimitWaitMs
|
|
8272
8171
|
});
|
|
8273
8172
|
if (summaryResult === null || summaryResult.text.trim().length === 0) {
|
|
8274
8173
|
logger.step("反思:摘要调用失败/无输出,追加 compaction/end(error)");
|
|
@@ -8277,11 +8176,9 @@ async function reflectPass(ctx, agent, config, target, signal) {
|
|
|
8277
8176
|
} catch {}
|
|
8278
8177
|
return;
|
|
8279
8178
|
}
|
|
8280
|
-
/** 反思摘要文本。 */
|
|
8281
8179
|
const report = summaryResult.text;
|
|
8282
8180
|
try {
|
|
8283
8181
|
logger.step("反思提交:追加 compaction/summary(影子价格认领)");
|
|
8284
|
-
/** compaction/summary 事件 seq(承担影子价格认领,紧随其后的替换消息消费)。 */
|
|
8285
8182
|
const summarySeq = appendCompactionSummary(session, {
|
|
8286
8183
|
lifecycle,
|
|
8287
8184
|
summary: report,
|
|
@@ -8308,7 +8205,6 @@ async function reflectPass(ctx, agent, config, target, signal) {
|
|
|
8308
8205
|
appendCompactionEnd(session, lifecycle);
|
|
8309
8206
|
logger.info(`反思完成(摘要 ${tokens} tokens ≥ 阈值 ${threshold},合并 ${blocks.length} 个块为一条)`);
|
|
8310
8207
|
} catch (error) {
|
|
8311
|
-
/** 提交失败信息(统一字符串)。 */
|
|
8312
8208
|
const message = error instanceof Error ? error.message : String(error);
|
|
8313
8209
|
logger.warn(`反思提交失败: ${message}`);
|
|
8314
8210
|
try {
|
|
@@ -8317,57 +8213,47 @@ async function reflectPass(ctx, agent, config, target, signal) {
|
|
|
8317
8213
|
}
|
|
8318
8214
|
}
|
|
8319
8215
|
/**
|
|
8320
|
-
*
|
|
8321
|
-
*
|
|
8216
|
+
* 观察:净压力 tokens(上下文压力 − 已压缩 <history> 块 token 合计)≥
|
|
8217
|
+
* observeThresholdTokens 时,摘要调用把未压缩消息压缩为观察日志,追加到旧摘要并
|
|
8218
|
+
* 替换被压缩消息区间。失败不产生部分替换。
|
|
8322
8219
|
*/
|
|
8323
8220
|
async function observePass(ctx, agent, config, tailCount, target, signal) {
|
|
8324
|
-
/** 当前会话。 */
|
|
8325
8221
|
const session = agent.session;
|
|
8326
|
-
/** 插件日志门面。 */
|
|
8327
8222
|
const logger = makeLogger(ctx, config.debug);
|
|
8328
8223
|
logger.step(`观察检查(观察阈值 ${config.observeThresholdTokens} tokens,尾部保留 ${tailCount} 条)`);
|
|
8329
|
-
/** 观察阈值(配置的绝对 token 数)。 */
|
|
8330
8224
|
const threshold = config.observeThresholdTokens;
|
|
8331
|
-
|
|
8332
|
-
const
|
|
8333
|
-
|
|
8334
|
-
|
|
8225
|
+
const pressureTokens = ctx.tokenMeter.measure(session).totalTokens;
|
|
8226
|
+
const { blocks } = historySection(session);
|
|
8227
|
+
const historyTokens = blocks.reduce((total, block) => total + estimateTextTokens(block.text), 0);
|
|
8228
|
+
const netTokens = pressureTokens - historyTokens;
|
|
8229
|
+
if (netTokens < threshold) {
|
|
8230
|
+
logger.step(`观察:净压力 ${netTokens} tokens(上下文压力 ${pressureTokens} − 已压缩块 ${historyTokens})< 阈值 ${threshold},跳过`);
|
|
8335
8231
|
return;
|
|
8336
8232
|
}
|
|
8337
|
-
logger.step(
|
|
8338
|
-
/** 观察压缩区间(压缩边界之后;尾部保留 tailCount 条不压缩;无可行区间则跳过)。 */
|
|
8233
|
+
logger.step(`观察:净压力 ${netTokens} tokens(上下文压力 ${pressureTokens} − 已压缩块 ${historyTokens})≥ 阈值 ${threshold},触发压缩`);
|
|
8339
8234
|
const range = computeCompressRange(session, tailCount);
|
|
8340
8235
|
if (!range) {
|
|
8341
8236
|
logger.step("观察:无可行压缩区间(边界后消息过短或配对无法平衡),跳过");
|
|
8342
8237
|
return;
|
|
8343
8238
|
}
|
|
8344
8239
|
logger.step(`观察:压缩区间 [${range.start}..${range.end}],遮蔽 ${range.shadowedSeqs.length} 个表层节点`);
|
|
8345
|
-
/** 旧压缩日志块(保留不替换、不进新消息遮蔽)。 */
|
|
8346
|
-
const { blocks } = historySection(session);
|
|
8347
|
-
/** 实际被替换的新消息表层节点(压缩边界之后;旧块不进入遮蔽、不被重写)。 */
|
|
8348
8240
|
const replaceSeqs = range.shadowedSeqs;
|
|
8349
|
-
/** 替换区间起点(首个新消息节点;区间内全部为块时无新消息可压缩)。 */
|
|
8350
8241
|
const replaceStart = replaceSeqs[0];
|
|
8351
8242
|
if (replaceStart === void 0) {
|
|
8352
8243
|
logger.step("观察:区间内无新消息(全部为压缩日志块),跳过");
|
|
8353
8244
|
return;
|
|
8354
8245
|
}
|
|
8355
|
-
/** 被替换 seq 集合(计算新消息 index 覆盖区间)。 */
|
|
8356
8246
|
const shadowedSet = new Set(replaceSeqs);
|
|
8357
|
-
/** 压缩区间内的完整消息(连续性校验的预期覆盖区间)。 */
|
|
8358
8247
|
const inRangeCms = indexCompleteMessages(session).filter((cm) => cm.seqs.every((seq) => shadowedSet.has(seq)));
|
|
8359
8248
|
if (inRangeCms.length === 0) {
|
|
8360
8249
|
logger.step("观察:区间内无完整消息,跳过");
|
|
8361
8250
|
return;
|
|
8362
8251
|
}
|
|
8363
|
-
/** 新消息起始/结束完整消息 index(模型输入携带绝对 index,校验按此区间约束)。 */
|
|
8364
8252
|
const startIndex = inRangeCms[0]?.index ?? 0;
|
|
8365
8253
|
const endIndex = inRangeCms[inRangeCms.length - 1]?.index ?? startIndex;
|
|
8366
8254
|
logger.step(`观察:保留旧块 ${blocks.length} 条,替换新消息 [${replaceSeqs[0]}..${range.end}](${replaceSeqs.length} 条),尾部保留 ${tailCount} 条(不压缩、不进日志),新消息 index ${startIndex}..${endIndex}`);
|
|
8367
|
-
|
|
8368
|
-
const
|
|
8369
|
-
logger.step(`观察:${inRangeCms.length} 条完整消息按 ${config.observeChunkTokens} tokens 边界分为 ${chunks.length} 块(每块摘要 maxTokens ${config.observeChunkMaxTokens},除最后一块外要求简单摘要)`);
|
|
8370
|
-
/** 本次压缩生命周期(compactionId + 当前轮次;start 在摘要调用前开启,UI 压缩中提示)。 */
|
|
8255
|
+
const instruction = buildHistoryPrompt();
|
|
8256
|
+
const contextText = renderMessages(session, replaceSeqs);
|
|
8371
8257
|
const lifecycle = {
|
|
8372
8258
|
compactionId: newCompactionId(),
|
|
8373
8259
|
turn: openTurnOf(session)
|
|
@@ -8376,62 +8262,40 @@ async function observePass(ctx, agent, config, tailCount, target, signal) {
|
|
|
8376
8262
|
logger.step("观察:追加 compaction/start(摘要调用前开启压缩中提示)");
|
|
8377
8263
|
appendCompactionStart(session, lifecycle, "observe");
|
|
8378
8264
|
} catch (error) {
|
|
8379
|
-
/** 启动失败信息(统一字符串)。 */
|
|
8380
8265
|
const message = error instanceof Error ? error.message : String(error);
|
|
8381
8266
|
logger.warn(`观察压缩启动失败: ${message}`);
|
|
8382
8267
|
return;
|
|
8383
8268
|
}
|
|
8384
|
-
|
|
8385
|
-
|
|
8386
|
-
|
|
8387
|
-
|
|
8388
|
-
|
|
8389
|
-
|
|
8390
|
-
|
|
8391
|
-
|
|
8392
|
-
|
|
8393
|
-
|
|
8394
|
-
maxAttempts: config.compressRetryCount + 1,
|
|
8395
|
-
expected: {
|
|
8396
|
-
start,
|
|
8397
|
-
end
|
|
8398
|
-
}
|
|
8399
|
-
});
|
|
8400
|
-
}));
|
|
8401
|
-
if (chunkResults.find((r) => r === null || r.text.trim().length === 0) !== void 0) {
|
|
8402
|
-
logger.step("观察:分块摘要存在失败/无输出,不产生部分替换,追加 compaction/end(error)");
|
|
8269
|
+
const summaryResult = await runSummarySubagent(ctx, agent, instruction, contextText, config.compressMaxTokens, target, config.debug, signal, {
|
|
8270
|
+
maxAttempts: config.compressRetryCount + 1,
|
|
8271
|
+
expected: {
|
|
8272
|
+
start: startIndex,
|
|
8273
|
+
end: endIndex
|
|
8274
|
+
},
|
|
8275
|
+
rateLimitWaitMs: config.rateLimitWaitMs
|
|
8276
|
+
});
|
|
8277
|
+
if (summaryResult === null || summaryResult.text.trim().length === 0) {
|
|
8278
|
+
logger.step("观察:摘要调用失败/无输出,追加 compaction/end(error)");
|
|
8403
8279
|
try {
|
|
8404
8280
|
appendCompactionEnd(session, lifecycle, "摘要调用失败/无输出");
|
|
8405
8281
|
} catch {}
|
|
8406
8282
|
return;
|
|
8407
8283
|
}
|
|
8408
|
-
|
|
8409
|
-
const
|
|
8410
|
-
|
|
8411
|
-
const report = mergeChunkReports(okResults.map((r) => r.text));
|
|
8412
|
-
/** 重试次数合计(各块重试次数之和;每块重试次数 = 该块成功尝试次数 - 1)。 */
|
|
8413
|
-
const attemptCount = okResults.reduce((sum, r) => sum + (r.attemptCount - 1), 0);
|
|
8414
|
-
/** token usage 合计(各块求和;全部缺省为 undefined)。 */
|
|
8415
|
-
const usage = mergeUsage(okResults.map((r) => r.usage).filter((u) => u !== void 0));
|
|
8416
|
-
/** 被替换表层节点的 token 估算合计(仅新消息区间)。 */
|
|
8284
|
+
const report = summaryResult.text;
|
|
8285
|
+
const attemptCount = summaryResult.attemptCount - 1;
|
|
8286
|
+
const usage = summaryResult.usage;
|
|
8417
8287
|
const shadowedTokenCount = replaceSeqs.reduce((total, seq) => {
|
|
8418
|
-
/** 当前表层事件。 */
|
|
8419
8288
|
const event = session.events[seq];
|
|
8420
|
-
/** 事件对应的消息(用于 token 估算)。 */
|
|
8421
8289
|
const message = event ? session.deriveEventMessage(event) : null;
|
|
8422
8290
|
return total + (message ? ctx.tokenMeter.estimateMessage(message) : 0);
|
|
8423
8291
|
}, 0);
|
|
8424
|
-
/** 被替换消息的字符数合计(压缩前文本长度;UI 标题统计用,递归计入 tool-result 内嵌文本)。 */
|
|
8425
8292
|
const shadowedCharCount = replaceSeqs.reduce((total, seq) => {
|
|
8426
|
-
/** 当前表层事件。 */
|
|
8427
8293
|
const event = session.events[seq];
|
|
8428
|
-
/** 事件对应的消息(取文本块字符数)。 */
|
|
8429
8294
|
const message = event ? session.deriveEventMessage(event) : null;
|
|
8430
8295
|
return total + (message ? textCharCount(message) : 0);
|
|
8431
8296
|
}, 0);
|
|
8432
8297
|
try {
|
|
8433
8298
|
logger.step("观察提交:追加 compaction/summary(影子价格认领)");
|
|
8434
|
-
/** compaction/summary 事件 seq(承担影子价格认领,紧随其后的替换消息消费)。 */
|
|
8435
8299
|
const summarySeq = appendCompactionSummary(session, {
|
|
8436
8300
|
lifecycle,
|
|
8437
8301
|
summary: report,
|
|
@@ -8444,7 +8308,7 @@ async function observePass(ctx, agent, config, tailCount, target, signal) {
|
|
|
8444
8308
|
shadowedCharCount,
|
|
8445
8309
|
provider: target.provider,
|
|
8446
8310
|
model: target.model,
|
|
8447
|
-
maxTokens: config.
|
|
8311
|
+
maxTokens: config.compressMaxTokens,
|
|
8448
8312
|
attemptCount,
|
|
8449
8313
|
...usage === void 0 ? {} : { usage }
|
|
8450
8314
|
});
|
|
@@ -8456,9 +8320,8 @@ async function observePass(ctx, agent, config, tailCount, target, signal) {
|
|
|
8456
8320
|
}, lifecycle.compactionId);
|
|
8457
8321
|
logger.step("观察提交:追加 compaction/end");
|
|
8458
8322
|
appendCompactionEnd(session, lifecycle);
|
|
8459
|
-
logger.info(
|
|
8323
|
+
logger.info(`观察压缩完成(净压力 ${netTokens} tokens(上下文压力 ${pressureTokens} − 已压缩块 ${historyTokens})≥ 阈值 ${threshold},替换 ${replaceSeqs.length} 个表层节点,约 ${shadowedTokenCount} tokens)`);
|
|
8460
8324
|
} catch (error) {
|
|
8461
|
-
/** 提交失败信息(统一字符串)。 */
|
|
8462
8325
|
const message = error instanceof Error ? error.message : String(error);
|
|
8463
8326
|
logger.warn(`观察压缩提交失败: ${message}`);
|
|
8464
8327
|
try {
|
|
@@ -8466,28 +8329,20 @@ async function observePass(ctx, agent, config, tailCount, target, signal) {
|
|
|
8466
8329
|
} catch {}
|
|
8467
8330
|
}
|
|
8468
8331
|
}
|
|
8469
|
-
/**
|
|
8470
|
-
* 压力检查 + 两级压缩:先反思(压缩过往摘要,有必要才做),后观察(压缩新消息,
|
|
8471
|
-
* 有必要才做)。在 pre-step 阻塞串行执行(避免压缩失败或重复压缩)。仅主会话生效。
|
|
8472
|
-
*/
|
|
8332
|
+
/** 压力检查 + 两级压缩入口:先反思后观察,pre-step 阻塞串行执行;仅主会话生效。 */
|
|
8473
8333
|
async function maybeCompress(ctx, agent, config, signal) {
|
|
8474
|
-
/** 当前会话。 */
|
|
8475
8334
|
const session = agent.session;
|
|
8476
|
-
/** 插件日志门面。 */
|
|
8477
8335
|
const logger = makeLogger(ctx, config.debug);
|
|
8478
|
-
/** omEnabled=false:关闭自动压缩(观察/反思均不触发,recall 工具不受影响)。 */
|
|
8479
8336
|
if (!config.omEnabled) {
|
|
8480
8337
|
logger.step("omEnabled=false,跳过压缩");
|
|
8481
8338
|
return;
|
|
8482
8339
|
}
|
|
8483
|
-
/** 会话路由目标(未路由无法查询容量)。 */
|
|
8484
8340
|
const target = routedTarget(session);
|
|
8485
8341
|
if (target === void 0) {
|
|
8486
8342
|
logger.step("会话未路由(无 provider/model),跳过压缩");
|
|
8487
8343
|
return;
|
|
8488
8344
|
}
|
|
8489
8345
|
logger.step(`会话路由:provider ${target.provider},model ${target.model}`);
|
|
8490
|
-
/** 尾部保留条数(config.tailMessageCount,缺省 10)。 */
|
|
8491
8346
|
const tailCount = config.tailMessageCount;
|
|
8492
8347
|
logger.step("反思 pass 开始");
|
|
8493
8348
|
await reflectPass(ctx, agent, config, target, signal);
|
|
@@ -8498,26 +8353,19 @@ async function maybeCompress(ctx, agent, config, signal) {
|
|
|
8498
8353
|
//#endregion
|
|
8499
8354
|
//#region src/model-download.ts
|
|
8500
8355
|
/**
|
|
8501
|
-
*
|
|
8502
|
-
*
|
|
8503
|
-
*
|
|
8504
|
-
*
|
|
8505
|
-
*
|
|
8506
|
-
*
|
|
8507
|
-
*
|
|
8508
|
-
* - 直连 huggingface.co 受限时设置环境变量 HF_ENDPOINT=https://hf-mirror.com 走镜像。
|
|
8509
|
-
* - 原子落盘:先写 .tmp 再改名,避免半截文件被当成完整模型加载。
|
|
8510
|
-
* - 日志:下载开始/结束(含跳过)经 log 回调输出(默认 console.log);失败时
|
|
8511
|
-
* 错误消息附带 HF_ENDPOINT=https://hf-mirror.com 镜像建议。
|
|
8512
|
-
*
|
|
8513
|
-
* 运行时编排见 embedding.ts 的 ensureModelReady(下载单飞、未就绪不阻塞);
|
|
8514
|
-
* 本模块只提供纯下载原语(供 src 与 scripts/download-model.mjs CLI 复用)。
|
|
8356
|
+
* 嵌入模型下载原语:recall-semantic 运行时按需下载与开发手动预下载(scripts/
|
|
8357
|
+
* download-model.mjs)共用。导出 EMBEDDING_MODEL_ID / ONNX_REL / MODEL_SMALL_FILES /
|
|
8358
|
+
* modelSourceUrl / modelTargetPath / needsDownload / downloadModel 及 DownloadResult /
|
|
8359
|
+
* ModelFetch 类型。
|
|
8360
|
+
* 模型为量化 ONNX(~113MB,不进 git,仅小文件随仓库/包分发);下载目标
|
|
8361
|
+
* <modelDir>/onnx/model_quantized.onnx;直连受限时设置 HF_ENDPOINT 走镜像;
|
|
8362
|
+
* 原子落盘(先写 .tmp 再改名)。运行时编排见 embedding.ts 的 ensureModelReady。
|
|
8515
8363
|
*/
|
|
8516
8364
|
/** 模型标识(models/ 下目录名 = 模型 id;transformers.js 加载时的模型名)。 */
|
|
8517
8365
|
const EMBEDDING_MODEL_ID = "paraphrase-multilingual-MiniLM-L12-v2";
|
|
8518
|
-
/**
|
|
8366
|
+
/** 量化 ONNX 文件的相对路径(相对模型目录)。 */
|
|
8519
8367
|
const ONNX_REL = "onnx/model_quantized.onnx";
|
|
8520
|
-
/** 随 npm 包分发的小模型文件(相对模型目录;onnx
|
|
8368
|
+
/** 随 npm 包分发的小模型文件(相对模型目录;onnx 二进制不随包分发)。 */
|
|
8521
8369
|
const MODEL_SMALL_FILES = [
|
|
8522
8370
|
"config.json",
|
|
8523
8371
|
"special_tokens_map.json",
|
|
@@ -8526,19 +8374,16 @@ const MODEL_SMALL_FILES = [
|
|
|
8526
8374
|
];
|
|
8527
8375
|
/**
|
|
8528
8376
|
* HuggingFace 原始文件 URL(resolve 会 302 到 CDN,fetch 自动跟随重定向)。
|
|
8529
|
-
* base
|
|
8377
|
+
* base 可用环境变量 HF_ENDPOINT 覆盖(直连受限时改用镜像,如 https://hf-mirror.com)。
|
|
8530
8378
|
*/
|
|
8531
8379
|
function modelSourceUrl(id = EMBEDDING_MODEL_ID, rel = ONNX_REL, base = process.env.HF_ENDPOINT || "https://huggingface.co") {
|
|
8532
8380
|
return `${base}/Xenova/${id}/resolve/main/${rel}`;
|
|
8533
8381
|
}
|
|
8534
|
-
/** 本地目标文件绝对路径(<modelDir>/<rel
|
|
8382
|
+
/** 本地目标文件绝对路径(<modelDir>/<rel>)。 */
|
|
8535
8383
|
function modelTargetPath(modelDir, rel = ONNX_REL) {
|
|
8536
8384
|
return path.join(modelDir, rel);
|
|
8537
8385
|
}
|
|
8538
|
-
/**
|
|
8539
|
-
* 是否需要下载(纯函数,供测试)。
|
|
8540
|
-
* force 强制下载;否则目标已存在即跳过。
|
|
8541
|
-
*/
|
|
8386
|
+
/** 是否需要下载:force 强制;否则目标已存在即跳过。 */
|
|
8542
8387
|
function needsDownload(target, { force = false } = {}) {
|
|
8543
8388
|
if (force) return true;
|
|
8544
8389
|
try {
|
|
@@ -8550,8 +8395,7 @@ function needsDownload(target, { force = false } = {}) {
|
|
|
8550
8395
|
}
|
|
8551
8396
|
/**
|
|
8552
8397
|
* 下载模型到 <modelDir>/onnx/model_quantized.onnx。
|
|
8553
|
-
* 开始/结束(含跳过)经 log
|
|
8554
|
-
* 并在错误消息中附带镜像建议(直连受限时设置 HF_ENDPOINT=https://hf-mirror.com)。
|
|
8398
|
+
* 开始/结束(含跳过)经 log 回调输出;失败时清理临时文件并在错误消息中附带镜像建议。
|
|
8555
8399
|
* 可注入 fetchImpl / log(供测试)。
|
|
8556
8400
|
*/
|
|
8557
8401
|
async function downloadModel(modelDir, { force = false, fetchImpl = fetch, log = console.log } = {}) {
|
|
@@ -8589,44 +8433,34 @@ async function downloadModel(modelDir, { force = false, fetchImpl = fetch, log =
|
|
|
8589
8433
|
//#endregion
|
|
8590
8434
|
//#region src/embedding.ts
|
|
8591
8435
|
/**
|
|
8592
|
-
* 本地语义嵌入(recall-semantic
|
|
8593
|
-
*
|
|
8594
|
-
*
|
|
8595
|
-
*
|
|
8596
|
-
*
|
|
8597
|
-
*
|
|
8598
|
-
*
|
|
8599
|
-
*
|
|
8600
|
-
*
|
|
8601
|
-
* 插件启动不阻塞;加载结果单例缓存(resetEmbedder 供测试重置)。
|
|
8602
|
-
* - 批处理:一次推理多条文本(mean pooling + L2 归一化),输出每条的向量。
|
|
8603
|
-
* - 相似度:cosineSimilarity 纯函数(归一化向量点积即余弦)。
|
|
8604
|
-
*
|
|
8605
|
-
* 依赖说明:@huggingface/transformers 为运行时依赖(v4,dtype q8 加载
|
|
8606
|
-
* model_quantized.onnx),node 侧使用 onnxruntime-node 原生绑定。
|
|
8436
|
+
* 本地语义嵌入(recall-semantic 的向量引擎):Xenova/paraphrase-multilingual-MiniLM-L12-v2
|
|
8437
|
+
* 量化 ONNX,懒加载 + 批量推理 + cosine 相似度,模型缺失时按需后台下载。
|
|
8438
|
+
* 导出 BUNDLED_MODEL_DIR / EMBEDDING_MODEL_ID / resolveDshHome / sharedModelDir /
|
|
8439
|
+
* ensureModelSmallFiles / getEmbedder / resetEmbedder / ensureModelReady /
|
|
8440
|
+
* resetModelDownloads / cosineSimilarity 及 EmbedFn / ModelStatus 类型。
|
|
8441
|
+
* 模型目录默认为跨版本共享目录($DSH_HOME/plugin-data/dsh-plugin-om/models/<id>):
|
|
8442
|
+
* 随包小文件缺失时从打包目录复制补齐(离线可用),onnx 二进制按需下载
|
|
8443
|
+
* (见 model-download.ts)。@huggingface/transformers 为运行时依赖(dtype q8 加载
|
|
8444
|
+
* model_quantized.onnx,node 侧使用 onnxruntime-node 原生绑定)。
|
|
8607
8445
|
*/
|
|
8608
8446
|
/** 当前模块所在路径(dist/ 或 src/,models 在其上一级)。 */
|
|
8609
8447
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
8610
8448
|
/** 打包模型目录:<包根>/models/<model-id>/。 */
|
|
8611
8449
|
const BUNDLED_MODEL_DIR = path.join(here, "..", "models", EMBEDDING_MODEL_ID);
|
|
8612
|
-
/** DSH 用户数据根目录:$DSH_HOME 优先(空白视为未设置),缺省 ~/.dsh
|
|
8450
|
+
/** DSH 用户数据根目录:$DSH_HOME 优先(空白视为未设置),缺省 ~/.dsh。 */
|
|
8613
8451
|
function resolveDshHome() {
|
|
8614
8452
|
const fromEnv = process.env.DSH_HOME;
|
|
8615
8453
|
if (fromEnv !== void 0 && fromEnv.trim().length > 0) return path.resolve(fromEnv);
|
|
8616
8454
|
return path.join(homedir(), ".dsh");
|
|
8617
8455
|
}
|
|
8618
|
-
/**
|
|
8619
|
-
* 跨插件版本共享的默认模型目录:$DSH_HOME/plugin-data/dsh-plugin-om/models/<模型id>。
|
|
8620
|
-
* 不随插件包(版本)变化,升级/重装插件后仍复用同一份已下载模型(onnx 不重复下载)。
|
|
8621
|
-
*/
|
|
8456
|
+
/** 跨插件版本共享的默认模型目录:$DSH_HOME/plugin-data/dsh-plugin-om/models/<模型id>。 */
|
|
8622
8457
|
function sharedModelDir() {
|
|
8623
8458
|
return path.join(resolveDshHome(), "plugin-data", "dsh-plugin-om", "models", EMBEDDING_MODEL_ID);
|
|
8624
8459
|
}
|
|
8625
8460
|
/**
|
|
8626
|
-
* 补齐模型目录的随包小文件(config/tokenizer 等):modelDir
|
|
8627
|
-
*
|
|
8628
|
-
*
|
|
8629
|
-
* 幂等、无网络。onnx 二进制不在此列(按需下载,见 model-download.ts)。
|
|
8461
|
+
* 补齐模型目录的随包小文件(config/tokenizer 等):modelDir 与打包目录不同时,
|
|
8462
|
+
* 把打包目录中缺失的小文件复制过去(已存在不覆盖;打包目录缺失则跳过)。
|
|
8463
|
+
* 幂等、无网络;onnx 二进制不在此列。
|
|
8630
8464
|
*/
|
|
8631
8465
|
function ensureModelSmallFiles(modelDir, bundledDir = BUNDLED_MODEL_DIR) {
|
|
8632
8466
|
if (path.resolve(modelDir) === path.resolve(bundledDir)) return;
|
|
@@ -8643,7 +8477,7 @@ const BATCH_SIZE = 32;
|
|
|
8643
8477
|
/** 懒加载中的 pipeline 单例(null = 尚未加载/已重置)。 */
|
|
8644
8478
|
let pipelinePromise = null;
|
|
8645
8479
|
/**
|
|
8646
|
-
* 获取(或加载)嵌入函数。首次调用动态 import transformers
|
|
8480
|
+
* 获取(或加载)嵌入函数。首次调用动态 import transformers 并加载本地模型,
|
|
8647
8481
|
* 之后复用同一 pipeline。失败时抛出可读错误(调用方自行降级)。
|
|
8648
8482
|
*/
|
|
8649
8483
|
function getEmbedder(modelDir = BUNDLED_MODEL_DIR) {
|
|
@@ -8663,7 +8497,6 @@ function getEmbedder(modelDir = BUNDLED_MODEL_DIR) {
|
|
|
8663
8497
|
pooling: "mean",
|
|
8664
8498
|
normalize: true
|
|
8665
8499
|
});
|
|
8666
|
-
/** 结果张量:dims = [batch, hidden],data 为扁平数组。 */
|
|
8667
8500
|
const dims = result.dims;
|
|
8668
8501
|
const hidden = dims.length > 1 ? dims[1] : 0;
|
|
8669
8502
|
const data = result.data;
|
|
@@ -8681,12 +8514,8 @@ function getEmbedder(modelDir = BUNDLED_MODEL_DIR) {
|
|
|
8681
8514
|
const inflightDownloads = /* @__PURE__ */ new Map();
|
|
8682
8515
|
/**
|
|
8683
8516
|
* 确保模型就绪(运行时按需下载编排)。
|
|
8684
|
-
*
|
|
8685
|
-
*
|
|
8686
|
-
* warn 记录日志并结束本次尝试,下次调用会重新触发下载(自动重试)。
|
|
8687
|
-
* - warn 可选:下载失败时的日志回调(默认静默;apply 注入 ctx.logger.warn)。
|
|
8688
|
-
* - log 可选:下载开始/结束日志回调(默认静默;apply 注入 console.log + ctx.logger.info 双通道)。
|
|
8689
|
-
* - fetchImpl 可注入(测试传替身,默认全局 fetch)。
|
|
8517
|
+
* 本地 onnx 已存在 → 'ready';缺失 → 启动后台下载(不阻塞、单飞,失败经 warn 记录,
|
|
8518
|
+
* 下次调用自动重试)并返回 'downloading'。warn/log/fetchImpl 可注入(测试传替身)。
|
|
8690
8519
|
*/
|
|
8691
8520
|
function ensureModelReady(modelDir = BUNDLED_MODEL_DIR, warn = () => {}, fetchImpl, log = () => {}) {
|
|
8692
8521
|
ensureModelSmallFiles(modelDir);
|
|
@@ -8704,18 +8533,12 @@ function ensureModelReady(modelDir = BUNDLED_MODEL_DIR, warn = () => {}, fetchIm
|
|
|
8704
8533
|
}
|
|
8705
8534
|
return Promise.resolve("downloading");
|
|
8706
8535
|
}
|
|
8707
|
-
/**
|
|
8708
|
-
* 两个向量的余弦相似度(向量已 L2 归一化时点积即余弦;这里兜底再归一化)。
|
|
8709
|
-
* 任一向量为零向量返回 0。
|
|
8710
|
-
*/
|
|
8536
|
+
/** 两个向量的余弦相似度(向量已 L2 归一化时点积即余弦;这里兜底再归一化)。零向量返回 0。 */
|
|
8711
8537
|
function cosineSimilarity(a, b) {
|
|
8712
8538
|
const len = Math.min(a.length, b.length);
|
|
8713
8539
|
if (len === 0) return 0;
|
|
8714
|
-
/** 点积累加。 */
|
|
8715
8540
|
let dot = 0;
|
|
8716
|
-
/** a 的模平方。 */
|
|
8717
8541
|
let normA = 0;
|
|
8718
|
-
/** b 的模平方。 */
|
|
8719
8542
|
let normB = 0;
|
|
8720
8543
|
for (let i = 0; i < len; i += 1) {
|
|
8721
8544
|
const x = a[i];
|
|
@@ -8730,35 +8553,34 @@ function cosineSimilarity(a, b) {
|
|
|
8730
8553
|
//#endregion
|
|
8731
8554
|
//#region src/config.ts
|
|
8732
8555
|
/**
|
|
8733
|
-
*
|
|
8734
|
-
*
|
|
8556
|
+
* 插件配置:默认值与合并。
|
|
8557
|
+
* 导出 PluginConfig / DEFAULT_CONFIG / resolveConfig;宽松校验——未知键忽略、
|
|
8558
|
+
* 非法值回退默认值,配置错误不影响插件加载。
|
|
8735
8559
|
*/
|
|
8736
8560
|
/** 默认配置(冻结对象,resolveConfig 合并的基底;debug 缺省值在解析时按 NODE_ENV 判定)。 */
|
|
8737
8561
|
const DEFAULT_CONFIG = Object.freeze({
|
|
8738
|
-
observeThresholdTokens:
|
|
8739
|
-
reflectThresholdTokens:
|
|
8740
|
-
compressMaxTokens:
|
|
8741
|
-
|
|
8742
|
-
|
|
8743
|
-
|
|
8744
|
-
compressRetryCount: 10,
|
|
8562
|
+
observeThresholdTokens: 35e3,
|
|
8563
|
+
reflectThresholdTokens: 4e4,
|
|
8564
|
+
compressMaxTokens: 5e3,
|
|
8565
|
+
rateLimitWaitMs: 6e4,
|
|
8566
|
+
tailMessageCount: 5,
|
|
8567
|
+
compressRetryCount: 5,
|
|
8745
8568
|
omEnabled: true,
|
|
8746
8569
|
debug: false,
|
|
8747
8570
|
recallEnabled: true,
|
|
8748
8571
|
semanticRecallEnabled: true,
|
|
8749
8572
|
modelDir: sharedModelDir()
|
|
8750
8573
|
});
|
|
8751
|
-
/** 数值键校验参数表:键名 + [
|
|
8574
|
+
/** 数值键校验参数表:键名 + [是否必须为整数]。 */
|
|
8752
8575
|
const NUMBER_KEYS = [
|
|
8753
8576
|
["observeThresholdTokens", true],
|
|
8754
8577
|
["reflectThresholdTokens", true],
|
|
8755
8578
|
["compressMaxTokens", true],
|
|
8756
|
-
["
|
|
8757
|
-
["observeChunkMaxTokens", true],
|
|
8579
|
+
["rateLimitWaitMs", true],
|
|
8758
8580
|
["tailMessageCount", true],
|
|
8759
8581
|
["compressRetryCount", true]
|
|
8760
8582
|
];
|
|
8761
|
-
/** 归一化原始配置输入:缺省 / null /
|
|
8583
|
+
/** 归一化原始配置输入:缺省 / null / 空串 / 非对象视为空对象。 */
|
|
8762
8584
|
function normalizeConfigInput(raw) {
|
|
8763
8585
|
if (raw === void 0 || raw === null) return {};
|
|
8764
8586
|
if (typeof raw === "string" && raw.trim() === "") return {};
|
|
@@ -8772,17 +8594,11 @@ function resolveBoolean(raw, defaultValue) {
|
|
|
8772
8594
|
if (typeof raw === "boolean") return raw;
|
|
8773
8595
|
return defaultValue;
|
|
8774
8596
|
}
|
|
8775
|
-
/**
|
|
8776
|
-
* 解析合并配置:未知键忽略、不合法值回退默认值,返回冻结的完整配置。
|
|
8777
|
-
* 允许所有配置留空——缺省 / null / 空串的键回退默认值,未给出的键亦取默认值。
|
|
8778
|
-
*/
|
|
8597
|
+
/** 解析合并配置:未知键忽略、不合法值回退默认值,返回冻结的完整配置。 */
|
|
8779
8598
|
function resolveConfig(raw) {
|
|
8780
|
-
/** 原始输入(留空 / 不合法视为空对象)。 */
|
|
8781
8599
|
const input = normalizeConfigInput(raw);
|
|
8782
|
-
/** 合并结果(以默认值为基底)。 */
|
|
8783
8600
|
const config = { ...DEFAULT_CONFIG };
|
|
8784
8601
|
for (const [key, integer] of NUMBER_KEYS) {
|
|
8785
|
-
/** 该键的原始值(留空 / 不合法则跳过,保持默认值)。 */
|
|
8786
8602
|
const value = input[key];
|
|
8787
8603
|
if (value === void 0 || value === null) continue;
|
|
8788
8604
|
if (typeof value === "string" && value.trim() === "") continue;
|
|
@@ -8790,47 +8606,126 @@ function resolveConfig(raw) {
|
|
|
8790
8606
|
if (integer && !Number.isInteger(value)) continue;
|
|
8791
8607
|
config[key] = value;
|
|
8792
8608
|
}
|
|
8793
|
-
/** OM 自动压缩开关(缺省启用;false 关闭自动压缩,recall 工具不受影响)。 */
|
|
8794
8609
|
config.omEnabled = resolveBoolean(input.omEnabled, true);
|
|
8795
|
-
/** 步骤级日志开关(缺省按 NODE_ENV !== 'production' 判定)。 */
|
|
8796
8610
|
config.debug = resolveBoolean(input.debug, process.env.NODE_ENV !== "production");
|
|
8797
|
-
/** recall 工具开关(缺省启用)。 */
|
|
8798
8611
|
config.recallEnabled = resolveBoolean(input.recallEnabled, true);
|
|
8799
|
-
/** recall-semantic 工具开关(缺省启用)。 */
|
|
8800
8612
|
config.semanticRecallEnabled = resolveBoolean(input.semanticRecallEnabled, true);
|
|
8801
|
-
/** modelDir 原始值(留空 / 非空串以外回退默认;非空串必须为字符串)。 */
|
|
8802
8613
|
const modelDir = input.modelDir;
|
|
8803
8614
|
if (typeof modelDir === "string" && modelDir.trim() !== "") config.modelDir = modelDir;
|
|
8804
8615
|
return Object.freeze(config);
|
|
8805
8616
|
}
|
|
8806
8617
|
//#endregion
|
|
8618
|
+
//#region src/json-schema.ts
|
|
8619
|
+
/**
|
|
8620
|
+
* zod schema → 工具 wire 参数 JSON Schema 的转换工具。导出 parametersFromZod。
|
|
8621
|
+
* 工具参数定义统一由 zod 的 toJSONSchema 生成(.describe() 描述透传),剥除
|
|
8622
|
+
* $schema 元键与 additionalProperties(未知键在执行期由 zod 解析剥离)。
|
|
8623
|
+
*/
|
|
8624
|
+
/** 由 zod schema 生成 wire 参数 JSON Schema(根 type:'object',含 properties/required)。 */
|
|
8625
|
+
function parametersFromZod(schema) {
|
|
8626
|
+
const { $schema: _schema, additionalProperties: _additional, ...rest } = toJSONSchema(schema);
|
|
8627
|
+
return rest;
|
|
8628
|
+
}
|
|
8629
|
+
//#endregion
|
|
8630
|
+
//#region src/recall-output.ts
|
|
8631
|
+
/** 纯文本结果(无图片)的便捷构造。 */
|
|
8632
|
+
function textOnly(text) {
|
|
8633
|
+
return {
|
|
8634
|
+
text,
|
|
8635
|
+
images: []
|
|
8636
|
+
};
|
|
8637
|
+
}
|
|
8638
|
+
/** 一张图片的文本标注行:[图片附件:name(mediaType W×H,N bytes)]。 */
|
|
8639
|
+
function imageNote(ref) {
|
|
8640
|
+
return `[图片附件${ref.name ? `:${ref.name}` : ""}(${ref.mediaType} ${ref.width}×${ref.height},${ref.bytes} bytes)]`;
|
|
8641
|
+
}
|
|
8642
|
+
/** 输出值 wire schema:宿主按此校验 execute 返回值。 */
|
|
8643
|
+
const RECALL_OUTPUT_SCHEMA = {
|
|
8644
|
+
type: "object",
|
|
8645
|
+
additionalProperties: false,
|
|
8646
|
+
properties: {
|
|
8647
|
+
text: {
|
|
8648
|
+
type: "string",
|
|
8649
|
+
description: "完整消息内容的文本部分(带图位置以 [图片附件:…] 标注行提示)。"
|
|
8650
|
+
},
|
|
8651
|
+
images: {
|
|
8652
|
+
type: "array",
|
|
8653
|
+
description: "文本中引用的图片附件元数据(按出现顺序;渲染为随文本之后的 image 内容块,宿主按 attachmentId 解析字节)。",
|
|
8654
|
+
items: {
|
|
8655
|
+
type: "object",
|
|
8656
|
+
additionalProperties: false,
|
|
8657
|
+
properties: {
|
|
8658
|
+
attachmentId: {
|
|
8659
|
+
type: "string",
|
|
8660
|
+
description: "附件服务的持久图片 id。"
|
|
8661
|
+
},
|
|
8662
|
+
mediaType: {
|
|
8663
|
+
type: "string",
|
|
8664
|
+
description: "图片媒体类型(如 image/png)。"
|
|
8665
|
+
},
|
|
8666
|
+
bytes: {
|
|
8667
|
+
type: "integer",
|
|
8668
|
+
description: "字节数。"
|
|
8669
|
+
},
|
|
8670
|
+
width: {
|
|
8671
|
+
type: "integer",
|
|
8672
|
+
description: "固有显示宽度(px)。"
|
|
8673
|
+
},
|
|
8674
|
+
height: {
|
|
8675
|
+
type: "integer",
|
|
8676
|
+
description: "固有显示高度(px)。"
|
|
8677
|
+
},
|
|
8678
|
+
name: {
|
|
8679
|
+
type: "string",
|
|
8680
|
+
description: "原始文件名(可选)。"
|
|
8681
|
+
}
|
|
8682
|
+
},
|
|
8683
|
+
required: [
|
|
8684
|
+
"attachmentId",
|
|
8685
|
+
"mediaType",
|
|
8686
|
+
"bytes",
|
|
8687
|
+
"width",
|
|
8688
|
+
"height"
|
|
8689
|
+
]
|
|
8690
|
+
}
|
|
8691
|
+
}
|
|
8692
|
+
},
|
|
8693
|
+
required: ["text", "images"]
|
|
8694
|
+
};
|
|
8695
|
+
/** 把输出值投影为模型内容:text 块在前,images 逐个投影为 image 块。 */
|
|
8696
|
+
function renderRecallOutput(value) {
|
|
8697
|
+
const blocks = [{
|
|
8698
|
+
type: "text",
|
|
8699
|
+
text: value.text
|
|
8700
|
+
}];
|
|
8701
|
+
for (const ref of value.images) {
|
|
8702
|
+
const block = {
|
|
8703
|
+
type: "image",
|
|
8704
|
+
attachment: ref
|
|
8705
|
+
};
|
|
8706
|
+
blocks.push(block);
|
|
8707
|
+
}
|
|
8708
|
+
return blocks;
|
|
8709
|
+
}
|
|
8710
|
+
//#endregion
|
|
8807
8711
|
//#region src/recall.ts
|
|
8808
8712
|
/**
|
|
8809
|
-
* recall
|
|
8810
|
-
*
|
|
8811
|
-
*
|
|
8812
|
-
*
|
|
8813
|
-
* recall 自身不设输出上限:超大的工具结果由 tool-result-pruner 裁剪(pruneContent),
|
|
8814
|
-
* 输出 token 由 pruner 配置控制。
|
|
8815
|
-
*
|
|
8816
|
-
* 参数由 zod schema(recallArgsSchema)在 execute 入口校验:start 必填(number),
|
|
8817
|
-
* end/offset 至少提供一个;非法参数抛出可读错误。
|
|
8713
|
+
* recall 工具:按完整消息 index 区间回看原始会话(含被压缩内容)。
|
|
8714
|
+
* 导出 recallArgsSchema / RecallArgs / parseRecallArgs / buildRecallTool。
|
|
8715
|
+
* 输出值为 { text, images }(契约见 recall-output.ts);recall 自身不设输出上限,
|
|
8716
|
+
* 超大工具结果由 tool-result-pruner 裁剪。
|
|
8818
8717
|
*/
|
|
8819
8718
|
/**
|
|
8820
|
-
* recall 工具参数 schema:start
|
|
8821
|
-
*
|
|
8719
|
+
* recall 工具参数 schema:start 必填;end 与 offset 至少提供一个(同时给出时 end 优先)。
|
|
8720
|
+
* 各字段描述经 .describe() 透传到 wire JSON Schema。
|
|
8822
8721
|
*/
|
|
8823
8722
|
const recallArgsSchema = z.object({
|
|
8824
|
-
start: z.number(),
|
|
8825
|
-
end: z.number().optional(),
|
|
8826
|
-
offset: z.number().optional()
|
|
8723
|
+
start: z.number().describe("完整消息序号(index),作为基准边界,和 end 或 offset 配合,指定区间;end 与 offset 至少提供一个(同时给出时 end 优先)。"),
|
|
8724
|
+
end: z.number().optional().describe("与 offset 互斥,指定区间的另一个边界。"),
|
|
8725
|
+
offset: z.number().optional().describe("与 end 互斥。相对 start 的步数:正数向后、负数向前。")
|
|
8827
8726
|
}).refine((args) => args.end !== void 0 || args.offset !== void 0, { message: "end 与 offset 至少提供一个" });
|
|
8828
|
-
/**
|
|
8829
|
-
* 解析并校验 recall 调用参数:校验失败时抛出首个校验问题的可读消息
|
|
8830
|
-
* (普通 Error 而非 ZodError,兼容 SDK 展示)。
|
|
8831
|
-
*/
|
|
8727
|
+
/** 解析并校验 recall 调用参数:失败时抛出首个校验问题的可读消息(普通 Error,兼容 SDK 展示)。 */
|
|
8832
8728
|
function parseRecallArgs(raw) {
|
|
8833
|
-
/** safeParse 结果(成功时 data 为 RecallArgs,失败时 error 携带问题列表)。 */
|
|
8834
8729
|
const result = recallArgsSchema.safeParse(raw);
|
|
8835
8730
|
if (!result.success) throw new Error(result.error.issues.map((issue) => `${issue.path.join(".") || "args"}: ${issue.message}`).join("; "));
|
|
8836
8731
|
return result.data;
|
|
@@ -8842,116 +8737,89 @@ function parseRecallArgs(raw) {
|
|
|
8842
8737
|
function buildRecallTool(getPruner) {
|
|
8843
8738
|
return {
|
|
8844
8739
|
name: "recall",
|
|
8845
|
-
description: `${COMPLETE_MESSAGE_DEFINITION}此工具可以精确查询完整消息。用index
|
|
8846
|
-
parameters:
|
|
8847
|
-
start: {
|
|
8848
|
-
type: "number",
|
|
8849
|
-
description: "完整消息序号(index),作为基准边界,和end或offset配合,指定区间",
|
|
8850
|
-
required: true
|
|
8851
|
-
},
|
|
8852
|
-
end: {
|
|
8853
|
-
type: "number",
|
|
8854
|
-
description: "与 offset 互斥,指定区间的另一个边界。"
|
|
8855
|
-
},
|
|
8856
|
-
offset: {
|
|
8857
|
-
type: "number",
|
|
8858
|
-
description: "与 end 互斥。相对 start 的步数:正数向后、负数向前。"
|
|
8859
|
-
}
|
|
8860
|
-
},
|
|
8740
|
+
description: `${COMPLETE_MESSAGE_DEFINITION}此工具可以精确查询完整消息。用index指定一个区间,返回区间内所有完整消息的内容;完整消息携带的图片附件随结果保留(文本段以 [图片附件:…] 标注,随后附 image 内容块)。`,
|
|
8741
|
+
parameters: parametersFromZod(recallArgsSchema),
|
|
8861
8742
|
output: {
|
|
8862
|
-
schema:
|
|
8863
|
-
render: (_args, value) =>
|
|
8864
|
-
type: "text",
|
|
8865
|
-
text: String(value)
|
|
8866
|
-
}]
|
|
8743
|
+
schema: RECALL_OUTPUT_SCHEMA,
|
|
8744
|
+
render: (_args, value) => renderRecallOutput(value)
|
|
8867
8745
|
},
|
|
8868
8746
|
async execute(args, exec) {
|
|
8869
|
-
/** 解析并校验后的调用参数(不满足 schema 时抛出可读错误)。 */
|
|
8870
8747
|
const { start, end, offset } = parseRecallArgs(args);
|
|
8871
|
-
/** 当前会话(缺失则无法回看)。 */
|
|
8872
8748
|
const session = exec.agent?.session;
|
|
8873
|
-
if (!session) return "会话异常";
|
|
8874
|
-
if (!isMainSession(session)) return "recall 仅主会话可用";
|
|
8875
|
-
/** 完整消息索引(index = 数组下标,0 起)。 */
|
|
8749
|
+
if (!session) return textOnly("会话异常");
|
|
8750
|
+
if (!isMainSession(session)) return textOnly("recall 仅主会话可用");
|
|
8876
8751
|
const cms = indexCompleteMessages(session);
|
|
8877
|
-
/** 起始下标(非整数 floor;越界返回提示)。 */
|
|
8878
8752
|
const startIndex = Number.isFinite(start) ? Math.floor(start) : 0;
|
|
8879
|
-
if (startIndex < 0 || startIndex >= cms.length) return `start ${start} 越界(完整消息共 ${cms.length} 条,index 范围 0..${cms.length - 1}
|
|
8880
|
-
/** 终点下标(end 优先,否则 startIndex + offset)。 */
|
|
8753
|
+
if (startIndex < 0 || startIndex >= cms.length) return textOnly(`start ${start} 越界(完整消息共 ${cms.length} 条,index 范围 0..${cms.length - 1})`);
|
|
8881
8754
|
let endIndex;
|
|
8882
8755
|
if (end !== void 0) {
|
|
8883
|
-
/** end 取整(非整数 floor)。 */
|
|
8884
8756
|
const e = Number.isFinite(end) ? Math.floor(end) : 0;
|
|
8885
|
-
if (e < 0 || e >= cms.length) return `end ${end} 越界(完整消息共 ${cms.length} 条,index 范围 0..${cms.length - 1}
|
|
8757
|
+
if (e < 0 || e >= cms.length) return textOnly(`end ${end} 越界(完整消息共 ${cms.length} 条,index 范围 0..${cms.length - 1})`);
|
|
8886
8758
|
endIndex = e;
|
|
8887
8759
|
} else {
|
|
8888
|
-
/** offset 数值(refine 保证此分支 offset 已提供,schema 保证为 number)。 */
|
|
8889
8760
|
const raw = offset ?? 0;
|
|
8890
8761
|
endIndex = startIndex + (Number.isFinite(raw) ? Math.floor(raw) : 0);
|
|
8891
8762
|
}
|
|
8892
|
-
/** 区间下界(钳制到 [0, len-1])。 */
|
|
8893
8763
|
const lo = Math.max(0, Math.min(startIndex, endIndex));
|
|
8894
|
-
/** 区间上界(钳制到 [0, len-1])。 */
|
|
8895
8764
|
const hi = Math.min(cms.length - 1, Math.max(startIndex, endIndex));
|
|
8896
|
-
/** tool-result-pruner(可选,裁剪超大工具结果)。 */
|
|
8897
8765
|
const pruner = getPruner?.();
|
|
8898
|
-
/** 渲染结果缓冲(每条完整消息一段,标 index + 类型)。 */
|
|
8899
8766
|
const parts = [];
|
|
8767
|
+
const images = [];
|
|
8900
8768
|
for (let i = lo; i <= hi; i += 1) {
|
|
8901
|
-
/** 当前完整消息。 */
|
|
8902
8769
|
const cm = cms[i];
|
|
8903
8770
|
if (!cm) continue;
|
|
8904
|
-
/** 该条完整消息的呈现文本(pruner 裁剪超大结果;单条失败不影响整体)。 */
|
|
8905
8771
|
let text = "";
|
|
8772
|
+
let cmImages = [];
|
|
8906
8773
|
try {
|
|
8907
|
-
|
|
8774
|
+
const rendered = renderCompleteMessageParts(session, cm, pruner);
|
|
8775
|
+
text = rendered.text;
|
|
8776
|
+
cmImages = rendered.images;
|
|
8908
8777
|
} catch {}
|
|
8909
|
-
/** 类型标注(toolcall 附调用 id,便于模型关联)。 */
|
|
8910
8778
|
const callAttr = cm.type === "toolcall" && cm.callId ? ` callId=${cm.callId}` : "";
|
|
8911
|
-
|
|
8779
|
+
let body = text;
|
|
8780
|
+
if (cmImages.length > 0) {
|
|
8781
|
+
body += (body === "" ? "" : "\n") + cmImages.map(imageNote).join("\n");
|
|
8782
|
+
images.push(...cmImages);
|
|
8783
|
+
}
|
|
8784
|
+
parts.push(`-- [index ${cm.index}] ${cm.type}${callAttr} --\n${body}`);
|
|
8912
8785
|
}
|
|
8913
|
-
if (parts.length === 0) return "指定区间没有完整消息";
|
|
8914
|
-
return
|
|
8786
|
+
if (parts.length === 0) return textOnly("指定区间没有完整消息");
|
|
8787
|
+
return {
|
|
8788
|
+
text: parts.join("\n\n"),
|
|
8789
|
+
images
|
|
8790
|
+
};
|
|
8915
8791
|
}
|
|
8916
8792
|
};
|
|
8917
8793
|
}
|
|
8918
8794
|
//#endregion
|
|
8919
8795
|
//#region src/semantic-recall.ts
|
|
8920
8796
|
/**
|
|
8921
|
-
* recall-semantic 工具:按自然语言 query
|
|
8922
|
-
*
|
|
8923
|
-
*
|
|
8924
|
-
*
|
|
8925
|
-
*
|
|
8926
|
-
*
|
|
8927
|
-
*
|
|
8928
|
-
* - 区间缺省(start 未提供)→ 检索全部消息;区间不合法(start/end 越界等)→
|
|
8929
|
-
* 不报错,回退全量检索并在输出中明确告知(模型可见)。
|
|
8930
|
-
* - 向量:本地 ONNX embedding(embedding.ts,懒加载 + 批量);相似度 = cosine。
|
|
8931
|
-
* - 输出:命中消息的 index/类型(toolcall 附调用 id)+ 完整渲染文本 + 匹配说明(相似度、
|
|
8932
|
-
* 命中的关键词);超大结果由 tool-result-pruner 裁剪(同 recall)。
|
|
8933
|
-
* - 仅主会话可用(subagent 拒绝,与 recall 一致)。
|
|
8797
|
+
* recall-semantic 工具:按自然语言 query 对会话全部完整消息(含被压缩/遮蔽的)做
|
|
8798
|
+
* 语义检索,返回最匹配的完整消息与匹配说明。
|
|
8799
|
+
* 导出 semanticRecallArgsSchema / SemanticRecallArgs / parseSemanticRecallArgs /
|
|
8800
|
+
* resolveSemanticRange / tokenize / matchExplanation / buildSemanticRecallTool。
|
|
8801
|
+
* 向量为本地 ONNX 嵌入(cosine 相似度);只匹配文本,纯图片消息不进候选池;
|
|
8802
|
+
* 区间缺省检索全部、区间不合法回退全量并在输出中告知;输出契约同 recall
|
|
8803
|
+
* ({ text, images },超大结果由 tool-result-pruner 裁剪);仅主会话可用。
|
|
8934
8804
|
*/
|
|
8935
8805
|
/** recall-semantic 工具参数 schema:query 必填;top_k 默认 3(1-10);区间参数均可选。 */
|
|
8936
8806
|
const semanticRecallArgsSchema = z.object({
|
|
8937
|
-
query: z.string(),
|
|
8938
|
-
top_k: z.number().int().min(1).max(10).optional(),
|
|
8939
|
-
start: z.number().optional(),
|
|
8940
|
-
end: z.number().optional(),
|
|
8941
|
-
offset: z.number().optional()
|
|
8807
|
+
query: z.string().describe("描述要找的内容的自然语言 query(可混用中英文与代码术语,如 \"修复 retry backoff 的逻辑\"),不能为空。"),
|
|
8808
|
+
top_k: z.number().int().min(1).max(10).optional().describe("返回最匹配的完整消息条数(1-10,默认 3)。"),
|
|
8809
|
+
start: z.number().optional().describe("完整消息序号(index),可选。作为基准边界,和end或offset配合,指定搜索区间。"),
|
|
8810
|
+
end: z.number().optional().describe("必须和start配合使用,与 offset 互斥,指定搜索区间的另一个边界。"),
|
|
8811
|
+
offset: z.number().optional().describe("必须和start配合使用,与 end 互斥。相对 start 的步数:正数向后、负数向前。")
|
|
8942
8812
|
}).refine((args) => args.query.trim().length > 0, { message: "query 不能为空" });
|
|
8943
8813
|
/** 解析并校验参数:失败时抛出可读错误(普通 Error 而非 ZodError)。 */
|
|
8944
8814
|
function parseSemanticRecallArgs(raw) {
|
|
8945
|
-
/** safeParse 结果。 */
|
|
8946
8815
|
const result = semanticRecallArgsSchema.safeParse(raw);
|
|
8947
8816
|
if (!result.success) throw new Error(result.error.issues.map((issue) => `${issue.path.join(".") || "args"}:${issue.message}`).join("; "));
|
|
8948
8817
|
return result.data;
|
|
8949
8818
|
}
|
|
8950
8819
|
/**
|
|
8951
8820
|
* 解析检索区间(完整消息 index,含端点)。
|
|
8952
|
-
*
|
|
8953
|
-
*
|
|
8954
|
-
* start/end 越界或区间为空 → fallback=true(全量 + 提示)。
|
|
8821
|
+
* start 未提供 → 全量(fallback=false);start/end 越界或区间为空 → fallback=true
|
|
8822
|
+
* (回退全量并在输出中告知)。
|
|
8955
8823
|
*/
|
|
8956
8824
|
function resolveSemanticRange(total, args) {
|
|
8957
8825
|
if (total <= 0) return {
|
|
@@ -8964,17 +8832,14 @@ function resolveSemanticRange(total, args) {
|
|
|
8964
8832
|
hi: total - 1,
|
|
8965
8833
|
fallback: false
|
|
8966
8834
|
};
|
|
8967
|
-
/** start 取整(非有限数按 -1 处理,走越界回退)。 */
|
|
8968
8835
|
const start = Number.isFinite(args.start) ? Math.floor(args.start) : -1;
|
|
8969
8836
|
if (start < 0 || start >= total) return {
|
|
8970
8837
|
lo: 0,
|
|
8971
8838
|
hi: total - 1,
|
|
8972
8839
|
fallback: true
|
|
8973
8840
|
};
|
|
8974
|
-
/** 终点 index(end 优先;否则 start + offset 取整)。 */
|
|
8975
8841
|
let end;
|
|
8976
8842
|
if (args.end !== void 0) {
|
|
8977
|
-
/** end 取整(非有限数按 -1 处理,走越界回退)。 */
|
|
8978
8843
|
const found = Number.isFinite(args.end) ? Math.floor(args.end) : -1;
|
|
8979
8844
|
if (found < 0 || found >= total) return {
|
|
8980
8845
|
lo: 0,
|
|
@@ -8984,9 +8849,7 @@ function resolveSemanticRange(total, args) {
|
|
|
8984
8849
|
end = found;
|
|
8985
8850
|
} else if (args.offset !== void 0) end = start + (Number.isFinite(args.offset) ? Math.floor(args.offset) : 0);
|
|
8986
8851
|
else end = start;
|
|
8987
|
-
/** 区间下界(钳制)。 */
|
|
8988
8852
|
const lo = Math.max(0, Math.min(start, end));
|
|
8989
|
-
/** 区间上界(钳制)。 */
|
|
8990
8853
|
const hi = Math.min(total - 1, Math.max(start, end));
|
|
8991
8854
|
if (lo > hi) return {
|
|
8992
8855
|
lo: 0,
|
|
@@ -9001,92 +8864,53 @@ function resolveSemanticRange(total, args) {
|
|
|
9001
8864
|
}
|
|
9002
8865
|
/** 简易分词:按非字母数字切分为小写词元(匹配要点用,非检索核心)。 */
|
|
9003
8866
|
function tokenize(text) {
|
|
9004
|
-
/** 词元集合。 */
|
|
9005
8867
|
const tokens = /* @__PURE__ */ new Set();
|
|
9006
8868
|
for (const part of text.toLowerCase().split(/[^\p{L}\p{N}]+/u)) if (part.length > 0) tokens.add(part);
|
|
9007
8869
|
return tokens;
|
|
9008
8870
|
}
|
|
9009
8871
|
/** 命中消息的匹配说明行:相似度 + 与 query 共有的关键词(最多 8 个)。 */
|
|
9010
8872
|
function matchExplanation(query, text, score) {
|
|
9011
|
-
/** query 词元。 */
|
|
9012
8873
|
const qTokens = tokenize(query);
|
|
9013
|
-
/** 命中消息文本词元。 */
|
|
9014
8874
|
const mTokens = tokenize(text);
|
|
9015
|
-
/** 共有词元(交集,按 query 词序)。 */
|
|
9016
8875
|
const shared = [];
|
|
9017
8876
|
for (const token of qTokens) {
|
|
9018
8877
|
if (mTokens.has(token)) shared.push(token);
|
|
9019
8878
|
if (shared.length >= 8) break;
|
|
9020
8879
|
}
|
|
9021
|
-
/** 关键词部分(无共有词则省略)。 */
|
|
9022
8880
|
const keywords = shared.length > 0 ? `,命中关键词: ${shared.join(" ")}` : "";
|
|
9023
8881
|
return `相似度 ${score.toFixed(3)}${keywords}`;
|
|
9024
8882
|
}
|
|
9025
|
-
/**
|
|
8883
|
+
/** 模型未就绪时返回给模型的文案(下载完成后直接再次调用即可)。 */
|
|
9026
8884
|
const SEMANTIC_MODEL_NOT_READY_MESSAGE = "语义检索暂不可用,本地向量模型尚未就绪。";
|
|
9027
8885
|
/** 构建 recall-semantic 工具定义(embedder/modelStatus 可注入,测试传替身;缺省用本地模型)。 */
|
|
9028
8886
|
function buildSemanticRecallTool(options) {
|
|
9029
|
-
/** pruner 获取器(可选,裁剪超大工具结果)。 */
|
|
9030
8887
|
const getPruner = options?.getPruner ?? (() => void 0);
|
|
9031
|
-
/** 嵌入函数(注入替身或懒加载本地模型)。 */
|
|
9032
8888
|
const embed = options?.embedder ?? ((texts) => getEmbedder().then((fn) => fn(texts)));
|
|
9033
8889
|
return {
|
|
9034
8890
|
name: "recall-semantic",
|
|
9035
|
-
description: `${COMPLETE_MESSAGE_DEFINITION}
|
|
9036
|
-
parameters:
|
|
9037
|
-
query: {
|
|
9038
|
-
type: "string",
|
|
9039
|
-
description: "描述要找的内容的自然语言 query(可混用中英文与代码术语,如 \"修复 retry backoff 的逻辑\")。",
|
|
9040
|
-
required: true
|
|
9041
|
-
},
|
|
9042
|
-
top_k: {
|
|
9043
|
-
type: "number",
|
|
9044
|
-
description: "返回最匹配的完整消息条数(1-10,默认 3)。"
|
|
9045
|
-
},
|
|
9046
|
-
start: {
|
|
9047
|
-
type: "number",
|
|
9048
|
-
description: "完整消息序号(index),可选。作为基准边界,和end或offset配合,指定搜索区间。"
|
|
9049
|
-
},
|
|
9050
|
-
end: {
|
|
9051
|
-
type: "number",
|
|
9052
|
-
description: "必须和start配合使用,与 offset 互斥,指定搜索区间的另一个边界。"
|
|
9053
|
-
},
|
|
9054
|
-
offset: {
|
|
9055
|
-
type: "number",
|
|
9056
|
-
description: "必须和start配合使用,与 end 互斥。相对 start 的步数:正数向后、负数向前。"
|
|
9057
|
-
}
|
|
9058
|
-
},
|
|
8891
|
+
description: `${COMPLETE_MESSAGE_DEFINITION}此工具可以按自然语言含义,检索最符合的完整消息。默认在全消息范围搜索,可以指定区间。注意:语义检索只匹配文本,图片内容不参与匹配(纯图片消息无法命中,也不能按图片内容检索);命中的完整消息携带的图片附件随结果保留(文本段以 [图片附件:…] 标注,随后附 image 内容块),与 recall 相同。`,
|
|
8892
|
+
parameters: parametersFromZod(semanticRecallArgsSchema),
|
|
9059
8893
|
output: {
|
|
9060
|
-
schema:
|
|
9061
|
-
render: (_args, value) =>
|
|
9062
|
-
type: "text",
|
|
9063
|
-
text: String(value)
|
|
9064
|
-
}]
|
|
8894
|
+
schema: RECALL_OUTPUT_SCHEMA,
|
|
8895
|
+
render: (_args, value) => renderRecallOutput(value)
|
|
9065
8896
|
},
|
|
9066
8897
|
async execute(args, exec) {
|
|
9067
|
-
/** 解析并校验后的调用参数。 */
|
|
9068
8898
|
const { query, top_k, start, end, offset } = parseSemanticRecallArgs(args);
|
|
9069
|
-
/** 当前会话。 */
|
|
9070
8899
|
const session = exec.agent?.session;
|
|
9071
|
-
if (!session) return "会话异常";
|
|
9072
|
-
if (!isMainSession(session)) return "recall-semantic 仅主会话可用";
|
|
9073
|
-
if ((await options?.modelStatus?.() ?? "ready") !== "ready") return SEMANTIC_MODEL_NOT_READY_MESSAGE;
|
|
9074
|
-
/** 完整消息索引(全部事件,含被压缩/遮蔽)。 */
|
|
8900
|
+
if (!session) return textOnly("会话异常");
|
|
8901
|
+
if (!isMainSession(session)) return textOnly("recall-semantic 仅主会话可用");
|
|
8902
|
+
if ((await options?.modelStatus?.() ?? "ready") !== "ready") return textOnly(SEMANTIC_MODEL_NOT_READY_MESSAGE);
|
|
9075
8903
|
const cms = indexCompleteMessages(session);
|
|
9076
|
-
if (cms.length === 0) return "会话中没有可检索的消息";
|
|
9077
|
-
/** 检索区间(完整消息 index;区间不合法时回退全量并标记)。 */
|
|
8904
|
+
if (cms.length === 0) return textOnly("会话中没有可检索的消息");
|
|
9078
8905
|
const range = resolveSemanticRange(cms.length, {
|
|
9079
8906
|
start,
|
|
9080
8907
|
end,
|
|
9081
8908
|
offset
|
|
9082
8909
|
});
|
|
9083
|
-
/** 候选完整消息渲染文本(区间内;跳过渲染失败/空文本)。 */
|
|
9084
8910
|
const candidates = [];
|
|
9085
8911
|
for (let i = range.lo; i <= range.hi; i += 1) {
|
|
9086
|
-
/** 当前完整消息。 */
|
|
9087
8912
|
const cm = cms[i];
|
|
9088
8913
|
if (!cm) continue;
|
|
9089
|
-
/** 渲染文本(嵌入用完整文本,输出时再裁剪)。 */
|
|
9090
8914
|
let text = "";
|
|
9091
8915
|
try {
|
|
9092
8916
|
text = renderCompleteMessage(session, cm);
|
|
@@ -9096,67 +8920,54 @@ function buildSemanticRecallTool(options) {
|
|
|
9096
8920
|
text
|
|
9097
8921
|
});
|
|
9098
8922
|
}
|
|
9099
|
-
if (candidates.length === 0) return "指定范围内没有可检索的消息";
|
|
9100
|
-
/** 查询向量(单条)。 */
|
|
8923
|
+
if (candidates.length === 0) return textOnly("指定范围内没有可检索的消息");
|
|
9101
8924
|
const [queryVec] = await embed([query]);
|
|
9102
|
-
if (!queryVec) return "语义检索失败:无法生成查询向量";
|
|
9103
|
-
/** 候选文本批量嵌入。 */
|
|
8925
|
+
if (!queryVec) return textOnly("语义检索失败:无法生成查询向量");
|
|
9104
8926
|
const vectors = await embed(candidates.map((c) => c.text));
|
|
9105
|
-
/** 打分结果(相似度 + 候选)。 */
|
|
9106
8927
|
const scored = candidates.map((c, i) => ({
|
|
9107
8928
|
cm: c.cm,
|
|
9108
8929
|
text: c.text,
|
|
9109
8930
|
score: cosineSimilarity(queryVec, vectors[i] ?? /* @__PURE__ */ new Float32Array(0))
|
|
9110
8931
|
}));
|
|
9111
8932
|
scored.sort((a, b) => b.score - a.score);
|
|
9112
|
-
/** 返回条数(top_k 默认 3)。 */
|
|
9113
8933
|
const limit = top_k ?? 3;
|
|
9114
8934
|
const hits = scored.slice(0, limit);
|
|
9115
|
-
/** 结果缓冲。 */
|
|
9116
8935
|
const parts = [];
|
|
9117
|
-
|
|
8936
|
+
const images = [];
|
|
9118
8937
|
const rangeNote = range.fallback ? "指定区间不合法(start/end 越界等),已回退检索全部消息" : start === void 0 ? `检索全部消息(${candidates.length} 条可嵌入)` : `检索区间 [${range.lo}..${range.hi}](${candidates.length} 条可嵌入)`;
|
|
9119
8938
|
parts.push(`查询: ${query}`);
|
|
9120
8939
|
parts.push(rangeNote);
|
|
9121
8940
|
parts.push(`匹配 TOP-${hits.length}(共 ${scored.length} 条候选):`);
|
|
9122
8941
|
for (let i = 0; i < hits.length; i += 1) {
|
|
9123
|
-
/** 当前命中。 */
|
|
9124
8942
|
const hit = hits[i];
|
|
9125
8943
|
if (!hit) continue;
|
|
9126
|
-
/** 类型标注(toolcall 附调用 id,与 recall 输出一致)。 */
|
|
9127
8944
|
const callAttr = hit.cm.type === "toolcall" && hit.cm.callId ? ` callId=${hit.cm.callId}` : "";
|
|
9128
|
-
/** 输出文本(pruner 裁剪超大内容;裁剪失败保留嵌入用文本)。 */
|
|
9129
8945
|
let text = hit.text;
|
|
8946
|
+
let hitImages = [];
|
|
9130
8947
|
try {
|
|
9131
|
-
/** tool-result-pruner(可选)。 */
|
|
9132
8948
|
const pruner = getPruner();
|
|
9133
|
-
|
|
8949
|
+
const rendered = renderCompleteMessageParts(session, hit.cm, pruner);
|
|
8950
|
+
text = rendered.text;
|
|
8951
|
+
hitImages = rendered.images;
|
|
9134
8952
|
} catch {}
|
|
9135
8953
|
parts.push(`-- [${i + 1}] index ${hit.cm.index} ${hit.cm.type}${callAttr} — ${matchExplanation(query, hit.text, hit.score)} --`);
|
|
9136
|
-
parts.push(text);
|
|
8954
|
+
parts.push(hitImages.length > 0 ? [text, ...hitImages.map(imageNote)].join("\n") : text);
|
|
8955
|
+
images.push(...hitImages);
|
|
9137
8956
|
}
|
|
9138
|
-
return
|
|
8957
|
+
return {
|
|
8958
|
+
text: parts.join("\n\n"),
|
|
8959
|
+
images
|
|
8960
|
+
};
|
|
9139
8961
|
}
|
|
9140
8962
|
};
|
|
9141
8963
|
}
|
|
9142
8964
|
//#endregion
|
|
9143
8965
|
//#region src/index.ts
|
|
9144
8966
|
/**
|
|
9145
|
-
* dsh-plugin-om
|
|
9146
|
-
*
|
|
9147
|
-
*
|
|
9148
|
-
*
|
|
9149
|
-
* - recall.ts recall({ start, end?, offset? }) 工具:按完整消息 index 回看原始会话
|
|
9150
|
-
* - semantic-recall.ts recall-semantic({ query, top_k?, start?, end?, offset? }) 工具:
|
|
9151
|
-
* 按语义在全部完整消息(含被压缩/遮蔽)中检索,返回最匹配的完整消息与匹配说明
|
|
9152
|
-
* (本地 ONNX embedding,模型随插件打包,懒加载)
|
|
9153
|
-
* - compress.ts 自动压缩(OM 观察/反思两级阈值):pre-step 阻塞串行执行——
|
|
9154
|
-
* 反思(<history> 块 tokens 合计 ≥ reflectThresholdTokens 时摘要调用精简合并)、
|
|
9155
|
-
* 观察(未压缩消息 tokens ≥ observeThresholdTokens 时摘要调用压缩为观察日志并追加)
|
|
9156
|
-
*
|
|
9157
|
-
* 约束:不引入自定义会话事件类型——压缩复用宿主已知的 compaction/* 生命周期事件
|
|
9158
|
-
* (start/summary/end)与 checkpoint 标记,结果写入消息记录与轨迹。
|
|
9159
|
-
* 仅主会话生效(subagent 不压缩、recall 拒绝)。
|
|
8967
|
+
* dsh-plugin-om 入口(tsdown 打包入口):导出 name / inject / apply。
|
|
8968
|
+
* apply 注册 recall / recall-semantic 工具,并接线 agent/pre-step 自动压缩
|
|
8969
|
+
* (先反思后观察,仅主会话生效)。压缩与检索的实现见 compress.ts / recall.ts /
|
|
8970
|
+
* semantic-recall.ts。
|
|
9160
8971
|
*/
|
|
9161
8972
|
/** 插件名(Loader 识别入口的稳定标识)。 */
|
|
9162
8973
|
const name = "dsh-plugin-om";
|
|
@@ -9167,21 +8978,14 @@ const inject = [
|
|
|
9167
8978
|
"tokenMeter",
|
|
9168
8979
|
"sessions"
|
|
9169
8980
|
];
|
|
9170
|
-
/**
|
|
9171
|
-
* 插件激活入口:注册 recall / recall-semantic 工具(由配置键 recallEnabled /
|
|
9172
|
-
* semanticRecallEnabled 控制),并在 agent/pre-step 阻塞触发两级自动压缩
|
|
9173
|
-
* (先反思后观察)。仅主会话生效。
|
|
9174
|
-
*/
|
|
8981
|
+
/** 插件激活入口:解析配置、注册工具、接线 pre-step 自动压缩。 */
|
|
9175
8982
|
function apply(ctx, config) {
|
|
9176
|
-
/** 解析后的插件配置(默认值合并 + 校验)。 */
|
|
9177
8983
|
const resolved = resolveConfig(config);
|
|
9178
|
-
/** 插件日志门面(step=debug 按配置 debug 开关输出;info/warn 始终输出)。 */
|
|
9179
8984
|
const logger = makeLogger(ctx, resolved.debug);
|
|
9180
8985
|
logger.step(`apply 启动:observeThresholdTokens=${String(resolved.observeThresholdTokens)} reflectThresholdTokens=${String(resolved.reflectThresholdTokens)} compressMaxTokens=${String(resolved.compressMaxTokens)} tailMessageCount=${String(resolved.tailMessageCount)} omEnabled=${String(resolved.omEnabled)} debug=${String(resolved.debug)}`);
|
|
9181
8986
|
if (resolved.recallEnabled) ctx.tools.register(buildRecallTool(() => ctx.get("toolResultPruner")));
|
|
9182
8987
|
if (resolved.semanticRecallEnabled) {
|
|
9183
8988
|
const warnModel = (message) => ctx.logger.warn(`dsh-plugin-om: ${message}`);
|
|
9184
|
-
/** 下载开始/结束日志:console + 插件日志双通道(失败经 warnModel 输出)。 */
|
|
9185
8989
|
const logModel = (message) => {
|
|
9186
8990
|
console.log(message);
|
|
9187
8991
|
logger.info(message);
|