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:
ryan
2026-08-29 19:28:39 +08:00
parent 9f79fb9969
commit dbaa3bf140
1327 changed files with 91634 additions and 4157 deletions
+2 -2
View File
@@ -83,8 +83,8 @@ invoice.FilePath = "uploads/2026/01/02/123.pdf"
import (
"bytes"
"github.com/Rain-kl/Wavelet/internal/apps/upload"
"github.com/Rain-kl/Wavelet/internal/model"
"OpenFlare/internal/apps/upload"
"OpenFlare/internal/model"
)
func ingestMirrorFile(ctx context.Context, userID uint64, data []byte, hash, filename, mime, ext string) (model.Upload, error) {
+3 -3
View File
@@ -54,7 +54,7 @@ Wavelet 后端路由采用了**严格的框架层与业务层隔离机制**。
package root
import (
"github.com/Rain-kl/Wavelet/internal/apps/custom"
"OpenFlare/internal/apps/custom"
"github.com/gin-gonic/gin"
)
@@ -75,7 +75,7 @@ Wavelet 后端路由采用了**严格的框架层与业务层隔离机制**。
package v1
import (
"github.com/Rain-kl/Wavelet/internal/apps/custom"
"OpenFlare/internal/apps/custom"
"github.com/gin-gonic/gin"
)
@@ -127,7 +127,7 @@ internal/
在 `internal/apps/custom/routers.go` 中编写 Handler:
- 负责请求参数绑定与校验(使用 `ShouldBindJSON`/`ShouldBindQuery`)。
- 负责提取 Session / 用户身份。
- 调用业务逻辑层,并使用 `github.com/Rain-kl/Wavelet/internal/shared/response` 统一返回响应:
- 调用业务逻辑层,并使用 `OpenFlare/internal/shared/response` 统一返回响应:
- 成功时返回:`response.OK(data)` 或 `response.OKNil()`
- 失败时返回:`response.Err(msg)`
- 编写规范的 Swagger 注释。
@@ -6,8 +6,8 @@ package references
import (
"net/http"
"github.com/Rain-kl/Wavelet/internal/service"
"github.com/Rain-kl/Wavelet/internal/util"
"OpenFlare/internal/service"
"OpenFlare/internal/util"
"github.com/gin-gonic/gin"
)
@@ -8,7 +8,7 @@ import (
"errors"
"fmt"
"github.com/Rain-kl/Wavelet/pkg/logger"
"OpenFlare/pkg/logger"
"go.uber.org/zap"
)
@@ -8,7 +8,7 @@ import (
"errors"
"fmt"
"github.com/Rain-kl/Wavelet/pkg/logger"
"OpenFlare/pkg/logger"
"go.uber.org/zap"
)
@@ -10,7 +10,7 @@
package upload
import (
"github.com/Rain-kl/Wavelet/internal/infra/task"
"OpenFlare/internal/infra/task"
)
// 异步任务类型标识。格式建议为 "{module}:{action}"。
@@ -83,7 +83,7 @@ package upload
import (
"context"
"github.com/Rain-kl/Wavelet/internal/infra/task"
"OpenFlare/internal/infra/task"
)
type CleanupUnusedUploadsHandler struct{}
@@ -114,7 +114,7 @@ import (
"fmt"
"strings"
"github.com/Rain-kl/Wavelet/internal/infra/task"
"OpenFlare/internal/infra/task"
)
type SendEmailPayload struct {
@@ -169,9 +169,9 @@ func (h *SendEmailHandler) Execute(ctx context.Context, payload []byte) (*task.T
package handlers
import (
"github.com/Rain-kl/Wavelet/internal/apps/upload"
"github.com/Rain-kl/Wavelet/internal/apps/user"
"github.com/Rain-kl/Wavelet/internal/infra/task"
"OpenFlare/internal/apps/upload"
"OpenFlare/internal/apps/user"
"OpenFlare/internal/infra/task"
)
func Register() {
+3 -3
View File
@@ -38,8 +38,8 @@ import (
"context"
"time"
"github.com/Rain-kl/Wavelet/internal/apps/admin/push"
"github.com/Rain-kl/Wavelet/internal/listener"
"OpenFlare/internal/apps/admin/push"
"OpenFlare/internal/listener"
)
var NewUserRegistered = push.EventMetadata{
@@ -84,7 +84,7 @@ func Register() {
在业务逻辑完成处(如 `internal/apps/user/routers.go`)仅 import `internal/listener` 并发射事件:
```go
import "github.com/Rain-kl/Wavelet/internal/listener"
import "OpenFlare/internal/listener"
func Register(c *gin.Context) {
// ... 注册成功逻辑 ...
+13 -10
View File
@@ -20,7 +20,7 @@ env:
FRONTEND_DIR: frontend
FRONTEND_BUILD_COMMAND: pnpm build:embed
FRONTEND_OUT_DIR: frontend/out
EMBED_DIST_DIR: internal/router/root/dist
EMBED_DIST_DIR: backend/OpenFlare/plugins/server/router/root/dist
EXTRA_FILES: |
LICENSE
README.md
@@ -212,18 +212,18 @@ jobs:
binary_name="${binary_name}.exe"
fi
ldflags="$GO_LDFLAGS -X github.com/Rain-kl/Wavelet/internal/buildinfo.Version=$VERSION -X github.com/Rain-kl/Wavelet/internal/buildinfo.BuildTime=$BUILD_DATE"
ldflags="$GO_LDFLAGS -X Wavelet/pkg/buildinfo.Version=$VERSION -X Wavelet/pkg/buildinfo.BuildTime=$BUILD_DATE"
build_args=(
-trimpath
-ldflags "$ldflags"
-o "dist/$binary_name"
-o "$GITHUB_WORKSPACE/dist/$binary_name"
)
if [[ -n "$GO_BUILD_TAGS" ]]; then
build_args=(-tags "$GO_BUILD_TAGS" "${build_args[@]}")
fi
go build "${build_args[@]}" "$GO_MAIN"
( cd backend && go build "${build_args[@]}" "$GO_MAIN" )
- name: Package artifact
id: package
@@ -307,9 +307,10 @@ jobs:
ASSET_NAME: ${{ matrix.asset_name }}
VERSION: ${{ needs.create-release.outputs.version }}
run: |
cd backend
go mod download
mkdir -p dist
go build -trimpath -ldflags "-s -w -X 'github.com/Rain-kl/Wavelet/internal/apps/agent/config.Version=$VERSION'" -o "dist/$ASSET_NAME" ./cmd/agent/main.go
mkdir -p "$GITHUB_WORKSPACE/dist"
go build -trimpath -ldflags "-s -w -X 'Wavelet/OpenFlare/plugins/agent/config.Version=$VERSION'" -o "$GITHUB_WORKSPACE/dist/$ASSET_NAME" ./cmd/agent/main.go
- name: Upload release artifact
uses: softprops/action-gh-release@v2
@@ -354,9 +355,10 @@ jobs:
ASSET_NAME: ${{ matrix.asset_name }}
VERSION: ${{ needs.create-release.outputs.version }}
run: |
cd backend
go mod download
mkdir -p dist
go build -trimpath -ldflags "-s -w -X 'github.com/Rain-kl/Wavelet/internal/apps/relay/config.Version=$VERSION'" -o "dist/$ASSET_NAME" ./cmd/relay/main.go
mkdir -p "$GITHUB_WORKSPACE/dist"
go build -trimpath -ldflags "-s -w -X 'Wavelet/OpenFlare/plugins/relay/config.Version=$VERSION'" -o "$GITHUB_WORKSPACE/dist/$ASSET_NAME" ./cmd/relay/main.go
- name: Upload release artifact
uses: softprops/action-gh-release@v2
@@ -401,9 +403,10 @@ jobs:
ASSET_NAME: ${{ matrix.asset_name }}
VERSION: ${{ needs.create-release.outputs.version }}
run: |
cd backend
go mod download
mkdir -p dist
go build -trimpath -ldflags "-s -w -X 'github.com/Rain-kl/Wavelet/internal/apps/flared/config.Version=$VERSION'" -o "dist/$ASSET_NAME" ./cmd/flared/main.go
mkdir -p "$GITHUB_WORKSPACE/dist"
go build -trimpath -ldflags "-s -w -X 'Wavelet/OpenFlare/plugins/flared/config.Version=$VERSION'" -o "$GITHUB_WORKSPACE/dist/$ASSET_NAME" ./cmd/flared/main.go
- name: Upload release artifact
uses: softprops/action-gh-release@v2
+7
View File
@@ -87,3 +87,10 @@ profile.cov
# i18n 生成物(由 scripts/merge-i18n-fragments.mjs 从 fragments 生成)
frontend/messages/zh-CN.json
frontend/messages/en.json
# 上游 vendoring 目录内禁止出现运行期产物
backend/plugins/**/uploads/
backend/plugins/**/dist/
backend/core/**/dist/
backend/pkg/**/uploads/
/backend/OpenFlare/plugins/server/upload/filesrv/uploads/
+23 -11
View File
@@ -72,7 +72,7 @@ Strong success criteria let you loop independently. Weak criteria ("make it work
| `new-async-task` | Asynq 任务、定时任务、TaskHandler、任务元数据 |
| `new-setting` | 系统/业务/公开设置、`/admin/system`、`/admin/settings` |
| `database-migration` | 表结构、goose 迁移(PG/SQLite/ClickHouse)、seed |
| `logstore` | 日志/分析用途表、`internal/repository/logstore`、切换日志主库、PG/SQLite 回落 |
| `logstore` | 日志/分析用途表、`backend/internal/repository/logstore`、切换日志主库、PG/SQLite 回落 |
| `clickhouse-batchwriter` | CH 批量写入、batchwriter、分析表 flush/背压 |
| `file-upload` | 上传/摄取、`upload.Ingest`、文件访问、`w_uploads` |
| `cache-framework` | 业务缓存(RAM/Redis/DB)、失效、多节点同步 |
@@ -82,19 +82,31 @@ Strong success criteria let you loop independently. Weak criteria ("make it work
## 硬性约束
### 上游/下游改动归属(Cordis)
- 触碰框架目录 `backend/{core,pkg,plugins}` 前,先判断能力归属:
- **通用能力**(与 OpenFlare 业务无关、任何下游都用得上)→ 必须同步在 **Wavelet 上游**完成修改,
本仓库只能通过 `scripts/sync-upstream.sh` 取得,不得长期持有本地补丁。
- **非通用能力**(OpenFlare 业务特有)→ 在自己的插件内(`backend/OpenFlare/plugins/<name>/`)实现,
或新建一个下游插件,禁止塞进上游目录。
- 开发下游功能优先**复用上游已有能力**(`core/contracts`、`backend/plugins/*`、`backend/pkg/*`);
发现上游已提供而下游仍保留本地副本的,删除本地副本改为复用,或把差量回流上游。
- 上游暂缺而确属通用能力时,可先在本仓库实现并登记到 `backend/OpenFlare/upstream-patches.md`
(`sync-upstream.sh` 同步后会提示核对),回流 Wavelet 后删除登记并重新同步。
- 禁止删除 `frontend/node_modules`。
- `pkg/util/` 保持纯净:禁止导入 Gin、GORM、sessions 等 HTTP/Web/DB 框架(会话选项在 `internal/apps/oauth/session.go`)。
- `backend/pkg/util/` 保持纯净:禁止导入 Gin、GORM、sessions 等 HTTP/Web/DB 框架(会话选项在 `backend/OpenFlare/plugins/server/oauth/session.go`)。
- 测试临时目录只用 `t.TempDir()`,禁止硬编码相对路径写源码树。
- HTTP 路由仅在 `internal/router/router.go` 注册;`Serve()` 只挂路由与中间件,禁止进程级初始化(如 `SyncEvents`、`InitLogWriter`)。
- HTTP 路由只由插件在 `Apply` 中经 `ctx.Router()` 声明;`router.BuildEngine()` 只挂引擎级中间件与前端 SPA 兜底,禁止进程级初始化(如 `SyncEvents`、`InitLogWriter`)。
- API 变更后:`make swagger`;开发完成:`make code-check`;提交前:`make format`。
- 缓存/文件管理复用平台实现,业务包禁止自建缓存目录或旁路存储后端。
- 文件摄取走 `upload.Ingest`(`PolicyCreate` / `PolicyDedupNewRecord` / `PolicyResolveExisting`);删除走 `upload.Remove` / `upload.RemoveOwned`。禁止业务直接 `repository.CreateUpload` / `SoftDeleteUpload` 或 `db.Create(&model.Upload{})`。
- **分层**:`apps → repository → model`,`repository → infra/persistence`;禁止 `model → repository`。
- `model`:实体、表名、配置 key、查询 DTO、无 IO 规则。禁止 `db.DB` / Redis / CH;禁止 `import repository`。GORM hook 仅可 mutate 自身字段,禁止在 hook 内再查 DB/缓存。
- `repository`:唯一持久化入口。apps/logics 禁止为业务 CRUD 直调 `db.DB`(管理端 SQL 控制台、infra 内部等例外保留)。禁止新增 `model.Get/List/Create/...` 类数据访问 API。
- 日志/分析表(节点访问日志、用户访问日志、可观测时序)走 `internal/repository/logstore`,禁止 apps 直连 `repository/analytics` 或 `db.ChConn`/`db.ChDB`。判定与接入步骤见 `logstore` skill。
- 跨模块集成(任务 Handler、推送事件、域监听、完成钩子)禁止 `init()` 注册;经 `internal/platform/bootstrap` 在 `internal/cmd` 入口显式装配。
- 核心业务(如 `oauth`、`user`)禁止直接 import push/custom_events;经 `internal/listener` 发域事件,push 在 bootstrap 订阅。
- 日志/分析表(节点访问日志、用户访问日志、可观测时序)走 `backend/OpenFlare/plugins/server/repository/logstore`,禁止 apps 直连 `repository/analytics` 或 `db.ChConn`/`db.ChDB`。判定与接入步骤见 `logstore` skill。
- 跨模块集成(任务 Handler、推送事件、域监听、完成钩子)禁止 `init()` 注册;经 `backend/OpenFlare/plugins/server/platform/bootstrap` 在 `backend/cmd` 入口显式装配。
- 核心业务(如 `oauth`、`user`)禁止直接 import push/custom_events;经 `backend/OpenFlare/plugins/server/listener` 发域事件,push 在 bootstrap 订阅。
- 依赖任务/推送注册的测试须显式 `bootstrap.RegisterTasks()` / `RegisterPushDomainEvents()` 等,不依赖 `init()`。
- API 错误必须 `response.Abort*` + `ErrorHandlerMiddleware`;禁止 Handler 直接 `c.JSON(..., response.Err(...))` 或用 HTTP 200 表示失败。
@@ -132,7 +144,7 @@ Conventional Commits:`<type>(<scope>): <subject>`(例:`feat(auth): support
- 命名:动词 + 名词(`ListUsers`);绑定用 `ShouldBindQuery` / `ShouldBindJSON`。
- 每个 HTTP API 需完整 Swagger 注释;API 变更后 `make swagger`。
- Handler:绑定 → 调 logic → 映射为 `Abort*` 或 `response.OK`。
- `logics.go`:接受 `context.Context`,返回结果/error;**禁止**依赖 `*gin.Context`、调用 `Abort*` / `c.JSON`。参考 `internal/apps/user/logics.go`。
- `logics.go`:接受 `context.Context`,返回结果/error;**禁止**依赖 `*gin.Context`、调用 `Abort*` / `c.JSON`。参考 `backend/internal/apps/user/logics.go`。
### API 响应
@@ -158,7 +170,7 @@ Swagger:`@Success 200` 用具体类型或 `response.Any`;每个可能 Abort
### 日志
- 运行时错误(DB/Redis/第三方/IO)在 Handler 或 logic 边界用 `pkg/logger`(带 `ctx`)记录,再返回安全 Abort/业务错误。
- 运行时错误(DB/Redis/第三方/IO)在 Handler 或 logic 边界用 `backend/pkg/logger`(带 `ctx`)记录,再返回安全 Abort/业务错误。
- 吞错、转通用响应、worker 忽略前必须先记日志。
- 禁止 `_ = err` 静默丢弃重要错误;best-effort 可忽略时加简短注释。
- 只在处理/抑制边界记一次,避免重复刷日志。
@@ -166,7 +178,7 @@ Swagger:`@Success 200` 用具体类型或 `response.Any`;每个可能 Abort
### 路由与装配
- `router.go` 只做高层分发,禁止直接挂业务 Handler。归属与开发步骤见 `new-api` skill。
- 跨模块副作用:在 `bootstrap` 增 `Register*`,于对应 `internal/cmd/*.go` 调用(`RegisterAPI` / `RegisterWorker` / `RegisterAll`)。
- 跨模块副作用:在 `bootstrap` 增 `Register*`,于对应 `backend/internal/cmd/*.go` 调用(`RegisterAPI` / `RegisterWorker` / `RegisterAll`)。
- API/`all` 模式:`bootstrap.Init` 须在 `RegisterPushDomainEvents()` **之后**调用,保证 `SyncEvents` 同步内置推送元数据。
### 中间件
@@ -177,13 +189,13 @@ Swagger:`@Success 200` 用具体类型或 `response.Any`;每个可能 Abort
### 配置
- 运行时只读 `config.Config`,禁止 `os.Getenv()`。
- 新增配置同步 `config.example.yaml` 与 `internal/infra/config/model.go`。
- 新增配置同步 `config.example.yaml` 与 `backend/internal/infra/config/model.go`。
### 数据库
- 持久化只经 `repository`(或 analytics);复杂查询不进 Handler;编排在 logics。
- repository 内用 `db.DB(ctx)`(链路追踪)。
- 迁移:`internal/infra/persistence/migrator/goose/` SQL;禁止 GORM AutoMigrate。
- 迁移:`backend/internal/infra/persistence/migrator/goose/` SQL;禁止 GORM AutoMigrate。
- 不建物理外键,关系字段加显式索引。
- 列默认值与 Go 零值(`nil`/`0`/`false`/`""`)一致。
+24 -24
View File
@@ -2,7 +2,7 @@
VERSION ?= dev
BUILD_DATE ?= $(shell date -u +'%Y-%m-%dT%H:%M:%SZ')
MODULE := $(shell go list -m)
MODULE := $(shell cd backend && go list -m)
swagger:
scripts/swagger.sh
@@ -19,7 +19,7 @@ format:
echo "goimports not found, installing..."; \
go install golang.org/x/tools/cmd/goimports@latest; \
}
goimports -w $$(find . -type f -name '*.go' -not -path './.git/*' -not -path './frontend/*')
goimports -w $$(find backend -type f -name '*.go')
@echo "==> Formatting frontend source and removing unused imports..."
cd frontend && pnpm format
@@ -29,50 +29,50 @@ build-embedded:
NEXT_PUBLIC_APP_VERSION="$(VERSION)" \
NEXT_PUBLIC_APP_BUILD_DATE="$(BUILD_DATE)" \
pnpm build:embed
rm -rf internal/router/root/dist
cp -R frontend/out internal/router/root/dist
go build \
rm -rf backend/OpenFlare/plugins/server/router/root/dist
cp -R frontend/out backend/OpenFlare/plugins/server/router/root/dist
cd backend && go build \
-tags embed_frontend \
-ldflags "-s -w -X '$(MODULE)/internal/buildinfo.Version=$(VERSION)' -X '$(MODULE)/internal/buildinfo.BuildTime=$(BUILD_DATE)'" \
-o bin/openflare-server \
-ldflags "-s -w -X '$(MODULE)/pkg/buildinfo.Version=$(VERSION)' -X '$(MODULE)/pkg/buildinfo.BuildTime=$(BUILD_DATE)'" \
-o ../bin/openflare-server \
main.go
code-check:
@echo "==> Architecture guards..."
@command -v rg >/dev/null 2>&1 || { echo 'error: rg (ripgrep) is required for architecture guards' >&2; exit 1; }
@if rg -n 'db\.DB\(|db\.Redis' internal/model --glob '*.go' -g '!*_test.go' ; then \
@if rg -n 'db\.DB\(|db\.Redis' backend/OpenFlare/plugins/server/model --glob '*.go' -g '!*_test.go' ; then \
echo 'error: internal/model must not access db.DB or db.Redis (non-test code)' >&2; \
exit 1; \
fi
golangci-lint run
cd backend && golangci-lint run
cd frontend && node scripts/merge-i18n-fragments.mjs && pnpm tsc --noEmit --jsx preserve && npx eslint . --max-warnings 0
build-backend:
@echo "==> Building backend version=$(VERSION) build_date=$(BUILD_DATE)..."
go build \
-ldflags "-s -w -X '$(MODULE)/internal/buildinfo.Version=$(VERSION)' -X '$(MODULE)/internal/buildinfo.BuildTime=$(BUILD_DATE)'" \
-o bin/openflare-server \
cd backend && go build \
-ldflags "-s -w -X '$(MODULE)/pkg/buildinfo.Version=$(VERSION)' -X '$(MODULE)/pkg/buildinfo.BuildTime=$(BUILD_DATE)'" \
-o ../bin/openflare-server \
main.go
build-agent:
@echo "==> Building agent version=$(VERSION)..."
go build \
-ldflags "-s -w -X '$(MODULE)/internal/apps/agent/config.Version=$(VERSION)'" \
-o bin/openflare-agent \
cd backend && go build \
-ldflags "-s -w -X '$(MODULE)/OpenFlare/plugins/agent/config.Version=$(VERSION)'" \
-o ../bin/openflare-agent \
cmd/agent/main.go
build-relay:
@echo "==> Building relay version=$(VERSION)..."
go build \
-ldflags "-s -w -X '$(MODULE)/internal/apps/relay/config.Version=$(VERSION)'" \
-o bin/openflare-relay \
cd backend && go build \
-ldflags "-s -w -X '$(MODULE)/OpenFlare/plugins/relay/config.Version=$(VERSION)'" \
-o ../bin/openflare-relay \
cmd/relay/main.go
build-flared:
@echo "==> Building flared version=$(VERSION)..."
go build \
-ldflags "-s -w -X '$(MODULE)/internal/apps/flared/config.Version=$(VERSION)'" \
-o bin/flared \
cd backend && go build \
-ldflags "-s -w -X '$(MODULE)/OpenFlare/plugins/flared/config.Version=$(VERSION)'" \
-o ../bin/flared \
cmd/flared/main.go
build-all: build-backend build-agent build-relay build-flared
@@ -89,7 +89,7 @@ build-test:
@PIDS=""; \
STATUS=0; \
( cd frontend && pnpm build:embed 2>&1 | sed 's/^/[frontend] /' ) & PIDS="$$PIDS $$!"; \
( go test ./... && go build -o /dev/null ./... 2>&1 | sed 's/^/[backend] /' ) & PIDS="$$PIDS $$!"; \
( cd backend && go test ./... && go build -o /dev/null ./... 2>&1 | sed 's/^/[backend] /' ) & PIDS="$$PIDS $$!"; \
for PID in $$PIDS; do \
wait $$PID || STATUS=1; \
done; \
@@ -124,14 +124,14 @@ dev-f:
dev-b:
@echo "==> Starting backend development server..."
go run main.go all
cd backend && go run main.go all
dev:
@echo "==> Starting frontend and backend development servers in parallel..."
@PIDS=""; \
STATUS=0; \
( cd frontend && pnpm dev 2>&1 | sed 's/^/[frontend] /' ) & PIDS="$$PIDS $$!"; \
( go run main.go all 2>&1 | sed 's/^/[backend] /' ) & PIDS="$$PIDS $$!"; \
( cd backend && go run main.go all 2>&1 | sed 's/^/[backend] /' ) & PIDS="$$PIDS $$!"; \
for PID in $$PIDS; do \
wait $$PID || STATUS=1; \
done; \
+48
View File
@@ -0,0 +1,48 @@
# OpenFlare 下游树
本目录占据上游 `backend/downstream/` 的位置,存放 OpenFlare 的全部业务,
按功能职责拆为 **4 个插件 + 1 个共享层**。上游目录
(`backend/{core,pkg,plugins}`)由 `scripts/sync-upstream.sh` 覆盖同步,
本目录与 `backend/cmd` 永不被覆盖。
```
backend/OpenFlare/
├── plugins/
│ ├── server/ # 控制面插件:站点/区域/Cloudflare/Pages/WAF/节点/回源/健康/配置版本
│ │ ├── openflare/ admin/ oauth/ user/ upload/ cap/ config/ health/ # 业务域
│ │ ├── repository/ model/ infra/ shared/ pkg/ # 支撑层
│ │ └── router/ platform/ listener/ integration/ testhelper/ # 装配与接线
│ ├── agent/ # 边缘 nginx/WAF 代理守护进程插件
│ ├── relay/ # frps 中继守护进程插件
│ └── flared/ # frpc 隧道客户端守护进程插件
└── share/ # 插件间共享资源(见 share/README.md)
```
装配根在 `backend/cmd`(与上游同构):`main.go` + `cmd/*.go` 为 server 的
api/worker/schedule/all profile 入口,`cmd/{agent,relay,flared}/main.go` 为三个
守护进程入口。
## 依赖规则
1. `plugins/<A>` 与 `plugins/<B>` 之间禁止互相 import;需要协作时走 `core/contracts`
或 `ctx.Events()`。
2. 插件只允许 import 本插件内部包、`Wavelet/core`、`Wavelet/core/contracts`、
`Wavelet/pkg` 与 `Wavelet/OpenFlare/share`。
3. `share/` 禁止 import 任何插件实现与下游业务包。
4. 表单一所有者:`of_*` 全部由 `server` 插件建表与读写;`w_*` 由上游平台插件拥有,
下游只能经契约或事件访问。
## 收敛路线
- 已完成:`agent`/`relay`/`flared` 各有 `plugin.go` 实现 `core.Plugin` + `core.Driver`
(`DriverTypeAgent`/`DriverTypeRelay`/`DriverTypeFlared`),入口 `backend/cmd/{agent,relay,flared}/main.go`
已改为 `core.NewApp(core.WithProfile(...))` + `app.Run()` 装配,`-config` 旗标、
默认路径、退出码与启动日志保持原样;JSON 配置由各插件 `Apply` 自行加载。
- 已完成:`server/plugin.go` 实现 `core.Plugin`,`Apply` 以 `ctx.Router().Group(api_prefix)`
声明根级与 `/v1` 全部路由;`router.Serve` 已删除,装配根改为
`core.App` + `driver_http.New(WithEngine(router.BuildEngine()))`,监听与优雅退出归内核。
路由保真由 `plugin_parity_test.go` 对拍 `baseline/routes-engine.txt`(256 条方法+路径)保证。
- 待办:`platform/bootstrap` 的任务、设置与迁移注册迁入 `Apply`;各业务域按插件标准
分层规范收敛到 `handler/ service/ repository/ model/ errs/ migrations/`(模式 2);
引擎级中间件、NoRoute 前端兜底与白名单生效三项能力回流上游后,去掉
`BuildEngine` 交给 `WithEngine` 这一例外。
@@ -12,13 +12,13 @@ import (
"strings"
"time"
"github.com/Rain-kl/Wavelet/internal/apps/agent/config"
agentheartbeat "github.com/Rain-kl/Wavelet/internal/apps/agent/heartbeat"
"github.com/Rain-kl/Wavelet/internal/apps/agent/protocol"
"github.com/Rain-kl/Wavelet/internal/apps/agent/state"
"github.com/Rain-kl/Wavelet/internal/apps/agent/wsclient"
edgeheartbeat "github.com/Rain-kl/Wavelet/internal/apps/edge/heartbeat"
"github.com/Rain-kl/Wavelet/pkg/util"
"Wavelet/OpenFlare/plugins/agent/config"
agentheartbeat "Wavelet/OpenFlare/plugins/agent/heartbeat"
"Wavelet/OpenFlare/plugins/agent/protocol"
"Wavelet/OpenFlare/plugins/agent/state"
"Wavelet/OpenFlare/plugins/agent/wsclient"
edgeheartbeat "Wavelet/OpenFlare/share/edge/heartbeat"
"Wavelet/pkg/util"
)
// HeartbeatService handles node registration and periodic heartbeat reporting.
@@ -13,11 +13,11 @@ import (
"testing"
"time"
"github.com/Rain-kl/Wavelet/internal/apps/agent/config"
agentheartbeat "github.com/Rain-kl/Wavelet/internal/apps/agent/heartbeat"
"github.com/Rain-kl/Wavelet/internal/apps/agent/protocol"
"github.com/Rain-kl/Wavelet/internal/apps/agent/state"
"github.com/Rain-kl/Wavelet/internal/apps/agent/updater"
"Wavelet/OpenFlare/plugins/agent/config"
agentheartbeat "Wavelet/OpenFlare/plugins/agent/heartbeat"
"Wavelet/OpenFlare/plugins/agent/protocol"
"Wavelet/OpenFlare/plugins/agent/state"
"Wavelet/OpenFlare/plugins/agent/updater"
)
func withHeartbeatCycle(runner *Runner, observabilityBuffer *state.ObservabilityBufferStore) *Runner {
@@ -15,8 +15,8 @@ import (
"strings"
"time"
"github.com/Rain-kl/Wavelet/internal/apps/edge/nodeip"
"github.com/Rain-kl/Wavelet/pkg/util"
"Wavelet/OpenFlare/share/edge/nodeip"
"Wavelet/pkg/util"
)
const (
@@ -13,9 +13,9 @@ import (
"testing"
"time"
"github.com/Rain-kl/Wavelet/internal/apps/edge/nodeip"
"github.com/Rain-kl/Wavelet/pkg/geoip"
"github.com/Rain-kl/Wavelet/pkg/geoip/iputil"
"Wavelet/OpenFlare/share/edge/nodeip"
"Wavelet/OpenFlare/share/geoip"
"Wavelet/OpenFlare/share/geoip/iputil"
)
func TestLoadDefaultsToManagedBinaryPaths(t *testing.T) {
@@ -3,7 +3,7 @@
package config
import edgeconfig "github.com/Rain-kl/Wavelet/internal/apps/edge/config"
import edgeconfig "Wavelet/OpenFlare/share/edge/config"
// MillisecondDuration is an alias for the edge config millisecond-precision duration type.
type MillisecondDuration = edgeconfig.MillisecondDuration
@@ -13,7 +13,7 @@ import (
"path/filepath"
"time"
"github.com/Rain-kl/Wavelet/pkg/geoip"
"Wavelet/OpenFlare/share/geoip"
)
// Updater periodically downloads a fresh GeoIP MMDB file and seeds missing
@@ -11,13 +11,13 @@ import (
"strings"
"time"
"github.com/Rain-kl/Wavelet/internal/apps/agent/config"
"github.com/Rain-kl/Wavelet/internal/apps/agent/observability"
"github.com/Rain-kl/Wavelet/internal/apps/agent/protocol"
"github.com/Rain-kl/Wavelet/internal/apps/agent/state"
"github.com/Rain-kl/Wavelet/internal/apps/agent/updater"
edgeheartbeat "github.com/Rain-kl/Wavelet/internal/apps/edge/heartbeat"
"github.com/Rain-kl/Wavelet/internal/apps/edge/nodeip"
"Wavelet/OpenFlare/plugins/agent/config"
"Wavelet/OpenFlare/plugins/agent/observability"
"Wavelet/OpenFlare/plugins/agent/protocol"
"Wavelet/OpenFlare/plugins/agent/state"
"Wavelet/OpenFlare/plugins/agent/updater"
edgeheartbeat "Wavelet/OpenFlare/share/edge/heartbeat"
"Wavelet/OpenFlare/share/edge/nodeip"
)
// SyncService is the interface used by Cycle to sync active configuration and WAF IP groups.
@@ -6,7 +6,7 @@ package heartbeat
import (
"context"
"github.com/Rain-kl/Wavelet/internal/apps/agent/protocol"
"Wavelet/OpenFlare/plugins/agent/protocol"
)
// RemoteClient is the interface that abstracts the remote API calls performed by Service.
@@ -13,8 +13,8 @@ import (
"net/http"
"time"
"github.com/Rain-kl/Wavelet/internal/apps/agent/protocol"
edgehttp "github.com/Rain-kl/Wavelet/internal/apps/edge/httpclient"
"Wavelet/OpenFlare/plugins/agent/protocol"
edgehttp "Wavelet/OpenFlare/share/edge/httpclient"
)
const pagesControlResponseMaxBytes = int64(64 * 1024)
@@ -4,7 +4,7 @@
// Package logging configures structured logging for the agent process.
package logging
import edgelogging "github.com/Rain-kl/Wavelet/internal/apps/edge/logging"
import edgelogging "Wavelet/OpenFlare/share/edge/logging"
// Setup initialises structured logging for the agent process.
func Setup() {
@@ -26,12 +26,12 @@ import (
"sync"
"time"
sharedprotocol "github.com/Rain-kl/Wavelet/pkg/protocol"
openrestyrender "github.com/Rain-kl/Wavelet/pkg/render/openresty"
"github.com/Rain-kl/Wavelet/pkg/util"
sharedprotocol "Wavelet/OpenFlare/share/protocol"
openrestyrender "Wavelet/OpenFlare/share/render/openresty"
"Wavelet/pkg/util"
"github.com/Rain-kl/Wavelet/internal/apps/agent/protocol"
"github.com/Rain-kl/Wavelet/internal/apps/agent/runtimeuser"
"Wavelet/OpenFlare/plugins/agent/protocol"
"Wavelet/OpenFlare/plugins/agent/runtimeuser"
)
// RuntimeConfigDirPlaceholder is substituted into generated configs at apply time.
@@ -16,9 +16,9 @@ import (
"strings"
"testing"
"github.com/Rain-kl/Wavelet/internal/apps/agent/protocol"
sharedprotocol "github.com/Rain-kl/Wavelet/pkg/protocol"
openrestyrender "github.com/Rain-kl/Wavelet/pkg/render/openresty"
"Wavelet/OpenFlare/plugins/agent/protocol"
sharedprotocol "Wavelet/OpenFlare/share/protocol"
openrestyrender "Wavelet/OpenFlare/share/render/openresty"
)
type runCall struct {
@@ -3,7 +3,7 @@
package nginx
import "github.com/Rain-kl/Wavelet/internal/apps/agent/protocol"
import "Wavelet/OpenFlare/plugins/agent/protocol"
// Local OpenResty observability endpoint (target model):
// GET /openflare/observability returns instantaneous health/connections only.
@@ -8,7 +8,7 @@ import (
"path/filepath"
"strings"
"github.com/Rain-kl/Wavelet/internal/apps/agent/protocol"
"Wavelet/OpenFlare/plugins/agent/protocol"
)
//go:embed pow_static

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

@@ -4,7 +4,7 @@
package nginx
import (
"github.com/Rain-kl/Wavelet/internal/apps/agent/runtimeuser"
"Wavelet/OpenFlare/plugins/agent/runtimeuser"
)
// OpenFlareRuntimeUser is the shared OS account for the agent process and
@@ -4,7 +4,7 @@
package nginx
import (
"github.com/Rain-kl/Wavelet/internal/apps/agent/protocol"
"Wavelet/OpenFlare/plugins/agent/protocol"
)
const openRestySWRuntimeLua = `local _M = {}
@@ -6,7 +6,7 @@ package nginx
import (
_ "embed"
"github.com/Rain-kl/Wavelet/internal/apps/agent/protocol"
"Wavelet/OpenFlare/plugins/agent/protocol"
)
//go:embed waf_runtime.lua
@@ -13,10 +13,10 @@ import (
"strings"
"time"
"github.com/Rain-kl/Wavelet/internal/apps/agent/config"
"github.com/Rain-kl/Wavelet/internal/apps/agent/protocol"
"github.com/Rain-kl/Wavelet/internal/apps/agent/state"
edgeobs "github.com/Rain-kl/Wavelet/internal/apps/edge/observability"
"Wavelet/OpenFlare/plugins/agent/config"
"Wavelet/OpenFlare/plugins/agent/protocol"
"Wavelet/OpenFlare/plugins/agent/state"
edgeobs "Wavelet/OpenFlare/share/edge/observability"
)
const nodeHealthEventInitialCapacity = 2
@@ -10,7 +10,7 @@ import (
"net/http"
"time"
"github.com/Rain-kl/Wavelet/internal/apps/agent/config"
"Wavelet/OpenFlare/plugins/agent/config"
)
const openRestyObservabilityPath = "/openflare/observability"
@@ -9,7 +9,7 @@ import (
"net/http/httptest"
"testing"
"github.com/Rain-kl/Wavelet/internal/apps/agent/config"
"Wavelet/OpenFlare/plugins/agent/config"
)
func TestCollectEdgeHealth(t *testing.T) {
@@ -15,9 +15,9 @@ import (
"strings"
"time"
"github.com/Rain-kl/Wavelet/internal/apps/agent/config"
"github.com/Rain-kl/Wavelet/internal/apps/agent/protocol"
"github.com/Rain-kl/Wavelet/internal/apps/agent/state"
"Wavelet/OpenFlare/plugins/agent/config"
"Wavelet/OpenFlare/plugins/agent/protocol"
"Wavelet/OpenFlare/plugins/agent/state"
)
type accessLogRecord struct {
@@ -9,8 +9,8 @@ import (
"strings"
"testing"
"github.com/Rain-kl/Wavelet/internal/apps/agent/config"
"github.com/Rain-kl/Wavelet/internal/apps/agent/state"
"Wavelet/OpenFlare/plugins/agent/config"
"Wavelet/OpenFlare/plugins/agent/state"
)
func TestCollectAccessLogsReturnsFactsOnly(t *testing.T) {
+198
View File
@@ -0,0 +1,198 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
// Package agent 装载 OpenFlare 边缘 agent 插件:openresty/WAF 运行时管理、
// 心跳同步、配置下发与 WebSocket 控制通道,以 Cordis 驱动形态在 profile "agent" 下运行。
package agent
import (
"context"
"errors"
"fmt"
"log/slog"
"Wavelet/OpenFlare/plugins/agent/agent"
"Wavelet/OpenFlare/plugins/agent/config"
"Wavelet/OpenFlare/plugins/agent/geoipupdate"
"Wavelet/OpenFlare/plugins/agent/heartbeat"
"Wavelet/OpenFlare/plugins/agent/httpclient"
"Wavelet/OpenFlare/plugins/agent/nginx"
"Wavelet/OpenFlare/plugins/agent/runtimeuser"
"Wavelet/OpenFlare/plugins/agent/state"
syncservice "Wavelet/OpenFlare/plugins/agent/sync"
"Wavelet/OpenFlare/plugins/agent/updater"
"Wavelet/OpenFlare/plugins/agent/wsclient"
"Wavelet/core"
"Wavelet/pkg/util"
)
// DriverTypeAgent 是边缘 agent 守护进程专属的驱动类型:
// 只有 profile 与之相等时内核才会 Start/Stop 本插件。
const DriverTypeAgent core.DriverType = "agent"
// Plugin 实现 core.Plugin 与 core.Driver,承载 agent 进程的全部装配与生命周期。
type Plugin struct {
configPath string
runner *agent.Runner
geo *geoipupdate.Updater
done chan error
started bool
}
// New 创建 agent 插件,configPath 指向其 JSON 配置文件。
func New(configPath string) *Plugin {
return &Plugin{configPath: configPath, done: make(chan error, 1)}
}
// Name 返回插件标识。
func (p *Plugin) Name() string { return "agent" }
// Type 返回驱动类型。
func (p *Plugin) Type() core.DriverType { return DriverTypeAgent }
// Apply 加载配置、确保运行环境、装配各服务并注册驱动。
func (p *Plugin) Apply(ctx *core.Context) error {
cfg, err := config.Load(p.configPath)
if err != nil {
return fmt.Errorf("load agent config: %w", err)
}
if err := runtimeuser.EnsureProcessUser(); err != nil {
return fmt.Errorf("ensure runtime user: %w", err)
}
if err := runtimeuser.EnsurePathOwnership(
cfg.DataDir, runtimeuser.DefaultDirPerm, runtimeuser.DefaultFilePerm,
); err != nil {
return fmt.Errorf("ensure data dir ownership, data_dir=%s: %w", cfg.DataDir, err)
}
nginxOptions := nginx.ExecutorOptions{
NginxPath: cfg.OpenrestyPath,
MainConfigPath: cfg.MainConfigPath,
RouteConfigPath: cfg.RouteConfigPath,
CertDir: cfg.CertDir,
NginxCertDir: cfg.OpenrestyCertDir,
LuaDir: cfg.LuaDir,
NginxLuaDir: cfg.OpenrestyLuaDir,
OpenrestyObservabilityPort: cfg.OpenrestyObservabilityPort,
}
cfg.ExtVersion = nginx.DetectVersion(context.Background(), nginxOptions)
logConfigLoaded(cfg)
runtimeManager := newRuntimeManager(cfg, nginxOptions)
if err := runtimeManager.EnsureLuaAssets(); err != nil {
return fmt.Errorf("ensure managed lua assets: %w", err)
}
client := httpclient.New(cfg.ServerURL, cfg.InitialAuthToken(), cfg.RequestTimeout.Duration())
wsClient := wsclient.New(cfg.ServerURL, cfg.InitialAuthToken(), cfg.RequestTimeout.Duration())
stateStore := state.NewStore(cfg.StatePath)
observabilityBuffer := state.NewObservabilityBufferStore(cfg.ObservabilityBufferPath)
syncService := syncservice.New(client, runtimeManager, stateStore)
syncService.SetPagesDir(cfg.PagesDir)
heartbeatService := heartbeat.New(client)
updateService := updater.New()
p.runner = &agent.Runner{
Config: cfg,
StateStore: stateStore,
HeartbeatCycle: &heartbeat.Cycle{
Config: cfg,
StateStore: stateStore,
ObservabilityBuffer: observabilityBuffer,
Heartbeat: heartbeatService,
Sync: syncService,
Updater: updateService,
},
HeartbeatService: heartbeatService,
SyncService: syncService,
RuntimeManager: runtimeManager,
WebSocketService: wsClient,
}
p.geo = newGeoIPUpdater(cfg)
return ctx.RegisterDriver(p)
}
// Start 预热 GeoIP 库并拉起心跳/同步/控制通道主循环。
//
// runner.Run 自身阻塞至 ctx 取消,因此放到独立 goroutine 中执行,
// 由 Stop 收敛其结果——内核要求驱动 Start 不得阻塞。
func (p *Plugin) Start(ctx context.Context) error {
if err := p.geo.EnsureInitialDatabases(ctx); err != nil {
slog.Warn("failed to prepare GeoIP databases before agent startup", "error", err)
}
util.Go(func() { p.geo.Run(ctx) })
util.Go(func() { p.done <- p.runner.Run(ctx) })
p.started = true
slog.Info("agent process started")
return nil
}
// Stop 等待主循环退出并返回其结果;ctx 超时时报告超时而非静默成功。
func (p *Plugin) Stop(ctx context.Context) error {
if !p.started {
return nil
}
select {
case err := <-p.done:
p.started = false
if err != nil && !errors.Is(err, context.Canceled) {
return err
}
slog.Info("agent process stopped")
return nil
case <-ctx.Done():
p.started = false
return fmt.Errorf("agent shutdown timeout: %w", ctx.Err())
}
}
// newGeoIPUpdater 装配国家/城市两套 mmdb 的下载与周期更新。
func newGeoIPUpdater(cfg *config.Config) *geoipupdate.Updater {
return &geoipupdate.Updater{
MMDBPath: cfg.MMDBPath,
DownloadURL: cfg.MMDBDownloadURL,
CityMMDBPath: cfg.CityMMDBPath,
CityDownloadURL: cfg.CityMMDBDownloadURL,
UpdateInterval: cfg.MMDBUpdateInterval.Duration(),
}
}
// newRuntimeManager 构造 openresty 配置与证书/Lua 资产的运行时管理器。
func newRuntimeManager(cfg *config.Config, nginxOptions nginx.ExecutorOptions) *nginx.Manager {
return &nginx.Manager{
MainConfigPath: cfg.MainConfigPath,
RouteConfigPath: cfg.RouteConfigPath,
AccessLogPath: cfg.AccessLogPath,
CertDir: cfg.CertDir,
NginxCertDir: cfg.OpenrestyCertDir,
LuaDir: cfg.LuaDir,
NginxLuaDir: cfg.OpenrestyLuaDir,
RuntimeConfigDir: cfg.RuntimeConfigDir,
MMDBPath: cfg.MMDBPath,
CityMMDBPath: cfg.CityMMDBPath,
PagesDir: cfg.PagesDir,
OpenrestyObservabilityListen: nginx.ObservabilityListenAddress(cfg.OpenrestyObservabilityPort),
OpenrestyObservabilityPort: cfg.OpenrestyObservabilityPort,
OpenrestyResolverDirective: "",
Executor: nginx.NewExecutor(nginxOptions),
}
}
func logConfigLoaded(cfg *config.Config) {
slog.Info("agent config loaded",
"server", cfg.ServerURL,
"node", cfg.NodeName,
"ip", cfg.NodeIP,
"heartbeat_interval", cfg.HeartbeatInterval,
"route_config", cfg.RouteConfigPath,
"access_log", cfg.AccessLogPath,
"cert_dir", cfg.CertDir,
"lua_dir", cfg.LuaDir,
"runtime_config_dir", cfg.RuntimeConfigDir,
"mmdb_path", cfg.MMDBPath,
"city_mmdb_path", cfg.CityMMDBPath,
)
}
@@ -0,0 +1,52 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package agent
import (
"context"
"testing"
"time"
"Wavelet/OpenFlare/plugins/agent/config"
"Wavelet/core"
)
func TestPluginIdentity(t *testing.T) {
p := New("./agent.json")
if got := p.Name(); got != "agent" {
t.Errorf("Name() = %q, want %q", got, "agent")
}
if got := p.Type(); got != DriverTypeAgent {
t.Errorf("Type() = %q, want %q", got, DriverTypeAgent)
}
// 驱动类型必须等于 profile 字符串,否则内核的 profile 过滤会漏掉本驱动。
if got, want := string(p.Type()), string(core.Profile("agent")); got != want {
t.Errorf("driver type %q must equal profile %q", got, want)
}
}
func TestApplyFailsOnMissingConfig(t *testing.T) {
ctx := core.NewContext(context.Background())
if err := New("./does-not-exist.json").Apply(ctx); err == nil {
t.Fatal("Apply(missing config) error = nil, want error")
}
}
func TestNewGeoIPUpdaterWiresCountryAndCity(t *testing.T) {
cfg := &config.Config{
MMDBPath: "/data/GeoLite2-Country.mmdb",
MMDBDownloadURL: "https://geo.example/GeoLite2-Country.mmdb",
CityMMDBPath: "/data/GeoLite2-City.mmdb",
CityMMDBDownloadURL: "https://geo.example/GeoLite2-City.mmdb",
MMDBUpdateInterval: config.MillisecondDuration(time.Hour),
}
got := newGeoIPUpdater(cfg)
if got.MMDBPath != cfg.MMDBPath || got.DownloadURL != cfg.MMDBDownloadURL ||
got.CityMMDBPath != cfg.CityMMDBPath || got.CityDownloadURL != cfg.CityMMDBDownloadURL {
t.Fatalf("GeoIP updater wiring incomplete: %#v", got)
}
if want := cfg.MMDBUpdateInterval.Duration(); got.UpdateInterval != want {
t.Errorf("UpdateInterval = %v, want %v", got.UpdateInterval, want)
}
}
@@ -4,7 +4,7 @@
// Package protocol defines type aliases and constants for the agent protocol.
package protocol
import pkgprotocol "github.com/Rain-kl/Wavelet/pkg/protocol"
import pkgprotocol "Wavelet/OpenFlare/share/protocol"
// APIResponse is an alias for pkgprotocol.APIResponse.
type APIResponse[T any] = pkgprotocol.APIResponse[T]
@@ -15,7 +15,7 @@ import (
"strconv"
"strings"
openrestyrender "github.com/Rain-kl/Wavelet/pkg/render/openresty"
openrestyrender "Wavelet/OpenFlare/share/render/openresty"
)
// Name is the dedicated service account shared by the agent and OpenResty workers.
@@ -14,7 +14,7 @@ import (
"strings"
"sync"
"github.com/Rain-kl/Wavelet/internal/apps/agent/protocol"
"Wavelet/OpenFlare/plugins/agent/protocol"
)
const observabilityBufferWindowSeconds = 60
@@ -9,7 +9,7 @@ import (
"strings"
"testing"
"github.com/Rain-kl/Wavelet/internal/apps/agent/protocol"
"Wavelet/OpenFlare/plugins/agent/protocol"
)
func TestObservabilityBufferStoreUpsertReplayAndAck(t *testing.T) {
@@ -18,9 +18,9 @@ import (
"strconv"
"strings"
"github.com/Rain-kl/Wavelet/internal/apps/agent/protocol"
"github.com/Rain-kl/Wavelet/internal/apps/agent/state"
"github.com/Rain-kl/Wavelet/pkg/pagesarchive"
"Wavelet/OpenFlare/plugins/agent/protocol"
"Wavelet/OpenFlare/plugins/agent/state"
"Wavelet/OpenFlare/share/pagesarchive"
)
const (
@@ -11,8 +11,8 @@ import (
"strings"
"testing"
"github.com/Rain-kl/Wavelet/internal/apps/agent/protocol"
"github.com/Rain-kl/Wavelet/internal/apps/agent/state"
"Wavelet/OpenFlare/plugins/agent/protocol"
"Wavelet/OpenFlare/plugins/agent/state"
)
func TestEnsurePagesProjectRejectsMetadataBeyondAgentCapsBeforeDownload(t *testing.T) {
@@ -17,11 +17,11 @@ import (
"strings"
"sync"
openrestyrender "github.com/Rain-kl/Wavelet/pkg/render/openresty"
openrestyrender "Wavelet/OpenFlare/share/render/openresty"
"github.com/Rain-kl/Wavelet/internal/apps/agent/nginx"
"github.com/Rain-kl/Wavelet/internal/apps/agent/protocol"
"github.com/Rain-kl/Wavelet/internal/apps/agent/state"
"Wavelet/OpenFlare/plugins/agent/nginx"
"Wavelet/OpenFlare/plugins/agent/protocol"
"Wavelet/OpenFlare/plugins/agent/state"
)
// Apply result constants indicate the outcome reported back to the server.
@@ -17,10 +17,10 @@ import (
"testing"
"time"
"github.com/Rain-kl/Wavelet/internal/apps/agent/nginx"
"github.com/Rain-kl/Wavelet/internal/apps/agent/protocol"
"github.com/Rain-kl/Wavelet/internal/apps/agent/state"
"github.com/Rain-kl/Wavelet/pkg/pagesarchive"
"Wavelet/OpenFlare/plugins/agent/nginx"
"Wavelet/OpenFlare/plugins/agent/protocol"
"Wavelet/OpenFlare/plugins/agent/state"
"Wavelet/OpenFlare/share/pagesarchive"
)
type fakeExecutor struct {
@@ -8,9 +8,9 @@ import (
"log/slog"
"strings"
"github.com/Rain-kl/Wavelet/internal/apps/agent/nginx"
"github.com/Rain-kl/Wavelet/internal/apps/agent/protocol"
"github.com/Rain-kl/Wavelet/internal/apps/agent/state"
"Wavelet/OpenFlare/plugins/agent/nginx"
"Wavelet/OpenFlare/plugins/agent/protocol"
"Wavelet/OpenFlare/plugins/agent/state"
)
func syncMode(startup bool) string {
@@ -5,8 +5,8 @@
package updater
import (
"github.com/Rain-kl/Wavelet/internal/apps/agent/config"
edgeupdater "github.com/Rain-kl/Wavelet/internal/apps/edge/updater"
"Wavelet/OpenFlare/plugins/agent/config"
edgeupdater "Wavelet/OpenFlare/share/edge/updater"
)
// Service is an alias for the edge updater service type used by the agent.
@@ -8,8 +8,8 @@ import (
"context"
"time"
"github.com/Rain-kl/Wavelet/internal/apps/agent/protocol"
edgews "github.com/Rain-kl/Wavelet/internal/apps/edge/wsclient"
"Wavelet/OpenFlare/plugins/agent/protocol"
edgews "Wavelet/OpenFlare/share/edge/wsclient"
)
// WSMessage is an alias for the WebSocket message type.
@@ -12,7 +12,7 @@ import (
"strings"
"time"
edgeconfig "github.com/Rain-kl/Wavelet/internal/apps/edge/config"
edgeconfig "Wavelet/OpenFlare/share/edge/config"
)
const (
@@ -8,13 +8,13 @@ import (
"context"
"log/slog"
edgerunner "github.com/Rain-kl/Wavelet/internal/apps/edge/runner"
"github.com/Rain-kl/Wavelet/internal/apps/flared/config"
"github.com/Rain-kl/Wavelet/internal/apps/flared/frpc"
"github.com/Rain-kl/Wavelet/internal/apps/flared/heartbeat"
"github.com/Rain-kl/Wavelet/internal/apps/flared/httpclient"
"github.com/Rain-kl/Wavelet/internal/apps/flared/sync"
"github.com/Rain-kl/Wavelet/internal/apps/flared/wsclient"
"Wavelet/OpenFlare/plugins/flared/config"
"Wavelet/OpenFlare/plugins/flared/frpc"
"Wavelet/OpenFlare/plugins/flared/heartbeat"
"Wavelet/OpenFlare/plugins/flared/httpclient"
"Wavelet/OpenFlare/plugins/flared/sync"
"Wavelet/OpenFlare/plugins/flared/wsclient"
edgerunner "Wavelet/OpenFlare/share/edge/runner"
)
// Runner is the top-level orchestrator for the flared agent. It wires together
@@ -20,9 +20,9 @@ import (
"syscall"
"time"
"github.com/Rain-kl/Wavelet/internal/apps/flared/config"
service "github.com/Rain-kl/Wavelet/pkg/protocol"
"github.com/Rain-kl/Wavelet/pkg/util"
"Wavelet/OpenFlare/plugins/flared/config"
service "Wavelet/OpenFlare/share/protocol"
"Wavelet/pkg/util"
)
const (
@@ -14,8 +14,8 @@ import (
"testing"
"time"
"github.com/Rain-kl/Wavelet/internal/apps/flared/config"
service "github.com/Rain-kl/Wavelet/pkg/protocol"
"Wavelet/OpenFlare/plugins/flared/config"
service "Wavelet/OpenFlare/share/protocol"
)
// Helper to write control file for the dummy script
@@ -8,13 +8,13 @@ import (
"context"
"log/slog"
edgeheartbeat "github.com/Rain-kl/Wavelet/internal/apps/edge/heartbeat"
"github.com/Rain-kl/Wavelet/internal/apps/edge/nodeip"
"github.com/Rain-kl/Wavelet/internal/apps/flared/config"
"github.com/Rain-kl/Wavelet/internal/apps/flared/frpc"
"github.com/Rain-kl/Wavelet/internal/apps/flared/httpclient"
"github.com/Rain-kl/Wavelet/internal/apps/flared/updater"
service "github.com/Rain-kl/Wavelet/pkg/protocol"
"Wavelet/OpenFlare/plugins/flared/config"
"Wavelet/OpenFlare/plugins/flared/frpc"
"Wavelet/OpenFlare/plugins/flared/httpclient"
"Wavelet/OpenFlare/plugins/flared/updater"
edgeheartbeat "Wavelet/OpenFlare/share/edge/heartbeat"
"Wavelet/OpenFlare/share/edge/nodeip"
service "Wavelet/OpenFlare/share/protocol"
)
// Service sends periodic heartbeat payloads and applies tunnel settings from responses.
@@ -8,8 +8,8 @@ import (
"context"
"time"
edgehttp "github.com/Rain-kl/Wavelet/internal/apps/edge/httpclient"
service "github.com/Rain-kl/Wavelet/pkg/protocol"
edgehttp "Wavelet/OpenFlare/share/edge/httpclient"
service "Wavelet/OpenFlare/share/protocol"
)
// APIResponse is an alias for service.APIResponse.
+106
View File
@@ -0,0 +1,106 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
// Package flared 装载 OpenFlare 隧道客户端插件:frpc 进程管理、配置同步与心跳上报,
// 以 Cordis 驱动形态在 profile "flared" 下运行。
package flared
import (
"context"
"errors"
"fmt"
"log/slog"
"Wavelet/OpenFlare/plugins/flared/config"
flaredrunner "Wavelet/OpenFlare/plugins/flared/flared"
"Wavelet/OpenFlare/plugins/flared/frpc"
"Wavelet/OpenFlare/plugins/flared/heartbeat"
"Wavelet/OpenFlare/plugins/flared/httpclient"
"Wavelet/OpenFlare/plugins/flared/sync"
"Wavelet/OpenFlare/plugins/flared/wsclient"
"Wavelet/core"
"Wavelet/pkg/util"
)
// DriverTypeFlared 是隧道客户端守护进程专属的驱动类型。
const DriverTypeFlared core.DriverType = "flared"
// Plugin 实现 core.Plugin 与 core.Driver。
type Plugin struct {
configPath string
runner *flaredrunner.Runner
done chan error
started bool
}
// New 创建 flared 插件,configPath 指向其 JSON 配置文件。
func New(configPath string) *Plugin {
return &Plugin{configPath: configPath, done: make(chan error, 1)}
}
// Name 返回插件标识。
func (p *Plugin) Name() string { return "flared" }
// Type 返回驱动类型。
func (p *Plugin) Type() core.DriverType { return DriverTypeFlared }
// Apply 加载配置、恢复 frpc 状态并装配各服务,然后注册驱动。
func (p *Plugin) Apply(ctx *core.Context) error {
cfg, err := config.Load(p.configPath)
if err != nil {
return fmt.Errorf("load flared config: %w", err)
}
slog.Info("flared config loaded",
"server", cfg.ServerURL,
"frpc_path", cfg.FrpcPath,
"data_dir", cfg.DataDir,
"heartbeat_interval", cfg.HeartbeatInterval,
"sync_interval", cfg.SyncInterval,
)
frpcManager := frpc.NewManager(cfg)
_ = frpcManager.LoadState()
slog.Info("detected frpc version", "version", frpcManager.GetVersion(context.Background()))
httpClient := httpclient.New(cfg.ServerURL, cfg.InitialAuthToken(), cfg.RequestTimeout.Duration())
wsClient := wsclient.New(cfg.ServerURL, cfg.InitialAuthToken(), cfg.RequestTimeout.Duration())
p.runner = &flaredrunner.Runner{
Config: cfg,
FrpcManager: frpcManager,
HTTPClient: httpClient,
WebSocketService: wsClient,
HeartbeatService: heartbeat.New(httpClient, frpcManager, cfg),
SyncService: sync.New(httpClient, frpcManager, cfg),
}
return ctx.RegisterDriver(p)
}
// Start 拉起隧道主循环;runner.Run 阻塞至 ctx 取消,故置于独立 goroutine。
func (p *Plugin) Start(ctx context.Context) error {
util.Go(func() { p.done <- p.runner.Run(ctx) })
p.started = true
slog.Info("flared process started")
return nil
}
// Stop 等待主循环退出并回传其结果。
func (p *Plugin) Stop(ctx context.Context) error {
if !p.started {
return nil
}
select {
case err := <-p.done:
p.started = false
if err != nil && !errors.Is(err, context.Canceled) {
return err
}
slog.Info("flared process stopped")
return nil
case <-ctx.Done():
p.started = false
return fmt.Errorf("flared shutdown timeout: %w", ctx.Err())
}
}
@@ -0,0 +1,29 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package flared
import (
"context"
"testing"
"Wavelet/core"
)
func TestPluginIdentity(t *testing.T) {
p := New("./flared.json")
if got := p.Name(); got != "flared" {
t.Errorf("Name() = %q, want %q", got, "flared")
}
// 驱动类型必须等于 profile 字符串,否则内核的 profile 过滤会漏掉本驱动。
if got, want := string(p.Type()), string(core.Profile("flared")); got != want {
t.Errorf("Type() = %q, want profile %q", got, want)
}
}
func TestApplyFailsOnMissingConfig(t *testing.T) {
ctx := core.NewContext(context.Background())
if err := New("./does-not-exist.json").Apply(ctx); err == nil {
t.Fatal("Apply(missing config) error = nil, want error")
}
}
@@ -9,10 +9,10 @@ import (
"log/slog"
"time"
"github.com/Rain-kl/Wavelet/internal/apps/flared/config"
"github.com/Rain-kl/Wavelet/internal/apps/flared/frpc"
"github.com/Rain-kl/Wavelet/internal/apps/flared/httpclient"
service "github.com/Rain-kl/Wavelet/pkg/protocol"
"Wavelet/OpenFlare/plugins/flared/config"
"Wavelet/OpenFlare/plugins/flared/frpc"
"Wavelet/OpenFlare/plugins/flared/httpclient"
service "Wavelet/OpenFlare/share/protocol"
)
// Service synchronizes tunnel configuration from the control plane to the local frpc manager.
@@ -5,8 +5,8 @@
package updater
import (
edgeupdater "github.com/Rain-kl/Wavelet/internal/apps/edge/updater"
"github.com/Rain-kl/Wavelet/internal/apps/flared/config"
"Wavelet/OpenFlare/plugins/flared/config"
edgeupdater "Wavelet/OpenFlare/share/edge/updater"
)
// Service is an alias for the edge updater Service.
@@ -8,7 +8,7 @@ import (
"context"
"time"
edgews "github.com/Rain-kl/Wavelet/internal/apps/edge/wsclient"
edgews "Wavelet/OpenFlare/share/edge/wsclient"
)
// WSMessage is a WebSocket message exchanged with the control plane.
@@ -12,8 +12,8 @@ import (
"strings"
"time"
edgeconfig "github.com/Rain-kl/Wavelet/internal/apps/edge/config"
"github.com/Rain-kl/Wavelet/internal/apps/edge/nodeip"
edgeconfig "Wavelet/OpenFlare/share/edge/config"
"Wavelet/OpenFlare/share/edge/nodeip"
)
const (
@@ -20,7 +20,7 @@ import (
"syscall"
"time"
service "github.com/Rain-kl/Wavelet/pkg/protocol"
service "Wavelet/OpenFlare/share/protocol"
)
const (
@@ -14,7 +14,7 @@ import (
"testing"
"time"
service "github.com/Rain-kl/Wavelet/pkg/protocol"
service "Wavelet/OpenFlare/share/protocol"
)
// Helper to write control file for the dummy script
@@ -8,15 +8,15 @@ import (
"context"
"log/slog"
edgeheartbeat "github.com/Rain-kl/Wavelet/internal/apps/edge/heartbeat"
"github.com/Rain-kl/Wavelet/internal/apps/edge/nodeip"
"github.com/Rain-kl/Wavelet/internal/apps/relay/config"
"github.com/Rain-kl/Wavelet/internal/apps/relay/frps"
"github.com/Rain-kl/Wavelet/internal/apps/relay/httpclient"
"github.com/Rain-kl/Wavelet/internal/apps/relay/observability"
"github.com/Rain-kl/Wavelet/internal/apps/relay/state"
"github.com/Rain-kl/Wavelet/internal/apps/relay/updater"
service "github.com/Rain-kl/Wavelet/pkg/protocol"
"Wavelet/OpenFlare/plugins/relay/config"
"Wavelet/OpenFlare/plugins/relay/frps"
"Wavelet/OpenFlare/plugins/relay/httpclient"
"Wavelet/OpenFlare/plugins/relay/observability"
"Wavelet/OpenFlare/plugins/relay/state"
"Wavelet/OpenFlare/plugins/relay/updater"
edgeheartbeat "Wavelet/OpenFlare/share/edge/heartbeat"
"Wavelet/OpenFlare/share/edge/nodeip"
service "Wavelet/OpenFlare/share/protocol"
)
// Service sends periodic heartbeat payloads to the server, updates the frps
@@ -8,8 +8,8 @@ import (
"context"
"time"
edgehttp "github.com/Rain-kl/Wavelet/internal/apps/edge/httpclient"
service "github.com/Rain-kl/Wavelet/pkg/protocol"
edgehttp "Wavelet/OpenFlare/share/edge/httpclient"
service "Wavelet/OpenFlare/share/protocol"
)
// APIResponse is an alias for service.APIResponse.

Some files were not shown because too many files have changed in this diff Show More