From 03a5613d80ff179fd9ab8417b176a354519a832f Mon Sep 17 00:00:00 2001 From: WuYongJie <549727955@qq.com> Date: Tue, 10 Nov 2026 15:04:09 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=86=E5=BC=80=E5=8A=A0=E8=BD=BD=E9=A3=8E?= =?UTF-8?q?=E6=A0=BC=E5=8C=96=E5=93=81=E7=89=8C=E4=B8=8E=E6=A8=A1=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../agent-workflow/template-prompts/route.ts | 20 +++ app/api/site-settings/route.ts | 39 +++-- .../[id]/CanvasEditorLegacyPageClient.tsx | 136 +++++++++++++----- .../agents/AgentGenerationWorkspaceDarkV2.tsx | 13 ++ lib/agents/workflow-composer.ts | 49 ++++--- lib/agents/workflow-loading.ts | 18 +++ lib/ai/vip98k-models.ts | 4 +- lib/client/canvas-site-settings.ts | 18 ++- tests/routes/agent-project-recovery.test.ts | 27 ++++ tests/routes/model-classification.test.ts | 6 + 10 files changed, 263 insertions(+), 67 deletions(-) create mode 100644 app/api/agent-workflow/template-prompts/route.ts create mode 100644 lib/agents/workflow-loading.ts diff --git a/app/api/agent-workflow/template-prompts/route.ts b/app/api/agent-workflow/template-prompts/route.ts new file mode 100644 index 000000000..ecf5258b2 --- /dev/null +++ b/app/api/agent-workflow/template-prompts/route.ts @@ -0,0 +1,20 @@ +import { z } from "zod"; +import { auth } from "@/app/(auth)/auth"; +import { resolveAgentWorkflowComposerSelections } from "@/lib/agents/workflow-composer"; +import { AGENT_WORKFLOW_UI_CONFIG_KEY, normalizeAgentWorkflowUiConfig } from "@/lib/agents/workflow-ui-config"; +import { getSystemSetting } from "@/lib/db/queries"; + +const selectionsSchema = z.record(z.string().max(160), z.string().max(8192)) + .refine((selections) => Object.keys(selections).length <= 100); + +export async function POST(request: Request) { + const session = await auth(); + if (!session?.user?.id) return Response.json({ error: "请先登录" }, { status: 401 }); + + const selections = selectionsSchema.safeParse(await request.json().catch(() => null)); + if (!selections.success) return Response.json({ error: "风格模板选择无效" }, { status: 400 }); + + const setting = await getSystemSetting({ key: AGENT_WORKFLOW_UI_CONFIG_KEY }); + const config = normalizeAgentWorkflowUiConfig(setting?.value); + return Response.json({ selections: resolveAgentWorkflowComposerSelections({ config, selections: selections.data }) }); +} diff --git a/app/api/site-settings/route.ts b/app/api/site-settings/route.ts index 3c194fb27..9c21ae70a 100644 --- a/app/api/site-settings/route.ts +++ b/app/api/site-settings/route.ts @@ -10,7 +10,8 @@ import { systemSettingSnapshot, systemSettings, } from "@/lib/db/schema"; -import { and, eq, inArray } from "drizzle-orm"; +import { and, eq, inArray, sql } from "drizzle-orm"; +import { omitAgentWorkflowPrompts } from "@/lib/agents/workflow-loading"; import { auth } from "@/app/(auth)/auth"; import { isAgentWorkflowAdminType, isSiteAdminType } from "@/lib/middleware/admin"; import { @@ -553,6 +554,8 @@ function normalizeAgentCategoryTreeConfig(value: unknown): AgentCategoryTreeConf type PublicSiteSettingsScope = | "agent-brand-model" | "agent-canvas" + | "agent-canvas-base" + | "agent-workflow-templates" | "agent-entry" | "agent-presets" | "auth" @@ -565,6 +568,8 @@ type PublicSiteSettingsScope = const PUBLIC_SITE_SETTINGS_SCOPES = new Set([ "agent-brand-model", "agent-canvas", + "agent-canvas-base", + "agent-workflow-templates", "agent-entry", "agent-presets", "auth", @@ -682,7 +687,7 @@ function createScopedSiteSettingsResponse( return response; } -async function readScopedSiteSettings(keys: string[]) { +async function readScopedSiteSettings(keys: string[], omitWorkflowSteps = false) { const [settings, settingRows] = await Promise.all([ db .select() @@ -690,8 +695,14 @@ async function readScopedSiteSettings(keys: string[]) { .limit(1) .then((rows) => rows[0]), keys.length > 0 - ? db - .select() + ? (omitWorkflowSteps + ? db.select({ + key: systemSettings.key, + value: sql`case when ${systemSettings.key} = ${AGENT_WORKFLOW_UI_CONFIG_KEY} + and jsonb_typeof(${systemSettings.value}) = 'object' + then ${systemSettings.value} - 'steps' else ${systemSettings.value} end`, + }) + : db.select()) .from(systemSettings) .where(inArray(systemSettings.key, keys)) : Promise.resolve([]), @@ -779,8 +790,17 @@ async function getScopedPublicSiteSettings( }); } - if (scope === "agent-canvas" || scope === "agent-entry") { - const { settings, valueByKey } = await readScopedSiteSettings(AGENT_CANVAS_SETTING_KEYS); + if (scope === "agent-workflow-templates") { + const [setting] = await db.select({ value: systemSettings.value }) + .from(systemSettings).where(eq(systemSettings.key, AGENT_WORKFLOW_UI_CONFIG_KEY)).limit(1); + const config = normalizeAgentWorkflowUiConfig(setting?.value); + return createScopedSiteSettingsResponse(scope, { + steps: omitAgentWorkflowPrompts(config.steps), + }, "MISS", options.cacheKey || scope, { etag: options.etag }); + } + + if (scope === "agent-canvas" || scope === "agent-canvas-base" || scope === "agent-entry") { + const { settings, valueByKey } = await readScopedSiteSettings(AGENT_CANVAS_SETTING_KEYS, scope !== "agent-canvas"); const canvasFeatureAvailability = await readCanvasFeatureAvailability( valueByKey ); @@ -831,7 +851,9 @@ async function getScopedPublicSiteSettings( pageFeatureSettings: rawUiConfig?.pageFeatureSettings, businessModeSettings: rawUiConfig?.businessModeSettings, } - : rawUiConfig + : scope === "agent-canvas-base" + ? { ...rawUiConfig, steps: [] } + : rawUiConfig ), agentWorkflowShowcaseConfig: normalizeAgentWorkflowShowcaseConfig( valueByKey.get(AGENT_WORKFLOW_SHOWCASE_CONFIG_KEY) @@ -1090,7 +1112,8 @@ export async function GET(request: NextRequest) { const cacheKey = brandName.trim() ? `${scopedScope}:${brandName.trim().toLowerCase()}` : scopedScope; - const etag = scopedScope === "agent-canvas" || scopedScope === "agent-entry" + const etag = scopedScope === "agent-canvas" || scopedScope === "agent-entry" || + scopedScope === "agent-canvas-base" || scopedScope === "agent-workflow-templates" ? await getAgentCanvasSettingsEtag(scopedScope) : undefined; if (etag && request.headers.get("If-None-Match") === etag) { diff --git a/app/canvas/[id]/CanvasEditorLegacyPageClient.tsx b/app/canvas/[id]/CanvasEditorLegacyPageClient.tsx index 2efb45b3a..0ec275fe9 100644 --- a/app/canvas/[id]/CanvasEditorLegacyPageClient.tsx +++ b/app/canvas/[id]/CanvasEditorLegacyPageClient.tsx @@ -233,7 +233,8 @@ import { type WorkflowSceneRecord, } from "@/lib/video-workflow/runtime"; import { agentProjectService } from "@/services/agent-projects"; -import { fetchCanvasSiteSettings } from "@/lib/client/canvas-site-settings"; +import { getWorkflowSelectionsRequest } from "@/lib/agents/workflow-loading"; +import { fetchCanvasSiteSettings, fetchWorkflowTemplatePrompts } from "@/lib/client/canvas-site-settings"; import { canvasService, imageGenerationService, @@ -273,6 +274,17 @@ import type { const CANVAS_SITE_SETTINGS_REFRESH_INTERVAL_MS = 15000; const CANVAS_SURFACE_BACKGROUND = "#f5f5f5"; +function WorkflowLoadState({ name, error, retrying, onRetry }: { name: string; error?: Error; retrying?: boolean; onRetry: () => void }) { + const failed = Boolean(error && !retrying); + const timedOut = error?.name === "AbortError" || error?.name === "TimeoutError"; + return ( +
+ {failed ? `${name}${timedOut ? "加载超时" : "加载失败"}` : `正在加载${name}…`} + {failed && } +
+ ); +} + function CanvasLazyPanelLoader({ label, className, @@ -2176,13 +2188,13 @@ export default function CanvasEditorLegacyPage({ ); const canvasSiteSettingsKey = requestedAgentMode ? hasActiveAgentProject - ? "/api/site-settings?scope=agent-canvas" + ? "/api/site-settings?scope=agent-canvas-base" : "/api/site-settings?scope=agent-entry" : "/api/site-settings?scope=canvas"; const [hasResolvedCanvasSiteSettings, setHasResolvedCanvasSiteSettings] = useState(false); - const { data: siteSettingsData } = useSWR<{ + const { data: siteSettingsData, error: siteSettingsError, isValidating: siteSettingsLoading, mutate: reloadSiteSettings } = useSWR<{ siteName?: string; siteLogo?: string | null; siteLogoDark?: string | null; @@ -2213,6 +2225,7 @@ export default function CanvasEditorLegacyPage({ ? 0 : CANVAS_SITE_SETTINGS_REFRESH_INTERVAL_MS, errorRetryCount: 1, + shouldRetryOnError: !requestedAgentMode || !hasActiveAgentProject, dedupingInterval: 30_000, revalidateOnFocus: true, revalidateOnReconnect: true, @@ -2279,9 +2292,33 @@ export default function CanvasEditorLegacyPage({ !requestedAgentMode || !agentProjectId || loadedAgentProjectId === agentProjectId; + const [workflowTemplateMarker, setWorkflowTemplateMarker] = useState(null); + const [workflowTemplatesRequested, setWorkflowTemplatesRequested] = useState(false); + useEffect(() => { + if (!isAgentMode || !hasActiveAgentProject || workflowTemplatesRequested) return; + const element = workflowTemplateMarker; + if (!element) return; + const observer = new IntersectionObserver(([entry]) => { + if (entry?.isIntersecting) setWorkflowTemplatesRequested(true); + }); + observer.observe(element); + return () => observer.disconnect(); + }, [hasActiveAgentProject, isAgentMode, workflowTemplateMarker, workflowTemplatesRequested]); + const { data: workflowTemplates, error: workflowTemplatesError, isValidating: workflowTemplatesLoading, mutate: reloadWorkflowTemplates } = useSWR<{ + steps: AgentWorkflowUiConfig["steps"]; + }>( + isAgentMode && hasActiveAgentProject && siteSettingsData?.agentWorkflowUiConfig && workflowTemplatesRequested + ? "/api/site-settings?scope=agent-workflow-templates" + : null, + fetchCanvasSiteSettings, + { shouldRetryOnError: false } + ); const resolvedWorkflowUiConfig = useMemo( - () => siteSettingsData?.agentWorkflowUiConfig || defaultAgentWorkflowUiConfig, - [siteSettingsData?.agentWorkflowUiConfig] + () => { + const base = siteSettingsData?.agentWorkflowUiConfig || defaultAgentWorkflowUiConfig; + return requestedAgentMode ? { ...base, steps: workflowTemplates?.steps ?? [] } : base; + }, + [requestedAgentMode, siteSettingsData?.agentWorkflowUiConfig, workflowTemplates?.steps] ); const workflowPageFeatureSettings = useMemo( () => getAgentWorkflowPageFeatureSettings(resolvedWorkflowUiConfig), @@ -2330,7 +2367,7 @@ export default function CanvasEditorLegacyPage({ }); const sanitizedWorkflowSelections = useMemo( () => - requestedAgentMode && !siteSettingsData?.agentWorkflowUiConfig + requestedAgentMode && !workflowTemplates ? workflowSelections : sanitizeAgentWorkflowSelections({ config: resolvedWorkflowUiConfig || defaultAgentWorkflowUiConfig, @@ -2338,7 +2375,7 @@ export default function CanvasEditorLegacyPage({ }), [ requestedAgentMode, - siteSettingsData?.agentWorkflowUiConfig, + workflowTemplates, resolvedWorkflowUiConfig, workflowSelections, ] @@ -2346,6 +2383,14 @@ export default function CanvasEditorLegacyPage({ const deferredSanitizedWorkflowSelections = useDeferredValue( sanitizedWorkflowSelections ); + const workflowSelectionRequest = isAgentMode && hasActiveAgentProject && hasLoadedCurrentAgentProject + ? getWorkflowSelectionsRequest(sanitizedWorkflowSelections) + : ""; + const { data: workflowPromptData, error: workflowPromptError, isValidating: workflowPromptLoading, mutate: reloadWorkflowPrompts } = useSWR( + workflowSelectionRequest ? ["/api/agent-workflow/template-prompts", workflowSelectionRequest] as const : null, + fetchWorkflowTemplatePrompts, + { shouldRetryOnError: false } + ); const resolvedBrandModelConfig = useMemo( () => normalizeAgentWorkflowBrandModelConfig( @@ -9921,6 +9966,8 @@ export default function CanvasEditorLegacyPage({ selectedBrandModel, appearanceRecognitionEnabled, selections: deferredSanitizedWorkflowSelections, + resolvedSelections: workflowPromptData?.selectionRequest === getWorkflowSelectionsRequest(deferredSanitizedWorkflowSelections) + ? workflowPromptData?.selections : undefined, }); if (canceled) { @@ -9943,6 +9990,7 @@ export default function CanvasEditorLegacyPage({ selectedBrandModel, workflowInputs, workflowMaterials, + workflowPromptData, ]); useEffect(() => { workflowInputsRef.current = workflowInputs; @@ -10064,15 +10112,23 @@ export default function CanvasEditorLegacyPage({ [patchPersistedAgentWorkflowState] ); const getLatestWorkflowContext = useCallback(() => { + const selections = workflowSelectionsRef.current; return buildAgentWorkflowComposerContext({ config: resolvedWorkflowUiConfig || defaultAgentWorkflowUiConfig, inputs: workflowInputsRef.current, materials: workflowMaterialsRef.current, selectedBrandModel: selectedBrandModelRef.current, appearanceRecognitionEnabled: appearanceRecognitionEnabledRef.current, - selections: workflowSelectionsRef.current, + selections, + resolvedSelections: workflowPromptData?.selectionRequest === getWorkflowSelectionsRequest(selections) + ? workflowPromptData?.selections : undefined, }); - }, [resolvedWorkflowUiConfig]); + }, [resolvedWorkflowUiConfig, workflowPromptData]); + const getWorkflowPromptStatus = useCallback(() => { + const currentRequest = getWorkflowSelectionsRequest(workflowSelectionsRef.current); + if (!currentRequest || currentRequest === workflowPromptData?.selectionRequest) return "ready"; + return workflowPromptError && currentRequest === workflowSelectionRequest ? "error" : "loading"; + }, [workflowPromptData, workflowPromptError, workflowSelectionRequest]); const effectiveSelectedAgentPresetId = null; const { data: selectedAgentPresetResponse } = useSWR<{ success?: boolean; @@ -10447,30 +10503,37 @@ export default function CanvasEditorLegacyPage({ ) : null; - const workflowStylePanel = useMemo( - () => ( - - ), - [ - handleWorkflowInputsChange, - handleWorkflowMaterialsChange, - handleWorkflowSelectionsChange, - resolvedWorkflowUiConfig, - sanitizedWorkflowSelections, - workflowInputs, - workflowMaterials, - ] - ); + const workflowStylePanel = useMemo(() => ( +
+ {workflowTemplates ? ( + + ) : siteSettingsError && !siteSettingsData && !siteSettingsLoading ? ( +

请先在上方重试加载品牌和类目。

+ ) : !workflowTemplatesRequested ? ( + + ) : ( + void reloadWorkflowTemplates().catch(() => {})} /> + )} + {workflowPromptError && ( + void reloadWorkflowPrompts().catch(() => {})} /> + )} +
+ ), [ + handleWorkflowInputsChange, handleWorkflowMaterialsChange, handleWorkflowSelectionsChange, + reloadWorkflowPrompts, reloadWorkflowTemplates, resolvedWorkflowUiConfig, sanitizedWorkflowSelections, + siteSettingsData, siteSettingsError, siteSettingsLoading, workflowInputs, workflowMaterials, workflowPromptError, + workflowPromptLoading, workflowTemplates, workflowTemplatesError, workflowTemplatesLoading, workflowTemplatesRequested, + ]); if (canvasLoadError && !isAgentMode) { return
@@ -10513,17 +10576,20 @@ export default function CanvasEditorLegacyPage({ theme="dark" /> } - brandStorePanel={ + brandStorePanel={siteSettingsData?.agentWorkflowUiConfig ? ( - } + ) : ( + void reloadSiteSettings().catch(() => {})} /> + )} chatId={agentModeChatId} deletedResultCollections={agentProjectDeletedResultCollections} getLatestWorkflowContext={getLatestWorkflowContext} + getWorkflowPromptStatus={getWorkflowPromptStatus} materialSelectionPanel={ isWorkflowMaterialStepEnabled ? ( AgentWorkflowComposerContext | null; + getWorkflowPromptStatus?: () => "ready" | "loading" | "error"; onBack?: () => void; brandStorePanel?: ReactNode; materialSelectionPanel?: ReactNode; @@ -3085,6 +3086,7 @@ export function AgentGenerationWorkspaceDarkV2({ workflowFeatureSettings = null, workflowPromptOptimizationEnabled = true, getLatestWorkflowContext, + getWorkflowPromptStatus, onBack, brandStorePanel, materialSelectionPanel, @@ -6318,6 +6320,11 @@ export function AgentGenerationWorkspaceDarkV2({ toast.error(`请先查询并选择${skuReferenceSourceLabel}参考图`); return; } + const promptStatus = getWorkflowPromptStatus?.(); + if (promptStatus === "loading" || promptStatus === "error") { + toast.error(promptStatus === "error" ? "请先在风格模板区重试加载提示词" : "正在加载所选模板的提示词,请稍候"); + return; + } const resolvedWorkflowContext = getLatestWorkflowContext?.() ?? workflowContext; if ( isDetailReplacementPreset && @@ -6629,6 +6636,7 @@ export function AgentGenerationWorkspaceDarkV2({ skuResultCards, userMessageCount, getLatestWorkflowContext, + getWorkflowPromptStatus, isSceneGenerationPreset, workflowContext, sceneMessageAttachments, @@ -7802,6 +7810,11 @@ export function AgentGenerationWorkspaceDarkV2({ toast.error(detailGenerationValidationIssues[0] || "请先补齐 AI详情生成参数"); return; } + const promptStatus = getWorkflowPromptStatus?.(); + if (promptStatus === "loading" || promptStatus === "error") { + toast.error(promptStatus === "error" ? "请先在风格模板区重试加载提示词" : "正在加载所选模板的提示词,请稍候"); + return; + } const productAttachments = detailGenerationProductSlotIndexes.map((index) => detailGenerationSlots[index] diff --git a/lib/agents/workflow-composer.ts b/lib/agents/workflow-composer.ts index e5dd75c4a..1866bc7db 100644 --- a/lib/agents/workflow-composer.ts +++ b/lib/agents/workflow-composer.ts @@ -407,14 +407,10 @@ function buildPromptSection(title: string, lines: string[]) { return `【${title}】\n${normalizedLines.join("\n")}`; } -export function buildAgentWorkflowComposerContext(params: { +export function resolveAgentWorkflowComposerSelections(params: { config?: AgentWorkflowUiConfig | null; - inputs: AgentWorkflowInputsValue; - materials?: AgentWorkflowMaterialSelections | null; - selectedBrandModel?: AgentWorkflowSelectedBrandModel | null; - appearanceRecognitionEnabled?: boolean; selections: Partial>; -}): AgentWorkflowComposerContext | null { +}): AgentWorkflowComposerContext["selections"] { const config = params.config || defaultAgentWorkflowUiConfig; const steps = flattenVisibleAgentWorkflowSteps( getEnabledAgentWorkflowStepTree(Array.isArray(config.steps) ? config.steps : []), @@ -422,22 +418,31 @@ export function buildAgentWorkflowComposerContext(params: { ) .map(({ step }) => step) .filter((step) => Array.isArray(step.options) && step.options.length > 0); - const selections = steps - .map((step) => { - const selectedOptions = getAgentWorkflowSelectedOptionPath({ - step, - selectionValue: params.selections[step.key], - }); - if (selectedOptions.length === 0) return []; - return selectedOptions.map((option) => ({ - stepKey: step.key, - stepTitle: step.title, - optionKey: option.key, - optionName: option.buttonName, - promptFragment: option.promptFragment || option.buttonName, - })); - }) - .flat() as AgentWorkflowComposerContext["selections"]; + return steps.flatMap((step) => { + const selectedOptions = getAgentWorkflowSelectedOptionPath({ + step, + selectionValue: params.selections[step.key], + }); + return selectedOptions.map((option) => ({ + stepKey: step.key, + stepTitle: step.title, + optionKey: option.key, + optionName: option.buttonName, + promptFragment: option.promptFragment || option.buttonName, + })); + }); +} + +export function buildAgentWorkflowComposerContext(params: { + config?: AgentWorkflowUiConfig | null; + inputs: AgentWorkflowInputsValue; + materials?: AgentWorkflowMaterialSelections | null; + selectedBrandModel?: AgentWorkflowSelectedBrandModel | null; + appearanceRecognitionEnabled?: boolean; + selections: Partial>; + resolvedSelections?: AgentWorkflowComposerContext["selections"]; +}): AgentWorkflowComposerContext | null { + const selections = params.resolvedSelections ?? resolveAgentWorkflowComposerSelections(params); const referenceImages = getSelectedWorkflowReferenceImages({ inputs: params.inputs, materials: params.materials, diff --git a/lib/agents/workflow-loading.ts b/lib/agents/workflow-loading.ts new file mode 100644 index 000000000..a26eb20b3 --- /dev/null +++ b/lib/agents/workflow-loading.ts @@ -0,0 +1,18 @@ +import type { AgentWorkflowOptionUiConfig, AgentWorkflowStepUiConfig } from "@/types/agent"; + +export function omitAgentWorkflowPrompts(steps: AgentWorkflowStepUiConfig[]): AgentWorkflowStepUiConfig[] { + const omitOptionPrompt = ({ promptFragment: _prompt, children, ...option }: AgentWorkflowOptionUiConfig): AgentWorkflowOptionUiConfig => ({ + ...option, + ...(children?.length ? { children: children.map(omitOptionPrompt) } : {}), + }); + return steps.map(({ options, children, ...step }) => ({ + ...step, + options: options.map(omitOptionPrompt), + ...(children?.length ? { children: omitAgentWorkflowPrompts(children) } : {}), + })); +} + +export function getWorkflowSelectionsRequest(selections: Record) { + const entries = Object.entries(selections).filter(([, value]) => value?.trim()); + return entries.length ? JSON.stringify(Object.fromEntries(entries.sort(([a], [b]) => a.localeCompare(b)))) : ""; +} diff --git a/lib/ai/vip98k-models.ts b/lib/ai/vip98k-models.ts index 96fc475ac..886120235 100644 --- a/lib/ai/vip98k-models.ts +++ b/lib/ai/vip98k-models.ts @@ -143,6 +143,7 @@ const IMAGE_TOOL_MODEL_IDS = new Set([ "id-photo", "image-expansion", "image-upscaler", + "ocr", "photo-cleanup", "remove-background", "remove-object", @@ -1041,6 +1042,7 @@ function createToolPreset( const inputModes = getInputModes(model); const outputModes = getOutputModes(model); const isImageTool = type === "image_tool"; + const isOcr = modelId.toLowerCase() === "ocr"; const toolOperation = IMAGE_TOOL_OPERATION_BY_MODEL_ID[modelId.toLowerCase()] || type; return { @@ -1056,7 +1058,7 @@ function createToolPreset( kind: type.replace(/_/g, "-"), mediaType: isImageTool ? "image" : type === "audio" ? "audio" : "document", modelType: type, - outputType: outputModes[0] || (isImageTool ? "image" : "text"), + outputType: isOcr ? "text" : outputModes[0] || (isImageTool ? "image" : "text"), inputModes, outputModes, supportedModes: getCapabilities(model), diff --git a/lib/client/canvas-site-settings.ts b/lib/client/canvas-site-settings.ts index 70cebdb58..afa249e83 100644 --- a/lib/client/canvas-site-settings.ts +++ b/lib/client/canvas-site-settings.ts @@ -1,11 +1,27 @@ +import type { AgentWorkflowComposerContext } from "@/types/agent"; + const settingsCache = new Map(); +export async function fetchWorkflowTemplatePrompts([url, selectionRequest]: readonly [string, string]) { + const response = await fetch(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: selectionRequest, + signal: AbortSignal.timeout(15_000), + }); + if (!response.ok) throw new Error("风格模板提示词加载失败"); + const data = await response.json() as { selections?: AgentWorkflowComposerContext["selections"] }; + if (!Array.isArray(data.selections)) throw new Error("风格模板提示词返回异常"); + return { selectionRequest, selections: data.selections }; +} + export async function fetchCanvasSiteSettings(url: string) { const cached = settingsCache.get(url); const controller = new AbortController(); const timeoutId = setTimeout( () => controller.abort(), - url === "/api/site-settings?scope=agent-canvas" ? 30_000 : 10_000 + url === "/api/site-settings?scope=agent-canvas" || + url === "/api/site-settings?scope=agent-workflow-templates" ? 30_000 : 10_000 ); try { const response = await fetch(url, { diff --git a/tests/routes/agent-project-recovery.test.ts b/tests/routes/agent-project-recovery.test.ts index 7bb318967..f97dfe132 100644 --- a/tests/routes/agent-project-recovery.test.ts +++ b/tests/routes/agent-project-recovery.test.ts @@ -214,6 +214,11 @@ test("entry config stays small and unchanged config returns 304 before loading v const entryData = await entry.json(); expect(entryData.agentWorkflowUiConfig.steps).toEqual([]); expect(JSON.stringify(entryData).length).toBeLessThan(20_000); + const base = await (await route.GET(request("agent-canvas-base"))).json(); + expect(base.agentWorkflowUiConfig.steps).toEqual([]); + const templates = await (await route.GET(request("agent-workflow-templates"))).json(); + expect(templates.steps[0].options[0]).toMatchObject({ key: "test", buttonName: "风格一" }); + expect(JSON.stringify(templates)).not.toContain("生成提示词"); const full = await route.GET(request("agent-canvas")); expect((await full.json()).agentWorkflowUiConfig.steps[0].options[0].promptFragment).toBe("生成提示词".repeat(3000).slice(0, 20_000)); const etag = full.headers.get("ETag"); @@ -227,3 +232,25 @@ test("entry config stays small and unchanged config returns 304 before loading v expect(changed.status).toBe(200); expect(changed.headers.get("ETag")).not.toBe(etag); }); + +test("selected template prompts preserve the full path without downloading unrelated prompts", async () => { + const option = (key: string, promptFragment: string, children: unknown[] = []) => ({ + key, buttonName: key, enabled: true, sortOrder: 0, promptFragment, children, previewImageUrl: "https://example.com/style.webp", + }); + const value = { steps: [{ key: "style", title: "风格", enabled: true, sortOrder: 0, options: [ + option("selected", "完整风格提示词", [option("leaf", "选中的子模板提示词")]), + option("unrelated", "不应下载的其他提示词"), + ] }] }; + const load = loadRuntime({ + "@/app/(auth)/auth": { auth: async () => ({ user: { id: owner } }) }, + "@/lib/db/queries": { getSystemSetting: async () => ({ value }) }, + }); + const { serializeAgentWorkflowSelectionPath } = load("lib/agents/workflow-ui-config.ts"); + const { POST } = load("app/api/agent-workflow/template-prompts/route.ts"); + const result = await POST(new Request("http://localhost/api/agent-workflow/template-prompts", { + method: "POST", body: JSON.stringify({ style: serializeAgentWorkflowSelectionPath(["selected", "leaf"]) }), + })); + expect(result.status).toBe(200); + const { selections } = await result.json(); + expect(selections.map((s: { promptFragment: string }) => s.promptFragment)).toEqual(["完整风格提示词", "选中的子模板提示词"]); +}); diff --git a/tests/routes/model-classification.test.ts b/tests/routes/model-classification.test.ts index 989f5e625..1c0ff644b 100644 --- a/tests/routes/model-classification.test.ts +++ b/tests/routes/model-classification.test.ts @@ -30,6 +30,12 @@ const atlasHunyuan3D = { tags: ["TEXT-TO-3D", "NEW"], }; +test("syncing the provider catalog keeps OCR out of image generation", () => { + const [ocr] = buildVip98kModelPresets([{ id: "ocr", type: "image", capabilities: ["media.tasks"] }]); + expect(ocr).toMatchObject({ modelId: "ocr", isHidden: true, supportsImageGen: false, capabilities: { outputType: "text" } }); + expect(isImageGenerationModelDescriptor({ ...ocr, providerType: "vip98k" })).toBe(false); +}); + test.describe("AI model media classification", () => { test("classifies Qwen Image 3.0 as Alibaba text-to-image and image editing", () => { expect(isAlibabaImageTextToImageModelId("qwen-image-3.0")).toBe(true);