diff --git a/packageA/pages/personalInfo/personalInfo.vue b/packageA/pages/personalInfo/personalInfo.vue index 9eb1737..1e5f3c1 100644 --- a/packageA/pages/personalInfo/personalInfo.vue +++ b/packageA/pages/personalInfo/personalInfo.vue @@ -82,7 +82,7 @@ const { userInfo } = storeToRefs(useUserStore()); const { getUserResume } = useUserStore(); const { dictLabel, oneDictData } = useDictStore(); const openSelectPopup = inject('openSelectPopup'); -import { FileValidator } from '@/static/js/fileValidator.js'; //文件校验 +import { FileValidator } from '@/utils/fileValidator.js'; //文件校验 const percent = ref('0%'); const state = reactive({ @@ -404,4 +404,4 @@ function selectAvatar() { color: #FFFFFF; text-align: center; line-height: 90rpx - + \ No newline at end of file diff --git a/pages/chat/components/ai-paging.vue b/pages/chat/components/ai-paging.vue index a72a13f..901838b 100644 --- a/pages/chat/components/ai-paging.vue +++ b/pages/chat/components/ai-paging.vue @@ -282,7 +282,7 @@ import successIcon from '@/static/icon/success.png'; import useUserStore from '@/stores/useUserStore'; const { isMachineEnv } = storeToRefs(useUserStore()); -import {FileValidator} from "@/static/js/fileValidator.js" //文件校验 +import { FileValidator } from '@/utils/fileValidator.js'; //文件校验 // hook // 语音识别 const { diff --git a/utils/fileValidator.js b/utils/fileValidator.js new file mode 100644 index 0000000..bace6b7 --- /dev/null +++ b/utils/fileValidator.js @@ -0,0 +1,170 @@ +const KNOWN_SIGNATURES = { + png: '89504E470D0A1A0A', + jpg: 'FFD8FF', + jpeg: 'FFD8FF', + gif: '47494638', + webp: '52494646', + docx: '504B0304', + xlsx: '504B0304', + pptx: '504B0304', + doc: 'D0CF11E0', + xls: 'D0CF11E0', + ppt: 'D0CF11E0', + pdf: '25504446', + txt: 'TYPE_TEXT', + csv: 'TYPE_TEXT', + md: 'TYPE_TEXT', + json: 'TYPE_TEXT', +}; +export class FileValidator { + version = '1.0.0'; + signs = Object.keys(KNOWN_SIGNATURES); + constructor(options = {}) { + this.maxSizeMB = options.maxSizeMB || 10; + if (options.allowedExtensions && Array.isArray(options.allowedExtensions)) { + this.allowedConfig = {}; + options.allowedExtensions.forEach((ext) => { + const key = ext.toLowerCase(); + if (KNOWN_SIGNATURES[key]) { + this.allowedConfig[key] = KNOWN_SIGNATURES[key]; + } else { + console.warn(`[FileValidator] 未知的文件类型: .${key},已忽略`); + } + }); + } else { + this.allowedConfig = { + ...KNOWN_SIGNATURES, + }; + } + } + _isValidUTF8(buffer) { + try { + const decoder = new TextDecoder('utf-8', { + fatal: true, + }); + decoder.decode(buffer); + return true; + } catch (e) { + return false; + } + } + _bufferToHex(buffer) { + return Array.prototype.map + .call(new Uint8Array(buffer), (x) => ('00' + x.toString(16)).slice(-2)) + .join('') + .toUpperCase(); + } + _countCSVRows(buffer) { + const decoder = new TextDecoder('utf-8'); + const text = decoder.decode(buffer); + let rowCount = 0; + let inQuote = false; + let len = text.length; + for (let i = 0; i < len; i++) { + const char = text[i]; + if (char === '"') { + inQuote = !inQuote; + } else if (char === '\n' && !inQuote) { + rowCount++; + } + } + if (len > 0 && text[len - 1] !== '\n') { + rowCount++; + } + return rowCount; + } + _validateTextContent(buffer, extension) { + let contentStr = ''; + try { + const decoder = new TextDecoder('utf-8', { + fatal: true, + }); + contentStr = decoder.decode(buffer); + } catch (e) { + console.warn('UTF-8 解码失败', e); + return false; + } + if (contentStr.includes('\0')) { + return false; + } + if (extension === 'json') { + try { + JSON.parse(contentStr); + } catch (e) { + console.warn('无效的 JSON 格式'); + return false; + } + } + return true; + } + validate(file) { + return new Promise((resolve, reject) => { + if (!file || !file.name) return reject('无效的文件对象'); + if (file.size > this.maxSizeMB * 1024 * 1024) { + return reject(`文件大小超出限制 (最大 ${this.maxSizeMB}MB)`); + } + const fileName = file.name.toLowerCase(); + const extension = fileName.substring(fileName.lastIndexOf('.') + 1); + const expectedMagic = this.allowedConfig[extension]; + if (!expectedMagic) { + return reject(`不支持的文件格式: .${extension}`); + } + const reader = new FileReader(); + reader.onload = (e) => { + const buffer = e.target.result; + let isSafe = false; + if (expectedMagic === 'TYPE_TEXT') { + if (this._validateTextContent(buffer, extension)) { + isSafe = true; + } else { + if (extension === 'json') { + return reject(`文件异常:不是有效的 JSON 文件`); + } + return reject(`文件异常:.${extension} 包含非法二进制内容或编码错误`); + } + if (extension === 'csv' && this.csvMaxRows > 0) { + const rows = this._countCSVRows(buffer); + if (rows > this.csvMaxRows) { + return reject(`CSV 行数超出限制 (当前 ${rows} 行,最大允许 ${this.csvMaxRows} 行)`); + } + } + } else { + const fileHeader = this._bufferToHex(buffer.slice(0, 8)); + if (fileHeader.startsWith(expectedMagic)) { + isSafe = true; + } else { + return reject(`文件可能已被篡改 (真实类型与 .${extension} 不符)`); + } + } + if (isSafe) resolve(true); + }; + reader.onerror = () => reject('文件读取失败,无法校验'); + if (expectedMagic === 'TYPE_TEXT' && extension === 'json') { + reader.readAsArrayBuffer(file); + } else { + reader.readAsArrayBuffer(file.slice(0, 2048)); + } + }); + } +} + + +// 【demo】 +// 如果传入了 allowedExtensions,则只使用传入的;否则使用全部 KNOWN_SIGNATURES +// const imageValidator = new FileValidator({ +// maxSizeMB: 5, +// allowedExtensions: ['png', 'jpg', 'jpeg'], +// }); + +// imageValidator +// .validate(file) +// .then(() => { +// statusDiv.textContent = `检测通过: ${file.name}`; +// statusDiv.style.color = 'green'; +// console.log('图片校验通过,开始上传...'); +// // upload(file)... +// }) +// .catch((err) => { +// statusDiv.textContent = `检测失败: ${err}`; +// statusDiv.style.color = 'red'; +// }); \ No newline at end of file