Files
llm-proxy/admin/app.py
hubian 82100cdf00 feat: 后台管理增加提供商动态管理功能
- 新增:添加新的大模型接口提供商
- 新增:编辑已有提供商的参数(API地址、Key、模型列表等)
- 新增:删除提供商
- 新增:拖拽排序调整auto模式的优先级顺序
- 新增:启用/禁用提供商开关
- 优化:主服务动态读取配置,后台修改实时生效
2026-04-08 18:14:09 +08:00

443 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
大模型API中转系统 - 后台管理系统
支持动态添加、编辑、删除提供商和优先级调整
"""
from flask import Flask, render_template, jsonify, request
from flask_cors import CORS
import json
import time
import uuid
from datetime import datetime
from pathlib import Path
import sys
import requests
# 添加父目录到路径
sys.path.insert(0, str(Path(__file__).parent.parent))
from config.settings import (
DEFAULT_PROVIDERS, DEFAULT_MODEL_ALIASES,
SERVER_CONFIG, LOG_CONFIG, RETRY_CONFIG,
load_config, save_config, get_providers,
get_provider, add_provider, update_provider,
delete_provider, update_priority, get_model_aliases,
update_model_alias
)
app = Flask(__name__)
CORS(app)
# 数据目录
DATA_DIR = Path(__file__).parent.parent / 'data'
DATA_DIR.mkdir(exist_ok=True)
STATS_FILE = DATA_DIR / 'stats.json'
CONFIG_FILE = DATA_DIR / 'config.json'
LOGS_DIR = Path(__file__).parent.parent / 'logs'
# 提供商状态缓存
provider_status = {}
def refresh_provider_status():
"""刷新提供商状态"""
providers = get_providers()
for provider in providers:
if provider['name'] not in provider_status:
provider_status[provider['name']] = {
'available': True,
'last_check': None,
'error_count': 0,
'last_error': None,
'request_count': 0,
'success_count': 0,
'total_tokens': 0,
}
def load_stats():
"""加载统计数据"""
if STATS_FILE.exists():
return json.loads(STATS_FILE.read_text(encoding='utf-8'))
return {
'total_requests': 0,
'total_success': 0,
'total_errors': 0,
'total_tokens': 0,
'requests_today': 0,
'providers': {},
'last_updated': None
}
def save_stats(stats):
"""保存统计数据"""
stats['last_updated'] = datetime.now().isoformat()
STATS_FILE.write_text(json.dumps(stats, ensure_ascii=False, indent=2), encoding='utf-8')
# ============ 页面路由 ============
@app.route('/')
def index():
return render_template('index.html')
@app.route('/providers')
def providers_page():
return render_template('providers.html')
@app.route('/models')
def models_page():
return render_template('models.html')
@app.route('/logs')
def logs_page():
return render_template('logs.html')
@app.route('/config')
def config_page():
return render_template('config.html')
# ============ API路由 ============
@app.route('/api/stats')
def api_stats():
"""获取统计数据"""
stats = load_stats()
providers = get_providers()
refresh_provider_status()
# 统计提供商状态
available_count = sum(1 for p in providers if provider_status.get(p['name'], {}).get('available', True))
return jsonify({
'total_requests': stats.get('total_requests', 0),
'total_success': stats.get('total_success', 0),
'total_errors': stats.get('total_errors', 0),
'total_tokens': stats.get('total_tokens', 0),
'providers_count': len(providers),
'available_providers': available_count,
'models_count': len(get_model_aliases()),
'uptime': time.time(),
})
@app.route('/api/providers')
def api_providers():
"""获取提供商列表"""
providers = get_providers()
refresh_provider_status()
stats = load_stats()
providers_data = []
for provider in sorted(providers, key=lambda x: x['priority']):
p_stats = stats.get('providers', {}).get(provider['name'], {})
p_status = provider_status.get(provider['name'], {})
providers_data.append({
'id': provider.get('id', provider['name'].lower().replace(' ', '-')),
'name': provider['name'],
'priority': provider['priority'],
'enabled': provider['enabled'],
'available': p_status.get('available', True),
'base_url': provider['base_url'],
'api_key': provider['api_key'],
'models': provider['models'],
'default_model': provider['default_model'],
'timeout': provider.get('timeout', 120),
'request_count': p_stats.get('request_count', 0),
'success_count': p_stats.get('success_count', 0),
'error_count': p_status.get('error_count', 0),
'last_error': p_status.get('last_error'),
'last_check': p_status.get('last_check'),
})
return jsonify(providers_data)
@app.route('/api/providers/<provider_id>', methods=['GET'])
def api_provider_detail(provider_id):
"""获取提供商详情"""
provider = get_provider(provider_id)
if not provider:
return jsonify({'error': 'Provider not found'}), 404
stats = load_stats()
p_stats = stats.get('providers', {}).get(provider['name'], {})
p_status = provider_status.get(provider['name'], {})
return jsonify({
**provider,
'status': {
'available': p_status.get('available', True),
'error_count': p_status.get('error_count', 0),
'last_error': p_status.get('last_error'),
'request_count': p_stats.get('request_count', 0),
'success_count': p_stats.get('success_count', 0),
}
})
@app.route('/api/providers', methods=['POST'])
def api_add_provider():
"""添加新提供商"""
data = request.get_json()
if not data:
return jsonify({'error': 'Invalid request body'}), 400
# 验证必填字段
required = ['name', 'base_url', 'api_key', 'models']
for field in required:
if not data.get(field):
return jsonify({'error': f'Missing required field: {field}'}), 400
# 构建提供商数据
providers = get_providers()
max_priority = max([p['priority'] for p in providers]) if providers else 0
new_provider = {
'id': data.get('id') or data['name'].lower().replace(' ', '-').replace('.', '-'),
'name': data['name'],
'priority': data.get('priority', max_priority + 1),
'base_url': data['base_url'].rstrip('/'),
'api_key': data['api_key'],
'models': data['models'] if isinstance(data['models'], list) else data['models'].split(','),
'default_model': data.get('default_model', data['models'][0] if isinstance(data['models'], list) else data['models'].split(',')[0]),
'timeout': data.get('timeout', 120),
'enabled': data.get('enabled', True),
}
# 添加到配置
result = add_provider(new_provider)
# 初始化状态
provider_status[result['name']] = {
'available': True,
'last_check': None,
'error_count': 0,
'last_error': None,
}
return jsonify({'success': True, 'provider': result})
@app.route('/api/providers/<provider_id>', methods=['PUT'])
def api_update_provider(provider_id):
"""更新提供商"""
data = request.get_json()
if not data:
return jsonify({'error': 'Invalid request body'}), 400
# 处理models字段
if 'models' in data and isinstance(data['models'], str):
data['models'] = [m.strip() for m in data['models'].split(',') if m.strip()]
result = update_provider(provider_id, data)
if not result:
return jsonify({'error': 'Provider not found'}), 404
return jsonify({'success': True, 'provider': result})
@app.route('/api/providers/<provider_id>', methods=['DELETE'])
def api_delete_provider(provider_id):
"""删除提供商"""
result = delete_provider(provider_id)
if not result:
return jsonify({'error': 'Provider not found'}), 404
# 清理状态
providers = get_providers()
for p in providers:
if p.get('id') == provider_id:
if p['name'] in provider_status:
del provider_status[p['name']]
break
return jsonify({'success': True})
@app.route('/api/providers/priority', methods=['POST'])
def api_update_priority():
"""更新优先级顺序(拖拽排序)"""
data = request.get_json()
if not data or 'order' not in data:
return jsonify({'error': 'Missing order field'}), 400
# order 是提供商ID列表按新顺序排列
provider_ids = data['order']
result = update_priority(provider_ids)
return jsonify({'success': True, 'providers': result})
@app.route('/api/providers/<provider_id>/toggle', methods=['POST'])
def api_toggle_provider(provider_id):
"""切换提供商启用状态"""
provider = get_provider(provider_id)
if not provider:
return jsonify({'error': 'Provider not found'}), 404
new_enabled = not provider.get('enabled', True)
result = update_provider(provider_id, {'enabled': new_enabled})
return jsonify({'success': True, 'enabled': new_enabled})
@app.route('/api/providers/<provider_id>/test', methods=['POST'])
def api_test_provider(provider_id):
"""测试提供商连接"""
provider = get_provider(provider_id)
if not provider:
return jsonify({'success': False, 'error': 'Provider not found'}), 404
try:
# 测试模型列表接口
url = f"{provider['base_url'].rstrip('/')}/models"
headers = {"Authorization": f"Bearer {provider['api_key']}"}
response = requests.get(url, headers=headers, timeout=10)
if response.status_code == 200:
provider_status[provider['name']] = {
'available': True,
'last_check': datetime.now().isoformat(),
'error_count': 0,
'last_error': None,
}
# 尝试解析返回的模型列表
models_data = []
try:
resp_json = response.json()
models_data = resp_json.get('data', [])
except:
pass
return jsonify({
'success': True,
'message': 'Connection successful',
'models_count': len(models_data)
})
else:
provider_status[provider['name']] = {
'available': False,
'last_check': datetime.now().isoformat(),
'error_count': provider_status.get(provider['name'], {}).get('error_count', 0) + 1,
'last_error': f'HTTP {response.status_code}',
}
return jsonify({
'success': False,
'error': f'HTTP {response.status_code}: {response.text[:200]}'
})
except Exception as e:
provider_status[provider['name']] = {
'available': False,
'last_check': datetime.now().isoformat(),
'error_count': provider_status.get(provider['name'], {}).get('error_count', 0) + 1,
'last_error': str(e),
}
return jsonify({'success': False, 'error': str(e)})
@app.route('/api/models')
def api_models():
"""获取模型列表"""
providers = get_providers()
aliases = get_model_aliases()
models_list = []
added = set()
# 添加auto
models_list.append({
'alias': 'auto',
'target': 'auto',
'description': '自动选择可用模型(按优先级)'
})
added.add('auto')
# 从提供商获取模型
for provider in sorted(providers, key=lambda x: x['priority']):
for model in provider['models']:
if model not in added:
models_list.append({
'alias': model,
'target': model,
'provider': provider['name'],
'priority': provider['priority'],
})
added.add(model)
# 添加别名
for alias, target in aliases.items():
if alias != 'auto' and alias not in added:
# 找到目标模型对应的提供商
provider_name = None
for p in providers:
if target in p['models']:
provider_name = p['name']
break
models_list.append({
'alias': alias,
'target': target,
'provider': provider_name,
})
return jsonify(models_list)
@app.route('/api/logs')
def api_logs():
"""获取日志"""
log_file = LOGS_DIR / 'proxy.log'
lines = []
if log_file.exists():
content = log_file.read_text(encoding='utf-8')
lines = content.strip().split('\n')[-100:] # 最近100条
return jsonify({
'logs': lines,
'total_lines': len(lines)
})
@app.route('/api/config')
def api_config():
"""获取配置"""
providers = get_providers()
aliases = get_model_aliases()
return jsonify({
'providers': [{
'id': p.get('id', p['name'].lower().replace(' ', '-')),
'name': p['name'],
'priority': p['priority'],
'base_url': p['base_url'],
'models': p['models'],
'timeout': p.get('timeout', 120),
'enabled': p['enabled'],
} for p in providers],
'model_aliases': aliases,
'retry_config': RETRY_CONFIG,
'server_config': {
'port': SERVER_CONFIG['port'],
}
})
@app.route('/api/reload', methods=['POST'])
def api_reload_config():
"""通知主服务重新加载配置"""
# 这个接口可以被主服务调用以重新加载配置
# 这里只返回成功,实际重载由主服务自己处理
return jsonify({'success': True, 'message': 'Config saved, restart main service to apply'})
@app.route('/api/requests/recent')
def api_recent_requests():
"""获取最近请求记录"""
# 模拟数据
return jsonify([])
if __name__ == '__main__':
print("=" * 50)
print("大模型API中转系统 - 后台管理")
print("=" * 50)
print(f"访问地址: http://localhost:19008")
print(f"前台地址: http://localhost:19007")
print("=" * 50)
app.run(host='0.0.0.0', port=19008, debug=True)