ai-chatbot/lib/agents/generation-project-state.ts

651 lines
23 KiB
TypeScript

import {
createEmptyAgentModeWorkspaceState,
normalizeAgentModeWorkspaceState,
} from "@/lib/agents/mode-workspace-state";
import {
createEmptyAgentWorkflowMaterialSelections,
normalizeAgentWorkflowMaterialSelections,
} from "@/lib/agents/workflow-material-config";
import type {
AgentGenerationProjectPersistedState,
AgentGenerationProjectReplacementResultState,
AgentGenerationProjectResultCollections,
AgentGenerationResultModeType,
AgentGenerationReplacementResultStatus,
AgentGenerationProjectWorkspaceState,
AgentDetailReplacementReferenceItem,
AgentWorkflowBusinessModeKey,
AgentWorkflowPersistedState,
} from "@/types/agent";
export function createEmptyAgentGenerationProjectWorkspaceState(): AgentGenerationProjectWorkspaceState {
return {
input: "",
attachments: [],
selectedModelId: "auto",
activeBusinessModeKey: "main_sku",
imageParams: {
aspectRatio: "auto",
imageSize: "2K",
imageCount: 1,
},
};
}
export function createEmptyAgentWorkflowPersistedState(): AgentWorkflowPersistedState {
return {
inputs: {
primarySku: "",
secondarySku: "",
brand: "",
store: "",
officialSecondCategory: "",
officialThirdCategory: "",
thirdCategory: "",
fourthCategory: "",
fifthCategory: "",
priceBand: "",
},
materials: createEmptyAgentWorkflowMaterialSelections(),
appearanceRecognitionEnabled: false,
selections: {},
selectedBrandModel: null,
selectedAgentPresetId: null,
currentStage: "workflow",
agentModeState: createEmptyAgentModeWorkspaceState(),
};
}
export function createEmptyAgentGenerationProjectPersistedState(): AgentGenerationProjectPersistedState {
return {
workflowState: createEmptyAgentWorkflowPersistedState(),
workspaceState: createEmptyAgentGenerationProjectWorkspaceState(),
resultImageUrls: [],
resultCollections: {},
deletedResultCollections: {},
replacementResultState: {},
};
}
function normalizeResultCollections(value: unknown): AgentGenerationProjectResultCollections {
const raw =
value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: {};
const normalizeUrlList = (input: unknown) =>
Array.isArray(input)
? Array.from(
new Set(
input
.map((item) => String(item || "").trim())
.filter((item) => /^https?:\/\//i.test(item))
)
).slice(0, 128)
: [];
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),
};
}
const REPLACEMENT_RESULT_STATUSES = new Set<AgentGenerationReplacementResultStatus>([
"completed",
"passthrough",
"fallback",
]);
function normalizeOptionalHttpUrl(value: unknown) {
const raw = String(value || "").trim();
return /^https?:\/\//i.test(raw) ? raw : null;
}
function normalizeReplacementResultStatus(value: unknown): AgentGenerationReplacementResultStatus {
return REPLACEMENT_RESULT_STATUSES.has(value as AgentGenerationReplacementResultStatus)
? (value as AgentGenerationReplacementResultStatus)
: "completed";
}
export function normalizeAgentGenerationProjectReplacementResultState(
value: unknown
): AgentGenerationProjectReplacementResultState {
const raw =
value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: {};
const normalizeModeState = (input: unknown) => {
const records =
input && typeof input === "object" && !Array.isArray(input)
? (input as Record<string, unknown>)
: {};
const entries: Array<
readonly [
string,
NonNullable<AgentGenerationProjectReplacementResultState["detail_replacement"]>[string],
]
> = [];
Object.entries(records).forEach(([rawCardId, rawRecord]) => {
const cardId = String(rawCardId || "").trim();
if (!cardId || !rawRecord || typeof rawRecord !== "object" || Array.isArray(rawRecord)) {
return;
}
const record = rawRecord as Record<string, unknown>;
const resultUrl = normalizeOptionalHttpUrl(record.url);
const text = String(record.text || "").trim();
const compareSourceUrl = normalizeOptionalHttpUrl(record.compareSourceUrl);
const referenceImageUrl = normalizeOptionalHttpUrl(record.referenceImageUrl);
const referencePreviewUrl = normalizeOptionalHttpUrl(record.referencePreviewUrl);
const label = String(record.label || "").trim() || null;
const updatedAt = Number(record.updatedAt);
if (!resultUrl && !text && !compareSourceUrl && !referenceImageUrl && !referencePreviewUrl) {
return;
}
entries.push([
cardId.slice(0, 160),
{
url: resultUrl,
text,
status: normalizeReplacementResultStatus(record.status),
updatedAt: Number.isFinite(updatedAt) && updatedAt > 0 ? Math.round(updatedAt) : 0,
compareSourceUrl,
referenceImageUrl,
referencePreviewUrl,
label,
},
]);
});
return Object.fromEntries(entries.slice(0, 256));
};
return {
detail_replacement: normalizeModeState(raw.detail_replacement),
sku_replacement: normalizeModeState(raw.sku_replacement),
};
}
function getReplacementResultUrls(
value:
| NonNullable<AgentGenerationProjectReplacementResultState["detail_replacement"]>
| undefined
| null
) {
return Array.from(
new Set(
Object.values(value || {})
.map((record) => String(record?.url || "").trim())
.filter((url) => /^https?:\/\//i.test(url))
)
).slice(0, 128);
}
function reconcileReplacementResultCollections(params: {
resultCollections: AgentGenerationProjectResultCollections;
replacementResultState: AgentGenerationProjectReplacementResultState;
}) {
const detailReplacementUrls = getReplacementResultUrls(
params.replacementResultState.detail_replacement
);
const skuReplacementUrls = getReplacementResultUrls(
params.replacementResultState.sku_replacement
);
if (detailReplacementUrls.length === 0 && skuReplacementUrls.length === 0) {
return params.resultCollections;
}
return {
...params.resultCollections,
detail_replacement:
detailReplacementUrls.length > 0
? detailReplacementUrls
: params.resultCollections.detail_replacement,
sku_replacement:
skuReplacementUrls.length > 0
? skuReplacementUrls
: params.resultCollections.sku_replacement,
} satisfies AgentGenerationProjectResultCollections;
}
const REPLACEMENT_MODE_TYPES = [
"detail_replacement",
"sku_replacement",
] as const;
function getReplacementReferences(
workflowState: AgentWorkflowPersistedState,
modeType: (typeof REPLACEMENT_MODE_TYPES)[number]
) {
return modeType === "detail_replacement"
? workflowState.agentModeState?.detailReplacement?.references || []
: workflowState.agentModeState?.skuReplacement?.references || [];
}
function reconcileReplacementResultState(params: {
workflowState: AgentWorkflowPersistedState;
resultCollections: AgentGenerationProjectResultCollections;
replacementResultState: AgentGenerationProjectReplacementResultState;
}) {
const nextState: AgentGenerationProjectReplacementResultState = {
detail_replacement: {
...(params.replacementResultState.detail_replacement || {}),
},
sku_replacement: {
...(params.replacementResultState.sku_replacement || {}),
},
};
REPLACEMENT_MODE_TYPES.forEach((modeType) => {
const modeState = nextState[modeType] || {};
const hasPersistedResultRecord = Object.values(modeState).some((record) =>
Boolean(record?.url)
);
if (hasPersistedResultRecord) {
return;
}
const resultUrls = params.resultCollections[modeType] || [];
if (resultUrls.length === 0) {
return;
}
const references = getReplacementReferences(params.workflowState, modeType);
const usedCardIds = new Set(Object.keys(modeState));
const nextModeState = { ...modeState };
resultUrls.forEach((resultUrl, index) => {
const reference = references[index] || null;
const preferredCardId = String(reference?.id || "").trim();
const cardId =
preferredCardId && !usedCardIds.has(preferredCardId)
? preferredCardId
: `${modeType}-legacy-result-${index + 1}`;
usedCardIds.add(cardId);
nextModeState[cardId] = {
url: resultUrl,
text: "",
status: "completed",
updatedAt: 0,
compareSourceUrl: reference?.imageUrl || reference?.previewUrl || null,
referenceImageUrl: reference?.imageUrl || null,
referencePreviewUrl: reference?.previewUrl || null,
label: reference?.label || `历史结果 ${index + 1}`,
};
});
nextState[modeType] = nextModeState;
});
return nextState;
}
function buildRecoveredReplacementReference(params: {
cardId: string;
index: number;
record: NonNullable<
AgentGenerationProjectReplacementResultState["detail_replacement"]
>[string];
}): AgentDetailReplacementReferenceItem {
const referenceImageUrl =
params.record.referenceImageUrl ||
params.record.referencePreviewUrl ||
params.record.compareSourceUrl ||
null;
return {
id: params.cardId,
label: params.record.label || `历史结果 ${params.index + 1}`,
imageUrl: referenceImageUrl,
previewUrl: params.record.referencePreviewUrl || referenceImageUrl,
sortOrder: params.index,
};
}
function reconcileWorkflowReplacementReferences(params: {
workflowState: AgentWorkflowPersistedState;
replacementResultState: AgentGenerationProjectReplacementResultState;
}) {
const agentModeState =
params.workflowState.agentModeState || createEmptyAgentModeWorkspaceState();
const reconcileModeReferences = (
modeType: (typeof REPLACEMENT_MODE_TYPES)[number],
references: AgentDetailReplacementReferenceItem[]
) => {
const existingIds = new Set(references.map((reference) => reference.id));
const recovered = Object.entries(
params.replacementResultState[modeType] || {}
)
.filter(([cardId, record]) => cardId && Boolean(record?.url) && !existingIds.has(cardId))
.map(([cardId, record], index) =>
buildRecoveredReplacementReference({
cardId,
index: references.length + index,
record,
})
);
return recovered.length > 0 ? [...references, ...recovered] : references;
};
const detailReferences = reconcileModeReferences(
"detail_replacement",
agentModeState.detailReplacement.references
);
const skuReferences = reconcileModeReferences(
"sku_replacement",
agentModeState.skuReplacement.references
);
if (
detailReferences === agentModeState.detailReplacement.references &&
skuReferences === agentModeState.skuReplacement.references
) {
return params.workflowState;
}
return {
...params.workflowState,
agentModeState: {
...agentModeState,
detailReplacement: {
...agentModeState.detailReplacement,
references: detailReferences,
},
skuReplacement: {
...agentModeState.skuReplacement,
references: skuReferences,
},
},
};
}
function inferLegacyResultModeType(
workflowState: AgentWorkflowPersistedState,
resultUrls: string[]
): AgentGenerationResultModeType {
const detailReferenceCount = workflowState.agentModeState?.detailReplacement?.references?.length || 0;
const skuReferenceCount = workflowState.agentModeState?.skuReplacement?.references?.length || 0;
const resultCount = resultUrls.length;
if (detailReferenceCount > 0 && skuReferenceCount === 0) {
return "detail_replacement";
}
if (skuReferenceCount > 0 && detailReferenceCount === 0) {
return "sku_replacement";
}
if (detailReferenceCount > 0 || skuReferenceCount > 0) {
const detailGap = Math.abs(resultCount - detailReferenceCount);
const skuGap = Math.abs(resultCount - skuReferenceCount);
return detailGap <= skuGap ? "detail_replacement" : "sku_replacement";
}
return "scene_generation";
}
function normalizeMigratedResultCollections(params: {
workflowState: AgentWorkflowPersistedState;
resultImageUrls: string[];
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;
if (!hasExplicitBuckets && params.resultImageUrls.length > 0) {
const inferredModeType = inferLegacyResultModeType(
params.workflowState,
params.resultImageUrls
);
return {
scene_generation:
inferredModeType === "scene_generation" ? params.resultImageUrls : [],
detail_replacement:
inferredModeType === "detail_replacement" ? params.resultImageUrls : [],
sku_replacement:
inferredModeType === "sku_replacement" ? params.resultImageUrls : [],
} satisfies AgentGenerationProjectResultCollections;
}
const detailReferenceCount =
params.workflowState.agentModeState?.detailReplacement?.references?.length || 0;
const skuReferenceCount =
params.workflowState.agentModeState?.skuReplacement?.references?.length || 0;
if (
normalizedSceneUrls.length > 0 &&
normalizedDetailUrls.length === 0 &&
normalizedSkuUrls.length === 0
) {
if (
detailReferenceCount > 0 &&
normalizedSceneUrls.length >= Math.max(4, Math.floor(detailReferenceCount * 0.6))
) {
return {
scene_generation: [],
detail_generation: [],
detail_replacement: normalizedSceneUrls,
sku_replacement: [],
} satisfies AgentGenerationProjectResultCollections;
}
if (
skuReferenceCount > 0 &&
normalizedSceneUrls.length >= Math.max(4, Math.floor(skuReferenceCount * 0.6))
) {
return {
scene_generation: [],
detail_generation: [],
detail_replacement: [],
sku_replacement: normalizedSceneUrls,
} satisfies AgentGenerationProjectResultCollections;
}
}
return params.resultCollections;
}
function normalizeWorkspaceState(value: unknown): AgentGenerationProjectWorkspaceState {
const raw =
value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: {};
const attachments = Array.isArray(raw.attachments)
? raw.attachments
.map((item) => {
if (!item || typeof item !== "object" || Array.isArray(item)) {
return null;
}
const record = item as Record<string, unknown>;
const url = String(record.url || "").trim();
if (!url) return null;
return {
url,
name: String(record.name || "").trim() || null,
contentType: String(record.contentType || "").trim() || null,
assetId: String(record.assetId || "").trim() || null,
};
})
.slice(0, 16)
: [];
const rawImageParams =
raw.imageParams && typeof raw.imageParams === "object" && !Array.isArray(raw.imageParams)
? (raw.imageParams as Record<string, unknown>)
: {};
const imageCount = Number(rawImageParams.imageCount);
const activeBusinessModeKey = String(raw.activeBusinessModeKey || "").trim();
const normalizedBusinessModeKey: AgentWorkflowBusinessModeKey =
activeBusinessModeKey === "detail" ||
activeBusinessModeKey === "buyer_show" ||
activeBusinessModeKey === "main_sku"
? activeBusinessModeKey
: "main_sku";
return {
input: String(raw.input || "").trim(),
attachments,
selectedModelId: String(raw.selectedModelId || "").trim() || "auto",
activeBusinessModeKey: normalizedBusinessModeKey,
imageParams: {
aspectRatio: String(rawImageParams.aspectRatio || "").trim() || "auto",
imageSize: String(rawImageParams.imageSize || "").trim() || "2K",
imageCount: Number.isFinite(imageCount) ? Math.max(1, Math.min(8, Math.round(imageCount))) : 1,
},
};
}
function hasExplicitWorkspaceBusinessModeKey(value: unknown) {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return false;
}
return Boolean(String((value as Record<string, unknown>).activeBusinessModeKey || "").trim());
}
function inferWorkspaceBusinessModeKey(params: {
workflowState: AgentWorkflowPersistedState;
resultCollections: AgentGenerationProjectResultCollections;
replacementResultState: AgentGenerationProjectReplacementResultState;
}): AgentWorkflowBusinessModeKey {
const detailReferenceCount =
params.workflowState.agentModeState?.detailReplacement?.references?.length || 0;
const skuReferenceCount =
params.workflowState.agentModeState?.skuReplacement?.references?.length || 0;
const normalizedResultCollections = normalizeResultCollections(params.resultCollections);
const hasDetailResults =
(normalizedResultCollections.detail_generation || []).length > 0 ||
(normalizedResultCollections.detail_replacement || []).length > 0 ||
(normalizedResultCollections.sku_replacement || []).length > 0 ||
getReplacementResultUrls(params.replacementResultState.detail_replacement).length > 0 ||
getReplacementResultUrls(params.replacementResultState.sku_replacement).length > 0;
return detailReferenceCount > 0 || skuReferenceCount > 0 || hasDetailResults
? "detail"
: "main_sku";
}
function normalizeWorkflowState(value: unknown): AgentWorkflowPersistedState {
const raw =
value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: {};
return {
inputs: {
primarySku: String(raw.inputs && typeof raw.inputs === "object" ? (raw.inputs as Record<string, unknown>).primarySku || "" : "").trim(),
secondarySku: String(raw.inputs && typeof raw.inputs === "object" ? (raw.inputs as Record<string, unknown>).secondarySku || "" : "").trim(),
brand: String(raw.inputs && typeof raw.inputs === "object" ? (raw.inputs as Record<string, unknown>).brand || "" : "").trim(),
store: String(raw.inputs && typeof raw.inputs === "object" ? (raw.inputs as Record<string, unknown>).store || "" : "").trim(),
officialSecondCategory: String(raw.inputs && typeof raw.inputs === "object" ? (raw.inputs as Record<string, unknown>).officialSecondCategory || "" : "").trim(),
officialThirdCategory: String(raw.inputs && typeof raw.inputs === "object" ? (raw.inputs as Record<string, unknown>).officialThirdCategory || "" : "").trim(),
thirdCategory: String(raw.inputs && typeof raw.inputs === "object" ? (raw.inputs as Record<string, unknown>).thirdCategory || "" : "").trim(),
fourthCategory: String(raw.inputs && typeof raw.inputs === "object" ? (raw.inputs as Record<string, unknown>).fourthCategory || "" : "").trim(),
fifthCategory: String(raw.inputs && typeof raw.inputs === "object" ? (raw.inputs as Record<string, unknown>).fifthCategory || "" : "").trim(),
priceBand: String(raw.inputs && typeof raw.inputs === "object" ? (raw.inputs as Record<string, unknown>).priceBand || "" : "").trim(),
},
materials: normalizeAgentWorkflowMaterialSelections(raw.materials),
appearanceRecognitionEnabled: raw.appearanceRecognitionEnabled === true,
selections:
raw.selections && typeof raw.selections === "object" && !Array.isArray(raw.selections)
? (raw.selections as Record<string, string>)
: {},
selectedBrandModel:
raw.selectedBrandModel && typeof raw.selectedBrandModel === "object" && !Array.isArray(raw.selectedBrandModel)
? {
...(raw.selectedBrandModel as Record<string, unknown>),
brandName: String((raw.selectedBrandModel as Record<string, unknown>).brandName || "").trim(),
id: String((raw.selectedBrandModel as Record<string, unknown>).id || "").trim(),
imageUrl: String((raw.selectedBrandModel as Record<string, unknown>).imageUrl || "").trim(),
assetId: String((raw.selectedBrandModel as Record<string, unknown>).assetId || "").trim() || null,
displayName: String((raw.selectedBrandModel as Record<string, unknown>).displayName || "").trim() || null,
mimeType: String((raw.selectedBrandModel as Record<string, unknown>).mimeType || "").trim() || null,
sortOrder: Number.isFinite(Number((raw.selectedBrandModel as Record<string, unknown>).sortOrder))
? Math.max(0, Math.min(999, Math.round(Number((raw.selectedBrandModel as Record<string, unknown>).sortOrder))))
: 0,
}
: null,
selectedAgentPresetId: String(raw.selectedAgentPresetId || "").trim() || null,
currentStage: raw.currentStage === "mode" ? "mode" : "workflow",
agentModeState: normalizeAgentModeWorkspaceState(raw.agentModeState),
};
}
export function normalizeAgentGenerationProjectPersistedState(
value: unknown
): AgentGenerationProjectPersistedState {
const raw =
value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: {};
const resultImageUrls = Array.isArray(raw.resultImageUrls)
? Array.from(
new Set(
raw.resultImageUrls
.map((item) => String(item || "").trim())
.filter((item) => /^https?:\/\//i.test(item))
)
).slice(0, 64)
: [];
let workflowState = normalizeWorkflowState(raw.workflowState);
const deletedResultCollections = normalizeResultCollections(raw.deletedResultCollections);
const initialReplacementResultState = normalizeAgentGenerationProjectReplacementResultState(
raw.replacementResultState
);
let resultCollections = reconcileReplacementResultCollections({
replacementResultState: initialReplacementResultState,
resultCollections: normalizeMigratedResultCollections({
workflowState,
resultImageUrls,
resultCollections: normalizeResultCollections(raw.resultCollections),
}),
});
const replacementResultState = reconcileReplacementResultState({
workflowState,
resultCollections,
replacementResultState: initialReplacementResultState,
});
workflowState = reconcileWorkflowReplacementReferences({
workflowState,
replacementResultState,
});
resultCollections = reconcileReplacementResultCollections({
resultCollections,
replacementResultState,
});
const workspaceState = normalizeWorkspaceState(raw.workspaceState);
if (!hasExplicitWorkspaceBusinessModeKey(raw.workspaceState)) {
workspaceState.activeBusinessModeKey = inferWorkspaceBusinessModeKey({
workflowState,
resultCollections,
replacementResultState,
});
}
return {
workflowState,
workspaceState,
resultImageUrls,
resultCollections,
deletedResultCollections,
replacementResultState,
};
}