- ICMP checker 的 type/configKey/YAML 配置键/接口属性名从 ping 改为 icmp - IcmpChecker 添加 platform 构造函数注入,修复 Windows 测试兼容性 - 前端 target 表格延迟列优化:标题简化为「延迟」,单位下移到单元格,宽度 80px - Drawer 概览页 Descriptions 添加 tableLayout=auto 收窄 label 宽度 - 同步更新 README.md、DEVELOPMENT.md、probes.example.yaml、JSON Schema 和全部测试
113 lines
4.4 KiB
TypeScript
113 lines
4.4 KiB
TypeScript
import { isNumber, isPlainObject, isString } from "es-toolkit";
|
|
|
|
import type { ConfigValidationIssue } from "../../schema/issues";
|
|
import type { CheckerValidationInput } from "../types";
|
|
|
|
import { normalizeExpectMatchers } from "../../expect/normalize";
|
|
import { validateValueMatcher } from "../../expect/validate-matcher";
|
|
import { issue, joinPath } from "../../schema/issues";
|
|
|
|
export function validatePingConfig(input: CheckerValidationInput): ConfigValidationIssue[] {
|
|
const issues: ConfigValidationIssue[] = [];
|
|
|
|
const defaults = input.defaults["icmp"];
|
|
if (defaults !== undefined && defaults !== null) {
|
|
const targetName = "defaults.icmp";
|
|
if (!isPlainObject(defaults)) {
|
|
issues.push(issue("invalid-type", "defaults.icmp", "必须为对象", targetName));
|
|
} else {
|
|
const icmpDefaults = defaults as Record<string, unknown>;
|
|
for (const key of Object.keys(icmpDefaults)) {
|
|
issues.push(issue("unknown-field", joinPath("defaults.icmp", key), "是未知字段", targetName));
|
|
}
|
|
}
|
|
}
|
|
|
|
for (let i = 0; i < input.targets.length; i++) {
|
|
const target = input.targets[i] as unknown;
|
|
if (!isPlainObject(target)) continue;
|
|
const targetRecord = target as Record<string, unknown>;
|
|
if (targetRecord["type"] !== "icmp") continue;
|
|
issues.push(...validatePingTarget(targetRecord, `targets[${i}]`));
|
|
}
|
|
|
|
return issues;
|
|
}
|
|
|
|
function getTargetName(target: Record<string, unknown>): string | undefined {
|
|
if (isString(target["name"])) return target["name"];
|
|
return isString(target["id"]) ? target["id"] : undefined;
|
|
}
|
|
|
|
function validatePingExpect(target: Record<string, unknown>, path: string): ConfigValidationIssue[] {
|
|
const rawExpect = target["expect"];
|
|
if (rawExpect === undefined || rawExpect === null || !isPlainObject(rawExpect)) return [];
|
|
const expect = rawExpect as Record<string, unknown>;
|
|
const issues: ConfigValidationIssue[] = [];
|
|
const targetName = getTargetName(target);
|
|
const expectPath = joinPath(path, "expect");
|
|
|
|
normalizeExpectMatchers(expect, ["packetLossPercent", "avgLatencyMs", "maxLatencyMs", "durationMs"]);
|
|
|
|
if (expect["alive"] !== undefined && typeof expect["alive"] !== "boolean") {
|
|
issues.push(issue("invalid-type", joinPath(expectPath, "alive"), "必须为布尔值", targetName));
|
|
}
|
|
for (const key of ["packetLossPercent", "avgLatencyMs", "maxLatencyMs", "durationMs"]) {
|
|
if (expect[key] !== undefined) {
|
|
issues.push(...validateValueMatcher(expect[key], joinPath(expectPath, key), targetName));
|
|
}
|
|
}
|
|
|
|
const allowedKeys = new Set(["alive", "avgLatencyMs", "durationMs", "maxLatencyMs", "packetLossPercent"]);
|
|
for (const key of Object.keys(expect)) {
|
|
if (!allowedKeys.has(key)) {
|
|
issues.push(issue("unknown-field", joinPath(expectPath, key), "是未知字段", targetName));
|
|
}
|
|
}
|
|
|
|
return issues;
|
|
}
|
|
|
|
function validatePingTarget(target: Record<string, unknown>, path: string): ConfigValidationIssue[] {
|
|
const issues: ConfigValidationIssue[] = [];
|
|
const targetName = getTargetName(target);
|
|
const rawIcmp = target["icmp"];
|
|
|
|
if (!isPlainObject(rawIcmp)) {
|
|
issues.push(issue("required", joinPath(path, "icmp"), "缺少 icmp 配置分组", targetName));
|
|
issues.push(...validatePingExpect(target, path));
|
|
return issues;
|
|
}
|
|
const icmp = rawIcmp as Record<string, unknown>;
|
|
|
|
if (!isString(icmp["host"]) || icmp["host"].trim() === "") {
|
|
issues.push(issue("required", joinPath(joinPath(path, "icmp"), "host"), "缺少 icmp.host 字段", targetName));
|
|
}
|
|
if (icmp["count"] !== undefined) {
|
|
const count = icmp["count"];
|
|
if (!isNumber(count) || !Number.isInteger(count) || count < 1 || count > 100) {
|
|
issues.push(
|
|
issue("invalid-value", joinPath(joinPath(path, "icmp"), "count"), "必须为 1-100 的正整数", targetName),
|
|
);
|
|
}
|
|
}
|
|
if (icmp["packetSize"] !== undefined) {
|
|
const packetSize = icmp["packetSize"];
|
|
if (!isNumber(packetSize) || !Number.isInteger(packetSize) || packetSize < 1 || packetSize > 65500) {
|
|
issues.push(
|
|
issue("invalid-value", joinPath(joinPath(path, "icmp"), "packetSize"), "必须为 1-65500 的正整数", targetName),
|
|
);
|
|
}
|
|
}
|
|
|
|
const allowedIcmpKeys = new Set(["count", "host", "packetSize"]);
|
|
for (const key of Object.keys(icmp)) {
|
|
if (!allowedIcmpKeys.has(key)) {
|
|
issues.push(issue("unknown-field", joinPath(joinPath(path, "icmp"), key), "是未知字段", targetName));
|
|
}
|
|
}
|
|
|
|
issues.push(...validatePingExpect(target, path));
|
|
return issues;
|
|
}
|