diff --git a/web/src/components/auth/auth-session-hydrator.tsx b/web/src/components/auth/auth-session-hydrator.tsx
index b6ec439aa..d9f43bb15 100644
--- a/web/src/components/auth/auth-session-hydrator.tsx
+++ b/web/src/components/auth/auth-session-hydrator.tsx
@@ -25,7 +25,7 @@ export function AuthSessionHydrator({ children }: { children: ReactNode }) {
durationMs: performance.now() - startedAt,
});
if (!payload.user) {
- applyAnonymousSession(payload);
+ await applyAnonymousSession(payload);
recordDiagnosticEvent({
category: "navigation",
level: "info",
@@ -48,9 +48,9 @@ export function AuthSessionHydrator({ children }: { children: ReactNode }) {
});
preloadWorkspaceRoute(window.location.pathname);
})
- .catch(() => {
+ .catch(async () => {
if (!cancelled) {
- applyAnonymousSession({ user: null });
+ await applyAnonymousSession({ user: null });
recordDiagnosticEvent({
category: "navigation",
level: "warning",
@@ -68,11 +68,7 @@ export function AuthSessionHydrator({ children }: { children: ReactNode }) {
return hydrated ? children : ;
}
-function applyAnonymousSession(payload: AuthSessionPayload) {
- const store = useUserStore.getState();
- store.clearSession();
- store.setRuntimeLimits(payload.runtimeLimits);
- store.setDrawingEngine(payload.drawingEngine);
- store.setFeatures(payload.features);
- store.setHydrated(true);
+async function applyAnonymousSession(payload: AuthSessionPayload) {
+ const { applyUserSession } = await import("@/lib/user-session");
+ await applyUserSession({ ...payload, user: null });
}
diff --git a/web/src/components/auth/require-auth.tsx b/web/src/components/auth/require-auth.tsx
index ee69e209c..75af4a617 100644
--- a/web/src/components/auth/require-auth.tsx
+++ b/web/src/components/auth/require-auth.tsx
@@ -4,12 +4,12 @@ import { Navigate, useLocation } from "react-router";
import { FullScreenLoader } from "@/components/ui/aceternity/full-screen-loader";
import { useUserStore } from "@/stores/use-user-store";
-export function RequireAuth({ children }: { children: ReactNode }) {
+export function RequireAuth({ children, allowAnonymous = false }: { children: ReactNode; allowAnonymous?: boolean }) {
const location = useLocation();
const hydrated = useUserStore((state) => state.hydrated);
const user = useUserStore((state) => state.user);
if (!hydrated) return ;
- if (!user) return ;
+ if (!user && !allowAnonymous) return ;
return children;
}
diff --git a/web/src/components/layout/workspace-account-menu.tsx b/web/src/components/layout/workspace-account-menu.tsx
index 6a92c3b86..d460dd4f9 100644
--- a/web/src/components/layout/workspace-account-menu.tsx
+++ b/web/src/components/layout/workspace-account-menu.tsx
@@ -1,8 +1,7 @@
import { Popover } from "antd";
import { Switch } from "@/components/ui/base/switch";
-import { LogIn, Moon, Sun } from "lucide-react";
+import { CircleUserRound, Moon, Sun } from "lucide-react";
import { useState } from "react";
-import { Link } from "react-router";
import { AppChangelogButton } from "@/components/layout/app-changelog-modal";
import { WorkspaceAccountCard } from "./workspace-account-card";
@@ -51,9 +50,5 @@ export function WorkspaceAccountMenu() {
>
- ) : (
-
-
-
- );
+ ) : 游客;
}
diff --git a/web/src/components/layout/workspace-sidebar-nav.tsx b/web/src/components/layout/workspace-sidebar-nav.tsx
index 8c72668bb..ad69892b4 100644
--- a/web/src/components/layout/workspace-sidebar-nav.tsx
+++ b/web/src/components/layout/workspace-sidebar-nav.tsx
@@ -70,7 +70,7 @@ function WorkspaceSidebarProfile({ collapsed, user }: { collapsed: boolean; user
useEffect(() => setFailed(false), [avatarUrl]);
if (!user) {
- return 登录;
+ return
{!collapsed ? 游客模式 : null}
;
}
const avatar = avatarUrl && !failed ?
setFailed(true)} /> : ;
diff --git a/web/src/lib/guest-workspace.ts b/web/src/lib/guest-workspace.ts
new file mode 100644
index 000000000..593554ab7
--- /dev/null
+++ b/web/src/lib/guest-workspace.ts
@@ -0,0 +1,3 @@
+export function isGuestWorkspacePath(pathname: string) {
+ return pathname === "/" || pathname === "/create";
+}
diff --git a/web/src/pages/admin/settings/components/welcome-setting.tsx b/web/src/pages/admin/settings/components/welcome-setting.tsx
index 4e35095b7..a33e68e28 100644
--- a/web/src/pages/admin/settings/components/welcome-setting.tsx
+++ b/web/src/pages/admin/settings/components/welcome-setting.tsx
@@ -48,7 +48,7 @@ export function WelcomeSetting() {
启用欢迎页
-
切换后立即保存。关闭后访问 /welcome 将跳转到首页。
+
切换后立即保存。关闭后访问 /welcome 将进入创作工作台;游客可浏览,生成时需登录。
{error ? (
{error}
diff --git a/web/src/pages/create/creation-login-dialog.tsx b/web/src/pages/create/creation-login-dialog.tsx
new file mode 100644
index 000000000..cfaff9e90
--- /dev/null
+++ b/web/src/pages/create/creation-login-dialog.tsx
@@ -0,0 +1,73 @@
+import { useEffect, useState, type FormEvent } from "react";
+import { App, Button, Input } from "antd";
+import { ArrowRight, LockKeyhole, UserRound } from "lucide-react";
+import { Link } from "react-router";
+
+import { AppModal } from "@/components/ui/product/app-modal";
+import { getAuthSession, login } from "@/services/api/auth";
+
+export function CreationLoginDialog({ open, onClose }: { open: boolean; onClose: () => void }) {
+ const { message } = App.useApp();
+ const [username, setUsername] = useState("");
+ const [password, setPassword] = useState("");
+ const [submitting, setSubmitting] = useState(false);
+
+ useEffect(() => {
+ if (!open) {
+ setUsername("");
+ setPassword("");
+ setSubmitting(false);
+ }
+ }, [open]);
+
+ const submit = async (event: FormEvent
) => {
+ event.preventDefault();
+ if (submitting) return;
+ setSubmitting(true);
+ try {
+ await login({ username: username.trim(), password });
+ const { applyUserSession } = await import("@/lib/user-session");
+ await applyUserSession(await getAuthSession());
+ message.success("登录成功,请再次点击生成");
+ onClose();
+ } catch (error) {
+ message.error(error instanceof Error ? error.message : "登录失败");
+ } finally {
+ setSubmitting(false);
+ }
+ };
+
+ return (
+
+
+
你的创作内容会保留。登录成功后,请再次点击生成。
+
+
还没有账号? 注册
+
+
+ );
+}
diff --git a/web/src/pages/create/index.tsx b/web/src/pages/create/index.tsx
index 057edf59a..8d33e3ae3 100644
--- a/web/src/pages/create/index.tsx
+++ b/web/src/pages/create/index.tsx
@@ -35,6 +35,7 @@ import { CreationComposer, CreationEmptySuggest, CreationFeaturedWorks, Creation
import { CreationAgentEntry } from "./creation-agent-entry";
import { createCreationSubmitGate } from "./creation-submit-gate";
import { creationVideoConfig } from "./creation-generation-config";
+import { CreationLoginDialog } from "./creation-login-dialog";
const AssetLibraryPickerModal = lazy(() => import("@/components/assets/asset-library-picker-modal").then((module) => ({ default: module.AssetLibraryPickerModal })));
const loadCreationRuntime = () => import("./creation-runtime");
@@ -97,6 +98,9 @@ export default function CreatePage() {
const [activeId, setActiveId] = useState("");
const activeIdRef = useRef("");
const [hydrated, setHydrated] = useState(false);
+ const userId = useUserStore((state) => state.user?.id || null);
+ const userSessionHydrated = useUserStore((state) => state.hydrated);
+ const [conversationScope, setConversationScope] = useState(null);
const [mode, setMode] = useState(() => initialComposerPreferences.mode || defaultCreationMode);
const [prompt, setPrompt] = useState("");
const [attachments, setAttachments] = useState([]);
@@ -115,6 +119,7 @@ export default function CreatePage() {
const [busy, setBusy] = useState(false);
const [referenceReplacementBusy, setReferenceReplacementBusy] = useState(false);
const [historyOpen, setHistoryOpen] = useState(false);
+ const [loginDialogOpen, setLoginDialogOpen] = useState(false);
const [libraryOpen, setLibraryOpen] = useState(false);
const externalAssetSources = useExternalAssetSources(libraryOpen);
const abortRef = useRef(null);
@@ -256,20 +261,25 @@ export default function CreatePage() {
}, [attachments, maxReferences, mentionReferences, mode, videoReferenceLimits]);
useEffect(() => {
+ if (!userSessionHydrated) return;
let cancelled = false;
+ const scope = getActiveUserScope();
+ setHydrated(false);
+ setConversationScope(null);
void loadCreationConversations().then((stored) => {
- if (cancelled) return;
+ if (cancelled || getActiveUserScope() !== scope || (useUserStore.getState().user?.id || null) !== userId) return;
const next = stored?.length ? stored : [newConversation()];
conversationsRef.current = next;
setConversations(next);
setActiveId(next[0].id);
+ setConversationScope(scope);
setHydrated(true);
});
return () => {
cancelled = true;
// 页面卸载只停止当前页面的状态更新,后台任务由任务中心继续执行,返回页面后再恢复状态。
};
- }, []);
+ }, [userId, userSessionHydrated]);
useEffect(() => () => abortRef.current?.abort(), []);
@@ -279,8 +289,8 @@ export default function CreatePage() {
useEffect(() => {
conversationsRef.current = conversations;
- if (hydrated) void saveCreationConversations(conversations);
- }, [conversations, hydrated]);
+ if (hydrated && userSessionHydrated && conversationScope === getActiveUserScope()) void saveCreationConversations(conversations);
+ }, [conversations, conversationScope, hydrated, userSessionHydrated]);
useEffect(() => {
if (!hydrated || !recoveryTaskKey || !pendingTaskIds.length) return;
@@ -570,6 +580,12 @@ export default function CreatePage() {
releaseSubmitGate();
return;
}
+ if (!useUserStore.getState().user) {
+ setLoginDialogOpen(true);
+ releaseRetryLock();
+ releaseSubmitGate();
+ return;
+ }
if (!selectedModel) {
toast.warning(`请先在设置中配置${modeLabels[mode]}模型`);
releaseRetryLock();
@@ -1066,6 +1082,7 @@ export default function CreatePage() {
}
setHistoryOpen(false)} onSelect={selectConversation} onDelete={confirmDeleteConversation} onRename={renameConversationTitle} />
+ setLoginDialogOpen(false)} />
{libraryOpen ? import("@/pages/admin"));
const AnalyticsPage = lazy(() => import("@/pages/admin/admin-route-pages").then((module) => ({ default: module.AnalyticsPage })));
@@ -68,7 +69,7 @@ function AuthenticatedWorkspaceLayout() {
const { pathname } = useLocation();
const isCanvasProjectRoute = pathname.startsWith("/canvas/");
const fallback = isCanvasProjectRoute ? : ;
- return ;
+ return ;
}
/**
@@ -104,8 +105,8 @@ export const router = createBrowserRouter([
element: ,
errorElement: ,
children: [
- { path: "/", element: {deferred()} },
- { path: "/create", element: {deferred()} },
+ { path: "/", element: deferred() },
+ { path: "/create", element: deferred() },
{
path: "/tasks",
element: (
diff --git a/web/test/guest-workspace-access.test.ts b/web/test/guest-workspace-access.test.ts
new file mode 100644
index 000000000..1bbe3789d
--- /dev/null
+++ b/web/test/guest-workspace-access.test.ts
@@ -0,0 +1,13 @@
+import { describe, expect, test } from "bun:test";
+
+import { isGuestWorkspacePath } from "../src/lib/guest-workspace";
+
+describe("guest workspace route access", () => {
+ test("allows only the public creation workspace routes", () => {
+ expect(isGuestWorkspacePath("/")).toBe(true);
+ expect(isGuestWorkspacePath("/create")).toBe(true);
+ expect(isGuestWorkspacePath("/assets")).toBe(false);
+ expect(isGuestWorkspacePath("/admin")).toBe(false);
+ expect(isGuestWorkspacePath("/create/anything")).toBe(false);
+ });
+});