1530 lines
60 KiB
JavaScript
1530 lines
60 KiB
JavaScript
// AI助手 - 后台管理前端
|
||
|
||
const API_BASE = '';
|
||
|
||
let currentPage = 'stats';
|
||
let llmConfigs = [];
|
||
let agents = [];
|
||
let toolConfigs = [];
|
||
let chatConfigs = [];
|
||
let systemConfigs = {};
|
||
|
||
// ==================== 登录 ====================
|
||
|
||
document.getElementById('loginBtn').addEventListener('click', async () => {
|
||
const username = document.getElementById('loginUsername').value.trim();
|
||
const password = document.getElementById('loginPassword').value;
|
||
|
||
if (!username || !password) {
|
||
showToast('请输入用户名和密码');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const res = await fetch(`${API_BASE}/api/admin/login`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ username, password })
|
||
});
|
||
|
||
const data = await res.json();
|
||
|
||
if (data.success) {
|
||
localStorage.setItem('adminLoggedIn', 'true');
|
||
localStorage.setItem('adminUsername', username);
|
||
document.getElementById('loginPage').style.display = 'none';
|
||
document.getElementById('adminPage').style.display = 'block';
|
||
document.getElementById('adminName').textContent = username;
|
||
loadPage('stats');
|
||
} else {
|
||
showToast(data.error || '登录失败');
|
||
}
|
||
} catch (e) {
|
||
showToast('登录失败,请检查网络');
|
||
}
|
||
});
|
||
|
||
// 检查登录状态
|
||
if (localStorage.getItem('adminLoggedIn') === 'true') {
|
||
document.getElementById('loginPage').style.display = 'none';
|
||
document.getElementById('adminPage').style.display = 'block';
|
||
document.getElementById('adminName').textContent = localStorage.getItem('adminUsername');
|
||
loadPage('stats');
|
||
}
|
||
|
||
// 退出登录
|
||
document.getElementById('logoutBtn').addEventListener('click', () => {
|
||
localStorage.removeItem('adminLoggedIn');
|
||
localStorage.removeItem('adminUsername');
|
||
document.getElementById('adminPage').style.display = 'none';
|
||
document.getElementById('loginPage').style.display = 'flex';
|
||
});
|
||
|
||
// ==================== 侧边栏导航 ====================
|
||
|
||
document.querySelectorAll('.sidebar-item').forEach(item => {
|
||
item.addEventListener('click', () => {
|
||
const page = item.getAttribute('data-page');
|
||
document.querySelectorAll('.sidebar-item').forEach(i => i.classList.remove('active'));
|
||
item.classList.add('active');
|
||
loadPage(page);
|
||
});
|
||
});
|
||
|
||
// ==================== 页面加载 ====================
|
||
|
||
async function loadPage(page) {
|
||
currentPage = page;
|
||
const content = document.getElementById('mainContent');
|
||
|
||
switch (page) {
|
||
case 'stats':
|
||
await loadStatsPage(content);
|
||
break;
|
||
case 'users':
|
||
await loadUsersPage(content);
|
||
break;
|
||
case 'llm':
|
||
await loadLLMPage(content);
|
||
break;
|
||
case 'agents':
|
||
await loadAgentsPage(content);
|
||
break;
|
||
case 'chat':
|
||
await loadChatConfigPage(content);
|
||
break;
|
||
case 'tools':
|
||
await loadToolsPage(content);
|
||
break;
|
||
case 'system':
|
||
await loadSystemPage(content);
|
||
break;
|
||
}
|
||
}
|
||
|
||
// ==================== 统计页面 ====================
|
||
|
||
async function loadStatsPage(content) {
|
||
const stats = await fetchAPI('/api/admin/stats');
|
||
|
||
content.innerHTML = `
|
||
<div class="content-header">
|
||
<h1 class="content-title">统计信息</h1>
|
||
</div>
|
||
|
||
<div class="stats-grid">
|
||
<div class="stat-card">
|
||
<div class="stat-icon">🧠</div>
|
||
<div class="stat-value">${stats.llm_total_calls || 0}</div>
|
||
<div class="stat-label">LLM 总调用</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-icon">📊</div>
|
||
<div class="stat-value">${stats.llm_today_calls || 0}</div>
|
||
<div class="stat-label">今日 LLM 调用</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-icon">🔍</div>
|
||
<div class="stat-value">${stats.search_total_calls || 0}</div>
|
||
<div class="stat-label">搜索总调用</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-icon">📈</div>
|
||
<div class="stat-value">${stats.search_today_calls || 0}</div>
|
||
<div class="stat-label">今日搜索</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="stats-grid">
|
||
<div class="stat-card">
|
||
<div class="stat-icon">🤖</div>
|
||
<div class="stat-value">${stats.agent_count || 0}</div>
|
||
<div class="stat-label">智能体数量</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-icon">🧠</div>
|
||
<div class="stat-value">${stats.llm_count || 0}</div>
|
||
<div class="stat-label">LLM 配置数量</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-icon">👥</div>
|
||
<div class="stat-value">${stats.total_users || 0}</div>
|
||
<div class="stat-label">注册用户数</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-icon">📅</div>
|
||
<div class="stat-value">${new Date().toLocaleDateString()}</div>
|
||
<div class="stat-label">统计日期</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="data-table" style="margin-top: 24px;">
|
||
<h3 style="padding: 16px; background: var(--bg-color);">热门智能体使用排行</h3>
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>智能体</th>
|
||
<th>使用次数</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${stats.agent_usage?.map(a => `
|
||
<tr>
|
||
<td>${a.log_key}</td>
|
||
<td>${a.count}</td>
|
||
</tr>
|
||
`).join('') || '<tr><td colspan="2">暂无数据</td></tr>'}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// ==================== 用户管理页面 ====================
|
||
|
||
let users = [];
|
||
|
||
async function loadUsersPage(content) {
|
||
users = await fetchAPI('/api/admin/users');
|
||
|
||
content.innerHTML = `
|
||
<div class="content-header">
|
||
<h1 class="content-title">用户管理</h1>
|
||
<div style="display: flex; gap: 12px;">
|
||
<input type="text" class="form-input" id="userSearch" placeholder="搜索用户名/手机/邮箱" style="width: 200px;" onkeyup="searchUsers(event)">
|
||
<button class="add-btn" onclick="searchUsersBtn()">搜索</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="stats-grid" style="grid-template-columns: repeat(4, 1fr);">
|
||
<div class="stat-card">
|
||
<div class="stat-icon">👥</div>
|
||
<div class="stat-value">${users.length}</div>
|
||
<div class="stat-label">总用户数</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-icon">🆕</div>
|
||
<div class="stat-value">${users.filter(u => {
|
||
const today = new Date().toISOString().slice(0, 10);
|
||
return u.created_at && u.created_at.startsWith(today);
|
||
}).length}</div>
|
||
<div class="stat-label">今日新增</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-icon">📧</div>
|
||
<div class="stat-value">${users.filter(u => u.email).length}</div>
|
||
<div class="stat-label">已绑定邮箱</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-icon">📱</div>
|
||
<div class="stat-value">${users.length}</div>
|
||
<div class="stat-label">已绑定手机</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="data-table">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>ID</th>
|
||
<th>头像</th>
|
||
<th>用户名</th>
|
||
<th>手机号</th>
|
||
<th>邮箱</th>
|
||
<th>注册时间</th>
|
||
<th>最后登录</th>
|
||
<th>操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${users.length === 0 ? '<tr><td colspan="8" style="text-align: center; color: #999;">暂无用户</td></tr>' :
|
||
users.map(u => `
|
||
<tr>
|
||
<td>${u.id}</td>
|
||
<td style="font-size: 24px;">${u.avatar || '👤'}</td>
|
||
<td>${u.username}</td>
|
||
<td>${u.phone}</td>
|
||
<td>${u.email || '-'}</td>
|
||
<td>${formatDate(u.created_at)}</td>
|
||
<td>${u.last_login_at ? formatDate(u.last_login_at) : '从未登录'}</td>
|
||
<td>
|
||
<div class="action-btns">
|
||
<button class="action-btn edit" onclick="showEditUserModal(${u.id})">编辑</button>
|
||
<button class="action-btn" style="background: #8b5cf6; color: white;" onclick="showUserConversations(${u.id}, '${u.username}')">查看对话</button>
|
||
<button class="action-btn" style="background: #f59e0b; color: white;" onclick="showResetPasswordModal(${u.id})">重置密码</button>
|
||
<button class="action-btn delete" onclick="deleteUser(${u.id})">删除</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
`).join('')
|
||
}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function formatDate(dateStr) {
|
||
if (!dateStr) return '-';
|
||
const d = new Date(dateStr);
|
||
return d.toLocaleDateString() + ' ' + d.toLocaleTimeString().slice(0, 5);
|
||
}
|
||
|
||
async function searchUsers(event) {
|
||
if (event && event.key !== 'Enter') return;
|
||
const search = document.getElementById('userSearch').value.trim();
|
||
users = await fetchAPI(`/api/admin/users?search=${encodeURIComponent(search)}`);
|
||
loadUsersPage(document.getElementById('mainContent'));
|
||
}
|
||
|
||
async function searchUsersBtn() {
|
||
const search = document.getElementById('userSearch').value.trim();
|
||
users = await fetchAPI(`/api/admin/users?search=${encodeURIComponent(search)}`);
|
||
loadUsersPage(document.getElementById('mainContent'));
|
||
}
|
||
|
||
function showEditUserModal(id) {
|
||
const user = users.find(u => u.id === id);
|
||
if (!user) return;
|
||
|
||
const avatars = ['👤', '😊', '😎', '🤓', '🦸', '🧙', '🥷', '👨', '👩', '🧑', '👴', '👵', '👦', '👧', '🤖', '👽', '🧛', '🧜', '🧚', '🦊'];
|
||
|
||
showModal('编辑用户', `
|
||
<div class="form-group">
|
||
<label class="form-label">用户名</label>
|
||
<input type="text" class="form-input" id="editUserName" value="${user.username}">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">手机号(不可修改)</label>
|
||
<input type="text" class="form-input" id="editUserPhone" value="${user.phone}" readonly style="background: #f5f7fa;">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">邮箱</label>
|
||
<input type="email" class="form-input" id="editUserEmail" value="${user.email || ''}">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">头像</label>
|
||
<select class="form-select" id="editUserAvatar">
|
||
${avatars.map(a => `<option value="${a}" ${a === (user.avatar || '👤') ? 'selected' : ''}>${a}</option>`).join('')}
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">个性签名</label>
|
||
<input type="text" class="form-input" id="editUserSignature" value="${user.signature || ''}" maxlength="50">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">性别</label>
|
||
<select class="form-select" id="editUserGender">
|
||
<option value="" ${!user.gender ? 'selected' : ''}>未设置</option>
|
||
<option value="male" ${user.gender === 'male' ? 'selected' : ''}>男</option>
|
||
<option value="female" ${user.gender === 'female' ? 'selected' : ''}>女</option>
|
||
<option value="other" ${user.gender === 'other' ? 'selected' : ''}>其他</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">年龄</label>
|
||
<input type="number" class="form-input" id="editUserAge" value="${user.age || ''}" min="1" max="150">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">地区</label>
|
||
<input type="text" class="form-input" id="editUserRegion" value="${user.region || ''}">
|
||
</div>
|
||
<button class="form-submit" onclick="updateUser(${id})">保存</button>
|
||
`);
|
||
}
|
||
|
||
async function updateUser(id) {
|
||
const data = {
|
||
username: document.getElementById('editUserName').value,
|
||
email: document.getElementById('editUserEmail').value,
|
||
avatar: document.getElementById('editUserAvatar').value,
|
||
signature: document.getElementById('editUserSignature').value,
|
||
gender: document.getElementById('editUserGender').value,
|
||
age: document.getElementById('editUserAge').value ? parseInt(document.getElementById('editUserAge').value) : null,
|
||
region: document.getElementById('editUserRegion').value
|
||
};
|
||
|
||
if (!data.username) {
|
||
showToast('用户名不能为空');
|
||
return;
|
||
}
|
||
|
||
await fetchAPI(`/api/admin/users/${id}`, 'PUT', data);
|
||
closeModal();
|
||
showToast('更新成功');
|
||
loadPage('users');
|
||
}
|
||
|
||
function showResetPasswordModal(id) {
|
||
const user = users.find(u => u.id === id);
|
||
if (!user) return;
|
||
|
||
showModal('重置密码', `
|
||
<div style="padding: 16px; background: #f5f7fa; border-radius: 8px; margin-bottom: 16px;">
|
||
<p><strong>用户:</strong> ${user.username}</p>
|
||
<p><strong>手机:</strong> ${user.phone}</p>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">新密码</label>
|
||
<input type="text" class="form-input" id="resetPassword" placeholder="输入新密码(至少6位)">
|
||
</div>
|
||
<p style="color: #999; font-size: 12px;">⚠️ 重置后用户需使用新密码登录</p>
|
||
<button class="form-submit" onclick="resetPassword(${id})">确认重置</button>
|
||
`);
|
||
}
|
||
|
||
async function resetPassword(id) {
|
||
const password = document.getElementById('resetPassword').value;
|
||
|
||
if (!password || password.length < 6) {
|
||
showToast('密码长度至少6位');
|
||
return;
|
||
}
|
||
|
||
await fetchAPI(`/api/admin/users/${id}/password`, 'PUT', { password });
|
||
closeModal();
|
||
showToast('密码已重置');
|
||
}
|
||
|
||
async function deleteUser(id) {
|
||
const user = users.find(u => u.id === id);
|
||
if (!user) return;
|
||
|
||
if (!confirm(`确定删除用户 "${user.username}"?\n此操作不可恢复!`)) return;
|
||
|
||
await fetchAPI(`/api/admin/users/${id}`, 'DELETE');
|
||
showToast('删除成功');
|
||
loadPage('users');
|
||
}
|
||
|
||
// ==================== 查看用户对话记录 ====================
|
||
|
||
async function showUserConversations(userId, username) {
|
||
// 加载用户对话列表
|
||
const conversations = await fetchAPI(`/api/user/${userId}/conversations`);
|
||
|
||
const content = document.getElementById('mainContent');
|
||
|
||
content.innerHTML = `
|
||
<div class="content-header">
|
||
<h1 class="content-title">用户对话记录 - ${username}</h1>
|
||
<button class="add-btn" style="background: #718096;" onclick="loadPage('users')">返回用户列表</button>
|
||
</div>
|
||
|
||
<div class="stats-grid" style="grid-template-columns: repeat(3, 1fr);">
|
||
<div class="stat-card">
|
||
<div class="stat-icon">💬</div>
|
||
<div class="stat-value">${conversations.length}</div>
|
||
<div class="stat-label">对话总数</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-icon">🤖</div>
|
||
<div class="stat-value">${conversations.filter(c => c.agentId).length}</div>
|
||
<div class="stat-label">智能体对话</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-icon">📝</div>
|
||
<div class="stat-value">${conversations.reduce((sum, c) => sum + (c.messages?.length || 0), 0)}</div>
|
||
<div class="stat-label">消息总数</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="data-table">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>ID</th>
|
||
<th>标题</th>
|
||
<th>智能体</th>
|
||
<th>消息数</th>
|
||
<th>创建时间</th>
|
||
<th>更新时间</th>
|
||
<th>操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${conversations.length === 0 ? '<tr><td colspan="7" style="text-align: center; color: #999;">暂无对话记录</td></tr>' :
|
||
conversations.map(conv => `
|
||
<tr>
|
||
<td>${conv.id}</td>
|
||
<td>${conv.title || '新对话'}</td>
|
||
<td>${conv.agentId ? getAgentName(conv.agentId) : '普通对话'}</td>
|
||
<td>${conv.messages?.length || 0}</td>
|
||
<td>${formatDate(conv.createdAt || conv.created_at)}</td>
|
||
<td>${formatDate(conv.updatedAt || conv.updated_at)}</td>
|
||
<td>
|
||
<div class="action-btns">
|
||
<button class="action-btn edit" onclick="showConversationMessages(${userId}, ${conv.id}, '${conv.title || '新对话'}')">查看详情</button>
|
||
<button class="action-btn delete" onclick="deleteUserConversation(${userId}, ${conv.id})">删除</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
`).join('')
|
||
}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function getAgentName(agentId) {
|
||
const agent = agents.find(a => a.agent_id === agentId);
|
||
return agent ? `${agent.avatar} ${agent.name}` : agentId;
|
||
}
|
||
|
||
async function showConversationMessages(userId, convId, title) {
|
||
// 获取对话详情
|
||
const conv = await fetchAPI(`/api/user/${userId}/conversations/${convId}`);
|
||
|
||
const content = document.getElementById('mainContent');
|
||
|
||
const messages = conv.messages || [];
|
||
|
||
content.innerHTML = `
|
||
<div class="content-header">
|
||
<h1 class="content-title">对话详情 - ${title}</h1>
|
||
<button class="add-btn" style="background: #718096;" onclick="showUserConversations(${userId}, '用户')">返回对话列表</button>
|
||
</div>
|
||
|
||
<div style="background: white; padding: 16px; border-radius: 12px; margin-bottom: 16px;">
|
||
<div style="display: flex; gap: 16px; color: #718096;">
|
||
<span>💬 消息数: ${messages.length}</span>
|
||
<span>🤖 智能体: ${conv.agentId ? getAgentName(conv.agentId) : '普通对话'}</span>
|
||
<span>📅 创建: ${formatDate(conv.createdAt || conv.created_at)}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="data-table">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th style="width: 60px;">序号</th>
|
||
<th style="width: 80px;">角色</th>
|
||
<th>内容</th>
|
||
<th style="width: 150px;">时间</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${messages.length === 0 ? '<tr><td colspan="4" style="text-align: center; color: #999;">暂无消息</td></tr>' :
|
||
messages.map((msg, idx) => `
|
||
<tr>
|
||
<td>${idx + 1}</td>
|
||
<td style="color: ${msg.role === 'user' ? '#3b82f6' : '#10b981'};">
|
||
${msg.role === 'user' ? '👤 用户' : '🤖 AI'}
|
||
</td>
|
||
<td style="max-width: 500px; white-space: pre-wrap; word-break: break-all;">
|
||
${escapeHtml(msg.content?.slice(0, 500) || '')}${msg.content?.length > 500 ? '...' : ''}
|
||
${msg.thinking ? `<div style="color: #f59e0b; margin-top: 8px; font-size: 12px;">💭 思考: ${escapeHtml(msg.thinking?.slice(0, 200) || '')}...</div>` : ''}
|
||
</td>
|
||
<td>${formatDate(msg.timestamp || msg.createdAt)}</td>
|
||
</tr>
|
||
`).join('')
|
||
}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
async function deleteUserConversation(userId, convId) {
|
||
if (!confirm('确定删除此对话?此操作不可恢复!')) return;
|
||
|
||
await fetchAPI(`/api/user/${userId}/conversations/${convId}`, 'DELETE');
|
||
showToast('删除成功');
|
||
showUserConversations(userId, '用户');
|
||
}
|
||
|
||
function escapeHtml(text) {
|
||
if (!text) return '';
|
||
const div = document.createElement('div');
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
}
|
||
|
||
// ==================== 大模型配置页面 ====================
|
||
|
||
async function loadLLMPage(content) {
|
||
llmConfigs = await fetchAPI('/api/admin/llm');
|
||
|
||
content.innerHTML = `
|
||
<div class="content-header">
|
||
<h1 class="content-title">大模型配置</h1>
|
||
<button class="add-btn" onclick="showAddLLMModal()">+ 添加配置</button>
|
||
</div>
|
||
|
||
<div class="data-table">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>名称</th>
|
||
<th>提供商</th>
|
||
<th>模型</th>
|
||
<th>API URL</th>
|
||
<th>状态</th>
|
||
<th>操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${llmConfigs.map(c => `
|
||
<tr>
|
||
<td>${c.name} ${c.is_default ? '<span class="default-badge">默认</span>' : ''}</td>
|
||
<td>${c.provider}</td>
|
||
<td>${c.model}</td>
|
||
<td style="max-width: 200px; overflow: hidden; text-overflow: ellipsis;">${c.api_url}</td>
|
||
<td>${c.is_active ? '✅ 启用' : '❌ 禁用'}</td>
|
||
<td>
|
||
<div class="action-btns">
|
||
<button class="action-btn edit" onclick="showEditLLMModal(${c.id})">编辑</button>
|
||
${!c.is_default ? `<button class="action-btn default" onclick="setDefaultLLM(${c.id})">设为默认</button>` : ''}
|
||
<button class="action-btn delete" onclick="deleteLLM(${c.id})">删除</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
`).join('')}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function showAddLLMModal() {
|
||
showModal('添加大模型配置', `
|
||
<div class="form-group">
|
||
<label class="form-label">名称</label>
|
||
<input type="text" class="form-input" id="llmName" placeholder="如:智谱GLM">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">提供商</label>
|
||
<select class="form-select" id="llmProvider">
|
||
<option value="zhipu">智谱</option>
|
||
<option value="openai">OpenAI</option>
|
||
<option value="anthropic">Anthropic</option>
|
||
<option value="custom">自定义</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">API URL</label>
|
||
<input type="text" class="form-input" id="llmApiUrl" placeholder="API endpoint URL">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">API Key</label>
|
||
<input type="text" class="form-input" id="llmApiKey" placeholder="API Key">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">模型</label>
|
||
<input type="text" class="form-input" id="llmModel" placeholder="如:glm-4.5-air">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Max Tokens</label>
|
||
<input type="number" class="form-input" id="llmMaxTokens" value="2048">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Temperature</label>
|
||
<input type="number" class="form-input" id="llmTemperature" value="0.7" step="0.1">
|
||
</div>
|
||
<button class="form-submit" onclick="saveLLM()">保存</button>
|
||
`);
|
||
}
|
||
|
||
function showEditLLMModal(id) {
|
||
const config = llmConfigs.find(c => c.id === id);
|
||
if (!config) return;
|
||
|
||
showModal('编辑大模型配置', `
|
||
<div class="form-group">
|
||
<label class="form-label">名称</label>
|
||
<input type="text" class="form-input" id="llmName" value="${config.name}">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">提供商</label>
|
||
<select class="form-select" id="llmProvider">
|
||
<option value="zhipu" ${config.provider === 'zhipu' ? 'selected' : ''}>智谱</option>
|
||
<option value="openai" ${config.provider === 'openai' ? 'selected' : ''}>OpenAI</option>
|
||
<option value="anthropic" ${config.provider === 'anthropic' ? 'selected' : ''}>Anthropic</option>
|
||
<option value="custom" ${config.provider === 'custom' ? 'selected' : ''}>自定义</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">API URL</label>
|
||
<input type="text" class="form-input" id="llmApiUrl" value="${config.api_url}">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">API Key</label>
|
||
<input type="text" class="form-input" id="llmApiKey" value="${config.api_key}">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">模型</label>
|
||
<input type="text" class="form-input" id="llmModel" value="${config.model}">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Max Tokens</label>
|
||
<input type="number" class="form-input" id="llmMaxTokens" value="${config.max_tokens}">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Temperature</label>
|
||
<input type="number" class="form-input" id="llmTemperature" value="${config.temperature}" step="0.1">
|
||
</div>
|
||
<button class="form-submit" onclick="updateLLM(${id})">保存</button>
|
||
`);
|
||
}
|
||
|
||
async function saveLLM() {
|
||
const data = {
|
||
name: document.getElementById('llmName').value,
|
||
provider: document.getElementById('llmProvider').value,
|
||
api_url: document.getElementById('llmApiUrl').value,
|
||
api_key: document.getElementById('llmApiKey').value,
|
||
model: document.getElementById('llmModel').value,
|
||
max_tokens: parseInt(document.getElementById('llmMaxTokens').value),
|
||
temperature: parseFloat(document.getElementById('llmTemperature').value)
|
||
};
|
||
|
||
if (!data.name || !data.api_url || !data.api_key || !data.model) {
|
||
showToast('请填写完整信息');
|
||
return;
|
||
}
|
||
|
||
await fetchAPI('/api/admin/llm', 'POST', data);
|
||
closeModal();
|
||
showToast('添加成功');
|
||
loadPage('llm');
|
||
}
|
||
|
||
async function updateLLM(id) {
|
||
const data = {
|
||
name: document.getElementById('llmName').value,
|
||
provider: document.getElementById('llmProvider').value,
|
||
api_url: document.getElementById('llmApiUrl').value,
|
||
api_key: document.getElementById('llmApiKey').value,
|
||
model: document.getElementById('llmModel').value,
|
||
max_tokens: parseInt(document.getElementById('llmMaxTokens').value),
|
||
temperature: parseFloat(document.getElementById('llmTemperature').value)
|
||
};
|
||
|
||
await fetchAPI(`/api/admin/llm/${id}`, 'PUT', data);
|
||
closeModal();
|
||
showToast('更新成功');
|
||
loadPage('llm');
|
||
}
|
||
|
||
async function setDefaultLLM(id) {
|
||
await fetchAPI(`/api/admin/llm/${id}/default`, 'POST');
|
||
showToast('已设为默认');
|
||
loadPage('llm');
|
||
}
|
||
|
||
async function deleteLLM(id) {
|
||
if (!confirm('确定删除此配置?')) return;
|
||
await fetchAPI(`/api/admin/llm/${id}`, 'DELETE');
|
||
showToast('删除成功');
|
||
loadPage('llm');
|
||
}
|
||
|
||
// ==================== 智能体管理页面 ====================
|
||
|
||
async function loadAgentsPage(content) {
|
||
agents = await fetchAPI('/api/admin/agents');
|
||
llmConfigs = await fetchAPI('/api/admin/llm');
|
||
toolConfigs = await fetchAPI('/api/admin/tools');
|
||
|
||
const categories = {
|
||
basic: '基础',
|
||
work: '工作',
|
||
study: '学习',
|
||
life: '生活'
|
||
};
|
||
|
||
const tagLabels = {
|
||
'hot': '🔥 热门',
|
||
'popular': '👍 推荐',
|
||
'new': '🆕 新品',
|
||
'': ''
|
||
};
|
||
|
||
content.innerHTML = `
|
||
<div class="content-header">
|
||
<h1 class="content-title">智能体管理</h1>
|
||
<button class="add-btn" onclick="showAddAgentModal()">+ 添加智能体</button>
|
||
</div>
|
||
|
||
<div class="data-table">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>头像</th>
|
||
<th>名称</th>
|
||
<th>类别</th>
|
||
<th>标签</th>
|
||
<th>热度</th>
|
||
<th>上线状态</th>
|
||
<th>操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${agents.map(a => `
|
||
<tr>
|
||
<td style="font-size: 24px;">${a.avatar}</td>
|
||
<td>${a.name}</td>
|
||
<td>${categories[a.category] || a.category}</td>
|
||
<td>${a.tags ? a.tags.split(',').map(t => tagLabels[t] || t).filter(t => t).join(' ') : '-'}</td>
|
||
<td>${a.heat || 0}</td>
|
||
<td>
|
||
<span style="cursor: pointer; color: ${a.is_online ? '#22c55e' : '#e53e3e'};" onclick="toggleAgentOnline('${a.agent_id}', ${a.is_online})">
|
||
${a.is_online ? '✅ 已上线' : '❌ 未上线'}
|
||
</span>
|
||
</td>
|
||
<td>
|
||
<div class="action-btns">
|
||
<button class="action-btn edit" onclick="showEditAgentModal('${a.agent_id}')">编辑</button>
|
||
<button class="action-btn delete" onclick="deleteAgent('${a.agent_id}')">删除</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
`).join('')}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div style="margin-top: 24px; padding: 16px; background: white; border-radius: 12px;">
|
||
<h3 style="margin-bottom: 16px;">说明</h3>
|
||
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px;">
|
||
<div style="padding: 12px; background: #f5f7fa; border-radius: 8px;">
|
||
<strong>上线状态</strong>
|
||
<p style="color: #718096; font-size: 12px; margin-top: 4px;">点击可快速切换,未上线则APP不可见</p>
|
||
</div>
|
||
<div style="padding: 12px; background: #f5f7fa; border-radius: 8px;">
|
||
<strong>标签</strong>
|
||
<p style="color: #718096; font-size: 12px; margin-top: 4px;">hot=热门, popular=推荐, new=新品</p>
|
||
</div>
|
||
<div style="padding: 12px; background: #f5f7fa; border-radius: 8px;">
|
||
<strong>类别</strong>
|
||
<p style="color: #718096; font-size: 12px; margin-top: 4px;">basic/work/study/life</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
async function toggleAgentOnline(agentId, currentStatus) {
|
||
const newStatus = currentStatus ? 0 : 1;
|
||
await fetchAPI(`/api/admin/agents/${agentId}/online`, 'POST', { is_online: newStatus });
|
||
showToast(newStatus ? '已上线' : '已下线');
|
||
loadPage('agents');
|
||
}
|
||
|
||
function showAddAgentModal() {
|
||
showModal('添加智能体', `
|
||
<div class="form-group">
|
||
<label class="form-label">ID</label>
|
||
<input type="text" class="form-input" id="agentId" placeholder="如:assistant">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">名称</label>
|
||
<input type="text" class="form-input" id="agentName" placeholder="如:通用助手">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">头像</label>
|
||
<select class="form-select" id="agentAvatar">
|
||
${['🤖', '✍️', '👨💻', '🌐', '💼', '📊', '📈', '📚', '🔤', '🔢', '🏥', '✈️', '🍳', '🧑', '😊', '😎', '🤓', '🦸', '🧙', '🥷'].map(a =>
|
||
`<option value="${a}">${a}</option>`
|
||
).join('')}
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">类别</label>
|
||
<select class="form-select" id="agentCategory">
|
||
<option value="basic">基础</option>
|
||
<option value="work">工作</option>
|
||
<option value="study">学习</option>
|
||
<option value="life">生活</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">标签(逗号分隔)</label>
|
||
<input type="text" class="form-input" id="agentTags" placeholder="如:hot,popular">
|
||
<span style="color: #999; font-size: 12px;">可选: hot(热门), popular(推荐), new(新品)</span>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">描述</label>
|
||
<input type="text" class="form-input" id="agentDescription" placeholder="智能体描述">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">System Prompt</label>
|
||
<textarea class="form-textarea" id="agentPrompt" placeholder="系统提示词"></textarea>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">LLM配置(可选)</label>
|
||
<select class="form-select" id="agentLLM">
|
||
<option value="">使用默认</option>
|
||
${llmConfigs.map(c => `<option value="${c.id}">${c.name}</option>`).join('')}
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">可使用工具(逗号分隔)</label>
|
||
<input type="text" class="form-input" id="agentEnableTools" placeholder="如:search,calculator">
|
||
<span style="color: #999; font-size: 12px;">已配置工具: ${toolConfigs.map(t => t.tool_id).join(', ')}</span>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">热度</label>
|
||
<input type="number" class="form-input" id="agentHeat" value="0">
|
||
</div>
|
||
<div class="form-group">
|
||
<label style="display: flex; align-items: center; gap: 8px;">
|
||
<input type="checkbox" id="agentIsOnline" checked> 立即上线
|
||
</label>
|
||
</div>
|
||
<button class="form-submit" onclick="saveAgent()">保存</button>
|
||
`);
|
||
}
|
||
|
||
function showEditAgentModal(agentId) {
|
||
const agent = agents.find(a => a.agent_id === agentId);
|
||
if (!agent) return;
|
||
|
||
showModal('编辑智能体', `
|
||
<div class="form-group">
|
||
<label class="form-label">ID(不可修改)</label>
|
||
<input type="text" class="form-input" id="agentId" value="${agent.agent_id}" readonly style="background: #f5f7fa;">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">名称</label>
|
||
<input type="text" class="form-input" id="agentName" value="${agent.name}">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">头像</label>
|
||
<select class="form-select" id="agentAvatar">
|
||
${['🤖', '✍️', '👨💻', '🌐', '💼', '📊', '📈', '📚', '🔤', '🔢', '🏥', '✈️', '🍳', '🧑', '😊', '😎', '🤓', '🦸', '🧙', '🥷'].map(a =>
|
||
`<option value="${a}" ${a === agent.avatar ? 'selected' : ''}>${a}</option>`
|
||
).join('')}
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">类别</label>
|
||
<select class="form-select" id="agentCategory">
|
||
<option value="basic" ${agent.category === 'basic' ? 'selected' : ''}>基础</option>
|
||
<option value="work" ${agent.category === 'work' ? 'selected' : ''}>工作</option>
|
||
<option value="study" ${agent.category === 'study' ? 'selected' : ''}>学习</option>
|
||
<option value="life" ${agent.category === 'life' ? 'selected' : ''}>生活</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">标签(逗号分隔)</label>
|
||
<input type="text" class="form-input" id="agentTags" value="${agent.tags || ''}">
|
||
<span style="color: #999; font-size: 12px;">可选: hot(热门), popular(推荐), new(新品)</span>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">描述</label>
|
||
<input type="text" class="form-input" id="agentDescription" value="${agent.description || ''}">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">System Prompt</label>
|
||
<textarea class="form-textarea" id="agentPrompt">${agent.system_prompt || ''}</textarea>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">LLM配置(可选)</label>
|
||
<select class="form-select" id="agentLLM">
|
||
<option value="" ${!agent.llm_config_id ? 'selected' : ''}>使用默认</option>
|
||
${llmConfigs.map(c => `<option value="${c.id}" ${c.id === agent.llm_config_id ? 'selected' : ''}>${c.name}</option>`).join('')}
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">可使用工具(逗号分隔)</label>
|
||
<input type="text" class="form-input" id="agentEnableTools" value="${agent.enable_tools || ''}">
|
||
<span style="color: #999; font-size: 12px;">已配置工具: ${toolConfigs.map(t => t.tool_id).join(', ')}</span>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">热度</label>
|
||
<input type="number" class="form-input" id="agentHeat" value="${agent.heat || 0}">
|
||
</div>
|
||
<div class="form-group">
|
||
<label style="display: flex; align-items: center; gap: 8px;">
|
||
<input type="checkbox" id="agentIsOnline" ${agent.is_online ? 'checked' : ''}> 已上线
|
||
</label>
|
||
</div>
|
||
<button class="form-submit" onclick="updateAgent('${agentId}')">保存</button>
|
||
`);
|
||
}
|
||
|
||
async function saveAgent() {
|
||
const data = {
|
||
agent_id: document.getElementById('agentId').value,
|
||
name: document.getElementById('agentName').value,
|
||
avatar: document.getElementById('agentAvatar').value,
|
||
category: document.getElementById('agentCategory').value,
|
||
tags: document.getElementById('agentTags').value,
|
||
description: document.getElementById('agentDescription').value,
|
||
system_prompt: document.getElementById('agentPrompt').value,
|
||
llm_config_id: document.getElementById('agentLLM').value || null,
|
||
enable_tools: document.getElementById('agentEnableTools').value,
|
||
heat: parseInt(document.getElementById('agentHeat').value),
|
||
is_online: document.getElementById('agentIsOnline').checked ? 1 : 0
|
||
};
|
||
|
||
if (!data.agent_id || !data.name || !data.system_prompt) {
|
||
showToast('请填写完整信息');
|
||
return;
|
||
}
|
||
|
||
await fetchAPI('/api/admin/agents', 'POST', data);
|
||
closeModal();
|
||
showToast('添加成功');
|
||
loadPage('agents');
|
||
}
|
||
|
||
async function updateAgent(agentId) {
|
||
const data = {
|
||
name: document.getElementById('agentName').value,
|
||
avatar: document.getElementById('agentAvatar').value,
|
||
category: document.getElementById('agentCategory').value,
|
||
tags: document.getElementById('agentTags').value,
|
||
description: document.getElementById('agentDescription').value,
|
||
system_prompt: document.getElementById('agentPrompt').value,
|
||
llm_config_id: document.getElementById('agentLLM').value || null,
|
||
enable_tools: document.getElementById('agentEnableTools').value,
|
||
heat: parseInt(document.getElementById('agentHeat').value),
|
||
is_online: document.getElementById('agentIsOnline').checked ? 1 : 0
|
||
};
|
||
|
||
await fetchAPI(`/api/admin/agents/${agentId}`, 'PUT', data);
|
||
closeModal();
|
||
showToast('更新成功');
|
||
loadPage('agents');
|
||
}
|
||
|
||
async function deleteAgent(agentId) {
|
||
if (!confirm('确定删除此智能体?')) return;
|
||
await fetchAPI(`/api/admin/agents/${agentId}`, 'DELETE');
|
||
showToast('删除成功');
|
||
loadPage('agents');
|
||
}
|
||
|
||
// ==================== 对话配置页面 ====================
|
||
|
||
async function loadChatConfigPage(content) {
|
||
llmConfigs = await fetchAPI('/api/admin/llm');
|
||
toolConfigs = await fetchAPI('/api/admin/tools');
|
||
const chatConfig = await fetchAPI('/api/admin/chat');
|
||
|
||
content.innerHTML = `
|
||
<div class="content-header">
|
||
<h1 class="content-title">对话配置</h1>
|
||
</div>
|
||
|
||
<div style="background: white; padding: 24px; border-radius: 12px;">
|
||
<p style="color: #718096; margin-bottom: 24px;">
|
||
此配置对所有用户的普通对话生效,修改后立即生效。
|
||
</p>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">LLM 配置(对话使用的大模型)</label>
|
||
<select class="form-select" id="chatLLMConfig">
|
||
${llmConfigs.map(l => `<option value="${l.id}" ${l.id === chatConfig.llm_config_id ? 'selected' : ''}>${l.name}</option>`).join('')}
|
||
</select>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label style="display: flex; align-items: center; gap: 8px;">
|
||
<input type="checkbox" id="chatEnableSearch" ${chatConfig.enable_search ? 'checked' : ''} onchange="toggleEnableSearch()">
|
||
启用联网搜索
|
||
</label>
|
||
<span style="color: #999; font-size: 12px; margin-top: 4px;">开启后对话可联网搜索实时信息</span>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">可用工具(逗号分隔)</label>
|
||
<input type="text" class="form-input" id="chatEnableTools" value="${chatConfig.enable_tools || ''}" placeholder="如:search,calculator">
|
||
<span style="color: #999; font-size: 12px;">已配置工具: ${toolConfigs.map(t => t.tool_id).join(', ')}</span>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">历史记录数(对话上下文)</label>
|
||
<input type="number" class="form-input" id="chatMaxHistory" value="${chatConfig.max_history || 20}">
|
||
<span style="color: #999; font-size: 12px;">发送给模型的对话历史条数</span>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">Temperature(创造性)</label>
|
||
<input type="number" class="form-input" id="chatTemperature" value="${chatConfig.temperature || 0.7}" step="0.1" min="0" max="2">
|
||
<span style="color: #999; font-size: 12px;">0-2之间,越高越随机创造性,越低越稳定精确</span>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">系统提示词(可选)</label>
|
||
<textarea class="form-textarea" id="chatSystemPrompt" placeholder="全局系统提示词,对所有对话生效">${chatConfig.system_prompt || ''}</textarea>
|
||
</div>
|
||
|
||
<button class="form-submit" onclick="saveChatConfig()">保存配置</button>
|
||
</div>
|
||
|
||
<div style="margin-top: 24px; padding: 16px; background: white; border-radius: 12px;">
|
||
<h3 style="margin-bottom: 16px;">参数说明</h3>
|
||
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px;">
|
||
<div style="padding: 12px; background: #f5f7fa; border-radius: 8px;">
|
||
<strong>🧠 LLM配置</strong>
|
||
<p style="color: #718096; font-size: 12px; margin-top: 4px;">选择对话使用的大模型接口</p>
|
||
</div>
|
||
<div style="padding: 12px; background: #f5f7fa; border-radius: 8px;">
|
||
<strong>🔍 启用联网搜索</strong>
|
||
<p style="color: #718096; font-size: 12px; margin-top: 4px;">用户可在对话中开启联网搜索</p>
|
||
</div>
|
||
<div style="padding: 12px; background: #f5f7fa; border-radius: 8px;">
|
||
<strong>🔧 可用工具</strong>
|
||
<p style="color: #718096; font-size: 12px; margin-top: 4px;">对话中可使用的工具列表</p>
|
||
</div>
|
||
<div style="padding: 12px; background: #f5f7fa; border-radius: 8px;">
|
||
<strong>📜 历史记录数</strong>
|
||
<p style="color: #718096; font-size: 12px; margin-top: 4px;">发送多少条历史对话给模型</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function toggleEnableSearch() {
|
||
const enableSearch = document.getElementById('chatEnableSearch').checked;
|
||
const toolsInput = document.getElementById('chatEnableTools');
|
||
|
||
// 自动添加/移除 search 工具
|
||
let tools = toolsInput.value.split(',').filter(t => t.trim());
|
||
if (enableSearch) {
|
||
if (!tools.includes('search')) {
|
||
tools.push('search');
|
||
}
|
||
} else {
|
||
tools = tools.filter(t => t !== 'search');
|
||
}
|
||
toolsInput.value = tools.join(',');
|
||
}
|
||
|
||
async function saveChatConfig() {
|
||
const data = {
|
||
llm_config_id: parseInt(document.getElementById('chatLLMConfig').value),
|
||
enable_search: document.getElementById('chatEnableSearch').checked ? 1 : 0,
|
||
enable_tools: document.getElementById('chatEnableTools').value,
|
||
max_history: parseInt(document.getElementById('chatMaxHistory').value),
|
||
temperature: parseFloat(document.getElementById('chatTemperature').value),
|
||
system_prompt: document.getElementById('chatSystemPrompt').value
|
||
};
|
||
|
||
await fetchAPI('/api/admin/chat', 'PUT', data);
|
||
showToast('保存成功');
|
||
loadPage('chat');
|
||
}
|
||
|
||
// ==================== 工具配置页面 ====================
|
||
|
||
async function loadToolsPage(content) {
|
||
toolConfigs = await fetchAPI('/api/admin/tools');
|
||
|
||
const toolTypes = {
|
||
search: '搜索工具',
|
||
calculator: '计算器',
|
||
image: '图像生成',
|
||
code: '代码执行',
|
||
weather: '天气查询',
|
||
custom: '自定义'
|
||
};
|
||
|
||
content.innerHTML = `
|
||
<div class="content-header">
|
||
<h1 class="content-title">工具配置</h1>
|
||
<button class="add-btn" onclick="showAddToolModal()">+ 添加工具</button>
|
||
</div>
|
||
|
||
<div class="data-table">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>工具ID</th>
|
||
<th>名称</th>
|
||
<th>类型</th>
|
||
<th>提供商</th>
|
||
<th>API URL</th>
|
||
<th>状态</th>
|
||
<th>操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${toolConfigs.map(t => `
|
||
<tr>
|
||
<td>${t.tool_id} ${t.is_default ? '<span class="default-badge">默认</span>' : ''}</td>
|
||
<td>${t.name}</td>
|
||
<td>${toolTypes[t.type] || t.type}</td>
|
||
<td>${t.provider}</td>
|
||
<td style="max-width: 200px; overflow: hidden; text-overflow: ellipsis;">${t.api_url}</td>
|
||
<td>${t.is_active ? '✅ 启用' : '❌ 禁用'}</td>
|
||
<td>
|
||
<div class="action-btns">
|
||
<button class="action-btn edit" onclick="showEditToolModal('${t.tool_id}')">编辑</button>
|
||
${!t.is_default ? `<button class="action-btn default" onclick="setDefaultTool('${t.tool_id}')">设为默认</button>` : ''}
|
||
<button class="action-btn delete" onclick="deleteTool('${t.tool_id}')">删除</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
`).join('')}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div style="margin-top: 24px; padding: 16px; background: white; border-radius: 12px;">
|
||
<h3 style="margin-bottom: 16px;">工具类型说明</h3>
|
||
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px;">
|
||
<div style="padding: 12px; background: #f5f7fa; border-radius: 8px;">
|
||
<strong>🔍 search</strong>
|
||
<p style="color: #718096; font-size: 12px; margin-top: 4px;">联网搜索工具(Tavily、Google等)</p>
|
||
</div>
|
||
<div style="padding: 12px; background: #f5f7fa; border-radius: 8px;">
|
||
<strong>🧮 calculator</strong>
|
||
<p style="color: #718096; font-size: 12px; margin-top: 4px;">数学计算工具</p>
|
||
</div>
|
||
<div style="padding: 12px; background: #f5f7fa; border-radius: 8px;">
|
||
<strong>🎨 image</strong>
|
||
<p style="color: #718096; font-size: 12px; margin-top: 4px;">图像生成工具(DALL-E等)</p>
|
||
</div>
|
||
<div style="padding: 12px; background: #f5f7fa; border-radius: 8px;">
|
||
<strong>💻 code</strong>
|
||
<p style="color: #718096; font-size: 12px; margin-top: 4px;">代码执行工具</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function showAddToolModal() {
|
||
showModal('添加工具', `
|
||
<div class="form-group">
|
||
<label class="form-label">工具ID</label>
|
||
<input type="text" class="form-input" id="toolId" placeholder="如:search">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">名称</label>
|
||
<input type="text" class="form-input" id="toolName" placeholder="如:联网搜索">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">类型</label>
|
||
<select class="form-select" id="toolType">
|
||
<option value="search">搜索工具</option>
|
||
<option value="calculator">计算器</option>
|
||
<option value="image">图像生成</option>
|
||
<option value="code">代码执行</option>
|
||
<option value="weather">天气查询</option>
|
||
<option value="custom">自定义</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">提供商</label>
|
||
<input type="text" class="form-input" id="toolProvider" placeholder="如:tavily">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">API URL</label>
|
||
<input type="text" class="form-input" id="toolApiUrl" placeholder="API endpoint URL">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">API Key</label>
|
||
<input type="text" class="form-input" id="toolApiKey" placeholder="API Key(可选)">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">最大结果数</label>
|
||
<input type="number" class="form-input" id="toolMaxResults" value="10">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">额外配置(JSON格式)</label>
|
||
<textarea class="form-textarea" id="toolConfigJson" placeholder="{}"></textarea>
|
||
</div>
|
||
<button class="form-submit" onclick="saveTool()">保存</button>
|
||
`);
|
||
}
|
||
|
||
function showEditToolModal(toolId) {
|
||
const tool = toolConfigs.find(t => t.tool_id === toolId);
|
||
if (!tool) return;
|
||
|
||
const toolTypes = ['search', 'calculator', 'image', 'code', 'weather', 'custom'];
|
||
|
||
showModal('编辑工具', `
|
||
<div class="form-group">
|
||
<label class="form-label">工具ID(不可修改)</label>
|
||
<input type="text" class="form-input" id="toolId" value="${tool.tool_id}" readonly style="background: #f5f7fa;">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">名称</label>
|
||
<input type="text" class="form-input" id="toolName" value="${tool.name}">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">类型</label>
|
||
<select class="form-select" id="toolType">
|
||
${toolTypes.map(t => `<option value="${t}" ${t === tool.type ? 'selected' : ''}>${t}</option>`).join('')}
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">提供商</label>
|
||
<input type="text" class="form-input" id="toolProvider" value="${tool.provider}">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">API URL</label>
|
||
<input type="text" class="form-input" id="toolApiUrl" value="${tool.api_url}">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">API Key</label>
|
||
<input type="text" class="form-input" id="toolApiKey" value="${tool.api_key || ''}">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">最大结果数</label>
|
||
<input type="number" class="form-input" id="toolMaxResults" value="${tool.max_results || 10}">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">额外配置(JSON格式)</label>
|
||
<textarea class="form-textarea" id="toolConfigJson">${tool.config_json || '{}'}</textarea>
|
||
</div>
|
||
<button class="form-submit" onclick="updateTool('${toolId}')">保存</button>
|
||
`);
|
||
}
|
||
|
||
async function saveTool() {
|
||
const data = {
|
||
tool_id: document.getElementById('toolId').value,
|
||
name: document.getElementById('toolName').value,
|
||
type: document.getElementById('toolType').value,
|
||
provider: document.getElementById('toolProvider').value,
|
||
api_url: document.getElementById('toolApiUrl').value,
|
||
api_key: document.getElementById('toolApiKey').value,
|
||
max_results: parseInt(document.getElementById('toolMaxResults').value),
|
||
config_json: document.getElementById('toolConfigJson').value
|
||
};
|
||
|
||
if (!data.tool_id || !data.name || !data.type) {
|
||
showToast('请填写完整信息');
|
||
return;
|
||
}
|
||
|
||
await fetchAPI('/api/admin/tools', 'POST', data);
|
||
closeModal();
|
||
showToast('添加成功');
|
||
loadPage('tools');
|
||
}
|
||
|
||
async function updateTool(toolId) {
|
||
const data = {
|
||
name: document.getElementById('toolName').value,
|
||
type: document.getElementById('toolType').value,
|
||
provider: document.getElementById('toolProvider').value,
|
||
api_url: document.getElementById('toolApiUrl').value,
|
||
api_key: document.getElementById('toolApiKey').value,
|
||
max_results: parseInt(document.getElementById('toolMaxResults').value),
|
||
config_json: document.getElementById('toolConfigJson').value
|
||
};
|
||
|
||
await fetchAPI(`/api/admin/tools/${toolId}`, 'PUT', data);
|
||
closeModal();
|
||
showToast('更新成功');
|
||
loadPage('tools');
|
||
}
|
||
|
||
async function setDefaultTool(toolId) {
|
||
await fetchAPI(`/api/admin/tools/${toolId}/default`, 'POST');
|
||
showToast('已设为默认');
|
||
loadPage('tools');
|
||
}
|
||
|
||
async function deleteTool(toolId) {
|
||
if (!confirm('确定删除此工具?')) return;
|
||
await fetchAPI(`/api/admin/tools/${toolId}`, 'DELETE');
|
||
showToast('删除成功');
|
||
loadPage('tools');
|
||
}
|
||
|
||
// ==================== 系统设置页面 ====================
|
||
|
||
async function loadSystemPage(content) {
|
||
systemConfigs = await fetchAPI('/api/admin/system');
|
||
|
||
content.innerHTML = `
|
||
<div class="content-header">
|
||
<h1 class="content-title">系统设置</h1>
|
||
</div>
|
||
|
||
<div class="data-table" style="padding: 24px;">
|
||
<div class="form-group">
|
||
<label class="form-label">应用名称</label>
|
||
<input type="text" class="form-input" id="appName" value="${systemConfigs.app_name?.value || ''}">
|
||
<span style="color: #999; font-size: 12px;">${systemConfigs.app_name?.description || ''}</span>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">应用版本</label>
|
||
<input type="text" class="form-input" id="appVersion" value="${systemConfigs.app_version?.value || ''}">
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">应用简介</label>
|
||
<input type="text" class="form-input" id="appDescription" value="${systemConfigs.app_description?.value || ''}" placeholder="如:提供智能对话、多种智能体服务">
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">技术基础</label>
|
||
<input type="text" class="form-input" id="appTechnology" value="${systemConfigs.app_technology?.value || ''}" placeholder="如:智谱 GLM-4.5-Air 大模型">
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">开发者</label>
|
||
<input type="text" class="form-input" id="appDeveloper" value="${systemConfigs.app_developer?.value || ''}" placeholder="如:OpenClaw Team">
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">更新日期</label>
|
||
<input type="text" class="form-input" id="appUpdateDate" value="${systemConfigs.app_update_date?.value || ''}" placeholder="如:2026-04-27">
|
||
</div>
|
||
|
||
<h3 style="margin: 24px 0 16px; padding-top: 16px; border-top: 1px solid #e2e8f0;">游客使用限制</h3>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">每日对话会话数</label>
|
||
<input type="number" class="form-input" id="guestChatSessions" value="${systemConfigs.guest_chat_sessions?.value || 1}">
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">每日对话消息数</label>
|
||
<input type="number" class="form-input" id="guestChatMessages" value="${systemConfigs.guest_chat_messages?.value || 20}">
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">每日智能体消息数</label>
|
||
<input type="number" class="form-input" id="guestAgentMessages" value="${systemConfigs.guest_agent_messages?.value || 20}">
|
||
</div>
|
||
|
||
<h3 style="margin: 24px 0 16px; padding-top: 16px; border-top: 1px solid #e2e8f0;">管理员设置</h3>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">管理员密码</label>
|
||
<input type="text" class="form-input" id="adminPassword" value="${systemConfigs.admin_password?.value || ''}" placeholder="修改管理员密码">
|
||
</div>
|
||
|
||
<h3 style="margin: 24px 0 16px; padding-top: 16px; border-top: 1px solid #e2e8f0;">TTS语音配置</h3>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">TTS方案</label>
|
||
<select class="form-input" id="ttsProvider">
|
||
<option value="edge" ${systemConfigs.tts_provider?.value === 'edge' ? 'selected' : ''}>Edge TTS(免费)</option>
|
||
</select>
|
||
<span style="color: #999; font-size: 12px;">目前仅支持 Edge TTS,后续将添加更多方案</span>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">TTS语音</label>
|
||
<select class="form-input" id="ttsVoice">
|
||
<option value="zh-CN-XiaoxiaoNeural" ${systemConfigs.tts_voice?.value === 'zh-CN-XiaoxiaoNeural' ? 'selected' : ''}>晓晓(女声)</option>
|
||
<option value="zh-CN-YunxiNeural" ${systemConfigs.tts_voice?.value === 'zh-CN-YunxiNeural' ? 'selected' : ''}>云希(男声)</option>
|
||
<option value="zh-CN-YunjianNeural" ${systemConfigs.tts_voice?.value === 'zh-CN-YunjianNeural' ? 'selected' : ''}>云健(男声)</option>
|
||
<option value="zh-CN-XiaoyiNeural" ${systemConfigs.tts_voice?.value === 'zh-CN-XiaoyiNeural' ? 'selected' : ''}>晓伊(女声)</option>
|
||
<option value="zh-CN-YunfengNeural" ${systemConfigs.tts_voice?.value === 'zh-CN-YunfengNeural' ? 'selected' : ''}>云枫(男声)</option>
|
||
<option value="zh-CN-XiaochenNeural" ${systemConfigs.tts_voice?.value === 'zh-CN-XiaochenNeural' ? 'selected' : ''}>晓辰(女声)</option>
|
||
<option value="zh-CN-XiaohanNeural" ${systemConfigs.tts_voice?.value === 'zh-CN-XiaohanNeural' ? 'selected' : ''}>晓涵(女声)</option>
|
||
<option value="zh-CN-XiaomengNeural" ${systemConfigs.tts_voice?.value === 'zh-CN-XiaomengNeural' ? 'selected' : ''}>晓梦(女声)</option>
|
||
<option value="zh-CN-XiaomoNeural" ${systemConfigs.tts_voice?.value === 'zh-CN-XiaomoNeural' ? 'selected' : ''}>晓墨(女声)</option>
|
||
<option value="zh-CN-XiaoruiNeural" ${systemConfigs.tts_voice?.value === 'zh-CN-XiaoruiNeural' ? 'selected' : ''}>晓睿(女声)</option>
|
||
<option value="zh-CN-XiaoshuangNeural" ${systemConfigs.tts_voice?.value === 'zh-CN-XiaoshuangNeural' ? 'selected' : ''}>晓双(女声)</option>
|
||
<option value="zh-CN-XiaoxuanNeural" ${systemConfigs.tts_voice?.value === 'zh-CN-XiaoxuanNeural' ? 'selected' : ''}>晓萱(女声)</option>
|
||
<option value="zh-CN-XiaoyanNeural" ${systemConfigs.tts_voice?.value === 'zh-CN-XiaoyanNeural' ? 'selected' : ''}>晓颜(女声)</option>
|
||
<option value="zh-CN-XiaoyouNeural" ${systemConfigs.tts_voice?.value === 'zh-CN-XiaoyouNeural' ? 'selected' : ''}>晓悠(女声)</option>
|
||
<option value="zh-CN-YunyaNeural" ${systemConfigs.tts_voice?.value === 'zh-CN-YunyaNeural' ? 'selected' : ''}>云雅(女声)</option>
|
||
<option value="zh-CN-YunyangNeural" ${systemConfigs.tts_voice?.value === 'zh-CN-YunyangNeural' ? 'selected' : ''}>云扬(男声)</option>
|
||
</select>
|
||
<span style="color: #999; font-size: 12px;">选择AI回复的朗读语音</span>
|
||
</div>
|
||
|
||
<h3 style="margin: 24px 0 16px; padding-top: 16px; border-top: 1px solid #e2e8f0;">链接配置</h3>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">隐私政策链接</label>
|
||
<input type="text" class="form-input" id="privacyPolicyUrl" value="${systemConfigs.privacy_policy_url?.value || ''}" placeholder="隐私政策页面URL">
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">用户协议链接</label>
|
||
<input type="text" class="form-input" id="userAgreementUrl" value="${systemConfigs.user_agreement_url?.value || ''}" placeholder="用户协议页面URL">
|
||
</div>
|
||
|
||
<button class="form-submit" onclick="saveSystemConfig()">保存设置</button>
|
||
</div>
|
||
|
||
<div style="margin-top: 16px; padding: 12px; background: #f5f7fa; border-radius: 8px;">
|
||
<p style="color: #718096; font-size: 13px;">
|
||
💡 提示:启用联网搜索等对话相关配置请前往"对话配置"页面设置
|
||
</p>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
async function saveSystemConfig() {
|
||
const data = {
|
||
app_name: document.getElementById('appName').value,
|
||
app_version: document.getElementById('appVersion').value,
|
||
app_description: document.getElementById('appDescription').value,
|
||
app_technology: document.getElementById('appTechnology').value,
|
||
app_developer: document.getElementById('appDeveloper').value,
|
||
app_update_date: document.getElementById('appUpdateDate').value,
|
||
guest_chat_sessions: document.getElementById('guestChatSessions').value,
|
||
guest_chat_messages: document.getElementById('guestChatMessages').value,
|
||
guest_agent_messages: document.getElementById('guestAgentMessages').value,
|
||
admin_password: document.getElementById('adminPassword').value,
|
||
privacy_policy_url: document.getElementById('privacyPolicyUrl').value,
|
||
user_agreement_url: document.getElementById('userAgreementUrl').value,
|
||
tts_provider: document.getElementById('ttsProvider').value,
|
||
tts_voice: document.getElementById('ttsVoice').value,
|
||
};
|
||
|
||
await fetchAPI('/api/admin/system', 'POST', data);
|
||
showToast('保存成功');
|
||
}
|
||
|
||
// ==================== 工具函数 ====================
|
||
|
||
async function fetchAPI(url, method = 'GET', data = null) {
|
||
const options = {
|
||
method,
|
||
headers: { 'Content-Type': 'application/json' }
|
||
};
|
||
|
||
if (data) {
|
||
options.body = JSON.stringify(data);
|
||
}
|
||
|
||
const res = await fetch(`${API_BASE}${url}`, options);
|
||
return res.json();
|
||
}
|
||
|
||
function showModal(title, content) {
|
||
const modal = document.getElementById('modal');
|
||
const modalContent = document.getElementById('modalContent');
|
||
|
||
modalContent.innerHTML = `
|
||
<div class="modal-header">
|
||
<h2 class="modal-title">${title}</h2>
|
||
<span class="modal-close" onclick="closeModal()">×</span>
|
||
</div>
|
||
${content}
|
||
`;
|
||
|
||
modal.classList.add('show');
|
||
}
|
||
|
||
function closeModal() {
|
||
document.getElementById('modal').classList.remove('show');
|
||
}
|
||
|
||
function showToast(message) {
|
||
const toast = document.getElementById('toast');
|
||
toast.textContent = message;
|
||
toast.classList.add('show');
|
||
|
||
setTimeout(() => {
|
||
toast.classList.remove('show');
|
||
}, 2000);
|
||
}
|
||
|
||
// 点击模态框背景关闭
|
||
document.getElementById('modal').addEventListener('click', (e) => {
|
||
if (e.target.id === 'modal') {
|
||
closeModal();
|
||
}
|
||
}); |