feat(soul): 新增 soul 指令,SOUL.md 分區塊寫入(AniPersonaStudio 委託)

SOUL.md 目前只有使用者能透過編輯器直接改,AniPersonaStudio 的確認佇列
機制需要一個程式化的寫入路徑才能把使用者確認過的變更真的寫回核心。

新增 persona.mjs soul --session <id> --section <core_truths|boundaries|
vibe|continuity> --value <text> [--mode append|replace] [--by <確認人>]:

- 四個區塊各自用一組 <!-- aps:區塊:begin/end --> HTML 註解標記框住可寫範圍,
  標記外的內容(樣板原本寫好的條目、使用者手動編輯過的段落)永遠不動
- append(預設)接在後面,完全相同的內容不重複疊加;replace 整段換掉
- 標記不存在時(舊人格)自動在對應的 ## 標題後面補一組空的,不需要重建人格
- 權限模型跟 relation/mindmap 一致:requireSession + requireOwner
- --by 有帶會寫一筆 journal 留審計紀錄;成功後照既有慣例背景推 Wiki 區

SOUL.md 樣板補上四組標記;persona-lib.mjs 新增 SOUL_SECTIONS/
readSoulSection/updateSoulSection;selftest.mjs +9 案例(append/replace/
去重/權限/journal/舊人格自動補標記)。全部 781 項 selftest 通過。
This commit is contained in:
Jeffery
2026-08-11 11:11:41 +08:00
parent cbbb15ee58
commit 3e1c8eb221
4 changed files with 214 additions and 1 deletions
+46 -1
View File
@@ -53,7 +53,7 @@ function emit(payload, asJson, lines) {
const FLAGS = new Set([
"json", "quiet", "force", "takeover", "as-guest", "as-sleeper", "on", "off", "with-meta", "all",
"with-journal", "gzip", "record", "load", "allow-repeat", "clear", "release", "keep-lock", "contact",
"if-due", "no-gitea", "public", "rename", "from-source",
"if-due", "no-gitea", "public", "rename", "from-source", "show",
// 故事匯入:`--stdin` 吃管線進來的候選 JSON、`--accept-exact` 整批收下對得上節點的人名候選。
// `--apply`(novel merge)**故意不列**:`emotion --apply joy=+10` 也叫這個名字,
// 列進來會讓那個旗標變成布林,情緒就再也套不進去(踩過一次)。
@@ -1924,6 +1924,45 @@ commands.mindmap = ({ flags, positional }) => {
die(`未知 action:${action}(可用 show/thread/list)`);
};
/**
* SOUL.md 分區塊寫入(見 persona-lib.mjs 的 updateSoulSection 開頭說明)。
* 這是 SOUL.md 目前**唯一**的程式化寫入路徑;標記外的內容一律不動,
* 沒有 `--force` 這種繞過權限的旗標——這條指令本身就是明確授權的窄通道。
*/
commands.soul = ({ flags }) => {
const session = requireSession(flags);
const slug = hostOf(flags, session);
requireOwner(slug, session);
const section = str(flags.section).toLowerCase();
if (!(section in pl.SOUL_SECTIONS)) {
die(`\`--section\` 只能是 ${Object.keys(pl.SOUL_SECTIONS).join("/")},收到的是 \`${section || "(空)"}\`。`);
}
const mode = str(flags.mode, "append").toLowerCase();
if (!["append", "replace"].includes(mode)) die("`--mode` 只能是 append 或 replace(預設 append)。");
if (flags.show || (!flags.value && flags.value !== "")) {
const current = pl.readSoulSection(slug, section) || "(這個區塊目前是空的)";
emit({ persona: slug, section, value: pl.readSoulSection(slug, section) }, flags.json, [
`\`${slug}\` 的 SOUL.md/${pl.SOUL_SECTIONS[section]}(標記內,不含樣板原本的內容):`,
...current.split("\n").map((l) => ` ${l}`),
]);
return;
}
const value = str(flags.value);
const by = str(flags.by) || null;
let result;
try {
result = pl.updateSoulSection(slug, section, value, { mode, by });
} catch (err) {
die(err.message);
}
if (result.before === result.after) {
ok(`\`${pl.SOUL_SECTIONS[section]}\` 沒有變化(內容跟已經有的完全一樣)。`);
} else {
ok(`\`${slug}\` 的 SOUL.md/${pl.SOUL_SECTIONS[section]} 已${mode === "replace" ? "整段換掉" : "追加"}${by ? `(${by} 確認)` : ""}。`);
}
pushWikiLater(slug, session, flags);
};
commands.relation = ({ flags, positional }) => {
const session = requireSession(flags);
const slug = hostOf(flags, session);
@@ -3272,6 +3311,12 @@ const HELP = `persona.mjs — jsc-persona 人格 / 記憶 / 情緒 / 關係圖 C
套用時會飽和、受單輪預算限制(|delta| 總和 ≤ 60),再經交互抑制、慣性、
當日底色與疲勞調整——實際生效的值看 last_trigger.scaled
mindmap show|thread|list --session <id> [--topic <t>] [--force]
soul --session <id> --section <core_truths|boundaries|vibe|continuity>
[--value "<文字>" [--mode append|replace] [--by <確認人>] | --show]
SOUL.md 唯一的程式化寫入路徑;只碰對應區塊裡 aps:區塊:begin/end 那組 HTML 註解
標記之內的內容,標記外(含樣板原本寫好的條目、使用者手動編輯過的段落)
永遠不動。append(預設)接在後面且完全相同的內容不重複疊加;replace 整段換掉。
不帶 --value 或帶 --show 只讀不寫。標記不存在(舊人格)會自動補一組空的。
relation node|edge|render|show --session <id> [--name --id --kind --bond --closeness --trust --note --tags --from --to --label --affinity]
relation style --session <id> --name <who> [--facet 稱呼 --value 親愛的 --except anger>=40 --since --clear]
relation rift --session <id> --name <who> [--about "<那件事>" --at <日期> --memory <記憶名> | --heal]