Fix agent workspace chat request validation
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-27 11:18:11 +08:00
parent d133af4cbe
commit 25b20e1730

View File

@ -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,