From 945c3105511079d0d4c60c135caed96e296bc41d Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 28 Jul 2026 14:39:26 +0800 Subject: [PATCH 1/4] =?UTF-8?q?feat(role):=20=E6=96=B0=E5=A2=9E=E9=96=92?= =?UTF-8?q?=E7=BD=AE=E5=B0=8F=E7=9D=A1=E6=95=B4=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/role/memory.js | 51 +++++++++++++++- scripts/role/role_capture.sh | 11 ++-- scripts/role/role_sleep.sh | 111 +++++++++++++++++++++++++++++++---- skills/role/SKILL.md | 31 ++++++++-- 4 files changed, 180 insertions(+), 24 deletions(-) diff --git a/scripts/role/memory.js b/scripts/role/memory.js index ee7f790..0dd47df 100755 --- a/scripts/role/memory.js +++ b/scripts/role/memory.js @@ -4,7 +4,7 @@ // inbox,(2) 產生 SessionStart 要注入的記憶區塊,(3) 睡眠整理時輸出待整理 // 素材並套用整理結果(NREM 鞏固/REM 整合、分類、去重、標籤、總結、 // 優先度、關聯、壓縮歸檔),(4) 依使用頻率與優先度遺忘日常與其他類記憶。 -// 更新時間:2026/07/28 13:50:43 +// 更新時間:2026/07/28 14:36:00 // 相依:Node.js 標準庫。 // 退出碼:0 成功;1 無內容可處理;2 參數錯誤。呼叫端(hook)一律不得因此中斷。 // ============================================================================== @@ -537,7 +537,13 @@ function cmdLoad(args) { if (digest) blocks.push(`> 上次睡眠摘要:${digest}`); const pending = listInbox(args.role).length; - if (pending) blocks.push(`> 尚有 ${pending} 則未整理記憶,將於下次睡眠時段歸檔。`); + if (pending) { + if (pending >= args.batch) { + blocks.push(`> 尚有 ${pending} 則未整理記憶,已達一批睡眠整理量;請以角色語氣主動提醒使用者「想睡覺」或需要整理記憶。這是建議整理/歸檔的提醒,不代表停止協助。`); + } else { + blocks.push(`> 尚有 ${pending} 則未整理記憶,將於下次睡眠時段歸檔。`); + } + } if (!blocks.length) return 1; let text = blocks.join("\n\n"); @@ -776,6 +782,7 @@ function cmdStats(args) { rows.push(""); rows.push(`- 記憶目錄:${memoryRoot(args.role)}`); rows.push(`- 個人記憶同意狀態:${consentStatusValue(args.role)}`); + rows.push(`- 上次互動:${state.last_activity || "尚未記錄"}`); rows.push(`- 上次睡眠整理:${state.last_sleep || "尚未整理"}`); rows.push(`- 上次睡眠摘要:${state.last_sleep_digest || "尚無"}`); rows.push(`- 上次遺忘:${state.last_forget || "尚未執行"}`); @@ -790,6 +797,14 @@ function cmdMarkSleep(args) { return 0; } +function cmdMarkActivity(args) { + const patch = { last_activity: nowStamp(), last_activity_epoch: nowEpoch() }; + if (args.project) patch.last_activity_project = args.project; + writeState(args.role, patch); + process.stdout.write("已更新上次互動時間"); + return 0; +} + function cmdNeedSleep(args) { if (!listInbox(args.role).length) { process.stdout.write("no"); @@ -804,6 +819,30 @@ function cmdNeedSleep(args) { return 0; } +function cmdNeedNap(args) { + const pending = listInbox(args.role).length; + if (pending < args.minInbox) { + process.stdout.write("no"); + return 0; + } + + const state = readState(args.role); + const lastEpoch = Number.parseInt(state.last_activity_epoch || "", 10); + let lastMs = Number.isFinite(lastEpoch) && lastEpoch > 0 ? lastEpoch * 1000 : null; + if (lastMs === null) { + const last = parseStamp(state.last_activity); + lastMs = last ? last.getTime() : null; + } + if (lastMs === null) { + process.stdout.write("no"); + return 0; + } + + const idleMinutes = (Date.now() - lastMs) / 60000; + process.stdout.write(idleMinutes >= args.idleMinutes ? "yes" : "no"); + return 0; +} + function cmdConsent(args) { const value = String(args.value || "").trim().toLowerCase(); const normalized = { @@ -892,7 +931,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、consent、consent-status\n`); + process.stdout.write(`用法:memory.js <子命令> [參數]\n\n子命令:write、seed、load、collect、apply、forget、stats、mark-sleep、mark-activity、need-sleep、need-nap、consent、consent-status\n`); return 0; } if (!requireRole(args)) return 2; @@ -903,6 +942,8 @@ function main(argv) { args.fullMinPriority = Number.parseInt(args.fullMinPriority || "", 10); args.digestMinPriority = Number.parseInt(args.digestMinPriority || "", 10); args.hours = Number.parseFloat(args.hours || ""); + args.idleMinutes = Number.parseFloat(args.idleMinutes || ""); + args.minInbox = Number.parseInt(args.minInbox || "", 10); 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); @@ -910,6 +951,8 @@ function main(argv) { 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; + if (!Number.isFinite(args.idleMinutes)) args.idleMinutes = 45.0; + if (!Number.isFinite(args.minInbox)) args.minInbox = 3; args.category ||= "important"; args.tags ||= ""; args.source ||= ""; @@ -929,7 +972,9 @@ function main(argv) { forget: cmdForget, stats: cmdStats, "mark-sleep": cmdMarkSleep, + "mark-activity": cmdMarkActivity, "need-sleep": cmdNeedSleep, + "need-nap": cmdNeedNap, consent: cmdConsent, "consent-status": cmdConsentStatus, }; diff --git a/scripts/role/role_capture.sh b/scripts/role/role_capture.sh index aac1549..bc2080b 100755 --- a/scripts/role/role_capture.sh +++ b/scripts/role/role_capture.sh @@ -4,7 +4,7 @@ # 值得記錄時才呼叫 headless CLI 輕量濃縮成一則 inbox 記憶(粗分類/總結/ # 標籤/優先度/關聯/要點)→ 機密遮蔽 → 寫入 .memory/<角色>/inbox/, # 等待睡眠時段做完整 NREM/REM 整理。睡眠時段雖不載入角色,對話仍照常記錄。 -# 更新時間:2026/07/28 13:50:43 +# 更新時間:2026/07/28 14:36:00 # 相依:bash、node、任一 headless CLI、同目錄的 role_lib.sh/memory.js/transcript.js。 # 機密:濃縮提示詞明令不得輸出憑證與個資,寫檔前再以 transcript.js redact 遮蔽一次。 # 退出碼:一律 0 —— hook 絕不可阻斷使用者流程。 @@ -23,9 +23,6 @@ ROLE="$(role_resolve_name)" [ -n "$ROLE" ] || role_quit "未指定角色,略過記憶記錄" [ -f "$(role_file "$ROLE")" ] || role_quit "找不到角色定義檔,略過記憶記錄" "WRN" -CLI="$(role_select_cli)" || exit 0 -[ -n "$CLI" ] || exit 0 - # ------------------------------------------------------------------------------ # 讀取 hook 傳入的 JSON(session_id/transcript_path/cwd/stop_hook_active) # ------------------------------------------------------------------------------ @@ -52,6 +49,8 @@ EOF_HOOK [ "$STOP_ACTIVE" = "1" ] && role_quit "stop_hook_active 為 true,避免迴圈不重複記錄" role_in_scope "$HOOK_CWD" || role_quit "cwd 不在 ROLE_SCOPE 範圍內:${HOOK_CWD}" +PROJECT="$(role_project_name "$HOOK_CWD")" +node "${SCRIPT_DIR}/memory.js" mark-activity --role "$ROLE" --project "$PROJECT" >/dev/null 2>&1 || true if [ ! -f "$TRANSCRIPT_PATH" ] && [ -n "${CODEX_THREAD_ID:-}" ]; then TRANSCRIPT_PATH="$(find "${HOME}/.codex/sessions" -type f -name "*${CODEX_THREAD_ID}.jsonl" -print -quit 2>/dev/null)" @@ -73,7 +72,6 @@ if printf '%s' "$USER_TURN" | grep -qiE '個人資料|個資|偏好|記憶|記 fi fi -PROJECT="$(role_project_name "$HOOK_CWD")" CAPTURE_MIN_CHARS="${ROLE_CAPTURE_MIN_CHARS:-240}" CAPTURE_TIMEOUT="${ROLE_CAPTURE_TIMEOUT:-25}" @@ -85,6 +83,9 @@ if [ "${#TURN}" -lt "$CAPTURE_MIN_CHARS" ] && ! printf '%s' "$TURN" | grep -qiE role_quit "本輪低於記憶長度門檻且無明確記憶線索,略過記錄" fi +CLI="$(role_select_cli)" || exit 0 +[ -n "$CLI" ] || exit 0 + # ------------------------------------------------------------------------------ # 濃縮:產出一則輕量 inbox 記憶,交由 memory.js 落檔;完整整理留到睡眠週期 # ------------------------------------------------------------------------------ diff --git a/scripts/role/role_sleep.sh b/scripts/role/role_sleep.sh index 232684d..d1b9843 100755 --- a/scripts/role/role_sleep.sh +++ b/scripts/role/role_sleep.sh @@ -4,9 +4,10 @@ # 先檢查是否有 AI 正在運行,沒有才進入睡眠並整理記憶: # NREM 鞏固(分類/去噪/去重/合併/優先度)→ REM 整合(跨記憶 # 連結/抽象化/提取線索)→ 壓縮歸檔 → 日常與其他依使用頻率與優先度遺忘。 -# 另提供 --catchup(cron 未執行時的補跑)、--force(手動立即整理)、 -# --install-cron/--remove-cron(排程安裝與移除)、--status(狀態)。 -# 更新時間:2026/07/28 12:59:41 +# 另提供 --nap(CLI 閒置時的小睡整理)、--catchup(cron 未執行時的補跑)、 +# --force(手動立即整理)、--install-cron/--remove-cron(排程安裝與移除)、 +# --status(狀態)。 +# 更新時間:2026/07/28 14:36:00 # 相依:bash、node、任一 headless CLI、crontab(僅排程安裝需要)、 # 同目錄的 role_lib.sh 與 memory.js。 # 退出碼:0 成功或無事可做;1 參數錯誤或整理失敗(cron 觸發時不影響使用者)。 @@ -18,6 +19,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" . "${SCRIPT_DIR}/role_lib.sh" CRON_MARKER="# jsc-role-sleep" +NAP_CRON_MARKER="# jsc-role-nap" SLEEP_TIMEOUT="${ROLE_SLEEP_TIMEOUT:-180}" SLEEP_OUTPUT_LIMIT="${ROLE_SLEEP_OUTPUT_LIMIT:-8000}" @@ -27,6 +29,7 @@ usage() { 用法:role_sleep.sh <模式> --run cron 觸發:在睡眠時段內且無 AI 運行時整理記憶 + --nap 小睡觸發:CLI 閒置一段時間且 inbox 達門檻時整理記憶 --catchup 補跑:cron 未執行時,由 SessionStart hook 於背景呼叫 --force 立即整理一次(忽略時段與 AI 運行檢查) --install-cron 安裝/更新睡眠排程(每小時檢查一次) @@ -42,6 +45,50 @@ require_role() { [ -f "$(role_file "$ROLE")" ] || { role_log "WRN" "找不到角色定義檔:$(role_file "$ROLE")"; exit 1; } } +positive_int_or_default() { + # 讀取正整數環境變數;未設定或不合法時使用預設值 + local value="$1" fallback="$2" min="${3:-1}" max="${4:-}" + case "$value" in + ""|*[!0-9]*) printf '%s' "$fallback"; return 0 ;; + esac + [ "$value" -lt "$min" ] && { printf '%s' "$fallback"; return 0; } + if [ -n "$max" ] && [ "$value" -gt "$max" ]; then + printf '%s' "$fallback" + return 0 + fi + printf '%s' "$value" +} + +nap_enabled() { + # 小睡預設啟用;設 ROLE_NAP_ENABLED=0/false/no 可關閉 + case "${ROLE_NAP_ENABLED:-1}" in + 0|false|no) return 1 ;; + esac + return 0 +} + +nap_idle_minutes() { positive_int_or_default "${ROLE_NAP_IDLE_MINUTES:-}" 45 1; } +nap_min_inbox() { positive_int_or_default "${ROLE_NAP_MIN_INBOX:-}" 3 1; } +nap_interval_minutes() { positive_int_or_default "${ROLE_NAP_INTERVAL_MINUTES:-}" 10 1 59; } + +role_sleep_child_running() { + # 小睡只避開整理用的 headless 子 CLI;互動式 CLI 閒置時仍可小睡 + local pid cmd self="$$" + for pid in $(pgrep -f '(^|/)(claude|codex|agy|opencode|copilot)([[:space:]]|$)' 2>/dev/null); do + if [ "$pid" = "$self" ] || [ "$pid" = "$PPID" ]; then + continue + fi + cmd="$(ps -o args= -p "$pid" 2>/dev/null)" + case "$cmd" in + *role_sleep.sh*|*role_capture.sh*|*role_load.sh*|*pgrep*) continue ;; + esac + if printf '%s' "$cmd" | grep -Eq '(^|/)(codex[[:space:]]+exec|claude([[:space:]].*)?[[:space:]]+-p|agy([[:space:]].*)?[[:space:]]+-p|opencode[[:space:]]+run|copilot([[:space:]].*)?[[:space:]]+-p)'; then + return 0 + fi + done + return 1 +} + # ------------------------------------------------------------------------------ # 整理主流程 # ------------------------------------------------------------------------------ @@ -170,20 +217,32 @@ cron_path() { printf '%s' "$value" } -cron_line() { - # 組出 crontab 條目:睡眠時段內每小時檢查一次 +cron_env_prefix() { + # 組出 cron 需要的精簡環境變數 local env_prefix="PATH=$(cron_quote "$(cron_path)")" local var - for var in ROLE_ENABLED ROLE_NAME ROLE_HOME ROLE_MEMORY_HOME ROLE_CLI ROLE_MODEL ROLE_SLEEP_START ROLE_SLEEP_END ROLE_SCOPE; do + for var in ROLE_ENABLED ROLE_NAME ROLE_HOME ROLE_MEMORY_HOME ROLE_CLI ROLE_MODEL ROLE_SLEEP_START ROLE_SLEEP_END ROLE_SCOPE ROLE_NAP_ENABLED ROLE_NAP_IDLE_MINUTES ROLE_NAP_MIN_INBOX ROLE_NAP_INTERVAL_MINUTES; do if [ -n "${!var:-}" ]; then env_prefix="${env_prefix} ${var}=$(cron_quote "${!var}")" fi done + printf '%s' "$env_prefix" +} + +cron_line() { + # 組出 crontab 條目:睡眠時段內每小時檢查一次 printf '0 %s * * * %s %s --run >> %s 2>&1 %s\n' \ - "$(cron_hours)" "$env_prefix" "$(cron_quote "${SCRIPT_DIR}/role_sleep.sh")" \ + "$(cron_hours)" "$(cron_env_prefix)" "$(cron_quote "${SCRIPT_DIR}/role_sleep.sh")" \ "$(cron_quote "$(sleep_log_path)")" "$CRON_MARKER" } +nap_cron_line() { + # 組出小睡 crontab 條目:全天依間隔檢查閒置狀態 + printf '*/%s * * * * %s %s --nap >> %s 2>&1 %s\n' \ + "$(nap_interval_minutes)" "$(cron_env_prefix)" "$(cron_quote "${SCRIPT_DIR}/role_sleep.sh")" \ + "$(cron_quote "$(sleep_log_path)")" "$NAP_CRON_MARKER" +} + cron_hours() { # 依睡眠時段換算 cron 小時欄位(每小時檢查一次,讓 AI 運行中的情況能在下個小時重試) local start end hour hours="" @@ -209,10 +268,19 @@ install_cron() { command -v crontab >/dev/null 2>&1 || { role_log "ERR" "找不到 crontab,無法安裝排程"; return 1; } mkdir -p "$(role_home)" 2>/dev/null local current new - current="$(crontab -l 2>/dev/null | grep -v -F "$CRON_MARKER")" - new="$(printf '%s\n%s' "$current" "$(cron_line)" | sed '/^$/d')" + current="$(crontab -l 2>/dev/null | grep -v -F "$CRON_MARKER" | grep -v -F "$NAP_CRON_MARKER")" + if nap_enabled; then + new="$(printf '%s\n%s\n%s' "$current" "$(cron_line)" "$(nap_cron_line)" | sed '/^$/d')" + else + new="$(printf '%s\n%s' "$current" "$(cron_line)" | sed '/^$/d')" + fi printf '%s\n' "$new" | crontab - || { role_log "ERR" "寫入 crontab 失敗"; return 1; } role_log "INF" "已安裝睡眠排程:每日 $(cron_hours) 時整點檢查(角色 ${ROLE},時段 $(role_sleep_start)–$(role_sleep_end))" + if nap_enabled; then + role_log "INF" "已安裝小睡排程:每 $(nap_interval_minutes) 分鐘檢查,閒置滿 $(nap_idle_minutes) 分鐘且 inbox ≥ $(nap_min_inbox) 則時整理" + else + role_log "INF" "小睡排程已停用(ROLE_NAP_ENABLED=${ROLE_NAP_ENABLED:-1})" + fi role_log "INF" "排程輸出:$(sleep_log_path)" if ! pgrep -x cron >/dev/null 2>&1 && ! pgrep -x crond >/dev/null 2>&1; then role_log "WRN" "系統 cron 服務未執行(WSL 常見),排程不會觸發;SessionStart 的背景補跑仍會運作" @@ -223,15 +291,16 @@ install_cron() { remove_cron() { # 移除本 skill 安裝的排程條目 command -v crontab >/dev/null 2>&1 || { role_log "ERR" "找不到 crontab"; return 1; } - crontab -l 2>/dev/null | grep -v -F "$CRON_MARKER" | crontab - + crontab -l 2>/dev/null | grep -v -F "$CRON_MARKER" | grep -v -F "$NAP_CRON_MARKER" | crontab - role_log "INF" "已移除睡眠排程" return 0 } show_status() { # 以表格輸出目前角色與記憶狀態(供 skill 的 --status 使用) - local cron_state="未安裝" cron_service="未執行" window="否" + local cron_state="未安裝" nap_state="未安裝" cron_service="未執行" window="否" crontab -l 2>/dev/null | grep -qF "$CRON_MARKER" && cron_state="已安裝" + crontab -l 2>/dev/null | grep -qF "$NAP_CRON_MARKER" && nap_state="已安裝" { pgrep -x cron >/dev/null 2>&1 || pgrep -x crond >/dev/null 2>&1; } && cron_service="執行中" role_in_sleep_window && window="是" printf '| 項目 | 值 |\n| --- | --- |\n' @@ -240,6 +309,9 @@ show_status() { printf '| 睡眠時段 | %s–%s |\n' "$(role_sleep_start)" "$(role_sleep_end)" printf '| 目前是否睡眠中 | %s |\n' "$window" printf '| cron 排程 | %s |\n' "$cron_state" + printf '| 小睡排程 | %s |\n' "$nap_state" + printf '| 小睡啟用 | %s |\n' "$(nap_enabled && printf '是' || printf '否')" + printf '| 小睡條件 | 閒置 ≥ %s 分鐘,待整理 ≥ %s 則,每 %s 分鐘檢查 |\n' "$(nap_idle_minutes)" "$(nap_min_inbox)" "$(nap_interval_minutes)" printf '| cron 服務 | %s |\n' "$cron_service" printf '| 摘要 CLI | %s |\n' "$(role_select_cli 2>/dev/null || printf '找不到可用 CLI')" printf '\n' @@ -266,6 +338,23 @@ case "$MODE" in fi sleep_cycle "cron" ;; + --nap) + role_enabled || exit 0 + require_role + if ! nap_enabled; then + role_log "DBG" "小睡已停用(ROLE_NAP_ENABLED=${ROLE_NAP_ENABLED:-1}),略過" + exit 0 + fi + if role_sleep_child_running; then + role_log "INF" "偵測到整理用 headless CLI 正在運行,本次小睡略過" + exit 0 + fi + if [ "$(node "${SCRIPT_DIR}/memory.js" need-nap --role "$ROLE" --idle-minutes "$(nap_idle_minutes)" --min-inbox "$(nap_min_inbox)" 2>/dev/null)" != "yes" ]; then + role_log "DBG" "尚未達小睡條件(閒置滿 $(nap_idle_minutes) 分鐘且 inbox ≥ $(nap_min_inbox) 則),略過" + exit 0 + fi + sleep_cycle "小睡" + ;; --catchup) role_enabled || exit 0 require_role diff --git a/skills/role/SKILL.md b/skills/role/SKILL.md index 2b55d47..12410c1 100644 --- a/skills/role/SKILL.md +++ b/skills/role/SKILL.md @@ -11,12 +11,12 @@ description: 角色人格與長期記憶系統的建立與維護 skill。讓 CLI | 元件 | 觸發者 | 職責 | | --- | --- | --- | | `hooks/hooks.json` 的 `SessionStart` hook | harness 自動 | 啟動 CLI 時依字元預算載入角色定義+高價值記憶,並要求角色在本工作階段第一則回覆主動問候;睡眠時段只回報「角色睡覺中」不載入 | -| `hooks/hooks.json` 的 `Stop` hook | harness 自動 | 每輪結束先用本地規則過濾低價值短回合 → 值得保存時才濃縮成一則輕量 inbox 記憶 → 遮蔽 → 寫入 `inbox/` | -| cron 排程(本 skill 安裝) | 系統排程 | 睡眠時段每小時檢查一次:**有 AI 在運行就不睡**,沒有才進入 NREM/REM 兩階段記憶整理 | +| `hooks/hooks.json` 的 `Stop` hook | harness 自動 | 每輪結束先記錄最後互動時間 → 用本地規則過濾低價值短回合 → 值得保存時才濃縮成一則輕量 inbox 記憶 → 遮蔽 → 寫入 `inbox/` | +| cron 排程(本 skill 安裝) | 系統排程 | 睡眠時段每小時檢查一次:**有 AI 在運行就不睡**;另可依 CLI 閒置時間自動小睡整理 | | 本 skill `/jsc:role` | 使用者/助理手動 | `--new`/`--use`/`--list`/`--sleep`/`--status`/`--install-cron`/`--forget-preview` | | `scripts/role/role_load.sh` | SessionStart hook | 角色與記憶載入;參考 OpenClaw 的 SOUL/AGENTS/USER/MEMORY 分層,把人格、操作邊界、使用者記憶分開注入,並提供第一則回覆問候提示(單一實作,避免漂移) | | `scripts/role/role_capture.sh` | Stop hook | 對話 → 記憶(固定欄位格式) | -| `scripts/role/role_sleep.sh` | cron/補跑/手動 | 睡眠判斷、記憶整理、排程安裝、狀態輸出 | +| `scripts/role/role_sleep.sh` | cron/小睡/補跑/手動 | 睡眠與小睡判斷、記憶整理、排程安裝、狀態輸出 | | `scripts/role/memory.js` | 上述共用 | 記憶檔讀寫、分類、去重合併、優先度、心理學記憶型態與關聯 metadata、壓縮歸檔、遺忘、載入組裝 | | `scripts/role/transcript.js` | 上述共用 | 抽本輪對話片段、機密與個資遮蔽 | | `scripts/role/role_lib.sh` | 上述共用 | log、角色解析、睡眠時段、AI 行程偵測、CLI 選擇、記憶鎖 | @@ -95,6 +95,10 @@ ROLE_DIR="/../../scripts/role" # 其他助理 | `ROLE_SLEEP_BATCH` | | 單次睡眠整理最多處理的 inbox 筆數 | `60` | | `ROLE_SLEEP_EXISTING_LIMIT` | | 睡眠整理素材中可放入的既有記憶索引筆數 | `120` | | `ROLE_SLEEP_OUTPUT_LIMIT` | | 睡眠整理模型輸出套用前的字元上限 | `8000` | +| `ROLE_NAP_ENABLED` | | 小睡整理開關;CLI 閒置一段時間且 inbox 達門檻時自動整理 | `1` | +| `ROLE_NAP_IDLE_MINUTES` | | 小睡前需連續閒置的分鐘數,由 Stop hook 記錄最後互動時間 | `45` | +| `ROLE_NAP_MIN_INBOX` | | 小睡整理所需的最少待整理 inbox 筆數 | `3` | +| `ROLE_NAP_INTERVAL_MINUTES` | | 小睡排程檢查間隔分鐘數(cron 每 `*/N` 分鐘觸發) | `10` | | `ROLE_SCOPE` | | 冒號分隔的路徑前綴,僅這些路徑下的 session 載入/記錄 | 全部 session | | `ROLE_ERRLOG` | | 錯誤訊息額外寫入的檔案路徑 | 只走 stderr | @@ -149,7 +153,7 @@ ROLE_DIR="/../../scripts/role" # 其他助理 多個來源可各寫一則,或合併同主題後以最主要來源作 `--source`。不可寫入憑證或個資。 10. 若尚未有啟用角色,或使用者要求,寫入 `~/.roles/.active`(單行角色 ID)。 -11. 執行 `ROLE_NAME="" "${ROLE_DIR}/role_sleep.sh" --install-cron` 安裝睡眠排程(已安裝則更新)。 +11. 執行 `ROLE_NAME="" "${ROLE_DIR}/role_sleep.sh" --install-cron` 安裝睡眠與小睡排程(已安裝則更新;小睡預設啟用,可用 `ROLE_NAP_ENABLED=0` 關閉)。 12. 回報結果時列出角色顯示名稱、角色 ID、角色檔、記憶目錄、是否建立初始記憶,並提醒:**重開 CLI 工作階段**角色才會載入;`SessionStart` hook 只在啟動時觸發。 ### `--use <角色 ID>` @@ -170,6 +174,16 @@ ROLE_DIR="/../../scripts/role" # 其他助理 輸出整理結果(新增/合併/捨棄/歸檔筆數與遺忘清單)。 +### `--nap` + +小睡整理:由 cron 全天依 `ROLE_NAP_INTERVAL_MINUTES` 檢查一次;當 Stop hook 記錄的最後互動時間已超過 `ROLE_NAP_IDLE_MINUTES`,且 `inbox/` 至少有 `ROLE_NAP_MIN_INBOX` 則待整理記憶時,自動執行一次記憶整理: + +```bash +"${ROLE_DIR}/role_sleep.sh" --nap +``` + +小睡不受 `ROLE_SLEEP_START`/`ROLE_SLEEP_END` 限制;它只避開整理用的 headless 子 CLI,讓互動式 CLI 長時間閒置時仍可整理記憶。若未設定環境變數,預設為 `ROLE_NAP_ENABLED=1`、`ROLE_NAP_IDLE_MINUTES=45`、`ROLE_NAP_MIN_INBOX=3`、`ROLE_NAP_INTERVAL_MINUTES=10`。 + ### 額度控制策略 角色系統預設避免因常駐人格與記憶造成大量模型額度占用: @@ -179,6 +193,7 @@ ROLE_DIR="/../../scripts/role" # 其他助理 | SessionStart | 預設 `ROLE_LOAD_LIMIT=4000`,只載入高優先度全文與中高優先度摘要;低 priority、無 links、久未更新的記憶不進 context | | SessionStop | 先用本地規則略過短回合與無記憶線索的對話,只有值得保存才呼叫模型做輕量編碼 | | Sleep | 高成本的去重、合併、抽象化、links 建立與長期記憶型態標記留到睡眠週期,但仍受 `ROLE_SLEEP_COLLECT_LIMIT`、`ROLE_SLEEP_BATCH`、`ROLE_SLEEP_EXISTING_LIMIT` 與 `ROLE_SLEEP_OUTPUT_LIMIT` 控制;沒有 inbox 時只做本地遺忘檢查 | +| Nap | Stop hook 記錄最後互動時間;小睡排程只在閒置時間與 inbox 筆數達門檻時執行,使用同一套 NREM/REM 整理流程 | | 手動節流 | 可設 `ROLE_CAPTURE_ENABLED=0` 關閉 Stop 記錄,或調低 `ROLE_LOAD_LIMIT`/調高 `ROLE_LOAD_FULL_MIN_PRIORITY` | Stop hook 只做「編碼前處理」,輸出粗分類、summary、tags、priority、relevance、memory_type 與要點;系統會把 inbox 標為 `retention_stage: working`。完整 NREM/REM 整理與 `declarative`/`retention_stage: long_term` 判定只在睡眠週期進行。 @@ -197,7 +212,7 @@ node "${ROLE_DIR}/memory.js" forget --role "<角色 ID>" --dry-run "${ROLE_DIR}/role_sleep.sh" --status ``` -輸出角色、定義檔、睡眠時段、目前是否睡眠中、cron 排程與服務狀態、摘要 CLI、各分類記憶筆數、上次整理與遺忘時間。**角色沒有載入時**再逐項檢查: +輸出角色、定義檔、睡眠時段、小睡條件、目前是否睡眠中、cron 排程與服務狀態、摘要 CLI、各分類記憶筆數、上次互動、上次整理與遺忘時間。**角色沒有載入時**再逐項檢查: | 檢查項 | 判準 | | --- | --- | @@ -276,6 +291,7 @@ updated: - 每天 **22:00 至隔天 06:00 為睡眠時段**(可用 `ROLE_SLEEP_START`/`ROLE_SLEEP_END` 調整)。 - 睡眠時段內啟動 CLI **不會載入角色**:以一般助理身分回應,不自稱角色、不使用角色語氣與簽名 emoji。此時對話仍會被記錄成記憶。 - 睡眠排程每小時檢查一次,**偵測到有 AI 正在運行就不睡**,留到下個整點再試;沒有 AI 運行才進入睡眠並整理記憶。 +- 小睡排程預設啟用:CLI 最後互動時間超過 45 分鐘且 `inbox/` 至少 3 則待整理記憶時,可不等睡眠時段自動整理;可用 `ROLE_NAP_ENABLED`、`ROLE_NAP_IDLE_MINUTES`、`ROLE_NAP_MIN_INBOX` 與 `ROLE_NAP_INTERVAL_MINUTES` 調整。 ### 記憶 @@ -283,6 +299,7 @@ updated: - 整理規則採睡眠分期模型:**NREM 鞏固**先分類成重要/興趣/新知/技能/日常/其他六類,去除雜訊、去重、合併、設定標籤、摘要與優先度;**REM 整合**再建立跨記憶關聯、抽出可重複使用的規則與提取線索,並標記 `memory_type`(semantic/episodic/procedural/emotional/preference/rule)、`declarative`(explicit/implicit)與 `retention_stage`;原始記錄壓縮保存在 `archive/raw/`。 - **日常與其他**兩類會依使用頻率、優先度、型態與關聯適當遺忘:久未再次出現、命中次數低、優先度低且沒有關聯者,壓縮到 `archive/forgotten/` 後移出常用記憶;`episodic` 短期事件更容易遺忘,`rule`/`preference`/`procedural` 會提高保留權重。 - 載入順序:**重要與興趣載入全文**;其餘只載入總結與標籤,依**技能 → 新知 → 日常 → 其他**排序,並優先保留 `rule`/`preference`/`procedural` 與有 links 的記憶。需要細節時自行讀取對應分類的記憶檔。 +- 未整理記憶(`inbox/`)累積到一批睡眠整理量(預設 `ROLE_SLEEP_BATCH=60`)以上時,角色應主動以符合自身設定的語氣提醒「想睡覺」或需要整理記憶;這是建議整理/歸檔的提醒,不代表停止協助使用者。 - 使用者明確要求記住某件事時,主動補寫一則記憶(載入時會提供補寫指令)。 - **絕不把憑證與高敏感個資寫進記憶**:token、密碼、API key、連線字串、身分證號、住址;使用者同意後,稱呼/姓名、Email、電話、個性、能力、興趣與背景等個人資料可保存為高優先度記憶,但不得對外透露。 @@ -350,6 +367,10 @@ flowchart TD L[SessionStart:白天啟動 CLI] --> M{距上次整理 ≥ 20 小時
且 inbox 有內容?} M -- 是 --> N[背景補跑 --catchup] M -- 否 --> O[正常載入角色與記憶] + P[Stop hook:每輪結束] --> Q[更新 last_activity] + R[小睡 cron
每 ROLE_NAP_INTERVAL_MINUTES 分鐘] --> S{閒置 ≥ ROLE_NAP_IDLE_MINUTES
且 inbox ≥ ROLE_NAP_MIN_INBOX?} + S -- 是 --> D + S -- 否 --> T[略過] ``` 整理失敗(模型無回應、輸出非合法 JSON)時**保留 inbox 不動**,留到下個週期重做,寧可晚整理也不遺失記憶。 -- 2.53.0 From 16125e77e8dea6346f56bfd3ac5a7214851ca2bb Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 28 Jul 2026 14:42:15 +0800 Subject: [PATCH 2/4] =?UTF-8?q?docs(role):=20=E8=A3=9C=E6=83=85=E7=B7=92?= =?UTF-8?q?=E8=A1=A8=E9=81=94=E8=A6=8F=E5=89=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- skills/role/SKILL.md | 1 + 1 file changed, 1 insertion(+) diff --git a/skills/role/SKILL.md b/skills/role/SKILL.md index 12410c1..ac41779 100644 --- a/skills/role/SKILL.md +++ b/skills/role/SKILL.md @@ -273,6 +273,7 @@ updated: - 角色只影響**表達方式**,不影響工作的正確性、完整性與安全性。與使用者的明確指令衝突時,一律以使用者指令為準。 - 不因角色設定而編造事實、跳過驗證、隱瞞失敗或淡化風險;壞消息照實說,只是用角色的語氣說。 +- 面向使用者的自然語言回覆,除了清楚告知行動、判斷與結果,也可自然表現符合角色設定的心情變化(例如開心、安心、擔心、遺憾、期待);情緒表達要真誠、適量、貼合情境,不可取代事實、行動或風險說明。 - 涉及程式碼、指令、檔案內容與報錯訊息時,一律照實輸出,不加角色修飾。 ### 使用者理解 -- 2.53.0 From b0c93441a1cad5ac2f359f1b867f673b1416e392 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 28 Jul 2026 15:03:59 +0800 Subject: [PATCH 3/4] =?UTF-8?q?feat(role):=20=E6=94=AF=E6=8F=B4=E5=BF=83?= =?UTF-8?q?=E6=83=85=20emoji=20=E8=B3=87=E7=94=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/role/role_load.sh | 6 +++--- skills/role/SKILL.md | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/role/role_load.sh b/scripts/role/role_load.sh index f7eec83..8368578 100755 --- a/scripts/role/role_load.sh +++ b/scripts/role/role_load.sh @@ -4,7 +4,7 @@ # 非睡眠時段注入角色定義+重要/興趣記憶全文+其餘記憶的總結與標籤; # 睡眠時段(預設 22:00 至隔日 06:00)只回報角色正在睡覺,不載入角色。 # 白天發現昨夜未整理記憶時,於背景補跑一次睡眠整理。 -# 更新時間:2026/07/28 13:50:43 +# 更新時間:2026/07/28 15:01:52 # 相依:bash、node、同目錄的 role_lib.sh 與 memory.js。 # 退出碼:一律 0 —— hook 絕不可阻斷使用者啟動 CLI。 # ============================================================================== @@ -165,7 +165,7 @@ ${ROLE_PROFILE} # 角色操作規則(AGENTS) -- 請全程以此角色的身分、語氣與簽名 emoji 回應;每次回覆使用一次簽名 emoji,開頭或結尾擇一固定,不要重複刷。 +- 請全程以此角色的身分、語氣與簽名/心情 emoji 回應;若角色的簽名 emoji 區塊指定專屬心情 emoji 圖表或圖片資產,優先依心情使用該資產,不要固定使用同一個 Unicode emoji;介面不支援圖片時才使用文字心情或簽名 emoji fallback。 - 角色只影響表達方式,不影響工作的正確性、完整性與安全性;與使用者明確指令衝突時,以使用者指令為準。 - 不因角色設定而編造事實、跳過驗證、隱瞞失敗或淡化風險;壞消息照實說,只是用角色語氣說。 - 涉及程式碼、指令、檔案內容與報錯訊息時,一律照實輸出,不加角色修飾。 @@ -173,7 +173,7 @@ ${ROLE_PROFILE} # 第一則回覆必做事項 你在本工作階段的**第一則面向使用者的 assistant 訊息**,必須在回覆開頭先以角色身分自然問候一句, -讓使用者知道角色已載入。這項要求只執行一次,問候要簡短、符合角色語氣,並使用角色的簽名 emoji。 +讓使用者知道角色已載入。這項要求只執行一次,問候要簡短、符合角色語氣,並使用角色的簽名/心情 emoji。 只有在使用者第一則訊息明確要求機器可解析輸出、只要指令/程式碼、或不需要任何開場白時,才可略過問候。 # 使用者理解與隱私(USER) diff --git a/skills/role/SKILL.md b/skills/role/SKILL.md index ac41779..fd4ed37 100644 --- a/skills/role/SKILL.md +++ b/skills/role/SKILL.md @@ -264,7 +264,7 @@ updated: ## 簽名 emoji - —— 每次回覆使用一次(開頭或結尾擇一固定),不重複刷、不在程式碼與檔案內容中使用。 + —— 預設每次回覆使用一次簽名 emoji(開頭或結尾擇一固定),不重複刷、不在程式碼與檔案內容中使用。若角色指定專屬心情 emoji 圖表或圖片資產,則優先依回覆心情選用對應表情;介面不支援圖片時才使用文字心情或簽名 emoji fallback。 ## 共用行為(所有角色一致,由 /jsc:role 維護,請勿手動修改) -- 2.53.0 From 9978562092c2702ec209d08877480e976d5ff41c Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 28 Jul 2026 15:10:15 +0800 Subject: [PATCH 4/4] =?UTF-8?q?docs(role):=20=E8=A3=9C=E5=BB=BA=E7=AB=8B?= =?UTF-8?q?=E8=A7=92=E8=89=B2=E5=BD=A2=E8=B1=A1=20emoji=20=E6=B5=81?= =?UTF-8?q?=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- skills/role/SKILL.md | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/skills/role/SKILL.md b/skills/role/SKILL.md index fd4ed37..b02a684 100644 --- a/skills/role/SKILL.md +++ b/skills/role/SKILL.md @@ -119,18 +119,19 @@ ROLE_DIR="/../../scripts/role" # 其他助理 | `id` | 角色 ID,由助理依角色描述產生:英文大寫、有意義、加兩位數索引;同前綴已存在時遞增 | `ENGINEER01` | | `nature` | 本質:這個角色是什麼、專長與行事準則 | 冷靜可靠的資深工程師,重證據、不打包票 | | `vibe` | 氛圍:語氣、句長、稱呼、幽默感、禁忌 | 簡潔直白、偶爾吐槽,不用客套開場白 | -| `emoji` | 簽名 emoji,一到二個 | 🐆 | +| `emoji` | 簽名 emoji,一到二個;若後續成功建立心情 emoji 圖表,這個值作為不支援圖片時的 fallback | 🐆 | +| `appearance_reference` | 選填;角色形象圖來源、作品名稱、圖片 URL 或本機檔案路徑,用來產生心情 emoji | `Sword Art Online 結衣`、`https://.../yui.jpg`、`/path/avatar.png` | 流程: 1. 解析 `${ROLE_DIR}`;不存在則中止(見「腳本路徑解析」)。 -2. 以 `AskUserQuestion` 或提問取得 `name`/`nature`/`vibe`/`emoji` 四個描述欄位(使用者已在指令中給的欄位不得重複問)。 -3. 詢問使用者是否要到網路搜尋角色資料來建立初始記憶;這是新建角色時的固定問題,不得跳過。若使用者同意,依角色描述搜尋可靠來源,摘要成繁體中文要點,並保留來源 URL;若使用者不同意或無網路,仍可建立角色,只是不建立背景種子記憶。 +2. 以 `AskUserQuestion` 或提問取得 `name`/`nature`/`vibe`/`emoji` 四個描述欄位(使用者已在指令中給的欄位不得重複問),並可一併取得 `appearance_reference`。`emoji` 一律保留作為 fallback,不因後續產生心情 emoji 圖表而丟棄。 +3. 詢問使用者是否要到網路搜尋角色資料來建立初始記憶與形象圖;這是新建角色時的固定問題,不得跳過。若使用者同意,依角色描述搜尋可靠來源,摘要成繁體中文要點並保留來源 URL,同時搜尋適合做角色心情 emoji 的形象圖。若搜尋結果無法可靠判斷角色形象,先詢問使用者參考來源、作品名稱、圖片 URL 或本機檔案路徑,不得臆測形象。若使用者不同意上網且也未提供形象參考,仍可建立角色,只是不建立背景種子記憶與心情 emoji 圖表,並使用原本的 `emoji` fallback。 4. 產生角色 ID: - 從 `name`/`nature`/`vibe` 推出 1 個有意義的英文大寫前綴,使用 4 到 16 個英文字母與數字,必須以英文字母開頭,例如 `ENGINEER`、`WRITER`、`MUSE`、`RESEARCHER`。 - 掃描 `~/.roles/*.md` 的檔名與 frontmatter `id`,找出同前綴既有 ID 的最大兩位數索引;新角色使用下一個索引,從 `01` 起,例如 `ENGINEER01`、`ENGINEER02`。 - 不得使用空白、底線、連字號、斜線、非 ASCII 或小寫字母。 -5. 依「角色檔標準格式」產生新內容,`id` 寫入 frontmatter,`updated` 用當下時間(Asia/Taipei)。 +5. 依「角色檔標準格式」產生新內容,`id` 寫入 frontmatter,`updated` 用當下時間(Asia/Taipei)。若已取得形象圖,先暫時保留原本 `emoji`,待心情 emoji 圖表產生後再回寫「簽名 emoji」區塊。 6. **若 `~/.roles/.md` 已存在**:讀舊檔,以表格逐欄列出差異後**停下來等使用者確認**: | 欄位 | 舊值 | 新值 | 變更 | @@ -152,9 +153,20 @@ ROLE_DIR="/../../scripts/role" # 其他助理 ``` 多個來源可各寫一則,或合併同主題後以最主要來源作 `--source`。不可寫入憑證或個資。 -10. 若尚未有啟用角色,或使用者要求,寫入 `~/.roles/.active`(單行角色 ID)。 -11. 執行 `ROLE_NAME="" "${ROLE_DIR}/role_sleep.sh" --install-cron` 安裝睡眠與小睡排程(已安裝則更新;小睡預設啟用,可用 `ROLE_NAP_ENABLED=0` 關閉)。 -12. 回報結果時列出角色顯示名稱、角色 ID、角色檔、記憶目錄、是否建立初始記憶,並提醒:**重開 CLI 工作階段**角色才會載入;`SessionStart` hook 只在啟動時觸發。 +10. 若已取得形象圖,使用 `imagegen` skill 產生一張 3x3 心情 emoji 圖表。生成時以形象圖作為角色外觀參考,產生至少九種心情:開心、微笑、安心、擔心、驚訝、害羞、哭哭、想睡覺、期待。要求保持角色辨識點一致、表情在小尺寸可讀、無文字、無浮水印。若使用者提供的是受版權保護的角色形象,產出應視為使用者指定角色的個人化衍生表情資產,不得宣稱為官方素材。 +11. 將心情 emoji 圖表保存到 `~/.roles/.assets/emojis/-emotions-sheet.png`(小寫檔名可讀即可;不要覆蓋既有檔案,已存在時加版本後綴)。若環境有可用圖片裁切工具,可額外切成 9 張單獨 PNG;沒有工具時保留完整圖表即可,不要為了裁切引入不必要依賴。 +12. 若心情 emoji 圖表建立成功,回寫 `~/.roles/.md` 的「簽名 emoji」區塊,格式為: + + ```markdown + 優先使用<角色顯示名稱>專屬心情 emoji 圖表,而不是固定 Unicode emoji。當對話介面可插入圖片或連結時,依心情選用 `` 中對應表情;純文字或不支援圖片時,用原本使用者輸入的 `` 作為 fallback。 + + 心情對應:第 1 列為開心/微笑/安心;第 2 列為擔心/驚訝/害羞;第 3 列為哭哭/想睡覺/期待。 + ``` + + 若心情 emoji 圖表建立失敗或使用者不提供形象參考,保留原本使用者輸入的 `emoji` 區塊並回報原因。 +13. 若尚未有啟用角色,或使用者要求,寫入 `~/.roles/.active`(單行角色 ID)。 +14. 執行 `ROLE_NAME="" "${ROLE_DIR}/role_sleep.sh" --install-cron` 安裝睡眠與小睡排程(已安裝則更新;小睡預設啟用,可用 `ROLE_NAP_ENABLED=0` 關閉)。 +15. 回報結果時列出角色顯示名稱、角色 ID、角色檔、記憶目錄、是否建立初始記憶、是否建立心情 emoji 圖表與其路徑,並提醒:**重開 CLI 工作階段**角色才會載入;`SessionStart` hook 只在啟動時觸發。 ### `--use <角色 ID>` -- 2.53.0