Files
ai-chat-system/templates/admin_v2/index.html

821 lines
54 KiB
HTML
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.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI对话系统 v2.0 - 后台管理</title>
<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/remixicon@3.5.0/fonts/remixicon.css" rel="stylesheet">
<style>
:root { --sidebar-width: 240px; }
body { background: #f5f7fa; }
.sidebar { position: fixed; left: 0; top: 0; width: var(--sidebar-width); height: 100vh; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px 0; }
.sidebar .logo { padding: 0 20px 30px; border-bottom: 1px solid rgba(255,255,255,0.1); }
.sidebar .logo h4 { margin: 0; font-size: 18px; }
.sidebar .logo .version { font-size: 12px; color: rgba(255,255,255,0.7); }
.sidebar .nav-item { padding: 10px 20px; cursor: pointer; transition: all 0.2s; }
.sidebar .nav-item:hover { background: rgba(255,255,255,0.1); }
.sidebar .nav-item.active { background: rgba(255,255,255,0.2); border-left: 3px solid white; }
.sidebar .nav-item i { margin-right: 10px; }
.main-content { margin-left: var(--sidebar-width); padding: 30px; }
.page-header { margin-bottom: 30px; }
.page-header h2 { margin: 0; font-size: 24px; }
.card { border: none; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.08); }
.card-header { background: white; border-bottom: 1px solid #eee; padding: 15px 20px; }
.btn-primary { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border: none; }
.btn-primary:hover { background: linear-gradient(135deg, #5a6fd6 0%, #6a4190 100%); }
.table th { background: #f8f9fa; font-weight: 600; }
.page-section { display: none; }
.page-section.active { display: block; }
.channel-web { border-left: 4px solid #17a2b8; }
.channel-matrix { border-left: 4px solid #28a745; }
.thinking-config { background: #f8f9fa; padding: 15px; border-radius: 8px; margin-top: 10px; }
</style>
</head>
<body>
<div class="sidebar">
<div class="logo">
<h4><i class="ri-robot-line"></i> AI对话系统</h4>
<div class="version">v2.0.0 后台管理</div>
</div>
<div class="nav mt-3">
<div class="nav-item active" data-page="llm-providers"><i class="ri-cloud-line"></i> 大模型池</div>
<div class="nav-item" data-page="agents"><i class="ri-robot-2-line"></i> Agent管理</div>
<div class="nav-item" data-page="channels"><i class="ri-chat-3-line"></i> 渠道管理</div>
<div class="nav-item" data-page="tools"><i class="ri-tools-line"></i> 工具管理</div>
<div class="nav-item" data-page="stats"><i class="ri-bar-chart-box-line"></i> 统计数据</div>
</div>
</div>
<div class="main-content">
<!-- 大模型池 -->
<div class="page-section active" id="page-llm-providers">
<div class="page-header"><h2><i class="ri-cloud-line"></i> 大模型池管理</h2></div>
<div class="card">
<div class="card-header d-flex justify-content-between">
<span>大模型提供商列表</span>
<button class="btn btn-primary btn-sm" onclick="showAddProviderModal()"><i class="ri-add-line"></i> 添加</button>
</div>
<div class="card-body">
<table class="table">
<thead><tr><th>名称</th><th>API地址</th><th>默认模型</th><th>思考</th><th>视觉</th><th>FC</th><th>状态</th><th>操作</th></tr></thead>
<tbody id="providers-list"><tr><td colspan="8" class="text-center">加载中...</td></tr></tbody>
</table>
</div>
</div>
</div>
<!-- Agent -->
<div class="page-section" id="page-agents">
<div class="page-header"><h2><i class="ri-robot-2-line"></i> Agent管理</h2></div>
<div class="card">
<div class="card-header d-flex justify-content-between">
<span>Agent列表</span>
<button class="btn btn-primary btn-sm" onclick="showAddAgentModal()"><i class="ri-add-line"></i> 添加</button>
</div>
<div class="card-body">
<table class="table">
<thead><tr><th>名称</th><th>显示名</th><th>大模型池</th><th>思考</th><th>默认</th><th>状态</th><th>操作</th></tr></thead>
<tbody id="agents-list"><tr><td colspan="7" class="text-center">加载中...</td></tr></tbody>
</table>
</div>
</div>
</div>
<!-- 渠道 -->
<div class="page-section" id="page-channels">
<div class="page-header"><h2><i class="ri-chat-3-line"></i> 渠道管理</h2></div>
<div class="row">
<div class="col-md-6 mb-4">
<div class="card channel-web">
<div class="card-header"><i class="ri-global-line"></i> 网页端渠道</div>
<div class="card-body" id="web-channel-info">加载中...</div>
</div>
</div>
<div class="col-md-6 mb-4">
<div class="card channel-matrix">
<div class="card-header"><i class="ri-message-3-line"></i> Matrix渠道</div>
<div class="card-body" id="matrix-channel-info">加载中...</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">渠道-Agent绑定关系</div>
<div class="card-body">
<table class="table">
<thead><tr><th>渠道</th><th>绑定Agent</th><th>优先级</th><th>模式</th><th>条件</th><th>操作</th></tr></thead>
<tbody id="mappings-list"><tr><td colspan="6" class="text-center">加载中...</td></tr></tbody>
</table>
</div>
</div>
</div>
<!-- 工具管理 -->
<div class="page-section" id="page-tools">
<div class="page-header"><h2><i class="ri-tools-line"></i> 工具管理</h2></div>
<div class="row">
<div class="col-md-8">
<div class="card">
<div class="card-header d-flex justify-content-between">
<span>工具列表(搜索、计算器、代码执行等)</span>
<button class="btn btn-primary btn-sm" onclick="showAddToolModal()"><i class="ri-add-line"></i> 添加工具</button>
</div>
<div class="card-body">
<table class="table">
<thead><tr><th>名称</th><th>类型</th><th>提供商</th><th>调用次数</th><th>成功率</th><th>默认</th><th>状态</th><th>操作</th></tr></thead>
<tbody id="tools-list"><tr><td colspan="8" class="text-center">加载中...</td></tr></tbody>
</table>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header"><i class="ri-bar-chart-line"></i> 工具使用统计近7天</div>
<div class="card-body" id="tool-stats">加载中...</div>
</div>
</div>
</div>
</div>
<!-- 统计 -->
<div class="page-section" id="page-stats">
<div class="page-header"><h2><i class="ri-bar-chart-box-line"></i> 统计数据</h2></div>
<div class="row">
<div class="col-md-3 mb-4"><div class="card"><div class="card-body text-center"><i class="ri-user-line" style="font-size:40px;color:#667eea;"></i><h3 id="stat-users" class="mt-2">-</h3><p class="text-muted">用户数</p></div></div></div>
<div class="col-md-3 mb-4"><div class="card"><div class="card-body text-center"><i class="ri-chat-1-line" style="font-size:40px;color:#28a745;"></i><h3 id="stat-conversations" class="mt-2">-</h3><p class="text-muted">对话数</p></div></div></div>
<div class="col-md-3 mb-4"><div class="card"><div class="card-body text-center"><i class="ri-message-2-line" style="font-size:40px;color:#ffc107;"></i><h3 id="stat-messages" class="mt-2">-</h3><p class="text-muted">消息数</p></div></div></div>
<div class="col-md-3 mb-4"><div class="card"><div class="card-body text-center"><i class="ri-robot-line" style="font-size:40px;color:#17a2b8;"></i><h3 id="stat-agents" class="mt-2">-</h3><p class="text-muted">Agent数</p></div></div></div>
</div>
</div>
</div>
<!-- Provider Modal -->
<div class="modal fade" id="providerModal"><div class="modal-dialog modal-lg"><div class="modal-content">
<div class="modal-header"><h5 class="modal-title">添加/编辑大模型</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div>
<div class="modal-body"><form id="provider-form">
<input type="hidden" id="provider-id">
<div class="row"><div class="col-md-6"><label class="form-label">名称 *</label><input type="text" class="form-control" id="provider-name" required></div><div class="col-md-6"><label class="form-label">默认模型</label><input type="text" class="form-control" id="provider-default-model"></div></div>
<div class="row mt-3"><div class="col-md-12"><label class="form-label">API地址 *</label><input type="text" class="form-control" id="provider-api-base" required></div></div>
<div class="row mt-3"><div class="col-md-12"><label class="form-label">API密钥</label><input type="text" class="form-control" id="provider-api-key"></div></div>
<div class="row mt-3"><div class="col-md-4"><label class="form-label">MaxTokens</label><input type="number" class="form-control" id="provider-max-tokens" value="4096"></div><div class="col-md-4"><label class="form-label">Temperature</label><input type="number" class="form-control" id="provider-temperature" value="0.7" step="0.1"></div><div class="col-md-4"><label class="form-label">优先级</label><input type="number" class="form-control" id="provider-priority" value="0"></div></div>
<div class="mt-3"><label class="form-label">描述</label><textarea class="form-control" id="provider-description" rows="2"></textarea></div>
<div class="mt-3 form-check"><input type="checkbox" class="form-check-input" id="provider-active" checked><label class="form-check-label">启用</label></div>
<hr><h6>思考功能</h6>
<div class="thinking-config"><div class="row"><div class="col-md-6 form-check"><input type="checkbox" class="form-check-input" id="provider-supports-thinking"><label class="form-check-label">支持原生思考</label></div><div class="col-md-6"><label class="form-label">思考模型名</label><input type="text" class="form-control" id="provider-thinking-model"></div></div></div>
<hr><h6>视觉能力</h6>
<div class="thinking-config"><div class="row"><div class="col-md-6 form-check"><input type="checkbox" class="form-check-input" id="provider-supports-vision"><label class="form-check-label">支持图片理解</label></div><div class="col-md-6"><label class="form-label">视觉模型名</label><input type="text" class="form-control" id="provider-vision-model" placeholder="留空则使用默认模型"></div></div><small class="text-muted mt-2 d-block">启用后可上传图片让AI识别分析内容</small></div>
<hr><h6>Function Calling</h6>
<div class="thinking-config"><div class="form-check"><input type="checkbox" class="form-check-input" id="provider-supports-function-calling"><label class="form-check-label">支持函数调用</label></div><small class="text-muted mt-2 d-block">启用后LLM可自主决定何时调用工具更智能</small></div>
<div class="mt-3"><button type="button" class="btn btn-outline-primary" onclick="fetchProviderModels()"><i class="ri-refresh-line"></i> 获取模型</button><button type="button" class="btn btn-outline-secondary" onclick="testProviderConnection()"><i class="ri-link"></i> 测试连接</button></div>
<div class="mt-2" id="provider-models-preview"></div><div class="mt-2" id="provider-test-result"></div>
</form></div>
<div class="modal-footer"><button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button><button type="button" class="btn btn-primary" onclick="saveProvider()">保存</button></div>
</div></div></div>
<!-- Agent Modal -->
<div class="modal fade" id="agentModal"><div class="modal-dialog modal-lg"><div class="modal-content">
<div class="modal-header"><h5 class="modal-title">添加/编辑Agent</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div>
<div class="modal-body"><form id="agent-form">
<input type="hidden" id="agent-id">
<div class="row"><div class="col-md-4"><label class="form-label">名称 *</label><input type="text" class="form-control" id="agent-name" required></div><div class="col-md-4"><label class="form-label">显示名</label><input type="text" class="form-control" id="agent-display-name"></div><div class="col-md-4"><label class="form-label">大模型池 *</label><select class="form-select" id="agent-llm-provider" required><option value="">请选择...</option></select></div></div>
<div class="row mt-3"><div class="col-md-6"><label class="form-label">模型覆盖</label><input type="text" class="form-control" id="agent-model-override"></div><div class="col-md-6"><label class="form-label">最大历史</label><input type="number" class="form-control" id="agent-max-history" value="20"></div></div>
<div class="mt-3"><label class="form-label">系统设定</label><textarea class="form-control" id="agent-system-prompt" rows="3">你是一个有用的AI助手。</textarea></div>
<div class="mt-3"><label class="form-label">描述</label><textarea class="form-control" id="agent-description" rows="2"></textarea></div>
<div class="row mt-3"><div class="col-md-4 form-check"><input type="checkbox" class="form-check-input" id="agent-active" checked><label class="form-check-label">启用</label></div><div class="col-md-4 form-check"><input type="checkbox" class="form-check-input" id="agent-default"><label class="form-check-label">默认</label></div><div class="col-md-4"><label class="form-label">温度覆盖</label><input type="number" class="form-control" id="agent-temperature-override" step="0.1"></div></div>
<hr><h6>思考功能</h6>
<div class="thinking-config"><div class="form-check"><input type="checkbox" class="form-check-input" id="agent-enable-thinking" checked><label class="form-check-label">启用思考</label></div><div class="mt-2"><label class="form-label">思考提示词</label><textarea class="form-control" id="agent-thinking-prompt" rows="2"></textarea></div><div class="row mt-2"><div class="col-md-6"><label class="form-label">前缀</label><input type="text" class="form-control" id="agent-thinking-prefix"></div><div class="col-md-6"><label class="form-label">后缀</label><input type="text" class="form-control" id="agent-thinking-suffix"></div></div></div>
<hr><h6>工具配置</h6>
<div class="thinking-config">
<div id="agent-tools-list">加载中...</div>
<small class="text-muted">从系统工具列表中选择Agent可使用的工具</small>
</div>
</form></div>
<div class="modal-footer"><button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button><button type="button" class="btn btn-primary" onclick="saveAgent()">保存</button></div>
</div></div></div>
<!-- Binding Modal -->
<div class="modal fade" id="bindingModal"><div class="modal-dialog"><div class="modal-content">
<div class="modal-header"><h5 class="modal-title">绑定Agent</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div>
<div class="modal-body"><form id="binding-form">
<input type="hidden" id="binding-channel-id">
<div class="mb-3"><label class="form-label">选择Agent</label><select class="form-select" id="binding-agent-id" required><option value="">请选择...</option></select></div>
<div class="mb-3"><label class="form-label">优先级</label><input type="number" class="form-control" id="binding-priority" value="0"></div>
<div class="mb-3"><label class="form-label">模式</label><select class="form-select" id="binding-mode"><option value="single">single</option><option value="round_robin">轮询</option><option value="fallback">备用</option></select></div>
<div class="mb-3"><label class="form-label">条件(JSON)</label><textarea class="form-control" id="binding-conditions" rows="2"></textarea></div>
</form></div>
<div class="modal-footer"><button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button><button type="button" class="btn btn-primary" onclick="saveBinding()">保存</button></div>
</div></div></div>
<!-- Matrix Config Modal -->
<div class="modal fade" id="matrixConfigModal"><div class="modal-dialog"><div class="modal-content">
<div class="modal-header"><h5 class="modal-title">Matrix配置</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div>
<div class="modal-body"><form id="matrix-config-form">
<div class="mb-3"><label class="form-label">服务器地址</label><input type="text" class="form-control" id="matrix-homeserver"></div>
<div class="mb-3"><label class="form-label">Bot用户名</label><input type="text" class="form-control" id="matrix-username"></div>
<div class="mb-3"><label class="form-label">密码</label><input type="text" class="form-control" id="matrix-password"></div>
</form></div>
<div class="modal-footer"><button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button><button type="button" class="btn btn-primary" onclick="saveMatrixConfig()">保存</button></div>
</div></div></div>
<!-- Tool Modal -->
<div class="modal fade" id="toolModal"><div class="modal-dialog modal-lg"><div class="modal-content">
<div class="modal-header"><h5 class="modal-title">添加/编辑工具</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div>
<div class="modal-body"><form id="tool-form">
<input type="hidden" id="tool-id">
<div class="row">
<div class="col-md-6"><label class="form-label">名称 *</label><input type="text" class="form-control" id="tool-name" required></div>
<div class="col-md-6"><label class="form-label">类型 *</label><select class="form-select" id="tool-type" onchange="updateToolForm()">
<option value="search">搜索工具</option>
<option value="calculator">计算器</option>
<option value="code_runner">代码执行</option>
<option value="image_gen">图像生成</option>
<option value="translator">翻译</option>
<option value="weather">天气查询</option>
<option value="custom">自定义</option>
</select></div>
</div>
<div class="row mt-3">
<div class="col-md-6"><label class="form-label">提供商</label><select class="form-select" id="tool-provider">
<option value=""></option>
<option value="tavily">Tavily</option>
<option value="google">Google</option>
<option value="bing">Bing</option>
<option value="wolfram">Wolfram</option>
<option value="openai">OpenAI</option>
<option value="anthropic">Anthropic</option>
<option value="custom">自定义</option>
</select></div>
<div class="col-md-6"><label class="form-label">API Key</label><input type="text" class="form-control" id="tool-api-key"></div>
</div>
<div class="mt-3"><label class="form-label">配置参数JSON格式</label><textarea class="form-control" id="tool-config" rows="3">{"max_results": 5, "search_depth": "basic"}</textarea></div>
<div class="mt-3 form-check"><input type="checkbox" class="form-check-input" id="tool-active" checked><label class="form-check-label">启用</label></div>
<div class="mt-2 form-check"><input type="checkbox" class="form-check-input" id="tool-default"><label class="form-check-label">设为该类型默认</label></div>
</form></div>
<div class="modal-footer"><button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button><button type="button" class="btn btn-primary" onclick="saveTool()">保存</button></div>
</div></div></div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
// 全局数据
let providersData = [], agentsData = [], channelsData = [], toolsData = [];
// 页面切换函数
function loadPageData(page) {
if (page === 'llm-providers') loadProviders();
if (page === 'agents') loadAgents();
if (page === 'channels') loadChannels();
if (page === 'tools') loadTools();
if (page === 'stats') loadStats();
}
// 加载工具列表用于Agent配置
async function loadToolsList() {
try {
const res = await fetch('/api/v2/tools');
const data = await res.json();
toolsData = data.tools || [];
renderAgentToolsList();
} catch (e) { console.error('加载工具列表失败:', e); }
}
// 渲染Agent工具选择列表
function renderAgentToolsList(selectedTools = []) {
const container = document.getElementById('agent-tools-list');
if (!container) return;
if (toolsData.length === 0) {
container.innerHTML = '<div class="text-muted">暂无可用工具,请先在「工具管理」中添加</div>';
return;
}
container.innerHTML = toolsData.filter(t => t.is_active).map(t => {
const toolType = t.tool_type || 'unknown';
const isSelected = selectedTools.includes(toolType);
const icon = getToolIcon(toolType);
return `<div class="form-check">
<input type="checkbox" class="form-check-input agent-tool-checkbox" id="agent-tool-${t.id}" value="${toolType}" ${isSelected ? 'checked' : ''}>
<label class="form-check-label" for="agent-tool-${t.id}">
<i class="${icon}"></i> ${t.name} <small class="text-muted">(${toolType})</small>
</label>
</div>`;
}).join('');
}
// 工具图标
function getToolIcon(toolType) {
const icons = {
'search': 'ri-search-line',
'calculator': 'ri-calculator-line',
'code': 'ri-code-line',
'image': 'ri-image-line',
'web': 'ri-global-line'
};
return icons[toolType] || 'ri-tools-line';
}
// 获取Agent选中的工具列表
function getSelectedAgentTools() {
const checkboxes = document.querySelectorAll('.agent-tool-checkbox:checked');
return Array.from(checkboxes).map(cb => cb.value);
}
// ===== 大模型池 =====
async function loadProviders() {
const res = await fetch('/api/v2/providers');
const data = await res.json();
providersData = data.providers || [];
const tbody = document.getElementById('providers-list');
if (providersData.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" class="text-center text-muted">暂无数据</td></tr>';
return;
}
tbody.innerHTML = providersData.map(p => `<tr>
<td><strong>${p.name}</strong></td><td><small>${p.api_base||'-'}</small></td><td>${p.default_model||'auto'}</td>
<td>${p.supports_thinking?'<span class="badge bg-success">支持</span>':'<span class="badge bg-secondary">不支持</span>'}</td>
<td>${p.supports_vision?'<span class="badge bg-info">支持</span>':'<span class="badge bg-secondary">不支持</span>'}</td>
<td>${p.supports_function_calling?'<span class="badge bg-primary">支持</span>':'<span class="badge bg-secondary">不支持</span>'}</td>
<td>${p.is_active?'<span class="badge bg-success">启用</span>':'<span class="badge bg-secondary">禁用</span>'}</td>
<td><button class="btn btn-sm btn-outline-primary" onclick="editProvider(${p.id})"><i class="ri-edit-line"></i></button>
<button class="btn btn-sm btn-outline-danger" onclick="deleteProvider(${p.id},'${p.name}')"><i class="ri-delete-bin-line"></i></button></td>
</tr>`).join('');
updateProviderSelect();
}
function updateProviderSelect() {
const select = document.getElementById('agent-llm-provider');
select.innerHTML = '<option value="">请选择...</option>' + providersData.filter(p=>p.is_active).map(p=>`<option value="${p.id}">${p.name}</option>`).join('');
}
function showAddProviderModal() {
document.getElementById('provider-form').reset();
document.getElementById('provider-id').value = '';
document.getElementById('provider-active').checked = true;
document.getElementById('provider-supports-thinking').checked = false;
document.getElementById('provider-supports-vision').checked = false;
document.getElementById('provider-supports-function-calling').checked = false;
document.getElementById('provider-models-preview').innerHTML = '';
document.getElementById('provider-test-result').innerHTML = '';
new bootstrap.Modal(document.getElementById('providerModal')).show();
}
function editProvider(id) {
const p = providersData.find(x => x.id === id);
if (!p) return;
document.getElementById('provider-id').value = id;
document.getElementById('provider-name').value = p.name;
document.getElementById('provider-api-base').value = p.api_base || '';
document.getElementById('provider-api-key').value = p.api_key || '';
document.getElementById('provider-default-model').value = p.default_model || '';
document.getElementById('provider-max-tokens').value = p.max_tokens || 4096;
document.getElementById('provider-temperature').value = p.temperature || 0.7;
document.getElementById('provider-priority').value = p.priority || 0;
document.getElementById('provider-description').value = p.description || '';
document.getElementById('provider-active').checked = p.is_active;
document.getElementById('provider-supports-thinking').checked = p.supports_thinking;
document.getElementById('provider-thinking-model').value = p.thinking_model || '';
document.getElementById('provider-supports-vision').checked = p.supports_vision;
document.getElementById('provider-vision-model').value = p.vision_model || '';
document.getElementById('provider-supports-function-calling').checked = p.supports_function_calling;
new bootstrap.Modal(document.getElementById('providerModal')).show();
}
async function saveProvider() {
const id = document.getElementById('provider-id').value;
const data = {
name: document.getElementById('provider-name').value,
api_base: document.getElementById('provider-api-base').value,
api_key: document.getElementById('provider-api-key').value,
default_model: document.getElementById('provider-default-model').value || 'auto',
max_tokens: parseInt(document.getElementById('provider-max-tokens').value) || 4096,
temperature: parseFloat(document.getElementById('provider-temperature').value) || 0.7,
priority: parseInt(document.getElementById('provider-priority').value) || 0,
description: document.getElementById('provider-description').value,
is_active: document.getElementById('provider-active').checked,
supports_thinking: document.getElementById('provider-supports-thinking').checked,
thinking_model: document.getElementById('provider-thinking-model').value,
supports_vision: document.getElementById('provider-supports-vision').checked,
vision_model: document.getElementById('provider-vision-model').value,
supports_function_calling: document.getElementById('provider-supports-function-calling').checked
};
const res = await fetch(id ? `/api/v2/providers/${id}` : '/api/v2/providers', { method: id ? 'PUT' : 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify(data) });
const result = await res.json();
if (result.success) { bootstrap.Modal.getInstance(document.getElementById('providerModal')).hide(); loadProviders(); alert('保存成功'); }
else alert('失败: ' + result.message);
}
async function deleteProvider(id, name) {
if (!confirm(`确定删除 "${name}"`)) return;
const res = await fetch(`/api/v2/providers/${id}`, {method:'DELETE'});
const result = await res.json();
if (result.success) { loadProviders(); alert('删除成功'); }
else alert('失败: ' + result.message);
}
async function fetchProviderModels() {
const api_base = document.getElementById('provider-api-base').value;
if (!api_base) { alert('请先填写API地址'); return; }
document.getElementById('provider-models-preview').innerHTML = '获取中...';
const res = await fetch('/api/v2/providers/models', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({api_base, api_key: document.getElementById('provider-api-key').value}) });
const result = await res.json();
document.getElementById('provider-models-preview').innerHTML = result.models?.length > 0
? '<strong>可用模型:</strong> ' + result.models.map(m=>`<span class="badge bg-secondary">${m.id}</span>`).join(' ')
: '<span class="text-warning">无法获取</span>';
}
async function testProviderConnection() {
const api_base = document.getElementById('provider-api-base').value;
if (!api_base) { alert('请先填写API地址'); return; }
document.getElementById('provider-test-result').innerHTML = '测试中...';
const res = await fetch('/api/v2/providers/test', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({api_base, api_key: document.getElementById('provider-api-key').value, model: document.getElementById('provider-default-model').value||'auto'}) });
const result = await res.json();
document.getElementById('provider-test-result').innerHTML = result.success
? '<span class="text-success">✅ ' + result.message + '</span>'
: '<span class="text-danger">❌ ' + result.message + '</span>';
}
// ===== Agent =====
async function loadAgents() {
const res = await fetch('/api/v2/agents');
const data = await res.json();
agentsData = data.agents || [];
const tbody = document.getElementById('agents-list');
if (agentsData.length === 0) { tbody.innerHTML = '<tr><td colspan="7" class="text-center text-muted">暂无数据</td></tr>'; return; }
tbody.innerHTML = agentsData.map(a => `<tr>
<td><strong>${a.name}</strong></td><td>${a.display_name||a.name}</td><td>${a.llm_provider_name||'-'}</td>
<td>${a.enable_thinking?'<span class="badge bg-success">开启</span>':'<span class="badge bg-secondary">关闭</span>'}</td>
<td>${a.is_default?'<span class="badge bg-warning">默认</span>':'-'}</td>
<td>${a.is_active?'<span class="badge bg-success">启用</span>':'<span class="badge bg-secondary">禁用</span>'}</td>
<td><button class="btn btn-sm btn-outline-primary" onclick="editAgent(${a.id})"><i class="ri-edit-line"></i></button>
${!a.is_default?`<button class="btn btn-sm btn-outline-warning" onclick="setDefaultAgent(${a.id})"><i class="ri-star-line"></i></button><button class="btn btn-sm btn-outline-danger" onclick="deleteAgent(${a.id},'${a.name}')"><i class="ri-delete-bin-line"></i></button>`:''}</td>
</tr>`).join('');
updateAgentSelect();
}
function updateAgentSelect() {
const select = document.getElementById('binding-agent-id');
select.innerHTML = '<option value="">请选择...</option>' + agentsData.filter(a=>a.is_active).map(a=>`<option value="${a.id}">${a.display_name||a.name}</option>`).join('');
}
function showAddAgentModal() {
document.getElementById('agent-form').reset();
document.getElementById('agent-id').value = '';
document.getElementById('agent-active').checked = true;
document.getElementById('agent-enable-thinking').checked = true;
document.getElementById('agent-system-prompt').value = '你是一个有用的AI助手。';
updateProviderSelect();
loadToolsList(); // 加载工具列表
new bootstrap.Modal(document.getElementById('agentModal')).show();
}
function editAgent(id) {
const a = agentsData.find(x => x.id === id);
if (!a) return;
updateProviderSelect();
document.getElementById('agent-id').value = id;
document.getElementById('agent-name').value = a.name;
document.getElementById('agent-display-name').value = a.display_name || '';
document.getElementById('agent-llm-provider').value = a.llm_provider_id;
document.getElementById('agent-model-override').value = a.model_override || '';
document.getElementById('agent-max-history').value = a.max_history || 20;
document.getElementById('agent-system-prompt').value = a.system_prompt || '';
document.getElementById('agent-description').value = a.description || '';
document.getElementById('agent-active').checked = a.is_active;
document.getElementById('agent-default').checked = a.is_default;
document.getElementById('agent-temperature-override').value = a.temperature_override || '';
document.getElementById('agent-enable-thinking').checked = a.enable_thinking;
document.getElementById('agent-thinking-prompt').value = a.thinking_prompt || '';
document.getElementById('agent-thinking-prefix').value = a.thinking_prefix || '';
document.getElementById('agent-thinking-suffix').value = a.thinking_suffix || '';
// 工具配置 - 从工具列表渲染并勾选Agent已有的工具
loadToolsList();
setTimeout(() => renderAgentToolsList(a.tools || []), 100); // 等工具列表加载完成
new bootstrap.Modal(document.getElementById('agentModal')).show();
}
async function saveAgent() {
const id = document.getElementById('agent-id').value;
// 获取选中的工具列表
const tools = getSelectedAgentTools();
const data = {
name: document.getElementById('agent-name').value,
display_name: document.getElementById('agent-display-name').value,
llm_provider_id: parseInt(document.getElementById('agent-llm-provider').value),
model_override: document.getElementById('agent-model-override').value,
max_history: parseInt(document.getElementById('agent-max-history').value) || 20,
system_prompt: document.getElementById('agent-system-prompt').value,
description: document.getElementById('agent-description').value,
is_active: document.getElementById('agent-active').checked,
is_default: document.getElementById('agent-default').checked,
temperature_override: parseFloat(document.getElementById('agent-temperature-override').value) || null,
enable_thinking: document.getElementById('agent-enable-thinking').checked,
thinking_prompt: document.getElementById('agent-thinking-prompt').value,
thinking_prefix: document.getElementById('agent-thinking-prefix').value,
thinking_suffix: document.getElementById('agent-thinking-suffix').value,
tools: tools // 工具列表(从系统工具中选择)
};
const res = await fetch(id ? `/api/v2/agents/${id}` : '/api/v2/agents', { method: id ? 'PUT' : 'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(data) });
const result = await res.json();
if (result.success) { bootstrap.Modal.getInstance(document.getElementById('agentModal')).hide(); loadAgents(); alert('保存成功'); }
else alert('失败: ' + result.message);
}
async function setDefaultAgent(id) {
const res = await fetch(`/api/v2/agents/${id}/default`, {method:'POST'});
const result = await res.json();
if (result.success) { loadAgents(); alert('已设为默认'); }
}
async function deleteAgent(id, name) {
if (!confirm(`确定删除 "${name}"`)) return;
const res = await fetch(`/api/v2/agents/${id}`, {method:'DELETE'});
const result = await res.json();
if (result.success) { loadAgents(); alert('删除成功'); }
else alert('失败: ' + result.message);
}
// ===== 渠道 =====
async function loadChannels() {
const res = await fetch('/api/v2/channels');
const data = await res.json();
channelsData = data.channels || [];
const mappings = data.mappings || [];
const webChannel = channelsData.find(c => c.channel_type === 'web');
const matrixChannel = channelsData.find(c => c.channel_type === 'matrix');
document.getElementById('web-channel-info').innerHTML = renderChannelInfo(webChannel, 'web');
document.getElementById('matrix-channel-info').innerHTML = renderChannelInfo(matrixChannel, 'matrix');
const tbody = document.getElementById('mappings-list');
if (mappings.length === 0) { tbody.innerHTML = '<tr><td colspan="6" class="text-center text-muted">暂无绑定</td></tr>'; return; }
tbody.innerHTML = mappings.map(m => `<tr>
<td><span class="badge bg-info">${m.channel_name}</span></td><td>${m.agent_name}</td><td>${m.priority}</td>
<td><span class="badge bg-secondary">${m.mode}</span></td><td>${m.conditions?JSON.stringify(m.conditions):'-'}</td>
<td><button class="btn btn-sm btn-outline-danger" onclick="deleteMapping(${m.id})"><i class="ri-delete-bin-line"></i></button></td>
</tr>`).join('');
}
function renderChannelInfo(channel, type) {
if (!channel) return '<p class="text-muted">未配置</p>';
const agentsHtml = channel.agent_mappings?.length > 0
? channel.agent_mappings.map(m => `<span class="badge bg-secondary">${m.agent.display_name}</span>`).join(' ')
: '<span class="text-muted">未绑定</span>';
const configBtn = type === 'matrix'
? `<button class="btn btn-sm btn-outline-primary mt-2" onclick="editMatrixConfig(${channel.id})"><i class="ri-settings-3-line"></i> 配置</button>` : '';
return `<p><strong>名称:</strong> ${channel.name}</p><p><strong>状态:</strong> ${channel.is_active?'<span class="badge bg-success">启用</span>':'<span class="badge bg-secondary">禁用</span>'}</p>
<p><strong>绑定Agent:</strong> ${agentsHtml}</p><button class="btn btn-sm btn-primary" onclick="showBindAgentModal(${channel.id})">绑定Agent</button>${configBtn}`;
}
function showBindAgentModal(channelId) {
document.getElementById('binding-channel-id').value = channelId;
document.getElementById('binding-form').reset();
updateAgentSelect();
new bootstrap.Modal(document.getElementById('bindingModal')).show();
}
async function saveBinding() {
const channelId = document.getElementById('binding-channel-id').value;
const conditionsStr = document.getElementById('binding-conditions').value;
const data = {
channel_id: parseInt(channelId),
agent_id: parseInt(document.getElementById('binding-agent-id').value),
priority: parseInt(document.getElementById('binding-priority').value) || 0,
mode: document.getElementById('binding-mode').value,
conditions: conditionsStr ? JSON.parse(conditionsStr) : null
};
const res = await fetch('/api/v2/channels/bind', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(data) });
const result = await res.json();
if (result.success) { bootstrap.Modal.getInstance(document.getElementById('bindingModal')).hide(); loadChannels(); alert('绑定成功'); }
else alert('失败: ' + result.message);
}
async function deleteMapping(id) {
if (!confirm('确定解绑?')) return;
const res = await fetch(`/api/v2/channels/unbind/${id}`, {method:'DELETE'});
const result = await res.json();
if (result.success) { loadChannels(); alert('解绑成功'); }
}
function editMatrixConfig(channelId) {
const channel = channelsData.find(c => c.id === channelId);
if (!channel) return;
const config = channel.config || {};
document.getElementById('matrix-homeserver').value = config.homeserver || '';
document.getElementById('matrix-username').value = config.username || '';
document.getElementById('matrix-password').value = config.password || '';
document.getElementById('matrix-config-form').dataset.channelId = channelId;
new bootstrap.Modal(document.getElementById('matrixConfigModal')).show();
}
async function saveMatrixConfig() {
const channelId = document.getElementById('matrix-config-form').dataset.channelId;
const res = await fetch(`/api/v2/channels/${channelId}/config`, { method:'PUT', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ config: { homeserver: document.getElementById('matrix-homeserver').value, username: document.getElementById('matrix-username').value, password: document.getElementById('matrix-password').value } }) });
const result = await res.json();
if (result.success) { bootstrap.Modal.getInstance(document.getElementById('matrixConfigModal')).hide(); loadChannels(); alert('配置已更新'); }
}
// ===== 统计 =====
async function loadStats() {
const res = await fetch('/api/admin/stats');
const data = await res.json();
document.getElementById('stat-users').textContent = data.total_users || 0;
document.getElementById('stat-conversations').textContent = data.total_conversations || 0;
document.getElementById('stat-messages').textContent = data.total_messages || 0;
const agentsRes = await fetch('/api/v2/agents');
const agentsData = await agentsRes.json();
document.getElementById('stat-agents').textContent = (agentsData.agents || []).length;
}
// ===== 工具管理 =====
async function loadTools() {
const res = await fetch('/api/v2/tools');
const data = await res.json();
const tools = data.tools || [];
const tbody = document.getElementById('tools-list');
if (tools.length === 0) {
tbody.innerHTML = '<tr><td colspan="8" class="text-center text-muted">暂无工具,点击添加按钮创建</td></tr>';
} else {
tbody.innerHTML = tools.map(t => {
const successRate = t.total_calls > 0 ? Math.round(t.success_calls / t.total_calls * 100) : 0;
return `
<tr>
<td>${t.name}</td>
<td><span class="badge bg-info">${t.tool_type}</span></td>
<td>${t.provider || '-'}</td>
<td>${t.total_calls}</td>
<td><span class="badge ${successRate >= 90 ? 'bg-success' : successRate >= 70 ? 'bg-warning' : 'bg-danger'}">${successRate}%</span></td>
<td>${t.is_default ? '<i class="ri-check-line text-success"></i>' : ''}</td>
<td>${t.is_active ? '<span class="badge bg-success">启用</span>' : '<span class="badge bg-secondary">禁用</span>'}</td>
<td>
<button class="btn btn-sm btn-outline-primary" onclick="editTool(${t.id})"><i class="ri-edit-line"></i></button>
<button class="btn btn-sm btn-outline-danger" onclick="deleteTool(${t.id})"><i class="ri-delete-bin-line"></i></button>
</td>
</tr>
`;
}).join('');
}
// 加载统计
loadToolStats();
}
async function loadToolStats() {
const res = await fetch('/api/v2/tools/stats?days=7');
const stats = await res.json();
const container = document.getElementById('tool-stats');
if (stats.total_calls === 0) {
container.innerHTML = '<p class="text-muted">暂无使用记录</p>';
return;
}
let html = `<p><strong>总调用:</strong> ${stats.total_calls} 次</p>`;
html += `<p><strong>成功率:</strong> ${Math.round(stats.success_rate)}%</p>`;
if (stats.by_type) {
html += '<hr><h6>按类型:</h6>';
for (const [type, data] of Object.entries(stats.by_type)) {
html += `<p><span class="badge bg-info">${type}</span> ${data.total}次 (成功${data.success})</p>`;
}
}
if (stats.recent_errors && stats.recent_errors.length > 0) {
html += '<hr><h6>最近错误:</h6>';
stats.recent_errors.slice(0, 3).forEach(e => {
html += `<p class="text-danger small">${e.tool}: ${e.error}</p>`;
});
}
container.innerHTML = html;
}
function showAddToolModal() {
document.getElementById('tool-id').value = '';
document.getElementById('tool-name').value = '';
document.getElementById('tool-type').value = 'search';
document.getElementById('tool-provider').value = 'tavily';
document.getElementById('tool-api-key').value = '';
document.getElementById('tool-config').value = '{"max_results": 5, "search_depth": "basic"}';
document.getElementById('tool-active').checked = true;
document.getElementById('tool-default').checked = false;
updateToolForm();
new bootstrap.Modal(document.getElementById('toolModal')).show();
}
function updateToolForm() {
const type = document.getElementById('tool-type').value;
const providerSelect = document.getElementById('tool-provider');
const configTextarea = document.getElementById('tool-config');
// 根据类型设置默认配置
const defaults = {
'search': { provider: 'tavily', config: '{"api_key": "", "max_results": 5, "search_depth": "basic"}' },
'calculator': { provider: 'wolfram', config: '{"api_key": ""}' },
'code_runner': { provider: '', config: '{"timeout": 30, "language": "python"}' },
'image_gen': { provider: 'openai', config: '{"api_key": "", "model": "dall-e-3"}' },
'translator': { provider: '', config: '{"source": "auto", "target": "zh"}' },
'weather': { provider: '', config: '{"api_key": ""}' },
'custom': { provider: '', config: '{}' }
};
const def = defaults[type] || defaults['custom'];
providerSelect.value = def.provider;
configTextarea.value = def.config;
}
async function editTool(id) {
const res = await fetch('/api/v2/tools');
const data = await res.json();
const tool = (data.tools || []).find(t => t.id === id);
if (!tool) return;
document.getElementById('tool-id').value = tool.id;
document.getElementById('tool-name').value = tool.name;
document.getElementById('tool-type').value = tool.tool_type;
document.getElementById('tool-provider').value = tool.provider || '';
document.getElementById('tool-config').value = JSON.stringify(tool.config || {});
document.getElementById('tool-active').checked = tool.is_active;
document.getElementById('tool-default').checked = tool.is_default;
// 提取 API Key
const apiKey = tool.config?.api_key || '';
document.getElementById('tool-api-key').value = apiKey;
new bootstrap.Modal(document.getElementById('toolModal')).show();
}
async function saveTool() {
const id = document.getElementById('tool-id').value;
// 解析配置 JSON
let config = {};
try {
config = JSON.parse(document.getElementById('tool-config').value);
} catch (e) {
alert('配置 JSON 格式错误');
return;
}
// 添加 API Key 到配置
const apiKey = document.getElementById('tool-api-key').value;
if (apiKey) config.api_key = apiKey;
const data = {
name: document.getElementById('tool-name').value,
tool_type: document.getElementById('tool-type').value,
provider: document.getElementById('tool-provider').value,
config: config,
is_active: document.getElementById('tool-active').checked,
is_default: document.getElementById('tool-default').checked
};
let res;
if (id) {
res = await fetch(`/api/v2/tools/${id}`, { method: 'PUT', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(data) });
} else {
res = await fetch('/api/v2/tools', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(data) });
}
const result = await res.json();
if (result.success) {
bootstrap.Modal.getInstance(document.getElementById('toolModal')).hide();
loadTools();
alert('保存成功');
} else {
alert('保存失败: ' + result.message);
}
}
async function deleteTool(id) {
if (!confirm('确定删除此工具配置?')) return;
const res = await fetch(`/api/v2/tools/${id}`, { method: 'DELETE' });
const result = await res.json();
if (result.success) {
loadTools();
alert('删除成功');
} else {
alert('删除失败');
}
}
// ===== 初始化 =====
document.addEventListener('DOMContentLoaded', function() {
// 页面切换事件绑定
document.querySelectorAll('.sidebar .nav-item').forEach(item => {
item.addEventListener('click', () => {
document.querySelectorAll('.sidebar .nav-item').forEach(i => i.classList.remove('active'));
item.classList.add('active');
const page = item.dataset.page;
document.querySelectorAll('.page-section').forEach(s => s.classList.remove('active'));
document.getElementById('page-' + page).classList.add('active');
loadPageData(page);
});
});
// 初始加载
loadProviders();
loadStats();
});
</script>
</body>
</html>