InfoRadar / client /src /ts /utils /demoPathUtils.ts
dqy08's picture
增加请求缓存功能; 解耦聊天模板和生成api;支持最大新token限制
4030987
/**
* Demo路径和文件名工具函数
* 统一管理所有与demo路径、文件名相关的工具函数
*/
import type { AnalyzeResponse } from '../api/GLTR_API';
import { tr } from '../lang/i18n-lite';
/**
* 标准化文件夹路径
* - 确保以 / 开头
* - 去除多余的连续斜杠
* - 去除尾部斜杠(除非是根路径 /)
* - 空字符串或 null/undefined 返回 '/'
*
* @param path 原始路径
* @returns 标准化后的路径,空路径返回 '/'
*
* @example
* normalizeFolderPath('') => '/'
* normalizeFolderPath(null) => '/'
* normalizeFolderPath('/folder1//folder2/') => '/folder1/folder2'
* normalizeFolderPath('folder1/folder2') => '/folder1/folder2'
*/
export const normalizeFolderPath = (path: string | null | undefined): string => {
if (!path || path.trim() === '' || path === '/') {
return '/';
}
const prefixed = path.startsWith('/') ? path : `/${path}`;
const condensed = prefixed.replace(/\/{2,}/g, '/');
return condensed.length > 1 && condensed.endsWith('/') ? condensed.slice(0, -1) : condensed;
};
/**
* 组合 demo 的完整路径
*
* @param folderPath 文件夹路径
* @param fileName 文件名(不含扩展名)
* @returns 完整路径,如果文件名为空则返回 null
*
* @example
* composeDemoFullPath('/', 'demo1') => '/demo1'
* composeDemoFullPath('/folder1', 'demo1') => '/folder1/demo1'
*/
export const composeDemoFullPath = (folderPath: string | null | undefined, fileName: string | null | undefined): string | null => {
if (!fileName || !fileName.trim()) {
return null;
}
const normalizedFolder = normalizeFolderPath(folderPath);
const safeFileName = fileName.trim();
return normalizedFolder === '/' ? `/${safeFileName}` : `${normalizedFolder}/${safeFileName}`;
};
/**
* 验证文件名合法性
* @param fileName 文件名(不含扩展名)
* @returns 验证结果:{ valid: boolean, message?: string }
*/
export const validateFileName = (fileName: string): { valid: boolean; message?: string } => {
if (!fileName || !fileName.trim()) {
return { valid: false, message: 'File name cannot be empty' };
}
const trimmed = fileName.trim();
// 检查长度
if (trimmed.length > 255) {
return { valid: false, message: 'File name too long (max 255 characters)' };
}
// Windows 和 Unix 系统都不允许的字符
const illegalChars = /[<>:"|?*\x00-\x1f]/;
if (illegalChars.test(trimmed)) {
return { valid: false, message: 'File name contains invalid characters (cannot contain < > : " | ? * or control characters)' };
}
// 检查保留名称(Windows)
const reservedNames = /^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(\.|$)/i;
if (reservedNames.test(trimmed)) {
return { valid: false, message: 'File name cannot be a system reserved name' };
}
// 检查不能以点或空格开头/结尾(某些系统)
if (trimmed.startsWith('.') || trimmed.endsWith('.')) {
return { valid: false, message: 'File name cannot start or end with a dot' };
}
// 检查不能包含路径分隔符
if (trimmed.includes('/') || trimmed.includes('\\')) {
return { valid: false, message: 'File name cannot contain path separators' };
}
return { valid: true };
};
/**
* 生成 demo 默认名称:优先使用现有文件名,否则取分析文本第一行的前50个字符
*
* @param currentData 当前分析数据
* @param textFieldValue 文本输入框的值
* @param existingFileName 可选的现有文件名(如果提供且有效,则优先使用)
* @returns 默认名称
*/
export const getDefaultDemoName = (
currentData: AnalyzeResponse | null,
textFieldValue: string,
existingFileName?: string | null
): string => {
// 如果提供了现有文件名且有效,则使用它(去掉 .json 后缀)
if (existingFileName && existingFileName.trim() && existingFileName !== '未选择文件') {
const trimmed = existingFileName.trim();
// 去掉 .json 后缀(如果存在)
const nameWithoutExt = trimmed.toLowerCase().endsWith('.json')
? trimmed.slice(0, -5)
: trimmed;
if (nameWithoutExt) {
return nameWithoutExt;
}
}
// 否则,使用第一行逻辑(与主文本框一致不 trim)
const rawText = currentData ? currentData.request.text : textFieldValue || '';
if (!rawText) {
return '新Demo';
}
const firstLineBreak = rawText.search(/[\r\n]/);
const firstLine = firstLineBreak === -1 ? rawText : rawText.slice(0, firstLineBreak);
return (firstLine.length ? firstLine : '新Demo').slice(0, 50);
};
/**
* 构建保存目录选项(根目录优先)
* 统一文件夹排序和选项生成逻辑
*
* @param folders 文件夹列表
* @param lastPath 上次保存的路径(从 localStorage 获取)
* @returns 选项列表和默认路径
*/
export const buildFolderOptions = (
folders: string[],
lastPath: string | null
): { options: Array<{ value: string; text: string }>, defaultPath: string } => {
// 排序文件夹列表:根目录排在最前面
const sorted = [...folders].sort((a, b) => {
const normA = normalizeFolderPath(a);
const normB = normalizeFolderPath(b);
if (normA === '/' && normB !== '/') return -1;
if (normA !== '/' && normB === '/') return 1;
return normA.localeCompare(normB, 'zh-CN', { numeric: true, sensitivity: 'base' });
});
// 转换为选项格式
const options = sorted.map(folder => ({
value: folder,
text: folder === '' || folder === '/' ? tr('/ (Root)') : folder
}));
// 设置默认路径:优先使用上次保存的路径,如果不存在则使用根目录
let defaultPath = '/';
if (lastPath && sorted.includes(lastPath)) {
defaultPath = lastPath;
} else if (sorted.length > 0) {
defaultPath = sorted[0] || '/';
}
return { options, defaultPath };
};