Files
OpenFlare/docs/superpowers/plans/2026-08-31-api-unify-and-server-layout.md
T
ryan d1e5c9acd8 docs(cordis): 添加 API 收口与 server 目录整理计划
Wavelet 去掉 cap/health 别名,前端改 v1;00001 补 OF 种子;
合并 migrate/updater 并删除 76 条历史 SQL。
2026-08-30 16:36:19 +08:00

18 KiB
Raw Blame History

API 收口与 server 目录整理 Implementation Plan

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: 去掉 cap/health 双路径,前端改打 /api/v1/cap 与 /api/healthz;把 OpenFlare server 收成单一 migrate/ + updater/,压缩 00001 含 of_* 建表与 OF 种子。

Architecture: 先在 Wavelet feat/cordis-alignment 删别名路由,再 merge 进 OpenFlare;然后改前端与测试;最后用金标准差集生成种子、合并迁移包并删除 76 文件。

Tech Stack: Go 1.25、Gin、goose、Next.js 前端 cap-solver.ts。

规格来源: docs/superpowers/specs/2026-08-31-api-unify-and-server-layout-design.md

Global Constraints

  • cap 只留 /api/v1/cap/*;删除 /api/cap/*。
  • health 只留 GET /api/healthz(JSON {status: ok});删除 /healthz 与 /api/health。
  • 路由改在 Wavelet 插件,禁止在 OF server 再挂别名。
  • 00001 = of_* DDL + OF 种子;种子 key = 金标准 w_system_configs 减 Wavelet 平台种子。
  • 禁止把 OF 种子写进 Wavelet admin 迁移。
  • 禁止 DROP TABLE / DROP COLUMN / TRUNCATE。
  • 76 历史 SQL 从仓库删除。
  • 不重组 openflare/ 业务子域;不改 agent CLI。
  • 金标准 /Users/ryan/Code/Go/OpenFlare 只读。

文件结构

仓库 路径 职责
Wavelet backend/plugins/domain/cap/plugin.go、handlers.go、测试 去掉 /api/cap
Wavelet backend/plugins/domain/system/plugin.go、health_route_test.go 只留 /api/healthz
Wavelet backend/plugins/domain/auth/plugin.go 白名单 与 cap 路径一致
OpenFlare frontend/lib/cap-solver.ts、README 请求 v1 cap / healthz
OpenFlare backend/cmd/app_test.go、parity 新路径断言
OpenFlare backend/OpenFlare/plugins/server/migrate/ stamp + SQL + CH
OpenFlare backend/OpenFlare/plugins/server/updater/ 原 admin/updater

Task 1: Wavelet cap 只留 /api/v1/cap

Files:

  • Modify: /Users/ryan/Code/Go/Wavelet/backend/plugins/domain/cap/plugin.go(约 82–93 行)
  • Modify: /Users/ryan/Code/Go/Wavelet/backend/plugins/domain/cap/handlers.go(@Router)
  • Modify: /Users/ryan/Code/Go/Wavelet/backend/plugins/domain/cap/plugin_captcha_contract_test.go
  • Modify: /Users/ryan/Code/Go/Wavelet/backend/plugins/domain/auth/plugin.go 白名单(若仍列 /api/cap)

Interfaces:

  • Consumes: 现有 Challenge/Redeem、CaptchaService

  • Produces: 路由仅 GET/POST /api/v1/cap/challenge、POST /api/v1/cap/redeem

  • Step 1: 改测试为「必须有 v1、禁止 legacy」

在 plugin_captcha_contract_test.go 的路由集合里:

want := map[string]bool{
    "GET /api/v1/cap/challenge":  false,
    "POST /api/v1/cap/challenge": false,
    "POST /api/v1/cap/redeem":    false,
}
// after scanning routes, all want values true
for _, rd := range ctx.Router().Routes() {
    key := rd.Method + " " + rd.Path
    if key == "POST /api/cap/challenge" || key == "POST /api/cap/redeem" {
        t.Errorf("legacy route must not exist: %s", key)
    }
}
  • Step 2: 跑测试确认仍看到 legacy(或改完测试后 FAIL 在「缺少禁止断言」之前)
cd /Users/ryan/Code/Go/Wavelet/backend && go test ./plugins/domain/cap/ -run TestApplyRegistersUnversionedCapRoutes -count=1

Expected: FAIL(测试改为禁止 legacy 后,现实现仍注册 /api/cap)。

  • Step 3: 删除 legacy 组

plugin.go 删除:

legacy := ctx.Router().Group("/api/cap")
legacy.POST("/challenge", Challenge)
legacy.POST("/redeem", Redeem)
ctx.Router().RegisterWhitelist("/api/cap/challenge", "/api/cap/redeem")

改为:

ctx.Router().RegisterWhitelist("/api/v1/cap/challenge", "/api/v1/cap/redeem")

(若 Group 已能白名单则只保留 v1 组 + RegisterWhitelist v1 路径。)

handlers.go:@Router /api/v1/cap/challenge [post]、@Router /api/v1/cap/redeem [post](GET challenge 同前缀)。

auth/plugin.go 的 publicEndpoints 去掉任何 /api/cap 非 v1 项。

  • Step 4: 测试通过
cd /Users/ryan/Code/Go/Wavelet/backend && go test ./plugins/domain/cap/ ./plugins/domain/auth/ -count=1

Expected: PASS。

  • Step 5: 提交(Wavelet feat/cordis-alignment,不要提交 main)
cd /Users/ryan/Code/Go/Wavelet
git add backend/plugins/domain/cap backend/plugins/domain/auth
git commit -m "fix(cap): keep only /api/v1/cap routes"

Task 2: Wavelet health 只留 /api/healthz

Files:

  • Modify: /Users/ryan/Code/Go/Wavelet/backend/plugins/domain/system/plugin.go
  • Modify: /Users/ryan/Code/Go/Wavelet/backend/plugins/domain/system/health_route_test.go
  • Modify: auth 白名单中的 /healthz(改为 /api/healthz)

Interfaces:

  • Produces: 唯一健康路由 GET /api/healthz → gin.H{"status":"ok"}

  • Step 1: 改 health_route_test

func TestHealthzIsTheOnlyHealthRoute(t *testing.T) {
    ctx := core.NewContext(context.Background())
    if err := New().Apply(ctx); err != nil {
        t.Fatal(err)
    }
    var hasHealthz bool
    for _, rd := range ctx.Router().Routes() {
        key := rd.Method + " " + rd.Path
        switch key {
        case "GET /api/healthz":
            hasHealthz = true
        case "GET /healthz", "GET /api/health":
            t.Errorf("removed health route still registered: %s", key)
        }
    }
    if !hasHealthz {
        t.Fatal("GET /api/healthz missing")
    }
    if !ctx.Router().IsWhitelisted("/api/healthz") {
        t.Fatal("GET /api/healthz not whitelisted")
    }
}

用 httptest 调 /api/healthz,body 含 "status":"ok"。删除对 Health/OKNil//api/health 的旧测试。

  • Step 2: 跑测试确认 FAIL
cd /Users/ryan/Code/Go/Wavelet/backend && go test ./plugins/domain/system/ -run TestHealth -count=1

Expected: FAIL(仍注册 /healthz 或 /api/health)。

  • Step 3: 实现

plugin.go 健康检查块改为:

ctx.Router().GET("/api/healthz", func(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{"status": "ok"})
})
ctx.Router().RegisterWhitelist("/api/healthz")

删除 /healthz、/api/health 注册。若 Health 函数不再被引用则删除(含 swagger 注释)。

auth 白名单:/healthz → /api/healthz。

  • Step 4: 测试
cd /Users/ryan/Code/Go/Wavelet/backend && go test ./plugins/domain/system/ ./plugins/domain/auth/ ./plugins/domain/cap/ -count=1
go test ./...

Expected: PASS。

  • Step 5: 提交
cd /Users/ryan/Code/Go/Wavelet
git add backend/plugins/domain/system backend/plugins/domain/auth
git commit -m "fix(system): expose only GET /api/healthz"

Task 3: Merge Wavelet 路由进 OpenFlare

Files:

  • Modify: OpenFlare backend/{core,pkg,plugins} via merge

Interfaces:

  • Consumes: Wavelet feat/cordis-alignment HEAD(含 Task 1–2)

  • Produces: OF plugins/domain/cap 与 system 与 Wavelet 零差

  • Step 1: merge

cd /Users/ryan/Code/Go/OpenFlare-cordis
git fetch wavelet
git merge wavelet/feat/cordis-alignment

冲突:backend/{core,pkg,plugins} 以 Wavelet 为准;frontend/、backend/OpenFlare/、docs/superpowers/ ours。不要 rebase。若尚未配置 include.path,用 git -c merge.ours.driver=true merge ...。

  • Step 2: 确认路由来自 Wavelet
rg -n 'Group\("/api/cap"\)|GET\("/api/health"|GET\("/healthz"' backend/plugins/domain

Expected: 无匹配。

  • Step 3: cd backend && go test ./plugins/domain/cap/ ./plugins/domain/system/ -count=1

Expected: PASS。./cmd/ 可能因旧断言 FAIL,Task 5 修。

  • Step 4: 提交 merge(若 merge 已产生 commit 则无需空提交)

Task 4: 前端 cap 与文档探针

Files:

  • Modify: frontend/lib/cap-solver.ts(约 138、174 行)
  • Modify: README.md、README_zh.md(健康检查 URL)
  • Modify: 其它文档中的 /api/health、/api/cap(rg 列出后改产品文档,不改 docs/superpowers 历史 spec 原文)

Interfaces:

  • Produces: 浏览器只请求 /api/v1/cap/challenge 与 /api/v1/cap/redeem

  • Step 1: 改 cap-solver.ts

const challengeRes = await fetch('/api/v1/cap/challenge', {
const redeemRes = await fetch('/api/v1/cap/redeem', {
  • Step 2: 健康检查文档

把 http://localhost:8000/api/health 换成 http://localhost:8000/api/healthz。

rg -n '/api/cap|/api/health[^\w]|/healthz' frontend README.md README_zh.md docs --glob '!docs/superpowers/**'

产品文档中的旧路径改完。docs/superpowers 历史设计稿保留原文。

  • Step 3: 确认 cap-solver 无旧路径
rg -n '/api/cap/' frontend/lib/cap-solver.ts

Expected: 无输出。

  • Step 4: 提交
git add frontend/lib/cap-solver.ts README.md README_zh.md
git commit -m "fix(frontend): call /api/v1/cap and document /api/healthz"

Task 5: OpenFlare 测试与 swagger 跟随新路径

Files:

  • Modify: backend/cmd/app_test.go
  • Modify: parity 测试(backend/cmd/parity_test.go 或现文件)若硬编码 /api/health、/api/cap
  • Modify: docs/swagger.json / yaml 经 make swagger

Interfaces:

  • Consumes: Task 3 的 Wavelet 路由

  • Produces: 断言 POST /api/v1/cap/challenge、GET /api/healthz;禁止旧路径

  • Step 1: 改 app_test 必有路径列表

must := []string{
    "GET /api/healthz",
    "POST /api/v1/cap/challenge",
}
forbidden := []string{
    "GET /api/health",
    "GET /healthz",
    "POST /api/cap/challenge",
}

parity:金标准子集里去掉已废弃路径,或对这三条做 forbidden 覆盖(不要因为金标准 routes-engine.txt 仍含 /api/health 而失败——该夹具是 v3.5.4 历史,本任务有意改契约。改 parity:加载 baseline 后 delete 这几条再做 ⊆ 断言,并额外要求新路径存在)。

  • Step 2: cd backend && go test ./cmd/ -count=1

Expected: PASS。

  • Step 3: make swagger,确认 paths 无 /api/cap、/api/health、/healthz
python3 -c "import json;d=json.load(open('docs/swagger.json'));
print('\n'.join(p for p in sorted(d.get('paths',{})) if 'cap' in p or 'health' in p))"

Expected: 只有 /api/v1/cap/... 与 /api/healthz(若 swagger 扫到 healthz)。

  • Step 4: 提交
git add backend/cmd docs/swagger.json docs/swagger.yaml backend/docs
git commit -m "test(cmd): require v1 cap and /api/healthz only"

Task 6: 生成 OF 种子差集并写入 00001

Files:

  • Modify: backend/OpenFlare/plugins/server/migrations/sqlite/00001_initial.sql
  • Modify: backend/OpenFlare/plugins/server/migrations/postgres/00001_initial.sql
  • Create: backend/OpenFlare/plugins/server/migrate/seed_diff_test.go(或 Task 7 搬家后再放 migrate/;本任务可先写在 migrations 旁的测试,Task 7 一起搬)

Interfaces:

  • Consumes: 金标准 /Users/ryan/Code/Go/OpenFlare @ 9f79fb99;Wavelet admin 00001 种子

  • Produces: 00001 含 DDL + w_schedules 四条 + w_system_configs 差集

  • Step 1: 导出差集(只读金标准,临时目录)

金标准库 G:

GOLD=/Users/ryan/Code/Go/OpenFlare
TMP=$(mktemp -d)
# 按 Task 11/15 已验证方式:拷 config 到 TMP,SQLITE 指向 $TMP/gold.db,跑 gold api 直到 goose 202608090003
sqlite3 "$TMP/gold.db" "SELECT key FROM w_system_configs ORDER BY key;" > /tmp/keys-G.txt
sqlite3 "$TMP/gold.db" "SELECT task_type FROM w_schedules ORDER BY task_type;" > /tmp/sched-G.txt

Wavelet 种子 W:从 Wavelet/backend/plugins/domain/admin/migrations/sqlite/00001_initial.sql 解析 INSERT INTO w_system_configs 的 key 列表(或空库只跑 Wavelet 插件)。G \ W 写入计划检查清单。

定时任务:G 中应有 of_ssl_renew、of_waf_ip_group_sync、of_uptime_kuma_sync、of_pages_source_scan;无 of_database_auto_cleanup。system_cleanup 属于 W,不要插入。

  • Step 2: 写失败测试(新装 sqlite:跑 server 迁移后断言种子)

最小:解析 00001_initial.sql 文本:

func TestInitialSQLContainsScheduleSeeds(t *testing.T) {
    b, err := os.ReadFile("sqlite/00001_initial.sql") // 相对测试包
    if err != nil { t.Fatal(err) }
    s := string(b)
    for _, needle := range []string{"of_ssl_renew", "of_waf_ip_group_sync", "of_uptime_kuma_sync", "of_pages_source_scan"} {
        if !strings.Contains(s, needle) {
            t.Errorf("00001 missing schedule seed %s", needle)
        }
    }
    if strings.Contains(s, "of_database_auto_cleanup") {
        t.Error("must not seed of_database_auto_cleanup")
    }
}

另测:SQL 含 INSERT 且含至少一个差集 key(例如 agent_discovery_token、geoip_provider)。

  • Step 3: 跑测试确认 FAIL(当前 00001 无 INSERT)
cd /Users/ryan/Code/Go/OpenFlare-cordis/backend && go test ./OpenFlare/plugins/server/migrations/ -count=1

若测试放在 stamp 包,改路径。Expected: FAIL missing of_ssl_renew。

  • Step 4: 在 sqlite 与 postgres 00001 末尾追加种子

sqlite 示例:

INSERT OR IGNORE INTO w_schedules (id, name, task_type, cron, payload, is_active, created_at, updated_at)
VALUES
    (101, 'OpenFlare SSL 自动续期', 'of_ssl_renew', '0 0 * * *', '{}', 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
    (103, 'OpenFlare WAF IP 组同步', 'of_waf_ip_group_sync', '*/5 * * * *', '{}', 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
    (104, 'OpenFlare Uptime Kuma 同步', 'of_uptime_kuma_sync', '* * * * *', '{}', 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);

