Files
OpenFlare/docs/superpowers/plans/2026-08-29-openflare-cordis-foundation.md
T
ryan dbaa3bf140 feat(cordis): add OpenFlare Cordis 架构改造设计
docs(changelog): 修正表述笔误

refactor(cordis): 磁盘缓存改用上上游能力并清理本地副本

按上游/下游归属规约:类型断言守卫已回流 Wavelet(f3d85d5,附回归用例),
本仓库删除 OpenFlare/plugins/server/pkg/cache 整包并改 import 到
Wavelet/pkg/cache/disk,同步后与上游零漂移。

验证:go build 通过;go test ./... exit 0(137 包 ok);256 条路由对拍与
232 条 swagger 操作均零差异;make build-all 四进制;前端零改动。

docs(cordis): 记录 T1 清理结果与五个复用阻塞点

refactor(cordis): server 复用上游 pkg 能力并删除等价本地副本

按上游/下游归属规约清理重复实现,删除 7 个与上游等价的本地包并改 import:
shared/response→pkg/response、pkg/{logger,mail,trace,httppool,cache/ram}→
上游同名包、infra/persistence/batchwriter→pkg/batchwriter。逐项核过差异:
httppool 逐字节相同;logger 的 Config 字段完全一致;response 的 7 个 Abort*
一致;cache/ram 换过去顺带把裸 go 变回带 panic 恢复的 util.Go。

两处非等价差异按语义处理:
- batchwriter.Stats 与 status DTO 原为类型别名,改为消费侧逐字段转换,
  避免 model 反向依赖基础设施类型;
- 上游 pkg/idgen 要求显式 Init(本地副本为懒加载自动初始化),本次保留本地
  副本,待与 infra 初始化一并迁移(已登记在清理计划)。

验证:go build 通过;go test ./... exit 0(138 包 ok);256 条路由对拍零差异;
make swagger 232 条操作零增减,且归一化后与旧文档深度相等——差异仅为
response.Any / logger.LogEntry 两个定义名随包路径改名,接口形状未变。

chore(cordis): 回流内核与 pkg/util 通用能力并清理 vendoring 污染

按新增的上游/下游归属规约:HandleRaw/BasePath 与版本比较、网络、格式化助手
属通用能力,已提交到 Wavelet 分支 feat/cordis-router-raw-routes,本仓库改为
纯同步获取(pkg/util 已零漂移),补丁登记保留至上游合并。

