1
0

feat: 结构化 observation 替代 statusDetail,API 层动态构造 detail

- CheckResult: statusDetail -> observation (持久化) + detail (API 动态派生)
- 存储: status_detail 列 -> observation TEXT (JSON)
- CheckerDefinition: 新增 buildDetail(observation) 方法
- 各 checker 返回结构化 observation,API 层通过 registry 调用 buildDetail
- HTTP: bodyPreview 在 status/header 失败时也提前采集
- UDP: observation 包含 durationMs,未响应归为 error failure
- CMD: 超时/输出超限时保留已收集 observation
- TCP: connectTimeMs 仅含连接建立耗时,不含 banner 等待
- 新增 buildDetail 单测和 mapCheckResult 覆盖测试
- 同步 openspec 主规范,归档 checker-observation 变更
This commit is contained in:
2026-05-19 22:49:00 +08:00
parent 22c06820fa
commit 375dd3492b
64 changed files with 915 additions and 965 deletions

View File

@@ -1,2 +0,0 @@
schema: spec-driven
created: 2026-05-19

View File

@@ -1,159 +0,0 @@
## Context
当前所有 checkerHTTP/TCP/UDP/ICMP/DB/CMD/LLM在 execute 执行后,将丰富的结构化观测数据压缩为一个 `statusDetail: string | null` 字符串,然后丢弃原始数据。`CheckResult` 仅有 5 个字段durationMs、failure、matched、statusDetail、timestamp无法承载排障所需的上下文信息也无法支持历史观测数据的结构化分析。
现有代码中LLM checker 已有执行期 `LlmCheckObservation` 对象且 `expect.output` 依赖其中的完整 `outputText`ICMP/Ping checker 已有 `PingStats` 结构,其余 checker 的观测数据散布在 execute 函数的局部变量中。
前端通过 `statusDetail` 字符串直接展示摘要,仅在 `history-table-columns.tsx``OverviewTab.tsx` 两处引用。
项目未上线,不需要考虑向前兼容和数据迁移。
现有 `openspec/specs/probe-data-store/spec.md``check_results.target_id` 仍写为 INTEGER但当前代码中的 `targets.id``check_results.target_id` 均为 TEXT。本 change 的 `probe-data-store` delta 在替换 status_detail 时同步修正该过时 spec 描述,不引入额外代码类型变更。
## Goals / Non-Goals
**Goals:**
- 在 CheckResult 中引入结构化 observation 字段,持久化完整观测数据
-`detail` 字段替代 `statusDetail`,从 observation 动态构造,不再持久化
- 各 checker 定义各自的 observation schema 和 detail 构造逻辑
- 对大文本/集合字段执行截断,控制存储膨胀
- 前端展示行为不变(仍显示人可读摘要字符串)
**Non-Goals:**
- 前端 observation 详情展开 UI后续单独做
- observation 的 JSON 深层查询(不需要 json_extract 等)
- 数据脱敏(仅截断,不脱敏)
- 引入新的序列化依赖(使用原生 JSON
## Decisions
### Decision 1: observation 字段类型使用 `Record<string, unknown> | null`
**选择**: 使用 `Record<string, unknown>` 作为 observation 的共享类型,各 checker 内部定义强类型 interface 用于构造,序列化后类型擦除。
**替代方案**:
- Discriminated union`{ type: "http", ... } | { type: "tcp", ... }`):类型安全但导致共享层与所有 checker 类型耦合,每新增一个 checker 都需要修改共享类型
- `unknown`:过于宽松,缺乏结构约束
**理由**: 折中方案。共享层不关心 observation 的内部结构(只需序列化/反序列化),类型安全由各 checker 内部保证。buildDetail 方法接收 `Record<string, unknown>` 并在内部做类型断言。
### Decision 2: 存储格式使用 JSON TEXT
**选择**: observation 列使用 SQLite TEXT 类型JSON.stringify 写入JSON.parse 读出。
**替代方案**:
- BLOB + MessagePack更紧凑但需要引入 `@msgpack/msgpack` 依赖
- BLOB + JSON Buffer不引入依赖但 sqlite3 CLI 无法直接查看内容
**理由**: 不引入新依赖(项目规范);开发者可以用 sqlite3 CLI 直接查看和调试 observation 数据。空间换可读性,实际膨胀有限(截断策略控制了上界)。
### Decision 3: detail 在 API 序列化层动态构造
**选择**: detail 不持久化到数据库,在 API 路由的 `mapCheckResult` 中通过 `checkerRegistry.get(type).buildDetail(observation)` 动态生成。
**替代方案**:
- 在 execute 中同时生成 detail 并持久化:简单但存储冗余数据,且 detail 格式变更需要回填
- 在前端从 observation 构造:前端需要了解所有 checker 类型的 detail 格式,违反关注点分离
**理由**: detail 是 observation 的派生数据不应独立存储。API 层已经能获取 target typedashboard 路由遍历 targetshistory 路由已查出 target调用 buildDetail 的成本极低。mapCheckResult 函数签名调整为接收 type 参数。
### Decision 4: statusDetail 重命名为 detail
**选择**: API 合约中 `statusDetail` 字段重命名为 `detail`
**理由**: 更简洁。项目未上线,无兼容性负担。
### Decision 5: CheckerDefinition 接口新增 buildDetail 方法
**选择**: 在 `CheckerDefinition` 接口中新增 `buildDetail(observation: Record<string, unknown>): string | null`
**替代方案**:
- 独立的 detailBuilder registry过度设计且打破 checker 内聚性
- 在 observation 中嵌入 detail 字段:混淆数据和展示
**理由**: buildDetail 是 checker 领域知识的一部分,与 execute/serialize 同属 checker 职责。放在接口中保持 checker 内聚。
### Decision 6: 可收集的负向结果仍写入 observation
**选择**: observation 为 null 仅表示无法形成有意义的领域观测,例如进程 spawn 失败、执行框架内部异常、请求在拿到响应前失败且没有可记录元数据。TCP 连接拒绝、UDP 未收到响应、Ping 不可达、DB 连接失败、CMD 非零退出、HTTP 非 2xx/expect 不匹配、LLM 返回错误状态等可收集上下文的负向结果 SHALL 返回结构化 observation并通过 failure 表示失败原因。
**理由**: observation 的目标是排障和趋势分析。可预期的负向结果本身就是关键观测数据,如果统一写 null 会丢失连接错误、丢包率、stderr、HTTP status/body 等上下文。
### Decision 7: HTTP 成功拿到响应后始终采集 bodyPreview
**选择**: HTTP checker 在 fetch 成功返回 Response 后,无论是否配置 body expect都读取响应体前 1024 字符作为 `bodyPreview`。当配置 body expect 时,仍按现有 `maxBodyBytes` 读取完整可校验范围,并从已读取文本派生 `bodyPreview`
**理由**: 主要排障诉求包含“HTTP 502 返回了什么 body”。如果仅在配置 body expect 时读取 body默认 HTTP 探测无法提供失败响应正文。该行为会增加一次响应体读取成本,但 preview 上限较小,且 HTTP 响应体不会被后续其他逻辑复用。
### Decision 8: 截断策略
各 checker 的截断上限:
| 字段 | 上限 | 说明 |
|------|------|------|
| HTTP bodyPreview | 1024 chars | 错误页面/API 错误体足够排障 |
| HTTP headers | 前 20 个 | 避免大量自定义 header 膨胀 |
| TCP banner | 256 chars | 与现有 truncateBanner 逻辑一致 |
| UDP responsePreview | 512 chars | 协议响应通常较短 |
| DB rowsPreview | 前 5 行 | 验证查询结果形态即可 |
| CMD stdoutPreview | 1024 chars | 错误日志/诊断输出的前段 |
| CMD stderrPreview | 1024 chars | 同上 |
| LLM outputPreview | 512 chars | 输出文本摘要即可 |
| LLM headers | 前 20 个 | 同 HTTP |
### Decision 9: execute 返回值变更策略
各 checker 的 execute 方法改造方式:
- **LLM**: 执行期继续使用包含完整 `outputText``LlmCheckObservation` 支撑 expect 校验;写入 CheckResult.observation 前派生持久化 observation`outputText` 转为 `outputPreview``outputLength`,并截断 HTTP headers。不能直接把执行期 `outputText` 替换为 preview否则会破坏 `expect.output`
- **ICMP**: 已有 `PingStats`,直接作为 observation 基础,补充 error 字段。
- **HTTP/TCP/UDP/DB/CMD**: 在 execute 函数中将散布的局部变量聚合为 observation 对象。现有的 `buildStatusDetail` 辅助函数逻辑迁移到 `buildDetail` 方法中,输入改为 observation。
### Decision 10: 数据流架构
```
Checker.execute()
├─ observation: { statusCode: 200, headers: {...}, ... }
├─ detail: 不设置
├─ matched / failure / durationMs / timestamp
Engine.writeResult()
├─ observation → JSON.stringify → TEXT 列
├─ failure → JSON.stringify → TEXT 列
├─ matched / durationMs / timestamp → 原样写入
API Route (dashboard / history)
├─ 已知 target.type
├─ observation → JSON.parse
├─ detail = checkerRegistry.get(type).buildDetail(obs)
API Response (CheckResult)
├─ observation: { ... } ← 结构化数据,前端可用于未来排障 UI
├─ detail: "HTTP 200" ← 人可读摘要,前端直接展示
├─ matched / failure / durationMs / timestamp
Frontend
├─ 显示 detail与原 statusDetail 行为一致)
└─ observation 暂不使用
```
## Risks / Trade-offs
**[存储膨胀]** → observation JSON 比原 statusDetail 字符串占用更多空间。通过截断策略控制上界:单条 observation 最大约 5-10KB含 CMD stdout + stderr 各 1024 chars远小于 SQLite 单行存储上限。配合已有的 data-retention prune 机制,整体可控。
**[buildDetail 性能]** → 每次 API 请求都需要 JSON.parse + buildDetail 调用。对于 dashboard仅取 latest check per target和 history分页默认 20 条/页),开销极小。如果未来需要批量处理大量记录,可以考虑缓存或批量优化。
**[类型安全断层]** → observation 在共享层是 `Record<string, unknown>`buildDetail 内部需要做类型断言。如果 observation 结构与 buildDetail 期望不一致,会产生运行时错误。通过 checker 内部 execute、持久化 observation 派生函数和 buildDetail 共享同一个 observation interface仅 checker 内部可见)来降低风险。
**[前端字段重命名]** → statusDetail → detail 需要修改前端 2 处引用。变更量小,但需要确保前端编译通过。
**[HTTP body 读取成本]** → HTTP checker 将在拿到响应后读取 body preview即使未配置 body expect。通过 1024 字符 preview 上限控制额外内存占用;如果 body 解码失败,仍应保留 status/headers/contentLength 等已收集 observation并通过 failure 描述解码问题。

