feat: add dynamic Robot configuration choices

This commit is contained in:
2026-08-16 00:55:06 +08:00
parent 7bef261fb8
commit fe55deed04
7 changed files with 524 additions and 10 deletions

View File

@@ -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<string, unknown> | 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<string, unknown> | 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<string, unknown>[] = [];
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<string, unknown>[] = [];
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<Record<string, unknown>> {
const declared = Number(req.headers['content-length']);
if (Number.isFinite(declared) && declared > MAX_REQUEST_BYTES) {
@@ -323,6 +370,7 @@ const SAFE_UPSTREAM_ERRORS: Record<string, string> = {
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<boolean> {
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']));