const { getWxApi, isCancelError } = require('../utils/base-utils.js') const { formatBytes } = require('../utils/binary-utils.js') function formatExportStamp(date = new Date()) { const pad = (value, length = 2) => String(value).padStart(length, '0') return [ date.getFullYear(), pad(date.getMonth() + 1), pad(date.getDate()), '-', pad(date.getHours()), pad(date.getMinutes()), pad(date.getSeconds()) ].join('') } function normalizeExtensions(extensions = []) { return extensions .map((extension) => String(extension || '').trim().replace(/^\./, '').toLowerCase()) .filter(Boolean) } function escapeRegExp(text) { return String(text).replace(/[.*+?^${}()|[\]\\]/g, '\\$&') } function getRuntimeInfo() { const wxApi = getWxApi() try { if (typeof wxApi.getDeviceInfo === 'function') return wxApi.getDeviceInfo() || {} } catch (error) {} try { if (typeof wxApi.getSystemInfoSync === 'function') return wxApi.getSystemInfoSync() || {} } catch (error) {} return {} } function getRuntimePlatform() { const info = getRuntimeInfo() return String(info.platform || info.system || '').toLowerCase() } function isPcRuntime() { const platform = getRuntimePlatform() return /windows|mac|devtools/.test(platform) } function getPathFileName(filePath) { const text = String(filePath || '').split(/[\\/]/).pop() || '' try { return decodeURIComponent(text) } catch (error) { return text } } function getFileName(file, fallback = '未命名文件') { const name = file && (file.name || file.fileName) if (name) return String(name) return getPathFileName(getFilePath(file)) || fallback } function getFilePath(file) { return file && (file.path || file.tempFilePath || file.filePath) ? (file.path || file.tempFilePath || file.filePath) : '' } function getFileSize(file) { const size = Number(file && file.size) return Number.isFinite(size) && size >= 0 ? size : 0 } function normalizeSelectedFiles(result, fallbackName = '未命名文件') { const sourceFiles = Array.isArray(result && result.tempFiles) ? result.tempFiles : [] const pathFiles = Array.isArray(result && result.tempFilePaths) ? result.tempFilePaths.map((filePath) => ({ path: filePath })) : [] const directFile = result && (result.path || result.tempFilePath || result.filePath) ? [result] : [] return sourceFiles.concat(pathFiles, directFile) .map((file, index) => { const path = getFilePath(file) return { file, name: getFileName(file, index === 0 ? fallbackName : `${fallbackName}-${index + 1}`), path, size: getFileSize(file) } }) .filter((file) => !!file.path) } function getFirstSelectedFile(result, fallbackName = '未命名文件') { const files = normalizeSelectedFiles(result, fallbackName) const fileInfo = files[0] if (!fileInfo) throw new Error('没有选择文件') return fileInfo } function assertFileExtension(fileInfo, extensions = [], message = '文件格式不符') { const normalizedExtensions = normalizeExtensions(extensions) if (!normalizedExtensions.length) return const nameText = `${fileInfo && fileInfo.name ? fileInfo.name : ''} ${fileInfo && fileInfo.path ? fileInfo.path : ''}` const matched = normalizedExtensions.some((extension) => ( new RegExp(`\\.${escapeRegExp(extension)}$`, 'i').test(nameText) )) if (!matched) throw new Error(message) } function chooseMessageFile(options = {}) { const wxApi = getWxApi() const extensions = normalizeExtensions(options.extensions || options.extension || []) const type = extensions.length ? 'file' : (options.type || 'all') return new Promise((resolve, reject) => { if (typeof wxApi.chooseMessageFile !== 'function') { reject(new Error('当前微信版本不支持从聊天记录选择文件,请升级微信或将文件转发到文件传输助手后重试')) return } const chooseOptions = { count: options.count || 1, type, success: resolve, fail: reject } if (extensions.length) chooseOptions.extension = extensions wxApi.chooseMessageFile(chooseOptions) }) } function chooseLocalFile(options = {}) { const wxApi = getWxApi() const extensions = normalizeExtensions(options.extensions || options.extension || []) const type = extensions.length ? 'file' : (options.type || 'all') return new Promise((resolve, reject) => { if (typeof wxApi.chooseFile !== 'function') { reject(new Error(options.unsupportedMessage || '当前微信环境不支持本地文件选择,请将文件发送到文件传输助手后从聊天记录选择')) return } const chooseOptions = { count: options.count || 1, type, success: resolve, fail: reject } if (extensions.length) chooseOptions.extension = extensions wxApi.chooseFile(chooseOptions) }) } async function chooseFile(source = 'message', options = {}) { const normalizedSource = source === 'local' || source === 'auto' ? source : 'message' // PC 微信更接近本地文件选择,移动端更稳定的是从聊天记录取文件。 const preferLocal = normalizedSource === 'local' || (normalizedSource === 'auto' && isPcRuntime()) const firstChooser = preferLocal ? chooseLocalFile : chooseMessageFile const fallbackChooser = preferLocal ? chooseMessageFile : chooseLocalFile try { return await firstChooser(options) } catch (error) { if (isCancelError(error) || options.fallback === false) throw error if (normalizedSource !== 'auto' && options.fallbackToOtherSource === false) throw error try { return await fallbackChooser(options) } catch (fallbackError) { if (isCancelError(fallbackError)) throw fallbackError const firstMessage = error && (error.errMsg || error.message || error) const fallbackMessage = fallbackError && (fallbackError.errMsg || fallbackError.message || fallbackError) throw new Error([ firstMessage || '文件选择失败', fallbackMessage || '' ].filter(Boolean).join(';')) } } } function readFile(filePath, options = {}) { const wxApi = getWxApi() return new Promise((resolve, reject) => { if (typeof wxApi.getFileSystemManager !== 'function') { reject(new Error('当前微信版本不支持读取文件')) return } const fs = wxApi.getFileSystemManager() const readOptions = { filePath, success: (res) => resolve(res.data), fail: reject } if (options.encoding) readOptions.encoding = options.encoding fs.readFile(readOptions) }) } function toUint8Array(data) { if (data instanceof Uint8Array) return data if (data instanceof ArrayBuffer) return new Uint8Array(data) if (ArrayBuffer.isView(data)) { return new Uint8Array(data.buffer, data.byteOffset, data.byteLength) } return new Uint8Array(data || new ArrayBuffer(0)) } async function loadSelectedFile(source = 'message', options = {}) { const result = await chooseFile(source, options) const fileInfo = getFirstSelectedFile(result, options.fallbackName || '未命名文件') assertFileExtension(fileInfo, options.extensions || options.extension || [], options.extensionMessage || '文件格式不符') const data = await readFile(fileInfo.path, { encoding: options.encoding }) const bytes = options.encoding ? null : toUint8Array(data) return { ...fileInfo, bytes, data, size: bytes ? bytes.length : (fileInfo.size || String(data || '').length), sizeText: formatBytes(bytes ? bytes.length : (fileInfo.size || String(data || '').length)), text: options.encoding ? String(data || '') : '' } } function getUserDataFilePath(fileName) { const wxApi = getWxApi() const userDataPath = wxApi.env && wxApi.env.USER_DATA_PATH if (!userDataPath) throw new Error('当前微信版本不支持生成文件') return `${userDataPath}/${fileName}` } function writeTextFile(filePath, data, encoding = 'utf8') { const wxApi = getWxApi() return new Promise((resolve, reject) => { if (typeof wxApi.getFileSystemManager !== 'function') { reject(new Error('当前微信版本不支持生成文件')) return } const fs = wxApi.getFileSystemManager() if (typeof fs.writeFileSync === 'function') { try { fs.writeFileSync(filePath, data, encoding) resolve(filePath) } catch (error) { reject(error) } return } if (typeof fs.writeFile !== 'function') { reject(new Error('当前微信版本不支持生成文件')) return } fs.writeFile({ data, encoding, filePath, fail: reject, success: () => resolve(filePath) }) }) } function saveFileToDisk(filePath) { const wxApi = getWxApi() return new Promise((resolve, reject) => { if (typeof wxApi.saveFileToDisk !== 'function') { reject(new Error('当前微信环境不支持保存到电脑磁盘')) return } wxApi.saveFileToDisk({ filePath, fail: reject, success: resolve }) }) } function shareFileToChat(filePath, fileName) { const wxApi = getWxApi() return new Promise((resolve, reject) => { if (typeof wxApi.shareFileMessage !== 'function') { reject(new Error('当前微信版本不支持发送文件到聊天')) return } wxApi.shareFileMessage({ fileName, filePath, success: resolve, fail: reject }) }) } async function exportFile(filePath, fileName, options = {}) { const wxApi = getWxApi() const preferDisk = options.target === 'disk' || (options.target !== 'chat' && isPcRuntime() && typeof wxApi.saveFileToDisk === 'function') if (preferDisk && typeof wxApi.saveFileToDisk === 'function') { try { await saveFileToDisk(filePath) return { method: 'disk' } } catch (error) { if (isCancelError(error) || typeof wxApi.shareFileMessage !== 'function') throw error } } if (typeof wxApi.shareFileMessage === 'function') { await shareFileToChat(filePath, fileName) return { method: 'chat' } } if (typeof wxApi.saveFileToDisk === 'function') { await saveFileToDisk(filePath) return { method: 'disk' } } throw new Error('当前微信环境不支持导出文件,请升级微信后重试') } async function saveTextFileToChat(fileName, data) { const filePath = getUserDataFilePath(fileName) await writeTextFile(filePath, data, 'utf8') const result = await exportFile(filePath, fileName) return { filePath, ...result } } module.exports = { assertFileExtension, chooseFile, chooseLocalFile, chooseMessageFile, exportFile, formatBytes, formatExportStamp, getFirstSelectedFile, getRuntimePlatform, getUserDataFilePath, isCancelError, isPcRuntime, loadSelectedFile, normalizeSelectedFiles, readFile, saveFileToDisk, saveTextFileToChat, shareFileToChat, toUint8Array, writeTextFile }