This commit is contained in:
ryan
2026-06-18 17:08:21 +08:00
parent dfc480c73a
commit 01c28bb88b
11 changed files with 494 additions and 129 deletions
@@ -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
View File
@@ -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:
+1
View File
@@ -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
+62
View File
@@ -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 接口不变,充分压测 |
+201 -28
View File
@@ -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]` |
+1
View File
@@ -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 基建 |
## 使用建议