861 lines
26 KiB
TypeScript
861 lines
26 KiB
TypeScript
import crypto from 'node:crypto';
|
|
|
|
import type {
|
|
CreatorIntentReadiness,
|
|
CustomWorldAgentMessage,
|
|
CustomWorldAgentOperationRecord,
|
|
CustomWorldAgentSessionSnapshot,
|
|
CustomWorldAgentStage,
|
|
CustomWorldAssetCoverageSummary,
|
|
CustomWorldDraftCardSummary,
|
|
CustomWorldPendingClarification,
|
|
CustomWorldSuggestedAction,
|
|
EightAnchorContent,
|
|
} from '../../../packages/shared/src/contracts/customWorldAgent.js';
|
|
import type { CustomWorldSessionRecord as LegacyCustomWorldSessionRecord } from '../../../packages/shared/src/contracts/runtime.js';
|
|
import type { RuntimeRepositoryPort } from '../repositories/runtimeRepository.js';
|
|
import {
|
|
buildPendingClarifications,
|
|
evaluateCreatorIntentReadiness,
|
|
resolveCreatorIntentStage,
|
|
} from './customWorldAgentClarificationService.js';
|
|
import {
|
|
normalizeCreatorIntentRecord,
|
|
} from './customWorldAgentIntentExtractionService.js';
|
|
import { rebuildRoleAssetCoverage } from './customWorldAgentRoleAssetStateService.js';
|
|
import {
|
|
buildAnchorPackFromEightAnchorContent,
|
|
buildCreatorIntentFromEightAnchorContent,
|
|
buildDraftSummaryFromEightAnchorContent,
|
|
buildDraftTitleFromEightAnchorContent,
|
|
buildEightAnchorContentFromCreatorIntent,
|
|
createEmptyEightAnchorContent,
|
|
estimateProgressPercentFromAnchorContent,
|
|
normalizeEightAnchorContent,
|
|
} from './eightAnchorCompatibilityService.js';
|
|
|
|
export const CUSTOM_WORLD_AGENT_SESSION_ID_PREFIX =
|
|
'custom-world-agent-session-';
|
|
|
|
export type CustomWorldAgentSessionRecord = {
|
|
sessionId: string;
|
|
userId: string;
|
|
seedText: string;
|
|
currentTurn: number;
|
|
anchorContent: EightAnchorContent;
|
|
progressPercent: number;
|
|
lastAssistantReply: string | null;
|
|
stage: CustomWorldAgentStage;
|
|
focusCardId: string | null;
|
|
creatorIntent: Record<string, unknown> | null;
|
|
creatorIntentReadiness: CreatorIntentReadiness;
|
|
anchorPack: Record<string, unknown> | null;
|
|
lockState: Record<string, unknown> | null;
|
|
draftProfile: Record<string, unknown> | null;
|
|
messages: CustomWorldAgentMessage[];
|
|
draftCards: CustomWorldDraftCardSummary[];
|
|
pendingClarifications: CustomWorldPendingClarification[];
|
|
suggestedActions: CustomWorldSuggestedAction[];
|
|
recommendedReplies: string[];
|
|
qualityFindings: Array<{
|
|
id: string;
|
|
severity: 'info' | 'warning' | 'blocker';
|
|
code: string;
|
|
targetId?: string | null;
|
|
message: string;
|
|
}>;
|
|
assetCoverage: CustomWorldAssetCoverageSummary;
|
|
operations: CustomWorldAgentOperationRecord[];
|
|
checkpoints: Array<{
|
|
checkpointId: string;
|
|
createdAt: string;
|
|
label: string;
|
|
}>;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
};
|
|
|
|
type CreateSessionInput = {
|
|
seedText?: string;
|
|
welcomeMessage: string;
|
|
currentTurn?: number;
|
|
anchorContent?: EightAnchorContent;
|
|
progressPercent?: number;
|
|
lastAssistantReply?: string | null;
|
|
pendingClarifications: CustomWorldAgentSessionRecord['pendingClarifications'];
|
|
creatorIntent?: CustomWorldAgentSessionRecord['creatorIntent'];
|
|
creatorIntentReadiness?: CreatorIntentReadiness;
|
|
anchorPack?: CustomWorldAgentSessionRecord['anchorPack'];
|
|
draftProfile?: CustomWorldAgentSessionRecord['draftProfile'];
|
|
stage?: CustomWorldAgentStage;
|
|
suggestedActions: CustomWorldSuggestedAction[];
|
|
recommendedReplies?: string[];
|
|
};
|
|
|
|
function cloneRecord<T>(value: T): T {
|
|
return JSON.parse(JSON.stringify(value)) as T;
|
|
}
|
|
|
|
function toRecord(value: unknown) {
|
|
return value && typeof value === 'object' && !Array.isArray(value)
|
|
? (value as Record<string, unknown>)
|
|
: null;
|
|
}
|
|
|
|
function toText(value: unknown) {
|
|
return typeof value === 'string' ? value.trim() : '';
|
|
}
|
|
|
|
function isStage(value: unknown): value is CustomWorldAgentStage {
|
|
return (
|
|
value === 'collecting_intent' ||
|
|
value === 'clarifying' ||
|
|
value === 'foundation_review' ||
|
|
value === 'object_refining' ||
|
|
value === 'visual_refining' ||
|
|
value === 'long_tail_review' ||
|
|
value === 'ready_to_publish' ||
|
|
value === 'published' ||
|
|
value === 'error'
|
|
);
|
|
}
|
|
|
|
function isAgentSessionRecord(
|
|
value: unknown,
|
|
): value is CustomWorldAgentSessionRecord {
|
|
const record = toRecord(value);
|
|
if (!record) {
|
|
return false;
|
|
}
|
|
|
|
return (
|
|
typeof record.sessionId === 'string' &&
|
|
record.sessionId.startsWith(CUSTOM_WORLD_AGENT_SESSION_ID_PREFIX) &&
|
|
typeof record.userId === 'string' &&
|
|
isStage(record.stage) &&
|
|
Array.isArray(record.messages) &&
|
|
Array.isArray(record.operations) &&
|
|
typeof record.createdAt === 'string' &&
|
|
typeof record.updatedAt === 'string'
|
|
);
|
|
}
|
|
|
|
function isCreatorIntentReadiness(
|
|
value: unknown,
|
|
): value is CreatorIntentReadiness {
|
|
const record = toRecord(value);
|
|
if (!record) {
|
|
return false;
|
|
}
|
|
|
|
return (
|
|
typeof record.isReady === 'boolean' &&
|
|
Array.isArray(record.completedKeys) &&
|
|
Array.isArray(record.missingKeys)
|
|
);
|
|
}
|
|
|
|
function mapLegacyClarificationTargetKey(id: string) {
|
|
if (id === 'world_hook') return 'world_hook';
|
|
if (id === 'player_premise') return 'player_premise';
|
|
if (id === 'theme_and_tone' || id === 'tone_boundary') {
|
|
return 'theme_and_tone';
|
|
}
|
|
if (id === 'core_conflict') return 'core_conflict';
|
|
if (id === 'relationship_seed' || id === 'relationship_hook') {
|
|
return 'relationship_seed';
|
|
}
|
|
if (id === 'iconic_element' || id === 'iconic_elements') {
|
|
return 'iconic_element';
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function hasUserInput(record: CustomWorldAgentSessionRecord) {
|
|
return (
|
|
Boolean(record.seedText.trim()) ||
|
|
record.messages.some(
|
|
(message) => message.role === 'user' && message.text.trim(),
|
|
)
|
|
);
|
|
}
|
|
|
|
function buildCompatibleCreatorIntent(record: CustomWorldAgentSessionRecord) {
|
|
const compatibleAnchorIntent = buildCreatorIntentFromEightAnchorContent(
|
|
normalizeEightAnchorContent(
|
|
(record as Record<string, unknown>).anchorContent ?? null,
|
|
),
|
|
);
|
|
|
|
if (
|
|
compatibleAnchorIntent &&
|
|
(compatibleAnchorIntent.worldHook ||
|
|
compatibleAnchorIntent.rawSettingText ||
|
|
compatibleAnchorIntent.playerPremise ||
|
|
compatibleAnchorIntent.openingSituation ||
|
|
compatibleAnchorIntent.coreConflicts.length > 0 ||
|
|
compatibleAnchorIntent.keyCharacters.length > 0 ||
|
|
compatibleAnchorIntent.iconicElements.length > 0)
|
|
) {
|
|
return compatibleAnchorIntent;
|
|
}
|
|
|
|
return normalizeCreatorIntentRecord(record.creatorIntent);
|
|
}
|
|
|
|
function buildCompatibleCurrentTurn(record: CustomWorldAgentSessionRecord) {
|
|
if (typeof (record as Record<string, unknown>).currentTurn === 'number') {
|
|
return Math.max(
|
|
0,
|
|
Math.round((record as Record<string, unknown>).currentTurn as number),
|
|
);
|
|
}
|
|
|
|
return record.messages.filter((message) => message.role === 'user').length;
|
|
}
|
|
|
|
function buildCompatibleAnchorContent(record: CustomWorldAgentSessionRecord) {
|
|
const normalized = normalizeEightAnchorContent(
|
|
(record as Record<string, unknown>).anchorContent ?? null,
|
|
);
|
|
|
|
if (
|
|
normalized.worldPromise ||
|
|
normalized.playerFantasy ||
|
|
normalized.themeBoundary ||
|
|
normalized.playerEntryPoint ||
|
|
normalized.coreConflict ||
|
|
normalized.keyRelationships.length > 0 ||
|
|
normalized.hiddenLines ||
|
|
normalized.iconicElements
|
|
) {
|
|
return normalized;
|
|
}
|
|
|
|
return buildEightAnchorContentFromCreatorIntent(
|
|
buildCompatibleCreatorIntent(record),
|
|
);
|
|
}
|
|
|
|
function buildCompatibleProgressPercent(record: CustomWorldAgentSessionRecord) {
|
|
const rawProgress = (record as Record<string, unknown>).progressPercent;
|
|
if (typeof rawProgress === 'number' && Number.isFinite(rawProgress)) {
|
|
return Math.max(0, Math.min(100, Math.round(rawProgress)));
|
|
}
|
|
|
|
if (
|
|
record.stage === 'foundation_review' ||
|
|
record.stage === 'object_refining' ||
|
|
record.stage === 'visual_refining' ||
|
|
record.stage === 'long_tail_review' ||
|
|
record.stage === 'ready_to_publish' ||
|
|
record.stage === 'published'
|
|
) {
|
|
return 100;
|
|
}
|
|
|
|
return estimateProgressPercentFromAnchorContent(
|
|
buildCompatibleAnchorContent(record),
|
|
);
|
|
}
|
|
|
|
function buildCompatibleLastAssistantReply(record: CustomWorldAgentSessionRecord) {
|
|
const existingReply = (record as Record<string, unknown>).lastAssistantReply;
|
|
if (typeof existingReply === 'string') {
|
|
return existingReply;
|
|
}
|
|
|
|
const lastAssistantMessage = [...record.messages]
|
|
.reverse()
|
|
.find((message) => message.role === 'assistant' && message.text.trim());
|
|
|
|
return lastAssistantMessage?.text ?? null;
|
|
}
|
|
|
|
function buildCompatibleReadiness(record: CustomWorldAgentSessionRecord) {
|
|
if (
|
|
isCreatorIntentReadiness(
|
|
(record as Record<string, unknown>).creatorIntentReadiness,
|
|
)
|
|
) {
|
|
return record.creatorIntentReadiness;
|
|
}
|
|
|
|
return evaluateCreatorIntentReadiness(
|
|
normalizeCreatorIntentRecord(record.creatorIntent),
|
|
);
|
|
}
|
|
|
|
function buildCompatiblePendingClarifications(
|
|
record: CustomWorldAgentSessionRecord,
|
|
) {
|
|
const normalizedIntent = normalizeCreatorIntentRecord(record.creatorIntent);
|
|
const readiness = buildCompatibleReadiness(record);
|
|
const legacyClarifications = Array.isArray(record.pendingClarifications)
|
|
? record.pendingClarifications
|
|
: [];
|
|
|
|
const nextClarifications = legacyClarifications
|
|
.map((entry, index) => {
|
|
const targetKey = mapLegacyClarificationTargetKey(entry.id);
|
|
if (!targetKey) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
id: entry.id || targetKey,
|
|
label: entry.label || '待补充问题',
|
|
question: entry.question || '',
|
|
targetKey,
|
|
priority:
|
|
typeof entry.priority === 'number' ? entry.priority : index + 1,
|
|
answer: entry.answer,
|
|
} satisfies CustomWorldPendingClarification;
|
|
})
|
|
.filter((entry): entry is CustomWorldPendingClarification =>
|
|
Boolean(entry?.question),
|
|
)
|
|
.slice(0, 3);
|
|
|
|
if (nextClarifications.length > 0) {
|
|
return nextClarifications;
|
|
}
|
|
|
|
return buildPendingClarifications(normalizedIntent, readiness);
|
|
}
|
|
|
|
function buildCompatibleDraftProfile(
|
|
record: CustomWorldAgentSessionRecord,
|
|
) {
|
|
const anchorContent = buildCompatibleAnchorContent(record);
|
|
const existingDraftProfile = toRecord(record.draftProfile);
|
|
const hasFoundationContent = Boolean(
|
|
existingDraftProfile &&
|
|
(typeof existingDraftProfile.name === 'string' ||
|
|
Array.isArray(existingDraftProfile.playableNpcs) ||
|
|
Array.isArray(existingDraftProfile.landmarks) ||
|
|
Array.isArray(existingDraftProfile.factions) ||
|
|
Array.isArray(existingDraftProfile.threads) ||
|
|
Array.isArray(existingDraftProfile.chapters)),
|
|
);
|
|
|
|
if (hasFoundationContent) {
|
|
return {
|
|
...existingDraftProfile,
|
|
name:
|
|
toText(existingDraftProfile?.name) ||
|
|
toText(existingDraftProfile?.title) ||
|
|
buildDraftTitleFromEightAnchorContent(anchorContent),
|
|
summary:
|
|
toText(existingDraftProfile?.summary) ||
|
|
buildDraftSummaryFromEightAnchorContent(anchorContent),
|
|
};
|
|
}
|
|
|
|
return {
|
|
...(existingDraftProfile ?? {}),
|
|
title:
|
|
toText(existingDraftProfile?.title) ||
|
|
buildDraftTitleFromEightAnchorContent(anchorContent),
|
|
summary:
|
|
toText(existingDraftProfile?.summary) ||
|
|
buildDraftSummaryFromEightAnchorContent(anchorContent),
|
|
};
|
|
}
|
|
|
|
function buildCompatibleSuggestedActions(params: {
|
|
record: CustomWorldAgentSessionRecord;
|
|
stage: CustomWorldAgentStage;
|
|
readiness: CreatorIntentReadiness;
|
|
draftProfile: Record<string, unknown>;
|
|
}) {
|
|
if (params.record.suggestedActions.length > 0) {
|
|
return params.record.suggestedActions;
|
|
}
|
|
|
|
const actions: CustomWorldSuggestedAction[] = [
|
|
{
|
|
id: 'request_summary',
|
|
type: 'request_summary',
|
|
label:
|
|
params.stage === 'object_refining' || params.stage === 'visual_refining'
|
|
? '总结当前世界底稿'
|
|
: '总结当前设定',
|
|
},
|
|
];
|
|
const playableNpcs = Array.isArray(params.draftProfile.playableNpcs)
|
|
? params.draftProfile.playableNpcs
|
|
: [];
|
|
const storyNpcs = Array.isArray(params.draftProfile.storyNpcs)
|
|
? params.draftProfile.storyNpcs
|
|
: [];
|
|
const landmarks = Array.isArray(params.draftProfile.landmarks)
|
|
? params.draftProfile.landmarks
|
|
: [];
|
|
|
|
if (params.stage === 'foundation_review' && params.readiness.isReady) {
|
|
actions.push({
|
|
id: 'draft_foundation',
|
|
type: 'draft_foundation',
|
|
label: '整理一版世界底稿',
|
|
});
|
|
return actions;
|
|
}
|
|
|
|
if (params.stage === 'object_refining' || params.stage === 'visual_refining') {
|
|
const firstCharacter = toRecord([...playableNpcs, ...storyNpcs][0]);
|
|
const firstLandmark = toRecord(landmarks[0]);
|
|
|
|
actions.push({
|
|
id: 'refine_world',
|
|
type: 'refine_focus_target',
|
|
label: '先看世界总卡',
|
|
targetId: 'world-foundation',
|
|
});
|
|
|
|
if (firstCharacter) {
|
|
actions.push({
|
|
id: `refine-character-${toText(firstCharacter.id) || 'seed'}`,
|
|
type: 'refine_focus_target',
|
|
label: `精修角色:${toText(firstCharacter.name) || '关键角色'}`,
|
|
targetId: toText(firstCharacter.id) || null,
|
|
});
|
|
}
|
|
|
|
if (firstLandmark) {
|
|
actions.push({
|
|
id: `refine-landmark-${toText(firstLandmark.id) || 'seed'}`,
|
|
type: 'refine_focus_target',
|
|
label: `继续补地点:${toText(firstLandmark.name) || '关键地点'}`,
|
|
targetId: toText(firstLandmark.id) || null,
|
|
});
|
|
}
|
|
}
|
|
|
|
return actions;
|
|
}
|
|
|
|
function normalizeRecommendedReplies(value: unknown) {
|
|
if (!Array.isArray(value)) {
|
|
return [];
|
|
}
|
|
|
|
return value
|
|
.map((item) => toText(item))
|
|
.filter(Boolean)
|
|
.slice(0, 3);
|
|
}
|
|
|
|
function buildCompatibleAssetCoverage(
|
|
record: CustomWorldAgentSessionRecord,
|
|
draftProfile: Record<string, unknown>,
|
|
) {
|
|
const derivedCoverage = rebuildRoleAssetCoverage(draftProfile);
|
|
const existingCoverage = toRecord(record.assetCoverage);
|
|
const sceneAssets =
|
|
derivedCoverage.sceneAssets.length > 0
|
|
? derivedCoverage.sceneAssets
|
|
: Array.isArray(existingCoverage?.sceneAssets)
|
|
? existingCoverage.sceneAssets
|
|
: [];
|
|
const allSceneAssetsReady =
|
|
derivedCoverage.sceneAssets.length > 0
|
|
? derivedCoverage.allSceneAssetsReady
|
|
: typeof existingCoverage?.allSceneAssetsReady === 'boolean'
|
|
? existingCoverage.allSceneAssetsReady
|
|
: false;
|
|
|
|
return {
|
|
...derivedCoverage,
|
|
sceneAssets,
|
|
allSceneAssetsReady,
|
|
} satisfies CustomWorldAssetCoverageSummary;
|
|
}
|
|
|
|
function applyCompatibility(record: CustomWorldAgentSessionRecord) {
|
|
const creatorIntent = buildCompatibleCreatorIntent(record);
|
|
const currentTurn = buildCompatibleCurrentTurn(record);
|
|
const anchorContent = buildCompatibleAnchorContent(record);
|
|
const progressPercent = buildCompatibleProgressPercent(record);
|
|
const lastAssistantReply = buildCompatibleLastAssistantReply(record);
|
|
const creatorIntentReadiness =
|
|
progressPercent >= 100
|
|
? {
|
|
isReady: true,
|
|
completedKeys: [
|
|
'world_hook',
|
|
'player_premise',
|
|
'theme_and_tone',
|
|
'core_conflict',
|
|
'relationship_seed',
|
|
'iconic_element',
|
|
],
|
|
missingKeys: [],
|
|
}
|
|
: evaluateCreatorIntentReadiness(creatorIntent);
|
|
const stage =
|
|
record.stage === 'object_refining' ||
|
|
record.stage === 'visual_refining' ||
|
|
record.stage === 'long_tail_review' ||
|
|
record.stage === 'ready_to_publish' ||
|
|
record.stage === 'published'
|
|
? record.stage
|
|
: progressPercent >= 100
|
|
? ('foundation_review' as const)
|
|
: resolveCreatorIntentStage({
|
|
hasUserInput: hasUserInput(record),
|
|
readiness: creatorIntentReadiness,
|
|
});
|
|
const pendingClarifications = buildCompatiblePendingClarifications({
|
|
...record,
|
|
creatorIntent,
|
|
creatorIntentReadiness,
|
|
});
|
|
const draftProfile = buildCompatibleDraftProfile(record);
|
|
|
|
return {
|
|
...record,
|
|
currentTurn,
|
|
anchorContent,
|
|
progressPercent,
|
|
lastAssistantReply,
|
|
stage,
|
|
creatorIntent,
|
|
creatorIntentReadiness,
|
|
anchorPack:
|
|
record.anchorPack && Object.keys(record.anchorPack).length > 0
|
|
? record.anchorPack
|
|
: buildAnchorPackFromEightAnchorContent(anchorContent, progressPercent),
|
|
draftProfile,
|
|
pendingClarifications,
|
|
suggestedActions: buildCompatibleSuggestedActions({
|
|
record,
|
|
stage,
|
|
readiness: creatorIntentReadiness,
|
|
draftProfile,
|
|
}),
|
|
assetCoverage: buildCompatibleAssetCoverage(record, draftProfile),
|
|
recommendedReplies: normalizeRecommendedReplies(
|
|
(record as Record<string, unknown>).recommendedReplies,
|
|
),
|
|
} satisfies CustomWorldAgentSessionRecord;
|
|
}
|
|
|
|
function toSnapshot(
|
|
record: CustomWorldAgentSessionRecord,
|
|
): CustomWorldAgentSessionSnapshot {
|
|
return {
|
|
sessionId: record.sessionId,
|
|
currentTurn: record.currentTurn,
|
|
anchorContent: cloneRecord(record.anchorContent),
|
|
progressPercent: record.progressPercent,
|
|
lastAssistantReply: record.lastAssistantReply,
|
|
stage: record.stage,
|
|
focusCardId: record.focusCardId,
|
|
creatorIntent: cloneRecord(record.creatorIntent),
|
|
creatorIntentReadiness: cloneRecord(record.creatorIntentReadiness),
|
|
anchorPack: cloneRecord(record.anchorPack),
|
|
lockState: cloneRecord(record.lockState),
|
|
draftProfile: cloneRecord(record.draftProfile),
|
|
messages: cloneRecord(record.messages),
|
|
draftCards: cloneRecord(record.draftCards),
|
|
pendingClarifications: cloneRecord(record.pendingClarifications),
|
|
suggestedActions: cloneRecord(record.suggestedActions),
|
|
recommendedReplies: cloneRecord(record.recommendedReplies),
|
|
qualityFindings: cloneRecord(record.qualityFindings),
|
|
assetCoverage: cloneRecord(record.assetCoverage),
|
|
updatedAt: record.updatedAt,
|
|
};
|
|
}
|
|
|
|
export class CustomWorldAgentSessionStore {
|
|
constructor(private readonly runtimeRepository: RuntimeRepositoryPort) {}
|
|
|
|
private async persist(record: CustomWorldAgentSessionRecord) {
|
|
await this.runtimeRepository.upsertCustomWorldSession(
|
|
record.userId,
|
|
record.sessionId,
|
|
record as unknown as LegacyCustomWorldSessionRecord,
|
|
);
|
|
return cloneRecord(record);
|
|
}
|
|
|
|
private async mutate(
|
|
userId: string,
|
|
sessionId: string,
|
|
mutateFn: (record: CustomWorldAgentSessionRecord) => void,
|
|
) {
|
|
const current = await this.get(userId, sessionId);
|
|
if (!current) {
|
|
return null;
|
|
}
|
|
|
|
const nextRecord = cloneRecord(current);
|
|
mutateFn(nextRecord);
|
|
nextRecord.updatedAt = new Date().toISOString();
|
|
return this.persist(nextRecord);
|
|
}
|
|
|
|
async create(userId: string, input: CreateSessionInput) {
|
|
const sessionId = `${CUSTOM_WORLD_AGENT_SESSION_ID_PREFIX}${crypto.randomBytes(16).toString('hex')}`;
|
|
const now = new Date().toISOString();
|
|
const welcomeMessage: CustomWorldAgentMessage = {
|
|
id: `message-${crypto.randomBytes(8).toString('hex')}`,
|
|
role: 'assistant',
|
|
kind: 'chat',
|
|
text: input.welcomeMessage,
|
|
createdAt: now,
|
|
relatedOperationId: null,
|
|
};
|
|
const record: CustomWorldAgentSessionRecord = {
|
|
sessionId,
|
|
userId,
|
|
seedText: input.seedText?.trim() ?? '',
|
|
currentTurn: Math.max(0, Math.round(input.currentTurn ?? 0)),
|
|
anchorContent: normalizeEightAnchorContent(
|
|
input.anchorContent ?? createEmptyEightAnchorContent(),
|
|
),
|
|
progressPercent: Math.max(
|
|
0,
|
|
Math.min(100, Math.round(input.progressPercent ?? 0)),
|
|
),
|
|
lastAssistantReply: input.lastAssistantReply ?? input.welcomeMessage,
|
|
stage: input.stage ?? 'collecting_intent',
|
|
focusCardId: null,
|
|
creatorIntent: cloneRecord(input.creatorIntent ?? {}),
|
|
creatorIntentReadiness: input.creatorIntentReadiness ?? {
|
|
isReady: false,
|
|
completedKeys: [],
|
|
missingKeys: [],
|
|
},
|
|
anchorPack: cloneRecord(input.anchorPack ?? {}),
|
|
lockState: {},
|
|
draftProfile: cloneRecord(input.draftProfile ?? {}),
|
|
messages: [welcomeMessage],
|
|
draftCards: [],
|
|
pendingClarifications: cloneRecord(input.pendingClarifications),
|
|
suggestedActions: cloneRecord(input.suggestedActions),
|
|
recommendedReplies: cloneRecord(input.recommendedReplies ?? []),
|
|
qualityFindings: [],
|
|
assetCoverage: rebuildRoleAssetCoverage(input.draftProfile ?? {}),
|
|
operations: [],
|
|
checkpoints: [],
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
};
|
|
|
|
const compatibleRecord = applyCompatibility(record);
|
|
await this.persist(compatibleRecord);
|
|
return cloneRecord(compatibleRecord);
|
|
}
|
|
|
|
async list(userId: string) {
|
|
const records =
|
|
await this.runtimeRepository.listCustomWorldSessions(userId);
|
|
|
|
return records
|
|
.filter((record) => isAgentSessionRecord(record))
|
|
.map((record) => cloneRecord(applyCompatibility(record)))
|
|
.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
|
|
}
|
|
|
|
async get(userId: string, sessionId: string) {
|
|
if (!sessionId.trim()) {
|
|
return null;
|
|
}
|
|
|
|
const record = await this.runtimeRepository.getCustomWorldSession(
|
|
userId,
|
|
sessionId,
|
|
);
|
|
if (!isAgentSessionRecord(record)) {
|
|
return null;
|
|
}
|
|
|
|
return cloneRecord(applyCompatibility(record));
|
|
}
|
|
|
|
async getSnapshot(userId: string, sessionId: string) {
|
|
const record = await this.get(userId, sessionId);
|
|
return record ? toSnapshot(record) : null;
|
|
}
|
|
|
|
async appendMessage(
|
|
userId: string,
|
|
sessionId: string,
|
|
message: CustomWorldAgentMessage,
|
|
) {
|
|
return this.mutate(userId, sessionId, (record) => {
|
|
record.messages.push(cloneRecord(message));
|
|
});
|
|
}
|
|
|
|
async replaceDerivedState(
|
|
userId: string,
|
|
sessionId: string,
|
|
patch: Partial<
|
|
Pick<
|
|
CustomWorldAgentSessionRecord,
|
|
| 'currentTurn'
|
|
| 'anchorContent'
|
|
| 'progressPercent'
|
|
| 'lastAssistantReply'
|
|
| 'stage'
|
|
| 'creatorIntent'
|
|
| 'creatorIntentReadiness'
|
|
| 'anchorPack'
|
|
| 'lockState'
|
|
| 'draftProfile'
|
|
| 'pendingClarifications'
|
|
| 'suggestedActions'
|
|
| 'recommendedReplies'
|
|
| 'draftCards'
|
|
| 'qualityFindings'
|
|
| 'focusCardId'
|
|
| 'assetCoverage'
|
|
>
|
|
>,
|
|
) {
|
|
return this.mutate(userId, sessionId, (record) => {
|
|
if (typeof patch.currentTurn === 'number') {
|
|
record.currentTurn = Math.max(0, Math.round(patch.currentTurn));
|
|
}
|
|
if (patch.anchorContent !== undefined) {
|
|
record.anchorContent = normalizeEightAnchorContent(patch.anchorContent);
|
|
}
|
|
if (typeof patch.progressPercent === 'number') {
|
|
record.progressPercent = Math.max(
|
|
0,
|
|
Math.min(100, Math.round(patch.progressPercent)),
|
|
);
|
|
}
|
|
if (patch.lastAssistantReply !== undefined) {
|
|
record.lastAssistantReply = patch.lastAssistantReply;
|
|
}
|
|
if (patch.stage) {
|
|
record.stage = patch.stage;
|
|
}
|
|
if (patch.focusCardId !== undefined) {
|
|
record.focusCardId = patch.focusCardId;
|
|
}
|
|
if (patch.creatorIntent !== undefined) {
|
|
record.creatorIntent = cloneRecord(patch.creatorIntent);
|
|
}
|
|
if (patch.creatorIntentReadiness !== undefined) {
|
|
record.creatorIntentReadiness = cloneRecord(
|
|
patch.creatorIntentReadiness,
|
|
);
|
|
}
|
|
if (patch.anchorPack !== undefined) {
|
|
record.anchorPack = cloneRecord(patch.anchorPack);
|
|
}
|
|
if (patch.lockState !== undefined) {
|
|
record.lockState = cloneRecord(patch.lockState);
|
|
}
|
|
if (patch.draftProfile !== undefined) {
|
|
record.draftProfile = cloneRecord(patch.draftProfile);
|
|
}
|
|
if (patch.pendingClarifications !== undefined) {
|
|
record.pendingClarifications = cloneRecord(patch.pendingClarifications);
|
|
}
|
|
if (patch.suggestedActions !== undefined) {
|
|
record.suggestedActions = cloneRecord(patch.suggestedActions);
|
|
}
|
|
if (patch.recommendedReplies !== undefined) {
|
|
record.recommendedReplies = cloneRecord(patch.recommendedReplies);
|
|
}
|
|
if (patch.draftCards !== undefined) {
|
|
record.draftCards = cloneRecord(patch.draftCards);
|
|
}
|
|
if (patch.qualityFindings !== undefined) {
|
|
record.qualityFindings = cloneRecord(patch.qualityFindings);
|
|
}
|
|
if (patch.assetCoverage !== undefined) {
|
|
record.assetCoverage = cloneRecord(patch.assetCoverage);
|
|
}
|
|
});
|
|
}
|
|
|
|
async createOperation(
|
|
userId: string,
|
|
sessionId: string,
|
|
operation: CustomWorldAgentOperationRecord,
|
|
) {
|
|
return this.mutate(userId, sessionId, (record) => {
|
|
record.operations.push(cloneRecord(operation));
|
|
});
|
|
}
|
|
|
|
async getOperation(userId: string, sessionId: string, operationId: string) {
|
|
const record = await this.get(userId, sessionId);
|
|
if (!record) {
|
|
return null;
|
|
}
|
|
|
|
const operation = record.operations.find(
|
|
(item) => item.operationId === operationId,
|
|
);
|
|
return operation ? cloneRecord(operation) : null;
|
|
}
|
|
|
|
async updateOperation(
|
|
userId: string,
|
|
sessionId: string,
|
|
operationId: string,
|
|
patch: Partial<CustomWorldAgentOperationRecord>,
|
|
) {
|
|
return this.mutate(userId, sessionId, (record) => {
|
|
const operation = record.operations.find(
|
|
(item) => item.operationId === operationId,
|
|
);
|
|
if (!operation) {
|
|
return;
|
|
}
|
|
|
|
if (patch.type) {
|
|
operation.type = patch.type;
|
|
}
|
|
if (patch.status) {
|
|
operation.status = patch.status;
|
|
}
|
|
if (patch.phaseLabel) {
|
|
operation.phaseLabel = patch.phaseLabel;
|
|
}
|
|
if (patch.phaseDetail) {
|
|
operation.phaseDetail = patch.phaseDetail;
|
|
}
|
|
if (typeof patch.progress === 'number') {
|
|
operation.progress = patch.progress;
|
|
}
|
|
if (patch.error !== undefined) {
|
|
operation.error = patch.error;
|
|
}
|
|
});
|
|
}
|
|
|
|
async appendCheckpoint(
|
|
userId: string,
|
|
sessionId: string,
|
|
input: {
|
|
checkpointId?: string;
|
|
label: string;
|
|
},
|
|
) {
|
|
return this.mutate(userId, sessionId, (record) => {
|
|
record.checkpoints.push({
|
|
checkpointId:
|
|
input.checkpointId ||
|
|
`checkpoint-${crypto.randomBytes(8).toString('hex')}`,
|
|
createdAt: new Date().toISOString(),
|
|
label: input.label,
|
|
});
|
|
});
|
|
}
|
|
|
|
async listDraftCards(userId: string, sessionId: string) {
|
|
const record = await this.get(userId, sessionId);
|
|
return record ? cloneRecord(record.draftCards) : null;
|
|
}
|
|
}
|