Files
TaskPool/web/src/lib/log-render.ts
T
admin e6956aa001 Initial commit: TaskPool React panel
- React frontend with route-level code splitting
- Backend rebranded from Baihu to TaskPool
- DB brand migration script and local compatibility
2026-07-26 08:43:52 +08:00

104 lines
2.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 日志内容处理:base64 压缩解压 + ANSI 转 HTML
*/
/** 尝试将后端可能返回的压缩/base64 内容还原为明文 */
export function decodeLogContent(raw: string): string {
if (!raw) return ''
// 若包含 ANSI 或明显是明文,直接返回
if (raw.includes('\u001b[') || raw.includes('\n') || raw.length < 40) return raw
// 纯 base64 形态尝试解码
const compact = raw.replace(/\s+/g, '')
if (!/^[A-Za-z0-9+/=]+$/.test(compact) || compact.length % 4 !== 0) return raw
try {
const binary = atob(compact)
// 尝试 inflate(部分后端用 gzip/deflate 再 base64
if (typeof DecompressionStream !== 'undefined') {
// 同步路径:先按 UTF-8 解码;若失败再当明文 base64 文本
try {
const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0))
// 非 gzip 魔数则直接当 UTF-8
if (bytes[0] === 0x1f && bytes[1] === 0x8b) {
// 异步不适合这里,回退文本
}
return new TextDecoder().decode(bytes)
} catch {
return binary
}
}
return decodeURIComponent(escape(binary))
} catch {
return raw
}
}
/** 转义 HTML */
function escapeHtml(s: string) {
return s
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
}
const ANSI_COLORS: Record<string, string> = {
'30': '#000',
'31': '#ef4444',
'32': '#22c55e',
'33': '#eab308',
'34': '#3b82f6',
'35': '#a855f7',
'36': '#06b6d4',
'37': '#e5e7eb',
'90': '#6b7280',
'91': '#f87171',
'92': '#4ade80',
'93': '#facc15',
'94': '#60a5fa',
'95': '#c084fc',
'96': '#22d3ee',
'97': '#f9fafb',
}
/** ANSI → HTML(轻量实现,覆盖常用前景色/粗体/重置) */
export function ansiToHtml(input: string): string {
const text = decodeLogContent(input)
if (!text.includes('\u001b[') && !text.includes('\x1b[')) {
return escapeHtml(text)
}
let html = ''
let open = false
let style = ''
const re = /\x1b\[([0-9;]*)m/g
let last = 0
let m: RegExpExecArray | null
while ((m = re.exec(text))) {
const chunk = text.slice(last, m.index)
if (chunk) html += open ? `<span style="${style}">${escapeHtml(chunk)}</span>` : escapeHtml(chunk)
last = m.index + m[0].length
const codes = (m[1] || '0').split(';').filter(Boolean)
if (codes.length === 0 || codes.includes('0')) {
open = false
style = ''
continue
}
const parts: string[] = []
for (const c of codes) {
if (c === '1') parts.push('font-weight:700')
else if (c === '2') parts.push('opacity:.75')
else if (c === '4') parts.push('text-decoration:underline')
else if (ANSI_COLORS[c]) parts.push(`color:${ANSI_COLORS[c]}`)
}
if (parts.length) {
open = true
style = parts.join(';')
}
}
const rest = text.slice(last)
if (rest) html += open ? `<span style="${style}">${escapeHtml(rest)}</span>` : escapeHtml(rest)
return html
}