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 ```text
src/ src/
server/ server/
app.ts Bun HTTP 路由入口(路由分发 + API 汇聚) app.ts Bun HTTP 路由入口(路由分发 + API 汇聚、StaticAssets 接口定义
config.ts CLI 参数解析 config.ts CLI 参数解析(仅提取配置文件路径)
dev.ts 生产/开发启动入口 dev.ts 开发模式启动入口
server.ts HTTP server 启动工厂 server.ts HTTP server 启动工厂(接收 StartServerOptions
helpers.ts 共享响应格式化工具(jsonResponse、createHeaders 等 helpers.ts 共享响应格式化工具(见下方函数清单
middleware.ts API 参数校验中间件guardGetHead、validateTargetId middleware.ts API 参数校验中间件guardGetHead、validateTargetId、validateTimeRange、validatePagination
static.ts 静态资源服务与 SPA fallback static.ts 静态资源服务与 SPA fallback
routes/ API 路由 handler按端点拆分 routes/ API 路由 handler按端点拆分,签名因端点而异
health.ts GET /health health.ts GET /health(无 store 参数)
summary.ts GET /api/summary summary.ts GET /api/summary
targets.ts GET /api/targets targets.ts GET /api/targets
history.ts GET /api/targets/:id/history history.ts GET /api/targets/:id/history
trend.ts GET /api/targets/:id/trend trend.ts GET /api/targets/:id/trend
checker/ checker/
types.ts 基础类型定义ResolvedTargetBase、RawTargetConfig、DefaultsConfig 等 base interface types.ts 基础类型定义ResolvedTargetBase、RawTargetConfig、DefaultsConfig、CheckResult 等基础 interface
config-loader.ts YAML 配置解析、契约校验、语义校验与运行期解析 config-loader.ts YAML 配置解析、契约校验、语义校验与运行期解析(输出 ResolvedConfig
schema/ TypeBox + Ajv 配置契约、schema fragments、issue 渲染和 schema 导出入口 schema/ TypeBox + Ajv 配置契约、schema fragments、issue 渲染和 schema 导出入口
builder.ts 全量 JSON Schema 组装(遍历 registry 生成) builder.ts 全量 JSON Schema 组装(遍历 registry 生成)
fragments.ts 共享 TypeBox schema 片段duration、size、operator 等) fragments.ts 共享 TypeBox schema 片段duration、size、operator 等)
@@ -43,48 +43,46 @@ src/
issues.ts 校验问题类型与渲染 issues.ts 校验问题类型与渲染
types.ts schema 层类型 types.ts schema 层类型
export.ts JSON Schema 文件导出 export.ts JSON Schema 文件导出
store.ts SQLite 数据存储 store.ts SQLite 数据存储(含 syncTargets、prune 等生命周期方法)
engine.ts 调度引擎(按 interval 分组的 es-toolkit groupBy + Semaphore 并发控制) engine.ts 调度引擎(按 interval 分组的 es-toolkit groupBy + Semaphore 并发控制 + 数据清理
utils.ts 共享工具函数parseSize、parseDuration utils.ts 共享工具函数parseSize、parseDuration
expect/ 共享 expect 断言基础设施(跨 checker 复用) expect/ 共享 expect 断言基础设施(跨 checker 复用)
types.ts ExpectResult 共享断言类型 types.ts ExpectResult 共享断言类型
failure.ts 失败信息构造errorFailure、mismatchFailure failure.ts 失败信息构造errorFailure、mismatchFailure、truncateActual
operator.ts 操作符系统applyOperator、checkExpectValue、evaluateJsonPath operator.ts 操作符系统applyOperator、evaluateJsonPath
duration.ts 耗时断言checkDuration duration.ts 耗时断言checkDuration
validate-operator.ts 操作符语义校验validateOperatorObject、isJsonValue validate-operator.ts 操作符语义校验validateOperatorObject、isJsonValue、isPlainRecord
runner/ Checker 统一抽象与注册机制 runner/ Checker 统一抽象与注册机制
types.ts CheckerDefinition、CheckerContext、CheckerSchemas、ResolveContext types.ts CheckerDefinition、CheckerContext、CheckerSchemas、ResolveContext
registry.ts CheckerRegistry 注册中心 registry.ts CheckerRegistry 注册中心
index.ts 注册入口(显式数组 + 循环注册) index.ts 注册入口(显式数组 + 循环注册)
http/ HTTP Checker自包含模块 http/ HTTP Checker自包含模块,含 types/schema/execute/expect/validate/body
index.ts 模块入口re-export HttpChecker command/ Command Checker自包含模块含 types/schema/execute/expect/validate/text
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
shared/ shared/
api.ts 前后端共享 TypeScript 类型 api.ts 前后端共享 TypeScript 类型
web/ Vite + React 前端 Dashboard web/ Vite + React 前端 Dashboard
components/ UI 组件表格、分组、Drawer、状态条等 app.tsx 根组件(编排全局状态与布局
constants/ 常量定义(列配置、类型映射、排序/筛选/颜色阈值函数 main.tsx 入口QueryClient 挂载 + ErrorBoundary + ReactQueryDevtools
hooks/ TanStack Query 数据层useTargetDetail 集成轮询/条件查询) 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/ 前端工具函数 utils/ 前端工具函数
time.ts 时间处理subtractHours
scripts/ 开发、构建、schema 生成和 smoke test 脚本 scripts/ 开发、构建、schema 生成和 smoke test 脚本
tests/ Bun test 测试 tests/ Bun test 测试(结构镜像 src 目录)
openspec/ OpenSpec 变更与规格文档 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` 的运行时实现。 前端只通过 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) dev.ts → readRuntimeConfig(cli args, 仅提取 configPath)
ProbeStore(db) → ProbeEngine(store, targets) → startServer(store) 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() 定时器(tick) → ProbeEngine.probeGroup()
HTTP: fetcher.ts / Command: command-runner.ts checkerRegistry.get(target.type).execute()
→ runner/*/expect.ts 校验 → store.insertCheckResult() → runner/*/expect.ts 校验 → engine.writeResult() → store.insertCheckResult()
数据清理: 定时 prune(retentionMs),每小时执行一次
HTTP 请求: HTTP 请求:
Request → app.ts(路由分发) → routes/*.ts(handler) Request → app.ts(路由分发) → routes/*.ts(handler)
@@ -126,19 +128,29 @@ HTTP 请求:
### 1.3 API 路由开发 ### 1.3 API 路由开发
路由文件位于 `src/server/routes/`每个端点一个文件。handler 函数签名统一为 路由文件位于 `src/server/routes/`每个端点一个文件。handler 函数签名因端点而异
```typescript ```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 匹配路由 1. `app.ts``createFetchHandler` 作为总入口,根据 URL pattern 匹配路由
2. API 路由统一经过 `guardGetHead` 做方法检查(仅允许 GET/HEAD 2. `/health` 路由独立处理,不经过 `guardGetHead`(使用 `helpers.ts``allowsGetHead` 自行校验方法
3. 各 handler 内部通过 `middleware.ts` 提供的 `validateTargetId``validateTimeRange``validatePagination` 做参数校验 3. `/api/*` 路由统一经过 `guardGetHead` 做方法检查(仅允许 GET/HEAD返回 `null` 表示通过
4. 校验函数返回 `Response` 表示校验失败(直接返回),返回数据对象表示通过 4. 各 handler 内部通过 `middleware.ts` 提供的 `validateTargetId``validateTimeRange``validatePagination` 做参数校验
5. 业务逻辑通过 `store` 查询数据,用 `helpers.ts``jsonResponse``mapCheckResult``formatDuration` 等格式化输出 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 共享工具 ### 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` - **`middleware.ts`**API 参数校验函数(`guardGetHead``validateTargetId``validateTimeRange``validatePagination`
- **`static.ts`**:生产模式下的静态资源服务与 SPA fallback - **`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` `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`、不启用类型强制转换、不注入默认值、不自动删除未知字段。 契约层使用 `src/server/checker/schema/` 中的 TypeBox fragments 生成 JSON Schema并用 Ajv 执行启动期校验。Ajv 必须保持严格拒绝模式:`allErrors: true`、不启用类型强制转换、不注入默认值、不自动删除未知字段。
默认对象策略是 `additionalProperties: false`。只有明确声明的动态键值表可以开放任意键名,例如 `http.headers``defaults.http.headers``expect.headers``command.env` 默认对象策略是 `additionalProperties: false`。只有明确声明的动态键值表可以开放任意键名,例如 `http.headers``defaults.http.headers``expect.headers``command.env`
@@ -217,72 +249,18 @@ checkerRegistry单例
#### 1.7.2 步骤一:创建 Checker 目录与类型 #### 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 - `XxxTargetConfig` — YAML 原始配置类型
import type { ResolvedTargetBase } from "../../types"; - `XxxExpectConfig` — expect 字段类型
- `XxxDefaultsConfig` — defaults 专属字段类型
export interface TcpTargetConfig { - `ResolvedXxxTarget extends ResolvedTargetBase` — resolve 后的完整类型,含 `type: "xxx"` 字面量
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";
}
```
**注意**:不需要修改顶层 `checker/types.ts`。base interface 使用 index signature`[key: string]: unknown`checker 专属类型通过 `extends ResolvedTargetBase` 自动兼容。 **注意**:不需要修改顶层 `checker/types.ts`。base interface 使用 index signature`[key: string]: unknown`checker 专属类型通过 `extends ResolvedTargetBase` 自动兼容。
#### 1.7.3 步骤二:创建 TypeBox 契约 Schema #### 1.7.3 步骤二:创建 TypeBox 契约 Schema
`src/server/checker/runner/tcp/schema.ts` 中定义三部分 schema `src/server/checker/runner/tcp/schema.ts` 中定义 `CheckerSchemas`config / defaults / expect 三部分)。参考 `http/schema.ts``command/schema.ts`,使用 `schema/fragments.ts` 中的共享片段。
```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 },
),
};
```
**可复用的共享 fragments**(来自 `schema/fragments.ts` **可复用的共享 fragments**(来自 `schema/fragments.ts`
@@ -301,29 +279,10 @@ export const tcpCheckerSchemas: CheckerSchemas = {
#### 1.7.4 步骤三:实现语义校验 #### 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 ```typescript
import type { ConfigValidationIssue } from "../../schema/issues"; export function validateTcpConfig(input: CheckerValidationInput): ConfigValidationIssue[];
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;
}
``` ```
**共享校验工具**`expect/validate-operator.ts` **共享校验工具**`expect/validate-operator.ts`
@@ -335,89 +294,18 @@ export function validateTcpConfig(input: CheckerValidationInput): ConfigValidati
#### 1.7.5 步骤四:实现 Checker 类 #### 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"; TcpChecker implements Checker
import type { Checker, CheckerContext, CheckerValidationInput, ResolveContext } from "../types"; readonly configKey ← "tcp"(对应 YAML 中的 target.tcp 字段)
readonly type ← "tcp"
readonly schemas ← tcpCheckerSchemas
import { checkDuration } from "../../expect/duration"; validate(input) ← 调用 validateTcpConfig(input)
import { errorFailure } from "../../expect/failure"; resolve(target, ctx)← 默认值合并 + 解析,返回 satisfies ResolvedTcpTarget
import { tcpCheckerSchemas } from "./schema"; execute(target, ctx)← 执行检查,返回 CheckResult
import type { ResolvedTcpTarget, TcpExpectConfig, TcpTargetConfig } from "./types"; serialize(target) ← 返回 { config, target } 用于 DB 持久化
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}`,
};
}
}
``` ```
**`resolve()` 规范** **`resolve()` 规范**
@@ -437,45 +325,23 @@ export class TcpChecker implements Checker {
**可用的共享断言工具**`checker/expect/` **可用的共享断言工具**`checker/expect/`
| 模块 | 函数 | 用途 | | 模块 | 函数 | 用途 |
| ---------------------- | ----------------------------------------------------- | ---------------------- | | ---------------------- | ----------------------------------------------------- | ------------------------------------- |
| `failure.ts` | `errorFailure(phase, path, msg)` | 构造错误类型 failure | | `failure.ts` | `errorFailure(phase, path, msg)` | 构造错误类型 failure |
| `failure.ts` | `mismatchFailure(phase, path, expected, actual, msg)` | 构造不匹配类型 failure | | `failure.ts` | `mismatchFailure(phase, path, expected, actual, msg)` | 构造不匹配类型 failure |
| `duration.ts` | `checkDuration(ms, maxMs?)` | 耗时断言 | | `failure.ts` | `truncateActual(value, maxLen?)` | 截断过长的 actual 值(默认 200 字符) |
| `operator.ts` | `applyOperator(actual, operator)` | 执行单个操作符比较 | | `duration.ts` | `checkDuration(ms, maxMs?)` | 耗时断言 |
| `operator.ts` | `evaluateJsonPath(json, path)` | JSONPath 提取 | | `operator.ts` | `applyOperator(actual, operator)` | 执行单个操作符比较 |
| `validate-operator.ts` | `validateOperatorObject(ops, path, name)` | 操作符语义校验 | | `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 **Checker 专属断言**(如需要)放在同目录的 `expect.ts` 中,参考 `http/expect.ts`checkStatus、checkHeaders`command/expect.ts`checkExitCode
#### 1.7.6 步骤五:创建模块入口并注册 #### 1.7.6 步骤五:创建模块入口并注册
创建 `src/server/checker/runner/tcp/index.ts` 创建 `src/server/checker/runner/tcp/index.ts`re-export Checker 类)。
```typescript `src/server/checker/runner/index.ts` 中添加一行导入和一个数组元素(参考现有 HttpChecker/CommandChecker
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();
```
注册后,以下管线会通过 registry 自动委托,**无需新增 type 分支** 注册后,以下管线会通过 registry 自动委托,**无需新增 type 分支**
@@ -544,6 +410,21 @@ export const checkerRegistry = createDefaultCheckerRegistry();
基于 `bun:sqlite`WAL 模式运行,数据库文件位于配置的 `dataDir` 下。 基于 `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 使用规范** **Statement 使用规范**
| 场景 | 方式 | 原因 | | 场景 | 方式 | 原因 |
@@ -566,11 +447,12 @@ export const checkerRegistry = createDefaultCheckerRegistry();
### 1.9 拨测引擎 ### 1.9 拨测引擎
- **调度**`ProbeEngine``es-toolkit/groupBy` 按 interval 分组,每组独立 `setInterval` 定时触发 - **调度**`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 })` - **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()` - **超时控制**`ProbeEngine` 为每次检查创建 `AbortController` 并按 `target.timeoutMs` 触发 abortchecker 必须使用 `CheckerContext.signal` 感知超时HTTP 将 signal 传给 `fetch()`Command 在 signal abort 时 `proc.kill()`
- **结果写入**:检查结果通过 `store.insertCheckResult()` 写入 SQLiteengine 通过 `targetNameToId` 缓存 name→id 映射 - **结果写入**:检查结果通过 `store.insertCheckResult()` 写入 SQLiteengine 通过 `targetNameToId` 缓存 name→id 映射
- **生命周期**`start()`/`stop()` 管理定时器,`stop()` 清理所有 `setInterval` - **数据清理**:当 `retentionMs > 0`engine 启动时立即执行一次 `store.prune()`,之后每小时定时执行,按 `timestamp` 清理过期数据
- **生命周期**`start()`/`stop()` 管理定时器(含调度定时器和清理定时器),`stop()` 清理所有 `setInterval`
### 1.10 expect 断言系统 ### 1.10 expect 断言系统
@@ -615,7 +497,12 @@ CommandChecker.execute → 收集观测(exitCode/stdout/stderr/durationMs)
### 1.12 测试规范 ### 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()` - 使用 `bun:test` 框架(`describe`/`test`/`expect`),测试数据库用临时目录 + `tmpdir()`
- 新增 store 方法必须编写单元测试;新增 API 端点必须在 `app.test.ts` 中添加集成测试 - 新增 store 方法必须编写单元测试;新增 API 端点必须在 `app.test.ts` 中添加集成测试
- 测试后清理:`afterAll``store.close()` + `rm(tempDir, { recursive: true })` - 测试后清理:`afterAll``store.close()` + `rm(tempDir, { recursive: true })`
@@ -626,15 +513,15 @@ CommandChecker.execute → 收集观测(exitCode/stdout/stderr/durationMs)
### 2.1 技术栈概览 ### 2.1 技术栈概览
| 层面 | 技术 | 用途 | | 层面 | 技术 | 用途 |
| ------ | ----------------------------------- | ---------------------------- | | ------ | --------------------------------------------------- | ---------------------------- |
| 框架 | React 19 | UI 组件开发 | | 框架 | React 19 | UI 组件开发 |
| 构建 | Vite 8 | 开发服务与生产构建 | | 构建 | Vite 8 | 开发服务与生产构建 |
| 语言 | TypeScript 6 | 类型安全 | | 语言 | TypeScript 6 | 类型安全 |
| UI 库 | TDesign React + tdesign-icons-react | UI 组件与图标 | | UI 库 | TDesign React + tdesign-icons-react | UI 组件与图标 |
| 数据层 | TanStack Query (React Query) | 服务端状态管理与自动轮询 | | 数据层 | TanStack Query (React Query) + React Query Devtools | 服务端状态管理与自动轮询 |
| 图表 | Recharts | 拨测趋势折线图与状态环状图 | | 图表 | Recharts | 拨测趋势折线图与状态环状图 |
| 路由 | 无(单页面 Dashboard | 仅需 Drawer/Tab 做页面内导航 | | 路由 | 无(单页面 Dashboard | 仅需 Drawer/Tab 做页面内导航 |
**不引入的依赖**React Router单页面场景不需要、状态管理库TanStack Query 即服务端状态层,组件内用 `useState` 足够) **不引入的依赖**React Router单页面场景不需要、状态管理库TanStack Query 即服务端状态层,组件内用 `useState` 足够)
@@ -642,18 +529,21 @@ CommandChecker.execute → 收集观测(exitCode/stdout/stderr/durationMs)
``` ```
main.tsx main.tsx
└── QueryClientProviderTanStack Query 全局挂载) └── StrictMode
└── App根组件 └── ErrorBoundaryReact 错误边界
── SummaryCards总览统计卡片 ── QueryClientProviderTanStack Query 全局挂载
└── useSummary() ─── GET /api/summary8s 轮询 ├── App根组件
└── TargetBoard目标列表 │ ├── SummaryCards总览统计卡片
├── useTargets() ─── GET /api/targets8s 轮询) │ │ └── useSummary() ─── GET /api/summary8s 轮询)
└── TargetGroup[](按 group 字段分组 └── TargetBoard目标列表
── PrimaryTable ← TARGET_TABLE_COLUMNS列定义排序/筛选/渲染 ── useTargets() ─── GET /api/targets8s 轮询
└── TargetDetailDrawer目标详情抽屉 │ └── TargetGroup[](按 group 字段分组
└── useTargetDetail() ── 按需发起 trend + history 查询 │ └── PrimaryTable ← TARGET_TABLE_COLUMNS列定义排序/筛选/渲染)
── Tab: 概览 → Statistic + TrendChart + StatusDonut + Descriptions ── TargetDetailDrawer目标详情抽屉
└── Tab: 记录 → PrimaryTable分页历史记录 └── useTargetDetail() ── 按需发起 trend + history 查询
│ ├── Tab: 概览 → Statistic + TrendChart + StatusDonut + Descriptions
│ └── Tab: 记录 → PrimaryTable分页历史记录
└── ReactQueryDevtools开发工具仅开发环境
``` ```
**数据层架构** **数据层架构**
@@ -759,23 +649,24 @@ export function TargetGroup({ name, targets, onTargetClick }: TargetGroupProps)
- **展示组件**`components/`):纯渲染逻辑,通过 props 接收数据,通过回调返回事件 - **展示组件**`components/`):纯渲染逻辑,通过 props 接收数据,通过回调返回事件
- **容器逻辑**放在 hooks 中,组件只做数据消费 - **容器逻辑**放在 hooks 中,组件只做数据消费
- **常量数据**(列定义、排序器、筛选器)放在 `constants/`,不放在组件内部 - **常量数据**(列定义、排序器、筛选器、颜色阈值)放在 `constants/`,不放在组件内部
- **工具函数**(时间处理等)放在 `utils/`,保持纯函数无副作用 - **工具函数**(时间处理等)放在 `utils/`,保持纯函数无副作用
#### 现有组件清单 #### 现有组件清单
| 组件 | 文件 | 用途 | | 组件 | 文件 | 用途 |
| -------------------- | ----------------------------------- | ---------------------------------- | | -------------------- | ----------------------------------- | ----------------------------------------- |
| `App` | `app.tsx` | 根组件,编排全局状态与布局 | | `App` | `app.tsx` | 根组件,编排全局状态与布局 |
| `SummaryCards` | `components/SummaryCards.tsx` | 总览统计卡片(全部/正常/异常) | | `ErrorBoundary` | `components/ErrorBoundary.tsx` | React 错误边界,捕获渲染异常并展示降级 UI |
| `TargetBoard` | `components/TargetBoard.tsx` | 按分组渲染目标表格列表 | | `SummaryCards` | `components/SummaryCards.tsx` | 总览统计卡片(全部/正常/异常) |
| `TargetGroup` | `components/TargetGroup.tsx` | 单个分组标题 + PrimaryTable | | `TargetBoard` | `components/TargetBoard.tsx` | 按分组渲染目标表格列表 |
| `TargetDetailDrawer` | `components/TargetDetailDrawer.tsx` | 目标详情抽屉(概览/记录 Tab | | `TargetGroup` | `components/TargetGroup.tsx` | 单个分组标题 + PrimaryTable |
| `TrendChart` | `components/TrendChart.tsx` | Recharts 双轴折线图(耗时/可用率) | | `TargetDetailDrawer` | `components/TargetDetailDrawer.tsx` | 目标详情抽屉(概览/记录 Tab |
| `StatusDonut` | `components/StatusDonut.tsx` | Recharts 环状图UP/DOWN 分布) | | `TrendChart` | `components/TrendChart.tsx` | Recharts 双轴折线图(耗时/可用率) |
| `StatusDot` | `components/StatusDot.tsx` | 圆形状态指示点(绿/红) | | `StatusDonut` | `components/StatusDonut.tsx` | Recharts 环状图UP/DOWN 分布) |
| `StatusBar` | `components/StatusBar.tsx` | 最近采样状态条(多色块) | | `StatusDot` | `components/StatusDot.tsx` | 圆形状态指示点(绿/红) |
| `GroupHeader` | `components/GroupHeader.tsx` | 分组标题(名称 + 统计) | | `StatusBar` | `components/StatusBar.tsx` | 最近采样状态条(多色块) |
| `GroupHeader` | `components/GroupHeader.tsx` | 分组标题(名称 + 统计) |
### 2.5 新增功能开发步骤 ### 2.5 新增功能开发步骤
@@ -817,7 +708,7 @@ export function TargetGroup({ name, targets, onTargetClick }: TargetGroupProps)
### 2.7 前端测试规范 ### 2.7 前端测试规范
- 测试目录:`tests/web/`,结构对应 `src/web/` - 测试目录:`tests/web/`,结构对应 `src/web/`
- 重点测试 **constants/** 中的纯函数(排序器、筛选器、颜色阈值等) - 重点测试 **constants/** 中的纯函数(排序器、筛选器、颜色阈值、类型映射等)
- 使用 `bun:test` 框架 - 使用 `bun:test` 框架
--- ---
@@ -858,9 +749,10 @@ bun run dev:web
#### 开发期代理 #### 开发期代理
Vite 配置了开发代理(`vite.config.ts` Vite 配置了开发代理(`vite.config.ts`和代码分割策略
```typescript ```typescript
// 开发代理
server: { server: {
proxy: { proxy: {
"/api": { "/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 配置。 前端访问 `/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 ./dist/dial-server probes.yaml
@@ -1006,16 +903,17 @@ bun run test:smoke
### 3.8 项目配置文件 ### 3.8 项目配置文件
| 文件 | 用途 | | 文件 | 用途 |
| --------------------- | ---------------------------------------------- | | ---------------------- | ---------------------------------------------- |
| `package.json` | 项目信息、脚本、依赖声明 | | `package.json` | 项目信息、脚本、依赖声明 |
| `tsconfig.json` | TypeScript 配置ESNext 模块、严格模式) | | `tsconfig.json` | TypeScript 配置ESNext 模块、严格模式) |
| `vite.config.ts` | Vite 开发代理与构建配置 | | `vite.config.ts` | Vite 开发代理与构建配置(含代码分割策略) |
| `eslint.config.js` | ESLint 规则(含前端不得 import server 的检查) | | `eslint.config.js` | ESLint 规则(含前端不得 import server 的检查) |
| `.prettierrc.json` | Prettier 格式化规则(`printWidth: 120` | | `commitlint.config.js` | commitlint 提交信息格式校验 |
| `.prettierignore` | Prettier 排除路径 | | `.prettierrc.json` | Prettier 格式化规则(`printWidth: 120` |
| `probes.example.yaml` | 配置文件示例 | | `.prettierignore` | Prettier 排除路径 |
| `opencode.json` | OpenCode 工具配置TDesign MCP server | | `probes.example.yaml` | 配置文件示例 |
| `opencode.json` | OpenCode 工具配置TDesign MCP server |
### 3.9 依赖管理 ### 3.9 依赖管理
@@ -1083,6 +981,7 @@ bun run check # 一键运行 schema:check + typecheck + lint + test
| `noUnusedParameters` | false | 保留关闭(路由 handler 统一签名需要,如 `handleXxx(store, method, mode)` | | `noUnusedParameters` | false | 保留关闭(路由 handler 统一签名需要,如 `handleXxx(store, method, mode)` |
| `noPropertyAccessFromIndexSignature` | true | 禁止通过点号访问索引签名属性,强制使用括号语法 | | `noPropertyAccessFromIndexSignature` | true | 禁止通过点号访问索引签名属性,强制使用括号语法 |
| `noUncheckedIndexedAccess` | true | 数组/Map 访问必须运行时真值检查 | | `noUncheckedIndexedAccess` | true | 数组/Map 访问必须运行时真值检查 |
| `noImplicitOverride` | true | 子类覆盖父类方法时必须显式使用 `override` 关键字 |
| `verbatimModuleSyntax` | true | 强制 `import type` 纯类型导入 | | `verbatimModuleSyntax` | true | 强制 `import type` 纯类型导入 |
### Git Hooks ### Git Hooks
@@ -1107,4 +1006,4 @@ bun run verify # 完整验证check + 构建 + smoke test
## 已知限制 ## 已知限制
当前不做告警通知、数据自动清理、拨测目标动态增删、认证鉴权和分布式部署。Command 类型拨测不支持 Windows 环境。 当前不做告警通知、拨测目标动态增删、认证鉴权和分布式部署。Command 类型拨测不支持 Windows 环境。