([\s\S]*?)<\/table>/g, (match) => {
const encodedTable = encodeURIComponent(match);
return ``;
});
};
const decodeHtmlEntities = (escaped: string): string => {
const textarea = document.createElement('textarea');
textarea.innerHTML = escaped;
return textarea.value;
};
// 给渲染后 HTML 中的 ```markdown / ```md 代码块加上操作工具栏(复制/下载/预览)
export const wrapMarkdownCodeBlocks = (html: string, icons: { copy: string; download: string; preview: string; copySuccess: string }): string => {
return html.replace(/([\s\S]*?)<\/code><\/pre>/g, (match, escaped: string) => {
const encoded = encodeURIComponent(decodeHtmlEntities(escaped));
return ``;
});
};
export const tableHtmlToMarkdown = (tableHtml: string): string => {
const parser = new DOMParser();
const doc = parser.parseFromString(tableHtml, 'text/html');
const table = doc.querySelector('table');
if (!table) return tableHtml;
const rows: string[][] = [];
table.querySelectorAll('tr').forEach((tr) => {
const cells: string[] = [];
tr.querySelectorAll('th, td').forEach((cell) => {
cells.push(cell.textContent?.trim() || '');
});
rows.push(cells);
});
if (rows.length === 0) return tableHtml;
const colCount = Math.max(...rows.map((r) => r.length));
const normalized = rows.map((r) => {
while (r.length < colCount) r.push('');
return r;
});
const lines: string[] = [];
lines.push('| ' + normalized[0].join(' | ') + ' |');
lines.push('| ' + normalized[0].map(() => '---').join(' | ') + ' |');
for (let i = 1; i < normalized.length; i++) {
lines.push('| ' + normalized[i].join(' | ') + ' |');
}
return lines.join('\n');
};
export const tableHtmlToCsv = (tableHtml: string): string => {
const parser = new DOMParser();
const doc = parser.parseFromString(tableHtml, 'text/html');
const table = doc.querySelector('table');
if (!table) return '';
const rows: string[] = [];
table.querySelectorAll('tr').forEach((tr) => {
const cells: string[] = [];
tr.querySelectorAll('th, td').forEach((cell) => {
const text = (cell.textContent?.trim() || '').replace(/"/g, '""');
cells.push(`"${text}"`);
});
rows.push(cells.join(','));
});
return '' + rows.join('\n');
};
export const formatFileSize = (bytes: number): string => {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + ' KB';
return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
};
export const isImageFile = (filename: string): boolean => /\.(jpg|jpeg|png|gif|bmp|webp|svg|ico|tiff|tif)$/i.test(filename);
export const isPdfFile = (filename: string): boolean => /\.pdf$/i.test(filename);
export const isVideoFile = (filename: string): boolean => /\.(mp4|webm|ogg|mov|avi|mkv|flv|wmv|m4v)$/i.test(filename);
export const isAudioFile = (filename: string): boolean => /\.(mp3|wav|ogg|aac|flac|wma|m4a|opus)$/i.test(filename);
export const isMdFile = (filename: string): boolean => /\.md$/i.test(filename);
export const isWordFile = (filename: string): boolean => /\.(doc|docx)$/i.test(filename);
export const isOldDocFile = (filename: string): boolean => /\.doc$/i.test(filename) && !/\.docx$/i.test(filename);
export const isExcelFile = (filename: string): boolean => /\.(xls|xlsx)$/i.test(filename);
export const isCsvFile = (filename: string): boolean => /\.csv$/i.test(filename);
export const isTextFile = (filename: string): boolean =>
/\.(txt|md|json|csv|xml|yaml|yml|log|ini|toml|cfg|conf|env|gitignore|dockerignore|editorconfig|prettierrc|eslintrc|babelrc|properties|sh|bat|ps1|cmd|bash|zsh|fish|sql|graphql|proto|js|ts|jsx|tsx|vue|css|scss|sass|less|html|htm|svelte|py|java|kt|kts|c|cpp|h|hpp|cc|cxx|cs|go|rs|rb|php|swift|m|mm|r|R|lua|pl|pm|hs|ex|exs|erl|clj|scala|dart|zig|nim|v|sol|tf|hcl|gradle|cmake|makefile|mk)$/i.test(
filename
);
export const getFileExtension = (filename: string): string => {
const match = filename.match(/\.([^.]+)$/);
return match ? `.${match[1]}`.toUpperCase() : '';
};
export const getFileIconVar = (filename: string): string => {
if (isMdFile(filename)) return 'var(--img-chat-file-md-icon)';
if (isPdfFile(filename)) return 'var(--img-chat-file-pdf-icon)';
if (isWordFile(filename)) return 'var(--img-chat-file-word-icon)';
if (isExcelFile(filename) || isCsvFile(filename)) return 'var(--img-chat-file-excel-icon)';
if (isImageFile(filename)) return 'var(--img-chat-file-img-icon)';
if (isTextFile(filename)) return 'var(--img-chat-file-txt-icon)';
return 'var(--img-chat-attach-icon)';
};
// 按文件类型拉取远端文件内容并构造右侧面板预览对象(消息内文件卡片与胶囊面板共用)。
// 不支持的格式返回 null,由调用方回退为直接下载
export const fetchPreviewFile = async (url: string, filename: string): Promise => {
const resp = await fetch(url, { headers: { 'X-Access-Token': getToken() } });
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const ext = filename.split('.').pop()?.toLowerCase() || '';
const mimeMap: Record = {
md: 'text/markdown',
txt: 'text/plain',
json: 'application/json',
xml: 'text/xml',
yaml: 'text/yaml',
yml: 'text/yaml',
csv: 'text/csv',
};
const id = `report-${url}`;
const uploadTime = new Date().toISOString();
if (mimeMap[ext]) {
const content = await resp.text();
return { id, name: filename, size: content.length, type: mimeMap[ext], content, uploadTime };
}
if (isWordFile(filename) || isExcelFile(filename)) {
const arrayBuffer = await resp.arrayBuffer();
return { id, name: filename, size: arrayBuffer.byteLength, type: 'application/octet-stream', arrayBuffer, uploadTime };
}
if (isImageFile(filename) || isPdfFile(filename)) {
const blob = await resp.blob();
const dataUrl = await new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result as string);
reader.onerror = () => reject(reader.error);
reader.readAsDataURL(blob);
});
return { id, name: filename, size: blob.size, type: blob.type, preview: dataUrl, uploadTime };
}
return null;
};