INSERT INTO w_schedules (name, task_type, cron, payload, is_active, created_at, updated_at)
SELECT 'OpenFlare Pages 部署源扫描', 'of_pages_source_scan', '0 0 * * *', '{}', 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
WHERE NOT EXISTS (SELECT 1 FROM w_schedules WHERE task_type = 'of_pages_source_scan');

postgres 用 INSERT … ON CONFLICT (id) DO NOTHING;pages scan 用 WHERE NOT EXISTS。

w_system_configs:对每个 G \ W 的 key,用历史链最终 default(create_of_options + 后续 202608* 文件的字面量,不要 SELECT FROM of_options)。INSERT OR IGNORE / ON CONFLICT (key) DO NOTHING。

禁止插入 Wavelet 已有 key(cap_login_enabled、smtp_host、storage_config 等)。禁止 DROP。

把 Step 1 的 G \ W 全写进 SQL。测试增加:每个差集 key 都出现在 sqlite 与 postgres 两个文件中。

  • Step 5: 测试通过并提交
cd /Users/ryan/Code/Go/OpenFlare-cordis/backend && go test ./OpenFlare/plugins/server/... -count=1
git add backend/OpenFlare/plugins/server/migrations
git commit -m "fix(server): seed OpenFlare schedules and configs in 00001"

