diff --git a/backend/app.py b/backend/app.py index 30b3913..412a05e 100644 --- a/backend/app.py +++ b/backend/app.py @@ -166,6 +166,20 @@ def init_db(): ) ''') + # 对话表(用户对话数据) + cursor.execute(''' + CREATE TABLE IF NOT EXISTS conversations ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + title TEXT NOT NULL, + agent_id TEXT, + messages TEXT NOT NULL DEFAULT '[]', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) + ) + ''') + # 初始化默认大模型配置 cursor.execute('SELECT COUNT(*) FROM llm_configs') if cursor.fetchone()[0] == 0: @@ -581,6 +595,118 @@ def change_user_password(id): return jsonify({'success': True}) +# ==================== 用户对话数据同步 ==================== + +@app.route('/api/user//conversations', methods=['GET']) +def get_user_conversations(user_id): + """获取用户所有对话""" + conn = get_db() + cursor = conn.cursor() + cursor.execute(''' + SELECT id, title, agent_id, messages, created_at, updated_at + FROM conversations WHERE user_id = ? ORDER BY updated_at DESC + ''', (user_id,)) + + conversations = [] + for row in cursor.fetchall(): + conv = dict(row) + # 解析消息JSON + try: + conv['messages'] = json.loads(conv['messages']) if conv['messages'] else [] + except: + conv['messages'] = [] + # 转换为前端格式(使用字符串ID) + conv['id'] = str(conv['id']) + conv['createdAt'] = int(datetime.strptime(conv['created_at'], '%Y-%m-%d %H:%M:%S').timestamp() * 1000) if conv['created_at'] else 0 + conv['updatedAt'] = int(datetime.strptime(conv['updated_at'], '%Y-%m-%d %H:%M:%S').timestamp() * 1000) if conv['updated_at'] else 0 + conv['agentId'] = conv['agent_id'] + conversations.append(conv) + + conn.close() + return jsonify(conversations) + + +@app.route('/api/user//conversations', methods=['POST']) +def create_user_conversation(user_id): + """创建新对话""" + data = request.json + title = data.get('title', '新对话') + agent_id = data.get('agentId') or data.get('agent_id') + messages = data.get('messages', []) + + conn = get_db() + cursor = conn.cursor() + cursor.execute(''' + INSERT INTO conversations (user_id, title, agent_id, messages) + VALUES (?, ?, ?, ?) + ''', (user_id, title, agent_id, json.dumps(messages))) + conn.commit() + + conv_id = cursor.lastrowid + conn.close() + + return jsonify({'success': True, 'id': str(conv_id)}) + + +@app.route('/api/user//conversations/', methods=['PUT']) +def update_user_conversation(user_id, conv_id): + """更新对话(添加消息、修改标题等)""" + data = request.json + conn = get_db() + cursor = conn.cursor() + + # 验证对话属于该用户 + cursor.execute('SELECT id FROM conversations WHERE id = ? AND user_id = ?', (conv_id, user_id)) + if not cursor.fetchone(): + conn.close() + return jsonify({'error': '对话不存在'}), 404 + + # 更新字段 + updates = [] + values = [] + + if 'title' in data: + updates.append('title = ?') + values.append(data['title']) + + if 'messages' in data: + updates.append('messages = ?') + values.append(json.dumps(data['messages'])) + + if 'agentId' in data or 'agent_id' in data: + updates.append('agent_id = ?') + values.append(data.get('agentId') or data.get('agent_id')) + + if updates: + updates.append('updated_at = CURRENT_TIMESTAMP') + sql = f'UPDATE conversations SET {", ".join(updates)} WHERE id = ?' + values.append(conv_id) + cursor.execute(sql, values) + conn.commit() + + conn.close() + return jsonify({'success': True}) + + +@app.route('/api/user//conversations/', methods=['DELETE']) +def delete_user_conversation(user_id, conv_id): + """删除对话""" + conn = get_db() + cursor = conn.cursor() + + # 验证对话属于该用户 + cursor.execute('SELECT id FROM conversations WHERE id = ? AND user_id = ?', (conv_id, user_id)) + if not cursor.fetchone(): + conn.close() + return jsonify({'error': '对话不存在'}), 404 + + cursor.execute('DELETE FROM conversations WHERE id = ?', (conv_id,)) + conn.commit() + conn.close() + + return jsonify({'success': True}) + + # ==================== 大模型接口管理 ==================== @app.route('/api/admin/llm', methods=['GET']) diff --git a/www/app.js b/www/app.js index d304b99..8a19d92 100644 --- a/www/app.js +++ b/www/app.js @@ -1620,14 +1620,43 @@ function showEditProfilePage() { return; } - // 获取完整用户信息 - const users = JSON.parse(localStorage.getItem('registeredUsers') || '[]'); - const fullUser = users.find(u => u.username === currentUser.username); - + // 如果用户已登录且有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 = fullUser?.phone || ''; - const email = fullUser?.email || ''; + const phone = currentUser.phone || ''; + const email = currentUser.email || ''; const gender = currentUser.gender || ''; const age = currentUser.age || ''; const region = currentUser.region || ''; @@ -1773,26 +1802,6 @@ function handleSaveProfile() { return; } - // 检查用户名是否被其他人占用 - const users = JSON.parse(localStorage.getItem('registeredUsers') || '[]'); - if (newUsername !== currentUser.username) { - if (users.find(u => u.username === newUsername)) { - showToast('用户名已被占用'); - return; - } - // 更新用户名 - const userIndex = users.findIndex(u => u.username === currentUser.username); - if (userIndex >= 0) { - users[userIndex].username = newUsername; - } - } - - // 更新邮箱 - const userIndex = users.findIndex(u => u.username === currentUser.username); - if (userIndex >= 0) { - users[userIndex].email = newEmail; - } - // 更新 currentUser currentUser.avatar = newAvatar; currentUser.username = newUsername; @@ -1800,12 +1809,42 @@ function handleSaveProfile() { currentUser.gender = newGender; currentUser.age = newAge ? parseInt(newAge) : ''; currentUser.region = newRegion; + currentUser.email = newEmail; - saveCurrentUser(); - localStorage.setItem('registeredUsers', JSON.stringify(users)); - - showToast('保存成功'); - switchPage('profile'); + // 如果用户已登录,调用 backend API 更新 + if (currentUser.id) { + 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'); + } } // ==================== 消息通知页面 ==================== @@ -2466,8 +2505,12 @@ function handleLogin() { registeredAt: Date.now() }; saveCurrentUser(); - showToast('登录成功'); - showMainPage(); + + // 从 backend 加载用户对话数据 + loadUserConversations().then(() => { + showToast('登录成功'); + showMainPage(); + }); } else { showToast(data.error || '用户名或密码错误'); } @@ -2633,8 +2676,12 @@ function handleRegister() { registeredAt: Date.now() }; saveCurrentUser(); - showToast('注册成功'); - showMainPage(); + + // 加载对话数据(新用户为空) + loadUserConversations().then(() => { + showToast('注册成功'); + showMainPage(); + }); } else { showToast(data.error || '注册失败'); } @@ -3496,6 +3543,17 @@ function deleteConversation(id) { 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(); } @@ -4166,7 +4224,91 @@ function setupScrollListener() { // 保存对话列表 function saveConversations() { + // 保存到本地存储(离线可用) localStorage.setItem('conversations', JSON.stringify(conversations)); + + // 如果用户已登录,同步到 backend + if (currentUser && currentUser.id) { + syncConversationsToBackend(); + } +} + +// 同步对话数据到 backend +async function syncConversationsToBackend() { + if (!currentUser || !currentUser.id) return; + + try { + // 批量同步所有对话 + for (const conv of conversations) { + const convId = parseInt(conv.id); + if (convId > 0) { + // 更新现有对话 + 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 { + // 新对话,需要创建 + 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; + } + } + } + } 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); + // 使用本地数据 + } } // 显示提示