chore: initialize qiming workspace repository
This commit is contained in:
472
qiming-mobile/subpackages/utils/fileTree.uts
Normal file
472
qiming-mobile/subpackages/utils/fileTree.uts
Normal file
@@ -0,0 +1,472 @@
|
||||
import type { FileNode } from '@/types/interfaces/agent'
|
||||
import { ACCESS_TOKEN } from '@/constants/home.constants'
|
||||
import { COMMON_HEADERS } from '@/constants/common.constants'
|
||||
import { t } from "@/utils/i18n";
|
||||
|
||||
/**
|
||||
* 文件相关常量
|
||||
*/
|
||||
export const FILE_CONSTANTS = {
|
||||
// 支持预览的文件扩展名白名单
|
||||
SUPPORTED_EXTENSIONS: [
|
||||
// 图片文件
|
||||
'jpg',
|
||||
'jpeg',
|
||||
'png',
|
||||
'gif',
|
||||
'bmp',
|
||||
'webp',
|
||||
'svg',
|
||||
'ico',
|
||||
'tiff',
|
||||
// 代码文件
|
||||
'ts',
|
||||
'tsx',
|
||||
'js',
|
||||
'jsx',
|
||||
'mjs',
|
||||
'cjs',
|
||||
'css',
|
||||
'less',
|
||||
'scss',
|
||||
'sass',
|
||||
'html',
|
||||
'htm',
|
||||
'vue',
|
||||
'json',
|
||||
'jsonc',
|
||||
'yaml',
|
||||
'yml',
|
||||
'xml',
|
||||
'toml',
|
||||
'ini',
|
||||
'py',
|
||||
'java',
|
||||
'c',
|
||||
'cpp',
|
||||
'cs',
|
||||
'php',
|
||||
'rb',
|
||||
'go',
|
||||
'rs',
|
||||
'swift',
|
||||
'kt',
|
||||
'scala',
|
||||
'sh',
|
||||
'bash',
|
||||
'zsh',
|
||||
'fish',
|
||||
'ps1',
|
||||
'bat',
|
||||
'sql',
|
||||
'dockerfile',
|
||||
'makefile',
|
||||
// 文本文件
|
||||
'txt',
|
||||
'md',
|
||||
'markdown',
|
||||
'log',
|
||||
'csv',
|
||||
'tsv',
|
||||
'rtf',
|
||||
],
|
||||
// 忽略的文件模式
|
||||
IGNORED_FILE_PATTERNS: [
|
||||
/^\./, // 以 . 开头的隐藏文件
|
||||
/^\.DS_Store$/,
|
||||
/^Thumbs\.db$/,
|
||||
/\.tmp$/,
|
||||
/\.bak$/,
|
||||
],
|
||||
DEFAULT_FILE_LANGUAGE: 'Plain Text',
|
||||
FALLBACK_SIZE: 0,
|
||||
TREE_ROOT_LEVEL: 0,
|
||||
INDENT_SIZE: 16,
|
||||
REQUEST_ID_PREFIX: 'req_',
|
||||
SESSION_ID_PREFIX: 'session_',
|
||||
};
|
||||
|
||||
/**
|
||||
* 将扁平的文件列表转换为树形结构
|
||||
*/
|
||||
export const transformFlatListToTree = (files: any[]): FileNode[] => {
|
||||
const root: FileNode[] = [];
|
||||
const map = new Map<string, FileNode>();
|
||||
|
||||
// 过滤掉系统文件
|
||||
const filteredFiles = files.filter((file) => {
|
||||
const fileName = file.name.split('/').pop();
|
||||
return !FILE_CONSTANTS.IGNORED_FILE_PATTERNS.some((pattern) =>
|
||||
pattern.test(fileName || ''),
|
||||
);
|
||||
});
|
||||
|
||||
// 创建所有文件节点和必要的文件夹节点
|
||||
filteredFiles.forEach((file) => {
|
||||
const pathParts = file.name.split('/').filter(Boolean);
|
||||
const fileName = pathParts[pathParts.length - 1];
|
||||
// 如果文件是目录,则认为是文件(后端给了isDir字段,表示是否为目录),兼容之前逻辑
|
||||
const isFile = !file.isDir || fileName.includes('.');
|
||||
|
||||
const node: FileNode = {
|
||||
id: file.name,
|
||||
name: fileName,
|
||||
type: isFile ? 'file' : 'folder',
|
||||
path: file.name,
|
||||
children: [],
|
||||
binary: file.binary || false,
|
||||
size:
|
||||
file.size || file.sizeExceeded
|
||||
? FILE_CONSTANTS.FALLBACK_SIZE
|
||||
: file.contents?.length || FILE_CONSTANTS.FALLBACK_SIZE,
|
||||
status: file.status || null,
|
||||
fullPath: file.name,
|
||||
parentPath: pathParts.slice(0, -1).join('/') || null,
|
||||
content: file.contents || '',
|
||||
lastModified: Date.now(),
|
||||
fileProxyUrl: file?.fileProxyUrl || '',
|
||||
isLink: file?.isLink || false,
|
||||
};
|
||||
|
||||
map.set(file.name, node);
|
||||
|
||||
// 如果文件在子目录中,确保创建所有必要的父文件夹节点
|
||||
if (pathParts.length > 1) {
|
||||
for (let i = pathParts.length - 2; i >= 0; i--) {
|
||||
const parentPath = pathParts.slice(0, i + 1).join('/');
|
||||
const parentName = pathParts[i];
|
||||
|
||||
if (!map.has(parentPath)) {
|
||||
const parentNode: FileNode = {
|
||||
id: parentPath,
|
||||
name: parentName,
|
||||
type: 'folder',
|
||||
path: parentPath,
|
||||
children: [],
|
||||
parentPath: i > 0 ? pathParts.slice(0, i).join('/') : null,
|
||||
lastModified: Date.now(),
|
||||
};
|
||||
map.set(parentPath, parentNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 构建层次结构
|
||||
map.forEach((node) => {
|
||||
if (node.parentPath && map.has(node.parentPath)) {
|
||||
const parentNode = map.get(node.parentPath)!;
|
||||
if (
|
||||
!parentNode.children?.find((child: FileNode) => child.id === node.id)
|
||||
) {
|
||||
parentNode.children?.push(node);
|
||||
}
|
||||
} else if (!node.parentPath) {
|
||||
if (!root.find((item: FileNode) => item.id === node.id)) {
|
||||
root.push(node);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 排序:文件夹在前,文件在后,同类型按名称排序
|
||||
const sortNodes = (nodes: FileNode[]): FileNode[] => {
|
||||
return nodes.sort((a, b) => {
|
||||
if (a.type !== b.type) {
|
||||
return a.type === 'folder' ? -1 : 1;
|
||||
}
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
};
|
||||
|
||||
return sortNodes(root).map((node) => ({
|
||||
...node,
|
||||
children: node.children ? sortNodes(node.children) : undefined,
|
||||
}));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 从响应头中解析文件名
|
||||
* @param contentDisposition Content-Disposition 响应头
|
||||
* @param defaultName 默认文件名
|
||||
*/
|
||||
const parseFileName = (contentDisposition: string | undefined, defaultName: string): string => {
|
||||
if (!contentDisposition) return defaultName;
|
||||
|
||||
// 解析 Content-Disposition 头中的文件名
|
||||
const filenameMatch = contentDisposition.match(
|
||||
/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/,
|
||||
);
|
||||
if (filenameMatch && filenameMatch[1]) {
|
||||
return filenameMatch[1].replace(/['"]/g, '');
|
||||
}
|
||||
return defaultName;
|
||||
};
|
||||
|
||||
/**
|
||||
* H5 环境:导出整个项目压缩包
|
||||
* @param blob Blob 对象
|
||||
* @param headers 响应头
|
||||
* @param defaultName 默认文件名称
|
||||
*/
|
||||
// #ifdef H5 || WEB
|
||||
export const exportWholeProjectZipH5 = async (blob: Blob, headers: any, defaultName: string) => {
|
||||
// 从响应头中获取文件名
|
||||
const contentDisposition = headers?.['content-disposition'];
|
||||
const filename = parseFileName(contentDisposition, defaultName);
|
||||
|
||||
// 创建下载链接
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
|
||||
// 触发下载
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
// 清理URL对象
|
||||
window.URL.revokeObjectURL(url);
|
||||
};
|
||||
// #endif
|
||||
|
||||
/**
|
||||
* 小程序环境:下载文件并打包成zip
|
||||
* @param downloadUrl 下载地址(应该是返回zip包的接口)
|
||||
* @param filename 文件名称
|
||||
* @returns 返回保存的zip文件路径
|
||||
*/
|
||||
// #ifdef MP-WEIXIN || MP-ALIPAY || MP-BAIDU || MP-TOUTIAO || MP-QQ
|
||||
export const downloadFileInMiniProgram = async (downloadUrl: string, filename: string): Promise<string> => {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
// 获取访问令牌并构建请求头
|
||||
const accessToken = uni.getStorageSync(ACCESS_TOKEN);
|
||||
const header: any = {
|
||||
// ...COMMON_HEADERS,
|
||||
};
|
||||
if (accessToken) {
|
||||
header['Authorization'] = `Bearer ${accessToken}`;
|
||||
}
|
||||
|
||||
uni.showLoading({ title: t("Mobile.FileTree.downloading") });
|
||||
uni.downloadFile({
|
||||
url: downloadUrl,
|
||||
header, // 添加请求头,确保认证通过
|
||||
success: (res) => {
|
||||
console.log('下载文件响应:', res);
|
||||
|
||||
if (res.statusCode === 200) {
|
||||
// 验证下载的文件是否为zip格式
|
||||
const fs = uni.getFileSystemManager();
|
||||
const tempPath = res.tempFilePath;
|
||||
|
||||
// 检查文件信息
|
||||
fs.stat({
|
||||
path: tempPath,
|
||||
success: (statRes) => {
|
||||
console.log('文件信息:', statRes);
|
||||
|
||||
// 如果文件太小(小于1KB),可能是JSON错误响应
|
||||
if (statRes.size < 1024) {
|
||||
// 尝试读取文件内容检查是否为JSON
|
||||
fs.readFile({
|
||||
filePath: tempPath,
|
||||
encoding: 'utf8',
|
||||
success: (readRes) => {
|
||||
try {
|
||||
const content = readRes.data as string;
|
||||
if (content.trim().startsWith('{') || content.trim().startsWith('[')) {
|
||||
// 是JSON响应,说明下载失败
|
||||
uni.hideLoading();
|
||||
const errorData = JSON.parse(content);
|
||||
uni.showToast({
|
||||
title:
|
||||
errorData.message ||
|
||||
t("Mobile.FileTree.downloadServerError"),
|
||||
icon: 'none',
|
||||
duration: 3000,
|
||||
});
|
||||
reject(
|
||||
new Error(
|
||||
errorData.message ||
|
||||
t("Mobile.FileTree.serverError"),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
// 不是JSON,继续处理
|
||||
}
|
||||
},
|
||||
fail: () => {
|
||||
// 读取失败,继续保存流程
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 先使用 uni.saveFile 保存文件,然后尝试重命名为正确的zip文件名
|
||||
// 确保文件名有 .zip 扩展名
|
||||
const zipFileName = filename.endsWith('.zip') ? filename : `${filename}.zip`;
|
||||
|
||||
uni.saveFile({
|
||||
tempFilePath: tempPath,
|
||||
success: (saveRes) => {
|
||||
let savedFilePath = saveRes.savedFilePath;
|
||||
console.log('初始保存路径:', savedFilePath);
|
||||
|
||||
// 如果保存的文件没有正确的扩展名,尝试重命名
|
||||
if (!savedFilePath.endsWith('.zip')) {
|
||||
// 获取文件目录和原文件名
|
||||
const pathParts = savedFilePath.split('/');
|
||||
const dirPath = pathParts.slice(0, -1).join('/');
|
||||
const newPath = `${dirPath}/${zipFileName}`;
|
||||
|
||||
// 尝试重命名文件(异步方式)
|
||||
fs.rename({
|
||||
oldPath: savedFilePath,
|
||||
newPath: newPath,
|
||||
success: () => {
|
||||
savedFilePath = newPath;
|
||||
console.log('文件已重命名为:', savedFilePath);
|
||||
finishSave(savedFilePath, zipFileName);
|
||||
},
|
||||
fail: (renameErr) => {
|
||||
console.warn('重命名文件失败,尝试复制方式:', renameErr);
|
||||
|
||||
// 如果重命名失败,尝试读取原文件并写入新文件
|
||||
fs.readFile({
|
||||
filePath: savedFilePath,
|
||||
success: (readFileRes) => {
|
||||
fs.writeFile({
|
||||
filePath: newPath,
|
||||
data: readFileRes.data as ArrayBuffer,
|
||||
success: () => {
|
||||
// 删除旧文件
|
||||
fs.unlink({
|
||||
filePath: savedFilePath,
|
||||
success: () => {
|
||||
console.log('已删除旧文件');
|
||||
},
|
||||
fail: (e) => {
|
||||
console.warn('删除旧文件失败:', e);
|
||||
},
|
||||
});
|
||||
savedFilePath = newPath;
|
||||
console.log('文件已复制并重命名为:', savedFilePath);
|
||||
finishSave(savedFilePath, zipFileName);
|
||||
},
|
||||
fail: (writeErr) => {
|
||||
console.error('写入新文件失败:', writeErr);
|
||||
// 如果复制也失败,使用原路径
|
||||
finishSave(savedFilePath, zipFileName);
|
||||
},
|
||||
});
|
||||
},
|
||||
fail: (readErr) => {
|
||||
console.error('读取文件失败:', readErr);
|
||||
// 如果读取失败,使用原路径
|
||||
finishSave(savedFilePath, zipFileName);
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// 已经有正确的扩展名,直接完成保存
|
||||
finishSave(savedFilePath, zipFileName);
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
uni.hideLoading();
|
||||
console.error('保存ZIP文件失败:', err);
|
||||
uni.showToast({
|
||||
title: t("Mobile.FileTree.saveFailed"),
|
||||
icon: 'none',
|
||||
duration: 2000,
|
||||
});
|
||||
reject(err);
|
||||
},
|
||||
});
|
||||
|
||||
// 完成保存流程的辅助函数
|
||||
const finishSave = (filePath: string, zipFileName: string) => {
|
||||
uni.hideLoading();
|
||||
|
||||
// 弹窗提示用户文件保存位置
|
||||
uni.showModal({
|
||||
title: t("Mobile.FileTree.downloadComplete"),
|
||||
content: t("Mobile.FileTree.zipSavedContent", {
|
||||
fileName: zipFileName,
|
||||
filePath,
|
||||
}),
|
||||
confirmText: t("Mobile.Common.open"),
|
||||
cancelText: t("Mobile.Common.cancel"),
|
||||
success: (modalRes) => {
|
||||
if (modalRes.confirm) {
|
||||
// 打开文件
|
||||
uni.openDocument({
|
||||
filePath: filePath,
|
||||
fileType: 'zip',
|
||||
showMenu: true,
|
||||
success: () => {
|
||||
console.log('打开文件成功');
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('打开文件失败:', err);
|
||||
uni.showToast({
|
||||
title: t("Mobile.FileTree.openFailed"),
|
||||
icon: 'none',
|
||||
duration: 3000,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
resolve(filePath);
|
||||
};
|
||||
},
|
||||
fail: (statErr) => {
|
||||
uni.hideLoading();
|
||||
console.error('获取文件信息失败:', statErr);
|
||||
uni.showToast({
|
||||
title: t("Mobile.FileTree.fileValidateFailed"),
|
||||
icon: 'none',
|
||||
duration: 2000,
|
||||
});
|
||||
reject(statErr);
|
||||
},
|
||||
});
|
||||
} else {
|
||||
uni.hideLoading();
|
||||
uni.showToast({
|
||||
title: t("Mobile.FileTree.downloadFailedWithCode", {
|
||||
code: res.statusCode,
|
||||
}),
|
||||
icon: 'none',
|
||||
duration: 2000,
|
||||
});
|
||||
reject(
|
||||
new Error(
|
||||
t("Mobile.FileTree.downloadFailedWithCode", {
|
||||
code: res.statusCode,
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
uni.hideLoading();
|
||||
console.error('下载文件失败:', err);
|
||||
uni.showToast({
|
||||
title: t("Mobile.FileTree.downloadNetworkFailed"),
|
||||
icon: 'none',
|
||||
duration: 2000,
|
||||
});
|
||||
reject(err);
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
// #endif
|
||||
Reference in New Issue
Block a user