1
0

feat: 重构为多类型 checker 通用框架,支持 HTTP 与命令检查

- 引入 typed target 判别联合,支持 http 与 command 两种 checker
- expect 重构为有序规则数组,按配置顺序快速失败并生成结构化 failure
- 新增 command runner,支持 exec + args 本地命令执行
- 引入全局并发限制 maxConcurrentChecks 和 size 解析 (KB/MB/GB)
- HTTP/command 各自独立 expect pipeline,应用领域默认成功语义
- SQLite schema、API、Dashboard 全链路调整为 checker 通用契约
- 补充完整测试覆盖(192 tests),更新 README 与示例配置
This commit is contained in:
2026-05-10 22:25:21 +08:00
parent 599d973cbd
commit b8810f1182
46 changed files with 3562 additions and 1062 deletions

View File

@@ -1,52 +1,6 @@
import type { CheckResult, ExpectConfig, ResolvedTarget } from "./types";
import { checkBodyExpect } from "./body-expect";
export async function fetchTarget(target: ResolvedTarget): Promise<CheckResult> {
const timestamp = new Date().toISOString();
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), target.timeoutMs);
try {
const start = performance.now();
const response = await fetch(target.url, {
method: target.method,
headers: target.headers,
body: target.method !== "GET" && target.method !== "HEAD" ? target.body : undefined,
signal: controller.signal,
});
const latencyMs = Math.round(performance.now() - start);
const body = await response.text();
const responseHeaders = headersToRecord(response.headers);
const matched = checkExpect(response.status, body, latencyMs, responseHeaders, target.expect);
return {
targetName: target.name,
timestamp,
success: true,
statusCode: response.status,
latencyMs,
error: null,
matched,
};
} catch (error) {
const isTimeout = error instanceof DOMException && error.name === "AbortError";
return {
targetName: target.name,
timestamp,
success: false,
statusCode: null,
latencyMs: null,
error: isTimeout ? `请求超时 (${target.timeoutMs}ms)` : error instanceof Error ? error.message : String(error),
matched: false,
};
} finally {
clearTimeout(timeoutId);
}
}
import type { CheckResult, ResolvedHttpTarget } from "./types";
import { checkHttpExpect } from "./expect/http";
import { errorFailure } from "./expect/failure";
function headersToRecord(headers: Headers): Record<string, string> {
const result: Record<string, string> = {};
@@ -56,35 +10,95 @@ function headersToRecord(headers: Headers): Record<string, string> {
return result;
}
export function checkExpect(
statusCode: number,
body: string,
latencyMs: number,
responseHeaders: Record<string, string>,
expect?: ExpectConfig,
): boolean {
if (!expect) return true;
export async function runHttpCheck(target: ResolvedHttpTarget): Promise<CheckResult> {
const timestamp = new Date().toISOString();
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), target.timeoutMs);
if (expect.status && !expect.status.includes(statusCode)) {
return false;
}
try {
const start = performance.now();
if (expect.headers) {
for (const [key, expectedValue] of Object.entries(expect.headers)) {
const actualValue = responseHeaders[key.toLowerCase()];
if (!actualValue || actualValue !== expectedValue) {
return false;
}
const response = await fetch(target.http.url, {
method: target.http.method,
headers: target.http.headers,
body: target.http.method !== "GET" && target.http.method !== "HEAD" ? target.http.body : undefined,
signal: controller.signal,
});
const durationMs = Math.round(performance.now() - start);
const statusCode = response.status;
const responseHeaders = headersToRecord(response.headers);
const hasBodyRules = !!(target.expect?.body && target.expect.body.length > 0);
const preBodyExpect = target.expect
? { status: target.expect.status, maxDurationMs: target.expect.maxDurationMs, headers: target.expect.headers }
: undefined;
const preBodyObs = { statusCode, headers: responseHeaders, body: null as string | null, durationMs };
const preBodyResult = checkHttpExpect(preBodyObs, preBodyExpect);
if (!hasBodyRules || !preBodyResult.matched) {
clearTimeout(timeoutId);
return {
targetName: target.name,
timestamp,
success: preBodyResult.matched,
matched: preBodyResult.matched,
durationMs,
statusDetail: `HTTP ${statusCode}`,
failure: preBodyResult.failure,
};
}
}
if (!checkBodyExpect(body, expect.body)) {
return false;
}
const bodyBuffer = await response.arrayBuffer();
clearTimeout(timeoutId);
if (expect.maxLatencyMs !== undefined && latencyMs > expect.maxLatencyMs) {
return false;
}
if (bodyBuffer.byteLength > target.http.maxBodyBytes) {
return {
targetName: target.name,
timestamp,
success: false,
matched: false,
durationMs,
statusDetail: `HTTP ${statusCode}`,
failure: errorFailure(
"body",
"body",
`响应体大小 ${bodyBuffer.byteLength} 超过限制 ${target.http.maxBodyBytes}`,
),
};
}
return true;
const body = new TextDecoder().decode(bodyBuffer);
const fullObs = { statusCode, headers: responseHeaders, body, durationMs };
const fullResult = checkHttpExpect(fullObs, target.expect);
return {
targetName: target.name,
timestamp,
success: fullResult.matched,
matched: fullResult.matched,
durationMs,
statusDetail: `HTTP ${statusCode}`,
failure: fullResult.failure,
};
} catch (error) {
clearTimeout(timeoutId);
const isTimeout = error instanceof DOMException && error.name === "AbortError";
return {
targetName: target.name,
timestamp,
success: false,
matched: false,
durationMs: null,
statusDetail: null,
failure: errorFailure(
"status",
"request",
isTimeout ? `请求超时 (${target.timeoutMs}ms)` : error instanceof Error ? error.message : String(error),
),
};
}
}