fix(role): 人格檔的自由章節與身分檔前言段落也要注入

使用者依新格式在人格檔補寫了核心信念、語氣與風格、邊界與規範,並在身分檔標題下
以條目寫存在本質、角色原型、主要稱呼 —— 但兩者都不會被載入,等於白寫:

- role_load.sh 原本只抽 ## 本質 與 ## 氛圍 兩節,人格檔其他章節被靜默丟棄
- 也只抽 frontmatter 與具名章節,身分檔「標題後、第一個 ## 之前」的前言段落被丟棄

修正:
- 新增 extraSections():注入人格檔除本質與氛圍之外的所有 ## 章節,不限章節名
- 新增 preamble():注入身分檔的前言段落
- SKILL.md 補完整人格檔結構範例(核心信念/語氣與風格/邊界與規範為選填章節)、
  新增「注入規則」對照表,並註明角色專屬邊界只能加嚴不可放寬共用行為
- --new 流程第 8 步說明兩個檔案各自該寫什麼

驗證方法的修正也一併記錄:第一次驗證時字串比對命中的其實是「近期對話」區塊裡
使用者剛貼上的原文,造成 16/17 的假陽性。改為只比對 SOUL 區塊後,才驗出前言段落
實際未被注入。往後驗證注入結果一律限定在目標區塊內比對。

回歸:新格式(含前言與自由章節)、舊格式單一檔案、新格式但缺人格檔三種情境
均正常載入,缺人格檔時輸出警告且不會崩。

版號沿用 0.0.5(master 為 0.0.4,同一 PR 不再累加)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jeffery
2026-07-29 11:30:10 +08:00
co-authored by Claude Opus 5
parent f76915bc87
commit 40c2d2ae46
2 changed files with 67 additions and 1 deletions
+35
View File
@@ -157,6 +157,37 @@ const natureFm = soulRaw ? soulFm : fm;
const nature = section(natureSrc, "本質(nature") || natureFm.nature || "";
const vibe = section(natureSrc, "氛圍(vibe") || natureFm.vibe || "";
// soul 檔的其餘章節(例如核心信念、語氣與風格、邊界與規範)也要注入。
// 只抽固定的兩節會讓使用者在人格檔裡寫的其他章節被靜默丟棄。
function extraSections(text, skip) {
if (!text) return "";
const body = text.replace(/^---\n[\s\S]*?\n---\n?/, "");
const out = [];
const re = /^##\s+(.+)$/gm;
const marks = [];
let m;
while ((m = re.exec(body)) !== null) marks.push([m.index, m[0].length, m[1].trim()]);
for (let i = 0; i < marks.length; i += 1) {
const [idx, len, title] = marks[i];
if (skip.some((s) => title.startsWith(s))) continue;
const end = i + 1 < marks.length ? marks[i + 1][0] : body.length;
const content = body.slice(idx + len, end).trim();
if (content) out.push(`## ${title}`, "", content);
}
return out.join("\n");
}
const extra = extraSections(soulRaw, ["本質", "氛圍"]);
// 標題後、第一個 ## 之前的前言段落(使用者常在此寫存在本質、角色原型等摘要條目)。
// 只抽 frontmatter 與具名章節會讓這段被靜默丟棄。
function preamble(text) {
if (!text) return "";
const body = text.replace(/^---\n[\s\S]*?\n---\n?/, "").replace(/^#\s+[^\n]*\n/, "");
const idx = body.search(/^##\s+/m);
return (idx < 0 ? body : body.slice(0, idx)).trim();
}
const intro = preamble(raw);
// 身分只可能在 identity/舊檔裡
const emoji = section(raw, "簽名 emoji") || fm.emoji || "";
const source = section(raw, "來源(source") || fm.source || "";
@@ -167,6 +198,7 @@ const lines = [
`- 顯示名稱:${fm.name || title || ""}`,
`- 簽名 emoji${fm.emoji || ""}`,
];
if (intro) lines.push("", intro);
if (source) lines.push("", "## 來源(source", "", source);
if (relationship) lines.push("", "## 關係定位(relationship", "", relationship);
lines.push(
@@ -178,6 +210,9 @@ lines.push(
"## 氛圍(vibe",
"",
vibe || "(未設定)",
);
if (extra) lines.push("", extra);
lines.push(
"",
"## 簽名 emoji",
"",