From 6ea185315fdddf7ed16ee0adf2121c5d3e6b1f30 Mon Sep 17 00:00:00 2001 From: lanyuanxiaoyao Date: Wed, 13 May 2026 17:27:33 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E4=BF=AE=E6=AD=A3=20DEVELOPMENT.md=20?= =?UTF-8?q?=E4=B8=8E=E5=AE=9E=E9=99=85=E4=BB=A3=E7=A0=81=E7=9A=84=E5=B7=AE?= =?UTF-8?q?=E5=BC=82=E5=B9=B6=E7=B2=BE=E7=AE=80=20tcp=20=E7=A4=BA=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DEVELOPMENT.md | 487 ++++++++++++++++++++----------------------------- 1 file changed, 193 insertions(+), 294 deletions(-) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index b1742c7..ae78ab2 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -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 { - 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()` | 查询全部 targets(default 分组优先排序) | +| `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` 触发 abort;checker 必须使用 `CheckerContext.signal` 感知超时,HTTP 将 signal 传给 `fetch()`,Command 在 signal abort 时 `proc.kill()` - **结果写入**:检查结果通过 `store.insertCheckResult()` 写入 SQLite,engine 通过 `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 -└── QueryClientProvider(TanStack Query 全局挂载) - └── App(根组件) - ├── SummaryCards(总览统计卡片) - │ └── useSummary() ─── GET /api/summary(8s 轮询) - └── TargetBoard(目标列表) - ├── useTargets() ─── GET /api/targets(8s 轮询) - └── TargetGroup[](按 group 字段分组) - └── PrimaryTable ← TARGET_TABLE_COLUMNS(列定义:排序/筛选/渲染) - └── TargetDetailDrawer(目标详情抽屉) - └── useTargetDetail() ── 按需发起 trend + history 查询 - ├── Tab: 概览 → Statistic + TrendChart + StatusDonut + Descriptions - └── Tab: 记录 → PrimaryTable(分页历史记录) +└── StrictMode + └── ErrorBoundary(React 错误边界) + └── QueryClientProvider(TanStack Query 全局挂载) + ├── App(根组件) + │ ├── SummaryCards(总览统计卡片) + │ │ └── useSummary() ─── GET /api/summary(8s 轮询) + │ └── TargetBoard(目标列表) + │ ├── useTargets() ─── GET /api/targets(8s 轮询) + │ └── 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` + `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 环境。