feat: 重构配置校验为 TypeBox + Ajv + semantic validator,严格禁止未知字段
- 新增 config-contract 模块(TypeBox fragments、Ajv 契约校验、ConfigValidationIssue) - CheckerDefinition 扩展为含 configKey、schemas、validate 的完整插件接口 - HTTP/Command 各自维护 contract.ts + validate.ts,校验从 resolve 中分离 - resolve 不再承担校验,只做默认值合并和路径/单位解析 - config-loader 流程: unknown → RawProbeConfig → ValidatedProbeConfig → ResolvedConfig - 导出 probe-config.schema.json,新增 schema/schema:check 脚本 - 更新 DEVELOPMENT.md 新增 1.7 开发新 Checker 完整指引 - 同步更新 4 个 main specs(probe-config、command-checker、expect-body-checkers、checker-runner-abstraction)
This commit is contained in:
453
DEVELOPMENT.md
453
DEVELOPMENT.md
@@ -35,27 +35,32 @@ src/
|
||||
trend.ts GET /api/targets/:id/trend
|
||||
checker/
|
||||
types.ts 类型定义
|
||||
config-loader.ts YAML 配置解析与校验
|
||||
config-loader.ts YAML 配置解析、契约校验、语义校验与运行期解析
|
||||
config-contract/ TypeBox + Ajv 配置契约、schema fragments、issue 渲染和 schema 导出入口
|
||||
store.ts SQLite 数据存储
|
||||
engine.ts 调度引擎(按 interval 分组的 es-toolkit groupBy + Semaphore 并发控制)
|
||||
size.ts 大小单位解析
|
||||
runner/ Checker 统一抽象与注册机制
|
||||
types.ts Checker 接口、CheckerContext、ResolveContext
|
||||
types.ts CheckerDefinition、CheckerContext、ResolveContext
|
||||
registry.ts CheckerRegistry 注册中心
|
||||
index.ts 注册入口(registerCheckers)
|
||||
shared/ 共享 expect 断言函数(跨 checker 复用)
|
||||
shared/ 共享 expect 断言和启动期 validator(跨 checker 复用)
|
||||
failure.ts 失败信息类型
|
||||
operator.ts 操作符系统(applyOperator、evaluateJsonPath)
|
||||
duration.ts 耗时断言
|
||||
text.ts 文本规则断言
|
||||
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 配置与 expect 启动期校验
|
||||
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 专属启动期语义校验
|
||||
shared/
|
||||
api.ts 前后端共享 TypeScript 类型
|
||||
web/ Vite + React 前端 Dashboard
|
||||
@@ -63,9 +68,10 @@ src/
|
||||
constants/ 常量定义(列配置、类型映射、排序/筛选/颜色阈值函数)
|
||||
hooks/ TanStack Query 数据层(useTargetDetail 集成轮询/条件查询)
|
||||
utils/ 前端工具函数
|
||||
scripts/ 开发、构建和 smoke test 脚本
|
||||
scripts/ 开发、构建、schema 生成和 smoke test 脚本
|
||||
tests/ Bun test 测试
|
||||
openspec/ OpenSpec 变更与规格文档
|
||||
probe-config.schema.json 用户配置 JSON Schema 导出物
|
||||
```
|
||||
|
||||
## 前后端边界
|
||||
@@ -143,9 +149,417 @@ 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 类型分离
|
||||
- 配置类型(`ProbeConfig`、`TargetConfig`)支持 discriminated union,通过 `type` 字段区分 http/command
|
||||
- 配置类型按生命周期区分:YAML 解析后的 `RawProbeConfig`、已通过契约与语义校验的 `ValidatedProbeConfig`、运行期使用的 `ResolvedConfig`/`ResolvedTarget`
|
||||
|
||||
### 1.6 数据存储规范
|
||||
### 1.6 配置契约与校验
|
||||
|
||||
配置加载流程固定为:`unknown -> RawProbeConfig -> ValidatedProbeConfig -> ResolvedConfig`。
|
||||
|
||||
`config-loader.ts` 只负责 YAML 解析、契约调度、公共语义校验和最终运行期解析;checker 专属规则必须下沉到对应 checker 的 `contract.ts` 和 `validate.ts`。
|
||||
|
||||
契约层使用 `src/server/checker/config-contract/` 中的 TypeBox fragments 生成 JSON Schema,并用 Ajv 执行启动期校验。Ajv 必须保持严格拒绝模式:`allErrors: true`、不启用类型强制转换、不注入默认值、不自动删除未知字段。
|
||||
|
||||
默认对象策略是 `additionalProperties: false`。只有明确声明的动态键值表可以开放任意键名,例如 `http.headers`、`defaults.http.headers`、`expect.headers`、`command.env`。
|
||||
|
||||
契约校验和语义 validator 都必须返回 `ConfigValidationIssue[]`,不要在 validator 内直接拼接最终用户错误字符串。最终错误由 `formatConfigIssues()` 统一渲染,错误路径需要尽量包含 `targetName` 或 `defaults`/root 路径。
|
||||
|
||||
新增或修改配置字段时必须同步更新:TypeBox schema fragments、`probe-config.schema.json` 导出、对应语义 validator、单元测试和 README/DEVELOPMENT 用户文档。提交前运行 `bun run schema:check` 确认导出 schema 与 fragments 一致。
|
||||
|
||||
### 1.7 开发新 Checker
|
||||
|
||||
Checker 是本项目的核心扩展单元。得益于插件式注册架构,完成一个新 checker 后,**配置校验、引擎调度、数据存储、API 层会自动适配**,无需修改这些中间层代码。
|
||||
|
||||
以下以新增 `tcp` 类型 checker 为例,说明完整的开发步骤。
|
||||
|
||||
#### 1.7.1 架构总览
|
||||
|
||||
```
|
||||
checkerRegistry(单例)
|
||||
│
|
||||
├── registerCheckers() ← 注册入口,所有 checker 在此集中注册
|
||||
│ ├── HttpChecker
|
||||
│ ├── CommandChecker
|
||||
│ └── TcpChecker ← 新增
|
||||
│
|
||||
├── config-contract/schema.ts ← 自动遍历 registry 生成全量 JSON Schema
|
||||
├── config-loader.ts ← 自动遍历 registry 调用 validate() + resolve()
|
||||
├── engine.ts ← 自动按 target.type 分发到 execute()
|
||||
└── store.ts ← 自动按 target.type 分发到 serialize()
|
||||
```
|
||||
|
||||
每个 checker 是 `src/server/checker/runner/<type>/` 下的自包含模块,包含四个文件:
|
||||
|
||||
| 文件 | 职责 |
|
||||
| ------------- | ------------------------------------------------------------------------------------- |
|
||||
| `contract.ts` | TypeBox 契约 schema(config / defaults / expect 三部分) |
|
||||
| `validate.ts` | 启动期语义校验(JSON Schema 无法表达的规则) |
|
||||
| `runner.ts` | Checker 类:resolve(默认值合并 + 解析)、execute(执行检查)、serialize(DB 持久化) |
|
||||
| `expect.ts` | Checker 专用断言函数 |
|
||||
|
||||
#### 1.7.2 步骤一:定义类型
|
||||
|
||||
在 `src/server/checker/types.ts` 中添加 checker 专属类型接口,并更新联合类型:
|
||||
|
||||
```typescript
|
||||
// 1. 添加 TargetConfig(YAML 中 target.tcp 字段的原始类型)
|
||||
export interface TcpTargetConfig {
|
||||
host: string;
|
||||
port: number;
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
// 2. 添加 ExpectConfig 扩展(如果 checker 有专属 expect 字段)
|
||||
export interface TcpExpectConfig {
|
||||
connected?: boolean;
|
||||
}
|
||||
|
||||
// 3. 添加 DefaultsConfig(defaults.tcp 字段)
|
||||
export interface TcpDefaultsConfig {
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
// 4. 添加 Resolved 变体(运行期已合并默认值、已解析路径)
|
||||
export interface ResolvedTcpTarget {
|
||||
type: "tcp";
|
||||
name: string;
|
||||
group: string;
|
||||
intervalMs: number;
|
||||
timeoutMs: number;
|
||||
tcp: {
|
||||
host: string;
|
||||
port: number;
|
||||
connectTimeout: number;
|
||||
};
|
||||
expect?: TcpExpectConfig;
|
||||
}
|
||||
```
|
||||
|
||||
然后更新以下联合类型:
|
||||
|
||||
```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 专属(如果复用公共字段则不需要)
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.7.3 步骤二:创建 TypeBox 契约 Schema
|
||||
|
||||
在 `src/server/checker/runner/tcp/contract.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(),
|
||||
port: Type.Integer({ maximum: 65535, minimum: 0 }),
|
||||
connectTimeout: Type.Optional(Type.Integer({ minimum: 100 })),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
),
|
||||
|
||||
// defaults.tcp 字段的 schema
|
||||
defaults: Type.Object(
|
||||
{
|
||||
connectTimeout: Type.Optional(Type.Integer({ minimum: 100 })),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
),
|
||||
|
||||
// target.expect 中 tcp 专属字段的 schema(如果无专属字段则用 Type.Object({}))
|
||||
expect: Type.Object(
|
||||
{
|
||||
connected: Type.Optional(Type.Boolean()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
),
|
||||
};
|
||||
```
|
||||
|
||||
**可复用的共享 fragments**(来自 `config-contract/fragments.ts`):
|
||||
|
||||
| Fragment | 用途 |
|
||||
| ---------------------------- | ---------------------------------------------- |
|
||||
| `durationSchema` | 时长字符串(`"30s"`、`"5m"`、`"500ms"`) |
|
||||
| `httpMethodSchema` | HTTP 方法枚举 |
|
||||
| `sizeSchema` | 大小单位(字符串如 `"10MB"` 或整数) |
|
||||
| `statusCodePatternSchema` | 状态码(`100`-`599` 或 `"2xx"`) |
|
||||
| `stringMapSchema` | `Record<string, string>`(用于 headers / env) |
|
||||
| `createBodyRulesSchema()` | body 规则数组(json/css/xpath/contains/regex) |
|
||||
| `createTextRulesSchema()` | 文本规则数组(stdout/stderr) |
|
||||
| `createPureOperatorSchema()` | 操作符对象 |
|
||||
| `operatorProperties()` | 所有操作符字段的 Record |
|
||||
|
||||
**注意**:默认对象策略为 `additionalProperties: false`。只有明确的动态键值表(如 `http.headers`、`command.env`)可以开放任意键名。
|
||||
|
||||
#### 1.7.4 步骤三:实现语义校验
|
||||
|
||||
在 `src/server/checker/runner/tcp/validate.ts` 中实现 JSON Schema 无法表达的语义规则:
|
||||
|
||||
```typescript
|
||||
import type { ConfigValidationIssue } from "../../config-contract/issues";
|
||||
import type { CheckerValidationInput } from "../types";
|
||||
|
||||
import { issue } from "../../config-contract/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;
|
||||
|
||||
// 校验 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));
|
||||
}
|
||||
}
|
||||
|
||||
// 校验 expect(如有公共部分可使用 shared/validate.ts 的工具函数)
|
||||
// validateBodyRules、validateTextRules、validateOperatorObject 等
|
||||
}
|
||||
|
||||
return issues;
|
||||
}
|
||||
```
|
||||
|
||||
**共享校验工具**(`runner/shared/validate.ts`):
|
||||
|
||||
| 函数 | 用途 |
|
||||
| --------------------------------------------------------- | --------------------------------- |
|
||||
| `validateBodyRules(body, path, targetName)` | 校验 body 规则数组 |
|
||||
| `validateTextRules(rules, path, targetName)` | 校验文本规则数组(stdout/stderr) |
|
||||
| `validateOperatorObject(ops, path, targetName, options?)` | 校验操作符对象 |
|
||||
| `validateJsonPath(path, rulePath, targetName)` | 校验 JSONPath 格式 |
|
||||
|
||||
#### 1.7.5 步骤四:实现 Checker 类
|
||||
|
||||
在 `src/server/checker/runner/tcp/runner.ts` 中实现 `CheckerDefinition` 接口的全部成员:
|
||||
|
||||
```typescript
|
||||
import type { CheckResult, ResolvedTarget, TargetConfig } from "../../types";
|
||||
import type { Checker, CheckerContext, CheckerValidationInput, ResolveContext } from "../types";
|
||||
|
||||
import { tcpCheckerSchemas } from "./contract";
|
||||
import { validateTcpConfig } from "./validate";
|
||||
|
||||
export class TcpChecker implements Checker {
|
||||
readonly configKey = "tcp"; // YAML 中 target.tcp / defaults.tcp 的键名
|
||||
readonly type = "tcp"; // target.type 的判别值
|
||||
readonly schemas = tcpCheckerSchemas;
|
||||
|
||||
// 启动期语义校验入口
|
||||
validate(input: CheckerValidationInput): ConfigValidationIssue[] {
|
||||
return validateTcpConfig(input);
|
||||
}
|
||||
|
||||
// 将原始配置解析为运行期配置(合并默认值、解析路径和单位)
|
||||
resolve(target: TargetConfig, context: ResolveContext): ResolvedTarget {
|
||||
const t = target as TargetConfig & { tcp: TcpTargetConfig; type: "tcp" };
|
||||
const defaults = context.defaults.tcp;
|
||||
|
||||
return {
|
||||
expect: target.expect,
|
||||
group: target.group ?? "default",
|
||||
intervalMs: context.defaultIntervalMs,
|
||||
name: t.name,
|
||||
tcp: {
|
||||
connectTimeout: t.tcp.connectTimeout ?? defaults?.connectTimeout ?? 3000,
|
||||
host: t.tcp.host,
|
||||
port: t.tcp.port,
|
||||
},
|
||||
timeoutMs: context.defaultTimeoutMs,
|
||||
type: "tcp",
|
||||
} satisfies ResolvedTcpTarget;
|
||||
}
|
||||
|
||||
// 执行实际检查,评估 expect,返回 CheckResult
|
||||
async execute(target: ResolvedTarget, ctx: CheckerContext): Promise<CheckResult> {
|
||||
const t = target as ResolvedTcpTarget;
|
||||
const timestamp = new Date().toISOString();
|
||||
const start = performance.now();
|
||||
|
||||
try {
|
||||
// 执行检查逻辑(如 TCP 连接)
|
||||
// ...
|
||||
|
||||
// 评估 expect 规则
|
||||
// 首个失败即停止,返回 failure
|
||||
|
||||
const durationMs = Math.round(performance.now() - start);
|
||||
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)),
|
||||
matched: false,
|
||||
statusDetail: null,
|
||||
targetName: t.name,
|
||||
timestamp,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 序列化为 DB 存储格式
|
||||
serialize(target: ResolvedTarget): { 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 }),
|
||||
target: `${t.tcp.host}:${t.tcp.port}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**`resolve()` 规范**:
|
||||
|
||||
- 只做默认值合并、路径解析、单位转换,**不执行校验**
|
||||
- 返回 `satisfies ResolvedXxxTarget` 确保类型正确
|
||||
- 通过 `context.defaults[this.configKey]` 访问 checker 专属默认值
|
||||
|
||||
**`execute()` 规范**:
|
||||
|
||||
- 始终记录 `timestamp`(ISO 字符串)和 `start = performance.now()`
|
||||
- 通过 `ctx.signal`(`AbortSignal`)支持超时取消
|
||||
- 首个 expect 失败即停止,返回带 `failure` 的结果
|
||||
- 成功时 `failure: null, matched: true`
|
||||
- 异常时使用 `errorFailure(phase, path, message)` 构造 failure
|
||||
- 不匹配时使用 `mismatchFailure(phase, path, expected, actual, message)` 构造 failure
|
||||
|
||||
**可用的共享断言工具**(`runner/shared/`):
|
||||
|
||||
| 模块 | 函数 | 用途 |
|
||||
| ------------- | ----------------------------------------------------- | ---------------------- |
|
||||
| `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 提取 |
|
||||
|
||||
#### 1.7.6 步骤五:注册 Checker
|
||||
|
||||
在 `src/server/checker/runner/index.ts` 中注册:
|
||||
|
||||
```typescript
|
||||
import { TcpChecker } from "./tcp/runner"; // ← 新增导入
|
||||
|
||||
export function registerCheckers(registry = checkerRegistry): void {
|
||||
registry.register(new HttpChecker());
|
||||
registry.register(new CommandChecker());
|
||||
registry.register(new TcpChecker()); // ← 新增注册
|
||||
}
|
||||
```
|
||||
|
||||
注册后,以下管线会自动适配,**无需修改**:
|
||||
|
||||
| 模块 | 自动行为 |
|
||||
| ----------------------------- | ------------------------------------------------------------------------ |
|
||||
| `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()` |
|
||||
|
||||
#### 1.7.7 步骤六:更新前端展示
|
||||
|
||||
| 文件 | 修改内容 |
|
||||
| ------------------------------------------- | ------------------------------------------------------------ |
|
||||
| `src/web/constants/target-type-display.ts` | 在 `TARGET_TYPE_DISPLAY` 中添加 `"tcp": "TCP"` |
|
||||
| `src/web/constants/target-table-filters.ts` | 在 `typeFilter.list` 中添加 `{ label: "TCP", value: "tcp" }` |
|
||||
|
||||
#### 1.7.8 步骤七:编写测试
|
||||
|
||||
测试文件放在 `tests/server/checker/runner/tcp/` 下,镜像源文件结构。必须覆盖:
|
||||
|
||||
| 测试类别 | 覆盖内容 | 参考 |
|
||||
| ---------------- | ------------------------------------------ | ---------------------------------------------------------- |
|
||||
| **契约测试** | TypeBox schema 与 JSON Schema 导出一致性 | `config-contract/validate.test.ts` |
|
||||
| **语义校验测试** | `validateTcpConfig()` 各种合法/非法输入 | `http/validate.test.ts`(通过 `runner.test.ts` 间接测试) |
|
||||
| **resolve 测试** | 默认值合并、路径解析、单位转换 | `http/runner.test.ts` 的 `HttpChecker.resolve` describe 块 |
|
||||
| **execute 测试** | 成功/失败/超时/expect 各种规则组合 | `http/runner.test.ts` 的集成测试 |
|
||||
| **注册测试** | fresh registry 不污染全局、多 checker 注册 | `registry.test.ts` |
|
||||
| **配置加载测试** | 含新 checker 的 YAML 完整加载流程 | `config-loader.test.ts` |
|
||||
|
||||
#### 1.7.9 步骤八:更新文档和 Schema
|
||||
|
||||
| 操作 | 命令/文件 |
|
||||
| --------------------------------- | -------------------------------------------- |
|
||||
| 重新生成 JSON Schema 导出 | `bun run schema` |
|
||||
| 检查导出 schema 与 fragments 一致 | `bun run schema:check` |
|
||||
| 更新配置示例 | `probes.example.yaml` 中添加新类型示例 |
|
||||
| 更新用户文档 | `README.md` 中的配置格式说明 |
|
||||
| 更新项目结构 | `DEVELOPMENT.md` 项目结构中的 runner/ 目录树 |
|
||||
|
||||
#### 1.7.10 完整检查清单
|
||||
|
||||
```
|
||||
□ src/server/checker/types.ts — 新增类型接口 + 更新联合类型
|
||||
□ src/server/checker/runner/tcp/contract.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/web/constants/target-table-filters.ts — 前端类型筛选
|
||||
□ tests/ — 契约 + 校验 + resolve + execute + 注册 测试
|
||||
□ probes.example.yaml — 配置示例
|
||||
□ bun run schema + bun run schema:check — Schema 导出同步
|
||||
□ bun run check — 全量质量检查通过
|
||||
□ bun run verify — 完整验证(含 build + smoke test)
|
||||
□ README.md — 用户文档
|
||||
□ DEVELOPMENT.md — 项目结构目录树
|
||||
```
|
||||
|
||||
### 1.8 数据存储规范
|
||||
|
||||
基于 `bun:sqlite`,WAL 模式运行,数据库文件位于配置的 `dataDir` 下。
|
||||
|
||||
@@ -168,7 +582,7 @@ export function handleXxx(params, store: ProbeStore, method: string, mode: Runti
|
||||
- `check_results` 表:target_id(FK CASCADE)、timestamp、matched(0/1)、duration_ms、status_detail、failure(JSON)
|
||||
- 复合索引:`(target_id, timestamp)`
|
||||
|
||||
### 1.7 拨测引擎
|
||||
### 1.9 拨测引擎
|
||||
|
||||
- **调度**:`ProbeEngine` 用 `es-toolkit/groupBy` 按 interval 分组,每组独立 `setInterval` 定时触发
|
||||
- **并发控制**:`es-toolkit/Semaphore` 限制全局最大并发数(`maxConcurrentChecks`),`acquire()` 阻塞等待
|
||||
@@ -177,7 +591,7 @@ export function handleXxx(params, store: ProbeStore, method: string, mode: Runti
|
||||
- **结果写入**:检查结果通过 `store.insertCheckResult()` 写入 SQLite,engine 通过 `targetNameToId` 缓存 name→id 映射
|
||||
- **生命周期**:`start()`/`stop()` 管理定时器,`stop()` 清理所有 `setInterval`
|
||||
|
||||
### 1.8 expect 断言系统
|
||||
### 1.10 expect 断言系统
|
||||
|
||||
两层模型:**观测值收集** → **规则校验**。
|
||||
|
||||
@@ -209,14 +623,14 @@ runCommandCheck → 收集观测(exitCode/stdout/stderr/durationMs)
|
||||
|
||||
**操作符**:`equals`(深度比较,`es-toolkit/isEqual`)、`contains`、`match`(正则)、`empty`(`isNil`+`isEmptyObject`)、`exists`、`gte`/`lte`/`gt`/`lt`
|
||||
|
||||
### 1.9 错误模式
|
||||
### 1.11 错误模式
|
||||
|
||||
- **API 错误**:`{ error: "描述", status: <code> }`,状态码 400/404/405/503
|
||||
- **CheckFailure**:`{ kind: "error"|"mismatch", phase, path, expected?, actual?, message }`
|
||||
- **错误处理**:expect 校验失败记录首个失败原因;网络/超时/进程崩溃统一为 `kind:"error"`,请求/TLS/timeout 错误归属 `phase:"request"`,body 超限/解码/解析错误归属 `phase:"body"`
|
||||
- **日志**:解析失败等非致命异常用 `console.warn`,启动失败用 `console.error` + `process.exit(1)`
|
||||
|
||||
### 1.10 测试规范
|
||||
### 1.12 测试规范
|
||||
|
||||
- 测试文件与源文件对应:`tests/server/checker/runner/shared/body.test.ts` ↔ `src/server/checker/runner/shared/body.ts`
|
||||
- 使用 `bun:test` 框架(`describe`/`test`/`expect`),测试数据库用临时目录 + `tmpdir()`
|
||||
@@ -591,12 +1005,14 @@ bun run test:smoke
|
||||
|
||||
### 3.6 脚本说明
|
||||
|
||||
| 脚本 | 文件 | 说明 |
|
||||
| -------------------- | ------------------ | ------------------------------ |
|
||||
| `bun run dev` | `scripts/dev.ts` | 同时启动前后端开发服务 |
|
||||
| `bun run build` | `scripts/build.ts` | Vite 构建 + Bun 编译可执行文件 |
|
||||
| `bun run test:smoke` | `scripts/smoke.ts` | 构建后的端到端验证 |
|
||||
| `bun run clean` | `scripts/clean.ts` | 清理构建缓存与临时文件 |
|
||||
| 脚本 | 文件 | 说明 |
|
||||
| ---------------------- | ----------------------------------- | ------------------------------- |
|
||||
| `bun run dev` | `scripts/dev.ts` | 同时启动前后端开发服务 |
|
||||
| `bun run build` | `scripts/build.ts` | Vite 构建 + Bun 编译可执行文件 |
|
||||
| `bun run schema` | `scripts/generate-config-schema.ts` | 生成 `probe-config.schema.json` |
|
||||
| `bun run schema:check` | `scripts/generate-config-schema.ts` | 检查配置 schema 导出物是否同步 |
|
||||
| `bun run test:smoke` | `scripts/smoke.ts` | 构建后的端到端验证 |
|
||||
| `bun run clean` | `scripts/clean.ts` | 清理构建缓存与临时文件 |
|
||||
|
||||
### 3.7 环境变量
|
||||
|
||||
@@ -648,9 +1064,10 @@ bun run test:smoke
|
||||
```bash
|
||||
bun run lint # ESLint 检查(含类型感知规则、导入排序、导入验证、Prettier 格式)
|
||||
bun run format # Prettier 自动格式化
|
||||
bun run schema:check # 检查 probe-config.schema.json 是否与 TypeBox fragments 同步
|
||||
bun run typecheck # TypeScript 类型检查(含 noUnusedLocals、noPropertyAccessFromIndexSignature)
|
||||
bun test # 运行所有测试
|
||||
bun run check # 一键运行 typecheck + lint + test
|
||||
bun run check # 一键运行 schema:check + typecheck + lint + test
|
||||
```
|
||||
|
||||
`check` 是日常开发推荐的质量检查命令。
|
||||
|
||||
Reference in New Issue
Block a user