Files
story/web/src/pages/OpsConfigWorkspace.tsx
T

331 lines
41 KiB
TypeScript
Raw Normal View History

2026-08-26 06:41:07 +08:00
import { type FormEvent, useEffect, useMemo, useState } from "react";
import {
APIRequestError, platformApi, type CurrentUser, type OpsConfigEvaluation, type OpsConfigRollout,
type OpsConfigTransitionRequest, type OpsConfigVersion, type OpsDeviceGroup,
type OpsResourceReference
} from "../api/client";
type Kind = "PERSONA" | "VOICE" | "MODEL" | "ROUTING" | "AGENT_BUNDLE";
type Scope = "TENANT" | "SUBJECT" | "DEVICE_GROUP";
type BundleField = "persona_version_id" | "voice_version_id" | "model_version_id" | "routing_version_id";
type EvaluationDraft = { item: OpsConfigVersion; suiteVersion: string; reportRef: string; result: "" | "PASSED" | "FAILED"; acknowledged: boolean };
type ConfigControlDraft =
| { kind: "resource_revoke"; item: OpsResourceReference; reason: string; acknowledged: boolean }
| { kind: "rollout_pause" | "rollout_resume"; item: OpsConfigRollout; reason: string; acknowledged: boolean };
type VersionTransitionDraft =
| { kind: "submit"; item: OpsConfigVersion; acknowledged: boolean }
| { kind: "publish" | "rollback"; item: OpsConfigVersion; scopeType: Scope; scopeID: string; rolloutPercent: number; maxConcurrentCalls: number; reason: string; acknowledged: boolean };
const bundleFields: Array<{ field: BundleField; kind: Exclude<Kind, "AGENT_BUNDLE">; label: string }> = [
{ field: "persona_version_id", kind: "PERSONA", label: "人设版本" },
{ field: "voice_version_id", kind: "VOICE", label: "音色版本" },
{ field: "model_version_id", kind: "MODEL", label: "模型版本" },
{ field: "routing_version_id", kind: "ROUTING", label: "路由版本" }
];
export function OpsConfigWorkspace({ accessToken, accounts }: { accessToken: string; accounts: CurrentUser["accounts"] }) {
const opsAccounts = accounts.filter((item) => item.platform_role !== "customer");
const [tenantID, setTenantID] = useState(opsAccounts[0]?.tenant_id ?? "");
const account = opsAccounts.find((item) => item.tenant_id === tenantID) ?? opsAccounts[0];
const [items, setItems] = useState<OpsConfigVersion[]>([]);
const [resourceRefs, setResourceRefs] = useState<OpsResourceReference[]>([]);
const [deviceGroups, setDeviceGroups] = useState<OpsDeviceGroup[]>([]);
const [rollouts, setRollouts] = useState<OpsConfigRollout[]>([]);
const [evaluations, setEvaluations] = useState<Record<string, OpsConfigEvaluation[]>>({});
const [evaluationDraft, setEvaluationDraft] = useState<EvaluationDraft>();
const [configControlDraft, setConfigControlDraft] = useState<ConfigControlDraft>();
const [versionTransitionDraft, setVersionTransitionDraft] = useState<VersionTransitionDraft>();
const [loading, setLoading] = useState(true);
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string>();
const [message, setMessage] = useState<string>();
const [kind, setKind] = useState<Kind>("ROUTING");
const [configKey, setConfigKey] = useState("default-agent-routing");
const [displayName, setDisplayName] = useState("默认语音路由");
const [payloadText, setPayloadText] = useState(defaultPayload("ROUTING"));
const [secretRefsText, setSecretRefsText] = useState("{}");
const [summary, setSummary] = useState("");
const [resourceType, setResourceType] = useState<"PROVIDER_CREDENTIAL" | "VOICE_RESOURCE" | "MODEL_RESOURCE">("PROVIDER_CREDENTIAL");
const [referenceName, setReferenceName] = useState("");
const [referenceDisplayName, setReferenceDisplayName] = useState("");
const [groupName, setGroupName] = useState("");
const [groupDeviceIDs, setGroupDeviceIDs] = useState("");
const canDraft = account && ["ops_operator", "ops_approver", "ops_admin"].includes(account.platform_role);
const canApprove = account && ["ops_approver", "ops_admin"].includes(account.platform_role);
async function load(signal?: AbortSignal) {
if (!tenantID) return;
setEvaluationDraft(undefined);
setConfigControlDraft(undefined);
setVersionTransitionDraft(undefined);
setLoading(true); setError(undefined);
try {
const [versions, refs, deviceGroupResponse, rolloutResponse] = await Promise.all([
platformApi.opsConfigVersions(tenantID, accessToken, signal),
platformApi.opsResourceReferences(tenantID, accessToken, signal),
platformApi.opsDeviceGroups(tenantID, accessToken, signal),
platformApi.opsConfigRollouts(tenantID, accessToken, signal)
]);
setItems(versions.data.items); setResourceRefs(refs.data.items); setDeviceGroups(deviceGroupResponse.data.items); setRollouts(rolloutResponse.data.items);
} catch (cause) {
if (!(cause instanceof DOMException && cause.name === "AbortError")) setError(errorText(cause));
} finally { if (!signal?.aborted) setLoading(false); }
}
useEffect(() => {
const controller = new AbortController(); void load(controller.signal);
return () => controller.abort();
}, [tenantID, accessToken]);
useEffect(() => {
setKind("ROUTING"); setConfigKey("default-agent-routing"); setDisplayName("默认语音路由");
setPayloadText(defaultPayload("ROUTING")); setSecretRefsText("{}"); setSummary("");
}, [tenantID]);
const versionGroups = useMemo(() => {
const result = new Map<string, OpsConfigVersion[]>();
for (const item of items) result.set(item.config_key, [...(result.get(item.config_key) ?? []), item]);
return [...result.entries()];
}, [items]);
const bundleCandidates = useMemo(() => {
const result = new Map<Exclude<Kind, "AGENT_BUNDLE">, OpsConfigVersion[]>();
for (const definition of bundleFields) {
result.set(definition.kind, componentCandidates(items, definition.kind));
}
return result;
}, [items]);
const bundlePayload = useMemo(() => readBundlePayload(payloadText), [payloadText]);
const unavailableBundleKinds = bundleFields.filter((definition) => !(bundleCandidates.get(definition.kind)?.length));
const bundleReady = bundleFields.every((definition) => {
const selected = bundlePayload[definition.field];
return Boolean(selected && bundleCandidates.get(definition.kind)?.some((candidate) => candidate.id === selected));
});
const controllableRollouts = rollouts.filter((item) => item.status === "ACTIVE" || item.status === "PAUSED");
function changeKind(next: Kind) {
setKind(next); setPayloadText(next === "AGENT_BUNDLE" ? defaultAgentBundlePayload(items) : defaultPayload(next));
setSecretRefsText(next === "VOICE" || next === "MODEL" ? JSON.stringify({ credential_ref: "kms://voice-platform/provider/local-dev" }, null, 2) : "{}");
const defaults: Record<Kind, [string, string]> = {
PERSONA: ["default-persona", "默认长者陪伴人设"], VOICE: ["default-voice", "默认语音音色"],
MODEL: ["default-model", "默认对话模型"], ROUTING: ["default-agent-routing", "默认语音路由"],
AGENT_BUNDLE: ["default-agent-bundle", "默认Agent配置组合"]
};
setConfigKey(defaults[next][0]); setDisplayName(defaults[next][1]);
}
async function create(event: FormEvent) {
event.preventDefault();
let payload: Record<string, unknown>; let secret_refs: Record<string, string>;
try { payload = JSON.parse(payloadText); secret_refs = JSON.parse(secretRefsText); }
catch { setError("配置正文和密钥引用必须是有效JSON对象。"); return; }
if (kind === "AGENT_BUNDLE" && !bundleReady) {
setError(unavailableBundleKinds.length ? `请先发布可用的${unavailableBundleKinds.map((item) => item.label).join("、")},再创建Agent配置组合。` : "请选择四个仍可用的已发布或已退役组件版本。");
return;
}
await run(async () => {
await platformApi.createOpsConfigVersion(tenantID, { config_key: configKey.trim(), display_name: displayName.trim(), config_kind: kind, payload, secret_refs, change_summary: summary.trim() }, accessToken);
setSummary(""); setMessage("新的不可变草稿已创建;提交后还需离线评测通过和另一名审批人员发布。"); await load();
});
}
function selectBundleComponent(field: BundleField, versionID: string) {
setPayloadText(JSON.stringify({ ...bundlePayload, [field]: versionID }, null, 2));
}
function openVersionTransition(kind: VersionTransitionDraft["kind"], item: OpsConfigVersion) {
setEvaluationDraft(undefined); setConfigControlDraft(undefined); setError(undefined); setMessage(undefined);
setVersionTransitionDraft(kind === "submit"
? { kind, item, acknowledged: false }
: { kind, item, scopeType: "TENANT", scopeID: "", rolloutPercent: 100, maxConcurrentCalls: 100, reason: "", acknowledged: false });
}
async function submitVersionTransition(event: FormEvent) {
event.preventDefault(); const draft = versionTransitionDraft;
if (!draft || !versionTransitionDraftReady(draft)) return;
await run(async () => {
if (draft.kind === "submit") {
await platformApi.submitOpsConfigVersion(tenantID, draft.item, accessToken);
} else {
const input: OpsConfigTransitionRequest = { reason: draft.reason.trim(), rollout: { scope_type: draft.scopeType, ...(draft.scopeType === "TENANT" ? {} : { scope_id: draft.scopeID }), rollout_percent: draft.scopeType === "SUBJECT" ? 100 : draft.rolloutPercent, max_concurrent_calls: draft.maxConcurrentCalls } };
if (draft.kind === "publish") await platformApi.publishOpsConfigVersion(tenantID, draft.item, input, accessToken);
else await platformApi.rollbackOpsConfigVersion(tenantID, draft.item, input, accessToken);
}
setMessage(draft.kind === "submit" ? "配置已通过结构检查并提交审批。" : draft.kind === "publish" ? "配置已按确认范围发布,只影响后续新建通话。" : "已按确认的历史内容和范围创建新的已发布回滚版本。"); await load();
});
}
function openEvaluation(item: OpsConfigVersion) {
if (item.config_kind === "AGENT_BUNDLE") { setError("Agent配置组合必须使用标准评测工具生成并登记AGENT_QUALITY_V1证据,运营页面不能手工填写通过结论。"); return; }
setConfigControlDraft(undefined); setVersionTransitionDraft(undefined); setEvaluationDraft({ item, suiteVersion: "", reportRef: "", result: "", acknowledged: false }); setError(undefined); setMessage(undefined);
}
async function addEvaluation(event: FormEvent) {
event.preventDefault(); const draft = evaluationDraft;
if (!draft || !evaluationDraftReady(draft)) return;
await run(async () => {
await platformApi.createOpsConfigEvaluation(tenantID, draft.item.id, { evidence_type: "GENERIC", suite_version: draft.suiteVersion.trim(), result: draft.result as "PASSED" | "FAILED", report_ref: draft.reportRef.trim(), metrics: { registered_from: "operations-workspace", gate: "runtime-config-publish" } }, accessToken);
const response = await platformApi.opsConfigEvaluations(tenantID, draft.item.id, accessToken);
setEvaluations((current) => ({ ...current, [draft.item.id]: response.data.items })); setEvaluationDraft(undefined);
setMessage(`已登记${draft.result === "PASSED" ? "通过" : "失败"}的离线评测证据。`);
});
}
async function loadEvaluations(configID: string) {
await run(async () => { const response = await platformApi.opsConfigEvaluations(tenantID, configID, accessToken); setEvaluations((current) => ({ ...current, [configID]: response.data.items })); }, false);
}
async function addResourceReference(event: FormEvent) {
event.preventDefault();
await run(async () => {
await platformApi.createOpsResourceReference(tenantID, { resource_type: resourceType, reference_name: referenceName.trim(), display_name: referenceDisplayName.trim(), verified_at: new Date().toISOString() }, accessToken);
setReferenceName(""); setReferenceDisplayName(""); setMessage("受控资源引用已登记;系统未接收或保存资源密钥明文。"); await load();
});
}
function openResourceRevoke(item: OpsResourceReference) {
setEvaluationDraft(undefined); setVersionTransitionDraft(undefined); setConfigControlDraft({ kind: "resource_revoke", item, reason: "", acknowledged: false }); setError(undefined); setMessage(undefined);
}
function openRolloutTransition(action: "pause" | "resume", item: OpsConfigRollout) {
setEvaluationDraft(undefined); setVersionTransitionDraft(undefined); setConfigControlDraft({ kind: action === "pause" ? "rollout_pause" : "rollout_resume", item, reason: "", acknowledged: false }); setError(undefined); setMessage(undefined);
}
async function submitConfigControl(event: FormEvent) {
event.preventDefault(); const draft = configControlDraft;
if (!draft || !configControlDraftReady(draft)) return;
await run(async () => {
if (draft.kind === "resource_revoke") {
await platformApi.revokeOpsResourceReference(tenantID, draft.item, draft.reason.trim(), accessToken);
setMessage("资源引用已撤销;后续依赖该引用的配置发布将被门禁阻断。");
} else if (draft.kind === "rollout_pause") {
await platformApi.pauseOpsConfigRollout(tenantID, draft.item, draft.reason.trim(), accessToken);
setMessage("配置投放已暂停,后续新建通话不再命中该范围。");
} else {
await platformApi.resumeOpsConfigRollout(tenantID, draft.item, draft.reason.trim(), accessToken);
setMessage("配置投放已恢复,后续新建通话可再次命中该范围。");
}
await load();
});
}
async function addDeviceGroup(event: FormEvent) {
event.preventDefault();
const device_ids = groupDeviceIDs.split(/[\s,,;;]+/).map((value) => value.trim()).filter(Boolean);
await run(async () => {
await platformApi.createOpsDeviceGroup(tenantID, { name: groupName.trim(), selector: { source: "manual" }, device_ids }, accessToken);
setGroupName(""); setGroupDeviceIDs(""); setMessage("设备灰度分组已创建,可在发布范围中选择。"); await load();
});
}
async function run(operation: () => Promise<void>, clearMessage = true) {
setBusy(true); setError(undefined); if (clearMessage) setMessage(undefined);
try { await operation(); } catch (cause) { setError(errorText(cause)); } finally { setBusy(false); }
}
if (!opsAccounts.length) return null;
return <section className="ops-workspace" aria-label="内部运营配置工作台">
<header className="ops-workspace__head"><div><p className="eyebrow">OPERATIONS CONTROL</p><h2>配置发布与版本控制</h2><p>发布前必须具备通过的离线评测;密钥、模型和音色仅保存经核验的受控引用。</p></div><label><span>运营客户组织</span><select value={tenantID} onChange={(event) => setTenantID(event.target.value)}>{opsAccounts.map((item) => <option key={item.user_id} value={item.tenant_id}>{item.tenant_name} · {opsRoleLabel(item.platform_role)}</option>)}</select></label></header>
{error ? <div className="notice notice--error" role="alert"><strong>操作未完成</strong><p>{error}</p></div> : null}
{message ? <div className="notice" role="status"><strong>操作成功</strong><p>{message}</p></div> : null}
{canDraft ? <div className="ops-gate-grid">
<form className="ops-gate-card" onSubmit={(event) => void addResourceReference(event)}><strong>受控资源引用</strong><p>引用须在180天内核验;不接收密钥正文。</p><label><span>资源类型</span><select value={resourceType} onChange={(event) => setResourceType(event.target.value as typeof resourceType)}><option value="PROVIDER_CREDENTIAL">供应商凭据</option><option value="VOICE_RESOURCE">音色资源</option><option value="MODEL_RESOURCE">模型资源</option></select></label><label><span>引用标识</span><input value={referenceName} onChange={(event) => setReferenceName(event.target.value)} placeholder="kms://provider/prod" required /></label><label><span>显示名称</span><input value={referenceDisplayName} onChange={(event) => setReferenceDisplayName(event.target.value)} required /></label><button type="submit" disabled={busy}>登记引用</button><small>当前有效引用:{resourceRefs.filter((item) => item.status === "ACTIVE").length}</small></form>
<form className="ops-gate-card" onSubmit={(event) => void addDeviceGroup(event)}><strong>设备灰度分组</strong><p>成员必须属于当前客户组织,跨租户编号会被拒绝。</p><label><span>分组名称</span><input value={groupName} onChange={(event) => setGroupName(event.target.value)} required /></label><label><span>设备公开编号</span><textarea rows={3} value={groupDeviceIDs} onChange={(event) => setGroupDeviceIDs(event.target.value)} placeholder="每行一个UUID,也可用逗号分隔" required /></label><button type="submit" disabled={busy}>创建设备组</button><small>当前有效设备组:{deviceGroups.filter((item) => item.status === "ACTIVE").length}</small></form>
</div> : null}
<div className="ops-ledger-grid">
<section className="ops-ledger"><div><strong>配置投放控制</strong><span>{controllableRollouts.filter((item) => item.status === "ACTIVE").length} 个活动 · {controllableRollouts.filter((item) => item.status === "PAUSED").length} 个暂停</span></div>{controllableRollouts.length ? <ul>{controllableRollouts.map((item) => <li key={item.id}><span><strong>{item.config_key}</strong><small>{scopeLabel(item.scope_type)}{item.scope_id ? ` · ${item.scope_id}` : ""} · {item.status === "ACTIVE" ? "投放中" : "已暂停"} · 行版本 v{item.row_version}</small></span><span>{item.rollout_percent}% · 并发≤{item.max_concurrent_calls}</span>{canApprove ? <button type="button" className={`button-small ${item.status === "ACTIVE" ? "button-danger" : ""}`} disabled={busy} onClick={() => openRolloutTransition(item.status === "ACTIVE" ? "pause" : "resume", item)}>{item.status === "ACTIVE" ? "暂停" : "恢复"}</button> : null}</li>)}</ul> : <p>尚无活动或暂停的配置投放。</p>}</section>
<section className="ops-ledger"><div><strong>资源引用状态</strong><span>{resourceRefs.length} 条</span></div>{resourceRefs.length ? <ul>{resourceRefs.map((item) => <li key={item.id}><span><strong>{item.display_name}</strong><small>{item.reference_name} · {item.status === "ACTIVE" ? "有效" : "已撤销"} · 行版本 v{item.row_version}</small></span>{item.status === "ACTIVE" && canDraft ? <button type="button" className="button-small button-danger" disabled={busy} onClick={() => openResourceRevoke(item)}>撤销</button> : null}</li>)}</ul> : <p>尚未登记资源引用。</p>}</section>
</div>
{configControlDraft ? <form className={`sensitive-access-request controlled-operation ops-config-control ${configControlDraft.kind === "rollout_resume" ? "" : "controlled-operation--danger"}`} aria-label="运营配置受控操作" onSubmit={(event) => void submitConfigControl(event)}><div className="sensitive-access-request__head"><div><p className="eyebrow">CONTROLLED CONFIG OPERATION</p><h3>{configControlTitle(configControlDraft.kind)}</h3></div><span className={`status ${configControlDraft.kind === "rollout_resume" ? "warning" : "danger"}`}>{configControlDraft.kind === "rollout_resume" ? "恢复前复核" : "高影响操作"}</span></div>{configControlDraft.kind === "resource_revoke" ? <p>资源 <strong>{configControlDraft.item.id}</strong> · {configControlDraft.item.display_name} · {configControlDraft.item.resource_type} · {configControlDraft.item.reference_name} · 当前有效 · 行版本 v{configControlDraft.item.row_version}。撤销只改变受控引用状态,不删除外部密钥,也不改写已冻结的历史会话。</p> : <p>投放 <strong>{configControlDraft.item.id}</strong> · {configControlDraft.item.config_key} · {scopeLabel(configControlDraft.item.scope_type)}{configControlDraft.item.scope_id ? ` ${configControlDraft.item.scope_id}` : ""} · {configControlDraft.item.status === "ACTIVE" ? "投放中" : "已暂停"} · {configControlDraft.item.rollout_percent}% · 并发≤{configControlDraft.item.max_concurrent_calls} · 行版本 v{configControlDraft.item.row_version}。已建立通话继续使用创建时冻结的配置。</p>}<label><span>{configControlDraft.kind === "resource_revoke" ? "撤销原因" : configControlDraft.kind === "rollout_pause" ? "暂停原因和观察依据" : "恢复原因和确认依据"}</span><textarea autoFocus required maxLength={500} value={configControlDraft.reason} onChange={(event) => setConfigControlDraft({ ...configControlDraft, reason: event.target.value })} placeholder={configControlReasonPlaceholder(configControlDraft.kind)} /></label><label className="controlled-operation__confirmation"><input type="checkbox" checked={configControlDraft.acknowledged} onChange={(event) => setConfigControlDraft({ ...configControlDraft, acknowledged: event.target.checked })} />{configControlAcknowledgement(configControlDraft.kind)}</label><div className="sensitive-access-request__actions"><button type="button" className="button-secondary" disabled={busy} onClick={() => setConfigControlDraft(undefined)}>取消</button><button type="submit" className={configControlDraft.kind === "rollout_resume" ? "" : "button-danger"} disabled={busy || !configControlDraftReady(configControlDraft)}>{busy ? "正在处理…" : configControlSubmitLabel(configControlDraft.kind)}</button></div></form> : null}
{canDraft ? <form className="ops-config-form" onSubmit={(event) => void create(event)}>
<div className="ops-config-form__intro"><strong>新建不可变草稿版本</strong><span>提交时执行结构、必填字段、敏感键和密钥引用检查。</span></div>
<label><span>配置类型</span><select value={kind} onChange={(event) => changeKind(event.target.value as Kind)}><option value="PERSONA">人设</option><option value="VOICE">音色</option><option value="MODEL">模型</option><option value="ROUTING">语音路由</option><option value="AGENT_BUNDLE">配置组合</option></select></label>
<label><span>版本族配置键</span><input value={configKey} pattern="[a-z][a-z0-9_.-]{2,99}" onChange={(event) => setConfigKey(event.target.value)} required /><small>{kind === "AGENT_BUNDLE" ? "可使用业务自定义键;发布后按灰度范围自动参与新通话选择。" : "同一键下的版本形成连续、可回滚的发布历史。"}</small></label>
<label><span>显示名称</span><input value={displayName} maxLength={200} onChange={(event) => setDisplayName(event.target.value)} required /></label>
{kind === "AGENT_BUNDLE" ? <fieldset className="ops-bundle-composer ops-config-form__wide"><legend>组合组件版本</legend>{bundleFields.map((definition) => {
const candidates = bundleCandidates.get(definition.kind) ?? [];
return <label key={definition.field}><span>{definition.label}</span><select value={bundlePayload[definition.field]} disabled={!candidates.length} required onChange={(event) => selectBundleComponent(definition.field, event.target.value)}><option value="">{candidates.length ? "请选择已发布版本" : "暂无可用版本"}</option>{candidates.map((candidate) => <option key={candidate.id} value={candidate.id}>{candidate.display_name} · {candidate.config_key} v{candidate.version_no}{candidate.status === "RETIRED" ? " · 已退役(可追溯复用)" : " · 已发布"}</option>)}</select></label>;
})}<small className={unavailableBundleKinds.length ? "ops-bundle-composer__warning" : ""}>{unavailableBundleKinds.length ? `缺少可用组件:${unavailableBundleKinds.map((item) => item.label).join("、")}。请先完成对应配置的审批发布。` : "仅列出当前客户组织内已经发布过的不可变版本;提交和发布时服务端仍会复核租户、类型与状态。"}</small></fieldset> : <label className="ops-config-form__wide"><span>配置正文(JSON)</span><textarea rows={8} value={payloadText} onChange={(event) => setPayloadText(event.target.value)} spellCheck={false} /></label>}
{kind === "AGENT_BUNDLE" ? <p className="ops-config-form__wide ops-config-form__note">组合不直接保存密钥引用,模型和音色凭据由各自已发布组件管理。</p> : <label className="ops-config-form__wide"><span>受控密钥引用(JSON,不得填写密钥明文)</span><textarea rows={3} value={secretRefsText} onChange={(event) => setSecretRefsText(event.target.value)} spellCheck={false} /></label>}
<label className="ops-config-form__wide"><span>变更说明</span><input value={summary} maxLength={500} onChange={(event) => setSummary(event.target.value)} placeholder="说明变更目的、预期影响和验证依据" required /></label>
<button type="submit" disabled={busy || !summary.trim() || !configKey.trim() || !displayName.trim() || (kind === "AGENT_BUNDLE" && !bundleReady)}>创建草稿</button>
</form> : <p className="workspace__readonly-note">当前运营角色只能查看版本历史,不能创建配置。</p>}
<div className="ops-version-list"><div className="section-title"><h3>配置版本历史</h3><span>{items.length}</span></div>
{loading ? <p className="empty-copy">正在加载配置版本…</p> : versionGroups.length ? versionGroups.map(([key, versions]) => <section className="ops-version-group" key={key}><div><strong>{versions[0].display_name}</strong><span>{key} · {configKindLabel(versions[0].config_kind)}</span></div>{versions.map((item, index) => <article className="ops-version" key={item.id}>
<div className="ops-version__summary"><strong>v{item.version_no} · {configStatusLabel(item.status)}</strong><span>{item.change_summary}</span><small>行版本 v{item.row_version} · {formatDate(item.updated_at)}</small></div>
<details><summary>查看配置与版本差异</summary><div className="ops-diff"><pre>{JSON.stringify(index < versions.length - 1 ? versions[index + 1].payload : {}, null, 2)}</pre><pre>{JSON.stringify(item.payload, null, 2)}</pre></div><p>密钥引用:{Object.values(item.secret_refs).join("、") || "无"}</p></details>
{evaluations[item.id] ? <ul className="ops-evidence-list">{evaluations[item.id].map((evaluation) => <li key={evaluation.id}><strong>{evaluation.result === "PASSED" ? "通过" : "失败"}</strong><span>{evaluation.evidence_type === "AGENT_QUALITY_V1" ? "Agent标准质量" : "通用离线"} · {evaluation.suite_version} · {evaluation.report_ref}</span></li>)}</ul> : null}
<div className="ops-version__actions"><button type="button" className="button-small" disabled={busy} onClick={() => void loadEvaluations(item.id)}>评测证据</button>{(item.status === "DRAFT" || item.status === "PENDING_APPROVAL") && canDraft && item.config_kind !== "AGENT_BUNDLE" ? <button type="button" className="button-small" disabled={busy} onClick={() => openEvaluation(item)}>登记通用离线评测</button> : null}{item.status === "DRAFT" && canDraft ? <button type="button" className="button-small" disabled={busy} onClick={() => openVersionTransition("submit", item)}>提交审批</button> : null}{item.status === "PENDING_APPROVAL" && canApprove ? <button type="button" className="button-small" disabled={busy} onClick={() => openVersionTransition("publish", item)}>审批并发布</button> : null}{item.status === "RETIRED" && canApprove ? <button type="button" className="button-small button-danger" disabled={busy} onClick={() => openVersionTransition("rollback", item)}>回滚到此内容</button> : null}</div>
{versionTransitionDraft?.item.id === item.id ? <form className={`sensitive-access-request controlled-operation ops-version-transition ${versionTransitionDraft.kind === "rollback" ? "controlled-operation--danger" : ""}`} aria-label="配置版本受控流转" onSubmit={(event) => void submitVersionTransition(event)}><div className="sensitive-access-request__head"><div><p className="eyebrow">CONTROLLED VERSION TRANSITION</p><h4>{versionTransitionTitle(versionTransitionDraft.kind)}</h4></div><span className={`status ${versionTransitionDraft.kind === "rollback" ? "danger" : "warning"}`}>{versionTransitionDraft.kind === "submit" ? "进入审批" : versionTransitionDraft.kind === "publish" ? "双人审批" : "历史内容回滚"}</span></div><p>配置 <strong>{item.id}</strong> · {configKindLabel(item.config_kind)} · {item.config_key} v{item.version_no} · {configStatusLabel(item.status)} · 行版本 v{item.row_version} · 创建人 {item.created_by}。变更说明:{item.change_summary}</p>{versionTransitionDraft.kind === "submit" ? <p className="recording-notice">本操作只把不可变草稿送入审批,不会发布配置或改变现有投放;提交后必须由另一名具备审批权限的人员完成发布。</p> : <div className="ops-rollout-panel ops-version-transition__scope"><div><strong>本次确认的发布范围</strong><p>范围参数只属于当前固定版本;切换目标或取消后不会保留为其他版本的隐式参数。</p></div><label><span>范围类型</span><select value={versionTransitionDraft.scopeType} onChange={(event) => { const scopeType = event.target.value as Scope; setVersionTransitionDraft({ ...versionTransitionDraft, scopeType, scopeID: "", rolloutPercent: scopeType === "SUBJECT" ? 100 : versionTransitionDraft.rolloutPercent, acknowledged: false }); }}><option value="TENANT">整个客户组织</option><option value="DEVICE_GROUP">设备组</option><option value="SUBJECT">单个试验对象</option></select></label>{versionTransitionDraft.scopeType === "DEVICE_GROUP" ? <label><span>设备组</span><select required value={versionTransitionDraft.scopeID} onChange={(event) => setVersionTransitionDraft({ ...versionTransitionDraft, scopeID: event.target.value, acknowledged: false })}><option value="">请选择</option>{deviceGroups.filter((group) => group.status === "ACTIVE").map((group) => <option key={group.id} value={group.id}>{group.name} · {group.device_ids.length}台</option>)}</select></label> : versionTransitionDraft.scopeType === "SUBJECT" ? <label><span>试验对象公开编号</span><input required value={versionTransitionDraft.scopeID} onChange={(event) => setVersionTransitionDraft({ ...versionTransitionDraft, scopeID: event.target.value.trim(), acknowledged: false })} placeholder="UUID" /></label> : null}<label><span>灰度比例(%)</span><input type="number" min={1} max={100} value={versionTransitionDraft.scopeType === "SUBJECT" ? 100 : versionTransitionDraft.rolloutPercent} disabled={versionTransitionDraft.scopeType === "SUBJECT"} onChange={(event) => setVersionTransitionDraft({ ...versionTransitionDraft, rolloutPercent: Number(event.target.value), acknowledged: false })} /></label><label><span>并发影响上限</span><input type="number" min={1} max={100000} value={versionTransitionDraft.maxConcurrentCalls} onChange={(event) => setVersionTransitionDraft({ ...versionTransitionDraft, maxConcurrentCalls: Number(event.target.value), acknowledged: false })} /></label><label className="ops-rollout-panel__reason"><span>{versionTransitionDraft.kind === "publish" ? "审批发布依据" : "回滚依据和故障说明"}</span><textarea autoFocus required maxLength={500} value={versionTransitionDraft.reason} onChange={(event) => setVersionTransitionDraft({ ...versionTransitionDraft, reason: event.target.value, acknowledged: false })} placeholder={versionTransitionDraft.kind === "publish" ? "填写评测复核、审批依据、影响范围和观察计划" : "填写故障现象、历史版本选择依据、影响范围和回退观察计划"} /></label></div>}<label clas
{evaluationDraft?.item.id === item.id ? <form className="sensitive-access-request controlled-operation ops-evaluation-editor" aria-label="通用离线评测登记" onSubmit={(event) => void addEvaluation(event)}><div className="sensitive-access-request__head"><div><p className="eyebrow">GENERIC OFFLINE EVALUATION</p><h4>登记通用离线评测证据</h4></div><span className="status warning">不会替代Agent标准评测</span></div><p>配置 <strong>{item.id}</strong> · {configKindLabel(item.config_kind)} · {item.config_key} v{item.version_no} · 行版本 v{item.row_version}。本表单只登记通用证据,Agent配置组合和标准质量报告必须使用受控评测工具。</p><label><span>评测套件版本</span><input autoFocus required maxLength={100} value={evaluationDraft.suiteVersion} onChange={(event) => setEvaluationDraft({ ...evaluationDraft, suiteVersion: event.target.value })} placeholder="如 voice-routing-regression-v3" /></label><label><span>评测报告引用</span><input required maxLength={300} value={evaluationDraft.reportRef} onChange={(event) => setEvaluationDraft({ ...evaluationDraft, reportRef: event.target.value })} placeholder="填写可追溯的报告编号或受控存储引用" /></label><fieldset><legend>评测结论</legend><label><input type="radio" name={`evaluation-result-${item.id}`} checked={evaluationDraft.result === "PASSED"} onChange={() => setEvaluationDraft({ ...evaluationDraft, result: "PASSED", acknowledged: false })} />通过</label><label><input type="radio" name={`evaluation-result-${item.id}`} checked={evaluationDraft.result === "FAILED"} onChange={() => setEvaluationDraft({ ...evaluationDraft, result: "FAILED", acknowledged: false })} />失败</label></fieldset>{evaluationDraft.result === "PASSED" ? <label className="controlled-operation__confirmation"><input type="checkbox" checked={evaluationDraft.acknowledged} onChange={(event) => setEvaluationDraft({ ...evaluationDraft, acknowledged: event.target.checked })} />我确认报告内容和指标支持“通过”结论,并理解该记录可能参与当前配置的发布门禁</label> : null}<div className="sensitive-access-request__actions"><button type="button" className="button-secondary" disabled={busy} onClick={() => setEvaluationDraft(undefined)}>取消</button><button type="submit" disabled={busy || !evaluationDraftReady(evaluationDraft)}>{busy ? "正在登记…" : "登记评测证据"}</button></div></form> : null}
{item.config_kind === "AGENT_BUNDLE" && (item.status === "DRAFT" || item.status === "PENDING_APPROVAL") ? <small>Agent配置组合只接受由固定评测集、观测文件和报告共同校验后登记的标准质量证据。</small> : null}
</article>)}</section>) : <p className="empty-copy">当前客户组织尚未创建运行配置版本。</p>}
</div>
</section>;
}
function defaultPayload(kind: Kind) {
const values: Record<Kind, unknown> = {
PERSONA: { system_prompt: "你是一位耐心、尊重长者的陪伴型语音助手。", tone: "warm" },
VOICE: { provider: "volcengine", voice_resource_ref: "voice://approved/default" }, MODEL: { provider: "volcengine", model_id: "model://approved/default" },
ROUTING: { default_engine_mode: "s2s_hybrid", routes: [{ goal: "open_conversation", engine_mode: "s2s_hybrid" }] },
AGENT_BUNDLE: { persona_version_id: "", voice_version_id: "", model_version_id: "", routing_version_id: "" }
};
return JSON.stringify(values[kind], null, 2);
}
function readBundlePayload(value: string): Record<BundleField, string> {
const empty: Record<BundleField, string> = { persona_version_id: "", voice_version_id: "", model_version_id: "", routing_version_id: "" };
try {
const parsed = JSON.parse(value) as Record<string, unknown>;
for (const definition of bundleFields) if (typeof parsed[definition.field] === "string") empty[definition.field] = String(parsed[definition.field]);
} catch { /* The create handler owns user-facing JSON errors for non-structured payloads. */ }
return empty;
}
function defaultAgentBundlePayload(items: OpsConfigVersion[]) {
const selected = readBundlePayload(defaultPayload("AGENT_BUNDLE"));
for (const definition of bundleFields) {
const candidate = componentCandidates(items, definition.kind)[0];
selected[definition.field] = candidate?.id ?? "";
}
return JSON.stringify(selected, null, 2);
}
function componentCandidates(items: OpsConfigVersion[], kind: Exclude<Kind, "AGENT_BUNDLE">) {
return items.filter((item) => item.config_kind === kind && (item.status === "PUBLISHED" || item.status === "RETIRED")).sort((left, right) => {
if (left.status !== right.status) return left.status === "PUBLISHED" ? -1 : 1;
const publishedOrder = new Date(right.published_at ?? right.updated_at).getTime() - new Date(left.published_at ?? left.updated_at).getTime();
return publishedOrder || right.version_no - left.version_no || left.config_key.localeCompare(right.config_key);
});
}
function evaluationDraftReady(draft: EvaluationDraft) { return Boolean(draft.suiteVersion.trim() && draft.reportRef.trim() && draft.result && (draft.result !== "PASSED" || draft.acknowledged)); }
function configControlDraftReady(draft: ConfigControlDraft) { return Boolean(draft.reason.trim() && draft.acknowledged); }
function versionTransitionDraftReady(draft: VersionTransitionDraft) {
if (!draft.acknowledged) return false;
if (draft.kind === "submit") return true;
return Boolean(draft.reason.trim() && (draft.scopeType === "TENANT" || draft.scopeID.trim()) && draft.rolloutPercent >= 1 && draft.rolloutPercent <= 100 && draft.maxConcurrentCalls >= 1 && draft.maxConcurrentCalls <= 100000);
}
function versionTransitionTitle(kind: VersionTransitionDraft["kind"]) { return ({ submit: "提交配置版本审批", publish: "审批并发布配置版本", rollback: "回滚到历史配置内容" } as const)[kind]; }
function versionTransitionSubmitLabel(kind: VersionTransitionDraft["kind"]) { return ({ submit: "确认提交审批", publish: "确认审批并发布", rollback: "确认创建回滚版本" } as const)[kind]; }
function versionTransitionAcknowledgement(draft: VersionTransitionDraft) {
if (draft.kind === "submit") return "我确认目标版本、变更说明和配置内容已经复核;提交只进入审批,不会直接影响现有或后续通话";
const range = `${scopeLabel(draft.scopeType)}${draft.scopeID ? ` ${draft.scopeID}` : ""}、${draft.scopeType === "SUBJECT" ? 100 : draft.rolloutPercent}%灰度、并发上限${draft.maxConcurrentCalls}`;
return draft.kind === "publish"
? `我确认离线评测、审批职责和发布范围(${range})均已复核;发布只影响后续新建通话,既有通话继续使用冻结配置`
: `我确认已核对目标历史内容和回滚范围(${range}),理解系统会创建新的不可变版本并只影响后续新建通话,且已准备观察和再次回退方案`;
}
function configControlTitle(kind: ConfigControlDraft["kind"]) { return ({ resource_revoke: "撤销受控资源引用", rollout_pause: "暂停配置投放", rollout_resume: "恢复配置投放" } as const)[kind]; }
function configControlSubmitLabel(kind: ConfigControlDraft["kind"]) { return ({ resource_revoke: "确认撤销引用", rollout_pause: "确认暂停投放", rollout_resume: "确认恢复投放" } as const)[kind]; }
function configControlReasonPlaceholder(kind: ConfigControlDraft["kind"]) { return ({ resource_revoke: "说明撤销依据、替代资源和影响范围", rollout_pause: "说明异常现象、暂停依据和观察计划", rollout_resume: "说明问题已消除的证据、恢复范围和观察计划" } as const)[kind]; }
function configControlAcknowledgement(kind: ConfigControlDraft["kind"]) { return ({ resource_revoke: "我确认后续依赖该引用的配置发布将被阻断,并已核对替代资源;本操作不会删除外部密钥或改写历史会话", rollout_pause: "我确认该投放将不再被后续新建通话选中;已建立通话仍继续使用冻结配置", rollout_resume: "我确认目标配置仍可发布、同范围没有其他活动投放,并已准备恢复后的观察和回退方案" } as const)[kind]; }
function errorText(value: unknown) { return value instanceof APIRequestError || value instanceof Error ? value.message : "未知错误"; }
function opsRoleLabel(value: string) { return ({ ops_viewer: "运营只读", ops_operator: "运营操作", ops_approver: "配置审批", ops_admin: "平台管理员" } as Record<string, string>)[value] ?? value; }
function configKindLabel(value: string) { return ({ PERSONA: "人设", VOICE: "音色", MODEL: "模型", ROUTING: "路由", AGENT_BUNDLE: "配置组合" } as Record<string, string>)[value] ?? value; }
function configStatusLabel(value: string) { return ({ DRAFT: "草稿", PENDING_APPROVAL: "待审批", PUBLISHED: "已发布", RETIRED: "已退役" } as Record<string, string>)[value] ?? value; }
function scopeLabel(value: string) { return ({ TENANT: "客户组织", SUBJECT: "试验对象", DEVICE_GROUP: "设备组" } as Record<string, string>)[value] ?? value; }
function formatDate(value: string) { return new Intl.DateTimeFormat("zh-CN", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" }).format(new Date(value)); }