diff --git a/common/js/media-upload.js b/common/js/media-upload.js index 1649855..e65c0d4 100644 --- a/common/js/media-upload.js +++ b/common/js/media-upload.js @@ -25,6 +25,10 @@ import store from '@/store/index.js' const SIZE_LIMIT_PHP_PASSTHROUGH = 2 * 1024 * 1024 // 2MB const SIZE_LIMIT_H5_COMPRESS = 3 * 1024 * 1024 // 3MB (从 10MB 下调,移动端 H5 体验更流畅) +// PR-FRONTEND-MEDIA-INLINE-BASE64 (2026-09-16): WB Agent J7 inline base64 上限 5MB +// 与 backend WbAgentOsClient::INLINE_MEDIA_LIMIT = 5242880 对齐 +// 超过这个 size 后 buildMediaBlocks 会改走 URL fetch 路径, 上限失效 +const SIZE_LIMIT_INLINE_BASE64 = 5 * 1024 * 1024 // 5MB (与后端 INLINE_MEDIA_LIMIT 对齐) // PR-FRONTEND-MEDIA-UPLOAD-FIX (2026-09-16): 现代格式 mime 同步后端 guessMimeFromPath // STS 直传绕过后端 mime 推断, 前端必须自带, 否则附件 mime 降级 application/octet-stream @@ -428,6 +432,89 @@ async function pathToFile(filePath, kind) { // #endif } +/** + * PR-FRONTEND-MEDIA-INLINE-BASE64 (2026-09-16): 把附件原文件读成 base64, + * 用于 ai-chat-message.vue sendMessage 给 attachment 加 `data` 字段, + * 让 backend 走 WbAgentOsClient::buildMediaBlocks 路径 ① (inline base64) + * 而不是路径 ②/③ (URL fetch / resource_link). + * + * 为什么必要: 之前 PR-FRONTEND-MEDIA-UPLOAD-FIX 已让上传链路 (uni.compressImage + * → backend Upload.php) 返回了公网 URL, 但此 URL 是 backend Upload.php::buildPublicUrl + * 用 request()->domain() 拼出来的 (例如 http://192.168.x.x:8050/upload/...). + * 买家端 AI chat 把这个 URL 透给 backend, backend MediaGuard::urlAllowed + * 把 192.168.x.x 当内网 IP (FILTER_FLAG_NO_PRIV_RANGE) 拒绝, 附件被静默丢弃. + * + * 修复: 同步给 attachment 加 data: 字段. + * - backend MessageMedia::normalizeInput 直接透传 data (line 207-208) + * - backend MediaGuard::check 看到 data 非空就无条件放行, 不参与 URL 校验 (line 137-140) + * - backend WbAgentOsClient::buildMediaBlocks 路径 ① 优先 data + image/audio → 直接 base64 内联 + * 全链路 bypass URL 公网可达性 (无论 LAN 还是公网, 都不依赖 WB Cloud 拉 URL). + * + * 返回: { data: '', size, mime } | null + * - null 时表示文件 > maxBytes (默认 5MB, J7 上限) 或读取失败 → 调用方跳过 data 字段, 走 URL 路径 + * - bare-base64 (不带 data:image/png;base64, 前缀) — 后端 buildMediaBlocks 需要纯 base64 + * - mime 从 blob.type (H5) / 调用方传 mimeHint (uni-app 平台不知道 blob.type) + * + * @param {string} filePath 选图/选文件时的原始路径 (blob: URL 或 _doc/tmp_xxx 路径) + * @param {object} [opts] + * @param {number} [opts.maxBytes=5MB] 超限返回 null (frontend 主动限流, 减轻带宽/内存) + * @param {string} [opts.mimeHint] 非 H5 平台拿不到 blob.type, 用 pending.uploaded.mime 兜底 + */ +export async function fileToBase64(filePath, opts = {}) { + const maxBytes = opts.maxBytes || SIZE_LIMIT_INLINE_BASE64 + if (!filePath) return null + + // #ifdef H5 + try { + const res = await fetch(filePath) + if (!res.ok) throw new Error('fetch failed: ' + res.status) + const blob = await res.blob() + if (blob.size > maxBytes) { + console.log('[media-upload] fileToBase64: size', blob.size, '> maxBytes', maxBytes, '→ 跳过内联') + return null + } + const dataUri = await new Promise((resolve, reject) => { + const reader = new FileReader() + reader.onload = () => resolve(reader.result) + reader.onerror = () => reject(new Error('FileReader failed')) + reader.readAsDataURL(blob) + }) + const m = /^data:([^;,]+);base64,(.*)$/.exec(dataUri) + if (!m) throw new Error('unexpected data URI shape: ' + String(dataUri).slice(0, 40)) + return { data: m[2], size: blob.size, mime: m[1] } + } catch (e) { + console.warn('[media-upload] fileToBase64 H5 失败:', e) + return null + } + // #endif + + // #ifdef MP-WEIXIN || APP-PLUS + try { + const fsm = uni.getFileSystemManager() + // 1. 探测大小, 超限直接返回 null + let size = 0 + try { + const stat = fsm.statSync(filePath) + // statSync 在部分平台返回 {size}, 部分返回 {fileSize}, 部分直接是数字 + size = (stat && (stat.size || stat.fileSize)) || 0 + } catch (e) { + // statSync 失败 (权限/路径) → 直接尝试 readFile, 后端会再校验 size + console.warn('[media-upload] fileToBase64 statSync failed, fallback to readFile:', e) + } + if (size > maxBytes) { + console.log('[media-upload] fileToBase64: size', size, '> maxBytes', maxBytes, '→ 跳过内联') + return null + } + // 2. readFile 拿 base64 — 同步 API 简单可靠, base64 在 5MB 文件 ≈ 6.7MB 字符串, 内存可控 + const data = fsm.readFileSync(filePath, 'base64') + return { data, size: size || 0, mime: opts.mimeHint || '' } + } catch (e) { + console.warn('[media-upload] fileToBase64 uni-app 失败:', e) + return null + } + // #endif +} + function guessMime(ext) { return EXT_MIME_MAP[ext] || 'application/octet-stream' } diff --git a/pages_tool/ai-chat/ai-chat-message.vue b/pages_tool/ai-chat/ai-chat-message.vue index 3ecade6..a973709 100644 --- a/pages_tool/ai-chat/ai-chat-message.vue +++ b/pages_tool/ai-chat/ai-chat-message.vue @@ -472,7 +472,7 @@