484 lines
16 KiB
TypeScript
484 lines
16 KiB
TypeScript
// TypeScript API client for AI Voicebot server
|
|
import { components } from "./api-types";
|
|
import { base } from "./Common";
|
|
|
|
// Re-export commonly used types from the generated schema
|
|
export type LobbyModel = components["schemas"]["LobbyModel"];
|
|
export type LobbyListItem = components["schemas"]["LobbyListItem"];
|
|
export type LobbyCreateData = components["schemas"]["LobbyCreateData"];
|
|
export type NamePasswordRecord = components["schemas"]["NamePasswordRecord"];
|
|
|
|
// Type aliases for API methods
|
|
export type AdminNamesResponse = components["schemas"]["AdminNamesResponse"];
|
|
export type AdminActionResponse = components["schemas"]["AdminActionResponse"];
|
|
export type AdminSetPassword = components["schemas"]["AdminSetPassword"];
|
|
export type AdminClearPassword = components["schemas"]["AdminClearPassword"];
|
|
export type HealthResponse = components["schemas"]["HealthResponse"];
|
|
export type LobbiesResponse = components["schemas"]["LobbiesResponse"];
|
|
export type SessionResponse = components["schemas"]["SessionResponse"];
|
|
export type LobbyCreateRequest = components["schemas"]["LobbyCreateRequest"];
|
|
export type LobbyCreateResponse = components["schemas"]["LobbyCreateResponse"];
|
|
|
|
// Bot Provider Types (manually defined until API types are regenerated)
|
|
export interface BotInfoModel {
|
|
name: string;
|
|
description: string;
|
|
}
|
|
|
|
export interface BotProviderModel {
|
|
provider_id: string;
|
|
base_url: string;
|
|
name: string;
|
|
description: string;
|
|
registered_at: number;
|
|
last_seen: number;
|
|
}
|
|
|
|
export interface BotProviderListResponse {
|
|
providers: BotProviderModel[];
|
|
}
|
|
|
|
export interface BotListResponse {
|
|
bots: BotInfoModel[];
|
|
providers: Record<string, string>;
|
|
}
|
|
|
|
export interface BotJoinLobbyRequest {
|
|
bot_name: string;
|
|
lobby_id: string;
|
|
nick?: string;
|
|
provider_id?: string;
|
|
}
|
|
|
|
export interface BotJoinLobbyResponse {
|
|
status: string;
|
|
bot_name: string;
|
|
run_id: string;
|
|
provider_id: string;
|
|
}
|
|
|
|
export interface BotLeaveLobbyRequest {
|
|
session_id: string;
|
|
}
|
|
|
|
export interface BotLeaveLobbyResponse {
|
|
status: string;
|
|
session_id: string;
|
|
run_id?: string;
|
|
}
|
|
|
|
export class ApiError extends Error {
|
|
constructor(public status: number, public statusText: string, public data?: any) {
|
|
super(`HTTP ${status}: ${statusText}`);
|
|
this.name = "ApiError";
|
|
}
|
|
}
|
|
|
|
export class ApiClient {
|
|
private baseURL: string;
|
|
private defaultHeaders: Record<string, string>;
|
|
|
|
constructor(baseURL?: string) {
|
|
// Use the current window location instead of localhost, just like WebSocket connections
|
|
const defaultBaseURL =
|
|
typeof window !== "undefined" ? `${window.location.protocol}//${window.location.host}` : "http://localhost:8001";
|
|
|
|
this.baseURL = baseURL || process.env.REACT_APP_API_URL || defaultBaseURL;
|
|
this.defaultHeaders = {};
|
|
}
|
|
|
|
/**
|
|
* Construct API path using PUBLIC_URL environment variable
|
|
* Replaces hardcoded /ai-voicebot prefix with dynamic base from environment
|
|
*/
|
|
private getApiPath(schemaPath: string): string {
|
|
// Replace the hardcoded /ai-voicebot prefix with the dynamic base
|
|
return schemaPath.replace("/ai-voicebot", base);
|
|
}
|
|
|
|
private async request<T>(
|
|
path: string,
|
|
options: {
|
|
method: string;
|
|
body?: any;
|
|
params?: Record<string, string>;
|
|
}
|
|
): Promise<T> {
|
|
const url = new URL(path, this.baseURL);
|
|
|
|
if (options.params) {
|
|
Object.entries(options.params).forEach(([key, value]) => {
|
|
url.searchParams.append(key, value);
|
|
});
|
|
}
|
|
|
|
const requestInit: RequestInit = {
|
|
method: options.method,
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
...this.defaultHeaders,
|
|
},
|
|
};
|
|
|
|
if (options.body && options.method !== "GET") {
|
|
requestInit.body = JSON.stringify(options.body);
|
|
}
|
|
|
|
const response = await fetch(url.toString(), requestInit);
|
|
|
|
if (!response.ok) {
|
|
let errorData;
|
|
// Clone the response before trying to read it, in case JSON parsing fails
|
|
const responseClone = response.clone();
|
|
try {
|
|
errorData = await response.json();
|
|
} catch {
|
|
try {
|
|
errorData = await responseClone.text();
|
|
} catch {
|
|
errorData = `HTTP ${response.status}: ${response.statusText}`;
|
|
}
|
|
}
|
|
throw new ApiError(response.status, response.statusText, errorData);
|
|
}
|
|
|
|
const contentType = response.headers.get("content-type");
|
|
if (contentType && contentType.includes("application/json")) {
|
|
return response.json();
|
|
}
|
|
|
|
return response.text() as unknown as T;
|
|
}
|
|
|
|
// Admin API methods
|
|
async adminListNames(): Promise<AdminNamesResponse> {
|
|
return this.request<AdminNamesResponse>(this.getApiPath("/ai-voicebot/api/admin/names"), { method: "GET" });
|
|
}
|
|
|
|
async adminSetPassword(data: AdminSetPassword): Promise<AdminActionResponse> {
|
|
return this.request<AdminActionResponse>(this.getApiPath("/ai-voicebot/api/admin/set_password"), {
|
|
method: "POST",
|
|
body: data,
|
|
});
|
|
}
|
|
|
|
async adminClearPassword(data: AdminClearPassword): Promise<AdminActionResponse> {
|
|
return this.request<AdminActionResponse>(this.getApiPath("/ai-voicebot/api/admin/clear_password"), {
|
|
method: "POST",
|
|
body: data,
|
|
});
|
|
}
|
|
|
|
// Health check
|
|
async healthCheck(): Promise<HealthResponse> {
|
|
return this.request<HealthResponse>(this.getApiPath("/ai-voicebot/api/health"), { method: "GET" });
|
|
}
|
|
|
|
// Session methods
|
|
async getSession(): Promise<SessionResponse> {
|
|
return this.request<SessionResponse>(this.getApiPath("/ai-voicebot/api/session"), { method: "GET" });
|
|
}
|
|
|
|
// Lobby methods
|
|
async getLobbies(): Promise<LobbiesResponse> {
|
|
return this.request<LobbiesResponse>(this.getApiPath("/ai-voicebot/api/lobby"), { method: "GET" });
|
|
}
|
|
|
|
async createLobby(sessionId: string, data: LobbyCreateRequest): Promise<LobbyCreateResponse> {
|
|
return this.request<LobbyCreateResponse>(this.getApiPath(`/ai-voicebot/api/lobby/${sessionId}`), {
|
|
method: "POST",
|
|
body: data,
|
|
});
|
|
}
|
|
|
|
// Bot Provider methods
|
|
async getBotProviders(): Promise<BotProviderListResponse> {
|
|
return this.request<BotProviderListResponse>(this.getApiPath("/ai-voicebot/api/bots/providers"), { method: "GET" });
|
|
}
|
|
|
|
async getAvailableBots(): Promise<BotListResponse> {
|
|
return this.request<BotListResponse>(this.getApiPath("/ai-voicebot/api/bots"), { method: "GET" });
|
|
}
|
|
|
|
async requestBotJoinLobby(botName: string, request: BotJoinLobbyRequest): Promise<BotJoinLobbyResponse> {
|
|
return this.request<BotJoinLobbyResponse>(
|
|
this.getApiPath(`/ai-voicebot/api/bots/${encodeURIComponent(botName)}/join`),
|
|
{
|
|
method: "POST",
|
|
body: request,
|
|
}
|
|
);
|
|
}
|
|
|
|
async requestBotLeaveLobby(request: BotLeaveLobbyRequest): Promise<BotLeaveLobbyResponse> {
|
|
return this.request<BotLeaveLobbyResponse>(this.getApiPath("/ai-voicebot/api/bots/leave"), {
|
|
method: "POST",
|
|
body: request,
|
|
});
|
|
}
|
|
|
|
// Auto-generated endpoints will be added here by update-api-client.js
|
|
// DO NOT MANUALLY EDIT BELOW THIS LINE
|
|
|
|
// Auto-generated endpoints
|
|
async lobbyCreate(session_id: string, data: any): Promise<any> {
|
|
return this.request<any>(this.getApiPath(`/ai-voicebot/api/lobby/${session_id}`), { method: "POST", body: data });
|
|
}
|
|
}
|
|
|
|
// API Evolution Detection System
|
|
interface ApiEndpoint {
|
|
path: string;
|
|
method: string;
|
|
implemented: boolean;
|
|
}
|
|
|
|
class ApiEvolutionChecker {
|
|
private static instance: ApiEvolutionChecker;
|
|
private checkedOnce = false;
|
|
|
|
static getInstance(): ApiEvolutionChecker {
|
|
if (!this.instance) {
|
|
this.instance = new ApiEvolutionChecker();
|
|
}
|
|
return this.instance;
|
|
}
|
|
|
|
private getImplementedEndpoints(): Set<string> {
|
|
// Define all endpoints that are currently implemented in ApiClient
|
|
// This list is automatically updated by update-api-client.js
|
|
return new Set([
|
|
'GET:/ai-voicebot/api/admin/names',
|
|
'GET:/ai-voicebot/api/bots',
|
|
'GET:/ai-voicebot/api/bots/providers',
|
|
'GET:/ai-voicebot/api/health',
|
|
'GET:/ai-voicebot/api/lobby',
|
|
'GET:/ai-voicebot/api/session',
|
|
'POST:/ai-voicebot/api/admin/clear_password',
|
|
'POST:/ai-voicebot/api/admin/set_password',
|
|
'POST:/ai-voicebot/api/lobby/{sessionId}',
|
|
'POST:/ai-voicebot/api/lobby/{session_id}'
|
|
]);
|
|
}
|
|
|
|
private getAvailableEndpoints(): ApiEndpoint[] {
|
|
// Extract all endpoints from the generated paths type
|
|
const endpoints: ApiEndpoint[] = [];
|
|
const implementedSet = this.getImplementedEndpoints();
|
|
|
|
// Type-safe extraction of paths from the generated schema
|
|
const pathKeys = [
|
|
'/ai-voicebot/api/admin/names',
|
|
'/ai-voicebot/api/admin/set_password',
|
|
'/ai-voicebot/api/admin/clear_password',
|
|
'/ai-voicebot/api/health',
|
|
'/ai-voicebot/api/session',
|
|
'/ai-voicebot/api/lobby',
|
|
'/ai-voicebot/api/lobby/{session_id}',
|
|
'/ai-voicebot/{path}' // Generic catch-all, we'll skip this one
|
|
] as const;
|
|
|
|
pathKeys.forEach(path => {
|
|
if (path === '/ai-voicebot/{path}') {
|
|
// Skip the generic proxy endpoint
|
|
return;
|
|
}
|
|
|
|
// Check each HTTP method that might be available for this path
|
|
const possibleMethods = ['get', 'post', 'put', 'delete', 'patch'] as const;
|
|
|
|
possibleMethods.forEach(method => {
|
|
const endpointKey = `${method.toUpperCase()}:${path}`;
|
|
const implemented = implementedSet.has(endpointKey);
|
|
|
|
// We can't directly check if the method exists at runtime due to TypeScript compilation,
|
|
// but we know from our grep search what exists. Let's be more explicit:
|
|
const knownEndpoints: Record<string, string[]> = {
|
|
'/ai-voicebot/api/admin/names': ['GET'],
|
|
'/ai-voicebot/api/admin/set_password': ['POST'],
|
|
'/ai-voicebot/api/admin/clear_password': ['POST'],
|
|
'/ai-voicebot/api/health': ['GET'],
|
|
'/ai-voicebot/api/session': ['GET'],
|
|
'/ai-voicebot/api/lobby': ['GET'],
|
|
'/ai-voicebot/api/lobby/{session_id}': ['POST']
|
|
};
|
|
|
|
const pathMethods = knownEndpoints[path];
|
|
if (pathMethods && pathMethods.includes(method.toUpperCase())) {
|
|
endpoints.push({
|
|
path,
|
|
method: method.toUpperCase(),
|
|
implemented
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
return endpoints;
|
|
}
|
|
|
|
checkForNewEndpoints(): void {
|
|
if (this.checkedOnce) {
|
|
return; // Only check once per session to avoid spam
|
|
}
|
|
|
|
this.checkedOnce = true;
|
|
|
|
try {
|
|
const endpoints = this.getAvailableEndpoints();
|
|
const unimplemented = endpoints.filter(ep => !ep.implemented);
|
|
|
|
if (unimplemented.length > 0) {
|
|
console.group('🚨 API Evolution Warning');
|
|
console.warn(
|
|
`Found ${unimplemented.length} API endpoints that are not implemented in ApiClient:`
|
|
);
|
|
|
|
unimplemented.forEach(endpoint => {
|
|
console.warn(` • ${endpoint.method} ${endpoint.path}`);
|
|
});
|
|
|
|
console.warn('Consider updating api-client.ts to implement these endpoints.');
|
|
console.warn('Available types can be found in api-types.ts');
|
|
console.groupEnd();
|
|
} else {
|
|
console.info('✅ All available API endpoints are implemented in ApiClient');
|
|
}
|
|
|
|
// Also check for potential parameter changes by looking at method signatures
|
|
this.checkForParameterChanges();
|
|
|
|
} catch (error) {
|
|
console.warn('Failed to check for API evolution:', error);
|
|
}
|
|
}
|
|
|
|
private checkForParameterChanges(): void {
|
|
// This is a simpler check - we could extend this to compare parameter schemas
|
|
// For now, we'll just remind developers to check if their implementation matches the schema
|
|
console.info('💡 Tip: Regularly compare your method implementations with the generated types in api-types.ts to ensure parameter compatibility');
|
|
}
|
|
|
|
// Method to manually trigger a fresh check (useful for development)
|
|
recheckEndpoints(): void {
|
|
this.checkedOnce = false;
|
|
this.checkForNewEndpoints();
|
|
}
|
|
}
|
|
|
|
// Export the checker for manual use if needed
|
|
export const apiEvolutionChecker = ApiEvolutionChecker.getInstance();
|
|
|
|
// Utility functions for development
|
|
export const devUtils = {
|
|
/**
|
|
* Manually check for API evolution and log results
|
|
*/
|
|
async checkApiEvolution() {
|
|
if (process.env.NODE_ENV !== 'development') {
|
|
console.warn('API evolution checking is only available in development mode');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const { advancedApiChecker } = await import('./api-evolution-checker');
|
|
const evolution = await advancedApiChecker.checkSchemaEvolution();
|
|
|
|
console.group('🔍 Manual API Evolution Check');
|
|
console.log('Unimplemented endpoints:', evolution.unimplementedEndpoints.length);
|
|
console.log('New endpoints:', evolution.newEndpoints.length);
|
|
console.log('Schema changed:', evolution.hasChangedEndpoints);
|
|
|
|
if (evolution.unimplementedEndpoints.length > 0) {
|
|
console.log('\nImplementation stubs:');
|
|
console.log(advancedApiChecker.generateImplementationStubs(evolution.unimplementedEndpoints));
|
|
}
|
|
|
|
console.groupEnd();
|
|
return evolution;
|
|
} catch (error) {
|
|
console.error('Failed to check API evolution:', error);
|
|
return null;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Force a recheck of endpoints (bypasses the "once per session" limitation)
|
|
*/
|
|
recheckEndpoints() {
|
|
apiEvolutionChecker.recheckEndpoints();
|
|
}
|
|
};
|
|
|
|
// Default client instance
|
|
export const apiClient = new ApiClient();
|
|
|
|
// Convenience API namespaces
|
|
export const adminApi = {
|
|
listNames: () => apiClient.adminListNames(),
|
|
setPassword: (data: AdminSetPassword) => apiClient.adminSetPassword(data),
|
|
clearPassword: (data: AdminClearPassword) => apiClient.adminClearPassword(data),
|
|
};
|
|
|
|
export const healthApi = { check: () => apiClient.healthCheck() };
|
|
export const lobbiesApi = { getAll: () => apiClient.getLobbies() };
|
|
export const sessionsApi = {
|
|
getCurrent: () => apiClient.getSession(),
|
|
createLobby: (sessionId: string, data: LobbyCreateRequest) => apiClient.createLobby(sessionId, data),
|
|
};
|
|
|
|
export const botsApi = {
|
|
getProviders: () => apiClient.getBotProviders(),
|
|
getAvailable: () => apiClient.getAvailableBots(),
|
|
requestJoinLobby: (botName: string, request: BotJoinLobbyRequest) => apiClient.requestBotJoinLobby(botName, request),
|
|
};
|
|
|
|
// Automatically check for API evolution when this module is loaded
|
|
// This will warn developers if new endpoints are available but not implemented
|
|
if (process.env.NODE_ENV === 'development') {
|
|
// Import the advanced checker dynamically to avoid circular dependencies
|
|
import('./api-evolution-checker').then(({ advancedApiChecker }) => {
|
|
// Run the check after a short delay to ensure all modules are loaded
|
|
setTimeout(async () => {
|
|
try {
|
|
const evolution = await advancedApiChecker.checkSchemaEvolution();
|
|
|
|
if (evolution.unimplementedEndpoints.length > 0 || evolution.hasNewEndpoints) {
|
|
console.group('🚨 API Evolution Detection');
|
|
|
|
if (evolution.hasNewEndpoints && evolution.newEndpoints.length > 0) {
|
|
console.warn('🆕 New API endpoints detected:');
|
|
evolution.newEndpoints.forEach(ep => {
|
|
console.warn(` • ${ep.method} ${ep.path} (${ep.operationId})`);
|
|
});
|
|
}
|
|
|
|
if (evolution.unimplementedEndpoints.length > 0) {
|
|
console.warn('⚠️ Unimplemented API endpoints:');
|
|
evolution.unimplementedEndpoints.forEach(ep => {
|
|
console.warn(` • ${ep.method} ${ep.path}`);
|
|
});
|
|
}
|
|
|
|
if (evolution.hasChangedEndpoints) {
|
|
console.warn('🔄 API schema has changed - check for parameter updates');
|
|
}
|
|
|
|
console.groupCollapsed('💡 Implementation suggestions:');
|
|
console.log('Add these methods to ApiClient:');
|
|
console.log(advancedApiChecker.generateImplementationStubs(evolution.unimplementedEndpoints));
|
|
console.groupEnd();
|
|
|
|
console.groupEnd();
|
|
} else {
|
|
console.info('✅ All API endpoints are implemented');
|
|
}
|
|
} catch (error) {
|
|
console.warn('API evolution check failed:', error);
|
|
// Fallback to simple checker
|
|
apiEvolutionChecker.checkForNewEndpoints();
|
|
}
|
|
}, 1000);
|
|
});
|
|
}
|