Lots of tweaks
This commit is contained in:
parent
36548171d6
commit
71555c5230
@ -263,7 +263,9 @@ const LobbyView: React.FC<LobbyProps> = (props: LobbyProps) => {
|
||||
))} */}
|
||||
|
||||
<Box sx={{ display: "flex", gap: 2, flexWrap: "wrap" }}>
|
||||
{session && socketUrl && <UserList socketUrl={socketUrl} session={session} />}
|
||||
{session && socketUrl && lobby && (
|
||||
<UserList socketUrl={socketUrl} session={session} lobbyId={lobby.id} />
|
||||
)}
|
||||
{session && socketUrl && lobby && (
|
||||
<LobbyChat socketUrl={socketUrl} session={session} lobbyId={lobby.id} />
|
||||
)}
|
||||
|
@ -15,8 +15,15 @@
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.bot-config-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.bot-config-header h3 {
|
||||
margin: 0 0 8px 0;
|
||||
margin: 0;
|
||||
color: #212529;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
@ -172,6 +179,30 @@
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.config-refresh-button {
|
||||
padding: 6px 12px;
|
||||
background: #28a745;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s ease-in-out;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.config-refresh-button:hover:not(:disabled) {
|
||||
background: #1e7e34;
|
||||
}
|
||||
|
||||
.config-refresh-button:disabled {
|
||||
background: #6c757d;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.bot-config-loading,
|
||||
.bot-config-error,
|
||||
.bot-config-unavailable {
|
||||
|
@ -7,11 +7,12 @@
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import './BotConfig.css';
|
||||
import { base } from "./Common";
|
||||
import "./BotConfig.css";
|
||||
|
||||
interface ConfigParameter {
|
||||
name: string;
|
||||
type: 'string' | 'number' | 'boolean' | 'select' | 'range';
|
||||
type: "string" | "number" | "boolean" | "select" | "range";
|
||||
label: string;
|
||||
description: string;
|
||||
default_value?: any;
|
||||
@ -47,58 +48,74 @@ interface BotConfigProps {
|
||||
onConfigUpdate?: (config: BotConfig) => void;
|
||||
}
|
||||
|
||||
const BotConfigComponent: React.FC<BotConfigProps> = ({
|
||||
botName,
|
||||
lobbyId,
|
||||
onConfigUpdate
|
||||
}) => {
|
||||
const BotConfigComponent: React.FC<BotConfigProps> = ({ botName, lobbyId, onConfigUpdate }) => {
|
||||
const [schema, setSchema] = useState<ConfigSchema | null>(null);
|
||||
const [currentConfig, setCurrentConfig] = useState<BotConfig | null>(null);
|
||||
const [configValues, setConfigValues] = useState<{ [key: string]: any }>({});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
// Fetch configuration schema
|
||||
useEffect(() => {
|
||||
const fetchSchema = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await fetch(`/api/bots/config/schema/${botName}`);
|
||||
|
||||
if (response.ok) {
|
||||
const schemaData = await response.json();
|
||||
setSchema(schemaData);
|
||||
|
||||
// Initialize config values with defaults
|
||||
const defaultValues: { [key: string]: any } = {};
|
||||
schemaData.parameters.forEach((param: ConfigParameter) => {
|
||||
if (param.default_value !== undefined) {
|
||||
defaultValues[param.name] = param.default_value;
|
||||
}
|
||||
});
|
||||
setConfigValues(defaultValues);
|
||||
} else if (response.status === 404) {
|
||||
setError(`Bot "${botName}" does not support configuration`);
|
||||
} else {
|
||||
setError('Failed to fetch configuration schema');
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Network error while fetching configuration schema');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
const fetchSchema = async (forceRefresh = false) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
// Use refresh endpoint if force refresh is requested
|
||||
const url = forceRefresh
|
||||
? `${base}/api/bots/config/schema/${botName}/refresh`
|
||||
: `${base}/api/bots/config/schema/${botName}`;
|
||||
|
||||
const method = forceRefresh ? "POST" : "GET";
|
||||
|
||||
const response = await fetch(url, { method });
|
||||
|
||||
if (response.ok) {
|
||||
const responseData = await response.json();
|
||||
// For refresh endpoint, the schema is in the 'schema' field
|
||||
const schemaData = forceRefresh ? responseData.schema : responseData;
|
||||
|
||||
setSchema(schemaData);
|
||||
|
||||
// Initialize config values with defaults
|
||||
const defaultValues: { [key: string]: any } = {};
|
||||
schemaData.parameters.forEach((param: ConfigParameter) => {
|
||||
if (param.default_value !== undefined) {
|
||||
defaultValues[param.name] = param.default_value;
|
||||
}
|
||||
});
|
||||
setConfigValues(defaultValues);
|
||||
} else if (response.status === 404) {
|
||||
setError(`Bot "${botName}" does not support configuration`);
|
||||
} else {
|
||||
setError("Failed to fetch configuration schema");
|
||||
}
|
||||
} catch (err) {
|
||||
setError("Network error while fetching configuration schema");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchSchema();
|
||||
}, [botName]);
|
||||
|
||||
// Handle schema refresh
|
||||
const handleRefreshSchema = async () => {
|
||||
setRefreshing(true);
|
||||
await fetchSchema(true);
|
||||
setRefreshing(false);
|
||||
};
|
||||
|
||||
// Fetch current configuration
|
||||
useEffect(() => {
|
||||
const fetchCurrentConfig = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/bots/config/lobby/${lobbyId}/bot/${botName}`);
|
||||
|
||||
const response = await fetch(`${base}/api/bots/config/lobby/${lobbyId}/bot/${botName}`);
|
||||
|
||||
if (response.ok) {
|
||||
const config = await response.json();
|
||||
setCurrentConfig(config);
|
||||
@ -106,7 +123,7 @@ const BotConfigComponent: React.FC<BotConfigProps> = ({
|
||||
}
|
||||
// If 404, no existing config - that's fine
|
||||
} catch (err) {
|
||||
console.warn('Failed to fetch current config:', err);
|
||||
console.warn("Failed to fetch current config:", err);
|
||||
}
|
||||
};
|
||||
|
||||
@ -116,9 +133,9 @@ const BotConfigComponent: React.FC<BotConfigProps> = ({
|
||||
}, [botName, lobbyId, schema]);
|
||||
|
||||
const handleValueChange = (paramName: string, value: any) => {
|
||||
setConfigValues(prev => ({
|
||||
setConfigValues((prev) => ({
|
||||
...prev,
|
||||
[paramName]: value
|
||||
[paramName]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
@ -127,15 +144,15 @@ const BotConfigComponent: React.FC<BotConfigProps> = ({
|
||||
setSaving(true);
|
||||
setError(null);
|
||||
|
||||
const response = await fetch('/api/bots/config/update', {
|
||||
method: 'POST',
|
||||
const response = await fetch(`${base}/api/bots/config/update`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
bot_name: botName,
|
||||
lobby_id: lobbyId,
|
||||
config_values: configValues
|
||||
config_values: configValues,
|
||||
}),
|
||||
});
|
||||
|
||||
@ -147,10 +164,10 @@ const BotConfigComponent: React.FC<BotConfigProps> = ({
|
||||
onConfigUpdate(result.updated_config);
|
||||
}
|
||||
} else {
|
||||
setError(result.message || 'Failed to save configuration');
|
||||
setError(result.message || "Failed to save configuration");
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Network error while saving configuration');
|
||||
setError("Network error while saving configuration");
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
@ -160,7 +177,7 @@ const BotConfigComponent: React.FC<BotConfigProps> = ({
|
||||
const value = configValues[param.name];
|
||||
|
||||
switch (param.type) {
|
||||
case 'boolean':
|
||||
case "boolean":
|
||||
return (
|
||||
<div key={param.name} className="config-parameter">
|
||||
<label className="config-label">
|
||||
@ -175,16 +192,16 @@ const BotConfigComponent: React.FC<BotConfigProps> = ({
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'select':
|
||||
case "select":
|
||||
return (
|
||||
<div key={param.name} className="config-parameter">
|
||||
<label className="config-label">{param.label}</label>
|
||||
<select
|
||||
value={value || ''}
|
||||
value={value || ""}
|
||||
onChange={(e) => handleValueChange(param.name, e.target.value)}
|
||||
className="config-select"
|
||||
>
|
||||
{param.options?.map(option => (
|
||||
{param.options?.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
@ -194,7 +211,7 @@ const BotConfigComponent: React.FC<BotConfigProps> = ({
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'range':
|
||||
case "range":
|
||||
return (
|
||||
<div key={param.name} className="config-parameter">
|
||||
<label className="config-label">{param.label}</label>
|
||||
@ -214,7 +231,7 @@ const BotConfigComponent: React.FC<BotConfigProps> = ({
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'number':
|
||||
case "number":
|
||||
return (
|
||||
<div key={param.name} className="config-parameter">
|
||||
<label className="config-label">{param.label}</label>
|
||||
@ -223,7 +240,7 @@ const BotConfigComponent: React.FC<BotConfigProps> = ({
|
||||
min={param.min_value}
|
||||
max={param.max_value}
|
||||
step={param.step || 1}
|
||||
value={value || ''}
|
||||
value={value || ""}
|
||||
onChange={(e) => handleValueChange(param.name, Number(e.target.value))}
|
||||
className="config-input"
|
||||
/>
|
||||
@ -231,7 +248,7 @@ const BotConfigComponent: React.FC<BotConfigProps> = ({
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'string':
|
||||
case "string":
|
||||
default:
|
||||
return (
|
||||
<div key={param.name} className="config-parameter">
|
||||
@ -240,7 +257,7 @@ const BotConfigComponent: React.FC<BotConfigProps> = ({
|
||||
type="text"
|
||||
maxLength={param.max_length}
|
||||
pattern={param.pattern}
|
||||
value={value || ''}
|
||||
value={value || ""}
|
||||
onChange={(e) => handleValueChange(param.name, e.target.value)}
|
||||
className="config-input"
|
||||
/>
|
||||
@ -254,12 +271,12 @@ const BotConfigComponent: React.FC<BotConfigProps> = ({
|
||||
if (!schema) return null;
|
||||
|
||||
if (schema.categories && schema.categories.length > 0) {
|
||||
return schema.categories.map(category => (
|
||||
return schema.categories.map((category) => (
|
||||
<div key={category.name} className="config-category">
|
||||
<h4 className="category-title">{category.name}</h4>
|
||||
<div className="category-parameters">
|
||||
{category.parameters.map(paramName => {
|
||||
const param = schema.parameters.find(p => p.name === paramName);
|
||||
{category.parameters.map((paramName) => {
|
||||
const param = schema.parameters.find((p) => p.name === paramName);
|
||||
return param ? renderParameter(param) : null;
|
||||
})}
|
||||
</div>
|
||||
@ -285,26 +302,28 @@ const BotConfigComponent: React.FC<BotConfigProps> = ({
|
||||
return (
|
||||
<div className="bot-config-container">
|
||||
<div className="bot-config-header">
|
||||
<h3>Configure {botName}</h3>
|
||||
<div className="bot-config-title">
|
||||
<h3>Configure {botName}</h3>
|
||||
<button
|
||||
onClick={handleRefreshSchema}
|
||||
disabled={refreshing || loading}
|
||||
className="config-refresh-button"
|
||||
title="Refresh configuration schema from bot"
|
||||
>
|
||||
{refreshing ? "⟳" : "↻"} Refresh Schema
|
||||
</button>
|
||||
</div>
|
||||
<p>Lobby: {lobbyId}</p>
|
||||
{currentConfig && (
|
||||
<p className="config-meta">
|
||||
Last updated: {new Date(currentConfig.updated_at * 1000).toLocaleString()}
|
||||
</p>
|
||||
<p className="config-meta">Last updated: {new Date(currentConfig.updated_at * 1000).toLocaleString()}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="bot-config-form">
|
||||
{renderParametersByCategory()}
|
||||
</div>
|
||||
<div className="bot-config-form">{renderParametersByCategory()}</div>
|
||||
|
||||
<div className="bot-config-actions">
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
className="config-save-button"
|
||||
>
|
||||
{saving ? 'Saving...' : 'Save Configuration'}
|
||||
<button onClick={handleSave} disabled={saving} className="config-save-button">
|
||||
{saving ? "Saving..." : "Save Configuration"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -2,12 +2,19 @@ import React, { useState, useEffect, useCallback } from "react";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import List from "@mui/material/List";
|
||||
import Button from "@mui/material/Button";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import Dialog from "@mui/material/Dialog";
|
||||
import DialogTitle from "@mui/material/DialogTitle";
|
||||
import DialogContent from "@mui/material/DialogContent";
|
||||
import DialogActions from "@mui/material/DialogActions";
|
||||
import SettingsIcon from "@mui/icons-material/Settings";
|
||||
import "./UserList.css";
|
||||
import { MediaControl, MediaAgent, Peer } from "./MediaControl";
|
||||
import Box from "@mui/material/Box";
|
||||
import { Session } from "./GlobalContext";
|
||||
import useWebSocket from "react-use-websocket";
|
||||
import { ApiClient, BotLeaveLobbyRequest } from "./api-client";
|
||||
import BotConfig from "./BotConfig";
|
||||
|
||||
type User = {
|
||||
name: string;
|
||||
@ -24,13 +31,18 @@ type User = {
|
||||
type UserListProps = {
|
||||
socketUrl: string;
|
||||
session: Session;
|
||||
lobbyId: string;
|
||||
};
|
||||
|
||||
const UserList: React.FC<UserListProps> = (props: UserListProps) => {
|
||||
const { socketUrl, session } = props;
|
||||
const { socketUrl, session, lobbyId } = props;
|
||||
const [users, setUsers] = useState<User[] | null>(null);
|
||||
const [peers, setPeers] = useState<Record<string, Peer>>({});
|
||||
const [videoClass, setVideoClass] = useState<string>("Large");
|
||||
|
||||
// Bot configuration state
|
||||
const [botConfigDialogOpen, setBotConfigDialogOpen] = useState(false);
|
||||
const [selectedBotForConfig, setSelectedBotForConfig] = useState<User | null>(null);
|
||||
const [leavingBots, setLeavingBots] = useState<Set<string>>(new Set());
|
||||
|
||||
const apiClient = new ApiClient();
|
||||
@ -58,6 +70,17 @@ const UserList: React.FC<UserListProps> = (props: UserListProps) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Bot configuration handlers
|
||||
const handleOpenBotConfig = (user: User) => {
|
||||
setSelectedBotForConfig(user);
|
||||
setBotConfigDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleCloseBotConfig = () => {
|
||||
setBotConfigDialogOpen(false);
|
||||
setSelectedBotForConfig(null);
|
||||
};
|
||||
|
||||
const sortUsers = useCallback(
|
||||
(A: any, B: any) => {
|
||||
if (!session) {
|
||||
@ -155,16 +178,28 @@ const UserList: React.FC<UserListProps> = (props: UserListProps) => {
|
||||
)}
|
||||
</div>
|
||||
{user.is_bot && !user.local && (
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
onClick={() => handleBotLeave(user)}
|
||||
disabled={leavingBots.has(user.session_id)}
|
||||
style={{ fontSize: "0.7em", minWidth: "50px", height: "24px" }}
|
||||
>
|
||||
{leavingBots.has(user.session_id) ? "..." : "Leave"}
|
||||
</Button>
|
||||
<div style={{ display: "flex", gap: "4px" }}>
|
||||
{user.bot_run_id && (
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => handleOpenBotConfig(user)}
|
||||
style={{ width: "24px", height: "24px", fontSize: "0.7em" }}
|
||||
title="Configure bot"
|
||||
>
|
||||
<SettingsIcon style={{ fontSize: "14px" }} />
|
||||
</IconButton>
|
||||
)}
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
onClick={() => handleBotLeave(user)}
|
||||
disabled={leavingBots.has(user.session_id)}
|
||||
style={{ fontSize: "0.7em", minWidth: "50px", height: "24px" }}
|
||||
>
|
||||
{leavingBots.has(user.session_id) ? "..." : "Leave"}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{user.name && !user.live && <div className="NoNetwork"></div>}
|
||||
@ -198,6 +233,26 @@ const UserList: React.FC<UserListProps> = (props: UserListProps) => {
|
||||
</Box>
|
||||
))}
|
||||
</List>
|
||||
|
||||
{/* Bot Configuration Dialog */}
|
||||
<Dialog open={botConfigDialogOpen} onClose={handleCloseBotConfig} maxWidth="md" fullWidth>
|
||||
<DialogTitle>Configure Bot</DialogTitle>
|
||||
<DialogContent>
|
||||
{selectedBotForConfig && (
|
||||
<BotConfig
|
||||
botName={selectedBotForConfig.name || "unknown"}
|
||||
lobbyId={lobbyId}
|
||||
onConfigUpdate={(config) => {
|
||||
console.log("Bot configuration updated:", config);
|
||||
// Configuration updates are handled via WebSocket, so we don't need to do anything special here
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleCloseBotConfig}>Close</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
@ -40,6 +40,7 @@ services:
|
||||
- ./.env
|
||||
environment:
|
||||
- PRODUCTION=${PRODUCTION:-false}
|
||||
- PYTHONPATH=/:/server
|
||||
restart: always
|
||||
ports:
|
||||
- "8001:8000"
|
||||
|
@ -5,15 +5,9 @@ This module contains admin-only endpoints for managing users, sessions, and syst
|
||||
Extracted from main.py to improve maintainability and separation of concerns.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
# Add the parent directory of server to the path to access shared
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
server_dir = os.path.dirname(current_dir)
|
||||
project_root = os.path.dirname(server_dir)
|
||||
sys.path.insert(0, project_root)
|
||||
|
||||
from fastapi import APIRouter, Request, Response, Body
|
||||
from shared.models import (
|
||||
@ -44,8 +38,8 @@ class AdminAPI:
|
||||
session_manager: "SessionManager",
|
||||
lobby_manager: "LobbyManager",
|
||||
auth_manager: "AuthManager",
|
||||
admin_token: str = None,
|
||||
public_url: str = "/"
|
||||
admin_token: Optional[str] = None,
|
||||
public_url: str = "/",
|
||||
):
|
||||
self.session_manager = session_manager
|
||||
self.lobby_manager = lobby_manager
|
||||
@ -65,7 +59,7 @@ class AdminAPI:
|
||||
"""Register all admin routes"""
|
||||
|
||||
@self.router.get("/names", response_model=AdminNamesResponse)
|
||||
def list_names(request: Request):
|
||||
def list_names(request: Request): # type: ignore
|
||||
if not self._require_admin(request):
|
||||
return Response(status_code=403)
|
||||
|
||||
@ -73,7 +67,7 @@ class AdminAPI:
|
||||
return AdminNamesResponse(name_passwords=name_passwords_models)
|
||||
|
||||
@self.router.post("/set_password", response_model=AdminActionResponse)
|
||||
def set_password(request: Request, payload: AdminSetPassword = Body(...)):
|
||||
def set_password(request: Request, payload: AdminSetPassword = Body(...)): # type: ignore
|
||||
if not self._require_admin(request):
|
||||
return Response(status_code=403)
|
||||
|
||||
@ -82,7 +76,7 @@ class AdminAPI:
|
||||
return AdminActionResponse(status="ok", name=payload.name)
|
||||
|
||||
@self.router.post("/clear_password", response_model=AdminActionResponse)
|
||||
def clear_password(request: Request, payload: AdminClearPassword = Body(...)):
|
||||
def clear_password(request: Request, payload: AdminClearPassword = Body(...)): # type: ignore
|
||||
if not self._require_admin(request):
|
||||
return Response(status_code=403)
|
||||
|
||||
@ -92,7 +86,7 @@ class AdminAPI:
|
||||
return AdminActionResponse(status="not_found", name=payload.name)
|
||||
|
||||
@self.router.post("/cleanup_sessions", response_model=AdminActionResponse)
|
||||
def cleanup_sessions(request: Request):
|
||||
def cleanup_sessions(request: Request): # type: ignore
|
||||
if not self._require_admin(request):
|
||||
return Response(status_code=403)
|
||||
|
||||
@ -107,7 +101,7 @@ class AdminAPI:
|
||||
return AdminActionResponse(status="error", name=f"Error: {str(e)}")
|
||||
|
||||
@self.router.get("/session_metrics", response_model=AdminMetricsResponse)
|
||||
def session_metrics(request: Request):
|
||||
def session_metrics(request: Request): # type: ignore
|
||||
if not self._require_admin(request):
|
||||
return Response(status_code=403)
|
||||
|
||||
@ -118,7 +112,7 @@ class AdminAPI:
|
||||
return Response(status_code=500)
|
||||
|
||||
@self.router.get("/validate_sessions", response_model=AdminValidationResponse)
|
||||
def validate_sessions(request: Request):
|
||||
def validate_sessions(request: Request): # type: ignore
|
||||
if not self._require_admin(request):
|
||||
return Response(status_code=403)
|
||||
|
||||
@ -137,7 +131,7 @@ class AdminAPI:
|
||||
return AdminValidationResponse(status="error", error=str(e))
|
||||
|
||||
@self.router.post("/cleanup_lobbies", response_model=AdminActionResponse)
|
||||
def cleanup_lobbies(request: Request):
|
||||
def cleanup_lobbies(request: Request): # type: ignore
|
||||
if not self._require_admin(request):
|
||||
return Response(status_code=403)
|
||||
|
||||
|
@ -5,9 +5,10 @@ This module provides REST API endpoints for managing bot configurations
|
||||
including schema discovery, configuration CRUD operations, and real-time updates.
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Optional, Any
|
||||
from typing import Dict, Any
|
||||
from fastapi import APIRouter, HTTPException, BackgroundTasks, WebSocket
|
||||
|
||||
from core.bot_manager import BotManager
|
||||
from logger import logger
|
||||
from core.bot_config_manager import BotConfigManager
|
||||
|
||||
@ -28,67 +29,52 @@ try:
|
||||
)
|
||||
except ImportError:
|
||||
try:
|
||||
# Try direct import (when PYTHONPATH is set)
|
||||
from shared.models import (
|
||||
BotConfigSchema,
|
||||
BotLobbyConfig,
|
||||
BotConfigUpdateRequest,
|
||||
BotConfigUpdateResponse,
|
||||
BotConfigListResponse
|
||||
BotConfigListResponse,
|
||||
)
|
||||
except ImportError:
|
||||
# Create dummy models for standalone testing
|
||||
from pydantic import BaseModel
|
||||
|
||||
class BotConfigSchema(BaseModel):
|
||||
bot_name: str
|
||||
version: str = "1.0"
|
||||
parameters: List[Dict[str, Any]]
|
||||
|
||||
class BotLobbyConfig(BaseModel):
|
||||
bot_name: str
|
||||
lobby_id: str
|
||||
provider_id: str
|
||||
config_values: Dict[str, Any]
|
||||
created_at: float
|
||||
updated_at: float
|
||||
created_by: str
|
||||
|
||||
class BotConfigUpdateRequest(BaseModel):
|
||||
bot_name: str
|
||||
lobby_id: str
|
||||
config_values: Dict[str, Any]
|
||||
|
||||
class BotConfigUpdateResponse(BaseModel):
|
||||
success: bool
|
||||
message: str
|
||||
updated_config: Optional[BotLobbyConfig] = None
|
||||
|
||||
class BotConfigListResponse(BaseModel):
|
||||
lobby_id: str
|
||||
configs: List[BotLobbyConfig]
|
||||
# Log a warning for debugging (optional)
|
||||
import warnings
|
||||
|
||||
warnings.warn(
|
||||
"Relative import failed, ensure PYTHONPATH includes project root or run as package"
|
||||
)
|
||||
# Rely on environment setup or raise a clear error
|
||||
raise ImportError(
|
||||
"Cannot import shared.models. Ensure the project is run as a package or PYTHONPATH is set."
|
||||
)
|
||||
|
||||
|
||||
def create_bot_config_router(config_manager: BotConfigManager, bot_manager) -> APIRouter:
|
||||
def create_bot_config_router(
|
||||
config_manager: BotConfigManager, bot_manager: BotManager
|
||||
) -> APIRouter:
|
||||
"""Create FastAPI router for bot configuration endpoints"""
|
||||
|
||||
|
||||
router = APIRouter(prefix="/api/bots/config", tags=["Bot Configuration"])
|
||||
|
||||
|
||||
@router.get("/schema/{bot_name}")
|
||||
async def get_bot_config_schema(bot_name: str) -> BotConfigSchema:
|
||||
async def get_bot_config_schema(bot_name: str) -> BotConfigSchema: # type: ignore
|
||||
"""Get configuration schema for a specific bot"""
|
||||
try:
|
||||
# Check if we have cached schema
|
||||
schema = config_manager.get_bot_config_schema(bot_name)
|
||||
|
||||
|
||||
if not schema:
|
||||
# Try to discover schema from bot provider
|
||||
providers = bot_manager.get_providers()
|
||||
for provider_id, provider in providers.items():
|
||||
providers_response = bot_manager.list_providers()
|
||||
for provider in providers_response.providers:
|
||||
try:
|
||||
# Check if this provider has the bot
|
||||
provider_bots = await bot_manager.get_provider_bots(provider_id)
|
||||
provider_bots = await bot_manager.get_provider_bots(
|
||||
provider.provider_id
|
||||
)
|
||||
bot_names = [bot.name for bot in provider_bots.bots]
|
||||
|
||||
|
||||
if bot_name in bot_names:
|
||||
schema = await config_manager.discover_bot_config_schema(
|
||||
bot_name, provider.base_url
|
||||
@ -96,34 +82,61 @@ def create_bot_config_router(config_manager: BotConfigManager, bot_manager) -> A
|
||||
if schema:
|
||||
break
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to check provider {provider_id} for bot {bot_name}: {e}")
|
||||
logger.warning(
|
||||
f"Failed to check provider {provider.provider_id} for bot {bot_name}: {e}"
|
||||
)
|
||||
continue
|
||||
|
||||
else:
|
||||
# We have a cached schema, but check if it might be stale
|
||||
# Try to refresh it automatically if it's older than 1 hour
|
||||
providers_response = bot_manager.list_providers()
|
||||
for provider in providers_response.providers:
|
||||
try:
|
||||
provider_bots = await bot_manager.get_provider_bots(
|
||||
provider.provider_id
|
||||
)
|
||||
bot_names = [bot.name for bot in provider_bots.bots]
|
||||
|
||||
if bot_name in bot_names:
|
||||
# This will only refresh if the cached schema is older than 1 hour
|
||||
fresh_schema = (
|
||||
await config_manager.discover_bot_config_schema(
|
||||
bot_name, provider.base_url, force_refresh=False
|
||||
)
|
||||
)
|
||||
if fresh_schema:
|
||||
schema = fresh_schema
|
||||
break
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to refresh schema for {bot_name}: {e}")
|
||||
# Continue with cached schema if refresh fails
|
||||
continue
|
||||
|
||||
if not schema:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"No configuration schema found for bot '{bot_name}'"
|
||||
status_code=404,
|
||||
detail=f"No configuration schema found for bot '{bot_name}'",
|
||||
)
|
||||
|
||||
|
||||
return schema
|
||||
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get bot config schema for {bot_name}: {e}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
|
||||
@router.get("/lobby/{lobby_id}")
|
||||
async def get_lobby_bot_configs(lobby_id: str) -> BotConfigListResponse:
|
||||
"""Get all bot configurations for a lobby"""
|
||||
try:
|
||||
configs = config_manager.get_lobby_configs(lobby_id)
|
||||
return BotConfigListResponse(lobby_id=lobby_id, configs=configs)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get lobby configs for {lobby_id}: {e}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
|
||||
@router.get("/lobby/{lobby_id}/bot/{bot_name}")
|
||||
async def get_lobby_bot_config(lobby_id: str, bot_name: str) -> BotLobbyConfig:
|
||||
"""Get specific bot configuration for a lobby"""
|
||||
@ -132,37 +145,41 @@ def create_bot_config_router(config_manager: BotConfigManager, bot_manager) -> A
|
||||
if not config:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"No configuration found for bot '{bot_name}' in lobby '{lobby_id}'"
|
||||
detail=f"No configuration found for bot '{bot_name}' in lobby '{lobby_id}'",
|
||||
)
|
||||
|
||||
|
||||
return config
|
||||
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get config for bot {bot_name} in lobby {lobby_id}: {e}")
|
||||
logger.error(
|
||||
f"Failed to get config for bot {bot_name} in lobby {lobby_id}: {e}"
|
||||
)
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
|
||||
@router.post("/update")
|
||||
async def update_bot_config(
|
||||
request: BotConfigUpdateRequest,
|
||||
background_tasks: BackgroundTasks,
|
||||
session_id: str = "unknown" # TODO: Get from auth/session context
|
||||
session_id: str = "unknown", # TODO: Get from auth/session context
|
||||
) -> BotConfigUpdateResponse:
|
||||
"""Update bot configuration for a lobby"""
|
||||
try:
|
||||
# Find the provider for this bot
|
||||
provider_id = None
|
||||
provider_url = None
|
||||
|
||||
providers = bot_manager.get_providers()
|
||||
for pid, provider in providers.items():
|
||||
|
||||
providers_response = bot_manager.list_providers()
|
||||
for provider in providers_response.providers:
|
||||
try:
|
||||
provider_bots = await bot_manager.get_provider_bots(pid)
|
||||
provider_bots = await bot_manager.get_provider_bots(
|
||||
provider.provider_id
|
||||
)
|
||||
bot_names = [bot.name for bot in provider_bots.bots]
|
||||
|
||||
if request.bot_name in bot_names:
|
||||
provider_id = pid
|
||||
provider_id = provider.provider_id
|
||||
provider_url = provider.base_url
|
||||
break
|
||||
except Exception:
|
||||
@ -175,6 +192,12 @@ def create_bot_config_router(config_manager: BotConfigManager, bot_manager) -> A
|
||||
)
|
||||
|
||||
# Update configuration
|
||||
if not provider_id or not provider_url:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Bot {request.bot_name} not found in any registered provider",
|
||||
)
|
||||
|
||||
config = config_manager.set_bot_config(
|
||||
lobby_id=request.lobby_id,
|
||||
bot_name=request.bot_name,
|
||||
@ -260,16 +283,20 @@ def create_bot_config_router(config_manager: BotConfigManager, bot_manager) -> A
|
||||
try:
|
||||
async def refresh_task():
|
||||
refreshed = 0
|
||||
providers = bot_manager.get_providers()
|
||||
|
||||
for provider_id, provider in providers.items():
|
||||
providers_response = bot_manager.list_providers()
|
||||
|
||||
for provider in providers_response.providers:
|
||||
try:
|
||||
provider_bots = await bot_manager.get_provider_bots(provider_id)
|
||||
provider_bots = await bot_manager.get_provider_bots(
|
||||
provider.provider_id
|
||||
)
|
||||
|
||||
for bot in provider_bots.bots:
|
||||
try:
|
||||
schema = await config_manager.discover_bot_config_schema(
|
||||
bot.name, provider.base_url
|
||||
schema = (
|
||||
await config_manager.discover_bot_config_schema(
|
||||
bot.name, provider.base_url, force_refresh=True
|
||||
)
|
||||
)
|
||||
if schema:
|
||||
refreshed += 1
|
||||
@ -277,7 +304,9 @@ def create_bot_config_router(config_manager: BotConfigManager, bot_manager) -> A
|
||||
logger.warning(f"Failed to refresh schema for {bot.name}: {e}")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to refresh schemas from provider {provider_id}: {e}")
|
||||
logger.warning(
|
||||
f"Failed to refresh schemas from provider {provider.provider_id}: {e}"
|
||||
)
|
||||
|
||||
logger.info(f"Refreshed {refreshed} bot configuration schemas")
|
||||
|
||||
@ -291,6 +320,70 @@ def create_bot_config_router(config_manager: BotConfigManager, bot_manager) -> A
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to start schema refresh: {e}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
@router.post("/schema/{bot_name}/refresh")
|
||||
async def refresh_bot_schema(bot_name: str) -> Dict[str, Any]:
|
||||
"""Refresh configuration schema for a specific bot"""
|
||||
try:
|
||||
# Find the provider for this bot
|
||||
providers_response = bot_manager.list_providers()
|
||||
|
||||
for provider in providers_response.providers:
|
||||
try:
|
||||
provider_bots = await bot_manager.get_provider_bots(
|
||||
provider.provider_id
|
||||
)
|
||||
|
||||
for bot in provider_bots.bots:
|
||||
if bot.name == bot_name:
|
||||
schema = await config_manager.refresh_bot_schema(
|
||||
bot_name, provider.base_url
|
||||
)
|
||||
if schema:
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Schema refreshed for bot {bot_name}",
|
||||
"schema": schema.model_dump(),
|
||||
}
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Bot {bot_name} does not support configuration",
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Failed to check provider {provider.provider_id}: {e}"
|
||||
)
|
||||
continue
|
||||
|
||||
raise HTTPException(status_code=404, detail=f"Bot {bot_name} not found")
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to refresh schema for bot {bot_name}: {e}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
@router.delete("/schema/{bot_name}/cache")
|
||||
async def clear_bot_schema_cache(bot_name: str) -> Dict[str, Any]:
|
||||
"""Clear cached schema for a specific bot"""
|
||||
try:
|
||||
success = config_manager.clear_bot_schema_cache(bot_name)
|
||||
if success:
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Schema cache cleared for bot {bot_name}",
|
||||
}
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"No cached schema found for bot {bot_name}"
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to clear schema cache for bot {bot_name}: {e}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
return router
|
||||
|
||||
|
@ -8,7 +8,6 @@ Extracted from main.py to improve maintainability and separation of concerns.
|
||||
import hashlib
|
||||
import binascii
|
||||
import secrets
|
||||
import os
|
||||
import threading
|
||||
from typing import Dict, Optional, Tuple
|
||||
|
||||
@ -18,17 +17,19 @@ try:
|
||||
from ...shared.models import NamePasswordRecord
|
||||
except ImportError:
|
||||
try:
|
||||
# Try absolute import (when running directly)
|
||||
import sys
|
||||
import os
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
# Try direct import (when PYTHONPATH is set)
|
||||
from shared.models import NamePasswordRecord
|
||||
except ImportError:
|
||||
# Fallback: create minimal model for testing
|
||||
from pydantic import BaseModel
|
||||
class NamePasswordRecord(BaseModel):
|
||||
name: str
|
||||
password: str
|
||||
# Log a warning for debugging (optional)
|
||||
import warnings
|
||||
|
||||
warnings.warn(
|
||||
"Relative import failed, ensure PYTHONPATH includes project root or run as package"
|
||||
)
|
||||
# Rely on environment setup or raise a clear error
|
||||
raise ImportError(
|
||||
"Cannot import shared.models. Ensure the project is run as a package or PYTHONPATH is set."
|
||||
)
|
||||
|
||||
from logger import logger
|
||||
|
||||
@ -163,14 +164,10 @@ class AuthManager:
|
||||
|
||||
def validate_integrity(self) -> list[str]:
|
||||
"""Validate auth data integrity and return list of issues"""
|
||||
issues = []
|
||||
issues: list[str] = []
|
||||
|
||||
with self.lock:
|
||||
for name, record in self.name_passwords.items():
|
||||
if not isinstance(record, dict):
|
||||
issues.append(f"Name '{name}' has invalid record type: {type(record)}")
|
||||
continue
|
||||
|
||||
if "salt" not in record or "hash" not in record:
|
||||
issues.append(f"Name '{name}' missing salt or hash")
|
||||
continue
|
||||
|
@ -148,10 +148,25 @@ class BotConfigManager:
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to save bot schema {bot_name}: {e}")
|
||||
|
||||
async def discover_bot_config_schema(self, bot_name: str, provider_url: str) -> Optional[BotConfigSchema]:
|
||||
|
||||
async def discover_bot_config_schema(
|
||||
self, bot_name: str, provider_url: str, force_refresh: bool = False
|
||||
) -> Optional[BotConfigSchema]:
|
||||
"""Discover configuration schema from bot provider"""
|
||||
try:
|
||||
# Check if we have a cached schema and it's not forced refresh
|
||||
if not force_refresh and bot_name in self.schema_cache:
|
||||
cached_schema = self.schema_cache[bot_name]
|
||||
# Check if schema is less than 1 hour old
|
||||
schema_file = self._get_schema_file(bot_name)
|
||||
if schema_file.exists():
|
||||
file_age = time.time() - schema_file.stat().st_mtime
|
||||
if file_age < 3600: # 1 hour
|
||||
logger.debug(
|
||||
f"Using cached schema for bot {bot_name} (age: {file_age:.0f}s)"
|
||||
)
|
||||
return cached_schema
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
# Try to get configuration schema from bot provider
|
||||
response = await client.get(
|
||||
@ -162,24 +177,62 @@ class BotConfigManager:
|
||||
if response.status_code == 200:
|
||||
schema_data = response.json()
|
||||
schema = BotConfigSchema(**schema_data)
|
||||
|
||||
|
||||
# Check if schema has actually changed
|
||||
if bot_name in self.schema_cache:
|
||||
old_schema = self.schema_cache[bot_name]
|
||||
if old_schema.model_dump() == schema.model_dump():
|
||||
logger.debug(
|
||||
f"Schema for bot {bot_name} unchanged, updating timestamp only"
|
||||
)
|
||||
else:
|
||||
logger.info(f"Schema for bot {bot_name} has been updated")
|
||||
|
||||
# Cache the schema
|
||||
self.schema_cache[bot_name] = schema
|
||||
self._save_bot_schema(bot_name)
|
||||
|
||||
logger.info(f"Discovered config schema for bot {bot_name}")
|
||||
|
||||
logger.info(
|
||||
f"Discovered/refreshed config schema for bot {bot_name}"
|
||||
)
|
||||
return schema
|
||||
else:
|
||||
logger.warning(f"Bot {bot_name} does not support configuration (HTTP {response.status_code})")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to discover config schema for bot {bot_name}: {e}")
|
||||
# Return cached schema if available, even if refresh failed
|
||||
if bot_name in self.schema_cache:
|
||||
logger.info(
|
||||
f"Returning cached schema for bot {bot_name} after refresh failure"
|
||||
)
|
||||
return self.schema_cache[bot_name]
|
||||
|
||||
return None
|
||||
|
||||
def get_bot_config_schema(self, bot_name: str) -> Optional[BotConfigSchema]:
|
||||
"""Get cached configuration schema for a bot"""
|
||||
return self.schema_cache.get(bot_name)
|
||||
|
||||
async def refresh_bot_schema(
|
||||
self, bot_name: str, provider_url: str
|
||||
) -> Optional[BotConfigSchema]:
|
||||
"""Force refresh of bot schema from provider"""
|
||||
return await self.discover_bot_config_schema(
|
||||
bot_name, provider_url, force_refresh=True
|
||||
)
|
||||
|
||||
def clear_bot_schema_cache(self, bot_name: str) -> bool:
|
||||
"""Clear cached schema for a specific bot"""
|
||||
if bot_name in self.schema_cache:
|
||||
del self.schema_cache[bot_name]
|
||||
# Also remove the cached file
|
||||
schema_file = self._get_schema_file(bot_name)
|
||||
if schema_file.exists():
|
||||
schema_file.unlink()
|
||||
logger.info(f"Cleared schema cache for bot {bot_name}")
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_lobby_bot_config(self, lobby_id: str, bot_name: str) -> Optional[BotLobbyConfig]:
|
||||
"""Get bot configuration for a specific lobby"""
|
||||
|
@ -28,6 +28,7 @@ try:
|
||||
)
|
||||
except ImportError:
|
||||
try:
|
||||
# Try direct import (when PYTHONPATH is set)
|
||||
from shared.models import (
|
||||
BotProviderModel,
|
||||
BotProviderRegisterRequest,
|
||||
@ -44,70 +45,16 @@ except ImportError:
|
||||
BotJoinPayload,
|
||||
)
|
||||
except ImportError:
|
||||
# Create dummy models for standalone testing
|
||||
from pydantic import BaseModel
|
||||
|
||||
class BotProviderModel(BaseModel):
|
||||
provider_id: str
|
||||
base_url: str
|
||||
name: str
|
||||
description: str
|
||||
provider_key: str
|
||||
registered_at: float
|
||||
last_seen: float
|
||||
|
||||
class BotProviderRegisterRequest(BaseModel):
|
||||
base_url: str
|
||||
name: str
|
||||
description: str
|
||||
provider_key: str
|
||||
|
||||
class BotProviderRegisterResponse(BaseModel):
|
||||
provider_id: str
|
||||
|
||||
class BotProviderListResponse(BaseModel):
|
||||
providers: List[BotProviderModel]
|
||||
|
||||
class BotInfoModel(BaseModel):
|
||||
name: str
|
||||
description: str
|
||||
has_media: bool = False
|
||||
|
||||
class BotListResponse(BaseModel):
|
||||
bots: List[BotInfoModel]
|
||||
providers: Dict[str, str]
|
||||
|
||||
class BotJoinLobbyRequest(BaseModel):
|
||||
lobby_id: str
|
||||
provider_id: Optional[str] = None
|
||||
nick: Optional[str] = None
|
||||
|
||||
class BotJoinLobbyResponse(BaseModel):
|
||||
status: str
|
||||
bot_name: str
|
||||
run_id: str
|
||||
provider_id: str
|
||||
|
||||
class BotLeaveLobbyRequest(BaseModel):
|
||||
session_id: str
|
||||
|
||||
class BotLeaveLobbyResponse(BaseModel):
|
||||
status: str
|
||||
session_id: str
|
||||
run_id: Optional[str] = None
|
||||
|
||||
class BotProviderBotsResponse(BaseModel):
|
||||
bots: List[BotInfoModel]
|
||||
|
||||
class BotProviderJoinResponse(BaseModel):
|
||||
run_id: str
|
||||
|
||||
class BotJoinPayload(BaseModel):
|
||||
lobby_id: str
|
||||
session_id: str
|
||||
nick: str
|
||||
server_url: str
|
||||
insecure: bool = True
|
||||
# Log a warning for debugging (optional)
|
||||
import warnings
|
||||
|
||||
warnings.warn(
|
||||
"Relative import failed, ensure PYTHONPATH includes project root or run as package"
|
||||
)
|
||||
# Rely on environment setup or raise a clear error
|
||||
raise ImportError(
|
||||
"Cannot import shared.models. Ensure the project is run as a package or PYTHONPATH is set."
|
||||
)
|
||||
|
||||
|
||||
class BotProviderConfig:
|
||||
@ -234,6 +181,26 @@ class BotManager:
|
||||
continue
|
||||
|
||||
return BotListResponse(bots=bots, providers=providers)
|
||||
|
||||
async def get_provider_bots(self, provider_id: str) -> BotProviderBotsResponse:
|
||||
"""Get bots from a specific provider"""
|
||||
provider = self.get_provider(provider_id)
|
||||
if not provider:
|
||||
raise ValueError(f"Provider {provider_id} not found")
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(f"{provider.base_url}/bots", timeout=5.0)
|
||||
if response.status_code == 200:
|
||||
return BotProviderBotsResponse.model_validate(response.json())
|
||||
else:
|
||||
logger.warning(
|
||||
f"Failed to fetch bots from provider {provider.name}: HTTP {response.status_code}"
|
||||
)
|
||||
return BotProviderBotsResponse(bots=[])
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching bots from provider {provider.name}: {e}")
|
||||
return BotProviderBotsResponse(bots=[])
|
||||
|
||||
async def request_bot_join(self, bot_name: str, request: BotJoinLobbyRequest, session_manager, lobby_manager) -> BotJoinLobbyResponse:
|
||||
"""Request a bot to join a specific lobby"""
|
||||
|
@ -250,16 +250,18 @@ class SessionManager:
|
||||
"""Create a new session with given or generated ID"""
|
||||
if not session_id:
|
||||
session_id = secrets.token_hex(16)
|
||||
|
||||
# Check if session already exists
|
||||
existing_session = self.get_session(session_id)
|
||||
if existing_session:
|
||||
logger.debug(f"Session {session_id[:8]} already exists, returning existing session")
|
||||
return existing_session
|
||||
|
||||
session = Session(session_id, is_bot=is_bot, has_media=has_media)
|
||||
|
||||
|
||||
with self.lock:
|
||||
# Check if session already exists (now inside the lock for atomicity)
|
||||
existing_session = self.get_session(session_id)
|
||||
if existing_session:
|
||||
logger.debug(
|
||||
f"Session {session_id[:8]} already exists, returning existing session"
|
||||
)
|
||||
return existing_session
|
||||
|
||||
# Create new session
|
||||
session = Session(session_id, is_bot=is_bot, has_media=has_media)
|
||||
self._instances.append(session)
|
||||
|
||||
self.save()
|
||||
@ -276,12 +278,11 @@ class SessionManager:
|
||||
|
||||
def get_session(self, session_id: str) -> Optional[Session]:
|
||||
"""Get session by ID"""
|
||||
if not self._loaded:
|
||||
self.load()
|
||||
logger.info(f"Loaded {len(self._instances)} sessions from disk...")
|
||||
self._loaded = True
|
||||
|
||||
with self.lock:
|
||||
if not self._loaded:
|
||||
self.load()
|
||||
logger.info(f"Loaded {len(self._instances)} sessions from disk...")
|
||||
|
||||
for s in self._instances:
|
||||
if s.id == session_id:
|
||||
return s
|
||||
@ -402,6 +403,19 @@ class SessionManager:
|
||||
logger.info(f"Expiring session {s_saved.id[:8]}:{name} during load")
|
||||
continue
|
||||
|
||||
# Check if session already exists in _instances (deduplication)
|
||||
existing_session = None
|
||||
for existing in self._instances:
|
||||
if existing.id == s_saved.id:
|
||||
existing_session = existing
|
||||
break
|
||||
|
||||
if existing_session:
|
||||
logger.debug(
|
||||
f"Session {s_saved.id[:8]} already loaded, skipping duplicate"
|
||||
)
|
||||
continue
|
||||
|
||||
session = Session(
|
||||
s_saved.id,
|
||||
is_bot=getattr(s_saved, "is_bot", False),
|
||||
@ -426,6 +440,9 @@ class SessionManager:
|
||||
logger.info(f"Expired {sessions_expired} old sessions during load")
|
||||
self.save()
|
||||
|
||||
# Mark as loaded to prevent duplicate loads
|
||||
self._loaded = True
|
||||
|
||||
@staticmethod
|
||||
def _should_remove_session_static(
|
||||
name: str,
|
||||
|
@ -17,6 +17,7 @@ fi
|
||||
|
||||
export VIRTUAL_ENV=/server/.venv
|
||||
export PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||
export PYTHONPATH="/:/server"
|
||||
|
||||
if [ -f "${SSL_CERTFILE}" ] && [ -f "${SSL_KEYFILE}" ]; then
|
||||
echo "Starting server with SSL..."
|
||||
|
@ -452,18 +452,9 @@ def get_config_schema() -> Dict[str, Any]:
|
||||
}
|
||||
],
|
||||
"categories": [
|
||||
{
|
||||
"name": "AI Settings",
|
||||
"parameters": ["personality", "ai_provider", "streaming"]
|
||||
},
|
||||
{
|
||||
"name": "Behavior Settings",
|
||||
"parameters": ["memory_enabled", "response_length", "creativity_level"]
|
||||
},
|
||||
{
|
||||
"name": "Communication Style",
|
||||
"parameters": ["response_style"]
|
||||
}
|
||||
{"AI Settings": ["personality", "ai_provider", "streaming"]},
|
||||
{"Behavior Settings": ["memory_enabled", "response_length", "creativity_level"]},
|
||||
{"Communication Style": ["response_style"]}
|
||||
]
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user