ai-chatbot/components/canvas/chat-panel/canvas-chat-session.ts

169 lines
5.2 KiB
TypeScript

import type { Attachment, ChatMessage } from "@/lib/types";
import { isOpenClawChatFileAttachment } from "@/lib/openclaw-chat/media-attachments";
export const SESSION_TITLE_MAX_LEN = 18;
export const MAX_CHAT_TEXT_PART_LENGTH = 2000;
export const MAX_CHAT_HIDDEN_PROMPT_LENGTH = 4000;
export const MAX_CHAT_AUTO_CONTEXT_LENGTH = 4000;
export 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",
"video/mp4",
"video/quicktime",
"video/webm",
"video/x-matroska",
"application/zip",
"application/x-zip-compressed",
]);
const GENERIC_CANVAS_PROMPT_SET = new Set([
"对话生成",
"图片生成",
"AI生成",
"智能生成",
"自动生成",
]);
export interface PickedObjectAttachment extends Attachment {
pickedObject?: {
objectName: string;
objectPosition: string;
clickX: number;
clickY: number;
boundingBox: { x: number; y: number; width: number; height: number };
};
}
export function toRawCanvasChatId(value: string): string {
return value.startsWith("canvas-") ? value.replace("canvas-", "") : value;
}
export function isValidChatUuid(value?: string | null): value is string {
return Boolean(value && CHAT_UUID_REGEX.test(String(value).trim()));
}
export 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;
}
}
export function clampChatRequestText(
value?: string | null,
maxLength = MAX_CHAT_TEXT_PART_LENGTH
): string {
return String(value || "").slice(0, maxLength);
}
export function normalizeChatAttachmentName(value?: string | null): string {
const normalized = String(value || "").trim() || "参考图";
return normalized.slice(0, MAX_CHAT_ATTACHMENT_NAME_LENGTH);
}
export 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;
}
if (isOpenClawChatFileAttachment("", normalized)) {
return normalized;
}
return "image/jpeg";
}
export function sanitizeChatAttachment(
attachment?: PickedObjectAttachment | null
): PickedObjectAttachment | null {
if (!attachment || !isValidChatHttpUrl(attachment.url)) {
return null;
}
const width = Number(attachment.width);
const height = Number(attachment.height);
const safeWidth = Number.isFinite(width) && width > 0 ? Math.round(width) : undefined;
const safeHeight = Number.isFinite(height) && height > 0 ? Math.round(height) : undefined;
return {
...attachment,
url: attachment.url,
name: normalizeChatAttachmentName(attachment.name),
contentType: normalizeChatMediaType(attachment.contentType),
assetId: isValidChatUuid(attachment.assetId) ? attachment.assetId : undefined,
width: safeWidth,
height: safeHeight,
};
}
export function getSessionStorageKey(canvasId: string, scope: "canvas" | "agent"): string {
return `canvas-chat-sessions:${scope}:${canvasId}`;
}
export function getSessionRootStorageKey(canvasId: string, scope: "canvas" | "agent"): string {
return `canvas-chat-root:${scope}:${canvasId}`;
}
export function extractUserTextPrompt(message: ChatMessage): string {
const parts = (message as any)?.parts || [];
const textFromParts = parts
.filter((part: any) => part?.type === "text" && typeof part.text === "string")
.map((part: any) =>
part.text
.replace(
/【注意:图片中有标注(箭头、圆圈、文字等)标记的区域是需要重点关注或修改的部分,请根据标注进行处理】\n?/g,
""
)
.trim()
)
.filter(Boolean)
.join("\n")
.trim();
if (textFromParts) return textFromParts;
const content = (message as any)?.content;
return typeof content === "string" ? content.trim() : "";
}
export function findNearestUserPrompt(messages: ChatMessage[], assistantIndex: number): string {
for (let i = assistantIndex - 1; i >= 0; i -= 1) {
const candidate = messages[i];
if (candidate?.role !== "user") continue;
const prompt = extractUserTextPrompt(candidate);
if (prompt) return prompt;
}
return "";
}
export function isGenericCanvasPrompt(prompt: unknown): boolean {
if (typeof prompt !== "string") return true;
const normalized = prompt.trim();
if (!normalized) return true;
return GENERIC_CANVAS_PROMPT_SET.has(normalized);
}
export function deriveSessionTitleFromMessages(messages: ChatMessage[]): string {
const firstUserMessage = messages.find((message) => message.role === "user");
if (!firstUserMessage) return "";
const prompt = extractUserTextPrompt(firstUserMessage).replace(/\s+/g, " ").trim();
if (!prompt) return "";
return prompt.length > SESSION_TITLE_MAX_LEN
? `${prompt.slice(0, SESSION_TITLE_MAX_LEN)}...`
: prompt;
}