flat: 合并代码
This commit is contained in:
@@ -630,6 +630,10 @@ export function sm4Encrypt(key, value, mode = "hex") {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function reloadBrowser() {
|
||||||
|
window.location.reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export const $api = {
|
export const $api = {
|
||||||
msg,
|
msg,
|
||||||
@@ -679,5 +683,6 @@ export default {
|
|||||||
aes_Decrypt,
|
aes_Decrypt,
|
||||||
sm2_Decrypt,
|
sm2_Decrypt,
|
||||||
sm2_Encrypt,
|
sm2_Encrypt,
|
||||||
safeReLaunch
|
safeReLaunch,
|
||||||
|
reloadBrowser
|
||||||
}
|
}
|
||||||
@@ -1,158 +0,0 @@
|
|||||||
import {
|
|
||||||
ref,
|
|
||||||
onUnmounted,
|
|
||||||
readonly
|
|
||||||
} from 'vue';
|
|
||||||
|
|
||||||
const defaultExtractSpeechText = (text) => text;
|
|
||||||
|
|
||||||
|
|
||||||
export function useTTSPlayer() {
|
|
||||||
const synth = window.speechSynthesis;
|
|
||||||
const isSpeaking = ref(false);
|
|
||||||
const isPaused = ref(false);
|
|
||||||
const utteranceRef = ref(null);
|
|
||||||
|
|
||||||
const cleanup = () => {
|
|
||||||
isSpeaking.value = false;
|
|
||||||
isPaused.value = false;
|
|
||||||
utteranceRef.value = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} text - The text to be spoken.
|
|
||||||
* @param {object} [options] - Optional settings for the speech.
|
|
||||||
* @param {string} [options.lang] - Language (e.g., 'en-US', 'es-ES').
|
|
||||||
* @param {number} [options.rate] - Speed (0.1 to 10, default 1).
|
|
||||||
* @param {number} [options.pitch] - Pitch (0 to 2, default 1).
|
|
||||||
* @param {SpeechSynthesisVoice} [options.voice] - A specific voice object.
|
|
||||||
* @param {function(string): string} [options.extractSpeechText] - A function to filter/clean the text before speaking.
|
|
||||||
*/
|
|
||||||
const speak = (text, options = {}) => {
|
|
||||||
if (!synth) {
|
|
||||||
console.error('SpeechSynthesis API is not supported in this browser.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isSpeaking.value) {
|
|
||||||
synth.cancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
const filteredText = extractSpeechText(text);
|
|
||||||
|
|
||||||
if (!filteredText || typeof filteredText !== 'string' || filteredText.trim() === '') {
|
|
||||||
console.warn('Text to speak is empty after filtering.');
|
|
||||||
cleanup(); // Ensure state is clean
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newUtterance = new SpeechSynthesisUtterance(filteredText); // Use filtered text
|
|
||||||
utteranceRef.value = newUtterance;
|
|
||||||
|
|
||||||
newUtterance.lang = 'zh-CN';
|
|
||||||
newUtterance.rate = options.rate || 1;
|
|
||||||
newUtterance.pitch = options.pitch || 1;
|
|
||||||
if (options.voice) {
|
|
||||||
newUtterance.voice = options.voice;
|
|
||||||
}
|
|
||||||
|
|
||||||
newUtterance.onstart = () => {
|
|
||||||
isSpeaking.value = true;
|
|
||||||
isPaused.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
newUtterance.onpause = () => {
|
|
||||||
isPaused.value = true;
|
|
||||||
};
|
|
||||||
newUtterance.onresume = () => {
|
|
||||||
isPaused.value = false;
|
|
||||||
};
|
|
||||||
newUtterance.onend = () => {
|
|
||||||
cleanup();
|
|
||||||
};
|
|
||||||
newUtterance.onerror = (event) => {
|
|
||||||
console.error('SpeechSynthesis Error:', event.error);
|
|
||||||
cleanup();
|
|
||||||
};
|
|
||||||
|
|
||||||
synth.speak(newUtterance);
|
|
||||||
};
|
|
||||||
|
|
||||||
const pause = () => {
|
|
||||||
if (synth && isSpeaking.value && !isPaused.value) {
|
|
||||||
synth.pause();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const resume = () => {
|
|
||||||
if (synth && isPaused.value) {
|
|
||||||
synth.resume();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const cancelAudio = () => {
|
|
||||||
if (synth) {
|
|
||||||
synth.cancel();
|
|
||||||
}
|
|
||||||
cleanup();
|
|
||||||
};
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
cancelAudio();
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
speak,
|
|
||||||
pause,
|
|
||||||
resume,
|
|
||||||
cancelAudio,
|
|
||||||
isSpeaking: readonly(isSpeaking),
|
|
||||||
isPaused: readonly(isPaused),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractSpeechText(markdown) {
|
|
||||||
const jobRegex = /``` job-json\s*({[\s\S]*?})\s*```/g;
|
|
||||||
const jobs = [];
|
|
||||||
let match;
|
|
||||||
let lastJobEndIndex = 0;
|
|
||||||
let firstJobStartIndex = -1;
|
|
||||||
|
|
||||||
// 提取岗位 json 数据及前后位置
|
|
||||||
while ((match = jobRegex.exec(markdown)) !== null) {
|
|
||||||
const jobStr = match[1];
|
|
||||||
try {
|
|
||||||
const job = JSON.parse(jobStr);
|
|
||||||
jobs.push(job);
|
|
||||||
if (firstJobStartIndex === -1) {
|
|
||||||
firstJobStartIndex = match.index;
|
|
||||||
}
|
|
||||||
lastJobEndIndex = jobRegex.lastIndex;
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('JSON 解析失败', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 提取引导语(第一个 job-json 之前的文字)
|
|
||||||
const guideText = firstJobStartIndex > 0 ?
|
|
||||||
markdown.slice(0, firstJobStartIndex).trim() :
|
|
||||||
'';
|
|
||||||
|
|
||||||
// 提取结束语(最后一个 job-json 之后的文字)
|
|
||||||
const endingText = lastJobEndIndex < markdown.length ?
|
|
||||||
markdown.slice(lastJobEndIndex).trim() :
|
|
||||||
'';
|
|
||||||
|
|
||||||
// 岗位信息格式化为语音文本
|
|
||||||
const jobTexts = jobs.map((job, index) => {
|
|
||||||
return `第 ${index + 1} 个岗位,岗位名称是:${job.jobTitle},公司是:${job.companyName},薪资:${job.salary},地点:${job.location},学历要求:${job.education},经验要求:${job.experience}。`;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 拼接总语音内容
|
|
||||||
const finalTextParts = [];
|
|
||||||
if (guideText) finalTextParts.push(guideText);
|
|
||||||
finalTextParts.push(...jobTexts);
|
|
||||||
if (endingText) finalTextParts.push(endingText);
|
|
||||||
|
|
||||||
return finalTextParts.join('\n');
|
|
||||||
}
|
|
||||||
@@ -273,9 +273,7 @@ import useScreenStore from '@/stores/useScreenStore'
|
|||||||
const screenStore = useScreenStore();
|
const screenStore = useScreenStore();
|
||||||
// 系统功能hook和阿里云hook
|
// 系统功能hook和阿里云hook
|
||||||
import { useAudioRecorder } from '@/hook/useRealtimeRecorder.js';
|
import { useAudioRecorder } from '@/hook/useRealtimeRecorder.js';
|
||||||
// import { useAudioRecorder } from '@/hook/useSystemSpeechReader.js';
|
|
||||||
import { useTTSPlayer } from '@/hook/useTTSPlayer.js';
|
import { useTTSPlayer } from '@/hook/useTTSPlayer.js';
|
||||||
// import { useTTSPlayer } from '@/hook/useSystemPlayer.js';
|
|
||||||
// 全局
|
// 全局
|
||||||
const { $api, navTo, throttle } = inject('globalFunction');
|
const { $api, navTo, throttle } = inject('globalFunction');
|
||||||
const emit = defineEmits(['onConfirm']);
|
const emit = defineEmits(['onConfirm']);
|
||||||
|
|||||||
@@ -11,7 +11,12 @@
|
|||||||
<image class="bg-text" mode="widthFix" src="@/static/icon/index-text-bg.png"></image>
|
<image class="bg-text" mode="widthFix" src="@/static/icon/index-text-bg.png"></image>
|
||||||
<view class="search-inner">
|
<view class="search-inner">
|
||||||
<view class="inner-left">
|
<view class="inner-left">
|
||||||
<image class="bg-text2" mode="widthFix" src="@/static/icon/index-text-bg2.png"></image>
|
<image
|
||||||
|
class="bg-text2"
|
||||||
|
mode="widthFix"
|
||||||
|
@click="reloadBrowser()"
|
||||||
|
src="@/static/icon/index-text-bg2.png"
|
||||||
|
></image>
|
||||||
<view class="search-input button-click" @click="navTo('/pages/search/search')">
|
<view class="search-input button-click" @click="navTo('/pages/search/search')">
|
||||||
<image class="icon" src="@/static/icon/index-search.png"></image>
|
<image class="icon" src="@/static/icon/index-search.png"></image>
|
||||||
<text class="inpute">请告诉我想找什么工作</text>
|
<text class="inpute">请告诉我想找什么工作</text>
|
||||||
@@ -20,7 +25,7 @@
|
|||||||
<image class="bg-robot button-click" mode="widthFix" src="@/static/icon/index-robot.png"></image>
|
<image class="bg-robot button-click" mode="widthFix" src="@/static/icon/index-robot.png"></image>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view v-if="!isMachineEnv" class="ai-card-out" >
|
<view v-if="!isMachineEnv" class="ai-card-out">
|
||||||
<view class="ai-card">
|
<view class="ai-card">
|
||||||
<image class="ai-card-bg" src="@/static/icon/ai-card-bg.png" />
|
<image class="ai-card-bg" src="@/static/icon/ai-card-bg.png" />
|
||||||
<view class="ai-card-inner">
|
<view class="ai-card-inner">
|
||||||
@@ -56,7 +61,7 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view v-if="hasLogin" :class="{'match-move-top':isMachineEnv}" class="match-card-out">
|
<view v-if="hasLogin" :class="{ 'match-move-top': isMachineEnv }" class="match-card-out">
|
||||||
<view class="match-card">
|
<view class="match-card">
|
||||||
<image class="match-card-bg" src="@/static/icon/match-card-bg.png" />
|
<image class="match-card-bg" src="@/static/icon/match-card-bg.png" />
|
||||||
<view class="title">简历匹配职位</view>
|
<view class="title">简历匹配职位</view>
|
||||||
@@ -65,7 +70,7 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view :class="{'cards-move-top':isMachineEnv && !hasLogin}" class="cards">
|
<view :class="{ 'cards-move-top': isMachineEnv && !hasLogin }" class="cards">
|
||||||
<view class="card card1 press-button" @click="navTo('/pages/nearby/nearby')">
|
<view class="card card1 press-button" @click="navTo('/pages/nearby/nearby')">
|
||||||
<view class="card-title">附近工作</view>
|
<view class="card-title">附近工作</view>
|
||||||
<view class="card-text">好岗职等你来</view>
|
<view class="card-text">好岗职等你来</view>
|
||||||
@@ -254,11 +259,11 @@
|
|||||||
import { reactive, inject, watch, ref, onMounted, watchEffect, nextTick, getCurrentInstance } from 'vue';
|
import { reactive, inject, watch, ref, onMounted, watchEffect, nextTick, getCurrentInstance } from 'vue';
|
||||||
import img from '@/static/icon/filter.png';
|
import img from '@/static/icon/filter.png';
|
||||||
import dictLabel from '@/components/dict-Label/dict-Label.vue';
|
import dictLabel from '@/components/dict-Label/dict-Label.vue';
|
||||||
const { $api, navTo, vacanciesTo, formatTotal, throttle } = inject('globalFunction');
|
const { $api, navTo, vacanciesTo, formatTotal, throttle, reloadBrowser } = inject('globalFunction');
|
||||||
import { onLoad, onShow } from '@dcloudio/uni-app';
|
import { onLoad, onShow } from '@dcloudio/uni-app';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import useUserStore from '@/stores/useUserStore';
|
import useUserStore from '@/stores/useUserStore';
|
||||||
const { userInfo, hasLogin ,isMachineEnv} = storeToRefs(useUserStore());
|
const { userInfo, hasLogin, isMachineEnv } = storeToRefs(useUserStore());
|
||||||
import useDictStore from '@/stores/useDictStore';
|
import useDictStore from '@/stores/useDictStore';
|
||||||
const { getTransformChildren, oneDictData } = useDictStore();
|
const { getTransformChildren, oneDictData } = useDictStore();
|
||||||
import useLocationStore from '@/stores/useLocationStore';
|
import useLocationStore from '@/stores/useLocationStore';
|
||||||
@@ -271,7 +276,6 @@ const recommedIndexDb = useRecommedIndexedDBStore();
|
|||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
import AIMatch from './AIMatch.vue';
|
import AIMatch from './AIMatch.vue';
|
||||||
|
|
||||||
|
|
||||||
const { proxy } = getCurrentInstance();
|
const { proxy } = getCurrentInstance();
|
||||||
|
|
||||||
const maskFirstEntry = ref(true);
|
const maskFirstEntry = ref(true);
|
||||||
@@ -363,7 +367,7 @@ onMounted(() => {
|
|||||||
let firstEntry = uni.getStorageSync('firstEntry') === false ? false : true; // 默认未读
|
let firstEntry = uni.getStorageSync('firstEntry') === false ? false : true; // 默认未读
|
||||||
maskFirstEntry.value = firstEntry;
|
maskFirstEntry.value = firstEntry;
|
||||||
getMatchTags();
|
getMatchTags();
|
||||||
console.log(isMachineEnv.value,'+++++++++')
|
// console.log(isMachineEnv.value, '+++++++++');
|
||||||
});
|
});
|
||||||
|
|
||||||
async function getMatchTags() {
|
async function getMatchTags() {
|
||||||
|
|||||||
@@ -9,6 +9,11 @@ import {
|
|||||||
} from '@/common/globalFunction.js'
|
} from '@/common/globalFunction.js'
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
|
|
||||||
|
const defalutLongLat = {
|
||||||
|
longitude: 120.382665,
|
||||||
|
latitude: 36.066938
|
||||||
|
}
|
||||||
|
|
||||||
const useLocationStore = defineStore("location", () => {
|
const useLocationStore = defineStore("location", () => {
|
||||||
// 定义状态
|
// 定义状态
|
||||||
const longitudeVal = ref(null) // 经度
|
const longitudeVal = ref(null) // 经度
|
||||||
@@ -25,46 +30,39 @@ const useLocationStore = defineStore("location", () => {
|
|||||||
resole(data)
|
resole(data)
|
||||||
},
|
},
|
||||||
fail: function(data) {
|
fail: function(data) {
|
||||||
longitudeVal.value = 120.382665
|
longitudeVal.value = defalutLongLat.longitude
|
||||||
latitudeVal.value = 36.066938
|
latitudeVal.value = defalutLongLat.latitude
|
||||||
resole({
|
resole(defalutLongLat)
|
||||||
longitude: 120.382665,
|
|
||||||
latitude: 36.066938
|
|
||||||
})
|
|
||||||
msg('用户位置获取失败')
|
msg('用户位置获取失败')
|
||||||
|
console.log('失败3', data)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
uni.getLocation({
|
uni.getLocation({
|
||||||
type: 'gcj02',
|
type: 'gcj02',
|
||||||
highAccuracyExpireTime: 3000,
|
// highAccuracyExpireTime: 3000,
|
||||||
isHighAccuracy: true,
|
// isHighAccuracy: true,
|
||||||
timeout: 2000,
|
// timeout: 2000,
|
||||||
success: function(data) {
|
success: function(data) {
|
||||||
longitudeVal.value = Number(data.longitude)
|
longitudeVal.value = Number(data.longitude)
|
||||||
latitudeVal.value = Number(data.latitude)
|
latitudeVal.value = Number(data.latitude)
|
||||||
resole(data)
|
resole(data)
|
||||||
},
|
},
|
||||||
fail: function(data) {
|
fail: function(data) {
|
||||||
longitudeVal.value = 120.382665
|
longitudeVal.value = defalutLongLat.longitude
|
||||||
latitudeVal.value = 36.066938
|
latitudeVal.value = defalutLongLat.latitude
|
||||||
resole({
|
resole(defalutLongLat)
|
||||||
longitude: 120.382665,
|
|
||||||
latitude: 36.066938
|
|
||||||
})
|
|
||||||
msg('用户位置获取失败')
|
msg('用户位置获取失败')
|
||||||
|
console.log('失败2', data)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
longitudeVal.value = 120.382665
|
longitudeVal.value = defalutLongLat.longitude
|
||||||
latitudeVal.value = 36.066938
|
latitudeVal.value = defalutLongLat.latitude
|
||||||
resole({
|
resole(defalutLongLat)
|
||||||
longitude: 120.382665,
|
|
||||||
latitude: 36.066938
|
|
||||||
})
|
|
||||||
msg('测试环境,使用模拟定位')
|
msg('测试环境,使用模拟定位')
|
||||||
console.log('失败', data)
|
console.log('失败1', e)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -84,7 +82,7 @@ const useLocationStore = defineStore("location", () => {
|
|||||||
latitudeVal
|
latitudeVal
|
||||||
|
|
||||||
}
|
}
|
||||||
},{
|
}, {
|
||||||
unistorage: true,
|
unistorage: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,22 @@ export class FileValidator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 改进版:检查是否为有效的 UTF-8 文本
|
||||||
|
*/
|
||||||
|
_isValidUTF8(buffer) {
|
||||||
|
try {
|
||||||
|
// fatal: true 会在遇到无效编码时抛出错误,而不是用 替换
|
||||||
|
const decoder = new TextDecoder('utf-8', {
|
||||||
|
fatal: true
|
||||||
|
});
|
||||||
|
decoder.decode(buffer);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 辅助:ArrayBuffer 转 Hex 字符串
|
* 辅助:ArrayBuffer 转 Hex 字符串
|
||||||
*/
|
*/
|
||||||
@@ -79,23 +95,79 @@ export class FileValidator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 辅助:纯文本抽样检测
|
* 【新增】统计 CSV 行数(严谨版:忽略引号内的换行符)
|
||||||
|
* 性能:对于 10MB 文件,现代浏览器处理通常在 100ms 以内
|
||||||
*/
|
*/
|
||||||
_isCleanText(buffer) {
|
_countCSVRows(buffer) {
|
||||||
const bytes = new Uint8Array(buffer);
|
const decoder = new TextDecoder('utf-8');
|
||||||
const checkLen = Math.min(bytes.length, 1000);
|
const text = decoder.decode(buffer);
|
||||||
let suspiciousCount = 0;
|
|
||||||
|
|
||||||
for (let i = 0; i < checkLen; i++) {
|
let rowCount = 0;
|
||||||
const byte = bytes[i];
|
let inQuote = false;
|
||||||
// 允许常见控制符: 9(Tab), 10(LF), 13(CR)
|
let len = text.length;
|
||||||
// 0-31 范围内其他的通常是二进制控制符
|
|
||||||
if (byte < 32 && ![9, 10, 13].includes(byte)) {
|
// 遍历每一个字符
|
||||||
suspiciousCount++;
|
for (let i = 0; i < len; i++) {
|
||||||
|
const char = text[i];
|
||||||
|
|
||||||
|
// 切换引号状态
|
||||||
|
if (char === '"') {
|
||||||
|
inQuote = !inQuote;
|
||||||
|
}
|
||||||
|
// 只有在非引号状态下的换行符,才算作一行结束
|
||||||
|
else if (char === '\n' && !inQuote) {
|
||||||
|
rowCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 如果可疑字符占比 < 5%,认为是纯文本
|
|
||||||
return suspiciousCount / checkLen < 0.05;
|
// 处理最后一行没有换行符的情况(且文件不为空)
|
||||||
|
if (len > 0 && text[len - 1] !== '\n') {
|
||||||
|
rowCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return rowCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 【核心】:校验纯文本内容
|
||||||
|
* 1. 检查是否包含乱码 (非 UTF-8)
|
||||||
|
* 2. 针对特定格式 (JSON) 进行语法解析
|
||||||
|
*/
|
||||||
|
_validateTextContent(buffer, extension) {
|
||||||
|
// 1. 尝试解码为 UTF-8
|
||||||
|
let contentStr = '';
|
||||||
|
try {
|
||||||
|
const decoder = new TextDecoder('utf-8', {
|
||||||
|
fatal: true
|
||||||
|
});
|
||||||
|
contentStr = decoder.decode(buffer);
|
||||||
|
} catch (e) {
|
||||||
|
// 如果解码失败,说明包含非文本的二进制数据
|
||||||
|
console.warn('UTF-8 解码失败', e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 检查是否存在过多的空字符 (二进制文件特征)
|
||||||
|
// 某些二进制文件可能勉强通过 UTF-8 解码,但会包含大量 \0
|
||||||
|
if (contentStr.includes('\u0000')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 针对特定后缀进行语法校验 (可选,更严格)
|
||||||
|
if (extension === 'json') {
|
||||||
|
try {
|
||||||
|
JSON.parse(contentStr);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('无效的 JSON 格式');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是 CSV,可以简单检查行数(可选)
|
||||||
|
// if (extension === 'csv') { ... }
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -132,10 +204,23 @@ export class FileValidator {
|
|||||||
|
|
||||||
// 分支处理:纯文本 vs 二进制
|
// 分支处理:纯文本 vs 二进制
|
||||||
if (expectedMagic === 'TYPE_TEXT') {
|
if (expectedMagic === 'TYPE_TEXT') {
|
||||||
if (this._isCleanText(buffer)) {
|
if (this._validateTextContent(buffer, extension)) {
|
||||||
isSafe = true;
|
isSafe = true;
|
||||||
} else {
|
} else {
|
||||||
return reject(`文件异常:.${extension} 文件包含非法二进制内容`);
|
// 细化报错信息
|
||||||
|
if (extension === 'json') {
|
||||||
|
return reject(`文件异常:不是有效的 JSON 文件`);
|
||||||
|
}
|
||||||
|
return reject(`文件异常:.${extension} 包含非法二进制内容或编码错误`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 【新增】专门针对 CSV 的行数检查
|
||||||
|
if (extension === 'csv' && this.csvMaxRows > 0) {
|
||||||
|
const rows = this._countCSVRows(buffer);
|
||||||
|
// 注意:这里通常把表头也算作 1 行,如果不算表头可以将 limit + 1
|
||||||
|
if (rows > this.csvMaxRows) {
|
||||||
|
return reject(`CSV 行数超出限制 (当前 ${rows} 行,最大允许 ${this.csvMaxRows} 行)`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 获取文件头 Hex (读取足够长的字节以覆盖最长的魔数,PNG需8字节)
|
// 获取文件头 Hex (读取足够长的字节以覆盖最长的魔数,PNG需8字节)
|
||||||
@@ -155,7 +240,13 @@ export class FileValidator {
|
|||||||
reader.onerror = () => reject('文件读取失败,无法校验');
|
reader.onerror = () => reject('文件读取失败,无法校验');
|
||||||
|
|
||||||
// 读取前 1KB 进行判断
|
// 读取前 1KB 进行判断
|
||||||
reader.readAsArrayBuffer(file.slice(0, 1024));
|
if (expectedMagic === 'TYPE_TEXT' && extension === 'json') {
|
||||||
|
// JSON 必须读全量才能 parse,建议限制 JSON 文件大小
|
||||||
|
reader.readAsArrayBuffer(file);
|
||||||
|
} else {
|
||||||
|
// 图片/普通文本 读取前 2KB 足够判断头部和编码特征
|
||||||
|
reader.readAsArrayBuffer(file.slice(0, 2048));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user