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:
@@ -110,11 +110,24 @@ fi
|
||||
# ------------------------------------------------------------------------------
|
||||
# 非睡眠時段:組出角色人格 + 操作規則 + 記憶
|
||||
# ------------------------------------------------------------------------------
|
||||
ROLE_PROFILE="$(node - "$ROLE_DEF" <<'NODE_PROFILE' 2>/dev/null
|
||||
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?/);
|
||||
@@ -135,15 +148,28 @@ function section(text, title) {
|
||||
}
|
||||
|
||||
const fm = parseFrontmatter(raw);
|
||||
const soulFm = soulRaw ? parseFrontmatter(soulRaw) : {};
|
||||
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 || "";
|
||||
|
||||
// 人格優先取自 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 || ""}`,
|
||||
`- 角色 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)",
|
||||
"",
|
||||
@@ -156,7 +182,7 @@ const lines = [
|
||||
"## 簽名 emoji",
|
||||
"",
|
||||
emoji || fm.emoji || "(未設定)",
|
||||
];
|
||||
);
|
||||
|
||||
process.stdout.write(lines.join("\n"));
|
||||
NODE_PROFILE
|
||||
|
||||
Reference in New Issue
Block a user