feat:移除了弹窗,服务器添加sls

This commit is contained in:
2026-09-08 22:39:45 +08:00
commit 6a295f9a7a
4082 changed files with 1322534 additions and 0 deletions

View File

@ -0,0 +1,648 @@
import type { LogLevel } from '../types'
/**
* 日志高亮引擎 — 移植自 LogShare-Web-UI (src/lib/logParser.worker.ts)
*
* 逐行正则 + Trie 预筛 + LRU 缓存,输出 HTMLspan + class
* 样式类与 LogShare 的 LogsAnalysis.css 对应(由 LogViewport 提供样式)。
* LogShare-Web-UI 为 MIT License, Copyright (c) 2024 LogShare.CN Team详见 packages/ui/COPYING.md。
* 零外部依赖,可在 Node 中独立运行(用于 benchmark
*/
export type LogHighlightMode = 'full' | 'lite' | 'line-only' | 'raw'
/**
* 单行长度保险:超过阈值跳过行内 token 高亮,只做级别检测;
* 超过硬上限完全不解析。防止超长行Base64、巨型堆栈拖垮渲染链路。
*/
const LONG_LINE_LIMIT = 4096
const LONG_LINE_HARD_LIMIT = 65536
const HTML_ESCAPE_MAP: Record<string, string> = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;',
}
const RE_HTML_ESCAPE = /[&<>"']/g
const RE_MARK_OPEN = /&lt;mark&gt;/gi
const RE_MARK_CLOSE = /&lt;\/mark&gt;/gi
/**
* Minecraft § 颜色码 → 样式类(与 LogShare CSS 的 .format-* 对应)
* k混淆在 LogShare CSS 中未定义样式,这里同样忽略
*/
const COLOR_STYLE_MAP: Record<string, string> = {
'0': 'format-black',
'1': 'format-darkblue',
'2': 'format-darkgreen',
'3': 'format-darkaqua',
'4': 'format-darkred',
'5': 'format-darkpurple',
'6': 'format-gold',
'7': 'format-gray',
'8': 'format-darkgray',
'9': 'format-blue',
a: 'format-green',
b: 'format-aqua',
c: 'format-red',
d: 'format-lightpurple',
e: 'format-yellow',
f: 'format-white',
l: 'format-bold',
m: 'format-strike',
n: 'format-underline',
o: 'format-italic',
r: 'format-reset',
}
const RE_COLOR_CODE = /§([0-9a-fk-or])/gi
const RE_WARN = /(?:\[|: |(?:\/\s))WARN(?:ING)?(?:]|:|\s)/i
const RE_ERROR_LEVEL = /(?:\[|: |(?:\/\s))(?:ERR(?:OR)?|FATAL|CRITICAL|EMERGENCY|SEVERE)(?:]|:|\s)/i
const RE_DEBUG = /(?:\[|: |(?:\/\s))DEBUG(?:]|:|\s)/i
const RE_TRACE = /(?:\[|: |(?:\/\s))TRACE(?:]|:|\s)/i
const RE_NOTICE = /(?:\[|: |(?:\/\s))NOTICE(?:]|:|\s)/i
const RE_EXCEPTION_NAME = /\b[A-Za-z0-9_$]*(?:Exception|Error|Throwable)\b/
const RE_STACK_AT = /^\s*at\s+/
const RE_CAUSED_BY = /^Caused by:\s*/
const RE_STACK_FRAME = /^(\s*)(at\s+)([^(]+)(\(([^)]+)\))?/
const RE_EXCEPTION_CLASS =
/([A-Za-z0-9_$]+(?:\.[A-Za-z0-9_$]+)*\.)?([A-Za-z0-9_$]*(?:Exception|Error|Throwable))\b/g
const RE_MORE_STACK = /^\s*\.\.\.\s+\d+\s+more\s*$/
const RE_SUPPRESSED = /^\s*Suppressed:\s+/
const RE_PYTHON_TRACEBACK = /^Traceback\s*\(most\s+recent\s+call\s+last\)\s*:\s*$/
const RE_PYTHON_FILE = /^\s*File\s+"[^"]*",\s*line\s+\d+/i
const RE_ERROR_PREFIX = /^(?:\s*\[?\s*)?(?:(?:ERROR?\s*[:;]|FATAL\s*[:;]|CRITICAL\s*[:;]))/i
const RE_FAIL_KEYWORDS =
/\b(?:Failed\s+to|Cannot\s+|Unable\s+to|Could\s+not|Illegal\s+|Invalid\s+|Unsupported\s+|Not\s+found\s*[:;]|Missing\s+)/i
const RE_THREAD_PREFIX = /(\[[^\]]+\/(?:INFO|WARN(?:ING)?|ERROR?|FATAL|DEBUG|TRACE|NOTICE)\])/g
const RE_BRACKET_TAG =
/(?:^|\s)(\[[A-Za-z0-9_\u00a1-\uffff][A-Za-z0-9_\u00a1-\uffff .\-/]{0,32}\])(?=\s|$)/g
const RE_FATAL_LEVEL = /\b(?:FATAL|CRITICAL|EMERGENCY)\b/i
const RE_MOD_LIST_HEADER = /(?:Loading\s+\d*\s*mods?\s*:?|--\s*Mod\s+List\s*--|Mod\s+List:?)\s*$/i
const RE_MOD_ENTRY = /(\s+[-*]\s+)([A-Za-z_][\w.-]*)((?:\s+|@))(\S[\S ]*)$/
const RE_MOD_TABLE_HEADER = /^\|\s*(?:Id|Name|Version|Status)\s*\|/i
const RE_MOD_TABLE_ROW =
/^(\|\s+)([\w.-]+)(\s+\|\s+)(\S[\S ]*?)(\s+\|\s+)([^|]+?)(\s+\|\s+)(\S[\S ]*?)(\s+\|)$/
const RE_FABRIC_LOADER_HEADER = /Loading\s+Minecraft\s+[\d.]+\s+with\s+Fabric\s+Loader\s+\S+/i
const RE_MOD_TREE_ENTRY = /^(\s*)([|\\]--\s+)([A-Za-z_][\w.-]*)((?:\s+|@))(\S[\S ]*)$/
const RE_NEOCRSH_MOD_ROW =
/^(.+?\.jar)\s*\|\s*([^|]+)\s*\|\s*([^|]+)\s*\|\s*([^|]+?)\s*\|\s*(Manifest:\s*\S+)/i
const RE_NEOMOD_LIST_HEADER = /^\s*Name\s+Version\s+\(Mod\s+Id\)\s*$/i
const RE_NEOMOD_LIST_ITEM = /^\s*(?!\[)(.+?)\s+\(([a-z_][\w.-]*)\)$/
const RE_ARGS_LINE = /(?:ModLauncher\s+running:\s*args|JVM\s*Args?:|^\s*--\w)/i
const RE_ARG_FLAG = /(--[A-Za-z_][\w.-]*)/g
class LRUCache<V> {
private capacity: number
private cache: Map<string, V>
constructor(capacity: number) {
this.capacity = capacity
this.cache = new Map()
}
get(key: string): V | undefined {
if (!this.cache.has(key)) return undefined
const value = this.cache.get(key)!
this.cache.delete(key)
this.cache.set(key, value)
return value
}
set(key: string, value: V): void {
if (this.cache.has(key)) {
this.cache.delete(key)
} else if (this.cache.size >= this.capacity) {
const firstKey = this.cache.keys().next().value
if (firstKey !== undefined) this.cache.delete(firstKey)
}
this.cache.set(key, value)
}
clear(): void {
this.cache.clear()
}
}
const formatCache = new LRUCache<string>(2000)
class TrieNode {
children: Map<string, TrieNode> = new Map()
id: number = -1
}
class Trie {
root: TrieNode = new TrieNode()
insert(word: string, id: number): void {
let node = this.root
for (const ch of word) {
let child = node.children.get(ch)
if (!child) {
child = new TrieNode()
node.children.set(ch, child)
}
node = child
}
node.id = id
}
}
function isWordChar(ch: string): boolean {
return (
(ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') || ch === '_'
)
}
function trieCollect(text: string, trie: Trie): Array<{ start: number; end: number; id: number }> {
const results: Array<{ start: number; end: number; id: number }> = []
const len = text.length
for (let i = 0; i < len; i++) {
let node = trie.root
let j = i
while (j < len) {
const ch = text[j]!
const child = node.children.get(ch)
if (!child) break
node = child
j++
if (node.id !== -1) {
const prev = i > 0 ? text[i - 1]! : ' '
const next = j < len ? text[j]! : ' '
if (!isWordChar(prev) && !isWordChar(next)) {
results.push({ start: i, end: j, id: node.id })
}
}
}
}
return results
}
/**
* 用 Trie 一次扫描完成关键词高亮替换matches 需已按 start 排序,忽略重叠)
*/
function trieHighlight(
text: string,
matches: Array<{ start: number; end: number; id: number }>,
classMap: Record<number, string>,
): string {
if (matches.length === 0) return text
const parts: string[] = []
let lastEnd = 0
for (const m of matches) {
if (m.start < lastEnd) continue
parts.push(text.slice(lastEnd, m.start))
parts.push('<span class="' + classMap[m.id] + '">')
parts.push(text.slice(m.start, m.end))
parts.push('</span>')
lastEnd = m.end
}
parts.push(text.slice(lastEnd))
return parts.join('')
}
const ENV_KEYWORD_MAP: Record<number, string> = {
0: 'level-mod-status-ok',
1: 'level-mod-status-ok',
2: 'level-mod-status-ok',
3: 'level-mod-status-error',
4: 'level-mod-status-error',
5: 'level-mod-status-error',
6: 'level-env-key',
7: 'level-env-key',
}
function buildEnvTrie(): Trie {
const t = new Trie()
t.insert('success', 0)
t.insert('Success', 1)
t.insert('SUCCESS', 2)
t.insert('failed', 3)
t.insert('Failed', 4)
t.insert('FAILED', 5)
t.insert('DLOPEN', 6)
t.insert('dlopen', 7)
return t
}
const envKeywordTrie = buildEnvTrie()
type EngineLevel = 'error' | 'warning' | 'debug' | 'info' | 'fatal' | 'trace'
function buildLevelTrie(): Trie {
const t = new Trie()
let id = 0
const add = (w: string) => t.insert(w, id++)
add('FATAL')
add('CRITICAL')
add('EMERGENCY')
add('SEVERE')
add('ERROR')
add('ERR')
add('WARN')
add('WARNING')
add('DEBUG')
add('TRACE')
add('NOTICE')
add('Exception')
add('Throwable')
add('Caused')
add('Suppressed')
add('Traceback')
add('Failed')
add('Cannot')
add('Unable')
add('Could')
add('Illegal')
add('Invalid')
add('Unsupported')
add('Missing')
add('Stacktrace:')
add('Details:')
return t
}
const levelTrie = buildLevelTrie()
function getLevel(line: string): EngineLevel {
if (RE_PYTHON_TRACEBACK.test(line)) return 'error'
if (RE_PYTHON_FILE.test(line)) return 'error'
if (RE_STACK_AT.test(line)) return 'error'
if (RE_CAUSED_BY.test(line)) return 'error'
if (RE_MORE_STACK.test(line)) return 'error'
if (RE_SUPPRESSED.test(line)) return 'error'
if (RE_EXCEPTION_NAME.test(line)) return 'error'
if (RE_ERROR_PREFIX.test(line)) return 'error'
if (/^\s*(?:Stacktrace|Details):/.test(line)) return 'error'
if (/^-- Affected level --$/.test(line)) return 'error'
const levelHits = trieCollect(line, levelTrie)
if (levelHits.length === 0) return 'info'
if (RE_FATAL_LEVEL.test(line)) return 'fatal'
if (RE_ERROR_LEVEL.test(line)) return 'error'
if (RE_WARN.test(line)) return 'warning'
if (RE_TRACE.test(line)) return 'trace'
if (RE_DEBUG.test(line)) return 'debug'
if (RE_NOTICE.test(line)) return 'info'
if (
/^\s*(?:Failed\s+to|Cannot\s+|Unable\s+to|Could\s+not|Illegal\s+|Invalid\s+|Unsupported\s+|Not\s+found\s*[:;]|Missing\s+)/i.test(
line,
)
) {
return 'error'
}
if (RE_FAIL_KEYWORDS.test(line)) return 'warning'
return 'info'
}
function mapLevel(level: EngineLevel): LogLevel {
if (level === 'warning') return 'warn'
if (level === 'fatal') return 'error'
return level
}
/**
* 合并常用高亮模式为单次扫描:时间戳 | ISO时间戳 | 文件路径 | IPv4 | 行内错误关键词
*/
const RE_COMMON_HIGHLIGHTS =
/(\[\d{1,2}:\d{2}:\d{2}(?:[.,]\d{3,6})?\])|(\b\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[+-]\d{2}:?\d{2}|Z)?\b)|(\/(?:[A-Za-z0-9_.@-]+(?:\/[A-Za-z0-9_.@-]+)+))|(\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(?::\d{2,5})?\b)|(\b(?:Error|Exception|Warning|Fatal|Critical)\b)/g
function applyCommonHighlights(out: string): string {
return out.replace(RE_COMMON_HIGHLIGHTS, (match, ts, isots, fp, ip, errword) => {
if (ts) return '<span class="level-timestamp">' + ts + '</span>'
if (isots) return '<span class="level-timestamp">' + isots + '</span>'
if (fp) return '<span class="level-filepath">' + fp + '</span>'
if (ip) return '<span class="level-dimmed">' + ip + '</span>'
if (errword) return '<span class="level-error-word">' + errword + '</span>'
return match
})
}
function applyStackHighlights(out: string, text: string): string {
if (RE_STACK_AT.test(text)) {
out = out.replace(RE_STACK_FRAME, (_, indent, atKw, className, _paren, location) => {
const locHtml = location ? '<span class="level-stack-location">(' + location + ')</span>' : ''
return (
indent +
'<span class="level-stack-frame">' +
atKw +
'</span><span class="level-stack-class">' +
className +
'</span>' +
locHtml
)
})
}
out = out.replace(
/^(Caused by:\s*)(.+)$/m,
'<span class="level-stack-caused-by">$1</span><span class="level-stack-exception">$2</span>',
)
out = out.replace(
/^(Suppressed:\s*)(.+)$/m,
'<span class="level-stack-caused-by">$1</span><span class="level-stack-exception">$2</span>',
)
if (!RE_STACK_AT.test(text) && RE_EXCEPTION_NAME.test(text)) {
out = out.replace(RE_EXCEPTION_CLASS, '<span class="level-stack-exception">$1$2</span>')
}
if (RE_PYTHON_FILE.test(text)) {
out = out.replace(
/^(\s*File\s+)("[^"]*")(\s*,\s*line\s+)(\d+)/i,
'<span class="level-stack-frame">$1</span><span class="level-stack-location">$2$3</span><span class="level-stack-class">$4</span>',
)
}
if (RE_PYTHON_TRACEBACK.test(text)) {
out = out.replace(
/^(Traceback\s*\()(most recent call last)(\)\s*:\s*)$/,
'<span class="level-stack-frame">$1</span><span class="level-stack-location">$2</span><span class="level-stack-frame">$3</span>',
)
}
return out
}
function applyModHighlights(out: string, text: string): string {
if (RE_MOD_LIST_HEADER.test(text)) {
if (/--\s*Mod\s+List\s*--/i.test(text)) {
out = out.replace(
/^(\s*)(--\s*Mod\s+List\s*--)(\s*)$/,
'$1<span class="level-mod-header">$2</span>$3',
)
} else if (/Mod\s+List:?/i.test(text)) {
out = out.replace(
/^(.*?)(Mod\s+List:?)(.*)$/i,
'$1<span class="level-mod-header">$2</span>$3',
)
} else {
out = out.replace(
/^(.*?)(Loading\s+\d*\s*mods?\s*:?)(.*)$/i,
'$1<span class="level-mod-header">$2</span>$3',
)
}
}
if (RE_MOD_ENTRY.test(text)) {
out = out.replace(
RE_MOD_ENTRY,
'$1<span class="level-mod-id">$2</span>$3<span class="level-mod-version">$4</span>',
)
}
if (RE_MOD_TABLE_HEADER.test(text)) {
out = out.replace(/^(\|)([\s\w|]+)(\|)$/, (_, open, body, close) => {
const cells = body.split('|').map((c: string) => c.trim())
const wrapped = cells
.map((c: string) => '<span class="level-mod-header">' + c + '</span>')
.join(' | ')
return open + ' ' + wrapped + ' ' + close
})
}
if (RE_MOD_TABLE_ROW.test(text)) {
out = out.replace(
RE_MOD_TABLE_ROW,
(_match, open, id, sep1, version, sep2, name, sep3, status, close) => {
const statusClean = status.trim().toLowerCase()
let statusClass = 'level-mod-status'
if (statusClean === 'ok' || statusClean === 'done') {
statusClass += ' level-mod-status-ok'
} else if (
statusClean === 'error' ||
statusClean === 'missing' ||
statusClean === 'outdated'
) {
statusClass += ' level-mod-status-error'
} else if (statusClean === 'warning') {
statusClass += ' level-mod-status-warn'
}
return (
open +
'<span class="level-mod-id">' +
id +
'</span>' +
sep1 +
'<span class="level-mod-version">' +
version +
'</span>' +
sep2 +
'<span class="level-mod-name">' +
name.trim() +
'</span>' +
sep3 +
'<span class="' +
statusClass +
'">' +
status.trim() +
'</span>' +
close
)
},
)
}
if (RE_FABRIC_LOADER_HEADER.test(text)) {
out = out.replace(RE_FABRIC_LOADER_HEADER, '<span class="level-mod-header">$&</span>')
}
if (RE_MOD_TREE_ENTRY.test(text)) {
out = out.replace(
RE_MOD_TREE_ENTRY,
'$1<span class="level-mod-tree">$2</span><span class="level-mod-id">$3</span>$4<span class="level-mod-version">$5</span>',
)
}
if (RE_NEOCRSH_MOD_ROW.test(text)) {
out = out.replace(
RE_NEOCRSH_MOD_ROW,
'<span class="level-mod-dim">$1</span> | <span class="level-mod-name">$2</span> | <span class="level-mod-id">$3</span> | <span class="level-mod-version">$4</span> | <span class="level-mod-status">$5</span>',
)
}
if (RE_NEOMOD_LIST_HEADER.test(text)) {
out = out.replace(RE_NEOMOD_LIST_HEADER, '<span class="level-mod-header">$&</span>')
}
if (RE_NEOMOD_LIST_ITEM.test(text)) {
out = out.replace(
RE_NEOMOD_LIST_ITEM,
'<span class="level-mod-name">$1</span> (<span class="level-mod-id">$2</span>)',
)
}
return out
}
/**
* 单行格式化text → HTMLspan + class
*/
function formatLine(text: string, mode: LogHighlightMode): string {
const cached = formatCache.get(mode + text)
if (cached !== undefined) return cached
let out = escapeHtml(text)
// 恢复搜索高亮(搜索层注入的 <mark> 已转义)
out = out.replace(RE_MARK_OPEN, '<mark>').replace(RE_MARK_CLOSE, '</mark>')
// Minecraft § 颜色码
out = out.replace(RE_COLOR_CODE, (_match, code) => {
const cls = COLOR_STYLE_MAP[code.toLowerCase() as keyof typeof COLOR_STYLE_MAP]
return cls ? '<span class="' + cls + '">' : _match
})
if (mode === 'full') {
out = applyStackHighlights(out, text)
out = applyModHighlights(out, text)
}
// [Thread/LEVEL] 前缀
out = out.replace(RE_THREAD_PREFIX, (match) => {
const sepIdx = match.lastIndexOf('/')
if (sepIdx === -1) return match
const thread = match.slice(0, sepIdx) + '/'
const level = match.slice(sepIdx + 1, -1)
const levelLower = level.toLowerCase()
const levelClass =
levelLower === 'error' || levelLower === 'fatal'
? 'level-error-word'
: levelLower === 'warn' || levelLower === 'warning'
? 'level-warning-tag'
: 'level-info-prefix'
return (
'<span class="level-thread">' +
escapeHtml(thread) +
'</span><span class="' +
levelClass +
'">' +
escapeHtml(level) +
'</span>]'
)
})
// [插件名] 标记保留前导空格LogShare 原版会吞掉)
out = out.replace(RE_BRACKET_TAG, (match) => {
const leading = /^\s*/.exec(match)?.[0] ?? ''
const tag = match.trim()
if (tag.length > 40) return match
return leading + '<span class="level-plugin">' + escapeHtml(tag) + '</span>'
})
// 一次扫描完成:时间戳 / 文件路径 / IPv4 / 错误关键词
out = applyCommonHighlights(out)
// 分隔线 / 环境变量 / DLOPEN / 启动参数
out = out.replace(
/^(={5,})\s*([^=\n]*)\s*(={5,})$/m,
'<span class="level-section-header">$&</span>',
)
out = out.replace(
/^(\s*)((?:Added\s+)?[Ee]nv\s*:\s*)/m,
'$1<span class="level-env-key">$2</span>',
)
const envMatches = trieCollect(out, envKeywordTrie)
if (envMatches.length > 0) {
envMatches.sort((a, b) => a.start - b.start)
out = trieHighlight(out, envMatches, ENV_KEYWORD_MAP)
}
out = out.replace(
/(Environment:\s*)(Environment\[[^\]]*\])/g,
'$1<span class="level-env-key">$2</span>',
)
if (RE_ARGS_LINE.test(text)) {
out = out.replace(RE_ARG_FLAG, '<span class="level-arg-flag">$1</span>')
out = out.replace(/(^|\s)(-D[A-Za-z_][\w.]*)/gm, '$1<span class="level-arg-flag">$2</span>')
}
out = out.replace(/\b(Stacktrace:|Details:)/g, '<span class="level-section-marker">$1</span>')
out = out.replace(/^-- Affected level --$/m, '<span class="level-section-header">$&</span>')
formatCache.set(mode + text, out)
return out
}
function escapeHtml(text: string): string {
return text.replace(RE_HTML_ESCAPE, (ch) => HTML_ESCAPE_MAP[ch] || ch)
}
/**
* 行内级别整行着色line-only 模式 / 降级兜底)
*/
export function colorizeByLevel(level: LogLevel | null, text: string): string {
const levelClass =
level === 'error'
? 'level-error'
: level === 'warn'
? 'level-warning'
: level === 'debug' || level === 'trace'
? 'level-debug'
: 'level-info'
return '<span class="level ' + levelClass + '">' + escapeHtml(text) + '</span>'
}
/**
* 单行高亮入口text → HTML
*
* 内置行长度保险:超长行跳过行内 token只做级别检测超硬上限完全不解析。
*/
export function highlightLine(text: string, mode: LogHighlightMode = 'full'): string {
if (mode === 'raw') return escapeHtml(text)
if (text.length > LONG_LINE_HARD_LIMIT) return escapeHtml(text)
if (mode === 'line-only' || text.length > LONG_LINE_LIMIT) {
return colorizeByLevel(mapLevel(getLevel(text)), text)
}
return formatLine(text, mode)
}
/**
* 级别检测LogShare 语义,供 line-only 模式与统计使用)
*/
export function detectStrictLevel(text: string): LogLevel | null {
if (text.length > LONG_LINE_HARD_LIMIT) return null
return mapLevel(getLevel(text))
}
/**
* 清空高亮缓存(一般不需要,测试用)
*/
export function clearHighlightCache(): void {
formatCache.clear()
}