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

@@ -6,6 +6,7 @@ import { sendJson } from '../route-utils';
import { WORKS_SQUARE_CONFIG } from '../works-config';
import { getValidWorksSquareAccessToken } from '../../services/works-square-session';
import { proxyAwareFetch } from '../../utils/proxy-fetch';
import type { RobotHotspotModule } from '../../robot-hotspot';
const MAX_REQUEST_BYTES = 64 * 1024;
const MAX_RESPONSE_BYTES = 256 * 1024;
@@ -16,6 +17,7 @@ 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 HOTSPOT_CANDIDATE_ID = /^[A-Za-z0-9_-]{1,128}$/;
const MAX_RETRY_AFTER_SECONDS = 2;
const PROVISIONING_PORTAL_URL = 'http://192.168.4.1/';
const GUIDED_HOTSPOT_BINDING_ENABLED = process.env.NIANCODE_AI_HARDWARE_GUIDED_HOTSPOT_BINDING !== '0';
@@ -30,6 +32,7 @@ export type AiHardwareRouteDependencies = {
timeoutMs?: number;
guidedHotspotBinding?: boolean;
openExternal?: (url: string) => Promise<void>;
robotHotspot?: RobotHotspotModule;
};
class SafeRouteError extends Error {
@@ -291,6 +294,52 @@ function requireRevision(body: Record<string, unknown>): number {
return revision;
}
function requireHotspotCandidateId(value: unknown): string {
if (typeof value !== 'string' || !HOTSPOT_CANDIDATE_ID.test(value)) {
throw new SafeRouteError(400, 'AI_HARDWARE_INVALID_REQUEST', 'Invalid AI hardware request');
}
return value;
}
const HOTSPOT_ERRORS: Record<string, { status: number; code: string; error: string }> = {
unsupported: {
status: 501,
code: 'AI_HARDWARE_HOTSPOT_UNSUPPORTED',
error: 'Robot hotspot connection is not supported on this platform',
},
permission_denied: {
status: 403,
code: 'AI_HARDWARE_HOTSPOT_PERMISSION_DENIED',
error: 'Permission to access nearby Robot hotspots was denied',
},
busy: {
status: 409,
code: 'AI_HARDWARE_HOTSPOT_BUSY',
error: 'Another Robot hotspot operation is in progress',
},
candidate_expired: {
status: 409,
code: 'AI_HARDWARE_HOTSPOT_CANDIDATE_EXPIRED',
error: 'The Robot hotspot selection expired; scan again',
},
scan_failed: {
status: 502,
code: 'AI_HARDWARE_HOTSPOT_SCAN_FAILED',
error: 'Robot hotspots could not be scanned',
},
connect_failed: {
status: 502,
code: 'AI_HARDWARE_HOTSPOT_CONNECT_FAILED',
error: 'The Robot hotspot could not be connected',
},
};
function asHotspotRouteError(error: unknown, fallback: 'scan_failed' | 'connect_failed'): SafeRouteError {
const nativeCode = isRecord(error) && typeof error.code === 'string' ? error.code : fallback;
const safe = HOTSPOT_ERRORS[nativeCode] ?? HOTSPOT_ERRORS[fallback];
return new SafeRouteError(safe.status, safe.code, safe.error);
}
function takeOperationId(body: Record<string, unknown>, createUuid: () => string): string {
const candidate = body.client_operation_id ?? createUuid();
if (typeof candidate !== 'string' || !OPERATION_ID.test(candidate)) {
@@ -442,6 +491,9 @@ export function createAiHardwareRouteHandler(dependencies: AiHardwareRouteDepend
const timeoutMs = dependencies.timeoutMs ?? DEFAULT_TIMEOUT_MS;
const guidedHotspotBinding = dependencies.guidedHotspotBinding ?? GUIDED_HOTSPOT_BINDING_ENABLED;
const openExternal = dependencies.openExternal ?? ((url: string) => shell.openExternal(url));
const getRobotHotspot = dependencies.robotHotspot
? async () => dependencies.robotHotspot as RobotHotspotModule
: async () => (await import('../../robot-hotspot')).robotHotspotModule;
return async function handleAiHardwareRoutes(
req: IncomingMessage,
@@ -493,6 +545,67 @@ export function createAiHardwareRouteHandler(dependencies: AiHardwareRouteDepend
sendJson(res, 200, { success: true, data: { opened: true } });
return true;
}
if (method === 'POST' && url.pathname === `${LOCAL_ROOT}/provisioning-hotspots/scan`) {
if (url.search) {
throw new SafeRouteError(400, 'AI_HARDWARE_INVALID_REQUEST', 'Invalid AI hardware request');
}
const input = await readBoundedJson(req);
ensureExactKeys(input, new Set());
if (!guidedHotspotBinding) {
throw new SafeRouteError(
403,
'AI_HARDWARE_PROVISIONING_DISABLED',
'AI hardware guided provisioning is not enabled',
);
}
try {
const result = await (await getRobotHotspot()).scan();
sendJson(res, 200, {
success: true,
data: {
platform: result.platform,
hotspots: result.hotspots.map((hotspot) => ({
candidate_id: hotspot.candidateId,
ssid: hotspot.ssid,
signal_percent: hotspot.signalPercent,
connected: hotspot.connected,
})),
},
});
} catch (error) {
throw asHotspotRouteError(error, 'scan_failed');
}
return true;
}
if (method === 'POST' && url.pathname === `${LOCAL_ROOT}/provisioning-hotspots/connect`) {
if (url.search) {
throw new SafeRouteError(400, 'AI_HARDWARE_INVALID_REQUEST', 'Invalid AI hardware request');
}
const input = await readBoundedJson(req);
ensureExactKeys(input, new Set(['candidate_id']));
const candidateId = requireHotspotCandidateId(input.candidate_id);
if (!guidedHotspotBinding) {
throw new SafeRouteError(
403,
'AI_HARDWARE_PROVISIONING_DISABLED',
'AI hardware guided provisioning is not enabled',
);
}
try {
const result = await (await getRobotHotspot()).connect(candidateId);
sendJson(res, 200, {
success: true,
data: {
connected: result.connected,
candidate_id: result.candidateId,
ssid: result.ssid,
},
});
} catch (error) {
throw asHotspotRouteError(error, 'connect_failed');
}
return true;
}
let upstreamPath: string;
let body: Record<string, unknown> | undefined;