1
0
Files
DiAL/openspec/specs/probe-api/spec.md
lanyuanxiaoyao cfca03b4d6 refactor: 规范审查与重组,合并细粒度规范,清理过时内容
- 合并 20+ 细粒度 spec 为粗粒度主题规范:dashboard、data-store、probe-engine、probe-api、probe-config 等
- 删除完全冗余规范:data-retention(被 probe-engine+data-store 覆盖)、backend-code-quality(DEVELOPMENT.md 已记录)
- 补充 http-checker 规范至完整标准(配置+执行+expect+校验+observation),匹配代码 440 行实现
- 清理 tcp/udp/llm checker 规范中已废弃 defaults 配置段的残留 Scenario
- 清理 checker-cohesion-structure 中的实现路径引用(src/server/...)
- 统一所有 spec 格式(## Purpose 开头,去除 # Capability/Title 形式)
- 更新 prompt-spec-review.md 审查提示文档
2026-05-22 18:55:18 +08:00

376 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## Purpose
定义拨测系统的 REST API 端点Dashboard 聚合 API、单目标指标 API、历史记录 API、Meta 信息 API、共享类型定义和 API 错误处理。
## Requirements
### Requirement: Dashboard 聚合 API
系统 SHALL 提供 `GET /api/dashboard` 端点,返回 Dashboard 首屏所需的总览统计和目标列表数据。targets 列表 SHALL 仅包含活跃目标。
#### Scenario: 获取 Dashboard 数据
- **WHEN** 客户端请求 `GET /api/dashboard?window=24h&recentLimit=30`
- **THEN** 系统 SHALL 返回 JSON 包含 summary 和 targets 字段targets 仅包含 active=1 的目标
#### Scenario: summary 字段
- **WHEN** Dashboard 响应包含 summary
- **THEN** summary SHALL 仅统计活跃目标total活跃目标数、up活跃正常目标数、down活跃异常目标数、lastCheckTime最近一次检查时间、incidents活跃目标在指定窗口内异常事件数、windowfrom/to/label字段
#### Scenario: targets 字段
- **WHEN** Dashboard 响应包含 targets
- **THEN** targets 数组中每个元素 SHALL 为活跃目标包含目标基本信息id、name、description、group、type、target、interval、latestCheck、stats、currentStreak 和 recentSamples 字段,其中 name 和 description 均为 null 或字符串
#### Scenario: target name 字段为 null
- **WHEN** 某个 target 未配置 `name` 或显式配置 `name: null`
- **THEN** Dashboard targets 响应中对应元素 SHALL 返回 `name: null`
#### Scenario: target description 字段
- **WHEN** 某个 target 配置了 `description`
- **THEN** Dashboard targets 响应中对应元素 SHALL 返回该 description 值
#### Scenario: window 参数缺失
- **WHEN** 客户端请求 `GET /api/dashboard` 未提供 window 参数
- **THEN** 系统 SHALL 默认使用 window=`24h`
#### Scenario: window 参数语义
- **WHEN** 系统处理 Dashboard 请求
- **THEN** 系统 SHALL 以服务端当前时间作为 window.to以 window 参数换算 window.from并在响应中回显 window.from、window.to、window.label
#### Scenario: window 参数有效值
- **WHEN** 客户端请求 Dashboard 端点并指定 window 参数
- **THEN** 系统 SHALL 接受 `24h` 作为有效值;其他值 SHALL 返回 400 状态码和错误信息
#### Scenario: 不支持的 window 参数
- **WHEN** 客户端请求 `GET /api/dashboard?window=abc`
- **THEN** 系统 SHALL 返回 400 状态码和错误信息
#### Scenario: recentLimit 参数缺失
- **WHEN** 客户端请求 `GET /api/dashboard?window=24h` 未提供 recentLimit 参数
- **THEN** 系统 SHALL 默认使用 recentLimit=30
#### Scenario: 不支持的 recentLimit 参数
- **WHEN** 客户端请求 `GET /api/dashboard?recentLimit=0` 或超过系统上限的 recentLimit
- **THEN** 系统 SHALL 返回 400 状态码和错误信息
#### Scenario: 目标无历史记录
- **WHEN** 某目标尚未执行过任何拨测
- **THEN** 其 latestCheck 为 nullrecentSamples 为空数组stats.totalChecks 为 0stats.availability 为 0currentStreak 为 null
### Requirement: Dashboard 指标字段
Dashboard API SHALL 返回基于时间窗口计算的目标统计和连续状态字段。
#### Scenario: 目标 stats 字段
- **WHEN** Dashboard 响应包含目标 stats
- **THEN** stats SHALL 包含 totalChecks、upChecks、downChecks、availability 字段,且这些字段 SHALL 基于请求 window 对应的时间范围计算
#### Scenario: 目标 currentStreak 字段
- **WHEN** Dashboard 响应包含目标 currentStreak
- **THEN** currentStreak SHALL 为 `{ up: boolean, count: number, capped?: boolean }` 或 null
#### Scenario: currentStreak 达到 recentLimit
- **WHEN** 连续状态次数达到 recentLimit 上限
- **THEN** currentStreak.capped SHALL 为 true
#### Scenario: recentSamples 字段
- **WHEN** Dashboard 响应包含 recentSamples
- **THEN** 每个 recentSamples 元素 SHALL 包含 timestamp、durationMs、up 字段,其中 up 为 boolean 且等于 matched
### Requirement: 单目标指标 API
系统 SHALL 提供 `GET /api/targets/:id/metrics` 端点,返回单个目标在指定时间窗口内的概览统计和趋势数据。仅活跃目标的指标 SHALL 可查询。
#### Scenario: 获取目标指标
- **WHEN** 客户端请求 `GET /api/targets/1/metrics?from=ISO&to=ISO&bucket=1h` 且该目标为活跃目标
- **THEN** 系统 SHALL 返回 JSON 对象包含 targetId、window、stats、trend 字段
#### Scenario: from 或 to 参数缺失
- **WHEN** 客户端请求 `GET /api/targets/1/metrics` 未提供 from 或 to 参数
- **THEN** 系统 SHALL 返回 400 状态码和错误信息
#### Scenario: 目标不存在或非活跃
- **WHEN** 客户端请求 `GET /api/targets/999/metrics?from=ISO&to=ISO` 且该目标不存在或 active=0
- **THEN** 系统 SHALL 返回 404 状态码和错误信息
#### Scenario: 无效的目标 ID
- **WHEN** 客户端请求 `GET /api/targets/abc/metrics?from=ISO&to=ISO`
- **THEN** 系统 SHALL 返回 400 状态码和错误信息
#### Scenario: bucket 参数缺失
- **WHEN** 客户端请求 `GET /api/targets/1/metrics?from=ISO&to=ISO` 未提供 bucket 参数
- **THEN** 系统 SHALL 默认使用 bucket=`1h`
#### Scenario: 不支持的 bucket 参数
- **WHEN** 客户端请求 `GET /api/targets/1/metrics?from=ISO&to=ISO&bucket=5m`
- **THEN** 系统 SHALL 返回 400 状态码和错误信息
### Requirement: 指标统计字段
单目标指标 API SHALL 返回基于时间窗口计算的完整统计字段。
#### Scenario: stats 字段
- **WHEN** metrics 响应包含 stats
- **THEN** stats SHALL 包含 totalChecks、upChecks、downChecks、availability、avgDurationMs、p95DurationMs、p99DurationMs、mttr、longestOutage、incidentCount、currentStreak 字段
#### Scenario: trend 字段
- **WHEN** metrics 响应包含 trend
- **THEN** trend SHALL 为数组,每个元素包含 bucketStart、avgDurationMs、minDurationMs、maxDurationMs、availability、totalChecks、upChecks、downChecks 字段
### Requirement: P95/P99 延迟计算
系统 SHALL 在后端应用层计算 P95 和 P99 延迟百分位数。
#### Scenario: 正常计算 P95
- **WHEN** 时间窗口内存在成功检查记录matched=1 且 duration_ms 不为 null
- **THEN** 系统 SHALL 取出所有成功检查的 duration_ms在后端应用层排序后取第 95 百分位值返回为 p95DurationMs
#### Scenario: 正常计算 P99
- **WHEN** 时间窗口内存在成功检查记录
- **THEN** 系统 SHALL 取第 99 百分位值返回为 p99DurationMs
#### Scenario: 无成功检查记录
- **WHEN** 时间窗口内无 matched=1 且 duration_ms 不为 null 的记录
- **THEN** p95DurationMs 和 p99DurationMs SHALL 返回 null
#### Scenario: 百分位计算方法
- **WHEN** 计算第 N 百分位
- **THEN** 系统 SHALL 将 duration_ms 升序排列,取 index = ceil(count * N / 100) - 1 位置的值
### Requirement: MTTR 计算
系统 SHALL 在后端应用层计算平均恢复时间Mean Time To Recovery
#### Scenario: 存在已恢复的故障段
- **WHEN** 时间窗口内存在至少一个已恢复的故障段(连续 matched=0 后跟 matched=1
- **THEN** 系统 SHALL 计算所有已恢复故障段的平均持续时间(从首个 matched=0 的 timestamp 到恢复后首个 matched=1 的 timestamp 之差),返回为 mttr毫秒
#### Scenario: 无已恢复的故障段
- **WHEN** 时间窗口内无已恢复的故障段(全部正常,或当前仍在故障中且无历史恢复)
- **THEN** mttr SHALL 返回 null
#### Scenario: 当前正在故障中
- **WHEN** 时间窗口内最后一段故障尚未恢复
- **THEN** 该未恢复的故障段 SHALL 不计入 MTTR 平均值
#### Scenario: 窗口起始即为故障且后续恢复
- **WHEN** 时间窗口内第一条记录即为 matched=0故障跨越了 from 边界),且该故障段在窗口内恢复
- **THEN** 该故障段 SHALL 不计入 MTTR 平均值(因无法确定真实故障开始时间),但 SHALL 计入 incidentCount
### Requirement: 最长故障时长
系统 SHALL 在后端应用层计算时间窗口内最长的单次故障持续时间。
#### Scenario: 存在故障段
- **WHEN** 时间窗口内存在故障段
- **THEN** 系统 SHALL 返回最长故障段的持续时间为 longestOutage毫秒
#### Scenario: 无故障
- **WHEN** 时间窗口内无 matched=0 的记录
- **THEN** longestOutage SHALL 返回 null
#### Scenario: 窗口起始即为故障
- **WHEN** 时间窗口内第一条记录即为 matched=0
- **THEN** 该故障段的持续时间 SHALL 从 from 参数开始计算
#### Scenario: 当前正在故障中
- **WHEN** 最后一段故障尚未恢复
- **THEN** 该故障段的持续时间 SHALL 计算为从故障开始到时间窗口 to 参数的时间差
### Requirement: 故障事件计数
系统 SHALL 在后端应用层计算时间窗口内的故障事件次数。
#### Scenario: 计算故障事件数
- **WHEN** 时间窗口内存在状态翻转matched 从 1 变为 0
- **THEN** 系统 SHALL 返回翻转次数为 incidentCount
#### Scenario: 无故障事件
- **WHEN** 时间窗口内所有检查均为 matched=1
- **THEN** incidentCount SHALL 返回 0
#### Scenario: 窗口起始即为故障
- **WHEN** 时间窗口内第一条记录即为 matched=0 且无前序记录可判断翻转
- **THEN** 该故障 SHALL 计为 1 次事件
#### Scenario: 连续异常只计一次
- **WHEN** 某目标连续 10 次 matched=0
- **THEN** 该连续异常段 SHALL 仅计为 1 次事件
### Requirement: 当前连续状态
系统 SHALL 返回目标当前的连续状态信息。
#### Scenario: 当前连续正常
- **WHEN** 目标最近的检查记录连续为 matched=1
- **THEN** currentStreak SHALL 返回 `{ up: true, count: N }`N 为连续正常的检查次数
#### Scenario: 当前连续异常
- **WHEN** 目标最近的检查记录连续为 matched=0
- **THEN** currentStreak SHALL 返回 `{ up: false, count: N }`N 为连续异常的检查次数
#### Scenario: 连续状态达到取数上限
- **WHEN** 连续状态次数达到后端取数或计算上限
- **THEN** currentStreak SHALL 返回 `{ up: boolean, count: N, capped: true }`,前端据此展示上限标记
#### Scenario: 无检查记录
- **WHEN** 目标没有任何检查记录
- **THEN** currentStreak SHALL 返回 null
### Requirement: 趋势数据应用层分桶
系统 SHALL 在后端应用层按 UTC 小时分桶生成趋势数据。
#### Scenario: 按小时生成趋势
- **WHEN** metrics 请求 bucket=`1h`
- **THEN** 系统 SHALL 按 UTC 小时生成 trend 数组,每个点包含该小时内的 totalChecks、upChecks、downChecks、availability、avgDurationMs、minDurationMs、maxDurationMs
#### Scenario: 小时内无成功检查
- **WHEN** 某小时内存在检查记录但无成功检查记录
- **THEN** avgDurationMs、minDurationMs、maxDurationMs SHALL 返回 nullavailability SHALL 基于 upChecks/totalChecks 返回 0
#### Scenario: 小时内无检查记录
- **WHEN** 某小时内没有任何检查记录
- **THEN** 系统 MAY 不返回该小时对应的 trend 点
### Requirement: 无数据口径
系统 SHALL 对无数据窗口返回稳定的空指标口径。
#### Scenario: 窗口内无检查记录
- **WHEN** 指定时间窗口内没有任何检查记录
- **THEN** stats SHALL 返回 totalChecks=0、upChecks=0、downChecks=0、availability=0、avgDurationMs=null、p95DurationMs=null、p99DurationMs=null、mttr=null、longestOutage=null、incidentCount=0、currentStreak=nulltrend SHALL 返回空数组
### Requirement: 历史记录 API
系统 SHALL 保留 `GET /api/targets/:id/history` 端点,支持时间范围筛选和分页返回指定目标的拨测记录。仅活跃目标的历史记录 SHALL 可查询。
#### Scenario: 获取指定时间范围内的历史记录
- **WHEN** 客户端请求 `GET /api/targets/1/history?from=ISO&to=ISO&page=1&pageSize=20` 且该目标为活跃目标
- **THEN** 系统 SHALL 返回带分页信息的历史记录,包含 items、total、page、pageSize按时间倒序排列
#### Scenario: 使用默认分页参数
- **WHEN** 客户端请求 `GET /api/targets/1/history?from=ISO&to=ISO`(未指定 page 或 pageSize
- **THEN** 系统 SHALL 使用默认 page=1, pageSize=20
#### Scenario: from 或 to 参数缺失
- **WHEN** 客户端请求 `GET /api/targets/1/history` 未提供 from 或 to 参数
- **THEN** 系统 SHALL 返回 400 状态码和错误信息
#### Scenario: 目标不存在或非活跃
- **WHEN** 客户端请求 `GET /api/targets/999/history?from=ISO&to=ISO` 且该目标不存在或 active=0
- **THEN** 系统 SHALL 返回 404 状态码和错误信息
### 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
### Requirement: 保留健康检查端点
系统 SHALL 保留 `GET /health` 端点,不受拨测功能影响。
#### Scenario: 访问健康检查
- **WHEN** 客户端请求 `GET /health`
- **THEN** 系统 SHALL 返回与之前格式一致的健康检查响应
### Requirement: API 错误处理
系统 SHALL 对不存在的目标 ID、非活跃目标、无效参数和超出范围的分页参数返回适当的 HTTP 错误响应。非活跃目标与不存在的目标 SHALL 返回相同的 404 响应。
#### Scenario: 查询不存在的目标
- **WHEN** 客户端请求 `GET /api/targets/999/metrics?from=ISO&to=ISO`
- **THEN** 系统 SHALL 返回 404 状态码和错误信息
#### Scenario: 查询非活跃目标
- **WHEN** 客户端请求 `GET /api/targets/<id>/metrics?from=ISO&to=ISO` 且该目标 active=0
- **THEN** 系统 SHALL 返回 404 状态码和错误信息
#### Scenario: 无效的 from/to 参数
- **WHEN** 客户端请求 `GET /api/targets/1/metrics?from=invalid&to=ISO`
- **THEN** 系统 SHALL 返回 400 状态码和错误信息
#### Scenario: from 晚于 to
- **WHEN** 客户端请求 `GET /api/targets/1/metrics?from=<较晚时间>&to=<较早时间>`
- **THEN** 系统 SHALL 返回 400 状态码和错误信息,提示 from 必须早于 to
#### Scenario: 无效的分页参数
- **WHEN** 客户端请求 `GET /api/targets/1/history?from=ISO&to=ISO&page=abc`
- **THEN** 系统 SHALL 返回 400 状态码和错误信息
#### Scenario: pageSize 超过上限
- **WHEN** 客户端请求 `GET /api/targets/1/history?from=ISO&to=ISO&pageSize=201`
- **THEN** 系统 SHALL 返回 400 状态码和错误信息,提示 pageSize 不能超过 200
#### Scenario: pageSize 等于上限
- **WHEN** 客户端请求 `GET /api/targets/1/history?from=ISO&to=ISO&pageSize=200`
- **THEN** 系统 SHALL 正常返回数据
#### Scenario: 无效的目标 ID
- **WHEN** 客户端请求 `GET /api/targets/abc/metrics?from=ISO&to=ISO`
- **THEN** 系统 SHALL 返回 400 状态码和错误信息
### Requirement: 失败信息 API 契约
系统 SHALL 通过 API 返回结构化 failure 信息,供 Dashboard 展示和后续排查。
#### Scenario: 返回 expect 不匹配信息
- **WHEN** 最近一次检查结果包含 failure.kind=`mismatch`
- **THEN** `/api/targets``/api/targets/:id/history` SHALL 返回该 failure 的 kind、phase、path、expected、actual、message 字段
#### Scenario: 无失败信息
- **WHEN** 检查结果 matched=true
- **THEN** API SHALL 返回 failure 为 null
### Requirement: Meta 信息 API
系统 SHALL 提供 `GET /api/meta` 端点,返回系统运行时元数据,包括应用版本号和 checker 类型列表。未匹配 method SHALL 按 API 通配符处理为 JSON 404。
#### Scenario: 获取 checker 类型列表和版本号
- **WHEN** 客户端请求 `GET /api/meta`
- **THEN** 系统 SHALL 返回 JSON `{ checkerTypes: string[], version: string }`,其中 `checkerTypes` 包含所有已注册的 checker 类型标识符,`version` 为当前运行实例的 `MAJOR.MINOR.PATCH` 应用版本
#### Scenario: 类型列表来源
- **WHEN** 系统启动并注册了 checker
- **THEN** `/api/meta` 返回的 `checkerTypes` SHALL 与 `CheckerRegistry.supportedTypes` 完全一致
#### Scenario: 版本号来源
- **WHEN** 系统启动并确定应用版本
- **THEN** `/api/meta` 返回的 `version` SHALL 与启动时注入的应用版本完全一致
#### Scenario: 不支持的 method 请求
- **WHEN** 客户端使用 POST/PUT/DELETE/HEAD 等未声明 method 请求 `/api/meta`
- **THEN** `/api/*` 通配符 SHALL 返回 JSON 404 响应
### Requirement: MetaResponse 共享类型
系统 SHALL 在 `src/shared/api.ts` 中定义 `MetaResponse` 类型。
#### Scenario: MetaResponse 类型定义
- **WHEN** 前后端引用 `MetaResponse` 类型
- **THEN** 该类型 SHALL 包含 `checkerTypes: string[]``version: string` 字段