Files
ai-chat-system/templates/admin_v2/index.html
hubian 0c9bfca346 feat: 添加搜索工具功能(Tavily Search)
- 新增 SearchToolConfig 模型:支持搜索工具配置
- Agent 增加 tools 字段:可配置可用工具列表
- 后台管理增加搜索工具配置页面
- Agent 管理增加工具启用开关
- 网页端增加搜索工具禁用复选框
- WebSocket chat 处理增加搜索调用逻辑
- 默认配置 Tavily Search API
2026-04-13 13:26:43 +08:00

645 lines
45 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="search-tools"><i class="ri-search-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>操作</th></tr></thead>
<tbody id="providers-list"><tr><td colspan="6" 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-search-tools">
<div class="page-header"><h2><i class="ri-search-line"></i> 搜索工具配置</h2></div>
<div class="card">
<div class="card-header d-flex justify-content-between">
<span>搜索工具列表Tavily、Google等</span>
<button class="btn btn-primary btn-sm" onclick="showAddSearchToolModal()"><i class="ri-add-line"></i> 添加</button>
</div>
<div class="card-body">
<table class="table">
<thead><tr><th>名称</th><th>提供商</th><th>API Key</th><th>最大结果</th><th>默认</th><th>状态</th><th>操作</th></tr></thead>
<tbody id="search-tools-list"><tr><td colspan="7" class="text-center">加载中...</td></tr></tbody>
</table>
</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>
<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 class="form-check"><input type="checkbox" class="form-check-input" id="agent-tool-search"><label class="form-check-label">启用搜索工具</label></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>
<!-- Search Tool Modal -->
<div class="modal fade" id="searchToolModal"><div class="modal-dialog"><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="search-tool-form">
<input type="hidden" id="search-tool-id">
<div class="mb-3"><label class="form-label">名称</label><input type="text" class="form-control" id="search-tool-name" value="Tavily Search"></div>
<div class="mb-3"><label class="form-label">提供商</label><select class="form-select" id="search-tool-provider"><option value="tavily">Tavily</option><option value="google">Google</option><option value="bing">Bing</option></select></div>
<div class="mb-3"><label class="form-label">API Key *</label><input type="text" class="form-control" id="search-tool-api-key" required></div>
<div class="mb-3"><label class="form-label">API地址可选</label><input type="text" class="form-control" id="search-tool-api-base"></div>
<div class="row"><div class="col-md-6"><label class="form-label">最大结果数</label><input type="number" class="form-control" id="search-tool-max-results" value="5"></div><div class="col-md-6"><label class="form-label">搜索深度</label><select class="form-select" id="search-tool-depth"><option value="basic">Basic</option><option value="advanced">Advanced</option></select></div></div>
<div class="mt-3 form-check"><input type="checkbox" class="form-check-input" id="search-tool-active" checked><label class="form-check-label">启用</label></div>
<div class="mt-2 form-check"><input type="checkbox" class="form-check-input" id="search-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="saveSearchTool()">保存</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 = [];
// 页面切换函数
function loadPageData(page) {
if (page === 'llm-providers') loadProviders();
if (page === 'agents') loadAgents();
if (page === 'channels') loadChannels();
if (page === 'search-tools') loadSearchTools();
if (page === 'stats') loadStats();
}
// ===== 大模型池 =====
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.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-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 || '';
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
};
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();
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 || '';
// 工具配置
const tools = a.tools || [];
document.getElementById('agent-tool-search').checked = tools.includes('search');
new bootstrap.Modal(document.getElementById('agentModal')).show();
}
async function saveAgent() {
const id = document.getElementById('agent-id').value;
// 处理工具列表
const tools = [];
if (document.getElementById('agent-tool-search').checked) tools.push('search');
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 loadSearchTools() {
const res = await fetch('/api/v2/search-tools');
const data = await res.json();
const tools = data.configs || [];
const tbody = document.getElementById('search-tools-list');
if (tools.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" class="text-center text-muted">暂无配置,点击添加按钮创建</td></tr>';
return;
}
tbody.innerHTML = tools.map(t => `
<tr>
<td>${t.name}</td>
<td>${t.provider}</td>
<td><small class="text-muted">${t.api_key ? t.api_key.substring(0, 10) + '...' : '-'}</small></td>
<td>${t.max_results}</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="editSearchTool(${t.id})"><i class="ri-edit-line"></i></button>
<button class="btn btn-sm btn-outline-danger" onclick="deleteSearchTool(${t.id})"><i class="ri-delete-bin-line"></i></button>
</td>
</tr>
`).join('');
}
function showAddSearchToolModal() {
document.getElementById('search-tool-id').value = '';
document.getElementById('search-tool-name').value = 'Tavily Search';
document.getElementById('search-tool-provider').value = 'tavily';
document.getElementById('search-tool-api-key').value = '';
document.getElementById('search-tool-api-base').value = '';
document.getElementById('search-tool-max-results').value = 5;
document.getElementById('search-tool-depth').value = 'basic';
document.getElementById('search-tool-active').checked = true;
document.getElementById('search-tool-default').checked = false;
new bootstrap.Modal(document.getElementById('searchToolModal')).show();
}
async function editSearchTool(id) {
const res = await fetch('/api/v2/search-tools');
const data = await res.json();
const tool = (data.configs || []).find(t => t.id === id);
if (!tool) return;
document.getElementById('search-tool-id').value = tool.id;
document.getElementById('search-tool-name').value = tool.name;
document.getElementById('search-tool-provider').value = tool.provider;
document.getElementById('search-tool-api-key').value = tool.api_key || '';
document.getElementById('search-tool-api-base').value = tool.api_base || '';
document.getElementById('search-tool-max-results').value = tool.max_results;
document.getElementById('search-tool-depth').value = tool.search_depth;
document.getElementById('search-tool-active').checked = tool.is_active;
document.getElementById('search-tool-default').checked = tool.is_default;
new bootstrap.Modal(document.getElementById('searchToolModal')).show();
}
async function saveSearchTool() {
const id = document.getElementById('search-tool-id').value;
const data = {
name: document.getElementById('search-tool-name').value,
provider: document.getElementById('search-tool-provider').value,
api_key: document.getElementById('search-tool-api-key').value,
api_base: document.getElementById('search-tool-api-base').value,
max_results: parseInt(document.getElementById('search-tool-max-results').value),
search_depth: document.getElementById('search-tool-depth').value,
is_active: document.getElementById('search-tool-active').checked,
is_default: document.getElementById('search-tool-default').checked
};
let res;
if (id) {
res = await fetch(`/api/v2/search-tools/${id}`, { method: 'PUT', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(data) });
} else {
res = await fetch('/api/v2/search-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('searchToolModal')).hide();
loadSearchTools();
alert('保存成功');
} else {
alert('保存失败: ' + result.message);
}
}
async function deleteSearchTool(id) {
if (!confirm('确定删除此搜索工具配置?')) return;
const res = await fetch(`/api/v2/search-tools/${id}`, { method: 'DELETE' });
const result = await res.json();
if (result.success) {
loadSearchTools();
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>