fix(platform): 存储与 Agent - 修复配额准入、Live2D 与用户筛选

This commit is contained in:
Rou
2026-09-30 18:28:19 +08:00
parent 53050a7a30
commit 87fa805caf
14 changed files with 211 additions and 39 deletions

View File

@@ -32,7 +32,9 @@ import { CanvasAgentImageApprovalSettings } from "./canvas-agent-image-approval-
import { markdownPlainText } from "@/lib/markdown-plain-text";
import type { ApprovalState } from "./canvas-cloud-agent-events";
export function AgentLauncher({ theme, statusColor, approvalPending, reducedMotion, onOpen }: { theme: CanvasTheme; statusColor: string; approvalPending: boolean; reducedMotion: boolean; onOpen: () => void }) {
// hidden 为 true 时入口保持挂载但不显示:Live2D 模型加载开销大,面板开合不能卸载它,
// 否则每次重新挂载都要重新下载模型并重建渲染上下文,期间只能显示默认形象。
export function AgentLauncher({ theme, statusColor, approvalPending, reducedMotion, hidden = false, onOpen }: { theme: CanvasTheme; statusColor: string; approvalPending: boolean; reducedMotion: boolean; hidden?: boolean; onOpen: () => void }) {
const appearance = useAppearanceStore((state) => state.appearance.canvas) || DEFAULT_CANVAS_APPEARANCE;
const live = appearance.avatarType === "live2d" && Boolean(appearance.live2dResourceId && appearance.live2dEntry);
const [viewport, setViewport] = useState(() => ({ width: window.innerWidth, height: window.innerHeight }));
@@ -50,7 +52,9 @@ export function AgentLauncher({ theme, statusColor, approvalPending, reducedMoti
aria-label={`打开${appearance.agentName}`}
title={`${approvalPending ? "Agent 等待你的审批" : "打开 Agent 助手"} · 拖动可调整位置,聚焦后可用方向键移动`}
className={cn("canvas-agent-launcher fixed z-[calc(var(--z-toast)+1)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-current/35", dragging && "is-dragging", live && "canvas-agent-launcher-live2d")}
style={{ ...position, width, height, color: theme.node.text, "--canvas-agent-launcher-shadow": theme.spatial.shadow } as CSSProperties}
style={{ ...position, width, height, color: theme.node.text, "--canvas-agent-launcher-shadow": theme.spatial.shadow, ...(hidden ? { display: "none" } : null) } as CSSProperties}
aria-hidden={hidden || undefined}
tabIndex={hidden ? -1 : undefined}
data-canvas-no-zoom
{...handlers}
whileHover={reducedMotion || dragging ? undefined : { scale: 1.035 }}

View File

@@ -857,7 +857,7 @@ export function CanvasCloudAgentPanel({ canvasId, domainProjectId, nodeCount, se
return (
<>
{!open ? <AgentLauncher theme={theme} statusColor={statusColor} approvalPending={Boolean(approval)} reducedMotion={Boolean(reducedMotion)} onOpen={onOpen} /> : null}
<AgentLauncher theme={theme} statusColor={statusColor} approvalPending={Boolean(approval)} reducedMotion={Boolean(reducedMotion)} hidden={open} onOpen={onOpen} />
<AnimatePresence>
{open ? (
<motion.aside

View File

@@ -20,11 +20,11 @@ export default function StorageResourcesPanel() {
const kind = normalizeOption(searchParams.get("kind"), ["image", "video", "audio", "file", "live2d"]);
const status = normalizeOption(searchParams.get("status"), ["pending", "ready", "failed", "deleted"]);
const provider = normalizeOption(searchParams.get("provider"), ["local", "aliyun", "tencent", "qiniu", "s3"]);
const userId = searchParams.get("userId") || "";
const userQuery = searchParams.get("user") || searchParams.get("userId") || "";
const page = positiveInt(searchParams.get("page"), 1);
const pageSize = normalizePageSize(searchParams.get("pageSize"));
const debouncedKeyword = useDebouncedValue(keyword);
const debouncedUserId = useDebouncedValue(userId);
const debouncedUserQuery = useDebouncedValue(userQuery);
const [resources, setResources] = useState<AdminStorageResource[]>([]);
const [stats, setStats] = useState<AdminStorageStats | null>(null);
const [total, setTotal] = useState(0);
@@ -35,12 +35,12 @@ export default function StorageResourcesPanel() {
const [deleting, setDeleting] = useState(false);
const [refreshKey, setRefreshKey] = useState(0);
const requestSequence = useRef(0);
const hasFilters = Boolean(keyword || userId || kind !== "all" || status !== "all" || provider !== "all");
const hasFilters = Boolean(keyword || userQuery || kind !== "all" || status !== "all" || provider !== "all");
const updateUrl = (patch: Record<string, string | number>, replace = false) => {
const next = new URLSearchParams(searchParams);
Object.entries(patch).forEach(([key, value]) => {
const isDefault = ((key === "filter" || key === "userId") && value === "") || (["kind", "status", "provider"].includes(key) && value === "all") || (key === "page" && value === 1) || (key === "pageSize" && value === 20);
const isDefault = ((key === "filter" || key === "user" || key === "userId") && value === "") || (["kind", "status", "provider"].includes(key) && value === "all") || (key === "page" && value === 1) || (key === "pageSize" && value === 20);
if (isDefault) next.delete(key);
else next.set(key, String(value));
});
@@ -68,7 +68,8 @@ export default function StorageResourcesPanel() {
kind: kind === "all" ? undefined : kind,
status: status === "all" ? undefined : status,
provider: provider === "all" ? undefined : provider,
userId: debouncedUserId || undefined,
user: debouncedUserQuery || undefined,
userId: debouncedUserQuery || undefined,
page,
pageSize: pageSize,
},
@@ -86,7 +87,7 @@ export default function StorageResourcesPanel() {
})
.finally(() => sequence === requestSequence.current && setLoading(false));
return () => controller.abort();
}, [debouncedKeyword, debouncedUserId, kind, status, provider, page, pageSize, refreshKey]);
}, [debouncedKeyword, debouncedUserQuery, kind, status, provider, page, pageSize, refreshKey]);
const columns = useMemo<ColumnsType<AdminStorageResource>>(
() => [
@@ -210,7 +211,7 @@ export default function StorageResourcesPanel() {
placeholder="资源 ID 或对象路径"
onChange={(event) => updateUrl({ filter: event.target.value, page: 1 }, true)}
/>
<Input aria-label="按用户 ID 筛选" autoComplete="off" allowClear className="w-48" value={userId} placeholder="用户" onChange={(event) => updateUrl({ userId: event.target.value, page: 1 }, true)} />
<Input aria-label="按用户名、昵称、邮箱或用户 ID 筛选" autoComplete="off" allowClear className="w-64" value={userQuery} placeholder="用户名 / 昵称 / 邮箱 / 用户 ID" onChange={(event) => updateUrl({ user: event.target.value, userId: "", page: 1 }, true)} />
<Select aria-label="筛选资源类型" className="w-32" value={kind} onChange={(value) => updateUrl({ kind: value, page: 1 })} options={kindOptions} />
<Select aria-label="筛选资源状态" className="w-32" value={status} onChange={(value) => updateUrl({ status: value, page: 1 })} options={statusOptions} />
<Select aria-label="筛选存储类型" className="w-36" value={provider} onChange={(value) => updateUrl({ provider: value, page: 1 })} options={providerOptions} />
@@ -219,14 +220,14 @@ export default function StorageResourcesPanel() {
toolbarActiveFilters={
<>
{keyword ? <AdminFilterChip label={`搜索:${keyword}`} onRemove={() => updateUrl({ filter: "", page: 1 })} /> : null}
{userId ? <AdminFilterChip label={`用户:${userId}`} onRemove={() => updateUrl({ userId: "", page: 1 })} /> : null}
{userQuery ? <AdminFilterChip label={`用户:${userQuery}`} onRemove={() => updateUrl({ user: "", userId: "", page: 1 })} /> : null}
{kind !== "all" ? <AdminFilterChip label={`类型:${kindLabel(kind)}`} onRemove={() => updateUrl({ kind: "all", page: 1 })} /> : null}
{status !== "all" ? <AdminFilterChip label={`状态:${statusLabel(status)}`} onRemove={() => updateUrl({ status: "all", page: 1 })} /> : null}
{provider !== "all" ? <AdminFilterChip label={`存储:${providerLabel(provider)}`} onRemove={() => updateUrl({ provider: "all", page: 1 })} /> : null}
</>
}
toolbarActive={hasFilters}
onReset={() => updateUrl({ filter: "", userId: "", kind: "all", status: "all", provider: "all", page: 1 })}
onReset={() => updateUrl({ filter: "", user: "", userId: "", kind: "all", status: "all", provider: "all", page: 1 })}
batchActions={
<AdminBatchBar count={selectedIds.length} onClear={() => setSelectedIds([])}>
<Button danger size="small" icon={<Trash2 className="size-3.5" />} loading={deleting} onClick={() => confirmDelete(selectedIds)}>

View File

@@ -49,6 +49,7 @@ export type AdminResourceQuery = {
kind?: string;
status?: string;
provider?: string;
user?: string;
userId?: string;
page?: number;
pageSize?: number;