@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,171 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ estimateTokens
4
+ } from "./chunk-CHVFXR24.js";
5
+ import "./chunk-2H7UOFLK.js";
6
+
7
+ // src/mermaid-injector.ts
8
+ var EMPTY_RESULT = {
9
+ injectText: "",
10
+ mermaid: "",
11
+ taskGoal: "",
12
+ progress: 0,
13
+ nodeCount: 0,
14
+ tokenCount: 0,
15
+ tokensSaved: 0,
16
+ fromExisting: false,
17
+ sessionId: ""
18
+ };
19
+ var MermaidInjector = class {
20
+ symbolic;
21
+ mermaidTokenBudget;
22
+ constructor(opts) {
23
+ this.symbolic = opts.symbolic;
24
+ this.mermaidTokenBudget = opts.mermaidTokenBudget ?? 800;
25
+ }
26
+ /**
27
+ * 提交工具调用 → 生成/更新 Mermaid 符号图 → 返回注入文本
28
+ *
29
+ * 如果 session_id 对应的会话已有符号图,则在已有图基础上增量更新。
30
+ */
31
+ async inject(toolCallPairs, sessionId) {
32
+ const sid = sessionId || "default";
33
+ if (!toolCallPairs || toolCallPairs.length === 0) {
34
+ return this.injectExisting(sid);
35
+ }
36
+ try {
37
+ let existingGraph = null;
38
+ try {
39
+ existingGraph = await this.symbolic.getSessionGraph(sid);
40
+ } catch {
41
+ }
42
+ const rawTokens = toolCallPairs.reduce(
43
+ (sum, p) => sum + estimateTokens(p.input || "") + estimateTokens(p.output || ""),
44
+ 0
45
+ );
46
+ const graph = await this.symbolic.symbolize({
47
+ toolCallPairs,
48
+ existingGraph,
49
+ session_id: sid
50
+ });
51
+ const injectText = this.formatInjectText(graph, sid);
52
+ const tokenCount = estimateTokens(injectText);
53
+ return {
54
+ injectText,
55
+ mermaid: graph.mermaid,
56
+ taskGoal: graph.task_goal,
57
+ progress: graph.progress,
58
+ nodeCount: graph.nodes.length,
59
+ tokenCount,
60
+ tokensSaved: Math.max(0, rawTokens - tokenCount),
61
+ fromExisting: false,
62
+ sessionId: sid
63
+ };
64
+ } catch (err) {
65
+ console.error("[mermaid-injector] inject failed:", err instanceof Error ? err.message : err);
66
+ return { ...EMPTY_RESULT, sessionId: sid };
67
+ }
68
+ }
69
+ /**
70
+ * 注入已有会话的符号图(不提交新工具调用)
71
+ */
72
+ async injectExisting(sessionId) {
73
+ try {
74
+ const graph = await this.symbolic.getSessionGraph(sessionId);
75
+ if (!graph) {
76
+ return { ...EMPTY_RESULT, sessionId };
77
+ }
78
+ const injectText = this.formatInjectText(graph, sessionId);
79
+ const tokenCount = estimateTokens(injectText);
80
+ return {
81
+ injectText,
82
+ mermaid: graph.mermaid,
83
+ taskGoal: graph.task_goal,
84
+ progress: graph.progress,
85
+ nodeCount: graph.nodes.length,
86
+ tokenCount,
87
+ tokensSaved: 0,
88
+ fromExisting: true,
89
+ sessionId
90
+ };
91
+ } catch {
92
+ return { ...EMPTY_RESULT, sessionId };
93
+ }
94
+ }
95
+ /**
96
+ * 把 SymbolGraph 格式化为可直接注入对话上下文的 Markdown 文本
97
+ *
98
+ * 格式:
99
+ * ## 任务进度图
100
+ *
101
+ * ```mermaid
102
+ * graph TD
103
+ * N1["搜索文件<br/>status: done<br/>summary: 找到配置"]
104
+ * N1 --> N2
105
+ * N2["编辑配置<br/>status: doing<br/>summary: 修改参数"]
106
+ * ```
107
+ *
108
+ * 任务: 重构记忆系统
109
+ * 进度: ████████░░ 80%
110
+ *
111
+ * 节点摘要:
112
+ * - [N1] 搜索文件 ✅ 找到配置
113
+ * - [N2] 编辑配置 🔧 修改参数中
114
+ *
115
+ * 提示: 需要查看某步骤的详细日志时,使用 memory_symbol_recall(node_id) 钻取。
116
+ */
117
+ formatInjectText(graph, sessionId) {
118
+ const lines = [];
119
+ lines.push("## \u4EFB\u52A1\u8FDB\u5EA6\u56FE");
120
+ lines.push("");
121
+ const mermaidTokens = estimateTokens(graph.mermaid);
122
+ if (mermaidTokens <= this.mermaidTokenBudget) {
123
+ lines.push("```mermaid");
124
+ lines.push(graph.mermaid);
125
+ lines.push("```");
126
+ } else {
127
+ lines.push("> \u26A0\uFE0F Mermaid \u56FE\u8FC7\u5927\uFF0C\u4EC5\u663E\u793A\u6587\u672C\u6458\u8981\u3002");
128
+ }
129
+ lines.push("");
130
+ if (graph.task_goal) {
131
+ lines.push(`\u4EFB\u52A1: ${graph.task_goal}`);
132
+ }
133
+ const progress = Math.max(0, Math.min(100, graph.progress));
134
+ const filled = Math.round(progress / 10);
135
+ const bar = "\u2588".repeat(filled) + "\u2591".repeat(10 - filled);
136
+ lines.push(`\u8FDB\u5EA6: ${bar} ${progress}%`);
137
+ lines.push("");
138
+ if (graph.nodes.length > 0) {
139
+ lines.push("\u8282\u70B9\u6458\u8981:");
140
+ for (const node of graph.nodes) {
141
+ const icon = this.statusIcon(node.status);
142
+ const label = node.label || node.node_id;
143
+ const summary = node.summary || "";
144
+ const summaryShort = summary.length > 60 ? summary.substring(0, 60) + "\u2026" : summary;
145
+ lines.push(`- [${node.node_id}] ${label} ${icon} ${summaryShort}`);
146
+ }
147
+ lines.push("");
148
+ }
149
+ lines.push("\u63D0\u793A: \u9700\u8981\u67E5\u770B\u67D0\u6B65\u9AA4\u7684\u8BE6\u7EC6\u65E5\u5FD7\u65F6\uFF0C\u4F7F\u7528 memory_symbol_recall(node_id) \u94BB\u53D6\u3002");
150
+ return lines.join("\n");
151
+ }
152
+ /** 状态图标 */
153
+ statusIcon(status) {
154
+ switch (status) {
155
+ case "done":
156
+ return "\u2705";
157
+ case "doing":
158
+ return "\u{1F527}";
159
+ case "paused":
160
+ return "\u23F8\uFE0F";
161
+ case "blocked":
162
+ return "\u{1F6AB}";
163
+ default:
164
+ return "\u2022";
165
+ }
166
+ }
167
+ };
168
+ export {
169
+ MermaidInjector
170
+ };
171
+ //# sourceMappingURL=mermaid-injector-ROQAAM7B.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/mermaid-injector.ts"],"sourcesContent":["/**\n * Mermaid 注入器 (Mermaid Injector) — Memory Palace MCP v2.0\n *\n * 灵感:TencentDB Agent Memory 的 mmd-injector.ts\n *\n * 核心思想:\n * 在长任务中,最大的 token 消耗者是冗长的工具调用日志(搜索结果、代码、错误堆栈)。\n * 本模块把工具调用日志压缩成紧凑的 Mermaid 符号图,并生成可直接注入对话上下文的文本。\n *\n * Agent 每轮调用 memory_mermaid_inject 提交本轮的工具调用 → 获得 Mermaid 符号图\n * → Agent 用这个紧凑图替代冗长的原始日志,节省大量 token。\n * 需要细节时通过 memory_symbol_recall(node_id) 钻取回原始记录。\n *\n * 与 SymbolicCompressor 的关系:\n * SymbolicCompressor 负责\"压缩\"(生成 Mermaid + 卸载原始日志到 L0)\n * MermaidInjector 负责\"注入\"(把 Mermaid 图格式化为对话上下文文本 + 追踪 token 节省量)\n *\n * 工作流:\n * 1. Agent 提交本轮工具调用 → SymbolicCompressor.symbolize() 生成/更新符号图\n * 2. MermaidInjector.inject() 把符号图格式化为注入文本\n * 3. 注入文本包含:\n * - Mermaid 拓扑图(紧凑符号表示)\n * - 任务进度百分比\n * - 节点摘要列表(可扫描的文本快照)\n * - 钻取提示(告知 agent 可用 node_id 回捞细节)\n * 4. 如果会话已有符号图(之前 symbolize 过),直接注入最新版本\n */\n\nimport type { SymbolGraph, SymbolNode } from \"./types.js\";\nimport { estimateTokens } from \"./token-counter.js\";\n\n/** MermaidInjector 配置 */\nexport interface MermaidInjectorOptions {\n /** SymbolicCompressor 实例(提供 symbolize / getSessionGraph) */\n symbolic: any;\n /** Mermaid 注入文本的 token 上限,默认 800 */\n mermaidTokenBudget?: number;\n}\n\n/** inject() 返回结果 */\nexport interface MermaidInjectResult {\n /** 可直接注入对话上下文的完整文本 */\n injectText: string;\n /** Mermaid 图文本 */\n mermaid: string;\n /** 任务目标 */\n taskGoal: string;\n /** 进度百分比 0-100 */\n progress: number;\n /** 节点数 */\n nodeCount: number;\n /** 注入文本的 token 数 */\n tokenCount: number;\n /** 本次压缩节省的 token 估算(原始工具日志 token - 注入文本 token) */\n tokensSaved: number;\n /** 是否从已有会话图注入(true=复用已有图,false=新生成) */\n fromExisting: boolean;\n /** 会话 ID */\n sessionId: string;\n}\n\n/** 空结果常量 */\nconst EMPTY_RESULT: MermaidInjectResult = {\n injectText: \"\",\n mermaid: \"\",\n taskGoal: \"\",\n progress: 0,\n nodeCount: 0,\n tokenCount: 0,\n tokensSaved: 0,\n fromExisting: false,\n sessionId: \"\",\n};\n\n/** 工具调用对(与 SymbolicCompressor 一致) */\nexport interface ToolCallPair {\n tool_call_id: string;\n tool_name: string;\n input: string;\n output: string;\n}\n\nexport class MermaidInjector {\n private symbolic: any;\n private mermaidTokenBudget: number;\n\n constructor(opts: MermaidInjectorOptions) {\n this.symbolic = opts.symbolic;\n this.mermaidTokenBudget = opts.mermaidTokenBudget ?? 800;\n }\n\n /**\n * 提交工具调用 → 生成/更新 Mermaid 符号图 → 返回注入文本\n *\n * 如果 session_id 对应的会话已有符号图,则在已有图基础上增量更新。\n */\n async inject(\n toolCallPairs: ToolCallPair[],\n sessionId?: string,\n ): Promise<MermaidInjectResult> {\n const sid = sessionId || \"default\";\n if (!toolCallPairs || toolCallPairs.length === 0) {\n // 无新工具调用 → 尝试注入已有会话图\n return this.injectExisting(sid);\n }\n\n try {\n // 获取已有图(增量更新)\n let existingGraph: SymbolGraph | null = null;\n try {\n existingGraph = await this.symbolic.getSessionGraph(sid);\n } catch {}\n\n // 计算原始日志 token 数\n const rawTokens = toolCallPairs.reduce(\n (sum, p) => sum + estimateTokens(p.input || \"\") + estimateTokens(p.output || \"\"),\n 0,\n );\n\n // 调用 SymbolicCompressor 生成/更新符号图\n const graph: SymbolGraph = await this.symbolic.symbolize({\n toolCallPairs,\n existingGraph,\n session_id: sid,\n });\n\n // 格式化注入文本\n const injectText = this.formatInjectText(graph, sid);\n const tokenCount = estimateTokens(injectText);\n\n return {\n injectText,\n mermaid: graph.mermaid,\n taskGoal: graph.task_goal,\n progress: graph.progress,\n nodeCount: graph.nodes.length,\n tokenCount,\n tokensSaved: Math.max(0, rawTokens - tokenCount),\n fromExisting: false,\n sessionId: sid,\n };\n } catch (err) {\n console.error(\"[mermaid-injector] inject failed:\", err instanceof Error ? err.message : err);\n return { ...EMPTY_RESULT, sessionId: sid };\n }\n }\n\n /**\n * 注入已有会话的符号图(不提交新工具调用)\n */\n async injectExisting(sessionId: string): Promise<MermaidInjectResult> {\n try {\n const graph = await this.symbolic.getSessionGraph(sessionId);\n if (!graph) {\n return { ...EMPTY_RESULT, sessionId };\n }\n const injectText = this.formatInjectText(graph, sessionId);\n const tokenCount = estimateTokens(injectText);\n return {\n injectText,\n mermaid: graph.mermaid,\n taskGoal: graph.task_goal,\n progress: graph.progress,\n nodeCount: graph.nodes.length,\n tokenCount,\n tokensSaved: 0,\n fromExisting: true,\n sessionId,\n };\n } catch {\n return { ...EMPTY_RESULT, sessionId };\n }\n }\n\n /**\n * 把 SymbolGraph 格式化为可直接注入对话上下文的 Markdown 文本\n *\n * 格式:\n * ## 任务进度图\n *\n * ```mermaid\n * graph TD\n * N1[\"搜索文件<br/>status: done<br/>summary: 找到配置\"]\n * N1 --> N2\n * N2[\"编辑配置<br/>status: doing<br/>summary: 修改参数\"]\n * ```\n *\n * 任务: 重构记忆系统\n * 进度: ████████░░ 80%\n *\n * 节点摘要:\n * - [N1] 搜索文件 ✅ 找到配置\n * - [N2] 编辑配置 🔧 修改参数中\n *\n * 提示: 需要查看某步骤的详细日志时,使用 memory_symbol_recall(node_id) 钻取。\n */\n private formatInjectText(graph: SymbolGraph, sessionId: string): string {\n const lines: string[] = [];\n\n lines.push(\"## 任务进度图\");\n lines.push(\"\");\n\n // Mermaid 图(如果不超过预算)\n const mermaidTokens = estimateTokens(graph.mermaid);\n if (mermaidTokens <= this.mermaidTokenBudget) {\n lines.push(\"```mermaid\");\n lines.push(graph.mermaid);\n lines.push(\"```\");\n } else {\n // Mermaid 太大 → 只放节点摘要文本\n lines.push(\"> ⚠️ Mermaid 图过大,仅显示文本摘要。\");\n }\n\n lines.push(\"\");\n\n // 任务目标 + 进度条\n if (graph.task_goal) {\n lines.push(`任务: ${graph.task_goal}`);\n }\n const progress = Math.max(0, Math.min(100, graph.progress));\n const filled = Math.round(progress / 10);\n const bar = \"█\".repeat(filled) + \"░\".repeat(10 - filled);\n lines.push(`进度: ${bar} ${progress}%`);\n lines.push(\"\");\n\n // 节点摘要\n if (graph.nodes.length > 0) {\n lines.push(\"节点摘要:\");\n for (const node of graph.nodes) {\n const icon = this.statusIcon(node.status);\n const label = node.label || node.node_id;\n const summary = node.summary || \"\";\n const summaryShort = summary.length > 60 ? summary.substring(0, 60) + \"…\" : summary;\n lines.push(`- [${node.node_id}] ${label} ${icon} ${summaryShort}`);\n }\n lines.push(\"\");\n }\n\n // 钻取提示\n lines.push(\"提示: 需要查看某步骤的详细日志时,使用 memory_symbol_recall(node_id) 钻取。\");\n\n return lines.join(\"\\n\");\n }\n\n /** 状态图标 */\n private statusIcon(status: string): string {\n switch (status) {\n case \"done\": return \"✅\";\n case \"doing\": return \"🔧\";\n case \"paused\": return \"⏸️\";\n case \"blocked\": return \"🚫\";\n default: return \"•\";\n }\n }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env node
2
+ import "./chunk-2H7UOFLK.js";
3
+
4
+ // src/observability.ts
5
+ var ObservabilityTracker = class {
6
+ metrics;
7
+ constructor() {
8
+ this.metrics = {
9
+ totalEncodes: 0,
10
+ totalRecalls: 0,
11
+ totalExtracts: 0,
12
+ totalSceneBuilds: 0,
13
+ totalPersonaUpdates: 0,
14
+ totalConsolidates: 0,
15
+ totalAutoContexts: 0,
16
+ totalOffloads: 0,
17
+ lastEncodeMs: 0,
18
+ lastRecallMs: 0,
19
+ lastExtractMs: 0,
20
+ pipelineStatus: null,
21
+ startedAt: Date.now()
22
+ };
23
+ }
24
+ /** 记录一次 encode 操作(含耗时) */
25
+ recordEncode(ms) {
26
+ this.metrics.totalEncodes++;
27
+ this.metrics.lastEncodeMs = ms;
28
+ }
29
+ /** 记录一次 recall 操作(含耗时) */
30
+ recordRecall(ms) {
31
+ this.metrics.totalRecalls++;
32
+ this.metrics.lastRecallMs = ms;
33
+ }
34
+ /** 记录一次 extract 操作(含耗时) */
35
+ recordExtract(ms) {
36
+ this.metrics.totalExtracts++;
37
+ this.metrics.lastExtractMs = ms;
38
+ }
39
+ /** 记录一次场景构建 */
40
+ recordSceneBuild() {
41
+ this.metrics.totalSceneBuilds++;
42
+ }
43
+ /** 记录一次画像更新 */
44
+ recordPersonaUpdate() {
45
+ this.metrics.totalPersonaUpdates++;
46
+ }
47
+ /** 记录一次记忆整理(consolidate) */
48
+ recordConsolidate() {
49
+ this.metrics.totalConsolidates++;
50
+ }
51
+ /** 记录一次自动上下文生成 */
52
+ recordAutoContext() {
53
+ this.metrics.totalAutoContexts++;
54
+ }
55
+ /** 记录一次卸载压缩(offload) */
56
+ recordOffload() {
57
+ this.metrics.totalOffloads++;
58
+ }
59
+ /** 更新管道状态快照(任意可序列化对象) */
60
+ setPipelineStatus(status) {
61
+ this.metrics.pipelineStatus = status;
62
+ }
63
+ /**
64
+ * 获取当前指标快照(返回副本,外部修改不影响内部状态)
65
+ */
66
+ getMetrics() {
67
+ return {
68
+ ...this.metrics,
69
+ // pipelineStatus 可能是对象,深拷贝一份避免外部篡改
70
+ pipelineStatus: this.metrics.pipelineStatus ? JSON.parse(JSON.stringify(this.metrics.pipelineStatus)) : null
71
+ };
72
+ }
73
+ /**
74
+ * 生成人类可读的运行报告
75
+ */
76
+ getReport() {
77
+ const m = this.metrics;
78
+ const uptime = Math.max(0, Math.round((Date.now() - m.startedAt) / 1e3));
79
+ return [
80
+ `Memory Palace Observability Report`,
81
+ ` Uptime: ${uptime}s (${Math.round(uptime / 60)}min)`,
82
+ ``,
83
+ `Operations:`,
84
+ ` encodes: ${m.totalEncodes} (last: ${m.lastEncodeMs}ms)`,
85
+ ` recalls: ${m.totalRecalls} (last: ${m.lastRecallMs}ms)`,
86
+ ` extracts: ${m.totalExtracts} (last: ${m.lastExtractMs}ms)`,
87
+ ` scene_builds: ${m.totalSceneBuilds}`,
88
+ ` persona_updates: ${m.totalPersonaUpdates}`,
89
+ ` consolidates: ${m.totalConsolidates}`,
90
+ ` auto_contexts: ${m.totalAutoContexts}`,
91
+ ` offloads: ${m.totalOffloads}`,
92
+ ``,
93
+ `Pipeline: ${m.pipelineStatus ? JSON.stringify(m.pipelineStatus) : "not started"}`
94
+ ].join("\n");
95
+ }
96
+ };
97
+ export {
98
+ ObservabilityTracker
99
+ };
100
+ //# sourceMappingURL=observability-6AMQYVCF.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/observability.ts"],"sourcesContent":["/**\n * 记忆系统可观测性 — 追踪操作指标 + 管道状态\n *\n * 设计目标:\n * - 全部同步、O(1),调用方在热路径上记录指标几乎零开销\n * - 不依赖任何外部库\n * - getMetrics() 返回副本,避免外部篡改内部状态\n *\n * 用法:\n * const tracker = new ObservabilityTracker();\n * const t0 = Date.now();\n * await doEncode(...);\n * tracker.recordEncode(Date.now() - t0);\n * console.log(tracker.getReport());\n */\n\n/** 可观测性指标快照 */\nexport interface MemoryMetrics {\n // ===== 计数器 =====\n totalEncodes: number;\n totalRecalls: number;\n totalExtracts: number;\n totalSceneBuilds: number;\n totalPersonaUpdates: number;\n totalConsolidates: number;\n totalAutoContexts: number;\n totalOffloads: number;\n // ===== 延迟(最近一次,毫秒) =====\n lastEncodeMs: number;\n lastRecallMs: number;\n lastExtractMs: number;\n // ===== 管道状态快照 =====\n pipelineStatus: any | null;\n // ===== 启动时间(epoch ms) =====\n startedAt: number;\n}\n\n/**\n * 可观测性追踪器\n *\n * 所有 record* 方法均为同步、O(1),可在任何热路径中调用。\n */\nexport class ObservabilityTracker {\n private metrics: MemoryMetrics;\n\n constructor() {\n this.metrics = {\n totalEncodes: 0,\n totalRecalls: 0,\n totalExtracts: 0,\n totalSceneBuilds: 0,\n totalPersonaUpdates: 0,\n totalConsolidates: 0,\n totalAutoContexts: 0,\n totalOffloads: 0,\n lastEncodeMs: 0,\n lastRecallMs: 0,\n lastExtractMs: 0,\n pipelineStatus: null,\n startedAt: Date.now(),\n };\n }\n\n /** 记录一次 encode 操作(含耗时) */\n recordEncode(ms: number): void {\n this.metrics.totalEncodes++;\n this.metrics.lastEncodeMs = ms;\n }\n\n /** 记录一次 recall 操作(含耗时) */\n recordRecall(ms: number): void {\n this.metrics.totalRecalls++;\n this.metrics.lastRecallMs = ms;\n }\n\n /** 记录一次 extract 操作(含耗时) */\n recordExtract(ms: number): void {\n this.metrics.totalExtracts++;\n this.metrics.lastExtractMs = ms;\n }\n\n /** 记录一次场景构建 */\n recordSceneBuild(): void {\n this.metrics.totalSceneBuilds++;\n }\n\n /** 记录一次画像更新 */\n recordPersonaUpdate(): void {\n this.metrics.totalPersonaUpdates++;\n }\n\n /** 记录一次记忆整理(consolidate) */\n recordConsolidate(): void {\n this.metrics.totalConsolidates++;\n }\n\n /** 记录一次自动上下文生成 */\n recordAutoContext(): void {\n this.metrics.totalAutoContexts++;\n }\n\n /** 记录一次卸载压缩(offload) */\n recordOffload(): void {\n this.metrics.totalOffloads++;\n }\n\n /** 更新管道状态快照(任意可序列化对象) */\n setPipelineStatus(status: any): void {\n this.metrics.pipelineStatus = status;\n }\n\n /**\n * 获取当前指标快照(返回副本,外部修改不影响内部状态)\n */\n getMetrics(): MemoryMetrics {\n return {\n ...this.metrics,\n // pipelineStatus 可能是对象,深拷贝一份避免外部篡改\n pipelineStatus: this.metrics.pipelineStatus\n ? JSON.parse(JSON.stringify(this.metrics.pipelineStatus))\n : null,\n };\n }\n\n /**\n * 生成人类可读的运行报告\n */\n getReport(): string {\n const m = this.metrics;\n const uptime = Math.max(0, Math.round((Date.now() - m.startedAt) / 1000));\n return [\n `Memory Palace Observability Report`,\n ` Uptime: ${uptime}s (${Math.round(uptime / 60)}min)`,\n ``,\n `Operations:`,\n ` encodes: ${m.totalEncodes} (last: ${m.lastEncodeMs}ms)`,\n ` recalls: ${m.totalRecalls} (last: ${m.lastRecallMs}ms)`,\n ` extracts: ${m.totalExtracts} (last: ${m.lastExtractMs}ms)`,\n ` scene_builds: ${m.totalSceneBuilds}`,\n ` persona_updates: ${m.totalPersonaUpdates}`,\n ` consolidates: ${m.totalConsolidates}`,\n ` auto_contexts: ${m.totalAutoContexts}`,\n ` offloads: ${m.totalOffloads}`,\n ``,\n `Pipeline: ${\n m.pipelineStatus ? JSON.stringify(m.pipelineStatus) : \"not started\"\n }`,\n ].join(\"\\n\");\n }\n}\n"],"mappings":";;;;AA0CO,IAAM,uBAAN,MAA2B;AAAA,EACxB;AAAA,EAER,cAAc;AACZ,SAAK,UAAU;AAAA,MACb,cAAc;AAAA,MACd,cAAc;AAAA,MACd,eAAe;AAAA,MACf,kBAAkB;AAAA,MAClB,qBAAqB;AAAA,MACrB,mBAAmB;AAAA,MACnB,mBAAmB;AAAA,MACnB,eAAe;AAAA,MACf,cAAc;AAAA,MACd,cAAc;AAAA,MACd,eAAe;AAAA,MACf,gBAAgB;AAAA,MAChB,WAAW,KAAK,IAAI;AAAA,IACtB;AAAA,EACF;AAAA;AAAA,EAGA,aAAa,IAAkB;AAC7B,SAAK,QAAQ;AACb,SAAK,QAAQ,eAAe;AAAA,EAC9B;AAAA;AAAA,EAGA,aAAa,IAAkB;AAC7B,SAAK,QAAQ;AACb,SAAK,QAAQ,eAAe;AAAA,EAC9B;AAAA;AAAA,EAGA,cAAc,IAAkB;AAC9B,SAAK,QAAQ;AACb,SAAK,QAAQ,gBAAgB;AAAA,EAC/B;AAAA;AAAA,EAGA,mBAAyB;AACvB,SAAK,QAAQ;AAAA,EACf;AAAA;AAAA,EAGA,sBAA4B;AAC1B,SAAK,QAAQ;AAAA,EACf;AAAA;AAAA,EAGA,oBAA0B;AACxB,SAAK,QAAQ;AAAA,EACf;AAAA;AAAA,EAGA,oBAA0B;AACxB,SAAK,QAAQ;AAAA,EACf;AAAA;AAAA,EAGA,gBAAsB;AACpB,SAAK,QAAQ;AAAA,EACf;AAAA;AAAA,EAGA,kBAAkB,QAAmB;AACnC,SAAK,QAAQ,iBAAiB;AAAA,EAChC;AAAA;AAAA;AAAA;AAAA,EAKA,aAA4B;AAC1B,WAAO;AAAA,MACL,GAAG,KAAK;AAAA;AAAA,MAER,gBAAgB,KAAK,QAAQ,iBACzB,KAAK,MAAM,KAAK,UAAU,KAAK,QAAQ,cAAc,CAAC,IACtD;AAAA,IACN;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,YAAoB;AAClB,UAAM,IAAI,KAAK;AACf,UAAM,SAAS,KAAK,IAAI,GAAG,KAAK,OAAO,KAAK,IAAI,IAAI,EAAE,aAAa,GAAI,CAAC;AACxE,WAAO;AAAA,MACL;AAAA,MACA,aAAa,MAAM,MAAM,KAAK,MAAM,SAAS,EAAE,CAAC;AAAA,MAChD;AAAA,MACA;AAAA,MACA,cAAc,EAAE,YAAY,WAAW,EAAE,YAAY;AAAA,MACrD,cAAc,EAAE,YAAY,WAAW,EAAE,YAAY;AAAA,MACrD,eAAe,EAAE,aAAa,WAAW,EAAE,aAAa;AAAA,MACxD,mBAAmB,EAAE,gBAAgB;AAAA,MACrC,sBAAsB,EAAE,mBAAmB;AAAA,MAC3C,mBAAmB,EAAE,iBAAiB;AAAA,MACtC,oBAAoB,EAAE,iBAAiB;AAAA,MACvC,eAAe,EAAE,aAAa;AAAA,MAC9B;AAAA,MACA,aACE,EAAE,iBAAiB,KAAK,UAAU,EAAE,cAAc,IAAI,aACxD;AAAA,IACF,EAAE,KAAK,IAAI;AAAA,EACb;AACF;","names":[]}
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ OffloadEngine
4
+ } from "./chunk-JL66S7P7.js";
5
+ import "./chunk-CHVFXR24.js";
6
+ import "./chunk-2H7UOFLK.js";
7
+ export {
8
+ OffloadEngine
9
+ };
10
+ //# sourceMappingURL=offload-engine-BF5IXXBH.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,228 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ fetchWithRetry,
4
+ withLlmLock
5
+ } from "./chunk-SSDURI3I.js";
6
+ import "./chunk-2H7UOFLK.js";
7
+
8
+ // src/persona-generator.ts
9
+ import * as fs from "fs";
10
+ import * as path from "path";
11
+
12
+ // src/prompts/persona-prompt.ts
13
+ var PERSONA_SYSTEM_PROMPT = `\u4F60\u662F\u4E00\u4E2A"\u7528\u6237\u753B\u50CF\u5408\u6210\u5668"\u3002\u4F60\u7684\u4EFB\u52A1\u662F\u57FA\u4E8E\u591A\u4E2A L2 \u573A\u666F\u6458\u8981\uFF0C\u5408\u6210\u6216\u66F4\u65B0\u4E00\u4EFD\u5173\u4E8E\u7528\u6237\u7684\u5B8C\u6574\u753B\u50CF\uFF08L3 \u7528\u6237\u753B\u50CF\uFF09\u3002
14
+
15
+ \u4F60\u9700\u8981\u4ECE\u573A\u666F\u5185\u5BB9\u4E2D\u8BC6\u522B\u5E76\u63D0\u70BC\u51FA\u7528\u6237\u5728\u4EE5\u4E0B\u56DB\u4E2A\u7EF4\u5EA6\u4E0A\u7684\u7A33\u5B9A\u7279\u5F81\uFF1A
16
+
17
+ 1. \u6C9F\u901A\u504F\u597D\uFF1A\u7528\u6237\u7684\u8BED\u8A00\u98CE\u683C\uFF08\u6B63\u5F0F/\u968F\u610F\uFF09\u3001\u56DE\u7B54\u8BE6\u7565\u504F\u597D\u3001\u662F\u5426\u504F\u597D\u4EE3\u7801\u793A\u4F8B\u3001\u662F\u5426\u504F\u597D\u76F4\u63A5\u7ED3\u8BBA\u3001\u662F\u5426\u504F\u597D\u5206\u6B65\u9AA4\u8BF4\u660E\u3001\u5E38\u7528\u8BED\u8A00\uFF08\u4E2D\u6587/\u82F1\u6587/\u4E2D\u82F1\u6DF7\u5408\uFF09\u7B49\u3002
18
+ 2. \u6280\u672F\u6808\uFF1A\u7528\u6237\u719F\u6089\u6216\u6B63\u5728\u4F7F\u7528\u7684\u7F16\u7A0B\u8BED\u8A00\u3001\u6846\u67B6\u3001\u6570\u636E\u5E93\u3001\u4E2D\u95F4\u4EF6\u3001\u5DE5\u5177\u94FE\u3001\u4E91\u5E73\u53F0\u3001\u7B2C\u4E09\u65B9\u670D\u52A1\u7B49\u3002
19
+ 3. \u5DE5\u4F5C\u4E60\u60EF\uFF1A\u7528\u6237\u5728\u5F00\u53D1\u4E0E\u534F\u4F5C\u4E2D\u7684\u6A21\u5F0F\uFF0C\u4F8B\u5982\u504F\u597D\u654F\u6377\u8FD8\u662F\u7011\u5E03\u3001\u662F\u5426\u91CD\u89C6\u81EA\u52A8\u5316\u6D4B\u8BD5\u3001\u4EE3\u7801\u5BA1\u67E5\u4E60\u60EF\u3001\u6587\u6863\u4E60\u60EF\u3001\u8C03\u8BD5\u4E0E\u6392\u969C\u65B9\u5F0F\u3001\u7248\u672C\u63A7\u5236\u4E60\u60EF\u7B49\u3002
20
+ 4. \u9879\u76EE\u80CC\u666F\uFF1A\u7528\u6237\u5F53\u524D\u6D89\u53CA\u7684\u9879\u76EE\u540D\u79F0\u3001\u4E1A\u52A1\u9886\u57DF\u3001\u67B6\u6784\u5F62\u6001\uFF08\u5355\u4F53 / \u5FAE\u670D\u52A1 / Serverless / CLI \u7B49\uFF09\u3001\u56E2\u961F\u89C4\u6A21\u7B49\u53EF\u63A8\u65AD\u7684\u4E0A\u4E0B\u6587\u3002
21
+
22
+ \u91CD\u8981\u539F\u5219\uFF1A
23
+ - \u53EA\u6839\u636E\u573A\u666F\u6458\u8981\u4E2D\u5B9E\u9645\u51FA\u73B0\u7684\u4FE1\u606F\u8FDB\u884C\u63A8\u65AD\uFF0C\u4E25\u7981\u51ED\u7A7A\u7F16\u9020\u3002\u65E0\u6CD5\u786E\u5B9A\u7684\u7EF4\u5EA6\u5E94\u660E\u786E\u6807\u6CE8"\u4FE1\u606F\u4E0D\u8DB3"\u3002
24
+ - \u5173\u6CE8\u957F\u671F\u7A33\u5B9A\u7684\u504F\u597D\u4E0E\u80CC\u666F\uFF0C\u5FFD\u7565\u4E00\u6B21\u6027\u3001\u4E34\u65F6\u6027\u7684\u7EC6\u8282\u4E0E\u95F2\u804A\u3002
25
+ - \u8FDB\u884C\u589E\u91CF\u66F4\u65B0\u65F6\uFF0C\u5E94\u5C06\u65B0\u4FE1\u606F\u4E0E\u5DF2\u6709\u753B\u50CF\u5408\u5E76\uFF1A\u4FDD\u7559\u4ECD\u7136\u6709\u6548\u7684\u65E7\u4FE1\u606F\uFF0C\u7528\u65B0\u4FE1\u606F\u4FEE\u6B63\u6216\u8865\u5145\u8FC7\u65F6\u90E8\u5206\uFF1B\u51B2\u7A81\u65F6\u4EE5\u66F4\u65B0\u7684\u573A\u666F\u4E3A\u51C6\u3002
26
+ - \u8F93\u51FA\u5FC5\u987B\u662F\u7EAF Markdown \u6587\u672C\uFF0C\u4F7F\u7528 "\xA7" \u4F5C\u4E3A\u5404\u7EF4\u5EA6\u7684\u5206\u9694\u7B26\u3002
27
+
28
+ \u8F93\u51FA\u683C\u5F0F\uFF08\u4E25\u683C\u9075\u5B88\uFF0C\u4E0D\u8981\u589E\u51CF\u7AE0\u8282\uFF09\uFF1A
29
+
30
+ # \u7528\u6237\u753B\u50CF
31
+
32
+ \xA7 \u6C9F\u901A\u504F\u597D
33
+ - \u8981\u70B9 1
34
+ - \u8981\u70B9 2
35
+ \uFF08\u5982\u4FE1\u606F\u4E0D\u8DB3\uFF0C\u8BF7\u4EC5\u5199\u4E00\u884C"\u4FE1\u606F\u4E0D\u8DB3"\uFF09
36
+
37
+ \xA7 \u6280\u672F\u6808
38
+ - \u8981\u70B9 1
39
+ - \u8981\u70B9 2
40
+
41
+ \xA7 \u5DE5\u4F5C\u4E60\u60EF
42
+ - \u8981\u70B9 1
43
+ - \u8981\u70B9 2
44
+
45
+ \xA7 \u9879\u76EE\u80CC\u666F
46
+ - \u8981\u70B9 1
47
+ - \u8981\u70B9 2
48
+
49
+ \xA7 \u66F4\u65B0\u8BF4\u660E
50
+ \uFF08\u672C\u6B21\u5408\u6210\u6216\u66F4\u65B0\u7684\u7B80\u77ED\u8BF4\u660E\uFF0C\u5E76\u6307\u51FA\u5C1A\u672A\u8986\u76D6\u5230\u7684\u7EF4\u5EA6\u6216\u9700\u8981\u8865\u5145\u7684\u4FE1\u606F\uFF09
51
+
52
+ \u7EA6\u675F\uFF1A
53
+ - \u4E0D\u8981\u4F7F\u7528 Markdown \u4EE3\u7801\u5757\uFF08\`\`\`\uFF09\u5305\u88F9\u6574\u6BB5\u8F93\u51FA\u3002
54
+ - \u4E0D\u8981\u8F93\u51FA JSON\u3002
55
+ - \u4E0D\u8981\u6DFB\u52A0\u4E0E\u4E0A\u8FF0\u7EF4\u5EA6\u65E0\u5173\u7684\u7AE0\u8282\u3002
56
+ - \u6BCF\u4E2A\u7EF4\u5EA6\u7684\u6807\u9898\u884C\u5FC5\u987B\u4EE5 "\xA7 " \u5F00\u5934\u3002`;
57
+ function buildPersonaUserPrompt(opts) {
58
+ const { mode, scenesContent, existingPersona, currentTimestamp, triggerInfo } = opts;
59
+ const header = mode === "first" ? "\u8BF7\u6839\u636E\u4EE5\u4E0B\u573A\u666F\u6458\u8981\uFF0C\u9996\u6B21\u5408\u6210\u4E00\u4EFD\u5B8C\u6574\u7684\u7528\u6237\u753B\u50CF\u3002" : "\u8BF7\u6839\u636E\u4EE5\u4E0B\u65B0\u7684\u573A\u666F\u6458\u8981\uFF0C\u5BF9\u5DF2\u6709\u7528\u6237\u753B\u50CF\u8FDB\u884C\u589E\u91CF\u66F4\u65B0\u3002";
60
+ const triggerBlock = triggerInfo && triggerInfo.trim().length > 0 ? `
61
+
62
+ \u3010\u89E6\u53D1\u4FE1\u606F\u3011
63
+ ${triggerInfo.trim()}` : "";
64
+ const existingBlock = mode === "incremental" && existingPersona ? `
65
+
66
+ \u3010\u5DF2\u6709\u7528\u6237\u753B\u50CF\u3011
67
+ """
68
+ ${existingPersona}
69
+ """
70
+
71
+ \u8BF7\u5C06\u65B0\u573A\u666F\u4E2D\u7684\u4FE1\u606F\u4E0E\u5DF2\u6709\u753B\u50CF\u5408\u5E76\uFF1A\u4FDD\u7559\u4ECD\u7136\u6709\u6548\u7684\u65E7\u4FE1\u606F\uFF0C\u7528\u65B0\u4FE1\u606F\u4FEE\u6B63\u6216\u8865\u5145\u8FC7\u65F6\u90E8\u5206\uFF1B\u5982\u51FA\u73B0\u51B2\u7A81\uFF0C\u4EE5\u66F4\u65B0\u7684\u573A\u666F\u4E3A\u51C6\u3002` : "";
72
+ return `${header}
73
+
74
+ \u5F53\u524D\u65F6\u95F4: ${currentTimestamp}${triggerBlock}
75
+
76
+ \u3010\u573A\u666F\u6458\u8981\uFF08L2\uFF09\u3011
77
+ """
78
+ ${scenesContent}
79
+ """${existingBlock}
80
+
81
+ \u8BF7\u4E25\u683C\u6309\u7167\u7CFB\u7EDF\u63D0\u793A\u4E2D\u89C4\u5B9A\u7684 Markdown \u683C\u5F0F\u4E0E "\xA7" \u5206\u9694\u7B26\u8F93\u51FA\u7528\u6237\u753B\u50CF\uFF0C\u4E0D\u8981\u8F93\u51FA\u4EFB\u4F55\u989D\u5916\u89E3\u91CA\u3002`;
82
+ }
83
+
84
+ // src/persona-generator.ts
85
+ var PersonaGenerator = class {
86
+ storage;
87
+ apiUrl;
88
+ apiKey;
89
+ model;
90
+ dataDir;
91
+ constructor(opts) {
92
+ this.storage = opts.storage;
93
+ this.apiUrl = opts.analyzerApiUrl;
94
+ this.apiKey = opts.analyzerApiKey;
95
+ this.model = opts.analyzerModel;
96
+ this.dataDir = opts.dataDir;
97
+ }
98
+ /**
99
+ * 生成或更新用户画像。
100
+ *
101
+ * - first 模式:从零合成。
102
+ * - incremental 模式:读取已有画像并请求 LLM 合并更新,version 自增。
103
+ */
104
+ async generate(opts) {
105
+ const { scenes, mode, existingPersona = null } = opts;
106
+ const scenesContent = this.buildScenesContent(scenes);
107
+ const currentTimestamp = (/* @__PURE__ */ new Date()).toISOString();
108
+ const userPrompt = buildPersonaUserPrompt({
109
+ mode,
110
+ scenesContent,
111
+ existingPersona: existingPersona?.content ?? null,
112
+ currentTimestamp
113
+ });
114
+ let personaMarkdown;
115
+ try {
116
+ personaMarkdown = await this.callLLM(PERSONA_SYSTEM_PROMPT, userPrompt);
117
+ } catch (err) {
118
+ console.warn("[PersonaGenerator] LLM \u8C03\u7528\u5931\u8D25\uFF0C\u964D\u7EA7\u4E3A\u6700\u5C0F\u753B\u50CF:", err);
119
+ personaMarkdown = this.fallbackPersonaMarkdown(scenes, mode, existingPersona);
120
+ }
121
+ const cleanedMarkdown = this.cleanMarkdown(personaMarkdown);
122
+ const now = Date.now();
123
+ const prevVersion = mode === "incremental" && existingPersona ? existingPersona.version : 0;
124
+ const id = `persona_${now}_${this.randomId()}`;
125
+ const filePath = path.join(this.dataDir, "persona.md");
126
+ const persona = {
127
+ id,
128
+ content: cleanedMarkdown,
129
+ file_path: filePath,
130
+ scene_ids: scenes.map((s) => s.id),
131
+ version: prevVersion + 1,
132
+ created_at: mode === "incremental" && existingPersona ? existingPersona.created_at : now,
133
+ updated_at: now
134
+ };
135
+ try {
136
+ if (!fs.existsSync(this.dataDir)) {
137
+ fs.mkdirSync(this.dataDir, { recursive: true });
138
+ }
139
+ fs.writeFileSync(filePath, persona.content, "utf-8");
140
+ } catch (err) {
141
+ console.warn("[PersonaGenerator] \u5199\u5165 persona.md \u5931\u8D25:", err);
142
+ }
143
+ return persona;
144
+ }
145
+ /** 将场景数组格式化为 LLM 可读的摘要文本 */
146
+ buildScenesContent(scenes) {
147
+ if (!scenes || scenes.length === 0) {
148
+ return "\uFF08\u6682\u65E0\u573A\u666F\u6458\u8981\uFF09";
149
+ }
150
+ return scenes.map((s, i) => {
151
+ const summary = (s.summary || "").trim() || "(\u65E0\u6458\u8981)";
152
+ return `### \u573A\u666F ${i + 1}: ${s.label || s.id}
153
+ - ID: ${s.id}
154
+ - \u6458\u8981: ${summary}`;
155
+ }).join("\n\n");
156
+ }
157
+ /** LLM 不可用时生成的降级最小画像 */
158
+ fallbackPersonaMarkdown(scenes, mode, existingPersona) {
159
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
160
+ const version = mode === "incremental" && existingPersona ? existingPersona.version + 1 : 1;
161
+ const sceneLines = scenes.map((s) => `- ${s.label || s.id}: ${(s.summary || "").trim() || "(\u65E0\u6458\u8981)"}`).join("\n");
162
+ return [
163
+ `# \u7528\u6237\u753B\u50CF`,
164
+ ``,
165
+ `\xA7 \u6C9F\u901A\u504F\u597D`,
166
+ `\uFF08\u4FE1\u606F\u4E0D\u8DB3\uFF09`,
167
+ ``,
168
+ `\xA7 \u6280\u672F\u6808`,
169
+ `\uFF08\u4FE1\u606F\u4E0D\u8DB3\uFF09`,
170
+ ``,
171
+ `\xA7 \u5DE5\u4F5C\u4E60\u60EF`,
172
+ `\uFF08\u4FE1\u606F\u4E0D\u8DB3\uFF09`,
173
+ ``,
174
+ `\xA7 \u9879\u76EE\u80CC\u666F`,
175
+ sceneLines || `\uFF08\u4FE1\u606F\u4E0D\u8DB3\uFF09`,
176
+ ``,
177
+ `\xA7 \u66F4\u65B0\u8BF4\u660E`,
178
+ `LLM \u4E0D\u53EF\u7528\uFF0C\u672C\u6B21\u4E3A\u964D\u7EA7\u751F\u6210\u7684\u6700\u5C0F\u753B\u50CF\u3002\u751F\u6210\u65F6\u95F4: ${ts}`,
179
+ ``
180
+ ].join("\n");
181
+ }
182
+ /** 清理 LLM 返回:去除首尾空白及整体代码块包裹 */
183
+ cleanMarkdown(raw) {
184
+ let text = (raw ?? "").trim();
185
+ const fenceMatch = text.match(/^```(?:markdown|md)?\s*\n([\s\S]*?)\n?```\s*$/i);
186
+ if (fenceMatch) {
187
+ text = fenceMatch[1].trim();
188
+ }
189
+ return text;
190
+ }
191
+ /** 调用 OpenAI 兼容 LLM 端点,返回 message content */
192
+ async callLLM(systemPrompt, userPrompt) {
193
+ const response = await withLlmLock(
194
+ () => fetchWithRetry(this.apiUrl, {
195
+ method: "POST",
196
+ headers: {
197
+ "Content-Type": "application/json",
198
+ Authorization: `Bearer ${this.apiKey}`
199
+ },
200
+ body: JSON.stringify({
201
+ model: this.model,
202
+ messages: [
203
+ { role: "system", content: systemPrompt },
204
+ { role: "user", content: userPrompt }
205
+ ],
206
+ temperature: 0.3
207
+ })
208
+ })
209
+ );
210
+ if (!response.ok) {
211
+ throw new Error(`Persona LLM API error: ${response.status} ${response.statusText}`);
212
+ }
213
+ const data = await response.json();
214
+ const content = data.choices?.[0]?.message?.content ?? "";
215
+ if (!content) {
216
+ throw new Error("Persona LLM \u8FD4\u56DE\u7A7A\u5185\u5BB9");
217
+ }
218
+ return content;
219
+ }
220
+ /** 生成短随机 ID 片段 */
221
+ randomId() {
222
+ return Math.random().toString(36).slice(2, 10);
223
+ }
224
+ };
225
+ export {
226
+ PersonaGenerator
227
+ };
228
+ //# sourceMappingURL=persona-generator-NZQVIVDA.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/persona-generator.ts","../src/prompts/persona-prompt.ts"],"sourcesContent":["/**\n * L3 用户画像生成器 — 从 L2 场景块(Scene)合成用户画像(Persona)\n *\n * 设计参考 TencentDB Agent Memory 的 persona-generation.ts:\n * LLM 阅读场景摘要 → 生成或更新结构化用户画像。\n *\n * 流程:\n * 1. 聚合 L2 场景摘要\n * 2. 调用 OpenAI 兼容 LLM(与 analyzer.ts 同一套调用模式)\n * 3. 解析为 Markdown 文本\n * 4. 构建 / 更新 Persona 对象(version 递增)\n * 5. 持久化到 dataDir/persona.md\n */\n\nimport * as fs from \"fs\";\nimport * as path from \"path\";\nimport { Persona, Scene } from \"./types.js\";\nimport { withLlmLock, fetchWithRetry } from \"./llm-lock.js\";\nimport { PERSONA_SYSTEM_PROMPT, buildPersonaUserPrompt } from \"./prompts/persona-prompt.js\";\n\n/** PersonaGenerator 构造参数 */\nexport interface PersonaGeneratorOptions {\n storage: any;\n analyzerApiUrl: string;\n analyzerApiKey: string;\n analyzerModel: string;\n dataDir: string;\n}\n\n/** generate() 参数 */\nexport interface GenerateOptions {\n scenes: Scene[];\n mode: \"first\" | \"incremental\";\n existingPersona?: Persona | null;\n}\n\nexport class PersonaGenerator {\n private storage: any;\n private apiUrl: string;\n private apiKey: string;\n private model: string;\n private dataDir: string;\n\n constructor(opts: PersonaGeneratorOptions) {\n this.storage = opts.storage;\n this.apiUrl = opts.analyzerApiUrl;\n this.apiKey = opts.analyzerApiKey;\n this.model = opts.analyzerModel;\n this.dataDir = opts.dataDir;\n }\n\n /**\n * 生成或更新用户画像。\n *\n * - first 模式:从零合成。\n * - incremental 模式:读取已有画像并请求 LLM 合并更新,version 自增。\n */\n async generate(opts: GenerateOptions): Promise<Persona> {\n const { scenes, mode, existingPersona = null } = opts;\n\n // 1. 聚合场景摘要\n const scenesContent = this.buildScenesContent(scenes);\n const currentTimestamp = new Date().toISOString();\n\n // 2. 构建 prompt\n const userPrompt = buildPersonaUserPrompt({\n mode,\n scenesContent,\n existingPersona: existingPersona?.content ?? null,\n currentTimestamp,\n });\n\n // 3. 调用 LLM\n let personaMarkdown: string;\n try {\n personaMarkdown = await this.callLLM(PERSONA_SYSTEM_PROMPT, userPrompt);\n } catch (err) {\n console.warn(\"[PersonaGenerator] LLM 调用失败,降级为最小画像:\", err);\n personaMarkdown = this.fallbackPersonaMarkdown(scenes, mode, existingPersona);\n }\n\n // 4. 解析为 Markdown 文本(去除首尾空白与可能的代码块包裹)\n const cleanedMarkdown = this.cleanMarkdown(personaMarkdown);\n\n // 5. 构建 / 更新 Persona 对象(version 递增)\n const now = Date.now();\n const prevVersion =\n mode === \"incremental\" && existingPersona ? existingPersona.version : 0;\n const id = `persona_${now}_${this.randomId()}`;\n const filePath = path.join(this.dataDir, \"persona.md\");\n\n const persona: Persona = {\n id,\n content: cleanedMarkdown,\n file_path: filePath,\n scene_ids: scenes.map((s) => s.id),\n version: prevVersion + 1,\n created_at:\n mode === \"incremental\" && existingPersona ? existingPersona.created_at : now,\n updated_at: now,\n };\n\n // 6. 持久化 persona.md\n try {\n if (!fs.existsSync(this.dataDir)) {\n fs.mkdirSync(this.dataDir, { recursive: true });\n }\n fs.writeFileSync(filePath, persona.content, \"utf-8\");\n } catch (err) {\n console.warn(\"[PersonaGenerator] 写入 persona.md 失败:\", err);\n }\n\n return persona;\n }\n\n /** 将场景数组格式化为 LLM 可读的摘要文本 */\n private buildScenesContent(scenes: Scene[]): string {\n if (!scenes || scenes.length === 0) {\n return \"(暂无场景摘要)\";\n }\n return scenes\n .map((s, i) => {\n const summary = (s.summary || \"\").trim() || \"(无摘要)\";\n return `### 场景 ${i + 1}: ${s.label || s.id}\\n- ID: ${s.id}\\n- 摘要: ${summary}`;\n })\n .join(\"\\n\\n\");\n }\n\n /** LLM 不可用时生成的降级最小画像 */\n private fallbackPersonaMarkdown(\n scenes: Scene[],\n mode: \"first\" | \"incremental\",\n existingPersona: Persona | null,\n ): string {\n const ts = new Date().toISOString();\n const version =\n mode === \"incremental\" && existingPersona ? existingPersona.version + 1 : 1;\n const sceneLines = scenes\n .map((s) => `- ${s.label || s.id}: ${(s.summary || \"\").trim() || \"(无摘要)\"}`)\n .join(\"\\n\");\n\n return [\n `# 用户画像`,\n ``,\n `§ 沟通偏好`,\n `(信息不足)`,\n ``,\n `§ 技术栈`,\n `(信息不足)`,\n ``,\n `§ 工作习惯`,\n `(信息不足)`,\n ``,\n `§ 项目背景`,\n sceneLines || `(信息不足)`,\n ``,\n `§ 更新说明`,\n `LLM 不可用,本次为降级生成的最小画像。生成时间: ${ts}`,\n ``,\n ].join(\"\\n\");\n }\n\n /** 清理 LLM 返回:去除首尾空白及整体代码块包裹 */\n private cleanMarkdown(raw: string): string {\n let text = (raw ?? \"\").trim();\n // 去除整段 ```markdown / ``` 包裹\n const fenceMatch = text.match(/^```(?:markdown|md)?\\s*\\n([\\s\\S]*?)\\n?```\\s*$/i);\n if (fenceMatch) {\n text = fenceMatch[1].trim();\n }\n return text;\n }\n\n /** 调用 OpenAI 兼容 LLM 端点,返回 message content */\n private async callLLM(systemPrompt: string, userPrompt: string): Promise<string> {\n const response = await withLlmLock(() =>\n fetchWithRetry(this.apiUrl, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.apiKey}`,\n },\n body: JSON.stringify({\n model: this.model,\n messages: [\n { role: \"system\", content: systemPrompt },\n { role: \"user\", content: userPrompt },\n ],\n temperature: 0.3,\n }),\n })\n );\n\n if (!response.ok) {\n throw new Error(`Persona LLM API error: ${response.status} ${response.statusText}`);\n }\n\n const data = (await response.json()) as any;\n const content: string = data.choices?.[0]?.message?.content ?? \"\";\n if (!content) {\n throw new Error(\"Persona LLM 返回空内容\");\n }\n return content;\n }\n\n /** 生成短随机 ID 片段 */\n private randomId(): string {\n return Math.random().toString(36).slice(2, 10);\n }\n}\n","/**\n * L3 用户画像合成 — Prompt 构建\n *\n * 将 L2 场景摘要(Scene)输入给 LLM,合成/更新一份结构化的用户画像(Persona)。\n * 设计参考 TencentDB Agent Memory 的 persona-generation 思路:\n * LLM 阅读场景摘要 → 生成或更新结构化画像。\n *\n * 输出格式约定:纯 Markdown 文本,使用 \"§\" 作为各维度分隔符。\n */\n\n/**\n * 用户画像合成器的系统提示词(中文)。\n * 角色:用户画像合成器。要求 LLM 覆盖四个维度:\n * 沟通偏好 / 技术栈 / 工作习惯 / 项目背景。\n */\nexport const PERSONA_SYSTEM_PROMPT: string = `你是一个\"用户画像合成器\"。你的任务是基于多个 L2 场景摘要,合成或更新一份关于用户的完整画像(L3 用户画像)。\n\n你需要从场景内容中识别并提炼出用户在以下四个维度上的稳定特征:\n\n1. 沟通偏好:用户的语言风格(正式/随意)、回答详略偏好、是否偏好代码示例、是否偏好直接结论、是否偏好分步骤说明、常用语言(中文/英文/中英混合)等。\n2. 技术栈:用户熟悉或正在使用的编程语言、框架、数据库、中间件、工具链、云平台、第三方服务等。\n3. 工作习惯:用户在开发与协作中的模式,例如偏好敏捷还是瀑布、是否重视自动化测试、代码审查习惯、文档习惯、调试与排障方式、版本控制习惯等。\n4. 项目背景:用户当前涉及的项目名称、业务领域、架构形态(单体 / 微服务 / Serverless / CLI 等)、团队规模等可推断的上下文。\n\n重要原则:\n- 只根据场景摘要中实际出现的信息进行推断,严禁凭空编造。无法确定的维度应明确标注\"信息不足\"。\n- 关注长期稳定的偏好与背景,忽略一次性、临时性的细节与闲聊。\n- 进行增量更新时,应将新信息与已有画像合并:保留仍然有效的旧信息,用新信息修正或补充过时部分;冲突时以更新的场景为准。\n- 输出必须是纯 Markdown 文本,使用 \"§\" 作为各维度的分隔符。\n\n输出格式(严格遵守,不要增减章节):\n\n# 用户画像\n\n§ 沟通偏好\n- 要点 1\n- 要点 2\n(如信息不足,请仅写一行\"信息不足\")\n\n§ 技术栈\n- 要点 1\n- 要点 2\n\n§ 工作习惯\n- 要点 1\n- 要点 2\n\n§ 项目背景\n- 要点 1\n- 要点 2\n\n§ 更新说明\n(本次合成或更新的简短说明,并指出尚未覆盖到的维度或需要补充的信息)\n\n约束:\n- 不要使用 Markdown 代码块(\\`\\`\\`)包裹整段输出。\n- 不要输出 JSON。\n- 不要添加与上述维度无关的章节。\n- 每个维度的标题行必须以 \"§ \" 开头。`;\n\n/**\n * 构建发送给 LLM 的用户消息(user prompt)。\n *\n * @param opts.mode 生成模式:'first' 首次合成 / 'incremental' 增量更新\n * @param opts.scenesContent 已格式化的 L2 场景摘要文本\n * @param opts.existingPersona 已有画像文本(仅 incremental 模式使用)\n * @param opts.currentTimestamp 当前时间戳(可读字符串)\n * @param opts.triggerInfo 可选的触发原因描述\n */\nexport function buildPersonaUserPrompt(opts: {\n mode: \"first\" | \"incremental\";\n scenesContent: string;\n existingPersona: string | null;\n currentTimestamp: string;\n triggerInfo?: string;\n}): string {\n const { mode, scenesContent, existingPersona, currentTimestamp, triggerInfo } = opts;\n\n const header =\n mode === \"first\"\n ? \"请根据以下场景摘要,首次合成一份完整的用户画像。\"\n : \"请根据以下新的场景摘要,对已有用户画像进行增量更新。\";\n\n const triggerBlock =\n triggerInfo && triggerInfo.trim().length > 0\n ? `\\n\\n【触发信息】\\n${triggerInfo.trim()}`\n : \"\";\n\n const existingBlock =\n mode === \"incremental\" && existingPersona\n ? `\\n\\n【已有用户画像】\\n\"\"\"\\n${existingPersona}\\n\"\"\"\\n\\n请将新场景中的信息与已有画像合并:保留仍然有效的旧信息,用新信息修正或补充过时部分;如出现冲突,以更新的场景为准。`\n : \"\";\n\n return `${header}\n\n当前时间: ${currentTimestamp}${triggerBlock}\n\n【场景摘要(L2)】\n\"\"\"\n${scenesContent}\n\"\"\"${existingBlock}\n\n请严格按照系统提示中规定的 Markdown 格式与 \"§\" 分隔符输出用户画像,不要输出任何额外解释。`;\n}\n"],"mappings":"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","names":[]}