feat: 完成 G 群組 — 角色人格層

- archetype-params.ts:六原型(傲嬌/冷淡/天然呆/元氣/大小姐/三無)參數表,
  串接進 D 群組 EmotionService(情緒觸發閾值)與 E 群組 SentimentLedgerService(信任成長速度)
- 修正一個真實落差:觸發閾值原本只縮放強度、不影響是否觸發,加入 MIN_TRIGGER_INTENSITY
  門檻後「難觸發」原型才會對弱刺激真的沒反應
- expressiveness.ts:G-3 外顯函數,傲嬌反向表達(傲嬌值=內外差值,隨內部親密度先升後降)
- language-style.ts:G-2 稱呼系統(陌生→您、認識→名字、摯友→暱稱,含跨階事件)與角色禁則清單,
  已串接進 F-3 輸出過濾層與口癖後處理
- contrast-behavior.service.ts:G-4 反差萌,親密度+情緒+機率門檻皆滿足才觸發,
  新增 ContrastTriggerLog 資料表記錄冷卻期
- character-creation.service.ts:G-5 由角色設定表一次產生語意記憶/情節記憶/情緒參數/關係初始值
- cuteness-behavior.ts:G-6 撒嬌/鬧彆扭節奏控制/吃醋/笨拙的努力/專屬揭露觸發條件;
  記住小事擴充了 C-4 睡眠固化,使用者瑣事即使情緒強度低也會寫入語意記憶
- scripts/smoke/G.mjs:涵蓋六原型差異化反應、稱呼進化、傲嬌外顯曲線、反差冷卻、
  可愛行為觸發條件、瑣事固化

npm run restart && npm run smoke -- G 皆通過(G-V),A/B/C/D/E/F 群組冒煙測試無回歸。
This commit is contained in:
Jeffery
2026-08-13 11:26:54 +08:00
parent caf45f69e1
commit 503024c92f
17 changed files with 757 additions and 16 deletions
+11 -1
View File
@@ -4,11 +4,14 @@ import { PrismaService } from "../prisma/prisma.service.js";
import { RuleBasedEmotionTagger } from "./emotion-tagger.js";
import { nextEmotionState, type TransitionContext } from "./emotion-state-machine.js";
import { toResponseStyle, type ResponseStyle } from "./response-style.js";
import { getArchetypeParams } from "../personality/archetype-params.js";
// D-2 時間衰減:預設半衰期 2 小時,實際速度由角色參數(G 群組)覆寫。
const DEFAULT_HALF_LIFE_MS = 2 * 60 * 60 * 1000;
// 低於此值視為「沒有明顯情緒」,dominantState 回報平靜。
const DOMINANT_EPSILON = 5;
// 標記強度低於此值視為刺激太弱,不足以真正觸發情緒轉移(高情緒觸發閾值的原型因此「難觸發」)。
const MIN_TRIGGER_INTENSITY = 0.25;
interface Dimensions {
calm: number;
@@ -95,7 +98,14 @@ export class EmotionService {
const { dims: decayed } = await this.decayedDimensions(characterId, now, options.halfLifeMs);
const currentDominant = dominantState(decayed);
const signal = this.tagger.tag({ text, triggerThreshold: options.triggerThreshold });
// G-1:情緒觸發閾值預設由角色性格原型決定,呼叫端仍可明確覆寫。
const character = await this.prisma.client.character.findUniqueOrThrow({ where: { id: characterId } });
const archetypeParams = getArchetypeParams(character.personalityArchetype);
const triggerThreshold = options.triggerThreshold ?? archetypeParams.emotionTriggerThreshold;
const rawSignal = this.tagger.tag({ text, triggerThreshold });
// 情緒觸發閾值除了縮放強度,也要能讓「難觸發」原型對弱刺激完全不觸發(而不只是觸發得比較小力)。
const signal = rawSignal.intensity < MIN_TRIGGER_INTENSITY ? { tag: "CALM" as const, intensity: 0 } : rawSignal;
const next = nextEmotionState(currentDominant, signal, options);
let updated: Dimensions;