@cup319/mmpl 2.5.0

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.
@@ -0,0 +1,502 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ fetchWithRetry,
4
+ withLlmLock
5
+ } from "./chunk-SSDURI3I.js";
6
+ import "./chunk-2H7UOFLK.js";
7
+
8
+ // src/symbolic.ts
9
+ import { promises as fs } from "fs";
10
+ import path from "path";
11
+ var NODE_STATUSES = ["done", "doing", "paused", "blocked"];
12
+ var SymbolicCompressor = class {
13
+ storage;
14
+ analyzerApiUrl;
15
+ analyzerApiKey;
16
+ analyzerModel;
17
+ dataDir;
18
+ /** L0 原始记录与符号图的文件根目录 */
19
+ rawDir;
20
+ graphDir;
21
+ nodeIndexPath;
22
+ constructor(opts) {
23
+ this.storage = opts.storage;
24
+ this.analyzerApiUrl = opts.analyzerApiUrl;
25
+ this.analyzerApiKey = opts.analyzerApiKey;
26
+ this.analyzerModel = opts.analyzerModel;
27
+ this.dataDir = opts.dataDir;
28
+ const root = path.join(this.dataDir, "symbolic");
29
+ this.rawDir = path.join(root, "raw");
30
+ this.graphDir = path.join(root, "graphs");
31
+ this.nodeIndexPath = path.join(root, "node-index.json");
32
+ }
33
+ // ===== 公开 API =====
34
+ /**
35
+ * 把一批工具调用压缩成符号图。
36
+ * - 每条 tool_call_pair 作为 RawRecord(type='offload') 下沉到 L0
37
+ * - 调 LLM 生成/更新 Mermaid 拓扑
38
+ * - 返回含渲染好 mermaid 文本的 SymbolGraph
39
+ */
40
+ async symbolize(opts) {
41
+ const sessionId = opts.session_id ?? "default";
42
+ const pairs = opts.toolCallPairs ?? [];
43
+ const now = /* @__PURE__ */ new Date();
44
+ const isoNow = now.toISOString();
45
+ const rawRecordIds = [];
46
+ for (const pair of pairs) {
47
+ const rawId = `raw_${Date.now()}_${this.randomId()}`;
48
+ const record = {
49
+ id: rawId,
50
+ type: "offload",
51
+ content: JSON.stringify({
52
+ tool_call_id: pair.tool_call_id,
53
+ tool_name: pair.tool_name,
54
+ input: pair.input,
55
+ output: pair.output
56
+ }),
57
+ source_id: null,
58
+ session_id: sessionId,
59
+ created_at: Date.now(),
60
+ metadata: {
61
+ tool_name: pair.tool_name,
62
+ tool_call_id: pair.tool_call_id,
63
+ input_chars: pair.input?.length ?? 0,
64
+ output_chars: pair.output?.length ?? 0
65
+ }
66
+ };
67
+ try {
68
+ await this.persistRaw(record);
69
+ rawRecordIds.push(rawId);
70
+ } catch (err) {
71
+ console.warn(`[symbolic] \u5199\u5165 RawRecord \u5931\u8D25 (${rawId}):`, err.message);
72
+ }
73
+ }
74
+ const summaries = pairs.map((p, i) => {
75
+ const inp = this.truncate(p.input ?? "", 200);
76
+ const out = this.truncate(p.output ?? "", 200);
77
+ return `#${i + 1} [${p.tool_name}] (${p.tool_call_id})
78
+ \u8F93\u5165: ${inp}
79
+ \u8F93\u51FA: ${out}`;
80
+ });
81
+ let parsed;
82
+ try {
83
+ parsed = await this.generateGraphViaLLM(summaries, opts.existingGraph ?? null, rawRecordIds);
84
+ } catch (err) {
85
+ console.warn("[symbolic] LLM \u751F\u6210\u62D3\u6251\u5931\u8D25\uFF0C\u964D\u7EA7\u4E3A\u673A\u68B0\u94FE\u72B6\u56FE:", err.message);
86
+ parsed = this.fallbackGraph(summaries, rawRecordIds);
87
+ }
88
+ const nodes = this.normalizeNodes(parsed.nodes, rawRecordIds, isoNow);
89
+ const edges = this.normalizeEdges(parsed.edges, nodes);
90
+ const progress = this.clampInt(parsed.progress, 0, 100);
91
+ const taskGoal = parsed.task_goal || opts.existingGraph?.task_goal || (pairs.length > 0 ? `\u6267\u884C ${pairs.length} \u6B21\u5DE5\u5177\u8C03\u7528` : "\uFF08\u7A7A\u4EFB\u52A1\uFF09");
92
+ const graph = {
93
+ task_goal: taskGoal,
94
+ progress,
95
+ created_time: opts.existingGraph?.created_time ?? isoNow,
96
+ updated_time: isoNow,
97
+ nodes,
98
+ edges,
99
+ mermaid: ""
100
+ // 先占位,下面渲染
101
+ };
102
+ graph.mermaid = this.buildMermaid(graph);
103
+ try {
104
+ await this.saveSessionGraph(sessionId, graph);
105
+ await this.updateNodeIndex(nodes, sessionId);
106
+ } catch (err) {
107
+ console.warn("[symbolic] \u6301\u4E45\u5316\u7B26\u53F7\u56FE\u5931\u8D25:", err.message);
108
+ }
109
+ return graph;
110
+ }
111
+ /**
112
+ * 钻取:根据节点 node_id 回捞对应的原始工具日志(RawRecord)。
113
+ */
114
+ async symbolRecall(node_id) {
115
+ let rawRecordId = null;
116
+ try {
117
+ const index = await this.loadNodeIndex();
118
+ const entry = index[node_id];
119
+ if (entry) rawRecordId = entry.raw_record_id;
120
+ } catch (err) {
121
+ console.warn("[symbolic] \u8BFB\u53D6\u8282\u70B9\u7D22\u5F15\u5931\u8D25:", err.message);
122
+ }
123
+ if (!rawRecordId) {
124
+ rawRecordId = node_id;
125
+ }
126
+ try {
127
+ return await this.loadRaw(rawRecordId);
128
+ } catch (err) {
129
+ console.warn(`[symbolic] symbolRecall \u8BFB\u53D6\u539F\u59CB\u8BB0\u5F55\u5931\u8D25 (${rawRecordId}):`, err.message);
130
+ return null;
131
+ }
132
+ }
133
+ /**
134
+ * 取某会话最新的符号图。
135
+ */
136
+ async getSessionGraph(session_id) {
137
+ return this.loadSessionGraph(session_id);
138
+ }
139
+ // ===== LLM 调用 =====
140
+ /**
141
+ * 调 LLM 生成/更新拓扑。返回规整前的 parsed 对象。
142
+ */
143
+ async generateGraphViaLLM(summaries, existingGraph, rawRecordIds) {
144
+ if (!this.analyzerApiUrl || !this.analyzerApiKey) {
145
+ throw new Error("\u672A\u914D\u7F6E analyzerApiUrl / analyzerApiKey");
146
+ }
147
+ const systemPrompt = "\u4F60\u662F\u4E00\u540D\u300C\u4EFB\u52A1\u62D3\u6251\u67B6\u6784\u5E08\u300D\u3002\u4F60\u7684\u804C\u8D23\u662F\u628A\u4E00\u7CFB\u5217\u5DE5\u5177\u8C03\u7528\u538B\u7F29\u6210\u4E00\u5F20\u7D27\u51D1\u7684 Mermaid \u62D3\u6251\u6D41\u7A0B\u56FE\uFF0C\u53EA\u4FDD\u7559\u5B8C\u6210\u4EFB\u52A1\u6240\u5FC5\u9700\u7684\u5173\u952E\u8282\u70B9\u4E0E\u4F9D\u8D56\u5173\u7CFB\u3002\u4E25\u683C\u6309 JSON \u8FD4\u56DE\uFF0C\u4E0D\u8981\u4EFB\u4F55\u89E3\u91CA\u3002";
148
+ const existingHint = existingGraph ? `\u5F53\u524D\u5DF2\u5B58\u5728\u7684\u7B26\u53F7\u56FE\uFF08\u8BF7\u5728\u6B64\u57FA\u7840\u4E0A\u589E\u91CF\u66F4\u65B0\uFF0C\u4FDD\u7559\u4ECD\u6709\u6548\u7684\u5386\u53F2\u8282\u70B9\uFF0C\u5408\u5E76\u91CD\u590D\u8282\u70B9\uFF09\uFF1A
149
+ ${JSON.stringify(
150
+ {
151
+ task_goal: existingGraph.task_goal,
152
+ progress: existingGraph.progress,
153
+ nodes: existingGraph.nodes.map((n) => ({
154
+ node_id: n.node_id,
155
+ label: n.label,
156
+ status: n.status,
157
+ summary: n.summary
158
+ })),
159
+ edges: existingGraph.edges
160
+ },
161
+ null,
162
+ 2
163
+ )}
164
+ ` : "\u5F53\u524D\u65E0\u5DF2\u6709\u7B26\u53F7\u56FE\uFF0C\u8BF7\u4ECE\u96F6\u6784\u5EFA\u3002\n";
165
+ const rawHint = rawRecordIds.length > 0 ? `\u672C\u6B21\u65B0\u589E\u7684\u539F\u59CB\u8BB0\u5F55 ID\uFF08\u8BF7\u6309\u8282\u70B9\u987A\u5E8F\u586B\u5165\u5BF9\u5E94 node \u7684 raw_record_id \u5B57\u6BB5\uFF09\uFF1A
166
+ ${JSON.stringify(
167
+ rawRecordIds
168
+ )}
169
+ ` : "\u672C\u6B21\u65E0\u65B0\u589E\u539F\u59CB\u8BB0\u5F55\u3002\n";
170
+ const userPrompt = `\u8BF7\u6839\u636E\u4EE5\u4E0B\u5DE5\u5177\u8C03\u7528\u6458\u8981\uFF0C\u751F\u6210/\u66F4\u65B0\u4EFB\u52A1\u62D3\u6251\u7B26\u53F7\u56FE\u3002
171
+
172
+ ${existingHint}
173
+ ${rawHint}
174
+ \u5DE5\u5177\u8C03\u7528\u6458\u8981\uFF1A
175
+ ${summaries.length > 0 ? summaries.join("\n\n") : "\uFF08\u672C\u8F6E\u65E0\u5DE5\u5177\u8C03\u7528\uFF09"}
176
+
177
+ \u8FD4\u56DE\u683C\u5F0F\uFF08\u7EAF JSON\uFF0C\u4E0D\u8981 markdown \u5305\u88F9\uFF09\uFF1A
178
+ {
179
+ "task_goal": "\u672C\u6B21\u4EFB\u52A1\u7684\u603B\u4F53\u76EE\u6807\uFF08\u4E00\u53E5\u8BDD\uFF09",
180
+ "progress": 0-100 \u7684\u6574\u6570,
181
+ "mermaid": "\u53EF\u9009\uFF1A\u4F60\u5EFA\u8BAE\u7684 mermaid \u6587\u672C",
182
+ "nodes": [
183
+ {
184
+ "node_id": "\u8282\u70B9\u552F\u4E00 id\uFF08\u5B57\u7B26\u4E32\uFF09",
185
+ "label": "\u8282\u70B9\u7B80\u77ED\u6807\u7B7E\uFF08\u5982\uFF1A\u641C\u7D22\u6587\u4EF6\uFF09",
186
+ "status": "done | doing | paused | blocked",
187
+ "summary": "\u8BE5\u6B65\u9AA4\u505A\u4E86\u4EC0\u4E48/\u7ED3\u679C\u6458\u8981\uFF08\u4E00\u53E5\u8BDD\uFF09",
188
+ "timestamp": "ISO8601 \u65F6\u95F4\u5B57\u7B26\u4E32",
189
+ "raw_record_id": "\u5BF9\u5E94\u539F\u59CB\u8BB0\u5F55 id\uFF08\u6765\u81EA\u4E0A\u65B9\u5217\u8868\uFF0C\u6309\u8282\u70B9\u987A\u5E8F\uFF09"
190
+ }
191
+ ],
192
+ "edges": [
193
+ { "from": "node_id", "to": "node_id", "label": "\u53EF\u9009\u4F9D\u8D56\u8BF4\u660E" }
194
+ ]
195
+ }
196
+
197
+ \u8981\u6C42\uFF1A
198
+ - \u8282\u70B9\u5C3D\u91CF\u5C11\u800C\u7CBE\uFF0C\u5408\u5E76\u540C\u7C7B\u5DE5\u5177\u8C03\u7528\u3002
199
+ - edges \u5FC5\u987B\u5F15\u7528 nodes \u4E2D\u771F\u5B9E\u5B58\u5728\u7684 node_id\u3002
200
+ - \u53EA\u8FD4\u56DE JSON\u3002`;
201
+ const content = await this.callLLM(systemPrompt, userPrompt);
202
+ const parsed = this.parseJsonLoose(content);
203
+ if (!parsed || typeof parsed !== "object") {
204
+ throw new Error("LLM \u8FD4\u56DE\u975E JSON");
205
+ }
206
+ return {
207
+ task_goal: typeof parsed.task_goal === "string" ? parsed.task_goal : "",
208
+ progress: typeof parsed.progress === "number" ? parsed.progress : 0,
209
+ nodes: Array.isArray(parsed.nodes) ? parsed.nodes : [],
210
+ edges: Array.isArray(parsed.edges) ? parsed.edges : []
211
+ };
212
+ }
213
+ /**
214
+ * 调用 LLM(OpenAI 兼容 API)。复用全局 LLM 锁与重试。
215
+ */
216
+ async callLLM(systemPrompt, userPrompt) {
217
+ const response = await withLlmLock(
218
+ () => fetchWithRetry(
219
+ this.analyzerApiUrl,
220
+ {
221
+ method: "POST",
222
+ headers: {
223
+ "Content-Type": "application/json",
224
+ Authorization: `Bearer ${this.analyzerApiKey}`
225
+ },
226
+ body: JSON.stringify({
227
+ model: this.analyzerModel,
228
+ messages: [
229
+ { role: "system", content: systemPrompt },
230
+ { role: "user", content: userPrompt }
231
+ ],
232
+ temperature: 0.3
233
+ })
234
+ }
235
+ )
236
+ );
237
+ if (!response.ok) {
238
+ throw new Error(`Symbolic LLM API error: ${response.status} ${response.statusText}`);
239
+ }
240
+ const data = await response.json();
241
+ return data.choices?.[0]?.message?.content ?? "";
242
+ }
243
+ // ===== Mermaid 渲染 =====
244
+ /**
245
+ * 把 SymbolGraph 渲染成 Mermaid 'graph TD' 文本。
246
+ *
247
+ * 例:
248
+ * graph TD
249
+ * N1["搜索文件<br/>status: done<br/>summary: 找到配置"]
250
+ * N2["编辑配置<br/>status: done<br/>summary: 修改参数"]
251
+ * N1 --> N2
252
+ */
253
+ buildMermaid(graph) {
254
+ const lines = ["graph TD"];
255
+ const idMap = /* @__PURE__ */ new Map();
256
+ graph.nodes.forEach((n, i) => {
257
+ idMap.set(n.node_id, `N${i + 1}`);
258
+ });
259
+ for (const node of graph.nodes) {
260
+ const mermaidId = idMap.get(node.node_id) ?? "N?";
261
+ const label = this.mermaidText(node.label || node.node_id);
262
+ const status = this.mermaidText(node.status || "");
263
+ const summary = this.mermaidText(node.summary || "");
264
+ lines.push(`${mermaidId}["${label}<br/>status: ${status}<br/>summary: ${summary}"]`);
265
+ }
266
+ for (const edge of graph.edges) {
267
+ const from = idMap.get(edge.from);
268
+ const to = idMap.get(edge.to);
269
+ if (!from || !to) continue;
270
+ if (edge.label && String(edge.label).trim()) {
271
+ lines.push(`${from} -->|${this.mermaidText(String(edge.label))}| ${to}`);
272
+ } else {
273
+ lines.push(`${from} --> ${to}`);
274
+ }
275
+ }
276
+ return lines.join("\n");
277
+ }
278
+ // ===== 持久化(文件为源真值,storage 镜像为可选) =====
279
+ /** 写入一条 RawRecord。先写文件,再尽力镜像到 storage。 */
280
+ async persistRaw(record) {
281
+ await fs.mkdir(this.rawDir, { recursive: true });
282
+ await fs.writeFile(path.join(this.rawDir, `${record.id}.json`), JSON.stringify(record), "utf8");
283
+ if (this.storage && typeof this.storage.saveRawRecord === "function") {
284
+ try {
285
+ await this.storage.saveRawRecord(record);
286
+ } catch (err) {
287
+ console.warn("[symbolic] storage.saveRawRecord \u955C\u50CF\u5931\u8D25:", err.message);
288
+ }
289
+ }
290
+ }
291
+ /** 读取一条 RawRecord。优先文件(源真值),缺失再试 storage。 */
292
+ async loadRaw(id) {
293
+ const file = path.join(this.rawDir, `${id}.json`);
294
+ try {
295
+ const txt = await fs.readFile(file, "utf8");
296
+ return JSON.parse(txt);
297
+ } catch {
298
+ }
299
+ if (this.storage && typeof this.storage.getRawRecord === "function") {
300
+ try {
301
+ return await this.storage.getRawRecord(id);
302
+ } catch (err) {
303
+ console.warn(`[symbolic] storage.getRawRecord \u5931\u8D25 (${id}):`, err.message);
304
+ }
305
+ }
306
+ return null;
307
+ }
308
+ /** 保存会话最新符号图(按 session_id 覆盖 = 始终是最新)。 */
309
+ async saveSessionGraph(sessionId, graph) {
310
+ await fs.mkdir(this.graphDir, { recursive: true });
311
+ const safe = this.sanitizeFilename(sessionId) || "default";
312
+ await fs.writeFile(path.join(this.graphDir, `${safe}.json`), JSON.stringify(graph), "utf8");
313
+ if (this.storage && typeof this.storage.saveSymbolGraph === "function") {
314
+ try {
315
+ await this.storage.saveSymbolGraph(sessionId, graph);
316
+ } catch (err) {
317
+ console.warn("[symbolic] storage.saveSymbolGraph \u955C\u50CF\u5931\u8D25:", err.message);
318
+ }
319
+ }
320
+ }
321
+ /** 读取会话最新符号图。 */
322
+ async loadSessionGraph(sessionId) {
323
+ const safe = this.sanitizeFilename(sessionId) || "default";
324
+ try {
325
+ const txt = await fs.readFile(path.join(this.graphDir, `${safe}.json`), "utf8");
326
+ return JSON.parse(txt);
327
+ } catch {
328
+ }
329
+ if (this.storage && typeof this.storage.getSymbolGraph === "function") {
330
+ try {
331
+ return await this.storage.getSymbolGraph(sessionId);
332
+ } catch (err) {
333
+ console.warn(`[symbolic] storage.getSymbolGraph \u5931\u8D25 (${sessionId}):`, err.message);
334
+ }
335
+ }
336
+ return null;
337
+ }
338
+ /** 读取节点索引(node_id → {raw_record_id, session_id})。 */
339
+ async loadNodeIndex() {
340
+ try {
341
+ const txt = await fs.readFile(this.nodeIndexPath, "utf8");
342
+ const obj = JSON.parse(txt);
343
+ return typeof obj === "object" && obj ? obj : {};
344
+ } catch {
345
+ return {};
346
+ }
347
+ }
348
+ /** 合并更新节点索引(覆盖本批节点条目)。 */
349
+ async updateNodeIndex(nodes, sessionId) {
350
+ await fs.mkdir(this.graphDir, { recursive: true });
351
+ const index = await this.loadNodeIndex();
352
+ for (const n of nodes) {
353
+ index[n.node_id] = {
354
+ raw_record_id: n.raw_record_id,
355
+ session_id: sessionId
356
+ };
357
+ }
358
+ await fs.writeFile(this.nodeIndexPath, JSON.stringify(index), "utf8");
359
+ }
360
+ // ===== 规整 / 降级 / 工具方法 =====
361
+ /** 规整节点数组:校验字段、补全 raw_record_id / timestamp,去重 node_id。 */
362
+ normalizeNodes(rawNodes, rawRecordIds, isoNow) {
363
+ const seen = /* @__PURE__ */ new Set();
364
+ const result = [];
365
+ let autoIdx = 0;
366
+ rawNodes.forEach((rn, i) => {
367
+ if (!rn || typeof rn !== "object") return;
368
+ let nodeId = String(rn.node_id ?? "").trim();
369
+ if (!nodeId) {
370
+ nodeId = `sym_${Date.now()}_${this.randomId()}`;
371
+ }
372
+ if (seen.has(nodeId)) {
373
+ nodeId = `${nodeId}_${i}`;
374
+ }
375
+ seen.add(nodeId);
376
+ let rawId = rn.raw_record_id ? String(rn.raw_record_id).trim() : "";
377
+ if ((!rawId || rawId === "null" || rawId === "undefined") && i < rawRecordIds.length) {
378
+ rawId = rawRecordIds[i];
379
+ }
380
+ const rawRecordId = rawId && rawId !== "null" && rawId !== "undefined" ? rawId : null;
381
+ const status = NODE_STATUSES.includes(String(rn.status)) ? String(rn.status) : "done";
382
+ result.push({
383
+ node_id: nodeId,
384
+ label: this.truncate(String(rn.label ?? "\u6B65\u9AA4").trim() || "\u6B65\u9AA4", 60),
385
+ status,
386
+ summary: this.truncate(String(rn.summary ?? "").trim(), 120),
387
+ timestamp: rn.timestamp ? String(rn.timestamp) : isoNow,
388
+ raw_record_id: rawRecordId
389
+ });
390
+ autoIdx++;
391
+ });
392
+ if (result.length === 0 && rawRecordIds.length > 0) {
393
+ rawRecordIds.forEach((rid, i) => {
394
+ const nodeId = `sym_${Date.now()}_${i}_${this.randomId()}`;
395
+ result.push({
396
+ node_id: nodeId,
397
+ label: `\u6B65\u9AA4 ${i + 1}`,
398
+ status: "done",
399
+ summary: "",
400
+ timestamp: isoNow,
401
+ raw_record_id: rid
402
+ });
403
+ });
404
+ }
405
+ return result;
406
+ }
407
+ /** 规整边:过滤掉引用不存在节点的悬空边。 */
408
+ normalizeEdges(rawEdges, nodes) {
409
+ const validIds = new Set(nodes.map((n) => n.node_id));
410
+ const result = [];
411
+ const seen = /* @__PURE__ */ new Set();
412
+ for (const e of rawEdges) {
413
+ if (!e || typeof e !== "object") continue;
414
+ const from = String(e.from ?? "").trim();
415
+ const to = String(e.to ?? "").trim();
416
+ if (!from || !to) continue;
417
+ if (!validIds.has(from) || !validIds.has(to)) continue;
418
+ const key = `${from}->${to}`;
419
+ if (seen.has(key)) continue;
420
+ seen.add(key);
421
+ const label = e.label != null ? String(e.label).trim() : void 0;
422
+ result.push({ from, to, label: label || void 0 });
423
+ }
424
+ return result;
425
+ }
426
+ /** 降级:LLM 不可用时,把工具调用串成一条线性链。 */
427
+ fallbackGraph(summaries, rawRecordIds) {
428
+ const nodes = [];
429
+ const edges = [];
430
+ summaries.forEach((s, i) => {
431
+ const firstLine = s.split("\n")[0];
432
+ const labelMatch = firstLine.match(/\[([^\]]+)\]/);
433
+ const label = labelMatch ? labelMatch[1] : `\u6B65\u9AA4 ${i + 1}`;
434
+ const nodeId = `sym_${Date.now()}_${i}_${this.randomId()}`;
435
+ nodes.push({
436
+ node_id: nodeId,
437
+ label,
438
+ status: "done",
439
+ summary: this.truncate(s.replace(/\n/g, " "), 120),
440
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
441
+ raw_record_id: rawRecordIds[i] ?? null
442
+ });
443
+ if (i > 0) {
444
+ edges.push({ from: nodes[i - 1].node_id, to: nodeId });
445
+ }
446
+ });
447
+ return {
448
+ task_goal: "\uFF08\u964D\u7EA7\u751F\u6210\u7684\u7EBF\u6027\u5DE5\u5177\u94FE\uFF09",
449
+ progress: summaries.length > 0 ? 100 : 0,
450
+ nodes,
451
+ edges
452
+ };
453
+ }
454
+ /** 宽松 JSON 解析:去 markdown 围栏、截取最外层花括号。 */
455
+ parseJsonLoose(content) {
456
+ let s = (content ?? "").trim();
457
+ const fence = s.match(/```(?:json)?\s*([\s\S]*?)```/);
458
+ if (fence) s = fence[1].trim();
459
+ const start = s.indexOf("{");
460
+ const end = s.lastIndexOf("}");
461
+ if (start >= 0 && end > start) s = s.substring(start, end + 1);
462
+ try {
463
+ return JSON.parse(s);
464
+ } catch {
465
+ return null;
466
+ }
467
+ }
468
+ // ===== 纯工具 =====
469
+ /** 截断字符串,超出加省略号。 */
470
+ truncate(s, max) {
471
+ if (!s) return "";
472
+ if (s.length <= max) return s;
473
+ return s.slice(0, max - 1) + "\u2026";
474
+ }
475
+ /** 把整数限制在 [min, max]。 */
476
+ clampInt(v, min, max) {
477
+ const n = Math.round(Number(v));
478
+ if (!Number.isFinite(n)) return min;
479
+ return Math.min(max, Math.max(min, n));
480
+ }
481
+ /**
482
+ * Mermaid 节点文本转义:
483
+ * - 双引号 → &quot;
484
+ * - 换行 → <br/>
485
+ * - 方括号 → 全角,避免破坏节点语法
486
+ */
487
+ mermaidText(s) {
488
+ return (s ?? "").replace(/\\/g, "\\\\").replace(/"/g, "&quot;").replace(/\r?\n/g, "<br/>").replace(/\[/g, "\u3010").replace(/]/g, "\u3011");
489
+ }
490
+ /** 文件名安全化:只保留字母数字、下划线、短横、点。 */
491
+ sanitizeFilename(name) {
492
+ return (name ?? "").replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 128);
493
+ }
494
+ /** 6 位随机 id。 */
495
+ randomId() {
496
+ return Math.random().toString(36).slice(2, 8);
497
+ }
498
+ };
499
+ export {
500
+ SymbolicCompressor
501
+ };
502
+ //# sourceMappingURL=symbolic-A4XAGBBB.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/symbolic.ts"],"sourcesContent":["/**\n * 符号压缩器 (Symbolic Compressor) — Memory Palace MCP v2.0\n *\n * 灵感:TencentDB Agent Memory 的 Context Offload + Mermaid canvas。\n *\n * 核心思想:\n * 把冗长的工具调用日志(~数万 token)压缩成紧凑的 Mermaid 流程图符号图(~数百 token),\n * 原始日志下沉到 L0 存储(offload),上下文中只保留符号图。\n * 需要细节时再通过 symbolRecall 钻取回原始记录。\n *\n * 工作流:\n * symbolize() ──┬─→ 每条 tool_call_pair 作为 RawRecord(type='offload') 写入 L0\n * ├─→ 调用 LLM(任务拓扑架构师)生成/更新 Mermaid 拓扑图\n * └─→ 返回 SymbolGraph(含渲染好的 mermaid 文本)\n * symbolRecall(node_id) ──→ 钻取回该节点对应的原始工具日志\n * getSessionGraph(session_id) ──→ 取回某会话最新的符号图\n *\n * 持久化策略:\n * storage 对象若 duck-type 出 saveRawRecord/getRawRecord 等方法则尽力镜像写入;\n * 但源真值(source of truth)始终落在 dataDir 下的 JSON 文件中,\n * 以保证即便 storage 尚未实现相关 CRUD 也能独立工作(不修改既有文件)。\n */\n\nimport { promises as fs } from \"node:fs\";\nimport path from \"node:path\";\nimport { SymbolGraph, SymbolNode, RawRecord } from \"./types.js\";\nimport { withLlmLock, fetchWithRetry } from \"./llm-lock.js\";\n\n/** 工具调用对(输入 + 输出) */\nexport interface ToolCallPair {\n tool_call_id: string;\n tool_name: string;\n input: string;\n output: string;\n}\n\n/** symbolize 入参 */\nexport interface SymbolizeOptions {\n toolCallPairs: ToolCallPair[];\n existingGraph?: SymbolGraph | null;\n session_id?: string;\n}\n\n/** constructor 配置 */\nexport interface SymbolicCompressorOptions {\n storage: any;\n analyzerApiUrl: string;\n analyzerApiKey: string;\n analyzerModel: string;\n dataDir: string;\n}\n\n/** 节点状态白名单 */\nconst NODE_STATUSES: readonly string[] = [\"done\", \"doing\", \"paused\", \"blocked\"] as const;\n\n/** 节点索引条目:node_id → 关联信息(供 symbolRecall 钻取) */\ninterface NodeIndexEntry {\n raw_record_id: string | null;\n session_id: string;\n}\n\nexport class SymbolicCompressor {\n private storage: any;\n private analyzerApiUrl: string;\n private analyzerApiKey: string;\n private analyzerModel: string;\n private dataDir: string;\n\n /** L0 原始记录与符号图的文件根目录 */\n private readonly rawDir: string;\n private readonly graphDir: string;\n private readonly nodeIndexPath: string;\n\n constructor(opts: SymbolicCompressorOptions) {\n this.storage = opts.storage;\n this.analyzerApiUrl = opts.analyzerApiUrl;\n this.analyzerApiKey = opts.analyzerApiKey;\n this.analyzerModel = opts.analyzerModel;\n this.dataDir = opts.dataDir;\n\n const root = path.join(this.dataDir, \"symbolic\");\n this.rawDir = path.join(root, \"raw\");\n this.graphDir = path.join(root, \"graphs\");\n this.nodeIndexPath = path.join(root, \"node-index.json\");\n }\n\n // ===== 公开 API =====\n\n /**\n * 把一批工具调用压缩成符号图。\n * - 每条 tool_call_pair 作为 RawRecord(type='offload') 下沉到 L0\n * - 调 LLM 生成/更新 Mermaid 拓扑\n * - 返回含渲染好 mermaid 文本的 SymbolGraph\n */\n async symbolize(opts: SymbolizeOptions): Promise<SymbolGraph> {\n const sessionId = opts.session_id ?? \"default\";\n const pairs = opts.toolCallPairs ?? [];\n const now = new Date();\n const isoNow = now.toISOString();\n\n // 1) 每条工具调用下沉为 L0 RawRecord,记录 raw_record_id 列表\n const rawRecordIds: string[] = [];\n for (const pair of pairs) {\n const rawId = `raw_${Date.now()}_${this.randomId()}`;\n const record: RawRecord = {\n id: rawId,\n type: \"offload\",\n content: JSON.stringify({\n tool_call_id: pair.tool_call_id,\n tool_name: pair.tool_name,\n input: pair.input,\n output: pair.output,\n }),\n source_id: null,\n session_id: sessionId,\n created_at: Date.now(),\n metadata: {\n tool_name: pair.tool_name,\n tool_call_id: pair.tool_call_id,\n input_chars: pair.input?.length ?? 0,\n output_chars: pair.output?.length ?? 0,\n },\n };\n try {\n await this.persistRaw(record);\n rawRecordIds.push(rawId);\n } catch (err) {\n console.warn(`[symbolic] 写入 RawRecord 失败 (${rawId}):`, (err as Error).message);\n }\n }\n\n // 2) 构造给 LLM 的紧凑摘要(截断,正是压缩的意义所在)\n const summaries = pairs.map((p, i) => {\n const inp = this.truncate(p.input ?? \"\", 200);\n const out = this.truncate(p.output ?? \"\", 200);\n return `#${i + 1} [${p.tool_name}] (${p.tool_call_id})\\n输入: ${inp}\\n输出: ${out}`;\n });\n\n // 3) 调 LLM 生成/更新拓扑图;失败则降级为机械图(一条链)\n let parsed: {\n task_goal: string;\n progress: number;\n nodes: any[];\n edges: any[];\n };\n\n try {\n parsed = await this.generateGraphViaLLM(summaries, opts.existingGraph ?? null, rawRecordIds);\n } catch (err) {\n console.warn(\"[symbolic] LLM 生成拓扑失败,降级为机械链状图:\", (err as Error).message);\n parsed = this.fallbackGraph(summaries, rawRecordIds);\n }\n\n // 4) 规整节点:补全 raw_record_id / timestamp / status,做 ID 去重\n const nodes = this.normalizeNodes(parsed.nodes, rawRecordIds, isoNow);\n const edges = this.normalizeEdges(parsed.edges, nodes);\n const progress = this.clampInt(parsed.progress, 0, 100);\n\n // 任务目标:LLM 给出则用,否则从已有图继承,再否则用占位\n const taskGoal =\n parsed.task_goal ||\n opts.existingGraph?.task_goal ||\n (pairs.length > 0 ? `执行 ${pairs.length} 次工具调用` : \"(空任务)\");\n\n // 5) 构造 SymbolGraph 并渲染 mermaid\n const graph: SymbolGraph = {\n task_goal: taskGoal,\n progress,\n created_time: opts.existingGraph?.created_time ?? isoNow,\n updated_time: isoNow,\n nodes,\n edges,\n mermaid: \"\", // 先占位,下面渲染\n };\n graph.mermaid = this.buildMermaid(graph);\n\n // 6) 持久化(会话级最新图)+ 更新节点索引\n try {\n await this.saveSessionGraph(sessionId, graph);\n await this.updateNodeIndex(nodes, sessionId);\n } catch (err) {\n console.warn(\"[symbolic] 持久化符号图失败:\", (err as Error).message);\n }\n\n return graph;\n }\n\n /**\n * 钻取:根据节点 node_id 回捞对应的原始工具日志(RawRecord)。\n */\n async symbolRecall(node_id: string): Promise<RawRecord | null> {\n // 先查节点索引拿到 raw_record_id\n let rawRecordId: string | null = null;\n try {\n const index = await this.loadNodeIndex();\n const entry = index[node_id];\n if (entry) rawRecordId = entry.raw_record_id;\n } catch (err) {\n console.warn(\"[symbolic] 读取节点索引失败:\", (err as Error).message);\n }\n\n if (!rawRecordId) {\n // 兜底:直接当 raw_record_id 用\n rawRecordId = node_id;\n }\n\n try {\n return await this.loadRaw(rawRecordId);\n } catch (err) {\n console.warn(`[symbolic] symbolRecall 读取原始记录失败 (${rawRecordId}):`, (err as Error).message);\n return null;\n }\n }\n\n /**\n * 取某会话最新的符号图。\n */\n async getSessionGraph(session_id: string): Promise<SymbolGraph | null> {\n return this.loadSessionGraph(session_id);\n }\n\n // ===== LLM 调用 =====\n\n /**\n * 调 LLM 生成/更新拓扑。返回规整前的 parsed 对象。\n */\n private async generateGraphViaLLM(\n summaries: string[],\n existingGraph: SymbolGraph | null,\n rawRecordIds: string[]\n ): Promise<{ task_goal: string; progress: number; nodes: any[]; edges: any[] }> {\n if (!this.analyzerApiUrl || !this.analyzerApiKey) {\n throw new Error(\"未配置 analyzerApiUrl / analyzerApiKey\");\n }\n\n const systemPrompt =\n \"你是一名「任务拓扑架构师」。你的职责是把一系列工具调用压缩成一张紧凑的 Mermaid 拓扑流程图,\" +\n \"只保留完成任务所必需的关键节点与依赖关系。严格按 JSON 返回,不要任何解释。\";\n\n const existingHint = existingGraph\n ? `当前已存在的符号图(请在此基础上增量更新,保留仍有效的历史节点,合并重复节点):\\n${JSON.stringify(\n {\n task_goal: existingGraph.task_goal,\n progress: existingGraph.progress,\n nodes: existingGraph.nodes.map((n) => ({\n node_id: n.node_id,\n label: n.label,\n status: n.status,\n summary: n.summary,\n })),\n edges: existingGraph.edges,\n },\n null,\n 2\n )}\\n`\n : \"当前无已有符号图,请从零构建。\\n\";\n\n const rawHint =\n rawRecordIds.length > 0\n ? `本次新增的原始记录 ID(请按节点顺序填入对应 node 的 raw_record_id 字段):\\n${JSON.stringify(\n rawRecordIds\n )}\\n`\n : \"本次无新增原始记录。\\n\";\n\n const userPrompt = `请根据以下工具调用摘要,生成/更新任务拓扑符号图。\n\n${existingHint}\n${rawHint}\n工具调用摘要:\n${summaries.length > 0 ? summaries.join(\"\\n\\n\") : \"(本轮无工具调用)\"}\n\n返回格式(纯 JSON,不要 markdown 包裹):\n{\n \"task_goal\": \"本次任务的总体目标(一句话)\",\n \"progress\": 0-100 的整数,\n \"mermaid\": \"可选:你建议的 mermaid 文本\",\n \"nodes\": [\n {\n \"node_id\": \"节点唯一 id(字符串)\",\n \"label\": \"节点简短标签(如:搜索文件)\",\n \"status\": \"done | doing | paused | blocked\",\n \"summary\": \"该步骤做了什么/结果摘要(一句话)\",\n \"timestamp\": \"ISO8601 时间字符串\",\n \"raw_record_id\": \"对应原始记录 id(来自上方列表,按节点顺序)\"\n }\n ],\n \"edges\": [\n { \"from\": \"node_id\", \"to\": \"node_id\", \"label\": \"可选依赖说明\" }\n ]\n}\n\n要求:\n- 节点尽量少而精,合并同类工具调用。\n- edges 必须引用 nodes 中真实存在的 node_id。\n- 只返回 JSON。`;\n\n const content = await this.callLLM(systemPrompt, userPrompt);\n const parsed = this.parseJsonLoose(content);\n\n if (!parsed || typeof parsed !== \"object\") {\n throw new Error(\"LLM 返回非 JSON\");\n }\n return {\n task_goal: typeof parsed.task_goal === \"string\" ? parsed.task_goal : \"\",\n progress: typeof parsed.progress === \"number\" ? parsed.progress : 0,\n nodes: Array.isArray(parsed.nodes) ? parsed.nodes : [],\n edges: Array.isArray(parsed.edges) ? parsed.edges : [],\n };\n }\n\n /**\n * 调用 LLM(OpenAI 兼容 API)。复用全局 LLM 锁与重试。\n */\n private async callLLM(systemPrompt: string, userPrompt: string): Promise<string> {\n const response = await withLlmLock(() =>\n fetchWithRetry(\n this.analyzerApiUrl,\n {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.analyzerApiKey}`,\n },\n body: JSON.stringify({\n model: this.analyzerModel,\n messages: [\n { role: \"system\", content: systemPrompt },\n { role: \"user\", content: userPrompt },\n ],\n temperature: 0.3,\n }),\n }\n )\n );\n\n if (!response.ok) {\n throw new Error(`Symbolic LLM API error: ${response.status} ${response.statusText}`);\n }\n\n const data = (await response.json()) as any;\n return data.choices?.[0]?.message?.content ?? \"\";\n }\n\n // ===== Mermaid 渲染 =====\n\n /**\n * 把 SymbolGraph 渲染成 Mermaid 'graph TD' 文本。\n *\n * 例:\n * graph TD\n * N1[\"搜索文件<br/>status: done<br/>summary: 找到配置\"]\n * N2[\"编辑配置<br/>status: done<br/>summary: 修改参数\"]\n * N1 --> N2\n */\n private buildMermaid(graph: SymbolGraph): string {\n const lines: string[] = [\"graph TD\"];\n\n // node_id → 短 mermaid 标识(N1, N2 ...),保证合法且稳定\n const idMap = new Map<string, string>();\n graph.nodes.forEach((n, i) => {\n idMap.set(n.node_id, `N${i + 1}`);\n });\n\n // 节点定义\n for (const node of graph.nodes) {\n const mermaidId = idMap.get(node.node_id) ?? \"N?\";\n const label = this.mermaidText(node.label || node.node_id);\n const status = this.mermaidText(node.status || \"\");\n const summary = this.mermaidText(node.summary || \"\");\n lines.push(`${mermaidId}[\"${label}<br/>status: ${status}<br/>summary: ${summary}\"]`);\n }\n\n // 边定义\n for (const edge of graph.edges) {\n const from = idMap.get(edge.from);\n const to = idMap.get(edge.to);\n if (!from || !to) continue; // 丢弃悬空边\n if (edge.label && String(edge.label).trim()) {\n lines.push(`${from} -->|${this.mermaidText(String(edge.label))}| ${to}`);\n } else {\n lines.push(`${from} --> ${to}`);\n }\n }\n\n return lines.join(\"\\n\");\n }\n\n // ===== 持久化(文件为源真值,storage 镜像为可选) =====\n\n /** 写入一条 RawRecord。先写文件,再尽力镜像到 storage。 */\n private async persistRaw(record: RawRecord): Promise<void> {\n await fs.mkdir(this.rawDir, { recursive: true });\n await fs.writeFile(path.join(this.rawDir, `${record.id}.json`), JSON.stringify(record), \"utf8\");\n\n // 可选:镜像到 storage(若存在相应方法)\n if (this.storage && typeof this.storage.saveRawRecord === \"function\") {\n try {\n await this.storage.saveRawRecord(record);\n } catch (err) {\n console.warn(\"[symbolic] storage.saveRawRecord 镜像失败:\", (err as Error).message);\n }\n }\n }\n\n /** 读取一条 RawRecord。优先文件(源真值),缺失再试 storage。 */\n private async loadRaw(id: string): Promise<RawRecord | null> {\n const file = path.join(this.rawDir, `${id}.json`);\n try {\n const txt = await fs.readFile(file, \"utf8\");\n return JSON.parse(txt) as RawRecord;\n } catch {\n /* 文件不存在则尝试 storage */\n }\n\n if (this.storage && typeof this.storage.getRawRecord === \"function\") {\n try {\n return await this.storage.getRawRecord(id);\n } catch (err) {\n console.warn(`[symbolic] storage.getRawRecord 失败 (${id}):`, (err as Error).message);\n }\n }\n return null;\n }\n\n /** 保存会话最新符号图(按 session_id 覆盖 = 始终是最新)。 */\n private async saveSessionGraph(sessionId: string, graph: SymbolGraph): Promise<void> {\n await fs.mkdir(this.graphDir, { recursive: true });\n const safe = this.sanitizeFilename(sessionId) || \"default\";\n await fs.writeFile(path.join(this.graphDir, `${safe}.json`), JSON.stringify(graph), \"utf8\");\n\n if (this.storage && typeof this.storage.saveSymbolGraph === \"function\") {\n try {\n await this.storage.saveSymbolGraph(sessionId, graph);\n } catch (err) {\n console.warn(\"[symbolic] storage.saveSymbolGraph 镜像失败:\", (err as Error).message);\n }\n }\n }\n\n /** 读取会话最新符号图。 */\n private async loadSessionGraph(sessionId: string): Promise<SymbolGraph | null> {\n const safe = this.sanitizeFilename(sessionId) || \"default\";\n try {\n const txt = await fs.readFile(path.join(this.graphDir, `${safe}.json`), \"utf8\");\n return JSON.parse(txt) as SymbolGraph;\n } catch {\n /* 文件不存在 */\n }\n\n if (this.storage && typeof this.storage.getSymbolGraph === \"function\") {\n try {\n return await this.storage.getSymbolGraph(sessionId);\n } catch (err) {\n console.warn(`[symbolic] storage.getSymbolGraph 失败 (${sessionId}):`, (err as Error).message);\n }\n }\n return null;\n }\n\n /** 读取节点索引(node_id → {raw_record_id, session_id})。 */\n private async loadNodeIndex(): Promise<Record<string, NodeIndexEntry>> {\n try {\n const txt = await fs.readFile(this.nodeIndexPath, \"utf8\");\n const obj = JSON.parse(txt);\n return typeof obj === \"object\" && obj ? obj : {};\n } catch {\n return {};\n }\n }\n\n /** 合并更新节点索引(覆盖本批节点条目)。 */\n private async updateNodeIndex(nodes: SymbolNode[], sessionId: string): Promise<void> {\n await fs.mkdir(this.graphDir, { recursive: true });\n const index = await this.loadNodeIndex();\n for (const n of nodes) {\n index[n.node_id] = {\n raw_record_id: n.raw_record_id,\n session_id: sessionId,\n };\n }\n await fs.writeFile(this.nodeIndexPath, JSON.stringify(index), \"utf8\");\n }\n\n // ===== 规整 / 降级 / 工具方法 =====\n\n /** 规整节点数组:校验字段、补全 raw_record_id / timestamp,去重 node_id。 */\n private normalizeNodes(rawNodes: any[], rawRecordIds: string[], isoNow: string): SymbolNode[] {\n const seen = new Set<string>();\n const result: SymbolNode[] = [];\n let autoIdx = 0;\n\n rawNodes.forEach((rn, i) => {\n if (!rn || typeof rn !== \"object\") return;\n\n let nodeId = String(rn.node_id ?? \"\").trim();\n if (!nodeId) {\n nodeId = `sym_${Date.now()}_${this.randomId()}`;\n }\n if (seen.has(nodeId)) {\n nodeId = `${nodeId}_${i}`;\n }\n seen.add(nodeId);\n\n // raw_record_id:若 LLM 没给/给了占位,则按顺序回填本批 raw id\n let rawId = rn.raw_record_id ? String(rn.raw_record_id).trim() : \"\";\n if ((!rawId || rawId === \"null\" || rawId === \"undefined\") && i < rawRecordIds.length) {\n rawId = rawRecordIds[i];\n }\n const rawRecordId = rawId && rawId !== \"null\" && rawId !== \"undefined\" ? rawId : null;\n\n const status = NODE_STATUSES.includes(String(rn.status))\n ? (String(rn.status) as SymbolNode[\"status\"])\n : \"done\";\n\n result.push({\n node_id: nodeId,\n label: this.truncate(String(rn.label ?? \"步骤\").trim() || \"步骤\", 60),\n status,\n summary: this.truncate(String(rn.summary ?? \"\").trim(), 120),\n timestamp: rn.timestamp ? String(rn.timestamp) : isoNow,\n raw_record_id: rawRecordId,\n });\n autoIdx++;\n });\n\n // 若 LLM 一个节点都没产出,但有原始记录,则按 raw 生成一条兜底节点\n if (result.length === 0 && rawRecordIds.length > 0) {\n rawRecordIds.forEach((rid, i) => {\n const nodeId = `sym_${Date.now()}_${i}_${this.randomId()}`;\n result.push({\n node_id: nodeId,\n label: `步骤 ${i + 1}`,\n status: \"done\",\n summary: \"\",\n timestamp: isoNow,\n raw_record_id: rid,\n });\n });\n }\n\n return result;\n }\n\n /** 规整边:过滤掉引用不存在节点的悬空边。 */\n private normalizeEdges(rawEdges: any[], nodes: SymbolNode[]): SymbolGraph[\"edges\"] {\n const validIds = new Set(nodes.map((n) => n.node_id));\n const result: SymbolGraph[\"edges\"] = [];\n const seen = new Set<string>();\n\n for (const e of rawEdges) {\n if (!e || typeof e !== \"object\") continue;\n const from = String(e.from ?? \"\").trim();\n const to = String(e.to ?? \"\").trim();\n if (!from || !to) continue;\n if (!validIds.has(from) || !validIds.has(to)) continue;\n const key = `${from}->${to}`;\n if (seen.has(key)) continue;\n seen.add(key);\n const label = e.label != null ? String(e.label).trim() : undefined;\n result.push({ from, to, label: label || undefined });\n }\n return result;\n }\n\n /** 降级:LLM 不可用时,把工具调用串成一条线性链。 */\n private fallbackGraph(\n summaries: string[],\n rawRecordIds: string[]\n ): { task_goal: string; progress: number; nodes: any[]; edges: any[] } {\n const nodes: any[] = [];\n const edges: any[] = [];\n summaries.forEach((s, i) => {\n const firstLine = s.split(\"\\n\")[0]; // 形如 \"#1 [tool_name] (id)\"\n const labelMatch = firstLine.match(/\\[([^\\]]+)\\]/);\n const label = labelMatch ? labelMatch[1] : `步骤 ${i + 1}`;\n const nodeId = `sym_${Date.now()}_${i}_${this.randomId()}`;\n nodes.push({\n node_id: nodeId,\n label,\n status: \"done\",\n summary: this.truncate(s.replace(/\\n/g, \" \"), 120),\n timestamp: new Date().toISOString(),\n raw_record_id: rawRecordIds[i] ?? null,\n });\n if (i > 0) {\n edges.push({ from: nodes[i - 1].node_id, to: nodeId });\n }\n });\n return {\n task_goal: \"(降级生成的线性工具链)\",\n progress: summaries.length > 0 ? 100 : 0,\n nodes,\n edges,\n };\n }\n\n /** 宽松 JSON 解析:去 markdown 围栏、截取最外层花括号。 */\n private parseJsonLoose(content: string): any | null {\n let s = (content ?? \"\").trim();\n const fence = s.match(/```(?:json)?\\s*([\\s\\S]*?)```/);\n if (fence) s = fence[1].trim();\n const start = s.indexOf(\"{\");\n const end = s.lastIndexOf(\"}\");\n if (start >= 0 && end > start) s = s.substring(start, end + 1);\n try {\n return JSON.parse(s);\n } catch {\n return null;\n }\n }\n\n // ===== 纯工具 =====\n\n /** 截断字符串,超出加省略号。 */\n private truncate(s: string, max: number): string {\n if (!s) return \"\";\n if (s.length <= max) return s;\n return s.slice(0, max - 1) + \"…\";\n }\n\n /** 把整数限制在 [min, max]。 */\n private clampInt(v: any, min: number, max: number): number {\n const n = Math.round(Number(v));\n if (!Number.isFinite(n)) return min;\n return Math.min(max, Math.max(min, n));\n }\n\n /**\n * Mermaid 节点文本转义:\n * - 双引号 → &quot;\n * - 换行 → <br/>\n * - 方括号 → 全角,避免破坏节点语法\n */\n private mermaidText(s: string): string {\n return (s ?? \"\")\n .replace(/\\\\/g, \"\\\\\\\\\")\n .replace(/\"/g, \"&quot;\")\n .replace(/\\r?\\n/g, \"<br/>\")\n .replace(/\\[/g, \"【\")\n .replace(/]/g, \"】\");\n }\n\n /** 文件名安全化:只保留字母数字、下划线、短横、点。 */\n private sanitizeFilename(name: string): string {\n return (name ?? \"\").replace(/[^A-Za-z0-9._-]/g, \"_\").slice(0, 128);\n }\n\n /** 6 位随机 id。 */\n private randomId(): string {\n return Math.random().toString(36).slice(2, 8);\n }\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@cup319/mmpl",
3
+ "version": "2.5.0",
4
+ "publishConfig": {
5
+ "access": "public",
6
+ "registry": "https://registry.npmjs.org/"
7
+ },
8
+ "description": "MMPL (Memory Palace) — DuckDB 向量记忆系统 (L0-L3 分层 + SM-2 衰减 + 符号压缩 + 8 精简工具)",
9
+ "type": "module",
10
+ "main": "dist/index.js",
11
+ "bin": {
12
+ "mmpl": "dist/index.js"
13
+ },
14
+ "scripts": {
15
+ "build": "tsup",
16
+ "dev": "tsx src/index.ts",
17
+ "start": "node dist/index.js",
18
+ "start:http": "node dist/index.js",
19
+ "test": "vitest run",
20
+ "test:watch": "vitest",
21
+ "lint": "tsc --noEmit",
22
+ "inspector": "npx @modelcontextprotocol/inspector node dist/index.js"
23
+ },
24
+ "keywords": [
25
+ "mcp",
26
+ "memory",
27
+ "palace",
28
+ "spaced-repetition",
29
+ "ai-agent"
30
+ ],
31
+ "license": "MIT",
32
+ "dependencies": {
33
+ "@huggingface/transformers": "^4.2.0",
34
+ "@modelcontextprotocol/sdk": "^1.12.1",
35
+ "@node-rs/jieba": "^2.0.1",
36
+ "duckdb": "^1.4.4",
37
+ "onnxruntime-node": "^1.26.0"
38
+ },
39
+ "devDependencies": {
40
+ "@types/better-sqlite3": "^7.6.13",
41
+ "@types/node": "^22.15.0",
42
+ "tsup": "^8.4.0",
43
+ "tsx": "^4.19.0",
44
+ "typescript": "^5.8.0",
45
+ "vitest": "^3.1.0"
46
+ }
47
+ }