feat: add agent ai detail workflow mode
This commit is contained in:
parent
83102c559f
commit
900a1c9e38
@ -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
191
lib/agents/detail-generation-config.ts
Normal file
191
lib/agents/detail-generation-config.ts
Normal 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();
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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[];
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user