feat(稱呼): 害羞時叫不出名字,節點可設羞怯稱呼

既有的 except(anger>=40 → 退回全名)走的是生氣方向:規則被暫停、退回查表的
更正式講法。害羞是反方向——不是更正式,是叫不出來:改口、講到一半換稱呼、
把名字吞掉。所以另開一條路,不硬塞進 except。

shyAddress():羞恥度過 SHY_ADDRESS_AT(60)就換稱呼。節點設了新的 羞怯稱呼
facet 就用它,沒設就吞掉名字改用「你」——不叫名字本身就是訊號,這是預設值
而不是缺省行為。

門檻取 60,跟破口、標點、嘴硬那一級同一條線。女性預設 70 因此本來就落在這條線上,
那是 G3 要的效果,不是副作用。

羞怯稱呼不列進「他要求過的語氣規則」那一行:它只在過門檻時生效,兩邊都列會讓
人格同時看到「叫他⋯那個」跟「平常叫他⋯那個」。relation style 的清單裡照樣看得到。

toneDirective 改吃 opts,才拿得到這一輪的 state/prev/tone 去算羞恥度。

selftest 728 → 735。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-03 08:33:19 +00:00
co-authored by Claude Opus 5
parent b5a5f0a6bf
commit fe17b20010
3 changed files with 77 additions and 7 deletions
+39 -4
View File
@@ -4593,7 +4593,28 @@ export function toneFor(node) {
// 個人化語氣規則直接掛在關係節點的 `style` 上(不另外開檔案):
// node.style = { 稱呼: { value: "親愛的", except: "anger>=40", since: "2026-07-30" }, ... }
// 放這裡的理由:關係圖每輪都會被讀進 `<persona-context>`,稱呼規則就不用跟關鍵詞搶召回。
export const STYLE_FACETS = ["稱呼", "敬語", "口頭禪", "禁忌", "習慣"];
export const STYLE_FACETS = ["稱呼", "敬語", "口頭禪", "禁忌", "習慣", "羞怯稱呼"];
/** 羞恥度到這裡就換稱呼(跟破口、標點同一條門檻線)。 */
export const SHY_ADDRESS_AT = 60;
/** 「害羞時要怎麼叫他」掛在關係節點的這個 facet 上。 */
export const SHY_FACET = "羞怯稱呼";
/**
* 害羞時的稱呼
*
* `except``anger>=40` 退回全名走的是**生氣**方向規則被暫停退回查表的講法
* 害羞是反方向不是退回更正式**叫不出來**改口講到一半換稱呼把名字吞掉
*
* 節點自己設了 `羞怯稱呼` 就用它沒設就吞掉名字改用
* 不叫名字本身就是訊號這是預設值而不是缺省行為
*/
export function shyAddress(slug, node, modesty) {
const value = Number(modesty);
if (!node || !Number.isFinite(value) || value < SHY_ADDRESS_AT) return null;
const own = node.style?.[SHY_FACET]?.value;
return { modesty: Math.round(value), value: own ? injectSafeLine(own) : null, own: Boolean(own) };
}
/** `except` 形如 `anger>=40`:拿當下情緒判斷這條規則現在生不生效。 */
function styleSuspended(except, emotion) {
@@ -4713,7 +4734,7 @@ export function toneLayerOf(slug) {
return node ? toneFor(node).layer : null;
}
export function toneDirective(slug) {
export function toneDirective(slug, opts = {}) {
const node = speakerNode(slug);
if (!node) return "";
const t = toneFor(node);
@@ -4722,7 +4743,9 @@ export function toneDirective(slug) {
`${t.inferred ? "bond 由 tags 推測" : ""})→ **語氣層:${t.layer}** — ${t.guide}`,
];
// 他本人要求過的規則優先於查表:查表算距離,這裡記「他要你怎麼叫他」。
const rules = styleRules(slug, node);
// `羞怯稱呼` 不列在這裡——它只在羞恥度過門檻時生效,由下面那行單獨講。
// 兩邊都列會讓人格同時看到「叫他⋯那個」與「平常叫他⋯那個」,那是兩件事。
const rules = styleRules(slug, node).filter((r) => r.facet !== SHY_FACET);
const live = rules.filter((r) => !r.suspended);
const held = rules.filter((r) => r.suspended);
if (live.length) {
@@ -4738,6 +4761,18 @@ export function toneDirective(slug) {
" — 這時候退回語氣層的預設講法。",
);
}
// 害羞是「叫不出來」,不是「退回更正式」——所以它不走 except 那條路(見 shyAddress)。
const shy = shyAddress(slug, node, modestyState(slug, opts).value);
if (shy) {
lines.push(
shy.own
? ` **現在害羞(羞恥度 ${shy.modesty}):叫他「${shy.value}」**,不是平常那個叫法。` +
"改口、講到一半換稱呼都算——卡住的那個地方就是害羞。"
: ` **現在害羞(羞恥度 ${shy.modesty}):把名字吞掉,改用「你」**。` +
"不叫名字本身就是訊號;改口、講到一半換稱呼都算。" +
`要專屬的叫法就在那個節點設 \`${SHY_FACET}\``,
);
}
lines.push(
" 這一層決定距離感與稱呼,情緒只負責溫度與句長,不會蓋過它;" +
"檢查標準:這句話換成對一個「禮貌層」的人說也毫無違和,就代表你沒進到這一層。",
@@ -5859,7 +5894,7 @@ export function turnContext(slug, sessionId, prompt = "") {
split: Boolean(split),
});
// 語氣層:由「使用者在關係圖裡是誰」+ bond × 親近度算出來,不靠人格自己記得。
const tone = toneDirective(slug);
const tone = toneDirective(slug, speakOpts);
if (tone) lines.push(tone);
// 語氣層:他講過的原句與「遇到事會做什麼」。每輪最多 2 到 3 條,而且挑跟這一輪
// 對象/話題對得上的——全注入會變成表演(他開始照抄自己的舊台詞)。