refactor: extract canvas chat session helpers
This commit is contained in:
parent
36412dee83
commit
a8f5dff6f8
@ -75,7 +75,7 @@ import {
|
||||
getImageModelCreditDiscountLabel,
|
||||
} from "@/lib/ai/image-model-pricing";
|
||||
import { getVideoToolOptionsForMode } from "@/lib/ai/video-tool-capability";
|
||||
import type { Attachment, ChatMessage } from "@/lib/types";
|
||||
import type { ChatMessage } from "@/lib/types";
|
||||
import type {
|
||||
AgentPreset,
|
||||
AgentWorkflowComposerContext,
|
||||
@ -106,6 +106,25 @@ import {
|
||||
type CanvasToolbarConfig,
|
||||
type CanvasToolbarItemKey,
|
||||
} from "@/lib/canvas-toolbar-config";
|
||||
import {
|
||||
clampChatRequestText,
|
||||
deriveSessionTitleFromMessages,
|
||||
extractUserTextPrompt,
|
||||
findNearestUserPrompt,
|
||||
getSessionRootStorageKey,
|
||||
getSessionStorageKey,
|
||||
isGenericCanvasPrompt,
|
||||
isValidChatHttpUrl,
|
||||
isValidChatUuid,
|
||||
MAX_CHAT_AUTO_CONTEXT_LENGTH,
|
||||
MAX_CHAT_HIDDEN_PROMPT_LENGTH,
|
||||
MAX_CHAT_TEXT_PART_LENGTH,
|
||||
normalizeChatAttachmentName,
|
||||
normalizeChatMediaType,
|
||||
type PickedObjectAttachment,
|
||||
sanitizeChatAttachment,
|
||||
toRawCanvasChatId,
|
||||
} from "@/components/canvas/chat-panel/canvas-chat-session";
|
||||
|
||||
// 标签悬停预览信息
|
||||
interface TagPreviewInfo {
|
||||
@ -1365,64 +1384,6 @@ function getVideoImageLimit(
|
||||
}
|
||||
}
|
||||
|
||||
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() : "";
|
||||
}
|
||||
|
||||
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 "";
|
||||
}
|
||||
|
||||
const GENERIC_CANVAS_PROMPT_SET = new Set([
|
||||
"对话生成",
|
||||
"图片生成",
|
||||
"AI生成",
|
||||
"智能生成",
|
||||
"自动生成",
|
||||
]);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// 🆕 拾取物品的附件类型扩展
|
||||
interface PickedObjectAttachment extends Attachment {
|
||||
pickedObject?: {
|
||||
objectName: string;
|
||||
objectPosition: string;
|
||||
clickX: number;
|
||||
clickY: number;
|
||||
boundingBox: { x: number; y: number; width: number; height: number };
|
||||
};
|
||||
}
|
||||
|
||||
type MessageVote = {
|
||||
chatId: string;
|
||||
messageId: string;
|
||||
@ -1640,95 +1601,6 @@ const DETAIL_GENERATION_MODE_OPTIONS: Array<{
|
||||
},
|
||||
];
|
||||
|
||||
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 {
|
||||
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 {
|
||||
return `canvas-chat-sessions:${scope}:${canvasId}`;
|
||||
}
|
||||
|
||||
function getSessionRootStorageKey(canvasId: string, scope: "canvas" | "agent"): string {
|
||||
return `canvas-chat-root:${scope}:${canvasId}`;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function extractDetailFrameCount(text: string): number {
|
||||
const normalized = text.replace(/\s+/g, "");
|
||||
const matched = normalized.match(/(\d{1,2})(?:张|屏|页)/);
|
||||
|
||||
152
components/canvas/chat-panel/canvas-chat-session.ts
Normal file
152
components/canvas/chat-panel/canvas-chat-session.ts
Normal file
@ -0,0 +1,152 @@
|
||||
import type { Attachment, ChatMessage } from "@/lib/types";
|
||||
|
||||
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",
|
||||
]);
|
||||
|
||||
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;
|
||||
}
|
||||
return "image/jpeg";
|
||||
}
|
||||
|
||||
export 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,
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user