feat(role): 角色分層載入與個人記憶同意狀態

role_load.sh 不再原封不動注入整份角色檔,改採 SOUL/AGENTS/USER/MEMORY
分層:只抽出角色 ID、顯示名稱、本質、氛圍與簽名 emoji 作為人格層,由 hook
產生固定操作邊界,記憶與同意狀態獨立成一區。避免人格檔裡的背景故事與模板
文字污染工程與安全規則。

memory.js 新增 consent/consent-status 子命令,同意狀態寫入
~/.memory/<角色 ID>/state.json 的 personal_memory_consent(accepted/
declined/unknown);role_capture.sh 於本輪出現明確同意或拒絕時更新狀態,
偵測不到明確表述時不自行推論。accepted 後不再每次重問。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jeffery
2026-07-28 14:21:48 +08:00
co-authored by Claude Opus 5
parent 3748d2e61a
commit c713359fb4
3 changed files with 162 additions and 15 deletions
+60 -2
View File
@@ -4,7 +4,7 @@
// inbox(2) 產生 SessionStart 要注入的記憶區塊,(3) 睡眠整理時輸出待整理
// 素材並套用整理結果(NREM 鞏固/REM 整合、分類、去重、標籤、總結、
// 優先度、關聯、壓縮歸檔),(4) 依使用頻率與優先度遺忘日常與其他類記憶。
// 更新時間:2026/07/28 12:40:24
// 更新時間:2026/07/28 13:50:43
// 相依:Node.js 標準庫。
// 退出碼:0 成功;1 無內容可處理;2 參數錯誤。呼叫端(hook)一律不得因此中斷。
// ==============================================================================
@@ -775,6 +775,7 @@ function cmdStats(args) {
rows.push(`| 待整理(inbox | ${listInbox(args.role).length} | - |`);
rows.push("");
rows.push(`- 記憶目錄:${memoryRoot(args.role)}`);
rows.push(`- 個人記憶同意狀態:${consentStatusValue(args.role)}`);
rows.push(`- 上次睡眠整理:${state.last_sleep || "尚未整理"}`);
rows.push(`- 上次睡眠摘要:${state.last_sleep_digest || "尚無"}`);
rows.push(`- 上次遺忘:${state.last_forget || "尚未執行"}`);
@@ -803,6 +804,61 @@ function cmdNeedSleep(args) {
return 0;
}
function cmdConsent(args) {
const value = String(args.value || "").trim().toLowerCase();
const normalized = {
accept: "accepted",
accepted: "accepted",
yes: "accepted",
true: "accepted",
"1": "accepted",
decline: "declined",
declined: "declined",
no: "declined",
false: "declined",
"0": "declined",
unknown: "unknown",
}[value];
if (!normalized) {
process.stderr.write("同意狀態只接受 accepteddeclinedunknown\n");
return 2;
}
writeState(args.role, {
personal_memory_consent: normalized,
personal_memory_consent_updated: nowStamp(),
});
process.stdout.write(normalized);
return 0;
}
function cmdConsentStatus(args) {
process.stdout.write(consentStatusValue(args.role));
return 0;
}
function consentStatusValue(role) {
ensureLayout(role);
const state = readState(role);
const status = String(state.personal_memory_consent || "unknown").trim();
if (["accepted", "declined"].includes(status)) return status;
const haystacks = [];
for (const [meta, content] of listInbox(role)) haystacks.push(`${meta.summary}\n${content}`);
for (const category of CATEGORIES) {
for (const [meta, content] of listMemories(role, category)) haystacks.push(`${meta.summary}\n${content}`);
}
const consentText = haystacks
.filter((text) => /個人資料|個資|記憶|記住|保存|同意|拒絕/.test(text))
.join("\n");
if (/不同意|不願意|不要保存|不要記住|拒絕|不可以保存|不可以記住/.test(consentText)) {
return "declined";
}
if (/已同意|明確同意|同意.*保存|同意.*記住|可以保存|可以記住|願意.*保存|願意.*記住/.test(consentText)) {
return "accepted";
}
return "unknown";
}
function parseArgs(argv) {
const command = argv[0];
const opts = { command };
@@ -836,7 +892,7 @@ function requireRole(args) {
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`);
process.stdout.write(`用法:memory.js <子命令> [參數]\n\n子命令:write、seed、load、collect、apply、forget、stats、mark-sleep、need-sleep、consent、consent-status\n`);
return 0;
}
if (!requireRole(args)) return 2;
@@ -874,6 +930,8 @@ function main(argv) {
stats: cmdStats,
"mark-sleep": cmdMarkSleep,
"need-sleep": cmdNeedSleep,
consent: cmdConsent,
"consent-status": cmdConsentStatus,
};
if (!commands[args.command]) {
process.stderr.write("未知子命令\n");