|
|
@@ -27,7 +27,15 @@
|
|
|
|
|
|
import { readFileSync, readdirSync } from 'node:fs';
|
|
|
|
|
|
-const { toChatAttachmentUrls, ApiChatCoordinator } = await import('./_attachments.mjs');
|
|
|
+const {
|
|
|
+ toChatAttachmentUrls,
|
|
|
+ ApiChatCoordinator,
|
|
|
+ useBusinessAssistantUpload,
|
|
|
+ SUPPORTED_IMAGE_EXTENSIONS,
|
|
|
+ SUPPORTED_ATTACHMENT_EXTENSIONS,
|
|
|
+ SUPPORTED_IMAGE_LABEL,
|
|
|
+ SUPPORTED_ATTACHMENT_LABEL,
|
|
|
+} = await import('./_attachments.mjs');
|
|
|
|
|
|
let pass = 0;
|
|
|
let fail = 0;
|
|
|
@@ -162,5 +170,121 @@ console.log('\n【4】旧方案已停用(源码级)');
|
|
|
check('没有活跃调用点(地址不再拼进 question)', callers.length === 0, callers.join(', '));
|
|
|
}
|
|
|
|
|
|
+console.log('\n【5】支持格式的白名单(与产品提示同一口径)');
|
|
|
+{
|
|
|
+ check(
|
|
|
+ '图片:JPEG / PNG / GIF / WebP',
|
|
|
+ JSON.stringify(SUPPORTED_IMAGE_EXTENSIONS) === JSON.stringify(['jpg', 'jpeg', 'png', 'gif', 'webp']),
|
|
|
+ JSON.stringify(SUPPORTED_IMAGE_EXTENSIONS)
|
|
|
+ );
|
|
|
+ check(
|
|
|
+ '文件:Excel / DOCX / PDF',
|
|
|
+ JSON.stringify(SUPPORTED_ATTACHMENT_EXTENSIONS) === JSON.stringify(['xls', 'xlsx', 'docx', 'pdf']),
|
|
|
+ JSON.stringify(SUPPORTED_ATTACHMENT_EXTENSIONS)
|
|
|
+ );
|
|
|
+ // 用户 2026-09-23 指出的缺口:提示里写了 GIF,白名单里原来没有
|
|
|
+ check('**GIF 确实在白名单里**(提示说支持就得真支持)', SUPPORTED_IMAGE_EXTENSIONS.includes('gif'));
|
|
|
+ check('**Excel 确实在白名单里**', SUPPORTED_ATTACHMENT_EXTENSIONS.includes('xlsx') && SUPPORTED_ATTACHMENT_EXTENSIONS.includes('xls'));
|
|
|
+
|
|
|
+ // 提示文案与白名单必须描述同一批格式:文案里出现的每个格式名都要能在白名单里找到
|
|
|
+ const labelCovers = (label, list, aliases = {}) => {
|
|
|
+ const named = String(label).split(/[、,,/]/).map((x) => x.trim()).filter(Boolean);
|
|
|
+ return named.every((name) => {
|
|
|
+ const key = aliases[name] || name.toLowerCase();
|
|
|
+ // 文案里可能写成「文字PDF」这类带修饰的写法 → 用包含匹配(PDF 在白名单里即可)
|
|
|
+ return list.includes(key) || list.some((ext) => name.toLowerCase().includes(ext));
|
|
|
+ });
|
|
|
+ };
|
|
|
+ check(
|
|
|
+ '提示文案里的图片格式都在白名单里',
|
|
|
+ labelCovers(SUPPORTED_IMAGE_LABEL, SUPPORTED_IMAGE_EXTENSIONS),
|
|
|
+ `${SUPPORTED_IMAGE_LABEL} ↔ ${JSON.stringify(SUPPORTED_IMAGE_EXTENSIONS)}`
|
|
|
+ );
|
|
|
+ check(
|
|
|
+ '提示文案里的文件格式都在白名单里',
|
|
|
+ labelCovers(SUPPORTED_ATTACHMENT_LABEL, SUPPORTED_ATTACHMENT_EXTENSIONS, { Excel: 'xlsx', PDF: 'pdf', DOCX: 'docx' }),
|
|
|
+ `${SUPPORTED_ATTACHMENT_LABEL} ↔ ${JSON.stringify(SUPPORTED_ATTACHMENT_EXTENSIONS)}`
|
|
|
+ );
|
|
|
+
|
|
|
+ // ⚠️ 另一处口径:PC / Mobile 的 <input accept="…"> 若与白名单不一致,就会出现
|
|
|
+ // 「文件选择框里选不了某些格式」或「选得进但被校验拒掉」(用户这次报的就是这类)
|
|
|
+ for (const file of ['src/components/BusinessAssistantPC.vue', 'src/components/BusinessAssistantMobile.vue']) {
|
|
|
+ const src = readFileSync(file, 'utf8');
|
|
|
+ const imageAccept = (src.match(/accept="(image\/[^"]+)"/) || [])[1] || '';
|
|
|
+ const fileAccept = (src.match(/accept="((?:\.[a-z0-9]+)(?:,\.[a-z0-9]+)*)"/) || [])[1] || '';
|
|
|
+ // ⚠️ jpg 与 jpeg 是**同一个 MIME**(image/jpeg)—— 两边都要归一后再比,
|
|
|
+ // 否则「accept 写 image/jpeg、白名单同时列了 jpg 与 jpeg」会被误报成不一致
|
|
|
+ const norm = (exts) => [...new Set(exts.map((x) => (x === 'jpeg' ? 'jpg' : x)))].sort();
|
|
|
+ const acceptExts = imageAccept
|
|
|
+ .split(',')
|
|
|
+ .map((x) => x.trim().replace(/^image\//, ''))
|
|
|
+ .filter(Boolean);
|
|
|
+ check(
|
|
|
+ `${file.split('/').pop()} 的图片 accept 与白名单一致`,
|
|
|
+ JSON.stringify(norm(acceptExts)) === JSON.stringify(norm(SUPPORTED_IMAGE_EXTENSIONS)),
|
|
|
+ `${imageAccept} ↔ ${JSON.stringify(SUPPORTED_IMAGE_EXTENSIONS)}`
|
|
|
+ );
|
|
|
+ check(
|
|
|
+ `${file.split('/').pop()} 的文件 accept 与白名单一致`,
|
|
|
+ JSON.stringify(fileAccept.split(',').map((x) => x.trim().replace(/^\./, '')).sort()) ===
|
|
|
+ JSON.stringify(SUPPORTED_ATTACHMENT_EXTENSIONS.slice().sort()),
|
|
|
+ `${fileAccept} ↔ ${JSON.stringify(SUPPORTED_ATTACHMENT_EXTENSIONS)}`
|
|
|
+ );
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+console.log('\n【6】不支持的格式:toast 提示 + **不放进输入框**(调真实上传模块)');
|
|
|
+{
|
|
|
+ // node 里没有这几样,补上桩(只在「支持的格式」那条路径会用到)
|
|
|
+ if (!globalThis.URL.createObjectURL) {
|
|
|
+ globalThis.URL.createObjectURL = () => 'blob:stub';
|
|
|
+ globalThis.URL.revokeObjectURL = () => {};
|
|
|
+ }
|
|
|
+ const stubFetchOk = () =>
|
|
|
+ new Response(JSON.stringify({ code: 200, content: '/static/photo/1889-c_question-x.png' }), { status: 200 });
|
|
|
+ const restoreFetch = globalThis.fetch;
|
|
|
+
|
|
|
+ const toasts = [];
|
|
|
+ const { uploadedFiles, handleFiles } = useBusinessAssistantUpload({ notify: (m) => toasts.push(m) });
|
|
|
+ const f = (name, type = '') => new File(['x'], name, { type });
|
|
|
+
|
|
|
+ globalThis.fetch = async () => stubFetchOk();
|
|
|
+
|
|
|
+ // 6.1 全是不支持的格式 → 每个都提示一次,且一个都不加入
|
|
|
+ handleFiles([f('a.svg', 'image/svg+xml'), f('b.zip'), f('c.doc'), f('d.txt')], false);
|
|
|
+ await new Promise((r) => setTimeout(r, 20));
|
|
|
+ check('**四个不支持的文件各弹一次提示**', toasts.length === 4, JSON.stringify(toasts));
|
|
|
+ check('提示里带文件名与格式要求', toasts[0].includes('a.svg') && toasts[0].includes('Excel'), toasts[0]);
|
|
|
+ check('**一个都没放进输入框**', uploadedFiles.value.length === 0, JSON.stringify(uploadedFiles.value.map((x) => x.name)));
|
|
|
+
|
|
|
+ // 6.2 图片通道同样校验 —— **MIME 是 image/* 但扩展名不在白名单的(SVG/BMP/HEIC)也必须拦**
|
|
|
+ // (原来这里只看 `type.startsWith('image/')`,SVG 会被当图片收下 —— 已修)
|
|
|
+ toasts.length = 0;
|
|
|
+ handleFiles([f('e.svg', 'image/svg+xml'), f('f.bmp', 'image/bmp'), f('g.heic', 'image/heic')], true);
|
|
|
+ await new Promise((r) => setTimeout(r, 20));
|
|
|
+ check('伪图片(image/* 但扩展名不在白名单)**也要拦**', toasts.length === 3, JSON.stringify(toasts));
|
|
|
+ check('图片通道的提示给的是图片格式', toasts[0]?.includes('JPEG'), JSON.stringify(toasts[0]));
|
|
|
+ check('伪图片没放进去', uploadedFiles.value.length === 0, JSON.stringify(uploadedFiles.value.map((x) => x.name)));
|
|
|
+
|
|
|
+ // 6.3 支持的格式照旧放进去(证明上面的拦截没误伤)—— 注意图片走图片通道、文件走文件通道
|
|
|
+ toasts.length = 0;
|
|
|
+ handleFiles([f('ok.xlsx')], false);
|
|
|
+ handleFiles([f('ok2.png', 'image/png'), f('ok3.gif', 'image/gif'), f('ok4.jpeg', 'image/jpeg')], true);
|
|
|
+ await new Promise((r) => setTimeout(r, 30));
|
|
|
+ check('支持的格式**没有**被拦', toasts.length === 0, JSON.stringify(toasts));
|
|
|
+ check('支持的格式进了输入框(xlsx + png + gif + jpeg)', uploadedFiles.value.length === 4, JSON.stringify(uploadedFiles.value.map((x) => x.name)));
|
|
|
+
|
|
|
+ // 6.4 超过大小限制也走同一条提示(此前同样是 window.alert)
|
|
|
+ toasts.length = 0;
|
|
|
+ const tooBig = f('big.pdf');
|
|
|
+ Object.defineProperty(tooBig, 'size', { value: 51 * 1024 * 1024 }); // 只改 size,不真分配 51MB
|
|
|
+ handleFiles([tooBig], false);
|
|
|
+ await new Promise((r) => setTimeout(r, 20));
|
|
|
+ check('超限时提示(且不是 window.alert)', toasts.length === 1 && toasts[0].includes('大小限制'), JSON.stringify(toasts));
|
|
|
+ check('超限的文件没进输入框', uploadedFiles.value.length === 4, String(uploadedFiles.value.length));
|
|
|
+
|
|
|
+ globalThis.fetch = restoreFetch;
|
|
|
+}
|
|
|
+
|
|
|
console.log(`\n===== 通过 ${pass} 项,失败 ${fail} 项 =====`);
|
|
|
process.exit(fail ? 1 : 0);
|