1
0

docs: 修正 DEVELOPMENT.md 与实际代码的差异并精简 tcp 示例

This commit is contained in:
2026-05-13 17:27:33 +08:00
parent ecd47748d2
commit 6ea185315f

View File

@@ -20,22 +20,22 @@
```text
src/
server/
app.ts Bun HTTP 路由入口(路由分发 + API 汇聚)
config.ts CLI 参数解析
dev.ts 生产/开发启动入口
server.ts HTTP server 启动工厂
helpers.ts 共享响应格式化工具(jsonResponse、createHeaders 等
middleware.ts API 参数校验中间件guardGetHead、validateTargetId
app.ts Bun HTTP 路由入口(路由分发 + API 汇聚、StaticAssets 接口定义
config.ts CLI 参数解析(仅提取配置文件路径)
dev.ts 开发模式启动入口
server.ts HTTP server 启动工厂(接收 StartServerOptions
helpers.ts 共享响应格式化工具(见下方函数清单
middleware.ts API 参数校验中间件guardGetHead、validateTargetId、validateTimeRange、validatePagination
static.ts 静态资源服务与 SPA fallback
routes/ API 路由 handler按端点拆分
health.ts GET /health
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 等 base interface
config-loader.ts YAML 配置解析、契约校验、语义校验与运行期解析
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 等)
@@ -43,48 +43,46 @@ src/
issues.ts 校验问题类型与渲染
types.ts schema 层类型
export.ts JSON Schema 文件导出
store.ts SQLite 数据存储
engine.ts 调度引擎(按 interval 分组的 es-toolkit groupBy + Semaphore 并发控制)
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
operator.ts 操作符系统applyOperator、checkExpectValue、evaluateJsonPath
types.ts ExpectResult 共享断言类型
failure.ts 失败信息构造errorFailure、mismatchFailure、truncateActual
operator.ts 操作符系统applyOperator、evaluateJsonPath
duration.ts 耗时断言checkDuration
validate-operator.ts 操作符语义校验validateOperatorObject、isJsonValue
validate-operator.ts 操作符语义校验validateOperatorObject、isJsonValue、isPlainRecord
runner/ Checker 统一抽象与注册机制
types.ts CheckerDefinition、CheckerContext、CheckerSchemas、ResolveContext
registry.ts CheckerRegistry 注册中心
index.ts 注册入口(显式数组 + 循环注册)
http/ HTTP Checker自包含模块
index.ts 模块入口re-export HttpChecker
types.ts HTTP 专属类型ResolvedHttpTarget、HttpTargetConfig、HttpExpectConfig 等)
schema.ts HTTP TypeBox 契约defaults、target.http、expect
execute.ts HttpChecker 类resolve/execute/serialize/validate
expect.ts HTTP 专用断言checkStatus、checkHeaders
validate.ts HTTP 语义校验URL、body rules、header operators 等)
body.ts Body 规则断言JSONPath/XPath/CSS/contains/regex
command/ Command Checker自包含模块
index.ts 模块入口re-export CommandChecker
types.ts Command 专属类型ResolvedCommandTarget、CommandTargetConfig、CommandExpectConfig 等)
schema.ts Command TypeBox 契约defaults、target.command、expect
execute.ts CommandChecker 类resolve/execute/serialize/validate
expect.ts Command 专用断言checkExitCode
validate.ts Command 语义校验text rules 等)
text.ts 文本规则断言checkTextRules
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
components/ UI 组件表格、分组、Drawer、状态条等
constants/ 常量定义(列配置、类型映射、排序/筛选/颜色阈值函数
hooks/ TanStack Query 数据层useTargetDetail 集成轮询/条件查询)
app.tsx 根组件(编排全局状态与布局
main.tsx 入口QueryClient 挂载 + ErrorBoundary + ReactQueryDevtools
styles.css 全局样式与自定义 CSS 变量
components/ UI 组件(见下方组件清单)
constants/ 常量与纯函数
target-type-display.ts 类型名称映射
target-table-columns.tsx 表格列定义
target-table-filters.ts 表格筛选器
target-table-sorters.ts 表格排序器
color-threshold.ts 可用率颜色阈值函数
hooks/ TanStack Query 数据层
useTargetDetail.ts 集成轮询/条件查询的组合 hook
utils/ 前端工具函数
time.ts 时间处理subtractHours
scripts/ 开发、构建、schema 生成和 smoke test 脚本
tests/ Bun test 测试
tests/ Bun test 测试(结构镜像 src 目录)
openspec/ OpenSpec 变更与规格文档
probe-config.schema.json 用户配置 JSON Schema 导出物
probe-config.schema.json 用户配置 JSON Schema 导出物(用于 IDE 自动补全和校验)
```
> **说明**`runner/http/` 和 `runner/command/` 的完整文件结构见 [1.7.1 架构总览](#171-架构总览) 中的标准文件表。
## 前后端边界
前端只通过 HTTP 调用后端API 路径为 `/api/*`。共享类型放在 `src/shared`,前端不得 import `src/server` 的运行时实现。
@@ -97,13 +95,17 @@ probe-config.schema.json 用户配置 JSON Schema 导出物
```
启动流程:
dev.ts → readRuntimeConfig(cli args) → loadConfig(yaml)
ProbeStore(db) → ProbeEngine(store, targets) → startServer(store)
dev.ts → readRuntimeConfig(cli args, 仅提取 configPath)
loadConfig(yaml) → ResolvedConfig{ host, port, dataDir, maxConcurrentChecks, retentionMs, targets }
→ ProbeStore(db) → store.syncTargets(targets)
→ ProbeEngine(store, targets, maxConcurrentChecks, retentionMs)
→ startServer({ config, mode: "development", store })
运行时:
定时器(tick) → ProbeEngine.probeGroup()
HTTP: fetcher.ts / Command: command-runner.ts
→ runner/*/expect.ts 校验 → store.insertCheckResult()
checkerRegistry.get(target.type).execute()
→ runner/*/expect.ts 校验 → engine.writeResult() → store.insertCheckResult()
数据清理: 定时 prune(retentionMs),每小时执行一次
HTTP 请求:
Request → app.ts(路由分发) → routes/*.ts(handler)
@@ -126,19 +128,29 @@ HTTP 请求:
### 1.3 API 路由开发
路由文件位于 `src/server/routes/`每个端点一个文件。handler 函数签名统一为
路由文件位于 `src/server/routes/`每个端点一个文件。handler 函数签名因端点而异
```typescript
export function handleXxx(params, store: ProbeStore, method: string, mode: RuntimeMode): Response;
// 无 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. API 路由统一经过 `guardGetHead` 做方法检查(仅允许 GET/HEAD
3. 各 handler 内部通过 `middleware.ts` 提供的 `validateTargetId``validateTimeRange``validatePagination` 做参数校验
4. 校验函数返回 `Response` 表示校验失败(直接返回),返回数据对象表示通过
5. 业务逻辑通过 `store` 查询数据,用 `helpers.ts``jsonResponse``mapCheckResult``formatDuration` 等格式化输出
2. `/health` 路由独立处理,不经过 `guardGetHead`(使用 `helpers.ts``allowsGetHead` 自行校验方法
3. `/api/*` 路由统一经过 `guardGetHead` 做方法检查(仅允许 GET/HEAD返回 `null` 表示通过
4. 各 handler 内部通过 `middleware.ts` 提供的 `validateTargetId``validateTimeRange``validatePagination` 做参数校验
5. 校验函数返回 `Response` 实例表示校验失败(直接返回),返回数据对象表示通过
6. 业务逻辑通过 `store` 查询数据,用 `helpers.ts``jsonResponse``mapCheckResult``formatDuration` 等格式化输出
**新增路由步骤**
@@ -149,7 +161,15 @@ export function handleXxx(params, store: ProbeStore, method: string, mode: Runti
### 1.4 共享工具
- **`helpers.ts`**:跨路由共用的响应工具函数`jsonResponse``createHeaders``createApiError``mapCheckResult``formatDuration``createHealthResponse`
- **`helpers.ts`**:跨路由共用的响应工具函数
- `allowsGetHead(method)` — 判断是否为 GET/HEAD 方法
- `createApiError(error, status)` — 构造 API 错误体
- `createHeaders(mode, init)` — 创建响应 Headers生产模式附加安全头
- `createHealthResponse()` — 构造健康检查响应
- `formatDuration(ms)` — 毫秒转为可读时长字符串
- `jsonResponse(body, options)` — JSON 响应构造(自动处理 HEAD 空体)
- `mapCheckResult(row)` — 数据库行转 API CheckResult
- `methodNotAllowedResponse(allow, mode)` — 构造 405 响应
- **`middleware.ts`**API 参数校验函数(`guardGetHead``validateTargetId``validateTimeRange``validatePagination`
- **`static.ts`**:生产模式下的静态资源服务与 SPA fallback
@@ -172,6 +192,18 @@ export function handleXxx(params, store: ProbeStore, method: string, mode: Runti
`config-loader.ts` 只负责 YAML 解析、契约调度、公共语义校验和最终运行期解析checker 专属规则必须下沉到对应 checker 的 `schema.ts``validate.ts`
`ResolvedConfig` 包含以下字段:
| 字段 | 来源 | 默认值 |
| --------------------- | ----------------------------- | ----------- |
| `configDir` | 配置文件所在目录 | — |
| `dataDir` | `server.dataDir` | `./data` |
| `host` | `server.host` | `127.0.0.1` |
| `port` | `server.port` | `3000` |
| `maxConcurrentChecks` | `runtime.maxConcurrentChecks` | `20` |
| `retentionMs` | `runtime.retention` | `7d` |
| `targets` | `targets[]` 经 resolve 后 | — |
契约层使用 `src/server/checker/schema/` 中的 TypeBox fragments 生成 JSON Schema并用 Ajv 执行启动期校验。Ajv 必须保持严格拒绝模式:`allErrors: true`、不启用类型强制转换、不注入默认值、不自动删除未知字段。
默认对象策略是 `additionalProperties: false`。只有明确声明的动态键值表可以开放任意键名,例如 `http.headers``defaults.http.headers``expect.headers``command.env`
@@ -217,72 +249,18 @@ checkerRegistry单例
#### 1.7.2 步骤一:创建 Checker 目录与类型
`src/server/checker/runner/tcp/types.ts` 中定义 checker 专属类型:
`src/server/checker/runner/tcp/types.ts` 中定义 checker 专属类型(参考 `http/types.ts``command/types.ts`
```typescript
import type { ResolvedTargetBase } from "../../types";
export interface TcpTargetConfig {
host: string;
port: number;
connectTimeout?: number;
}
export interface TcpExpectConfig {
connected?: boolean;
maxDurationMs?: number;
}
export interface TcpDefaultsConfig {
connectTimeout?: number;
}
export interface ResolvedTcpTarget extends ResolvedTargetBase {
expect?: TcpExpectConfig;
tcp: {
connectTimeout: number;
host: string;
port: number;
};
type: "tcp";
}
```
- `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` 中定义三部分 schema
```typescript
import { Type } from "@sinclair/typebox";
import type { CheckerSchemas } from "../types";
export const tcpCheckerSchemas: CheckerSchemas = {
config: Type.Object(
{
host: Type.String(),
port: Type.Integer({ maximum: 65535, minimum: 0 }),
connectTimeout: Type.Optional(Type.Integer({ minimum: 100 })),
},
{ additionalProperties: false },
),
defaults: Type.Object(
{
connectTimeout: Type.Optional(Type.Integer({ minimum: 100 })),
},
{ additionalProperties: false },
),
expect: Type.Object(
{
connected: Type.Optional(Type.Boolean()),
},
{ additionalProperties: false },
),
};
```
`src/server/checker/runner/tcp/schema.ts` 中定义 `CheckerSchemas`config / defaults / expect 三部分)。参考 `http/schema.ts``command/schema.ts`,使用 `schema/fragments.ts` 中的共享片段。
**可复用的共享 fragments**(来自 `schema/fragments.ts`
@@ -301,29 +279,10 @@ export const tcpCheckerSchemas: CheckerSchemas = {
#### 1.7.4 步骤三:实现语义校验
`src/server/checker/runner/tcp/validate.ts` 中实现 JSON Schema 无法表达的语义规则:
`src/server/checker/runner/tcp/validate.ts` 中实现 JSON Schema 无法表达的语义规则(参考 `http/validate.ts``command/validate.ts`)。函数签名统一为
```typescript
import type { ConfigValidationIssue } from "../../schema/issues";
import type { CheckerValidationInput } from "../types";
import { issue } from "../../schema/issues";
export function validateTcpConfig(input: CheckerValidationInput): ConfigValidationIssue[] {
const issues: ConfigValidationIssue[] = [];
for (const target of input.targets) {
if (target.type !== "tcp") continue;
const name = target.name;
const tcp = target["tcp"] as { host?: string } | undefined;
if (tcp && typeof tcp.host === "string" && tcp.host.trim() === "") {
issues.push(issue("invalid-value", "tcp.host", "host 不能为空字符串", name));
}
}
return issues;
}
export function validateTcpConfig(input: CheckerValidationInput): ConfigValidationIssue[];
```
**共享校验工具**`expect/validate-operator.ts`
@@ -335,89 +294,18 @@ export function validateTcpConfig(input: CheckerValidationInput): ConfigValidati
#### 1.7.5 步骤四:实现 Checker 类
`src/server/checker/runner/tcp/execute.ts` 中实现 `CheckerDefinition` 接口的全部成员:
`src/server/checker/runner/tcp/execute.ts` 中实现 `CheckerDefinition` 接口的全部成员(参考 `http/execute.ts``command/execute.ts`
```typescript
import type { CheckResult, RawTargetConfig, ResolvedTargetBase } from "../../types";
import type { Checker, CheckerContext, CheckerValidationInput, ResolveContext } from "../types";
```
TcpChecker implements Checker
readonly configKey ← "tcp"(对应 YAML 中的 target.tcp 字段)
readonly type ← "tcp"
readonly schemas ← tcpCheckerSchemas
import { checkDuration } from "../../expect/duration";
import { errorFailure } from "../../expect/failure";
import { tcpCheckerSchemas } from "./schema";
import type { ResolvedTcpTarget, TcpExpectConfig, TcpTargetConfig } from "./types";
import { validateTcpConfig } from "./validate";
export class TcpChecker implements Checker {
readonly configKey = "tcp";
readonly type = "tcp";
readonly schemas = tcpCheckerSchemas;
validate(input: CheckerValidationInput) {
return validateTcpConfig(input);
}
resolve(target: RawTargetConfig, context: ResolveContext): ResolvedTargetBase {
const t = target as RawTargetConfig & { tcp: TcpTargetConfig; type: "tcp" };
const defaults = context.defaults["tcp"] as { connectTimeout?: number } | undefined;
return {
expect: target.expect as TcpExpectConfig | undefined,
group: target.group ?? "default",
intervalMs: context.defaultIntervalMs,
name: t.name,
tcp: {
connectTimeout: t.tcp.connectTimeout ?? defaults?.connectTimeout ?? 3000,
host: t.tcp.host,
port: t.tcp.port,
},
timeoutMs: context.defaultTimeoutMs,
type: "tcp",
} satisfies ResolvedTcpTarget;
}
async execute(target: ResolvedTargetBase, ctx: CheckerContext): Promise<CheckResult> {
const t = target as ResolvedTcpTarget;
const timestamp = new Date().toISOString();
const start = performance.now();
try {
// 执行 TCP 连接检查...
const durationMs = Math.round(performance.now() - start);
const durationResult = checkDuration(durationMs, t.expect?.maxDurationMs);
if (!durationResult.matched) {
return {
durationMs,
failure: durationResult.failure,
matched: false,
statusDetail: "TCP connected",
targetName: t.name,
timestamp,
};
}
return { durationMs, failure: null, matched: true, statusDetail: "TCP connected", targetName: t.name, timestamp };
} catch (error) {
const durationMs = Math.round(performance.now() - start);
return {
durationMs,
failure: errorFailure("connection", "connection", String(error)),
matched: false,
statusDetail: null,
targetName: t.name,
timestamp,
};
}
}
serialize(target: ResolvedTargetBase): { config: string; target: string } {
const t = target as ResolvedTcpTarget;
return {
config: JSON.stringify({ connectTimeout: t.tcp.connectTimeout, host: t.tcp.host, port: t.tcp.port }),
target: `${t.tcp.host}:${t.tcp.port}`,
};
}
}
validate(input) ← 调用 validateTcpConfig(input)
resolve(target, ctx)← 默认值合并 + 解析,返回 satisfies ResolvedTcpTarget
execute(target, ctx)← 执行检查,返回 CheckResult
serialize(target) ← 返回 { config, target } 用于 DB 持久化
```
**`resolve()` 规范**
@@ -437,45 +325,23 @@ export class TcpChecker implements Checker {
**可用的共享断言工具**`checker/expect/`
| 模块 | 函数 | 用途 |
| ---------------------- | ----------------------------------------------------- | ---------------------- |
| `failure.ts` | `errorFailure(phase, path, msg)` | 构造错误类型 failure |
| `failure.ts` | `mismatchFailure(phase, path, expected, actual, msg)` | 构造不匹配类型 failure |
| `duration.ts` | `checkDuration(ms, maxMs?)` | 耗时断言 |
| `operator.ts` | `applyOperator(actual, operator)` | 执行单个操作符比较 |
| `operator.ts` | `evaluateJsonPath(json, path)` | JSONPath 提取 |
| `validate-operator.ts` | `validateOperatorObject(ops, path, name)` | 操作符语义校验 |
| 模块 | 函数 | 用途 |
| ---------------------- | ----------------------------------------------------- | ------------------------------------- |
| `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`
创建 `src/server/checker/runner/tcp/index.ts`re-export Checker 类)。
```typescript
export { TcpChecker } from "./execute";
```
`src/server/checker/runner/index.ts` 中添加一行导入和一个数组元素:
```typescript
import { CommandChecker } from "./command";
import { HttpChecker } from "./http";
import { TcpChecker } from "./tcp"; // ← 新增
import { CheckerRegistry } from "./registry";
const checkers = [new HttpChecker(), new CommandChecker(), new TcpChecker()]; // ← 新增
export function createDefaultCheckerRegistry(): CheckerRegistry {
const registry = new CheckerRegistry();
for (const checker of checkers) {
registry.register(checker);
}
return registry;
}
export const checkerRegistry = createDefaultCheckerRegistry();
```
`src/server/checker/runner/index.ts` 中添加一行导入和一个数组元素(参考现有 HttpChecker/CommandChecker
注册后,以下管线会通过 registry 自动委托,**无需新增 type 分支**
@@ -544,6 +410,21 @@ export const checkerRegistry = createDefaultCheckerRegistry();
基于 `bun:sqlite`WAL 模式运行,数据库文件位于配置的 `dataDir` 下。
**核心方法**
| 方法 | 用途 |
| ---------------------- | ---------------------------------------------------------------- |
| `syncTargets(targets)` | 启动期同步 targets基于 name 做 upsert + delete 事务) |
| `insertCheckResult()` | 写入单条检查结果 |
| `getTargets()` | 查询全部 targetsdefault 分组优先排序) |
| `getLatestChecksMap()` | 批量获取每个 target 的最新检查结果(单次 SQL 聚合) |
| `getAllTargetStats()` | 批量获取每个 target 的可用率统计GROUP BY 聚合) |
| `getSummary()` | 获取总览统计(基于 `getLatestChecksMap` 内存计算 up/down/total |
| `getTrend()` | 获取按小时聚合的趋势数据 |
| `getHistory()` | 分页查询历史记录 |
| `getRecentSamples()` | 获取最近 N 条采样数据(用于状态条渲染) |
| `prune(retentionMs)` | 按 retention 策略清理过期数据(由 engine 定时调用) |
**Statement 使用规范**
| 场景 | 方式 | 原因 |
@@ -566,11 +447,12 @@ export const checkerRegistry = createDefaultCheckerRegistry();
### 1.9 拨测引擎
- **调度**`ProbeEngine``es-toolkit/groupBy` 按 interval 分组,每组独立 `setInterval` 定时触发
- **并发控制**`es-toolkit/Semaphore` 限制全局最大并发数(`maxConcurrentChecks``acquire()` 阻塞等待
- **并发控制**`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()`Command 在 signal abort 时 `proc.kill()`
- **结果写入**:检查结果通过 `store.insertCheckResult()` 写入 SQLiteengine 通过 `targetNameToId` 缓存 name→id 映射
- **生命周期**`start()`/`stop()` 管理定时器,`stop()` 清理所有 `setInterval`
- **数据清理**:当 `retentionMs > 0`engine 启动时立即执行一次 `store.prune()`,之后每小时定时执行,按 `timestamp` 清理过期数据
- **生命周期**`start()`/`stop()` 管理定时器(含调度定时器和清理定时器),`stop()` 清理所有 `setInterval`
### 1.10 expect 断言系统
@@ -615,7 +497,12 @@ CommandChecker.execute → 收集观测(exitCode/stdout/stderr/durationMs)
### 1.12 测试规范
- 测试文件与源文件对应:`tests/server/checker/runner/shared/body.test.ts``src/server/checker/runner/shared/body.ts`
- 测试目录 `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 })`
@@ -626,15 +513,15 @@ CommandChecker.execute → 收集观测(exitCode/stdout/stderr/durationMs)
### 2.1 技术栈概览
| 层面 | 技术 | 用途 |
| ------ | ----------------------------------- | ---------------------------- |
| 框架 | React 19 | UI 组件开发 |
| 构建 | Vite 8 | 开发服务与生产构建 |
| 语言 | TypeScript 6 | 类型安全 |
| UI 库 | TDesign React + tdesign-icons-react | UI 组件与图标 |
| 数据层 | TanStack Query (React Query) | 服务端状态管理与自动轮询 |
| 图表 | Recharts | 拨测趋势折线图与状态环状图 |
| 路由 | 无(单页面 Dashboard | 仅需 Drawer/Tab 做页面内导航 |
| 层面 | 技术 | 用途 |
| ------ | --------------------------------------------------- | ---------------------------- |
| 框架 | 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` 足够)
@@ -642,18 +529,21 @@ CommandChecker.execute → 收集观测(exitCode/stdout/stderr/durationMs)
```
main.tsx
└── QueryClientProviderTanStack Query 全局挂载)
└── App根组件
── SummaryCards总览统计卡片
└── useSummary() ─── GET /api/summary8s 轮询
└── TargetBoard目标列表
├── useTargets() ─── GET /api/targets8s 轮询)
└── TargetGroup[](按 group 字段分组
── PrimaryTable ← TARGET_TABLE_COLUMNS列定义排序/筛选/渲染
└── TargetDetailDrawer目标详情抽屉
└── useTargetDetail() ── 按需发起 trend + history 查询
── Tab: 概览 → Statistic + TrendChart + StatusDonut + Descriptions
└── Tab: 记录 → PrimaryTable分页历史记录
└── StrictMode
└── ErrorBoundaryReact 错误边界
── QueryClientProviderTanStack Query 全局挂载
├── App根组件
│ ├── SummaryCards总览统计卡片
│ │ └── useSummary() ─── GET /api/summary8s 轮询)
└── TargetBoard目标列表
── useTargets() ─── GET /api/targets8s 轮询
│ └── TargetGroup[](按 group 字段分组
│ └── PrimaryTable ← TARGET_TABLE_COLUMNS列定义排序/筛选/渲染)
── TargetDetailDrawer目标详情抽屉
└── useTargetDetail() ── 按需发起 trend + history 查询
│ ├── Tab: 概览 → Statistic + TrendChart + StatusDonut + Descriptions
│ └── Tab: 记录 → PrimaryTable分页历史记录
└── ReactQueryDevtools开发工具仅开发环境
```
**数据层架构**
@@ -759,23 +649,24 @@ export function TargetGroup({ name, targets, onTargetClick }: TargetGroupProps)
- **展示组件**`components/`):纯渲染逻辑,通过 props 接收数据,通过回调返回事件
- **容器逻辑**放在 hooks 中,组件只做数据消费
- **常量数据**(列定义、排序器、筛选器)放在 `constants/`,不放在组件内部
- **常量数据**(列定义、排序器、筛选器、颜色阈值)放在 `constants/`,不放在组件内部
- **工具函数**(时间处理等)放在 `utils/`,保持纯函数无副作用
#### 现有组件清单
| 组件 | 文件 | 用途 |
| -------------------- | ----------------------------------- | ---------------------------------- |
| `App` | `app.tsx` | 根组件,编排全局状态与布局 |
| `SummaryCards` | `components/SummaryCards.tsx` | 总览统计卡片(全部/正常/异常) |
| `TargetBoard` | `components/TargetBoard.tsx` | 按分组渲染目标表格列表 |
| `TargetGroup` | `components/TargetGroup.tsx` | 单个分组标题 + PrimaryTable |
| `TargetDetailDrawer` | `components/TargetDetailDrawer.tsx` | 目标详情抽屉(概览/记录 Tab |
| `TrendChart` | `components/TrendChart.tsx` | Recharts 双轴折线图(耗时/可用率) |
| `StatusDonut` | `components/StatusDonut.tsx` | Recharts 环状图UP/DOWN 分布) |
| `StatusDot` | `components/StatusDot.tsx` | 圆形状态指示点(绿/红) |
| `StatusBar` | `components/StatusBar.tsx` | 最近采样状态条(多色块) |
| `GroupHeader` | `components/GroupHeader.tsx` | 分组标题(名称 + 统计) |
| 组件 | 文件 | 用途 |
| -------------------- | ----------------------------------- | ----------------------------------------- |
| `App` | `app.tsx` | 根组件,编排全局状态与布局 |
| `ErrorBoundary` | `components/ErrorBoundary.tsx` | React 错误边界,捕获渲染异常并展示降级 UI |
| `SummaryCards` | `components/SummaryCards.tsx` | 总览统计卡片(全部/正常/异常) |
| `TargetBoard` | `components/TargetBoard.tsx` | 按分组渲染目标表格列表 |
| `TargetGroup` | `components/TargetGroup.tsx` | 单个分组标题 + PrimaryTable |
| `TargetDetailDrawer` | `components/TargetDetailDrawer.tsx` | 目标详情抽屉(概览/记录 Tab |
| `TrendChart` | `components/TrendChart.tsx` | Recharts 双轴折线图(耗时/可用率) |
| `StatusDonut` | `components/StatusDonut.tsx` | Recharts 环状图UP/DOWN 分布) |
| `StatusDot` | `components/StatusDot.tsx` | 圆形状态指示点(绿/红) |
| `StatusBar` | `components/StatusBar.tsx` | 最近采样状态条(多色块) |
| `GroupHeader` | `components/GroupHeader.tsx` | 分组标题(名称 + 统计) |
### 2.5 新增功能开发步骤
@@ -817,7 +708,7 @@ export function TargetGroup({ name, targets, onTargetClick }: TargetGroupProps)
### 2.7 前端测试规范
- 测试目录:`tests/web/`,结构对应 `src/web/`
- 重点测试 **constants/** 中的纯函数(排序器、筛选器、颜色阈值等)
- 重点测试 **constants/** 中的纯函数(排序器、筛选器、颜色阈值、类型映射等)
- 使用 `bun:test` 框架
---
@@ -858,9 +749,10 @@ bun run dev:web
#### 开发期代理
Vite 配置了开发代理(`vite.config.ts`
Vite 配置了开发代理(`vite.config.ts`和代码分割策略
```typescript
// 开发代理
server: {
proxy: {
"/api": {
@@ -869,6 +761,11 @@ server: {
},
},
}
// 生产代码分割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 配置。
@@ -879,7 +776,7 @@ server: {
#### 生产期集成
生产可执行文件是单体应用:前端静态资源嵌入 binary后端同时提供 API 和静态文件服务。
生产可执行文件是单体应用:前端静态资源嵌入 binary(通过 `StaticAssets` 接口:`files: Record<string, Blob>` + `indexHtml: Blob`,后端同时提供 API 和静态文件服务。
```
./dist/dial-server probes.yaml
@@ -1006,16 +903,17 @@ bun run test:smoke
### 3.8 项目配置文件
| 文件 | 用途 |
| --------------------- | ---------------------------------------------- |
| `package.json` | 项目信息、脚本、依赖声明 |
| `tsconfig.json` | TypeScript 配置ESNext 模块、严格模式) |
| `vite.config.ts` | Vite 开发代理与构建配置 |
| `eslint.config.js` | ESLint 规则(含前端不得 import server 的检查) |
| `.prettierrc.json` | Prettier 格式化规则(`printWidth: 120` |
| `.prettierignore` | Prettier 排除路径 |
| `probes.example.yaml` | 配置文件示例 |
| `opencode.json` | OpenCode 工具配置TDesign MCP server |
| 文件 | 用途 |
| ---------------------- | ---------------------------------------------- |
| `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 依赖管理
@@ -1083,6 +981,7 @@ bun run check # 一键运行 schema:check + typecheck + lint + test
| `noUnusedParameters` | false | 保留关闭(路由 handler 统一签名需要,如 `handleXxx(store, method, mode)` |
| `noPropertyAccessFromIndexSignature` | true | 禁止通过点号访问索引签名属性,强制使用括号语法 |
| `noUncheckedIndexedAccess` | true | 数组/Map 访问必须运行时真值检查 |
| `noImplicitOverride` | true | 子类覆盖父类方法时必须显式使用 `override` 关键字 |
| `verbatimModuleSyntax` | true | 强制 `import type` 纯类型导入 |
### Git Hooks
@@ -1107,4 +1006,4 @@ bun run verify # 完整验证check + 构建 + smoke test
## 已知限制
当前不做告警通知、数据自动清理、拨测目标动态增删、认证鉴权和分布式部署。Command 类型拨测不支持 Windows 环境。
当前不做告警通知、拨测目标动态增删、认证鉴权和分布式部署。Command 类型拨测不支持 Windows 环境。