Wavelet 去掉 cap/health 别名,前端改 v1;00001 补 OF 种子; 合并 migrate/updater 并删除 76 条历史 SQL。
18 KiB
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-alignmentHEAD(含 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 admin00001种子 -
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/updatergit mv) - Modify:
plugin.goembed 与 import - Modify:
router/v1/openflare/register_updater.goimport - 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):空库
newOpenFlareAppPrepare 后
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。