暂时无法打开欢迎页,请稍后重试。
+ + 返回首页 +From 88c24c120f1cafb12adc83c78e2907cfb8aca941 Mon Sep 17 00:00:00 2001
From: MasterChiefCN <49179444+MasterChiefCN@users.noreply.github.com>
Date: Tue, 15 Sep 2026 11:10:12 +0800
Subject: [PATCH] =?UTF-8?q?feat(welcome):=20=E7=AB=99=E7=82=B9=E5=A4=96?=
=?UTF-8?q?=E8=A7=82=20-=20=E5=A2=9E=E5=8A=A0=E6=AC=A2=E8=BF=8E=E9=A1=B5?=
=?UTF-8?q?=E5=BC=80=E6=94=BE=E5=BC=80=E5=85=B3=E5=B9=B6=E4=BF=AE=E5=A4=8D?=
=?UTF-8?q?=E5=85=A5=E5=8F=A3=E5=88=86=E5=8C=85?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../internal/app/feature_availability_test.go | 26 +++++++++
.../internal/handler/feature_availability.go | 10 ++++
backend/internal/handler/openapi.yaml | 10 ++++
.../internal/handler/welcome_routes_test.go | 56 +++++++++++++++++++
.../internal/platform/feature_availability.go | 2 +
docs/content/docs/backend/http-api.mdx | 6 ++
docs/content/docs/progress/pending-test.mdx | 8 +++
web/src/main.tsx | 2 +-
.../components/feature-availability-panel.tsx | 1 +
.../settings/appearance-settings-page.tsx | 2 +
.../settings/components/welcome-setting.tsx | 56 +++++++++++++++++++
web/src/services/api/auth.ts | 2 +-
web/src/services/api/welcome.ts | 5 ++
web/src/stores/use-user-store.ts | 2 +
web/src/welcome-application.tsx | 23 +++++++-
web/vite.config.ts | 8 +++
16 files changed, 216 insertions(+), 3 deletions(-)
create mode 100644 backend/internal/handler/welcome_routes_test.go
create mode 100644 web/src/pages/admin/settings/components/welcome-setting.tsx
create mode 100644 web/src/services/api/welcome.ts
diff --git a/backend/internal/app/feature_availability_test.go b/backend/internal/app/feature_availability_test.go
index 914aaa715..41ed91bca 100644
--- a/backend/internal/app/feature_availability_test.go
+++ b/backend/internal/app/feature_availability_test.go
@@ -5,6 +5,7 @@ import (
"testing"
"infinite-canvas/backend/internal/model"
+ "infinite-canvas/backend/internal/platform"
"infinite-canvas/backend/internal/protocol"
"infinite-canvas/backend/internal/repository"
@@ -19,6 +20,9 @@ func TestFeatureAvailabilityDefaultsToDisableFrontendModels(t *testing.T) {
if err != nil {
t.Fatal(err)
}
+ if !setting.WelcomeEnabled {
+ t.Fatal("welcome should be enabled by default")
+ }
if setting.Configured || !setting.ShortDramaEnabled || !setting.TaskCenterEnabled || !setting.CreditsEnabled || !setting.CustomChannelsEnabled || setting.FrontendModelsEnabled || !setting.PluginCenterEnabled || !setting.SystemPluginsVisibleToUsers {
t.Fatalf("FeatureAvailability() = %#v", setting)
}
@@ -38,6 +42,28 @@ func TestFeatureAvailabilityLegacySettingKeepsCustomChannelsEnabled(t *testing.T
if setting.ShortDramaEnabled || !setting.CustomChannelsEnabled || setting.FrontendModelsEnabled || !setting.PluginCenterEnabled || !setting.SystemPluginsVisibleToUsers {
t.Fatalf("FeatureAvailability() = %#v", setting)
}
+ if !setting.WelcomeEnabled {
+ t.Fatal("existing settings without welcomeEnabled should keep welcome enabled")
+ }
+}
+
+func TestWelcomeAvailabilityCanBeDisabledAndReenabled(t *testing.T) {
+ svc, _ := newFeatureAvailabilityTestService(t)
+ actor := &model.User{ID: "admin-welcome", Role: model.UserRoleAdmin}
+ for _, enabled := range []bool{false, true} {
+ value := platform.DefaultFeatureAvailability()
+ value.WelcomeEnabled = enabled
+ if _, err := svc.UpdateFeatureAvailability(actor, value); err != nil {
+ t.Fatal(err)
+ }
+ setting, err := svc.FeatureAvailability()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if setting.WelcomeEnabled != enabled {
+ t.Fatalf("welcomeEnabled = %v, want %v", setting.WelcomeEnabled, enabled)
+ }
+ }
}
func TestUpdateFeatureAvailabilityPersistsAndAudits(t *testing.T) {
diff --git a/backend/internal/handler/feature_availability.go b/backend/internal/handler/feature_availability.go
index 7a0229c3d..1b277b800 100644
--- a/backend/internal/handler/feature_availability.go
+++ b/backend/internal/handler/feature_availability.go
@@ -9,6 +9,16 @@ import (
)
func RegisterFeatureAvailabilityRoutes(r *gin.RouterGroup, svc *service.Service) {
+ r.GET("/public/welcome", func(c *gin.Context) {
+ c.Header("Cache-Control", "no-store")
+ setting, err := svc.FeatureAvailability()
+ if err != nil {
+ failService(c, err)
+ return
+ }
+ ok(c, gin.H{"welcomeEnabled": setting.WelcomeEnabled})
+ })
+
r.GET("/features", func(c *gin.Context) {
if _, err := currentUser(c, svc); err != nil {
failService(c, err)
diff --git a/backend/internal/handler/openapi.yaml b/backend/internal/handler/openapi.yaml
index 434d2ffe4..752ec8df8 100644
--- a/backend/internal/handler/openapi.yaml
+++ b/backend/internal/handler/openapi.yaml
@@ -24,6 +24,16 @@ tags:
- name: agent
- name: admin
paths:
+ /public/welcome:
+ get:
+ tags: [meta]
+ security: []
+ summary: 读取欢迎页开放状态
+ responses:
+ "200":
+ description: 信封 data 仅包含 welcomeEnabled 布尔值,默认 true;Cache-Control 为 no-store
+ "500":
+ description: 配置读取失败
/agent/capabilities:
get:
tags: [agent]
diff --git a/backend/internal/handler/welcome_routes_test.go b/backend/internal/handler/welcome_routes_test.go
new file mode 100644
index 000000000..a4c10622f
--- /dev/null
+++ b/backend/internal/handler/welcome_routes_test.go
@@ -0,0 +1,56 @@
+package handler
+
+import (
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "infinite-canvas/backend/internal/model"
+ "infinite-canvas/backend/internal/repository"
+ "infinite-canvas/backend/internal/service"
+
+ "github.com/gin-gonic/gin"
+ "gorm.io/driver/sqlite"
+ "gorm.io/gorm"
+)
+
+func TestWelcomeAvailabilityPublicRoute(t *testing.T) {
+ gin.SetMode(gin.TestMode)
+ db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
+ if err != nil {
+ t.Fatal(err)
+ }
+ sqlDB, err := db.DB()
+ if err != nil {
+ t.Fatal(err)
+ }
+ sqlDB.SetMaxOpenConns(1)
+ t.Cleanup(func() { _ = sqlDB.Close() })
+ if err := db.AutoMigrate(&model.SystemSetting{}); err != nil {
+ t.Fatal(err)
+ }
+ router := gin.New()
+ RegisterFeatureAvailabilityRoutes(router.Group("/api"), service.New(repository.New(db), t.TempDir()))
+ for _, enabled := range []bool{true, false, true} {
+ payload, _ := json.Marshal(map[string]bool{"welcomeEnabled": enabled})
+ if err := db.Save(&model.SystemSetting{Key: "feature_availability", ValueJSON: string(payload)}).Error; err != nil {
+ t.Fatal(err)
+ }
+ response := httptest.NewRecorder()
+ router.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/api/public/welcome", nil))
+ if response.Code != http.StatusOK || response.Header().Get("Cache-Control") != "no-store" {
+ t.Fatalf("unexpected public response: %d %s", response.Code, response.Body.String())
+ }
+ var body struct {
+ Code int `json:"code"`
+ Data map[string]bool `json:"data"`
+ }
+ if err := json.Unmarshal(response.Body.Bytes(), &body); err != nil {
+ t.Fatal(err)
+ }
+ if body.Code != 0 || len(body.Data) != 1 || body.Data["welcomeEnabled"] != enabled {
+ t.Fatalf("unexpected public payload: %s", response.Body.String())
+ }
+ }
+}
diff --git a/backend/internal/platform/feature_availability.go b/backend/internal/platform/feature_availability.go
index bb36bed44..9e13c87eb 100644
--- a/backend/internal/platform/feature_availability.go
+++ b/backend/internal/platform/feature_availability.go
@@ -26,6 +26,7 @@ const (
)
type FeatureAvailability struct {
+ WelcomeEnabled bool `json:"welcomeEnabled"`
ShortDramaEnabled bool `json:"shortDramaEnabled"`
TaskCenterEnabled bool `json:"taskCenterEnabled"`
CreditsEnabled bool `json:"creditsEnabled"`
@@ -46,6 +47,7 @@ type PublicFeatureAvailability struct {
func DefaultFeatureAvailability() FeatureAvailability {
// 缺少配置代表尚未由运维接管;前台模型需要明确配置后才开放。
return FeatureAvailability{
+ WelcomeEnabled: true,
ShortDramaEnabled: true,
TaskCenterEnabled: true,
CreditsEnabled: true,
diff --git a/docs/content/docs/backend/http-api.mdx b/docs/content/docs/backend/http-api.mdx
index 5c59f5159..eb7f2417a 100644
--- a/docs/content/docs/backend/http-api.mdx
+++ b/docs/content/docs/backend/http-api.mdx
@@ -9,6 +9,12 @@ description: 信封、错误码与 OpenAPI
机器可读规范:运行中的 `GET /api/openapi.yaml`,源文件 `backend/internal/handler/openapi.yaml`。前端 TypeScript 仍走 `web/src/services/api` 的 `http`,不从 OpenAPI 生成客户端。
+## 欢迎页开放配置
+
+`GET /public/welcome` 无需登录,信封 `data` 仅包含布尔值 `welcomeEnabled`,响应禁用缓存。读取失败按标准错误信封返回。
+
+管理员通过 `GET/PATCH /admin/settings/features` 读取和保存 `welcomeEnabled`,默认 `true`,与其他功能开关一起存入 `system_settings` 的 `feature_availability` JSON,并记录管理员审计。关闭后新访问 `/welcome` 的浏览器跳转到 `/`。
+
## 云端 Agent
所有 `/agent` 路径都需要登录 Cookie。`GET /agent/capabilities` 返回当前版本(2)、权限模式、上下文范围、技能和工具清单。Agent 运行由服务端任务 Worker 驱动,浏览器断开不会停止运行。
diff --git a/docs/content/docs/progress/pending-test.mdx b/docs/content/docs/progress/pending-test.mdx
index 5eea03be6..971d3ee7e 100644
--- a/docs/content/docs/progress/pending-test.mdx
+++ b/docs/content/docs/progress/pending-test.mdx
@@ -5,6 +5,14 @@ description: 当前项目已实现功能的手工验证清单与未确认边界
# 待测试
+## 欢迎页开放开关
+
+- 管理员在 `/admin/settings/appearance` 的英文品牌标识下方切换欢迎页,立即保存,沿用功能配置保存及审计流程,默认开启;功能开放页不再编辑该字段。
+- 欢迎页入口直接导入展示组件,Babel runtime 辅助代码固定独立分包(不按最小体积合并回路由),避免 AntD 反向加载工作台入口触发循环初始化错误。
+- 已通过前端类型检查和生产构建,并用构建产物在浏览器验证启用展示、关闭跳转、接口失败重试入口;外观页开关位置及真实管理员保存待部署验收。
+- 待浏览器验收:关闭后访客及已登录用户访问 `/welcome`、`/welcome/` 均跳转首页;重新开启后恢复展示;配置请求失败时显示重试入口,不展示欢迎页内容。
+- 开关在进入页面时检查,已经打开的欢迎页不主动退出;静态图片、视频资源不受此页面开关限制。
+
## 混合节点画布 Agent
- 未注册 Agent 能力的节点(包括插件、配置、绘图节点)不再阻断整张画布的 Agent 摘要与分页读取;返回基础信息和 `agentSupported: false`,不读取任意 metadata,也不授予创建、修改或媒体引用能力。
diff --git a/web/src/main.tsx b/web/src/main.tsx
index cd35b11a7..221db2319 100644
--- a/web/src/main.tsx
+++ b/web/src/main.tsx
@@ -2,6 +2,6 @@ import "@fontsource-variable/inter";
import "@fontsource-variable/jetbrains-mono";
import { bootstrapAppearance } from "@/services/appearance-bootstrap";
-// Keep the public film page independent of workspace and appearance requests.
+// The public film entry checks its availability independently of workspace bootstrap.
if (/^\/welcome\/?$/.test(window.location.pathname)) void import("./welcome-application");
else void bootstrapAppearance().finally(() => import("./application"));
diff --git a/web/src/pages/admin/components/feature-availability-panel.tsx b/web/src/pages/admin/components/feature-availability-panel.tsx
index 21db1e997..341d13614 100644
--- a/web/src/pages/admin/components/feature-availability-panel.tsx
+++ b/web/src/pages/admin/components/feature-availability-panel.tsx
@@ -368,6 +368,7 @@ function parseFeatureAvailability(value: unknown): FeatureAvailability {
if (typeof record[key] !== "boolean") throw new Error("功能开放配置响应缺少有效开关状态");
}
return {
+ welcomeEnabled: record.welcomeEnabled as boolean,
shortDramaEnabled: record.shortDramaEnabled as boolean,
taskCenterEnabled: record.taskCenterEnabled as boolean,
creditsEnabled: record.creditsEnabled as boolean,
diff --git a/web/src/pages/admin/settings/appearance-settings-page.tsx b/web/src/pages/admin/settings/appearance-settings-page.tsx
index 219aee771..caee726b5 100644
--- a/web/src/pages/admin/settings/appearance-settings-page.tsx
+++ b/web/src/pages/admin/settings/appearance-settings-page.tsx
@@ -9,6 +9,7 @@ import { AdminStatusBadge, SettingsSectionCard } from "@/pages/admin/components/
import { cn } from "@/lib/utils";
import { cloneSkinDefinition, DEFAULT_CLASSIC_SKIN, duplicateSkinDefinition, normalizeSkinDefinition, type SkinDefinition } from "@/lib/skin-themes";
import { SkinThemeEditor } from "@/pages/admin/settings/components/skin-theme-editor";
+import { WelcomeSetting } from "@/pages/admin/settings/components/welcome-setting";
import { deleteAdminResources } from "@/services/api/admin-storage";
import { getAdminAppearance, resetAdminAppearance, updateAdminAppearance, uploadAppearanceAsset, type AdminAppearance, type AppearanceAssetSlot } from "@/services/api/appearance";
import { commitPublicAppearance, DEFAULT_PUBLIC_APPEARANCE } from "@/stores/use-appearance-store";
@@ -473,6 +474,7 @@ export default function AppearanceSettingsPage() {
onChange={(event) => setBrandSlug(event.target.value.toLocaleLowerCase().replace(/[^a-z0-9-]/g, ""))}
/>
+
切换后立即保存。关闭后访问 /welcome 将跳转到首页。
+ {error ?暂时无法打开欢迎页,请稍后重试。
+ + 返回首页 +