@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.
- package/README.md +187 -0
- package/dist/auto-context-NUQFETSY.js +248 -0
- package/dist/auto-context-NUQFETSY.js.map +1 -0
- package/dist/checkpoint-ABRNCBCA.js +68 -0
- package/dist/checkpoint-ABRNCBCA.js.map +1 -0
- package/dist/chunk-2H7UOFLK.js +11 -0
- package/dist/chunk-2H7UOFLK.js.map +1 -0
- package/dist/chunk-CHVFXR24.js +19 -0
- package/dist/chunk-CHVFXR24.js.map +1 -0
- package/dist/chunk-JL66S7P7.js +151 -0
- package/dist/chunk-JL66S7P7.js.map +1 -0
- package/dist/chunk-SSDURI3I.js +46 -0
- package/dist/chunk-SSDURI3I.js.map +1 -0
- package/dist/index.js +21841 -0
- package/dist/index.js.map +1 -0
- package/dist/mermaid-injector-ROQAAM7B.js +171 -0
- package/dist/mermaid-injector-ROQAAM7B.js.map +1 -0
- package/dist/observability-6AMQYVCF.js +100 -0
- package/dist/observability-6AMQYVCF.js.map +1 -0
- package/dist/offload-engine-BF5IXXBH.js +10 -0
- package/dist/offload-engine-BF5IXXBH.js.map +1 -0
- package/dist/persona-generator-NZQVIVDA.js +228 -0
- package/dist/persona-generator-NZQVIVDA.js.map +1 -0
- package/dist/pipeline-manager-IJ2JLUMK.js +485 -0
- package/dist/pipeline-manager-IJ2JLUMK.js.map +1 -0
- package/dist/scene-builder-TLVG23BJ.js +296 -0
- package/dist/scene-builder-TLVG23BJ.js.map +1 -0
- package/dist/scene-index-SU6GMVDL.js +178 -0
- package/dist/scene-index-SU6GMVDL.js.map +1 -0
- package/dist/symbolic-A4XAGBBB.js +502 -0
- package/dist/symbolic-A4XAGBBB.js.map +1 -0
- package/package.json +47 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
fetchWithRetry,
|
|
4
|
+
withLlmLock
|
|
5
|
+
} from "./chunk-SSDURI3I.js";
|
|
6
|
+
import "./chunk-2H7UOFLK.js";
|
|
7
|
+
|
|
8
|
+
// src/scene-builder.ts
|
|
9
|
+
import * as fs from "fs";
|
|
10
|
+
import * as path from "path";
|
|
11
|
+
import * as crypto from "crypto";
|
|
12
|
+
|
|
13
|
+
// src/prompts/scene-prompt.ts
|
|
14
|
+
var SCENE_SYSTEM_PROMPT = `\u4F60\u662F\u300C\u8BB0\u5FC6\u573A\u666F\u5F52\u7EB3\u5668\u300D\uFF0C\u804C\u8D23\u662F\u5C06\u4E00\u7EC4\u76F8\u5173\u8054\u7684\u539F\u5B50\u8BB0\u5FC6\uFF08L1\uFF09\u805A\u7C7B\u4E3A\u82E5\u5E72\u4E3B\u9898\u573A\u666F\uFF08L2\uFF09\uFF0C\u5E76\u4EA7\u51FA\u53EF\u8BFB\u7684\u573A\u666F\u6458\u8981\u3002
|
|
15
|
+
|
|
16
|
+
## \u4EFB\u52A1
|
|
17
|
+
1. \u901A\u8BFB\u7ED9\u5B9A\u7684\u5168\u90E8 L1 \u8BB0\u5FC6\u3002
|
|
18
|
+
2. \u6309\u4E3B\u9898 / \u8BED\u4E49\u76F8\u5173\u6027\u5C06\u5B83\u4EEC\u805A\u7C7B\u4E3A\u82E5\u5E72\u300C\u573A\u666F\u300D\uFF1B\u540C\u4E00\u4E3B\u9898\u7684\u8BB0\u5FC6\u5F52\u5165\u540C\u4E00\u573A\u666F\u3002
|
|
19
|
+
3. \u4E3A\u6BCF\u4E2A\u573A\u666F\u4EA7\u51FA\uFF1A
|
|
20
|
+
- label: \u7B80\u77ED\u7684\u4E3B\u9898\u6807\u7B7E\uFF082-12 \u4E2A\u6C49\u5B57\uFF0C\u540D\u8BCD\u77ED\u8BED\uFF0C\u5982\u300C\u6570\u636E\u5E93\u8FD0\u7EF4\u504F\u597D\u300D\u300C\u524D\u7AEF\u6280\u672F\u6808\u300D\u300C\u5B89\u5168\u89C4\u5219\u300D\uFF09\u3002
|
|
21
|
+
- summary: 1-3 \u53E5\u4E2D\u6587\u6458\u8981\uFF0C\u51DD\u7EC3\u8BE5\u573A\u666F\u4E0B\u6240\u6709\u8BB0\u5FC6\u7684\u6838\u5FC3\u4FE1\u606F\uFF0C\u4FDD\u7559\u5173\u952E\u5B9E\u4F53\u4E0E\u6570\u503C\u3002
|
|
22
|
+
- memory_ids: \u5C5E\u4E8E\u8BE5\u573A\u666F\u7684 L1 \u8BB0\u5FC6 ID \u5217\u8868\u3002
|
|
23
|
+
|
|
24
|
+
## \u805A\u7C7B\u89C4\u5219
|
|
25
|
+
- \u6BCF\u6761\u8BB0\u5FC6\u5FC5\u987B\u4E14\u53EA\u80FD\u5F52\u5C5E\u4E00\u4E2A\u573A\u666F\uFF1A\u4E0D\u53EF\u9057\u6F0F\u3001\u4E0D\u53EF\u91CD\u590D\u5F52\u5C5E\u3001\u4E0D\u53EF\u7F16\u9020\u65B0\u7684\u8BB0\u5FC6 ID\u3002
|
|
26
|
+
- \u8BED\u4E49\u76F8\u8FD1\u7684\u8BB0\u5FC6\u5C3D\u91CF\u5408\u5E76\u5230\u540C\u4E00\u573A\u666F\uFF0C\u907F\u514D\u8FC7\u5EA6\u788E\u7247\u5316\u3002
|
|
27
|
+
- \u82E5\u8BB0\u5FC6\u5DEE\u5F02\u5F88\u5927\u65E0\u6CD5\u5408\u5E76\uFF0C\u5141\u8BB8\u62C6\u5206\u4E3A\u591A\u4E2A\u573A\u666F\uFF0C\u4F46\u573A\u666F\u603B\u6570\u4E0D\u5F97\u8D85\u8FC7\u7528\u6237\u7ED9\u51FA\u7684 maxScenes \u4E0A\u9650\u3002
|
|
28
|
+
- \u82E5\u6240\u6709\u8BB0\u5FC6\u786E\u5B9E\u540C\u5C5E\u4E00\u4E2A\u4E3B\u9898\uFF0C\u53EA\u8F93\u51FA\u4E00\u4E2A\u573A\u666F\u5373\u53EF\u3002
|
|
29
|
+
- summary \u4E2D\u53EA\u80FD\u4F7F\u7528\u8BB0\u5FC6\u91CC\u771F\u5B9E\u51FA\u73B0\u7684\u4FE1\u606F\uFF0C\u4E25\u7981\u81C6\u6D4B\u3002
|
|
30
|
+
|
|
31
|
+
## \u8F93\u51FA\u683C\u5F0F
|
|
32
|
+
\u4E25\u683C\u8FD4\u56DE\u4E00\u4E2A JSON \u6570\u7EC4\uFF08\u7EAF JSON\uFF0C\u7981\u6B62\u4F7F\u7528 markdown \u4EE3\u7801\u5757\u5305\u88F9\uFF0C\u7981\u6B62\u4EFB\u4F55\u89E3\u91CA\u6587\u5B57\uFF09\uFF1A
|
|
33
|
+
[
|
|
34
|
+
{
|
|
35
|
+
"label": "\u573A\u666F\u6807\u7B7E",
|
|
36
|
+
"summary": "\u573A\u666F\u6458\u8981",
|
|
37
|
+
"memory_ids": ["mem_xxx", "mem_yyy"]
|
|
38
|
+
}
|
|
39
|
+
]`;
|
|
40
|
+
function buildSceneUserPrompt(opts) {
|
|
41
|
+
const { memoriesJson, existingScenes, maxScenes, currentTimestamp } = opts;
|
|
42
|
+
const existingBlock = existingScenes && existingScenes.trim().length > 0 ? existingScenes : "\uFF08\u6682\u65E0\u5DF2\u6709\u573A\u666F\uFF09";
|
|
43
|
+
return `\u5F53\u524D\u65F6\u95F4: ${currentTimestamp}
|
|
44
|
+
\u672C\u6B21\u6700\u591A\u751F\u6210 ${maxScenes} \u4E2A\u573A\u666F\u3002
|
|
45
|
+
|
|
46
|
+
## \u5F85\u805A\u7C7B\u7684 L1 \u8BB0\u5FC6\uFF08JSON\uFF09
|
|
47
|
+
${memoriesJson}
|
|
48
|
+
|
|
49
|
+
## \u5DF2\u6709\u573A\u666F\uFF08\u4F9B\u53C2\u8003\uFF1A\u8BED\u4E49\u91CD\u5408\u7684\u5C3D\u91CF\u5E76\u5165\u5DF2\u6709\u573A\u666F\uFF0C\u800C\u975E\u65B0\u5EFA\uFF09
|
|
50
|
+
${existingBlock}
|
|
51
|
+
|
|
52
|
+
\u8BF7\u4E25\u683C\u6309\u7CFB\u7EDF\u6307\u4EE4\u805A\u7C7B\uFF0C\u4EC5\u8FD4\u56DE JSON \u6570\u7EC4\u3002`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// src/scene-builder.ts
|
|
56
|
+
var SceneBuilder = class {
|
|
57
|
+
storage;
|
|
58
|
+
analyzerApiUrl;
|
|
59
|
+
analyzerApiKey;
|
|
60
|
+
analyzerModel;
|
|
61
|
+
dataDir;
|
|
62
|
+
embeddingService;
|
|
63
|
+
constructor(opts) {
|
|
64
|
+
this.storage = opts.storage;
|
|
65
|
+
this.analyzerApiUrl = opts.analyzerApiUrl;
|
|
66
|
+
this.analyzerApiKey = opts.analyzerApiKey;
|
|
67
|
+
this.analyzerModel = opts.analyzerModel;
|
|
68
|
+
this.dataDir = opts.dataDir;
|
|
69
|
+
this.embeddingService = opts.embeddingService ?? null;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* 构建场景:调用 LLM 聚类记忆 → 解析 JSON → 生成 Scene 对象。
|
|
73
|
+
* 失败时打印告警并返回空数组,不抛出。
|
|
74
|
+
*/
|
|
75
|
+
async buildScenes(opts) {
|
|
76
|
+
const { memories, maxScenes = 8 } = opts;
|
|
77
|
+
if (!memories || memories.length === 0) {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
if (!this.analyzerApiUrl || !this.analyzerApiKey) {
|
|
81
|
+
console.warn(
|
|
82
|
+
"[scene-builder] \u672A\u914D\u7F6E analyzerApiUrl / analyzerApiKey\uFF0C\u8DF3\u8FC7\u573A\u666F\u6784\u5EFA"
|
|
83
|
+
);
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
const cappedMemories = memories.slice(0, 20);
|
|
88
|
+
const memoriesJson = JSON.stringify(
|
|
89
|
+
cappedMemories.map((m) => ({
|
|
90
|
+
id: m.id,
|
|
91
|
+
content: m.content.substring(0, 150),
|
|
92
|
+
type: m.type,
|
|
93
|
+
tags: m.tags,
|
|
94
|
+
entities: m.entities,
|
|
95
|
+
importance: m.importance
|
|
96
|
+
}))
|
|
97
|
+
);
|
|
98
|
+
const existingScenes = await this.readExistingScenes();
|
|
99
|
+
const userPrompt = buildSceneUserPrompt({
|
|
100
|
+
memoriesJson,
|
|
101
|
+
existingScenes,
|
|
102
|
+
maxScenes,
|
|
103
|
+
currentTimestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
104
|
+
});
|
|
105
|
+
const content = await this.callLLM(SCENE_SYSTEM_PROMPT, userPrompt);
|
|
106
|
+
const rawScenes = this.parseSceneResponse(content);
|
|
107
|
+
const now = Date.now();
|
|
108
|
+
const validIds = new Set(memories.map((m) => m.id));
|
|
109
|
+
const scenes = [];
|
|
110
|
+
for (const raw of rawScenes.slice(0, maxScenes)) {
|
|
111
|
+
const label = this.asString(raw.label).trim();
|
|
112
|
+
const summary = this.asString(raw.summary).trim();
|
|
113
|
+
const memoryIds = this.asStringArray(raw.memory_ids).filter(
|
|
114
|
+
(id) => validIds.has(id)
|
|
115
|
+
);
|
|
116
|
+
if (!label || !summary || memoryIds.length === 0) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
scenes.push({
|
|
120
|
+
id: this.generateSceneId(),
|
|
121
|
+
label,
|
|
122
|
+
summary,
|
|
123
|
+
memory_ids: memoryIds,
|
|
124
|
+
file_path: null,
|
|
125
|
+
// 调用 writeSceneMarkdown 后回填
|
|
126
|
+
created_at: now,
|
|
127
|
+
updated_at: now,
|
|
128
|
+
embedding: []
|
|
129
|
+
// 先置空,下面批量计算后回填
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
for (const scene of scenes) {
|
|
133
|
+
try {
|
|
134
|
+
if (this.embeddingService) {
|
|
135
|
+
const text = `${scene.label} ${scene.summary}`;
|
|
136
|
+
scene.embedding = await this.embeddingService.embed(text);
|
|
137
|
+
}
|
|
138
|
+
} catch (err) {
|
|
139
|
+
console.warn(`[scene-builder] \u573A\u666F\u5D4C\u5165\u8BA1\u7B97\u5931\u8D25 (${scene.id}):`, err);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return scenes;
|
|
143
|
+
} catch (err) {
|
|
144
|
+
console.warn("[scene-builder] \u573A\u666F\u6784\u5EFA\u5931\u8D25:", err);
|
|
145
|
+
return [];
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* 将单个场景写入 {dataDir}/scenes/{id}.md,并在 scene.file_path 回填路径。
|
|
150
|
+
* 返回写入的文件绝对路径。
|
|
151
|
+
*/
|
|
152
|
+
async writeSceneMarkdown(scene, memories) {
|
|
153
|
+
const scenesDir = path.join(this.dataDir, "scenes");
|
|
154
|
+
if (!fs.existsSync(scenesDir)) {
|
|
155
|
+
fs.mkdirSync(scenesDir, { recursive: true });
|
|
156
|
+
}
|
|
157
|
+
const idSet = new Set(scene.memory_ids);
|
|
158
|
+
const related = memories.filter((m) => idSet.has(m.id));
|
|
159
|
+
const md = this.renderSceneMarkdown(scene, related);
|
|
160
|
+
const filePath = path.join(scenesDir, `${scene.id}.md`);
|
|
161
|
+
fs.writeFileSync(filePath, md, "utf-8");
|
|
162
|
+
scene.file_path = filePath;
|
|
163
|
+
return filePath;
|
|
164
|
+
}
|
|
165
|
+
// ===== 内部方法 =====
|
|
166
|
+
/**
|
|
167
|
+
* OpenAI 兼容的 LLM 调用(复用 analyzer.ts 的 fetch 模式)。
|
|
168
|
+
* 返回 message.content 原始字符串。
|
|
169
|
+
*/
|
|
170
|
+
async callLLM(systemPrompt, userPrompt) {
|
|
171
|
+
const response = await withLlmLock(
|
|
172
|
+
() => fetchWithRetry(this.analyzerApiUrl, {
|
|
173
|
+
method: "POST",
|
|
174
|
+
headers: {
|
|
175
|
+
"Content-Type": "application/json",
|
|
176
|
+
Authorization: `Bearer ${this.analyzerApiKey}`
|
|
177
|
+
},
|
|
178
|
+
body: JSON.stringify({
|
|
179
|
+
model: this.analyzerModel,
|
|
180
|
+
messages: [
|
|
181
|
+
{ role: "system", content: systemPrompt },
|
|
182
|
+
{ role: "user", content: userPrompt }
|
|
183
|
+
],
|
|
184
|
+
temperature: 0.3,
|
|
185
|
+
max_tokens: 4e3
|
|
186
|
+
})
|
|
187
|
+
})
|
|
188
|
+
);
|
|
189
|
+
if (!response.ok) {
|
|
190
|
+
throw new Error(
|
|
191
|
+
`Analyzer API error: ${response.status} ${response.statusText}`
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
const data = await response.json();
|
|
195
|
+
return data.choices?.[0]?.message?.content ?? "";
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* 解析 LLM 返回的场景 JSON 数组。
|
|
199
|
+
* 兼容 markdown 代码块包裹 / 前后多余文本,提取首个 [ 到末个 ] 的片段。
|
|
200
|
+
*/
|
|
201
|
+
parseSceneResponse(content) {
|
|
202
|
+
let jsonStr = content.trim();
|
|
203
|
+
if (!jsonStr) {
|
|
204
|
+
console.warn("[scene-builder] LLM \u8FD4\u56DE\u7A7A\u5185\u5BB9\uFF0C\u8DF3\u8FC7\u573A\u666F\u89E3\u6790");
|
|
205
|
+
return [];
|
|
206
|
+
}
|
|
207
|
+
const fenceMatch = jsonStr.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
208
|
+
if (fenceMatch) {
|
|
209
|
+
jsonStr = fenceMatch[1].trim();
|
|
210
|
+
}
|
|
211
|
+
const start = jsonStr.indexOf("[");
|
|
212
|
+
const end = jsonStr.lastIndexOf("]");
|
|
213
|
+
if (start >= 0 && end > start) {
|
|
214
|
+
jsonStr = jsonStr.substring(start, end + 1);
|
|
215
|
+
}
|
|
216
|
+
let parsed;
|
|
217
|
+
try {
|
|
218
|
+
parsed = JSON.parse(jsonStr);
|
|
219
|
+
} catch (err) {
|
|
220
|
+
console.warn("[scene-builder] JSON \u89E3\u6790\u5931\u8D25\uFF0C\u8FD4\u56DE\u7A7A\u6570\u7EC4:", err.message);
|
|
221
|
+
return [];
|
|
222
|
+
}
|
|
223
|
+
if (!Array.isArray(parsed)) {
|
|
224
|
+
console.warn("[scene-builder] \u573A\u666F\u54CD\u5E94\u4E0D\u662F JSON \u6570\u7EC4");
|
|
225
|
+
return [];
|
|
226
|
+
}
|
|
227
|
+
return parsed;
|
|
228
|
+
}
|
|
229
|
+
/** 读取已有场景摘要,供 LLM 参考去重;失败时静默返回空串。 */
|
|
230
|
+
async readExistingScenes() {
|
|
231
|
+
try {
|
|
232
|
+
if (this.storage && typeof this.storage.getScenes === "function") {
|
|
233
|
+
const scenes = await this.storage.getScenes();
|
|
234
|
+
if (Array.isArray(scenes) && scenes.length > 0) {
|
|
235
|
+
return JSON.stringify(
|
|
236
|
+
scenes.map((s) => ({ id: s.id, label: s.label, summary: s.summary })),
|
|
237
|
+
null,
|
|
238
|
+
2
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
} catch (err) {
|
|
243
|
+
console.warn("[scene-builder] \u8BFB\u53D6\u5DF2\u6709\u573A\u666F\u5931\u8D25\uFF0C\u5FFD\u7565:", err);
|
|
244
|
+
}
|
|
245
|
+
return "";
|
|
246
|
+
}
|
|
247
|
+
/** 渲染场景为可读 Markdown */
|
|
248
|
+
renderSceneMarkdown(scene, memories) {
|
|
249
|
+
const lines = [];
|
|
250
|
+
lines.push(`# ${scene.label}`);
|
|
251
|
+
lines.push("");
|
|
252
|
+
lines.push(`> \u573A\u666F ID: \`${scene.id}\``);
|
|
253
|
+
lines.push(`> \u521B\u5EFA\u65F6\u95F4: ${new Date(scene.created_at).toISOString()}`);
|
|
254
|
+
lines.push("");
|
|
255
|
+
lines.push("## \u6458\u8981");
|
|
256
|
+
lines.push("");
|
|
257
|
+
lines.push(scene.summary);
|
|
258
|
+
lines.push("");
|
|
259
|
+
lines.push("## \u5173\u8054\u8BB0\u5FC6");
|
|
260
|
+
lines.push("");
|
|
261
|
+
if (memories.length === 0) {
|
|
262
|
+
lines.push("- \uFF08\u65E0\u5173\u8054\u8BB0\u5FC6\uFF09");
|
|
263
|
+
} else {
|
|
264
|
+
for (const m of memories) {
|
|
265
|
+
lines.push(`- [${m.id}] ${this.escapeMd(m.content)}`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
lines.push("");
|
|
269
|
+
lines.push("## \u5143\u4FE1\u606F");
|
|
270
|
+
lines.push("");
|
|
271
|
+
lines.push(`- \u8BB0\u5FC6\u6570: ${memories.length}`);
|
|
272
|
+
lines.push(`- \u66F4\u65B0\u65F6\u95F4: ${new Date(scene.updated_at).toISOString()}`);
|
|
273
|
+
lines.push("");
|
|
274
|
+
return lines.join("\n");
|
|
275
|
+
}
|
|
276
|
+
/** 生成场景 ID: scene_{timestamp}_{randomId} */
|
|
277
|
+
generateSceneId() {
|
|
278
|
+
const randomId = crypto.randomUUID().replace(/-/g, "").slice(0, 12);
|
|
279
|
+
return `scene_${Date.now()}_${randomId}`;
|
|
280
|
+
}
|
|
281
|
+
asString(v) {
|
|
282
|
+
return typeof v === "string" ? v : v == null ? "" : String(v);
|
|
283
|
+
}
|
|
284
|
+
asStringArray(v) {
|
|
285
|
+
if (!Array.isArray(v)) return [];
|
|
286
|
+
return v.map((x) => typeof x === "string" ? x : String(x));
|
|
287
|
+
}
|
|
288
|
+
/** 转义 Markdown 列表/标题敏感字符,避免内容破坏文档结构 */
|
|
289
|
+
escapeMd(s) {
|
|
290
|
+
return s.replace(/\r?\n/g, " ").trim();
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
export {
|
|
294
|
+
SceneBuilder
|
|
295
|
+
};
|
|
296
|
+
//# sourceMappingURL=scene-builder-TLVG23BJ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/scene-builder.ts","../src/prompts/scene-prompt.ts"],"sourcesContent":["/**\n * L2 场景构建器 — 将相关 L1 记忆聚类为主题场景块(L2)\n *\n * 灵感来源: TencentDB Agent Memory 的 scene-extraction.ts\n *\n * 设计要点:\n * - LLM 通读相关 L1 记忆 → 产出 { label, summary, memory_ids } 数组\n * - 每个场景写为一个可读的 Markdown 文件(dataDir/scenes/{id}.md)\n * - LLM 调用完全复用 analyzer.ts 的 OpenAI 兼容 fetch 模式\n * (withLlmLock + fetchWithRetry,Bearer 鉴权,temperature 0.3)\n * - 失败时降级为空数组,绝不抛出阻断主流程\n *\n * 记忆层级: L0 原始日志 → L1 原子事实 → **L2 场景块(本模块)** → L3 用户画像\n */\n\nimport * as fs from \"fs\";\nimport * as path from \"path\";\nimport * as crypto from \"crypto\";\nimport type { Scene, Memory } from \"./types.js\";\nimport { withLlmLock, fetchWithRetry } from \"./llm-lock.js\";\nimport {\n SCENE_SYSTEM_PROMPT,\n buildSceneUserPrompt,\n} from \"./prompts/scene-prompt.js\";\n\n/** SceneBuilder 构造参数 */\nexport interface SceneBuilderOptions {\n /** 存储层(任意类型,仅按需调用其可选的 getScenes 方法读取已有场景) */\n storage: any;\n /** OpenAI 兼容的 Analyzer API 地址 */\n analyzerApiUrl: string;\n /** Analyzer API 密钥 */\n analyzerApiKey: string;\n /** 使用的模型名,如 \"glm-4-flash\" */\n analyzerModel: string;\n /** 数据目录,场景 Markdown 写入 {dataDir}/scenes/{id}.md */\n dataDir: string;\n /** 嵌入服务(EmbeddingService),用于计算场景向量;缺失则降级为空向量 */\n embeddingService?: any;\n}\n\n/** LLM 返回的单个场景原始结构 */\ninterface RawScene {\n label?: unknown;\n summary?: unknown;\n memory_ids?: unknown;\n}\n\n/** buildScenes 的入参 */\nexport interface BuildScenesOptions {\n /** 待聚类的 L1 记忆列表 */\n memories: Memory[];\n /** 本次允许生成的最大场景数,默认 8 */\n maxScenes?: number;\n}\n\n/**\n * 场景构建器:调用 LLM 把相关 L1 记忆聚类为 L2 场景块,并落盘为 Markdown。\n */\nexport class SceneBuilder {\n private storage: any;\n private analyzerApiUrl: string;\n private analyzerApiKey: string;\n private analyzerModel: string;\n private dataDir: string;\n private embeddingService: any;\n\n constructor(opts: SceneBuilderOptions) {\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 this.embeddingService = opts.embeddingService ?? null;\n }\n\n /**\n * 构建场景:调用 LLM 聚类记忆 → 解析 JSON → 生成 Scene 对象。\n * 失败时打印告警并返回空数组,不抛出。\n */\n async buildScenes(opts: BuildScenesOptions): Promise<Scene[]> {\n const { memories, maxScenes = 8 } = opts;\n\n if (!memories || memories.length === 0) {\n return [];\n }\n\n // 无 LLM 配置时直接放弃(场景归纳强依赖 LLM,不提供规则降级)\n if (!this.analyzerApiUrl || !this.analyzerApiKey) {\n console.warn(\n \"[scene-builder] 未配置 analyzerApiUrl / analyzerApiKey,跳过场景构建\"\n );\n return [];\n }\n\n try {\n // 1. 序列化记忆为精简 JSON(剔除体积庞大的 embedding 等字段,控制 token)\n // 限制最多 20 条记忆,每条 content 截断到 150 字符,防止 LLM token 溢出\n const cappedMemories = memories.slice(0, 20);\n const memoriesJson = JSON.stringify(\n cappedMemories.map((m) => ({\n id: m.id,\n content: m.content.substring(0, 150),\n type: m.type,\n tags: m.tags,\n entities: m.entities,\n importance: m.importance,\n }))\n );\n\n // 2. 读取已有场景(若 storage 暴露了 getScenes),供 LLM 参考避免重复\n const existingScenes = await this.readExistingScenes();\n\n // 3. 构造 user prompt 并调用 LLM\n const userPrompt = buildSceneUserPrompt({\n memoriesJson,\n existingScenes,\n maxScenes,\n currentTimestamp: new Date().toISOString(),\n });\n\n const content = await this.callLLM(SCENE_SYSTEM_PROMPT, userPrompt);\n\n // 4. 解析 JSON 数组\n const rawScenes = this.parseSceneResponse(content);\n\n // 5. 组装 Scene 对象\n const now = Date.now();\n const validIds = new Set(memories.map((m) => m.id));\n const scenes: Scene[] = [];\n\n for (const raw of rawScenes.slice(0, maxScenes)) {\n const label = this.asString(raw.label).trim();\n const summary = this.asString(raw.summary).trim();\n const memoryIds = this.asStringArray(raw.memory_ids).filter((id) =>\n validIds.has(id)\n );\n\n // 必要字段缺失或无有效记忆归属则丢弃\n if (!label || !summary || memoryIds.length === 0) {\n continue;\n }\n\n scenes.push({\n id: this.generateSceneId(),\n label,\n summary,\n memory_ids: memoryIds,\n file_path: null, // 调用 writeSceneMarkdown 后回填\n created_at: now,\n updated_at: now,\n embedding: [], // 先置空,下面批量计算后回填\n });\n }\n\n // 批量计算场景嵌入向量(失败不阻断流程,保持 [])\n for (const scene of scenes) {\n try {\n if (this.embeddingService) {\n const text = `${scene.label} ${scene.summary}`;\n scene.embedding = await this.embeddingService.embed(text);\n }\n } catch (err) {\n console.warn(`[scene-builder] 场景嵌入计算失败 (${scene.id}):`, err);\n }\n }\n\n return scenes;\n } catch (err) {\n console.warn(\"[scene-builder] 场景构建失败:\", err);\n return [];\n }\n }\n\n /**\n * 将单个场景写入 {dataDir}/scenes/{id}.md,并在 scene.file_path 回填路径。\n * 返回写入的文件绝对路径。\n */\n async writeSceneMarkdown(scene: Scene, memories: Memory[]): Promise<string> {\n const scenesDir = path.join(this.dataDir, \"scenes\");\n if (!fs.existsSync(scenesDir)) {\n fs.mkdirSync(scenesDir, { recursive: true });\n }\n\n // 按 memory_ids 过滤出归属本场景的记忆\n const idSet = new Set(scene.memory_ids);\n const related = memories.filter((m) => idSet.has(m.id));\n\n const md = this.renderSceneMarkdown(scene, related);\n const filePath = path.join(scenesDir, `${scene.id}.md`);\n fs.writeFileSync(filePath, md, \"utf-8\");\n\n // 回填路径\n scene.file_path = filePath;\n\n return filePath;\n }\n\n // ===== 内部方法 =====\n\n /**\n * OpenAI 兼容的 LLM 调用(复用 analyzer.ts 的 fetch 模式)。\n * 返回 message.content 原始字符串。\n */\n private async callLLM(systemPrompt: string, userPrompt: string): Promise<string> {\n const response = await withLlmLock(() =>\n fetchWithRetry(this.analyzerApiUrl, {\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 max_tokens: 4000,\n }),\n })\n );\n\n if (!response.ok) {\n throw new Error(\n `Analyzer API error: ${response.status} ${response.statusText}`\n );\n }\n\n const data = (await response.json()) as any;\n return data.choices?.[0]?.message?.content ?? \"\";\n }\n\n /**\n * 解析 LLM 返回的场景 JSON 数组。\n * 兼容 markdown 代码块包裹 / 前后多余文本,提取首个 [ 到末个 ] 的片段。\n */\n private parseSceneResponse(content: string): RawScene[] {\n let jsonStr = content.trim();\n\n // 空内容直接返回空数组(不抛异常)\n if (!jsonStr) {\n console.warn(\"[scene-builder] LLM 返回空内容,跳过场景解析\");\n return [];\n }\n\n // 剥离 ```json ... ``` 代码块\n const fenceMatch = jsonStr.match(/```(?:json)?\\s*([\\s\\S]*?)```/);\n if (fenceMatch) {\n jsonStr = fenceMatch[1].trim();\n }\n\n // 截取首个 [ 到末个 ]\n const start = jsonStr.indexOf(\"[\");\n const end = jsonStr.lastIndexOf(\"]\");\n if (start >= 0 && end > start) {\n jsonStr = jsonStr.substring(start, end + 1);\n }\n\n // JSON 解析防护\n let parsed: any;\n try {\n parsed = JSON.parse(jsonStr);\n } catch (err) {\n console.warn(\"[scene-builder] JSON 解析失败,返回空数组:\", (err as Error).message);\n return [];\n }\n if (!Array.isArray(parsed)) {\n console.warn(\"[scene-builder] 场景响应不是 JSON 数组\");\n return [];\n }\n return parsed as RawScene[];\n }\n\n /** 读取已有场景摘要,供 LLM 参考去重;失败时静默返回空串。 */\n private async readExistingScenes(): Promise<string> {\n try {\n if (\n this.storage &&\n typeof this.storage.getScenes === \"function\"\n ) {\n const scenes = await this.storage.getScenes();\n if (Array.isArray(scenes) && scenes.length > 0) {\n return JSON.stringify(\n scenes.map((s: any) => ({ id: s.id, label: s.label, summary: s.summary })),\n null,\n 2\n );\n }\n }\n } catch (err) {\n console.warn(\"[scene-builder] 读取已有场景失败,忽略:\", err);\n }\n return \"\";\n }\n\n /** 渲染场景为可读 Markdown */\n private renderSceneMarkdown(scene: Scene, memories: Memory[]): string {\n const lines: string[] = [];\n lines.push(`# ${scene.label}`);\n lines.push(\"\");\n lines.push(`> 场景 ID: \\`${scene.id}\\``);\n lines.push(`> 创建时间: ${new Date(scene.created_at).toISOString()}`);\n lines.push(\"\");\n lines.push(\"## 摘要\");\n lines.push(\"\");\n lines.push(scene.summary);\n lines.push(\"\");\n lines.push(\"## 关联记忆\");\n lines.push(\"\");\n\n if (memories.length === 0) {\n lines.push(\"- (无关联记忆)\");\n } else {\n for (const m of memories) {\n // 形如: - [{memory.id}] {memory.content}\n lines.push(`- [${m.id}] ${this.escapeMd(m.content)}`);\n }\n }\n\n lines.push(\"\");\n lines.push(\"## 元信息\");\n lines.push(\"\");\n lines.push(`- 记忆数: ${memories.length}`);\n lines.push(`- 更新时间: ${new Date(scene.updated_at).toISOString()}`);\n lines.push(\"\");\n\n return lines.join(\"\\n\");\n }\n\n /** 生成场景 ID: scene_{timestamp}_{randomId} */\n private generateSceneId(): string {\n const randomId = crypto.randomUUID().replace(/-/g, \"\").slice(0, 12);\n return `scene_${Date.now()}_${randomId}`;\n }\n\n private asString(v: unknown): string {\n return typeof v === \"string\" ? v : v == null ? \"\" : String(v);\n }\n\n private asStringArray(v: unknown): string[] {\n if (!Array.isArray(v)) return [];\n return v.map((x) => (typeof x === \"string\" ? x : String(x)));\n }\n\n /** 转义 Markdown 列表/标题敏感字符,避免内容破坏文档结构 */\n private escapeMd(s: string): string {\n return s.replace(/\\r?\\n/g, \" \").trim();\n }\n}\n","/**\n * L2 场景提取 Prompt — 指导 LLM 将相关 L1 记忆聚类为主题场景(L2)\n *\n * 灵感来源: TencentDB Agent Memory 的 scene-extraction.ts\n * LLM 通读相关记忆,生成 label + summary,输出可写入 Markdown 的场景块。\n *\n * 记忆层级:\n * L0 = 原始对话/日志\n * L1 = 原子事实(memories 表中的单条记忆)\n * L2 = 场景块(本模块的产物,由若干 L1 聚类而成)\n * L3 = 用户画像\n */\n\n/**\n * 场景归纳器系统提示词。\n * 角色「记忆场景归纳器」:将一组相关 L1 记忆聚类为若干主题场景,\n * 输出 JSON 数组 [{ label, summary, memory_ids }]。\n */\nexport const SCENE_SYSTEM_PROMPT: string = `你是「记忆场景归纳器」,职责是将一组相关联的原子记忆(L1)聚类为若干主题场景(L2),并产出可读的场景摘要。\n\n## 任务\n1. 通读给定的全部 L1 记忆。\n2. 按主题 / 语义相关性将它们聚类为若干「场景」;同一主题的记忆归入同一场景。\n3. 为每个场景产出:\n - label: 简短的主题标签(2-12 个汉字,名词短语,如「数据库运维偏好」「前端技术栈」「安全规则」)。\n - summary: 1-3 句中文摘要,凝练该场景下所有记忆的核心信息,保留关键实体与数值。\n - memory_ids: 属于该场景的 L1 记忆 ID 列表。\n\n## 聚类规则\n- 每条记忆必须且只能归属一个场景:不可遗漏、不可重复归属、不可编造新的记忆 ID。\n- 语义相近的记忆尽量合并到同一场景,避免过度碎片化。\n- 若记忆差异很大无法合并,允许拆分为多个场景,但场景总数不得超过用户给出的 maxScenes 上限。\n- 若所有记忆确实同属一个主题,只输出一个场景即可。\n- summary 中只能使用记忆里真实出现的信息,严禁臆测。\n\n## 输出格式\n严格返回一个 JSON 数组(纯 JSON,禁止使用 markdown 代码块包裹,禁止任何解释文字):\n[\n {\n \"label\": \"场景标签\",\n \"summary\": \"场景摘要\",\n \"memory_ids\": [\"mem_xxx\", \"mem_yyy\"]\n }\n]`;\n\n/** buildSceneUserPrompt 的入参 */\nexport interface BuildSceneUserPromptOptions {\n /** 待聚类的 L1 记忆(已序列化为 JSON 字符串) */\n memoriesJson: string;\n /** 已有场景的描述(供 LLM 参考,避免重复 / 可合并进去);为空表示暂无 */\n existingScenes: string;\n /** 本次允许生成的最大场景数 */\n maxScenes: number;\n /** 当前时间戳(人类可读字符串),用于上下文感知 */\n currentTimestamp: string;\n}\n\n/**\n * 构造场景提取的用户提示词。\n * 将待聚类记忆、已有场景、上限与当前时间组装成结构化 user prompt。\n */\nexport function buildSceneUserPrompt(opts: BuildSceneUserPromptOptions): string {\n const { memoriesJson, existingScenes, maxScenes, currentTimestamp } = opts;\n const existingBlock =\n existingScenes && existingScenes.trim().length > 0\n ? existingScenes\n : \"(暂无已有场景)\";\n\n return `当前时间: ${currentTimestamp}\n本次最多生成 ${maxScenes} 个场景。\n\n## 待聚类的 L1 记忆(JSON)\n${memoriesJson}\n\n## 已有场景(供参考:语义重合的尽量并入已有场景,而非新建)\n${existingBlock}\n\n请严格按系统指令聚类,仅返回 JSON 数组。`;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "./chunk-2H7UOFLK.js";
|
|
3
|
+
|
|
4
|
+
// src/scene-index.ts
|
|
5
|
+
import { promises as fs } from "fs";
|
|
6
|
+
import path from "path";
|
|
7
|
+
var SCENE_NAVIGATION_MARKER = "---\n## \u{1F5FA}\uFE0F \u573A\u666F\u5BFC\u822A";
|
|
8
|
+
var SceneIndex = class _SceneIndex {
|
|
9
|
+
constructor(dataDir) {
|
|
10
|
+
this.dataDir = dataDir;
|
|
11
|
+
this.indexPath = path.join(dataDir, ".metadata", "scene_index.json");
|
|
12
|
+
this.scenesDir = path.join(dataDir, "scenes");
|
|
13
|
+
}
|
|
14
|
+
dataDir;
|
|
15
|
+
/** 索引 JSON 文件的绝对路径(dataDir/.metadata/scene_index.json) */
|
|
16
|
+
indexPath;
|
|
17
|
+
/** 内存中的索引条目 */
|
|
18
|
+
entries = [];
|
|
19
|
+
/** 场景 Markdown 所在目录(dataDir/scenes) */
|
|
20
|
+
scenesDir;
|
|
21
|
+
/**
|
|
22
|
+
* 初始化:从磁盘加载索引。
|
|
23
|
+
* 文件不存在或解析失败时静默降级为空数组,不抛出。
|
|
24
|
+
*/
|
|
25
|
+
async init() {
|
|
26
|
+
try {
|
|
27
|
+
const raw = await fs.readFile(this.indexPath, "utf-8");
|
|
28
|
+
const parsed = JSON.parse(raw);
|
|
29
|
+
this.entries = Array.isArray(parsed) ? parsed : [];
|
|
30
|
+
this.entries = this.entries.filter(
|
|
31
|
+
(e) => e && typeof e.scene_id === "string"
|
|
32
|
+
);
|
|
33
|
+
} catch {
|
|
34
|
+
this.entries = [];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* 添加/更新场景到索引。
|
|
39
|
+
* 已存在相同 scene_id 的条目则就地更新(保留已有 heat),否则新增。
|
|
40
|
+
* 更新后会持久化到磁盘。
|
|
41
|
+
*/
|
|
42
|
+
async upsert(entry) {
|
|
43
|
+
const existing = this.entries.find((e) => e.scene_id === entry.scene_id);
|
|
44
|
+
if (existing) {
|
|
45
|
+
existing.label = entry.label;
|
|
46
|
+
existing.summary = entry.summary;
|
|
47
|
+
existing.filename = entry.filename;
|
|
48
|
+
existing.updated = entry.updated;
|
|
49
|
+
if (entry.created) existing.created = entry.created;
|
|
50
|
+
} else {
|
|
51
|
+
this.entries.push({
|
|
52
|
+
scene_id: entry.scene_id,
|
|
53
|
+
filename: entry.filename,
|
|
54
|
+
label: entry.label,
|
|
55
|
+
summary: entry.summary,
|
|
56
|
+
created: entry.created,
|
|
57
|
+
updated: entry.updated,
|
|
58
|
+
heat: entry.heat ?? 0
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
await this.persist();
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* 增加场景热度(检索命中时调用)。命中则 heat++ 并持久化。
|
|
65
|
+
* @returns 是否成功命中(true=存在并已自增)
|
|
66
|
+
*/
|
|
67
|
+
async incrementHeat(sceneId) {
|
|
68
|
+
const entry = this.entries.find((e) => e.scene_id === sceneId);
|
|
69
|
+
if (!entry) return false;
|
|
70
|
+
entry.heat++;
|
|
71
|
+
await this.persist();
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* 从索引中移除某场景(场景被删除时调用)。
|
|
76
|
+
* @returns 是否存在并已移除
|
|
77
|
+
*/
|
|
78
|
+
async remove(sceneId) {
|
|
79
|
+
const before = this.entries.length;
|
|
80
|
+
this.entries = this.entries.filter((e) => e.scene_id !== sceneId);
|
|
81
|
+
const removed = this.entries.length < before;
|
|
82
|
+
if (removed) await this.persist();
|
|
83
|
+
return removed;
|
|
84
|
+
}
|
|
85
|
+
/** 按 scene_id 查询单条索引;不存在返回 undefined。 */
|
|
86
|
+
getBySceneId(sceneId) {
|
|
87
|
+
return this.entries.find((e) => e.scene_id === sceneId);
|
|
88
|
+
}
|
|
89
|
+
/** 索引中的场景总数。 */
|
|
90
|
+
get size() {
|
|
91
|
+
return this.entries.length;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* 获取全部场景索引(按热度降序,热度相同则按更新时间倒序)。
|
|
95
|
+
* 返回的是排序后的副本,外部修改不会影响内部状态。
|
|
96
|
+
*/
|
|
97
|
+
getAll() {
|
|
98
|
+
return [...this.entries].sort((a, b) => {
|
|
99
|
+
if (b.heat !== a.heat) return b.heat - a.heat;
|
|
100
|
+
return (b.updated || "").localeCompare(a.updated || "");
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* 获取某场景的 Markdown 绝对路径(dataDir/scenes/{filename})。
|
|
105
|
+
* 供 agent 直接 read_file。
|
|
106
|
+
*/
|
|
107
|
+
getScenePath(entry) {
|
|
108
|
+
return path.join(this.scenesDir, entry.filename);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* 生成场景导航 Markdown(可追加到 persona.md 末尾或注入上下文)。
|
|
112
|
+
* 按热度降序排列,热度高者标注火焰图标;每条包含可直接读取的绝对路径。
|
|
113
|
+
* 索引为空时返回空字符串。
|
|
114
|
+
*/
|
|
115
|
+
generateNavigation() {
|
|
116
|
+
const sorted = this.getAll();
|
|
117
|
+
if (sorted.length === 0) return "";
|
|
118
|
+
const blocks = sorted.map((e) => {
|
|
119
|
+
const heat = e.heat >= 100 ? " \u{1F525}\u{1F525}" : e.heat >= 10 ? " \u{1F525}" : "";
|
|
120
|
+
const scenePath = this.getScenePath(e);
|
|
121
|
+
return `### ${e.label}${heat}
|
|
122
|
+
Path: ${scenePath}
|
|
123
|
+
Summary: ${e.summary}`;
|
|
124
|
+
});
|
|
125
|
+
return [
|
|
126
|
+
SCENE_NAVIGATION_MARKER,
|
|
127
|
+
" (Scene Index)",
|
|
128
|
+
"*\u4EE5\u4E0B\u662F\u5F53\u524D\u573A\u666F\u8BB0\u5FC6\u7684\u7D22\u5F15\uFF0C\u53EF\u6839\u636E\u9700\u8981\u8BFB\u53D6\u8BE6\u7EC6\u5185\u5BB9\u3002*",
|
|
129
|
+
"",
|
|
130
|
+
blocks.join("\n\n"),
|
|
131
|
+
"",
|
|
132
|
+
"\u{1F4CC} \u4F7F\u7528\u8BF4\u660E\uFF1APath \u662F\u573A\u666F\u6587\u4EF6\u8DEF\u5F84\uFF0C\u53EF\u76F4\u63A5\u8BFB\u53D6\u5B8C\u6574\u5185\u5BB9\uFF1B\u70ED\u5EA6\u8D8A\u9AD8\u8D8A\u91CD\u8981\u3002"
|
|
133
|
+
].join("\n");
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* 从 persona 内容中剥离旧的场景导航段(含其起始标记到字符串末尾)。
|
|
137
|
+
* 用于在重新注入导航前先清理旧内容,避免重复堆积。
|
|
138
|
+
*/
|
|
139
|
+
static stripNavigation(personaContent) {
|
|
140
|
+
const idx = personaContent.indexOf(SCENE_NAVIGATION_MARKER);
|
|
141
|
+
if (idx === -1) return personaContent;
|
|
142
|
+
return personaContent.slice(0, idx).trimEnd();
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* 刷新 persona 内容中的场景导航段:先剥离旧段,再追加当前导航。
|
|
146
|
+
* 索引为空且原内容无导航段时,原样返回(不附加空段)。
|
|
147
|
+
*/
|
|
148
|
+
refreshNavigation(personaContent) {
|
|
149
|
+
const stripped = _SceneIndex.stripNavigation(personaContent);
|
|
150
|
+
const nav = this.generateNavigation();
|
|
151
|
+
if (!nav) return stripped;
|
|
152
|
+
return `${stripped}
|
|
153
|
+
|
|
154
|
+
${nav}
|
|
155
|
+
`;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* 持久化到磁盘(写临时文件 + 原子 rename)。
|
|
159
|
+
* 失败时打印告警,不抛出(避免阻断主流程)。
|
|
160
|
+
*/
|
|
161
|
+
async persist() {
|
|
162
|
+
try {
|
|
163
|
+
const dir = path.dirname(this.indexPath);
|
|
164
|
+
await fs.mkdir(dir, { recursive: true });
|
|
165
|
+
const data = JSON.stringify(this.entries, null, 2);
|
|
166
|
+
const tmpPath = `${this.indexPath}.tmp`;
|
|
167
|
+
await fs.writeFile(tmpPath, data, "utf-8");
|
|
168
|
+
await fs.rename(tmpPath, this.indexPath);
|
|
169
|
+
} catch (err) {
|
|
170
|
+
console.warn("[scene-index] \u6301\u4E45\u5316\u7D22\u5F15\u5931\u8D25\uFF0C\u5FFD\u7565:", err);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
export {
|
|
175
|
+
SCENE_NAVIGATION_MARKER,
|
|
176
|
+
SceneIndex
|
|
177
|
+
};
|
|
178
|
+
//# sourceMappingURL=scene-index-SU6GMVDL.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/scene-index.ts"],"sourcesContent":["/**\n * L2 场景索引 — 热度追踪 + JSON 持久化 + 场景导航 Markdown 生成\n *\n * 灵感来源: TencentDB Agent Memory 的 scene-index.ts + scene-navigation.ts + scene-format.ts\n *\n * 设计要点:\n * - 维护一个 JSON 索引文件 (dataDir/.metadata/scene_index.json),记录每个场景的\n * 元信息(label / summary / filename / scene_id)以及被检索命中的热度(heat)。\n * - 场景被检索命中时调用 incrementHeat(sceneId),heat++;热度越高越重要。\n * - generateNavigation() 产出一段 Markdown(含场景文件的绝对路径),可追加到\n * persona.md 末尾或注入 agent 上下文,使 agent 能按需 read_file 读取场景详情。\n * - 持久化采用「写临时文件 + 原子 rename」,避免半写损坏。\n * - 所有方法均吞掉自身的非致命错误,绝不阻断主流程(与 scene-builder 一致)。\n *\n * 记忆层级: L0 原始日志 → L1 原子事实 → L2 场景块(scene-builder 产出)→ L3 用户画像\n * ↑ 本模块为 L2 场景块的索引/导航层\n */\n\nimport { promises as fs } from \"node:fs\";\nimport path from \"node:path\";\n\n/** 单条场景索引记录 */\nexport interface SceneIndexEntry {\n /** 场景 ID(对应 Scene.id,如 scene_1700000000000_abcdef123456) */\n scene_id: string;\n /** 场景 Markdown 文件名(如 scene_xxx.md,不含目录) */\n filename: string;\n /** 场景标题 */\n label: string;\n /** 场景一句话摘要 */\n summary: string;\n /** 被检索命中的累计次数(热度) */\n heat: number;\n /** 创建时间(ISO 字符串) */\n created: string;\n /** 最近更新时间(ISO 字符串) */\n updated: string;\n}\n\n/** upsert 入参:除 heat 外其余必填(heat 可选,默认 0) */\nexport type SceneIndexUpsertInput = Omit<SceneIndexEntry, \"heat\"> & {\n heat?: number;\n};\n\n/** 场景导航段在 Markdown 中的起始标记(stripNavigation 据此剥离旧段) */\nexport const SCENE_NAVIGATION_MARKER = \"---\\n## 🗺️ 场景导航\";\n\n/**\n * 场景索引:负责场景元信息的索引维护、热度追踪与导航 Markdown 生成。\n */\nexport class SceneIndex {\n /** 索引 JSON 文件的绝对路径(dataDir/.metadata/scene_index.json) */\n private indexPath: string;\n /** 内存中的索引条目 */\n private entries: SceneIndexEntry[] = [];\n /** 场景 Markdown 所在目录(dataDir/scenes) */\n private scenesDir: string;\n\n constructor(private dataDir: string) {\n this.indexPath = path.join(dataDir, \".metadata\", \"scene_index.json\");\n this.scenesDir = path.join(dataDir, \"scenes\");\n }\n\n /**\n * 初始化:从磁盘加载索引。\n * 文件不存在或解析失败时静默降级为空数组,不抛出。\n */\n async init(): Promise<void> {\n try {\n const raw = await fs.readFile(this.indexPath, \"utf-8\");\n const parsed = JSON.parse(raw);\n this.entries = Array.isArray(parsed) ? (parsed as SceneIndexEntry[]) : [];\n // 防御:剔除明显不合法的条目\n this.entries = this.entries.filter(\n (e) => e && typeof e.scene_id === \"string\"\n );\n } catch {\n this.entries = [];\n }\n }\n\n /**\n * 添加/更新场景到索引。\n * 已存在相同 scene_id 的条目则就地更新(保留已有 heat),否则新增。\n * 更新后会持久化到磁盘。\n */\n async upsert(entry: SceneIndexUpsertInput): Promise<void> {\n const existing = this.entries.find((e) => e.scene_id === entry.scene_id);\n if (existing) {\n existing.label = entry.label;\n existing.summary = entry.summary;\n existing.filename = entry.filename;\n existing.updated = entry.updated;\n // created 保留首次值,除非显式传入\n if (entry.created) existing.created = entry.created;\n } else {\n this.entries.push({\n scene_id: entry.scene_id,\n filename: entry.filename,\n label: entry.label,\n summary: entry.summary,\n created: entry.created,\n updated: entry.updated,\n heat: entry.heat ?? 0,\n });\n }\n await this.persist();\n }\n\n /**\n * 增加场景热度(检索命中时调用)。命中则 heat++ 并持久化。\n * @returns 是否成功命中(true=存在并已自增)\n */\n async incrementHeat(sceneId: string): Promise<boolean> {\n const entry = this.entries.find((e) => e.scene_id === sceneId);\n if (!entry) return false;\n entry.heat++;\n await this.persist();\n return true;\n }\n\n /**\n * 从索引中移除某场景(场景被删除时调用)。\n * @returns 是否存在并已移除\n */\n async remove(sceneId: string): Promise<boolean> {\n const before = this.entries.length;\n this.entries = this.entries.filter((e) => e.scene_id !== sceneId);\n const removed = this.entries.length < before;\n if (removed) await this.persist();\n return removed;\n }\n\n /** 按 scene_id 查询单条索引;不存在返回 undefined。 */\n getBySceneId(sceneId: string): SceneIndexEntry | undefined {\n return this.entries.find((e) => e.scene_id === sceneId);\n }\n\n /** 索引中的场景总数。 */\n get size(): number {\n return this.entries.length;\n }\n\n /**\n * 获取全部场景索引(按热度降序,热度相同则按更新时间倒序)。\n * 返回的是排序后的副本,外部修改不会影响内部状态。\n */\n getAll(): SceneIndexEntry[] {\n return [...this.entries].sort((a, b) => {\n if (b.heat !== a.heat) return b.heat - a.heat;\n // heat 相同时,updated 更新的靠前\n return (b.updated || \"\").localeCompare(a.updated || \"\");\n });\n }\n\n /**\n * 获取某场景的 Markdown 绝对路径(dataDir/scenes/{filename})。\n * 供 agent 直接 read_file。\n */\n getScenePath(entry: SceneIndexEntry): string {\n return path.join(this.scenesDir, entry.filename);\n }\n\n /**\n * 生成场景导航 Markdown(可追加到 persona.md 末尾或注入上下文)。\n * 按热度降序排列,热度高者标注火焰图标;每条包含可直接读取的绝对路径。\n * 索引为空时返回空字符串。\n */\n generateNavigation(): string {\n const sorted = this.getAll();\n if (sorted.length === 0) return \"\";\n\n const blocks = sorted.map((e) => {\n const heat = e.heat >= 100 ? \" 🔥🔥\" : e.heat >= 10 ? \" 🔥\" : \"\";\n const scenePath = this.getScenePath(e);\n return `### ${e.label}${heat}\\nPath: ${scenePath}\\nSummary: ${e.summary}`;\n });\n\n return [\n SCENE_NAVIGATION_MARKER,\n \" (Scene Index)\",\n \"*以下是当前场景记忆的索引,可根据需要读取详细内容。*\",\n \"\",\n blocks.join(\"\\n\\n\"),\n \"\",\n \"📌 使用说明:Path 是场景文件路径,可直接读取完整内容;热度越高越重要。\",\n ].join(\"\\n\");\n }\n\n /**\n * 从 persona 内容中剥离旧的场景导航段(含其起始标记到字符串末尾)。\n * 用于在重新注入导航前先清理旧内容,避免重复堆积。\n */\n static stripNavigation(personaContent: string): string {\n const idx = personaContent.indexOf(SCENE_NAVIGATION_MARKER);\n if (idx === -1) return personaContent;\n return personaContent.slice(0, idx).trimEnd();\n }\n\n /**\n * 刷新 persona 内容中的场景导航段:先剥离旧段,再追加当前导航。\n * 索引为空且原内容无导航段时,原样返回(不附加空段)。\n */\n refreshNavigation(personaContent: string): string {\n const stripped = SceneIndex.stripNavigation(personaContent);\n const nav = this.generateNavigation();\n if (!nav) return stripped;\n return `${stripped}\\n\\n${nav}\\n`;\n }\n\n /**\n * 持久化到磁盘(写临时文件 + 原子 rename)。\n * 失败时打印告警,不抛出(避免阻断主流程)。\n */\n private async persist(): Promise<void> {\n try {\n const dir = path.dirname(this.indexPath);\n await fs.mkdir(dir, { recursive: true });\n const data = JSON.stringify(this.entries, null, 2);\n // 原子写:先写 .tmp 再 rename,防止半写损坏索引\n const tmpPath = `${this.indexPath}.tmp`;\n await fs.writeFile(tmpPath, data, \"utf-8\");\n await fs.rename(tmpPath, this.indexPath);\n } catch (err) {\n console.warn(\"[scene-index] 持久化索引失败,忽略:\", err);\n }\n }\n}\n"],"mappings":";;;;AAkBA,SAAS,YAAY,UAAU;AAC/B,OAAO,UAAU;AA0BV,IAAM,0BAA0B;AAKhC,IAAM,aAAN,MAAM,YAAW;AAAA,EAQtB,YAAoB,SAAiB;AAAjB;AAClB,SAAK,YAAY,KAAK,KAAK,SAAS,aAAa,kBAAkB;AACnE,SAAK,YAAY,KAAK,KAAK,SAAS,QAAQ;AAAA,EAC9C;AAAA,EAHoB;AAAA;AAAA,EANZ;AAAA;AAAA,EAEA,UAA6B,CAAC;AAAA;AAAA,EAE9B;AAAA;AAAA;AAAA;AAAA;AAAA,EAWR,MAAM,OAAsB;AAC1B,QAAI;AACF,YAAM,MAAM,MAAM,GAAG,SAAS,KAAK,WAAW,OAAO;AACrD,YAAM,SAAS,KAAK,MAAM,GAAG;AAC7B,WAAK,UAAU,MAAM,QAAQ,MAAM,IAAK,SAA+B,CAAC;AAExE,WAAK,UAAU,KAAK,QAAQ;AAAA,QAC1B,CAAC,MAAM,KAAK,OAAO,EAAE,aAAa;AAAA,MACpC;AAAA,IACF,QAAQ;AACN,WAAK,UAAU,CAAC;AAAA,IAClB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,OAAO,OAA6C;AACxD,UAAM,WAAW,KAAK,QAAQ,KAAK,CAAC,MAAM,EAAE,aAAa,MAAM,QAAQ;AACvE,QAAI,UAAU;AACZ,eAAS,QAAQ,MAAM;AACvB,eAAS,UAAU,MAAM;AACzB,eAAS,WAAW,MAAM;AAC1B,eAAS,UAAU,MAAM;AAEzB,UAAI,MAAM,QAAS,UAAS,UAAU,MAAM;AAAA,IAC9C,OAAO;AACL,WAAK,QAAQ,KAAK;AAAA,QAChB,UAAU,MAAM;AAAA,QAChB,UAAU,MAAM;AAAA,QAChB,OAAO,MAAM;AAAA,QACb,SAAS,MAAM;AAAA,QACf,SAAS,MAAM;AAAA,QACf,SAAS,MAAM;AAAA,QACf,MAAM,MAAM,QAAQ;AAAA,MACtB,CAAC;AAAA,IACH;AACA,UAAM,KAAK,QAAQ;AAAA,EACrB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,cAAc,SAAmC;AACrD,UAAM,QAAQ,KAAK,QAAQ,KAAK,CAAC,MAAM,EAAE,aAAa,OAAO;AAC7D,QAAI,CAAC,MAAO,QAAO;AACnB,UAAM;AACN,UAAM,KAAK,QAAQ;AACnB,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,OAAO,SAAmC;AAC9C,UAAM,SAAS,KAAK,QAAQ;AAC5B,SAAK,UAAU,KAAK,QAAQ,OAAO,CAAC,MAAM,EAAE,aAAa,OAAO;AAChE,UAAM,UAAU,KAAK,QAAQ,SAAS;AACtC,QAAI,QAAS,OAAM,KAAK,QAAQ;AAChC,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,aAAa,SAA8C;AACzD,WAAO,KAAK,QAAQ,KAAK,CAAC,MAAM,EAAE,aAAa,OAAO;AAAA,EACxD;AAAA;AAAA,EAGA,IAAI,OAAe;AACjB,WAAO,KAAK,QAAQ;AAAA,EACtB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,SAA4B;AAC1B,WAAO,CAAC,GAAG,KAAK,OAAO,EAAE,KAAK,CAAC,GAAG,MAAM;AACtC,UAAI,EAAE,SAAS,EAAE,KAAM,QAAO,EAAE,OAAO,EAAE;AAEzC,cAAQ,EAAE,WAAW,IAAI,cAAc,EAAE,WAAW,EAAE;AAAA,IACxD,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,aAAa,OAAgC;AAC3C,WAAO,KAAK,KAAK,KAAK,WAAW,MAAM,QAAQ;AAAA,EACjD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,qBAA6B;AAC3B,UAAM,SAAS,KAAK,OAAO;AAC3B,QAAI,OAAO,WAAW,EAAG,QAAO;AAEhC,UAAM,SAAS,OAAO,IAAI,CAAC,MAAM;AAC/B,YAAM,OAAO,EAAE,QAAQ,MAAM,wBAAU,EAAE,QAAQ,KAAK,eAAQ;AAC9D,YAAM,YAAY,KAAK,aAAa,CAAC;AACrC,aAAO,OAAO,EAAE,KAAK,GAAG,IAAI;AAAA,QAAW,SAAS;AAAA,WAAc,EAAE,OAAO;AAAA,IACzE,CAAC;AAED,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,OAAO,KAAK,MAAM;AAAA,MAClB;AAAA,MACA;AAAA,IACF,EAAE,KAAK,IAAI;AAAA,EACb;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,OAAO,gBAAgB,gBAAgC;AACrD,UAAM,MAAM,eAAe,QAAQ,uBAAuB;AAC1D,QAAI,QAAQ,GAAI,QAAO;AACvB,WAAO,eAAe,MAAM,GAAG,GAAG,EAAE,QAAQ;AAAA,EAC9C;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,kBAAkB,gBAAgC;AAChD,UAAM,WAAW,YAAW,gBAAgB,cAAc;AAC1D,UAAM,MAAM,KAAK,mBAAmB;AACpC,QAAI,CAAC,IAAK,QAAO;AACjB,WAAO,GAAG,QAAQ;AAAA;AAAA,EAAO,GAAG;AAAA;AAAA,EAC9B;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAc,UAAyB;AACrC,QAAI;AACF,YAAM,MAAM,KAAK,QAAQ,KAAK,SAAS;AACvC,YAAM,GAAG,MAAM,KAAK,EAAE,WAAW,KAAK,CAAC;AACvC,YAAM,OAAO,KAAK,UAAU,KAAK,SAAS,MAAM,CAAC;AAEjD,YAAM,UAAU,GAAG,KAAK,SAAS;AACjC,YAAM,GAAG,UAAU,SAAS,MAAM,OAAO;AACzC,YAAM,GAAG,OAAO,SAAS,KAAK,SAAS;AAAA,IACzC,SAAS,KAAK;AACZ,cAAQ,KAAK,+EAA6B,GAAG;AAAA,IAC/C;AAAA,EACF;AACF;","names":[]}
|