dsh-tdai-memory 0.3.2 → 0.3.4
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 +21 -21
- package/README.md +15 -19
- package/README.zh.md +7 -1
- package/THIRD-PARTY-NOTICES.md +20 -20
- package/client.js +2 -2
- package/cordis.patch.yml +8 -8
- package/index.js +13 -22
- package/package.json +94 -78
- package/recall-inject.js +86 -86
- package/vendor/tdai/adapters/index.js +15 -15
- package/vendor/tdai/adapters/openclaw/host-adapter.js +83 -83
- package/vendor/tdai/adapters/openclaw/index.js +5 -5
- package/vendor/tdai/adapters/openclaw/llm-runner.js +69 -69
- package/vendor/tdai/adapters/standalone/host-adapter.js +58 -58
- package/vendor/tdai/adapters/standalone/index.js +5 -5
- package/vendor/tdai/core/conversation/l0-recorder.js +420 -420
- package/vendor/tdai/core/hooks/auto-capture.js +236 -236
- package/vendor/tdai/core/hooks/auto-recall.js +640 -640
- package/vendor/tdai/core/index.js +8 -8
- package/vendor/tdai/core/persona/persona-generator.js +184 -184
- package/vendor/tdai/core/persona/persona-trigger.js +94 -94
- package/vendor/tdai/core/profile/profile-sync.js +200 -200
- package/vendor/tdai/core/prompts/l1-dedup.js +72 -72
- package/vendor/tdai/core/prompts/l1-extraction.js +34 -34
- package/vendor/tdai/core/prompts/persona-generation.js +35 -35
- package/vendor/tdai/core/prompts/scene-extraction.js +45 -45
- package/vendor/tdai/core/record/l1-dedup.js +318 -318
- package/vendor/tdai/core/record/l1-extractor.js +379 -379
- package/vendor/tdai/core/record/l1-reader.js +178 -178
- package/vendor/tdai/core/record/l1-writer.js +141 -141
- package/vendor/tdai/core/report/reporter.js +86 -86
- package/vendor/tdai/core/scene/filename-normalizer.js +161 -161
- package/vendor/tdai/core/scene/scene-extractor.js +410 -410
- package/vendor/tdai/core/scene/scene-format.js +53 -53
- package/vendor/tdai/core/scene/scene-index.js +82 -82
- package/vendor/tdai/core/scene/scene-navigation.js +57 -57
- package/vendor/tdai/core/seed/input.js +404 -404
- package/vendor/tdai/core/seed/types.js +9 -9
- package/vendor/tdai/core/store/bm25-client.js +117 -117
- package/vendor/tdai/core/store/bm25-local.js +68 -68
- package/vendor/tdai/core/store/embedding.js +632 -632
- package/vendor/tdai/core/store/factory.js +95 -95
- package/vendor/tdai/core/store/search-utils.js +54 -54
- package/vendor/tdai/core/store/sqlite.js +1632 -1632
- package/vendor/tdai/core/store/tcvdb-client.js +221 -221
- package/vendor/tdai/core/store/tcvdb.js +1047 -1047
- package/vendor/tdai/core/store/types.js +17 -17
- package/vendor/tdai/core/tools/conversation-search.js +199 -199
- package/vendor/tdai/core/tools/memory-search.js +207 -207
- package/vendor/tdai/core/types.js +12 -12
- package/vendor/tdai/utils/backup.js +189 -189
- package/vendor/tdai/utils/checkpoint.js +360 -360
- package/vendor/tdai/utils/clean-context-runner.js +472 -472
- package/vendor/tdai/utils/ensure-hook-policy.js +213 -213
- package/vendor/tdai/utils/env.js +13 -13
- package/vendor/tdai/utils/managed-timer.js +127 -127
- package/vendor/tdai/utils/manifest.js +91 -91
- package/vendor/tdai/utils/memory-cleaner.js +300 -300
- package/vendor/tdai/utils/no-think-fetch.js +109 -109
- package/vendor/tdai/utils/openclaw-state-dir.js +26 -26
- package/vendor/tdai/utils/pipeline-factory.js +517 -517
- package/vendor/tdai/utils/pipeline-manager.js +892 -892
- package/vendor/tdai/utils/sanitize.js +360 -360
- package/vendor/tdai/utils/serial-queue.js +103 -103
- package/vendor/tdai/utils/session-filter.js +88 -88
- package/vendor/tdai/utils/text-utils.js +29 -29
- package/vendor/tdai/utils/time.js +252 -252
|
@@ -1,53 +1,53 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Scene Block file format: parse and format the META-delimited Markdown files.
|
|
3
|
-
*/
|
|
4
|
-
const META_START = "-----META-START-----";
|
|
5
|
-
const META_END = "-----META-END-----";
|
|
6
|
-
/**
|
|
7
|
-
* Parse a Scene Block file into structured data.
|
|
8
|
-
*/
|
|
9
|
-
export function parseSceneBlock(raw, filename) {
|
|
10
|
-
const startIdx = raw.indexOf(META_START);
|
|
11
|
-
const endIdx = raw.indexOf(META_END);
|
|
12
|
-
if (startIdx === -1 || endIdx === -1) {
|
|
13
|
-
// No META section — treat entire file as content
|
|
14
|
-
return {
|
|
15
|
-
filename,
|
|
16
|
-
meta: { created: "", updated: "", summary: "", heat: 0 },
|
|
17
|
-
content: raw.trim(),
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
const metaBlock = raw.slice(startIdx + META_START.length, endIdx).trim();
|
|
21
|
-
const content = raw.slice(endIdx + META_END.length).trim();
|
|
22
|
-
const meta = {
|
|
23
|
-
created: extractMetaField(metaBlock, "created"),
|
|
24
|
-
updated: extractMetaField(metaBlock, "updated"),
|
|
25
|
-
summary: extractMetaField(metaBlock, "summary"),
|
|
26
|
-
heat: parseInt(extractMetaField(metaBlock, "heat"), 10) || 0,
|
|
27
|
-
};
|
|
28
|
-
return { filename, meta, content };
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Format a Scene Block back into file content.
|
|
32
|
-
*/
|
|
33
|
-
export function formatSceneBlock(meta, content) {
|
|
34
|
-
return `${formatMeta(meta)}\n\n${content}`;
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Format the META section.
|
|
38
|
-
*/
|
|
39
|
-
export function formatMeta(meta) {
|
|
40
|
-
return [
|
|
41
|
-
META_START,
|
|
42
|
-
`created: ${meta.created}`,
|
|
43
|
-
`updated: ${meta.updated}`,
|
|
44
|
-
`summary: ${meta.summary}`,
|
|
45
|
-
`heat: ${meta.heat}`,
|
|
46
|
-
META_END,
|
|
47
|
-
].join("\n");
|
|
48
|
-
}
|
|
49
|
-
function extractMetaField(metaBlock, field) {
|
|
50
|
-
const re = new RegExp(`^${field}:\\s*(.*)$`, "m");
|
|
51
|
-
const m = metaBlock.match(re);
|
|
52
|
-
return m ? m[1].trim() : "";
|
|
53
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Scene Block file format: parse and format the META-delimited Markdown files.
|
|
3
|
+
*/
|
|
4
|
+
const META_START = "-----META-START-----";
|
|
5
|
+
const META_END = "-----META-END-----";
|
|
6
|
+
/**
|
|
7
|
+
* Parse a Scene Block file into structured data.
|
|
8
|
+
*/
|
|
9
|
+
export function parseSceneBlock(raw, filename) {
|
|
10
|
+
const startIdx = raw.indexOf(META_START);
|
|
11
|
+
const endIdx = raw.indexOf(META_END);
|
|
12
|
+
if (startIdx === -1 || endIdx === -1) {
|
|
13
|
+
// No META section — treat entire file as content
|
|
14
|
+
return {
|
|
15
|
+
filename,
|
|
16
|
+
meta: { created: "", updated: "", summary: "", heat: 0 },
|
|
17
|
+
content: raw.trim(),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
const metaBlock = raw.slice(startIdx + META_START.length, endIdx).trim();
|
|
21
|
+
const content = raw.slice(endIdx + META_END.length).trim();
|
|
22
|
+
const meta = {
|
|
23
|
+
created: extractMetaField(metaBlock, "created"),
|
|
24
|
+
updated: extractMetaField(metaBlock, "updated"),
|
|
25
|
+
summary: extractMetaField(metaBlock, "summary"),
|
|
26
|
+
heat: parseInt(extractMetaField(metaBlock, "heat"), 10) || 0,
|
|
27
|
+
};
|
|
28
|
+
return { filename, meta, content };
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Format a Scene Block back into file content.
|
|
32
|
+
*/
|
|
33
|
+
export function formatSceneBlock(meta, content) {
|
|
34
|
+
return `${formatMeta(meta)}\n\n${content}`;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Format the META section.
|
|
38
|
+
*/
|
|
39
|
+
export function formatMeta(meta) {
|
|
40
|
+
return [
|
|
41
|
+
META_START,
|
|
42
|
+
`created: ${meta.created}`,
|
|
43
|
+
`updated: ${meta.updated}`,
|
|
44
|
+
`summary: ${meta.summary}`,
|
|
45
|
+
`heat: ${meta.heat}`,
|
|
46
|
+
META_END,
|
|
47
|
+
].join("\n");
|
|
48
|
+
}
|
|
49
|
+
function extractMetaField(metaBlock, field) {
|
|
50
|
+
const re = new RegExp(`^${field}:\\s*(.*)$`, "m");
|
|
51
|
+
const m = metaBlock.match(re);
|
|
52
|
+
return m ? m[1].trim() : "";
|
|
53
|
+
}
|
|
@@ -1,82 +1,82 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Scene Index: maintains a JSON index of all scene blocks for quick lookup.
|
|
3
|
-
*/
|
|
4
|
-
import fs from "node:fs/promises";
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
import { parseSceneBlock } from "./scene-format.js";
|
|
7
|
-
/**
|
|
8
|
-
* Read the scene index from disk.
|
|
9
|
-
*
|
|
10
|
-
* The index is written exclusively by syncSceneIndex() (engineering side).
|
|
11
|
-
* The LLM is sandboxed to scene_blocks/ and cannot access this file.
|
|
12
|
-
*/
|
|
13
|
-
export async function readSceneIndex(dataDir) {
|
|
14
|
-
const indexPath = path.join(dataDir, ".metadata", "scene_index.json");
|
|
15
|
-
try {
|
|
16
|
-
const raw = await fs.readFile(indexPath, "utf-8");
|
|
17
|
-
const parsed = JSON.parse(raw);
|
|
18
|
-
if (!Array.isArray(parsed))
|
|
19
|
-
return [];
|
|
20
|
-
const entries = [];
|
|
21
|
-
for (const item of parsed) {
|
|
22
|
-
if (!item || typeof item !== "object")
|
|
23
|
-
continue;
|
|
24
|
-
const filename = typeof item.filename === "string" ? item.filename : "";
|
|
25
|
-
if (!filename)
|
|
26
|
-
continue;
|
|
27
|
-
entries.push({
|
|
28
|
-
filename,
|
|
29
|
-
summary: typeof item.summary === "string" ? item.summary : "",
|
|
30
|
-
heat: typeof item.heat === "number" ? item.heat : 0,
|
|
31
|
-
created: typeof item.created === "string" ? item.created : "",
|
|
32
|
-
updated: typeof item.updated === "string" ? item.updated : "",
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
return entries;
|
|
36
|
-
}
|
|
37
|
-
catch {
|
|
38
|
-
return [];
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* Write the scene index to disk.
|
|
43
|
-
*/
|
|
44
|
-
export async function writeSceneIndex(dataDir, entries) {
|
|
45
|
-
const indexPath = path.join(dataDir, ".metadata", "scene_index.json");
|
|
46
|
-
await fs.mkdir(path.dirname(indexPath), { recursive: true });
|
|
47
|
-
await fs.writeFile(indexPath, JSON.stringify(entries, null, 2), "utf-8");
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Rebuild scene index by scanning all .md files in the scene_blocks directory.
|
|
51
|
-
*/
|
|
52
|
-
export async function syncSceneIndex(dataDir) {
|
|
53
|
-
const blocksDir = path.join(dataDir, "scene_blocks");
|
|
54
|
-
let files;
|
|
55
|
-
try {
|
|
56
|
-
files = (await fs.readdir(blocksDir)).filter((f) => f.endsWith(".md"));
|
|
57
|
-
}
|
|
58
|
-
catch {
|
|
59
|
-
files = [];
|
|
60
|
-
}
|
|
61
|
-
const entries = [];
|
|
62
|
-
for (const file of files) {
|
|
63
|
-
try {
|
|
64
|
-
const raw = await fs.readFile(path.join(blocksDir, file), "utf-8");
|
|
65
|
-
const block = parseSceneBlock(raw, file);
|
|
66
|
-
entries.push({
|
|
67
|
-
filename: file,
|
|
68
|
-
summary: block.meta.summary,
|
|
69
|
-
heat: block.meta.heat,
|
|
70
|
-
created: block.meta.created,
|
|
71
|
-
updated: block.meta.updated,
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
catch {
|
|
75
|
-
// File may have been deleted between readdir and readFile (e.g. by concurrent
|
|
76
|
-
// SceneExtractor soft-delete). Skip it and continue syncing the rest.
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
await writeSceneIndex(dataDir, entries);
|
|
81
|
-
return entries;
|
|
82
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Scene Index: maintains a JSON index of all scene blocks for quick lookup.
|
|
3
|
+
*/
|
|
4
|
+
import fs from "node:fs/promises";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { parseSceneBlock } from "./scene-format.js";
|
|
7
|
+
/**
|
|
8
|
+
* Read the scene index from disk.
|
|
9
|
+
*
|
|
10
|
+
* The index is written exclusively by syncSceneIndex() (engineering side).
|
|
11
|
+
* The LLM is sandboxed to scene_blocks/ and cannot access this file.
|
|
12
|
+
*/
|
|
13
|
+
export async function readSceneIndex(dataDir) {
|
|
14
|
+
const indexPath = path.join(dataDir, ".metadata", "scene_index.json");
|
|
15
|
+
try {
|
|
16
|
+
const raw = await fs.readFile(indexPath, "utf-8");
|
|
17
|
+
const parsed = JSON.parse(raw);
|
|
18
|
+
if (!Array.isArray(parsed))
|
|
19
|
+
return [];
|
|
20
|
+
const entries = [];
|
|
21
|
+
for (const item of parsed) {
|
|
22
|
+
if (!item || typeof item !== "object")
|
|
23
|
+
continue;
|
|
24
|
+
const filename = typeof item.filename === "string" ? item.filename : "";
|
|
25
|
+
if (!filename)
|
|
26
|
+
continue;
|
|
27
|
+
entries.push({
|
|
28
|
+
filename,
|
|
29
|
+
summary: typeof item.summary === "string" ? item.summary : "",
|
|
30
|
+
heat: typeof item.heat === "number" ? item.heat : 0,
|
|
31
|
+
created: typeof item.created === "string" ? item.created : "",
|
|
32
|
+
updated: typeof item.updated === "string" ? item.updated : "",
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
return entries;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Write the scene index to disk.
|
|
43
|
+
*/
|
|
44
|
+
export async function writeSceneIndex(dataDir, entries) {
|
|
45
|
+
const indexPath = path.join(dataDir, ".metadata", "scene_index.json");
|
|
46
|
+
await fs.mkdir(path.dirname(indexPath), { recursive: true });
|
|
47
|
+
await fs.writeFile(indexPath, JSON.stringify(entries, null, 2), "utf-8");
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Rebuild scene index by scanning all .md files in the scene_blocks directory.
|
|
51
|
+
*/
|
|
52
|
+
export async function syncSceneIndex(dataDir) {
|
|
53
|
+
const blocksDir = path.join(dataDir, "scene_blocks");
|
|
54
|
+
let files;
|
|
55
|
+
try {
|
|
56
|
+
files = (await fs.readdir(blocksDir)).filter((f) => f.endsWith(".md"));
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
files = [];
|
|
60
|
+
}
|
|
61
|
+
const entries = [];
|
|
62
|
+
for (const file of files) {
|
|
63
|
+
try {
|
|
64
|
+
const raw = await fs.readFile(path.join(blocksDir, file), "utf-8");
|
|
65
|
+
const block = parseSceneBlock(raw, file);
|
|
66
|
+
entries.push({
|
|
67
|
+
filename: file,
|
|
68
|
+
summary: block.meta.summary,
|
|
69
|
+
heat: block.meta.heat,
|
|
70
|
+
created: block.meta.created,
|
|
71
|
+
updated: block.meta.updated,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// File may have been deleted between readdir and readFile (e.g. by concurrent
|
|
76
|
+
// SceneExtractor soft-delete). Skip it and continue syncing the rest.
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
await writeSceneIndex(dataDir, entries);
|
|
81
|
+
return entries;
|
|
82
|
+
}
|
|
@@ -1,60 +1,60 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Scene navigation: generates a summary navigation section appended to persona.md.
|
|
3
|
-
*
|
|
4
|
-
* The navigation includes **absolute** file paths so the agent can directly
|
|
5
|
-
* use read_file for on-demand scene loading (progressive disclosure).
|
|
6
|
-
*/
|
|
7
|
-
import path from "node:path";
|
|
8
|
-
const NAV_HEADER = "---\n## 🗺️ Scene Navigation (Scene Index)";
|
|
1
|
+
/**
|
|
2
|
+
* Scene navigation: generates a summary navigation section appended to persona.md.
|
|
3
|
+
*
|
|
4
|
+
* The navigation includes **absolute** file paths so the agent can directly
|
|
5
|
+
* use read_file for on-demand scene loading (progressive disclosure).
|
|
6
|
+
*/
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
const NAV_HEADER = "---\n## 🗺️ Scene Navigation (Scene Index)";
|
|
9
9
|
const NAV_FOOTER = `📌 使用说明:
|
|
10
10
|
- Path 是 scene block 的绝对路径,可直接使用 read_file 读取完整内容
|
|
11
11
|
- 热度:该场景被记忆命中的累计次数,越高越重要
|
|
12
|
-
- Summary:场景的核心要点摘要`;
|
|
13
|
-
/**
|
|
14
|
-
* Build a fire-emoji string based on heat value (visual priority cue for the agent).
|
|
15
|
-
*/
|
|
16
|
-
function heatEmoji(heat) {
|
|
17
|
-
if (heat >= 1000)
|
|
18
|
-
return " 🔥🔥🔥🔥🔥";
|
|
19
|
-
if (heat >= 500)
|
|
20
|
-
return " 🔥🔥🔥🔥";
|
|
21
|
-
if (heat >= 200)
|
|
22
|
-
return " 🔥🔥🔥";
|
|
23
|
-
if (heat >= 100)
|
|
24
|
-
return " 🔥🔥";
|
|
25
|
-
if (heat >= 50)
|
|
26
|
-
return " 🔥";
|
|
27
|
-
return "";
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* Generate the scene navigation Markdown section.
|
|
31
|
-
*
|
|
32
|
-
* @param entries - Scene index entries
|
|
33
|
-
* @param dataDir - Absolute path to the plugin data directory; when provided,
|
|
34
|
-
* scene paths are rendered as absolute paths so the agent can
|
|
35
|
-
* call read_file directly without path concatenation.
|
|
36
|
-
*/
|
|
37
|
-
export function generateSceneNavigation(entries, dataDir) {
|
|
38
|
-
if (entries.length === 0)
|
|
39
|
-
return "";
|
|
40
|
-
const sorted = [...entries].sort((a, b) => b.heat - a.heat);
|
|
41
|
-
const blocks = sorted.map((e) => {
|
|
42
|
-
const scenePath = dataDir
|
|
43
|
-
? path.join(dataDir, "scene_blocks", e.filename)
|
|
44
|
-
: `scene_blocks/${e.filename}`;
|
|
45
|
-
const pathLine = `### Path: ${scenePath}`;
|
|
46
|
-
const heatLine = `**热度**: ${e.heat}${heatEmoji(e.heat)}${e.updated ? ` | **更新**: ${e.updated}` : ""}`;
|
|
47
|
-
const summaryLine = `Summary: ${e.summary}`;
|
|
48
|
-
return `${pathLine}\n${heatLine}\n${summaryLine}`;
|
|
49
|
-
});
|
|
50
|
-
return `${NAV_HEADER}\n*以下是当前场景记忆的索引,可根据需要 read_file 读取详细内容。*\n\n${blocks.join("\n\n")}\n\n${NAV_FOOTER}`;
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* Strip the scene navigation section from persona content.
|
|
54
|
-
*/
|
|
55
|
-
export function stripSceneNavigation(personaContent) {
|
|
56
|
-
const idx = personaContent.indexOf(NAV_HEADER);
|
|
57
|
-
if (idx === -1)
|
|
58
|
-
return personaContent;
|
|
59
|
-
return personaContent.slice(0, idx).trimEnd();
|
|
60
|
-
}
|
|
12
|
+
- Summary:场景的核心要点摘要`;
|
|
13
|
+
/**
|
|
14
|
+
* Build a fire-emoji string based on heat value (visual priority cue for the agent).
|
|
15
|
+
*/
|
|
16
|
+
function heatEmoji(heat) {
|
|
17
|
+
if (heat >= 1000)
|
|
18
|
+
return " 🔥🔥🔥🔥🔥";
|
|
19
|
+
if (heat >= 500)
|
|
20
|
+
return " 🔥🔥🔥🔥";
|
|
21
|
+
if (heat >= 200)
|
|
22
|
+
return " 🔥🔥🔥";
|
|
23
|
+
if (heat >= 100)
|
|
24
|
+
return " 🔥🔥";
|
|
25
|
+
if (heat >= 50)
|
|
26
|
+
return " 🔥";
|
|
27
|
+
return "";
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Generate the scene navigation Markdown section.
|
|
31
|
+
*
|
|
32
|
+
* @param entries - Scene index entries
|
|
33
|
+
* @param dataDir - Absolute path to the plugin data directory; when provided,
|
|
34
|
+
* scene paths are rendered as absolute paths so the agent can
|
|
35
|
+
* call read_file directly without path concatenation.
|
|
36
|
+
*/
|
|
37
|
+
export function generateSceneNavigation(entries, dataDir) {
|
|
38
|
+
if (entries.length === 0)
|
|
39
|
+
return "";
|
|
40
|
+
const sorted = [...entries].sort((a, b) => b.heat - a.heat);
|
|
41
|
+
const blocks = sorted.map((e) => {
|
|
42
|
+
const scenePath = dataDir
|
|
43
|
+
? path.join(dataDir, "scene_blocks", e.filename)
|
|
44
|
+
: `scene_blocks/${e.filename}`;
|
|
45
|
+
const pathLine = `### Path: ${scenePath}`;
|
|
46
|
+
const heatLine = `**热度**: ${e.heat}${heatEmoji(e.heat)}${e.updated ? ` | **更新**: ${e.updated}` : ""}`;
|
|
47
|
+
const summaryLine = `Summary: ${e.summary}`;
|
|
48
|
+
return `${pathLine}\n${heatLine}\n${summaryLine}`;
|
|
49
|
+
});
|
|
50
|
+
return `${NAV_HEADER}\n*以下是当前场景记忆的索引,可根据需要 read_file 读取详细内容。*\n\n${blocks.join("\n\n")}\n\n${NAV_FOOTER}`;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Strip the scene navigation section from persona content.
|
|
54
|
+
*/
|
|
55
|
+
export function stripSceneNavigation(personaContent) {
|
|
56
|
+
const idx = personaContent.indexOf(NAV_HEADER);
|
|
57
|
+
if (idx === -1)
|
|
58
|
+
return personaContent;
|
|
59
|
+
return personaContent.slice(0, idx).trimEnd();
|
|
60
|
+
}
|