feat(robot): connect provisioning hotspots in app

This commit is contained in:
2026-08-16 20:18:28 +08:00
parent abecd5f344
commit c1326a2980
17 changed files with 1950 additions and 11 deletions

View File

@@ -35,8 +35,15 @@ const SAFE_ERROR_MESSAGES: Record<string, string> = {
AI_HARDWARE_INVALID_RESPONSE: 'AI hardware service returned an invalid response',
AI_HARDWARE_PROVISIONING_DISABLED: 'AI hardware guided provisioning is not enabled',
AI_HARDWARE_PORTAL_OPEN_FAILED: 'AI hardware provisioning portal could not be opened',
AI_HARDWARE_HOTSPOT_UNSUPPORTED: 'Robot hotspot connection is not supported on this platform',
AI_HARDWARE_HOTSPOT_PERMISSION_DENIED: 'Permission to access nearby Robot hotspots was denied',
AI_HARDWARE_HOTSPOT_BUSY: 'Another Robot hotspot operation is in progress',
AI_HARDWARE_HOTSPOT_CANDIDATE_EXPIRED: 'The Robot hotspot selection expired; scan again',
AI_HARDWARE_HOTSPOT_SCAN_FAILED: 'Robot hotspots could not be scanned',
AI_HARDWARE_HOTSPOT_CONNECT_FAILED: 'The Robot hotspot could not be connected',
};
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 HOTSPOT_CANDIDATE_ID = /^[A-Za-z0-9_-]{1,128}$/;
export type AiHardwareStatus =
| 'unprovisioned'
@@ -67,6 +74,24 @@ export type AiHardwareProvisioningCapabilities = {
guidedHotspotBinding: boolean;
};
export type AiHardwareProvisioningHotspot = {
candidateId: string;
ssid: string;
signalPercent: number;
connected: boolean;
};
export type AiHardwareProvisioningHotspotScan = {
platform: 'windows' | 'macos';
hotspots: AiHardwareProvisioningHotspot[];
};
export type AiHardwareProvisioningHotspotConnection = {
connected: true;
candidateId: string;
ssid: string;
};
export type AiHardwareAgentConfiguration = AiHardwareAgent & {
system_prompt: string | null;
lang_code: string | null;
@@ -360,6 +385,41 @@ function readPortalOpened(value: unknown): { opened: true } {
return { opened: true };
}
function readProvisioningHotspot(value: unknown): AiHardwareProvisioningHotspot {
if (!isRecord(value)
|| !hasOnlyKeys(value, ['candidate_id', 'ssid', 'signal_percent', 'connected'])
|| typeof value.candidate_id !== 'string' || !HOTSPOT_CANDIDATE_ID.test(value.candidate_id)
|| typeof value.ssid !== 'string' || value.ssid.length < 1 || value.ssid.length > 32
|| !isInteger(value.signal_percent, 0, 100)
|| typeof value.connected !== 'boolean') invalidPayload();
return {
candidateId: value.candidate_id,
ssid: value.ssid,
signalPercent: value.signal_percent,
connected: value.connected,
};
}
function readProvisioningHotspotScan(value: unknown): AiHardwareProvisioningHotspotScan {
if (!isRecord(value)
|| !hasOnlyKeys(value, ['platform', 'hotspots'])
|| (value.platform !== 'windows' && value.platform !== 'macos')
|| !Array.isArray(value.hotspots) || value.hotspots.length > 64) invalidPayload();
return {
platform: value.platform,
hotspots: value.hotspots.map(readProvisioningHotspot),
};
}
function readProvisioningHotspotConnection(value: unknown): AiHardwareProvisioningHotspotConnection {
if (!isRecord(value)
|| !hasOnlyKeys(value, ['connected', 'candidate_id', 'ssid'])
|| value.connected !== true
|| typeof value.candidate_id !== 'string' || !HOTSPOT_CANDIDATE_ID.test(value.candidate_id)
|| typeof value.ssid !== 'string' || value.ssid.length < 1 || value.ssid.length > 32) invalidPayload();
return { connected: true, candidateId: value.candidate_id, ssid: value.ssid };
}
function throwEnvelopeError(envelope: MainEnvelope): never {
const status = isInteger(envelope.status, 400, 599) ? envelope.status : 502;
const code = typeof envelope.code === 'string' && /^[A-Za-z0-9._-]{1,128}$/.test(envelope.code)
@@ -536,6 +596,25 @@ export async function openAiHardwareProvisioningPortal(): Promise<{ opened: true
) as Promise<{ opened: true }>;
}
export async function scanAiHardwareProvisioningHotspots(): Promise<AiHardwareProvisioningHotspotScan> {
return request(
'/provisioning-hotspots/scan',
jsonInit('POST', {}),
readProvisioningHotspotScan,
) as Promise<AiHardwareProvisioningHotspotScan>;
}
export async function connectAiHardwareProvisioningHotspot(
candidateId: string,
): Promise<AiHardwareProvisioningHotspotConnection> {
if (!HOTSPOT_CANDIDATE_ID.test(candidateId)) throw new TypeError('candidateId is invalid');
return request(
'/provisioning-hotspots/connect',
jsonInit('POST', { candidate_id: candidateId }),
readProvisioningHotspotConnection,
) as Promise<AiHardwareProvisioningHotspotConnection>;
}
export async function getAiHardwareConfigurationCatalog(
ttsModelId?: string,
): Promise<AiHardwareConfigurationCatalog> {