const MINDSPACE_ASSET_ID_RE = /\/mindspace\/v1\/assets\/([^/?#]+)/i; const PUBLIC_IMAGE_PATH_RE = /\/public\/images\/([^?#\s"'<>]+)/i; const IMAGE_URL_LINE_RE = /^\[图片\d+]: (.+)$/; const IMAGE_URL_LINES_RE = /\n*\[图片\d+]: [^\n]+/g; const TKMIND_VISION_NOTE_RE = /\n*【TKMind 图片分析结果[\s\S]*$/; export function extractImageAssetKey(url) { const value = String(url ?? '').trim(); if (!value) return ''; const assetMatch = value.match(MINDSPACE_ASSET_ID_RE); if (assetMatch?.[1]) return `asset:${decodeURIComponent(assetMatch[1])}`; const publicMatch = value.match(PUBLIC_IMAGE_PATH_RE); if (publicMatch?.[1]) return `public:${decodeURIComponent(publicMatch[1])}`; try { const parsed = value.startsWith('http://') || value.startsWith('https://') ? new URL(value) : new URL(value, 'http://local'); return `path:${parsed.pathname}${parsed.search}`; } catch { return `raw:${value}`; } } export function dedupeImageUrlsByAssetKey(urls) { const seen = new Set(); const result = []; for (const raw of urls) { const url = String(raw ?? '').trim(); if (!url) continue; const key = extractImageAssetKey(url); if (seen.has(key)) continue; seen.add(key); result.push(url); } return result; } export function collectImageUrlsFromMessageContent(content) { const urls = []; if (!Array.isArray(content)) return urls; for (const item of content) { if (item?.type === 'image_url' && item.image_url?.url) { urls.push(String(item.image_url.url).trim()); continue; } if (item?.type !== 'text' || typeof item.text !== 'string') continue; for (const line of item.text.split('\n')) { const match = line.trim().match(IMAGE_URL_LINE_RE); if (match?.[1]) urls.push(match[1].trim()); } } return urls; } /** * Current-turn image URLs: metadata.imageUrls is authoritative when present. */ export function extractCurrentTurnImageUrls(userMessage) { const fromMetadata = Array.isArray(userMessage?.metadata?.imageUrls) ? userMessage.metadata.imageUrls .filter((url) => typeof url === 'string' && url.trim()) .map((url) => url.trim()) : []; if (fromMetadata.length > 0) { return dedupeImageUrlsByAssetKey(fromMetadata); } return dedupeImageUrlsByAssetKey(collectImageUrlsFromMessageContent(userMessage?.content)); } function stripAgentImageText(text) { return String(text ?? '') .replace(TKMIND_VISION_NOTE_RE, '') .replace(IMAGE_URL_LINES_RE, '') .trim(); } /** * Remove image attachments from a persisted user message so later turns cannot reuse them. * UI displayText / previewImageUrls are preserved for chat history rendering. */ export function scrubUserMessageImageAttachments(message) { if (!message || message.role !== 'user') return { message, changed: false }; const metadata = message.metadata && typeof message.metadata === 'object' && !Array.isArray(message.metadata) ? { ...message.metadata } : {}; const hadImageMetadata = Array.isArray(metadata.imageUrls) && metadata.imageUrls.length > 0 || Array.isArray(metadata.previewImageUrls) && metadata.previewImageUrls.length > 0; if (Array.isArray(metadata.imageUrls) && metadata.imageUrls.length > 0) { metadata.archivedImageUrls = metadata.imageUrls; } if (Array.isArray(metadata.previewImageUrls) && metadata.previewImageUrls.length > 0) { metadata.archivedPreviewImageUrls = metadata.previewImageUrls; } delete metadata.imageUrls; delete metadata.previewImageUrls; let contentChanged = false; const content = Array.isArray(message.content) ? message.content.map((item) => { if (item?.type !== 'text' || typeof item.text !== 'string') return item; const nextText = stripAgentImageText(item.text); if (nextText === item.text) return item; contentChanged = true; return nextText ? { ...item, text: nextText } : null; }).filter(Boolean) : message.content; const displayText = typeof metadata.displayText === 'string' ? metadata.displayText : null; const changed = hadImageMetadata || contentChanged; if (!changed) return { message, changed: false }; return { message: { ...message, content, metadata, ...(displayText != null ? {} : {}), }, changed: true, }; } export function scrubConversationHistoricalImageAttachments(conversation, activeMessageId) { const activeId = String(activeMessageId ?? '').trim(); if (!Array.isArray(conversation) || !activeId) { return { conversation, changed: false }; } let changed = false; const nextConversation = conversation.map((message) => { if (message?.role !== 'user') return message; if (String(message?.id ?? '').trim() === activeId) return message; const scrubbed = scrubUserMessageImageAttachments(message); if (scrubbed.changed) changed = true; return scrubbed.message; }); return { conversation: nextConversation, changed, }; } export function buildCurrentTurnImageScopeNote(imageItems) { if (!Array.isArray(imageItems) || imageItems.length === 0) return ''; const lines = imageItems.map((item, index) => { const assetKey = extractImageAssetKey(item.rawUrl ?? item.embedUrl ?? ''); const assetId = assetKey.startsWith('asset:') ? assetKey.slice('asset:'.length) : `图片${index + 1}`; return ` ${index + 1}. asset=${assetId} 页面嵌入=${item.embedUrl ?? item.rawUrl ?? ''}`; }); return ( '【本轮图片主题(硬性)】\n' + `- 本轮用户仅上传 ${imageItems.length} 张图片;每次上传都是独立主题,不得与历史轮次混用。\n` + `- 用户说「这个图片 / 根据这张图 / 这张」时,只能使用下列本轮附件:\n${lines.join('\n')}\n` + '- 对话历史中更早消息里出现过的图片、asset、 路径均已作废,禁止用于本轮任务。\n\n' ); } export const chatImageTurnScopeInternals = { IMAGE_URL_LINE_RE, TKMIND_VISION_NOTE_RE, };