feat: add agent ai detail workflow mode
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-03-24 21:30:11 +08:00
parent 83102c559f
commit 900a1c9e38
6 changed files with 1493 additions and 53 deletions

View File

@ -233,6 +233,10 @@ function areResultCollectionsEqual(
normalizeResultCollectionUrls(left.scene_generation),
normalizeResultCollectionUrls(right.scene_generation)
) &&
areStringArraysEqual(
normalizeResultCollectionUrls(left.detail_generation),
normalizeResultCollectionUrls(right.detail_generation)
) &&
areStringArraysEqual(
normalizeResultCollectionUrls(left.detail_replacement),
normalizeResultCollectionUrls(right.detail_replacement)
@ -249,6 +253,7 @@ function getFirstResultUrlFromCollections(
) {
return (
normalizeResultCollectionUrls(collections.scene_generation)[0] ||
normalizeResultCollectionUrls(collections.detail_generation)[0] ||
normalizeResultCollectionUrls(collections.detail_replacement)[0] ||
normalizeResultCollectionUrls(collections.sku_replacement)[0] ||
null
@ -654,6 +659,9 @@ export default function CanvasEditorPage({ params }: PageProps) {
scene_generation: normalizeResultCollectionUrls(
agentProjectResultCollections.scene_generation
),
detail_generation: normalizeResultCollectionUrls(
agentProjectResultCollections.detail_generation
),
detail_replacement: normalizeResultCollectionUrls(
agentProjectResultCollections.detail_replacement
),
@ -665,6 +673,9 @@ export default function CanvasEditorPage({ params }: PageProps) {
scene_generation: normalizeResultCollectionUrls(
agentProjectDeletedResultCollections.scene_generation
),
detail_generation: normalizeResultCollectionUrls(
agentProjectDeletedResultCollections.detail_generation
),
detail_replacement: normalizeResultCollectionUrls(
agentProjectDeletedResultCollections.detail_replacement
),
@ -675,6 +686,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
resultImageUrls: Array.from(
new Set([
...normalizeResultCollectionUrls(agentProjectResultCollections.scene_generation),
...normalizeResultCollectionUrls(agentProjectResultCollections.detail_generation),
...normalizeResultCollectionUrls(agentProjectResultCollections.detail_replacement),
...normalizeResultCollectionUrls(agentProjectResultCollections.sku_replacement),
])

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,191 @@
import type {
AgentDetailBriefForm,
AgentDetailGenerationMode,
AgentDetailGenerationSlot,
AgentDetailGenerationSlotRole,
} from "@/types/agent";
export const AGENT_DETAIL_TARGET_WIDTH = 790;
export const AGENT_DETAIL_TARGET_HEIGHT = 1300;
export const AGENT_DETAIL_SLOT_CONFIGS: Array<{
key: string;
label: string;
role: AgentDetailGenerationSlotRole;
}> = [
{ key: "product-1", label: "产品图1", role: "product" },
{ key: "product-2", label: "产品图2", role: "product" },
{ key: "product-3", label: "产品图3", role: "product" },
{ key: "product-4", label: "产品图4", role: "product" },
{ key: "product-5", label: "产品图5", role: "product" },
{ key: "model-1", label: "模特图", role: "model" },
{ key: "bundle-1", label: "搭配图1", role: "bundle" },
{ key: "bundle-2", label: "搭配图2", role: "bundle" },
{ key: "bundle-3", label: "搭配图3", role: "bundle" },
];
export const AGENT_DETAIL_PRODUCT_SLOT_INDEXES = AGENT_DETAIL_SLOT_CONFIGS.map((slot, index) =>
slot.role === "product" ? index : -1
).filter((index) => index >= 0);
export const AGENT_DETAIL_MODEL_SLOT_INDEX = AGENT_DETAIL_SLOT_CONFIGS.findIndex(
(slot) => slot.role === "model"
);
export const AGENT_DETAIL_BUNDLE_SLOT_INDEXES = AGENT_DETAIL_SLOT_CONFIGS.map((slot, index) =>
slot.role === "bundle" ? index : -1
).filter((index) => index >= 0);
export const AGENT_DETAIL_FIELD_EXAMPLES: Record<keyof AgentDetailBriefForm, string> = {
productSpu: "示例:SPU-889901",
demandType: "示例:清洁除尘 / 户外通勤 / 礼赠",
productName: "示例:Buyi-AI 无线手持吸尘器",
targetAudience: "示例:25-40岁养宠家庭、宝妈",
mainUser: "示例:家庭主理人、租房上班族",
usageScenario: "示例:下班回家10分钟快速清洁",
triggerMoment: "示例:宠物掉毛高峰、饭后碎屑清理",
coreNeed: "示例:省时省力清除毛发与灰尘",
riskConcern: "示例:担心吸力不足、续航短、噪音大",
expectedResult: "示例:3分钟清洁桌面,地面无明显毛发",
coreSellingPoint: "示例:18000Pa吸力、0.9kg轻量、一键倒尘",
positioningStatement: "示例:让日常清洁更快更轻松",
userVoice: "示例:家里有猫掉毛太多,希望安静一点不吵宝宝",
competitorReference: "示例:对标戴森入门款,突出轻便高性价比",
priceBand: "示例:199-299元",
brandTone: "示例:专业、可靠、亲和",
bannedWords: "示例:最强、第一、100%治愈",
};
export const AGENT_DETAIL_GENERATION_MODE_OPTIONS: Array<{
value: AgentDetailGenerationMode;
label: string;
description: string;
}> = [
{
value: "classic",
label: "经典详情",
description: "整页直接生图,适合当前智能体页快速生成。",
},
{
value: "agent_layout",
label: "AGENT排版",
description: "保留给后续共享画布排版层能力后开放。",
},
];
export function createEmptyAgentDetailBriefForm(): AgentDetailBriefForm {
return {
productSpu: "",
demandType: "",
productName: "",
targetAudience: "",
mainUser: "",
usageScenario: "",
triggerMoment: "",
coreNeed: "",
riskConcern: "",
expectedResult: "",
coreSellingPoint: "",
positioningStatement: "",
userVoice: "",
competitorReference: "",
priceBand: "",
brandTone: "",
bannedWords: "",
};
}
export function createEmptyAgentDetailSlots(): Array<AgentDetailGenerationSlot | null> {
return AGENT_DETAIL_SLOT_CONFIGS.map(() => null);
}
export function normalizeAgentDetailGenerationMode(
value: unknown
): AgentDetailGenerationMode {
return value === "agent_layout" ? "agent_layout" : "classic";
}
export function sanitizeAgentDetailBrief(input: unknown): AgentDetailBriefForm {
const raw =
input && typeof input === "object" && !Array.isArray(input)
? (input as Record<string, unknown>)
: {};
const normalize = (key: keyof AgentDetailBriefForm) =>
String(raw[key] || "")
.replace(/\s+/g, " ")
.trim();
return {
productSpu: normalize("productSpu"),
demandType: normalize("demandType"),
productName: normalize("productName"),
targetAudience: normalize("targetAudience"),
mainUser: normalize("mainUser"),
usageScenario: normalize("usageScenario"),
triggerMoment: normalize("triggerMoment"),
coreNeed: normalize("coreNeed"),
riskConcern: normalize("riskConcern"),
expectedResult: normalize("expectedResult"),
coreSellingPoint: normalize("coreSellingPoint"),
positioningStatement: normalize("positioningStatement"),
userVoice: normalize("userVoice"),
competitorReference: normalize("competitorReference"),
priceBand: normalize("priceBand"),
brandTone: normalize("brandTone"),
bannedWords: normalize("bannedWords"),
};
}
export function extractAgentDetailFrameCount(text: string) {
const normalized = String(text || "").replace(/\s+/g, "");
const matched = normalized.match(/(\d{1,2})(?:张|屏|页)/);
if (!matched) return 14;
const value = Number(matched[1]);
if (!Number.isFinite(value)) return 14;
return Math.min(20, Math.max(5, Math.round(value)));
}
export function sanitizeDetailImageModelPrompt(prompt: string): string {
const filteredLines = String(prompt || "")
.split(/\n+/)
.map((line) => line.trim())
.filter(Boolean)
.map((line) => {
if (
/^(字体规范|标题规范|副标题规范|正文规范|版式规范|安全区|留白节奏|Typography|Layout|Palette|固定字体锁定|固定副标题锁定|固定正文锁定|固定排版安全区|固定留白节奏)/i.test(
line
)
) {
return "";
}
return line;
})
.filter(Boolean);
let result = filteredLines.join("\n");
const literalReplacements: Array<[RegExp, string]> = [
[/画布固定(?:竖版)?\s*\d+\s*x\s*\d+\s*(?:像素|pixels?)。?/gi, "画布为竖版详情页构图。"],
[/尺寸固定\s*\d+\s*x\s*\d+\s*(?:像素|pixels?)。?/gi, "尺寸为竖版详情页比例。"],
[/输出为竖版电商详情图,尺寸固定\s*\d+\s*x\s*\d+\s*(?:像素|pixels?)。?/gi, "输出为竖版电商详情图。"],
[/y\s*=\s*\d+\s*[-~~]\s*\d+/gi, "顶部标题安全区"],
[/左右边距\s*\d+\s*px/gi, "左右留白充足"],
[/模块间距\s*\d+\s*px/gi, "模块间距均匀"],
[/标题和副标题间距\s*\d+\s*px/gi, "标题与副标题间距清晰"],
[/主标题\s*\d+\s*px[^,。;\n]*/gi, "主标题醒目突出"],
[/副标题\s*\d+\s*px[^,。;\n]*/gi, "副标题层级清晰"],
[/正文\s*\d+\s*px[^,。;\n]*/gi, "正文精简易读"],
];
for (const [pattern, replacement] of literalReplacements) {
result = result.replace(pattern, replacement);
}
return result
.replace(/\b\d+\s*px\b/gi, "")
.replace(/[((]\s*顶部标题安全区\s*[))]/g, "顶部标题安全区")
.replace(/;{2,}/g, ";")
.replace(/。{2,}/g, "。")
.replace(/\s{2,}/g, " ")
.replace(/\n{3,}/g, "\n\n")
.trim();
}

View File

@ -74,6 +74,7 @@ function normalizeResultCollections(value: unknown): AgentGenerationProjectResul
return {
scene_generation: normalizeUrlList(raw.scene_generation),
detail_generation: normalizeUrlList(raw.detail_generation),
detail_replacement: normalizeUrlList(raw.detail_replacement),
sku_replacement: normalizeUrlList(raw.sku_replacement),
};
@ -110,10 +111,12 @@ function normalizeMigratedResultCollections(params: {
resultCollections: AgentGenerationProjectResultCollections;
}) {
const normalizedSceneUrls = params.resultCollections.scene_generation || [];
const normalizedDetailGenerationUrls = params.resultCollections.detail_generation || [];
const normalizedDetailUrls = params.resultCollections.detail_replacement || [];
const normalizedSkuUrls = params.resultCollections.sku_replacement || [];
const hasExplicitBuckets =
normalizedSceneUrls.length > 0 ||
normalizedDetailGenerationUrls.length > 0 ||
normalizedDetailUrls.length > 0 ||
normalizedSkuUrls.length > 0;
@ -148,6 +151,7 @@ function normalizeMigratedResultCollections(params: {
) {
return {
scene_generation: [],
detail_generation: [],
detail_replacement: normalizedSceneUrls,
sku_replacement: [],
} satisfies AgentGenerationProjectResultCollections;
@ -159,6 +163,7 @@ function normalizeMigratedResultCollections(params: {
) {
return {
scene_generation: [],
detail_generation: [],
detail_replacement: [],
sku_replacement: normalizedSceneUrls,
} satisfies AgentGenerationProjectResultCollections;

View File

@ -4,15 +4,33 @@ import type {
AgentModeWorkspaceState,
AgentWorkflowMaterialQueryType,
} from "@/types/agent";
import {
createEmptyAgentDetailBriefForm,
createEmptyAgentDetailSlots,
normalizeAgentDetailGenerationMode,
sanitizeAgentDetailBrief,
} from "@/lib/agents/detail-generation-config";
export function createEmptyAgentModeWorkspaceState(): AgentModeWorkspaceState {
return {
detailReplacement: {
method: "reference_replace",
query: "",
queryType: "spu",
candidates: [],
selectedCandidateId: null,
references: [],
generationMode: "classic",
brief: createEmptyAgentDetailBriefForm(),
slots: createEmptyAgentDetailSlots(),
progress: {
status: "idle",
stage: "",
current: 0,
total: 0,
startedAt: null,
finishedAt: null,
},
},
skuReplacement: {
query: "",
@ -55,6 +73,47 @@ function normalizeQueryType(value: unknown, fallback: AgentWorkflowMaterialQuery
return value === "sku" || value === "spu" ? value : fallback;
}
function normalizeGenerationSlots(value: unknown) {
if (!Array.isArray(value)) return createEmptyAgentDetailSlots();
const base = createEmptyAgentDetailSlots();
return base.map((_, index) => {
const item = value[index];
if (!item || typeof item !== "object" || Array.isArray(item)) {
return null;
}
const raw = item as Record<string, unknown>;
const url = String(raw.url || "").trim();
if (!url) return null;
return {
url,
name: String(raw.name || "").trim() || null,
contentType: String(raw.contentType || "").trim() || null,
assetId: String(raw.assetId || "").trim() || null,
};
});
}
function normalizeGenerationProgress(value: unknown) {
const raw =
value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: {};
const status =
raw.status === "running" ||
raw.status === "done" ||
raw.status === "error"
? raw.status
: "idle";
return {
status,
stage: String(raw.stage || "").trim(),
current: Number.isFinite(Number(raw.current)) ? Math.max(0, Math.round(Number(raw.current))) : 0,
total: Number.isFinite(Number(raw.total)) ? Math.max(0, Math.round(Number(raw.total))) : 0,
startedAt: String(raw.startedAt || "").trim() || null,
finishedAt: String(raw.finishedAt || "").trim() || null,
} as const;
}
function normalizeCandidate(value: unknown, index: number): AgentModeReferenceCandidate | null {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return null;
@ -114,11 +173,16 @@ export function normalizeAgentModeWorkspaceState(value: unknown): AgentModeWorks
return {
detailReplacement: {
method: detailRaw.method === "ai_detail_generate" ? "ai_detail_generate" : "reference_replace",
query: String(detailRaw.query || detailRaw.spuQuery || "").trim(),
queryType: normalizeQueryType(detailRaw.queryType, "spu"),
candidates: normalizeCandidates(detailRaw.candidates),
selectedCandidateId: String(detailRaw.selectedCandidateId || "").trim() || null,
references: normalizeReferenceItems(detailRaw.references),
generationMode: normalizeAgentDetailGenerationMode(detailRaw.generationMode),
brief: sanitizeAgentDetailBrief(detailRaw.brief),
slots: normalizeGenerationSlots(detailRaw.slots),
progress: normalizeGenerationProgress(detailRaw.progress),
},
skuReplacement: {
query: String(skuRaw.query || skuRaw.spuQuery || "").trim(),

View File

@ -9,6 +9,9 @@ export type AgentPresetModeType =
| "sku_replacement";
export type AgentTaskDetailReferenceSource = "main" | "sku" | "detail";
export type AgentDetailMethod = "reference_replace" | "ai_detail_generate";
export type AgentDetailGenerationMode = "classic" | "agent_layout";
export type AgentDetailGenerationSlotRole = "product" | "model" | "bundle";
export interface AgentSceneGenerationModeConfig {
productImageSlotCount: number;
@ -251,6 +254,42 @@ export interface AgentWorkflowComposerContext {
promptText: string;
}
export interface AgentDetailBriefForm {
productSpu: string;
demandType: string;
productName: string;
targetAudience: string;
mainUser: string;
usageScenario: string;
triggerMoment: string;
coreNeed: string;
riskConcern: string;
expectedResult: string;
coreSellingPoint: string;
positioningStatement: string;
userVoice: string;
competitorReference: string;
priceBand: string;
brandTone: string;
bannedWords: string;
}
export interface AgentDetailGenerationSlot {
url: string;
name?: string | null;
contentType?: string | null;
assetId?: string | null;
}
export interface AgentDetailGenerationProgressState {
status: "idle" | "running" | "done" | "error";
stage: string;
current: number;
total: number;
startedAt?: string | null;
finishedAt?: string | null;
}
export interface AgentDetailReplacementReferenceItem {
id: string;
label: string;
@ -277,11 +316,16 @@ export interface AgentModeReferenceCandidate {
}
export interface AgentDetailReplacementState {
method: AgentDetailMethod;
query: string;
queryType: AgentWorkflowMaterialQueryType;
candidates: AgentModeReferenceCandidate[];
selectedCandidateId?: string | null;
references: AgentDetailReplacementReferenceItem[];
generationMode?: AgentDetailGenerationMode;
brief?: AgentDetailBriefForm;
slots?: Array<AgentDetailGenerationSlot | null>;
progress?: AgentDetailGenerationProgressState | null;
}
export interface AgentSkuReplacementState {
@ -312,11 +356,13 @@ export interface AgentGenerationProjectImageParams {
export type AgentGenerationResultModeType =
| "scene_generation"
| "detail_generation"
| "detail_replacement"
| "sku_replacement";
export interface AgentGenerationProjectResultCollections {
scene_generation?: string[];
detail_generation?: string[];
detail_replacement?: string[];
sku_replacement?: string[];
}