refactor: checker 模块内聚化 — 每个 checker 自包含于独立目录
将 checker 架构重构为完全内聚模式:每个 checker 目录包含自身的 types、schema、validate、execute、expect 和 index,新增 checker 只需创建一个目录并在 runner/index.ts 添加一行注册。 主要变更: - runner/shared/ 拆分:断言基础设施迁入 checker/expect/, body.ts 迁入 http/,text.ts 迁入 command/ - config-contract/ 重命名为 schema/,schema.ts → builder.ts - size.ts + parseDuration 合并为 utils.ts - 顶层 types.ts 改为 base interface + index signature, checker 专属类型下沉到各自 types.ts - runner/index.ts 改为显式数组注册模式 - 更新 DEVELOPMENT.md 项目结构和开发新 Checker 指南
This commit is contained in:
349
DEVELOPMENT.md
349
DEVELOPMENT.md
@@ -34,33 +34,44 @@ src/
|
||||
history.ts GET /api/targets/:id/history
|
||||
trend.ts GET /api/targets/:id/trend
|
||||
checker/
|
||||
types.ts 类型定义
|
||||
types.ts 基础类型定义(ResolvedTargetBase、RawTargetConfig、DefaultsConfig 等 base interface)
|
||||
config-loader.ts YAML 配置解析、契约校验、语义校验与运行期解析
|
||||
config-contract/ TypeBox + Ajv 配置契约、schema fragments、issue 渲染和 schema 导出入口
|
||||
schema/ TypeBox + Ajv 配置契约、schema fragments、issue 渲染和 schema 导出入口
|
||||
builder.ts 全量 JSON Schema 组装(遍历 registry 生成)
|
||||
fragments.ts 共享 TypeBox schema 片段(duration、size、operator 等)
|
||||
validate.ts Ajv 契约校验入口
|
||||
issues.ts 校验问题类型与渲染
|
||||
types.ts schema 层类型
|
||||
export.ts JSON Schema 文件导出
|
||||
store.ts SQLite 数据存储
|
||||
engine.ts 调度引擎(按 interval 分组的 es-toolkit groupBy + Semaphore 并发控制)
|
||||
size.ts 大小单位解析
|
||||
utils.ts 共享工具函数(parseSize、parseDuration)
|
||||
expect/ 共享 expect 断言基础设施(跨 checker 复用)
|
||||
types.ts ExpectResult 等共享断言类型
|
||||
failure.ts 失败信息构造(errorFailure、mismatchFailure)
|
||||
operator.ts 操作符系统(applyOperator、checkExpectValue、evaluateJsonPath)
|
||||
duration.ts 耗时断言(checkDuration)
|
||||
validate-operator.ts 操作符语义校验(validateOperatorObject、isJsonValue)
|
||||
runner/ Checker 统一抽象与注册机制
|
||||
types.ts CheckerDefinition、CheckerContext、ResolveContext
|
||||
types.ts CheckerDefinition、CheckerContext、CheckerSchemas、ResolveContext
|
||||
registry.ts CheckerRegistry 注册中心
|
||||
index.ts 注册入口(registerCheckers)
|
||||
shared/ 共享 expect 断言和启动期 validator(跨 checker 复用)
|
||||
failure.ts 失败信息类型
|
||||
operator.ts 操作符系统(applyOperator、evaluateJsonPath)
|
||||
duration.ts 耗时断言
|
||||
text.ts 文本规则断言
|
||||
index.ts 注册入口(显式数组 + 循环注册)
|
||||
http/ HTTP Checker(自包含模块)
|
||||
index.ts 模块入口(re-export HttpChecker)
|
||||
types.ts HTTP 专属类型(ResolvedHttpTarget、HttpTargetConfig、HttpExpectConfig 等)
|
||||
schema.ts HTTP TypeBox 契约(defaults、target.http、expect)
|
||||
execute.ts HttpChecker 类(resolve/execute/serialize/validate)
|
||||
expect.ts HTTP 专用断言(checkStatus、checkHeaders)
|
||||
validate.ts HTTP 语义校验(URL、body rules、header operators 等)
|
||||
body.ts Body 规则断言(JSONPath/XPath/CSS/contains/regex)
|
||||
validate.ts 共享 operator/text/body 语义校验
|
||||
http/ HTTP Checker 子包
|
||||
contract.ts HTTP defaults、target.http、expect TypeBox 契约
|
||||
runner.ts HttpChecker(resolve/execute/serialize)
|
||||
expect.ts HTTP 专用断言(status/headers)
|
||||
validate.ts HTTP 专属启动期语义校验
|
||||
command/ Command Checker 子包
|
||||
contract.ts Command defaults、target.command、expect TypeBox 契约
|
||||
runner.ts CommandChecker(resolve/execute/serialize)
|
||||
expect.ts Command 专用断言(exitCode)
|
||||
validate.ts Command 专属启动期语义校验
|
||||
command/ Command Checker(自包含模块)
|
||||
index.ts 模块入口(re-export CommandChecker)
|
||||
types.ts Command 专属类型(ResolvedCommandTarget、CommandTargetConfig、CommandExpectConfig 等)
|
||||
schema.ts Command TypeBox 契约(defaults、target.command、expect)
|
||||
execute.ts CommandChecker 类(resolve/execute/serialize/validate)
|
||||
expect.ts Command 专用断言(checkExitCode)
|
||||
validate.ts Command 语义校验(text rules 等)
|
||||
text.ts 文本规则断言(checkTextRules)
|
||||
shared/
|
||||
api.ts 前后端共享 TypeScript 类型
|
||||
web/ Vite + React 前端 Dashboard
|
||||
@@ -149,15 +160,19 @@ export function handleXxx(params, store: ProbeStore, method: string, mode: Runti
|
||||
- **严格联合类型**优先于宽类型:如 `phase: "status" | "duration" | ...` 而非 `phase: string`
|
||||
- **后端内部扩展**:`checker/types.ts` 中 `CheckResult` 通过 `extends` 共享版本的 `ApiCheckResult` 增加 `targetName` 等内部字段
|
||||
- 存储层类型(`StoredTarget`、`StoredCheckResult`)独立定义,与 API 类型分离
|
||||
- 配置类型按生命周期区分:YAML 解析后的 `RawProbeConfig`、已通过契约与语义校验的 `ValidatedProbeConfig`、运行期使用的 `ResolvedConfig`/`ResolvedTarget`
|
||||
- **Checker 类型分层**:
|
||||
- `checker/types.ts` 定义 base interface(`ResolvedTargetBase`、`RawTargetConfig`、`DefaultsConfig`),使用 index signature 支持扩展
|
||||
- 各 checker 在自己的 `types.ts` 中定义具体类型(如 `ResolvedHttpTarget`、`ResolvedCommandTarget`),满足 base interface 约束
|
||||
- 中间层(engine、store、config-loader)只依赖 base interface,不感知具体 checker 类型
|
||||
- Checker 内部通过 `as` 类型断言将 base 窄化为具体类型
|
||||
|
||||
### 1.6 配置契约与校验
|
||||
|
||||
配置加载流程固定为:`unknown -> RawProbeConfig -> ValidatedProbeConfig -> ResolvedConfig`。
|
||||
|
||||
`config-loader.ts` 只负责 YAML 解析、契约调度、公共语义校验和最终运行期解析;checker 专属规则必须下沉到对应 checker 的 `contract.ts` 和 `validate.ts`。
|
||||
`config-loader.ts` 只负责 YAML 解析、契约调度、公共语义校验和最终运行期解析;checker 专属规则必须下沉到对应 checker 的 `schema.ts` 和 `validate.ts`。
|
||||
|
||||
契约层使用 `src/server/checker/config-contract/` 中的 TypeBox fragments 生成 JSON Schema,并用 Ajv 执行启动期校验。Ajv 必须保持严格拒绝模式:`allErrors: true`、不启用类型强制转换、不注入默认值、不自动删除未知字段。
|
||||
契约层使用 `src/server/checker/schema/` 中的 TypeBox fragments 生成 JSON Schema,并用 Ajv 执行启动期校验。Ajv 必须保持严格拒绝模式:`allErrors: true`、不启用类型强制转换、不注入默认值、不自动删除未知字段。
|
||||
|
||||
默认对象策略是 `additionalProperties: false`。只有明确声明的动态键值表可以开放任意键名,例如 `http.headers`、`defaults.http.headers`、`expect.headers`、`command.env`。
|
||||
|
||||
@@ -167,9 +182,7 @@ export function handleXxx(params, store: ProbeStore, method: string, mode: Runti
|
||||
|
||||
### 1.7 开发新 Checker
|
||||
|
||||
Checker 是本项目的核心扩展单元。得益于插件式注册架构,完成一个新 checker 并注册后,**配置契约组装、引擎调度、数据存储、API 层会自动走 registry 委托链路**,无需在这些中间层添加新的 type 分支。
|
||||
|
||||
当前 checker 执行链路已经注册化,但新增 checker 仍需更新中央类型定义、默认注册入口、前端展示常量、配置示例、用户/开发文档和测试。下文清单以这些必要更新为准。
|
||||
Checker 是本项目的核心扩展单元。架构设计目标是**完全内聚**:每个 checker 是 `src/server/checker/runner/<type>/` 下的自包含目录,包含该 checker 所需的全部类型、schema、校验、执行逻辑和断言。新增一个 checker 只需创建一个目录并在 `runner/index.ts` 中添加一行注册。
|
||||
|
||||
以下以新增 `tcp` 类型 checker 为例,说明完整的开发步骤。
|
||||
|
||||
@@ -178,108 +191,74 @@ Checker 是本项目的核心扩展单元。得益于插件式注册架构,完
|
||||
```
|
||||
checkerRegistry(单例)
|
||||
│
|
||||
├── registerCheckers() ← 注册入口,所有 checker 在此集中注册
|
||||
│ ├── HttpChecker
|
||||
│ ├── CommandChecker
|
||||
│ └── TcpChecker ← 新增
|
||||
├── runner/index.ts ← 显式数组注册,新增 checker 只需一行
|
||||
│ ├── new HttpChecker()
|
||||
│ ├── new CommandChecker()
|
||||
│ └── new TcpChecker() ← 新增
|
||||
│
|
||||
├── config-contract/schema.ts ← 自动遍历 registry 生成全量 JSON Schema
|
||||
├── config-loader.ts ← 自动遍历 registry 调用 validate() + resolve()
|
||||
├── engine.ts ← 自动按 target.type 分发到 execute()
|
||||
└── store.ts ← 自动按 target.type 分发到 serialize()
|
||||
├── schema/builder.ts ← 自动遍历 registry 生成全量 JSON Schema
|
||||
├── schema/validate.ts ← 自动遍历 registry 构建 Ajv 校验
|
||||
├── config-loader.ts ← 自动遍历 registry 调用 validate() + resolve()
|
||||
├── engine.ts ← 自动按 target.type 分发到 execute()
|
||||
└── store.ts ← 自动按 target.type 分发到 serialize()
|
||||
```
|
||||
|
||||
每个 checker 是 `src/server/checker/runner/<type>/` 下的自包含模块,包含四个文件:
|
||||
每个 checker 目录的标准文件结构:
|
||||
|
||||
| 文件 | 职责 |
|
||||
| ------------- | ------------------------------------------------------------------------------------- |
|
||||
| `contract.ts` | TypeBox 契约 schema(config / defaults / expect 三部分) |
|
||||
| `index.ts` | 模块入口,re-export Checker 类 |
|
||||
| `types.ts` | Checker 专属类型(ResolvedXxxTarget、XxxTargetConfig、XxxExpectConfig 等) |
|
||||
| `schema.ts` | TypeBox 契约 schema(config / defaults / expect 三部分) |
|
||||
| `validate.ts` | 启动期语义校验(JSON Schema 无法表达的规则) |
|
||||
| `runner.ts` | Checker 类:resolve(默认值合并 + 解析)、execute(执行检查)、serialize(DB 持久化) |
|
||||
| `execute.ts` | Checker 类:resolve(默认值合并 + 解析)、execute(执行检查)、serialize(DB 持久化) |
|
||||
| `expect.ts` | Checker 专用断言函数 |
|
||||
| `*.ts` | 其他 checker 专属逻辑(如 http/body.ts、command/text.ts) |
|
||||
|
||||
#### 1.7.2 步骤一:定义类型
|
||||
#### 1.7.2 步骤一:创建 Checker 目录与类型
|
||||
|
||||
在 `src/server/checker/types.ts` 中添加 checker 专属类型接口,并更新联合类型:
|
||||
在 `src/server/checker/runner/tcp/types.ts` 中定义 checker 专属类型:
|
||||
|
||||
```typescript
|
||||
// 1. 添加 TargetConfig(YAML 中 target.tcp 字段的原始类型)
|
||||
import type { ResolvedTargetBase } from "../../types";
|
||||
|
||||
export interface TcpTargetConfig {
|
||||
host: string;
|
||||
port: number;
|
||||
timeout?: number;
|
||||
connectTimeout?: number;
|
||||
}
|
||||
|
||||
// 2. 添加 ExpectConfig 扩展(如果 checker 有专属 expect 字段)
|
||||
export interface TcpExpectConfig {
|
||||
connected?: boolean;
|
||||
maxDurationMs?: number;
|
||||
}
|
||||
|
||||
// 3. 添加 DefaultsConfig(defaults.tcp 字段)
|
||||
export interface TcpDefaultsConfig {
|
||||
timeout?: number;
|
||||
connectTimeout?: number;
|
||||
}
|
||||
|
||||
// 4. 添加 Resolved 变体(运行期已合并默认值、已解析路径)
|
||||
export interface ResolvedTcpTarget {
|
||||
type: "tcp";
|
||||
name: string;
|
||||
group: string;
|
||||
intervalMs: number;
|
||||
timeoutMs: number;
|
||||
export interface ResolvedTcpTarget extends ResolvedTargetBase {
|
||||
expect?: TcpExpectConfig;
|
||||
tcp: {
|
||||
connectTimeout: number;
|
||||
host: string;
|
||||
port: number;
|
||||
connectTimeout: number;
|
||||
};
|
||||
expect?: TcpExpectConfig;
|
||||
type: "tcp";
|
||||
}
|
||||
```
|
||||
|
||||
然后更新以下联合类型:
|
||||
|
||||
```typescript
|
||||
// TargetConfig 联合 — 新增一个分支
|
||||
export type TargetConfig = BaseTargetConfig &
|
||||
(
|
||||
| { http: HttpTargetConfig; type: "http" }
|
||||
| { command: CommandTargetConfig; type: "command" }
|
||||
| { tcp: TcpTargetConfig; type: "tcp" } // ← 新增
|
||||
);
|
||||
|
||||
// ResolvedTarget 联合
|
||||
export type ResolvedTarget = ResolvedHttpTarget | ResolvedCommandTarget | ResolvedTcpTarget; // ← 新增
|
||||
|
||||
// DefaultsConfig — 新增可选字段
|
||||
export interface DefaultsConfig {
|
||||
interval?: string;
|
||||
timeout?: string;
|
||||
http?: HttpDefaultsConfig;
|
||||
command?: CommandDefaultsConfig;
|
||||
tcp?: TcpDefaultsConfig; // ← 新增
|
||||
}
|
||||
|
||||
// TargetType 联合
|
||||
export type TargetType = "command" | "http" | "tcp"; // ← 新增
|
||||
|
||||
// ExpectConfig — 如有专属字段则扩展
|
||||
export interface ExpectConfig {
|
||||
// ... 现有字段
|
||||
connected?: boolean; // ← TcpChecker 专属(如果复用公共字段则不需要)
|
||||
}
|
||||
```
|
||||
**注意**:不需要修改顶层 `checker/types.ts`。base interface 使用 index signature(`[key: string]: unknown`),checker 专属类型通过 `extends ResolvedTargetBase` 自动兼容。
|
||||
|
||||
#### 1.7.3 步骤二:创建 TypeBox 契约 Schema
|
||||
|
||||
在 `src/server/checker/runner/tcp/contract.ts` 中定义三部分 schema:
|
||||
在 `src/server/checker/runner/tcp/schema.ts` 中定义三部分 schema:
|
||||
|
||||
```typescript
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import type { CheckerSchemas } from "../types";
|
||||
import { sizeSchema } from "../../config-contract/fragments"; // 复用共享 fragments
|
||||
|
||||
export const tcpCheckerSchemas: CheckerSchemas = {
|
||||
// target.tcp 字段的 schema
|
||||
config: Type.Object(
|
||||
{
|
||||
host: Type.String(),
|
||||
@@ -289,7 +268,6 @@ export const tcpCheckerSchemas: CheckerSchemas = {
|
||||
{ additionalProperties: false },
|
||||
),
|
||||
|
||||
// defaults.tcp 字段的 schema
|
||||
defaults: Type.Object(
|
||||
{
|
||||
connectTimeout: Type.Optional(Type.Integer({ minimum: 100 })),
|
||||
@@ -297,7 +275,6 @@ export const tcpCheckerSchemas: CheckerSchemas = {
|
||||
{ additionalProperties: false },
|
||||
),
|
||||
|
||||
// target.expect 中 tcp 专属字段的 schema(如果无专属字段则用 Type.Object({}))
|
||||
expect: Type.Object(
|
||||
{
|
||||
connected: Type.Optional(Type.Boolean()),
|
||||
@@ -307,12 +284,11 @@ export const tcpCheckerSchemas: CheckerSchemas = {
|
||||
};
|
||||
```
|
||||
|
||||
**可复用的共享 fragments**(来自 `config-contract/fragments.ts`):
|
||||
**可复用的共享 fragments**(来自 `schema/fragments.ts`):
|
||||
|
||||
| Fragment | 用途 |
|
||||
| ---------------------------- | ---------------------------------------------- |
|
||||
| `durationSchema` | 时长字符串(`"30s"`、`"5m"`、`"500ms"`) |
|
||||
| `httpMethodSchema` | HTTP 方法枚举 |
|
||||
| `sizeSchema` | 大小单位(字符串如 `"10MB"` 或整数) |
|
||||
| `statusCodePatternSchema` | 状态码(`100`-`599` 或 `"2xx"`) |
|
||||
| `stringMapSchema` | `Record<string, string>`(用于 headers / env) |
|
||||
@@ -328,79 +304,64 @@ export const tcpCheckerSchemas: CheckerSchemas = {
|
||||
在 `src/server/checker/runner/tcp/validate.ts` 中实现 JSON Schema 无法表达的语义规则:
|
||||
|
||||
```typescript
|
||||
import type { ConfigValidationIssue } from "../../config-contract/issues";
|
||||
import type { ConfigValidationIssue } from "../../schema/issues";
|
||||
import type { CheckerValidationInput } from "../types";
|
||||
|
||||
import { issue } from "../../config-contract/issues";
|
||||
import { issue } from "../../schema/issues";
|
||||
|
||||
export function validateTcpConfig(input: CheckerValidationInput): ConfigValidationIssue[] {
|
||||
const issues: ConfigValidationIssue[] = [];
|
||||
|
||||
// 1. 校验 defaults.tcp(如需要)
|
||||
const defaults = input.defaults.tcp;
|
||||
if (defaults) {
|
||||
// 语义校验示例:connectTimeout 不能超过某个上限
|
||||
}
|
||||
|
||||
// 2. 遍历所有 tcp 类型的 target
|
||||
for (const target of input.targets) {
|
||||
if (target.type !== "tcp") continue;
|
||||
const name = target.name;
|
||||
const tcp = target["tcp"] as { host?: string } | undefined;
|
||||
|
||||
// 校验 target.tcp 中的语义规则
|
||||
const tcp = (target as any).tcp;
|
||||
if (tcp) {
|
||||
// 示例:host 不能为空字符串
|
||||
if (typeof tcp.host === "string" && tcp.host.trim() === "") {
|
||||
issues.push(issue("invalid-value", "tcp.host", "host 不能为空字符串", name));
|
||||
}
|
||||
if (tcp && typeof tcp.host === "string" && tcp.host.trim() === "") {
|
||||
issues.push(issue("invalid-value", "tcp.host", "host 不能为空字符串", name));
|
||||
}
|
||||
|
||||
// 校验 expect(如有公共部分可使用 shared/validate.ts 的工具函数)
|
||||
// validateBodyRules、validateTextRules、validateOperatorObject 等
|
||||
}
|
||||
|
||||
return issues;
|
||||
}
|
||||
```
|
||||
|
||||
**共享校验工具**(`runner/shared/validate.ts`):
|
||||
**共享校验工具**(`expect/validate-operator.ts`):
|
||||
|
||||
| 函数 | 用途 |
|
||||
| --------------------------------------------------------- | --------------------------------- |
|
||||
| `validateBodyRules(body, path, targetName)` | 校验 body 规则数组 |
|
||||
| `validateTextRules(rules, path, targetName)` | 校验文本规则数组(stdout/stderr) |
|
||||
| `validateOperatorObject(ops, path, targetName, options?)` | 校验操作符对象 |
|
||||
| `validateJsonPath(path, rulePath, targetName)` | 校验 JSONPath 格式 |
|
||||
| 函数 | 用途 |
|
||||
| --------------------------------------------------------- | ---------------------- |
|
||||
| `validateOperatorObject(ops, path, targetName, options?)` | 校验操作符对象 |
|
||||
| `isJsonValue(value)` | 判断是否为合法 JSON 值 |
|
||||
|
||||
#### 1.7.5 步骤四:实现 Checker 类
|
||||
|
||||
在 `src/server/checker/runner/tcp/runner.ts` 中实现 `CheckerDefinition` 接口的全部成员:
|
||||
在 `src/server/checker/runner/tcp/execute.ts` 中实现 `CheckerDefinition` 接口的全部成员:
|
||||
|
||||
```typescript
|
||||
import type { CheckResult, ResolvedTarget, TargetConfig } from "../../types";
|
||||
import type { CheckResult, RawTargetConfig, ResolvedTargetBase } from "../../types";
|
||||
import type { Checker, CheckerContext, CheckerValidationInput, ResolveContext } from "../types";
|
||||
|
||||
import { tcpCheckerSchemas } from "./contract";
|
||||
import { checkDuration } from "../../expect/duration";
|
||||
import { errorFailure } from "../../expect/failure";
|
||||
import { tcpCheckerSchemas } from "./schema";
|
||||
import type { ResolvedTcpTarget, TcpExpectConfig, TcpTargetConfig } from "./types";
|
||||
import { validateTcpConfig } from "./validate";
|
||||
|
||||
export class TcpChecker implements Checker {
|
||||
readonly configKey = "tcp"; // YAML 中 target.tcp / defaults.tcp 的键名
|
||||
readonly type = "tcp"; // target.type 的判别值
|
||||
readonly configKey = "tcp";
|
||||
readonly type = "tcp";
|
||||
readonly schemas = tcpCheckerSchemas;
|
||||
|
||||
// 启动期语义校验入口
|
||||
validate(input: CheckerValidationInput): ConfigValidationIssue[] {
|
||||
validate(input: CheckerValidationInput) {
|
||||
return validateTcpConfig(input);
|
||||
}
|
||||
|
||||
// 将原始配置解析为运行期配置(合并默认值、解析路径和单位)
|
||||
resolve(target: TargetConfig, context: ResolveContext): ResolvedTarget {
|
||||
const t = target as TargetConfig & { tcp: TcpTargetConfig; type: "tcp" };
|
||||
const defaults = context.defaults.tcp;
|
||||
resolve(target: RawTargetConfig, context: ResolveContext): ResolvedTargetBase {
|
||||
const t = target as RawTargetConfig & { tcp: TcpTargetConfig; type: "tcp" };
|
||||
const defaults = context.defaults["tcp"] as { connectTimeout?: number } | undefined;
|
||||
|
||||
return {
|
||||
expect: target.expect,
|
||||
expect: target.expect as TcpExpectConfig | undefined,
|
||||
group: target.group ?? "default",
|
||||
intervalMs: context.defaultIntervalMs,
|
||||
name: t.name,
|
||||
@@ -414,33 +375,33 @@ export class TcpChecker implements Checker {
|
||||
} satisfies ResolvedTcpTarget;
|
||||
}
|
||||
|
||||
// 执行实际检查,评估 expect,返回 CheckResult
|
||||
async execute(target: ResolvedTarget, ctx: CheckerContext): Promise<CheckResult> {
|
||||
async execute(target: ResolvedTargetBase, ctx: CheckerContext): Promise<CheckResult> {
|
||||
const t = target as ResolvedTcpTarget;
|
||||
const timestamp = new Date().toISOString();
|
||||
const start = performance.now();
|
||||
|
||||
try {
|
||||
// 执行检查逻辑(如 TCP 连接)
|
||||
// ...
|
||||
|
||||
// 评估 expect 规则
|
||||
// 首个失败即停止,返回 failure
|
||||
// 执行 TCP 连接检查...
|
||||
|
||||
const durationMs = Math.round(performance.now() - start);
|
||||
return {
|
||||
durationMs,
|
||||
failure: null,
|
||||
matched: true,
|
||||
statusDetail: "TCP connected",
|
||||
targetName: t.name,
|
||||
timestamp,
|
||||
};
|
||||
const durationResult = checkDuration(durationMs, t.expect?.maxDurationMs);
|
||||
if (!durationResult.matched) {
|
||||
return {
|
||||
durationMs,
|
||||
failure: durationResult.failure,
|
||||
matched: false,
|
||||
statusDetail: "TCP connected",
|
||||
targetName: t.name,
|
||||
timestamp,
|
||||
};
|
||||
}
|
||||
|
||||
return { durationMs, failure: null, matched: true, statusDetail: "TCP connected", targetName: t.name, timestamp };
|
||||
} catch (error) {
|
||||
const durationMs = Math.round(performance.now() - start);
|
||||
return {
|
||||
durationMs,
|
||||
failure: errorFailure("connection", "connection", isError(error) ? error.message : String(error)),
|
||||
failure: errorFailure("connection", "connection", String(error)),
|
||||
matched: false,
|
||||
statusDetail: null,
|
||||
targetName: t.name,
|
||||
@@ -449,11 +410,10 @@ export class TcpChecker implements Checker {
|
||||
}
|
||||
}
|
||||
|
||||
// 序列化为 DB 存储格式
|
||||
serialize(target: ResolvedTarget): { config: string; target: string } {
|
||||
serialize(target: ResolvedTargetBase): { config: string; target: string } {
|
||||
const t = target as ResolvedTcpTarget;
|
||||
return {
|
||||
config: JSON.stringify({ host: t.tcp.host, port: t.tcp.port, connectTimeout: t.tcp.connectTimeout }),
|
||||
config: JSON.stringify({ connectTimeout: t.tcp.connectTimeout, host: t.tcp.host, port: t.tcp.port }),
|
||||
target: `${t.tcp.host}:${t.tcp.port}`,
|
||||
};
|
||||
}
|
||||
@@ -464,7 +424,7 @@ export class TcpChecker implements Checker {
|
||||
|
||||
- 只做默认值合并、路径解析、单位转换,**不执行校验**
|
||||
- 返回 `satisfies ResolvedXxxTarget` 确保类型正确
|
||||
- 通过 `context.defaults[this.configKey]` 访问 checker 专属默认值
|
||||
- 通过 `context.defaults[this.configKey]` 访问 checker 专属默认值(需 `as` 断言为具体类型)
|
||||
|
||||
**`execute()` 规范**:
|
||||
|
||||
@@ -475,43 +435,59 @@ export class TcpChecker implements Checker {
|
||||
- 异常时使用 `errorFailure(phase, path, message)` 构造 failure
|
||||
- 不匹配时使用 `mismatchFailure(phase, path, expected, actual, message)` 构造 failure
|
||||
|
||||
**可用的共享断言工具**(`runner/shared/`):
|
||||
**可用的共享断言工具**(`checker/expect/`):
|
||||
|
||||
| 模块 | 函数 | 用途 |
|
||||
| ------------- | ----------------------------------------------------- | ---------------------- |
|
||||
| `failure.ts` | `errorFailure(phase, path, msg)` | 构造错误类型 failure |
|
||||
| `failure.ts` | `mismatchFailure(phase, path, expected, actual, msg)` | 构造不匹配类型 failure |
|
||||
| `duration.ts` | `checkDuration(ms, maxMs?)` | 耗时断言 |
|
||||
| `body.ts` | `checkBodyExpect(body, rules)` | Body 规则断言 |
|
||||
| `text.ts` | `checkTextRules(text, rules, phase)` | 文本规则断言 |
|
||||
| `operator.ts` | `applyOperator(actual, operator)` | 执行单个操作符比较 |
|
||||
| `operator.ts` | `evaluateJsonPath(json, path)` | JSONPath 提取 |
|
||||
| 模块 | 函数 | 用途 |
|
||||
| ---------------------- | ----------------------------------------------------- | ---------------------- |
|
||||
| `failure.ts` | `errorFailure(phase, path, msg)` | 构造错误类型 failure |
|
||||
| `failure.ts` | `mismatchFailure(phase, path, expected, actual, msg)` | 构造不匹配类型 failure |
|
||||
| `duration.ts` | `checkDuration(ms, maxMs?)` | 耗时断言 |
|
||||
| `operator.ts` | `applyOperator(actual, operator)` | 执行单个操作符比较 |
|
||||
| `operator.ts` | `evaluateJsonPath(json, path)` | JSONPath 提取 |
|
||||
| `validate-operator.ts` | `validateOperatorObject(ops, path, name)` | 操作符语义校验 |
|
||||
|
||||
#### 1.7.6 步骤五:注册 Checker
|
||||
**Checker 专属断言**(如需要)放在同目录的 `expect.ts` 中,参考 `http/expect.ts`(checkStatus、checkHeaders)和 `command/expect.ts`(checkExitCode)。
|
||||
|
||||
在 `src/server/checker/runner/index.ts` 中注册:
|
||||
#### 1.7.6 步骤五:创建模块入口并注册
|
||||
|
||||
创建 `src/server/checker/runner/tcp/index.ts`:
|
||||
|
||||
```typescript
|
||||
import { TcpChecker } from "./tcp/runner"; // ← 新增导入
|
||||
export { TcpChecker } from "./execute";
|
||||
```
|
||||
|
||||
export function registerCheckers(registry = checkerRegistry): void {
|
||||
registry.register(new HttpChecker());
|
||||
registry.register(new CommandChecker());
|
||||
registry.register(new TcpChecker()); // ← 新增注册
|
||||
在 `src/server/checker/runner/index.ts` 中添加一行导入和一个数组元素:
|
||||
|
||||
```typescript
|
||||
import { CommandChecker } from "./command";
|
||||
import { HttpChecker } from "./http";
|
||||
import { TcpChecker } from "./tcp"; // ← 新增
|
||||
import { CheckerRegistry } from "./registry";
|
||||
|
||||
const checkers = [new HttpChecker(), new CommandChecker(), new TcpChecker()]; // ← 新增
|
||||
|
||||
export function createDefaultCheckerRegistry(): CheckerRegistry {
|
||||
const registry = new CheckerRegistry();
|
||||
for (const checker of checkers) {
|
||||
registry.register(checker);
|
||||
}
|
||||
return registry;
|
||||
}
|
||||
|
||||
export const checkerRegistry = createDefaultCheckerRegistry();
|
||||
```
|
||||
|
||||
注册后,以下管线会通过 registry 自动委托,**无需新增 type 分支**:
|
||||
|
||||
| 模块 | 自动行为 |
|
||||
| ----------------------------- | ------------------------------------------------------------------------ |
|
||||
| `config-contract/schema.ts` | 遍历 registry 生成全量 JSON Schema(defaults.tcp + target.tcp + expect) |
|
||||
| `config-contract/validate.ts` | 按注册 checker 构建 Ajv 校验,自动识别 `type: tcp` |
|
||||
| `config-loader.ts` | 遍历 registry 调用每个 checker 的 `validate()` + `resolve()` |
|
||||
| `engine.ts` | 按 `target.type` 从 registry 取对应 checker 执行 `execute()` |
|
||||
| `store.ts` | 按 `target.type` 从 registry 取对应 checker 执行 `serialize()` |
|
||||
| 模块 | 自动行为 |
|
||||
| -------------------- | ------------------------------------------------------------------------ |
|
||||
| `schema/builder.ts` | 遍历 registry 生成全量 JSON Schema(defaults.tcp + target.tcp + expect) |
|
||||
| `schema/validate.ts` | 按注册 checker 构建 Ajv 校验,自动识别 `type: tcp` |
|
||||
| `config-loader.ts` | 遍历 registry 调用每个 checker 的 `validate()` + `resolve()` |
|
||||
| `engine.ts` | 按 `target.type` 从 registry 取对应 checker 执行 `execute()` |
|
||||
| `store.ts` | 按 `target.type` 从 registry 取对应 checker 执行 `serialize()` |
|
||||
|
||||
注意:自动适配指上述中间层不需要新增 `switch/case` 或类型分支;开发者仍需按后续步骤更新类型、注册、前端展示、示例、文档和测试。
|
||||
注意:自动适配指上述中间层不需要新增 `switch/case` 或类型分支;开发者仍需按后续步骤更新前端展示常量、配置示例、文档和测试。
|
||||
|
||||
#### 1.7.7 步骤六:更新前端展示
|
||||
|
||||
@@ -546,13 +522,14 @@ export function registerCheckers(registry = checkerRegistry): void {
|
||||
#### 1.7.10 完整检查清单
|
||||
|
||||
```
|
||||
□ src/server/checker/types.ts — 新增类型接口 + 更新联合类型
|
||||
□ src/server/checker/runner/tcp/contract.ts — TypeBox schemas
|
||||
□ src/server/checker/runner/tcp/types.ts — 专属类型(extends ResolvedTargetBase)
|
||||
□ src/server/checker/runner/tcp/schema.ts — TypeBox schemas
|
||||
□ src/server/checker/runner/tcp/validate.ts — 语义校验
|
||||
□ src/server/checker/runner/tcp/runner.ts — Checker 类
|
||||
□ src/server/checker/runner/tcp/expect.ts — 专用断言(如需要)
|
||||
□ src/server/checker/runner/index.ts — 注册
|
||||
□ src/web/constants/target-type-display.ts — 前端类型标签
|
||||
□ src/server/checker/runner/tcp/execute.ts — Checker 类
|
||||
□ src/server/checker/runner/tcp/expect.ts — 专用断言(如需要)
|
||||
□ src/server/checker/runner/tcp/index.ts — 模块入口(re-export)
|
||||
□ src/server/checker/runner/index.ts — 注册(一行导入 + 一个数组元素)
|
||||
□ src/web/constants/target-type-display.ts — 前端类型标签
|
||||
□ src/web/constants/target-table-filters.ts — 前端类型筛选
|
||||
□ tests/ — 契约 + 校验 + resolve + execute + 注册 测试
|
||||
□ probes.example.yaml — 配置示例
|
||||
@@ -597,7 +574,7 @@ export function registerCheckers(registry = checkerRegistry): void {
|
||||
|
||||
### 1.10 expect 断言系统
|
||||
|
||||
两层模型:**观测值收集** → **规则校验**。
|
||||
两层模型:**观测值收集** → **规则校验**。共享断言基础设施位于 `checker/expect/`,checker 专属断言位于各自目录。
|
||||
|
||||
**HTTP 校验流程**:
|
||||
|
||||
@@ -617,7 +594,7 @@ CommandChecker.execute → 收集观测(exitCode/stdout/stderr/durationMs)
|
||||
→ 首个失败即停止
|
||||
```
|
||||
|
||||
**Body 规则类型**:
|
||||
**Body 规则类型**(`runner/http/body.ts`):
|
||||
|
||||
- `contains`:文本包含匹配
|
||||
- `regex`:正则表达式匹配(注意:body 正则字段为 `regex`,不是 `match`)
|
||||
@@ -625,7 +602,9 @@ CommandChecker.execute → 收集观测(exitCode/stdout/stderr/durationMs)
|
||||
- `css`:cheerio CSS 选择器 + 操作符比较
|
||||
- `xpath`:XPath 节点提取 + 操作符比较
|
||||
|
||||
**操作符**:`equals`(深度比较,`es-toolkit/isEqual`)、`contains`、`match`(正则)、`empty`(`isNil`+`isEmptyObject`)、`exists`、`gte`/`lte`/`gt`/`lt`
|
||||
**文本规则**(`runner/command/text.ts`):stdout/stderr 文本匹配,支持 `contains`、`match`(正则)、操作符比较
|
||||
|
||||
**操作符**(`expect/operator.ts`):`equals`(深度比较,`es-toolkit/isEqual`)、`contains`、`match`(正则)、`empty`(`isNil`+`isEmptyObject`)、`exists`、`gte`/`lte`/`gt`/`lt`
|
||||
|
||||
### 1.11 错误模式
|
||||
|
||||
|
||||
Reference in New Issue
Block a user