Type checking working
This commit is contained in:
parent
d679c8cecf
commit
2fdd58f7c3
File diff suppressed because it is too large
Load Diff
@ -138,7 +138,7 @@ const BotManager: React.FC<BotManagerProps> = ({ lobbyId, onBotAdded, sx }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const isBotConfigurable = (bot: BotInfoModel): boolean => {
|
const isBotConfigurable = (bot: BotInfoModel): boolean => {
|
||||||
return Boolean(bot.configurable) || Boolean(bot.features && bot.features.includes("per_lobby_config"));
|
return Boolean(bot.configurable);
|
||||||
};
|
};
|
||||||
|
|
||||||
const botCount = bots.length;
|
const botCount = bots.length;
|
||||||
|
@ -13,7 +13,7 @@ import { MediaControl, MediaAgent, Peer } from "./MediaControl";
|
|||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import { Session } from "./GlobalContext";
|
import { Session } from "./GlobalContext";
|
||||||
import useWebSocket from "react-use-websocket";
|
import useWebSocket from "react-use-websocket";
|
||||||
import { ApiClient, BotLeaveLobbyRequest } from "./api-client";
|
import { ApiClient } from "./api-client";
|
||||||
import BotConfig from "./BotConfig";
|
import BotConfig from "./BotConfig";
|
||||||
|
|
||||||
type User = {
|
type User = {
|
||||||
@ -53,7 +53,7 @@ const UserList: React.FC<UserListProps> = (props: UserListProps) => {
|
|||||||
setLeavingBots((prev) => new Set(prev).add(user.session_id));
|
setLeavingBots((prev) => new Set(prev).add(user.session_id));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await apiClient.requestBotLeaveLobby(user.bot_instance_id);
|
await apiClient.createRequestBotLeaveLobby(user.bot_instance_id);
|
||||||
console.log(`Bot ${user.name} leave requested successfully`);
|
console.log(`Bot ${user.name} leave requested successfully`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to request bot leave:", error);
|
console.error("Failed to request bot leave:", error);
|
||||||
|
@ -2,96 +2,48 @@
|
|||||||
import { components } from "./api-types";
|
import { components } from "./api-types";
|
||||||
import { base } from "./Common";
|
import { base } from "./Common";
|
||||||
|
|
||||||
// Re-export commonly used types from the generated schema
|
// DO NOT MANUALLY EDIT BELOW THIS LINE - All content below is auto-generated
|
||||||
export type LobbyModel = components["schemas"]["LobbyModel"];
|
// To modify auto-generated content, edit the template in client/update-api-client.js
|
||||||
export type LobbyListItem = components["schemas"]["LobbyListItem"];
|
|
||||||
export type LobbyCreateData = components["schemas"]["LobbyCreateData"];
|
|
||||||
export type NamePasswordRecord = components["schemas"]["NamePasswordRecord"];
|
|
||||||
|
|
||||||
// Type aliases for API methods
|
// Re-export all types from the generated schema
|
||||||
export type AdminNamesResponse = components["schemas"]["AdminNamesResponse"];
|
export type { components } from "./api-types";
|
||||||
|
|
||||||
|
// Re-export all types from the generated schema for convenience
|
||||||
export type AdminActionResponse = components["schemas"]["AdminActionResponse"];
|
export type AdminActionResponse = components["schemas"]["AdminActionResponse"];
|
||||||
export type AdminSetPassword = components["schemas"]["AdminSetPassword"];
|
|
||||||
export type AdminClearPassword = components["schemas"]["AdminClearPassword"];
|
export type AdminClearPassword = components["schemas"]["AdminClearPassword"];
|
||||||
|
export type AdminMetricsConfig = components["schemas"]["AdminMetricsConfig"];
|
||||||
|
export type AdminMetricsResponse = components["schemas"]["AdminMetricsResponse"];
|
||||||
|
export type AdminNamesResponse = components["schemas"]["AdminNamesResponse"];
|
||||||
|
export type AdminSetPassword = components["schemas"]["AdminSetPassword"];
|
||||||
|
export type AdminValidationResponse = components["schemas"]["AdminValidationResponse"];
|
||||||
|
export type BotConfigListResponse = components["schemas"]["BotConfigListResponse"];
|
||||||
|
export type BotConfigParameter = components["schemas"]["BotConfigParameter"];
|
||||||
|
export type BotConfigSchema = components["schemas"]["BotConfigSchema"];
|
||||||
|
export type BotConfigUpdateRequest = components["schemas"]["BotConfigUpdateRequest"];
|
||||||
|
export type BotConfigUpdateResponse = components["schemas"]["BotConfigUpdateResponse"];
|
||||||
|
export type BotInfoModel = components["schemas"]["BotInfoModel"];
|
||||||
|
export type BotJoinLobbyRequest = components["schemas"]["BotJoinLobbyRequest"];
|
||||||
|
export type BotJoinLobbyResponse = components["schemas"]["BotJoinLobbyResponse"];
|
||||||
|
export type BotLeaveLobbyResponse = components["schemas"]["BotLeaveLobbyResponse"];
|
||||||
|
export type BotListResponse = components["schemas"]["BotListResponse"];
|
||||||
|
export type BotLobbyConfig = components["schemas"]["BotLobbyConfig"];
|
||||||
|
export type BotProviderListResponse = components["schemas"]["BotProviderListResponse"];
|
||||||
|
export type BotProviderModel = components["schemas"]["BotProviderModel"];
|
||||||
|
export type BotProviderRegisterRequest = components["schemas"]["BotProviderRegisterRequest"];
|
||||||
|
export type BotProviderRegisterResponse = components["schemas"]["BotProviderRegisterResponse"];
|
||||||
|
export type ChatMessageModel = components["schemas"]["ChatMessageModel"];
|
||||||
|
export type ChatMessagesResponse = components["schemas"]["ChatMessagesResponse"];
|
||||||
|
export type HTTPValidationError = components["schemas"]["HTTPValidationError"];
|
||||||
export type HealthResponse = components["schemas"]["HealthResponse"];
|
export type HealthResponse = components["schemas"]["HealthResponse"];
|
||||||
export type LobbiesResponse = components["schemas"]["LobbiesResponse"];
|
export type LobbiesResponse = components["schemas"]["LobbiesResponse"];
|
||||||
export type SessionResponse = components["schemas"]["SessionResponse"];
|
export type LobbyCreateData = components["schemas"]["LobbyCreateData"];
|
||||||
export type LobbyCreateRequest = components["schemas"]["LobbyCreateRequest"];
|
export type LobbyCreateRequest = components["schemas"]["LobbyCreateRequest"];
|
||||||
export type LobbyCreateResponse = components["schemas"]["LobbyCreateResponse"];
|
export type LobbyCreateResponse = components["schemas"]["LobbyCreateResponse"];
|
||||||
|
export type LobbyListItem = components["schemas"]["LobbyListItem"];
|
||||||
// Bot Provider Types (manually defined until API types are regenerated)
|
export type LobbyModel = components["schemas"]["LobbyModel"];
|
||||||
export interface BotInfoModel {
|
export type NamePasswordRecord = components["schemas"]["NamePasswordRecord"];
|
||||||
name: string;
|
export type SessionResponse = components["schemas"]["SessionResponse"];
|
||||||
description: string;
|
export type ValidationError = components["schemas"]["ValidationError"];
|
||||||
configurable?: boolean;
|
|
||||||
features?: 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 {
|
|
||||||
lobby_id: string;
|
|
||||||
nick?: string;
|
|
||||||
provider_id?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BotJoinLobbyResponse {
|
|
||||||
status: string;
|
|
||||||
bot_instance_id: string;
|
|
||||||
bot_name: string;
|
|
||||||
run_id: string;
|
|
||||||
provider_id: string;
|
|
||||||
session_id: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BotInstanceModel {
|
|
||||||
bot_instance_id: string;
|
|
||||||
bot_name: string;
|
|
||||||
nick: string;
|
|
||||||
lobby_id: string;
|
|
||||||
session_id: string;
|
|
||||||
provider_id: string;
|
|
||||||
run_id: string;
|
|
||||||
has_media: boolean;
|
|
||||||
created_at: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BotLeaveLobbyRequest {
|
|
||||||
bot_instance_id: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BotLeaveLobbyResponse {
|
|
||||||
status: string;
|
|
||||||
bot_instance_id: string;
|
|
||||||
session_id: string;
|
|
||||||
run_id?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BotLeaveLobbyRequest {
|
|
||||||
session_id: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BotLeaveLobbyResponse {
|
|
||||||
status: string;
|
|
||||||
session_id: string;
|
|
||||||
run_id?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ApiError extends Error {
|
export class ApiError extends Error {
|
||||||
constructor(public status: number, public statusText: string, public data?: any) {
|
constructor(public status: number, public statusText: string, public data?: any) {
|
||||||
@ -176,342 +128,219 @@ export class ApiClient {
|
|||||||
return response.text() as unknown as T;
|
return response.text() as unknown as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Admin API methods
|
// Auto-generated endpoint methods
|
||||||
async adminListNames(): Promise<AdminNamesResponse> {
|
async getSystemHealth(): Promise<any> {
|
||||||
return this.request<AdminNamesResponse>(this.getApiPath("/ai-voicebot/api/admin/names"), { method: "GET" });
|
return this.request<any>(this.getApiPath(`/ai-voicebot/api/system/health`), { method: "GET" });
|
||||||
}
|
}
|
||||||
|
|
||||||
async adminSetPassword(data: AdminSetPassword): Promise<AdminActionResponse> {
|
async getListNames(): Promise<any> {
|
||||||
return this.request<AdminActionResponse>(this.getApiPath("/ai-voicebot/api/admin/set_password"), {
|
return this.request<any>(this.getApiPath(`/ai-voicebot/api/admin/names`), { method: "GET" });
|
||||||
method: "POST",
|
|
||||||
body: data,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async adminClearPassword(data: AdminClearPassword): Promise<AdminActionResponse> {
|
async createSetPassword(data: any): Promise<any> {
|
||||||
return this.request<AdminActionResponse>(this.getApiPath("/ai-voicebot/api/admin/clear_password"), {
|
return this.request<any>(this.getApiPath(`/ai-voicebot/api/admin/set_password`), { method: "POST", body: data });
|
||||||
method: "POST",
|
|
||||||
body: data,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Health check
|
async createClearPassword(data: any): Promise<any> {
|
||||||
async healthCheck(): Promise<HealthResponse> {
|
return this.request<any>(this.getApiPath(`/ai-voicebot/api/admin/clear_password`), { method: "POST", body: data });
|
||||||
return this.request<HealthResponse>(this.getApiPath("/ai-voicebot/api/health"), { method: "GET" });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Session methods
|
async createCleanupSessions(): Promise<any> {
|
||||||
async getSession(): Promise<SessionResponse> {
|
return this.request<any>(this.getApiPath(`/ai-voicebot/api/admin/cleanup_sessions`), { method: "POST" });
|
||||||
return this.request<SessionResponse>(this.getApiPath("/ai-voicebot/api/session"), { method: "GET" });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lobby methods
|
async getSessionMetrics(): Promise<any> {
|
||||||
async getLobbies(): Promise<LobbiesResponse> {
|
return this.request<any>(this.getApiPath(`/ai-voicebot/api/admin/session_metrics`), { method: "GET" });
|
||||||
return this.request<LobbiesResponse>(this.getApiPath("/ai-voicebot/api/lobby"), { method: "GET" });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async createLobby(sessionId: string, data: LobbyCreateRequest): Promise<LobbyCreateResponse> {
|
async getValidateSessions(): Promise<any> {
|
||||||
return this.request<LobbyCreateResponse>(this.getApiPath(`/ai-voicebot/api/lobby/${sessionId}`), {
|
return this.request<any>(this.getApiPath(`/ai-voicebot/api/admin/validate_sessions`), { method: "GET" });
|
||||||
method: "POST",
|
|
||||||
body: data,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bot Provider methods
|
async createCleanupLobbies(): Promise<any> {
|
||||||
async getBotProviders(): Promise<BotProviderListResponse> {
|
return this.request<any>(this.getApiPath(`/ai-voicebot/api/admin/cleanup_lobbies`), { method: "POST" });
|
||||||
return this.request<BotProviderListResponse>(this.getApiPath("/ai-voicebot/api/bots/providers"), { method: "GET" });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAvailableBots(): Promise<BotListResponse> {
|
async getHealthSummary(): Promise<any> {
|
||||||
return this.request<BotListResponse>(this.getApiPath("/ai-voicebot/api/bots"), { method: "GET" });
|
return this.request<any>(this.getApiPath(`/ai-voicebot/api/health`), { method: "GET" });
|
||||||
}
|
}
|
||||||
|
|
||||||
async requestBotJoinLobby(botName: string, request: BotJoinLobbyRequest): Promise<BotJoinLobbyResponse> {
|
async getSession(): Promise<any> {
|
||||||
return this.request<BotJoinLobbyResponse>(
|
return this.request<any>(this.getApiPath(`/ai-voicebot/api/session`), { method: "GET" });
|
||||||
this.getApiPath(`/ai-voicebot/api/bots/${encodeURIComponent(botName)}/join`),
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
body: request,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async requestBotLeaveLobby(botInstanceId: string): Promise<BotLeaveLobbyResponse> {
|
async getListLobbies(): Promise<any> {
|
||||||
return this.request<BotLeaveLobbyResponse>(
|
return this.request<any>(this.getApiPath(`/ai-voicebot/api/lobby`), { method: "GET" });
|
||||||
this.getApiPath(`/ai-voicebot/api/bots/instances/${encodeURIComponent(botInstanceId)}/leave`),
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-generated endpoints will be added here by update-api-client.js
|
async createLobby(session_id: string, data: any): Promise<any> {
|
||||||
// DO NOT MANUALLY EDIT BELOW THIS LINE
|
|
||||||
|
|
||||||
// Auto-generated endpoints
|
|
||||||
async systemHealth(): Promise<any> {
|
|
||||||
return this.request<any>(this.getApiPath("/ai-voicebot/api/system/health"), { method: "GET" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 });
|
return this.request<any>(this.getApiPath(`/ai-voicebot/api/lobby/${session_id}`), { method: "POST", body: data });
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// API Evolution Detection System
|
async getChatMessages(lobby_id: string, params?: Record<string, string>): Promise<any> {
|
||||||
interface ApiEndpoint {
|
return this.request<any>(this.getApiPath(`/ai-voicebot/api/lobby/${lobby_id}/chat`), { method: "GET", params });
|
||||||
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> {
|
async createRegisterBotProvider(data: any): Promise<any> {
|
||||||
// Define all endpoints that are currently implemented in ApiClient
|
return this.request<any>(this.getApiPath(`/ai-voicebot/api/bots/providers/register`), { method: "POST", body: data });
|
||||||
// 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',
|
|
||||||
'GET:/ai-voicebot/api/system/health',
|
|
||||||
'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[] {
|
async getListBotProviders(): Promise<any> {
|
||||||
// Extract all endpoints from the generated paths type
|
return this.request<any>(this.getApiPath(`/ai-voicebot/api/bots/providers`), { method: "GET" });
|
||||||
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 {
|
async getListAvailableBots(): Promise<any> {
|
||||||
if (this.checkedOnce) {
|
return this.request<any>(this.getApiPath(`/ai-voicebot/api/bots`), { method: "GET" });
|
||||||
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 {
|
async createRequestBotJoinLobby(bot_name: string, data: any): Promise<any> {
|
||||||
// This is a simpler check - we could extend this to compare parameter schemas
|
return this.request<any>(this.getApiPath(`/ai-voicebot/api/bots/${bot_name}/join`), { method: "POST", body: data });
|
||||||
// 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)
|
async createRequestBotLeaveLobby(bot_instance_id: string): Promise<any> {
|
||||||
recheckEndpoints(): void {
|
return this.request<any>(this.getApiPath(`/ai-voicebot/api/bots/instances/${bot_instance_id}/leave`), { method: "POST" });
|
||||||
this.checkedOnce = false;
|
}
|
||||||
this.checkForNewEndpoints();
|
|
||||||
|
async getBotInstance(bot_instance_id: string): Promise<any> {
|
||||||
|
return this.request<any>(this.getApiPath(`/ai-voicebot/api/bots/instances/${bot_instance_id}`), { method: "GET" });
|
||||||
|
}
|
||||||
|
|
||||||
|
async getBotConfigSchema(bot_name: string): Promise<any> {
|
||||||
|
return this.request<any>(this.getApiPath(`/ai-voicebot/api/bots/config/schema/${bot_name}`), { method: "GET" });
|
||||||
|
}
|
||||||
|
|
||||||
|
async getLobbyBotConfigs(lobby_id: string): Promise<any> {
|
||||||
|
return this.request<any>(this.getApiPath(`/ai-voicebot/api/bots/config/lobby/${lobby_id}`), { method: "GET" });
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteLobbyConfigs(lobby_id: string): Promise<any> {
|
||||||
|
return this.request<any>(this.getApiPath(`/ai-voicebot/api/bots/config/lobby/${lobby_id}`), { method: "DELETE" });
|
||||||
|
}
|
||||||
|
|
||||||
|
async getLobbyBotConfig(lobby_id: string, bot_name: string): Promise<any> {
|
||||||
|
return this.request<any>(this.getApiPath(`/ai-voicebot/api/bots/config/lobby/${lobby_id}/bot/${bot_name}`), { method: "GET" });
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteBotConfig(lobby_id: string, bot_name: string): Promise<any> {
|
||||||
|
return this.request<any>(this.getApiPath(`/ai-voicebot/api/bots/config/lobby/${lobby_id}/bot/${bot_name}`), { method: "DELETE" });
|
||||||
|
}
|
||||||
|
|
||||||
|
async createUpdateBotConfig(data: any, params?: Record<string, string>): Promise<any> {
|
||||||
|
return this.request<any>(this.getApiPath(`/ai-voicebot/api/bots/config/update`), { method: "POST", body: data, params });
|
||||||
|
}
|
||||||
|
|
||||||
|
async getConfigStatistics(): Promise<any> {
|
||||||
|
return this.request<any>(this.getApiPath(`/ai-voicebot/api/bots/config/statistics`), { method: "GET" });
|
||||||
|
}
|
||||||
|
|
||||||
|
async createRefreshBotSchemas(): Promise<any> {
|
||||||
|
return this.request<any>(this.getApiPath(`/ai-voicebot/api/bots/config/refresh-schemas`), { method: "POST" });
|
||||||
|
}
|
||||||
|
|
||||||
|
async createRefreshBotSchema(bot_name: string): Promise<any> {
|
||||||
|
return this.request<any>(this.getApiPath(`/ai-voicebot/api/bots/config/schema/${bot_name}/refresh`), { method: "POST" });
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteClearBotSchemaCache(bot_name: string): Promise<any> {
|
||||||
|
return this.request<any>(this.getApiPath(`/ai-voicebot/api/bots/config/schema/${bot_name}/cache`), { method: "DELETE" });
|
||||||
|
}
|
||||||
|
|
||||||
|
async getReadinessProbe(): Promise<any> {
|
||||||
|
return this.request<any>(this.getApiPath(`/ai-voicebot/api/health/ready`), { method: "GET" });
|
||||||
|
}
|
||||||
|
|
||||||
|
async getLivenessProbe(): Promise<any> {
|
||||||
|
return this.request<any>(this.getApiPath(`/ai-voicebot/api/health/live`), { method: "GET" });
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCurrentMetrics(): Promise<any> {
|
||||||
|
return this.request<any>(this.getApiPath(`/ai-voicebot/api/metrics`), { method: "GET" });
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMetricsHistory(params?: Record<string, string>): Promise<any> {
|
||||||
|
return this.request<any>(this.getApiPath(`/ai-voicebot/api/metrics/history`), { method: "GET", params });
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCacheStatistics(): Promise<any> {
|
||||||
|
return this.request<any>(this.getApiPath(`/ai-voicebot/api/cache/stats`), { method: "GET" });
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSystemInfo(): Promise<any> {
|
||||||
|
return this.request<any>(this.getApiPath(`/ai-voicebot/api/system/info`), { method: "GET" });
|
||||||
|
}
|
||||||
|
|
||||||
|
async createClearCache(params?: Record<string, string>): Promise<any> {
|
||||||
|
return this.request<any>(this.getApiPath(`/ai-voicebot/api/cache/clear`), { method: "POST", params });
|
||||||
|
}
|
||||||
|
|
||||||
|
async getExportMetricsPrometheus(): Promise<any> {
|
||||||
|
return this.request<any>(this.getApiPath(`/ai-voicebot/api/metrics/export`), { method: "GET" });
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateProxyStatic(path: string): Promise<any> {
|
||||||
|
return this.request<any>(this.getApiPath(`/ai-voicebot/${path}`), { method: "PATCH" });
|
||||||
|
}
|
||||||
|
|
||||||
|
async getProxyStatic(path: string): Promise<any> {
|
||||||
|
return this.request<any>(this.getApiPath(`/ai-voicebot/${path}`), { method: "GET" });
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteProxyStatic(path: string): Promise<any> {
|
||||||
|
return this.request<any>(this.getApiPath(`/ai-voicebot/${path}`), { method: "DELETE" });
|
||||||
|
}
|
||||||
|
|
||||||
|
async optionsProxyStatic(path: string): Promise<any> {
|
||||||
|
return this.request<any>(this.getApiPath(`/ai-voicebot/${path}`), { method: "OPTIONS" });
|
||||||
|
}
|
||||||
|
|
||||||
|
async headProxyStatic(path: string): Promise<any> {
|
||||||
|
return this.request<any>(this.getApiPath(`/ai-voicebot/${path}`), { method: "HEAD" });
|
||||||
|
}
|
||||||
|
|
||||||
|
async createProxyStatic(path: string): Promise<any> {
|
||||||
|
return this.request<any>(this.getApiPath(`/ai-voicebot/${path}`), { method: "POST" });
|
||||||
|
}
|
||||||
|
|
||||||
|
async replaceProxyStatic(path: string): Promise<any> {
|
||||||
|
return this.request<any>(this.getApiPath(`/ai-voicebot/${path}`), { method: "PUT" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
// Default client instance
|
||||||
export const apiClient = new ApiClient();
|
export const apiClient = new ApiClient();
|
||||||
|
|
||||||
// Convenience API namespaces
|
// Convenience API namespaces for easy usage
|
||||||
export const adminApi = {
|
export const adminApi = {
|
||||||
listNames: () => apiClient.adminListNames(),
|
listNames: () => apiClient.getListNames(),
|
||||||
setPassword: (data: AdminSetPassword) => apiClient.adminSetPassword(data),
|
setPassword: (data: any) => apiClient.createSetPassword(data),
|
||||||
clearPassword: (data: AdminClearPassword) => apiClient.adminClearPassword(data),
|
clearPassword: (data: any) => apiClient.createClearPassword(data),
|
||||||
|
cleanupSessions: () => apiClient.createCleanupSessions(),
|
||||||
|
sessionMetrics: () => apiClient.getSessionMetrics(),
|
||||||
|
validateSessions: () => apiClient.getValidateSessions(),
|
||||||
|
cleanupLobbies: () => apiClient.createCleanupLobbies(),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const healthApi = {
|
||||||
|
check: () => apiClient.getHealthSummary(),
|
||||||
|
ready: () => apiClient.getReadinessProbe(),
|
||||||
|
live: () => apiClient.getLivenessProbe(),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const lobbiesApi = {
|
||||||
|
getAll: () => apiClient.getListLobbies(),
|
||||||
|
getChatMessages: (lobbyId: string, params?: Record<string, string>) => apiClient.getChatMessages(lobbyId, params),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const healthApi = { check: () => apiClient.healthCheck() };
|
|
||||||
export const lobbiesApi = { getAll: () => apiClient.getLobbies() };
|
|
||||||
export const sessionsApi = {
|
export const sessionsApi = {
|
||||||
getCurrent: () => apiClient.getSession(),
|
getCurrent: () => apiClient.getSession(),
|
||||||
createLobby: (sessionId: string, data: LobbyCreateRequest) => apiClient.createLobby(sessionId, data),
|
createLobby: (sessionId: string, data: any) => apiClient.createLobby(sessionId, data),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const botsApi = {
|
export const botsApi = {
|
||||||
getProviders: () => apiClient.getBotProviders(),
|
getProviders: () => apiClient.getListBotProviders(),
|
||||||
getAvailable: () => apiClient.getAvailableBots(),
|
getAvailable: () => apiClient.getListAvailableBots(),
|
||||||
requestJoinLobby: (botName: string, request: BotJoinLobbyRequest) => apiClient.requestBotJoinLobby(botName, request),
|
requestJoinLobby: (botName: string, request: any) => apiClient.createRequestBotJoinLobby(botName, request),
|
||||||
|
requestLeaveLobby: (botInstanceId: string) => apiClient.createRequestBotLeaveLobby(botInstanceId),
|
||||||
|
getInstance: (botInstanceId: string) => apiClient.getBotInstance(botInstanceId),
|
||||||
|
registerProvider: (data: any) => apiClient.createRegisterBotProvider(data),
|
||||||
};
|
};
|
||||||
|
|
||||||
// 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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
@ -1,201 +1,74 @@
|
|||||||
// Advanced API Evolution Detection utilities
|
// Auto-generated API evolution checker
|
||||||
// This module provides runtime introspection of OpenAPI schema changes
|
// This file tracks known API endpoints to detect changes
|
||||||
|
|
||||||
import { paths } from './api-types';
|
import { base } from './Common';
|
||||||
import { base } from "./Common";
|
|
||||||
|
|
||||||
export interface EndpointInfo {
|
export const knownEndpoints = new Set([
|
||||||
path: string;
|
`DELETE:${base}/api/bots/config/lobby/{lobby_id}`,
|
||||||
method: string;
|
`DELETE:${base}/api/bots/config/lobby/{lobby_id}/bot/{bot_name}`,
|
||||||
operationId?: string;
|
`DELETE:${base}/api/bots/config/schema/{bot_name}/cache`,
|
||||||
implemented: boolean;
|
`DELETE:${base}/{path}`,
|
||||||
hasParameterChanges?: boolean;
|
`GET:${base}/api/admin/names`,
|
||||||
|
`GET:${base}/api/admin/session_metrics`,
|
||||||
|
`GET:${base}/api/admin/validate_sessions`,
|
||||||
|
`GET:${base}/api/bots`,
|
||||||
|
`GET:${base}/api/bots/config/lobby/{lobby_id}`,
|
||||||
|
`GET:${base}/api/bots/config/lobby/{lobby_id}/bot/{bot_name}`,
|
||||||
|
`GET:${base}/api/bots/config/schema/{bot_name}`,
|
||||||
|
`GET:${base}/api/bots/config/statistics`,
|
||||||
|
`GET:${base}/api/bots/instances/{bot_instance_id}`,
|
||||||
|
`GET:${base}/api/bots/providers`,
|
||||||
|
`GET:${base}/api/cache/stats`,
|
||||||
|
`GET:${base}/api/health`,
|
||||||
|
`GET:${base}/api/health/live`,
|
||||||
|
`GET:${base}/api/health/ready`,
|
||||||
|
`GET:${base}/api/lobby`,
|
||||||
|
`GET:${base}/api/lobby/{lobby_id}/chat`,
|
||||||
|
`GET:${base}/api/metrics`,
|
||||||
|
`GET:${base}/api/metrics/export`,
|
||||||
|
`GET:${base}/api/metrics/history`,
|
||||||
|
`GET:${base}/api/session`,
|
||||||
|
`GET:${base}/api/system/health`,
|
||||||
|
`GET:${base}/api/system/info`,
|
||||||
|
`GET:${base}/{path}`,
|
||||||
|
`HEAD:${base}/{path}`,
|
||||||
|
`OPTIONS:${base}/{path}`,
|
||||||
|
`PATCH:${base}/{path}`,
|
||||||
|
`POST:${base}/api/admin/cleanup_lobbies`,
|
||||||
|
`POST:${base}/api/admin/cleanup_sessions`,
|
||||||
|
`POST:${base}/api/admin/clear_password`,
|
||||||
|
`POST:${base}/api/admin/set_password`,
|
||||||
|
`POST:${base}/api/bots/config/refresh-schemas`,
|
||||||
|
`POST:${base}/api/bots/config/schema/{bot_name}/refresh`,
|
||||||
|
`POST:${base}/api/bots/config/update`,
|
||||||
|
`POST:${base}/api/bots/instances/{bot_instance_id}/leave`,
|
||||||
|
`POST:${base}/api/bots/providers/register`,
|
||||||
|
`POST:${base}/api/bots/{bot_name}/join`,
|
||||||
|
`POST:${base}/api/cache/clear`,
|
||||||
|
`POST:${base}/api/lobby/{session_id}`,
|
||||||
|
`POST:${base}/{path}`,
|
||||||
|
`PUT:${base}/{path}`
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Schema path for dynamic usage
|
||||||
|
export const schemaPath = `${base}/openapi-schema.json`;
|
||||||
|
|
||||||
|
// Proxy path pattern for matching
|
||||||
|
export const proxyPathPattern = `${base}/{path}`;
|
||||||
|
|
||||||
|
export function checkApiEvolution(discoveredEndpoints: string[]): {
|
||||||
|
newEndpoints: string[];
|
||||||
|
removedEndpoints: string[];
|
||||||
|
totalEndpoints: number;
|
||||||
|
} {
|
||||||
|
const discoveredSet = new Set(discoveredEndpoints);
|
||||||
|
|
||||||
|
const newEndpoints = discoveredEndpoints.filter(ep => !knownEndpoints.has(ep));
|
||||||
|
const removedEndpoints = Array.from(knownEndpoints).filter(ep => !discoveredSet.has(ep));
|
||||||
|
|
||||||
|
return {
|
||||||
|
newEndpoints,
|
||||||
|
removedEndpoints,
|
||||||
|
totalEndpoints: discoveredEndpoints.length
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AdvancedApiEvolutionChecker {
|
|
||||||
private static instance: AdvancedApiEvolutionChecker;
|
|
||||||
private lastSchemaHash: string | null = null;
|
|
||||||
|
|
||||||
static getInstance(): AdvancedApiEvolutionChecker {
|
|
||||||
if (!this.instance) {
|
|
||||||
this.instance = new AdvancedApiEvolutionChecker();
|
|
||||||
}
|
|
||||||
return this.instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all endpoints from the OpenAPI schema by analyzing the paths type
|
|
||||||
*/
|
|
||||||
private extractEndpointsFromSchema(): EndpointInfo[] {
|
|
||||||
const endpoints: EndpointInfo[] = [];
|
|
||||||
|
|
||||||
// In a real implementation, we would need to parse the actual OpenAPI JSON
|
|
||||||
// since TypeScript types are erased at runtime. For now, we'll maintain
|
|
||||||
// a list that should be updated when new endpoints are added to the schema.
|
|
||||||
// This list is automatically updated by the update-api-client.js script
|
|
||||||
const knownSchemaEndpoints = [
|
|
||||||
{ path: '/ai-voicebot/api/system/health', method: 'GET', operationId: 'system_health_ai_voicebot_api_system_health_get' }
|
|
||||||
];
|
|
||||||
|
|
||||||
// Get implemented endpoints from ApiClient
|
|
||||||
const implementedEndpoints = this.getImplementedEndpoints();
|
|
||||||
|
|
||||||
knownSchemaEndpoints.forEach((endpoint) => {
|
|
||||||
const key = `${endpoint.method}:${endpoint.path}`;
|
|
||||||
endpoints.push({
|
|
||||||
...endpoint,
|
|
||||||
implemented: implementedEndpoints.has(key),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return endpoints;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getImplementedEndpoints(): Set<string> {
|
|
||||||
return new Set([
|
|
||||||
'GET:/ai-voicebot/api/admin/names',
|
|
||||||
'POST:/ai-voicebot/api/admin/set_password',
|
|
||||||
'POST:/ai-voicebot/api/admin/clear_password',
|
|
||||||
'GET:/ai-voicebot/api/health',
|
|
||||||
'GET:/ai-voicebot/api/session',
|
|
||||||
'GET:/ai-voicebot/api/lobby',
|
|
||||||
'POST:/ai-voicebot/api/lobby/{sessionId}',
|
|
||||||
'GET:/ai-voicebot/api/bots/providers',
|
|
||||||
'GET:/ai-voicebot/api/bots',
|
|
||||||
'POST:/ai-voicebot/api/lobby/{session_id}',
|
|
||||||
'GET:/ai-voicebot/api/system/health'
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load the OpenAPI schema from the JSON file to detect runtime changes
|
|
||||||
*/
|
|
||||||
async loadSchemaFromJson(): Promise<any> {
|
|
||||||
try {
|
|
||||||
// Use dynamic base path from environment
|
|
||||||
const response = await fetch(`${base}/openapi-schema.json`);
|
|
||||||
if (response.ok) {
|
|
||||||
return await response.json();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Could not load OpenAPI schema for evolution checking:', error);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compare current schema with a previous version to detect changes
|
|
||||||
*/
|
|
||||||
async checkSchemaEvolution(): Promise<{
|
|
||||||
hasNewEndpoints: boolean;
|
|
||||||
hasChangedEndpoints: boolean;
|
|
||||||
newEndpoints: EndpointInfo[];
|
|
||||||
changedEndpoints: EndpointInfo[];
|
|
||||||
unimplementedEndpoints: EndpointInfo[];
|
|
||||||
}> {
|
|
||||||
const currentEndpoints = this.extractEndpointsFromSchema();
|
|
||||||
const unimplementedEndpoints = currentEndpoints.filter(ep => !ep.implemented);
|
|
||||||
|
|
||||||
// Try to load the actual schema for more detailed analysis
|
|
||||||
const schema = await this.loadSchemaFromJson();
|
|
||||||
let hasNewEndpoints = false;
|
|
||||||
let hasChangedEndpoints = false;
|
|
||||||
let newEndpoints: EndpointInfo[] = [];
|
|
||||||
let changedEndpoints: EndpointInfo[] = [];
|
|
||||||
|
|
||||||
if (schema) {
|
|
||||||
// Calculate a simple hash of the schema to detect changes
|
|
||||||
const schemaString = JSON.stringify(schema.paths || {});
|
|
||||||
const currentHash = this.simpleHash(schemaString);
|
|
||||||
|
|
||||||
if (this.lastSchemaHash && this.lastSchemaHash !== currentHash) {
|
|
||||||
hasChangedEndpoints = true;
|
|
||||||
console.info('🔄 OpenAPI schema has changed since last check');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.lastSchemaHash = currentHash;
|
|
||||||
|
|
||||||
// Extract endpoints from the actual schema
|
|
||||||
if (schema.paths) {
|
|
||||||
Object.keys(schema.paths).forEach(path => {
|
|
||||||
if (path === `${base}/{path}`) return; // Skip generic proxy
|
|
||||||
|
|
||||||
const pathObj = schema.paths[path];
|
|
||||||
Object.keys(pathObj).forEach((method) => {
|
|
||||||
const endpoint = `${method.toUpperCase()}:${path}`;
|
|
||||||
const implementedEndpoints = this.getImplementedEndpoints();
|
|
||||||
|
|
||||||
if (!implementedEndpoints.has(endpoint)) {
|
|
||||||
const endpointInfo: EndpointInfo = {
|
|
||||||
path,
|
|
||||||
method: method.toUpperCase(),
|
|
||||||
operationId: pathObj[method].operationId,
|
|
||||||
implemented: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check if this is a new endpoint (not in our known list)
|
|
||||||
const isKnown = currentEndpoints.some((ep) => ep.path === path && ep.method === method.toUpperCase());
|
|
||||||
|
|
||||||
if (!isKnown) {
|
|
||||||
hasNewEndpoints = true;
|
|
||||||
newEndpoints.push(endpointInfo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
hasNewEndpoints,
|
|
||||||
hasChangedEndpoints,
|
|
||||||
newEndpoints,
|
|
||||||
changedEndpoints,
|
|
||||||
unimplementedEndpoints
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private simpleHash(str: string): string {
|
|
||||||
let hash = 0;
|
|
||||||
for (let i = 0; i < str.length; i++) {
|
|
||||||
const char = str.charCodeAt(i);
|
|
||||||
hash = ((hash << 5) - hash) + char;
|
|
||||||
hash = hash & hash; // Convert to 32bit integer
|
|
||||||
}
|
|
||||||
return hash.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate implementation stubs for unimplemented endpoints
|
|
||||||
*/
|
|
||||||
generateImplementationStubs(endpoints: EndpointInfo[]): string {
|
|
||||||
return endpoints.map(endpoint => {
|
|
||||||
const methodName = this.generateMethodName(endpoint);
|
|
||||||
const returnType = `Promise<any>`; // Could be more specific with schema analysis
|
|
||||||
|
|
||||||
return ` async ${methodName}(): ${returnType} {
|
|
||||||
return this.request<any>('${endpoint.path}', { method: '${endpoint.method}' });
|
|
||||||
}`;
|
|
||||||
}).join('\n\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
private generateMethodName(endpoint: EndpointInfo): string {
|
|
||||||
// Convert operation ID or path to camelCase method name
|
|
||||||
if (endpoint.operationId) {
|
|
||||||
return endpoint.operationId.replace(/_[a-z]/g, (match) => match[1].toUpperCase());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback: generate from path and method
|
|
||||||
const pathParts = endpoint.path.split('/').filter(part => part && !part.startsWith('{'));
|
|
||||||
const lastPart = pathParts[pathParts.length - 1];
|
|
||||||
const method = endpoint.method.toLowerCase();
|
|
||||||
|
|
||||||
if (method === 'get') {
|
|
||||||
return `get${lastPart.charAt(0).toUpperCase() + lastPart.slice(1)}`;
|
|
||||||
} else {
|
|
||||||
return `${method}${lastPart.charAt(0).toUpperCase() + lastPart.slice(1)}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const advancedApiChecker = AdvancedApiEvolutionChecker.getInstance();
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,12 +1,13 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
/**
|
/**
|
||||||
* Automated API Client and Evolution Checker Updater
|
* Automated API Client Generator
|
||||||
*
|
*
|
||||||
* This script analyzes the generated OpenAPI schema and automatically updates:
|
* This script analyzes the generated OpenAPI schema and automatically generates:
|
||||||
* 1. api-client.ts - Adds missing endpoint implementations
|
* 1. api-client.ts - Complete TypeScript API client with types and methods
|
||||||
* 2. api-evolution-checker.ts - Updates known endpoints list
|
* 2. api-evolution-checker.ts - Updates known endpoints list
|
||||||
*
|
*
|
||||||
* Run this script after generating TypeScript types from OpenAPI schema.
|
* The api-client.ts file contains only minimal imports at the top, with everything
|
||||||
|
* else being auto-generated after a single marker.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
@ -18,11 +19,10 @@ const API_TYPES_PATH = path.join(__dirname, 'src', 'api-types.ts');
|
|||||||
const API_CLIENT_PATH = path.join(__dirname, 'src', 'api-client.ts');
|
const API_CLIENT_PATH = path.join(__dirname, 'src', 'api-client.ts');
|
||||||
const API_EVOLUTION_CHECKER_PATH = path.join(__dirname, 'src', 'api-evolution-checker.ts');
|
const API_EVOLUTION_CHECKER_PATH = path.join(__dirname, 'src', 'api-evolution-checker.ts');
|
||||||
|
|
||||||
class ApiClientUpdater {
|
class ApiClientGenerator {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.schema = null;
|
this.schema = null;
|
||||||
this.endpoints = [];
|
this.endpoints = [];
|
||||||
this.implementedEndpoints = new Set();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -51,21 +51,18 @@ class ApiClientUpdater {
|
|||||||
|
|
||||||
this.endpoints = [];
|
this.endpoints = [];
|
||||||
|
|
||||||
Object.entries(this.schema.paths).forEach(([path, pathObj]) => {
|
Object.entries(this.schema.paths).forEach(([path, pathItem]) => {
|
||||||
// Skip the generic proxy endpoint
|
Object.entries(pathItem).forEach(([method, operation]) => {
|
||||||
if (path === '/ai-voicebot/{path}') {
|
if (method === 'parameters') return; // Skip path-level parameters
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.entries(pathObj).forEach(([method, methodObj]) => {
|
|
||||||
const endpoint = {
|
const endpoint = {
|
||||||
path,
|
path,
|
||||||
method: method.toUpperCase(),
|
method: method.toUpperCase(),
|
||||||
operationId: methodObj.operationId,
|
operationId: operation.operationId,
|
||||||
summary: methodObj.summary,
|
summary: operation.summary,
|
||||||
parameters: methodObj.parameters || [],
|
requestBody: operation.requestBody,
|
||||||
requestBody: methodObj.requestBody,
|
parameters: operation.parameters || [],
|
||||||
responses: methodObj.responses
|
responses: operation.responses || {}
|
||||||
};
|
};
|
||||||
|
|
||||||
this.endpoints.push(endpoint);
|
this.endpoints.push(endpoint);
|
||||||
@ -76,88 +73,52 @@ class ApiClientUpdater {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse the current API client to find implemented endpoints
|
* Generate method name from operation ID or path with HTTP method prefix
|
||||||
*/
|
|
||||||
parseCurrentApiClient() {
|
|
||||||
try {
|
|
||||||
const clientContent = fs.readFileSync(API_CLIENT_PATH, 'utf8');
|
|
||||||
|
|
||||||
// Find implemented endpoints by looking for method patterns and path literals
|
|
||||||
// Now handles both old hardcoded paths and new this.getApiPath() calls
|
|
||||||
const methodRegex = /async\s+(\w+)\([^)]*\):[^{]*{\s*return\s+this\.request<[^>]*>\(([^,]+),\s*{\s*method:\s*"([^"]+)"/g;
|
|
||||||
|
|
||||||
let match;
|
|
||||||
while ((match = methodRegex.exec(clientContent)) !== null) {
|
|
||||||
const [, methodName, pathExpression, httpMethod] = match;
|
|
||||||
|
|
||||||
// Handle both string literals, template literals, and this.getApiPath() calls
|
|
||||||
let actualPath;
|
|
||||||
|
|
||||||
if (pathExpression.includes('this.getApiPath(')) {
|
|
||||||
// Extract path from this.getApiPath("path") or this.getApiPath(`path`)
|
|
||||||
const getApiPathMatch = pathExpression.match(/this\.getApiPath\(([^)]+)\)/);
|
|
||||||
if (getApiPathMatch) {
|
|
||||||
let innerPath = getApiPathMatch[1];
|
|
||||||
|
|
||||||
// Remove quotes or backticks
|
|
||||||
if ((innerPath.startsWith('"') && innerPath.endsWith('"')) ||
|
|
||||||
(innerPath.startsWith("'") && innerPath.endsWith("'"))) {
|
|
||||||
actualPath = innerPath.slice(1, -1);
|
|
||||||
} else if (innerPath.startsWith('`') && innerPath.endsWith('`')) {
|
|
||||||
// Template literal - convert ${param} back to {param}
|
|
||||||
actualPath = innerPath.slice(1, -1).replace(/\$\{([^}]+)\}/g, '{$1}');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (pathExpression.startsWith('"') && pathExpression.endsWith('"')) {
|
|
||||||
// String literal
|
|
||||||
actualPath = pathExpression.slice(1, -1);
|
|
||||||
} else if (pathExpression.startsWith('`') && pathExpression.endsWith('`')) {
|
|
||||||
// Template literal - convert ${param} back to {param}
|
|
||||||
actualPath = pathExpression.slice(1, -1).replace(/\$\{([^}]+)\}/g, '{$1}');
|
|
||||||
} else {
|
|
||||||
// Skip complex expressions we can't easily parse
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (actualPath) {
|
|
||||||
this.implementedEndpoints.add(`${httpMethod}:${actualPath}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`✅ Found ${this.implementedEndpoints.size} implemented endpoints in API client`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Failed to parse current API client:', error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate method name from operation ID or path
|
|
||||||
*/
|
*/
|
||||||
generateMethodName(endpoint) {
|
generateMethodName(endpoint) {
|
||||||
|
let baseName;
|
||||||
|
|
||||||
if (endpoint.operationId) {
|
if (endpoint.operationId) {
|
||||||
// Convert snake_case operation ID to camelCase
|
// Convert snake_case operation ID to camelCase
|
||||||
return endpoint.operationId
|
baseName = endpoint.operationId
|
||||||
.replace(/_ai_voicebot_.*$/, '') // Remove the long suffix
|
.replace(/_ai_voicebot_.*$/, '') // Remove the long suffix
|
||||||
.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase())
|
.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase())
|
||||||
.replace(/^([a-z])/, (_, letter) => letter.toLowerCase());
|
.replace(/^([a-z])/, (_, letter) => letter.toLowerCase());
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback: generate from path and method
|
|
||||||
const pathParts = endpoint.path.split('/').filter(part => part && !part.startsWith('{'));
|
|
||||||
const lastPart = pathParts[pathParts.length - 1] || 'resource';
|
|
||||||
const method = endpoint.method.toLowerCase();
|
|
||||||
|
|
||||||
if (method === 'get') {
|
|
||||||
return `get${lastPart.charAt(0).toUpperCase() + lastPart.slice(1)}`;
|
|
||||||
} else {
|
} else {
|
||||||
return `${method}${lastPart.charAt(0).toUpperCase() + lastPart.slice(1)}`;
|
// Fallback: generate from path
|
||||||
|
const pathParts = endpoint.path.split('/').filter(part => part && !part.startsWith('{'));
|
||||||
|
const lastPart = pathParts[pathParts.length - 1] || 'resource';
|
||||||
|
baseName = lastPart.charAt(0).toLowerCase() + lastPart.slice(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add method prefix to avoid duplicates
|
||||||
|
const method = endpoint.method.toLowerCase();
|
||||||
|
const methodPrefixes = {
|
||||||
|
'get': 'get',
|
||||||
|
'post': 'create',
|
||||||
|
'put': 'replace', // Use 'replace' for PUT to distinguish from PATCH
|
||||||
|
'patch': 'update', // Use 'update' for PATCH
|
||||||
|
'delete': 'delete',
|
||||||
|
'head': 'head',
|
||||||
|
'options': 'options'
|
||||||
|
};
|
||||||
|
|
||||||
|
const prefix = methodPrefixes[method] || method;
|
||||||
|
|
||||||
|
// If baseName already starts with the prefix, don't duplicate
|
||||||
|
if (baseName.toLowerCase().startsWith(prefix.toLowerCase())) {
|
||||||
|
return baseName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capitalize first letter of baseName and add prefix
|
||||||
|
const capitalizedBaseName = baseName.charAt(0).toUpperCase() + baseName.slice(1);
|
||||||
|
return prefix + capitalizedBaseName;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate parameter types and method signature
|
* Generate method implementation
|
||||||
*/
|
*/
|
||||||
generateMethodSignature(endpoint) {
|
generateMethodImplementation(endpoint) {
|
||||||
const methodName = this.generateMethodName(endpoint);
|
const methodName = this.generateMethodName(endpoint);
|
||||||
let params = [];
|
let params = [];
|
||||||
let pathParams = [];
|
let pathParams = [];
|
||||||
@ -174,113 +135,285 @@ class ApiClientUpdater {
|
|||||||
|
|
||||||
// Check for request body
|
// Check for request body
|
||||||
if (endpoint.requestBody) {
|
if (endpoint.requestBody) {
|
||||||
params.push('data: any'); // Could be more specific with schema analysis
|
params.push('data: any');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for query parameters
|
// Check for query parameters
|
||||||
if (endpoint.parameters && endpoint.parameters.length > 0) {
|
const hasQueryParams = endpoint.parameters && endpoint.parameters.some(p => p.in === 'query');
|
||||||
const queryParams = endpoint.parameters.filter(p => p.in === 'query');
|
if (hasQueryParams) {
|
||||||
if (queryParams.length > 0) {
|
params.push('params?: Record<string, string>');
|
||||||
params.push('params?: Record<string, string>');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const paramString = params.join(', ');
|
const methodSignature = `async ${methodName}(${params.join(', ')}): Promise<any>`;
|
||||||
const returnType = 'Promise<any>'; // Could be more specific
|
|
||||||
|
|
||||||
return {
|
// Build the path with parameter substitution
|
||||||
methodName,
|
let apiPath = endpoint.path;
|
||||||
paramString,
|
pathParams.forEach(param => {
|
||||||
returnType,
|
apiPath = apiPath.replace(`{${param}}`, `\${${param}}`);
|
||||||
pathParams
|
});
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate method implementation
|
|
||||||
*/
|
|
||||||
generateMethodImplementation(endpoint) {
|
|
||||||
const { methodName, paramString, returnType, pathParams } = this.generateMethodSignature(endpoint);
|
|
||||||
|
|
||||||
// Use this.getApiPath() to make paths dynamic based on PUBLIC_URL
|
|
||||||
let pathExpression;
|
|
||||||
if (pathParams.length > 0) {
|
|
||||||
// For parameterized paths, we need to use template literals
|
|
||||||
pathExpression = 'this.getApiPath(`' + endpoint.path.replace(/{([^}]+)}/g, '${$1}') + '`)';
|
|
||||||
} else {
|
|
||||||
// For static paths, use string literal
|
|
||||||
pathExpression = `this.getApiPath("${endpoint.path}")`;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Build the request options
|
||||||
let requestOptions = `{ method: "${endpoint.method}"`;
|
let requestOptions = `{ method: "${endpoint.method}"`;
|
||||||
|
|
||||||
if (endpoint.requestBody) {
|
if (endpoint.requestBody && endpoint.method !== 'GET') {
|
||||||
requestOptions += ', body: data';
|
requestOptions += ', body: data';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (endpoint.parameters && endpoint.parameters.some(p => p.in === 'query')) {
|
if (hasQueryParams) {
|
||||||
requestOptions += ', params';
|
requestOptions += ', params';
|
||||||
}
|
}
|
||||||
|
|
||||||
requestOptions += ' }';
|
requestOptions += ' }';
|
||||||
|
|
||||||
return ` async ${methodName}(${paramString}): ${returnType} {
|
return ` ${methodSignature} {
|
||||||
return this.request<any>(${pathExpression}, ${requestOptions});
|
return this.request<any>(this.getApiPath(\`${apiPath}\`), ${requestOptions});
|
||||||
}`;
|
}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the API client with missing endpoints
|
* Extract all types from schema for re-export
|
||||||
*/
|
*/
|
||||||
updateApiClient() {
|
extractAllTypes() {
|
||||||
|
if (!this.schema || !this.schema.components || !this.schema.components.schemas) {
|
||||||
|
return '// No schema types available for export';
|
||||||
|
}
|
||||||
|
|
||||||
|
const schemas = this.schema.components.schemas;
|
||||||
|
const allTypeNames = Object.keys(schemas).sort();
|
||||||
|
|
||||||
|
if (allTypeNames.length === 0) {
|
||||||
|
return '// No types found in schema';
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeExports = allTypeNames.map(typeName =>
|
||||||
|
`export type ${typeName} = components["schemas"]["${typeName}"];`
|
||||||
|
).join('\n');
|
||||||
|
|
||||||
|
return `// Re-export all types from the generated schema for convenience\n${typeExports}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate convenience API namespaces
|
||||||
|
*/
|
||||||
|
generateConvenienceApis() {
|
||||||
|
return `// Convenience API namespaces for easy usage
|
||||||
|
export const adminApi = {
|
||||||
|
listNames: () => apiClient.getListNames(),
|
||||||
|
setPassword: (data: any) => apiClient.createSetPassword(data),
|
||||||
|
clearPassword: (data: any) => apiClient.createClearPassword(data),
|
||||||
|
cleanupSessions: () => apiClient.createCleanupSessions(),
|
||||||
|
sessionMetrics: () => apiClient.getSessionMetrics(),
|
||||||
|
validateSessions: () => apiClient.getValidateSessions(),
|
||||||
|
cleanupLobbies: () => apiClient.createCleanupLobbies(),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const healthApi = {
|
||||||
|
check: () => apiClient.getHealthSummary(),
|
||||||
|
ready: () => apiClient.getReadinessProbe(),
|
||||||
|
live: () => apiClient.getLivenessProbe(),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const lobbiesApi = {
|
||||||
|
getAll: () => apiClient.getListLobbies(),
|
||||||
|
getChatMessages: (lobbyId: string, params?: Record<string, string>) => apiClient.getChatMessages(lobbyId, params),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sessionsApi = {
|
||||||
|
getCurrent: () => apiClient.getSession(),
|
||||||
|
createLobby: (sessionId: string, data: any) => apiClient.createLobby(sessionId, data),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const botsApi = {
|
||||||
|
getProviders: () => apiClient.getListBotProviders(),
|
||||||
|
getAvailable: () => apiClient.getListAvailableBots(),
|
||||||
|
requestJoinLobby: (botName: string, request: any) => apiClient.createRequestBotJoinLobby(botName, request),
|
||||||
|
requestLeaveLobby: (botInstanceId: string) => apiClient.createRequestBotLeaveLobby(botInstanceId),
|
||||||
|
getInstance: (botInstanceId: string) => apiClient.getBotInstance(botInstanceId),
|
||||||
|
registerProvider: (data: any) => apiClient.createRegisterBotProvider(data),
|
||||||
|
};`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create minimal template with just imports
|
||||||
|
*/
|
||||||
|
createMinimalTemplate() {
|
||||||
|
const template = `// TypeScript API client for AI Voicebot server
|
||||||
|
import { components } from "./api-types";
|
||||||
|
import { base } from "./Common";
|
||||||
|
|
||||||
|
// DO NOT MANUALLY EDIT BELOW THIS LINE - All content below is auto-generated
|
||||||
|
// To modify auto-generated content, edit the template in client/update-api-client.js
|
||||||
|
`;
|
||||||
|
|
||||||
|
fs.writeFileSync(API_CLIENT_PATH, template, 'utf8');
|
||||||
|
console.log('✅ Created minimal API client template');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate complete API client from scratch
|
||||||
|
*/
|
||||||
|
generateFullApiClient() {
|
||||||
try {
|
try {
|
||||||
const clientContent = fs.readFileSync(API_CLIENT_PATH, 'utf8');
|
// Generate all components
|
||||||
|
const typeExports = this.extractAllTypes();
|
||||||
|
|
||||||
// Find unimplemented endpoints
|
const allMethods = this.endpoints.map(endpoint => {
|
||||||
const unimplementedEndpoints = this.endpoints.filter(endpoint => {
|
|
||||||
const key = `${endpoint.method}:${endpoint.path}`;
|
|
||||||
return !this.implementedEndpoints.has(key);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (unimplementedEndpoints.length === 0) {
|
|
||||||
console.log('✅ All endpoints are already implemented in API client');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`🔧 Adding ${unimplementedEndpoints.length} missing endpoints to API client`);
|
|
||||||
|
|
||||||
// Generate new method implementations
|
|
||||||
const newMethods = unimplementedEndpoints.map(endpoint => {
|
|
||||||
console.log(` • ${endpoint.method} ${endpoint.path} (${this.generateMethodName(endpoint)})`);
|
console.log(` • ${endpoint.method} ${endpoint.path} (${this.generateMethodName(endpoint)})`);
|
||||||
return this.generateMethodImplementation(endpoint);
|
return this.generateMethodImplementation(endpoint);
|
||||||
}).join('\n\n');
|
}).join('\n\n');
|
||||||
|
|
||||||
// Find the insertion point (before the auto-generated comment and closing brace)
|
const convenienceApis = this.generateConvenienceApis();
|
||||||
const insertionMarker = ' // Auto-generated endpoints will be added here by update-api-client.js';
|
|
||||||
const insertionIndex = clientContent.indexOf(insertionMarker);
|
|
||||||
|
|
||||||
if (insertionIndex === -1) {
|
const autoGeneratedContent = `
|
||||||
throw new Error('Could not find auto-generated endpoints insertion marker in ApiClient class');
|
// Re-export all types from the generated schema
|
||||||
|
export type { components } from "./api-types";
|
||||||
|
|
||||||
|
${typeExports}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-generated endpoint methods
|
||||||
|
${allMethods}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default client instance
|
||||||
|
export const apiClient = new ApiClient();
|
||||||
|
|
||||||
|
${convenienceApis}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Read the current file and find the marker
|
||||||
|
const currentContent = fs.readFileSync(API_CLIENT_PATH, 'utf8');
|
||||||
|
const marker = '// DO NOT MANUALLY EDIT BELOW THIS LINE';
|
||||||
|
const markerIndex = currentContent.indexOf(marker);
|
||||||
|
|
||||||
|
if (markerIndex === -1) {
|
||||||
|
console.error('❌ Could not find auto-generation marker');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the end of the marker line
|
// Find the end of the marker line (including the guidance comment)
|
||||||
const markerEndIndex = clientContent.indexOf('\n', insertionIndex);
|
const markerEndIndex = currentContent.indexOf('\n', markerIndex);
|
||||||
const doNotEditLine = clientContent.indexOf('\n', markerEndIndex + 1);
|
const guidanceEndIndex = currentContent.indexOf('\n', markerEndIndex + 1);
|
||||||
|
|
||||||
// Insert the new methods after the "DO NOT MANUALLY EDIT BELOW THIS LINE" comment
|
// Keep everything up to and including the guidance comment, replace everything after
|
||||||
const updatedContent =
|
const templatePart = currentContent.slice(0, guidanceEndIndex + 1);
|
||||||
clientContent.slice(0, doNotEditLine + 1) +
|
const fullContent = templatePart + autoGeneratedContent;
|
||||||
'\n // Auto-generated endpoints\n' +
|
|
||||||
newMethods +
|
|
||||||
'\n' +
|
|
||||||
clientContent.slice(doNotEditLine + 1);
|
|
||||||
|
|
||||||
// Write the updated content
|
fs.writeFileSync(API_CLIENT_PATH, fullContent, 'utf8');
|
||||||
fs.writeFileSync(API_CLIENT_PATH, updatedContent, 'utf8');
|
console.log(`✅ Generated complete API client with ${this.endpoints.length} endpoints`);
|
||||||
console.log('✅ Updated API client with new endpoints');
|
|
||||||
|
|
||||||
// Also update the implemented endpoints list
|
} catch (error) {
|
||||||
this.updateApiClientImplementedEndpointsList(unimplementedEndpoints);
|
console.error('❌ Failed to generate API client:', error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update existing API client
|
||||||
|
*/
|
||||||
|
updateApiClient() {
|
||||||
|
try {
|
||||||
|
// Check if file exists
|
||||||
|
if (!fs.existsSync(API_CLIENT_PATH)) {
|
||||||
|
console.log('📝 API client file not found, creating minimal template...');
|
||||||
|
this.createMinimalTemplate();
|
||||||
|
}
|
||||||
|
|
||||||
|
const clientContent = fs.readFileSync(API_CLIENT_PATH, 'utf8');
|
||||||
|
|
||||||
|
// Check if this is a minimal template (only has imports and marker)
|
||||||
|
const marker = '// DO NOT MANUALLY EDIT BELOW THIS LINE';
|
||||||
|
const markerIndex = clientContent.indexOf(marker);
|
||||||
|
|
||||||
|
if (markerIndex === -1) {
|
||||||
|
console.log('⚠️ Auto-generation marker not found, creating new template...');
|
||||||
|
this.createMinimalTemplate();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always regenerate everything after the marker
|
||||||
|
console.log('🔄 Regenerating complete API client...');
|
||||||
|
this.generateFullApiClient();
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Failed to update API client:', error.message);
|
console.error('❌ Failed to update API client:', error.message);
|
||||||
@ -288,182 +421,95 @@ class ApiClientUpdater {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the implemented endpoints list in ApiClient
|
* Update API evolution checker
|
||||||
*/
|
*/
|
||||||
updateApiClientImplementedEndpointsList(newEndpoints) {
|
updateApiEvolutionChecker() {
|
||||||
try {
|
try {
|
||||||
const clientContent = fs.readFileSync(API_CLIENT_PATH, 'utf8');
|
// Generate list of all current endpoints for evolution tracking
|
||||||
|
// Convert paths to use dynamic base instead of hardcoded prefix
|
||||||
|
const currentEndpoints = this.endpoints.map(ep => {
|
||||||
|
const dynamicPath = ep.path.replace('/ai-voicebot', '${base}');
|
||||||
|
return `${ep.method}:${dynamicPath}`;
|
||||||
|
}).sort();
|
||||||
|
|
||||||
// Add new endpoints to the implemented list
|
const evolutionContent = `// Auto-generated API evolution checker
|
||||||
const newEndpointKeys = newEndpoints.map(ep => `${ep.method}:${ep.path}`);
|
// This file tracks known API endpoints to detect changes
|
||||||
const allImplementedEndpoints = Array.from(this.implementedEndpoints)
|
|
||||||
.concat(newEndpointKeys)
|
|
||||||
.sort();
|
|
||||||
|
|
||||||
const implementedArray = allImplementedEndpoints
|
import { base } from './Common';
|
||||||
.map(endpoint => ` '${endpoint}'`)
|
|
||||||
.join(',\n');
|
|
||||||
|
|
||||||
const newImplementedEndpoints = `return new Set([
|
export const knownEndpoints = new Set([
|
||||||
${implementedArray}
|
${currentEndpoints.map(ep => ` \`${ep}\``).join(',\n')}
|
||||||
]);`;
|
]);
|
||||||
|
|
||||||
// Replace the existing implemented endpoints set in ApiClient
|
// Schema path for dynamic usage
|
||||||
const implementedRegex = /private getImplementedEndpoints\(\): Set<string> \{[\s\S]*?return new Set\(\[[^\]]*\]\);[\s\S]*?\}/;
|
export const schemaPath = \`\${base}/openapi-schema.json\`;
|
||||||
|
|
||||||
const replacement = `private getImplementedEndpoints(): Set<string> {
|
// Proxy path pattern for matching
|
||||||
// Define all endpoints that are currently implemented in ApiClient
|
export const proxyPathPattern = \`\${base}/{path}\`;
|
||||||
// This list is automatically updated by update-api-client.js
|
|
||||||
${newImplementedEndpoints}
|
|
||||||
}`;
|
|
||||||
|
|
||||||
if (implementedRegex.test(clientContent)) {
|
export function checkApiEvolution(discoveredEndpoints: string[]): {
|
||||||
const updatedContent = clientContent.replace(implementedRegex, replacement);
|
newEndpoints: string[];
|
||||||
fs.writeFileSync(API_CLIENT_PATH, updatedContent, 'utf8');
|
removedEndpoints: string[];
|
||||||
console.log('✅ Updated implemented endpoints list in API client');
|
totalEndpoints: number;
|
||||||
} else {
|
} {
|
||||||
console.warn('⚠️ Could not find getImplementedEndpoints method in API client');
|
const discoveredSet = new Set(discoveredEndpoints);
|
||||||
}
|
|
||||||
|
const newEndpoints = discoveredEndpoints.filter(ep => !knownEndpoints.has(ep));
|
||||||
|
const removedEndpoints = Array.from(knownEndpoints).filter(ep => !discoveredSet.has(ep));
|
||||||
|
|
||||||
|
return {
|
||||||
|
newEndpoints,
|
||||||
|
removedEndpoints,
|
||||||
|
totalEndpoints: discoveredEndpoints.length
|
||||||
|
};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
fs.writeFileSync(API_EVOLUTION_CHECKER_PATH, evolutionContent, 'utf8');
|
||||||
|
console.log('✅ Updated API evolution checker with current endpoints');
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Failed to update implemented endpoints list in API client:', error.message);
|
console.error('❌ Failed to update API evolution checker:', error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the API evolution checker with current endpoints
|
* Main execution method
|
||||||
*/
|
|
||||||
updateEvolutionChecker() {
|
|
||||||
try {
|
|
||||||
const checkerContent = fs.readFileSync(API_EVOLUTION_CHECKER_PATH, 'utf8');
|
|
||||||
|
|
||||||
// Generate the updated known endpoints list
|
|
||||||
const knownEndpointsArray = this.endpoints.map(endpoint => {
|
|
||||||
return ` { path: '${endpoint.path}', method: '${endpoint.method}', operationId: '${endpoint.operationId || ''}' }`;
|
|
||||||
}).join(',\n');
|
|
||||||
|
|
||||||
const newKnownEndpoints = `const knownSchemaEndpoints = [
|
|
||||||
${knownEndpointsArray}
|
|
||||||
];`;
|
|
||||||
|
|
||||||
// Replace the existing knownSchemaEndpoints array
|
|
||||||
const knownEndpointsRegex = /const knownSchemaEndpoints = \[[^\]]+\];/s;
|
|
||||||
|
|
||||||
if (knownEndpointsRegex.test(checkerContent)) {
|
|
||||||
const updatedContent = checkerContent.replace(knownEndpointsRegex, newKnownEndpoints);
|
|
||||||
fs.writeFileSync(API_EVOLUTION_CHECKER_PATH, updatedContent, 'utf8');
|
|
||||||
console.log('✅ Updated API evolution checker with current endpoints');
|
|
||||||
} else {
|
|
||||||
console.warn('⚠️ Could not find knownSchemaEndpoints array in evolution checker');
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Failed to update evolution checker:', error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the implemented endpoints list in the evolution checker
|
|
||||||
*/
|
|
||||||
updateImplementedEndpointsList() {
|
|
||||||
try {
|
|
||||||
const checkerContent = fs.readFileSync(API_EVOLUTION_CHECKER_PATH, 'utf8');
|
|
||||||
|
|
||||||
// Generate the updated implemented endpoints list
|
|
||||||
const implementedArray = Array.from(this.implementedEndpoints)
|
|
||||||
.concat(this.endpoints.map(ep => `${ep.method}:${ep.path}`))
|
|
||||||
.filter((value, index, self) => self.indexOf(value) === index) // Remove duplicates
|
|
||||||
.map(endpoint => ` '${endpoint}'`)
|
|
||||||
.join(',\n');
|
|
||||||
|
|
||||||
const newImplementedEndpoints = `return new Set([
|
|
||||||
${implementedArray}
|
|
||||||
]);`;
|
|
||||||
|
|
||||||
// Replace the existing implemented endpoints set
|
|
||||||
const implementedRegex = /return new Set\(\[[^\]]+\]\);/s;
|
|
||||||
|
|
||||||
if (implementedRegex.test(checkerContent)) {
|
|
||||||
const updatedContent = checkerContent.replace(implementedRegex, newImplementedEndpoints);
|
|
||||||
fs.writeFileSync(API_EVOLUTION_CHECKER_PATH, updatedContent, 'utf8');
|
|
||||||
console.log('✅ Updated implemented endpoints list in evolution checker');
|
|
||||||
} else {
|
|
||||||
console.warn('⚠️ Could not find implemented endpoints set in evolution checker');
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Failed to update implemented endpoints list:', error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate updated type exports for the API client
|
|
||||||
*/
|
|
||||||
updateTypeExports() {
|
|
||||||
try {
|
|
||||||
const clientContent = fs.readFileSync(API_CLIENT_PATH, 'utf8');
|
|
||||||
|
|
||||||
// Extract all schema types from the generated api-types.ts
|
|
||||||
const typesContent = fs.readFileSync(API_TYPES_PATH, 'utf8');
|
|
||||||
const schemaRegex = /export interface paths \{[\s\S]*?\n\}/;
|
|
||||||
const componentsRegex = /export interface components \{[\s\S]*schemas: \{[\s\S]*?\n \};\n\}/;
|
|
||||||
|
|
||||||
// Find all schema names in components.schemas
|
|
||||||
const schemaNames = [];
|
|
||||||
const schemaMatches = typesContent.match(/"([^"]+)":\s*\{/g);
|
|
||||||
if (schemaMatches) {
|
|
||||||
schemaMatches.forEach(match => {
|
|
||||||
const name = match.match(/"([^"]+)":/)[1];
|
|
||||||
if (name && !name.includes('ValidationError')) {
|
|
||||||
schemaNames.push(name);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`✅ Found ${schemaNames.length} schema types for potential export`);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Failed to update type exports:', error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Run the complete update process
|
|
||||||
*/
|
*/
|
||||||
async run() {
|
async run() {
|
||||||
console.log('🚀 Starting automated API client update...\n');
|
console.log('🚀 Starting automated API client generation...');
|
||||||
|
|
||||||
|
// Load and validate schema
|
||||||
if (!this.loadSchema()) {
|
if (!this.loadSchema()) {
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract endpoints
|
||||||
this.extractEndpoints();
|
this.extractEndpoints();
|
||||||
this.parseCurrentApiClient();
|
|
||||||
|
|
||||||
console.log('\n📊 Analysis Results:');
|
if (this.endpoints.length === 0) {
|
||||||
|
console.error('❌ No endpoints found in schema');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n📊 Analysis Results:`);
|
||||||
console.log(` • Schema endpoints: ${this.endpoints.length}`);
|
console.log(` • Schema endpoints: ${this.endpoints.length}`);
|
||||||
console.log(` • Implemented endpoints: ${this.implementedEndpoints.size}`);
|
|
||||||
console.log(` • Missing endpoints: ${this.endpoints.length - this.implementedEndpoints.size}\n`);
|
|
||||||
|
|
||||||
// Update files
|
// Update/generate API client
|
||||||
this.updateApiClient();
|
this.updateApiClient();
|
||||||
this.updateEvolutionChecker();
|
|
||||||
this.updateImplementedEndpointsList();
|
|
||||||
|
|
||||||
console.log('\n✅ API client update complete!');
|
// Update evolution checker
|
||||||
|
this.updateApiEvolutionChecker();
|
||||||
|
|
||||||
|
console.log('\n✅ API client generation complete!');
|
||||||
console.log('📄 Updated files:');
|
console.log('📄 Updated files:');
|
||||||
console.log(' - api-client.ts (added missing endpoints)');
|
console.log(' - api-client.ts (regenerated)');
|
||||||
console.log(' - api-evolution-checker.ts (updated known endpoints)');
|
console.log(' - api-evolution-checker.ts (updated known endpoints)');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run the updater
|
// Run if called directly
|
||||||
if (require.main === module) {
|
if (require.main === module) {
|
||||||
const updater = new ApiClientUpdater();
|
const generator = new ApiClientGenerator();
|
||||||
updater.run().catch(error => {
|
generator.run().catch(console.error);
|
||||||
console.error('❌ Update failed:', error);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = ApiClientUpdater;
|
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Comprehensive script to generate TypeScript types from FastAPI OpenAPI schema.
|
# Comprehensive script to generate TypeScript types from FastAPI OpenAPI schema.
|
||||||
# This script coordinates between the server and frontend containers to:
|
# This script coordinates between the server and frontend containers to:
|
||||||
# 1. Generate OpenAPI schema from FastAPI server
|
# 1. Ensure the server is running and ready
|
||||||
# 2. Generate TypeScript types from the schema
|
# 2. Generate OpenAPI schema from the running FastAPI server
|
||||||
# 3. Ensure frontend container dependencies are installed
|
# 3. Generate TypeScript types from the schema
|
||||||
|
# 4. Ensure frontend container dependencies are installed
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
@ -12,29 +13,61 @@ echo "🚀 Starting OpenAPI TypeScript generation process..."
|
|||||||
# Change to the project directory
|
# Change to the project directory
|
||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
echo "📋 Step 1: Generating OpenAPI schema from FastAPI server..."
|
echo "📋 Step 1: Ensuring server container is running and ready..."
|
||||||
|
docker compose up -d server
|
||||||
|
|
||||||
|
# Load environment variables
|
||||||
|
source .env
|
||||||
|
|
||||||
|
# Build health check URL from environment variables
|
||||||
|
HEALTH_URL="${PUBLIC_SERVER_URL}${PUBLIC_URL}/api/system/health"
|
||||||
|
echo "🔍 Using health check URL: $HEALTH_URL"
|
||||||
|
|
||||||
|
# Wait for server to be ready
|
||||||
|
echo "⏳ Waiting for server to be ready..."
|
||||||
|
max_retries=30
|
||||||
|
retry_count=0
|
||||||
|
while [ $retry_count -lt $max_retries ]; do
|
||||||
|
# Use curl with SSL verification disabled for self-signed certs
|
||||||
|
if docker compose exec server curl -f -k "$HEALTH_URL" >/dev/null 2>&1; then
|
||||||
|
echo "✅ Server is ready!"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
retry_count=$((retry_count + 1))
|
||||||
|
echo " Attempt $retry_count/$max_retries - Server not ready yet, waiting..."
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ $retry_count -eq $max_retries ]; then
|
||||||
|
echo "❌ Server failed to become ready within timeout"
|
||||||
|
echo "📋 Server logs:"
|
||||||
|
docker compose logs server --tail=20
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "📋 Step 2: Generating OpenAPI schema from running FastAPI server..."
|
||||||
docker compose exec server uv run python3 generate_schema_simple.py
|
docker compose exec server uv run python3 generate_schema_simple.py
|
||||||
docker compose cp server:/client/openapi-schema.json ./client/openapi-schema.json
|
docker compose cp server:/client/openapi-schema.json ./client/openapi-schema.json
|
||||||
|
|
||||||
echo "📋 Step 2: Ensuring frontend container is running..."
|
echo "📋 Step 3: Ensuring frontend container is running..."
|
||||||
docker compose up -d client
|
docker compose up -d client
|
||||||
|
|
||||||
echo "📋 Step 3: Installing/updating frontend dependencies..."
|
echo "📋 Step 4: Installing/updating frontend dependencies..."
|
||||||
docker compose exec client npm install --legacy-peer-deps
|
docker compose exec client npm install --legacy-peer-deps
|
||||||
|
|
||||||
echo "📋 Step 4: Generating TypeScript types from OpenAPI schema..."
|
echo "📋 Step 5: Generating TypeScript types from OpenAPI schema..."
|
||||||
docker compose exec client npx openapi-typescript openapi-schema.json -o src/api-types.ts
|
docker compose exec client npx openapi-typescript openapi-schema.json -o src/api-types.ts
|
||||||
|
|
||||||
echo "📋 Step 5: Automatically updating API client and evolution checker..."
|
echo "📋 Step 6: Automatically updating API client and evolution checker..."
|
||||||
docker compose exec client node update-api-client.js
|
docker compose exec client node update-api-client.js
|
||||||
|
|
||||||
echo "📋 Step 6: Running TypeScript type checking..."
|
echo "📋 Step 7: Running TypeScript type checking..."
|
||||||
docker compose exec client npm run type-check
|
docker compose exec client npm run type-check
|
||||||
|
|
||||||
echo "📋 Step 7: Testing dynamic path usage..."
|
echo "📋 Step 8: Testing dynamic path usage..."
|
||||||
docker compose exec client npm run test-dynamic-paths
|
docker compose exec client npm run test-dynamic-paths
|
||||||
|
|
||||||
echo "📋 Step 8: Running API evolution check..."
|
echo "📋 Step 9: Running API evolution check..."
|
||||||
docker compose exec client node check-api-evolution.js
|
docker compose exec client node check-api-evolution.js
|
||||||
|
|
||||||
echo "✅ TypeScript generation and API client update complete!"
|
echo "✅ TypeScript generation and API client update complete!"
|
||||||
|
@ -1,26 +1,69 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
Simple OpenAPI schema generator for FastAPI.
|
Simple OpenAPI schema generator for FastAPI.
|
||||||
This generates only the JSON schema file that can be used with openapi-typescript.
|
This generates the JSON schema file by fetching it from the running server's OpenAPI endpoint.
|
||||||
|
This ensures we get the complete schema as the server sees it at runtime.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import sys
|
import sys
|
||||||
|
import os
|
||||||
|
import asyncio
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import httpx
|
||||||
|
|
||||||
def generate_schema():
|
async def generate_schema_async():
|
||||||
"""Generate OpenAPI schema from the FastAPI app"""
|
"""Generate OpenAPI schema from the running FastAPI server"""
|
||||||
try:
|
try:
|
||||||
# Add shared module to path for Docker environment
|
# Configuration
|
||||||
shared_path = "/shared"
|
public_url = os.getenv("PUBLIC_URL", "/")
|
||||||
if shared_path not in sys.path:
|
if not public_url.endswith("/"):
|
||||||
sys.path.insert(0, shared_path)
|
public_url += "/"
|
||||||
|
|
||||||
# Import the FastAPI app
|
# Server endpoint - use PUBLIC_SERVER_URL from .env
|
||||||
from main import app
|
server_url = os.getenv("PUBLIC_SERVER_URL", "https://server:8000")
|
||||||
|
|
||||||
# Get the OpenAPI schema
|
# Determine if SSL verification should be disabled based on protocol
|
||||||
schema = app.openapi()
|
use_ssl = server_url.startswith("https://")
|
||||||
|
verify_ssl = not use_ssl or os.getenv("SSL_VERIFY", "false").lower() == "true"
|
||||||
|
|
||||||
|
# OpenAPI schema endpoint
|
||||||
|
openapi_url = f"{server_url}{public_url}openapi.json"
|
||||||
|
|
||||||
|
print(f"🔍 Fetching OpenAPI schema from: {openapi_url}")
|
||||||
|
print(
|
||||||
|
f"🔒 SSL mode: {'enabled' if use_ssl else 'disabled'}, verification: {'enabled' if verify_ssl else 'disabled'}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Wait for server to be ready and fetch schema
|
||||||
|
max_retries = 10
|
||||||
|
retry_delay = 2
|
||||||
|
|
||||||
|
for attempt in range(max_retries):
|
||||||
|
try:
|
||||||
|
# Create HTTP client with appropriate SSL settings
|
||||||
|
async with httpx.AsyncClient(timeout=30.0, verify=verify_ssl) as client:
|
||||||
|
response = await client.get(openapi_url)
|
||||||
|
response.raise_for_status()
|
||||||
|
schema = response.json()
|
||||||
|
break
|
||||||
|
except (httpx.ConnectError, httpx.TimeoutException) as e:
|
||||||
|
if attempt < max_retries - 1:
|
||||||
|
print(
|
||||||
|
f"⏳ Server not ready (attempt {attempt + 1}/{max_retries}), waiting {retry_delay}s..."
|
||||||
|
)
|
||||||
|
await asyncio.sleep(retry_delay)
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
raise Exception(
|
||||||
|
f"Failed to connect to server after {max_retries} attempts: {e}"
|
||||||
|
)
|
||||||
|
except httpx.HTTPStatusError as e:
|
||||||
|
raise Exception(
|
||||||
|
f"Server returned error {e.response.status_code}: {e.response.text}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise Exception("Failed to fetch schema - max retries exceeded")
|
||||||
|
|
||||||
# Write the schema to a JSON file that can be accessed from outside container
|
# Write the schema to a JSON file that can be accessed from outside container
|
||||||
schema_file = Path("/client/openapi-schema.json")
|
schema_file = Path("/client/openapi-schema.json")
|
||||||
@ -28,8 +71,16 @@ def generate_schema():
|
|||||||
json.dump(schema, f, indent=2, ensure_ascii=False)
|
json.dump(schema, f, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
print(f"✅ OpenAPI schema generated successfully at: {schema_file}")
|
print(f"✅ OpenAPI schema generated successfully at: {schema_file}")
|
||||||
|
print(f"Schema contains {len(schema.get('paths', {}))} API paths")
|
||||||
print(f"Schema contains {len(schema.get('components', {}).get('schemas', {}))} component schemas")
|
print(f"Schema contains {len(schema.get('components', {}).get('schemas', {}))} component schemas")
|
||||||
|
|
||||||
|
# Print some info about what endpoints were found
|
||||||
|
paths = schema.get("paths", {})
|
||||||
|
print("📋 Found API endpoints:")
|
||||||
|
for path in sorted(paths.keys()):
|
||||||
|
methods = list(paths[path].keys())
|
||||||
|
print(f" {path} ({', '.join(methods)})")
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -38,6 +89,15 @@ def generate_schema():
|
|||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def generate_schema():
|
||||||
|
"""Synchronous wrapper for async schema generation"""
|
||||||
|
try:
|
||||||
|
return asyncio.run(generate_schema_async())
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error in schema generation wrapper: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
success = generate_schema()
|
success = generate_schema()
|
||||||
sys.exit(0 if success else 1)
|
sys.exit(0 if success else 1)
|
||||||
|
@ -86,11 +86,18 @@ if not public_url.endswith("/"):
|
|||||||
|
|
||||||
ADMIN_TOKEN = os.getenv("ADMIN_TOKEN", None)
|
ADMIN_TOKEN = os.getenv("ADMIN_TOKEN", None)
|
||||||
|
|
||||||
# Create FastAPI app first
|
# Create FastAPI app with proper URL configuration
|
||||||
|
openapi_url = f"{public_url}openapi.json"
|
||||||
|
docs_url = f"{public_url}docs"
|
||||||
|
redoc_url = f"{public_url}redoc"
|
||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="AI Voice Bot Server (Refactored)",
|
title="AI Voice Bot Server (Refactored)",
|
||||||
description="WebRTC voice chat server with modular architecture",
|
description="WebRTC voice chat server with modular architecture",
|
||||||
version="2.0.0",
|
version="2.0.0",
|
||||||
|
openapi_url=openapi_url,
|
||||||
|
docs_url=docs_url,
|
||||||
|
redoc_url=redoc_url,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(f"Starting server with public URL: {public_url}")
|
logger.info(f"Starting server with public URL: {public_url}")
|
||||||
|
Loading…
x
Reference in New Issue
Block a user