Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 773fb89b01 | |||
| 8199773ef6 | |||
| d153986f3d |
126
backend/app.py
126
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')
|
cursor.execute('SELECT COUNT(*) FROM llm_configs')
|
||||||
if cursor.fetchone()[0] == 0:
|
if cursor.fetchone()[0] == 0:
|
||||||
@@ -581,6 +595,118 @@ def change_user_password(id):
|
|||||||
return jsonify({'success': True})
|
return jsonify({'success': True})
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== 用户对话数据同步 ====================
|
||||||
|
|
||||||
|
@app.route('/api/user/<int:user_id>/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/<int:user_id>/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/<int:user_id>/conversations/<int:conv_id>', 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/<int:user_id>/conversations/<int:conv_id>', 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'])
|
@app.route('/api/admin/llm', methods=['GET'])
|
||||||
|
|||||||
335
www/app.js
335
www/app.js
@@ -1620,14 +1620,43 @@ function showEditProfilePage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取完整用户信息
|
// 如果用户已登录且有ID,从 backend 获取完整用户信息
|
||||||
const users = JSON.parse(localStorage.getItem('registeredUsers') || '[]');
|
if (currentUser.id) {
|
||||||
const fullUser = users.find(u => u.username === currentUser.username);
|
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 avatar = currentUser.avatar || '👤';
|
||||||
const signature = currentUser.signature || '';
|
const signature = currentUser.signature || '';
|
||||||
const phone = fullUser?.phone || '';
|
const phone = currentUser.phone || '';
|
||||||
const email = fullUser?.email || '';
|
const email = currentUser.email || '';
|
||||||
const gender = currentUser.gender || '';
|
const gender = currentUser.gender || '';
|
||||||
const age = currentUser.age || '';
|
const age = currentUser.age || '';
|
||||||
const region = currentUser.region || '';
|
const region = currentUser.region || '';
|
||||||
@@ -1773,26 +1802,6 @@ function handleSaveProfile() {
|
|||||||
return;
|
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
|
||||||
currentUser.avatar = newAvatar;
|
currentUser.avatar = newAvatar;
|
||||||
currentUser.username = newUsername;
|
currentUser.username = newUsername;
|
||||||
@@ -1800,12 +1809,42 @@ function handleSaveProfile() {
|
|||||||
currentUser.gender = newGender;
|
currentUser.gender = newGender;
|
||||||
currentUser.age = newAge ? parseInt(newAge) : '';
|
currentUser.age = newAge ? parseInt(newAge) : '';
|
||||||
currentUser.region = newRegion;
|
currentUser.region = newRegion;
|
||||||
|
currentUser.email = newEmail;
|
||||||
|
|
||||||
saveCurrentUser();
|
// 如果用户已登录,调用 backend API 更新
|
||||||
localStorage.setItem('registeredUsers', JSON.stringify(users));
|
if (currentUser.id) {
|
||||||
|
fetch(`/api/user/${currentUser.id}`, {
|
||||||
showToast('保存成功');
|
method: 'PUT',
|
||||||
switchPage('profile');
|
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()
|
registeredAt: Date.now()
|
||||||
};
|
};
|
||||||
saveCurrentUser();
|
saveCurrentUser();
|
||||||
showToast('登录成功');
|
|
||||||
showMainPage();
|
// 从 backend 加载用户对话数据
|
||||||
|
loadUserConversations().then(() => {
|
||||||
|
showToast('登录成功');
|
||||||
|
showMainPage();
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
showToast(data.error || '用户名或密码错误');
|
showToast(data.error || '用户名或密码错误');
|
||||||
}
|
}
|
||||||
@@ -2499,15 +2542,6 @@ function showRegisterPage() {
|
|||||||
<label>手机号 <span class="required">*</span></label>
|
<label>手机号 <span class="required">*</span></label>
|
||||||
<input type="tel" id="registerPhone" placeholder="请输入手机号" maxlength="11" autocomplete="tel">
|
<input type="tel" id="registerPhone" placeholder="请输入手机号" maxlength="11" autocomplete="tel">
|
||||||
</div>
|
</div>
|
||||||
<div class="auth-input-group" style="display: flex; gap: 8px;">
|
|
||||||
<div style="flex: 1;">
|
|
||||||
<label>验证码 <span class="required">*</span></label>
|
|
||||||
<input type="text" id="registerCode" placeholder="请输入验证码" maxlength="6">
|
|
||||||
</div>
|
|
||||||
<div style="flex-shrink: 0; align-self: flex-end;">
|
|
||||||
<button class="send-code-btn" id="getCodeBtn">获取验证码</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="auth-input-group">
|
<div class="auth-input-group">
|
||||||
<label>邮箱 <span class="optional">(可选)</span></label>
|
<label>邮箱 <span class="optional">(可选)</span></label>
|
||||||
<input type="email" id="registerEmail" placeholder="请输入邮箱(可选)" autocomplete="email">
|
<input type="email" id="registerEmail" placeholder="请输入邮箱(可选)" autocomplete="email">
|
||||||
@@ -2552,12 +2586,6 @@ function showRegisterPage() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取验证码按钮
|
|
||||||
const getCodeBtn = document.getElementById('getCodeBtn');
|
|
||||||
if (getCodeBtn) {
|
|
||||||
getCodeBtn.addEventListener('click', handleGetCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 回车注册
|
// 回车注册
|
||||||
document.getElementById('registerPasswordConfirm')?.addEventListener('keydown', (e) => {
|
document.getElementById('registerPasswordConfirm')?.addEventListener('keydown', (e) => {
|
||||||
if (e.key === 'Enter') handleRegister();
|
if (e.key === 'Enter') handleRegister();
|
||||||
@@ -2570,50 +2598,6 @@ function showRegisterPage() {
|
|||||||
e.target.value = e.target.value.replace(/\D/g, '');
|
e.target.value = e.target.value.replace(/\D/g, '');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证码输入限制(只允许数字)
|
|
||||||
const codeInput = document.getElementById('registerCode');
|
|
||||||
if (codeInput) {
|
|
||||||
codeInput.addEventListener('input', (e) => {
|
|
||||||
e.target.value = e.target.value.replace(/\D/g, '');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取验证码(模拟)
|
|
||||||
let verifyCode = '';
|
|
||||||
let codeExpireTime = 0;
|
|
||||||
|
|
||||||
function handleGetCode() {
|
|
||||||
const phone = document.getElementById('registerPhone')?.value.trim();
|
|
||||||
|
|
||||||
if (!validatePhone(phone)) {
|
|
||||||
showToast('请先输入正确的手机号');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生成6位随机验证码
|
|
||||||
verifyCode = Math.floor(100000 + Math.random() * 900000).toString();
|
|
||||||
codeExpireTime = Date.now() + 5 * 60 * 1000; // 5分钟有效
|
|
||||||
|
|
||||||
// 模拟发送(实际项目中应调用短信API)
|
|
||||||
showToast(`验证码已发送: ${verifyCode}(演示)`);
|
|
||||||
|
|
||||||
// 60秒倒计时
|
|
||||||
const getCodeBtn = document.getElementById('getCodeBtn');
|
|
||||||
if (getCodeBtn) {
|
|
||||||
getCodeBtn.disabled = true;
|
|
||||||
let countdown = 60;
|
|
||||||
const timer = setInterval(() => {
|
|
||||||
getCodeBtn.textContent = `${countdown}s`;
|
|
||||||
countdown--;
|
|
||||||
if (countdown <= 0) {
|
|
||||||
clearInterval(timer);
|
|
||||||
getCodeBtn.disabled = false;
|
|
||||||
getCodeBtn.textContent = '获取验证码';
|
|
||||||
}
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证手机号格式
|
// 验证手机号格式
|
||||||
@@ -2634,7 +2618,6 @@ function handleRegister() {
|
|||||||
const username = document.getElementById('registerUsername')?.value.trim();
|
const username = document.getElementById('registerUsername')?.value.trim();
|
||||||
const phone = document.getElementById('registerPhone')?.value.trim();
|
const phone = document.getElementById('registerPhone')?.value.trim();
|
||||||
const email = document.getElementById('registerEmail')?.value.trim();
|
const email = document.getElementById('registerEmail')?.value.trim();
|
||||||
const code = document.getElementById('registerCode')?.value.trim();
|
|
||||||
const password = document.getElementById('registerPassword')?.value;
|
const password = document.getElementById('registerPassword')?.value;
|
||||||
const passwordConfirm = document.getElementById('registerPasswordConfirm')?.value;
|
const passwordConfirm = document.getElementById('registerPasswordConfirm')?.value;
|
||||||
|
|
||||||
@@ -2650,18 +2633,6 @@ function handleRegister() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证验证码
|
|
||||||
if (!code || code.length !== 6) {
|
|
||||||
showToast('请输入6位验证码');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查验证码是否正确(模拟)
|
|
||||||
if (code !== verifyCode || Date.now() > codeExpireTime) {
|
|
||||||
showToast('验证码不正确或已过期');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证邮箱(可选)
|
// 验证邮箱(可选)
|
||||||
if (email && !validateEmail(email)) {
|
if (email && !validateEmail(email)) {
|
||||||
showToast('请输入正确的邮箱格式');
|
showToast('请输入正确的邮箱格式');
|
||||||
@@ -2679,7 +2650,9 @@ function handleRegister() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 调用后台注册API
|
// 调用后台注册API(验证码暂时跳过,传随机值)
|
||||||
|
const code = Math.floor(100000 + Math.random() * 900000).toString();
|
||||||
|
|
||||||
fetch('/api/register', {
|
fetch('/api/register', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
@@ -2703,8 +2676,12 @@ function handleRegister() {
|
|||||||
registeredAt: Date.now()
|
registeredAt: Date.now()
|
||||||
};
|
};
|
||||||
saveCurrentUser();
|
saveCurrentUser();
|
||||||
showToast('注册成功');
|
|
||||||
showMainPage();
|
// 加载对话数据(新用户为空)
|
||||||
|
loadUserConversations().then(() => {
|
||||||
|
showToast('注册成功');
|
||||||
|
showMainPage();
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
showToast(data.error || '注册失败');
|
showToast(data.error || '注册失败');
|
||||||
}
|
}
|
||||||
@@ -2846,7 +2823,7 @@ function showAgentHistoryPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 打开智能体对话
|
// 打开智能体对话
|
||||||
function openAgent(agentId) {
|
async function openAgent(agentId) {
|
||||||
currentAgent = systemAgents.find(a => a.id === agentId);
|
currentAgent = systemAgents.find(a => a.id === agentId);
|
||||||
if (!currentAgent) return;
|
if (!currentAgent) return;
|
||||||
|
|
||||||
@@ -2867,9 +2844,31 @@ function openAgent(agentId) {
|
|||||||
setAgentUsed(agentId);
|
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 = {
|
const newConv = {
|
||||||
id: Date.now().toString(),
|
id: backendId || Date.now().toString(),
|
||||||
title: currentAgent.name,
|
title: currentAgent.name,
|
||||||
messages: [],
|
messages: [],
|
||||||
agentId: agentId,
|
agentId: agentId,
|
||||||
@@ -3320,15 +3319,33 @@ function togglePinConversation(convId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 创建新对话
|
// 创建新对话
|
||||||
function createNewConversation() {
|
async function createNewConversation() {
|
||||||
// 检查未登录用户的对话限制
|
// 检查未登录用户的对话限制
|
||||||
if (!currentUser && !canCreateNewChat()) {
|
if (!currentUser && !canCreateNewChat()) {
|
||||||
showLimitDialog('chat_session');
|
showLimitDialog('chat_session');
|
||||||
return;
|
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 = {
|
const newConv = {
|
||||||
id: Date.now().toString(),
|
id: backendId || Date.now().toString(), // 使用 backend ID 或本地 ID
|
||||||
title: '新对话',
|
title: '新对话',
|
||||||
messages: [],
|
messages: [],
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
@@ -3566,6 +3583,17 @@ function deleteConversation(id) {
|
|||||||
|
|
||||||
conversations = conversations.filter(c => c.id !== id);
|
conversations = conversations.filter(c => c.id !== id);
|
||||||
saveConversations();
|
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();
|
showConversationList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3634,6 +3662,9 @@ async function sendMessage() {
|
|||||||
currentConversation.updatedAt = Date.now();
|
currentConversation.updatedAt = Date.now();
|
||||||
saveConversations();
|
saveConversations();
|
||||||
|
|
||||||
|
// 同步当前对话到 backend(用户已登录)
|
||||||
|
syncConversationToBackend(currentConversation);
|
||||||
|
|
||||||
// 增加消息计数(未登录用户)
|
// 增加消息计数(未登录用户)
|
||||||
if (!currentUser) {
|
if (!currentUser) {
|
||||||
if (currentConversation.agentId) {
|
if (currentConversation.agentId) {
|
||||||
@@ -3754,6 +3785,7 @@ async function streamGenerate(userMsgIndex) {
|
|||||||
contentEl.innerHTML = renderMarkdown(currentConversation.messages[aiMessageIndex].content);
|
contentEl.innerHTML = renderMarkdown(currentConversation.messages[aiMessageIndex].content);
|
||||||
currentConversation.updatedAt = Date.now();
|
currentConversation.updatedAt = Date.now();
|
||||||
saveConversations();
|
saveConversations();
|
||||||
|
syncConversationToBackend(currentConversation);
|
||||||
renderMessages();
|
renderMessages();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -3830,6 +3862,7 @@ async function streamGenerate(userMsgIndex) {
|
|||||||
hideStopGenerateBtn();
|
hideStopGenerateBtn();
|
||||||
currentConversation.updatedAt = Date.now();
|
currentConversation.updatedAt = Date.now();
|
||||||
saveConversations();
|
saveConversations();
|
||||||
|
syncConversationToBackend(currentConversation);
|
||||||
renderMessages();
|
renderMessages();
|
||||||
|
|
||||||
// 自动总结标题:第一次对话和每隔5次对话
|
// 自动总结标题:第一次对话和每隔5次对话
|
||||||
@@ -4236,9 +4269,87 @@ function setupScrollListener() {
|
|||||||
|
|
||||||
// 保存对话列表
|
// 保存对话列表
|
||||||
function saveConversations() {
|
function saveConversations() {
|
||||||
|
// 保存到本地存储(离线可用)
|
||||||
localStorage.setItem('conversations', JSON.stringify(conversations));
|
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) {
|
function showToast(message) {
|
||||||
const toast = document.createElement('div');
|
const toast = document.createElement('div');
|
||||||
|
|||||||
Reference in New Issue
Block a user