# DiAL 开发文档 本文档面向 DiAL 项目的开发者,介绍项目结构、构建流程、测试、代码规范等内容。 用户使用说明请参阅 [README.md](README.md)。 ## 目录 - [项目结构](#项目结构) - [一、后端开发指引](#一后端开发指引) - [二、前端开发指引](#二前端开发指引) - [三、项目运行、集成与打包](#三项目运行集成与打包) - [代码质量](#代码质量) - [已知限制](#已知限制) --- ## 项目结构 ```text src/ server/ app.ts Bun HTTP 路由入口(路由分发 + API 汇聚、StaticAssets 接口定义) config.ts CLI 参数解析(仅提取配置文件路径) dev.ts 开发模式启动入口 server.ts HTTP server 启动工厂(接收 StartServerOptions) helpers.ts 共享响应格式化工具(见下方函数清单) middleware.ts API 参数校验中间件(guardGetHead、validateTargetId、validateTimeRange、validatePagination) static.ts 静态资源服务与 SPA fallback routes/ API 路由 handler(按端点拆分,签名因端点而异) health.ts GET /health(无 store 参数) 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 基础类型定义(ResolvedTargetBase、RawTargetConfig、DefaultsConfig、CheckResult 等基础 interface) config-loader.ts YAML 配置解析、契约校验、语义校验与运行期解析(输出 ResolvedConfig) 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 数据存储(含 syncTargets、prune 等生命周期方法) engine.ts 调度引擎(按 interval 分组的 es-toolkit groupBy + Semaphore 并发控制 + 数据清理) utils.ts 共享工具函数(parseSize、parseDuration) expect/ 共享 expect 断言基础设施(跨 checker 复用) types.ts ExpectResult 共享断言类型 failure.ts 失败信息构造(errorFailure、mismatchFailure、truncateActual) operator.ts 操作符系统(applyOperator、evaluateJsonPath) duration.ts 耗时断言(checkDuration) validate-operator.ts 操作符语义校验(validateOperatorObject、isJsonValue、isPlainRecord) runner/ Checker 统一抽象与注册机制 types.ts CheckerDefinition、CheckerContext、CheckerSchemas、ResolveContext registry.ts CheckerRegistry 注册中心 index.ts 注册入口(显式数组 + 循环注册) http/ HTTP Checker(自包含模块,含 types/schema/execute/expect/validate/body) command/ Command Checker(自包含模块,含 types/schema/execute/expect/validate/text) shared/ api.ts 前后端共享 TypeScript 类型 web/ Vite + React 前端 Dashboard app.tsx 根组件(编排全局状态与布局) main.tsx 入口(QueryClient 挂载 + ErrorBoundary + ReactQueryDevtools) styles.css 全局样式与自定义 CSS 变量 components/ UI 组件(见下方组件清单) constants/ 常量与纯函数 target-type-display.ts 类型名称映射 target-table-columns.tsx 表格列定义 target-table-filters.ts 表格筛选器 target-table-sorters.ts 表格排序器 color-threshold.ts 可用率颜色阈值函数 hooks/ TanStack Query 数据层 useTargetDetail.ts 集成轮询/条件查询的组合 hook utils/ 前端工具函数 time.ts 时间处理(subtractHours) scripts/ 开发、构建、schema 生成和 smoke test 脚本 tests/ Bun test 测试(结构镜像 src 目录) openspec/ OpenSpec 变更与规格文档 probe-config.schema.json 用户配置 JSON Schema 导出物(用于 IDE 自动补全和校验) ``` > **说明**:`runner/http/` 和 `runner/command/` 的完整文件结构见 [1.7.1 架构总览](#171-架构总览) 中的标准文件表。 ## 前后端边界 前端只通过 HTTP 调用后端,API 路径为 `/api/*`。共享类型放在 `src/shared`,前端不得 import `src/server` 的运行时实现。 --- ## 一、后端开发指引 ### 1.1 架构概览 ``` 启动流程: dev.ts → readRuntimeConfig(cli args, 仅提取 configPath) → loadConfig(yaml) → ResolvedConfig{ host, port, dataDir, maxConcurrentChecks, retentionMs, targets } → ProbeStore(db) → store.syncTargets(targets) → ProbeEngine(store, targets, maxConcurrentChecks, retentionMs) → startServer({ config, mode: "development", store }) 运行时: 定时器(tick) → ProbeEngine.probeGroup() → checkerRegistry.get(target.type).execute() → runner/*/expect.ts 校验 → engine.writeResult() → store.insertCheckResult() 数据清理: 定时 prune(retentionMs),每小时执行一次 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 函数签名因端点而异: ```typescript // 无 store 的路由(健康检查不依赖数据库) export function handleHealth(method: string, mode: RuntimeMode): Response; // 仅有 store 的路由 export function handleSummary(store: ProbeStore, method: string, mode: RuntimeMode): Response; export function handleTargets(store: ProbeStore, method: string, mode: RuntimeMode): Response; // 带 target ID 和查询参数的路由 export function handleHistory(idStr: string, url: URL, method: string, store: ProbeStore, mode: RuntimeMode): Response; export function handleTrend(idStr: string, url: URL, method: string, store: ProbeStore, mode: RuntimeMode): Response; ``` **请求处理流程**: 1. `app.ts` 的 `createFetchHandler` 作为总入口,根据 URL pattern 匹配路由 2. `/health` 路由独立处理,不经过 `guardGetHead`(使用 `helpers.ts` 的 `allowsGetHead` 自行校验方法) 3. `/api/*` 路由统一经过 `guardGetHead` 做方法检查(仅允许 GET/HEAD),返回 `null` 表示通过 4. 各 handler 内部通过 `middleware.ts` 提供的 `validateTargetId`、`validateTimeRange`、`validatePagination` 做参数校验 5. 校验函数返回 `Response` 实例表示校验失败(直接返回),返回数据对象表示通过 6. 业务逻辑通过 `store` 查询数据,用 `helpers.ts` 的 `jsonResponse`、`mapCheckResult`、`formatDuration` 等格式化输出 **新增路由步骤**: 1. 在 `src/server/routes/` 下创建 `.ts` 2. 实现 handler 函数并 export 3. 在 `app.ts` 的 `createFetchHandler` 中注册路径匹配和调用 4. 在 `tests/server/app.test.ts` 中添加对应测试 ### 1.4 共享工具 - **`helpers.ts`**:跨路由共用的响应工具函数 - `allowsGetHead(method)` — 判断是否为 GET/HEAD 方法 - `createApiError(error, status)` — 构造 API 错误体 - `createHeaders(mode, init)` — 创建响应 Headers(生产模式附加安全头) - `createHealthResponse()` — 构造健康检查响应 - `formatDuration(ms)` — 毫秒转为可读时长字符串 - `jsonResponse(body, options)` — JSON 响应构造(自动处理 HEAD 空体) - `mapCheckResult(row)` — 数据库行转 API CheckResult - `methodNotAllowedResponse(allow, mode)` — 构造 405 响应 - **`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 类型分离 - **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 的 `schema.ts` 和 `validate.ts`。 `ResolvedConfig` 包含以下字段: | 字段 | 来源 | 默认值 | | --------------------- | ----------------------------- | ----------- | | `configDir` | 配置文件所在目录 | — | | `dataDir` | `server.dataDir` | `./data` | | `host` | `server.host` | `127.0.0.1` | | `port` | `server.port` | `3000` | | `maxConcurrentChecks` | `runtime.maxConcurrentChecks` | `20` | | `retentionMs` | `runtime.retention` | `7d` | | `targets` | `targets[]` 经 resolve 后 | — | 契约层使用 `src/server/checker/schema/` 中的 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 是 `src/server/checker/runner//` 下的自包含目录,包含该 checker 所需的全部类型、schema、校验、执行逻辑和断言。新增一个 checker 只需创建一个目录并在 `runner/index.ts` 中添加一行注册。 以下以新增 `tcp` 类型 checker 为例,说明完整的开发步骤。 #### 1.7.1 架构总览 ``` checkerRegistry(单例) │ ├── runner/index.ts ← 显式数组注册,新增 checker 只需一行 │ ├── new HttpChecker() │ ├── new CommandChecker() │ └── new TcpChecker() ← 新增 │ ├── 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 目录的标准文件结构: | 文件 | 职责 | | ------------- | ------------------------------------------------------------------------------------- | | `index.ts` | 模块入口,re-export Checker 类 | | `types.ts` | Checker 专属类型(ResolvedXxxTarget、XxxTargetConfig、XxxExpectConfig 等) | | `schema.ts` | TypeBox 契约 schema(config / defaults / expect 三部分) | | `validate.ts` | 启动期语义校验(JSON Schema 无法表达的规则) | | `execute.ts` | Checker 类:resolve(默认值合并 + 解析)、execute(执行检查)、serialize(DB 持久化) | | `expect.ts` | Checker 专用断言函数 | | `*.ts` | 其他 checker 专属逻辑(如 http/body.ts、command/text.ts) | #### 1.7.2 步骤一:创建 Checker 目录与类型 在 `src/server/checker/runner/tcp/types.ts` 中定义 checker 专属类型(参考 `http/types.ts`、`command/types.ts`): - `XxxTargetConfig` — YAML 原始配置类型 - `XxxExpectConfig` — expect 字段类型 - `XxxDefaultsConfig` — defaults 专属字段类型 - `ResolvedXxxTarget extends ResolvedTargetBase` — resolve 后的完整类型,含 `type: "xxx"` 字面量 **注意**:不需要修改顶层 `checker/types.ts`。base interface 使用 index signature(`[key: string]: unknown`),checker 专属类型通过 `extends ResolvedTargetBase` 自动兼容。 #### 1.7.3 步骤二:创建 TypeBox 契约 Schema 在 `src/server/checker/runner/tcp/schema.ts` 中定义 `CheckerSchemas`(config / defaults / expect 三部分)。参考 `http/schema.ts`、`command/schema.ts`,使用 `schema/fragments.ts` 中的共享片段。 **可复用的共享 fragments**(来自 `schema/fragments.ts`): | Fragment | 用途 | | ---------------------------- | -------------------------------------------------------- | | `durationSchema` | 时长字符串(`"30s"`、`"5m"`、`"2h"`、`"7d"`、`"500ms"`) | | `sizeSchema` | 大小单位(字符串如 `"10MB"` 或整数) | | `statusCodePatternSchema` | 状态码(`100`-`599` 或 `"2xx"`) | | `stringMapSchema` | `Record`(用于 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 无法表达的语义规则(参考 `http/validate.ts`、`command/validate.ts`)。函数签名统一为: ```typescript export function validateTcpConfig(input: CheckerValidationInput): ConfigValidationIssue[]; ``` **共享校验工具**(`expect/validate-operator.ts`): | 函数 | 用途 | | --------------------------------------------------------- | ---------------------- | | `validateOperatorObject(ops, path, targetName, options?)` | 校验操作符对象 | | `isJsonValue(value)` | 判断是否为合法 JSON 值 | #### 1.7.5 步骤四:实现 Checker 类 在 `src/server/checker/runner/tcp/execute.ts` 中实现 `CheckerDefinition` 接口的全部成员(参考 `http/execute.ts`、`command/execute.ts`): ``` TcpChecker implements Checker readonly configKey ← "tcp"(对应 YAML 中的 target.tcp 字段) readonly type ← "tcp" readonly schemas ← tcpCheckerSchemas validate(input) ← 调用 validateTcpConfig(input) resolve(target, ctx)← 默认值合并 + 解析,返回 satisfies ResolvedTcpTarget execute(target, ctx)← 执行检查,返回 CheckResult serialize(target) ← 返回 { config, target } 用于 DB 持久化 ``` **`resolve()` 规范**: - 只做默认值合并、路径解析、单位转换,**不执行校验** - 返回 `satisfies ResolvedXxxTarget` 确保类型正确 - 通过 `context.defaults[this.configKey]` 访问 checker 专属默认值(需 `as` 断言为具体类型) **`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 **可用的共享断言工具**(`checker/expect/`): | 模块 | 函数 | 用途 | | ---------------------- | ----------------------------------------------------- | ------------------------------------- | | `failure.ts` | `errorFailure(phase, path, msg)` | 构造错误类型 failure | | `failure.ts` | `mismatchFailure(phase, path, expected, actual, msg)` | 构造不匹配类型 failure | | `failure.ts` | `truncateActual(value, maxLen?)` | 截断过长的 actual 值(默认 200 字符) | | `duration.ts` | `checkDuration(ms, maxMs?)` | 耗时断言 | | `operator.ts` | `applyOperator(actual, operator)` | 执行单个操作符比较 | | `operator.ts` | `evaluateJsonPath(json, path)` | JSONPath 提取 | | `validate-operator.ts` | `validateOperatorObject(ops, path, name)` | 操作符语义校验 | **Checker 专属断言**(如需要)放在同目录的 `expect.ts` 中,参考 `http/expect.ts`(checkStatus、checkHeaders)和 `command/expect.ts`(checkExitCode)。 #### 1.7.6 步骤五:创建模块入口并注册 创建 `src/server/checker/runner/tcp/index.ts`(re-export Checker 类)。 在 `src/server/checker/runner/index.ts` 中添加一行导入和一个数组元素(参考现有 HttpChecker/CommandChecker)。 注册后,以下管线会通过 registry 自动委托,**无需新增 type 分支**: | 模块 | 自动行为 | | -------------------- | ------------------------------------------------------------------------ | | `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` 或类型分支;开发者仍需按后续步骤更新前端展示常量、配置示例、文档和测试。 #### 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/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/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 — 配置示例 □ 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` 下。 **核心方法**: | 方法 | 用途 | | ---------------------- | ---------------------------------------------------------------- | | `syncTargets(targets)` | 启动期同步 targets(基于 name 做 upsert + delete 事务) | | `insertCheckResult()` | 写入单条检查结果 | | `getTargets()` | 查询全部 targets(default 分组优先排序) | | `getLatestChecksMap()` | 批量获取每个 target 的最新检查结果(单次 SQL 聚合) | | `getAllTargetStats()` | 批量获取每个 target 的可用率统计(GROUP BY 聚合) | | `getSummary()` | 获取总览统计(基于 `getLatestChecksMap` 内存计算 up/down/total) | | `getTrend()` | 获取按小时聚合的趋势数据 | | `getHistory()` | 分页查询历史记录 | | `getRecentSamples()` | 获取最近 N 条采样数据(用于状态条渲染) | | `prune(retentionMs)` | 按 retention 策略清理过期数据(由 engine 定时调用) | **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)、grp - `check_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`,默认 20),`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 映射 - **数据清理**:当 `retentionMs > 0` 时,engine 启动时立即执行一次 `store.prune()`,之后每小时定时执行,按 `timestamp` 清理过期数据 - **生命周期**:`start()`/`stop()` 管理定时器(含调度定时器和清理定时器),`stop()` 清理所有 `setInterval` ### 1.10 expect 断言系统 两层模型:**观测值收集** → **规则校验**。共享断言基础设施位于 `checker/expect/`,checker 专属断言位于各自目录。 **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 规则类型**(`runner/http/body.ts`): - `contains`:文本包含匹配 - `regex`:正则表达式匹配(注意:body 正则字段为 `regex`,不是 `match`) - `json`:JSONPath 提取 + 操作符比较(使用 `es-toolkit/isPlainObject` 区分纯值和操作符) - `css`:cheerio CSS 选择器 + 操作符比较 - `xpath`:XPath 节点提取 + 操作符比较 **文本规则**(`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 错误模式 - **API 错误**:`{ error: "描述", status: }`,状态码 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/` 镜像 `src/` 目录结构,但共享模块的测试集中放在 `tests/server/checker/runner/shared/` 下 - `tests/server/checker/runner/shared/failure.test.ts` ↔ `src/server/checker/expect/failure.ts` - `tests/server/checker/runner/shared/duration.test.ts` ↔ `src/server/checker/expect/duration.ts` - `tests/server/checker/runner/shared/operator.test.ts` ↔ `src/server/checker/expect/operator.ts` - `tests/server/checker/runner/shared/body.test.ts` ↔ `src/server/checker/runner/http/body.ts` - `tests/server/checker/runner/shared/text.test.ts` ↔ `src/server/checker/runner/command/text.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) + React Query Devtools | 服务端状态管理与自动轮询 | | 图表 | Recharts | 拨测趋势折线图与状态环状图 | | 路由 | 无(单页面 Dashboard) | 仅需 Drawer/Tab 做页面内导航 | **不引入的依赖**:React Router(单页面场景不需要)、状态管理库(TanStack Query 即服务端状态层,组件内用 `useState` 足够) ### 2.2 组件树与数据流 ``` main.tsx └── StrictMode └── ErrorBoundary(React 错误边界) └── 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(分页历史记录) └── ReactQueryDevtools(开发工具,仅开发环境) ``` **数据层架构**: ``` 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 规范 ```typescript 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 → 参数(粒度从粗到细) #### 查询配置规范 ```typescript // 全局面板级查询(需要持续刷新) useQuery({ queryKey: queryKeys.summary(), queryFn: () => fetchJson("/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 封装 ```typescript async function fetchJson(url: string): Promise { const response = await fetch(url); if (!response.ok) throw new Error(`HTTP ${response.status}`); return response.json() as Promise; } ``` - 统一使用 `fetch`(不引入 axios),与后端共享 Web API 生态 - 错误抛异常,由 TanStack Query 的 `error` 状态承接 #### QueryClient 全局配置 ```typescript 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 { ... }`) ```typescript 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` | 根组件,编排全局状态与布局 | | `ErrorBoundary` | `components/ErrorBoundary.tsx` | React 错误边界,捕获渲染异常并展示降级 UI | | `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"为例: 1. **确认数据需求**:是已有 API 数据还是需要新端点? - 如有新端点,先在 `src/server/routes/` 添加,参考 [1.3 新增路由步骤](#13-api-路由开发) - 如有新字段,更新 `src/shared/api.ts` 类型定义 2. **实现 hooks**:在 `src/web/hooks/useTargetDetail.ts` 中新增 `useQuery`(写好 `queryKey` 和 `enabled` 条件) 3. **编写组件**:在 `src/web/components/` 创建组件文件 - 在 `TargetDetailDrawer.tsx` 中新增 `` 引用 4. **编写常量**:如有列定义/排序器/筛选器,放在 `src/web/constants/` 5. **编写测试**:在 `tests/web/` 下添加对应的单元测试 ### 2.6 样式开发规范 前端基于 TDesign React 构建 UI,样式开发遵循以下优先级(从高到低): 1. **使用 TDesign 组件**:布局、间距、排版优先使用 TDesign 组件(如 Space、Divider、Typography) 2. **使用 TDesign 组件 props**:通过组件的 props 参数控制外观(如 `theme`、`variant`、`size`) 3. **使用 TDesign CSS tokens**:颜色、间距、字体等使用 `--td-*` CSS 变量(如 `--td-success-color`、`--td-comp-margin-xxl`) 4. **在 styles.css 中定义 CSS 类**:无法通过上述方式满足的样式需求,集中定义在 `styles.css` 中 5. **自行开发组件**:仅在 TDesign 无法满足需求时自行开发 **红线**: - **严禁在组件中使用 `style` 属性内联调整样式** - **严禁通过 CSS 覆盖 TDesign 组件内部类名**(如 `.t-tab-panel`),如需定制使用组件的 `className` prop - **严禁使用 `!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`)通过 `rowClassName` prop 应用 ### 2.7 前端测试规范 - 测试目录:`tests/web/`,结构对应 `src/web/` - 重点测试 **constants/** 中的纯函数(排序器、筛选器、颜色阈值、类型映射等) - 使用 `bun:test` 框架 --- ## 三、项目运行、集成与打包 ### 3.1 开发期运行 #### 同时启动前后端 ```bash 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` 环境变量可覆盖后端端口 #### 分别启动 ```bash # 启动后端(含 watch 模式自动重启) bun run dev:server probes.yaml # 另开终端启动前端 bun run dev:web ``` ### 3.2 前后端集成方式 #### 开发期代理 Vite 配置了开发代理(`vite.config.ts`)和代码分割策略: ```typescript // 开发代理 server: { proxy: { "/api": { target: `http://127.0.0.1:${backendPort}`, changeOrigin: true, }, }, } // 生产代码分割(rolldownOptions.output.codeSplitting.groups) // vendor-react: react/react-dom/scheduler // vendor-tdesign: tdesign // vendor-chart: recharts/d3-* ``` 前端访问 `/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(通过 `StaticAssets` 接口:`files: Record` + `indexHtml: Blob`),后端同时提供 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 构建打包 #### 构建命令 ```bash 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`) | #### 运行可执行文件 ```bash ./dist/dial-server probes.yaml ``` #### 清理 ```bash bun run clean # 清理 dist/ 构建产物、.build/ 缓存和 *.bun-build 临时文件 ``` ### 3.4 开发工作流 #### 日常开发循环 ```bash bun run dev probes.yaml # 启动开发环境 # 修改代码 → Vite HMR(前端)/ bun --watch(后端自动重启) bun run check # 提交前运行完整质量检查 ``` #### 完整验证流程 ```bash bun run verify # = bun run check + bun run build + bun run test:smoke ``` `verify` 适合 CI 或正式提交前,会完整验证类型检查、lint、格式、单元测试、构建、smoke test。 ### 3.5 Smoke Test ```bash bun run test:smoke ``` `scripts/smoke.ts` 构建后验证流程: 1. 动态分配空闲端口 2. 用临时配置文件启动 `dist/dial-server` 3. 等待健康检查通过 4. 验证所有 API 端点返回正确数据 5. 验证静态资源服务(含 SPA fallback 和 404 处理) 6. 验证安全 headers 7. 测试结束清理临时目录和进程 ### 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 的检查) | | `commitlint.config.js` | commitlint 提交信息格式校验 | | `.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 自动化。 ```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 # 一键运行 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 访问必须运行时真值检查 | | `noImplicitOverride` | true | 子类覆盖父类方法时必须显式使用 `override` 关键字 | | `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,无需手动配置。 ## 测试 ```bash bun run check # 日常开发(类型检查 + lint(含格式) + 单元测试) bun run verify # 完整验证(check + 构建 + smoke test) ``` ## 已知限制 当前不做告警通知、拨测目标动态增删、认证鉴权和分布式部署。Command 类型拨测不支持 Windows 环境。