Skip to content

Commit cd57dca

Browse files
committed
Add runtime log insights
1 parent a645b6b commit cd57dca

2 files changed

Lines changed: 104 additions & 19 deletions

File tree

webui/src/components/pages/RuntimeLogsPanel.vue

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import Input from "@/components/ui/Input.vue";
77
import { useActionLock } from "@/composables/useActionLock";
88
import { useMagicNet } from "@/composables/useMagicNet";
99
import { copyText } from "@/utils";
10+
import { sanitizeOutputText } from "./outputDiagnostics";
11+
import { buildRuntimeLogInsight, formatRuntimeLogIssueReport, runtimeLogInsightTone } from "./runtimeLogInsights";
1012
1113
const { runCli, state, compactOutput } = useMagicNet();
1214
const { isRunning, withAction } = useActionLock();
@@ -18,8 +20,10 @@ const output = ref("");
1820
const copied = ref(false);
1921
const issueCopied = ref(false);
2022
const lastLabel = ref("");
23+
const loadedTarget = ref<"sing-box" | "mcp">("sing-box");
2124
const autoRefresh = ref(false);
2225
let timer = 0;
26+
const issuePattern = /\b(warn|warning|fail|failed|error|fatal|panic|denied|timeout|not found)\b/i;
2327
2428
const commandPreview = computed(() => {
2529
const count = normalizedLines();
@@ -32,16 +36,16 @@ const filteredLines = computed(() => logLines.value.filter((line) => {
3236
const matchesKeyword = !keyword || lower.includes(keyword);
3337
const matchesLevel = level.value === "all"
3438
|| (level.value === "warn" && /\b(warn|warning)\b/i.test(line))
35-
|| (level.value === "error" && /\b(error|failed|fatal|panic)\b/i.test(line));
39+
|| (level.value === "error" && /\b(error|fail|failed|fatal|panic|denied|timeout|not found)\b/i.test(line));
3640
return matchesKeyword && matchesLevel;
3741
}));
3842
const warningCount = computed(() => logLines.value.filter((line) => /\b(warn|warning)\b/i.test(line)).length);
39-
const errorCount = computed(() => logLines.value.filter((line) => /\b(error|failed|fatal|panic)\b/i.test(line)).length);
43+
const errorCount = computed(() => logLines.value.filter((line) => /\b(error|fail|failed|fatal|panic)\b/i.test(line)).length);
4044
const visibleOutput = computed(() => filteredLines.value.join("\n"));
4145
const issueLines = computed(() => logLines.value
42-
.filter((line) => /\b(warn|warning|error|failed|fatal|panic)\b/i.test(line))
46+
.filter((line) => issuePattern.test(line))
4347
.slice(-80));
44-
const lastIssueLine = computed(() => issueLines.value.at(-1) || "");
48+
const logInsight = computed(() => buildRuntimeLogInsight(logLines.value, warningCount.value, errorCount.value, issueLines.value));
4549
const quickFilters = [
4650
{ label: "错误", query: "", level: "error" },
4751
{ label: "警告", query: "", level: "warn" },
@@ -56,6 +60,7 @@ async function refreshLogs(): Promise<void> {
5660
await withAction("runtime-logs", async () => {
5761
output.value = await runCli(command, label);
5862
lastLabel.value = label;
63+
loadedTarget.value = target.value;
5964
copied.value = false;
6065
issueCopied.value = false;
6166
});
@@ -74,20 +79,19 @@ function toggleAutoRefresh(): void {
7479
}
7580
7681
async function copyLogs(): Promise<void> {
77-
copied.value = await copyText(visibleOutput.value || output.value);
78-
state.output = copied.value ? "运行日志已复制" : "剪贴板不可用,运行日志未复制。";
82+
copied.value = await copyText(sanitizeOutputText(visibleOutput.value || output.value));
83+
state.output = copied.value ? "脱敏运行日志已复制" : "剪贴板不可用,运行日志未复制。";
7984
}
8085
8186
async function copyIssueSummary(): Promise<void> {
82-
const text = [
83-
`MagicNet ${target.value} log issues`,
84-
`lines=${logLines.value.length}`,
85-
`warnings=${warningCount.value}`,
86-
`errors=${errorCount.value}`,
87-
"",
88-
...issueLines.value
89-
].join("\n").trim();
90-
issueCopied.value = await copyText(text);
87+
issueCopied.value = await copyText(formatRuntimeLogIssueReport({
88+
target: loadedTarget.value,
89+
lines: logLines.value,
90+
issueLines: issueLines.value,
91+
warningCount: warningCount.value,
92+
errorCount: errorCount.value,
93+
otherIssueCount: Math.max(0, issueLines.value.length - warningCount.value - errorCount.value)
94+
}));
9195
state.output = issueCopied.value ? "日志问题摘要已复制。" : "剪贴板不可用,日志问题摘要未复制。";
9296
}
9397
@@ -160,7 +164,12 @@ onUnmounted(stopTimer);
160164
<span>日志 {{ logLines.length }} 行</span>
161165
<span>命中 {{ filteredLines.length }} 行</span>
162166
<span class="text-amber-300">警告 {{ warningCount }}</span>
163-
<span class="text-red-300">错误 {{ errorCount }}</span>
167+
<span class="text-red-300">问题 {{ issueLines.length }}</span>
168+
</div>
169+
<div v-if="output" class="rounded-md border p-3 text-sm leading-6" :class="runtimeLogInsightTone(logInsight.status)">
170+
<p class="font-medium">{{ logInsight.label }}</p>
171+
<p class="mt-1 text-xs opacity-80">{{ logInsight.detail }}</p>
172+
<p v-if="logInsight.lastIssue" class="mt-2 truncate text-xs opacity-90">最近问题:{{ logInsight.lastIssue }}</p>
164173
</div>
165174
<div v-if="output" class="flex flex-wrap gap-2">
166175
<Button
@@ -174,9 +183,6 @@ onUnmounted(stopTimer);
174183
</Button>
175184
<Button size="sm" variant="ghost" :disabled="!query && level === 'all'" @click="query = ''; level = 'all'">全部</Button>
176185
</div>
177-
<p v-if="lastIssueLine" class="truncate rounded-md border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-xs text-amber-100">
178-
最近问题:{{ lastIssueLine }}
179-
</p>
180186
<Button v-if="issueLines.length" size="sm" variant="outline" @click="copyIssueSummary">
181187
<Copy :size="15" />{{ issueCopied ? "已复制摘要" : "复制问题摘要" }}
182188
</Button>
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { sanitizeOutputText } from "./outputDiagnostics";
2+
3+
export type RuntimeLogInsight = {
4+
status: "idle" | "ok" | "warning" | "error";
5+
label: string;
6+
detail: string;
7+
lastIssue: string;
8+
};
9+
10+
export type RuntimeLogIssueReportInput = {
11+
target: string;
12+
lines: string[];
13+
issueLines: string[];
14+
warningCount: number;
15+
errorCount: number;
16+
otherIssueCount: number;
17+
};
18+
19+
export function buildRuntimeLogInsight(lines: string[], warningCount: number, errorCount: number, issueLines: string[]): RuntimeLogInsight {
20+
if (!lines.length) {
21+
return {
22+
status: "idle",
23+
label: "等待日志",
24+
detail: "刷新后会基于真实日志尾部判断错误和警告。",
25+
lastIssue: ""
26+
};
27+
}
28+
const lastIssue = sanitizeOutputText(issueLines.at(-1) || "");
29+
if (errorCount) {
30+
return {
31+
status: "error",
32+
label: "发现错误",
33+
detail: `${errorCount} 行错误,${warningCount} 行警告。建议复制问题摘要排查。`,
34+
lastIssue
35+
};
36+
}
37+
if (warningCount) {
38+
return {
39+
status: "warning",
40+
label: "发现警告",
41+
detail: `${warningCount} 行警告,暂未匹配 fatal/error。`,
42+
lastIssue
43+
};
44+
}
45+
if (issueLines.length) {
46+
return {
47+
status: "warning",
48+
label: "发现异常线索",
49+
detail: `${issueLines.length} 行匹配 timeout/denied/not found 等异常关键词。`,
50+
lastIssue
51+
};
52+
}
53+
return {
54+
status: "ok",
55+
label: "日志正常",
56+
detail: `${lines.length} 行日志未匹配常见错误关键词。`,
57+
lastIssue: ""
58+
};
59+
}
60+
61+
export function formatRuntimeLogIssueReport(input: RuntimeLogIssueReportInput): string {
62+
return [
63+
`MagicNet ${input.target} log issues`,
64+
"privacy_note=log lines are sanitized before export",
65+
`lines=${input.lines.length}`,
66+
`warnings=${input.warningCount}`,
67+
`errors=${input.errorCount}`,
68+
`other_issues=${input.otherIssueCount}`,
69+
"",
70+
...input.issueLines.map((line) => sanitizeOutputText(line))
71+
].join("\n").trim();
72+
}
73+
74+
export function runtimeLogInsightTone(status: RuntimeLogInsight["status"]): string {
75+
if (status === "error") return "border-red-400/30 bg-red-400/10 text-red-100";
76+
if (status === "warning") return "border-amber-400/30 bg-amber-400/10 text-amber-100";
77+
if (status === "ok") return "border-emerald-400/25 bg-emerald-400/10 text-emerald-100";
78+
return "border-zinc-800 bg-zinc-950 text-zinc-400";
79+
}

0 commit comments

Comments
 (0)