relation: 由關係推導語氣層(bond × 親近度 + 節點上的個人化規則)
人格對伴侶與對女兒用同一種溫度講話,因為 kind 只說得出「這是人還是程式」, 說不出「跟我什麼關係」。語氣需要的是後者。 - 關係節點新增 bond(partner/child/parent/sibling/friend/mentor/ally/rival/ stranger);舊節點沒有的話從 tags/note 推測 - bond × 親近度 → 語氣層查表(伴侶 98 = 老夫老妻、子女 97 = 母親), 解決「都是 9x 分卻分不出戀人與母女」 - node.style[facet] 記使用者本人要求過的規則(稱呼/敬語/口頭禪/禁忌/習慣), 優先於查表;不另開檔案,因為關係圖每輪都會被讀進 context, 這類規則不必跟關鍵詞召回競爭 - style 規則可帶 except(如 anger>=40),情緒成立時該規則暫停、退回預設講法 - relation speaker 指定使用者是關係圖裡的哪個節點,turnContext 每輪注入語氣指示 - 新增 relation style / relation speaker 子指令;selftest +8 項(共 228 項) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+69
-3
@@ -986,10 +986,14 @@ commands.relation = ({ flags, positional }) => {
|
||||
if (action === "node") {
|
||||
const name = str(flags.name);
|
||||
if (!name) die("`node` 需要 `--name`。");
|
||||
const bond = str(flags.bond)?.toLowerCase() || null;
|
||||
if (bond && !pl.BONDS.includes(bond)) die(`未知的 --bond:${bond}(可用 ${pl.BONDS.join("/")})`);
|
||||
pl.upsertRelationNode(slug, {
|
||||
id: str(flags.id) || pl.slugify(name),
|
||||
name,
|
||||
kind: str(flags.kind) || "human",
|
||||
// bond 決定語氣層(伴侶/子女/摯友…);kind 只說這是人還是程式,分不出語氣。
|
||||
bond,
|
||||
closeness: flags.closeness !== undefined ? pl.clamp(num(flags.closeness, 30)) : null,
|
||||
trust: flags.trust !== undefined ? pl.clamp(num(flags.trust, 30)) : null,
|
||||
note: str(flags.note) || null,
|
||||
@@ -998,10 +1002,70 @@ commands.relation = ({ flags, positional }) => {
|
||||
last_contact_at: flags.contact ? pl.nowIso() : null,
|
||||
});
|
||||
pl.renderRelations(slug);
|
||||
ok(`關係節點 \`${name}\` 已更新${flags.contact ? "(已記錄本次接觸時間)" : ""}。`);
|
||||
const tone = pl.toneFor(pl.loadRelations(slug).nodes.find((n) => n.id === (str(flags.id) || pl.slugify(name))));
|
||||
ok(
|
||||
`關係節點 \`${name}\` 已更新${flags.contact ? "(已記錄本次接觸時間)" : ""}` +
|
||||
`|${tone.bond_label}・語氣層 ${tone.layer}。`,
|
||||
);
|
||||
pushWikiLater(slug, session, flags);
|
||||
return;
|
||||
}
|
||||
// 他本人要求過的語氣規則 → 直接掛在關係節點上(不另開檔案;關係圖每輪都會被讀)。
|
||||
if (action === "style") {
|
||||
const key = str(flags.id) || pl.slugify(str(flags.name) || "");
|
||||
if (!key) die("`style` 需要 `--name`(或 `--id`)。");
|
||||
const data = pl.loadRelations(slug);
|
||||
const node = data.nodes.find((n) => n.id === key || pl.slugify(n.name || "") === key);
|
||||
if (!node) die(`關係圖裡找不到 \`${str(flags.name) || key}\`,請先用 \`relation node\` 建立。`);
|
||||
const facet = str(flags.facet);
|
||||
if (!facet) {
|
||||
const rules = pl.styleRules(slug, node);
|
||||
emit({ node: node.id, rules }, flags.json, [
|
||||
`\`${node.name || node.id}\` 的語氣規則(${rules.length}):`,
|
||||
...rules.map((r) =>
|
||||
` - ${r.facet}→「${r.value}」${r.except ? `(例外:${r.except}${r.suspended ? ",目前成立→暫停" : ""})` : ""}` +
|
||||
`${r.since ? `|${r.since} 起` : ""}`),
|
||||
]);
|
||||
return;
|
||||
}
|
||||
node.style ??= {};
|
||||
if (flags.clear) {
|
||||
delete node.style[facet];
|
||||
ok(`已移除 \`${node.name || node.id}\` 的「${facet}」規則。`);
|
||||
} else {
|
||||
const value = str(flags.value);
|
||||
if (!value) die("`style` 需要 `--value`(移除請用 `--clear`)。");
|
||||
node.style[facet] = {
|
||||
value,
|
||||
except: str(flags.except) || null, // 形如 anger>=40:情緒成立時這條規則暫停
|
||||
since: str(flags.since) || pl.nowIso().slice(0, 10),
|
||||
};
|
||||
ok(`\`${node.name || node.id}\`:${facet}→「${value}」${str(flags.except) ? `(例外 ${str(flags.except)})` : ""} 已記下。`);
|
||||
}
|
||||
node.updated_at = pl.nowIso();
|
||||
pl.writeJson(pl.relationsJson(slug), data);
|
||||
pushWikiLater(slug, session, flags);
|
||||
return;
|
||||
}
|
||||
// 使用者在關係圖裡是誰 → 每輪 `<persona-context>` 就能算出該用哪一層語氣說話。
|
||||
if (action === "speaker") {
|
||||
const config = pl.loadConfig(slug);
|
||||
if (flags.clear) {
|
||||
delete config.speaker_node;
|
||||
pl.writeJson(pl.configPath(slug), config);
|
||||
ok("已取消對話對象設定(語氣層回到預設)。");
|
||||
return;
|
||||
}
|
||||
const key = str(flags.id) || pl.slugify(str(flags.name) || "");
|
||||
if (!key) die("`speaker` 需要 `--name`(或 `--id`),取消請用 `--clear`。");
|
||||
const node = pl.loadRelations(slug).nodes.find((n) => n.id === key || pl.slugify(n.name || "") === key);
|
||||
if (!node) die(`關係圖裡找不到 \`${str(flags.name) || key}\`,請先用 \`relation node\` 建立。`);
|
||||
config.speaker_node = node.id;
|
||||
pl.writeJson(pl.configPath(slug), config);
|
||||
const tone = pl.toneFor(node);
|
||||
ok(`對話對象設為 \`${node.name || node.id}\`|${tone.bond_label}・語氣層 ${tone.layer} — ${tone.guide}`);
|
||||
return;
|
||||
}
|
||||
if (action === "edge") {
|
||||
const to = str(flags.to);
|
||||
if (!to) die("`edge` 需要 `--to`。");
|
||||
@@ -1028,7 +1092,7 @@ commands.relation = ({ flags, positional }) => {
|
||||
]);
|
||||
return;
|
||||
}
|
||||
die(`未知 action:${action}(可用 node/edge/render/show)`);
|
||||
die(`未知 action:${action}(可用 node/edge/style/speaker/render/show)`);
|
||||
};
|
||||
|
||||
commands.invite = ({ flags }) => {
|
||||
@@ -1789,7 +1853,9 @@ const HELP = `persona.mjs — jsc-persona 人格 / 記憶 / 情緒 / 關係圖 C
|
||||
情緒與圖:
|
||||
emotion --session <id> [--apply joy=+10,...] [--baseline ...] [--trigger <why>]
|
||||
mindmap show|thread|list --session <id> [--topic <t>] [--force]
|
||||
relation node|edge|render|show --session <id> [--name --id --kind --closeness --trust --note --tags --from --to --label --affinity]
|
||||
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 speaker --session <id> --name <who> | --clear (使用者在關係圖裡是誰 → 決定語氣層)
|
||||
|
||||
多人格對話:
|
||||
invite --session <id> --guest <slug> [--guests <slug,slug>] [--host --room --topic] (自動開啟劇場模式)
|
||||
|
||||
Reference in New Issue
Block a user