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" }}>
|
<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 && (
|
{session && socketUrl && lobby && (
|
||||||
<LobbyChat socketUrl={socketUrl} session={session} lobbyId={lobby.id} />
|
<LobbyChat socketUrl={socketUrl} session={session} lobbyId={lobby.id} />
|
||||||
)}
|
)}
|
||||||
|
@ -15,8 +15,15 @@
|
|||||||
border-bottom: 1px solid #dee2e6;
|
border-bottom: 1px solid #dee2e6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bot-config-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.bot-config-header h3 {
|
.bot-config-header h3 {
|
||||||
margin: 0 0 8px 0;
|
margin: 0;
|
||||||
color: #212529;
|
color: #212529;
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
}
|
}
|
||||||
@ -172,6 +179,30 @@
|
|||||||
cursor: not-allowed;
|
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-loading,
|
||||||
.bot-config-error,
|
.bot-config-error,
|
||||||
.bot-config-unavailable {
|
.bot-config-unavailable {
|
||||||
|
@ -7,11 +7,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import './BotConfig.css';
|
import { base } from "./Common";
|
||||||
|
import "./BotConfig.css";
|
||||||
|
|
||||||
interface ConfigParameter {
|
interface ConfigParameter {
|
||||||
name: string;
|
name: string;
|
||||||
type: 'string' | 'number' | 'boolean' | 'select' | 'range';
|
type: "string" | "number" | "boolean" | "select" | "range";
|
||||||
label: string;
|
label: string;
|
||||||
description: string;
|
description: string;
|
||||||
default_value?: any;
|
default_value?: any;
|
||||||
@ -47,57 +48,73 @@ interface BotConfigProps {
|
|||||||
onConfigUpdate?: (config: BotConfig) => void;
|
onConfigUpdate?: (config: BotConfig) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BotConfigComponent: React.FC<BotConfigProps> = ({
|
const BotConfigComponent: React.FC<BotConfigProps> = ({ botName, lobbyId, onConfigUpdate }) => {
|
||||||
botName,
|
|
||||||
lobbyId,
|
|
||||||
onConfigUpdate
|
|
||||||
}) => {
|
|
||||||
const [schema, setSchema] = useState<ConfigSchema | null>(null);
|
const [schema, setSchema] = useState<ConfigSchema | null>(null);
|
||||||
const [currentConfig, setCurrentConfig] = useState<BotConfig | null>(null);
|
const [currentConfig, setCurrentConfig] = useState<BotConfig | null>(null);
|
||||||
const [configValues, setConfigValues] = useState<{ [key: string]: any }>({});
|
const [configValues, setConfigValues] = useState<{ [key: string]: any }>({});
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
|
|
||||||
// Fetch configuration schema
|
// Fetch configuration schema
|
||||||
useEffect(() => {
|
const fetchSchema = async (forceRefresh = false) => {
|
||||||
const fetchSchema = async () => {
|
try {
|
||||||
try {
|
setLoading(true);
|
||||||
setLoading(true);
|
setError(null);
|
||||||
const response = await fetch(`/api/bots/config/schema/${botName}`);
|
|
||||||
|
|
||||||
if (response.ok) {
|
// Use refresh endpoint if force refresh is requested
|
||||||
const schemaData = await response.json();
|
const url = forceRefresh
|
||||||
setSchema(schemaData);
|
? `${base}/api/bots/config/schema/${botName}/refresh`
|
||||||
|
: `${base}/api/bots/config/schema/${botName}`;
|
||||||
|
|
||||||
// Initialize config values with defaults
|
const method = forceRefresh ? "POST" : "GET";
|
||||||
const defaultValues: { [key: string]: any } = {};
|
|
||||||
schemaData.parameters.forEach((param: ConfigParameter) => {
|
const response = await fetch(url, { method });
|
||||||
if (param.default_value !== undefined) {
|
|
||||||
defaultValues[param.name] = param.default_value;
|
if (response.ok) {
|
||||||
}
|
const responseData = await response.json();
|
||||||
});
|
// For refresh endpoint, the schema is in the 'schema' field
|
||||||
setConfigValues(defaultValues);
|
const schemaData = forceRefresh ? responseData.schema : responseData;
|
||||||
} else if (response.status === 404) {
|
|
||||||
setError(`Bot "${botName}" does not support configuration`);
|
setSchema(schemaData);
|
||||||
} else {
|
|
||||||
setError('Failed to fetch configuration schema');
|
// Initialize config values with defaults
|
||||||
}
|
const defaultValues: { [key: string]: any } = {};
|
||||||
} catch (err) {
|
schemaData.parameters.forEach((param: ConfigParameter) => {
|
||||||
setError('Network error while fetching configuration schema');
|
if (param.default_value !== undefined) {
|
||||||
} finally {
|
defaultValues[param.name] = param.default_value;
|
||||||
setLoading(false);
|
}
|
||||||
|
});
|
||||||
|
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();
|
fetchSchema();
|
||||||
}, [botName]);
|
}, [botName]);
|
||||||
|
|
||||||
|
// Handle schema refresh
|
||||||
|
const handleRefreshSchema = async () => {
|
||||||
|
setRefreshing(true);
|
||||||
|
await fetchSchema(true);
|
||||||
|
setRefreshing(false);
|
||||||
|
};
|
||||||
|
|
||||||
// Fetch current configuration
|
// Fetch current configuration
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchCurrentConfig = async () => {
|
const fetchCurrentConfig = async () => {
|
||||||
try {
|
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) {
|
if (response.ok) {
|
||||||
const config = await response.json();
|
const config = await response.json();
|
||||||
@ -106,7 +123,7 @@ const BotConfigComponent: React.FC<BotConfigProps> = ({
|
|||||||
}
|
}
|
||||||
// If 404, no existing config - that's fine
|
// If 404, no existing config - that's fine
|
||||||
} catch (err) {
|
} 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]);
|
}, [botName, lobbyId, schema]);
|
||||||
|
|
||||||
const handleValueChange = (paramName: string, value: any) => {
|
const handleValueChange = (paramName: string, value: any) => {
|
||||||
setConfigValues(prev => ({
|
setConfigValues((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[paramName]: value
|
[paramName]: value,
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -127,15 +144,15 @@ const BotConfigComponent: React.FC<BotConfigProps> = ({
|
|||||||
setSaving(true);
|
setSaving(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
const response = await fetch('/api/bots/config/update', {
|
const response = await fetch(`${base}/api/bots/config/update`, {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
bot_name: botName,
|
bot_name: botName,
|
||||||
lobby_id: lobbyId,
|
lobby_id: lobbyId,
|
||||||
config_values: configValues
|
config_values: configValues,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -147,10 +164,10 @@ const BotConfigComponent: React.FC<BotConfigProps> = ({
|
|||||||
onConfigUpdate(result.updated_config);
|
onConfigUpdate(result.updated_config);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setError(result.message || 'Failed to save configuration');
|
setError(result.message || "Failed to save configuration");
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError('Network error while saving configuration');
|
setError("Network error while saving configuration");
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
@ -160,7 +177,7 @@ const BotConfigComponent: React.FC<BotConfigProps> = ({
|
|||||||
const value = configValues[param.name];
|
const value = configValues[param.name];
|
||||||
|
|
||||||
switch (param.type) {
|
switch (param.type) {
|
||||||
case 'boolean':
|
case "boolean":
|
||||||
return (
|
return (
|
||||||
<div key={param.name} className="config-parameter">
|
<div key={param.name} className="config-parameter">
|
||||||
<label className="config-label">
|
<label className="config-label">
|
||||||
@ -175,16 +192,16 @@ const BotConfigComponent: React.FC<BotConfigProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
case 'select':
|
case "select":
|
||||||
return (
|
return (
|
||||||
<div key={param.name} className="config-parameter">
|
<div key={param.name} className="config-parameter">
|
||||||
<label className="config-label">{param.label}</label>
|
<label className="config-label">{param.label}</label>
|
||||||
<select
|
<select
|
||||||
value={value || ''}
|
value={value || ""}
|
||||||
onChange={(e) => handleValueChange(param.name, e.target.value)}
|
onChange={(e) => handleValueChange(param.name, e.target.value)}
|
||||||
className="config-select"
|
className="config-select"
|
||||||
>
|
>
|
||||||
{param.options?.map(option => (
|
{param.options?.map((option) => (
|
||||||
<option key={option.value} value={option.value}>
|
<option key={option.value} value={option.value}>
|
||||||
{option.label}
|
{option.label}
|
||||||
</option>
|
</option>
|
||||||
@ -194,7 +211,7 @@ const BotConfigComponent: React.FC<BotConfigProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
case 'range':
|
case "range":
|
||||||
return (
|
return (
|
||||||
<div key={param.name} className="config-parameter">
|
<div key={param.name} className="config-parameter">
|
||||||
<label className="config-label">{param.label}</label>
|
<label className="config-label">{param.label}</label>
|
||||||
@ -214,7 +231,7 @@ const BotConfigComponent: React.FC<BotConfigProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
case 'number':
|
case "number":
|
||||||
return (
|
return (
|
||||||
<div key={param.name} className="config-parameter">
|
<div key={param.name} className="config-parameter">
|
||||||
<label className="config-label">{param.label}</label>
|
<label className="config-label">{param.label}</label>
|
||||||
@ -223,7 +240,7 @@ const BotConfigComponent: React.FC<BotConfigProps> = ({
|
|||||||
min={param.min_value}
|
min={param.min_value}
|
||||||
max={param.max_value}
|
max={param.max_value}
|
||||||
step={param.step || 1}
|
step={param.step || 1}
|
||||||
value={value || ''}
|
value={value || ""}
|
||||||
onChange={(e) => handleValueChange(param.name, Number(e.target.value))}
|
onChange={(e) => handleValueChange(param.name, Number(e.target.value))}
|
||||||
className="config-input"
|
className="config-input"
|
||||||
/>
|
/>
|
||||||
@ -231,7 +248,7 @@ const BotConfigComponent: React.FC<BotConfigProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
case 'string':
|
case "string":
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
<div key={param.name} className="config-parameter">
|
<div key={param.name} className="config-parameter">
|
||||||
@ -240,7 +257,7 @@ const BotConfigComponent: React.FC<BotConfigProps> = ({
|
|||||||
type="text"
|
type="text"
|
||||||
maxLength={param.max_length}
|
maxLength={param.max_length}
|
||||||
pattern={param.pattern}
|
pattern={param.pattern}
|
||||||
value={value || ''}
|
value={value || ""}
|
||||||
onChange={(e) => handleValueChange(param.name, e.target.value)}
|
onChange={(e) => handleValueChange(param.name, e.target.value)}
|
||||||
className="config-input"
|
className="config-input"
|
||||||
/>
|
/>
|
||||||
@ -254,12 +271,12 @@ const BotConfigComponent: React.FC<BotConfigProps> = ({
|
|||||||
if (!schema) return null;
|
if (!schema) return null;
|
||||||
|
|
||||||
if (schema.categories && schema.categories.length > 0) {
|
if (schema.categories && schema.categories.length > 0) {
|
||||||
return schema.categories.map(category => (
|
return schema.categories.map((category) => (
|
||||||
<div key={category.name} className="config-category">
|
<div key={category.name} className="config-category">
|
||||||
<h4 className="category-title">{category.name}</h4>
|
<h4 className="category-title">{category.name}</h4>
|
||||||
<div className="category-parameters">
|
<div className="category-parameters">
|
||||||
{category.parameters.map(paramName => {
|
{category.parameters.map((paramName) => {
|
||||||
const param = schema.parameters.find(p => p.name === paramName);
|
const param = schema.parameters.find((p) => p.name === paramName);
|
||||||
return param ? renderParameter(param) : null;
|
return param ? renderParameter(param) : null;
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
@ -285,26 +302,28 @@ const BotConfigComponent: React.FC<BotConfigProps> = ({
|
|||||||
return (
|
return (
|
||||||
<div className="bot-config-container">
|
<div className="bot-config-container">
|
||||||
<div className="bot-config-header">
|
<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>
|
<p>Lobby: {lobbyId}</p>
|
||||||
{currentConfig && (
|
{currentConfig && (
|
||||||
<p className="config-meta">
|
<p className="config-meta">Last updated: {new Date(currentConfig.updated_at * 1000).toLocaleString()}</p>
|
||||||
Last updated: {new Date(currentConfig.updated_at * 1000).toLocaleString()}
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bot-config-form">
|
<div className="bot-config-form">{renderParametersByCategory()}</div>
|
||||||
{renderParametersByCategory()}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bot-config-actions">
|
<div className="bot-config-actions">
|
||||||
<button
|
<button onClick={handleSave} disabled={saving} className="config-save-button">
|
||||||
onClick={handleSave}
|
{saving ? "Saving..." : "Save Configuration"}
|
||||||
disabled={saving}
|
|
||||||
className="config-save-button"
|
|
||||||
>
|
|
||||||
{saving ? 'Saving...' : 'Save Configuration'}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,12 +2,19 @@ import React, { useState, useEffect, useCallback } from "react";
|
|||||||
import Paper from "@mui/material/Paper";
|
import Paper from "@mui/material/Paper";
|
||||||
import List from "@mui/material/List";
|
import List from "@mui/material/List";
|
||||||
import Button from "@mui/material/Button";
|
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 "./UserList.css";
|
||||||
import { MediaControl, MediaAgent, Peer } from "./MediaControl";
|
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, BotLeaveLobbyRequest } from "./api-client";
|
||||||
|
import BotConfig from "./BotConfig";
|
||||||
|
|
||||||
type User = {
|
type User = {
|
||||||
name: string;
|
name: string;
|
||||||
@ -24,13 +31,18 @@ type User = {
|
|||||||
type UserListProps = {
|
type UserListProps = {
|
||||||
socketUrl: string;
|
socketUrl: string;
|
||||||
session: Session;
|
session: Session;
|
||||||
|
lobbyId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const UserList: React.FC<UserListProps> = (props: UserListProps) => {
|
const UserList: React.FC<UserListProps> = (props: UserListProps) => {
|
||||||
const { socketUrl, session } = props;
|
const { socketUrl, session, lobbyId } = props;
|
||||||
const [users, setUsers] = useState<User[] | null>(null);
|
const [users, setUsers] = useState<User[] | null>(null);
|
||||||
const [peers, setPeers] = useState<Record<string, Peer>>({});
|
const [peers, setPeers] = useState<Record<string, Peer>>({});
|
||||||
const [videoClass, setVideoClass] = useState<string>("Large");
|
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 [leavingBots, setLeavingBots] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
const apiClient = new ApiClient();
|
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(
|
const sortUsers = useCallback(
|
||||||
(A: any, B: any) => {
|
(A: any, B: any) => {
|
||||||
if (!session) {
|
if (!session) {
|
||||||
@ -155,16 +178,28 @@ const UserList: React.FC<UserListProps> = (props: UserListProps) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{user.is_bot && !user.local && (
|
{user.is_bot && !user.local && (
|
||||||
<Button
|
<div style={{ display: "flex", gap: "4px" }}>
|
||||||
size="small"
|
{user.bot_run_id && (
|
||||||
variant="outlined"
|
<IconButton
|
||||||
color="secondary"
|
size="small"
|
||||||
onClick={() => handleBotLeave(user)}
|
onClick={() => handleOpenBotConfig(user)}
|
||||||
disabled={leavingBots.has(user.session_id)}
|
style={{ width: "24px", height: "24px", fontSize: "0.7em" }}
|
||||||
style={{ fontSize: "0.7em", minWidth: "50px", height: "24px" }}
|
title="Configure bot"
|
||||||
>
|
>
|
||||||
{leavingBots.has(user.session_id) ? "..." : "Leave"}
|
<SettingsIcon style={{ fontSize: "14px" }} />
|
||||||
</Button>
|
</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>
|
</div>
|
||||||
{user.name && !user.live && <div className="NoNetwork"></div>}
|
{user.name && !user.live && <div className="NoNetwork"></div>}
|
||||||
@ -198,6 +233,26 @@ const UserList: React.FC<UserListProps> = (props: UserListProps) => {
|
|||||||
</Box>
|
</Box>
|
||||||
))}
|
))}
|
||||||
</List>
|
</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>
|
</Paper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -40,6 +40,7 @@ services:
|
|||||||
- ./.env
|
- ./.env
|
||||||
environment:
|
environment:
|
||||||
- PRODUCTION=${PRODUCTION:-false}
|
- PRODUCTION=${PRODUCTION:-false}
|
||||||
|
- PYTHONPATH=/:/server
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
- "8001:8000"
|
- "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.
|
Extracted from main.py to improve maintainability and separation of concerns.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
from typing import TYPE_CHECKING, Optional
|
||||||
import os
|
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
# Add the parent directory of server to the path to access shared
|
# 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 fastapi import APIRouter, Request, Response, Body
|
||||||
from shared.models import (
|
from shared.models import (
|
||||||
@ -44,8 +38,8 @@ class AdminAPI:
|
|||||||
session_manager: "SessionManager",
|
session_manager: "SessionManager",
|
||||||
lobby_manager: "LobbyManager",
|
lobby_manager: "LobbyManager",
|
||||||
auth_manager: "AuthManager",
|
auth_manager: "AuthManager",
|
||||||
admin_token: str = None,
|
admin_token: Optional[str] = None,
|
||||||
public_url: str = "/"
|
public_url: str = "/",
|
||||||
):
|
):
|
||||||
self.session_manager = session_manager
|
self.session_manager = session_manager
|
||||||
self.lobby_manager = lobby_manager
|
self.lobby_manager = lobby_manager
|
||||||
@ -65,7 +59,7 @@ class AdminAPI:
|
|||||||
"""Register all admin routes"""
|
"""Register all admin routes"""
|
||||||
|
|
||||||
@self.router.get("/names", response_model=AdminNamesResponse)
|
@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):
|
if not self._require_admin(request):
|
||||||
return Response(status_code=403)
|
return Response(status_code=403)
|
||||||
|
|
||||||
@ -73,7 +67,7 @@ class AdminAPI:
|
|||||||
return AdminNamesResponse(name_passwords=name_passwords_models)
|
return AdminNamesResponse(name_passwords=name_passwords_models)
|
||||||
|
|
||||||
@self.router.post("/set_password", response_model=AdminActionResponse)
|
@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):
|
if not self._require_admin(request):
|
||||||
return Response(status_code=403)
|
return Response(status_code=403)
|
||||||
|
|
||||||
@ -82,7 +76,7 @@ class AdminAPI:
|
|||||||
return AdminActionResponse(status="ok", name=payload.name)
|
return AdminActionResponse(status="ok", name=payload.name)
|
||||||
|
|
||||||
@self.router.post("/clear_password", response_model=AdminActionResponse)
|
@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):
|
if not self._require_admin(request):
|
||||||
return Response(status_code=403)
|
return Response(status_code=403)
|
||||||
|
|
||||||
@ -92,7 +86,7 @@ class AdminAPI:
|
|||||||
return AdminActionResponse(status="not_found", name=payload.name)
|
return AdminActionResponse(status="not_found", name=payload.name)
|
||||||
|
|
||||||
@self.router.post("/cleanup_sessions", response_model=AdminActionResponse)
|
@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):
|
if not self._require_admin(request):
|
||||||
return Response(status_code=403)
|
return Response(status_code=403)
|
||||||
|
|
||||||
@ -107,7 +101,7 @@ class AdminAPI:
|
|||||||
return AdminActionResponse(status="error", name=f"Error: {str(e)}")
|
return AdminActionResponse(status="error", name=f"Error: {str(e)}")
|
||||||
|
|
||||||
@self.router.get("/session_metrics", response_model=AdminMetricsResponse)
|
@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):
|
if not self._require_admin(request):
|
||||||
return Response(status_code=403)
|
return Response(status_code=403)
|
||||||
|
|
||||||
@ -118,7 +112,7 @@ class AdminAPI:
|
|||||||
return Response(status_code=500)
|
return Response(status_code=500)
|
||||||
|
|
||||||
@self.router.get("/validate_sessions", response_model=AdminValidationResponse)
|
@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):
|
if not self._require_admin(request):
|
||||||
return Response(status_code=403)
|
return Response(status_code=403)
|
||||||
|
|
||||||
@ -137,7 +131,7 @@ class AdminAPI:
|
|||||||
return AdminValidationResponse(status="error", error=str(e))
|
return AdminValidationResponse(status="error", error=str(e))
|
||||||
|
|
||||||
@self.router.post("/cleanup_lobbies", response_model=AdminActionResponse)
|
@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):
|
if not self._require_admin(request):
|
||||||
return Response(status_code=403)
|
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.
|
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 fastapi import APIRouter, HTTPException, BackgroundTasks, WebSocket
|
||||||
|
|
||||||
|
from core.bot_manager import BotManager
|
||||||
from logger import logger
|
from logger import logger
|
||||||
from core.bot_config_manager import BotConfigManager
|
from core.bot_config_manager import BotConfigManager
|
||||||
|
|
||||||
@ -28,53 +29,36 @@ try:
|
|||||||
)
|
)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
try:
|
try:
|
||||||
|
# Try direct import (when PYTHONPATH is set)
|
||||||
from shared.models import (
|
from shared.models import (
|
||||||
BotConfigSchema,
|
BotConfigSchema,
|
||||||
BotLobbyConfig,
|
BotLobbyConfig,
|
||||||
BotConfigUpdateRequest,
|
BotConfigUpdateRequest,
|
||||||
BotConfigUpdateResponse,
|
BotConfigUpdateResponse,
|
||||||
BotConfigListResponse
|
BotConfigListResponse,
|
||||||
)
|
)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
# Create dummy models for standalone testing
|
# Log a warning for debugging (optional)
|
||||||
from pydantic import BaseModel
|
import warnings
|
||||||
|
|
||||||
class BotConfigSchema(BaseModel):
|
warnings.warn(
|
||||||
bot_name: str
|
"Relative import failed, ensure PYTHONPATH includes project root or run as package"
|
||||||
version: str = "1.0"
|
)
|
||||||
parameters: List[Dict[str, Any]]
|
# Rely on environment setup or raise a clear error
|
||||||
|
raise ImportError(
|
||||||
class BotLobbyConfig(BaseModel):
|
"Cannot import shared.models. Ensure the project is run as a package or PYTHONPATH is set."
|
||||||
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]
|
|
||||||
|
|
||||||
|
|
||||||
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"""
|
"""Create FastAPI router for bot configuration endpoints"""
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/bots/config", tags=["Bot Configuration"])
|
router = APIRouter(prefix="/api/bots/config", tags=["Bot Configuration"])
|
||||||
|
|
||||||
@router.get("/schema/{bot_name}")
|
@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"""
|
"""Get configuration schema for a specific bot"""
|
||||||
try:
|
try:
|
||||||
# Check if we have cached schema
|
# Check if we have cached schema
|
||||||
@ -82,11 +66,13 @@ def create_bot_config_router(config_manager: BotConfigManager, bot_manager) -> A
|
|||||||
|
|
||||||
if not schema:
|
if not schema:
|
||||||
# Try to discover schema from bot provider
|
# Try to discover schema from bot provider
|
||||||
providers = bot_manager.get_providers()
|
providers_response = bot_manager.list_providers()
|
||||||
for provider_id, provider in providers.items():
|
for provider in providers_response.providers:
|
||||||
try:
|
try:
|
||||||
# Check if this provider has the bot
|
# 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]
|
bot_names = [bot.name for bot in provider_bots.bots]
|
||||||
|
|
||||||
if bot_name in bot_names:
|
if bot_name in bot_names:
|
||||||
@ -96,13 +82,40 @@ def create_bot_config_router(config_manager: BotConfigManager, bot_manager) -> A
|
|||||||
if schema:
|
if schema:
|
||||||
break
|
break
|
||||||
except Exception as e:
|
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
|
continue
|
||||||
|
|
||||||
if not schema:
|
if not schema:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=404,
|
status_code=404,
|
||||||
detail=f"No configuration schema found for bot '{bot_name}'"
|
detail=f"No configuration schema found for bot '{bot_name}'",
|
||||||
)
|
)
|
||||||
|
|
||||||
return schema
|
return schema
|
||||||
@ -132,7 +145,7 @@ def create_bot_config_router(config_manager: BotConfigManager, bot_manager) -> A
|
|||||||
if not config:
|
if not config:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=404,
|
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
|
return config
|
||||||
@ -140,14 +153,16 @@ def create_bot_config_router(config_manager: BotConfigManager, bot_manager) -> A
|
|||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
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")
|
raise HTTPException(status_code=500, detail="Internal server error")
|
||||||
|
|
||||||
@router.post("/update")
|
@router.post("/update")
|
||||||
async def update_bot_config(
|
async def update_bot_config(
|
||||||
request: BotConfigUpdateRequest,
|
request: BotConfigUpdateRequest,
|
||||||
background_tasks: BackgroundTasks,
|
background_tasks: BackgroundTasks,
|
||||||
session_id: str = "unknown" # TODO: Get from auth/session context
|
session_id: str = "unknown", # TODO: Get from auth/session context
|
||||||
) -> BotConfigUpdateResponse:
|
) -> BotConfigUpdateResponse:
|
||||||
"""Update bot configuration for a lobby"""
|
"""Update bot configuration for a lobby"""
|
||||||
try:
|
try:
|
||||||
@ -155,14 +170,16 @@ def create_bot_config_router(config_manager: BotConfigManager, bot_manager) -> A
|
|||||||
provider_id = None
|
provider_id = None
|
||||||
provider_url = None
|
provider_url = None
|
||||||
|
|
||||||
providers = bot_manager.get_providers()
|
providers_response = bot_manager.list_providers()
|
||||||
for pid, provider in providers.items():
|
for provider in providers_response.providers:
|
||||||
try:
|
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]
|
bot_names = [bot.name for bot in provider_bots.bots]
|
||||||
|
|
||||||
if request.bot_name in bot_names:
|
if request.bot_name in bot_names:
|
||||||
provider_id = pid
|
provider_id = provider.provider_id
|
||||||
provider_url = provider.base_url
|
provider_url = provider.base_url
|
||||||
break
|
break
|
||||||
except Exception:
|
except Exception:
|
||||||
@ -175,6 +192,12 @@ def create_bot_config_router(config_manager: BotConfigManager, bot_manager) -> A
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Update configuration
|
# 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(
|
config = config_manager.set_bot_config(
|
||||||
lobby_id=request.lobby_id,
|
lobby_id=request.lobby_id,
|
||||||
bot_name=request.bot_name,
|
bot_name=request.bot_name,
|
||||||
@ -260,16 +283,20 @@ def create_bot_config_router(config_manager: BotConfigManager, bot_manager) -> A
|
|||||||
try:
|
try:
|
||||||
async def refresh_task():
|
async def refresh_task():
|
||||||
refreshed = 0
|
refreshed = 0
|
||||||
providers = bot_manager.get_providers()
|
providers_response = bot_manager.list_providers()
|
||||||
|
|
||||||
for provider_id, provider in providers.items():
|
for provider in providers_response.providers:
|
||||||
try:
|
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:
|
for bot in provider_bots.bots:
|
||||||
try:
|
try:
|
||||||
schema = await config_manager.discover_bot_config_schema(
|
schema = (
|
||||||
bot.name, provider.base_url
|
await config_manager.discover_bot_config_schema(
|
||||||
|
bot.name, provider.base_url, force_refresh=True
|
||||||
|
)
|
||||||
)
|
)
|
||||||
if schema:
|
if schema:
|
||||||
refreshed += 1
|
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}")
|
logger.warning(f"Failed to refresh schema for {bot.name}: {e}")
|
||||||
|
|
||||||
except Exception as 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")
|
logger.info(f"Refreshed {refreshed} bot configuration schemas")
|
||||||
|
|
||||||
@ -292,6 +321,70 @@ def create_bot_config_router(config_manager: BotConfigManager, bot_manager) -> A
|
|||||||
logger.error(f"Failed to start schema refresh: {e}")
|
logger.error(f"Failed to start schema refresh: {e}")
|
||||||
raise HTTPException(status_code=500, detail="Internal server error")
|
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
|
return router
|
||||||
|
|
||||||
|
|
||||||
|
@ -8,7 +8,6 @@ Extracted from main.py to improve maintainability and separation of concerns.
|
|||||||
import hashlib
|
import hashlib
|
||||||
import binascii
|
import binascii
|
||||||
import secrets
|
import secrets
|
||||||
import os
|
|
||||||
import threading
|
import threading
|
||||||
from typing import Dict, Optional, Tuple
|
from typing import Dict, Optional, Tuple
|
||||||
|
|
||||||
@ -18,17 +17,19 @@ try:
|
|||||||
from ...shared.models import NamePasswordRecord
|
from ...shared.models import NamePasswordRecord
|
||||||
except ImportError:
|
except ImportError:
|
||||||
try:
|
try:
|
||||||
# Try absolute import (when running directly)
|
# Try direct import (when PYTHONPATH is set)
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
|
||||||
from shared.models import NamePasswordRecord
|
from shared.models import NamePasswordRecord
|
||||||
except ImportError:
|
except ImportError:
|
||||||
# Fallback: create minimal model for testing
|
# Log a warning for debugging (optional)
|
||||||
from pydantic import BaseModel
|
import warnings
|
||||||
class NamePasswordRecord(BaseModel):
|
|
||||||
name: str
|
warnings.warn(
|
||||||
password: str
|
"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
|
from logger import logger
|
||||||
|
|
||||||
@ -163,14 +164,10 @@ class AuthManager:
|
|||||||
|
|
||||||
def validate_integrity(self) -> list[str]:
|
def validate_integrity(self) -> list[str]:
|
||||||
"""Validate auth data integrity and return list of issues"""
|
"""Validate auth data integrity and return list of issues"""
|
||||||
issues = []
|
issues: list[str] = []
|
||||||
|
|
||||||
with self.lock:
|
with self.lock:
|
||||||
for name, record in self.name_passwords.items():
|
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:
|
if "salt" not in record or "hash" not in record:
|
||||||
issues.append(f"Name '{name}' missing salt or hash")
|
issues.append(f"Name '{name}' missing salt or hash")
|
||||||
continue
|
continue
|
||||||
|
@ -149,9 +149,24 @@ class BotConfigManager:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to save bot schema {bot_name}: {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"""
|
"""Discover configuration schema from bot provider"""
|
||||||
try:
|
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:
|
async with httpx.AsyncClient() as client:
|
||||||
# Try to get configuration schema from bot provider
|
# Try to get configuration schema from bot provider
|
||||||
response = await client.get(
|
response = await client.get(
|
||||||
@ -163,17 +178,35 @@ class BotConfigManager:
|
|||||||
schema_data = response.json()
|
schema_data = response.json()
|
||||||
schema = BotConfigSchema(**schema_data)
|
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
|
# Cache the schema
|
||||||
self.schema_cache[bot_name] = schema
|
self.schema_cache[bot_name] = schema
|
||||||
self._save_bot_schema(bot_name)
|
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
|
return schema
|
||||||
else:
|
else:
|
||||||
logger.warning(f"Bot {bot_name} does not support configuration (HTTP {response.status_code})")
|
logger.warning(f"Bot {bot_name} does not support configuration (HTTP {response.status_code})")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Failed to discover config schema for bot {bot_name}: {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
|
return None
|
||||||
|
|
||||||
@ -181,6 +214,26 @@ class BotConfigManager:
|
|||||||
"""Get cached configuration schema for a bot"""
|
"""Get cached configuration schema for a bot"""
|
||||||
return self.schema_cache.get(bot_name)
|
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]:
|
def get_lobby_bot_config(self, lobby_id: str, bot_name: str) -> Optional[BotLobbyConfig]:
|
||||||
"""Get bot configuration for a specific lobby"""
|
"""Get bot configuration for a specific lobby"""
|
||||||
if lobby_id in self.config_cache and bot_name in self.config_cache[lobby_id]:
|
if lobby_id in self.config_cache and bot_name in self.config_cache[lobby_id]:
|
||||||
|
@ -28,6 +28,7 @@ try:
|
|||||||
)
|
)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
try:
|
try:
|
||||||
|
# Try direct import (when PYTHONPATH is set)
|
||||||
from shared.models import (
|
from shared.models import (
|
||||||
BotProviderModel,
|
BotProviderModel,
|
||||||
BotProviderRegisterRequest,
|
BotProviderRegisterRequest,
|
||||||
@ -44,70 +45,16 @@ except ImportError:
|
|||||||
BotJoinPayload,
|
BotJoinPayload,
|
||||||
)
|
)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
# Create dummy models for standalone testing
|
# Log a warning for debugging (optional)
|
||||||
from pydantic import BaseModel
|
import warnings
|
||||||
|
|
||||||
class BotProviderModel(BaseModel):
|
warnings.warn(
|
||||||
provider_id: str
|
"Relative import failed, ensure PYTHONPATH includes project root or run as package"
|
||||||
base_url: str
|
)
|
||||||
name: str
|
# Rely on environment setup or raise a clear error
|
||||||
description: str
|
raise ImportError(
|
||||||
provider_key: str
|
"Cannot import shared.models. Ensure the project is run as a package or PYTHONPATH is set."
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
class BotProviderConfig:
|
class BotProviderConfig:
|
||||||
@ -235,6 +182,26 @@ class BotManager:
|
|||||||
|
|
||||||
return BotListResponse(bots=bots, providers=providers)
|
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:
|
async def request_bot_join(self, bot_name: str, request: BotJoinLobbyRequest, session_manager, lobby_manager) -> BotJoinLobbyResponse:
|
||||||
"""Request a bot to join a specific lobby"""
|
"""Request a bot to join a specific lobby"""
|
||||||
|
|
||||||
|
@ -251,15 +251,17 @@ class SessionManager:
|
|||||||
if not session_id:
|
if not session_id:
|
||||||
session_id = secrets.token_hex(16)
|
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:
|
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._instances.append(session)
|
||||||
|
|
||||||
self.save()
|
self.save()
|
||||||
@ -276,12 +278,11 @@ class SessionManager:
|
|||||||
|
|
||||||
def get_session(self, session_id: str) -> Optional[Session]:
|
def get_session(self, session_id: str) -> Optional[Session]:
|
||||||
"""Get session by ID"""
|
"""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:
|
with self.lock:
|
||||||
|
if not self._loaded:
|
||||||
|
self.load()
|
||||||
|
logger.info(f"Loaded {len(self._instances)} sessions from disk...")
|
||||||
|
|
||||||
for s in self._instances:
|
for s in self._instances:
|
||||||
if s.id == session_id:
|
if s.id == session_id:
|
||||||
return s
|
return s
|
||||||
@ -402,6 +403,19 @@ class SessionManager:
|
|||||||
logger.info(f"Expiring session {s_saved.id[:8]}:{name} during load")
|
logger.info(f"Expiring session {s_saved.id[:8]}:{name} during load")
|
||||||
continue
|
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(
|
session = Session(
|
||||||
s_saved.id,
|
s_saved.id,
|
||||||
is_bot=getattr(s_saved, "is_bot", False),
|
is_bot=getattr(s_saved, "is_bot", False),
|
||||||
@ -426,6 +440,9 @@ class SessionManager:
|
|||||||
logger.info(f"Expired {sessions_expired} old sessions during load")
|
logger.info(f"Expired {sessions_expired} old sessions during load")
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
|
# Mark as loaded to prevent duplicate loads
|
||||||
|
self._loaded = True
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _should_remove_session_static(
|
def _should_remove_session_static(
|
||||||
name: str,
|
name: str,
|
||||||
|
@ -17,6 +17,7 @@ fi
|
|||||||
|
|
||||||
export VIRTUAL_ENV=/server/.venv
|
export VIRTUAL_ENV=/server/.venv
|
||||||
export PATH="$VIRTUAL_ENV/bin:$PATH"
|
export PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||||
|
export PYTHONPATH="/:/server"
|
||||||
|
|
||||||
if [ -f "${SSL_CERTFILE}" ] && [ -f "${SSL_KEYFILE}" ]; then
|
if [ -f "${SSL_CERTFILE}" ] && [ -f "${SSL_KEYFILE}" ]; then
|
||||||
echo "Starting server with SSL..."
|
echo "Starting server with SSL..."
|
||||||
|
@ -452,18 +452,9 @@ def get_config_schema() -> Dict[str, Any]:
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"categories": [
|
"categories": [
|
||||||
{
|
{"AI Settings": ["personality", "ai_provider", "streaming"]},
|
||||||
"name": "AI Settings",
|
{"Behavior Settings": ["memory_enabled", "response_length", "creativity_level"]},
|
||||||
"parameters": ["personality", "ai_provider", "streaming"]
|
{"Communication Style": ["response_style"]}
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Behavior Settings",
|
|
||||||
"parameters": ["memory_enabled", "response_length", "creativity_level"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Communication Style",
|
|
||||||
"parameters": ["response_style"]
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user