1
0

feat: 基础设施加固 — 修复构建、数据保留、错误边界、bundle 拆分

- 修复 build script 引用已删除的 registerCheckers,恢复生产构建
- 生产入口添加 SIGINT/SIGTERM 优雅关闭(与 dev.ts 一致)
- 新增 runtime.retention 配置(默认 7d),ProbeStore.prune() 定时清理过期数据
- parseDuration 扩展支持 h/d 单位
- 新增前端 ErrorBoundary 组件,防止渲染错误白屏
- Vite codeSplitting.groups 拆分 vendor chunks(业务代码 1180KB → 47KB)
- 同步 delta specs 到主规范
This commit is contained in:
2026-05-13 16:48:56 +08:00
parent 26f0bfe104
commit bcfb907bd3
25 changed files with 458 additions and 26 deletions

View File

@@ -15,6 +15,7 @@ const DEFAULT_DATA_DIR = "./data";
const DEFAULT_INTERVAL = "30s";
const DEFAULT_TIMEOUT = "10s";
const DEFAULT_MAX_CONCURRENT_CHECKS = 20;
const DEFAULT_RETENTION = "7d";
export interface ResolvedConfig {
configDir: string;
@@ -22,6 +23,7 @@ export interface ResolvedConfig {
host: string;
maxConcurrentChecks: number;
port: number;
retentionMs: number;
targets: ResolvedTargetBase[];
}
@@ -68,6 +70,7 @@ export async function loadConfig(configPath: string): Promise<ResolvedConfig> {
const dataDir = server.dataDir ?? DEFAULT_DATA_DIR;
const maxConcurrentChecks = resolveMaxConcurrentChecks(runtime);
const retentionMs = resolveRetention(runtime);
const allRuntimeIssues = [...allIssues];
if (allRuntimeIssues.length > 0) {
@@ -81,7 +84,7 @@ export async function loadConfig(configPath: string): Promise<ResolvedConfig> {
resolveTarget(target, defaults, defaultIntervalMs, defaultTimeoutMs, configDir),
);
return { configDir, dataDir, host, maxConcurrentChecks, port, targets };
return { configDir, dataDir, host, maxConcurrentChecks, port, retentionMs, targets };
}
function canRunSemanticValidation(value: unknown): boolean {
@@ -117,6 +120,10 @@ function resolveMaxConcurrentChecks(runtime: EngineRuntimeConfig): number {
return runtime.maxConcurrentChecks;
}
function resolveRetention(runtime: EngineRuntimeConfig): number {
return parseDuration(runtime.retention ?? DEFAULT_RETENTION);
}
function resolveTarget(
target: RawTargetConfig,
defaults: DefaultsConfig,
@@ -194,6 +201,11 @@ function validateConfig(config: RawProbeConfig): ConfigValidationIssue[] {
validateDurationValue(config.defaults?.interval, "defaults.interval", issues);
validateDurationValue(config.defaults?.timeout, "defaults.timeout", issues);
validateDurationValue(
typeof config.runtime?.retention === "string" ? config.runtime.retention : undefined,
"runtime.retention",
issues,
);
for (let i = 0; i < config.targets.length; i++) {
const target = config.targets[i] as unknown;
if (!isRecord(target)) continue;

View File

@@ -5,17 +5,21 @@ import type { CheckResult, ResolvedTargetBase } from "./types";
import { checkerRegistry } from "./runner";
const PRUNE_INTERVAL_MS = 3600000;
export class ProbeEngine {
private retentionMs: number;
private semaphore: Semaphore;
private store: ProbeStore;
private targetNameToId = new Map<string, number>();
private targets: ResolvedTargetBase[];
private timers: Array<ReturnType<typeof setInterval>> = [];
constructor(store: ProbeStore, targets: ResolvedTargetBase[], maxConcurrentChecks?: number) {
constructor(store: ProbeStore, targets: ResolvedTargetBase[], maxConcurrentChecks?: number, retentionMs?: number) {
this.store = store;
this.targets = targets;
this.semaphore = new Semaphore(maxConcurrentChecks ?? 20);
this.retentionMs = retentionMs ?? 0;
this.refreshCache();
}
@@ -31,6 +35,14 @@ export class ProbeEngine {
this.timers.push(timer);
}
if (this.retentionMs > 0) {
this.store.prune(this.retentionMs);
const pruneTimer = setInterval(() => {
this.store.prune(this.retentionMs);
}, PRUNE_INTERVAL_MS);
this.timers.push(pruneTimer);
}
}
stop(): void {

View File

@@ -21,7 +21,10 @@ export function createProbeConfigSchema(checkers: CheckerDefinition[], external
defaults: Type.Optional(createDefaultsSchema(checkers)),
runtime: Type.Optional(
Type.Object(
{ maxConcurrentChecks: Type.Optional(Type.Integer({ minimum: 1 })) },
{
maxConcurrentChecks: Type.Optional(Type.Integer({ minimum: 1 })),
retention: Type.Optional(durationSchema),
},
{ additionalProperties: false },
),
),

View File

@@ -257,6 +257,13 @@ export class ProbeStore {
);
}
prune(retentionMs: number): number {
if (this.closed) return 0;
const cutoff = new Date(Date.now() - retentionMs).toISOString();
const result = this.db.run("DELETE FROM check_results WHERE timestamp < ?", [cutoff]);
return result.changes;
}
syncTargets(targets: ResolvedTargetBase[]): void {
if (this.closed) return;
const existingRows = this.db.query("SELECT id, name FROM targets").all() as Array<{

View File

@@ -12,6 +12,7 @@ export interface DefaultsConfig {
export interface EngineRuntimeConfig {
maxConcurrentChecks?: number;
retention?: string;
}
export interface ExpectOperator {

View File

@@ -1,17 +1,18 @@
const DURATION_REGEX = /^(\d+(?:\.\d+)?)(ms|s|m)$/;
const DURATION_REGEX = /^(\d+(?:\.\d+)?)(ms|s|m|h|d)$/;
const SIZE_REGEX = /^(\d+(?:\.\d+)?)(B|KB|MB|GB)$/;
export function parseDuration(value: string): number {
const match = DURATION_REGEX.exec(value);
if (!match) {
throw new Error(`无效的时长格式: "${value}",支持格式如 "30s"、"5m"、"500ms"`);
throw new Error(`无效的时长格式: "${value}",支持格式如 "30s"、"5m"、"2h"、"7d"、"500ms"`);
}
const num = parseFloat(match[1]!);
const unit = match[2]!;
const durationMs = unit === "ms" ? num : unit === "s" ? num * 1000 : num * 60 * 1000;
const multipliers: Record<string, number> = { d: 86400000, h: 3600000, m: 60000, ms: 1, s: 1000 };
const durationMs = num * multipliers[unit]!;
if (!Number.isInteger(durationMs) || durationMs <= 0 || !Number.isFinite(durationMs)) {
throw new Error(`无效的时长格式: "${value}",解析结果必须为正整数毫秒`);
}

View File

@@ -11,7 +11,7 @@ async function main() {
const store = new ProbeStore(`${config.dataDir}/probe.db`);
store.syncTargets(config.targets);
const engine = new ProbeEngine(store, config.targets, config.maxConcurrentChecks);
const engine = new ProbeEngine(store, config.targets, config.maxConcurrentChecks, config.retentionMs);
engine.start();
const shutdown = () => {