feat: 新增两个 OpenSpec 变更提案 — 前端架构重构与 HTTP Checker 质量加固
- frontend-architecture-refactor: 拆分 hooks/组件、类型筛选器动态化 - http-checker-quality-hardening: ReDoS 防护、failure 格式修正、测试补全
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-05-13
|
||||
145
openspec/changes/frontend-architecture-refactor/design.md
Normal file
145
openspec/changes/frontend-architecture-refactor/design.md
Normal file
@@ -0,0 +1,145 @@
|
||||
## Context
|
||||
|
||||
当前前端代码约 970 行,功能完整但存在以下架构问题:
|
||||
|
||||
1. **hook 职责过重**:`hooks/useTargetDetail.ts`(113 行)同时承载全局查询(`useSummary`、`useTargets`)、Drawer 状态管理、条件查询和通用 `fetchJson` 封装,文件名与实际职责不匹配
|
||||
2. **组件体积膨胀**:`TargetDetailDrawer.tsx`(228 行)混合了时间选择逻辑、两个 Tab 的完整渲染、列定义和统计计算
|
||||
3. **类型维护重复**:`target-type-display.ts` 和 `target-table-filters.ts` 各自硬编码 checker 类型列表,新增 checker 需改两处前端文件
|
||||
4. **测试覆盖不足**:仅 `constants/` 下 4 个纯函数有测试,`utils/time.ts` 和组件内统计逻辑未覆盖
|
||||
5. **小问题**:`StatusDonut` 用数组索引做 key、`StatusBar` 硬编码 30 格、`TrendChart` 冗余 loading prop
|
||||
|
||||
后端 `CheckerRegistry` 已有 `supportedTypes` 属性,可直接暴露给前端。项目未上线,不需要向前兼容。
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- hook 按职责拆分,文件名匹配实际内容
|
||||
- `TargetDetailDrawer` 拆分为 3 个组件,每个 < 100 行
|
||||
- 类型筛选器由后端 meta API 驱动,新增 checker 前端零改动
|
||||
- 删除 type label 转换层,直接使用 type 原始文本
|
||||
- 补齐前端纯函数测试
|
||||
- 修复已知小问题
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- 不引入路由库或状态管理库
|
||||
- 不重构后端 API 结构
|
||||
- 不改变现有轮询策略和 QueryClient 配置
|
||||
- 不新增 CSS 文件(继续使用单一 `styles.css`)
|
||||
|
||||
## Decisions
|
||||
|
||||
### Decision 1: hook 拆分策略
|
||||
|
||||
**选择**:按查询层级拆分为 `use-queries.ts` 和 `use-target-detail.ts`
|
||||
|
||||
- `use-queries.ts`:`queryKeys`、`fetchJson`(不导出)、`useSummary`、`useTargets`、`useMeta`
|
||||
- `use-target-detail.ts`:仅保留 Drawer 状态管理(`selectedTargetId`、时间范围、分页、`openDrawer`/`closeDrawer`)和条件查询(trend/history)
|
||||
|
||||
**理由**:全局查询(summary/targets/meta)是面板级别的,与 Drawer 详情无关。拆分后各文件职责单一,命名自解释。`fetchJson` 仅被 query 层使用,留在 `use-queries.ts` 内部不导出。
|
||||
|
||||
**备选方案**:
|
||||
- 仅重命名为 `useQueries.ts` — 解决命名问题但不解决职责混合
|
||||
- 每个 query 一个文件 — 过度拆分,增加文件数量但无实质收益
|
||||
|
||||
### Decision 2: TargetDetailDrawer 拆分方式
|
||||
|
||||
**选择**:拆为 3 个组件 + 1 个常量文件
|
||||
|
||||
```
|
||||
TargetDetailDrawer.tsx ← Drawer 壳 + 时间选择 + Tab 切换
|
||||
OverviewTab.tsx ← 统计 + TrendChart + StatusDonut + Descriptions
|
||||
HistoryTab.tsx ← PrimaryTable + 分页
|
||||
constants/history-table-columns.tsx ← HISTORY_COLUMNS
|
||||
```
|
||||
|
||||
**理由**:两个 Tab 的内容完全独立,拆分后各组件 < 100 行。`HISTORY_COLUMNS` 与 `TARGET_TABLE_COLUMNS` 性质相同,应放在 `constants/` 下保持一致。
|
||||
|
||||
统计计算逻辑(`totalChecks`/`upChecks`/`downChecks`)提取为 `utils/stats.ts` 纯函数,便于测试和 `useMemo`。
|
||||
|
||||
### Decision 3: Meta API 设计
|
||||
|
||||
**选择**:`GET /api/meta` 返回 `{ checkerTypes: string[] }`
|
||||
|
||||
```typescript
|
||||
// src/shared/api.ts
|
||||
export interface MetaResponse {
|
||||
checkerTypes: string[];
|
||||
}
|
||||
```
|
||||
|
||||
**理由**:
|
||||
- 从 `checkerRegistry.supportedTypes` 直接获取,无需额外维护
|
||||
- 返回 `string[]` 而非 `{ key, label }[]`,因为决策是不做 label 转换
|
||||
- 端点命名为 `/api/meta` 而非 `/api/types`,为未来扩展预留空间(如版本号、功能开关等)
|
||||
- `staleTime: Infinity`,应用生命周期内只请求一次
|
||||
|
||||
**备选方案**:
|
||||
- 从 targets 响应中动态提取 — 只能获取"当前有数据的类型",不能获取"系统支持的全部类型"
|
||||
- 在 health 端点中附带 — 语义不匹配,health 应保持最小化
|
||||
|
||||
### Decision 4: 类型展示策略
|
||||
|
||||
**选择**:删除 `target-type-display.ts`,所有展示位置直接使用 `target.type` 原始文本
|
||||
|
||||
**影响位置**:
|
||||
- `target-table-columns.tsx` 类型列 cell:`row.type` 直接渲染
|
||||
- `TargetDetailDrawer.tsx` 标题栏 Tag:`target.type` 直接渲染
|
||||
- `typeFilter` 列表:从 meta API 获取,label 和 value 均为原始 type 文本
|
||||
|
||||
**理由**:type 文本本身已足够清晰(`http`、`command`、`tcp`),无需额外映射层。消除了前后端重复维护的问题。
|
||||
|
||||
### Decision 5: 列定义动态化
|
||||
|
||||
**选择**:`TARGET_TABLE_COLUMNS` 从静态常量改为工厂函数
|
||||
|
||||
```typescript
|
||||
export function createTargetTableColumns(checkerTypes: string[]): PrimaryTableCol<TargetStatus>[] {
|
||||
const typeFilter = {
|
||||
list: [
|
||||
{ label: "全部", value: "" },
|
||||
...checkerTypes.map(t => ({ label: t, value: t })),
|
||||
],
|
||||
type: "single" as const,
|
||||
};
|
||||
// ... 返回列定义数组
|
||||
}
|
||||
```
|
||||
|
||||
**数据流**:`useMeta()` → `checkerTypes` → `createTargetTableColumns(checkerTypes)` → `TargetGroup` columns prop
|
||||
|
||||
**理由**:`typeFilter` 是唯一需要动态数据的部分,通过工厂函数注入参数,保持列定义的纯函数特性。`statusFilter` 保持静态(UP/DOWN 是固定的)。
|
||||
|
||||
`TargetGroup` 新增 `columns` prop 接收动态列定义,`TargetBoard` 负责调用工厂函数并传递。
|
||||
|
||||
### Decision 6: StatusBar maxSlots 参数化
|
||||
|
||||
**选择**:新增 `maxSlots` prop(默认 30),组件根据 prop 渲染格数
|
||||
|
||||
```typescript
|
||||
interface StatusBarProps {
|
||||
samples: Array<{ up: boolean }>;
|
||||
maxSlots?: number;
|
||||
}
|
||||
```
|
||||
|
||||
**理由**:消除硬编码魔数,使组件可复用。默认值 30 保持向后兼容。
|
||||
|
||||
### Decision 7: TrendChart 移除 loading prop
|
||||
|
||||
**选择**:移除 `loading` prop,组件只接收 `data: TrendPoint[]`
|
||||
|
||||
**理由**:调用方(`OverviewTab`)已用 `Skeleton` 处理 loading 状态,`TrendChart` 只在有数据时渲染。组件内部的 `if (loading)` 分支和 `loading={false}` 传参都是死代码。
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
| Risk | Mitigation |
|
||||
|------|-----------|
|
||||
| 列定义改为函数后,每次 `checkerTypes` 变化会重新创建列数组 | `useMemo` 包裹 `createTargetTableColumns` 调用;meta 数据 `staleTime: Infinity` 确保不会频繁变化 |
|
||||
| meta API 在 targets 之前未返回时,筛选器暂时为空 | meta 请求极轻量(无 DB 查询),通常先于 targets 返回;即使晚到,筛选器会在数据到达后自动出现 |
|
||||
| 拆分后组件间 props 传递增多 | 层级仅增加一层(Drawer → Tab),props 类型明确,不会造成 prop drilling 问题 |
|
||||
|
||||
## Open Questions
|
||||
|
||||
无。方案已在 explore 阶段与用户确认。
|
||||
34
openspec/changes/frontend-architecture-refactor/proposal.md
Normal file
34
openspec/changes/frontend-architecture-refactor/proposal.md
Normal file
@@ -0,0 +1,34 @@
|
||||
## Why
|
||||
|
||||
前端代码经过多轮功能迭代后,出现了 hook 职责过重(`useTargetDetail.ts` 承载全部数据层)、组件体积膨胀(`TargetDetailDrawer` 228 行混合多种逻辑)、类型筛选器与后端硬编码重复维护等架构问题。需要通过拆分、动态化和测试补齐来提升可维护性,使新增 checker 类型时前端零改动。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 拆分 `hooks/useTargetDetail.ts` 为 `use-queries.ts`(全局查询)和 `use-target-detail.ts`(Drawer 状态管理)
|
||||
- 拆分 `TargetDetailDrawer.tsx` 为 Drawer 壳、`OverviewTab.tsx`、`HistoryTab.tsx` 三个组件
|
||||
- 将 `HISTORY_COLUMNS` 移至 `constants/history-table-columns.tsx`
|
||||
- 提取统计计算逻辑为 `utils/stats.ts` 纯函数
|
||||
- 后端新增 `GET /api/meta` 端点,返回 `checkerTypes` 列表
|
||||
- 前端新增 `useMeta()` hook 消费 meta API,动态生成类型筛选器
|
||||
- **BREAKING** 删除 `constants/target-type-display.ts`,前端直接使用 type 原始文本,不再做 label 转换
|
||||
- 列定义从静态常量改为工厂函数 `createTargetTableColumns(checkerTypes)`
|
||||
- 修复小问题:`StatusDonut` key、`StatusBar` 硬编码、`TrendChart` 冗余 prop、统计计算 useMemo
|
||||
- 补充前端测试:`utils/time.ts`、`utils/stats.ts`、动态列生成
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `meta-api`: 后端 meta 信息 API,提供 checker 类型列表等运行时元数据
|
||||
|
||||
### Modified Capabilities
|
||||
- `target-type-display`: 移除前端静态映射,改为直接使用后端返回的 type 原始文本,筛选器列表由 meta API 动态驱动
|
||||
- `tanstack-query-data-layer`: hook 文件拆分为 `use-queries.ts` 和 `use-target-detail.ts`,新增 `useMeta()` 查询
|
||||
- `target-detail-drawer`: 组件拆分为 Drawer 壳 + OverviewTab + HistoryTab,统计计算提取为纯函数
|
||||
- `target-table`: 列定义从静态常量改为工厂函数,接收动态 checkerTypes 参数;类型列直接显示 type 原始文本
|
||||
|
||||
## Impact
|
||||
|
||||
- 后端:新增 `src/server/routes/meta.ts`、`src/shared/api.ts` 增加 `MetaResponse` 类型、`app.ts` 注册路由
|
||||
- 前端:hooks/components/constants 目录结构调整,删除 `target-type-display.ts`
|
||||
- 测试:删除 `target-type-display.test.ts`,新增 `time.test.ts`、`stats.test.ts`,更新 `target-table-filters.test.ts`
|
||||
- 文档:更新 DEVELOPMENT.md 中前端目录结构和组件清单
|
||||
@@ -0,0 +1,27 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Meta 信息 API
|
||||
系统 SHALL 提供 `GET /api/meta` 端点,返回系统运行时元数据。
|
||||
|
||||
#### Scenario: 获取 checker 类型列表
|
||||
- **WHEN** 客户端请求 `GET /api/meta`
|
||||
- **THEN** 系统 SHALL 返回 JSON `{ checkerTypes: string[] }`,包含所有已注册的 checker 类型标识符(如 `["http", "command"]`)
|
||||
|
||||
#### Scenario: 类型列表来源
|
||||
- **WHEN** 系统启动并注册了 checker
|
||||
- **THEN** `/api/meta` 返回的 `checkerTypes` SHALL 与 `CheckerRegistry.supportedTypes` 完全一致
|
||||
|
||||
#### Scenario: 仅允许 GET/HEAD 方法
|
||||
- **WHEN** 客户端使用 POST/PUT/DELETE 等方法请求 `/api/meta`
|
||||
- **THEN** 系统 SHALL 返回 405 状态码
|
||||
|
||||
#### Scenario: HEAD 请求返回空体
|
||||
- **WHEN** 客户端使用 HEAD 方法请求 `/api/meta`
|
||||
- **THEN** 系统 SHALL 返回 200 状态码和正确的 Content-Type header,body 为空
|
||||
|
||||
### Requirement: MetaResponse 共享类型
|
||||
系统 SHALL 在 `src/shared/api.ts` 中定义 `MetaResponse` 类型。
|
||||
|
||||
#### Scenario: MetaResponse 类型定义
|
||||
- **WHEN** 前后端引用 `MetaResponse` 类型
|
||||
- **THEN** 该类型 SHALL 包含 `checkerTypes: string[]` 字段
|
||||
@@ -0,0 +1,109 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: TanStack Query 数据层
|
||||
前端 SHALL 使用 TanStack Query(@tanstack/react-query)管理所有 API 请求,数据层代码 SHALL 按职责拆分为独立 hook 文件。
|
||||
|
||||
#### Scenario: QueryClient 配置
|
||||
- **WHEN** 应用启动
|
||||
- **THEN** 系统 SHALL 创建 QueryClient,默认配置 retry=1、refetchOnWindowFocus=true、staleTime=5000
|
||||
|
||||
#### Scenario: QueryClientProvider 挂载
|
||||
- **WHEN** 应用渲染
|
||||
- **THEN** 根组件 SHALL 包裹在 QueryClientProvider 中,提供 QueryClient 实例
|
||||
|
||||
### Requirement: queryKey 工厂
|
||||
系统 SHALL 提供统一的 queryKey 工厂函数,确保 queryKey 的唯一性和一致性。
|
||||
|
||||
#### Scenario: summary queryKey
|
||||
- **WHEN** 查询 summary 数据
|
||||
- **THEN** queryKey SHALL 为 ["summary"]
|
||||
|
||||
#### Scenario: targets queryKey
|
||||
- **WHEN** 查询 targets 数据
|
||||
- **THEN** queryKey SHALL 为 ["targets"]
|
||||
|
||||
#### Scenario: meta queryKey
|
||||
- **WHEN** 查询 meta 数据
|
||||
- **THEN** queryKey SHALL 为 ["meta"]
|
||||
|
||||
#### Scenario: trend queryKey
|
||||
- **WHEN** 查询某目标的趋势数据
|
||||
- **THEN** queryKey SHALL 为 ["trend", targetId, from, to]
|
||||
|
||||
#### Scenario: history queryKey
|
||||
- **WHEN** 查询某目标的历史记录
|
||||
- **THEN** queryKey SHALL 为 ["history", targetId, from, to, page]
|
||||
|
||||
### Requirement: Hook 文件拆分
|
||||
数据层 hook SHALL 按职责拆分为独立文件。
|
||||
|
||||
#### Scenario: 全局查询 hook 文件
|
||||
- **WHEN** 开发者需要使用全局面板级查询
|
||||
- **THEN** `useSummary`、`useTargets`、`useMeta` SHALL 从 `hooks/use-queries.ts` 导出
|
||||
|
||||
#### Scenario: Drawer 状态 hook 文件
|
||||
- **WHEN** 开发者需要使用 Drawer 状态管理
|
||||
- **THEN** `useTargetDetail` SHALL 从 `hooks/use-target-detail.ts` 导出
|
||||
|
||||
#### Scenario: fetchJson 不导出
|
||||
- **WHEN** 数据层内部需要 fetch 封装
|
||||
- **THEN** `fetchJson` SHALL 定义在 `use-queries.ts` 内部,不作为公共 API 导出
|
||||
|
||||
#### Scenario: queryKeys 不导出
|
||||
- **WHEN** 数据层内部需要 query key
|
||||
- **THEN** `queryKeys` 对象 SHALL 定义在 `use-queries.ts` 内部,不作为公共 API 导出
|
||||
|
||||
### Requirement: Meta 查询
|
||||
系统 SHALL 提供 `useMeta` hook 查询系统元数据。
|
||||
|
||||
#### Scenario: meta 查询配置
|
||||
- **WHEN** 应用启动
|
||||
- **THEN** `useMeta` SHALL 请求 `/api/meta`,配置 `staleTime: Infinity`(应用生命周期内只请求一次)
|
||||
|
||||
#### Scenario: meta 数据返回
|
||||
- **WHEN** meta 查询成功
|
||||
- **THEN** hook SHALL 返回 `MetaResponse` 类型数据,包含 `checkerTypes` 字段
|
||||
|
||||
### Requirement: Summary 轮询查询
|
||||
系统 SHALL 使用 useQuery 实现总览统计的自动轮询。
|
||||
|
||||
#### Scenario: summary 自动轮询
|
||||
- **WHEN** Dashboard 页面处于打开状态
|
||||
- **THEN** 系统 SHALL 每 8 秒自动请求 /api/summary,使用 refetchInterval=8000
|
||||
|
||||
#### Scenario: summary 后台刷新
|
||||
- **WHEN** 页面处于后台标签页
|
||||
- **THEN** 系统 SHALL 暂停轮询(refetchIntervalInBackground=false)
|
||||
|
||||
### Requirement: Targets 轮询查询
|
||||
系统 SHALL 使用 useQuery 实现目标列表的自动轮询。
|
||||
|
||||
#### Scenario: targets 自动轮询
|
||||
- **WHEN** Dashboard 页面处于打开状态
|
||||
- **THEN** 系统 SHALL 每 8 秒自动请求 /api/targets,使用 refetchInterval=8000
|
||||
|
||||
### Requirement: 条件查询
|
||||
趋势和历史记录查询 SHALL 使用 enabled 条件控制,仅在目标被选中时触发。
|
||||
|
||||
#### Scenario: 未选中目标时不请求
|
||||
- **WHEN** 用户未点击任何目标表格行
|
||||
- **THEN** trend 和 history 的 useQuery SHALL enabled=false,不发起请求
|
||||
|
||||
#### Scenario: 选中目标时自动请求
|
||||
- **WHEN** 用户点击目标表格行
|
||||
- **THEN** trend 和 history 的 useQuery SHALL enabled=true,自动发起请求
|
||||
|
||||
#### Scenario: 时间范围变化时重新请求
|
||||
- **WHEN** 用户更改时间范围
|
||||
- **THEN** trend 和 history 的 useQuery SHALL 因 queryKey 变化自动重新请求
|
||||
|
||||
### Requirement: 开发调试面板
|
||||
开发环境下 SHALL 挂载 TanStack Query Devtools。
|
||||
|
||||
#### Scenario: 开发环境显示 Devtools
|
||||
- **WHEN** 应用在开发模式下运行
|
||||
- **THEN** 页面 SHALL 显示 ReactQueryDevtools 浮动面板
|
||||
|
||||
#### Scenario: 生产环境排除 Devtools
|
||||
- **WHEN** 应用在生产模式下构建
|
||||
- **THEN** ReactQueryDevtools SHALL 不被包含在产物中
|
||||
@@ -0,0 +1,91 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 目标详情 Drawer
|
||||
Dashboard SHALL 在用户点击目标表格行后从右侧滑出 Drawer,展示该目标的详细统计信息和检查记录。Drawer 标题栏和内容不使用内联 style。Drawer 内容 SHALL 拆分为独立的 Tab 组件。
|
||||
|
||||
#### Scenario: 打开 Drawer
|
||||
- **WHEN** 用户点击某个目标表格行
|
||||
- **THEN** 系统 SHALL 从右侧滑出 Drawer(placement="right"),宽度为视口 60%
|
||||
|
||||
#### Scenario: Drawer 标题栏
|
||||
- **WHEN** Drawer 渲染
|
||||
- **THEN** 标题栏 SHALL 使用 TDesign Space 组件(align="center")布局,包含 StatusDot、目标名称(TDesign Typography.Text strong)和类型标签(TDesign Tag,直接显示 target.type 原始文本),以及内建关闭按钮。不使用内联 style 的 flex 布局
|
||||
|
||||
#### Scenario: 关闭 Drawer
|
||||
- **WHEN** 用户点击关闭按钮、ESC 键或遮罩层
|
||||
- **THEN** Drawer SHALL 关闭
|
||||
|
||||
#### Scenario: Drawer 无底部按钮
|
||||
- **WHEN** Drawer 渲染
|
||||
- **THEN** Drawer SHALL 不显示底部操作栏(footer={false})
|
||||
|
||||
#### Scenario: Drawer 数据同步
|
||||
- **WHEN** Drawer 打开期间后台轮询刷新了 targets 数据
|
||||
- **THEN** Drawer 中 selectedTarget 的状态 SHALL 随之同步更新
|
||||
|
||||
#### Scenario: 切换目标重置 Tab
|
||||
- **WHEN** 用户从目标 A 切换到目标 B(点击不同的表格行)
|
||||
- **THEN** Drawer SHALL 重置为概览 Tab,使用 key={target.id} 确保组件状态不残留
|
||||
|
||||
#### Scenario: Drawer 内容区间距
|
||||
- **WHEN** Drawer 内容渲染
|
||||
- **THEN** 时间选择器、Tabs 等区块之间的间距 SHALL 通过 TDesign Space 组件(direction="vertical", size={16})统一管理,不使用内联 style 的 marginBottom
|
||||
|
||||
### Requirement: 概览面板组件化
|
||||
概览 Tab SHALL 作为独立组件 `OverviewTab` 实现,接收数据 props 进行渲染。
|
||||
|
||||
#### Scenario: OverviewTab 组件职责
|
||||
- **WHEN** 概览 Tab 渲染
|
||||
- **THEN** `OverviewTab` 组件 SHALL 负责统计卡片、趋势图、状态分布环形图和基本信息的渲染
|
||||
|
||||
#### Scenario: 统计计算使用纯函数
|
||||
- **WHEN** OverviewTab 需要计算 totalChecks、upChecks、downChecks
|
||||
- **THEN** 计算逻辑 SHALL 通过 `utils/stats.ts` 中的纯函数实现,并使用 `useMemo` 缓存结果
|
||||
|
||||
#### Scenario: OverviewTab props
|
||||
- **WHEN** OverviewTab 渲染
|
||||
- **THEN** 组件 SHALL 接收 `target: TargetStatus`、`trendData: TrendPoint[]`、`trendLoading: boolean` 作为 props
|
||||
|
||||
### Requirement: 记录面板组件化
|
||||
记录 Tab SHALL 作为独立组件 `HistoryTab` 实现。
|
||||
|
||||
#### Scenario: HistoryTab 组件职责
|
||||
- **WHEN** 记录 Tab 渲染
|
||||
- **THEN** `HistoryTab` 组件 SHALL 负责检查结果表格和分页的渲染
|
||||
|
||||
#### Scenario: HistoryTab props
|
||||
- **WHEN** HistoryTab 渲染
|
||||
- **THEN** 组件 SHALL 接收 `historyData: HistoryResponse`、`historyLoading: boolean`、`onPageChange: (page: number) => void` 作为 props
|
||||
|
||||
#### Scenario: 历史记录列定义外置
|
||||
- **WHEN** HistoryTab 渲染表格
|
||||
- **THEN** 列定义 SHALL 从 `constants/history-table-columns.tsx` 导入,不在组件内部定义
|
||||
|
||||
### Requirement: TrendChart 简化
|
||||
TrendChart 组件 SHALL 仅接收数据 props,不处理 loading 状态。
|
||||
|
||||
#### Scenario: TrendChart 无 loading prop
|
||||
- **WHEN** TrendChart 渲染
|
||||
- **THEN** 组件 SHALL 仅接收 `data: TrendPoint[]` prop,不接收 `loading` prop
|
||||
|
||||
#### Scenario: TrendChart 空数据
|
||||
- **WHEN** TrendChart 接收空数组
|
||||
- **THEN** 组件 SHALL 显示"暂无趋势数据"占位文本
|
||||
|
||||
### Requirement: StatusDonut key 修复
|
||||
StatusDonut 组件 SHALL 使用语义化的 key。
|
||||
|
||||
#### Scenario: Pie Cell key
|
||||
- **WHEN** StatusDonut 渲染 Pie Cell 列表
|
||||
- **THEN** 每个 Cell 的 key SHALL 使用 data item 的 `name` 字段,不使用数组索引
|
||||
|
||||
### Requirement: StatusBar 参数化
|
||||
StatusBar 组件 SHALL 支持可配置的格数。
|
||||
|
||||
#### Scenario: maxSlots prop
|
||||
- **WHEN** StatusBar 渲染
|
||||
- **THEN** 组件 SHALL 接收可选的 `maxSlots` prop(默认 30),根据该值渲染对应数量的格子
|
||||
|
||||
#### Scenario: 格子渲染逻辑
|
||||
- **WHEN** StatusBar 渲染且 samples 数量少于 maxSlots
|
||||
- **THEN** 多余的格子 SHALL 显示为 empty 状态
|
||||
@@ -0,0 +1,69 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 表格列定义
|
||||
每个分组的 PrimaryTable SHALL 包含状态、名称、类型、可用率、最近状态条、延迟、间隔 7 列,不含分组列(同组内冗余)。列渲染不使用内联 style。列定义 SHALL 通过工厂函数动态生成。
|
||||
|
||||
#### Scenario: 状态列
|
||||
- **WHEN** 表格渲染
|
||||
- **THEN** 状态列 SHALL 使用 StatusDot 组件渲染,标题显示"#",宽度 60px,fixed="left",居中对齐,支持筛选(UP/DOWN/全部)。StatusDot SHALL 通过 CSS 类(`.status-dot--up` / `.status-dot--down`)控制颜色,不使用内联 style
|
||||
|
||||
#### Scenario: 名称列
|
||||
- **WHEN** 表格渲染
|
||||
- **THEN** 名称列 SHALL 显示目标名称,支持字母排序(zh-CN),ellipsis 超长名称自动省略并 Tooltip 显示全名
|
||||
|
||||
#### Scenario: 类型列
|
||||
- **WHEN** 表格渲染
|
||||
- **THEN** 类型列 SHALL 使用 TDesign Tag 组件(size=small, theme=primary, variant=light-outline)直接显示 target.type 原始文本,支持单选筛选
|
||||
|
||||
#### Scenario: 类型筛选器动态生成
|
||||
- **WHEN** 表格渲染
|
||||
- **THEN** 类型列的筛选器列表 SHALL 从 meta API 返回的 `checkerTypes` 动态生成,包含"全部"选项和每个 checker 类型选项(label 和 value 均为 type 原始文本)
|
||||
|
||||
#### Scenario: 可用率列
|
||||
- **WHEN** 表格渲染
|
||||
- **THEN** 可用率列 SHALL 使用 TDesign Progress 组件(theme=line, size=small)渲染,颜色通过 CSS 自定义属性 `--avail-N`(基于项目自定义色值)控制,每 10% 一档,label 显示百分比数值,支持排序(升序优先,最差排最前)。color-threshold 函数 SHALL 返回 CSS 自定义属性引用而非硬编码色值
|
||||
|
||||
#### Scenario: 最近状态列
|
||||
- **WHEN** 表格渲染
|
||||
- **THEN** 最近状态列 SHALL 使用 StatusBar 组件渲染采样色块,宽度 220px。StatusBar SHALL 通过 CSS 类(`.status-bar-block--up` / `.status-bar-block--down` / `.status-bar-block--empty`)控制色块颜色,不使用内联 style
|
||||
|
||||
#### Scenario: 延迟列
|
||||
- **WHEN** 表格渲染
|
||||
- **THEN** 延迟列 SHALL 显示最近一次检查的延迟毫秒数,右对齐。颜色 SHALL 通过 CSS 类实现:≤100ms 使用 `.latency-ok`、100-500ms 使用 `.latency-warn`、>500ms 使用 `.latency-error`。无数据 SHALL 使用 `.text-disabled` 类显示 "-",数值 SHALL 使用 `.tabular-nums` 类等宽显示。不使用内联 style
|
||||
|
||||
#### Scenario: 间隔列
|
||||
- **WHEN** 表格渲染
|
||||
- **THEN** 间隔列 SHALL 显示检查间隔(如 "5s"、"30s"),居中对齐,宽度 72px
|
||||
|
||||
### Requirement: 列定义工厂函数
|
||||
列定义 SHALL 通过工厂函数生成,接收动态参数。
|
||||
|
||||
#### Scenario: createTargetTableColumns 函数
|
||||
- **WHEN** 需要生成表格列定义
|
||||
- **THEN** 系统 SHALL 调用 `createTargetTableColumns(checkerTypes: string[])` 函数,返回 `PrimaryTableCol<TargetStatus>[]`
|
||||
|
||||
#### Scenario: checkerTypes 为空数组
|
||||
- **WHEN** meta API 尚未返回或返回空数组
|
||||
- **THEN** 类型列的筛选器 SHALL 仅包含"全部"选项
|
||||
|
||||
#### Scenario: 列定义缓存
|
||||
- **WHEN** TargetBoard 组件渲染
|
||||
- **THEN** 列定义 SHALL 通过 `useMemo` 缓存,仅在 `checkerTypes` 变化时重新生成
|
||||
|
||||
### Requirement: TargetGroup 接收 columns prop
|
||||
TargetGroup 组件 SHALL 通过 prop 接收列定义,不再直接导入静态常量。
|
||||
|
||||
#### Scenario: columns prop
|
||||
- **WHEN** TargetGroup 渲染
|
||||
- **THEN** 组件 SHALL 接收 `columns: PrimaryTableCol<TargetStatus>[]` prop 并传递给 PrimaryTable
|
||||
|
||||
#### Scenario: TargetBoard 传递 columns
|
||||
- **WHEN** TargetBoard 渲染子组件
|
||||
- **THEN** TargetBoard SHALL 调用 `createTargetTableColumns` 生成列定义并传递给每个 TargetGroup
|
||||
|
||||
### Requirement: 列定义复用
|
||||
所有分组的表格 SHALL 共享同一套列定义常量。
|
||||
|
||||
#### Scenario: 列定义提取为常量
|
||||
- **WHEN** 多个分组表格渲染
|
||||
- **THEN** 列定义 SHALL 从独立的 constants/target-table-columns.tsx 导入,不在组件中重复定义
|
||||
@@ -0,0 +1,13 @@
|
||||
## REMOVED Requirements
|
||||
|
||||
### Requirement: 类型显示名称映射
|
||||
**Reason**: 前端不再维护 type → label 的静态映射,直接使用后端返回的 type 原始文本展示。类型筛选器列表改由 meta API 动态驱动。
|
||||
**Migration**: 所有使用 `getTargetTypeDisplay(type)` 的位置改为直接使用 `type` 字符串。`TARGET_TYPE_DISPLAY` 常量和 `target-type-display.ts` 文件删除。
|
||||
|
||||
### Requirement: 映射可扩展性
|
||||
**Reason**: 不再需要前端映射扩展机制,新增 checker 类型时后端注册即自动通过 meta API 暴露给前端。
|
||||
**Migration**: 无需迁移,删除即可。
|
||||
|
||||
### Requirement: 类型安全
|
||||
**Reason**: 不再有映射常量,无需 TypeScript 类型推导和 fallback 逻辑。
|
||||
**Migration**: 无需迁移,删除即可。
|
||||
48
openspec/changes/frontend-architecture-refactor/tasks.md
Normal file
48
openspec/changes/frontend-architecture-refactor/tasks.md
Normal file
@@ -0,0 +1,48 @@
|
||||
## 1. 后端 Meta API
|
||||
|
||||
- [ ] 1.1 在 `src/shared/api.ts` 中新增 `MetaResponse` 类型定义
|
||||
- [ ] 1.2 创建 `src/server/routes/meta.ts`,实现 `handleMeta` 从 `checkerRegistry.supportedTypes` 返回数据
|
||||
- [ ] 1.3 在 `src/server/app.ts` 中注册 `/api/meta` 路由
|
||||
- [ ] 1.4 在 `tests/server/app.test.ts` 中添加 `/api/meta` 端点测试(GET/HEAD/405)
|
||||
|
||||
## 2. 前端 Hook 拆分
|
||||
|
||||
- [ ] 2.1 创建 `src/web/hooks/use-queries.ts`,迁入 `queryKeys`、`fetchJson`、`useSummary`、`useTargets`,新增 `useMeta`
|
||||
- [ ] 2.2 重写 `src/web/hooks/use-target-detail.ts`,仅保留 Drawer 状态管理和条件查询(trend/history)
|
||||
- [ ] 2.3 更新 `src/web/app.tsx` 的 import 路径适配新 hook 文件
|
||||
|
||||
## 3. 前端组件拆分
|
||||
|
||||
- [ ] 3.1 创建 `src/web/utils/stats.ts`,提取统计计算纯函数(`computeTrendStats`)
|
||||
- [ ] 3.2 创建 `src/web/constants/history-table-columns.tsx`,将 `HISTORY_COLUMNS` 从 Drawer 中移出
|
||||
- [ ] 3.3 创建 `src/web/components/OverviewTab.tsx`,从 TargetDetailDrawer 中提取概览面板逻辑
|
||||
- [ ] 3.4 创建 `src/web/components/HistoryTab.tsx`,从 TargetDetailDrawer 中提取记录面板逻辑
|
||||
- [ ] 3.5 精简 `src/web/components/TargetDetailDrawer.tsx`,仅保留 Drawer 壳 + 时间选择 + Tab 切换
|
||||
|
||||
## 4. 类型筛选器动态化
|
||||
|
||||
- [ ] 4.1 将 `src/web/constants/target-table-columns.tsx` 中的 `TARGET_TABLE_COLUMNS` 改为工厂函数 `createTargetTableColumns(checkerTypes)`
|
||||
- [ ] 4.2 从 `src/web/constants/target-table-filters.ts` 中移除 `typeFilter`(`statusFilter` 保留)
|
||||
- [ ] 4.3 更新 `src/web/components/TargetBoard.tsx`,调用 `useMeta` + `useMemo` 生成列定义并传递给 TargetGroup
|
||||
- [ ] 4.4 更新 `src/web/components/TargetGroup.tsx`,新增 `columns` prop 替代静态导入
|
||||
- [ ] 4.5 删除 `src/web/constants/target-type-display.ts`
|
||||
- [ ] 4.6 更新 `src/web/components/TargetDetailDrawer.tsx` 标题栏,直接使用 `target.type` 替代 `getTargetTypeDisplay`
|
||||
|
||||
## 5. 小问题修复
|
||||
|
||||
- [ ] 5.1 修复 `StatusDonut.tsx`:Cell key 从 `index` 改为 `data[index].name`
|
||||
- [ ] 5.2 修复 `StatusBar.tsx`:新增 `maxSlots` prop(默认 30),用 prop 驱动格数渲染
|
||||
- [ ] 5.3 修复 `TrendChart.tsx`:移除 `loading` prop,仅保留 `data` prop
|
||||
|
||||
## 6. 测试补充与更新
|
||||
|
||||
- [ ] 6.1 创建 `tests/web/utils/time.test.ts`,测试 `subtractHours`(正常、跨天、跨月、0 小时)
|
||||
- [ ] 6.2 创建 `tests/web/utils/stats.test.ts`,测试 `computeTrendStats` 纯函数
|
||||
- [ ] 6.3 更新 `tests/web/constants/target-table-filters.test.ts`,移除 `typeFilter` 相关测试
|
||||
- [ ] 6.4 删除 `tests/web/constants/target-type-display.test.ts`
|
||||
- [ ] 6.5 创建 `tests/web/constants/target-table-columns.test.ts`,测试 `createTargetTableColumns` 工厂函数
|
||||
|
||||
## 7. 质量保障与文档
|
||||
|
||||
- [ ] 7.1 执行 `bun run check` 确保类型检查、lint、测试全部通过
|
||||
- [ ] 7.2 更新 DEVELOPMENT.md 中前端目录结构、组件清单和 hook 说明
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-05-13
|
||||
93
openspec/changes/http-checker-quality-hardening/design.md
Normal file
93
openspec/changes/http-checker-quality-hardening/design.md
Normal file
@@ -0,0 +1,93 @@
|
||||
## Context
|
||||
|
||||
HTTP checker 是 DiAL 拨测系统的核心 runner 之一,负责对 HTTP 目标执行请求并校验响应。经审查发现以下质量问题:
|
||||
|
||||
1. **actual 值截断格式不符合 spec**:spec 要求 failure 中的 actual 摘要需截断并附带字符计数,但当前 `truncateActual` 函数只加省略号无计数,导致用户无法判断原始响应体规模。
|
||||
2. **ReDoS 风险**:用户配置的 regex body 规则和 match operator 直接对大响应体执行 `new RegExp().test()`,恶意或不当正则可能导致 CPU 阻塞。
|
||||
3. **JSON 重复解析**:多条 json body 规则各自独立调用 `JSON.parse(body)`,对大 JSON 响应体造成不必要的重复开销。
|
||||
4. **CSS 规则分支冗余**:`checkCssRule` 中"无 operator 时检查元素存在"和"exists: true"是重复逻辑。
|
||||
5. **重定向测试不足**:303、307/308、相对路径 Location 等分支缺少测试覆盖。
|
||||
|
||||
当前代码结构:
|
||||
- `src/server/checker/expect/failure.ts` — failure 构造函数
|
||||
- `src/server/checker/runner/http/body.ts` — body 规则检查
|
||||
- `src/server/checker/runner/http/execute.ts` — HTTP 执行主流程
|
||||
- `src/server/checker/expect/operator.ts` — operator 匹配逻辑
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- 实现 failure actual 值截断,满足 spec 要求
|
||||
- 消除 regex 相关的 ReDoS 风险
|
||||
- 优化多条 JSON 规则的解析性能
|
||||
- 精简冗余代码分支
|
||||
- 补全重定向和集成测试覆盖
|
||||
|
||||
**Non-Goals:**
|
||||
- 不改变 CheckResult / CheckFailure 的类型结构(截断在构造时完成,对外接口不变)
|
||||
- 不引入新依赖
|
||||
- 不改变 HTTP checker 的功能行为(纯内部质量改进)
|
||||
- 不添加 response timing 分段记录(暂缓)
|
||||
- 不添加重试机制(拨测场景下重试会掩盖网络问题信号)
|
||||
|
||||
## Decisions
|
||||
|
||||
### Decision 1: actual 截断在 mismatchFailure 构造点统一实施
|
||||
|
||||
**选择**:在 `expect/failure.ts` 的 `mismatchFailure` 函数内部对 actual 参数截断,阈值 200 字符。
|
||||
|
||||
**替代方案**:
|
||||
- 在存储层(store.ts insertCheckResult)截断 — 但这样 API 实时返回的 failure 仍然很大
|
||||
- 在每个调用点手动截断 — 分散且容易遗漏
|
||||
|
||||
**理由**:构造点截断是最集中的拦截位置,所有 mismatch failure 都经过此函数,一处修改全局生效。expected 值不截断(来自用户配置,通常很短)。
|
||||
|
||||
**截断格式**:`<前 200 字符>…(共 N 字符)` — 保留前缀便于诊断,附带总长度便于判断规模(省略号为单字符 U+2026)。
|
||||
|
||||
### Decision 2: ReDoS 防护使用正则复杂度静态检测
|
||||
|
||||
**选择**:在启动期 validate 阶段对 regex body 规则和 match operator 进行静态复杂度检测,拒绝含有嵌套量词等危险模式的正则。运行期不做额外防护。
|
||||
|
||||
**替代方案**:
|
||||
- 运行期用 AbortSignal + setTimeout 强制中断 — Bun 的 RegExp 执行不可中断,无法实现
|
||||
- 使用 safe-regex 库 — 引入新依赖,违反项目规范
|
||||
- 限制正则执行的输入长度 — 会影响正常大响应体的匹配
|
||||
|
||||
**理由**:自行实现轻量级检测函数,检查常见 ReDoS 模式(嵌套量词 `(a+)+`、重叠交替 `(a|a)*`)。在 validate 阶段拒绝危险正则,比运行期防护更可靠——配置错误应该在启动时暴露。
|
||||
|
||||
**检测规则**:
|
||||
- 嵌套量词:量词内包含量词(如 `(a+)+`、`(a*)*`、`(a+)*`)
|
||||
- 重叠字符类交替后跟量词:`(x|x)+` 模式
|
||||
|
||||
### Decision 3: JSON parse 结果缓存在 checkBodyExpect 层
|
||||
|
||||
**选择**:在 `checkBodyExpect` 函数中,首次遇到 json 规则时执行 `JSON.parse`,将结果缓存并传递给后续 json 规则复用。
|
||||
|
||||
**实现方式**:修改 `checkSingleBodyRule` 签名,接受可选的 `parsedJson` 参数;在 `checkBodyExpect` 循环中维护一个 `let parsedJson: { ok: boolean; value?: unknown; error?: string }` 状态。
|
||||
|
||||
**理由**:改动最小,不改变外部接口,只在内部传递缓存。对于非 json 规则(contains、regex、css、xpath)无影响。
|
||||
|
||||
### Decision 4: CSS 规则分支合并策略
|
||||
|
||||
**选择**:将 `checkCssRule` 重构为线性流程:
|
||||
1. 解析 HTML
|
||||
2. 处理 `exists: false`(元素不存在即通过)
|
||||
3. 查找元素(不存在则失败)
|
||||
4. 处理 `exists: true`(到这里已确认存在,直接通过)
|
||||
5. 提取值(attr 或 text)
|
||||
6. 无 operator 时检查值非 undefined 即通过
|
||||
7. 有 operator 时执行匹配
|
||||
|
||||
**理由**:消除当前三层嵌套判断中的重复逻辑,使控制流线性化,更易理解和维护。
|
||||
|
||||
### Decision 5: execute.ts 提前 duration 检查保留但加注释
|
||||
|
||||
**选择**:保留第 56-74 行的提前 duration 检查逻辑(它是有效的性能优化——避免读取注定超时的 body),但重构为独立的 helper 函数使意图更明确。
|
||||
|
||||
**理由**:删除它会导致超时场景下仍然读取完整 body 后才报错,浪费网络带宽和时间。提取为 `checkEarlyTimeout` 函数名即可自解释。
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **ReDoS 静态检测的误报**:过于严格的检测可能拒绝合法但看起来复杂的正则。→ 缓解:只检测最常见的嵌套量词模式,不做过度分析;提供清晰的错误信息指导用户修改。
|
||||
- **actual 截断丢失诊断信息**:截断后用户无法看到完整 actual 值。→ 缓解:200 字符的前缀通常足够定位问题;如需完整响应体,用户应直接请求目标 URL 查看。
|
||||
- **JSON parse 缓存的内存占用**:对于大 JSON 响应体,缓存的 parsed 对象会在整个 body rules 检查期间驻留内存。→ 缓解:这是短暂的(单次检查周期内),且原本每条规则都会各自 parse 一份,缓存反而减少了峰值内存。
|
||||
28
openspec/changes/http-checker-quality-hardening/proposal.md
Normal file
28
openspec/changes/http-checker-quality-hardening/proposal.md
Normal file
@@ -0,0 +1,28 @@
|
||||
## Why
|
||||
|
||||
HTTP checker 经过审查发现若干质量问题:failure 中 actual 值截断格式不符合 spec 要求(缺少字符计数)导致诊断信息不完整、regex 规则缺少 ReDoS 防护存在 CPU 阻塞风险、多条 JSON body 规则重复 parse 造成不必要开销、CSS 规则分支冗余、重定向测试覆盖不足。需要统一修复以提升健壮性和代码质量。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 修正 `mismatchFailure` 中 actual 值截断格式,添加字符计数信息,格式为 `前 N 字符…(共 M 字符)`
|
||||
- 为 regex body 规则和 match operator 添加 ReDoS 防护(执行超时或正则复杂度检测)
|
||||
- 优化多条 JSON body 规则共享同一次 `JSON.parse` 结果,避免重复解析
|
||||
- 精简 `body.ts` 中 `checkCssRule` 的冗余分支逻辑
|
||||
- 精简 `execute.ts` 中提前 duration 检查的代码结构
|
||||
- 补充重定向相关测试:303 method 转换、307/308 保持 method、相对路径 Location、混合 body rules 集成测试
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
### Modified Capabilities
|
||||
- `expect-body-checkers`: 新增 actual 值截断的具体实现要求(spec 已声明但未细化截断阈值和格式)
|
||||
|
||||
## Impact
|
||||
|
||||
- `src/server/checker/expect/failure.ts` — 新增截断逻辑
|
||||
- `src/server/checker/runner/http/body.ts` — JSON parse 优化、CSS 分支精简
|
||||
- `src/server/checker/runner/http/execute.ts` — duration 检查精简
|
||||
- `src/server/checker/expect/operator.ts` — match operator ReDoS 防护
|
||||
- `tests/server/checker/runner/http/runner.test.ts` — 补充重定向和集成测试
|
||||
- `tests/server/checker/runner/shared/body.test.ts` — 补充截断相关测试
|
||||
@@ -0,0 +1,101 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
> 注:仅展示变更的 scenarios,其余 scenarios 保持不变
|
||||
|
||||
### Requirement: 结构化 expect 失败信息
|
||||
系统 SHALL 在任一 expect 规则失败时生成结构化 failure,用于标识失败阶段、规则路径、期望值、实际值和可读错误信息。actual 值 SHALL 在构造时截断至不超过 200 字符,超出部分以省略标记和总字符数替代。expected 值不截断。
|
||||
|
||||
#### Scenario: body 规则失败信息
|
||||
- **WHEN** HTTP target 的 `expect.body[1].json` 规则失败
|
||||
- **THEN** failure SHALL 包含 kind=`mismatch`、phase=`body`、path 指向 `expect.body[1]`,并包含 message
|
||||
|
||||
#### Scenario: actual 值截断
|
||||
- **WHEN** 失败规则的实际值为字符串且长度超过 200 字符
|
||||
- **THEN** failure.actual SHALL 为前 200 字符加 `…(共 N 字符)` 后缀,其中 N 为原始总字符数
|
||||
|
||||
#### Scenario: actual 值未超限
|
||||
- **WHEN** 失败规则的实际值为字符串且长度不超过 200 字符
|
||||
- **THEN** failure.actual SHALL 保留完整原始值,不做截断
|
||||
|
||||
#### Scenario: actual 值为对象或数组
|
||||
- **WHEN** 失败规则的实际值为对象或数组,且 JSON 序列化后长度超过 200 字符
|
||||
- **THEN** failure.actual SHALL 为序列化后前 200 字符加 `…(共 N 字符)` 后缀
|
||||
|
||||
#### Scenario: actual 值为标量
|
||||
- **WHEN** 失败规则的实际值为 number、boolean、null 或 undefined
|
||||
- **THEN** failure.actual SHALL 保留原始值,不做截断
|
||||
|
||||
### Requirement: HTTP expect 规则启动期校验
|
||||
系统 SHALL 在启动期校验 HTTP expect 中已支持字段的类型、格式、未知字段和可编译表达式。HTTP expect、body rule、json/css/xpath rule 和 operator 对象中的未知字段 SHALL 导致启动期配置失败。每个 body rule 对象 MUST 恰好包含 contains、regex、json、css、xpath 中的一种规则类型。纯 operator 对象 MUST 至少包含一个已知 operator;body 提取规则可以不配置 operator,并以路径、元素或节点存在作为通过语义。`equals` operator SHALL 支持任意 JSON value,包括数组和对象。系统 SHALL 在启动期对 regex body 规则和 match operator 的正则表达式进行 ReDoS 安全检测,含有嵌套量词等危险模式的正则 SHALL 导致启动期配置失败。
|
||||
|
||||
#### Scenario: body rule 使用 regex 字段
|
||||
- **WHEN** HTTP target 配置 `expect.body: [{regex: "ok|healthy"}]` 且 regex 可编译且无 ReDoS 风险
|
||||
- **THEN** 系统 SHALL 接受该配置,并在运行期按 regex body 规则匹配响应体
|
||||
|
||||
#### Scenario: body rule 不支持 match 字段
|
||||
- **WHEN** HTTP target 配置 `expect.body: [{match: "ok"}]` 且该规则没有 contains、regex、json、css、xpath 任一支持字段
|
||||
- **THEN** 系统 SHALL 在启动期配置校验失败
|
||||
|
||||
#### Scenario: body rule 未知字段启动失败
|
||||
- **WHEN** HTTP target 配置 `expect.body: [{contains: "ok", note: "ignored"}]`
|
||||
- **THEN** 系统 SHALL 在启动期配置校验失败,提示 `note` 是未知字段
|
||||
|
||||
#### Scenario: body rule 多支持字段非法
|
||||
- **WHEN** HTTP target 的同一条 body rule 同时配置 contains 和 regex
|
||||
- **THEN** 系统 SHALL 在启动期配置校验失败
|
||||
|
||||
#### Scenario: operator match 正则非法
|
||||
- **WHEN** HTTP target 的 expect.headers、json、css 或 xpath operator 配置了不可编译的 match 正则
|
||||
- **THEN** 系统 SHALL 在启动期配置校验失败
|
||||
|
||||
#### Scenario: operator 数值比较类型非法
|
||||
- **WHEN** HTTP target 的 expect operator 配置 gt、gte、lt 或 lte,且对应值不是有限数字
|
||||
- **THEN** 系统 SHALL 在启动期配置校验失败
|
||||
|
||||
#### Scenario: operator 布尔类型非法
|
||||
- **WHEN** HTTP target 的 expect operator 配置 empty 或 exists,且对应值不是布尔值
|
||||
- **THEN** 系统 SHALL 在启动期配置校验失败
|
||||
|
||||
#### Scenario: JSONPath 子集非法
|
||||
- **WHEN** HTTP target 的 json body rule path 不符合系统支持的 JSONPath 子集
|
||||
- **THEN** 系统 SHALL 在启动期配置校验失败
|
||||
|
||||
#### Scenario: operator 未知字段非法
|
||||
- **WHEN** HTTP target 的 expect operator 配置了 `foo: "bar"` 等未知 operator 字段
|
||||
- **THEN** 系统 SHALL 在启动期配置校验失败
|
||||
|
||||
#### Scenario: equals 支持对象
|
||||
- **WHEN** HTTP target 配置 `expect.body: [{json: {path: "$.payload", equals: {status: "ok"}}}]`
|
||||
- **THEN** 系统 SHALL 接受该配置,并在运行期使用深度相等比较提取值和对象期望
|
||||
|
||||
#### Scenario: equals 支持数组
|
||||
- **WHEN** HTTP target 配置 `expect.body: [{json: {path: "$.items", equals: ["a", "b"]}}]`
|
||||
- **THEN** 系统 SHALL 接受该配置,并在运行期使用深度相等比较提取值和数组期望
|
||||
|
||||
#### Scenario: 纯 operator 对象不能为空
|
||||
- **WHEN** HTTP target 的 `expect.headers` 中某个 header 期望配置为空对象 `{}`
|
||||
- **THEN** 系统 SHALL 在启动期配置校验失败,要求显式配置至少一个 operator
|
||||
|
||||
#### Scenario: json rule 允许存在性语义
|
||||
- **WHEN** HTTP target 配置 `expect.body: [{json: {path: "$.status"}}]`
|
||||
- **THEN** 系统 SHALL 接受该配置,并在运行期以 JSONPath 值存在作为通过语义
|
||||
|
||||
#### Scenario: css rule 未知字段非法
|
||||
- **WHEN** HTTP target 配置 `expect.body: [{css: {selector: "h1", unknown: true}}]`
|
||||
- **THEN** 系统 SHALL 在启动期配置校验失败,提示未知字段
|
||||
|
||||
#### Scenario: xpath rule 未知字段非法
|
||||
- **WHEN** HTTP target 配置 `expect.body: [{xpath: {path: "/html/body", unknown: true}}]`
|
||||
- **THEN** 系统 SHALL 在启动期配置校验失败,提示未知字段
|
||||
|
||||
#### Scenario: regex body 规则含嵌套量词启动失败
|
||||
- **WHEN** HTTP target 配置 `expect.body: [{regex: "(a+)+$"}]`
|
||||
- **THEN** 系统 SHALL 在启动期配置校验失败,提示正则存在 ReDoS 风险
|
||||
|
||||
#### Scenario: match operator 含嵌套量词启动失败
|
||||
- **WHEN** HTTP target 的 expect operator 配置 `{match: "(\\d+)*x"}`
|
||||
- **THEN** 系统 SHALL 在启动期配置校验失败,提示正则存在 ReDoS 风险
|
||||
|
||||
#### Scenario: 安全正则通过校验
|
||||
- **WHEN** HTTP target 配置 `expect.body: [{regex: "\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}"}]`
|
||||
- **THEN** 系统 SHALL 接受该配置(无嵌套量词,无 ReDoS 风险)
|
||||
38
openspec/changes/http-checker-quality-hardening/tasks.md
Normal file
38
openspec/changes/http-checker-quality-hardening/tasks.md
Normal file
@@ -0,0 +1,38 @@
|
||||
## 1. failure actual 截断
|
||||
|
||||
- [ ] 1.1 修改 `src/server/checker/expect/failure.ts` 中 `truncateActual` 函数,截断后缀从 `...` 改为 `…(共 N 字符)`,其中省略号为单字符 U+2026
|
||||
- [ ] 1.2 更新 `tests/server/checker/runner/shared/failure.test.ts` 中截断相关测试断言,匹配新格式(检查省略号为单字符且带字符计数)
|
||||
|
||||
## 2. ReDoS 防护
|
||||
|
||||
- [ ] 2.1 在 `src/server/checker/expect/` 下新增 `redos.ts`,实现 `isUnsafeRegex(pattern: string): boolean` 函数,检测嵌套量词模式
|
||||
- [ ] 2.2 在 `src/server/checker/runner/http/validate.ts` 的 `validateRegexRule` 和 `src/server/checker/expect/validate-operator.ts` 的 match 校验中调用 `isUnsafeRegex`,不安全时返回 issue
|
||||
- [ ] 2.3 在 `tests/server/checker/runner/http/runner.test.ts` 中补充 ReDoS 正则启动校验失败的测试用例
|
||||
- [ ] 2.4 在 `tests/server/checker/runner/shared/` 下新增 `redos.test.ts`,覆盖常见 ReDoS 模式和安全正则的判定
|
||||
|
||||
## 3. JSON parse 优化
|
||||
|
||||
- [ ] 3.1 修改 `src/server/checker/runner/http/body.ts` 中 `checkBodyExpect` 函数,维护 parsedJson 缓存状态,首次 json 规则 parse 后复用结果
|
||||
- [ ] 3.2 修改 `checkJsonRule` 签名接受可选的预解析 JSON 对象,避免重复 `JSON.parse`
|
||||
- [ ] 3.3 在 `tests/server/checker/runner/shared/body.test.ts` 中补充多条 json 规则共享 parse 结果的测试(验证行为正确性)
|
||||
|
||||
## 4. CSS 规则精简
|
||||
|
||||
- [ ] 4.1 重构 `src/server/checker/runner/http/body.ts` 中 `checkCssRule` 为线性流程:解析 HTML → exists:false 短路 → 查找元素 → exists:true 短路 → 提取值 → operator 匹配
|
||||
- [ ] 4.2 确认 `tests/server/checker/runner/shared/body.test.ts` 中现有 CSS 测试全部通过
|
||||
|
||||
## 5. execute.ts 精简
|
||||
|
||||
- [ ] 5.1 将 `src/server/checker/runner/http/execute.ts` 第 56-74 行的提前 duration 检查提取为 `checkEarlyTimeout` 辅助函数,明确意图
|
||||
|
||||
## 6. 补充测试
|
||||
|
||||
- [ ] 6.1 在 `tests/server/checker/runner/http/runner.test.ts` 中补充 303 重定向 method 转 GET 的测试
|
||||
- [ ] 6.2 在 `tests/server/checker/runner/http/runner.test.ts` 中补充 307/308 保持原始 method 和 body 的测试
|
||||
- [ ] 6.3 在 `tests/server/checker/runner/http/runner.test.ts` 中补充相对路径 Location header 重定向的测试
|
||||
- [ ] 6.4 在 `tests/server/checker/runner/http/runner.test.ts` 中补充混合 body rules(contains + json + css)集成测试
|
||||
|
||||
## 7. 质量保障
|
||||
|
||||
- [ ] 7.1 执行完整测试套件 `bun test`、代码检查 `bun run lint`、格式检查 `bun run format:check` 确保无回归
|
||||
- [ ] 7.2 更新 DEVELOPMENT.md 中 ReDoS 校验相关说明(如有必要)
|
||||
Reference in New Issue
Block a user