Reorganize internal packages into platform/infra/shared layers and update imports, docs, and seed-count tests to match current system configs.
30 KiB
WAF 可编排规则实现计划
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: 将固定顺序的 WAF 规则组重构为可用 React Flow 编辑、发布时编译、OpenResty 内存执行的有序 DAG 规则系统。
Architecture: 控制面以带修订号的版本化 JSON 保存整张编辑态图,Server 保存与发布时执行同一套强校验并编译为精简运行态 DAG。规则仅随配置发布和 OpenResty reload 加载一次;动态 IP 组由协调 Worker 每 5 秒检查 checksum,变化时更新共享快照和各 Worker 本地内存对象。
Tech Stack: Go 1.25、Gin、GORM、goose、PostgreSQL/SQLite、OpenResty Lua、Next.js 16 App Router、React 19、TypeScript、@xyflow/react、TanStack Query、shadcn/ui、Vitest。
实现状态(2026-07-13)
Tasks 1–11 已实现,包含三段数据库迁移、图模型与编译器、规则 API、发布快照、OpenResty 内存执行器、IP 组协调刷新、React Flow 编辑器、有序绑定、GeoLite2 City/Country 支持以及中文文档与 Swagger 更新。React Flow 画布使用本地受控节点状态处理拖动,并支持显式或键盘删除普通节点与连线。Country 与 City MMDB 均随 Agent 内嵌,缺失文件在启动时从程序内初始化,网络仅用于后续周期更新。地域属性栏使用完整国家与 ISO 3166-2 一级行政区数据,国家同时展示中文名称与代码,行政区支持按名称或代码搜索。发布器保证空规则绑定编码为 [],Lua 运行时兼容旧快照中的 null 数组,避免未启用或空绑定规则导致请求 500。PoW 节点通过共享内存暂存配置,并以 ngx.exec 显式参数把配置键传入内部挑战处理器,避免内部重定向丢失请求上下文后误报节点未执行。
当前工作区已完成 go test ./...、前端全量 Vitest(56 项)、make swagger、make code-check 与 git diff --check 验证。Next.js 生产构建在本机持续停留于 Turbopack 的 Creating an optimized production build ...,未返回编译错误或成功状态,故不计为通过。
Global Constraints
- 每张图恰好一个
start和一个allow;block可多个;图必须无环、无悬空、无不可达节点,所有路径必须抵达allow或block。 ip_match与geo_match只输出true/false,分别表示匹配与未匹配;pow只输出next。- 全局规则固定前置;路由自定义规则按绑定
sequence升序执行;当前规则allow后继续下一条,block立即终止。 - 规则运行态 JSON 仅在 OpenResty reload 后由 Worker 加载一次;请求路径禁止文件 I/O、checksum 和 JSON 解码。
- IP 组请求路径始终读取 Worker 本地对象;整个实例每 5 秒最多一个 Worker 检查 checksum。
- 迁移只保留规则名称、全局标记、启用状态和绑定关系,旧策略统一重置为
开始 → 通过。 - 所有 HTTP 路由仅在
internal/router/router.go的既有分发体系中通过internal/router/v1/openflare/register_waf.go注册;API 失败使用response.Abort*。 - API Handler 变化后运行
make swagger;代码完成后运行make code-check;代码变更写入docs/changelog/index.md的[Unreleased]。 - 前端不得删除
frontend/node_modules;使用 shadcn variant 与全局 CSS 变量,页面根容器保持w-full py-6 px-1。 - 实现前阅读
.agent/skills/database-migration/SKILL.md、.agent/skills/new-api/SKILL.md、.agent/skills/shadcn/SKILL.md以及frontend/node_modules/next/dist/docs/01-app/03-api-reference/03-file-conventions/page.md等匹配的 Next.js 本地文档。
1. 目标与背景
当前 OpenFlareWAFRuleGroup 把 IP/地域黑白名单、PoW 与阻止响应平铺为固定字段,Lua 按硬编码顺序判断,无法表达用户自定义分支。本次交付包含图模型、强校验、API、迁移、发布编译、Lua DAG 执行、有序绑定、IP 组五秒内存刷新和 React Flow 编辑器;不包含循环、脚本节点、表达式节点、子图和跨规则跳转。
2. 数据与控制流
flowchart LR
UI[React Flow 编辑态图] -->|revision + graph| API[Server 校验与保存]
API --> DB[(规则 graph JSON)]
DB --> PUB[发布编译器]
PUB --> SNAP[运行态 DAG 快照]
SNAP --> AGENT[Agent 原子落盘]
AGENT -->|reload| LUA[OpenResty Worker 内存]
IPS[IP 组异步更新] -->|JSON 后 checksum| TIMER[5 秒协调定时器]
TIMER --> SHM[ngx.shared 原始快照]
SHM --> LUA
3. 文件结构与职责
internal/apps/openflare/waf/graph_types.go:编辑态图、节点配置、运行态图和默认图类型。internal/apps/openflare/waf/graph_validate.go:结构、端口、配置、引用、可达性和终止性校验。internal/apps/openflare/waf/graph_compile.go:删除 UI 字段、编译索引化 DAG、收集 IP 组引用。internal/apps/openflare/waf/rule_logics.go:规则元数据、修订保存和有序绑定逻辑;从现有过大的logics.go中抽离规则职责。internal/apps/openflare/waf/rule_routers.go:规则 API Handler 与 Swagger;IP 组 Handler 留在现有文件或后续独立拆分。internal/apps/agent/nginx/waf_assets.go:只负责嵌入 Lua 文件;实际 Lua 拆到internal/apps/agent/nginx/waf_runtime.lua与waf_ip_groups.lua并使用go:embed,避免继续膨胀 Go 字符串。frontend/app/(main)/waf/page.tsx:规则/IP 组列表和仅名称创建流程。frontend/app/(main)/waf/rules/editor/page.tsx:静态可导出的编辑器路由入口,通过查询参数读取规则 ID,避免 Next 静态导出的动态参数限制。frontend/app/(main)/waf/rules/editor/components/:画布、节点、节点库、属性栏和校验提示,单文件保持低于 600 行。
Task 1: 数据库迁移与持久化模型
Files:
- Create:
internal/infra/persistence/migrator/goose/postgres/202607150001_orchestrate_waf_rules.sql - Create:
internal/infra/persistence/migrator/goose/sqlite/202607150001_orchestrate_waf_rules.sql - Create:
internal/infra/persistence/migrator/goose/postgres/202607150002_reset_waf_rule_graphs.sql - Create:
internal/infra/persistence/migrator/goose/sqlite/202607150002_reset_waf_rule_graphs.sql - Modify:
internal/model/openflare_waf.go - Create:
internal/model/openflare_waf_graph_test.go
Interfaces:
-
Produces:
Graph string,Revision uint64,Sequence int;UpdateOpenFlareWAFRuleGraph(ctx, id, revision, graph) (uint64, error);绑定查询按sequence, id排序。 -
Consumes: 现有
OpenFlareWAFRuleGroup与OpenFlareWAFRuleGroupBinding。 -
Step 1: 阅读数据库迁移 Skill 并写迁移失败测试
测试建立旧 Schema、插入两个规则和无序绑定、执行迁移后断言图统一为默认图、revision = 1、绑定顺序稳定。测试核心断言:
require.JSONEq(t, `{"schema_version":1,"nodes":[{"id":"start","type":"start","position":{"x":0,"y":0},"config":{}},{"id":"allow","type":"allow","position":{"x":320,"y":0},"config":{}}],"edges":[{"id":"start-allow","source":"start","source_handle":"next","target":"allow"}]}`, group.Graph)
assert.Equal(t, uint64(1), group.Revision)
assert.Equal(t, []int{0, 1}, []int{bindings[0].Sequence, bindings[1].Sequence})
- Step 2: 运行模型测试确认失败
Run: go test ./internal/model -run 'TestOpenFlareWAFGraph|TestReplaceOpenFlareWAFRuleGroupBindings' -count=1
Expected: FAIL,缺少新字段或迁移列。
- Step 3: 编写 PostgreSQL 与 SQLite goose 迁移
202607150001 只执行 DDL:两端都增加 graph TEXT NOT NULL、revision BIGINT/INTEGER NOT NULL DEFAULT 1、sequence INTEGER NOT NULL DEFAULT 0。202607150002 只执行 DML:用确定性的 id 顺序为每个 proxy_route_id 回填 sequence,并将所有 graph 重置为同一默认 JSON。Down 分别恢复数据语义与旧列结构;不要创建物理外键,禁止把 DDL 与 DML 放入同一个迁移文件。
- Step 4: 实现乐观锁与有序绑定模型方法
var ErrWAFRuleRevisionConflict = errors.New("waf rule revision conflict")
func UpdateOpenFlareWAFRuleGraph(ctx context.Context, id uint, revision uint64, graph string) (uint64, error) {
result := db.DB(ctx).Model(&OpenFlareWAFRuleGroup{}).
Where("id = ? AND revision = ?", id, revision).
Updates(map[string]any{"graph": graph, "revision": gorm.Expr("revision + 1")})
if result.Error != nil { return 0, result.Error }
if result.RowsAffected != 1 { return 0, ErrWAFRuleRevisionConflict }
return revision + 1, nil
}
绑定替换在事务中按输入下标写 Sequence: index;查询显式 Order("sequence asc").Order("id asc")。
- Step 5: 运行测试并提交
Run: go test ./internal/model ./internal/infra/persistence/migrator/... -count=1
Expected: PASS。
Commit: feat(waf): add graph persistence and binding order
Task 2: 图类型、默认图和强校验器
Files:
- Create:
internal/apps/openflare/waf/graph_types.go - Create:
internal/apps/openflare/waf/graph_validate.go - Create:
internal/apps/openflare/waf/graph_validate_test.go - Modify:
internal/apps/openflare/waf/errs.go
Interfaces:
-
Produces:
RuleGraph,RuleNode,RuleEdge,DefaultRuleGraph() RuleGraph,ValidateRuleGraph(ctx context.Context, graph RuleGraph, ipGroupExists func(context.Context, uint) (bool, error)) error。 -
Consumes: Task 1 的 JSON 持久化字段。
-
Step 1: 写表驱动失败测试
覆盖合法默认图、重复 start/allow、环、不可达节点、悬空端口、错误 handle、同 handle 多目标、无终止路径、未知类型、无效 IP/CIDR、缺失 IP 组、非法国家/地区、PoW 范围和超限图。
tests := []struct{name string; mutate func(*RuleGraph); want string}{
{"cycle", addCycle, "规则图不能包含循环"},
{"missing false edge", removeFalseEdge, "节点 match-1 的 false 出口未连接"},
{"unreachable", addUnreachableNode, "节点 orphan 无法从开始节点到达"},
}
- Step 2: 运行测试确认失败
Run: go test ./internal/apps/openflare/waf -run TestValidateRuleGraph -count=1
Expected: FAIL,类型和校验函数不存在。
- Step 3: 定义带判别联合的图类型
type RuleNodeType string
const (
RuleNodeStart RuleNodeType = "start"
RuleNodeAllow RuleNodeType = "allow"
RuleNodeBlock RuleNodeType = "block"
RuleNodeIPMatch RuleNodeType = "ip_match"
RuleNodeGeoMatch RuleNodeType = "geo_match"
RuleNodePoW RuleNodeType = "pow"
)
type RuleGraph struct { SchemaVersion int `json:"schema_version"`; Nodes []RuleNode `json:"nodes"`; Edges []RuleEdge `json:"edges"` }
type RuleEdge struct { ID, Source, SourceHandle, Target string }
RuleNode.Config 先用 json.RawMessage 解码到明确的 IPMatchConfig、GeoMatchConfig、PoWNodeConfig、BlockNodeConfig,禁止透传未知字段。
- Step 4: 实现结构和 DFS/Kahn 校验
先检查大小、ID、类型、端口和配置,再用 Kahn 检测环,用从 start 的 DFS 检测可达性,用反向图从所有终止节点遍历检测终止性。错误文案携带节点/边 ID,供前端定位。
- Step 5: 运行测试并提交
Run: go test ./internal/apps/openflare/waf -run 'Test(DefaultRuleGraph|ValidateRuleGraph)' -count=1
Expected: PASS。
Commit: feat(waf): validate composable rule graphs
Task 3: 运行态图编译器
Files:
- Create:
internal/apps/openflare/waf/graph_compile.go - Create:
internal/apps/openflare/waf/graph_compile_test.go
Interfaces:
-
Produces:
CompileRuleGraph(graph RuleGraph) (RuntimeRuleGraph, error)、ReferencedIPGroupIDs(graph RuleGraph) []uint。 -
Consumes: Task 2 的已校验图类型。
-
Step 1: 写编译快照测试
断言位置和显示名不进入 JSON、节点通过 ID map O(1) 查找、出口按 handle 编译、IP 组 ID 去重排序。
compiled, err := CompileRuleGraph(graph)
require.NoError(t, err)
assert.Equal(t, "start", compiled.Entry)
assert.Equal(t, "allow", compiled.Nodes["match"].Next["true"])
assert.Equal(t, []uint{2, 7}, ReferencedIPGroupIDs(graph))
- Step 2: 运行测试确认失败
Run: go test ./internal/apps/openflare/waf -run 'TestCompileRuleGraph|TestReferencedIPGroupIDs' -count=1
Expected: FAIL,编译接口不存在。
- Step 3: 实现确定性编译
输出结构只保留 entry、按节点 ID 索引的类型化运行配置和 handle→target 映射;序列化前对可排序切片排序,确保相同图生成相同快照和 checksum。
- Step 4: 运行测试并提交
Run: go test ./internal/apps/openflare/waf -run 'TestCompileRuleGraph|TestReferencedIPGroupIDs' -count=1
Expected: PASS。
Commit: feat(waf): compile rule graphs for runtime
Task 4: 规则 API、修订冲突与有序绑定
Files:
- Create:
internal/apps/openflare/waf/rule_logics.go - Create:
internal/apps/openflare/waf/rule_routers.go - Create:
internal/apps/openflare/waf/rule_logics_test.go - Modify:
internal/apps/openflare/waf/logics.go - Modify:
internal/apps/openflare/waf/routers.go - Modify:
internal/router/v1/openflare/register_waf.go
Interfaces:
-
Produces:
CreateRuleInput{Name string}、SaveRuleGraphInput{Revision uint64; Graph RuleGraph}、UpdateRuleMetaInput{Name string; Enabled bool};创建、详情、元数据、图保存、删除和有序绑定 API。 -
Consumes: Tasks 1–3 的模型、默认图和校验器。
-
Step 1: 阅读 new-api Skill,写逻辑与 Handler 失败测试
覆盖只传名称创建默认图、空名 400、图非法 400、revision 冲突 409、绑定顺序往返不变、全局规则不能被路由绑定排序覆盖。
- Step 2: 运行测试确认失败
Run: go test ./internal/apps/openflare/waf ./internal/router/v1/openflare -run 'Test(CreateRule|SaveRuleGraph|ReplaceSiteRuleGroups)' -count=1
Expected: FAIL,API 输入与路由尚未实现。
- Step 3: 实现 logic 与安全错误映射
func SaveRuleGraph(ctx context.Context, id uint, input SaveRuleGraphInput) (*RuleView, error) {
if err := ValidateRuleGraph(ctx, input.Graph, ipGroupExists); err != nil { return nil, err }
raw, err := json.Marshal(input.Graph)
if err != nil { return nil, err }
if _, err = model.UpdateOpenFlareWAFRuleGraph(ctx, id, input.Revision, string(raw)); err != nil { return nil, err }
return GetRule(ctx, id)
}
数据库/编码错误用 pkg/logger 记录后映射 AbortInternal;校验错误用 AbortBadRequest;revision 冲突用 AbortConflict。
- Step 4: 注册路由并补全 Swagger
保留 /rule-groups 路径以减少前端与兼容面变化,但将创建 payload 改为仅名称,新增 POST /rule-groups/:id/graph 与 POST /rule-groups/:id/meta。所有 @Failure 400/404/409/500 与统一 response envelope 完整声明。
- Step 5: 运行测试并提交
Run: go test ./internal/apps/openflare/waf ./internal/router/v1/openflare -count=1
Expected: PASS。
Commit: feat(api): expose orchestrated waf rules
Task 5: 发布快照与规则顺序
Files:
- Modify:
internal/apps/openflare/config_version/snapshot.go - Modify:
internal/apps/openflare/config_version/logics_test.go - Create:
internal/apps/openflare/config_version/waf_graph_snapshot_test.go
Interfaces:
-
Produces: 发布快照中的
rule_groups[].graph运行态 DAG、按 sequence 排序的bindings[].rule_group_ids、图引用 IP 组集合。 -
Consumes: Task 3 编译器与 Task 1 有序绑定。
-
Step 1: 写失败测试
建立全局规则和两个自定义图,绑定顺序 [customB, customA],断言快照保持该顺序、运行图无 position、只包含图引用的 IP 组;非法启用图阻止预览/发布。
- Step 2: 运行测试确认失败
Run: go test ./internal/apps/openflare/config_version -run 'TestWAFGraphSnapshot|TestBuildSnapshotRejectsInvalidWAFGraph' -count=1
Expected: FAIL,快照仍输出旧固定字段并按 ID 排序。
- Step 3: 替换固定字段快照编译
删除 snapshotWAFRuleGroup 的旧黑白名单/PoW 字段,加入 Graph waf.RuntimeRuleGraph;buildSnapshotWAFIPGroups 从所有编辑图的 ReferencedIPGroupIDs 聚合;绑定不再按 group ID 排序。
- Step 4: 运行测试并提交
Run: go test ./internal/apps/openflare/config_version ./internal/apps/openflare/integration -count=1
Expected: PASS。
Commit: feat(waf): publish ordered runtime graphs
Task 6: OpenResty 内存 DAG 执行器
Files:
- Create:
internal/apps/agent/nginx/waf_runtime.lua - Create:
internal/apps/agent/nginx/waf_runtime_spec.lua - Modify:
internal/apps/agent/nginx/waf_assets.go - Modify:
internal/apps/agent/nginx/waf_assets_test.go - Modify:
internal/apps/agent/nginx/manager.go - Modify:
internal/apps/agent/nginx/manager_test.go - Modify:
internal/apps/agent/nginx/pow_assets.go
Interfaces:
-
Produces:
require("waf.runtime").check(),模块加载时读取一次规则配置,请求时执行内存 DAG。 -
Consumes: Task 5 的运行态快照;现有 PoW challenge/session 代码。
-
Step 1: 写 Lua 执行器失败测试
用 stub ngx 覆盖 IP true/false、地域 true/false、PoW 接管/完成、多个 block 响应、全局前置、自定义顺序、未知节点 fail-closed、请求期间 io.open 调用次数为 0。
- Step 2: 运行测试确认失败
Run: go test ./internal/apps/agent/nginx -run 'TestWAFRuntime' -count=1
Expected: FAIL,运行时仍为固定链且每次请求读取配置。
- Step 3: 将规则加载移到 Lua 模块初始化
local rules_config = assert(load_json_once(runtime_dir .. "/waf_config.json"))
function _M.check()
local rules = active_rules_for_site(rules_config, ngx.var.openflare_waf_site or "")
for _, rule in ipairs(rules) do
local decision = execute_graph(rule.graph)
if decision.kind == "block" then return render_block(decision.config) end
end
end
在 manager.go 生成的 http 块中显式加入 init_worker_by_lua_block { require("waf.runtime").init() },使新 Worker 在 reload 启动阶段完成读取与解析,而不是推迟到首个请求。模块缓存使每个 Worker 只解析一次;执行器设置最大步数为节点数,任何损坏图都记录限频错误并返回阻止响应。
- Step 4: 将 PoW 变为节点执行接口
抽取现有 PoW runtime 为 pow.evaluate(config):完成返回 true,未完成直接输出/重定向挑战并返回接管标记。移除“按站点选择第一个 pow_enabled 规则”的旧扫描逻辑。
- Step 5: 运行测试并提交
Run: go test ./internal/apps/agent/nginx ./internal/apps/agent/sync -count=1
Expected: PASS。
Commit: feat(agent): execute waf graphs from worker memory
Task 7: IP 组 checksum 与五秒内存刷新
Files:
- Create:
internal/apps/agent/nginx/waf_ip_groups.lua - Create:
internal/apps/agent/nginx/waf_ip_groups_spec.lua - Modify:
internal/apps/agent/sync/service.go - Modify:
internal/apps/agent/sync/service_test.go - Modify:
internal/apps/agent/nginx/waf_assets.go
Interfaces:
-
Produces:
waf_ip_groups.json.checksum;Luaip_groups.current()返回 Worker 本地对象;协调刷新间隔固定 5 秒。 -
Consumes: 现有 Agent IP 组同步 payload 与独立的
ngx.shared.openflare_waf_ip_groups(64 MiB);完整运行时快照上限为 20 MiB。 -
Step 1: 写失败测试
断言 Agent 先原子替换 JSON、最后原子替换 checksum;Lua 稳定状态 15 秒只读取 checksum 3 次且不读 JSON;变化时全实例只读一次 JSON;非法新 JSON 保留旧对象。
- Step 2: 运行测试确认失败
Run: go test ./internal/apps/agent/sync ./internal/apps/agent/nginx -run 'TestWAFIPGroup(Checksum|Refresh)' -count=1
Expected: FAIL,checksum sidecar 和定时器不存在。
- Step 3: Agent 写 checksum sidecar
checksum 使用 Agent 已有快照 checksum;写入采用同目录临时文件、fsync/close、rename 的现有原子文件工具。严格顺序为 JSON rename 成功后 checksum rename。
- Step 4: 实现协调 Worker 刷新
local function tick(premature)
if premature then return end
local ok = shared:add("ip_refresh_lock", true, 4)
if ok then refresh_from_checksum() end
adopt_shared_snapshot_if_changed()
end
ngx.timer.every(5, tick)
协调 Worker 变化时把 raw JSON 和 checksum 写共享字典;每个 Worker 只在 shared version 变化时 decode 到模块局部 current_groups。请求只调用 ip_groups.current()。
- Step 5: 运行测试并提交
Run: go test ./internal/apps/agent/nginx ./internal/apps/agent/sync -count=1
Expected: PASS。
Commit: perf(waf): refresh ip groups by checksum timer
Task 8: 前端类型、Service 与创建流程
Files:
- Modify:
frontend/package.json - Modify:
frontend/pnpm-lock.yaml - Modify:
frontend/lib/services/openflare/types.ts - Modify:
frontend/lib/services/openflare/waf.service.ts - Modify:
frontend/app/(main)/waf/page.tsx - Create:
frontend/app/(main)/waf/components/create-rule-dialog.tsx - Modify:
frontend/app/(main)/waf/components/rule-groups-table.tsx - Delete after replacement:
frontend/app/(main)/waf/components/rule-group-sheet.tsx - Test:
frontend/tests/unit/waf-rule-service.test.ts
Interfaces:
-
Produces: TypeScript 判别联合
WAFRuleNode、WAFRuleGraph、WAFRule;WafService.createRule({name})、saveRuleGraph(id, {revision, graph})。 -
Consumes: Task 4 API。
-
Step 1: 阅读 shadcn 与 Next 本地文档,安装 React Flow
Run: cd frontend && pnpm add @xyflow/react
Expected: package.json 与 lockfile 增加同一版本的 @xyflow/react。
- Step 2: 写 Service 与创建流程失败测试
断言创建 payload 只有 {name},保存包含 revision,创建成功导航到 /waf/rules/editor?id=<id>,不再打开旧规则大表单。
- Step 3: 运行测试确认失败
Run: cd frontend && pnpm vitest run tests/unit/waf-rule-service.test.ts
Expected: FAIL,旧 payload 仍要求固定字段。
- Step 4: 实现类型、Service 和名称对话框
export type WAFRuleNode =
| {id: string; type: 'start'; position: XYPosition; config: Record<string, never>}
| {id: string; type: 'ip_match'; position: XYPosition; config: IPMatchConfig}
| {id: string; type: 'geo_match'; position: XYPosition; config: GeoMatchConfig}
| {id: string; type: 'pow'; position: XYPosition; config: PoWNodeConfig}
| {id: string; type: 'allow'; position: XYPosition; config: Record<string, never>}
| {id: string; type: 'block'; position: XYPosition; config: BlockNodeConfig};
静态方法作为 React Query 回调时继续用箭头函数包裹。列表创建成功后 router.push('/waf/rules/editor?id=' + rule.id)。
- Step 5: 运行测试并提交
Run: cd frontend && pnpm vitest run tests/unit/waf-rule-service.test.ts && pnpm lint
Expected: PASS。
Commit: feat(frontend): create orchestrated waf rules
Task 9: React Flow 编排器与固定属性栏
Files:
- Create:
frontend/app/(main)/waf/rules/editor/page.tsx - Create:
frontend/app/(main)/waf/rules/editor/components/rule-flow-canvas.tsx - Create:
frontend/app/(main)/waf/rules/editor/components/rule-node.tsx - Create:
frontend/app/(main)/waf/rules/editor/components/node-library.tsx - Create:
frontend/app/(main)/waf/rules/editor/components/node-properties.tsx - Create:
frontend/app/(main)/waf/rules/editor/components/graph-validation.ts - Create:
frontend/app/(main)/waf/rules/editor/components/graph-validation.test.ts - Create:
frontend/app/(main)/waf/rules/editor/components/unsaved-changes.tsx
Interfaces:
-
Produces: 全宽 React Flow 编辑器;前端
validateGraph(graph): GraphIssue[];Server 错误节点定位。 -
Consumes: Task 8 类型与 Service。
-
Step 1: 写前端图校验失败测试
覆盖唯一 start/allow、必需 handle、禁止环、不可达、终止性以及删除节点同步删边。
- Step 2: 运行测试确认失败
Run: cd frontend && pnpm vitest run 'app/(main)/waf/rules/editor/components/graph-validation.test.ts'
Expected: FAIL,校验器不存在。
- Step 3: 实现页面骨架和数据状态
page.tsx 直接维护 query、mutation、React Flow nodes/edges、dirty、selection 和右侧栏状态,不创建同名中转容器。根节点使用 w-full py-6 px-1,标题严格使用既定图标和 h1 规范。
- Step 4: 实现节点、handle 和连线约束
start/pow 只显示 next source handle,ip_match/geo_match 显示 true、false,allow/block 只显示 target handle。isValidConnection 阻止错误端口、同端口重复连接和形成环;start/allow 禁止删除。
- Step 5: 实现固定右侧属性栏
属性栏按节点判别联合渲染 IP/CIDR 与 IP 组多选、国家/地区多选、PoW 配置、阻止状态码与 HTML。颜色和阴影通过节点组件 variant/CSS 变量集中定义,不在业务调用点硬编码。
- Step 6: 实现保存、冲突和未保存提示
仅图合法时启用保存;409 显示“规则已在其他页面更新,请重新加载”;Server 返回节点/边 ID 时选中并聚焦;浏览器离开和应用内返回均提示未保存变更。
- Step 7: 运行测试、构建并提交
Run: cd frontend && pnpm vitest run && pnpm lint && pnpm build
Expected: PASS;静态导出包含 /waf/rules/editor。
Commit: feat(frontend): add visual waf rule composer
Task 10: 路由绑定排序 UI 与旧界面清理
Files:
- Modify:
frontend/app/(main)/waf/components/site-binding-sheet.tsx - Modify:
frontend/app/(main)/proxy-routes/detail/components/waf-section.tsx - Modify:
frontend/app/(main)/waf/components/helpers.ts(删除仅旧规则表单使用的导出;若清空则删除文件) - Delete:
frontend/app/(main)/waf/components/pow-config-panel.tsx - Delete:
frontend/app/(main)/waf/components/rule-entry-dialog.tsx - Delete:
frontend/app/(main)/waf/components/rule-list-section.tsx - Test:
frontend/tests/unit/waf-binding-order.test.tsx
Interfaces:
-
Produces: 拖拽或上下移动的有序绑定列表,提交 ID 顺序不被排序。
-
Consumes: Task 4 有序绑定 API 和 Task 8 Service。
-
Step 1: 写绑定顺序失败测试
选择规则 A/B/C,移动为 C/A/B,断言 API payload 为 {ids:[C,A,B]};全局规则单独展示为固定前置且不可拖动。
- Step 2: 运行测试确认失败
Run: cd frontend && pnpm vitest run tests/unit/waf-binding-order.test.tsx
Expected: FAIL,当前 UI 只表达集合。
- Step 3: 实现排序并删除旧固定表单组件
复用项目现有 @dnd-kit/sortable;为键盘用户提供上移/下移操作。清理旧字段、旧 PoW 面板和不再引用的 helper,保留 IP 组管理组件。
- Step 4: 运行测试并提交
Run: cd frontend && pnpm vitest run && pnpm lint && pnpm build
Expected: PASS。
Commit: refactor(frontend): order waf bindings and remove legacy editor
Task 11: 旧后端字段清理、Swagger、中文文档与端到端验证
Files:
- Create:
internal/infra/persistence/migrator/goose/postgres/202607150003_drop_legacy_waf_rule_fields.sql - Create:
internal/infra/persistence/migrator/goose/sqlite/202607150003_drop_legacy_waf_rule_fields.sql - Modify:
internal/model/openflare_waf.go - Modify:
internal/apps/openflare/waf/logics_test.go - Modify:
docs/design/waf-design.md - Modify:
docs/guide/waf-usage.md - Modify:
docs/changelog/index.md - Generated:
docs/docs.go,docs/swagger.json,docs/swagger.yaml
Interfaces:
-
Produces: 无旧固定策略字段的最终 Schema 与中文使用文档。
-
Consumes: Tasks 1–10 的完整替代实现。
-
Step 1: 写迁移与集成失败测试
断言最终表不再包含 block_status_code、ip_whitelist、ip_blacklist、地域名单、pow_enabled、pow_config;端到端图分别产生 allow、block、PoW 接管,IP 组变化在 5–10 秒内生效。
- Step 2: 运行测试确认失败
Run: go test ./internal/apps/openflare/integration ./internal/model -run 'TestOrchestratedWAF|TestLegacyWAFColumnsRemoved' -count=1
Expected: FAIL,旧列仍存在。
- Step 3: 删除旧列和旧代码路径
PostgreSQL 直接 DROP COLUMN;SQLite 使用项目支持版本的 DROP COLUMN 或重建表迁移并复制 id/name/enabled/is_global/graph/revision/timestamps。删除 Go model/view/input 中的旧字段和固定链 helper,确保仓库中业务代码不再引用它们。
- Step 4: 更新中文文档与 changelog
waf-design.md 删除固定链作为现行设计的表述,链接可编排设计;waf-usage.md 写创建、节点语义、绑定顺序、发布生效和迁移警告;[Unreleased] 增加 WAF 可视编排、发布加载和 IP 组刷新条目。
- Step 5: 生成 Swagger 并运行全量验证
Run: make swagger
Expected: PASS,生成文件包含新 graph/meta API 与 409 response。
Run: go test ./... -count=1
Expected: PASS。
Run: cd frontend && pnpm vitest run && pnpm lint && pnpm build
Expected: PASS。
Run: make code-check
Expected: PASS,无格式、lint、测试或生成文件差异。
- Step 6: 最终人工数据面验收
创建规则并编排 开始 → IP 匹配 → true:通过 / false:地域匹配 → true:阻止A / false:PoW → 通过,绑定到测试路由并发布。用命中/未命中 IP、不同 GeoIP 和无 PoW cookie 请求验证三个分支;更新引用 IP 组后不发布,确认 5–10 秒内结果变化且 OpenResty 未 reload。
- Step 7: 提交
Commit: feat(waf): complete composable rule orchestration
4. 最终验收标准
- 用户新增规则时只输入名称并立即进入 React Flow 编排器。
- 默认规则为
开始 → 通过;特殊节点与处理节点满足设计约束。 - Server 和前端均拒绝非法图,发布再次校验,revision 冲突返回 409。
- 全局规则固定前置,自定义规则严格按绑定顺序执行。
- OpenResty 请求路径对规则与 IP 组均为纯内存读取。
- 规则只在发布 reload 时加载;IP 组每 5 秒 checksum 检查且仅变化时读取完整 JSON。
- PostgreSQL、SQLite、Go、Lua、前端、Swagger、构建与
make code-check全部通过。