mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-19 18:39:32 +08:00
fix(assistant): 拦截本地文件路径粘贴/拖拽,清洗消息中的本地路径引用 (#226)
用户将 C:\...\image.png 等本地路径粘贴到晴辰助手输入框,消息发送到 OpenAI 兼容 API 时触发 "Only base64, http or https URLs are supported" 错误;由于历史上下文包含坏消息,后续对话会继续报错形成循环。 三处修复: 1. paste 事件:如果剪贴板是纯文本且为本地路径(Windows/Unix/file://), 阻止默认粘贴并 toast 提示用户 2. drop 事件:若拖入的是路径文本(无 files),同样拦截并提示 3. buildMessageContent:调用 sanitizeUserTextForApi,把  这类 markdown 图片替换为占位文本 "[本地文件(已忽略)]", 让已经输入路径的老会话也能自愈,不再循环报错 路径识别支持: - Windows 绝对路径(C:\... / D:/...) - macOS/Linux 常见路径前缀(/Users /home /mnt /media /opt /tmp /var /root) - file:// URL 翻译覆盖 11 种语言(zh-CN/zh-TW/en/ja/ko/vi/es/pt/ru/fr/de)。 Refs: #226
This commit is contained in:
@@ -67,6 +67,20 @@ export default {
|
||||
imageTooLarge: _('图片太大,请选择小于 10MB 的图片', 'Image too large, please select one under 10MB', '圖片太大,請選擇小於 10MB 的圖片'),
|
||||
imageMessage: _('(图片消息)', '(image message)', '(圖片訊息)'),
|
||||
image: _('图片', 'Image', '圖片'),
|
||||
localPathBlocked: _(
|
||||
'请直接粘贴或拖拽图片本身,而不是本地文件路径',
|
||||
'Please paste or drag the image itself, not a local file path',
|
||||
'請直接貼上或拖曳圖片本身,而不是本地檔案路徑',
|
||||
'画像自体を貼り付けるかドラッグしてください(ローカルパスは不可)',
|
||||
'이미지 자체를 붙여넣거나 드래그하세요 (로컬 경로 불가)',
|
||||
'Vui lòng dán hoặc kéo hình ảnh, không phải đường dẫn tệp cục bộ',
|
||||
'Pegue o arrastre la imagen en sí, no una ruta de archivo local',
|
||||
'Cole ou arraste a imagem em si, não um caminho de arquivo local',
|
||||
'Пожалуйста, вставьте или перетащите само изображение, а не локальный путь',
|
||||
'Veuillez coller ou faire glisser l\'image elle-même, pas un chemin local',
|
||||
'Bitte fügen Sie das Bild selbst ein oder ziehen Sie es, keinen lokalen Pfad',
|
||||
),
|
||||
localPathSanitized: _('[本地文件(已忽略,请直接上传图片)]', '[local file (ignored — please upload the image)]', '[本地檔案(已忽略,請直接上傳圖片)]'),
|
||||
newSession: _('新建会话', 'New Session', '新建對話', '新しいセッション', '새 세션', 'Phiên mới', 'Nueva sesión', 'Nova sessão', 'Новая сессия', 'Nouvelle session', 'Neue Sitzung'),
|
||||
deleteSession: _('删除会话', 'Delete Session', '刪除對話', 'セッション削除', '세션 삭제', 'Xóa phiên', 'Eliminar sesión', 'Excluir sessão', 'Удалить сессию', 'Supprimer la session', 'Sitzung löschen'),
|
||||
noSessions: _('暂无会话', 'No sessions', '暫無對話', 'セッションなし', '세션 없음', 'Không có phiên', 'Sin sesiones', 'Sem sessões', 'Нет сессий', 'Aucune session', 'Keine Sitzungen'),
|
||||
|
||||
@@ -1353,6 +1353,30 @@ function processQueue() {
|
||||
sendMessageDirect(next.text)
|
||||
}
|
||||
|
||||
// ── 本地文件路径检测(Fix #226)──
|
||||
// 用于拦截用户意外粘贴/拖拽本地文件路径(而非图片内容本身)的场景
|
||||
// 例如:C:\Users\x\img.png、/Users/x/img.png、file:///C:/img.png 等
|
||||
// 这类字符串发送到 LLM 会触发 "Only base64/http/https URLs are supported" 错误
|
||||
const LOCAL_PATH_PREFIX_RE = /^(?:[a-zA-Z]:[\\/]|\/(?:Users|home|mnt|media|opt|tmp|var|root)\/|file:\/\/)/i
|
||||
// 匹配 markdown 图片语法中的本地路径:、
|
||||
const LOCAL_PATH_MD_IMG_RE = /!\[[^\]]*\]\((\s*(?:[a-zA-Z]:[\\/]|\/(?:Users|home|mnt|media|opt|tmp|var|root)\/|file:\/\/)[^)]+)\)/gi
|
||||
|
||||
function isLocalPathText(text) {
|
||||
if (!text) return false
|
||||
const trimmed = String(text).trim()
|
||||
if (!trimmed) return false
|
||||
// 多行粘贴:首行若匹配本地路径即视为路径
|
||||
const firstLine = trimmed.split(/\r?\n/)[0].trim()
|
||||
return LOCAL_PATH_PREFIX_RE.test(firstLine)
|
||||
}
|
||||
|
||||
// 在发送给 LLM 之前清洗用户消息中的本地路径 markdown 图片引用
|
||||
// LLM 不能访问本地文件,把路径替换为占位文本避免 API 报错
|
||||
function sanitizeUserTextForApi(text) {
|
||||
if (!text || typeof text !== 'string') return text
|
||||
return text.replace(LOCAL_PATH_MD_IMG_RE, t('assistant.localPathSanitized'))
|
||||
}
|
||||
|
||||
// ── 图片附件 ──
|
||||
const MAX_IMAGE_SIZE = 4 * 1024 * 1024 // 4MB
|
||||
const MAX_IMAGE_DIM = 2048 // 最大边长
|
||||
@@ -1429,9 +1453,11 @@ function clearPendingImages() {
|
||||
|
||||
// 构建多模态消息 content
|
||||
function buildMessageContent(text, images) {
|
||||
if (!images || images.length === 0) return text
|
||||
// Fix #226: 清洗 markdown 图片语法中的本地路径(LLM 无法访问)
|
||||
const sanitizedText = sanitizeUserTextForApi(text)
|
||||
if (!images || images.length === 0) return sanitizedText
|
||||
const parts = []
|
||||
if (text) parts.push({ type: 'text', text })
|
||||
if (sanitizedText) parts.push({ type: 'text', text: sanitizedText })
|
||||
for (const img of images) {
|
||||
parts.push({
|
||||
type: 'image_url',
|
||||
@@ -4434,7 +4460,13 @@ export async function render() {
|
||||
hasImage = true
|
||||
}
|
||||
}
|
||||
if (hasImage) e.preventDefault()
|
||||
if (hasImage) { e.preventDefault(); return }
|
||||
// Fix #226: 拦截纯文本的本地文件路径粘贴(LLM 无法访问本地文件)
|
||||
const pastedText = e.clipboardData?.getData('text/plain') || ''
|
||||
if (isLocalPathText(pastedText)) {
|
||||
e.preventDefault()
|
||||
toast(t('assistant.localPathBlocked'), 'warn')
|
||||
}
|
||||
})
|
||||
|
||||
// 拖拽图片
|
||||
@@ -4449,7 +4481,16 @@ export async function render() {
|
||||
mainEl.addEventListener('drop', (e) => {
|
||||
e.preventDefault()
|
||||
mainEl.classList.remove('ast-drag-over')
|
||||
for (const file of e.dataTransfer.files) addImageFromFile(file)
|
||||
// Fix #226: 拖拽路径文本(而非图片文件)时拦截
|
||||
const droppedFiles = e.dataTransfer?.files
|
||||
if (!droppedFiles || droppedFiles.length === 0) {
|
||||
const droppedText = e.dataTransfer?.getData('text/plain') || e.dataTransfer?.getData('text/uri-list') || ''
|
||||
if (isLocalPathText(droppedText)) {
|
||||
toast(t('assistant.localPathBlocked'), 'warn')
|
||||
}
|
||||
return
|
||||
}
|
||||
for (const file of droppedFiles) addImageFromFile(file)
|
||||
})
|
||||
|
||||
// 图片预览删除
|
||||
|
||||
Reference in New Issue
Block a user