View File

@@ -1,37 +0,0 @@
## Why
各 checker 在执行过程中收集了丰富的结构化数据HTTP 状态码/headers/body、ICMP 延迟/丢包率、LLM token 用量/首 token 延迟等),但 `CheckResult` 仅有一个 `statusDetail: string | null` 字段所有观测数据被压缩为人可读字符串后丢弃。这导致排障时无法获取失败上下文HTTP 502 返回了什么 bodyCMD 的 stderr 输出了什么、无法对历史观测数据做结构化查询和趋势分析ICMP 丢包率变化、LLM token 消耗趋势)。
## What Changes
- **BREAKING**: `CheckResult` 移除 `statusDetail` 字段,新增 `detail: string | null``observation: Record<string, unknown> | null`
- **BREAKING**: 存储层 `check_results` 表移除 `status_detail` 列,新增 `observation TEXT`JSON 格式)
- 每个 checker 在 execute 返回时组装结构化 observation 对象,包含该类型特有的观测数据(含截断策略);可收集的负向结果保留 observation仅无法形成领域观测时返回 null
- `CheckerDefinition` 接口新增 `buildDetail(observation)` 方法,从 observation 动态构造人可读摘要
- API 序列化层根据 target type 调用对应 checker 的 `buildDetail`,动态生成 `detail` 字段返回给前端
- 前端展示层将 `statusDetail` 引用改为 `detail`,逻辑不变
## Capabilities
### New Capabilities
- `checker-observation`: 定义 observation 数据模型、各 checker 的 observation schema、截断策略、序列化/反序列化规则
### Modified Capabilities
- `checker-runner-abstraction`: CheckerDefinition 接口新增 `buildDetail` 方法CheckResult 类型变更statusDetail → detail + observation
- `probe-engine`: checker 执行结果写入字段从 status_detail 改为 observationdetail 不进入存储层
- `probe-data-store`: check_results 表 schema 变更status_detail → observationinsert/query 方法适配新字段;同步修正现有 spec 中已过时的 target_id 类型为当前代码实际使用的 TEXT
- `probe-api`: CheckResult API 合约变更statusDetail → detail + observation序列化层需根据 target type 动态构造 detail
- `cmd-checker`: CMD 执行结果改为返回 observationdetail 由 buildDetail 构造
- `tcp-checker`: TCP 执行和 banner 摘要改为通过 observation/detail 表达
- `udp-checker`: UDP 执行和响应摘要改为通过 observation/detail 表达
- `icmp-checker`: Ping/ICMP 摘要改为通过 observation/detail 表达API registry type 仍为 `ping`
- `llm-checker`: LLM 执行期 observation 与持久化 preview 分层,状态摘要改为 detail
- `target-detail-drawer`: 记录面板详情列从 statusDetail 改为 detail
## Impact
- **后端**: 7 个 checker 的 execute/buildDetail 需改造返回 observationLLM 还涉及 types.ts、observation.ts、expect.ts 的执行期/持久化结构分层engine.ts/store.ts/helpers.ts/routes 适配新字段
- **前端**: 2 处源码 statusDetail 引用改为 detailhistory-table-columns.tsx、OverviewTab.tsx相关测试 fixture 同步更新
- **存储**: SQLite DDL 变更不做数据迁移项目未上线target_id 继续使用当前代码实际的 TEXT 类型
- **依赖**: 无新增依赖observation 使用 JSON.stringify/JSON.parse 序列化
- **测试**: 所有涉及 CheckResult、StoredCheckResult、CheckerDefinition mock、API dashboard/history、各 checker execute/buildDetail、前端展示的测试需适配新字段

View File

