mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-28 05:46:36 +08:00
migrate
This commit is contained in:
@@ -167,7 +167,7 @@ func AgentWebSocketHandler(c *gin.Context) {
|
||||
compat.Unauthorized(c, errInvalidAgentToken)
|
||||
return
|
||||
}
|
||||
websocket.ServeAgent(c, authNode.NodeID)
|
||||
websocket.ServeAgent(c, authNode.NodeID, HandleWSStatus)
|
||||
}
|
||||
|
||||
func okWithExtras(c *gin.Context, data any, extras gin.H) {
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
|
||||
ofws "github.com/Rain-kl/Wavelet/internal/apps/openflare/websocket"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
)
|
||||
|
||||
// HandleWSStatus processes an agent websocket status payload (replaces HTTP heartbeat in WS mode).
|
||||
func HandleWSStatus(ctx context.Context, nodeID, remoteAddr string, rawPayload json.RawMessage) {
|
||||
var payload NodePayload
|
||||
if err := json.Unmarshal(rawPayload, &payload); err != nil {
|
||||
slog.Debug("agent ws status payload decode failed", "node_id", nodeID, "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
authNode, err := model.GetOpenFlareNodeByNodeID(ctx, nodeID)
|
||||
if err != nil {
|
||||
slog.Debug("agent ws status reload node failed", "node_id", nodeID, "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
payload.IP = resolveReportedNodeIP(payload.IP, remoteAddr)
|
||||
response, err := HeartbeatNode(ctx, authNode, payload)
|
||||
if err != nil {
|
||||
slog.Debug("agent ws status handling failed", "node_id", nodeID, "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
settingsSent := false
|
||||
if response.AgentSettings != nil {
|
||||
settingsSent = ofws.SendAgentSettings(nodeID, response.AgentSettings)
|
||||
}
|
||||
activeConfigSent := false
|
||||
if response.ActiveConfig != nil {
|
||||
activeConfigSent = ofws.SendAgentActiveConfig(nodeID, response.ActiveConfig)
|
||||
}
|
||||
wafIPGroupsSent := false
|
||||
if len(response.WAFIPGroups) > 0 {
|
||||
wafIPGroupsSent = ofws.SendAgentWAFIPGroups(nodeID, response.WAFIPGroups)
|
||||
}
|
||||
|
||||
slog.Debug("agent ws status processed",
|
||||
"node_id", nodeID,
|
||||
"current_version", payload.CurrentVersion,
|
||||
"openresty_status", payload.OpenrestyStatus,
|
||||
"settings_sent", settingsSent,
|
||||
"active_config_sent", activeConfigSent,
|
||||
"waf_ip_groups_sent", wafIPGroupsSent,
|
||||
)
|
||||
}
|
||||
@@ -4,8 +4,10 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
ofws "github.com/Rain-kl/Wavelet/internal/apps/openflare/websocket"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
)
|
||||
|
||||
@@ -29,7 +31,23 @@ func computeNodeStatus(node *model.OpenFlareNode) string {
|
||||
}
|
||||
|
||||
func nodeViewLastSeenAt(node *model.OpenFlareNode) any {
|
||||
if node == nil || node.LastSeenAt == nil {
|
||||
if node == nil {
|
||||
return time.Time{}
|
||||
}
|
||||
nodeType := strings.TrimSpace(node.NodeType)
|
||||
if nodeType == "" {
|
||||
nodeType = "edge_node"
|
||||
}
|
||||
if nodeType == "tunnel_relay" && ofws.IsRelayConnected(node.NodeID) {
|
||||
return ofws.RelayWSConnectedLastSeenValue
|
||||
}
|
||||
if nodeType == "tunnel_client" && ofws.IsFlaredConnected(node.NodeID) {
|
||||
return ofws.FlaredWSConnectedLastSeenValue
|
||||
}
|
||||
if ofws.IsAgentConnected(node.NodeID) {
|
||||
return ofws.AgentWSConnectedLastSeenValue
|
||||
}
|
||||
if node.LastSeenAt == nil {
|
||||
return time.Time{}
|
||||
}
|
||||
return *node.LastSeenAt
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
ofws "github.com/Rain-kl/Wavelet/internal/apps/openflare/websocket"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
)
|
||||
|
||||
@@ -183,7 +184,23 @@ func computeNodeStatus(node *model.OpenFlareNode) string {
|
||||
}
|
||||
|
||||
func nodeViewLastSeenAt(node *model.OpenFlareNode) any {
|
||||
if node == nil || node.LastSeenAt == nil {
|
||||
if node == nil {
|
||||
return time.Time{}
|
||||
}
|
||||
nodeType := strings.TrimSpace(node.NodeType)
|
||||
if nodeType == "" {
|
||||
nodeType = "edge_node"
|
||||
}
|
||||
if nodeType == "tunnel_relay" && ofws.IsRelayConnected(node.NodeID) {
|
||||
return ofws.RelayWSConnectedLastSeenValue
|
||||
}
|
||||
if nodeType == "tunnel_client" && ofws.IsFlaredConnected(node.NodeID) {
|
||||
return ofws.FlaredWSConnectedLastSeenValue
|
||||
}
|
||||
if ofws.IsAgentConnected(node.NodeID) {
|
||||
return ofws.AgentWSConnectedLastSeenValue
|
||||
}
|
||||
if node.LastSeenAt == nil {
|
||||
return time.Time{}
|
||||
}
|
||||
return *node.LastSeenAt
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"sync"
|
||||
@@ -15,18 +16,26 @@ import (
|
||||
|
||||
const (
|
||||
// AgentWSConnectedLastSeenValue is the sentinel last_seen_at value when agent WS is connected.
|
||||
AgentWSConnectedLastSeenValue = "__OPENFLARE_AGENT_WS_CONNECTED__"
|
||||
AgentWSConnectedLastSeenValue = "__OPENFLARE_WS_CONNECTED__"
|
||||
|
||||
agentMessageTypeStatus = "status"
|
||||
agentMessageTypeSettings = "settings"
|
||||
agentMessageTypeActiveConfig = "active_config"
|
||||
agentMessageTypeForceSyncConfig = "force_sync_config"
|
||||
agentMessageTypeWAFIPGroups = "waf_ip_groups"
|
||||
)
|
||||
|
||||
// AgentStatusHandler processes inbound agent websocket status payloads.
|
||||
type AgentStatusHandler func(ctx context.Context, nodeID, remoteAddr string, payload json.RawMessage)
|
||||
|
||||
type agentClient struct {
|
||||
nodeID string
|
||||
conn *websocket.Conn
|
||||
send chan Message
|
||||
done chan struct{}
|
||||
once sync.Once
|
||||
nodeID string
|
||||
remoteAddr string
|
||||
conn *websocket.Conn
|
||||
send chan Message
|
||||
done chan struct{}
|
||||
onStatus AgentStatusHandler
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
func (c *agentClient) close() {
|
||||
@@ -47,7 +56,7 @@ type agentHub struct {
|
||||
var defaultAgentHub = &agentHub{clients: make(map[string]*agentClient)}
|
||||
|
||||
// ServeAgent handles an upgraded agent websocket connection.
|
||||
func ServeAgent(c *gin.Context, nodeID string) {
|
||||
func ServeAgent(c *gin.Context, nodeID string, onStatus AgentStatusHandler) {
|
||||
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
slog.Debug("agent ws upgrade failed", "node_id", nodeID, "error", err)
|
||||
@@ -55,15 +64,17 @@ func ServeAgent(c *gin.Context, nodeID string) {
|
||||
}
|
||||
|
||||
client := &agentClient{
|
||||
nodeID: nodeID,
|
||||
conn: conn,
|
||||
send: make(chan Message, 16),
|
||||
done: make(chan struct{}),
|
||||
nodeID: nodeID,
|
||||
remoteAddr: c.Request.RemoteAddr,
|
||||
conn: conn,
|
||||
send: make(chan Message, 16),
|
||||
done: make(chan struct{}),
|
||||
onStatus: onStatus,
|
||||
}
|
||||
defaultAgentHub.register(client)
|
||||
defer defaultAgentHub.unregister(client)
|
||||
|
||||
slog.Debug("agent ws connected", "node_id", nodeID, "remote", c.Request.RemoteAddr)
|
||||
slog.Debug("agent ws connected", "node_id", nodeID, "remote", client.remoteAddr)
|
||||
|
||||
go client.writePump()
|
||||
client.readPump()
|
||||
@@ -103,6 +114,21 @@ func IsAgentConnected(nodeID string) bool {
|
||||
}
|
||||
}
|
||||
|
||||
// SendAgentSettings pushes agent settings to a connected agent.
|
||||
func SendAgentSettings(nodeID string, payload any) bool {
|
||||
return sendAgentMessage(nodeID, Message{Type: agentMessageTypeSettings, Payload: payload})
|
||||
}
|
||||
|
||||
// SendAgentActiveConfig pushes active config metadata to a connected agent.
|
||||
func SendAgentActiveConfig(nodeID string, payload any) bool {
|
||||
return sendAgentMessage(nodeID, Message{Type: agentMessageTypeActiveConfig, Payload: payload})
|
||||
}
|
||||
|
||||
// SendAgentWAFIPGroups pushes WAF IP group updates to a connected agent.
|
||||
func SendAgentWAFIPGroups(nodeID string, payload any) bool {
|
||||
return sendAgentMessage(nodeID, Message{Type: agentMessageTypeWAFIPGroups, Payload: payload})
|
||||
}
|
||||
|
||||
// BroadcastWAFIPGroups pushes changed WAF IP groups to all connected agents.
|
||||
func BroadcastWAFIPGroups(payload any) int {
|
||||
if payload == nil {
|
||||
@@ -127,56 +153,62 @@ func BroadcastWAFIPGroups(payload any) int {
|
||||
|
||||
// SendForceSyncConfig notifies an agent to force sync configuration.
|
||||
func SendForceSyncConfig(nodeID string, payload any) bool {
|
||||
return sendAgentMessage(nodeID, Message{Type: agentMessageTypeForceSyncConfig, Payload: payload})
|
||||
}
|
||||
|
||||
func sendAgentMessage(nodeID string, message Message) bool {
|
||||
defaultAgentHub.mu.RLock()
|
||||
client := defaultAgentHub.clients[nodeID]
|
||||
defaultAgentHub.mu.RUnlock()
|
||||
if client == nil {
|
||||
return false
|
||||
}
|
||||
select {
|
||||
case <-client.done:
|
||||
return false
|
||||
case client.send <- Message{Type: agentMessageTypeForceSyncConfig, Payload: payload}:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
return client.enqueue(message)
|
||||
}
|
||||
|
||||
func (c *agentClient) readPump() {
|
||||
defer c.close()
|
||||
_ = c.conn.SetReadDeadline(time.Now().Add(90 * time.Second))
|
||||
c.conn.SetPongHandler(func(string) error {
|
||||
return c.conn.SetReadDeadline(time.Now().Add(90 * time.Second))
|
||||
})
|
||||
|
||||
for {
|
||||
_ = c.conn.SetReadDeadline(time.Now().Add(agentWSReadTimeout()))
|
||||
_, data, err := c.conn.ReadMessage()
|
||||
if err != nil {
|
||||
slog.Debug("agent ws read closed", "node_id", c.nodeID, "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
var message Message
|
||||
if err = json.Unmarshal(data, &message); err != nil {
|
||||
var inbound struct {
|
||||
Type string `json:"type"`
|
||||
Payload json.RawMessage `json:"payload,omitempty"`
|
||||
}
|
||||
if err = json.Unmarshal(data, &inbound); err != nil {
|
||||
slog.Debug("agent ws invalid message", "node_id", c.nodeID, "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
switch message.Type {
|
||||
slog.Debug("agent ws message received", "node_id", c.nodeID, "type", inbound.Type)
|
||||
switch inbound.Type {
|
||||
case agentMessageTypeStatus:
|
||||
if c.onStatus != nil {
|
||||
c.onStatus(context.Background(), c.nodeID, c.remoteAddr, inbound.Payload)
|
||||
}
|
||||
case messageTypePing:
|
||||
_ = c.enqueue(Message{Type: messageTypePong})
|
||||
case messageTypePong:
|
||||
default:
|
||||
_ = c.enqueue(Message{Type: messageTypeNotify, Payload: gin.H{
|
||||
"echo": true,
|
||||
"type": message.Type,
|
||||
"payload": message.Payload,
|
||||
}})
|
||||
slog.Debug("agent ws unsupported message type", "node_id", c.nodeID, "type", inbound.Type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func agentWSReadTimeout() time.Duration {
|
||||
timeout := 90 * time.Second
|
||||
if timeout < 30*time.Second {
|
||||
return 30 * time.Second
|
||||
}
|
||||
return timeout
|
||||
}
|
||||
|
||||
func (c *agentClient) writePump() {
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ services:
|
||||
|
||||
environment:
|
||||
OPENFLARE_SERVER_URL: "http://host.docker.internal:3000"
|
||||
OPENFLARE_AGENT_TOKEN: "111675c3b22c8cf39b43639b9305fa0a"
|
||||
OPENFLARE_AGENT_TOKEN: "ce5a664631a7c5c54d3406a376847535"
|
||||
LOG_LEVEL: "debug"
|
||||
|
||||
extra_hosts:
|
||||
|
||||
@@ -36,6 +36,7 @@ sidebar: false
|
||||
- 补全 OAuth 快捷路由(`/api/oauth/github`、`/api/oauth/wechat`、`/api/oauth/wechat/bind`、`/api/oauth/email/bind`)。
|
||||
- 新增 `internal/apps/openflare/tasks/` 集中承载 OpenFlare 定时/后台任务(主进程 cron,非 Asynq),含数据库可观测性自动清理、WAF IP 组周期同步、UptimeKuma 同步、ACME 证书自动续期。
|
||||
- 实装数据库可观测性手动/自动清理、WAF IP 组订阅/自动同步与测试接口、UptimeKuma 监控同步、TLS ACME 申请/续期(lego DNS-01)。
|
||||
- 修复 Wavelet Agent WebSocket 未处理 `status` 消息导致 WS 模式下 `last_seen_at` 停止更新、节点超时显示离线的问题;列表「最近心跳」恢复显示「WS 已连接」。
|
||||
|
||||
## [v2.3.4] - 2026-06-17
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
OpenFlare Server 是 Gin + GORM 单体控制面,负责管理端 UI、管理 API、Agent API、配置渲染、版本发布、数据存储与聚合查询。
|
||||
|
||||
> **迁移说明**:后端业务域正在迁入 [Wavelet](../plan/20260618-openflare-wavelet-backend-migration.md)。阶段一通过 Wavelet 的 `/api/*` legacy 兼容层联调旧前端;下文「Wavelet 后端」一节描述新后端的启动方式。
|
||||
|
||||
## 前置条件
|
||||
|
||||
| 项目 | 要求 |
|
||||
@@ -170,3 +172,63 @@ go run . --port 3000 --log-dir ./logs
|
||||
| `root` | `123456` |
|
||||
|
||||
首次登录后请立即修改默认密码。
|
||||
|
||||
## Wavelet 后端(迁移中)
|
||||
|
||||
阶段一将 OpenFlare 业务 API 运行在 Wavelet 框架内(`Wavelet/internal/apps/openflare/`),默认监听 `:3000`,与旧 Server 端口一致。旧前端仍使用 `openflare-server/web/build` 静态产物;API 请求指向 Wavelet 的 `/api/*` 兼容路由。
|
||||
|
||||
### 配置要点
|
||||
|
||||
复制 `Wavelet/config.example.yaml` 为 `config.yaml`,或使用 `Wavelet/.env.example` 中的环境变量。关键默认值:
|
||||
|
||||
| 项 | 值 |
|
||||
| --- | --- |
|
||||
| 监听地址 | `:3000` |
|
||||
| PostgreSQL 库名 | `openflare` |
|
||||
| SQLite 后备 | `openflare.db` |
|
||||
| `application_name` | `openflare-server` |
|
||||
| Redis 键前缀 | `openflare:` |
|
||||
|
||||
生产环境需启用 PostgreSQL(`database.enabled: true` 或 `DB_ENABLED=true`),并配置 Redis。
|
||||
|
||||
### 启动命令
|
||||
|
||||
```bash
|
||||
cd Wavelet
|
||||
|
||||
# 开发:API + Worker + Scheduler 合一
|
||||
go run . all
|
||||
|
||||
# 生产:分进程部署
|
||||
go run . api # HTTP API + OpenFlare 定时任务
|
||||
go run . worker # Wavelet Asynq 异步任务
|
||||
go run . scheduler # Wavelet Asynq 定时触发
|
||||
```
|
||||
|
||||
也可使用 `docker compose up`(见 `Wavelet/docker-compose.yml`)拉起 PostgreSQL、Redis 与 Wavelet 服务。
|
||||
|
||||
### OpenFlare 定时任务
|
||||
|
||||
OpenFlare 业务定时任务集中在 `internal/apps/openflare/tasks/`,通过 `robfig/cron` 在 **API 进程**内运行(非 Asynq):
|
||||
|
||||
| 任务 | 调度 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| 可观测数据自动清理 | 每日 03:00 | 受 Option `DatabaseAutoCleanupEnabled` 控制 |
|
||||
| WAF IP 组同步 | 每 5 分钟 | 向在线 Agent 下发 IP 组变更 |
|
||||
| UptimeKuma 同步 | 每分钟检查间隔 | 按 Option 配置的间隔触发 |
|
||||
| SSL 自动续期 | 每日 00:00 | ACME 证书续期 |
|
||||
|
||||
API 启动后日志中应出现 `[OpenFlareTasks] registered cron job` 行。`worker` 与 `scheduler` 进程不运行上述 cron。
|
||||
|
||||
### 验证
|
||||
|
||||
```bash
|
||||
cd Wavelet
|
||||
go build ./...
|
||||
go test ./internal/apps/openflare/... -count=1
|
||||
|
||||
# 健康检查
|
||||
curl http://127.0.0.1:3000/api/status
|
||||
```
|
||||
|
||||
更多迁移进度与接手说明见 [AI 接手文档](../plan/handover-openflare-backend-migration.md)。
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
> **文档类型**:实现计划(Implementation Plan)
|
||||
> **创建日期**:2026-06-18
|
||||
> **状态**:规划中
|
||||
> **状态**:实施中(阶段一旧前端联调,核心 API 与后台任务已落地)
|
||||
> **前置阅读**:[`Wavelet/AGENTS.md`](../../Wavelet/AGENTS.md)、[`docs/guideline/Constraints.md`](../guideline/Constraints.md)、[`docs/design/architecture.md`](../design/architecture.md)
|
||||
|
||||
---
|
||||
@@ -25,7 +25,7 @@ OpenFlare 当前控制面基于 `openflare-server`(Gin + GORM 单体),与
|
||||
| 重叠模块复用 Wavelet 内置能力 + 兼容适配层 | 多租户、细粒度 RBAC(超出 OpenFlare 单租户边界) |
|
||||
| 数据模型与 goose SQL 迁移(PG + SQLite 双份) | 英文文档同步 |
|
||||
| 旧路径 `/api/*` 兼容注册 | 阶段二新前端路径改造(见前端迁移计划) |
|
||||
| Cron/异步任务迁移至 Asynq + Scheduler | Wavelet 框架核心文件修改(`v1/user.go` 等禁止改动) |
|
||||
| OpenFlare 定时任务(**过渡**:API 主进程内 cron;**目标**:后续迁入统一任务框架) | Wavelet 框架核心文件修改(`v1/user.go` 等禁止改动) |
|
||||
| `pkg/geoip`、`pkg/render/openresty`、`pkg/protocol` 复用 | 微信登录长期维护(评估后降级或适配) |
|
||||
|
||||
### 1.3 迁移总原则
|
||||
@@ -89,7 +89,8 @@ Wavelet/
|
||||
│ │ ├── managed_domain/
|
||||
│ │ ├── pages/ # Pages 静态托管
|
||||
│ │ ├── dashboard/ # 仪表盘聚合
|
||||
│ │ ├── observability/ # 可观测性 + 分片
|
||||
│ │ ├── observability/ # 可观测性查询(v1 单表)
|
||||
│ │ ├── tasks/ # 🆕 定时/后台任务集中目录(主进程 cron)
|
||||
│ │ ├── apply_log/
|
||||
│ │ ├── option/ # OpenFlare 专有 Option(非框架 SystemConfig)
|
||||
│ │ ├── update/ # 服务自更新
|
||||
@@ -109,7 +110,7 @@ Wavelet/
|
||||
│ ├── geoip/ # 复用 OpenFlare pkg/geoip
|
||||
│ ├── render/openresty/ # 复用配置渲染引擎
|
||||
│ └── protocol/ # 复用节点协议
|
||||
└── internal/bootstrap/ # 注册 OpenFlare 任务/监听器
|
||||
└── internal/bootstrap/ # API 启动时注册并运行 openflare/tasks
|
||||
```
|
||||
|
||||
### 2.2 关键架构决策
|
||||
@@ -122,10 +123,10 @@ Wavelet/
|
||||
| 认证 Token 桥接 | 阶段一同时支持 `OpenFlare-Token`(JWT)与 Wavelet Session/AccessToken;登录后 legacy 层签发兼容 JWT 或映射到 AccessToken | 旧前端依赖 Header Token |
|
||||
| 角色模型 | OpenFlare `role(1/10/100)` 映射为 Wavelet `is_admin` + 扩展字段 `of_role`(SystemConfig 或用户扩展表) | Wavelet 仅 bool admin;Root 权限需扩展 |
|
||||
| 系统配置 | OpenFlare `options` 表保留(70+ 热加载项);框架 `system_configs` 仅放 Wavelet 平台项 | 业务配置语义不同,避免强行合并 |
|
||||
| 定时任务 | UptimeKuma、WAF IP 同步、SSL 续期、DB 清理 → Asynq Periodic Task | 符合 Wavelet 多进程模型 |
|
||||
| 数据库迁移 | Wavelet goose SQL(postgres + sqlite 双份);提供 SQLite→PG 一次性迁移脚本 | 与框架迁移机制一致 |
|
||||
| 可观测分片 | 保留 10 分片策略;评估 ClickHouse 或继续 PG/SQLite 分片 | 访问量大时可用 Wavelet ClickHouse |
|
||||
| 进程部署 | 生产必须 `api` + `worker` + `scheduler` 三进程 | Wavelet 架构要求 |
|
||||
| 定时任务(OpenFlare) | **当前**:`openflare/tasks` + `robfig/cron`,随 `wavelet api` / `wavelet all` 在 API 进程内运行;**后续**:迁入统一任务框架(文件按 job 拆分,便于替换 registry) | 阶段一优先功能完备;与 Wavelet Asynq Worker/Scheduler 解耦 |
|
||||
| 数据库迁移 | Wavelet goose SQL(postgres + sqlite 双份);默认库名 `openflare`;SQLite→PG 一次性迁移脚本待补 | 与框架迁移机制一致 |
|
||||
| 可观测存储 | **v1 单表**(`of_node_*` 无 `_00`~`_09` 分片);历史分片数据需 ETL;后续可评估 ClickHouse 或恢复分片 | 降低迁移复杂度,满足当前联调与中小规模部署 |
|
||||
| 进程部署 | Wavelet 生产仍建议 `api` + `worker` + `scheduler` 三进程;**OpenFlare 业务 cron 仅需 API 进程** | 框架任务与 OpenFlare cron 职责分离 |
|
||||
| 前端嵌入 | 阶段一继续托管 `openflare-server/web/build`;阶段二切换 `Wavelet/frontend` embed | 分阶段降低风险 |
|
||||
|
||||
### 2.3 鉴权兼容设计
|
||||
@@ -170,7 +171,7 @@ sequenceDiagram
|
||||
| **A. 直接复用 Wavelet** | 不迁移代码,仅适配路径/响应 | 12 |
|
||||
| **B. 兼容适配层** | 薄 Handler 包装 Wavelet Logic | 8 |
|
||||
| **C. 完整迁移 OpenFlare 业务** | 重写为 Wavelet 规范(Handler+Logic+errs) | 15 |
|
||||
| **D. 基础设施迁移** | Cron→Asynq、WS Hub、分片表 | 6 |
|
||||
| **D. 基础设施迁移** | 定时任务(主进程 cron)、WS Hub、可观测单表 | 6 |
|
||||
|
||||
---
|
||||
|
||||
@@ -316,7 +317,7 @@ sequenceDiagram
|
||||
|---|---|---|---|
|
||||
| C39 | IP 组列表 | `GET /api/waf/ip-groups` | `openflare/waf/ip_group.go` |
|
||||
| C40 | IP 组 CRUD | `POST /api/waf/ip-groups/*` | 同上 |
|
||||
| C41 | IP 组同步 | `POST /api/waf/ip-groups/:id/sync` | 同上 + Asynq 任务 |
|
||||
| C41 | IP 组同步 | `POST /api/waf/ip-groups/:id/sync` | 同上 + `tasks/waf_ip_group_sync` cron |
|
||||
| C42 | IP 组测试 | `POST /api/waf/ip-groups/test` | 同上 |
|
||||
| C43 | 规则组列表 | `GET /api/waf/rule-groups` | `openflare/waf/rule_group.go` |
|
||||
| C44 | 规则组 CRUD | `POST /api/waf/rule-groups/*` | 同上 |
|
||||
@@ -371,18 +372,20 @@ sequenceDiagram
|
||||
| C68 | 应用日志 | `GET /api/apply-logs/` | `openflare/apply_log/logics.go` |
|
||||
| C69 | 应用日志清理 | `POST /api/apply-logs/cleanup` | 同上 |
|
||||
| C70 | GeoIP 查询 | `POST /api/option/geoip/lookup` | `openflare/geoip/logics.go` |
|
||||
| C71 | 数据库清理 | `POST /api/option/database/cleanup` | `openflare/observability/cleanup.go` |
|
||||
| C71 | 数据库清理 | `POST /api/option/database/cleanup` | `openflare/tasks/database_cleanup.go` |
|
||||
|
||||
**分片表迁移**:保留 10 分片(`_00`~`_09`),迁移至 goose SQL;评估后续接入 ClickHouse。
|
||||
**可观测存储(v1)**:goose 单表 `of_node_system_profiles`、`of_node_metric_snapshots`、`of_node_request_reports`、`of_node_health_events`、`of_node_obs_openresty`、`of_node_obs_frps`、`of_node_obs_frpc`、`of_node_access_logs`(迁移 `202606190010`~`012`)。访问日志查询/聚合已实装于 `model/openflare_access_log.go`。
|
||||
|
||||
### 6.9 运维集成
|
||||
### 6.9 运维集成与定时任务
|
||||
|
||||
| # | 功能 | 旧路径 | 目标 Logic | 任务类型 |
|
||||
| # | 功能 | 旧路径 / 调度 | 实现位置 | 调度(当前) |
|
||||
|---|---|---|---|---|
|
||||
| C72 | UptimeKuma 同步 | `POST /api/uptimekuma/sync` | `openflare/uptimekuma/logics.go` | Asynq periodic |
|
||||
| C73 | SSL 自动续期 | Cron 每日 | `openflare/tls/renew_task.go` | Asynq periodic |
|
||||
| C74 | WAF IP 订阅同步 | Cron 5min | `openflare/waf/sync_task.go` | Asynq periodic |
|
||||
| C75 | 可观测数据自动清理 | Cron | `openflare/observability/cleanup_task.go` | Asynq periodic |
|
||||
| C72 | UptimeKuma 同步 | `POST /api/uptimekuma/sync`;Cron 每分钟检查间隔 | `openflare/uptimekuma/sync.go` | `tasks/uptimekuma_sync.go` `* * * * *` |
|
||||
| C73 | SSL 自动续期 | Cron 每日 00:00 | `openflare/tls/obtain.go` + `tasks/ssl_renew.go` | `0 0 * * *` |
|
||||
| C74 | WAF IP 组同步 | Cron 每 5 分钟;`POST .../ip-groups/:id/sync` | `openflare/waf/ip_group_sync.go` | `@every 5m` |
|
||||
| C75 | 可观测数据自动清理 | Cron 每日 03:00(`DatabaseAutoCleanupEnabled`) | `openflare/tasks/database_cleanup.go` | `0 3 * * *` |
|
||||
|
||||
**任务注册**:各 job 在 `init()` 中 `registerJob`;`bootstrap.Init(API)` 调用 `tasks.Start`。WAF job 经 `waf/register_tasks.go` 注册以避免 import cycle。
|
||||
|
||||
---
|
||||
|
||||
@@ -420,12 +423,12 @@ sequenceDiagram
|
||||
| `apply_logs` | `of_apply_logs` | P1 | `openflare_apply_log.go` |
|
||||
| `node_system_profiles` | `of_node_system_profiles` | P1 | `openflare_node_profile.go` |
|
||||
| `node_health_events` | `of_node_health_events` | P2 | `openflare_health_event.go` |
|
||||
| `node_metric_snapshots_XX` | `of_node_metric_snapshots_XX` | P3 | `openflare_metric_snapshot.go` |
|
||||
| `node_request_reports_XX` | `of_node_request_reports_XX` | P3 | `openflare_request_report.go` |
|
||||
| `node_access_logs_XX` | `of_node_access_logs_XX` | P3 | `openflare_access_log.go` |
|
||||
| `node_observation_openresties_XX` | `of_node_obs_openresty_XX` | P3 | `openflare_obs_openresty.go` |
|
||||
| `node_observation_frps_XX` | `of_node_obs_frps_XX` | P3 | `openflare_obs_frps.go` |
|
||||
| `node_observation_frpcs_XX` | `of_node_obs_frpc_XX` | P3 | `openflare_obs_frpc.go` |
|
||||
| `node_metric_snapshots_XX` | `of_node_metric_snapshots`(v1 单表) | ✅ | `openflare_observability.go` |
|
||||
| `node_request_reports_XX` | `of_node_request_reports`(v1 单表) | ✅ | 同上 |
|
||||
| `node_access_logs_XX` | `of_node_access_logs`(v1 单表 + 复合索引) | ✅ | `openflare_access_log.go` |
|
||||
| `node_observation_openresties_XX` | `of_node_obs_openresty`(v1 单表) | ✅ | `openflare_observability.go` |
|
||||
| `node_observation_frps_XX` | `of_node_obs_frps`(v1 单表) | ✅ | 同上 |
|
||||
| `node_observation_frpcs_XX` | `of_node_obs_frpc`(v1 单表) | ✅ | 同上 |
|
||||
|
||||
### 7.3 数据迁移脚本
|
||||
|
||||
@@ -439,19 +442,19 @@ sequenceDiagram
|
||||
|
||||
## 8. 分阶段实施计划
|
||||
|
||||
### 阶段 0:基建准备(1–2 周)
|
||||
### 阶段 0:基建准备(1–2 周)— ✅ 已完成
|
||||
|
||||
| 任务 ID | 任务 | 产出 | 验收标准 |
|
||||
|---|---|---|---|
|
||||
| B0-1 | 在 `Wavelet/go.mod` 添加 `pkg/geoip`、`pkg/render`、`pkg/protocol` 引用 | go.mod replace | `go build ./...` 通过 |
|
||||
| B0-2 | 创建 `internal/apps/openflare/` 骨架 | 包结构 | 符合 AGENTS.md |
|
||||
| B0-3 | 实现 `compat/auth.go`、`compat/response.go` | 鉴权/响应适配 | 单元测试覆盖 JWT 校验 |
|
||||
| B0-4 | 注册 legacy 路由组 `RegisterLegacyRoutes(apiGroup)` | `router/legacy.go` | `/api/status` 可访问 |
|
||||
| B0-5 | bootstrap 注册 OpenFlare 模块 | `bootstrap/openflare.go` | worker/scheduler 可启动 |
|
||||
| B0-3 | 实现 `compat/auth.go`、`compat/response.go` | 鉴权/响应适配 | 单元测试覆盖 |
|
||||
| B0-4 | 注册 legacy 路由组 | `legacy/register*.go` | `/api/status` 可访问 |
|
||||
| B0-5 | bootstrap 注册 OpenFlare 后台任务 | `bootstrap.go` + `openflare/tasks` | API 启动后 cron 注册日志可见 |
|
||||
| B0-6 | 同步 Wavelet Skills 到开发环境 | `.claude/skills/` | AI 开发可用 |
|
||||
| B0-7 | 编写 OpenFlare 业务设计补充文档 | `docs/design/openflare-wavelet-integration.md` | 评审通过 |
|
||||
| B0-7 | 编写 OpenFlare 业务设计补充文档 | `docs/design/openflare-wavelet-integration.md` | 待评审(可选) |
|
||||
|
||||
### 阶段 1:认证与用户联调(1 周)
|
||||
### 阶段 1:认证与用户联调(1 周)— ✅ 已完成
|
||||
|
||||
| 任务 ID | 任务 | 依赖 |
|
||||
|---|---|---|
|
||||
@@ -465,7 +468,7 @@ sequenceDiagram
|
||||
|
||||
**验收**:旧前端 `/login` 可登录,`OpenFlare-Token` 可访问 `/api/user/self`。
|
||||
|
||||
### 阶段 2:核心链路(2–3 周)
|
||||
### 阶段 2:核心链路(2–3 周)— ✅ 已完成
|
||||
|
||||
| 任务 ID | 模块 | API 数 |
|
||||
|---|---|---|
|
||||
@@ -479,37 +482,37 @@ sequenceDiagram
|
||||
|
||||
**验收**:创建节点 → 创建规则 → 发布配置 → Agent 拉取并回报 apply-log。
|
||||
|
||||
### 阶段 3:安全与证书(2 周)
|
||||
### 阶段 3:安全与证书(2 周)— ✅ 已完成
|
||||
|
||||
| 任务 ID | 模块 |
|
||||
|---|---|
|
||||
| B3-1 | WAF 规则组 + IP 组 + 站点绑定 |
|
||||
| B3-2 | Agent WAF 同步 |
|
||||
| B3-3 | TLS 证书 + ACME + DNS |
|
||||
| B3-4 | ManagedDomain |
|
||||
| B3-5 | SSL 续期 Asynq 任务 |
|
||||
| 任务 ID | 模块 | 状态 |
|
||||
|---|---|---|
|
||||
| B3-1 | WAF 规则组 + IP 组 + 站点绑定 | ✅ |
|
||||
| B3-2 | Agent WAF 同步(heartbeat + `/waf/ip-groups/sync`) | ✅ |
|
||||
| B3-3 | TLS 证书 + ACME(lego DNS-01)+ DNS | ✅ |
|
||||
| B3-4 | ManagedDomain | ✅ |
|
||||
| B3-5 | SSL 续期定时任务 | ✅ `tasks/ssl_renew.go` |
|
||||
|
||||
### 阶段 4:扩展能力(2–3 周)
|
||||
### 阶段 4:扩展能力(2–3 周)— ✅ 已完成(v1 单表)
|
||||
|
||||
| 任务 ID | 模块 |
|
||||
|---|---|
|
||||
| B4-1 | Pages 托管 + Agent 包下载 |
|
||||
| B4-2 | Relay + Flared API + WS |
|
||||
| B4-3 | Dashboard 聚合 |
|
||||
| B4-4 | 可观测性 + 分片表 |
|
||||
| B4-5 | 访问日志查询/清理 |
|
||||
| B4-6 | UptimeKuma 集成 |
|
||||
| 任务 ID | 模块 | 状态 |
|
||||
|---|---|---|
|
||||
| B4-1 | Pages 托管 + Agent 包下载 | ✅ |
|
||||
| B4-2 | Relay + Flared API + WS + 观测持久化 | ✅ |
|
||||
| B4-3 | Dashboard 聚合 | ✅ |
|
||||
| B4-4 | 可观测性单表 + Agent heartbeat 写入 | ✅ |
|
||||
| B4-5 | 访问日志查询/清理 | ✅ |
|
||||
| B4-6 | UptimeKuma 集成(API + cron) | ✅ |
|
||||
|
||||
### 阶段 5:运维与收尾(1 周)
|
||||
### 阶段 5:运维与收尾(1 周)— 进行中
|
||||
|
||||
| 任务 ID | 任务 |
|
||||
|---|---|
|
||||
| B5-1 | 服务自更新兼容 |
|
||||
| B5-2 | GeoIP 查询 |
|
||||
| B5-3 | 数据迁移脚本与文档 |
|
||||
| B5-4 | `make swagger` + `make code-check` 全绿 |
|
||||
| B5-5 | 全量 API 回归(对照旧后端 120+ 端点) |
|
||||
| B5-6 | 编写 Handover 文档 |
|
||||
| 任务 ID | 任务 | 状态 |
|
||||
|---|---|---|
|
||||
| B5-1 | 服务自更新兼容 | ✅ |
|
||||
| B5-2 | GeoIP 查询 + Agent Geo 自动更新 | ✅ |
|
||||
| B5-3 | 数据迁移脚本与文档 | ⏳ 待实现 `support-files/migration/` |
|
||||
| B5-4 | `make swagger` + `make code-check` 全绿 | ⏳ |
|
||||
| B5-5 | 全量 API 回归(对照旧后端 120+ 端点) | ⏳ |
|
||||
| B5-6 | Handover 文档 | ✅ 见 `handover-openflare-backend-migration.md` |
|
||||
|
||||
---
|
||||
|
||||
@@ -539,13 +542,14 @@ Wavelet/internal/apps/openflare/
|
||||
├── update/
|
||||
├── uptimekuma/
|
||||
├── geoip/
|
||||
├── tasks/ # 定时/后台任务(主进程 cron,后续可迁框架)
|
||||
└── websocket/
|
||||
|
||||
Wavelet/internal/model/openflare_*.go # 约 20 个实体
|
||||
Wavelet/internal/db/migrator/goose/ # postgres/ + sqlite/ 双份 SQL
|
||||
Wavelet/internal/router/legacy.go # RegisterLegacyRoutes
|
||||
Wavelet/internal/bootstrap/openflare.go # 任务/事件注册
|
||||
Wavelet/support-files/migration/ # 数据迁移工具
|
||||
Wavelet/internal/model/openflare_*.go # 业务实体 + access_log 查询
|
||||
Wavelet/internal/db/migrator/goose/ # postgres/ + sqlite/;of_* 至 202606190012
|
||||
Wavelet/internal/apps/openflare/legacy/ # RegisterRoutes 委派
|
||||
Wavelet/internal/bootstrap/bootstrap.go # RegisterOpenFlareBackgroundTasks + tasks.Start
|
||||
Wavelet/support-files/migration/ # 数据迁移工具(待建)
|
||||
```
|
||||
|
||||
### 9.2 修改文件(仅允许范围内)
|
||||
@@ -607,11 +611,11 @@ go test ./internal/apps/openflare/compat/... -v
|
||||
|
||||
```bash
|
||||
# Agent 心跳
|
||||
curl -X POST http://localhost:8000/api/agent/nodes/heartbeat \
|
||||
curl -X POST http://127.0.0.1:3000/api/agent/nodes/heartbeat \
|
||||
-H "X-Agent-Token: <token>" -d '{...}'
|
||||
|
||||
# 拉取配置
|
||||
curl http://localhost:8000/api/agent/config-versions/active \
|
||||
curl http://127.0.0.1:3000/api/agent/config-versions/active \
|
||||
-H "X-Agent-Token: <token>"
|
||||
```
|
||||
|
||||
@@ -624,7 +628,7 @@ curl http://localhost:8000/api/agent/config-versions/active \
|
||||
| JWT vs Session 双轨 | 旧前端鉴权失败 | compat 层统一;充分测试 |
|
||||
| 角色三级 vs is_admin | Root 权限丢失 | `of_role` 扩展字段 |
|
||||
| 70+ Option 热加载 | 配置行为不一致 | 保留 OptionMap 机制 |
|
||||
| 10 分片表迁移复杂 | 观测数据丢失 | 分阶段迁移;先 schema 后历史数据 |
|
||||
| 旧环境 10 分片观测数据 | 无法直接挂载单表 schema | v1 采用单表;提供 ETL 脚本(待 B5-3);新部署无影响 |
|
||||
| 微信登录无 Wavelet 原生 | 部分用户无法登录 | 评估废弃或独立 legacy |
|
||||
| 三进程部署运维复杂度 | 部署失败 | 提供 docker-compose 与 `wavelet all` 开发模式 |
|
||||
| WebSocket Hub 迁移 | 节点掉线 | 保持 Hub 接口不变,充分压测 |
|
||||
|
||||
@@ -1,40 +1,141 @@
|
||||
# OpenFlare 后端迁移 — 任务拆分与委派
|
||||
# OpenFlare 后端迁移 — AI 接手文档
|
||||
|
||||
> **状态**:进行中
|
||||
> **基建**:Batch 0 已完成(`Wavelet/internal/apps/openflare/compat/` + `legacy/register*.go` + `router.go` 挂载)
|
||||
> **状态**:进行中(阶段 5 收尾)
|
||||
> **主线**:`openflare-server` 业务域已迁入 `Wavelet/internal/apps/openflare/`,阶段一通过 `/api/*` legacy 层联调旧前端
|
||||
> **关联计划**:[实现计划](./20260618-openflare-wavelet-backend-migration.md)
|
||||
|
||||
## 任务隔离规则
|
||||
---
|
||||
|
||||
## 1. 当前任务状态
|
||||
|
||||
### 主线任务
|
||||
|
||||
将 OpenFlare 控制面后端从 `openflare-server` 迁移至 Wavelet 框架,保留旧前端 API 路径,复用 Wavelet 用户/OAuth/Cap 等平台能力,并完成 Agent/Relay/Flared 协议兼容。
|
||||
|
||||
### 开发分支
|
||||
|
||||
- 分支:`dev`(相对 `origin/dev` 领先若干提交)
|
||||
- 工作区:迁移计划文档 `docs/plan/20260618-openflare-wavelet-backend-migration.md` 可能仍有未提交修改
|
||||
|
||||
### 已完成(✅)
|
||||
|
||||
- [x] **Batch 0 基建**:`compat/`、`legacy/register*.go`、`router.go` 挂载 `/api/*`
|
||||
- [x] **T-AUTH ~ T-MISC** 全部 14 个业务任务队列项(见下表)
|
||||
- [x] **goose 迁移** `202606190001` ~ `202606190012`(PostgreSQL + SQLite 双份)
|
||||
- [x] **可观测性 v1 单表**:无 `_00`~`_09` 分片;heartbeat 持久化、访问日志查询、Relay/Flared 观测
|
||||
- [x] **定时任务包** `internal/apps/openflare/tasks/`(主进程 cron,非 Asynq)
|
||||
- [x] **TLS ACME** 申请/续期(lego DNS-01)
|
||||
- [x] **集成测试** 4 个场景包全部通过(23 用例)
|
||||
- [x] **配置默认值**:DB 名 `openflare`、监听 `:3000`、`application_name=openflare-server`、Redis 前缀 `openflare:`
|
||||
|
||||
### 进行中(阶段 5)
|
||||
|
||||
- [/] **B5-3** 旧环境数据迁移脚本 `support-files/migration/`(用户 + 业务表 ETL,含 10 分片观测数据合并)
|
||||
- [/] **B5-4** `make swagger` + `make code-check` 全绿
|
||||
- [/] **B5-5** 全量 API 回归(120+ 端点对照表)
|
||||
|
||||
### 待处理
|
||||
|
||||
- [ ] 生产环境:创建并**激活** config version,消除节点列表「异常」(`openresty_status=unhealthy` + `当前没有激活版本`)
|
||||
- [ ] 旧前端静态托管与 Wavelet 嵌入前端的联调路径文档化(阶段二切 `Wavelet/frontend`)
|
||||
|
||||
---
|
||||
|
||||
## 2. 任务队列(委派状态)
|
||||
|
||||
| ID | 板块 | 状态 | 负责目录 |
|
||||
|---|---|---|---|
|
||||
| T-AUTH | 认证/用户/OAuth/Cap | ✅ | `legacy/register_auth.go`, `openflare/auth/`, `compat/auth.go` |
|
||||
| T-OPTION | 状态/公告/Option | ✅ | `legacy/register_option.go`, `openflare/option/` |
|
||||
| T-ORIGIN | 源站 | ✅ | `legacy/register_origin.go`, `openflare/origin/` |
|
||||
| T-APPLYLOG | 应用日志 | ✅ | `legacy/register_apply_log.go`, `openflare/apply_log/` |
|
||||
| T-PROXY | 代理规则 | ✅ | `legacy/register_proxy_route.go`, `openflare/proxy_route/` |
|
||||
| T-NODE | 节点管理 | ✅ | `legacy/register_node.go`, `openflare/node/` |
|
||||
| T-WAF | WAF | ✅ | `legacy/register_waf.go`, `openflare/waf/` |
|
||||
| T-TLS | TLS/证书/域名/DNS | ✅ | `legacy/register_tls.go`, `openflare/tls/` |
|
||||
| T-CFGVER | 配置版本 | ✅ | `legacy/register_config_version.go`, `openflare/config_version/` |
|
||||
| T-AGENT | Agent API + WS | ✅ | `legacy/register_agent.go`, `openflare/agent/`, `openflare/websocket/` |
|
||||
| T-PAGES | Pages 托管 | ✅ | `legacy/register_pages.go`, `openflare/pages/` |
|
||||
| T-RELAY | Relay + Flared | ✅ | `legacy/register_relay_flared.go`, `openflare/relay/`, `openflare/flared/` |
|
||||
| T-OBS | 仪表盘 + 可观测 | ✅ | `legacy/register_dashboard_obs.go`, `openflare/dashboard/`, `openflare/observability/` |
|
||||
| T-MISC | 升级/GeoIP/UptimeKuma | ✅ | `legacy/register_misc.go`, `openflare/update/`, `openflare/geoip/`, `openflare/uptimekuma/` |
|
||||
|
||||
### 任务隔离规则
|
||||
|
||||
| 规则 | 说明 |
|
||||
|---|---|
|
||||
| 文件所有权 | 每个任务 **仅修改** 自己的 `internal/apps/openflare/<module>/`、对应 `legacy/register_<module>.go`、`internal/model/openflare_<module>.go`、goose SQL |
|
||||
| 禁止修改 | `v1/user.go`、`v1/admin.go`、`model/users.go`、其他任务的 `register_*.go` |
|
||||
| 响应格式 | 旧前端兼容 API 使用 `compat.OK/Fail/Unauthorized`(`{success,message,data}`) |
|
||||
| 鉴权 | 管理端 `compat.AdminAuth()` / `compat.RootAuth()`;用户 `compat.UserAuth()` |
|
||||
| 鉴权 | 管理端 `compat.AdminAuth()` / `compat.RootAuth()`;用户 `compat.UserAuth()`;全局 `OpenFlare-Token` 桥接见 `legacy/register.go` |
|
||||
| Logic 层 | `logics.go` 使用 `context.Context`,不依赖 `*gin.Context` |
|
||||
| 数据源 | `db.DB(ctx)` 获取 GORM |
|
||||
| 质量门禁 | 完成后 `go build ./...` 并通过本模块测试 |
|
||||
|
||||
## 任务队列
|
||||
---
|
||||
|
||||
| ID | 板块 | 状态 | 负责文件 |
|
||||
## 3. 核心文件与上下文
|
||||
|
||||
### 路由与兼容层
|
||||
|
||||
| 路径 | 职责 |
|
||||
|---|---|
|
||||
| `Wavelet/internal/router/router.go` | 在 `apiGroup` 下调用 `oflegacy.RegisterRoutes` |
|
||||
| `Wavelet/internal/apps/openflare/legacy/register.go` | 汇总各 `register_*.go`;`OpenFlare-Token` 全局桥接 |
|
||||
| `Wavelet/internal/apps/openflare/compat/auth.go` | JWT ↔ Session/AccessToken、角色 `of_role` 解析 |
|
||||
| `Wavelet/internal/apps/openflare/compat/response.go` | Wavelet 信封 → 旧 `{success,message,data}` |
|
||||
|
||||
### 定时任务(OpenFlare cron)
|
||||
|
||||
| 文件 | Job 名 | Cron | 说明 |
|
||||
|---|---|---|---|
|
||||
| T-AUTH | 认证/用户/OAuth/Cap | ✅ 完成 | `legacy/register_auth.go`, `openflare/auth/`, `legacy/auth_*.go` |
|
||||
| T-OPTION | 状态/公告/Option | ✅ 完成 | `legacy/register_option.go`, `openflare/option/` |
|
||||
| T-ORIGIN | 源站 | ✅ 完成 | `legacy/register_origin.go`, `openflare/origin/` |
|
||||
| T-APPLYLOG | 应用日志 | ✅ 完成 | `legacy/register_apply_log.go`, `openflare/apply_log/` |
|
||||
| T-PROXY | 代理规则 | ✅ 完成 | `legacy/register_proxy_route.go`, `openflare/proxy_route/` |
|
||||
| T-NODE | 节点管理 | ✅ 完成 | `legacy/register_node.go`, `openflare/node/` |
|
||||
| T-WAF | WAF | ✅ 完成 | `legacy/register_waf.go`, `openflare/waf/` |
|
||||
| T-TLS | TLS/证书/域名/DNS | ✅ 完成 | `legacy/register_tls.go`, `openflare/tls/` |
|
||||
| T-CFGVER | 配置版本 | ✅ 完成 | `legacy/register_config_version.go`, `openflare/config_version/` |
|
||||
| T-AGENT | Agent API + WS | ✅ 完成 | `legacy/register_agent.go`, `openflare/agent/`, `openflare/websocket/` |
|
||||
| T-PAGES | Pages 托管 | ✅ 完成 | `legacy/register_pages.go`, `openflare/pages/` |
|
||||
| T-RELAY | Relay + Flared | ✅ 完成 | `legacy/register_relay_flared.go`, `openflare/relay/`, `openflare/flared/` |
|
||||
| T-OBS | 仪表盘 + 可观测 | ✅ 完成 | `legacy/register_dashboard_obs.go`, `openflare/dashboard/`, `openflare/observability/` |
|
||||
| T-MISC | 升级/GeoIP/UptimeKuma | ✅ 完成 | `legacy/register_misc.go`, `openflare/update/`, `openflare/geoip/` |
|
||||
| `tasks/database_cleanup.go` | `database_auto_cleanup` | `0 3 * * *` | 可观测数据自动清理(受 Option 开关控制) |
|
||||
| `tasks/waf_ip_group_sync.go` | `waf_ip_group_sync` | `@every 5m` | WAF IP 组周期同步 |
|
||||
| `tasks/uptimekuma_sync.go` | `uptime_kuma_sync` | `* * * * *` | 按 Option 间隔触发 UptimeKuma 同步 |
|
||||
| `tasks/ssl_renew.go` | `ssl_renew` | `0 0 * * *` | ACME 证书自动续期 |
|
||||
| `waf/register_tasks.go` | — | — | 通过 `RegisterCronJob` 注册,避免 import cycle |
|
||||
|
||||
## 集成测试结果(2026-06-19)
|
||||
**启动链路**:`wavelet api` / `wavelet all` → `bootstrap.RegisterAPI()` → `bootstrap.Init(ctx, {API:true})` → `oftasks.Start(ctx)`。
|
||||
**注意**:OpenFlare cron **仅在 API 进程**运行;`wavelet worker` / `wavelet scheduler` 负责 Wavelet 框架 Asynq 任务,与 OpenFlare cron 解耦。
|
||||
|
||||
### 数据库迁移
|
||||
|
||||
| 版本 | 内容 |
|
||||
|---|---|
|
||||
| `202606190001` | `of_options` |
|
||||
| `202606190002` | `of_origins` |
|
||||
| `202606190003` | `of_apply_logs` |
|
||||
| `202606190004` | `of_proxy_routes` |
|
||||
| `202606190005` | `of_nodes` |
|
||||
| `202606190006` | `of_waf_*` |
|
||||
| `202606190007` | `of_tls_*` |
|
||||
| `202606190008` | `of_config_versions` |
|
||||
| `202606190009` | `of_pages_*` |
|
||||
| `202606190010` | 可观测性单表(profiles/metrics/reports/health/openresty/frps/access_logs) |
|
||||
| `202606190011` | `of_node_access_logs` 复合索引 `(node_id, logged_at)` |
|
||||
| `202606190012` | `of_node_obs_frpc` |
|
||||
|
||||
路径:`Wavelet/internal/db/migrator/goose/postgres/` 与 `sqlite/` 各一份。
|
||||
|
||||
### 近期修复要点(2026-06-19)
|
||||
|
||||
| 问题 | 根因 | 修复位置 |
|
||||
|---|---|---|
|
||||
| Agent 已连接但节点显示「异常」 | 无激活 config version → `openresty_status=unhealthy` | 运维:发布并激活配置版本 |
|
||||
| 节点详情无错误日志 | 可观测性表未迁移 | `202606190010` + `agent/observability.go` |
|
||||
| WAF IP 组未下发 | Agent sync 未实装 | `agent/waf_ip_group.go`、`waf/ip_group_sync.go` |
|
||||
| Pages 包下载 404 | Agent 路由缺失 | `pages/logics.go`、`agent/routers.go` |
|
||||
| 旧前端 Token 部分路由 401 | 未全局桥接 | `legacy/register.go` |
|
||||
| 访问日志查询空 | 单表查询层缺失 | `model/openflare_access_log.go`、`202606190011` |
|
||||
| Relay/Flared 观测缺失 | heartbeat 未持久化 | `relay/observability.go`、`flared/observability.go`、`202606190012` |
|
||||
|
||||
### 依赖修复
|
||||
|
||||
`replace github.com/rain-kl/openflare => ../` 会引入根 `go.mod` 的 `gomodule/redigo v2.0.0+incompatible`,与 `gin-contrib/sessions/redistore` 不兼容。已在 `Wavelet/go.mod` 添加 `exclude` 并锁定 `redigo v1.9.3`。
|
||||
|
||||
---
|
||||
|
||||
## 4. 集成测试
|
||||
|
||||
| 测试包 | 场景 | 结果 |
|
||||
|---|---|---|
|
||||
@@ -44,13 +145,85 @@
|
||||
| `integration/agent_protocol_test.go` | Agent/Relay/Flared 协议 | ✅ 5/5 |
|
||||
|
||||
```bash
|
||||
go test ./internal/apps/openflare/... -count=1 # 全部通过
|
||||
cd Wavelet
|
||||
go test ./internal/apps/openflare/... -count=1
|
||||
go build ./...
|
||||
```
|
||||
|
||||
**依赖修复(2026-06-19)**:`replace github.com/rain-kl/openflare => ../` 会引入 OpenFlare 根 `go.mod` 的 `gomodule/redigo v2.0.0+incompatible`,与 `gin-contrib/sessions/redistore` 不兼容。已在 `Wavelet/go.mod` 添加 `exclude` 并锁定 `redigo v1.9.3`,`go build ./...` 正常。
|
||||
---
|
||||
|
||||
## 源代码参考
|
||||
## 5. 本地启动(Wavelet 后端)
|
||||
|
||||
- 旧后端:`openflare-server/internal/controller/`、`service/`、`model/`
|
||||
- 迁移计划:`docs/plan/20260618-openflare-wavelet-backend-migration.md`
|
||||
- Wavelet 规范:`Wavelet/AGENTS.md`
|
||||
### 配置
|
||||
|
||||
复制 `Wavelet/config.example.yaml` → `config.yaml`,关键项:
|
||||
|
||||
```yaml
|
||||
app:
|
||||
app_name: "openflare"
|
||||
addr: ":3000"
|
||||
database:
|
||||
enabled: true
|
||||
database: "openflare"
|
||||
application_name: "openflare-server"
|
||||
redis:
|
||||
key_prefix: "openflare:"
|
||||
```
|
||||
|
||||
或使用 `.env` / 环境变量:`DB_ENABLED=true`、`DB_NAME=openflare`、`POSTGRES_DB=openflare`。
|
||||
|
||||
### 启动命令
|
||||
|
||||
```bash
|
||||
cd Wavelet
|
||||
# 开发:API + Worker + Scheduler 合一
|
||||
go run . all
|
||||
|
||||
# 生产:分进程(OpenFlare cron 仅需 api 进程)
|
||||
go run . api
|
||||
go run . worker
|
||||
go run . scheduler
|
||||
```
|
||||
|
||||
### 阶段一旧前端联调
|
||||
|
||||
- **API**:指向 Wavelet `:3000` 的 `/api/*`(legacy 兼容层)
|
||||
- **静态页面**:阶段一仍使用 `openflare-server/web/build`(由旧 Server 托管或独立静态服务);Wavelet 默认嵌入的是 `Wavelet/frontend`,非旧 OpenFlare UI
|
||||
- **鉴权**:旧前端使用 `OpenFlare-Token` Header;登录 `POST /api/user/login` 由 legacy 层签发 JWT
|
||||
|
||||
---
|
||||
|
||||
## 6. 待决策与遗留问题
|
||||
|
||||
| 项 | 说明 | 当前处理 |
|
||||
|---|---|---|
|
||||
| 观测数据 10 分片 → 单表 | 旧生产环境有 `node_*_XX` 分片表 | v1 单表;B5-3 ETL 脚本待建 |
|
||||
| 微信登录 | Wavelet 无内置 | legacy 快捷路由已补;长期评估废弃或独立维护 |
|
||||
| OpenFlare cron vs Asynq | 两套任务体系并存 | 阶段一 cron 在 API 进程;后续可迁入统一框架 |
|
||||
| 节点「异常」展示 | 连接正常但无激活版本 | 属预期行为;需运维发布配置 |
|
||||
| `make code-check` | 全仓库静态检查 | 阶段 5 待跑通 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 下一步行动指南
|
||||
|
||||
新接手 AI 建议按以下顺序执行:
|
||||
|
||||
1. **确认环境**:`cd Wavelet && go build ./... && go test ./internal/apps/openflare/... -count=1`
|
||||
2. **跑质量门禁**:`make code-check`(修复 swagger/静态检查问题)
|
||||
3. **实现 B5-3**:在 `Wavelet/support-files/migration/` 编写 `users` → `w_users` 与业务表 ETL(含分片观测合并)
|
||||
4. **API 回归**:对照实现计划 §12 端点表,用 curl/集成测试逐项验证
|
||||
5. **联调验证**:旧前端登录 → 创建规则 → 发布并**激活** config version → Agent 心跳 → 检查节点状态与 apply-log
|
||||
|
||||
---
|
||||
|
||||
## 8. 参考文档
|
||||
|
||||
| 文档 | 用途 |
|
||||
|---|---|
|
||||
| [实现计划](./20260618-openflare-wavelet-backend-migration.md) | 完整模块清单、端点对照、分阶段验收 |
|
||||
| [前端迁移计划](./20260618-openflare-wavelet-frontend-migration.md) | 阶段二 UI 迁移 |
|
||||
| [`Wavelet/AGENTS.md`](../../Wavelet/AGENTS.md) | 框架 Guardrails 与 Skills |
|
||||
| [`docs/guideline/Constraints.md`](../guideline/Constraints.md) | 开发约束 |
|
||||
| 旧后端源码 | `openflare-server/internal/controller/`、`service/`、`model/` |
|
||||
| Changelog | [`docs/changelog/index.md`](../changelog/index.md) `[Unreleased]` |
|
||||
@@ -14,6 +14,7 @@
|
||||
| 计划 | 说明 |
|
||||
| --- | --- |
|
||||
| [OpenFlare → Wavelet 后端迁移计划](./20260618-openflare-wavelet-backend-migration.md) | 将 `openflare-server` 后端迁移至 Wavelet 框架,保留 `/api/*` 路径,复用用户/认证等平台能力 |
|
||||
| [OpenFlare 后端迁移 — AI 接手](./handover-openflare-backend-migration.md) | 后端迁移当前进度、任务队列、定时任务、goose 版本与下一步行动(阶段 5 收尾) |
|
||||
| [OpenFlare → Wavelet 前端迁移计划](./20260618-openflare-wavelet-frontend-migration.md) | 将 `openflare-server/web` 业务 UI 按 Wavelet 设计风格重写,复用框架组件与 Admin 基建 |
|
||||
|
||||
## 使用建议
|
||||
|
||||
Reference in New Issue
Block a user