1
0

refactor: 移除顶层 defaults 配置段,简化为 target 显式字段 > 代码内置默认值

- 移除 DefaultsConfig 类型、ProbeConfig.defaults 字段
- 移除 CheckerSchemas.defaults、ResolveContext.defaults、CheckerValidationInput.defaults
- 更新所有 checker schema/resolve/validate 删除 defaults 合并逻辑
- 更新 config-loader 不再读取传递 defaults
- 更新测试、README、DEVELOPMENT、probes.example.yaml
- 重新生成 probe-config.schema.json(不含 defaults)
- 同步 delta specs 到主规范
- 归档 openspec change
This commit is contained in:
2026-05-21 16:53:12 +08:00
parent e448cb4654
commit 79358ba50d
52 changed files with 196 additions and 940 deletions

View File

@@ -209,12 +209,11 @@ export class CommandChecker implements CheckerDefinition<ResolvedCommandTarget>
resolve(target: RawTargetConfig, context: ResolveContext): ResolvedCommandTarget {
const t = target as RawTargetConfig & { cmd: CommandTargetConfig; type: "cmd" };
const cmdDefaults = context.defaults["cmd"] as undefined | { cwd?: string; maxOutputBytes?: string };
const cwd = t.cmd.cwd ?? cmdDefaults?.cwd ?? ".";
const cwd = t.cmd.cwd ?? ".";
const resolvedCwd = resolve(context.configDir, cwd);
const maxOutputBytes = parseSize(t.cmd.maxOutputBytes ?? cmdDefaults?.maxOutputBytes ?? "100MB");
const maxOutputBytes = parseSize(t.cmd.maxOutputBytes ?? "100MB");
const env = { ...process.env, ...(t.cmd.env ?? {}) } as Record<string, string>;

View File

@@ -20,13 +20,6 @@ export const commandCheckerSchemas: CheckerSchemas = {
},
{ additionalProperties: false },
),
defaults: Type.Object(
{
cwd: Type.Optional(Type.String()),
maxOutputBytes: Type.Optional(sizeSchema),
},
{ additionalProperties: false },
),
expect: Type.Object(
{
durationMs: Type.Optional(createRawValueExpectationSchema()),

View File

@@ -6,11 +6,6 @@ import type {
} from "../../expect/types";
import type { ResolvedTargetBase } from "../../types";
export interface CommandDefaultsConfig {
cwd?: string;
maxOutputBytes?: string;
}
export interface CommandTargetConfig {
args?: string[];
cwd?: string;

View File

@@ -9,12 +9,6 @@ import { parseSize } from "../../utils";
export function validateCommandConfig(input: CheckerValidationInput): ConfigValidationIssue[] {
const issues: ConfigValidationIssue[] = [];
const defaults =
isPlainRecord(input.defaults) && isPlainRecord(input.defaults["cmd"]) ? input.defaults["cmd"] : undefined;
if (isSizeInput(defaults?.["maxOutputBytes"])) {
issues.push(...validateSizeValue(defaults["maxOutputBytes"], "defaults.cmd.maxOutputBytes"));
}
for (let i = 0; i < input.targets.length; i++) {
const target = input.targets[i] as unknown;

View File

@@ -20,7 +20,6 @@ export const dbCheckerSchemas: CheckerSchemas = {
},
{ additionalProperties: false },
),
defaults: Type.Object({}, { additionalProperties: false }),
expect: Type.Object(
{
durationMs: Type.Optional(createRawValueExpectationSchema()),

View File

@@ -175,12 +175,9 @@ export class HttpChecker implements CheckerDefinition<ResolvedHttpTarget> {
resolve(target: RawTargetConfig, context: ResolveContext): ResolvedHttpTarget {
const t = target as RawTargetConfig & { http: HttpTargetConfig; type: "http" };
const httpDefaults = context.defaults["http"] as
| undefined
| { headers?: Record<string, string>; maxBodyBytes?: string };
const method = t.http.method ?? "GET";
const maxBodyBytes = parseSize(t.http.maxBodyBytes ?? httpDefaults?.maxBodyBytes ?? "100MB");
const maxBodyBytes = parseSize(t.http.maxBodyBytes ?? "100MB");
const rawExpect = target.expect as RawHttpExpectConfig | undefined;
const resolvedExpect: ResolvedHttpExpectConfig = rawExpect
@@ -198,7 +195,7 @@ export class HttpChecker implements CheckerDefinition<ResolvedHttpTarget> {
group: target.group ?? "default",
http: {
body: t.http.body,
headers: { ...(httpDefaults?.headers ?? {}), ...(t.http.headers ?? {}) },
headers: { ...(t.http.headers ?? {}) },
ignoreSSL: t.http.ignoreSSL ?? false,
maxBodyBytes,
maxRedirects: t.http.maxRedirects ?? 0,

View File

@@ -25,13 +25,6 @@ export const httpCheckerSchemas: CheckerSchemas = {
},
{ additionalProperties: false },
),
defaults: Type.Object(
{
headers: Type.Optional(stringMapSchema),
maxBodyBytes: Type.Optional(sizeSchema),
},
{ additionalProperties: false },
),
expect: Type.Object(
{
body: Type.Optional(createRawContentExpectationsSchema()),

View File

@@ -8,12 +8,6 @@ import type {
} from "../../expect/types";
import type { ResolvedTargetBase } from "../../types";
export interface HttpDefaultsConfig {
headers?: Record<string, string>;
maxBodyBytes?: string;
method?: string;
}
export interface HttpTargetConfig {
body?: string;
headers?: Record<string, string>;

View File

@@ -16,12 +16,6 @@ const ALLOWED_PROTOCOLS = new Set(["http:", "https:"]);
export function validateHttpConfig(input: CheckerValidationInput): ConfigValidationIssue[] {
const issues: ConfigValidationIssue[] = [];
const defaults =
isPlainRecord(input.defaults) && isPlainRecord(input.defaults["http"]) ? input.defaults["http"] : undefined;
if (isSizeInput(defaults?.["maxBodyBytes"])) {
issues.push(...validateSizeValue(defaults["maxBodyBytes"], "defaults.http.maxBodyBytes"));
}
for (let i = 0; i < input.targets.length; i++) {
const target = input.targets[i] as unknown;

View File

@@ -13,7 +13,6 @@ export const icmpCheckerSchemas: CheckerSchemas = {
},
{ additionalProperties: false },
),
defaults: Type.Object({}, { additionalProperties: false }),
expect: Type.Object(
{
alive: Type.Optional(Type.Boolean()),

View File

@@ -9,19 +9,6 @@ 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 (!isPlainRecord(defaults)) {
issues.push(issue("invalid-type", "defaults.icmp", "必须为对象", targetName));
} else {
const icmpDefaults = defaults;
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 (!isPlainRecord(target)) continue;

View File

@@ -1,5 +1,3 @@
import type { JSONObject } from "@ai-sdk/provider";
import { APICallError, generateText, streamText } from "ai";
import { isError } from "es-toolkit";
@@ -133,43 +131,27 @@ export class LlmChecker implements CheckerDefinition<ResolvedLlmTarget> {
resolve(target: RawTargetConfig, context: ResolveContext): ResolvedLlmTarget {
const t = target as RawTargetConfig & { llm: LlmTargetConfig; type: "llm" };
const llmDefaults = context.defaults["llm"] as
| undefined
| {
headers?: Record<string, string>;
ignoreSSL?: boolean;
mode?: string;
options?: Record<string, unknown>;
providerOptions?: Record<string, Record<string, unknown>>;
};
const resolvedConfig = {
authToken: t.llm.authToken,
headers: { ...(llmDefaults?.headers ?? {}), ...(t.llm.headers ?? {}) },
ignoreSSL: t.llm.ignoreSSL ?? llmDefaults?.ignoreSSL ?? false,
headers: { ...(t.llm.headers ?? {}) },
ignoreSSL: t.llm.ignoreSSL ?? false,
key: t.llm.key ?? "",
mode: (t.llm.mode ?? llmDefaults?.mode ?? "http") as "http" | "stream",
mode: t.llm.mode ?? "http",
model: t.llm.model,
options: {
frequencyPenalty:
t.llm.options?.frequencyPenalty ?? (llmDefaults?.options?.["frequencyPenalty"] as number | undefined),
maxOutputTokens:
t.llm.options?.maxOutputTokens ?? (llmDefaults?.options?.["maxOutputTokens"] as number | undefined) ?? 16,
presencePenalty:
t.llm.options?.presencePenalty ?? (llmDefaults?.options?.["presencePenalty"] as number | undefined),
seed: t.llm.options?.seed ?? (llmDefaults?.options?.["seed"] as number | undefined),
stopSequences:
t.llm.options?.stopSequences ?? (llmDefaults?.options?.["stopSequences"] as string[] | undefined),
temperature: t.llm.options?.temperature ?? (llmDefaults?.options?.["temperature"] as number | undefined) ?? 0,
topK: t.llm.options?.topK ?? (llmDefaults?.options?.["topK"] as number | undefined),
topP: t.llm.options?.topP ?? (llmDefaults?.options?.["topP"] as number | undefined),
frequencyPenalty: t.llm.options?.frequencyPenalty,
maxOutputTokens: t.llm.options?.maxOutputTokens ?? 16,
presencePenalty: t.llm.options?.presencePenalty,
seed: t.llm.options?.seed,
stopSequences: t.llm.options?.stopSequences,
temperature: t.llm.options?.temperature ?? 0,
topK: t.llm.options?.topK,
topP: t.llm.options?.topP,
},
prompt: t.llm.prompt,
provider: t.llm.provider,
providerOptions: {
...((llmDefaults?.providerOptions ?? {}) as Record<string, JSONObject>),
...(t.llm.providerOptions ?? {}),
},
providerOptions: { ...(t.llm.providerOptions ?? {}) },
url: t.llm.url,
};

View File

@@ -43,16 +43,6 @@ export const llmCheckerSchemas: CheckerSchemas = {
},
{ additionalProperties: false },
),
defaults: Type.Object(
{
headers: Type.Optional(stringMapSchema),
ignoreSSL: Type.Optional(Type.Boolean()),
mode: Type.Optional(Type.Union([Type.Literal("http"), Type.Literal("stream")])),
options: Type.Optional(createLlmOptionsSchema()),
providerOptions: Type.Optional(Type.Record(Type.String(), Type.Object({}, { additionalProperties: true }))),
},
{ additionalProperties: false },
),
expect: Type.Object(
{
durationMs: Type.Optional(createRawValueExpectationSchema()),

View File

@@ -23,14 +23,6 @@ export interface LlmCheckObservation {
warnings: string[];
}
export interface LlmDefaultsConfig {
headers?: Record<string, string>;
ignoreSSL?: boolean;
mode?: LlmMode;
options?: LlmOptions;
providerOptions?: Record<string, JSONObject>;
}
export interface LlmHttpMetadata {
headers: Record<string, string>;
status: number;

View File

@@ -17,12 +17,6 @@ const ALLOWED_PROVIDERS = new Set(["anthropic", "openai", "openai-responses"]);
export function validateLlmConfig(input: CheckerValidationInput): ConfigValidationIssue[] {
const issues: ConfigValidationIssue[] = [];
const defaults =
isPlainRecord(input.defaults) && isPlainRecord(input.defaults["llm"]) ? input.defaults["llm"] : undefined;
if (defaults) {
issues.push(...validateLlmDefaults(defaults, "defaults.llm"));
}
for (let i = 0; i < input.targets.length; i++) {
const target = input.targets[i] as unknown;
@@ -39,28 +33,6 @@ function getTargetName(target: Record<string, unknown>): string | undefined {
return isString(target["id"]) ? target["id"] : undefined;
}
function validateLlmDefaults(defaults: Record<string, unknown>, path: string): ConfigValidationIssue[] {
const issues: ConfigValidationIssue[] = [];
if (defaults["mode"] !== undefined && !ALLOWED_MODES.has(defaults["mode"] as string)) {
issues.push(issue("invalid-type", joinPath(path, "mode"), "必须为 http 或 stream"));
}
if (defaults["ignoreSSL"] !== undefined && !isBoolean(defaults["ignoreSSL"])) {
issues.push(issue("invalid-type", joinPath(path, "ignoreSSL"), "必须为布尔值"));
}
if (defaults["headers"] !== undefined) {
issues.push(...validateStringMap(defaults["headers"], joinPath(path, "headers")));
}
if (defaults["options"] !== undefined) {
issues.push(...validateLlmOptions(defaults["options"], joinPath(path, "options")));
}
if (defaults["providerOptions"] !== undefined) {
issues.push(...validateProviderOptions(defaults["providerOptions"], joinPath(path, "providerOptions")));
}
return issues;
}
function validateLlmExpect(
target: Record<string, unknown>,
path: string,

View File

@@ -206,12 +206,9 @@ export class TcpChecker implements CheckerDefinition<ResolvedTcpTarget> {
resolve(target: RawTargetConfig, context: ResolveContext): ResolvedTcpTarget {
const t = target as RawTargetConfig & { tcp: TcpTargetConfig; type: "tcp" };
const tcpDefaults = context.defaults["tcp"] as
| undefined
| { bannerReadTimeout?: number; maxBannerBytes?: number | string };
const maxBannerBytes = parseSize(t.tcp.maxBannerBytes ?? tcpDefaults?.maxBannerBytes ?? DEFAULT_MAX_BANNER_BYTES);
const bannerReadTimeout = t.tcp.bannerReadTimeout ?? tcpDefaults?.bannerReadTimeout ?? DEFAULT_BANNER_READ_TIMEOUT;
const maxBannerBytes = parseSize(t.tcp.maxBannerBytes ?? DEFAULT_MAX_BANNER_BYTES);
const bannerReadTimeout = t.tcp.bannerReadTimeout ?? DEFAULT_BANNER_READ_TIMEOUT;
const rawExpect = target.expect as RawTcpExpectConfig | undefined;
const resolvedExpect: ResolvedTcpExpectConfig = rawExpect

View File

@@ -19,13 +19,6 @@ export const tcpCheckerSchemas: CheckerSchemas = {
},
{ additionalProperties: false },
),
defaults: Type.Object(
{
bannerReadTimeout: Type.Optional(Type.Number({ minimum: 0 })),
maxBannerBytes: Type.Optional(sizeSchema),
},
{ additionalProperties: false },
),
expect: Type.Object(
{
banner: Type.Optional(createRawContentExpectationsSchema()),

View File

@@ -37,11 +37,6 @@ export interface ResolvedTcpTarget extends ResolvedTargetBase {
type: "tcp";
}
export interface TcpDefaultsConfig {
bannerReadTimeout?: number;
maxBannerBytes?: number | string;
}
export interface TcpTargetConfig {
bannerReadTimeout?: number;
host: string;

View File

@@ -9,8 +9,6 @@ import { issue, joinPath } from "../../schema/issues";
export function validateTcpConfig(input: CheckerValidationInput): ConfigValidationIssue[] {
const issues: ConfigValidationIssue[] = [];
issues.push(...validateTcpDefaults(input));
for (let i = 0; i < input.targets.length; i++) {
const target = input.targets[i] as unknown;
if (!isPlainRecord(target)) continue;
@@ -30,40 +28,6 @@ function isNonNegativeFiniteNumber(value: unknown): boolean {
return isNumber(value) && Number.isFinite(value) && value >= 0;
}
function validateTcpDefaults(input: CheckerValidationInput): ConfigValidationIssue[] {
const issues: ConfigValidationIssue[] = [];
const defaults = input.defaults["tcp"];
if (defaults === undefined || defaults === null || !isPlainRecord(defaults)) return issues;
const targetName = "defaults.tcp";
if (defaults["bannerReadTimeout"] !== undefined && !isNonNegativeFiniteNumber(defaults["bannerReadTimeout"])) {
issues.push(issue("invalid-type", "defaults.tcp.bannerReadTimeout", "必须为非负有限数字", targetName));
}
if (defaults["maxBannerBytes"] !== undefined) {
if (
!isString(defaults["maxBannerBytes"]) &&
!(
isNumber(defaults["maxBannerBytes"]) &&
Number.isFinite(defaults["maxBannerBytes"]) &&
defaults["maxBannerBytes"] >= 0
)
) {
issues.push(issue("invalid-value", "defaults.tcp.maxBannerBytes", "必须为合法 size 值", targetName));
}
}
const allowedKeys = new Set(["bannerReadTimeout", "maxBannerBytes"]);
for (const key of Object.keys(defaults)) {
if (!allowedKeys.has(key)) {
issues.push(issue("unknown-field", joinPath("defaults.tcp", key), "是未知字段", targetName));
}
}
return issues;
}
function validateTcpExpect(
target: Record<string, unknown>,
path: string,
@@ -159,5 +123,3 @@ function validateTcpTarget(target: Record<string, unknown>, path: string): Confi
return issues;
}
export { validateTcpDefaults };

View File

@@ -1,7 +1,7 @@
import type { TSchema } from "@sinclair/typebox";
import type { ConfigValidationIssue } from "../schema/issues";
import type { CheckResult, DefaultsConfig, RawTargetConfig, ResolvedTargetBase } from "../types";
import type { CheckResult, RawTargetConfig, ResolvedTargetBase } from "../types";
export type Checker<TResolved extends ResolvedTargetBase = ResolvedTargetBase> = CheckerDefinition<TResolved>;
@@ -22,18 +22,15 @@ export interface CheckerDefinition<TResolved extends ResolvedTargetBase = Resolv
export interface CheckerSchemas {
config: TSchema;
defaults: TSchema;
expect: TSchema;
}
export interface CheckerValidationInput {
defaults: DefaultsConfig;
targets: RawTargetConfig[];
}
export interface ResolveContext {
configDir: string;
defaultIntervalMs: number;
defaults: DefaultsConfig;
defaultTimeoutMs: number;
}

View File

@@ -2,13 +2,7 @@ import { isError } from "es-toolkit";
import type { CheckResult, RawTargetConfig } from "../../types";
import type { CheckerContext, CheckerDefinition, CheckerValidationInput, ResolveContext } from "../types";
import type {
RawUdpExpectConfig,
ResolvedUdpExpectConfig,
ResolvedUdpTarget,
UdpDefaultsConfig,
UdpTargetConfig,
} from "./types";
import type { RawUdpExpectConfig, ResolvedUdpExpectConfig, ResolvedUdpTarget, UdpTargetConfig } from "./types";
import { resolveContentExpectations } from "../../expect/content";
import { errorFailure } from "../../expect/failure";
@@ -304,13 +298,10 @@ export class UdpChecker implements CheckerDefinition<ResolvedUdpTarget> {
resolve(target: RawTargetConfig, context: ResolveContext): ResolvedUdpTarget {
const t = target as RawTargetConfig & { type: "udp"; udp: UdpTargetConfig };
const udpDefaults = context.defaults["udp"] as UdpDefaultsConfig | undefined;
const encoding = t.udp.encoding ?? udpDefaults?.encoding ?? "text";
const responseEncoding = t.udp.responseEncoding ?? udpDefaults?.responseEncoding ?? "text";
const maxResponseBytes = parseSize(
t.udp.maxResponseBytes ?? udpDefaults?.maxResponseBytes ?? DEFAULT_MAX_RESPONSE_BYTES,
);
const encoding = t.udp.encoding ?? "text";
const responseEncoding = t.udp.responseEncoding ?? "text";
const maxResponseBytes = parseSize(t.udp.maxResponseBytes ?? DEFAULT_MAX_RESPONSE_BYTES);
const rawExpect = target.expect as RawUdpExpectConfig | undefined;
const resolvedExpect: ResolvedUdpExpectConfig = rawExpect

View File

@@ -20,14 +20,6 @@ export const udpCheckerSchemas: CheckerSchemas = {
},
{ additionalProperties: false },
),
defaults: Type.Object(
{
encoding: Type.Optional(Type.Union([Type.Literal("text"), Type.Literal("hex"), Type.Literal("base64")])),
maxResponseBytes: Type.Optional(sizeSchema),
responseEncoding: Type.Optional(Type.Union([Type.Literal("text"), Type.Literal("hex"), Type.Literal("base64")])),
},
{ additionalProperties: false },
),
expect: Type.Object(
{
durationMs: Type.Optional(createRawValueExpectationSchema()),

View File

@@ -44,12 +44,6 @@ export interface ResolvedUdpTarget extends ResolvedTargetBase {
udp: ResolvedUdpConfig;
}
export interface UdpDefaultsConfig {
encoding?: UdpEncoding;
maxResponseBytes?: number | string;
responseEncoding?: UdpEncoding;
}
export type UdpEncoding = "base64" | "hex" | "text";
export interface UdpTargetConfig {

View File

@@ -11,8 +11,6 @@ const VALID_ENCODINGS = new Set(["base64", "hex", "text"]);
export function validateUdpConfig(input: CheckerValidationInput): ConfigValidationIssue[] {
const issues: ConfigValidationIssue[] = [];
issues.push(...validateUdpDefaults(input));
for (let i = 0; i < input.targets.length; i++) {
const target = input.targets[i] as unknown;
if (!isPlainRecord(target)) continue;
@@ -44,29 +42,6 @@ function validateSize(value: unknown, path: string, targetName: string | undefin
return [];
}
function validateUdpDefaults(input: CheckerValidationInput): ConfigValidationIssue[] {
const issues: ConfigValidationIssue[] = [];
const defaults = input.defaults["udp"];
if (defaults === undefined || defaults === null || !isPlainRecord(defaults)) return issues;
const targetName = "defaults.udp";
issues.push(...validateEncoding(defaults["encoding"], joinPath("defaults.udp", "encoding"), targetName));
issues.push(
...validateEncoding(defaults["responseEncoding"], joinPath("defaults.udp", "responseEncoding"), targetName),
);
issues.push(...validateSize(defaults["maxResponseBytes"], joinPath("defaults.udp", "maxResponseBytes"), targetName));
const allowedKeys = new Set(["encoding", "maxResponseBytes", "responseEncoding"]);
for (const key of Object.keys(defaults)) {
if (!allowedKeys.has(key)) {
issues.push(issue("unknown-field", joinPath("defaults.udp", key), "是未知字段", targetName));
}
}
return issues;
}
function validateUdpExpect(target: Record<string, unknown>, path: string): ConfigValidationIssue[] {
const targetName = getTargetName(target);
const expect = target["expect"];