同时修掉我此前 git add -A 造成的污染:首次 vendoring 把上游工作区里被
gitignore 的运行期产物一起提交进来(upload 的 diskcache 缓存块 650 个与
driver_http/dist 前端构建物 380 个,共 12872 行/1030 文件)。sync-upstream.sh
现显式排除 uploads/dist/data/*.db,.gitignore 补上对应兜底规则。

AGENTS.md 增加上游/下游改动归属规约,并把仍指向前 Cordis 布局的硬性约束
(internal/router + Serve、internal/repository/logstore、internal/platform/bootstrap、
internal/cmd)改到当前插件路径。

验证:go build 通过;go test ./... exit 0(144 包 ok);make swagger 232 条
操作与基线逐条一致;make build-all 四进制;gofmt 干净。

feat(cordis): server 插件化并改由内核挂载控制面路由

新增 plugins/server/plugin.go:Apply 以 ctx.Router().Group(app.api_prefix)
声明根级与 /v1 全部路由;33 个注册函数由 *gin.RouterGroup 改为
core.RouterExtension,RegisterCollection 改用内核新增的 HandleRaw 保留
尾部斜杠变体,AdminMiddlewares 返回 []any(Go 不允许把 []T 展开为 ...any)。
删除 router.Serve 与 registerRoutes,装配根改为 core.App +
driver_http.New(WithEngine(router.BuildEngine())),监听、信号与优雅退出归内核;
前端 SPA 的 NoRoute 兜底因内核暂无贡献点而保留在引擎层。

路由保真证据:plugin_parity_test 对拍 baseline/routes-engine.txt 的 256 条
(方法 路径) 零差异;go test ./... exit 0(144 包 ok,含真实 handler 的
openflare/integration 用例走同一条挂载路径);make swagger 232 条操作与基线
逐条一致;golangci-lint 0 issues;make build-all 四进制;embed_frontend
标签编译通过;前端零改动。

已知待补:带 Redis 的实机 HTTP 冒烟(本机 6379 未启动,session store 与
改造前一样在建店阶段即 fatal),以及 bootstrap 的任务/设置/迁移注册迁入 Apply。

feat(core): RouterExtension 增加 HandleRaw 与 BasePath 以保真尾部斜杠路由

server 插件化的前置:Handle 经 cleanPath 会剥掉尾部斜杠,无法表达
/resource 与 /resource/ 两条不同路由,而 OpenFlare 有 20 个历史 list
端点两者都注册且部署关闭了 RedirectTrailingSlash,缺失即 404。新增
HandleRaw 与 BasePath(作用域包装器同样登记反注册),补 extpoints 用例;
并把 router.Serve 拆出 BuildEngine 以便交给 driver_http.WithEngine 复用,
新增路由表导出 harness,固化 256 条 (方法 路径) 基线供插件化对拍。
上游补丁登记于 backend/OpenFlare/upstream-patches.md,同步脚本改为按目录
前缀输出差异并在同步后提醒确认补丁是否仍在。

验证:go build 通过;go test ./... exit 0(143 包 ok);gofmt 干净。

docs(cordis): 记录 server 插件接入内核的可行路径与内核能力缺口

feat(cordis): agent/relay/flared 落地为内核驱动插件

三个边缘守护进程各新增 plugin.go,实现 core.Plugin + core.Driver
(自定义 DriverType 与同名 profile),装配与生命周期从 main 迁入
Apply/Start/Stop:Apply 负责 JSON 配置加载、运行环境与用户确保、
openresty/frps/frpc 管理器与各服务装配;Start 以 util.Go 拉起阻塞式
runner 与 GeoIP 周期更新;Stop 收敛主循环结果并在超时时报错而非静默。

入口改为 core.NewApp(core.WithProfile(...)) + Prepare/Run,保持
-config 旗标、默认路径、退出码与启动/停止日志不变。

验证:go build 通过;go test ./... exit 0(143 包 ok,含 3 个插件身份
与配置失败路径测试);make build-all 四进制产出;三进制实跑缺失配置
均 exit 1 且错误链保留 load {agent,relay,flared} config 原因;gofmt 干净。

refactor(cordis): 按功能职责拆分为 4 个插件与 share 共享层

backend/OpenFlare 不再平铺遗留分层,改为 plugins/{server,agent,relay,flared}
加 share/:控制面业务(openflare/admin/oauth/user/upload/cap/config/health 与
repository/model/infra/router 等支撑层)归 server;三个边缘守护进程各自成插件;
被两个以上插件消费的 protocol/geoip/wsclient/render/pagesarchive/edge 归 share。
同时把 pkg/util 与 buildinfo 合并回上游 pkg(上游已覆盖全部符号,仅 8 个函数与
2 个类型为 OpenFlare 独有,已一并迁入),装配根统一到 backend/cmd(含三个 daemon
入口),Dockerfile 与 release 工作流的构建路径和 -X 注入路径同步更新。

验证:go build 通过;go test ./... exit 0(141 包 ok);make swagger exit 0 且
232 条 API 操作与基线逐条一致;make build-all 产出 4 进制;-X 注入经二进制
strings 实测生效;日志后端直连门禁改写为按 server 插件业务域扫描并在扫描数为 0
时报错(防门禁静默失效);前端零改动。

feat(cordis): 落地 backend/share 共享层与上游同步脚本

跨插件共享资源(控制消息协议、GeoIP+iputil、边缘守护进程日志)从下游包
移入 backend/share,并声明其只能依赖 core/pkg 与标准/第三方库,禁止反向
引用下游业务与具体插件实现;新增 scripts/sync-upstream.sh 只覆盖
backend/{core,pkg,plugins},同步后 --check 报告零差异,证明与上游逐字一致。

go build 通过,go test ./... exit 0(142 包 ok),前端零改动。

refactor(cordis): 采用与 Wavelet 同构的单模块布局并引入上游内核

按上游结构落位:backend/{core,pkg,plugins} 为 Wavelet 上游拷贝,OpenFlare
全部业务收拢到上游 downstream 所对应的位置 backend/OpenFlare/,模块名保持
Wavelet 以保证上游 import 路径逐字一致、同步零改写;三个 daemon 入口移至
backend/OpenFlare/cmd,backend/cmd 与 main.go 作为控制面装配根。

行为不变:go build 通过,142 个测试包全绿(含上游插件测试),232 条 API
操作与改造前逐条一致,四进制产物正常,前端零改动。swagger 暂只扫描下游代码,
待 P4 挂载上游路由后再纳入 plugins/。

style: 修正模块路径改写导致的 import 分组排序漂移

refactor(layout): Go 代码迁入 backend/ 并将模块名简化为 OpenFlare

对齐上游 Wavelet 的仓库布局,为以第二 module 形态 vendoring Cordis 内核与
平台插件做准备:模块路径整体改写为 OpenFlare,Go 目标加 cd backend,
swaggo 产物移至 backend/docs 并把 json/yaml 复制回 docs/ 供站点消费,
Dockerfile 与 release 工作流的构建目录、ldflags 模块路径同步更新。

行为保持不变:232 条路由与改造前逐条一致,95 个测试包全绿,
四进制产物正常,前端零改动。

chore(cordis): 落地改造计划与 schema/路由基线

新增 legacy_dump_test 迁移快照 harness:在临时 sqlite 库上按生产顺序
(goose.UpTo → zone 导入 → goose.Up)跑完 76 个历史迁移并导出 schema 与
版本序列,作为改造前后一致性门禁的唯一事实来源。同时记录 232 条路由清单
与 foundation 实施计划。

docs(cordis): add OpenFlare Cordis 架构改造设计

明确上游以第二 module 形态 vendoring 进 backend/Wavelet、4 个插件
(server/agent/relay/flared) 全部装载内核,并规定保留 76 个历史 goose
迁移 + 一次性版本 stamp 桥接的迁移方案,配套三方 schema 一致性门禁,
确保已部署库不重跑历史、不丢数据。
2026-08-30 10:12:52 +08:00

32 KiB
Raw Blame History

OpenFlare Cordis 基础改造实施计划(P0–P3)

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: 把 OpenFlare 后端迁入 backend/ 双 module 结构、以 vendoring 方式引入 Wavelet Cordis 上游与 share 共享层,并在不改动任何已部署数据库语义的前提下完成 per-plugin 迁移与版本 stamp 桥接。

Architecture: backend/(module OpenFlare)持有 4 个业务插件与装配根;backend/Wavelet/(module Wavelet)为上游原样拷贝,replace + go.work 实现单命令构建。迁移引擎自持 w_schema_versions(plugin_id, version_id),历史 76 链以 plugin id openflare/legacy 保真注册,桥接逐行搬运 goose_db_version 状态,并保留 Go 侧 zone 导入钩子。

Tech Stack: Go 1.25、Gin、GORM、PostgreSQL/SQLite、ClickHouse(独立链,不动)、goose v3、Cobra、Viper。

规格来源: docs/superpowers/specs/2026-08-29-openflare-cordis-refactor-design.md

后续计划: P4(server 插件)、P5(agent/relay/flared 插件)、P6(平台分叉回流)、P7/P8(工具链与加固)各出独立 plan,依赖本 plan 产出的骨架与门禁。


进度与布局修正(2026-08-29 执行中更新)

已完成

  • Task 1 基线:docs/superpowers/specs/baseline/{schema-A.sql,versions-A.txt,routes.txt},由 backend/OpenFlare/infra/persistence/migrator/legacy_dump_test.go 产出(OF_DUMP_SCHEMA / OF_DUMP_VERSIONS 触发,A 路 43 表、版本止于 202608090003)。
  • Task 2 布局:Go 树已入 backend/(提交 f6dd7988)。

Task 3 / Task 4 作废原命令,改为:与 Wavelet 同构的单 module(提交 9bf0b2de)——backend/{core,pkg,plugins} 为上游拷贝,下游业务整体位于 backend/OpenFlare/(占据上游 backend/downstream 的位置),模块名保持 Wavelet。因此不再有 go.work、replace、第二 go.mod。

  • Task 3 剩余动作:仅需 scripts/sync-upstream.sh(覆盖 backend/{core,pkg,plugins},排除 share、OpenFlare)与上游 scripts/check_cordis_architecture.sh 接入。
  • Task 4 改为:新建 backend/share/(import Wavelet/share/...)并写入所有权声明;把跨插件共享资源 pkg/{protocol,geoip,wsclient}、apps/edge/logging 移入;退役与上游重复的 OpenFlare/pkg/{logger,trace,mail,httppool} 与 OpenFlare/{buildinfo,testhelper}(已核实上游覆盖除 util 与 testhelper.SetupLogStoresForTest 外的全部符号);OpenFlare/pkg/util 的 8 个仅存函数与 IdentifiableTimeRecord/VersionInfo 合并进上游 pkg/util。
  • 已实测门禁:go build ./... 通过、go test ./... exit 0(142 包 ok)、make swagger exit 0(232 条操作与基线逐条一致)、make build-all 四进制产出、frontend/ 零改动。
  • 遗留待办:swaggo 暂 --exclude plugins,core,Task 4/P4 需连带解除该排除。

关键事实(执行前必读,均已核实)

  • 历史链:internal/infra/persistence/migrator/goose/{postgres,sqlite} 各 76 个文件,终版 202608090003_add_log_indexes.sql;版本表为 goose 默认 goose_db_version。ClickHouse 链独立:goose/clickhouse 14 文件 + goose_clickhouse_version。
  • 历史链含 Go 侧数据迁移:migrator.Migrate()(internal/infra/persistence/migrator/migrator.go:91-103)先 goose.UpTo(202607120002),再在版本窗口 [202607120002, 202607130001) 内执行 zone.ImportLegacyTx,最后 goose.Up()。改造后该钩子必须仍然生效,且判断依据改为 w_schema_versions 中 openflare/legacy 的 max version。
  • 另有 PG 序列修复 resyncGooseVersionSequence(同文件 :158-183)与迁移后 clearSystemConfigCache(:185-189),两者必须保留。
  • 上游迁移引擎实现位于 Wavelet/backend/cmd/app.go:112-360(sharedStore + gooseEngine),core.MigrationEngine 为公开接口,OpenFlare 需自持一份实现(放 OpenFlare/internal/platform/migration),不可依赖上游 cmd 包内未导出符号。
  • MigrationEntry.Dir 被上游引擎忽略;目录基名必须为 postgres/sqlite。goose 会给每个 plugin_id 插入哨兵 version_id=0。
  • 内核:core.Profile("agent") 合法(未知 profile 原样透传);App.Run 始终阻塞在信号;长生命周期服务用 ctx.RegisterDriver + ctx.OnDispose。
  • 全仓 476 个 .go 文件引用 github.com/Rain-kl/Wavelet,且无任何非 import 出现(已验证),模块路径替换可全局进行。
  • 前端 frontend/ 禁止改动;//go:embed all:dist 位于 internal/router/root/frontend.go:18,dist 由 make build-embedded 拷入。

文件结构(本计划涉及)

动作 路径 职责
Create backend/(git mv 而来) Go 代码根
Modify backend/go.mod module OpenFlare + require/replace Wavelet
Create backend/go.work 本地双 module 一体构建
Create backend/Wavelet/** 上游拷贝(core、plugins、pkg、scripts、go.mod)
Create backend/Wavelet/share/{protocol,wsclient,geoip,edge}/ 跨插件共享资源(OpenFlare 所有,同步排除)
Create backend/Wavelet/share/README.md 所有权与同步排除声明
Create backend/internal/platform/migration/{store,engine,bridge,lock}.go 迁移引擎、桥接、锁
Create backend/plugins/server/migrations/{postgres,sqlite}/*.sql 76×2 历史链原样保真
Create backend/cmd/migrate-audit/main.go 三方 schema 一致性门禁工具
Create scripts/sync-upstream.sh 上游同步(排除 share/)
Modify Makefile、docker/Dockerfile*、.github/workflows/* 路径切至 backend/、ldflags 模块路径

Task 1: 基线快照(P0)

Files:

  • Create: docs/superpowers/specs/baseline/routes.txt

  • Create: docs/superpowers/specs/baseline/go-test.txt

  • Step 1: 导出改造前路由与测试基线

cd /Users/ryan/Code/Go/OpenFlare-cordis
mkdir -p docs/superpowers/specs/baseline
python3 -c "import json;d=json.load(open('docs/swagger.json'));print('\n'.join(sorted(f'{m.upper()} {p}' for p,v in d.get('paths',{}).items() for m in v)))" > docs/superpowers/specs/baseline/routes.txt
go test ./... > docs/superpowers/specs/baseline/go-test.txt 2>&1; echo "exit=$?"

Expected: exit=0,routes.txt 非空。

  • Step 2: 生成 A 路 schema 基线(改造前二进制,供 Task 9 对拍)
go build -o /tmp/of-baseline-server .
OF_DB_PATH=/tmp/of-baseline-a.db timeout 25 /tmp/of-baseline-server api >/tmp/of-a.log 2>&1 || true
sqlite3 /tmp/of-baseline-a.db "SELECT name,sql FROM sqlite_master WHERE type='table' ORDER BY name;" > docs/superpowers/specs/baseline/schema-A.sql
sqlite3 /tmp/of-baseline-a.db "SELECT version FROM goose_db_version WHERE is_applied=1 ORDER BY version;" > docs/superpowers/specs/baseline/versions-A.txt

Expected: schema-A.sql 含 w_users、of_zones;versions-A.txt 末行为 202608090003。

  • Step 3: 提交基线
git add docs/superpowers/specs/baseline && git commit -m "chore(cordis): 记录改造前 schema 与路由基线"

Task 2: Go 树迁入 backend/ 并改 module 名(P1)

Files:

  • Modify: backend/**(全部 Go 文件)、Makefile、docker/*、.github/workflows/*

  • Step 1: 移动文件(保留 git 历史)

mkdir -p backend
git mv cmd internal pkg main.go go.mod go.sum backend/
  • Step 2: 改 module 名并重写全部 import 路径
perl -pi -e 's{^module github\.com/Rain-kl/Wavelet$}{module OpenFlare}' backend/go.mod
grep -rl 'github.com/Rain-kl/Wavelet' --include='*.go' backend | xargs perl -pi -e 's{\bgithub\.com/Rain-kl/Wavelet\b}{OpenFlare}g'
grep -rl 'github.com/Rain-kl/Wavelet' --include='*.yml' --include='Dockerfile*' Makefile .github docker | xargs perl -pi -e 's{github\.com/Rain-kl/Wavelet}{OpenFlare}g'
  • Step 3: Makefile 全部 Go 目标切到 backend/(对齐 Wavelet 惯例)

MODULE := $(shell cd backend && go list -m);每个含 go 命令的目标前缀 cd backend &&;swagger 输出目录仍为仓库根 docs/(swag init -g backend/main.go -o docs --dir backend),确保 frontend 消费路径不变。

  • Step 4: 修正内嵌与 ldflags 路径

build-embedded 中前端导出物拷贝目标改为 backend/internal/router/root/dist;.github/workflows/build-release.yml 三处 -X 'OpenFlare/plugins/{agent,relay,flared}/config.Version=...' 在 Task 12 前暂以 OpenFlare/internal/apps/... 为准(P5 再随插件落位改一次),并全部加 working-directory: backend。

  • Step 5: 验证编译与测试未回归
cd backend && go build ./... && go test ./... 2>&1 | tail -5

Expected: build 无输出;test 与 docs/superpowers/specs/baseline/go-test.txt 同结果(exit 0)。

  • Step 6: 验证 A 路 schema 仍一致(移动未改变行为)

重跑 Task 1 Step 2(输出改 schema-A2.sql、versions-A2.txt)并 diff -u 两个 schema 文件。 Expected: diff 为空。

  • Step 7: 提交
git add -A && git commit -m "refactor(layout): Go 代码迁入 backend/ 并将模块名简化为 OpenFlare"

Task 3: Vendoring 上游为第二 module(P2)

Files:

  • Create: backend/Wavelet/**、backend/Wavelet/go.mod、backend/go.work、scripts/sync-upstream.sh

  • Step 1: 拷入上游内核与插件(排除上游装配根与运行期产物)

cd /Users/ryan/Code/Go/OpenFlare-cordis/backend
mkdir -p Wavelet
rsync -a --delete --exclude data --exclude uploads --exclude docs --exclude cmd --exclude main.go --exclude downstream \
  /Users/ryan/Code/Go/Wavelet/backend/ Wavelet/

Expected: Wavelet/core、Wavelet/plugins/{drivers,infra,domain}、Wavelet/pkg、Wavelet/scripts、Wavelet/go.mod 存在。

  • Step 2: 建立双 module 关联
printf 'go 1.25.7\n\nuse (\n\t.\n\t./Wavelet\n)\n' > go.work
perl -pi -e 's{^go \d.*$}{go 1.25.7\n\nrequire Wavelet v0.0.0\n\nreplace Wavelet => ./Wavelet}' go.mod
go work sync && go build ./... && cd Wavelet && go build ./... && cd ..

Expected: 两次 go build ./... 均无输出。若 replace 插入位置与 go.mod 现有语句冲突,手工将 require Wavelet v0.0.0 / replace Wavelet => ./Wavelet 置于 go 指令之后。

  • Step 3: 写同步脚本(share/ 永不覆盖)
cat > ../scripts/sync-upstream.sh <<'SH'
#!/usr/bin/env bash
# 从 Wavelet 上游同步内核与插件;share/ 由 OpenFlare 拥有,显式排除。
set -euo pipefail
SRC="${1:-/Users/ryan/Code/Go/Wavelet/backend}"
DST="$(cd "$(dirname "$0")/../backend/Wavelet" && pwd)"
rsync -a --delete \
  --exclude 'data' --exclude 'uploads' --exclude 'docs' --exclude 'cmd' \
  --exclude 'main.go' --exclude 'downstream' --exclude 'share' \
  "$SRC/" "$DST/"
echo "synced upstream -> $DST (share/ preserved)"
SH
chmod +x ../scripts/sync-upstream.sh
  • Step 4: 验证 go.mod 中上游依赖不丢失

go build ./... 若报 missing go.sum entry,执行 go mod tidy && cd Wavelet && go mod tidy && cd .. && go build ./...。 Expected: 构建通过。

  • Step 5: 提交
git add -A && git commit -m "feat(cordis): 以第二 module 形态 vendoring Wavelet 内核与平台插件"

Task 4: share 共享层(P2,G3)

Files:

  • Create: backend/Wavelet/share/{protocol,wsclient,geoip,edge}/、backend/Wavelet/share/README.md

  • Step 1: 建立所有权声明

mkdir -p Wavelet/share
cat > Wavelet/share/README.md <<'MD'
# share — 跨插件共享资源层

本目录由 **OpenFlare 拥有**,不属于 Wavelet 上游同步范围(`scripts/sync-upstream.sh` 显式排除)。

用途:被 `server` 与 `agent`/`relay`/`flared` 中两个及以上插件共同消费、且无法放入
`Wavelet/pkg/`(禁止 import 业务代码)或插件内部(禁止插件互相 import)的资源。
MD
  • Step 2: 迁移四个真正跨插件的资源包
mkdir -p Wavelet/share/{protocol,wsclient,geoip,edge}
git mv pkg/protocol  Wavelet/share/protocol
git mv pkg/wsclient  Wavelet/share/wsclient
git mv pkg/geoip     Wavelet/share/geoip
git mv internal/apps/edge/logging Wavelet/share/edge/logging
grep -rl 'OpenFlare/pkg/protocol\|OpenFlare/pkg/wsclient\|OpenFlare/pkg/geoip\|OpenFlare/internal/apps/edge/logging' --include='*.go' . \
  | xargs perl -pi -e 's{OpenFlare/pkg/(protocol|wsclient|geoip)}{Wavelet/share/$1}g; s{OpenFlare/internal/apps/edge/logging}{Wavelet/share/edge/logging}g'
go build ./...

Expected: 构建通过。

  • Step 3: 消除 pkg 与上游 Wavelet/pkg 的重复

对每个 OpenFlare 保留包,先比对符号集,再决定归属:

for p in util logger trace cache mail response idgen ginutil httppool buildinfo testhelper; do
  echo "=== $p: openflare-only symbols ==="
  comm -23 <(grep -rhoE '^func [A-Z][A-Za-z0-9_]*' OpenFlare/pkg/$p/*.go 2>/dev/null | awk '{print $2}' | sort -u) \
           <(grep -rhoE '^func [A-Z][A-Za-z0-9_]*' Wavelet/pkg/$p/*.go 2>/dev/null | awk '{print $2}' | sort -u)
done
  • 上游同名包已覆盖的符号 → 删除 OpenFlare 副本,import 改指 Wavelet/pkg/...。

  • 仅 OpenFlare 需要的符号(如 util.Go、util.EscapeLike、util.DummyCheckPassword)→ 追加到 Wavelet/pkg/<p>/openflare.go(OpenFlare 拥有该 fork,P6 回流上游),再删除 OpenFlare 副本。

  • pkg/cap、pkg/push、pkg/render、pkg/pagesarchive → 与上游有交集者归 Wavelet/pkg,纯边缘共享者归 Wavelet/share。 每迁一包后 go build ./... && go test ./...,全部完成后提交。

  • Step 4: 验证共享层被真实使用并提交

grep -rn 'Wavelet/share' --include='*.go' plugins internal cmd 2>/dev/null | head -5
go build ./... && git add -A && git commit -m "feat(cordis): 新增 Wavelet/share 跨插件共享层并收敛重复 pkg 实现"

Expected: 至少 server 与一个 daemon 路径 import Wavelet/share/...。

Task 5: 迁移引擎骨架(P3)

Files:

  • Create: backend/internal/platform/migration/store.go(自持 sharedStore)

  • Create: backend/internal/platform/migration/engine.go(实现 core.MigrationEngine)

  • Test: backend/internal/platform/migration/store_test.go

  • Step 1: 写失败测试——版本表 DDL 与上游逐字节一致

func TestSharedStoreDDLMatchesUpstream(t *testing.T) {
	for _, dialect := range []string{"sqlite3", "postgres"} {
		s := &sharedStore{pluginID: "t", dialect: dialect}
		db, err := sql.Open("sqlite", ":memory:")
		require.NoError(t, err)
		t.Cleanup(func() { _ = db.Close() })
		require.NoError(t, s.CreateVersionTable(context.Background(), db))
		var got string
		require.NoError(t, db.QueryRow("SELECT sql FROM sqlite_master WHERE name='w_schema_versions'").Scan(&got))
		require.Contains(t, got, "plugin_id")
		require.Contains(t, got, "version_id")
		require.Contains(t, got, "PRIMARY KEY (plugin_id, version_id)")
	}
}
  • Step 2: 运行验证失败

Run: cd backend && go test ./internal/platform/migration/ -run TestSharedStoreDDLMatchesUpstream -v Expected: FAIL(sharedStore 未定义)。

  • Step 3: 从上游移植 sharedStore

将 Wavelet/backend/cmd/app.go:142-240 的 sharedStore(Tablename/CreateVersionTable/Insert/Delete/GetMigration/GetLatestVersion/ListMigrations/placeholder)整段复制到 store.go,仅改包名与注释;DDL 字符串一字不动。 Run: go test ./internal/platform/migration/ -run TestSharedStoreDDLMatchesUpstream Expected: PASS。

  • Step 4: 写失败测试——历史链按 plugin id 独立计数
func TestLegacyPluginResumesFromStampedMax(t *testing.T) {
	dir := t.TempDir()
	db, err := sql.Open("sqlite", filepath.Join(dir, "t.db"))
	require.NoError(t, err)
	defer func() { _ = db.Close() }()
	s := &sharedStore{pluginID: LegacyPluginID, dialect: "sqlite3"}
	ctx := context.Background()
	require.NoError(t, s.CreateVersionTable(ctx, db))
	require.NoError(t, s.Insert(ctx, db, goosedb.InsertRequest{Version: 202608090003}))
	got, err := s.GetLatestVersion(ctx, db)
	require.NoError(t, err)
	require.Equal(t, int64(202608090003), got)
}

Run: go test ./internal/platform/migration/ -run TestLegacyPluginResumesFromStampedMax Expected: FAIL(LegacyPluginID 未定义)。

  • Step 5: 实现引擎
// LegacyPluginID 是 OpenFlare 76 个历史 goose 文件的归属标识。
const LegacyPluginID = "openflare/legacy"

const (
	zoneImportSQLVersion     int64 = 202607120002
	zoneDropLegacySQLVersion int64 = 202607130001
)

engine.go 以上游 gooseEngine.Migrate(cmd/app.go:243-306)为基线,差异仅两处:

  1. 遍历 entries 时,entry.PluginID == LegacyPluginID 走三段式:provider.Up 到 zoneImportSQLVersion → 若 store.GetLatestVersion 落在 [zoneImportSQLVersion, zoneDropLegacySQLVersion) 则在事务内执行 zone.ImportLegacyTx → 继续 provider.Up 至末尾;
  2. 引擎入口先调用 resyncGooseVersionSequence(sqlDB)(自 migrator.go:158-183 原样移植,改收 *sql.DB 与方言参数),并在全部 entry 完成后调用 clearSystemConfigCache。

Run: go test ./internal/platform/migration/ -v Expected: PASS。

  • Step 6: 提交
git add backend/internal/platform/migration && git commit -m "feat(migration): 自持 w_schema_versions 引擎并保留 zone 导入钩子"

Task 6: 历史链归属与注册(P3)

Files:

  • Create: backend/internal/platform/migration/legacy/migrations/{postgres,sqlite}(76×2 文件)

  • Modify: backend/plugins/server/plugin.go(ctx.Migrations().Register(LegacyPluginID, legacyFS))

  • Step 1: 原样保真移动 SQL

mkdir -p internal/platform/migration/legacy/migrations
git mv internal/infra/persistence/migrator/goose/postgres internal/platform/migration/legacy/migrations/postgres
git mv internal/infra/persistence/migrator/goose/sqlite   internal/platform/migration/legacy/migrations/sqlite
git mv internal/infra/persistence/migrator/goose/clickhouse internal/platform/migration/legacy/migrations-clickhouse

Expected: git diff --stat -M HEAD~1 -- '*/*.sql' 显示纯 rename,零内容变更。

  • Step 2: embed 并注册
