共用場景資料庫(Scene/ScenePresence/SceneLine/SceneFact/SceneInteraction, 角色中立客觀記錄)與投影(ProjectionService 寫入 EpisodicMemory/ SemanticMemory/PersonalityTraitSnapshot/CharacterRelationshipEventLog, 逐一可追溯到來源場景)完全分層。歸屬信心分數(明示標記決定性、語言指紋 從已歸屬語料統計、場景名冊硬約束、對話輪替)與隔離區/指紋冷啟動(低信心 或樣本不足一律隔離,指紋精煉後可重新嘗試釋放)。多角色投影與修正(改一處 場景資料庫、reprojectScene 同步更新所有受影響角色,使用者互動記憶因為 從不帶 sourceSceneId 而結構性地不受影響,發現得晚的歸屬錯誤額外記一筆 CORRECTION 事件)。作品名冊建置門檻(達標自動建置,門檻檢查排在投影完成 之後才做,避免用到投影前的舊統計)。錨定點知識邊界(雙向:前進載入、 後退遺忘)與矛盾偵測(同優先序來源待人工確認,跨優先序可依小說原文> 官方設定集>動畫改編自動裁決)。插圖索引與衍生服裝目錄/姿勢語彙表。 刻意簡化:章節→場景切分與互動正負權重皆採結構化輸入(不做自動 NLP 場景邊界偵測與情感分析),比照 F-7 MockProvider 的做法——機制先做對, 之後有真實 NLP/LLM 能力再替換輸入來源。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
55 lines
2.8 KiB
TypeScript
55 lines
2.8 KiB
TypeScript
import { Injectable } from "@nestjs/common";
|
|
import { PrismaService } from "../prisma/prisma.service.js";
|
|
import { ProjectionService } from "./projection.service.js";
|
|
import { WorkRosterService } from "./work-roster.service.js";
|
|
|
|
// M-9 錨定點與知識邊界:錨定點之前是角色的親身經歷、之後不存在。場景匯入時只寫入共用場景資料庫,
|
|
// 只有 storyOrder <= 錨定點的場景會被投影進角色記憶——移動錨定點(無論前進或後退)都要讓「已投影」的
|
|
// 場景集合與「storyOrder <= 錨定點」的場景集合重新同步。
|
|
@Injectable()
|
|
export class AnchorService {
|
|
constructor(
|
|
private readonly prisma: PrismaService,
|
|
private readonly projection: ProjectionService,
|
|
private readonly roster: WorkRosterService,
|
|
) {}
|
|
|
|
async setAnchor(workId: string, newStoryOrder: number): Promise<{ projected: number; unprojected: number }> {
|
|
const work = await this.prisma.client.work.findUniqueOrThrow({ where: { id: workId } });
|
|
|
|
const toProject = await this.prisma.client.scene.findMany({
|
|
where: { workId, storyOrder: { lte: newStoryOrder }, projected: false },
|
|
include: { participants: true },
|
|
});
|
|
for (const scene of toProject) {
|
|
await this.projection.projectScene(scene.id);
|
|
for (const presence of scene.participants) {
|
|
await this.roster.checkAndPromote(presence.characterId);
|
|
}
|
|
}
|
|
|
|
// 錨定點後退:她「不再知道」超出新錨定點的後段劇情——移除這些場景先前投影出的所有角色資料。
|
|
const toUnproject = await this.prisma.client.scene.findMany({
|
|
where: { workId, storyOrder: { gt: newStoryOrder }, projected: true },
|
|
});
|
|
for (const scene of toUnproject) {
|
|
await this.prisma.client.episodicMemory.deleteMany({ where: { sourceSceneId: scene.id } });
|
|
await this.prisma.client.semanticMemory.deleteMany({ where: { sourceSceneId: scene.id } });
|
|
await this.prisma.client.personalityTraitSnapshot.deleteMany({ where: { sourceSceneId: scene.id } });
|
|
await this.prisma.client.characterRelationshipEventLog.deleteMany({ where: { sceneId: scene.id } });
|
|
await this.prisma.client.scene.update({ where: { id: scene.id }, data: { projected: false } });
|
|
}
|
|
|
|
await this.prisma.client.work.update({ where: { id: work.id }, data: { anchorStoryOrder: newStoryOrder } });
|
|
return { projected: toProject.length, unprojected: toUnproject.length };
|
|
}
|
|
|
|
// M-9 依錨定點取用「當時的她」的性格參數:取 atStoryOrder <= 錨定點裡最新的一筆快照。
|
|
async effectivePersonalitySnapshot(characterId: string, atStoryOrder: number) {
|
|
return this.prisma.client.personalityTraitSnapshot.findFirst({
|
|
where: { characterId, atStoryOrder: { lte: atStoryOrder } },
|
|
orderBy: { atStoryOrder: "desc" },
|
|
});
|
|
}
|
|
}
|