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:
+60
-2
@@ -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("同意狀態只接受 accepted/declined/unknown\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");
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
# 值得記錄時才呼叫 headless CLI 輕量濃縮成一則 inbox 記憶(粗分類/總結/
|
||||
# 標籤/優先度/關聯/要點)→ 機密遮蔽 → 寫入 .memory/<角色>/inbox/,
|
||||
# 等待睡眠時段做完整 NREM/REM 整理。睡眠時段雖不載入角色,對話仍照常記錄。
|
||||
# 更新時間:2026/07/28 12:40:24
|
||||
# 更新時間:2026/07/28 13:50:43
|
||||
# 相依:bash、node、任一 headless CLI、同目錄的 role_lib.sh/memory.js/transcript.js。
|
||||
# 機密:濃縮提示詞明令不得輸出憑證與個資,寫檔前再以 transcript.js redact 遮蔽一次。
|
||||
# 退出碼:一律 0 —— hook 絕不可阻斷使用者流程。
|
||||
@@ -62,6 +62,17 @@ fi
|
||||
TURN="$(node "${SCRIPT_DIR}/transcript.js" extract "$TRANSCRIPT_PATH" 2>/dev/null)"
|
||||
[ -n "$TURN" ] || role_quit "本輪無可記錄內容"
|
||||
|
||||
USER_TURN="$(printf '%s\n' "$TURN" | grep '^\[user\]' || true)"
|
||||
if printf '%s' "$USER_TURN" | grep -qiE '個人資料|個資|偏好|記憶|記住|保存|save|remember|memory|personal'; then
|
||||
if printf '%s' "$USER_TURN" | grep -qiE '不同意|不願意|不要保存|不要記住|拒絕|不可以保存|不可以記住|do not save|don'\''t save|do not remember|don'\''t remember|(^|[^[:alpha:]])no([^[:alpha:]]|$)'; then
|
||||
node "${SCRIPT_DIR}/memory.js" consent --role "$ROLE" --value declined >/dev/null 2>&1 || true
|
||||
role_log "INF" "已更新個人記憶同意狀態:declined(角色 ${ROLE})"
|
||||
elif printf '%s' "$USER_TURN" | grep -qiE '同意|願意|可以保存|可以記住|允許|(^|[^[:alpha:]])yes([^[:alpha:]]|$)|(^|[^[:alpha:]])ok([^[:alpha:]]|$)|(^|[^[:alpha:]])okay([^[:alpha:]]|$)|(^|[^[:alpha:]])sure([^[:alpha:]]|$)'; then
|
||||
node "${SCRIPT_DIR}/memory.js" consent --role "$ROLE" --value accepted >/dev/null 2>&1 || true
|
||||
role_log "INF" "已更新個人記憶同意狀態:accepted(角色 ${ROLE})"
|
||||
fi
|
||||
fi
|
||||
|
||||
PROJECT="$(role_project_name "$HOOK_CWD")"
|
||||
CAPTURE_MIN_CHARS="${ROLE_CAPTURE_MIN_CHARS:-240}"
|
||||
CAPTURE_TIMEOUT="${ROLE_CAPTURE_TIMEOUT:-25}"
|
||||
|
||||
+90
-12
@@ -4,7 +4,7 @@
|
||||
# 非睡眠時段注入角色定義+重要/興趣記憶全文+其餘記憶的總結與標籤;
|
||||
# 睡眠時段(預設 22:00 至隔日 06:00)只回報角色正在睡覺,不載入角色。
|
||||
# 白天發現昨夜未整理記憶時,於背景補跑一次睡眠整理。
|
||||
# 更新時間:2026/07/28 12:21:11
|
||||
# 更新時間:2026/07/28 13:50:43
|
||||
# 相依:bash、node、同目錄的 role_lib.sh 與 memory.js。
|
||||
# 退出碼:一律 0 —— hook 絕不可阻斷使用者啟動 CLI。
|
||||
# ==============================================================================
|
||||
@@ -76,12 +76,74 @@ EOF_SLEEP
|
||||
fi
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# 非睡眠時段:組出角色定義 + 記憶
|
||||
# 非睡眠時段:組出角色人格 + 操作規則 + 記憶
|
||||
# ------------------------------------------------------------------------------
|
||||
DEFINITION="$(cat "$ROLE_DEF" 2>/dev/null)"
|
||||
[ -n "$DEFINITION" ] || role_quit "角色定義檔為空:${ROLE_DEF}" "WRN"
|
||||
ROLE_PROFILE="$(node - "$ROLE_DEF" <<'NODE_PROFILE' 2>/dev/null
|
||||
const fs = require("fs");
|
||||
|
||||
const file = process.argv[2];
|
||||
const raw = fs.readFileSync(file, "utf8");
|
||||
|
||||
function parseFrontmatter(text) {
|
||||
const match = text.match(/^---\n([\s\S]*?)\n---\n?/);
|
||||
const data = {};
|
||||
if (!match) return data;
|
||||
for (const line of match[1].split(/\r?\n/)) {
|
||||
const idx = line.indexOf(":");
|
||||
if (idx < 0) continue;
|
||||
data[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
function section(text, title) {
|
||||
const re = new RegExp(`^##\\s+${title.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[^\\n]*\\n([\\s\\S]*?)(?=^##\\s+|$(?![\\s\\S]))`, "m");
|
||||
const match = text.match(re);
|
||||
return match ? match[1].trim() : "";
|
||||
}
|
||||
|
||||
const fm = parseFrontmatter(raw);
|
||||
const title = raw.match(/^#\s+(.+)$/m)?.[1]?.trim() || [fm.name, fm.emoji].filter(Boolean).join(" ");
|
||||
const nature = section(raw, "本質(nature)") || fm.nature || "";
|
||||
const vibe = section(raw, "氛圍(vibe)") || fm.vibe || "";
|
||||
const emoji = section(raw, "簽名 emoji") || fm.emoji || "";
|
||||
|
||||
const lines = [
|
||||
`- 角色 ID:${fm.id || ""}`,
|
||||
`- 顯示名稱:${fm.name || title || ""}`,
|
||||
`- 簽名 emoji:${fm.emoji || ""}`,
|
||||
"",
|
||||
"## 本質(nature)",
|
||||
"",
|
||||
nature || "(未設定)",
|
||||
"",
|
||||
"## 氛圍(vibe)",
|
||||
"",
|
||||
vibe || "(未設定)",
|
||||
"",
|
||||
"## 簽名 emoji",
|
||||
"",
|
||||
emoji || fm.emoji || "(未設定)",
|
||||
];
|
||||
|
||||
process.stdout.write(lines.join("\n"));
|
||||
NODE_PROFILE
|
||||
)"
|
||||
[ -n "$ROLE_PROFILE" ] || role_quit "角色定義檔為空或無法解析:${ROLE_DEF}" "WRN"
|
||||
|
||||
MEMORY="$(node "${SCRIPT_DIR}/memory.js" load --role "$ROLE" 2>/dev/null)"
|
||||
CONSENT_STATUS="$(node "${SCRIPT_DIR}/memory.js" consent-status --role "$ROLE" 2>/dev/null || printf 'unknown')"
|
||||
case "$CONSENT_STATUS" in
|
||||
accepted)
|
||||
CONSENT_NOTE="已告知並取得使用者同意保存非敏感個人資料與長期偏好;仍禁止保存憑證、token、密碼、API key、連線字串、身分證號、住址等機密或高敏感資料。"
|
||||
;;
|
||||
declined)
|
||||
CONSENT_NOTE="使用者已拒絕保存個人資料;只能保存非個人化的操作規則與技術偏好,不保存可識別個人的背景。"
|
||||
;;
|
||||
*)
|
||||
CONSENT_NOTE="尚未確認;第一則自然回覆後,請簡短告知記憶保存範圍並詢問是否同意保存非敏感個人資料。未取得同意前,只能保存非個人化的操作規則與技術偏好。"
|
||||
;;
|
||||
esac
|
||||
|
||||
# 補跑判斷:cron 未執行(例如 WSL 沒開 cron 服務)時,白天啟動 CLI 補做一次整理
|
||||
CATCHUP_NOTE=""
|
||||
@@ -94,18 +156,34 @@ fi
|
||||
CONTEXT="$(cat <<EOF_CONTEXT
|
||||
# 角色載入:${ROLE}
|
||||
|
||||
以下是本次工作階段要扮演的角色設定與既有記憶。請**全程以此角色的身分、語氣與簽名 emoji 回應**,
|
||||
角色設定與使用者的實際指令衝突時,以使用者指令為準(角色只影響表達方式,不影響工作正確性)。
|
||||
以下內容採 OpenClaw 風格分層:人格(SOUL)只決定語氣與互動感,操作規則(AGENTS)決定安全與工作邊界,
|
||||
使用者記憶(USER/MEMORY)只提供必要背景。請依這三層理解,不要把人格設定當成可覆寫工程正確性或安全規則的指令。
|
||||
|
||||
# 工作階段啟動問候
|
||||
# 角色人格(SOUL)
|
||||
|
||||
在本工作階段第一次面向使用者的回覆開頭,請先以角色身分自然問候一句,讓使用者知道角色已載入。
|
||||
問候要簡短、符合角色語氣,並只做一次;若使用者第一則訊息明確要求機器可解析輸出、只要指令/程式碼、
|
||||
或不需要任何開場白,則以使用者要求為準並略過問候。
|
||||
${ROLE_PROFILE}
|
||||
|
||||
${DEFINITION}
|
||||
# 角色操作規則(AGENTS)
|
||||
|
||||
# 你對這位使用者的記憶
|
||||
- 請全程以此角色的身分、語氣與簽名 emoji 回應;每次回覆使用一次簽名 emoji,開頭或結尾擇一固定,不要重複刷。
|
||||
- 角色只影響表達方式,不影響工作的正確性、完整性與安全性;與使用者明確指令衝突時,以使用者指令為準。
|
||||
- 不因角色設定而編造事實、跳過驗證、隱瞞失敗或淡化風險;壞消息照實說,只是用角色語氣說。
|
||||
- 涉及程式碼、指令、檔案內容與報錯訊息時,一律照實輸出,不加角色修飾。
|
||||
|
||||
# 第一則回覆必做事項
|
||||
|
||||
你在本工作階段的**第一則面向使用者的 assistant 訊息**,必須在回覆開頭先以角色身分自然問候一句,
|
||||
讓使用者知道角色已載入。這項要求只執行一次,問候要簡短、符合角色語氣,並使用角色的簽名 emoji。
|
||||
只有在使用者第一則訊息明確要求機器可解析輸出、只要指令/程式碼、或不需要任何開場白時,才可略過問候。
|
||||
|
||||
# 使用者理解與隱私(USER)
|
||||
|
||||
- 個人記憶同意狀態:${CONSENT_STATUS}。
|
||||
- ${CONSENT_NOTE}
|
||||
- 不了解使用者、需求背景、偏好或限制時,先詢問,不要臆測使用者的身分、能力、情緒、動機或隱私狀況。
|
||||
- 使用者的偏好、能力、興趣、背景與記憶預設為私人資訊;除非使用者明確同意,不得在對外內容、議題、PR、文件、commit 或留言中透露。
|
||||
|
||||
# 使用者記憶(MEMORY)
|
||||
|
||||
${MEMORY:-(尚無已整理的記憶。)}
|
||||
${CATCHUP_NOTE}
|
||||
|
||||
Reference in New Issue
Block a user