Reorganize modules into layered directories

This commit is contained in:
Youzini-afk
2026-04-08 01:17:47 +08:00
parent 59942541ea
commit feec17f3e3
90 changed files with 284 additions and 219 deletions

240
retrieval/injector.js Normal file
View File

@@ -0,0 +1,240 @@
// ST-BME: Prompt 注入模块
// 将检索结果格式化为表格注入到 LLM 上下文中
import { getSchemaType } from "../graph/schema.js";
import { normalizeMemoryScope } from "../graph/memory-scope.js";
/**
* 将检索结果转换为注入文本
*
* @param {object} retrievalResult - retriever.retrieve() 的返回值
* @param {object[]} schema - 节点类型 Schema
* @returns {string} 注入文本
*/
export function formatInjection(retrievalResult, schema) {
const { coreNodes, recallNodes, groupedRecallNodes, scopeBuckets } =
retrievalResult;
const parts = [];
const appended = new Set();
if (scopeBuckets && typeof scopeBuckets === "object") {
appendScopeSection(
parts,
"[Memory - Character POV]",
scopeBuckets.characterPov,
schema,
appended,
);
appendScopeSection(
parts,
"[Memory - User POV / Not Character Facts]",
scopeBuckets.userPov,
schema,
appended,
"这些是用户/玩家侧主观记忆,不等于角色已知事实;只能作为关系、承诺、情绪和长期互动背景参考。",
);
appendScopeSection(
parts,
"[Memory - Objective / Current Region]",
scopeBuckets.objectiveCurrentRegion,
schema,
appended,
);
appendScopeSection(
parts,
"[Memory - Objective / Global]",
scopeBuckets.objectiveGlobal,
schema,
appended,
);
if (parts.length > 0) {
return parts.join("\n");
}
}
// ========== Core 常驻注入 ==========
if (coreNodes.length > 0) {
parts.push("[Memory - Core]");
const grouped = groupByType(coreNodes);
for (const [typeId, nodes] of grouped) {
const typeDef = getSchemaType(schema, typeId);
if (!typeDef) continue;
const table = formatTable(nodes, typeDef, appended);
if (table) parts.push(table);
}
}
// ========== Recall 召回注入 ==========
if (recallNodes.length > 0) {
parts.push("");
parts.push("[Memory - Recalled]");
const buckets = groupedRecallNodes || {
state: recallNodes.filter(
(n) => n.type === "character" || n.type === "location",
),
episodic: recallNodes.filter(
(n) => n.type === "event" || n.type === "thread",
),
reflective: recallNodes.filter(
(n) => n.type === "reflection" || n.type === "synopsis",
),
rule: recallNodes.filter((n) => n.type === "rule"),
other: recallNodes.filter(
(n) =>
![
"character",
"location",
"event",
"thread",
"reflection",
"synopsis",
"rule",
].includes(n.type),
),
};
appendBucket(parts, "当前状态记忆", buckets.state, schema, appended);
appendBucket(parts, "情景事件记忆", buckets.episodic, schema, appended);
appendBucket(parts, "反思与长期锚点", buckets.reflective, schema, appended);
appendBucket(parts, "规则与约束", buckets.rule, schema, appended);
appendBucket(parts, "其他关联记忆", buckets.other, schema, appended);
}
return parts.join("\n");
}
function appendScopeSection(parts, title, nodes, schema, appended, note = "") {
if (!Array.isArray(nodes) || nodes.length === 0) return;
if (parts.length > 0) {
parts.push("");
}
parts.push(title);
if (note) {
parts.push(note);
}
const grouped = groupByType(nodes);
for (const [typeId, groupedNodes] of grouped) {
const typeDef = getSchemaType(schema, typeId);
if (!typeDef) continue;
const table = formatTable(groupedNodes, typeDef, appended);
if (table) parts.push(table);
}
}
/**
* 按类型分组节点
*/
function groupByType(nodes) {
const map = new Map();
for (const node of nodes) {
if (!map.has(node.type)) map.set(node.type, []);
map.get(node.type).push(node);
}
return map;
}
function appendBucket(parts, title, nodes, schema, appended) {
if (!nodes || nodes.length === 0) return;
parts.push(`## ${title}`);
const grouped = groupByType(nodes);
for (const [typeId, groupedNodes] of grouped) {
const typeDef = getSchemaType(schema, typeId);
if (!typeDef) continue;
const table = formatTable(groupedNodes, typeDef, appended);
if (table) parts.push(table);
}
}
/**
* 将同类型节点格式化为 Markdown 表格
*/
function formatTable(nodes, typeDef, appended = new Set()) {
if (!Array.isArray(nodes) || nodes.length === 0) return "";
const uniqueNodes = nodes.filter((node) => {
if (!node?.id || appended.has(node.id)) return false;
appended.add(node.id);
return true;
});
if (uniqueNodes.length === 0) return "";
// 确定要展示的列(有实际数据的列)
const activeCols = typeDef.columns.filter((col) =>
uniqueNodes.some(
(n) => n.fields?.[col.name] != null && n.fields[col.name] !== "",
),
);
const derivedCols = buildDerivedColumns(uniqueNodes, typeDef);
const allCols = [...derivedCols, ...activeCols];
if (allCols.length === 0) return "";
// 表头
const header = `| ${allCols.map((c) => c.name).join(" | ")} |`;
const separator = `| ${allCols.map(() => "---").join(" | ")} |`;
// 数据行
const rows = uniqueNodes.map((node) => {
const cells = allCols.map((col) => {
const val =
typeof col.getValue === "function"
? col.getValue(node)
: node.fields?.[col.name] ?? "";
// 转义管道符,限制单元格长度
return String(val)
.replace(/\|/g, "\\|")
.replace(/\n/g, " ")
.slice(0, 200);
});
return `| ${cells.join(" | ")} |`;
});
return `${typeDef.tableName}:\n${header}\n${separator}\n${rows.join("\n")}`;
}
function buildDerivedColumns(nodes, typeDef) {
if (typeDef?.id !== "pov_memory") {
return [];
}
return [
{
name: "owner",
getValue(node) {
const scope = normalizeMemoryScope(node?.scope);
const ownerLabel = scope.ownerName || scope.ownerId || "未命名";
if (scope.ownerType === "user") {
return `用户: ${ownerLabel}`;
}
if (scope.ownerType === "character") {
return `角色: ${ownerLabel}`;
}
return `POV: ${ownerLabel}`;
},
},
];
}
/**
* 获取注入提示词的总 token 估算
* 粗略估算1 个 token ≈ 2 个中文字符 或 4 个英文字符
*
* @param {string} injectionText
* @returns {number} 估算 token 数
*/
export function estimateTokens(injectionText) {
if (!injectionText) return 0;
// 简单估算:中文 2 字符/token英文 4 字符/token
const cnChars = (injectionText.match(/[\u4e00-\u9fff]/g) || []).length;
const otherChars = injectionText.length - cnChars;
return Math.ceil(cnChars / 2 + otherChars / 4);
}