// AI助手 - 前端应用
// 使用智谱 GLM-4.5-Air 模型(流式输出 + 多对话管理)
const CONFIG = {
apiUrl: 'https://open.bigmodel.cn/api/paas/v4/chat/completions',
apiKey: '2259e33a1357460abe17919aaf81e73d.K44a8LPQTmFM5PKm',
model: 'glm-4.5-air',
maxTokens: 2048,
// Tavily Search API
tavilyApiUrl: 'https://api.tavily.com/search',
tavilyApiKey: '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'
};
// 数据结构
let conversations = []; // 对话列表
let currentConversation = null; // 当前对话
let isLoading = false;
// 当前页面状态
let currentPage = 'chats'; // chats | agents | profile
// 系统智能体数据(从后台API加载)
let systemAgents = [];
// 用户智能体界面显示的智能体
let agents = [];
// 用户添加的智能体(按类别分组)
let myAgents = {
basic: [],
work: [],
study: [],
life: []
};
// 收藏的智能体列表
let favoriteAgents = [];
// 置顶的智能体列表(按类别)
let pinnedAgents = {
basic: [],
work: [],
study: [],
life: []
};
let currentAgent = null; // 当前选中的智能体
// 后台配置
let backendConfig = null; // 从API获取的配置
// 用户状态
let currentUser = null; // 当前登录用户 { username, password, registeredAt }
// 每日使用统计(未登录用户)
let dailyUsage = {
date: null, // 日期 YYYY-MM-DD
chatSessions: 0, // 对话会话数
chatMessages: 0, // 对话消息数
agentUsed: null, // 使用的智能体ID(只能用一个)
agentMessages: 0 // 智能体消息数
};
// 未登录用户限制(从后台配置加载)
let GUEST_LIMITS = {
maxChatSessionsPerDay: 1,
maxChatMessagesPerDay: 20,
maxAgentPerDay: 1,
maxAgentMessagesPerDay: 20
};
// 获取今日日期字符串
function getTodayDate() {
const today = new Date();
return `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
}
// 从后台API加载配置
async function loadBackendConfig() {
try {
// 使用相对路径,自动适应当前域名
const API_BASE = window.location.origin;
const res = await fetch(`${API_BASE}/api/config`);
backendConfig = await res.json();
// 加载智能体列表
if (backendConfig.agents) {
systemAgents = backendConfig.agents.map(a => ({
id: a.agent_id,
name: a.name,
avatar: a.avatar,
category: a.category,
desc: a.description,
systemPrompt: a.system_prompt,
heat: a.heat || 0,
tags: a.tags || '',
enable_tools: a.enable_tools || ''
}));
// 初始化用户智能体列表(按类别)
systemAgents.forEach(agent => {
if (!myAgents[agent.category]) {
myAgents[agent.category] = [];
}
myAgents[agent.category].push(agent.id);
});
}
// 加载游客限制配置
if (backendConfig.system?.guestLimits) {
GUEST_LIMITS = {
maxChatSessionsPerDay: backendConfig.system.guestLimits.chatSessions || 1,
maxChatMessagesPerDay: backendConfig.system.guestLimits.chatMessages || 20,
maxAgentPerDay: 1, // 游客只能用通用助手
maxAgentMessagesPerDay: backendConfig.system.guestLimits.agentMessages || 20
};
}
// 加载工具列表
if (backendConfig.allTools) {
// 过滤掉联网搜索(已有单独按钮)
allTools = backendConfig.allTools.filter(t => t.type !== 'search');
}
// 不加载默认启用的工具,所有工具默认未启用
updateAgentsDisplay();
console.log('后台配置已加载', backendConfig);
} catch (e) {
console.error('加载后台配置失败', e);
// 使用默认配置
systemAgents = [
{ id: 'assistant', name: '通用助手', avatar: '🤖', category: 'basic', desc: '能回答各类问题,帮助写作、分析、解答疑惑', systemPrompt: '你是一个智能助手,能够回答各类问题,帮助用户解决问题。', heat: 9500, tags: 'hot' },
];
updateAgentsDisplay();
}
}
// 检查并重置每日使用统计
function checkDailyUsage() {
const today = getTodayDate();
if (dailyUsage.date !== today) {
// 新的一天,重置统计
dailyUsage = {
date: today,
chatSessions: 0,
chatMessages: 0,
agentUsed: null,
agentMessages: 0
};
saveDailyUsage();
}
}
// 保存用户状态
function saveCurrentUser() {
if (currentUser) {
localStorage.setItem('currentUser', JSON.stringify(currentUser));
} else {
localStorage.removeItem('currentUser');
}
}
// 保存每日使用统计
function saveDailyUsage() {
localStorage.setItem('dailyUsage', JSON.stringify(dailyUsage));
}
// 检查是否可以创建新对话(未登录用户)
function canCreateNewChat() {
if (currentUser) return true; // 登录用户无限制
checkDailyUsage();
return dailyUsage.chatSessions < GUEST_LIMITS.maxChatSessionsPerDay;
}
// 检查是否可以发送对话消息(未登录用户)
function canSendChatMessage() {
if (currentUser) return true; // 登录用户无限制
checkDailyUsage();
return dailyUsage.chatMessages < GUEST_LIMITS.maxChatMessagesPerDay;
}
// 检查是否可以使用智能体(未登录用户)
function canUseAgent(agentId) {
if (currentUser) return true; // 登录用户无限制
checkDailyUsage();
// 未登录用户只能使用通用助手
if (agentId !== 'assistant') {
return { allowed: false, reason: 'guest_agent_only' };
}
// 如果今天还没用智能体,可以用
if (!dailyUsage.agentUsed) {
return { allowed: true };
}
// 如果已经用了其他智能体,不能换
if (dailyUsage.agentUsed !== agentId) {
return { allowed: false, reason: 'guest_one_agent_only' };
}
return { allowed: true };
}
// 检查是否可以发送智能体消息(未登录用户)
function canSendAgentMessage() {
if (currentUser) return true; // 登录用户无限制
checkDailyUsage();
return dailyUsage.agentMessages < GUEST_LIMITS.maxAgentMessagesPerDay;
}
// 增加对话会话计数
function incrementChatSession() {
if (!currentUser) {
checkDailyUsage();
dailyUsage.chatSessions++;
saveDailyUsage();
}
}
// 增加对话消息计数
function incrementChatMessage() {
if (!currentUser) {
checkDailyUsage();
dailyUsage.chatMessages++;
saveDailyUsage();
}
}
// 设置使用的智能体(未登录用户)
function setAgentUsed(agentId) {
if (!currentUser) {
checkDailyUsage();
if (!dailyUsage.agentUsed) {
dailyUsage.agentUsed = agentId;
saveDailyUsage();
}
}
}
// 增加智能体消息计数
function incrementAgentMessage() {
if (!currentUser) {
checkDailyUsage();
dailyUsage.agentMessages++;
saveDailyUsage();
}
}
// 获取剩余配额提示
function getRemainingQuota() {
if (currentUser) return null; // 登录用户无限制
checkDailyUsage();
const chatSessionRemain = GUEST_LIMITS.maxChatSessionsPerDay - dailyUsage.chatSessions;
const chatMsgRemain = GUEST_LIMITS.maxChatMessagesPerDay - dailyUsage.chatMessages;
const agentMsgRemain = GUEST_LIMITS.maxAgentMessagesPerDay - dailyUsage.agentMessages;
return {
chatSessionRemain,
chatMsgRemain,
agentMsgRemain,
agentUsed: dailyUsage.agentUsed
};
}
// 获取使用智能体的对话列表(按时间倒序)
// 只返回真正有对话消息的记录(messages.length > 0)
function getAgentConversationHistory(limit = 5) {
return conversations
.filter(conv => conv.agentId && conv.messages && conv.messages.length > 0) // 筛选有智能体且有消息的对话
.sort((a, b) => b.updatedAt - a.updatedAt) // 按更新时间倒序
.slice(0, limit)
.map(conv => {
const agent = systemAgents.find(a => a.id === conv.agentId);
return {
...conv,
agent: agent
};
});
}
// 功能开关
let enableThinking = false; // 深度思考
let enableSearch = false; // 联网搜索
let autoScrollEnabled = true; // 自动滚动(用户滚动后可关闭)
let enabledTools = []; // 启用的工具列表(多选)
let allTools = []; // 所有可用的工具列表
// DOM 元素(初始为 null,在 openConversation 时重新获取)
let appContainer = null;
let messagesContainer = null;
let messagesDiv = null;
let userInput = null;
let sendBtn = null;
let welcome = null;
let thinkingBtn = null;
let searchBtn = null;
// 初始化
document.addEventListener('DOMContentLoaded', async () => {
// 初始化 appContainer
appContainer = document.getElementById('app');
// 加载用户登录状态(优先检查)
const savedUser = localStorage.getItem('currentUser');
if (savedUser) {
currentUser = JSON.parse(savedUser);
}
// 如果用户已登录且有ID,从 backend 加载对话数据
if (currentUser && currentUser.id) {
try {
const res = await fetch(`/api/user/${currentUser.id}/conversations`);
const data = await res.json();
if (Array.isArray(data) && data.length > 0) {
// 使用 backend 数据替换本地数据
conversations = data;
// 更新本地存储(离线可用)
localStorage.setItem('conversations', JSON.stringify(conversations));
} else {
// backend 无数据,使用本地数据
const saved = localStorage.getItem('conversations');
if (saved) {
conversations = JSON.parse(saved);
}
}
} catch (e) {
// backend 加载失败,使用本地数据
console.error('加载 backend 对话失败:', e);
const saved = localStorage.getItem('conversations');
if (saved) {
conversations = JSON.parse(saved);
}
}
} else {
// 未登录用户,从本地存储加载对话列表
const saved = localStorage.getItem('conversations');
if (saved) {
conversations = JSON.parse(saved);
}
}
// 兼容旧数据格式(chat_history)
const oldHistory = localStorage.getItem('chat_history');
if (oldHistory && conversations.length === 0) {
const oldMessages = JSON.parse(oldHistory);
if (oldMessages.length > 0) {
// 转换旧数据为新格式
const convertedConv = {
id: Date.now().toString(),
title: oldMessages[0].content.slice(0, 30) + (oldMessages[0].content.length > 30 ? '...' : ''),
messages: oldMessages,
createdAt: Date.now(),
updatedAt: Date.now()
};
conversations.push(convertedConv);
saveConversations();
localStorage.removeItem('chat_history'); // 清理旧数据
}
}
// 加载用户智能体数据(我的智能体)
const savedMyAgents = localStorage.getItem('myAgents');
if (savedMyAgents) {
myAgents = JSON.parse(savedMyAgents);
}
// 加载收藏的智能体
const savedFavoriteAgents = localStorage.getItem('favoriteAgents');
if (savedFavoriteAgents) {
favoriteAgents = JSON.parse(savedFavoriteAgents);
}
// 加载置顶的智能体
const savedPinnedAgents = localStorage.getItem('pinnedAgents');
if (savedPinnedAgents) {
pinnedAgents = JSON.parse(savedPinnedAgents);
}
// 如果用户已登录且有ID,从 backend 加载智能体配置
if (currentUser && currentUser.id) {
try {
const res = await fetch(`/api/user/${currentUser.id}/agents`);
const data = await res.json();
if (data.myAgents) {
myAgents = data.myAgents;
localStorage.setItem('myAgents', JSON.stringify(myAgents));
}
if (data.favoriteAgents) {
favoriteAgents = data.favoriteAgents;
localStorage.setItem('favoriteAgents', JSON.stringify(favoriteAgents));
}
if (data.pinnedAgents) {
pinnedAgents = data.pinnedAgents;
localStorage.setItem('pinnedAgents', JSON.stringify(pinnedAgents));
}
} catch (e) {
console.error('加载智能体配置失败:', e);
}
}
// 加载每日使用统计
const savedDailyUsage = localStorage.getItem('dailyUsage');
if (savedDailyUsage) {
dailyUsage = JSON.parse(savedDailyUsage);
}
// 检查并重置每日使用统计
checkDailyUsage();
// 根据用户配置更新显示的智能体
// updateAgentsDisplay(); // 先不调用,等后台配置加载后再更新
// 加载当前页面状态
const savedPage = localStorage.getItem('currentPage');
if (savedPage) {
currentPage = savedPage;
}
// 加载后台配置并显示主页
loadBackendConfig().then(() => {
showMainPage();
}).catch(() => {
// 即使加载失败也显示主页
showMainPage();
});
});
// 根据用户配置更新显示的智能体
function updateAgentsDisplay() {
agents = [];
Object.keys(myAgents).forEach(category => {
myAgents[category].forEach(agentId => {
const agent = systemAgents.find(a => a.id === agentId);
if (agent) {
agents.push({
...agent,
is_pinned: pinnedAgents[category]?.includes(agentId) || false,
is_favorite: favoriteAgents.includes(agentId)
});
}
});
});
}
// 保存用户智能体配置
function saveMyAgents() {
localStorage.setItem('myAgents', JSON.stringify(myAgents));
updateAgentsDisplay();
}
// 保存收藏的智能体
function saveFavoriteAgents() {
localStorage.setItem('favoriteAgents', JSON.stringify(favoriteAgents));
}
// 保存置顶的智能体
function savePinnedAgents() {
localStorage.setItem('pinnedAgents', JSON.stringify(pinnedAgents));
}
// ==================== 主页(底部导航栏) ====================
function showMainPage() {
currentConversation = null;
currentAgent = null;
const mainHtml = `
`;
appContainer.innerHTML = mainHtml;
// 绑定底部导航事件
document.querySelectorAll('.nav-item').forEach(item => {
item.addEventListener('click', () => {
const page = item.getAttribute('data-page');
switchPage(page);
});
});
// 根据当前页面绑定其他事件
bindPageEvents();
}
// 渲染当前页面内容
function renderCurrentPage() {
switch (currentPage) {
case 'chats':
return renderChatsPage();
case 'agents':
return renderAgentsPage();
case 'profile':
return renderProfilePage();
default:
return renderChatsPage();
}
}
// 切换页面
function switchPage(page) {
currentPage = page;
localStorage.setItem('currentPage', page);
// 先确保页面结构正确(如果当前不在主页结构)
const mainContent = document.getElementById('mainContent');
if (!mainContent) {
// 重新渲染主页结构
showMainPage();
return; // showMainPage 会自动调用 renderCurrentPage
}
// 更新导航栏状态
document.querySelectorAll('.nav-item').forEach(item => {
item.classList.toggle('active', item.getAttribute('data-page') === page);
});
// 更新内容区
mainContent.innerHTML = renderCurrentPage();
// 绑定页面事件
bindPageEvents();
}
// 绑定页面事件
function bindPageEvents() {
switch (currentPage) {
case 'chats':
bindChatsPageEvents();
break;
case 'agents':
bindAgentsPageEvents();
break;
case 'profile':
bindProfilePageEvents();
break;
}
}
// ==================== 对话页面 ====================
function renderChatsPage() {
// 只显示没有智能体的普通对话
const normalConversations = conversations.filter(conv => !conv.agentId);
return `
${normalConversations.length === 0
? '
暂无对话记录
点击右上角 + 开始新对话
'
: sortNormalConversations(normalConversations).map(conv => `
${conv.is_pinned ? '
📌' : ''}
${escapeHtml(conv.title)}
${conv.messages.length} 条消息 · ${formatTime(conv.updatedAt)}
`).join('')
}
`;
}
function bindChatsPageEvents() {
const newChatBtn = document.getElementById('newChatBtn');
if (newChatBtn) {
newChatBtn.addEventListener('click', createNewConversation);
}
// 搜索功能
const searchToggleBtn = document.getElementById('searchToggleBtn');
const searchBar = document.getElementById('searchBar');
const searchInput = document.getElementById('searchInput');
const searchCloseBtn = document.getElementById('searchCloseBtn');
const searchResults = document.getElementById('searchResults');
if (searchToggleBtn) {
searchToggleBtn.addEventListener('click', () => {
if (searchBar) {
searchBar.classList.add('show');
if (searchInput) {
searchInput.focus();
}
}
});
}
if (searchCloseBtn) {
searchCloseBtn.addEventListener('click', () => {
hideSearchBarInChats();
});
}
if (searchInput) {
searchInput.addEventListener('input', (e) => {
const keyword = e.target.value.trim();
if (keyword) {
searchConversations(keyword);
} else {
if (searchResults) searchResults.innerHTML = '';
}
});
searchInput.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
hideSearchBarInChats();
}
});
}
if (searchResults) {
searchResults.addEventListener('click', (e) => {
const item = e.target.closest('.search-result-item');
if (item) {
const id = item.getAttribute('data-id');
hideSearchBarInChats();
openConversation(id);
}
});
}
function hideSearchBarInChats() {
if (searchBar) {
searchBar.classList.remove('show');
}
if (searchInput) {
searchInput.value = '';
}
if (searchResults) {
searchResults.innerHTML = '';
}
}
const conversationList = document.getElementById('conversationList');
if (conversationList) {
conversationList.addEventListener('click', (e) => {
const item = e.target.closest('.conversation-item');
if (item) {
const id = item.getAttribute('data-id');
openConversation(id);
}
});
// 长按事件
setupLongPressEvents(conversationList);
}
// 操作菜单事件
const actionMenu = document.getElementById('actionMenu');
if (actionMenu) {
actionMenu.addEventListener('click', (e) => {
const item = e.target.closest('.action-menu-item');
if (item && currentActionConvId) {
const action = item.getAttribute('data-action');
handleActionMenuAction(action, currentActionConvId);
hideActionMenu();
}
});
// 点击其他地方关闭菜单
document.addEventListener('click', (e) => {
if (actionMenu.classList.contains('show') && !actionMenu.contains(e.target)) {
hideActionMenu();
}
});
}
}
// ==================== 智能体页面 ====================
function renderAgentsPage() {
// 根据后台配置筛选智能体
// 热门智能体 = 标签中包含 "hot"
const hotAgents = agents.filter(a => a.tags && a.tags.split(',').includes('hot'));
// 其他类别 = 根据 category 字段分类
const basicAgents = agents.filter(a => a.category === 'basic');
const workAgents = agents.filter(a => a.category === 'work');
const studyAgents = agents.filter(a => a.category === 'study');
const lifeAgents = agents.filter(a => a.category === 'life');
// 获取使用智能体的对话历史(最多5个)
const recentAgentConvos = getAgentConversationHistory(5);
const totalAgentConvos = conversations.filter(c => c.agentId).length;
// 是否有收藏的智能体
const hasFavorites = favoriteAgents.length > 0;
return `
${recentAgentConvos.length > 0 ? `
🕐 最近使用
${totalAgentConvos > 5 ? `
更多>>
` : ''}
${recentAgentConvos.map(conv => `
${conv.agent ? conv.agent.avatar : '🤖'}
${conv.title}
${conv.agent ? conv.agent.name : '未知智能体'}
${formatTime(conv.updatedAt)}
`).join('')}
` : ''}
${hotAgents.length > 0 ? `
🔥 热门智能体
${hotAgents.map(agent => renderAgentCard(agent)).join('')}
` : ''}
${workAgents.length > 0 ? `
💼 工作助手
${workAgents.map(agent => renderAgentCard(agent)).join('')}
` : ''}
${studyAgents.length > 0 ? `
📚 学习助手
${studyAgents.map(agent => renderAgentCard(agent)).join('')}
` : ''}
${lifeAgents.length > 0 ? `
🏠 生活助手
${lifeAgents.map(agent => renderAgentCard(agent)).join('')}
` : ''}
`;
}
// 渲染智能体卡片
function renderAgentCard(agent) {
const pinnedClass = agent.is_pinned ? 'pinned' : '';
const favoriteClass = agent.is_favorite ? 'favorite' : '';
return `
${agent.is_pinned ? '
📌' : ''}
${agent.is_favorite ? '
⭐' : ''}
${agent.avatar}
${agent.name}
${agent.desc}
`;
}
function bindAgentsPageEvents() {
// 发现智能体按钮
const agentDiscoverBtn = document.getElementById('agentDiscoverBtn');
if (agentDiscoverBtn) {
agentDiscoverBtn.addEventListener('click', () => {
showAgentDiscoverPage();
});
}
// 收藏夹按钮
const agentFavoriteBtn = document.getElementById('agentFavoriteBtn');
if (agentFavoriteBtn) {
agentFavoriteBtn.addEventListener('click', () => {
showAgentFavoritePage();
});
}
// 智能体卡片点击(新建对话)
document.querySelectorAll('.agent-card').forEach(card => {
card.addEventListener('click', (e) => {
// 如果是长按后的点击,不触发
if (card.classList.contains('long-pressed')) {
card.classList.remove('long-pressed');
return;
}
const id = card.getAttribute('data-id');
openAgent(id);
});
// 长按事件
setupAgentLongPress(card);
});
// 最近使用对话点击(打开已有对话)
document.querySelectorAll('.recent-agent-item').forEach(item => {
item.addEventListener('click', () => {
const convId = item.getAttribute('data-conv-id');
if (convId) {
openConversation(convId);
}
});
});
// 查看全部历史使用
const showAllRecentBtn = document.getElementById('showAllRecentBtn');
if (showAllRecentBtn) {
showAllRecentBtn.addEventListener('click', () => {
showAgentHistoryPage();
});
}
// 智能体操作菜单事件
const agentActionMenu = document.getElementById('agentActionMenu');
if (agentActionMenu) {
agentActionMenu.addEventListener('click', (e) => {
const item = e.target.closest('.agent-action-item');
if (item && currentActionAgentId) {
const action = item.getAttribute('data-action');
handleAgentAction(action, currentActionAgentId);
hideAgentActionMenu();
}
});
// 点击其他地方关闭菜单
document.addEventListener('click', (e) => {
if (agentActionMenu.classList.contains('show') && !agentActionMenu.contains(e.target)) {
hideAgentActionMenu();
}
});
}
}
// 当前操作的智能体ID
let currentActionAgentId = null;
// 智能体长按事件
function setupAgentLongPress(card) {
let longPressTimer = null;
card.addEventListener('touchstart', (e) => {
longPressTimer = setTimeout(() => {
currentActionAgentId = card.getAttribute('data-id');
card.classList.add('long-pressed');
showAgentActionMenu(currentActionAgentId);
}, 500);
});
card.addEventListener('touchend', () => {
if (longPressTimer) {
clearTimeout(longPressTimer);
longPressTimer = null;
}
});
card.addEventListener('touchmove', () => {
if (longPressTimer) {
clearTimeout(longPressTimer);
longPressTimer = null;
}
});
// 鼠标长按(PC端)
card.addEventListener('mousedown', (e) => {
longPressTimer = setTimeout(() => {
currentActionAgentId = card.getAttribute('data-id');
card.classList.add('long-pressed');
showAgentActionMenu(currentActionAgentId);
}, 500);
});
card.addEventListener('mouseup', () => {
if (longPressTimer) {
clearTimeout(longPressTimer);
longPressTimer = null;
}
});
card.addEventListener('mouseleave', () => {
if (longPressTimer) {
clearTimeout(longPressTimer);
longPressTimer = null;
}
});
}
// 显示智能体操作菜单
function showAgentActionMenu(agentId) {
const agentActionMenu = document.getElementById('agentActionMenu');
const agent = systemAgents.find(a => a.id === agentId);
const displayAgent = agents.find(a => a.id === agentId);
if (!agentActionMenu || !displayAgent) return;
// 更新按钮文字
const pinText = document.getElementById('agentPinText');
const favoriteText = document.getElementById('agentFavoriteText');
if (pinText) {
pinText.textContent = displayAgent.is_pinned ? '取消置顶' : '置顶';
}
if (favoriteText) {
favoriteText.textContent = displayAgent.is_favorite ? '取消收藏' : '收藏';
}
agentActionMenu.classList.add('show');
}
// 隐藏智能体操作菜单
function hideAgentActionMenu() {
const agentActionMenu = document.getElementById('agentActionMenu');
if (agentActionMenu) {
agentActionMenu.classList.remove('show');
}
currentActionAgentId = null;
}
// 处理智能体操作
function handleAgentAction(action, agentId) {
const agent = systemAgents.find(a => a.id === agentId);
if (!agent) return;
switch (action) {
case 'pin':
toggleAgentPin(agentId);
break;
case 'favorite':
toggleAgentFavorite(agentId);
break;
case 'remove':
removeAgentFromMyAgents(agentId);
break;
}
// 刷新页面
switchPage('agents');
}
// 置顶/取消置顶智能体
function toggleAgentPin(agentId) {
const agent = agents.find(a => a.id === agentId);
if (!agent) return;
const category = agent.category;
let is_pinned;
if (pinnedAgents[category]?.includes(agentId)) {
// 取消置顶
pinnedAgents[category] = pinnedAgents[category].filter(id => id !== agentId);
agent.is_pinned = false;
is_pinned = 0;
showToast('已取消置顶');
} else {
// 置顶
if (!pinnedAgents[category]) {
pinnedAgents[category] = [];
}
pinnedAgents[category].push(agentId);
agent.is_pinned = true;
is_pinned = 1;
showToast('已置顶');
}
savePinnedAgents();
saveMyAgents(); // 更新显示
// 同步到 backend
if (currentUser && currentUser.id) {
fetch(`/api/user/${currentUser.id}/agents/${agentId}/pin`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ is_pinned, category })
}).catch(e => console.error('同步置顶失败:', e));
}
}
// 收藏/取消收藏智能体
function toggleAgentFavorite(agentId) {
const agent = agents.find(a => a.id === agentId);
if (!agent) return;
let is_favorite;
if (favoriteAgents.includes(agentId)) {
// 取消收藏
favoriteAgents = favoriteAgents.filter(id => id !== agentId);
agent.is_favorite = false;
is_favorite = 0;
showToast('已取消收藏');
} else {
// 收藏
favoriteAgents.push(agentId);
agent.is_favorite = true;
is_favorite = 1;
showToast('已收藏');
}
saveFavoriteAgents();
saveMyAgents(); // 更新显示
// 同步到 backend
if (currentUser && currentUser.id) {
fetch(`/api/user/${currentUser.id}/agents/${agentId}/favorite`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ is_favorite, category: agent.category })
}).catch(e => console.error('同步收藏失败:', e));
}
}
// 从用户智能体列表移除
function removeAgentFromMyAgents(agentId) {
const agent = systemAgents.find(a => a.id === agentId);
if (!agent) return;
const category = agent.category;
// 从对应类别移除
if (myAgents[category]) {
myAgents[category] = myAgents[category].filter(id => id !== agentId);
}
// 同时取消置顶
if (pinnedAgents[category]) {
pinnedAgents[category] = pinnedAgents[category].filter(id => id !== agentId);
}
// 同时取消收藏
if (favoriteAgents.includes(agentId)) {
favoriteAgents = favoriteAgents.filter(id => id !== agentId);
}
saveMyAgents();
savePinnedAgents();
saveFavoriteAgents();
showToast('已移除');
// 同步到 backend
if (currentUser && currentUser.id) {
fetch(`/api/user/${currentUser.id}/agents/${agentId}`, {
method: 'DELETE'
}).catch(e => console.error('同步移除失败:', e));
}
}
// ==================== 智能体发现页面 ====================
function showAgentDiscoverPage() {
// 按热度排序所有系统智能体
const sortedAgents = [...systemAgents].sort((a, b) => b.heat - a.heat);
// 分类显示
// 热门智能体 = 标签中包含 "hot"
const hotAgents = systemAgents.filter(a => a.tags && a.tags.split(',').includes('hot'));
// 其他类别
const basicAgents = systemAgents.filter(a => a.category === 'basic');
const workAgents = systemAgents.filter(a => a.category === 'work');
const studyAgents = systemAgents.filter(a => a.category === 'study');
const lifeAgents = systemAgents.filter(a => a.category === 'life');
const discoverHtml = `
${hotAgents.length > 0 ? `
🔥 热门智能体
${hotAgents.map(agent => renderDiscoverCard(agent)).join('')}
` : ''}
${basicAgents.length > 0 ? `
⭐ 基础智能体
${basicAgents.map(agent => renderDiscoverCard(agent)).join('')}
` : ''}
${workAgents.length > 0 ? `
💼 工作助手
${workAgents.map(agent => renderDiscoverCard(agent)).join('')}
` : ''}
${studyAgents.length > 0 ? `
📚 学习助手
${studyAgents.map(agent => renderDiscoverCard(agent)).join('')}
` : ''}
${lifeAgents.length > 0 ? `
🏠 生活助手
${lifeAgents.map(agent => renderDiscoverCard(agent)).join('')}
` : ''}
`;
appContainer.innerHTML = discoverHtml;
// 绑定返回按钮
const backBtn = document.getElementById('discoverBackBtn');
if (backBtn) {
backBtn.addEventListener('click', () => {
showMainPage();
});
}
// 绑定搜索
const searchInput = document.getElementById('discoverSearchInput');
if (searchInput) {
searchInput.addEventListener('input', (e) => {
const keyword = e.target.value.trim().toLowerCase();
filterDiscoverAgents(keyword);
});
}
// 绑定添加/收藏按钮
bindDiscoverCardEvents();
}
// 渲染发现页面智能体卡片
function renderDiscoverCard(agent) {
const isAdded = myAgents[agent.category]?.includes(agent.id);
const isFavorite = favoriteAgents.includes(agent.id);
// 格式化热度显示
let heatDisplay = '';
if (agent.heat >= 9000) {
heatDisplay = '🔥🔥🔥';
} else if (agent.heat >= 6000) {
heatDisplay = '🔥🔥';
} else if (agent.heat >= 3000) {
heatDisplay = '🔥';
} else {
heatDisplay = `${Math.floor(agent.heat / 1000)}k`;
}
return `
${agent.desc}
`;
}
// 获取类别标签
function getCategoryLabel(category) {
const labels = {
hot: '热门',
work: '工作',
study: '学习',
life: '生活'
};
return labels[category] || category;
}
// 搜索过滤发现页面智能体
function filterDiscoverAgents(keyword) {
if (!keyword) {
// 显示所有
showDiscoverSection('hot', systemAgents.filter(a => a.category === 'hot'));
showDiscoverSection('basic', systemAgents.filter(a => a.category === 'basic'));
showDiscoverSection('work', systemAgents.filter(a => a.category === 'work'));
showDiscoverSection('study', systemAgents.filter(a => a.category === 'study'));
showDiscoverSection('life', systemAgents.filter(a => a.category === 'life'));
return;
}
// 搜索所有智能体
const filtered = systemAgents.filter(agent =>
agent.name.toLowerCase().includes(keyword) ||
agent.desc.toLowerCase().includes(keyword)
);
// 显示搜索结果
showDiscoverSection('hot', filtered.filter(a => a.category === 'hot'));
showDiscoverSection('basic', filtered.filter(a => a.category === 'basic'));
showDiscoverSection('work', filtered.filter(a => a.category === 'work'));
showDiscoverSection('study', filtered.filter(a => a.category === 'study'));
showDiscoverSection('life', filtered.filter(a => a.category === 'life'));
}
// 显示发现页面某个类别的智能体
function showDiscoverSection(category, agents) {
const gridId = `discover${category.charAt(0).toUpperCase() + category.slice(1)}Grid`;
const grid = document.getElementById(gridId);
if (grid) {
grid.innerHTML = agents.map(agent => renderDiscoverCard(agent)).join('');
bindDiscoverCardEvents();
}
}
// 绑定发现页面卡片按钮事件
function bindDiscoverCardEvents() {
document.querySelectorAll('.discover-action-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const agentId = btn.getAttribute('data-id');
const action = btn.getAttribute('data-action');
if (action === 'add') {
addAgentToMyAgents(agentId);
btn.classList.toggle('added');
btn.textContent = btn.classList.contains('added') ? '已添加' : '+ 添加';
} else if (action === 'favorite') {
toggleAgentFavoriteFromDiscover(agentId);
btn.classList.toggle('favorited');
btn.textContent = btn.classList.contains('favorited') ? '★ 已收藏' : '☆ 收藏';
}
});
});
}
// 添加智能体到用户列表
function addAgentToMyAgents(agentId) {
const agent = systemAgents.find(a => a.id === agentId);
if (!agent) return;
// 检查是否已添加
if (myAgents[agent.category]?.includes(agentId)) {
showToast('已添加过该智能体');
return;
}
// 添加到对应类别
if (!myAgents[agent.category]) {
myAgents[agent.category] = [];
}
myAgents[agent.category].push(agentId);
saveMyAgents();
showToast(`已添加 ${agent.name}`);
// 同步到 backend
if (currentUser && currentUser.id) {
fetch(`/api/user/${currentUser.id}/agents/${agentId}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ category: agent.category })
}).catch(e => console.error('同步添加智能体失败:', e));
}
}
// 从发现页面收藏智能体
function toggleAgentFavoriteFromDiscover(agentId) {
const agent = systemAgents.find(a => a.id === agentId);
if (!agent) return;
if (favoriteAgents.includes(agentId)) {
favoriteAgents = favoriteAgents.filter(id => id !== agentId);
showToast('已取消收藏');
} else {
favoriteAgents.push(agentId);
showToast('已收藏');
// 如果没有添加到用户列表,自动添加
if (!myAgents[agent.category]?.includes(agentId)) {
addAgentToMyAgents(agentId);
}
}
saveFavoriteAgents();
}
// ==================== 智能体收藏夹页面 ====================
function showAgentFavoritePage() {
// 获取收藏的智能体详情
const favoriteAgentsList = favoriteAgents.map(id => {
const agent = systemAgents.find(a => a.id === id);
return agent ? { ...agent, is_favorite: true } : null;
}).filter(a => a);
const favoriteHtml = `
${favoriteAgentsList.length === 0
? '
⭐
暂无收藏的智能体
点击右上角搜索按钮发现更多智能体
'
: `
${favoriteAgentsList.map(agent => `
${agent.avatar}
${agent.name}
${agent.desc}
`).join('')}
`}
`;
appContainer.innerHTML = favoriteHtml;
// 绑定返回按钮
const backBtn = document.getElementById('favoriteBackBtn');
if (backBtn) {
backBtn.addEventListener('click', () => {
showMainPage();
});
}
// 绑定智能体卡片点击(进入对话)
document.querySelectorAll('.favorite-agent-card').forEach(card => {
card.addEventListener('click', (e) => {
// 如果点击的是取消收藏按钮,不进入对话
if (e.target.classList.contains('favorite-agent-unfav')) {
return;
}
const agentId = card.getAttribute('data-id');
openAgent(agentId);
});
});
// 绑定取消收藏按钮
document.querySelectorAll('.favorite-agent-unfav').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const agentId = btn.getAttribute('data-id');
toggleAgentFavorite(agentId);
// 刷新收藏夹页面
showAgentFavoritePage();
});
});
}
// ==================== 我的页面 ====================
function renderProfilePage() {
const quota = getRemainingQuota();
const userAvatar = currentUser?.avatar || '👤';
const userSignature = currentUser?.signature || '这个人很懒,什么都没写~';
// 渲染头像(支持 emoji 和上传图片)
const avatarHtml = renderAvatar(userAvatar);
return `
${avatarHtml}
${currentUser ? currentUser.username : '游客'}
${currentUser ? userSignature : '登录后解锁更多功能'}
${currentUser
? `
›
`
: `
`
}
${!currentUser && quota ? `
今日使用配额(游客)
对话会话
${quota.chatSessionRemain}/${GUEST_LIMITS.maxChatSessionsPerDay}
对话消息
${quota.chatMsgRemain}/${GUEST_LIMITS.maxChatMessagesPerDay}
智能体消息
${quota.agentMsgRemain}/${GUEST_LIMITS.maxAgentMessagesPerDay}
登录后解锁全部功能
` : ''}
${currentUser ? `
` : ''}
`;
}
function bindProfilePageEvents() {
// 用户卡片点击(已登录用户进入编辑页面)
const userCard = document.getElementById('userCard');
if (userCard && currentUser) {
userCard.addEventListener('click', () => {
showEditProfilePage();
});
}
// 小登录按钮
const loginBtnSmall = document.getElementById('loginBtnSmall');
if (loginBtnSmall) {
loginBtnSmall.addEventListener('click', () => {
showLoginPage();
});
}
// 小注册按钮
const registerBtnSmall = document.getElementById('registerBtnSmall');
if (registerBtnSmall) {
registerBtnSmall.addEventListener('click', () => {
showRegisterPage();
});
}
// 用户编辑按钮
const editProfileBtn = document.getElementById('editProfileBtn');
if (editProfileBtn) {
editProfileBtn.addEventListener('click', () => {
showEditProfilePage();
});
}
// 消息通知按钮
const notificationBtn = document.getElementById('notificationBtn');
if (notificationBtn) {
notificationBtn.addEventListener('click', () => {
showNotificationPage();
});
}
// 隐私安全按钮
const privacyBtn = document.getElementById('privacyBtn');
if (privacyBtn) {
privacyBtn.addEventListener('click', () => {
showPrivacyPage();
});
}
// 帮助与反馈按钮
const helpBtn = document.getElementById('helpBtn');
if (helpBtn) {
helpBtn.addEventListener('click', () => {
showHelpPage();
});
}
// 关于按钮
const aboutBtn = document.getElementById('aboutBtn');
if (aboutBtn) {
aboutBtn.addEventListener('click', () => {
showAboutPage();
});
}
// 退出登录按钮
const logoutBtn = document.getElementById('logoutBtn');
if (logoutBtn) {
logoutBtn.addEventListener('click', () => {
if (confirm('确定要退出登录吗?')) {
currentUser = null;
saveCurrentUser();
showToast('已退出登录');
switchPage('profile');
}
});
}
// 清除数据按钮
const clearDataBtn = document.getElementById('clearDataBtn');
if (clearDataBtn) {
clearDataBtn.addEventListener('click', () => {
if (confirm('确定要清除所有数据吗?这将删除所有对话记录和账户信息。')) {
localStorage.clear();
conversations = [];
currentUser = null;
dailyUsage = { date: null, chatSessions: 0, chatMessages: 0, agentUsed: null, agentMessages: 0 };
showToast('数据已清除');
showMainPage();
}
});
}
}
// ==================== 用户编辑页面 ====================
function showEditProfilePage() {
if (!currentUser) {
showToast('请先登录');
return;
}
// 如果用户已登录且有ID,从 backend 获取完整用户信息
if (currentUser.id) {
fetch(`/api/admin/users/${currentUser.id}`)
.then(res => res.json())
.then(data => {
if (data && data.id) {
// 更新 currentUser 的完整信息
currentUser.avatar = data.avatar || '👤';
currentUser.signature = data.signature || '';
currentUser.email = data.email || '';
currentUser.gender = data.gender || '';
currentUser.age = data.age || '';
currentUser.region = data.region || '';
currentUser.phone = data.phone || '';
// 渲染编辑页面
renderEditProfilePage();
} else {
// 使用本地数据
renderEditProfilePage();
}
})
.catch(e => {
// 使用本地数据
renderEditProfilePage();
});
} else {
// 游客用户,使用本地数据
renderEditProfilePage();
}
}
function renderEditProfilePage() {
const avatar = currentUser.avatar || '👤';
const signature = currentUser.signature || '';
const phone = currentUser.phone || '';
const email = currentUser.email || '';
const gender = currentUser.gender || '';
const age = currentUser.age || '';
const region = currentUser.region || '';
// 渲染头像预览(支持 emoji 和上传图片)
const avatarPreviewHtml = renderAvatar(avatar);
const editHtml = `
${avatarPreviewHtml}
点击更换头像
${['👤', '😊', '😎', '🤓', '🧑', '👨', '👩', '🧒', '👴', '👵', '🦸', '🧙', '🤖', '👽', '🥷'].map(a => `
${a}
`).join('')}
`;
appContainer.innerHTML = editHtml;
// 绑定返回按钮
const backBtn = document.getElementById('editBackBtn');
if (backBtn) {
backBtn.addEventListener('click', () => {
switchPage('profile');
});
}
// 绑定头像选择
document.querySelectorAll('.avatar-option').forEach(option => {
option.addEventListener('click', () => {
const newAvatar = option.getAttribute('data-avatar');
document.getElementById('editAvatarPreview').textContent = newAvatar;
document.querySelectorAll('.avatar-option').forEach(o => o.classList.remove('selected'));
option.classList.add('selected');
// 清除上传的头像标记
uploadedAvatarData = null;
});
});
// 绑定头像上传按钮
const uploadBtn = document.getElementById('avatarUploadBtn');
const uploadInput = document.getElementById('avatarUploadInput');
if (uploadBtn && uploadInput) {
uploadBtn.addEventListener('click', () => {
uploadInput.click();
});
uploadInput.addEventListener('change', (e) => {
const file = e.target.files[0];
if (file) {
handleAvatarUpload(file);
}
});
}
// 绑定性别选择
document.querySelectorAll('.gender-option').forEach(option => {
option.addEventListener('click', () => {
document.querySelectorAll('.gender-option').forEach(o => o.classList.remove('selected'));
option.classList.add('selected');
});
});
// 绑定保存按钮
const saveBtn = document.getElementById('saveProfileBtn');
if (saveBtn) {
saveBtn.addEventListener('click', handleSaveProfile);
}
}
// 上传头像变量
let uploadedAvatarData = null;
// 处理头像上传
async function handleAvatarUpload(file) {
// 验证文件类型
if (!file.type.startsWith('image/')) {
showToast('请上传图片文件');
return;
}
// 验证文件大小(最大 2MB)
if (file.size > 2 * 1024 * 1024) {
showToast('图片大小不能超过2MB');
return;
}
try {
// 读取文件为 base64
const reader = new FileReader();
reader.onload = async (e) => {
const base64Data = e.target.result;
// 显示预览
const previewEl = document.getElementById('editAvatarPreview');
if (previewEl) {
previewEl.innerHTML = `
`;
}
// 清除预设头像选中状态
document.querySelectorAll('.avatar-option').forEach(o => o.classList.remove('selected'));
// 保存上传的头像数据
uploadedAvatarData = base64Data;
showToast('头像已加载,保存后生效');
};
reader.readAsDataURL(file);
} catch (e) {
showToast('头像加载失败');
}
}
async function handleSaveProfile() {
// 检查是否有上传的头像
let newAvatar = uploadedAvatarData;
// 如果没有上传头像,使用预设头像
if (!newAvatar) {
newAvatar = document.querySelector('.avatar-option.selected')?.getAttribute('data-avatar') || currentUser.avatar || '👤';
}
const newUsername = document.getElementById('editUsername')?.value.trim();
const newSignature = document.getElementById('editSignature')?.value.trim();
const newEmail = document.getElementById('editEmail')?.value.trim();
const newGender = document.querySelector('.gender-option.selected')?.getAttribute('data-gender') || '';
const newAge = document.getElementById('editAge')?.value;
const newRegion = document.getElementById('editRegion')?.value.trim();
// 验证用户名
if (!newUsername || newUsername.length < 2 || newUsername.length > 20) {
showToast('用户名需要2-20个字符');
return;
}
// 验证邮箱
if (newEmail && !validateEmail(newEmail)) {
showToast('请输入正确的邮箱格式');
return;
}
// 验证年龄
if (newAge && (parseInt(newAge) < 1 || parseInt(newAge) > 120)) {
showToast('年龄需要在1-120之间');
return;
}
// 更新 currentUser
currentUser.avatar = newAvatar;
currentUser.username = newUsername;
currentUser.signature = newSignature;
currentUser.gender = newGender;
currentUser.age = newAge ? parseInt(newAge) : '';
currentUser.region = newRegion;
currentUser.email = newEmail;
// 如果用户已登录,调用 backend API 更新
if (currentUser.id) {
// 先处理头像上传(如果有上传的头像)
if (uploadedAvatarData) {
try {
const avatarRes = await fetch(`/api/user/${currentUser.id}/avatar`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ avatar: uploadedAvatarData })
});
const avatarData = await avatarRes.json();
if (avatarData.success && avatarData.avatar_url) {
// 使用上传后的头像 URL
newAvatar = avatarData.avatar;
currentUser.avatar = newAvatar;
} else {
showToast(avatarData.error || '头像上传失败');
return;
}
} catch (e) {
showToast('头像上传失败,请重试');
return;
}
}
// 更新用户资料
fetch(`/api/user/${currentUser.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: newUsername,
email: newEmail,
avatar: newAvatar,
signature: newSignature,
gender: newGender,
age: newAge ? parseInt(newAge) : null,
region: newRegion
})
})
.then(res => res.json())
.then(data => {
if (data.success) {
saveCurrentUser();
showToast('保存成功');
switchPage('profile');
} else {
showToast(data.error || '保存失败');
}
})
.catch(e => {
showToast('网络错误,请重试');
});
} else {
// 未登录用户,保存到本地
saveCurrentUser();
showToast('保存成功');
switchPage('profile');
}
}
// ==================== 消息通知页面 ====================
function showNotificationPage() {
// 加载通知设置
const settings = JSON.parse(localStorage.getItem('notificationSettings') || '{}');
const enableNotification = settings.enableNotification !== false;
const enableSound = settings.enableSound !== false;
const enableVibration = settings.enableVibration !== false;
const quietHoursStart = settings.quietHoursStart || '23:00';
const quietHoursEnd = settings.quietHoursEnd || '08:00';
const notificationHtml = `
`;
appContainer.innerHTML = notificationHtml;
// 绑定返回按钮
const backBtn = document.getElementById('notificationBackBtn');
if (backBtn) {
backBtn.addEventListener('click', () => {
saveNotificationSettings();
switchPage('profile');
});
}
// 绑定开关切换
document.querySelectorAll('.settings-toggle').forEach(toggle => {
toggle.addEventListener('click', () => {
toggle.classList.toggle('active');
});
});
}
function saveNotificationSettings() {
const settings = {
enableNotification: document.getElementById('toggleNotification')?.classList.contains('active'),
enableSound: document.getElementById('toggleSound')?.classList.contains('active'),
enableVibration: document.getElementById('toggleVibration')?.classList.contains('active'),
quietHoursStart: document.getElementById('quietHoursStart')?.value || '23:00',
quietHoursEnd: document.getElementById('quietHoursEnd')?.value || '08:00'
};
localStorage.setItem('notificationSettings', JSON.stringify(settings));
}
// ==================== 隐私安全页面 ====================
function showPrivacyPage() {
const privacyHtml = `
账号安全
${currentUser ? `
` : `
登录后可管理账号安全
`}
数据安全
数据本地存储
所有数据存储在本地,不上传服务器
安全
`;
appContainer.innerHTML = privacyHtml;
// 绑定返回按钮
const backBtn = document.getElementById('privacyBackBtn');
if (backBtn) {
backBtn.addEventListener('click', () => {
switchPage('profile');
});
}
// 修改密码
const changePasswordBtn = document.getElementById('changePasswordBtn');
if (changePasswordBtn) {
changePasswordBtn.addEventListener('click', () => {
showChangePasswordPage();
});
}
// 账号信息
const showAccountInfoBtn = document.getElementById('showAccountInfoBtn');
if (showAccountInfoBtn) {
showAccountInfoBtn.addEventListener('click', () => {
showAccountInfoDialog();
});
}
// 清除聊天记录
const clearChatsBtn = document.getElementById('clearChatsBtn');
if (clearChatsBtn) {
clearChatsBtn.addEventListener('click', () => {
if (confirm('确定要清除所有聊天记录吗?')) {
conversations = [];
saveConversations();
showToast('聊天记录已清除');
}
});
}
// 清除缓存
const clearCacheBtn = document.getElementById('clearCacheBtn');
if (clearCacheBtn) {
clearCacheBtn.addEventListener('click', () => {
// 清除临时数据,保留用户数据
localStorage.removeItem('currentPage');
localStorage.removeItem('dailyUsage');
showToast('缓存已清除');
});
}
}
function showChangePasswordPage() {
const changePwdHtml = `
`;
appContainer.innerHTML = changePwdHtml;
// 绑定返回按钮
const backBtn = document.getElementById('changePwdBackBtn');
if (backBtn) {
backBtn.addEventListener('click', () => {
showPrivacyPage();
});
}
// 绑定提交按钮
const submitBtn = document.getElementById('changePwdSubmitBtn');
if (submitBtn) {
submitBtn.addEventListener('click', handleChangePassword);
}
}
function handleChangePassword() {
const currentPwd = document.getElementById('currentPassword')?.value;
const newPwd = document.getElementById('newPassword')?.value;
const confirmNewPwd = document.getElementById('confirmNewPassword')?.value;
// 验证
if (!currentPwd) {
showToast('请输入当前密码');
return;
}
if (!newPwd || newPwd.length < 6 || newPwd.length > 20) {
showToast('新密码需要6-20个字符');
return;
}
if (newPwd !== confirmNewPwd) {
showToast('两次密码不一致');
return;
}
// 验证当前密码
const users = JSON.parse(localStorage.getItem('registeredUsers') || '[]');
const user = users.find(u => u.username === currentUser.username);
if (!user || user.password !== currentPwd) {
showToast('当前密码错误');
return;
}
// 更新密码
user.password = newPwd;
localStorage.setItem('registeredUsers', JSON.stringify(users));
showToast('密码已修改');
showPrivacyPage();
}
function showAccountInfoDialog() {
const users = JSON.parse(localStorage.getItem('registeredUsers') || '[]');
const user = users.find(u => u.username === currentUser.username);
const registeredAt = user?.registeredAt ? new Date(user.registeredAt).toLocaleString('zh-CN') : '未知';
const phone = user?.phone || '未设置';
const email = user?.email || '未设置';
const dialogHtml = `
账号信息
用户名
${currentUser.username}
手机号
${phone}
邮箱
${email}
注册时间
${registeredAt}
`;
document.body.insertAdjacentHTML('beforeend', dialogHtml);
document.getElementById('closeAccountInfo')?.addEventListener('click', () => {
document.getElementById('accountInfoDialog')?.remove();
});
document.getElementById('accountInfoDialog')?.addEventListener('click', (e) => {
if (e.target.id === 'accountInfoDialog') {
document.getElementById('accountInfoDialog')?.remove();
}
});
}
// ==================== 帮助与反馈页面 ====================
function showHelpPage() {
const helpHtml = `
使用帮助
💬
如何开始对话
点击对话页面的 + 按钮创建新对话
🤖
如何使用智能体
切换到智能体页面,选择想要使用的智能体
🔍
如何搜索对话
点击对话页面的搜索按钮输入关键词
⭐
如何收藏智能体
长按智能体卡片选择收藏,或去发现页面添加
`;
appContainer.innerHTML = helpHtml;
// 绑定返回按钮
const backBtn = document.getElementById('helpBackBtn');
if (backBtn) {
backBtn.addEventListener('click', () => {
switchPage('profile');
});
}
// 提交反馈
const submitBtn = document.getElementById('submitFeedbackBtn');
if (submitBtn) {
submitBtn.addEventListener('click', () => {
const content = document.getElementById('feedbackContent')?.value.trim();
if (!content) {
showToast('请输入反馈内容');
return;
}
// 保存反馈(模拟)
const feedbacks = JSON.parse(localStorage.getItem('feedbacks') || '[]');
feedbacks.push({
content,
userId: currentUser?.username || 'guest',
createdAt: Date.now()
});
localStorage.setItem('feedbacks', JSON.stringify(feedbacks));
showToast('感谢您的反馈');
document.getElementById('feedbackContent').value = '';
});
}
}
// ==================== 关于页面 ====================
function showAboutPage() {
const appName = CONFIG?.system?.appName || 'AI助手';
const version = CONFIG?.system?.version || '3.10.0';
const developer = CONFIG?.system?.developer || 'OpenClaw Team';
const updateDate = CONFIG?.system?.updateDate || '2026-04-27';
const technology = CONFIG?.system?.technology || '智谱 GLM-4.5-Air 大模型';
const description = CONFIG?.system?.description || '提供智能对话、多种智能体服务';
const aboutHtml = `
🤖
${appName}
v${version}
基于 ${technology}
${description}
开发者
${developer}
更新日期
${updateDate}
`;
appContainer.innerHTML = aboutHtml;
// 绑定返回按钮
const backBtn = document.getElementById('aboutBackBtn');
if (backBtn) {
backBtn.addEventListener('click', () => {
switchPage('profile');
});
}
// 检查更新
const checkUpdateBtn = document.getElementById('checkUpdateBtn');
if (checkUpdateBtn) {
checkUpdateBtn.addEventListener('click', () => {
showToast('当前已是最新版本');
});
}
// 隐私政策
const privacyPolicyBtn = document.getElementById('privacyPolicyBtn');
if (privacyPolicyBtn) {
privacyPolicyBtn.addEventListener('click', () => {
showToast('隐私政策页面待完善');
});
}
// 用户协议
const userAgreementBtn = document.getElementById('userAgreementBtn');
if (userAgreementBtn) {
userAgreementBtn.addEventListener('click', () => {
showToast('用户协议页面待完善');
});
}
}
// ==================== 登录页面 ====================
function showLoginPage() {
const loginHtml = `
`;
appContainer.innerHTML = loginHtml;
// 绑定事件
const backBtn = document.getElementById('loginBackBtn');
if (backBtn) {
backBtn.addEventListener('click', () => {
showMainPage();
});
}
const submitBtn = document.getElementById('loginSubmitBtn');
if (submitBtn) {
submitBtn.addEventListener('click', handleLogin);
}
const goToRegister = document.getElementById('goToRegister');
if (goToRegister) {
goToRegister.addEventListener('click', () => {
showRegisterPage();
});
}
// 回车登录
document.getElementById('loginPassword')?.addEventListener('keydown', (e) => {
if (e.key === 'Enter') handleLogin();
});
}
function handleLogin() {
const username = document.getElementById('loginUsername')?.value.trim();
const password = document.getElementById('loginPassword')?.value;
if (!username) {
showToast('请输入用户名');
return;
}
if (!password) {
showToast('请输入密码');
return;
}
// 调用后台登录API
fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
})
.then(res => res.json())
.then(data => {
if (data.success) {
currentUser = {
id: data.user.id,
username: data.user.username,
phone: data.user.phone,
email: data.user.email || '',
avatar: data.user.avatar || '👤',
signature: data.user.signature || '',
gender: data.user.gender || '',
age: data.user.age || '',
region: data.user.region || '',
registeredAt: Date.now()
};
saveCurrentUser();
// 从 backend 加载用户对话数据
loadUserConversations().then(() => {
showToast('登录成功');
showMainPage();
});
} else {
showToast(data.error || '用户名或密码错误');
}
})
.catch(e => {
showToast('网络错误,请重试');
});
}
// ==================== 注册页面 ====================
function showRegisterPage() {
const registerHtml = `
`;
appContainer.innerHTML = registerHtml;
// 绑定事件
const backBtn = document.getElementById('registerBackBtn');
if (backBtn) {
backBtn.addEventListener('click', () => {
showMainPage();
});
}
const submitBtn = document.getElementById('registerSubmitBtn');
if (submitBtn) {
submitBtn.addEventListener('click', handleRegister);
}
const goToLogin = document.getElementById('goToLogin');
if (goToLogin) {
goToLogin.addEventListener('click', () => {
showLoginPage();
});
}
// 回车注册
document.getElementById('registerPasswordConfirm')?.addEventListener('keydown', (e) => {
if (e.key === 'Enter') handleRegister();
});
// 手机号输入限制(只允许数字)
const phoneInput = document.getElementById('registerPhone');
if (phoneInput) {
phoneInput.addEventListener('input', (e) => {
e.target.value = e.target.value.replace(/\D/g, '');
});
}
}
// 验证手机号格式
function validatePhone(phone) {
// 中国手机号:11位,以1开头
const phoneRegex = /^1[3-9]\d{9}$/;
return phoneRegex.test(phone);
}
// 验证邮箱格式
function validateEmail(email) {
if (!email) return true; // 可选,空值有效
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
function handleRegister() {
const username = document.getElementById('registerUsername')?.value.trim();
const phone = document.getElementById('registerPhone')?.value.trim();
const email = document.getElementById('registerEmail')?.value.trim();
const password = document.getElementById('registerPassword')?.value;
const passwordConfirm = document.getElementById('registerPasswordConfirm')?.value;
// 验证用户名
if (!username || username.length < 2 || username.length > 20) {
showToast('用户名需要2-20个字符');
return;
}
// 验证手机号
if (!validatePhone(phone)) {
showToast('请输入正确的手机号');
return;
}
// 验证邮箱(可选)
if (email && !validateEmail(email)) {
showToast('请输入正确的邮箱格式');
return;
}
// 验证密码
if (!password || password.length < 6 || password.length > 20) {
showToast('密码需要6-20个字符');
return;
}
if (password !== passwordConfirm) {
showToast('两次密码不一致');
return;
}
// 调用后台注册API(验证码暂时跳过,传随机值)
const code = Math.floor(100000 + Math.random() * 900000).toString();
fetch('/api/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username,
phone,
email: email || '',
password,
code
})
})
.then(res => res.json())
.then(data => {
if (data.success) {
// 注册成功,自动登录
currentUser = {
id: data.user_id,
username: username,
phone: phone,
email: email || '',
registeredAt: Date.now()
};
saveCurrentUser();
// 加载对话数据(新用户为空)
loadUserConversations().then(() => {
showToast('注册成功');
showMainPage();
});
} else {
showToast(data.error || '注册失败');
}
})
.catch(e => {
showToast('网络错误,请重试');
});
}
// ==================== 限制提示 ====================
function showLimitDialog(type) {
let title = '';
let message = '';
let showLoginBtn = true;
switch (type) {
case 'chat_session':
title = '对话会话已达上限';
message = `游客每天只能创建 ${GUEST_LIMITS.maxChatSessionsPerDay} 个对话会话。\n登录后即可无限使用。`;
break;
case 'chat_message':
title = '对话消息已达上限';
message = `游客每天最多发送 ${GUEST_LIMITS.maxChatMessagesPerDay} 条对话消息。\n登录后即可无限使用。`;
break;
case 'agent_type':
title = '智能体限制';
message = `游客只能使用「通用助手」智能体。\n登录后即可使用全部智能体。`;
break;
case 'agent_change':
title = '智能体切换限制';
message = `游客每天只能使用一个智能体。\n您今天已使用过智能体,明天才能更换。\n登录后即可自由切换智能体。`;
break;
case 'agent_message':
title = '智能体消息已达上限';
message = `游客每天最多发送 ${GUEST_LIMITS.maxAgentMessagesPerDay} 条智能体消息。\n登录后即可无限使用。`;
break;
default:
title = '使用限制';
message = '登录后解锁全部功能。';
}
const dialogHtml = `
⚠️
${title}
${message}
${showLoginBtn ? '' : ''}
`;
// 添加到页面
const existingDialog = document.getElementById('limitDialog');
if (existingDialog) existingDialog.remove();
document.body.insertAdjacentHTML('beforeend', dialogHtml);
// 绑定事件
const loginBtn = document.getElementById('limitLoginBtn');
if (loginBtn) {
loginBtn.addEventListener('click', () => {
document.getElementById('limitDialog')?.remove();
showLoginPage();
});
}
const cancelBtn = document.getElementById('limitCancelBtn');
if (cancelBtn) {
cancelBtn.addEventListener('click', () => {
document.getElementById('limitDialog')?.remove();
});
}
// 点击背景关闭
document.getElementById('limitDialog')?.addEventListener('click', (e) => {
if (e.target.id === 'limitDialog') {
document.getElementById('limitDialog')?.remove();
}
});
}
// 查看全部历史使用
function showAgentHistoryPage() {
const allAgentConvos = getAgentConversationHistory(100); // 获取所有
const historyHtml = `
${allAgentConvos.length === 0
? '
暂无历史使用记录
'
: allAgentConvos.map(conv => `
${conv.agent ? conv.agent.avatar : '🤖'}
${conv.title}
${conv.agent ? conv.agent.name : '未知智能体'}
${formatTime(conv.updatedAt)}
`).join('')
}
`;
appContainer.innerHTML = historyHtml;
// 绑定返回按钮
const backBtn = document.getElementById('historyBackBtn');
if (backBtn) {
backBtn.addEventListener('click', () => {
showMainPage();
});
}
// 绑定对话点击(打开已有对话)
document.querySelectorAll('.agent-history-item').forEach(item => {
item.addEventListener('click', () => {
const convId = item.getAttribute('data-conv-id');
if (convId) {
openConversation(convId);
}
});
});
}
// 打开智能体对话
async function openAgent(agentId) {
currentAgent = systemAgents.find(a => a.id === agentId);
if (!currentAgent) return;
// 加载该智能体的工具选择状态
loadToolsState();
// 检查未登录用户的智能体限制
if (!currentUser) {
const check = canUseAgent(agentId);
if (!check.allowed) {
if (check.reason === 'guest_agent_only') {
showLimitDialog('agent_type');
return;
} else if (check.reason === 'guest_one_agent_only') {
showLimitDialog('agent_change');
return;
}
}
// 记录使用的智能体
setAgentUsed(agentId);
}
// 如果用户已登录,先在 backend 创建对话
let backendId = null;
if (currentUser && currentUser.id) {
try {
const res = await fetch(`/api/user/${currentUser.id}/conversations`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title: currentAgent.name,
messages: [],
agentId: agentId
})
});
const data = await res.json();
if (data.success && data.id) {
backendId = data.id;
}
} catch (e) {
console.error('创建智能体对话失败:', e);
}
}
// 创建新对话并设置智能体
const newConv = {
id: backendId || Date.now().toString(),
title: '新对话', // 和普通对话一样,初始标题为"新对话",后续自动生成
messages: [],
agentId: agentId,
createdAt: Date.now(),
updatedAt: Date.now()
};
conversations.unshift(newConv);
saveConversations();
currentConversation = newConv;
// 显示对话界面
showAgentChatPage();
}
// 显示智能体对话界面
function showAgentChatPage() {
if (!currentAgent || !currentConversation) return;
const chatHtml = `
`;
appContainer.innerHTML = chatHtml;
// 重新获取 DOM 元素
messagesContainer = document.getElementById('messagesContainer');
messagesDiv = document.getElementById('messages');
userInput = document.getElementById('userInput');
sendBtn = document.getElementById('sendBtn');
welcome = document.getElementById('welcome');
thinkingBtn = document.getElementById('thinkingBtn');
searchBtn = document.getElementById('searchBtn');
// 重置自动滚动状态
autoScrollEnabled = true;
// 设置滚动监听
setupScrollListener();
// 绑定事件
const backBtn = document.getElementById('backBtn');
if (backBtn) {
backBtn.addEventListener('click', () => {
showMainPage();
});
}
// 绑定功能开关按钮事件
if (thinkingBtn) {
thinkingBtn.addEventListener('click', () => {
enableThinking = !enableThinking;
thinkingBtn.classList.toggle('active', enableThinking);
});
}
if (searchBtn) {
searchBtn.addEventListener('click', () => {
enableSearch = !enableSearch;
searchBtn.classList.toggle('active', enableSearch);
});
}
// 绑定更多工具按钮
const moreToolsBtn = document.getElementById('moreToolsBtn');
if (moreToolsBtn) {
moreToolsBtn.addEventListener('click', () => {
showToolsSelector();
});
}
// 绑定输入事件
userInput.addEventListener('keydown', handleKeyDown);
userInput.addEventListener('input', (e) => autoResize(e.target));
sendBtn.addEventListener('click', sendMessage);
// 绑定置顶置底按钮事件
const scrollTopBtn = document.getElementById('scrollTopBtn');
const scrollBottomBtn = document.getElementById('scrollBottomBtn');
if (scrollTopBtn) {
scrollTopBtn.addEventListener('click', () => {
if (messagesContainer) {
messagesContainer.scrollTo({
top: 0,
behavior: 'smooth'
});
}
});
}
if (scrollBottomBtn) {
scrollBottomBtn.addEventListener('click', () => {
if (messagesContainer) {
messagesContainer.scrollTo({
top: messagesContainer.scrollHeight,
behavior: 'smooth'
});
}
});
}
// 绑定上传按钮事件
const attachBtn = document.getElementById('attachBtn');
const attachPanel = document.getElementById('attachPanel');
const imageInput = document.getElementById('imageInput');
const fileInput = document.getElementById('fileInput');
if (attachBtn) {
attachBtn.addEventListener('click', (e) => {
e.stopPropagation();
attachPanel.classList.toggle('show');
});
}
document.addEventListener('click', (e) => {
if (attachPanel && attachPanel.classList.contains('show') &&
!attachPanel.contains(e.target) && !attachBtn.contains(e.target)) {
attachPanel.classList.remove('show');
}
});
attachPanel.querySelectorAll('.attach-item').forEach(item => {
item.addEventListener('click', () => {
const type = item.getAttribute('data-type');
attachPanel.classList.remove('show');
if (type === 'image') {
imageInput.click();
} else if (type === 'file') {
fileInput.click();
}
});
});
imageInput.addEventListener('change', handleImageUpload);
fileInput.addEventListener('change', handleFileUpload);
// 渲染消息
renderMessages();
userInput.focus();
}
// 设置长按事件
// 全局变量:当前操作的对话ID
let currentActionConvId = null;
function setupLongPressEvents(container) {
let longPressTimer = null;
container.addEventListener('touchstart', (e) => {
const item = e.target.closest('.conversation-item');
if (item) {
longPressTimer = setTimeout(() => {
currentActionConvId = item.getAttribute('data-id');
showActionMenu(currentActionConvId);
}, 500);
}
});
container.addEventListener('touchend', () => {
if (longPressTimer) {
clearTimeout(longPressTimer);
longPressTimer = null;
}
});
container.addEventListener('touchmove', () => {
if (longPressTimer) {
clearTimeout(longPressTimer);
longPressTimer = null;
}
});
container.addEventListener('mousedown', (e) => {
const item = e.target.closest('.conversation-item');
if (item) {
longPressTimer = setTimeout(() => {
currentActionConvId = item.getAttribute('data-id');
showActionMenu(currentActionConvId);
}, 500);
}
});
container.addEventListener('mouseup', () => {
if (longPressTimer) {
clearTimeout(longPressTimer);
longPressTimer = null;
}
});
container.addEventListener('mouseleave', () => {
if (longPressTimer) {
clearTimeout(longPressTimer);
longPressTimer = null;
}
});
}
function showConversationList() {
currentConversation = null;
showMainPage(); // 返回主页,会正确显示过滤后的对话列表
}
// 排序对话(置顶在前)
function sortConversations() {
return [...conversations].sort((a, b) => {
// 置顶优先
if (a.is_pinned && !b.is_pinned) return -1;
if (!a.is_pinned && b.is_pinned) return 1;
// 然后按更新时间
return b.updatedAt - a.updatedAt;
});
}
// 排序普通对话(没有智能体的)
function sortNormalConversations(convList) {
return [...convList].sort((a, b) => {
// 置顶优先
if (a.is_pinned && !b.is_pinned) return -1;
if (!a.is_pinned && b.is_pinned) return 1;
// 然后按更新时间
return b.updatedAt - a.updatedAt;
});
}
// 搜索普通对话(不包含智能体对话)
function searchConversations(keyword) {
const searchResults = document.getElementById('searchResults');
if (!searchResults) return;
keyword = keyword.toLowerCase();
// 只搜索没有智能体的普通对话
const normalConversations = conversations.filter(conv => !conv.agentId);
// 搜索标题和消息内容
const results = normalConversations.filter(conv => {
// 搜索标题
if (conv.title.toLowerCase().includes(keyword)) return true;
// 搜索消息内容
if (conv.messages.some(m => m.content.toLowerCase().includes(keyword))) return true;
return false;
});
if (results.length === 0) {
searchResults.innerHTML = '未找到相关对话
';
return;
}
searchResults.innerHTML = results.map(conv => {
// 找到匹配的消息片段
let matchSnippet = '';
const matchedMsg = conv.messages.find(m => m.content.toLowerCase().includes(keyword));
if (matchedMsg) {
const content = matchedMsg.content;
const idx = content.toLowerCase().indexOf(keyword);
const start = Math.max(0, idx - 30);
const end = Math.min(content.length, idx + keyword.length + 30);
matchSnippet = (start > 0 ? '...' : '') + content.slice(start, end) + (end < content.length ? '...' : '');
}
return `
${conv.is_pinned ? '
📌' : ''}
${escapeHtml(conv.title)}
${matchSnippet ? `
${escapeHtml(matchSnippet)}
` : ''}
${conv.messages.length} 条消息 · ${formatTime(conv.updatedAt)}
`;
}).join('');
}
// 显示操作菜单
function showActionMenu(convId) {
const actionMenu = document.getElementById('actionMenu');
const conv = conversations.find(c => c.id === convId);
if (!actionMenu || !conv) return;
// 更新置顶按钮文字
const pinText = document.getElementById('pinText');
if (pinText) {
pinText.textContent = conv.is_pinned ? '取消置顶' : '置顶';
}
// 显示菜单
actionMenu.classList.add('show');
}
// 隐藏操作菜单
function hideActionMenu() {
const actionMenu = document.getElementById('actionMenu');
if (actionMenu) {
actionMenu.classList.remove('show');
}
}
// 处理操作菜单动作
function handleActionMenuAction(action, convId) {
const conv = conversations.find(c => c.id === convId);
if (!conv) return;
switch (action) {
case 'rename':
renameConversation(convId);
break;
case 'share':
shareConversation(convId);
break;
case 'pin':
togglePinConversation(convId);
break;
case 'delete':
deleteConversation(convId);
break;
}
}
// 重命名对话
function renameConversation(convId) {
const conv = conversations.find(c => c.id === convId);
if (!conv) return;
const newTitle = prompt('请输入新的对话标题:', conv.title);
if (newTitle && newTitle.trim() && newTitle !== conv.title) {
conv.title = newTitle.trim();
conv.updatedAt = Date.now();
saveConversations();
showConversationList();
showToast('已重命名');
}
}
// 分享对话
function shareConversation(convId) {
const conv = conversations.find(c => c.id === convId);
if (!conv) return;
// 构建分享内容
const shareContent = `【${conv.title}】\n\n${conv.messages.map(m =>
`${m.role === 'user' ? '👤 用户' : '🤖 AI'}: ${m.content}`
).join('\n\n')}`;
// 复制到剪贴板
try {
const textarea = document.createElement('textarea');
textarea.value = shareContent;
textarea.style.position = 'fixed';
textarea.style.top = '0';
textarea.style.left = '0';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
showToast('对话已复制到剪贴板');
} catch (err) {
showToast('分享失败');
}
}
// 置顶/取消置顶对话
function togglePinConversation(convId) {
const conv = conversations.find(c => c.id === convId);
if (!conv) return;
conv.is_pinned = !conv.is_pinned;
conv.updatedAt = Date.now();
saveConversations();
showConversationList();
showToast(conv.is_pinned ? '已置顶' : '已取消置顶');
}
// 创建新对话
async function createNewConversation() {
// 检查未登录用户的对话限制
if (!currentUser && !canCreateNewChat()) {
showLimitDialog('chat_session');
return;
}
// 如果用户已登录,先在 backend 创建对话
let backendId = null;
if (currentUser && currentUser.id) {
try {
const res = await fetch(`/api/user/${currentUser.id}/conversations`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: '新对话', messages: [] })
});
const data = await res.json();
if (data.success && data.id) {
backendId = data.id;
}
} catch (e) {
console.error('创建对话失败:', e);
}
}
const newConv = {
id: backendId || Date.now().toString(), // 使用 backend ID 或本地 ID
title: '新对话',
messages: [],
createdAt: Date.now(),
updatedAt: Date.now()
};
conversations.unshift(newConv);
saveConversations();
// 增加对话会话计数(未登录用户)
incrementChatSession();
openConversation(newConv.id);
}
// 打开对话
function openConversation(id) {
currentConversation = conversations.find(c => c.id === id);
if (!currentConversation) {
showConversationList();
return;
}
// 普通对话,加载普通对话的工具状态
currentAgent = null;
loadToolsState();
// 渲染对话页面
const chatHtml = `
`;
appContainer.innerHTML = chatHtml;
// 重新获取 DOM 元素
messagesContainer = document.getElementById('messagesContainer');
messagesDiv = document.getElementById('messages');
userInput = document.getElementById('userInput');
sendBtn = document.getElementById('sendBtn');
welcome = document.getElementById('welcome');
thinkingBtn = document.getElementById('thinkingBtn');
searchBtn = document.getElementById('searchBtn');
// 重置自动滚动状态
autoScrollEnabled = true;
// 设置滚动监听
setupScrollListener();
// 绑定按钮事件
const backBtn = document.getElementById('backBtn');
if (backBtn) backBtn.addEventListener('click', showConversationList);
const clearBtn = document.getElementById('clearBtn');
if (clearBtn) clearBtn.addEventListener('click', clearCurrentChat);
// 绑定功能开关按钮事件
if (thinkingBtn) {
thinkingBtn.addEventListener('click', () => {
enableThinking = !enableThinking;
thinkingBtn.classList.toggle('active', enableThinking);
});
}
if (searchBtn) {
searchBtn.addEventListener('click', () => {
enableSearch = !enableSearch;
searchBtn.classList.toggle('active', enableSearch);
});
}
// 绑定更多工具按钮
const moreToolsBtn = document.getElementById('moreToolsBtn');
if (moreToolsBtn) {
moreToolsBtn.addEventListener('click', () => {
showToolsSelector();
});
}
// 绑定置顶置底按钮事件
const scrollTopBtn = document.getElementById('scrollTopBtn');
const scrollBottomBtn = document.getElementById('scrollBottomBtn');
if (scrollTopBtn) {
scrollTopBtn.addEventListener('click', () => {
if (messagesContainer) {
messagesContainer.scrollTo({
top: 0,
behavior: 'smooth'
});
}
});
}
if (scrollBottomBtn) {
scrollBottomBtn.addEventListener('click', () => {
if (messagesContainer) {
messagesContainer.scrollTo({
top: messagesContainer.scrollHeight,
behavior: 'smooth'
});
}
});
}
// 绑定输入事件
userInput.addEventListener('keydown', handleKeyDown);
userInput.addEventListener('input', (e) => autoResize(e.target));
sendBtn.addEventListener('click', sendMessage);
// 绑定上传按钮事件
const attachBtn = document.getElementById('attachBtn');
const attachPanel = document.getElementById('attachPanel');
const imageInput = document.getElementById('imageInput');
const fileInput = document.getElementById('fileInput');
if (attachBtn) {
attachBtn.addEventListener('click', (e) => {
e.stopPropagation(); // 阻止冒泡到 document
attachPanel.classList.toggle('show');
});
}
// 点击其他地方关闭面板
document.addEventListener('click', (e) => {
if (attachPanel && attachPanel.classList.contains('show') &&
!attachPanel.contains(e.target) && !attachBtn.contains(e.target)) {
attachPanel.classList.remove('show');
}
});
// 上传选项点击
attachPanel.querySelectorAll('.attach-item').forEach(item => {
item.addEventListener('click', () => {
const type = item.getAttribute('data-type');
attachPanel.classList.remove('show');
if (type === 'image') {
imageInput.click();
} else if (type === 'file') {
fileInput.click();
}
});
});
// 图片上传处理
imageInput.addEventListener('change', handleImageUpload);
// 文件上传处理
fileInput.addEventListener('change', handleFileUpload);
// 绑定快捷按钮事件
document.querySelectorAll('.quick-btn').forEach(btn => {
btn.addEventListener('click', () => {
const text = btn.getAttribute('data-text');
userInput.value = text;
sendMessage();
});
});
// 渲染消息
renderMessages();
userInput.focus();
}
// 删除对话
function deleteConversation(id) {
if (!confirm('确定要删除这个对话吗?')) return;
conversations = conversations.filter(c => c.id !== id);
saveConversations();
// 如果用户已登录,从 backend 删除
if (currentUser && currentUser.id) {
const convId = parseInt(id);
if (convId > 0) {
fetch(`/api/user/${currentUser.id}/conversations/${convId}`, {
method: 'DELETE'
}).catch(e => console.error('删除对话失败:', e));
}
}
showConversationList();
}
// ==================== 对话页面 ====================
// 自动调整输入框高度
function autoResize(textarea) {
textarea.style.height = 'auto';
textarea.style.height = Math.min(textarea.scrollHeight, 120) + 'px';
}
// 处理键盘事件
function handleKeyDown(event) {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
sendMessage();
}
}
// 发送快捷消息
function sendQuickMessage(text) {
userInput.value = text;
sendMessage();
}
// 发送消息(流式输出)
async function sendMessage() {
if (!currentConversation) return;
const text = userInput.value.trim();
if (!text || isLoading) return;
// 检查未登录用户的消息限制
if (!currentUser) {
if (currentConversation.agentId) {
// 智能体对话
if (!canSendAgentMessage()) {
showLimitDialog('agent_message');
return;
}
} else {
// 普通对话
if (!canSendChatMessage()) {
showLimitDialog('chat_message');
return;
}
}
}
// 隐藏欢迎界面
welcome.style.display = 'none';
// 添加用户消息
currentConversation.messages.push({ role: 'user', content: text });
// 更新对话标题(第一条用户消息)
if (currentConversation.title === '新对话') {
currentConversation.title = text.slice(0, 30) + (text.length > 30 ? '...' : '');
// 更新标题显示
const titleEl = document.querySelector('.header h1');
if (titleEl) {
titleEl.textContent = currentConversation.title;
}
}
currentConversation.updatedAt = Date.now();
saveConversations();
// 同步当前对话到 backend(用户已登录)
syncConversationToBackend(currentConversation);
// 增加消息计数(未登录用户)
if (!currentUser) {
if (currentConversation.agentId) {
incrementAgentMessage();
} else {
incrementChatMessage();
}
}
// 发送新消息时启用自动滚动
autoScrollEnabled = true;
renderMessages();
userInput.value = '';
autoResize(userInput);
// 调用流式生成
await streamGenerate(currentConversation.messages.length - 1);
}
// 流式生成 AI 回复
async function streamGenerate(userMsgIndex) {
isLoading = true;
sendBtn.disabled = true;
const aiMessageIndex = currentConversation.messages.length;
const userMessage = currentConversation.messages[userMsgIndex];
// 如果开启联网搜索,先执行搜索
let searchResults = null;
if (enableSearch && userMessage.role === 'user') {
searchResults = await performSearch(userMessage.content);
}
// 只有开启深度思考时才添加 thinking 字段,开启搜索时添加 search_results 字段
currentConversation.messages.push({
role: 'assistant',
content: '',
...(enableThinking ? { thinking: '' } : {}),
...(searchResults ? { search_results: searchResults } : {})
});
renderMessages();
const lastMessageEl = messagesDiv.lastElementChild;
const contentEl = lastMessageEl.querySelector('.message-content');
const thinkingEl = lastMessageEl.querySelector('.thinking-content');
// 深度思考模式:思考块默认展开
if (enableThinking && thinkingEl) {
const thinkingBlock = lastMessageEl.querySelector('.thinking-block');
if (thinkingBlock) thinkingBlock.classList.add('expanded');
thinkingEl.innerHTML = '思考中...';
}
contentEl.innerHTML = '▌';
// 显示停止生成按钮
showStopGenerateBtn();
try {
// 构建消息数组
let messagesToSend = currentConversation.messages.slice(0, aiMessageIndex).map(m => ({
role: m.role,
content: m.content
}));
// 如果有搜索结果,将搜索内容添加到消息中
if (searchResults) {
const searchContext = formatSearchResultsForLLM(searchResults);
messagesToSend.push({
role: 'system',
content: `以下是搜索结果,请根据这些信息回答用户问题:\n\n${searchContext}`
});
}
// 构建请求体 - 统一使用 glm-4.5-air,通过 thinking 参数控制
const requestBody = {
model: CONFIG.model,
messages: messagesToSend,
max_tokens: CONFIG.maxTokens,
stream: true,
thinking: {
type: enableThinking ? 'enabled' : 'disabled'
}
};
const response = await fetch(CONFIG.apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${CONFIG.apiKey}`
},
body: JSON.stringify(requestBody)
});
if (!response.ok) {
throw new Error(`API 错误: ${response.status}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let thinkingOutputStarted = false; // 正式内容是否开始输出
let abortController = new AbortController(); // 用于中断流
// 绑定停止按钮事件
const stopBtn = document.getElementById('stopGenerateBtn');
if (stopBtn) {
stopBtn.onclick = () => {
abortController.abort();
isLoading = false;
sendBtn.disabled = false;
hideStopGenerateBtn();
// 更新最终内容
if (thinkingEl && enableThinking && currentConversation.messages[aiMessageIndex].thinking) {
thinkingEl.innerHTML = renderMarkdown(currentConversation.messages[aiMessageIndex].thinking);
}
contentEl.innerHTML = renderMarkdown(currentConversation.messages[aiMessageIndex].content);
currentConversation.updatedAt = Date.now();
saveConversations();
syncConversationToBackend(currentConversation);
renderMessages();
};
}
while (true) {
if (abortController.signal.aborted) break; // 检查是否已停止
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.startsWith('data: ')) {
const jsonStr = line.slice(6).trim();
if (jsonStr === '[DONE]') continue;
try {
const data = JSON.parse(jsonStr);
const delta = data.choices?.[0]?.delta;
if (delta) {
// 只有开启深度思考时才处理思考内容
if (enableThinking && (delta.reasoning_content || delta.thinking)) {
const thinkingChunk = delta.reasoning_content || delta.thinking;
currentConversation.messages[aiMessageIndex].thinking += thinkingChunk;
if (thinkingEl) {
thinkingEl.innerHTML = renderMarkdown(currentConversation.messages[aiMessageIndex].thinking) + '▌';
}
// 确保思考块展开
const thinkingBlock = lastMessageEl.querySelector('.thinking-block');
if (thinkingBlock && !thinkingBlock.classList.contains('expanded')) {
thinkingBlock.classList.add('expanded');
}
scrollToBottom();
}
// 处理正式回复内容
if (delta.content) {
// 如果开启深度思考且开始输出正式内容,说明思考完成,立即折叠思考块
if (enableThinking && !thinkingOutputStarted && currentConversation.messages[aiMessageIndex].thinking) {
thinkingOutputStarted = true;
// 折叠思考内容
const thinkingBlock = lastMessageEl.querySelector('.thinking-block');
if (thinkingBlock) thinkingBlock.classList.remove('expanded');
if (thinkingEl) thinkingEl.innerHTML = renderMarkdown(currentConversation.messages[aiMessageIndex].thinking);
}
currentConversation.messages[aiMessageIndex].content += delta.content;
contentEl.innerHTML = renderMarkdown(currentConversation.messages[aiMessageIndex].content) + '▌';
scrollToBottom();
}
}
} catch (e) {}
}
}
}
// 最终渲染
if (thinkingEl && enableThinking && currentConversation.messages[aiMessageIndex].thinking) {
thinkingEl.innerHTML = renderMarkdown(currentConversation.messages[aiMessageIndex].thinking);
}
contentEl.innerHTML = renderMarkdown(currentConversation.messages[aiMessageIndex].content);
} catch (error) {
console.error('Error:', error);
currentConversation.messages[aiMessageIndex].content = `抱歉,出现了错误:${error.message}\n\n请检查网络连接后重试。`;
contentEl.innerHTML = renderMarkdown(currentConversation.messages[aiMessageIndex].content);
} finally {
isLoading = false;
sendBtn.disabled = false;
hideStopGenerateBtn();
currentConversation.updatedAt = Date.now();
saveConversations();
syncConversationToBackend(currentConversation);
renderMessages();
// 记录统计到 backend
logStatsToBackend('llm_call', currentConversation.agentId || 'chat', 1);
// 自动总结标题:第一次对话和每隔5次对话
const totalMessages = currentConversation.messages.length;
// 第一次对话(用户+AI=2条)或每5次对话(10条)
if (totalMessages === 2 || totalMessages % 10 === 0) {
await generateConversationTitle();
}
}
}
// 记录统计到 backend
function logStatsToBackend(type, key, value = 1) {
fetch('/api/admin/stats/log', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type, key, value })
}).catch(e => console.error('统计记录失败:', e));
}
// 显示工具选择弹窗
function showToolsSelector() {
if (allTools.length === 0) {
showToast('暂无可用工具');
return;
}
// 工具类型图标映射
const toolIcons = {
search: '🔍',
calculator: '🧮',
image: '🎨',
code: '💻',
weather: '🌤️',
custom: '🔧'
};
const toolsHtml = allTools.map(tool => `
`).join('');
const popupHtml = `
`;
// 添加到页面
const existingPopup = document.getElementById('toolsPopup');
if (existingPopup) existingPopup.remove();
document.body.appendChild(document.createElement('div'));
document.body.lastElementChild.outerHTML = popupHtml;
// 绑定事件
document.querySelectorAll('.tool-option').forEach(option => {
option.addEventListener('click', () => {
option.classList.toggle('selected');
// 更新复选框图标
const checkbox = option.querySelector('.tool-checkbox svg');
const isSelected = option.classList.contains('selected');
checkbox.innerHTML = isSelected
? ''
: '';
});
});
// 关闭按钮
document.getElementById('toolsPopupClose').addEventListener('click', closeToolsPopup);
document.getElementById('toolsCancelBtn').addEventListener('click', closeToolsPopup);
document.getElementById('toolsConfirmBtn').addEventListener('click', confirmToolsSelection);
// 点击背景关闭
document.getElementById('toolsPopup').addEventListener('click', (e) => {
if (e.target.id === 'toolsPopup') {
closeToolsPopup();
}
});
}
function closeToolsPopup() {
const popup = document.getElementById('toolsPopup');
if (popup) popup.remove();
}
function confirmToolsSelection() {
// 获取选中的工具
const selectedTools = [];
document.querySelectorAll('.tool-option.selected').forEach(option => {
selectedTools.push(option.getAttribute('data-tool-id'));
});
enabledTools = selectedTools;
// 保存工具选择状态
saveToolsState();
// 更新按钮显示
const moreToolsBtn = document.getElementById('moreToolsBtn');
if (moreToolsBtn) {
const countSpan = moreToolsBtn.querySelector('.tools-count');
if (enabledTools.length > 0) {
if (countSpan) {
countSpan.textContent = enabledTools.length;
} else {
const span = document.createElement('span');
span.className = 'tools-count';
span.textContent = enabledTools.length;
moreToolsBtn.appendChild(span);
}
} else if (countSpan) {
countSpan.remove();
}
}
closeToolsPopup();
showToast(`已启用 ${enabledTools.length} 个工具`);
}
// 保存工具选择状态
function saveToolsState() {
if (currentAgent) {
// 智能体对话:保存到 localStorage(按智能体ID)
localStorage.setItem(`agentEnabledTools_${currentAgent.id}`, JSON.stringify(enabledTools));
} else {
// 普通对话:保存到 localStorage
localStorage.setItem('chatEnabledTools', JSON.stringify(enabledTools));
}
}
// 加载工具选择状态
function loadToolsState() {
if (currentAgent) {
// 智能体对话:加载该智能体的工具状态
const saved = localStorage.getItem(`agentEnabledTools_${currentAgent.id}`);
if (saved) {
enabledTools = JSON.parse(saved);
} else {
enabledTools = [];
}
} else {
// 普通对话:加载普通对话的工具状态
const saved = localStorage.getItem('chatEnabledTools');
if (saved) {
enabledTools = JSON.parse(saved);
} else {
enabledTools = [];
}
}
}
// 渲染头像(支持 emoji 和上传的图片)
function renderAvatar(avatar) {
if (!avatar) return '👤';
// 检查是否是 emoji(单个字符或者预设列表中的)
const emojiAvatars = ['👤', '😊', '😎', '🤓', '🧑', '👨', '👩', '🧒', '👴', '👵', '🦸', '🧙', '🤖', '👽', '🥷'];
if (emojiAvatars.includes(avatar) || avatar.length <= 4) {
return avatar;
}
// 否则认为是上传的文件名
return `
`;
}
// 执行 Tavily 搜索
async function performSearch(query) {
try {
const response = await fetch(CONFIG.tavilyApiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${CONFIG.tavilyApiKey}`
},
body: JSON.stringify({
query: query,
max_results: 10,
include_raw_content: false
})
});
if (!response.ok) {
console.error('搜索失败:', response.status);
return null;
}
const data = await response.json();
// 记录搜索统计
logStatsToBackend('search_call', 'tavily', 1);
return data.results || [];
} catch (error) {
console.error('搜索错误:', error);
return null;
}
}
// 格式化搜索结果给 LLM
function formatSearchResultsForLLM(results) {
if (!results || results.length === 0) return '无搜索结果';
return results.map((r, i) =>
`${i + 1}. 【${r.title}】\n来源: ${r.url}\n摘要: ${r.content || '无摘要'}\n`
).join('\n');
}
// 显示停止生成按钮
function showStopGenerateBtn() {
// 检查是否已存在
if (document.getElementById('stopGenerateBtn')) return;
const stopBtn = document.createElement('button');
stopBtn.id = 'stopGenerateBtn';
stopBtn.className = 'stop-generate-btn';
stopBtn.innerHTML = `
停止生成
`;
// 插入到消息容器底部
if (messagesContainer) {
messagesContainer.appendChild(stopBtn);
}
}
// 隐藏停止生成按钮
function hideStopGenerateBtn() {
const stopBtn = document.getElementById('stopGenerateBtn');
if (stopBtn) {
stopBtn.remove();
}
}
// 生成对话标题
async function generateConversationTitle() {
if (!currentConversation) return;
console.log('开始生成标题,当前消息数:', currentConversation.messages.length);
// 构建对话摘要
const conversationText = currentConversation.messages.map(m =>
`${m.role === 'user' ? '用户' : 'AI'}: ${m.content.slice(0, 200)}`
).join('\n');
const titlePrompt = `请用不超过20个字总结以下对话的主题,只输出标题,不要其他内容:
${conversationText}`;
try {
const response = await fetch(CONFIG.apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${CONFIG.apiKey}`
},
body: JSON.stringify({
model: CONFIG.model,
messages: [{ role: 'user', content: titlePrompt }],
max_tokens: 100,
thinking: { type: 'disabled' } // 禁用思考模式,直接输出标题
})
});
console.log('标题API响应状态:', response.status);
if (response.ok) {
const data = await response.json();
console.log('标题API响应数据:', data);
const newTitle = data.choices?.[0]?.message?.content?.trim();
console.log('生成的标题:', newTitle);
if (newTitle && newTitle.length > 0) {
// 去掉可能的引号和多余符号
const cleanTitle = newTitle.replace(/^["'"']+|["'"']+$/g, '').trim();
if (cleanTitle.length > 0 && cleanTitle.length <= 30) {
currentConversation.title = cleanTitle;
currentConversation.updatedAt = Date.now();
saveConversations();
console.log('标题已保存:', cleanTitle);
// 更新页面标题显示
const titleEl = document.querySelector('.header h1');
if (titleEl) {
titleEl.textContent = cleanTitle;
}
// 更新侧边栏标题(如果在对话列表页面)
const convItem = document.querySelector(`.conversation-item[data-id="${currentConversation.id}"] .conv-title`);
if (convItem) {
convItem.textContent = cleanTitle;
}
showToast('标题已更新');
}
}
} else {
const errorText = await response.text();
console.error('标题API错误:', errorText);
}
} catch (error) {
console.error('生成标题失败:', error);
}
}
// 重新生成 AI 回复
async function regenerate(index) {
if (!currentConversation || isLoading || index < 1) return;
const userMsgIndex = index - 1;
if (currentConversation.messages[userMsgIndex].role !== 'user') return;
currentConversation.messages.splice(index, 1);
currentConversation.updatedAt = Date.now();
saveConversations();
await streamGenerate(userMsgIndex);
}
// 删除消息
function deleteMessage(index) {
if (!currentConversation || isLoading) return;
const msg = currentConversation.messages[index];
if (msg.role === 'assistant') {
if (index > 0 && currentConversation.messages[index - 1].role === 'user') {
currentConversation.messages.splice(index - 1, 2);
} else {
currentConversation.messages.splice(index, 1);
}
} else {
if (index < currentConversation.messages.length - 1 && currentConversation.messages[index + 1].role === 'assistant') {
currentConversation.messages.splice(index, 2);
} else {
currentConversation.messages.splice(index, 1);
}
}
currentConversation.updatedAt = Date.now();
saveConversations();
renderMessages();
if (currentConversation.messages.length === 0) {
welcome.style.display = 'block';
}
}
// 复制消息(复制原文)
function copyMessage(index) {
if (!currentConversation) return;
const msg = currentConversation.messages[index];
// 如果是图片消息,复制图片描述或提示
let content = msg.content;
if (msg.image && content === '[图片]') {
content = '[图片: ' + (msg.imageName || '未命名') + ']';
}
// HTTP 环境下 navigator.clipboard 不工作,优先使用 fallback
try {
const textarea = document.createElement('textarea');
textarea.value = content;
textarea.style.position = 'fixed';
textarea.style.top = '0';
textarea.style.left = '0';
textarea.style.opacity = '0';
textarea.style.pointerEvents = 'none';
document.body.appendChild(textarea);
textarea.focus();
textarea.select();
const success = document.execCommand('copy');
document.body.removeChild(textarea);
if (success) {
showToast('已复制到剪贴板');
} else {
showToast('复制失败,请手动复制');
}
} catch (err) {
console.error('复制失败:', err);
showToast('复制失败,请手动复制');
}
}
// 清空当前对话
function clearCurrentChat() {
if (!currentConversation) return;
if (confirm('确定要清空当前对话吗?')) {
currentConversation.messages = [];
currentConversation.updatedAt = Date.now();
saveConversations();
renderMessages();
welcome.style.display = 'block';
}
}
// 渲染消息
function renderMessages() {
if (!currentConversation) return;
// 根据消息数量显示/隐藏欢迎界面
if (welcome) {
welcome.style.display = currentConversation.messages.length > 0 ? 'none' : 'block';
}
messagesDiv.innerHTML = currentConversation.messages.map((msg, index) => {
const isUser = msg.role === 'user';
// 用户头像使用实际头像,AI头像使用智能体头像或默认
const avatar = isUser
? (currentUser?.avatar || '👤')
: (currentAgent?.avatar || '🤖');
const avatarHtml = isUser ? renderAvatar(avatar) : avatar;
// 处理消息内容(支持图片)
let contentHtml = '';
if (msg.image) {
// 图片消息
contentHtml = ``;
if (msg.content && msg.content !== '[图片]') {
contentHtml += `${renderMarkdown(msg.content)}
`;
}
} else {
contentHtml = renderMarkdown(msg.content);
}
// 思考内容块(仅AI消息)
let thinkingHtml = '';
if (!isUser && 'thinking' in msg) {
// 判断是否是当前正在生成的消息(有thinking字段且正在加载)
const isGenerating = index === currentConversation.messages.length - 1 && isLoading && enableThinking;
// 思考进行中且没有正式内容时展开
const expandedClass = isGenerating && !msg.content ? 'expanded' : '';
thinkingHtml = `
${renderMarkdown(msg.thinking || '思考中...')}
`;
}
// 搜索结果块(仅AI消息,放在思考块前面)
let searchHtml = '';
if (!isUser && msg.search_results && msg.search_results.length > 0) {
searchHtml = `
${msg.search_results.map((r, i) => `
`).join('')}
`;
}
const copyIcon = ``;
const actions = isUser
? ``
: ``;
return `
${avatarHtml}
${searchHtml}
${thinkingHtml}
${contentHtml}
${actions}
`;
}).join('');
// 绑定消息操作按钮事件(事件委托)
messagesDiv.querySelectorAll('.copy-btn').forEach(btn => {
btn.addEventListener('click', () => copyMessage(parseInt(btn.dataset.index)));
});
messagesDiv.querySelectorAll('.regenerate-btn').forEach(btn => {
btn.addEventListener('click', () => regenerate(parseInt(btn.dataset.index)));
});
messagesDiv.querySelectorAll('.delete-btn').forEach(btn => {
btn.addEventListener('click', () => deleteMessage(parseInt(btn.dataset.index)));
});
scrollToBottom();
}
// 折叠/展开思考内容
function toggleThinking(block) {
block.classList.toggle('expanded');
}
// 折叠/展开搜索结果
function toggleSearchResults(block) {
block.classList.toggle('expanded');
}
// ==================== 工具函数 ====================
// 渲染 Markdown
function renderMarkdown(text) {
if (!text) return '';
marked.setOptions({
breaks: true,
gfm: true
});
return marked.parse(text);
}
// 滚动到底部(智能滚动:只在用户已在底部时自动滚动)
function scrollToBottom() {
if (messagesContainer) {
// 判断用户是否在底部附近(距离底部100px以内)
const isNearBottom = messagesContainer.scrollHeight - messagesContainer.scrollTop - messagesContainer.clientHeight < 100;
if (isNearBottom || autoScrollEnabled) {
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
}
}
// 监听用户滚动行为
function setupScrollListener() {
if (messagesContainer) {
messagesContainer.addEventListener('scroll', () => {
// 判断用户是否在底部附近
const isNearBottom = messagesContainer.scrollHeight - messagesContainer.scrollTop - messagesContainer.clientHeight < 100;
if (isNearBottom) {
autoScrollEnabled = true;
} else {
// 用户往上滚动,停止自动滚动
autoScrollEnabled = false;
}
});
}
}
// 保存对话列表
function saveConversations() {
// 保存到本地存储(离线可用)
localStorage.setItem('conversations', JSON.stringify(conversations));
}
// 同步单个对话到 backend(新创建或更新)
async function syncConversationToBackend(conv) {
if (!currentUser || !currentUser.id) return;
const convId = parseInt(conv.id);
try {
if (convId > 0 && convId < 1e12) {
// 已有 backend ID,更新对话
await fetch(`/api/user/${currentUser.id}/conversations/${convId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title: conv.title,
messages: conv.messages,
agentId: conv.agentId
})
});
} else {
// 新对话(本地 ID 是时间戳),创建
const res = await fetch(`/api/user/${currentUser.id}/conversations`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title: conv.title,
messages: conv.messages,
agentId: conv.agentId
})
});
const data = await res.json();
if (data.success && data.id) {
// 更新本地 ID 为 backend ID
conv.id = data.id;
// 更新 localStorage
localStorage.setItem('conversations', JSON.stringify(conversations));
}
}
} catch (e) {
console.error('同步对话失败:', e);
}
}
// 从 backend 加载用户对话数据
async function loadUserConversations() {
if (!currentUser || !currentUser.id) return;
try {
const res = await fetch(`/api/user/${currentUser.id}/conversations`);
const data = await res.json();
if (Array.isArray(data)) {
// 合并本地和云端数据(云端优先)
const localConvIds = conversations.map(c => c.id);
for (const cloudConv of data) {
const localIdx = localConvIds.indexOf(cloudConv.id);
if (localIdx >= 0) {
// 更新本地对话(云端数据优先)
conversations[localIdx] = cloudConv;
} else {
// 添加云端对话
conversations.push(cloudConv);
}
}
// 按更新时间排序
conversations.sort((a, b) => b.updatedAt - a.updatedAt);
// 保存到本地
localStorage.setItem('conversations', JSON.stringify(conversations));
}
} catch (e) {
console.error('加载对话失败:', e);
// 使用本地数据
}
}
// 显示提示
function showToast(message) {
const toast = document.createElement('div');
toast.className = 'toast';
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => toast.classList.add('show'), 10);
setTimeout(() => {
toast.classList.remove('show');
setTimeout(() => document.body.removeChild(toast), 300);
}, 2000);
}
// HTML转义
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// 格式化时间
function formatTime(timestamp) {
const date = new Date(timestamp);
const now = new Date();
const diff = now - date;
if (diff < 60000) return '刚刚';
if (diff < 3600000) return Math.floor(diff / 60000) + '分钟前';
if (diff < 86400000) return Math.floor(diff / 3600000) + '小时前';
if (diff < 604800000) return Math.floor(diff / 86400000) + '天前';
return date.toLocaleDateString('zh-CN');
}
// PWA 注册
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('sw.js').catch(() => {});
}
// ==================== 文件上传处理 ====================
// 处理图片上传
async function handleImageUpload(e) {
const file = e.target.files[0];
if (!file) return;
// 读取图片为base64
const reader = new FileReader();
reader.onload = async (event) => {
const base64 = event.target.result;
// 添加用户消息(显示图片)
currentConversation.messages.push({
role: 'user',
content: '[图片]',
image: base64,
imageName: file.name
});
currentConversation.updatedAt = Date.now();
saveConversations();
renderMessages();
// 隐藏欢迎界面
if (welcome) welcome.style.display = 'none';
// 调用AI生成
await streamGenerateWithImage(base64, file.name);
};
reader.readAsDataURL(file);
// 清空input以便再次选择同一文件
e.target.value = '';
}
// 处理文件上传
async function handleFileUpload(e) {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = async (event) => {
const content = event.target.result;
const fileName = file.name;
// 添加用户消息
currentConversation.messages.push({
role: 'user',
content: `[文件: ${fileName}]\\n\\n${content.slice(0, 500)}${content.length > 500 ? '...' : ''}`
});
currentConversation.updatedAt = Date.now();
saveConversations();
renderMessages();
if (welcome) welcome.style.display = 'none';
// 调用AI生成
await streamGenerateWithFile(content, fileName);
};
// 根据文件类型读取
if (file.name.endsWith('.pdf') || file.name.endsWith('.doc') || file.name.endsWith('.docx')) {
// PDF/Word文件暂时只显示文件名
showToast('PDF/Word文件暂不支持解析,请上传文本文件');
e.target.value = '';
return;
}
reader.readAsText(file);
e.target.value = '';
}
// 带图片的流式生成
async function streamGenerateWithImage(base64, imageName) {
isLoading = true;
sendBtn.disabled = true;
const aiMessageIndex = currentConversation.messages.length;
currentConversation.messages.push({ role: 'assistant', content: '' });
renderMessages();
const lastMessageEl = messagesDiv.lastElementChild;
const contentEl = lastMessageEl.querySelector('.message-content');
contentEl.innerHTML = '▌';
try {
// 构建多模态消息
const messages = currentConversation.messages.slice(0, aiMessageIndex).map(m => {
if (m.image) {
return {
role: m.role,
content: [
{ type: 'image_url', image_url: { url: m.image } },
{ type: 'text', text: '请分析这张图片' }
]
};
}
return { role: m.role, content: m.content };
});
const response = await fetch(CONFIG.apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${CONFIG.apiKey}`
},
body: JSON.stringify({
model: 'glm-4v-flash', // 视觉模型
messages: messages,
max_tokens: CONFIG.maxTokens,
stream: true
})
});
if (!response.ok) {
throw new Error(`API错误: ${response.status}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.startsWith('data: ')) {
const jsonStr = line.slice(6).trim();
if (jsonStr === '[DONE]') continue;
try {
const data = JSON.parse(jsonStr);
if (data.choices && data.choices[0]?.delta?.content) {
currentConversation.messages[aiMessageIndex].content += data.choices[0].delta.content;
contentEl.innerHTML = renderMarkdown(currentConversation.messages[aiMessageIndex].content) + '▌';
scrollToBottom();
}
} catch (err) {}
}
}
}
contentEl.innerHTML = renderMarkdown(currentConversation.messages[aiMessageIndex].content);
} catch (error) {
console.error('Error:', error);
currentConversation.messages[aiMessageIndex].content = `抱歉,图片分析失败:${error.message}`;
contentEl.innerHTML = renderMarkdown(currentConversation.messages[aiMessageIndex].content);
} finally {
isLoading = false;
sendBtn.disabled = false;
currentConversation.updatedAt = Date.now();
saveConversations();
renderMessages();
}
}
// 带文件的流式生成
async function streamGenerateWithFile(content, fileName) {
isLoading = true;
sendBtn.disabled = true;
const aiMessageIndex = currentConversation.messages.length;
currentConversation.messages.push({ role: 'assistant', content: '' });
renderMessages();
const lastMessageEl = messagesDiv.lastElementChild;
const contentEl = lastMessageEl.querySelector('.message-content');
contentEl.innerHTML = '▌';
try {
const messages = currentConversation.messages.slice(0, aiMessageIndex).map(m => ({
role: m.role,
content: m.content
}));
// 添加文件内容作为系统提示
messages.unshift({
role: 'system',
content: `以下是用户上传的文件内容,请根据内容回答问题:\\n文件名:${fileName}\\n内容:\\n${content}`
});
const response = await fetch(CONFIG.apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${CONFIG.apiKey}`
},
body: JSON.stringify({
model: CONFIG.model,
messages: messages,
max_tokens: CONFIG.maxTokens,
stream: true
})
});
if (!response.ok) {
throw new Error(`API错误: ${response.status}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.startsWith('data: ')) {
const jsonStr = line.slice(6).trim();
if (jsonStr === '[DONE]') continue;
try {
const data = JSON.parse(jsonStr);
if (data.choices && data.choices[0]?.delta?.content) {
currentConversation.messages[aiMessageIndex].content += data.choices[0].delta.content;
contentEl.innerHTML = renderMarkdown(currentConversation.messages[aiMessageIndex].content) + '▌';
scrollToBottom();
}
} catch (err) {}
}
}
}
contentEl.innerHTML = renderMarkdown(currentConversation.messages[aiMessageIndex].content);
} catch (error) {
console.error('Error:', error);
currentConversation.messages[aiMessageIndex].content = `抱歉,文件处理失败:${error.message}`;
contentEl.innerHTML = renderMarkdown(currentConversation.messages[aiMessageIndex].content);
} finally {
isLoading = false;
sendBtn.disabled = false;
currentConversation.updatedAt = Date.now();
saveConversations();
renderMessages();
}
}