ppxans-harness 2.6.0 → 3.2.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/LICENSE +201 -201
- package/README.md +218 -262
- package/bin/ppx-channels.js +2 -2
- package/bin/ppx-serve.js +5 -5
- package/bin/ppx-setup.js +124 -0
- package/bin/ppx-web.js +140 -0
- package/bin/ppx.js +2 -2
- package/config/identity.md +6 -6
- package/config/ishiki.md +16 -16
- package/config/ppx.json +6 -124
- package/config/ppx.json.example +143 -0
- package/package.json +16 -6
- package/skills/.usage.json +6 -0
- package/skills/agent-professional-training/SKILL.md +94 -0
- package/skills/brainstorm/SKILL.md +24 -0
- package/skills/cupid-lover-comms/SKILL.md +37 -0
- package/skills/debug/SKILL.md +26 -0
- package/skills/plan/SKILL.md +25 -0
- package/skills/ponytail/SKILL.md +25 -0
- package/skills/ppx-memory/SKILL.md +91 -0
- package/skills/ppx-memory/scripts/cli.js +192 -0
- package/skills/ppx-memory/scripts/experience.js +133 -0
- package/skills/ppx-memory/scripts/fact-store.js +842 -0
- package/skills/ppx-memory/scripts/l0.js +52 -0
- package/skills/ppx-memory/scripts/l2.js +146 -0
- package/skills/ppx-memory/scripts/l3.js +112 -0
- package/skills/ppx-memory/scripts/memory-ticker.js +238 -0
- package/skills/ppx-memory/scripts/pii.js +42 -0
- package/skills/ppx-memory/scripts/schema.js +80 -0
- package/skills/ppx-memory/scripts/session.js +398 -0
- package/skills/ppx-memory/scripts/similarity.js +43 -0
- package/skills/ppx-memory/scripts/store.js +116 -0
- package/skills/ppx-memory/scripts/wal.js +38 -0
- package/skills/ppx-selfheal/SKILL.md +24 -0
- package/skills/ppx-selfheal/scripts/cli.js +80 -0
- package/skills/ppx-selfheal/scripts/healer.js +184 -0
- package/skills/ppx-selfheal/scripts/logger.js +17 -0
- package/skills/ppx-selfheal/scripts/store.js +116 -0
- package/skills/prompt-depth-kit/SKILL.md +28 -0
- package/skills/session-naming/SKILL.md +36 -0
- package/skills/verify/SKILL.md +25 -0
- package/src/agent/index.js +1340 -717
- package/src/agent/prompts.js +46 -2
- package/src/aml-server.js +197 -151
- package/src/ans/eviction.js +123 -143
- package/src/ans/guard.js +159 -120
- package/src/ans/lifecycle.js +96 -93
- package/src/ans/proactive.js +112 -129
- package/src/ans/reward.js +95 -111
- package/src/ans/values.js +15 -15
- package/src/audit/audit-chain.js +43 -7
- package/src/audit/verifier.js +157 -120
- package/src/bus/circuit-breaker.js +9 -1
- package/src/bus/runtime-bus.js +107 -93
- package/src/channels/base.js +57 -34
- package/src/channels/feishu.js +118 -126
- package/src/channels/http.js +1090 -651
- package/src/channels/index.js +111 -110
- package/src/channels/log.js +29 -29
- package/src/channels/wechat-crypto.js +73 -74
- package/src/channels/wechat.js +191 -197
- package/src/channels/workspace.js +94 -0
- package/src/channels-cli.js +126 -124
- package/src/cli.js +129 -120
- package/src/commands/index.js +142 -0
- package/src/config/channels.js +137 -170
- package/src/config/index.js +277 -224
- package/src/config/placeholder.js +31 -0
- package/src/config/providers.js +148 -182
- package/src/config/settings.js +156 -182
- package/src/core/policy.js +69 -18
- package/src/core/trace.js +8 -6
- package/src/edit/editblock.js +266 -0
- package/src/edit/snapshot.js +67 -0
- package/src/evidence/index.js +153 -0
- package/src/evolve/playbook.js +9 -10
- package/src/hooks/index.js +113 -0
- package/src/llm/client.js +4 -3
- package/src/llm/dsml.js +74 -74
- package/src/llm/embedder.js +41 -35
- package/src/llm/fence.js +52 -52
- package/src/llm/index.js +4 -4
- package/src/llm/local-embedder.js +94 -0
- package/src/llm/presets.js +113 -0
- package/src/llm/pricing.js +93 -0
- package/src/llm/retry.js +73 -73
- package/src/llm/router.js +92 -89
- package/src/mcp/admin.js +44 -10
- package/src/mcp/client.js +487 -477
- package/src/mcp/http.js +23 -37
- package/src/mcp/index.js +116 -116
- package/src/mcp/server.js +23 -18
- package/src/mcp/tasks.js +9 -14
- package/src/memory/asset-hub.js +6 -11
- package/src/memory/canvas.js +2 -5
- package/src/memory/compaction.js +28 -28
- package/src/memory/experience.js +133 -122
- package/src/memory/fact-store.js +914 -698
- package/src/memory/failure-episode.js +20 -11
- package/src/memory/fork.js +17 -8
- package/src/memory/index.js +8 -6
- package/src/memory/l0.js +53 -52
- package/src/memory/l2.js +145 -130
- package/src/memory/l3.js +111 -111
- package/src/memory/legion-board.js +71 -0
- package/src/memory/memory-ticker.js +239 -240
- package/src/memory/session.js +398 -397
- package/src/memory/sqlite-store.js +581 -0
- package/src/mode/blackboard.js +49 -49
- package/src/mode/graph.js +42 -41
- package/src/mode/index.js +64 -64
- package/src/mode/legion.js +54 -51
- package/src/mode/plan-exec.js +50 -50
- package/src/mode/router.js +28 -40
- package/src/orchestrator/agent-worker.js +69 -69
- package/src/orchestrator/dag.js +90 -83
- package/src/orchestrator/experts.js +76 -0
- package/src/orchestrator/index.js +1 -1
- package/src/orchestrator/legion.js +179 -187
- package/src/orchestrator/supervisor.js +6 -8
- package/src/permissions/index.js +378 -0
- package/src/persona/index.js +28 -29
- package/src/plugin/builtin.js +313 -215
- package/src/plugin/context.js +80 -79
- package/src/plugin/index.js +62 -62
- package/src/plugin/v3.js +73 -0
- package/src/protocol/index.js +148 -0
- package/src/repomap/index.js +309 -0
- package/src/review/index.js +393 -0
- package/src/seam/registry.js +3 -0
- package/src/seam/shell.js +55 -55
- package/src/security/injection.js +79 -0
- package/src/selfheal/evolve.js +67 -67
- package/src/selfheal/healer.js +184 -167
- package/src/selfheal/run.js +9 -9
- package/src/server.js +63 -60
- package/src/services/diagnose.js +180 -0
- package/src/services/learning-service.js +9 -0
- package/src/services/memory-health.js +34 -6
- package/src/services/memory-service.js +42 -9
- package/src/services/triage.js +138 -0
- package/src/session/parts.js +76 -0
- package/src/session/projection.js +73 -0
- package/src/session/rollout.js +54 -0
- package/src/session/turn.js +137 -0
- package/src/skills/lint.js +72 -0
- package/src/skills/loader.js +231 -150
- package/src/skills/search.js +58 -0
- package/src/skills/verify.js +95 -100
- package/src/tools/advanced.js +388 -352
- package/src/tools/builtin.js +384 -297
- package/src/tools/catalog.js +283 -159
- package/src/tools/command-guard.js +112 -112
- package/src/tools/custom.js +47 -47
- package/src/tools/delegate.js +383 -297
- package/src/tools/document.js +254 -253
- package/src/tools/git.js +151 -0
- package/src/tools/governance.js +47 -20
- package/src/tools/index.js +16 -11
- package/src/tools/methods.js +178 -178
- package/src/tools/ocr.js +59 -59
- package/src/tools/sandbox-worker.js +40 -0
- package/src/tools/sandbox.js +92 -0
- package/src/tools/seam.js +162 -125
- package/src/tools/selfmod.js +196 -176
- package/src/tools/v3.js +225 -0
- package/src/tools/vad.js +176 -0
- package/src/tools/voice.js +238 -0
- package/src/utils/async.js +14 -0
- package/src/utils/config-file.js +53 -0
- package/src/utils/crashguard.js +88 -0
- package/src/utils/http.js +53 -0
- package/src/utils/id.js +8 -0
- package/src/utils/json-state.js +33 -0
- package/src/utils/logger.js +17 -17
- package/src/utils/ndjson.js +25 -0
- package/src/utils/pii.js +42 -42
- package/src/utils/rate-limit.js +50 -0
- package/src/utils/schema.js +80 -0
- package/src/utils/similarity.js +43 -0
- package/src/utils/store.js +170 -108
- package/src/utils/text.js +15 -15
- package/src/utils/trace.js +153 -153
- package/src/utils/wal.js +39 -0
- package/src/utils/winutf8.js +16 -15
- package/src/wiki/index.js +170 -0
|
@@ -3,13 +3,27 @@
|
|
|
3
3
|
// 每次失败存结构化 episode (错误类型/根因/修复/置信度), 下次相似故障检索历史辅助诊断, 自愈不再从零推理。
|
|
4
4
|
// 与经验库 (Experience, L4) 互补: 经验库是"学到的教训", 本模块是"故障的结构化病历" (可检索、可回放)。
|
|
5
5
|
// 纯代码可测, 检索用词法相似 (零依赖), 可升级 embedding。
|
|
6
|
-
|
|
6
|
+
// ✅ 接线状态 (2026-10-03 已接入): agent/_runTool 在工具失败时 record() 写入病历,
|
|
7
|
+
// 并在返回前 search() 检索历史同类故障, 把"已知根因/修法"附在错误结果后供模型参考
|
|
8
|
+
// (闭环: 失败 → 病历 → 下次同类失败直接带出历史结论, 不必从零推理)。
|
|
7
9
|
import path from "node:path";
|
|
8
|
-
import { ensureDir,
|
|
10
|
+
import { ensureDir, readJson, writeJson } from "../utils/store.js";
|
|
11
|
+
import { shortId } from "../utils/id.js";
|
|
9
12
|
import { lexicalSimilarity } from "../evolve/playbook.js";
|
|
10
13
|
|
|
11
14
|
export const FAILURE_CATEGORY = ["throttle", "network", "validation", "auth", "unknown"];
|
|
12
15
|
|
|
16
|
+
// 按错误文本归类 (零依赖词法判据, 与 FAILURE_CATEGORY 一一对应)
|
|
17
|
+
// 顺序即优先级: 限流/网络属瞬态, 鉴权/参数属确定性错误, 二者处置方式完全不同
|
|
18
|
+
export function classifyFailure(text) {
|
|
19
|
+
const s = String(text || "").toLowerCase();
|
|
20
|
+
if (/429|rate.?limit|too many requests|限流|频率超限/.test(s)) return "throttle";
|
|
21
|
+
if (/econn|etimedout|eai_again|enotfound|socket hang up|network|timeout|超时|连接失败|网络/.test(s)) return "network";
|
|
22
|
+
if (/401|403|unauthor|forbidden|invalid.?(api.?)?key|鉴权|未授权|无权限|权限不足/.test(s)) return "auth";
|
|
23
|
+
if (/参数|校验|应为|必填|非法|invalid|required|schema|enum|validation/.test(s)) return "validation";
|
|
24
|
+
return "unknown";
|
|
25
|
+
}
|
|
26
|
+
|
|
13
27
|
export class FailureEpisodeStore {
|
|
14
28
|
constructor(dataDir, { maxEpisodes = 500 } = {}) {
|
|
15
29
|
this.dir = path.join(dataDir, "memory", "failures");
|
|
@@ -20,24 +34,19 @@ export class FailureEpisodeStore {
|
|
|
20
34
|
}
|
|
21
35
|
|
|
22
36
|
_load() {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const d = JSON.parse(fs.readFileSync(this.file, "utf8"));
|
|
26
|
-
if (Array.isArray(d)) return d;
|
|
27
|
-
}
|
|
28
|
-
} catch {}
|
|
29
|
-
return [];
|
|
37
|
+
const d = readJson(this.file, null);
|
|
38
|
+
return Array.isArray(d) ? d : [];
|
|
30
39
|
}
|
|
31
40
|
|
|
32
41
|
_save() {
|
|
33
|
-
|
|
42
|
+
writeJson(this.file, this._episodes);
|
|
34
43
|
}
|
|
35
44
|
|
|
36
45
|
// 记录一次失败 episode
|
|
37
46
|
// { tool, error, category, rootCause, fix, confidence, traceRef }
|
|
38
47
|
record(ep) {
|
|
39
48
|
const e = {
|
|
40
|
-
id: "fe"
|
|
49
|
+
id: shortId("fe", 5),
|
|
41
50
|
tool: ep.tool || "unknown",
|
|
42
51
|
error: String(ep.error || "").slice(0, 500),
|
|
43
52
|
category: FAILURE_CATEGORY.includes(ep.category) ? ep.category : "unknown",
|
package/src/memory/fork.js
CHANGED
|
@@ -7,6 +7,7 @@ import fs from "node:fs";
|
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import { ensureDir, writeText, readText } from "../utils/store.js";
|
|
9
9
|
import { lexicalSimilarity } from "../evolve/playbook.js";
|
|
10
|
+
import { debug } from "../utils/logger.js";
|
|
10
11
|
|
|
11
12
|
// 从主 agent 导出记忆快照到子 dataDir
|
|
12
13
|
// 快照内容: L1 facts (top N) + L3 persona (若存在) + 全局经验精选 (top M)
|
|
@@ -25,18 +26,26 @@ export function exportMemorySnapshot({ agent, toDataDir, factsLimit = 50, experi
|
|
|
25
26
|
writeText(path.join(snapDir, "facts.md"), `# L1 事实快照 (fork 基线)\n${lines}\n`);
|
|
26
27
|
wrote.facts = top.length;
|
|
27
28
|
}
|
|
28
|
-
} catch {}
|
|
29
|
+
} catch (e) { debug(`[memory/fork] 已忽略异常: ${e && e.message ? e.message : e}`); }
|
|
29
30
|
|
|
30
|
-
// L3 persona: 主 agent 画像
|
|
31
|
+
// L3 persona: 主 agent 画像 (用户画像 + agent 人格)
|
|
32
|
+
// 修复 (2026-09-17): 原调用 agent.personaStore.read() —— PersonaStore 并不存在 read(),
|
|
33
|
+
// 真实 API 是 userPersona() / agentPersona()。虽有 typeof 守卫不致崩, 但分支永不进入,
|
|
34
|
+
// persona.md 从未生成 (静默降级)。此处改用真实方法, 并把两份画像一并导出。
|
|
31
35
|
try {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
const ps = agent.personaStore;
|
|
37
|
+
if (ps && typeof ps.userPersona === "function") {
|
|
38
|
+
const parts = [];
|
|
39
|
+
const u = typeof ps.userPersona === "function" ? ps.userPersona() : "";
|
|
40
|
+
const a = typeof ps.agentPersona === "function" ? ps.agentPersona() : "";
|
|
41
|
+
if (u) parts.push(String(u).trim());
|
|
42
|
+
if (a) parts.push(String(a).trim());
|
|
43
|
+
if (parts.length) {
|
|
44
|
+
writeText(path.join(snapDir, "persona.md"), `# L3 画像快照 (fork 基线)\n\n${parts.join("\n\n---\n\n")}\n`);
|
|
36
45
|
wrote.persona = true;
|
|
37
46
|
}
|
|
38
47
|
}
|
|
39
|
-
} catch {}
|
|
48
|
+
} catch (e) { debug(`[memory/fork] 已忽略异常: ${e && e.message ? e.message : e}`); }
|
|
40
49
|
|
|
41
50
|
// 全局经验精选
|
|
42
51
|
try {
|
|
@@ -48,7 +57,7 @@ export function exportMemorySnapshot({ agent, toDataDir, factsLimit = 50, experi
|
|
|
48
57
|
wrote.experience = list.length;
|
|
49
58
|
}
|
|
50
59
|
}
|
|
51
|
-
} catch {}
|
|
60
|
+
} catch (e) { debug(`[memory/fork] 已忽略异常: ${e && e.message ? e.message : e}`); }
|
|
52
61
|
|
|
53
62
|
return { wrote, path: snapDir };
|
|
54
63
|
}
|
package/src/memory/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
// src/memory/index.js - 记忆系统统一出口 (v0.3
|
|
2
|
-
export { FactStore } from "./fact-store.js";
|
|
3
|
-
export {
|
|
4
|
-
export {
|
|
5
|
-
export {
|
|
6
|
-
export {
|
|
1
|
+
// src/memory/index.js - 记忆系统统一出口 (v0.3 五层架构)
|
|
2
|
+
export { FactStore } from "./fact-store.js";
|
|
3
|
+
export { SqliteFactStore } from "./sqlite-store.js";
|
|
4
|
+
export { MemoryTicker } from "./memory-ticker.js";
|
|
5
|
+
export { Experience } from "./experience.js";
|
|
6
|
+
export { LegionBoard } from "./legion-board.js";
|
|
7
|
+
export { L0Recorder } from "./l0.js";
|
|
8
|
+
export { SceneStore } from "./l2.js";
|
|
7
9
|
export { PersonaStore } from "./l3.js";
|
package/src/memory/l0.js
CHANGED
|
@@ -1,52 +1,53 @@
|
|
|
1
|
-
// src/memory/l0.js - L0 原始对话记录 (腾讯风格)
|
|
2
|
-
// 架构: 会话事件日志 (SessionStore) 为唯一事实源, L0 是其只读派生视图
|
|
3
|
-
// 不再独立维护 l0/*.jsonl -- 对话原文由 session 事件日志全量保存, 消除 today/l0/facts 三处重复
|
|
4
|
-
// 过滤噪音: 短消息/命令/注入标签 (保留, 用于判断"是否值得记录")
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
import { ensureDir, logicalDay } from "../utils/store.js";
|
|
7
|
-
import { SessionStore, EVENTS } from "./session.js";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
if (c
|
|
14
|
-
if (
|
|
15
|
-
if (c
|
|
16
|
-
if (
|
|
17
|
-
if (
|
|
18
|
-
return
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
this.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
this.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
1
|
+
// src/memory/l0.js - L0 原始对话记录 (腾讯风格)
|
|
2
|
+
// 架构: 会话事件日志 (SessionStore) 为唯一事实源, L0 是其只读派生视图
|
|
3
|
+
// 不再独立维护 l0/*.jsonl -- 对话原文由 session 事件日志全量保存, 消除 today/l0/facts 三处重复
|
|
4
|
+
// 过滤噪音: 短消息/命令/注入标签 (保留, 用于判断"是否值得记录")
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { ensureDir, logicalDay } from "../utils/store.js";
|
|
7
|
+
import { SessionStore, EVENTS } from "./session.js";
|
|
8
|
+
import { debug } from "../utils/logger.js";
|
|
9
|
+
|
|
10
|
+
// 短消息/命令/噪音过滤
|
|
11
|
+
function shouldCapture(content) {
|
|
12
|
+
const c = String(content || "").trim();
|
|
13
|
+
if (!c) return false;
|
|
14
|
+
if (c.length < 2) return false; // 太短
|
|
15
|
+
if (/^(\/|!|\.)/.test(c)) return false; // 命令
|
|
16
|
+
if (c.length < 3) return false; // 太短(含2字寒暄)
|
|
17
|
+
if (["你好","您好","在吗","哈喽","拜拜","再见","晚安","早上好","下午好","晚上好"].includes(c)) return false; // 纯寒暄
|
|
18
|
+
if (/\b(hi|hello|ok|thanks)\b/i.test(c) && c.length < 8) return false;
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class L0Recorder {
|
|
23
|
+
// 兼容两种构造: (sessionStore[, dataDir]) 或 (dataDir) -- 后者内部新建 SessionStore
|
|
24
|
+
constructor(sessionStoreOrDir, dataDir) {
|
|
25
|
+
if (sessionStoreOrDir && typeof sessionStoreOrDir.eventsByDay === "function") {
|
|
26
|
+
this.sessionStore = sessionStoreOrDir;
|
|
27
|
+
this.dir = dataDir ? path.join(dataDir, "memory", "l0") : null;
|
|
28
|
+
} else {
|
|
29
|
+
const dir = sessionStoreOrDir;
|
|
30
|
+
this.sessionStore = new SessionStore(dir);
|
|
31
|
+
this.dir = path.join(dir, "memory", "l0");
|
|
32
|
+
}
|
|
33
|
+
if (this.dir) { try { ensureDir(this.dir); } catch (e) { debug(`[memory/l0] 已忽略异常: ${e && e.message ? e.message : e}`); } }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 记录: 代理到 session 事件日志 (session 是唯一事实源, 不再独立写 JSONL)
|
|
37
|
+
record({ role, content, sessionKey = "default" } = {}) {
|
|
38
|
+
if (!shouldCapture(content)) return null;
|
|
39
|
+
const type = role === "assistant" ? EVENTS.ASSISTANT : EVENTS.USER;
|
|
40
|
+
return this.sessionStore.append(sessionKey, type, { content: String(content).slice(0, 4000) });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 读取某天的对话 (从 session 事件按天派生, 最近 N 条)
|
|
44
|
+
read(day = logicalDay(), limit = 50) {
|
|
45
|
+
return this.sessionStore.eventsByDay(day)
|
|
46
|
+
.map((r) => ({ role: r.role, content: r.content, sessionKey: r.sessionKey, timestamp: r.timestamp }))
|
|
47
|
+
.slice(-limit);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
count() {
|
|
51
|
+
return this.sessionStore.eventsByDay(logicalDay()).length;
|
|
52
|
+
}
|
|
53
|
+
}
|
package/src/memory/l2.js
CHANGED
|
@@ -1,131 +1,146 @@
|
|
|
1
|
-
// src/memory/l2.js - L2 场景记忆 (腾讯风格 scene extraction)
|
|
2
|
-
// 把相关记忆归档成场景: { name, keywords, facts[], lastUpdated }
|
|
3
|
-
// 零依赖: 用关键词聚类 + 时间窗聚合
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
import { ensureDir, readJson, writeJson, logicalDay, withFileLock } from "../utils/store.js";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
//
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
id:
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
return
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
1
|
+
// src/memory/l2.js - L2 场景记忆 (腾讯风格 scene extraction)
|
|
2
|
+
// 把相关记忆归档成场景: { name, keywords, facts[], lastUpdated }
|
|
3
|
+
// 零依赖: 用关键词聚类 + 时间窗聚合
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { ensureDir, readJson, writeJson, logicalDay, withFileLock } from "../utils/store.js";
|
|
6
|
+
import { migrateData, writeSchema } from "../utils/schema.js";
|
|
7
|
+
|
|
8
|
+
// scenes.json 当前 schema 版本 (纯数组基线 = 1); 未来数据结构变更时 +1 并注册迁移
|
|
9
|
+
export const SCENES_SCHEMA_VERSION = 1;
|
|
10
|
+
|
|
11
|
+
// 中文简单分词: 提取有意义的词 (2字以上连续片段 + 已知高频概念)
|
|
12
|
+
const STOP = new Set(["这个","那个","我们","你们","他们","什么","怎么","可以","一个","就是","知道","没有","如果","因为","所以","但是","然后","现在","今天","昨天","明天","已经","还有","所有","这样","那样","自己","的时候","一下","一点","一些","这些","那些","东西","事情","问题","觉得","应该","需要","开始","继续","大家","真的","只是","可能","不是","都是","一直","非常","其实","最后","主要","联系","关系"]);
|
|
13
|
+
|
|
14
|
+
function tokenize(text) {
|
|
15
|
+
const clean = String(text || "").replace(/[^\u4e00-\u9fa5a-zA-Z0-9]/g, " ");
|
|
16
|
+
const words = clean.split(/\s+/).filter(Boolean);
|
|
17
|
+
const cjk = clean.match(/[\u4e00-\u9fa5]{2,4}/g) || [];
|
|
18
|
+
return [...new Set([...words, ...cjk.map((w) => w.toLowerCase())].filter((w) => !STOP.has(w) && w.length >= 2))];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class SceneStore {
|
|
22
|
+
constructor(dataDir) {
|
|
23
|
+
this.dir = path.join(dataDir, "memory", "l2");
|
|
24
|
+
ensureDir(this.dir);
|
|
25
|
+
this.file = path.join(this.dir, "scenes.json");
|
|
26
|
+
this.scenes = readJson(this.file, []);
|
|
27
|
+
// schema 版本迁移 (旁挂 .schema 文件; 数据文件保持纯数组)
|
|
28
|
+
const mig = migrateData({
|
|
29
|
+
file: this.file,
|
|
30
|
+
name: "scenes",
|
|
31
|
+
data: this.scenes,
|
|
32
|
+
currentVersion: SCENES_SCHEMA_VERSION,
|
|
33
|
+
});
|
|
34
|
+
this.scenes = mig.data;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 关键词命中数最高的场景。命中数 0 不会成为候选, 故返回的 scene 非空 ⟺ 至少命中 1 个关键词。
|
|
38
|
+
_bestScene(tokens) {
|
|
39
|
+
let best = null, bestScore = 0;
|
|
40
|
+
for (const s of this.scenes) {
|
|
41
|
+
let score = 0;
|
|
42
|
+
for (const t of tokens) if ((s.keywords || []).includes(t)) score++;
|
|
43
|
+
if (score > bestScore) { bestScore = score; best = s; }
|
|
44
|
+
}
|
|
45
|
+
return { scene: best, score: bestScore };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 把一条事实归入最匹配的场景 (或新建)
|
|
49
|
+
assign(fact) {
|
|
50
|
+
const tokens = tokenize(fact.content);
|
|
51
|
+
if (!tokens.length) return null;
|
|
52
|
+
|
|
53
|
+
let best = this._bestScene(tokens).scene;
|
|
54
|
+
|
|
55
|
+
if (best) {
|
|
56
|
+
best.facts.push({ id: fact.id, content: fact.content, ts: fact.created });
|
|
57
|
+
if (best.facts.length > 50) best.facts = best.facts.slice(-50);
|
|
58
|
+
best.lastUpdated = logicalDay();
|
|
59
|
+
// 合并新关键词
|
|
60
|
+
for (const t of tokens) if (!best.keywords.includes(t)) best.keywords.push(t);
|
|
61
|
+
if (best.keywords.length > 30) best.keywords = best.keywords.slice(-30);
|
|
62
|
+
} else {
|
|
63
|
+
best = {
|
|
64
|
+
id: "s_" + Math.random().toString(36).slice(2, 8),
|
|
65
|
+
name: tokens.slice(0, 3).join("·"),
|
|
66
|
+
keywords: tokens.slice(0, 10),
|
|
67
|
+
facts: [{ id: fact.id, content: fact.content, ts: fact.created }],
|
|
68
|
+
mode: "auto",
|
|
69
|
+
description: tokens.slice(1, 4).join("、") || "自动场景",
|
|
70
|
+
canHelp: "基于该话题的对话与记忆提供帮助",
|
|
71
|
+
created: logicalDay(),
|
|
72
|
+
lastUpdated: logicalDay(),
|
|
73
|
+
};
|
|
74
|
+
this.scenes.push(best);
|
|
75
|
+
}
|
|
76
|
+
// v1.0.9: 写盘加文件锁 (防军团多进程共享 dataDir 时写交错)
|
|
77
|
+
return withFileLock(this.file, () => {
|
|
78
|
+
writeJson(this.file, this.scenes);
|
|
79
|
+
writeSchema(this.file, "scenes", SCENES_SCHEMA_VERSION);
|
|
80
|
+
return best;
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 手动创建场景 (用户设定人设/能力, 类似灵魂文件)
|
|
85
|
+
create({ name, description, canHelp, keywords = [] }) {
|
|
86
|
+
const scene = {
|
|
87
|
+
id: "s_" + Math.random().toString(36).slice(2, 8),
|
|
88
|
+
name: String(name || "").slice(0, 50),
|
|
89
|
+
keywords: keywords.slice(0, 15),
|
|
90
|
+
facts: [],
|
|
91
|
+
mode: "manual",
|
|
92
|
+
description: String(description || "").slice(0, 300),
|
|
93
|
+
canHelp: String(canHelp || "").slice(0, 300),
|
|
94
|
+
created: logicalDay(),
|
|
95
|
+
lastUpdated: logicalDay(),
|
|
96
|
+
};
|
|
97
|
+
this.scenes.push(scene);
|
|
98
|
+
this._save();
|
|
99
|
+
return scene;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 列出所有场景 (含介绍)
|
|
103
|
+
listWithDesc() {
|
|
104
|
+
return this.scenes.map((s) => ({
|
|
105
|
+
id: s.id, name: s.name, mode: s.mode || "auto",
|
|
106
|
+
description: s.description || "", canHelp: s.canHelp || "",
|
|
107
|
+
facts: (s.facts || []).length, lastUpdated: s.lastUpdated,
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 按文本匹配激活场景 (关键词命中)
|
|
112
|
+
findMatch(text) {
|
|
113
|
+
const tokens = tokenize(text);
|
|
114
|
+
if (!tokens.length) return null;
|
|
115
|
+
return this._bestScene(tokens).scene;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 激活场景的上下文块 (人设 + 能力)
|
|
119
|
+
activeContext(text) {
|
|
120
|
+
const s = this.findMatch(text);
|
|
121
|
+
if (!s) return "";
|
|
122
|
+
return [
|
|
123
|
+
`【当前场景:${s.name}】`,
|
|
124
|
+
s.description ? `场景介绍: ${s.description}` : "",
|
|
125
|
+
s.canHelp ? `你可以帮用户: ${s.canHelp}` : "",
|
|
126
|
+
s.mode === "manual" ? "(用户手动设定, 请遵循此场景行为)" : "",
|
|
127
|
+
].filter(Boolean).join("\n");
|
|
128
|
+
} // 按记忆 id 找回场景
|
|
129
|
+
findByFactId(id) {
|
|
130
|
+
return this.scenes.find((s) => s.facts.some((f) => f.id === id));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
context(limit = 5) {
|
|
134
|
+
return this.scenes.slice(-limit).map((s) =>
|
|
135
|
+
`【场景:${s.name}】\n${s.facts.slice(-5).map((f) => ` - ${f.content}`).join("\n")}`
|
|
136
|
+
).join("\n");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
count() { return this.scenes.length; }
|
|
140
|
+
|
|
141
|
+
// v1.0.9: _save 加锁 (create/scene_describe 等写盘路径)
|
|
142
|
+
_save() { withFileLock(this.file, () => {
|
|
143
|
+
writeJson(this.file, this.scenes);
|
|
144
|
+
writeSchema(this.file, "scenes", SCENES_SCHEMA_VERSION);
|
|
145
|
+
}); }
|
|
131
146
|
}
|