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:
@@ -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),
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user