Files
shared/scripts/role/role_load.sh
T
JefferyandClaude Opus 5 46caabe4d8 feat(role): 新增點名載入,對話中叫名字即可切換角色
- `UserPromptSubmit` hook(`role_call.sh`)比對訊息開頭的角色名稱/ID/`aliases` 別名,
  命中即注入該角色人格與記憶並接手本輪;未點名時不輸出內容也不寫檔案。
- 抽出 `role_context.sh` 共用 context 組裝,SessionStart 與點名載入共用同一份人格與操作規則,
  避免兩種載入方式漂移;實測 SessionStart 注入內容與改動前 byte-identical。
- 新增階段角色狀態(`~/.roles/.sessions/<工作階段>.role`),`Stop` hook 改以「本階段實際角色」
  寫記憶,避免點名換人後把跟 A 的對話記進 B 的記憶。
- `SessionEnd` 改為釋放本階段名下所有角色鎖並清掉階段狀態檔。
- 切換時先確認取得目標角色鎖才釋放原角色鎖,避免出現兩個角色都沒有的空窗。
- 睡眠時段、目標角色已被其他活躍階段佔用、點名的是已在場的角色時,一律不切換。
- 新增 `ROLE_CALL_ENABLED`(總開關)與 `ROLE_CALL_MARKER_ONLY`(只認 `@名字`)。
- 順帶修正 `role_capture.sh` 的 `--postcompact` 分支在 `PROJECT` 賦值前就使用它,
  導致壓縮摘要記憶的 `sources` 一直為空。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 13:38:25 +08:00

