## 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 中的 indexHtml,Content-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` 完成启动