Task 7: 合并 migrate/、updater/,删除历史链

Files:

  • Create: backend/OpenFlare/plugins/server/migrate/{stamp.go,stamp_test.go,clickhouse.go,clickhouse_test.go,postgres/,sqlite/,clickhouse/}
  • Create: backend/OpenFlare/plugins/server/updater/*(从 admin/updater git mv)
  • Modify: plugin.go embed 与 import
  • Modify: router/v1/openflare/register_updater.go import
  • Delete: admin/、migrator/、stamp/、chmigrate/、migrations/(内容已迁走后)
  • Delete: router/root/frontend_static.go 及无引用测试(若仍存在)

Interfaces:

  • Consumes: Task 6 的 00001 文本

  • Produces: package migrate 导出 Legacy(ctx *core.Context) error(现 stamp.Legacy)与 UpClickHouse() error

  • Step 1: git mv

cd backend/OpenFlare/plugins/server
git mv stamp migrate/stamp_pkg_tmp  # 或直接 mv 文件进 migrate/ 并改 package 名为 migrate

推荐:把 stamp/*.go 的 package stamp 改为 package migrate,函数名保持 Legacy。cmd 里 stamp.Legacy 改为 migrate.Legacy。

git mv chmigrate/*.go migrate/
# CH sql
mkdir -p migrate/clickhouse
git mv chmigrate/goose/clickhouse/*.sql migrate/clickhouse/
git mv migrations/postgres migrate/postgres
git mv migrations/sqlite migrate/sqlite
git mv admin/updater updater

改所有 import。plugin.go:

//go:embed migrate/postgres/*.sql migrate/sqlite/*.sql
var serverMigrations embed.FS

ctx.Migrations().Register("server", serverMigrations)
if err := migrate.UpClickHouse(); err != nil {
    return err
}

cmd/app.go:core.WithMigrationBaseline(migrate.Legacy)。

  • Step 2: 删除空壳与 76 文件
git rm -r migrator admin stamp chmigrate migrations

确认无 package migrator、无 admin/updater import。

  • Step 3: 编译测试
cd backend && go test ./cmd/ ./OpenFlare/... -count=1
rg -n 'package migrator|admin/updater|OpenFlare/plugins/server/stamp"' .

Expected: 测试 PASS;rg 无产品引用。

  • Step 4: 提交
git add -A backend/OpenFlare backend/cmd
git commit -m "refactor(server): merge migrate package and drop unused admin/migrator trees"

Task 8: 全量门禁

Files: 无新功能;修到绿

  • Step 1: cd backend && go test ./...

Expected: PASS。

  • Step 2: 新装冒烟(可选 sqlite):空库 newOpenFlareApp Prepare 后
SELECT COUNT(*) FROM sqlite_master WHERE name LIKE 'of_%';
SELECT task_type FROM w_schedules WHERE task_type LIKE 'of_%' ORDER BY 1;

Expected: 含 5.2 四条 task_type;无 of_database_auto_cleanup。

  • Step 3: 升级测试仍过(go test ./cmd/ -run TestUpgradeFromGolden -count=1 -timeout 120s)

Expected: PASS(stamp 跳过 00001,数据仍在)。

  • Step 4: git diff --name-only 确认 frontend 只有 cap/文档;金标准 git status --short 空。

有失败则修并提交 fix(cordis): satisfy api unify gates。


自检(对照 spec)

Spec Task
D1 cap 只 v1 1、4、5
D2 health 只 /api/healthz 2、4、5
D3 改 Wavelet 再进 OF 1–3
D4 删 76 文件 7
D5/D6 建表+差集种子 6
D7 updater 路径 7
D8 单一 migrate/ 7
前端 cap 4
新装/升级 6、8

无 TBD。migrate.Legacy / UpClickHouse 名称在 Task 7 定义,cmd 同步改 import。