Files
Genarrative/src/components/creative-agent/creativeAgentViewModel.ts
2026-05-14 14:21:17 +08:00

583 lines
17 KiB
TypeScript

import type {
CreativeAgentSessionSnapshot,
CreativeAgentSseEvent,
CreativeAgentStage,
CreativeTargetSessionBinding,
} from '../../../packages/shared/src/contracts/creativeAgent';
import type {
PuzzleCreativeTemplateProtocol,
PuzzleCreativeTemplateSelection,
PuzzleLevelGenerationMode,
PuzzleSupportedLevelMode,
} from '../../../packages/shared/src/contracts/puzzleCreativeTemplate';
export const CREATIVE_AGENT_STAGE_LABEL: Record<CreativeAgentStage, string> = {
idle: '等待输入',
perceiving: '正在理解素材',
thinking: '正在构思',
remembering: '正在整理上下文',
selecting_puzzle_template: '正在选择拼图模板',
waiting_template_confirmation: '等待确认',
planning_puzzle_levels: '正在规划关卡',
acting: '正在生成草稿',
reflecting: '正在检查结果',
collaborating: '正在协作收口',
target_ready: '草稿已就绪',
waiting_user: '等待补充',
failed: '生成失败',
};
const CREATIVE_AGENT_STAGE_DONE_LABEL: Partial<
Record<CreativeAgentStage, string>
> = {
perceiving: '素材已理解',
thinking: '构思已完成',
remembering: '上下文已整理',
selecting_puzzle_template: '模板已选择',
planning_puzzle_levels: '关卡已规划',
acting: '草稿已生成',
reflecting: '结果已检查',
collaborating: '协作已收口',
target_ready: '草稿已就绪',
};
const CREATIVE_AGENT_STAGE_WAITING_LABEL: Partial<
Record<CreativeAgentStage, string>
> = {
idle: '等待输入',
waiting_template_confirmation: '等待确认',
waiting_user: '等待补充',
failed: '生成失败',
};
const CREATIVE_AGENT_STAGE_IDLE_LABEL: Record<CreativeAgentStage, string> = {
idle: '等待输入',
perceiving: '理解素材',
thinking: '构思方向',
remembering: '整理上下文',
selecting_puzzle_template: '选择拼图模板',
waiting_template_confirmation: '等待确认',
planning_puzzle_levels: '规划关卡',
acting: '生成草稿',
reflecting: '检查结果',
collaborating: '协作收口',
target_ready: '草稿就绪',
waiting_user: '等待补充',
failed: '生成失败',
};
export const CREATIVE_AGENT_TIMELINE: CreativeAgentStage[] = [
'perceiving',
'thinking',
'remembering',
'selecting_puzzle_template',
'planning_puzzle_levels',
'acting',
'reflecting',
'collaborating',
'target_ready',
];
export type CreativeAgentTargetSelectionStage =
| 'puzzle-agent-workspace'
| 'puzzle-result';
export function resolveCreativeAgentTargetSelectionStage(
targetStage: CreativeTargetSessionBinding['targetStage'],
): CreativeAgentTargetSelectionStage {
return targetStage === 'puzzle-agent-workspace'
? 'puzzle-agent-workspace'
: 'puzzle-result';
}
export function createCreativeAgentClientMessageId(prefix = 'creative-agent') {
return `${prefix}-${Date.now().toString(36)}-${Math.random()
.toString(36)
.slice(2, 8)}`;
}
function resolveTemplateLevelMode(
supportedLevelMode: PuzzleSupportedLevelMode,
defaultLevelCount: number,
): PuzzleLevelGenerationMode {
if (supportedLevelMode === 'single') {
return 'single_level';
}
if (supportedLevelMode === 'multi') {
return 'multi_level';
}
return defaultLevelCount > 1 ? 'multi_level' : 'single_level';
}
function resolveTemplatePlannedLevelCount(
supportedLevelMode: PuzzleSupportedLevelMode,
defaultLevelCount: number,
) {
if (supportedLevelMode === 'single') {
return 1;
}
if (supportedLevelMode === 'multi') {
return Math.max(2, defaultLevelCount);
}
return Math.max(1, defaultLevelCount);
}
export function buildPuzzleTemplateSelectionFromProtocol(
template: PuzzleCreativeTemplateProtocol,
): PuzzleCreativeTemplateSelection {
const plannedLevelCount = resolveTemplatePlannedLevelCount(
template.supportedLevelMode,
template.defaultLevelCount,
);
return {
templateId: template.templateId,
title: template.title,
reason: template.summary,
costRange: template.costRange,
supportedLevelMode: template.supportedLevelMode,
selectedLevelMode: resolveTemplateLevelMode(
template.supportedLevelMode,
plannedLevelCount,
),
plannedLevelCount,
requiresUserConfirmation: true,
};
}
export type CreativeAgentStageDisplayStatus =
| 'active'
| 'done'
| 'waiting'
| 'idle';
export function getCreativeAgentStageDisplayLabel(
stage: CreativeAgentStage,
status: CreativeAgentStageDisplayStatus,
) {
if (status === 'done') {
return CREATIVE_AGENT_STAGE_DONE_LABEL[stage] ?? CREATIVE_AGENT_STAGE_LABEL[stage];
}
if (status === 'waiting') {
return CREATIVE_AGENT_STAGE_WAITING_LABEL[stage] ?? CREATIVE_AGENT_STAGE_LABEL[stage];
}
if (status === 'idle') {
return CREATIVE_AGENT_STAGE_IDLE_LABEL[stage];
}
return CREATIVE_AGENT_STAGE_LABEL[stage];
}
export type CreativeAgentProcessTone =
| 'active'
| 'done'
| 'info'
| 'warning'
| 'danger';
export type CreativeAgentProcessItem = {
id: string;
meta: string;
title: string;
detail: string | null;
detailLines: string[];
tone: CreativeAgentProcessTone;
};
const CREATIVE_AGENT_TOOL_LABEL: Record<string, string> = {
retrieve_puzzle_template_catalog: '读取拼图模板',
select_puzzle_template: '选择拼图模板',
confirm_puzzle_template: '确认模板',
create_puzzle_agent_session: '创建拼图草稿',
compile_puzzle_creative_draft: '编译拼图草稿',
plan_puzzle_level_images: '规划关卡图片',
generate_puzzle_level_images: '生成关卡图片',
apply_puzzle_draft_natural_language_edit: '写回草稿修改',
validate_puzzle_result_preview: '校验草稿预览',
start_puzzle_draft_test_run: '启动拼图试玩',
};
type ProcessBuildContext = {
activeStageEventIndex: number;
eventLog: CreativeAgentSseEvent[];
isComplete: boolean;
};
function formatPuzzleLevelMode(mode: PuzzleLevelGenerationMode) {
return mode === 'single_level' ? '单关卡' : '多关卡';
}
function formatTargetStage(stage: CreativeTargetSessionBinding['targetStage']) {
return stage === 'puzzle-agent-workspace'
? '拼图工作区'
: stage === 'puzzle-runtime'
? '拼图运行态'
: '拼图结果页';
}
function resolveToolLabel(toolName: string) {
return CREATIVE_AGENT_TOOL_LABEL[toolName] ?? toolName;
}
function buildStageProcessItem(
event: Extract<CreativeAgentSseEvent, { event: 'stage' }>,
index: number,
context: ProcessBuildContext,
): CreativeAgentProcessItem {
const stage = event.data.stage;
const isWaiting =
stage === 'waiting_template_confirmation' || stage === 'waiting_user';
const isActive =
index === context.activeStageEventIndex &&
!context.isComplete &&
!isWaiting &&
stage !== 'target_ready' &&
stage !== 'failed';
const tone: CreativeAgentProcessTone =
stage === 'failed'
? 'danger'
: isWaiting
? 'warning'
: isActive
? 'active'
: 'done';
return {
id: `${index}-stage-${stage}`,
meta: '阶段',
title: getCreativeAgentStageDisplayLabel(
stage,
isActive ? 'active' : isWaiting ? 'waiting' : 'done',
),
detail: '阶段切换',
detailLines: [],
tone,
};
}
function buildEventProcessItem(
event: CreativeAgentSseEvent,
index: number,
context: ProcessBuildContext,
): CreativeAgentProcessItem | null {
switch (event.event) {
case 'stage':
return buildStageProcessItem(event, index, context);
case 'tool_started': {
const toolLabel = resolveToolLabel(event.data.toolName);
const isToolResolved =
context.isComplete ||
context.eventLog.slice(index + 1).some(
(nextEvent) =>
nextEvent.event === 'tool_completed' &&
(nextEvent.data.toolCallId === event.data.toolCallId ||
nextEvent.data.toolName === event.data.toolName),
);
return {
id: `${index}-tool-started-${event.data.toolCallId}`,
meta: '工具调用',
title: `开始:${toolLabel}`,
detail: event.data.summary ?? event.data.toolName,
detailLines: [],
tone: isToolResolved ? 'done' : 'active',
};
}
case 'tool_completed': {
const toolLabel = resolveToolLabel(event.data.toolName);
return {
id: `${index}-tool-completed-${event.data.toolCallId}`,
meta: '工具完成',
title: `完成:${toolLabel}`,
detail: event.data.summary ?? event.data.toolName,
detailLines: [],
tone: 'done',
};
}
case 'puzzle_template_selection':
return {
id: `${index}-template-${event.data.selection.templateId}`,
meta: '模板',
title: `选择 ${event.data.selection.title}`,
detail: event.data.selection.reason,
detailLines: [
`${formatPuzzleLevelMode(event.data.selection.selectedLevelMode)} · ${event.data.selection.plannedLevelCount}`,
],
tone: 'info',
};
case 'puzzle_template_catalog':
return {
id: `${index}-template-catalog`,
meta: '模板',
title: `读取 ${event.data.templates.length} 个模板`,
detail:
event.data.templates
.slice(0, 3)
.map((template) => template.title)
.join('、') || null,
detailLines: [],
tone: 'info',
};
case 'puzzle_cost_range':
return {
id: `${index}-cost-${event.data.costRange.minPoints}-${event.data.costRange.maxPoints}`,
meta: '消耗',
title: `预计 ${event.data.costRange.minPoints}-${event.data.costRange.maxPoints} 泥点`,
detail: event.data.costRange.reason,
detailLines: [],
tone: 'info',
};
case 'puzzle_level_plan': {
const candidateCount = event.data.plan.levels.reduce(
(total, level) => total + level.candidateCount,
0,
);
return {
id: `${index}-level-plan-${event.data.plan.templateId}`,
meta: '关卡',
title: `规划 ${event.data.plan.levels.length} 个关卡`,
detail: `${formatPuzzleLevelMode(event.data.plan.mode)} · ${candidateCount} 张候选图`,
detailLines: event.data.plan.levels.slice(0, 4).map((level) =>
[
level.levelName,
level.pictureDescription,
level.pictureReference ? `参考图:${level.pictureReference}` : null,
]
.filter(Boolean)
.join(' · '),
),
tone: 'info',
};
}
case 'reflection':
return {
id: `${index}-reflection`,
meta: '检查',
title: event.data.pass ? '检查通过' : '需要调整',
detail: event.data.summary,
detailLines: event.data.warnings,
tone: event.data.pass ? 'done' : 'warning',
};
case 'target_session':
return {
id: `${index}-target-${event.data.binding.targetSessionId}`,
meta: '目标',
title: '拼图草稿已绑定',
detail: formatTargetStage(event.data.binding.targetStage),
detailLines: [`目标会话:${event.data.binding.targetSessionId}`],
tone: 'done',
};
case 'error':
return {
id: `${index}-error-${event.data.code}`,
meta: '异常',
title: event.data.message,
detail: event.data.code,
detailLines: [],
tone: 'danger',
};
case 'done':
return {
id: `${index}-done`,
meta: '完成',
title: '本轮完成',
detail: null,
detailLines: [],
tone: 'done',
};
case 'agent_message_delta':
case 'thought_summary_delta':
case 'session':
return null;
}
return null;
}
function buildThoughtSummaryItems(
eventLog: CreativeAgentSseEvent[],
isComplete: boolean,
): CreativeAgentProcessItem[] {
const thoughtMap = new Map<string, string>();
let latestThoughtId: string | null = null;
for (const event of eventLog) {
if (event.event !== 'thought_summary_delta') {
continue;
}
const currentText = thoughtMap.get(event.data.thoughtId) ?? '';
thoughtMap.set(event.data.thoughtId, `${currentText}${event.data.textDelta}`);
latestThoughtId = event.data.thoughtId;
}
const items: CreativeAgentProcessItem[] = [];
for (const [thoughtId, text] of thoughtMap.entries()) {
const detail = text.trim();
if (!detail) {
continue;
}
items.push({
id: `thought-${thoughtId}`,
meta: '思考',
title: '思考摘要',
detail,
detailLines: [],
tone: !isComplete && thoughtId === latestThoughtId ? 'active' : 'done',
});
}
return items;
}
function buildSessionFallbackItems(
session: CreativeAgentSessionSnapshot | null,
eventLog: CreativeAgentSseEvent[],
): CreativeAgentProcessItem[] {
if (!session) {
return [];
}
const items: CreativeAgentProcessItem[] = [];
if (
session.puzzleTemplateCatalog.length > 0 &&
!eventLog.some((event) => event.event === 'puzzle_template_catalog')
) {
items.push({
id: `session-template-catalog-${session.puzzleTemplateCatalog.length}`,
meta: '模板',
title: `读取 ${session.puzzleTemplateCatalog.length} 个模板`,
detail: session.puzzleTemplateCatalog
.slice(0, 3)
.map((template) => template.title)
.join('、'),
detailLines: [],
tone: 'info',
});
}
if (
session.puzzleTemplateSelection &&
!eventLog.some((event) => event.event === 'puzzle_template_selection')
) {
items.push({
id: `session-template-${session.puzzleTemplateSelection.templateId}`,
meta: '模板',
title: `选择 ${session.puzzleTemplateSelection.title}`,
detail: session.puzzleTemplateSelection.reason,
detailLines: [
`${formatPuzzleLevelMode(session.puzzleTemplateSelection.selectedLevelMode)} · ${session.puzzleTemplateSelection.plannedLevelCount}`,
],
tone: 'info',
});
}
if (
session.puzzleImageGenerationPlan &&
!eventLog.some((event) => event.event === 'puzzle_level_plan')
) {
const plan = session.puzzleImageGenerationPlan;
items.push({
id: `session-level-plan-${plan.templateId}`,
meta: '关卡',
title: `规划 ${plan.levels.length} 个关卡`,
detail: `${formatPuzzleLevelMode(plan.mode)} · ${plan.estimatedCostRange.minPoints}-${plan.estimatedCostRange.maxPoints} 泥点`,
detailLines: plan.levels.slice(0, 4).map((level) =>
[
level.levelName,
level.pictureDescription,
level.pictureReference ? `参考图:${level.pictureReference}` : null,
]
.filter(Boolean)
.join(' · '),
),
tone: 'info',
});
}
if (
session.targetBinding &&
!eventLog.some((event) => event.event === 'target_session')
) {
items.push({
id: `session-target-${session.targetBinding.targetSessionId}`,
meta: '目标',
title: '拼图草稿已绑定',
detail: formatTargetStage(session.targetBinding.targetStage),
detailLines: [`目标会话:${session.targetBinding.targetSessionId}`],
tone: 'done',
});
}
if (
items.length === 0 &&
session.stage !== 'idle' &&
!eventLog.some((event) => event.event === 'stage')
) {
const isWaiting =
session.stage === 'waiting_template_confirmation' ||
session.stage === 'waiting_user';
const isDone = session.stage === 'target_ready';
const tone: CreativeAgentProcessTone =
session.stage === 'failed'
? 'danger'
: isWaiting
? 'warning'
: isDone
? 'done'
: 'active';
items.push({
id: `session-stage-${session.stage}`,
meta: '阶段',
title: getCreativeAgentStageDisplayLabel(
session.stage,
tone === 'active' ? 'active' : tone === 'done' ? 'done' : 'waiting',
),
detail: '当前状态',
detailLines: [],
tone,
});
}
return items;
}
export function buildCreativeAgentProcessItems(
eventLog: CreativeAgentSseEvent[],
session: CreativeAgentSessionSnapshot | null,
) {
const terminalStageSeen = eventLog.some(
(event) =>
event.event === 'stage' &&
(event.data.stage === 'waiting_template_confirmation' ||
event.data.stage === 'target_ready' ||
event.data.stage === 'waiting_user' ||
event.data.stage === 'failed'),
);
const isComplete =
eventLog.some((event) => event.event === 'done') ||
terminalStageSeen ||
session?.stage === 'waiting_template_confirmation' ||
session?.stage === 'target_ready' ||
session?.stage === 'waiting_user' ||
session?.stage === 'failed';
const activeStageEventIndex = eventLog.reduce(
(latestIndex, event, index) => (event.event === 'stage' ? index : latestIndex),
-1,
);
const context: ProcessBuildContext = {
activeStageEventIndex,
eventLog,
isComplete,
};
return [
...buildThoughtSummaryItems(eventLog, isComplete),
...eventLog
.map((event, index) => buildEventProcessItem(event, index, context))
.filter((item): item is CreativeAgentProcessItem => Boolean(item)),
...buildSessionFallbackItems(session, eventLog),
].slice(-24);
}