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, ""))} /> +
(null); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(""); + const [revision, setRevision] = useState(0); + const setFeatures = useUserStore((state) => state.setFeatures); + + useEffect(() => { + let active = true; + setError(""); + getAdminFeatureAvailability().then(({ features }) => { + if (typeof features.welcomeEnabled !== "boolean") throw new Error("欢迎页配置无效"); + if (active) setEnabled(features.welcomeEnabled); + }).catch(() => { + if (active) setError("读取欢迎页状态失败,请重试。"); + }); + return () => { active = false; }; + }, [revision]); + + async function changeWelcome(value: boolean) { + if (saving) return; + setSaving(true); + try { + const { features } = await updateAdminFeatureAvailability({ welcomeEnabled: value }); + if (features.welcomeEnabled !== value) throw new Error("欢迎页状态未保存,请重试"); + setEnabled(features.welcomeEnabled); + setFeatures(features); + message.success(value ? "欢迎页已启用" : "欢迎页已关闭"); + } catch (error) { + message.error(error instanceof Error ? error.message : "保存欢迎页状态失败"); + } finally { + setSaving(false); + } + } + + return ( +
+
+ 启用欢迎页 +

切换后立即保存。关闭后访问 /welcome 将跳转到首页。

+ {error ?
{error}
: null} +
+
+ {enabled === null ? "读取中" : saving ? "保存中" : enabled ? "已启用" : "已关闭"} + void changeWelcome(value)} /> +
+
+ ); +} diff --git a/web/src/services/api/auth.ts b/web/src/services/api/auth.ts index 72913f531..2464811e7 100644 --- a/web/src/services/api/auth.ts +++ b/web/src/services/api/auth.ts @@ -396,7 +396,7 @@ export function getAdminFeatureAvailability() { return http.get<{ features: FeatureAvailability }>("/admin/settings/features"); } -export function updateAdminFeatureAvailability(features: Pick) { +export function updateAdminFeatureAvailability(features: Partial>) { return http.patch<{ features: FeatureAvailability }>("/admin/settings/features", features); } diff --git a/web/src/services/api/welcome.ts b/web/src/services/api/welcome.ts new file mode 100644 index 000000000..c34d3d183 --- /dev/null +++ b/web/src/services/api/welcome.ts @@ -0,0 +1,5 @@ +import { http } from "./request"; + +export function getWelcomeAvailability() { + return http.get<{ welcomeEnabled: boolean }>("/public/welcome"); +} diff --git a/web/src/stores/use-user-store.ts b/web/src/stores/use-user-store.ts index 882032630..892919e2f 100644 --- a/web/src/stores/use-user-store.ts +++ b/web/src/stores/use-user-store.ts @@ -25,6 +25,7 @@ export type RuntimeLimits = { }; export type FeatureAvailability = { + welcomeEnabled: boolean; shortDramaEnabled: boolean; taskCenterEnabled: boolean; creditsEnabled: boolean; @@ -38,6 +39,7 @@ export type FeatureAvailability = { }; export const defaultFeatureAvailability: FeatureAvailability = { + welcomeEnabled: true, shortDramaEnabled: true, taskCenterEnabled: true, creditsEnabled: true, diff --git a/web/src/welcome-application.tsx b/web/src/welcome-application.tsx index 18d0e91d4..454a7571f 100644 --- a/web/src/welcome-application.tsx +++ b/web/src/welcome-application.tsx @@ -1,5 +1,26 @@ import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; +import { getWelcomeAvailability } from "@/services/api/welcome"; import WelcomePage from "@/pages/welcome"; -createRoot(document.getElementById("root")!).render(); +async function renderWelcome() { + try { + const { welcomeEnabled } = await getWelcomeAvailability(); + if (welcomeEnabled !== true) { + window.location.replace("/"); + return; + } + createRoot(document.getElementById("root")!).render(); + } catch (error) { + console.error("Welcome page initialization failed", error); + createRoot(document.getElementById("root")!).render( +
+

暂时无法打开欢迎页,请稍后重试。

+ + 返回首页 +
, + ); + } +} + +void renderWelcome(); diff --git a/web/vite.config.ts b/web/vite.config.ts index e176694de..3824d4657 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -45,6 +45,14 @@ export default defineConfig({ includeDependenciesRecursively: false, minSize: 40 * 1024, groups: [ + { + // Shared interop helpers must not be emitted into a route entry: + // AntD would import that entry back and execute its bootstrap early. + name: "vendor-babel-runtime", + minSize: 0, + test: /node_modules[\\/]@babel[\\/]runtime[\\/]/, + priority: 40, + }, { name: "vendor-react", test: /node_modules[\\/](?:react(?:-dom|-router|-router-dom)?|scheduler|zustand|use-sync-external-store|@tanstack[\\/](?:query-core|react-query))[\\/]/,