Files
llm-proxy/admin/app.py
T
hubian db114269f9 feat: 添加对话功能
新增功能:
- 对话页面 (/chat)
- 选择不同模型进行对话
- 历史对话保存和加载
- 支持继续历史对话

API:
- GET /api/chat/models - 获取可用模型
- GET /api/chat/list - 获取对话列表
- GET /api/chat/<id> - 获取对话详情
- POST /api/chat/send - 发送消息
- DELETE /api/chat/<id> - 删除对话
- POST /api/chat/<id>/clear - 清空对话
2026-04-09 18:29:54 +08:00

792 lines
24 KiB
Python

"""
大模型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, DEFAULT_AUTO_PROFILES,
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, get_auto_profiles, get_auto_profile,
add_auto_profile, update_auto_profile, delete_auto_profile
)
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')
@app.route('/chat')
def chat_page():
return render_template('chat.html')
@app.route('/auto-profiles')
def auto_profiles_page():
return render_template('auto-profiles.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'})
# ============ 对话功能 ============
CHATS_FILE = DATA_DIR / 'chats.json'
def load_chats():
"""加载对话数据"""
if CHATS_FILE.exists():
return json.loads(CHATS_FILE.read_text(encoding='utf-8'))
return {'chats': []}
def save_chats(data):
"""保存对话数据"""
CHATS_FILE.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding='utf-8')
@app.route('/api/chat/models')
def api_chat_models():
"""获取可用模型列表"""
providers = get_providers()
profiles = get_auto_profiles()
models = []
added = set()
# 添加所有auto配置
for name, profile in profiles.items():
if name not in added:
models.append({
'id': name,
'description': profile.get('description', 'Auto-select')
})
added.add(name)
# 添加提供商的模型
for provider in providers:
if not provider['enabled']:
continue
for model in provider['models']:
if model not in added:
models.append({
'id': model,
'description': provider['name']
})
added.add(model)
return jsonify(models)
@app.route('/api/chat/list')
def api_chat_list():
"""获取对话列表"""
data = load_chats()
chats = []
for chat in data.get('chats', []):
chats.append({
'id': chat['id'],
'title': chat.get('title', '新对话'),
'model': chat.get('model', 'auto'),
'message_count': len(chat.get('messages', [])),
'created_at': chat.get('created_at'),
'updated_at': chat.get('updated_at')
})
# 按更新时间倒序
chats.sort(key=lambda x: x.get('updated_at', ''), reverse=True)
return jsonify(chats)
@app.route('/api/chat/<chat_id>')
def api_chat_detail(chat_id):
"""获取对话详情"""
data = load_chats()
for chat in data.get('chats', []):
if chat['id'] == chat_id:
return jsonify(chat)
return jsonify({'error': 'Chat not found'}), 404
@app.route('/api/chat/send', methods=['POST'])
def api_chat_send():
"""发送消息"""
req = request.get_json()
user_message = req.get('message', '')
model = req.get('model', 'auto')
chat_id = req.get('chat_id')
if not user_message:
return jsonify({'error': 'Message is required'}), 400
data = load_chats()
# 查找或创建对话
chat = None
if chat_id:
for c in data['chats']:
if c['id'] == chat_id:
chat = c
break
if not chat:
chat_id = str(uuid.uuid4())[:8]
chat = {
'id': chat_id,
'title': '新对话',
'model': model,
'messages': [],
'created_at': datetime.now().isoformat(),
'updated_at': datetime.now().isoformat()
}
data['chats'].append(chat)
# 添加用户消息
chat['messages'].append({
'role': 'user',
'content': user_message,
'time': datetime.now().isoformat()
})
# 调用LLM API
try:
proxy_url = f"http://localhost:{SERVER_CONFIG['port']}/v1/chat/completions"
# 构建消息历史
messages = []
for msg in chat['messages'][-20:]: # 最多保留20条历史
messages.append({
'role': msg['role'],
'content': msg['content']
})
response = requests.post(proxy_url, json={
'model': model,
'messages': messages,
'stream': False
}, timeout=120)
if response.status_code == 200:
result = response.json()
assistant_message = result['choices'][0]['message']['content']
used_model = result.get('model', model)
# 添加助手消息
chat['messages'].append({
'role': 'assistant',
'content': assistant_message,
'model': used_model,
'time': datetime.now().isoformat()
})
# 更新标题(如果是第一条消息)
if len(chat['messages']) == 2:
chat['title'] = user_message[:30] + ('...' if len(user_message) > 30 else '')
chat['updated_at'] = datetime.now().isoformat()
save_chats(data)
return jsonify({
'success': True,
'chat_id': chat_id,
'response': assistant_message,
'model': used_model,
'title': chat['title']
})
else:
error_msg = response.json().get('error', {}).get('message', 'Unknown error')
return jsonify({'error': error_msg}), response.status_code
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/chat/<chat_id>', methods=['DELETE'])
def api_delete_chat(chat_id):
"""删除对话"""
data = load_chats()
data['chats'] = [c for c in data['chats'] if c['id'] != chat_id]
save_chats(data)
return jsonify({'success': True})
@app.route('/api/chat/<chat_id>/clear', methods=['POST'])
def api_clear_chat(chat_id):
"""清空对话消息"""
data = load_chats()
for chat in data['chats']:
if chat['id'] == chat_id:
chat['messages'] = []
chat['updated_at'] = datetime.now().isoformat()
save_chats(data)
return jsonify({'success': True})
return jsonify({'error': 'Chat not found'}), 404
@app.route('/api/requests/recent')
def api_recent_requests():
"""获取最近请求记录"""
# 模拟数据
return jsonify([])
# ============ Auto配置管理 ============
@app.route('/api/auto-profiles')
def api_auto_profiles():
"""获取所有Auto配置"""
profiles = get_auto_profiles()
providers = get_providers()
result = []
for name, profile in profiles.items():
# 解析允许的提供商信息
allowed_providers = profile.get('providers', ['*'])
provider_details = []
if '*' in allowed_providers:
provider_details = [{'id': '*', 'name': '所有启用的提供商'}]
else:
for p in providers:
if p.get('id') in allowed_providers or p['name'] in allowed_providers:
provider_details.append({
'id': p.get('id'),
'name': p['name'],
'priority': p['priority']
})
result.append({
'name': name,
'display_name': profile.get('name', name),
'description': profile.get('description', ''),
'strategy': profile.get('strategy', 'priority'),
'providers': allowed_providers,
'provider_details': provider_details,
})
return jsonify(result)
@app.route('/api/auto-profiles/<profile_name>', methods=['GET'])
def api_auto_profile_detail(profile_name):
"""获取单个Auto配置详情"""
profile = get_auto_profile(profile_name)
if not profile:
return jsonify({'error': 'Profile not found'}), 404
providers = get_providers()
allowed_providers = profile.get('providers', ['*'])
provider_details = []
if '*' in allowed_providers:
provider_details = [{'id': '*', 'name': '所有启用的提供商', 'selected': True}]
for p in providers:
provider_details.append({
'id': p.get('id'),
'name': p['name'],
'priority': p['priority'],
'selected': True
})
else:
for p in providers:
selected = p.get('id') in allowed_providers or p['name'] in allowed_providers
provider_details.append({
'id': p.get('id'),
'name': p['name'],
'priority': p['priority'],
'selected': selected
})
return jsonify({
'name': profile_name,
'display_name': profile.get('name', profile_name),
'description': profile.get('description', ''),
'strategy': profile.get('strategy', 'priority'),
'providers': allowed_providers,
'provider_details': provider_details,
})
@app.route('/api/auto-profiles', methods=['POST'])
def api_add_auto_profile():
"""添加新的Auto配置"""
data = request.get_json()
if not data or not data.get('name'):
return jsonify({'error': 'Missing profile name'}), 400
profile_name = data['name'].lower().replace(' ', '-').replace('.', '-')
if profile_name in get_auto_profiles():
return jsonify({'error': 'Profile already exists'}), 400
profile_data = {
'name': data.get('display_name', data['name']),
'description': data.get('description', ''),
'providers': data.get('providers', ['*']),
'strategy': data.get('strategy', 'priority'),
}
result = add_auto_profile(profile_name, profile_data)
return jsonify({'success': True, 'profile': {profile_name: profile_data}})
@app.route('/api/auto-profiles/<profile_name>', methods=['PUT'])
def api_update_auto_profile(profile_name):
"""更新Auto配置"""
data = request.get_json()
if not data:
return jsonify({'error': 'Invalid request body'}), 400
profile_data = {}
if 'display_name' in data:
profile_data['name'] = data['display_name']
if 'description' in data:
profile_data['description'] = data['description']
if 'providers' in data:
profile_data['providers'] = data['providers']
if 'strategy' in data:
profile_data['strategy'] = data['strategy']
result = update_auto_profile(profile_name, profile_data)
if not result:
return jsonify({'error': 'Profile not found'}), 404
return jsonify({'success': True, 'profile': result})
@app.route('/api/auto-profiles/<profile_name>', methods=['DELETE'])
def api_delete_auto_profile(profile_name):
"""删除Auto配置"""
result = delete_auto_profile(profile_name)
if not result:
return jsonify({'error': 'Cannot delete default auto profile or profile not found'}), 400
return jsonify({'success': True})
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)