From f2134753a4a664ac568513611c0ba4df137e7c71 Mon Sep 17 00:00:00 2001 From: ThearchyHelios Date: Mon, 19 Oct 2026 11:40:53 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AE=BE=E7=BD=AE=E9=A1=B5=E5=8A=A0=E3=80=8C?= =?UTF-8?q?=E8=BF=90=E8=A1=8C=E5=B9=B6=E5=8F=91=E3=80=8D=E4=B8=80=E8=8A=82?= =?UTF-8?q?=EF=BC=9A=E5=85=A8=E7=AB=99=E5=90=8C=E6=97=B6=E8=BF=90=E8=A1=8C?= =?UTF-8?q?=E7=9A=84=E4=B8=8A=E9=99=90=EF=BC=8C=E4=BA=A4=E4=BA=92=E3=80=81?= =?UTF-8?q?=E5=BA=94=E7=94=A8=E3=80=81=E5=AE=9A=E6=97=B6=E3=80=81=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E5=9B=9B=E7=B1=BB=E5=90=84=E8=87=AA=E7=9A=84=E4=B8=8A?= =?UTF-8?q?=E9=99=90=E5=92=8C=E4=BC=98=E5=85=88=E7=BA=A7=EF=BC=8C=E6=97=81?= =?UTF-8?q?=E8=BE=B9=E5=86=99=E5=90=84=E7=B1=BB=E7=8E=B0=E5=9C=A8=E5=9C=A8?= =?UTF-8?q?=E6=89=A7=E8=A1=8C=E3=80=81=E5=9C=A8=E7=AD=89=E5=90=8D=E9=A2=9D?= =?UTF-8?q?=E7=9A=84=E4=B8=AA=E6=95=B0=EF=BC=88/system=20=E7=9A=84=20clust?= =?UTF-8?q?er.runs=EF=BC=89=EF=BC=9B=E7=AE=A1=E7=90=86=E5=91=98=E5=8F=AF?= =?UTF-8?q?=E6=94=B9=EF=BC=8C=E6=90=AD=E5=BB=BA=E8=80=85=E5=8F=AA=E8=AF=BB?= =?UTF-8?q?=EF=BC=8C=E8=87=AA=E5=B7=B1=E8=AF=BB=E5=86=99=20concurrency=20?= =?UTF-8?q?=E4=B8=80=E7=BB=84=E3=80=81=E5=B8=A6=E7=A6=BB=E5=BC=80=E5=89=8D?= =?UTF-8?q?=E7=A1=AE=E8=AE=A4=EF=BC=88=E5=90=8C=E8=AF=84=E6=B5=8B=E9=82=A3?= =?UTF-8?q?=E4=B8=80=E8=8A=82=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/ConcurrencySettings.tsx | 145 +++++++++++++++++++++ frontend/src/pages/SettingsPage.tsx | 3 + 2 files changed, 148 insertions(+) create mode 100644 frontend/src/pages/ConcurrencySettings.tsx diff --git a/frontend/src/pages/ConcurrencySettings.tsx b/frontend/src/pages/ConcurrencySettings.tsx new file mode 100644 index 00000000..fbcfc18a --- /dev/null +++ b/frontend/src/pages/ConcurrencySettings.tsx @@ -0,0 +1,145 @@ +import { useEffect, useState } from 'react' +import { api } from '../api/client' +import { Field, Notice, SectionBar, Spinner, confirmDialog } from '../components/ui' +import { errorMessage } from '../lib/errors' +import { useLeaveGuard } from '../lib/leave' +import { CONCURRENCY_CLASSES, CONCURRENCY_TEXT } from '../lib/termsConcurrency' +import { atLeast, usePrincipal } from '../store/auth' + +const T = CONCURRENCY_TEXT +type Klass = (typeof CONCURRENCY_CLASSES)[number] +interface Draft { total: string; limits: Record; priorities: Record } +type Now = Partial> + +const draftOf = (group: Record | undefined): Draft => ({ + total: group?.max_total == null ? '' : String(group.max_total), + limits: Object.fromEntries(CONCURRENCY_CLASSES.map((k) => [k, String(group?.classes?.[k]?.limit ?? '')])) as Record, + priorities: Object.fromEntries(CONCURRENCY_CLASSES.map((k) => [k, String(group?.classes?.[k]?.priority ?? '')])) as Record, +}) + +const changedCount = (a: Draft, b: Draft): number => + (a.total.trim() !== b.total ? 1 : 0) + + CONCURRENCY_CLASSES.filter((k) => a.limits[k].trim() !== b.limits[k]).length + + CONCURRENCY_CLASSES.filter((k) => a.priorities[k].trim() !== b.priorities[k]).length + +/** + * 设置页「偏好设置」里「运行并发」一节(docs/design/orchestration-round §5 W2-b):全站总上限,交互、应用、定时、系统四类各自的 + * 上限和优先级,旁边写各类现在在跑、在等几个(/system 的 cluster.runs)。 + * + * 自己读、自己存(PUT /settings 只带 concurrency 一组),带自己的离开前确认,同评测那一节的做法。搭建者只读、管理员写 + */ +export function ConcurrencySettingsSection() { + const me = usePrincipal() + const admin = atLeast(me, 'admin') + const [saved, setSaved] = useState(null) + const [draft, setDraft] = useState(null) + const [now, setNow] = useState(null) + const [loadError, setLoadError] = useState(null) + const [saving, setSaving] = useState(false) + const [error, setError] = useState(null) + const [flash, setFlash] = useState(false) + + const loadNow = () => api.system().then((s) => setNow(s?.cluster?.runs?.classes ?? null), () => setNow(null)) + useEffect(() => { + api.settings.get().then((v) => { + setSaved(draftOf(v?.concurrency)) + setLoadError(null) + }, setLoadError) + void loadNow() + }, []) + + const cur = draft ?? saved + const changed = !!(cur && saved) && changedCount(cur, saved) > 0 + useLeaveGuard(changed && !saving, () => confirmDialog({ + title: T.leaveTitle(cur && saved ? changedCount(cur, saved) : 0), + consequences: T.leaveConsequences, confirmLabel: T.discard, danger: true, + })) + + const save = async () => { + if (!cur || !changed) return + setSaving(true) + setError(null) + try { + const value = { + max_total: Number(cur.total.trim()), + classes: Object.fromEntries(CONCURRENCY_CLASSES.map((k) => [k, { + limit: Number(cur.limits[k].trim()), priority: Number(cur.priorities[k].trim()), + }])), + } + const out = await api.settings.put({ concurrency: value }) + setSaved(draftOf(out?.concurrency ?? value)) + setDraft(null) + setFlash(true) + setTimeout(() => setFlash(false), 1600) + void loadNow() + } catch (e) { + setError(errorMessage(e)) + } finally { + setSaving(false) + } + } + + if (loadError || !cur) return null + const edit = (patch: Partial) => { + setError(null) + setDraft({ ...cur, ...patch, limits: { ...cur.limits, ...(patch.limits ?? {}) }, + priorities: { ...cur.priorities, ...(patch.priorities ?? {}) } }) + } + return ( +
+ +
+ + {(p) => ( + edit({ total: e.target.value })} data-concurrency-setting="max_total" /> + )} + +
+ {CONCURRENCY_CLASSES.map((k) => { + const info = T.classes[k] + const live = now?.[k] + return ( +
+
+
{info.label}
+
{info.hint}
+ {live &&
{T.now(live.running ?? 0, live.waiting ?? 0)}
} +
+ + {(p) => ( + edit({ limits: { [k]: e.target.value } as Record })} + data-concurrency-setting={`${k}.limit`} /> + )} + + + {(p) => ( + edit({ priorities: { [k]: e.target.value } as Record })} + data-concurrency-setting={`${k}.priority`} /> + )} + +
+ ) + })} +
+
+ {!admin &&

{T.readOnly}

} + {error && {error}} + {admin && (changed || flash) && ( +
+ {flash && !changed && {T.saved}} + {changed && ( + <> + + + + )} +
+ )} +
+ ) +} diff --git a/frontend/src/pages/SettingsPage.tsx b/frontend/src/pages/SettingsPage.tsx index 3d19eb5c..27e32f07 100644 --- a/frontend/src/pages/SettingsPage.tsx +++ b/frontend/src/pages/SettingsPage.tsx @@ -34,6 +34,7 @@ import { } from './VerifySettings' import { DraftModelSection } from './DraftModelSettings' import { EvalSettingsSection } from './EvalSettings' +import { ConcurrencySettingsSection } from './ConcurrencySettings' import { GeneralTab } from './settings/GeneralTab' import { IdentityTab } from './settings/IdentityTab' import { NotifyTab } from './settings/NotifyTab' @@ -1204,6 +1205,8 @@ function PrefsTab() { {values.eval && } + {values.concurrency && } + {values.ledger && } {values.catalog_draft && (