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

@@ -0,0 +1,158 @@
## Purpose
定义 CheckResult 的 observation 数据模型、各 checker 类型 observation 结构、截断策略、序列化规则和 detail 动态构造机制。
## Requirements
### Requirement: Observation 数据模型
CheckResult SHALL 包含 `observation: Record<string, unknown> | null` 字段,用于承载 checker 执行过程中收集的结构化观测数据。observation 为 null 表示执行过程中无法形成有意义的领域观测数据(如进程 spawn 失败、内部异常、请求在拿到响应前失败且无可记录元数据等场景)。各 checker SHALL 自行定义 observation 的内部结构,不做跨 checker 类型的统一约束。
#### Scenario: 正常执行返回 observation
- **WHEN** checker 执行成功、expect 断言失败或产生可收集上下文的负向结果
- **THEN** CheckResult.observation SHALL 包含该 checker 类型定义的完整观测数据
#### Scenario: 异常执行返回 null observation
- **WHEN** checker 执行过程中发生无法收集领域观测数据的异常
- **THEN** CheckResult.observation SHALL 为 null
### Requirement: HTTP Checker Observation
HTTP checker 的 observation SHALL 包含 statusCodenumber、headersRecord<string, string>截断、bodyPreviewstring | null截断、contentTypestring | null、contentLengthnumber | null
#### Scenario: 正常 HTTP 响应
- **WHEN** HTTP 请求成功返回
- **THEN** observation SHALL 包含响应状态码、截断后的响应 headers、响应体预览、Content-Type 和 Content-Length即使未配置 body expect 也 SHALL 采集 bodyPreview
#### Scenario: HTTP 请求失败
- **WHEN** HTTP 请求因网络错误或超时失败
- **THEN** observation SHALL 为 null
### Requirement: TCP Checker Observation
TCP checker 的 observation SHALL 包含 connectedboolean、connectTimeMsnumber | null、bannerstring | null截断、errorstring | null
#### Scenario: TCP 连接成功且读取 banner
- **WHEN** TCP 连接成功并读取到 banner
- **THEN** observation SHALL 包含 connected=true、连接耗时、截断后的 banner 内容
#### Scenario: TCP 连接失败
- **WHEN** TCP 连接失败
- **THEN** observation SHALL 包含 connected=false 和错误信息
### Requirement: UDP Checker Observation
UDP checker 的 observation SHALL 包含 respondedboolean、durationMsnumber、responseSizenumber | null、responsePreviewstring | null截断、sourceAddressstring | null、sourcePortnumber | null、errorstring | null。durationMs 用于 API 序列化层生成包含耗时的 UDP detail 摘要。
#### Scenario: UDP 收到响应
- **WHEN** UDP 发送数据后收到响应
- **THEN** observation SHALL 包含 responded=true、响应大小、截断后的响应预览、来源地址和端口
#### Scenario: UDP 未收到响应
- **WHEN** UDP 发送数据后超时未收到响应
- **THEN** observation SHALL 包含 responded=false
### Requirement: Ping Checker Observation
Ping/ICMP checker 的 observation SHALL 包含 aliveboolean、transmittednumber、receivednumber、packetLossnumber、avgLatencyMsnumber | null、maxLatencyMsnumber | null、minLatencyMsnumber | null、errorstring | null。API registry type SHALL 仍为 `ping`
#### Scenario: Ping 正常返回统计
- **WHEN** ping 命令成功执行并解析出统计数据
- **THEN** observation SHALL 包含完整的 PingStats 字段
#### Scenario: Ping 命令失败
- **WHEN** ping 命令未找到或超时
- **THEN** observation SHALL 为 null 或包含 error 字段
### Requirement: DB Checker Observation
DB checker 的 observation SHALL 包含 connectedboolean、rowCountnumber | null、rowsPreviewunknown[] | null截断前 N 行、errorstring | null
#### Scenario: 数据库查询成功
- **WHEN** 数据库连接和查询成功
- **THEN** observation SHALL 包含 connected=true、行数、截断后的行预览
#### Scenario: 仅探活无查询
- **WHEN** 数据库配置仅探活连接(无 query
- **THEN** observation SHALL 包含 connected=truerowCount 和 rowsPreview 为 null
### Requirement: CMD Checker Observation
CMD checker 的 observation SHALL 包含 exitCodenumber | null、stdoutPreviewstring | null截断、stderrPreviewstring | null截断、errorstring | null
#### Scenario: 命令正常执行
- **WHEN** 命令执行完成
- **THEN** observation SHALL 包含退出码、截断后的 stdout 和 stderr 预览
#### Scenario: 命令 spawn 失败
- **WHEN** 命令进程无法启动
- **THEN** observation SHALL 为 null
### Requirement: LLM Checker Observation
LLM checker SHALL 保留执行期 `LlmCheckObservation.outputText` 完整文本用于 expect 校验,并从执行期 observation 派生持久化 observation。持久化 observation SHALL 包含 providerstring、modestring、modelstring、http{ status, statusText, headers } | nullheaders 截断、finishReasonstring | null、rawFinishReasonstring | null、outputPreviewstring | null截断、outputLengthnumber | null、usage{ inputTokens, outputTokens, totalTokens } | null、stream{ completed, firstTokenMs } | null、warningsstring[])。
#### Scenario: LLM HTTP 模式成功
- **WHEN** LLM 以 HTTP 模式成功执行
- **THEN** observation SHALL 包含 provider、mode、model、HTTP 元数据、finish 原因、截断后的输出预览、完整输出长度和 token 用量
#### Scenario: LLM Stream 模式成功
- **WHEN** LLM 以 stream 模式成功执行
- **THEN** observation SHALL 额外包含 stream 观测数据completed、firstTokenMs
### Requirement: Observation 截断策略
各 checker SHALL 对 observation 中的大文本和集合字段执行截断,防止存储膨胀。
#### Scenario: HTTP body 截断
- **WHEN** HTTP 响应体超过 1024 字符
- **THEN** bodyPreview SHALL 截断为前 1024 字符
#### Scenario: HTTP headers 截断
- **WHEN** HTTP 响应 headers 超过 20 个
- **THEN** headers SHALL 仅保留前 20 个
#### Scenario: TCP banner 截断
- **WHEN** TCP banner 内容超过 256 字符
- **THEN** banner SHALL 截断为前 256 字符
#### Scenario: UDP response 截断
- **WHEN** UDP 响应预览超过 512 字符
- **THEN** responsePreview SHALL 截断为前 512 字符
#### Scenario: DB rows 截断
- **WHEN** 查询返回超过 5 行
- **THEN** rowsPreview SHALL 仅保留前 5 行
#### Scenario: CMD stdout/stderr 截断
- **WHEN** stdout 或 stderr 超过 1024 字符
- **THEN** 对应 Preview 字段 SHALL 截断为前 1024 字符
#### Scenario: LLM output 截断
- **WHEN** LLM 输出文本超过 512 字符
- **THEN** outputPreview SHALL 截断为前 512 字符
#### Scenario: LLM headers 截断
- **WHEN** LLM 响应 headers 超过 20 个
- **THEN** headers SHALL 仅保留前 20 个
### Requirement: Observation 序列化规则
observation SHALL 使用 JSON.stringify 序列化为 TEXT 格式存入 SQLite使用 JSON.parse 反序列化读出。不引入额外的序列化依赖。
#### Scenario: 写入 observation
- **WHEN** 存储 CheckResult 到数据库
- **THEN** observation 字段 SHALL 使用 JSON.stringify 序列化后存入 TEXT 列observation 为 null 时 SHALL 存入 SQL NULL
#### Scenario: 读取 observation
- **WHEN** 从数据库读取 CheckResult
- **THEN** observation 字段 SHALL 使用 JSON.parse 反序列化SQL NULL 值 SHALL 映射为 null
#### Scenario: 使用 SQLite CLI 直接查看
- **WHEN** 开发者使用 sqlite3 CLI 工具查看 check_results 表
- **THEN** observation 列 SHALL 为人可读的 JSON 文本
### Requirement: Detail 动态构造
CheckResult SHALL 包含 `detail: string | null` 字段(替代原 statusDetail该字段 SHALL 不持久化到数据库,而是在 API 序列化层根据 target type 从 observation 动态构造。每个 checker SHALL 提供 `buildDetail(observation)` 方法,定义该 checker 类型的人可读摘要格式。
#### Scenario: API 返回 detail 字段
- **WHEN** API 序列化 CheckResult 返回给前端
- **THEN** 系统 SHALL 根据 target type 调用对应 checker 的 buildDetail 方法,从 observation 动态生成 detail 字段
#### Scenario: observation 为 null 时
- **WHEN** observation 为 null
- **THEN** detail SHALL 为 null
#### Scenario: 各 checker 的 detail 格式
- **WHEN** 各 checker 的 buildDetail 被调用
- **THEN** HTTP SHALL 返回 `"HTTP {statusCode}"` 格式TCP SHALL 返回连接状态和 banner 摘要UDP SHALL 返回响应状态和大小摘要Ping SHALL 返回存活状态、平均延迟和丢包率摘要DB SHALL 返回连接状态或行数摘要CMD SHALL 返回 `"exitCode={N}"` 格式LLM SHALL 返回 provider、mode、状态码、finish 原因、输出长度和 token 用量摘要

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` 转换并截断后的响应摘要