From 8af0d5c387833813094a1f3aa425c1bf2759cdb9 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 3 Aug 2026 09:03:06 +0000 Subject: [PATCH] =?UTF-8?q?feat(=E9=97=9C=E4=BF=82):=20=E6=9C=AA=E4=BF=AE?= =?UTF-8?q?=E5=BE=A9=E7=9A=84=E8=A3=82=E7=97=95=EF=BC=8C=E8=A6=AA=E8=BF=91?= =?UTF-8?q?=E5=BA=A6=E9=AB=98=E4=B8=8D=E7=AD=89=E6=96=BC=E6=B2=92=E4=BA=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit closeness/trust 是純量:吵完架把親近度調低,看起來像「本來就沒那麼近」, 而不是「很近,但上次那件事還沒和好」。後者才是真的關係——兩個人可以很親近, 同時之間卡著一件事,那兩件事各自獨立,壓不進同一個數字裡。 節點多一個 rifts 陣列(一個人可以同時卡著幾件事),relation rift 管它。 語氣上:riftBrief() 在 toneDirective 裡明講「親近度高不等於沒事,這件事解決之前 語氣裡要留一點沒散掉的東西」,並要求不必一直提它、但不能像沒發生過。 回想上:裂痕帶著那則長期記憶的名字(--memory),人格要細節自己 recall, 不必等使用者提起那件事——沒和好的事本來就會自己浮上來。 --heal 是寫上 healed_at,不是刪掉那一筆:和好過跟沒發生過不一樣,之後回頭看 要分得出來。列出來時和好過的用 ✔ 標。 Co-Authored-By: Claude Opus 5 (1M context) --- scripts/persona-lib.mjs | 43 ++++++++++++++++++++++++++++++++++ scripts/persona.mjs | 51 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) diff --git a/scripts/persona-lib.mjs b/scripts/persona-lib.mjs index 522c998..bee3c31 100644 --- a/scripts/persona-lib.mjs +++ b/scripts/persona-lib.mjs @@ -4811,6 +4811,46 @@ export function toneFor(node) { // 放這裡的理由:關係圖每輪都會被讀進 ``,稱呼規則就不用跟關鍵詞搶召回。 export const STYLE_FACETS = ["稱呼", "敬語", "口頭禪", "禁忌", "習慣", "羞怯稱呼"]; +/** + * 未修復的裂痕。 + * + * `closeness`/`trust` 是純量:吵完架把親近度調低,看起來像「本來就沒那麼近」, + * 而不是「很近,但上次那件事還沒和好」。後者才是真的關係——**兩個人可以很親近, + * 同時之間卡著一件事**,那兩件事各自獨立,壓不進同一個數字裡。 + * + * 一個人可以同時卡著幾件事,所以是陣列;和好不是刪掉,是寫上 `healed_at`—— + * 「和好過」跟「沒發生過」不一樣。 + */ +export function openRifts(slug, node) { + const rifts = Array.isArray(node?.rifts) ? node.rifts : []; + return rifts + .filter((r) => r && r.about && !r.healed_at) + .map((r) => ({ + at: injectSafeLine(r.at || ""), + about: injectSafeLine(r.about, 120), + memory: injectSafeLine(r.memory || ""), + })); +} + +/** + * 注入用的一段:跟這個人之間還卡著什麼。 + * + * 這一段同時是**回想**的入口:裂痕帶著那則長期記憶的名字,人格要細節自己 + * `recall` 得到,不必等使用者提起那件事——沒和好的事本來就會自己浮上來。 + */ +export function riftBrief(slug, node) { + const rifts = openRifts(slug, node); + if (!rifts.length) return ""; + const who = injectSafeLine(node?.name || node?.id); + return ( + ` **跟 ${who} 之間還有沒和好的事**(${rifts.length} 件):` + + rifts.map((r) => `${r.about}${r.at ? `(${r.at} 起)` : ""}${r.memory ? `|細節去 recall \`${r.memory}\`` : ""}`) + .join(";") + "\n" + + " 親近度高**不等於**沒事:這件事解決之前,語氣裡要留一點沒散掉的東西" + + "(不必一直提它,但不能像沒發生過)。真的談開了才用 `relation rift --heal` 收掉。" + ); +} + /** 羞恥度到這裡就換稱呼(跟破口、標點同一條門檻線)。 */ export const SHY_ADDRESS_AT = 60; /** 「害羞時要怎麼叫他」掛在關係節點的這個 facet 上。 */ @@ -4977,6 +5017,9 @@ export function toneDirective(slug, opts = {}) { " — 這時候退回語氣層的預設講法。", ); } + // 沒和好的事:純量調不出「很近但卡著一件事」,所以它獨立於 closeness/trust。 + const rift = riftBrief(slug, node); + if (rift) lines.push(rift); // 害羞是「叫不出來」,不是「退回更正式」——所以它不走 except 那條路(見 shyAddress)。 const shy = shyAddress(slug, node, modestyState(slug, opts).value); if (shy) { diff --git a/scripts/persona.mjs b/scripts/persona.mjs index d8139e4..00981a2 100644 --- a/scripts/persona.mjs +++ b/scripts/persona.mjs @@ -1965,6 +1965,53 @@ commands.relation = ({ flags, positional }) => { pushWikiLater(slug, session, flags); return; } + // 未修復的裂痕:純量(closeness/trust)調不出「很近,但上次那件事還沒和好」。 + if (action === "rift") { + const key = str(flags.id) || pl.slugify(str(flags.name) || ""); + if (!key) die("`rift` 需要 `--name`(或 `--id`)。"); + const data = graphOrDie(); + const node = data.nodes.find((n) => n.id === key || pl.slugify(n.name || "") === key); + if (!node) die(`關係圖裡找不到 \`${str(flags.name) || key}\`,請先用 \`relation node\` 建立。`); + node.rifts ??= []; + const about = str(flags.about); + if (flags.heal) { + // 和好不是刪掉那一筆:「和好過」跟「沒發生過」不一樣,之後回頭看要分得出來。 + const open = node.rifts.filter((r) => r && r.about && !r.healed_at); + if (!open.length) die(`跟 \`${node.name || key}\` 之間沒有還沒和好的事。`); + const target = about ? open.filter((r) => String(r.about).includes(about)) : open; + if (!target.length) die(`找不到「${about}」這一件(還沒和好的有:${open.map((r) => r.about).join("/")})。`); + for (const r of target) r.healed_at = pl.nowIso().slice(0, 10); + node.updated_at = pl.nowIso(); + pl.writeJson(pl.relationsJson(slug), data); + pl.renderRelations(slug); + ok(`收掉 ${target.length} 件:${target.map((r) => r.about).join("/")}。`); + pushWikiLater(slug, session, flags); + return; + } + if (!about) { + const open = pl.openRifts(slug, node); + const healed = (node.rifts || []).filter((r) => r?.healed_at); + emit({ node: node.id, open, healed }, flags.json, [ + `\`${node.name || node.id}\`:還沒和好 ${open.length} 件、和好過 ${healed.length} 件。`, + ...open.map((r) => ` - ${r.about}${r.at ? `(${r.at} 起)` : ""}${r.memory ? `|記憶 ${r.memory}` : ""}`), + ...healed.map((r) => ` - ✔ ${pl.injectSafeLine(r.about)}(${r.healed_at} 和好)`), + ]); + return; + } + node.rifts.push({ + at: str(flags.at) || pl.nowIso().slice(0, 10), + about, + // 帶上長期記憶的名字,人格要細節自己 recall——不必等使用者提起那件事。 + ...(str(flags.memory) ? { memory: str(flags.memory) } : {}), + }); + node.updated_at = pl.nowIso(); + pl.writeJson(pl.relationsJson(slug), data); + pl.renderRelations(slug); + ok(`記下來了:跟 \`${node.name || key}\` 之間「${about}」還沒和好。` + + "談開了再用 `relation rift --heal` 收掉。"); + pushWikiLater(slug, session, flags); + return; + } // 他本人要求過的語氣規則 → 直接掛在關係節點上(不另開檔案;關係圖每輪都會被讀)。 if (action === "style") { const key = str(flags.id) || pl.slugify(str(flags.name) || ""); @@ -3227,6 +3274,10 @@ const HELP = `persona.mjs — jsc-persona 人格 / 記憶 / 情緒 / 關係圖 C mindmap show|thread|list --session [--topic ] [--force] relation node|edge|render|show --session [--name --id --kind --bond --closeness --trust --note --tags --from --to --label --affinity] relation style --session --name [--facet 稱呼 --value 親愛的 --except anger>=40 --since --clear] + relation rift --session --name [--about "<那件事>" --at <日期> --memory <記憶名> | --heal] + 沒和好的事。純量(closeness/trust)調不出「很近,但上次那件事還沒和好」—— + 把親近度調低看起來像本來就沒那麼近。不帶 --about 就是列出來看。 + --heal 是寫上 healed_at,不是刪掉那一筆:和好過跟沒發生過不一樣。 relation speaker --session --name | --clear (使用者在關係圖裡是誰 → 決定語氣層) relation doctor --session [--json] 記憶裡的人名對不對得上關係圖節點:對不到的人名、 同名歧義、指到不存在節點的 id、沒人提到的節點。