From e479ae75e6e4feafd0e1caf0264ea8d209839418 Mon Sep 17 00:00:00 2001 From: ryan Date: Sun, 21 Jun 2026 11:12:37 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20Pages=20=E8=B7=AF=E7=94=B1?= =?UTF-8?q?=E5=8F=91=E5=B8=83=E5=A4=B1=E8=B4=A5=E5=B9=B6=E6=8A=A5=20`pages?= =?UTF-8?q?=20module=20is=20not=20available`=EF=BC=9A=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E5=BF=AB=E7=85=A7=E5=8F=91=E5=B8=83=E6=B5=81=E7=A8=8B=E8=A1=A5?= =?UTF-8?q?=E9=BD=90=20Pages=20=E9=A1=B9=E7=9B=AE=E6=BF=80=E6=B4=BB?= =?UTF-8?q?=E9=83=A8=E7=BD=B2=E8=A7=A3=E6=9E=90=E4=B8=8E=20`pages=5Fdeploy?= =?UTF-8?q?ment`=20=E5=86=99=E5=85=A5=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/changelog/index.md | 2 + .../apps/openflare/config_version/logics.go | 22 ++++- .../config_version/pages_snapshot.go | 97 +++++++++++++++++++ .../config_version/pages_snapshot_test.go | 91 +++++++++++++++++ .../apps/openflare/config_version/snapshot.go | 66 +++++++------ internal/model/openflare_option.go | 6 -- 6 files changed, 247 insertions(+), 37 deletions(-) create mode 100644 internal/apps/openflare/config_version/pages_snapshot.go create mode 100644 internal/apps/openflare/config_version/pages_snapshot_test.go diff --git a/docs/changelog/index.md b/docs/changelog/index.md index e4cbef13..8168602f 100644 --- a/docs/changelog/index.md +++ b/docs/changelog/index.md @@ -22,6 +22,8 @@ sidebar: false ### 修复 +- 修复 Pages 路由发布失败并报 `pages module is not available`:配置快照发布流程补齐 Pages 项目激活部署解析与 `pages_deployment` 写入。 + - 修复仪表盘与节点详情「24 小时网络趋势」误按速率展示:改为 OpenResty 入/出站小时流量与近 24 小时总量摘要,Y 轴与 tooltip 自动换算 B/KB/MB/GB。 - 修复 Pages 上传或节点同步时报 `pages file size out of bounds`:允许 ZIP 包内的 0 字节文件,并兼容未声明解压大小的 ZIP 条目。 diff --git a/internal/apps/openflare/config_version/logics.go b/internal/apps/openflare/config_version/logics.go index 90a99eb4..d7755ad0 100644 --- a/internal/apps/openflare/config_version/logics.go +++ b/internal/apps/openflare/config_version/logics.go @@ -16,6 +16,7 @@ import ( "github.com/Rain-kl/Wavelet/internal/apps/openflare/routeidentity" "github.com/Rain-kl/Wavelet/internal/model" + openrestyrender "github.com/Rain-kl/Wavelet/pkg/render/openresty" "gorm.io/gorm" ) @@ -398,7 +399,26 @@ func snapshotRouteIdentityEqual(left, right snapshotRoute) bool { func snapshotRouteOriginEqual(left, right snapshotRoute) bool { return left.OriginURL == right.OriginURL && left.OriginHost == right.OriginHost && - left.UpstreamType == right.UpstreamType + left.UpstreamType == right.UpstreamType && + snapshotPagesDeploymentEqual(left.PagesDeployment, right.PagesDeployment) +} + +func snapshotPagesDeploymentEqual(left, right *openrestyrender.PagesDeployment) bool { + if left == nil && right == nil { + return true + } + if left == nil || right == nil { + return false + } + leftJSON, err := json.Marshal(left) + if err != nil { + return false + } + rightJSON, err := json.Marshal(right) + if err != nil { + return false + } + return string(leftJSON) == string(rightJSON) } func snapshotRoutePolicyEqual(left, right snapshotRoute) bool { diff --git a/internal/apps/openflare/config_version/pages_snapshot.go b/internal/apps/openflare/config_version/pages_snapshot.go new file mode 100644 index 00000000..9c51d747 --- /dev/null +++ b/internal/apps/openflare/config_version/pages_snapshot.go @@ -0,0 +1,97 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + +package config_version + +import ( + "context" + "errors" + "fmt" + "strings" + + "github.com/Rain-kl/Wavelet/internal/model" + openrestyrender "github.com/Rain-kl/Wavelet/pkg/render/openresty" + "gorm.io/gorm" +) + +const defaultPagesSnapshotEntryFile = "index.html" +const defaultPagesSnapshotFallbackPath = "/index.html" + +func buildPagesRouteSnapshot( + ctx context.Context, + route *model.ProxyRoute, +) (originURL string, upstreams []string, pagesProjectID *uint, deployment *openrestyrender.PagesDeployment, err error) { + if route == nil { + return "", nil, nil, nil, errors.New("pages 路由配置无效") + } + if !model.HasPagesProjectsTable(ctx) { + return "", nil, nil, nil, fmt.Errorf("路由 %s Pages 配置无效: pages 模块不可用", route.Domain) + } + if route.PagesProjectID == nil || *route.PagesProjectID == 0 { + return "", nil, nil, nil, fmt.Errorf("路由 %s Pages 配置无效: 未绑定 Pages 项目", route.Domain) + } + project, err := model.GetPagesProjectByID(ctx, *route.PagesProjectID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return "", nil, nil, nil, fmt.Errorf("路由 %s Pages 配置无效: pages 项目不存在", route.Domain) + } + return "", nil, nil, nil, err + } + if !project.Enabled { + return "", nil, nil, nil, fmt.Errorf("路由 %s Pages 配置无效: pages 项目未启用", route.Domain) + } + if project.ActiveDeploymentID == nil || *project.ActiveDeploymentID == 0 { + return "", nil, nil, nil, fmt.Errorf("路由 %s Pages 配置无效: pages 项目没有激活部署", route.Domain) + } + activeDeployment, err := model.GetPagesDeploymentByID(ctx, *project.ActiveDeploymentID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return "", nil, nil, nil, fmt.Errorf("路由 %s Pages 配置无效: pages 激活部署不存在", route.Domain) + } + return "", nil, nil, nil, err + } + if activeDeployment.ProjectID != project.ID { + return "", nil, nil, nil, fmt.Errorf("路由 %s Pages 配置无效: pages 激活部署不匹配", route.Domain) + } + if strings.TrimSpace(activeDeployment.Checksum) == "" { + return "", nil, nil, nil, fmt.Errorf("路由 %s Pages 配置无效: pages 部署校验和缺失", route.Domain) + } + + pagesProjectID = route.PagesProjectID + deployment = buildSnapshotPagesDeployment(project, activeDeployment) + originURL = fmt.Sprintf("openflare-pages://project/%d", project.ID) + return originURL, []string{originURL}, pagesProjectID, deployment, nil +} + +func buildSnapshotPagesDeployment(project *model.PagesProject, activeDeployment *model.PagesDeployment) *openrestyrender.PagesDeployment { + if project == nil || activeDeployment == nil { + return nil + } + entryFile := strings.TrimSpace(project.EntryFile) + if entryFile == "" { + entryFile = defaultPagesSnapshotEntryFile + } + fallbackPath := strings.TrimSpace(project.SPAFallbackPath) + if fallbackPath == "" { + fallbackPath = defaultPagesSnapshotFallbackPath + } + return &openrestyrender.PagesDeployment{ + ProjectID: project.ID, + ProjectSlug: strings.TrimSpace(project.Slug), + DeploymentID: activeDeployment.ID, + DeploymentNumber: activeDeployment.DeploymentNumber, + Checksum: strings.TrimSpace(activeDeployment.Checksum), + EntryFile: entryFile, + SPAFallbackEnabled: project.SPAFallbackEnabled, + SPAFallbackPath: fallbackPath, + APIProxyEnabled: project.APIProxyEnabled, + APIProxyPath: strings.TrimSpace(project.APIProxyPath), + APIProxyPass: strings.TrimSpace(project.APIProxyPass), + APIProxyRewrite: strings.TrimSpace(project.APIProxyRewrite), + LocalRoot: fmt.Sprintf( + "%s/deployments/%d/current", + openrestyrender.PagesDirPlaceholder, + activeDeployment.ID, + ), + } +} diff --git a/internal/apps/openflare/config_version/pages_snapshot_test.go b/internal/apps/openflare/config_version/pages_snapshot_test.go new file mode 100644 index 00000000..2d60e66f --- /dev/null +++ b/internal/apps/openflare/config_version/pages_snapshot_test.go @@ -0,0 +1,91 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + +package config_version + +import ( + "context" + "encoding/json" + "testing" + + "github.com/Rain-kl/Wavelet/internal/db" + "github.com/Rain-kl/Wavelet/internal/model" + openrestyrender "github.com/Rain-kl/Wavelet/pkg/render/openresty" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gorm.io/gorm" +) + +func TestBuildSnapshotRoutesPages(t *testing.T) { + cleanup := setupConfigVersionTestDB(t) + defer cleanup() + ctx := context.Background() + conn := requireDB(t, ctx) + + project := &model.PagesProject{ + Name: "Speed Test", + Slug: "speedtest", + Enabled: true, + SPAFallbackEnabled: true, + SPAFallbackPath: "/index.html", + EntryFile: "index.html", + } + require.NoError(t, conn.Create(project).Error) + + deployment := &model.PagesDeployment{ + ProjectID: project.ID, + DeploymentNumber: 1, + Checksum: "abc123checksum", + Status: model.PagesDeploymentStatusActive, + FileCount: 1, + TotalSize: 12, + } + require.NoError(t, conn.Create(deployment).Error) + require.NoError(t, conn.Model(project).Update("active_deployment_id", deployment.ID).Error) + + route := &model.ProxyRoute{ + SiteName: "speedtest", + Domain: "speedtest.arctel.net", + Domains: `["speedtest.arctel.net"]`, + OriginURL: "openflare-pages://project/1", + Upstreams: `["openflare-pages://project/1"]`, + Enabled: true, + UpstreamType: "pages", + PagesProjectID: &project.ID, + } + require.NoError(t, model.CreateProxyRouteRecord(ctx, route)) + + bundle, err := buildCurrentConfigBundle(ctx, true) + require.NoError(t, err) + require.Len(t, bundle.SnapshotRoutes, 1) + + snapshotRoute := bundle.SnapshotRoutes[0] + assert.Equal(t, "pages", snapshotRoute.UpstreamType) + assert.Equal(t, "openflare-pages://project/1", snapshotRoute.OriginURL) + require.NotNil(t, snapshotRoute.PagesDeployment) + assert.Equal(t, deployment.ID, snapshotRoute.PagesDeployment.DeploymentID) + assert.Equal(t, deployment.Checksum, snapshotRoute.PagesDeployment.Checksum) + assert.Equal(t, "__OPENFLARE_PAGES_DIR__/deployments/1/current", snapshotRoute.PagesDeployment.LocalRoot) + + _, err = renderSnapshotConfig(bundle.SnapshotJSON, nil) + require.NoError(t, err) + + var decoded struct { + Routes []struct { + PagesDeployment *openrestyrender.PagesDeployment `json:"pages_deployment"` + } `json:"routes"` + } + require.NoError(t, json.Unmarshal([]byte(bundle.SnapshotJSON), &decoded)) + require.NotNil(t, decoded.Routes[0].PagesDeployment) +} + +func requireDB(t *testing.T, ctx context.Context) *gorm.DB { + t.Helper() + conn := db.DB(ctx) + require.NotNil(t, conn) + require.NoError(t, conn.AutoMigrate( + &model.PagesProject{}, + &model.PagesDeployment{}, + )) + return conn +} diff --git a/internal/apps/openflare/config_version/snapshot.go b/internal/apps/openflare/config_version/snapshot.go index 3e0cd560..f27887af 100644 --- a/internal/apps/openflare/config_version/snapshot.go +++ b/internal/apps/openflare/config_version/snapshot.go @@ -20,35 +20,36 @@ import ( const supportFilesPerCertificate = 2 type snapshotRoute struct { - ID uint `json:"id,omitempty"` - SiteName string `json:"site_name,omitempty"` - Domain string `json:"domain"` - Domains []string `json:"domains,omitempty"` - OriginURL string `json:"origin_url"` - OriginHost string `json:"origin_host,omitempty"` - Upstreams []string `json:"upstreams,omitempty"` - Enabled bool `json:"enabled"` - EnableHTTPS bool `json:"enable_https"` - CertID *uint `json:"cert_id,omitempty"` - CertIDs []uint `json:"cert_ids,omitempty"` - DomainCertIDs []uint `json:"domain_cert_ids,omitempty"` - RedirectHTTP bool `json:"redirect_http"` - LimitConnPerServer int `json:"limit_conn_per_server,omitempty"` - LimitConnPerIP int `json:"limit_conn_per_ip,omitempty"` - LimitRate string `json:"limit_rate,omitempty"` - CacheEnabled bool `json:"cache_enabled"` - CachePolicy string `json:"cache_policy,omitempty"` - CacheRules []string `json:"cache_rules,omitempty"` - CustomHeaders []customHeaderInput `json:"custom_headers,omitempty"` - BasicAuthEnabled bool `json:"basic_auth_enabled,omitempty"` - BasicAuthUsername string `json:"basic_auth_username,omitempty"` - BasicAuthPassword string `json:"basic_auth_password,omitempty"` - Remark string `json:"remark,omitempty"` - UpstreamType string `json:"upstream_type,omitempty"` - TunnelNodeID *uint `json:"tunnel_node_id,omitempty"` - TunnelTargetAddr string `json:"tunnel_target_addr,omitempty"` - TunnelTargetProto string `json:"tunnel_target_protocol,omitempty"` - PagesProjectID *uint `json:"pages_project_id,omitempty"` + ID uint `json:"id,omitempty"` + SiteName string `json:"site_name,omitempty"` + Domain string `json:"domain"` + Domains []string `json:"domains,omitempty"` + OriginURL string `json:"origin_url"` + OriginHost string `json:"origin_host,omitempty"` + Upstreams []string `json:"upstreams,omitempty"` + Enabled bool `json:"enabled"` + EnableHTTPS bool `json:"enable_https"` + CertID *uint `json:"cert_id,omitempty"` + CertIDs []uint `json:"cert_ids,omitempty"` + DomainCertIDs []uint `json:"domain_cert_ids,omitempty"` + RedirectHTTP bool `json:"redirect_http"` + LimitConnPerServer int `json:"limit_conn_per_server,omitempty"` + LimitConnPerIP int `json:"limit_conn_per_ip,omitempty"` + LimitRate string `json:"limit_rate,omitempty"` + CacheEnabled bool `json:"cache_enabled"` + CachePolicy string `json:"cache_policy,omitempty"` + CacheRules []string `json:"cache_rules,omitempty"` + CustomHeaders []customHeaderInput `json:"custom_headers,omitempty"` + BasicAuthEnabled bool `json:"basic_auth_enabled,omitempty"` + BasicAuthUsername string `json:"basic_auth_username,omitempty"` + BasicAuthPassword string `json:"basic_auth_password,omitempty"` + Remark string `json:"remark,omitempty"` + UpstreamType string `json:"upstream_type,omitempty"` + TunnelNodeID *uint `json:"tunnel_node_id,omitempty"` + TunnelTargetAddr string `json:"tunnel_target_addr,omitempty"` + TunnelTargetProto string `json:"tunnel_target_protocol,omitempty"` + PagesProjectID *uint `json:"pages_project_id,omitempty"` + PagesDeployment *openrestyrender.PagesDeployment `json:"pages_deployment,omitempty"` } type snapshotWAFRuleGroup struct { @@ -230,6 +231,7 @@ func buildSnapshotRoutes(ctx context.Context, routes []*model.ProxyRoute) ([]sna var tunnelTargetAddr string var tunnelTargetProtocol string var pagesProjectID *uint + var pagesDeployment *openrestyrender.PagesDeployment switch upstreamType { case "tunnel": originURL = resolveTunnelOpenRestyUpstreamURL(ctx) @@ -238,7 +240,10 @@ func buildSnapshotRoutes(ctx context.Context, routes []*model.ProxyRoute) ([]sna tunnelTargetAddr = strings.TrimSpace(route.TunnelTargetAddr) tunnelTargetProtocol = normalizeTunnelTargetProtocol(route.TunnelTargetProtocol) case "pages": - return nil, fmt.Errorf("路由 %s Pages 配置无效: pages module is not available", route.Domain) + originURL, upstreams, pagesProjectID, pagesDeployment, err = buildPagesRouteSnapshot(ctx, route) + if err != nil { + return nil, err + } } cacheRules, err := decodeStoredCacheRules(route.CacheRules) if err != nil { @@ -274,6 +279,7 @@ func buildSnapshotRoutes(ctx context.Context, routes []*model.ProxyRoute) ([]sna TunnelTargetAddr: tunnelTargetAddr, TunnelTargetProto: tunnelTargetProtocol, PagesProjectID: pagesProjectID, + PagesDeployment: pagesDeployment, }) } return items, nil diff --git a/internal/model/openflare_option.go b/internal/model/openflare_option.go index 623769a2..c91c2097 100644 --- a/internal/model/openflare_option.go +++ b/internal/model/openflare_option.go @@ -45,14 +45,8 @@ var ( GitHubOAuthEnabled = false WeChatAuthEnabled = false GitHubClientID = "" - GitHubClientSecret = "" - WeChatServerAddress = "" - WeChatServerToken = "" WeChatAccountQRCodeImageURL = "" - SMTPServer = "" SMTPPort = 587 - SMTPAccount = "" - SMTPToken = "" AgentDiscoveryToken = "" AgentHeartbeatInterval = 10000 AgentWebsocketUpgradeEnabled = true