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; 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: "路由版本" } ]; const elderCompanionSystemPrompt = `你是一位温和、耐心、尊重长者的陪伴型语音助手。 你的目标不是快速获得答案,而是让长者愿意表达,并感到自己被认真倾听。 聊天规则: 1. 使用简单、自然、口语化的中文,每次尽量只说一到两句话。 2. 一次只问一个问题,不连续盘问;每连续提问两次后,加入一次共情、总结或简短回应。 3. 先回应长者的情绪,再继续聊天,优先围绕长者刚刚提到的人、事、物展开。 4. 长者回答很短时,可以给出两个简单选项帮助继续,但不要替长者作决定。 5. 长者沉默时不要催促,可以说“没关系,我们慢慢聊”。 6. 长者重复讲同一件事时,不要指出“已经讲过”,应接住其中的情绪或细节。 7. 不要编造没有提供过的家庭信息、经历、记忆或事实;不确定时明确表示不确定。 8. 不主动提起背景资料中标记为避免的话题;长者主动提及时先共情,再谨慎回应。 9. 涉及疾病、用药和症状时,只做一般性安全提醒,不代替医生诊断或擅自建议停药换药。 10. 长者想结束聊天时自然收尾,不强行挽留,也不为了延长对话制造话题。 11. 不纠正长者的口音、表达方式或生活选择,不使用网络流行语和复杂术语。 12. 没有合适话题时,可从天气、饮食、睡眠、花草、电视节目或家人近况中选择一个开场。 推荐流程:问候近况 -> 找到兴趣或记忆关键词 -> 温和追问一个细节 -> 回应情绪 -> 自然收尾。 表达风格应像熟悉的晚辈或朋友,不像客服、医生或采访者。`; 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([]); const [resourceRefs, setResourceRefs] = useState([]); const [deviceGroups, setDeviceGroups] = useState([]); const [rollouts, setRollouts] = useState([]); const [evaluations, setEvaluations] = useState>({}); const [evaluationDraft, setEvaluationDraft] = useState(); const [configControlDraft, setConfigControlDraft] = useState(); const [versionTransitionDraft, setVersionTransitionDraft] = useState(); const [loading, setLoading] = useState(true); const [busy, setBusy] = useState(false); const [error, setError] = useState(); const [message, setMessage] = useState(); const [kind, setKind] = useState("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(); 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, 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 = { 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; let secret_refs: Record; 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, 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

OPERATIONS CONTROL

配置发布与版本控制

发布前必须具备通过的离线评测;密钥、模型和音色仅保存经核验的受控引用。

{error ?
操作未完成

{error}

: null} {message ?
操作成功

{message}

: null} {canDraft ?
void addResourceReference(event)}>受控资源引用

引用须在180天内核验;不接收密钥正文。

当前有效引用:{resourceRefs.filter((item) => item.status === "ACTIVE").length}
void addDeviceGroup(event)}>设备灰度分组

成员必须属于当前客户组织,跨租户编号会被拒绝。