import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Bot, Link2, Loader2, Plus, RefreshCw, Settings2 } from 'lucide-react'; import { FeedbackState } from '@/components/common/FeedbackState'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; import { AiHardwareApiError, bindAiHardwareDevice, createAiHardwareAgent, getAiHardwareAgentConfiguration, getAiHardwareAssignment, getAiHardwareOverview, recoverAiHardwareCredential, updateAiHardwareAgentConfiguration, updateAiHardwareAssignment, type AiHardwareAgent, type AiHardwareAgentConfiguration, type AiHardwareAgentConfigurationUpdate, type AiHardwareClearableField, type AiHardwareDevice, type AiHardwareOverview, } from '@/lib/ai-hardware'; type PageState = 'loading' | 'ready' | 'disabled' | 'auth' | 'error'; type BindRetryIntent = { agentId: string; fingerprint: ArrayBuffer; operationId: string }; type ConfigDraft = { agent_name: string; system_prompt: string; language: string; lang_code: string; asr_model_id: string; vad_model_id: string; llm_model_id: string; slm_model_id: string; vllm_model_id: string; tts_model_id: string; tts_voice_id: string; tts_volume: string; tts_rate: string; tts_pitch: string; tts_language: string; mem_model_id: string; intent_model_id: string; chat_history_conf: string; }; const nullableTextFields = [ 'system_prompt', 'language', 'lang_code', 'asr_model_id', 'vad_model_id', 'llm_model_id', 'slm_model_id', 'vllm_model_id', 'tts_model_id', 'tts_voice_id', 'tts_language', 'mem_model_id', 'intent_model_id', ] as const; const nullableNumberFields = ['tts_volume', 'tts_rate', 'tts_pitch'] as const; const shortId = (value: string) => value.length > 12 ? `${value.slice(0, 6)}…${value.slice(-4)}` : value; const isRevisionConflict = (error: unknown) => error instanceof AiHardwareApiError && ['ai_hardware_revision_conflict', 'REVISION_MISMATCH', 'AI_HARDWARE_REVISION_CONFLICT'].includes(error.code); function safeMessage(error: unknown): string { if (!(error instanceof AiHardwareApiError)) return '操作没有完成,请稍后重试。'; if (isRevisionConflict(error)) return '内容已在其他位置更新,请重新核对后再保存。'; const messages: Record = { AI_HARDWARE_AUTH_REQUIRED: '请先登录 Works Square,然后重试。', AI_HARDWARE_FORBIDDEN: '当前账号无权执行此操作。', AI_HARDWARE_DISABLED: 'AI 机器模块尚未启用,请联系管理员。', ai_hardware_unconfigured: 'AI 机器服务尚未配置,请联系管理员。', ai_hardware_credential_recovery_required: '需要管理员恢复设备凭据后才能继续。', ai_hardware_credential_unavailable: '设备凭据暂不可用,请联系管理员恢复。', ai_hardware_credential_recovery_unavailable: '当前状态无需或不能恢复凭据,请刷新查看最新状态。', ai_hardware_activation_code_invalid: '激活码无效或已过期,请从设备上获取新激活码。', ai_hardware_device_already_bound: '设备已被绑定,无法重复绑定。', ai_hardware_idempotency_conflict: '本次输入与待重试操作不一致,请再次提交以启动新操作。', AI_HARDWARE_RATE_LIMITED: '请求过于频繁,请稍后重试。', }; if (error.code === 'ai_hardware_operation_in_progress') { return error.retryAfterSeconds === null ? '操作仍在处理中,请稍后重试。' : `操作仍在处理中,请在 ${error.retryAfterSeconds} 秒后重试。`; } return messages[error.code] ?? (error.retryable ? '服务暂时不可用,请稍后重试。' : '操作没有完成,请稍后重试。'); } const retryOperationId = (error: unknown): string | null => error instanceof AiHardwareApiError && (error.retryable || error.code === 'ai_hardware_operation_in_progress') ? error.operationId : null; function draftFrom(config: AiHardwareAgentConfiguration): ConfigDraft { return { agent_name: config.name, system_prompt: config.system_prompt ?? '', language: config.language ?? '', lang_code: config.lang_code ?? '', asr_model_id: config.asr_model_id ?? '', vad_model_id: config.vad_model_id ?? '', llm_model_id: config.llm_model_id ?? '', slm_model_id: config.slm_model_id ?? '', vllm_model_id: config.vllm_model_id ?? '', tts_model_id: config.tts_model_id ?? '', tts_voice_id: config.tts_voice_id ?? '', tts_volume: config.tts_volume?.toString() ?? '', tts_rate: config.tts_rate?.toString() ?? '', tts_pitch: config.tts_pitch?.toString() ?? '', tts_language: config.tts_language ?? '', mem_model_id: config.mem_model_id ?? '', intent_model_id: config.intent_model_id ?? '', chat_history_conf: config.chat_history_conf?.toString() ?? '', }; } function configurationUpdate(config: AiHardwareAgentConfiguration, draft: ConfigDraft): AiHardwareAgentConfigurationUpdate { const update: AiHardwareAgentConfigurationUpdate = {}; const clearFields: AiHardwareClearableField[] = []; if (draft.agent_name.trim() !== config.name) update.agent_name = draft.agent_name.trim(); for (const field of nullableTextFields) { const value = draft[field].trim(); if (!value && config[field] !== null) clearFields.push(field); else if (value && value !== config[field]) update[field] = value; } for (const field of nullableNumberFields) { const raw = draft[field].trim(); if (!raw && config[field] !== null) clearFields.push(field); else if (raw && Number(raw) !== config[field]) update[field] = Number(raw); } if (draft.chat_history_conf !== '') { const history = Number(draft.chat_history_conf); if (history !== config.chat_history_conf) update.chat_history_conf = history; } if (clearFields.length) update.clear_fields = clearFields; return update; } function replayConfigurationUpdate( config: AiHardwareAgentConfiguration, update: AiHardwareAgentConfigurationUpdate, ): ConfigDraft { const next = draftFrom(config); if (update.agent_name !== undefined) next.agent_name = update.agent_name; for (const field of nullableTextFields) { if (update[field] !== undefined) next[field] = update[field]; if (update.clear_fields?.includes(field)) next[field] = ''; } for (const field of nullableNumberFields) { if (update[field] !== undefined) next[field] = update[field].toString(); if (update.clear_fields?.includes(field)) next[field] = ''; } if (update.chat_history_conf !== undefined) next.chat_history_conf = update.chat_history_conf.toString(); return next; } async function createBindFingerprintKey(): Promise { return crypto.subtle.generateKey({ name: 'HMAC', hash: 'SHA-256' }, false, ['sign']); } async function bindFingerprint(key: CryptoKey, activationCode: string): Promise { return crypto.subtle.sign('HMAC', key, new TextEncoder().encode(activationCode)); } function equalFingerprint(left: ArrayBuffer, right: ArrayBuffer): boolean { const a = new Uint8Array(left); const b = new Uint8Array(right); if (a.length !== b.length) return false; let difference = 0; for (let index = 0; index < a.length; index += 1) difference |= a[index] ^ b[index]; return difference === 0; } export function AiHardware() { const [pageState, setPageState] = useState('loading'); const [overview, setOverview] = useState(null); const [selectedAgentId, setSelectedAgentId] = useState(null); const [config, setConfig] = useState(null); const [configRevision, setConfigRevision] = useState(null); const [configLoading, setConfigLoading] = useState(false); const [configLoadFailed, setConfigLoadFailed] = useState(false); const [configReloadKey, setConfigReloadKey] = useState(0); const [notice, setNotice] = useState(null); const [createOpen, setCreateOpen] = useState(false); const [bindOpen, setBindOpen] = useState(false); const [configOpen, setConfigOpen] = useState(false); const [assignmentDevice, setAssignmentDevice] = useState(null); const [agentName, setAgentName] = useState(''); const [dialogAgentId, setDialogAgentId] = useState(''); const [draft, setDraft] = useState(null); const [dialogError, setDialogError] = useState(null); const [busy, setBusy] = useState(false); const [createOperationId, setCreateOperationId] = useState(null); const [configOperationId, setConfigOperationId] = useState(null); const [assignmentOperationId, setAssignmentOperationId] = useState(null); const [createOperationBody, setCreateOperationBody] = useState(null); const [configOperationBody, setConfigOperationBody] = useState(null); const [assignmentOperationBody, setAssignmentOperationBody] = useState(null); const [recoveryOperationId, setRecoveryOperationId] = useState(null); const [recoveryError, setRecoveryError] = useState(null); const [recoveryBusy, setRecoveryBusy] = useState(false); const activationCodeInputRef = useRef(null); const bindFingerprintKeyRef = useRef(null); const bindRetryIntentRef = useRef(null); const clearCreateOperation = () => { setCreateOperationId(null); setCreateOperationBody(null); }; const clearBindOperation = () => { bindRetryIntentRef.current = null; bindFingerprintKeyRef.current = null; if (activationCodeInputRef.current) activationCodeInputRef.current.value = ''; }; const clearConfigOperation = () => { setConfigOperationId(null); setConfigOperationBody(null); }; const clearAssignmentOperation = () => { setAssignmentOperationId(null); setAssignmentOperationBody(null); }; const recoverCredential = async () => { if (recoveryBusy) return; setRecoveryBusy(true); setRecoveryError(null); try { const value = recoveryOperationId ? await recoverAiHardwareCredential({ operationId: recoveryOperationId }) : await recoverAiHardwareCredential(); setRecoveryOperationId(null); setOverview(value); setPageState('ready'); setSelectedAgentId(value.agents[0]?.id ?? null); } catch (error) { if (error instanceof AiHardwareApiError && error.code === 'ai_hardware_credential_recovery_unavailable') { setRecoveryOperationId(null); await loadOverview(); return; } setRecoveryOperationId(retryOperationId(error)); setRecoveryError(safeMessage(error)); } finally { setRecoveryBusy(false); } }; const loadOverview = useCallback(async () => { setPageState('loading'); setNotice(null); try { const value = await getAiHardwareOverview(); setOverview(value); setPageState('ready'); setSelectedAgentId((current) => value.agents.some((item) => item.id === current) ? current : value.agents[0]?.id ?? null); } catch (error) { setOverview(null); if (error instanceof AiHardwareApiError && error.code === 'AI_HARDWARE_AUTH_REQUIRED') setPageState('auth'); else if (error instanceof AiHardwareApiError && ['AI_HARDWARE_DISABLED', 'ai_hardware_unconfigured'].includes(error.code)) setPageState('disabled'); else setPageState('error'); } }, []); useEffect(() => { void loadOverview(); }, [loadOverview]); useEffect(() => { if (!selectedAgentId) { setConfig(null); setConfigRevision(null); setConfigLoading(false); setConfigLoadFailed(false); return; } let active = true; setConfig(null); setConfigRevision(null); setConfigLoadFailed(false); setNotice(null); setConfigLoading(true); void getAiHardwareAgentConfiguration(selectedAgentId).then((result) => { if (!active) return; setConfig(result.data); setConfigRevision(result.revision); }).catch(() => { if (!active) return; setConfigLoadFailed(true); setNotice('无法读取智能体配置,请刷新后重试。'); }) .finally(() => { if (active) setConfigLoading(false); }); return () => { active = false; }; }, [configReloadKey, selectedAgentId]); const selectedAgent = overview?.agents.find((item) => item.id === selectedAgentId) ?? null; const devices = useMemo(() => overview?.devices ?? [], [overview]); const resetDialog = () => { setDialogError(null); setBusy(false); clearCreateOperation(); clearBindOperation(); clearConfigOperation(); clearAssignmentOperation(); }; const createAgent = async () => { const name = agentName.trim(); if (!name || name.length > 64) { setDialogError('名称需要包含 1–64 个字符。'); return; } setBusy(true); setDialogError(null); try { const canRetry = createOperationId && createOperationBody === name; const agent = canRetry ? await createAiHardwareAgent(name, { operationId: createOperationId }) : await createAiHardwareAgent(name); setCreateOperationId(null); setCreateOperationBody(null); setCreateOpen(false); setAgentName(''); await loadOverview(); setSelectedAgentId(agent.id); } catch (error) { setCreateOperationId(retryOperationId(error)); setCreateOperationBody(name); setDialogError(safeMessage(error)); } finally { setBusy(false); } }; const bindDevice = async () => { const activationCode = activationCodeInputRef.current?.value ?? ''; if (!/^[0-9]{6}$/.test(activationCode)) { setDialogError('请输入 6 位数字激活码。'); return; } if (!dialogAgentId) { setDialogError('请选择要绑定的智能体。'); return; } setBusy(true); setDialogError(null); try { const key = bindFingerprintKeyRef.current ?? await createBindFingerprintKey(); bindFingerprintKeyRef.current = key; const fingerprint = await bindFingerprint(key, activationCode); const retry = bindRetryIntentRef.current; if (retry?.agentId === dialogAgentId && equalFingerprint(retry.fingerprint, fingerprint)) { await bindAiHardwareDevice(activationCode, dialogAgentId, { operationId: retry.operationId }); } else await bindAiHardwareDevice(activationCode, dialogAgentId); clearBindOperation(); setBindOpen(false); await loadOverview(); } catch (error) { const operationId = retryOperationId(error); if (operationId && bindFingerprintKeyRef.current) { bindRetryIntentRef.current = { agentId: dialogAgentId, fingerprint: await bindFingerprint(bindFingerprintKeyRef.current, activationCode), operationId, }; } else bindRetryIntentRef.current = null; setDialogError(safeMessage(error)); if (activationCodeInputRef.current) activationCodeInputRef.current.value = ''; } finally { setBusy(false); } }; const saveConfiguration = async () => { if (!config || !draft || configRevision === null) return; if (!draft.agent_name.trim() || draft.agent_name.trim().length > 64) { setDialogError('名称需要包含 1–64 个字符。'); return; } for (const field of nullableNumberFields) { if (draft[field] && (!Number.isInteger(Number(draft[field])) || Number(draft[field]) < -100 || Number(draft[field]) > 100)) { setDialogError('音量、语速和音调必须是 -100 到 100 的整数。'); return; } } const update = configurationUpdate(config, draft); if (!Object.keys(update).length) { setConfigOpen(false); return; } setBusy(true); setDialogError(null); try { const body = JSON.stringify(update); const result = configOperationId && configOperationBody === body ? await updateAiHardwareAgentConfiguration(config.id, configRevision, update, { operationId: configOperationId }) : await updateAiHardwareAgentConfiguration(config.id, configRevision, update); setConfigOperationId(null); setConfigOperationBody(null); setConfig(result.data); setConfigRevision(result.revision); setConfigOpen(false); await loadOverview(); } catch (error) { if (isRevisionConflict(error)) { setConfigOperationId(null); const fresh = await getAiHardwareAgentConfiguration(config.id).catch(() => null); if (fresh) { setConfig(fresh.data); setConfigRevision(fresh.revision); setDraft(replayConfigurationUpdate(fresh.data, update)); } } else { setConfigOperationId(retryOperationId(error)); setConfigOperationBody(JSON.stringify(update)); } setDialogError(safeMessage(error)); } finally { setBusy(false); } }; const openAssignment = async (device: AiHardwareDevice) => { setDialogError(null); setBusy(true); setDialogAgentId(device.agent_id); setAssignmentOperationId(null); try { const result = await getAiHardwareAssignment(device.id); setAssignmentDevice({ ...result.data, assignment_revision: result.revision }); } catch { setNotice('无法读取设备指派,请刷新后重试。'); } finally { setBusy(false); } }; const saveAssignment = async () => { if (!assignmentDevice || !dialogAgentId) return; setBusy(true); setDialogError(null); try { const body = dialogAgentId; if (assignmentOperationId && assignmentOperationBody === body) await updateAiHardwareAssignment(assignmentDevice.id, assignmentDevice.assignment_revision, dialogAgentId, { operationId: assignmentOperationId }); else await updateAiHardwareAssignment(assignmentDevice.id, assignmentDevice.assignment_revision, dialogAgentId); setAssignmentOperationId(null); setAssignmentOperationBody(null); setAssignmentDevice(null); await loadOverview(); } catch (error) { if (isRevisionConflict(error)) { setAssignmentOperationId(null); const fresh = await getAiHardwareAssignment(assignmentDevice.id).catch(() => null); if (fresh) setAssignmentDevice({ ...fresh.data, assignment_revision: fresh.revision }); } else { setAssignmentOperationId(retryOperationId(error)); setAssignmentOperationBody(dialogAgentId); } setDialogError(safeMessage(error)); } finally { setBusy(false); } }; if (pageState === 'loading') return
; if (pageState === 'auth') return
void loadOverview()}>重新检查} />
; if (pageState === 'disabled') return
AI 机器尚未启用管理员启用并完成连接配置后,这里会显示你的智能体和机器人设备。
; if (pageState === 'error' || !overview) return
void loadOverview()}>重试} />
; if (overview.status === 'credential_recovery_required' || overview.status === 'invalid') return
void recoverCredential()}>{recoveryBusy && }恢复设备凭据} />{recoveryError ?

{recoveryError}

: null}
; if (overview.status === 'provisioning') return
void loadOverview()}>刷新状态} />
; return (

机器人工作台

管理智能体、机器人设备绑定与公开配置。

{notice ?

{notice}

: null} {overview.agents.length === 0 ? ( 创建第一个智能体{overview.status === 'unprovisioned' ? '创建智能体将同时开通你的机器人工作台。' : '智能体创建后,才能把机器人设备绑定给它。'} ) : (
智能体{overview.agents.length} 个
{overview.agents.map((agent) => )}
{selectedAgent?.name ?? '智能体配置'}基础对话和语音设置
{config ?
语言
{config.language || config.lang_code || '未设置'}
语音
{config.tts_voice_id || '未设置'}
系统提示
{config.system_prompt || '未设置'}
: configLoading ? : configLoadFailed ? setConfigReloadKey((value) => value + 1)}>重试读取配置} /> : null}
设备{devices.length} 台已绑定设备
{devices.length ?
{devices.map((device) =>
设备 {shortId(device.id)}指派 r{device.assignment_revision}
)}
: }
)} { if (!busy) { setCreateOpen(open); if (!open) { setAgentName(''); resetDialog(); } } }}>创建智能体输入一个容易识别的名称,稍后仍可修改。
{ setAgentName(e.target.value); clearCreateOperation(); }} />
{dialogError ?

{dialogError}

: null}
{ if (!busy) { setBindOpen(open); if (!open) resetDialog(); } }}>绑定设备激活码只用于本次绑定,关闭窗口后会立即清除。
{ e.currentTarget.value = e.currentTarget.value.replace(/\D/g, '').slice(0, 6); }} />
{ setDialogAgentId(value); clearBindOperation(); }} />{dialogError ?

{dialogError}

: null}
{ if (!busy) { setConfigOpen(open); if (!open) resetDialog(); } }}>编辑智能体配置留空可清除可选字段;保存时不会发送空值。{draft ? { setDraft(next); clearConfigOperation(); }} /> : null}{dialogError ?

{dialogError}

: null}
{ if (!open && !busy) { setAssignmentDevice(null); resetDialog(); } }}>重新指派设备选择接收这台设备的智能体。系统会检查最新修订,避免覆盖其他更改。 { setDialogAgentId(value); clearAssignmentOperation(); }} />{dialogError ?

{dialogError}

: null}
); } function AgentSelect({ agents, value, onChange }: { agents: AiHardwareAgent[]; value: string; onChange: (value: string) => void }) { return
; } function ConfigFields({ draft, setDraft }: { draft: ConfigDraft; setDraft: (draft: ConfigDraft) => void }) { const field = (key: keyof ConfigDraft, label: string, type = 'text') =>
setDraft({ ...draft, [key]: e.target.value })} />
; return
{field('agent_name', '名称')}