74 KiB
DiAL 开发文档
本文档面向 DiAL 项目的开发者,介绍项目结构、构建流程、测试、代码规范等内容。
用户使用说明请参阅 README.md。
目录
版本管理
DiAL 使用 package.json.version 作为应用版本号的唯一来源,遵循 SemVer 语义化版本规范(MAJOR.MINOR.PATCH)。
版本升迁命令:
| 命令 | 说明 |
|---|---|
bun run version:patch |
升迁 patch 版本(bugfix、文档、测试、内部重构) |
bun run version:minor |
升迁 minor 版本(新功能、新 checker、新配置字段) |
bun run version:major |
升迁 major 版本(不兼容的配置格式、API 行为变化) |
bun run version:set <version> |
显式设置版本号 |
版本展示:
- Dashboard Header 品牌区域展示当前运行实例版本号(如
v0.1.0) - 版号通过
/api/meta接口返回,前端通过useMetahook 获取 - 生产构建时版本号固化到可执行文件中,不依赖运行时外部
package.json
暂不支持:
- CLI
--version参数 - 自动创建 git commit、git tag 或 changelog
- prerelease 版本格式(如
1.0.0-beta.1)
项目结构
src/
server/
bootstrap.ts 后端统一启动引导(loadConfig → store → engine → startServer → shutdown)
config.ts CLI 参数解析(仅提取配置文件路径)
dev.ts 开发模式启动入口(mode: "development",仅 API server)
main.ts 生产模式启动入口(mode: "production",安全头启用)
server.ts HTTP server 启动工厂(Bun.serve routes 声明式路由 + fetch fallback 静态资源服务)
helpers.ts 共享响应格式化工具(见下方函数清单)
middleware.ts API 参数校验中间件(validateTargetId、validateTimeRange、validatePagination)
version.ts 应用版本读取与校验
routes/ API 路由 handler(按端点拆分)
health.ts GET /health(无 store 参数)
meta.ts GET /api/meta
dashboard.ts GET /api/dashboard
metrics.ts GET /api/targets/:id/metrics
history.ts GET /api/targets/:id/history
checker/
types.ts 基础类型定义(ResolvedTargetBase、RawTargetConfig、DefaultsConfig、CheckResult 等基础 interface)
config-loader.ts YAML 配置解析、契约校验、语义校验与运行期解析(输出 ResolvedConfig)
variables.ts 配置 variables 提取、target 字符串变量替换和 unresolved-variable issue 生成
schema/ TypeBox + Ajv 配置契约、schema fragments、issue 渲染和 schema 导出入口
builder.ts 全量 JSON Schema 组装(遍历 registry 生成)
fragments.ts 共享 TypeBox schema 片段(duration、size、ValueMatcher、ContentExpectations、KeyedExpectations 等)
validate.ts Ajv 契约校验入口
issues.ts 校验问题类型与渲染
types.ts schema 层类型
export.ts JSON Schema 文件导出
store.ts SQLite 数据存储(含 syncTargets、prune 等生命周期方法)
engine.ts 调度引擎(按 interval 分组的 es-toolkit groupBy + Semaphore 并发控制 + 数据清理)
utils.ts 共享工具函数(parseSize、parseDuration)
expect/ 共享 expect 断言基础设施(跨 checker 复用)
types.ts Raw/Resolved ValueExpectation、ContentExpectations、KeyedExpectations、ExpectationResult 类型
failure.ts 失败信息构造(errorFailure、mismatchFailure、truncateActual)
value.ts ValueExpectation resolve(primitive→equals)和执行、JSONPath 提取
content.ts Resolved ContentExpectations 执行(kind=value/json/css/xpath)和 Raw resolve
keyed.ts Resolved KeyedExpectations 执行(顺序 + key 规范化)和 Raw resolve
headers.ts HTTP/LLM 共享 header keyed expectation 包装(大小写不敏感)
status.ts HTTP/LLM 共享 status code 断言(精确数值与 1xx-5xx 范围)
validate.ts Raw value/content/keyed expectation 语义校验(不修改输入)
redos.ts regex ReDoS 风险检测
runner/ Checker 统一抽象与注册机制
types.ts CheckerDefinition、CheckerContext、CheckerSchemas、ResolveContext
registry.ts CheckerRegistry 注册中心
index.ts 注册入口(显式数组 + 循环注册)
http/ HTTP Checker(自包含模块,含 types/schema/execute/expect/validate)
cmd/ Cmd Checker(自包含模块,含 types/schema/execute/expect/validate)
db/ DB Checker(自包含模块,含 types/schema/execute/expect/validate)
tcp/ TCP Checker(自包含模块,含 types/schema/execute/expect/validate)
icmp/ ICMP Checker(自包含模块,含 types/schema/execute/expect/validate/parse)
udp/ UDP Checker(自包含模块,含 types/schema/execute/expect/validate/encoding)
llm/ LLM Checker(自包含模块,含 types/schema/execute/expect/validate/provider/observation)
shared/
api.ts 前后端共享 TypeScript 类型
web/ React 前端 Dashboard(通过 Bun HTML import 集成)
app.tsx 根组件(编排全局状态与布局)
main.tsx 入口(QueryClient 挂载 + ErrorBoundary + ReactQueryDevtools)
styles.css 全局样式与自定义 CSS 变量
components/ UI 组件(见下方组件清单)
constants/ 常量与纯函数
history-table-columns.tsx 历史记录表格列定义
target-table-columns.tsx 目标表格列定义工厂
target-table-filters.ts 表格筛选器
target-table-sorters.ts 表格排序器
color-threshold.ts 可用率颜色阈值函数
hooks/ React hooks(数据查询、Drawer 状态、浏览器 UI 偏好)
use-queries.ts 全局面板查询 hook(dashboard/meta/metrics)
use-target-detail.ts 目标详情 Drawer 状态与条件查询 hook
use-theme-preference.ts 主题模式偏好、本地存储和 TDesign theme-mode 应用 hook
utils/ 前端工具函数
time.ts 时间处理(subtractHours、相对时间、动态时长单位)
scripts/ 构建、schema 生成和清理脚本
tests/ Bun test 测试(结构镜像 src 目录)
openspec/ OpenSpec 变更与规格文档
probe-config.schema.json 用户配置 JSON Schema 导出物(用于 IDE 自动补全和校验)
说明:
runner/http/和runner/cmd/的完整文件结构见 1.7.1 架构总览 中的标准文件表。
前后端边界
前端只通过 HTTP 调用后端,API 路径为 /api/*。共享类型放在 src/shared,前端不得 import src/server 的运行时实现。
一、后端开发指引
1.1 架构概览
启动流程:
dev.ts / main.ts → readRuntimeConfig(cli args, 仅提取 configPath)
→ bootstrap({ configPath, mode })
→ loadConfig(yaml:YAML 解析 → 变量替换 → 契约校验 → 语义校验 → resolve)
→ ResolvedConfig{ host, port, dataDir, maxConcurrentChecks, retentionMs, targets }
→ ProbeStore(db) → store.syncTargets(targets)
→ ProbeEngine(store, targets, maxConcurrentChecks, retentionMs) → engine.start()
→ startServer({ config, mode, store })
→ 注册 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 → Bun.serve routes 声明式匹配 → routes/*.ts(handler)
→ middleware.ts(参数校验) → helpers.ts(响应格式化) → Response
前端: fetch fallback → serveStaticAsset (生产) / Vite proxy (开发)
1.2 库使用优先级
后端代码开发遵循严格的库选择顺序:
| 优先级 | 来源 | 典型用途 |
|---|---|---|
| 1 | Bun 内置 API | Bun.serve、bun:sqlite、Bun.spawn、Bun.file、Bun.YAML |
| 2 | es-toolkit | 类型判断(isPlainObject/isNil/isEmptyObject)、深度比较(isEqual)、错误判断(isError)、并发控制(Semaphore)、集合操作(groupBy) |
| 3 | 标准 Web API | Object.fromEntries、Headers、fetch、AbortController |
| 4 | 主流三方库 | cheerio(HTML 解析)、xpath + @xmldom/xmldom(XML 解析) |
| 5 | 自行实现 | 仅在以上都无法满足时(如 parseDuration、parseSize、evaluateJsonPath 等专项逻辑) |
原则:新增依赖前先检查上述每一层级是否已有可用方案。禁止随意引入新依赖。
1.3 API 路由开发
路由文件位于 src/server/routes/,每个端点一个文件。路由通过 server.ts 的 Bun.serve({ routes }) 声明式注册,使用 per-method handler 对象:
// server.ts 中的路由注册
routes: {
"/*": homepage, // HTML import,SPA fallback
"/api/*": () => jsonResponse(createApiError("API route not found", 404), { mode, status: 404 }),
"/api/meta": { GET: () => handleMeta(mode) },
"/api/dashboard": { GET: (req) => handleDashboard(new URL(req.url), store, mode) },
"/api/targets/:id/history": { GET: (req) => handleHistory(req.params.id, new URL(req.url), store, mode) },
"/api/targets/:id/metrics": { GET: (req) => handleMetrics(req.params.id, new URL(req.url), store, mode) },
"/health": { GET: () => handleHealth(mode) },
}
Handler 函数签名因端点而异:
// 无 store 的路由
export function handleHealth(mode: RuntimeMode): Response;
export function handleMeta(mode: RuntimeMode): Response;
// 带 target ID 和查询参数的路由
export function handleDashboard(url: URL, store: ProbeStore, mode: RuntimeMode): Response;
export function handleHistory(idStr: string, url: URL, store: ProbeStore, mode: RuntimeMode): Response;
export function handleMetrics(idStr: string, url: URL, store: ProbeStore, mode: RuntimeMode): Response;
请求处理流程:
Bun.serve的routes对象按路径 + HTTP 方法匹配请求- 未匹配方法的请求落入
/api/*通配符(返回 404) - 各 handler 内部通过
middleware.ts提供的validateTargetId、validateTimeRange、validatePagination、validateDashboardWindow、validateRecentLimit、validateMetricsBucket做参数校验,pageSize最大值为200 - 校验函数返回
Response实例表示校验失败(直接返回),返回数据对象表示通过 - 业务逻辑通过
store查询数据,用helpers.ts的jsonResponse、mapCheckResult、formatDuration等格式化输出
新增路由步骤:
- 在
src/server/routes/下创建<name>.ts - 实现 handler 函数并 export
- 在
server.ts的routes对象中注册路径和 method handler - 在
tests/server/app.test.ts中添加对应测试
1.4 共享工具
helpers.ts:跨路由共用的响应工具函数createApiError(error, status)— 构造 API 错误体createHeaders(mode, init)— 创建响应 Headers(生产模式附加安全头)createHealthResponse()— 构造健康检查响应formatDuration(ms)— 毫秒转为可读时长字符串jsonResponse(body, options)— JSON 响应构造mapCheckResult(row, type)— 数据库行转 API CheckResult,反序列化 observation 并按 checker type 动态生成 detail
middleware.ts:API 参数校验函数(validateTargetId、validateTimeRange、validatePagination、validateDashboardWindow、validateRecentLimit、validateMetricsBucket,其中pageSize和recentLimit上限为200)
1.5 类型定义规范
- 共享类型以
src/shared/api.ts为唯一源头,前后端共同引用 - 前端不得
import src/server/下的任何文件 - 严格联合类型优先于宽类型:如
phase: "status" | "duration" | ...而非phase: string - 后端内部扩展:
checker/types.ts中CheckResult通过extends共享版本的ApiCheckResult增加targetId等内部字段 - 存储层类型(
StoredTarget、StoredCheckResult)独立定义,与 API 类型分离 - Checker 类型分层:
checker/types.ts定义 base interface(ResolvedTargetBase、RawTargetConfig、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。
变量替换阶段由 variables.ts 负责,在 YAML 解析之后、AJV 契约校验之前执行。顶层 variables 支持 string/number/boolean 字面量,target 字符串字段支持 ${key}、${key|default} 和 $${key},解析优先级为 variables -> process.env -> 默认值;替换范围仅限 targets,且跳过 id 和 type 字段。
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。只有明确声明的动态键值表可以开放任意键名,例如 variables、http.headers、defaults.http.headers、expect.headers、cmd.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()
│ └── new IcmpChecker() ← 新增
│
├── schema/builder.ts ← 自动遍历 registry 生成全量 JSON Schema
├── schema/validate.ts ← 自动遍历 registry 构建 Ajv 校验
├── config-loader.ts ← 自动遍历 registry 调用 validate() + resolve()
├── engine.ts ← 自动按 target.type 分发到 execute()
└── store.ts ← 自动按 target.type 分发到 serialize()
每个 checker 目录的标准文件结构:
| 文件 | 职责 |
|---|---|
index.ts |
模块入口,re-export Checker 类 |
types.ts |
Checker 专属类型(RawXxxTargetConfig、Raw/Resolved XxxExpectConfig、ResolvedXxxTarget 等) |
schema.ts |
TypeBox 契约 schema(config / defaults / expect 三部分) |
validate.ts |
启动期语义校验(JSON Schema 无法表达的规则) |
execute.ts |
Checker 类:resolve(默认值合并 + 解析)、execute(执行检查)、serialize(DB 持久化) |
expect.ts |
Checker 专用断言函数 |
*.ts |
其他 checker 专属逻辑(如协议解析、编码、provider 适配、平台命令封装) |
1.7.2 步骤一:创建 Checker 目录与类型
在 src/server/checker/runner/tcp/types.ts 中定义 checker 专属类型(参考 http/types.ts、cmd/types.ts):
RawXxxTargetConfig— YAML 原始配置类型RawXxxExpectConfig/ResolvedXxxExpectConfig— Raw expect 字段类型与运行期 Resolved expect 执行计划类型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、cmd/schema.ts,使用 schema/fragments.ts 中的共享片段。
可复用的共享 fragments(来自 schema/fragments.ts):
| Fragment | 用途 |
|---|---|
durationSchema |
时长字符串("30s"、"5m"、"2h"、"7d"、"500ms") |
sizeSchema |
大小单位(字符串如 "10MB" 或整数) |
statusCodePatternSchema |
状态码(100-599 或 "2xx") |
stringMapSchema |
Record<string, string>(用于 headers / env) |
createValueMatcherSchema() |
ValueMatcher 对象(equals/contains/regex/数值比较等) |
createContentExpectationsSchema() |
ContentExpectations 数组(value/json/css/xpath 内容断言) |
createKeyedExpectationsSchema() |
动态键 KeyedExpectations(headers、DB rows 列值) |
matcherProperties() |
matcher 字段 Record,供 extractor schema 复用 |
注意:默认对象策略为 additionalProperties: false。只有明确的动态键值表(如 http.headers、cmd.env)可以开放任意键名。
1.7.4 步骤三:实现语义校验
在 src/server/checker/runner/tcp/validate.ts 中实现 JSON Schema 无法表达的语义规则(参考 http/validate.ts、cmd/validate.ts)。函数签名统一为:
export function validateTcpConfig(input: CheckerValidationInput): ConfigValidationIssue[];
共享校验工具(expect/validate.ts):
| 函数 | 用途 |
|---|---|
validateRawValueExpectation(value, path, targetName, opts?) |
校验 Raw ValueExpectation(primitive 或 matcher) |
validateRawContentExpectations(value, path, targetName) |
校验 Raw ContentExpectations 数组、extractor 互斥 |
validateRawKeyedExpectations(value, path, targetName, opts?) |
校验 Raw KeyedExpectations,可选大小写不敏感重复 |
validateJsonPath(path, rulePath, targetName) |
校验项目支持的 JSONPath 子集 |
isJsonValue(value) |
判断是否为合法 JSON value |
1.7.5 步骤四:实现 Checker 类
在 src/server/checker/runner/tcp/execute.ts 中实现 CheckerDefinition 接口的全部成员(参考 http/execute.ts、cmd/execute.ts):
TcpChecker implements Checker
readonly configKey ← "tcp"(对应 YAML 中的 target.tcp 字段)
readonly type ← "tcp"
readonly schemas ← tcpCheckerSchemas
validate(input) ← 调用 validateTcpConfig(input)
resolve(target, ctx)← 默认值合并 + 解析,返回 satisfies ResolvedTcpTarget
execute(target, ctx)← 执行检查,返回 CheckResult
serialize(target) ← 返回 { config, target } 用于 DB 持久化
resolve() 规范:
- 只做默认值合并、路径解析、单位转换,不执行校验
- 若 checker 支持 expect,必须保留
rawExpect为变量替换后的 Raw 配置快照,并生成只供execute()使用的 Resolvedexpect - 返回
satisfies ResolvedXxxTarget确保类型正确 - 通过
context.defaults[this.configKey]访问 checker 专属默认值(需as断言为具体类型)
expect 五层管线:每种断言模型从定义到执行经过五个阶段,每层使用对应的共享函数:
| 断言模型 | 类型层(Raw) | Schema 层 | Validate 层 | Resolve 层 | Execute 层 |
|---|---|---|---|---|---|
ValueExpectation |
number | ValueMatcher |
createValueMatcherSchema() |
validateRawValueExpectation() |
resolveValueExpectation() |
checkValueExpectation() |
ContentExpectations |
(ValueMatcher | ExtractorRule)[] |
createContentExpectationsSchema() |
validateRawContentExpectations() |
resolveContentExpectations() |
checkContentExpectations() |
KeyedExpectations |
Record<string, ValueExpectation> |
createKeyedExpectationsSchema() |
validateRawKeyedExpectations() |
resolveKeyedExpectations() |
checkKeyedExpectations() |
选择哪种模型参考 1.10 expect 字段选择规范的决策树。
resolve 中的标准模式:
// resolve() 内:逐字段调用对应的 resolve 函数,未配置的字段保持 undefined
const rawExpect = raw.expect ?? {};
expect: {
durationMs: rawExpect.durationMs != null ? resolveValueExpectation(rawExpect.durationMs) : undefined,
body: rawExpect.body != null ? resolveContentExpectations(rawExpect.body) : undefined,
headers: rawExpect.headers != null ? resolveKeyedExpectations(rawExpect.headers) : undefined,
}
execute 中的标准模式:
// execute() 内:按快速失败顺序依次检查,首个失败即返回
const r = resolved.expect;
if (r.durationMs) {
const result = checkValueExpectation(elapsed, r.durationMs, { phase: "duration", path: "durationMs" });
if (!result.matched) return { ..., failure: result.failure, matched: false };
}
if (r.body) {
const result = checkContentExpectations(bodyText, r.body, { phase: "body", path: "body" });
if (!result.matched) return { ..., failure: result.failure, matched: false };
}
execute() 规范:
- 始终记录
timestamp(ISO 字符串)和start = performance.now() - 通过
ctx.signal(AbortSignal)支持超时取消 - 首个 expect 失败即停止,返回带
failure的结果 - 成功时
failure: null, matched: true - 异常时使用
errorFailure(phase, path, message)构造 failure - 不匹配时使用
mismatchFailure(phase, path, expected, actual, message)构造 failure mismatchFailure的expected参数应传用户可读值,使用displayValueExpectation(matcher)解包单字段{ equals: x }为x
可用的共享断言工具(checker/expect/):
| 模块 | 函数 | 用途 |
|---|---|---|
failure.ts |
errorFailure(phase, path, msg) |
构造错误类型 failure |
failure.ts |
mismatchFailure(phase, path, expected, actual, msg) |
构造不匹配类型 failure |
failure.ts |
truncateActual(value, maxLen?) |
截断过长的 actual 值(默认 200 字符) |
value.ts |
applyValueMatcher(actual, matcher, options?) |
执行 Resolved ValueMatcher AND 匹配 |
value.ts |
checkValueExpectation(actual, matcher, options) |
执行 matcher 并返回 ExpectationResult |
value.ts |
resolveValueExpectation(raw) |
Raw ValueExpectation → Resolved ValueExpectation |
value.ts |
displayValueExpectation(matcher) |
解包单字段 { equals: x } 为 x,用于 failure 展示 |
value.ts |
evaluateJsonPath(json, path) |
JSONPath 提取 |
content.ts |
checkContentExpectations(source, expectations, options) |
执行 Resolved ContentExpectations |
content.ts |
resolveContentExpectations(raw) |
Raw → Resolved ContentExpectations |
keyed.ts |
checkKeyedExpectations(actual, expectations, options) |
执行 Resolved KeyedExpectations |
keyed.ts |
resolveKeyedExpectations(raw) |
Raw Record → Resolved 有序数组 |
headers.ts |
checkHeaderExpectations(headers, expectations, options?) |
HTTP/LLM headers 大小写不敏感包装 |
status.ts |
checkStatusCode(actual, expected, phase, path) |
HTTP/LLM status code(精确数值与 1xx-5xx 范围) |
validate.ts |
validateRawValueExpectation/ContentExpectations/KeyedExpectations |
Raw expectation 语义校验(不修改输入) |
Checker 专属断言(如需要)放在同目录的 expect.ts 中,参考 cmd/expect.ts(checkExitCode)、tcp/expect.ts(checkConnected)、udp/expect.ts(checkResponded)和 icmp/expect.ts(checkAlive)。HTTP/LLM 复用的 status 与 headers 断言放在共享 expect 模块。
1.7.6 步骤五:创建模块入口并注册
创建 src/server/checker/runner/tcp/index.ts(re-export Checker 类)。
在 src/server/checker/runner/index.ts 中添加一行导入和一个数组元素(参考现有 HttpChecker/CommandChecker)。
注册后,以下管线会通过 registry 自动委托,无需新增 type 分支:
| 模块 | 自动行为 |
|---|---|
schema/builder.ts |
遍历 registry 生成全量 JSON 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 — 完整验证(check + build)
□ README.md — 用户文档
□ DEVELOPMENT.md — 项目结构目录树
1.8 数据存储规范
基于 bun:sqlite,WAL 模式运行,数据库文件位于配置的 dataDir 下。
核心方法:
| 方法 | 用途 |
|---|---|
syncTargets(targets) |
启动期同步 targets(基于配置 id 做 upsert + delete 事务) |
insertCheckResult() |
写入单条检查结果 |
getTargets() |
查询全部 targets(default 分组优先排序) |
getLatestChecksMap() |
批量获取每个 target 的最新检查结果(单次 SQL 聚合) |
getAllTargetWindowStats(from, to) |
批量获取窗口内每个 target 的 total/up/down 基础计数 |
getDashboardIncidentStates(from, to) |
获取 Dashboard 窗口内状态序列,供应用层计算 incidents |
getAllRecentSamples(limit) |
批量获取每个 target 的最近 N 条采样(用于状态条和连续状态) |
getTargetCheckpoints(targetId, from, to) |
获取单目标窗口内检查点序列,供 metrics 应用层分桶和故障分析 |
getTargetDurations(targetId, from, to) |
获取单目标窗口内成功检查耗时升序数组,供应用层计算 P95/P99 |
getHistory() |
分页查询历史记录 |
getRecentSamples() |
获取最近 N 条采样数据(用于状态条渲染) |
prune(retentionMs) |
按 retention 策略清理过期数据(由 engine 定时调用) |
Statement 使用规范:
| 场景 | 方式 | 原因 |
|---|---|---|
| 单次读/写 | this.db.query(sql).get()/all()/run() |
bun:sqlite 内置 statement 缓存,自动复用 |
| 事务内多次复用 | this.db.prepare(sql) 缓存为局部变量 |
事务闭包中需要持有引用 |
查询优化:
- 避免 N+1 查询:批量场景优先用单次 SQL 聚合(GROUP BY、子查询 JOIN)+ 内存组装
- 新增批量查询方法时必须编写对应单元测试
GET /api/dashboard的响应组装通过getLatestChecksMap+getAllTargetWindowStats+getAllRecentSamples+getDashboardIncidentStates实现批量查询
轻数据库指标计算规范:
- 数据库只负责存储、筛选、排序、分页、LIMIT 和标准 SQL 基础聚合(如
COUNT、SUM(CASE)、AVG、MIN、MAX、GROUP BY),用于减少应用层输入数据量 - 指标语义必须在后端应用层实现,包括可用率舍入、百分位、状态翻转、故障段识别、MTTR、最长故障、连续状态、趋势 UTC 小时分桶和窗口边界处理
- 禁止用 SQLite 专有时间函数承载趋势分桶语义,禁止用复杂 SQL/window function 承载故障事件或恢复时长等业务规则
Schema:
targets表:id(TEXT PRIMARY KEY,配置 target id)、name(TEXT,可 NULL,展示名称)、description(TEXT,可 NULL,描述)、type、target(展示摘要)、config(JSON)、interval_ms、timeout_ms、expect(JSON)、grpcheck_results表:target_id(TEXT FK CASCADE,引用配置 target id)、timestamp、matched(0/1)、duration_ms、observation(JSON TEXT)、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(),Cmd 和 ICMP 在 signal abort 时proc.kill() - 结果写入:检查结果通过
store.insertCheckResult()写入 SQLite,engine 基于配置 target id 确认目标仍存在;detail 为 API 层从 observation 派生,不进入存储层 - 异常可观测:
probeGroup()对Promise.allSettled的 rejected 结果通过索引关联 target,并写入phase:"internal"的失败记录 - 数据清理:当
retentionMs > 0时,engine 启动时立即执行一次store.prune(),之后每小时定时执行,按timestamp清理过期数据 - 生命周期:
start()/stop()管理定时器(含调度定时器和清理定时器),stop()清理所有setInterval
1.10 expect 断言系统
两层模型:观测值收集 → 规则校验。共享断言基础设施位于 checker/expect/,checker 专属状态断言位于各自目录。
Raw vs Resolved:用户 YAML 写的是 Raw 形态(primitive 简写、{ json: { path, equals } } 提取器对象、Record<string, value> 键值表),config-loader 的 resolve 阶段将其转换为 Resolved 形态供运行期执行({ equals: primitive }、{ kind, matcher, ... } content 联合、{ key, matcher }[] 有序数组)。Store 持久化 Raw 快照(rawExpect),checker.execute 消费 Resolved expect。
共享模型:
| 模型 | 用途 | 典型字段 |
|---|---|---|
ValueExpectation |
单个值、数字指标和字符串元数据断言 | durationMs、rowCount、usage.totalTokens、finishReason |
ContentExpectations |
返回内容或半结构化内容断言,必须是数组 | body、stdout、stderr、banner、response、output、result |
KeyedExpectations |
动态键值断言,字面量等价于 { equals: value } |
headers、DB rows[] 中的列值 |
ValueMatcher 支持 equals、contains、regex、empty、exists、gte、lte、gt、lt。一个 matcher 对象内多个字段为 AND 语义;exists: false 不能和其他 matcher 组合;equals 使用 es-toolkit/isEqual 做 JSON 深度相等;regex 固定为无 flags 的 new RegExp(pattern).test(String(actual))。ValueExpectation Raw 输入可使用 string、number、boolean 或 null 简写,resolve 阶段归一化为 { equals: value };数组和对象简写不支持,必须显式写成 { equals: ... }。
ContentExpectations 数组按顺序快速失败。数组项可以是直接 matcher,也可以是 { json: {...} }、{ css: {...} }、{ xpath: {...} } 提取器规则;一条规则不能混用直接 matcher 和 extractor,多个 extractor 也不能共存。Extractor 未配置 matcher 时等价于 exists: true。对对象或数组源执行直接 contains/regex 时会先 JSON 序列化,equals 仍对原始结构做深度相等。
启动期语义校验统一由 expect/validate.ts 负责,会校验空 matcher、未知字段、字段类型、exists:false 组合、ContentExpectations 互斥性、JSONPath 子集、XPath 可编译性、regex 可编译性、ReDoS 风险以及 HTTP/LLM headers 大小写归一化后重复 key。语义校验不修改 Raw 输入。旧字段 match、maxDurationMs、ICMP 的 max* 阈值字段不再支持。
快速失败顺序:
| Checker | 顺序 |
|---|---|
| HTTP | status → headers → body → durationMs |
| Cmd | exitCode → durationMs → stdout → stderr |
| DB | durationMs → rowCount → rows → result |
| TCP | connected → banner → durationMs |
| UDP | responded → responseSize → response → sourceHost → sourcePort → durationMs |
| ICMP | alive → packetLossPercent → avgLatencyMs → maxLatencyMs → durationMs |
| LLM http | status → headers → output → finishReason → rawFinishReason → usage → durationMs |
| LLM stream | status → headers → stream.completed → stream.firstTokenMs → output → finishReason → rawFinishReason → usage → durationMs |
HTTP checker 的 durationMs 覆盖完整执行(含重定向、按需响应体读取、解码和 expect 校验)。未配置 body expectation、status 失败或 headers 失败时不读取 body;有 body expectation 时,在读取 body 前可先检查 durationMs 上界 matcher 是否已不可能通过,避免无意义读取。
expect 字段选择规范:
新增或修改 checker 的 expect 字段时,按以下决策树选择合适的断言模型(选定后,各层的具体函数映射参考 1.7.5 的五层管线表):
expect 字段
│
├─ 状态类结果,结果集合小且稳定
│ └─ enum / boolean
│ HTTP/LLM status、Cmd exitCode、TCP connected、
│ UDP responded、ICMP alive
│
├─ 数字指标 / 字符串元数据
│ └─ ValueMatcher
│ durationMs、rowCount、responseSize、sourceHost、sourcePort、
│ packetLossPercent、avgLatencyMs、maxLatencyMs、
│ finishReason、rawFinishReason、usage.*、stream.firstTokenMs
│
└─ 返回内容 / 半结构化内容 / 不完全确定的值
├─ 内容断言 → ContentExpectations(数组)
│ HTTP body、Cmd stdout/stderr、TCP banner、
│ UDP response、LLM output、DB result
│
└─ 键值断言 → KeyedExpectations(动态键对象)
HTTP/LLM headers、DB rows[] 中的列值
选择原则:
-
状态类字段使用 enum 或 boolean。结果集合小且稳定时(如 HTTP status 200/2xx、exitCode 0),枚举和布尔比 matcher 更贴近协议语义,配置也更直观。不要为了统一而把状态类字段改成 ValueMatcher。
-
单值数字指标和字符串元数据使用 ValueMatcher。观测值是一个明确的标量(耗时、行数、丢包率、finish reason),但阈值不确定时,使用
{ lte: 100 }或{ regex: "^(stop|end)$" }等 matcher 表达;精确匹配 primitive 可直接写100或"stop"。 -
返回内容使用 ContentExpectations 数组。观测值是文本、JSON、HTML 或 XML 内容,且可能需要多步提取或多条规则时,使用 ContentExpectations。即使只有一条规则也必须写成数组形式(
[{ contains: "ok" }]),不支持对象快捷写法。 -
键值对使用 KeyedExpectations。观测值是动态键值表(如 headers),且需要对每个键独立断言时使用。字面量值自动等价于
{ equals: value }。 -
不要混用模型。一个 expect 字段只能对应一种断言模型。例如
finishReason是单值字符串元数据,用 ValueMatcher 而非 ContentExpectations(ContentExpectations 的 json/css/xpath 提取器对单字符串无意义,且会增加数组包装的配置冗余)。 -
failure phase 命名遵循去单位后缀规则。数字指标字段的 phase 去掉单位后缀(
durationMs→duration、packetLossPercent→packetLoss、avgLatencyMs→avgLatency),不带单位后缀的字段直接使用字段名(rowCount→rowCount、finishReason→finishReason)。 -
实现时参考 1.7.5 五层管线 中的对应表。决策树解决"选哪种模型",五层管线表解决"每种模型从类型定义到执行分别调哪个函数"。
1.11 错误模式
- API 错误:
{ error: "描述", status: <code> },状态码 400/404/503 - CheckFailure:
{ kind: "error"|"mismatch", phase, path, expected?, actual?, message } - 错误处理:expect 校验失败记录首个失败原因;网络/超时/进程崩溃统一为
kind:"error",请求/TLS/timeout 错误归属phase:"request",body 超限/解码/解析错误归属phase:"body" - 日志:解析失败等非致命异常用
console.warn,启动失败用console.error+process.exit(1)
1.12 测试规范
- 测试目录
tests/镜像src/目录结构,但共享 expect 模块的测试集中放在tests/server/checker/runner/shared/下,覆盖failure.ts、value.ts(operator)、content.ts(body/text)、keyed.ts(headers/duplicate-key)、validate.ts(shorthand)和redos.ts - 使用
bun:test框架(describe/test/expect),测试数据库用临时目录 +tmpdir() - 新增 store 方法必须编写单元测试;新增 API 端点必须在
app.test.ts中添加集成测试 - 测试后清理:
afterAll中store.close()+rm(tempDir, { recursive: true })
二、前端开发指引
2.1 技术栈概览
| 层面 | 技术 | 用途 |
|---|---|---|
| 框架 | React 19 | UI 组件开发 |
| 构建 | Bun HTML import(fullstack 模式) | 开发服务与生产构建 |
| 语言 | TypeScript 6 | 类型安全 |
| UI 库 | TDesign React + tdesign-icons-react | UI 组件与图标 |
| 数据层 | TanStack Query (React Query) + React Query Devtools | 服务端状态管理与自动轮询 |
| 图表 | Recharts | 拨测趋势折线图 |
| 动画 | @number-flow/react | 倒计时数字滚动过渡 |
| 路由 | 无(单页面 Dashboard) | 仅需 Drawer/Tab 做页面内导航 |
不引入的依赖:React Router(单页面场景不需要)、状态管理库(TanStack Query 即服务端状态层,组件内用 useState 足够)、Vite(已由 Bun 原生 fullstack 替代)
2.2 组件树与数据流
main.tsx
└── StrictMode
└── ErrorBoundary(React 错误边界)
└── QueryClientProvider(TanStack Query 全局挂载)
├── App(根组件,Layout + HeadMenu 骨架)
│ ├── useThemePreference() ─── Header 主题模式 RadioGroup(系统/明亮/黑暗,本地存储记忆 + theme-mode 应用)
│ ├── useDashboard(refreshInterval) ─── GET /api/dashboard?window=24h&recentLimit=30(动态刷新间隔,RadioGroup 频率选择 + 倒计时/手动刷新按钮)
│ ├── SummaryCards(单 Card 内嵌居中 Statistic,无 shadow)
│ └── TargetBoard(目标列表,Space 24px 间距)
│ ├── DashboardResponse.targets
│ ├── useMeta() ───── GET /api/meta(应用生命周期内缓存)
│ └── TargetGroup[](Card 包裹 PrimaryTable,headerBordered)
│ └── PrimaryTable ← createTargetTableColumns(checkerTypes)
│ └── TargetDetailDrawer(目标详情抽屉,响应式默认宽度、支持鼠标拖拽调整,TDesign 生命周期控制)
│ └── useTargetDetail() ── 按需发起 metrics 查询,history 延迟到记录 Tab 激活后请求
│ ├── activeTab 受控 Tabs 状态,每次打开重置为 overview
│ ├── OverviewTab → Descriptions(直接展示)+ 4×2 统计卡片 + TrendChart
│ └── HistoryTab → PrimaryTable(分页历史记录,TabPanel 懒渲染 + destroyOnHide=false)
└── ReactQueryDevtools(开发工具,仅开发环境)
Hook 架构:
hooks/use-queries.ts(全局面板级查询)
├── queryKeys(dashboard/meta/metrics 结构化 query key)
├── useDashboard(refetchInterval) → /api/dashboard?window=24h&recentLimit=30(动态刷新间隔,由调用方传入)
├── useTargetMetrics() → /api/targets/:id/metrics(详情按需加载)
└── useMeta() → /api/meta(staleTime: Infinity)
hooks/use-target-detail.ts(Drawer 状态与详情级条件查询)
├── 内部复用 useDashboard(false) 的缓存来查找 selectedTarget
├── activeTab 受控 Tabs 状态(每次 openDrawer 重置为 overview)
├── useTargetMetrics(/api/targets/:id/metrics)(条件查询:enabled 仅当 Drawer 打开且时间范围有效)
└── useQuery(/api/targets/:id/history)(条件查询:enabled 仅当 Drawer 打开 + 时间范围有效 + activeTab=history)
hooks/use-theme-preference.ts(浏览器 UI 偏好)
├── ThemePreference: system / light / dark(RadioGroup 受控值)
├── EffectiveTheme: light / dark(写入 document.documentElement theme-mode)
├── localStorage key: dial.theme.preference(同一浏览器记忆)
└── matchMedia("(prefers-color-scheme: dark)")(系统模式下跟随系统明暗变化)
2.3 TanStack Query 数据层
Query Key 规范
const queryKeys = {
dashboard: () => ["dashboard", "24h", 30] as const,
meta: () => ["meta"] as const,
metrics: (targetId: number, from: string, to: string, bucket: "1h") =>
["metrics", targetId, from, to, bucket] as const,
history: (targetId: number, from: string, to: string, page: number) => ["history", targetId, from, to, page] as const,
};
- Key 使用 structured array(非字符串),以便精确匹配和按 prefix 失效
- 使用
as const保持字面量类型 - 排序:scope → id → 参数(粒度从粗到细)
查询配置规范
// 全局面板级查询(需要持续刷新)
useQuery({
queryKey: queryKeys.dashboard(),
queryFn: () => fetchJson<DashboardResponse>("/api/dashboard?window=24h&recentLimit=30"),
refetchInterval, // 由调用方传入的动态刷新间隔(false 禁用轮询)
refetchIntervalInBackground: false, // 切后台不轮询
});
// 详情级查询(按需加载)
useQuery({
queryKey: selectedTargetId ? queryKeys.metrics(id, from, to, "1h") : ["metrics", "disabled"],
queryFn: () => fetchJson(`/api/targets/${id}/metrics?...`),
enabled: selectedTargetId !== null && !!timeFrom && !!timeTo, // 条件查询
});
fetch 封装
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 全局配置
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 { ... })
import type { TargetStatus } from "../../shared/api";
import { StatusDot } from "./StatusDot";
interface TargetGroupProps {
name: string;
targets: TargetStatus[];
onTargetClick: (target: TargetStatus) => void;
}
export function TargetGroup({ name, targets, onTargetClick }: TargetGroupProps) {
// ...
}
组件拆分原则
- 展示组件(
components/):纯渲染逻辑,通过 props 接收数据,通过回调返回事件 - 容器逻辑放在 hooks 中,组件只做数据消费
- 常量数据(列定义、排序器、筛选器、颜色阈值)放在
constants/,不放在组件内部 - 工具函数(时间处理等)放在
utils/,保持纯函数无副作用
现有组件清单
| 组件 | 文件 | 用途 |
|---|---|---|
App |
app.tsx |
根组件,Layout + HeadMenu 骨架、主题模式选择、刷新倒计时、Skeleton 加载 |
ErrorBoundary |
components/ErrorBoundary.tsx |
React 错误边界,捕获渲染异常并展示降级 UI |
SummaryCards |
components/SummaryCards.tsx |
总览统计卡片(单 Card 内嵌居中 Statistic,无 shadow) |
TargetBoard |
components/TargetBoard.tsx |
按分组渲染目标表格列表(Space 24px 间距) |
TargetGroup |
components/TargetGroup.tsx |
单个分组 Card(title+actions+headerBordered)+ PrimaryTable |
TargetDetailDrawer |
components/TargetDetailDrawer.tsx |
目标详情抽屉(响应式默认宽度、支持鼠标拖拽调整、TDesign 生命周期控制、preventScrollThrough、受控 Tabs、记录 TabPanel 懒渲染) |
OverviewTab |
components/OverviewTab.tsx |
目标详情概览(Descriptions 直接展示 + 4×2 统计卡片 + 趋势) |
HistoryTab |
components/HistoryTab.tsx |
目标历史记录表格和分页 |
TrendChart |
components/TrendChart.tsx |
Recharts 趋势折线图(耗时+延迟范围) |
StatusDot |
components/StatusDot.tsx |
圆形状态指示点(绿/红) |
StatusBar |
components/StatusBar.tsx |
最近采样状态条(多色块 + Tooltip 提示时间和状态) |
RefreshCountdown |
components/RefreshCountdown.tsx |
Header 刷新倒计时(NumberFlow 数字滚动),手动刷新按钮,刷新中/等待首次刷新文本 |
2.5 新增功能开发步骤
以"新增一个详情页面 Tab"为例:
- 确认数据需求:是已有 API 数据还是需要新端点?
- 如有新端点,先在
src/server/routes/添加,参考 1.3 新增路由步骤 - 如有新字段,更新
src/shared/api.ts类型定义
- 如有新端点,先在
- 实现 hooks:全局查询放在
src/web/hooks/use-queries.ts;目标详情条件查询放在src/web/hooks/use-target-detail.ts(写好queryKey和enabled条件) - 编写组件:在
src/web/components/创建组件文件- 在
TargetDetailDrawer.tsx中新增<Tabs.TabPanel>引用
- 在
- 编写常量:如有列定义/排序器/筛选器,放在
src/web/constants/ - 编写测试:在
tests/web/下添加对应的单元测试
2.6 样式开发规范
前端基于 TDesign React 构建 UI,样式开发遵循以下优先级(从高到低):
- 使用 TDesign 组件:布局、间距、排版优先使用 TDesign 组件(如 Space、Divider、Typography)
- 使用 TDesign 组件 props:通过组件的 props 参数控制外观(如
theme、variant、size) - 使用 TDesign CSS tokens:颜色、间距、字体等使用
--td-*CSS 变量(如--td-success-color、--td-comp-margin-xxl) - 在 styles.css 中定义 CSS 类:无法通过上述方式满足的样式需求,集中定义在
styles.css中 - 自行开发组件:仅在 TDesign 无法满足需求时自行开发
红线:
- 严禁在组件中使用
style属性内联调整样式 - 严禁通过 CSS 覆盖 TDesign 组件内部类名(如
.t-tab-panel),如需定制使用组件的classNameprop - 严禁使用
!important - 颜色统一使用 TDesign CSS tokens(
--td-success-color、--td-error-color、--td-warning-color等),不使用硬编码色值
styles.css 组织:
- 自定义 CSS 变量(如可用率渐变色
--avail-0~--avail-9)定义在:root中 - 布局类(
.dashboard、.dashboard-header-controls)定义全局页面结构和 Header 右侧单行操作区 - 组件修饰类(
.status-dot--up、.latency-ok)为自定义视觉组件提供样式变体 - TDesign 表格行高亮(
.row-down)通过rowClassNameprop 应用
2.7 前端测试规范
- 测试目录:
tests/web/,结构对应src/web/ - 重点测试 constants/ 中的纯函数(排序器、筛选器、颜色阈值、类型映射等)
- 使用
bun:test框架
三、项目运行、集成与打包
3.1 开发期运行
bun run dev probes.yaml
scripts/dev.ts 同时启动两个进程:
- Bun API server(端口 3000):后端 API 服务,
--watch监听后端文件变更自动重启 - Vite dev server(端口 5173):前端 SPA + HMR 热更新
开发时访问 http://127.0.0.1:5173,Vite 自动将 /api 和 /health 请求代理到后端。
也可以单独启动:
bun run dev:server probes.yaml # 仅启动后端 API server
bun run dev:web # 仅启动 Vite dev server
3.2 前后端集成方式
双进程开发架构
开发模式下前后端分别由 Vite 和 Bun 服务:
- Vite dev server 负责前端 SPA、HMR、模块热替换
- Bun API server 负责后端 API 路由
- Vite 通过 proxy 配置将
/api/*和/health转发到 Bun
生产模式架构
生产模式下前端通过 Vite 构建为静态资源,通过 import with { type: "file" } 嵌入 Bun 可执行文件:
// server.ts
const server = Bun.serve({
fetch(req) {
// staticAssets 存在时服务嵌入的前端资源
return serveStaticAsset(new URL(req.url).pathname, staticAssets);
},
routes: {
"/api/*": () => ..., // API 通配符(未匹配路由返回 404)
"/api/dashboard": { GET: (req) => handleDashboard(...) },
"/health": { GET: () => handleHealth(mode) },
// ...
},
});
路由优先级
Bun routes 的匹配规则:具体路径 > 通配符。/api/dashboard 优先于 /api/*,/health 优先于 /*。
未匹配 method 的请求(如 POST /api/dashboard)会落入 /api/* 通配符返回 404。
非 API 路径由 fetch fallback 处理:有文件扩展名的返回对应静态资源或 404,无扩展名的返回 SPA index.html。
3.3 构建打包
构建命令
bun run build
构建流程
scripts/build.ts 执行三步流水线:
1. Vite build → dist/web/ (前端静态资源,含 code splitting)
2. Code generation → .build/static-assets.ts + .build/server-entry.ts
3. Bun compile → dist/dial-server (单可执行文件)
- Vite 构建前端资源到
dist/web/,自动 code splitting(vendor-react、vendor-tdesign、vendor-chart) - Code generation 扫描
dist/web/生成import with { type: "file" }声明,将资源嵌入 binary - Bun compile 以
.build/server-entry.ts为入口编译最终可执行文件 .build/临时目录在构建完成后自动清理
产物
| 产物 | 用途 |
|---|---|
dist/dial-server |
生产可执行文件(含前端资源,单文件部署) |
dist/web/ |
Vite 构建的前端资源(构建中间产物) |
构建参数
| 环境变量 | 说明 |
|---|---|
BUN_TARGET/BUILD_TARGET |
交叉编译目标平台(如 bun-linux-x64) |
运行可执行文件
./dist/dial-server probes.yaml
启动后:
- 访问
http://127.0.0.1:3000/→ 返回前端 SPA - 访问
http://127.0.0.1:3000/api/*→ 返回后端 API - 访问
/dashboard等前端路由 → SPA fallback 到 index.html
清理
bun run clean
# 清理 dist/ 构建产物和 .build/ 临时文件
3.4 开发工作流
日常开发循环
bun run dev probes.yaml # 启动双进程开发环境(Vite + API server)
# 访问 http://127.0.0.1:5173
# 修改前端代码 → Vite HMR 热更新 / 修改后端代码 → --watch 自动重启
bun run check # 提交前运行完整质量检查
完整验证流程
bun run verify
# = bun run check + bun run build
verify 适合 CI 或正式提交前,会完整验证类型检查、lint、格式、单元测试和生产构建。
3.5 Executable/E2E 验证
原 scripts/smoke.ts 覆盖过薄,已从当前工作流移除。后续如需验证 production executable 的 API、静态资源服务、SPA fallback 行为,应重新设计独立的 executable/E2E 测试。
3.6 脚本说明
| 脚本 | 文件 | 说明 |
|---|---|---|
bun run dev |
scripts/dev.ts |
双进程开发服务(Vite :5173 + API :3000) |
bun run dev:server |
src/server/dev.ts |
仅启动后端 API server |
bun run dev:web |
Vite CLI | 仅启动 Vite dev server |
bun run build |
scripts/build.ts |
Vite → codegen → Bun compile 三步构建 |
bun run schema |
scripts/generate-config-schema.ts |
生成 probe-config.schema.json |
bun run schema:check |
scripts/generate-config-schema.ts |
检查配置 schema 导出物是否同步 |
bun run clean |
scripts/clean.ts |
清理构建缓存与临时文件 |
3.7 环境变量
| 变量 | 用途 | 默认值 |
|---|---|---|
BUN_TARGET/BUILD_TARGET |
交叉编译目标平台(仅在 bun run build 时有效) |
当前平台 |
3.8 项目配置文件
| 文件 | 用途 |
|---|---|
package.json |
项目信息、脚本、依赖声明 |
tsconfig.json |
TypeScript 配置(ESNext 模块、严格模式) |
eslint.config.js |
ESLint 规则(含前端不得 import server 的检查) |
commitlint.config.js |
commitlint 提交信息格式校验 |
.prettierrc.json |
Prettier 格式化规则(printWidth: 120) |
.prettierignore |
Prettier 排除路径 |
probes.example.yaml |
配置文件示例 |
opencode.json |
OpenCode 工具配置(TDesign MCP server) |
3.9 依赖管理
- 包管理器:仅使用
bun,禁止使用 npm、pnpm、yarn - 安装依赖:
bun install - 运行工具:使用
bunx,禁止使用npx、pnpx - 锁文件:
bun.lock
3.10 目录约定
| 目录 | 约定 |
|---|---|
src/server/ |
后端代码,不能 import src/web/(HTML import 除外) |
src/web/ |
前端代码,不能 import src/server/ |
src/shared/ |
前后端共享类型,双向可引用 |
scripts/ |
独立运行脚本,可 import 项目源码 |
tests/ |
测试目录,结构镜像 src 目录 |
dist/ |
构建产物(gitignore) |
openspec/ |
OpenSpec 变更管理与规格文档 |
data/ |
默认数据目录(gitignore,运行期生成 SQLite) |
代码质量
项目使用多层代码质量保障体系:ESLint 类型感知规则 + Perfectionist 导入排序 + Prettier 格式化(通过 eslint-plugin-prettier 集成至 ESLint)+ TypeScript 严格模式 + Git hooks 自动化。
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 统一签名需要) |
noPropertyAccessFromIndexSignature |
true | 禁止通过点号访问索引签名属性,强制使用括号语法 |
noUncheckedIndexedAccess |
true | 数组/Map 访问必须运行时真值检查 |
noImplicitOverride |
true | 子类覆盖父类方法时必须显式使用 override 关键字 |
verbatimModuleSyntax |
true | 强制 import type 纯类型导入 |
Git Hooks
通过 husky 在 commit 阶段自动执行检查:
| Hook | 行为 |
|---|---|
pre-commit |
lint-staged 对变更文件运行 eslint --fix(TS/TSX,含 Prettier 格式修复)或 prettier --write(MD/JSON/YAML) |
commit-msg |
commitlint 校验提交信息格式 类型: 简短描述 |
提交类型限定:feat、fix、refactor、docs、style、test、chore。
bun install 时自动初始化 husky hooks,无需手动配置。
测试
项目采用两层测试体系:单元测试 + 组件测试。所有测试使用 bun:test 运行。
测试分层
| 层级 | 覆盖范围 | 位置 | 命令 |
|---|---|---|---|
| 单元测试 | 后端函数、纯函数、常量 | tests/server/**/*.test.ts、tests/web/{constants,utils,hooks}/**/*.test.ts |
bun test tests/server、bun test tests/web |
| 组件测试 | React 组件渲染和交互 | tests/web/components/**/*.test.tsx |
bun test tests/web/components |
运行命令
bun test # 运行所有单元测试和组件测试
bun test tests/server # 只运行后端单元测试
bun test tests/web # 只运行前端测试(单元 + 组件)
bun run check # 日常开发(类型检查 + lint + 测试)
bun run verify # 完整验证(check + 构建)
组件测试环境
组件测试使用 jsdom 模拟浏览器环境,配置位于 tests/setup.ts(通过 bunfig.toml preload 加载):
- jsdom 提供完整的 DOM 环境
- TDesign 组件所需的 polyfill:ResizeObserver、IntersectionObserver、matchMedia、attachEvent
- recharts 图表组件被 mock 为占位元素(SVG 渲染在 jsdom 中不可靠)
编写规范
- 优先使用
@testing-library/react的语义化查询(getByText、getByRole)而非 CSS 选择器 - 测试用户行为而非实现细节:模拟用户点击、输入等操作,而非直接调用组件方法
- 只 mock 系统边界:mock fetch 返回预设响应,使用真实的 QueryClientProvider 包裹组件
- 组件测试文件命名:
tests/web/components/ComponentName.test.tsx
已知限制
当前不做告警通知、拨测目标动态增删、认证鉴权和分布式部署。