fix(media-upload): H5 pathToFile 修复 multipart filename 缺扩展名

PR-FRONTEND-MEDIA-UPLOAD-FIX-HOTFIX (2026-09-16)

诊断: 直接打后端 (curl) 验证后端 ext_rule 完全 OK,
所有 jpg/png/heic 标准 mime 都返回 code:0, 只有 filename=空
(无扩展名) 才会被拒. 所以问题在前端.

定位: common/js/media-upload.js pathToFile() H5 分支:
  原代码 filePath.split('/').pop() 对 blob:http://xxx/uuid
  取出 'uuid' (无扩展名)
  → new File([blob], 'uuid', ...) 没有 .jpg/.png 后缀
  → multipart Content-Disposition: filename='uuid'
  → 后端 fileExt 规则检查 'uuid' 不在白名单 → 拒绝

修复: 提取 extractExt() 到模块顶层 (之前内联在 uploadViaSts 里),
pathToFile 调用 extractExt(filePath, blob.type) 三段兜底:
  1) filePath 最后一段正则
  2) filePath.name 兜底
  3) blob.type mimeHint 反查 EXT_MIME_MAP → png/jpg/heic 等

修复后 multipart filename = 'test-img1.png' (有扩展名),
后端 ext_rule 通过, 上传成功.

E2E 验证 (verify_filename2.py):
  - 修复前: filename='test-img2' → 后端拒绝
  - 修复后: filename='test-img2.png' → 后端接受

教训: PR-FRONTEND-MEDIA-UPLOAD-FIX 加 extractExt 时只修了 STS 路径,
PHP 路径漏修; H5 blob URL + multipart 是双盲点 (前端不知道后端需要
filename 扩展名, 后端拿到 filename 才检查). 任何 file.toBlob() 或
URL.createObjectURL 出来的 file 都要在 File 构造时补扩展名.
This commit is contained in:
2026-09-16 17:32:03 +08:00
parent f2cb69cb06
commit 065fba7cf8

View File

@@ -227,13 +227,19 @@ async function uploadViaPhp(filePath, kind, addon, agentId, knownSize = 0) {
* { provider, bucket, region, accessKeyId, accessKeySecret, securityToken,
* key_prefix_tpl, site_id, host? (single-URL 模式), endpoint? (qiniu 7 参兼容) }
*/
async function uploadViaSts(filePath, kind, addon, agentId, config, knownSize = 0) {
// PR-FRONTEND-MEDIA-UPLOAD-FIX (2026-09-16): 用更稳的扩展名提取
// H5 blob URL (blob:https://xxx/uuid) 无扩展名 → split('.').pop() 会拿整条 URL
// 优先 file.name (H5 file / APP tempFile.name), 都没有就 blob URL → 'bin'
/**
* PR-FRONTEND-MEDIA-UPLOAD-FIX (2026-09-16): 三段兜底扩展名提取
* 1) filePath 最后一段正则匹配 .xxx
* 2) filePath.name 兜底 (H5 File 对象 / APP tempFile.name)
* 3) mimeHint 反查 EXT_MIME_MAP (H5 blob URL 没扩展名但 blob.type 通常是 image/png)
* 4) 最终 'bin' (防 URL 片段被误当扩展名)
*
* PR-FRONTEND-MEDIA-UPLOAD-FIX-HOTFIX (2026-09-16): 提到模块顶层,
* 之前只在 uploadViaSts 里, pathToFile 引用不到 → "extractExt is not defined"
*/
function extractExt(filePath, mimeHint = '') {
let ext = 'bin'
if (typeof filePath === 'string') {
// 1) 优先从 filePath 里找 .xxx (路径最后一段)
const lastSeg = filePath.split('?')[0].split('/').pop() || ''
const m = lastSeg.match(/\.([a-z0-9]+)$/i)
if (m) ext = m[1].toLowerCase()
@@ -242,7 +248,22 @@ async function uploadViaSts(filePath, kind, addon, agentId, config, knownSize =
const m2 = String(filePath.name).match(/\.([a-z0-9]+)$/i)
if (m2) ext = m2[1].toLowerCase()
}
if (!/^[a-z0-9]{2,5}$/.test(ext)) ext = 'bin' // 防止 ext 是 'https' 这种 URL 片段
// 3) mime→ext 反查 (PR-FRONTEND-MEDIA-UPLOAD-FIX-HOTFIX): blob URL 无扩展名但
// blob.type 是 image/png 这种, 反查 EXT_MIME_MAP → png
if (ext === 'bin' && mimeHint) {
for (const [e, m] of Object.entries(EXT_MIME_MAP)) {
if (m === mimeHint) { ext = e; break }
}
}
if (!/^[a-z0-9]{2,5}$/.test(ext)) ext = 'bin'
return ext
}
async function uploadViaSts(filePath, kind, addon, agentId, config, knownSize = 0) {
// PR-FRONTEND-MEDIA-UPLOAD-FIX (2026-09-16): 用更稳的扩展名提取
// H5 blob URL (blob:https://xxx/uuid) 无扩展名 → split('.').pop() 会拿整条 URL
// 优先 file.name (H5 file / APP tempFile.name), 都没有就 blob URL → 'bin'
const ext = extractExt(filePath)
// 1. 拿 STS 凭证 (addon 模式 URL)
const stsUrl = (addon === 'aikefu' ? '/aikefu/api/upload/' : '/aiagent/api/upload/') + 'ossSts'
@@ -393,7 +414,17 @@ async function pathToFile(filePath, kind) {
// #ifdef H5
const res = await fetch(filePath)
const blob = await res.blob()
return new File([blob], filePath.split('/').pop() || `${kind}.bin`, { type: blob.type })
// PR-FRONTEND-MEDIA-UPLOAD-FIX-HOTFIX (2026-09-16): H5 blob URL 路径修复扩展名
// 原代码 `filePath.split('/').pop()` 对 blob:http://xxx/uuid 取出 'uuid' (无扩展名)
// → 后端 multipart filename 缺失扩展名 → ext_rule 拒绝 "上传文件后缀不允许"
// extractExt 三段兜底: 1) lastSegment 正则 2) file.name 3) 'bin' 最终兜底
// 若 ext='bin', 走 guessMime(EXT_MIME_MAP) 看能不能从 mime 反推 jpg/png (mime→ext)
const rawName = filePath.split('/').pop() || `${kind}.bin`
const ext = extractExt(filePath, blob.type)
const finalName = ext !== 'bin' ? `${rawName}.${ext}` : rawName
// blob.type 通常是 image/png 或空; 空时用 guessMime 兜底
const mime = blob.type || guessMime(ext)
return new File([blob], finalName, { type: mime })
// #endif
}