import React, { useEffect, useMemo, useState } from "react";
import {
Activity,
AlertTriangle,
ArrowUpRight,
Ban,
Bell,
CheckCircle2,
CircleDollarSign,
Clock3,
Database,
Download,
FileKey2,
FileText,
Film,
Gauge,
HardDrive,
KeyRound,
ListChecks,
LogIn,
ListFilter,
Network,
Play,
Plus,
RefreshCw,
Save,
Search,
ServerCog,
Settings2,
ShieldAlert,
ShieldCheck,
SlidersHorizontal,
MessageSquare,
Link2,
Paperclip,
Send,
AtSign,
ChevronLeft,
ChevronRight,
X,
UserRoundCheck,
Users,
UserRoundCog,
Webhook,
Workflow,
XCircle,
Zap
} from "lucide-react";
import {
createApiClient,
createSystemUser,
acceptInvitation,
cancelMfaSetup,
changePassword,
fetchAuthSessions,
fetchAuthDevices,
fetchSecurityEvents,
fetchAdminQueue,
fetchWorkerStatus,
dispatchWorker,
disableMfa,
enableMfa,
fetchJob,
fetchInvitations,
fetchMfaStatus,
fetchSystemConfig,
fetchSystemHealth,
fetchSystemReadiness,
createSystemBackup,
probeModel,
registerModel,
retryJob,
revokeAuthSession,
revokeOtherAuthSessions,
trustAuthDevice,
untrustAuthDevice,
cancelJob,
runJob,
updateModel,
updateApiClient,
rotateApiClientKey,
updateJobPriority,
operateRunner,
saveSystemConfig,
startMfaSetup,
updateFeatureFlag,
updateNotificationChannel,
previewInvitation,
registerInvitedUser,
testNotification,
fetchNotificationDeliveries,
fetchStorageUsage,
fetchSsoProviders,
fetchIdentityCenter,
fetchOrganizationCommercial,
fetchOrganizationUsage,
fetchOrganizationInvoices,
exportOrganizationCommercial,
exportOrganizationUsage,
exportOrganizationUsageCsv,
exportOrganizationInvoices,
exportOrganizationInvoicesCsv,
fetchSystemUser,
fetchSystemUsers,
resetSystemUserMfa,
resetSystemUserPassword,
revokeSystemUserSessions,
updateOrganizationBilling,
generateOrganizationInvoice,
updateOrganizationInvoiceStatus,
updateCostCenter,
updateQuotaAllocation,
updateSystemUser,
updateSystemUserMemberships,
batchQueueAction,
updateIdentityPolicy,
createIdentityProvider,
updateIdentityProvider,
probeIdentityProvider,
createDirectorySync,
updateDirectorySync,
rotateDirectorySyncToken,
ssoStartUrl,
fetchWorkItems,
fetchTasks,
createTask,
updateTask,
fetchTaskDetail,
createTaskComment,
addTaskLink,
removeTaskLink,
fetchProjectActivity,
fetchProductionGraph,
fetchAuditEvents,
fetchAuditEvent,
exportAuditEvents,
fetchNotifications,
fetchNotificationPreferences,
markNotificationRead,
markAllNotificationsRead,
updateNotificationPreference
} from "../lib/api";
import { downloadJson } from "../lib/exporters";
function toneForStatus(status) {
if (["ready", "active", "completed", "pass", "enforced"].includes(status)) return "ok";
if (["failed", "blocked", "error", "disabled"].includes(status)) return "warn";
return "neutral";
}
function AdminHeader({ eyebrow, title, description, action }) {
return (
{eyebrow}
{title}
{description}
{action}
);
}
function AdminKpi({ label, value, detail, icon: Icon, tone = "neutral" }) {
return (
);
}
function SectionBar({ title, detail, action }) {
return {title}
{detail && {detail}}{action}
;
}
function ContextLink({ children, onClick }) {
return ;
}
const workItemKindLabels = {
task: "协作任务",
review: "审片",
job: "生成",
asset: "资产",
delivery: "交付",
invitation: "邀请"
};
function formatWorkItemTime(value) {
const date = new Date(value || "");
if (Number.isNaN(date.getTime())) return "刚刚";
const elapsed = Math.max(0, Date.now() - date.getTime());
if (elapsed < 60 * 1000) return "刚刚";
if (elapsed < 60 * 60 * 1000) return `${Math.floor(elapsed / 60000)} 分钟前`;
if (elapsed < 24 * 60 * 60 * 1000) return `${Math.floor(elapsed / 3600000)} 小时前`;
return date.toLocaleDateString("zh-CN", { month: "numeric", day: "numeric" });
}
function workItemTone(item) {
if (item.priority === "high" || ["failed", "blocked", "changes_requested", "rejected", "needs-evidence"].includes(item.status)) return "warn";
if (item.status === "approved" || item.status === "completed") return "ok";
return "neutral";
}
const notificationCategoryLabels = {
job: "生成任务",
review: "审片流程",
delivery: "交付运营",
task: "协作任务",
access: "组织访问",
billing: "用量与配额",
system: "系统通知"
};
function formatNotificationTime(value) {
const date = new Date(value || "");
if (Number.isNaN(date.getTime())) return "刚刚";
const elapsed = Math.max(0, Date.now() - date.getTime());
if (elapsed < 60 * 1000) return "刚刚";
if (elapsed < 60 * 60 * 1000) return `${Math.floor(elapsed / 60000)} 分钟前`;
if (elapsed < 24 * 60 * 60 * 1000) return `${Math.floor(elapsed / 3600000)} 小时前`;
return date.toLocaleString("zh-CN", { month: "numeric", day: "numeric", hour: "2-digit", minute: "2-digit" });
}
const taskStatusLabels = {
open: "待处理",
in_progress: "进行中",
blocked: "已阻塞",
done: "已完成",
cancelled: "已取消"
};
const taskPriorityLabels = {
high: "高优先级",
medium: "普通",
low: "低优先级"
};
function formatTaskDue(value, overdue = false) {
if (!value) return "无截止时间";
const date = new Date(value);
if (Number.isNaN(date.getTime())) return "截止时间待确认";
return `${overdue ? "已逾期 · " : "截止 "}${date.toLocaleDateString("zh-CN", { month: "numeric", day: "numeric" })}`;
}
function notificationIcon(severity) {
if (severity === "error") return AlertTriangle;
if (severity === "success") return CheckCircle2;
if (severity === "warning") return ShieldAlert;
return Bell;
}
function notificationTone(severity) {
if (severity === "error" || severity === "warning") return "warn";
if (severity === "success") return "ok";
return "neutral";
}
export function NotificationBell({ contextOverrides = {}, onOpen }) {
const [data, setData] = useState({ notifications: [], unreadCount: 0 });
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const scopeKey = [contextOverrides.userId, contextOverrides.organizationId, contextOverrides.workspaceId].join("|");
async function load() {
setLoading(true);
try {
setData(await fetchNotifications({ limit: 8 }, contextOverrides));
} catch {
setData((current) => current);
} finally {
setLoading(false);
}
}
useEffect(() => {
load();
const timer = window.setInterval(load, 30000);
return () => window.clearInterval(timer);
}, [scopeKey]);
async function readAndOpen(notification) {
try {
const result = await markNotificationRead(notification.id, true, contextOverrides);
setData(result);
} catch {
// The destination page still remains usable if the notification was already read elsewhere.
}
setOpen(false);
if (notification.targetTab) onOpen?.(notification.targetTab);
}
async function readAll(event) {
event.stopPropagation();
try {
setData(await markAllNotificationsRead(contextOverrides));
} catch {
// Keep the popover open so the user can retry after the API recovers.
}
}
return (
{open && <>
);
}
export function NotificationCenterPage({ contextOverrides = {}, setActiveTab }) {
const [data, setData] = useState({ notifications: [], unreadCount: 0 });
const [preferences, setPreferences] = useState([]);
const [filter, setFilter] = useState("all");
const [loading, setLoading] = useState(true);
const [preferenceBusy, setPreferenceBusy] = useState("");
const [notice, setNotice] = useState("");
const scopeKey = [contextOverrides.userId, contextOverrides.organizationId, contextOverrides.workspaceId].join("|");
async function load() {
setLoading(true);
try {
const [nextData, nextPreferences] = await Promise.all([
fetchNotifications({ limit: 200, unreadOnly: filter === "unread" }, contextOverrides),
fetchNotificationPreferences(contextOverrides)
]);
setData(nextData);
setPreferences(nextPreferences.preferences || []);
setNotice("");
} catch (error) {
setNotice(error.message);
} finally {
setLoading(false);
}
}
useEffect(() => { load(); }, [scopeKey, filter]);
async function markRead(notification, read = true) {
try {
setData(await markNotificationRead(notification.id, read, contextOverrides));
} catch (error) {
setNotice(error.message);
}
}
async function markAllRead() {
try {
setData(await markAllNotificationsRead(contextOverrides));
setNotice("当前工作区通知已全部标记为已读");
} catch (error) {
setNotice(error.message);
}
}
async function togglePreference(preference) {
setPreferenceBusy(preference.category);
try {
const result = await updateNotificationPreference(preference.category, !preference.enabled, contextOverrides);
setPreferences(result.preferences || []);
setNotice(`${preference.label}通知已${!preference.enabled ? "开启" : "关闭"}`);
} catch (error) {
setNotice(error.message);
} finally {
setPreferenceBusy("");
}
}
return (
} />
{notice && {notice}
}
setFilter("all")}>全部 setFilter("unread")}>未读 {data.unreadCount ? `(${data.unreadCount})` : ""}} />
{loading ? 读取通知…
: {data.notifications.map((notification) => { const Icon = notificationIcon(notification.severity); return
{notificationCategoryLabels[notification.category] || notification.category}{formatNotificationTime(notification.createdAt)}
{notification.title}{notification.body}
{notification.eventKey}{notification.targetId && 关联 {notification.targetId}}
{notification.targetTab &&
{ markRead(notification); setActiveTab?.(notification.targetTab); }}>打开}
markRead(notification, !notification.read)}>{notification.read ? "标记未读" : "标记已读"}; })}{!data.notifications.length &&
当前筛选下没有消息。
}
}
{preferences.map((preference) =>
{preference.label}{preference.description}
togglePreference(preference)} disabled={preferenceBusy === preference.category}>{preferenceBusy === preference.category ? "保存中…" : preference.enabled ? "已开启" : "已关闭"})}{!preferences.length &&
暂无可配置的通知类别。
}
);
}
export function TaskCenterPage({ platformContext, contextOverrides = {} }) {
const current = platformContext?.context || {};
const permissions = current.permissions || [];
const canManage = permissions.includes("task:manage");
const canComplete = permissions.includes("task:complete");
const members = [...(platformContext?.platform?.projectMembers || []), ...(platformContext?.platform?.workspaceMembers || [])]
.filter((member) => member.status === "active")
.filter((member, index, list) => list.findIndex((item) => item.user_id === member.user_id) === index)
.map((member) => ({ id: member.user_id, displayName: member.display_name, email: member.email || "" }));
const currentUser = current.currentUser || {};
const assigneeOptions = [{ id: currentUser.id, displayName: `${currentUser.displayName || currentUser.display_name || currentUser.email || "我"}(我)`, email: currentUser.email || "" }, ...members.filter((member) => member.id !== currentUser.id)]
.filter((member, index, list) => member.id && list.findIndex((item) => item.id === member.id) === index);
const scopeKey = [contextOverrides.userId, contextOverrides.organizationId, contextOverrides.workspaceId, contextOverrides.projectId].join("|");
const [statusFilter, setStatusFilter] = useState("all");
const [mineOnly, setMineOnly] = useState(false);
const [data, setData] = useState({ tasks: [], summary: { total: 0, open: 0, blocked: 0, done: 0, overdue: 0 } });
const [activity, setActivity] = useState([]);
const [catalog, setCatalog] = useState({ shots: [], assets: [] });
const [loading, setLoading] = useState(true);
const [activityLoading, setActivityLoading] = useState(true);
const [busyId, setBusyId] = useState("");
const [notice, setNotice] = useState("");
const [error, setError] = useState("");
const [selectedTaskId, setSelectedTaskId] = useState("");
const [detail, setDetail] = useState(null);
const [detailLoading, setDetailLoading] = useState(false);
const [detailTab, setDetailTab] = useState("discussion");
const [comment, setComment] = useState("");
const [replyTo, setReplyTo] = useState("");
const [mentionIds, setMentionIds] = useState([]);
const [linkType, setLinkType] = useState("shot");
const [linkTargetId, setLinkTargetId] = useState("");
const [filePath, setFilePath] = useState("");
const [form, setForm] = useState({ title: "", description: "", kind: "production", priority: "medium", assigneeUserId: currentUser.id || "", targetTab: "creator-home", targetId: "", dueAt: "", linkShotId: "" });
useEffect(() => {
if (currentUser.id && !form.assigneeUserId) setForm((currentForm) => ({ ...currentForm, assigneeUserId: currentUser.id }));
}, [currentUser.id]);
async function load() {
setLoading(true);
setActivityLoading(true);
const [taskResult, activityResult, graphResult] = await Promise.allSettled([
fetchTasks({ status: statusFilter, assignedTo: mineOnly ? "me" : "", limit: 200 }, contextOverrides),
fetchProjectActivity({ limit: 80 }, contextOverrides),
fetchProductionGraph(contextOverrides)
]);
if (taskResult.status === "fulfilled") { setData(taskResult.value); setError(""); } else setError(taskResult.reason.message);
if (activityResult.status === "fulfilled") setActivity(activityResult.value.activities || []);
if (graphResult.status === "fulfilled") setCatalog({ shots: graphResult.value.graph?.shots || [], assets: graphResult.value.graph?.assets || [] });
setLoading(false);
setActivityLoading(false);
}
useEffect(() => {
let active = true;
Promise.allSettled([
fetchTasks({ status: statusFilter, assignedTo: mineOnly ? "me" : "", limit: 200 }, contextOverrides),
fetchProjectActivity({ limit: 80 }, contextOverrides),
fetchProductionGraph(contextOverrides)
]).then(([taskResult, activityResult, graphResult]) => {
if (!active) return;
if (taskResult.status === "fulfilled") { setData(taskResult.value); setError(""); } else setError(taskResult.reason.message);
if (activityResult.status === "fulfilled") setActivity(activityResult.value.activities || []);
if (graphResult.status === "fulfilled") setCatalog({ shots: graphResult.value.graph?.shots || [], assets: graphResult.value.graph?.assets || [] });
}).finally(() => { if (active) { setLoading(false); setActivityLoading(false); } });
return () => { active = false; };
}, [scopeKey, statusFilter, mineOnly]);
async function openTask(taskId) {
setSelectedTaskId(taskId);
setDetailLoading(true);
setDetailTab("discussion");
try { setDetail(await fetchTaskDetail(taskId, contextOverrides)); } catch (loadError) { setError(loadError.message); } finally { setDetailLoading(false); }
}
async function submitTask(event) {
event.preventDefault();
setBusyId("create");
setNotice("");
try {
const result = await createTask({ ...form, dueAt: form.dueAt ? new Date(`${form.dueAt}T23:59:59`).toISOString() : null }, contextOverrides);
if (form.linkShotId) await addTaskLink(result.task.id, { linkType: "shot", targetId: form.linkShotId }, contextOverrides);
setForm({ title: "", description: "", kind: "production", priority: "medium", assigneeUserId: currentUser.id || "", targetTab: "creator-home", targetId: "", dueAt: "", linkShotId: "" });
setNotice("协作任务已创建,负责人、关联镜头和审计记录已建立");
await load();
await openTask(result.task.id);
} catch (createError) { setError(createError.message); } finally { setBusyId(""); }
}
async function changeTaskStatus(task, status) {
setBusyId(task.id);
setNotice("");
try {
const result = await updateTask(task.id, { status }, contextOverrides);
setData((currentData) => ({ ...currentData, tasks: currentData.tasks.map((item) => item.id === task.id ? result.task : item) }));
if (selectedTaskId === task.id) setDetail((currentDetail) => currentDetail ? { ...currentDetail, task: result.task } : currentDetail);
setNotice(status === "done" ? "任务已完成" : "任务状态已更新,并已进入项目活动流");
await load();
} catch (updateError) { setError(updateError.message); } finally { setBusyId(""); }
}
async function submitComment(event) {
event.preventDefault();
if (!selectedTaskId || !comment.trim()) return;
setBusyId("comment");
try {
const result = await createTaskComment(selectedTaskId, { body: comment.trim(), parentCommentId: replyTo || null, mentionUserIds: mentionIds }, contextOverrides);
setDetail(result);
setComment("");
setReplyTo("");
setMentionIds([]);
setNotice("讨论已记录,相关负责人和 @成员已收到站内通知");
const activityResult = await fetchProjectActivity({ limit: 80 }, contextOverrides);
setActivity(activityResult.activities || []);
setData((currentData) => ({ ...currentData, tasks: currentData.tasks.map((item) => item.id === selectedTaskId ? result.task : item) }));
} catch (commentError) { setError(commentError.message); } finally { setBusyId(""); }
}
async function submitLink(event) {
event.preventDefault();
if (!selectedTaskId) return;
const targetId = linkType === "file" ? filePath.trim() : linkTargetId;
if (!targetId) return;
setBusyId("link");
try {
const result = await addTaskLink(selectedTaskId, { linkType, targetId }, contextOverrides);
setDetail(result);
setLinkTargetId("");
setFilePath("");
setNotice("生产对象已关联到任务");
const activityResult = await fetchProjectActivity({ limit: 80 }, contextOverrides);
setActivity(activityResult.activities || []);
} catch (linkError) { setError(linkError.message); } finally { setBusyId(""); }
}
async function unlink(linkId) {
setBusyId(linkId);
try { setDetail(await removeTaskLink(selectedTaskId, linkId, contextOverrides)); setNotice("任务关联已移除"); } catch (linkError) { setError(linkError.message); } finally { setBusyId(""); }
}
const selectedTask = detail?.task || data.tasks.find((task) => task.id === selectedTaskId);
const linkOptions = linkType === "shot" ? catalog.shots : catalog.assets;
return (
刷新工作区} />
{notice && {notice}
}
{error && }
setStatusFilter("all")}>全部 setStatusFilter("open")}>待处理 setStatusFilter("in_progress")}>进行中 setStatusFilter("blocked")}>阻塞 setStatusFilter("done")}>已完成 setMineOnly((value) => !value)}>只看我的
} />
{loading ? 读取协作任务…
: {(data.tasks || []).map((task) =>
openTask(task.id)} onKeyDown={(event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); openTask(task.id); } }} role="button" tabIndex="0">
{taskPriorityLabels[task.priority] || task.priority}{task.kind}{formatTaskDue(task.dueAt, task.overdue)}
{task.title}{task.description || "暂无任务说明"}
负责人:{task.assignee?.displayName || "未分派"}创建人:{task.createdBy?.displayName || "未知"} {task.commentCount || 0} {task.linkCount || 0}更新于 {formatNotificationTime(task.updatedAt)}
{canManage ? : task.assignee?.id === currentUser.id && canComplete && !["done", "cancelled"].includes(task.status) ? { event.stopPropagation(); changeTaskStatus(task, "done"); }} disabled={busyId === task.id}>完成 : }
)}{!data.tasks?.length &&
当前筛选下没有协作任务。
}
}
{canManage ? : 当前角色可以查看项目任务{canComplete ? ",并更新自己负责的任务状态" : ",但不能创建或分派任务"}。
}
刷新} />
{activityLoading ? 读取项目活动…
: {activity.slice(0, 24).map((item) =>
{item.actor?.displayName || "系统"} {item.label}{item.targetLabel || item.targetId}
{formatNotificationTime(item.createdAt)} · {item.targetType} {item.result})}{!activity.length &&
当前项目还没有可展示的活动记录。
}
}
{selectedTaskId && setSelectedTaskId("")}>
}
);
}
export function CreatorHomePage({ project, platformContext, contextOverrides = {}, setActiveTab, canCreateProject, allowedTabs = [] }) {
const platform = platformContext?.platform || {};
const current = platformContext?.context;
const projects = platform.projects || [];
const jobs = project.productionJobs || [];
const readyShots = project.shots.filter((shot) => shot.status === "ready").length;
const currentProject = current?.currentProject?.name || project.series.title;
const canOpen = (tab) => allowedTabs.includes(tab);
const [workItems, setWorkItems] = useState({ items: [], summary: { total: 0 } });
const [workItemsLoading, setWorkItemsLoading] = useState(true);
const [workItemsError, setWorkItemsError] = useState("");
const scopeKey = [contextOverrides.userId, contextOverrides.organizationId, contextOverrides.workspaceId, contextOverrides.projectId].join("|");
async function loadWorkItems() {
setWorkItemsLoading(true);
try {
const payload = await fetchWorkItems(contextOverrides);
setWorkItems(payload);
setWorkItemsError("");
} catch (error) {
setWorkItemsError(error.message);
} finally {
setWorkItemsLoading(false);
}
}
useEffect(() => {
let active = true;
setWorkItemsLoading(true);
fetchWorkItems(contextOverrides).then((payload) => {
if (active) {
setWorkItems(payload);
setWorkItemsError("");
}
}).catch((error) => {
if (active) setWorkItemsError(error.message);
}).finally(() => {
if (active) setWorkItemsLoading(false);
});
return () => { active = false; };
}, [scopeKey]);
const visibleWorkItems = (workItems.items || []).filter((item) => canOpen(item.targetTab));
return (
setActiveTab("factory")}>新建项目}
/>
setActiveTab("factory")}>查看全部项目} />
项目类型阶段准备度最近更新操作
{(projects.length ? projects : [{ id: project.series.id, name: project.series.title, type: "AI 漫剧", status: "production", readiness: 72, updated_at: "刚刚" }]).map((item) => (
{item.name || item.title}
{item.type}
{item.readiness ?? 72}%
{item.updated_at?.slice(0, 10) || item.updatedAt || "今天"}
canOpen("director") && setActiveTab("director")} disabled={!canOpen("director")}>打开
))}
} />
{workItemsError && }
{workItemsLoading &&
正在读取当前组织 / 工作区 / 项目的待办事项…
}
{!workItemsLoading && visibleWorkItems.map((item) => (
setActiveTab(item.targetTab)}>
{item.title}{workItemKindLabels[item.kind] || item.kind} · {item.detail} · {formatWorkItemTime(item.updatedAt)}
))}
{!workItemsLoading && !visibleWorkItems.length && !workItemsError &&
当前角色没有待处理事项。
}
{[
["剧本拆解", "长文本 → 分集/场次/镜头", "script", Workflow],
["资产锁定", "角色、场景、道具、声线", "casting", Users],
["导演工作台", "首尾帧与 continuity ledger", "director", SlidersHorizontal],
["生成队列", "本地模型任务与重试", "jobs", Zap]
].filter(([, , tab]) => canOpen(tab)).map(([title, detail, tab, Icon]) => setActiveTab(tab)}>{title}{detail})}
);
}
export function LoginPage({ onLogin, loading = false, error = "", initialChallenge = null }) {
const [email, setEmail] = useState("producer@local.test");
const [password, setPassword] = useState("Demo@123456");
const [showPassword, setShowPassword] = useState(false);
const [mfaChallenge, setMfaChallenge] = useState(initialChallenge);
const [mfaSetup, setMfaSetup] = useState(null);
const [mfaCode, setMfaCode] = useState("");
const [ssoProviders, setSsoProviders] = useState([]);
const enrollmentMode = Boolean(mfaChallenge?.mfaEnrollmentRequired);
useEffect(() => {
if (initialChallenge) setMfaChallenge(initialChallenge);
}, [initialChallenge]);
useEffect(() => {
let active = true;
fetchSsoProviders().then((payload) => {
if (active) setSsoProviders(payload.providers || []);
}).catch(() => {
if (active) setSsoProviders([]);
});
return () => { active = false; };
}, []);
async function submit(event) {
event.preventDefault();
if (mfaChallenge && enrollmentMode && !mfaSetup) {
const result = await onLogin({ enrollmentAction: "setup", enrollmentToken: mfaChallenge.enrollmentToken });
if (result?.setup) {
setMfaSetup(result.setup);
setMfaCode("");
}
return;
}
const result = await onLogin(mfaChallenge ? enrollmentMode ? { enrollmentAction: "complete", enrollmentToken: mfaChallenge.enrollmentToken, methodId: mfaSetup.methodId, code: mfaCode } : { challengeToken: mfaChallenge.challengeToken, code: mfaCode } : { email, password });
if (result?.mfaRequired && !mfaChallenge) {
setMfaChallenge(result);
setMfaCode("");
}
}
return (
);
}
export function RegisterPage({ inviteToken, onRegistered }) {
const [invitation, setInvitation] = useState(null);
const [form, setForm] = useState({ displayName: "", password: "", confirmPassword: "" });
const [loading, setLoading] = useState(true);
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
useEffect(() => {
let cancelled = false;
previewInvitation(inviteToken).then((result) => {
if (!cancelled) setInvitation(result.invitation);
}).catch((nextError) => {
if (!cancelled) setError(nextError.message);
}).finally(() => {
if (!cancelled) setLoading(false);
});
return () => { cancelled = true; };
}, [inviteToken]);
async function submit(event) {
event.preventDefault();
if (form.password !== form.confirmPassword) {
setError("两次输入的密码不一致");
return;
}
setBusy(true);
setError("");
try {
const result = await registerInvitedUser({ inviteToken, displayName: form.displayName, password: form.password });
await onRegistered?.(result);
} catch (nextError) {
setError(nextError.message);
} finally {
setBusy(false);
}
}
return ;
}
export function AccountSecurityPage({ authSession, platformContext, contextOverrides, onInvitationAccepted }) {
const [form, setForm] = useState({ currentPassword: "", nextPassword: "", confirmPassword: "" });
const [invitations, setInvitations] = useState([]);
const [sessions, setSessions] = useState([]);
const [devices, setDevices] = useState([]);
const [securityEvents, setSecurityEvents] = useState([]);
const [mfa, setMfa] = useState({ enabled: false, method: null });
const [mfaSetup, setMfaSetup] = useState(null);
const [mfaCode, setMfaCode] = useState("");
const [mfaPassword, setMfaPassword] = useState("");
const [showSessionHistory, setShowSessionHistory] = useState(false);
const [loading, setLoading] = useState(true);
const [sessionsLoading, setSessionsLoading] = useState(true);
const [devicesLoading, setDevicesLoading] = useState(true);
const [securityEventsLoading, setSecurityEventsLoading] = useState(true);
const [busy, setBusy] = useState(false);
const [notice, setNotice] = useState("");
const [error, setError] = useState("");
async function loadInvitations() {
setLoading(true);
try {
const result = await fetchInvitations(contextOverrides);
setInvitations(result.invitations || []);
setError("");
} catch (nextError) {
setError(nextError.message);
} finally {
setLoading(false);
}
}
async function loadSessions() {
setSessionsLoading(true);
try {
const result = await fetchAuthSessions();
setSessions(result.sessions || []);
setError("");
} catch (nextError) {
setError(nextError.message);
} finally {
setSessionsLoading(false);
}
}
async function loadSecurityEvents() {
setSecurityEventsLoading(true);
try {
const result = await fetchSecurityEvents({ limit: 80 }, contextOverrides);
setSecurityEvents(result.events || []);
} catch (nextError) {
setError(nextError.message);
} finally {
setSecurityEventsLoading(false);
}
}
async function loadDevices() {
setDevicesLoading(true);
try {
const result = await fetchAuthDevices();
setDevices(result.devices || []);
setError("");
} catch (nextError) {
setError(nextError.message);
} finally {
setDevicesLoading(false);
}
}
async function loadMfa() {
try {
const result = await fetchMfaStatus(contextOverrides);
setMfa(result);
if (result.enabled) setMfaSetup(null);
} catch (nextError) {
setError(nextError.message);
}
}
useEffect(() => { loadInvitations(); loadSessions(); loadDevices(); loadSecurityEvents(); loadMfa(); }, [contextOverrides?.organizationId, contextOverrides?.workspaceId]);
async function beginMfaSetup() {
setBusy(true);
try {
const result = await startMfaSetup(contextOverrides);
setMfaSetup(result.setup);
setMfaCode("");
setNotice("MFA 初始化密钥已生成,请在身份验证器中添加后输入验证码。");
setError("");
} catch (nextError) {
setError(nextError.message);
} finally {
setBusy(false);
}
}
async function confirmMfaSetup(event) {
event.preventDefault();
if (!mfaSetup || mfaCode.length !== 6) return;
setBusy(true);
try {
const result = await enableMfa({ methodId: mfaSetup.methodId, code: mfaCode }, contextOverrides);
setMfa(result);
setMfaSetup(null);
setMfaCode("");
setNotice("多因素认证已启用,之后登录需要密码和身份验证器验证码。");
await loadSecurityEvents();
setError("");
} catch (nextError) {
setError(nextError.message);
} finally {
setBusy(false);
}
}
async function cancelMfaInitialization() {
if (!mfaSetup) return;
setBusy(true);
try {
const result = await cancelMfaSetup({ methodId: mfaSetup.methodId }, contextOverrides);
setMfa(result);
setMfaSetup(null);
setMfaCode("");
setNotice("MFA 初始化已取消,未启用密钥已清理。");
await loadSecurityEvents();
setError("");
} catch (nextError) {
setError(nextError.message);
} finally {
setBusy(false);
}
}
async function removeMfa(event) {
event.preventDefault();
setBusy(true);
try {
const result = await disableMfa({ currentPassword: mfaPassword, code: mfaCode }, contextOverrides);
setMfa(result);
setMfaPassword("");
setMfaCode("");
setNotice("多因素认证已关闭。");
await loadSecurityEvents();
setError("");
} catch (nextError) {
setError(nextError.message);
} finally {
setBusy(false);
}
}
async function submitPassword(event) {
event.preventDefault();
if (form.nextPassword !== form.confirmPassword) {
setError("两次输入的新密码不一致");
return;
}
setBusy(true);
try {
await changePassword({ currentPassword: form.currentPassword, nextPassword: form.nextPassword }, contextOverrides);
setForm({ currentPassword: "", nextPassword: "", confirmPassword: "" });
setNotice("密码已更新,当前登录会话保持有效。");
await loadSecurityEvents();
setError("");
} catch (nextError) {
setError(nextError.message);
} finally {
setBusy(false);
}
}
async function accept(invitation) {
setBusy(true);
try {
const result = await acceptInvitation(invitation.id, contextOverrides);
setInvitations((current) => current.filter((item) => item.id !== invitation.id));
setNotice(`已接受“${invitation.organization_name}”的邀请,正在切换到新组织。`);
await onInvitationAccepted?.(result.invitation);
} catch (nextError) {
setError(nextError.message);
} finally {
setBusy(false);
}
}
async function revokeSession(session) {
setBusy(true);
try {
const result = await revokeAuthSession(session.id);
setSessions(result.sessions || []);
setNotice("已撤销选中的登录会话。");
await loadSecurityEvents();
setError("");
} catch (nextError) {
setError(nextError.message);
} finally {
setBusy(false);
}
}
async function revokeOthers() {
setBusy(true);
try {
const result = await revokeOtherAuthSessions();
setSessions(result.sessions || []);
setNotice(`已撤销 ${result.revokedCount || 0} 个其他登录会话。`);
await loadSecurityEvents();
setError("");
} catch (nextError) {
setError(nextError.message);
} finally {
setBusy(false);
}
}
async function setDeviceTrust(device, trusted) {
setBusy(true);
try {
const result = trusted ? await trustAuthDevice(device.id) : await untrustAuthDevice(device.id);
setDevices(result.devices || []);
setNotice(trusted ? `已将“${device.label || "浏览器设备"}”标记为信任设备。` : `已取消“${device.label || "浏览器设备"}”的信任状态。`);
await Promise.all([loadSessions(), loadSecurityEvents()]);
setError("");
} catch (nextError) {
setError(nextError.message);
} finally {
setBusy(false);
}
}
const expiresAt = authSession?.expiresAt || "";
const currentUser = platformContext?.context?.currentUser;
const visibleSessions = sessions.filter((session) => showSessionHistory || ["current", "active"].includes(session.status));
const activeSessionCount = sessions.filter((session) => ["current", "active"].includes(session.status)).length;
return (
刷新邀请} />
{notice && {notice}
}
{error && }
{currentUser?.display_name || "当前用户"}{currentUser?.email || "已登录用户"}会话有效至 {expiresAt ? new Date(expiresAt).toLocaleString("zh-CN", { hour12: false }) : "当前浏览器会话"}
服务端 Bearer 鉴权
组织 / 工作区 / 项目 scope
前端隐藏不替代后端 403
setShowSessionHistory((current) => !current)}>{showSessionHistory ? "隐藏历史" : `查看历史(${sessions.filter((session) => !["current", "active"].includes(session.status)).length})`}刷新 session.status === "active").length === 0}>撤销其他会话 } />
{visibleSessions.map((session) =>
{session.current ? "当前浏览器会话" : session.status === "active" ? "其他登录会话" : "历史会话"} · {session.device?.label || "未登记设备"}{session.userAgent || "本地客户端"} · {session.device?.status === "trusted" ? "信任设备" : session.device?.status === "revoked" ? "已撤销设备" : "普通设备"}IP {session.ipAddress || "本机"} · 最近活动 {session.lastSeenAt ? new Date(session.lastSeenAt).toLocaleString("zh-CN", { hour12: false }) : "未知"} · 风险 {riskLevelLabel(session.riskLevel)}({session.riskScore})
{session.status === "active" &&
revokeSession(session)} disabled={busy}>撤销}
)}{!sessionsLoading && !visibleSessions.length &&
当前没有可显示的登录会话。
}
刷新设备} />
{devices.filter((device) => device.status === "trusted").length}信任设备
{devices.filter((device) => device.latestRiskLevel === "high").length}高风险设备
{devices.reduce((count, device) => count + Number(device.activeSessionCount || 0), 0)}有效设备会话
{devices.map((device) =>
{device.label || "浏览器设备"}{device.userAgent || "本地客户端"}最近 IP {device.lastIpAddress || "本机"} · 最近活动 {device.lastSeenAt ? new Date(device.lastSeenAt).toLocaleString("zh-CN", { hour12: false }) : "未知"} · 首次登记 {device.firstSeenAt ? new Date(device.firstSeenAt).toLocaleString("zh-CN", { hour12: false }) : "未知"}
{device.activeSessionCount || 0} 个有效会话
{device.status === "trusted" ? setDeviceTrust(device, false)} disabled={busy}>取消信任 : device.status !== "revoked" && setDeviceTrust(device, true)} disabled={busy}>设为信任}
)}{!devicesLoading && !devices.length &&
登录后会自动登记当前浏览器设备。
}
刷新} />
{securityEvents.map((event) =>
{formatAdminDate(event.createdAt)}
{securityEventLabel(event.eventType)}{securityEventSummary(event)}
{event.ipAddress || "本机"}
)}{!securityEventsLoading && !securityEvents.length &&
还没有可显示的账号安全事件。
}
{invitations.map((invitation) =>
{invitation.organization_name}{invitation.workspace_name || "组织级"} · {invitation.role_name || invitation.role_key}{invitation.inviter_name || "组织管理员"} 邀请 · 有效至 {new Date(invitation.expires_at).toLocaleString("zh-CN", { hour12: false })}
accept(invitation)} disabled={busy}>接受邀请)}{!loading && !invitations.length &&
当前邮箱没有待接受邀请。
}
);
}
function StatusBadge({ status, label }) {
return {label || status};
}
function UsageTrendCard({ rows }) {
const trendRows = Array.isArray(rows) ? rows : [];
const totals = trendRows.reduce((summary, row) => ({
events: summary.events + Number(row.events || 0),
units: summary.units + Number(row.units || 0),
estimatedCost: summary.estimatedCost + Number(row.estimatedCost || 0)
}), { events: 0, units: 0, estimatedCost: 0 });
const peak = trendRows.reduce((current, row) => Number(row.estimatedCost || 0) > Number(current?.estimatedCost || 0) ? row : current, trendRows[0] || null);
const maxCost = Math.max(1, ...trendRows.map((row) => Number(row.estimatedCost || 0)));
return (
} />
{!trendRows.length ? 最近 31 天还没有可展示的用量事件。
: <>
区间事件{totals.events}
区间单位{totals.units}
区间成本{totals.estimatedCost.toFixed(2)} CNY
峰值日期{peak?.day?.slice(5) || "—"}
{trendRows.map((row) => {
const estimatedCost = Number(row.estimatedCost || 0);
const barHeight = estimatedCost ? Math.max(8, Math.round((estimatedCost / maxCost) * 100)) : 3;
return
{row.day?.slice(5) || "—"}{estimatedCost.toFixed(0)};
})}
柱高表示日计量成本(CNY)峰值 {peak?.day || "—"} · {Number(peak?.estimatedCost || 0).toFixed(2)} CNY
>}
);
}
export function AdminOverviewPage({ platformContext, setActiveTab }) {
const platform = platformContext?.platform || {};
const summary = platformContext?.summary || {};
const permissions = new Set(platformContext?.context?.permissions || []);
const canViewSystem = Boolean(platformContext?.context?.systemAdmin) || permissions.has("system:settings:view");
const canManageQueue = permissions.has("queue:manage");
const runners = platform.runnerHealth || [];
const members = platform.members || [];
const projects = platform.projects || [];
const usage = platform.usage || {};
const attention = runners.filter((runner) => runner.status !== "ready").length;
return (
window.location.reload()}>刷新概览} />
setActiveTab("admin-queue")}>打开队列} />
{runners.map((runner) =>
{runner.name}{runner.lastHeartbeat || "local"}
queue {runner.queueDepth})}
外部云连接已禁用符合 local-runner-only 策略
{summary.queueDepth || 0} 个任务排队可以调整队列并发或优先级
{canViewSystem && setActiveTab("system-generation")}>查看系统策略}
setActiveTab("admin-usage")}>成本中心} />
{usage.totalCost ?? 86}本月本地计量成本
{(usage.quotas || []).slice(0, 3).map((quota) => {quota.metric}{quota.used_value}/{quota.limit_value} {quota.unit}
)}
);
}
export function AdminUsagePage({ contextOverrides, platformContext }) {
const organizationId = contextOverrides?.organizationId;
const permissions = new Set(platformContext?.context?.permissions || []);
const canManagePlan = permissions.has("billing:manage");
const canManageQuota = permissions.has("quota:manage");
const [data, setData] = useState({ billing: null, seat: {}, quotas: [], usage: { usage: [] } });
const [usageData, setUsageData] = useState({ items: [], summary: { eventCount: 0, totalUnits: 0, totalCost: 0, byKind: [] }, pagination: { page: 1, pageSize: 25, total: 0, totalPages: 0 }, facets: { workspaces: [], projects: [], users: [], kinds: [], costCenters: [] } });
const [invoiceData, setInvoiceData] = useState({ invoices: [], summary: { count: 0, amount: 0, draft: 0, issued: 0, paid: 0, overdue: 0, void: 0 }, pagination: { page: 1, pageSize: 8, total: 0, totalPages: 0 } });
const [invoiceFilters, setInvoiceFilters] = useState({ status: "", query: "", page: 1, pageSize: 8 });
const [invoiceDetail, setInvoiceDetail] = useState(null);
const [usageFilters, setUsageFilters] = useState(() => {
const today = new Date();
const start = new Date(today.getFullYear(), today.getMonth(), 1);
return { from: formatDateInputValue(start), to: formatDateInputValue(today), workspaceId: "", projectId: "", userId: "", kind: "", costCenter: "", query: "", page: 1, pageSize: 25 };
});
const [planForm, setPlanForm] = useState({ planName: "", billingCycle: "monthly", currency: "CNY", baseFee: 0, seatUnitPrice: 0, storageUnitPrice: 0, clipUnitPrice: 0, seatLimit: 0, storageGb: 0, monthlyClipQuota: 0, quotaWarningPercent: 80, localRunnerOnly: true, cloudConnectorsRequireApproval: true });
const [quotaDrafts, setQuotaDrafts] = useState({});
const [costCenterDrafts, setCostCenterDrafts] = useState({});
const [loading, setLoading] = useState(true);
const [usageLoading, setUsageLoading] = useState(false);
const [busy, setBusy] = useState("");
const [notice, setNotice] = useState("");
async function loadUsage(nextFilters = usageFilters) {
if (!organizationId) return;
setUsageLoading(true);
try {
const result = await fetchOrganizationUsage(organizationId, nextFilters, contextOverrides);
setUsageData(result);
} catch (error) {
setNotice(error.message);
} finally {
setUsageLoading(false);
}
}
async function loadInvoices(nextFilters = invoiceFilters) {
if (!organizationId) return;
try {
const result = await fetchOrganizationInvoices(organizationId, nextFilters, contextOverrides);
setInvoiceData(result);
} catch (error) {
setNotice(error.message);
}
}
async function load() {
setLoading(true);
try {
const [result, usageResult, invoiceResult] = await Promise.all([
fetchOrganizationCommercial(organizationId, contextOverrides),
fetchOrganizationUsage(organizationId, usageFilters, contextOverrides),
fetchOrganizationInvoices(organizationId, invoiceFilters, contextOverrides)
]);
setData(result);
setUsageData(usageResult);
setInvoiceData(invoiceResult);
setInvoiceDetail(null);
setPlanForm({
planName: result.billing?.plan_name || "",
billingCycle: result.billing?.billing_cycle || "monthly",
currency: result.billing?.currency || "CNY",
baseFee: Number(result.billing?.base_fee || 0),
seatUnitPrice: Number(result.billing?.seat_unit_price || 0),
storageUnitPrice: Number(result.billing?.storage_unit_price || 0),
clipUnitPrice: Number(result.billing?.clip_unit_price || 0),
seatLimit: Number(result.billing?.seat_limit || 0),
storageGb: Number(result.billing?.storage_gb || 0),
monthlyClipQuota: Number(result.billing?.monthly_clip_quota || 0),
quotaWarningPercent: Number(result.billing?.quota_warning_percent || 80),
localRunnerOnly: Boolean(result.billing?.local_runner_only),
cloudConnectorsRequireApproval: Boolean(result.billing?.cloud_connectors_require_approval)
});
setQuotaDrafts(Object.fromEntries((result.quotas || []).map((quota) => [quota.id, quota.limitValue])));
setCostCenterDrafts(Object.fromEntries((result.costCenters || []).map((center) => [center.id, center.monthly_budget])));
setNotice("");
} catch (error) {
setNotice(error.message);
} finally {
setLoading(false);
}
}
useEffect(() => { if (organizationId) load(); }, [organizationId]);
async function savePlan(event) {
event.preventDefault();
if (!canManagePlan) return;
setBusy("plan");
try {
const result = await updateOrganizationBilling(organizationId, planForm, contextOverrides);
setData(result);
setNotice("套餐与组织策略已保存,并已写入审计日志");
} catch (error) {
setNotice(error.message);
} finally {
setBusy("");
}
}
async function generateInvoice() {
if (!canManagePlan) return;
setBusy("invoice-generate");
try {
const result = await generateOrganizationInvoice(organizationId, { dueDays: 30, taxRate: 0 }, contextOverrides);
setInvoiceDetail(result);
await loadInvoices();
setNotice(result.idempotent ? "当前账期账单已存在,未重复生成" : "当前账期账单已生成草稿,并已写入审计日志");
} catch (error) {
setNotice(error.message);
} finally {
setBusy("");
}
}
async function openInvoice(invoiceId) {
setBusy(`invoice-${invoiceId}`);
try {
const result = await fetchOrganizationInvoice(organizationId, invoiceId, contextOverrides);
setInvoiceDetail(result);
} catch (error) {
setNotice(error.message);
} finally {
setBusy("");
}
}
async function changeInvoiceStatus(status) {
if (!canManagePlan || !invoiceDetail?.invoice?.id) return;
setBusy(`invoice-status-${status}`);
try {
const result = await updateOrganizationInvoiceStatus(organizationId, invoiceDetail.invoice.id, status, contextOverrides);
setInvoiceDetail(result);
await loadInvoices();
setNotice(`账单已变更为${result.invoice.statusLabel}`);
} catch (error) {
setNotice(error.message);
} finally {
setBusy("");
}
}
async function exportInvoices(format) {
try {
const filters = { ...invoiceFilters, page: 1, pageSize: 10000 };
if (format === "csv") {
const result = await exportOrganizationInvoicesCsv(organizationId, filters, contextOverrides);
downloadBrowserBlob(`invoices-${organizationId}-${new Date().toISOString().slice(0, 10)}.csv`, result.blob);
setNotice("账单台账已导出为 CSV");
} else {
const result = await exportOrganizationInvoices(organizationId, filters, contextOverrides);
downloadJson(`invoices-${organizationId}-${new Date().toISOString().slice(0, 10)}.json`, result);
setNotice("账单台账已导出为 JSON");
}
} catch (error) {
setNotice(error.message);
}
}
function submitInvoiceFilters(event) {
event.preventDefault();
const nextFilters = { ...invoiceFilters, page: 1 };
setInvoiceFilters(nextFilters);
loadInvoices(nextFilters);
}
function changeInvoicePage(page) {
const nextFilters = { ...invoiceFilters, page };
setInvoiceFilters(nextFilters);
loadInvoices(nextFilters);
}
async function saveQuota(quota) {
if (!canManageQuota) return;
const limitValue = Number(quotaDrafts[quota.id]);
setBusy(quota.id);
try {
const result = await updateQuotaAllocation(organizationId, quota.id, { limitValue }, contextOverrides);
setData(result);
setQuotaDrafts(Object.fromEntries((result.quotas || []).map((item) => [item.id, item.limitValue])));
setNotice(`${quota.workspaceName} / ${quota.metric} 配额已更新`);
} catch (error) {
setNotice(error.message);
} finally {
setBusy("");
}
}
async function saveCostCenter(center) {
if (!canManagePlan) return;
setBusy(center.id);
try {
const result = await updateCostCenter(organizationId, center.id, { monthlyBudget: Number(costCenterDrafts[center.id]) }, contextOverrides);
setData(result);
setCostCenterDrafts(Object.fromEntries((result.costCenters || []).map((item) => [item.id, item.monthly_budget])));
setNotice(`${center.name} 预算已更新`);
} catch (error) {
setNotice(error.message);
} finally {
setBusy("");
}
}
async function exportCommercial() {
try {
const result = await exportOrganizationCommercial(organizationId, contextOverrides);
downloadJson(`commercial-${organizationId}-${new Date().toISOString().slice(0, 10)}.json`, result);
setNotice("商业运营数据已导出为本地 JSON");
} catch (error) {
setNotice(error.message);
}
}
async function exportUsage(format) {
try {
const filters = { ...usageFilters, page: 1, pageSize: 10000 };
if (format === "csv") {
const result = await exportOrganizationUsageCsv(organizationId, filters, contextOverrides);
downloadBrowserBlob(`usage-${organizationId}-${new Date().toISOString().slice(0, 10)}.csv`, result.blob);
setNotice("用量明细已导出为 CSV");
} else {
const result = await exportOrganizationUsage(organizationId, filters, contextOverrides);
downloadJson(`usage-${organizationId}-${new Date().toISOString().slice(0, 10)}.json`, result);
setNotice("用量明细已导出为 JSON");
}
} catch (error) {
setNotice(error.message);
}
}
function submitUsageFilters(event) {
event.preventDefault();
const nextFilters = { ...usageFilters, page: 1 };
setUsageFilters(nextFilters);
loadUsage(nextFilters);
}
function resetUsageFilters() {
const today = new Date();
const start = new Date(today.getFullYear(), today.getMonth(), 1);
const nextFilters = { from: formatDateInputValue(start), to: formatDateInputValue(today), workspaceId: "", projectId: "", userId: "", kind: "", costCenter: "", query: "", page: 1, pageSize: 25 };
setUsageFilters(nextFilters);
loadUsage(nextFilters);
}
function changeUsagePage(page) {
const nextFilters = { ...usageFilters, page };
setUsageFilters(nextFilters);
loadUsage(nextFilters);
}
function focusUsageByCostCenter(costCenter) {
const nextFilters = { ...usageFilters, costCenter, page: 1 };
setUsageFilters(nextFilters);
loadUsage(nextFilters);
document.getElementById("usage-detail-card")?.scrollIntoView({ behavior: "smooth", block: "start" });
}
function focusUsageByWorkspace(workspaceId) {
if (!workspaceId) return;
const nextFilters = { ...usageFilters, workspaceId, projectId: "", page: 1 };
setUsageFilters(nextFilters);
loadUsage(nextFilters);
document.getElementById("usage-detail-card")?.scrollIntoView({ behavior: "smooth", block: "start" });
}
const seat = data.seat || {};
const clipQuota = (data.quotas || []).filter((quota) => quota.metric === "clip").reduce((sum, quota) => sum + quota.usedValue, 0);
const clipLimit = Number(data.billing?.monthly_clip_quota || 0);
const storageQuota = (data.quotas || []).filter((quota) => quota.metric === "storage").reduce((max, quota) => Math.max(max, quota.usedValue), 0);
const storageLimit = Number(data.billing?.storage_gb || 0);
const usagePagination = usageData.pagination || {};
const usageTotal = Number(usagePagination.total || 0);
const usagePage = Number(usagePagination.page || 1);
const usagePageSize = Number(usagePagination.pageSize || 25);
const usageStart = usageTotal ? (usagePage - 1) * usagePageSize + 1 : 0;
const usageEnd = Math.min(usagePage * usagePageSize, usageTotal);
const usageFacets = usageData.facets || { workspaces: [], projects: [], users: [], kinds: [], costCenters: [] };
const invoicePagination = invoiceData.pagination || {};
const invoiceTotal = Number(invoicePagination.total || 0);
const invoicePage = Number(invoicePagination.page || 1);
const invoicePageSize = Number(invoicePagination.pageSize || 8);
const invoiceStart = invoiceTotal ? (invoicePage - 1) * invoicePageSize + 1 : 0;
const invoiceEnd = Math.min(invoicePage * invoicePageSize, invoiceTotal);
return (
} />
{notice && {notice}
}
{loading ? 读取组织商业运营数据…
: <>
.85 ? "warn" : "ok"} />
.85 ? "warn" : "ok"} />
已占用{seat.active || 0}
待入组{seat.pending || 0}
可用{seat.remaining || 0}
新邀请和 SSO/SCIM 入组都会经过席位检查;达到上限时由后端返回 `409 seat_limit_reached`。
工作区指标已用上限剩余周期操作
{(data.quotas || []).map((quota) =>
{quota.workspaceName}{quota.metric === "clip" ? "生成片段" : quota.metric === "storage" ? "存储" : quota.metric}{quota.usedValue} {quota.unit}{quota.remainingValue} {quota.unit}{String(quota.period_end || "").slice(0, 10)} saveQuota(quota)} disabled={!canManageQuota || busy === quota.id}>{busy === quota.id ? "保存中" : "保存"}
)}{!(data.quotas || []).length &&
当前组织还没有工作区配额。
}
{(data.quotaWarnings || []).map((warning) =>
focusUsageByWorkspace(warning.workspaceId)}>{warning.workspaceName || "组织"} · {warning.label})}{!(data.quotaWarnings || []).length &&
当前没有达到预警阈值的额度。
}
{(data.billingHistory || []).slice(0, 5).map((event) =>
{formatAdminDate(event.created_at)}
{event.event_type}{event.actor_name || "system"}
)}{!(data.billingHistory || []).length &&
还没有套餐变更记录。
}
成本中心本月使用月度预算剩余利用率操作
{(data.costCenters || []).map((center) =>
{center.name}{center.used} {center.currency}{center.remaining} {center.currency}= 100 ? "failed" : center.utilization >= 80 ? "needs-evidence" : "active"} label={`${center.utilization}%`} /> saveCostCenter(center)} disabled={!canManagePlan || busy === center.id}>{busy === center.id ? "保存中" : "保存"}
)}{!(data.costCenters || []).length &&
当前组织没有成本中心。
}
{(data.costCenterDetail || []).slice(0, 8).map((item) =>
{item.code}{item.workspaceName}
{item.units}{item.cost} CNY · {item.events} 事件 focusUsageByCostCenter(item.code)}>查看明细)}
exportInvoices("json")}>导出 JSON exportInvoices("csv")}>导出 CSV{canManagePlan && {busy === "invoice-generate" ? "生成中…" : "生成当前账期"}}} />
账单总数{invoiceData.summary?.count || 0}
草稿{invoiceData.summary?.draft || 0}
待收金额{Number((invoiceData.summary?.issued || 0) + (invoiceData.summary?.overdue || 0))} 张
非作废金额{Number(invoiceData.summary?.amount || 0).toFixed(2)} {data.billing?.currency || "CNY"}
账单号账期状态金额到期日操作
{invoiceData.invoices.map((invoice) =>
openInvoice(invoice.id)}>{invoice.invoiceNumber}{invoice.billingCycle} · {invoice.currency}{String(invoice.periodStart || "").slice(0, 10)} ~ {String(invoice.periodEnd || "").slice(0, 10)}{Number(invoice.totalAmount || 0).toFixed(2)} {invoice.currency}{String(invoice.dueAt || "").slice(0, 10) || "—"} openInvoice(invoice.id)} disabled={busy === `invoice-${invoice.id}`}>详情 )}{!invoiceData.invoices.length &&
当前组织还没有账单。管理员可以生成当前账期草稿。
}
显示 {invoiceStart}-{invoiceEnd} / {invoiceTotal} changeInvoicePage(invoicePage - 1)} disabled={invoicePage <= 1}> changeInvoicePage(invoicePage + 1)} disabled={invoicePage >= (invoicePagination.totalPages || 0)}>
{invoiceDetail?.invoice && setInvoiceDetail(null)}>} />账期{String(invoiceDetail.invoice.periodStart || "").slice(0, 10)} ~ {String(invoiceDetail.invoice.periodEnd || "").slice(0, 10)}
小计{Number(invoiceDetail.invoice.subtotal || 0).toFixed(2)} {invoiceDetail.invoice.currency}
税额{Number(invoiceDetail.invoice.taxAmount || 0).toFixed(2)} {invoiceDetail.invoice.currency}
应付合计{Number(invoiceDetail.invoice.totalAmount || 0).toFixed(2)} {invoiceDetail.invoice.currency}
{(invoiceDetail.lines || []).map((line) =>
{line.description}{line.quantity} {line.unitName} · 单价 {Number(line.unitPrice || 0).toFixed(2)} {invoiceDetail.invoice.currency}
{Number(line.amount || 0).toFixed(2)} {invoiceDetail.invoice.currency})}
{canManagePlan && invoiceDetail.invoice.status === "draft" && <> changeInvoiceStatus("issued")} disabled={busy === "invoice-status-issued"}>开票 changeInvoiceStatus("void")} disabled={busy === "invoice-status-void"}>作废>}{canManagePlan && invoiceDetail.invoice.status === "issued" && <> changeInvoiceStatus("paid")} disabled={busy === "invoice-status-paid"}>标记已支付 changeInvoiceStatus("overdue")} disabled={busy === "invoice-status-overdue"}>标记逾期 changeInvoiceStatus("void")} disabled={busy === "invoice-status-void"}>作废>}{canManagePlan && invoiceDetail.invoice.status === "overdue" && <> changeInvoiceStatus("paid")} disabled={busy === "invoice-status-paid"}>补记已支付 changeInvoiceStatus("void")} disabled={busy === "invoice-status-void"}>作废>}
}
exportUsage("json")}>导出 JSON exportUsage("csv")}>导出 CSV} />
事件数{usageData.summary?.eventCount || 0}
计量单位{usageData.summary?.totalUnits || 0}
估算成本{Number(usageData.summary?.totalCost || 0).toFixed(2)} CNY
当前页{usagePage}/{usagePagination.totalPages || 0}
发生时间事件工作区 / 项目操作者计量成本元数据
{usageLoading &&
正在读取用量明细…
}
{!usageLoading && usageData.items.map((item) =>
{formatAdminDate(item.createdAt)}{item.kind}{item.unitName}
{item.workspaceName}{item.projectName}
{item.userName}{item.userEmail || item.userId || "系统"}
{item.units} {item.unitName}{Number(item.estimatedCost || 0).toFixed(2)} CNY{item.costCenter} · {Object.keys(item.metadata || {}).length ? JSON.stringify(item.metadata).slice(0, 76) : "—"} )}
{!usageLoading && !usageData.items.length &&
当前筛选条件没有用量事件。
}
显示 {usageStart}-{usageEnd} / {usageTotal} changeUsagePage(usagePage - 1)} disabled={usageLoading || usagePage <= 1}> changeUsagePage(usagePage + 1)} disabled={usageLoading || usagePage >= (usagePagination.totalPages || 0)}>
本地 Runner 优先
外部连接器审批
席位预留校验
服务端配额阻断
{(usageData.summary?.byKind || []).slice(0, 6).map((item) =>
{item.kind}{item.events} 事件 · {item.unitName}
{item.units}{Number(item.estimatedCost || 0).toFixed(2)} CNY)}{!(usageData.summary?.byKind || []).length &&
暂无分类汇总
}
>}
);
}
export function AdminQueuePage({ platformContext, contextOverrides }) {
const [queue, setQueue] = useState({ jobs: [], runners: [], summary: {} });
const [workerInfo, setWorkerInfo] = useState(null);
const [loading, setLoading] = useState(true);
const [filter, setFilter] = useState("all");
const [notice, setNotice] = useState("");
const [selectedJob, setSelectedJob] = useState(null);
const [detailLoading, setDetailLoading] = useState(false);
const [selectedJobIds, setSelectedJobIds] = useState([]);
const canManageRunners = Boolean(platformContext?.context?.systemAdmin || platformContext?.context?.permissions?.includes("runner:manage"));
async function load() {
setLoading(true);
try {
const [nextQueue, nextWorker] = await Promise.all([fetchAdminQueue(contextOverrides), fetchWorkerStatus(contextOverrides)]);
setQueue(nextQueue);
setWorkerInfo(nextWorker.worker || null);
setNotice("");
} catch (error) {
setNotice(error.message);
} finally {
setLoading(false);
}
}
useEffect(() => { load(); }, [contextOverrides.organizationId, contextOverrides.workspaceId]);
const jobs = useMemo(() => filter === "all" ? queue.jobs : queue.jobs.filter((job) => job.status === filter), [filter, queue.jobs]);
function jobContext(job) {
return { ...contextOverrides, projectId: job.project_id || contextOverrides.projectId };
}
async function showJobDetail(job) {
setDetailLoading(true);
try {
const result = await fetchJob(job.id, jobContext(job));
setSelectedJob(result.job);
} catch (error) {
setNotice(error.message);
} finally {
setDetailLoading(false);
}
}
async function runJobAction(job, action, body = {}) {
try {
const scoped = jobContext(job);
if (action === "run") {
const approveExternal = job.cost_mode !== "local" && Boolean(job.approval_required);
const result = await runJob(job.id, { approveExternal }, scoped);
setQueue((current) => ({ ...current, jobs: current.jobs.map((item) => item.id === job.id ? result.job : item) }));
setNotice(`任务 ${job.id} ${approveExternal ? "已批准并提交执行" : "已提交执行"},状态:${result.job.status}`);
setSelectedJob(result.job);
return;
}
const result = action === "retry"
? await retryJob(job.id, scoped)
: action === "cancel"
? await cancelJob(job.id, scoped)
: await updateJobPriority(job.id, body.priority, scoped);
setQueue((current) => ({ ...current, jobs: current.jobs.map((item) => item.id === job.id ? { ...item, ...result.job } : item) }));
setSelectedJob((current) => current?.id === job.id ? { ...current, ...result.job } : current);
setNotice(action === "retry" ? `任务 ${job.id} 已重新排队` : action === "cancel" ? `任务 ${job.id} 已取消` : `任务 ${job.id} 优先级已调整为 ${body.priority}`);
} catch (error) {
setNotice(error.message);
}
}
async function runRunnerAction(runner, action) {
try {
const result = await operateRunner(runner.serviceKey || runner.id, action, contextOverrides);
setQueue((current) => ({ ...current, runners: result.runners || current.runners }));
setNotice(`${runner.name} 已记录“${action === "pause" ? "暂停" : action === "resume" ? "恢复" : action === "drain" ? "排空" : action === "restart" ? "重启请求" : "心跳刷新"}”操作`);
} catch (error) {
setNotice(error.message);
}
}
async function dispatchWorkerNow() {
try {
const result = await dispatchWorker(contextOverrides);
setWorkerInfo(result.worker || null);
await load();
setNotice(result.worker?.result?.jobId ? `Worker 已领取任务 ${result.worker.result.jobId}` : "Worker 已检查队列,目前没有可自动执行的本地任务");
} catch (error) {
setNotice(error.message);
}
}
async function runBatchAction(action) {
if (!selectedJobIds.length) return;
try {
const result = await batchQueueAction({ action, jobIds: selectedJobIds }, contextOverrides);
setSelectedJobIds([]);
await load();
setNotice(`批量${action === "retry" ? "重试" : action === "cancel" ? "取消" : "调整"}完成:${result.successCount} 成功,${result.failureCount} 失败`);
} catch (error) {
setNotice(error.message);
}
}
return (
} />
{notice && }
本地后台 Worker{workerInfo?.workerId || "local-worker"} · 并发 {workerInfo?.maxConcurrency || 0} · 轮询 {workerInfo?.pollMs || 0}ms
可自动领取{workerInfo?.queueDepth ?? 0} 个本地就绪任务
最近心跳{workerInfo?.lastHeartbeatAt ? workerInfo.lastHeartbeatAt.replace("T", " ").slice(0, 19) : "等待启动"}
队列告警{workerInfo?.queueAlert?.level === "critical" ? "严重" : workerInfo?.queueAlert?.level === "warning" ? "预警" : "正常"}
{selectedJobIds.length > 0 && <> runBatchAction("retry")}>批量重试 runBatchAction("cancel")}>批量取消>}} />
选择任务项目 / 镜头适配器优先级尝试状态创建者操作
{jobs.length ? jobs.map((job) =>
{job.kind}{job.project_name} · {job.shot_id || "全局"}{job.adapter_id}{job.attempts}{job.creator_name || job.created_by} showJobDetail(job)}>日志{["queued", "blocked", "failed", "cancelled"].includes(job.status) &&
runJobAction(job, "run")}>{job.cost_mode !== "local" && job.approval_required ? "批准并执行" : "执行"}}{["failed", "cancelled"].includes(job.status) &&
runJobAction(job, "retry")}>重试}{["queued", "running"].includes(job.status) &&
runJobAction(job, "cancel")}>取消}
) :
{loading ? "正在读取本地队列…" : "当前工作区没有任务。"}
}
{selectedJob && event.target === event.currentTarget && setSelectedJob(null)}>
EXECUTION LOG
{selectedJob.kind} · {selectedJob.id}
setSelectedJob(null)} title="关闭执行日志" aria-label="关闭执行日志"> {detailLoading ? 读取执行日志…
: <>项目{selectedJob.project_name || selectedJob.project_id}
状态
适配器{selectedJob.adapter_id}
尝试次数{selectedJob.attempts || selectedJob.attemptLog?.length || 0}
{selectedJob.attemptLog?.map((attempt) =>
Attempt {attempt.attempt_number}{attempt.runner_id}{attempt.started_at || attempt.created_at} {attempt.finished_at ? `→ ${attempt.finished_at}` : ""}{attempt.error_message &&
{attempt.error_message}
}
) ||
没有执行尝试记录。
}
请求合同{JSON.stringify(selectedJob.request || {}, null, 2)} 结果证据{JSON.stringify(selectedJob.result || {}, null, 2)} >} }
{(queue.runners || []).map((runner) =>
{runner.name}
queue {runner.queueDepth}
{runner.lastHeartbeat}{canManageRunners &&
runRunnerAction(runner, runner.status === "paused" || runner.status === "draining" ? "resume" : "pause")}>{runner.status === "paused" || runner.status === "draining" ? "恢复" : "暂停"} runRunnerAction(runner, "restart")}>重启 runRunnerAction(runner, "heartbeat")}>心跳 }
)}
);
}
function emptyModelConnectorForm() {
return { label: "", endpoint: "http://127.0.0.1:7860", kind: "http-json", capability: "image-to-video", costMode: "local", approvalRequired: false, authEnv: "", protocolJson: "" };
}
function modelConnectorForm(model) {
return {
label: model.label || "",
endpoint: model.endpoint || "",
kind: model.kind || "http-json",
capability: (model.capability || []).join(", "),
costMode: model.costMode || model.cost_mode || "local",
approvalRequired: Boolean(model.approvalRequired ?? model.approval_required),
authEnv: model.auth_env || "",
protocolJson: JSON.stringify(model.protocol || {}, null, 2)
};
}
function ModelConnectorFields({ form, setForm, widePrefix = "" }) {
const external = form.costMode !== "local";
return <>
>;
}
export function AdminModelsPage({ platformContext, contextOverrides, onModelsChanged }) {
const platform = platformContext?.platform || {};
const [models, setModels] = useState(platform.modelRegistry || []);
const [form, setForm] = useState(emptyModelConnectorForm);
const [editingModel, setEditingModel] = useState(null);
const [editForm, setEditForm] = useState(emptyModelConnectorForm);
const [notice, setNotice] = useState("");
useEffect(() => setModels(platform.modelRegistry || []), [platform.modelRegistry]);
function syncModels(nextModels) {
setModels(nextModels);
onModelsChanged?.(nextModels);
}
async function submit(event) {
event.preventDefault();
try {
let protocol = {};
if (form.protocolJson.trim()) {
try { protocol = JSON.parse(form.protocolJson); } catch { setNotice("协议配置 JSON 无效,请检查括号和引号"); return; }
}
const result = await registerModel({ ...form, authEnv: form.authEnv.trim(), protocol, capability: form.capability.split(",").map((item) => item.trim()).filter(Boolean) }, contextOverrides);
syncModels(result.models || [result.model, ...models]);
setForm((current) => ({ ...current, label: "" }));
setNotice("模型连接器已写入当前组织");
} catch (error) {
setNotice(error.message);
}
}
function openEditor(model) {
setEditingModel(model);
setEditForm(modelConnectorForm(model));
setNotice("");
}
async function saveEdit(event) {
event.preventDefault();
if (!editingModel) return;
try {
let protocol = {};
if (editForm.protocolJson.trim()) {
try { protocol = JSON.parse(editForm.protocolJson); } catch { setNotice("协议配置 JSON 无效,请检查括号和引号"); return; }
}
const result = await updateModel(editingModel.id, { ...editForm, authEnv: editForm.authEnv.trim(), protocol, capability: editForm.capability.split(",").map((item) => item.trim()).filter(Boolean) }, contextOverrides);
const nextModels = models.map((item) => item.id === editingModel.id ? result.model : item);
syncModels(nextModels);
setEditingModel(null);
setNotice("模型连接器配置已更新,密钥仍只通过环境变量引用");
} catch (error) {
setNotice(error.message);
}
}
async function probe(model) {
try {
const result = await probeModel(model.id, contextOverrides);
syncModels(result.models || models);
setNotice(result.model.status === "ready" ? `${model.label} 探活成功${result.model.latency_ms ? `,${result.model.latency_ms}ms` : ""}` : `${model.label} 探活失败:${result.message || result.model.error_message || "连接器不可用"}`);
} catch (error) {
setNotice(error.message);
}
}
async function toggle(model) {
try {
const nextStatus = model.status === "paused" ? "ready" : "paused";
const result = await updateModel(model.id, { status: nextStatus }, contextOverrides);
const nextModels = models.map((item) => item.id === model.id ? result.model : item);
syncModels(nextModels);
setNotice(`${model.label} 已${nextStatus === "ready" ? "启用" : "暂停"}`);
} catch (error) {
setNotice(error.message);
}
}
return (
{notice &&
{notice}
}
} />
名称能力Endpoint成本审批状态操作
{models.map((model) =>
{model.label}{(model.capability || []).join(" · ")}{model.endpoint}{model.last_probe_at ? `最近探活 ${model.latency_ms || "-"}ms` : "尚未探活"}{model.costMode || model.cost_mode}{model.approvalRequired || model.approval_required ? "需要审批" : "无需"} openEditor(model)}>编辑 probe(model)}>检测{["ready", "paused"].includes(model.status) &&
toggle(model)}>{model.status === "paused" ? "启用" : "暂停"}}
)}
自有模型平台优先
单画面生成契约
云端节点调用
ComfyUI 桥接
{editingModel &&
event.target === event.currentTarget && setEditingModel(null)}>
MODEL CONNECTOR
编辑连接器 · {editingModel.label}
setEditingModel(null)} title="关闭编辑连接器" aria-label="关闭编辑连接器">修改会写入当前组织的连接器注册表。平台只保存环境变量名称,不会把 API Key 写入数据库。
}
);
}
const auditResultLabels = {
ok: "成功",
pass: "通过",
success: "成功",
failure: "失败",
error: "错误",
blocked: "已阻断",
"requires-approval": "需审批",
challenge: "挑战"
};
function auditResultLabel(result) {
return auditResultLabels[result] || result || "未知";
}
function auditScopeLabel(scope) {
if (scope?.mode === "global") return "全局审计视图";
if (scope?.mode === "organization") return "当前组织全部工作区";
if (scope?.mode === "project") return "当前项目及组织级事件";
return "当前工作区及组织级事件";
}
function auditInitialState(platformContext) {
const initial = platformContext?.platform?.auditLog || [];
return {
auditLog: initial,
pagination: { page: 1, pageSize: 25, total: initial.length, totalPages: 1, hasMore: false },
facets: { actions: [], targetTypes: [], results: [], actors: [] },
scope: { mode: platformContext?.context?.systemAdmin ? "global" : "organization" }
};
}
export function AdminAuditPage({ platformContext, contextOverrides }) {
const [filters, setFilters] = useState({ page: 1, pageSize: 25, query: "", action: "", targetType: "", targetId: "", result: "", actorUserId: "", from: "", to: "" });
const [draft, setDraft] = useState(filters);
const [data, setData] = useState(() => auditInitialState(platformContext));
const [loading, setLoading] = useState(false);
const [exporting, setExporting] = useState(false);
const [notice, setNotice] = useState("");
const [selectedId, setSelectedId] = useState("");
const [detail, setDetail] = useState(null);
const [detailLoading, setDetailLoading] = useState(false);
async function load(nextFilters = filters) {
setLoading(true);
setNotice("");
try {
const result = await fetchAuditEvents(nextFilters, contextOverrides);
setData(result);
setFilters(nextFilters);
setDraft(nextFilters);
} catch (error) {
setNotice(error.message || "审计记录读取失败");
} finally {
setLoading(false);
}
}
useEffect(() => {
load(filters);
}, []);
async function openDetail(id) {
setSelectedId(id);
setDetail(null);
setDetailLoading(true);
try {
setDetail(await fetchAuditEvent(id, contextOverrides));
} catch (error) {
setNotice(error.message || "审计详情读取失败");
} finally {
setDetailLoading(false);
}
}
async function handleExport() {
setExporting(true);
setNotice("");
try {
const result = await exportAuditEvents({ ...filters, pageSize: 5000 }, contextOverrides);
downloadJson(`audit-${new Date().toISOString().slice(0, 10)}.json`, result);
setNotice(`已导出 ${result.total ?? result.auditLog?.length ?? 0} 条审计记录`);
} catch (error) {
setNotice(error.message || "审计导出失败");
} finally {
setExporting(false);
}
}
function updateDraft(key, value) {
setDraft((current) => ({ ...current, [key]: value }));
}
function submitFilters(event) {
event.preventDefault();
load({ ...draft, page: 1 });
}
function clearFilters() {
const cleared = { page: 1, pageSize: filters.pageSize || 25, query: "", action: "", targetType: "", targetId: "", result: "", actorUserId: "", from: "", to: "" };
setDraft(cleared);
load(cleared);
}
const pagination = data.pagination || { page: 1, pageSize: 25, total: data.auditLog?.length || 0, totalPages: 1, hasMore: false };
const canGoPrevious = pagination.page > 1;
const canGoNext = Boolean(pagination.hasMore);
const events = data.auditLog || [];
const selectedEvent = detail?.event;
return
load(filters)} disabled={loading}>{loading ? "读取中…" : "刷新"}{exporting ? "导出中…" : "导出 JSON"} } />
{notice && {notice}
}
} />
{events.map((item) =>
openDetail(item.id)}>
{formatAdminDate(item.created_at)}
{item.action}{item.target_type} · {item.target_id}{item.organization_name || item.organizationName || ""}{item.workspace_name ? ` / ${item.workspace_name}` : ""}{item.project_name ? ` / ${item.project_name}` : ""}
{item.actor_name || item.actorName || item.actor_user_id || "system"}{item.actor_email || ""}
)}
{!loading && !events.length &&
没有匹配的审计事件。
}
{loading &&
正在读取审计事件…
}
显示 {(pagination.page - 1) * pagination.pageSize + (events.length ? 1 : 0)}-{Math.min(pagination.page * pagination.pageSize, pagination.total)} / {pagination.total} load({ ...filters, page: pagination.page - 1 })} disabled={!canGoPrevious || loading}> load({ ...filters, page: pagination.page + 1 })} disabled={!canGoNext || loading}>
{selectedId &&
}
服务端分页与筛选
组织 / 系统管理员隔离
JSON / CSV 归档
;
}
function formatAdminDate(value) {
if (!value) return "—";
const date = new Date(value);
return Number.isNaN(date.getTime()) ? String(value) : date.toLocaleString("zh-CN", { hour12: false });
}
function downloadBrowserBlob(filename, blob) {
const url = URL.createObjectURL(blob);
const anchor = document.createElement("a");
anchor.href = url;
anchor.download = filename;
anchor.click();
window.setTimeout(() => URL.revokeObjectURL(url), 1000);
}
function formatDateInputValue(date) {
return [date.getFullYear(), String(date.getMonth() + 1).padStart(2, "0"), String(date.getDate()).padStart(2, "0")].join("-");
}
const securityEventLabels = {
"account.created": "账号创建",
"account.locked": "账号锁定",
"login.success": "登录成功",
"login.failure": "登录失败",
"login.blocked": "登录被阻断",
"mfa.challenge.created": "MFA 挑战",
"mfa.enrollment.challenge.created": "MFA 绑定挑战",
"mfa.challenge.success": "MFA 验证成功",
"mfa.challenge.failure": "MFA 验证失败",
"mfa.setup.started": "开始设置 MFA",
"mfa.setup.cancelled": "取消 MFA 设置",
"mfa.enabled": "启用 MFA",
"mfa.disabled": "关闭 MFA",
"mfa.reset": "管理员重置 MFA",
"session.created": "创建登录会话",
"session.revoked": "撤销登录会话",
"session.logout": "退出登录",
"password.changed": "修改密码",
"password.failure": "密码验证失败",
"password.reset": "管理员重置密码"
};
function securityEventLabel(eventType) {
return securityEventLabels[eventType] || eventType || "安全事件";
}
function securityResultTone(result) {
if (result === "success") return "active";
if (result === "challenge") return "needs-evidence";
if (result === "blocked") return "blocked";
return "failed";
}
function securityResultLabel(result) {
return result === "success" ? "成功" : result === "challenge" ? "挑战" : result === "blocked" ? "阻断" : "失败";
}
function securityEventSummary(event) {
const metadata = event?.metadata || {};
if (metadata.reason === "unknown_email") return `邮箱 ${metadata.email || "未识别"}`;
if (metadata.reason === "lockout_active" || metadata.reason === "failed_login_threshold") return metadata.lockedUntil ? `锁定至 ${formatAdminDate(metadata.lockedUntil)}` : "达到失败次数阈值";
if (metadata.reason === "invalid_code" || metadata.reason === "invalid_setup_code" || metadata.reason === "invalid_disable_code") return "验证码不正确";
if (metadata.reason === "current_password_invalid") return "当前密码不正确";
if (metadata.method) return `方式:${metadata.method}`;
if (metadata.authMethod) return `方式:${metadata.authMethod}`;
if (metadata.reason) return String(metadata.reason);
return event?.userAgent || "身份安全事件";
}
function riskBadgeStatus(level) {
return level === "high" ? "blocked" : level === "low" ? "active" : "neutral";
}
function riskLevelLabel(level) {
return level === "high" ? "高" : level === "low" ? "低" : "中";
}
function deviceStatusLabel(status) {
return status === "trusted" ? "信任设备" : status === "revoked" ? "已撤销" : "普通设备";
}
function systemUserStatusLabel(status) {
return status === "active" ? "正常" : status === "suspended" ? "已停用" : "待激活";
}
export function SystemUsersPage({ contextOverrides }) {
const [data, setData] = useState({ users: [], summary: {}, filters: {} });
const [queryInput, setQueryInput] = useState("");
const [query, setQuery] = useState("");
const [status, setStatus] = useState("");
const [selectedId, setSelectedId] = useState("");
const [detail, setDetail] = useState(null);
const [loading, setLoading] = useState(true);
const [detailLoading, setDetailLoading] = useState(false);
const [busyId, setBusyId] = useState("");
const [notice, setNotice] = useState("");
const [createForm, setCreateForm] = useState({ displayName: "", email: "", password: "", systemAdmin: false, organizationId: "", organizationRoleKey: "org_member", workspaceId: "", workspaceRoleKey: "writer", projectId: "", projectRoleKey: "project_editor" });
const [createOpen, setCreateOpen] = useState(false);
const [membershipForm, setMembershipForm] = useState({ organizationId: "", organizationRoleKey: "org_member", workspaceId: "", workspaceRoleKey: "writer", projectId: "", projectRoleKey: "project_editor" });
async function load(nextQuery = query, nextStatus = status) {
setLoading(true);
try {
const result = await fetchSystemUsers(contextOverrides, { query: nextQuery, status: nextStatus, limit: 160 });
setData(result);
setNotice("");
if (selectedId && !result.users.some((user) => user.id === selectedId)) {
setSelectedId("");
setDetail(null);
}
} catch (error) {
setNotice(error.message);
} finally {
setLoading(false);
}
}
async function openDetail(userId) {
setSelectedId(userId);
setDetailLoading(true);
try {
setDetail(await fetchSystemUser(userId, contextOverrides));
} catch (error) {
setNotice(error.message);
} finally {
setDetailLoading(false);
}
}
useEffect(() => { load("", ""); }, [contextOverrides?.userId, contextOverrides?.organizationId]);
async function submitSearch(event) {
event.preventDefault();
setQuery(queryInput.trim());
await load(queryInput.trim(), status);
}
async function setUserStatus(user) {
const nextStatus = user.status === "suspended" ? "active" : "suspended";
const action = nextStatus === "suspended" ? "停用" : "恢复";
if (!window.confirm(`确认${action}“${user.displayName}”吗?${nextStatus === "suspended" ? "停用后该用户的全部登录会话会立即失效。" : "恢复后需要重新登录。"}`)) return;
setBusyId(user.id);
try {
const result = await updateSystemUser(user.id, { status: nextStatus }, contextOverrides);
setDetail(result);
await load();
setNotice(`${user.displayName}已${action}${result.revokedSessionCount ? `,撤销 ${result.revokedSessionCount} 个会话` : ""}`);
} catch (error) {
setNotice(error.message);
} finally {
setBusyId("");
}
}
async function revokeSessions(user) {
if (!window.confirm(`确认撤销“${user.displayName}”的全部登录会话吗?`)) return;
setBusyId(user.id);
try {
const result = await revokeSystemUserSessions(user.id, contextOverrides);
setDetail(result);
await load();
setNotice(`已撤销 ${result.revokedSessionCount || 0} 个登录会话`);
} catch (error) {
setNotice(error.message);
} finally {
setBusyId("");
}
}
async function createUser(event) {
event.preventDefault();
setBusyId("create");
try {
const payload = { ...createForm };
if (!payload.password) delete payload.password;
if (!payload.organizationId) delete payload.organizationId;
if (!payload.workspaceId) delete payload.workspaceId;
if (!payload.projectId) delete payload.projectId;
const result = await createSystemUser(payload, contextOverrides);
await load();
setCreateForm({ displayName: "", email: "", password: "", systemAdmin: false, organizationId: "", organizationRoleKey: "org_member", workspaceId: "", workspaceRoleKey: "writer", projectId: "", projectRoleKey: "project_editor" });
setCreateOpen(false);
setNotice(result.temporaryPassword ? `账号已创建。一次性初始密码:${result.temporaryPassword},请立即交付给用户并要求登录后修改。` : "账号已创建并写入审计日志");
if (result.user?.id) await openDetail(result.user.id);
} catch (error) {
setNotice(error.message);
} finally {
setBusyId("");
}
}
async function resetPassword(user) {
if (!window.confirm(`确认重置“${user.displayName}”的密码吗?原有会话会全部失效。`)) return;
setBusyId(user.id);
try {
const result = await resetSystemUserPassword(user.id, {}, contextOverrides);
setDetail(result);
setNotice(result.temporaryPassword ? `密码已重置。一次性密码:${result.temporaryPassword}` : "密码已重置,用户需要重新登录。");
await load();
} catch (error) {
setNotice(error.message);
} finally {
setBusyId("");
}
}
async function resetMfa(user) {
if (!window.confirm(`确认清除“${user.displayName}”的 MFA 绑定吗?该用户下次登录需要重新绑定。`)) return;
setBusyId(user.id);
try {
const result = await resetSystemUserMfa(user.id, contextOverrides);
setDetail(result);
setNotice(`MFA 已重置,撤销 ${result.revokedSessionCount || 0} 个会话`);
await load();
} catch (error) {
setNotice(error.message);
} finally {
setBusyId("");
}
}
async function saveMemberships(event) {
event.preventDefault();
if (!detail?.user?.id) return;
setBusyId(detail.user.id);
try {
const payload = { ...membershipForm };
if (!payload.organizationId) delete payload.organizationId;
if (!payload.workspaceId) delete payload.workspaceId;
if (!payload.projectId) delete payload.projectId;
const result = await updateSystemUserMemberships(detail.user.id, payload, contextOverrides);
setDetail(result);
await load();
setNotice("组织、工作区或项目归属已更新");
} catch (error) {
setNotice(error.message);
} finally {
setBusyId("");
}
}
async function toggleSystemAdmin(user) {
const enabled = !user.systemAdmin;
if (!window.confirm(`确认${enabled ? "授予" : "移除"}“${user.displayName}”的系统管理员权限吗?`)) return;
setBusyId(user.id);
try {
const result = await updateSystemUser(user.id, { systemAdmin: enabled }, contextOverrides);
setDetail(result);
await load();
setNotice(`系统管理员权限已${enabled ? "授予" : "移除"}`);
} catch (error) {
setNotice(error.message);
} finally {
setBusyId("");
}
}
const users = data.users || [];
const organizations = [...new Map(users.flatMap((user) => user.organizations || []).map((item) => [item.id, item])).values()];
const workspaces = [...new Map(users.flatMap((user) => user.workspaces || []).map((item) => [item.id, item])).values()];
const projects = [...new Map(users.flatMap((user) => user.projects || []).map((item) => [item.id, item])).values()];
return (
load()} disabled={loading}>刷新目录}
/>
{notice && {notice}
}
setQueryInput(event.target.value)} placeholder="搜索姓名、邮箱或用户 ID" />
查询} />
{loading ? 读取全局用户目录…
: 用户组织归属工作区 / 项目身份安全登录活动状态操作
{users.map((user) =>
openDetail(user.id)}>
{user.displayName?.slice(0, 1) || "U"}{user.displayName}{user.email}{user.id}{user.systemAdmin ? " · 系统管理员" : ""}
{user.organizations.slice(0, 2).map((organization) => {organization.name} · {organization.role_name || organization.role_key})}{user.organizations.length > 2 && +{user.organizations.length - 2} 个组织}{!user.organizations.length && 未加入组织}
{user.workspaceCount} 工作区 · {user.projectCount} 项目{user.activeSessionCount} 个活跃会话
{formatAdminDate(user.lastLoginAt)}最近登录{formatAdminDate(user.lastSessionSeenAt)}
event.stopPropagation()}>
openDetail(user.id)}>详情{user.activeSessionCount > 0 &&
revokeSessions(user)} disabled={busyId === user.id}>撤销会话}{user.status !== "invited" &&
setUserStatus(user)} disabled={busyId === user.id}>{user.status === "suspended" ? : }{user.status === "suspended" ? "恢复" : "停用"}}
)}{!users.length &&
没有匹配的全局用户。
}
}
{selectedId && { setSelectedId(""); setDetail(null); }}>} />{detailLoading || !detail ? 读取用户详情…
: <>{detail.user.displayName?.slice(0, 1) || "U"}{detail.user.displayName}
{detail.user.email} · {detail.user.id}
创建于 {formatAdminDate(detail.user.createdAt)} · 最近登录 {formatAdminDate(detail.user.lastLoginAt)} resetPassword(detail.user)} disabled={busyId === detail.user.id}>重置密码 resetMfa(detail.user)} disabled={busyId === detail.user.id}>重置 MFA toggleSystemAdmin(detail.user)} disabled={busyId === detail.user.id}>{detail.user.systemAdmin ? "移除系统管理员" : "授予系统管理员"} revokeSessions(detail.user)} disabled={busyId === detail.user.id}>撤销全部会话{detail.user.status !== "invited" && setUserStatus(detail.user)} disabled={busyId === detail.user.id}>{detail.user.status === "suspended" ? : }{detail.user.status === "suspended" ? "恢复账号" : "停用账号"}}
{detail.user.organizations.map((organization) =>
{organization.name}{organization.role_name || organization.role_key}
)}{!detail.user.organizations.length &&
暂无组织归属
}
{detail.user.workspaces.slice(0, 8).map((workspace) =>
{workspace.organization_name} / {workspace.name}{workspace.role_name || workspace.role_key}
)}{!detail.user.workspaces.length &&
暂无工作区归属
}
{(detail.devices || []).slice(0, 8).map((device) =>
{device.label || "浏览器设备"}{device.lastIpAddress || "未知 IP"} · 最近活动 {formatAdminDate(device.lastSeenAt)} · {device.activeSessionCount || 0} 个有效会话
)}{!(detail.devices || []).length &&
暂无设备登记
}
session.status === "active" || session.status === "current").length} 个有效`} />{detail.sessions.slice(0, 8).map((session) => {session.current ? "当前查看会话" : session.device?.label || session.userAgent || "浏览器会话"}{session.ipAddress || "未知 IP"} · 最近活动 {formatAdminDate(session.lastSeenAt)} · 风险 {riskLevelLabel(session.riskLevel)}({session.riskScore})
)}{!detail.sessions.length && 暂无登录会话
} {(detail.securityEvents || []).slice(0, 8).map((event) =>
{securityEventLabel(event.eventType)}{formatAdminDate(event.createdAt)} · {securityEventSummary(event)}
)}{!(detail.securityEvents || []).length &&
暂无账号安全事件
}
{detail.recentAudit.slice(0, 8).map((audit) =>
{audit.action}{formatAdminDate(audit.created_at)} · {audit.actor_name || "system"}
)}{!detail.recentAudit.length &&
暂无相关事件
}
>}}
);
}
export function IdentityCenterPage({ contextOverrides }) {
const [data, setData] = useState({
policy: { passwordLoginEnabled: true, mfaRequiredForAdmins: false, mfaRequiredForAll: false, ssoEnabled: false, localLoginFallback: true, sessionTtlHours: 12, maxSessionsPerUser: 10 },
providers: [],
directorySyncs: [],
organizationOptions: [],
workspaceOptions: [],
summary: {}
});
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [notice, setNotice] = useState("");
const [providerForm, setProviderForm] = useState({ name: "企业 OIDC", kind: "oidc", organizationId: contextOverrides.organizationId || "", workspaceId: contextOverrides.workspaceId || "", issuerUrl: "", clientId: "", clientSecretRef: "OIDC_CLIENT_SECRET", scopes: "openid, profile, email", entryPoint: "", idpCertRef: "AI_DRAMA_SAML_IDP_CERT", spIssuer: "http://127.0.0.1:8787/api/auth/sso/saml/metadata", audience: "", samlNameIdFormat: "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", wantAssertionsSigned: true, wantAuthnResponseSigned: true, validateInResponseTo: "always", claimMapping: { email: "email", displayName: "name", externalId: "sub" }, autoProvision: true, defaultRoleKey: "org_member", defaultWorkspaceRoleKey: "writer", enabled: false });
const [directoryForm, setDirectoryForm] = useState({ name: "企业目录", syncMode: "provision-and-deprovision", schedule: "manual" });
const [tokenNotice, setTokenNotice] = useState("");
async function load() {
setLoading(true);
try {
setData(await fetchIdentityCenter(contextOverrides));
setNotice("");
} catch (error) {
setNotice(error.message);
} finally {
setLoading(false);
}
}
useEffect(() => { load(); }, [contextOverrides.organizationId]);
function setPolicy(key, value) {
setData((current) => ({ ...current, policy: { ...current.policy, [key]: value } }));
}
async function savePolicy() {
setSaving(true);
try {
const result = await updateIdentityPolicy(data.policy, contextOverrides);
setData(result);
setNotice("身份策略已保存,并已写入审计日志");
} catch (error) {
setNotice(error.message);
} finally {
setSaving(false);
}
}
async function submitProvider(event) {
event.preventDefault();
try {
const result = await createIdentityProvider({ ...providerForm, scopes: providerForm.scopes.split(",").map((item) => item.trim()).filter(Boolean) }, contextOverrides);
setData(result);
setNotice("身份提供商已登记;启用前请先完成探测");
setProviderForm((current) => ({ ...current, name: current.kind === "saml" ? "企业 SAML" : "企业 OIDC", issuerUrl: "", clientId: "", entryPoint: "", enabled: false }));
} catch (error) {
setNotice(error.message);
}
}
async function toggleProvider(provider) {
try {
const result = await updateIdentityProvider(provider.id, { enabled: !provider.enabled }, contextOverrides);
setData(result);
setNotice(`身份提供商“${provider.name}”已${provider.enabled ? "停用" : "启用"}`);
} catch (error) {
setNotice(error.message);
}
}
async function probeProvider(provider) {
try {
const result = await probeIdentityProvider(provider.id, contextOverrides);
setData((current) => ({ ...current, providers: current.providers.map((item) => item.id === result.provider.id ? result.provider : item), summary: { ...current.summary, readyProviders: current.providers.filter((item) => item.id === result.provider.id ? result.provider.status === "ready" : item.status === "ready").length } }));
setNotice(`“${provider.name}”探测完成:${result.provider.status === "ready" ? "就绪" : result.provider.status}`);
} catch (error) {
setNotice(error.message);
await load();
}
}
async function submitDirectory(event) {
event.preventDefault();
try {
const result = await createDirectorySync(directoryForm, contextOverrides);
setData((current) => ({ ...current, directorySyncs: [...current.directorySyncs, result.directorySync], summary: { ...current.summary, managedOrganizations: new Set([...current.directorySyncs, result.directorySync].map((item) => item.organizationId).filter(Boolean)).size } }));
setTokenNotice(`SCIM 令牌只显示这一次:${result.token}`);
setDirectoryForm({ name: "企业目录", syncMode: "provision-and-deprovision", schedule: "manual" });
} catch (error) {
setNotice(error.message);
}
}
async function toggleDirectory(directory) {
try {
const result = await updateDirectorySync(directory.id, { enabled: !directory.enabled }, contextOverrides);
setData(result);
setNotice(`目录同步“${directory.name}”已${directory.enabled ? "停用" : "启用"}`);
} catch (error) {
setNotice(error.message);
}
}
async function rotateToken(directory) {
try {
const result = await rotateDirectorySyncToken(directory.id, contextOverrides);
setData((current) => ({ ...current, directorySyncs: current.directorySyncs.map((item) => item.id === directory.id ? result.directorySync : item) }));
setTokenNotice(`SCIM 令牌已轮换,只显示这一次:${result.token}`);
} catch (error) {
setNotice(error.message);
}
}
return (
刷新{saving ? "保存中…" : "保存身份策略"} } />
{notice && {notice}
}
{tokenNotice && {tokenNotice} setTokenNotice("")}>
}
{loading ? 读取身份配置…
: <>
{data.providers.map((provider) =>
{provider.name}{provider.kind.toUpperCase()} · {provider.issuerUrl || (provider.kind === "saml" ? provider.entryPoint || "尚未填写 Entry Point" : "尚未填写 Issuer")}{provider.kind === "saml" ? `SP ${provider.spIssuer || "未填写"} · 证书 ${provider.idpCertRef || "未绑定"}` : provider.clientId ? `Client ID ${provider.clientId}` : "缺少 Client ID"}{provider.organizationId ? ` · 组织 ${data.organizationOptions.find((item) => item.id === provider.organizationId)?.name || provider.organizationId}` : " · 未绑定组织"}{provider.workspaceId ? ` · 工作区 ${data.workspaceOptions.find((item) => item.id === provider.workspaceId)?.name || provider.workspaceId}` : ""}{provider.errorMessage ? ` · ${provider.errorMessage}` : ""}
probeProvider(provider)}>探测 toggleProvider(provider)}>{provider.enabled ? "启用" : "停用"})}
{!data.providers.length && 还没有身份提供商。先登记一个 OIDC 或 SAML 配置,再进行探测。
}
{data.directorySyncs.map((directory) =>
{directory.name}{directory.organizationName || directory.organizationId} · {directory.syncMode}{directory.endpointPath} · {directory.tokenHint}
rotateToken(directory)}>轮换令牌 toggleDirectory(directory)}>{directory.enabled ? "启用" : "停用"})}
{!data.directorySyncs.length && 还没有 SCIM 目录。创建后可接收企业目录的用户新增、更新和停用事件。
}
>}
);
}
const settingLabels = { general: "基础设置", deployment: "部署与安全", storage: "存储", queue: "队列", generation: "生成策略", voice: "声音策略", qa: "质量门", notifications: "通知", api: "API" };
const fallbackApiClientScopes = [
{ key: "jobs:read", label: "读取任务", description: "查看当前项目的生成任务和详情" },
{ key: "jobs:write", label: "写入任务", description: "创建、执行、重试、取消和调整任务" },
{ key: "models:read", label: "读取模型", description: "查看模型连接器和 Runner" },
{ key: "models:write", label: "管理模型", description: "登记、修改和探测模型连接器" },
{ key: "audit:read", label: "读取审计", description: "查看和导出审计记录" }
];
function SettingInput({ setting, value, onChange }) {
if (setting.value_type === "boolean") return onChange(!value)} aria-pressed={value}>{value ? "已启用" : "已关闭"};
return onChange(setting.value_type === "number" ? Number(event.target.value) : event.target.value)} />;
}
function readinessBadge(check) {
const map = {
ready: { status: "ready", label: "正常" },
configured: { status: "active", label: "已配置" },
"active-local": { status: "needs-evidence", label: "当前本地" },
"not-configured": { status: "needs-evidence", label: "未配置" },
"needs-config": { status: "failed", label: "待配置" },
unsafe: { status: "failed", label: "高风险" },
missing: { status: "needs-evidence", label: "未创建" },
failed: { status: "failed", label: "失败" }
};
return map[check.status] || { status: "neutral", label: check.status || "未知" };
}
export function SystemSettingsPage({ section = "overview", contextOverrides }) {
const [config, setConfig] = useState({ settings: [], featureFlags: [], notifications: [], apiClients: [], apiClientScopes: fallbackApiClientScopes });
const [health, setHealth] = useState({ services: [], summary: {} });
const [readiness, setReadiness] = useState({ checks: [], summary: {}, backups: { backups: [], count: 0 } });
const [storage, setStorage] = useState(null);
const [deliveries, setDeliveries] = useState([]);
const [originalSettings, setOriginalSettings] = useState({});
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [notice, setNotice] = useState("");
const [clientName, setClientName] = useState("");
const [apiKeyNotice, setApiKeyNotice] = useState("");
const [backupBusy, setBackupBusy] = useState(false);
async function load() {
setLoading(true);
try {
const [nextConfig, nextHealth, nextReadiness, nextStorage, nextDeliveries] = await Promise.all([fetchSystemConfig(contextOverrides), fetchSystemHealth(contextOverrides), fetchSystemReadiness(contextOverrides), fetchStorageUsage(contextOverrides), fetchNotificationDeliveries(contextOverrides)]);
setConfig(nextConfig);
const values = Object.fromEntries(nextConfig.settings.map((setting) => [setting.key, setting.value]));
setOriginalSettings(values);
setHealth(nextHealth);
setReadiness(nextReadiness);
setStorage(nextStorage.storage || null);
setDeliveries(nextDeliveries.deliveries || []);
setNotice("");
} catch (error) {
setNotice(error.message);
} finally {
setLoading(false);
}
}
useEffect(() => { load(); }, [contextOverrides.organizationId]);
const visibleCategories = section === "generation" ? ["generation", "voice", "qa"] : section === "storage" ? ["storage", "queue"] : section === "notifications" ? ["notifications", "api"] : section === "deployment" ? ["deployment", "general"] : section === "features" ? [] : null;
const visibleSettings = config.settings.filter((setting) => !visibleCategories || visibleCategories.includes(setting.category));
const dirtySettings = config.settings.filter((setting) => JSON.stringify(originalSettings[setting.key]) !== JSON.stringify(setting.value));
const title = section === "overview" ? "系统总览" : section === "generation" ? "生成策略" : section === "storage" ? "存储与队列" : section === "notifications" ? "通知与 API" : section === "features" ? "功能开关" : "部署与安全";
const description = section === "overview" ? "部署实例、服务健康、功能开关和关键策略的系统级控制面。" : "系统管理员配置会影响所有组织和工作区,变更会写入审计日志。";
function setSetting(key, value) { setConfig((current) => ({ ...current, settings: current.settings.map((setting) => setting.key === key ? { ...setting, value } : setting) })); }
async function save() {
if (!dirtySettings.length) { setNotice("当前没有待保存的系统配置"); return; }
setSaving(true);
try {
const result = await saveSystemConfig(dirtySettings.map((setting) => ({ key: setting.key, value: setting.value })), contextOverrides);
setConfig((current) => ({ ...current, settings: result.settings }));
setOriginalSettings(Object.fromEntries(result.settings.map((setting) => [setting.key, setting.value])));
setNotice(`已保存 ${dirtySettings.length} 项系统配置,并写入审计日志`);
} catch (error) { setNotice(error.message); } finally { setSaving(false); }
}
async function toggleFlag(flag) {
try { const result = await updateFeatureFlag(flag.key, !flag.enabled, contextOverrides); setConfig((current) => ({ ...current, featureFlags: result.featureFlags })); setNotice(`功能开关“${flag.label}”已更新`); } catch (error) { setNotice(error.message); }
}
async function toggleNotification(channel) {
try { const result = await updateNotificationChannel({ ...channel, enabled: !channel.enabled }, contextOverrides); setConfig((current) => ({ ...current, notifications: result.notifications })); setNotice(`通知渠道“${channel.name}”已更新`); } catch (error) { setNotice(error.message); }
}
async function sendNotificationTest() {
try {
const result = await testNotification({ event: "system.changed", payload: { source: "system-settings", testedAt: new Date().toISOString() } }, contextOverrides);
setDeliveries(result.deliveries || []);
setNotice(`通知测试完成,新增 ${result.created?.length || 0} 条投递记录`);
} catch (error) { setNotice(error.message); }
}
async function createClient(event) {
event.preventDefault();
try { const result = await createApiClient({ name: clientName || "本地 API 客户端", scopes: ["jobs:read", "jobs:write", "models:read"] }, contextOverrides); setConfig((current) => ({ ...current, apiClients: result.apiClients || [] })); setApiKeyNotice(`客户端已创建。请立即保存一次性密钥:${result.clientKey}`); setClientName(""); } catch (error) { setApiKeyNotice(error.message); }
}
async function toggleApiClient(client) {
try {
const nextStatus = client.status === "active" ? "revoked" : "active";
const result = await updateApiClient(client.id, { status: nextStatus }, contextOverrides);
setConfig((current) => ({ ...current, apiClients: result.apiClients || current.apiClients }));
setApiKeyNotice(`API 客户端“${client.name}”已${nextStatus === "active" ? "恢复" : "撤销"}`);
} catch (error) {
setApiKeyNotice(error.message);
}
}
async function toggleApiClientScope(client, scopeKey) {
const nextScopes = client.scopes.includes(scopeKey) ? client.scopes.filter((scope) => scope !== scopeKey) : [...client.scopes, scopeKey];
if (!nextScopes.length) {
setApiKeyNotice("API 客户端至少需要保留一个 scope");
return;
}
try {
const result = await updateApiClient(client.id, { scopes: nextScopes }, contextOverrides);
setConfig((current) => ({ ...current, apiClients: result.apiClients || current.apiClients }));
setApiKeyNotice(`API 客户端“${client.name}”的权限范围已更新`);
} catch (error) {
setApiKeyNotice(error.message);
}
}
async function rotateClientKey(client) {
if (typeof window !== "undefined" && !window.confirm(`确认轮换“${client.name}”的 API 密钥吗?旧密钥会立即失效。`)) return;
try {
const result = await rotateApiClientKey(client.id, contextOverrides);
setConfig((current) => ({ ...current, apiClients: result.apiClients || current.apiClients }));
setApiKeyNotice(`“${client.name}”的新密钥只显示这一次,请立即保存:${result.clientKey}`);
} catch (error) {
setApiKeyNotice(error.message);
}
}
async function createBackup() {
setBackupBusy(true);
try {
const result = await createSystemBackup(contextOverrides);
setReadiness((current) => ({ ...current, backups: { ...(current.backups || {}), count: result.count, latest: result.latest, backups: result.backups } }));
setNotice(`数据库快照已创建:${result.backup.relativePath}`);
} catch (error) {
setNotice(error.message);
} finally {
setBackupBusy(false);
}
}
return (
刷新{section !== "overview" && {saving ? "保存中…" : dirtySettings.length ? `保存 ${dirtySettings.length} 项` : "保存配置"}} } />
{notice && {notice}
}
{loading ? 读取系统配置…
: <>
{section === "overview" && flag.enabled).length}/${config.featureFlags.length}`} detail="当前启用" tone="ok" /> }
{(section === "overview" || section === "deployment") && {health.services.map((service) =>
{service.label}
{service.endpoint}{service.latency_ms ? `${service.latency_ms}ms` : "未上报延迟"}
)}
}
{(section === "overview" || section === "deployment") && } />{(readiness.checks || []).map((check) => { const badge = readinessBadge(check); return
{check.label}
{check.detail}{check.envName &&
{check.envName}}
; })}
}
{(section !== "notifications" && section !== "overview" && section !== "features") && SQLite / system_settings} />{visibleSettings.map((setting) =>
{settingLabels[setting.category] || setting.category} · {setting.key}{setting.description}
setSetting(setting.key, value)} />)}
}
{(section === "overview" || section === "generation" || section === "deployment" || section === "features") && {config.featureFlags.map((flag) =>
{flag.label}{flag.description}
toggleFlag(flag)}>{flag.enabled ? "启用" : "关闭"})}
}
{(section === "storage" || section === "overview") && {storage ? `${storage.usedGb} GB` : "—"}已使用 / {storage?.limitGb || 0} GB
文件系统测量{storage ? `${storage.usedBytes.toLocaleString()} / ${storage.limitBytes.toLocaleString()} bytes` : "—"}
资产、任务结果和本地合成输出均计入存储配额。最大文件{storage?.largestFiles?.slice(0, 4).map((file) =>
{file.relativePath}{(file.bytes / 1024 ** 2).toFixed(2)} MB
) ||
暂无文件}
}
{(section === "overview" || section === "deployment" || section === "storage") && {backupBusy ? "创建中…" : "立即创建快照"}} />当前业务真源:SQLite快照写入 `data/backups/`,创建动作进入审计日志。
{(readiness.backups?.backups || []).slice(0, 6).map((backup) =>
{backup.relativePath}{formatAdminDate(backup.modifiedAt)} · {(Number(backup.bytes || 0) / 1024 ** 2).toFixed(2)} MB
)}{!(readiness.backups?.backups || []).length &&
还没有数据库快照。
}
}
{(section === "overview" || section === "notifications") && 发送测试} />{config.notifications.map((channel) =>
{channel.name}{channel.kind} · {channel.endpoint || "本地日志"}
toggleNotification(channel)}>{channel.enabled ? "启用" : "关闭"})}
最近投递{deliveries.length} 条
{deliveries.slice(0, 5).map((delivery) =>
{delivery.event_key}{delivery.channel_name} · {delivery.created_at?.replace("T", " ").slice(0, 19)}
)}{!deliveries.length &&
还没有通知投递记录。}
{config.apiClients.map((client) =>
{client.name}{client.client_key_preview || client.client_key}{(config.apiClientScopes?.length ? config.apiClientScopes : fallbackApiClientScopes).map((scope) => toggleApiClientScope(client, scope.key)}>{scope.label})}
rotateClientKey(client)}>轮换密钥 toggleApiClient(client)}>{client.status === "active" ? "撤销" : "恢复"})}
{apiKeyNotice && {apiKeyNotice}
} }
>}
);
}