- failure actual 截断格式改为 …(共 N 字符),标量不序列化直接返回 - 新增 redos.ts 实现 ReDoS 静态检测(嵌套量词/重叠交替),启动期拒绝危险正则 - JSON body rules 共享同一次 JSON.parse 结果,避免重复解析 - checkCssRule 重构为线性流程,消除 exist:true 与无 operator 的冗余分支 - extract checkEarlyTimeout 辅助函数,明确提前 duration 检查意图 - 补充 303/307/308 重定向、相对路径 Location、混合 body rules 集成测试
1022 lines
55 KiB
Markdown
1022 lines
55 KiB
Markdown
# DiAL 开发文档
|
||
|
||
本文档面向 DiAL 项目的开发者,介绍项目结构、构建流程、测试、代码规范等内容。
|
||
|
||
用户使用说明请参阅 [README.md](README.md)。
|
||
|
||
## 目录
|
||
|
||
- [项目结构](#项目结构)
|
||
- [一、后端开发指引](#一后端开发指引)
|
||
- [二、前端开发指引](#二前端开发指引)
|
||
- [三、项目运行、集成与打包](#三项目运行集成与打包)
|
||
- [代码质量](#代码质量)
|
||
- [已知限制](#已知限制)
|
||
|
||
---
|
||
|
||
## 项目结构
|
||
|
||
```text
|
||
src/
|
||
server/
|
||
app.ts Bun HTTP 路由入口(路由分发 + API 汇聚、StaticAssets 接口定义)
|
||
bootstrap.ts 后端统一启动引导(loadConfig → store → engine → startServer → shutdown)
|
||
config.ts CLI 参数解析(仅提取配置文件路径)
|
||
dev.ts 开发模式启动入口
|
||
server.ts HTTP server 启动工厂(接收 StartServerOptions)
|
||
helpers.ts 共享响应格式化工具(见下方函数清单)
|
||
middleware.ts API 参数校验中间件(guardGetHead、validateTargetId、validateTimeRange、validatePagination)
|
||
static.ts 静态资源服务与 SPA fallback
|
||
routes/ API 路由 handler(按端点拆分,签名因端点而异)
|
||
health.ts GET /health(无 store 参数)
|
||
summary.ts GET /api/summary
|
||
targets.ts GET /api/targets
|
||
history.ts GET /api/targets/:id/history
|
||
trend.ts GET /api/targets/:id/trend
|
||
checker/
|
||
types.ts 基础类型定义(ResolvedTargetBase、RawTargetConfig、DefaultsConfig、CheckResult 等基础 interface)
|
||
config-loader.ts YAML 配置解析、契约校验、语义校验与运行期解析(输出 ResolvedConfig)
|
||
schema/ TypeBox + Ajv 配置契约、schema fragments、issue 渲染和 schema 导出入口
|
||
builder.ts 全量 JSON Schema 组装(遍历 registry 生成)
|
||
fragments.ts 共享 TypeBox schema 片段(duration、size、operator 等)
|
||
validate.ts Ajv 契约校验入口
|
||
issues.ts 校验问题类型与渲染
|
||
types.ts schema 层类型
|
||
export.ts JSON Schema 文件导出
|
||
store.ts SQLite 数据存储(含 syncTargets、prune 等生命周期方法)
|
||
engine.ts 调度引擎(按 interval 分组的 es-toolkit groupBy + Semaphore 并发控制 + 数据清理)
|
||
utils.ts 共享工具函数(parseSize、parseDuration)
|
||
expect/ 共享 expect 断言基础设施(跨 checker 复用)
|
||
types.ts ExpectResult 共享断言类型
|
||
failure.ts 失败信息构造(errorFailure、mismatchFailure、truncateActual)
|
||
operator.ts 操作符系统(applyOperator、evaluateJsonPath)
|
||
duration.ts 耗时断言(checkDuration)
|
||
validate-operator.ts 操作符语义校验(validateOperatorObject、isJsonValue、isPlainRecord)
|
||
runner/ Checker 统一抽象与注册机制
|
||
types.ts CheckerDefinition、CheckerContext、CheckerSchemas、ResolveContext
|
||
registry.ts CheckerRegistry 注册中心
|
||
index.ts 注册入口(显式数组 + 循环注册)
|
||
http/ HTTP Checker(自包含模块,含 types/schema/execute/expect/validate/body)
|
||
command/ Command Checker(自包含模块,含 types/schema/execute/expect/validate/text)
|
||
shared/
|
||
api.ts 前后端共享 TypeScript 类型
|
||
web/ Vite + React 前端 Dashboard
|
||
app.tsx 根组件(编排全局状态与布局)
|
||
main.tsx 入口(QueryClient 挂载 + ErrorBoundary + ReactQueryDevtools)
|
||
styles.css 全局样式与自定义 CSS 变量
|
||
components/ UI 组件(见下方组件清单)
|
||
constants/ 常量与纯函数
|
||
history-table-columns.tsx 历史记录表格列定义
|
||
target-table-columns.tsx 目标表格列定义工厂
|
||
target-table-filters.ts 表格筛选器
|
||
target-table-sorters.ts 表格排序器
|
||
color-threshold.ts 可用率颜色阈值函数
|
||
hooks/ TanStack Query 数据层
|
||
use-queries.ts 全局面板查询 hook(summary/targets/meta)
|
||
use-target-detail.ts 目标详情 Drawer 状态与条件查询 hook
|
||
utils/ 前端工具函数
|
||
time.ts 时间处理(subtractHours)
|
||
stats.ts 趋势统计计算(computeTrendStats)
|
||
scripts/ 开发、构建、schema 生成和 smoke test 脚本
|
||
tests/ Bun test 测试(结构镜像 src 目录)
|
||
openspec/ OpenSpec 变更与规格文档
|
||
probe-config.schema.json 用户配置 JSON Schema 导出物(用于 IDE 自动补全和校验)
|
||
```
|
||
|
||
> **说明**:`runner/http/` 和 `runner/command/` 的完整文件结构见 [1.7.1 架构总览](#171-架构总览) 中的标准文件表。
|
||
|
||
## 前后端边界
|
||
|
||
前端只通过 HTTP 调用后端,API 路径为 `/api/*`。共享类型放在 `src/shared`,前端不得 import `src/server` 的运行时实现。
|
||
|
||
---
|
||
|
||
## 一、后端开发指引
|
||
|
||
### 1.1 架构概览
|
||
|
||
```
|
||
启动流程:
|
||
dev.ts / build entry → readRuntimeConfig(cli args, 仅提取 configPath)
|
||
→ bootstrap({ configPath, mode, staticAssets? })
|
||
→ loadConfig(yaml) → ResolvedConfig{ host, port, dataDir, maxConcurrentChecks, retentionMs, targets }
|
||
→ ProbeStore(db) → store.syncTargets(targets)
|
||
→ ProbeEngine(store, targets, maxConcurrentChecks, retentionMs) → engine.start()
|
||
→ startServer({ config, mode, store, staticAssets? })
|
||
→ 注册 SIGINT/SIGTERM shutdown(engine.stop + store.close)
|
||
|
||
运行时:
|
||
定时器(tick) → ProbeEngine.probeGroup()
|
||
→ checkerRegistry.get(target.type).execute()
|
||
→ runner/*/expect.ts 校验 → engine.writeResult() → store.insertCheckResult()
|
||
数据清理: 定时 prune(retentionMs),每小时执行一次
|
||
|
||
HTTP 请求:
|
||
Request → app.ts(路由分发) → routes/*.ts(handler)
|
||
→ middleware.ts(参数校验) → helpers.ts(响应格式化) → Response
|
||
```
|
||
|
||
### 1.2 库使用优先级
|
||
|
||
后端代码开发遵循严格的库选择顺序:
|
||
|
||
| 优先级 | 来源 | 典型用途 |
|
||
| ------ | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||
| 1 | Bun 内置 API | `Bun.serve`、`bun:sqlite`、`Bun.spawn`、`Bun.file`、`Bun.YAML` |
|
||
| 2 | es-toolkit | 类型判断(`isPlainObject`/`isNil`/`isEmptyObject`)、深度比较(`isEqual`)、错误判断(`isError`)、并发控制(`Semaphore`)、集合操作(`groupBy`) |
|
||
| 3 | 标准 Web API | `Object.fromEntries`、`Headers`、`fetch`、`AbortController` |
|
||
| 4 | 主流三方库 | cheerio(HTML 解析)、xpath + @xmldom/xmldom(XML 解析) |
|
||
| 5 | 自行实现 | 仅在以上都无法满足时(如 `parseDuration`、`parseSize`、`evaluateJsonPath` 等专项逻辑) |
|
||
|
||
**原则**:新增依赖前先检查上述每一层级是否已有可用方案。禁止随意引入新依赖。
|
||
|
||
### 1.3 API 路由开发
|
||
|
||
路由文件位于 `src/server/routes/`,每个端点一个文件。handler 函数签名因端点而异:
|
||
|
||
```typescript
|
||
// 无 store 的路由(健康检查不依赖数据库)
|
||
export function handleHealth(method: string, mode: RuntimeMode): Response;
|
||
|
||
// 仅有 store 的路由
|
||
export function handleSummary(store: ProbeStore, method: string, mode: RuntimeMode): Response;
|
||
export function handleTargets(store: ProbeStore, method: string, mode: RuntimeMode): Response;
|
||
|
||
// 带 target ID 和查询参数的路由
|
||
export function handleHistory(idStr: string, url: URL, method: string, store: ProbeStore, mode: RuntimeMode): Response;
|
||
export function handleTrend(idStr: string, url: URL, method: string, store: ProbeStore, mode: RuntimeMode): Response;
|
||
```
|
||
|
||
**请求处理流程**:
|
||
|
||
1. `app.ts` 的 `createFetchHandler` 作为总入口,根据 URL pattern 匹配路由
|
||
2. `/health` 路由独立处理,不经过 `guardGetHead`(使用 `helpers.ts` 的 `allowsGetHead` 自行校验方法)
|
||
3. `/api/*` 路由统一经过 `guardGetHead` 做方法检查(仅允许 GET/HEAD),返回 `null` 表示通过
|
||
4. 各 handler 内部通过 `middleware.ts` 提供的 `validateTargetId`、`validateTimeRange`、`validatePagination` 做参数校验,`pageSize` 最大值为 `200`
|
||
5. 校验函数返回 `Response` 实例表示校验失败(直接返回),返回数据对象表示通过
|
||
6. 业务逻辑通过 `store` 查询数据,用 `helpers.ts` 的 `jsonResponse`、`mapCheckResult`、`formatDuration` 等格式化输出
|
||
|
||
**新增路由步骤**:
|
||
|
||
1. 在 `src/server/routes/` 下创建 `<name>.ts`
|
||
2. 实现 handler 函数并 export
|
||
3. 在 `app.ts` 的 `createFetchHandler` 中注册路径匹配和调用
|
||
4. 在 `tests/server/app.test.ts` 中添加对应测试
|
||
|
||
### 1.4 共享工具
|
||
|
||
- **`helpers.ts`**:跨路由共用的响应工具函数
|
||
- `allowsGetHead(method)` — 判断是否为 GET/HEAD 方法
|
||
- `createApiError(error, status)` — 构造 API 错误体
|
||
- `createHeaders(mode, init)` — 创建响应 Headers(生产模式附加安全头)
|
||
- `createHealthResponse()` — 构造健康检查响应
|
||
- `formatDuration(ms)` — 毫秒转为可读时长字符串
|
||
- `jsonResponse(body, options)` — JSON 响应构造(自动处理 HEAD 空体)
|
||
- `mapCheckResult(row)` — 数据库行转 API CheckResult
|
||
- `methodNotAllowedResponse(allow, mode)` — 构造 405 响应
|
||
- **`middleware.ts`**:API 参数校验函数(`guardGetHead`、`validateTargetId`、`validateTimeRange`、`validatePagination`,其中 `pageSize` 上限为 `200`)
|
||
- **`static.ts`**:生产模式下的静态资源服务与 SPA fallback
|
||
|
||
### 1.5 类型定义规范
|
||
|
||
- **共享类型**以 `src/shared/api.ts` 为唯一源头,前后端共同引用
|
||
- 前端不得 `import src/server/` 下的任何文件
|
||
- **严格联合类型**优先于宽类型:如 `phase: "status" | "duration" | ...` 而非 `phase: string`
|
||
- **后端内部扩展**:`checker/types.ts` 中 `CheckResult` 通过 `extends` 共享版本的 `ApiCheckResult` 增加 `targetName` 等内部字段
|
||
- 存储层类型(`StoredTarget`、`StoredCheckResult`)独立定义,与 API 类型分离
|
||
- **Checker 类型分层**:
|
||
- `checker/types.ts` 定义 base interface(`ResolvedTargetBase`、`RawTargetConfig`、`DefaultsConfig`),使用 index signature 支持扩展
|
||
- 各 checker 在自己的 `types.ts` 中定义具体类型(如 `ResolvedHttpTarget`、`ResolvedCommandTarget`),满足 base interface 约束
|
||
- 中间层(engine、store、config-loader)只依赖 base interface,不感知具体 checker 类型
|
||
- `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`。
|
||
|
||
`config-loader.ts` 只负责 YAML 解析、契约调度、公共语义校验和最终运行期解析;checker 专属规则必须下沉到对应 checker 的 `schema.ts` 和 `validate.ts`。
|
||
|
||
`ResolvedConfig` 包含以下字段:
|
||
|
||
| 字段 | 来源 | 默认值 |
|
||
| --------------------- | -------------------------------------------------- | ---------------- |
|
||
| `configDir` | 配置文件所在目录 | — |
|
||
| `dataDir` | `server.dataDir`(基于配置文件目录解析为绝对路径) | `configDir/data` |
|
||
| `host` | `server.host` | `127.0.0.1` |
|
||
| `port` | `server.port` | `3000` |
|
||
| `maxConcurrentChecks` | `runtime.maxConcurrentChecks` | `20` |
|
||
| `retentionMs` | `runtime.retention` | `7d` |
|
||
| `targets` | `targets[]` 经 resolve 后 | — |
|
||
|
||
契约层使用 `src/server/checker/schema/` 中的 TypeBox fragments 生成 JSON Schema,并用 Ajv 执行启动期校验。Ajv 必须保持严格拒绝模式:`allErrors: true`、不启用类型强制转换、不注入默认值、不自动删除未知字段。
|
||
|
||
默认对象策略是 `additionalProperties: false`。只有明确声明的动态键值表可以开放任意键名,例如 `http.headers`、`defaults.http.headers`、`expect.headers`、`command.env`。
|
||
|
||
契约校验和语义 validator 都必须返回 `ConfigValidationIssue[]`,不要在 validator 内直接拼接最终用户错误字符串。最终错误由 `formatConfigIssues()` 统一渲染,错误路径需要尽量包含 `targetName` 或 `defaults`/root 路径。
|
||
|
||
新增或修改配置字段时必须同步更新:TypeBox schema fragments、`probe-config.schema.json` 导出、对应语义 validator、单元测试和 README/DEVELOPMENT 用户文档。提交前运行 `bun run schema:check` 确认导出 schema 与 fragments 一致。
|
||
|
||
### 1.7 开发新 Checker
|
||
|
||
Checker 是本项目的核心扩展单元。架构设计目标是**完全内聚**:每个 checker 是 `src/server/checker/runner/<type>/` 下的自包含目录,包含该 checker 所需的全部类型、schema、校验、执行逻辑和断言。新增一个 checker 只需创建一个目录并在 `runner/index.ts` 中添加一行注册。
|
||
|
||
以下以新增 `tcp` 类型 checker 为例,说明完整的开发步骤。
|
||
|
||
#### 1.7.1 架构总览
|
||
|
||
```
|
||
checkerRegistry(单例)
|
||
│
|
||
├── runner/index.ts ← 显式数组注册,新增 checker 只需一行
|
||
│ ├── new HttpChecker()
|
||
│ ├── new CommandChecker()
|
||
│ └── new TcpChecker() ← 新增
|
||
│
|
||
├── schema/builder.ts ← 自动遍历 registry 生成全量 JSON Schema
|
||
├── schema/validate.ts ← 自动遍历 registry 构建 Ajv 校验
|
||
├── config-loader.ts ← 自动遍历 registry 调用 validate() + resolve()
|
||
├── engine.ts ← 自动按 target.type 分发到 execute()
|
||
└── store.ts ← 自动按 target.type 分发到 serialize()
|
||
```
|
||
|
||
每个 checker 目录的标准文件结构:
|
||
|
||
| 文件 | 职责 |
|
||
| ------------- | ------------------------------------------------------------------------------------- |
|
||
| `index.ts` | 模块入口,re-export Checker 类 |
|
||
| `types.ts` | Checker 专属类型(ResolvedXxxTarget、XxxTargetConfig、XxxExpectConfig 等) |
|
||
| `schema.ts` | TypeBox 契约 schema(config / defaults / expect 三部分) |
|
||
| `validate.ts` | 启动期语义校验(JSON Schema 无法表达的规则) |
|
||
| `execute.ts` | Checker 类:resolve(默认值合并 + 解析)、execute(执行检查)、serialize(DB 持久化) |
|
||
| `expect.ts` | Checker 专用断言函数 |
|
||
| `*.ts` | 其他 checker 专属逻辑(如 http/body.ts、command/text.ts) |
|
||
|
||
#### 1.7.2 步骤一:创建 Checker 目录与类型
|
||
|
||
在 `src/server/checker/runner/tcp/types.ts` 中定义 checker 专属类型(参考 `http/types.ts`、`command/types.ts`):
|
||
|
||
- `XxxTargetConfig` — YAML 原始配置类型
|
||
- `XxxExpectConfig` — expect 字段类型
|
||
- `XxxDefaultsConfig` — defaults 专属字段类型
|
||
- `ResolvedXxxTarget extends ResolvedTargetBase` — resolve 后的完整类型,含 `type: "xxx"` 字面量
|
||
|
||
**注意**:不需要修改顶层 `checker/types.ts`。base interface 使用 index signature(`[key: string]: unknown`),checker 专属类型通过 `extends ResolvedTargetBase` 自动兼容。
|
||
|
||
#### 1.7.3 步骤二:创建 TypeBox 契约 Schema
|
||
|
||
在 `src/server/checker/runner/tcp/schema.ts` 中定义 `CheckerSchemas`(config / defaults / expect 三部分)。参考 `http/schema.ts`、`command/schema.ts`,使用 `schema/fragments.ts` 中的共享片段。
|
||
|
||
**可复用的共享 fragments**(来自 `schema/fragments.ts`):
|
||
|
||
| Fragment | 用途 |
|
||
| ---------------------------- | -------------------------------------------------------- |
|
||
| `durationSchema` | 时长字符串(`"30s"`、`"5m"`、`"2h"`、`"7d"`、`"500ms"`) |
|
||
| `sizeSchema` | 大小单位(字符串如 `"10MB"` 或整数) |
|
||
| `statusCodePatternSchema` | 状态码(`100`-`599` 或 `"2xx"`) |
|
||
| `stringMapSchema` | `Record<string, string>`(用于 headers / env) |
|
||
| `createBodyRulesSchema()` | body 规则数组(json/css/xpath/contains/regex) |
|
||
| `createTextRulesSchema()` | 文本规则数组(stdout/stderr) |
|
||
| `createPureOperatorSchema()` | 操作符对象 |
|
||
| `operatorProperties()` | 所有操作符字段的 Record |
|
||
|
||
**注意**:默认对象策略为 `additionalProperties: false`。只有明确的动态键值表(如 `http.headers`、`command.env`)可以开放任意键名。
|
||
|
||
#### 1.7.4 步骤三:实现语义校验
|
||
|
||
在 `src/server/checker/runner/tcp/validate.ts` 中实现 JSON Schema 无法表达的语义规则(参考 `http/validate.ts`、`command/validate.ts`)。函数签名统一为:
|
||
|
||
```typescript
|
||
export function validateTcpConfig(input: CheckerValidationInput): ConfigValidationIssue[];
|
||
```
|
||
|
||
**共享校验工具**(`expect/validate-operator.ts`):
|
||
|
||
| 函数 | 用途 |
|
||
| --------------------------------------------------------- | ---------------------- |
|
||
| `validateOperatorObject(ops, path, targetName, options?)` | 校验操作符对象 |
|
||
| `isJsonValue(value)` | 判断是否为合法 JSON 值 |
|
||
|
||
#### 1.7.5 步骤四:实现 Checker 类
|
||
|
||
在 `src/server/checker/runner/tcp/execute.ts` 中实现 `CheckerDefinition` 接口的全部成员(参考 `http/execute.ts`、`command/execute.ts`):
|
||
|
||
```
|
||
TcpChecker implements Checker
|
||
readonly configKey ← "tcp"(对应 YAML 中的 target.tcp 字段)
|
||
readonly type ← "tcp"
|
||
readonly schemas ← tcpCheckerSchemas
|
||
|
||
validate(input) ← 调用 validateTcpConfig(input)
|
||
resolve(target, ctx)← 默认值合并 + 解析,返回 satisfies ResolvedTcpTarget
|
||
execute(target, ctx)← 执行检查,返回 CheckResult
|
||
serialize(target) ← 返回 { config, target } 用于 DB 持久化
|
||
```
|
||
|
||
**`resolve()` 规范**:
|
||
|
||
- 只做默认值合并、路径解析、单位转换,**不执行校验**
|
||
- 返回 `satisfies ResolvedXxxTarget` 确保类型正确
|
||
- 通过 `context.defaults[this.configKey]` 访问 checker 专属默认值(需 `as` 断言为具体类型)
|
||
|
||
**`execute()` 规范**:
|
||
|
||
- 始终记录 `timestamp`(ISO 字符串)和 `start = performance.now()`
|
||
- 通过 `ctx.signal`(`AbortSignal`)支持超时取消
|
||
- 首个 expect 失败即停止,返回带 `failure` 的结果
|
||
- 成功时 `failure: null, matched: true`
|
||
- 异常时使用 `errorFailure(phase, path, message)` 构造 failure
|
||
- 不匹配时使用 `mismatchFailure(phase, path, expected, actual, message)` 构造 failure
|
||
|
||
**可用的共享断言工具**(`checker/expect/`):
|
||
|
||
| 模块 | 函数 | 用途 |
|
||
| ---------------------- | ----------------------------------------------------- | ------------------------------------- |
|
||
| `failure.ts` | `errorFailure(phase, path, msg)` | 构造错误类型 failure |
|
||
| `failure.ts` | `mismatchFailure(phase, path, expected, actual, msg)` | 构造不匹配类型 failure |
|
||
| `failure.ts` | `truncateActual(value, maxLen?)` | 截断过长的 actual 值(默认 200 字符) |
|
||
| `duration.ts` | `checkDuration(ms, maxMs?)` | 耗时断言 |
|
||
| `operator.ts` | `applyOperator(actual, operator)` | 执行单个操作符比较 |
|
||
| `operator.ts` | `evaluateJsonPath(json, path)` | JSONPath 提取 |
|
||
| `validate-operator.ts` | `validateOperatorObject(ops, path, name)` | 操作符语义校验 |
|
||
|
||
**Checker 专属断言**(如需要)放在同目录的 `expect.ts` 中,参考 `http/expect.ts`(checkStatus、checkHeaders)和 `command/expect.ts`(checkExitCode)。
|
||
|
||
#### 1.7.6 步骤五:创建模块入口并注册
|
||
|
||
创建 `src/server/checker/runner/tcp/index.ts`(re-export Checker 类)。
|
||
|
||
在 `src/server/checker/runner/index.ts` 中添加一行导入和一个数组元素(参考现有 HttpChecker/CommandChecker)。
|
||
|
||
注册后,以下管线会通过 registry 自动委托,**无需新增 type 分支**:
|
||
|
||
| 模块 | 自动行为 |
|
||
| -------------------- | ------------------------------------------------------------------------ |
|
||
| `schema/builder.ts` | 遍历 registry 生成全量 JSON Schema(defaults.tcp + target.tcp + expect) |
|
||
| `schema/validate.ts` | 按注册 checker 构建 Ajv 校验,自动识别 `type: tcp` |
|
||
| `config-loader.ts` | 遍历 registry 调用每个 checker 的 `validate()` + `resolve()` |
|
||
| `engine.ts` | 按 `target.type` 从 registry 取对应 checker 执行 `execute()` |
|
||
| `store.ts` | 按 `target.type` 从 registry 取对应 checker 执行 `serialize()` |
|
||
|
||
注意:自动适配指上述中间层不需要新增 `switch/case` 或类型分支;开发者仍需按后续步骤更新配置示例、文档和测试。
|
||
|
||
#### 1.7.7 步骤六:确认前端类型展示
|
||
|
||
前端通过 `/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 — 完整验证(含 build + smoke test)
|
||
□ README.md — 用户文档
|
||
□ DEVELOPMENT.md — 项目结构目录树
|
||
```
|
||
|
||
### 1.8 数据存储规范
|
||
|
||
基于 `bun:sqlite`,WAL 模式运行,数据库文件位于配置的 `dataDir` 下。
|
||
|
||
**核心方法**:
|
||
|
||
| 方法 | 用途 |
|
||
| ----------------------- | ---------------------------------------------------------------- |
|
||
| `syncTargets(targets)` | 启动期同步 targets(基于 name 做 upsert + delete 事务) |
|
||
| `insertCheckResult()` | 写入单条检查结果 |
|
||
| `getTargets()` | 查询全部 targets(default 分组优先排序) |
|
||
| `getLatestChecksMap()` | 批量获取每个 target 的最新检查结果(单次 SQL 聚合) |
|
||
| `getAllTargetStats()` | 批量获取每个 target 的可用率统计(GROUP BY 聚合) |
|
||
| `getAllRecentSamples()` | 批量获取每个 target 的最近 N 条采样(window function) |
|
||
| `getSummary()` | 获取总览统计(基于 `getLatestChecksMap` 内存计算 up/down/total) |
|
||
| `getTrend()` | 获取按小时聚合的趋势数据 |
|
||
| `getHistory()` | 分页查询历史记录 |
|
||
| `getRecentSamples()` | 获取最近 N 条采样数据(用于状态条渲染) |
|
||
| `prune(retentionMs)` | 按 retention 策略清理过期数据(由 engine 定时调用) |
|
||
|
||
**Statement 使用规范**:
|
||
|
||
| 场景 | 方式 | 原因 |
|
||
| -------------- | -------------------------------------- | ---------------------------------------- |
|
||
| 单次读/写 | `this.db.query(sql).get()/all()/run()` | bun:sqlite 内置 statement 缓存,自动复用 |
|
||
| 事务内多次复用 | `this.db.prepare(sql)` 缓存为局部变量 | 事务闭包中需要持有引用 |
|
||
|
||
**查询优化**:
|
||
|
||
- 避免 N+1 查询:批量场景优先用单次 SQL 聚合(GROUP BY、子查询 JOIN)+ 内存组装
|
||
- 新增批量查询方法时必须编写对应单元测试
|
||
- `getSummary()` 和 `GET /api/targets` 的响应组装已通过 `getLatestChecksMap` + `getAllTargetStats` + `getAllRecentSamples` 实现批量查询
|
||
|
||
**Schema**:
|
||
|
||
- `targets` 表:name(UNIQUE)、type、target(展示摘要)、config(JSON)、interval_ms、timeout_ms、expect(JSON)、grp
|
||
- `check_results` 表:target_id(FK CASCADE)、timestamp、matched(0/1)、duration_ms、status_detail、failure(JSON)
|
||
- 复合索引:`(target_id, timestamp)`
|
||
|
||
### 1.9 拨测引擎
|
||
|
||
- **调度**:`ProbeEngine` 用 `es-toolkit/groupBy` 按 interval 分组,每组独立 `setInterval` 定时触发
|
||
- **并发控制**:`es-toolkit/Semaphore` 限制全局最大并发数(`maxConcurrentChecks`,默认 20),`acquire()` 阻塞等待
|
||
- **Runner 选择**:`engine.runCheck()` 通过 `checkerRegistry.get(target.type)` 获取 checker,并调用 `checker.execute(target, { signal })`
|
||
- **超时控制**:`ProbeEngine` 为每次检查创建 `AbortController` 并按 `target.timeoutMs` 触发 abort;checker 必须使用 `CheckerContext.signal` 感知超时,HTTP 将 signal 传给 `fetch()`,Command 在 signal abort 时 `proc.kill()`
|
||
- **结果写入**:检查结果通过 `store.insertCheckResult()` 写入 SQLite,engine 通过 `targetNameToId` 缓存 name→id 映射
|
||
- **异常可观测**:`probeGroup()` 对 `Promise.allSettled` 的 rejected 结果通过索引关联 target,并写入 `phase:"internal"` 的失败记录
|
||
- **数据清理**:当 `retentionMs > 0` 时,engine 启动时立即执行一次 `store.prune()`,之后每小时定时执行,按 `timestamp` 清理过期数据
|
||
- **生命周期**:`start()`/`stop()` 管理定时器(含调度定时器和清理定时器),`stop()` 清理所有 `setInterval`
|
||
|
||
### 1.10 expect 断言系统
|
||
|
||
两层模型:**观测值收集** → **规则校验**。共享断言基础设施位于 `checker/expect/`,checker 专属断言位于各自目录。
|
||
|
||
**HTTP 校验流程**:
|
||
|
||
```
|
||
HttpChecker.execute → 收集观测(statusCode/headers)
|
||
→ status → headers → (early duration) → body(按需) → (final duration)
|
||
→ 首个失败即停止,返回 CheckFailure
|
||
```
|
||
|
||
HTTP checker 的 `durationMs` 覆盖完整执行(含重定向、响应体读取、解码和 expect 校验)。status 或 headers 失败时不读取 body;进入 body 前若已超过 `maxDurationMs`,直接返回 duration failure。
|
||
|
||
**Command 校验流程**:
|
||
|
||
```
|
||
CommandChecker.execute → 收集观测(exitCode/stdout/stderr/durationMs)
|
||
→ exitCode → duration → stdout → stderr
|
||
→ 首个失败即停止
|
||
```
|
||
|
||
**Body 规则类型**(`runner/http/body.ts`):
|
||
|
||
- `contains`:文本包含匹配
|
||
- `regex`:正则表达式匹配(注意:body 正则字段为 `regex`,不是 `match`,启动期会拒绝嵌套量词等 ReDoS 风险模式)
|
||
- `json`:JSONPath 提取 + 操作符比较(使用 `es-toolkit/isPlainObject` 区分纯值和操作符)
|
||
- `css`:cheerio CSS 选择器 + 操作符比较
|
||
- `xpath`:XPath 节点提取 + 操作符比较
|
||
|
||
**文本规则**(`runner/command/text.ts`):stdout/stderr 文本匹配,支持 `contains`、`match`(正则)、操作符比较
|
||
|
||
**操作符**(`expect/operator.ts`):`equals`(深度比较,`es-toolkit/isEqual`)、`contains`、`match`(正则,启动期通过 `expect/redos.ts` 拒绝 ReDoS 风险模式)、`empty`(`isNil`+`isEmptyObject`)、`exists`、`gte`/`lte`/`gt`/`lt`
|
||
|
||
启动期语义校验会对 HTTP body `regex` 规则和所有 `match` operator 执行静态 ReDoS 检测,常见的嵌套量词模式如 `(a+)+`、`(\\d+)*x` 会被拒绝,避免运行期正则在大响应体上阻塞事件循环。
|
||
|
||
### 1.11 错误模式
|
||
|
||
- **API 错误**:`{ error: "描述", status: <code> }`,状态码 400/404/405/503
|
||
- **CheckFailure**:`{ kind: "error"|"mismatch", phase, path, expected?, actual?, message }`
|
||
- **错误处理**:expect 校验失败记录首个失败原因;网络/超时/进程崩溃统一为 `kind:"error"`,请求/TLS/timeout 错误归属 `phase:"request"`,body 超限/解码/解析错误归属 `phase:"body"`
|
||
- **日志**:解析失败等非致命异常用 `console.warn`,启动失败用 `console.error` + `process.exit(1)`
|
||
|
||
### 1.12 测试规范
|
||
|
||
- 测试目录 `tests/` 镜像 `src/` 目录结构,但共享模块的测试集中放在 `tests/server/checker/runner/shared/` 下
|
||
- `tests/server/checker/runner/shared/failure.test.ts` ↔ `src/server/checker/expect/failure.ts`
|
||
- `tests/server/checker/runner/shared/duration.test.ts` ↔ `src/server/checker/expect/duration.ts`
|
||
- `tests/server/checker/runner/shared/operator.test.ts` ↔ `src/server/checker/expect/operator.ts`
|
||
- `tests/server/checker/runner/shared/body.test.ts` ↔ `src/server/checker/runner/http/body.ts`
|
||
- `tests/server/checker/runner/shared/text.test.ts` ↔ `src/server/checker/runner/command/text.ts`
|
||
- 使用 `bun:test` 框架(`describe`/`test`/`expect`),测试数据库用临时目录 + `tmpdir()`
|
||
- 新增 store 方法必须编写单元测试;新增 API 端点必须在 `app.test.ts` 中添加集成测试
|
||
- 测试后清理:`afterAll` 中 `store.close()` + `rm(tempDir, { recursive: true })`
|
||
|
||
---
|
||
|
||
## 二、前端开发指引
|
||
|
||
### 2.1 技术栈概览
|
||
|
||
| 层面 | 技术 | 用途 |
|
||
| ------ | --------------------------------------------------- | ---------------------------- |
|
||
| 框架 | React 19 | UI 组件开发 |
|
||
| 构建 | Vite 8 | 开发服务与生产构建 |
|
||
| 语言 | TypeScript 6 | 类型安全 |
|
||
| UI 库 | TDesign React + tdesign-icons-react | UI 组件与图标 |
|
||
| 数据层 | TanStack Query (React Query) + React Query Devtools | 服务端状态管理与自动轮询 |
|
||
| 图表 | Recharts | 拨测趋势折线图与状态环状图 |
|
||
| 路由 | 无(单页面 Dashboard) | 仅需 Drawer/Tab 做页面内导航 |
|
||
|
||
**不引入的依赖**:React Router(单页面场景不需要)、状态管理库(TanStack Query 即服务端状态层,组件内用 `useState` 足够)
|
||
|
||
### 2.2 组件树与数据流
|
||
|
||
```
|
||
main.tsx
|
||
└── StrictMode
|
||
└── ErrorBoundary(React 错误边界)
|
||
└── QueryClientProvider(TanStack Query 全局挂载)
|
||
├── App(根组件)
|
||
│ ├── SummaryCards(总览统计卡片)
|
||
│ │ └── useSummary() ─── GET /api/summary(8s 轮询)
|
||
│ └── TargetBoard(目标列表)
|
||
│ ├── useTargets() ─── GET /api/targets(8s 轮询)
|
||
│ ├── useMeta() ────── GET /api/meta(应用生命周期内缓存)
|
||
│ └── TargetGroup[](按 group 字段分组)
|
||
│ └── PrimaryTable ← createTargetTableColumns(checkerTypes)
|
||
│ └── TargetDetailDrawer(目标详情抽屉)
|
||
│ └── useTargetDetail() ── 按需发起 trend + history 查询
|
||
│ ├── OverviewTab → Statistic + TrendChart + StatusDonut + Descriptions
|
||
│ └── HistoryTab → PrimaryTable(分页历史记录)
|
||
└── ReactQueryDevtools(开发工具,仅开发环境)
|
||
```
|
||
|
||
**数据层架构**:
|
||
|
||
```
|
||
hooks/use-queries.ts(全局面板级查询)
|
||
├── queryKeys(summary/targets/meta 结构化 query key)
|
||
├── useSummary() → /api/summary(8s 自动轮询)
|
||
├── useTargets() → /api/targets(8s 自动轮询)
|
||
└── useMeta() → /api/meta(staleTime: Infinity)
|
||
|
||
hooks/use-target-detail.ts(Drawer 状态与详情级条件查询)
|
||
├── 内部复用 useTargets() 的缓存来查找 selectedTarget
|
||
├── useQuery(/api/targets/:id/trend)(条件查询:enabled 仅当 Drawer 打开且时间范围有效)
|
||
└── useQuery(/api/targets/:id/history)(条件查询:含分页)
|
||
```
|
||
|
||
### 2.3 TanStack Query 数据层
|
||
|
||
#### Query Key 规范
|
||
|
||
```typescript
|
||
const queryKeys = {
|
||
summary: () => ["summary"] as const,
|
||
targets: () => ["targets"] as const,
|
||
meta: () => ["meta"] as const,
|
||
trend: (targetId: number, from: string, to: string) => ["trend", targetId, from, to] as const,
|
||
history: (targetId: number, from: string, to: string, page: number) => ["history", targetId, from, to, page] as const,
|
||
};
|
||
```
|
||
|
||
- Key 使用 **structured array**(非字符串),以便精确匹配和按 prefix 失效
|
||
- 使用 `as const` 保持字面量类型
|
||
- 排序:scope → id → 参数(粒度从粗到细)
|
||
|
||
#### 查询配置规范
|
||
|
||
```typescript
|
||
// 全局面板级查询(需要持续刷新)
|
||
useQuery({
|
||
queryKey: queryKeys.summary(),
|
||
queryFn: () => fetchJson<SummaryResponse>("/api/summary"),
|
||
refetchInterval: 8000, // 自动轮询间隔
|
||
refetchIntervalInBackground: false, // 切后台不轮询
|
||
});
|
||
|
||
// 详情级查询(按需加载)
|
||
useQuery({
|
||
queryKey: selectedTargetId ? queryKeys.trend(id, from, to) : ["trend", "disabled"],
|
||
queryFn: () => fetchJson(`/api/targets/${id}/trend?...`),
|
||
enabled: selectedTargetId !== null && !!timeFrom && !!timeTo, // 条件查询
|
||
});
|
||
```
|
||
|
||
#### fetch 封装
|
||
|
||
```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` | 根组件,编排全局状态与布局 |
|
||
| `ErrorBoundary` | `components/ErrorBoundary.tsx` | React 错误边界,捕获渲染异常并展示降级 UI |
|
||
| `SummaryCards` | `components/SummaryCards.tsx` | 总览统计卡片(全部/正常/异常) |
|
||
| `TargetBoard` | `components/TargetBoard.tsx` | 按分组渲染目标表格列表 |
|
||
| `TargetGroup` | `components/TargetGroup.tsx` | 单个分组标题 + PrimaryTable |
|
||
| `TargetDetailDrawer` | `components/TargetDetailDrawer.tsx` | 目标详情抽屉壳、时间选择和 Tab 切换 |
|
||
| `OverviewTab` | `components/OverviewTab.tsx` | 目标详情概览(统计/趋势/状态分布/信息) |
|
||
| `HistoryTab` | `components/HistoryTab.tsx` | 目标历史记录表格和分页 |
|
||
| `TrendChart` | `components/TrendChart.tsx` | Recharts 双轴折线图(耗时/可用率) |
|
||
| `StatusDonut` | `components/StatusDonut.tsx` | Recharts 环状图(UP/DOWN 分布) |
|
||
| `StatusDot` | `components/StatusDot.tsx` | 圆形状态指示点(绿/红) |
|
||
| `StatusBar` | `components/StatusBar.tsx` | 最近采样状态条(多色块) |
|
||
| `GroupHeader` | `components/GroupHeader.tsx` | 分组标题(名称 + 统计) |
|
||
|
||
### 2.5 新增功能开发步骤
|
||
|
||
以"新增一个详情页面 Tab"为例:
|
||
|
||
1. **确认数据需求**:是已有 API 数据还是需要新端点?
|
||
- 如有新端点,先在 `src/server/routes/` 添加,参考 [1.3 新增路由步骤](#13-api-路由开发)
|
||
- 如有新字段,更新 `src/shared/api.ts` 类型定义
|
||
2. **实现 hooks**:全局查询放在 `src/web/hooks/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`)定义全局页面结构
|
||
- 组件修饰类(`.status-dot--up`、`.latency-ok`)为自定义视觉组件提供样式变体
|
||
- TDesign 表格行高亮(`.row-down`)通过 `rowClassName` prop 应用
|
||
|
||
### 2.7 前端测试规范
|
||
|
||
- 测试目录:`tests/web/`,结构对应 `src/web/`
|
||
- 重点测试 **constants/** 中的纯函数(排序器、筛选器、颜色阈值、类型映射等)
|
||
- 使用 `bun:test` 框架
|
||
|
||
---
|
||
|
||
## 三、项目运行、集成与打包
|
||
|
||
### 3.1 开发期运行
|
||
|
||
#### 同时启动前后端
|
||
|
||
```bash
|
||
bun run dev probes.yaml
|
||
```
|
||
|
||
`scripts/dev.ts` 通过 `Bun.spawn` 同时启动两个子进程:
|
||
|
||
```
|
||
bun run dev probes.yaml
|
||
├── bun run dev:server probes.yaml → Bun HTTP 后端(默认 3000 端口)
|
||
└── bun run dev:web → Vite 前端开发服务器(5173 端口)
|
||
```
|
||
|
||
- 任一子进程退出会导致整体退出
|
||
- `SIGINT`/`SIGTERM` 信号会同时终止两个子进程
|
||
- `BACKEND_PORT` 环境变量可覆盖后端端口
|
||
|
||
#### 分别启动
|
||
|
||
```bash
|
||
# 启动后端(含 watch 模式自动重启)
|
||
bun run dev:server probes.yaml
|
||
|
||
# 另开终端启动前端
|
||
bun run dev:web
|
||
```
|
||
|
||
### 3.2 前后端集成方式
|
||
|
||
#### 开发期代理
|
||
|
||
Vite 配置了开发代理(`vite.config.ts`)和代码分割策略:
|
||
|
||
```typescript
|
||
// 开发代理
|
||
server: {
|
||
proxy: {
|
||
"/api": {
|
||
target: `http://127.0.0.1:${backendPort}`,
|
||
changeOrigin: true,
|
||
},
|
||
},
|
||
}
|
||
|
||
// 生产代码分割(rolldownOptions.output.codeSplitting.groups)
|
||
// vendor-react: react/react-dom/scheduler
|
||
// vendor-tdesign: tdesign
|
||
// vendor-chart: recharts/d3-*
|
||
```
|
||
|
||
前端访问 `/api/*` 时,Vite 开发服务器自动转发到后端 `http://127.0.0.1:${backendPort}`,无需 CORS 配置。
|
||
|
||
前端开发地址为 `http://127.0.0.1:5173`(严格端口 `strictPort: true`)。
|
||
|
||
后端在开发模式下不提供静态资源服务,访问 `http://127.0.0.1:3000` 会提示"请通过 Vite 前端地址访问"。
|
||
|
||
#### 生产期集成
|
||
|
||
生产可执行文件是单体应用:前端静态资源嵌入 binary(通过 `StaticAssets` 接口:`files: Record<string, Blob>` + `indexHtml: Blob`),后端同时提供 API 和静态文件服务。
|
||
|
||
```
|
||
./dist/dial-server probes.yaml
|
||
|
||
启动后:
|
||
访问 http://127.0.0.1:3000/ → 返回前端 SPA(index.html)
|
||
访问 http://127.0.0.1:3000/api/* → 返回后端 API
|
||
访问 /assets/* → 返回带不可变缓存的静态资源
|
||
```
|
||
|
||
SPA fallback 逻辑(`src/server/static.ts`):
|
||
|
||
- `/` → index.html
|
||
- 匹配 `/assets/*` → 返回对应文件(未匹配则 404)
|
||
- 其他路径(如 `/dashboard`)→ fallback 到 index.html(SPA 路由)
|
||
|
||
### 3.3 构建打包
|
||
|
||
#### 构建命令
|
||
|
||
```bash
|
||
bun run build
|
||
```
|
||
|
||
#### 构建流程详解
|
||
|
||
`scripts/build.ts` 执行以下步骤:
|
||
|
||
```
|
||
1. vite build
|
||
├── 入口:src/web/index.html
|
||
└── 输出:dist/web/(index.html + assets/)
|
||
|
||
2. 生成 .build/static-assets.ts(临时文件)
|
||
├── import Vite 产物为 Bun.file
|
||
└── 导出 staticAssets: StaticAssets 对象
|
||
|
||
3. 生成 .build/server-entry.ts(临时文件)
|
||
└── import bootstrap + staticAssets,调用 production bootstrap,作为 Bun.build 入口
|
||
|
||
4. Bun.build({ compile, minify, sourcemap: "linked" })
|
||
└── 输出:dist/dial-server(单文件可执行 binary)
|
||
```
|
||
|
||
#### 产物
|
||
|
||
| 产物 | 用途 |
|
||
| ------------------ | -------------------------- |
|
||
| `dist/dial-server` | 生产可执行文件 |
|
||
| `dist/web/` | Vite 构建产物(中间产物) |
|
||
| `.build/` | 临时生成文件(构建后清理) |
|
||
|
||
#### 构建参数
|
||
|
||
| 环境变量 | 说明 |
|
||
| --------------------------- | -------------------------------------- |
|
||
| `BUN_TARGET`/`BUILD_TARGET` | 交叉编译目标平台(如 `bun-linux-x64`) |
|
||
|
||
#### 运行可执行文件
|
||
|
||
```bash
|
||
./dist/dial-server probes.yaml
|
||
```
|
||
|
||
#### 清理
|
||
|
||
```bash
|
||
bun run clean
|
||
# 清理 dist/ 构建产物、.build/ 缓存和 *.bun-build 临时文件
|
||
```
|
||
|
||
### 3.4 开发工作流
|
||
|
||
#### 日常开发循环
|
||
|
||
```bash
|
||
bun run dev probes.yaml # 启动开发环境
|
||
# 修改代码 → Vite HMR(前端)/ bun --watch(后端自动重启)
|
||
bun run check # 提交前运行完整质量检查
|
||
```
|
||
|
||
#### 完整验证流程
|
||
|
||
```bash
|
||
bun run verify
|
||
# = bun run check + bun run build + bun run test:smoke
|
||
```
|
||
|
||
`verify` 适合 CI 或正式提交前,会完整验证类型检查、lint、格式、单元测试、构建、smoke test。
|
||
|
||
### 3.5 Smoke Test
|
||
|
||
```bash
|
||
bun run test:smoke
|
||
```
|
||
|
||
`scripts/smoke.ts` 构建后验证流程:
|
||
|
||
1. 动态分配空闲端口
|
||
2. 用临时配置文件启动 `dist/dial-server`
|
||
3. 等待健康检查通过
|
||
4. 验证所有 API 端点返回正确数据
|
||
5. 验证静态资源服务(含 SPA fallback 和 404 处理)
|
||
6. 验证安全 headers
|
||
7. 测试结束清理临时目录和进程
|
||
|
||
### 3.6 脚本说明
|
||
|
||
| 脚本 | 文件 | 说明 |
|
||
| ---------------------- | ----------------------------------- | ------------------------------- |
|
||
| `bun run dev` | `scripts/dev.ts` | 同时启动前后端开发服务 |
|
||
| `bun run build` | `scripts/build.ts` | Vite 构建 + Bun 编译可执行文件 |
|
||
| `bun run schema` | `scripts/generate-config-schema.ts` | 生成 `probe-config.schema.json` |
|
||
| `bun run schema:check` | `scripts/generate-config-schema.ts` | 检查配置 schema 导出物是否同步 |
|
||
| `bun run test:smoke` | `scripts/smoke.ts` | 构建后的端到端验证 |
|
||
| `bun run clean` | `scripts/clean.ts` | 清理构建缓存与临时文件 |
|
||
|
||
### 3.7 环境变量
|
||
|
||
| 变量 | 用途 | 默认值 |
|
||
| --------------------------- | ---------------------------------------------------- | -------- |
|
||
| `PORT`/`BACKEND_PORT` | 后端监听端口(开发期 Vite 代理目标、生产期监听端口) | `3000` |
|
||
| `BUN_TARGET`/`BUILD_TARGET` | 交叉编译目标平台(仅在 `bun run build` 时有效) | 当前平台 |
|
||
|
||
### 3.8 项目配置文件
|
||
|
||
| 文件 | 用途 |
|
||
| ---------------------- | ---------------------------------------------- |
|
||
| `package.json` | 项目信息、脚本、依赖声明 |
|
||
| `tsconfig.json` | TypeScript 配置(ESNext 模块、严格模式) |
|
||
| `vite.config.ts` | Vite 开发代理与构建配置(含代码分割策略) |
|
||
| `eslint.config.js` | ESLint 规则(含前端不得 import server 的检查) |
|
||
| `commitlint.config.js` | commitlint 提交信息格式校验 |
|
||
| `.prettierrc.json` | Prettier 格式化规则(`printWidth: 120`) |
|
||
| `.prettierignore` | Prettier 排除路径 |
|
||
| `probes.example.yaml` | 配置文件示例 |
|
||
| `opencode.json` | OpenCode 工具配置(TDesign MCP server) |
|
||
|
||
### 3.9 依赖管理
|
||
|
||
- **包管理器**:仅使用 `bun`,禁止使用 npm、pnpm、yarn
|
||
- **安装依赖**:`bun install`
|
||
- **运行工具**:使用 `bunx`,禁止使用 `npx`、`pnpx`
|
||
- **锁文件**:`bun.lock`
|
||
|
||
### 3.10 目录约定
|
||
|
||
| 目录 | 约定 |
|
||
| ------------- | -------------------------------------------- |
|
||
| `src/server/` | 后端代码,不能 import `src/web/` |
|
||
| `src/web/` | 前端代码,不能 import `src/server/` |
|
||
| `src/shared/` | 前后端共享类型,双向可引用 |
|
||
| `scripts/` | 独立运行脚本,可 import 项目源码 |
|
||
| `tests/` | 测试目录,结构镜像 src 目录 |
|
||
| `dist/` | 构建产物(gitignore) |
|
||
| `.build/` | 构建临时文件(gitignore) |
|
||
| `openspec/` | OpenSpec 变更管理与规格文档 |
|
||
| `data/` | 默认数据目录(gitignore,运行期生成 SQLite) |
|
||
|
||
---
|
||
|
||
## 代码质量
|
||
|
||
项目使用多层代码质量保障体系:ESLint 类型感知规则 + Perfectionist 导入排序 + Prettier 格式化(通过 eslint-plugin-prettier 集成至 ESLint)+ TypeScript 严格模式 + Git hooks 自动化。
|
||
|
||
```bash
|
||
bun run lint # ESLint 检查(含类型感知规则、导入排序、导入验证、Prettier 格式)
|
||
bun run format # Prettier 自动格式化
|
||
bun run schema:check # 检查 probe-config.schema.json 是否与 TypeBox fragments 同步
|
||
bun run typecheck # TypeScript 类型检查(含 noUnusedLocals、noPropertyAccessFromIndexSignature)
|
||
bun test # 运行所有测试
|
||
bun run check # 一键运行 schema:check + typecheck + lint + test
|
||
```
|
||
|
||
`check` 是日常开发推荐的质量检查命令。
|
||
|
||
### ESLint 规则
|
||
|
||
配置文件:`eslint.config.js`
|
||
|
||
| 配置来源 | 用途 |
|
||
| --------------------------------------------------------------- | -------------------------------------------------- |
|
||
| `@eslint/js` recommended | JavaScript 基础规则 |
|
||
| `typescript-eslint` recommended-type-checked | TypeScript 类型感知规则(no-floating-promises 等) |
|
||
| `typescript-eslint` stylistic-type-checked | TypeScript 风格规则(命名规范、语法选择等) |
|
||
| `eslint-plugin-perfectionist` recommended-natural | 导入语句和命名导出自动排序 |
|
||
| `eslint-plugin-import` | 导入路径验证、循环依赖检测、重复导入合并 |
|
||
| `eslint-plugin-prettier` recommended + `eslint-config-prettier` | 将 Prettier 格式集成为 ESLint 规则,禁用冲突规则 |
|
||
|
||
### Prettier 配置
|
||
|
||
配置文件:`.prettierrc.json`,通过 `eslint-plugin-prettier` 集成为 ESLint 规则(`lint` 命令同时检查格式),也可通过 `format` 命令独立运行。
|
||
|
||
显式声明所有格式化参数(`printWidth: 120`、`semi: true`、`singleQuote: false`、`trailingComma: "all"`、`endOfLine: "lf"` 等),确保不同开发环境产出完全一致的格式化结果。
|
||
|
||
### TypeScript 严格标志
|
||
|
||
| 标志 | 值 | 说明 |
|
||
| ------------------------------------ | ----- | -------------------------------------------------------------------------- |
|
||
| `strict` | true | 全局严格模式 |
|
||
| `noUnusedLocals` | true | 未使用局部变量视为错误 |
|
||
| `noUnusedParameters` | false | 保留关闭(路由 handler 统一签名需要,如 `handleXxx(store, method, mode)`) |
|
||
| `noPropertyAccessFromIndexSignature` | true | 禁止通过点号访问索引签名属性,强制使用括号语法 |
|
||
| `noUncheckedIndexedAccess` | true | 数组/Map 访问必须运行时真值检查 |
|
||
| `noImplicitOverride` | true | 子类覆盖父类方法时必须显式使用 `override` 关键字 |
|
||
| `verbatimModuleSyntax` | true | 强制 `import type` 纯类型导入 |
|
||
|
||
### Git Hooks
|
||
|
||
通过 husky 在 commit 阶段自动执行检查:
|
||
|
||
| Hook | 行为 |
|
||
| ------------ | -------------------------------------------------------------------------------------------------------------- |
|
||
| `pre-commit` | lint-staged 对变更文件运行 `eslint --fix`(TS/TSX,含 Prettier 格式修复)或 `prettier --write`(MD/JSON/YAML) |
|
||
| `commit-msg` | commitlint 校验提交信息格式 `类型: 简短描述` |
|
||
|
||
提交类型限定:`feat`、`fix`、`refactor`、`docs`、`style`、`test`、`chore`。
|
||
|
||
`bun install` 时自动初始化 husky hooks,无需手动配置。
|
||
|
||
## 测试
|
||
|
||
```bash
|
||
bun run check # 日常开发(类型检查 + lint(含格式) + 单元测试)
|
||
bun run verify # 完整验证(check + 构建 + smoke test)
|
||
```
|
||
|
||
## 已知限制
|
||
|
||
当前不做告警通知、拨测目标动态增删、认证鉴权和分布式部署。Command 类型拨测不支持 Windows 环境。
|