1
0
Files
DiAL/DEVELOPMENT.md
lanyuanxiaoyao cf847ccd7a feat: 重构配置生命周期为 Authoring/Normalized/Resolved 三层
将变量替换和 expect 简写展开统一放入 Normalized 阶段,
运行时 AJV 使用 Normalized schema,导出 schema 面向 Authoring Config。

主要变更:
- 新增 normalizer.ts 实现 normalizeAuthoringConfig()
- 拆分 Authoring/Normalized 双 schema,checker 接口支持 authoring/normalized 片段
- config-loader 流程:normalize → Normalized AJV → semantic → resolve
- validator 兼容层自动分派 raw/normalized expect 形态
- 删除 rawExpect,store.expect 列写入 null
- Authoring schema 对 integer/boolean/enum 字段接受变量引用
- 修复 DB/HTTP validate 入口守卫和 LLM options integer 变量引用
- 优化 compact() 避免 undefined 覆盖隐患
- 移除 content.ts 恒为 true 的前置条件
- 同步 5 个主规范并归档 change
2026-05-22 14:00:47 +08:00

1350 lines
85 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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、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 resolveprimitive→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 全局面板查询 hookdashboard/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(yamlYAML 解析 → Authoring normalize变量替换 + expect 简写展开)→ Normalized 契约校验 → 语义校验 → 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 shutdownengine.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 | 主流三方库 | cheerioHTML 解析、xpath + @xmldom/xmldomXML 解析) |
| 5 | 自行实现 | 仅在以上都无法满足时(如 `parseDuration``parseSize``evaluateJsonPath` 等专项逻辑) |
**原则**:新增依赖前先检查上述每一层级是否已有可用方案。禁止随意引入新依赖。
### 1.3 API 路由开发
路由文件位于 `src/server/routes/`,每个端点一个文件。路由通过 `server.ts``Bun.serve({ routes })` 声明式注册,使用 per-method handler 对象:
```typescript
// server.ts 中的路由注册
routes: {
"/*": homepage, // HTML importSPA 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`),使用 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 -> AuthoringProbeConfig -> NormalizedProbeConfig -> ValidatedProbeConfig -> ResolvedConfig`
Authoring Config 是用户 YAML 可书写形态,允许 `${key}``${key|default}``${key|}``$${key}` 变量引用和 expect primitive/keyed/content 简写。`normalizeAuthoringConfig()` 在 YAML 解析之后、AJV 契约校验之前执行,只做去糖:调用 `variables.ts` 完成变量替换,展开 expect 简写,并移除顶层 `variables` 段。Normalized Config 不包含变量引用、不包含 Raw expect primitive 简写,也不补默认值、不解析 duration/size/path/env、不合并 `cmd.env`
根目录 `probe-config.schema.json` 由 Authoring schema 导出,服务 VSCode 和外部用户校验;运行时 `validateProbeConfigContract()` 使用 Normalized schema。checker 必须提供 `schemas.authoring.config``schemas.authoring.expect``schemas.normalized.config``schemas.normalized.expect` 四个 TypeBox 片段Authoring 片段描述用户可写 DSLNormalized 片段描述 normalizer 输出。
变量替换解析优先级为 `variables -> process.env -> 默认值 -> unresolved-variable 报错`;替换范围不包含对象 key且仅跳过 `targets[].id``targets[].type` 字段。字段值完整等于单个变量引用时保留 number/boolean/string 类型推断,部分拼接时统一转为字符串。
`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``expect.headers``cmd.env`
契约校验和语义 validator 都必须返回 `ConfigValidationIssue[]`,不要在 validator 内直接拼接最终用户错误字符串。最终错误由 `formatConfigIssues()` 统一渲染,错误路径需要尽量包含 `targetName` 或 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 契约 schemaconfig / expect 两部分) |
| `validate.ts` | 启动期语义校验JSON Schema 无法表达的规则) |
| `execute.ts` | Checker 类resolve默认值合并 + 解析、execute执行检查、serializeDB 持久化) |
| `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 执行计划类型
- `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 / 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()` 规范**
- 只做内置默认值填充、路径解析、单位转换,**不执行校验**
- `resolve()` 接收的 target 已通过 Normalized schema 和语义校验expect 已是 normalized 形态primitive 简写已展开为 `{equals}`、keyed 已转为 `{key, matcher}[]`、content 已转为带 `kind` 的执行结构)
- `resolve()` 对 expect 只做默认值填充(如未配置 `status` 时物化 `[200]`)和 spread 透传,不再调用 Raw 简写展开函数
- 返回 `satisfies ResolvedXxxTarget` 确保类型正确
**expect 管线**:配置从定义到执行经过 Authoring → Normalized → Resolved → Execute 四层,简写展开在 normalizer 阶段完成:
| 断言模型 | Authoring 输入 | Normalizer 层 | Schema 层Authoring / Normalized | Validate 层(接收 Normalized | Execute 层 |
| --------------------- | ----------------------------------- | ------------------------------ | -------------------------------------------------------------------- | ---------------------------------- | ---------------------------- |
| `ValueExpectation` | `number \| ValueMatcher` | `resolveValueExpectation()` | `createAuthoringValueExpectationSchema()` / `createNormalized*()` | `validateRawValueExpectation()` | `checkValueExpectation()` |
| `ContentExpectations` | `(ValueMatcher \| ExtractorRule)[]` | `resolveContentExpectations()` | `createAuthoringContentExpectationsSchema()` / `createNormalized*()` | `validateRawContentExpectations()` | `checkContentExpectations()` |
| `KeyedExpectations` | `Record<string, ValueExpectation>` | `resolveKeyedExpectations()` | `createAuthoringKeyedExpectationsSchema()` / `createNormalized*()` | `validateRawKeyedExpectations()` | `checkKeyedExpectations()` |
选择哪种模型参考 [1.10 expect 字段选择规范](#110-expect-断言系统)的决策树。
**resolve 中的标准模式**
```typescript
// resolve() 内expect 已是 normalized 形态spread 后补默认值
const expect = target.expect as ResolvedXxxExpectConfig | undefined;
const resolvedExpect: ResolvedXxxExpectConfig = expect
? { ...expect, status: expect.status ?? [200] }
: { status: [200] };
```
**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 Schematarget.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()` | 查询全部 targetsdefault 分组优先排序) |
| `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`idTEXT PRIMARY KEY配置 target id、nameTEXT可 NULL展示名称、descriptionTEXT可 NULL描述、type、target展示摘要、configJSON、interval_ms、timeout_ms、expectJSON、grp
- `check_results`target_idTEXT FK CASCADE引用配置 target id、timestamp、matched0/1、duration_ms、observationJSON TEXT、failureJSON
- 复合索引:`(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` 触发 abortchecker 必须使用 `CheckerContext.signal` 感知超时HTTP 将 signal 传给 `fetch()`Cmd 和 ICMP 在 signal abort 时 `proc.kill()`
- **结果写入**:检查结果通过 `store.insertCheckResult()` 写入 SQLiteengine 基于配置 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` | 级别 errorchecker 执行异常) |
| `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 专属状态断言位于各自目录。
**Authoring → Normalized → Resolved**:用户 YAML 写的是 Authoring 形态primitive 简写、`{ json: { path, equals } }` 提取器对象、`Record<string, value>` 键值表),`normalizeAuthoringConfig()` 在 AJV 前将其转换为 Normalized 形态(`{ equals: primitive }``{ kind, matcher, ... }` content 联合、`{ key, matcher }[]` 有序数组)。语义校验器接收 Normalized 形态并校验。`resolve()` 只补默认值和做运行期转换。Store `targets.expect` 列当前写入 NULL不持久化 expect 快照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 Authoring 输入可使用 string、number、boolean 或 null 简写normalizer 阶段归一化为 `{ equals: value }`;数组和对象简写不支持,必须显式写成 `{ equals: ... }`
`ContentExpectations` 数组按顺序快速失败。数组项可以是直接 matcher也可以是 `{ json: {...} }``{ css: {...} }``{ xpath: {...} }` 提取器规则;一条规则不能混用直接 matcher 和 extractor多个 extractor 也不能共存。Extractor 未配置 matcher 时等价于 `exists: true`。对对象或数组源执行直接 `contains`/`regex` 时会先 JSON 序列化,`equals` 仍对原始结构做深度相等。
启动期语义校验统一由 `expect/validate.ts` 负责,校验器通过兼容层同时支持 Raw 和 Normalized expect 形态(`validateRawKeyedExpectations` 遇到数组自动分派到 `validateNormalizedKeyedExpectations``validateRawContentExpectation` 遇到 `kind` 字段自动分派到 `validateNormalizedContentExpectation`)。校验内容包括空 matcher、未知字段、字段类型、`exists:false` 组合、ContentExpectations 互斥性、JSONPath 子集、XPath 可编译性、regex 可编译性、ReDoS 风险以及 HTTP/LLM headers 大小写归一化后重复 key。语义校验不修改输入。旧字段 `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 而非 ContentExpectationsContentExpectations 的 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 importfullstack 模式) | 开发服务与生产构建 |
| 语言 | 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
└── ErrorBoundaryReact 错误边界)
└── QueryClientProviderTanStack 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 包裹 PrimaryTableheaderBordered
│ └── 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全局面板级查询
├── queryKeysdashboard/meta/metrics 结构化 query key
├── useDashboard(refetchInterval) → /api/dashboard?window=24h&recentLimit=30动态刷新间隔由调用方传入
├── useTargetMetrics() → /api/targets/:id/metrics详情按需加载
└── useMeta() → /api/metastaleTime: Infinity
hooks/use-target-detail.tsDrawer 状态与详情级条件查询)
├── 内部复用 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 / darkRadioGroup 受控值)
├── 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` | 单个分组 Cardtitle+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 splittingvendor-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 Docker 镜像
Docker 镜像使用 Alpine 多阶段构建,保持与生产单可执行文件交付模型一致:
```
oven/bun:1-alpine → bun install --frozen-lockfile
→ BUN_TARGET=bun-linux-*-musl bun run build
→ dist/dial-server
alpine → 仅复制 /usr/local/bin/dial-server
→ 安装 ca-certificates、iputils-ping、libgcc、libstdc++、tzdata
→ 使用非 root dial 用户运行
```
#### 架构目标映射
Dockerfile 通过 Docker 提供的 `TARGETARCH` 选择 Bun compile target
| `TARGETARCH` | `BUN_TARGET` | 说明 |
| ------------ | ---------------------- | ----------------- |
| `amd64` | `bun-linux-x64-musl` | Alpine x64 musl |
| `arm64` | `bun-linux-arm64-musl` | Alpine ARM64 musl |
不支持的架构必须在构建阶段失败,避免生成无法运行的镜像。
#### 运行时边界
- 最终镜像不复制源码、`node_modules``.build``dist/web`
- 默认入口为 `/usr/local/bin/dial-server`,默认配置路径为 `/etc/dial/probes.yaml`
- 容器示例配置为 `docker/probes.yaml`,默认监听 `0.0.0.0:3000`,数据目录为 `/data/dial`
- 健康检查使用 Alpine 自带 `wget` 请求 `/health`,不为健康检查安装 `curl`
- `libgcc``libstdc++` 是 Bun musl executable 在 Alpine 中运行所需的基础运行库
- ICMP checker 依赖镜像内置的 `iputils-ping`,运行容器时仍需要按需授予 `--cap-add=NET_RAW`
- CMD checker 的额外命令环境不进入官方镜像,用户需要 `curl``dig``psql` 等命令时通过派生镜像安装
#### 验证命令
```bash
bun run check
bun run build
docker build -t dial:alpine .
docker run --rm -p 3000:3000 -v dial-data:/data/dial dial:alpine
docker buildx build --platform linux/amd64,linux/arm64 -t dial:alpine .
```
如本地 Docker 环境不支持 buildx 或多架构模拟,需在变更记录中说明未执行原因。
### 3.5 跨平台发布
#### 发布命令
```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 组件所需的 polyfillResizeObserver、IntersectionObserver、matchMedia、attachEvent
- recharts 图表组件被 mock 为占位元素SVG 渲染在 jsdom 中不可靠)
### 编写规范
- **优先使用 `@testing-library/react`** 的语义化查询getByText、getByRole而非 CSS 选择器
- **测试用户行为而非实现细节**:模拟用户点击、输入等操作,而非直接调用组件方法
- **只 mock 系统边界**mock fetch 返回预设响应,使用真实的 QueryClientProvider 包裹组件
- **组件测试文件命名**`tests/web/components/ComponentName.test.tsx`
## 已知限制
当前不做告警通知、拨测目标动态增删、认证鉴权和分布式部署。