Files
ttstd-infinite-canvas/web/src/services/art-critique-execution.ts

53 lines
3.2 KiB
TypeScript

import { inspectAgentImage } from "./agent-image-preview";
import { runBackendToolGenerationTask } from "./api/generation-task";
import { runArtCritiquePipeline, type ArtCritiquePipelineOptions } from "@/lib/art-critique/pipeline";
import { artCritiqueSourceFingerprint, isArtCritiqueImageInput } from "@/lib/art-critique/contracts";
import type { CanvasNodeData } from "@/types/canvas";
import type { AiConfig } from "@/stores/use-config-store";
import { getActiveUserScope } from "@/lib/user-scope";
export const ART_CRITIQUE_MAX_MODEL_CALLS = 9;
// One execution boundary for manual UI and future approved Agent runs.
// The executor may be replaced by an approval-aware task submitter; parsing stays in the pipeline.
export async function executeArtCritique(input: {
nodeId: string;
runId: string;
source: CanvasNodeData;
config: AiConfig;
signal: AbortSignal;
onTaskCreated: (stage: string, taskId: string) => void;
onStage?: ArtCritiquePipelineOptions["onStage"];
onDraftReport?: ArtCritiquePipelineOptions["onDraftReport"];
}, dependencies = { inspectImage: inspectAgentImage, runTask: runBackendToolGenerationTask, runPipeline: runArtCritiquePipeline }) {
if (!input.nodeId || !input.runId || !isArtCritiqueImageInput(input.source)) throw new Error("分析需要真实目标节点、执行标识和已有图片");
const scope = getActiveUserScope();
const assertLive = () => {
if (input.signal.aborted || getActiveUserScope() !== scope) throw new DOMException("分析已停止或账号已切换", "AbortError");
};
assertLive();
const content = input.source.metadata?.content || input.source.metadata?.previewContent || "";
const image = await dependencies.inspectImage({ storageKey: input.source.metadata?.storageKey, ...(content.startsWith("data:") ? { dataUrl: content } : { url: content }), name: input.source.title }, "original");
assertLive();
const calledStages = new Set<string>();
return dependencies.runPipeline(input.config, { dataUrl: image.storageKey, title: input.source.title, sourceFingerprint: artCritiqueSourceFingerprint(input.source) }, {
signal: input.signal,
onStage: (stage) => { assertLive(); input.onStage?.(stage); },
onDraftReport: (report) => { assertLive(); input.onDraftReport?.(report); },
requestStage: async ({ config, messages, tool, toolName, signal }) => {
assertLive();
if (calledStages.has(toolName) || calledStages.size >= ART_CRITIQUE_MAX_MODEL_CALLS) throw new Error("分析阶段重复或超过本次调用数量上限");
calledStages.add(toolName);
const result = await dependencies.runTask({
config, messages, tools: [tool], toolChoice: { type: "function", name: toolName },
prompt: `图片审美分析:${toolName}`, signal,
metadata: { source: "art-critique", nodeId: input.nodeId, runId: input.runId, stage: toolName },
// A task can be accepted while the user stops observing; retain that receipt.
onTaskCreated: (task) => { if (getActiveUserScope() === scope) input.onTaskCreated(toolName, task.id); },
});
assertLive();
return result;
},
});
}