Fix agent mode chat request sanitization
This commit is contained in:
parent
17dcfcd55d
commit
d133af4cbe
@ -1340,11 +1340,75 @@ const DETAIL_GENERATION_MODE_OPTIONS: Array<{
|
|||||||
];
|
];
|
||||||
|
|
||||||
const SESSION_TITLE_MAX_LEN = 18;
|
const SESSION_TITLE_MAX_LEN = 18;
|
||||||
|
const MAX_CHAT_TEXT_PART_LENGTH = 2000;
|
||||||
|
const MAX_CHAT_HIDDEN_PROMPT_LENGTH = 4000;
|
||||||
|
const MAX_CHAT_AUTO_CONTEXT_LENGTH = 4000;
|
||||||
|
const MAX_CHAT_ATTACHMENT_NAME_LENGTH = 100;
|
||||||
|
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 toRawCanvasChatId(value: string): string {
|
function toRawCanvasChatId(value: string): string {
|
||||||
return value.startsWith("canvas-") ? value.replace("canvas-", "") : value;
|
return value.startsWith("canvas-") ? value.replace("canvas-", "") : value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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?: PickedObjectAttachment | null,
|
||||||
|
): PickedObjectAttachment | 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function getSessionStorageKey(canvasId: string, scope: "canvas" | "agent"): string {
|
function getSessionStorageKey(canvasId: string, scope: "canvas" | "agent"): string {
|
||||||
return `canvas-chat-sessions:${scope}:${canvasId}`;
|
return `canvas-chat-sessions:${scope}:${canvasId}`;
|
||||||
}
|
}
|
||||||
@ -4637,9 +4701,11 @@ export function CanvasChatPanel({
|
|||||||
if (!launchPayload?.autoSend) return;
|
if (!launchPayload?.autoSend) return;
|
||||||
if (!selectedModelId || status !== "ready") return;
|
if (!selectedModelId || status !== "ready") return;
|
||||||
|
|
||||||
const launchAttachments = launchPayload.attachments || [];
|
const launchAttachments = (launchPayload.attachments || [])
|
||||||
|
.map((attachment) => sanitizeChatAttachment(attachment as PickedObjectAttachment))
|
||||||
|
.filter((attachment): attachment is PickedObjectAttachment => Boolean(attachment));
|
||||||
const textContent =
|
const textContent =
|
||||||
launchPayload.prompt?.trim() ||
|
clampChatRequestText(launchPayload.prompt?.trim() || "", MAX_CHAT_TEXT_PART_LENGTH) ||
|
||||||
(launchAttachments.length > 0 ? "请基于这些图片继续生成设计方案" : "");
|
(launchAttachments.length > 0 ? "请基于这些图片继续生成设计方案" : "");
|
||||||
|
|
||||||
if (!textContent && launchAttachments.length === 0) {
|
if (!textContent && launchAttachments.length === 0) {
|
||||||
@ -4649,10 +4715,10 @@ export function CanvasChatPanel({
|
|||||||
|
|
||||||
const parts: any[] = launchAttachments.map((attachment) => ({
|
const parts: any[] = launchAttachments.map((attachment) => ({
|
||||||
type: "file" as const,
|
type: "file" as const,
|
||||||
mediaType: attachment.contentType || "image/jpeg",
|
mediaType: normalizeChatMediaType(attachment.contentType),
|
||||||
name: attachment.name || "参考图",
|
name: normalizeChatAttachmentName(attachment.name),
|
||||||
url: attachment.url,
|
url: attachment.url,
|
||||||
assetId: attachment.assetId,
|
assetId: isValidChatUuid(attachment.assetId) ? attachment.assetId : undefined,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (textContent) {
|
if (textContent) {
|
||||||
@ -5193,36 +5259,40 @@ export function CanvasChatPanel({
|
|||||||
const imageNodes = nodes.filter(
|
const imageNodes = nodes.filter(
|
||||||
(node) =>
|
(node) =>
|
||||||
node.type === "image" &&
|
node.type === "image" &&
|
||||||
typeof (node.data as any)?.src === "string" &&
|
isValidChatHttpUrl(String((node.data as any)?.src || "").trim()),
|
||||||
String((node.data as any).src).trim().length > 0,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (imageNodes.length === 0) return undefined;
|
if (imageNodes.length === 0) return undefined;
|
||||||
|
|
||||||
const effectiveComposerMode = activeComposerModeRef.current || activeComposerMode;
|
const effectiveComposerMode = activeComposerModeRef.current || activeComposerMode;
|
||||||
const shouldIsolateDirectModeContext = effectiveComposerMode !== "agent";
|
const shouldIsolateDirectModeContext = effectiveComposerMode !== "agent";
|
||||||
if (shouldIsolateDirectModeContext && selectedImageNodes.length === 0) {
|
const selectedImageNodesForContext = selectedImageNodes.filter((node) =>
|
||||||
|
isValidChatHttpUrl(String((node.data as any)?.src || "").trim())
|
||||||
|
);
|
||||||
|
|
||||||
|
if (shouldIsolateDirectModeContext && selectedImageNodesForContext.length === 0) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_CONTEXT_IMAGES = 12;
|
const MAX_CONTEXT_IMAGES = 12;
|
||||||
const selectedIds = new Set(selectedImageNodes.map((node) => node.id));
|
const selectedContextNodes = selectedImageNodesForContext.slice(0, 20);
|
||||||
|
const selectedIds = new Set(selectedContextNodes.map((node) => node.id));
|
||||||
const prioritizedNodes =
|
const prioritizedNodes =
|
||||||
shouldIsolateDirectModeContext
|
shouldIsolateDirectModeContext
|
||||||
? selectedImageNodes
|
? selectedContextNodes
|
||||||
: selectedImageNodes.length > 0
|
: selectedContextNodes.length > 0
|
||||||
? [
|
? [
|
||||||
...selectedImageNodes,
|
...selectedContextNodes,
|
||||||
...imageNodes.filter((node) => !selectedIds.has(node.id)),
|
...imageNodes.filter((node) => !selectedIds.has(node.id)),
|
||||||
]
|
]
|
||||||
: imageNodes;
|
: imageNodes;
|
||||||
const contextNodes = prioritizedNodes.slice(0, MAX_CONTEXT_IMAGES);
|
const contextNodes = prioritizedNodes.slice(0, MAX_CONTEXT_IMAGES);
|
||||||
const selectedAssetIds = Array.from(
|
const selectedAssetIds = Array.from(
|
||||||
new Set(
|
new Set(
|
||||||
selectedImageNodes
|
selectedContextNodes
|
||||||
.map((node) => {
|
.map((node) => {
|
||||||
const assetId = (node.data as any)?.assetId;
|
const assetId = (node.data as any)?.assetId;
|
||||||
return typeof assetId === "string" && assetId.trim() ? assetId.trim() : null;
|
return isValidChatUuid(assetId) ? assetId.trim() : null;
|
||||||
})
|
})
|
||||||
.filter((value): value is string => Boolean(value)),
|
.filter((value): value is string => Boolean(value)),
|
||||||
),
|
),
|
||||||
@ -5230,17 +5300,16 @@ export function CanvasChatPanel({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
source: "canvas" as const,
|
source: "canvas" as const,
|
||||||
projectId: canvasId || undefined,
|
projectId: isValidChatUuid(canvasId) ? canvasId : undefined,
|
||||||
selectedNodeIds: selectedImageNodes.map((node) => node.id),
|
selectedNodeIds: selectedContextNodes.map((node) => node.id).slice(0, 20),
|
||||||
selectedAssetIds,
|
selectedAssetIds,
|
||||||
focusPolicy: selectedImageNodes.length > 0 ? ("selected" as const) : ("auto" as const),
|
focusPolicy: selectedContextNodes.length > 0 ? ("selected" as const) : ("auto" as const),
|
||||||
canvasImageNodes: contextNodes.map((node) => ({
|
canvasImageNodes: contextNodes.map((node) => ({
|
||||||
nodeId: node.id,
|
nodeId: node.id,
|
||||||
assetId:
|
assetId: isValidChatUuid((node.data as any)?.assetId)
|
||||||
typeof (node.data as any)?.assetId === "string" && (node.data as any).assetId.trim()
|
? String((node.data as any).assetId).trim()
|
||||||
? String((node.data as any).assetId).trim()
|
: undefined,
|
||||||
: undefined,
|
url: String((node.data as any)?.src || "").trim(),
|
||||||
url: String((node.data as any)?.src || ""),
|
|
||||||
title:
|
title:
|
||||||
typeof (node.data as any)?.title === "string"
|
typeof (node.data as any)?.title === "string"
|
||||||
? String((node.data as any).title).slice(0, 256)
|
? String((node.data as any).title).slice(0, 256)
|
||||||
@ -7477,7 +7546,9 @@ export function CanvasChatPanel({
|
|||||||
// 构建 parts 数组:文件在前,文本在后(与 multimodal-input 一致)
|
// 构建 parts 数组:文件在前,文本在后(与 multimodal-input 一致)
|
||||||
const parts: any[] = [];
|
const parts: any[] = [];
|
||||||
|
|
||||||
let effectiveAttachments = [...attachments];
|
let effectiveAttachments = attachments
|
||||||
|
.map((attachment) => sanitizeChatAttachment(attachment))
|
||||||
|
.filter((attachment): attachment is PickedObjectAttachment => Boolean(attachment));
|
||||||
if (hasAgentUploadSlots) {
|
if (hasAgentUploadSlots) {
|
||||||
if (effectiveAttachments.length > agentRequiredUploadCount) {
|
if (effectiveAttachments.length > agentRequiredUploadCount) {
|
||||||
effectiveAttachments = effectiveAttachments.slice(0, agentRequiredUploadCount);
|
effectiveAttachments = effectiveAttachments.slice(0, agentRequiredUploadCount);
|
||||||
@ -7521,10 +7592,10 @@ export function CanvasChatPanel({
|
|||||||
const att = a as PickedObjectAttachment;
|
const att = a as PickedObjectAttachment;
|
||||||
parts.push({
|
parts.push({
|
||||||
type: "file" as const,
|
type: "file" as const,
|
||||||
mediaType: a.contentType,
|
mediaType: normalizeChatMediaType(a.contentType),
|
||||||
name: a.name,
|
name: normalizeChatAttachmentName(a.name),
|
||||||
url: a.url,
|
url: a.url,
|
||||||
assetId: a.assetId,
|
assetId: isValidChatUuid(a.assetId) ? a.assetId : undefined,
|
||||||
hasAnnotations: hasAnnotatedImages, // 🆕 标记图片是否有标注
|
hasAnnotations: hasAnnotatedImages, // 🆕 标记图片是否有标注
|
||||||
pickedObject: att.pickedObject, // 🆕 包含物品信息
|
pickedObject: att.pickedObject, // 🆕 包含物品信息
|
||||||
});
|
});
|
||||||
@ -7573,6 +7644,7 @@ export function CanvasChatPanel({
|
|||||||
if (!textContent && selectedSkillTag) {
|
if (!textContent && selectedSkillTag) {
|
||||||
textContent = `请按「${selectedSkillTag.name}」标签的要求处理。`;
|
textContent = `请按「${selectedSkillTag.name}」标签的要求处理。`;
|
||||||
}
|
}
|
||||||
|
textContent = clampChatRequestText(textContent, MAX_CHAT_TEXT_PART_LENGTH);
|
||||||
|
|
||||||
// 🆕 隐藏提示词(不在 UI 中展示)
|
// 🆕 隐藏提示词(不在 UI 中展示)
|
||||||
const hiddenSegments: string[] = [];
|
const hiddenSegments: string[] = [];
|
||||||
@ -7625,10 +7697,12 @@ export function CanvasChatPanel({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
const hiddenPrompt = hiddenSegments.join('\n\n').trim();
|
const hiddenPrompt = hiddenSegments.join('\n\n').trim();
|
||||||
hiddenPromptRef.current = hiddenPrompt || null;
|
hiddenPromptRef.current =
|
||||||
|
clampChatRequestText(hiddenPrompt, MAX_CHAT_HIDDEN_PROMPT_LENGTH) || null;
|
||||||
|
|
||||||
const autoContext = buildAutoContext();
|
const autoContext = buildAutoContext();
|
||||||
autoContextRef.current = autoContext ? autoContext : null;
|
autoContextRef.current =
|
||||||
|
clampChatRequestText(autoContext, MAX_CHAT_AUTO_CONTEXT_LENGTH) || null;
|
||||||
|
|
||||||
if (textContent) {
|
if (textContent) {
|
||||||
parts.push({ type: "text" as const, text: textContent });
|
parts.push({ type: "text" as const, text: textContent });
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user