kyush-llm-router/server/src/routes/admin.ts

430 lines
12 KiB
TypeScript

import { Router, Request, Response } from 'express';
import { UserModel } from '../models/User';
import { BackendModel } from '../models/Backend';
import { ModelRewriteModel } from '../models/ModelRewrite';
import { PermissionModel } from '../models/Permission';
import scriptRoutes from './scripts';
import {
CreateBackendData,
CreateModelRewriteData,
CreatePermissionData,
CreateUserData,
UpdateBackendData,
UpdateModelRewriteData,
UpdateUserData,
} from '../../../shared/types';
import { getUtcTimestamp } from '../utils/time';
import { ModelCatalogService } from '../services/ModelCatalogService';
import { AnalyticsService } from '../services/AnalyticsService';
const router: Router = Router();
router.use('/scripts', scriptRoutes);
function sendRewriteCycleError(res: Response, rules: ReturnType<typeof ModelRewriteModel.findAll>): boolean {
const cycle = ModelCatalogService.detectRewriteCycle(rules);
if (!cycle) {
return false;
}
res.status(409).json({
error: 'Model rewrite cycle detected',
cycle,
});
return true;
}
router.get('/dashboard/summary', (req: Request, res: Response) => {
const days = req.query.days ? Number(req.query.days) : 30;
res.json(AnalyticsService.getDashboardSummary(days));
});
// ============ User Management ============
router.get('/users', (req: Request, res: Response) => {
const users = UserModel.findAll();
res.json(users);
});
router.post('/users', (req: Request, res: Response) => {
const { name, email, api_key, detail_logging } = req.body as CreateUserData;
if (!name?.trim()) {
res.status(400).json({ error: 'Name is required' });
return;
}
try {
const user = UserModel.create({
name: name.trim(),
email: email?.trim() || undefined,
api_key: api_key?.trim() || undefined,
detail_logging,
});
res.status(201).json(user);
} catch (error) {
if (error instanceof Error && error.message.includes('UNIQUE')) {
res.status(409).json({ error: 'API key already exists' });
return;
}
res.status(500).json({ error: 'Failed to create user' });
}
});
router.get('/users/:id', (req: Request, res: Response) => {
const id = Number(req.params.id);
const user = UserModel.findById(id);
if (!user) {
res.status(404).json({ error: 'User not found' });
return;
}
res.json(user);
});
router.put('/users/:id', (req: Request, res: Response) => {
const id = Number(req.params.id);
const user = UserModel.findById(id);
if (!user) {
res.status(404).json({ error: 'User not found' });
return;
}
const { name, email, api_key, is_active, detail_logging } = req.body as UpdateUserData;
if (typeof name === 'string' && !name.trim()) {
res.status(400).json({ error: 'Name cannot be empty' });
return;
}
try {
const updatedUser = UserModel.update(id, {
name: typeof name === 'string' ? name.trim() : undefined,
email: typeof email === 'string' ? email.trim() || undefined : undefined,
api_key: typeof api_key === 'string' ? api_key.trim() || undefined : undefined,
is_active,
detail_logging,
});
res.json(updatedUser);
} catch (error) {
if (error instanceof Error && error.message.includes('UNIQUE')) {
res.status(409).json({ error: 'API key already exists' });
return;
}
res.status(500).json({ error: 'Failed to update user' });
}
});
router.delete('/users/:id', (req: Request, res: Response) => {
const id = Number(req.params.id);
const success = UserModel.delete(id);
if (!success) {
res.status(404).json({ error: 'User not found' });
return;
}
res.status(204).send();
});
router.post('/users/:id/regenerate-api-key', (req: Request, res: Response) => {
const id = Number(req.params.id);
const user = UserModel.findById(id);
if (!user) {
res.status(404).json({ error: 'User not found' });
return;
}
const newApiKey = UserModel.regenerateApiKey(id);
if (!newApiKey) {
res.status(500).json({ error: 'Failed to regenerate API key' });
return;
}
res.json({ ...user, api_key: newApiKey });
});
// ============ Backend Management ============
router.get('/backends', (req: Request, res: Response) => {
const backends = ModelCatalogService.getBackendsWithSummary();
res.json(backends);
});
router.post('/backends', (req: Request, res: Response) => {
const { name, base_url, api_key, detail_logging } = req.body as CreateBackendData;
if (!name || !base_url) {
res.status(400).json({ error: 'Name and base_url are required' });
return;
}
const backend = BackendModel.create({ name, base_url, api_key, detail_logging });
res.status(201).json(backend);
});
router.get('/backends/:id', (req: Request, res: Response) => {
const id = Number(req.params.id);
const backend = ModelCatalogService.getBackendsWithSummary().find((item) => item.id === id);
if (!backend) {
res.status(404).json({ error: 'Backend not found' });
return;
}
res.json(backend);
});
router.put('/backends/:id', async (req: Request, res: Response) => {
const id = Number(req.params.id);
const backend = BackendModel.findById(id);
if (!backend) {
res.status(404).json({ error: 'Backend not found' });
return;
}
const { name, base_url, api_key, is_active, detail_logging } = req.body as UpdateBackendData;
const updatedBackend = BackendModel.update(id, { name, base_url, api_key, is_active, detail_logging });
await ModelCatalogService.handleBackendUpdated(id);
res.json(ModelCatalogService.getBackendsWithSummary().find((item) => item.id === id) || updatedBackend);
});
router.delete('/backends/:id', async (req: Request, res: Response) => {
const id = Number(req.params.id);
const success = BackendModel.delete(id);
if (!success) {
res.status(404).json({ error: 'Backend not found' });
return;
}
await ModelCatalogService.handleBackendUpdated(id);
res.status(204).send();
});
router.get('/backends/:id/models', (req: Request, res: Response) => {
const id = Number(req.params.id);
const payload = ModelCatalogService.getBackendModelsResponse(id);
if (!payload) {
res.status(404).json({ error: 'Backend not found' });
return;
}
res.json(payload);
});
router.post('/backends/:id/models/refresh', async (req: Request, res: Response) => {
const id = Number(req.params.id);
const backend = BackendModel.findById(id);
if (!backend) {
res.status(404).json({ error: 'Backend not found' });
return;
}
if (!backend.is_active) {
res.status(409).json({ error: 'Inactive backends cannot refresh model cache' });
return;
}
const cache = await ModelCatalogService.refreshBackendModels(id, { force: true, reason: 'admin-manual' });
res.json({
backend: ModelCatalogService.getBackendsWithSummary().find((item) => item.id === id) || backend,
cache,
snapshots: ModelCatalogService.getBackendModelsResponse(id)?.snapshots || [],
models: ModelCatalogService.getBackendModelsResponse(id)?.models || [],
});
});
router.get('/models/cache', (req: Request, res: Response) => {
res.json(ModelCatalogService.getCacheOverview());
});
// ============ Permission Management ============
router.get('/permissions', (req: Request, res: Response) => {
const permissions = PermissionModel.findAll();
res.json(permissions);
});
router.get('/permissions/user/:userId', (req: Request, res: Response) => {
const userId = Number(req.params.userId);
const permissions = PermissionModel.findByUserId(userId);
res.json(permissions);
});
router.get('/permissions/backend/:backendId', (req: Request, res: Response) => {
const backendId = Number(req.params.backendId);
const permissions = PermissionModel.findByBackendId(backendId);
res.json(permissions);
});
router.post('/permissions', (req: Request, res: Response) => {
const { user_id, backend_id } = req.body as CreatePermissionData;
if (!user_id || !backend_id) {
res.status(400).json({ error: 'user_id and backend_id are required' });
return;
}
try {
const permission = PermissionModel.create({ user_id, backend_id });
res.status(201).json(permission);
} catch (error) {
if (error instanceof Error && error.message.includes('already exists')) {
res.status(409).json({ error: error.message });
return;
}
res.status(500).json({ error: 'Failed to create permission' });
}
});
router.delete('/permissions', (req: Request, res: Response) => {
const { user_id, backend_id } = req.query as { user_id?: string; backend_id?: string };
if (!user_id || !backend_id) {
res.status(400).json({ error: 'user_id and backend_id are required' });
return;
}
const success = PermissionModel.delete(Number(user_id), Number(backend_id));
if (!success) {
res.status(404).json({ error: 'Permission not found' });
return;
}
res.status(204).send();
});
router.get('/model-rewrites', (req: Request, res: Response) => {
res.json(ModelRewriteModel.findAll());
});
router.post('/model-rewrites', (req: Request, res: Response) => {
const { source_model, target_model, is_active, force, note } = req.body as CreateModelRewriteData;
if (!source_model?.trim() || !target_model?.trim()) {
res.status(400).json({ error: 'source_model and target_model are required' });
return;
}
const sourceModel = source_model.trim();
const targetModel = target_model.trim();
const timestamp = getUtcTimestamp();
const candidateRules = [
...ModelRewriteModel.findAll(),
{
id: 0,
source_model: sourceModel,
target_model: targetModel,
is_active: is_active === false ? false : true,
force: !!force,
note,
created_at: timestamp,
updated_at: timestamp,
},
];
if (sendRewriteCycleError(res, candidateRules)) {
return;
}
try {
const rule = ModelRewriteModel.create({
source_model: sourceModel,
target_model: targetModel,
is_active,
force,
note,
});
ModelCatalogService.loadRewriteMap();
res.status(201).json(rule);
} catch (error) {
if (error instanceof Error && error.message.includes('UNIQUE')) {
res.status(409).json({ error: 'Rewrite rule already exists for this source_model' });
return;
}
res.status(500).json({ error: 'Failed to create model rewrite rule' });
}
});
router.put('/model-rewrites/:id', (req: Request, res: Response) => {
const id = Number(req.params.id);
const existing = ModelRewriteModel.findById(id);
if (!existing) {
res.status(404).json({ error: 'Model rewrite rule not found' });
return;
}
const data = req.body as UpdateModelRewriteData;
if (typeof data.source_model === 'string' && !data.source_model.trim()) {
res.status(400).json({ error: 'source_model cannot be empty' });
return;
}
if (typeof data.target_model === 'string' && !data.target_model.trim()) {
res.status(400).json({ error: 'target_model cannot be empty' });
return;
}
const candidateRules = ModelRewriteModel.findAll().map((rule) => {
if (rule.id !== id) {
return rule;
}
return {
...rule,
source_model: typeof data.source_model === 'string' ? data.source_model.trim() : rule.source_model,
target_model: typeof data.target_model === 'string' ? data.target_model.trim() : rule.target_model,
is_active: data.is_active !== undefined ? data.is_active : rule.is_active,
force: data.force !== undefined ? data.force : rule.force,
note: data.note !== undefined ? data.note : rule.note,
};
});
if (sendRewriteCycleError(res, candidateRules)) {
return;
}
try {
const updated = ModelRewriteModel.update(id, {
...data,
source_model: typeof data.source_model === 'string' ? data.source_model.trim() : undefined,
target_model: typeof data.target_model === 'string' ? data.target_model.trim() : undefined,
});
ModelCatalogService.loadRewriteMap();
res.json(updated);
} catch (error) {
if (error instanceof Error && error.message.includes('UNIQUE')) {
res.status(409).json({ error: 'Rewrite rule already exists for this source_model' });
return;
}
res.status(500).json({ error: 'Failed to update model rewrite rule' });
}
});
router.delete('/model-rewrites/:id', (req: Request, res: Response) => {
const id = Number(req.params.id);
const success = ModelRewriteModel.delete(id);
if (!success) {
res.status(404).json({ error: 'Model rewrite rule not found' });
return;
}
ModelCatalogService.loadRewriteMap();
res.status(204).send();
});
// ============ Health Check ============
router.get('/health', (req: Request, res: Response) => {
res.json({ status: 'ok', timestamp: getUtcTimestamp() });
});
export default router;