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

221 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## Purpose
定义基于 Vite + Bun 的全栈应用运行时,包括统一服务 bootstrap、声明式 Bun routes、API 路由组织、HTTP server、API 命名空间、健康检查、生产静态资源服务和 SPA fallback 行为。
## Requirements
### Requirement: Bun HTTP 运行时
系统 SHALL 运行一个 Bun HTTP server使用 `routes` 对象声明式注册 HTML 页面路由和 API 端点,由单个进程提供后端 API、健康检查和前端服务。
#### Scenario: 启动运行时服务器
- **WHEN** server 进程成功启动
- **THEN** 它 SHALL 监听配置文件中指定的 host 和 port通过 routes 对象注册所有路由,并记录实际 server URL
#### Scenario: 通过 YAML 配置提供运行时参数
- **WHEN** 通过 YAML 配置文件提供 host、port、数据目录等参数
- **THEN** server SHALL 使用该值,且不需要重新构建
#### Scenario: CLI 只接受配置文件路径
- **WHEN** 用户通过命令行启动程序
- **THEN** 系统 SHALL 只接受一个命令行参数作为 YAML 配置文件路径
#### Scenario: 提供拨测相关 API
- **WHEN** server 启动完成
- **THEN** 系统 SHALL 通过 routes 对象提供 `/api/summary``/api/targets``/api/targets/:id/history``/api/targets/:id/trend` 端点
### Requirement: HTTP method 语义
系统 SHALL 只为运行时端点声明实际支持的 GET handler不支持的 API method SHALL 按未匹配 API 路由处理,不再保留自定义 405 和 Allow header 语义。
#### Scenario: GET 请求访问运行时端点
- **WHEN** 客户端使用 `GET` 请求 `/health``/api/*` 端点
- **THEN** Bun server SHALL 返回对应端点的成功响应
#### Scenario: 不支持的 API method 请求
- **WHEN** 客户端使用不支持的 method 请求已存在的 `/api/*` 端点
- **THEN** `/api/*` 通配符 SHALL 返回包含 `error``status` 字段的 JSON 404 响应
### 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: API 错误响应一致性
系统 SHALL 为 API 命名空间内的未匹配路由和未匹配 method 返回机器可读 JSON 404 响应。
#### 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 404 响应
### Requirement: 健康检查端点
系统 SHALL 在前端 SPA fallback 之外暴露健康检查端点。
#### Scenario: 健康检查成功
- **WHEN** 客户端请求 `/health`
- **THEN** Bun server SHALL 返回成功的、机器可读的健康检查响应
### Requirement: 生产静态资源服务
系统 SHALL 在生产模式下通过自定义 `serveStaticAsset` 函数服务嵌入的 Vite 前端产出。
#### Scenario: 请求构建后的资源
- **WHEN** 客户端请求 `/assets/*` 路径下的前端资源
- **THEN** 系统 SHALL 从 StaticAssets 的 files map 中查找并返回对应资源Content-Type 根据扩展名推断
#### Scenario: 请求前端根路径
- **WHEN** 客户端请求 `/`
- **THEN** 系统 SHALL 返回 StaticAssets 中的 indexHtmlContent-Type 为 `text/html; charset=utf-8`
### Requirement: 生产缓存策略
系统 SHALL 为生产静态资源提供基于文件名 content hash 的缓存策略。
#### Scenario: 请求前端入口 HTML
- **WHEN** 生产 server 返回前端入口 HTML 文档
- **THEN** 响应 SHALL 包含 `Cache-Control: no-cache` header
#### Scenario: 请求构建后的静态资源
- **WHEN** 生产 server 返回 `/assets/*` 路径下的静态资源
- **THEN** 响应 SHALL 包含 `Cache-Control: public, max-age=31536000, immutable` header
### Requirement: 低风险安全响应头
系统 SHALL 在生产运行时的 JSON API 响应中附加低风险安全响应头HTML 和静态资源响应由 Bun HTML import manifest 返回其内置 headers。
#### Scenario: 生产 JSON 响应包含安全头
- **WHEN** 生产 Bun server 返回 `/health``/api/*` JSON 响应
- **THEN** 响应 SHALL 包含 `X-Content-Type-Options: nosniff``Referrer-Policy` headers
#### Scenario: 生产静态资源响应
- **WHEN** 生产 server 返回前端 HTML 文档或构建后的静态资源
- **THEN** 响应 SHALL 不要求附加自定义安全 headers仅需 Content-Type 和 Cache-Control
### Requirement: SPA fallback 行为
系统 SHALL 通过 fetch fallback 为非 API、非静态资源路径返回前端入口 HTML 文档。
#### Scenario: 刷新前端路由
- **WHEN** 客户端请求不包含文件扩展名的非 API 路径(如 `/dashboard`
- **THEN** fetch fallback SHALL 返回前端入口 HTML 文档
#### Scenario: 保留 API 错误语义
- **WHEN** 客户端请求未知的 `/api/*` 路由
- **THEN** routes 中的 `/api/*` 通配符 MUST 返回 JSON 404 响应,而不是前端入口 HTML 文档
### Requirement: 优雅关机
系统 SHALL 在收到终止信号时正确清理资源。
#### Scenario: SIGINT/SIGTERM 处理
- **WHEN** 开发模式进程收到 SIGINT 或 SIGTERM 信号
- **THEN** 系统 SHALL 调用 engine.stop() 停止调度、调用 store.close() 关闭数据库连接后退出进程
### Requirement: 路径参数支持
系统 SHALL 使用 routes 对象的 `:param` 语法声明路径参数,替代手动 regex 匹配。
#### Scenario: 带路径参数的 API 路由
- **WHEN** 客户端请求 `/api/targets/123/history`
- **THEN** 系统 SHALL 通过 `routes` 中注册的 `/api/targets/:id/history` 匹配,并通过 `req.params.id` 获取参数值 `"123"`
#### Scenario: 路径参数类型
- **WHEN** route handler 接收到路径参数
- **THEN** 参数值 SHALL 为字符串类型handler 负责进行类型转换和校验
### Requirement: HTTP Method 声明
系统 SHALL 在 routes 对象中为每个 API 端点以 per-method handler 形式声明支持的 HTTP method未匹配 method 的 API 请求 SHALL 落入 `/api/*` 通配符并返回 JSON 404。
#### Scenario: 单 method 端点
- **WHEN** API 端点只支持 GET 方法
- **THEN** 该端点 SHALL 以 `{ GET(req) { ... } }` 形式注册
#### Scenario: 不支持的 method 请求
- **WHEN** 客户端使用未声明的 method 请求 API 端点
- **THEN** `/api/*` 通配符 SHALL 返回 JSON 格式的 404 错误响应
### Requirement: 路由按职责拆分
系统 SHALL 将 HTTP 路由处理逻辑按 API 端点拆分为独立模块,每个模块导出 route handler 函数供 routes 对象统一注册。
#### Scenario: health 端点独立路由
- **WHEN** 客户端请求 `GET /health`
- **THEN** `routes/health.ts` 导出的 handler 负责处理,返回 HealthResponse JSON
#### Scenario: summary 端点独立路由
- **WHEN** 客户端请求 `GET /api/summary`
- **THEN** `routes/summary.ts` 导出的 handler 负责处理,委托 store 查询并返回 SummaryResponse JSON
#### Scenario: targets 端点独立路由
- **WHEN** 客户端请求 `GET /api/targets`
- **THEN** `routes/targets.ts` 导出的 handler 负责处理,委托 store 查询并返回 TargetStatus[] JSON
#### Scenario: history 端点独立路由
- **WHEN** 客户端请求 `GET /api/targets/:id/history?from=ISO&to=ISO`
- **THEN** `routes/history.ts` 导出的 handler 负责处理,通过 `req.params.id` 获取路径参数包含参数校验、store 查询和 HistoryResponse 返回
#### Scenario: trend 端点独立路由
- **WHEN** 客户端请求 `GET /api/targets/:id/trend?from=ISO&to=ISO`
- **THEN** `routes/trend.ts` 导出的 handler 负责处理,通过 `req.params.id` 获取路径参数包含参数校验、store 查询和 TrendPoint[] 返回
### Requirement: 共享辅助函数集中管理
系统 SHALL 将跨路由共享的响应格式化函数抽取到 helpers.ts 模块,单一职责、集中管理。
#### Scenario: createApiError 集中定义
- **WHEN** 任意路由需要返回 API 错误响应
- **THEN** 从 `helpers.ts` 导入 `createApiError` 函数,提供错误信息和状态码
#### Scenario: jsonResponse 集中定义
- **WHEN** 任意路由需要返回 JSON 响应
- **THEN** 从 `helpers.ts` 导入 `jsonResponse` 函数,处理 HEAD 方法、Content-Type 和安全头
#### Scenario: mapCheckResult 集中定义
- **WHEN** 需要将 StoredCheckResult 映射为 API CheckResult
- **THEN** 从 `helpers.ts` 导入 `mapCheckResult` 函数,处理 failure JSON 解析和格式转换
### Requirement: 统一启动引导函数
系统 SHALL 提供 `src/server/bootstrap.ts` 导出 `bootstrap(options: BootstrapOptions)` 函数,封装完整的服务启动序列:加载配置、初始化正式 logger、创建 store、同步 targets、创建并启动 engine、启动 HTTP server、注册 shutdown handler。配置加载失败发生在正式 logger 初始化之前,系统 SHALL 使用 console fallback 输出启动失败信息。配置加载成功后的启动失败 SHALL 使用正式 logger 输出 `fatal` 后退出。
#### Scenario: 开发模式启动
- **WHEN** `dev.ts` 调用 `bootstrap({ configPath, mode: "development" })`
- **THEN** 系统 SHALL 完成完整启动序列,不传入 staticAssets并初始化运行时 logger
#### Scenario: 生产模式启动(带静态资源)
- **WHEN** code-generated entry 调用 `bootstrap({ configPath, mode: "production", staticAssets })`
- **THEN** 系统 SHALL 完成完整启动序列,将 staticAssets 传递给 startServer并初始化运行时 logger
#### Scenario: 配置加载失败处理
- **WHEN** 配置文件读取、YAML 解析或配置校验失败
- **THEN** 系统 SHALL 通过 console fallback 输出错误信息并以非零退出码退出进程
#### Scenario: 配置加载后的启动失败处理
- **WHEN** logger、store、engine 或 HTTP server 初始化失败
- **THEN** 系统 SHALL 通过正式 logger 输出 `fatal` 日志并以非零退出码退出进程
#### Scenario: 优雅关机
- **WHEN** 进程收到 SIGINT 或 SIGTERM 信号
- **THEN** bootstrap 注册的 shutdown handler SHALL 调用 engine.stop()、store.close() 和 logger.flush() 后退出
### Requirement: BootstrapOptions 接口
`bootstrap` 函数 SHALL 接受 `BootstrapOptions` 参数,包含 `configPath: string``mode: RuntimeMode` 和可选的 `staticAssets?: StaticAssets`
#### Scenario: 最小配置(开发模式)
- **WHEN** 仅传入 configPath 和 mode
- **THEN** 系统 SHALL 正常启动startServer 不接收 staticAssets 参数
#### Scenario: 生产模式配置
- **WHEN** 传入 configPath、mode 和 staticAssets
- **THEN** 系统 SHALL 将 staticAssets 传递给 startServer
### Requirement: dev.ts 和生产入口使用 bootstrap
`dev.ts``src/server/main.ts` SHALL 调用 `bootstrap()` 而非各自维护启动序列。
#### Scenario: dev.ts 调用 bootstrap
- **WHEN** 开发者运行 `bun run dev`
- **THEN** `dev.ts` SHALL 调用 `bootstrap` 完成启动
#### Scenario: main.ts 调用 bootstrap
- **WHEN** 生产可执行文件启动
- **THEN** `main.ts` SHALL 调用 `bootstrap` 完成启动