refactor(role): 將角色記憶工具改寫為 Node.js

This commit is contained in:
Jeffery
2026-07-28 12:30:28 +08:00
parent e71f165e9a
commit fec3e1d0cb
8 changed files with 1090 additions and 1125 deletions
+727
View File
@@ -0,0 +1,727 @@
#!/usr/bin/env node
// ==============================================================================
// 用途:角色記憶(.memory/<角色>/)的儲存引擎。負責 (1) 把每輪對話濃縮結果寫入
// inbox(2) 產生 SessionStart 要注入的記憶區塊,(3) 睡眠整理時輸出待整理
// 素材並套用整理結果(NREM 鞏固/REM 整合、分類、去重、標籤、總結、
// 優先度、關聯、壓縮歸檔),(4) 依使用頻率與優先度遺忘日常與其他類記憶。
// 更新時間:2026/07/28 12:21:11
// 相依:Node.js 標準庫。
// 退出碼:0 成功;1 無內容可處理;2 參數錯誤。呼叫端(hook)一律不得因此中斷。
// ==============================================================================
const fs = require("fs");
const path = require("path");
const crypto = require("crypto");
const zlib = require("zlib");
const CATEGORIES = ["important", "interest", "news", "skill", "daily", "other"];
const CATEGORY_LABELS = {
important: "重要",
interest: "興趣",
news: "新知",
skill: "技能",
daily: "日常",
other: "其他",
};
const LABEL_TO_CATEGORY = Object.fromEntries(Object.entries(CATEGORY_LABELS).map(([key, value]) => [value, key]));
const FULL_CATEGORIES = ["important", "interest"];
const DIGEST_CATEGORIES = ["skill", "news", "daily", "other"];
const FORGET_RULES = { daily: [14, 1], other: [7, 1] };
const DEFAULT_PRIORITY = { important: 5, interest: 4, skill: 4, news: 3, daily: 2, other: 1 };
const SLEEP_BATCH = 60;
const COLLECT_LIMIT = 12000;
const EXISTING_INDEX_LIMIT = 120;
const CONTENT_LIMIT = 1200;
const DEFAULT_LOAD_LIMIT = 4000;
const DEFAULT_FULL_MIN_PRIORITY = 4;
const DEFAULT_DIGEST_MIN_PRIORITY = 3;
function readStdin() {
try {
return fs.readFileSync(0, "utf8");
} catch {
return "";
}
}
function pad(value) {
return String(value).padStart(2, "0");
}
function taipeiDate() {
return new Date(Date.now() + 8 * 60 * 60 * 1000);
}
function nowStamp() {
const d = taipeiDate();
return `${d.getUTCFullYear()}/${pad(d.getUTCMonth() + 1)}/${pad(d.getUTCDate())} ${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())}`;
}
function nowEpoch() {
return Math.floor(Date.now() / 1000);
}
function parseStamp(value) {
if (typeof value !== "string" || !value.trim()) return null;
const m = value.trim().match(/^(\d{4})\/(\d{2})\/(\d{2}) (\d{2}):(\d{2}):(\d{2})$/);
if (!m) return null;
const [, y, mo, d, h, mi, s] = m.map(Number);
const ms = Date.UTC(y, mo - 1, d, h - 8, mi, s);
return Number.isNaN(ms) ? null : new Date(ms);
}
function memoryRoot(role) {
const base = process.env.ROLE_MEMORY_HOME || path.join(process.env.HOME || "", ".memory");
return path.join(base, role);
}
function ensureLayout(role) {
const root = memoryRoot(role);
for (const sub of ["inbox", "archive/raw", "archive/forgotten", ...CATEGORIES]) {
fs.mkdirSync(path.join(root, sub), { recursive: true });
}
return root;
}
function statePath(role) {
return path.join(memoryRoot(role), "state.json");
}
function readState(role) {
try {
const data = JSON.parse(fs.readFileSync(statePath(role), "utf8"));
return data && typeof data === "object" && !Array.isArray(data) ? data : {};
} catch {
return {};
}
}
function writeState(role, patch) {
const state = { ...readState(role), ...patch };
ensureLayout(role);
fs.writeFileSync(statePath(role), JSON.stringify(state, null, 2), "utf8");
return state;
}
function normalizeCategory(value) {
const raw = String(value || "").trim().toLowerCase();
if (CATEGORIES.includes(raw)) return raw;
return LABEL_TO_CATEGORY[String(value || "").trim()] || "other";
}
function uniquePush(items, value, fold = false) {
const text = String(value || "").trim().replace(/^[#\[]+|[\]]+$/g, "").trim();
if (!text) return;
const exists = fold
? items.some((item) => item.toLowerCase() === text.toLowerCase())
: items.includes(text);
if (!exists) items.push(text);
}
function normalizeTags(value) {
const parts = Array.isArray(value) ? value.map(String) : typeof value === "string" ? value.split(/[,、|]/) : [];
const tags = [];
for (const part of parts) uniquePush(tags, part, true);
return tags.slice(0, 6);
}
function normalizeList(value, limit = 8) {
const parts = Array.isArray(value) ? value.map(String) : typeof value === "string" ? value.split(/[,、|]/) : [];
const items = [];
for (const part of parts) uniquePush(items, part, false);
return items.slice(0, limit);
}
function normalizePriority(value, category = "other") {
const fallback = DEFAULT_PRIORITY[normalizeCategory(category)] || 1;
const parsed = Number.parseInt(String(value ?? "").trim(), 10);
const priority = Number.isFinite(parsed) ? parsed : fallback;
return Math.max(1, Math.min(5, priority));
}
function oneLine(value, limit = 120) {
return String(value || "").replace(/\s+/g, " ").trim().slice(0, limit);
}
function dumpMemory(meta, content) {
const lines = ["---"];
for (const key of [
"id",
"category",
"summary",
"tags",
"priority",
"relevance",
"links",
"sleep_stage",
"created",
"updated",
"last_replayed",
"hits",
"sources",
]) {
if (!Object.prototype.hasOwnProperty.call(meta, key)) continue;
let value = meta[key];
if (Array.isArray(value)) value = `[${value.map(String).join(", ")}]`;
lines.push(`${key}: ${value}`);
}
lines.push("---", "", String(content || "").trim(), "");
return lines.join("\n");
}
function loadMemory(filePath) {
let raw;
try {
raw = fs.readFileSync(filePath, "utf8");
} catch {
return [null, ""];
}
const meta = { path: filePath, hits: 0, tags: [] };
let body = raw;
if (raw.startsWith("---")) {
const parts = raw.split("---");
if (parts.length >= 3) {
body = parts.slice(2).join("---");
for (const line of parts[1].split(/\r?\n/)) {
if (!line.includes(":")) continue;
const idx = line.indexOf(":");
const key = line.slice(0, idx).trim();
const value = line.slice(idx + 1).trim();
if (key === "tags" || key === "sources") meta[key] = normalizeTags(value.replace(/^\[|\]$/g, ""));
else if (key === "relevance" || key === "links") meta[key] = normalizeList(value.replace(/^\[|\]$/g, ""));
else if (key === "hits" || key === "priority") meta[key] = /^\d+$/.test(value) ? Number.parseInt(value, 10) : 0;
else meta[key] = value;
}
}
}
meta.id ||= path.basename(filePath, ".md");
meta.summary ||= "";
meta.created ||= "";
meta.updated ||= meta.created || "";
meta.relevance ||= [];
meta.links ||= [];
meta.priority = normalizePriority(meta.priority, meta.category || "other");
return [meta, body.trim()];
}
function newId(seed) {
const digest = crypto.createHash("sha1").update(seed, "utf8").digest("hex").slice(0, 6);
const d = taipeiDate();
const stamp = `${d.getUTCFullYear()}${pad(d.getUTCMonth() + 1)}${pad(d.getUTCDate())}-${pad(d.getUTCHours())}${pad(d.getUTCMinutes())}${pad(d.getUTCSeconds())}`;
return `${stamp}-${digest}`;
}
function listMemories(role, category) {
const directory = path.join(memoryRoot(role), category);
if (!fs.existsSync(directory)) return [];
const items = [];
for (const name of fs.readdirSync(directory).sort()) {
if (!name.endsWith(".md")) continue;
const [meta, content] = loadMemory(path.join(directory, name));
if (!meta) continue;
meta.category = category;
items.push([meta, content]);
}
items.sort((a, b) => {
const am = a[0];
const bm = b[0];
const av = [normalizePriority(am.priority, category), am.links?.length ? 1 : 0, am.updated || ""];
const bv = [normalizePriority(bm.priority, category), bm.links?.length ? 1 : 0, bm.updated || ""];
for (let i = 0; i < av.length; i += 1) {
if (av[i] < bv[i]) return 1;
if (av[i] > bv[i]) return -1;
}
return 0;
});
return items;
}
function listInbox(role) {
const directory = path.join(memoryRoot(role), "inbox");
if (!fs.existsSync(directory)) return [];
const items = [];
for (const name of fs.readdirSync(directory).sort()) {
if (!name.endsWith(".md")) continue;
const [meta, content] = loadMemory(path.join(directory, name));
if (meta) items.push([meta, content]);
}
return items;
}
function findMemory(role, memoryId) {
for (const category of CATEGORIES) {
const filePath = path.join(memoryRoot(role), category, `${memoryId}.md`);
if (!fs.existsSync(filePath)) continue;
const [meta, content] = loadMemory(filePath);
if (meta) {
meta.category = category;
return [meta, content];
}
}
return [null, ""];
}
function memoryHint(meta) {
const relevance = (meta.relevance || []).join("、") || "-";
const links = (meta.links || []).join("、") || "-";
return `優先度:${normalizePriority(meta.priority, meta.category)};關聯:${relevance};連結:${links}`;
}
function archiveFile(filePath, destinationDir) {
fs.mkdirSync(destinationDir, { recursive: true });
const target = path.join(destinationDir, `${path.basename(filePath)}.gz`);
try {
const raw = fs.readFileSync(filePath);
fs.writeFileSync(target, zlib.gzipSync(raw));
fs.unlinkSync(filePath);
return true;
} catch {
return false;
}
}
const FIELD_PATTERN = /^\s*(CATEGORY|SUMMARY|TAGS|PRIORITY|RELEVANCE|CONTENT)\s*[:]\s*(.*)$/i;
function parseCapture(text) {
let category = "";
let summary = "";
let priority = null;
let tags = [];
let relevance = [];
const contentLines = [];
let inContent = false;
for (const line of String(text || "").split(/\r?\n/)) {
const match = line.match(FIELD_PATTERN);
if (match && !(inContent && match[1].toUpperCase() !== "CONTENT")) {
const field = match[1].toUpperCase();
const value = match[2];
if (field === "CATEGORY") category = value;
else if (field === "SUMMARY") summary = value;
else if (field === "TAGS") tags = normalizeTags(value);
else if (field === "PRIORITY") priority = value;
else if (field === "RELEVANCE") relevance = normalizeList(value);
else if (field === "CONTENT") {
inContent = true;
if (value.trim()) contentLines.push(value);
}
continue;
}
if (inContent) contentLines.push(line);
}
return { category, summary, tags, priority, relevance, content: contentLines.join("\n").trim() };
}
function cmdWrite(args) {
const parsed = parseCapture(readStdin());
if (!parsed.content && !parsed.summary) return 1;
const content = (parsed.content || parsed.summary).slice(0, CONTENT_LIMIT);
const stamp = nowStamp();
const meta = {
id: newId(content + stamp),
category: normalizeCategory(parsed.category),
summary: oneLine(parsed.summary) || oneLine(content),
tags: parsed.tags,
priority: normalizePriority(parsed.priority, parsed.category),
relevance: parsed.relevance.length ? parsed.relevance : ["inbox"],
links: [],
sleep_stage: "encoding",
created: stamp,
updated: stamp,
hits: 1,
};
if (args.project) meta.sources = [args.project];
ensureLayout(args.role);
fs.writeFileSync(path.join(memoryRoot(args.role), "inbox", `${meta.id}.md`), dumpMemory(meta, content), "utf8");
process.stdout.write(meta.id);
return 0;
}
function cmdSeed(args) {
const raw = readStdin().trim();
if (!raw && !args.summary) return 1;
const content = (raw || args.summary).slice(0, CONTENT_LIMIT);
const stamp = nowStamp();
const category = normalizeCategory(args.category);
const meta = {
id: newId(content + args.summary + stamp),
category,
summary: oneLine(args.summary) || oneLine(content),
tags: normalizeTags(args.tags),
priority: normalizePriority(args.priority, category),
relevance: normalizeList(args.relevance).length ? normalizeList(args.relevance) : ["explicit", "background"],
links: [],
sleep_stage: "seed",
created: stamp,
updated: stamp,
hits: 1,
};
if (args.source) meta.sources = [args.source];
ensureLayout(args.role);
fs.writeFileSync(path.join(memoryRoot(args.role), category, `${meta.id}.md`), dumpMemory(meta, content), "utf8");
process.stdout.write(meta.id);
return 0;
}
function cmdLoad(args) {
const blocks = [];
for (const category of FULL_CATEGORIES) {
const lines = [`### ${CATEGORY_LABELS[category]}記憶(全文)`];
for (const [meta, content] of listMemories(args.role, category)) {
if (normalizePriority(meta.priority, category) < args.fullMinPriority) continue;
const tags = (meta.tags || []).join("、") || "無標籤";
lines.push(`- **${meta.summary || "(無總結)"}**(標籤:${tags}${memoryHint(meta)}`);
for (const line of content.split(/\r?\n/)) {
if (line.trim()) lines.push(` ${line.trim()}`);
}
}
if (lines.length > 1) blocks.push(lines.join("\n"));
}
const digestLines = [];
for (const category of DIGEST_CATEGORIES) {
const items = listMemories(args.role, category);
if (!items.length) continue;
digestLines.push(`### ${CATEGORY_LABELS[category]}記憶(總結)`);
for (const [meta] of items) {
const priority = normalizePriority(meta.priority, category);
if (priority < args.digestMinPriority && !(meta.links || []).length) continue;
const tags = (meta.tags || []).join("、") || "無標籤";
digestLines.push(`- ${meta.summary || "(無總結)"}(標籤:${tags}${memoryHint(meta)}`);
}
}
if (digestLines.length) blocks.push(digestLines.join("\n"));
const digest = readState(args.role).last_sleep_digest;
if (digest) blocks.push(`> 上次睡眠摘要:${digest}`);
const pending = listInbox(args.role).length;
if (pending) blocks.push(`> 尚有 ${pending} 則未整理記憶,將於下次睡眠時段歸檔。`);
if (!blocks.length) return 1;
let text = blocks.join("\n\n");
if (text.length > args.limit) {
text = `${text.slice(0, args.limit)}\n\n> (記憶內容超過 ${args.limit} 字元預算已截斷;可用 ROLE_LOAD_LIMIT 調整,完整記憶仍保存在磁碟)`;
}
process.stdout.write(text);
return 0;
}
function cmdCollect(args) {
const batchSize = Math.max(1, args.batch);
const collectLimit = Math.max(2000, args.limit);
const existingLimit = Math.max(0, args.existingLimit);
const inbox = listInbox(args.role).slice(0, batchSize);
if (!inbox.length) return 1;
const lines = ["=== INBOX(待整理,每則以 id 標識)==="];
for (const [meta, content] of inbox) {
lines.push(`--- id: ${meta.id} | 時間: ${meta.created || "-"} ---`);
lines.push(`初判分類: ${CATEGORY_LABELS[meta.category || "other"] || "其他"}`);
lines.push(`初判總結: ${meta.summary || ""}`);
lines.push(`初判標籤: ${(meta.tags || []).join("、") || "無"}`);
lines.push(`初判優先度: ${normalizePriority(meta.priority, meta.category || "other")}`);
lines.push(`初判關聯: ${(meta.relevance || []).join("、") || "-"}`);
lines.push("內容:", content, "");
}
lines.push("=== EXISTING(既有記憶索引,供去重與合併判斷)===");
const rows = [];
for (const category of CATEGORIES) {
for (const [meta] of listMemories(args.role, category)) {
const tags = (meta.tags || []).join("、") || "無";
rows.push(`- id: ${meta.id} | 分類: ${CATEGORY_LABELS[category]} | 優先度: ${normalizePriority(meta.priority, category)} | 標籤: ${tags} | 關聯: ${(meta.relevance || []).join("、") || "-"} | links: ${(meta.links || []).join("、") || "-"} | 總結: ${meta.summary || ""}`);
}
}
if (rows.length) {
lines.push(...rows.slice(0, existingLimit));
if (rows.length > existingLimit) lines.push(`…(既有記憶索引超過 ${existingLimit} 則,已依優先度與更新時間截斷)…`);
} else {
lines.push("(尚無既有記憶)");
}
let text = lines.join("\n");
if (text.length > collectLimit) {
text = `${text.slice(0, collectLimit)}\n…(睡眠整理素材超過 ${collectLimit} 字元預算已截斷,其餘留待下個睡眠週期)…`;
}
process.stdout.write(text);
return 0;
}
function extractJson(text) {
let stripped = String(text || "").trim();
const fence = stripped.match(/```(?:json)?\s*([\s\S]*?)```/);
if (fence) stripped = fence[1].trim();
const start = stripped.indexOf("{");
const end = stripped.lastIndexOf("}");
if (start < 0 || end <= start) return null;
try {
return JSON.parse(stripped.slice(start, end + 1));
} catch {
return null;
}
}
function cmdApply(args) {
const data = extractJson(readStdin());
if (!data || typeof data !== "object" || Array.isArray(data)) {
process.stderr.write("整理結果非合法 JSON\n");
return 1;
}
const entries = data.memories;
if (!Array.isArray(entries) || !entries.length) {
process.stderr.write("整理結果不含 memories\n");
return 1;
}
const root = ensureLayout(args.role);
const stamp = nowStamp();
const counts = { new: 0, merge: 0, drop: 0 };
const consumed = [];
for (const entry of entries) {
if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
let action = String(entry.action || "new").trim().toLowerCase();
const sources = Array.isArray(entry.from) ? entry.from.map((item) => String(item).trim()).filter(Boolean) : [];
if (action === "drop") {
consumed.push(...sources);
counts.drop += 1;
continue;
}
const content = String(entry.content || "").trim().slice(0, CONTENT_LIMIT);
const summary = oneLine(entry.summary);
const tags = normalizeTags(entry.tags);
const entryCategory = normalizeCategory(entry.category);
const priority = normalizePriority(entry.priority, entryCategory);
const relevance = normalizeList(entry.relevance);
const links = normalizeList(entry.links);
const sleepStage = oneLine(entry.sleep_stage || entry.sleepStage || "nrem-rem", 40);
if (!content && !summary) continue;
if (action === "merge") {
const targetId = String(entry.target || "").trim();
const [meta, oldContent] = findMemory(args.role, targetId);
if (!meta) {
action = "new";
} else {
const category = normalizeCategory(entry.category || meta.category);
const newMeta = {
id: meta.id,
category,
summary: summary || meta.summary || "",
tags: normalizeTags([...(meta.tags || []), ...tags]),
priority: Math.max(normalizePriority(meta.priority, category), priority),
relevance: normalizeList([...(meta.relevance || []), ...relevance]),
links: normalizeList([...(meta.links || []), ...links]),
sleep_stage: sleepStage,
created: meta.created || stamp,
updated: stamp,
last_replayed: meta.last_replayed || "",
hits: Number.parseInt(meta.hits || 0, 10) + 1,
};
const oldPath = meta.path;
const newPath = path.join(root, category, `${meta.id}.md`);
fs.writeFileSync(newPath, dumpMemory(newMeta, content || oldContent), "utf8");
if (path.resolve(oldPath) !== path.resolve(newPath)) {
try {
fs.unlinkSync(oldPath);
} catch {}
}
consumed.push(...sources);
counts.merge += 1;
continue;
}
}
const category = entryCategory;
const meta = {
id: newId(content + summary + stamp),
category,
summary: summary || oneLine(content),
tags,
priority,
relevance,
links,
sleep_stage: sleepStage,
created: stamp,
updated: stamp,
hits: 1,
};
fs.writeFileSync(path.join(root, category, `${meta.id}.md`), dumpMemory(meta, content || summary), "utf8");
consumed.push(...sources);
counts.new += 1;
}
let archived = 0;
const d = taipeiDate();
const monthDir = path.join(root, "archive", "raw", `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}`);
for (const sourceId of new Set(consumed)) {
const filePath = path.join(root, "inbox", `${sourceId}.md`);
if (fs.existsSync(filePath) && archiveFile(filePath, monthDir)) archived += 1;
}
const patch = { last_sleep: stamp, last_sleep_epoch: nowEpoch() };
if (typeof data.sleepDigest === "string" && data.sleepDigest.trim()) {
patch.last_sleep_digest = oneLine(data.sleepDigest, 300);
}
writeState(args.role, patch);
process.stdout.write(`新增 ${counts.new} 則、合併 ${counts.merge} 則、捨棄 ${counts.drop} 則、歸檔原始記憶 ${archived}`);
return 0;
}
function cmdForget(args) {
const root = ensureLayout(args.role);
const now = new Date();
const forgotten = [];
for (const [category, [days, maxHits]] of Object.entries(FORGET_RULES)) {
for (const [meta] of listMemories(args.role, category)) {
const updated = parseStamp(meta.updated) || parseStamp(meta.created);
if (!updated) continue;
if ((now - updated) / 86400000 < days) continue;
if (Number.parseInt(meta.hits || 0, 10) > maxHits) continue;
if (normalizePriority(meta.priority, category) > 2) continue;
if ((meta.links || []).length) continue;
if (args.dryRun) {
forgotten.push(`${CATEGORY_LABELS[category]}${meta.summary || ""}`);
continue;
}
if (archiveFile(meta.path, path.join(root, "archive", "forgotten"))) {
forgotten.push(`${CATEGORY_LABELS[category]}${meta.summary || ""}`);
}
}
}
if (!forgotten.length) {
process.stdout.write("沒有符合遺忘條件的記憶");
return 0;
}
process.stdout.write(`${args.dryRun ? "(預覽)" : ""}遺忘 ${forgotten.length} 則:\n${forgotten.map((item) => `- ${item}`).join("\n")}`);
if (!args.dryRun) writeState(args.role, { last_forget: nowStamp() });
return 0;
}
function cmdStats(args) {
ensureLayout(args.role);
const state = readState(args.role);
const rows = ["| 分類 | 筆數 | 平均優先度 |", "| --- | --- | --- |"];
for (const category of CATEGORIES) {
const items = listMemories(args.role, category);
let avg = "-";
if (items.length) {
const value = items.reduce((sum, [meta]) => sum + normalizePriority(meta.priority, category), 0) / items.length;
avg = value.toFixed(1);
}
rows.push(`| ${CATEGORY_LABELS[category]} | ${items.length} | ${avg} |`);
}
rows.push(`| 待整理(inbox | ${listInbox(args.role).length} | - |`);
rows.push("");
rows.push(`- 記憶目錄:${memoryRoot(args.role)}`);
rows.push(`- 上次睡眠整理:${state.last_sleep || "尚未整理"}`);
rows.push(`- 上次睡眠摘要:${state.last_sleep_digest || "尚無"}`);
rows.push(`- 上次遺忘:${state.last_forget || "尚未執行"}`);
process.stdout.write(rows.join("\n"));
return 0;
}
function cmdMarkSleep(args) {
writeState(args.role, { last_sleep: nowStamp(), last_sleep_epoch: nowEpoch() });
process.stdout.write("已更新上次整理時間");
return 0;
}
function cmdNeedSleep(args) {
if (!listInbox(args.role).length) {
process.stdout.write("no");
return 0;
}
const last = parseStamp(readState(args.role).last_sleep);
if (!last) {
process.stdout.write("yes");
return 0;
}
process.stdout.write((Date.now() - last.getTime()) / 3600000 >= args.hours ? "yes" : "no");
return 0;
}
function parseArgs(argv) {
const command = argv[0];
const opts = { command };
for (let i = 1; i < argv.length; i += 1) {
const arg = argv[i];
if (!arg.startsWith("--")) continue;
const key = arg.slice(2);
if (key === "dry-run") {
opts.dryRun = true;
} else {
opts[key.replace(/-([a-z])/g, (_, c) => c.toUpperCase())] = argv[i + 1] ?? "";
i += 1;
}
}
return opts;
}
function envInt(name, fallback) {
const value = Number.parseInt(process.env[name] || "", 10);
return Number.isFinite(value) ? value : fallback;
}
function requireRole(args) {
if (!args.role) {
process.stderr.write("缺少 --role\n");
return false;
}
return true;
}
function main(argv) {
const args = parseArgs(argv);
if (!args.command || args.command === "-h" || args.command === "--help") {
process.stdout.write(`用法:memory.js <子命令> [參數]\n\n子命令:write、seed、load、collect、apply、forget、stats、mark-sleep、need-sleep\n`);
return 0;
}
if (!requireRole(args)) return 2;
args.limit = Number.parseInt(args.limit || "", 10);
args.batch = Number.parseInt(args.batch || "", 10);
args.existingLimit = Number.parseInt(args.existingLimit || "", 10);
args.fullMinPriority = Number.parseInt(args.fullMinPriority || "", 10);
args.digestMinPriority = Number.parseInt(args.digestMinPriority || "", 10);
args.hours = Number.parseFloat(args.hours || "");
if (!Number.isFinite(args.limit)) args.limit = args.command === "load" ? envInt("ROLE_LOAD_LIMIT", DEFAULT_LOAD_LIMIT) : envInt("ROLE_SLEEP_COLLECT_LIMIT", COLLECT_LIMIT);
if (!Number.isFinite(args.batch)) args.batch = envInt("ROLE_SLEEP_BATCH", SLEEP_BATCH);
if (!Number.isFinite(args.existingLimit)) args.existingLimit = envInt("ROLE_SLEEP_EXISTING_LIMIT", EXISTING_INDEX_LIMIT);
if (!Number.isFinite(args.fullMinPriority)) args.fullMinPriority = envInt("ROLE_LOAD_FULL_MIN_PRIORITY", DEFAULT_FULL_MIN_PRIORITY);
if (!Number.isFinite(args.digestMinPriority)) args.digestMinPriority = envInt("ROLE_LOAD_DIGEST_MIN_PRIORITY", DEFAULT_DIGEST_MIN_PRIORITY);
if (!Number.isFinite(args.hours)) args.hours = 20.0;
args.category ||= "important";
args.tags ||= "";
args.source ||= "";
args.priority ||= "4";
args.relevance ||= "explicit,background";
args.summary ||= "";
args.project ||= "";
const commands = {
write: cmdWrite,
seed: cmdSeed,
load: cmdLoad,
collect: cmdCollect,
apply: cmdApply,
forget: cmdForget,
stats: cmdStats,
"mark-sleep": cmdMarkSleep,
"need-sleep": cmdNeedSleep,
};
if (!commands[args.command]) {
process.stderr.write("未知子命令\n");
return 2;
}
return commands[args.command](args);
}
process.exit(main(process.argv.slice(2)));