Files
ttstd-infinite-canvas/web/src/pages/admin/settings/runtime-policy-settings-page.tsx
AetherNo2332 9f37065e47 feat(cloud-agent): 单步输出上限与秒级墙钟,超时/空输出可自恢复
画布 Agent 一轮要跑很多步模型调用,此前只有"按任务类型的分钟级超时":某一步卡住只能等
整轮判死;更根本的是每一步请求根本不带输出上限,不写 max_tokens 时上游按剩余上下文放行,
思考模型可以把单步拖到几分钟,且偶发把整个输出预算花在推理上、正文与工具调用皆空。

- 新增运行时策略 task.agentStepTimeoutSeconds 与 task.agentStepMaxOutputTokens:秒级墙钟
  与单步输出上限,都支持环境变量给部署级默认值、管理端热更新;旧配置 JSON 缺字段按默认值
  回填,无需数据迁移。
- 单步墙钟到点中止这一步时不再把整轮判死:以可识别标记记为该步失败,运行期关掉上游思考
  重试同一步一次(model_failure_recovered / step_timeout_retried),重试仍超时才以可读原因
  (model_step_timeout)结束本轮。
- 空输出催办用尽后新增一级升级:关思考 + 放大输出预算重试同一步
  (model_failure_recovered / empty_output_escalated),而不是直接判死。放大档在原值上翻倍
  并以 131072 封顶,策略值为 0 时用 32768 兜底,让重试仍然有界。
- 输出上限真正下发到上游:textOptions.maxOutputTokens 成为任务信封字段,Agent 的两条请求
  路径(内置协议与声明式协议)组装请求体时写入 max_tokens / max_output_tokens,与既有的
  input.MaxOutputTokens 取较小值,避免策略上限超过模型声明的物理上限。
- task_worker:执行时限改用带画布 Agent 分支的 taskExecutionTimeout;执行时限到点本身不再
  被当成"租约丢失",否则任务会停在 running 等租约过期后被反复重跑(实测一次调用被重跑成
  三次上游请求)。续租仍用上游已合入的"不继承父 context 取消"的独立 context。
2026-09-22 01:20:36 +08:00

