2026-06-08 16:11:20 +08:00
|
|
|
|
import http from './http'
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 「日志管理」前端胶水:对应 MiGu.Server `LogsController`(`/api/logs/*`)。
|
|
|
|
|
|
*
|
2026-08-26 17:48:25 +08:00
|
|
|
|
* 数据是 Simple3 内核 `Diagnosis.Post / Diagnosis.Log` 写到工作目录 `log/{日期}/xxx.log` 的落盘日志
|
2026-06-08 16:11:20 +08:00
|
|
|
|
* (俗称 DLog)。后端直接读文件并解析为结构化条目,提供:概览 / 文件列表 / 条目分页 /
|
|
|
|
|
|
* 「按标签合订」/ 原文 / 下载。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 与 config.ts 一致直接返回数据对象(非 reflection 的 success/data 信封);错误由 http.ts
|
|
|
|
|
|
* 拦截器翻译成中文。VITE_USE_MOCK=true 时返回内置样例,便于无后端调试界面。
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
const MOCK = import.meta.env.VITE_USE_MOCK === 'true'
|
|
|
|
|
|
const API_BASE = (import.meta.env.VITE_API_BASE as string | undefined) ?? '/api'
|
|
|
|
|
|
|
2026-08-26 17:48:25 +08:00
|
|
|
|
/** Simple3 projection 投影 API 统一信封(同 reflection.ts)。 */
|
2026-06-08 16:11:20 +08:00
|
|
|
|
interface SlEnvelope<T> { success: boolean; code: number; data: T | null; message: string }
|
|
|
|
|
|
|
2026-08-26 17:48:25 +08:00
|
|
|
|
/** 经 YARP 反代到 Simple3 EmbedIO 的诊断投影端点(/api/sl/projection/diagnosis)。 */
|
2026-06-08 16:11:20 +08:00
|
|
|
|
const SL_DIAG = '/sl/projection/diagnosis'
|
|
|
|
|
|
|
|
|
|
|
|
export interface LogEntry {
|
|
|
|
|
|
lineNo: number
|
|
|
|
|
|
/** ISO 时间;无法解析时间戳的续行/异常行为 null。 */
|
|
|
|
|
|
time: string | null
|
|
|
|
|
|
prefix: string
|
|
|
|
|
|
/** 空串表示无标签(滚动记录)。 */
|
|
|
|
|
|
tag: string
|
|
|
|
|
|
content: string
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export interface LogFile {
|
|
|
|
|
|
/** 相对日志根的路径,如 `2026-06-01/20260601-12Q(30).log`,作为其它接口的 file 参数。 */
|
|
|
|
|
|
rel: string
|
|
|
|
|
|
name: string
|
|
|
|
|
|
/** 一级目录(通常是日期),无子目录时为「(根目录)」。 */
|
|
|
|
|
|
day: string
|
|
|
|
|
|
dir: string
|
|
|
|
|
|
bytes: number
|
|
|
|
|
|
mtime: string
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export interface LogDayStat {
|
|
|
|
|
|
day: string
|
|
|
|
|
|
files: number
|
|
|
|
|
|
bytes: number
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-26 17:48:25 +08:00
|
|
|
|
export interface LogDiskStatus {
|
|
|
|
|
|
known: boolean
|
|
|
|
|
|
drive: string
|
|
|
|
|
|
freeGB: number
|
|
|
|
|
|
alertEnabled: boolean
|
|
|
|
|
|
alertGB: number
|
|
|
|
|
|
belowThreshold: boolean
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export interface LogCleanupConfig {
|
|
|
|
|
|
enabled: boolean
|
|
|
|
|
|
retentionDays: number
|
|
|
|
|
|
checkIntervalHours: number
|
|
|
|
|
|
runOnStartup: boolean
|
|
|
|
|
|
diskAlertEnabled: boolean
|
|
|
|
|
|
diskFreeAlertGB: number
|
|
|
|
|
|
diskCheckIntervalMinutes: number
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export interface LogCleanupResult {
|
|
|
|
|
|
deletedFiles: number
|
|
|
|
|
|
freedBytes: number
|
|
|
|
|
|
freedMB: number
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-08 16:11:20 +08:00
|
|
|
|
export interface LogOverview {
|
|
|
|
|
|
exists: boolean
|
|
|
|
|
|
root: string | null
|
|
|
|
|
|
workingDirectory: string | null
|
|
|
|
|
|
totalFiles?: number
|
|
|
|
|
|
totalBytes?: number
|
|
|
|
|
|
latestFileTime?: string | null
|
|
|
|
|
|
days?: LogDayStat[]
|
|
|
|
|
|
message?: string
|
2026-08-26 17:48:25 +08:00
|
|
|
|
disk?: LogDiskStatus
|
2026-06-08 16:11:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export interface LogFilesResult {
|
|
|
|
|
|
root: string
|
|
|
|
|
|
total: number
|
|
|
|
|
|
returned: number
|
|
|
|
|
|
files: LogFile[]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export interface LogEntriesResult {
|
|
|
|
|
|
file: string
|
|
|
|
|
|
bytes: number
|
|
|
|
|
|
scannedLines: number
|
|
|
|
|
|
truncated: boolean
|
|
|
|
|
|
total: number
|
|
|
|
|
|
offset: number
|
|
|
|
|
|
limit: number
|
|
|
|
|
|
order: string
|
|
|
|
|
|
entries: LogEntry[]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 合订本「一册」:同一标签聚合后的视图。 */
|
|
|
|
|
|
export interface LogBook {
|
|
|
|
|
|
tag: string
|
|
|
|
|
|
count: number
|
|
|
|
|
|
firstTime?: string | null
|
|
|
|
|
|
lastTime?: string | null
|
|
|
|
|
|
latest?: string
|
|
|
|
|
|
latestTime?: string | null
|
|
|
|
|
|
entries: LogEntry[]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export interface LogDigest {
|
|
|
|
|
|
source: 'file' | 'day' | string
|
|
|
|
|
|
target: string
|
|
|
|
|
|
files: number
|
|
|
|
|
|
truncated: boolean
|
|
|
|
|
|
tagCount: number
|
|
|
|
|
|
untaggedCount: number
|
|
|
|
|
|
books: LogBook[]
|
|
|
|
|
|
untagged: LogBook
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export interface EntriesQuery {
|
|
|
|
|
|
file: string
|
|
|
|
|
|
keyword?: string
|
|
|
|
|
|
tag?: string
|
|
|
|
|
|
onlyTagged?: boolean
|
|
|
|
|
|
order?: 'asc' | 'desc'
|
|
|
|
|
|
limit?: number
|
|
|
|
|
|
offset?: number
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export interface DigestQuery {
|
|
|
|
|
|
file?: string
|
|
|
|
|
|
day?: string
|
|
|
|
|
|
keyword?: string
|
|
|
|
|
|
maxPerTag?: number
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-26 17:48:25 +08:00
|
|
|
|
/** 实时诊断单条:对应 Simple3 内核 Diagnosis 的内存态 Post/Toast。 */
|
2026-06-08 16:11:20 +08:00
|
|
|
|
export interface LiveDiagItem {
|
|
|
|
|
|
index: number
|
|
|
|
|
|
time: string
|
|
|
|
|
|
/** 空串=无标签(滚动记录);非空=按标签合订(同标签仅留最新一条)。 */
|
|
|
|
|
|
tag: string
|
|
|
|
|
|
tagged: boolean
|
|
|
|
|
|
content: string
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export interface LiveDiagnosis {
|
|
|
|
|
|
serverTime: string
|
|
|
|
|
|
total: number
|
|
|
|
|
|
taggedCount: number
|
|
|
|
|
|
untaggedCount: number
|
|
|
|
|
|
items: LiveDiagItem[]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 目录浏览:子文件夹。 */
|
|
|
|
|
|
export interface BrowseDir {
|
|
|
|
|
|
name: string
|
|
|
|
|
|
rel: string
|
|
|
|
|
|
mtime: string
|
|
|
|
|
|
dirCount: number
|
|
|
|
|
|
fileCount: number
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 目录浏览:文件。 */
|
|
|
|
|
|
export interface BrowseFile {
|
|
|
|
|
|
name: string
|
|
|
|
|
|
rel: string
|
|
|
|
|
|
bytes: number
|
|
|
|
|
|
mtime: string
|
|
|
|
|
|
isLog: boolean
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export interface BrowseResult {
|
|
|
|
|
|
root: string
|
|
|
|
|
|
exists: boolean
|
|
|
|
|
|
/** 当前相对路径(""=日志根)。 */
|
|
|
|
|
|
path: string
|
|
|
|
|
|
/** 上一级相对路径;位于根时为 null。 */
|
|
|
|
|
|
parent: string | null
|
|
|
|
|
|
dirCount: number
|
|
|
|
|
|
fileCount: number
|
|
|
|
|
|
dirs: BrowseDir[]
|
|
|
|
|
|
files: BrowseFile[]
|
|
|
|
|
|
message?: string
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ───────────────────────────────────────────────── 日志分析器 ──
|
|
|
|
|
|
|
|
|
|
|
|
/** 标签分布一项。 */
|
|
|
|
|
|
export interface AnalyzeTag { tag: string; count: number; percent: number }
|
|
|
|
|
|
|
|
|
|
|
|
/** 日志量直方图:稀疏桶(仅含有数据的时刻)+ Top 标签拆分(与 buckets 等长对齐)。 */
|
|
|
|
|
|
export interface AnalyzeVolume {
|
|
|
|
|
|
granularity: string
|
|
|
|
|
|
buckets: string[]
|
|
|
|
|
|
total: number[]
|
|
|
|
|
|
topTags: Array<{ tag: string; counts: number[] }>
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 识别出的数值字段统计。 */
|
|
|
|
|
|
export interface AnalyzeField {
|
|
|
|
|
|
name: string
|
|
|
|
|
|
samples: number
|
|
|
|
|
|
min: number
|
|
|
|
|
|
max: number
|
|
|
|
|
|
avg: number
|
|
|
|
|
|
last: number
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 选定字段的时序点序列。 */
|
|
|
|
|
|
export interface AnalyzeSeries {
|
|
|
|
|
|
field: string
|
|
|
|
|
|
tag: string
|
|
|
|
|
|
count: number
|
|
|
|
|
|
points: Array<{ t: string; v: number }>
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export interface AnalyzeResult {
|
|
|
|
|
|
source: string
|
|
|
|
|
|
target: string
|
|
|
|
|
|
files: number
|
|
|
|
|
|
truncated: boolean
|
|
|
|
|
|
total: number
|
|
|
|
|
|
timeRange: { start: string | null; end: string | null }
|
|
|
|
|
|
granularity: string
|
|
|
|
|
|
tags: AnalyzeTag[]
|
|
|
|
|
|
volume: AnalyzeVolume
|
|
|
|
|
|
fields: AnalyzeField[]
|
|
|
|
|
|
series: AnalyzeSeries | null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export interface AnalyzeQuery {
|
|
|
|
|
|
file?: string
|
|
|
|
|
|
day?: string
|
|
|
|
|
|
granularity?: 'second' | 'minute' | 'hour'
|
|
|
|
|
|
tag?: string
|
|
|
|
|
|
keyword?: string
|
|
|
|
|
|
field?: string
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ───────────────────────────────────────────────────────────── mock ──
|
|
|
|
|
|
|
|
|
|
|
|
function mockOverview(): LogOverview {
|
|
|
|
|
|
return {
|
|
|
|
|
|
exists: true,
|
2026-08-26 17:48:25 +08:00
|
|
|
|
root: 'E:\\...\\Simple3\\bin\\Debug\\log',
|
|
|
|
|
|
workingDirectory: 'E:\\...\\Simple3\\bin\\Debug',
|
2026-06-08 16:11:20 +08:00
|
|
|
|
totalFiles: 3,
|
|
|
|
|
|
totalBytes: 5_233_649,
|
|
|
|
|
|
latestFileTime: new Date().toISOString(),
|
2026-08-26 17:48:25 +08:00
|
|
|
|
days: [{ day: '2026-06-01', files: 3, bytes: 5_233_649 }],
|
|
|
|
|
|
disk: { known: true, drive: 'E:\\', freeGB: 118.7, alertEnabled: true, alertGB: 5, belowThreshold: false }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function mockCleanupConfig(): LogCleanupConfig {
|
|
|
|
|
|
return {
|
|
|
|
|
|
enabled: true,
|
|
|
|
|
|
retentionDays: 30,
|
|
|
|
|
|
checkIntervalHours: 24,
|
|
|
|
|
|
runOnStartup: true,
|
|
|
|
|
|
diskAlertEnabled: true,
|
|
|
|
|
|
diskFreeAlertGB: 5,
|
|
|
|
|
|
diskCheckIntervalMinutes: 30
|
2026-06-08 16:11:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function mockFiles(): LogFilesResult {
|
|
|
|
|
|
const files: LogFile[] = [
|
|
|
|
|
|
{ rel: '2026-06-01/20260601-12Q(30).log', name: '20260601-12Q(30).log', day: '2026-06-01', dir: '2026-06-01', bytes: 5_223_512, mtime: '2026-06-01T12:44:59' },
|
|
|
|
|
|
{ rel: '2026-06-01/20260601-12Q(15).log', name: '20260601-12Q(15).log', day: '2026-06-01', dir: '2026-06-01', bytes: 1188, mtime: '2026-06-01T12:21:44' },
|
|
|
|
|
|
{ rel: '2026-06-01/20260601-08Q(45).log', name: '20260601-08Q(45).log', day: '2026-06-01', dir: '2026-06-01', bytes: 949, mtime: '2026-06-01T08:47:20' }
|
|
|
|
|
|
]
|
|
|
|
|
|
return { root: 'mock', total: files.length, returned: files.length, files }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function mockEntries(q: EntriesQuery): LogEntriesResult {
|
|
|
|
|
|
const base: LogEntry[] = [
|
|
|
|
|
|
{ lineNo: 1, time: '2026-06-01T12:21:40.120', prefix: '', tag: 'Persistence', content: 'worker started, schema=1.2' },
|
|
|
|
|
|
{ lineNo: 2, time: '2026-06-01T12:21:41.330', prefix: '', tag: '', content: 'loaded 12 sites, 18 tracks' },
|
|
|
|
|
|
{ lineNo: 3, time: '2026-06-01T12:21:42.880', prefix: '', tag: 'Dispatch', content: 'dispatch loop 50Hz online' },
|
|
|
|
|
|
{ lineNo: 4, time: '2026-06-01T12:21:44.010', prefix: '', tag: 'Persistence', content: 'flush 4 entities ok' },
|
|
|
|
|
|
{ lineNo: 5, time: '2026-06-01T12:21:45.220', prefix: '', tag: 'UI-Error', content: 'panel repaint skipped (terminal closing)' }
|
|
|
|
|
|
]
|
|
|
|
|
|
let list = base
|
|
|
|
|
|
if (q.onlyTagged) list = list.filter((e) => e.tag)
|
|
|
|
|
|
if (q.tag) list = list.filter((e) => e.tag.includes(q.tag!))
|
|
|
|
|
|
if (q.keyword) list = list.filter((e) => e.content.includes(q.keyword!) || e.tag.includes(q.keyword!))
|
|
|
|
|
|
if (q.order !== 'asc') list = [...list].reverse()
|
|
|
|
|
|
return { file: q.file, bytes: 5_223_512, scannedLines: base.length, truncated: false, total: list.length, offset: 0, limit: 300, order: q.order ?? 'desc', entries: list }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function mockDigest(): LogDigest {
|
|
|
|
|
|
const mk = (tag: string, n: number, latest: string): LogBook => ({
|
|
|
|
|
|
tag, count: n, firstTime: '2026-06-01T12:00:00', lastTime: '2026-06-01T12:44:00',
|
|
|
|
|
|
latest, latestTime: '2026-06-01T12:44:00',
|
|
|
|
|
|
entries: Array.from({ length: Math.min(n, 3) }, (_, i) => ({
|
|
|
|
|
|
lineNo: i + 1, time: '2026-06-01T12:4' + i + ':00.000', prefix: '', tag, content: `${latest} #${i + 1}`
|
|
|
|
|
|
}))
|
|
|
|
|
|
})
|
|
|
|
|
|
return {
|
|
|
|
|
|
source: 'file', target: 'mock', files: 1, truncated: false, tagCount: 3, untaggedCount: 42,
|
|
|
|
|
|
books: [mk('Persistence', 128, 'flush ok'), mk('Dispatch', 64, 'loop tick'), mk('UI-Error', 3, 'repaint skipped')],
|
|
|
|
|
|
untagged: { tag: '', count: 42, latest: 'misc rolling line', entries: [
|
|
|
|
|
|
{ lineNo: 2, time: '2026-06-01T12:21:41.330', prefix: '', tag: '', content: 'loaded 12 sites, 18 tracks' }
|
|
|
|
|
|
] }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────── client ──
|
|
|
|
|
|
|
|
|
|
|
|
function mockLiveDiagnosis(): LiveDiagnosis {
|
|
|
|
|
|
const now = new Date()
|
|
|
|
|
|
const iso = (sec: number) => new Date(now.getTime() - sec * 1000).toISOString().slice(0, 23)
|
|
|
|
|
|
const items: LiveDiagItem[] = [
|
|
|
|
|
|
{ index: 0, time: iso(2), tag: 'Persistence', tagged: true, content: 'flush 4 entities ok' },
|
|
|
|
|
|
{ index: 1, time: iso(4), tag: 'Dispatch', tagged: true, content: 'dispatch loop 50Hz online' },
|
|
|
|
|
|
{ index: 2, time: iso(6), tag: 'InternalAuth', tagged: true, content: '仅放行本机回环(无 internal token 配置)' },
|
2026-08-26 17:48:25 +08:00
|
|
|
|
{ index: 3, time: iso(1), tag: '', tagged: false, content: '[Simple3] Projection API listening on http://127.0.0.1:8222/projection/' },
|
2026-06-08 16:11:20 +08:00
|
|
|
|
{ index: 4, time: iso(8), tag: '', tagged: false, content: 'loaded 12 sites, 18 tracks' }
|
|
|
|
|
|
]
|
|
|
|
|
|
const taggedCount = items.filter((i) => i.tagged).length
|
|
|
|
|
|
return { serverTime: now.toISOString().slice(0, 23), total: items.length, taggedCount, untaggedCount: items.length - taggedCount, items }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function mockBrowse(path?: string): BrowseResult {
|
|
|
|
|
|
if (!path) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
root: 'mock-log', exists: true, path: '', parent: null, dirCount: 1, fileCount: 0,
|
|
|
|
|
|
dirs: [{ name: '2026-06-01', rel: '2026-06-01', mtime: '2026-06-01T12:44:59', dirCount: 0, fileCount: 3 }],
|
|
|
|
|
|
files: []
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return {
|
|
|
|
|
|
root: 'mock-log', exists: true, path, parent: '', dirCount: 0, fileCount: 3,
|
|
|
|
|
|
dirs: [],
|
|
|
|
|
|
files: [
|
|
|
|
|
|
{ name: '20260601-12Q(30).log', rel: `${path}/20260601-12Q(30).log`, bytes: 5_223_512, mtime: '2026-06-01T12:44:59', isLog: true },
|
|
|
|
|
|
{ name: '20260601-12Q(15).log', rel: `${path}/20260601-12Q(15).log`, bytes: 1188, mtime: '2026-06-01T12:21:44', isLog: true },
|
|
|
|
|
|
{ name: '20260601-08Q(45).log', rel: `${path}/20260601-08Q(45).log`, bytes: 949, mtime: '2026-06-01T08:47:20', isLog: true }
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function mockAnalyze(q: AnalyzeQuery): AnalyzeResult {
|
|
|
|
|
|
const now = Date.now()
|
|
|
|
|
|
const gran = q.granularity ?? 'minute'
|
|
|
|
|
|
const step = gran === 'second' ? 1000 : gran === 'hour' ? 3_600_000 : 60_000
|
|
|
|
|
|
const n = 30
|
|
|
|
|
|
const buckets = Array.from({ length: n }, (_, i) => new Date(now - (n - 1 - i) * step).toISOString())
|
|
|
|
|
|
const wave = (amp: number, base: number, ph: number) =>
|
|
|
|
|
|
buckets.map((_, i) => Math.max(0, Math.round(base + amp * Math.sin(i / 3 + ph))))
|
|
|
|
|
|
const fields: AnalyzeField[] = [
|
|
|
|
|
|
{ name: 'cost', samples: 420, min: 2, max: 88, avg: 18.4, last: 21 },
|
|
|
|
|
|
{ name: 'queue', samples: 380, min: 0, max: 32, avg: 6.1, last: 4 },
|
|
|
|
|
|
{ name: 'speed', samples: 300, min: 0, max: 1.5, avg: 0.7, last: 0.9 }
|
|
|
|
|
|
]
|
|
|
|
|
|
const series: AnalyzeSeries | null = q.field
|
|
|
|
|
|
? {
|
|
|
|
|
|
field: q.field, tag: q.tag ?? '', count: n,
|
|
|
|
|
|
points: buckets.map((t, i) => ({ t, v: Math.round((18 + 10 * Math.sin(i / 2)) * 10) / 10 }))
|
|
|
|
|
|
}
|
|
|
|
|
|
: null
|
|
|
|
|
|
return {
|
|
|
|
|
|
source: q.file ? 'file' : 'day', target: q.file ?? q.day ?? 'mock', files: 1, truncated: false,
|
|
|
|
|
|
total: 1280,
|
|
|
|
|
|
timeRange: { start: buckets[0], end: buckets[n - 1] },
|
|
|
|
|
|
granularity: gran,
|
|
|
|
|
|
tags: [
|
|
|
|
|
|
{ tag: 'Dispatch', count: 520, percent: 40.6 },
|
|
|
|
|
|
{ tag: 'Persistence', count: 360, percent: 28.1 },
|
|
|
|
|
|
{ tag: '交管#3', count: 210, percent: 16.4 },
|
|
|
|
|
|
{ tag: 'UI-Error', count: 90, percent: 7.0 },
|
|
|
|
|
|
{ tag: '', count: 100, percent: 7.8 }
|
|
|
|
|
|
],
|
|
|
|
|
|
volume: {
|
|
|
|
|
|
granularity: gran, buckets,
|
|
|
|
|
|
total: wave(20, 40, 0),
|
|
|
|
|
|
topTags: [
|
|
|
|
|
|
{ tag: 'Dispatch', counts: wave(10, 18, 0) },
|
|
|
|
|
|
{ tag: 'Persistence', counts: wave(8, 12, 1) },
|
|
|
|
|
|
{ tag: '交管#3', counts: wave(6, 7, 2) }
|
|
|
|
|
|
]
|
|
|
|
|
|
},
|
|
|
|
|
|
fields,
|
|
|
|
|
|
series
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export const logsApi = {
|
|
|
|
|
|
overview: (): Promise<LogOverview> => MOCK
|
|
|
|
|
|
? Promise.resolve(mockOverview())
|
|
|
|
|
|
: http.get<LogOverview>('/logs/overview').then((r) => r.data),
|
|
|
|
|
|
|
|
|
|
|
|
files: (params?: { day?: string; keyword?: string; limit?: number }): Promise<LogFilesResult> => MOCK
|
|
|
|
|
|
? Promise.resolve(mockFiles())
|
|
|
|
|
|
: http.get<LogFilesResult>('/logs/files', { params }).then((r) => r.data),
|
|
|
|
|
|
|
|
|
|
|
|
entries: (q: EntriesQuery): Promise<LogEntriesResult> => MOCK
|
|
|
|
|
|
? Promise.resolve(mockEntries(q))
|
|
|
|
|
|
: http.get<LogEntriesResult>('/logs/entries', { params: q }).then((r) => r.data),
|
|
|
|
|
|
|
|
|
|
|
|
digest: (q: DigestQuery): Promise<LogDigest> => MOCK
|
|
|
|
|
|
? Promise.resolve(mockDigest())
|
|
|
|
|
|
: http.get<LogDigest>('/logs/digest', { params: q }).then((r) => r.data),
|
|
|
|
|
|
|
2026-08-26 17:48:25 +08:00
|
|
|
|
/** 实时内存诊断(Simple3 Diagnosis.GetAllDiagnosis,经 YARP 反代)。需 Simple3 在运行,否则 502。 */
|
2026-06-08 16:11:20 +08:00
|
|
|
|
liveDiagnosis: (): Promise<LiveDiagnosis> => MOCK
|
|
|
|
|
|
? Promise.resolve(mockLiveDiagnosis())
|
|
|
|
|
|
: http.get<SlEnvelope<LiveDiagnosis>>(`${SL_DIAG}/all`).then((r) => {
|
|
|
|
|
|
if (!r.data?.success || !r.data.data) throw new Error(r.data?.message ?? '获取实时诊断失败')
|
|
|
|
|
|
return r.data.data
|
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
|
|
/** 目录浏览:列出 log/ 下某相对目录的子文件夹 + 文件(path 为空=根)。 */
|
|
|
|
|
|
browse: (path?: string): Promise<BrowseResult> => MOCK
|
|
|
|
|
|
? Promise.resolve(mockBrowse(path))
|
|
|
|
|
|
: http.get<BrowseResult>('/logs/browse', { params: path ? { path } : undefined }).then((r) => r.data),
|
|
|
|
|
|
|
|
|
|
|
|
/** 日志分析:标签分布 / 时间直方图 / 数值字段识别 / 选定字段时序。file 或 day 二选一。 */
|
|
|
|
|
|
analyze: (q: AnalyzeQuery): Promise<AnalyzeResult> => MOCK
|
|
|
|
|
|
? Promise.resolve(mockAnalyze(q))
|
|
|
|
|
|
: http.get<AnalyzeResult>('/logs/analyze', { params: q }).then((r) => r.data),
|
|
|
|
|
|
|
|
|
|
|
|
/** 取文件尾部 N 行原文(text/plain)。 */
|
|
|
|
|
|
raw: (file: string, tail = 2000): Promise<string> => MOCK
|
|
|
|
|
|
? Promise.resolve('[2026/06/01-12:21:40.120] >Persistence: worker started\n[2026/06/01-12:21:41.330] >/: loaded 12 sites')
|
|
|
|
|
|
: http.get('/logs/raw', { params: { file, tail }, responseType: 'text' }).then((r) => r.data as string),
|
|
|
|
|
|
|
|
|
|
|
|
/** 浏览器直链下载(GET,靠 httpOnly Cookie 鉴权)。 */
|
|
|
|
|
|
downloadUrl: (file: string): string =>
|
2026-08-26 17:48:25 +08:00
|
|
|
|
`${API_BASE}/logs/download?file=${encodeURIComponent(file)}`,
|
|
|
|
|
|
|
|
|
|
|
|
cleanupConfig: (): Promise<LogCleanupConfig> => MOCK
|
|
|
|
|
|
? Promise.resolve(mockCleanupConfig())
|
|
|
|
|
|
: http.get<LogCleanupConfig>('/logs/cleanup-config').then((r) => r.data),
|
|
|
|
|
|
|
|
|
|
|
|
saveCleanupConfig: (body: LogCleanupConfig): Promise<LogCleanupConfig> => MOCK
|
|
|
|
|
|
? Promise.resolve({ ...body })
|
|
|
|
|
|
: http.put<LogCleanupConfig>('/logs/cleanup-config', body).then((r) => r.data),
|
|
|
|
|
|
|
|
|
|
|
|
cleanupNow: (): Promise<LogCleanupResult> => MOCK
|
|
|
|
|
|
? Promise.resolve({ deletedFiles: 0, freedBytes: 0, freedMB: 0 })
|
|
|
|
|
|
: http.post<LogCleanupResult>('/logs/cleanup-now').then((r) => r.data)
|
2026-06-08 16:11:20 +08:00
|
|
|
|
}
|