1
0

refactor: 移除 success 字段,简化为 matched 单层判定模型

This commit is contained in:
2026-05-11 13:12:55 +08:00
parent 548b44d28e
commit 35ba56888b
93 changed files with 3893 additions and 103 deletions

View File

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

View File

@@ -0,0 +1,121 @@
## Context
当前项目是 Bun + TypeScript 的最小工程,入口文件只有 `index.ts`,尚未形成前端、后端、共享类型、测试和构建的边界。目标是在保持 Bun 单文件部署优势的同时,引入完整的 Vite + React 前端开发体验。
业界成熟实践通常不是让后端参与前端 HMR而是开发期让 Vite dev server 独立承载前端,使用 proxy 将 `/api/*` 转发给后端生产期由前端构建生成静态资源再由后端服务这些资源。Go/Rust 生态常用类似 `embed` 的方式把 Vite `dist/` 打入单个二进制Bun 可通过 `bun build --compile` 与 embedded files/full-stack asset 能力实现同类目标。
```
开发期
Browser
|
v
Vite dev server :5173
|-- React HMR
|-- /api/* proxy
|
v
Bun API server :3000
生产期
Browser
|
v
gateway-checker executable
|-- /api/* Bun API
|-- /health 健康检查
|-- /assets/* Vite 静态资源
|-- /* React SPA fallback
```
## Goals / Non-Goals
**Goals:**
- 建立 Vite + React + TypeScript 前端应用结构,保留开发期 HMR。
- 建立 Bun 后端服务结构,统一承载 API、健康检查和生产前端资源。
- 提供一个可运行 demo前端页面通过 `/api/demo` 调用后端并展示响应。
- 建立前后端共享类型边界,避免重复定义基础接口类型。
- 建立生产构建链路,输出单个 Bun standalone executable。
- 保持前端可拆离:前端只通过 HTTP `/api/*` 依赖后端,不直接 import 后端实现。
- 更新 README记录结构、命令、测试、构建和运行方式。
**Non-Goals:**
- 不引入 SSR、React Server Components、Next.js 或其他全栈框架。
- 不引入数据库、认证、用户系统或业务功能。
- 不要求一次性完成多平台发布矩阵,只定义可扩展的 target 机制。
- 不把运行期配置、日志或可变数据嵌入 executable。
- 不在开发期强制单端口访问;开发期可以使用 Vite 端口作为浏览器入口。
## Decisions
### Decision: 使用 Vite + React 作为前端开发框架
采用 Vite + React + TypeScript开发期由 Vite 提供 HMR生产期由 `vite build` 输出静态资源。React 适合后续构建复杂管理界面、状态页、图表和交互式检测视图。
替代方案:使用 Bun 原生 HTML imports。该方案更简单、依赖更少但前端生态、插件、测试和组件体系弱于 Vite。
替代方案:使用 Next.js。该方案能力更强但 SSR/路由/部署模型与“Bun 单 executable”目标存在额外摩擦。
### Decision: 开发期 Vite proxy `/api/*` 到 Bun 后端
浏览器开发入口默认使用 Vite dev server前端请求统一使用相对路径 `/api/*`。Vite 负责把这些请求代理到 Bun 后端服务,从而保持同源开发体验,避免 CORS 和硬编码后端地址。
替代方案Bun 后端反向代理 Vite dev server。该方案可以让开发期也统一一个端口但会增加胶水代码并且容易干扰 Vite HMR 行为。
### Decision: 生产期由 Bun 服务 Vite `dist/`
生产构建先执行 Vite build再让 Bun 后端服务 `index.html``assets/*` 和其他静态资源。非 API、非静态资源路径 fallback 到 `index.html`,用于支持 React SPA 路由刷新。
替代方案:前端独立部署到 CDN。该方案扩展性更好但不满足当前“一个可执行程序包含前后端”的目标。
### Decision: 单 executable 是发布形态,不是代码耦合方式
前端和后端在源码层保持清晰边界,只通过 HTTP API 和共享类型协作。打包层负责将 Vite 产物嵌入 Bun executable。这样未来若需要 CDN、独立前端部署或多客户端复用 API不需要重写后端业务代码。
替代方案:后端源码直接 import 前端源码或前端模块。该方案短期简单,但会模糊运行时边界,增加后续拆离成本。
### Decision: API 路径统一保留在 `/api/*`
所有业务 API 使用 `/api/*` 前缀,健康检查使用 `/health`。demo API 使用 `/api/demo`,返回前端可展示的 JSON 响应。生产期路由优先级为 API、健康检查、静态资源、SPA fallback。未命中的 `/api/*` 必须返回 JSON 404不能 fallback 到前端页面。
替代方案API 与页面路径混排。该方案不利于 Vite proxy、生产 fallback 和未来前端独立部署。
### Decision: 运行配置使用环境变量或 CLI 参数
host、port、日志级别等运行期配置不嵌入 executable优先从 CLI 参数或环境变量读取。executable 内只包含只读程序代码和前端静态资源。
替代方案:构建期写死配置。该方案部署简单,但不同环境需要重新构建,且不利于发布同一个二进制。
### Decision: demo 是验收基线而不是业务功能
demo 只证明前端开发、后端 API、生产静态服务和 executable 打包链路能跑通。页面应展示来自 `/api/demo` 的后端响应,并在 README 中记录开发期访问方式和 executable 运行后的验证方式。
替代方案:只搭建空白 React 页面和空 API。该方案能证明结构存在但不能证明前后端开发和打包后联通链路真实可用。
## Risks / Trade-offs
- [Risk] Bun standalone executable 与 Vite `dist/` 嵌入方式相对 Go `embed` 更年轻。→ Mitigation: 先实现最小静态资源嵌入和端到端构建测试,再扩展多平台构建。
- [Risk] Vite hashed assets、SPA fallback 和 Bun 静态路由可能出现路径映射问题。→ Mitigation: 对 `/`, `/assets/*`, 前端路由刷新和 `/api/*` 404 编写测试或构建后验证。
- [Risk] 依赖数量会明显增加。→ Mitigation: 初期只引入 Vite、React、React DOM 和必要类型,不引入 UI 组件库、状态管理或路由库,除非后续需求明确。
- [Risk] 单 executable 会把前端资源大小计入二进制。→ Mitigation: 保留 Vite 产物压缩能力,后续可按需启用分离部署或 CDN。
- [Risk] 开发期前端和后端是两个进程,启动命令更复杂。→ Mitigation: 提供 `dev:web``dev:server``dev` 聚合脚本,并在 README 中说明。
## Migration Plan
1. 保留当前最小入口语义,重构为新的 server 入口。
2. 新增 web、server、shared 和 scripts 目录结构。
3. 引入最小 Vite + React 依赖并配置开发代理。
4. 实现 Bun API、健康检查和生产静态资源服务。
5. 增加测试和构建验证。
6. 更新 README 作为项目结构和命令的权威说明。
回滚策略:如果 Vite 集成阻塞,可以保留 Bun 后端结构,移除 web 目录和前端构建脚本,退回 Bun 原生 HTML imports 或后端-only 形态。
## Open Questions
- 前端是否需要路由库,例如 React Router还是先保持单页面组件状态
- 是否需要 UI 组件库,例如 TDesign、shadcn/ui 或保持纯 CSS
- 生产 executable 首期目标平台是当前 macOS还是同时需要 Linux x64/arm64

View File

@@ -0,0 +1,33 @@
## Why
当前项目只有 Bun + TypeScript 的最小入口,尚不能支撑完整的前后端服务开发。引入 Vite + React 开发体验,并保持 Bun 后端可打包为单个可执行程序,可以同时满足本地快速迭代、前后端同源集成和简单部署的目标。
## What Changes
- 新增基于 Vite + React + TypeScript 的前端应用开发能力,开发期保留 Vite HMR。
- 新增 Bun 后端服务作为 API 与生产静态资源承载层API 统一位于 `/api/*`
- 新增开发期前端代理后端 API 的同源调用约定,避免前端写死后端地址。
- 新增生产期构建链路:先构建前端静态资源,再将后端与前端产物打包为单个 Bun standalone executable。
- 新增可验收 demo前端页面调用后端 API 并展示响应,开发期和 executable 运行期都能验证前后端联通。
- 新增 SPA fallback 行为:生产环境非 API 前端路由返回前端入口页面。
- 更新 README记录项目结构、开发命令、测试命令、构建命令与部署方式。
## Capabilities
### New Capabilities
- `frontend-development-workflow`: 约定 Vite + React 前端开发、API proxy、共享类型和本地开发命令。
- `fullstack-app-runtime`: 约定 Bun 服务在运行期同时提供 API、健康检查、静态资源和 SPA fallback。
- `single-executable-packaging`: 约定生产构建将 Vite 前端产物和 Bun 后端服务打包为单个可执行程序。
### Modified Capabilities
无。当前 `openspec/specs/` 为空,没有既有 capability 需要修改。
## Impact
- 代码结构将从单入口 `index.ts` 扩展为前端、后端、共享类型和构建脚本的模块化结构。
- 依赖将新增 Vite、React、React DOM 及相关 TypeScript 类型;测试依赖按实现方案最小化引入。
- 开发脚本将覆盖前端 dev server、后端 dev server、并行开发、测试和生产构建。
- 生产产物将从直接运行 TypeScript 入口变为 `dist/` 下的平台相关 executable。
- demo 验收将覆盖开发期联调和生产 executable 运行后的前端页面、API 与健康检查。
- README 需要同步说明模块结构、API 路径约定、构建产物和运行参数。

View File

@@ -0,0 +1,63 @@
## ADDED Requirements
### Requirement: Vite React 开发服务器
系统 SHALL 提供基于 Vite + React + TypeScript 的前端开发工作流,并支持热模块替换。
#### Scenario: 启动前端开发服务器
- **WHEN** 开发者启动前端开发命令
- **THEN** 前端 SHALL 由 Vite 提供服务,并启用 React 热模块替换
#### Scenario: 构建前端静态资源
- **WHEN** 开发者运行前端生产构建命令
- **THEN** 系统 SHALL 产出可由 Bun 后端服务的前端静态资源
### Requirement: 前端开发期 API 代理
前端开发服务器 SHALL 在本地开发期间将 `/api/*` 请求代理到 Bun 后端服务。
#### Scenario: 前端开发期调用 API
- **WHEN** 浏览器从 Vite 开发源请求 `/api/demo`
- **THEN** Vite SHALL 将请求转发到 Bun 后端服务,且不需要浏览器 CORS 配置
#### Scenario: 开发期访问非 API 前端路由
- **WHEN** 浏览器从 Vite 开发源请求非 API 前端路由
- **THEN** Vite SHALL 将该请求作为前端应用流量处理,而不是转发到后端
### Requirement: 前端使用相对 API 路径
除非有文档化的部署配置覆盖该行为,前端代码 MUST 通过相对 `/api/*` URL 调用后端 API。
#### Scenario: 前端获取后端数据
- **WHEN** 前端代码调用后端 API
- **THEN** 请求 URL 默认 MUST 使用相对 `/api/*` 路径
#### Scenario: 运行环境变化
- **WHEN** host 或 port 在开发环境和生产环境之间变化
- **THEN** 前端 API 调用 SHALL 无需修改源码即可继续工作
### Requirement: 端到端开发 demo
项目 SHALL 提供一个可见的开发 demo用于证明 React 前端可以通过 Vite 代理调用 Bun 后端。
#### Scenario: Demo 页面展示后端响应
- **WHEN** 开发者启动文档化的开发命令并打开前端 URL
- **THEN** 页面 SHALL 调用 `/api/demo` 并展示 Bun 后端返回的数据
#### Scenario: 开发期后端不可用
- **WHEN** 前端 demo 无法访问 `/api/demo`
- **THEN** 页面 SHALL 展示清晰的错误状态,而不是静默显示为成功
### Requirement: 集成开发命令
项目 SHALL 提供一个文档化命令,用于在 demo 开发期间同时运行前端和后端。
#### Scenario: 启动全栈开发
- **WHEN** 开发者运行文档化的全栈开发命令
- **THEN** 系统 SHALL 启动 Vite 前端开发服务器和 `/api/demo` 所需的 Bun 后端服务器
### Requirement: 共享 TypeScript 契约
项目 SHALL 为前端和后端共同使用的请求与响应类型提供共享 TypeScript 边界。
#### Scenario: 定义 API 响应结构
- **WHEN** 前端和后端都需要某个 API 响应类型
- **THEN** 该类型 SHALL 定义在 shared 模块中,而不是在两端重复定义
#### Scenario: 前端导入共享类型
- **WHEN** 前端代码导入共享 API 类型
- **THEN** 该导入 SHALL 不要求将后端运行时实现打包进前端

View File

@@ -0,0 +1,67 @@
## ADDED Requirements
### Requirement: Bun HTTP 运行时
系统 SHALL 运行一个 Bun HTTP server由单个进程提供后端 API、健康检查、生产静态资源和 SPA fallback 行为。
#### Scenario: 启动运行时服务器
- **WHEN** server 进程成功启动
- **THEN** 它 SHALL 监听配置的 host 和 port并记录实际 server URL
#### Scenario: 提供运行时配置
- **WHEN** 通过支持的运行时配置提供 host 或 port
- **THEN** server SHALL 使用该值,且不需要重新构建
### Requirement: API 路由命名空间
系统 MUST 将 `/api/*` 保留给后端 API 路由。
#### Scenario: API 路由匹配
- **WHEN** 请求匹配已注册的 `/api/*` 路由
- **THEN** Bun server SHALL 返回 API handler 的响应
#### Scenario: API 路由未命中
- **WHEN** 请求访问未注册的 `/api/*` 路由
- **THEN** Bun server MUST 返回 JSON 404 响应,而不是前端 HTML 文档
### Requirement: Demo API 端点
系统 SHALL 暴露 `/api/demo` 作为稳定 demo 端点,用于证明前后端集成可用。
#### Scenario: Demo API 成功响应
- **WHEN** 客户端请求 `/api/demo`
- **THEN** Bun server SHALL 返回包含可读 message 和 runtime metadata 的 JSON 响应
#### Scenario: Demo API 内容类型
- **WHEN** 客户端请求 `/api/demo`
- **THEN** Bun server SHALL 返回 JSON content type 的响应
### Requirement: 健康检查端点
系统 SHALL 在前端 SPA fallback 之外暴露健康检查端点。
#### Scenario: 健康检查成功
- **WHEN** 客户端请求 `/health`
- **THEN** Bun server SHALL 返回成功的、机器可读的健康检查响应
### Requirement: 生产静态资源服务
系统 SHALL 在生产模式下由 Bun runtime 服务 Vite 生产资源。
#### Scenario: 请求构建后的资源
- **WHEN** 客户端请求构建后的前端资源,例如 `/assets/app.js`
- **THEN** Bun server SHALL 返回该资源并带有适当的 content type
#### Scenario: 请求前端根路径
- **WHEN** 客户端请求 `/`
- **THEN** Bun server SHALL 返回前端入口 HTML 文档
#### Scenario: 生产 demo 页面调用 API
- **WHEN** 客户端从生产 Bun runtime 打开前端页面
- **THEN** demo 页面 SHALL 能够从同源调用 `/api/demo` 并展示后端响应
### Requirement: SPA fallback 行为
系统 SHALL 在生产环境中为非 API、非静态资源的前端路由返回前端入口 HTML 文档。
#### Scenario: 刷新前端路由
- **WHEN** 客户端请求前端路由,例如 `/dashboard`
- **THEN** Bun server SHALL 返回前端入口 HTML 文档
#### Scenario: 保留 API 错误语义
- **WHEN** 客户端请求未知的 `/api/*` 路由
- **THEN** Bun server MUST NOT 返回前端入口 HTML 文档

View File

@@ -0,0 +1,49 @@
## ADDED Requirements
### Requirement: 生产构建顺序
生产构建 MUST 在编译 Bun 后端 executable 之前先构建 Vite 前端。
#### Scenario: 运行生产构建
- **WHEN** 开发者运行生产构建命令
- **THEN** 系统 MUST 在调用 Bun standalone executable 编译之前生成前端静态资源
#### Scenario: 前端构建失败
- **WHEN** 前端生产构建失败
- **THEN** 系统 MUST 停止生产构建,且不能输出 stale executable
### Requirement: 单 executable 输出
生产构建 SHALL 输出一个 standalone executable其中包含 Bun 后端、必要 server 依赖和构建后的前端资源。
#### Scenario: 在目标机器运行 executable
- **WHEN** 生成的 executable 在兼容目标平台上运行
- **THEN** 它 SHALL 启动全栈应用,且不要求目标机器安装 Node.js、Bun、Vite 或 `node_modules`
#### Scenario: 服务嵌入的前端
- **WHEN** executable 收到前端根路径请求
- **THEN** 它 SHALL 从 executable 内包含的资源服务前端,且不需要外部 `dist/` 目录
#### Scenario: 服务嵌入 demo API 和页面
- **WHEN** 生成的 executable 启动,且浏览器打开前端根路径
- **THEN** 页面 SHALL 展示同一个 executable 进程中 `/api/demo` 返回的数据
### Requirement: 外部运行时配置
executable MUST 将环境相关运行时配置保留在嵌入的前端和 server bundle 之外。
#### Scenario: 修改监听端口
- **WHEN** 操作者修改受支持的 port 配置
- **THEN** 同一个 executable SHALL 在不重新构建的情况下监听新端口
#### Scenario: 缺少可选配置
- **WHEN** 可选运行时配置被省略
- **THEN** executable SHALL 使用文档化的默认值
### Requirement: 构建验证
项目 SHALL 提供验证,证明生产 executable 可以服务 API、健康检查、静态资源和 SPA fallback 路由。
#### Scenario: 验证 executable 路由
- **WHEN** 构建验证针对生成的 executable 运行
- **THEN** 它 SHALL 检查 `/api/demo``/health`、前端根路径、静态资源和前端 fallback 请求
#### Scenario: 验证失败
- **WHEN** 任一代表性生产路由检查失败
- **THEN** 验证 SHALL 使构建或测试命令失败

View File

@@ -0,0 +1,41 @@
## 1. 项目结构与依赖
- [x] 1.1 创建 `src/server``src/web``src/shared``scripts` 和测试目录结构
- [x] 1.2 调整 `package.json` 脚本以覆盖前端开发、后端开发、并行开发、测试和生产构建
- [x] 1.3 引入 Vite、React、React DOM 和必要 TypeScript 类型依赖
- [x] 1.4 创建或更新 README 记录项目结构、开发规范和命令
## 2. 前端开发工作流
- [x] 2.1 创建 Vite + React + TypeScript 前端入口和基础页面
- [x] 2.2 配置 Vite 开发服务器将 `/api/*` 代理到 Bun 后端
- [x] 2.3 实现前端 demo 页面调用相对路径 `/api/demo` 并展示成功和失败状态
- [x] 2.4 建立 `src/shared` 共享类型并确保前端不引入后端运行时实现
- [x] 2.5 提供一个 documented fullstack dev command 同时启动 Vite 前端和 Bun 后端
## 3. Bun 后端运行时
- [x] 3.1 创建 Bun server 入口并支持 host 和 port 运行期配置
- [x] 3.2 实现 `/health` 健康检查响应
- [x] 3.3 实现 `/api/demo` JSON 路由并返回前端可展示的 message 和 runtime metadata
- [x] 3.4 实现未命中 `/api/*` 路由返回 JSON 404 的行为
- [x] 3.5 实现生产环境静态资源服务和 SPA fallback 行为
## 4. 单可执行程序构建
- [x] 4.1 创建生产构建脚本,确保先执行 Vite build 再执行 Bun compile
- [x] 4.2 将 Vite `dist/` 产物嵌入 Bun executable运行时不依赖外部 `dist/` 目录
- [x] 4.3 配置 executable 输出路径和当前平台默认构建目标
- [x] 4.4 确保 executable 运行不依赖本机 Node.js、Bun、Vite 或 `node_modules`
## 5. 测试与验证
- [x] 5.1 为 `/api/demo``/health`、API 404 和 SPA fallback 增加测试
- [x] 5.2 为生产构建脚本增加失败中断或防止 stale executable 的验证
- [x] 5.3 增加构建后 executable smoke test 覆盖 `/api/demo`、健康检查、静态资源、前端 fallback 和 demo 页面内容
- [x] 5.4 运行完整测试和生产构建,确认所有任务满足 specs
## 6. 文档收尾
- [x] 6.1 更新 README 中的运行参数、构建产物、部署方式和已知限制
- [x] 6.2 在 README 中记录前端可拆离原则、`/api/*` 路径约定和 demo 验证步骤

View File

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

View File

@@ -0,0 +1,52 @@
## Context
当前项目根目录存在三项冗余:
1. `index.ts` — 仅包含 `import "./src/server/dev.ts"`,与 `package.json``"start"` 脚本功能完全重复,无任何其他文件或脚本引用它
2. `"module": "src/server/dev.ts"``private: true` 项目不会被发布ESM 入口字段无消费者;且指向一个启动服务器的副作用文件本身就不合理
3. `.build/` 目录 — 由 `scripts/build.ts` 在每次构建时生成,包含 `server-entry.ts``static-assets.ts` 两个中间文件。构建完成后这些文件残留在磁盘上,虽已被 `.gitignore` 忽略但不必要地占用空间
项目构建流程为:`vite build` → 生成 `.build/` 中间文件 → `Bun.build()` 编译为单可执行文件 → 输出到 `dist/gateway-checker`
## Goals / Non-Goals
**Goals:**
- 移除无实际作用的文件和配置,减少项目结构噪音
- 构建成功后自动清理中间产物,保持项目目录整洁
- 构建失败时保留中间产物以便排查问题
**Non-Goals:**
- 不改变构建产物本身(输出路径、可执行文件行为不变)
- 不引入新的构建步骤或依赖
- 不调整 RuntimeMode 或开发/生产模式的区分逻辑
## Decisions
### Decision 1: 直接删除 `index.ts`
**选择**:删除文件
**备选**:保留但添加注释说明用途
**理由**:无任何脚本、文件或构建流程引用它,保留只会增加困惑。`package.json``"start"` 脚本已直接指向 `src/server/dev.ts`
### Decision 2: 移除 `"module"` 字段
**选择**:从 `package.json` 中删除 `"module"` 字段
**备选**:改为 `"main"` 字段
**理由**`private: true` 意味着不会被 npm 发布,任何入口字段都没有消费者。改为 `"main"` 同样无意义,因为这不是一个库。完全移除最简洁。
### Decision 3: 构建成功后清理 `.build/`
**选择**:在 `Bun.build()` 成功后调用 `await rm(buildDir, { recursive: true, force: true })`
**备选**
- 始终保留 `.build/`(当前行为)
- 使用临时目录(`os.tmpdir()`
**理由**`build.ts` 开头已导入 `rm` 且已在构建开始时执行清理,只需在成功路径末尾复用同一行代码。构建失败时 `.build/` 自然保留,兼顾排查需求。不使用临时目录是因为 `Bun.build()``import ... with { type: "file" }` 需要相对路径引用 `dist/web/` 下的实际文件,临时目录增加路径复杂度。
具体修改位置:`scripts/build.ts` 第 53 行 `console.log(...)` 之后。
## Risks / Trade-offs
- **[极低风险] 删除 `index.ts` 影响外部工具**:如果 CI/CD 或其他自动化流程通过 `bun index.ts` 启动,会中断。→ 检查确认无此类用法。`package.json``"start": "bun src/server/dev.ts"` 是标准入口。
- **[极低风险] 移除 `module` 字段影响 IDE 解析**:某些 IDE 可能依赖 `module` 字段进行跳转。→ 实际指向 `dev.ts`,对代码导航无帮助。
- **[低风险] 清理 `.build/` 后无法排查偶现构建问题**:→ 构建失败时 `.build/` 保留,只有成功时才清理,排查路径完整。

View File

@@ -0,0 +1,25 @@
## Why
项目根目录存在冗余文件和无效配置:`index.ts``start` 脚本功能完全重复,`package.json``module` 字段在 `private` 项目中无实际作用,`.build/` 中间产物在构建成功后未清理导致磁盘残留。这些虽不影响运行,但增加了维护负担和项目结构的困惑。
## What Changes
- 删除根目录 `index.ts`,它是 `src/server/dev.ts` 的无意义包装,无任何脚本或文件引用它
- 移除 `package.json` 中的 `"module": "src/server/dev.ts"` 字段,`private: true` 的应用项目不需要此字段,且指向副作用文件作为 ESM 入口本身就不合理
-`scripts/build.ts` 中,`Bun.build()` 成功后自动清理 `.build/` 目录,构建失败时保留以便排查
## Capabilities
### New Capabilities
(无新增能力)
### Modified Capabilities
- `single-executable-packaging`: 构建流程新增成功后清理 `.build/` 中间产物目录的步骤
## Impact
- 删除文件:`index.ts`
- 修改文件:`package.json`(移除 1 行)、`scripts/build.ts`(新增 1 行)
- 不影响任何现有功能、API 或开发工作流

View File

@@ -0,0 +1,24 @@
## MODIFIED Requirements
### Requirement: 单 executable 输出
生产构建 SHALL 输出一个 standalone executable其中包含 Bun 后端、必要 server 依赖和构建后的前端资源。构建成功后 SHALL 自动清理中间产物目录(`.build/`),构建失败时 SHALL 保留中间产物以便排查。
#### Scenario: 在目标机器运行 executable
- **WHEN** 生成的 executable 在兼容目标平台上运行
- **THEN** 它 SHALL 启动全栈应用,且不要求目标机器安装 Node.js、Bun、Vite 或 `node_modules`
#### Scenario: 服务嵌入的前端
- **WHEN** executable 收到前端根路径请求
- **THEN** 它 SHALL 从 executable 内包含的资源服务前端,且不需要外部 `dist/` 目录
#### Scenario: 服务嵌入 demo API 和页面
- **WHEN** 生成的 executable 启动,且浏览器打开前端根路径
- **THEN** 页面 SHALL 展示同一个 executable 进程中 `/api/demo` 返回的数据
#### Scenario: 构建成功后清理中间产物
- **WHEN** 生产构建成功完成并输出 executable
- **THEN** 系统 SHALL 自动删除 `.build/` 目录及其所有内容
#### Scenario: 构建失败时保留中间产物
- **WHEN** 生产构建在任意步骤失败前端构建、中间产物生成、Bun 编译)
- **THEN** `.build/` 目录 SHALL 保留在磁盘上以供排查

View File

@@ -0,0 +1,13 @@
## 1. 移除冗余文件和配置
- [x] 1.1 删除根目录 `index.ts` 文件
- [x] 1.2 从 `package.json` 中移除 `"module": "src/server/dev.ts"` 字段
## 2. 构建后清理中间产物
- [x] 2.1 在 `scripts/build.ts``Bun.build()` 成功后添加清理 `.build/` 目录的代码
## 3. 验证
- [x] 3.1 运行 `bun run check` 确认类型检查、lint、格式化、测试全部通过
- [x] 3.2 运行 `bun run verify` 确认完整构建和 smoke test 通过

View File

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

View File

