51 KiB
DiAL 开发文档
本文档面向 DiAL 项目的开发者,介绍项目结构、构建流程、测试、代码规范等内容。
用户使用说明请参阅 README.md。
目录
项目结构
src/
server/
app.ts Bun HTTP 路由入口(路由分发 + API 汇聚)
config.ts CLI 参数解析
dev.ts 生产/开发启动入口
server.ts HTTP server 启动工厂
helpers.ts 共享响应格式化工具(jsonResponse、createHeaders 等)
middleware.ts API 参数校验中间件(guardGetHead、validateTargetId 等)
static.ts 静态资源服务与 SPA fallback
routes/ API 路由 handler(按端点拆分)
health.ts GET /health
summary.ts GET /api/summary
targets.ts GET /api/targets
history.ts GET /api/targets/:id/history
trend.ts GET /api/targets/:id/trend
checker/
types.ts 类型定义
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 CheckerDefinition、CheckerContext、ResolveContext
registry.ts CheckerRegistry 注册中心
index.ts 注册入口(registerCheckers)
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 专属启动期语义校验
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
components/ UI 组件(表格、分组、Drawer、状态条等)
constants/ 常量定义(列配置、类型映射、排序/筛选/颜色阈值函数)
hooks/ TanStack Query 数据层(useTargetDetail 集成轮询/条件查询)
utils/ 前端工具函数
scripts/ 开发、构建、schema 生成和 smoke test 脚本
tests/ Bun test 测试
openspec/ OpenSpec 变更与规格文档
probe-config.schema.json 用户配置 JSON Schema 导出物
前后端边界
前端只通过 HTTP 调用后端,API 路径为 /api/*。共享类型放在 src/shared,前端不得 import src/server 的运行时实现。
一、后端开发指引
1.1 架构概览
启动流程:
dev.ts → readRuntimeConfig(cli args) → loadConfig(yaml)
→ ProbeStore(db) → ProbeEngine(store, targets) → startServer(store)
运行时:
定时器(tick) → ProbeEngine.probeGroup()
→ HTTP: fetcher.ts / Command: command-runner.ts
→ runner/*/expect.ts 校验 → store.insertCheckResult()
HTTP 请求:
Request → app.ts(路由分发) → routes/*.ts(handler)
→ middleware.ts(参数校验) → helpers.ts(响应格式化) → Response
1.2 库使用优先级
后端代码开发遵循严格的库选择顺序:
| 优先级 | 来源 | 典型用途 |
|---|---|---|
| 1 | Bun 内置 API | Bun.serve、bun:sqlite、Bun.spawn、Bun.file、Bun.YAML |
| 2 | es-toolkit | 类型判断(isPlainObject/isNil/isEmptyObject)、深度比较(isEqual)、错误判断(isError)、并发控制(Semaphore)、集合操作(groupBy) |
| 3 | 标准 Web API | Object.fromEntries、Headers、fetch、AbortController |
| 4 | 主流三方库 | cheerio(HTML 解析)、xpath + @xmldom/xmldom(XML 解析) |
| 5 | 自行实现 | 仅在以上都无法满足时(如 parseDuration、parseSize、evaluateJsonPath 等专项逻辑) |
原则:新增依赖前先检查上述每一层级是否已有可用方案。禁止随意引入新依赖。
1.3 API 路由开发
路由文件位于 src/server/routes/,每个端点一个文件。handler 函数签名统一为:
export function handleXxx(params, store: ProbeStore, method: string, mode: RuntimeMode): Response;
请求处理流程:
app.ts的createFetchHandler作为总入口,根据 URL pattern 匹配路由- API 路由统一经过
guardGetHead做方法检查(仅允许 GET/HEAD) - 各 handler 内部通过
middleware.ts提供的validateTargetId、validateTimeRange、validatePagination做参数校验 - 校验函数返回
Response表示校验失败(直接返回),返回数据对象表示通过 - 业务逻辑通过
store查询数据,用helpers.ts的jsonResponse、mapCheckResult、formatDuration等格式化输出
新增路由步骤:
- 在
src/server/routes/下创建<name>.ts - 实现 handler 函数并 export
- 在
app.ts的createFetchHandler中注册路径匹配和调用 - 在
tests/server/app.test.ts中添加对应测试
1.4 共享工具
helpers.ts:跨路由共用的响应工具函数(jsonResponse、createHeaders、createApiError、mapCheckResult、formatDuration、createHealthResponse)middleware.ts:API 参数校验函数(guardGetHead、validateTargetId、validateTimeRange、validatePagination)static.ts:生产模式下的静态资源服务与 SPA fallback
1.5 类型定义规范
- 共享类型以
src/shared/api.ts为唯一源头,前后端共同引用 - 前端不得
import src/server/下的任何文件 - 严格联合类型优先于宽类型:如
phase: "status" | "duration" | ...而非phase: string - 后端内部扩展:
checker/types.ts中CheckResult通过extends共享版本的ApiCheckResult增加targetName等内部字段 - 存储层类型(
StoredTarget、StoredCheckResult)独立定义,与 API 类型分离 - 配置类型按生命周期区分:YAML 解析后的
RawProbeConfig、已通过契约与语义校验的ValidatedProbeConfig、运行期使用的ResolvedConfig/ResolvedTarget
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 层会自动走 registry 委托链路,无需在这些中间层添加新的 type 分支。
当前 checker 执行链路已经注册化,但新增 checker 仍需更新中央类型定义、默认注册入口、前端展示常量、配置示例、用户/开发文档和测试。下文清单以这些必要更新为准。
以下以新增 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 专属类型接口,并更新联合类型:
// 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;
}
然后更新以下联合类型:
// 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:
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 无法表达的语义规则:
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 接口的全部成员:
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 中注册:
import { TcpChecker } from "./tcp/runner"; // ← 新增导入
export function registerCheckers(registry = checkerRegistry): void {
registry.register(new HttpChecker());
registry.register(new CommandChecker());
registry.register(new TcpChecker()); // ← 新增注册
}
注册后,以下管线会通过 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() |
注意:自动适配指上述中间层不需要新增 switch/case 或类型分支;开发者仍需按后续步骤更新类型、注册、前端展示、示例、文档和测试。
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 下。
Statement 使用规范:
| 场景 | 方式 | 原因 |
|---|---|---|
| 单次读/写 | this.db.query(sql).get()/all()/run() |
bun:sqlite 内置 statement 缓存,自动复用 |
| 事务内多次复用 | this.db.prepare(sql) 缓存为局部变量 |
事务闭包中需要持有引用 |
查询优化:
- 避免 N+1 查询:批量场景优先用单次 SQL 聚合(GROUP BY、子查询 JOIN)+ 内存组装
- 新增批量查询方法时必须编写对应单元测试
getSummary()和GET /api/targets的响应组装已通过getLatestChecksMap+getAllTargetStats实现批量查询
Schema:
targets表:name(UNIQUE)、type、target(展示摘要)、config(JSON)、interval_ms、timeout_ms、expect(JSON)、grpcheck_results表:target_id(FK CASCADE)、timestamp、matched(0/1)、duration_ms、status_detail、failure(JSON)- 复合索引:
(target_id, timestamp)
1.9 拨测引擎
- 调度:
ProbeEngine用es-toolkit/groupBy按 interval 分组,每组独立setInterval定时触发 - 并发控制:
es-toolkit/Semaphore限制全局最大并发数(maxConcurrentChecks),acquire()阻塞等待 - Runner 选择:
engine.runCheck()通过checkerRegistry.get(target.type)获取 checker,并调用checker.execute(target, { signal }) - 超时控制:
ProbeEngine为每次检查创建AbortController并按target.timeoutMs触发 abort;checker 必须使用CheckerContext.signal感知超时,HTTP 将 signal 传给fetch(),Command 在 signal abort 时proc.kill() - 结果写入:检查结果通过
store.insertCheckResult()写入 SQLite,engine 通过targetNameToId缓存 name→id 映射 - 生命周期:
start()/stop()管理定时器,stop()清理所有setInterval
1.10 expect 断言系统
两层模型:观测值收集 → 规则校验。
HTTP 校验流程:
HttpChecker.execute → 收集观测(statusCode/headers)
→ status → headers → (early duration) → body(按需) → (final duration)
→ 首个失败即停止,返回 CheckFailure
HTTP checker 的 durationMs 覆盖完整执行(含重定向、响应体读取、解码和 expect 校验)。status 或 headers 失败时不读取 body;进入 body 前若已超过 maxDurationMs,直接返回 duration failure。
Command 校验流程:
CommandChecker.execute → 收集观测(exitCode/stdout/stderr/durationMs)
→ exitCode → duration → stdout → stderr
→ 首个失败即停止
Body 规则类型:
contains:文本包含匹配regex:正则表达式匹配(注意:body 正则字段为regex,不是match)json:JSONPath 提取 + 操作符比较(使用es-toolkit/isPlainObject区分纯值和操作符)css:cheerio CSS 选择器 + 操作符比较xpath:XPath 节点提取 + 操作符比较
操作符:equals(深度比较,es-toolkit/isEqual)、contains、match(正则)、empty(isNil+isEmptyObject)、exists、gte/lte/gt/lt
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.12 测试规范
- 测试文件与源文件对应:
tests/server/checker/runner/shared/body.test.ts↔src/server/checker/runner/shared/body.ts - 使用
bun:test框架(describe/test/expect),测试数据库用临时目录 +tmpdir() - 新增 store 方法必须编写单元测试;新增 API 端点必须在
app.test.ts中添加集成测试 - 测试后清理:
afterAll中store.close()+rm(tempDir, { recursive: true })
二、前端开发指引
2.1 技术栈概览
| 层面 | 技术 | 用途 |
|---|---|---|
| 框架 | React 19 | UI 组件开发 |
| 构建 | Vite 8 | 开发服务与生产构建 |
| 语言 | TypeScript 6 | 类型安全 |
| UI 库 | TDesign React + tdesign-icons-react | UI 组件与图标 |
| 数据层 | TanStack Query (React Query) | 服务端状态管理与自动轮询 |
| 图表 | Recharts | 拨测趋势折线图与状态环状图 |
| 路由 | 无(单页面 Dashboard) | 仅需 Drawer/Tab 做页面内导航 |
不引入的依赖:React Router(单页面场景不需要)、状态管理库(TanStack Query 即服务端状态层,组件内用 useState 足够)
2.2 组件树与数据流
main.tsx
└── QueryClientProvider(TanStack Query 全局挂载)
└── App(根组件)
├── SummaryCards(总览统计卡片)
│ └── useSummary() ─── GET /api/summary(8s 轮询)
└── TargetBoard(目标列表)
├── useTargets() ─── GET /api/targets(8s 轮询)
└── TargetGroup[](按 group 字段分组)
└── PrimaryTable ← TARGET_TABLE_COLUMNS(列定义:排序/筛选/渲染)
└── TargetDetailDrawer(目标详情抽屉)
└── useTargetDetail() ── 按需发起 trend + history 查询
├── Tab: 概览 → Statistic + TrendChart + StatusDonut + Descriptions
└── Tab: 记录 → PrimaryTable(分页历史记录)
数据层架构:
hooks/useTargetDetail.ts(唯一的数据层入口)
├── queryKeys(结构化 query key,确保缓存粒度精确)
├── useSummary() → /api/summary(8s 自动轮询)
├── useTargets() → /api/targets(8s 自动轮询)
└── useTargetDetail()(组合 hook,管理 Drawer 全部状态)
├── 内部复用 useTargets() 的缓存来查找 selectedTarget
├── useQuery(/api/targets/:id/trend)(条件查询:enabled 仅当 Drawer 打开且时间范围有效)
└── useQuery(/api/targets/:id/history)(条件查询:含分页)
2.3 TanStack Query 数据层
Query Key 规范
const queryKeys = {
summary: () => ["summary"] as const,
targets: () => ["targets"] as const,
trend: (targetId: number, from: string, to: string) => ["trend", targetId, from, to] as const,
history: (targetId: number, from: string, to: string, page: number) => ["history", targetId, from, to, page] as const,
};
- Key 使用 structured array(非字符串),以便精确匹配和按 prefix 失效
- 使用
as const保持字面量类型 - 排序:scope → id → 参数(粒度从粗到细)
查询配置规范
// 全局面板级查询(需要持续刷新)
useQuery({
queryKey: queryKeys.summary(),
queryFn: () => fetchJson<SummaryResponse>("/api/summary"),
refetchInterval: 8000, // 自动轮询间隔
refetchIntervalInBackground: false, // 切后台不轮询
});
// 详情级查询(按需加载)
useQuery({
queryKey: selectedTargetId ? queryKeys.trend(id, from, to) : ["trend", "disabled"],
queryFn: () => fetchJson(`/api/targets/${id}/trend?...`),
enabled: selectedTargetId !== null && !!timeFrom && !!timeTo, // 条件查询
});
fetch 封装
async function fetchJson<T>(url: string): Promise<T> {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json() as Promise<T>;
}
- 统一使用
fetch(不引入 axios),与后端共享 Web API 生态 - 错误抛异常,由 TanStack Query 的
error状态承接
QueryClient 全局配置
new QueryClient({
defaultOptions: {
queries: {
retry: 1, // 失败重试 1 次
refetchOnWindowFocus: true, // 窗口聚焦时刷新
staleTime: 5000, // 5s 内视为 fresh,避免重复请求
},
},
});
2.4 组件开发规范
文件命名与导入
- 每个 React 组件一个
.tsx文件,文件名使用 PascalCase(如StatusDot.tsx) - 组件 props 定义为
interface XxxProps,紧邻组件函数声明 - 类型从
../../shared/api导入,使用type导入(import type { ... })
import type { TargetStatus } from "../../shared/api";
import { StatusDot } from "./StatusDot";
interface TargetGroupProps {
name: string;
targets: TargetStatus[];
onTargetClick: (target: TargetStatus) => void;
}
export function TargetGroup({ name, targets, onTargetClick }: TargetGroupProps) {
// ...
}
组件拆分原则
- 展示组件(
components/):纯渲染逻辑,通过 props 接收数据,通过回调返回事件 - 容器逻辑放在 hooks 中,组件只做数据消费
- 常量数据(列定义、排序器、筛选器)放在
constants/,不放在组件内部 - 工具函数(时间处理等)放在
utils/,保持纯函数无副作用
现有组件清单
| 组件 | 文件 | 用途 |
|---|---|---|
App |
app.tsx |
根组件,编排全局状态与布局 |
SummaryCards |
components/SummaryCards.tsx |
总览统计卡片(全部/正常/异常) |
TargetBoard |
components/TargetBoard.tsx |
按分组渲染目标表格列表 |
TargetGroup |
components/TargetGroup.tsx |
单个分组标题 + PrimaryTable |
TargetDetailDrawer |
components/TargetDetailDrawer.tsx |
目标详情抽屉(概览/记录 Tab) |
TrendChart |
components/TrendChart.tsx |
Recharts 双轴折线图(耗时/可用率) |
StatusDonut |
components/StatusDonut.tsx |
Recharts 环状图(UP/DOWN 分布) |
StatusDot |
components/StatusDot.tsx |
圆形状态指示点(绿/红) |
StatusBar |
components/StatusBar.tsx |
最近采样状态条(多色块) |
GroupHeader |
components/GroupHeader.tsx |
分组标题(名称 + 统计) |
2.5 新增功能开发步骤
以"新增一个详情页面 Tab"为例:
- 确认数据需求:是已有 API 数据还是需要新端点?
- 如有新端点,先在
src/server/routes/添加,参考 1.3 新增路由步骤 - 如有新字段,更新
src/shared/api.ts类型定义
- 如有新端点,先在
- 实现 hooks:在
src/web/hooks/useTargetDetail.ts中新增useQuery(写好queryKey和enabled条件) - 编写组件:在
src/web/components/创建组件文件- 在
TargetDetailDrawer.tsx中新增<Tabs.TabPanel>引用
- 在
- 编写常量:如有列定义/排序器/筛选器,放在
src/web/constants/ - 编写测试:在
tests/web/下添加对应的单元测试
2.6 样式开发规范
前端基于 TDesign React 构建 UI,样式开发遵循以下优先级(从高到低):
- 使用 TDesign 组件:布局、间距、排版优先使用 TDesign 组件(如 Space、Divider、Typography)
- 使用 TDesign 组件 props:通过组件的 props 参数控制外观(如
theme、variant、size) - 使用 TDesign CSS tokens:颜色、间距、字体等使用
--td-*CSS 变量(如--td-success-color、--td-comp-margin-xxl) - 在 styles.css 中定义 CSS 类:无法通过上述方式满足的样式需求,集中定义在
styles.css中 - 自行开发组件:仅在 TDesign 无法满足需求时自行开发
红线:
- 严禁在组件中使用
style属性内联调整样式 - 严禁通过 CSS 覆盖 TDesign 组件内部类名(如
.t-tab-panel),如需定制使用组件的classNameprop - 严禁使用
!important - 颜色统一使用 TDesign CSS tokens(
--td-success-color、--td-error-color、--td-warning-color等),不使用硬编码色值
styles.css 组织:
- 自定义 CSS 变量(如可用率渐变色
--avail-0~--avail-9)定义在:root中 - 布局类(
.dashboard、.dashboard-header)定义全局页面结构 - 组件修饰类(
.status-dot--up、.latency-ok)为自定义视觉组件提供样式变体 - TDesign 表格行高亮(
.row-down)通过rowClassNameprop 应用
2.7 前端测试规范
- 测试目录:
tests/web/,结构对应src/web/ - 重点测试 constants/ 中的纯函数(排序器、筛选器、颜色阈值等)
- 使用
bun:test框架
三、项目运行、集成与打包
3.1 开发期运行
同时启动前后端
bun run dev probes.yaml
scripts/dev.ts 通过 Bun.spawn 同时启动两个子进程:
bun run dev probes.yaml
├── bun run dev:server probes.yaml → Bun HTTP 后端(默认 3000 端口)
└── bun run dev:web → Vite 前端开发服务器(5173 端口)
- 任一子进程退出会导致整体退出
SIGINT/SIGTERM信号会同时终止两个子进程BACKEND_PORT环境变量可覆盖后端端口
分别启动
# 启动后端(含 watch 模式自动重启)
bun run dev:server probes.yaml
# 另开终端启动前端
bun run dev:web
3.2 前后端集成方式
开发期代理
Vite 配置了开发代理(vite.config.ts):
server: {
proxy: {
"/api": {
target: `http://127.0.0.1:${backendPort}`,
changeOrigin: true,
},
},
}
前端访问 /api/* 时,Vite 开发服务器自动转发到后端 http://127.0.0.1:${backendPort},无需 CORS 配置。
前端开发地址为 http://127.0.0.1:5173(严格端口 strictPort: true)。
后端在开发模式下不提供静态资源服务,访问 http://127.0.0.1:3000 会提示"请通过 Vite 前端地址访问"。
生产期集成
生产可执行文件是单体应用:前端静态资源嵌入 binary,后端同时提供 API 和静态文件服务。
./dist/dial-server probes.yaml
启动后:
访问 http://127.0.0.1:3000/ → 返回前端 SPA(index.html)
访问 http://127.0.0.1:3000/api/* → 返回后端 API
访问 /assets/* → 返回带不可变缓存的静态资源
SPA fallback 逻辑(src/server/static.ts):
/→ index.html- 匹配
/assets/*→ 返回对应文件(未匹配则 404) - 其他路径(如
/dashboard)→ fallback 到 index.html(SPA 路由)
3.3 构建打包
构建命令
bun run build
构建流程详解
scripts/build.ts 执行以下步骤:
1. vite build
├── 入口:src/web/index.html
└── 输出:dist/web/(index.html + assets/)
2. 生成 .build/static-assets.ts(临时文件)
├── import Vite 产物为 Bun.file
└── 导出 staticAssets: StaticAssets 对象
3. 生成 .build/server-entry.ts(临时文件)
└── import 后端入口模块 + staticAssets,作为 Bun.build 入口
4. Bun.build({ compile, minify, sourcemap: "linked" })
└── 输出:dist/dial-server(单文件可执行 binary)
产物
| 产物 | 用途 |
|---|---|
dist/dial-server |
生产可执行文件 |
dist/web/ |
Vite 构建产物(中间产物) |
.build/ |
临时生成文件(构建后清理) |
构建参数
| 环境变量 | 说明 |
|---|---|
BUN_TARGET/BUILD_TARGET |
交叉编译目标平台(如 bun-linux-x64) |
运行可执行文件
./dist/dial-server probes.yaml
清理
bun run clean
# 清理 dist/ 构建产物、.build/ 缓存和 *.bun-build 临时文件
3.4 开发工作流
日常开发循环
bun run dev probes.yaml # 启动开发环境
# 修改代码 → Vite HMR(前端)/ bun --watch(后端自动重启)
bun run check # 提交前运行完整质量检查
完整验证流程
bun run verify
# = bun run check + bun run build + bun run test:smoke
verify 适合 CI 或正式提交前,会完整验证类型检查、lint、格式、单元测试、构建、smoke test。
3.5 Smoke Test
bun run test:smoke
scripts/smoke.ts 构建后验证流程:
- 动态分配空闲端口
- 用临时配置文件启动
dist/dial-server - 等待健康检查通过
- 验证所有 API 端点返回正确数据
- 验证静态资源服务(含 SPA fallback 和 404 处理)
- 验证安全 headers
- 测试结束清理临时目录和进程
3.6 脚本说明
| 脚本 | 文件 | 说明 |
|---|---|---|
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 环境变量
| 变量 | 用途 | 默认值 |
|---|---|---|
PORT/BACKEND_PORT |
后端监听端口(开发期 Vite 代理目标、生产期监听端口) | 3000 |
BUN_TARGET/BUILD_TARGET |
交叉编译目标平台(仅在 bun run build 时有效) |
当前平台 |
3.8 项目配置文件
| 文件 | 用途 |
|---|---|
package.json |
项目信息、脚本、依赖声明 |
tsconfig.json |
TypeScript 配置(ESNext 模块、严格模式) |
vite.config.ts |
Vite 开发代理与构建配置 |
eslint.config.js |
ESLint 规则(含前端不得 import server 的检查) |
.prettierrc.json |
Prettier 格式化规则(printWidth: 120) |
.prettierignore |
Prettier 排除路径 |
probes.example.yaml |
配置文件示例 |
opencode.json |
OpenCode 工具配置(TDesign MCP server) |
3.9 依赖管理
- 包管理器:仅使用
bun,禁止使用 npm、pnpm、yarn - 安装依赖:
bun install - 运行工具:使用
bunx,禁止使用npx、pnpx - 锁文件:
bun.lock
3.10 目录约定
| 目录 | 约定 |
|---|---|
src/server/ |
后端代码,不能 import src/web/ |
src/web/ |
前端代码,不能 import src/server/ |
src/shared/ |
前后端共享类型,双向可引用 |
scripts/ |
独立运行脚本,可 import 项目源码 |
tests/ |
测试目录,结构镜像 src 目录 |
dist/ |
构建产物(gitignore) |
.build/ |
构建临时文件(gitignore) |
openspec/ |
OpenSpec 变更管理与规格文档 |
data/ |
默认数据目录(gitignore,运行期生成 SQLite) |
代码质量
项目使用多层代码质量保障体系:ESLint 类型感知规则 + Perfectionist 导入排序 + Prettier 格式化(通过 eslint-plugin-prettier 集成至 ESLint)+ TypeScript 严格模式 + Git hooks 自动化。
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 # 一键运行 schema:check + typecheck + lint + test
check 是日常开发推荐的质量检查命令。
ESLint 规则
配置文件:eslint.config.js
| 配置来源 | 用途 |
|---|---|
@eslint/js recommended |
JavaScript 基础规则 |
typescript-eslint recommended-type-checked |
TypeScript 类型感知规则(no-floating-promises 等) |
typescript-eslint stylistic-type-checked |
TypeScript 风格规则(命名规范、语法选择等) |
eslint-plugin-perfectionist recommended-natural |
导入语句和命名导出自动排序 |
eslint-plugin-import |
导入路径验证、循环依赖检测、重复导入合并 |
eslint-plugin-prettier recommended + eslint-config-prettier |
将 Prettier 格式集成为 ESLint 规则,禁用冲突规则 |
Prettier 配置
配置文件:.prettierrc.json,通过 eslint-plugin-prettier 集成为 ESLint 规则(lint 命令同时检查格式),也可通过 format 命令独立运行。
显式声明所有格式化参数(printWidth: 120、semi: true、singleQuote: false、trailingComma: "all"、endOfLine: "lf" 等),确保不同开发环境产出完全一致的格式化结果。
TypeScript 严格标志
| 标志 | 值 | 说明 |
|---|---|---|
strict |
true | 全局严格模式 |
noUnusedLocals |
true | 未使用局部变量视为错误 |
noUnusedParameters |
false | 保留关闭(路由 handler 统一签名需要,如 handleXxx(store, method, mode)) |
noPropertyAccessFromIndexSignature |
true | 禁止通过点号访问索引签名属性,强制使用括号语法 |
noUncheckedIndexedAccess |
true | 数组/Map 访问必须运行时真值检查 |
verbatimModuleSyntax |
true | 强制 import type 纯类型导入 |
Git Hooks
通过 husky 在 commit 阶段自动执行检查:
| Hook | 行为 |
|---|---|
pre-commit |
lint-staged 对变更文件运行 eslint --fix(TS/TSX,含 Prettier 格式修复)或 prettier --write(MD/JSON/YAML) |
commit-msg |
commitlint 校验提交信息格式 类型: 简短描述 |
提交类型限定:feat、fix、refactor、docs、style、test、chore。
bun install 时自动初始化 husky hooks,无需手动配置。
测试
bun run check # 日常开发(类型检查 + lint(含格式) + 单元测试)
bun run verify # 完整验证(check + 构建 + smoke test)
已知限制
当前不做告警通知、数据自动清理、拨测目标动态增删、认证鉴权和分布式部署。Command 类型拨测不支持 Windows 环境。