暂时无法打开欢迎页,请稍后重试。
+ + 返回首页 +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 ?暂时无法打开欢迎页,请稍后重试。
+ + 返回首页 +