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
@@ -0,0 +1,96 @@
import { Injectable } from "@nestjs/common";
import { PrismaService } from "../prisma/prisma.service.js";
import { RuleBasedEmotionTagger } from "../emotion/emotion-tagger.js";
import { stageForIntimacy } from "../relationship/constants.js";
export interface InitialRelationshipInput {
userId: string;
intimacy?: number;
trust?: number;
}
export interface CreateCharacterInput {
id?: string;
source: "ORIGINAL" | "EXISTING_WORK";
buildStatus?: "CANDIDATE" | "BUILT";
workId?: string;
formalName: string;
basicInfo: string;
backgroundStory: string;
personalityArchetype: string;
likesDislikes: string;
goalsObsessions: string;
speechStyle: string;
initialRelationValue?: string;
initialRelationship?: InitialRelationshipInput;
}
// G-5 角色建立流程:由角色設定表產生初始語意記憶+初始情節記憶+情緒參數+關係初始值。
@Injectable()
export class CharacterCreationService {
constructor(
private readonly prisma: PrismaService,
private readonly tagger: RuleBasedEmotionTagger,
) {}
async createCharacter(input: CreateCharacterInput, now: Date = new Date()) {
const character = await this.prisma.client.character.create({
data: {
id: input.id,
source: input.source,
buildStatus: input.buildStatus ?? "CANDIDATE",
workId: input.workId,
basicInfo: input.basicInfo,
backgroundStory: input.backgroundStory,
personalityArchetype: input.personalityArchetype,
likesDislikes: input.likesDislikes,
goalsObsessions: input.goalsObsessions,
speechStyle: input.speechStyle,
initialRelationValue: input.initialRelationValue,
},
});
await this.prisma.client.characterAlias.create({
data: { characterId: character.id, formalName: input.formalName },
});
// 初始語意記憶:關於自己的事實。
await this.prisma.client.semanticMemory.create({
data: { characterId: character.id, fact: input.basicInfo, about: "自己" },
});
// 初始情節記憶:背景故事,重用 D-1 的規則標記器判斷情緒基調(帶情緒標記)。
const signal = this.tagger.tag({ text: input.backgroundStory });
await this.prisma.client.episodicMemory.create({
data: {
characterId: character.id,
content: input.backgroundStory,
occurredAt: now,
emotionTag: signal.tag,
emotionIntensity: signal.intensity || 0.3,
source: "OFFLINE_GENERATED",
weight: 2,
},
});
// 情緒參數:預設值(calm=100,其餘為 0)。
await this.prisma.client.emotionState.create({ data: { characterId: character.id } });
// 關係初始值(若指定了對象)。
if (input.initialRelationship) {
const intimacy = input.initialRelationship.intimacy ?? 0;
await this.prisma.client.relationship.create({
data: {
characterId: character.id,
userId: input.initialRelationship.userId,
intimacy,
trust: input.initialRelationship.trust ?? 0,
firstMet: now,
stage: stageForIntimacy(intimacy),
},
});
}
return character;
}
}