diff --git a/README.md b/README.md index b69d307..b23085e 100644 --- a/README.md +++ b/README.md @@ -181,7 +181,7 @@ targets: **RecentSample**: `timestamp`、`durationMs`、`up` -**CheckResult**: `timestamp`、`success`、`matched`、`durationMs`、`statusDetail`、`failure` +**CheckResult**: `timestamp`、`matched`、`durationMs`、`statusDetail`、`failure` **CheckFailure**: `kind`(error/mismatch)、`phase`、`path`、`expected`、`actual`、`message` @@ -245,12 +245,13 @@ bun run verify ## 目标状态判定 -两层判定模型,适用于 HTTP 和 Command 两种类型: +单层判定模型,适用于 HTTP 和 Command 两种类型: -- **success**: 拨测是否成功完成(HTTP 收到响应 / Command 正常退出) - **matched**: 是否符合 expect 规则(无 expect 时默认为 true) -- **UP** = success AND matched -- **DOWN** = NOT success OR NOT matched +- **UP** = matched +- **DOWN** = NOT matched + +执行失败(网络错误、超时、进程崩溃)和 expect 不匹配都统一为 `matched=false`,通过 `failure.kind` 区分(`"error"` vs `"mismatch"`)。 ## 已知限制 diff --git a/openspec/changes/archive/2026-05-09-add-vite-react-bun-executable/.openspec.yaml b/openspec/changes/archive/2026-05-09-add-vite-react-bun-executable/.openspec.yaml new file mode 100644 index 0000000..0478d8f --- /dev/null +++ b/openspec/changes/archive/2026-05-09-add-vite-react-bun-executable/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-05-09 diff --git a/openspec/changes/archive/2026-05-09-add-vite-react-bun-executable/design.md b/openspec/changes/archive/2026-05-09-add-vite-react-bun-executable/design.md new file mode 100644 index 0000000..bf4aef5 --- /dev/null +++ b/openspec/changes/archive/2026-05-09-add-vite-react-bun-executable/design.md @@ -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? diff --git a/openspec/changes/archive/2026-05-09-add-vite-react-bun-executable/proposal.md b/openspec/changes/archive/2026-05-09-add-vite-react-bun-executable/proposal.md new file mode 100644 index 0000000..33d7635 --- /dev/null +++ b/openspec/changes/archive/2026-05-09-add-vite-react-bun-executable/proposal.md @@ -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 路径约定、构建产物和运行参数。 diff --git a/openspec/changes/archive/2026-05-09-add-vite-react-bun-executable/specs/frontend-development-workflow/spec.md b/openspec/changes/archive/2026-05-09-add-vite-react-bun-executable/specs/frontend-development-workflow/spec.md new file mode 100644 index 0000000..2213892 --- /dev/null +++ b/openspec/changes/archive/2026-05-09-add-vite-react-bun-executable/specs/frontend-development-workflow/spec.md @@ -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 不要求将后端运行时实现打包进前端 diff --git a/openspec/changes/archive/2026-05-09-add-vite-react-bun-executable/specs/fullstack-app-runtime/spec.md b/openspec/changes/archive/2026-05-09-add-vite-react-bun-executable/specs/fullstack-app-runtime/spec.md new file mode 100644 index 0000000..aa86fe0 --- /dev/null +++ b/openspec/changes/archive/2026-05-09-add-vite-react-bun-executable/specs/fullstack-app-runtime/spec.md @@ -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 文档 diff --git a/openspec/changes/archive/2026-05-09-add-vite-react-bun-executable/specs/single-executable-packaging/spec.md b/openspec/changes/archive/2026-05-09-add-vite-react-bun-executable/specs/single-executable-packaging/spec.md new file mode 100644 index 0000000..e5d993b --- /dev/null +++ b/openspec/changes/archive/2026-05-09-add-vite-react-bun-executable/specs/single-executable-packaging/spec.md @@ -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 使构建或测试命令失败 diff --git a/openspec/changes/archive/2026-05-09-add-vite-react-bun-executable/tasks.md b/openspec/changes/archive/2026-05-09-add-vite-react-bun-executable/tasks.md new file mode 100644 index 0000000..90bae10 --- /dev/null +++ b/openspec/changes/archive/2026-05-09-add-vite-react-bun-executable/tasks.md @@ -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 验证步骤 diff --git a/openspec/changes/archive/2026-05-09-cleanup-project-artifacts/.openspec.yaml b/openspec/changes/archive/2026-05-09-cleanup-project-artifacts/.openspec.yaml new file mode 100644 index 0000000..0478d8f --- /dev/null +++ b/openspec/changes/archive/2026-05-09-cleanup-project-artifacts/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-05-09 diff --git a/openspec/changes/archive/2026-05-09-cleanup-project-artifacts/design.md b/openspec/changes/archive/2026-05-09-cleanup-project-artifacts/design.md new file mode 100644 index 0000000..7617fb2 --- /dev/null +++ b/openspec/changes/archive/2026-05-09-cleanup-project-artifacts/design.md @@ -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/` 保留,只有成功时才清理,排查路径完整。 diff --git a/openspec/changes/archive/2026-05-09-cleanup-project-artifacts/proposal.md b/openspec/changes/archive/2026-05-09-cleanup-project-artifacts/proposal.md new file mode 100644 index 0000000..b2eb99a --- /dev/null +++ b/openspec/changes/archive/2026-05-09-cleanup-project-artifacts/proposal.md @@ -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 或开发工作流 diff --git a/openspec/changes/archive/2026-05-09-cleanup-project-artifacts/specs/single-executable-packaging/spec.md b/openspec/changes/archive/2026-05-09-cleanup-project-artifacts/specs/single-executable-packaging/spec.md new file mode 100644 index 0000000..dc7a07b --- /dev/null +++ b/openspec/changes/archive/2026-05-09-cleanup-project-artifacts/specs/single-executable-packaging/spec.md @@ -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 保留在磁盘上以供排查 diff --git a/openspec/changes/archive/2026-05-09-cleanup-project-artifacts/tasks.md b/openspec/changes/archive/2026-05-09-cleanup-project-artifacts/tasks.md new file mode 100644 index 0000000..8868bee --- /dev/null +++ b/openspec/changes/archive/2026-05-09-cleanup-project-artifacts/tasks.md @@ -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 通过 diff --git a/openspec/changes/archive/2026-05-09-harden-fullstack-packaging-foundation/.openspec.yaml b/openspec/changes/archive/2026-05-09-harden-fullstack-packaging-foundation/.openspec.yaml new file mode 100644 index 0000000..0478d8f --- /dev/null +++ b/openspec/changes/archive/2026-05-09-harden-fullstack-packaging-foundation/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-05-09 diff --git a/openspec/changes/archive/2026-05-09-harden-fullstack-packaging-foundation/design.md b/openspec/changes/archive/2026-05-09-harden-fullstack-packaging-foundation/design.md new file mode 100644 index 0000000..2f37dad --- /dev/null +++ b/openspec/changes/archive/2026-05-09-harden-fullstack-packaging-foundation/design.md @@ -0,0 +1,90 @@ +## Context + +当前项目是 Bun + TypeScript 的前后端一体化 demo:开发期由 Vite React 提供前端 HMR,Bun 提供 `/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` 作为日常反馈通道。 diff --git a/openspec/changes/archive/2026-05-09-harden-fullstack-packaging-foundation/proposal.md b/openspec/changes/archive/2026-05-09-harden-fullstack-packaging-foundation/proposal.md new file mode 100644 index 0000000..cd971ae --- /dev/null +++ b/openspec/changes/archive/2026-05-09-harden-fullstack-packaging-foundation/proposal.md @@ -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`,需要同步开发命令、验证命令、构建流程和边界说明。 diff --git a/openspec/changes/archive/2026-05-09-harden-fullstack-packaging-foundation/specs/code-quality-gates/spec.md b/openspec/changes/archive/2026-05-09-harden-fullstack-packaging-foundation/specs/code-quality-gates/spec.md new file mode 100644 index 0000000..8897d41 --- /dev/null +++ b/openspec/changes/archive/2026-05-09-harden-fullstack-packaging-foundation/specs/code-quality-gates/spec.md @@ -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 以非零状态退出且不能继续声明验证成功 diff --git a/openspec/changes/archive/2026-05-09-harden-fullstack-packaging-foundation/specs/frontend-development-workflow/spec.md b/openspec/changes/archive/2026-05-09-harden-fullstack-packaging-foundation/specs/frontend-development-workflow/spec.md new file mode 100644 index 0000000..52b71de --- /dev/null +++ b/openspec/changes/archive/2026-05-09-harden-fullstack-packaging-foundation/specs/frontend-development-workflow/spec.md @@ -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` 用于提交前或发布前完整验证 diff --git a/openspec/changes/archive/2026-05-09-harden-fullstack-packaging-foundation/specs/fullstack-app-runtime/spec.md b/openspec/changes/archive/2026-05-09-harden-fullstack-packaging-foundation/specs/fullstack-app-runtime/spec.md new file mode 100644 index 0000000..cfda156 --- /dev/null +++ b/openspec/changes/archive/2026-05-09-harden-fullstack-packaging-foundation/specs/fullstack-app-runtime/spec.md @@ -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 diff --git a/openspec/changes/archive/2026-05-09-harden-fullstack-packaging-foundation/specs/single-executable-packaging/spec.md b/openspec/changes/archive/2026-05-09-harden-fullstack-packaging-foundation/specs/single-executable-packaging/spec.md new file mode 100644 index 0000000..a54b606 --- /dev/null +++ b/openspec/changes/archive/2026-05-09-harden-fullstack-packaging-foundation/specs/single-executable-packaging/spec.md @@ -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 使构建或测试命令失败 diff --git a/openspec/changes/archive/2026-05-09-harden-fullstack-packaging-foundation/tasks.md b/openspec/changes/archive/2026-05-09-harden-fullstack-packaging-foundation/tasks.md new file mode 100644 index 0000000..1c5453b --- /dev/null +++ b/openspec/changes/archive/2026-05-09-harden-fullstack-packaging-foundation/tasks.md @@ -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 不再告警 diff --git a/openspec/changes/archive/2026-05-09-http-probe-checker/.openspec.yaml b/openspec/changes/archive/2026-05-09-http-probe-checker/.openspec.yaml new file mode 100644 index 0000000..0478d8f --- /dev/null +++ b/openspec/changes/archive/2026-05-09-http-probe-checker/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-05-09 diff --git a/openspec/changes/archive/2026-05-09-http-probe-checker/design.md b/openspec/changes/archive/2026-05-09-http-probe-checker/design.md new file mode 100644 index 0000000..2fe0e9b --- /dev/null +++ b/openspec/changes/archive/2026-05-09-http-probe-checker/design.md @@ -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. 数据存储:SQLite(bun: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 的目标共享 timer,timer 数量 = 不同 interval 值的数量 +- 组内并发保证批量效率,组间隔离互不影响 + +### 4. 前端更新策略:轮询 + +**选择**:前端每 5-10 秒轮询 `/api/summary` 和 `/api/targets`。 + +**替代方案**: +- SSE 服务端推送 → 实现复杂,拨测间隔 15-60s 级别无必要 +- WebSocket → 更复杂,过度设计 + +**理由**: +- 拨测间隔本身是 15-60s,5s 轮询延迟完全可接受 +- 实现简单,无需维护长连接状态 +- 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 状态]** → 拨测是幂等的(无状态定时任务),重启后立即开始新一轮即可,无需恢复状态 diff --git a/openspec/changes/archive/2026-05-09-http-probe-checker/proposal.md b/openspec/changes/archive/2026-05-09-http-probe-checker/proposal.md new file mode 100644 index 0000000..a2c2624 --- /dev/null +++ b/openspec/changes/archive/2026-05-09-http-probe-checker/proposal.md @@ -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` diff --git a/openspec/changes/archive/2026-05-09-http-probe-checker/specs/frontend-development-workflow/spec.md b/openspec/changes/archive/2026-05-09-http-probe-checker/specs/frontend-development-workflow/spec.md new file mode 100644 index 0000000..0f3ff1b --- /dev/null +++ b/openspec/changes/archive/2026-05-09-http-probe-checker/specs/frontend-development-workflow/spec.md @@ -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 将该请求作为前端应用流量处理,而不是转发到后端 diff --git a/openspec/changes/archive/2026-05-09-http-probe-checker/specs/fullstack-app-runtime/spec.md b/openspec/changes/archive/2026-05-09-http-probe-checker/specs/fullstack-app-runtime/spec.md new file mode 100644 index 0000000..93ff843 --- /dev/null +++ b/openspec/changes/archive/2026-05-09-http-probe-checker/specs/fullstack-app-runtime/spec.md @@ -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 diff --git a/openspec/changes/archive/2026-05-09-http-probe-checker/specs/probe-api/spec.md b/openspec/changes/archive/2026-05-09-http-probe-checker/specs/probe-api/spec.md new file mode 100644 index 0000000..00bdd18 --- /dev/null +++ b/openspec/changes/archive/2026-05-09-http-probe-checker/specs/probe-api/spec.md @@ -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 为 null,stats 中 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 状态码和错误信息 diff --git a/openspec/changes/archive/2026-05-09-http-probe-checker/specs/probe-config/spec.md b/openspec/changes/archive/2026-05-09-http-probe-checker/specs/probe-config/spec.md new file mode 100644 index 0000000..c60550d --- /dev/null +++ b/openspec/changes/archive/2026-05-09-http-probe-checker/specs/probe-config/spec.md @@ -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()` 将内容解析为配置对象 diff --git a/openspec/changes/archive/2026-05-09-http-probe-checker/specs/probe-dashboard/spec.md b/openspec/changes/archive/2026-05-09-http-probe-checker/specs/probe-dashboard/spec.md new file mode 100644 index 0000000..79ef566 --- /dev/null +++ b/openspec/changes/archive/2026-05-09-http-probe-checker/specs/probe-dashboard/spec.md @@ -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 显示错误提示,并在下一次轮询周期自动重试 diff --git a/openspec/changes/archive/2026-05-09-http-probe-checker/specs/probe-data-store/spec.md b/openspec/changes/archive/2026-05-09-http-probe-checker/specs/probe-data-store/spec.md new file mode 100644 index 0000000..8f0e55c --- /dev/null +++ b/openspec/changes/archive/2026-05-09-http-probe-checker/specs/probe-data-store/spec.md @@ -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 返回按小时分组的聚合数据,包括每小时的平均延迟和可用率 diff --git a/openspec/changes/archive/2026-05-09-http-probe-checker/specs/probe-engine/spec.md b/openspec/changes/archive/2026-05-09-http-probe-checker/specs/probe-engine/spec.md new file mode 100644 index 0000000..b9def1b --- /dev/null +++ b/openspec/changes/archive/2026-05-09-http-probe-checker/specs/probe-engine/spec.md @@ -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 interval,target 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 diff --git a/openspec/changes/archive/2026-05-09-http-probe-checker/tasks.md b/openspec/changes/archive/2026-05-09-http-probe-checker/tasks.md new file mode 100644 index 0000000..927bebf --- /dev/null +++ b/openspec/changes/archive/2026-05-09-http-probe-checker/tasks.md @@ -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.ts:CLI 只接受配置文件路径参数,从 YAML 读取 host/port/dataDir +- [x] 2.4 为配置解析和校验编写完整测试 + +## 3. 数据存储层 + +- [x] 3.1 实现 store.ts:SQLite 初始化(建表、WAL 模式、复合索引)、数据目录自动创建 +- [x] 3.2 实现 targets 表同步逻辑(根据 name 匹配:新增插入、删除移除、修改更新) +- [x] 3.3 实现 check_results 追加写入方法 +- [x] 3.4 实现查询方法:按 target+时间范围查询、按小时聚合趋势、计算可用率/平均延迟/P99 +- [x] 3.5 为数据存储层编写完整测试(初始化、同步、写入、查询、聚合) + +## 4. 拨测引擎 + +- [x] 4.1 实现 fetcher.ts:HTTP 请求执行(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 实现 hooks:useSummary(轮询 /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.css:Dashboard 布局样式(卡片、表格、详情面板、响应式) +- [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 展示 diff --git a/openspec/changes/archive/2026-05-10-abstract-target-expect-checkers/.openspec.yaml b/openspec/changes/archive/2026-05-10-abstract-target-expect-checkers/.openspec.yaml new file mode 100644 index 0000000..0478d8f --- /dev/null +++ b/openspec/changes/archive/2026-05-10-abstract-target-expect-checkers/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-05-09 diff --git a/openspec/changes/archive/2026-05-10-abstract-target-expect-checkers/design.md b/openspec/changes/archive/2026-05-10-abstract-target-expect-checkers/design.md new file mode 100644 index 0000000..cdbe0cb --- /dev/null +++ b/openspec/changes/archive/2026-05-10-abstract-target-expect-checkers/design.md @@ -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 code,command 详情可显示 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`。 diff --git a/openspec/changes/archive/2026-05-10-abstract-target-expect-checkers/proposal.md b/openspec/changes/archive/2026-05-10-abstract-target-expect-checkers/proposal.md new file mode 100644 index 0000000..804cfc0 --- /dev/null +++ b/openspec/changes/archive/2026-05-10-abstract-target-expect-checkers/proposal.md @@ -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 能力。 diff --git a/openspec/changes/archive/2026-05-10-abstract-target-expect-checkers/specs/command-checker/spec.md b/openspec/changes/archive/2026-05-10-abstract-target-expect-checkers/specs/command-checker/spec.md new file mode 100644 index 0000000..79935a5 --- /dev/null +++ b/openspec/changes/archive/2026-05-10-abstract-target-expect-checkers/specs/command-checker/spec.md @@ -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 规则 diff --git a/openspec/changes/archive/2026-05-10-abstract-target-expect-checkers/specs/expect-body-checkers/spec.md b/openspec/changes/archive/2026-05-10-abstract-target-expect-checkers/specs/expect-body-checkers/spec.md new file mode 100644 index 0000000..f62a858 --- /dev/null +++ b/openspec/changes/archive/2026-05-10-abstract-target-expect-checkers/specs/expect-body-checkers/spec.md @@ -0,0 +1,126 @@ +## MODIFIED Requirements + +### Requirement: 响应体多种校验方法 +系统 SHALL 支持对 HTTP 响应体进行五种可组合的校验方法:contains(子串)、regex(正则)、json(JSONPath)、css(CSS 选择器)、xpath(XPath)。这些方法 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 摘要,而不是持久化完整响应体或命令输出 diff --git a/openspec/changes/archive/2026-05-10-abstract-target-expect-checkers/specs/probe-api/spec.md b/openspec/changes/archive/2026-05-10-abstract-target-expect-checkers/specs/probe-api/spec.md new file mode 100644 index 0000000..9854938 --- /dev/null +++ b/openspec/changes/archive/2026-05-10-abstract-target-expect-checkers/specs/probe-api/spec.md @@ -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 为 null,stats 中 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 diff --git a/openspec/changes/archive/2026-05-10-abstract-target-expect-checkers/specs/probe-config/spec.md b/openspec/changes/archive/2026-05-10-abstract-target-expect-checkers/specs/probe-config/spec.md new file mode 100644 index 0000000..ea1d1a6 --- /dev/null +++ b/openspec/changes/archive/2026-05-10-abstract-target-expect-checkers/specs/probe-config/spec.md @@ -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 diff --git a/openspec/changes/archive/2026-05-10-abstract-target-expect-checkers/specs/probe-dashboard/spec.md b/openspec/changes/archive/2026-05-10-abstract-target-expect-checkers/specs/probe-dashboard/spec.md new file mode 100644 index 0000000..a7e0604 --- /dev/null +++ b/openspec/changes/archive/2026-05-10-abstract-target-expect-checkers/specs/probe-dashboard/spec.md @@ -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,并将目标摘要显示为命令摘要 diff --git a/openspec/changes/archive/2026-05-10-abstract-target-expect-checkers/specs/probe-data-store/spec.md b/openspec/changes/archive/2026-05-10-abstract-target-expect-checkers/specs/probe-data-store/spec.md new file mode 100644 index 0000000..055991f --- /dev/null +++ b/openspec/changes/archive/2026-05-10-abstract-target-expect-checkers/specs/probe-data-store/spec.md @@ -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 diff --git a/openspec/changes/archive/2026-05-10-abstract-target-expect-checkers/specs/probe-engine/spec.md b/openspec/changes/archive/2026-05-10-abstract-target-expect-checkers/specs/probe-engine/spec.md new file mode 100644 index 0000000..3308e24 --- /dev/null +++ b/openspec/changes/archive/2026-05-10-abstract-target-expect-checkers/specs/probe-engine/spec.md @@ -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_detail,failure 为 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 执行该目标 diff --git a/openspec/changes/archive/2026-05-10-abstract-target-expect-checkers/tasks.md b/openspec/changes/archive/2026-05-10-abstract-target-expect-checkers/tasks.md new file mode 100644 index 0000000..095785c --- /dev/null +++ b/openspec/changes/archive/2026-05-10-abstract-target-expect-checkers/tasks.md @@ -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、测试和示例同步更新 diff --git a/openspec/changes/archive/2026-05-10-enhance-expect-rules/.openspec.yaml b/openspec/changes/archive/2026-05-10-enhance-expect-rules/.openspec.yaml new file mode 100644 index 0000000..0478d8f --- /dev/null +++ b/openspec/changes/archive/2026-05-10-enhance-expect-rules/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-05-09 diff --git a/openspec/changes/archive/2026-05-10-enhance-expect-rules/design.md b/openspec/changes/archive/2026-05-10-enhance-expect-rules/design.md new file mode 100644 index 0000000..da7d0ff --- /dev/null +++ b/openspec/changes/archive/2026-05-10-enhance-expect-rules/design.md @@ -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.` 而非平铺 `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 配置存在时解析 HTML(cheerio) + 5. xpath → 仅当 xpath 配置存在时解析 HTML/XML(xmldom) + +解析失败(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 会打包进二进制,对最终产物影响有限 diff --git a/openspec/changes/archive/2026-05-10-enhance-expect-rules/proposal.md b/openspec/changes/archive/2026-05-10-enhance-expect-rules/proposal.md new file mode 100644 index 0000000..4fa7e93 --- /dev/null +++ b/openspec/changes/archive/2026-05-10-enhance-expect-rules/proposal.md @@ -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 diff --git a/openspec/changes/archive/2026-05-10-enhance-expect-rules/specs/expect-body-checkers/spec.md b/openspec/changes/archive/2026-05-10-enhance-expect-rules/specs/expect-body-checkers/spec.md new file mode 100644 index 0000000..662647b --- /dev/null +++ b/openspec/changes/archive/2026-05-10-enhance-expect-rules/specs/expect-body-checkers/spec.md @@ -0,0 +1,113 @@ +## ADDED Requirements + +### Requirement: 响应体多种校验方法 +系统 SHALL 支持对 HTTP 响应体进行五种可组合的校验方法:contains(子串)、regex(正则)、json(JSONPath)、css(CSS 选择器)、xpath(XPath),配置在 `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 diff --git a/openspec/changes/archive/2026-05-10-enhance-expect-rules/specs/probe-config/spec.md b/openspec/changes/archive/2026-05-10-enhance-expect-rules/specs/probe-config/spec.md new file mode 100644 index 0000000..adb22d7 --- /dev/null +++ b/openspec/changes/archive/2026-05-10-enhance-expect-rules/specs/probe-config/spec.md @@ -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"` diff --git a/openspec/changes/archive/2026-05-10-enhance-expect-rules/specs/probe-engine/spec.md b/openspec/changes/archive/2026-05-10-enhance-expect-rules/specs/probe-engine/spec.md new file mode 100644 index 0000000..37d7f92 --- /dev/null +++ b/openspec/changes/archive/2026-05-10-enhance-expect-rules/specs/probe-engine/spec.md @@ -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 diff --git a/openspec/changes/archive/2026-05-10-enhance-expect-rules/tasks.md b/openspec/changes/archive/2026-05-10-enhance-expect-rules/tasks.md new file mode 100644 index 0000000..62444a2 --- /dev/null +++ b/openspec/changes/archive/2026-05-10-enhance-expect-rules/tasks.md @@ -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 实现操作符比较函数 applyOperator(equals/contains/match/empty/exists/gte/lte/gt/lt) +- [x] 3.3 实现 checkExpectValue 函数:标量 → equals,对象 → 遍历操作符 +- [x] 3.4 实现 checkBodyContains:body.includes 包装 +- [x] 3.5 实现 checkBodyRegex:new RegExp().test 包装 +- [x] 3.6 实现 checkBodyJson:JSON.parse + evaluateJsonPath + applyOperator +- [x] 3.7 实现 checkBodyCss:cheerio.load + 选择器查询 + text/attr 提取 + applyOperator +- [x] 3.8 实现 checkBodyXpath:xmldom 解析 + 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 依赖列表(如有需要) diff --git a/openspec/changes/archive/2026-05-11-card-ui-refactor/.openspec.yaml b/openspec/changes/archive/2026-05-11-card-ui-refactor/.openspec.yaml new file mode 100644 index 0000000..ac20efa --- /dev/null +++ b/openspec/changes/archive/2026-05-11-card-ui-refactor/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-05-10 diff --git a/openspec/changes/archive/2026-05-11-card-ui-refactor/design.md b/openspec/changes/archive/2026-05-11-card-ui-refactor/design.md new file mode 100644 index 0000000..14e06eb --- /dev/null +++ b/openspec/changes/archive/2026-05-11-card-ui-refactor/design.md @@ -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 覆盖足够长的最近检查周期 diff --git a/openspec/changes/archive/2026-05-11-card-ui-refactor/proposal.md b/openspec/changes/archive/2026-05-11-card-ui-refactor/proposal.md new file mode 100644 index 0000000..8c36869 --- /dev/null +++ b/openspec/changes/archive/2026-05-11-card-ui-refactor/proposal.md @@ -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 移除 avgDurationMs;targets 返回 group 和 recentSamples 替代 sparkline;trend 改用 from/to 参数替代 hours;history 改用 from/to + page/pageSize 并返回带分页信息的结构 +- `probe-data-store`: targets 表新增 grp 列存储分组信息;新增 getRecentSamples 方法替代 getSparkline;trend/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 库 diff --git a/openspec/changes/archive/2026-05-11-card-ui-refactor/specs/card-dashboard/spec.md b/openspec/changes/archive/2026-05-11-card-ui-refactor/specs/card-dashboard/spec.md new file mode 100644 index 0000000..75b54ed --- /dev/null +++ b/openspec/changes/archive/2026-05-11-card-ui-refactor/specs/card-dashboard/spec.md @@ -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 显示绿色(#1fbf75),DOWN 显示红色(#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 秒自动刷新数据 diff --git a/openspec/changes/archive/2026-05-11-card-ui-refactor/specs/probe-api/spec.md b/openspec/changes/archive/2026-05-11-card-ui-refactor/specs/probe-api/spec.md new file mode 100644 index 0000000..a16bce8 --- /dev/null +++ b/openspec/changes/archive/2026-05-11-card-ui-refactor/specs/probe-api/spec.md @@ -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`(boolean,success && 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 为 null,recentSamples 为空数组 + +### 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 diff --git a/openspec/changes/archive/2026-05-11-card-ui-refactor/specs/probe-config/spec.md b/openspec/changes/archive/2026-05-11-card-ui-refactor/specs/probe-config/spec.md new file mode 100644 index 0000000..4238a0f --- /dev/null +++ b/openspec/changes/archive/2026-05-11-card-ui-refactor/specs/probe-config/spec.md @@ -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 中对应字段影响 diff --git a/openspec/changes/archive/2026-05-11-card-ui-refactor/specs/probe-dashboard/spec.md b/openspec/changes/archive/2026-05-11-card-ui-refactor/specs/probe-dashboard/spec.md new file mode 100644 index 0000000..c28c1de --- /dev/null +++ b/openspec/changes/archive/2026-05-11-card-ui-refactor/specs/probe-dashboard/spec.md @@ -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中展示 diff --git a/openspec/changes/archive/2026-05-11-card-ui-refactor/specs/probe-data-store/spec.md b/openspec/changes/archive/2026-05-11-card-ui-refactor/specs/probe-data-store/spec.md new file mode 100644 index 0000000..e3cb418 --- /dev/null +++ b/openspec/changes/archive/2026-05-11-card-ui-refactor/specs/probe-data-store/spec.md @@ -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,新方法返回更丰富的结构化数据 diff --git a/openspec/changes/archive/2026-05-11-card-ui-refactor/specs/target-detail-modal/spec.md b/openspec/changes/archive/2026-05-11-card-ui-refactor/specs/target-detail-modal/spec.md new file mode 100644 index 0000000..a490131 --- /dev/null +++ b/openspec/changes/archive/2026-05-11-card-ui-refactor/specs/target-detail-modal/spec.md @@ -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 分为上下两部分,上方展示统计图表,下方展示检查结果列表和分页器 diff --git a/openspec/changes/archive/2026-05-11-card-ui-refactor/specs/target-grouping/spec.md b/openspec/changes/archive/2026-05-11-card-ui-refactor/specs/target-grouping/spec.md new file mode 100644 index 0000000..37a5653 --- /dev/null +++ b/openspec/changes/archive/2026-05-11-card-ui-refactor/specs/target-grouping/spec.md @@ -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"` diff --git a/openspec/changes/archive/2026-05-11-card-ui-refactor/tasks.md b/openspec/changes/archive/2026-05-11-card-ui-refactor/tasks.md new file mode 100644 index 0000000..0f2b851 --- /dev/null +++ b/openspec/changes/archive/2026-05-11-card-ui-refactor/tasks.md @@ -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.p99DurationMs,TargetStatus 中 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 + MiniSparkline,hover 上浮效果,点击触发回调 +- [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.tsx:SummaryCards 从 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:配置说明新增 group,API 端点变更说明,项目结构更新组件列表 + +## 8. 质量保障 + +- [x] 8.1 执行 bun run check(typecheck + lint + format:check + 单元测试),修复所有问题 +- [x] 8.2 执行 bun run verify(check + build + smoke test),确保构建产物正常运行 diff --git a/openspec/changes/archive/2026-05-11-simplify-judgment-model/.openspec.yaml b/openspec/changes/archive/2026-05-11-simplify-judgment-model/.openspec.yaml new file mode 100644 index 0000000..81cd71f --- /dev/null +++ b/openspec/changes/archive/2026-05-11-simplify-judgment-model/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-05-11 diff --git a/openspec/changes/archive/2026-05-11-simplify-judgment-model/design.md b/openspec/changes/archive/2026-05-11-simplify-judgment-model/design.md new file mode 100644 index 0000000..9fe0dc4 --- /dev/null +++ b/openspec/changes/archive/2026-05-11-simplify-judgment-model/design.md @@ -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` 全量验证 diff --git a/openspec/changes/archive/2026-05-11-simplify-judgment-model/proposal.md b/openspec/changes/archive/2026-05-11-simplify-judgment-model/proposal.md new file mode 100644 index 0000000..621b1fb --- /dev/null +++ b/openspec/changes/archive/2026-05-11-simplify-judgment-model/proposal.md @@ -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**: 目标状态判定模型说明需更新 diff --git a/openspec/changes/archive/2026-05-11-simplify-judgment-model/specs/card-dashboard/spec.md b/openspec/changes/archive/2026-05-11-simplify-judgment-model/specs/card-dashboard/spec.md new file mode 100644 index 0000000..c28dbe2 --- /dev/null +++ b/openspec/changes/archive/2026-05-11-simplify-judgment-model/specs/card-dashboard/spec.md @@ -0,0 +1,12 @@ +## MODIFIED Requirements + +### Requirement: 卡片状态展示 +系统 SHALL 在卡片上展示目标的 UP/DOWN 状态。 + +#### Scenario: 卡片 UP 状态 +- **WHEN** 目标最近一次拨测 matched=true +- **THEN** 系统 SHALL 显示绿色状态点 + +#### Scenario: 卡片 DOWN 状态 +- **WHEN** 目标最近一次拨测 matched=false +- **THEN** 系统 SHALL 显示红色状态点 diff --git a/openspec/changes/archive/2026-05-11-simplify-judgment-model/specs/command-checker/spec.md b/openspec/changes/archive/2026-05-11-simplify-judgment-model/specs/command-checker/spec.md new file mode 100644 index 0000000..01155b5 --- /dev/null +++ b/openspec/changes/archive/2026-05-11-simplify-judgment-model/specs/command-checker/spec.md @@ -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 中写入输出超限信息 diff --git a/openspec/changes/archive/2026-05-11-simplify-judgment-model/specs/probe-api/spec.md b/openspec/changes/archive/2026-05-11-simplify-judgment-model/specs/probe-api/spec.md new file mode 100644 index 0000000..3c4b192 --- /dev/null +++ b/openspec/changes/archive/2026-05-11-simplify-judgment-model/specs/probe-api/spec.md @@ -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 diff --git a/openspec/changes/archive/2026-05-11-simplify-judgment-model/specs/probe-dashboard/spec.md b/openspec/changes/archive/2026-05-11-simplify-judgment-model/specs/probe-dashboard/spec.md new file mode 100644 index 0000000..61f6546 --- /dev/null +++ b/openspec/changes/archive/2026-05-11-simplify-judgment-model/specs/probe-dashboard/spec.md @@ -0,0 +1,12 @@ +## MODIFIED Requirements + +### Requirement: 状态判定与展示 +系统 SHALL 根据最近一次拨测结果展示目标状态。 + +#### Scenario: 目标 UP 状态 +- **WHEN** 目标最近一次拨测 matched=true +- **THEN** 系统 SHALL 显示绿色 UP 状态 + +#### Scenario: 目标 DOWN 状态 +- **WHEN** 目标最近一次拨测 matched=false +- **THEN** 系统 SHALL 显示红色 DOWN 状态 diff --git a/openspec/changes/archive/2026-05-11-simplify-judgment-model/specs/probe-data-store/spec.md b/openspec/changes/archive/2026-05-11-simplify-judgment-model/specs/probe-data-store/spec.md new file mode 100644 index 0000000..202c8a0 --- /dev/null +++ b/openspec/changes/archive/2026-05-11-simplify-judgment-model/specs/probe-data-store/spec.md @@ -0,0 +1,41 @@ +## MODIFIED Requirements + +### Requirement: 数据库表结构 +系统 SHALL 使用 SQLite 存储 targets 和 check_results 两张表。 + +#### Scenario: check_results 表结构 +- **THEN** check_results 表 SHALL 包含 id(INTEGER PRIMARY KEY AUTOINCREMENT)、target_id(INTEGER NOT NULL)、timestamp(TEXT NOT NULL)、matched(INTEGER NOT NULL)、duration_ms(REAL)、status_detail(TEXT)、failure(TEXT)列,不包含 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 或 DOWN:matched=true 为 UP,matched=false 为 DOWN diff --git a/openspec/changes/archive/2026-05-11-simplify-judgment-model/specs/probe-engine/spec.md b/openspec/changes/archive/2026-05-11-simplify-judgment-model/specs/probe-engine/spec.md new file mode 100644 index 0000000..4e81750 --- /dev/null +++ b/openspec/changes/archive/2026-05-11-simplify-judgment-model/specs/probe-engine/spec.md @@ -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_detail,failure 为 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 和结构化输出超限错误 diff --git a/openspec/changes/archive/2026-05-11-simplify-judgment-model/specs/target-grouping/spec.md b/openspec/changes/archive/2026-05-11-simplify-judgment-model/specs/target-grouping/spec.md new file mode 100644 index 0000000..433099d --- /dev/null +++ b/openspec/changes/archive/2026-05-11-simplify-judgment-model/specs/target-grouping/spec.md @@ -0,0 +1,8 @@ +## MODIFIED Requirements + +### Requirement: 分组排序 +系统 SHALL 对非 default 分组按 YAML 配置中的首次出现顺序排列。 + +#### Scenario: 非默认分组排序 +- **WHEN** 查询目标列表 +- **THEN** 非 default 分组 SHALL 按 id 自增顺序排列(即 YAML 配置中的首次出现顺序),而非字母序 diff --git a/openspec/changes/archive/2026-05-11-simplify-judgment-model/tasks.md b/openspec/changes/archive/2026-05-11-simplify-judgment-model/tasks.md new file mode 100644 index 0000000..eebea16 --- /dev/null +++ b/openspec/changes/archive/2026-05-11-simplify-judgment-model/tasks.md @@ -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 返回类型移除 success,SELECT 移除 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 字段描述 diff --git a/openspec/specs/card-dashboard/spec.md b/openspec/specs/card-dashboard/spec.md index 1e7b0e2..29f2e70 100644 --- a/openspec/specs/card-dashboard/spec.md +++ b/openspec/specs/card-dashboard/spec.md @@ -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: 卡片状态条可视化 diff --git a/openspec/specs/command-checker/spec.md b/openspec/specs/command-checker/spec.md index 54d6a86..a1a386e 100644 --- a/openspec/specs/command-checker/spec.md +++ b/openspec/specs/command-checker/spec.md @@ -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 的阶段顺序快速失败。 diff --git a/openspec/specs/probe-api/spec.md b/openspec/specs/probe-api/spec.md index 8005be7..1ee55a4 100644 --- a/openspec/specs/probe-api/spec.md +++ b/openspec/specs/probe-api/spec.md @@ -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`(boolean,success && matched) +- **THEN** 响应中每个目标 SHALL 包含 `recentSamples` 数组,每个元素包含 `timestamp`(ISO 8601)、`durationMs`(number | null)、`up`(boolean,matched === 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 diff --git a/openspec/specs/probe-dashboard/spec.md b/openspec/specs/probe-dashboard/spec.md index cb1dd89..e32b039 100644 --- a/openspec/specs/probe-dashboard/spec.md +++ b/openspec/specs/probe-dashboard/spec.md @@ -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: 卡片状态条可视化 diff --git a/openspec/specs/probe-data-store/spec.md b/openspec/specs/probe-data-store/spec.md index b057e39..8a5f7b0 100644 --- a/openspec/specs/probe-data-store/spec.md +++ b/openspec/specs/probe-data-store/spec.md @@ -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 表包含 id(INTEGER PRIMARY KEY AUTOINCREMENT)、target_id(INTEGER NOT NULL)、timestamp(TEXT NOT NULL)、matched(INTEGER NOT NULL)、duration_ms(REAL)、status_detail(TEXT)、failure(TEXT),不包含 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 或 DOWN:matched=true 为 UP,matched=false 为 DOWN + ### Requirement: 目标展示摘要持久化 数据存储 SHALL 为每个 target 持久化一个领域无关的展示摘要字段 `target`。 diff --git a/openspec/specs/probe-engine/spec.md b/openspec/specs/probe-engine/spec.md index cf9ff58..a191c10 100644 --- a/openspec/specs/probe-engine/spec.md +++ b/openspec/specs/probe-engine/spec.md @@ -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_detail,failure 为 null +- **THEN** 系统 SHALL 记录 matched=true、duration_ms、status_detail,failure 为 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 执行检查。 diff --git a/src/server/app.ts b/src/server/app.ts index 4a61236..385f3c3 100644 --- a/src/server/app.ts +++ b/src/server/app.ts @@ -205,7 +205,7 @@ function createTargetsResponse(store: ProbeStore): TargetStatus[] { recentSamples: recentSamples.map((s) => ({ timestamp: s.timestamp, durationMs: s.duration_ms, - up: s.success === 1 && s.matched === 1, + up: s.matched === 1, })), stats: { totalChecks: stats.totalChecks, @@ -223,7 +223,6 @@ function mapCheckResult(row: StoredCheckResult): CheckResult { return { timestamp: row.timestamp, - success: row.success === 1, matched: row.matched === 1, durationMs: row.duration_ms, statusDetail: row.status_detail, diff --git a/src/server/checker/command-runner.ts b/src/server/checker/command-runner.ts index a680504..18f4e93 100644 --- a/src/server/checker/command-runner.ts +++ b/src/server/checker/command-runner.ts @@ -61,6 +61,7 @@ export async function runCommandCheck(target: ResolvedCommandTarget): Promise 0 THEN (SUM(CASE WHEN matched = 1 THEN 1 ELSE 0 END) * 100.0 / COUNT(*)) ELSE 0 END as availability, COUNT(*) as totalChecks FROM check_results @@ -222,7 +219,7 @@ export class ProbeStore { const latest = this.getLatestCheck(target.id); if (latest) { - if (latest.success && latest.matched) { + if (latest.matched) { up++; } else { down++; @@ -247,15 +244,14 @@ export class ProbeStore { getRecentSamples( targetId: number, limit: number, - ): Array<{ timestamp: string; duration_ms: number | null; success: number; matched: number }> { + ): Array<{ timestamp: string; duration_ms: number | null; matched: number }> { return this.db .prepare( - "SELECT timestamp, duration_ms, success, matched FROM check_results WHERE target_id = ? ORDER BY timestamp DESC LIMIT ?", + "SELECT timestamp, duration_ms, matched FROM check_results WHERE target_id = ? ORDER BY timestamp DESC LIMIT ?", ) .all(targetId, limit) as Array<{ timestamp: string; duration_ms: number | null; - success: number; matched: number; }>; } diff --git a/src/server/checker/types.ts b/src/server/checker/types.ts index 1d460fc..c9e8b68 100644 --- a/src/server/checker/types.ts +++ b/src/server/checker/types.ts @@ -159,7 +159,6 @@ export interface CheckFailure { export interface CheckResult { targetName: string; timestamp: string; - success: boolean; matched: boolean; durationMs: number | null; statusDetail: string | null; @@ -182,7 +181,6 @@ export interface StoredCheckResult { id: number; target_id: number; timestamp: string; - success: number; matched: number; duration_ms: number | null; status_detail: string | null; diff --git a/src/shared/api.ts b/src/shared/api.ts index 3a79853..59fa957 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -50,7 +50,6 @@ export interface HistoryResponse { export interface CheckResult { timestamp: string; - success: boolean; matched: boolean; durationMs: number | null; statusDetail: string | null; diff --git a/src/web/components/TargetCard.tsx b/src/web/components/TargetCard.tsx index 46bd298..c33fea7 100644 --- a/src/web/components/TargetCard.tsx +++ b/src/web/components/TargetCard.tsx @@ -9,7 +9,7 @@ interface TargetCardProps { } export function TargetCard({ target, onClick }: TargetCardProps) { - const isUp = target.latestCheck?.success && target.latestCheck?.matched; + const isUp = target.latestCheck?.matched; return (
diff --git a/src/web/components/TargetDetailModal.tsx b/src/web/components/TargetDetailModal.tsx index 0e05254..2a87886 100644 --- a/src/web/components/TargetDetailModal.tsx +++ b/src/web/components/TargetDetailModal.tsx @@ -37,7 +37,7 @@ export function TargetDetailModal({ onPageChange, onClose, }: TargetDetailModalProps) { - const isUp = target.latestCheck?.success && target.latestCheck?.matched; + const isUp = target.latestCheck?.matched; const totalChecks = trendData.reduce((sum, p) => sum + p.totalChecks, 0); const upChecks = trendData.reduce((sum, p) => sum + Math.round((p.availability / 100) * p.totalChecks), 0); @@ -89,8 +89,8 @@ export function TargetDetailModal({ {historyData.items.map((item, idx) => ( - - {item.success && item.matched ? "UP" : "DOWN"} + + {item.matched ? "UP" : "DOWN"} {new Date(item.timestamp).toLocaleString("zh-CN")} diff --git a/src/web/components/TargetGroup.tsx b/src/web/components/TargetGroup.tsx index 1b1119a..6656dbf 100644 --- a/src/web/components/TargetGroup.tsx +++ b/src/web/components/TargetGroup.tsx @@ -9,7 +9,7 @@ interface TargetGroupProps { } export function TargetGroup({ name, targets, onTargetClick }: TargetGroupProps) { - const up = targets.filter((t) => t.latestCheck?.success && t.latestCheck?.matched).length; + const up = targets.filter((t) => t.latestCheck?.matched).length; const down = targets.length - up; return ( diff --git a/tests/server/app.test.ts b/tests/server/app.test.ts index 53d1c20..46e93a3 100644 --- a/tests/server/app.test.ts +++ b/tests/server/app.test.ts @@ -58,7 +58,6 @@ describe("API 路由", () => { store.insertCheckResult({ targetId: targets[0]!.id, timestamp: "2025-01-01T00:00:00.000Z", - success: true, matched: true, durationMs: 150, statusDetail: "200 OK", @@ -67,7 +66,6 @@ describe("API 路由", () => { store.insertCheckResult({ targetId: targets[0]!.id, timestamp: "2025-01-01T00:00:30.000Z", - success: false, matched: false, durationMs: null, statusDetail: null, @@ -121,7 +119,6 @@ describe("API 路由", () => { expect(tA.target).toBe("http://a.com"); expect(tA.group).toBe("default"); expect(tA.latestCheck).not.toBeNull(); - expect(tA.latestCheck!.success).toBe(false); expect(tA.latestCheck!.matched).toBe(false); expect(tA.latestCheck!.failure).not.toBeNull(); expect(tA.recentSamples).toBeDefined(); diff --git a/tests/server/checker/command-runner.test.ts b/tests/server/checker/command-runner.test.ts index 2577519..a4a0762 100644 --- a/tests/server/checker/command-runner.test.ts +++ b/tests/server/checker/command-runner.test.ts @@ -27,7 +27,6 @@ function makeTarget( describe("runCommandCheck", () => { test("exitCode=0 成功", async () => { const result = await runCommandCheck(makeTarget({ exec: "true", args: [] })); - expect(result.success).toBe(true); expect(result.matched).toBe(true); expect(result.statusDetail).toBe("exitCode=0"); expect(result.failure).toBeNull(); @@ -35,7 +34,6 @@ describe("runCommandCheck", () => { test("exitCode=1 不匹配默认 [0]", async () => { const result = await runCommandCheck(makeTarget({ exec: "false", args: [] })); - expect(result.success).toBe(false); expect(result.matched).toBe(false); expect(result.statusDetail).toBe("exitCode=1"); expect(result.failure).not.toBeNull(); @@ -44,14 +42,13 @@ describe("runCommandCheck", () => { test("exitCode=1 匹配自定义 [1]", async () => { const result = await runCommandCheck(makeTarget({ exec: "false", args: [] }, { expect: { exitCode: [1] } })); - expect(result.success).toBe(true); expect(result.matched).toBe(true); expect(result.statusDetail).toBe("exitCode=1"); }); test("命令不存在返回 spawn 错误", async () => { const result = await runCommandCheck(makeTarget({ exec: "/nonexistent/command/xyz" })); - expect(result.success).toBe(false); + expect(result.matched).toBe(false); expect(result.failure).not.toBeNull(); expect(result.failure!.phase).toBe("exitCode"); expect(result.failure!.message).toBeTruthy(); @@ -59,21 +56,20 @@ describe("runCommandCheck", () => { test("超时返回错误", async () => { const result = await runCommandCheck(makeTarget({ exec: "sleep", args: ["10"] }, { timeoutMs: 100 })); - expect(result.success).toBe(false); + expect(result.matched).toBe(false); expect(result.failure).not.toBeNull(); expect(result.failure!.message).toContain("超时"); }); test("stdout 输出捕获", async () => { const result = await runCommandCheck(makeTarget({ exec: "echo", args: ["hello world"] })); - expect(result.success).toBe(true); + expect(result.matched).toBe(true); }); test("stdout 匹配 expect", async () => { const result = await runCommandCheck( makeTarget({ exec: "echo", args: ["hello"] }, { expect: { stdout: [{ contains: "hello" }] } }), ); - expect(result.success).toBe(true); expect(result.matched).toBe(true); }); @@ -81,7 +77,7 @@ describe("runCommandCheck", () => { const result = await runCommandCheck( makeTarget({ exec: "echo", args: ["hello"] }, { expect: { stdout: [{ contains: "nonexistent" }] } }), ); - expect(result.success).toBe(false); + expect(result.matched).toBe(false); expect(result.failure!.phase).toBe("stdout"); }); @@ -89,7 +85,6 @@ describe("runCommandCheck", () => { const result = await runCommandCheck( makeTarget({ exec: "bash", args: ["-c", "echo error >&2"] }, { expect: { stderr: [{ contains: "error" }] } }), ); - expect(result.success).toBe(true); expect(result.matched).toBe(true); }); @@ -101,7 +96,7 @@ describe("runCommandCheck", () => { maxOutputBytes: 10, }), ); - expect(result.success).toBe(false); + expect(result.matched).toBe(false); expect(result.failure).not.toBeNull(); expect(result.failure!.message).toContain("超过限制"); }); @@ -114,7 +109,7 @@ describe("runCommandCheck", () => { test("ls 命令执行成功", async () => { const result = await runCommandCheck(makeTarget({ exec: "ls", args: ["/tmp"] })); - expect(result.success).toBe(true); + expect(result.matched).toBe(true); expect(result.statusDetail).toBe("exitCode=0"); }); @@ -122,11 +117,11 @@ describe("runCommandCheck", () => { const result = await runCommandCheck( makeTarget({ exec: "echo", args: ["*"] }, { expect: { stdout: [{ contains: "*" }] } }), ); - expect(result.success).toBe(true); + expect(result.matched).toBe(true); }); test("不提供 stdin,等待输入的命令会阻塞超时", async () => { const result = await runCommandCheck(makeTarget({ exec: "bash", args: ["-c", "read line"] }, { timeoutMs: 500 })); - expect(result.success).toBe(false); + expect(result.matched).toBe(false); }); }); diff --git a/tests/server/checker/engine.test.ts b/tests/server/checker/engine.test.ts index 9e4eb26..d32bf66 100644 --- a/tests/server/checker/engine.test.ts +++ b/tests/server/checker/engine.test.ts @@ -69,7 +69,6 @@ describe("ProbeEngine", () => { const results = (mockStore as unknown as { _results: Array> })._results; expect(results.length).toBe(1); - expect(results[0]!.success).toBe(true); expect(results[0]!.matched).toBe(true); expect(results[0]!.statusDetail).toBe("exitCode=0"); }); @@ -111,8 +110,8 @@ describe("ProbeEngine", () => { const results = (mockStore as unknown as { _results: Array> })._results; expect(results.length).toBe(2); - const badResult = results.find((r) => r.success === false); - const goodResult = results.find((r) => r.success === true); + const badResult = results.find((r) => r.matched === false); + const goodResult = results.find((r) => r.matched === true); expect(badResult).toBeDefined(); expect(goodResult).toBeDefined(); }); @@ -135,7 +134,7 @@ describe("ProbeEngine", () => { const results = (mockStore as unknown as { _results: Array> })._results; expect(results.length).toBe(5); for (const r of results) { - expect(r.success).toBe(true); + expect(r.matched).toBe(true); } }); @@ -198,7 +197,7 @@ describe("ProbeEngine", () => { const results = (mockStore as unknown as { _results: Array> })._results; expect(results.length).toBe(1); - expect(results[0]!.success).toBe(true); + expect(results[0]!.matched).toBe(true); expect(results[0]!.statusDetail).toBe("HTTP 200"); } finally { httpServer.stop(); diff --git a/tests/server/checker/fetcher.test.ts b/tests/server/checker/fetcher.test.ts index 71bbe75..08e12b7 100644 --- a/tests/server/checker/fetcher.test.ts +++ b/tests/server/checker/fetcher.test.ts @@ -77,7 +77,6 @@ describe("runHttpCheck 集成", () => { test("成功请求 200", async () => { const result = await runHttpCheck(makeTarget({ url: `${baseUrl}/ok` })); - expect(result.success).toBe(true); expect(result.matched).toBe(true); expect(result.statusDetail).toBe("HTTP 200"); expect(result.durationMs).not.toBeNull(); @@ -86,7 +85,6 @@ describe("runHttpCheck 集成", () => { test("404 不匹配默认 status [200]", async () => { const result = await runHttpCheck(makeTarget({ url: `${baseUrl}/notfound` })); - expect(result.success).toBe(false); expect(result.matched).toBe(false); expect(result.statusDetail).toBe("HTTP 404"); expect(result.failure).not.toBeNull(); @@ -100,7 +98,6 @@ describe("runHttpCheck 集成", () => { expect: { status: [404] }, }), ); - expect(result.success).toBe(true); expect(result.matched).toBe(true); }); @@ -111,7 +108,6 @@ describe("runHttpCheck 集成", () => { expect: { headers: { "x-custom": "test-value" } }, }), ); - expect(result.success).toBe(true); expect(result.matched).toBe(true); }); @@ -122,7 +118,7 @@ describe("runHttpCheck 集成", () => { expect: { headers: { "x-custom": "wrong-value" } }, }), ); - expect(result.success).toBe(false); + expect(result.matched).toBe(false); expect(result.failure!.phase).toBe("headers"); }); @@ -133,7 +129,6 @@ describe("runHttpCheck 集成", () => { expect: { body: [{ contains: "hello" }] }, }), ); - expect(result.success).toBe(true); expect(result.matched).toBe(true); }); @@ -144,7 +139,7 @@ describe("runHttpCheck 集成", () => { expect: { body: [{ contains: "nonexistent" }] }, }), ); - expect(result.success).toBe(false); + expect(result.matched).toBe(false); expect(result.failure!.phase).toBe("body"); }); @@ -155,7 +150,7 @@ describe("runHttpCheck 集成", () => { expect: { body: [{ json: { path: "$.status", equals: "ok" } }] }, }), ); - expect(result.success).toBe(true); + expect(result.matched).toBe(true); }); test("响应体超过 maxBodyBytes", async () => { @@ -166,7 +161,7 @@ describe("runHttpCheck 集成", () => { expect: { body: [{ contains: "x" }] }, }), ); - expect(result.success).toBe(false); + expect(result.matched).toBe(false); expect(result.failure).not.toBeNull(); expect(result.failure!.phase).toBe("body"); expect(result.failure!.message).toContain("超过限制"); @@ -188,7 +183,7 @@ describe("runHttpCheck 集成", () => { timeoutMs: 100, }), ); - expect(result.success).toBe(false); + expect(result.matched).toBe(false); expect(result.failure).not.toBeNull(); expect(result.failure!.message).toContain("超时"); } finally { @@ -203,7 +198,7 @@ describe("runHttpCheck 集成", () => { expect: { status: [200], body: [{ contains: "something" }] }, }), ); - expect(result.success).toBe(false); + expect(result.matched).toBe(false); expect(result.failure!.phase).toBe("status"); }); @@ -214,7 +209,7 @@ describe("runHttpCheck 集成", () => { expect: { headers: { "x-missing": "value" }, body: [{ contains: "hello" }] }, }), ); - expect(result.success).toBe(false); + expect(result.matched).toBe(false); expect(result.failure!.phase).toBe("headers"); }); @@ -225,13 +220,13 @@ describe("runHttpCheck 集成", () => { expect: { status: [200], body: [{ contains: "not-in-body" }] }, }), ); - expect(result.success).toBe(false); + expect(result.matched).toBe(false); expect(result.failure!.phase).toBe("body"); }); test("无 expect 时默认检查 status 200", async () => { const result = await runHttpCheck(makeTarget({ url: `${baseUrl}/ok`, expect: undefined })); - expect(result.success).toBe(true); + expect(result.matched).toBe(true); }); test("POST 请求携带 body", async () => { @@ -244,7 +239,7 @@ describe("runHttpCheck 集成", () => { expect: { status: [200], body: [{ json: { path: "$.body", equals: "present" } }] }, }), ); - expect(result.success).toBe(true); + expect(result.matched).toBe(true); }); test("仅 contains 规则时不解析 JSON", async () => { @@ -254,6 +249,6 @@ describe("runHttpCheck 集成", () => { expect: { body: [{ contains: "hello world" }] }, }), ); - expect(result.success).toBe(true); + expect(result.matched).toBe(true); }); }); diff --git a/tests/server/checker/store.test.ts b/tests/server/checker/store.test.ts index 9d19d13..dc52521 100644 --- a/tests/server/checker/store.test.ts +++ b/tests/server/checker/store.test.ts @@ -132,7 +132,6 @@ describe("ProbeStore", () => { store.insertCheckResult({ targetId: t1Id, timestamp: "2025-01-01T00:00:00.000Z", - success: true, matched: true, durationMs: 150.5, statusDetail: "200 OK", @@ -142,7 +141,6 @@ describe("ProbeStore", () => { store.insertCheckResult({ targetId: t1Id, timestamp: "2025-01-01T00:00:30.000Z", - success: true, matched: true, durationMs: 300, statusDetail: "200 OK", @@ -161,7 +159,6 @@ describe("ProbeStore", () => { store.insertCheckResult({ targetId: t1Id, timestamp: "2025-01-01T00:01:00.000Z", - success: false, matched: false, durationMs: null, statusDetail: null, @@ -173,7 +170,7 @@ describe("ProbeStore", () => { expect(history.items[0]!.timestamp).toBe("2025-01-01T00:01:00.000Z"); const latest = store.getLatestCheck(t1Id)!; - expect(latest.success).toBe(0); + expect(latest.matched).toBe(0); expect(latest.failure).not.toBeNull(); const parsedFailure = JSON.parse(latest.failure!) as CheckFailure; expect(parsedFailure.kind).toBe("error"); @@ -189,7 +186,6 @@ describe("ProbeStore", () => { store.insertCheckResult({ targetId: t1Id, timestamp: `2025-01-01T01:${String(i).padStart(2, "0")}:00.000Z`, - success: true, matched: true, durationMs: 100 + i, statusDetail: "200 OK", @@ -250,7 +246,6 @@ describe("ProbeStore", () => { expect(samples.length).toBeGreaterThan(0); for (const sample of samples) { expect(typeof sample.timestamp).toBe("string"); - expect(typeof sample.success).toBe("number"); expect(typeof sample.matched).toBe("number"); } });