//go:embed legacy/migrations/postgres/*.sql legacy/migrations/sqlite/*.sql
var legacyFS embed.FS

ClickHouse 目录不放在 migrations/ 下(避免被 findMigrationFS 的 postgres|sqlite 目录探测命中),继续由既有 MigrateClickHouse + goose_clickhouse_version 驱动,行为与改造前完全一致。

  • Step 3: 校验双方言文件集合一致
diff <(ls internal/platform/migration/legacy/migrations/postgres) \
     <(ls internal/platform/migration/legacy/migrations/sqlite)

Expected: 无差异(否则 version_id 会跨方言漂移)。

  • Step 4: 构建并提交

go build ./... && go test ./... → PASS 后

git add -A && git commit -m "refactor(migration): 历史 goose 链原样归入 openflare/legacy 插件标识"

Task 7: 版本 stamp 桥接(P3)

Files:

  • Create: backend/internal/platform/migration/bridge.go

  • Create: backend/internal/platform/migration/lock.go

  • Test: backend/internal/platform/migration/bridge_test.go

  • Step 1: 写失败测试——老库逐行搬运且不重复执行

func TestBridgeCopiesLegacyVersions(t *testing.T) {
	db, err := sql.Open("sqlite", filepath.Join(t.TempDir(), "old.db"))
	require.NoError(t, err)
	defer func() { _ = db.Close() }()
	_, err = db.Exec(`CREATE TABLE goose_db_version (id INTEGER PRIMARY KEY AUTOINCREMENT,
		version_id INTEGER NOT NULL, is_applied INTEGER NOT NULL, timestamp DATETIME)`)
	require.NoError(t, err)
	_, err = db.Exec(`INSERT INTO goose_db_version (version_id,is_applied) VALUES
		(202606090001,1),(202607120002,1),(202608090003,1),(202608100001,0)`)
	require.NoError(t, err)

	n, err := Bridge(context.Background(), db, "sqlite3")
	require.NoError(t, err)
	require.Equal(t, 3, n) // 仅 is_applied=1 行被搬运

	again, err := Bridge(context.Background(), db, "sqlite3")
	require.NoError(t, err)
	require.Equal(t, 0, again) // 幂等

	var maxV int64
	require.NoError(t, db.QueryRow(
		"SELECT MAX(version_id) FROM w_schema_versions WHERE plugin_id='openflare/legacy'").Scan(&maxV))
	require.Equal(t, int64(202608090003), maxV)
}