573 lines
33 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { App, Button, Form, InputNumber, Skeleton } from "antd";
import { AlertTriangle, Bot, Database, Gauge, Infinity as InfinityIcon, Network, RefreshCw, RotateCcw, Save, ShieldCheck, TimerReset } from "lucide-react";
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
import { useBlocker } from "react-router";
import { cn } from "@/lib/utils";
import { getAdminRuntimePolicySetting, getAdminSelfUseRuntimePolicy, resetAdminRuntimePolicySetting, updateAdminRuntimePolicySetting, type RuntimePolicySetting } from "@/services/api/auth";
import { useAdminContext } from "../admin-context";
import { AdminPageFrame } from "../components/admin-shell";
import { AdminStatTile, AdminStatusBadge, SettingsSectionCard } from "../components/admin-ui";
type PolicyGroup = "resource" | "task" | "request";
type RuntimePolicyDraft = Pick<RuntimePolicySetting, "resource" | "task" | "request">;
type PolicyField = { group: PolicyGroup; name: string; label: string; extra: string; unit: string; min?: number; max: number };
type PolicySectionDefinition = {
id: string;
icon: ReactNode;
title: string;
shortTitle: string;
description: string;
fields: PolicyField[];
status?: ReactNode;
};
const resourceFields: PolicyField[] = [
{ group: "resource", name: "resourceUploadMB", label: "普通资源单文件", extra: "素材上传和远程导入的单文件业务上限。", unit: "MB", max: 999 },
{ group: "resource", name: "generatedFileMB", label: "单个生成资源", extra: "上游生成响应和落库资源的单文件上限。", unit: "MB", max: 999 },
{ group: "resource", name: "dailyUploadMB", label: "每日上传总量", extra: "按 UTC 自然日累计资源与附件上传。", unit: "MB", max: 999_999 },
{ group: "resource", name: "storedFileGB", label: "账号文件总量", extra: "资源文件与 Agent 会话附件合计。", unit: "GB", max: 999 },
{ group: "resource", name: "structuredDataMB", label: "结构化数据总量", extra: "画布、素材和 Agent 会话结构化数据合计。", unit: "MB", max: 999_999 },
{ group: "resource", name: "taskDataGB", label: "任务数据总量", extra: "任务历史、结果和上游请求日志合计。", unit: "GB", max: 999 },
{ group: "resource", name: "assetCount", label: "素材数量", extra: "单账号可保存的素材记录数。", unit: "条", max: 999_999_999 },
{ group: "resource", name: "canvasCount", label: "画布数量", extra: "单账号可保存的画布数量。", unit: "个", max: 999_999_999 },
{ group: "resource", name: "taskCount", label: "任务历史数量", extra: "单账号保留的任务历史记录数。", unit: "条", max: 999_999_999 },
{ group: "resource", name: "apiCallLogCount", label: "请求日志数量", extra: "单账号保留的上游请求日志数。", unit: "条", max: 999_999_999 },
{ group: "resource", name: "recycleBinRetentionDays", label: "回收站自动清理时间", extra: "素材移入回收站后自动彻底删除的天数,0 表示不自动清理。", unit: "天", min: 0, max: 365 },
];
const concurrencyFields: PolicyField[] = [
{ group: "task", name: "workerConcurrency", label: "Worker 并发", extra: "集群同时执行的后台任务数。", unit: "个", max: 999 },
{ group: "task", name: "channelConcurrency", label: "全局渠道并发", extra: "渠道选择跟随系统时采用的并发上限。", unit: "个", max: 999 },
{ group: "task", name: "activeTaskLimit", label: "账号活动任务", extra: "单账号跨项目同时排队或运行的任务总数。", unit: "个", max: 999 },
];
const timeoutFields: PolicyField[] = [
{ group: "task", name: "imageTimeoutMinutes", label: "图片任务超时", extra: "图片任务进入失败状态前的最长执行时间。", unit: "分钟", max: 9_999 },
{ group: "task", name: "textTimeoutMinutes", label: "文本任务超时", extra: "文本任务的最长执行时间。", unit: "分钟", max: 9_999 },
{ group: "task", name: "audioTimeoutMinutes", label: "音频任务超时", extra: "音频任务的最长执行时间。", unit: "分钟", max: 9_999 },
{ group: "task", name: "videoTimeoutMinutes", label: "视频任务超时", extra: "视频任务的最长执行时间。", unit: "分钟", max: 9_999 },
{ group: "task", name: "storyboardTimeoutMinutes", label: "分镜任务超时", extra: "Agent 分镜任务的最长执行时间。", unit: "分钟", max: 9_999 },
{ group: "task", name: "defaultTimeoutMinutes", label: "默认任务超时", extra: "未匹配专用类型时使用的最长执行时间。", unit: "分钟", max: 9_999 },
];
const agentFields: PolicyField[] = [
{
group: "task",
name: "agentStepMaxOutputTokens",
label: "单步输出上限",
extra: "画布 Agent 每次模型调用的输出上限,含思考、正文与工具调用参数。0 表示不限制,此时只有单步超时兜底;抬高可以避免长思考模型被截断后返回空内容。",
unit: "token",
min: 0,
max: 131_072,
},
{
group: "task",
name: "agentStepTimeoutSeconds",
label: "单步超时",
extra: "画布 Agent 单步模型调用的最长等待时间,到点会中止这一步并自动关思考重试一次。0 表示沿用“文本任务超时”,不再单独计时。",
unit: "秒",
min: 0,
max: 3_600,
},
];
const rateFields: PolicyField[] = [
{ group: "request", name: "taskCreatePerMinute", label: "任务创建", extra: "每账号每分钟允许创建的任务数。", unit: "次/分钟", max: 999_999 },
{ group: "request", name: "resourceUploadPerMinute", label: "资源上传", extra: "每账号每分钟上传资源的次数。", unit: "次/分钟", max: 999_999 },
{ group: "request", name: "resourceImportPerMinute", label: "资源导入", extra: "每账号每分钟导入远程资源的次数。", unit: "次/分钟", max: 999_999 },
{ group: "request", name: "assetWritePerMinute", label: "素材写入", extra: "每账号每分钟写入素材的次数。", unit: "次/分钟", max: 999_999 },
{ group: "request", name: "canvasWritePerMinute", label: "画布写入", extra: "每账号每分钟写入画布的次数。", unit: "次/分钟", max: 999_999 },
{ group: "request", name: "registerPerHour", label: "账号注册", extra: "每 IP 每小时允许的注册请求次数,包含失败尝试;多人共用网络时建议至少 30 次。", unit: "次/小时", max: 999_999 },
{ group: "request", name: "emailCodePerHour", label: "邮箱验证码", extra: "每 IP 每小时的验证码请求次数,建议至少 60 次;单邮箱另限每小时 10 次,发送后需间隔 60 秒。", unit: "次/小时", max: 999_999 },
{ group: "request", name: "loginIPPerTenMinutes", label: "登录 IP", extra: "每 IP 每 10 分钟允许登录的次数。", unit: "次/10分钟", max: 999_999 },
{ group: "request", name: "loginAccountPerTenMinutes", label: "登录账号组合", extra: "同一 IP 与账号组合每 10 分钟的登录次数。", unit: "次/10分钟", max: 999_999 },
{ group: "request", name: "systemRelayPerMinute", label: "系统渠道中转", extra: "每账号每分钟使用系统渠道的请求数。", unit: "次/分钟", max: 999_999 },
{ group: "request", name: "customRelayPerMinute", label: "自定义渠道中转", extra: "每账号每分钟使用自定义渠道的请求数。", unit: "次/分钟", max: 999_999 },
];
const relayFields: PolicyField[] = [
{ group: "request", name: "customRelayConcurrency", label: "自定义渠道并发", extra: "单账号同时进行的自定义渠道请求数。", unit: "个", max: 999 },
{ group: "request", name: "customRelayRequestMB", label: "自定义渠道请求体", extra: "中转到自定义上游的请求体上限。", unit: "MB", max: 999 },
{ group: "request", name: "customRelayResponseMB", label: "自定义渠道响应体", extra: "自定义上游 JSON 与流式响应的读取上限。", unit: "MB", max: 999 },
{ group: "request", name: "customRelayTimeoutMinutes", label: "自定义渠道超时", extra: "自定义渠道连接与响应的最长等待时间。", unit: "分钟", max: 9_999 },
{ group: "request", name: "systemRelayRequestMB", label: "系统渠道请求体", extra: "中转到系统渠道的请求体上限。", unit: "MB", max: 999 },
{ group: "request", name: "systemRelayResponseMB", label: "系统渠道响应体", extra: "系统渠道上游响应的读取上限。", unit: "MB", max: 999 },
{ group: "request", name: "channelCircuitFailureCount", label: "熔断失败次数", extra: "一分钟内连续失败达到该值后打开熔断。", unit: "次", max: 999 },
{ group: "request", name: "channelCircuitOpenSeconds", label: "熔断持续时间", extra: "渠道熔断打开后拒绝请求的时间。", unit: "秒", max: 86_400 },
];
const policySections: PolicySectionDefinition[] = [
{ id: "policy-resource", icon: <Database className="size-4" aria-hidden="true" />, title: "资源与账号配额", shortTitle: "资源配额", description: "上传、文件容量、结构化数据和历史记录上限。", fields: resourceFields },
{
id: "policy-concurrency",
icon: <Gauge className="size-4" aria-hidden="true" />,
title: "任务与并发",
shortTitle: "任务并发",
description: "后台任务消费、渠道调度和单账号活动任务上限。",
fields: concurrencyFields,
status: <AdminStatusBadge label="保存后热更新" tone="info" />,
},
{ id: "policy-timeout", icon: <TimerReset className="size-4" aria-hidden="true" />, title: "任务超时", shortTitle: "任务超时", description: "不同生成类型的最长执行时间。", fields: timeoutFields },
{
id: "policy-agent",
icon: <Bot className="size-4" aria-hidden="true" />,
title: "画布 Agent 单步",
shortTitle: "Agent 单步",
description: "画布 Agent 每一步模型调用的输出上限与等待时限;两者一起决定单步最坏耗时。",
fields: agentFields,
status: <AdminStatusBadge label="保存后热更新" tone="info" />,
},
{ id: "policy-rate", icon: <ShieldCheck className="size-4" aria-hidden="true" />, title: "业务频控", shortTitle: "业务频控", description: "账号与 IP 维度的固定窗口请求限制。", fields: rateFields },
{ id: "policy-relay", icon: <Network className="size-4" aria-hidden="true" />, title: "渠道中转与熔断", shortTitle: "中转与熔断", description: "请求体、响应体、并发、超时和上游故障保护。", fields: relayFields },
];
const allPolicyFields = policySections.flatMap((section) => section.fields);
export default function RuntimePolicySettingsPage() {
const { message, modal } = App.useApp();
const { references } = useAdminContext();
const [savedSetting, setSavedSetting] = useState<RuntimePolicySetting | null>(null);
const [draft, setDraft] = useState<RuntimePolicyDraft | null>(null);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [saving, setSaving] = useState(false);
const [resetting, setResetting] = useState(false);
const [presetLoading, setPresetLoading] = useState(false);
const [loadError, setLoadError] = useState("");
const [saveError, setSaveError] = useState("");
const [form] = Form.useForm<RuntimePolicyDraft>();
const requestVersionRef = useRef(0);
const formReadyRef = useRef(false);
const navigationConfirmOpenRef = useRef(false);
const navigationTriggerRef = useRef<HTMLElement | null>(null);
const userNameById = useMemo(() => new Map(references.users.map((user) => [user.id, user.displayName || user.username])), [references.users]);
const load = useCallback(
async (initial = false, announce = false) => {
const requestVersion = ++requestVersionRef.current;
if (initial) setLoading(true);
else setRefreshing(true);
setLoadError("");
try {
const result = await getAdminRuntimePolicySetting();
const value = parseRuntimePolicySetting(result.setting);
if (requestVersion !== requestVersionRef.current) return;
const nextDraft = toPolicyDraft(value);
setSavedSetting(value);
setDraft(nextDraft);
if (formReadyRef.current) form.setFieldsValue(nextDraft);
setSaveError("");
if (announce) message.success("已重新读取当前资源与策略配置");
} catch (error) {
if (requestVersion !== requestVersionRef.current) return;
const errorMessage = error instanceof Error ? error.message : "读取资源与策略失败";
setLoadError(errorMessage);
if (!initial) message.error(errorMessage);
} finally {
if (requestVersion === requestVersionRef.current) {
setLoading(false);
setRefreshing(false);
}
}
},
[form, message],
);
useEffect(() => {
void load(true);
return () => {
requestVersionRef.current += 1;
};
}, [load]);
useEffect(() => {
if (draft) formReadyRef.current = true;
}, [draft]);
const dirtyFields = useMemo(() => {
if (!savedSetting || !draft) return [];
return allPolicyFields.filter((field) => readPolicyValue(savedSetting, field) !== readPolicyValue(draft, field));
}, [draft, savedSetting]);
const dirty = dirtyFields.length > 0;
const busy = loading || refreshing || saving || resetting || presetLoading;
const blocker = useBlocker(dirty && !saving && !resetting);
useEffect(() => {
const beforeUnload = (event: BeforeUnloadEvent) => {
if (!dirty || saving || resetting) return;
event.preventDefault();
};
window.addEventListener("beforeunload", beforeUnload);
return () => window.removeEventListener("beforeunload", beforeUnload);
}, [dirty, resetting, saving]);
useEffect(() => {
if (blocker.state !== "blocked" || navigationConfirmOpenRef.current) return;
navigationConfirmOpenRef.current = true;
navigationTriggerRef.current = document.activeElement instanceof HTMLElement && document.activeElement !== document.body ? document.activeElement : null;
modal.confirm({
title: "放弃资源与策略调整?",
content: `当前有 ${dirtyFields.length} 项调整尚未保存,离开后暂存内容会丢失。`,
okText: "放弃并离开",
cancelText: "继续编辑",
okButtonProps: { danger: true },
onOk: () => {
navigationConfirmOpenRef.current = false;
navigationTriggerRef.current = null;
blocker.proceed();
},
onCancel: () => {
navigationConfirmOpenRef.current = false;
blocker.reset();
window.requestAnimationFrame(() => {
const fallback = document.querySelector<HTMLButtonElement>(".admin-runtime-policy-command-actions button");
const target = navigationTriggerRef.current?.isConnected ? navigationTriggerRef.current : fallback;
target?.focus();
navigationTriggerRef.current = null;
});
},
});
}, [blocker, dirtyFields.length, modal]);
const resetDraft = () => {
if (!savedSetting || busy) return;
const nextDraft = toPolicyDraft(savedSetting);
form.setFieldsValue(nextDraft);
setDraft(nextDraft);
setSaveError("");
message.info("已撤销本页尚未保存的调整");
};
const requestRefresh = () => {
if (!dirty) {
void load(false, true);
return;
}
modal.confirm({
title: "放弃调整并重新读取?",
content: `当前有 ${dirtyFields.length} 项调整尚未保存。重新读取会丢弃暂存内容,并以服务端当前配置为准。`,
okText: "放弃并刷新",
cancelText: "继续编辑",
okButtonProps: { danger: true },
onOk: () => load(false, true),
});
};
const requestSelfMode = () => {
modal.confirm({
rootClassName: "admin-runtime-policy-confirm-modal",
width: 580,
icon: <AlertTriangle className="size-5" aria-hidden="true" />,
title: "填入自用模式上限?",
content: (
<div className="admin-runtime-policy-confirm-copy">
<p>这会把全部 42 项配额、并发、频控和超时填到允许范围的高值,并把熔断等待缩短到 1 秒。</p>
<p>{dirty ? `当前 ${dirtyFields.length} 项暂存调整会被覆盖。` : "只会改动本页草稿,点击保存修改后生效。"}</p>
</div>
),
okText: "填入草稿",
cancelText: "取消",
onOk: async () => {
setPresetLoading(true);
try {
const result = await getAdminSelfUseRuntimePolicy();
const value = parseRuntimePolicySetting(result.setting);
const nextDraft = toPolicyDraft(value);
form.setFieldsValue(nextDraft);
setDraft(nextDraft);
setSaveError("");
message.info("已填入自用模式上限,点击保存修改后生效");
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "读取自用模式失败";
message.error(errorMessage);
throw error;
} finally {
setPresetLoading(false);
}
},
});
};
const requestReset = () => {
if (!savedSetting?.configured || resetting) return;
modal.confirm({
rootClassName: "admin-runtime-policy-confirm-modal",
width: 580,
icon: <AlertTriangle className="size-5" aria-hidden="true" />,
title: "恢复全部系统默认策略?",
content: (
<div className="admin-runtime-policy-confirm-copy">
<p>确认后会删除管理员保存的自定义策略,全部 42 项立即恢复系统默认值。</p>
{dirty ? <p>当前 {dirtyFields.length} 项未保存调整也会一并丢弃。</p> : null}
</div>
),
okText: "恢复系统默认",
cancelText: "取消",
okButtonProps: { danger: true },
onOk: async () => {
setResetting(true);
setSaveError("");
try {
const result = await resetAdminRuntimePolicySetting();
const value = parseRuntimePolicySetting(result.setting);
if (value.configured) throw new Error("服务端未确认已恢复系统默认策略,请刷新后核对");
const nextDraft = toPolicyDraft(value);
setSavedSetting(value);
setDraft(nextDraft);
form.setFieldsValue(nextDraft);
message.success("已恢复系统默认策略");
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "恢复系统默认策略失败";
setSaveError(`${errorMessage}。未自动重试,请重新读取当前配置后核对。`);
message.error(errorMessage);
throw error;
} finally {
setResetting(false);
}
},
});
};
const save = async () => {
if (!draft || !dirty || saving) return;
const values = await form.validateFields();
const expected = toPolicyDraft(values);
const relationshipError = validatePolicyRelationships(expected);
if (relationshipError) {
setSaveError(relationshipError);
form.scrollToField(["resource", "storedFileGB"], { behavior: "smooth", block: "center" });
throw new Error(relationshipError);
}
setSaving(true);
setSaveError("");
try {
const result = await updateAdminRuntimePolicySetting(expected);
const value = parseRuntimePolicySetting(result.setting);
if (!samePolicy(value, expected)) throw new Error("服务端返回的策略与本次保存内容不一致,请刷新后核对");
const nextDraft = toPolicyDraft(value);
setSavedSetting(value);
setDraft(nextDraft);
form.setFieldsValue(nextDraft);
message.success("资源与策略已保存并即时生效");
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "保存资源与策略失败";
setSaveError(`${errorMessage}。未自动重试,请重新读取当前配置后再决定是否保存。`);
message.error(errorMessage);
throw error;
} finally {
setSaving(false);
}
};
const submitSave = async () => {
if (!savedSetting || !draft || !dirty || busy) return;
try {
await save();
} catch {
// 表单与业务错误已在 save 中就地提示。
}
};
if (loading && !draft) {
return (
<AdminPageFrame title="资源与策略" description="账号配额、任务调度与请求安全策略" scroll>
<div className="admin-settings-stack admin-runtime-policy" aria-label="正在读取资源与策略配置" role="status">
<div className="admin-runtime-policy-command-bar">
<Skeleton active title={{ width: 190 }} paragraph={false} />
</div>
<div className="admin-runtime-policy-overview">
{Array.from({ length: 4 }).map((_, index) => (
<div key={index} className="admin-stat-tile">
<Skeleton active title={{ width: 96 }} paragraph={{ rows: 1 }} />
</div>
))}
</div>
<div className="admin-runtime-policy-loading-card">
<Skeleton active paragraph={{ rows: 8 }} />
</div>
</div>
</AdminPageFrame>
);
}
if (!draft || !savedSetting) {
return (
<AdminPageFrame title="资源与策略" description="账号配额、任务调度与请求安全策略" scroll>
<div className="admin-settings-stack admin-runtime-policy">
<div className="admin-runtime-policy-load-error" role="alert">
<span className="admin-runtime-policy-load-error-icon">
<AlertTriangle className="size-5" aria-hidden="true" />
</span>
<div>
<h2>无法读取资源与策略配置</h2>
<p>{loadError || "当前没有可显示的配置,请稍后重试。"}</p>
</div>
<Button icon={<RefreshCw className="size-4" />} loading={loading} onClick={() => void load(true)}>
重新读取
</Button>
</div>
</div>
</AdminPageFrame>
);
}
const updateActor = savedSetting.updatedBy ? userNameById.get(savedSetting.updatedBy) || savedSetting.updatedBy : "";
const updateDetail = savedSetting.configured ? `${formatTime(savedSetting.updatedAt)}${updateActor ? ` · ${updateActor}` : ""}` : "尚未由管理员保存,使用系统默认值";
const workerConcurrency = readDraftNumber(draft, "task", "workerConcurrency");
const activeTaskLimit = readDraftNumber(draft, "task", "activeTaskLimit");
return (
<AdminPageFrame title="资源与策略" description="账号配额、任务调度与请求安全策略" scroll>
<Form
form={form}
initialValues={draft}
layout="vertical"
requiredMark={false}
disabled={busy}
onValuesChange={(_, values) => {
setDraft(toPolicyDraft(values));
setSaveError("");
}}
>
<div className="admin-settings-stack admin-runtime-policy">
<div className={cn("admin-runtime-policy-command-bar", dirty && "is-dirty")}>
<div className="admin-runtime-policy-command-copy" aria-live="polite">
<span className="admin-runtime-policy-command-icon">
<ShieldCheck className="size-4" aria-hidden="true" />
</span>
<div>
<div className="flex flex-wrap items-center gap-2">
<strong>{dirty ? `${dirtyFields.length} 项调整待保存` : "资源与策略状态已同步"}</strong>
<AdminStatusBadge label={dirty ? "尚未生效" : savedSetting.configured ? "管理员配置" : "系统默认"} tone={dirty ? "warning" : savedSetting.configured ? "success" : "neutral"} />
</div>
<p>{dirty ? "当前调整只在本页暂存;点击保存修改后才会影响运行中的业务。" : updateDetail}</p>
</div>
</div>
<div className="admin-runtime-policy-command-actions">
{dirty ? (
<Button icon={<RotateCcw className="size-4" />} disabled={busy} onClick={resetDraft}>
撤销改动
</Button>
) : null}
<Button icon={<RefreshCw className="size-4" />} loading={refreshing} disabled={saving || resetting || presetLoading} onClick={requestRefresh}>
刷新状态
</Button>
<Button icon={<InfinityIcon className="size-4" />} loading={presetLoading} disabled={saving || resetting || refreshing} onClick={requestSelfMode}>
自用模式草稿
</Button>
{savedSetting.configured && !dirty ? (
<Button danger icon={<RotateCcw className="size-4" />} loading={resetting} disabled={saving || refreshing || presetLoading} onClick={requestReset}>
恢复系统默认
</Button>
) : null}
{dirty ? (
<Button type="primary" icon={<Save className="size-4" />} loading={saving} disabled={busy} onClick={() => void submitSave()}>
保存修改
</Button>
) : null}
</div>
</div>
{loadError || saveError ? (
<div className="admin-runtime-policy-inline-alert" role="alert">
<AlertTriangle className="size-4 shrink-0" aria-hidden="true" />
<span>{saveError || `${loadError}。页面仍显示上一次成功读取的配置。`}</span>
</div>
) : null}
<div className="admin-runtime-policy-overview" aria-label="资源与策略配置概览">
<AdminStatTile label="配置来源" value={savedSetting.configured ? "管理员配置" : "系统默认"} detail={updateDetail} />
<AdminStatTile label="待保存调整" value={dirtyFields.length} detail={dirty ? "仅本页暂存,尚未生效" : "当前草稿与服务端一致"} />
<AdminStatTile label="Worker 并发" value={formatNumber(workerConcurrency)} detail={dirtyFields.some((field) => field.name === "workerConcurrency") ? "暂存状态预览" : "集群后台任务并发"} />
<AdminStatTile label="账号活动任务" value={formatNumber(activeTaskLimit)} detail={dirtyFields.some((field) => field.name === "activeTaskLimit") ? "暂存状态预览" : "单账号排队与运行总数"} />
</div>
{policySections.map((section) => (
<div key={section.id} id={section.id} className="admin-settings-anchor admin-runtime-policy-anchor">
<PolicySection {...section} />
</div>
))}
</div>
</Form>
</AdminPageFrame>
);
}
function PolicySection({ icon, title, description, fields, status }: PolicySectionDefinition) {
return (
<SettingsSectionCard icon={icon} title={title} description={description} status={status}>
<div className="admin-runtime-policy-field-grid">
{fields.map((field) => {
const inputId = `admin-runtime-policy-${field.group}-${field.name}`;
const min = field.min ?? 1;
return (
<Form.Item key={`${field.group}.${field.name}`} label={field.label} htmlFor={inputId} extra={field.extra}>
<div className="admin-runtime-policy-number-control">
<Form.Item
noStyle
name={[field.group, field.name]}
rules={[
{ required: true, message: `请填写${field.label}` },
{ type: "number", min, max: field.max, message: `${field.label}必须是 ${min}-${field.max} 的整数` },
]}
>
<InputNumber id={inputId} min={min} max={field.max} precision={0} aria-label={field.label} />
</Form.Item>
<span className="admin-runtime-policy-number-unit" aria-hidden="true">
{field.unit}
</span>
</div>
</Form.Item>
);
})}
</div>
</SettingsSectionCard>
);
}
function toPolicyDraft(value: RuntimePolicyDraft): RuntimePolicyDraft {
return { resource: { ...value.resource }, task: { ...value.task }, request: { ...value.request } };
}
function readPolicyValue(value: RuntimePolicyDraft, field: PolicyField) {
return Number((value[field.group] as unknown as Record<string, unknown>)?.[field.name]);
}
function readDraftNumber(value: RuntimePolicyDraft, group: PolicyGroup, name: string) {
return Number((value[group] as unknown as Record<string, unknown>)?.[name]);
}
function samePolicy(left: RuntimePolicyDraft, right: RuntimePolicyDraft) {
return allPolicyFields.every((field) => readPolicyValue(left, field) === readPolicyValue(right, field));
}
function parseRuntimePolicySetting(value: RuntimePolicySetting): RuntimePolicySetting {
if (!value || typeof value !== "object") throw new Error("服务端返回的资源与策略格式无效");
for (const field of allPolicyFields) {
const fieldValue = readPolicyValue(value, field);
const min = field.min ?? 1;
if (!Number.isInteger(fieldValue) || fieldValue < min || fieldValue > field.max) throw new Error(`服务端返回的“${field.label}”无效`);
}
if (typeof value.configured !== "boolean") throw new Error("服务端未返回有效的配置来源状态");
return value;
}
function validatePolicyRelationships(value: RuntimePolicyDraft) {
const accountCapacityMB = readDraftNumber(value, "resource", "storedFileGB") * 1024;
const oversized = resourceFields.slice(0, 3).find((field) => readPolicyValue(value, field) > accountCapacityMB);
return oversized ? `${oversized.label}不能大于账号文件总量` : "";
}
function formatPolicyValue(value: number, unit: string) {
return `${formatNumber(value)} ${unit}`;
}
function formatNumber(value: number) {
return Number.isFinite(value) ? new Intl.NumberFormat("zh-CN").format(value) : "--";
}
function formatTime(value?: string) {
if (!value) return "--";
const time = new Date(value);
if (!Number.isFinite(time.getTime()) || time.getUTCFullYear() <= 1) return "--";
return time.toLocaleString("zh-CN", { hour12: false });
}