# DiAL 开发文档 本文档面向 DiAL 项目的开发者,介绍项目结构、构建流程、测试、代码规范等内容。 用户使用说明请参阅 [README.md](README.md)。 ## 目录 - [项目结构](#项目结构) - [一、后端开发指引](#一后端开发指引) - [二、前端开发指引](#二前端开发指引) - [三、项目运行、集成与打包](#三项目运行集成与打包) - [代码质量](#代码质量) - [已知限制](#已知限制) --- ## 项目结构 ```text src/ server/ bootstrap.ts 后端统一启动引导(loadConfig → store → engine → startServer → shutdown) config.ts CLI 参数解析(仅提取配置文件路径) dev.ts 开发模式启动入口(mode: "development",仅 API server) main.ts 生产模式启动入口(mode: "production",安全头启用) server.ts HTTP server 启动工厂(Bun.serve routes 声明式路由 + fetch fallback 静态资源服务) helpers.ts 共享响应格式化工具(见下方函数清单) middleware.ts API 参数校验中间件(validateTargetId、validateTimeRange、validatePagination) routes/ API 路由 handler(按端点拆分) health.ts GET /health(无 store 参数) meta.ts GET /api/meta dashboard.ts GET /api/dashboard metrics.ts GET /api/targets/:id/metrics history.ts GET /api/targets/:id/history checker/ types.ts 基础类型定义(ResolvedTargetBase、RawTargetConfig、DefaultsConfig、CheckResult 等基础 interface) config-loader.ts YAML 配置解析、契约校验、语义校验与运行期解析(输出 ResolvedConfig) variables.ts 配置 variables 提取、target 字符串变量替换和 unresolved-variable issue 生成 schema/ TypeBox + Ajv 配置契约、schema fragments、issue 渲染和 schema 导出入口 builder.ts 全量 JSON Schema 组装(遍历 registry 生成) fragments.ts 共享 TypeBox schema 片段(duration、size、ValueMatcher、ContentExpectations、KeyedExpectations 等) 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 Raw/Resolved ValueExpectation、ContentExpectations、KeyedExpectations、ExpectationResult 类型 failure.ts 失败信息构造(errorFailure、mismatchFailure、truncateActual) value.ts ValueExpectation resolve(primitive→equals)和执行、JSONPath 提取 content.ts Resolved ContentExpectations 执行(kind=value/json/css/xpath)和 Raw resolve keyed.ts Resolved KeyedExpectations 执行(顺序 + key 规范化)和 Raw resolve headers.ts HTTP/LLM 共享 header keyed expectation 包装(大小写不敏感) status.ts HTTP/LLM 共享 status code 断言(精确数值与 1xx-5xx 范围) validate.ts Raw value/content/keyed expectation 语义校验(不修改输入) redos.ts regex ReDoS 风险检测 runner/ Checker 统一抽象与注册机制 types.ts CheckerDefinition、CheckerContext、CheckerSchemas、ResolveContext registry.ts CheckerRegistry 注册中心 index.ts 注册入口(显式数组 + 循环注册) http/ HTTP Checker(自包含模块,含 types/schema/execute/expect/validate) cmd/ Cmd Checker(自包含模块,含 types/schema/execute/expect/validate) db/ DB Checker(自包含模块,含 types/schema/execute/expect/validate) tcp/ TCP Checker(自包含模块,含 types/schema/execute/expect/validate) icmp/ ICMP Checker(自包含模块,含 types/schema/execute/expect/validate/parse) udp/ UDP Checker(自包含模块,含 types/schema/execute/expect/validate/encoding) llm/ LLM Checker(自包含模块,含 types/schema/execute/expect/validate/provider/observation) shared/ api.ts 前后端共享 TypeScript 类型 web/ React 前端 Dashboard(通过 Bun HTML import 集成) app.tsx 根组件(编排全局状态与布局) main.tsx 入口(QueryClient 挂载 + ErrorBoundary + ReactQueryDevtools) styles.css 全局样式与自定义 CSS 变量 components/ UI 组件(见下方组件清单) constants/ 常量与纯函数 history-table-columns.tsx 历史记录表格列定义 target-table-columns.tsx 目标表格列定义工厂 target-table-filters.ts 表格筛选器 target-table-sorters.ts 表格排序器 color-threshold.ts 可用率颜色阈值函数 hooks/ React hooks(数据查询、Drawer 状态、浏览器 UI 偏好) use-queries.ts 全局面板查询 hook(dashboard/meta/metrics) use-target-detail.ts 目标详情 Drawer 状态与条件查询 hook use-theme-preference.ts 主题模式偏好、本地存储和 TDesign theme-mode 应用 hook utils/ 前端工具函数 time.ts 时间处理(subtractHours、相对时间、动态时长单位) scripts/ 构建、schema 生成和清理脚本 tests/ Bun test 测试(结构镜像 src 目录) openspec/ OpenSpec 变更与规格文档 probe-config.schema.json 用户配置 JSON Schema 导出物(用于 IDE 自动补全和校验) ``` > **说明**:`runner/http/` 和 `runner/cmd/` 的完整文件结构见 [1.7.1 架构总览](#171-架构总览) 中的标准文件表。 ## 前后端边界 前端只通过 HTTP 调用后端,API 路径为 `/api/*`。共享类型放在 `src/shared`,前端不得 import `src/server` 的运行时实现。 --- ## 一、后端开发指引 ### 1.1 架构概览 ``` 启动流程: dev.ts / main.ts → readRuntimeConfig(cli args, 仅提取 configPath) → bootstrap({ configPath, mode }) → loadConfig(yaml:YAML 解析 → 变量替换 → 契约校验 → 语义校验 → resolve) → ResolvedConfig{ host, port, dataDir, maxConcurrentChecks, retentionMs, targets } → ProbeStore(db) → store.syncTargets(targets) → ProbeEngine(store, targets, maxConcurrentChecks, retentionMs) → engine.start() → startServer({ config, mode, store }) → 注册 SIGINT/SIGTERM shutdown(engine.stop + store.close) 运行时: 定时器(tick) → ProbeEngine.probeGroup() → checkerRegistry.get(target.type).execute() → runner/*/expect.ts 校验 → engine.writeResult() → store.insertCheckResult() 数据清理: 定时 prune(retentionMs),每小时执行一次 HTTP 请求: Request → Bun.serve routes 声明式匹配 → routes/*.ts(handler) → middleware.ts(参数校验) → helpers.ts(响应格式化) → Response 前端: fetch fallback → serveStaticAsset (生产) / Vite proxy (开发) ``` ### 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/`,每个端点一个文件。路由通过 `server.ts` 的 `Bun.serve({ routes })` 声明式注册,使用 per-method handler 对象: ```typescript // server.ts 中的路由注册 routes: { "/*": homepage, // HTML import,SPA fallback "/api/*": () => jsonResponse(createApiError("API route not found", 404), { mode, status: 404 }), "/api/meta": { GET: () => handleMeta(mode) }, "/api/dashboard": { GET: (req) => handleDashboard(new URL(req.url), store, mode) }, "/api/targets/:id/history": { GET: (req) => handleHistory(req.params.id, new URL(req.url), store, mode) }, "/api/targets/:id/metrics": { GET: (req) => handleMetrics(req.params.id, new URL(req.url), store, mode) }, "/health": { GET: () => handleHealth(mode) }, } ``` Handler 函数签名因端点而异: ```typescript // 无 store 的路由 export function handleHealth(mode: RuntimeMode): Response; export function handleMeta(mode: RuntimeMode): Response; // 带 target ID 和查询参数的路由 export function handleDashboard(url: URL, store: ProbeStore, mode: RuntimeMode): Response; export function handleHistory(idStr: string, url: URL, store: ProbeStore, mode: RuntimeMode): Response; export function handleMetrics(idStr: string, url: URL, store: ProbeStore, mode: RuntimeMode): Response; ``` **请求处理流程**: 1. `Bun.serve` 的 `routes` 对象按路径 + HTTP 方法匹配请求 2. 未匹配方法的请求落入 `/api/*` 通配符(返回 404) 3. 各 handler 内部通过 `middleware.ts` 提供的 `validateTargetId`、`validateTimeRange`、`validatePagination`、`validateDashboardWindow`、`validateRecentLimit`、`validateMetricsBucket` 做参数校验,`pageSize` 最大值为 `200` 4. 校验函数返回 `Response` 实例表示校验失败(直接返回),返回数据对象表示通过 5. 业务逻辑通过 `store` 查询数据,用 `helpers.ts` 的 `jsonResponse`、`mapCheckResult`、`formatDuration` 等格式化输出 **新增路由步骤**: 1. 在 `src/server/routes/` 下创建 `.ts` 2. 实现 handler 函数并 export 3. 在 `server.ts` 的 `routes` 对象中注册路径和 method handler 4. 在 `tests/server/app.test.ts` 中添加对应测试 ### 1.4 共享工具 - **`helpers.ts`**:跨路由共用的响应工具函数 - `createApiError(error, status)` — 构造 API 错误体 - `createHeaders(mode, init)` — 创建响应 Headers(生产模式附加安全头) - `createHealthResponse()` — 构造健康检查响应 - `formatDuration(ms)` — 毫秒转为可读时长字符串 - `jsonResponse(body, options)` — JSON 响应构造 - `mapCheckResult(row, type)` — 数据库行转 API CheckResult,反序列化 observation 并按 checker type 动态生成 detail - **`middleware.ts`**:API 参数校验函数(`validateTargetId`、`validateTimeRange`、`validatePagination`、`validateDashboardWindow`、`validateRecentLimit`、`validateMetricsBucket`,其中 `pageSize` 和 `recentLimit` 上限为 `200`) ### 1.5 类型定义规范 - **共享类型**以 `src/shared/api.ts` 为唯一源头,前后端共同引用 - 前端不得 `import src/server/` 下的任何文件 - **严格联合类型**优先于宽类型:如 `phase: "status" | "duration" | ...` 而非 `phase: string` - **后端内部扩展**:`checker/types.ts` 中 `CheckResult` 通过 `extends` 共享版本的 `ApiCheckResult` 增加 `targetId` 等内部字段 - 存储层类型(`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 类型 - `CheckerDefinition` 使用泛型约束 `resolve` 返回值以及 `execute`、`serialize` 的 target 参数 - checker 实现指定具体 `ResolvedXxxTarget` 类型,中间层(registry、engine、config-loader、store)使用默认泛型参数完成类型擦除 - Checker 内部 `execute` 和 `serialize` 直接接收具体类型;`resolve` 输入仍是 `RawTargetConfig`,可在读取 checker 专属原始配置时做必要窄化 ### 1.6 配置契约与校验 配置加载流程固定为:`unknown -> 变量替换 -> RawProbeConfig -> ValidatedProbeConfig -> ResolvedConfig`。 变量替换阶段由 `variables.ts` 负责,在 YAML 解析之后、AJV 契约校验之前执行。顶层 `variables` 支持 string/number/boolean 字面量,target 字符串字段支持 `${key}`、`${key|default}` 和 `$${key}`,解析优先级为 `variables -> process.env -> 默认值`;替换范围仅限 `targets`,且跳过 `id` 和 `type` 字段。 `config-loader.ts` 只负责 YAML 解析、契约调度、公共语义校验和最终运行期解析;checker 专属规则必须下沉到对应 checker 的 `schema.ts` 和 `validate.ts`。 `ResolvedConfig` 包含以下字段: | 字段 | 来源 | 默认值 | | --------------------- | -------------------------------------------------- | ---------------- | | `configDir` | 配置文件所在目录 | — | | `dataDir` | `server.dataDir`(基于配置文件目录解析为绝对路径) | `configDir/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`。只有明确声明的动态键值表可以开放任意键名,例如 `variables`、`http.headers`、`defaults.http.headers`、`expect.headers`、`cmd.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() │ └── new IcmpChecker() ← 新增 │ ├── 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 专属类型(RawXxxTargetConfig、Raw/Resolved XxxExpectConfig、ResolvedXxxTarget 等) | | `schema.ts` | TypeBox 契约 schema(config / defaults / expect 三部分) | | `validate.ts` | 启动期语义校验(JSON Schema 无法表达的规则) | | `execute.ts` | Checker 类:resolve(默认值合并 + 解析)、execute(执行检查)、serialize(DB 持久化) | | `expect.ts` | Checker 专用断言函数 | | `*.ts` | 其他 checker 专属逻辑(如协议解析、编码、provider 适配、平台命令封装) | #### 1.7.2 步骤一:创建 Checker 目录与类型 在 `src/server/checker/runner/tcp/types.ts` 中定义 checker 专属类型(参考 `http/types.ts`、`cmd/types.ts`): - `RawXxxTargetConfig` — YAML 原始配置类型 - `RawXxxExpectConfig` / `ResolvedXxxExpectConfig` — Raw expect 字段类型与运行期 Resolved 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`、`cmd/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) | | `createValueMatcherSchema()` | `ValueMatcher` 对象(equals/contains/regex/数值比较等) | | `createContentExpectationsSchema()` | `ContentExpectations` 数组(value/json/css/xpath 内容断言) | | `createKeyedExpectationsSchema()` | 动态键 `KeyedExpectations`(headers、DB rows 列值) | | `matcherProperties()` | matcher 字段 Record,供 extractor schema 复用 | **注意**:默认对象策略为 `additionalProperties: false`。只有明确的动态键值表(如 `http.headers`、`cmd.env`)可以开放任意键名。 #### 1.7.4 步骤三:实现语义校验 在 `src/server/checker/runner/tcp/validate.ts` 中实现 JSON Schema 无法表达的语义规则(参考 `http/validate.ts`、`cmd/validate.ts`)。函数签名统一为: ```typescript export function validateTcpConfig(input: CheckerValidationInput): ConfigValidationIssue[]; ``` **共享校验工具**(`expect/validate.ts`): | 函数 | 用途 | | -------------------------------------------------------------- | --------------------------------------------------- | | `validateRawValueExpectation(value, path, targetName, opts?)` | 校验 Raw `ValueExpectation`(primitive 或 matcher) | | `validateRawContentExpectations(value, path, targetName)` | 校验 Raw `ContentExpectations` 数组、extractor 互斥 | | `validateRawKeyedExpectations(value, path, targetName, opts?)` | 校验 Raw `KeyedExpectations`,可选大小写不敏感重复 | | `validateJsonPath(path, rulePath, targetName)` | 校验项目支持的 JSONPath 子集 | | `isJsonValue(value)` | 判断是否为合法 JSON value | #### 1.7.5 步骤四:实现 Checker 类 在 `src/server/checker/runner/tcp/execute.ts` 中实现 `CheckerDefinition` 接口的全部成员(参考 `http/execute.ts`、`cmd/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()` 规范**: - 只做默认值合并、路径解析、单位转换,**不执行校验** - 若 checker 支持 expect,必须保留 `rawExpect` 为变量替换后的 Raw 配置快照,并生成只供 `execute()` 使用的 Resolved `expect` - 返回 `satisfies ResolvedXxxTarget` 确保类型正确 - 通过 `context.defaults[this.configKey]` 访问 checker 专属默认值(需 `as` 断言为具体类型) **expect 五层管线**:每种断言模型从定义到执行经过五个阶段,每层使用对应的共享函数: | 断言模型 | 类型层(Raw) | Schema 层 | Validate 层 | Resolve 层 | Execute 层 | | --------------------- | ----------------------------------- | ----------------------------------- | ---------------------------------- | ------------------------------ | ---------------------------- | | `ValueExpectation` | `number \| ValueMatcher` | `createValueMatcherSchema()` | `validateRawValueExpectation()` | `resolveValueExpectation()` | `checkValueExpectation()` | | `ContentExpectations` | `(ValueMatcher \| ExtractorRule)[]` | `createContentExpectationsSchema()` | `validateRawContentExpectations()` | `resolveContentExpectations()` | `checkContentExpectations()` | | `KeyedExpectations` | `Record` | `createKeyedExpectationsSchema()` | `validateRawKeyedExpectations()` | `resolveKeyedExpectations()` | `checkKeyedExpectations()` | 选择哪种模型参考 [1.10 expect 字段选择规范](#110-expect-断言系统)的决策树。 **resolve 中的标准模式**: ```typescript // resolve() 内:逐字段调用对应的 resolve 函数,未配置的字段保持 undefined const rawExpect = raw.expect ?? {}; expect: { durationMs: rawExpect.durationMs != null ? resolveValueExpectation(rawExpect.durationMs) : undefined, body: rawExpect.body != null ? resolveContentExpectations(rawExpect.body) : undefined, headers: rawExpect.headers != null ? resolveKeyedExpectations(rawExpect.headers) : undefined, } ``` **execute 中的标准模式**: ```typescript // execute() 内:按快速失败顺序依次检查,首个失败即返回 const r = resolved.expect; if (r.durationMs) { const result = checkValueExpectation(elapsed, r.durationMs, { phase: "duration", path: "durationMs" }); if (!result.matched) return { ..., failure: result.failure, matched: false }; } if (r.body) { const result = checkContentExpectations(bodyText, r.body, { phase: "body", path: "body" }); if (!result.matched) return { ..., failure: result.failure, matched: false }; } ``` **`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 - `mismatchFailure` 的 `expected` 参数应传用户可读值,使用 `displayValueExpectation(matcher)` 解包单字段 `{ equals: x }` 为 `x` **可用的共享断言工具**(`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 字符) | | `value.ts` | `applyValueMatcher(actual, matcher, options?)` | 执行 Resolved `ValueMatcher` AND 匹配 | | `value.ts` | `checkValueExpectation(actual, matcher, options)` | 执行 matcher 并返回 `ExpectationResult` | | `value.ts` | `resolveValueExpectation(raw)` | Raw `ValueExpectation` → Resolved `ValueExpectation` | | `value.ts` | `displayValueExpectation(matcher)` | 解包单字段 `{ equals: x }` 为 `x`,用于 failure 展示 | | `value.ts` | `evaluateJsonPath(json, path)` | JSONPath 提取 | | `content.ts` | `checkContentExpectations(source, expectations, options)` | 执行 Resolved `ContentExpectations` | | `content.ts` | `resolveContentExpectations(raw)` | Raw → Resolved `ContentExpectations` | | `keyed.ts` | `checkKeyedExpectations(actual, expectations, options)` | 执行 Resolved `KeyedExpectations` | | `keyed.ts` | `resolveKeyedExpectations(raw)` | Raw Record → Resolved 有序数组 | | `headers.ts` | `checkHeaderExpectations(headers, expectations, options?)` | HTTP/LLM headers 大小写不敏感包装 | | `status.ts` | `checkStatusCode(actual, expected, phase, path)` | HTTP/LLM status code(精确数值与 1xx-5xx 范围) | | `validate.ts` | `validateRawValueExpectation/ContentExpectations/KeyedExpectations` | Raw expectation 语义校验(不修改输入) | **Checker 专属断言**(如需要)放在同目录的 `expect.ts` 中,参考 `cmd/expect.ts`(checkExitCode)、`tcp/expect.ts`(checkConnected)、`udp/expect.ts`(checkResponded)和 `icmp/expect.ts`(checkAlive)。HTTP/LLM 复用的 status 与 headers 断言放在共享 expect 模块。 #### 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 步骤六:确认前端类型展示 前端通过 `/api/meta` 获取 `checkerRegistry.supportedTypes` 并动态生成类型筛选器,类型列和详情标题直接显示 `target.type` 原始文本。新增 checker 注册后无需更新前端类型映射或筛选常量。 #### 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 — 注册(一行导入 + 一个数组元素) □ tests/ — 契约 + 校验 + resolve + execute + 注册 测试 □ probes.example.yaml — 配置示例 □ bun run schema + bun run schema:check — Schema 导出同步 □ bun run check — 全量质量检查通过 □ bun run verify — 完整验证(check + build) □ README.md — 用户文档 □ DEVELOPMENT.md — 项目结构目录树 ``` ### 1.8 数据存储规范 基于 `bun:sqlite`,WAL 模式运行,数据库文件位于配置的 `dataDir` 下。 **核心方法**: | 方法 | 用途 | | ------------------------------------------ | ----------------------------------------------------------- | | `syncTargets(targets)` | 启动期同步 targets(基于配置 `id` 做 upsert + delete 事务) | | `insertCheckResult()` | 写入单条检查结果 | | `getTargets()` | 查询全部 targets(default 分组优先排序) | | `getLatestChecksMap()` | 批量获取每个 target 的最新检查结果(单次 SQL 聚合) | | `getAllTargetWindowStats(from, to)` | 批量获取窗口内每个 target 的 total/up/down 基础计数 | | `getDashboardIncidentStates(from, to)` | 获取 Dashboard 窗口内状态序列,供应用层计算 incidents | | `getAllRecentSamples(limit)` | 批量获取每个 target 的最近 N 条采样(用于状态条和连续状态) | | `getTargetCheckpoints(targetId, from, to)` | 获取单目标窗口内检查点序列,供 metrics 应用层分桶和故障分析 | | `getTargetDurations(targetId, from, to)` | 获取单目标窗口内成功检查耗时升序数组,供应用层计算 P95/P99 | | `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)+ 内存组装 - 新增批量查询方法时必须编写对应单元测试 - `GET /api/dashboard` 的响应组装通过 `getLatestChecksMap` + `getAllTargetWindowStats` + `getAllRecentSamples` + `getDashboardIncidentStates` 实现批量查询 **轻数据库指标计算规范**: - 数据库只负责存储、筛选、排序、分页、LIMIT 和标准 SQL 基础聚合(如 `COUNT`、`SUM(CASE)`、`AVG`、`MIN`、`MAX`、`GROUP BY`),用于减少应用层输入数据量 - 指标语义必须在后端应用层实现,包括可用率舍入、百分位、状态翻转、故障段识别、MTTR、最长故障、连续状态、趋势 UTC 小时分桶和窗口边界处理 - 禁止用 SQLite 专有时间函数承载趋势分桶语义,禁止用复杂 SQL/window function 承载故障事件或恢复时长等业务规则 **Schema**: - `targets` 表:id(TEXT PRIMARY KEY,配置 target id)、name(TEXT,可 NULL,展示名称)、description(TEXT,可 NULL,描述)、type、target(展示摘要)、config(JSON)、interval_ms、timeout_ms、expect(JSON)、grp - `check_results` 表:target_id(TEXT FK CASCADE,引用配置 target id)、timestamp、matched(0/1)、duration_ms、observation(JSON TEXT)、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()`,Cmd 和 ICMP 在 signal abort 时 `proc.kill()` - **结果写入**:检查结果通过 `store.insertCheckResult()` 写入 SQLite,engine 基于配置 target id 确认目标仍存在;detail 为 API 层从 observation 派生,不进入存储层 - **异常可观测**:`probeGroup()` 对 `Promise.allSettled` 的 rejected 结果通过索引关联 target,并写入 `phase:"internal"` 的失败记录 - **数据清理**:当 `retentionMs > 0` 时,engine 启动时立即执行一次 `store.prune()`,之后每小时定时执行,按 `timestamp` 清理过期数据 - **生命周期**:`start()`/`stop()` 管理定时器(含调度定时器和清理定时器),`stop()` 清理所有 `setInterval` ### 1.10 expect 断言系统 两层模型:**观测值收集** → **规则校验**。共享断言基础设施位于 `checker/expect/`,checker 专属状态断言位于各自目录。 **Raw vs Resolved**:用户 YAML 写的是 Raw 形态(primitive 简写、`{ json: { path, equals } }` 提取器对象、`Record` 键值表),`config-loader` 的 resolve 阶段将其转换为 Resolved 形态供运行期执行(`{ equals: primitive }`、`{ kind, matcher, ... }` content 联合、`{ key, matcher }[]` 有序数组)。Store 持久化 Raw 快照(`rawExpect`),checker.execute 消费 Resolved `expect`。 **共享模型**: | 模型 | 用途 | 典型字段 | | --------------------- | ---------------------------------------------- | -------------------------------------------------------------------- | | `ValueExpectation` | 单个值、数字指标和字符串元数据断言 | `durationMs`、`rowCount`、`usage.totalTokens`、`finishReason` | | `ContentExpectations` | 返回内容或半结构化内容断言,必须是数组 | `body`、`stdout`、`stderr`、`banner`、`response`、`output`、`result` | | `KeyedExpectations` | 动态键值断言,字面量等价于 `{ equals: value }` | `headers`、DB `rows[]` 中的列值 | `ValueMatcher` 支持 `equals`、`contains`、`regex`、`empty`、`exists`、`gte`、`lte`、`gt`、`lt`。一个 matcher 对象内多个字段为 AND 语义;`exists: false` 不能和其他 matcher 组合;`equals` 使用 `es-toolkit/isEqual` 做 JSON 深度相等;`regex` 固定为无 flags 的 `new RegExp(pattern).test(String(actual))`。ValueExpectation Raw 输入可使用 string、number、boolean 或 null 简写,resolve 阶段归一化为 `{ equals: value }`;数组和对象简写不支持,必须显式写成 `{ equals: ... }`。 `ContentExpectations` 数组按顺序快速失败。数组项可以是直接 matcher,也可以是 `{ json: {...} }`、`{ css: {...} }`、`{ xpath: {...} }` 提取器规则;一条规则不能混用直接 matcher 和 extractor,多个 extractor 也不能共存。Extractor 未配置 matcher 时等价于 `exists: true`。对对象或数组源执行直接 `contains`/`regex` 时会先 JSON 序列化,`equals` 仍对原始结构做深度相等。 启动期语义校验统一由 `expect/validate.ts` 负责,会校验空 matcher、未知字段、字段类型、`exists:false` 组合、ContentExpectations 互斥性、JSONPath 子集、XPath 可编译性、regex 可编译性、ReDoS 风险以及 HTTP/LLM headers 大小写归一化后重复 key。语义校验不修改 Raw 输入。旧字段 `match`、`maxDurationMs`、ICMP 的 `max*` 阈值字段不再支持。 **快速失败顺序**: | Checker | 顺序 | | ---------- | -------------------------------------------------------------------------------------------------------------------------- | | HTTP | `status → headers → body → durationMs` | | Cmd | `exitCode → durationMs → stdout → stderr` | | DB | `durationMs → rowCount → rows → result` | | TCP | `connected → banner → durationMs` | | UDP | `responded → responseSize → response → sourceHost → sourcePort → durationMs` | | ICMP | `alive → packetLossPercent → avgLatencyMs → maxLatencyMs → durationMs` | | LLM http | `status → headers → output → finishReason → rawFinishReason → usage → durationMs` | | LLM stream | `status → headers → stream.completed → stream.firstTokenMs → output → finishReason → rawFinishReason → usage → durationMs` | HTTP checker 的 `durationMs` 覆盖完整执行(含重定向、按需响应体读取、解码和 expect 校验)。未配置 body expectation、status 失败或 headers 失败时不读取 body;有 body expectation 时,在读取 body 前可先检查 `durationMs` 上界 matcher 是否已不可能通过,避免无意义读取。 **expect 字段选择规范**: 新增或修改 checker 的 expect 字段时,按以下决策树选择合适的断言模型(选定后,各层的具体函数映射参考 [1.7.5 的五层管线表](#175-步骤四实现-checker-类)): ``` expect 字段 │ ├─ 状态类结果,结果集合小且稳定 │ └─ enum / boolean │ HTTP/LLM status、Cmd exitCode、TCP connected、 │ UDP responded、ICMP alive │ ├─ 数字指标 / 字符串元数据 │ └─ ValueMatcher │ durationMs、rowCount、responseSize、sourceHost、sourcePort、 │ packetLossPercent、avgLatencyMs、maxLatencyMs、 │ finishReason、rawFinishReason、usage.*、stream.firstTokenMs │ └─ 返回内容 / 半结构化内容 / 不完全确定的值 ├─ 内容断言 → ContentExpectations(数组) │ HTTP body、Cmd stdout/stderr、TCP banner、 │ UDP response、LLM output、DB result │ └─ 键值断言 → KeyedExpectations(动态键对象) HTTP/LLM headers、DB rows[] 中的列值 ``` 选择原则: 1. **状态类字段使用 enum 或 boolean**。结果集合小且稳定时(如 HTTP status 200/2xx、exitCode 0),枚举和布尔比 matcher 更贴近协议语义,配置也更直观。不要为了统一而把状态类字段改成 ValueMatcher。 2. **单值数字指标和字符串元数据使用 ValueMatcher**。观测值是一个明确的标量(耗时、行数、丢包率、finish reason),但阈值不确定时,使用 `{ lte: 100 }` 或 `{ regex: "^(stop|end)$" }` 等 matcher 表达;精确匹配 primitive 可直接写 `100` 或 `"stop"`。 3. **返回内容使用 ContentExpectations 数组**。观测值是文本、JSON、HTML 或 XML 内容,且可能需要多步提取或多条规则时,使用 ContentExpectations。即使只有一条规则也必须写成数组形式(`[{ contains: "ok" }]`),不支持对象快捷写法。 4. **键值对使用 KeyedExpectations**。观测值是动态键值表(如 headers),且需要对每个键独立断言时使用。字面量值自动等价于 `{ equals: value }`。 5. **不要混用模型**。一个 expect 字段只能对应一种断言模型。例如 `finishReason` 是单值字符串元数据,用 ValueMatcher 而非 ContentExpectations(ContentExpectations 的 json/css/xpath 提取器对单字符串无意义,且会增加数组包装的配置冗余)。 6. **failure phase 命名遵循去单位后缀规则**。数字指标字段的 phase 去掉单位后缀(`durationMs` → `duration`、`packetLossPercent` → `packetLoss`、`avgLatencyMs` → `avgLatency`),不带单位后缀的字段直接使用字段名(`rowCount` → `rowCount`、`finishReason` → `finishReason`)。 7. **实现时参考 [1.7.5 五层管线](#175-步骤四实现-checker-类) 中的对应表**。决策树解决"选哪种模型",五层管线表解决"每种模型从类型定义到执行分别调哪个函数"。 ### 1.11 错误模式 - **API 错误**:`{ error: "描述", status: }`,状态码 400/404/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/` 目录结构,但共享 expect 模块的测试集中放在 `tests/server/checker/runner/shared/` 下,覆盖 `failure.ts`、`value.ts`(operator)、`content.ts`(body/text)、`keyed.ts`(headers/duplicate-key)、`validate.ts`(shorthand)和 `redos.ts` - 使用 `bun:test` 框架(`describe`/`test`/`expect`),测试数据库用临时目录 + `tmpdir()` - 新增 store 方法必须编写单元测试;新增 API 端点必须在 `app.test.ts` 中添加集成测试 - 测试后清理:`afterAll` 中 `store.close()` + `rm(tempDir, { recursive: true })` --- ## 二、前端开发指引 ### 2.1 技术栈概览 | 层面 | 技术 | 用途 | | ------ | --------------------------------------------------- | ---------------------------- | | 框架 | React 19 | UI 组件开发 | | 构建 | Bun HTML import(fullstack 模式) | 开发服务与生产构建 | | 语言 | TypeScript 6 | 类型安全 | | UI 库 | TDesign React + tdesign-icons-react | UI 组件与图标 | | 数据层 | TanStack Query (React Query) + React Query Devtools | 服务端状态管理与自动轮询 | | 图表 | Recharts | 拨测趋势折线图 | | 动画 | @number-flow/react | 倒计时数字滚动过渡 | | 路由 | 无(单页面 Dashboard) | 仅需 Drawer/Tab 做页面内导航 | **不引入的依赖**:React Router(单页面场景不需要)、状态管理库(TanStack Query 即服务端状态层,组件内用 `useState` 足够)、Vite(已由 Bun 原生 fullstack 替代) ### 2.2 组件树与数据流 ``` main.tsx └── StrictMode └── ErrorBoundary(React 错误边界) └── QueryClientProvider(TanStack Query 全局挂载) ├── App(根组件,Layout + HeadMenu 骨架) │ ├── useThemePreference() ─── Header 主题模式 RadioGroup(系统/明亮/黑暗,本地存储记忆 + theme-mode 应用) │ ├── useDashboard(refreshInterval) ─── GET /api/dashboard?window=24h&recentLimit=30(动态刷新间隔,RadioGroup 频率选择 + 倒计时/手动刷新按钮) │ ├── SummaryCards(单 Card 内嵌居中 Statistic,无 shadow) │ └── TargetBoard(目标列表,Space 24px 间距) │ ├── DashboardResponse.targets │ ├── useMeta() ───── GET /api/meta(应用生命周期内缓存) │ └── TargetGroup[](Card 包裹 PrimaryTable,headerBordered) │ └── PrimaryTable ← createTargetTableColumns(checkerTypes) │ └── TargetDetailDrawer(目标详情抽屉,响应式默认宽度、支持鼠标拖拽调整,TDesign 生命周期控制) │ └── useTargetDetail() ── 按需发起 metrics 查询,history 延迟到记录 Tab 激活后请求 │ ├── activeTab 受控 Tabs 状态,每次打开重置为 overview │ ├── OverviewTab → Descriptions(直接展示)+ 4×2 统计卡片 + TrendChart │ └── HistoryTab → PrimaryTable(分页历史记录,TabPanel 懒渲染 + destroyOnHide=false) └── ReactQueryDevtools(开发工具,仅开发环境) ``` **Hook 架构**: ``` hooks/use-queries.ts(全局面板级查询) ├── queryKeys(dashboard/meta/metrics 结构化 query key) ├── useDashboard(refetchInterval) → /api/dashboard?window=24h&recentLimit=30(动态刷新间隔,由调用方传入) ├── useTargetMetrics() → /api/targets/:id/metrics(详情按需加载) └── useMeta() → /api/meta(staleTime: Infinity) hooks/use-target-detail.ts(Drawer 状态与详情级条件查询) ├── 内部复用 useDashboard(false) 的缓存来查找 selectedTarget ├── activeTab 受控 Tabs 状态(每次 openDrawer 重置为 overview) ├── useTargetMetrics(/api/targets/:id/metrics)(条件查询:enabled 仅当 Drawer 打开且时间范围有效) └── useQuery(/api/targets/:id/history)(条件查询:enabled 仅当 Drawer 打开 + 时间范围有效 + activeTab=history) hooks/use-theme-preference.ts(浏览器 UI 偏好) ├── ThemePreference: system / light / dark(RadioGroup 受控值) ├── EffectiveTheme: light / dark(写入 document.documentElement theme-mode) ├── localStorage key: dial.theme.preference(同一浏览器记忆) └── matchMedia("(prefers-color-scheme: dark)")(系统模式下跟随系统明暗变化) ``` ### 2.3 TanStack Query 数据层 #### Query Key 规范 ```typescript const queryKeys = { dashboard: () => ["dashboard", "24h", 30] as const, meta: () => ["meta"] as const, metrics: (targetId: number, from: string, to: string, bucket: "1h") => ["metrics", targetId, from, to, bucket] 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.dashboard(), queryFn: () => fetchJson("/api/dashboard?window=24h&recentLimit=30"), refetchInterval, // 由调用方传入的动态刷新间隔(false 禁用轮询) refetchIntervalInBackground: false, // 切后台不轮询 }); // 详情级查询(按需加载) useQuery({ queryKey: selectedTargetId ? queryKeys.metrics(id, from, to, "1h") : ["metrics", "disabled"], queryFn: () => fetchJson(`/api/targets/${id}/metrics?...`), 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` | 根组件,Layout + HeadMenu 骨架、主题模式选择、刷新倒计时、Skeleton 加载 | | `ErrorBoundary` | `components/ErrorBoundary.tsx` | React 错误边界,捕获渲染异常并展示降级 UI | | `SummaryCards` | `components/SummaryCards.tsx` | 总览统计卡片(单 Card 内嵌居中 Statistic,无 shadow) | | `TargetBoard` | `components/TargetBoard.tsx` | 按分组渲染目标表格列表(Space 24px 间距) | | `TargetGroup` | `components/TargetGroup.tsx` | 单个分组 Card(title+actions+headerBordered)+ PrimaryTable | | `TargetDetailDrawer` | `components/TargetDetailDrawer.tsx` | 目标详情抽屉(响应式默认宽度、支持鼠标拖拽调整、TDesign 生命周期控制、preventScrollThrough、受控 Tabs、记录 TabPanel 懒渲染) | | `OverviewTab` | `components/OverviewTab.tsx` | 目标详情概览(Descriptions 直接展示 + 4×2 统计卡片 + 趋势) | | `HistoryTab` | `components/HistoryTab.tsx` | 目标历史记录表格和分页 | | `TrendChart` | `components/TrendChart.tsx` | Recharts 趋势折线图(耗时+延迟范围) | | `StatusDot` | `components/StatusDot.tsx` | 圆形状态指示点(绿/红) | | `StatusBar` | `components/StatusBar.tsx` | 最近采样状态条(多色块 + Tooltip 提示时间和状态) | | `RefreshCountdown` | `components/RefreshCountdown.tsx` | Header 刷新倒计时(NumberFlow 数字滚动),手动刷新按钮,刷新中/等待首次刷新文本 | ### 2.5 新增功能开发步骤 以"新增一个详情页面 Tab"为例: 1. **确认数据需求**:是已有 API 数据还是需要新端点? - 如有新端点,先在 `src/server/routes/` 添加,参考 [1.3 新增路由步骤](#13-api-路由开发) - 如有新字段,更新 `src/shared/api.ts` 类型定义 2. **实现 hooks**:全局查询放在 `src/web/hooks/use-queries.ts`;目标详情条件查询放在 `src/web/hooks/use-target-detail.ts`(写好 `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-controls`)定义全局页面结构和 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 API server**(端口 3000):后端 API 服务,`--watch` 监听后端文件变更自动重启 - **Vite dev server**(端口 5173):前端 SPA + HMR 热更新 开发时访问 `http://127.0.0.1:5173`,Vite 自动将 `/api` 和 `/health` 请求代理到后端。 也可以单独启动: ```bash bun run dev:server probes.yaml # 仅启动后端 API server bun run dev:web # 仅启动 Vite dev server ``` ### 3.2 前后端集成方式 #### 双进程开发架构 开发模式下前后端分别由 Vite 和 Bun 服务: - Vite dev server 负责前端 SPA、HMR、模块热替换 - Bun API server 负责后端 API 路由 - Vite 通过 proxy 配置将 `/api/*` 和 `/health` 转发到 Bun #### 生产模式架构 生产模式下前端通过 Vite 构建为静态资源,通过 `import with { type: "file" }` 嵌入 Bun 可执行文件: ```typescript // server.ts const server = Bun.serve({ fetch(req) { // staticAssets 存在时服务嵌入的前端资源 return serveStaticAsset(new URL(req.url).pathname, staticAssets); }, routes: { "/api/*": () => ..., // API 通配符(未匹配路由返回 404) "/api/dashboard": { GET: (req) => handleDashboard(...) }, "/health": { GET: () => handleHealth(mode) }, // ... }, }); ``` #### 路由优先级 Bun routes 的匹配规则:具体路径 > 通配符。`/api/dashboard` 优先于 `/api/*`,`/health` 优先于 `/*`。 未匹配 method 的请求(如 POST /api/dashboard)会落入 `/api/*` 通配符返回 404。 非 API 路径由 fetch fallback 处理:有文件扩展名的返回对应静态资源或 404,无扩展名的返回 SPA index.html。 ### 3.3 构建打包 #### 构建命令 ```bash bun run build ``` #### 构建流程 `scripts/build.ts` 执行三步流水线: ``` 1. Vite build → dist/web/ (前端静态资源,含 code splitting) 2. Code generation → .build/static-assets.ts + .build/server-entry.ts 3. Bun compile → dist/dial-server (单可执行文件) ``` - Vite 构建前端资源到 `dist/web/`,自动 code splitting(vendor-react、vendor-tdesign、vendor-chart) - Code generation 扫描 `dist/web/` 生成 `import with { type: "file" }` 声明,将资源嵌入 binary - Bun compile 以 `.build/server-entry.ts` 为入口编译最终可执行文件 - `.build/` 临时目录在构建完成后自动清理 #### 产物 | 产物 | 用途 | | ------------------ | ---------------------------------------- | | `dist/dial-server` | 生产可执行文件(含前端资源,单文件部署) | | `dist/web/` | Vite 构建的前端资源(构建中间产物) | #### 构建参数 | 环境变量 | 说明 | | --------------------------- | -------------------------------------- | | `BUN_TARGET`/`BUILD_TARGET` | 交叉编译目标平台(如 `bun-linux-x64`) | #### 运行可执行文件 ```bash ./dist/dial-server probes.yaml ``` 启动后: - 访问 `http://127.0.0.1:3000/` → 返回前端 SPA - 访问 `http://127.0.0.1:3000/api/*` → 返回后端 API - 访问 `/dashboard` 等前端路由 → SPA fallback 到 index.html #### 清理 ```bash bun run clean # 清理 dist/ 构建产物和 .build/ 临时文件 ``` ### 3.4 开发工作流 #### 日常开发循环 ```bash bun run dev probes.yaml # 启动双进程开发环境(Vite + API server) # 访问 http://127.0.0.1:5173 # 修改前端代码 → Vite HMR 热更新 / 修改后端代码 → --watch 自动重启 bun run check # 提交前运行完整质量检查 ``` #### 完整验证流程 ```bash bun run verify # = bun run check + bun run build ``` `verify` 适合 CI 或正式提交前,会完整验证类型检查、lint、格式、单元测试和生产构建。 ### 3.5 Executable/E2E 验证 原 `scripts/smoke.ts` 覆盖过薄,已从当前工作流移除。后续如需验证 production executable 的 API、静态资源服务、SPA fallback 行为,应重新设计独立的 executable/E2E 测试。 ### 3.6 脚本说明 | 脚本 | 文件 | 说明 | | ---------------------- | ----------------------------------- | ---------------------------------------- | | `bun run dev` | `scripts/dev.ts` | 双进程开发服务(Vite :5173 + API :3000) | | `bun run dev:server` | `src/server/dev.ts` | 仅启动后端 API server | | `bun run dev:web` | Vite CLI | 仅启动 Vite dev server | | `bun run build` | `scripts/build.ts` | Vite → codegen → Bun compile 三步构建 | | `bun run schema` | `scripts/generate-config-schema.ts` | 生成 `probe-config.schema.json` | | `bun run schema:check` | `scripts/generate-config-schema.ts` | 检查配置 schema 导出物是否同步 | | `bun run clean` | `scripts/clean.ts` | 清理构建缓存与临时文件 | ### 3.7 环境变量 | 变量 | 用途 | 默认值 | | --------------------------- | ----------------------------------------------- | -------- | | `BUN_TARGET`/`BUILD_TARGET` | 交叉编译目标平台(仅在 `bun run build` 时有效) | 当前平台 | ### 3.8 项目配置文件 | 文件 | 用途 | | ---------------------- | ---------------------------------------------- | | `package.json` | 项目信息、脚本、依赖声明 | | `tsconfig.json` | TypeScript 配置(ESNext 模块、严格模式) | | `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/`(HTML import 除外) | | `src/web/` | 前端代码,不能 import `src/server/` | | `src/shared/` | 前后端共享类型,双向可引用 | | `scripts/` | 独立运行脚本,可 import 项目源码 | | `tests/` | 测试目录,结构镜像 src 目录 | | `dist/` | 构建产物(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 统一签名需要) | | `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,无需手动配置。 ## 测试 项目采用两层测试体系:单元测试 + 组件测试。所有测试使用 `bun:test` 运行。 ### 测试分层 | 层级 | 覆盖范围 | 位置 | 命令 | | -------- | ---------------------- | ----------------------------------------------------------------------------- | --------------------------------------------- | | 单元测试 | 后端函数、纯函数、常量 | `tests/server/**/*.test.ts`、`tests/web/{constants,utils,hooks}/**/*.test.ts` | `bun test tests/server`、`bun test tests/web` | | 组件测试 | React 组件渲染和交互 | `tests/web/components/**/*.test.tsx` | `bun test tests/web/components` | ### 运行命令 ```bash bun test # 运行所有单元测试和组件测试 bun test tests/server # 只运行后端单元测试 bun test tests/web # 只运行前端测试(单元 + 组件) bun run check # 日常开发(类型检查 + lint + 测试) bun run verify # 完整验证(check + 构建) ``` ### 组件测试环境 组件测试使用 jsdom 模拟浏览器环境,配置位于 `tests/setup.ts`(通过 `bunfig.toml` preload 加载): - jsdom 提供完整的 DOM 环境 - TDesign 组件所需的 polyfill:ResizeObserver、IntersectionObserver、matchMedia、attachEvent - recharts 图表组件被 mock 为占位元素(SVG 渲染在 jsdom 中不可靠) ### 编写规范 - **优先使用 `@testing-library/react`** 的语义化查询(getByText、getByRole)而非 CSS 选择器 - **测试用户行为而非实现细节**:模拟用户点击、输入等操作,而非直接调用组件方法 - **只 mock 系统边界**:mock fetch 返回预设响应,使用真实的 QueryClientProvider 包裹组件 - **组件测试文件命名**:`tests/web/components/ComponentName.test.tsx` ## 已知限制 当前不做告警通知、拨测目标动态增删、认证鉴权和分布式部署。