refactor(role): 角色檔拆成身分(identity)與人格(soul)兩檔
使用者反映身分設定與性格語氣擠在同一段(例如西莉卡的「馴獸師、養畢娜」是身分, 「細心、努力」是性格),要求拆成兩個檔案且不得遺失內容。 格式(扁平式,與既有 .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>
This commit is contained in:
+145
-7
@@ -37,6 +37,8 @@ usage() {
|
||||
--unlock 解除角色單一載入鎖(另一個工作階段已關閉但鎖仍在時使用)
|
||||
--agent <角色 ID> [輸出目錄]
|
||||
把角色匯出成 sub agent 定義(預設 ~/.claude/agents/)
|
||||
--migrate <角色 ID>
|
||||
把舊格式 <ID>.md 拆成 <ID>.identity.md 與 <ID>.soul.md
|
||||
--export <路徑> 匯出目前角色定義、資產與記憶為 .tar.gz
|
||||
--export <角色 ID> <路徑>
|
||||
--install-cron 安裝/更新睡眠排程(每小時檢查一次)
|
||||
@@ -424,6 +426,107 @@ remove_cron() {
|
||||
return 0
|
||||
}
|
||||
|
||||
migrate_role_files() {
|
||||
# 把舊格式單一 <ID>.md 拆成 <ID>.identity.md(身分)與 <ID>.soul.md(人格)。
|
||||
#
|
||||
# 拆分判準:「我是誰」進 identity(ID、顯示名稱、來源、關係定位、簽名 emoji),
|
||||
# 「我怎麼想」進 soul(本質、氛圍)。共用行為區塊**不再寫入角色檔** ——
|
||||
# 它由 role_load.sh 直接注入且 SKILL.md 有完整文件,重複第三份只會增加漏同步的機會。
|
||||
local id="$1" legacy identity soul stamp
|
||||
[ -n "$id" ] || { role_log "ERR" "缺少角色 ID"; return 1; }
|
||||
legacy="$(role_legacy_file "$id")"
|
||||
identity="$(role_identity_file "$id")"
|
||||
soul="$(role_soul_file "$id")"
|
||||
|
||||
[ -f "$legacy" ] || { role_log "ERR" "找不到舊格式角色檔:${legacy}"; return 1; }
|
||||
if [ -f "$identity" ] || [ -f "$soul" ]; then
|
||||
role_log "ERR" "新格式檔案已存在,為避免覆寫請先自行備份或移除:${identity} / ${soul}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
stamp="$(role_now)"
|
||||
node - "$legacy" "$identity" "$soul" "$stamp" <<'NODE_MIGRATE' || { role_log "ERR" "拆檔失敗:${legacy}"; return 1; }
|
||||
const fs = require("fs");
|
||||
const [, , legacy, identityOut, soulOut, stamp] = process.argv;
|
||||
const raw = fs.readFileSync(legacy, "utf8");
|
||||
|
||||
function parseFrontmatter(text) {
|
||||
const m = text.match(/^---\n([\s\S]*?)\n---\n?/);
|
||||
const data = {};
|
||||
if (!m) return data;
|
||||
for (const line of m[1].split(/\r?\n/)) {
|
||||
const i = line.indexOf(":");
|
||||
if (i < 0) continue;
|
||||
data[line.slice(0, i).trim()] = line.slice(i + 1).trim();
|
||||
}
|
||||
return data;
|
||||
}
|
||||
function section(text, title) {
|
||||
const re = new RegExp(`^##\\s+${title.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[^\\n]*\\n([\\s\\S]*?)(?=^##\\s+|$(?![\\s\\S]))`, "m");
|
||||
return (text.match(re) || [, ""])[1].trim();
|
||||
}
|
||||
|
||||
const fm = parseFrontmatter(raw);
|
||||
const id = fm.id || legacy.replace(/^.*\//, "").replace(/\.md$/, "");
|
||||
const name = fm.name || (raw.match(/^#\s+(.+)$/m) || [, id])[1].trim();
|
||||
const emoji = fm.emoji || "";
|
||||
const nature = section(raw, "本質(nature)") || fm.nature || "";
|
||||
const vibe = section(raw, "氛圍(vibe)") || fm.vibe || "";
|
||||
const emojiSection = section(raw, "簽名 emoji") || emoji;
|
||||
|
||||
const identity = [
|
||||
"---",
|
||||
`id: ${id}`,
|
||||
`name: ${name}`,
|
||||
`emoji: ${emoji}`,
|
||||
`created: ${fm.created || stamp}`,
|
||||
`updated: ${stamp}`,
|
||||
"---",
|
||||
"",
|
||||
`# ${name} ${emoji}`.trim(),
|
||||
"",
|
||||
"## 來源(source)",
|
||||
"",
|
||||
"(未設定:角色出自哪部作品、正式名稱或背景設定)",
|
||||
"",
|
||||
"## 關係定位(relationship)",
|
||||
"",
|
||||
"(未設定:與使用者的關係、偏好的稱呼、必須守住的邊界)",
|
||||
"",
|
||||
"## 簽名 emoji",
|
||||
"",
|
||||
emojiSection || "(未設定)",
|
||||
"",
|
||||
].join("\n");
|
||||
|
||||
const soul = [
|
||||
"---",
|
||||
`id: ${id}`,
|
||||
`updated: ${stamp}`,
|
||||
"---",
|
||||
"",
|
||||
"## 本質(nature)",
|
||||
"",
|
||||
nature || "(未設定)",
|
||||
"",
|
||||
"## 氛圍(vibe)",
|
||||
"",
|
||||
vibe || "(未設定)",
|
||||
"",
|
||||
].join("\n");
|
||||
|
||||
fs.writeFileSync(identityOut, identity, "utf8");
|
||||
fs.writeFileSync(soulOut, soul, "utf8");
|
||||
NODE_MIGRATE
|
||||
|
||||
role_log "INF" "已拆分:${identity}"
|
||||
role_log "INF" "已拆分:${soul}"
|
||||
role_log "INF" "舊檔保留未動:${legacy}(確認新格式正常後可自行移除或備份)"
|
||||
role_log "INF" "共用行為未寫入角色檔:由 role_load.sh 注入,內容見 role skill 文件"
|
||||
role_log "WRN" "來源與關係定位為待填空白,請補上後再重開工作階段"
|
||||
return 0
|
||||
}
|
||||
|
||||
export_agent_definition() {
|
||||
# 把角色的 SOUL 匯出成 sub agent 定義,讓任何角色都能被其他角色派工協助。
|
||||
#
|
||||
@@ -441,10 +544,15 @@ export_agent_definition() {
|
||||
|
||||
name="$(sed -n 's/^name:[[:space:]]*//p' "$def" | head -n 1)"
|
||||
emoji="$(sed -n 's/^emoji:[[:space:]]*//p' "$def" | head -n 1)"
|
||||
nature="$(sed -n '/^## 本質/,/^## /p' "$def" | sed '1d;/^##/d' | sed '/^[[:space:]]*$/d')"
|
||||
vibe="$(sed -n '/^## 氛圍/,/^## /p' "$def" | sed '1d;/^##/d' | sed '/^[[:space:]]*$/d')"
|
||||
[ -n "$nature" ] || nature="$(sed -n 's/^nature:[[:space:]]*//p' "$def" | head -n 1)"
|
||||
[ -n "$vibe" ] || vibe="$(sed -n 's/^vibe:[[:space:]]*//p' "$def" | head -n 1)"
|
||||
# 新格式的人格在 soul 檔,只讀 identity 會得到空人格
|
||||
local soul_src="$def"
|
||||
if role_is_new_format "$target_role" && [ -f "$(role_soul_file "$target_role")" ]; then
|
||||
soul_src="$(role_soul_file "$target_role")"
|
||||
fi
|
||||
nature="$(sed -n '/^## 本質/,/^## /p' "$soul_src" | sed '1d;/^##/d' | sed '/^[[:space:]]*$/d')"
|
||||
vibe="$(sed -n '/^## 氛圍/,/^## /p' "$soul_src" | sed '1d;/^##/d' | sed '/^[[:space:]]*$/d')"
|
||||
[ -n "$nature" ] || nature="$(sed -n 's/^nature:[[:space:]]*//p' "$soul_src" | head -n 1)"
|
||||
[ -n "$vibe" ] || vibe="$(sed -n 's/^vibe:[[:space:]]*//p' "$soul_src" | head -n 1)"
|
||||
name="${name:-$target_role}"
|
||||
|
||||
if [ -f "$out_file" ]; then
|
||||
@@ -535,7 +643,14 @@ show_status() {
|
||||
role_in_sleep_window && window="是"
|
||||
printf '| 項目 | 值 |\n| --- | --- |\n'
|
||||
printf '| 角色 | %s |\n' "$ROLE"
|
||||
printf '| 角色定義檔 | %s |\n' "$(role_file "$ROLE")"
|
||||
if role_is_new_format "$ROLE"; then
|
||||
printf '| 角色格式 | 新格式(身分/人格分離) |\n'
|
||||
printf '| 身分檔 | %s |\n' "$(role_identity_file "$ROLE")"
|
||||
printf '| 人格檔 | %s%s |\n' "$(role_soul_file "$ROLE")" "$([ -f "$(role_soul_file "$ROLE")" ] || printf '(缺少)')"
|
||||
else
|
||||
printf '| 角色格式 | 舊格式(單一檔案,可用 --migrate 拆分) |\n'
|
||||
printf '| 角色定義檔 | %s |\n' "$(role_file "$ROLE")"
|
||||
fi
|
||||
printf '| 睡眠時段 | %s–%s |\n' "$(role_sleep_start)" "$(role_sleep_end)"
|
||||
printf '| 目前是否睡眠中 | %s |\n' "$window"
|
||||
printf '| cron 排程 | %s |\n' "$cron_state"
|
||||
@@ -554,7 +669,7 @@ show_status() {
|
||||
|
||||
export_role_archive() {
|
||||
# 匯出目前角色定義、專屬資產與記憶目錄,供備份或轉移使用
|
||||
local destination="$1" stamp role_def role_assets memory_dir archive_dir archive tmp
|
||||
local destination="$1" stamp role_def role_assets role_checks memory_dir archive_dir archive tmp
|
||||
[ -n "$destination" ] || { role_log "ERR" "缺少匯出路徑"; return 1; }
|
||||
command -v tar >/dev/null 2>&1 || { role_log "ERR" "找不到 tar,無法建立壓縮檔"; return 1; }
|
||||
command -v mktemp >/dev/null 2>&1 || { role_log "ERR" "找不到 mktemp,無法建立暫存目錄"; return 1; }
|
||||
@@ -583,11 +698,30 @@ export_role_archive() {
|
||||
|
||||
role_def="$(role_file "$ROLE")"
|
||||
role_assets="$(role_home)/${ROLE}.assets"
|
||||
role_checks="$(role_home)/${ROLE}.checks"
|
||||
memory_dir="$(role_memory_home)/${ROLE}"
|
||||
tmp="$(mktemp -d)" || { role_log "ERR" "無法建立暫存目錄"; return 1; }
|
||||
mkdir -p "$tmp/.roles" "$tmp/.memory"
|
||||
cp "$role_def" "$tmp/.roles/${ROLE}.md" || { rm -rf "$tmp"; role_log "ERR" "無法複製角色定義檔"; return 1; }
|
||||
|
||||
# 依實際格式複製,不可一律當成舊格式的 <ID>.md ——
|
||||
# 否則新格式會被寫成舊檔名且遺失人格檔,備份就救不回角色
|
||||
if role_is_new_format "$ROLE"; then
|
||||
cp "$(role_identity_file "$ROLE")" "$tmp/.roles/${ROLE}.identity.md" \
|
||||
|| { rm -rf "$tmp"; role_log "ERR" "無法複製身分檔"; return 1; }
|
||||
if [ -f "$(role_soul_file "$ROLE")" ]; then
|
||||
cp "$(role_soul_file "$ROLE")" "$tmp/.roles/${ROLE}.soul.md" \
|
||||
|| { rm -rf "$tmp"; role_log "ERR" "無法複製人格檔"; return 1; }
|
||||
else
|
||||
role_log "WRN" "新格式缺少人格檔,匯出將不含 ${ROLE}.soul.md"
|
||||
fi
|
||||
# 遷移後尚未移除的舊檔一併保留,方便回溯
|
||||
[ -f "$(role_legacy_file "$ROLE")" ] && cp "$(role_legacy_file "$ROLE")" "$tmp/.roles/${ROLE}.md"
|
||||
else
|
||||
cp "$role_def" "$tmp/.roles/${ROLE}.md" || { rm -rf "$tmp"; role_log "ERR" "無法複製角色定義檔"; return 1; }
|
||||
fi
|
||||
|
||||
[ -d "$role_assets" ] && cp -a "$role_assets" "$tmp/.roles/"
|
||||
[ -d "$role_checks" ] && cp -a "$role_checks" "$tmp/.roles/"
|
||||
[ -d "$memory_dir" ] && cp -a "$memory_dir" "$tmp/.memory/"
|
||||
cat > "$tmp/role-export.json" <<EOF_EXPORT
|
||||
{
|
||||
@@ -662,6 +796,10 @@ case "$MODE" in
|
||||
require_role
|
||||
sleep_cycle "手動"
|
||||
;;
|
||||
--migrate)
|
||||
[ -n "${2:-}" ] || { role_log "ERR" "用法:role_sleep.sh --migrate <角色 ID>"; exit 1; }
|
||||
migrate_role_files "$2"
|
||||
;;
|
||||
--agent)
|
||||
[ -n "${2:-}" ] || { role_log "ERR" "用法:role_sleep.sh --agent <角色 ID> [輸出目錄]"; exit 1; }
|
||||
export_agent_definition "$2" "${3:-}"
|
||||
|
||||
Reference in New Issue
Block a user