diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..25c1319 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,131 @@ +# Contributing + +本文档是 DiAL 的贡献入口,重点指导新增或修改 checker。通用开发命令和全局规则见 [DEVELOPMENT.md](DEVELOPMENT.md),完整开发专题见 [docs/development/](docs/development/README.md)。 + +## 通用贡献规则 + +- 使用中文编写注释、文档和项目内交流内容。 +- 仅使用 `bun` 作为包管理器,禁止使用 npm、pnpm、yarn。 +- 运行工具使用 `bunx`,禁止使用 npx、pnpx。 +- 新增代码优先复用已有组件、工具和依赖库,不引入新依赖。 +- 新增逻辑必须编写测试,不允许跳过测试。 +- 每次代码变更必须执行文档影响分析,并按影响范围更新对应文档或说明无需更新。 + +## 新增或修改 Checker 前必读 + +| 文档 | 用途 | +| ------------------------------------------------------- | ---------------------------------------------------- | +| [README.md](README.md) | 项目定位、快速开始和用户入口 | +| [DEVELOPMENT.md](DEVELOPMENT.md) | 开发入口、常用命令、质量门禁和全局规则 | +| [docs/README.md](docs/README.md) | 文档索引和文档归属矩阵 | +| [Checker 开发](docs/development/checker-development.md) | checker 实现机制和详细 checklist | +| [配置文件](docs/user/configuration.md) | YAML 顶层结构、变量和 target 通用字段 | +| [校验规则](docs/user/expectations.md) | ValueMatcher、ContentExpectations、KeyedExpectations | +| [Checker 用户文档](docs/user/checkers/README.md) | 已支持 checker 的配置和示例 | + +还应阅读现有同类 checker 的实现和测试,例如 `src/server/checker/runner/http/`、`src/server/checker/runner/cmd/` 和对应 `tests/server/checker/runner/` 目录。 + +## Checker 设计原则 + +- 每个 checker 必须自包含在 `src/server/checker/runner//`。 +- checker 专属类型、schema、validate、execute、expect 和协议辅助逻辑放在同一目录。 +- 注册只修改 `src/server/checker/runner/index.ts`,中间层不新增 type switch。 +- schema 层只描述契约,语义规则放入 `validate.ts`。 +- `resolve()` 只做默认值填充、路径解析和单位转换,不执行校验。 +- `execute()` 必须支持 `CheckerContext.signal` 超时取消。 +- expect 字段必须选择合适断言模型,不为了统一而滥用 ValueMatcher。 +- failure phase 命名遵循去单位后缀规则,例如 `durationMs` 对应 `duration`。 + +## 文件清单 + +新增 checker 通常需要创建或修改: + +```text +src/server/checker/runner//types.ts +src/server/checker/runner//schema.ts +src/server/checker/runner//validate.ts +src/server/checker/runner//execute.ts +src/server/checker/runner//expect.ts +src/server/checker/runner//index.ts +src/server/checker/runner/index.ts +tests/server/checker/runner// +probes.example.yaml +probe-config.schema.json +docs/user/checkers/.md +docs/user/checkers/README.md +``` + +如果修改通用断言模型、开发机制或文档同步规则,还需要更新 `docs/user/expectations.md`、`docs/development/checker-development.md`、本文件或 `docs/README.md`。 + +## 分层要求 + +| 层 | 职责 | +| ----------------- | -------------------------------------------- | +| `types.ts` | Raw/Resolved target 和 expect 类型 | +| `schema.ts` | TypeBox Authoring/Normalized schema | +| `validate.ts` | JSON Schema 无法表达的语义校验 | +| `execute.ts` | Checker 类,包含 resolve、execute、serialize | +| `expect.ts` | checker 专用断言 | +| `runner/index.ts` | 注册 checker | + +## expect 模型选择 + +| 场景 | 模型 | +| ------------------------------------ | ------------------- | +| 状态类结果且集合小而稳定 | enum 或 boolean | +| 单值数字指标或字符串元数据 | ValueMatcher | +| 文本、JSON、HTML、XML 或半结构化内容 | ContentExpectations | +| 动态键值表 | KeyedExpectations | + +详细说明见 [校验规则](docs/user/expectations.md) 和 [Checker 开发](docs/development/checker-development.md)。 + +## 测试要求 + +| 测试类别 | 覆盖内容 | +| ------------ | ---------------------------------------- | +| 契约测试 | TypeBox schema 与 JSON Schema 导出一致性 | +| 语义校验测试 | 合法和非法配置 | +| resolve 测试 | 默认值合并、路径解析、单位转换 | +| execute 测试 | 成功、失败、超时、expect 组合 | +| 注册测试 | registry 注册行为 | +| 配置加载测试 | 含新 checker 的 YAML 完整加载流程 | + +## 文档同步矩阵 + +| 变更 | 必须更新 | +| ------------------------ | ------------------------------------------------------------------------------------- | +| 新增 checker | `docs/user/checkers/.md`、`docs/user/checkers/README.md`、`probes.example.yaml` | +| 修改 checker 配置字段 | 对应 checker 用户文档、schema、测试、示例 | +| 修改 checker expect 字段 | 对应 checker 用户文档,必要时更新 `docs/user/expectations.md` | +| 修改通用 expect 模型 | `docs/user/expectations.md`、`docs/development/checker-development.md` | +| 修改 checker 开发机制 | `docs/development/checker-development.md`、本文件 | +| 修改文档同步规则 | `docs/README.md`、`openspec/config.yaml` | + +## 验证命令 + +新增或修改 checker 后通常需要运行: + +```bash +bun run schema +bun run schema:check +bun run check +bun run verify +``` + +如果因环境限制无法执行完整验证,必须在收尾说明中记录未执行项和原因。 + +## 完成 checklist + +```text +□ checker 类型、schema、validate、resolve、execute、serialize 已实现 +□ checker 已在 runner/index.ts 注册 +□ 配置契约、语义校验和 JSON Schema 导出已同步 +□ probes.example.yaml 已添加或更新示例 +□ tests/server/checker/runner// 已覆盖契约、校验、resolve、execute、注册和配置加载 +□ docs/user/checkers/.md 已添加或更新 +□ docs/user/checkers/README.md 已添加或更新 +□ 文档影响分析已完成,必要文档已同步 +□ bun run schema 和 bun run schema:check 已通过 +□ bun run check 已通过 +□ bun run verify 已通过或记录未执行原因 +``` diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 42f38cb..4ae68fa 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -1,1351 +1,114 @@ -# DiAL 开发文档 - -本文档面向 DiAL 项目的开发者,介绍项目结构、构建流程、测试、代码规范等内容。 - -用户使用说明请参阅 [README.md](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 ` | 显式设置版本号 | - -**版本展示:** - -- Dashboard Header 品牌区域展示当前运行实例版本号(如 `v0.1.0`) -- 版号通过 `/api/meta` 接口返回,前端通过 `useMeta` hook 获取 -- 生产构建时版本号固化到可执行文件中,不依赖运行时外部 `package.json` - -**暂不支持:** - -- CLI `--version` 参数 -- 自动创建 git commit、git tag 或 changelog -- prerelease 版本格式(如 `1.0.0-beta.1`) - ---- - -## 项目结构 - -```text -src/ - server/ - bootstrap.ts 后端统一启动引导(loadConfig → logger → store → engine → startServer → shutdown) - config.ts CLI 参数解析(仅提取配置文件路径) - dev.ts 开发模式启动入口(mode: "development",仅 API server) - logger.ts 日志模块(Logger 接口、Pino 运行时封装、NoopLogger、MemoryLogger、ConsoleFallbackLogger) - 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、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) - dns/ DNS Checker(自包含模块,含 types/schema/execute/expect/codec/transport) - 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 架构总览](#171-架构总览) 中的标准文件表。 - -## 前后端边界 - -前端只通过 HTTP 调用后端,API 路径为 `/api/*`。共享类型放在 `src/shared`,前端不得 import `src/server` 的运行时实现。 - ---- - -## 一、后端开发指引 - -### 1.1 架构概览 - -``` -启动流程: - dev.ts / main.ts → readRuntimeConfig(cli args, 仅提取 configPath) - → bootstrap({ configPath, mode }) - → loadConfig(yaml:YAML 解析 → Authoring normalize(变量替换 + expect 简写展开)→ Normalized 契约校验 → 语义校验 → resolve) - → ResolvedConfig{ host(server.listen), port(server.listen), dataDir(server.storage), maxConcurrentChecks(probes.execution), retentionMs(server.storage), targets, logging(server.logging) } - → createRuntimeLogger(logging) → Logger(配置加载失败时使用 ConsoleFallbackLogger) - → ProbeStore(db) → store.syncTargets(targets) - → ProbeEngine(store, targets, maxConcurrentChecks, retentionMs, logger) → engine.start() - → startServer({ config, mode, store, logger }) - → 注册 SIGINT/SIGTERM shutdown(engine.stop + store.close + logger.flush) - -运行时: - 定时器(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 对象: - -```typescript -// 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 函数签名因端点而异: - -```typescript -// 无 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; -``` - -**请求处理流程**: - -1. `Bun.serve` 的 `routes` 对象按路径 + HTTP 方法匹配请求 -2. 未匹配方法的请求落入 `/api/*` 通配符(返回 404) -3. 各 handler 内部通过 `middleware.ts` 提供的 `validateTargetId`、`validateTimeRange`、`validatePagination`、`validateDashboardWindow`、`validateRecentLimit`、`validateMetricsBucket` 做参数校验,`pageSize` 最大值为 `200` -4. 校验函数返回 `Response` 实例表示校验失败(直接返回),返回数据对象表示通过 -5. 业务逻辑通过 `store` 查询数据,用 `helpers.ts` 的 `jsonResponse`、`mapCheckResult`、`formatDuration` 等格式化输出 - -**新增路由步骤**: - -1. 在 `src/server/routes/` 下创建 `.ts` -2. 实现 handler 函数并 export -3. 在 `server.ts` 的 `routes` 对象中注册路径和 method handler -4. 在 `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`),使用 index signature 支持扩展 - - 各 checker 在自己的 `types.ts` 中定义具体类型(如 `ResolvedHttpTarget`、`ResolvedCommandTarget`),满足 base interface 约束 - - 中间层(engine、store、config-loader)只依赖 base interface,不感知具体 checker 类型 - - `CheckerDefinition` 使用泛型约束 `resolve` 返回值以及 `execute`、`serialize` 的 target 参数 - - checker 实现指定具体 `ResolvedXxxTarget` 类型,中间层(registry、engine、config-loader、store)使用默认泛型参数完成类型擦除 - - Checker 内部 `execute` 和 `serialize` 直接接收具体类型;`resolve` 输入仍是 `RawTargetConfig`,可在读取 checker 专属原始配置时做必要窄化 - -### 1.6 配置契约与校验 - -配置加载流程固定为:`unknown -> AuthoringProbeConfig -> NormalizedProbeConfig -> ValidatedProbeConfig -> ResolvedConfig`。 - -Authoring Config 是用户 YAML 可书写形态,允许 `${key}`、`${key|default}`、`${key|}`、`$${key}` 变量引用和 expect primitive/keyed/content 简写。`normalizeAuthoringConfig()` 在 YAML 解析之后、AJV 契约校验之前执行,只做去糖:调用 `variables.ts` 完成变量替换,展开 expect 简写,并移除顶层 `variables` 段。Normalized Config 不包含变量引用、不包含 Raw expect primitive 简写,也不补默认值、不解析 duration/size/path/env、不合并 `cmd.env`。 - -根目录 `probe-config.schema.json` 由 Authoring schema 导出,服务 VSCode 和外部用户校验;运行时 `validateProbeConfigContract()` 使用 Normalized schema。checker 必须提供 `schemas.authoring.config`、`schemas.authoring.expect`、`schemas.normalized.config`、`schemas.normalized.expect` 四个 TypeBox 片段,Authoring 片段描述用户可写 DSL,Normalized 片段描述 normalizer 输出。 - -变量替换解析优先级为 `variables -> process.env -> 默认值 -> unresolved-variable 报错`;替换范围不包含对象 key,且仅跳过 `targets[].id` 和 `targets[].type` 字段。字段值完整等于单个变量引用时保留 number/boolean/string 类型推断,部分拼接时统一转为字符串。 - -`config-loader.ts` 只负责 YAML 解析、契约调度、公共语义校验和最终运行期解析;checker 专属规则必须下沉到对应 checker 的 `schema.ts` 和 `validate.ts`。 - -`ResolvedConfig` 包含以下字段: - -| 字段 | 来源 | 默认值 | -| --------------------- | ---------------------------------------------------------- | ---------------- | -| `configDir` | 配置文件所在目录 | — | -| `dataDir` | `server.storage.dataDir`(基于配置文件目录解析为绝对路径) | `configDir/data` | -| `host` | `server.listen.host` | `127.0.0.1` | -| `logging` | `server.logging`(等级继承、路径解析、滚动参数) | 见 logging 配置 | -| `port` | `server.listen.port` | `3000` | -| `maxConcurrentChecks` | `probes.execution.maxConcurrentChecks` | `20` | -| `retentionMs` | `server.storage.retention` | `7d` | -| `targets` | `targets[]` 经 resolve 后 | — | - -契约层使用 `src/server/checker/schema/` 中的 TypeBox fragments 生成 JSON Schema,并用 Ajv 执行启动期校验。Ajv 必须保持严格拒绝模式:`allErrors: true`、不启用类型强制转换、不注入默认值、不自动删除未知字段。 - -默认对象策略是 `additionalProperties: false`。只有明确声明的动态键值表可以开放任意键名,例如 `variables`、`http.headers`、`expect.headers`、`cmd.env`。 - -契约校验和语义 validator 都必须返回 `ConfigValidationIssue[]`,不要在 validator 内直接拼接最终用户错误字符串。最终错误由 `formatConfigIssues()` 统一渲染,错误路径需要尽量包含 `targetName` 或 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//` 下的自包含目录,包含该 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 / 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 执行计划类型 -- `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 / 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`(用于 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`)。函数签名统一为: - -```typescript -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()` 规范**: - -- 只做内置默认值填充、路径解析、单位转换,**不执行校验** -- `resolve()` 接收的 target 已通过 Normalized schema 和语义校验,expect 已是 normalized 形态(primitive 简写已展开为 `{equals}`、keyed 已转为 `{key, matcher}[]`、content 已转为带 `kind` 的执行结构) -- `resolve()` 对 expect 只做默认值填充(如未配置 `status` 时物化 `[200]`)和 spread 透传,不再调用 Raw 简写展开函数 -- 返回 `satisfies ResolvedXxxTarget` 确保类型正确 - -**expect 管线**:配置从定义到执行经过 Authoring → Normalized → Resolved → Execute 四层,简写展开在 normalizer 阶段完成: - -| 断言模型 | Authoring 输入 | Normalizer 层 | Schema 层(Authoring / Normalized) | Validate 层(接收 Normalized) | Execute 层 | -| --------------------- | ----------------------------------- | ------------------------------ | -------------------------------------------------------------------- | ---------------------------------- | ---------------------------- | -| `ValueExpectation` | `number \| ValueMatcher` | `resolveValueExpectation()` | `createAuthoringValueExpectationSchema()` / `createNormalized*()` | `validateRawValueExpectation()` | `checkValueExpectation()` | -| `ContentExpectations` | `(ValueMatcher \| ExtractorRule)[]` | `resolveContentExpectations()` | `createAuthoringContentExpectationsSchema()` / `createNormalized*()` | `validateRawContentExpectations()` | `checkContentExpectations()` | -| `KeyedExpectations` | `Record` | `resolveKeyedExpectations()` | `createAuthoringKeyedExpectationsSchema()` / `createNormalized*()` | `validateRawKeyedExpectations()` | `checkKeyedExpectations()` | - -选择哪种模型参考 [1.10 expect 字段选择规范](#110-expect-断言系统)的决策树。 - -**resolve 中的标准模式**: - -```typescript -// resolve() 内:expect 已是 normalized 形态,spread 后补默认值 -const expect = target.expect as ResolvedXxxExpectConfig | undefined; -const resolvedExpect: ResolvedXxxExpectConfig = expect - ? { ...expect, status: expect.status ?? [200] } - : { status: [200] }; -``` - -**execute 中的标准模式**: - -```typescript -// 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(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、最长故障、连续状态、趋势按标准 bucket 聚合(`resolveAutoBucket` 自动选择粒度,`buildTrend` 按 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)、grp -- `check_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` -- **日志集成**:engine 构造时接收 `Logger` 实例(可选,默认 NoopLogger),通过 `initStateCache()` 从 store 加载最新状态;状态变化时记录日志(UP→DOWN `warn`、DOWN→UP `info`、首次检查 DOWN `warn`、稳态无日志);每次检查产出 `debug` 级别结构化摘要 - -### 1.10 日志模块 - -日志模块位于 `src/server/logger.ts`,定义项目内部最小 `Logger` 接口,后端运行时代码统一通过此接口输出日志。 - -**Logger 接口**: - -| 方法 | 说明 | -| ------- | ---------------------------------------- | -| `trace` | 级别 trace(开发调试) | -| `debug` | 级别 debug(检查摘要、状态详情) | -| `info` | 级别 info(启动、恢复、正常操作) | -| `warn` | 级别 warn(状态变化 UP→DOWN、首次 DOWN) | -| `error` | 级别 error(checker 执行异常) | -| `fatal` | 级别 fatal(启动失败) | -| `child` | 创建子 logger(附加 bindings 上下文) | -| `flush` | 刷新缓冲(用于关机前确保日志落盘) | - -每个方法支持两种签名:`(msg: string)` 和 `(obj: Record, msg?: string)`。 - -**实现**: - -| 实现 | 用途 | -| ----------------------- | ----------------------------------------------- | -| `PinoLoggerWrapper` | 生产运行时,封装 Pino + pino-pretty + pino-roll | -| `NoopLogger` | 静默丢弃所有日志,用于不需要日志输出的场景 | -| `MemoryLogger` | 测试替身,将日志条目收集到 `entries` 数组供断言 | -| `ConsoleFallbackLogger` | 配置加载失败前的降级日志,直接输出到 console | - -**日志输出**: - -- **控制台**:始终开启,使用 pino-pretty 格式化(彩色、单行、时间戳 `yyyy-mm-dd HH:MM:ss.l`) -- **文件**:始终开启,JSONL 格式,通过 pino-roll 支持按大小和频率滚动 -- **根等级**:取 console 和 file 中的最低等级,确保两个流都能收到所需日志 -- **敏感信息**:自动 redact `authorization`、`cookie`、`set-cookie`、`authToken`、`key`、`password`、`token`、`apiKey` 及其嵌套路径,替换为 `[Redacted]` - -**测试用法**: - -```typescript -import { createMemoryLogger } from "../logger"; - -const logger = createMemoryLogger(); -const engine = new ProbeEngine(store, targets, 20, 0, logger); - -// 断言日志 -expect(logger.entries.filter((e) => e.level === "warn")).toHaveLength(1); -expect(logger.entries[0]!.msg).toContain("UP → DOWN"); -``` - -**运行时规范**: - -- `src/server/` 下的运行时代码禁止直接使用 `console.*`,必须通过注入的 `Logger` 实例输出 -- 配置加载失败(logger 尚未初始化)时使用 `ConsoleFallbackLogger` -- `bootstrap.ts` 在 shutdown 时调用 `logger.flush()` 确保缓冲日志写入磁盘 - -### 1.11 expect 断言系统 - -两层模型:**观测值收集** → **规则校验**。共享断言基础设施位于 `checker/expect/`,checker 专属状态断言位于各自目录。 - -**Authoring → Normalized → Resolved**:用户 YAML 写的是 Authoring 形态(primitive 简写、`{ json: { path, equals } }` 提取器对象、`Record` 键值表),`normalizeAuthoringConfig()` 在 AJV 前将其转换为 Normalized 形态(`{ equals: primitive }`、`{ kind, matcher, ... }` content 联合、`{ key, matcher }[]` 有序数组)。语义校验器接收 Normalized 形态并校验。`resolve()` 只补默认值和做运行期转换。Store `targets.expect` 列当前写入 NULL,不持久化 expect 快照;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 Authoring 输入可使用 string、number、boolean 或 null 简写,normalizer 阶段归一化为 `{ equals: value }`;数组和对象简写不支持,必须显式写成 `{ equals: ... }`。 - -`ContentExpectations` 数组按顺序快速失败。数组项可以是直接 matcher,也可以是 `{ json: {...} }`、`{ css: {...} }`、`{ xpath: {...} }` 提取器规则;一条规则不能混用直接 matcher 和 extractor,多个 extractor 也不能共存。Extractor 未配置 matcher 时等价于 `exists: true`。对对象或数组源执行直接 `contains`/`regex` 时会先 JSON 序列化,`equals` 仍对原始结构做深度相等。 - -启动期语义校验统一由 `expect/validate.ts` 负责,校验器通过兼容层同时支持 Raw 和 Normalized expect 形态(`validateRawKeyedExpectations` 遇到数组自动分派到 `validateNormalizedKeyedExpectations`,`validateRawContentExpectation` 遇到 `kind` 字段自动分派到 `validateNormalizedContentExpectation`)。校验内容包括空 matcher、未知字段、字段类型、`exists:false` 组合、ContentExpectations 互斥性、JSONPath 子集、XPath 可编译性、regex 可编译性、ReDoS 风险以及 HTTP/LLM headers 大小写归一化后重复 key。语义校验不修改输入。旧字段 `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` | -| DNS system | `values → valueCount → durationMs` | -| DNS server | `responded → rcode → values → valueCount → answerCount → ttlMin → ttlMax → authoritative → recursionAvailable → truncated → authenticatedData → result → 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 的五层管线表](#175-步骤四实现-checker-类)): - -``` -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[] 中的列值 -``` - -选择原则: - -1. **状态类字段使用 enum 或 boolean**。结果集合小且稳定时(如 HTTP status 200/2xx、exitCode 0),枚举和布尔比 matcher 更贴近协议语义,配置也更直观。不要为了统一而把状态类字段改成 ValueMatcher。 - -2. **单值数字指标和字符串元数据使用 ValueMatcher**。观测值是一个明确的标量(耗时、行数、丢包率、finish reason),但阈值不确定时,使用 `{ lte: 100 }` 或 `{ regex: "^(stop|end)$" }` 等 matcher 表达;精确匹配 primitive 可直接写 `100` 或 `"stop"`。 - -3. **返回内容使用 ContentExpectations 数组**。观测值是文本、JSON、HTML 或 XML 内容,且可能需要多步提取或多条规则时,使用 ContentExpectations。即使只有一条规则也必须写成数组形式(`[{ contains: "ok" }]`),不支持对象快捷写法。 - -4. **键值对使用 KeyedExpectations**。观测值是动态键值表(如 headers),且需要对每个键独立断言时使用。字面量值自动等价于 `{ equals: value }`。 - -5. **不要混用模型**。一个 expect 字段只能对应一种断言模型。例如 `finishReason` 是单值字符串元数据,用 ValueMatcher 而非 ContentExpectations(ContentExpectations 的 json/css/xpath 提取器对单字符串无意义,且会增加数组包装的配置冗余)。 - -6. **failure phase 命名遵循去单位后缀规则**。数字指标字段的 phase 去掉单位后缀(`durationMs` → `duration`、`packetLossPercent` → `packetLoss`、`avgLatencyMs` → `avgLatency`),不带单位后缀的字段直接使用字段名(`rowCount` → `rowCount`、`finishReason` → `finishReason`)。 - -7. **实现时参考 [1.7.5 五层管线](#175-步骤四实现-checker-类) 中的对应表**。决策树解决"选哪种模型",五层管线表解决"每种模型从类型定义到执行分别调哪个函数"。 - -### 1.12 错误模式 - -- **API 错误**:`{ error: "描述", status: }`,状态码 400/404/503 -- **CheckFailure**:`{ kind: "error"|"mismatch", phase, path, expected?, actual?, message }` -- **错误处理**:expect 校验失败记录首个失败原因;网络/超时/进程崩溃统一为 `kind:"error"`,请求/TLS/timeout 错误归属 `phase:"request"`,body 超限/解码/解析错误归属 `phase:"body"` -- **日志**:运行时日志通过 `Logger` 接口统一输出(Pino 运行时、Noop/Memory/ConsoleFallback 测试替身),配置加载失败前使用 ConsoleFallbackLogger;禁止在 `src/server/` 运行时代码中直接使用 `console.*` - -### 1.13 测试规范 - -- 测试目录 `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 规范 - -```typescript -const queryKeys = { - dashboard: () => ["dashboard", "24h", 30] as const, - meta: () => ["meta"] as const, - metrics: (targetId: number, from: string, to: string, bucket: "auto" | MetricsBucket) => - ["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 → 参数(粒度从粗到细) - -#### 查询配置规范 - -```typescript -// 全局面板级查询(需要持续刷新) -useQuery({ - queryKey: queryKeys.dashboard(), - queryFn: () => fetchJson("/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 封装 - -```typescript -async function fetchJson(url: string): Promise { - const response = await fetch(url); - if (!response.ok) throw new Error(`HTTP ${response.status}`); - return response.json() as Promise; -} -``` - -- 统一使用 `fetch`(不引入 axios),与后端共享 Web API 生态 -- 错误抛异常,由 TanStack Query 的 `error` 状态承接 - -#### QueryClient 全局配置 - -```typescript -new QueryClient({ - defaultOptions: { - queries: { - retry: 1, // 失败重试 1 次 - refetchOnWindowFocus: true, // 窗口聚焦时刷新 - staleTime: 5000, // 5s 内视为 fresh,避免重复请求 - }, - }, -}); -``` - -### 2.4 组件开发规范 - -#### 文件命名与导入 - -- 每个 React 组件一个 `.tsx` 文件,文件名使用 PascalCase(如 `StatusDot.tsx`) -- 组件 props 定义为 `interface XxxProps`,紧邻组件函数声明 -- 类型从 `../../shared/api` 导入,使用 `type` 导入(`import type { ... }`) - -```typescript -import type { TargetStatus } from "../../shared/api"; -import { StatusDot } from "./StatusDot"; - -interface TargetGroupProps { - name: string; - targets: TargetStatus[]; - onTargetClick: (target: TargetStatus) => void; -} - -export function TargetGroup({ name, targets, onTargetClick }: TargetGroupProps) { - // ... -} -``` - -#### 组件拆分原则 - -- **展示组件**(`components/`):纯渲染逻辑,通过 props 接收数据,通过回调返回事件 -- **容器逻辑**放在 hooks 中,组件只做数据消费 -- **常量数据**(列定义、排序器、筛选器、颜色阈值)放在 `constants/`,不放在组件内部 -- **工具函数**(时间处理等)放在 `utils/`,保持纯函数无副作用 - -#### 现有组件清单 - -| 组件 | 文件 | 用途 | -| -------------------- | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | -| `App` | `app.tsx` | 根组件,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"为例: - -1. **确认数据需求**:是已有 API 数据还是需要新端点? - - 如有新端点,先在 `src/server/routes/` 添加,参考 [1.3 新增路由步骤](#13-api-路由开发) - - 如有新字段,更新 `src/shared/api.ts` 类型定义 -2. **实现 hooks**:全局查询放在 `src/web/hooks/use-queries.ts`;目标详情条件查询放在 `src/web/hooks/use-target-detail.ts`(写好 `queryKey` 和 `enabled` 条件) -3. **编写组件**:在 `src/web/components/` 创建组件文件 - - 在 `TargetDetailDrawer.tsx` 中新增 `` 引用 -4. **编写常量**:如有列定义/排序器/筛选器,放在 `src/web/constants/` -5. **编写测试**:在 `tests/web/` 下添加对应的单元测试 - -### 2.6 样式开发规范 - -前端基于 TDesign React 构建 UI,样式开发遵循以下优先级(从高到低): - -1. **使用 TDesign 组件**:布局、间距、排版优先使用 TDesign 组件(如 Space、Divider、Typography) -2. **使用 TDesign 组件 props**:通过组件的 props 参数控制外观(如 `theme`、`variant`、`size`) -3. **使用 TDesign CSS tokens**:颜色、间距、字体等使用 `--td-*` CSS 变量(如 `--td-success-color`、`--td-comp-margin-xxl`) -4. **在 styles.css 中定义 CSS 类**:无法通过上述方式满足的样式需求,集中定义在 `styles.css` 中 -5. **自行开发组件**:仅在 TDesign 无法满足需求时自行开发 - -**红线**: - -- **严禁在组件中使用 `style` 属性内联调整样式** -- **严禁通过 CSS 覆盖 TDesign 组件内部类名**(如 `.t-tab-panel`),如需定制使用组件的 `className` prop -- **严禁使用 `!important`** -- 颜色统一使用 TDesign CSS tokens(`--td-success-color`、`--td-error-color`、`--td-warning-color` 等),不使用硬编码色值 - -**styles.css 组织**: - -- 自定义 CSS 变量(如可用率渐变色 `--avail-0` ~ `--avail-9`)定义在 `:root` 中 -- 布局类(`.dashboard`、`.dashboard-header-controls`)定义全局页面结构和 Header 右侧单行操作区 -- 组件修饰类(`.status-dot--up`、`.latency-ok`)为自定义视觉组件提供样式变体 -- TDesign 表格行高亮(`.row-down`)通过 `rowClassName` prop 应用 - -### 2.7 前端测试规范 - -- 测试目录:`tests/web/`,结构对应 `src/web/` -- 重点测试 **constants/** 中的纯函数(排序器、筛选器、颜色阈值、类型映射等) -- 使用 `bun:test` 框架 - ---- - -## 三、项目运行、集成与打包 - -### 3.1 开发期运行 - -```bash -bun run dev probes.yaml -``` - -`scripts/dev.ts` 同时启动两个进程: - -- **Bun API server**(端口 3000):后端 API 服务,`--watch` 监听后端文件变更自动重启 -- **Vite dev server**(端口 5173):前端 SPA + HMR 热更新 - -开发时访问 `http://127.0.0.1:5173`,Vite 自动将 `/api` 和 `/health` 请求代理到后端。 - -也可以单独启动: - -```bash -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 可执行文件: - -```typescript -// 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 构建打包 - -#### 构建命令 - -```bash -bun run build -``` - -#### 构建流程 - -构建逻辑拆分为两个文件:`scripts/build-common.ts`(共享函数)和 `scripts/build.ts`(编排逻辑)。 - -`scripts/build.ts` 执行三步流水线(函数来自 `build-common.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`) | - -#### 运行可执行文件 - -```bash -./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 - -#### 清理 - -```bash -bun run clean -# 清理 dist/ 构建产物、dist/release/ 发布产物和 .build/ 临时文件 -``` - -### 3.4 Docker 镜像 - -Docker 镜像使用 Alpine 多阶段构建,保持与生产单可执行文件交付模型一致: - -``` -oven/bun:1-alpine → bun install --frozen-lockfile - → BUN_TARGET=bun-linux-*-musl bun run build - → dist/dial-server - -alpine → 仅复制 /usr/local/bin/dial-server - → 安装 ca-certificates、iputils-ping、libgcc、libstdc++、tzdata - → 使用非 root dial 用户运行 -``` - -#### 架构目标映射 - -Dockerfile 通过 Docker 提供的 `TARGETARCH` 选择 Bun compile target: - -| `TARGETARCH` | `BUN_TARGET` | 说明 | -| ------------ | ---------------------- | ----------------- | -| `amd64` | `bun-linux-x64-musl` | Alpine x64 musl | -| `arm64` | `bun-linux-arm64-musl` | Alpine ARM64 musl | - -不支持的架构必须在构建阶段失败,避免生成无法运行的镜像。 - -#### 运行时边界 - -- 最终镜像不复制源码、`node_modules`、`.build` 或 `dist/web` -- 默认入口为 `/usr/local/bin/dial-server`,默认配置路径为 `/etc/dial/probes.yaml` -- 容器示例配置为 `docker/probes.yaml`,默认监听 `0.0.0.0:3000`,数据目录为 `/data/dial` -- 健康检查使用 Alpine 自带 `wget` 请求 `/health`,不为健康检查安装 `curl` -- `libgcc` 和 `libstdc++` 是 Bun musl executable 在 Alpine 中运行所需的基础运行库 -- ICMP checker 依赖镜像内置的 `iputils-ping`,运行容器时仍需要按需授予 `--cap-add=NET_RAW` -- CMD checker 的额外命令环境不进入官方镜像,用户需要 `curl`、`dig`、`psql` 等命令时通过派生镜像安装 - -#### 验证命令 +# DiAL 开发入口 + +本文档是 DiAL 项目的开发入口,保留常用命令、全局规则和专题导航。详细架构、前端、后端、构建、测试和 checker 开发说明位于 [`docs/development/`](docs/development/README.md)。用户使用说明见 [README.md](README.md) 和 [docs/README.md](docs/README.md)。 + +## 常用命令 + +| 命令 | 说明 | +| -------------------------------- | ---------------------------------------- | +| `bun install` | 安装依赖 | +| `bun run dev probes.yaml` | 启动双进程开发环境 | +| `bun run dev:server probes.yaml` | 仅启动后端 API server | +| `bun run dev:web` | 仅启动 Vite dev server | +| `bun run schema` | 生成 `probe-config.schema.json` | +| `bun run schema:check` | 检查导出 schema 是否同步 | +| `bun run typecheck` | TypeScript 类型检查 | +| `bun run lint` | ESLint 和 Prettier 格式检查 | +| `bun test` | 运行全部测试 | +| `bun run check` | `schema:check + typecheck + lint + test` | +| `bun run build` | 构建生产可执行文件 | +| `bun run verify` | `check + build` 完整验证 | +| `bun run release` | 跨平台发布打包 | +| `bun run clean` | 清理构建缓存与产物 | + +## 质量门禁 + +代码变更必须按影响范围执行验证。常规变更优先运行: ```bash bun run check -bun run build -docker build -t dial:alpine . -docker run --rm -p 3000:3000 -v dial-data:/data/dial dial:alpine -docker buildx build --platform linux/amd64,linux/arm64 -t dial:alpine . ``` -如本地 Docker 环境不支持 buildx 或多架构模拟,需在变更记录中说明未执行原因。 - -### 3.5 跨平台发布 - -#### 发布命令 - -```bash -bun run release # 编译全部 7 个目标平台 -bun run release --target linux-x64 # 编译指定平台 -bun run release --target linux-x64,windows-x64,darwin-arm64 # 多平台 -``` - -#### 发布流程 - -`scripts/release.ts` 复用 `build-common.ts` 的前端构建和代码生成,然后执行多目标交叉编译和打包: - -``` -1. Vite build → dist/web/ (前端静态资源,只执行一次) -2. Code generation → .build/ (资源嵌入代码) -3. 多目标 Bun compile → dist/release/binaries/ (7 个目标平台二进制) -4. tar.gz 打包 → dist/release/packages/ (压缩包 + SHA256 校验和) -``` - -#### 支持的目标平台 - -| CLI 参数 | Bun CompileTarget | 说明 | -| ------------------ | ---------------------- | ------------------- | -| `linux-x64` | `bun-linux-x64` | Linux x64 glibc | -| `linux-arm64` | `bun-linux-arm64` | Linux ARM64 glibc | -| `linux-x64-musl` | `bun-linux-x64-musl` | Linux x64 musl | -| `linux-arm64-musl` | `bun-linux-arm64-musl` | Linux ARM64 musl | -| `windows-x64` | `bun-windows-x64` | Windows x64 | -| `darwin-x64` | `bun-darwin-x64` | macOS Intel | -| `darwin-arm64` | `bun-darwin-arm64` | macOS Apple Silicon | - -#### 产出物 - -``` -dist/release/ -├── binaries/ ← 裸二进制 -│ ├── dial-server-{version}-{os}-{arch}[.exe] -│ └── ... -└── packages/ ← 压缩包 + 校验和 - ├── dial-server_{version}_{os}_{arch}.tar.gz - ├── dial-server_{version}_{os}_{arch}.tar.gz.sha256 - └── ... -``` - -压缩包内含可执行文件(`dial-server` 或 `dial-server.exe`)、`probes.example.yaml` 和 `LICENSE`。 - -#### 命名规范 - -- 裸二进制:`dial-server-{version}-{os}-{arch}[.exe]`,如 `dial-server-0.1.0-linux-x64` -- 压缩包:`dial-server_{version}_{os}_{arch}.tar.gz`,如 `dial-server_0.1.0_linux_x64.tar.gz` -- 校验和:`<压缩包文件名>.sha256`,格式兼容 `sha256sum -c` - -### 3.5 开发工作流 - -#### 日常开发循环 - -```bash -bun run dev probes.yaml # 启动双进程开发环境(Vite + API server) -# 访问 http://127.0.0.1:5173 -# 修改前端代码 → Vite HMR 热更新 / 修改后端代码 → --watch 自动重启 -bun run check # 提交前运行完整质量检查 -``` - -#### 完整验证流程 +正式提交或影响构建、部署、发布、前后端集成的变更运行: ```bash bun run verify -# = bun run check + bun run build ``` -`verify` 适合 CI 或正式提交前,会完整验证类型检查、lint、格式、单元测试和生产构建。 - -**构建 code generation 约定**:`scripts/build-common.ts` 中的 `toImportSpecifier()` 将文件系统相对路径转换为 ESM import specifier,输出在所有平台上都必须使用 `/` 分隔符,不能包含 Windows 反斜杠。跨平台路径测试不得使用当前平台的 `path.sep` 伪装其他平台行为,应使用 `node:path.win32` 或等价注入方式显式模拟目标平台语义。 - -### 3.6 Executable/E2E 验证 - -原 `scripts/smoke.ts` 覆盖过薄,已从当前工作流移除。后续如需验证 production executable 的 API、静态资源服务、SPA fallback 行为,应重新设计独立的 executable/E2E 测试。 - -### 3.7 脚本说明 - -| 脚本 | 文件 | 说明 | -| ---------------------- | ----------------------------------- | -------------------------------------------------- | -| `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 release` | `scripts/release.ts` | 跨平台发布打包(多目标交叉编译 + tar.gz + SHA256) | -| `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.8 环境变量 - -| 变量 | 用途 | 默认值 | -| --------------------------- | ----------------------------------------------- | -------- | -| `BUN_TARGET`/`BUILD_TARGET` | 交叉编译目标平台(仅在 `bun run build` 时有效) | 当前平台 | - -### 3.9 项目配置文件 - -| 文件 | 用途 | -| ---------------------- | ---------------------------------------------- | -| `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.10 依赖管理 - -- **包管理器**:仅使用 `bun`,禁止使用 npm、pnpm、yarn -- **安装依赖**:`bun install` -- **运行工具**:使用 `bunx`,禁止使用 `npx`、`pnpx` -- **锁文件**:`bun.lock` - -### 3.11 目录约定 - -| 目录 | 约定 | -| ------------- | ---------------------------------------------------- | -| `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 自动化。 +新增或修改配置 schema 时必须运行: ```bash -bun run lint # ESLint 检查(含类型感知规则、导入排序、导入验证、Prettier 格式) -bun run format # Prettier 自动格式化 -bun run schema:check # 检查 probe-config.schema.json 是否与 TypeBox fragments 同步 -bun run typecheck # TypeScript 类型检查(含 noUnusedLocals、noPropertyAccessFromIndexSignature) -bun test # 运行所有测试 -bun run check # 一键运行 schema:check + typecheck + lint + test +bun run schema +bun run schema:check ``` -`check` 是日常开发推荐的质量检查命令。 +## 全局工程规则 -### ESLint 规则 +- 使用中文编写注释、文档和项目内交流内容。 +- 仅使用 `bun` 作为包管理器,禁止使用 npm、pnpm、yarn。 +- 运行工具使用 `bunx`,禁止使用 npx、pnpx。 +- 新增代码优先复用已有组件、工具和依赖库,不引入新依赖;确需新增依赖时先说明原因。 +- 后端优先使用 Bun 内置 API,其次是 es-toolkit、主流三方库、项目公共工具,最后才自行实现。 +- 前端样式优先使用 TDesign 组件、组件 props、TDesign CSS tokens、`styles.css` CSS 类,最后才自行开发组件。 +- 前端禁止组件内联 `style`、覆盖 TDesign 内部类名、使用 `!important`、硬编码色值。 +- 当前项目未上线,不需要为旧行为做向前兼容,除非用户明确要求。 -配置文件:`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` | 导入路径验证、循环依赖检测、重复导入合并 | -| `no-restricted-syntax` | 禁止 `src/server/` 运行时代码直接使用 `console.*` | -| `eslint-plugin-prettier` recommended + `eslint-config-prettier` | 将 Prettier 格式集成为 ESLint 规则,禁用冲突规则 | +- 仅使用 `bun` 安装依赖和运行项目脚本,锁文件为 `bun.lock`。 +- 运行外部工具使用 `bunx`。 +- 新增依赖前先确认 Bun 内置 API、es-toolkit、现有三方库和项目公共工具是否已满足需求。 +- Git 提交信息使用中文,格式为 `类型: 简短描述`。 +- 提交类型限定为 `feat`、`fix`、`refactor`、`docs`、`style`、`test`、`chore`。 +- 多行提交描述时,标题和正文之间空一行。 -后端运行时代码的 `console.*` 检查使用中文定制提示:`后端运行时代码禁止直接使用 console.*;请通过注入的 Logger 实例输出日志,配置加载失败前使用 createConsoleFallback()。`。`src/server/logger.ts` 是唯一例外,用于封装 `ConsoleFallbackLogger`。 +## 目录边界 -### 测试代码 ESLint 规范 +| 目录 | 约定 | +| ------------------- | ---------------------------------------------------------- | +| `src/server/` | Bun 后端代码,不能 import `src/web/`,HTML import 集成除外 | +| `src/web/` | React Dashboard,不能 import `src/server/` 运行时实现 | +| `src/shared/` | 前后端共享 TypeScript 类型 | +| `scripts/` | 独立运行脚本,可 import 项目源码 | +| `tests/` | 测试目录,结构镜像 `src/` | +| `docs/user/` | 用户使用、配置、部署、checker 和排障文档 | +| `docs/development/` | 架构、开发规范、构建发布和测试质量 | +| `openspec/` | OpenSpec 变更管理与规格文档 | -测试代码与业务代码使用相同的 ESLint 规则集,应优先通过类型化 helper、类型化 mock、显式 no-op 和受控断言模式满足已启用的类型感知规则,最小化 `eslint-disable` 的使用。具体约定: +## 文档影响分析 -- 使用类型化 mock 变量(`vi.fn()`)替代动态 `require` 获取 mocked module -- 异步错误断言使用 helper 或显式 try/catch,避免依赖 Bun `expect(...).rejects` 与 `await-thenable` 规则的类型不匹配 -- polyfill 中的 intentional no-op 使用显式可解释写法(如 `() => undefined` 或共享 `noop` 函数) -- 对 `process.exit` 等系统 API 使用 `spyOn`(从 `bun:test` 导入)受控 mock 而非手动 monkey patch +每次代码变更都必须执行文档影响分析。 -### Prettier 配置 +| 如果变更影响 | 更新 | +| --------------------------------------------------- | ------------------------------------------ | +| 用户可见行为、配置、checker、expect、部署、状态模型 | `docs/user/` 对应文档 | +| 开发流程、架构、测试、构建发布、checker 开发机制 | `docs/development/` 或 `CONTRIBUTING.md` | +| 项目定位、快速开始、核心能力列表、文档导航 | `README.md` | +| 开发入口、质量门禁、全局规则、专题导航 | `DEVELOPMENT.md` | +| 文档同步规则 | `docs/README.md` 和 `openspec/config.yaml` | -配置文件:`.prettierrc.json`,通过 `eslint-plugin-prettier` 集成为 ESLint 规则(`lint` 命令同时检查格式),也可通过 `format` 命令独立运行。 +如果无需更新文档,必须在收尾说明中说明原因。详细规则见 [文档总览](docs/README.md)。 -显式声明所有格式化参数(`printWidth: 120`、`semi: true`、`singleQuote: false`、`trailingComma: "all"`、`endOfLine: "lf"` 等),确保不同开发环境产出完全一致的格式化结果。 +## OpenSpec 协作规则 -### TypeScript 严格标志 +- 本项目 OpenSpec 使用 `fast-drive` schema,变更文档只包含 `design.md` 和 `tasks.md`,不创建 `proposal.md` 或 `specs/*.md`。 +- `design.md` 是 scope、requirements、decisions、guardrails、execution direction 和 verification expectations 的 source of truth。 +- `tasks.md` 必须从 `design.md` 派生,一行一个 checkbox 任务。 +- 实现阶段按 `tasks.md` 顺序执行,完成后立即标记任务状态。 -| 标志 | 值 | 说明 | -| ------------------------------------ | ----- | ------------------------------------------------ | -| `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` | - -### 运行命令 - -```bash -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` +| 主题 | 文档 | +| ---------------- | ---------------------------------------------------------------------------------- | +| 开发文档索引 | [docs/development/README.md](docs/development/README.md) | +| 架构与边界 | [docs/development/architecture.md](docs/development/architecture.md) | +| 后端开发 | [docs/development/backend.md](docs/development/backend.md) | +| 前端开发 | [docs/development/frontend.md](docs/development/frontend.md) | +| Checker 开发机制 | [docs/development/checker-development.md](docs/development/checker-development.md) | +| 构建与发布 | [docs/development/build-release.md](docs/development/build-release.md) | +| 测试与质量 | [docs/development/testing-quality.md](docs/development/testing-quality.md) | +| 文档总览 | [docs/README.md](docs/README.md) | +| 贡献指南 | [CONTRIBUTING.md](CONTRIBUTING.md) | ## 已知限制 diff --git a/README.md b/README.md index e7a48d5..854d087 100644 --- a/README.md +++ b/README.md @@ -12,29 +12,14 @@ DiAL 是一个自托管的拨测监控工具,支持 **HTTP**、**命令行**、**数据库**、**TCP**、**UDP**、**DNS**、**ICMP** 和 **LLM** 多种拨测类型。通过 YAML 配置文件定义拨测目标,后端定时并发执行拨测并将结果持久化到本地 SQLite,前端 Dashboard 展示各目标的实时状态、可用率和耗时趋势。 -**功能亮点:** +## 功能亮点 -- 多种拨测类型:HTTP(GET/POST/PUT 等)、Cmd(命令行执行)、DB(PostgreSQL/MySQL/SQLite)、TCP(端口可达性 + Banner 探测)、UDP(自定义 payload 请求-响应)、DNS(本机解析检查 + DNS server 深度拨测)、ICMP(存活检测、延迟、丢包率)、LLM(大模型服务应用层健康检查) -- 丰富的校验规则:状态码、响应头、JSONPath、CSS 选择器、XPath、正则匹配、数值比较等 -- 结构化观测数据:检查结果保留按需读取的 HTTP body 预览、TCP/UDP 响应摘要、ICMP 丢包率、CMD 输出预览、LLM token 用量等 observation,便于排障和后续分析 -- 响应式 Dashboard:实时状态、可用率统计、动态粒度趋势图(avg/P95 + 状态条)、手动/自动刷新、版本号展示 +- 多类型拨测:HTTP、Cmd、DB、TCP、UDP、DNS、ICMP、LLM +- 丰富校验规则:状态码、响应头、JSONPath、CSS 选择器、XPath、正则匹配、数值比较等 +- 结构化观测数据:HTTP body 预览、TCP/UDP 响应摘要、ICMP 丢包率、CMD 输出、LLM token 用量等 +- 内置 Dashboard:实时状态、可用率统计、趋势图、最近状态条、手动/自动刷新、版本号展示 - 多主题支持:系统、明亮、黑暗三种主题模式 -- 零外部依赖:数据存储使用 SQLite,无需额外数据库服务 - -## 版本管理 - -DiAL 使用 `package.json.version` 作为唯一版本源,Dashboard Header 展示当前运行实例版本号(如 `v0.1.0`)。 - -**版本升迁命令:** - -```bash -bun run version:patch # 升迁 patch 版本(0.1.0 -> 0.1.1) -bun run version:minor # 升迁 minor 版本(0.1.0 -> 0.2.0) -bun run version:major # 升迁 major 版本(0.1.0 -> 1.0.0) -bun run version:set 0.2.0 # 显式设置版本 -``` - -版本升迁仅更新 `package.json`,不自动创建 git commit、tag 或 changelog。 +- 自托管部署:本地 SQLite 存储,无需额外数据库服务 ## 应用截图 @@ -50,693 +35,54 @@ bun run version:set 0.2.0 # 显式设置版本 ICMP checker 依赖系统 `ping` 命令。精简容器镜像需额外安装,例如 Alpine 可安装 `iputils-ping`。 ```bash -# 克隆仓库 git clone https://github.com/your-org/DiAL.git cd DiAL - -# 安装依赖 bun install - -# 复制示例配置并按需修改 cp probes.example.yaml probes.yaml - -# 启动开发服务器 bun run dev probes.yaml ``` `bun run dev` 会同时启动 Vite 开发服务器(`http://127.0.0.1:5173`)和 API 服务器(`http://127.0.0.1:3000`),访问前端地址即可使用 Dashboard。 -## 生产部署 - -```bash -# 构建 -bun run build - -# 运行 -./dist/dial-server ./probes.yaml -``` - -构建产物为独立可执行文件,只需一个 YAML 配置文件即可运行。 - -### Docker 部署 - -DiAL 提供基于 Alpine 的多阶段镜像。构建阶段使用 Bun 生成 musl 目标单可执行文件,运行阶段只包含 `dial-server`、基础证书、`ping`、musl executable 必需运行库、时区数据和容器运行所需目录。 - -```bash -# 构建当前架构镜像 -docker build -t dial:alpine . - -# 运行容器,使用内置容器配置示例 -docker run --rm -p 3000:3000 -v dial-data:/data/dial dial:alpine - -# 使用自定义配置文件 -docker run --rm -p 3000:3000 \ - -v "$PWD/docker/probes.yaml:/etc/dial/probes.yaml:ro" \ - -v dial-data:/data/dial \ - dial:alpine -``` - -容器默认读取 `/etc/dial/probes.yaml`,推荐将数据卷挂载到 `/data/dial`。容器专用示例配置位于 [`docker/probes.yaml`](docker/probes.yaml),默认监听 `0.0.0.0:3000`,并将 SQLite 数据和日志写入 `/data/dial`。 - -多架构镜像可通过 Docker Buildx 构建: - -```bash -docker buildx build --platform linux/amd64,linux/arm64 -t dial:alpine . -``` - -如需在容器中运行 ICMP checker,除镜像内置的 `iputils-ping` 外,还需要授予 `NET_RAW` capability: - -```bash -docker run --rm --cap-add=NET_RAW -p 3000:3000 -v dial-data:/data/dial dial:alpine -``` - -官方镜像不内置 `bun`、`node`、`curl`、`dig`、`psql`、`mysql`、`redis-cli` 等 CMD checker 可能需要的额外命令。需要这些命令时请使用派生镜像自行安装: - -```dockerfile -FROM dial:alpine - -USER root -RUN apk add --no-cache curl bind-tools postgresql-client -USER dial -``` - -### 跨平台发布打包 - -```bash -# 编译全部 7 个目标平台 -bun run release - -# 编译指定平台 -bun run release --target linux-x64 -bun run release --target linux-x64,windows-x64,darwin-arm64 -``` - -支持的目标平台:`linux-x64`、`linux-arm64`、`linux-x64-musl`、`linux-arm64-musl`、`windows-x64`、`darwin-x64`、`darwin-arm64` - -**产出物结构:** - -``` -dist/release/ -├── binaries/ ← 裸二进制(带版本号和平台标识) -│ ├── dial-server-0.1.0-linux-x64 -│ ├── dial-server-0.1.0-windows-x64.exe -│ └── ... -└── packages/ ← tar.gz 压缩包 + SHA256 校验和 - ├── dial-server_0.1.0_linux_x64.tar.gz - ├── dial-server_0.1.0_linux_x64.tar.gz.sha256 - └── ... -``` - -压缩包内含可执行文件、`probes.example.yaml` 和 `LICENSE`,解压后可直接使用。 - -## 配置文件 - -程序通过 YAML 配置文件定义所有运行参数,完整示例参见 [`probes.example.yaml`](probes.example.yaml)。 - -### 配置文件结构 +## 最小配置示例 ```yaml # yaml-language-server: $schema=./probe-config.schema.json -server: # 服务配置(均可省略) - listen: - host: "127.0.0.1" - port: "${server_port}" - storage: - dataDir: "/tmp/probes_data" - retention: "${retention}" - logging: - level: "${log_level|info}" - file: - path: "/logs/dial.log" - -probes: # 拨测运行时配置(可省略) - execution: - maxConcurrentChecks: "${max_checks}" - -variables: # 配置变量(可省略) - env_name: "生产" - base_url: "https://api.example.com" - server_port: 3000 - retention: "7d" - max_checks: 20 - default_interval: "30s" # 通过变量在多个 target 间共享常用值 - default_timeout: "10s" - -targets: # 拨测目标列表(必填) +targets: - id: "baidu-home" name: "Baidu" type: http http: url: "https://www.baidu.com" expect: - # ... - - id: "my-cmd" - name: "脚本检查" - type: cmd - cmd: - # ... - expect: - # ... - # ... 更多 targets + status: [200] + durationMs: + lte: 5000 ``` -### server.listen — 监听配置 +完整配置、checker 和 expect 规则参见 [配置文件](docs/user/configuration.md)、[Checker 参考](docs/user/checkers/README.md) 和 [校验规则](docs/user/expectations.md)。 -| 字段 | 说明 | 必填 | 默认值 | -| ------ | -------- | ---- | ----------- | -| `host` | 监听地址 | 否 | `127.0.0.1` | -| `port` | 监听端口 | 否 | `3000` | +## 生产运行 -### server.storage — 存储配置 - -| 字段 | 说明 | 必填 | 默认值 | -| ----------- | ------------------------------------------------ | ---- | -------- | -| `dataDir` | 数据目录,相对路径基于配置文件所在目录解析 | 否 | `./data` | -| `retention` | 历史数据保留时长,支持 `ms`/`s`/`m`/`h`/`d` 单位 | 否 | `7d` | - -### probes.execution — 拨测运行时配置 - -| 字段 | 说明 | 必填 | 默认值 | -| --------------------- | -------------- | ---- | ------ | -| `maxConcurrentChecks` | 最大并发拨测数 | 否 | `20` | - -### server.logging — 日志配置 - -| 字段 | 说明 | 必填 | 默认值 | -| ---------------------------------------- | ---------------------------------------------- | ---- | ------------------------- | -| `server.logging.level` | 全局日志等级,console 和 file 未指定时继承此值 | 否 | `info` | -| `server.logging.console.level` | 控制台日志等级 | 否 | 继承 `level` | -| `server.logging.file.level` | 文件日志等级 | 否 | 继承 `level` | -| `server.logging.file.path` | 日志文件路径,相对路径基于配置文件目录解析 | 否 | `/logs/dial.log` | -| `server.logging.file.rotation.size` | 按大小滚动,支持 `KB`/`MB`/`GB` 单位 | 否 | `50MB` | -| `server.logging.file.rotation.frequency` | 按时间滚动:`hourly`、`daily`、`weekly` | 否 | `daily` | -| `server.logging.file.rotation.maxFiles` | 保留的归档文件数量(不含活跃日志) | 否 | `14` | - -日志等级支持:`trace`、`debug`、`info`、`warn`、`error`、`fatal`。 - -控制台始终输出(pretty 格式),文件始终输出 JSONL 格式并支持滚动。`rotation.size` 和 `rotation.frequency` 任一条件触发即滚动。 - -### 内置默认值 - -未显式配置时,系统使用以下内置默认值: - -- `interval`:`30s`(拨测间隔) -- `timeout`:`10s`(超时时间) -- 各 checker 专属默认值见对应章节 - -如需在配置文件中共享相同的配置值,可使用 `variables` 定义变量,然后在 `server`、`probes` 和 `targets` 中通过 `${var}` 引用。例如在 `variables` 中定义 `default_interval: "30s"`,在多个 target 的 `interval` 字段写 `${default_interval}`。 - -### variables — 配置变量 - -`variables` 是顶层动态键值表,key 必须符合 `[a-zA-Z_][a-zA-Z0-9_]*`,value 仅支持 string、number、boolean。`server`、`probes` 和 `targets` 中的字符串值可引用变量: - -- `${key}`:引用 variables 或环境变量 -- `${key|default}`:变量和环境变量都不存在时使用默认值,第一个 `|` 后的内容为默认值 -- `${key|}`:变量和环境变量都不存在时使用空字符串作为默认值 -- `$${key}`:转义输出字面量 `${key}` - -解析优先级为 `variables -> process.env -> 默认值`,三者均不存在时配置校验失败。字段值完整等于单个变量引用时会保留 number/boolean/string 类型,环境变量和默认值会做类型推断,但空字符串保持为字符串;部分拼接时统一转为字符串。变量替换作用于 `server`、`probes` 和 `targets`,不作用于 `variables` 段自身,且不会替换 `targets[].id` 和 `targets[].type` 字段;对象 key 不参与替换。 - -配置加载内部区分三层形态:用户 YAML 属于 Authoring Config,允许变量引用和 expect 简写;`normalizeAuthoringConfig()` 会在启动时完成变量替换、expect primitive/keyed/content 简写展开并移除 `variables` 段,生成 Normalized Config;checker 的 `resolve()` 只在 ResolvedConfig 阶段补默认值并解析 duration、size、路径和运行期环境。根目录 `probe-config.schema.json` 面向 Authoring Config,因此 VSCode 校验会接受 `server.listen.port: "${server_port|3000}"`、`http.maxRedirects: "${MAX|5}"` 和 `expect.durationMs: 5000` 这类写法。 - -### targets — 拨测目标列表(必填) - -每个 target 的通用字段: - -| 字段 | 说明 | 必填 | 默认值 | -| ------------- | ------------------------------------------------------------------------------------ | ---- | --------- | -| `id` | 目标唯一标识,最长 30 字符,支持字母数字、下划线、连字符,不参与变量替换 | 是 | | -| `name` | 展示名称,最长 30 字符,支持变量替换,可省略或显式 null;前端展示时 null 回退到 `id` | 否 | | -| `description` | 目标描述,最长 500 字符,支持变量替换,可省略或显式 null,允许空字符串 | 否 | | -| `type` | 目标类型:`http`、`cmd`、`db`、`tcp`、`udp`、`dns`、`icmp`、`llm` | 是 | | -| `group` | 分组名称 | 否 | `default` | -| `interval` | 拨测间隔,未配置时使用内置默认值 `30s` | 否 | `30s` | -| `timeout` | 超时时间,未配置时使用内置默认值 `10s` | 否 | `10s` | - ---- - -### HTTP Checker(`type: http`) - -**配置项** - -| 字段 | 说明 | 必填 | 默认值 | -| ------------------- | ------------------- | ---- | ------- | -| `http.url` | 目标 URL | 是 | | -| `http.method` | HTTP 方法 | 否 | `GET` | -| `http.headers` | 请求头 | 否 | | -| `http.body` | 请求体 | 否 | | -| `http.ignoreSSL` | 忽略 HTTPS 证书校验 | 否 | `false` | -| `http.maxRedirects` | 最大重定向跟随次数 | 否 | `0` | - -**expect 校验项** - -| 字段 | 说明 | 必填 | 默认值 | -| ------------ | -------------------------------------------------- | ---- | ------- | -| `status` | 可接受的状态码列表,支持精确码和范围(如 `"2xx"`) | 否 | `[200]` | -| `headers` | 响应头校验,使用动态键名和 `KeyedExpectations` | 否 | | -| `body` | 响应体校验,使用 `ContentExpectations` 数组 | 否 | | -| `durationMs` | 完整执行耗时校验,使用 `ValueMatcher` | 否 | | - -**配置示例** - -```yaml -- id: "json-api" - name: "JSON API 示例" - type: http - http: - url: "https://httpbin.org/json" - headers: - Authorization: "Bearer token" - expect: - status: [200] - headers: - Content-Type: - contains: "application/json" - body: - - json: - path: "$.slideshow.title" - equals: "Sample Slide Show" - durationMs: - lte: 10000 +```bash +bun run build +./dist/dial-server ./probes.yaml ``` ---- - -### Cmd Checker(`type: cmd`) - -**配置项** - -| 字段 | 说明 | 必填 | 默认值 | -| ---------- | -------------------------------------- | ---- | ------ | -| `cmd.exec` | 可执行文件名或路径 | 是 | | -| `cmd.args` | 命令行参数列表 | 否 | `[]` | -| `cmd.env` | 环境变量覆盖(继承进程环境变量并合并) | 否 | | -| `cmd.cwd` | 工作目录(相对于配置文件所在目录) | 否 | | - -**expect 校验项** - -| 字段 | 说明 | 必填 | 默认值 | -| ------------ | --------------------------------------------- | ---- | ------ | -| `exitCode` | 可接受的退出码列表 | 否 | `[0]` | -| `stdout` | 标准输出校验,使用 `ContentExpectations` 数组 | 否 | | -| `stderr` | 标准错误校验,使用 `ContentExpectations` 数组 | 否 | | -| `durationMs` | 完整执行耗时校验,使用 `ValueMatcher` | 否 | | - -**配置示例** - -```yaml -- id: "bun-script" - name: "Bun 脚本检查" - type: cmd - cmd: - exec: "bun" - args: ["-e", "console.log('ok')"] - expect: - exitCode: [0] - stdout: - - contains: "ok" -``` - ---- - -### DB Checker(`type: db`) - -**配置项** - -| 字段 | 说明 | 必填 | 默认值 | -| ---------- | ------------------------------------------------------------- | ---- | ------ | -| `db.url` | 数据库连接字符串,支持 `postgres://`、`mysql://`、`sqlite://` | 是 | | -| `db.query` | SQL 查询语句,不配置时仅测试连接 | 否 | | - -**expect 校验项** - -| 字段 | 说明 | 必填 | 默认值 | -| ------------ | ----------------------------------------------------------------------- | ---- | ------ | -| `rowCount` | 查询返回行数校验,使用 `ValueMatcher` | 否 | | -| `rows` | 查询结果逐行校验,数组内每行为列名到 `KeyedExpectations` 的映射 | 否 | | -| `result` | 完整查询结果 `{ rows, rowCount }` 校验,使用 `ContentExpectations` 数组 | 否 | | -| `durationMs` | 完整执行耗时校验,使用 `ValueMatcher` | 否 | | - -**配置示例** - -```yaml -- id: "sqlite-query" - name: "SQLite 数据库检查" - type: db - db: - url: "sqlite:///path/to/db.sqlite" - query: "SELECT COUNT(*) as cnt FROM users WHERE status = 'active'" - expect: - durationMs: - lte: 5000 - rowCount: { gte: 1 } - rows: - - cnt: { gte: 0 } -``` - ---- - -### TCP Checker(`type: tcp`) - -**配置项** - -| 字段 | 说明 | 必填 | 默认值 | -| ----------------------- | ------------------------------------------- | ---- | ------- | -| `tcp.host` | 目标主机地址 | 是 | | -| `tcp.port` | 目标端口(1-65535) | 是 | | -| `tcp.readBanner` | 是否读取服务端 banner | 否 | `false` | -| `tcp.bannerReadTimeout` | banner 读取超时(毫秒) | 否 | `2000` | -| `tcp.maxBannerBytes` | banner 最大字节数,支持 `KB`/`MB`/`GB` 单位 | 否 | `4KB` | - -**expect 校验项** - -| 字段 | 说明 | 必填 | 默认值 | -| ------------ | ------------------------------------------------------------------------- | ---- | ------ | -| `connected` | 期望连接结果,`true` 可达或 `false` 期望不可达 | 否 | `true` | -| `banner` | Banner 内容校验,使用 `ContentExpectations` 数组,需开启 `tcp.readBanner` | 否 | | -| `durationMs` | 完整执行耗时校验,使用 `ValueMatcher` | 否 | | - -**配置示例** - -```yaml -- id: "redis-port" - name: "Redis 端口可达" - type: tcp - tcp: - host: "127.0.0.1" - port: 6379 - expect: - durationMs: - lte: 3000 -``` - ---- - -### UDP Checker(`type: udp`) - -**配置项** - -| 字段 | 说明 | 必填 | 默认值 | -| ---------------------- | ---------------------------------------- | ---- | ------ | -| `udp.host` | 目标主机地址 | 是 | | -| `udp.port` | 目标端口(1-65535) | 是 | | -| `udp.payload` | 发送数据 | 否 | `""` | -| `udp.encoding` | payload 编码:`text`、`hex`、`base64` | 否 | `text` | -| `udp.responseEncoding` | 响应解码:`text`、`hex`、`base64` | 否 | `text` | -| `udp.maxResponseBytes` | 响应最大字节数,支持 `KB`/`MB`/`GB` 单位 | 否 | `4KB` | - -**expect 校验项** - -| 字段 | 说明 | 必填 | 默认值 | -| -------------- | --------------------------------------------- | ---- | ------ | -| `responded` | 期望是否收到响应 | 否 | `true` | -| `response` | 响应内容校验,使用 `ContentExpectations` 数组 | 否 | | -| `responseSize` | 响应字节数校验,使用 `ValueMatcher` | 否 | | -| `sourceHost` | 响应来源地址校验,使用 `ValueMatcher` | 否 | | -| `sourcePort` | 响应来源端口校验,使用 `ValueMatcher` | 否 | | -| `durationMs` | 完整执行耗时校验,使用 `ValueMatcher` | 否 | | - -**配置示例** - -```yaml -- id: "udp-heartbeat" - name: "UDP 心跳检测" - type: udp - udp: - host: "127.0.0.1" - port: 9000 - payload: "PING" - expect: - responded: true - response: - - contains: "PONG" - durationMs: - lte: 100 -``` - ---- - -### ICMP Checker(`type: icmp`) - -**配置项** - -| 字段 | 说明 | 必填 | 默认值 | -| ----------------- | ------------------------- | ---- | ------ | -| `icmp.host` | 目标主机地址 | 是 | | -| `icmp.count` | ICMP 包数量,范围 `1-100` | 否 | `3` | -| `icmp.packetSize` | ICMP 包大小(bytes) | 否 | `56` | - -ICMP checker 通过系统 `ping` 命令执行 ICMP 探测,支持 Linux、macOS 和 Windows 输出解析。 - -**expect 校验项** - -| 字段 | 说明 | 必填 | 默认值 | -| ------------------- | --------------------------------------------------- | ---- | ------ | -| `alive` | 期望主机可达性 | 否 | `true` | -| `packetLossPercent` | 丢包率百分比校验,范围 `0-100`,使用 `ValueMatcher` | 否 | | -| `avgLatencyMs` | 平均延迟校验,使用 `ValueMatcher` | 否 | | -| `maxLatencyMs` | 最大单次延迟校验,使用 `ValueMatcher` | 否 | | -| `durationMs` | 完整执行耗时校验,使用 `ValueMatcher` | 否 | | - -**配置示例** - -```yaml -- id: "gateway-icmp" - name: "网关 ICMP 可达" - type: icmp - icmp: - host: "10.0.0.1" - count: 3 - packetSize: 56 - expect: - alive: true - packetLossPercent: - lte: 10 - avgLatencyMs: - lte: 100 - maxLatencyMs: - lte: 300 - durationMs: - lte: 5000 -``` - ---- - -### DNS Checker(`type: dns`) - -DNS Checker 支持两种解析模式,通过 `dns.resolver` 字段区分: - -- **`system` 模式**:使用本机 DNS 解析器检查域名是否能解析到预期地址,输出有限 observation。 -- **`server` 模式**:直接向指定 DNS server 发起 UDP/TCP 深度拨测,检查 DNS 协议级响应(RCODE、TTL、flags、记录值等)。 - -#### `dns.resolver: system` 配置项 - -| 字段 | 说明 | 必填 | 默认值 | -| -------------- | ---------- | ---- | -------- | -| `dns.resolver` | 解析模式 | 是 | `system` | -| `dns.name` | 待解析域名 | 是 | | -| `dns.family` | 地址族 | 否 | `any` | - -`family` 可选值:`any`(返回 IPv4 和 IPv6)、`ipv4`(仅 IPv4)、`ipv6`(仅 IPv6)。 - -**system 模式 expect 校验项** - -| 字段 | 说明 | 断言模型 | -| ------------ | -------------------- | --------------------------------- | -| `values` | 解析结果地址集合断言 | DNS 集合(include/exclude/exact) | -| `valueCount` | 解析结果数量 | ValueMatcher | -| `durationMs` | 解析耗时 | ValueMatcher | - -**示例**: - -```yaml -- id: "dns-system-api" - name: "本机 DNS 解析" - type: dns - dns: - resolver: system - name: "api.example.com" - family: any - expect: - values: - exact: - - "203.0.113.10" - durationMs: - lte: 500 -``` - -#### `dns.resolver: server` 配置项 - -| 字段 | 说明 | 必填 | 默认值 | -| ---------------------- | --------------------------------- | ---- | -------- | -| `dns.resolver` | 解析模式 | 是 | `server` | -| `dns.server` | DNS server 地址 | 是 | | -| `dns.name` | 查询域名 | 是 | | -| `dns.port` | DNS server 端口 | 否 | `53` | -| `dns.protocol` | 传输协议:`udp` / `tcp` | 否 | `udp` | -| `dns.recordType` | DNS 记录类型 | 否 | `A` | -| `dns.recursionDesired` | 是否设置 RD flag | 否 | `true` | -| `dns.tcpFallback` | UDP 响应 TC=1 时是否 TCP fallback | 否 | `true` | -| `dns.maxResponseBytes` | 响应最大字节数 | 否 | `4KB` | - -`recordType` 可选值:`A`、`AAAA`、`CNAME`、`NS`、`MX`、`TXT`、`SOA`、`SRV`、`CAA`、`PTR`。 - -**server 模式 expect 校验项** - -| 字段 | 说明 | 断言模型 | -| -------------------- | ---------------------------------- | --------------------------------- | -| `responded` | 是否收到 DNS response | boolean | -| `rcode` | 期望 RCODE 列表(如 `NOERROR`) | string[] | -| `values` | 目标类型记录值集合断言 | DNS 集合(include/exclude/exact) | -| `valueCount` | 目标类型记录数量 | ValueMatcher | -| `answerCount` | answer section 总记录数 | ValueMatcher | -| `ttlMin` | answer 中最小 TTL | ValueMatcher | -| `ttlMax` | answer 中最大 TTL | ValueMatcher | -| `authoritative` | AA flag | boolean | -| `recursionAvailable` | RA flag | boolean | -| `truncated` | TC flag | boolean | -| `authenticatedData` | AD flag | boolean | -| `result` | 完整结构化响应的 JSONPath 兜底断言 | ContentExpectations | -| `durationMs` | 完整查询耗时 | ValueMatcher | - -**示例**: - -```yaml -- id: "dns-server-api" - name: "Cloudflare DNS A 记录" - type: dns - dns: - resolver: server - server: "1.1.1.1" - port: 53 - protocol: udp - name: "api.example.com" - recordType: A - expect: - rcode: ["NOERROR"] - values: - include: - - "203.0.113.10" - ttlMin: - gte: 60 - durationMs: - lte: 200 - -- id: "dns-nxdomain-check" - name: "负向 DNS 检查" - type: dns - dns: - resolver: server - server: "1.1.1.1" - name: "nxdomain.example.com" - recordType: A - expect: - rcode: ["NXDOMAIN"] -``` - -**Notes**: - -- 未配置 expect 时,`system` 模式默认要求解析成功且 `valueCount > 0`,`server` 模式默认要求 `NOERROR + valueCount > 0`。 -- 显式配置非 `NOERROR` rcode(如 `NXDOMAIN`)时,不自动要求 `valueCount > 0`。 -- `values.exact` 忽略返回顺序(集合匹配);对 A/AAAA 查询,CNAME 链不计入 `values`,单独放入 `cnameChain` -- `values` 按记录类型规范化为字符串,格式:MX=`"10 mail.example.com"`、SOA=空格分隔字段、SRV=`"10 60 443 server.example.com"`、CAA=`"0 issue letsencrypt.org"` 等。 - ---- - -### LLM Checker(`type: llm`) - -**配置项** - -| 字段 | 说明 | 必填 | 默认值 | -| --------------------- | ------------------------------------------------------ | ---- | ------- | -| `llm.provider` | 模型提供方:`openai`、`openai-responses`、`anthropic` | 是 | | -| `llm.url` | API base URL | 是 | | -| `llm.model` | 模型名称 | 是 | | -| `llm.prompt` | 单轮 prompt | 是 | | -| `llm.mode` | 调用模式:`http`(非流式)或 `stream`(流式) | 否 | `http` | -| `llm.key` | API key,支持 `${VAR}` 变量替换 | 否 | `""` | -| `llm.authToken` | Bearer token(仅 `anthropic` provider,与 `key` 互斥) | 否 | | -| `llm.headers` | 附加请求头 | 否 | | -| `llm.ignoreSSL` | 忽略 HTTPS 证书校验 | 否 | `false` | -| `llm.options` | 生成选项 | 否 | | -| `llm.providerOptions` | Provider 专属选项 | 否 | | - -`llm.options` 支持 `maxOutputTokens`(默认 `16`)、`temperature`(默认 `0`)、`topP`、`topK`、`presencePenalty`、`frequencyPenalty`、`stopSequences`、`seed`。 - -**expect 校验项** - -| 字段 | 说明 | 必填 | 默认值 | -| ----------------- | ---------------------------------------------------------------------- | ---- | ------- | -| `status` | 可接受的状态码列表,支持精确码和范围(如 `"2xx"`) | 否 | `[200]` | -| `headers` | 响应头校验,使用动态键名和 `KeyedExpectations` | 否 | | -| `output` | 模型输出校验,使用 `ContentExpectations` 数组 | 否 | | -| `finishReason` | finish reason 校验,使用 `ValueMatcher` | 否 | | -| `rawFinishReason` | 原始 finish reason 校验,使用 `ValueMatcher` | 否 | | -| `usage` | Token usage 校验(`inputTokens`/`outputTokens`/`totalTokens` matcher) | 否 | | -| `stream` | 流式断言(`completed`、`firstTokenMs` matcher,仅 `mode: stream`) | 否 | | -| `durationMs` | 完整执行耗时校验,使用 `ValueMatcher` | 否 | | - -**配置示例** - -```yaml -- id: "llm-openai-probe" - name: "OpenAI 健康检查" - type: llm - llm: - provider: openai - url: "https://api.openai.com/v1" - model: "gpt-4o-mini" - prompt: "Say OK" - key: "${OPENAI_API_KEY}" - expect: - status: [200] - finishReason: "stop" - output: - - contains: "OK" -``` - ---- - -### 通用校验规则 - -#### ContentExpectations 校验项 - -`body`、`stdout`、`stderr`、`banner`、`response`、`output`、`result` 均使用数组: - -- `contains` — 响应体包含指定文本 -- `regex` — 正则匹配(启动期会拒绝存在 ReDoS 风险的模式) -- `json` — JSONPath 提取值比较(`path` 必填,如 `$.slideshow.title`) -- `css` — CSS 选择器提取 HTML 元素(`selector` 必填,`attr` 可选提取属性) -- `xpath` — XPath 提取 XML/HTML 节点(`path` 必填,如 `/html/body/h1/text()`) - -#### ValueMatcher - -`equals`、`contains`、`regex`、`empty`、`exists`、`gte`、`lte`、`gt`、`lt`。`equals` 支持 JSON 深度相等;`regex` 固定使用无 flags 正则;提取器未配置 matcher 时等价于 `exists: true`。ValueMatcher expect 字段也可直接写 string、number、boolean 或 null,等价于 `{ equals: value }`;数组和对象必须显式写成 `{ equals: ... }`。 - -#### KeyedExpectations - -`headers`、`rows` 中每行使用的校验结构,支持 ValueMatcher 的全部字段。 - -### 补充说明 - -- **大小说明**:`maxBodyBytes`、`maxOutputBytes`、`maxResponseBytes`、`maxBannerBytes` 支持 `KB`、`MB`、`GB` 单位,也可直接使用数字 -- **时长格式**:`500ms`、`30s`、`5m`、`2h`、`7d` -- **JSON Schema**:仓库根目录导出 `probe-config.schema.json`,在 YAML 文件顶部添加 `# yaml-language-server: $schema=./probe-config.schema.json` 即可在编辑器中获得提示和校验 -- **旧字段移除**:`maxDurationMs`、`maxPacketLoss`、`maxAvgLatencyMs`、`maxMaxLatencyMs` 和旧正则字段 `match` 已移除,请分别改用 `durationMs`、ICMP matcher 字段和 `regex` - -> **注意:** 配置校验在启动时执行,非法配置会阻止启动并输出错误信息。除动态键值表(`headers`、`env`、`variables`)外,未知字段会导致启动失败,请使用 YAML 注释。 - -## 目标状态判定 - -采用单层判定模型: - -- **UP** = 拨测结果符合 expect 规则 -- **DOWN** = 拨测结果不符合 expect 规则 - -执行失败(网络错误、超时、进程崩溃)和 expect 不匹配都统一为 DOWN,通过 `failure.kind` 区分原因(`"error"` vs `"mismatch"`)。 - -API 返回的检查结果包含 `detail` 和 `observation`:`detail` 是后端按 checker 类型从结构化 observation 动态生成的人可读摘要,`observation` 保存该次检查的结构化观测数据。`detail` 不写入 SQLite,存储层仅持久化 `observation` JSON、`failure` JSON、匹配状态、耗时和时间戳。 +Docker、跨平台发布包和运行时注意事项参见 [部署文档](docs/user/deployment.md)。 + +## 文档导航 + +| 入口 | 内容 | +| -------------------------------------------- | ---------------------------------------------------- | +| [文档总览](docs/README.md) | 全部文档入口和文档归属矩阵 | +| [配置文件](docs/user/configuration.md) | YAML 结构、变量、server、targets 通用字段 | +| [Checker 参考](docs/user/checkers/README.md) | 所有 checker 的配置、expect 和示例 | +| [校验规则](docs/user/expectations.md) | ValueMatcher、ContentExpectations、KeyedExpectations | +| [部署文档](docs/user/deployment.md) | 构建、Docker、发布包和容器运行边界 | +| [状态模型](docs/user/status-model.md) | UP/DOWN、failure、observation、detail | +| [故障排查](docs/user/troubleshooting.md) | 常见运行问题和排查入口 | ## 开发 @@ -745,7 +91,7 @@ bun run check # schema:check + typecheck + lint + test bun run verify # check + build ``` -开发相关文档(项目结构、构建、测试、代码规范等)请参阅 [DEVELOPMENT.md](DEVELOPMENT.md)。 +开发入口参见 [DEVELOPMENT.md](DEVELOPMENT.md)。新增或修改 checker 前请先阅读 [CONTRIBUTING.md](CONTRIBUTING.md)。 ## License diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..4e8315b --- /dev/null +++ b/docs/README.md @@ -0,0 +1,126 @@ +# DiAL 文档 + +本文档是 DiAL 的文档路由入口。AI 工具和开发者应先阅读本文件判断本次任务需要读取和更新哪些专题文档,再按任务类型读取最小必要上下文。 + +## 读者入口 + +| 读者 | 推荐入口 | +| -------------- | ------------------------------------------------------------------------------------------------------------ | +| 首次使用者 | [README 快速开始](../README.md#快速开始) | +| 配置编写者 | [配置文件](user/configuration.md)、[Checker 参考](user/checkers/README.md)、[校验规则](user/expectations.md) | +| 部署维护者 | [部署文档](user/deployment.md)、[故障排查](user/troubleshooting.md) | +| 项目开发者 | [开发文档索引](development/README.md)、[DEVELOPMENT.md](../DEVELOPMENT.md) | +| Checker 贡献者 | [CONTRIBUTING.md](../CONTRIBUTING.md)、[Checker 开发](development/checker-development.md) | +| AI 工具维护者 | 本文件的任务路由与文档归属矩阵、[OpenSpec 配置](../openspec/config.yaml) | + +## 按任务阅读路径 + +| 任务 | 必读文档 | +| ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 新增 checker | [CONTRIBUTING.md](../CONTRIBUTING.md)、[Checker 开发](development/checker-development.md)、[Checker 参考](user/checkers/README.md)、相近 checker 用户文档 | +| 修改 checker 配置 | 对应 `docs/user/checkers/.md`、[配置文件](user/configuration.md)、[校验规则](user/expectations.md) | +| 修改 expect 机制 | [校验规则](user/expectations.md)、[后端开发](development/backend.md)、[Checker 开发](development/checker-development.md) | +| 修改前端 | [DEVELOPMENT.md](../DEVELOPMENT.md)、[前端开发](development/frontend.md)、[测试与质量](development/testing-quality.md) | +| 修改后端 API、store、engine、logger | [DEVELOPMENT.md](../DEVELOPMENT.md)、[后端开发](development/backend.md)、[测试与质量](development/testing-quality.md) | +| 修改构建、Docker、release | [构建与发布](development/build-release.md)、[部署文档](user/deployment.md) | +| 修改配置 schema | [配置文件](user/configuration.md)、相关 checker 文档、[后端开发](development/backend.md)、[测试与质量](development/testing-quality.md) | +| 修改文档规则 | 本文件、[DEVELOPMENT.md](../DEVELOPMENT.md)、[OpenSpec 配置](../openspec/config.yaml) | + +## 目录结构 + +```text +docs/ + README.md + user/ + deployment.md + configuration.md + expectations.md + status-model.md + troubleshooting.md + checkers/ + README.md + http.md + cmd.md + db.md + tcp.md + udp.md + icmp.md + dns.md + llm.md + development/ + README.md + architecture.md + backend.md + frontend.md + checker-development.md + build-release.md + testing-quality.md + prompts/ + README.md +``` + +## 文档归属矩阵 + +| 变更类型 | 默认更新位置 | +| ------------------------------------------------------------- | ------------------------------------------------------------ | +| 项目定位、核心能力、快速开始、文档导航变化 | `README.md` | +| 用户安装、首次运行、基础使用流程变化 | `README.md` | +| YAML 顶层结构、server、variables、targets 通用字段变化 | `docs/user/configuration.md` | +| checker 配置、expect 字段、示例变化 | `docs/user/checkers/` 对应文档 | +| ValueMatcher、ContentExpectations、KeyedExpectations 规则变化 | `docs/user/expectations.md` | +| 构建产物运行、Docker、发布包、运行时能力变化 | `docs/user/deployment.md` | +| UP/DOWN 判定、failure、observation、detail 行为变化 | `docs/user/status-model.md` | +| 常见运行问题、依赖命令、容器权限、配置校验问题变化 | `docs/user/troubleshooting.md` | +| 开发入口、常用命令、质量门禁、全局规则变化 | `DEVELOPMENT.md` | +| 架构边界、启动流程、运行时流程变化 | `docs/development/architecture.md` | +| 后端 API、store、engine、logger、expect 实现机制变化 | `docs/development/backend.md` | +| 前端技术栈、组件、样式、数据层规范变化 | `docs/development/frontend.md` | +| 新增或修改 checker 的开发机制变化 | `CONTRIBUTING.md`、`docs/development/checker-development.md` | +| 构建、发布、脚本、项目配置维护方式变化 | `docs/development/build-release.md` | +| 测试、lint、typecheck、hooks、格式化规范变化 | `docs/development/testing-quality.md` | +| 包管理、依赖、目录、提交、OpenSpec 约定变化 | `DEVELOPMENT.md` | +| 文档同步规则、文档影响分析规则变化 | `docs/README.md`、`openspec/config.yaml` | +| AI 提示词资产变化 | `docs/prompts/` | + +## 文档影响分析 + +每次代码变更都必须执行文档影响分析。 + +```text +代码或配置变更 + ├─ 用户能感知吗? + │ ├─ 配置 / checker / expect -> docs/user/ + │ ├─ 部署 / 运行 / release -> docs/user/deployment.md + │ ├─ 状态 / observation / failure -> docs/user/status-model.md + │ └─ 项目入口变化 -> README.md + ├─ 开发者需要知道吗? + │ ├─ checker 机制 -> CONTRIBUTING.md + docs/development/checker-development.md + │ ├─ 架构边界 -> docs/development/architecture.md + │ ├─ 后端机制 -> docs/development/backend.md + │ ├─ 前端机制 -> docs/development/frontend.md + │ ├─ 构建测试质量 -> docs/development/testing-quality.md + │ └─ 开发入口规则 -> DEVELOPMENT.md + └─ 都不是 + └─ 收尾说明无需更新文档及原因 +``` + +## 维护原则 + +- 根目录入口文档保持轻量,不承载完整配置参考和实现教程。 +- 用户文档解释“如何使用”,开发文档解释“如何实现和维护”。 +- 配置事实来源是 TypeBox schema、`probe-config.schema.json`、语义校验器和测试;文档负责解释和示例。 +- 每次代码变更都必须做文档影响分析;有影响时更新对应专题文档,无影响时在收尾说明中写明原因。 +- 同一字段表只在最贴近读者的文档中完整展开,其他文档用链接引用。 +- README 不承载完整配置表和 checker 表,DEVELOPMENT 不承载完整架构百科和 checker 教程。 + +## 收尾说明示例 + +```text +文档影响分析:本次修改了 HTTP checker 的配置字段,已更新 docs/user/checkers/http.md、docs/user/configuration.md 和 probe-config.schema.json。 +``` + +无需更新文档时: + +```text +文档影响分析:本次仅调整内部测试 helper,未改变用户可见行为、配置、架构边界或开发流程,因此无需更新文档。 +``` diff --git a/docs/development/README.md b/docs/development/README.md new file mode 100644 index 0000000..e78a586 --- /dev/null +++ b/docs/development/README.md @@ -0,0 +1,25 @@ +# 开发文档 + +本目录承载 DiAL 的开发和维护专题。日常开发入口见 [`../../DEVELOPMENT.md`](../../DEVELOPMENT.md),新增或修改 checker 前先阅读 [`../../CONTRIBUTING.md`](../../CONTRIBUTING.md)。 + +## 专题索引 + +| 文档 | 内容 | +| ------------------------------------------------ | ------------------------------------------------------------------------------------------------- | +| [architecture.md](architecture.md) | 项目结构、启动流程、运行时流程、HTTP 请求流程、前后端边界 | +| [backend.md](backend.md) | 后端库优先级、API 路由、共享 helpers、类型规范、配置契约、store、engine、logger、expect、错误模型 | +| [frontend.md](frontend.md) | React、TDesign、TanStack Query、组件、样式和前端测试规范 | +| [checker-development.md](checker-development.md) | 新增或修改 checker 的实现机制和完整 checklist | +| [build-release.md](build-release.md) | 开发服务、前后端集成、构建、Docker、release、脚本、环境变量 | +| [testing-quality.md](testing-quality.md) | lint、format、typecheck、test、hooks 和测试编写规范 | +| [../../DEVELOPMENT.md](../../DEVELOPMENT.md) | 包管理、依赖、目录、提交、OpenSpec 和项目级约定 | +| [../README.md](../README.md) | 文档影响分析、文档归属矩阵和按任务阅读路径 | + +## 事实来源 + +| 主题 | 事实来源 | +| ---------------- | ---------------------------------------------------------- | +| 代码结构和实现 | `src/`、`scripts/`、`tests/` | +| 配置 schema | TypeBox fragments、`probe-config.schema.json`、schema 测试 | +| 项目全局规则 | `openspec/config.yaml`、`DEVELOPMENT.md`、本目录专题文档 | +| checker 贡献流程 | `CONTRIBUTING.md`、`checker-development.md` | diff --git a/docs/development/architecture.md b/docs/development/architecture.md new file mode 100644 index 0000000..9bf4adc --- /dev/null +++ b/docs/development/architecture.md @@ -0,0 +1,106 @@ +# 架构与边界 + +## 项目结构 + +```text +src/ + server/ + bootstrap.ts + config.ts + dev.ts + logger.ts + main.ts + server.ts + helpers.ts + middleware.ts + version.ts + routes/ + checker/ + config-loader.ts + variables.ts + schema/ + store.ts + engine.ts + expect/ + runner/ + shared/ + api.ts + web/ + app.tsx + main.tsx + styles.css + components/ + constants/ + hooks/ + utils/ +scripts/ +tests/ +docs/ +openspec/ +probe-config.schema.json +``` + +## 启动流程 + +```text +dev.ts / main.ts + -> readRuntimeConfig(cli args) + -> bootstrap({ configPath, mode }) + -> loadConfig(yaml) + -> createRuntimeLogger(logging) + -> ProbeStore(db) + -> store.syncTargets(targets) + -> ProbeEngine(...).start() + -> startServer({ config, mode, store, logger }) + -> 注册 SIGINT/SIGTERM shutdown +``` + +`loadConfig()` 的处理顺序:YAML 解析 -> Authoring normalize(变量替换 + expect 简写展开)-> Normalized 契约校验 -> 语义校验 -> resolve。 + +## 运行时流程 + +```text +定时器 tick + -> ProbeEngine.probeGroup() + -> checkerRegistry.get(target.type).execute() + -> runner/*/expect.ts 校验 + -> engine.writeResult() + -> store.insertCheckResult() +``` + +数据清理由 engine 定时调用 `store.prune(retentionMs)`,每小时执行一次。 + +## HTTP 请求流程 + +```text +Request + -> Bun.serve routes 声明式匹配 + -> routes/*.ts handler + -> middleware.ts 参数校验 + -> helpers.ts 响应格式化 + -> Response +``` + +生产模式下,非 API 路径由 fetch fallback 处理静态资源和 SPA fallback。开发模式下,Vite proxy 将 `/api` 和 `/health` 请求转发到 Bun API server。 + +## 前后端边界 + +- 前端只通过 HTTP 调用后端,API 路径为 `/api/*`。 +- 共享类型放在 `src/shared/`。 +- 前端不得 import `src/server/` 的运行时实现。 +- 后端不得依赖 `src/web/` 运行时代码,HTML import 集成除外。 + +## 主要模块职责 + +| 模块 | 职责 | +| ------------------------------------- | ------------------------------------------- | +| `src/server/bootstrap.ts` | 统一启动引导和 shutdown 编排 | +| `src/server/server.ts` | Bun HTTP server 和 routes 注册 | +| `src/server/routes/` | API handler,按端点拆分 | +| `src/server/checker/config-loader.ts` | YAML 解析、契约校验、语义校验、resolve 调度 | +| `src/server/checker/store.ts` | SQLite 数据存储 | +| `src/server/checker/engine.ts` | 定时调度、并发控制、结果写入、数据清理 | +| `src/server/checker/runner/` | 各 checker 自包含实现 | +| `src/server/checker/expect/` | 跨 checker 复用的断言基础设施 | +| `src/web/` | React Dashboard | +| `src/shared/api.ts` | 前后端共享 API 类型 | diff --git a/docs/development/backend.md b/docs/development/backend.md new file mode 100644 index 0000000..6cc394e --- /dev/null +++ b/docs/development/backend.md @@ -0,0 +1,123 @@ +# 后端开发 + +## 库使用优先级 + +| 优先级 | 来源 | 典型用途 | +| ------ | ------------ | -------------------------------------------------------------- | +| 1 | Bun 内置 API | `Bun.serve`、`bun:sqlite`、`Bun.spawn`、`Bun.file`、`Bun.YAML` | +| 2 | es-toolkit | 类型判断、深度比较、错误判断、并发控制、集合操作 | +| 3 | 标准 Web API | `Object.fromEntries`、`Headers`、`fetch`、`AbortController` | +| 4 | 主流三方库 | cheerio、xpath、@xmldom/xmldom | +| 5 | 自行实现 | 仅在以上都无法满足时 | + +新增依赖前必须先检查上述每一层是否已有可用方案。 + +## API 路由开发 + +路由文件位于 `src/server/routes/`,每个端点一个文件。路由通过 `server.ts` 的 `Bun.serve({ routes })` 声明式注册,使用 per-method handler 对象。 + +新增路由步骤: + +1. 在 `src/server/routes/` 下创建 `.ts`。 +2. 实现 handler 函数并 export。 +3. 在 `server.ts` 的 `routes` 对象中注册路径和 method handler。 +4. 在 `tests/server/app.test.ts` 中添加集成测试。 + +请求参数校验使用 `middleware.ts` 提供的 `validateTargetId`、`validateTimeRange`、`validatePagination`、`validateDashboardWindow`、`validateRecentLimit`、`validateMetricsBucket`。 + +## 共享 helpers + +| 函数 | 用途 | +| ------------------------------- | ------------------------------------ | +| `createApiError(error, status)` | 构造 API 错误体 | +| `createHeaders(mode, init)` | 创建响应 Headers,生产模式附加安全头 | +| `createHealthResponse()` | 构造健康检查响应 | +| `formatDuration(ms)` | 毫秒转为可读时长字符串 | +| `jsonResponse(body, options)` | JSON 响应构造 | +| `mapCheckResult(row, type)` | 数据库行转 API CheckResult | + +## 类型规范 + +- 共享类型以 `src/shared/api.ts` 为唯一源头。 +- 严格联合类型优先于宽类型。 +- 存储层类型与 API 类型分离。 +- checker 具体类型在各自目录定义,中间层通过 base interface 和 registry 完成类型擦除。 +- 纯类型导入使用 `import type`。 + +## 配置契约与校验 + +配置加载流程固定为:`unknown -> AuthoringProbeConfig -> NormalizedProbeConfig -> ValidatedProbeConfig -> ResolvedConfig`。 + +| 层级 | 职责 | +| ---------- | ------------------------------------------------ | +| Authoring | 用户 YAML 可书写形态,允许变量引用和 expect 简写 | +| Normalized | 变量替换和 expect 简写展开后的契约校验形态 | +| Validated | 通过契约校验和语义校验的形态 | +| Resolved | checker `resolve()` 后的运行期配置 | + +Ajv 保持严格拒绝模式:`allErrors: true`、不启用类型强制转换、不注入默认值、不自动删除未知字段。默认对象策略是 `additionalProperties: false`,只有明确的动态键值表可以开放任意键名。 + +新增或修改配置字段时必须同步更新 TypeBox schema fragments、`probe-config.schema.json`、语义 validator、测试和对应用户文档,并运行 `bun run schema:check`。 + +## 数据存储 + +存储层基于 `bun:sqlite`,WAL 模式运行,数据库文件位于配置的 `dataDir` 下。 + +| 方法 | 用途 | +| ------------------------------------------ | ---------------------------------- | +| `syncTargets(targets)` | 启动期同步 targets | +| `insertCheckResult()` | 写入单条检查结果 | +| `getTargets()` | 查询全部 targets | +| `getLatestChecksMap()` | 批量获取每个 target 的最新检查结果 | +| `getAllTargetWindowStats(from, to)` | 批量获取窗口基础计数 | +| `getDashboardIncidentStates(from, to)` | 获取 Dashboard 窗口状态序列 | +| `getAllRecentSamples(limit)` | 批量获取最近采样 | +| `getTargetCheckpoints(targetId, from, to)` | 获取单目标窗口检查点序列 | +| `getTargetDurations(targetId, from, to)` | 获取单目标成功耗时数组 | +| `getHistory()` | 分页查询历史记录 | +| `prune(retentionMs)` | 清理过期数据 | + +数据库只负责存储、筛选、排序、分页、LIMIT 和基础聚合。指标语义在后端应用层实现。 + +## 拨测引擎 + +- 按 interval 分组,每组独立定时触发。 +- 使用 `es-toolkit/Semaphore` 限制全局最大并发数。 +- 通过 `checkerRegistry.get(target.type)` 选择 runner。 +- 每次检查创建 `AbortController` 并按 `target.timeoutMs` 触发 abort。 +- 状态变化通过注入的 `Logger` 输出结构化日志。 + +## 日志模块 + +后端运行时代码统一通过 `Logger` 接口输出日志,禁止直接使用 `console.*`。配置加载失败前使用 `ConsoleFallbackLogger`。 + +| 实现 | 用途 | +| ----------------------- | --------------------------------------------- | +| `PinoLoggerWrapper` | 生产运行时,封装 Pino、pino-pretty、pino-roll | +| `NoopLogger` | 静默丢弃日志 | +| `MemoryLogger` | 测试替身 | +| `ConsoleFallbackLogger` | 配置加载失败前的降级日志 | + +敏感信息会自动 redact `authorization`、`cookie`、`set-cookie`、`authToken`、`key`、`password`、`token`、`apiKey` 及其嵌套路径。 + +## expect 系统 + +共享断言基础设施位于 `src/server/checker/expect/`。新增或修改 checker 的 expect 字段时,按以下原则选择模型: + +| 模型 | 用途 | 典型字段 | +| --------------------- | ---------------------------- | ------------------------------------------------------------------- | +| enum / boolean | 状态类结果,结果集合小且稳定 | HTTP status、Cmd exitCode、TCP connected、UDP responded、ICMP alive | +| `ValueMatcher` | 数字指标和字符串元数据 | durationMs、rowCount、finishReason、usage | +| `ContentExpectations` | 返回内容或半结构化内容 | body、stdout、stderr、banner、response、output、result | +| `KeyedExpectations` | 动态键值断言 | headers、DB rows 列值 | + +详细 checker 开发流程见 [Checker 开发](checker-development.md)。 + +## 错误模型 + +| 类型 | 结构 | +| ------------ | ----------------------------------- | ------------------------------------------------------- | +| API 错误 | `{ error: "描述", status: }` | +| CheckFailure | `{ kind: "error" | "mismatch", phase, path, expected?, actual?, message }` | + +expect 校验失败记录首个失败原因;网络、超时、进程崩溃统一为 `kind: "error"`。 diff --git a/docs/development/build-release.md b/docs/development/build-release.md new file mode 100644 index 0000000..9a56435 --- /dev/null +++ b/docs/development/build-release.md @@ -0,0 +1,107 @@ +# 构建与发布 + +## 开发期运行 + +```bash +bun run dev probes.yaml +``` + +`scripts/dev.ts` 同时启动两个进程: + +| 进程 | 用途 | +| --------------- | ------------------------------------------------- | +| Bun API server | 后端 API 服务,`--watch` 监听后端文件变更自动重启 | +| Vite dev server | 前端 SPA、HMR、模块热替换 | + +也可以单独启动: + +```bash +bun run dev:server probes.yaml +bun run dev:web +``` + +## 前后端集成 + +开发模式下,Vite 通过 proxy 将 `/api/*` 和 `/health` 转发到 Bun。 + +生产模式下,前端通过 Vite 构建为静态资源,通过 `import with { type: "file" }` 嵌入 Bun 可执行文件。非 API 路径由 fetch fallback 处理:有文件扩展名的返回静态资源或 404,无扩展名的返回 SPA index.html。 + +## 构建 + +```bash +bun run build +``` + +构建流程: + +```text +1. Vite build -> dist/web/ +2. Code generation -> .build/static-assets.ts + .build/server-entry.ts +3. Bun compile -> dist/dial-server +``` + +构建参数: + +| 环境变量 | 说明 | +| -------------- | ---------------- | +| `BUN_TARGET` | 交叉编译目标平台 | +| `BUILD_TARGET` | 交叉编译目标平台 | + +## Docker 镜像 + +Docker 镜像使用 Alpine 多阶段构建,保持与生产单可执行文件交付模型一致。 + +```text +oven/bun:1-alpine -> bun install --frozen-lockfile + -> BUN_TARGET=bun-linux-*-musl bun run build + -> dist/dial-server + +alpine -> 仅复制 /usr/local/bin/dial-server + -> 安装 ca-certificates、iputils-ping、libgcc、libstdc++、tzdata + -> 使用非 root dial 用户运行 +``` + +Dockerfile 通过 `TARGETARCH` 选择 Bun compile target。 + +| `TARGETARCH` | `BUN_TARGET` | +| ------------ | ---------------------- | +| `amd64` | `bun-linux-x64-musl` | +| `arm64` | `bun-linux-arm64-musl` | + +## Release + +```bash +bun run release +bun run release --target linux-x64 +bun run release --target linux-x64,windows-x64,darwin-arm64 +``` + +release 流程: + +```text +1. Vite build -> dist/web/ +2. Code generation -> .build/ +3. 多目标 Bun compile -> dist/release/binaries/ +4. tar.gz 打包 -> dist/release/packages/ +``` + +支持的平台见 [用户部署文档](../user/deployment.md#跨平台发布包)。 + +## 脚本说明 + +| 脚本 | 文件 | 说明 | +| ---------------------- | ----------------------------------- | ------------------------------ | +| `bun run dev` | `scripts/dev.ts` | 双进程开发服务 | +| `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 release` | `scripts/release.ts` | 多目标交叉编译和打包 | +| `bun run schema` | `scripts/generate-config-schema.ts` | 生成配置 JSON Schema | +| `bun run schema:check` | `scripts/generate-config-schema.ts` | 检查配置 JSON Schema 同步 | +| `bun run clean` | `scripts/clean.ts` | 清理构建缓存与临时文件 | + +## 维护约定 + +- `scripts/build-common.ts` 中的 import specifier 输出必须使用 `/` 分隔符。 +- 跨平台路径测试不得用当前平台 `path.sep` 伪装其他平台,应使用 `node:path.win32` 或等价注入方式模拟。 +- 如本地 Docker 环境不支持 buildx 或多架构模拟,需在变更记录中说明未执行原因。 diff --git a/docs/development/checker-development.md b/docs/development/checker-development.md new file mode 100644 index 0000000..d3b4487 --- /dev/null +++ b/docs/development/checker-development.md @@ -0,0 +1,161 @@ +# Checker 开发 + +Checker 是 DiAL 的核心扩展单元。每个 checker 是 `src/server/checker/runner//` 下的自包含目录,包含该 checker 所需的类型、schema、校验、执行逻辑和断言。 + +新增或修改 checker 前请同时阅读 [`../../CONTRIBUTING.md`](../../CONTRIBUTING.md)、[配置文件](../user/configuration.md)、[校验规则](../user/expectations.md) 和 [Checker 用户文档](../user/checkers/README.md)。 + +## 架构目标 + +```text +checkerRegistry + ├── runner/index.ts + ├── schema/builder.ts + ├── schema/validate.ts + ├── config-loader.ts + ├── engine.ts + └── store.ts +``` + +注册后,中间层通过 registry 自动委托 schema 生成、契约校验、配置 resolve、执行和序列化。新增 checker 不应在中间层新增 `switch/case` 或类型分支。 + +## 标准文件结构 + +| 文件 | 职责 | +| ------------- | ----------------------------------------------------- | +| `index.ts` | 模块入口,re-export Checker 类 | +| `types.ts` | Checker 专属类型 | +| `schema.ts` | TypeBox 契约 schema,包含 config 和 expect | +| `validate.ts` | 启动期语义校验 | +| `execute.ts` | Checker 类,实现 resolve、execute、serialize | +| `expect.ts` | Checker 专用断言函数 | +| 其他文件 | 协议解析、编码、provider 适配、平台命令封装等专属逻辑 | + +## 类型定义 + +在 `types.ts` 中定义: + +- `RawXxxTargetConfig` +- `RawXxxExpectConfig` +- `ResolvedXxxExpectConfig` +- `ResolvedXxxTarget extends ResolvedTargetBase` + +不需要修改顶层 `checker/types.ts`。base interface 使用 index signature 支持扩展。 + +## Schema + +checker 必须提供 `CheckerSchemas`,包含 Authoring 和 Normalized 两套 config/expect 片段。Authoring 描述用户 YAML 可写 DSL,Normalized 描述 normalizer 输出。 + +常用 fragments: + +| Fragment | 用途 | +| ----------------------------------- | ------------------------- | +| `durationSchema` | 时长字符串 | +| `sizeSchema` | 大小单位 | +| `statusCodePatternSchema` | HTTP 状态码或范围 | +| `stringMapSchema` | headers、env 等字符串映射 | +| `createValueMatcherSchema()` | ValueMatcher | +| `createContentExpectationsSchema()` | ContentExpectations | +| `createKeyedExpectationsSchema()` | KeyedExpectations | + +默认对象策略为 `additionalProperties: false`。只有明确的动态键值表可以开放任意键名。 + +## 语义校验 + +在 `validate.ts` 中实现 JSON Schema 无法表达的规则,统一返回 `ConfigValidationIssue[]`,不要直接拼接最终错误字符串。 + +共享校验工具包括: + +| 函数 | 用途 | +| -------------------------------- | ---------------------------- | +| `validateRawValueExpectation` | 校验 Raw ValueExpectation | +| `validateRawContentExpectations` | 校验 ContentExpectations | +| `validateRawKeyedExpectations` | 校验 KeyedExpectations | +| `validateJsonPath` | 校验项目支持的 JSONPath 子集 | +| `isJsonValue` | 判断合法 JSON value | + +## resolve 规范 + +`resolve()` 只做内置默认值填充、路径解析、单位转换,不执行校验。输入已经通过 Normalized schema 和语义校验,expect 已是 normalized 形态。 + +```typescript +const expect = target.expect as ResolvedXxxExpectConfig | undefined; +const resolvedExpect: ResolvedXxxExpectConfig = expect + ? { ...expect, status: expect.status ?? [200] } + : { status: [200] }; +``` + +返回值使用 `satisfies ResolvedXxxTarget` 确保类型正确。 + +## execute 规范 + +- 始终记录 `timestamp` 和 `start = performance.now()`。 +- 通过 `ctx.signal` 支持超时取消。 +- 首个 expect 失败即停止,返回带 `failure` 的结果。 +- 成功时 `failure: null, matched: true`。 +- 异常时使用 `errorFailure()`。 +- 不匹配时使用 `mismatchFailure()`。 +- `expected` 参数应传用户可读值,必要时使用 `displayValueExpectation()`。 + +## expect 字段选择 + +| 场景 | 模型 | +| ------------------------------------ | ------------------- | +| 状态类结果且集合小而稳定 | enum 或 boolean | +| 单值数字指标或字符串元数据 | ValueMatcher | +| 文本、JSON、HTML、XML 或半结构化内容 | ContentExpectations | +| 动态键值表 | KeyedExpectations | + +不要为了统一而把状态类字段改成 ValueMatcher。一个 expect 字段只能对应一种断言模型。 + +## 注册 + +1. 创建 `src/server/checker/runner//index.ts`。 +2. 在 `src/server/checker/runner/index.ts` 添加导入。 +3. 在 registry 初始化数组中添加 checker 实例。 + +注册后,schema builder、validate、config-loader、engine、store 会自动按 registry 分发。 + +## 测试要求 + +测试文件放在 `tests/server/checker/runner//`,结构镜像源文件。 + +| 测试类别 | 覆盖内容 | +| ------------ | ---------------------------------------- | +| 契约测试 | TypeBox schema 与 JSON Schema 导出一致性 | +| 语义校验测试 | 合法和非法配置 | +| resolve 测试 | 默认值合并、路径解析、单位转换 | +| execute 测试 | 成功、失败、超时、expect 组合 | +| 注册测试 | registry 注册行为 | +| 配置加载测试 | 含新 checker 的 YAML 完整加载流程 | + +## 文档和 schema 更新 + +新增或修改 checker 时通常需要更新: + +- `probes.example.yaml` +- `probe-config.schema.json`,通过 `bun run schema` 生成 +- `docs/user/checkers/.md` +- `docs/user/checkers/README.md` +- `docs/user/expectations.md`,仅当断言模型或通用规则变化 +- `docs/development/checker-development.md`,仅当开发机制变化 +- `CONTRIBUTING.md`,仅当贡献流程或 checklist 变化 + +## 完成检查清单 + +```text +□ src/server/checker/runner//types.ts +□ src/server/checker/runner//schema.ts +□ src/server/checker/runner//validate.ts +□ src/server/checker/runner//execute.ts +□ src/server/checker/runner//expect.ts +□ src/server/checker/runner//index.ts +□ src/server/checker/runner/index.ts +□ tests/server/checker/runner// +□ probes.example.yaml +□ probe-config.schema.json +□ docs/user/checkers/.md +□ bun run schema +□ bun run schema:check +□ bun run check +□ bun run verify +``` diff --git a/docs/development/frontend.md b/docs/development/frontend.md new file mode 100644 index 0000000..883477b --- /dev/null +++ b/docs/development/frontend.md @@ -0,0 +1,117 @@ +# 前端开发 + +## 技术栈 + +| 层面 | 技术 | 用途 | +| ------ | ------------------------------------- | ---------------------------------------------- | +| 框架 | React 19 | UI 组件开发 | +| 构建 | Bun HTML import + Vite dev server | 开发服务与生产构建 | +| 语言 | TypeScript 6 | 类型安全 | +| UI 库 | TDesign React + tdesign-icons-react | UI 组件与图标 | +| 数据层 | TanStack Query + React Query Devtools | 服务端状态管理与自动轮询 | +| 图表 | Recharts | 拨测趋势图 | +| 动画 | @number-flow/react | 倒计时数字滚动过渡 | +| 路由 | 无 | 单页面 Dashboard,通过 Drawer/Tab 做页面内导航 | + +不引入 React Router 或额外状态管理库。TanStack Query 承担服务端状态,组件内状态使用 `useState`。 + +## 组件树与数据流 + +```text +main.tsx +└── StrictMode + └── ErrorBoundary + └── QueryClientProvider + ├── App + │ ├── useThemePreference() + │ ├── useDashboard(refreshInterval) + │ ├── SummaryCards + │ └── TargetBoard + │ └── TargetGroup[] + │ └── PrimaryTable + │ └── TargetDetailDrawer + │ └── useTargetDetail() + │ ├── OverviewTab + │ └── HistoryTab + └── ReactQueryDevtools +``` + +## TanStack Query 规范 + +Query key 使用 structured array,排序为 scope -> id -> 参数。 + +```typescript +const queryKeys = { + dashboard: () => ["dashboard", "24h", 30] as const, + meta: () => ["meta"] as const, + metrics: (targetId: number, from: string, to: string, bucket: "auto" | MetricsBucket) => + ["metrics", targetId, from, to, bucket] as const, + history: (targetId: number, from: string, to: string, page: number) => ["history", targetId, from, to, page] as const, +}; +``` + +全局面板级查询可持续刷新,详情级查询必须按 Drawer 状态和 Tab 状态条件启用。 + +## fetch 封装 + +统一使用 `fetch`,不引入 axios。错误抛异常,由 TanStack Query 的 `error` 状态承接。 + +```typescript +async function fetchJson(url: string): Promise { + const response = await fetch(url); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + return response.json() as Promise; +} +``` + +## 组件开发规范 + +- 每个 React 组件一个 `.tsx` 文件,文件名使用 PascalCase。 +- 组件 props 定义为 `interface XxxProps`,紧邻组件函数声明。 +- 类型从 `../../shared/api` 导入,使用 `import type`。 +- 展示组件放在 `components/`,通过 props 接收数据,通过回调返回事件。 +- 容器逻辑放在 hooks 中,组件只做数据消费。 +- 列定义、排序器、筛选器、颜色阈值等常量放在 `constants/`。 +- 时间处理等纯函数放在 `utils/`。 + +## 现有组件 + +| 组件 | 用途 | +| -------------------- | ----------------------------------------------------------------- | +| `App` | 根组件,Layout + HeadMenu 骨架、主题模式、刷新控制、Skeleton 加载 | +| `ErrorBoundary` | React 错误边界 | +| `SummaryCards` | 总览统计卡片 | +| `TargetBoard` | 按分组渲染目标表格列表 | +| `TargetGroup` | 单个分组 Card + PrimaryTable | +| `TargetDetailDrawer` | 目标详情抽屉 | +| `OverviewTab` | 目标详情概览 | +| `HistoryTab` | 目标历史记录表格和分页 | +| `TrendChart` | 趋势折线图 | +| `StatusDot` | 圆形状态指示点 | +| `StatusBar` | 最近采样状态条 | +| `RefreshCountdown` | Header 刷新倒计时和手动刷新按钮 | + +## 样式规范 + +前端基于 TDesign React 构建 UI,样式开发优先级: + +1. TDesign 组件 +2. TDesign 组件 props +3. TDesign CSS tokens(`--td-*`) +4. `styles.css` CSS 类 +5. 自行开发组件 + +红线: + +- 严禁在组件中使用 `style` 属性内联调整样式。 +- 严禁通过 CSS 覆盖 TDesign 组件内部类名。 +- 严禁使用 `!important`。 +- 颜色统一使用 TDesign CSS tokens,不使用硬编码色值。 + +## 前端测试 + +- 测试目录为 `tests/web/`,结构对应 `src/web/`。 +- 重点测试 `constants/` 中的纯函数。 +- 组件测试使用 jsdom 和 `@testing-library/react`。 +- 测试用户行为而非实现细节。 +- 只 mock 系统边界,例如 `fetch`。 diff --git a/docs/development/testing-quality.md b/docs/development/testing-quality.md new file mode 100644 index 0000000..4d95923 --- /dev/null +++ b/docs/development/testing-quality.md @@ -0,0 +1,93 @@ +# 测试与质量 + +## 质量命令 + +| 命令 | 说明 | +| ---------------------- | -------------------------------------------------------------- | +| `bun run lint` | ESLint 检查,含类型感知规则、导入排序、导入验证、Prettier 格式 | +| `bun run format` | Prettier 自动格式化 | +| `bun run schema:check` | 检查 `probe-config.schema.json` 是否与 TypeBox fragments 同步 | +| `bun run typecheck` | TypeScript 类型检查 | +| `bun test` | 运行所有测试 | +| `bun run check` | `schema:check + typecheck + lint + test` | +| `bun run verify` | `check + build` | + +## ESLint + +配置文件:`eslint.config.js`。 + +| 配置来源 | 用途 | +| -------------------------------------------- | ---------------------------------------- | +| `@eslint/js` recommended | JavaScript 基础规则 | +| `typescript-eslint` recommended-type-checked | TypeScript 类型感知规则 | +| `typescript-eslint` stylistic-type-checked | TypeScript 风格规则 | +| `eslint-plugin-perfectionist` | 导入语句和命名导出排序 | +| `eslint-plugin-import` | 导入路径验证、循环依赖检测、重复导入合并 | +| `eslint-plugin-prettier` | 将 Prettier 格式集成为 ESLint 规则 | + +后端运行时代码禁止直接使用 `console.*`,请通过注入的 Logger 实例输出日志。 + +## Prettier + +配置文件:`.prettierrc.json`。显式声明格式化参数,包括 `printWidth: 120`、`semi: true`、`singleQuote: false`、`trailingComma: "all"`、`endOfLine: "lf"`。 + +## TypeScript 严格标志 + +| 标志 | 值 | 说明 | +| ------------------------------------ | ----- | ------------------------------- | +| `strict` | true | 全局严格模式 | +| `noUnusedLocals` | true | 未使用局部变量视为错误 | +| `noUnusedParameters` | false | 保留关闭 | +| `noPropertyAccessFromIndexSignature` | true | 索引签名必须用括号访问 | +| `noUncheckedIndexedAccess` | true | 数组和 Map 访问必须运行时检查 | +| `noImplicitOverride` | true | 覆盖父类方法必须显式 `override` | +| `verbatimModuleSyntax` | true | 强制 `import type` 纯类型导入 | + +## Git hooks + +| Hook | 行为 | +| ------------ | ------------------------------------------ | +| `pre-commit` | lint-staged 对变更文件运行 eslint/prettier | +| `commit-msg` | commitlint 校验提交信息格式 | + +提交信息格式为 `类型: 简短描述`,类型限定为 `feat`、`fix`、`refactor`、`docs`、`style`、`test`、`chore`。 + +## 测试分层 + +| 层级 | 覆盖范围 | 位置 | +| -------- | ---------------------- | ----------------------------------------------------------------------------- | +| 单元测试 | 后端函数、纯函数、常量 | `tests/server/**/*.test.ts`、`tests/web/{constants,utils,hooks}/**/*.test.ts` | +| 组件测试 | React 组件渲染和交互 | `tests/web/components/**/*.test.tsx` | + +## 测试命令 + +```bash +bun test +bun test tests/server +bun test tests/web +bun run check +bun run verify +``` + +## 组件测试环境 + +组件测试使用 jsdom,配置位于 `tests/setup.ts`,通过 `bunfig.toml` preload 加载。 + +包含的 polyfill 和 mock: + +- ResizeObserver +- IntersectionObserver +- matchMedia +- attachEvent +- recharts 图表 mock + +## 编写规范 + +- 优先使用 `@testing-library/react` 的语义化查询。 +- 测试用户行为而非实现细节。 +- 只 mock 系统边界。 +- 使用真实的 QueryClientProvider 包裹组件。 +- 组件测试文件命名为 `tests/web/components/ComponentName.test.tsx`。 +- 异步错误断言使用 helper 或显式 try/catch,避免依赖 Bun `expect(...).rejects` 与 `await-thenable` 规则的类型不匹配。 +- polyfill 中的 intentional no-op 使用显式可解释写法。 +- 对 `process.exit` 等系统 API 使用 `spyOn` 受控 mock。 diff --git a/docs/prompts/README.md b/docs/prompts/README.md index 1b8fb77..82c7796 100644 --- a/docs/prompts/README.md +++ b/docs/prompts/README.md @@ -7,7 +7,6 @@ | 文件 | 用途 | | ------------------------------------------------------ | ------------------------------------------------------------------------ | | [prompt-smart-merge.md](prompt-smart-merge.md) | 批量合并 `dev*` 分支到目标分支,含规则探测、依赖分析、冲突处理、安全回退 | -| [prompt-spec-review.md](prompt-spec-review.md) | 审查和整理 `openspec/specs/` 下的稳定规范,提升可检索性和一致性 | | [prompt-proposal-review.md](prompt-proposal-review.md) | 审查 proposal/design/tasks/specs 与讨论、代码现状、OpenSpec 规范的一致性 | | [prompt-apply-review.md](prompt-apply-review.md) | 审查 apply 后代码、测试、变更文档的一致性,并补齐遗漏或回写文档 | @@ -85,7 +84,7 @@ - 是否以代码、文档、讨论或用户确认为准 - 何时必须使用提问工具确认 - 删除、重写前是否必须备份 -- 改动后是否必须同步 README、测试、变更文档 +- 改动后是否必须同步相关用户文档、开发文档、测试、变更文档 ### 4. 计划与执行分离 @@ -124,7 +123,7 @@ - 作用域边界:改什么,不改什么 - 真相来源优先级:代码 / README / spec / 讨论 / 用户确认 - 风险动作边界:删除、重写、提交、推送、回退、stash、merge 等 -- 同步要求:测试、README、变更文档、现有 spec 是否要同步 +- 同步要求:测试、用户文档、开发文档、变更文档、现有 spec 是否要同步 - 降级规则:信息不足时如何处理 避免: @@ -142,7 +141,7 @@ 推荐做法: -- 先读仓库规则来源,如 `README.md`、配置、架构文档、近期提交、任务入口 +- 先读仓库规则来源,如 `README.md`、`DEVELOPMENT.md`、`CONTRIBUTING.md`、`docs/README.md`、配置、架构文档、近期提交、任务入口 - 先读直接相关 artifacts,再扩展到相关代码和测试 - 需要探测时,要求 AI 先探测再决定,不把仓库结构写死在提示词里 diff --git a/docs/prompts/prompt-spec-review.md b/docs/prompts/prompt-spec-review.md deleted file mode 100644 index af1bced..0000000 --- a/docs/prompts/prompt-spec-review.md +++ /dev/null @@ -1,143 +0,0 @@ -请审查并整理 `openspec/specs/` 下的稳定规范,使其成为可搜索、边界清晰、无冗余、与当前业务一致的能力索引,按以下流程执行。 - -## 约束 - -- `openspec/specs/` 描述长期稳定的业务能力、规则和外部行为,不记录变更过程、迁移说明、实现路径、内部类型名、组件 props、样式数值、层级分层等实现细节 -- 用户可感知或对外暴露的契约可以保留:公开 API 路径、请求/响应字段、协议名、错误码、数据约束、交互结果 -- `Requirement` 和 `Scenario` 应描述业务能力、外部行为或稳定约束,不以“使用某层/某组件/某库实现”作为标题或核心表述 -- 不把当前代码自动视为唯一真相;若代码、README、现有 spec 冲突且无法判断应以哪边为准,列入待确认清单,不直接改写规范 -- 仅删除内容已被其他规范完整覆盖且无独立检索价值的规范;非冗余内容仅迁移、合并、拆分或重命名 -- 每批重构执行前用提问工具获得用户确认;删除或重写前先备份原文件为 `{file}.bak.{timestamp}` -- 命名、Purpose、Requirement 标题都必须保留用户下一次最可能搜索的业务关键词 -- 使用subagents处理计算密集或多步骤的并行任务(如代码实现、测试执行);文件读取直接使用Read工具并行调用,禁止用subagent转发文件内容 - -## 1. 收集 - -读取: - -- `openspec/config.yaml` -- `README.md`,以及与模块结构、API、架构相关的 README 或文档 -- `openspec/specs/*/spec.md` - -默认不读取 `openspec/changes/**`、历史 proposal/design/tasks 作为稳定规范整理依据;仅在用户明确要求“连同历史变更一起校对”时再纳入。 - -先建立索引,不直接开始改写: - -| 索引 | 内容 | -| -------------- | ----------------------------------------------------------------------------- | -| `spec_index[]` | 每个 spec 的目录名、Purpose、Requirement 摘要、关键词、外部契约、疑似重叠对象 | -| `domain_map[]` | 从 README、API、模块文档中提炼的核心业务域、横切能力和术语 | -| `term_map[]` | 同义词、旧名、缩写和推荐标准术语 | -| `suspects[]` | 需要进一步对照代码或测试确认的 spec | - -仅对 `suspects[]` 做定向读取: - -- 读取与该 spec 对应的源码、测试、README 或架构文档 -- 不对 `backend/`、`frontend/` 做无差别逐文件扫描 - -判定依据优先级: - -- 当前稳定 spec 与 README 共同支持的事实,可直接视为高置信度 -- 仅代码可见但 README 和 spec 未体现的内容,先判断它是稳定外部行为还是临时实现细节 -- 代码、README、现有 spec 互相冲突且无法自动定夺时,进入 `待确认清单` - -## 2. 审查 - -按 spec、Requirement、Scenario 三层检查: - -| 维度 | 检查点 | -| --------- | --------------------------------------------------------------------------------- | -| 过时 | 描述的能力、术语、外部契约是否仍成立;是否存在 `TBD`、`TODO`、占位说明 | -| 冲突 | 不同规范是否对同一行为给出不同约束、命名或边界 | -| 重复/重叠 | 是否在文件级、Requirement 级、Scenario 级重复描述同一能力 | -| 错位 | 内容是否放错能力域;横切规则是否混入实体规范;平台实现是否混入通用能力规范 | -| 粒度 | 是否过大导致难检索,或过碎导致回答一个问题必须同时打开多个 spec | -| 术语 | 同一概念是否混用多个名字;旧名、别名、缩写是否需要归一并保留检索入口 | -| 命名/检索 | 目录名、Purpose、Requirement 标题是否准确;是否能被 README、API、业务术语直接命中 | -| 规范性 | 是否使用 SHALL/WHEN/THEN;是否混入变更记录、迁移说明、内部实现或 UI/代码细节 | -| 完整性 | Purpose 是否明确;是否存在空目录、非 spec 噪音文件、无清晰归属的孤立规范 | - -重构判定规则: - -- 若两个 spec 回答的是同一个核心问题,或其中一个只是另一个的子集,优先合并 -- 若一个 spec 混合多个独立检索意图,或同时包含横切规则与业务流程,优先拆分 -- 若内容正确但目录名、Purpose 或 Requirement 标题不利于检索,优先重命名或改写标题 -- 若多个术语指向同一概念,统一到一个标准术语,并在 Purpose 或 Requirement 中保留必要别名以支持搜索 -- 若某段内容只是内部实现细节,且不影响外部行为理解,删除该段而不是为其单独保留 spec -- 若某个具体值同时属于外部契约与内部实现,按“是否对调用方可见、是否影响兼容性”判断是否保留 - -### 命名约定 - -命名优先复用仓库已存在的稳定术语,如 `provider`、`model`、`stats`、`protocol`、`proxy`、`logging`、`validation`、`migration`、`frontend`、`desktop`、`mysql`。 - -| 类型 | 模式 | 示例 | -| ------------ | ---------------------------------------------------------- | -------------------------------------------------- | -| 实体生命周期 | `{entity}-management` | `provider-management`、`model-management` | -| 横切能力 | `{concern}` 或 `{concern}-{qualifier}` | `error-handling`、`structured-logging` | -| 协议/适配 | `{protocol}-{capability}` 或 `protocol-adapter-{protocol}` | `openai-protocol-proxy`、`protocol-adapter-openai` | -| 运行面/入口 | `{surface}` 或 `{surface}-{capability}` | `frontend`、`desktop-app` | -| 基础设施 | `{resource}-{operation}` | `database-migration`、`mysql-driver` | - -命名原则: - -- 1-4 个词,保持单一主题 -- 优先使用业务名词,不使用 `basic`、`general`、`misc`、`info`、`data` 等泛化词 -- 不使用 `crud`、`list`、`table`、`display` 等实现模式词,除非它本身就是外部契约的一部分 -- 同一主题的命名模式保持一致,不同时混用多套前后缀 - -## 3. 报告 - -输出分析结果: - -1. **问题总览表**:问题类型 × 涉及规范数 -2. **规范关系表**:每个 spec 的主主题、重叠对象、冲突对象、建议动作 -3. **术语归一表**:旧术语 / 别名 / 缩写 → 推荐标准术语 -4. **逐项分析**:每个有问题的规范说明位置、问题、影响、建议和目标规范 -5. **待确认清单**:代码、README、现有 spec 冲突且无法自动定夺的事项 -6. **重构方案**:按优先级分批 -7. **重构后目录结构**:预期的新 `openspec/specs/` 目录树 - -优先级建议: - -- P0:删除空目录、非 spec 噪音文件、占位内容 -- P1:删除完全冗余规范;将其内容映射到主规范 -- P2:合并重复/子集规范;拆分错位或过大规范 -- P3:重命名目录、改写 Purpose 和 Requirement 标题以提升检索性 -- P4:修正过时描述,清理实现细节、迁移说明和变更记录 - -若所有问题清单为空,输出“审查通过,未发现问题”,跳至步骤 5。 - -## 4. 计划(用户确认) - -先针对 `待确认清单` 用提问工具逐项向用户确认。 - -再按批次展示完整重构计划,每批必须包含: - -- 操作类型:删除、重命名、迁移、合并、拆分、改写 -- 路径变化:源路径 → 目标路径 -- 内容映射:源 spec 的 Requirement / Scenario 将迁移到哪里 -- 术语处理:哪些旧词保留为检索入口,哪些词统一替换 -- 执行原因:为什么这样做更利于检索、去重和边界清晰 -- 验证方式:如何确认没有丢失约束或引入新的冲突 - -用提问工具获得当前批次确认后再执行。 - -## 5. 执行 - -按 P0 → P4 逐批执行已确认的重构。 - -执行要求: - -- 合并或拆分时先写目标 spec,再删除或重命名源 spec -- 删除前确认其 Requirement 和 Scenario 已被完整保留、迁移或判定为纯冗余 -- 每批执行后重新读取受影响的 spec,并复核结构和内容 - -每批执行后至少验证: - -- 目录结构完整,`openspec/specs/*/spec.md` 可正常读取 -- 不存在未承接的 Requirement 或 Scenario -- Purpose、Requirement 标题、目录名可以直接表达主能力 -- 不再包含 `TBD`、变更记录、迁移说明、内部实现细节或噪音文件 -- 若本批涉及代码对照项,相关外部契约描述与当前仓库现状一致,或已列入残留待确认 - -收尾时输出:修改文件清单、备份文件清单、最终目录树、残留待确认事项和整理摘要。 diff --git a/docs/user/checkers/README.md b/docs/user/checkers/README.md new file mode 100644 index 0000000..62bf345 --- /dev/null +++ b/docs/user/checkers/README.md @@ -0,0 +1,37 @@ +# Checker 参考 + +Checker 是 DiAL 的拨测执行单元。每个 target 通过 `type` 选择一个 checker,并配置对应的专属字段和 `expect` 规则。 + +## 支持的类型 + +| 类型 | 用途 | 文档 | +| ------ | -------------------------------------- | --------------- | +| `http` | HTTP/HTTPS 应用层健康检查 | [HTTP](http.md) | +| `cmd` | 执行本地命令或脚本 | [Cmd](cmd.md) | +| `db` | PostgreSQL/MySQL/SQLite 连接和查询检查 | [DB](db.md) | +| `tcp` | TCP 端口可达性和 banner 探测 | [TCP](tcp.md) | +| `udp` | UDP payload 请求-响应检查 | [UDP](udp.md) | +| `icmp` | 基于系统 `ping` 的存活、延迟、丢包检查 | [ICMP](icmp.md) | +| `dns` | 本机解析或指定 DNS server 协议级检查 | [DNS](dns.md) | +| `llm` | 大模型服务应用层健康检查 | [LLM](llm.md) | + +## 选择建议 + +| 目标 | 推荐 checker | +| ---------------------------------- | ------------ | +| Web API、网页、HTTP 状态码或响应体 | `http` | +| 本机脚本、外部命令、CLI 工具 | `cmd` | +| 数据库连接或查询结果 | `db` | +| 端口是否可连接、服务 banner | `tcp` | +| UDP 服务响应或简单心跳 | `udp` | +| 主机可达性、延迟、丢包率 | `icmp` | +| 域名解析值、DNS RCODE、TTL、flags | `dns` | +| LLM API 是否可用、输出是否符合预期 | `llm` | + +## 通用字段 + +所有 checker 都共享 target 通用字段,见 [配置文件](../configuration.md#targets-通用字段)。 + +## 通用断言模型 + +各 checker 的 `expect` 字段复用 `ValueMatcher`、`ContentExpectations` 和 `KeyedExpectations`。详情见 [校验规则](../expectations.md)。 diff --git a/docs/user/checkers/cmd.md b/docs/user/checkers/cmd.md new file mode 100644 index 0000000..9e269ea --- /dev/null +++ b/docs/user/checkers/cmd.md @@ -0,0 +1,38 @@ +# Cmd Checker + +`type: cmd` 用于执行本地命令或脚本,并校验退出码、stdout、stderr 和耗时。 + +## 配置项 + +| 字段 | 说明 | 必填 | 默认值 | +| ---------- | ------------------------------------ | ---- | ------ | +| `cmd.exec` | 可执行文件名或路径 | 是 | 无 | +| `cmd.args` | 命令行参数列表 | 否 | `[]` | +| `cmd.env` | 环境变量覆盖,继承进程环境变量并合并 | 否 | 无 | +| `cmd.cwd` | 工作目录,相对于配置文件所在目录 | 否 | 无 | + +## expect 校验项 + +| 字段 | 说明 | 必填 | 默认值 | +| ------------ | --------------------------------------------- | ---- | ------ | +| `exitCode` | 可接受的退出码列表 | 否 | `[0]` | +| `stdout` | 标准输出校验,使用 `ContentExpectations` 数组 | 否 | 无 | +| `stderr` | 标准错误校验,使用 `ContentExpectations` 数组 | 否 | 无 | +| `durationMs` | 完整执行耗时校验,使用 `ValueMatcher` | 否 | 无 | + +## 示例 + +```yaml +- id: "bun-script" + name: "Bun 脚本检查" + type: cmd + cmd: + exec: "bun" + args: ["-e", "console.log('ok')"] + expect: + exitCode: [0] + stdout: + - contains: "ok" +``` + +Docker 官方镜像不内置常见外部命令。容器内使用 CMD checker 时,按需通过派生镜像安装依赖命令。 diff --git a/docs/user/checkers/db.md b/docs/user/checkers/db.md new file mode 100644 index 0000000..f6f4038 --- /dev/null +++ b/docs/user/checkers/db.md @@ -0,0 +1,38 @@ +# DB Checker + +`type: db` 用于数据库连接和查询结果检查,支持 PostgreSQL、MySQL 和 SQLite。 + +## 配置项 + +| 字段 | 说明 | 必填 | 默认值 | +| ---------- | ------------------------------------------------------------- | ---- | ------ | +| `db.url` | 数据库连接字符串,支持 `postgres://`、`mysql://`、`sqlite://` | 是 | 无 | +| `db.query` | SQL 查询语句,不配置时仅测试连接 | 否 | 无 | + +## expect 校验项 + +| 字段 | 说明 | 必填 | 默认值 | +| ------------ | ----------------------------------------------------------------------- | ---- | ------ | +| `rowCount` | 查询返回行数校验,使用 `ValueMatcher` | 否 | 无 | +| `rows` | 查询结果逐行校验,数组内每行为列名到 `KeyedExpectations` 的映射 | 否 | 无 | +| `result` | 完整查询结果 `{ rows, rowCount }` 校验,使用 `ContentExpectations` 数组 | 否 | 无 | +| `durationMs` | 完整执行耗时校验,使用 `ValueMatcher` | 否 | 无 | + +## 示例 + +```yaml +- id: "sqlite-query" + name: "SQLite 数据库检查" + type: db + db: + url: "sqlite:///path/to/db.sqlite" + query: "SELECT COUNT(*) as cnt FROM users WHERE status = 'active'" + expect: + durationMs: + lte: 5000 + rowCount: + gte: 1 + rows: + - cnt: + gte: 0 +``` diff --git a/docs/user/checkers/dns.md b/docs/user/checkers/dns.md new file mode 100644 index 0000000..21547e9 --- /dev/null +++ b/docs/user/checkers/dns.md @@ -0,0 +1,104 @@ +# DNS Checker + +`type: dns` 支持两种解析模式:本机解析器检查和指定 DNS server 协议级检查。 + +## resolver 模式 + +| 模式 | 说明 | +| -------- | ----------------------------------------------------------------------------- | +| `system` | 使用本机 DNS 解析器检查域名是否能解析到预期地址 | +| `server` | 直接向指定 DNS server 发起 UDP/TCP 深度拨测,检查 RCODE、TTL、flags、记录值等 | + +## `dns.resolver: system` 配置项 + +| 字段 | 说明 | 必填 | 默认值 | +| -------------- | ----------------------------- | ---- | -------- | +| `dns.resolver` | 解析模式 | 是 | `system` | +| `dns.name` | 待解析域名 | 是 | 无 | +| `dns.family` | 地址族:`any`、`ipv4`、`ipv6` | 否 | `any` | + +### system 模式 expect + +| 字段 | 说明 | 断言模型 | +| ------------ | -------------------- | --------------------------------- | +| `values` | 解析结果地址集合断言 | DNS 集合(include/exclude/exact) | +| `valueCount` | 解析结果数量 | ValueMatcher | +| `durationMs` | 解析耗时 | ValueMatcher | + +```yaml +- id: "dns-system-api" + name: "本机 DNS 解析" + type: dns + dns: + resolver: system + name: "api.example.com" + family: any + expect: + values: + exact: + - "203.0.113.10" + durationMs: + lte: 500 +``` + +## `dns.resolver: server` 配置项 + +| 字段 | 说明 | 必填 | 默认值 | +| ---------------------- | --------------------------------- | ---- | -------- | +| `dns.resolver` | 解析模式 | 是 | `server` | +| `dns.server` | DNS server 地址 | 是 | 无 | +| `dns.name` | 查询域名 | 是 | 无 | +| `dns.port` | DNS server 端口 | 否 | `53` | +| `dns.protocol` | 传输协议:`udp`、`tcp` | 否 | `udp` | +| `dns.recordType` | DNS 记录类型 | 否 | `A` | +| `dns.recursionDesired` | 是否设置 RD flag | 否 | `true` | +| `dns.tcpFallback` | UDP 响应 TC=1 时是否 TCP fallback | 否 | `true` | +| `dns.maxResponseBytes` | 响应最大字节数 | 否 | `4KB` | + +`recordType` 可选值:`A`、`AAAA`、`CNAME`、`NS`、`MX`、`TXT`、`SOA`、`SRV`、`CAA`、`PTR`。 + +### server 模式 expect + +| 字段 | 说明 | 断言模型 | +| -------------------- | ---------------------------------- | --------------------------------- | +| `responded` | 是否收到 DNS response | boolean | +| `rcode` | 期望 RCODE 列表,如 `NOERROR` | string[] | +| `values` | 目标类型记录值集合断言 | DNS 集合(include/exclude/exact) | +| `valueCount` | 目标类型记录数量 | ValueMatcher | +| `answerCount` | answer section 总记录数 | ValueMatcher | +| `ttlMin` | answer 中最小 TTL | ValueMatcher | +| `ttlMax` | answer 中最大 TTL | ValueMatcher | +| `authoritative` | AA flag | boolean | +| `recursionAvailable` | RA flag | boolean | +| `truncated` | TC flag | boolean | +| `authenticatedData` | AD flag | boolean | +| `result` | 完整结构化响应的 JSONPath 兜底断言 | ContentExpectations | +| `durationMs` | 完整查询耗时 | ValueMatcher | + +```yaml +- id: "dns-server-api" + name: "Cloudflare DNS A 记录" + type: dns + dns: + resolver: server + server: "1.1.1.1" + name: "api.example.com" + recordType: A + expect: + rcode: ["NOERROR"] + values: + include: + - "203.0.113.10" + ttlMin: + gte: 60 + durationMs: + lte: 200 +``` + +## 注意事项 + +- 未配置 expect 时,`system` 模式默认要求解析成功且 `valueCount > 0`,`server` 模式默认要求 `NOERROR + valueCount > 0`。 +- 显式配置非 `NOERROR` rcode(如 `NXDOMAIN`)时,不自动要求 `valueCount > 0`。 +- `values.exact` 忽略返回顺序。 +- 对 A/AAAA 查询,CNAME 链不计入 `values`,单独放入 `cnameChain`。 +- `values` 按记录类型规范化为字符串,例如 MX 为 `"10 mail.example.com"`,SRV 为 `"10 60 443 server.example.com"`。 diff --git a/docs/user/checkers/http.md b/docs/user/checkers/http.md new file mode 100644 index 0000000..758c275 --- /dev/null +++ b/docs/user/checkers/http.md @@ -0,0 +1,48 @@ +# HTTP Checker + +`type: http` 用于 HTTP/HTTPS 应用层健康检查。 + +## 配置项 + +| 字段 | 说明 | 必填 | 默认值 | +| ------------------- | ------------------- | ---- | ------- | +| `http.url` | 目标 URL | 是 | 无 | +| `http.method` | HTTP 方法 | 否 | `GET` | +| `http.headers` | 请求头 | 否 | 无 | +| `http.body` | 请求体 | 否 | 无 | +| `http.ignoreSSL` | 忽略 HTTPS 证书校验 | 否 | `false` | +| `http.maxRedirects` | 最大重定向跟随次数 | 否 | `0` | + +## expect 校验项 + +| 字段 | 说明 | 必填 | 默认值 | +| ------------ | -------------------------------------------------- | ---- | ------- | +| `status` | 可接受的状态码列表,支持精确码和范围(如 `"2xx"`) | 否 | `[200]` | +| `headers` | 响应头校验,使用 `KeyedExpectations` | 否 | 无 | +| `body` | 响应体校验,使用 `ContentExpectations` 数组 | 否 | 无 | +| `durationMs` | 完整执行耗时校验,使用 `ValueMatcher` | 否 | 无 | + +## 示例 + +```yaml +- id: "json-api" + name: "JSON API 示例" + type: http + http: + url: "https://httpbin.org/json" + headers: + Authorization: "Bearer token" + expect: + status: [200] + headers: + Content-Type: + contains: "application/json" + body: + - json: + path: "$.slideshow.title" + equals: "Sample Slide Show" + durationMs: + lte: 10000 +``` + +HTTP checker 的 `durationMs` 覆盖完整执行,包括重定向、按需响应体读取、解码和 expect 校验。未配置 body expectation、status 失败或 headers 失败时不会读取 body。 diff --git a/docs/user/checkers/icmp.md b/docs/user/checkers/icmp.md new file mode 100644 index 0000000..1d616c0 --- /dev/null +++ b/docs/user/checkers/icmp.md @@ -0,0 +1,45 @@ +# ICMP Checker + +`type: icmp` 使用系统 `ping` 命令执行 ICMP 探测,支持 Linux、macOS 和 Windows 输出解析。 + +## 配置项 + +| 字段 | 说明 | 必填 | 默认值 | +| ----------------- | ------------------------- | ---- | ------ | +| `icmp.host` | 目标主机地址 | 是 | 无 | +| `icmp.count` | ICMP 包数量,范围 `1-100` | 否 | `3` | +| `icmp.packetSize` | ICMP 包大小,bytes | 否 | `56` | + +## expect 校验项 + +| 字段 | 说明 | 必填 | 默认值 | +| ------------------- | --------------------------------------------------- | ---- | ------ | +| `alive` | 期望主机可达性 | 否 | `true` | +| `packetLossPercent` | 丢包率百分比校验,范围 `0-100`,使用 `ValueMatcher` | 否 | 无 | +| `avgLatencyMs` | 平均延迟校验,使用 `ValueMatcher` | 否 | 无 | +| `maxLatencyMs` | 最大单次延迟校验,使用 `ValueMatcher` | 否 | 无 | +| `durationMs` | 完整执行耗时校验,使用 `ValueMatcher` | 否 | 无 | + +## 示例 + +```yaml +- id: "gateway-icmp" + name: "网关 ICMP 可达" + type: icmp + icmp: + host: "10.0.0.1" + count: 3 + packetSize: 56 + expect: + alive: true + packetLossPercent: + lte: 10 + avgLatencyMs: + lte: 100 + maxLatencyMs: + lte: 300 + durationMs: + lte: 5000 +``` + +容器中运行 ICMP checker 通常需要 `--cap-add=NET_RAW`,详情见 [部署文档](../deployment.md#icmp-权限)。 diff --git a/docs/user/checkers/llm.md b/docs/user/checkers/llm.md new file mode 100644 index 0000000..400462c --- /dev/null +++ b/docs/user/checkers/llm.md @@ -0,0 +1,53 @@ +# LLM Checker + +`type: llm` 用于大模型服务应用层健康检查。 + +## 配置项 + +| 字段 | 说明 | 必填 | 默认值 | +| --------------------- | ----------------------------------------------------- | ---- | ------- | +| `llm.provider` | 模型提供方:`openai`、`openai-responses`、`anthropic` | 是 | 无 | +| `llm.url` | API base URL | 是 | 无 | +| `llm.model` | 模型名称 | 是 | 无 | +| `llm.prompt` | 单轮 prompt | 是 | 无 | +| `llm.mode` | 调用模式:`http` 或 `stream` | 否 | `http` | +| `llm.key` | API key,支持 `${VAR}` 变量替换 | 否 | `""` | +| `llm.authToken` | Bearer token,仅 `anthropic` provider,与 `key` 互斥 | 否 | 无 | +| `llm.headers` | 附加请求头 | 否 | 无 | +| `llm.ignoreSSL` | 忽略 HTTPS 证书校验 | 否 | `false` | +| `llm.options` | 生成选项 | 否 | 无 | +| `llm.providerOptions` | Provider 专属选项 | 否 | 无 | + +`llm.options` 支持 `maxOutputTokens`(默认 `16`)、`temperature`(默认 `0`)、`topP`、`topK`、`presencePenalty`、`frequencyPenalty`、`stopSequences`、`seed`。 + +## expect 校验项 + +| 字段 | 说明 | 必填 | 默认值 | +| ----------------- | --------------------------------------------------------------------------- | ---- | ------- | +| `status` | 可接受的状态码列表,支持精确码和范围(如 `"2xx"`) | 否 | `[200]` | +| `headers` | 响应头校验,使用 `KeyedExpectations` | 否 | 无 | +| `output` | 模型输出校验,使用 `ContentExpectations` 数组 | 否 | 无 | +| `finishReason` | finish reason 校验,使用 `ValueMatcher` | 否 | 无 | +| `rawFinishReason` | 原始 finish reason 校验,使用 `ValueMatcher` | 否 | 无 | +| `usage` | Token usage 校验,支持 `inputTokens`、`outputTokens`、`totalTokens` matcher | 否 | 无 | +| `stream` | 流式断言,支持 `completed`、`firstTokenMs` matcher,仅 `mode: stream` | 否 | 无 | +| `durationMs` | 完整执行耗时校验,使用 `ValueMatcher` | 否 | 无 | + +## 示例 + +```yaml +- id: "llm-openai-probe" + name: "OpenAI 健康检查" + type: llm + llm: + provider: openai + url: "https://api.openai.com/v1" + model: "gpt-4o-mini" + prompt: "Say OK" + key: "${OPENAI_API_KEY}" + expect: + status: [200] + finishReason: "stop" + output: + - contains: "OK" +``` diff --git a/docs/user/checkers/tcp.md b/docs/user/checkers/tcp.md new file mode 100644 index 0000000..a684852 --- /dev/null +++ b/docs/user/checkers/tcp.md @@ -0,0 +1,35 @@ +# TCP Checker + +`type: tcp` 用于 TCP 端口可达性和可选 banner 探测。 + +## 配置项 + +| 字段 | 说明 | 必填 | 默认值 | +| ----------------------- | --------------------------------------------- | ---- | ------- | +| `tcp.host` | 目标主机地址 | 是 | 无 | +| `tcp.port` | 目标端口,范围 `1-65535` | 是 | 无 | +| `tcp.readBanner` | 是否读取服务端 banner | 否 | `false` | +| `tcp.bannerReadTimeout` | banner 读取超时,毫秒 | 否 | `2000` | +| `tcp.maxBannerBytes` | banner 最大字节数,支持 `KB`、`MB`、`GB` 单位 | 否 | `4KB` | + +## expect 校验项 + +| 字段 | 说明 | 必填 | 默认值 | +| ------------ | ------------------------------------------------------------------------- | ---- | ------ | +| `connected` | 期望连接结果,`true` 可达或 `false` 期望不可达 | 否 | `true` | +| `banner` | Banner 内容校验,使用 `ContentExpectations` 数组,需开启 `tcp.readBanner` | 否 | 无 | +| `durationMs` | 完整执行耗时校验,使用 `ValueMatcher` | 否 | 无 | + +## 示例 + +```yaml +- id: "redis-port" + name: "Redis 端口可达" + type: tcp + tcp: + host: "127.0.0.1" + port: 6379 + expect: + durationMs: + lte: 3000 +``` diff --git a/docs/user/checkers/udp.md b/docs/user/checkers/udp.md new file mode 100644 index 0000000..c6bf9a9 --- /dev/null +++ b/docs/user/checkers/udp.md @@ -0,0 +1,43 @@ +# UDP Checker + +`type: udp` 用于 UDP payload 请求-响应检查。 + +## 配置项 + +| 字段 | 说明 | 必填 | 默认值 | +| ---------------------- | ------------------------------------------ | ---- | ------ | +| `udp.host` | 目标主机地址 | 是 | 无 | +| `udp.port` | 目标端口,范围 `1-65535` | 是 | 无 | +| `udp.payload` | 发送数据 | 否 | `""` | +| `udp.encoding` | payload 编码:`text`、`hex`、`base64` | 否 | `text` | +| `udp.responseEncoding` | 响应解码:`text`、`hex`、`base64` | 否 | `text` | +| `udp.maxResponseBytes` | 响应最大字节数,支持 `KB`、`MB`、`GB` 单位 | 否 | `4KB` | + +## expect 校验项 + +| 字段 | 说明 | 必填 | 默认值 | +| -------------- | --------------------------------------------- | ---- | ------ | +| `responded` | 期望是否收到响应 | 否 | `true` | +| `response` | 响应内容校验,使用 `ContentExpectations` 数组 | 否 | 无 | +| `responseSize` | 响应字节数校验,使用 `ValueMatcher` | 否 | 无 | +| `sourceHost` | 响应来源地址校验,使用 `ValueMatcher` | 否 | 无 | +| `sourcePort` | 响应来源端口校验,使用 `ValueMatcher` | 否 | 无 | +| `durationMs` | 完整执行耗时校验,使用 `ValueMatcher` | 否 | 无 | + +## 示例 + +```yaml +- id: "udp-heartbeat" + name: "UDP 心跳检测" + type: udp + udp: + host: "127.0.0.1" + port: 9000 + payload: "PING" + expect: + responded: true + response: + - contains: "PONG" + durationMs: + lte: 100 +``` diff --git a/docs/user/configuration.md b/docs/user/configuration.md new file mode 100644 index 0000000..228cabb --- /dev/null +++ b/docs/user/configuration.md @@ -0,0 +1,135 @@ +# 配置文件 + +DiAL 通过 YAML 配置文件定义运行参数和拨测目标。完整可运行示例参见 [`../../probes.example.yaml`](../../probes.example.yaml)。配置 JSON Schema 位于 [`../../probe-config.schema.json`](../../probe-config.schema.json)。 + +## 配置结构 + +```yaml +# yaml-language-server: $schema=./probe-config.schema.json + +server: + listen: + host: "127.0.0.1" + port: "${server_port}" + storage: + dataDir: "/tmp/probes_data" + retention: "${retention}" + logging: + level: "${log_level|info}" + file: + path: "/logs/dial.log" + +probes: + execution: + maxConcurrentChecks: "${max_checks}" + +variables: + server_port: 3000 + retention: "7d" + max_checks: 20 + default_interval: "30s" + default_timeout: "10s" + +targets: + - id: "baidu-home" + name: "Baidu" + type: http + interval: "${default_interval}" + timeout: "${default_timeout}" + http: + url: "https://www.baidu.com" + expect: + status: [200] +``` + +## server.listen + +| 字段 | 说明 | 必填 | 默认值 | +| ------ | -------- | ---- | ----------- | +| `host` | 监听地址 | 否 | `127.0.0.1` | +| `port` | 监听端口 | 否 | `3000` | + +## server.storage + +| 字段 | 说明 | 必填 | 默认值 | +| ----------- | ---------------------------------------------------- | ---- | -------- | +| `dataDir` | 数据目录,相对路径基于配置文件所在目录解析 | 否 | `./data` | +| `retention` | 历史数据保留时长,支持 `ms`、`s`、`m`、`h`、`d` 单位 | 否 | `7d` | + +## probes.execution + +| 字段 | 说明 | 必填 | 默认值 | +| --------------------- | -------------- | ---- | ------ | +| `maxConcurrentChecks` | 最大并发拨测数 | 否 | `20` | + +## server.logging + +| 字段 | 说明 | 必填 | 默认值 | +| ---------------------------------------- | ---------------------------------------------- | ---- | ------------------------- | +| `server.logging.level` | 全局日志等级,console 和 file 未指定时继承此值 | 否 | `info` | +| `server.logging.console.level` | 控制台日志等级 | 否 | 继承 `level` | +| `server.logging.file.level` | 文件日志等级 | 否 | 继承 `level` | +| `server.logging.file.path` | 日志文件路径,相对路径基于配置文件目录解析 | 否 | `/logs/dial.log` | +| `server.logging.file.rotation.size` | 按大小滚动,支持 `KB`、`MB`、`GB` 单位 | 否 | `50MB` | +| `server.logging.file.rotation.frequency` | 按时间滚动:`hourly`、`daily`、`weekly` | 否 | `daily` | +| `server.logging.file.rotation.maxFiles` | 保留的归档文件数量,不含活跃日志 | 否 | `14` | + +日志等级支持:`trace`、`debug`、`info`、`warn`、`error`、`fatal`。 + +控制台始终输出 pretty 格式,文件始终输出 JSONL 格式并支持滚动。`rotation.size` 和 `rotation.frequency` 任一条件触发即滚动。 + +## 内置默认值 + +| 字段 | 默认值 | +| ---------- | ------ | +| `interval` | `30s` | +| `timeout` | `10s` | + +各 checker 专属默认值见 [Checker 参考](checkers/README.md)。 + +## variables + +`variables` 是顶层动态键值表,key 必须符合 `[a-zA-Z_][a-zA-Z0-9_]*`,value 仅支持 string、number、boolean。`server`、`probes` 和 `targets` 中的字符串值可引用变量。 + +| 语法 | 说明 | +| --------- | ------------------------- | ------------------------------------------ | +| `${key}` | 引用 variables 或环境变量 | +| `${key | default}` | variables 和环境变量都不存在时使用默认值 | +| `${key | }` | variables 和环境变量都不存在时使用空字符串 | +| `$${key}` | 转义输出字面量 `${key}` | + +解析优先级为 `variables -> process.env -> 默认值`。三者均不存在时配置校验失败。字段值完整等于单个变量引用时会保留 number、boolean、string 类型;部分拼接时统一转为字符串。 + +变量替换作用于 `server`、`probes` 和 `targets`,不作用于 `variables` 段自身,且不会替换 `targets[].id` 和 `targets[].type` 字段;对象 key 不参与替换。 + +## 配置加载形态 + +配置加载内部区分三层形态: + +| 形态 | 说明 | +| ----------------- | ------------------------------------------------------------------------------------- | +| Authoring Config | 用户 YAML 可书写形态,允许变量引用和 expect 简写 | +| Normalized Config | `normalizeAuthoringConfig()` 完成变量替换、expect 简写展开并移除 `variables` 后的形态 | +| ResolvedConfig | checker `resolve()` 补默认值并解析 duration、size、路径和运行期环境后的形态 | + +根目录 `probe-config.schema.json` 面向 Authoring Config,因此 VSCode 校验会接受 `server.listen.port: "${server_port|3000}"`、`http.maxRedirects: "${MAX|5}"` 和 `expect.durationMs: 5000` 这类写法。 + +## targets 通用字段 + +| 字段 | 说明 | 必填 | 默认值 | +| ------------- | ------------------------------------------------------------------------------------ | ---- | --------- | +| `id` | 目标唯一标识,最长 30 字符,支持字母数字、下划线、连字符,不参与变量替换 | 是 | 无 | +| `name` | 展示名称,最长 30 字符,支持变量替换,可省略或显式 null;前端展示时 null 回退到 `id` | 否 | 无 | +| `description` | 目标描述,最长 500 字符,支持变量替换,可省略或显式 null,允许空字符串 | 否 | 无 | +| `type` | 目标类型:`http`、`cmd`、`db`、`tcp`、`udp`、`dns`、`icmp`、`llm` | 是 | 无 | +| `group` | 分组名称 | 否 | `default` | +| `interval` | 拨测间隔 | 否 | `30s` | +| `timeout` | 超时时间 | 否 | `10s` | + +## Checker 专属配置 + +每个 target 必须根据 `type` 配置对应的 checker 专属字段。详情见 [Checker 参考](checkers/README.md)。 + +## 校验规则 + +`expect` 字段按 checker 类型不同而变化。通用断言模型见 [校验规则](expectations.md)。 diff --git a/docs/user/deployment.md b/docs/user/deployment.md new file mode 100644 index 0000000..c6d93a4 --- /dev/null +++ b/docs/user/deployment.md @@ -0,0 +1,109 @@ +# 部署 + +本文档说明如何构建、运行、容器化和发布 DiAL。开发环境运行见 [README 快速开始](../../README.md#快速开始)。 + +## 生产构建和运行 + +```bash +bun run build +./dist/dial-server ./probes.yaml +``` + +构建产物为独立可执行文件,只需要一个 YAML 配置文件即可运行。 + +启动后: + +| 地址 | 行为 | +| ------------------------------ | ------------------ | +| `http://127.0.0.1:3000/` | 返回前端 Dashboard | +| `http://127.0.0.1:3000/api/*` | 返回后端 API | +| `http://127.0.0.1:3000/health` | 返回健康检查 | + +## Docker 部署 + +DiAL 提供基于 Alpine 的多阶段镜像。构建阶段使用 Bun 生成 musl 目标单可执行文件,运行阶段只包含 `dial-server`、基础证书、`ping`、Bun musl executable 必需运行库、时区数据和容器运行目录。 + +```bash +docker build -t dial:alpine . +docker run --rm -p 3000:3000 -v dial-data:/data/dial dial:alpine +``` + +容器默认读取 `/etc/dial/probes.yaml`,推荐将数据卷挂载到 `/data/dial`。 + +使用自定义配置文件: + +```bash +docker run --rm -p 3000:3000 \ + -v "$PWD/docker/probes.yaml:/etc/dial/probes.yaml:ro" \ + -v dial-data:/data/dial \ + dial:alpine +``` + +容器专用示例配置位于 [`../../docker/probes.yaml`](../../docker/probes.yaml),默认监听 `0.0.0.0:3000`,并将 SQLite 数据和日志写入 `/data/dial`。 + +## ICMP 权限 + +如需在容器中运行 ICMP checker,除镜像内置 `iputils-ping` 外,还需要授予 `NET_RAW` capability: + +```bash +docker run --rm --cap-add=NET_RAW -p 3000:3000 -v dial-data:/data/dial dial:alpine +``` + +## CMD checker 额外命令 + +官方镜像不内置 `bun`、`node`、`curl`、`dig`、`psql`、`mysql`、`redis-cli` 等 CMD checker 可能需要的额外命令。需要这些命令时请使用派生镜像自行安装: + +```dockerfile +FROM dial:alpine + +USER root +RUN apk add --no-cache curl bind-tools postgresql-client +USER dial +``` + +## 多架构镜像 + +```bash +docker buildx build --platform linux/amd64,linux/arm64 -t dial:alpine . +``` + +Dockerfile 通过 Docker 提供的 `TARGETARCH` 选择 Bun compile target。 + +| `TARGETARCH` | `BUN_TARGET` | +| ------------ | ---------------------- | +| `amd64` | `bun-linux-x64-musl` | +| `arm64` | `bun-linux-arm64-musl` | + +## 跨平台发布包 + +```bash +bun run release +bun run release --target linux-x64 +bun run release --target linux-x64,windows-x64,darwin-arm64 +``` + +支持的目标平台: + +| CLI 参数 | Bun CompileTarget | +| ------------------ | ---------------------- | +| `linux-x64` | `bun-linux-x64` | +| `linux-arm64` | `bun-linux-arm64` | +| `linux-x64-musl` | `bun-linux-x64-musl` | +| `linux-arm64-musl` | `bun-linux-arm64-musl` | +| `windows-x64` | `bun-windows-x64` | +| `darwin-x64` | `bun-darwin-x64` | +| `darwin-arm64` | `bun-darwin-arm64` | + +产出物结构: + +```text +dist/release/ +├── binaries/ +│ ├── dial-server-0.1.0-linux-x64 +│ └── dial-server-0.1.0-windows-x64.exe +└── packages/ + ├── dial-server_0.1.0_linux_x64.tar.gz + └── dial-server_0.1.0_linux_x64.tar.gz.sha256 +``` + +压缩包内含可执行文件、`probes.example.yaml` 和 `LICENSE`,解压后可直接使用。 diff --git a/docs/user/expectations.md b/docs/user/expectations.md new file mode 100644 index 0000000..4ae63d7 --- /dev/null +++ b/docs/user/expectations.md @@ -0,0 +1,104 @@ +# 校验规则 + +`expect` 描述拨测结果必须满足的条件。不同 checker 暴露不同字段,但共享三类基础断言模型:`ValueMatcher`、`ContentExpectations` 和 `KeyedExpectations`。 + +## ContentExpectations + +`body`、`stdout`、`stderr`、`banner`、`response`、`output`、`result` 等返回内容字段均使用数组。 + +| 规则 | 说明 | +| ---------- | ------------------------------------------------------ | +| `contains` | 内容包含指定文本 | +| `regex` | 正则匹配,启动期会拒绝存在 ReDoS 风险的模式 | +| `json` | JSONPath 提取值比较,`path` 必填 | +| `css` | CSS 选择器提取 HTML 元素,`selector` 必填,`attr` 可选 | +| `xpath` | XPath 提取 XML/HTML 节点,`path` 必填 | + +示例: + +```yaml +expect: + body: + - contains: "ok" + - json: + path: "$.status" + equals: "ready" +``` + +ContentExpectations 数组按顺序快速失败。数组项可以是直接 matcher,也可以是 `json`、`css`、`xpath` 提取器规则。一条规则不能混用直接 matcher 和 extractor,多个 extractor 也不能共存。Extractor 未配置 matcher 时等价于 `exists: true`。 + +## ValueMatcher + +`ValueMatcher` 用于单个标量值、数字指标和字符串元数据。 + +| 字段 | 说明 | +| ---------- | ------------------------------- | +| `equals` | 精确匹配,支持 JSON 深度相等 | +| `contains` | 字符串包含 | +| `regex` | 正则匹配,固定使用无 flags 正则 | +| `empty` | 判断是否为空 | +| `exists` | 判断是否存在 | +| `gte` | 大于等于 | +| `lte` | 小于等于 | +| `gt` | 大于 | +| `lt` | 小于 | + +一个 matcher 对象内多个字段为 AND 语义。`exists: false` 不能和其他 matcher 组合。 + +ValueMatcher expect 字段可直接写 string、number、boolean 或 null,等价于 `{ equals: value }`。数组和对象必须显式写成 `{ equals: ... }`。 + +```yaml +expect: + durationMs: + lte: 5000 + finishReason: "stop" +``` + +## KeyedExpectations + +`headers`、DB `rows[]` 中的列值等动态键值对象使用 `KeyedExpectations`。每个键的值支持 `ValueMatcher` 的全部字段,字面量值自动等价于 `{ equals: value }`。 + +```yaml +expect: + headers: + Content-Type: + contains: "application/json" +``` + +## 大小和时长格式 + +| 类型 | 示例 | +| ---- | -------------------------------- | +| 大小 | `4KB`、`10MB`、`1GB`、直接数字 | +| 时长 | `500ms`、`30s`、`5m`、`2h`、`7d` | + +`maxBodyBytes`、`maxOutputBytes`、`maxResponseBytes`、`maxBannerBytes` 等大小字段支持 `KB`、`MB`、`GB` 单位。 + +## 快速失败顺序 + +| 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` | +| DNS system | `values -> valueCount -> durationMs` | +| DNS server | `responded -> rcode -> values -> valueCount -> answerCount -> ttlMin -> ttlMax -> authoritative -> recursionAvailable -> truncated -> authenticatedData -> result -> durationMs` | +| LLM http | `status -> headers -> output -> finishReason -> rawFinishReason -> usage -> durationMs` | +| LLM stream | `status -> headers -> stream.completed -> stream.firstTokenMs -> output -> finishReason -> rawFinishReason -> usage -> durationMs` | + +## JSON Schema + +仓库根目录导出 `probe-config.schema.json`。在 YAML 文件顶部添加以下注释可在编辑器中获得提示和校验: + +```yaml +# yaml-language-server: $schema=./probe-config.schema.json +``` + +## 已移除字段 + +旧字段 `maxDurationMs`、`maxPacketLoss`、`maxAvgLatencyMs`、`maxMaxLatencyMs` 和旧正则字段 `match` 已移除,请分别改用 `durationMs`、ICMP matcher 字段和 `regex`。 + +非法配置会阻止启动并输出错误信息。除动态键值表(`headers`、`env`、`variables`)外,未知字段会导致启动失败,请使用 YAML 注释表达说明。 diff --git a/docs/user/status-model.md b/docs/user/status-model.md new file mode 100644 index 0000000..b71d119 --- /dev/null +++ b/docs/user/status-model.md @@ -0,0 +1,48 @@ +# 目标状态判定 + +DiAL 使用单层状态模型。 + +| 状态 | 含义 | +| ------ | ---------------------------------------- | +| `UP` | 拨测结果符合 `expect` 规则 | +| `DOWN` | 拨测结果不符合 `expect` 规则,或执行失败 | + +执行失败(网络错误、超时、进程崩溃)和 expect 不匹配都统一为 `DOWN`,通过 `failure.kind` 区分原因。 + +| `failure.kind` | 含义 | +| -------------- | ---------------------------------------- | +| `error` | 网络、超时、进程、协议解析或内部执行错误 | +| `mismatch` | 拨测完成,但结果不满足 expect | + +## API 结果字段 + +API 返回的检查结果包含 `detail` 和 `observation`。 + +| 字段 | 说明 | +| ------------- | ------------------------------------------------------------ | +| `detail` | 后端按 checker 类型从结构化 observation 动态生成的人可读摘要 | +| `observation` | 保存该次检查的结构化观测数据 | +| `failure` | 保存首个错误或不匹配原因 | +| `matched` | 是否符合 expect | +| `durationMs` | 本次检查耗时 | +| `timestamp` | 本次检查时间 | + +`detail` 不写入 SQLite。存储层仅持久化 `observation` JSON、`failure` JSON、匹配状态、耗时和时间戳。 + +## observation 示例 + +不同 checker 的 observation 字段不同,常见信息包括: + +| Checker | observation 内容示例 | +| ------- | ------------------------------------------------------------------ | +| HTTP | 状态码、响应头、按需读取的 body 预览 | +| Cmd | exit code、stdout/stderr 预览 | +| TCP | 连接结果、banner 摘要 | +| UDP | 响应内容、来源地址、响应大小 | +| ICMP | 存活结果、丢包率、平均延迟、最大延迟 | +| DNS | RCODE、记录值、TTL、flags、CNAME 链 | +| LLM | HTTP 状态、模型输出、finish reason、token usage、流式首 token 时间 | + +## 趋势与统计 + +Dashboard 基于存储的检查结果计算实时状态、可用率、耗时趋势、P95、状态条和故障段等指标。指标语义由后端应用层实现,SQLite 主要负责存储、筛选、排序、分页和基础聚合。 diff --git a/docs/user/troubleshooting.md b/docs/user/troubleshooting.md new file mode 100644 index 0000000..7968693 --- /dev/null +++ b/docs/user/troubleshooting.md @@ -0,0 +1,73 @@ +# 故障排查 + +本文档记录常见运行问题和排查入口。 + +## 配置校验失败 + +DiAL 启动时会校验 YAML 配置。除动态键值表(`headers`、`env`、`variables`)外,未知字段会导致启动失败。 + +排查顺序: + +1. 在 YAML 顶部添加 `# yaml-language-server: $schema=./probe-config.schema.json`。 +2. 对照 [配置文件](configuration.md) 检查顶层结构和通用字段。 +3. 对照 [Checker 参考](checkers/README.md) 检查 checker 专属字段。 +4. 对照 [校验规则](expectations.md) 检查 expect 写法。 + +## 变量无法解析 + +变量解析优先级为 `variables -> process.env -> 默认值`。如果三者均不存在,配置校验会失败。 + +常见修复: + +| 问题 | 修复 | +| -------------- | ----------------------------------- | --------- | +| 环境变量未设置 | 设置环境变量或在 `variables` 中声明 | +| 希望允许空值 | 使用 `${key | }` | +| 希望提供默认值 | 使用 `${key | default}` | +| 希望输出字面量 | 使用 `$${key}` | + +## ICMP checker 无法运行 + +ICMP checker 依赖系统 `ping` 命令。 + +| 环境 | 处理 | +| ------------------- | -------------------------------------- | +| Alpine 或精简镜像 | 安装 `iputils-ping` | +| Docker 容器 | 运行容器时增加 `--cap-add=NET_RAW` | +| Windows/macOS/Linux | 确认系统 `ping` 可执行且输出格式受支持 | + +Docker 示例: + +```bash +docker run --rm --cap-add=NET_RAW -p 3000:3000 -v dial-data:/data/dial dial:alpine +``` + +## CMD checker 找不到命令 + +官方 Docker 镜像不内置 `bun`、`node`、`curl`、`dig`、`psql`、`mysql`、`redis-cli` 等额外命令。需要这些命令时请使用派生镜像安装。 + +```dockerfile +FROM dial:alpine + +USER root +RUN apk add --no-cache curl bind-tools postgresql-client +USER dial +``` + +## Docker 数据或日志丢失 + +推荐将数据卷挂载到 `/data/dial`,并在配置中使用该目录作为 storage dataDir。 + +```bash +docker run --rm -p 3000:3000 -v dial-data:/data/dial dial:alpine +``` + +容器示例配置位于 [`../../docker/probes.yaml`](../../docker/probes.yaml)。 + +## HTTP 或 LLM 证书问题 + +HTTP 和 LLM checker 支持 `ignoreSSL`。该选项适合内网、自签名证书或测试环境;生产环境应优先修复证书链。 + +## 正则规则被拒绝 + +`regex` 启动期会执行 ReDoS 风险检测。被拒绝时应改写为更明确、回溯风险更低的表达式。 diff --git a/openspec/config.yaml b/openspec/config.yaml index f2e8ff1..11813c3 100644 --- a/openspec/config.yaml +++ b/openspec/config.yaml @@ -3,8 +3,15 @@ schema: fast-drive context: | - 使用中文(注释、文档、交流),面向中文开发者 - openspec文档的关键字按openspec规范使用,不要翻译为中文 - - **优先阅读README.md和DEVELOPMENT.md**获取项目概览与开发规范,所有代码风格、命名、注解、依赖、API等规范以DEVELOPMENT.md为准 - - 涉及模块结构、API、实体等变更时同步更新README.md + - **优先阅读docs/README.md**判断文档归属和本次任务需要读取的专题文档 + - README.md用于项目概览、快速开始和用户入口;DEVELOPMENT.md用于开发入口、全局规则和质量门禁;CONTRIBUTING.md仅在新增或修改checker、贡献流程时必读 + - 所有代码风格、命名、注解、依赖、API等开发规范以DEVELOPMENT.md和docs/development/下对应专题文档为准 + - 新增或修改checker时必须阅读CONTRIBUTING.md和docs/development/checker-development.md + - 每次代码变更都必须执行文档影响分析:判断是否影响用户可见行为、配置格式、checker行为、expect规则、API、部署方式、开发流程、架构边界、测试规范或构建发布流程 + - 若影响用户使用方式、配置格式、checker行为、expect规则、部署方式或运行行为,必须同步更新docs/user/下对应文档;README.md仅在项目定位、快速开始、核心能力列表或文档导航变化时更新 + - 若影响开发流程、架构边界、质量门禁、测试规范、构建发布流程或checker开发机制,必须同步更新DEVELOPMENT.md、CONTRIBUTING.md或docs/development/下对应文档 + - 若影响文档同步规则或文档归属矩阵,必须同步更新docs/README.md和openspec/config.yaml + - 若无需更新文档,必须在收尾说明中说明原因 - 新增代码优先复用已有组件、工具、依赖库,不引入新依赖 - 新增的逻辑必须编写完善的测试,并保证测试的正确性,不允许跳过任何测试 - 这是基于bun实现的前端后一体化项目,使用bun作为唯一包管理器,严禁使用pnpm、npm,使用bunx运行工具,严禁使用npx、pnpx @@ -20,10 +27,14 @@ context: | - (当前项目未上线,不需要考虑向前兼容) rules: + explore: + - 本项目openspec使用fast-drive自定义schema,变更文档只包含design.md和tasks.md,无proposal.md和specs design: - 先前的讨论技术方案要尽可能体现在设计文档中,便于指导实现阶段不偏离已定的技术路线 tasks: - 一行一个任务,严禁任务内容跨行 - 如果是代码存在更新必须 - 执行完整的测试、代码检查、格式检查等质量保障手段 - - 更新 README.md 和/或 DEVELOPMENT.md + - 执行文档影响分析,并按影响范围更新对应文档;若无需更新文档,必须在任务或收尾说明中明确写出原因 + - 新增或修改checker时必须更新docs/user/checkers/下对应用户文档,并在checker开发机制变化时更新CONTRIBUTING.md或docs/development/checker-development.md + - 新增或修改配置字段时必须更新probe-config.schema.json、probes.example.yaml、docs/user/configuration.md或对应checker文档