Run: go test ./internal/platform/migration/ -run TestBridgeCopiesLegacyVersions Expected: FAIL(Bridge 未定义)。

  • Step 2: 实现 Bridge

要点:以 SELECT version_id FROM goose_db_version WHERE is_applied=1 为源(表不存在则直接返回 0,视为新库);INSERT INTO w_schema_versions (plugin_id, version_id) VALUES (?, ?) ON CONFLICT (plugin_id, version_id) DO NOTHING 逐行写入 openflare/legacy,并以 RowsAffected 汇总真实新增数;同时补写哨兵 0。占位符沿用 sharedStore.placeholder。禁止读写任何业务表。

  • Step 3: 写失败测试——迁移期互斥锁
func TestMigrationLockIsExclusive(t *testing.T) {
	db, err := sql.Open("sqlite", filepath.Join(t.TempDir(), "l.db"))
	require.NoError(t, err)
	defer func() { _ = db.Close() }()
	unlock, err := LockMigration(context.Background(), db, "sqlite3")
	require.NoError(t, err)
	_, err2 := db.Exec("INSERT INTO w_schema_versions VALUES ('x',1,'2026-01-01')")
	require.Error(t, err2) // 锁持有期间其他写者被阻塞/失败
	require.NoError(t, unlock())
	_, err3 := db.Exec("INSERT INTO w_schema_versions VALUES ('x',1,'2026-01-01')")
	require.NoError(t, err3)
}

