From 25b20e1730a4574f96da6b7eda56a8a5a101db9a Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 27 Mar 2026 11:18:11 +0800 Subject: [PATCH] Fix agent workspace chat request validation --- .../agents/AgentGenerationWorkspaceDarkV2.tsx | 151 +++++++++++++++--- 1 file changed, 132 insertions(+), 19 deletions(-) diff --git a/components/agents/AgentGenerationWorkspaceDarkV2.tsx b/components/agents/AgentGenerationWorkspaceDarkV2.tsx index 5f5d2c0ea..6e330c213 100644 --- a/components/agents/AgentGenerationWorkspaceDarkV2.tsx +++ b/components/agents/AgentGenerationWorkspaceDarkV2.tsx @@ -141,6 +141,72 @@ interface AgentGenerationWorkspaceDarkV2Props { interface PickedAttachment extends Attachment {} +const MAX_CHAT_TEXT_PART_LENGTH = 2000; +const MAX_CHAT_HIDDEN_PROMPT_LENGTH = 4000; +const MAX_CHAT_ATTACHMENT_NAME_LENGTH = 100; +const MAX_AGENT_MODE_QUERY_LENGTH = 64; +const MAX_AGENT_MODE_REFERENCE_COUNT = 40; +const CHAT_UUID_REGEX = + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; +const ALLOWED_CHAT_MEDIA_TYPES = new Set([ + "image/jpeg", + "image/png", + "image/webp", + "image/gif", + "image/jpg", + "image/bmp", + "image/svg+xml", +]); + +function isValidChatUuid(value?: string | null): value is string { + return Boolean(value && CHAT_UUID_REGEX.test(String(value).trim())); +} + +function isValidChatHttpUrl(value?: string | null): value is string { + if (!value) return false; + try { + const parsed = new URL(String(value).trim()); + return parsed.protocol === "http:" || parsed.protocol === "https:"; + } catch { + return false; + } +} + +function clampChatRequestText(value?: string | null, maxLength = MAX_CHAT_TEXT_PART_LENGTH): string { + return String(value || "").slice(0, maxLength); +} + +function normalizeChatAttachmentName(value?: string | null): string { + const normalized = String(value || "").trim() || "参考图"; + return normalized.slice(0, MAX_CHAT_ATTACHMENT_NAME_LENGTH); +} + +function normalizeChatMediaType(value?: string | null): string { + const normalized = String(value || "").trim().toLowerCase(); + if (normalized === "image/pjpeg") return "image/jpeg"; + if (normalized === "image/x-png") return "image/png"; + if (ALLOWED_CHAT_MEDIA_TYPES.has(normalized)) { + return normalized; + } + return "image/jpeg"; +} + +function sanitizeChatAttachment( + attachment?: PickedAttachment | null, +): PickedAttachment | null { + if (!attachment || !isValidChatHttpUrl(attachment.url)) { + return null; + } + + return { + ...attachment, + url: attachment.url, + name: normalizeChatAttachmentName(attachment.name), + contentType: normalizeChatMediaType(attachment.contentType), + assetId: isValidChatUuid(attachment.assetId) ? attachment.assetId : undefined, + }; +} + interface GeneratedImageGroup { messageId: string; imageUrls: string[]; @@ -1368,6 +1434,12 @@ export function AgentGenerationWorkspaceDarkV2({ fetch: fetchWithErrorHandlers, prepareSendMessagesRequest(request) { const transportState = transportStateRef.current; + const resolvedAgentPresetId = + transportState.selectedAgentPresetId || selectedAgentPresetId || undefined; + const resolvedSelectedModelId = + transportState.selectedModelId !== "auto" + ? transportState.selectedModelId + : selectedAgentPreset?.defaultModelId || transportState.selectedModelId; const replacementModePayload = transportState.selectedAgentPresetType === "image" && (transportState.isDetailReplacementPreset || transportState.isSkuReplacementPreset) @@ -1375,9 +1447,12 @@ export function AgentGenerationWorkspaceDarkV2({ modeType: transportState.isDetailReplacementPreset ? ("detail_replacement" as const) : ("sku_replacement" as const), - query: transportState.isDetailReplacementPreset - ? transportState.detailReplacementQueryInput.trim() - : transportState.skuReplacementQueryInput.trim(), + query: clampChatRequestText( + transportState.isDetailReplacementPreset + ? transportState.detailReplacementQueryInput.trim() + : transportState.skuReplacementQueryInput.trim(), + MAX_AGENT_MODE_QUERY_LENGTH + ), queryType: transportState.isDetailReplacementPreset ? ("spu" as const) : transportState.skuReplacementQueryType, @@ -1389,12 +1464,15 @@ export function AgentGenerationWorkspaceDarkV2({ ? transportState.detailReplacementReferenceCards : transportState.skuReplacementReferenceCards ) + .slice(0, MAX_AGENT_MODE_REFERENCE_COUNT) .filter((item) => Boolean(String(item.imageUrl || "").trim())) .map((item) => ({ - id: item.id, - label: item.label, + id: String(item.id || "").slice(0, 128), + label: String(item.label || "").slice(0, 255), imageUrl: String(item.imageUrl || "").trim(), - candidateId: item.candidateId || null, + candidateId: item.candidateId + ? String(item.candidateId).slice(0, 128) + : null, sortOrder: item.sortOrder ?? 0, assignedProductIndex: transportState.selectedProductAttachmentCount > 0 @@ -1411,22 +1489,23 @@ export function AgentGenerationWorkspaceDarkV2({ id: rawChatId, message: request.messages.at(-1), hiddenPrompt: - generationRequestSnapshotRef.current.hiddenPrompt || - hiddenPromptRef.current || - undefined, - agentPresetId: - transportState.selectedAgentPresetId || selectedAgentPresetId || undefined, - selectedChatModel: - transportState.selectedModelId !== "auto" - ? transportState.selectedModelId - : selectedAgentPreset?.defaultModelId || transportState.selectedModelId, + clampChatRequestText( + generationRequestSnapshotRef.current.hiddenPrompt || + hiddenPromptRef.current || + "", + MAX_CHAT_HIDDEN_PROMPT_LENGTH + ) || undefined, + agentPresetId: isValidChatUuid(resolvedAgentPresetId) + ? resolvedAgentPresetId + : undefined, + selectedChatModel: String(resolvedSelectedModelId || "auto").trim() || "auto", composerMode: selectedAgentPreset?.type === "image" ? ("image" as const) : ("agent" as const), selectedVisibilityType: "private", imageParams: transportState.imageParams, contextPayload: { source: "chat" as const, - projectId, + projectId: isValidChatUuid(projectId) ? projectId : undefined, }, agentModePayload: replacementModePayload, }, @@ -2486,10 +2565,38 @@ export function AgentGenerationWorkspaceDarkV2({ toast.error(`当前模式需要上传 ${requiredUploadCount} 张图片`); return; } + const resolvedModelId = + selectedModelId !== "auto" + ? selectedModelId + : selectedAgentPreset?.defaultModelId || selectedModelId; + if (!resolvedModelId) { + toast.error("当前智能体未配置模型"); + return; + } + if ( + resolvedModelId !== "auto" && + !selectableModels.some((model) => model.id === resolvedModelId) + ) { + toast.error("当前智能体绑定的模型不存在或已停用,请重新编辑智能体模型配置"); + return; + } const parts: Array<{ type: "text"; text: string } | { type: "file"; mediaType: string; name: string; url: string; assetId?: string }> = []; - attachments.forEach((attachment) => { + const sanitizedAttachments = attachments + .map((attachment) => sanitizeChatAttachment(attachment)) + .filter((attachment): attachment is PickedAttachment => Boolean(attachment)); + if (hasAgentUploadSlots && sanitizedAttachments.length < requiredUploadCount) { + toast.error(`当前模式需要上传 ${requiredUploadCount} 张有效图片`); + return; + } + sanitizedAttachments.forEach((attachment) => { if (!attachment) return; - parts.push({ type: "file", mediaType: attachment.contentType || "image/jpeg", name: attachment.name || "参考图", url: attachment.url, assetId: attachment.assetId }); + parts.push({ + type: "file", + mediaType: normalizeChatMediaType(attachment.contentType), + name: normalizeChatAttachmentName(attachment.name), + url: attachment.url, + assetId: isValidChatUuid(attachment.assetId) ? attachment.assetId : undefined, + }); }); const resolvedWorkflowContext = getLatestWorkflowContext?.() ?? workflowContext; let textContent = input.trim(); @@ -2499,6 +2606,7 @@ export function AgentGenerationWorkspaceDarkV2({ if (!textContent && selectedAgentPreset?.type === "text" && selectedAgentPreset) { textContent = `请按「${selectedAgentPreset.name}」输出内容`; } + textContent = clampChatRequestText(textContent, MAX_CHAT_TEXT_PART_LENGTH); if (textContent) { parts.push({ type: "text", text: textContent }); } @@ -2507,7 +2615,10 @@ export function AgentGenerationWorkspaceDarkV2({ return; } const nextHiddenPrompt = resolvedWorkflowContext?.promptText - ? `【工作流设定】\n${resolvedWorkflowContext.promptText}\n\n请严格遵循以上工作流设定输出内容,并保持整体结果一致。` + ? clampChatRequestText( + `【工作流设定】\n${resolvedWorkflowContext.promptText}\n\n请严格遵循以上工作流设定输出内容,并保持整体结果一致。`, + MAX_CHAT_HIDDEN_PROMPT_LENGTH + ) : null; hiddenPromptRef.current = nextHiddenPrompt; generationRequestSnapshotRef.current = { @@ -2553,6 +2664,8 @@ export function AgentGenerationWorkspaceDarkV2({ requiredUploadCount, selectedAgentPreset, selectedAgentPresetId, + selectableModels, + selectedModelId, sendMessage, setPendingGeneration, skuReplacementReferenceCards.length,