diff --git a/.project-docs/20-architecture/system-overview.md b/.project-docs/20-architecture/system-overview.md index c5924c2..cfb8f2b 100644 --- a/.project-docs/20-architecture/system-overview.md +++ b/.project-docs/20-architecture/system-overview.md @@ -22,13 +22,14 @@ Makelore 是 Electron 桌面客户端。Renderer 负责项目操作与状态展 | AI Design Image Source Picker | 当前 Workspace 已完成图片或本地上传 | 图片 Brief 绑定图生图参考图;视频 Brief 绑定首帧;提交一个真实 Asset ID | | AI Design Gateway Routing | Main 云端适配器 ↔ Conversation WebSocket;Main → Host API/SSE → Renderer store | 命令、Run 与设计事件共用双向 WebSocket;Conversation 更新按 Workspace + Conversation 路由,任务更新按 Workspace 归并 | -| Robot Workspace | Account-scoped agent configuration, device activation/binding, assignment, and credential-recovery UI | Renderer receives only safe Works Square projections; credentials, activation-code history, and upstream error bodies remain outside durable UI state. | -| AI Hardware Main Route | Fixed `/api/works/ai-hardware` Host API to Works Square proxy | Main owns Bearer auth, stable operation IDs, bounded retry, ETag/If-Match, request/response limits, and error redaction. Versioned responses accept only canonical strong or weak numeric ETags that equal the DTO revision; mutations always emit strong `If-Match`. It never forwards Renderer authorization headers. | +| Robot Workspace | Account-scoped agent configuration, device activation/binding, assignment, and credential-recovery UI | Renderer receives only safe Works Square projections. Configuration choices come from the USER-scoped safe catalog; unavailable current values remain editable without exposing provider credentials or configuration internals. | +| AI Hardware Main Route | Fixed `/api/works/ai-hardware` Host API to Works Square proxy | Main owns Bearer auth, stable operation IDs, bounded retry, ETag/If-Match, request/response limits, error redaction, and the fixed no-store configuration-catalog proxy. Versioned responses accept only canonical strong or weak numeric ETags that equal the DTO revision; mutations always emit strong `If-Match`. It never forwards Renderer authorization headers. | ## Important Boundaries - Robot is the single enabled hardware product module; there is no separate Hardware card. Learning remains disabled. - AI hardware network access is Main-owned. Renderer cannot hold Works Square or Xiaozhi credentials and cannot select arbitrary upstream paths or headers. +- Robot model, language, and voice choices are dynamically projected from the Xiaozhi USER catalog through Works Square and Electron Main; the catalog is bounded, account-scoped, and `private, no-store` at each public hop. - One local account maps to one server-side Xiaozhi account binding. Agents and devices are resources beneath that account binding, not separate Xiaozhi users. - Robot/Canvas/module-selection routes must not initialize AI Programming projects or providers. diff --git a/.project-docs/30-worklog/current-state.md b/.project-docs/30-worklog/current-state.md index b5c134e..59799e9 100644 --- a/.project-docs/30-worklog/current-state.md +++ b/.project-docs/30-worklog/current-state.md @@ -5,6 +5,7 @@ This file is the integrated default-branch snapshot. Feature tasks record progre ## Integrated Through - `ea75b06`: Robot configuration reads accept canonical weak numeric response ETags introduced by public response compression only when the numeric revision exactly matches the strictly projected DTO; configuration and assignment writes continue to emit strong `If-Match`. +- `fe55dee`: Robot configuration editing uses the safe Xiaozhi/Works catalog for model, language, and voice selections, with bounded sliders for TTS numeric controls and no-store catalog responses. - `fd9b5b46a913c515e94e4e26f185d43866c2581f` / `7a811590c4943b7b1b7ea5f3b4d3ce3ce05622a5`: Codex-style persistent AI Programming context-compaction timeline, run-lifecycle separation, polling-idle completion, and cold-hydration hardening. - `22378efcee07e7fb80b651e65e3202f1a1dfea1d`: AI Canvas bidirectional Agent WebSocket commands, idempotent transport fallback, and Quote-based generation-task recovery. - `aba5cae286807093cf4ef643fe9f498050985c31`: Robot / AI hardware module, Main-owned Works Square proxy, and cross-repository wire contract. @@ -33,6 +34,7 @@ Updater 仍由 Electron Main 选择目标 feed、记录原始诊断并保持失 ## Recently Completed - 2026-08-15: Corrected the deployed Robot configuration-read contract after the compressed public Works response was observed with `ETag: W/\"0\"` and matching numeric `config_revision: 0`. Electron Main now accepts only canonical strong or weak numeric response tags, still requires exact DTO revision equality, and always sends strong `If-Match` for mutations. No production client rollout is claimed. +- 2026-08-16: Integrated selection-oriented Robot configuration editing. Enabled model and caller-safe voice metadata now flows from Xiaozhi through Works Square and Electron Main without exposing provider secrets; unavailable current values and `clear_fields` semantics remain intact. Production deployment of the matching service endpoints is still required. - 2026-08-15:AI 编程上下文压缩改为 Codex 风格的会话内时间线事件;手动与自动压缩原位展示并持久保留,历史回放去重且状态只允许从 running 单调进入 completed,压缩完成不再冒充整个 run idle。 - 2026-08-14: AI Canvas Agent 命令与流式事件改为共用双向 Conversation WebSocket,并保留仅面向传输故障的幂等 REST 回退;结构化 Gateway 错误不重试且未知文本脱敏;确认生成按 Quote 恢复已落库任务,切换 Conversation 后仍同步 Workspace 任务且不覆盖新会话状态或错误。 - 2026-08-13: Integrated the enabled `Makelore Robot` top-level module at `/ai-hardware`. Renderer uses typed safe DTOs while Electron Main owns Works Square authentication, idempotency identities, ETag/If-Match forwarding, response projection, limits, timeouts, and credential recovery. Robot and Canvas routes no longer initialize AI Programming projects/providers. No production deployment or real activation-code smoke is claimed. diff --git a/.project-docs/30-worklog/tasks/20260813-sync-push-main-9c2f71.md b/.project-docs/30-worklog/tasks/20260813-sync-push-main-9c2f71.md index 2b4c0d2..a293a5b 100644 --- a/.project-docs/30-worklog/tasks/20260813-sync-push-main-9c2f71.md +++ b/.project-docs/30-worklog/tasks/20260813-sync-push-main-9c2f71.md @@ -12,6 +12,7 @@ ## Scope +- On 2026-08-16, resume the existing Integration owner to merge reviewed Robot configuration-catalog/editor source `fe55dee` into local `main`, promote the accepted dynamic catalog boundary, and keep remote push outside this resumption. - On 2026-08-15, resume the existing Integration owner to merge reviewed Robot configuration-schema fix `ea75b06` into local `main`, accepting canonical weak numeric response ETags produced by the deployed compression layer while keeping strong `If-Match` writes. - On 2026-08-15, resume the existing Integration owner to merge reviewed Robot configuration-loading fix `1bcd519` into local `main`, preserving the page error/retry state and Main response-body deadline. - On 2026-08-15, resume the existing Integration owner to merge reviewed source commit `fd9b5b46a913c515e94e4e26f185d43866c2581f` for the Codex-style AI Programming context-compaction timeline into local `main` and promote its accepted lifecycle facts. @@ -124,6 +125,13 @@ Gate result: ## Plan +### 2026-08-16 Robot Configuration Catalog Integration Plan + +1. Merge reviewed source commit `fe55dee` into local `main` with a normal no-ff merge while preserving source history. +2. Exclude the source-owned task record from the final `main` tree and promote the accepted safe catalog boundary into canonical architecture/current-state memory. +3. Re-run the Robot catalog/page/Main regression suite, typecheck, lint, production build, project-document gates, and whitespace checks. +4. Obtain an independent read-only Sol PASS/FAIL review; do not push remotely as part of this resumption. + 1. Fetch `origin/main` and verify local `main`, the remote tip, and reviewed AI Canvas source commit topology. 2. Merge `22378ef` normally into local `main` without rebase/reset, and retain the source commit as a dedicated second parent. 3. Promote the bidirectional Gateway and Quote-reconciliation facts into canonical project memory. @@ -154,6 +162,9 @@ Gate result: ## Outcome +- On 2026-08-16, started a normal `--no-ff --no-commit` merge of reviewed Robot catalog/editor source `fe55dee`; Git reported no textual conflicts. The source task record remains reachable on the source commit and feature branch and is excluded from the local `main` result. +- Promoted the USER-scoped dynamic model/voice catalog boundary: Renderer receives only bounded display metadata through Electron Main and Works Square, all public catalog outcomes are no-store, and current unavailable values remain editable. +- Replaced avoidable configuration text/number inputs with catalog selects and bounded TTS sliders while preserving `clear_fields`, revision conflict, operation identity, and Main-owned credential boundaries. - On 2026-08-15, merged reviewed Robot configuration-schema source `ea75b06` into local `main` without textual conflict and excluded its source-owned task record from the final tree. - Created no-ff merge commit `9738e1c5aebad6237f436c8c94abaf877eb3d175` with local baseline `a26a53a7f4b1326be3084955029a65ef1f79929a` as first parent and reviewed source `ea75b0618a793928878edad736f0837ac9e4adc1` as second parent. - Confirmed the failure was transport-contract drift, not a Xiaozhi/Works DTO failure: response compression weakened the origin strong revision ETag to canonical `W/\"0\"`; Main now accepts only canonical strong or weak numeric response tags whose revision equals the strictly projected body. @@ -234,6 +245,10 @@ Gate result: ## Verification +- 2026-08-16 source and staged-merge Robot catalog regression selection — 5 files / 84 tests passed. +- 2026-08-16 Works Square catalog adapter/API suite — 131 tests passed; one existing Starlette/httpx deprecation warning. +- 2026-08-16 Makelore typecheck, focused ESLint, and production `build:vite` — passed; only existing chunk-size and mixed-import warnings remain. +- 2026-08-16 independent source review — `PASS`, with no P0-P3 findings after all-outcome no-store and 40px target fixes. - Robot configuration-schema source TDD recorded `2 failed, 23 passed` before the fix with `AI_HARDWARE_INVALID_ETAG` for the deployed weak-tag shape. - Merged-tree AI hardware route suite passed `25/25`; the seven-file Robot/Main regression selection passed `78/78`. - Merged-tree `pnpm run typecheck`, scoped ESLint, and `pnpm run build:vite` passed; build retains only existing chunk-size and mixed-import warnings. @@ -296,6 +311,7 @@ Gate result: ## Follow-ups +- Deploy matching Xiaozhi and Works Square catalog endpoints before releasing this client; otherwise the editor preserves current values but cannot populate dynamic choices. - The Robot integration is complete on local `main`. Production still requires matching Works Square/Xiaozhi deployment, feature configuration, credentials, and a real one-time activation-code smoke. - Stable mutation operation IDs are retained across ambiguous retries in the running app but are not persisted across an application restart. - Signed packaged updater smoke and live Works Square/Bailian image-to-image diff --git a/electron/api/routes/ai-hardware.ts b/electron/api/routes/ai-hardware.ts index c701de5..05aab02 100644 --- a/electron/api/routes/ai-hardware.ts +++ b/electron/api/routes/ai-hardware.ts @@ -13,6 +13,7 @@ const LOCAL_ROOT = '/api/works/ai-hardware'; const UPSTREAM_ROOT = '/api/ai-hardware'; const REVISION_ETAG = /^(?:W\/)?"(0|[1-9]\d*)"$/; const LOCAL_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,35}$/; +const CATALOG_ID = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,254}$/; const OPERATION_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; const MAX_RETRY_AFTER_SECONDS = 2; @@ -119,6 +120,52 @@ function projectOverview(value: unknown): Record | null { return { status: value.status, agents, devices }; } +const CATALOG_MODEL_TYPES = new Set([ + 'VAD', 'ASR', 'LLM', 'VLLM', 'Intent', 'Memory', 'TTS', +]); + +function projectCatalog(value: unknown): Record | null { + if (!isRecord(value) + || value.schema_version !== 1 + || !Array.isArray(value.models) + || value.models.length > 256 + || !Array.isArray(value.voices) + || value.voices.length > 512) return null; + const models: Record[] = []; + for (const item of value.models) { + if (!isRecord(item) + || typeof item.model_type !== 'string' + || !CATALOG_MODEL_TYPES.has(item.model_type) + || typeof item.model_id !== 'string' || item.model_id.length < 1 || item.model_id.length > 255 + || typeof item.model_name !== 'string' || item.model_name.length < 1 || item.model_name.length > 128 + || (item.supports_function_call !== null && typeof item.supports_function_call !== 'boolean')) return null; + models.push({ + model_type: item.model_type, + model_id: item.model_id, + model_name: item.model_name, + supports_function_call: item.supports_function_call, + }); + } + const voices: Record[] = []; + for (const item of value.voices) { + if (!isRecord(item) + || typeof item.tts_model_id !== 'string' || item.tts_model_id.length < 1 || item.tts_model_id.length > 255 + || typeof item.voice_id !== 'string' || item.voice_id.length < 1 || item.voice_id.length > 255 + || typeof item.voice_name !== 'string' || item.voice_name.length < 1 || item.voice_name.length > 128 + || !Array.isArray(item.languages) || item.languages.length > 16 + || item.languages.some((language) => typeof language !== 'string' || language.length < 1 || language.length > 50) + || typeof item.is_clone !== 'boolean') return null; + voices.push({ + tts_model_id: item.tts_model_id, + voice_id: item.voice_id, + voice_name: item.voice_name, + languages: item.languages, + is_clone: item.is_clone, + }); + } + return { schema_version: 1, models, voices }; +} + async function readBoundedJson(req: IncomingMessage): Promise> { const declared = Number(req.headers['content-length']); if (Number.isFinite(declared) && declared > MAX_REQUEST_BYTES) { @@ -323,6 +370,7 @@ const SAFE_UPSTREAM_ERRORS: Record = { ai_hardware_credential_recovery_required: 'AI hardware credential recovery is required', ai_hardware_credential_unavailable: 'AI hardware credential is unavailable', ai_hardware_credential_recovery_unavailable: 'AI hardware credential recovery is not currently available', + ai_hardware_catalog_unavailable: 'AI hardware configuration options are unavailable', ai_hardware_unconfigured: 'AI hardware integration is not configured', ai_hardware_disabled: 'AI hardware module is not enabled', ai_hardware_idempotency_key_invalid: 'AI hardware operation identity is invalid', @@ -384,6 +432,10 @@ export function createAiHardwareRouteHandler(dependencies: AiHardwareRouteDepend _ctx: HostApiContext, ): Promise { if (url.pathname !== LOCAL_ROOT && !url.pathname.startsWith(`${LOCAL_ROOT}/`)) return false; + if (url.pathname === `${LOCAL_ROOT}/catalog`) { + res.setHeader('Cache-Control', 'private, no-store'); + res.setHeader('Pragma', 'no-cache'); + } let operationId: string | undefined; try { @@ -399,6 +451,21 @@ export function createAiHardwareRouteHandler(dependencies: AiHardwareRouteDepend upstreamPath = UPSTREAM_ROOT; project = projectOverview; expectedStatus = 200; + } else if (method === 'GET' && url.pathname === `${LOCAL_ROOT}/catalog`) { + const keys = [...url.searchParams.keys()]; + const values = url.searchParams.getAll('tts_model_id'); + if (keys.some((key) => key !== 'tts_model_id') || values.length > 1) { + throw new SafeRouteError(400, 'AI_HARDWARE_INVALID_REQUEST', 'Invalid AI hardware request'); + } + const ttsModelId = values[0]; + if (ttsModelId !== undefined && !CATALOG_ID.test(ttsModelId)) { + throw new SafeRouteError(400, 'AI_HARDWARE_INVALID_REQUEST', 'Invalid AI hardware request'); + } + upstreamPath = `${UPSTREAM_ROOT}/catalog${ + ttsModelId === undefined ? '' : `?tts_model_id=${encodeURIComponent(ttsModelId)}` + }`; + project = projectCatalog; + expectedStatus = 200; } else if (method === 'POST' && url.pathname === `${LOCAL_ROOT}/credential-recovery`) { const input = await readBoundedJson(req); ensureExactKeys(input, new Set(['client_operation_id'])); diff --git a/src/lib/ai-hardware.ts b/src/lib/ai-hardware.ts index 1b8962f..f58c7d8 100644 --- a/src/lib/ai-hardware.ts +++ b/src/lib/ai-hardware.ts @@ -18,6 +18,7 @@ const SAFE_ERROR_MESSAGES: Record = { ai_hardware_credential_recovery_required: 'AI hardware credential recovery is required', ai_hardware_credential_unavailable: 'AI hardware credential is unavailable', ai_hardware_credential_recovery_unavailable: 'AI hardware credential recovery is not currently available', + ai_hardware_catalog_unavailable: 'AI hardware configuration options are unavailable', xiaozhi_hardware_unavailable: 'AI hardware service is unavailable', xiaozhi_hardware_timeout: 'AI hardware service timed out', ai_hardware_disabled: 'AI hardware module is not enabled', @@ -80,6 +81,36 @@ export type AiHardwareAgentConfiguration = AiHardwareAgent & { chat_history_conf: number | null; }; +export type AiHardwareCatalogModelType = + | 'VAD' + | 'ASR' + | 'LLM' + | 'VLLM' + | 'Intent' + | 'Memory' + | 'TTS'; + +export type AiHardwareCatalogModel = { + model_type: AiHardwareCatalogModelType; + model_id: string; + model_name: string; + supports_function_call: boolean | null; +}; + +export type AiHardwareCatalogVoice = { + tts_model_id: string; + voice_id: string; + voice_name: string; + languages: string[]; + is_clone: boolean; +}; + +export type AiHardwareConfigurationCatalog = { + schema_version: 1; + models: AiHardwareCatalogModel[]; + voices: AiHardwareCatalogVoice[]; +}; + export const AI_HARDWARE_CLEARABLE_FIELDS = [ 'system_prompt', 'lang_code', @@ -249,6 +280,62 @@ function readOverview(value: unknown): AiHardwareOverview { }; } +const CATALOG_MODEL_TYPES = new Set([ + 'VAD', 'ASR', 'LLM', 'VLLM', 'Intent', 'Memory', 'TTS', +]); + +function boundedString(value: unknown, maxLength: number): value is string { + return typeof value === 'string' && value.length > 0 && value.length <= maxLength; +} + +function readCatalogModel(value: unknown): AiHardwareCatalogModel { + if (!isRecord(value) + || !hasOnlyKeys(value, ['model_type', 'model_id', 'model_name', 'supports_function_call']) + || Object.keys(value).length !== 4 + || typeof value.model_type !== 'string' + || !CATALOG_MODEL_TYPES.has(value.model_type as AiHardwareCatalogModelType) + || !boundedString(value.model_id, 255) + || !boundedString(value.model_name, 128) + || (value.supports_function_call !== null && typeof value.supports_function_call !== 'boolean')) { + invalidPayload(); + } + return value as AiHardwareCatalogModel; +} + +function readCatalogVoice(value: unknown): AiHardwareCatalogVoice { + if (!isRecord(value) + || !hasOnlyKeys(value, ['tts_model_id', 'voice_id', 'voice_name', 'languages', 'is_clone']) + || Object.keys(value).length !== 5 + || !boundedString(value.tts_model_id, 255) + || !boundedString(value.voice_id, 255) + || !boundedString(value.voice_name, 128) + || !Array.isArray(value.languages) + || value.languages.length > 16 + || value.languages.some((language) => !boundedString(language, 50)) + || typeof value.is_clone !== 'boolean') { + invalidPayload(); + } + return value as AiHardwareCatalogVoice; +} + +function readConfigurationCatalog(value: unknown): AiHardwareConfigurationCatalog { + if (!isRecord(value) + || !hasOnlyKeys(value, ['schema_version', 'models', 'voices']) + || Object.keys(value).length !== 3 + || value.schema_version !== 1 + || !Array.isArray(value.models) + || value.models.length > 256 + || !Array.isArray(value.voices) + || value.voices.length > 512) { + invalidPayload(); + } + return { + schema_version: 1, + models: value.models.map(readCatalogModel), + voices: value.voices.map(readCatalogVoice), + }; +} + function readEnvelope(value: unknown): MainEnvelope { if (!isRecord(value) || !hasOnlyKeys(value, [ 'success', 'status', 'code', 'error', 'retryable', 'retry_after_seconds', 'operation_id', 'data', 'revision', @@ -416,6 +503,16 @@ export async function getAiHardwareOverview(): Promise { return request('', undefined, readOverview) as Promise; } +export async function getAiHardwareConfigurationCatalog( + ttsModelId?: string, +): Promise { + if (ttsModelId !== undefined) assertString(ttsModelId, 'tts_model_id', 255); + const query = ttsModelId === undefined + ? '' + : `?tts_model_id=${encodeURIComponent(ttsModelId)}`; + return request(`/catalog${query}`, undefined, readConfigurationCatalog) as Promise; +} + export async function recoverAiHardwareCredential( options?: AiHardwareMutationOptions, ): Promise { diff --git a/src/pages/AiHardware/index.tsx b/src/pages/AiHardware/index.tsx index 7ea1d90..623f12f 100644 --- a/src/pages/AiHardware/index.tsx +++ b/src/pages/AiHardware/index.tsx @@ -8,12 +8,14 @@ import { } from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; +import { Select } from '@/components/ui/select'; import { Textarea } from '@/components/ui/textarea'; import { AiHardwareApiError, bindAiHardwareDevice, createAiHardwareAgent, getAiHardwareAgentConfiguration, + getAiHardwareConfigurationCatalog, getAiHardwareAssignment, getAiHardwareOverview, recoverAiHardwareCredential, @@ -22,7 +24,9 @@ import { type AiHardwareAgent, type AiHardwareAgentConfiguration, type AiHardwareAgentConfigurationUpdate, + type AiHardwareCatalogModelType, type AiHardwareClearableField, + type AiHardwareConfigurationCatalog, type AiHardwareDevice, type AiHardwareOverview, } from '@/lib/ai-hardware'; @@ -158,6 +162,9 @@ export function AiHardware() { const [createOpen, setCreateOpen] = useState(false); const [bindOpen, setBindOpen] = useState(false); const [configOpen, setConfigOpen] = useState(false); + const [catalog, setCatalog] = useState(null); + const [catalogLoading, setCatalogLoading] = useState(false); + const [catalogError, setCatalogError] = useState(null); const [assignmentDevice, setAssignmentDevice] = useState(null); const [agentName, setAgentName] = useState(''); const [dialogAgentId, setDialogAgentId] = useState(''); @@ -176,6 +183,7 @@ export function AiHardware() { const activationCodeInputRef = useRef(null); const bindFingerprintKeyRef = useRef(null); const bindRetryIntentRef = useRef(null); + const catalogRequestRef = useRef(0); const clearCreateOperation = () => { setCreateOperationId(null); setCreateOperationBody(null); }; const clearBindOperation = () => { @@ -186,6 +194,24 @@ export function AiHardware() { const clearConfigOperation = () => { setConfigOperationId(null); setConfigOperationBody(null); }; const clearAssignmentOperation = () => { setAssignmentOperationId(null); setAssignmentOperationBody(null); }; + const loadConfigurationCatalog = useCallback(async (ttsModelId?: string) => { + const requestId = catalogRequestRef.current + 1; + catalogRequestRef.current = requestId; + setCatalogLoading(true); + setCatalogError(null); + setCatalog((current) => current ? { ...current, voices: [] } : null); + try { + const next = await getAiHardwareConfigurationCatalog(ttsModelId); + if (catalogRequestRef.current !== requestId) return; + setCatalog(next); + } catch { + if (catalogRequestRef.current !== requestId) return; + setCatalogError('暂时无法读取可选配置,请重试。当前配置会保持不变。'); + } finally { + if (catalogRequestRef.current === requestId) setCatalogLoading(false); + } + }, []); + const recoverCredential = async () => { if (recoveryBusy) return; setRecoveryBusy(true); setRecoveryError(null); @@ -254,6 +280,17 @@ export function AiHardware() { clearCreateOperation(); clearBindOperation(); clearConfigOperation(); clearAssignmentOperation(); }; + const openConfigurationEditor = () => { + if (!config) return; + resetDialog(); + const next = draftFrom(config); + setDraft(next); + setCatalog(null); + setCatalogError(null); + setConfigOpen(true); + void loadConfigurationCatalog(next.tts_model_id || undefined); + }; + const createAgent = async () => { const name = agentName.trim(); if (!name || name.length > 64) { setDialogError('名称需要包含 1–64 个字符。'); return; } @@ -378,7 +415,7 @@ export function AiHardware() {
智能体{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}
+
{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}
)}
: }
@@ -386,19 +423,101 @@ export function AiHardware() { { 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 (!busy) { setConfigOpen(open); if (!open) { resetDialog(); setCatalog(null); setCatalogError(null); } } }}>编辑智能体配置模型、语言和音色来自小智当前可用目录;选择“使用服务默认值”可清除可选字段。{draft ? { setDraft(next); clearConfigOperation(); }} onTtsModelChange={(value, next) => { setDraft(next); clearConfigOperation(); void loadConfigurationCatalog(value || undefined); }} onRetryCatalog={() => void loadConfigurationCatalog(draft.tts_model_id || undefined)} /> : 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
; + 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', '名称')}