feat: complete TASK-WP3-01 model configuration

This commit is contained in:
suyx
2026-08-03 02:40:48 +08:00
parent 1b0a05bbcf
commit d79ac74fdd
19 changed files with 7018 additions and 3 deletions
+197
View File
@@ -0,0 +1,197 @@
import { useEffect, useMemo, useRef, useState } from "react";
import "./admin-models.css";
interface AdminSession { csrf_token: string }
interface RuntimeAvailability {
available_for_new_jobs: boolean;
checked_at: string;
reason: string;
}
interface ModelConfig {
config_version: number;
contract_evidence_ref: string | null;
contract_validation_status: "blocked" | "unverified" | "verified";
credit_cost: number;
display_name: string;
enabled: boolean;
error_mapping_profile: Record<string, string>;
gateway_account_ref: string;
is_default: boolean;
model_id: string;
prompt_max_length: number;
recommendation_priority: number;
reference_limits: { max_file_bytes: number; max_files: number; max_total_bytes: number };
route_profile: Record<string, unknown>;
runtime_availability: RuntimeAvailability;
safety_source: string;
supported_ratios: string[];
}
interface ModelConfiguration {
config_set_version: number;
configured_default_model_id: string;
models: ModelConfig[];
recommended_model_id: string | null;
}
async function responseJson<T>(url: string, init?: RequestInit) {
const response = await fetch(url, { credentials: "same-origin", ...init });
const body = await response.json() as T;
return { body, response };
}
function idempotencyKey() {
return crypto.randomUUID().replaceAll("-", "") + crypto.randomUUID().replaceAll("-", "");
}
function editableModel(model: ModelConfig) {
const { config_version: _configVersion, contract_evidence_ref: _evidence, contract_validation_status: _status, runtime_availability: _runtime, ...editable } = model;
return editable;
}
function runtimeLabel(runtime: RuntimeAvailability) {
return runtime.available_for_new_jobs ? "可用于新任务" : `不可用 · ${runtime.reason}`;
}
export function AdminModelsPage() {
const [session, setSession] = useState<AdminSession>();
const [configuration, setConfiguration] = useState<ModelConfiguration>();
const [draft, setDraft] = useState<ModelConfig[]>();
const [loadingFailed, setLoadingFailed] = useState(false);
const [saving, setSaving] = useState(false);
const [notice, setNotice] = useState("");
const [conflicted, setConflicted] = useState(false);
const priorityRefs = useRef<Record<string, HTMLInputElement | null>>({});
const defaultRefs = useRef<Record<string, HTMLInputElement | null>>({});
async function load() {
setLoadingFailed(false);
setConflicted(false);
try {
const [sessionResult, modelResult] = await Promise.all([
responseJson<AdminSession>("/api/v1/admin-auth/session"),
responseJson<ModelConfiguration>("/api/v1/models"),
]);
if (!sessionResult.response.ok || !modelResult.response.ok) throw new Error("load_failed");
setSession(sessionResult.body);
setConfiguration(modelResult.body);
setDraft(structuredClone(modelResult.body.models));
setNotice("");
} catch {
setLoadingFailed(true);
}
}
useEffect(() => { void load(); }, []);
const validation = useMemo(() => {
if (!draft) return { valid: false, message: "" };
const invalidPriority = draft.find((model) => !Number.isSafeInteger(model.recommendation_priority) || model.recommendation_priority <= 0);
if (invalidPriority) return { field: invalidPriority.model_id, kind: "priority" as const, message: "推荐优先级必须是正整数。", valid: false };
const duplicatePriority = draft.find((model, index) => draft.findIndex((item) => item.recommendation_priority === model.recommendation_priority) !== index);
if (duplicatePriority) return { field: duplicatePriority.model_id, kind: "priority" as const, message: "推荐优先级不能重复。", valid: false };
const defaults = draft.filter((model) => model.enabled && model.is_default);
if (defaults.length !== 1) return { field: draft.find((model) => model.is_default)?.model_id ?? draft[0]?.model_id, kind: "default" as const, message: "必须指定一个已启用的默认模型。", valid: false };
return { message: "", valid: true };
}, [draft]);
function updateModel(modelId: string, change: Partial<ModelConfig>) {
setDraft((current) => current?.map((model) => model.model_id === modelId ? { ...model, ...change } : model));
setNotice("");
}
function chooseDefault(modelId: string) {
setDraft((current) => current?.map((model) => ({ ...model, is_default: model.model_id === modelId })));
setNotice("");
}
async function save() {
if (!configuration || !draft || !session || !validation.valid || saving || conflicted) return;
setSaving(true);
setNotice("");
try {
const { body, response } = await responseJson<ModelConfiguration & { error?: { code?: string } }>("/api/v1/admin/models/configuration", {
body: JSON.stringify({ expected_config_set_version: configuration.config_set_version, models: draft.map(editableModel) }),
headers: {
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey(),
"X-CSRF-Token": session.csrf_token,
},
method: "PUT",
});
if (!response.ok) {
const code = body.error?.code;
if (code === "MODEL_CONFIG_VERSION_CONFLICT") {
setConflicted(true);
setNotice("配置已被其他管理员更新,请刷新最新配置后重新编辑。");
} else if (code === "MODEL_RECOMMENDATION_PRIORITY_INVALID" || code === "MODEL_RECOMMENDATION_PRIORITY_CONFLICT") {
const modelId = validation.field ?? draft[0]?.model_id;
if (modelId) priorityRefs.current[modelId]?.focus();
setNotice("请修正推荐优先级后再保存。");
} else if (code === "MODEL_DEFAULT_REPLACEMENT_REQUIRED" || code === "MODEL_DEFAULT_REPLACEMENT_INVALID") {
const modelId = validation.field ?? draft[0]?.model_id;
if (modelId) defaultRefs.current[modelId]?.focus();
setNotice("请指定一个已启用的替代默认模型。");
} else setNotice("操作未完成。");
return;
}
setConfiguration(body);
setDraft(structuredClone(body.models));
setNotice("模型配置已原子保存并记录审计。");
} catch {
setNotice("操作未完成。");
} finally {
setSaving(false);
}
}
return (
<div className="admin-models-page">
<header className="admin-product-header">
<a href="/admin">DADA ADMIN</a>
<nav aria-label="后台导航"><a href="/admin/users"></a><a aria-current="page" href="/admin/models"></a><a href="/admin/audit"></a></nav>
</header>
<main>
<header className="admin-models-heading">
<div><p>MODEL OPERATIONS</p><h1></h1></div>
{configuration ? <strong> v{configuration.config_set_version}</strong> : null}
</header>
{!configuration && !loadingFailed ? <div aria-label="模型配置加载中" className="admin-models-loading"><span /><span /><span /></div> : null}
{loadingFailed ? <p className="admin-models-alert" role="alert"><button onClick={() => void load()} type="button"></button></p> : null}
{configuration && draft ? (
<>
<div className="admin-models-summary" aria-label="模型配置摘要">
<span> <strong>{configuration.configured_default_model_id}</strong></span>
<span> <strong>{configuration.recommended_model_id ?? "无"}</strong></span>
<span> <strong>{configuration.models.filter((model) => model.runtime_availability.available_for_new_jobs).length} / 3</strong></span>
</div>
<div className="admin-models-table-wrap">
<table>
<thead><tr><th></th><th></th><th></th><th></th><th></th><th></th><th></th><th></th><th></th></tr></thead>
<tbody>
{draft.map((model) => (
<tr key={model.model_id}>
<th scope="row"><strong>{model.display_name}</strong><code>{model.model_id}</code></th>
<td><input aria-label={`启用 ${model.display_name}`} checked={model.enabled} onChange={(event) => updateModel(model.model_id, { enabled: event.target.checked })} type="checkbox" /></td>
<td><input aria-label={`设为默认 ${model.display_name}`} checked={model.is_default} disabled={!model.enabled} name="configured-default" onChange={() => chooseDefault(model.model_id)} ref={(node) => { defaultRefs.current[model.model_id] = node; }} type="radio" /></td>
<td><input aria-label={`${model.display_name} 推荐优先级`} inputMode="numeric" min="1" onChange={(event) => updateModel(model.model_id, { recommendation_priority: Number(event.target.value) })} ref={(node) => { priorityRefs.current[model.model_id] = node; }} type="number" value={model.recommendation_priority} /></td>
<td><span className={`model-state is-${model.contract_validation_status}`}>{model.contract_validation_status}</span></td>
<td><span className={`model-state ${model.runtime_availability.available_for_new_jobs ? "is-available" : "is-unavailable"}`}>{runtimeLabel(model.runtime_availability)}</span><small>{new Date(model.runtime_availability.checked_at).toLocaleString("zh-CN")}</small></td>
<td>{configuration.recommended_model_id === model.model_id ? "是" : "否"}</td>
<td><span>{model.credit_cost} </span><small>{model.supported_ratios.join(" / ")} · {model.reference_limits.max_files} </small></td>
<td>v{model.config_version}</td>
</tr>
))}
</tbody>
</table>
</div>
<footer className="admin-models-actions">
<div aria-live="polite">{validation.message ? <p className="admin-models-validation">{validation.message}</p> : null}{notice ? <p className="admin-models-notice" role={conflicted ? "alert" : "status"}>{notice}</p> : null}</div>
<div>{conflicted ? <button onClick={() => void load()} type="button"></button> : null}<button disabled={!validation.valid || saving || conflicted} onClick={() => void save()} type="button">{saving ? "保存中" : "保存完整配置集合"}</button></div>
</footer>
</>
) : null}
</main>
</div>
);
}