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 一致性门禁,
确保已部署库不重跑历史、不丢数据。
32 KiB
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/(importWavelet/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 swaggerexit 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/clickhouse14 文件 +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)为基线,差异仅两处:
- 遍历
entries时,entry.PluginID == LegacyPluginID走三段式:provider.Up到zoneImportSQLVersion→ 若store.GetLatestVersion落在[zoneImportSQLVersion, zoneDropLegacySQLVersion)则在事务内执行zone.ImportLegacyTx→ 继续provider.Up至末尾; - 引擎入口先调用
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同步后会打印需确认的补丁文件。
剩余步骤(按序,每步都要过对拍)
openflare/apiutil.RegisterCollection改用route.Handle(method,"")+route.HandleRaw(method,"/")(取代route.BasePath()的 gin 用法与[]gin.HandlerFunc→[]any转换)。router/root:RegisterDefaultRootRoutes/RegisterCustomRootRoutes参数改core.RouterExtension;RegisterFrontend(NoRoute+ 静态资源,含 Next.js 动态 shell 回退)保留在引擎层,由BuildEngine()调用。- 33 个
*gin.RouterGroup注册函数改core.RouterExtension(本轮已验证方法面只需 GET/POST/PUT/DELETE/Group/Use,且无人接收返回值),随后删除registerRoutes()。 - 新增
OpenFlare/plugins/server/plugin.go:Name()="server",Apply(ctx)内以ctx.Router().Group(config.Config.App.APIPrefix)复刻registerRoutes的树 (/api→/v1→ v1/user/admin/openflare;根级 4 条走注册表绝对路径)。 - 装配根切换:
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()自带的信号循环。 - 对拍:新测试把插件注册表产出的 (方法 路径) 集合与
routes-engine.txt逐条比对, 差异必须为空;再实跑 server(sqlite)curl健康检查、一个免鉴权端点与一个需鉴权端点(期望 401)。