Merge pull request '分开加载风格化品牌与模板' (#65) from codex/stylization-load-20261110 into main

This commit is contained in:
WuYongJie 2026-11-10 15:04:19 +08:00
commit 5f24743bdb
10 changed files with 263 additions and 67 deletions

View File

@ -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 }) });
}

View File

@ -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<string>([
"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<unknown>`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) {

View File

@ -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 (
<div className="flex items-center gap-3 rounded-xl border border-white/10 px-4 py-3 text-sm text-white/65" role={failed ? "alert" : "status"}>
<span>{failed ? `${name}${timedOut ? "加载超时" : "加载失败"}` : `正在加载${name}…`}</span>
{failed && <Button type="button" size="sm" variant="outline" onClick={onRetry}>重试</Button>}
</div>
);
}
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<HTMLDivElement | null>(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({
</div>
) : null;
const workflowStylePanel = useMemo(
() => (
<AgentWorkflowPanelDarkV2
config={resolvedWorkflowUiConfig}
embedded
inputs={workflowInputs}
materials={workflowMaterials}
onInputsChange={handleWorkflowInputsChange}
onMaterialsChange={handleWorkflowMaterialsChange}
onSelectionsChange={handleWorkflowSelectionsChange}
selections={sanitizedWorkflowSelections}
showBrandStoreSection={false}
/>
),
[
handleWorkflowInputsChange,
handleWorkflowMaterialsChange,
handleWorkflowSelectionsChange,
resolvedWorkflowUiConfig,
sanitizedWorkflowSelections,
workflowInputs,
workflowMaterials,
]
);
const workflowStylePanel = useMemo(() => (
<div ref={setWorkflowTemplateMarker} className="space-y-3">
{workflowTemplates ? (
<AgentWorkflowPanelDarkV2
config={resolvedWorkflowUiConfig}
embedded
inputs={workflowInputs}
materials={workflowMaterials}
onInputsChange={handleWorkflowInputsChange}
onMaterialsChange={handleWorkflowMaterialsChange}
onSelectionsChange={handleWorkflowSelectionsChange}
selections={sanitizedWorkflowSelections}
showBrandStoreSection={false}
/>
) : siteSettingsError && !siteSettingsData && !siteSettingsLoading ? (
<p className="text-sm text-white/65">请先在上方重试加载品牌和类目。</p>
) : !workflowTemplatesRequested ? (
<Button type="button" variant="outline" onClick={() => setWorkflowTemplatesRequested(true)}>加载风格模板</Button>
) : (
<WorkflowLoadState name="风格模板" error={workflowTemplatesError} retrying={workflowTemplatesLoading} onRetry={() => void reloadWorkflowTemplates().catch(() => {})} />
)}
{workflowPromptError && (
<WorkflowLoadState name="所选模板的提示词" error={workflowPromptError} retrying={workflowPromptLoading} onRetry={() => void reloadWorkflowPrompts().catch(() => {})} />
)}
</div>
), [
handleWorkflowInputsChange, handleWorkflowMaterialsChange, handleWorkflowSelectionsChange,
reloadWorkflowPrompts, reloadWorkflowTemplates, resolvedWorkflowUiConfig, sanitizedWorkflowSelections,
siteSettingsData, siteSettingsError, siteSettingsLoading, workflowInputs, workflowMaterials, workflowPromptError,
workflowPromptLoading, workflowTemplates, workflowTemplatesError, workflowTemplatesLoading, workflowTemplatesRequested,
]);
if (canvasLoadError && !isAgentMode) {
return <div role="alert" className="flex h-dvh flex-col items-center justify-center gap-4">
@ -10513,17 +10576,20 @@ export default function CanvasEditorLegacyPage({
theme="dark"
/>
}
brandStorePanel={
brandStorePanel={siteSettingsData?.agentWorkflowUiConfig ? (
<AgentWorkflowBrandStorePanel
config={resolvedWorkflowUiConfig}
inputs={workflowInputs}
onInputsChange={handleWorkflowInputsChange}
theme="dark"
/>
}
) : (
<WorkflowLoadState name="品牌和类目" error={siteSettingsError} retrying={siteSettingsLoading} onRetry={() => void reloadSiteSettings().catch(() => {})} />
)}
chatId={agentModeChatId}
deletedResultCollections={agentProjectDeletedResultCollections}
getLatestWorkflowContext={getLatestWorkflowContext}
getWorkflowPromptStatus={getWorkflowPromptStatus}
materialSelectionPanel={
isWorkflowMaterialStepEnabled ? (
<AgentWorkflowMaterialSelectionPanel

View File

@ -176,6 +176,7 @@ export interface AgentGenerationWorkspaceDarkV2Props {
workflowFeatureSettings?: AgentWorkflowPageFeatureSettings | null;
workflowPromptOptimizationEnabled?: boolean;
getLatestWorkflowContext?: () => 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]

View File

@ -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<Record<AgentWorkflowStepKey, string>>;
}): 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<Record<AgentWorkflowStepKey, string>>;
resolvedSelections?: AgentWorkflowComposerContext["selections"];
}): AgentWorkflowComposerContext | null {
const selections = params.resolvedSelections ?? resolveAgentWorkflowComposerSelections(params);
const referenceImages = getSelectedWorkflowReferenceImages({
inputs: params.inputs,
materials: params.materials,

View File

@ -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<string, string | undefined>) {
const entries = Object.entries(selections).filter(([, value]) => value?.trim());
return entries.length ? JSON.stringify(Object.fromEntries(entries.sort(([a], [b]) => a.localeCompare(b)))) : "";
}

View File

@ -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),

View File

@ -1,11 +1,27 @@
import type { AgentWorkflowComposerContext } from "@/types/agent";
const settingsCache = new Map<string, { etag: string; data: unknown }>();
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, {

View File

@ -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(["完整风格提示词", "选中的子模板提示词"]);
});

View File

@ -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);