mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-05 15:26:36 +08:00
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 一致性门禁,
确保已部署库不重跑历史、不丢数据。
This commit is contained in:
@@ -0,0 +1,620 @@
|
||||
# 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: 导出改造前路由与测试基线**
|
||||
|
||||
```bash
|
||||
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 对拍)**
|
||||
|
||||
```bash
|
||||
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: 提交基线**
|
||||
|
||||
```bash
|
||||
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 历史)**
|
||||
|
||||
```bash
|
||||
mkdir -p backend
|
||||
git mv cmd internal pkg main.go go.mod go.sum backend/
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 改 module 名并重写全部 import 路径**
|
||||
|
||||
```bash
|
||||
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: 验证编译与测试未回归**
|
||||
|
||||
```bash
|
||||
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: 提交**
|
||||
|
||||
```bash
|
||||
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: 拷入上游内核与插件(排除上游装配根与运行期产物)**
|
||||
|
||||
```bash
|
||||
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 关联**
|
||||
|
||||
```bash
|
||||
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/` 永不覆盖)**
|
||||
|
||||
```bash
|
||||
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: 提交**
|
||||
|
||||
```bash
|
||||
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: 建立所有权声明**
|
||||
|
||||
```bash
|
||||
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: 迁移四个真正跨插件的资源包**
|
||||
|
||||
```bash
|
||||
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 保留包,先比对符号集,再决定归属:
|
||||
|
||||
```bash
|
||||
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: 验证共享层被真实使用并提交**
|
||||
|
||||
```bash
|
||||
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 与上游逐字节一致**
|
||||
|
||||
```go
|
||||
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 独立计数**
|
||||
|
||||
```go
|
||||
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: 实现引擎**
|
||||
|
||||
```go
|
||||
// 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: 提交**
|
||||
|
||||
```bash
|
||||
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**
|
||||
|
||||
```bash
|
||||
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
|
||||
//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: 校验双方言文件集合一致**
|
||||
|
||||
```bash
|
||||
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 后
|
||||
```bash
|
||||
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: 写失败测试——老库逐行搬运且不重复执行**
|
||||
|
||||
```go
|
||||
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: 写失败测试——迁移期互斥锁**
|
||||
|
||||
```go
|
||||
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: 全量测试并提交**
|
||||
|
||||
```bash
|
||||
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: 列出对齐缺口(只读探测,不写库)**
|
||||
|
||||
```sql
|
||||
-- 对 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: 缺口只以追加式迁移补齐**
|
||||
|
||||
```sql
|
||||
-- +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:
|
||||
|
||||
```bash
|
||||
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: 提交**
|
||||
|
||||
```bash
|
||||
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**
|
||||
|
||||
```go
|
||||
// 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 目标与期望输出**
|
||||
|
||||
```make
|
||||
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**
|
||||
|
||||
```bash
|
||||
make build-backend && make code-check && make format
|
||||
cd backend && go test -race ./... && go test -shuffle=on ./...
|
||||
```
|
||||
Expected: 全部 exit 0;`-shuffle=on` 无顺序依赖失败。
|
||||
|
||||
- [ ] **Step 3: 前端零改动断言与提交**
|
||||
|
||||
```bash
|
||||
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)。
|
||||
@@ -0,0 +1,98 @@
|
||||
# server 插件复用上游能力清理计划
|
||||
|
||||
**目标**:`backend/OpenFlare/plugins/server` 里大量能力上游 Wavelet 已提供,删除本地副本改为复用;
|
||||
按规约判定归属——通用能力回流上游,OpenFlare 业务留在插件内。
|
||||
|
||||
**事实基线**:`server` 实测 **68,117** 行非测试 Go 代码(早先记的 29.6k 只是 `openflare/` 一个域)。
|
||||
路由契约冻结基准:`docs/superpowers/specs/baseline/routes-engine.txt`(256 条 方法+路径)与
|
||||
`routes.txt`(232 条 swagger 操作),由 `plugin_parity_test.go` 逐条把守。
|
||||
|
||||
## 关键防线(先读,否则会做错)
|
||||
|
||||
直接挂上游插件**会破坏契约**,以下差异必须逐项处理,不允许“上游有就换”:
|
||||
|
||||
1. **验证码门禁丢失**:OF 在 `POST /api/v1/user/{login,register,send-email-code}` 上挂了
|
||||
`cap.VerifyMiddleware`(`router/v1/user.go:71-73`),上游 `domain/user` 没有该门禁
|
||||
(`user/plugin.go:126-129`)。直接替换=安全回归。
|
||||
2. **路径变化**:`/api/cap/{challenge,redeem}`(OF,挂在 `/api` 组)vs `/api/v1/cap/*`(上游,且多一个 `GET`);
|
||||
`GET /api/health` vs `/healthz` + `/api/healthz`;`/api/v1/upload` 的 `GET /my`、`PUT /:id`、
|
||||
`GET /download/:id`、`POST /download/batch` vs 上游 `GET ""`、`POST /batch-download`;
|
||||
OF 独有 `GET /api/v1/user/self`。
|
||||
3. **公共配置响应体不同**:`GET /api/v1/config/public` 路径相同但 JSON 字段不同,SPA 直接消费。
|
||||
4. 上游额外需要表 `w_message_channels` / `w_message_bindings` / `w_message_pairing_codes`(OF 库中不存在)。
|
||||
除此之外 13 张共享 `w_*` 表**列级零差异**;唯一缺口 `w_access_tokens.description/expires_at` 上游 Go 模型也未使用,非阻塞。
|
||||
|
||||
## 判定清单
|
||||
|
||||
| 包 | 行数 | 上游对应 | 判定 |
|
||||
| :-- | --: | :-- | :-- |
|
||||
| `shared/response` | 142 | `pkg/response` | 删(7/7 `Abort*` 逐字一致,仅风格差异) |
|
||||
| `pkg/logger` | 382 | `pkg/logger` | 删(`Config` 字段完全一致) |
|
||||
| `pkg/mail` | 266 | `pkg/mail` | 删 |
|
||||
| `pkg/httppool` | 106 | `pkg/httppool` | 删(逐字节相同) |
|
||||
| `pkg/trace` | 148 | `pkg/trace` | 删(差异仅默认 tracer 名,由 `app.app_name`/`otel.tracer_name` 决定) |
|
||||
| `pkg/cache/ram` | 305 | `pkg/cache/ram` | 删(OF 用裸 `go`,上游用 `util.Go`,换过去是修复) |
|
||||
| `infra/persistence/idgen` | 47 | `pkg/idgen` | 删(上游为超集) |
|
||||
| `infra/persistence/batchwriter` | 337 | `pkg/batchwriter` | 删 |
|
||||
| `listener` | 42 | `core/events.go Subscribe[T]` | 删,改事件订阅 |
|
||||
| `testhelper` | 479 | `pkg/testhelper` | 合并,仅留 `SetupLogStoresForTest` |
|
||||
| `pkg/cache/disk` | 412 | `pkg/cache/disk` | 先回流 OF 的 5 处类型断言 `ok` 守卫(通用健壮性修复),再删本地副本 |
|
||||
| `pkg/cap` | 511 | `plugins/domain/cap/pow` | **不能直接复用**(跨插件 import 被禁)。二选一:pow 提取到上游 `pkg/pow`,或留 `server` 内并放弃“复用” |
|
||||
| `infra/objectstore`、`infra/diskcache` | 1,058 | `plugins/infra/storage/*` | 改走 `contracts.StorageService`/上游 infra(P6) |
|
||||
| `infra/task`(worker/scheduler/handlers) | 1,125 | `plugins/drivers/driver_asynq_*` | 换内核驱动(P6) |
|
||||
| `infra/config`(全局 `Config` 单例) | 444 | `core/config.go` + `plugins/infra/config` | 改 `ctx.Config().Bind`(P6,18 处引用) |
|
||||
| `infra/persistence`(DB/Redis 初始化) | 639 | `plugins/infra/database` | 换 `contracts.DBService`(P6) |
|
||||
| `infra/persistence/migrator` | 298 | `w_schema_versions` + 每插件迁移 | 见 foundation 计划 P3(含历史 stamp 桥接) |
|
||||
| `repository/logstore` | 3,286 | `plugins/domain/risk_control/logstore` | 近似分叉:合并并保留 OF 的 `AccessLogStore`/`ObservabilityStore` 接口 |
|
||||
| `repository/analytics` | 2,848 | 同上(仅用户访问日志半边) | 合并半边;`node_*` 与 CH 维护上游没有,留 `server` |
|
||||
| `repository`、`model` | 8,527 | 各上游域插件内 | 拆分:`w_*` 部分随 P6 归上游,`of_*` 留 `server` |
|
||||
| `oauth`、`user`、`admin`、`upload`、`cap`、`config`、`health` | ~7,600 | `plugins/domain/{auth,user,admin,upload,cap,system}` | **受“关键防线”约束**,逐端点判定后合并,禁止整体替换 |
|
||||
| `admin/push` | 1,776 | `plugins/domain/message_gateway` | 合并并保留 OF 的 `RegisterBuiltInEvent`/`RegisterChannelDefinition`/`SyncEvents` 钩子 |
|
||||
| `admin/updater` | 832 | 上游仅 `GetUpdateStatus/ApplyUpdate` | OF 的 Prepare/Apply/Finish 三段式为业务能力,保留 |
|
||||
| `openflare/`(27 子包) | 29,678 | 无 | 保留(产品逻辑);`apiutil` 与 `integration/githubrelease` 若被多插件消费再移 `share` |
|
||||
| `router`、`router/root`、`router/v1` | 1,242 | `core/extpoints` + `driver_http` | 保留为接线层,直至上游具备引擎中间件与 NoRoute 贡献点 |
|
||||
|
||||
## 执行顺序
|
||||
|
||||
1. **T1 纯风格差异**(`shared/response`、`pkg/{logger,mail,httppool,trace,cache/ram}`、
|
||||
`infra/persistence/{idgen,batchwriter}`):改 import → 删本地副本 → 全量门禁。
|
||||
2. **T2 `listener` 改事件订阅**、`testhelper` 合并。
|
||||
3. **T3 `pkg/cache/disk` 断言守卫回流上游**,再删本地副本。
|
||||
4. **T4 基础设施换内核**(`infra/config` → `ctx.Config()`;`infra/persistence` → `contracts.DBService`;
|
||||
`infra/task` → asynq 驱动):与 P3/P4b 合并推进。
|
||||
5. **T5 平台域逐个合并**(oauth/user/admin/upload/cap/config/health/push):每个域先列差异
|
||||
(路由、门禁、响应体、表列),差异以 `server` 内薄封装或回流上游解决,逐域过 256 条对拍。
|
||||
6. **T6 `repository`/`model` 按表所有权拆分**:`w_*` 归上游,`of_*` 留 `server`。
|
||||
|
||||
每步完成判据:`go build ./...` + `go test ./...` 全绿 + `plugin_parity_test` 零差异 +
|
||||
`make swagger` 232 条零差异 + `golangci-lint` 0 issues + `frontend/` 零改动。
|
||||
|
||||
## 执行记录与阻塞点
|
||||
|
||||
### T1 已完成(提交 `0adbfc1a`)
|
||||
|
||||
删除并改用上游:`shared/response`→`pkg/response`、`pkg/{logger,mail,trace,httppool,cache/ram}`、
|
||||
`infra/persistence/batchwriter`→`pkg/batchwriter`。共 7 个本地包消失,`go test` 包数 144→138。
|
||||
|
||||
证据:256 条路由对拍零差异;232 条 swagger 操作零增减,且**把两个随包路径改名的定义
|
||||
(`…shared_response.Any`→`response.Any`、`…pkg_logger.LogEntry`→`logger.LogEntry`)归一化后,
|
||||
新旧 swagger.json 深度相等**——即接口形状完全未变。`$ref` 名由 Go 包路径派生,属生成物命名变化。
|
||||
|
||||
### 新发现的阻塞点(勿重复踩)
|
||||
|
||||
1. **`pkg/idgen` 暂不能换**:上游 `pkg/idgen` 要求先 `Init(nodeID)` 否则 panic
|
||||
(`idgen: Init must be called before generating IDs`),而 OpenFlare 副本是懒初始化。
|
||||
5 个 push/user 用例直接 panic。必须与 T4(`infra/config`、`infra/persistence` 改走
|
||||
`ctx.Config()`/`contracts.DBService`)一起做,在 bootstrap 阶段显式 `Init`。
|
||||
2. **`pkg/cap` 不能“复用上游”**:上游 pow 实现在 `plugins/domain/cap/pow` 里,
|
||||
跨插件 import 被架构门禁禁止。合规出路二选一:把 pow 提取为上游 `pkg/pow`(通用能力,
|
||||
按规约回流 Wavelet 后两边共用),或承认它是 server 的业务实现留在插件内。
|
||||
3. **`listener` 需先有内核上下文**:`core.Subscribe[T](bus, topic, fn)` 要 `*core.EventBus` 实例,
|
||||
而调用方(`oauth/handler_callback.go`、`user/routers.go`、`admin/push/custom_events/*`)都在
|
||||
内核之外、手上没有 `*core.Context`。现在改只会把全局变量从包级切片挪到包级总线。
|
||||
待 `server` 的 `Apply`/bootstrap 拿到 ctx 后随 T4 一起做。
|
||||
4. **`pkg/trace` 覆盖缺口(非本次引入)**:核实后上游与 OpenFlare 副本都是 4 个同名文件、
|
||||
都**没有**测试文件,因此删除本地副本没有降低覆盖率。要补测试应直接补在上游 `backend/pkg/trace/`。
|
||||
5. **`pkg/cache/disk` 已完成**:5 处类型断言的 `ok` 守卫按规约回流上游
|
||||
(Wavelet `f3d85d5`,附 `cache_corruption_test.go`:去掉守卫即 panic、加上守卫 4 用例全过),
|
||||
上游的 `Default()` 全局实例保留(下游不使用它,不构成行为差异);本地副本已删除。
|
||||
Reference in New Issue
Block a user