Files
Kokorone/apps/api/src/personality/character-creation.service.ts
T
Jeffery d82b834e47 feat: 完成 I 群組 — 生活作息與離線生活
- ScheduleService:I-1 平日/週末/假期時段表(可為角色寫入高中生預設作息)、
  I-3 時間感知(Asia/Taipei 當地時間,不受伺服器時區影響)、剛醒/睡前偵測
- constants.ts:I-2 時段狀態 → 回應可用度對照(FREE/SEMI_BUSY/BUSY/SLEEP →
  NORMAL/BRIEF/DELAYED/NO_REPLY),沒有作息表的角色一律視為 NORMAL(相容既有角色)
- ScheduleExceptionService:I-4 三種破例(深夜還陪你聊/上課偷回訊息/為你調整行程),
  各自獨立冷卻期,觸發時記入關係帳本
- ScheduleEmotionBaselineService:I-5 作息驅動情緒基線(社團剛結束的疲憊、假日偏高心情),
  只在本輪沒有明確情緒訊號時介入,串接進 EmotionService.processInput 的新增選項
- OfflineEventService:I-6 離線事件生成,依「今天已發生的固定行程」挑選對應模板,
  一天只生成一次,寫入低權重情節記憶
- DialogueService 整合以上機制:離線事件生成 → 情緒基線標記 → 累積親密度 →
  作息可用度判定 → 不符合則嘗試破例 → 依可用度決定回覆內容
- 修正真實 bug:G-5 角色建立流程誤用 source=OFFLINE_GENERATED 標記背景故事,
  與 I-6「今天是否已生成離線事件」的判斷共用同一標記,已改為 SOURCE_EXTRACTION
- ChatView.tsx 補上 NO_REPLY/context 為 null 的防呆(作息可用度整合後的必要調整)
- scripts/smoke/I.mjs:以獨立測試角色(避免污染種子角色的排程狀態)驗證全部六項

npm run restart && npm run smoke -- I 皆通過(I-V),A~H 群組冒煙測試無回歸。
2026-08-13 14:08:24 +08:00

99 lines
3.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
// 來自角色設定表本身,不是 I-6 的「離線生成日常事件」,兩者不可共用同一 source 值
// (I-6 用 source=OFFLINE_GENERATED 判斷「今天是否已生成過離線事件」,混用會誤判)。
source: "SOURCE_EXTRACTION",
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;
}
}