1
0

feat: 将 demo 项目转化为 HTTP 拨测监控工具

新增 YAML 配置解析(Bun 内置 YAML)、SQLite 数据存储(bun:sqlite)、按 interval 分组并发拨测引擎、REST API(/api/summary、/api/targets、/api/targets/:id/history、/api/targets/:id/trend)、React 前端 Dashboard(统计卡片、目标表格、可展开详情面板、recharts 趋势图)。CLI 简化为仅接受配置文件路径。移除 /api/demo 路由和相关 demo 代码。保留 /health、静态资源服务和 SPA fallback。
This commit is contained in:
2026-05-09 17:04:25 +08:00
parent 9267f6585c
commit 57d3a5cfb4
43 changed files with 2910 additions and 525 deletions

View File

@@ -0,0 +1,87 @@
import type { CheckResult, ResolvedTarget } from "./types";
import type { ProbeStore } from "./store";
import { fetchTarget } from "./fetcher";
export class ProbeEngine {
private timers: ReturnType<typeof setInterval>[] = [];
private store: ProbeStore;
private targetNameToId: Map<string, number> = new Map();
constructor(store: ProbeStore, targets: ResolvedTarget[]) {
this.store = store;
this.targets = targets;
this.refreshCache();
}
start(): void {
const groups = this.groupByInterval(this.targets);
for (const [intervalMs, groupTargets] of groups) {
void this.probeGroup(groupTargets);
const timer = setInterval(() => {
void this.probeGroup(groupTargets);
}, intervalMs);
this.timers.push(timer);
}
}
stop(): void {
for (const timer of this.timers) {
clearInterval(timer);
}
this.timers = [];
}
private groupByInterval(targets: ResolvedTarget[]): Map<number, ResolvedTarget[]> {
const groups = new Map<number, ResolvedTarget[]>();
for (const target of targets) {
const group = groups.get(target.intervalMs) ?? [];
group.push(target);
groups.set(target.intervalMs, group);
}
return groups;
}
private async probeGroup(targets: ResolvedTarget[]): Promise<void> {
const results = await Promise.allSettled(targets.map((t) => this.probeOne(t)));
for (const result of results) {
if (result.status === "fulfilled") {
this.writeResult(result.value);
}
}
}
private async probeOne(target: ResolvedTarget): Promise<CheckResult> {
return fetchTarget(target);
}
private writeResult(result: CheckResult): void {
const targetId = this.targetNameToId.get(result.targetName);
if (!targetId) return;
this.store.insertCheckResult({
targetId,
timestamp: result.timestamp,
success: result.success,
statusCode: result.statusCode,
latencyMs: result.latencyMs,
error: result.error,
matched: result.matched,
});
}
private refreshCache(): void {
this.targetNameToId.clear();
for (const target of this.store.getTargets()) {
this.targetNameToId.set(target.name, target.id);
}
}
private targets: ResolvedTarget[];
}