feat(emotion): 情緒先行、走向、飽和與偏差稽核(四層,v0.0.7 同批)

參考兩篇 ithelp 文章(Day25 情緒辨識模組、Day26 整合與情緒記憶池)的**架構**,
實作全部重寫——它們的六類關鍵字 first-match、罐頭哲學回應與模式切換都比現有的
十二情緒模型差,照抄是降級,而且它的罐頭回應正是上一個 commit 剛封殺的罐頭同理心。

一、輸入端:`readUserEmotion(text)`
  在這之前情緒**全部是人格自己填的** `--apply`,而人不會主動給自己扣分——正向一路貼頂、
  負向整天不動。現在每輪先讀對方那句話:十二類加權詞表(強3/一般2/弱1)、程度副詞乘係數、
  **否定會擋掉那次命中**(「我不害怕」不算 fear)、引號與 `code` 內不比對(提及不算使用)、
  標點只放大既有訊號不憑空長出新情緒(「居然修好了!!」不會被讀成憤怒)。
  強度用飽和曲線不是線性乘。回的是**訊號不是判定**,人格可以推翻。`emotion --read` 可單測。

二、怎麼接:`RESPONSE_STANCE`
  十二種各一條**動作**(悲傷→先接住不要急著給解法;憤怒→不辯解,先認可能認的那一小塊),
  不是罐頭句——罐頭句由 `SPEECH_BLACKLIST` 擋著。

三、走向:`state/felt.jsonl` + `feltTrend()`
  每輪的偵測結果與套用的 delta 都記下來,近重遠輕的加權算最近 5 輪的趨勢
  (不用多數決:會被離群值主導又丟掉強度),注入「他最近 3 輪的走向:悲傷 ↘ 在退」。
  新檔案已歸到 Gitea 檔案區(高頻)。

四、情緒調節:飽和與單輪預算
  `applyEmotion` 不再是加完直接 clamp。飽和=越接近端點同方向漲越慢(headroom^K,K=1,
  往 baseline 回的方向不壓);單輪預算=所有 |delta| 總和上限 60,超過等比例縮小。
  `emotion --audit` 印出最近幾輪往舒服/往難受的比例,正向 ≥90% 會被點名。
  依據:今天 85 次套用讓 joy/serenity/gratitude/anticipation 都到過 100、負向均值整天 14–17。

測試:297 項全過(新增 24 項,含讀取的 SF6/SNF4、飽和、預算、回歸方向不壓、走向、audit、
--read 不改狀態)。既有的「情緒有被施加」改成符合飽和後的語意。

版本:與 PR #10 同一批未合併的改動,維持 0.0.7。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-31 04:21:08 +00:00
co-authored by Claude Opus 5
parent 0f1d0cfc1e
commit 23751c2c05
8 changed files with 413 additions and 5 deletions
+32 -1
View File
@@ -922,13 +922,41 @@ commands.emotion = ({ flags }) => {
if (key in pl.EMOTIONS) state.baseline[key] = pl.clamp(value);
}
}
// 讀對方那句話的情緒訊號(不改狀態,只回報;`--read` 可以單獨拿來測詞表)
const readText = str(flags.read);
const read = readText ? pl.readUserEmotion(readText) : null;
if (read) {
emit({ persona: slug, read }, flags.json, [
`讀「${readText.slice(0, 40)}」:`,
read.signals.length
? ` 訊號:${read.signals.map((s) => `${s.zh} ${s.score}`).join("、")}|強度 ${read.intensity}${read.confident ? "夠強,值得參考" : "偏弱,別當判定"}`
: " 沒有讀到明確的情緒訊號。",
...(read.signals.length ? [` 怎麼接:${pl.RESPONSE_STANCE[read.signals[0].key]}`] : []),
]);
return;
}
if (flags.audit) {
const audit = pl.feltAudit(slug, num(flags.limit, 20));
const trend = pl.feltTrend(slug);
emit({ persona: slug, audit, trend }, flags.json, [
`\`${slug}\` 最近 ${audit.rounds} 輪的情緒套用:`,
` 往舒服的方向 ${audit.positive}|往難受的方向 ${audit.negative}` +
(audit.positive_ratio === null ? "" : ` → 正向佔 ${audit.positive_ratio}%`),
audit.positive_ratio !== null && audit.positive_ratio >= 90
? " ⚠ 幾乎只給自己加分——delta 是你自己挑的,這種偏差要自己看得見。"
: " 比例還算兩邊都有。",
trend ? ` 對方的走向:${trend.zh} ${trend.direction}(近 ${trend.rounds} 輪)` : " 還沒有足夠的輪次算走向。",
]);
return;
}
if (flags.apply) {
if (role === "guest") die("guestsub agent)不得改寫人格的情緒狀態。");
const deltas = parseDeltas(flags.apply);
state = pl.applyEmotion(state, deltas, str(flags.trigger));
pl.recordFelt(slug, { mine: state.last_trigger?.deltas || null, note: str(flags.trigger) });
pl.appendJsonl(pl.journalPath(slug), {
ts: pl.nowIso(), kind: "emotion", trigger: str(flags.trigger),
deltas, levels: state.levels, mood: pl.mood(state),
deltas, applied: state.last_trigger?.deltas || {}, levels: state.levels, mood: pl.mood(state),
});
}
if (role !== "guest") pl.writeJson(pl.emotionPath(slug), state);
@@ -1933,6 +1961,9 @@ const HELP = `persona.mjs — jsc-persona 人格 / 記憶 / 情緒 / 關係圖 C
情緒與圖:
emotion --session <id> [--apply joy=+10,...] [--baseline ...] [--trigger <why>]
[--read "<對方說的話>"] 只讀情緒訊號與該怎麼接,不改狀態
[--audit [--limit 20]] 最近幾輪的 delta 是不是只往一邊倒+對方的走向
套用時會飽和(越接近端點漲越慢)並受單輪預算限制(|delta| 總和 ≤ 40
mindmap show|thread|list --session <id> [--topic <t>] [--force]
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]