分开加载风格化品牌与模板 #65
20
app/api/agent-workflow/template-prompts/route.ts
Normal file
20
app/api/agent-workflow/template-prompts/route.ts
Normal 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 }) });
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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,
|
||||
|
||||
18
lib/agents/workflow-loading.ts
Normal file
18
lib/agents/workflow-loading.ts
Normal 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)))) : "";
|
||||
}
|
||||
@ -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),
|
||||
|
||||
@ -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, {
|
||||
|
||||
@ -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(["完整风格提示词", "选中的子模板提示词"]);
|
||||
});
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user