From cb928edafd637c3fd8a006621ef8606d455740ef Mon Sep 17 00:00:00 2001 From: key Date: Wed, 29 Jul 2026 02:01:45 +0800 Subject: [PATCH] fix(mobile): handle file picker cancellation --- mobile-uni/package.json | 2 +- mobile-uni/src/manifest.json | 4 +- .../src/pages/candidate/interview/index.vue | 5 + .../src/pages/candidate/materials/index.vue | 3 +- .../src/pages/user/assistant/capture.vue | 11 +- mobile-uni/src/pages/user/cases/index.vue | 5 +- mobile-uni/src/pages/user/practice/index.vue | 11 +- mobile-uni/src/pages/user/report/index.vue | 13 ++- mobile-uni/src/pages/user/sop/index.vue | 13 ++- mobile-uni/src/pages/user/today/index.vue | 3 +- mobile-uni/src/pages/user/web-ai/index.vue | 13 ++- mobile-uni/src/services/candidate.ts | 3 +- mobile-uni/src/services/case.ts | 3 +- mobile-uni/src/services/knowledge.ts | 3 +- mobile-uni/src/services/personal-assistant.ts | 3 +- mobile-uni/src/services/speech.ts | 3 +- mobile-uni/src/utils/file-selection.ts | 28 +++++ mobile-uni/tests/file-selection.test.mjs | 102 ++++++++++++++++++ 18 files changed, 202 insertions(+), 26 deletions(-) create mode 100644 mobile-uni/src/utils/file-selection.ts create mode 100644 mobile-uni/tests/file-selection.test.mjs diff --git a/mobile-uni/package.json b/mobile-uni/package.json index 4bf01be0..05c2dc7b 100644 --- a/mobile-uni/package.json +++ b/mobile-uni/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package", "name": "aihr-mobile-uni", - "version": "0.1.8", + "version": "0.1.9", "private": true, "type": "module", "scripts": { diff --git a/mobile-uni/src/manifest.json b/mobile-uni/src/manifest.json index 369861b8..46256782 100644 --- a/mobile-uni/src/manifest.json +++ b/mobile-uni/src/manifest.json @@ -3,8 +3,8 @@ "appid" : "__UNI__B36D8BE", "description" : "帮道员工端", "vueVersion" : "3", - "versionName" : "0.1.8", - "versionCode" : "108", + "versionName" : "0.1.9", + "versionCode" : "109", "uniStatistics" : { "enable" : false }, diff --git a/mobile-uni/src/pages/candidate/interview/index.vue b/mobile-uni/src/pages/candidate/interview/index.vue index 7e0cf417..e545bb85 100644 --- a/mobile-uni/src/pages/candidate/interview/index.vue +++ b/mobile-uni/src/pages/candidate/interview/index.vue @@ -164,6 +164,7 @@ import { } from '@/services/speech'; import { startSpeechCapture } from '@/services/speech-capture'; import type { SpeechCapture, SpeechCaptureSession } from '@/services/speech-capture'; +import { isFileSelectionCancelled } from '@/utils/file-selection'; const currentPath = '/pages/candidate/interview/index'; const records = ref([]); @@ -342,6 +343,10 @@ const transcribeQuestionFile = async (questionId: string) => { applyQuestionAsrResult(questionId, result); } catch (error) { if (generation !== questionTranscriptionGeneration) return; + if (isFileSelectionCancelled(error)) { + message.value = ''; + return; + } handleRequestError(error, '语音转写失败'); } finally { if (generation === questionTranscriptionGeneration) { diff --git a/mobile-uni/src/pages/candidate/materials/index.vue b/mobile-uni/src/pages/candidate/materials/index.vue index 0ca07d23..8ab3ed67 100644 --- a/mobile-uni/src/pages/candidate/materials/index.vue +++ b/mobile-uni/src/pages/candidate/materials/index.vue @@ -72,6 +72,7 @@ import { } from '@/services/candidate-view'; import { resetPageScroll } from '@/services/navigation'; import StateBlock from '@/components/StateBlock.vue'; +import { isFileSelectionCancelled } from '@/utils/file-selection'; const currentPath = '/pages/candidate/materials/index'; const materials = ref([]); @@ -131,7 +132,7 @@ const uploadMaterial = async () => { message.value = `资料已上传:#${saved.id}`; materials.value = await getCandidateMaterials(); } catch (error) { - handleRequestError(error, '资料上传失败'); + if (!isFileSelectionCancelled(error)) handleRequestError(error, '资料上传失败'); } finally { uploading.value = false; } diff --git a/mobile-uni/src/pages/user/assistant/capture.vue b/mobile-uni/src/pages/user/assistant/capture.vue index 2d645f6b..1e0f9b91 100644 --- a/mobile-uni/src/pages/user/assistant/capture.vue +++ b/mobile-uni/src/pages/user/assistant/capture.vue @@ -27,6 +27,7 @@ import { ref } from 'vue'; import { choosePersonalFile, createTextItem, createUrlItem, uploadPersonalFile, type PersonalSelectedFile } from '@/services/personal-assistant'; import type { PersonalItemSourceType } from '@/types/api'; +import { isFileSelectionCancelled } from '@/utils/file-selection'; const typeOptions = [{ label: '文字', value: 'TEXT' }, { label: '文件', value: 'FILE' }, { label: '网页', value: 'URL' }] as const; const captureType = ref('TEXT'); @@ -36,7 +37,15 @@ const url = ref(''); const selectedFile = ref(); const submitting = ref(false); const message = ref(''); -const chooseFile = async () => { try { selectedFile.value = await choosePersonalFile(); } catch (error) { message.value = error instanceof Error ? error.message : '未选择文件'; } }; +const chooseFile = async () => { + try { + selectedFile.value = await choosePersonalFile(); + } catch (error) { + if (!isFileSelectionCancelled(error)) { + message.value = error instanceof Error ? error.message : '选择文件失败'; + } + } +}; const submit = async () => { if (submitting.value) return; if (captureType.value === 'TEXT' && !content.value.trim()) { message.value = '请输入需要收藏的文字'; return; } diff --git a/mobile-uni/src/pages/user/cases/index.vue b/mobile-uni/src/pages/user/cases/index.vue index 8af4cca4..defa480c 100644 --- a/mobile-uni/src/pages/user/cases/index.vue +++ b/mobile-uni/src/pages/user/cases/index.vue @@ -148,6 +148,7 @@ import { isLoggedIn, rememberLoginRedirect } from '@/services/auth'; import { navigateBackToToday, resetPageScroll } from '@/services/navigation'; import { ensureEmployeePosition } from '@/services/position'; import StateBlock from '@/components/StateBlock.vue'; +import { isFileSelectionCancelled } from '@/utils/file-selection'; const loggedIn = ref(false); const selectedFile = ref(null); @@ -213,7 +214,9 @@ const selectAudio = async () => { selectedFile.value = await chooseCaseAudio(); resetResult(); } catch (error) { - message.value = error instanceof Error ? error.message : '选择音频失败'; + if (!isFileSelectionCancelled(error)) { + message.value = error instanceof Error ? error.message : '选择音频失败'; + } } }; diff --git a/mobile-uni/src/pages/user/practice/index.vue b/mobile-uni/src/pages/user/practice/index.vue index 2069746a..45e0c9fd 100644 --- a/mobile-uni/src/pages/user/practice/index.vue +++ b/mobile-uni/src/pages/user/practice/index.vue @@ -532,6 +532,7 @@ import { createPageRequestScope, currentAccountKey, type RequestScopeTicket } fr import { createRealtimePracticeController, createRealtimeSession, fetchRealtimePersonas, invokeRealtimeTool, type RealtimePersonaOption } from '@/services/realtime-practice'; import { stripInternalCodes } from '@/services/text'; import { describeMicrophoneError, microphoneDeniedMessage, requestMicrophonePermission } from '@/utils/microphone'; +import { isFileSelectionCancelled } from '@/utils/file-selection'; import { safeTopPadding } from '@/utils/safe-area'; type PracticeStatus = 'idle' | 'starting' | 'active' | 'submitting' | 'finished'; @@ -913,8 +914,10 @@ const composerPickAudio = async () => { await submitTranscribed(text, asr.audioUrl || '', normalizeOssId(asr.ossId)); } catch (error) { if (generation !== transcriptionGeneration) return; - const text = error instanceof Error ? error.message : '语音转写失败,请改用文字输入'; - if (text !== '未选择音频') uni.showToast({ title: text, icon: 'none' }); + if (!isFileSelectionCancelled(error)) { + const text = error instanceof Error ? error.message : '语音转写失败,请改用文字输入'; + uni.showToast({ title: text, icon: 'none' }); + } } finally { if (generation === transcriptionGeneration) { abortTranscription = null; @@ -1226,6 +1229,10 @@ const transcribeCapture = async (capture: SpeechCapture, assignmentId?: number) } } catch (error) { if (generation !== transcriptionGeneration) return; + if (isFileSelectionCancelled(error)) { + message.value = ''; + return; + } handleError(error, '语音转写失败'); } finally { if (generation === transcriptionGeneration) { diff --git a/mobile-uni/src/pages/user/report/index.vue b/mobile-uni/src/pages/user/report/index.vue index 1eef0471..94e2510c 100644 --- a/mobile-uni/src/pages/user/report/index.vue +++ b/mobile-uni/src/pages/user/report/index.vue @@ -244,6 +244,7 @@ import { } from '@/services/speech'; import type { SpeechCapture } from '@/services/speech-capture'; import type { SpeechPlaybackStatus } from '@/types/api'; +import { isFileSelectionCancelled } from '@/utils/file-selection'; import { listMyWorkReports, isCurrentWorkReportRequest, @@ -547,8 +548,10 @@ const addMediaAttachment = async () => { }); } catch (error) { if (!requestIsCurrent(generation, mediaSelectionGeneration, accountKey)) return; - const text = error instanceof Error ? error.message : '附件选择失败'; - if (text !== '未选择附件') pushSystem(text); + if (!isFileSelectionCancelled(error)) { + const text = error instanceof Error ? error.message : '附件选择失败'; + pushSystem(text); + } } finally { if (requestIsCurrent(generation, mediaSelectionGeneration, accountKey)) mediaSelecting.value = false; } @@ -580,8 +583,10 @@ const pickVoiceAudio = async () => { }); } catch (error) { if (!requestIsCurrent(generation, voiceTranscriptionGeneration, accountKey)) return; - const text = error instanceof Error ? error.message : '音频转写失败,请重试或改用键盘'; - if (text !== '未选择音频') pushSystem(text); + if (!isFileSelectionCancelled(error)) { + const text = error instanceof Error ? error.message : '音频转写失败,请重试或改用键盘'; + pushSystem(text); + } } finally { if (requestIsCurrent(generation, voiceTranscriptionGeneration, accountKey)) { abortVoiceTranscription = null; diff --git a/mobile-uni/src/pages/user/sop/index.vue b/mobile-uni/src/pages/user/sop/index.vue index 613a9ab1..5133620a 100644 --- a/mobile-uni/src/pages/user/sop/index.vue +++ b/mobile-uni/src/pages/user/sop/index.vue @@ -463,6 +463,7 @@ import { isNativeAppRuntime, type SpeechCapture } from '@/services/speech-captur import { downloadSummaryCardImage } from '@/services/summary-card-image'; import { getMemoryCandidates } from '@/services/personal-assistant'; import { currentAccountKey } from '@/services/request-scope'; +import { isFileSelectionCancelled } from '@/utils/file-selection'; import { clearBroadcastQuestionContext, getBroadcastMessage, @@ -1084,8 +1085,10 @@ const pickMedia = async () => { icon: 'none' }); } catch (error) { - const text = error instanceof Error ? error.message : '附件选择失败'; - if (text !== '未选择附件') uni.showToast({ title: text, icon: 'none' }); + if (!isFileSelectionCancelled(error)) { + const text = error instanceof Error ? error.message : '附件选择失败'; + uni.showToast({ title: text, icon: 'none' }); + } } }; @@ -1150,8 +1153,10 @@ const pickVoiceAudio = async () => { void sendQuestion(text, { voice: true }); } catch (error) { if (generation !== voiceTranscriptionGeneration) return; - const text = error instanceof Error ? error.message : '语音转写失败,请改用文字输入'; - if (text !== '未选择音频') pushSystem(text); + if (!isFileSelectionCancelled(error)) { + const text = error instanceof Error ? error.message : '语音转写失败,请改用文字输入'; + pushSystem(text); + } } finally { if (generation === voiceTranscriptionGeneration) { abortVoiceTranscription = null; diff --git a/mobile-uni/src/pages/user/today/index.vue b/mobile-uni/src/pages/user/today/index.vue index b42baf28..4bf0440e 100644 --- a/mobile-uni/src/pages/user/today/index.vue +++ b/mobile-uni/src/pages/user/today/index.vue @@ -218,6 +218,7 @@ import { stripInternalCodes } from '@/services/text'; import { ensureEmployeePosition } from '@/services/position'; import { createPageRequestScope, currentAccountKey } from '@/services/request-scope'; import { getBroadcastUnreadCount } from '@/services/broadcast'; +import { isFileSelectionCancelled } from '@/utils/file-selection'; const home = ref(null); const assignments = ref([]); @@ -499,7 +500,7 @@ const pickAudioFallback = async () => { if (text) ask(text); else message.value = '没听清,请再说一次'; } catch (error) { - if (error instanceof Error && error.message !== '未选择音频') { + if (!isFileSelectionCancelled(error) && error instanceof Error) { message.value = error.message; } } finally { diff --git a/mobile-uni/src/pages/user/web-ai/index.vue b/mobile-uni/src/pages/user/web-ai/index.vue index 1ffbcf48..529aed1c 100644 --- a/mobile-uni/src/pages/user/web-ai/index.vue +++ b/mobile-uni/src/pages/user/web-ai/index.vue @@ -123,6 +123,7 @@ import { chooseKnowledgeMedia } from '@/services/knowledge'; import { chooseSpeechAudio, createSpeechPlaybackController, MENTOR_VOICE_PROFILE, transcribeSpeechCapture, transcribeSpeechFile } from '@/services/speech'; import type { SpeechCapture } from '@/services/speech-capture'; import { getWebAiCapabilities, queryWebAi, queryWebAiWithImage } from '@/services/web-ai'; +import { isFileSelectionCancelled } from '@/utils/file-selection'; import { renderMarkdown } from '@/utils/markdown'; const path = '/pages/user/web-ai/index'; @@ -330,8 +331,10 @@ const handlePanelAction = async (key: string) => { composerRef.value?.setDraft(''); uni.showToast({ title: '已选择图片,发送时将联网识图', icon: 'none' }); } catch (error) { - const text = error instanceof Error ? error.message : '图片选择失败'; - if (text !== '未选择附件') uni.showToast({ title: text, icon: 'none' }); + if (!isFileSelectionCancelled(error)) { + const text = error instanceof Error ? error.message : '图片选择失败'; + uni.showToast({ title: text, icon: 'none' }); + } } return; } @@ -353,8 +356,10 @@ const handlePanelAction = async (key: string) => { void sendQuestion(text, { voice: true }); } catch (error) { if (generation !== voiceTranscriptionGeneration) return; - const text = error instanceof Error ? error.message : '语音转写失败,请改用文字输入'; - if (text !== '未选择音频') pushSystem(text); + if (!isFileSelectionCancelled(error)) { + const text = error instanceof Error ? error.message : '语音转写失败,请改用文字输入'; + pushSystem(text); + } } finally { if (generation === voiceTranscriptionGeneration) { abortVoiceTranscription = null; diff --git a/mobile-uni/src/services/candidate.ts b/mobile-uni/src/services/candidate.ts index f942b358..52cbc67d 100644 --- a/mobile-uni/src/services/candidate.ts +++ b/mobile-uni/src/services/candidate.ts @@ -7,6 +7,7 @@ import type { } from '@/types/api'; import { apiRequest, apiUrl, authHeaders, readTextPayload } from './api'; import { getAuth } from './auth'; +import { fileSelectionFailure } from '../utils/file-selection'; const materialExtensions = ['.pdf', '.doc', '.docx', '.jpg', '.jpeg', '.png', '.webp']; @@ -57,7 +58,7 @@ export const chooseCandidateFile = () => size: Number(file.size || 0) }); }, - fail: () => reject(new Error('未选择文件')) + fail: (error) => reject(fileSelectionFailure(error, '文件')) }); }); diff --git a/mobile-uni/src/services/case.ts b/mobile-uni/src/services/case.ts index d3468f21..281ba3f4 100644 --- a/mobile-uni/src/services/case.ts +++ b/mobile-uni/src/services/case.ts @@ -8,6 +8,7 @@ import type { CaseUploadResponse } from '@/types/api'; import { apiRequest, apiUrl, authHeaders, authenticatedFileUrl, readTextPayload } from './api'; +import { fileSelectionFailure } from '../utils/file-selection'; const audioExtensions = ['.mp3', '.wav', '.m4a', '.webm', '.aac', '.ogg']; @@ -31,7 +32,7 @@ export const chooseCaseAudio = () => size: Number(file.size || 0) }); }, - fail: () => reject(new Error('未选择音频')) + fail: (error) => reject(fileSelectionFailure(error, '音频')) }); }); diff --git a/mobile-uni/src/services/knowledge.ts b/mobile-uni/src/services/knowledge.ts index f81eefe7..4219a711 100644 --- a/mobile-uni/src/services/knowledge.ts +++ b/mobile-uni/src/services/knowledge.ts @@ -10,6 +10,7 @@ import type { } from '@/types/api'; import { apiRequest, apiUrl, authHeaders, readTextPayload } from './api'; import { getSelectedPosition } from './auth'; +import { fileSelectionFailure } from '../utils/file-selection'; type RawKnowledgeSnippet = KnowledgeSnippet & { title?: string; @@ -44,7 +45,7 @@ export const chooseKnowledgeMedia = () => } resolve({ path, name, size, kind }); }, - fail: () => reject(new Error('未选择附件')) + fail: (error) => reject(fileSelectionFailure(error, '附件')) }); }); diff --git a/mobile-uni/src/services/personal-assistant.ts b/mobile-uni/src/services/personal-assistant.ts index 19876113..ae55ec6b 100644 --- a/mobile-uni/src/services/personal-assistant.ts +++ b/mobile-uni/src/services/personal-assistant.ts @@ -17,6 +17,7 @@ import type { PersonalSpace, ServiceMemory } from '@/types/api'; +import { fileSelectionFailure } from '../utils/file-selection'; import type { CaptureStatusHistory } from '@/types/api'; import { apiRequest, apiUrl, authHeaders, readTextPayload } from './api'; import { personalItemApiPath, personalSessionApiPath } from './personal-assistant-id'; @@ -145,7 +146,7 @@ export const choosePersonalFile = () => new Promise((resol const path = file?.path || result.tempFilePaths?.[0] || ''; if (!path) return reject(new Error('未获取到文件路径')); resolve({ path, name: file?.name || path.split('/').pop() || 'personal-file', size: Number(file?.size || 0) }); - }, fail: () => reject(new Error('未选择文件')) }); + }, fail: (error) => reject(fileSelectionFailure(error, '文件')) }); }); export const uploadPersonalFile = (file: PersonalSelectedFile, title?: string, capturedAt?: string) => diff --git a/mobile-uni/src/services/speech.ts b/mobile-uni/src/services/speech.ts index 9135897a..4c71e3d3 100644 --- a/mobile-uni/src/services/speech.ts +++ b/mobile-uni/src/services/speech.ts @@ -5,6 +5,7 @@ import type { SpeechCapture } from './speech-capture'; import { estimateMpegAudioDuration } from './mpeg-audio-duration'; import { dataUriToBlobUrl, decodeDataAudioUri, hasNativeAudioFileSupport, isDataAudioUri, removeNativeTempAudio, writeNativeTempAudio } from './speech-audio-source'; import { applyTtsVoicePreference, getTtsDialectPreference, getTtsVoicePreference } from './tts-voice-preference'; +import { fileSelectionFailure } from '../utils/file-selection'; const audioExtensions = ['.mp3', '.wav', '.m4a', '.webm', '.aac', '.ogg']; @@ -45,7 +46,7 @@ export const chooseSpeechAudio = () => size: Number(file.size || 0) }); }, - fail: () => reject(new Error('未选择音频')) + fail: (error) => reject(fileSelectionFailure(error, '音频')) }); }); diff --git a/mobile-uni/src/utils/file-selection.ts b/mobile-uni/src/utils/file-selection.ts new file mode 100644 index 00000000..09ced0d7 --- /dev/null +++ b/mobile-uni/src/utils/file-selection.ts @@ -0,0 +1,28 @@ +export const FILE_SELECTION_CANCELLED = 'FILE_SELECTION_CANCELLED'; + +export class FileSelectionCancelledError extends Error { + readonly code = FILE_SELECTION_CANCELLED; + + constructor(label: string) { + super(`未选择${label}`); + this.name = 'FileSelectionCancelledError'; + } +} + +const failureSignal = (failure: unknown): string => { + if (typeof failure === 'string') return failure; + if (!failure || typeof failure !== 'object') return ''; + const value = failure as { errMsg?: unknown; message?: unknown }; + return String(value.errMsg || value.message || ''); +}; + +export const fileSelectionFailure = (failure: unknown, label: string): Error => { + const signal = failureSignal(failure).toLowerCase(); + if (signal.includes('cancel')) return new FileSelectionCancelledError(label); + return new Error(`无法选择${label},请重试`); +}; + +export const isFileSelectionCancelled = (error: unknown): boolean => + error instanceof FileSelectionCancelledError + || Boolean(error && typeof error === 'object' + && (error as { code?: unknown }).code === FILE_SELECTION_CANCELLED); diff --git a/mobile-uni/tests/file-selection.test.mjs b/mobile-uni/tests/file-selection.test.mjs new file mode 100644 index 00000000..b8a37198 --- /dev/null +++ b/mobile-uni/tests/file-selection.test.mjs @@ -0,0 +1,102 @@ +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import { after, test } from 'node:test'; +import { fileURLToPath } from 'node:url'; +import { createServer } from 'vite'; + +const vite = await createServer({ + root: fileURLToPath(new URL('..', import.meta.url)), + configFile: false, + optimizeDeps: { noDiscovery: true }, + server: { middlewareMode: true, hmr: false }, + appType: 'custom' +}); + +const originalUni = globalThis.uni; +const read = (path) => readFile(new URL(path, import.meta.url), 'utf8'); + +after(async () => { + await vite.close(); + if (originalUni === undefined) delete globalThis.uni; + else globalThis.uni = originalUni; +}); + +const [ + fileSelection, + speech, + caseService, + candidate, + knowledge, + personalAssistant +] = await Promise.all([ + vite.ssrLoadModule('/src/utils/file-selection.ts'), + vite.ssrLoadModule('/src/services/speech.ts'), + vite.ssrLoadModule('/src/services/case.ts'), + vite.ssrLoadModule('/src/services/candidate.ts'), + vite.ssrLoadModule('/src/services/knowledge.ts'), + vite.ssrLoadModule('/src/services/personal-assistant.ts') +]); + +const pickers = [ + ['音频', speech.chooseSpeechAudio], + ['音频', caseService.chooseCaseAudio], + ['文件', candidate.chooseCandidateFile], + ['附件', knowledge.chooseKnowledgeMedia], + ['文件', personalAssistant.choosePersonalFile] +]; + +test('所有文件选择器把用户取消识别为同一可静默处理的信号', async () => { + globalThis.uni = { + chooseFile: ({ fail }) => fail({ errMsg: 'chooseFile:fail cancel' }) + }; + + for (const [label, picker] of pickers) { + await assert.rejects(picker(), (error) => { + assert.equal(fileSelection.isFileSelectionCancelled(error), true); + assert.equal(error.code, fileSelection.FILE_SELECTION_CANCELLED); + assert.equal(error.message, `未选择${label}`); + return true; + }); + } +}); + +test('系统选择失败不是用户取消,并只展示稳定中文提示', async () => { + globalThis.uni = { + chooseFile: ({ fail }) => fail({ errMsg: 'chooseFile:fail permission denied' }) + }; + + for (const [label, picker] of pickers) { + await assert.rejects(picker(), (error) => { + assert.equal(fileSelection.isFileSelectionCancelled(error), false); + assert.equal(error.message, `无法选择${label},请重试`); + assert.doesNotMatch(error.message, /permission|denied|chooseFile/i); + return true; + }); + } +}); + +test('无结构异常也不会被误判为取消', () => { + const error = fileSelection.fileSelectionFailure(undefined, '附件'); + assert.equal(fileSelection.isFileSelectionCancelled(error), false); + assert.equal(error.message, '无法选择附件,请重试'); +}); + +test('所有使用文件选择器的页面都按取消信号恢复空闲态而不展示错误', async () => { + const pages = await Promise.all([ + '../src/pages/candidate/interview/index.vue', + '../src/pages/candidate/materials/index.vue', + '../src/pages/user/web-ai/index.vue', + '../src/pages/user/today/index.vue', + '../src/pages/user/cases/index.vue', + '../src/pages/user/sop/index.vue', + '../src/pages/user/practice/index.vue', + '../src/pages/user/report/index.vue', + '../src/pages/user/assistant/capture.vue' + ].map(read)); + const combined = pages.join('\n'); + + for (const page of pages) assert.match(page, /isFileSelectionCancelled/); + assert.doesNotMatch(combined, /(?:!==|===)\s*['"]未选择(?:音频|附件|文件)['"]/); + assert.match(pages[0], /isFileSelectionCancelled\(error\)[\s\S]*?message\.value = ''[\s\S]*?return/); + assert.match(pages[6], /isFileSelectionCancelled\(error\)[\s\S]*?message\.value = ''[\s\S]*?return/); +});