Files
llm-proxy/admin/app.py
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
Raw Permalink 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, 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)