feat: 备用大模型接口管理功能
- 新增 BackupLLMConfig 数据模型存储备用大模型配置 - 支持手动新增、编辑、删除备用大模型接口 - 支持测试连接功能 - 大模型配置页面静态表格改为动态管理的备用接口链接 - 默认初始化5个常用大模型服务商配置
This commit is contained in:
206
admin.py
206
admin.py
@@ -10,7 +10,7 @@ import json
|
|||||||
|
|
||||||
from models import (db, User, Translation, TranslationCache, GuestTranslation,
|
from models import (db, User, Translation, TranslationCache, GuestTranslation,
|
||||||
SystemConfig, OperationLog, DataPackage, UserPackage, DynamicConfig,
|
SystemConfig, OperationLog, DataPackage, UserPackage, DynamicConfig,
|
||||||
UserTypeConfig, MembershipPlanConfig)
|
UserTypeConfig, MembershipPlanConfig, BackupLLMConfig)
|
||||||
from config import USER_LIMITS, MEMBERSHIP_PLANS
|
from config import USER_LIMITS, MEMBERSHIP_PLANS
|
||||||
|
|
||||||
admin_bp = Blueprint('admin', __name__, url_prefix='/admin')
|
admin_bp = Blueprint('admin', __name__, url_prefix='/admin')
|
||||||
@@ -1217,4 +1217,206 @@ def get_membership_plan(plan_key):
|
|||||||
def get_all_membership_plans():
|
def get_all_membership_plans():
|
||||||
"""获取所有会员套餐配置"""
|
"""获取所有会员套餐配置"""
|
||||||
plans = MembershipPlanConfig.query.filter_by(is_active=True).order_by(MembershipPlanConfig.sort_order).all()
|
plans = MembershipPlanConfig.query.filter_by(is_active=True).order_by(MembershipPlanConfig.sort_order).all()
|
||||||
return [p.to_dict() for p in plans]
|
return [p.to_dict() for p in plans]
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== 备用大模型接口管理 ====================
|
||||||
|
@admin_bp.route('/backup-llm')
|
||||||
|
@admin_required
|
||||||
|
def backup_llm_list():
|
||||||
|
"""备用大模型接口列表"""
|
||||||
|
configs = BackupLLMConfig.query.order_by(BackupLLMConfig.sort_order).all()
|
||||||
|
|
||||||
|
# 如果数据库中没有数据,初始化默认配置
|
||||||
|
if not configs:
|
||||||
|
init_default_backup_llm()
|
||||||
|
configs = BackupLLMConfig.query.order_by(BackupLLMConfig.sort_order).all()
|
||||||
|
|
||||||
|
return render_template('admin/backup_llm.html', configs=configs)
|
||||||
|
|
||||||
|
|
||||||
|
@admin_bp.route('/backup-llm/add', methods=['GET', 'POST'])
|
||||||
|
@admin_required
|
||||||
|
def add_backup_llm():
|
||||||
|
"""添加备用大模型接口"""
|
||||||
|
if request.method == 'POST':
|
||||||
|
data = request.json if request.is_json else request.form
|
||||||
|
|
||||||
|
config = BackupLLMConfig(
|
||||||
|
provider_name=data.get('provider_name'),
|
||||||
|
api_base=data.get('api_base'),
|
||||||
|
api_key=data.get('api_key'),
|
||||||
|
model=data.get('model'),
|
||||||
|
is_active=data.get('is_active', True) if isinstance(data.get('is_active'), bool) else data.get('is_active') == 'true',
|
||||||
|
sort_order=int(data.get('sort_order', 0)),
|
||||||
|
description=data.get('description'),
|
||||||
|
)
|
||||||
|
|
||||||
|
db.session.add(config)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# 记录日志
|
||||||
|
log = OperationLog(
|
||||||
|
user_id=session.get('user_id'),
|
||||||
|
username='admin',
|
||||||
|
action='add_backup_llm',
|
||||||
|
target=config.provider_name,
|
||||||
|
detail=json.dumps(config.to_dict(), ensure_ascii=False)
|
||||||
|
)
|
||||||
|
db.session.add(log)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
if request.is_json:
|
||||||
|
return jsonify({'success': True, 'config': config.to_dict()})
|
||||||
|
flash('备用大模型接口已添加', 'success')
|
||||||
|
return redirect(url_for('admin.backup_llm_list'))
|
||||||
|
|
||||||
|
return render_template('admin/backup_llm_form.html', config=None)
|
||||||
|
|
||||||
|
|
||||||
|
@admin_bp.route('/backup-llm/<int:config_id>/edit', methods=['GET', 'POST'])
|
||||||
|
@admin_required
|
||||||
|
def edit_backup_llm(config_id):
|
||||||
|
"""编辑备用大模型接口"""
|
||||||
|
config = BackupLLMConfig.query.get_or_404(config_id)
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
data = request.json if request.is_json else request.form
|
||||||
|
|
||||||
|
config.provider_name = data.get('provider_name', config.provider_name)
|
||||||
|
config.api_base = data.get('api_base', config.api_base)
|
||||||
|
config.api_key = data.get('api_key', config.api_key)
|
||||||
|
config.model = data.get('model', config.model)
|
||||||
|
config.is_active = data.get('is_active', True) if isinstance(data.get('is_active'), bool) else data.get('is_active') == 'true'
|
||||||
|
config.sort_order = int(data.get('sort_order', config.sort_order))
|
||||||
|
config.description = data.get('description', config.description)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# 记录日志
|
||||||
|
log = OperationLog(
|
||||||
|
user_id=session.get('user_id'),
|
||||||
|
username='admin',
|
||||||
|
action='edit_backup_llm',
|
||||||
|
target=config.provider_name,
|
||||||
|
detail=json.dumps(config.to_dict(), ensure_ascii=False)
|
||||||
|
)
|
||||||
|
db.session.add(log)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
if request.is_json:
|
||||||
|
return jsonify({'success': True, 'config': config.to_dict()})
|
||||||
|
flash('备用大模型接口已更新', 'success')
|
||||||
|
return redirect(url_for('admin.backup_llm_list'))
|
||||||
|
|
||||||
|
return render_template('admin/backup_llm_form.html', config=config)
|
||||||
|
|
||||||
|
|
||||||
|
@admin_bp.route('/backup-llm/<int:config_id>/delete', methods=['POST'])
|
||||||
|
@admin_required
|
||||||
|
def delete_backup_llm(config_id):
|
||||||
|
"""删除备用大模型接口"""
|
||||||
|
config = BackupLLMConfig.query.get_or_404(config_id)
|
||||||
|
|
||||||
|
provider_name = config.provider_name
|
||||||
|
db.session.delete(config)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# 记录日志
|
||||||
|
log = OperationLog(
|
||||||
|
user_id=session.get('user_id'),
|
||||||
|
username='admin',
|
||||||
|
action='delete_backup_llm',
|
||||||
|
target=provider_name
|
||||||
|
)
|
||||||
|
db.session.add(log)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return jsonify({'success': True})
|
||||||
|
|
||||||
|
|
||||||
|
@admin_bp.route('/backup-llm/<int:config_id>/toggle', methods=['POST'])
|
||||||
|
@admin_required
|
||||||
|
def toggle_backup_llm(config_id):
|
||||||
|
"""切换备用大模型接口状态"""
|
||||||
|
config = BackupLLMConfig.query.get_or_404(config_id)
|
||||||
|
config.is_active = not config.is_active
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return jsonify({'success': True, 'is_active': config.is_active})
|
||||||
|
|
||||||
|
|
||||||
|
@admin_bp.route('/backup-llm/<int:config_id>/test', methods=['POST'])
|
||||||
|
@admin_required
|
||||||
|
def test_backup_llm(config_id):
|
||||||
|
"""测试备用大模型接口"""
|
||||||
|
config = BackupLLMConfig.query.get_or_404(config_id)
|
||||||
|
|
||||||
|
try:
|
||||||
|
from openai import OpenAI
|
||||||
|
|
||||||
|
client = OpenAI(
|
||||||
|
api_key=config.api_key or 'sk-test',
|
||||||
|
base_url=config.api_base,
|
||||||
|
)
|
||||||
|
|
||||||
|
model = config.model or 'default'
|
||||||
|
|
||||||
|
# 发送简单测试请求
|
||||||
|
response = client.chat.completions.create(
|
||||||
|
model=model,
|
||||||
|
messages=[{"role": "user", "content": "Hello"}],
|
||||||
|
max_tokens=10,
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'provider': config.provider_name,
|
||||||
|
'model': model,
|
||||||
|
'response': response.choices[0].message.content[:50] if response.choices else 'OK'
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'success': False, 'error': str(e)})
|
||||||
|
|
||||||
|
|
||||||
|
@admin_bp.route('/backup-llm/init', methods=['POST'])
|
||||||
|
@admin_required
|
||||||
|
def init_backup_llm():
|
||||||
|
"""初始化默认备用大模型"""
|
||||||
|
init_default_backup_llm()
|
||||||
|
return jsonify({'success': True})
|
||||||
|
|
||||||
|
|
||||||
|
def init_default_backup_llm():
|
||||||
|
"""初始化默认备用大模型接口配置"""
|
||||||
|
defaults = [
|
||||||
|
('本地LM Studio', 'http://localhost:1234/v1', None, None, 0),
|
||||||
|
('OpenAI', 'https://api.openai.com/v1', None, 'gpt-4', 1),
|
||||||
|
('DeepSeek', 'https://api.deepseek.com/v1', None, 'deepseek-chat', 2),
|
||||||
|
('阿里百炼', 'https://dashscope.aliyuncs.com/compatible-mode/v1', None, 'qwen-turbo', 3),
|
||||||
|
('SiliconFlow', 'https://api.siliconflow.cn/v1', None, 'Qwen/Qwen2.5-72B-Instruct', 4),
|
||||||
|
]
|
||||||
|
|
||||||
|
for provider_name, api_base, api_key, model, sort_order in defaults:
|
||||||
|
existing = BackupLLMConfig.query.filter_by(provider_name=provider_name).first()
|
||||||
|
if not existing:
|
||||||
|
config = BackupLLMConfig(
|
||||||
|
provider_name=provider_name,
|
||||||
|
api_base=api_base,
|
||||||
|
api_key=api_key,
|
||||||
|
model=model,
|
||||||
|
is_active=True,
|
||||||
|
sort_order=sort_order,
|
||||||
|
description=f'{provider_name} 默认接口',
|
||||||
|
)
|
||||||
|
db.session.add(config)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def get_backup_llm_configs():
|
||||||
|
"""获取所有备用大模型配置(供其他模块使用)"""
|
||||||
|
configs = BackupLLMConfig.query.filter_by(is_active=True).order_by(BackupLLMConfig.sort_order).all()
|
||||||
|
return [c.to_dict() for c in configs]
|
||||||
38
models.py
38
models.py
@@ -869,6 +869,44 @@ class EmailNotification(db.Model):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== 备用大模型接口配置 ====================
|
||||||
|
class BackupLLMConfig(db.Model):
|
||||||
|
"""备用大模型接口配置"""
|
||||||
|
__tablename__ = 'backup_llm_config'
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
|
||||||
|
# 服务商信息
|
||||||
|
provider_name = db.Column(db.String(100), nullable=False) # 服务商名称: OpenAI, DeepSeek, 阿里百炼, etc.
|
||||||
|
api_base = db.Column(db.String(255), nullable=False) # API地址
|
||||||
|
api_key = db.Column(db.String(255), nullable=True) # API Key(可选)
|
||||||
|
model = db.Column(db.String(100), nullable=True) # 默认模型
|
||||||
|
|
||||||
|
# 状态
|
||||||
|
is_active = db.Column(db.Boolean, default=True) # 是否启用
|
||||||
|
sort_order = db.Column(db.Integer, default=0) # 排序
|
||||||
|
|
||||||
|
# 备注
|
||||||
|
description = db.Column(db.String(255), nullable=True) # 备注
|
||||||
|
|
||||||
|
# 时间
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {
|
||||||
|
'id': self.id,
|
||||||
|
'provider_name': self.provider_name,
|
||||||
|
'api_base': self.api_base,
|
||||||
|
'api_key': self.api_key,
|
||||||
|
'model': self.model,
|
||||||
|
'is_active': self.is_active,
|
||||||
|
'sort_order': self.sort_order,
|
||||||
|
'description': self.description,
|
||||||
|
'created_at': self.created_at.isoformat() if self.created_at else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# ==================== 邮件模板配置 ====================
|
# ==================== 邮件模板配置 ====================
|
||||||
class EmailTemplateConfig(db.Model):
|
class EmailTemplateConfig(db.Model):
|
||||||
"""邮件模板配置"""
|
"""邮件模板配置"""
|
||||||
|
|||||||
173
templates/admin/backup_llm.html
Normal file
173
templates/admin/backup_llm.html
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>备用大模型接口 - 后台管理</title>
|
||||||
|
<link rel="icon" href="/static/img/favicon.svg" type="image/svg+xml">
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
body { background-color: #f5f5f5; }
|
||||||
|
.sidebar { position: fixed; top: 0; left: 0; height: 100vh; width: 250px; background: #343a40; padding-top: 60px; }
|
||||||
|
.sidebar .nav-link { color: #adb5bd; padding: 12px 20px; }
|
||||||
|
.sidebar .nav-link:hover, .sidebar .nav-link.active { color: #fff; background: rgba(255,255,255,0.1); }
|
||||||
|
.main-content { margin-left: 250px; padding: 20px; }
|
||||||
|
.status-badge { font-size: 0.75rem; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav class="sidebar">
|
||||||
|
<div class="position-absolute top-0 w-100 p-3 border-bottom border-secondary">
|
||||||
|
<h5 class="text-white mb-0"><i class="bi bi-gear-fill"></i> 后台管理</h5>
|
||||||
|
</div>
|
||||||
|
<ul class="nav flex-column">
|
||||||
|
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.dashboard') }}"><i class="bi bi-speedometer2"></i> 数据概览</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.users') }}"><i class="bi bi-people"></i> 用户管理</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.translations') }}"><i class="bi bi-file-text"></i> 翻译记录</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.cache_list') }}"><i class="bi bi-database"></i> 缓存管理</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.stats') }}"><i class="bi bi-bar-chart"></i> 统计报表</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.logs') }}"><i class="bi bi-list-check"></i> 操作日志</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.llm_config') }}"><i class="bi bi-cpu"></i> 大模型配置</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link active" href="{{ url_for('admin.backup_llm_list') }}"><i class="bi bi-cloud"></i> 备用大模型</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.settings') }}"><i class="bi bi-sliders"></i> 系统配置</a></li>
|
||||||
|
</ul>
|
||||||
|
<div class="position-absolute bottom-0 w-100 p-3 border-top border-secondary">
|
||||||
|
<a href="/" class="btn btn-outline-light btn-sm w-100"><i class="bi bi-house"></i> 返回前台</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main class="main-content">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h5><i class="bi bi-cloud"></i> 备用大模型接口</h5>
|
||||||
|
<div>
|
||||||
|
<a href="{{ url_for('admin.add_backup_llm') }}" class="btn btn-primary"><i class="bi bi-plus-lg"></i> 新增接口</a>
|
||||||
|
<button class="btn btn-outline-secondary" onclick="initDefaults()"><i class="bi bi-arrow-counterclockwise"></i> 初始化默认</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th style="width: 50px;">#</th>
|
||||||
|
<th>服务商</th>
|
||||||
|
<th>API地址</th>
|
||||||
|
<th>模型</th>
|
||||||
|
<th style="width: 80px;">状态</th>
|
||||||
|
<th style="width: 150px;">操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for config in configs %}
|
||||||
|
<tr id="row-{{ config.id }}">
|
||||||
|
<td>{{ config.sort_order }}</td>
|
||||||
|
<td>
|
||||||
|
<strong>{{ config.provider_name }}</strong>
|
||||||
|
{% if config.description %}
|
||||||
|
<br><small class="text-muted">{{ config.description }}</small>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td><code>{{ config.api_base }}</code></td>
|
||||||
|
<td><code>{{ config.model or '默认' }}</code></td>
|
||||||
|
<td>
|
||||||
|
{% if config.is_active %}
|
||||||
|
<span class="badge bg-success status-badge">启用</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary status-badge">禁用</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-sm btn-outline-primary" onclick="testConfig({{ config.id }})" title="测试连接">
|
||||||
|
<i class="bi bi-plug"></i>
|
||||||
|
</button>
|
||||||
|
<a href="{{ url_for('admin.edit_backup_llm', config_id=config.id) }}" class="btn btn-sm btn-outline-secondary" title="编辑">
|
||||||
|
<i class="bi bi-pencil"></i>
|
||||||
|
</a>
|
||||||
|
<button class="btn btn-sm btn-outline-warning" onclick="toggleConfig({{ config.id }})" title="切换状态">
|
||||||
|
<i class="bi bi-toggle2"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-outline-danger" onclick="deleteConfig({{ config.id }})" title="删除">
|
||||||
|
<i class="bi bi-trash"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{% if not configs %}
|
||||||
|
<div class="text-center text-muted py-4">
|
||||||
|
<i class="bi bi-cloud-slash" style="font-size: 2rem;"></i>
|
||||||
|
<p>暂无备用大模型配置</p>
|
||||||
|
<button class="btn btn-outline-primary" onclick="initDefaults()">点击初始化默认配置</button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="testResult" class="mt-3" style="display:none;"></div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function testConfig(id) {
|
||||||
|
const resultDiv = document.getElementById('testResult');
|
||||||
|
resultDiv.style.display = 'block';
|
||||||
|
resultDiv.innerHTML = '<div class="alert alert-info"><i class="bi bi-hourglass-split"></i> 正在测试连接...</div>';
|
||||||
|
|
||||||
|
fetch(`/admin/backup-llm/${id}/test`, {method: 'POST'})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(res => {
|
||||||
|
if (res.success) {
|
||||||
|
resultDiv.innerHTML = `<div class="alert alert-success"><i class="bi bi-check-circle"></i> ${res.provider} 连接成功!模型: ${res.model}</div>`;
|
||||||
|
} else {
|
||||||
|
resultDiv.innerHTML = `<div class="alert alert-danger"><i class="bi bi-x-circle"></i> 连接失败: ${res.error}</div>`;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
resultDiv.innerHTML = `<div class="alert alert-danger"><i class="bi bi-x-circle"></i> 请求失败: ${err}</div>`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleConfig(id) {
|
||||||
|
fetch(`/admin/backup-llm/${id}/toggle`, {method: 'POST'})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(res => {
|
||||||
|
if (res.success) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert('操作失败');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteConfig(id) {
|
||||||
|
if (confirm('确定删除此备用大模型接口吗?')) {
|
||||||
|
fetch(`/admin/backup-llm/${id}/delete`, {method: 'POST'})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(res => {
|
||||||
|
if (res.success) {
|
||||||
|
document.getElementById(`row-${id}`).remove();
|
||||||
|
} else {
|
||||||
|
alert('删除失败');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initDefaults() {
|
||||||
|
if (confirm('初始化默认备用大模型配置?')) {
|
||||||
|
fetch('/admin/backup-llm/init', {method: 'POST'})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(res => {
|
||||||
|
if (res.success) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert('初始化失败');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
188
templates/admin/backup_llm_form.html
Normal file
188
templates/admin/backup_llm_form.html
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>{{ config ? '编辑' : '添加' }}备用大模型接口 - 后台管理</title>
|
||||||
|
<link rel="icon" href="/static/img/favicon.svg" type="image/svg+xml">
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
body { background-color: #f5f5f5; }
|
||||||
|
.sidebar { position: fixed; top: 0; left: 0; height: 100vh; width: 250px; background: #343a40; padding-top: 60px; }
|
||||||
|
.sidebar .nav-link { color: #adb5bd; padding: 12px 20px; }
|
||||||
|
.sidebar .nav-link:hover, .sidebar .nav-link.active { color: #fff; background: rgba(255,255,255,0.1); }
|
||||||
|
.main-content { margin-left: 250px; padding: 20px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav class="sidebar">
|
||||||
|
<div class="position-absolute top-0 w-100 p-3 border-bottom border-secondary">
|
||||||
|
<h5 class="text-white mb-0"><i class="bi bi-gear-fill"></i> 后台管理</h5>
|
||||||
|
</div>
|
||||||
|
<ul class="nav flex-column">
|
||||||
|
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.dashboard') }}"><i class="bi bi-speedometer2"></i> 数据概览</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.users') }}"><i class="bi bi-people"></i> 用户管理</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.translations') }}"><i class="bi bi-file-text"></i> 翻译记录</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.cache_list') }}"><i class="bi bi-database"></i> 缓存管理</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.stats') }}"><i class="bi bi-bar-chart"></i> 统计报表</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.logs') }}"><i class="bi bi-list-check"></i> 操作日志</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.llm_config') }}"><i class="bi bi-cpu"></i> 大模型配置</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link active" href="{{ url_for('admin.backup_llm_list') }}"><i class="bi bi-cloud"></i> 备用大模型</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.settings') }}"><i class="bi bi-sliders"></i> 系统配置</a></li>
|
||||||
|
</ul>
|
||||||
|
<div class="position-absolute bottom-0 w-100 p-3 border-top border-secondary">
|
||||||
|
<a href="/" class="btn btn-outline-light btn-sm w-100"><i class="bi bi-house"></i> 返回前台</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main class="main-content">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h6 class="mb-0"><i class="bi bi-cloud-plus"></i> {{ config ? '编辑备用大模型接口' : '添加备用大模型接口' }}</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form id="configForm" onsubmit="return saveConfig(event)">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">服务商名称 <span class="text-danger">*</span></label>
|
||||||
|
<input type="text" class="form-control" name="provider_name"
|
||||||
|
value="{{ config.provider_name if config else '' }}"
|
||||||
|
placeholder="例如: OpenAI, DeepSeek, 阿里百炼" required>
|
||||||
|
<small class="text-muted">显示名称,方便识别</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">API地址 <span class="text-danger">*</span></label>
|
||||||
|
<input type="text" class="form-control" name="api_base"
|
||||||
|
value="{{ config.api_base if config else '' }}"
|
||||||
|
placeholder="https://api.openai.com/v1" required>
|
||||||
|
<small class="text-muted">LLM服务的API endpoint</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">API Key</label>
|
||||||
|
<input type="text" class="form-control" name="api_key"
|
||||||
|
value="{{ config.api_key if config else '' }}"
|
||||||
|
placeholder="sk-xxx(可选)">
|
||||||
|
<small class="text-muted">如果不需要可留空</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">默认模型</label>
|
||||||
|
<input type="text" class="form-control" name="model"
|
||||||
|
value="{{ config.model if config else '' }}"
|
||||||
|
placeholder="gpt-4, deepseek-chat">
|
||||||
|
<small class="text-muted">推荐使用的模型ID</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">备注</label>
|
||||||
|
<input type="text" class="form-control" name="description"
|
||||||
|
value="{{ config.description if config else '' }}"
|
||||||
|
placeholder="接口说明">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">排序</label>
|
||||||
|
<input type="number" class="form-control" name="sort_order"
|
||||||
|
value="{{ config.sort_order if config else 0 }}">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">状态</label>
|
||||||
|
<select class="form-select" name="is_active">
|
||||||
|
<option value="true" {% if not config or config.is_active %}selected{% endif %}>启用</option>
|
||||||
|
<option value="false" {% if config and not config.is_active %}selected{% endif %}>禁用</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<button type="submit" class="btn btn-primary"><i class="bi bi-check-lg"></i> 保存</button>
|
||||||
|
<button type="button" class="btn btn-outline-secondary" onclick="testConfig()"><i class="bi bi-plug"></i> 测试连接</button>
|
||||||
|
<a href="{{ url_for('admin.backup_llm_list') }}" class="btn btn-outline-secondary"><i class="bi bi-arrow-left"></i> 返回</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div id="testResult" class="mt-3" style="display:none;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function saveConfig(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const formData = new FormData(document.getElementById('configForm'));
|
||||||
|
const data = {};
|
||||||
|
formData.forEach((value, key) => {
|
||||||
|
if (key === 'sort_order') {
|
||||||
|
data[key] = parseInt(value) || 0;
|
||||||
|
} else if (key === 'is_active') {
|
||||||
|
data[key] = value === 'true';
|
||||||
|
} else {
|
||||||
|
data[key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
{% if config %}
|
||||||
|
const url = `/admin/backup-llm/{{ config.id }}/edit`;
|
||||||
|
{% else %}
|
||||||
|
const url = '/admin/backup-llm/add';
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(res => {
|
||||||
|
if (res.success) {
|
||||||
|
alert('保存成功!');
|
||||||
|
window.location.href = '/admin/backup-llm';
|
||||||
|
} else {
|
||||||
|
alert('保存失败: ' + (res.error || '未知错误'));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => alert('请求失败: ' + err));
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function testConfig() {
|
||||||
|
const formData = new FormData(document.getElementById('configForm'));
|
||||||
|
const resultDiv = document.getElementById('testResult');
|
||||||
|
resultDiv.style.display = 'block';
|
||||||
|
resultDiv.innerHTML = '<div class="alert alert-info"><i class="bi bi-hourglass-split"></i> 正在测试连接...</div>';
|
||||||
|
|
||||||
|
// 先临时保存,然后测试
|
||||||
|
{% if config %}
|
||||||
|
// 已有配置,直接测试
|
||||||
|
fetch(`/admin/backup-llm/{{ config.id }}/test`, {method: 'POST'})
|
||||||
|
{% else %}
|
||||||
|
// 新配置,需要先创建
|
||||||
|
const data = {};
|
||||||
|
formData.forEach((value, key) => {
|
||||||
|
if (key === 'sort_order') data[key] = parseInt(value) || 0;
|
||||||
|
else if (key === 'is_active') data[key] = value === 'true';
|
||||||
|
else data[key] = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 直接用输入的数据测试(不保存)
|
||||||
|
resultDiv.innerHTML = '<div class="alert alert-warning"><i class="bi bi-info-circle"></i> 新配置请先保存后再测试</div>';
|
||||||
|
return;
|
||||||
|
{% endif %}
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(res => {
|
||||||
|
if (res.success) {
|
||||||
|
resultDiv.innerHTML = `<div class="alert alert-success"><i class="bi bi-check-circle"></i> ${res.provider} 连接成功!模型: ${res.model}</div>`;
|
||||||
|
} else {
|
||||||
|
resultDiv.innerHTML = `<div class="alert alert-danger"><i class="bi bi-x-circle"></i> 连接失败: ${res.error}</div>`;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
resultDiv.innerHTML = `<div class="alert alert-danger"><i class="bi bi-x-circle"></i> 请求失败: ${err}</div>`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -90,20 +90,17 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card mt-3">
|
<div class="card mt-3">
|
||||||
<div class="card-header"><h6 class="mb-0"><i class="bi bi-info-circle"></i> 常用模型配置参考</h6></div>
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h6 class="mb-0"><i class="bi bi-cloud"></i> 备用大模型接口</h6>
|
||||||
|
<a href="{{ url_for('admin.backup_llm_list') }}" class="btn btn-sm btn-outline-primary">
|
||||||
|
<i class="bi bi-gear"></i> 管理备用接口
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<table class="table table-sm">
|
<p class="text-muted">备用大模型接口用于主接口不可用时切换备用服务。支持手动新增、编辑、测试连接。</p>
|
||||||
<thead class="table-light">
|
<a href="{{ url_for('admin.backup_llm_list') }}" class="btn btn-outline-secondary">
|
||||||
<tr><th>服务商</th><th>API地址</th><th>模型示例</th></tr>
|
<i class="bi bi-list"></i> 查看所有备用接口
|
||||||
</thead>
|
</a>
|
||||||
<tbody>
|
|
||||||
<tr><td>本地LM Studio</td><td>http://localhost:1234/v1</td><td>根据加载的模型</td></tr>
|
|
||||||
<tr><td>OpenAI</td><td>https://api.openai.com/v1</td><td>gpt-4, gpt-3.5-turbo</td></tr>
|
|
||||||
<tr><td>DeepSeek</td><td>https://api.deepseek.com/v1</td><td>deepseek-chat</td></tr>
|
|
||||||
<tr><td>阿里百炼</td><td>https://dashscope.aliyuncs.com/compatible-mode/v1</td><td>qwen-turbo, qwen-plus</td></tr>
|
|
||||||
<tr><td>SiliconFlow</td><td>https://api.siliconflow.cn/v1</td><td>Qwen/Qwen2.5-72B-Instruct</td></tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
Reference in New Issue
Block a user