# DiAL 开发文档

本文档面向 DiAL 项目的开发者，介绍项目结构、构建流程、测试、代码规范等内容。

用户使用说明请参阅 [README.md](README.md)。

## 目录

- [版本管理](#版本管理)
- [项目结构](#项目结构)
- [一、后端开发指引](#一后端开发指引)
- [二、前端开发指引](#二前端开发指引)
- [三、项目运行、集成与打包](#三项目运行集成与打包)
- [代码质量](#代码质量)
- [已知限制](#已知限制)

---

## 版本管理

DiAL 使用 `package.json.version` 作为应用版本号的唯一来源，遵循 SemVer 语义化版本规范（`MAJOR.MINOR.PATCH`）。

**版本升迁命令：**

| 命令                            | 说明                                              |
| ------------------------------- | ------------------------------------------------- |
| `bun run version:patch`         | 升迁 patch 版本（bugfix、文档、测试、内部重构）   |
| `bun run version:minor`         | 升迁 minor 版本（新功能、新 checker、新配置字段） |
| `bun run version:major`         | 升迁 major 版本（不兼容的配置格式、API 行为变化） |
| `bun run version:set <version>` | 显式设置版本号                                    |

**版本展示：**

- Dashboard Header 品牌区域展示当前运行实例版本号（如 `v0.1.0`）
- 版号通过 `/api/meta` 接口返回，前端通过 `useMeta` hook 获取
- 生产构建时版本号固化到可执行文件中，不依赖运行时外部 `package.json`

**暂不支持：**

- CLI `--version` 参数
- 自动创建 git commit、git tag 或 changelog
- prerelease 版本格式（如 `1.0.0-beta.1`）

---

## 项目结构

```text
src/
  server/
    bootstrap.ts    后端统一启动引导（loadConfig → logger → store → engine → startServer → shutdown）
    config.ts       CLI 参数解析（仅提取配置文件路径）
    dev.ts          开发模式启动入口（mode: "development"，仅 API server）
    logger.ts       日志模块（Logger 接口、Pino 运行时封装、NoopLogger、MemoryLogger、ConsoleFallbackLogger）
    main.ts         生产模式启动入口（mode: "production"，安全头启用）
    server.ts       HTTP server 启动工厂（Bun.serve routes 声明式路由 + fetch fallback 静态资源服务）
    helpers.ts      共享响应格式化工具（见下方函数清单）
    middleware.ts   API 参数校验中间件（validateTargetId、validateTimeRange、validatePagination）
    version.ts      应用版本读取与校验
    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(server.listen), port(server.listen), dataDir(server.storage), maxConcurrentChecks(probes.execution), retentionMs(server.storage), targets, logging(server.logging) }
  → createRuntimeLogger(logging) → Logger（配置加载失败时使用 ConsoleFallbackLogger）
  → ProbeStore(db) → store.syncTargets(targets)
  → ProbeEngine(store, targets, maxConcurrentChecks, retentionMs, logger) → engine.start()
  → startServer({ config, mode, store, logger })
  → 注册 SIGINT/SIGTERM shutdown（engine.stop + store.close + logger.flush）

运行时:
  定时器(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/` 下创建 `<name>.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<TResolved extends ResolvedTargetBase = ResolvedTargetBase>` 使用泛型约束 `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.storage.dataDir`（基于配置文件目录解析为绝对路径） | `configDir/data` |
| `host`                | `server.listen.host`                                       | `127.0.0.1`      |
| `logging`             | `server.logging`（等级继承、路径解析、滚动参数）           | 见 logging 配置  |
| `port`                | `server.listen.port`                                       | `3000`           |
| `maxConcurrentChecks` | `probes.execution.maxConcurrentChecks`                     | `20`             |
| `retentionMs`         | `server.storage.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/<type>/` 下的自包含目录，包含该 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<string, string>`（用于 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<string, ValueExpectation>`  | `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`
- **日志集成**：engine 构造时接收 `Logger` 实例（可选，默认 NoopLogger），通过 `initStateCache()` 从 store 加载最新状态；状态变化时记录日志（UP→DOWN `warn`、DOWN→UP `info`、首次检查 DOWN `warn`、稳态无日志）；每次检查产出 `debug` 级别结构化摘要

### 1.10 日志模块

日志模块位于 `src/server/logger.ts`，定义项目内部最小 `Logger` 接口，后端运行时代码统一通过此接口输出日志。

**Logger 接口**：

| 方法    | 说明                                     |
| ------- | ---------------------------------------- |
| `trace` | 级别 trace（开发调试）                   |
| `debug` | 级别 debug（检查摘要、状态详情）         |
| `info`  | 级别 info（启动、恢复、正常操作）        |
| `warn`  | 级别 warn（状态变化 UP→DOWN、首次 DOWN） |
| `error` | 级别 error（checker 执行异常）           |
| `fatal` | 级别 fatal（启动失败）                   |
| `child` | 创建子 logger（附加 bindings 上下文）    |
| `flush` | 刷新缓冲（用于关机前确保日志落盘）       |

每个方法支持两种签名：`(msg: string)` 和 `(obj: Record<string, unknown>, msg?: string)`。

**实现**：

| 实现                    | 用途                                            |
| ----------------------- | ----------------------------------------------- |
| `PinoLoggerWrapper`     | 生产运行时，封装 Pino + pino-pretty + pino-roll |
| `NoopLogger`            | 静默丢弃所有日志，用于不需要日志输出的场景      |
| `MemoryLogger`          | 测试替身，将日志条目收集到 `entries` 数组供断言 |
| `ConsoleFallbackLogger` | 配置加载失败前的降级日志，直接输出到 console    |

**日志输出**：

- **控制台**：始终开启，使用 pino-pretty 格式化（彩色、单行、时间戳 `yyyy-mm-dd HH:MM:ss.l`）
- **文件**：始终开启，JSONL 格式，通过 pino-roll 支持按大小和频率滚动
- **根等级**：取 console 和 file 中的最低等级，确保两个流都能收到所需日志
- **敏感信息**：自动 redact `authorization`、`cookie`、`set-cookie`、`authToken`、`key`、`password`、`token`、`apiKey` 及其嵌套路径，替换为 `[Redacted]`

**测试用法**：

```typescript
import { createMemoryLogger } from "../logger";

const logger = createMemoryLogger();
const engine = new ProbeEngine(store, targets, 20, 0, logger);

// 断言日志
expect(logger.entries.filter((e) => e.level === "warn")).toHaveLength(1);
expect(logger.entries[0]!.msg).toContain("UP → DOWN");
```

**运行时规范**：

- `src/server/` 下的运行时代码禁止直接使用 `console.*`，必须通过注入的 `Logger` 实例输出
- 配置加载失败（logger 尚未初始化）时使用 `ConsoleFallbackLogger`
- `bootstrap.ts` 在 shutdown 时调用 `logger.flush()` 确保缓冲日志写入磁盘

### 1.11 expect 断言系统

两层模型：**观测值收集** → **规则校验**。共享断言基础设施位于 `checker/expect/`，checker 专属状态断言位于各自目录。

**Raw vs Resolved**：用户 YAML 写的是 Raw 形态（primitive 简写、`{ json: { path, equals } }` 提取器对象、`Record<string, value>` 键值表），`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.12 错误模式

- **API 错误**：`{ error: "描述", status: <code> }`，状态码 400/404/503
- **CheckFailure**：`{ kind: "error"|"mismatch", phase, path, expected?, actual?, message }`
- **错误处理**：expect 校验失败记录首个失败原因；网络/超时/进程崩溃统一为 `kind:"error"`，请求/TLS/timeout 错误归属 `phase:"request"`，body 超限/解码/解析错误归属 `phase:"body"`
- **日志**：运行时日志通过 `Logger` 接口统一输出（Pino 运行时、Noop/Memory/ConsoleFallback 测试替身），配置加载失败前使用 ConsoleFallbackLogger；禁止在 `src/server/` 运行时代码中直接使用 `console.*`

### 1.13 测试规范

- 测试目录 `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<DashboardResponse>("/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<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 全局配置

```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` 中新增 `<Tabs.TabPanel>` 引用
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-common.ts`（共享函数）和 `scripts/build.ts`（编排逻辑）。

`scripts/build.ts` 执行三步流水线（函数来自 `build-common.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/ 构建产物、dist/release/ 发布产物和 .build/ 临时文件
```

### 3.4 跨平台发布

#### 发布命令

```bash
bun run release                          # 编译全部 7 个目标平台
bun run release --target linux-x64       # 编译指定平台
bun run release --target linux-x64,windows-x64,darwin-arm64  # 多平台
```

#### 发布流程

`scripts/release.ts` 复用 `build-common.ts` 的前端构建和代码生成，然后执行多目标交叉编译和打包：

```
1. Vite build        → dist/web/ (前端静态资源，只执行一次)
2. Code generation   → .build/ (资源嵌入代码)
3. 多目标 Bun compile → dist/release/binaries/ (7 个目标平台二进制)
4. tar.gz 打包       → dist/release/packages/ (压缩包 + SHA256 校验和)
```

#### 支持的目标平台

| CLI 参数           | Bun CompileTarget      | 说明                |
| ------------------ | ---------------------- | ------------------- |
| `linux-x64`        | `bun-linux-x64`        | Linux x64 glibc     |
| `linux-arm64`      | `bun-linux-arm64`      | Linux ARM64 glibc   |
| `linux-x64-musl`   | `bun-linux-x64-musl`   | Linux x64 musl      |
| `linux-arm64-musl` | `bun-linux-arm64-musl` | Linux ARM64 musl    |
| `windows-x64`      | `bun-windows-x64`      | Windows x64         |
| `darwin-x64`       | `bun-darwin-x64`       | macOS Intel         |
| `darwin-arm64`     | `bun-darwin-arm64`     | macOS Apple Silicon |

#### 产出物

```
dist/release/
├── binaries/                                          ← 裸二进制
│   ├── dial-server-{version}-{os}-{arch}[.exe]
│   └── ...
└── packages/                                          ← 压缩包 + 校验和
    ├── dial-server_{version}_{os}_{arch}.tar.gz
    ├── dial-server_{version}_{os}_{arch}.tar.gz.sha256
    └── ...
```

压缩包内含可执行文件（`dial-server` 或 `dial-server.exe`）、`probes.example.yaml` 和 `LICENSE`。

#### 命名规范

- 裸二进制：`dial-server-{version}-{os}-{arch}[.exe]`，如 `dial-server-0.1.0-linux-x64`
- 压缩包：`dial-server_{version}_{os}_{arch}.tar.gz`，如 `dial-server_0.1.0_linux_x64.tar.gz`
- 校验和：`<压缩包文件名>.sha256`，格式兼容 `sha256sum -c`

### 3.5 开发工作流

#### 日常开发循环

```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、格式、单元测试和生产构建。

**构建 code generation 约定**：`scripts/build-common.ts` 中的 `toImportSpecifier()` 将文件系统相对路径转换为 ESM import specifier，输出在所有平台上都必须使用 `/` 分隔符，不能包含 Windows 反斜杠。跨平台路径测试不得使用当前平台的 `path.sep` 伪装其他平台行为，应使用 `node:path.win32` 或等价注入方式显式模拟目标平台语义。

### 3.6 Executable/E2E 验证

原 `scripts/smoke.ts` 覆盖过薄，已从当前工作流移除。后续如需验证 production executable 的 API、静态资源服务、SPA fallback 行为，应重新设计独立的 executable/E2E 测试。

### 3.7 脚本说明

| 脚本                   | 文件                                | 说明                                               |
| ---------------------- | ----------------------------------- | -------------------------------------------------- |
| `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 release`      | `scripts/release.ts`                | 跨平台发布打包（多目标交叉编译 + tar.gz + SHA256） |
| `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.8 环境变量

| 变量                        | 用途                                            | 默认值   |
| --------------------------- | ----------------------------------------------- | -------- |
| `BUN_TARGET`/`BUILD_TARGET` | 交叉编译目标平台（仅在 `bun run build` 时有效） | 当前平台 |

### 3.9 项目配置文件

| 文件                   | 用途                                           |
| ---------------------- | ---------------------------------------------- |
| `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.10 依赖管理

- **包管理器**：仅使用 `bun`，禁止使用 npm、pnpm、yarn
- **安装依赖**：`bun install`
- **运行工具**：使用 `bunx`，禁止使用 `npx`、`pnpx`
- **锁文件**：`bun.lock`

### 3.11 目录约定

| 目录          | 约定                                                 |
| ------------- | ---------------------------------------------------- |
| `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`                                          | 导入路径验证、循环依赖检测、重复导入合并           |
| `no-restricted-syntax`                                          | 禁止 `src/server/` 运行时代码直接使用 `console.*`  |
| `eslint-plugin-prettier` recommended + `eslint-config-prettier` | 将 Prettier 格式集成为 ESLint 规则，禁用冲突规则   |

后端运行时代码的 `console.*` 检查使用中文定制提示：`后端运行时代码禁止直接使用 console.*；请通过注入的 Logger 实例输出日志，配置加载失败前使用 createConsoleFallback()。`。`src/server/logger.ts` 是唯一例外，用于封装 `ConsoleFallbackLogger`。

### 测试代码 ESLint 规范

测试代码与业务代码使用相同的 ESLint 规则集，应优先通过类型化 helper、类型化 mock、显式 no-op 和受控断言模式满足已启用的类型感知规则，最小化 `eslint-disable` 的使用。具体约定：

- 使用类型化 mock 变量（`vi.fn()`）替代动态 `require` 获取 mocked module
- 异步错误断言使用 helper 或显式 try/catch，避免依赖 Bun `expect(...).rejects` 与 `await-thenable` 规则的类型不匹配
- polyfill 中的 intentional no-op 使用显式可解释写法（如 `() => undefined` 或共享 `noop` 函数）
- 对 `process.exit` 等系统 API 使用 `spyOn`（从 `bun:test` 导入）受控 mock 而非手动 monkey patch

### 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`

## 已知限制

当前不做告警通知、拨测目标动态增删、认证鉴权和分布式部署。