120 lines
6.1 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# ==============================================================================
# 用途:SessionStart hook 主程式。CLI 工具啟動時載入角色設定與記憶:
# 非睡眠時段注入角色定義+重要/興趣記憶全文+其餘記憶的總結與標籤;
# 睡眠時段(預設 22:00 至隔日 06:00)只回報角色正在睡覺,不載入角色。
# 白天發現昨夜未整理記憶時,於背景補跑一次睡眠整理。
# 實際的 context 組裝在 role_context.sh,與對話中點名載入(role_call.sh)共用。
# 更新時間:2026/07/29 13:25:00
# 相依:bash、node、同目錄的 role_lib.sh、role_context.sh 與 memory.js。
# 退出碼:一律 0 —— hook 絕不可阻斷使用者啟動 CLI。
# ==============================================================================
ROLE_STAGE="role-load"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=./role_lib.sh
. "${SCRIPT_DIR}/role_lib.sh"
# shellcheck source=./role_context.sh
. "${SCRIPT_DIR}/role_context.sh"
role_is_child && exit 0
role_enabled || exit 0
command -v node >/dev/null 2>&1 || role_quit "找不到 node,略過角色載入" "WRN"
ROLE="$(role_resolve_name)"
[ -n "$ROLE" ] || role_quit "未指定角色(ROLE_NAME 與 .active 皆無),略過角色載入"
ROLE_DEF="$(role_file "$ROLE")"
[ -f "$ROLE_DEF" ] || role_quit "找不到角色定義檔:${ROLE_DEF}" "WRN"
# ------------------------------------------------------------------------------
# 讀取 hook 輸入(cwdtranscriptsession),並套用 ROLE_SCOPE 範圍限制
# ------------------------------------------------------------------------------
HOOK_INPUT="$(cat 2>/dev/null)"
HOOK_CWD="$PWD"
HOOK_TRANSCRIPT=""
HOOK_SESSION=""
if [ -n "$HOOK_INPUT" ]; then
HOOK_FIELDS="$(printf '%s' "$HOOK_INPUT" | node -e '
let raw = "";
process.stdin.setEncoding("utf8");
process.stdin.on("data", (chunk) => { raw += chunk; });
process.stdin.on("end", () => {
let data = {};
try { data = JSON.parse(raw); } catch {}
process.stdout.write([
data.cwd || "",
data.transcript_path || data.session_path || data.conversation_path || data.path || "",
data.session_id || data.thread_id || data.conversation_id || "",
].join("\n"));
});
' 2>/dev/null)"
HOOK_CWD="$(printf '%s' "$HOOK_FIELDS" | sed -n '1p')"
HOOK_TRANSCRIPT="$(printf '%s' "$HOOK_FIELDS" | sed -n '2p')"
HOOK_SESSION="$(printf '%s' "$HOOK_FIELDS" | sed -n '3p')"
[ -n "$HOOK_CWD" ] || HOOK_CWD="$PWD"
fi
role_in_scope "$HOOK_CWD" || role_quit "cwd 不在 ROLE_SCOPE 範圍內:${HOOK_CWD}"
# 本階段的角色狀態:供 Stop hook 判斷記憶該寫給誰(點名載入後會被 role_call.sh 覆寫)
SESSION_KEY="$(role_session_key "$HOOK_SESSION" "$HOOK_TRANSCRIPT")"
SLEEP_START="$(role_sleep_start)"
SLEEP_END="$(role_sleep_end)"
# ------------------------------------------------------------------------------
# 單一載入實例:角色已在另一個仍活躍的工作階段時,本次不載入人格
# 放在睡眠判斷之前,因為「已在別處使用」與「睡覺中」是互斥狀態,且不該佔用鎖
# ------------------------------------------------------------------------------
if ! role_instance_acquire "$ROLE" "$HOOK_TRANSCRIPT" "$HOOK_CWD"; then
LOCK_FILE="$(role_instance_lock_path "$ROLE")"
HOLDER_TIME="$(role_instance_lock_field "$LOCK_FILE" loaded)"
HOLDER_CWD="$(role_instance_lock_field "$LOCK_FILE" cwd)"
# 明確記成「本階段沒有人格」,點名載入才不會誤以為原角色還在
role_session_set "$SESSION_KEY" "" "$HOOK_TRANSCRIPT"
role_log "INF" "角色 ${ROLE} 已被其他工作階段載入(${HOLDER_TIME:-時間未知}),本次不載入"
role_context_emit "SessionStart" "$(cat <<EOF_BUSY
# 角色狀態:已在另一個工作階段中
角色「${ROLE}」目前已被另一個仍在使用的工作階段載入(載入時間 ${HOLDER_TIME:-未知},目錄 ${HOLDER_CWD:-未知})。
為避免使用者同時與兩個相同人格對話,本次**不載入角色人格與記憶**,請以一般助理身分回應,
不要自稱該角色、不要使用角色語氣或簽名 emoji。本階段的對話仍會被記錄成記憶。
若使用者詢問或需要在此階段使用該角色,可告知下列任一做法:
- 確定另一個工作階段已關閉時解除鎖定:\`role_sleep.sh --unlock\`
- 該階段閒置超過 $(role_instance_idle_minutes) 分鐘後會自動釋放
- 完全停用此限制:設定環境變數 \`ROLE_SINGLE_INSTANCE=0\`
- 在本階段改叫其他角色:直接以名字點名(例如「<其他角色名>,…」),該角色未被佔用時會即時接手
EOF_BUSY
)"
exit 0
fi
# ------------------------------------------------------------------------------
# 睡眠時段:不載入角色,只說明目前狀態
# ------------------------------------------------------------------------------
if role_in_sleep_window; then
# 明確記成「本階段沒有人格」,點名載入才不會誤以為原角色還在
role_session_set "$SESSION_KEY" "" "$HOOK_TRANSCRIPT"
role_context_emit "SessionStart" "$(cat <<EOF_SLEEP
# 角色狀態:睡眠中(${SLEEP_START}${SLEEP_END}
角色「${ROLE}」正在睡覺,本次工作階段**不載入角色人格與記憶**,請以一般助理身分回應,
不要自稱該角色、不要使用角色語氣或簽名 emoji。若使用者詢問角色,說明角色在睡眠時段整理記憶,
${SLEEP_END} 之後會恢復。本階段的對話仍會被記錄成記憶,於下個睡眠時段整理。
EOF_SLEEP
)"
exit 0
fi
# ------------------------------------------------------------------------------
# 非睡眠時段:組出角色人格 + 操作規則 + 記憶(與點名載入共用 role_context.sh
# ------------------------------------------------------------------------------
role_context_build "$ROLE" "$HOOK_TRANSCRIPT" "load" \
|| role_quit "角色定義檔為空或無法解析:${ROLE_DEF}" "WRN"
role_context_emit "SessionStart" "$ROLE_CONTEXT"
role_session_set "$SESSION_KEY" "$ROLE" "$HOOK_TRANSCRIPT"
role_log "INF" "已載入角色 ${ROLE}(記憶 ${ROLE_CONTEXT_MEMORY_BYTES} 位元組)"
exit 0