Merge pull request '分开加载风格化品牌与模板' (#65) from codex/stylization-load-20261110 into main
This commit is contained in:
commit
5f24743bdb
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,
|
systemSettingSnapshot,
|
||||||
systemSettings,
|
systemSettings,
|
||||||
} from "@/lib/db/schema";
|
} 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 { auth } from "@/app/(auth)/auth";
|
||||||
import { isAgentWorkflowAdminType, isSiteAdminType } from "@/lib/middleware/admin";
|
import { isAgentWorkflowAdminType, isSiteAdminType } from "@/lib/middleware/admin";
|
||||||
import {
|
import {
|
||||||
@ -553,6 +554,8 @@ function normalizeAgentCategoryTreeConfig(value: unknown): AgentCategoryTreeConf
|
|||||||
type PublicSiteSettingsScope =
|
type PublicSiteSettingsScope =
|
||||||
| "agent-brand-model"
|
| "agent-brand-model"
|
||||||
| "agent-canvas"
|
| "agent-canvas"
|
||||||
|
| "agent-canvas-base"
|
||||||
|
| "agent-workflow-templates"
|
||||||
| "agent-entry"
|
| "agent-entry"
|
||||||
| "agent-presets"
|
| "agent-presets"
|
||||||
| "auth"
|
| "auth"
|
||||||
@ -565,6 +568,8 @@ type PublicSiteSettingsScope =
|
|||||||
const PUBLIC_SITE_SETTINGS_SCOPES = new Set<string>([
|
const PUBLIC_SITE_SETTINGS_SCOPES = new Set<string>([
|
||||||
"agent-brand-model",
|
"agent-brand-model",
|
||||||
"agent-canvas",
|
"agent-canvas",
|
||||||
|
"agent-canvas-base",
|
||||||
|
"agent-workflow-templates",
|
||||||
"agent-entry",
|
"agent-entry",
|
||||||
"agent-presets",
|
"agent-presets",
|
||||||
"auth",
|
"auth",
|
||||||
@ -682,7 +687,7 @@ function createScopedSiteSettingsResponse(
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function readScopedSiteSettings(keys: string[]) {
|
async function readScopedSiteSettings(keys: string[], omitWorkflowSteps = false) {
|
||||||
const [settings, settingRows] = await Promise.all([
|
const [settings, settingRows] = await Promise.all([
|
||||||
db
|
db
|
||||||
.select()
|
.select()
|
||||||
@ -690,8 +695,14 @@ async function readScopedSiteSettings(keys: string[]) {
|
|||||||
.limit(1)
|
.limit(1)
|
||||||
.then((rows) => rows[0]),
|
.then((rows) => rows[0]),
|
||||||
keys.length > 0
|
keys.length > 0
|
||||||
? db
|
? (omitWorkflowSteps
|
||||||
.select()
|
? 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)
|
.from(systemSettings)
|
||||||
.where(inArray(systemSettings.key, keys))
|
.where(inArray(systemSettings.key, keys))
|
||||||
: Promise.resolve([]),
|
: Promise.resolve([]),
|
||||||
@ -779,8 +790,17 @@ async function getScopedPublicSiteSettings(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (scope === "agent-canvas" || scope === "agent-entry") {
|
if (scope === "agent-workflow-templates") {
|
||||||
const { settings, valueByKey } = await readScopedSiteSettings(AGENT_CANVAS_SETTING_KEYS);
|
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(
|
const canvasFeatureAvailability = await readCanvasFeatureAvailability(
|
||||||
valueByKey
|
valueByKey
|
||||||
);
|
);
|
||||||
@ -831,7 +851,9 @@ async function getScopedPublicSiteSettings(
|
|||||||
pageFeatureSettings: rawUiConfig?.pageFeatureSettings,
|
pageFeatureSettings: rawUiConfig?.pageFeatureSettings,
|
||||||
businessModeSettings: rawUiConfig?.businessModeSettings,
|
businessModeSettings: rawUiConfig?.businessModeSettings,
|
||||||
}
|
}
|
||||||
: rawUiConfig
|
: scope === "agent-canvas-base"
|
||||||
|
? { ...rawUiConfig, steps: [] }
|
||||||
|
: rawUiConfig
|
||||||
),
|
),
|
||||||
agentWorkflowShowcaseConfig: normalizeAgentWorkflowShowcaseConfig(
|
agentWorkflowShowcaseConfig: normalizeAgentWorkflowShowcaseConfig(
|
||||||
valueByKey.get(AGENT_WORKFLOW_SHOWCASE_CONFIG_KEY)
|
valueByKey.get(AGENT_WORKFLOW_SHOWCASE_CONFIG_KEY)
|
||||||
@ -1090,7 +1112,8 @@ export async function GET(request: NextRequest) {
|
|||||||
const cacheKey = brandName.trim()
|
const cacheKey = brandName.trim()
|
||||||
? `${scopedScope}:${brandName.trim().toLowerCase()}`
|
? `${scopedScope}:${brandName.trim().toLowerCase()}`
|
||||||
: scopedScope;
|
: 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)
|
? await getAgentCanvasSettingsEtag(scopedScope)
|
||||||
: undefined;
|
: undefined;
|
||||||
if (etag && request.headers.get("If-None-Match") === etag) {
|
if (etag && request.headers.get("If-None-Match") === etag) {
|
||||||
|
|||||||
@ -233,7 +233,8 @@ import {
|
|||||||
type WorkflowSceneRecord,
|
type WorkflowSceneRecord,
|
||||||
} from "@/lib/video-workflow/runtime";
|
} from "@/lib/video-workflow/runtime";
|
||||||
import { agentProjectService } from "@/services/agent-projects";
|
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 {
|
import {
|
||||||
canvasService,
|
canvasService,
|
||||||
imageGenerationService,
|
imageGenerationService,
|
||||||
@ -273,6 +274,17 @@ import type {
|
|||||||
const CANVAS_SITE_SETTINGS_REFRESH_INTERVAL_MS = 15000;
|
const CANVAS_SITE_SETTINGS_REFRESH_INTERVAL_MS = 15000;
|
||||||
const CANVAS_SURFACE_BACKGROUND = "#f5f5f5";
|
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({
|
function CanvasLazyPanelLoader({
|
||||||
label,
|
label,
|
||||||
className,
|
className,
|
||||||
@ -2176,13 +2188,13 @@ export default function CanvasEditorLegacyPage({
|
|||||||
);
|
);
|
||||||
const canvasSiteSettingsKey = requestedAgentMode
|
const canvasSiteSettingsKey = requestedAgentMode
|
||||||
? hasActiveAgentProject
|
? hasActiveAgentProject
|
||||||
? "/api/site-settings?scope=agent-canvas"
|
? "/api/site-settings?scope=agent-canvas-base"
|
||||||
: "/api/site-settings?scope=agent-entry"
|
: "/api/site-settings?scope=agent-entry"
|
||||||
: "/api/site-settings?scope=canvas";
|
: "/api/site-settings?scope=canvas";
|
||||||
const [hasResolvedCanvasSiteSettings, setHasResolvedCanvasSiteSettings] =
|
const [hasResolvedCanvasSiteSettings, setHasResolvedCanvasSiteSettings] =
|
||||||
useState(false);
|
useState(false);
|
||||||
|
|
||||||
const { data: siteSettingsData } = useSWR<{
|
const { data: siteSettingsData, error: siteSettingsError, isValidating: siteSettingsLoading, mutate: reloadSiteSettings } = useSWR<{
|
||||||
siteName?: string;
|
siteName?: string;
|
||||||
siteLogo?: string | null;
|
siteLogo?: string | null;
|
||||||
siteLogoDark?: string | null;
|
siteLogoDark?: string | null;
|
||||||
@ -2213,6 +2225,7 @@ export default function CanvasEditorLegacyPage({
|
|||||||
? 0
|
? 0
|
||||||
: CANVAS_SITE_SETTINGS_REFRESH_INTERVAL_MS,
|
: CANVAS_SITE_SETTINGS_REFRESH_INTERVAL_MS,
|
||||||
errorRetryCount: 1,
|
errorRetryCount: 1,
|
||||||
|
shouldRetryOnError: !requestedAgentMode || !hasActiveAgentProject,
|
||||||
dedupingInterval: 30_000,
|
dedupingInterval: 30_000,
|
||||||
revalidateOnFocus: true,
|
revalidateOnFocus: true,
|
||||||
revalidateOnReconnect: true,
|
revalidateOnReconnect: true,
|
||||||
@ -2279,9 +2292,33 @@ export default function CanvasEditorLegacyPage({
|
|||||||
!requestedAgentMode ||
|
!requestedAgentMode ||
|
||||||
!agentProjectId ||
|
!agentProjectId ||
|
||||||
loadedAgentProjectId === 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(
|
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(
|
const workflowPageFeatureSettings = useMemo(
|
||||||
() => getAgentWorkflowPageFeatureSettings(resolvedWorkflowUiConfig),
|
() => getAgentWorkflowPageFeatureSettings(resolvedWorkflowUiConfig),
|
||||||
@ -2330,7 +2367,7 @@ export default function CanvasEditorLegacyPage({
|
|||||||
});
|
});
|
||||||
const sanitizedWorkflowSelections = useMemo(
|
const sanitizedWorkflowSelections = useMemo(
|
||||||
() =>
|
() =>
|
||||||
requestedAgentMode && !siteSettingsData?.agentWorkflowUiConfig
|
requestedAgentMode && !workflowTemplates
|
||||||
? workflowSelections
|
? workflowSelections
|
||||||
: sanitizeAgentWorkflowSelections({
|
: sanitizeAgentWorkflowSelections({
|
||||||
config: resolvedWorkflowUiConfig || defaultAgentWorkflowUiConfig,
|
config: resolvedWorkflowUiConfig || defaultAgentWorkflowUiConfig,
|
||||||
@ -2338,7 +2375,7 @@ export default function CanvasEditorLegacyPage({
|
|||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
requestedAgentMode,
|
requestedAgentMode,
|
||||||
siteSettingsData?.agentWorkflowUiConfig,
|
workflowTemplates,
|
||||||
resolvedWorkflowUiConfig,
|
resolvedWorkflowUiConfig,
|
||||||
workflowSelections,
|
workflowSelections,
|
||||||
]
|
]
|
||||||
@ -2346,6 +2383,14 @@ export default function CanvasEditorLegacyPage({
|
|||||||
const deferredSanitizedWorkflowSelections = useDeferredValue(
|
const deferredSanitizedWorkflowSelections = useDeferredValue(
|
||||||
sanitizedWorkflowSelections
|
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(
|
const resolvedBrandModelConfig = useMemo(
|
||||||
() =>
|
() =>
|
||||||
normalizeAgentWorkflowBrandModelConfig(
|
normalizeAgentWorkflowBrandModelConfig(
|
||||||
@ -9921,6 +9966,8 @@ export default function CanvasEditorLegacyPage({
|
|||||||
selectedBrandModel,
|
selectedBrandModel,
|
||||||
appearanceRecognitionEnabled,
|
appearanceRecognitionEnabled,
|
||||||
selections: deferredSanitizedWorkflowSelections,
|
selections: deferredSanitizedWorkflowSelections,
|
||||||
|
resolvedSelections: workflowPromptData?.selectionRequest === getWorkflowSelectionsRequest(deferredSanitizedWorkflowSelections)
|
||||||
|
? workflowPromptData?.selections : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (canceled) {
|
if (canceled) {
|
||||||
@ -9943,6 +9990,7 @@ export default function CanvasEditorLegacyPage({
|
|||||||
selectedBrandModel,
|
selectedBrandModel,
|
||||||
workflowInputs,
|
workflowInputs,
|
||||||
workflowMaterials,
|
workflowMaterials,
|
||||||
|
workflowPromptData,
|
||||||
]);
|
]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
workflowInputsRef.current = workflowInputs;
|
workflowInputsRef.current = workflowInputs;
|
||||||
@ -10064,15 +10112,23 @@ export default function CanvasEditorLegacyPage({
|
|||||||
[patchPersistedAgentWorkflowState]
|
[patchPersistedAgentWorkflowState]
|
||||||
);
|
);
|
||||||
const getLatestWorkflowContext = useCallback(() => {
|
const getLatestWorkflowContext = useCallback(() => {
|
||||||
|
const selections = workflowSelectionsRef.current;
|
||||||
return buildAgentWorkflowComposerContext({
|
return buildAgentWorkflowComposerContext({
|
||||||
config: resolvedWorkflowUiConfig || defaultAgentWorkflowUiConfig,
|
config: resolvedWorkflowUiConfig || defaultAgentWorkflowUiConfig,
|
||||||
inputs: workflowInputsRef.current,
|
inputs: workflowInputsRef.current,
|
||||||
materials: workflowMaterialsRef.current,
|
materials: workflowMaterialsRef.current,
|
||||||
selectedBrandModel: selectedBrandModelRef.current,
|
selectedBrandModel: selectedBrandModelRef.current,
|
||||||
appearanceRecognitionEnabled: appearanceRecognitionEnabledRef.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 effectiveSelectedAgentPresetId = null;
|
||||||
const { data: selectedAgentPresetResponse } = useSWR<{
|
const { data: selectedAgentPresetResponse } = useSWR<{
|
||||||
success?: boolean;
|
success?: boolean;
|
||||||
@ -10447,30 +10503,37 @@ export default function CanvasEditorLegacyPage({
|
|||||||
</div>
|
</div>
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
const workflowStylePanel = useMemo(
|
const workflowStylePanel = useMemo(() => (
|
||||||
() => (
|
<div ref={setWorkflowTemplateMarker} className="space-y-3">
|
||||||
<AgentWorkflowPanelDarkV2
|
{workflowTemplates ? (
|
||||||
config={resolvedWorkflowUiConfig}
|
<AgentWorkflowPanelDarkV2
|
||||||
embedded
|
config={resolvedWorkflowUiConfig}
|
||||||
inputs={workflowInputs}
|
embedded
|
||||||
materials={workflowMaterials}
|
inputs={workflowInputs}
|
||||||
onInputsChange={handleWorkflowInputsChange}
|
materials={workflowMaterials}
|
||||||
onMaterialsChange={handleWorkflowMaterialsChange}
|
onInputsChange={handleWorkflowInputsChange}
|
||||||
onSelectionsChange={handleWorkflowSelectionsChange}
|
onMaterialsChange={handleWorkflowMaterialsChange}
|
||||||
selections={sanitizedWorkflowSelections}
|
onSelectionsChange={handleWorkflowSelectionsChange}
|
||||||
showBrandStoreSection={false}
|
selections={sanitizedWorkflowSelections}
|
||||||
/>
|
showBrandStoreSection={false}
|
||||||
),
|
/>
|
||||||
[
|
) : siteSettingsError && !siteSettingsData && !siteSettingsLoading ? (
|
||||||
handleWorkflowInputsChange,
|
<p className="text-sm text-white/65">请先在上方重试加载品牌和类目。</p>
|
||||||
handleWorkflowMaterialsChange,
|
) : !workflowTemplatesRequested ? (
|
||||||
handleWorkflowSelectionsChange,
|
<Button type="button" variant="outline" onClick={() => setWorkflowTemplatesRequested(true)}>加载风格模板</Button>
|
||||||
resolvedWorkflowUiConfig,
|
) : (
|
||||||
sanitizedWorkflowSelections,
|
<WorkflowLoadState name="风格模板" error={workflowTemplatesError} retrying={workflowTemplatesLoading} onRetry={() => void reloadWorkflowTemplates().catch(() => {})} />
|
||||||
workflowInputs,
|
)}
|
||||||
workflowMaterials,
|
{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) {
|
if (canvasLoadError && !isAgentMode) {
|
||||||
return <div role="alert" className="flex h-dvh flex-col items-center justify-center gap-4">
|
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"
|
theme="dark"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
brandStorePanel={
|
brandStorePanel={siteSettingsData?.agentWorkflowUiConfig ? (
|
||||||
<AgentWorkflowBrandStorePanel
|
<AgentWorkflowBrandStorePanel
|
||||||
config={resolvedWorkflowUiConfig}
|
config={resolvedWorkflowUiConfig}
|
||||||
inputs={workflowInputs}
|
inputs={workflowInputs}
|
||||||
onInputsChange={handleWorkflowInputsChange}
|
onInputsChange={handleWorkflowInputsChange}
|
||||||
theme="dark"
|
theme="dark"
|
||||||
/>
|
/>
|
||||||
}
|
) : (
|
||||||
|
<WorkflowLoadState name="品牌和类目" error={siteSettingsError} retrying={siteSettingsLoading} onRetry={() => void reloadSiteSettings().catch(() => {})} />
|
||||||
|
)}
|
||||||
chatId={agentModeChatId}
|
chatId={agentModeChatId}
|
||||||
deletedResultCollections={agentProjectDeletedResultCollections}
|
deletedResultCollections={agentProjectDeletedResultCollections}
|
||||||
getLatestWorkflowContext={getLatestWorkflowContext}
|
getLatestWorkflowContext={getLatestWorkflowContext}
|
||||||
|
getWorkflowPromptStatus={getWorkflowPromptStatus}
|
||||||
materialSelectionPanel={
|
materialSelectionPanel={
|
||||||
isWorkflowMaterialStepEnabled ? (
|
isWorkflowMaterialStepEnabled ? (
|
||||||
<AgentWorkflowMaterialSelectionPanel
|
<AgentWorkflowMaterialSelectionPanel
|
||||||
|
|||||||
@ -176,6 +176,7 @@ export interface AgentGenerationWorkspaceDarkV2Props {
|
|||||||
workflowFeatureSettings?: AgentWorkflowPageFeatureSettings | null;
|
workflowFeatureSettings?: AgentWorkflowPageFeatureSettings | null;
|
||||||
workflowPromptOptimizationEnabled?: boolean;
|
workflowPromptOptimizationEnabled?: boolean;
|
||||||
getLatestWorkflowContext?: () => AgentWorkflowComposerContext | null;
|
getLatestWorkflowContext?: () => AgentWorkflowComposerContext | null;
|
||||||
|
getWorkflowPromptStatus?: () => "ready" | "loading" | "error";
|
||||||
onBack?: () => void;
|
onBack?: () => void;
|
||||||
brandStorePanel?: ReactNode;
|
brandStorePanel?: ReactNode;
|
||||||
materialSelectionPanel?: ReactNode;
|
materialSelectionPanel?: ReactNode;
|
||||||
@ -3085,6 +3086,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
|||||||
workflowFeatureSettings = null,
|
workflowFeatureSettings = null,
|
||||||
workflowPromptOptimizationEnabled = true,
|
workflowPromptOptimizationEnabled = true,
|
||||||
getLatestWorkflowContext,
|
getLatestWorkflowContext,
|
||||||
|
getWorkflowPromptStatus,
|
||||||
onBack,
|
onBack,
|
||||||
brandStorePanel,
|
brandStorePanel,
|
||||||
materialSelectionPanel,
|
materialSelectionPanel,
|
||||||
@ -6318,6 +6320,11 @@ export function AgentGenerationWorkspaceDarkV2({
|
|||||||
toast.error(`请先查询并选择${skuReferenceSourceLabel}参考图`);
|
toast.error(`请先查询并选择${skuReferenceSourceLabel}参考图`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const promptStatus = getWorkflowPromptStatus?.();
|
||||||
|
if (promptStatus === "loading" || promptStatus === "error") {
|
||||||
|
toast.error(promptStatus === "error" ? "请先在风格模板区重试加载提示词" : "正在加载所选模板的提示词,请稍候");
|
||||||
|
return;
|
||||||
|
}
|
||||||
const resolvedWorkflowContext = getLatestWorkflowContext?.() ?? workflowContext;
|
const resolvedWorkflowContext = getLatestWorkflowContext?.() ?? workflowContext;
|
||||||
if (
|
if (
|
||||||
isDetailReplacementPreset &&
|
isDetailReplacementPreset &&
|
||||||
@ -6629,6 +6636,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
|||||||
skuResultCards,
|
skuResultCards,
|
||||||
userMessageCount,
|
userMessageCount,
|
||||||
getLatestWorkflowContext,
|
getLatestWorkflowContext,
|
||||||
|
getWorkflowPromptStatus,
|
||||||
isSceneGenerationPreset,
|
isSceneGenerationPreset,
|
||||||
workflowContext,
|
workflowContext,
|
||||||
sceneMessageAttachments,
|
sceneMessageAttachments,
|
||||||
@ -7802,6 +7810,11 @@ export function AgentGenerationWorkspaceDarkV2({
|
|||||||
toast.error(detailGenerationValidationIssues[0] || "请先补齐 AI详情生成参数");
|
toast.error(detailGenerationValidationIssues[0] || "请先补齐 AI详情生成参数");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const promptStatus = getWorkflowPromptStatus?.();
|
||||||
|
if (promptStatus === "loading" || promptStatus === "error") {
|
||||||
|
toast.error(promptStatus === "error" ? "请先在风格模板区重试加载提示词" : "正在加载所选模板的提示词,请稍候");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const productAttachments = detailGenerationProductSlotIndexes.map((index) =>
|
const productAttachments = detailGenerationProductSlotIndexes.map((index) =>
|
||||||
detailGenerationSlots[index]
|
detailGenerationSlots[index]
|
||||||
|
|||||||
@ -407,14 +407,10 @@ function buildPromptSection(title: string, lines: string[]) {
|
|||||||
return `【${title}】\n${normalizedLines.join("\n")}`;
|
return `【${title}】\n${normalizedLines.join("\n")}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildAgentWorkflowComposerContext(params: {
|
export function resolveAgentWorkflowComposerSelections(params: {
|
||||||
config?: AgentWorkflowUiConfig | null;
|
config?: AgentWorkflowUiConfig | null;
|
||||||
inputs: AgentWorkflowInputsValue;
|
|
||||||
materials?: AgentWorkflowMaterialSelections | null;
|
|
||||||
selectedBrandModel?: AgentWorkflowSelectedBrandModel | null;
|
|
||||||
appearanceRecognitionEnabled?: boolean;
|
|
||||||
selections: Partial<Record<AgentWorkflowStepKey, string>>;
|
selections: Partial<Record<AgentWorkflowStepKey, string>>;
|
||||||
}): AgentWorkflowComposerContext | null {
|
}): AgentWorkflowComposerContext["selections"] {
|
||||||
const config = params.config || defaultAgentWorkflowUiConfig;
|
const config = params.config || defaultAgentWorkflowUiConfig;
|
||||||
const steps = flattenVisibleAgentWorkflowSteps(
|
const steps = flattenVisibleAgentWorkflowSteps(
|
||||||
getEnabledAgentWorkflowStepTree(Array.isArray(config.steps) ? config.steps : []),
|
getEnabledAgentWorkflowStepTree(Array.isArray(config.steps) ? config.steps : []),
|
||||||
@ -422,22 +418,31 @@ export function buildAgentWorkflowComposerContext(params: {
|
|||||||
)
|
)
|
||||||
.map(({ step }) => step)
|
.map(({ step }) => step)
|
||||||
.filter((step) => Array.isArray(step.options) && step.options.length > 0);
|
.filter((step) => Array.isArray(step.options) && step.options.length > 0);
|
||||||
const selections = steps
|
return steps.flatMap((step) => {
|
||||||
.map((step) => {
|
const selectedOptions = getAgentWorkflowSelectedOptionPath({
|
||||||
const selectedOptions = getAgentWorkflowSelectedOptionPath({
|
step,
|
||||||
step,
|
selectionValue: params.selections[step.key],
|
||||||
selectionValue: params.selections[step.key],
|
});
|
||||||
});
|
return selectedOptions.map((option) => ({
|
||||||
if (selectedOptions.length === 0) return [];
|
stepKey: step.key,
|
||||||
return selectedOptions.map((option) => ({
|
stepTitle: step.title,
|
||||||
stepKey: step.key,
|
optionKey: option.key,
|
||||||
stepTitle: step.title,
|
optionName: option.buttonName,
|
||||||
optionKey: option.key,
|
promptFragment: option.promptFragment || option.buttonName,
|
||||||
optionName: option.buttonName,
|
}));
|
||||||
promptFragment: option.promptFragment || option.buttonName,
|
});
|
||||||
}));
|
}
|
||||||
})
|
|
||||||
.flat() as AgentWorkflowComposerContext["selections"];
|
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({
|
const referenceImages = getSelectedWorkflowReferenceImages({
|
||||||
inputs: params.inputs,
|
inputs: params.inputs,
|
||||||
materials: params.materials,
|
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",
|
"id-photo",
|
||||||
"image-expansion",
|
"image-expansion",
|
||||||
"image-upscaler",
|
"image-upscaler",
|
||||||
|
"ocr",
|
||||||
"photo-cleanup",
|
"photo-cleanup",
|
||||||
"remove-background",
|
"remove-background",
|
||||||
"remove-object",
|
"remove-object",
|
||||||
@ -1041,6 +1042,7 @@ function createToolPreset(
|
|||||||
const inputModes = getInputModes(model);
|
const inputModes = getInputModes(model);
|
||||||
const outputModes = getOutputModes(model);
|
const outputModes = getOutputModes(model);
|
||||||
const isImageTool = type === "image_tool";
|
const isImageTool = type === "image_tool";
|
||||||
|
const isOcr = modelId.toLowerCase() === "ocr";
|
||||||
const toolOperation = IMAGE_TOOL_OPERATION_BY_MODEL_ID[modelId.toLowerCase()] || type;
|
const toolOperation = IMAGE_TOOL_OPERATION_BY_MODEL_ID[modelId.toLowerCase()] || type;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -1056,7 +1058,7 @@ function createToolPreset(
|
|||||||
kind: type.replace(/_/g, "-"),
|
kind: type.replace(/_/g, "-"),
|
||||||
mediaType: isImageTool ? "image" : type === "audio" ? "audio" : "document",
|
mediaType: isImageTool ? "image" : type === "audio" ? "audio" : "document",
|
||||||
modelType: type,
|
modelType: type,
|
||||||
outputType: outputModes[0] || (isImageTool ? "image" : "text"),
|
outputType: isOcr ? "text" : outputModes[0] || (isImageTool ? "image" : "text"),
|
||||||
inputModes,
|
inputModes,
|
||||||
outputModes,
|
outputModes,
|
||||||
supportedModes: getCapabilities(model),
|
supportedModes: getCapabilities(model),
|
||||||
|
|||||||
@ -1,11 +1,27 @@
|
|||||||
|
import type { AgentWorkflowComposerContext } from "@/types/agent";
|
||||||
|
|
||||||
const settingsCache = new Map<string, { etag: string; data: unknown }>();
|
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) {
|
export async function fetchCanvasSiteSettings(url: string) {
|
||||||
const cached = settingsCache.get(url);
|
const cached = settingsCache.get(url);
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timeoutId = setTimeout(
|
const timeoutId = setTimeout(
|
||||||
() => controller.abort(),
|
() => 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 {
|
try {
|
||||||
const response = await fetch(url, {
|
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();
|
const entryData = await entry.json();
|
||||||
expect(entryData.agentWorkflowUiConfig.steps).toEqual([]);
|
expect(entryData.agentWorkflowUiConfig.steps).toEqual([]);
|
||||||
expect(JSON.stringify(entryData).length).toBeLessThan(20_000);
|
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"));
|
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));
|
expect((await full.json()).agentWorkflowUiConfig.steps[0].options[0].promptFragment).toBe("生成提示词".repeat(3000).slice(0, 20_000));
|
||||||
const etag = full.headers.get("ETag");
|
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.status).toBe(200);
|
||||||
expect(changed.headers.get("ETag")).not.toBe(etag);
|
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"],
|
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.describe("AI model media classification", () => {
|
||||||
test("classifies Qwen Image 3.0 as Alibaba text-to-image and image editing", () => {
|
test("classifies Qwen Image 3.0 as Alibaba text-to-image and image editing", () => {
|
||||||
expect(isAlibabaImageTextToImageModelId("qwen-image-3.0")).toBe(true);
|
expect(isAlibabaImageTextToImageModelId("qwen-image-3.0")).toBe(true);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user