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
This commit is contained in:
2026-07-26 08:43:52 +08:00
commit e6956aa001
397 changed files with 73621 additions and 0 deletions
+103
View File
@@ -0,0 +1,103 @@
/**
* 日志内容处理: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
}