Files
ai-chat-app-v4/www/admin.js
hubian de54d15d10 feat: AI Chat App v3.15.1 初始版本
- 大模型配置支持思考模式和视觉能力属性
- 前端根据LLM能力显示/隐藏深度思考和图片上传功能
- 后台管理系统:用户管理、LLM配置、智能体管理、工具配置
- TTS语音播放、Markdown显示、历史对话管理
- 智能体对话系统

端口: 19021
后台管理: admin / admin123
2026-04-29 12:29:53 +08:00

1342 lines
51 KiB
JavaScript
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.
// 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: #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 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>思考模式</th>
<th>视觉能力</th>
<th>API URL</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>${c.enable_thinking ? '✅ 支持' : '❌ 不支持'}</td>
<td>${c.enable_vision ? '✅ 支持' : '❌ 不支持'}</td>
<td style="max-width: 200px; overflow: hidden; text-overflow: ellipsis;">${c.api_url}</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>
<div class="form-group">
<label style="display: flex; align-items: center; gap: 8px;">
<input type="checkbox" id="llmEnableThinking"> 支持思考模式(深度思考)
</label>
<span style="color: #999; font-size: 12px;">模型是否支持原生思考功能如DeepSeek的think标签</span>
</div>
<div class="form-group">
<label style="display: flex; align-items: center; gap: 8px;">
<input type="checkbox" id="llmEnableVision"> 支持视觉能力(图片分析)
</label>
<span style="color: #999; font-size: 12px;">模型是否支持图片输入和分析</span>
</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>
<div class="form-group">
<label style="display: flex; align-items: center; gap: 8px;">
<input type="checkbox" id="llmEnableThinking" ${config.enable_thinking ? 'checked' : ''}> 支持思考模式(深度思考)
</label>
<span style="color: #999; font-size: 12px;">模型是否支持原生思考功能如DeepSeek的think标签</span>
</div>
<div class="form-group">
<label style="display: flex; align-items: center; gap: 8px;">
<input type="checkbox" id="llmEnableVision" ${config.enable_vision ? 'checked' : ''}> 支持视觉能力(图片分析)
</label>
<span style="color: #999; font-size: 12px;">模型是否支持图片输入和分析</span>
</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),
enable_thinking: document.getElementById('llmEnableThinking').checked ? 1 : 0,
enable_vision: document.getElementById('llmEnableVision').checked ? 1 : 0
};
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),
enable_thinking: document.getElementById('llmEnableThinking').checked ? 1 : 0,
enable_vision: document.getElementById('llmEnableVision').checked ? 1 : 0
};
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>
<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>
<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,
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
};
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();
}
});