Expected: 先 FAIL(LockMigration 未定义),实现后 PASS。

  • Step 4: 实现 LockMigration

PG:SELECT pg_advisory_lock($1) / pg_advisory_unlock($1)(固定 key 常量);SQLite:PRAGMA busy_timeout + 单写者事务(BEGIN IMMEDIATE)。返回 func() error 解锁,上游无 session locker,此处补齐多节点竞争保护。

  • Step 5: 接入装配根

Engine.Migrate 开头:unlock, err := LockMigration(...)、defer unlock();Bridge(...) 紧随 CreateVersionTable 之后、遍历 entries 之前。

  • Step 6: 全量测试并提交
go test ./internal/platform/migration/ -race && go test ./... && git add -A && git commit -m "feat(migration): 一次性 goose_db_version 到 w_schema_versions 桥接与迁移互斥锁"

Task 8: 上游插件初建与对齐(P3)

Files:

  • Create: backend/Wavelet/plugins/domain/*/migrations/{postgres,sqlite}/00002_openflare_align.sql(仅在确有缺口时)

  • Test: backend/internal/platform/migration/parity_test.go

  • Step 1: 列出对齐缺口(只读探测,不写库)

-- 对 A 路基线库执行,逐表比对上游模型期望列
SELECT table_name, column_name FROM information_schema.columns
WHERE table_name LIKE 'w\_%' ORDER BY table_name, column_name;

Expected: 产出「上游需要但现库缺失」的列/表清单;w_message_channels、w_message_bindings、w_message_pairing_codes 应出现在“缺失表”中(OpenFlare 无 bot 渠道表)。

  • Step 2: 缺口只以追加式迁移补齐
-- +goose Up
-- +goose StatementBegin
ALTER TABLE w_users ADD COLUMN IF NOT EXISTS need_change_password BOOLEAN NOT NULL DEFAULT FALSE;
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
SELECT 1;
-- +goose StatementEnd

Down 一律写成 no-op(禁止 DROP,避免回滚破坏现网数据)。sqlite 方言无 ADD COLUMN IF NOT EXISTS,需拆为独立文件并按上游惯例手写 sqlite 版本(不接受条件 SQL)。

  • Step 3: 一致性检查(B 路 vs A 路,复用 Task 1 的 dump harness)

legacydump_test.go 通过环境变量 OF_DUMP_SCHEMA=<输出路径> 触发,把「当前 embed 内的全部 SQL」应用到 t.TempDir() 的临时 sqlite 库并导出表/索引定义。B 路 = 迁移文件已全部就位后的 dump:

cd backend
OF_DUMP_SCHEMA=/tmp/of-b.sql go test ./internal/platform/migration/legacy -run DumpFreshSchema
diff -u docs/superpowers/specs/baseline/schema-A.sql /tmp/of-b.sql

Run: 上述 diff 命令 Expected: 初期 FAIL 并逐表列出漂移(缺失的 ADD COLUMN、被 align 迁移改动的列);补齐 align 迁移后 diff 为空。

  • Step 4: 提交
git add -A && git commit -m "fix(migration): 追加式 align 迁移使全新安装与升级路径 schema 收敛"

Task 9: 三方一致性门禁工具(P3 验收)

Files:

  • Create: backend/cmd/migrate-audit/main.go

  • Modify: Makefile(migrate-audit 目标)

  • Step 1: 实现只读 dump 与 diff CLI

// dump: sqlite  -> SELECT name,sql FROM sqlite_master WHERE type IN ('table','index') ORDER BY name;
//        postgres-> information_schema.columns + pg_indexes ORDER BY 1,2
// mode: A=基线文件  B=全新生成  C=恢复 A 后再跑一次
func main() {
	mode := flag.String("mode", "B", "A|B|C")
	dialect := flag.String("dialect", "sqlite3", "sqlite3|postgres")
	out := flag.String("out", "", "dump 输出路径")
	flag.Parse()
	// 1) 临时目录一律 os.MkdirTemp("", "of-audit-"),禁止写源码树
	// 2) B: 空库 -> bridge(no-op) -> Engine.Migrate(entries)
	// 3) C: 复制 A 的 db 文件 -> bridge(stamp) -> Engine.Migrate -> 断言 results 为空
	// 4) 输出规范化 dump 到 --out,交由 Makefile 的 diff 判定
}
  • Step 2: Makefile 目标与期望输出
migrate-audit:
	cd backend && go run ./cmd/migrate-audit --mode B --out /tmp/of-b.sql
	cd backend && go run ./cmd/migrate-audit --mode C --base docs/superpowers/specs/baseline/schema-A.sql --out /tmp/of-c.sql
	diff -u docs/superpowers/specs/baseline/schema-A.sql /tmp/of-b.sql
	@sqlite3 /tmp/of-c.db "SELECT count(*) FROM w_schema_versions" >/dev/null
	@grep -q 'no-op' /tmp/of-c.log && echo "MIGRATION PARITY OK"

Expected: make migrate-audit 两个 diff 全空并打印 MIGRATION PARITY OK。

  • Step 3: 提交并在 changelog 记录

docs/changelog/index.md 的 [Unreleased] 增补:迁移改为按插件版本表 w_schema_versions 管理,已部署库通过一次性桥接保持不重跑。

Task 10: 架构门禁与 CI 收口(P3 收尾)

  • Step 1: 接入上游架构检查脚本

make arch-check → bash backend/Wavelet/scripts/check_cordis_architecture.sh,并按 OpenFlare 布局增补 backend/plugins 与 backend/Wavelet/share 两条扫描根。 Expected: 首次运行对 P3 范围(尚无 4 插件)通过。

  • Step 2: 四二进制构建回归 + code-check + format
make build-backend && make code-check && make format
cd backend && go test -race ./... && go test -shuffle=on ./...

Expected: 全部 exit 0;-shuffle=on 无顺序依赖失败。

  • Step 3: 前端零改动断言与提交
git diff --name-only main...HEAD -- frontend/ | tee /tmp/fe.diff
test ! -s /tmp/fe.diff && echo "FRONTEND UNTOUCHED"
git add -A && git commit -m "chore(ci): cordis 布局路径门禁与迁移一致性检查接入"

Expected: 打印 FRONTEND UNTOUCHED。


完成定义(本计划)

  • backend/ 双 module 构建通过;Wavelet/ 与上游差异仅 share/ 与被显式记录的回流项。
  • make migrate-audit 三方 diff 为空;已部署库升级演练零 SQL 变更。
  • go test -race ./...、-shuffle=on、make code-check 全绿;frontend/ 零改动。
  • 4 个插件与 daemon 内核化由 P4/P5 承接,本计划不含业务路由迁移。

附录 A:server 插件接入内核的可行路径(P4 前置调查结论)

调查上游 core 与 plugins/drivers/driver_http 后确认:ctx.Router().Use 不会作用到 gin engine(只把中间件摊平进 RouteDefinition),且以下 OpenFlare 现有 engine 级行为 在内核中没有插件级贡献点:

需求 现状 出处
RedirectTrailingSlash = false 内核无处设置(全仓 0 命中) router/router.go:42-43
自有 CORS / logger 分级 / session cookie 语义与 fatal 策略 硬编码在 driver_http/engine.go router.go:45-79
Next.js 静态导出 NoRoute(含动态页 shell 回退) 被 registerFrontend 占用且缺该回退 router/root/frontend.go:104-111
http.Server 读写字节超时 / TLS 仅 ReadHeaderTimeout router.go:83-87
绑定成功后回调 onStarted 无驱动级回调 router.go:89-95
路由前缀取自 app.api_prefix ctx.Router() 只收字面量 config.example.yaml:18

结论:不改上游即可闭合——driver_http.New(driver_http.WithEngine(自建 engine)) 已存在。 装配根(backend/cmd)负责构造 gin engine(保留 Recovery/CORS/session/otelgin/ RedirectTrailingSlash/NoRoute 与超时),把 engine 交给 http 驱动;server 插件的 Apply 只做 ctx.Router().Group(...) 声明式路由注册。其余两点:

  • onStarted 改由 ctx.Events().On("app:ready") + ctx.Driver(core.DriverTypeHTTP).Addr() 提供; 启动横幅在 app.Run() 前后打印,不再依赖回调。
  • 免鉴权路径:保留 OpenFlare 自有 oauth.LoginRequired()(不查白名单),语义与今天一致。 注意内核 ctx.Router().RegisterWhitelist 目前是空转(driver_http.SetWhitelist/ IsPathWhitelisted 无非测试调用方),不得依赖它放行——否则会 401。

待办:以上三项(engine 中间件贡献点、NoRoute 贡献点、白名单真正生效)应回流上游 Wavelet, 届时可去掉 WithEngine 例外。契约校验仍以 docs/superpowers/specs/baseline/routes.txt 的 232 条 操作(方法×路径,.Any() 计 7)为准,与代码内 215 个注册调用点不矛盾。

附录 B:server 插件化已就位件与剩余步骤

已落地并验证

  • router.BuildEngine() 从 Serve() 中抽出(仅构造引擎与中间件,不监听),Serve() 复用它,行为不变。
  • router/routes_dump_test.go:以 OF_DUMP_ROUTES=<path> 导出现存注册路径的 (方法 路径) 全集, 已固化基线 docs/superpowers/specs/baseline/routes-engine.txt:256 条(含 20 条尾部斜杠变体, 全为 GET /api/v1/d/*)。这是 server 插件化唯一的路由对拍基准(232 是 swagger 操作数, 二者差异来自斜杠变体与 Any 展开,不是矛盾)。
  • 内核补丁:RouterExtension 新增 HandleRaw(保留尾部斜杠)与 BasePath,作用域包装器同步实现并登记反注册; 用例 core/extpoints/router_raw_test.go 覆盖;补丁登记在 backend/OpenFlare/upstream-patches.md, sync-upstream.sh 同步后会打印需确认的补丁文件。

剩余步骤(按序,每步都要过对拍)

  1. openflare/apiutil.RegisterCollection 改用 route.Handle(method,"") + route.HandleRaw(method,"/") (取代 route.BasePath() 的 gin 用法与 []gin.HandlerFunc→[]any 转换)。
  2. router/root:RegisterDefaultRootRoutes/RegisterCustomRootRoutes 参数改 core.RouterExtension; RegisterFrontend(NoRoute + 静态资源,含 Next.js 动态 shell 回退)保留在引擎层,由 BuildEngine() 调用。
  3. 33 个 *gin.RouterGroup 注册函数改 core.RouterExtension(本轮已验证方法面只需 GET/POST/PUT/DELETE/Group/Use,且无人接收返回值),随后删除 registerRoutes()。
  4. 新增 OpenFlare/plugins/server/plugin.go:Name()="server",Apply(ctx) 内以 ctx.Router().Group(config.Config.App.APIPrefix) 复刻 registerRoutes 的树 (/api → /v1 → v1/user/admin/openflare;根级 4 条走注册表绝对路径)。
  5. 装配根切换:cmd/{api,all}.go 改为 core.NewApp(core.WithProfile(...)) + app.Use(server.New()) + app.Use(driver_http.New(driver_http.WithEngine(router.BuildEngine()))), 启动横幅改挂 ctx.Events().On("app:ready") 并用 httpDrv.Addr() 取实际监听地址; otel_trace.Shutdown/bootstrap.Stop 迁入 ctx.OnDispose,删除 Serve() 自带的信号循环。
  6. 对拍:新测试把插件注册表产出的 (方法 路径) 集合与 routes-engine.txt 逐条比对, 差异必须为空;再实跑 server(sqlite)curl 健康检查、一个免鉴权端点与一个需鉴权端点(期望 401)。