feat(記憶與目標): 專案目標三條 + 固化候選不再重算(0.2.1) #17

Merged
admin merged 41 commits from develop into master 2026-08-03 09:15:21 +00:00
3 changed files with 77 additions and 7 deletions
Showing only changes of commit fe17b20010 - Show all commits
+10 -3
View File
@@ -125,10 +125,17 @@ Q5Q10 六題已於 2026/08/03 由使用者拍板,設計不再需要確認
- 為什麼:真人在聊天軟體上的情緒,一半是靠「又補了一句」傳出去的。這是文字獨有、目前完全沒用到的手段
- 驗收:慌的時候會多一則短的;平靜時永遠只有一則
- [ ] **5.3 稱呼隨情緒偏移**
- [x] **5.3 稱呼隨情緒偏移**2026/08/03 16:32:49 完成)
- `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 完成)
+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 條,而且挑跟這一輪
// 對象/話題對得上的——全注入會變成表演(他開始照抄自己的舊台詞)。
+28
View File
@@ -2026,6 +2026,34 @@ check("relation speaker 設定後,context 會注入語氣層與稱呼規則",
return res.status === 0 && pl.loadConfig("alpha").speaker_node === pl.slugify("伴侶甲") &&
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 之後就不再注入語氣指示", (() => {
cli(["relation", "speaker", "--session", S_HOST, "--clear"]);
return !pl.loadConfig("alpha").speaker_node &&