fix(*): 模型与数据同步 - 完善鉴权、供应商模型和本地持久化能力

This commit is contained in:
ddCat
2026-08-20 22:58:31 +08:00
parent 36666a53a2
commit 11ee5aafec
30 changed files with 688 additions and 203 deletions

View File

@@ -6,14 +6,11 @@ import { WorkspaceCommandPalette } from "@/components/layout/workspace-command-p
import { WorkspaceSidebarNav } from "@/components/layout/workspace-sidebar-nav";
import { WorkspaceTopBar } from "@/components/layout/workspace-top-bar";
import { cn } from "@/lib/utils";
import { refreshFeatureAvailability } from "@/lib/user-session";
import { isSpatialWorkbenchPath } from "@/lib/workspace-routes";
import { useUserStore } from "@/stores/use-user-store";
export function AppWorkspaceShell({ children }: { children: ReactNode }) {
const { pathname } = useLocation();
const navigate = useNavigate();
const user = useUserStore((state) => state.user);
const [mobileSidebarExpanded, setMobileSidebarExpanded] = useState(false);
const [desktopSidebarCollapsed, setDesktopSidebarCollapsed] = useState(false);
const [paletteOpen, setPaletteOpen] = useState(false);
@@ -60,17 +57,6 @@ export function AppWorkspaceShell({ children }: { children: ReactNode }) {
return () => window.removeEventListener("workspace:navigate", handleWorkspaceNavigation);
}, [navigate]);
useEffect(() => {
if (!user) return;
const refresh = () => void refreshFeatureAvailability().catch((error) => console.warn("功能开放状态刷新失败", error));
const timer = window.setInterval(refresh, 30_000);
window.addEventListener("focus", refresh);
return () => {
window.clearInterval(timer);
window.removeEventListener("focus", refresh);
};
}, [user]);
return (
<>
<div className={cn("app-workspace-shell flex h-dvh min-h-0 w-full flex-col overflow-hidden", spatialWorkbench && "is-spatial", creationWorkspace && "is-creation-workspace")}>

View File

@@ -8,12 +8,13 @@ import { useLocalDreaminaModelBootstrap } from "@/stores/use-local-dreamina-mode
import { useLocalRuntimeBootstrap } from "@/stores/use-local-runtime-store";
export function ClientRootInit({ children }: { children: ReactNode }) {
useLocalRuntimeBootstrap();
const config = useConfigStore((state) => state.config);
const localRuntimeConfigured = config.channels.some((channel) => channel.transport === "local-runtime" && channel.enabled !== false);
useLocalRuntimeBootstrap(localRuntimeConfigured);
useLocalDreaminaModelBootstrap();
const { message } = App.useApp();
const handledConfigParams = useRef(false);
const updateConfig = useConfigStore((state) => state.updateConfig);
const config = useConfigStore((state) => state.config);
useEffect(() => {
const interactiveSelector = 'button, [role="button"], a, [class*="card"], [class*="Card"]';

View File

@@ -1,13 +1,17 @@
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { Tag } from "antd";
import { Bell } from "lucide-react";
import { useCallback, useEffect, useRef, useState, type CSSProperties } from "react";
import { useState, type CSSProperties } from "react";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { AnnouncementTimelineModal } from "@/components/ui/aceternity/announcement-timeline-modal";
import { aceternityMotion } from "@/lib/aceternity-motion";
import { getAnnouncementFeed, markAnnouncementsRead, type SystemAnnouncement } from "@/services/api/announcements";
import { getAnnouncementFeed, markAnnouncementsRead } from "@/services/api/announcements";
const ANNOUNCEMENT_REFRESH_INTERVAL_MS = 60_000;
const ANNOUNCEMENT_REFRESH_INTERVAL_MS = 5 * 60_000;
const ANNOUNCEMENT_CACHE_TTL_MS = 60_000;
type AnnouncementFeed = Awaited<ReturnType<typeof getAnnouncementFeed>>;
type SystemAnnouncementCenterProps = {
userId: string;
@@ -20,63 +24,31 @@ type SystemAnnouncementCenterProps = {
export function SystemAnnouncementCenter({ userId, className, style, showLabel = false, labelClassName, staticMotion = false }: SystemAnnouncementCenterProps) {
const reducedMotion = useReducedMotion();
const activeUserIdRef = useRef(userId);
activeUserIdRef.current = userId;
const queryClient = useQueryClient();
const [open, setOpen] = useState(false);
const [announcements, setAnnouncements] = useState<SystemAnnouncement[]>([]);
const [unreadCount, setUnreadCount] = useState(0);
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const refresh = useCallback(async (showLoading = false) => {
if (!userId) return null;
const requestUserId = userId;
if (showLoading) setLoading(true);
try {
const feed = await getAnnouncementFeed();
if (activeUserIdRef.current !== requestUserId) return null;
setAnnouncements(feed.announcements || []);
setUnreadCount(Math.max(0, feed.unreadCount || 0));
setError("");
return feed;
} catch (requestError) {
if (activeUserIdRef.current !== requestUserId) return null;
setError(requestError instanceof Error ? requestError.message : "读取公告失败");
return null;
} finally {
if (showLoading && activeUserIdRef.current === requestUserId) setLoading(false);
}
}, [userId]);
useEffect(() => {
setAnnouncements([]);
setUnreadCount(0);
setError("");
if (!userId) return;
void refresh();
const timer = window.setInterval(() => void refresh(), ANNOUNCEMENT_REFRESH_INTERVAL_MS);
const onFocus = () => void refresh();
const onVisibilityChange = () => {
if (document.visibilityState === "visible") void refresh();
};
window.addEventListener("focus", onFocus);
document.addEventListener("visibilitychange", onVisibilityChange);
return () => {
window.clearInterval(timer);
window.removeEventListener("focus", onFocus);
document.removeEventListener("visibilitychange", onVisibilityChange);
};
}, [refresh, userId]);
const queryKey = ["system-announcements", userId] as const;
const feedQuery = useQuery({
queryKey,
queryFn: getAnnouncementFeed,
enabled: Boolean(userId),
staleTime: ANNOUNCEMENT_CACHE_TTL_MS,
refetchInterval: ANNOUNCEMENT_REFRESH_INTERVAL_MS,
// 公告在打开面板时会显式 refetch;不把浏览器 focus 变成每个工作区实例的请求触发器。
refetchOnWindowFocus: false,
});
const announcements = feedQuery.data?.announcements || [];
const unreadCount = Math.max(0, feedQuery.data?.unreadCount || 0);
const error = feedQuery.error instanceof Error ? feedQuery.error.message : feedQuery.error ? "读取公告失败" : "";
const openAnnouncements = async () => {
setOpen(true);
const feed = await refresh(announcements.length === 0);
const feed = (await feedQuery.refetch()).data;
if (!feed?.unreadCount) return;
try {
const result = await markAnnouncementsRead(feed.announcements.map((announcement) => announcement.id));
const nextUnreadCount = Math.max(0, result.unreadCount || 0);
setUnreadCount(nextUnreadCount);
if (nextUnreadCount > 0) void refresh();
queryClient.setQueryData<AnnouncementFeed>(queryKey, (current) => current ? { ...current, unreadCount: nextUnreadCount } : current);
if (nextUnreadCount > 0) void queryClient.invalidateQueries({ queryKey });
} catch {
// 已读状态是辅助读路径,失败时保留角标,下一次打开或轮询会继续尝试同步。
}
@@ -118,7 +90,7 @@ export function SystemAnnouncementCenter({ userId, className, style, showLabel =
</span>
) : null}
</motion.button>
<AnnouncementTimelineModal open={open} announcements={announcements} loading={loading} error={announcements.length ? "" : error} onClose={() => setOpen(false)} onRetry={() => void refresh(true)} />
<AnnouncementTimelineModal open={open} announcements={announcements} loading={feedQuery.isFetching} error={announcements.length ? "" : error} onClose={() => setOpen(false)} onRetry={() => void feedQuery.refetch()} />
</>
);
}