feat: add sku goods info query support
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-22 23:39:16 +08:00
parent 47df3b644c
commit 8e225e9ae1
7 changed files with 410 additions and 1 deletions

View File

@ -58,6 +58,10 @@ import {
defaultAgentWorkflowMaterialApiConfig,
normalizeAgentWorkflowMaterialApiConfig,
} from "@/lib/agents/workflow-material-config";
import {
defaultAgentWorkflowGoodsInfoApiConfig,
normalizeAgentWorkflowGoodsInfoApiConfig,
} from "@/lib/agents/workflow-goods-info-config";
import { fetchUploadWithRetry } from "@/lib/client/upload-with-retry";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
@ -76,6 +80,7 @@ import type {
AgentPresetLibrarySection,
AgentWorkflowBrandModelConfig,
AgentWorkflowBrandStoreEntry,
AgentWorkflowGoodsInfoApiConfig,
AgentWorkflowMaterialApiConfig,
AgentWorkflowOptionUiConfig,
AgentWorkflowShowcaseConfig,
@ -396,6 +401,7 @@ export function AgentWorkflowSettingsClient({
agentPresetLibraryConfig?: AgentPresetLibraryConfig;
agentWorkflowUiConfig?: AgentWorkflowUiConfig;
agentWorkflowMaterialApiConfig?: AgentWorkflowMaterialApiConfig;
agentWorkflowGoodsInfoApiConfig?: AgentWorkflowGoodsInfoApiConfig;
agentWorkflowBrandModelConfig?: AgentWorkflowBrandModelConfig;
agentWorkflowShowcaseConfig?: AgentWorkflowShowcaseConfig;
}>("/api/site-settings", fetcher);
@ -406,6 +412,9 @@ export function AgentWorkflowSettingsClient({
const [materialApiConfig, setMaterialApiConfig] = useState<AgentWorkflowMaterialApiConfig>(
defaultAgentWorkflowMaterialApiConfig
);
const [goodsInfoApiConfig, setGoodsInfoApiConfig] = useState<AgentWorkflowGoodsInfoApiConfig>(
defaultAgentWorkflowGoodsInfoApiConfig
);
const [showcaseConfig, setShowcaseConfig] = useState<AgentWorkflowShowcaseConfig>(
defaultAgentWorkflowShowcaseConfig
);
@ -438,6 +447,9 @@ export function AgentWorkflowSettingsClient({
setMaterialApiConfig(
normalizeAgentWorkflowMaterialApiConfig(data.agentWorkflowMaterialApiConfig)
);
setGoodsInfoApiConfig(
normalizeAgentWorkflowGoodsInfoApiConfig(data.agentWorkflowGoodsInfoApiConfig)
);
setShowcaseConfig(
normalizeAgentWorkflowShowcaseConfig(data.agentWorkflowShowcaseConfig)
);
@ -887,6 +899,7 @@ export function AgentWorkflowSettingsClient({
agentPresetLibraryConfig: presetLibraryConfig,
agentWorkflowUiConfig: config,
agentWorkflowMaterialApiConfig: materialApiConfig,
agentWorkflowGoodsInfoApiConfig: goodsInfoApiConfig,
agentWorkflowBrandModelConfig: brandModelConfig,
agentWorkflowShowcaseConfig: showcaseConfig,
}),
@ -2054,6 +2067,79 @@ export function AgentWorkflowSettingsClient({
</Card>
</motion.div>
<motion.div variants={itemVariants}>
<Card>
<CardHeader className="pb-4">
<div className="flex items-center gap-3">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-cyan-500/10">
<Package className="h-4 w-4 text-cyan-600" />
</div>
<div>
<CardTitle className="text-base">SKU 商品信息接口</CardTitle>
<CardDescription>配置货号 / 条码查询商品资料接口</CardDescription>
</div>
</div>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between rounded-lg border bg-muted/20 px-4 py-3">
<div>
<div className="text-sm font-medium">启用商品信息查询</div>
<div className="text-xs text-muted-foreground">
开启后 SKU 查询结果会显示商品名称、尺寸、材质、设计师、采购负责人、板材类型
</div>
</div>
<Switch
checked={goodsInfoApiConfig.enabled}
onCheckedChange={(checked) =>
setGoodsInfoApiConfig((prev) => ({ ...prev, enabled: checked }))
}
/>
</div>
<div className="space-y-2">
<Label className="text-xs">接口基地址</Label>
<Input
value={goodsInfoApiConfig.baseUrl}
onChange={(e) =>
setGoodsInfoApiConfig((prev) => ({ ...prev, baseUrl: e.target.value }))
}
placeholder="https://www.jiansun.vip:63211"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label className="text-xs">Client Key</Label>
<Input
value={goodsInfoApiConfig.clientKey}
onChange={(e) =>
setGoodsInfoApiConfig((prev) => ({
...prev,
clientKey: e.target.value,
}))
}
placeholder="例如:yunyingb"
/>
</div>
<div className="space-y-2">
<Label className="text-xs">Secret</Label>
<Input
type="password"
value={goodsInfoApiConfig.secret}
onChange={(e) =>
setGoodsInfoApiConfig((prev) => ({
...prev,
secret: e.target.value,
}))
}
placeholder="输入 Secret"
/>
</div>
</div>
</CardContent>
</Card>
</motion.div>
<motion.div variants={itemVariants}>
<Card>
<CardHeader className="pb-4">

View File

@ -1,4 +1,5 @@
import { generateText } from "ai";
import { createCipheriv, createHash } from "node:crypto";
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { auth } from "@/app/(auth)/auth";
@ -9,6 +10,10 @@ import {
AGENT_WORKFLOW_MATERIAL_API_CONFIG_KEY,
normalizeAgentWorkflowMaterialApiConfig,
} from "@/lib/agents/workflow-material-config";
import {
AGENT_WORKFLOW_GOODS_INFO_API_CONFIG_KEY,
normalizeAgentWorkflowGoodsInfoApiConfig,
} from "@/lib/agents/workflow-goods-info-config";
export const maxDuration = 180;
@ -62,10 +67,114 @@ type ExternalSearchResponse = {
};
};
type ExternalGoodsInfoResponse = {
success?: boolean;
errMsg?: string | null;
data?: Record<string, unknown> | null;
};
function normalizeBaseUrl(value: string) {
return String(value || "").trim().replace(/\/+$/, "");
}
function sanitizeGoodsInfoText(value: unknown, maxLength = 255) {
return String(value || "").trim().slice(0, maxLength);
}
function createGoodsInfoSignature(timespan: string, secret: string) {
const secretBuffer = Buffer.from(String(secret || "").trim(), "utf8");
if (secretBuffer.length !== 8) {
throw new Error("商品信息接口 Secret 长度必须为 8 位");
}
const cipher = createCipheriv("des-cbc", secretBuffer, secretBuffer);
cipher.setAutoPadding(true);
const encrypted = Buffer.concat([
cipher.update(String(timespan || "").trim(), "utf8"),
cipher.final(),
]);
const encryptedHex = encrypted.toString("hex").toUpperCase();
return createHash("md5").update(encryptedHex, "utf8").digest("hex").toUpperCase();
}
function buildGoodsInfoSummary(data: Record<string, unknown>) {
const productName =
sanitizeGoodsInfoText(data["商品名称"], 255) ||
sanitizeGoodsInfoText(data["名称"], 255) ||
null;
const sizeInfoParts = [
sanitizeGoodsInfoText(data["商品尺寸"], 128),
[data["长"], data["宽"], data["高"]]
.map((item) => sanitizeGoodsInfoText(item, 32))
.filter(Boolean)
.join(" x "),
].filter(Boolean);
const materialInfoParts = [
sanitizeGoodsInfoText(data["材质"], 128),
sanitizeGoodsInfoText(data["材质类型"], 128),
].filter(Boolean);
return {
productName,
sizeInfo: sizeInfoParts.join(" · ") || null,
materialInfo: materialInfoParts.join(" · ") || null,
designer: sanitizeGoodsInfoText(data["设计师"], 128) || null,
buyer: sanitizeGoodsInfoText(data["采购负责人"], 128) || null,
boardType: sanitizeGoodsInfoText(data["板材类型"], 128) || null,
matchedSpu:
sanitizeGoodsInfoText(data["spu"], 64) ||
sanitizeGoodsInfoText(data["SPU"], 64) ||
null,
};
}
async function fetchGoodsInfoByKeyword(params: {
keyword: string;
config: ReturnType<typeof normalizeAgentWorkflowGoodsInfoApiConfig>;
}) {
const baseUrl = normalizeBaseUrl(params.config.baseUrl);
const clientKey = String(params.config.clientKey || "").trim();
const secret = String(params.config.secret || "").trim();
if (!params.config.enabled || !baseUrl || !clientKey || !secret) {
return null;
}
const timespan = String(Math.floor(Date.now() / 1000));
const sign = createGoodsInfoSignature(timespan, secret);
const commonHeaders = {
Key: clientKey,
Timespan: timespan,
Sign: sign,
Accept: "application/json",
};
const requestPaths = [
`/api/GoodsInfo/SearchByProcode?procode=${encodeURIComponent(params.keyword)}`,
`/api/GoodsInfo/SearchBySkuoid?skuoid=${encodeURIComponent(params.keyword)}`,
];
for (const requestPath of requestPaths) {
try {
const response = await fetch(`${baseUrl}${requestPath}`, {
method: "GET",
headers: commonHeaders,
cache: "no-store",
});
const result = (await response.json().catch(() => ({}))) as ExternalGoodsInfoResponse;
if (response.ok && result.success === true && result.data && typeof result.data === "object") {
return buildGoodsInfoSummary(result.data);
}
} catch (error) {
console.warn("[Agent Workflow Material Search] Goods info query failed:", {
requestPath,
error,
});
}
}
return null;
}
function normalizeImageIdentityText(...values: Array<string | null | undefined>) {
return values
.map((value) => String(value || "").trim().toLowerCase().replace(/\s+/g, ""))
@ -564,6 +673,12 @@ export async function GET(request: NextRequest) {
const setting = await getSystemSetting({ key: AGENT_WORKFLOW_MATERIAL_API_CONFIG_KEY });
const config = normalizeAgentWorkflowMaterialApiConfig(setting?.value);
const goodsInfoSetting = await getSystemSetting({
key: AGENT_WORKFLOW_GOODS_INFO_API_CONFIG_KEY,
});
const goodsInfoConfig = normalizeAgentWorkflowGoodsInfoApiConfig(
goodsInfoSetting?.value
);
if (!config.enabled) {
return NextResponse.json(
{ error: "素材搜索功能未开启,请先在系统设置中启用" },
@ -643,12 +758,23 @@ export async function GET(request: NextRequest) {
totalCount: skuOptions.length,
images: [],
skuOptions,
goodsInfo: null,
},
});
}
const matchedSku = String(upstreamResult.data.sku || normalizedKeyword).trim() || normalizedKeyword;
const productModel = String(upstreamResult.data.productModel || "").trim() || null;
const goodsInfo =
queryType === "sku"
? await fetchGoodsInfoByKeyword({
keyword: normalizedKeyword,
config: goodsInfoConfig,
}).catch((error) => {
console.warn("[Agent Workflow Material Search] Goods info fetch failed:", error);
return null;
})
: null;
const requiredCount = Number.isFinite(Number(parsed.requiredCount))
? Math.max(1, Math.min(4, Math.round(Number(parsed.requiredCount))))
: null;
@ -734,7 +860,7 @@ export async function GET(request: NextRequest) {
query: normalizedKeyword,
queryType: "sku",
matchedSku,
matchedSpu: productModel,
matchedSpu: goodsInfo?.matchedSpu || productModel,
materialId: String(upstreamResult.data.materialId || "").trim() || null,
productModel,
zipUrl: String(upstreamResult.data.downloadUrls?.zip || "").trim() || null,
@ -744,6 +870,16 @@ export async function GET(request: NextRequest) {
images: importedImages,
recommendedImageIds,
skuOptions: [],
goodsInfo: goodsInfo
? {
productName: goodsInfo.productName,
sizeInfo: goodsInfo.sizeInfo,
materialInfo: goodsInfo.materialInfo,
designer: goodsInfo.designer,
buyer: goodsInfo.buyer,
boardType: goodsInfo.boardType,
}
: null,
},
});
} catch (error) {

View File

@ -46,6 +46,12 @@ import {
normalizeAgentWorkflowMaterialApiConfig,
normalizeAgentWorkflowMaterialApiConfigForPublic,
} from "@/lib/agents/workflow-material-config";
import {
AGENT_WORKFLOW_GOODS_INFO_API_CONFIG_KEY,
defaultAgentWorkflowGoodsInfoApiConfig,
normalizeAgentWorkflowGoodsInfoApiConfig,
normalizeAgentWorkflowGoodsInfoApiConfigForPublic,
} from "@/lib/agents/workflow-goods-info-config";
import {
AGENT_WORKFLOW_SHOWCASE_CONFIG_KEY,
defaultAgentWorkflowShowcaseConfig,
@ -245,6 +251,7 @@ export async function GET() {
agentSecondaryTypeConfigSetting,
agentWorkflowUiConfigSetting,
agentWorkflowMaterialApiConfigSetting,
agentWorkflowGoodsInfoApiConfigSetting,
agentWorkflowBrandModelConfigSetting,
agentWorkflowShowcaseConfigSetting,
] = await Promise.all([
@ -375,6 +382,12 @@ export async function GET() {
.where(eq(systemSettings.key, AGENT_WORKFLOW_MATERIAL_API_CONFIG_KEY))
.limit(1)
.then(rows => rows[0]),
db
.select()
.from(systemSettings)
.where(eq(systemSettings.key, AGENT_WORKFLOW_GOODS_INFO_API_CONFIG_KEY))
.limit(1)
.then(rows => rows[0]),
db
.select()
.from(systemSettings)
@ -439,6 +452,11 @@ export async function GET() {
: normalizeAgentWorkflowMaterialApiConfigForPublic(
agentWorkflowMaterialApiConfigSetting?.value
);
const agentWorkflowGoodsInfoApiConfig = canManageAgentWorkflow
? normalizeAgentWorkflowGoodsInfoApiConfig(agentWorkflowGoodsInfoApiConfigSetting?.value)
: normalizeAgentWorkflowGoodsInfoApiConfigForPublic(
agentWorkflowGoodsInfoApiConfigSetting?.value
);
const agentWorkflowBrandModelConfig = normalizeAgentWorkflowBrandModelConfig(
agentWorkflowBrandModelConfigSetting?.value
);
@ -475,6 +493,7 @@ export async function GET() {
agentSecondaryTypeConfig: defaultAgentSecondaryTypeConfig,
agentWorkflowUiConfig: defaultAgentWorkflowUiConfig,
agentWorkflowMaterialApiConfig: defaultAgentWorkflowMaterialApiConfig,
agentWorkflowGoodsInfoApiConfig: defaultAgentWorkflowGoodsInfoApiConfig,
agentWorkflowBrandModelConfig: defaultAgentWorkflowBrandModelConfig,
agentWorkflowShowcaseConfig: defaultAgentWorkflowShowcaseConfig,
realtimeCanvasEnabled: true,
@ -524,6 +543,7 @@ export async function GET() {
agentSecondaryTypeConfig,
agentWorkflowUiConfig,
agentWorkflowMaterialApiConfig,
agentWorkflowGoodsInfoApiConfig,
agentWorkflowBrandModelConfig,
agentWorkflowShowcaseConfig,
membershipConfig,
@ -696,6 +716,14 @@ const updateSchema = z.object({
apiKey: z.string().trim().max(500).optional(),
})
.optional(),
agentWorkflowGoodsInfoApiConfig: z
.object({
enabled: z.boolean().optional(),
baseUrl: z.string().trim().max(255).optional(),
clientKey: z.string().trim().max(255).optional(),
secret: z.string().trim().max(255).optional(),
})
.optional(),
agentWorkflowBrandModelConfig: z
.object({
entries: z
@ -807,6 +835,7 @@ export async function PUT(request: NextRequest) {
"agentPresetLibraryConfig",
"agentWorkflowUiConfig",
"agentWorkflowMaterialApiConfig",
"agentWorkflowGoodsInfoApiConfig",
"agentWorkflowBrandModelConfig",
"agentWorkflowShowcaseConfig",
]);
@ -829,6 +858,7 @@ export async function PUT(request: NextRequest) {
agentSecondaryTypeConfig,
agentWorkflowUiConfig,
agentWorkflowMaterialApiConfig,
agentWorkflowGoodsInfoApiConfig,
agentWorkflowBrandModelConfig,
agentWorkflowShowcaseConfig,
membershipConfig,
@ -1097,6 +1127,28 @@ export async function PUT(request: NextRequest) {
});
}
if (agentWorkflowGoodsInfoApiConfig !== undefined) {
const normalized = normalizeAgentWorkflowGoodsInfoApiConfig(
agentWorkflowGoodsInfoApiConfig
);
await db
.insert(systemSettings)
.values({
key: AGENT_WORKFLOW_GOODS_INFO_API_CONFIG_KEY,
value: normalized,
updatedAt: new Date(),
updatedBy: session.user.id,
})
.onConflictDoUpdate({
target: systemSettings.key,
set: {
value: normalized,
updatedAt: new Date(),
updatedBy: session.user.id,
},
});
}
if (agentWorkflowBrandModelConfig !== undefined) {
const normalized = normalizeAgentWorkflowBrandModelConfig(
agentWorkflowBrandModelConfig
@ -1323,6 +1375,10 @@ export async function PUT(request: NextRequest) {
agentWorkflowMaterialApiConfig !== undefined
? normalizeAgentWorkflowMaterialApiConfig(agentWorkflowMaterialApiConfig)
: defaultAgentWorkflowMaterialApiConfig,
agentWorkflowGoodsInfoApiConfig:
agentWorkflowGoodsInfoApiConfig !== undefined
? normalizeAgentWorkflowGoodsInfoApiConfig(agentWorkflowGoodsInfoApiConfig)
: defaultAgentWorkflowGoodsInfoApiConfig,
agentWorkflowBrandModelConfig:
agentWorkflowBrandModelConfig !== undefined
? normalizeAgentWorkflowBrandModelConfig(agentWorkflowBrandModelConfig)
@ -1868,6 +1924,10 @@ export async function PUT(request: NextRequest) {
agentWorkflowMaterialApiConfig !== undefined
? normalizeAgentWorkflowMaterialApiConfig(agentWorkflowMaterialApiConfig)
: defaultAgentWorkflowMaterialApiConfig,
agentWorkflowGoodsInfoApiConfig:
agentWorkflowGoodsInfoApiConfig !== undefined
? normalizeAgentWorkflowGoodsInfoApiConfig(agentWorkflowGoodsInfoApiConfig)
: defaultAgentWorkflowGoodsInfoApiConfig,
agentWorkflowBrandModelConfig:
agentWorkflowBrandModelConfig !== undefined
? normalizeAgentWorkflowBrandModelConfig(agentWorkflowBrandModelConfig)

View File

@ -73,6 +73,33 @@ export function AgentWorkflowMaterialPicker({
const hasSelectableImages = Boolean(
result?.images?.some((item) => Boolean(String(item.imageUrl || "").trim()))
);
const goodsInfoItems = [
{
label: "商品名称",
value: result?.goodsInfo?.productName || null,
fullWidth: true,
},
{
label: "尺寸信息",
value: result?.goodsInfo?.sizeInfo || null,
},
{
label: "材质信息",
value: result?.goodsInfo?.materialInfo || null,
},
{
label: "设计师",
value: result?.goodsInfo?.designer || null,
},
{
label: "采购负责人",
value: result?.goodsInfo?.buyer || null,
},
{
label: "板材类型",
value: result?.goodsInfo?.boardType || null,
},
].filter((item) => Boolean(item.value));
const remainingSelectableCount =
resolvedSelectionLimit === null ? null : Math.max(resolvedSelectionLimit - selectedCount, 0);
@ -309,6 +336,36 @@ export function AgentWorkflowMaterialPicker({
</div>
</div>
{goodsInfoItems.length > 0 ? (
<div
className={cn(
"grid gap-2 rounded-[24px] border p-3",
isDark
? "border-white/12 bg-white/[0.03]"
: "border-border bg-background/70",
"grid-cols-1 md:grid-cols-2 xl:grid-cols-3"
)}
>
{goodsInfoItems.map((item) => (
<div
key={`goods-info-${item.label}`}
className={cn(
"rounded-[18px] border px-3 py-2",
isDark ? "border-white/8 bg-black/20" : "border-border bg-muted/30",
item.fullWidth ? "md:col-span-2 xl:col-span-3" : ""
)}
>
<div className={cn("text-[11px]", isDark ? "text-white/45" : "text-muted-foreground")}>
{item.label}
</div>
<div className={cn("mt-1 text-sm font-medium leading-6", isDark ? "text-white/90" : "text-foreground")}>
{item.value}
</div>
</div>
))}
</div>
) : null}
{result.skuOptions?.length ? (
<div className="grid grid-cols-1 gap-3 xl:grid-cols-2">
{result.skuOptions.map((option) => (

View File

@ -0,0 +1,44 @@
import type { AgentWorkflowGoodsInfoApiConfig } from "@/types/agent";
export const AGENT_WORKFLOW_GOODS_INFO_API_CONFIG_KEY =
"agent_workflow_goods_info_api_config";
export const defaultAgentWorkflowGoodsInfoApiConfig: AgentWorkflowGoodsInfoApiConfig = {
enabled: false,
baseUrl: "https://www.jiansun.vip:63211",
clientKey: "",
secret: "",
};
function sanitizeText(value: unknown, maxLength: number) {
return String(value || "").trim().slice(0, maxLength);
}
export function normalizeAgentWorkflowGoodsInfoApiConfig(
value: unknown
): AgentWorkflowGoodsInfoApiConfig {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return defaultAgentWorkflowGoodsInfoApiConfig;
}
const raw = value as Partial<AgentWorkflowGoodsInfoApiConfig>;
return {
enabled: raw.enabled === true,
baseUrl:
sanitizeText(raw.baseUrl, 255) || defaultAgentWorkflowGoodsInfoApiConfig.baseUrl,
clientKey: sanitizeText(raw.clientKey, 255),
secret: sanitizeText(raw.secret, 255),
};
}
export function normalizeAgentWorkflowGoodsInfoApiConfigForPublic(
value: unknown
): AgentWorkflowGoodsInfoApiConfig {
const config = normalizeAgentWorkflowGoodsInfoApiConfig(value);
return {
...config,
clientKey: "",
secret: "",
};
}

View File

@ -121,6 +121,17 @@ function normalizeMaterialSearchResult(value: unknown): AgentWorkflowMaterialSea
)
: [],
skuOptions,
goodsInfo:
raw.goodsInfo && typeof raw.goodsInfo === "object" && !Array.isArray(raw.goodsInfo)
? {
productName: sanitizeText((raw.goodsInfo as Record<string, unknown>).productName, 255) || null,
sizeInfo: sanitizeText((raw.goodsInfo as Record<string, unknown>).sizeInfo, 255) || null,
materialInfo: sanitizeText((raw.goodsInfo as Record<string, unknown>).materialInfo, 255) || null,
designer: sanitizeText((raw.goodsInfo as Record<string, unknown>).designer, 128) || null,
buyer: sanitizeText((raw.goodsInfo as Record<string, unknown>).buyer, 128) || null,
boardType: sanitizeText((raw.goodsInfo as Record<string, unknown>).boardType, 128) || null,
}
: null,
};
}

View File

@ -118,6 +118,13 @@ export interface AgentWorkflowMaterialApiConfig {
apiKey: string;
}
export interface AgentWorkflowGoodsInfoApiConfig {
enabled: boolean;
baseUrl: string;
clientKey: string;
secret: string;
}
export type AgentWorkflowMaterialQueryType = "sku" | "spu";
export interface AgentWorkflowBrandModelImage {
@ -190,6 +197,14 @@ export interface AgentWorkflowMaterialSearchResult {
images: AgentWorkflowMaterialImage[];
recommendedImageIds?: string[];
skuOptions?: AgentWorkflowMaterialSkuOption[];
goodsInfo?: {
productName?: string | null;
sizeInfo?: string | null;
materialInfo?: string | null;
designer?: string | null;
buyer?: string | null;
boardType?: string | null;
} | null;
}
export interface AgentWorkflowMaterialSelectionState {