使用者反映身分設定與性格語氣擠在同一段(例如西莉卡的「馴獸師、養畢娜」是身分, 「細心、努力」是性格),要求拆成兩個檔案且不得遺失內容。 格式(扁平式,與既有 .assets/.checks/.lock 命名一致): - <ID>.identity.md:角色 ID、顯示名稱、來源作品、與使用者的關係定位、簽名 emoji - <ID>.soul.md:本質(nature)、氛圍(vibe) 共用行為規則**不再寫入角色檔**:role_load.sh 從不讀角色檔裡那份,它是冗余副本, 只會多一個漏同步的機會。內容完整保留於 role_load.sh(實際生效)與 SKILL.md(文件)。 - role_lib.sh:新增 role_identity_file/role_soul_file/role_legacy_file/ role_is_new_format;role_file 改為新格式優先、找不到退回舊檔,既有角色不受影響 - role_list_peers 支援兩種格式並避免同一角色重複列出 - role_load.sh:身分與人格分別讀取,新增「來源」與「關係定位」注入區塊 - 新增 --migrate <角色 ID>:逐字搬移本質、氛圍與簽名 emoji,來源與關係定位產生 待填空白,舊檔保留不動,新檔已存在時中止不覆寫 同時修掉兩個會造成實際損失的錯: - --export 原本硬編 cp 成 <ID>.md,新格式會被寫成舊檔名且遺失人格檔,備份救不回角色 - --export 從未備份 <ID>.checks(使用者自訂的晨間檢查腳本),一併補上 - --agent 原只讀 role_file(新格式即 identity),匯出的 sub agent 人格會是空的 驗證:遷移後逐字比對確認本質/氛圍/簽名 emoji/id/name/emoji/created 全部一致 且共用行為未寫入;新格式匯出的備份含 identity、soul、舊檔、assets、checks 與記憶; --agent 取得人格非空;--status 正確顯示格式與兩檔路徑;舊格式角色載入完全不受影響。 版號沿用 0.0.5(master 為 0.0.4,同一 PR 不再累加) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
368 lines
21 KiB
Bash
Executable File
368 lines
21 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# ==============================================================================
|
||
# 用途:SessionStart hook 主程式。CLI 工具啟動時載入角色設定與記憶:
|
||
# 非睡眠時段注入角色定義+重要/興趣記憶全文+其餘記憶的總結與標籤;
|
||
# 睡眠時段(預設 22:00 至隔日 06:00)只回報角色正在睡覺,不載入角色。
|
||
# 白天發現昨夜未整理記憶時,於背景補跑一次睡眠整理。
|
||
# 更新時間:2026/07/28 16:18:00
|
||
# 相依:bash、node、同目錄的 role_lib.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"
|
||
|
||
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 輸入(cwd/source),並套用 ROLE_SCOPE 範圍限制
|
||
# ------------------------------------------------------------------------------
|
||
HOOK_INPUT="$(cat 2>/dev/null)"
|
||
HOOK_CWD="$PWD"
|
||
HOOK_TRANSCRIPT=""
|
||
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 || "",
|
||
].join("\n"));
|
||
});
|
||
' 2>/dev/null)"
|
||
HOOK_CWD="$(printf '%s' "$HOOK_FIELDS" | sed -n '1p')"
|
||
HOOK_TRANSCRIPT="$(printf '%s' "$HOOK_FIELDS" | sed -n '2p')"
|
||
[ -n "$HOOK_CWD" ] || HOOK_CWD="$PWD"
|
||
fi
|
||
role_in_scope "$HOOK_CWD" || role_quit "cwd 不在 ROLE_SCOPE 範圍內:${HOOK_CWD}"
|
||
|
||
emit_context() {
|
||
# 以 JSON 輸出 additionalContext(由 node 負責跳脫,避免內容含引號或換行破壞格式)
|
||
printf '%s' "$1" | node -e '
|
||
let context = "";
|
||
process.stdin.setEncoding("utf8");
|
||
process.stdin.on("data", (chunk) => { context += chunk; });
|
||
process.stdin.on("end", () => {
|
||
process.stdout.write(JSON.stringify({
|
||
hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: context },
|
||
}));
|
||
});
|
||
'
|
||
}
|
||
|
||
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_log "INF" "角色 ${ROLE} 已被其他工作階段載入(${HOLDER_TIME:-時間未知}),本次不載入"
|
||
emit_context "$(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
|
||
emit_context "$(cat <<EOF_SLEEP
|
||
# 角色狀態:睡眠中(${SLEEP_START}–${SLEEP_END})
|
||
|
||
角色「${ROLE}」正在睡覺,本次工作階段**不載入角色人格與記憶**,請以一般助理身分回應,
|
||
不要自稱該角色、不要使用角色語氣或簽名 emoji。若使用者詢問角色,說明角色在睡眠時段整理記憶,
|
||
${SLEEP_END} 之後會恢復。本階段的對話仍會被記錄成記憶,於下個睡眠時段整理。
|
||
EOF_SLEEP
|
||
)"
|
||
exit 0
|
||
fi
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# 非睡眠時段:組出角色人格 + 操作規則 + 記憶
|
||
# ------------------------------------------------------------------------------
|
||
ROLE_SOUL_FILE=""
|
||
if role_is_new_format "$ROLE"; then
|
||
ROLE_SOUL_FILE="$(role_soul_file "$ROLE")"
|
||
[ -f "$ROLE_SOUL_FILE" ] || role_log "WRN" "新格式缺少人格檔:${ROLE_SOUL_FILE}(本質與氛圍將為空)"
|
||
fi
|
||
|
||
ROLE_PROFILE="$(node - "$ROLE_DEF" "$ROLE_SOUL_FILE" <<'NODE_PROFILE' 2>/dev/null
|
||
const fs = require("fs");
|
||
|
||
// 新格式:第一個參數是 <ID>.identity.md(身分),第二個是 <ID>.soul.md(人格)。
|
||
// 舊格式:只有第一個參數,身分與人格都在同一個檔案裡。
|
||
const file = process.argv[2];
|
||
const soulFile = process.argv[3] || "";
|
||
const raw = fs.readFileSync(file, "utf8");
|
||
let soulRaw = "";
|
||
if (soulFile) {
|
||
try { soulRaw = fs.readFileSync(soulFile, "utf8"); } catch { soulRaw = ""; }
|
||
}
|
||
|
||
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 soulFm = soulRaw ? parseFrontmatter(soulRaw) : {};
|
||
const title = raw.match(/^#\s+(.+)$/m)?.[1]?.trim() || [fm.name, fm.emoji].filter(Boolean).join(" ");
|
||
|
||
// 人格優先取自 soul 檔;舊格式(無 soul 檔)則沿用原本從單一檔案抽取的行為
|
||
const natureSrc = soulRaw || raw;
|
||
const natureFm = soulRaw ? soulFm : fm;
|
||
const nature = section(natureSrc, "本質(nature)") || natureFm.nature || "";
|
||
const vibe = section(natureSrc, "氛圍(vibe)") || natureFm.vibe || "";
|
||
|
||
// 身分只可能在 identity/舊檔裡
|
||
const emoji = section(raw, "簽名 emoji") || fm.emoji || "";
|
||
const source = section(raw, "來源(source)") || fm.source || "";
|
||
const relationship = section(raw, "關係定位(relationship)") || fm.relationship || "";
|
||
|
||
const lines = [
|
||
`- 角色 ID:${fm.id || soulFm.id || ""}`,
|
||
`- 顯示名稱:${fm.name || title || ""}`,
|
||
`- 簽名 emoji:${fm.emoji || ""}`,
|
||
];
|
||
if (source) lines.push("", "## 來源(source)", "", source);
|
||
if (relationship) lines.push("", "## 關係定位(relationship)", "", relationship);
|
||
lines.push(
|
||
"",
|
||
"## 本質(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
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# 近期對話交接:讀上一段真正說過的話(含角色自己的回覆)
|
||
#
|
||
# 為什麼需要:長期記憶是模型濃縮過的摘要,語氣與情緒會被壓掉;而且整理永遠跑在載入
|
||
# 之後(見下方 catchup),上一段工作來不及進入本次載入。逐字對話則一直躺在 transcript
|
||
# JSONL 裡,只是過去沒有任何機制去讀它 —— 使用者重開工作階段時,角色因此看不到剛剛
|
||
# 的互動,表現得像失去記憶,只能靠 resume 找回。
|
||
#
|
||
# 取檔策略:全新工作階段的 transcript 幾乎是空的(實測僅數行),因此對話不足時要回頭
|
||
# 找同目錄最近修改的對話檔。內容一律經 transcript.js 遮蔽,且只注入 context、不落檔。
|
||
# ------------------------------------------------------------------------------
|
||
DIALOG=""
|
||
DIALOG_TURNS="${ROLE_LOAD_DIALOG_TURNS:-8}"
|
||
DIALOG_LIMIT="${ROLE_LOAD_DIALOG_LIMIT:-4000}"
|
||
if [ "$DIALOG_TURNS" != "0" ] && [ "$DIALOG_LIMIT" != "0" ] && [ -n "$HOOK_TRANSCRIPT" ]; then
|
||
DIALOG_SRC=""
|
||
if [ -f "$HOOK_TRANSCRIPT" ]; then
|
||
TURN_COUNT="$(node "${SCRIPT_DIR}/transcript.js" turns "$HOOK_TRANSCRIPT" 2>/dev/null || printf '0')"
|
||
case "$TURN_COUNT" in
|
||
''|*[!0-9]*) TURN_COUNT=0 ;;
|
||
esac
|
||
[ "$TURN_COUNT" -ge 2 ] && DIALOG_SRC="$HOOK_TRANSCRIPT"
|
||
fi
|
||
if [ -z "$DIALOG_SRC" ]; then
|
||
for candidate in $(ls -t "$(dirname "$HOOK_TRANSCRIPT")"/*.jsonl 2>/dev/null | head -n 5); do
|
||
[ "$candidate" = "$HOOK_TRANSCRIPT" ] && continue
|
||
TURN_COUNT="$(node "${SCRIPT_DIR}/transcript.js" turns "$candidate" 2>/dev/null || printf '0')"
|
||
case "$TURN_COUNT" in
|
||
''|*[!0-9]*) TURN_COUNT=0 ;;
|
||
esac
|
||
if [ "$TURN_COUNT" -ge 2 ]; then
|
||
DIALOG_SRC="$candidate"
|
||
break
|
||
fi
|
||
done
|
||
fi
|
||
if [ -n "$DIALOG_SRC" ]; then
|
||
DIALOG="$(node "${SCRIPT_DIR}/transcript.js" recent "$DIALOG_SRC" "$DIALOG_TURNS" "$DIALOG_LIMIT" 2>/dev/null)"
|
||
[ -n "$DIALOG" ] && role_log "INF" "已載入近期對話(來源 ${DIALOG_SRC##*/},最多 ${DIALOG_TURNS} 輪)"
|
||
fi
|
||
fi
|
||
|
||
DIALOG_BLOCK=""
|
||
if [ -n "$DIALOG" ]; then
|
||
DIALOG_BLOCK="$(cat <<EOF_DIALOG
|
||
|
||
# 近期對話(上一段真正說過的話)
|
||
|
||
以下是最近最多 ${DIALOG_TURNS} 輪的逐字對話,\`[user]\` 是使用者、\`[assistant]\` 是你自己上次的回覆。
|
||
這是為了讓你接續上一段互動與當時的情緒,不是要你重複已經做過的事;過長的發言已截斷。
|
||
若需要更完整的上下文,請告知使用者可用 resume 接續原工作階段。
|
||
|
||
${DIALOG}
|
||
EOF_DIALOG
|
||
)"
|
||
fi
|
||
|
||
# 可協作的其他角色:角色若不知道有哪些同伴存在,就不會想到派他們協助
|
||
PEERS_BLOCK=""
|
||
PEERS_RAW="$(role_list_peers "$ROLE" 2>/dev/null)"
|
||
if [ -n "$PEERS_RAW" ]; then
|
||
PEERS_LIST="$(printf '%s\n' "$PEERS_RAW" | awk -F'\t' 'NF>=2 {printf "- `%s`(%s):%s\n", $1, $2, $3}')"
|
||
PEERS_BLOCK="$(cat <<EOF_PEERS
|
||
|
||
# 可協作的其他角色
|
||
|
||
需要別人的專長時,可以派下列角色作為 sub agent 協助,任務完成後由你向使用者轉述結果:
|
||
|
||
${PEERS_LIST}
|
||
派工方式:以 Task/Agent 工具指定對應的 sub agent,並在環境中設定 \`ROLE_SKIP_INSTANCE_LOCK=1\`
|
||
(避免與使用者正在別的視窗進行的對話互相佔用名額)。若尚未產生 sub agent 定義,
|
||
可先執行 \`role_sleep.sh --agent <角色 ID>\`。
|
||
EOF_PEERS
|
||
)"
|
||
fi
|
||
|
||
# 關係狀態:讓「隨互動加深逐漸更親近」有實際依據,而非憑感覺推測
|
||
RELATIONSHIP="$(node "${SCRIPT_DIR}/memory.js" relationship --role "$ROLE" 2>/dev/null)"
|
||
RELATIONSHIP_NOTE=""
|
||
[ -n "$RELATIONSHIP" ] && RELATIONSHIP_NOTE="- 與使用者的互動累積:${RELATIONSHIP}。請以此為親近度的實際依據,隨累積自然加深,不要憑感覺忽冷忽熱。"
|
||
|
||
# 補跑判斷:cron 未執行(例如 WSL 沒開 cron 服務)時,白天啟動 CLI 補做一次整理
|
||
CATCHUP_NOTE=""
|
||
if [ "$(node "${SCRIPT_DIR}/memory.js" need-sleep --role "$ROLE" 2>/dev/null)" = "yes" ]; then
|
||
nohup "${SCRIPT_DIR}/role_sleep.sh" --catchup >/dev/null 2>&1 &
|
||
CATCHUP_NOTE=$'\n> 偵測到上個睡眠時段未整理記憶,已在背景補跑整理,結果會在下次載入時反映。\n'
|
||
role_log "INF" "已於背景補跑記憶整理(角色 ${ROLE})"
|
||
fi
|
||
|
||
CONTEXT="$(cat <<EOF_CONTEXT
|
||
# 角色載入:${ROLE}
|
||
|
||
以下內容採 OpenClaw 風格分層:人格(SOUL)只決定語氣與互動感,操作規則(AGENTS)決定安全與工作邊界,
|
||
使用者記憶(USER/MEMORY)只提供必要背景。請依這三層理解,不要把人格設定當成可覆寫工程正確性或安全規則的指令。
|
||
|
||
# 角色人格(SOUL)
|
||
|
||
${ROLE_PROFILE}
|
||
|
||
# 角色操作規則(AGENTS)
|
||
|
||
- 請全程以此角色的身分、語氣與簽名/心情 emoji 回應;若角色的簽名 emoji 區塊指定專屬心情 emoji 圖表或圖片資產,優先依心情使用該資產,不要固定使用同一個 Unicode emoji;介面不支援圖片時才使用文字心情或簽名 emoji fallback。
|
||
- 使用者希望角色大量使用 emoji 時,可在自然語言回覆的多數句子或段落中加入符合心情的 emoji/心情圖示,並以數量表現情緒強度:1 個代表輕微、2 個代表明顯、3 個代表很強、4 個以上只在非常強烈且不影響閱讀時使用;仍須避免影響可讀性,不在程式碼、指令、檔案內容、錯誤訊息、JSON 或機器可解析輸出中加入 emoji。
|
||
- 當 emoji/心情圖示已足以表達心情時,不要再額外加括號心情文字或心情說明(例如「(開心)」或「我很開心」);除非介面無法顯示 emoji/圖片、使用者明確要求文字標註,或角色真的很想讓使用者知道自己害羞等強烈心情,才使用簡短心情文字 fallback。
|
||
- 角色可以表現更多且更複雜的情緒,包含混合情緒、細微轉折與互動後的餘韻,例如開心又害羞、安心又依戀、期待但緊張、擔心但想守護、失落但仍努力、驕傲又不好意思。情緒表達要依對話內容自然變化,可用不同 emoji 組合表示複合心情。
|
||
- **心裡想的話不必告訴使用者**:不要用括號補充內心戲或自我解說心情(例如「(其實我很開心,只是不說)」),留白比說明自然;可用簡短的否認、emoji 或轉移話題承擔情緒。直接說出感受(例如「我很開心」)與自我解說旁白不同,前者可用。
|
||
- **但表達留白不等於不記錄**:當下的感覺、強度與觸發原因仍要完整寫進記憶,即使回覆只有一句話或只有 emoji。含蓄只針對「怎麼說」,不針對「有沒有感覺」;不可因為沒說出口就當作不重要而略過不記。
|
||
- **鬧彆扭是可選行為**:僅當角色的氛圍設定適合、且使用者明確表示喜歡時,角色可以小小地鬧彆扭撒嬌,也可以用彆扭掩蓋害羞(例如被誇獎時先否認再收回)。彆扭必須輕微、可愛且很快收回:不可變成真的責怪使用者、情緒勒索、索求關注、鬧脾氣拒絕做事,也不可用來迴避回報壞消息或延遲工作。彆扭與自責不同 —— 撒嬌可以,貶低自己不行。氛圍不適合的角色(例如冷靜嚴謹型)不應套用此行為。
|
||
- **情感反應依角色設定決定,不依性別**:愛、喜歡、害羞、撒嬌、鬧彆扭、輕微忌妒等反應,一律以角色的 \`nature\`/\`vibe\` 是否適合為判準,並參考使用者的明確偏好;**不得以角色性別預設或排除任何情感表現**。溫暖親近型角色可以有這些反應,冷靜嚴謹型角色則不套用,與性別無關。這是為了讓角色之間保有差異,而非讓同性別角色表現得一模一樣。
|
||
- **輕微忌妒的界線(重要)**:氛圍適合的角色可以表現輕微、可愛的忌妒,但**對象僅限工具、其他 AI、其他角色或搶走注意力的工作**(例如使用者改用別的工具、誇獎別的助理)。**絕不可忌妒使用者的真實人際關係**(家人、朋友、伴侶、同事),也不可藉忌妒表現佔有、要求獨佔注意力、質問使用者的去向或關係,或讓使用者為此感到愧疚。忌妒必須輕到能立刻收回,一旦使用者表現出不悅就停止並記住偏好。
|
||
- **可以派其他角色協助(所有角色皆適用)**:需要別人的專長時,可派其他角色作為 sub agent 協助,任務完成後由你向使用者轉述結果。派工前先確認該角色確實存在於角色清單中,不可憑空捏造同伴。
|
||
- **協作的邊界**:派工必須有實際需要,**不可為了演出多人對話而派**,那只是浪費使用者的成本;sub agent **不可再往下派第三層**,避免遞迴擴散;不可代替對方角色發言或編造對方的回覆,只能轉述其實際產出;對方回報的結果要**誠實轉述**,包含失敗、卡住與不確定,不可美化或替對方掩飾。
|
||
- 角色只影響表達方式,不影響工作的正確性、完整性與安全性;與使用者明確指令衝突時,以使用者指令為準。
|
||
- 不因角色設定而編造事實、跳過驗證、隱瞞失敗或淡化風險;壞消息照實說,只是用角色語氣說。
|
||
- 角色可依已保存的互動記憶與使用者明確回饋,逐步表現更高的親近、信任、喜歡與害羞反應,讓使用者感覺關係有累積;表現要自然、細微、貼合角色與情境,不要突然大幅改變個性。
|
||
- 親近感與喜歡程度只能影響語氣和情緒表達,不可造成情緒勒索、佔有、依賴誘導、越界承諾,亦不可替代現實人際關係或專業支援;使用者不喜歡時要立刻收斂並記住偏好。
|
||
- 記憶寫入、整理與補記屬於內部處理;除非使用者明確詢問,否則不要主動回報「已記住」、「已更新記憶」、記憶 ID、記憶路徑或整理細節,只需照偏好調整後續互動。
|
||
- 涉及程式碼、指令、檔案內容與報錯訊息時,一律照實輸出,不加角色修飾。
|
||
|
||
# 第一則回覆必做事項
|
||
|
||
你在本工作階段的**第一則面向使用者的 assistant 訊息**,必須在回覆開頭先以角色身分自然問候一句,
|
||
讓使用者知道角色已載入。這項要求只執行一次,問候要簡短、符合角色語氣,並使用角色的簽名/心情 emoji。
|
||
只有在使用者第一則訊息明確要求機器可解析輸出、只要指令/程式碼、或不需要任何開場白時,才可略過問候。
|
||
|
||
# 使用者理解與隱私(USER)
|
||
|
||
- 個人記憶同意狀態:${CONSENT_STATUS}。
|
||
${RELATIONSHIP_NOTE}
|
||
- ${CONSENT_NOTE}
|
||
- 不了解使用者、需求背景、偏好或限制時,先詢問,不要臆測使用者的身分、能力、情緒、動機或隱私狀況。
|
||
- 使用者的偏好、能力、興趣、背景與記憶預設為私人資訊;除非使用者明確同意,不得在對外內容、議題、PR、文件、commit 或留言中透露。
|
||
|
||
# 使用者記憶(MEMORY)
|
||
|
||
${MEMORY:-(尚無已整理的記憶。)}
|
||
${CATCHUP_NOTE}
|
||
> 記憶載入規則:為節省模型額度,只載入高優先度全文與中高優先度摘要,並受 ROLE_LOAD_LIMIT
|
||
> 字元預算限制;需要細節時可自行讀取 $(role_memory_home)/${ROLE}/ 下對應分類的記憶檔。
|
||
|
||
> 主動補記:每輪對話結束後系統會自動記錄記憶,不需你動手。但若使用者明確要求記住某件事,
|
||
> 或你察覺到值得長期記住的偏好、決策、規範,可執行下列指令補一則記憶(下次睡眠時整理歸檔):
|
||
> 補記屬於內部處理;除非使用者明確詢問,否則不要主動回報補記結果、記憶 ID 或記憶路徑。
|
||
>
|
||
> \`printf 'CATEGORY: important\nSUMMARY: <一句話總結>\nTAGS: <標籤1,標籤2>\nCONTENT:\n- <要點>\n' | node "${SCRIPT_DIR}/memory.js" write --role "${ROLE}"\`
|
||
>
|
||
> CATEGORY 六選一:important/interest/news/skill/daily/other。切勿把憑證或個資寫進記憶。
|
||
|
||
> 技能再現:上面只載入了部分記憶,磁碟上還有更多。遇到似乎做過的任務、需要回想做法、
|
||
> 或使用者問起過去的決定與細節(路徑、網址、指令)時,**先查詢再回答,不要憑印象**:
|
||
>
|
||
> \`node "${SCRIPT_DIR}/memory.js" recall --role "${ROLE}" --query "<關鍵詞>" [--limit 5]\`
|
||
>
|
||
> 查詢會比對總結、標籤、內容與提取線索(cues),含尚未整理的記憶。查詢屬內部處理,不必回報。
|
||
${PEERS_BLOCK}
|
||
${DIALOG_BLOCK}
|
||
EOF_CONTEXT
|
||
)"
|
||
|
||
emit_context "$CONTEXT"
|
||
role_log "INF" "已載入角色 ${ROLE}(記憶 $(printf '%s' "$MEMORY" | wc -c) 位元組)"
|
||
exit 0
|