@@ -1,40 +0,0 @@
## MODIFIED Requirements
### Requirement: Checker 接口定义
系统 SHALL 在 `src/server/checker/runner/types.ts` 中定义面向扩展的泛型 `CheckerDefinition<TResolved extends ResolvedTargetBase = ResolvedTargetBase>`,包含 `type``configKey`、TypeBox 配置契约、启动期语义校验、`resolve``execute``serialize``buildDetail` 成员。泛型参数 SHALL 约束 `execute``serialize` 方法的 target 参数类型,使 checker 实现内部获得编译期类型安全。默认泛型参数 `= ResolvedTargetBase` 保证中间层registry、engine、config-loader无需指定泛型。
#### Scenario: Checker 接口包含必要方法
- **WHEN** 开发者实现一个新的 Checker
- **THEN** 该实现 MUST 提供 `type`(字符串标识)、`configKey`配置分组名、TypeBox 配置契约、启动期语义校验、`resolve(target, context)`(解析配置并填充默认值)、`execute(target, ctx)`(执行探测返回 CheckResult`serialize(target)`(返回 target 展示文本和 config JSON`buildDetail(observation)`(从 observation 构造人可读摘要)
#### Scenario: CheckerContext 注入 signal
- **WHEN** 引擎调用 `checker.execute(target, ctx)`
- **THEN** `ctx.signal` SHALL 是一个由引擎创建的 `AbortSignal`,在超时或引擎关闭时 abort
#### Scenario: resolve 不承担通用契约校验
- **WHEN** config-loader 调用 checker.resolve()
- **THEN** checker.resolve() SHALL 假定配置已经通过 TypeBox/Ajv 契约校验和启动期语义校验,只负责默认值填充、路径解析和领域配置转换
#### Scenario: type 与 configKey 默认一致
- **WHEN** checker 定义 `type: "tcp"`
- **THEN** checker 的 `configKey` SHALL 默认使用 `"tcp"`,对应 target 的 `tcp` 分组和 defaults.tcp 分组
#### Scenario: 接口方法使用泛型约束
- **WHEN** 开发者查看 `CheckerDefinition<TResolved>` 接口签名
- **THEN** `resolve` 的返回值 SHALL 为 `TResolved``execute` 的参数 SHALL 为 `TResolved``serialize` 的参数 SHALL 为 `TResolved`
#### Scenario: checker 实现无需手动断言
- **WHEN** HttpChecker 实现 `CheckerDefinition<ResolvedHttpTarget>`
- **THEN** `execute` 方法的 target 参数类型 SHALL 直接为 `ResolvedHttpTarget`,无需在方法内部使用 `as` 类型断言
#### Scenario: registry 使用默认泛型参数
- **WHEN** CheckerRegistry 存储和返回 checker 实例
- **THEN** registry 内部 SHALL 使用 `CheckerDefinition`(等价于 `CheckerDefinition<ResolvedTargetBase>`),实现类型擦除
#### Scenario: buildDetail 方法签名
- **WHEN** 开发者实现 buildDetail 方法
- **THEN** 方法签名 SHALL 为 `buildDetail(observation: Record<string, unknown>): string | null`,接收 observation 对象并返回人可读摘要字符串或 null
#### Scenario: buildDetail 由 API 层调用
- **WHEN** API 序列化 CheckResult
- **THEN** API 层 SHALL 通过 registry 获取对应 checker 并调用 buildDetail而非由 execute 方法直接生成 detail

View File

@@ -1,24 +0,0 @@
## MODIFIED Requirements
### Requirement: cmd checker 执行
系统 SHALL 按 cmd target 配置执行本地命令记录执行耗时、退出码、stdout 和 stderr observation并在执行失败时产生结构化错误信息。
#### Scenario: 命令正常退出
- **WHEN** cmd target 执行的进程正常退出且 exit code 为 0
- **THEN** 系统 SHALL 记录 `durationMs` 和包含 exitCode、stdoutPreview、stderrPreview 的 observation并进入 expect 校验API detail SHALL 为 `exitCode=0`
#### Scenario: 命令非零退出
- **WHEN** cmd target 执行的进程正常退出但 exit code 为 1
- **THEN** 系统 SHALL 记录包含 exitCode、stdoutPreview、stderrPreview 的 observation并由 expect.exitCode 决定 matched 结果API detail SHALL 为 `exitCode=1`
#### Scenario: 命令启动失败
- **WHEN** cmd target 的 exec 不存在或无法启动
- **THEN** 系统 SHALL 记录 `matched=false`observation SHALL 为 null并在 failure 中写入 kind=`error` 和可读错误信息
#### Scenario: 命令超时
- **WHEN** cmd target 在 timeout 时间内未结束
- **THEN** 系统 MUST 终止该子进程,记录 `matched=false`,并在 failure 中写入命令超时信息如已收集输出片段observation SHALL 包含 stdoutPreview、stderrPreview 和 error
#### Scenario: 命令输出超限
- **WHEN** cmd target 的 stdout 和 stderr 合计输出超过 `maxOutputBytes`
- **THEN** 系统 MUST 停止收集输出并终止该检查,记录 `matched=false`,并在 failure 中写入输出超限信息observation SHALL 包含已截断输出预览和 error

View File

@@ -1,16 +0,0 @@
## MODIFIED Requirements
### Requirement: ping detail 摘要
系统 SHALL 在 ping API 序列化时从 observation 动态生成结构化 detail 摘要展示关键指标。API registry type SHALL 仍为 `ping`
#### Scenario: 目标可达无丢包
- **WHEN** ping observation 为 alive=true, avgLatencyMs=12, packetLoss=0%, transmitted=3, received=3
- **THEN** detail SHALL 为 `alive, avg 12ms, loss 0% (3/3)`
#### Scenario: 目标可达有丢包
- **WHEN** ping observation 为 alive=true, avgLatencyMs=156, maxLatencyMs=340, packetLoss=33%, transmitted=3, received=2
- **THEN** detail SHALL 包含 avg、max 和 loss 信息
#### Scenario: 目标不可达
- **WHEN** ping observation 为 alive=false, transmitted=3, received=0
- **THEN** detail SHALL 为 `unreachable (0/3 received)`

View File

@@ -1,39 +0,0 @@
## MODIFIED Requirements
### Requirement: LLM Failure Phase 与状态摘要
LLM checker SHALL 使用 `request``status``headers``stream``output``finishReason``rawFinishReason``usage``duration` 作为第一版 failure phase。成功结果的 API detail SHALL 从持久化 observation 动态构造,简短描述 provider、mode、HTTP status、finish reason、raw finish reason、first token、输出长度和 token usage 中可用的信息。observation 和 detail MUST NOT 写入完整 prompt、完整输出或 key。
#### Scenario: request failure
- **WHEN** 模型请求因网络错误、认证调用异常、AbortSignal 或无 HTTP metadata 的 SDK 错误失败
- **THEN** LLM checker SHALL 返回 `phase: "request"` 的 error failure
#### Scenario: output mismatch failure
- **WHEN** 模型输出不满足 `expect.output`
- **THEN** LLM checker SHALL 返回 `phase: "output"` 的 mismatch failure
#### Scenario: 非流式成功摘要
- **WHEN** `provider: openai` 的非流式检查成功
- **THEN** detail SHALL 使用类似 `LLM openai http 200 finish=stop, output=2 chars, usage=12/2 tokens` 的简短格式
#### Scenario: 流式成功摘要
- **WHEN** `provider: anthropic` 的流式检查成功且存在 raw finish reason
- **THEN** detail SHALL 使用类似 `LLM anthropic stream 200 finish=stop raw=end_turn, firstToken=624ms, output=2 chars` 的简短格式
#### Scenario: serialize 展示文本
- **WHEN** store 同步 LLM target
- **THEN** LLM checker `serialize()` SHALL 返回类似 `openai:gpt-4o-mini @ https://api.openai.com/v1` 的 target 展示文本和 resolved config JSON
### Requirement: LLM Checker 测试策略
LLM checker 的自动化测试 MUST 不访问真实外部模型服务。测试 SHALL 使用本地 mock HTTP/SSE 服务模拟 OpenAI Chat Completions、OpenAI Responses 和 Anthropic Messages 的成功、错误和流式响应。测试 SHALL 覆盖 schema、语义校验、defaults 合并、变量替换、provider factory、observation、expect、execute、registry 注册、配置加载和 JSON Schema 导出。
#### Scenario: 本地 mock provider 测试成功路径
- **WHEN** 测试运行 LLM checker 的 OpenAI、OpenAI Responses 和 Anthropic 成功路径
- **THEN** 测试 SHALL 使用本地 mock 服务返回 provider 响应,不依赖外部网络或真实 API key
#### Scenario: 本地 mock provider 测试错误路径
- **WHEN** 测试运行 401、429、500、超时、stream error、stream abort、缺 usage 或无文本输出路径
- **THEN** 测试 SHALL 断言 LLM checker 返回符合 spec 的 matched、failure phase、actual、detail 和 observation
#### Scenario: 质量检查覆盖 LLM checker
- **WHEN** 实现完成后执行质量检查
- **THEN** `bun run schema:check``bun run check` SHALL 通过

View File

@@ -1,48 +0,0 @@
## MODIFIED Requirements
### Requirement: 新增共享类型
系统 SHALL 在 `src/shared/api.ts` 中定义 Dashboard 和 Metrics 相关共享类型。CheckResult SHALL 包含 durationMsnull | number、failureCheckFailure | null、matchedboolean、detailnull | string、observationRecord<string, unknown> | null、timestampstring。其中 detail 替代原 statusDetail 字段名。
#### Scenario: DashboardResponse 类型
- **WHEN** 前后端共享 `DashboardResponse` 类型
- **THEN** 该类型 SHALL 包含 summary 和 targets 字段
#### Scenario: TargetStatus 类型
- **WHEN** 前后端共享 `TargetStatus` 类型
- **THEN** 该类型 SHALL 包含目标基本信息字段id、name、description、group、type、target、interval、statstotalChecks、upChecks、downChecks、availability、currentStreak 和 recentSamples 字段,其中 name 和 description 类型均为 null 或字符串
#### Scenario: TargetMetricsResponse 类型
- **WHEN** 前后端共享 `TargetMetricsResponse` 类型
- **THEN** 该类型 SHALL 包含 targetId、window、stats 和 trend 字段
#### Scenario: TrendPoint 类型
- **WHEN** 前后端共享 `TrendPoint` 类型
- **THEN** 该类型 SHALL 包含 bucketStart、avgDurationMs、minDurationMs、maxDurationMs、availability、totalChecks、upChecks、downChecks 字段
#### Scenario: CheckResult 类型变更
- **WHEN** 前端或后端引用 CheckResult 类型
- **THEN** 该类型 SHALL 包含 `timestamp: string``matched: boolean``durationMs: number | null``detail: string | null``observation: Record<string, unknown> | null``failure` 字段,不包含 statusDetail 字段,不包含 success 字段
#### Scenario: RecentSample 类型
- **WHEN** 前后端共享 `RecentSample` 类型
- **THEN** 该类型 SHALL 包含 `timestamp: string``durationMs: number | null``up: boolean` 字段,其中 up 为 boolean 且等于 matched
#### Scenario: HistoryResponse 类型
- **WHEN** 前后端共享 `HistoryResponse` 类型
- **THEN** 该类型 SHALL 包含 `items: CheckResult[]``total: number``page: number``pageSize: number` 字段
#### Scenario: API 序列化构造 detail
- **WHEN** API 路由序列化 StoredCheckResult 为 API 响应
- **THEN** 系统 SHALL 从 StoredCheckResult 中反序列化 observation根据 target type 通过 checkerRegistry 获取对应 checker 并调用 buildDetail(observation) 动态生成 detail 字段
#### Scenario: mapCheckResult 接收 type 参数
- **WHEN** 序列化辅助函数 mapCheckResult 被调用
- **THEN** 函数 SHALL 接收 target type 参数,用于从 registry 获取对应 checker 调用 buildDetail
#### Scenario: Dashboard API 传递 type
- **WHEN** Dashboard 路由序列化 latestCheck
- **THEN** 路由 SHALL 将 target.type 传递给 mapCheckResult
#### Scenario: History API 传递 type
- **WHEN** History 路由序列化历史记录列表
- **THEN** 路由 SHALL 将已查询的 target.type 传递给 mapCheckResult

View File

@@ -1,41 +0,0 @@
## MODIFIED Requirements
### Requirement: SQLite 数据库初始化
系统 SHALL 使用 Bun 内置 `bun:sqlite` 模块在配置的数据目录下创建 SQLite 数据库文件,并以 WAL 模式运行。数据库 schema MUST 支持 typed checker target 和结构化检查结果targets 表 MUST 包含 `grp` 列存储分组信息,且 targets 表的 `name``description` 列 MUST 允许 NULL。
#### Scenario: 首次启动创建数据库
- **WHEN** 指定的数据目录下不存在数据库文件
- **THEN** 系统 SHALL 创建数据库文件并初始化 targets 表和 check_results 表check_results 表包含 idINTEGER PRIMARY KEY AUTOINCREMENT、target_idTEXT NOT NULL、timestampTEXT NOT NULL、matchedINTEGER NOT NULL、duration_msREAL、observationTEXT、failureTEXT不包含 status_detail 列,不包含 success 列
#### Scenario: targets name 列允许 NULL
- **WHEN** 系统首次创建 targets 表
- **THEN** targets.name 列 SHALL 允许存储 NULL
#### Scenario: 数据目录不存在
- **WHEN** 配置的数据目录路径不存在
- **THEN** 系统 SHALL 自动创建该目录
#### Scenario: 数据库已存在时启动
- **WHEN** 数据库文件已存在
- **THEN** 系统 SHALL 直接打开数据库,不重新建表
#### Scenario: 外键约束
- **THEN** 系统 SHALL 启用 `PRAGMA foreign_keys = ON`
#### Scenario: 级联删除
- **THEN** check_results 表的外键约束 SHALL 使用 `ON DELETE CASCADE`,确保删除目标时自动清理关联结果记录
### Requirement: check_results 表追加写入
系统 SHALL 将每次检查结果追加写入 check_results 表,不更新或删除已有记录。
#### Scenario: 写入检查结果
- **WHEN** 一次 checker 执行完成
- **THEN** 系统 SHALL 插入一条包含 target_id、timestamp、matched、duration_ms、observation、failure 的记录,其中 observation 使用 JSON.stringify 序列化为 TEXT
#### Scenario: 查询检查结果
- **WHEN** 系统查询 latest check 或历史 check_results
- **THEN** 存储层 SHALL 返回 observation 字段而非 status_detail 字段,供 API 序列化层反序列化并构造 detail
#### Scenario: 写入结构化失败信息
- **WHEN** checker 执行失败或 expect 不匹配
- **THEN** 系统 SHALL 将首个失败原因序列化写入 failure 字段

View File

@@ -1,16 +0,0 @@
## MODIFIED Requirements
### Requirement: 拨测结果记录
系统 SHALL 在每次 checker 完成后,将结果写入 SQLite 数据存储,包含 target_id、timestamp、matched、duration_ms、observation、failure 字段。detail SHALL 为 API 层派生字段,不写入存储层;系统 SHALL NOT 写入 status_detail 字段。
#### Scenario: 成功检查结果记录
- **WHEN** checker 成功执行且 expect 全部匹配
- **THEN** 系统 SHALL 记录 matched=true、duration_ms、observationfailure 为 null
#### Scenario: 执行失败结果记录
- **WHEN** checker 执行失败(网络错误、超时、命令启动失败、输出超限等)
- **THEN** 系统 SHALL 记录 matched=false、failure.kind="error" 和具体错误信息,并在可收集领域观测数据时记录 observation
#### Scenario: expect 不匹配结果记录
- **WHEN** checker 执行成功但 expect 不匹配
- **THEN** 系统 SHALL 记录 matched=false、observation、failure.kind="mismatch" 和具体不匹配信息

View File

@@ -1,16 +0,0 @@
## MODIFIED Requirements
### Requirement: 记录面板
记录 Tab SHALL 展示分页检查结果列表,使用 TDesign PrimaryTable。
#### Scenario: 检查结果表格
- **WHEN** 记录面板渲染且数据可用
- **THEN** 面板 SHALL 使用 TDesign PrimaryTable 展示检查结果列包含状态StatusDot 圆点、时间YYYY-MM-DD HH:mm:ss 格式)、耗时(标题含 ms 单位单元格仅显示数值居中对齐、详情detail 和 failure.message 用冒号拼接)
#### Scenario: 服务端分页
- **WHEN** 检查结果总数超过一页
- **THEN** 表格 SHALL 使用内建 paginationdisableDataPage=true分页器显示在表格底部
#### Scenario: 翻页触发请求
- **WHEN** 用户切换分页页码
- **THEN** 系统 SHALL 请求对应页码的服务端数据,表格更新

View File

@@ -1,51 +0,0 @@
## MODIFIED Requirements
### Requirement: tcp checker 执行
系统 SHALL 按 tcp target 配置建立 TCP 连接,记录完整执行耗时和 TCP observation并在连接失败、超时或资源超限时产生结构化失败信息。
#### Scenario: TCP 连接成功
- **WHEN** tcp target 指向可连接的 TCP 服务,且未配置 expect 或 `expect.connected``true`
- **THEN** 系统 SHALL 记录 `matched=true``durationMs` 和包含 connected、connectTimeMs、banner 的 observation并关闭 socket
#### Scenario: TCP 连接失败
- **WHEN** tcp target 指向不可连接的 host/port且未配置 expect 或 `expect.connected``true`
- **THEN** 系统 SHALL 记录 `matched=false`observation SHALL 包含 connected=false 和错误信息failure 的 kind 为 `error`phase 为 `connect`message 包含可读连接失败原因
#### Scenario: 期望端口不可达且连接失败
- **WHEN** tcp target 配置 `expect.connected: false`,且 TCP 连接失败
- **THEN** 系统 SHALL 记录 `matched=true`observation SHALL 包含 connected=false 和实际连接失败原因API detail SHALL 展示实际连接失败原因摘要
#### Scenario: 期望端口不可达但连接成功
- **WHEN** tcp target 配置 `expect.connected: false`,但 TCP 连接成功
- **THEN** 系统 SHALL 记录 `matched=false`observation SHALL 包含 connected=truefailure 的 kind 为 `mismatch`phase 为 `connected`
#### Scenario: TCP 执行超时
- **WHEN** 引擎注入的 `ctx.signal` 在 TCP 连接或 banner 读取过程中 abort
- **THEN** 系统 SHALL best-effort 关闭 socket记录 `matched=false`failure 的 kind 为 `error`phase 为 `connect``banner`message 包含超时信息,并在可收集时记录 observation
#### Scenario: duration 包含 banner 读取
- **WHEN** tcp target 开启 `readBanner` 且服务端延迟发送 banner
- **THEN** 结果中的 `durationMs` SHALL 覆盖连接建立、banner 等待、banner 读取和 expect 校验的完整耗时
### Requirement: tcp banner 读取
系统 SHALL 仅在 `tcp.readBanner: true` 时读取服务端主动发送的 banner 数据,并同时受 `bannerReadTimeout``maxBannerBytes` 限制。
#### Scenario: 默认不读取 banner
- **WHEN** tcp target 未配置 `readBanner` 或配置为 `false`
- **THEN** 系统 SHALL 在连接建立后立即进入 connected 和 duration 校验,不等待服务端数据
#### Scenario: 读取服务端 banner
- **WHEN** tcp target 配置 `readBanner: true`,且服务端连接后发送 `220 smtp.example.com ESMTP`
- **THEN** 系统 SHALL 收集 banner 文本,并允许后续 `expect.banner` 对该文本执行 operator 断言
#### Scenario: banner 等待超时无数据
- **WHEN** tcp target 配置 `readBanner: true`,但服务端在 `bannerReadTimeout` 内未发送任何数据
- **THEN** 系统 SHALL 将 banner 视为空字符串并继续执行 expect 校验,不将无 banner 本身作为连接错误
#### Scenario: banner 读取超过最大字节数
- **WHEN** 服务端发送的 banner 数据超过 `maxBannerBytes`
- **THEN** 系统 SHALL 停止读取并记录 `matched=false`、failure.kind=`error`、failure.phase=`banner` 的结构化错误
#### Scenario: banner detail 截断展示
- **WHEN** tcp target 成功读取到较长 banner
- **THEN** observation.banner SHALL 保存截断后的 banner 摘要API detail SHALL 展示截断后的 banner 摘要,避免 UI 展示过长文本

View File

@@ -1,55 +0,0 @@
## MODIFIED Requirements
### Requirement: udp checker 执行
系统 SHALL 使用 Bun connected UDP socket 向目标发送单个 datagram等待第一个 UDP 响应 datagram记录完整执行耗时和 UDP observation并在发送失败、超时、资源超限或底层 socket 错误时产生结构化失败信息。
#### Scenario: UDP 请求响应成功
- **WHEN** udp target 指向会返回 `PONG` 的 UDP 服务,且未配置 expect 或 `expect.responded``true`
- **THEN** 系统 SHALL 记录 `matched=true``durationMs` 和包含响应大小的 observation并关闭 socket
#### Scenario: 使用 hostname 执行 UDP 探测
- **WHEN** udp target 的 `udp.host` 为可解析域名或 `localhost`
- **THEN** 系统 SHALL 使用 Bun connected UDP socket 完成发送和接收,不要求配置 IP 地址
#### Scenario: 只处理第一个响应 datagram
- **WHEN** UDP 服务对一次请求返回多个 datagram
- **THEN** 系统 SHALL 仅使用第一个收到的 UDP datagram 执行 expect 校验,并关闭 socket
#### Scenario: UDP 无响应且默认期望响应
- **WHEN** udp target 指向在 timeout 内不返回 UDP datagram 的服务,且未配置 expect 或 `expect.responded``true`
- **THEN** 系统 SHALL 在 `ctx.signal` abort 后记录 `matched=false`observation SHALL 包含 responded=false 和 errorfailure 的 kind 为 `error`phase 为 `response`message 包含超时或未响应信息
#### Scenario: 期望无响应且实际无响应
- **WHEN** udp target 配置 `expect.responded: false`,且 timeout 内未收到 UDP datagram
- **THEN** 系统 SHALL 记录 `matched=true`observation SHALL 包含 responded=falseAPI detail SHALL 表示未收到响应
#### Scenario: 期望无响应但实际收到响应
- **WHEN** udp target 配置 `expect.responded: false`,但收到了 UDP datagram
- **THEN** 系统 SHALL 记录 `matched=false`observation SHALL 包含 responded=true 和响应摘要failure 的 kind 为 `mismatch`phase 为 `responded`
#### Scenario: UDP socket 底层错误
- **WHEN** Bun UDP socket 在发送或接收过程中触发 error 事件
- **THEN** 系统 SHALL best-effort 关闭 socket并记录 `matched=false`、failure.kind=`error` 和可读错误信息,并在可收集时记录 observation
#### Scenario: ICMP unreachable 不作为 UDP 响应
- **WHEN** 底层系统因目标端口不可达产生 ICMP unreachable
- **THEN** 系统 SHALL NOT 将其视为 `responded=true` 的 UDP datagram 响应
#### Scenario: UDP 执行超时关闭 socket
- **WHEN** 引擎注入的 `ctx.signal` 在 UDP 发送或等待响应过程中 abort
- **THEN** 系统 SHALL best-effort 关闭 UDP socket并记录结构化超时或未响应结果
### Requirement: udp detail 摘要
系统 SHALL 在 udp API 序列化时从 observation 动态生成简短 detail 摘要,展示关键结果并避免返回过长响应内容。
#### Scenario: 收到响应的摘要
- **WHEN** udp target 收到 4 字节响应且完整执行耗时为 12ms
- **THEN** detail SHALL 包含 `responded in 12ms``4 bytes`
#### Scenario: 未收到响应的摘要
- **WHEN** udp target 配置 `expect.responded: false` 且 timeout 内未收到 UDP datagram
- **THEN** detail SHALL 包含 `no response` 和执行耗时
#### Scenario: 响应内容摘要截断
- **WHEN** udp target 收到较长响应内容
- **THEN** detail SHALL 只展示按 `responseEncoding` 转换并截断后的响应摘要

View File

@@ -1,56 +0,0 @@
## 1. 共享类型与接口变更
- [ ] 1.1 修改 `src/shared/api.ts` 中 CheckResult 类型:移除 statusDetail新增 detail 和 observation 字段
- [ ] 1.2 修改 `src/server/checker/types.ts` 中 StoredCheckResult 类型status_detail 替换为 observation
- [ ] 1.3 修改 `src/server/checker/runner/types.ts` 中 CheckerDefinition 接口:新增 buildDetail 方法
## 2. 存储层适配
- [ ] 2.1 修改 `src/server/checker/store.ts` 中 check_results 表 DDLstatus_detail 列替换为 observation TEXT 列
- [ ] 2.2 修改 `src/server/checker/store.ts` 中 insertCheckResult 方法:写入 observationJSON.stringify替代 statusDetail
- [ ] 2.3 修改 `src/server/checker/store.ts` 中 getHistory、getLatestCheck、getLatestChecksMap 读取类型:返回 observation 字段替代 status_detail
- [ ] 2.4 修改 `src/server/checker/engine.ts` 中 writeResult 方法:传递 observation 替代 statusDetail异常兜底结果 observation 为 null
## 3. Checker execute 改造(返回 observation 替代 statusDetail
- [ ] 3.1 改造 HTTP checker execute.ts组装 observation 对象statusCode/headers/bodyPreview/contentType/contentLength拿到响应后始终采集 bodyPreview移除 statusDetail 赋值
- [ ] 3.2 改造 TCP checker execute.ts组装 observation 对象connected/connectTimeMs/banner/error移除 statusDetail 赋值和 buildStatusDetail 函数
- [ ] 3.3 改造 UDP checker execute.ts组装 observation 对象responded/responseSize/responsePreview/sourceAddress/sourcePort/error移除 statusDetail 赋值和 build*Detail 函数
- [ ] 3.4 改造 ICMP checker execute.ts组装 observation 对象(复用 PingStats 字段 + error移除 statusDetail 赋值和 buildStatusDetail 函数
- [ ] 3.5 改造 DB checker execute.ts组装 observation 对象connected/rowCount/rowsPreview/error移除 statusDetail 赋值
- [ ] 3.6 改造 CMD checker execute.ts组装 observation 对象exitCode/stdoutPreview/stderrPreview/error移除 statusDetail 赋值
- [ ] 3.7 改造 LLM checker types.ts 和 observation.ts保留执行期完整 outputText新增持久化 observation 派生结构outputPreview/outputLength/截断 headers
- [ ] 3.8 改造 LLM checker execute.ts返回持久化 observation继续用执行期 LlmCheckObservation 执行 expect移除 buildStatusDetail 函数
## 4. Checker buildDetail 实现
- [ ] 4.1 为 HTTP checker 实现 buildDetail 方法:返回 `"HTTP {statusCode}"` 格式
- [ ] 4.2 为 TCP checker 实现 buildDetail 方法:返回连接状态和 banner 摘要
- [ ] 4.3 为 UDP checker 实现 buildDetail 方法:返回响应状态和大小摘要
- [ ] 4.4 为 ICMP checker 实现 buildDetail 方法:返回存活状态、平均延迟和丢包率摘要
- [ ] 4.5 为 DB checker 实现 buildDetail 方法:返回连接状态或行数摘要
- [ ] 4.6 为 CMD checker 实现 buildDetail 方法:返回 `"exitCode={N}"` 格式
- [ ] 4.7 为 LLM checker 实现 buildDetail 方法:返回 provider/mode/status/finish/output/usage 摘要
## 5. API 序列化层适配
- [ ] 5.1 修改 `src/server/helpers.ts` 中 mapCheckResult接收 type 参数,反序列化 observationobservation 为 null 时 detail 为 null否则调用 buildDetail 动态构造 detail
- [ ] 5.2 修改 `src/server/routes/dashboard.ts`:传递 target.type 给 mapCheckResult
- [ ] 5.3 修改 `src/server/routes/history.ts`:传递 target.type 给 mapCheckResult
## 6. 前端适配
- [ ] 6.1 修改 `src/web/constants/history-table-columns.tsx`statusDetail 引用改为 detail
- [ ] 6.2 修改 `src/web/components/OverviewTab.tsx`statusDetail 引用改为 detail
## 7. 测试与质量保障
- [ ] 7.1 更新所有涉及 CheckResult 的现有测试,适配 statusDetail → detail + observation 字段变更
- [ ] 7.2 为各 checker 的 buildDetail 方法编写单元测试
- [ ] 7.3 更新 CheckerDefinition mock、store、engine、dashboard/history API、前端组件与 constants 的测试 fixture
- [ ] 7.4 为 mapCheckResult 编写 observation JSON parse、null observation、unknown type 或 malformed observation 的覆盖测试
- [ ] 7.5 执行完整测试套件、代码检查和格式检查,确保无回归
## 8. 文档更新
- [ ] 8.1 更新 README.md 和/或 DEVELOPMENT.md反映 CheckResult 类型变更和 observation 机制

View File

@@ -1,4 +1,8 @@
## ADDED Requirements
## Purpose
定义 CheckResult 的 observation 数据模型、各 checker 类型 observation 结构、截断策略、序列化规则和 detail 动态构造机制。
## Requirements
### Requirement: Observation 数据模型
CheckResult SHALL 包含 `observation: Record<string, unknown> | null` 字段,用于承载 checker 执行过程中收集的结构化观测数据。observation 为 null 表示执行过程中无法形成有意义的领域观测数据(如进程 spawn 失败、内部异常、请求在拿到响应前失败且无可记录元数据等场景)。各 checker SHALL 自行定义 observation 的内部结构,不做跨 checker 类型的统一约束。
@@ -34,7 +38,7 @@ TCP checker 的 observation SHALL 包含 connectedboolean、connectTimeMs
- **THEN** observation SHALL 包含 connected=false 和错误信息
### Requirement: UDP Checker Observation
UDP checker 的 observation SHALL 包含 respondedboolean、responseSizenumber | null、responsePreviewstring | null截断、sourceAddressstring | null、sourcePortnumber | null、errorstring | null
UDP checker 的 observation SHALL 包含 respondedbooleandurationMsnumberresponseSizenumber | null、responsePreviewstring | null截断、sourceAddressstring | null、sourcePortnumber | null、errorstring | nulldurationMs 用于 API 序列化层生成包含耗时的 UDP detail 摘要。
#### Scenario: UDP 收到响应
- **WHEN** UDP 发送数据后收到响应

View File

@@ -50,11 +50,11 @@
- **THEN** checker SHALL 返回 `ConfigValidationIssue`,而不是直接抛出最终用户错误字符串
### Requirement: Checker 接口定义
系统 SHALL 在 `src/server/checker/runner/types.ts` 中定义面向扩展的泛型 `CheckerDefinition<TResolved extends ResolvedTargetBase = ResolvedTargetBase>`,包含 `type``configKey`、TypeBox 配置契约、启动期语义校验、`resolve``execute``serialize` 成员。泛型参数 SHALL 约束 `execute``serialize` 方法的 target 参数类型,使 checker 实现内部获得编译期类型安全。默认泛型参数 `= ResolvedTargetBase` 保证中间层registry、engine、config-loader无需指定泛型。
系统 SHALL 在 `src/server/checker/runner/types.ts` 中定义面向扩展的泛型 `CheckerDefinition<TResolved extends ResolvedTargetBase = ResolvedTargetBase>`,包含 `type``configKey`、TypeBox 配置契约、启动期语义校验、`resolve``execute``serialize``buildDetail` 成员。泛型参数 SHALL 约束 `execute``serialize` 方法的 target 参数类型,使 checker 实现内部获得编译期类型安全。默认泛型参数 `= ResolvedTargetBase` 保证中间层registry、engine、config-loader无需指定泛型。
#### Scenario: Checker 接口包含必要方法
- **WHEN** 开发者实现一个新的 Checker
- **THEN** 该实现 MUST 提供 `type`(字符串标识)、`configKey`配置分组名、TypeBox 配置契约、启动期语义校验、`resolve(target, context)`(解析配置并填充默认值)、`execute(target, ctx)`(执行探测返回 CheckResult`serialize(target)`(返回 target 展示文本和 config JSON
- **THEN** 该实现 MUST 提供 `type`(字符串标识)、`configKey`配置分组名、TypeBox 配置契约、启动期语义校验、`resolve(target, context)`(解析配置并填充默认值)、`execute(target, ctx)`(执行探测返回 CheckResult`serialize(target)`(返回 target 展示文本和 config JSON`buildDetail(observation)`(从 observation 构造人可读摘要)
#### Scenario: CheckerContext 注入 signal
- **WHEN** 引擎调用 `checker.execute(target, ctx)`
@@ -80,6 +80,14 @@
- **WHEN** CheckerRegistry 存储和返回 checker 实例
- **THEN** registry 内部 SHALL 使用 `CheckerDefinition`(等价于 `CheckerDefinition<ResolvedTargetBase>`),实现类型擦除
#### Scenario: buildDetail 方法签名
- **WHEN** 开发者实现 buildDetail 方法
- **THEN** 方法签名 SHALL 为 `buildDetail(observation: Record<string, unknown>): string | null`,接收 observation 对象并返回人可读摘要字符串或 null
#### Scenario: buildDetail 由 API 层调用
- **WHEN** API 序列化 CheckResult
- **THEN** API 层 SHALL 通过 registry 获取对应 checker 并调用 buildDetail而非由 execute 方法直接生成 detail
### Requirement: CheckerRegistry 注册中心
系统 SHALL 在 `src/server/checker/runner/registry.ts` 中提供 `CheckerRegistry` 类,支持 `register(checker)``get(type)``supportedTypes`。重复注册同一 type SHALL 抛出错误。registry 内部 SHALL 存储 `CheckerDefinition`(使用默认泛型参数),对外提供类型擦除后的接口。

View File

@@ -32,27 +32,27 @@
- **THEN** 系统 MUST NOT 向子进程 stdin 写入数据,避免命令因等待输入而阻塞
### Requirement: cmd checker 执行
系统 SHALL 按 cmd target 配置执行本地命令记录执行耗时、退出码、stdout 和 stderr并在执行失败时产生结构化错误信息。
系统 SHALL 按 cmd target 配置执行本地命令记录执行耗时、退出码、stdout 和 stderr observation,并在执行失败时产生结构化错误信息。
#### Scenario: 命令正常退出
- **WHEN** cmd target 执行的进程正常退出且 exit code 为 0
- **THEN** 系统 SHALL 记录 `durationMs``statusDetail="exitCode=0"`,并进入 expect 校验
- **THEN** 系统 SHALL 记录 `durationMs` 和包含 exitCode、stdoutPreview、stderrPreview 的 observation并进入 expect 校验API detail SHALL 为 `exitCode=0`
#### Scenario: 命令非零退出
- **WHEN** cmd target 执行的进程正常退出但 exit code 为 1
- **THEN** 系统 SHALL 记录 `statusDetail="exitCode=1"`,并由 expect.exitCode 决定 matched 结果
- **THEN** 系统 SHALL 记录包含 exitCode、stdoutPreview、stderrPreview 的 observation并由 expect.exitCode 决定 matched 结果API detail SHALL 为 `exitCode=1`
#### Scenario: 命令启动失败
- **WHEN** cmd target 的 exec 不存在或无法启动
- **THEN** 系统 SHALL 记录 `matched=false`,并在 failure 中写入 kind=`error` 和可读错误信息
- **THEN** 系统 SHALL 记录 `matched=false`observation SHALL 为 null并在 failure 中写入 kind=`error` 和可读错误信息
#### Scenario: 命令超时
- **WHEN** cmd target 在 timeout 时间内未结束
- **THEN** 系统 MUST 终止该子进程,记录 `matched=false`,并在 failure 中写入命令超时信息
- **THEN** 系统 MUST 终止该子进程,记录 `matched=false`,并在 failure 中写入命令超时信息如已收集输出片段observation SHALL 包含 stdoutPreview、stderrPreview 和 error
#### Scenario: 命令输出超限
- **WHEN** cmd target 的 stdout 和 stderr 合计输出超过 `maxOutputBytes`
- **THEN** 系统 MUST 停止收集输出并终止该检查,记录 `matched=false`,并在 failure 中写入输出超限信息
- **THEN** 系统 MUST 停止收集输出并终止该检查,记录 `matched=false`,并在 failure 中写入输出超限信息observation SHALL 包含已截断输出预览和 error
### Requirement: cmd expect 校验
系统 SHALL 支持 cmd 专用 expect包括 `exitCode``durationMs``stdout``stderr`,并按 exitCode、durationMs、stdout、stderr 的阶段顺序快速失败。`exitCode` SHALL 保持有限整数数组语义,未配置时默认 `[0]``durationMs` SHALL 使用共享 `ValueMatcher` 校验完整命令执行耗时。`stdout``stderr` MUST 使用共享 `ContentRules` 数组,直接 matcher 作用于对应输出文本,`json` extractor SHALL 支持对 JSON CLI 输出执行 JSONPath 断言。

View File

@@ -172,17 +172,17 @@
- **WHEN** YAML 中 ping target 的 `expect.maxLatencyMs` 不是合法 `ValueMatcher`
- **THEN** 系统 SHALL 以配置错误退出,提示 expect.maxLatencyMs 格式错误
### Requirement: ping statusDetail 摘要
系统 SHALL 在 ping 执行成功后生成结构化 statusDetail 摘要,展示关键指标
### Requirement: ping detail 摘要
系统 SHALL 在 ping API 序列化时从 observation 动态生成结构化 detail 摘要展示关键指标。API registry type SHALL 仍为 `ping`
#### Scenario: 目标可达无丢包
- **WHEN** ping 结果为 alive=true, avg=12ms, packetLoss=0%, transmitted=3, received=3
- **THEN** statusDetail SHALL 为 `alive, avg 12ms, loss 0% (3/3)`
- **WHEN** ping observation 为 alive=true, avgLatencyMs=12, packetLoss=0%, transmitted=3, received=3
- **THEN** detail SHALL 为 `alive, avg 12ms, loss 0% (3/3)`
#### Scenario: 目标可达有丢包
- **WHEN** ping 结果为 alive=true, avg=156ms, max=340ms, packetLoss=33%, transmitted=3, received=2
- **THEN** statusDetail SHALL 包含 avg、max 和 loss 信息
- **WHEN** ping observation 为 alive=true, avgLatencyMs=156, maxLatencyMs=340, packetLoss=33%, transmitted=3, received=2
- **THEN** detail SHALL 包含 avg、max 和 loss 信息
#### Scenario: 目标不可达
- **WHEN** ping 结果为 alive=false, transmitted=3, received=0
- **THEN** statusDetail SHALL 为 `unreachable (0/3 received)`
- **WHEN** ping observation 为 alive=false, transmitted=3, received=0
- **THEN** detail SHALL 为 `unreachable (0/3 received)`

View File

@@ -205,7 +205,7 @@ LLM checker SHALL 仅允许 `mode: stream` 使用 `expect.stream`。`expect.stre
- **THEN** 默认 `stream.completed=true` SHALL NOT 阻断基于 status 和 headers 的 APICallError 状态探测
### Requirement: LLM Failure Phase 与状态摘要
LLM checker SHALL 使用 `request``status``headers``stream``output``finishReason``rawFinishReason``usage``duration` 作为第一版 failure phase。成功结果的 `statusDetail` SHALL 简短描述 provider、mode、HTTP status、finish reason、raw finish reason、first token、输出长度和 token usage 中可用的信息。`statusDetail` MUST NOT 写入完整 prompt、完整输出或 key。
LLM checker SHALL 使用 `request``status``headers``stream``output``finishReason``rawFinishReason``usage``duration` 作为第一版 failure phase。成功结果的 API detail SHALL 从持久化 observation 动态构造,简短描述 provider、mode、HTTP status、finish reason、raw finish reason、first token、输出长度和 token usage 中可用的信息。observation 和 detail MUST NOT 写入完整 prompt、完整输出或 key。
#### Scenario: request failure
- **WHEN** 模型请求因网络错误、认证调用异常、AbortSignal 或无 HTTP metadata 的 SDK 错误失败
@@ -217,11 +217,11 @@ LLM checker SHALL 使用 `request`、`status`、`headers`、`stream`、`output`
#### Scenario: 非流式成功摘要
- **WHEN** `provider: openai` 的非流式检查成功
- **THEN** `statusDetail` SHALL 使用类似 `LLM openai http 200 finish=stop, output=2 chars, usage=12/2 tokens` 的简短格式
- **THEN** detail SHALL 使用类似 `LLM openai http 200 finish=stop, output=2 chars, usage=12/2 tokens` 的简短格式
#### Scenario: 流式成功摘要
- **WHEN** `provider: anthropic` 的流式检查成功且存在 raw finish reason
- **THEN** `statusDetail` SHALL 使用类似 `LLM anthropic stream 200 finish=stop raw=end_turn, firstToken=624ms, output=2 chars` 的简短格式
- **THEN** detail SHALL 使用类似 `LLM anthropic stream 200 finish=stop raw=end_turn, firstToken=624ms, output=2 chars` 的简短格式
#### Scenario: serialize 展示文本
- **WHEN** store 同步 LLM target
@@ -236,7 +236,7 @@ LLM checker 的自动化测试 MUST 不访问真实外部模型服务。测试 S
#### Scenario: 本地 mock provider 测试错误路径
- **WHEN** 测试运行 401、429、500、超时、stream error、stream abort、缺 usage 或无文本输出路径
- **THEN** 测试 SHALL 断言 LLM checker 返回符合 spec 的 matched、failure phase、actual 和 statusDetail
- **THEN** 测试 SHALL 断言 LLM checker 返回符合 spec 的 matched、failure phase、actual、detail 和 observation
#### Scenario: 质量检查覆盖 LLM checker
- **WHEN** 实现完成后执行质量检查

View File

@@ -105,7 +105,7 @@ Dashboard API SHALL 返回基于时间窗口计算的目标统计和连续状态
- **THEN** 系统 SHALL 返回 400 状态码和错误信息
### Requirement: 新增共享类型
系统 SHALL 在 `src/shared/api.ts` 中定义 Dashboard 和 Metrics 相关共享类型。
系统 SHALL 在 `src/shared/api.ts` 中定义 Dashboard 和 Metrics 相关共享类型。CheckResult SHALL 包含 durationMsnull | number、failureCheckFailure | null、matchedboolean、detailnull | string、observationRecord<string, unknown> | null、timestampstring。其中 detail 替代原 statusDetail 字段名。
#### Scenario: DashboardResponse 类型
- **WHEN** 前后端共享 `DashboardResponse` 类型
@@ -123,9 +123,9 @@ Dashboard API SHALL 返回基于时间窗口计算的目标统计和连续状态
- **WHEN** 前后端共享 `TrendPoint` 类型
- **THEN** 该类型 SHALL 包含 bucketStart、avgDurationMs、minDurationMs、maxDurationMs、availability、totalChecks、upChecks、downChecks 字段
#### Scenario: CheckResult 类型
- **WHEN** 前后端共享 `CheckResult` 类型
- **THEN** 该类型 SHALL 包含 `timestamp: string``matched: boolean``durationMs: number | null``statusDetail: string | null``failure` 字段,不包含 success 字段
#### Scenario: CheckResult 类型变更
- **WHEN** 前端或后端引用 CheckResult 类型
- **THEN** 该类型 SHALL 包含 `timestamp: string``matched: boolean``durationMs: number | null``detail: string | null``observation: Record<string, unknown> | null``failure` 字段,不包含 statusDetail 字段,不包含 success 字段
#### Scenario: RecentSample 类型
- **WHEN** 前后端共享 `RecentSample` 类型
@@ -135,6 +135,22 @@ Dashboard API SHALL 返回基于时间窗口计算的目标统计和连续状态
- **WHEN** 前后端共享 `HistoryResponse` 类型
- **THEN** 该类型 SHALL 包含 `items: CheckResult[]``total: number``page: number``pageSize: number` 字段
#### Scenario: API 序列化构造 detail
- **WHEN** API 路由序列化 StoredCheckResult 为 API 响应
- **THEN** 系统 SHALL 从 StoredCheckResult 中反序列化 observation根据 target type 通过 checkerRegistry 获取对应 checker 并调用 buildDetail(observation) 动态生成 detail 字段
#### Scenario: mapCheckResult 接收 type 参数
- **WHEN** 序列化辅助函数 mapCheckResult 被调用
- **THEN** 函数 SHALL 接收 target type 参数,用于从 registry 获取对应 checker 调用 buildDetail
#### Scenario: Dashboard API 传递 type
- **WHEN** Dashboard 路由序列化 latestCheck
- **THEN** 路由 SHALL 将 target.type 传递给 mapCheckResult
#### Scenario: History API 传递 type
- **WHEN** History 路由序列化历史记录列表
- **THEN** 路由 SHALL 将已查询的 target.type 传递给 mapCheckResult
### Requirement: 保留健康检查端点
系统 SHALL 保留 `GET /health` 端点,不受拨测功能影响。

View File

@@ -9,7 +9,7 @@
#### Scenario: 首次启动创建数据库
- **WHEN** 指定的数据目录下不存在数据库文件
- **THEN** 系统 SHALL 创建数据库文件并初始化 targets 表和 check_results 表check_results 表包含 idINTEGER PRIMARY KEY AUTOINCREMENT、target_idINTEGER NOT NULL、timestampTEXT NOT NULL、matchedINTEGER NOT NULL、duration_msREALstatus_detailTEXT、failureTEXT不包含 success 列
- **THEN** 系统 SHALL 创建数据库文件并初始化 targets 表和 check_results 表check_results 表包含 idINTEGER PRIMARY KEY AUTOINCREMENT、target_idTEXT NOT NULL、timestampTEXT NOT NULL、matchedINTEGER NOT NULL、duration_msREALobservationTEXT、failureTEXT不包含 status_detail 列,不包含 success 列
#### Scenario: targets name 列允许 NULL
- **WHEN** 系统首次创建 targets 表
@@ -61,7 +61,11 @@
#### Scenario: 写入检查结果
- **WHEN** 一次 checker 执行完成
- **THEN** 系统 SHALL 插入一条包含 target_id、timestamp、matched、duration_ms、status_detail、failure 的记录
- **THEN** 系统 SHALL 插入一条包含 target_id、timestamp、matched、duration_ms、observation、failure 的记录,其中 observation 使用 JSON.stringify 序列化为 TEXT
#### Scenario: 查询检查结果
- **WHEN** 系统查询 latest check 或历史 check_results
- **THEN** 存储层 SHALL 返回 observation 字段而非 status_detail 字段,供 API 序列化层反序列化并构造 detail
#### Scenario: 写入结构化失败信息
- **WHEN** checker 执行失败或 expect 不匹配

View File

@@ -214,19 +214,19 @@ HTTP checker SHALL 将运行期失败归属到实际失败阶段。请求、网
- **THEN** 系统 SHALL 记录 `failure.phase="body"`,且 SHALL NOT 将该失败记录为 request 错误
### Requirement: 拨测结果记录
系统 SHALL 在每次 checker 完成后,将结果写入 SQLite 数据存储,包含 target_id、timestamp、matched、duration_ms、status_detail、failure 字段。
系统 SHALL 在每次 checker 完成后,将结果写入 SQLite 数据存储,包含 target_id、timestamp、matched、duration_ms、observation、failure 字段。detail SHALL 为 API 层派生字段,不写入存储层;系统 SHALL NOT 写入 status_detail 字段。
#### Scenario: 成功检查结果记录
- **WHEN** checker 成功执行且 expect 全部匹配
- **THEN** 系统 SHALL 记录 matched=true、duration_ms、status_detailfailure 为 null
- **THEN** 系统 SHALL 记录 matched=true、duration_ms、observationfailure 为 null
#### Scenario: 执行失败结果记录
- **WHEN** checker 执行失败(网络错误、超时、命令启动失败、输出超限等)
- **THEN** 系统 SHALL 记录 matched=false、failure.kind="error" 和具体错误信息
- **THEN** 系统 SHALL 记录 matched=false、failure.kind="error" 和具体错误信息,并在可收集领域观测数据时记录 observation
#### Scenario: expect 不匹配结果记录
- **WHEN** checker 执行成功但 expect 不匹配
- **THEN** 系统 SHALL 记录 matched=false、failure.kind="mismatch" 和具体不匹配信息
- **THEN** 系统 SHALL 记录 matched=false、observation、failure.kind="mismatch" 和具体不匹配信息
### Requirement: runner 选择
系统 SHALL 根据 target.type 选择对应 runner 执行检查。

View File

@@ -303,7 +303,7 @@ Drawer 顶部的时间范围快捷按钮和日期范围选择器 SHALL 在同一
#### Scenario: 检查结果表格
- **WHEN** 记录面板渲染且数据可用
- **THEN** 面板 SHALL 使用 TDesign PrimaryTable 展示检查结果列包含状态StatusDot 圆点、时间YYYY-MM-DD HH:mm:ss 格式)、耗时(标题含 ms 单位,单元格仅显示数值,居中对齐)、详情(statusDetail 和 failure.message 用冒号拼接)
- **THEN** 面板 SHALL 使用 TDesign PrimaryTable 展示检查结果列包含状态StatusDot 圆点、时间YYYY-MM-DD HH:mm:ss 格式)、耗时(标题含 ms 单位,单元格仅显示数值,居中对齐)、详情(detail 和 failure.message 用冒号拼接)
#### Scenario: 服务端分页
- **WHEN** 检查结果总数超过一页

View File

@@ -36,27 +36,27 @@
- **THEN** `target` 展示摘要 SHALL 为 `<host>:<port>``config` JSON SHALL 包含 resolved 后的 host、port、readBanner、bannerReadTimeout 和 maxBannerBytes
### Requirement: tcp checker 执行
系统 SHALL 按 tcp target 配置建立 TCP 连接,记录完整执行耗时,并在连接失败、超时或资源超限时产生结构化失败信息。
系统 SHALL 按 tcp target 配置建立 TCP 连接,记录完整执行耗时和 TCP observation,并在连接失败、超时或资源超限时产生结构化失败信息。
#### Scenario: TCP 连接成功
- **WHEN** tcp target 指向可连接的 TCP 服务,且未配置 expect 或 `expect.connected``true`
- **THEN** 系统 SHALL 记录 `matched=true``durationMs` `statusDetail`,并关闭 socket
- **THEN** 系统 SHALL 记录 `matched=true``durationMs`包含 connected、connectTimeMs、banner 的 observation,并关闭 socket
#### Scenario: TCP 连接失败
- **WHEN** tcp target 指向不可连接的 host/port且未配置 expect 或 `expect.connected``true`
- **THEN** 系统 SHALL 记录 `matched=false`failure 的 kind 为 `error`phase 为 `connect`message 包含可读连接失败原因
- **THEN** 系统 SHALL 记录 `matched=false`observation SHALL 包含 connected=false 和错误信息,failure 的 kind 为 `error`phase 为 `connect`message 包含可读连接失败原因
#### Scenario: 期望端口不可达且连接失败
- **WHEN** tcp target 配置 `expect.connected: false`,且 TCP 连接失败
- **THEN** 系统 SHALL 记录 `matched=true`statusDetail SHALL 展示实际连接失败原因摘要
- **THEN** 系统 SHALL 记录 `matched=true`observation SHALL 包含 connected=false 和实际连接失败原因API detail SHALL 展示实际连接失败原因摘要
#### Scenario: 期望端口不可达但连接成功
- **WHEN** tcp target 配置 `expect.connected: false`,但 TCP 连接成功
- **THEN** 系统 SHALL 记录 `matched=false`failure 的 kind 为 `mismatch`phase 为 `connected`
- **THEN** 系统 SHALL 记录 `matched=false`observation SHALL 包含 connected=truefailure 的 kind 为 `mismatch`phase 为 `connected`
#### Scenario: TCP 执行超时
- **WHEN** 引擎注入的 `ctx.signal` 在 TCP 连接或 banner 读取过程中 abort
- **THEN** 系统 SHALL best-effort 关闭 socket记录 `matched=false`failure 的 kind 为 `error`phase 为 `connect``banner`message 包含超时信息
- **THEN** 系统 SHALL best-effort 关闭 socket记录 `matched=false`failure 的 kind 为 `error`phase 为 `connect``banner`message 包含超时信息,并在可收集时记录 observation
#### Scenario: duration 包含 banner 读取
- **WHEN** tcp target 开启 `readBanner` 且服务端延迟发送 banner
@@ -81,9 +81,9 @@
- **WHEN** 服务端发送的 banner 数据超过 `maxBannerBytes`
- **THEN** 系统 SHALL 停止读取并记录 `matched=false`、failure.kind=`error`、failure.phase=`banner` 的结构化错误
#### Scenario: banner statusDetail 截断展示
#### Scenario: banner detail 截断展示
- **WHEN** tcp target 成功读取到较长 banner
- **THEN** `statusDetail` SHALL 展示截断后的 banner 摘要,避免 UI 和历史记录写入过长文本
- **THEN** observation.banner SHALL 保存截断后的 banner 摘要API detail SHALL 展示截断后的 banner 摘要,避免 UI 展示过长文本
### Requirement: tcp expect 校验
系统 SHALL 支持 tcp 专属 expect包括 `connected``banner``durationMs`,并按 connected、banner、durationMs 的阶段顺序快速失败。`connected` SHALL 保持布尔状态语义,未配置时默认 `true``banner` MUST 使用共享 `ContentRules` 数组,并仅在 `tcp.readBanner: true` 时允许配置。`durationMs` SHALL 使用共享 `ValueMatcher` 校验包含连接和 banner 读取在内的完整执行耗时。

View File

@@ -75,11 +75,11 @@
- **THEN** 系统 SHALL 以配置错误退出,提示 udp.payload 与 udp.encoding 不匹配
### Requirement: udp checker 执行
系统 SHALL 使用 Bun connected UDP socket 向目标发送单个 datagram等待第一个 UDP 响应 datagram记录完整执行耗时并在发送失败、超时、资源超限或底层 socket 错误时产生结构化失败信息。
系统 SHALL 使用 Bun connected UDP socket 向目标发送单个 datagram等待第一个 UDP 响应 datagram记录完整执行耗时和 UDP observation,并在发送失败、超时、资源超限或底层 socket 错误时产生结构化失败信息。
#### Scenario: UDP 请求响应成功
- **WHEN** udp target 指向会返回 `PONG` 的 UDP 服务,且未配置 expect 或 `expect.responded``true`
- **THEN** 系统 SHALL 记录 `matched=true``durationMs` 和包含响应大小的 `statusDetail`,并关闭 socket
- **THEN** 系统 SHALL 记录 `matched=true``durationMs` 和包含响应大小的 observation,并关闭 socket
#### Scenario: 使用 hostname 执行 UDP 探测
- **WHEN** udp target 的 `udp.host` 为可解析域名或 `localhost`
@@ -91,19 +91,19 @@
#### Scenario: UDP 无响应且默认期望响应
- **WHEN** udp target 指向在 timeout 内不返回 UDP datagram 的服务,且未配置 expect 或 `expect.responded``true`
- **THEN** 系统 SHALL 在 `ctx.signal` abort 后记录 `matched=false`failure 的 kind 为 `error`phase 为 `response`message 包含超时或未响应信息
- **THEN** 系统 SHALL 在 `ctx.signal` abort 后记录 `matched=false`observation SHALL 包含 responded=false 和 errorfailure 的 kind 为 `error`phase 为 `response`message 包含超时或未响应信息
#### Scenario: 期望无响应且实际无响应
- **WHEN** udp target 配置 `expect.responded: false`,且 timeout 内未收到 UDP datagram
- **THEN** 系统 SHALL 记录 `matched=true`statusDetail SHALL 表示未收到响应
- **THEN** 系统 SHALL 记录 `matched=true`observation SHALL 包含 responded=falseAPI detail SHALL 表示未收到响应
#### Scenario: 期望无响应但实际收到响应
- **WHEN** udp target 配置 `expect.responded: false`,但收到了 UDP datagram
- **THEN** 系统 SHALL 记录 `matched=false`failure 的 kind 为 `mismatch`phase 为 `responded`
- **THEN** 系统 SHALL 记录 `matched=false`observation SHALL 包含 responded=true 和响应摘要,failure 的 kind 为 `mismatch`phase 为 `responded`
#### Scenario: UDP socket 底层错误
- **WHEN** Bun UDP socket 在发送或接收过程中触发 error 事件
- **THEN** 系统 SHALL best-effort 关闭 socket并记录 `matched=false`、failure.kind=`error` 和可读错误信息
- **THEN** 系统 SHALL best-effort 关闭 socket并记录 `matched=false`、failure.kind=`error` 和可读错误信息,并在可收集时记录 observation
#### Scenario: ICMP unreachable 不作为 UDP 响应
- **WHEN** 底层系统因目标端口不可达产生 ICMP unreachable
@@ -187,17 +187,17 @@
- **WHEN** YAML 中 udp target 的 expect 包含 `status: [200]``maxDurationMs: 1000` 或其他非 udp expect 字段
- **THEN** 系统 SHALL 以配置错误退出,提示 expect 包含未知字段
### Requirement: udp statusDetail 摘要
系统 SHALL 在 udp 执行后生成简短 statusDetail 摘要,展示关键结果并避免写入过长响应内容。
### Requirement: udp detail 摘要
系统 SHALL 在 udp API 序列化时从 observation 动态生成简短 detail 摘要,展示关键结果和执行耗时并避免返回过长响应内容。UDP observation SHALL 包含 durationMs 以支持 detail 构造。
#### Scenario: 收到响应的摘要
- **WHEN** udp target 收到 4 字节响应且完整执行耗时为 12ms
- **THEN** statusDetail SHALL 包含 `responded in 12ms``4 bytes`
- **THEN** detail SHALL 包含 `responded in 12ms``4 bytes`
#### Scenario: 未收到响应的摘要
- **WHEN** udp target 配置 `expect.responded: false` 且 timeout 内未收到 UDP datagram
- **THEN** statusDetail SHALL 包含 `no response` 和执行耗时
- **THEN** detail SHALL 包含 `no response` 和执行耗时
#### Scenario: 响应内容摘要截断
- **WHEN** udp target 收到较长响应内容
- **THEN** statusDetail SHALL 只展示按 `responseEncoding` 转换并截断后的响应摘要
- **THEN** detail SHALL 只展示按 `responseEncoding` 转换并截断后的响应摘要