@@ -0,0 +1,90 @@
## Context
当前项目是 Bun + TypeScript 的前后端一体化 demo开发期由 Vite React 提供前端 HMRBun 提供 `/api/*``/health`;生产期先构建 Vite 静态资源,再通过 Bun file import 将资源和后端编译为单 executable。
现有能力已经通过 `typecheck`、单元测试和 executable smoke test 验证,但真实业务开发尚未开始。此变更聚焦平台基础设施硬化,目标是在业务 API、数据模型和前端业务页面扩展之前先把开发联调、代码质量、格式一致性、HTTP 契约和生产验证闭环固化下来。
## Goals / Non-Goals
**Goals:**
- 引入 ESLint 审查代码质量、React Hooks 规则和前后端边界。
- 引入 Prettier 统一代码风格,但不格式化 `openspec/`,避免影响 OpenSpec 文档和 tasks 一行一个任务的规则。
- 提供快速 `check` 和完整 `verify` 两层验证命令。
- 让开发期 Vite proxy 目标端口和 Bun server 监听端口保持一致。
- 补齐 HTTP method、JSON 404/405、静态资源缓存和低风险安全头的运行时契约。
- 增强生产 executable smoke test确保验证的是当前源码构建出的生产产物。
- 同步 README使文档描述与脚本、构建中间产物和验证流程一致。
**Non-Goals:**
- 不开发 gateway checker 真实业务能力。
- 不引入数据库、持久化、认证、React Router 或 UI 组件库。
- 不新增 CI 配置;本次仅提供本地 `check``verify` 命令CI 接入留给后续仓库托管策略。
- 不引入 CSP本次只加入低风险安全响应头避免提前约束未来前端资源策略。
- 不做大规模目录重构或业务框架抽象。
## Decisions
### ESLint 和 Prettier 分工
ESLint 只承担质量审查和边界约束不承担缩进、换行、引号等格式职责。Prettier 专门负责代码风格,避免 ESLint stylistic 规则和格式化器重复工作。
备选方案是只引入 ESLint 并启用 stylistic 规则,但后续维护成本更高,且容易和编辑器格式化行为冲突。另一个备选方案是只引入 Prettier但它无法检查 React Hooks、未处理 Promise 或前端误导入后端实现等质量问题。
本次采用的最小依赖集合为 `eslint``@eslint/js``typescript-eslint``eslint-plugin-react-hooks``eslint-plugin-react-refresh``prettier`。暂不引入 `eslint-config-prettier`,除非实现阶段引入会与 Prettier 冲突的 ESLint preset 或 stylistic 规则。
### 验证命令分层
新增 `check``verify` 两层命令:
```text
check
├─ typecheck
├─ lint
├─ format:check
└─ test
verify
├─ check
├─ build
└─ test:smoke
```
`check` 面向日常开发,反馈快;`verify` 面向提交前或发布前验证,包含生产构建和 executable smoke test。备选方案是只提供 `verify`,但每次都构建 executable 会降低日常迭代速度。
### Prettier 忽略范围
Prettier SHALL 忽略 `openspec/``dist/``.build/``node_modules/``bun.lock` 和临时构建产物。`openspec/` 排除是显式决策,因为 OpenSpec tasks 要求一行一个任务Markdown 自动折行可能破坏审阅体验和规则遵循。
### 开发期端口配置
文档化的全栈开发命令以 `PORT` 作为后端端口的唯一对外配置。Vite proxy 使用的 `BACKEND_PORT` 应由开发脚本从 `PORT` 派生,或者明确作为内部变量,避免用户只改 `BACKEND_PORT` 导致 proxy 与 server 分叉。直接运行 Bun server 或生产 executable 时仍可继续使用现有 CLI 参数覆盖 host 和 port。
### 运行配置校验
运行配置继续保持 CLI 参数优先于环境变量,缺省时使用 README 文档化默认值。端口配置必须拒绝非整数、小于 0 或大于 65535 的值,并通过单元测试覆盖默认值、优先级、非法输入和边界值,避免开发期和生产期配置行为分叉。
### HTTP method 和错误契约
现有 demo 端点按路径匹配,后续业务扩展前需要先固化 method 语义。`/health``/api/demo``GET` 为主,并支持 `HEAD` 返回相同状态和 headers 但无响应体;不支持的 method 返回 JSON 405并带 `Allow` header。未知 `/api/*` 继续返回 JSON 404不能落入前端 HTML fallback。
### 生产响应头策略
生产 HTML 使用 `Cache-Control: no-cache`Vite hash 静态资源使用长缓存 `public, max-age=31536000, immutable`。所有生产 HTTP 响应增加低风险安全头,例如 `X-Content-Type-Options: nosniff``Referrer-Policy`。CSP 暂不纳入本次变更,避免后续业务页面接入外部资源时产生过早约束。
### 构建确定性
生成 `.build/static-assets.ts` 时,嵌入资源列表应按稳定顺序输出。这样可以减少重复构建时的无意义差异,也方便 smoke test 和后续审查定位问题。
### Smoke test 增强
`test:smoke` SHALL 针对当前构建出的 executable 验证生产行为,包括 `/health``/api/demo`、未知 API、根 HTML、SPA fallback、静态资源、未知静态资源、生产 runtime mode、缓存头和低风险安全头。`verify` 必须先执行 build 再 smoke避免验证旧产物。
## Risks / Trade-offs
- 新增 ESLint 和 Prettier 会增加开发依赖与初次配置成本 → 采用最小依赖集合,只启用与当前项目直接相关的规则。
- 现有代码可能被 Prettier 产生格式化改动 → 本次作为平台硬化变更集中处理,后续业务变更减少格式噪音。
- 405 和 HEAD 行为会让 HTTP handler 稍复杂 → 在业务 API 扩展前处理,避免未来每个端点重复补语义。
- 安全头不包含 CSP安全强度有限 → 先采用低风险头CSP 在前端资源来源稳定后单独设计。
- `verify` 包含构建和 smoke运行更慢 → 保留快速 `check` 作为日常反馈通道。

View File

@@ -0,0 +1,35 @@
## Why
当前项目已经具备 Bun 后端、Vite React 前端、生产静态资源嵌入和单 executable 打包链路,但仍处于 demo 基础设施阶段。真实业务开发开始前,需要先收紧前后端开发、运行时 HTTP 契约、代码质量门禁和生产验证闭环,避免后续业务变更建立在不稳定或不可重复验证的基础上。
## What Changes
- 增加 ESLint 作为代码质量、React Hooks 和前后端边界审查工具。
- 增加 Prettier 作为代码风格格式化工具,并排除 `openspec/`、构建产物和依赖目录。
- 增加快速 `check` 命令和完整 `verify` 命令,其中 `verify` SHALL 覆盖类型检查、lint、格式检查、单元测试、生产构建和 executable smoke test。
- 明确开发期 Bun server 与 Vite proxy 的端口配置一致性,避免前端代理端口和后端监听端口分叉。
- 补充运行配置校验要求包括默认值、CLI 与环境变量优先级、无效端口拒绝和端口边界行为。
- 强化 HTTP 运行时契约,包括 method 语义、JSON 404/405 错误、静态资源缓存策略和低风险安全响应头。
- 强化单 executable 构建验证,包括确定性资源生成、生产模式验证、静态资源响应头、未知 API、未知 asset 和 SPA fallback 检查。
- 修正 OpenSpec `tasks` artifact 规则键名,避免 CLI 状态命令产生无效规则警告。
- 同步更新 README说明质量门禁、验证命令、构建中间产物和运行配置边界。
## Capabilities
### New Capabilities
- `code-quality-gates`: 定义 ESLint、Prettier、`check``verify` 的质量门禁行为要求。
### Modified Capabilities
- `fullstack-app-runtime`: 补充运行配置校验、HTTP method、JSON 错误、静态资源缓存和低风险安全响应头等运行时契约。
- `frontend-development-workflow`: 补充开发期 Bun server 与 Vite proxy 配置一致性的要求。
- `single-executable-packaging`: 补充确定性构建、完整验证命令和 smoke 覆盖增强要求。
## Impact
- 影响 `package.json` scripts 和开发依赖,新增 lint、format、check、verify 相关命令。
- 影响 ESLint、Prettier 配置文件和忽略规则。
- 影响 `src/server/*` 的 HTTP method、错误响应、静态资源响应头和配置处理。
- 影响 `scripts/build.ts``scripts/dev.ts``scripts/smoke.ts` 的构建、开发联调和验证逻辑。
- 影响 `tests/`需要补充配置解析、HTTP 语义、静态资源响应和验证行为相关测试。
- 影响 `openspec/config.yaml`,修正 `tasks` artifact 规则键名。
- 影响 `README.md`,需要同步开发命令、验证命令、构建流程和边界说明。

View File

@@ -0,0 +1,53 @@
## ADDED Requirements
### Requirement: ESLint 代码质量门禁
项目 SHALL 提供 ESLint 代码质量门禁,用于审查 TypeScript、React 前端、脚本和测试代码中的质量问题。
#### Scenario: 运行 lint 检查
- **WHEN** 开发者运行文档化的 lint 命令
- **THEN** 系统 SHALL 使用 ESLint 检查项目源码、脚本和测试代码,并在发现违规时以非零状态退出
#### Scenario: 检查 React Hooks 规则
- **WHEN** 前端 React 代码违反 Hooks 调用规则
- **THEN** lint 命令 MUST 失败并报告对应违规
#### Scenario: 保护前后端边界
- **WHEN** `src/web` 前端代码导入 `src/server` 后端运行时实现
- **THEN** lint 命令 MUST 失败并报告前后端边界违规
### Requirement: Prettier 代码格式门禁
项目 SHALL 提供 Prettier 格式化和格式检查命令,用于统一代码风格。
#### Scenario: 检查代码格式
- **WHEN** 开发者运行文档化的格式检查命令
- **THEN** 系统 SHALL 使用 Prettier 检查受管理文件,并在发现未格式化文件时以非零状态退出
#### Scenario: 自动格式化代码
- **WHEN** 开发者运行文档化的格式化命令
- **THEN** 系统 SHALL 使用 Prettier 重写受管理文件的格式
#### Scenario: 排除 OpenSpec 文档和生成产物
- **WHEN** Prettier 格式化或格式检查运行
- **THEN** 系统 MUST 排除 `openspec/``dist/``.build/``node_modules/``bun.lock` 和临时构建产物
### Requirement: 快速检查命令
项目 SHALL 提供快速 `check` 命令,用于日常开发期间验证代码质量和基础行为。
#### Scenario: 运行快速检查
- **WHEN** 开发者运行 `bun run check`
- **THEN** 系统 SHALL 依次执行类型检查、lint、格式检查和单元测试
#### Scenario: 快速检查失败
- **WHEN** `check` 中任一子检查失败
- **THEN** `check` MUST 以非零状态退出且不静默忽略失败
### Requirement: 完整验证命令
项目 SHALL 提供完整 `verify` 命令,用于提交前或发布前验证当前源码、测试和生产 executable 行为。
#### Scenario: 运行完整验证
- **WHEN** 开发者运行 `bun run verify`
- **THEN** 系统 SHALL 先运行 `check`,再运行生产构建和 executable smoke test
#### Scenario: 完整验证失败
- **WHEN** `verify` 中任一阶段失败
- **THEN** `verify` MUST 以非零状态退出且不能继续声明验证成功

View File

@@ -0,0 +1,23 @@
## ADDED Requirements
### Requirement: 开发期后端端口一致性
项目 SHALL 保证文档化的全栈开发命令中Vite proxy 目标端口与 Bun 后端监听端口来自同一配置来源。
#### Scenario: 使用默认开发端口
- **WHEN** 开发者未提供端口覆盖并运行文档化的全栈开发命令
- **THEN** Bun 后端 SHALL 监听默认端口,且 Vite SHALL 将 `/api/*` 代理到同一端口
#### Scenario: 使用 PORT 覆盖开发端口
- **WHEN** 开发者通过 `PORT` 覆盖后端端口并运行文档化的全栈开发命令
- **THEN** Bun 后端 SHALL 监听该端口,且 Vite SHALL 将 `/api/*` 代理到同一端口
#### Scenario: 避免代理端口与后端端口分叉
- **WHEN** 开发期脚本需要向 Vite 传递后端端口
- **THEN** 该代理端口 MUST 从文档化的后端端口配置派生,而不是作为独立对外配置导致分叉
### Requirement: 开发质量命令文档化
项目 SHALL 在前端开发工作流文档中说明日常检查和完整验证命令。
#### Scenario: 查阅开发命令
- **WHEN** 开发者阅读 README 的开发或测试章节
- **THEN** 文档 SHALL 说明 `check` 用于日常开发检查,`verify` 用于提交前或发布前完整验证

View File

@@ -0,0 +1,76 @@
## ADDED Requirements
### Requirement: HTTP method 语义
系统 SHALL 为运行时端点提供明确的 HTTP method 语义,避免不支持的 method 被错误地当作成功请求处理。
#### Scenario: GET 请求访问运行时端点
- **WHEN** 客户端使用 `GET` 请求 `/health``/api/demo`
- **THEN** Bun server SHALL 返回对应端点的成功响应
#### Scenario: HEAD 请求访问运行时端点
- **WHEN** 客户端使用 `HEAD` 请求 `/health``/api/demo`
- **THEN** Bun server SHALL 返回与 `GET` 相同的成功状态和 headers但 MUST NOT 返回响应体
#### Scenario: 不支持的 method 访问运行时端点
- **WHEN** 客户端使用不支持的 method 请求 `/health``/api/demo`
- **THEN** Bun server MUST 返回 JSON 405 响应,并带有描述允许 method 的 `Allow` header
### Requirement: 运行配置校验
系统 SHALL 对运行时 host 和 port 配置提供稳定、可测试的解析与校验行为。
#### Scenario: 使用默认运行配置
- **WHEN** 未提供 host 或 port 覆盖
- **THEN** server SHALL 使用 README 文档化的默认 host 和 port
#### Scenario: CLI 参数优先于环境变量
- **WHEN** CLI 参数和环境变量同时提供同一项运行配置
- **THEN** server SHALL 使用 CLI 参数中的值
#### Scenario: 拒绝无效端口
- **WHEN** port 配置不是整数、小于 0 或大于 65535
- **THEN** server MUST 拒绝启动并报告无效端口
#### Scenario: 接受端口边界值
- **WHEN** port 配置为 0 或 65535
- **THEN** server SHALL 将其作为有效端口配置处理
### Requirement: API 错误响应一致性
系统 SHALL 为 API 命名空间内的错误返回机器可读 JSON 响应。
#### Scenario: 未知 API 路由
- **WHEN** 客户端请求未知的 `/api/*` 路由
- **THEN** Bun server MUST 返回包含 `error``status` 字段的 JSON 404 响应,而不是前端 HTML 文档
#### Scenario: API method 不允许
- **WHEN** 客户端使用不支持的 method 请求已存在的 API 路由
- **THEN** Bun server MUST 返回包含 `error``status` 字段的 JSON 405 响应
### Requirement: 生产缓存策略
系统 SHALL 为生产静态资源和前端入口 HTML 使用明确的缓存策略。
#### Scenario: 请求前端入口 HTML
- **WHEN** 生产 Bun server 返回前端入口 HTML 文档
- **THEN** 响应 SHALL 使用 `Cache-Control: no-cache`
#### Scenario: 请求构建后的静态资源
- **WHEN** 生产 Bun server 返回 Vite 构建后的静态资源
- **THEN** 响应 SHALL 使用长缓存策略 `public, max-age=31536000, immutable`
#### Scenario: 请求未知静态资源
- **WHEN** 客户端请求不存在的 `/assets/*` 资源或带文件扩展名的未知路径
- **THEN** Bun server MUST 返回 404且 MUST NOT 返回前端入口 HTML 文档
### Requirement: 低风险安全响应头
系统 SHALL 在生产运行时响应中附加低风险安全响应头,提升基础安全性且不提前约束未来前端资源策略。
#### Scenario: 生产 HTML 响应包含安全头
- **WHEN** 生产 Bun server 返回前端 HTML 文档
- **THEN** 响应 SHALL 包含 `X-Content-Type-Options: nosniff``Referrer-Policy` headers
#### Scenario: 生产 JSON 响应包含安全头
- **WHEN** 生产 Bun server 返回 `/health``/api/*` JSON 响应
- **THEN** 响应 SHALL 包含 `X-Content-Type-Options: nosniff``Referrer-Policy` headers
#### Scenario: 生产静态资源响应包含安全头
- **WHEN** 生产 Bun server 返回 Vite 构建后的静态资源
- **THEN** 响应 SHALL 包含 `X-Content-Type-Options: nosniff``Referrer-Policy` headers

View File

@@ -0,0 +1,33 @@
## ADDED Requirements
### Requirement: 构建生成确定性
生产构建 SHALL 以稳定顺序生成嵌入静态资源清单,减少重复构建产生无意义差异。
#### Scenario: 生成静态资源清单
- **WHEN** 生产构建扫描 Vite 输出目录并生成嵌入资源模块
- **THEN** 资源条目 SHALL 按稳定顺序输出
#### Scenario: 重复构建相同前端产物
- **WHEN** Vite 输出内容未变化且生产构建重复运行
- **THEN** 生成的嵌入资源模块 SHALL 保持语义一致且不依赖文件系统遍历顺序
## MODIFIED Requirements
### Requirement: 构建验证
项目 SHALL 提供验证,证明生产 executable 可以服务 API、健康检查、静态资源和 SPA fallback 路由,并且完整验证 MUST 针对当前源码重新构建后的 executable 运行。
#### Scenario: 验证 executable 路由
- **WHEN** 构建验证针对生成的 executable 运行
- **THEN** 它 SHALL 检查 `/api/demo``/health`、前端根路径、静态资源、未知 API、未知静态资源和前端 fallback 请求
#### Scenario: 验证生产模式和响应头
- **WHEN** 构建验证针对生成的 executable 运行
- **THEN** 它 SHALL 检查 demo 响应处于 production runtime mode并验证代表性 HTML、JSON 和静态资源响应的缓存或低风险安全 headers
#### Scenario: 完整验证重新构建 executable
- **WHEN** 开发者运行完整验证命令
- **THEN** 系统 MUST 先基于当前源码执行生产构建,再对新生成的 executable 运行 smoke test
#### Scenario: 验证失败
- **WHEN** 任一代表性生产路由、响应头、生产模式或构建阶段检查失败
- **THEN** 验证 SHALL 使构建或测试命令失败

View File

@@ -0,0 +1,34 @@
## 1. 质量门禁配置
- [x] 1.1 添加 `eslint``@eslint/js``typescript-eslint``eslint-plugin-react-hooks``eslint-plugin-react-refresh``prettier` 开发依赖并更新 lockfile
- [x] 1.2 在 `package.json` 新增 `lint``format``format:check``check``verify` 脚本
- [x] 1.3 配置 ESLint 检查 TypeScript、React、脚本和测试代码并启用 React Hooks 规则
- [x] 1.4 配置 ESLint 禁止 `src/web` 导入 `src/server` 后端运行时实现
- [x] 1.5 配置 Prettier 和忽略规则,确保排除 `openspec/``dist/``.build/``node_modules/``bun.lock` 和临时构建产物
## 2. 开发期配置一致性
- [x] 2.1 调整全栈开发脚本,使 Vite proxy 端口从文档化的后端端口配置派生
- [x] 2.2 调整或确认运行配置校验覆盖默认值、CLI 优先级、无效端口和端口边界行为
- [x] 2.3 补充运行配置测试,覆盖默认端口、`PORT` 覆盖、CLI 优先级、无效端口和端口边界
## 3. HTTP 运行时契约
- [x] 3.1 为 `/health``/api/demo` 实现 `GET``HEAD` 语义,并对不支持 method 返回 JSON 405 和 `Allow` header
- [x] 3.2 统一 API 404 和 405 错误响应结构,确保包含 `error``status` 字段
- [x] 3.3 为生产 HTML、JSON 和静态资源响应添加低风险安全 headers
- [x] 3.4 明确生产 HTML、静态资源和未知静态资源的缓存与 404 行为
- [x] 3.5 补充 HTTP handler 单元测试,覆盖 method、HEAD、JSON 错误、缓存 headers、安全 headers 和未知静态资源
## 4. 构建与 Smoke 验证
- [x] 4.1 调整生产构建脚本,按稳定顺序生成嵌入静态资源清单
- [x] 4.2 增强 executable smoke test验证 production runtime mode、未知 API、未知静态资源、SPA fallback、缓存 headers 和低风险安全 headers
- [x] 4.3 确保 `verify` 先运行 `check`,再基于当前源码执行生产构建和 smoke test
## 5. 文档与最终验证
- [x] 5.1 更新 README说明 `check``verify`、lint、format、构建中间产物、运行配置和验证边界
- [x] 5.2 运行 `bun run check` 并修复发现的问题
- [x] 5.3 运行 `bun run verify` 并修复发现的问题
- [x] 5.4 修正 `openspec/config.yaml``tasks` artifact 规则键名并确认 OpenSpec CLI 不再告警

View File

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

View File

