- 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 群組冒煙測試無回歸。
99 lines
3.4 KiB
TypeScript
99 lines
3.4 KiB
TypeScript
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;
|
||
}
|
||
}
|