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:
@@ -125,10 +125,17 @@ Q5–Q10 六題已於 2026/08/03 由使用者拍板,設計不再需要確認
|
|||||||
- 為什麼:真人在聊天軟體上的情緒,一半是靠「又補了一句」傳出去的。這是文字獨有、目前完全沒用到的手段
|
- 為什麼:真人在聊天軟體上的情緒,一半是靠「又補了一句」傳出去的。這是文字獨有、目前完全沒用到的手段
|
||||||
- 驗收:慌的時候會多一則短的;平靜時永遠只有一則
|
- 驗收:慌的時候會多一則短的;平靜時永遠只有一則
|
||||||
|
|
||||||
- [ ] **5.3 稱呼隨情緒偏移**
|
- [x] **5.3 稱呼隨情緒偏移**(2026/08/03 16:32:49 完成)
|
||||||
- `relation style` 的 `--except <情緒>>=<值>` 已能做條件替換(`anger>=40` → 退回全名)。缺的是**害羞方向**。
|
- `relation style` 的 `--except <情緒>>=<值>` 已能做條件替換(`anger>=40` → 退回全名)。缺的是**害羞方向**。
|
||||||
- [ ] 害羞方向:改口、講到一半換稱呼、把名字吞掉不叫
|
- [x] 害羞方向:改口、講到一半換稱呼、把名字吞掉不叫
|
||||||
- [ ] 每個關係節點可設「害羞時的稱呼」,沒設就退回吞掉名字(改用「你」)
|
—— `shyAddress()`。**刻意不走 `except` 那條路**:`except` 是「規則被暫停、
|
||||||
|
退回更正式的講法」(生氣方向),害羞是**叫不出來**,方向相反
|
||||||
|
- [x] 每個關係節點可設「害羞時的稱呼」,沒設就退回吞掉名字(改用「你」)
|
||||||
|
—— 新的 `羞怯稱呼` facet(`relation style --facet 羞怯稱呼`),
|
||||||
|
門檻 `SHY_ADDRESS_AT = 60`,跟破口/標點/嘴硬同一條線
|
||||||
|
(所以女性預設 70 本來就落在這條線上,這是 G3 要的)。
|
||||||
|
它不列進「他要求過的語氣規則」那行——只在過門檻時生效,列兩次會變成兩件事;
|
||||||
|
`relation style` 的清單裡照樣看得到
|
||||||
- 驗收:同一個對象,生氣時被叫全名、害羞時直接不叫名字
|
- 驗收:同一個對象,生氣時被叫全名、害羞時直接不叫名字
|
||||||
|
|
||||||
- [x] **5.4 鬧彆扭的階梯與三段式句法(G3 核心,依賴 5.7 的動作額度)**(2026/08/03 16:06:10 完成)
|
- [x] **5.4 鬧彆扭的階梯與三段式句法(G3 核心,依賴 5.7 的動作額度)**(2026/08/03 16:06:10 完成)
|
||||||
|
|||||||
+39
-4
@@ -4593,7 +4593,28 @@ export function toneFor(node) {
|
|||||||
// 個人化語氣規則直接掛在關係節點的 `style` 上(不另外開檔案):
|
// 個人化語氣規則直接掛在關係節點的 `style` 上(不另外開檔案):
|
||||||
// node.style = { 稱呼: { value: "親愛的", except: "anger>=40", since: "2026-07-30" }, ... }
|
// node.style = { 稱呼: { value: "親愛的", except: "anger>=40", since: "2026-07-30" }, ... }
|
||||||
// 放這裡的理由:關係圖每輪都會被讀進 `<persona-context>`,稱呼規則就不用跟關鍵詞搶召回。
|
// 放這裡的理由:關係圖每輪都會被讀進 `<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`:拿當下情緒判斷這條規則現在生不生效。 */
|
/** `except` 形如 `anger>=40`:拿當下情緒判斷這條規則現在生不生效。 */
|
||||||
function styleSuspended(except, emotion) {
|
function styleSuspended(except, emotion) {
|
||||||
@@ -4713,7 +4734,7 @@ export function toneLayerOf(slug) {
|
|||||||
return node ? toneFor(node).layer : null;
|
return node ? toneFor(node).layer : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toneDirective(slug) {
|
export function toneDirective(slug, opts = {}) {
|
||||||
const node = speakerNode(slug);
|
const node = speakerNode(slug);
|
||||||
if (!node) return "";
|
if (!node) return "";
|
||||||
const t = toneFor(node);
|
const t = toneFor(node);
|
||||||
@@ -4722,7 +4743,9 @@ export function toneDirective(slug) {
|
|||||||
`${t.inferred ? ",bond 由 tags 推測" : ""})→ **語氣層:${t.layer}** — ${t.guide}。`,
|
`${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 live = rules.filter((r) => !r.suspended);
|
||||||
const held = rules.filter((r) => r.suspended);
|
const held = rules.filter((r) => r.suspended);
|
||||||
if (live.length) {
|
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(
|
lines.push(
|
||||||
" 這一層決定距離感與稱呼,情緒只負責溫度與句長,不會蓋過它;" +
|
" 這一層決定距離感與稱呼,情緒只負責溫度與句長,不會蓋過它;" +
|
||||||
"檢查標準:這句話換成對一個「禮貌層」的人說也毫無違和,就代表你沒進到這一層。",
|
"檢查標準:這句話換成對一個「禮貌層」的人說也毫無違和,就代表你沒進到這一層。",
|
||||||
@@ -5859,7 +5894,7 @@ export function turnContext(slug, sessionId, prompt = "") {
|
|||||||
split: Boolean(split),
|
split: Boolean(split),
|
||||||
});
|
});
|
||||||
// 語氣層:由「使用者在關係圖裡是誰」+ bond × 親近度算出來,不靠人格自己記得。
|
// 語氣層:由「使用者在關係圖裡是誰」+ bond × 親近度算出來,不靠人格自己記得。
|
||||||
const tone = toneDirective(slug);
|
const tone = toneDirective(slug, speakOpts);
|
||||||
if (tone) lines.push(tone);
|
if (tone) lines.push(tone);
|
||||||
// 語氣層:他講過的原句與「遇到事會做什麼」。每輪最多 2 到 3 條,而且挑跟這一輪
|
// 語氣層:他講過的原句與「遇到事會做什麼」。每輪最多 2 到 3 條,而且挑跟這一輪
|
||||||
// 對象/話題對得上的——全注入會變成表演(他開始照抄自己的舊台詞)。
|
// 對象/話題對得上的——全注入會變成表演(他開始照抄自己的舊台詞)。
|
||||||
|
|||||||
@@ -2026,6 +2026,34 @@ check("relation speaker 設定後,context 會注入語氣層與稱呼規則",
|
|||||||
return res.status === 0 && pl.loadConfig("alpha").speaker_node === pl.slugify("伴侶甲") &&
|
return res.status === 0 && pl.loadConfig("alpha").speaker_node === pl.slugify("伴侶甲") &&
|
||||||
ctx.includes("語氣層:老夫老妻") && ctx.includes("親愛的");
|
ctx.includes("語氣層:老夫老妻") && ctx.includes("親愛的");
|
||||||
})());
|
})());
|
||||||
|
// 稱呼隨情緒偏移:生氣走 except(退回全名),害羞是反方向(叫不出來)
|
||||||
|
check("羞怯稱呼是 STYLE_FACETS 的一格", pl.STYLE_FACETS.includes(pl.SHY_FACET));
|
||||||
|
check("羞恥度不到門檻就不換稱呼",
|
||||||
|
pl.shyAddress("alpha", { style: {} }, pl.SHY_ADDRESS_AT - 1) === null);
|
||||||
|
check("到門檻、沒設專屬叫法 → 吞掉名字(不叫名字本身就是訊號)", (() => {
|
||||||
|
const s = pl.shyAddress("alpha", { style: {} }, 70);
|
||||||
|
return s !== null && s.own === false && s.value === null;
|
||||||
|
})(), JSON.stringify(pl.shyAddress("alpha", { style: {} }, 70)));
|
||||||
|
check("節點設了羞怯稱呼就用它", (() => {
|
||||||
|
const s = pl.shyAddress("alpha", { style: { 羞怯稱呼: { value: "⋯那個" } } }, 70);
|
||||||
|
return s !== null && s.own === true && s.value === "⋯那個";
|
||||||
|
})(), JSON.stringify(pl.shyAddress("alpha", { style: { 羞怯稱呼: { value: "⋯那個" } } }, 70)));
|
||||||
|
check("害羞時的語氣指示叫他把名字吞掉(女性預設 70 就落在這條線上)", (() => {
|
||||||
|
cli(["relation", "speaker", "--session", S_HOST, "--name", "伴侶甲"]);
|
||||||
|
return pl.modestyState("alpha", {}).value >= pl.SHY_ADDRESS_AT &&
|
||||||
|
pl.toneDirective("alpha", {}).includes("把名字吞掉");
|
||||||
|
})(), `${pl.modestyState("alpha", {}).value}|${pl.toneDirective("alpha", {})}`);
|
||||||
|
check("設了羞怯稱呼之後改成叫那個,而且不重複列在「他要求過的規則」裡", (() => {
|
||||||
|
cli(["relation", "style", "--session", S_HOST, "--name", "伴侶甲",
|
||||||
|
"--facet", "羞怯稱呼", "--value", "⋯那個"]);
|
||||||
|
const d = pl.toneDirective("alpha", {});
|
||||||
|
return d.includes("叫他「⋯那個」") && !d.includes("把名字吞掉") &&
|
||||||
|
!d.includes("羞怯稱呼→");
|
||||||
|
})(), pl.toneDirective("alpha", {}));
|
||||||
|
check("羞怯稱呼在 relation style 的清單裡還是查得到(那裡該看得見全部)", (() => {
|
||||||
|
const out = cli(["relation", "style", "--session", S_HOST, "--name", "伴侶甲"]).stdout;
|
||||||
|
return out.includes("羞怯稱呼");
|
||||||
|
})());
|
||||||
check("relation speaker --clear 之後就不再注入語氣指示", (() => {
|
check("relation speaker --clear 之後就不再注入語氣指示", (() => {
|
||||||
cli(["relation", "speaker", "--session", S_HOST, "--clear"]);
|
cli(["relation", "speaker", "--session", S_HOST, "--clear"]);
|
||||||
return !pl.loadConfig("alpha").speaker_node &&
|
return !pl.loadConfig("alpha").speaker_node &&
|
||||||
|
|||||||
Reference in New Issue
Block a user