@@ -0,0 +1,130 @@
## Context
Gateway Checker 当前是一个 Bun + React 全栈脚手架,仅包含 demo 验证逻辑(`/api/demo` 端点 + 前端展示连接状态)。项目已有完整的开发、构建、打包、测试链路。需要将其转化为一个可用的 HTTP 拨测工具。
现有基础设施:
- Bun 后端:路由框架(`createFetchHandler`)、服务启动(`startServer`)、运行时配置解析(`readRuntimeConfig`
- React 前端Vite + React + TypeScript开发期通过 Vite proxy 转发 `/api/*`
- 构建Vite 前端构建 + Bun 单 executable 打包
- 测试Bun test + smoke test
## Goals / Non-Goals
**Goals:**
- 提供完整的 HTTP 拨测能力YAML 配置 → 定时并发拨测 → 结果持久化 → 可视化展示
- 支持灵活的拨测配置per-target interval、自定义 method/header/body、expect 校验
- 前端 Dashboard 实时展示:总览统计、目标状态列表、历史记录、延迟趋势图
- 保持现有项目架构风格和构建打包链路
- 零外部运行时依赖新增(仅前端 recharts
**Non-Goals:**
- 不做告警通知(邮件/短信/Webhook仅 Dashboard 展示
- 不做数据自动清理/过期策略,保留全部历史记录
- 不做 SSE/WebSocket 实时推送,用轮询即可
- 不做拨测目标动态增删(需修改 YAML 后重启)
- 不做认证/鉴权
- 不做分布式/集群部署
## Decisions
### 1. 配置管理YAML 统一配置 + 单 CLI 参数
**选择**所有配置server、数据目录、拨测默认值、目标列表统一到 YAML 文件CLI 只接受一个参数即配置文件路径。
**替代方案**
- CLI 参数 + 环境变量覆盖部分配置 → 配置分散,维护成本高
- TOML 格式 → Bun 无内置支持,需引入依赖
**理由**
- 用户明确要求"配置统一到 YAML 文件"
- `Bun.YAML.parse()` 内置支持,零依赖
- 单参数 CLI 最简洁:`./gateway-checker ./probes.yaml`
### 2. 数据存储SQLitebun:sqlite
**选择**:使用 Bun 内置 `bun:sqlite` 模块WAL 模式运行。
**替代方案**
- JSONL 文件追加 → 聚合查询需全表扫描,趋势计算复杂
- 外部 SQLite 库better-sqlite3→ bun:sqlite 已内置,无需引入
**理由**
- 趋势分析需要 `AVG(latency) GROUP BY hour` 等聚合查询SQL 原生支持
- bun:sqlite 是 Bun 内置模块,不违反"不引入新依赖"约束
- WAL 模式支持并发读写
-`.db` 文件,便于管理
### 3. 调度模型:按 interval 分组 + 组内并发
**选择**:将所有 target 按其 interval 值分组,每组一个 `setInterval` timer组内使用 `Promise.all` 并发拨测。
**替代方案**
- 全局统一 tick → 无法支持 per-target interval
- 每个 target 独立 timer → 目标多时 timer 数量大,资源浪费
- 使用调度队列(如 BullMQ→ 过度设计
**理由**
- 支持 per-target interval满足不同服务不同频率的需求
- 相同 interval 的目标共享 timertimer 数量 = 不同 interval 值的数量
- 组内并发保证批量效率,组间隔离互不影响
### 4. 前端更新策略:轮询
**选择**:前端每 5-10 秒轮询 `/api/summary``/api/targets`
**替代方案**
- SSE 服务端推送 → 实现复杂,拨测间隔 15-60s 级别无必要
- WebSocket → 更复杂,过度设计
**理由**
- 拨测间隔本身是 15-60s5s 轮询延迟完全可接受
- 实现简单,无需维护长连接状态
- Dashboard 面板按需加载趋势数据(展开详情时请求)
### 5. 趋势图recharts
**选择**:引入 recharts 作为前端图表库。
**替代方案**
- 纯 SVG 手写 sparkline → 零依赖但代码量大,交互能力有限
- Chart.js → 非 React 原生,需要 wrapper
- D3 → 过于底层
**理由**
- 用户确认允许引入轻量图表库
- recharts 是 React 原生图表库,与现有 React 技术栈一致
- 支持折线图、迷你 Sparkline满足需求
- 社区活跃,文档完善
### 6. 目标状态判定模型
**选择**:两层判定——`success`(请求是否完成)+ `matched`(是否符合 expect 规则)。
```
● UP = success ✓ && matched ✓
● DOWN = !success || !matched
```
**理由**
- 区分"网络不可达"和"返回了非预期状态码"两种故障场景
- expect 规则可选,不配置时 matched 默认为 true
- 前端可以根据 `success`/`matched` 分别展示不同故障原因
### 7. 数据库 Schema 设计
**targets 表**:从 YAML 同步初始化,运行时只读。
**check_results 表**:只追加写入,索引 `(target_id, timestamp)` 加速历史查询。
**理由**
- targets 从 YAML 来,不提供运行时动态增删(符合 Non-Goals
- check_results 追加写入,无需更新/删除,简单可靠
- 按时间范围查询是最高频操作,复合索引覆盖
## Risks / Trade-offs
- **[YAML 格式错误导致启动失败]** → 解析时做完整校验,输出清晰错误信息(字段缺失、格式不对、值非法等),提前失败而非运行时出错
- **[并发拨测对目标服务器压力]** → 每组内 Promise.all 并发,但同一 group 的 tick 间隔内不会重复拨测。如果用户配置了大量目标且 interval 很短,可能对目标产生压力,这是用户配置责任
- **[SQLite 数据文件增长]** → 当前不清理,长期运行会增长。预留清理策略接口,后续可通过配置保留天数
- **[recharts 包体积]** → recharts gzip 后约 70KB会增加前端 bundle 大小。对于内部工具可接受
- **[拨测请求超时阻塞]** → 使用 `AbortController` + `setTimeout` 实现超时,避免单个慢请求阻塞整组
- **[进程重启后丢失 timer 状态]** → 拨测是幂等的(无状态定时任务),重启后立即开始新一轮即可,无需恢复状态

View File

@@ -0,0 +1,36 @@
## Why
项目当前只有 demo 验证链路(`/api/demo` + 前端展示连接状态),缺少核心业务逻辑。需要一个 HTTP 拨测工具,通过 YAML 配置文件定义拨测目标URL、method、header、body、期望条件等后端按配置定时、并行批量拨测结果持久化到本地 SQLite前端 Dashboard 展示各目标实时状态、可用率、延迟趋势等。
## What Changes
- **清理 demo 样例代码**:移除 `/api/demo` 路由、`DemoResponse` 类型、前端 demo 展示逻辑,保留路由框架、服务启动、构建打包链路和 `/health` 端点
- **新增 YAML 配置文件解析**:使用 Bun 内置 `Bun.YAML.parse()` 读取拨测规则文件,包含 server 配置、数据目录、全局默认值和拨测目标列表
- **简化 CLI 参数**:只保留一个命令行参数——配置文件路径,所有配置统一到 YAML 文件
- **新增 SQLite 数据存储**:使用 `bun:sqlite` 存储拨测目标(从 YAML 同步)和拨测结果(追加写入),支持索引查询
- **新增拨测调度引擎**:按 target 的 interval 分组,每组独立 timer组内 `Promise.all` 并发拨测,支持 expect 校验(状态码、响应体、延迟阈值)
- **新增 REST API 层**:提供总览统计、目标列表含当前状态、历史记录、趋势聚合等接口
- **新增前端 Dashboard**:使用 React 组件展示统计卡片、目标列表表格(含状态圆点和迷你趋势线)、可展开详情面板(含完整趋势图),通过轮询 5-10s 更新数据
- **引入 recharts 依赖**:用于趋势图和迷你 Sparkline 可视化
## Capabilities
### New Capabilities
- `probe-config`: YAML 配置文件格式定义、解析校验与 CLI 启动流程
- `probe-engine`: 拨测调度引擎——按 interval 分组定时、并发拨测、expect 校验、结果存储
- `probe-data-store`: SQLite 数据存储——targets 同步、results 追加、索引与聚合查询
- `probe-api`: REST API 层——总览统计、目标列表含状态、历史记录、趋势聚合
- `probe-dashboard`: React 前端 Dashboard——统计卡片、目标表格、详情面板、趋势图
### Modified Capabilities
- `fullstack-app-runtime`: CLI 参数从 `--host/--port` 简化为单个配置文件路径参数;移除 `/api/demo` 路由;新增 `/api/*` 拨测相关 API 路由
- `frontend-development-workflow`: 前端从 demo 展示页面替换为拨测 Dashboard移除 `/api/demo` 相关代理场景
## Impact
- **代码变更**`src/server/app.ts` 路由重写、`src/server/config.ts` 简化、`src/shared/api.ts` 类型重写、`src/web/` 前端全部重写
- **新增模块**`src/server/checker/` 目录engine、fetcher、store、config-loader、types
- **新增依赖**`recharts`(前端图表)
- **无新增外部依赖**YAML 解析使用 Bun 内置 `Bun.YAML`SQLite 使用 Bun 内置 `bun:sqlite`
- **构建打包**:现有 single executable 打包链路不变YAML 配置文件为外部文件不嵌入 executable
- **API 变更****BREAKING** 移除 `/api/demo`,新增 `/api/summary``/api/targets``/api/targets/:id/history``/api/targets/:id/trend`

View File

@@ -0,0 +1,12 @@
## MODIFIED Requirements
### Requirement: 前端开发期 API 代理
前端开发服务器 SHALL 在本地开发期间将 `/api/*` 请求代理到 Bun 后端服务。
#### Scenario: 前端开发期调用拨测 API
- **WHEN** 浏览器从 Vite 开发源请求 `/api/summary``/api/targets` 等拨测 API
- **THEN** Vite SHALL 将请求转发到 Bun 后端服务,且不需要浏览器 CORS 配置
#### Scenario: 开发期访问非 API 前端路由
- **WHEN** 浏览器从 Vite 开发源请求非 API 前端路由
- **THEN** Vite SHALL 将该请求作为前端应用流量处理,而不是转发到后端

View File

@@ -0,0 +1,35 @@
## MODIFIED Requirements
### Requirement: Bun HTTP 运行时
系统 SHALL 运行一个 Bun HTTP server由单个进程提供后端 API、健康检查、生产静态资源和 SPA fallback 行为。
#### Scenario: 启动运行时服务器
- **WHEN** server 进程成功启动
- **THEN** 它 SHALL 监听 YAML 配置文件中指定的 host 和 port并记录实际 server URL
#### Scenario: 通过 YAML 配置提供运行时参数
- **WHEN** 通过 YAML 配置文件提供 host、port、数据目录等参数
- **THEN** server SHALL 使用该值,且不需要重新构建
#### Scenario: CLI 只接受配置文件路径
- **WHEN** 用户通过命令行启动程序
- **THEN** 系统 SHALL 只接受一个命令行参数作为 YAML 配置文件路径
#### Scenario: 提供拨测相关 API
- **WHEN** server 启动完成
- **THEN** 系统 SHALL 提供 `/api/summary``/api/targets``/api/targets/:id/history``/api/targets/:id/trend` 端点
### Requirement: HTTP method 语义
系统 SHALL 为运行时端点提供明确的 HTTP method 语义,避免不支持的 method 被错误地当作成功请求处理。
#### Scenario: GET 请求访问运行时端点
- **WHEN** 客户端使用 `GET` 请求 `/health``/api/*` 端点
- **THEN** Bun server SHALL 返回对应端点的成功响应
#### Scenario: HEAD 请求访问运行时端点
- **WHEN** 客户端使用 `HEAD` 请求 `/health``/api/*` 端点
- **THEN** Bun server SHALL 返回与 `GET` 相同的成功状态和 headers但 MUST NOT 返回响应体
#### Scenario: 不支持的 method 访问运行时端点
- **WHEN** 客户端使用不支持的 method 请求 `/health``/api/*` 端点
- **THEN** Bun server SHALL 返回 405 状态码和 Allow header

View File

@@ -0,0 +1,59 @@
## ADDED Requirements
### Requirement: 总览统计 API
系统 SHALL 提供 `GET /api/summary` 端点,返回所有目标的总体统计信息。
#### Scenario: 获取总览统计
- **WHEN** 客户端请求 `GET /api/summary`
- **THEN** 系统 SHALL 返回 JSON 包含 total总目标数、up正常数、down异常数、avgLatencyMs所有目标平均延迟、lastCheckTime最近一次拨测时间
### Requirement: 目标列表 API
系统 SHALL 提供 `GET /api/targets` 端点,返回所有目标及其最新状态和统计摘要。
#### Scenario: 获取目标列表
- **WHEN** 客户端请求 `GET /api/targets`
- **THEN** 系统 SHALL 返回 JSON 数组每个元素包含目标基本信息、最近一次拨测结果timestamp、success、statusCode、latencyMs、error、matched和统计摘要totalChecks、availability、avgLatencyMs、p99LatencyMs
#### Scenario: 目标无历史记录
- **WHEN** 某目标尚未执行过任何拨测
- **THEN** 其 latestCheck 为 nullstats 中 totalChecks 为 0
### Requirement: 历史记录 API
系统 SHALL 提供 `GET /api/targets/:id/history` 端点,返回指定目标的最近 N 条拨测记录。
#### Scenario: 获取最近历史记录
- **WHEN** 客户端请求 `GET /api/targets/1/history?limit=20`
- **THEN** 系统 SHALL 返回最多 20 条拨测记录,按时间倒序排列
#### Scenario: 使用默认 limit
- **WHEN** 客户端请求 `GET /api/targets/1/history`(未指定 limit
- **THEN** 系统 SHALL 默认返回最近 20 条记录
### Requirement: 趋势聚合 API
系统 SHALL 提供 `GET /api/targets/:id/trend` 端点,返回指定目标按小时聚合的趋势数据。
#### Scenario: 获取 24 小时趋势
- **WHEN** 客户端请求 `GET /api/targets/1/trend?hours=24`
- **THEN** 系统 SHALL 返回按小时分组的聚合数据,每个数据点包含 hour、avgLatencyMs、availability、totalChecks
#### Scenario: 使用默认时间范围
- **WHEN** 客户端请求 `GET /api/targets/1/trend`(未指定 hours
- **THEN** 系统 SHALL 默认返回最近 24 小时的趋势数据
### Requirement: 保留健康检查端点
系统 SHALL 保留 `GET /health` 端点,不受拨测功能影响。
#### Scenario: 访问健康检查
- **WHEN** 客户端请求 `GET /health`
- **THEN** 系统 SHALL 返回与之前格式一致的健康检查响应
### Requirement: API 错误处理
系统 SHALL 对不存在的目标 ID 和无效参数返回适当的 HTTP 错误响应。
#### Scenario: 查询不存在的目标
- **WHEN** 客户端请求 `GET /api/targets/999/history`
- **THEN** 系统 SHALL 返回 404 状态码和错误信息
#### Scenario: 无效的 limit 参数
- **WHEN** 客户端请求 `GET /api/targets/1/history?limit=abc`
- **THEN** 系统 SHALL 返回 400 状态码和错误信息

View File

@@ -0,0 +1,53 @@
## ADDED Requirements
### Requirement: YAML 配置文件格式
系统 SHALL 支持通过 YAML 配置文件定义全部运行参数,包括 server 配置、数据目录、拨测默认值和拨测目标列表。
#### Scenario: 完整配置文件解析
- **WHEN** 系统启动并读取包含 server、defaults、targets 的 YAML 配置文件
- **THEN** 系统 SHALL 正确解析所有字段并用于初始化服务
#### Scenario: 最简配置文件解析
- **WHEN** 系统读取只包含 targets 列表的 YAML 配置文件(省略 server 和 defaults
- **THEN** 系统 SHALL 使用内置默认值填充未指定的字段host=127.0.0.1, port=3000, dir=./data, interval=30s, timeout=10s, method=GET
#### Scenario: per-target 配置覆盖全局默认值
- **WHEN** 某个 target 指定了 interval、timeout 或 method
- **THEN** 该 target SHALL 使用其自身的值,不受 defaults 影响
### Requirement: CLI 参数
系统 SHALL 通过单一命令行参数接受 YAML 配置文件路径。
#### Scenario: 指定配置文件启动
- **WHEN** 用户执行 `./gateway-checker ./probes.yaml`
- **THEN** 系统 SHALL 读取并解析指定路径的 YAML 文件作为配置
#### Scenario: 未提供配置文件路径
- **WHEN** 用户启动程序时未提供任何命令行参数
- **THEN** 系统 SHALL 以错误退出并提示需要指定配置文件路径
#### Scenario: 配置文件不存在
- **WHEN** 用户指定的配置文件路径不存在
- **THEN** 系统 SHALL 以错误退出并提示文件不存在
### Requirement: 配置校验
系统 SHALL 在启动时对 YAML 配置进行完整校验,校验失败时以非零状态退出并输出清晰的错误信息。
#### Scenario: target 缺少必填字段
- **WHEN** YAML 中某个 target 缺少 name 或 url 字段
- **THEN** 系统 SHALL 以错误退出,提示哪个 target 缺少哪个字段
#### Scenario: target name 重复
- **WHEN** YAML 中存在两个 name 相同的 target
- **THEN** 系统 SHALL 以错误退出,提示重复的 name
#### Scenario: interval 格式非法
- **WHEN** interval 或 timeout 值不是有效的时长格式(如 `30s``5m`
- **THEN** 系统 SHALL 以错误退出并提示格式错误
### Requirement: YAML 配置使用 Bun 内置解析
系统 SHALL 使用 Bun 内置的 `Bun.YAML.parse()` 解析配置文件,不引入外部 YAML 解析库。
#### Scenario: 解析 YAML 内容
- **WHEN** 系统读取 YAML 文件内容
- **THEN** 系统 SHALL 调用 `Bun.YAML.parse()` 将内容解析为配置对象

View File

@@ -0,0 +1,69 @@
## ADDED Requirements
### Requirement: 总览统计卡片
Dashboard SHALL 在页面顶部展示总览统计卡片,包含总目标数、正常数、异常数和平均延迟。
#### Scenario: 展示统计卡片
- **WHEN** 用户打开 Dashboard 页面
- **THEN** 页面顶部 SHALL 显示 4 个统计卡片:全部目标数、正常目标数、异常目标数、所有目标平均延迟
#### Scenario: 统计数据自动刷新
- **WHEN** 页面处于打开状态
- **THEN** 统计卡片 SHALL 每 5-10 秒自动刷新数据
### Requirement: 目标列表表格
Dashboard SHALL 展示所有拨测目标的列表表格包含名称、URL、当前状态、最新延迟和迷你趋势线。
#### Scenario: 展示目标列表
- **WHEN** 用户打开 Dashboard 页面
- **THEN** 页面 SHALL 显示表格每行包含目标名称、URL、状态指示圆点● UP / ● DOWN、最新延迟值、迷你 Sparkline 趋势线
#### Scenario: 状态指示圆点
- **WHEN** 目标最近一次拨测 success=true 且 matched=true
- **THEN** 状态圆点 SHALL 显示为绿色UP
- **WHEN** 目标最近一次拨测 success=false 或 matched=false
- **THEN** 状态圆点 SHALL 显示为红色DOWN
### Requirement: 可展开的目标详情面板
Dashboard SHALL 支持在目标列表中展开某行,显示该目标的详细状态、统计摘要、趋势图和最近历史记录。
#### Scenario: 展开目标详情
- **WHEN** 用户点击目标列表中的某一行
- **THEN** 该行下方 SHALL 展开详情面板包含可用率百分比、平均延迟、P99 延迟、24 小时延迟趋势折线图、最近 5-10 条拨测记录列表
#### Scenario: 收起目标详情
- **WHEN** 用户再次点击已展开的目标行
- **THEN** 详情面板 SHALL 收起
#### Scenario: 趋势图按需加载
- **WHEN** 用户展开某个目标的详情面板
- **THEN** 系统 SHALL 此时请求该目标的趋势数据,而非页面加载时预加载所有目标的趋势数据
### Requirement: 历史记录展示
Dashboard SHALL 在目标详情面板中展示最近的拨测记录,包含时间、状态码、延迟和成功/失败标记。
#### Scenario: 展示历史记录
- **WHEN** 用户展开目标详情面板
- **THEN** 面板 SHALL 显示最近拨测记录列表每条包含时间戳、HTTP 状态码(或错误信息)、延迟毫秒数、成功/失败图标
### Requirement: 趋势图可视化
Dashboard SHALL 使用 recharts 库渲染趋势图,包括目标列表中的迷你 Sparkline 和详情面板中的完整折线图。
#### Scenario: 表格行内迷你趋势线
- **WHEN** 目标列表表格渲染
- **THEN** 每行 SHALL 包含一个基于 recharts 的迷你折线图,展示最近的延迟趋势
#### Scenario: 详情面板完整趋势图
- **WHEN** 用户展开目标详情面板
- **THEN** 面板 SHALL 展示基于 recharts 的完整折线图X 轴为时间小时Y 轴为平均延迟,并标注可用率
### Requirement: 页面加载与错误状态
Dashboard SHALL 正确处理加载状态和 API 错误。
#### Scenario: 首次加载
- **WHEN** 页面首次加载且数据尚未返回
- **THEN** 页面 SHALL 显示加载状态指示
#### Scenario: API 请求失败
- **WHEN** 前端轮询 API 请求失败
- **THEN** 页面 SHALL 显示错误提示,并在下一次轮询周期自动重试

View File

@@ -0,0 +1,56 @@
## ADDED Requirements
### Requirement: SQLite 数据库初始化
系统 SHALL 使用 Bun 内置 `bun:sqlite` 模块在配置的数据目录下创建 SQLite 数据库文件,并以 WAL 模式运行。
#### Scenario: 首次启动创建数据库
- **WHEN** 指定的数据目录下不存在数据库文件
- **THEN** 系统 SHALL 创建数据库文件并初始化 targets 和 check_results 表
#### Scenario: 数据目录不存在
- **WHEN** 配置的数据目录路径不存在
- **THEN** 系统 SHALL 自动创建该目录
#### Scenario: 数据库已存在时启动
- **WHEN** 数据库文件已存在
- **THEN** 系统 SHALL 直接打开数据库,不重新建表
### Requirement: targets 表同步
系统 SHALL 在启动时将 YAML 配置中的目标列表同步到 SQLite targets 表。
#### Scenario: 首次同步目标
- **WHEN** 数据库为空且 YAML 中定义了 N 个目标
- **THEN** 系统 SHALL 将所有目标插入 targets 表
#### Scenario: 配置变更后重新同步
- **WHEN** YAML 配置发生变更(新增、删除或修改目标)后重启
- **THEN** 系统 SHALL 根据 name 字段匹配:新增的插入、删除的移除、修改的更新
### Requirement: check_results 表追加写入
系统 SHALL 将每次拨测结果追加写入 check_results 表,不更新或删除已有记录。
#### Scenario: 写入拨测结果
- **WHEN** 一次拨测完成
- **THEN** 系统 SHALL 插入一条包含 target_id、timestamp、success、status_code、latency_ms、error、matched 的记录
### Requirement: 时间范围查询索引
系统 SHALL 在 check_results 表上创建 (target_id, timestamp) 复合索引,加速按目标和时间范围的查询。
#### Scenario: 查询某目标的历史记录
- **WHEN** 查询指定 target_id 的最近 N 条记录
- **THEN** 系统 SHALL 使用索引快速定位,无需全表扫描
### Requirement: 聚合查询支持
数据存储 SHALL 支持按时间段聚合查询用于计算可用率、平均延迟、P99 延迟等统计指标。
#### Scenario: 计算目标可用率
- **WHEN** 查询某目标在指定时间范围内的可用率
- **THEN** 系统 SHALL 返回 UP (success=true AND matched=true) 的记录数占总记录数的百分比
#### Scenario: 计算目标平均延迟
- **WHEN** 查询某目标在指定时间范围内的平均延迟
- **THEN** 系统 SHALL 返回 latency_ms 的平均值(仅计算 success=true 的记录)
#### Scenario: 按小时聚合趋势数据
- **WHEN** 查询某目标在指定时间范围内的趋势数据
- **THEN** 系统 SHALL 返回按小时分组的聚合数据,包括每小时的平均延迟和可用率

View File

@@ -0,0 +1,83 @@
## ADDED Requirements
### Requirement: 按 interval 分组调度
系统 SHALL 将拨测目标按 interval 值分组,每组使用独立的定时器进行调度。
#### Scenario: 相同 interval 的目标共享定时器
- **WHEN** 多个 target 配置了相同的 interval如 30s
- **THEN** 系统 SHALL 使用同一个 `setInterval` 定时器,每次 tick 并发拨测所有该组目标
#### Scenario: 不同 interval 的目标各自调度
- **WHEN** target A 配置 15s intervaltarget B 配置 30s interval
- **THEN** 系统 SHALL 创建两个独立定时器,分别按各自频率调度
### Requirement: 组内并发拨测
系统 SHALL 在每次调度 tick 时,使用 `Promise.all` 并发执行同组内所有目标的拨测。
#### Scenario: 同组目标并发执行
- **WHEN** 调度器触发一次 tick该组有 3 个目标
- **THEN** 系统 SHALL 同时发起 3 个 HTTP 请求,而非顺序执行
#### Scenario: 单个目标失败不影响同组其他目标
- **WHEN** 同组中某个目标的拨测请求超时或失败
- **THEN** 其他目标的拨测 SHALL 正常完成并记录结果
### Requirement: HTTP 拨测执行
系统 SHALL 对每个目标执行 HTTP 请求,支持 GET、POST、PUT、DELETE、PATCH、HEAD 方法,并携带配置的 headers 和 body。
#### Scenario: 执行 GET 请求
- **WHEN** 目标配置 method 为 GET
- **THEN** 系统 SHALL 发送 GET 请求到目标 URL
#### Scenario: 执行 POST 请求带 body
- **WHEN** 目标配置 method 为 POST 且指定了 body 和 Content-Type header
- **THEN** 系统 SHALL 发送带指定 body 的 POST 请求
#### Scenario: 携带自定义 headers
- **WHEN** 目标配置了 headers如 Authorization
- **THEN** 系统 SHALL 在请求中包含所有配置的 headers
### Requirement: 请求超时控制
系统 SHALL 对每次拨测请求实施超时控制,超时时间使用目标配置的 timeout 值。
#### Scenario: 请求超时
- **WHEN** 拨测请求在 timeout 时间内未收到响应
- **THEN** 系统 SHALL 中止该请求,记录为失败并标注超时错误
#### Scenario: 请求在超时前完成
- **WHEN** 拨测请求在 timeout 时间内收到响应
- **THEN** 系统 SHALL 正常记录响应结果
### Requirement: expect 校验
系统 SHALL 在拨测完成后根据目标的 expect 配置校验响应,校验结果记入 check result。
#### Scenario: 校验状态码
- **WHEN** 目标配置了 `expect.status: [200, 201]`
- **THEN** 系统 SHALL 检查响应状态码是否在列表中,将匹配结果记录到 matched 字段
#### Scenario: 校验响应体包含
- **WHEN** 目标配置了 `expect.bodyContains: "healthy"`
- **THEN** 系统 SHALL 检查响应体是否包含该文本,将匹配结果记录到 matched 字段
#### Scenario: 校验延迟阈值
- **WHEN** 目标配置了 `expect.maxLatencyMs: 3000`
- **THEN** 系统 SHALL 检查实际延迟是否超过阈值,将匹配结果记录到 matched 字段
#### Scenario: 无 expect 配置
- **WHEN** 目标未配置任何 expect 规则
- **THEN** 系统 SHALL 将 matched 字段设为 true
#### Scenario: 多条 expect 规则
- **WHEN** 目标同时配置了 status、bodyContains 和 maxLatencyMs
- **THEN** 系统 SHALL 所有规则全部通过时 matched 为 true任一不通过则为 false
### Requirement: 拨测结果记录
系统 SHALL 在每次拨测完成后,将结果写入 SQLite 数据存储,包含 target_id、timestamp、success、status_code、latency_ms、error、matched 字段。
#### Scenario: 成功拨测结果记录
- **WHEN** 拨测请求成功完成(收到 HTTP 响应)
- **THEN** 系统 SHALL 记录 success=true、status_code、latency_ms、matched
#### Scenario: 失败拨测结果记录
- **WHEN** 拨测请求失败(网络错误、超时等)
- **THEN** 系统 SHALL 记录 success=false、error 信息status_code 和 latency_ms 为 null

View File

@@ -0,0 +1,62 @@
## 1. 项目准备与依赖
- [x] 1.1 清理 demo 代码:移除 /api/demo 路由、DemoResponse 类型、前端 demo 展示逻辑
- [x] 1.2 安装 recharts 依赖
- [x] 1.3 创建 src/server/checker/ 目录结构和类型定义文件 types.ts
- [x] 1.4 创建示例 YAML 配置文件 probes.example.yaml
## 2. 配置解析层
- [x] 2.1 实现 YAML 配置类型定义ProbeConfig、TargetConfig、ExpectConfig 等)
- [x] 2.2 实现 config-loader.ts读取文件 + Bun.YAML.parse + 配置校验必填字段、name 唯一性、interval 格式、port 范围)
- [x] 2.3 重写 src/server/config.tsCLI 只接受配置文件路径参数,从 YAML 读取 host/port/dataDir
- [x] 2.4 为配置解析和校验编写完整测试
## 3. 数据存储层
- [x] 3.1 实现 store.tsSQLite 初始化建表、WAL 模式、复合索引)、数据目录自动创建
- [x] 3.2 实现 targets 表同步逻辑(根据 name 匹配:新增插入、删除移除、修改更新)
- [x] 3.3 实现 check_results 追加写入方法
- [x] 3.4 实现查询方法:按 target+时间范围查询、按小时聚合趋势、计算可用率/平均延迟/P99
- [x] 3.5 为数据存储层编写完整测试(初始化、同步、写入、查询、聚合)
## 4. 拨测引擎
- [x] 4.1 实现 fetcher.tsHTTP 请求执行method/header/body+ AbortController 超时控制
- [x] 4.2 实现 expect 校验逻辑status 列表匹配、bodyContains、maxLatencyMs
- [x] 4.3 实现 engine.ts按 interval 分组 → setInterval → Promise.all 并发拨测 → 结果写入 store
- [x] 4.4 为 fetcher 和 expect 校验编写完整测试(使用 mock HTTP server
- [x] 4.5 为调度引擎编写完整测试(分组逻辑、并发执行、单目标失败隔离)
## 5. API 路由层
- [x] 5.1 定义 src/shared/api.ts 响应类型SummaryResponse、TargetStatus、CheckResult、TrendPoint
- [x] 5.2 重写 src/server/app.ts注册新 API 路由(/api/summary、/api/targets、/api/targets/:id/history、/api/targets/:id/trend保留 /health
- [x] 5.3 实现 API 错误处理(目标不存在返回 404、参数无效返回 400
- [x] 5.4 为 API 路由编写完整测试(各端点正常响应、边界情况、错误处理)
## 6. 前端 Dashboard
- [x] 6.1 创建前端组件目录结构 src/web/components/ 和 src/web/hooks/
- [x] 6.2 实现 hooksuseSummary轮询 /api/summary、useTargets轮询 /api/targets、useTrend按需加载趋势数据
- [x] 6.3 实现 StatusDot 组件(绿色 UP / 红色 DOWN 圆点)
- [x] 6.4 实现 SummaryCards 组件4 个统计卡片)
- [x] 6.5 实现 SparklineChart 组件recharts 迷你折线图)
- [x] 6.6 实现 TrendChart 组件recharts 完整折线图,含时间轴和双 Y 轴)
- [x] 6.7 实现 TargetRow 组件表格行名称、URL、状态、延迟、Sparkline可展开
- [x] 6.8 实现 TargetDetail 组件(展开面板:统计摘要、趋势图、历史记录列表)
- [x] 6.9 实现 TargetTable 组件(组合 TargetRow 和 TargetDetail
- [x] 6.10 重写 App.tsx组合 SummaryCards + TargetTable处理加载和错误状态
- [x] 6.11 重写 styles.cssDashboard 布局样式(卡片、表格、详情面板、响应式)
- [x] 6.12 更新 vite.config.ts 代理配置确保 /api/* 转发
## 7. 集成与启动流程
- [x] 7.1 重写 src/server/dev.ts 和 src/server/server.ts启动流程为 读取配置 → 初始化 store → 同步 targets → 启动 engine → 启动 HTTP server
- [x] 7.2 更新构建脚本确保 recharts 正确打包进 executable
- [x] 7.3 更新 README.md新的 CLI 用法、YAML 配置说明、API 端点文档、项目结构变更
## 8. 端到端验证
- [x] 8.1 更新 smoke test 脚本适配新的 API 端点和前端路由
- [x] 8.2 手动验证完整流程YAML 配置 → 启动 → 拨测执行 → Dashboard 展示

View File

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

View File

@@ -0,0 +1,255 @@
## Context
当前实现的配置、执行、存储、API 和 Dashboard 都以 HTTP 请求为中心:`target.url` 是必填字段,执行器直接 `fetch(url)`,结果存储包含 `status_code``latency_ms`,前端展示 URL、method 和 HTTP 状态码。这种模型无法承载本地命令等非 HTTP checker也让 `expect` 只能表达 HTTP response 的 status/header/body。
项目尚未上线,不需要兼容旧 YAML、旧数据库 schema 或旧 API 契约,因此本次设计选择直接建立 typed target 与领域专用 expect而不是添加兼容分支。目标是让 HTTP 变成 runner 的一种实现,同时新增 command runner并为未来其他 checker 类型保留清晰扩展点。
```text
YAML target
ResolvedTarget(type)
ProbeEngine + concurrency limit
├─ http runner
│ └─ HTTP expect pipeline
└─ command runner
└─ command expect pipeline
CheckResult(success, matched, durationMs, statusDetail, failure)
SQLite + API + Dashboard
```
## Goals / Non-Goals
**Goals:**
- 使用 `target.type` 建模不同 checker 类型v1 支持 `http``command`
- 将 HTTP 配置放入 `target.http`,将命令配置放入 `target.command`,移除顶层 HTTP 字段。
- 为各 checker 类型定义领域专用 expect 名称HTTP 使用 `status``headers``body`command 使用 `exitCode``stdout``stderr`
- 为不同 checker 类型提供默认成功语义HTTP 默认 `status: [200]`command 默认 `exitCode: [0]`
- 将可排序内容检查表达为数组,保证 `body``stdout``stderr` 按配置顺序执行。
- 在 runner 和 expect pipeline 层共同实现快速失败,避免 status/header 已失败时仍读取或解析 body。
- 使用 `durationMs` 表达 checker 执行耗时,替代 HTTP-only 的 `latencyMs`
- 引入结构化失败信息并入库,区分执行错误和 expect 不匹配,耗时阈值字段统一为 `maxDurationMs`
- 引入全局并发限制和 100MB 默认读取上限,避免 HTTP body 或 command 输出造成资源失控。
**Non-Goals:**
- 不兼容旧的顶层 `url``method``headers``body` 配置。
- 不做旧 SQLite schema 迁移;实现阶段可以按新 schema 初始化和测试。
- 不支持 shell 字符串命令command v1 仅支持 `exec + args`
- 不持久化完整 HTTP body、stdout 或 stderr只持久化结构化失败摘要。
- 不引入新的解析或执行依赖。
- 不在本次实现告警通知、认证鉴权或动态增删目标。
## Decisions
### 1. 使用判别联合建模 Target
配置和解析后的目标都使用 `type` 判别:
```yaml
targets:
- name: "HTTP 健康检查"
type: http
http:
url: "https://example.com/health"
method: GET
- name: "Nginx 进程检查"
type: command
command:
exec: "pgrep"
args: ["nginx"]
```
理由HTTP 与 command 的领域字段差异明显,强行把 URL、exec、status、exitCode 抽成统一字段会降低语义清晰度。判别联合可以让 TypeScript 在执行器选择、配置校验和 expect 校验中获得更明确的类型约束。
替代方案:保留顶层 `url` 并通过字段存在性推断 HTTP。该方案兼容性更好但会继续让 HTTP 成为隐式默认类型,不符合当前无兼容包袱下的最佳模型。
### 2. defaults 分为通用和领域分组
建议配置形态:
```yaml
runtime:
maxConcurrentChecks: 20
defaults:
interval: "30s"
timeout: "10s"
http:
method: GET
maxBodyBytes: "100MB"
command:
cwd: "."
maxOutputBytes: "100MB"
```
通用默认值只覆盖所有 checker 都共享的调度与超时字段,领域默认值只覆盖对应 target type。target 自身配置优先级高于 defaults。
替代方案:继续使用 `defaults.method``defaults.headers` 等 HTTP 字段。该方案会在 command target 中产生无意义字段,因此不采用。
### 3. 默认 expect 是逻辑默认值
当用户未显式配置对应状态类 expect 时runner 在校验阶段应用领域默认值,而不是把默认值写回用户配置。
HTTP 默认:`status: [200]`
Command 默认:`exitCode: [0]`
示例:
```yaml
expect:
body:
- contains: "ok"
```
该 HTTP target 仍然先检查 `status == 200`,再检查 body。这样用户只写内容检查时不会把 HTTP 500 错误响应误判为 UP。
替代方案:只有完全不写 `expect` 时才应用默认值。该方案会让“只写 body”绕过 status 检查,不符合默认成功语义,因此不采用。
### 4. Expect pipeline 使用固定阶段顺序和有序规则数组
HTTP 顺序:
```text
status -> duration -> headers -> body[0] -> body[1] -> ...
```
Command 顺序:
```text
exitCode -> duration -> stdout[0] -> stdout[1] -> ... -> stderr[0] -> stderr[1] -> ...
```
`body``stdout``stderr` 使用数组表达配置顺序:
```yaml
expect:
body:
- contains: "healthy"
- json:
path: "$.status"
equals: "ok"
- regex: '"version":"\\d+\\.\\d+"'
```
理由:对象字段天然更像无序集合,不适合表达用户指定的检查顺序。数组规则可以直接生成 `path`,例如 `expect.body[1].json($.status)`,方便失败定位。
替代方案:保留对象结构并约定 contains/regex/json/css/xpath 固定顺序。该方案无法满足“按配置文件中的配置顺序依次检查”的要求,因此不采用。
### 5. 复用通用值操作符,但保持领域 expect 名称
保留并扩展现有操作符:`equals``contains``match``empty``exists``gte``lte``gt``lt`。这些操作符可用于 HTTP header、HTTP body 提取值、command stdout/stderr 文本等。
领域名称保持专用HTTP 使用 `status`command 使用 `exitCode`HTTP body 可使用 `json/css/xpath`command 输出只使用文本规则和通用操作符。
替代方案:把所有值统一抽象成 `status``metadata``payload`。该方案过度泛化,会让 YAML 对使用者不直观,因此不采用。
### 6. Runner 负责按需产生 Observation
HTTP runner 不应总是读取完整 response body。它先发起请求并取得 status、headers 和 duration再运行 status/duration/headers 阶段;只有配置中存在 body 规则且前置阶段通过时,才读取 body并受 `maxBodyBytes` 限制。
Command runner 需要执行命令并收集 exitCode、duration、stdout、stderr。stdout 和 stderr 合计受 `maxOutputBytes` 限制,默认 `100MB`。命令超时或输出超限时runner 产生 `success=false``failure.kind=error`
替代方案runner 总是完整产生所有字段,再交给 expect。该方案实现简单但无法真正快速失败也无法避免不必要的资源读取因此不采用。
### 7. Command 执行不经过 shell
command target 使用 `exec + args`,实现阶段优先使用 Bun 可用的子进程 API并禁止默认 shell 展开。
```yaml
command:
exec: "pgrep"
args: ["nginx"]
cwd: "."
env:
LANG: "C"
```
`cwd` 相对配置文件所在目录解析。`env` 默认继承当前进程环境并允许覆盖指定键。v1 不支持 stdin避免命令阻塞。
替代方案:允许 `shell: "pgrep nginx | wc -l"`。该方案更灵活,但引入转义、注入和跨平台 shell 差异,不适合作为第一版默认能力。
### 8. 全局并发限制由 ProbeEngine 统一执行
`runtime.maxConcurrentChecks` 默认 20。调度仍按 interval 分组触发,但每个目标进入全局并发池后再执行,避免同一 tick 或多个 tick 同时启动过多 HTTP 请求和本地进程。
理由command target 可能启动本地进程,继续无限 `Promise.allSettled` 会有资源风险。全局限制比按组限制更容易理解,也能覆盖不同 interval 组同时触发的情况。
替代方案:为 HTTP 和 command 分别设置并发上限。该方案更精细,但增加配置复杂度,当前需求只要求全局默认值。
### 9. CheckResult 使用结构化 failure
结果模型区分 runner 执行失败和 expect 不匹配:
```ts
interface CheckFailure {
kind: "error" | "mismatch";
phase: "status" | "duration" | "headers" | "body" | "exitCode" | "stdout" | "stderr";
path: string;
expected?: unknown;
actual?: unknown;
message: string;
}
```
`success=false` 表示 runner 未能正常产生可校验结果,例如网络错误、超时、命令启动失败、输出超限。`matched=false` 表示 runner 执行成功但 expect 不匹配。`failure` 字段存储首个失败原因,实际值需要截断,避免超长内容或敏感内容进入数据库和 API。
替代方案:继续只存 `error` 字符串。该方案无法区分执行失败与规则不匹配,也不能准确定位失败 path因此不采用。
### 10. 存储、API、Dashboard 改为 checker 通用语义
SQLite schema 建议从 HTTP-only 字段调整为:
```text
targets:
id, name, type, target, config, interval_ms, timeout_ms, expect
check_results:
id, target_id, timestamp, success, matched, duration_ms, status_detail, failure
```
`target` 是用于展示和搜索的目标摘要,例如 HTTP URL 或 command 命令行摘要;`config` 持久化解析后的领域配置 JSON`status_detail` 存储领域状态摘要,例如 `HTTP 200``exitCode=1`
API 共享类型使用 `durationMs``statusDetail``failure`Dashboard 表格展示“类型、目标、状态、耗时、最近失败原因、趋势”。HTTP 详情可显示 status codecommand 详情可显示 exit code但列表层不使用 HTTP-only 列名。
替代方案:继续保留 `url``method``status_code``latency_ms` 并为 command 填空。该方案会把领域语义混在一起,后续扩展成本高,因此不采用。
### 11. Size 字符串解析
新增 size 解析支持 `B``KB``MB``GB`,默认 `100MB` 等于 `104857600` bytes。HTTP `maxBodyBytes` 限制单次 body 读取command `maxOutputBytes` 限制 stdout 和 stderr 合计读取。
理由YAML 直接写字节数可读性差,二进制单位更适合内存和 buffer 限制。
替代方案:复用 duration 解析或只接受 number。前者语义不匹配后者配置可读性差。
## Risks / Trade-offs
- [Risk] `maxConcurrentChecks=20` 且单次读取上限为 `100MB` 时理论内存峰值较高 → [Mitigation] 提供全局并发限制和 per-target/per-default 读取上限,文档明确资源上限由用户配置共同决定。
- [Risk] 结构化失败信息可能包含敏感响应片段或命令输出 → [Mitigation] 只存首个失败原因,`actual` 做长度截断,默认不持久化完整 body/stdout/stderr。
- [Risk] command checker 允许执行本地命令,有误配置或高开销命令风险 → [Mitigation] 不支持 shell强制 timeout限制输出大小使用全局并发限制。
- [Risk] 不兼容旧配置会导致现有样例和测试全部失效 → [Mitigation] 项目未上线,实施时同步更新 README、示例配置、单元测试和 smoke test。
- [Risk] SQLite schema 重建会丢失旧数据 → [Mitigation] 当前无上线数据,不做迁移;若后续需要升级已部署实例,应另起兼容迁移 change。
## Migration Plan
- 更新类型定义、配置解析和 README 示例,先让新 YAML 契约成为唯一入口。
- 重构存储 schema 和共享 API 类型,再更新 Dashboard 使用新字段。
- 引入 expect 规则数组和结构化 failure迁移 HTTP runner 到新 pipeline。
- 添加 command runner并接入 ProbeEngine 的 runner 选择与全局并发限制。
- 更新测试覆盖配置、HTTP expect、command expect、存储、API、Dashboard 和 smoke test。
- 运行 `bun run check``bun run verify`,确保完整质量门禁通过。
## Open Questions
无。当前讨论已确认默认 HTTP status 使用 `[200]`、默认并发限制使用全局配置、HTTP body 与 command 输出默认上限均为 `100MB`

View File

@@ -0,0 +1,39 @@
## Why
当前系统以 HTTP 请求作为唯一 checker 形态,`target``expect`、存储、API 和 Dashboard 都围绕 URL、HTTP method、status code 与 response body 建模,无法自然表达本地命令检查等非 HTTP 场景。项目尚未上线,没有兼容性约束,适合一次性重构为面向多种 checker 类型的清晰模型。
## What Changes
- **BREAKING**: 移除顶层 `target.url``target.method``target.headers``target.body` 配置形态,改为 `target.type` 判别不同 checker 类型,并将领域字段放入 `http``command` 分组。
- **BREAKING**: `expect.body` 从对象分组改为有序规则数组,按配置顺序执行并快速失败。
- 引入 `http` target 类型,支持 HTTP URL、method、headers、body、最大 body 读取字节数和 HTTP 专用 expect。
- 引入 `command` target 类型,支持本地命令 `exec + args``cwd``env`、最大输出读取字节数和 command 专用 expect。
- 为不同 checker 类型提供领域默认成功语义HTTP 默认 `expect.status: [200]`command 默认 `expect.exitCode: [0]`
- 引入全局并发限制 `runtime.maxConcurrentChecks`,默认值为 20。
- 引入 size 配置解析,支持 `B``KB``MB``GB`HTTP `maxBodyBytes` 和 command `maxOutputBytes` 默认均为 `100MB`
- 调整 expect 执行管线:先执行状态类检查,再执行耗时检查,再执行元数据或内容检查;同一内容字段内部按数组顺序检查,任一失败立即返回结构化失败信息。
- 将 check result 的失败信息结构化入库,区分 runner 执行错误与 expect 不匹配,便于后续追查。
- 将 DB、API、Dashboard 从 HTTP-only 字段命名调整为通用 checker 展示模型,同时保留 HTTP 和 command 的领域专用细节。
- 同步更新 README、示例配置和测试覆盖 typed target、默认 expect、快速失败、输出限制、失败信息和 Dashboard 展示。
## Capabilities
### New Capabilities
- `command-checker`: 定义本地命令 checker 的配置、执行、安全边界、默认成功语义、输出限制和 expect 校验。
### Modified Capabilities
- `probe-config`: YAML 配置从 HTTP-only target 改为 typed target新增 runtime 并发限制、HTTP/command 默认配置和 size 字符串解析。
- `probe-engine`: 调度引擎从固定 HTTP fetch 改为按 target type 选择 runner并在全局并发限制下执行检查。
- `expect-body-checkers`: HTTP body expect 改为有序规则数组,通用值操作符可复用于 stdout/stderr/header/body 等不同字段。
- `probe-data-store`: targets 和 check_results schema 从 HTTP-only 字段改为 checker 通用字段,并持久化结构化失败信息。
- `probe-api`: API 响应从 URL/method/statusCode/latencyMs 为中心改为 type/target/durationMs/statusDetail/failure 等通用 checker 契约。
- `probe-dashboard`: Dashboard 从 HTTP 拨测视图调整为 checker 通用视图,展示类型、目标、耗时、最近失败原因和领域状态详情。
## Impact
- 影响后端类型定义、配置加载校验、调度执行、HTTP runner、command runner、expect 校验模块、SQLite schema、聚合查询和 API 映射。
- 影响前后端共享 API 类型和 Dashboard 表格、详情、历史记录、趋势图展示字段。
- 影响 README、`probes.example.yaml`、单元测试和 smoke test 配置样例。
- 不引入新依赖,优先复用 Bun、TypeScript、现有 cheerio/xpath 和 SQLite 能力。

View File

@@ -0,0 +1,78 @@
## ADDED Requirements
### Requirement: command target 配置
系统 SHALL 支持 `type: command` 的 target 配置,通过 `command.exec``command.args` 描述本地命令,并使用 command 专用字段配置工作目录、环境变量和输出限制。
#### Scenario: 解析 command target
- **WHEN** YAML 中 target 配置 `type: command``command.exec: "pgrep"``command.args: ["nginx"]`
- **THEN** 系统 SHALL 将其解析为 command checker并保留 exec、args、cwd、env、maxOutputBytes、interval、timeout 和 expect 配置
#### Scenario: command target 缺少 exec
- **WHEN** YAML 中 target 配置 `type: command` 但缺少 `command.exec`
- **THEN** 系统 SHALL 以配置错误退出,并提示该 target 缺少 command.exec 字段
#### Scenario: cwd 相对配置文件目录解析
- **WHEN** command target 配置 `command.cwd: "scripts"` 且配置文件位于 `/opt/checker/probes.yaml`
- **THEN** 系统 SHALL 将 cwd 解析为 `/opt/checker/scripts`
#### Scenario: command 不使用 shell
- **WHEN** command target 配置 `exec``args`
- **THEN** 系统 MUST 直接执行该程序和参数,不通过 shell 解释整段命令字符串
#### Scenario: env 默认继承并允许覆盖
- **WHEN** command target 配置 `command.env: {LANG: "C"}` 且当前进程环境包含 `PATH`
- **THEN** 系统 SHALL 继承当前进程的全部环境变量,并将 `LANG` 覆盖为 `"C"`
#### Scenario: 不支持 stdin
- **WHEN** command target 配置并执行命令
- **THEN** 系统 MUST NOT 向子进程 stdin 写入数据,避免命令因等待输入而阻塞
### Requirement: command checker 执行
系统 SHALL 按 command target 配置执行本地命令记录执行耗时、退出码、stdout 和 stderr并在执行失败时产生结构化错误信息。
#### Scenario: 命令正常退出
- **WHEN** command target 执行的进程正常退出且 exit code 为 0
- **THEN** 系统 SHALL 记录 `success=true``durationMs``statusDetail="exitCode=0"`,并进入 expect 校验
#### Scenario: 命令非零退出
- **WHEN** command target 执行的进程正常退出但 exit code 为 1
- **THEN** 系统 SHALL 记录 `success=true``statusDetail="exitCode=1"`,并由 expect.exitCode 决定 matched 结果
#### Scenario: 命令启动失败
- **WHEN** command target 的 exec 不存在或无法启动
- **THEN** 系统 SHALL 记录 `success=false``matched=false`,并在 failure 中写入 kind=`error`、phase=`exitCode` 和可读错误信息
#### Scenario: 命令超时
- **WHEN** command target 在 timeout 时间内未结束
- **THEN** 系统 MUST 终止该子进程,记录 `success=false``matched=false`,并在 failure 中写入命令超时信息
#### Scenario: 命令输出超限
- **WHEN** command target 的 stdout 和 stderr 合计输出超过 `maxOutputBytes`
- **THEN** 系统 MUST 停止收集输出并终止该检查,记录 `success=false``matched=false`,并在 failure 中写入输出超限信息
### Requirement: command expect 校验
系统 SHALL 支持 command 专用 expect包括 `exitCode``stdout``stderr`,并按 exitCode、duration、stdout、stderr 的阶段顺序快速失败。
#### Scenario: 默认 exitCode 成功语义
- **WHEN** command target 未显式配置 `expect.exitCode`
- **THEN** 系统 SHALL 使用默认 `expect.exitCode: [0]` 进行校验
#### Scenario: 显式 exitCode 校验
- **WHEN** command target 配置 `expect.exitCode: [0, 2]` 且实际 exit code 为 2
- **THEN** 系统 SHALL 判定 exitCode 阶段通过,并继续后续 expect 阶段
#### Scenario: exitCode 不匹配快速失败
- **WHEN** command target 配置 `expect.exitCode: [0]` 且实际 exit code 为 1
- **THEN** 系统 SHALL 立即返回 `matched=false`,并在 failure 中写入 phase=`exitCode`、path=`expect.exitCode`、expected 和 actual
#### Scenario: stdout 按配置顺序校验
- **WHEN** command target 配置 `expect.stdout` 为两个规则,第一条通过且第二条失败
- **THEN** 系统 SHALL 先执行第一条 stdout 规则,再执行第二条,并将 failure.path 指向失败的 `expect.stdout[1]`
#### Scenario: stderr 校验为空
- **WHEN** command target 配置 `expect.stderr: [{empty: true}]` 且实际 stderr 为空字符串
- **THEN** 系统 SHALL 判定 stderr 阶段通过
#### Scenario: stdout 失败后不检查 stderr
- **WHEN** command target 同时配置 stdout 和 stderr 规则,且 stdout 规则失败
- **THEN** 系统 SHALL 快速失败并 MUST NOT 继续执行 stderr 规则

View File

@@ -0,0 +1,126 @@
## MODIFIED Requirements
### Requirement: 响应体多种校验方法
系统 SHALL 支持对 HTTP 响应体进行五种可组合的校验方法contains子串、regex正则、jsonJSONPath、cssCSS 选择器、xpathXPath。这些方法 MUST 配置在 `expect.body` 有序数组中。
#### Scenario: contains 子串匹配
- **WHEN** HTTP target 配置 `expect.body: [{contains: "healthy"}]`,且响应体包含 `"healthy"`
- **THEN** 系统 SHALL 判定该 body 规则通过
#### Scenario: contains 不匹配
- **WHEN** HTTP target 配置 `expect.body: [{contains: "healthy"}]`,且响应体不包含该文本
- **THEN** 系统 SHALL 判定 matched 为 false并记录该规则的 failure.path
#### Scenario: regex 正则匹配
- **WHEN** HTTP target 配置 `expect.body: [{regex: '"status"\\s*:\\s*"ok"'}]`,且响应体匹配该正则
- **THEN** 系统 SHALL 判定该 body 规则通过
#### Scenario: regex 不匹配
- **WHEN** HTTP target 配置 regex body 规则,且响应体不匹配该正则
- **THEN** 系统 SHALL 判定 matched 为 false并记录该规则的 failure.path
#### Scenario: json JSONPath 等值匹配
- **WHEN** HTTP target 配置 `expect.body: [{json: {path: "$.status", equals: "ok"}}]`,且响应 JSON 中 `$.status` 值为 `"ok"`
- **THEN** 系统 SHALL 判定该 body 规则通过
#### Scenario: json JSONPath 值不匹配
- **WHEN** HTTP target 配置 json body 规则,且提取值不符合期望
- **THEN** 系统 SHALL 判定 matched 为 false并记录包含 JSONPath 的 failure.path
#### Scenario: json 解析失败
- **WHEN** HTTP target 配置了 json body 规则但响应体不是合法 JSON
- **THEN** 系统 SHALL 判定 matched 为 false
#### Scenario: css 选择器匹配
- **WHEN** HTTP target 配置 `expect.body: [{css: {selector: "div#health", equals: "OK"}}]`,且 HTML 中存在 `div#health` 元素文本为 `"OK"`
- **THEN** 系统 SHALL 判定该 body 规则通过
#### Scenario: css 选择器匹配属性值
- **WHEN** HTTP target 配置 css 规则带 `attr: "content"` 用于提取属性,且属性值匹配期望
- **THEN** 系统 SHALL 判定该 body 规则通过
#### Scenario: css 选择器无匹配元素
- **WHEN** HTTP target 配置了 css 选择器但 HTML 中无匹配元素
- **THEN** 系统 SHALL 判定 matched 为 false
#### Scenario: xpath 表达式匹配
- **WHEN** HTTP target 配置 `expect.body: [{xpath: {path: "/root/status/text()", equals: "ok"}}]`,且 XML 中 `/root/status` 节点文本为 `"ok"`
- **THEN** 系统 SHALL 判定该 body 规则通过
#### Scenario: xpath 表达式无匹配节点
- **WHEN** HTTP target 配置了 xpath 表达式但 XML 中无匹配节点
- **THEN** 系统 SHALL 判定 matched 为 false
### Requirement: 多种 body 校验方法 AND 组合
系统 SHALL 支持在 `expect.body` 数组中同时配置多种 body 校验方法,所有方法均通过时 matched 方为 true。
#### Scenario: 多种方法全部通过
- **WHEN** HTTP target 的 `expect.body` 数组依次配置 contains、json、regex且全部通过
- **THEN** 系统 SHALL 判定 matched 为 true
#### Scenario: 多种方法任一失败
- **WHEN** HTTP target 的 `expect.body` 数组第一条 contains 不通过,后续还有 json 规则
- **THEN** 系统 SHALL 判定 matched 为 false且不再检查后续 json 规则
### Requirement: 操作符系统
系统 SHALL 支持对提取值和文本值使用以下操作符进行比较equals默认等值、contains子串包含、match正则匹配、empty空值判断、exists存在性判断、gte/lte/gt/lt数值比较
#### Scenario: 标量值隐式 equals
- **WHEN** 配置的期望值为标量(字符串/数字/布尔/null`equals: "ok"`
- **THEN** 系统 SHALL 使用 equals 操作符,对实际值做严格相等比较
#### Scenario: 显式 contains 操作符
- **WHEN** 配置 `{contains: "success"}`,且实际值包含 `"success"`
- **THEN** 系统 SHALL 判定该规则通过
#### Scenario: 显式 match 操作符
- **WHEN** 配置 `{match: '\\d+\\.\\d+\\.\\d+'}`,且实际值匹配该正则
- **THEN** 系统 SHALL 判定该规则通过
#### Scenario: empty 操作符判断为空
- **WHEN** 配置 `{empty: true}`,且实际值为空数组 `[]`
- **THEN** 系统 SHALL 判定该规则通过
#### Scenario: empty 操作符判断非空
- **WHEN** 配置 `{empty: false}`,且实际值为 `[1, 2]`
- **THEN** 系统 SHALL 判定该规则通过
#### Scenario: exists 操作符判断存在
- **WHEN** 配置 `{exists: false}`,且实际值不存在
- **THEN** 系统 SHALL 判定该规则通过
#### Scenario: gte 数值比较
- **WHEN** 配置 `{gte: 10}`,且实际值为 `15`(数字)
- **THEN** 系统 SHALL 判定该规则通过
#### Scenario: gt/lt 数值比较
- **WHEN** 配置 `{gt: 0, lt: 1000}`,且实际值为 `500`
- **THEN** 系统 SHALL 对同一字段进行多操作符复合比较,全部通过则该规则通过
### Requirement: 响应头校验
系统 SHALL 支持通过 `expect.headers` 配置对 HTTP 响应头进行键值规则校验header 名称匹配 MUST 不区分大小写。
#### Scenario: 响应头匹配
- **WHEN** HTTP target 配置 `expect.headers: {"Content-Type": {contains: "application/json"}}`,且响应包含该 header 且值匹配
- **THEN** 系统 SHALL 判定 headers 阶段通过
#### Scenario: 响应头不匹配
- **WHEN** HTTP target 配置 `expect.headers: {"Content-Type": {equals: "application/json"}}`,且响应 header 值为 `"text/html"`
- **THEN** 系统 SHALL 判定 matched 为 false
#### Scenario: 响应头缺失
- **WHEN** HTTP target 配置了某个 header 但响应中不存在该 header
- **THEN** 系统 SHALL 判定 matched 为 false
## ADDED Requirements
### Requirement: 结构化 expect 失败信息
系统 SHALL 在任一 expect 规则失败时生成结构化 failure用于标识失败阶段、规则路径、期望值、实际值和可读错误信息。
#### Scenario: body 规则失败信息
- **WHEN** HTTP target 的 `expect.body[1].json` 规则失败
- **THEN** failure SHALL 包含 kind=`mismatch`、phase=`body`、path 指向 `expect.body[1]`,并包含 message
#### Scenario: actual 值截断
- **WHEN** 失败规则的实际值超过系统允许记录的摘要长度
- **THEN** 系统 MUST 截断 actual 摘要,而不是持久化完整响应体或命令输出

View File

@@ -0,0 +1,54 @@
## MODIFIED Requirements
### Requirement: 总览统计 API
系统 SHALL 提供 `GET /api/summary` 端点,返回所有目标的总体统计信息。
#### Scenario: 获取总览统计
- **WHEN** 客户端请求 `GET /api/summary`
- **THEN** 系统 SHALL 返回 JSON 包含 total总目标数、up正常数、down异常数、avgDurationMs所有目标平均耗时、lastCheckTime最近一次检查时间
### Requirement: 目标列表 API
系统 SHALL 提供 `GET /api/targets` 端点,返回所有 typed target 及其最新状态和统计摘要。
#### Scenario: 获取目标列表
- **WHEN** 客户端请求 `GET /api/targets`
- **THEN** 系统 SHALL 返回 JSON 数组每个元素包含目标基本信息id、name、type、target、interval、最近一次检查结果timestamp、success、matched、durationMs、statusDetail、failure和统计摘要totalChecks、availability、avgDurationMs、p99DurationMs
#### Scenario: 目标无历史记录
- **WHEN** 某目标尚未执行过任何检查
- **THEN** 其 latestCheck 为 nullstats 中 totalChecks 为 0
### Requirement: 历史记录 API
系统 SHALL 提供 `GET /api/targets/:id/history` 端点,返回指定目标的最近 N 条检查记录。
#### Scenario: 获取最近历史记录
- **WHEN** 客户端请求 `GET /api/targets/1/history?limit=20`
- **THEN** 系统 SHALL 返回最多 20 条检查记录,按时间倒序排列,且每条包含 success、matched、durationMs、statusDetail 和 failure
#### Scenario: 使用默认 limit
- **WHEN** 客户端请求 `GET /api/targets/1/history`(未指定 limit
- **THEN** 系统 SHALL 默认返回最近 20 条记录
### Requirement: 趋势聚合 API
系统 SHALL 提供 `GET /api/targets/:id/trend` 端点,返回指定目标按小时聚合的趋势数据。
#### Scenario: 获取 24 小时趋势
- **WHEN** 客户端请求 `GET /api/targets/1/trend?hours=24`
- **THEN** 系统 SHALL 返回按小时分组的聚合数据,每个数据点包含 hour、avgDurationMs、availability、totalChecks
#### Scenario: 使用默认时间范围
- **WHEN** 客户端请求 `GET /api/targets/1/trend`(未指定 hours
- **THEN** 系统 SHALL 默认返回最近 24 小时的趋势数据
## ADDED Requirements
### 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** 检查结果 success=true 且 matched=true
- **THEN** API SHALL 返回 failure 为 null

View File

@@ -0,0 +1,102 @@
## MODIFIED Requirements
### Requirement: YAML 配置文件格式
系统 SHALL 支持通过 YAML 配置文件定义全部运行参数,包括 server 配置、runtime 配置、checker 默认值和 typed target 列表。target MUST 使用 `type` 字段声明 checker 类型HTTP 领域字段 MUST 放在 `http` 分组command 领域字段 MUST 放在 `command` 分组。
#### Scenario: 完整配置文件解析
- **WHEN** 系统启动并读取包含 server、runtime、defaults、targets 的 YAML 配置文件
- **THEN** 系统 SHALL 正确解析所有字段并用于初始化服务、调度引擎和对应 checker runner
#### Scenario: 最简 HTTP 配置文件解析
- **WHEN** 系统读取只包含一个 `type: http` target 和 `http.url` 的 YAML 配置文件(省略 server、runtime、defaults 和 expect
- **THEN** 系统 SHALL 使用内置默认值填充未指定的字段host=127.0.0.1, port=3000, dir=./data, interval=30s, timeout=10s, runtime.maxConcurrentChecks=20, http.method=GET, http.maxBodyBytes=100MB
#### Scenario: 最简 command 配置文件解析
- **WHEN** 系统读取只包含一个 `type: command` target 和 `command.exec` 的 YAML 配置文件
- **THEN** 系统 SHALL 使用内置默认值填充未指定的字段interval=30s, timeout=10s, command.cwd 为配置文件所在目录, command.maxOutputBytes=100MB
#### Scenario: per-target 配置覆盖全局默认值
- **WHEN** 某个 target 指定 interval、timeout 或对应领域分组中的默认字段
- **THEN** 该 target SHALL 使用其自身的值,不受 defaults 中对应字段影响
### Requirement: 配置校验
系统 SHALL 在启动时对 YAML 配置进行完整校验,校验失败时以非零状态退出并输出清晰的错误信息。
#### Scenario: target 缺少必填字段
- **WHEN** YAML 中某个 target 缺少 name 或 type 字段
- **THEN** 系统 SHALL 以错误退出,提示哪个 target 缺少哪个字段
#### Scenario: HTTP target 缺少 url
- **WHEN** YAML 中某个 target 配置 `type: http` 但缺少 `http.url`
- **THEN** 系统 SHALL 以错误退出,提示该 target 缺少 http.url 字段
#### Scenario: command target 缺少 exec
- **WHEN** YAML 中某个 target 配置 `type: command` 但缺少 `command.exec`
- **THEN** 系统 SHALL 以错误退出,提示该 target 缺少 command.exec 字段
#### Scenario: target type 非法
- **WHEN** YAML 中某个 target 的 type 不是 `http``command`
- **THEN** 系统 SHALL 以错误退出,提示不支持的 target type
#### Scenario: target name 重复
- **WHEN** YAML 中存在两个 name 相同的 target
- **THEN** 系统 SHALL 以错误退出,提示重复的 name
#### Scenario: interval 格式非法
- **WHEN** interval 或 timeout 值不是有效的时长格式(如 `30s``5m``500ms`
- **THEN** 系统 SHALL 以错误退出并提示格式错误
#### Scenario: maxConcurrentChecks 非法
- **WHEN** runtime.maxConcurrentChecks 不是正整数
- **THEN** 系统 SHALL 以错误退出并提示 runtime.maxConcurrentChecks 格式错误
#### Scenario: size 格式非法
- **WHEN** maxBodyBytes 或 maxOutputBytes 值不是有效的 size 格式
- **THEN** 系统 SHALL 以错误退出并提示支持 B、KB、MB、GB 格式
### Requirement: expect 配置增强
系统 SHALL 支持 typed target 的领域专用 expect 配置,包括 HTTP 的 `status``headers``body` 和 command 的 `exitCode``stdout``stderr`。内容类 expect MUST 使用数组表达配置顺序。
#### Scenario: 解析 HTTP expect 配置
- **WHEN** YAML 配置文件中 HTTP target 的 expect 包含 status、headers、body 规则数组及内部方法
- **THEN** 系统 SHALL 正确解析并存储为 HTTP target 的 expect 字段
#### Scenario: 解析 command expect 配置
- **WHEN** YAML 配置文件中 command target 的 expect 包含 exitCode、stdout 和 stderr 规则数组
- **THEN** 系统 SHALL 正确解析并存储为 command target 的 expect 字段
#### Scenario: 解析 body 有序规则数组
- **WHEN** YAML 中 HTTP target 配置 `expect.body` 为 contains、json、regex 三个数组项
- **THEN** 系统 SHALL 保留数组顺序,供执行阶段按配置顺序快速失败
#### Scenario: 不配置 HTTP status
- **WHEN** HTTP target 未配置 `expect.status`
- **THEN** 系统 SHALL 在执行 expect 时使用默认 `status: [200]` 语义
#### Scenario: 不配置 command exitCode
- **WHEN** command target 未配置 `expect.exitCode`
- **THEN** 系统 SHALL 在执行 expect 时使用默认 `exitCode: [0]` 语义
## ADDED Requirements
### Requirement: size 配置解析
系统 SHALL 支持使用单位字符串配置读取上限,单位包括 `B``KB``MB``GB`
#### Scenario: 解析 MB
- **WHEN** YAML 中配置 `maxBodyBytes: "100MB"`
- **THEN** 系统 SHALL 将其解析为 104857600 bytes
#### Scenario: 解析 KB
- **WHEN** YAML 中配置 `maxOutputBytes: "512KB"`
- **THEN** 系统 SHALL 将其解析为 524288 bytes
### Requirement: runtime 并发配置
系统 SHALL 支持 `runtime.maxConcurrentChecks` 配置全局最大并发检查数。
#### Scenario: 使用默认并发限制
- **WHEN** YAML 中未配置 runtime.maxConcurrentChecks
- **THEN** 系统 SHALL 使用默认值 20
#### Scenario: 配置并发限制
- **WHEN** YAML 中配置 `runtime.maxConcurrentChecks: 5`
- **THEN** 系统 SHALL 将全局最大并发检查数设置为 5

View File

@@ -0,0 +1,71 @@
## MODIFIED Requirements
### Requirement: 总览统计卡片
Dashboard SHALL 在页面顶部展示总览统计卡片,包含总目标数、正常数、异常数和平均耗时。
#### Scenario: 展示统计卡片
- **WHEN** 用户打开 Dashboard 页面
- **THEN** 页面顶部 SHALL 显示 4 个统计卡片:全部目标数、正常目标数、异常目标数、所有目标平均耗时
#### Scenario: 统计数据自动刷新
- **WHEN** 页面处于打开状态
- **THEN** 统计卡片 SHALL 每 5-10 秒自动刷新数据
### Requirement: 目标列表表格
Dashboard SHALL 展示所有 checker target 的列表表格,包含名称、类型、目标摘要、当前状态、最新耗时、最近失败原因和迷你趋势线。
#### Scenario: 展示目标列表
- **WHEN** 用户打开 Dashboard 页面
- **THEN** 页面 SHALL 显示表格每行包含目标名称、类型、目标摘要、状态指示圆点UP / DOWN、最新耗时值、最近失败原因摘要、迷你 Sparkline 趋势线
#### Scenario: 状态指示圆点
- **WHEN** 目标最近一次检查 success=true 且 matched=true
- **THEN** 状态圆点 SHALL 显示为绿色UP
- **WHEN** 目标最近一次检查 success=false 或 matched=false
- **THEN** 状态圆点 SHALL 显示为红色DOWN
### Requirement: 可展开的目标详情面板
Dashboard SHALL 支持在目标列表中展开某行,显示该目标的详细状态、统计摘要、趋势图和最近历史记录。
#### Scenario: 展开目标详情
- **WHEN** 用户点击目标列表中的某一行
- **THEN** 该行下方 SHALL 展开详情面板包含可用率百分比、平均耗时、P99 耗时、24 小时耗时趋势折线图、最近 5-10 条检查记录列表、领域状态详情和失败信息
#### Scenario: 收起目标详情
- **WHEN** 用户再次点击已展开的目标行
- **THEN** 详情面板 SHALL 收起
#### Scenario: 趋势图按需加载
- **WHEN** 用户展开某个目标的详情面板
- **THEN** 系统 SHALL 此时请求该目标的趋势数据,而非页面加载时预加载所有目标的趋势数据
### Requirement: 历史记录展示
Dashboard SHALL 在目标详情面板中展示最近的检查记录,包含时间、领域状态详情、耗时、成功/失败标记和失败信息。
#### Scenario: 展示历史记录
- **WHEN** 用户展开目标详情面板
- **THEN** 面板 SHALL 显示最近检查记录列表每条包含时间戳、statusDetail如 HTTP 200 或 exitCode=1、耗时毫秒数、UP/DOWN 标记和 failure.message如存在
### Requirement: 趋势图可视化
Dashboard SHALL 使用 recharts 库渲染趋势图,包括目标列表中的迷你 Sparkline 和详情面板中的完整折线图。
#### Scenario: 表格行内迷你趋势线
- **WHEN** 目标列表表格渲染
- **THEN** 每行 SHALL 包含一个基于 recharts 的迷你折线图,展示最近的耗时趋势
#### Scenario: 详情面板完整趋势图
- **WHEN** 用户展开目标详情面板
- **THEN** 面板 SHALL 展示基于 recharts 的完整折线图X 轴为时间小时Y 轴为平均耗时,并标注可用率
## ADDED Requirements
### Requirement: checker 类型展示
Dashboard SHALL 在列表和详情中明确展示 target 的 checker 类型。
#### Scenario: 展示 HTTP 类型
- **WHEN** 目标 type 为 `http`
- **THEN** Dashboard SHALL 在类型列显示 HTTP并将目标摘要显示为 URL
#### Scenario: 展示 command 类型
- **WHEN** 目标 type 为 `command`
- **THEN** Dashboard SHALL 在类型列显示 Command并将目标摘要显示为命令摘要

View File

@@ -0,0 +1,74 @@
## MODIFIED Requirements
### Requirement: SQLite 数据库初始化
系统 SHALL 使用 Bun 内置 `bun:sqlite` 模块在配置的数据目录下创建 SQLite 数据库文件,并以 WAL 模式运行。数据库 schema MUST 支持 typed checker target 和结构化检查结果。
#### Scenario: 首次启动创建数据库
- **WHEN** 指定的数据目录下不存在数据库文件
- **THEN** 系统 SHALL 创建数据库文件并初始化包含 type、target、config、duration_ms、status_detail、failure 等字段的 targets 和 check_results 表
#### Scenario: 数据目录不存在
- **WHEN** 配置的数据目录路径不存在
- **THEN** 系统 SHALL 自动创建该目录
#### Scenario: 数据库已存在时启动
- **WHEN** 数据库文件已存在
- **THEN** 系统 SHALL 直接打开数据库,不重新建表
### Requirement: targets 表同步
系统 SHALL 在启动时将 YAML 配置中的目标列表同步到 SQLite targets 表,并持久化 target 类型、展示摘要、领域配置、调度配置和 expect 配置。
#### Scenario: 首次同步目标
- **WHEN** 数据库为空且 YAML 中定义了 N 个 typed target
- **THEN** 系统 SHALL 将所有目标插入 targets 表,包含 name、type、target、config、interval_ms、timeout_ms 和 expect
#### Scenario: 配置变更后重新同步
- **WHEN** YAML 配置发生变更(新增、删除或修改目标)后重启
- **THEN** 系统 SHALL 根据 name 字段匹配:新增的插入、删除的移除、修改的更新
### Requirement: check_results 表追加写入
系统 SHALL 将每次检查结果追加写入 check_results 表,不更新或删除已有记录。
#### Scenario: 写入检查结果
- **WHEN** 一次 checker 执行完成
- **THEN** 系统 SHALL 插入一条包含 target_id、timestamp、success、matched、duration_ms、status_detail、failure 的记录
#### Scenario: 写入结构化失败信息
- **WHEN** checker 执行失败或 expect 不匹配
- **THEN** 系统 SHALL 将首个失败原因序列化写入 failure 字段
### Requirement: 聚合查询支持
数据存储 SHALL 支持按时间段聚合查询用于计算可用率、平均耗时、P99 耗时等统计指标。
#### Scenario: 计算目标可用率
- **WHEN** 查询某目标在指定时间范围内的可用率
- **THEN** 系统 SHALL 返回 UP (success=true AND matched=true) 的记录数占总记录数的百分比
#### Scenario: 计算目标平均耗时
- **WHEN** 查询某目标在指定时间范围内的平均耗时
- **THEN** 系统 SHALL 返回 duration_ms 的平均值(仅计算 success=true 的记录)
#### Scenario: 按小时聚合趋势数据
- **WHEN** 查询某目标在指定时间范围内的趋势数据
- **THEN** 系统 SHALL 返回按小时分组的聚合数据,包括每小时的平均耗时和可用率
## ADDED Requirements
### Requirement: 目标展示摘要持久化
数据存储 SHALL 为每个 target 持久化一个领域无关的展示摘要字段 `target`
#### Scenario: HTTP target 展示摘要
- **WHEN** 同步 HTTP target
- **THEN** targets.target SHALL 存储该 target 的 URL
#### Scenario: command target 展示摘要
- **WHEN** 同步 command target
+- **THEN** targets.target SHALL 存储由 exec 和 args 组成的命令摘要
#### Scenario: HTTP target config 序列化
- **WHEN** 同步 HTTP target
- **THEN** targets.config SHALL 存储 JSON包含 url、method、headers、body、maxBodyBytes
#### Scenario: command target config 序列化
- **WHEN** 同步 command target
- **THEN** targets.config SHALL 存储 JSON包含 exec、args、cwd、env、maxOutputBytes

View File

@@ -0,0 +1,128 @@
## MODIFIED Requirements
### Requirement: 组内并发拨测
系统 SHALL 在每次调度 tick 时并发执行同组内目标的检查,但实际同时运行的检查数 MUST 受全局 `runtime.maxConcurrentChecks` 限制。
#### Scenario: 同组目标并发执行
- **WHEN** 调度器触发一次 tick该组有 3 个目标,且全局并发余量至少为 3
- **THEN** 系统 SHALL 同时执行 3 个 checker而非顺序执行
#### Scenario: 单个目标失败不影响同组其他目标
- **WHEN** 同组中某个目标的检查请求超时或失败
- **THEN** 其他目标的检查 SHALL 正常完成并记录结果
#### Scenario: 全局并发限制生效
- **WHEN** 调度器同时触发 10 个目标且 runtime.maxConcurrentChecks 为 3
- **THEN** 系统 MUST 同时最多运行 3 个检查,其余检查等待并发槽位释放
### Requirement: HTTP 拨测执行
系统 SHALL 对 `type: http` 的目标执行 HTTP 请求,支持 GET、POST、PUT、DELETE、PATCH、HEAD 方法,并携带 `http.headers``http.body`
#### Scenario: 执行 GET 请求
- **WHEN** HTTP target 配置 http.method 为 GET
- **THEN** 系统 SHALL 发送 GET 请求到 http.url
#### Scenario: 执行 POST 请求带 body
- **WHEN** HTTP target 配置 http.method 为 POST 且指定了 http.body 和 Content-Type header
- **THEN** 系统 SHALL 发送带指定 body 的 POST 请求
#### Scenario: 携带自定义 headers
- **WHEN** HTTP target 配置了 http.headers如 Authorization
- **THEN** 系统 SHALL 在请求中包含所有配置的 headers
#### Scenario: HTTP body 读取上限
- **WHEN** HTTP response body 超过该 target 的 maxBodyBytes
- **THEN** 系统 MUST 停止读取并记录 `success=false``matched=false` 和结构化输出超限错误
### Requirement: 请求超时控制
系统 SHALL 对每次 checker 执行实施超时控制,超时时间使用目标配置的 timeout 值。
#### Scenario: HTTP 请求超时
- **WHEN** HTTP 请求在 timeout 时间内未收到响应
- **THEN** 系统 SHALL 中止该请求,记录为失败并标注超时错误
#### Scenario: command 执行超时
- **WHEN** command 进程在 timeout 时间内未退出
- **THEN** 系统 MUST 终止该子进程,记录为失败并标注超时错误
#### Scenario: 请求在超时前完成
- **WHEN** checker 在超时前完成执行
- **THEN** 系统 SHALL 正常记录执行结果并进入 expect 校验
### Requirement: expect 校验
系统 SHALL 在 checker 执行完成后根据目标类型的 expect 配置校验观测结果,校验结果和首个失败原因记入 check result。
#### Scenario: HTTP 默认状态码
- **WHEN** HTTP target 未配置 `expect.status`
- **THEN** 系统 SHALL 按默认 `status: [200]` 校验响应状态码
#### Scenario: 校验 HTTP 状态码
- **WHEN** HTTP target 配置了 `expect.status: [200, 201]`
- **THEN** 系统 SHALL 检查响应状态码是否在列表中,将匹配结果记录到 matched 字段
#### Scenario: 校验 HTTP 响应头
- **WHEN** HTTP target 配置了 `expect.headers: {"Content-Type": {contains: "application/json"}}`
- **THEN** 系统 SHALL 检查响应头是否符合指定规则,全部匹配时继续后续阶段
#### Scenario: 校验 HTTP 响应体
- **WHEN** HTTP target 配置了有序 `expect.body` 规则数组
- **THEN** 系统 SHALL 按数组顺序执行 body 规则,任一失败立即记录 failure 并停止后续规则
#### Scenario: command 默认 exitCode
- **WHEN** command target 未配置 `expect.exitCode`
- **THEN** 系统 SHALL 按默认 `exitCode: [0]` 校验命令退出码
#### Scenario: 校验 command stdout
- **WHEN** command target 配置了有序 `expect.stdout` 规则数组
- **THEN** 系统 SHALL 按数组顺序执行 stdout 规则,任一失败立即记录 failure 并停止后续规则
#### Scenario: 校验耗时阈值
- **WHEN** 目标配置了 `expect.maxDurationMs`
- **THEN** 系统 SHALL 检查实际 durationMs 是否超过阈值,将匹配结果记录到 matched 字段
#### Scenario: 多条 expect 规则
- **WHEN** 目标同时配置状态、duration、元数据和内容规则
- **THEN** 系统 SHALL 所有规则全部通过时 matched 为 true任一不通过则为 false 并记录首个失败原因
### Requirement: Body 校验按需解析
系统 SHALL 仅在 HTTP target 配置了 body 校验且 status、duration、headers 阶段均通过时才读取并解析响应体,避免不必要的读取和解析开销。
#### Scenario: status 失败时不读取 body
- **WHEN** HTTP target 的 status 阶段不匹配
- **THEN** 系统 SHALL 立即返回 matched=false且 MUST NOT 读取 response body
#### Scenario: 仅配置 contains 时不解析 JSON
- **WHEN** HTTP target 仅配置 body contains 规则而未配置 json/css/xpath 规则
- **THEN** 系统 SHALL 不执行 JSON.parse 或 HTML/XML 解析
#### Scenario: 配置 json 时解析 JSON 失败
- **WHEN** HTTP target 配置了 body json 规则但响应体不是合法 JSON
- **THEN** 系统 SHALL 判定 matched 为 false并记录 json 规则对应的 failure.path
### Requirement: 拨测结果记录
系统 SHALL 在每次 checker 完成后,将结果写入 SQLite 数据存储,包含 target_id、timestamp、success、matched、duration_ms、status_detail、failure 字段。
#### Scenario: 成功检查结果记录
- **WHEN** checker 成功执行且 expect 全部匹配
- **THEN** 系统 SHALL 记录 success=true、matched=true、duration_ms、status_detailfailure 为 null
#### Scenario: 执行失败结果记录
- **WHEN** checker 执行失败(网络错误、超时、命令启动失败、输出超限等)
- **THEN** 系统 SHALL 记录 success=false、matched=false、failure.kind="error" 和具体错误信息
#### Scenario: expect 不匹配结果记录
- **WHEN** checker 执行成功但 expect 不匹配
- **THEN** 系统 SHALL 记录 success=true、matched=false、failure.kind="mismatch" 和具体不匹配信息
## ADDED Requirements
### Requirement: runner 选择
系统 SHALL 根据 target.type 选择对应 runner 执行检查。
#### Scenario: 选择 HTTP runner
- **WHEN** target.type 为 `http`
- **THEN** 系统 SHALL 使用 HTTP runner 执行该目标
#### Scenario: 选择 command runner
- **WHEN** target.type 为 `command`
- **THEN** 系统 SHALL 使用 command runner 执行该目标

View File

@@ -0,0 +1,50 @@
## 1. 类型与配置契约
- [x] 1.1 重构 checker 类型定义为 `http``command` 判别联合,并新增 `CheckFailure``durationMs``statusDetail` 等结果字段,将 `maxLatencyMs` 重命名为 `maxDurationMs`
- [x] 1.2 更新 YAML 配置类型,新增 `runtime.maxConcurrentChecks``defaults.http``defaults.command` 和 typed target 配置
- [x] 1.3 实现 size 解析工具,支持 `B``KB``MB``GB` 并覆盖 `100MB=104857600` 的测试
- [x] 1.4 重构 config-loader 校验逻辑,移除顶层 HTTP 字段支持并校验 type、http.url、command.exec、并发和 size 格式ResolvedConfig 需携带配置文件目录,用于 command cwd 相对路径解析
- [x] 1.5 更新配置解析测试,覆盖最简 HTTP、最简 command、per-target 覆盖、默认值、非法 type、缺失字段和非法 size
## 2. Expect 与失败信息
- [x] 2.1 抽取通用值操作符,使 equals、contains、match、empty、exists、gte、lte、gt、lt 可复用于 header、body、stdout 和 stderr
- [x] 2.2 将 HTTP `expect.body` 重构为有序规则数组,并支持 contains、regex、json、css、xpath 规则
- [x] 2.3 实现 HTTP expect pipeline按 status、duration、headers、body[] 顺序执行并应用默认 `status: [200]`
- [x] 2.4 实现 command expect pipeline按 exitCode、duration、stdout[]、stderr[] 顺序执行并应用默认 `exitCode: [0]`
- [x] 2.5 实现结构化 failure 生成与 actual 摘要截断,区分 `error``mismatch`
- [x] 2.6 将 expect 相关文件body、http、command、failure移入 `checker/expect/` 子目录,统一导入路径并更新测试文件引用
- [x] 2.7 更新 expect 单元测试,覆盖规则顺序、快速失败、默认 status、默认 exitCode、失败 path 和 actual 截断
## 3. Runner 与调度引擎
- [x] 3.1 将现有 fetcher 拆分或重命名为 HTTP runner并改为读取 status、duration、headers 后再按需读取 body
- [x] 3.2 在 HTTP runner 中实现 maxBodyBytes 限制、超时处理、statusDetail 和结构化执行错误
- [x] 3.3 新增 command runner使用 `exec + args` 执行本地命令且不经过 shell
- [x] 3.4 在 command runner 中实现 cwd 相对配置文件目录解析、env 覆盖、timeout kill 和 maxOutputBytes 合计限制
- [x] 3.5 重构 ProbeEngine 按 target.type 选择 runner并引入全局 maxConcurrentChecks 并发池
- [x] 3.6 更新 runner 和 engine 测试,覆盖 HTTP 快速失败不读 body、command 非零退出、启动失败、超时、输出超限和并发限制
## 4. 存储与 API
- [x] 4.1 重建 SQLite schema使用 targets 的 type、target、config 字段和 check_results 的 duration_ms、status_detail、failure 字段
- [x] 4.2 更新目标同步逻辑,持久化 HTTP URL 摘要和 command 命令摘要
- [x] 4.3 更新检查结果写入和聚合查询,使用 duration_ms 计算平均耗时、P99 耗时和趋势数据
- [x] 4.4 更新 shared API 类型,将 avgLatencyMs、p99LatencyMs、latencyMs、statusCode 替换为 avgDurationMs、p99DurationMs、durationMs、statusDetail 和 failure
- [x] 4.5 更新 API handler 映射逻辑,返回 type、target、durationMs、statusDetail、failure 和新的统计字段
- [x] 4.6 更新 store 和 API 测试,覆盖结构化 failure 入库、目标摘要、summary、targets、history 和 trend 响应
## 5. Dashboard 与文档
- [x] 5.1 更新 Dashboard 总览卡片、目标表格和详情面板,将 URL/方法/延迟改为类型、目标、耗时和失败原因展示
- [x] 5.2 更新趋势图和 Sparkline 数据字段,从 latency 切换为 duration
- [x] 5.3 更新前端类型引用和组件测试或相关断言,覆盖 HTTP 与 command target 展示
- [x] 5.4 更新 README 的项目说明、配置说明、目标状态判定、API 字段和已知限制
- [x] 5.5 更新 `probes.example.yaml`,提供 HTTP 与 command typed target 示例以及 100MB 默认说明
- [x] 5.6 更新 smoke test 配置和断言,确保生产 executable 可使用新配置启动并服务 API 与 Dashboard
## 6. 质量验证
- [x] 6.1 运行 `bun run check`修复类型检查、lint、格式检查和单元测试问题
- [x] 6.2 运行 `bun run verify`,修复生产构建和 smoke test 问题
- [x] 6.3 复查 OpenSpec change 与实现一致性,确认所有任务完成且 README、测试和示例同步更新

View File

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

View File

@@ -0,0 +1,112 @@
## Context
当前 expect 校验通过 `checkExpect()` 函数(`src/server/checker/fetcher.ts`)实现,仅支持 status 白名单、bodyContains 子串匹配、maxLatencyMs 延迟阈值。body 校验能力薄弱,无法处理 JSON 结构化数据和 HTML/XML 页面内容校验。
本次设计将 body 校验扩展为五种可组合方法,并引入操作符系统统一提取值的比较逻辑。同时新增响应头校验。
项目约束Bun 1.3.13 运行时、TypeScript、SQLite 持久化、YAML 配置格式。
## Goals / Non-Goals
**Goals:**
- body 校验支持五大方法contains、regex、json、css、xpath任意 AND 组合
- 操作符系统equals默认、contains、match、empty、exists、gte、lte、gt、lt
- 响应头校验 headers
- 保持 matched/success 两层判定模型不变
- 所有新逻辑有完整单元测试
**Non-Goals:**
- 不支持 json/csv/xpath 的 OR 组合(当前全 AND
- 不支持 JSONPath 的通配符/过滤器(`.items[*].name``.items[?(@.price>10)]`
- 不支持 CSS 伪类选择器(如 `:nth-child`
- 不改变前端 Dashboard UI
- 不做告警通知
## Decisions
### D1: body 分组嵌套结构
选择 `expect.body.<method>` 而非平铺 `expect.bodyXxx`
**理由**:五种 body 方法语义上同属一层嵌套结构比平铺更清晰YAML 可读性更好。代价是将 `bodyContains``ExpectConfig` 顶层迁移至 `body.contains`,属于 **BREAKING** 变更,但项目尚在早期,影响极小。
**替代方案**:平铺 `expect.bodyContains``expect.bodyRegex` 等。不选,因随着方法增多字段名会越来越长且缺乏层次。
### D2: 操作符采用"标量=equals对象=显式"的二态模型
```yaml
json:
$.status: ok # 标量 → equals
$.data.count: # 对象 → 显式操作符
gte: 1
```
**理由**90% 的拨测场景只需要等值比较,标量语法最简洁。需要复杂比较时展开为对象,二态在同一个 map 中共存,无需额外字段指示意图。
**替代方案**:每个规则必须是 `{ path, operator, value }` 对象。过于冗长,不如二态模型灵活。
### D3: CSS 选择器通过 `attr` 切换提取维度
```yaml
css:
"div.status": OK # 默认 textContent
"meta[name=build-hash]": # 提取属性
attr: content
empty: false
```
**理由**99% 的 CSS 选择器场景只需要 textContent。通过可选的 `attr` 字段覆盖属性提取场景,保持常见用法最简。
**替代方案**:在选择器字符串中编码(如 `meta[name=build]@content`)。不选,语法污染。
### D4: 依赖选型 cheerio + xpath + @xmldom/xmldom
| 包 | 用途 | 选型理由 |
|----|------|---------|
| cheerio | CSS 选择器 HTML 解析 | npm 27M+ 周下载jQuery API 熟悉度高,依赖树由同一组织维护 |
| xpath | XPath 1.0 引擎 | npm 600K+ 周下载,轻量,业界标准 |
| @xmldom/xmldom | xpath 的 DOM 实现 | 2M+ 周下载xmldom 官方维护 |
**替代方案**jsdom体积大~200KB、linkedom不支持 XPath。不选。
cheerio 和 xpath 使用不同的 DOM 模型,同一个 HTML body 需要各自解析。拨测场景(秒级频率,非高并发 HTML 解析)性能开销可忽略。
### D5: body 方法按需解析,短路 AND 执行
整体 checkExpect 执行顺序为 `status → headers → body → maxLatencyMs`,均为 AND 短路。body 内部执行顺序:
```
body 内部:
1. contains → 文本匹配,失败立即返回
2. regex → 文本匹配,失败立即返回
3. json → 仅当 json 配置存在时解析 JSON否则跳过
4. css → 仅当 css 配置存在时解析 HTMLcheerio
5. xpath → 仅当 xpath 配置存在时解析 HTML/XMLxmldom
解析失败JSON.parse 异常、cheerio 加载失败)→ matched=false
```
**理由**:避免不必要的解析开销(例如只配了 contains 时不解析 JSON/HTML。AND 短路语义与现有 expect 规则保持一致。
### D6: 操作符的类型转换策略
| 操作符 | 提取值类型 | 转换逻辑 |
|--------|-----------|---------|
| equals | 保留原类型 | strict === 比较 |
| contains | 强制 toString() | actual.toString().includes(expected) |
| match | 强制 toString() | new RegExp(pattern).test(actual.toString()) |
| empty | - | null、undefined、""、[]、{} → 判定为空 |
| exists | - | undefined vs 非 undefined |
| gte/lte/gt/lt | 强制 Number() | Number(actual) >= expected |
CSS/XPath 提取的值始终是 string数字比较时自动 Number() 转换。JSON 提取的值保留原类型number/boolean/null/string
单个提取值可配置多个操作符(如 `{gte: 10, lte: 100}`),所有操作符全部通过才算该字段通过,语义为 AND。
## Risks / Trade-offs
- **[兼容性风险]** `bodyContains``body.contains` 是 BREAKING 变更 → 通过 README 和示例配置文件说明,现有用户量极小,影响可控
- **[性能风险]** cheerio 和 xpath 各自解析 HTML → 同一 body 可能解析两次 → 拨测场景下无需缓存,单次解析耗时 <5ms整体影响可忽略
- **[JSONPath 功能局限]** 自实现简易路径解析不支持通配符和过滤器 → 通过文档说明限制,后续可按需增强
- **[XPath 浏览器兼容]** xpath 使用 xmldom 而非浏览器原生 evaluate → 语义上等价,测试覆盖保证行为正确
- **[依赖体积]** 新增 3 个包增加约 95KB → 这是 executable 构建Bun compile 会打包进二进制,对最终产物影响有限

View File

@@ -0,0 +1,39 @@
## Why
当前 expect 规则仅有 `status``bodyContains``maxLatencyMs` 三条,无法满足 API 网关拨测中对 JSON 返回值字段校验、HTML 页面内容校验、响应头校验等常见需求。body 校验能力单薄(仅子串匹配),需要增强为多种可组合的校验方法,覆盖主流响应格式。
## What Changes
- 新增 `headers` 规则,支持按响应头键值对校验
- 重构 body 校验:将独立的 `bodyContains` 移至 `body` 分组下,新增五种 body 校验方法:
- `contains`:子串匹配(从原 `bodyContains` 迁移)
- `regex`:正则表达式全文匹配
- `json`JSONPath 提取值后比较
- `css`CSS 选择器提取 HTML 元素文本/属性后比较
- `xpath`XPath 提取 XML/HTML 节点后比较
- body 五种方法可任意组合AND 串联
- 新增操作符系统:`equals`(默认)、`contains``match`(正则)、`empty``exists``gte``lte``gt``lt`
- 新增依赖:`cheerio`CSS 选择器)、`xpath` + `@xmldom/xmldom`XPath 引擎)
- **BREAKING**`expect.bodyContains` 迁移至 `expect.body.contains`
## Capabilities
### New Capabilities
- `expect-body-checkers`body 响应校验方法集contains/regex/json/css/xpath及操作符系统
### Modified Capabilities
- `probe-config`expect 配置 schema 变更,新增 headers/body 分组bodyContains 迁移
- `probe-engine`checkExpect 函数扩展,支持新的 body 校验方法和操作符
## Impact
- 类型定义:`src/server/checker/types.ts`ExpectConfig/BodyExpectConfig/ExpectOperator
- 配置加载:`src/server/checker/config-loader.ts`(解析新的 expect 结构)
- 拨测执行:`src/server/checker/fetcher.ts`checkExpect 扩展)
- 数据存储:`src/server/checker/store.ts`expect JSON 序列化兼容)
- 前端展示状态判定逻辑不变matched 字段语义不变)
- 配置文件:`probes.example.yaml`(更新示例)
- README.md更新配置文档
- 依赖:`package.json` 新增 cheerio、xpath、@xmldom/xmldom

View File

@@ -0,0 +1,113 @@
## ADDED Requirements
### Requirement: 响应体多种校验方法
系统 SHALL 支持对 HTTP 响应体进行五种可组合的校验方法contains子串、regex正则、jsonJSONPath、cssCSS 选择器、xpathXPath配置在 `expect.body` 分组下。
#### Scenario: contains 子串匹配
- **WHEN** 目标配置 `expect.body.contains: "healthy"`,且响应体包含 `"healthy"`
- **THEN** 系统 SHALL 判定 matched 为 true
#### Scenario: contains 不匹配
- **WHEN** 目标配置 `expect.body.contains: "healthy"`,且响应体不包含该文本
- **THEN** 系统 SHALL 判定 matched 为 false
#### Scenario: regex 正则匹配
- **WHEN** 目标配置 `expect.body.regex: '"status"\\s*:\\s*"ok"'`,且响应体匹配该正则
- **THEN** 系统 SHALL 判定 matched 为 true
#### Scenario: regex 不匹配
- **WHEN** 目标配置 `expect.body.regex: '"status"\\s*:\\s*"ok"'`,且响应体不匹配该正则
- **THEN** 系统 SHALL 判定 matched 为 false
#### Scenario: json JSONPath 等值匹配
- **WHEN** 目标配置 `expect.body.json: {"$.status": "ok"}`,且响应 JSON 中 `$.status` 值为 `"ok"`
- **THEN** 系统 SHALL 判定 matched 为 true
#### Scenario: json JSONPath 值不匹配
- **WHEN** 目标配置 `expect.body.json: {"$.status": "ok"}`,且响应 JSON 中 `$.status` 值为 `"error"`
- **THEN** 系统 SHALL 判定 matched 为 false
#### Scenario: json 解析失败
- **WHEN** 目标配置了 `expect.body.json` 但响应体不是合法 JSON
- **THEN** 系统 SHALL 判定 matched 为 false
#### Scenario: css 选择器匹配
- **WHEN** 目标配置 `expect.body.css: {"div#health": "OK"}`,且 HTML 中存在 `div#health` 元素文本为 `"OK"`
- **THEN** 系统 SHALL 判定 matched 为 true
#### Scenario: css 选择器匹配属性值
- **WHEN** 目标配置 css 规则带 `attr: "content"` 用于提取属性,且属性值匹配期望
- **THEN** 系统 SHALL 判定 matched 为 true
#### Scenario: css 选择器无匹配元素
- **WHEN** 目标配置了 css 选择器但 HTML 中无匹配元素
- **THEN** 系统 SHALL 判定 matched 为 false
#### Scenario: xpath 表达式匹配
- **WHEN** 目标配置 `expect.body.xpath: {"/root/status/text()": "ok"}`,且 XML 中 `/root/status` 节点文本为 `"ok"`
- **THEN** 系统 SHALL 判定 matched 为 true
#### Scenario: xpath 表达式无匹配节点
- **WHEN** 目标配置了 xpath 表达式但 XML 中无匹配节点
- **THEN** 系统 SHALL 判定 matched 为 false
### Requirement: 多种 body 校验方法 AND 组合
系统 SHALL 支持同时配置多种 body 校验方法,所有方法均通过时 matched 方为 true。
#### Scenario: 多种方法全部通过
- **WHEN** 目标同时配置 `body.contains``body.json``body.regex`,且全部通过
- **THEN** 系统 SHALL 判定 matched 为 true
#### Scenario: 多种方法任一失败
- **WHEN** 目标同时配置 `body.contains``body.json`,且 `body.contains` 不通过
- **THEN** 系统 SHALL 判定 matched 为 false且不再检查 `body.json`
### Requirement: 操作符系统
系统 SHALL 支持对 body 校验的提取值使用以下操作符进行比较equals默认等值、contains子串包含、match正则匹配、empty空值判断、exists存在性判断、gte/lte/gt/lt数值比较
#### Scenario: 标量值隐式 equals
- **WHEN** jsonPath 配置的期望值为标量(字符串/数字/布尔/null`$.status: ok`
- **THEN** 系统 SHALL 使用 equals 操作符,对提取值做严格相等比较
#### Scenario: 显式 contains 操作符
- **WHEN** 配置 `$.message: {contains: "success"}`,且提取值包含 `"success"`
- **THEN** 系统 SHALL 判定 matched 为 true
#### Scenario: 显式 match 操作符
- **WHEN** 配置 `$.version: {match: '\\d+\\.\\d+\\.\\d+'}`,且提取值匹配该正则
- **THEN** 系统 SHALL 判定 matched 为 true
#### Scenario: empty 操作符判断为空
- **WHEN** 配置 `$.items: {empty: true}`,且提取值为空数组 `[]`
- **THEN** 系统 SHALL 判定 matched 为 true
#### Scenario: empty 操作符判断非空
- **WHEN** 配置 `$.items: {empty: false}`,且提取值为 `[1, 2]`
- **THEN** 系统 SHALL 判定 matched 为 true
#### Scenario: exists 操作符判断存在
- **WHEN** 配置 `$.error: {exists: false}`,且 JSON 中不存在 `error` 字段
- **THEN** 系统 SHALL 判定 matched 为 true
#### Scenario: gte 数值比较
- **WHEN** 配置 `$.count: {gte: 10}`,且提取值为 `15`(数字)
- **THEN** 系统 SHALL 判定 matched 为 true
#### Scenario: gt/lt 数值比较
- **WHEN** 配置 `$.latency: {gt: 0, lt: 1000}`,且提取值为 `500`
- **THEN** 系统 SHALL 对同一字段进行多操作符复合比较,全部通过则 matched 为 true
### Requirement: 响应头校验
系统 SHALL 支持通过 `expect.headers` 配置对响应头进行键值对校验。
#### Scenario: 响应头匹配
- **WHEN** 目标配置 `expect.headers: {"Content-Type": "application/json"}`,且响应包含该 header 且值匹配
- **THEN** 系统 SHALL 判定 matched 为 true
#### Scenario: 响应头不匹配
- **WHEN** 目标配置 `expect.headers: {"Content-Type": "application/json"}`,且响应 header 值为 `"text/html"`
- **THEN** 系统 SHALL 判定 matched 为 false
#### Scenario: 响应头缺失
- **WHEN** 目标配置了某个 header 但响应中不存在该 header
- **THEN** 系统 SHALL 判定 matched 为 false

View File

@@ -0,0 +1,21 @@
## ADDED Requirements
### Requirement: expect 配置增强
系统 SHALL 支持增强的 expect 配置格式,包括 `headers` 响应头校验和 `body` 分组下的多种校验方法contains、regex、json、css、xpath
#### Scenario: 解析增强的 expect 配置
- **WHEN** YAML 配置文件中 target 的 expect 包含 headers、body 分组及内部方法
- **THEN** 系统 SHALL 正确解析并存储为 ResolvedTarget 的 expect 字段
#### Scenario: 解析仅含 body.contains 的最简配置
- **WHEN** YAML 中 target 配置 `expect.body.contains: "healthy"`
- **THEN** 系统 SHALL 正确解析,功能等价于旧版 `expect.bodyContains`
#### Scenario: 不配置 expect
- **WHEN** target 未配置任何 expect 规则
- **THEN** 系统 SHALL 正常处理expect 字段为 undefined
#### Scenario: 旧版 bodyContains 字段不再支持
- **WHEN** YAML 中使用 `expect.bodyContains: "xxx"` 格式
- **THEN** 该字段 SHALL 被忽略(系统仅识别 `expect.body.contains`
- **Migration**: 将配置文件中 `expect.bodyContains: "xxx"` 改为 `expect.body.contains: "xxx"`

View File

@@ -0,0 +1,61 @@
## MODIFIED Requirements
### Requirement: expect 校验
系统 SHALL 在拨测完成后根据目标的 expect 配置校验响应,校验结果记入 check result。
#### Scenario: 校验状态码
- **WHEN** 目标配置了 `expect.status: [200, 201]`
- **THEN** 系统 SHALL 检查响应状态码是否在列表中,将匹配结果记录到 matched 字段
#### Scenario: 校验响应头
- **WHEN** 目标配置了 `expect.headers: {"Content-Type": "application/json"}`
- **THEN** 系统 SHALL 检查响应头是否包含指定键值对,全部匹配时将 matched 设为 true
#### Scenario: 校验响应体包含
- **WHEN** 目标配置了 `expect.body.contains: "healthy"`
- **THEN** 系统 SHALL 检查响应体是否包含该文本,将匹配结果记录到 matched 字段
#### Scenario: 校验响应体正则
- **WHEN** 目标配置了 `expect.body.regex: '"status"\\s*:\\s*"ok"'`
- **THEN** 系统 SHALL 检查响应体是否匹配该正则,将匹配结果记录到 matched 字段
#### Scenario: 校验 JSON 响应
- **WHEN** 目标配置了 `expect.body.json: {"$.status": "ok"}`
- **THEN** 系统 SHALL 解析 JSON 并检查 JSONPath 对应值是否符合期望,将匹配结果记录到 matched 字段
#### Scenario: 校验 HTML 响应CSS 选择器)
- **WHEN** 目标配置了 `expect.body.css: {"div#health": "OK"}`
- **THEN** 系统 SHALL 解析 HTML 并用 CSS 选择器提取元素文本进行比较,将匹配结果记录到 matched 字段
#### Scenario: 校验 HTML/XML 响应XPath
- **WHEN** 目标配置了 `expect.body.xpath: {"/root/status/text()": "ok"}`
- **THEN** 系统 SHALL 解析文档并用 XPath 提取节点文本进行比较,将匹配结果记录到 matched 字段
#### Scenario: 校验延迟阈值
- **WHEN** 目标配置了 `expect.maxLatencyMs: 3000`
- **THEN** 系统 SHALL 检查实际延迟是否超过阈值,将匹配结果记录到 matched 字段
#### Scenario: 无 expect 配置
- **WHEN** 目标未配置任何 expect 规则
- **THEN** 系统 SHALL 将 matched 字段设为 true
#### Scenario: 多条 expect 规则
- **WHEN** 目标同时配置了 status、headers、body.contains、body.json 和 maxLatencyMs
- **THEN** 系统 SHALL 所有规则全部通过时 matched 为 true任一不通过则为 false
#### Scenario: 多种 body 方法 AND 组合
- **WHEN** 目标在 body 分组下配置了 contains、json、css 多种方法
- **THEN** 系统 SHALL 按 contains → regex → json → css → xpath 顺序执行,任一失败立即返回 false
## ADDED Requirements
### Requirement: Body 校验按需解析
系统 SHALL 仅在配置了对应 body 校验方法时才解析响应体为对应格式,避免不必要的解析开销。
#### Scenario: 仅配置 contains 时不解析 JSON
- **WHEN** 目标仅配置 `expect.body.contains` 而未配置 json/css/xpath
- **THEN** 系统 SHALL 不执行 JSON.parse 或 HTML/XML 解析
#### Scenario: 配置 json 时解析 JSON 失败
- **WHEN** 目标配置了 `expect.body.json` 但响应体不是合法 JSON
- **THEN** 系统 SHALL 判定 matched 为 false

View File

@@ -0,0 +1,53 @@
## 1. 依赖安装
- [x] 1.1 安装 cheerio、xpath、@xmldom/xmldom 依赖
## 2. 类型定义
- [x] 2.1 在 types.ts 中定义 ExpectOperator、BodyExpectConfig 接口
- [x] 2.2 更新 ExpectConfig 接口,新增 headers 字段,将 bodyContains 替换为 body 分组
- [x] 2.3 新增 CssExpect 类型ExpectValue | ExpectOperator & { attr?: string }
- [x] 2.4 导出 ExpectValue 联合类型
## 3. Body 校验核心实现
- [x] 3.1 实现简易 JSONPath 求值函数 evaluateJsonPath支持 $.a.b、$.a[0].b 等基本路径)
- [x] 3.2 实现操作符比较函数 applyOperatorequals/contains/match/empty/exists/gte/lte/gt/lt
- [x] 3.3 实现 checkExpectValue 函数:标量 → equals对象 → 遍历操作符
- [x] 3.4 实现 checkBodyContainsbody.includes 包装
- [x] 3.5 实现 checkBodyRegexnew RegExp().test 包装
- [x] 3.6 实现 checkBodyJsonJSON.parse + evaluateJsonPath + applyOperator
- [x] 3.7 实现 checkBodyCsscheerio.load + 选择器查询 + text/attr 提取 + applyOperator
- [x] 3.8 实现 checkBodyXpathxmldom 解析 + xpath 引擎 evaluate + applyOperator
## 4. Expect 校验重构
- [x] 4.1 重构 checkExpect 函数,新增 headers 检查逻辑
- [x] 4.2 将 bodyContains 检查替换为 checkBodyExpect 调用,按需分发到五种子方法
- [x] 4.3 实现 checkBodyExpect 主入口:按 contains → regex → json → css → xpath 顺序 AND 短路执行
## 5. 配置加载
- [x] 5.1 确认 config-loader 中 expect 透传逻辑对新结构的兼容性,更新类型引用
## 6. 数据存储兼容
- [x] 6.1 验证 store.ts 中 expect JSON 序列化对新结构的兼容性,必要时调整
## 7. 测试
- [x] 7.1 为 evaluateJsonPath 编写单元测试(嵌套对象、数组索引、不存在路径、边界情况)
- [x] 7.2 为 applyOperator 编写单元测试9 种操作符各至少 2 个 case
- [x] 7.3 为 checkBodyContains/checkBodyRegex 编写单元测试
- [x] 7.4 为 checkBodyJson 编写单元测试等值匹配、操作符匹配、JSON 解析失败、路径不存在)
- [x] 7.5 为 checkBodyCss 编写单元测试text 提取、attr 提取、无匹配元素)
- [x] 7.6 为 checkBodyXpath 编写单元测试节点文本、属性值、无匹配节点、XML 解析失败)
- [x] 7.7 为 checkExpect 新增测试用例headers 校验、body 多种方法 AND 组合、全量规则)
- [x] 7.8 更新 config-loader 测试用例(新 expect 格式解析、向后兼容验证)
- [x] 7.9 端到端模拟测试:构造完整 expect 配置并验证 checkExpect 整体行为
## 8. 文档与示例
- [x] 8.1 更新 probes.example.yaml展示 headers 和 body 分组全部用法示例
- [x] 8.2 更新 README.md 配置说明章节,补充 expect.body 和 headers 的文档
- [x] 8.3 更新 README.md 依赖列表(如有需要)

View File

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

View File

@@ -0,0 +1,122 @@
## Context
当前 Dashboard 使用 `TargetTable` + `TargetRow` + `TargetDetail` 的表格布局,所有目标扁平排列在同一个表格中,点击行展开内联详情面板。前端组件结构为:
```
App → SummaryCards(4) + TargetTable → TargetRow → TargetDetail(内联)
```
后端 API 提供 `GET /api/targets` 返回含 `sparkline: number[]` 的目标列表,`GET /api/targets/:id/trend?hours=24` 返回趋势数据,`GET /api/targets/:id/history?limit=20` 返回历史记录。全局汇总含 `avgDurationMs`
本次重构涉及全栈变更YAML 配置格式、后端数据存储、API 接口、前端组件和样式。
## Goals / Non-Goals
**Goals:**
- 引入 target 分组概念按组展示卡片default 组排最前
- 卡片内同时展示状态条UP/DOWN 可视化)和迷你 sparkline耗时趋势
- 模态框提供丰富的详情查看体验:多维统计图 + 带分页的检查结果列表
- 模态框支持自定义时间范围筛选(分钟精度)和快捷时间范围按钮
- 移除无实际意义的全局平均耗时统计
- 统一卡片迷你可视化的采样数量为全局可配置项 `recentSampleCount`
- 不引入新依赖,复用现有 recharts 库
**Non-Goals:**
- 分组折叠/展开功能
- 分组排序自定义(固定为 default 最前,其余按 YAML 出现顺序)
- per-target 的 sparkline 数量自定义(统一使用全局配置)
- 模态框内的状态筛选(仅支持时间范围筛选)
- 卡片内显示可用率数字
- 按耗时阈值筛选
## Decisions
### D1: group 配置采用扁平字段(方案 A
**选择**: 在每个 target 上加 `group?: string` 可选字段。
**替代方案**: 嵌套结构(`targets: [{ group: "x", items: [...] }]`)。
**理由**: 扁平字段是增量变更,完全向后兼容,不破坏现有 targets 数组格式。嵌套结构会改变整个配置文件的顶层结构,影响面大且无额外收益。
### D2: sparkline 替换为 recentSamples 结构化数据
**选择**: 将 `sparkline: number[]` 替换为 `recentSamples: RecentSample[]`,每个 sample 包含 `timestamp``durationMs``up`
**替代方案**: 新增独立的 status-bar API。
**理由**: 合并为一个接口减少请求数,前端一次数据同时满足状态条和 sparkline 两种可视化。`timestamp` 的包含使得 hover tooltip 有意义。
### D3: recentSampleCount 固定为 30
**选择**: StatusBar 和 MiniSparkline 的采样数量硬编码为 30。
**理由**: 30 是合理的默认值,覆盖最近 30 次检查,无需暴露配置项增加复杂度。
### D4: 模态框时间筛选同时支持快捷按钮和自定义日期选择器
**选择**: 快捷按钮1h/6h/24h/7d与分钟精度日期选择器并存联动设计——点击快捷按钮自动填入日期手动修改日期则快捷按钮取消高亮。
**理由**: 快捷按钮覆盖绝大多数场景,日期选择器提供精确控制能力。分钟精度对于拨测监控场景足够精确。
### D5: trend API 改用 from/to 时间范围参数
**选择**: `GET /api/targets/:id/trend?from=ISO&to=ISO` 替代 `?hours=24`
**理由**: 模态框支持自定义时间范围hours 参数无法表达任意时间范围。from/to 是更通用的设计。
### D6: history API 新增分页支持
**选择**: `GET /api/targets/:id/history?from=ISO&to=ISO&page=1&pageSize=20`,返回 `{ items, total, page, pageSize }`
**理由**: 自定义时间范围可能导致大量数据(如选择 7 天范围),分页避免一次性传输过多数据。
### D7: SummaryCards 从 4 个减为 3 个
**选择**: 移除"平均耗时"卡片,保留"全部/正常/异常"。
**理由**: 引入分组后,不同分组目标的平均耗时混合计算没有实际参考价值。具体目标的耗时信息在模态框中查看。
### D8: targets 表使用 grp 列名
**选择**: 数据库列名使用 `grp` 而非 `group`
**理由**: `group` 是 SQL 关键字,使用 `grp` 避免转义问题。API 层和前端仍使用 `group` 作为字段名。
### D9: 卡片固定宽度 280px + CSS Grid auto-fill 响应式
**选择**: `grid-template-columns: repeat(auto-fill, 280px)` 实现响应式布局。
**替代方案**: 百分比宽度或 flex-wrap。
**理由**: 固定宽度保证卡片内容一致性auto-fill 自动适应视口宽度变化,从 1 列到多列无缝适配。
### D10: 分组排序由后端 SQL 保证
**选择**: `ORDER BY CASE WHEN grp='default' THEN 0 ELSE 1 END, grp, id`
**理由**: 后端排序后前端只需顺序遍历渲染,无需额外排序逻辑。分组名按 YAML 首次出现顺序(即 id 顺序)自然排序。
### D11: 环形图Donut Chart展示状态分布
**选择**: 模态框统计图使用 recharts 的 PieChart + 内部标签实现环形图,中间显示可用率百分比。
**替代方案**: 纯饼图。
**理由**: 环形图中间可展示关键数字(可用率 %),信息密度更高。
### D12: 状态条使用连续色块
**选择**: 方块数量固定 30 个,每个 6px 宽 2px 间距UP 绿色 `#1fbf75`DOWN 红色 `#e5484d`,无数据灰色 `#e2e8f0`
**理由**: 类似 GitHub contribution graph 的可视化方式,直观展示最近检查状态。
## Risks / Trade-offs
- [卡片信息密度] 卡片宽度仅 280px同时放状态条和 sparkline 可能显得拥挤 → 状态条和 sparkline 各占一行,垂直堆叠,控制高度在合理范围
- [API BREAKING 变更] sparkline → recentSamples、trend hours → from/to、history limit → page/pageSize 均为不兼容变更 → 项目未上线无需向前兼容,一次性完成
- [targets 表 schema 变更] 新增 grp 列需要数据库 migration → SQLite ALTER TABLE ADD COLUMN 是安全操作,新列有默认值不影响已有数据
- [模态框复杂度] 时间选择器 + 分页 + 多图表实现复杂度较高 → 拆分为独立子组件,每个组件职责单一
- [recentSampleCount 默认值] 固定为 30无法通过配置调整 → 合理值30 覆盖足够长的最近检查周期

View File

@@ -0,0 +1,38 @@
## Why
当前 Dashboard 使用表格列表展示所有拨测目标,缺乏分组组织能力,无法直观反映目标的归属关系和批量状态。表格内联展开的详情面板信息密度低、交互不流畅。需要重构为卡片式布局,引入分组概念,并通过模态框提供更丰富的详情查看体验。同时移除全局平均耗时统计(跨分组平均耗时无实际意义),并优化 API 以支持时间范围筛选和分页。
## What Changes
- **BREAKING**: 将前端从表格布局重构为按分组展示的卡片式布局,每个卡片固定宽度,响应式排列
- **BREAKING**: target 配置新增可选 `group` 字段,未指定时默认为 `"default"`default 分组排最前
- **BREAKING**: 点击卡片弹出模态框替代内联展开详情,模态框左侧展示多维统计图(可用率趋势、耗时趋势、状态分布环形图),右侧展示带分页的检查结果列表
- **BREAKING**: 模态框支持时间范围筛选包含快捷按钮1h/6h/24h/7d和自定义日期时间选择器分钟精度
- **BREAKING**: API 接口变更sparkline 替换为 recentSamples包含状态信息trend/history 支持 `from/to` 时间范围参数history 支持分页
- **BREAKING**: 移除 SummaryResponse.avgDurationMs 及相关计算逻辑SummaryCards 从 4 个变为 3 个(全部/正常/异常)
- **BREAKING**: 移除 TargetStats.avgDurationMs 和 TargetStats.p99DurationMs这些统计仅在模态框详情中按需展示
## Capabilities
### New Capabilities
- `target-grouping`: target 分组能力,包括 YAML 配置的 group 字段、后端存储与 API 返回、前端按分组展示(带统计的分组标题)
- `card-dashboard`: 卡片式 Dashboard 布局,包括分组卡片网格、卡片内状态条和迷你 sparkline 双可视化、卡片点击交互
- `target-detail-modal`: 目标详情模态框,包括时间范围筛选器(快捷按钮 + 分钟精度日期选择器)、左侧多维统计图(可用率趋势折线、耗时趋势折线、状态分布环形图)、右侧分页检查结果列表
### Modified Capabilities
- `probe-config`: 新增 `targets[].group` 可选字段
- `probe-api`: API 端点变更——summary 移除 avgDurationMstargets 返回 group 和 recentSamples 替代 sparklinetrend 改用 from/to 参数替代 hourshistory 改用 from/to + page/pageSize 并返回带分页信息的结构
- `probe-data-store`: targets 表新增 grp 列存储分组信息;新增 getRecentSamples 方法替代 getSparklinetrend/history 查询改用时间范围参数history 查询支持分页;移除 avgDurationMs 相关聚合
- `probe-dashboard`: 全面重构前端组件从表格布局改为卡片式分组布局SummaryCards 减为 3 个TargetTable/TargetRow/TargetDetail 替换为 TargetBoard/TargetCard/TargetDetailModal 等
## Impact
- **配置文件**: `probes.example.yaml` 需更新示例,新增 group 字段示例
- **后端**: `types.ts``config-loader.ts``store.ts``app.ts` 需修改targets 表需 schema migration
- **共享类型**: `src/shared/api.ts` 需修改(新增 RecentSample、HistoryResponse 类型,移除/修改部分字段)
- **前端**: 组件全面重构,新增 StatusBar、GroupHeader、StatusDonut、TimeRangePicker、Pagination 等组件CSS 样式全面重写
- **测试**: 后端测试需覆盖新 API 参数和返回结构,前端测试需更新
- **依赖**: 不引入新依赖,全部使用现有 recharts 库

View File

@@ -0,0 +1,70 @@
## ADDED Requirements
### Requirement: 分组卡片布局
Dashboard SHALL 按分组展示所有拨测目标,每个分组包含带统计的分组标题和固定宽度的卡片网格。
#### Scenario: 按分组展示目标
- **WHEN** 用户打开 Dashboard 页面
- **THEN** 页面 SHALL 按分组展示目标卡片,"默认分组" 排在最上面,其余分组按 YAML 配置顺序排列
#### Scenario: 分组标题带统计
- **WHEN** 页面渲染某个分组
- **THEN** 分组标题 SHALL 显示分组名称、该分组内目标总数、正常数和异常数,格式为 `分组名 (N个, X UP / Y DOWN)`
#### Scenario: "default" 分组显示名称
- **WHEN** 分组名称为 "default"
- **THEN** 分组标题 SHALL 显示 "默认分组"
### Requirement: 响应式卡片网格
Dashboard SHALL 使用固定宽度的卡片配合响应式网格布局。
#### Scenario: 卡片固定宽度
- **WHEN** 页面渲染卡片
- **THEN** 每个卡片 SHALL 固定宽度 280px
#### Scenario: 响应式列数
- **WHEN** 视口宽度变化
- **THEN** 卡片网格 SHALL 自动调整列数,使用 CSS Grid auto-fill 适配可用空间
### Requirement: 目标卡片内容
每个目标卡片 SHALL 展示目标名称、当前状态、类型标签、状态条和迷你耗时趋势线。
#### Scenario: 卡片第一行内容
- **WHEN** 卡片渲染
- **THEN** 卡片第一行 SHALL 展示状态指示圆点UP 绿色 / DOWN 红色、目标名称和类型标签HTTP / Command
#### Scenario: 卡片状态指示圆点
- **WHEN** 目标最近一次拨测 success=true 且 matched=true
- **THEN** 卡片状态圆点 SHALL 显示为绿色
- **WHEN** 目标最近一次拨测 success=false 或 matched=false
- **THEN** 卡片状态圆点 SHALL 显示为红色
#### Scenario: 卡片状态条可视化
- **WHEN** 卡片渲染且 recentSamples 数据可用
- **THEN** 卡片 SHALL 展示一条状态条每个采样点为一个色块UP 显示绿色(#1fbf75DOWN 显示红色(#e5484d),无数据显示灰色(#e2e8f0
#### Scenario: 卡片迷你耗时趋势线
- **WHEN** 卡片渲染且 recentSamples 中有 durationMs 数据
- **THEN** 卡片 SHALL 展示基于 recharts 的迷你折线图,展示最近 N 次检查的耗时趋势
### Requirement: 卡片交互
卡片 SHALL 支持 hover 效果和点击打开模态框。
#### Scenario: 卡片 hover 效果
- **WHEN** 鼠标悬停在卡片上
- **THEN** 卡片 SHALL 显示上浮效果(阴影加深)
#### Scenario: 卡片点击打开详情
- **WHEN** 用户点击某个目标卡片
- **THEN** 系统 SHALL 打开该目标的详情模态框
### Requirement: SummaryCards 变更
Dashboard 顶部 SHALL 展示 3 个统计卡片:全部目标数、正常数、异常数。
#### Scenario: 展示 3 个统计卡片
- **WHEN** 用户打开 Dashboard 页面
- **THEN** 页面顶部 SHALL 显示 3 个统计卡片:全部目标数、正常目标数、异常目标数
#### Scenario: 统计数据自动刷新
- **WHEN** 页面处于打开状态
- **THEN** 统计卡片 SHALL 每 5-10 秒自动刷新数据

View File

@@ -0,0 +1,96 @@
## ADDED Requirements
### Requirement: 目标列表返回分组和采样数据
`GET /api/targets` SHALL 返回每个目标的分组信息和结构化采样数据,替代原有 sparkline。
#### Scenario: 返回分组信息
- **WHEN** 客户端请求 `GET /api/targets`
- **THEN** 响应中每个目标 SHALL 包含 `group` 字段,值为该目标所属的分组名称
#### Scenario: 返回 recentSamples
- **WHEN** 客户端请求 `GET /api/targets`
- **THEN** 响应中每个目标 SHALL 包含 `recentSamples` 数组,每个元素包含 `timestamp`ISO 8601`durationMs`number | null`up`booleansuccess && matched
#### Scenario: recentSamples 数量
- **WHEN** 客户端请求 `GET /api/targets`
- **THEN** 每个目标的 recentSamples SHALL 最多包含 30 个元素,按时间倒序排列
#### Scenario: 目标无历史记录
- **WHEN** 某目标尚未执行过任何拨测
- **THEN** 其 recentSamples SHALL 为空数组
### Requirement: 趋势 API 支持时间范围
`GET /api/targets/:id/trend` SHALL 支持 `from``to` 查询参数指定时间范围。
#### Scenario: 指定时间范围查询趋势
- **WHEN** 客户端请求 `GET /api/targets/1/trend?from=2026-05-03T00:00:00Z&to=2026-05-10T00:00:00Z`
- **THEN** 系统 SHALL 返回指定时间范围内按小时分组的聚合数据
#### Scenario: from 或 to 参数缺失
- **WHEN** 客户端请求 `GET /api/targets/1/trend` 未提供 from 或 to 参数
- **THEN** 系统 SHALL 返回 400 状态码和错误信息
#### Scenario: 无效的时间参数
- **WHEN** 客户端请求 `GET /api/targets/1/trend?from=invalid`
- **THEN** 系统 SHALL 返回 400 状态码和错误信息
### Requirement: 历史记录 API 支持时间范围和分页
`GET /api/targets/:id/history` SHALL 支持 `from``to` 时间范围参数和 `page``pageSize` 分页参数,返回带分页信息的结构。
#### Scenario: 指定时间范围和分页
- **WHEN** 客户端请求 `GET /api/targets/1/history?from=ISO&to=ISO&page=1&pageSize=20`
- **THEN** 系统 SHALL 返回 JSON 包含 `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/1/history?from=ISO&to=ISO&page=abc`
- **THEN** 系统 SHALL 返回 400 状态码和错误信息
### Requirement: 新增共享类型
系统 SHALL 在 `src/shared/api.ts` 中定义 `RecentSample``HistoryResponse` 类型。
#### Scenario: RecentSample 类型
- **WHEN** 前后端共享 `RecentSample` 类型
- **THEN** 该类型 SHALL 包含 `timestamp: string``durationMs: number | null``up: boolean` 字段
#### Scenario: HistoryResponse 类型
- **WHEN** 前后端共享 `HistoryResponse` 类型
- **THEN** 该类型 SHALL 包含 `items: CheckResult[]``total: number``page: number``pageSize: number` 字段
## MODIFIED Requirements
### Requirement: 总览统计 API
系统 SHALL 提供 `GET /api/summary` 端点,返回所有目标的总体统计信息(不含平均耗时)。
#### Scenario: 获取总览统计
- **WHEN** 客户端请求 `GET /api/summary`
- **THEN** 系统 SHALL 返回 JSON 包含 total总目标数、up正常数、down异常数、lastCheckTime最近一次检查时间
### Requirement: 目标列表 API
系统 SHALL 提供 `GET /api/targets` 端点,返回所有 typed target 及其最新状态、分组信息和结构化采样数据。
#### Scenario: 获取目标列表
- **WHEN** 客户端请求 `GET /api/targets`
- **THEN** 系统 SHALL 返回 JSON 数组每个元素包含目标基本信息id、name、group、type、target、interval、最近一次检查结果timestamp、success、matched、durationMs、statusDetail、failure和结构化采样数据 recentSamples代替原 sparkline
#### Scenario: 目标无历史记录
- **WHEN** 某目标尚未执行过任何拨测
- **THEN** 其 latestCheck 为 nullrecentSamples 为空数组
### Requirement: 历史记录 API
系统 SHALL 提供 `GET /api/targets/:id/history` 端点,支持时间范围筛选和分页返回指定目标的拨测记录。
#### 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

View File

@@ -0,0 +1,29 @@
## ADDED Requirements
### Requirement: target 分组字段
系统 SHALL 支持在每个 target 上配置可选的 `group` 字段。
#### Scenario: 配置分组名称
- **WHEN** YAML 配置中某个 target 指定 `group: "搜索引擎"`
- **THEN** 系统 SHALL 将该 group 值解析并传递给后续模块
#### Scenario: group 字段可选
- **WHEN** YAML 配置中某个 target 未指定 `group` 字段
- **THEN** 系统 SHALL 使用默认值 "default"
## MODIFIED Requirements
### Requirement: YAML 配置文件格式
系统 SHALL 支持通过 YAML 配置文件定义全部运行参数,包括 server 配置、runtime 配置、checker 默认值和 typed target 列表(含可选 group 字段。target MUST 使用 `type` 字段声明 checker 类型HTTP 领域字段 MUST 放在 `http` 分组command 领域字段 MUST 放在 `command` 分组。
#### Scenario: 完整配置文件解析
- **WHEN** 系统启动并读取包含 server、runtime、defaults、targets含 group 字段)的 YAML 配置文件
- **THEN** 系统 SHALL 正确解析所有字段并用于初始化服务、调度引擎和对应 checker runner
#### Scenario: 最简 HTTP 配置文件解析
- **WHEN** 系统读取只包含一个 `type: http` target 和 `http.url` 的 YAML 配置文件(省略 server、runtime、defaults 和 expect
- **THEN** 系统 SHALL 使用内置默认值填充未指定的字段host=127.0.0.1, port=3000, dir=./data, interval=30s, timeout=10s, runtime.maxConcurrentChecks=20, http.method=GET, http.maxBodyBytes=100MB, group="default"
#### Scenario: per-target 配置覆盖全局默认值
- **WHEN** 某个 target 指定 interval、timeout 或对应领域分组中的默认字段
- **THEN** 该 target SHALL 使用其自身的值,不受 defaults 中对应字段影响

View File

@@ -0,0 +1,113 @@
## ADDED Requirements
### Requirement: 卡片式分组布局
Dashboard SHALL 使用按分组展示的卡片式布局替代表格布局,每个分组包含带统计的分组标题和响应式卡片网格。
#### Scenario: 按分组渲染卡片
- **WHEN** 用户打开 Dashboard 页面
- **THEN** 页面 SHALL 按 group 字段将目标分组展示,每组一个区域,"默认分组" 排在最上面
#### Scenario: 无分组时的展示
- **WHEN** 所有目标均属于 "default" 分组
- **THEN** 页面 SHALL 显示一个 "默认分组" 区域,卡片正常展示
### Requirement: 分组标题展示
Dashboard SHALL 在每个分组区域上方显示带统计信息的分组标题。
#### Scenario: 显示分组统计
- **WHEN** 渲染分组区域
- **THEN** 分组标题 SHALL 显示格式为 `分组名 (N个, X UP / Y DOWN)` 的统计信息
#### Scenario: default 分组标题
- **WHEN** 分组名为 "default"
- **THEN** 标题 SHALL 显示 "默认分组"
### Requirement: 目标卡片交互
Dashboard SHALL 支持点击卡片弹出模态框查看详情。
#### Scenario: 点击卡片打开模态框
- **WHEN** 用户点击某个目标卡片
- **THEN** 系统 SHALL 弹出目标详情模态框,展示该目标的统计图表和检查记录
### Requirement: 卡片状态条可视化
Dashboard SHALL 在卡片中展示最近 N 次检查的状态条,每个采样点用色块表示 UP/DOWN 状态。
#### Scenario: 渲染状态条
- **WHEN** 卡片的 recentSamples 数据可用
- **THEN** 卡片 SHALL 展示一条由色块组成的状态条UP 为绿色DOWN 为红色,无数据为灰色
### Requirement: 卡片迷你耗时趋势线
Dashboard SHALL 在卡片中展示基于 recentSamples 的迷你耗时折线图。
#### Scenario: 渲染迷你趋势线
- **WHEN** 卡片的 recentSamples 中有 durationMs 数据
- **THEN** 卡片 SHALL 展示基于 recharts 的迷你折线图,展示最近采样的耗时趋势
### Requirement: 目标详情模态框
Dashboard SHALL 提供模态框展示目标详情,包含时间范围筛选、多维统计图和分页检查记录列表。
#### Scenario: 模态框布局
- **WHEN** 模态框打开
- **THEN** 模态框 SHALL 占据视口 80% 宽度,图表区在上方展示统计图,检查记录列表在下方展示
#### Scenario: 时间范围筛选
- **WHEN** 模态框打开
- **THEN** 筛选栏 SHALL 包含快捷按钮1h/6h/24h/7d和分钟精度的自定义日期时间选择器
#### Scenario: 统计图表
- **WHEN** 模态框加载完成
- **THEN** 左侧 SHALL 展示可用率趋势折线图、耗时趋势折线图和状态分布环形图
#### Scenario: 检查记录分页
- **WHEN** 检查记录超过一页
- **THEN** 右侧列表底部 SHALL 展示分页器
## MODIFIED Requirements
### Requirement: 总览统计卡片
Dashboard SHALL 在页面顶部展示总览统计卡片,包含总目标数、正常数和异常数(移除平均耗时)。
#### Scenario: 展示统计卡片
- **WHEN** 用户打开 Dashboard 页面
- **THEN** 页面顶部 SHALL 显示 3 个统计卡片:全部目标数、正常目标数、异常目标数
#### Scenario: 统计数据自动刷新
- **WHEN** 页面处于打开状态
- **THEN** 统计卡片 SHALL 每 5-10 秒自动刷新数据
### Requirement: 页面加载与错误状态
Dashboard SHALL 正确处理加载状态和 API 错误,适配卡片式布局。
#### Scenario: 首次加载
- **WHEN** 页面首次加载且数据尚未返回
- **THEN** 页面 SHALL 显示加载状态指示
#### Scenario: API 请求失败
- **WHEN** 前端轮询 API 请求失败
- **THEN** 页面 SHALL 显示错误提示,并在下一次轮询周期自动重试
#### Scenario: 模态框内部加载状态
- **WHEN** 模态框内趋势数据或历史记录正在加载
- **THEN** 对应图表或列表区域 SHALL 显示加载指示
## REMOVED Requirements
### Requirement: 目标列表表格
**Reason**: 替换为卡片式分组布局
**Migration**: 使用 TargetBoard + TargetGroup + CardGrid + TargetCard 替代 TargetTable + TargetRow
### Requirement: 可展开的目标详情面板
**Reason**: 替换为目标详情模态框
**Migration**: 使用 TargetDetailModal 替代内联展开的 TargetDetail
### Requirement: 历史记录展示
**Reason**: 合并到目标详情模态框的需求中
**Migration**: 历史记录在模态框右侧展示,支持时间范围筛选和分页
### Requirement: 趋势图可视化
**Reason**: 合并到目标详情模态框的需求中,卡片内的迷你图独立定义
**Migration**: 模态框内的趋势图在 target-detail-modal spec 中定义,卡片内的迷你图在 card-dashboard spec 中定义
### Requirement: checker 类型展示
**Reason**: 功能保留但合并到卡片内容需求中,无需独立 requirement
**Migration**: 类型标签在卡片的行1中展示

View File

@@ -0,0 +1,67 @@
## ADDED Requirements
### Requirement: targets 表分组列
系统 SHALL 在 targets 表中新增 `grp` 列存储分组信息。
#### Scenario: 新增 grp 列
- **WHEN** 数据库初始化
- **THEN** targets 表 SHALL 包含 `grp TEXT NOT NULL DEFAULT 'default'`
#### Scenario: 同步分组信息
- **WHEN** 系统同步 targets 到数据库
- **THEN** 每个 target 的 grp 列 SHALL 存储其 group 配置值,未配置的存储 'default'
### Requirement: 结构化采样数据查询
系统 SHALL 提供 `getRecentSamples` 方法替代 `getSparkline`,返回包含状态信息的结构化采样数据。
#### Scenario: 获取最近采样数据
- **WHEN** 调用 `getRecentSamples(targetId, 30)`
- **THEN** 系统 SHALL 返回最多 30 条记录,每条包含 timestamp、duration_ms、success、matched
#### Scenario: 采样数据排序
- **WHEN** 获取采样数据
- **THEN** 记录 SHALL 按 timestamp 降序排列(最新在前)
### Requirement: 趋势数据时间范围查询
系统 SHALL 支持按任意时间范围查询趋势聚合数据,替代固定 hours 参数。
#### Scenario: 按时间范围查询趋势
- **WHEN** 查询指定 target 在 from 到 to 时间范围内的趋势数据
- **THEN** 系统 SHALL 返回按小时分组的聚合数据,包括每小时的 avgDurationMs、availability 和 totalChecks
### Requirement: 历史记录时间范围和分页查询
系统 SHALL 支持按时间范围筛选并分页查询历史记录。
#### Scenario: 按时间范围筛选历史记录
- **WHEN** 查询指定 target 在 from 到 to 时间范围内的历史记录
- **THEN** 系统 SHALL 返回该时间范围内的记录,按 timestamp 降序排列
#### Scenario: 分页查询历史记录
- **WHEN** 查询指定 page 和 pageSize 的历史记录
- **THEN** 系统 SHALL 返回对应页的数据和总记录数
### Requirement: 目标列表按分组排序
系统 SHALL 保证 targets 查询结果按分组排序返回。
#### Scenario: 分组排序查询
- **WHEN** 查询所有 targets
- **THEN** 结果 SHALL 将 "default" 分组目标排在首位,其余分组按分组名称和目标插入顺序排列
## MODIFIED Requirements
### Requirement: targets 表同步
系统 SHALL 在启动时将 YAML 配置中的目标列表同步到 SQLite targets 表,并持久化 target 类型、展示摘要、领域配置、调度配置、expect 配置和分组信息。
#### Scenario: 首次同步目标
- **WHEN** 数据库为空且 YAML 中定义了 N 个 typed target
- **THEN** 系统 SHALL 将所有目标插入 targets 表,包含 name、type、target、config、interval_ms、timeout_ms、expect 和 grp
#### Scenario: 配置变更后重新同步
- **WHEN** YAML 配置发生变更(新增、删除或修改目标)后重启
- **THEN** 系统 SHALL 根据 name 字段匹配:新增的插入、删除的移除、修改的更新(含 grp 字段)
## REMOVED Requirements
### Requirement: sparkline 查询
**Reason**: 替换为结构化 getRecentSamples 方法
**Migration**: 使用 getRecentSamples 替代 getSparkline新方法返回更丰富的结构化数据

View File

@@ -0,0 +1,72 @@
## ADDED Requirements
### Requirement: 目标详情模态框
Dashboard SHALL 在用户点击目标卡片后弹出模态框,展示该目标的详细统计图表和检查结果列表。
#### Scenario: 打开模态框
- **WHEN** 用户点击某个目标卡片
- **THEN** 系统 SHALL 弹出模态框,占据视口 80% 宽度,展示该目标的详情
#### Scenario: 模态框默认时间范围
- **WHEN** 模态框打开
- **THEN** 筛选器 SHALL 默认选中"最近 24 小时"
#### Scenario: 关闭模态框
- **WHEN** 用户点击模态框关闭按钮或模态框外部区域
- **THEN** 模态框 SHALL 关闭
### Requirement: 时间范围筛选
模态框 SHALL 支持通过快捷按钮和自定义日期时间选择器筛选数据的时间范围。
#### Scenario: 快捷时间范围按钮
- **WHEN** 模态框渲染
- **THEN** 筛选栏 SHALL 显示快捷按钮1h、6h、24h、7d当前选中的按钮高亮显示
#### Scenario: 点击快捷按钮
- **WHEN** 用户点击快捷按钮(如 "24h"
- **THEN** 筛选器 SHALL 自动设置对应的起止时间,日期选择器显示对应的时间范围,该按钮高亮
#### Scenario: 自定义日期时间选择
- **WHEN** 用户通过日期时间选择器修改起止时间(分钟精度)
- **THEN** 快捷按钮 SHALL 取消高亮,表示当前为自定义时间范围
#### Scenario: 筛选触发数据刷新
- **WHEN** 时间范围发生变化(快捷按钮或自定义选择)
- **THEN** 系统 SHALL 重新请求该时间范围内的趋势数据和历史记录
### Requirement: 统计图表展示
模态框左侧 SHALL 展示可用率趋势折线图、耗时趋势折线图和状态分布环形图。
#### Scenario: 可用率趋势折线图
- **WHEN** 模态框加载完成且趋势数据可用
- **THEN** 左侧 SHALL 展示可用率随时间变化的折线图Y 轴为可用率百分比
#### Scenario: 耗时趋势折线图
- **WHEN** 模态框加载完成且趋势数据可用
- **THEN** 左侧 SHALL 展示耗时随时间变化的折线图Y 轴为耗时毫秒数
#### Scenario: 状态分布环形图
- **WHEN** 模态框加载完成
- **THEN** 左侧 SHALL 展示环形图Donut Chart外圈显示 UP/DOWN 比例(绿色/红色),中间显示可用率百分比数字
### Requirement: 检查结果列表
模态框右侧 SHALL 展示当前筛选时间范围内的检查结果列表,支持分页浏览。
#### Scenario: 展示检查结果
- **WHEN** 模态框加载完成且历史记录可用
- **THEN** 右侧 SHALL 展示检查结果列表每条包含时间戳、UP/DOWN 状态标记、耗时毫秒数、statusDetail 和 failure 信息
#### Scenario: 分页导航
- **WHEN** 检查结果总数超过一页
- **THEN** 列表底部 SHALL 展示分页器,用户可点击切换页码
#### Scenario: 翻页刷新
- **WHEN** 用户点击分页器切换页码
- **THEN** 系统 SHALL 请求对应页码的历史记录数据,列表更新
### Requirement: 模态框布局
模态框 SHALL 采用自上而下布局,上方展示统计图表,下方展示检查记录列表。
#### Scenario: 自上而下渲染
- **WHEN** 模态框渲染
- **THEN** 内容区域 SHALL 分为上下两部分,上方展示统计图表,下方展示检查结果列表和分页器

View File

@@ -0,0 +1,41 @@
## ADDED Requirements
### Requirement: target 分组配置
系统 SHALL 支持在每个 target 上配置可选的 `group` 字段,用于将目标归类到不同分组。未指定 `group` 的 target SHALL 归入 `"default"` 分组。
#### Scenario: 配置分组名称
- **WHEN** YAML 配置中某个 target 指定 `group: "搜索引擎"`
- **THEN** 系统 SHALL 将该 target 归类到 "搜索引擎" 分组
#### Scenario: 不配置分组
- **WHEN** YAML 配置中某个 target 未指定 `group` 字段
- **THEN** 系统 SHALL 将该 target 归类到 "default" 分组
#### Scenario: group 字段类型校验
- **WHEN** YAML 配置中某个 target 的 `group` 字段不是字符串
- **THEN** 系统 SHALL 以错误退出并提示 group 字段类型错误
### Requirement: 分组排序
系统 SHALL 保证 "default" 分组始终排在最前面,其余分组按配置文件中首次出现的顺序排列。
#### Scenario: default 分组排最前
- **WHEN** 配置中存在多个分组(包括 "default" 和自定义分组)
- **THEN** API 返回的目标列表中 "default" 分组的目标 SHALL 排在其他分组之前
#### Scenario: 自定义分组按出现顺序
- **WHEN** 配置中 "搜索引擎" 分组在 "后端服务" 分组之前首次出现
- **THEN** API 返回中 "搜索引擎" 分组 SHALL 排在 "后端服务" 分组之前
### Requirement: 分组信息 API 传递
系统 SHALL 在 API 响应中返回每个 target 的分组信息。
#### Scenario: targets 列表包含分组
- **WHEN** 客户端请求 `GET /api/targets`
- **THEN** 响应中每个目标 SHALL 包含 `group` 字段,值为该目标所属的分组名称
### Requirement: 分组存储
系统 SHALL 在数据库 targets 表中持久化每个 target 的分组信息。
#### Scenario: 持久化分组信息
- **WHEN** 系统同步 targets 到数据库
- **THEN** 每个 target 的 `grp` 列 SHALL 存储其分组名称,未配置分组的存储 `"default"`

View File

@@ -0,0 +1,64 @@
## 1. 后端:配置与类型
- [x] 1.1 types.ts: BaseTargetConfig 新增 group?: string, ResolvedTarget 新增 group: string
- [x] 1.2 config-loader.ts: 解析 group 字段(默认 "default"
- [x] 1.3 shared/api.ts: 新增 RecentSample 接口和 HistoryResponse 接口,移除 SummaryResponse.avgDurationMs移除 TargetStats.avgDurationMs 和 TargetStats.p99DurationMsTargetStatus 中 sparkline 替换为 recentSamples: RecentSample[],新增 group: string
- [x] 1.4 编写 config-loader 的 group 解析校验测试
## 2. 后端:数据存储
- [x] 2.1 store.ts: targets 表新增 grp 列ALTER TABLE 或重建建表语句syncTargets 写入 grp 值
- [x] 2.2 store.ts: 新增 getRecentSamples(targetId, limit) 方法替代 getSparkline返回包含 timestamp/duration_ms/success/matched 的结构化数据
- [x] 2.3 store.ts: getTrend 改用 from/to 时间范围参数替代 hours
- [x] 2.4 store.ts: getHistory 改用 from/to 时间范围 + page/pageSize 分页参数,返回 { items, total, page, pageSize }
- [x] 2.5 store.ts: getTargets 排序改为 ORDER BY CASE WHEN grp='default' THEN 0 ELSE 1 END, grp, id
- [x] 2.6 store.ts: getSummary 移除 avgDurationMs 计算逻辑
- [x] 2.7 store.ts: 移除 getSparkline 方法
- [x] 2.8 编写 store 的新增/变更方法的完整测试
## 3. 后端API 路由
- [x] 3.1 app.ts: createSummaryResponse 移除 avgDurationMs 字段
- [x] 3.2 app.ts: createTargetsResponse 返回 group 和 recentSamples 替代 sparkline移除 stats 中的 avgDurationMs 和 p99DurationMs
- [x] 3.3 app.ts: handleTrend 改用 from/to 查询参数(替代 hours校验参数格式
- [x] 3.4 app.ts: handleHistory 改用 from/to + page/pageSize 参数,返回 HistoryResponse 结构(含 total
- [x] 3.5 app.ts: 移除 mapCheckResult 中已不需要的字段映射
- [x] 3.6 编写 API 路由的测试,覆盖 from/to 参数校验、分页参数校验、recentSamples 返回结构
## 4. 前端:组件重构
- [x] 4.1 新增 StatusBar 组件:渲染 recentSampleCount 个色块UP 绿/DOWN 红/无数据灰)
- [x] 4.2 改造 SparklineChart 为 MiniSparkline接收 RecentSample[] 数据,提取 durationMs 绘制迷你折线图
- [x] 4.3 新增 GroupHeader 组件:显示分组名称和统计信息(分组名 (N个, X UP / Y DOWN)default 显示"默认分组"
- [x] 4.4 新增 TargetCard 组件:固定 280px 宽行1 为 StatusDot + 名称 + 类型标签行2 为 StatusBar + MiniSparklinehover 上浮效果,点击触发回调
- [x] 4.5 新增 CardGrid 组件CSS Grid auto-fill 280px 响应式布局,接收 targets 数组渲染 TargetCard
- [x] 4.6 新增 TargetGroup 组件:组合 GroupHeader + CardGrid接收分组名和该组 targets
- [x] 4.7 新增 TargetBoard 组件:接收 targets 数组,前端按 group 分组,顺序渲染 TargetGroup
- [x] 4.8 新增 StatusDonut 组件:基于 recharts PieChart 实现环形图,中间显示可用率百分比,外圈 UP/DOWN 比例
- [x] 4.9 新增 TimeRangePicker 组件快捷按钮1h/6h/24h/7d+ 分钟精度日期选择器,联动逻辑
- [x] 4.10 新增 Pagination 组件:显示页码按钮,支持翻页回调
- [x] 4.11 新增 TargetDetailModal 组件模态框布局80% 视口宽),筛选栏 + 左侧图表区40%+ 右侧列表区60%),组合 TrendChart/StatusDonut/Pagination
- [x] 4.12 改造 TrendChart适配 from/to 参数的时间范围,替代 hours
- [x] 4.13 改造 app.tsxSummaryCards 从 4 卡片改为 3 卡片TargetTable 替换为 TargetBoard模态框状态管理
- [x] 4.14 移除 TargetTable、TargetRow、旧版 TargetDetail 组件
## 5. 前端Hooks 与数据层
- [x] 5.1 新增 useTargetDetail hook管理模态框状态封装 trend + history 的并行请求逻辑
- [x] 5.2 改造 useTrend hook改用 from/to 参数请求 trend API
- [x] 5.3 新增 useHistory hook使用 from/to + page/pageSize 请求 history API返回 HistoryResponse 结构
## 6. 前端:样式
- [x] 6.1 重写 styles.css移除表格相关样式新增卡片样式280px 固定宽、圆角、阴影、分组样式、模态框样式backdrop + 居中 + 左右分栏、StatusBar 样式色块、TimeRangePicker 样式、Pagination 样式、响应式媒体查询
- [x] 6.2 SummaryCards grid 改为 repeat(3, 1fr)
## 7. 文档与配置示例
- [x] 7.1 更新 probes.example.yaml新增 group 字段示例
- [x] 7.2 更新 README.md配置说明新增 groupAPI 端点变更说明,项目结构更新组件列表
## 8. 质量保障
- [x] 8.1 执行 bun run checktypecheck + lint + format:check + 单元测试),修复所有问题
- [x] 8.2 执行 bun run verifycheck + build + smoke test确保构建产物正常运行

View File

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

View File

@@ -0,0 +1,63 @@
## Context
当前系统采用 `success` + `matched` 两层判定模型:
- `success`拨测是否成功完成HTTP 收到响应 / Command 正常退出)
- `matched`expect 规则是否匹配
- UP = `success AND matched`
但实际代码中 `fetcher.ts``command-runner.ts` 均将 `success` 设为 `expectResult.matched`,导致 `success ≡ matched`。两层模型从未真正生效,`success` 字段是冗余的。
同时发现两个附带问题:
1. 分组排序使用 `ORDER BY grp`字母序spec 要求按 YAML 首现顺序
2. `command-runner` 未设置 `stdin: "ignore"`spec 要求禁止写入 stdin
## Goals / Non-Goals
**Goals:**
- 移除 `success` 字段,将判定模型简化为 `matched` 单层判定
- 修复分组排序为 YAML 首现顺序
- 确保 command-runner 禁用 stdin
**Non-Goals:**
- 不涉及 `success` 以外的其他字段变更
- 不涉及前端 UI 样式或布局调整
- 不涉及新增功能特性
- 不处理代码质量问题P2/P3 级别留给后续 change
## Decisions
### 1. 判定模型简化为 matched 单层
**选择**: 移除 `success`,仅保留 `matched`
**理由**: `success``matched` 在实现中始终同值,没有独立语义。执行失败(网络错误、超时、进程崩溃)和 expect 不匹配都统一为 `matched=false`,通过 `failure.kind` 区分(`"error"` vs `"mismatch"`)。
**替代方案**: 修复 `success` 使其真正独立(如 HTTP 返回 404 时 success=true, matched=false。被否决——当前项目不需要区分"请求成功但内容不符"和"请求失败",单层判定更简洁。
### 2. 可用率计算基于 matched
**选择**: `availability = matched=true 的记录数 / 总记录数 * 100`
avgDurationMs 仅计算 `matched=true` 记录的平均耗时:`AVG(CASE WHEN matched = 1 THEN duration_ms END)`
**理由**: 用户关心的是"健康请求"的性能趋势,排除失败请求的干扰。
### 3. 分组排序改为按 id 排序
**选择**: `ORDER BY CASE WHEN grp='default' THEN 0 ELSE 1 END, id`
**理由**: 目标按 YAML 顺序插入,`id` 自增,`ORDER BY id` 天然等于 YAML 首现顺序。无需额外存储排序权重。
### 4. 数据库迁移策略
**选择**: 直接删除旧数据库文件重新创建
**理由**: 项目未上线无向前兼容要求。SQLite 不支持 `DROP COLUMN`(需重建表),直接删除是最简方案。
## Risks / Trade-offs
- [风险] 数据库 schema 变更导致已有数据丢失 → 项目未上线,可接受
- [风险] 前端 API 响应格式变更 → 前端代码同步修改,全量测试覆盖
- [风险] 大量文件同时修改可能引入遗漏 → 通过 `bun run verify` 全量验证

View File

@@ -0,0 +1,38 @@
## Why
当前系统使用 `success` + `matched` 两层判定模型,但实际实现中 `success` 始终等于 `matched`(两者永远同值),导致两层模型退化为单层。`success` 字段没有提供任何独立信息,反而增加了理解成本和维护负担。此外,分组排序使用字母序而非 YAML 配置中的首现顺序,与 spec 要求不一致。
## What Changes
- **BREAKING**: 移除 `success` 字段,将判定模型简化为 `matched` 单层判定
- 数据库 `check_results` 表移除 `success`
- 所有 CheckResult 类型(服务端、共享、前端)移除 `success` 字段
- UP/DOWN 判定统一为 `matched=true` / `matched=false`
- availability 计算简化为 `matched=true 占比`
- avgDurationMs 仅计算 `matched=true` 记录的平均耗时
- 修复分组排序:非 default 分组按 YAML 首现顺序(即 `id` 顺序)排列,而非字母序
- `command-runner` 添加 `stdin: "ignore"`,符合 spec 要求
## Capabilities
### New Capabilities
### Modified Capabilities
- `probe-engine`: 移除结果记录中的 `success` 字段,简化为 `matched` + `failure`
- `probe-api`: `CheckResult``RecentSample` 移除 `success` 字段UP 判定改为 `matched`
- `probe-data-store`: 数据库 schema 移除 `success` 列;可用率定义简化;排序规则修正
- `probe-dashboard`: UP/DOWN 判定改为 `matched`
- `card-dashboard`: UP/DOWN 判定改为 `matched`
- `command-checker`: 移除所有 `success` 引用;确保 stdin 禁用
- `target-grouping`: 排序规则明确为按 id 排序YAML 首现顺序)
## Impact
- **数据库**: `check_results` 表结构变更(移除列),已有数据库需删除重建(项目未上线,无向前兼容要求)
- **API**: `CheckResult` 响应移除 `success` 字段,`RecentSample.up` 计算逻辑变更
- **前端**: 所有依赖 `success` 字段的组件需更新TargetCard、TargetDetailModal、TargetGroup
- **测试**: 5 个测试文件需同步移除 `success` 相关断言
- **README**: 目标状态判定模型说明需更新

View File

@@ -0,0 +1,12 @@
## MODIFIED Requirements
### Requirement: 卡片状态展示
系统 SHALL 在卡片上展示目标的 UP/DOWN 状态。
#### Scenario: 卡片 UP 状态
- **WHEN** 目标最近一次拨测 matched=true
- **THEN** 系统 SHALL 显示绿色状态点
#### Scenario: 卡片 DOWN 状态
- **WHEN** 目标最近一次拨测 matched=false
- **THEN** 系统 SHALL 显示红色状态点

View File

@@ -0,0 +1,26 @@
## MODIFIED Requirements
### Requirement: 命令执行
系统 SHALL 使用 Bun.spawn 执行命令类型目标,继承父进程环境变量并支持覆盖。
#### Scenario: 禁止 stdin 交互
- **THEN** 系统 MUST 设置 stdin 为 "ignore",防止子进程等待标准输入而阻塞
### Requirement: 结果记录
系统 SHALL 记录命令执行的完整结果。
#### Scenario: 命令成功执行
- **WHEN** 命令正常退出
- **THEN** 系统 SHALL 记录 durationMs、statusDetail="exitCode=N",并进入 expect 校验
#### Scenario: 命令启动失败
- **WHEN** 命令无法启动
- **THEN** 系统 SHALL 记录 matched=false并在 failure 中写入 kind=error 和具体错误信息
#### Scenario: 命令超时
- **WHEN** 命令执行超过 timeout 限制
- **THEN** 系统 MUST 终止该子进程,记录 matched=false并在 failure 中写入命令超时信息
#### Scenario: 输出超限
- **WHEN** 命令输出超过 maxOutputBytes 限制
- **THEN** 系统 MUST 停止收集输出并终止该检查,记录 matched=false并在 failure 中写入输出超限信息

View File

@@ -0,0 +1,17 @@
## MODIFIED Requirements
### Requirement: 目标列表 API
系统 SHALL 提供 `GET /api/targets` 端点,返回所有目标及其最新状态。
#### Scenario: recentSamples.up 判定
- **WHEN** 系统返回 recentSamples 数组
- **THEN** 每个元素的 `up` 字段 SHALL 为 `matched === true`
### Requirement: 共享类型
系统 SHALL 在 `src/shared/api.ts` 中定义前后端共享的 TypeScript 类型。
#### Scenario: CheckResult 类型
- **THEN** `CheckResult` 类型 SHALL 包含 timestamp、matched、durationMs、statusDetail、failure 字段,不包含 success 字段
#### Scenario: RecentSample 类型
- **THEN** `RecentSample` 类型 SHALL 包含 timestamp、durationMs、up 字段,其中 up 为 boolean 且等于 matched

View File

@@ -0,0 +1,12 @@
## MODIFIED Requirements
### Requirement: 状态判定与展示
系统 SHALL 根据最近一次拨测结果展示目标状态。
#### Scenario: 目标 UP 状态
- **WHEN** 目标最近一次拨测 matched=true
- **THEN** 系统 SHALL 显示绿色 UP 状态
#### Scenario: 目标 DOWN 状态
- **WHEN** 目标最近一次拨测 matched=false
- **THEN** 系统 SHALL 显示红色 DOWN 状态

View File

@@ -0,0 +1,41 @@
## MODIFIED Requirements
### Requirement: 数据库表结构
系统 SHALL 使用 SQLite 存储 targets 和 check_results 两张表。
#### Scenario: check_results 表结构
- **THEN** check_results 表 SHALL 包含 idINTEGER PRIMARY KEY AUTOINCREMENT、target_idINTEGER NOT NULL、timestampTEXT NOT NULL、matchedINTEGER NOT NULL、duration_msREAL、status_detailTEXT、failureTEXT不包含 success 列
### Requirement: 结果写入
系统 SHALL 将每次拨测结果插入 check_results 表。
#### Scenario: 插入结果记录
- **THEN** 系统 SHALL 插入一条包含 target_id、timestamp、matched、duration_ms、status_detail、failure 的记录
### Requirement: 可用率计算
系统 SHALL 计算目标在指定时间范围内的可用率。
#### Scenario: 可用率定义
- **THEN** 系统 SHALL 返回 matched=true 的记录数占总记录数的百分比
#### Scenario: 平均耗时
- **THEN** 系统 SHALL 返回 duration_ms 的平均值(仅计算 matched=true 的记录)
### Requirement: 目标排序
系统 SHALL 按分组排序返回目标列表。
#### Scenario: 分组排序规则
- **WHEN** 查询目标列表
- **THEN** "default" 分组 SHALL 排在最前,其余分组 SHALL 按 YAML 配置中首次出现的顺序(即 id 自增顺序)排列
### Requirement: 最近采样查询
系统 SHALL 提供获取目标最近 N 条采样记录的方法。
#### Scenario: 采样记录返回字段
- **THEN** 系统 SHALL 返回最多 N 条记录,每条包含 timestamp、duration_ms、matched
### Requirement: 汇总查询
系统 SHALL 提供全局汇总统计。
#### Scenario: UP/DOWN 判定
- **THEN** 系统 SHALL 基于 latestCheck.matched 判定目标 UP 或 DOWNmatched=true 为 UPmatched=false 为 DOWN

View File

@@ -0,0 +1,21 @@
## MODIFIED Requirements
### Requirement: 结果记录
系统 SHALL 在每次 checker 完成后,将结果写入 SQLite 数据存储,包含 target_id、timestamp、matched、duration_ms、status_detail、failure 字段。
#### Scenario: 执行成功且 expect 全部匹配
- **WHEN** checker 执行成功且所有 expect 规则匹配
- **THEN** 系统 SHALL 记录 matched=true、duration_ms、status_detailfailure 为 null
#### Scenario: 执行失败(网络错误、超时、进程异常)
- **THEN** 系统 SHALL 记录 matched=false、failure.kind="error" 和具体错误信息
#### Scenario: expect 不匹配
- **THEN** 系统 SHALL 记录 matched=false、failure.kind="mismatch" 和具体不匹配信息
### Requirement: 输出读取限制
系统 SHALL 对 command checker 的 stdout/stderr 输出设置大小限制。
#### Scenario: 输出超过 maxOutputBytes
- **WHEN** 子进程输出超过 maxOutputBytes 限制
- **THEN** 系统 MUST 停止读取并记录 matched=false 和结构化输出超限错误

View File

@@ -0,0 +1,8 @@
## MODIFIED Requirements
### Requirement: 分组排序
系统 SHALL 对非 default 分组按 YAML 配置中的首次出现顺序排列。
#### Scenario: 非默认分组排序
- **WHEN** 查询目标列表
- **THEN** 非 default 分组 SHALL 按 id 自增顺序排列(即 YAML 配置中的首次出现顺序),而非字母序

View File

@@ -0,0 +1,49 @@
## 1. 核心类型变更
- [x] 1.1 从 `src/server/checker/types.ts` 的 CheckResult 和 StoredCheckResult 类型中移除 `success` 字段
- [x] 1.2 从 `src/shared/api.ts` 的 CheckResult 类型中移除 `success` 字段
## 2. 数据存储层变更
- [x] 2.1 修改 `src/server/checker/store.ts`DDL 移除 `success`INSERT 语句移除 success 绑定,所有查询中移除 success 引用
- [x] 2.2 修改 `src/server/checker/store.ts`getSummary 中 UP 判定改为 `latest.matched`
- [x] 2.3 修改 `src/server/checker/store.ts`getTargetStats 可用率计算改为 `matched = 1`
- [x] 2.4 修改 `src/server/checker/store.ts`getTrend 中 availability 和 avgDurationMs 改为基于 `matched = 1`
- [x] 2.5 修改 `src/server/checker/store.ts`getRecentSamples 返回类型移除 successSELECT 移除 success 列
- [x] 2.6 修改 `src/server/checker/store.ts`:分组排序 ORDER BY 移除 `grp`,改为 `ORDER BY CASE WHEN grp='default' THEN 0 ELSE 1 END, id`
## 3. 拨测执行层变更
- [x] 3.1 修改 `src/server/checker/fetcher.ts`:所有 CheckResult 返回值中移除 `success` 字段
- [x] 3.2 修改 `src/server/checker/command-runner.ts`:所有 CheckResult 返回值中移除 `success` 字段
- [x] 3.3 修改 `src/server/checker/command-runner.ts`Bun.spawn 添加 `stdin: "ignore"`
- [x] 3.4 修改 `src/server/checker/engine.ts`writeResult 调用中移除 `success` 传递
## 4. API 路由层变更
- [x] 4.1 修改 `src/server/app.ts`mapCheckResult 移除 `success` 字段映射
- [x] 4.2 修改 `src/server/app.ts`recentSamples.up 判定改为 `s.matched === 1`
## 5. 前端组件变更
- [x] 5.1 修改 `src/web/components/TargetCard.tsx`isUp 判定改为 `target.latestCheck?.matched`
- [x] 5.2 修改 `src/web/components/TargetGroup.tsx`up 计数改为 `t.latestCheck?.matched`
- [x] 5.3 修改 `src/web/components/TargetDetailModal.tsx`isUp 和 history 行状态改为基于 `matched`
## 6. 测试同步
- [x] 6.1 更新 `tests/server/checker/fetcher.test.ts`:移除所有 `success` 相关断言,改为 `matched` 断言
- [x] 6.2 更新 `tests/server/checker/command-runner.test.ts`:移除所有 `success` 相关断言,改为 `matched` 断言
- [x] 6.3 更新 `tests/server/checker/engine.test.ts`:移除所有 `success` 相关断言,改为 `matched` 断言
- [x] 6.4 更新 `tests/server/checker/store.test.ts`:插入数据移除 `success` 字段,查询断言移除 `success` 检查
- [x] 6.5 更新 `tests/server/app.test.ts`API 响应断言移除 `success` 字段
## 7. 质量验证
- [x] 7.1 执行 `bun run check`typecheck + lint + format + test确保全部通过
- [x] 7.2 执行 `bun run verify`check + build + smoke test确保全部通过
## 8. 文档更新
- [x] 8.1 更新 README.md目标状态判定模型改为 matched 单层判定,移除 success 说明
- [x] 8.2 更新 README.md响应字段中移除 CheckResult 的 success 字段描述

View File

@@ -38,9 +38,9 @@ Dashboard SHALL 使用固定宽度的卡片配合响应式网格布局。
- **THEN** 卡片第一行 SHALL 展示状态指示圆点UP 绿色 / DOWN 红色、目标名称和类型标签HTTP / Command
#### Scenario: 卡片状态指示圆点
- **WHEN** 目标最近一次拨测 success=true 且 matched=true
- **WHEN** 目标最近一次拨测 matched=true
- **THEN** 卡片状态圆点 SHALL 显示为绿色
- **WHEN** 目标最近一次拨测 success=false 或 matched=false
- **WHEN** 目标最近一次拨测 matched=false
- **THEN** 卡片状态圆点 SHALL 显示为红色
#### Scenario: 卡片状态条可视化

View File

@@ -36,23 +36,23 @@ TBD
#### Scenario: 命令正常退出
- **WHEN** command target 执行的进程正常退出且 exit code 为 0
- **THEN** 系统 SHALL 记录 `success=true``durationMs``statusDetail="exitCode=0"`,并进入 expect 校验
- **THEN** 系统 SHALL 记录 `durationMs``statusDetail="exitCode=0"`,并进入 expect 校验
#### Scenario: 命令非零退出
- **WHEN** command target 执行的进程正常退出但 exit code 为 1
- **THEN** 系统 SHALL 记录 `success=true``statusDetail="exitCode=1"`,并由 expect.exitCode 决定 matched 结果
- **THEN** 系统 SHALL 记录 `statusDetail="exitCode=1"`,并由 expect.exitCode 决定 matched 结果
#### Scenario: 命令启动失败
- **WHEN** command target 的 exec 不存在或无法启动
- **THEN** 系统 SHALL 记录 `success=false``matched=false`,并在 failure 中写入 kind=`error`、phase=`exitCode` 和可读错误信息
- **THEN** 系统 SHALL 记录 `matched=false`,并在 failure 中写入 kind=`error` 和可读错误信息
#### Scenario: 命令超时
- **WHEN** command target 在 timeout 时间内未结束
- **THEN** 系统 MUST 终止该子进程,记录 `success=false``matched=false`,并在 failure 中写入命令超时信息
- **THEN** 系统 MUST 终止该子进程,记录 `matched=false`,并在 failure 中写入命令超时信息
#### Scenario: 命令输出超限
- **WHEN** command target 的 stdout 和 stderr 合计输出超过 `maxOutputBytes`
- **THEN** 系统 MUST 停止收集输出并终止该检查,记录 `success=false``matched=false`,并在 failure 中写入输出超限信息
- **THEN** 系统 MUST 停止收集输出并终止该检查,记录 `matched=false`,并在 failure 中写入输出超限信息
### Requirement: command expect 校验
系统 SHALL 支持 command 专用 expect包括 `exitCode``stdout``stderr`,并按 exitCode、duration、stdout、stderr 的阶段顺序快速失败。

View File

@@ -16,7 +16,7 @@
#### Scenario: 获取目标列表
- **WHEN** 客户端请求 `GET /api/targets`
- **THEN** 系统 SHALL 返回 JSON 数组每个元素包含目标基本信息id、name、group、type、target、interval、最近一次检查结果timestamp、success、matched、durationMs、statusDetail、failure、统计摘要totalChecks、availability和结构化采样数据 recentSamples代替原 sparkline
- **THEN** 系统 SHALL 返回 JSON 数组每个元素包含目标基本信息id、name、group、type、target、interval、最近一次检查结果timestamp、matched、durationMs、statusDetail、failure、统计摘要totalChecks、availability和结构化采样数据 recentSamples代替原 sparkline
#### Scenario: 目标无历史记录
- **WHEN** 某目标尚未执行过任何拨测
@@ -57,7 +57,7 @@
#### Scenario: 返回 recentSamples
- **WHEN** 客户端请求 `GET /api/targets`
- **THEN** 响应中每个目标 SHALL 包含 `recentSamples` 数组,每个元素包含 `timestamp`ISO 8601`durationMs`number | null`up`booleansuccess && matched
- **THEN** 响应中每个目标 SHALL 包含 `recentSamples` 数组,每个元素包含 `timestamp`ISO 8601`durationMs`number | null`up`booleanmatched === true
#### Scenario: recentSamples 数量
- **WHEN** 客户端请求 `GET /api/targets`
@@ -68,11 +68,15 @@
- **THEN** 其 recentSamples SHALL 为空数组
### Requirement: 新增共享类型
系统 SHALL 在 `src/shared/api.ts` 中定义 `RecentSample``HistoryResponse` 类型。
系统 SHALL 在 `src/shared/api.ts` 中定义 `CheckResult``RecentSample``HistoryResponse` 类型。
#### Scenario: CheckResult 类型
- **WHEN** 前后端共享 `CheckResult` 类型
- **THEN** 该类型 SHALL 包含 `timestamp: string``matched: boolean``durationMs: number | null``statusDetail: string | null``failure` 字段,不包含 success 字段
#### Scenario: RecentSample 类型
- **WHEN** 前后端共享 `RecentSample` 类型
- **THEN** 该类型 SHALL 包含 `timestamp: string``durationMs: number | null``up: boolean` 字段
- **THEN** 该类型 SHALL 包含 `timestamp: string``durationMs: number | null``up: boolean` 字段,其中 up 为 boolean 且等于 matched
#### Scenario: HistoryResponse 类型
- **WHEN** 前后端共享 `HistoryResponse` 类型
@@ -116,5 +120,5 @@
- **THEN** `/api/targets``/api/targets/:id/history` SHALL 返回该 failure 的 kind、phase、path、expected、actual、message 字段
#### Scenario: 无失败信息
- **WHEN** 检查结果 success=true 且 matched=true
- **WHEN** 检查结果 matched=true
- **THEN** API SHALL 返回 failure 为 null

View File

@@ -56,9 +56,9 @@ Dashboard SHALL 使用固定宽度的卡片配合响应式网格布局。
- **THEN** 卡片第一行 SHALL 展示状态指示圆点UP 绿色 / DOWN 红色、目标名称和类型标签HTTP / Command
#### Scenario: 卡片状态指示圆点
- **WHEN** 目标最近一次拨测 success=true 且 matched=true
- **WHEN** 目标最近一次拨测 matched=true
- **THEN** 卡片状态圆点 SHALL 显示为绿色
- **WHEN** 目标最近一次拨测 success=false 或 matched=false
- **WHEN** 目标最近一次拨测 matched=false
- **THEN** 卡片状态圆点 SHALL 显示为红色
#### Scenario: 卡片状态条可视化

View File

@@ -9,7 +9,7 @@
#### Scenario: 首次启动创建数据库
- **WHEN** 指定的数据目录下不存在数据库文件
- **THEN** 系统 SHALL 创建数据库文件并初始化包含 type、target、config、grp、duration_ms、status_detail、failure 等字段的 targets 和 check_results
- **THEN** 系统 SHALL 创建数据库文件并初始化 targets 表和 check_results 表check_results 表包含 idINTEGER PRIMARY KEY AUTOINCREMENT、target_idINTEGER NOT NULL、timestampTEXT NOT NULL、matchedINTEGER NOT NULL、duration_msREAL、status_detailTEXT、failureTEXT不包含 success
#### Scenario: 数据目录不存在
- **WHEN** 配置的数据目录路径不存在
@@ -35,7 +35,7 @@
#### Scenario: 写入检查结果
- **WHEN** 一次 checker 执行完成
- **THEN** 系统 SHALL 插入一条包含 target_id、timestamp、success、matched、duration_ms、status_detail、failure 的记录
- **THEN** 系统 SHALL 插入一条包含 target_id、timestamp、matched、duration_ms、status_detail、failure 的记录
#### Scenario: 写入结构化失败信息
- **WHEN** checker 执行失败或 expect 不匹配
@@ -53,14 +53,14 @@
#### Scenario: 分组排序查询
- **WHEN** 查询所有 targets
- **THEN** 结果 SHALL 将 "default" 分组目标排在首位,其余分组按分组名称和目标插入顺序排列
- **THEN** 结果 SHALL 将 "default" 分组目标排在首位,其余分组按 YAML 配置中首次出现的顺序(即 id 自增顺序)排列
### Requirement: 结构化采样数据查询
系统 SHALL 提供 `getRecentSamples` 方法替代 `getSparkline`,返回包含状态信息的结构化采样数据。
#### Scenario: 获取最近采样数据
- **WHEN** 调用 `getRecentSamples(targetId, 30)`
- **THEN** 系统 SHALL 返回最多 30 条记录,每条包含 timestamp、duration_ms、success、matched
- **THEN** 系统 SHALL 返回最多 30 条记录,每条包含 timestamp、duration_ms、matched
#### Scenario: 采样数据排序
- **WHEN** 获取采样数据
@@ -89,16 +89,19 @@
#### Scenario: 计算目标可用率
- **WHEN** 查询某目标在指定时间范围内的可用率
- **THEN** 系统 SHALL 返回 UP (success=true AND matched=true) 的记录数占总记录数的百分比
- **THEN** 系统 SHALL 返回 matched=true 的记录数占总记录数的百分比
#### Scenario: 计算目标平均耗时
- **WHEN** 查询某目标在指定时间范围内的平均耗时
- **THEN** 系统 SHALL 返回 duration_ms 的平均值(仅计算 success=true 的记录)
- **THEN** 系统 SHALL 返回 duration_ms 的平均值(仅计算 matched=true 的记录)
#### Scenario: 按小时聚合趋势数据
- **WHEN** 查询某目标在指定时间范围内的趋势数据
- **THEN** 系统 SHALL 返回按小时分组的聚合数据,包括每小时的平均耗时和可用率
#### Scenario: UP/DOWN 判定
- **THEN** 系统 SHALL 基于 latestCheck.matched 判定目标 UP 或 DOWNmatched=true 为 UPmatched=false 为 DOWN
### Requirement: 目标展示摘要持久化
数据存储 SHALL 为每个 target 持久化一个领域无关的展示摘要字段 `target`

View File

@@ -47,7 +47,7 @@
#### Scenario: HTTP body 读取上限
- **WHEN** HTTP response body 超过该 target 的 maxBodyBytes
- **THEN** 系统 MUST 停止读取并记录 `success=false``matched=false` 和结构化输出超限错误
- **THEN** 系统 MUST 停止读取并记录 `matched=false` 和结构化输出超限错误
### Requirement: 请求超时控制
系统 SHALL 对每次 checker 执行实施超时控制,超时时间使用目标配置的 timeout 值。
@@ -115,19 +115,19 @@
- **THEN** 系统 SHALL 判定 matched 为 false并记录 json 规则对应的 failure.path
### Requirement: 拨测结果记录
系统 SHALL 在每次 checker 完成后,将结果写入 SQLite 数据存储,包含 target_id、timestamp、success、matched、duration_ms、status_detail、failure 字段。
系统 SHALL 在每次 checker 完成后,将结果写入 SQLite 数据存储,包含 target_id、timestamp、matched、duration_ms、status_detail、failure 字段。
#### Scenario: 成功检查结果记录
- **WHEN** checker 成功执行且 expect 全部匹配
- **THEN** 系统 SHALL 记录 success=true、matched=true、duration_ms、status_detailfailure 为 null
- **THEN** 系统 SHALL 记录 matched=true、duration_ms、status_detailfailure 为 null
#### Scenario: 执行失败结果记录
- **WHEN** checker 执行失败(网络错误、超时、命令启动失败、输出超限等)
- **THEN** 系统 SHALL 记录 success=false、matched=false、failure.kind="error" 和具体错误信息
- **THEN** 系统 SHALL 记录 matched=false、failure.kind="error" 和具体错误信息
#### Scenario: expect 不匹配结果记录
- **WHEN** checker 执行成功但 expect 不匹配
- **THEN** 系统 SHALL 记录 success=true、matched=false、failure.kind="mismatch" 和具体不匹配信息
- **THEN** 系统 SHALL 记录 matched=false、failure.kind="mismatch" 和具体不匹配信息
### Requirement: runner 选择
系统 SHALL 根据 target.type 选择对应 runner 执行检查。