Files
agentic-mobile-control/console/src/api.ts
T
q792602257andClaude Opus 4.6 c0a653fa3f feat(host-agent): serve built console via SPA-aware static mount
Add an optional single-process mode where the backend serves the built
console bundle itself, so operators don't need a separate `npm run dev`
for edge/dev setups. When RUNTIME_CONSOLE_STATIC_DIR points at the
console dist directory, the app mounts a SpaStaticFiles handler at /ui/
(with 404 fallback to index.html for client-side routing) and redirects
/ to /ui/. The console build uses an empty VITE_API_BASE_URL for relative
API paths (same-origin, no CORS), and Vite's base is set to /ui/ so
assets resolve under the mount. /console/* JSON API is unchanged and is
shared by both serve modes.

api.ts now treats an explicitly-empty VITE_API_BASE_URL as "use relative
paths" instead of falling back to the dev default, which previously
forced absolute URLs even in same-origin builds.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-14 13:29:58 +08:00

97 lines
2.6 KiB
TypeScript

import type {
Device,
RegisterDevicePayload,
RuntimeConfig,
TaskRecord,
TimelineRecord,
} from "./types";
const configuredBaseUrl = import.meta.env.VITE_API_BASE_URL as string | undefined;
export const API_BASE_URL = (
configuredBaseUrl !== undefined ? configuredBaseUrl : "http://127.0.0.1:8000"
).replace(/\/$/, "");
async function request<T>(path: string, init: RequestInit = {}): Promise<T> {
const response = await fetch(`${API_BASE_URL}${path}`, {
...init,
headers: {
Accept: "application/json",
...(init.body ? { "Content-Type": "application/json" } : {}),
...init.headers,
},
});
if (!response.ok) {
let message = `${response.status} ${response.statusText}`;
try {
const payload = (await response.json()) as { detail?: unknown };
if (typeof payload.detail === "string") {
message = payload.detail;
} else if (payload.detail) {
message = JSON.stringify(payload.detail);
}
} catch {
message = await response.text();
}
throw new Error(message);
}
if (response.status === 204) {
return undefined as T;
}
return (await response.json()) as T;
}
export function listDevices(): Promise<Device[]> {
return request<Device[]>("/console/devices");
}
export function registerDevice(payload: RegisterDevicePayload): Promise<Device> {
return request<Device>("/console/devices", {
method: "POST",
body: JSON.stringify(payload),
});
}
export function unregisterDevice(deviceId: string): Promise<void> {
return request<void>(`/console/devices/${encodeURIComponent(deviceId)}`, {
method: "DELETE",
});
}
export function listTasks(filters: {
deviceId?: string;
status?: string;
}): Promise<TaskRecord[]> {
const params = new URLSearchParams();
if (filters.deviceId) {
params.set("device_id", filters.deviceId);
}
if (filters.status) {
params.set("status", filters.status);
}
const query = params.toString();
return request<TaskRecord[]>(`/console/tasks${query ? `?${query}` : ""}`);
}
export function getTask(taskId: string): Promise<TaskRecord> {
return request<TaskRecord>(`/console/tasks/${encodeURIComponent(taskId)}`);
}
export function getTimeline(taskId: string): Promise<TimelineRecord[]> {
return request<TimelineRecord[]>(
`/console/tasks/${encodeURIComponent(taskId)}/timeline`,
);
}
export function getConfig(): Promise<RuntimeConfig> {
return request<RuntimeConfig>("/console/config");
}
export function updateConfig(payload: RuntimeConfig): Promise<RuntimeConfig> {
return request<RuntimeConfig>("/console/config", {
method: "PUT",
body: JSON.stringify(payload),
});
}