diff --git a/channel/web/chat.html b/channel/web/chat.html index 36706400..50e81fa9 100644 --- a/channel/web/chat.html +++ b/channel/web/chat.html @@ -170,6 +170,24 @@ flex: 1; } + #github-link { + display: flex; + align-items: center; + margin-left: 15px; + color: var(--text-color); + text-decoration: none; + transition: opacity 0.2s; + } + + #github-link:hover { + opacity: 0.8; + } + + #github-icon { + height: 24px; + width: 24px; + } + #messages { flex: 1; overflow-y: auto; @@ -210,7 +228,7 @@ } .user-container .avatar { - margin-left: 15px; + margin-left: 20px; margin-right: 0; } @@ -219,6 +237,7 @@ } .user-container .message { + padding: 13px 16px; background-color: var(--bot-msg-bg); border-radius: 10px; margin-bottom: 8px; @@ -266,7 +285,7 @@ } .message { - padding: 12px 16px; + padding: 5px 16px; border-radius: 10px; margin-top: 0; margin-bottom: 8px; @@ -400,7 +419,7 @@ .message img { max-width: 100%; height: auto; - margin: 1em 0; + margin: 0 0; } .timestamp { @@ -548,10 +567,12 @@ left: -260px; height: 100%; z-index: 1000; + transition: left 0.3s ease; } #sidebar.active { left: 0; + box-shadow: 2px 0 10px rgba(0, 0, 0, 0.2); } #menu-toggle { @@ -577,6 +598,23 @@ #header-logo { height: 24px; } + + /* 添加遮罩层,当侧边栏打开时显示 */ + #sidebar-overlay { + display: none; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + z-index: 999; + cursor: pointer; + } + + #sidebar.active + #sidebar-overlay { + display: block; + } } /* Dark mode support */ @@ -717,7 +755,7 @@ - +
AI Assistant Logo
AI 助手
+ + GitHub +
@@ -778,10 +819,17 @@ const newChatButton = document.getElementById('new-chat'); const chatHistory = document.getElementById('chat-history'); - // 简化变量,只保留用户ID - let userId = 'user_' + Math.random().toString(36).substring(2, 10); - let currentSessionId = 'default_session'; // 使用固定会话ID - + // 生成新的会话ID + function generateSessionId() { + return 'session_' + ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c => + (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) + ); + } + + // 生成初始会话ID + let sessionId = generateSessionId(); + console.log('Session ID:', sessionId); + // 添加一个变量来跟踪输入法状态 let isComposing = false; @@ -815,15 +863,18 @@ }); // 处理菜单切换 - menuToggle.addEventListener('click', function() { + menuToggle.addEventListener('click', function(event) { + event.stopPropagation(); // 防止事件冒泡到 main-content sidebar.classList.toggle('active'); }); - // 处理新对话按钮 - 创建新的用户ID和清空当前对话 + // 处理新对话按钮 - 创建新的会话ID和清空当前对话 newChatButton.addEventListener('click', function() { - // 生成新的用户ID - userId = 'user_' + Math.random().toString(36).substring(2, 10); - console.log('New conversation started with user ID:', userId); + // 生成新的会话ID + sessionId = generateSessionId(); + // 将新的会话ID保存到全局变量,供轮询函数使用 + window.sessionId = sessionId; + console.log('New conversation started with new session ID:', sessionId); // 清空聊天记录 clearChat(); @@ -881,27 +932,42 @@ input.style.height = '52px'; sendButton.disabled = true; - // 发送到服务器并等待响应 + // 使用当前的全局会话ID + const currentSessionId = window.sessionId || sessionId; + + // 发送到服务器并获取请求ID axios({ method: 'post', url: '/message', data: { - user_id: userId, + session_id: currentSessionId, // 使用最新的会话ID message: userMessage, - timestamp: timestamp.toISOString(), - session_id: currentSessionId + timestamp: timestamp.toISOString() }, - timeout: 120000 // 120秒超时 + timeout: 10000 // 10秒超时 }) .then(response => { - // 移除加载消息 - if (loadingContainer.parentNode) { - messagesDiv.removeChild(loadingContainer); - } - - // 添加AI回复 - if (response.data.reply) { - addBotMessage(response.data.reply, new Date()); + if (response.data.status === "success") { + // 保存当前请求ID,用于识别响应 + const currentRequestId = response.data.request_id; + + // 如果还没有开始轮询,则开始轮询 + if (!window.isPolling) { + startPolling(currentSessionId); + } + + // 将请求ID和加载容器关联起来 + window.loadingContainers = window.loadingContainers || {}; + window.loadingContainers[currentRequestId] = loadingContainer; + + // 初始化请求的响应容器映射 + window.requestContainers = window.requestContainers || {}; + } else { + // 处理错误 + if (loadingContainer.parentNode) { + messagesDiv.removeChild(loadingContainer); + } + addBotMessage("抱歉,发生了错误,请稍后再试。", new Date()); } }) .catch(error => { @@ -920,178 +986,108 @@ } } - // 添加加载中的消息 - function addLoadingMessage() { - const botContainer = document.createElement('div'); - botContainer.className = 'bot-container loading-container'; + // 修改轮询函数,确保正确处理多条回复 + function startPolling(sessionId) { + if (window.isPolling) return; - const messageContainer = document.createElement('div'); - messageContainer.className = 'message-container'; + window.isPolling = true; + console.log('Starting polling with session ID:', sessionId); - messageContainer.innerHTML = ` -
- -
-
-
-
- - - -
-
-
- `; - - botContainer.appendChild(messageContainer); - messagesDiv.appendChild(botContainer); - scrollToBottom(); - - return botContainer; - } - - // 替换 formatMessage 函数,使用 markdown-it 替代 marked - function formatMessage(content) { - try { - // 初始化 markdown-it 实例 - const md = window.markdownit({ - html: false, // 禁用 HTML 标签 - xhtmlOut: false, // 使用 '/' 关闭单标签 - breaks: true, // 将换行符转换为
- linkify: true, // 自动将 URL 转换为链接 - typographer: true, // 启用一些语言中性的替换和引号美化 - highlight: function(str, lang) { - if (lang && hljs.getLanguage(lang)) { - try { - return hljs.highlight(str, { language: lang }).value; - } catch (e) { - console.error('Error highlighting code:', e); + function poll() { + if (!window.isPolling) return; + + // 如果页面已关闭或导航离开,停止轮询 + if (document.hidden) { + setTimeout(poll, 5000); // 页面不可见时降低轮询频率 + return; + } + + // 使用当前的会话ID,而不是闭包中的sessionId + const currentSessionId = window.sessionId || sessionId; + + axios({ + method: 'post', + url: '/poll', + data: { + session_id: currentSessionId + }, + timeout: 5000 + }) + .then(response => { + if (response.data.status === "success") { + if (response.data.has_content) { + console.log('Received response:', response.data); + + // 获取请求ID和内容 + const requestId = response.data.request_id; + const content = response.data.content; + const timestamp = new Date(response.data.timestamp * 1000); + + // 检查是否有对应的加载容器 + if (window.loadingContainers && window.loadingContainers[requestId]) { + // 移除加载容器 + const loadingContainer = window.loadingContainers[requestId]; + if (loadingContainer && loadingContainer.parentNode) { + messagesDiv.removeChild(loadingContainer); + } + + // 删除已处理的加载容器引用 + delete window.loadingContainers[requestId]; } + + // 始终创建新的消息,无论是否是同一个请求的后续回复 + addBotMessage(content, timestamp, requestId); + + // 滚动到底部 + scrollToBottom(); } - return hljs.highlightAuto(str).value; - } - }); - - // 渲染 Markdown - return md.render(content); - } catch (e) { - console.error('Error parsing markdown:', e); - // 如果解析失败,至少确保换行符正确显示 - return content.replace(/\n/g, '
'); - } - } - - // 更新 applyHighlighting 函数 - function applyHighlighting() { - try { - document.querySelectorAll('pre code').forEach((block) => { - // 确保代码块有正确的类 - if (!block.classList.contains('hljs')) { - block.classList.add('hljs'); - } - - // 尝试获取语言 - let language = ''; - block.classList.forEach(cls => { - if (cls.startsWith('language-')) { - language = cls.replace('language-', ''); - } - }); - - // 应用高亮 - if (language && hljs.getLanguage(language)) { - try { - hljs.highlightBlock(block); - } catch (e) { - console.error('Error highlighting specific language:', e); - hljs.highlightAuto(block); - } + + // 继续轮询,使用原来的2秒间隔 + setTimeout(poll, 2000); } else { - hljs.highlightAuto(block); + // 处理错误但继续轮询 + console.error('Error in polling response:', response.data.message); + setTimeout(poll, 3000); } + }) + .catch(error => { + console.error('Error polling for response:', error); + // 出错后继续轮询,但间隔更长 + setTimeout(poll, 3000); }); - } catch (e) { - console.error('Error applying code highlighting:', e); } - } - - // 添加用户消息的函数 (保存到localStorage) - function addUserMessage(content, timestamp) { - // 显示消息 - displayUserMessage(content, timestamp); - // 保存到localStorage - saveMessageToLocalStorage({ - role: 'user', - content: content, - timestamp: timestamp.getTime() - }); + // 开始轮询 + poll(); } - // 添加机器人消息的函数 (保存到localStorage) - function addBotMessage(content, timestamp) { + // 添加机器人消息的函数 (保存到localStorage),增加requestId参数 + function addBotMessage(content, timestamp, requestId) { // 显示消息 - displayBotMessage(content, timestamp); + displayBotMessage(content, timestamp, requestId); // 保存到localStorage saveMessageToLocalStorage({ role: 'assistant', content: content, - timestamp: timestamp.getTime() + timestamp: timestamp.getTime(), + requestId: requestId }); } - // 只显示用户消息而不保存到localStorage - function displayUserMessage(content, timestamp) { - const userContainer = document.createElement('div'); - userContainer.className = 'user-container'; - - const messageContainer = document.createElement('div'); - messageContainer.className = 'message-container'; - - // 安全地格式化消息 - let formattedContent; - try { - formattedContent = formatMessage(content); - } catch (e) { - console.error('Error formatting user message:', e); - formattedContent = `

${content.replace(/\n/g, '
')}

`; - } - - messageContainer.innerHTML = ` -
- -
-
-
${formattedContent}
-
${formatTimestamp(timestamp)}
-
- `; - - userContainer.appendChild(messageContainer); - messagesDiv.appendChild(userContainer); - - // 应用代码高亮 - setTimeout(() => { - applyHighlighting(); - }, 0); - - scrollToBottom(); - } - - // 只显示机器人消息而不保存到localStorage - function displayBotMessage(content, timestamp) { + // 修改显示机器人消息的函数,增加requestId参数 + function displayBotMessage(content, timestamp, requestId) { const botContainer = document.createElement('div'); botContainer.className = 'bot-container'; + // 如果有requestId,将其存储在数据属性中 + if (requestId) { + botContainer.dataset.requestId = requestId; + } + const messageContainer = document.createElement('div'); messageContainer.className = 'message-container'; - // 确保时间戳是有效的 Date 对象 - if (!(timestamp instanceof Date) || isNaN(timestamp)) { - timestamp = new Date(); - } - // 安全地格式化消息 let formattedContent; try { @@ -1114,45 +1110,82 @@ botContainer.appendChild(messageContainer); messagesDiv.appendChild(botContainer); - // 使用setTimeout确保DOM已更新,并延长等待时间 + // 应用代码高亮 setTimeout(() => { - try { - // 直接对新添加的消息应用高亮 - const codeBlocks = botContainer.querySelectorAll('pre code'); - codeBlocks.forEach(block => { - // 确保代码块有正确的类 - if (!block.classList.contains('hljs')) { - block.classList.add('hljs'); - } - - // 尝试获取语言 - let language = ''; - block.classList.forEach(cls => { - if (cls.startsWith('language-')) { - language = cls.replace('language-', ''); - } - }); - - // 应用高亮 - if (language && hljs.getLanguage(language)) { - try { - hljs.highlightBlock(block); - } catch (e) { - console.error('Error highlighting specific language:', e); - hljs.highlightAuto(block); - } - } else { - hljs.highlightAuto(block); - } - }); - } catch (e) { - console.error('Error in delayed highlighting:', e); - } - }, 100); // 增加延迟以确保DOM完全更新 + applyHighlighting(); + }, 0); scrollToBottom(); } + // 处理响应 + function handleResponse(requestId, content) { + // 获取该请求的加载容器 + const loadingContainer = window.loadingContainers && window.loadingContainers[requestId]; + + // 如果有加载容器,移除它 + if (loadingContainer && loadingContainer.parentNode) { + messagesDiv.removeChild(loadingContainer); + delete window.loadingContainers[requestId]; + } + + // 为每个请求创建一个新的消息容器 + if (!window.requestContainers[requestId]) { + window.requestContainers[requestId] = createBotMessageContainer(content, new Date()); + } else { + // 更新现有消息容器 + updateBotMessageContent(window.requestContainers[requestId], content); + } + + // 保存消息到localStorage + saveMessageToLocalStorage({ + role: 'assistant', + content: content, + timestamp: new Date().getTime(), + request_id: requestId + }); + } + + // 修改createBotMessageContainer函数,使其返回创建的容器 + function createBotMessageContainer(content, timestamp) { + const botContainer = document.createElement('div'); + botContainer.className = 'bot-container'; + + const messageContainer = document.createElement('div'); + messageContainer.className = 'message-container'; + + // 安全地格式化消息 + let formattedContent; + try { + formattedContent = formatMessage(content); + } catch (e) { + console.error('Error formatting bot message:', e); + formattedContent = `

${content.replace(/\n/g, '
')}

`; + } + + messageContainer.innerHTML = ` +
+ +
+
+
${formattedContent}
+
${formatTimestamp(timestamp)}
+
+ `; + + botContainer.appendChild(messageContainer); + messagesDiv.appendChild(botContainer); + + // 应用代码高亮 + setTimeout(() => { + applyHighlighting(); + }, 0); + + scrollToBottom(); + + return botContainer; + } + // 格式化时间戳 function formatTimestamp(date) { return date.toLocaleTimeString(); @@ -1223,8 +1256,8 @@ }); }); - // 清空localStorage中的消息 - 使用用户ID作为键 - localStorage.setItem(`chatMessages_${userId}`, JSON.stringify([])); + // 清空localStorage中的消息 - 使用会话ID作为键 + localStorage.setItem(`chatMessages_${sessionId}`, JSON.stringify([])); // 在移动设备上关闭侧边栏 if (window.innerWidth <= 768) { @@ -1232,26 +1265,242 @@ } } - // 从localStorage加载消息 - 使用用户ID作为键 + // 从localStorage加载消息 - 使用会话ID作为键 function loadMessagesFromLocalStorage() { try { - return JSON.parse(localStorage.getItem(`chatMessages_${userId}`) || '[]'); + return JSON.parse(localStorage.getItem(`chatMessages_${sessionId}`) || '[]'); } catch (error) { console.error('Error loading messages from localStorage:', error); return []; } } - // 保存消息到localStorage - 使用用户ID作为键 + // 保存消息到localStorage - 使用会话ID作为键 function saveMessageToLocalStorage(message) { try { const messages = loadMessagesFromLocalStorage(); messages.push(message); - localStorage.setItem(`chatMessages_${userId}`, JSON.stringify(messages)); + localStorage.setItem(`chatMessages_${sessionId}`, JSON.stringify(messages)); } catch (error) { console.error('Error saving message to localStorage:', error); } } + + // 添加用户消息的函数 (保存到localStorage) + function addUserMessage(content, timestamp) { + // 显示消息 + displayUserMessage(content, timestamp); + + // 保存到localStorage + saveMessageToLocalStorage({ + role: 'user', + content: content, + timestamp: timestamp.getTime() + }); + } + + // 只显示用户消息而不保存到localStorage + function displayUserMessage(content, timestamp) { + const userContainer = document.createElement('div'); + userContainer.className = 'user-container'; + + const messageContainer = document.createElement('div'); + messageContainer.className = 'message-container'; + + // 安全地格式化消息 + let formattedContent; + try { + formattedContent = formatMessage(content); + } catch (e) { + console.error('Error formatting user message:', e); + formattedContent = `

${content.replace(/\n/g, '
')}

`; + } + + messageContainer.innerHTML = ` +
+ +
+
+
${formattedContent}
+
${formatTimestamp(timestamp)}
+
+ `; + + userContainer.appendChild(messageContainer); + messagesDiv.appendChild(userContainer); + + // 应用代码高亮 + setTimeout(() => { + applyHighlighting(); + }, 0); + + scrollToBottom(); + } + + // 添加加载中的消息 + function addLoadingMessage() { + const botContainer = document.createElement('div'); + botContainer.className = 'bot-container loading-container'; + + const messageContainer = document.createElement('div'); + messageContainer.className = 'message-container'; + + messageContainer.innerHTML = ` +
+ +
+
+
+
+ + + +
+
+
+ `; + + botContainer.appendChild(messageContainer); + messagesDiv.appendChild(botContainer); + scrollToBottom(); + + return botContainer; + } + + // 自动将链接设置为在新标签页打开 + const externalLinksPlugin = (md) => { + // 保存原始的链接渲染器 + const defaultRender = md.renderer.rules.link_open || function(tokens, idx, options, env, self) { + return self.renderToken(tokens, idx, options); + }; + + // 重写链接渲染器 + md.renderer.rules.link_open = function(tokens, idx, options, env, self) { + // 为所有链接添加 target="_blank" 和 rel="noopener noreferrer" + const token = tokens[idx]; + + // 添加 target="_blank" 属性 + token.attrPush(['target', '_blank']); + + // 添加 rel="noopener noreferrer" 以提高安全性 + token.attrPush(['rel', 'noopener noreferrer']); + + // 调用默认渲染器 + return defaultRender(tokens, idx, options, env, self); + }; + }; + + // 替换 formatMessage 函数,使用 markdown-it 替代 marked + function formatMessage(content) { + try { + // 初始化 markdown-it 实例 + const md = window.markdownit({ + html: false, // 禁用 HTML 标签 + xhtmlOut: false, // 使用 '/' 关闭单标签 + breaks: true, // 将换行符转换为
+ linkify: true, // 自动将 URL 转换为链接 + typographer: true, // 启用一些语言中性的替换和引号美化 + highlight: function(str, lang) { + if (lang && hljs.getLanguage(lang)) { + try { + return hljs.highlight(str, { language: lang }).value; + } catch (e) { + console.error('Error highlighting code:', e); + } + } + return hljs.highlightAuto(str).value; + } + }); + + // 自动将图片URL转换为图片标签 + const autoImagePlugin = (md) => { + const defaultRender = md.renderer.rules.text || function(tokens, idx, options, env, self) { + return self.renderToken(tokens, idx, options); + }; + + md.renderer.rules.text = function(tokens, idx, options, env, self) { + const token = tokens[idx]; + const text = token.content.trim(); + + // 检测是否完全是一个图片链接 (以https://开头,以图片扩展名结尾) + const imageRegex = /^https?:\/\/\S+\.(jpg|jpeg|png|gif|webp)(\?\S*)?$/i; + if (imageRegex.test(text)) { + return `Image`; + } + + // 使用默认渲染 + return defaultRender(tokens, idx, options, env, self); + }; + }; + + // 应用插件 + md.use(autoImagePlugin); + + // 应用外部链接插件 + md.use(externalLinksPlugin); + + // 渲染 Markdown + return md.render(content); + } catch (e) { + console.error('Error parsing markdown:', e); + // 如果解析失败,至少确保换行符正确显示 + return content.replace(/\n/g, '
'); + } + } + + // 更新 applyHighlighting 函数 + function applyHighlighting() { + try { + document.querySelectorAll('pre code').forEach((block) => { + // 确保代码块有正确的类 + if (!block.classList.contains('hljs')) { + block.classList.add('hljs'); + } + + // 尝试获取语言 + let language = ''; + block.classList.forEach(cls => { + if (cls.startsWith('language-')) { + language = cls.replace('language-', ''); + } + }); + + // 应用高亮 + if (language && hljs.getLanguage(language)) { + try { + hljs.highlightBlock(block); + } catch (e) { + console.error('Error highlighting specific language:', e); + hljs.highlightAuto(block); + } + } else { + hljs.highlightAuto(block); + } + }); + } catch (e) { + console.error('Error applying code highlighting:', e); + } + } + + // 在 #main-content 上添加点击事件,用于关闭侧边栏 + document.getElementById('main-content').addEventListener('click', function(event) { + // 只在移动视图下且侧边栏打开时处理 + if (window.innerWidth <= 768 && sidebar.classList.contains('active')) { + sidebar.classList.remove('active'); + } + }); + + // 阻止侧边栏内部点击事件冒泡到 main-content + document.getElementById('sidebar').addEventListener('click', function(event) { + event.stopPropagation(); + }); + + // 添加遮罩层点击事件,用于关闭侧边栏 + document.getElementById('sidebar-overlay').addEventListener('click', function() { + if (sidebar.classList.contains('active')) { + sidebar.classList.remove('active'); + } + }); \ No newline at end of file diff --git a/channel/web/static/github.png b/channel/web/static/github.png new file mode 100644 index 00000000..9122abb5 Binary files /dev/null and b/channel/web/static/github.png differ diff --git a/channel/web/web_channel.py b/channel/web/web_channel.py index c62598ae..057a745e 100644 --- a/channel/web/web_channel.py +++ b/channel/web/web_channel.py @@ -2,6 +2,7 @@ import sys import time import web import json +import uuid from queue import Queue, Empty from bridge.context import * from bridge.reply import Reply, ReplyType @@ -12,6 +13,8 @@ from common.singleton import singleton from config import conf import os import mimetypes # 添加这行来处理MIME类型 +import threading +import logging class WebMessage(ChatMessage): def __init__( @@ -43,39 +46,54 @@ class WebChannel(ChatChannel): def __init__(self): super().__init__() - self.message_queues = {} # 为每个用户存储一个消息队列 self.msg_id_counter = 0 # 添加消息ID计数器 + self.session_queues = {} # 存储session_id到队列的映射 + self.request_to_session = {} # 存储request_id到session_id的映射 def _generate_msg_id(self): """生成唯一的消息ID""" self.msg_id_counter += 1 return str(int(time.time())) + str(self.msg_id_counter) + def _generate_request_id(self): + """生成唯一的请求ID""" + return str(uuid.uuid4()) + def send(self, reply: Reply, context: Context): try: if reply.type in self.NOT_SUPPORT_REPLYTYPE: logger.warning(f"Web channel doesn't support {reply.type} yet") return + + if reply.type == ReplyType.IMAGE_URL: + time.sleep(0.5) + + # 获取请求ID和会话ID + request_id = context.get("request_id", None) + + if not request_id: + logger.error("No request_id found in context, cannot send message") + return - # 获取用户ID - user_id = context.get("receiver", None) - if not user_id: - logger.error("No receiver found in context, cannot send message") + # 通过request_id获取session_id + session_id = self.request_to_session.get(request_id) + if not session_id: + logger.error(f"No session_id found for request {request_id}") return - # 检查是否有响应队列 - response_queue = context.get("response_queue", None) - if response_queue: - # 直接将响应放入队列 + # 检查是否有会话队列 + if session_id in self.session_queues: + # 创建响应数据,包含请求ID以区分不同请求的响应 response_data = { "type": str(reply.type), "content": reply.content, - "timestamp": time.time() + "timestamp": time.time(), + "request_id": request_id } - response_queue.put(response_data) - logger.debug(f"Response sent to queue for user {user_id}") + self.session_queues[session_id].put(response_data) + logger.debug(f"Response sent to queue for session {session_id}, request {request_id}") else: - logger.warning(f"No response queue found for user {user_id}, response dropped") + logger.warning(f"No response queue found for session {session_id}, response dropped") except Exception as e: logger.error(f"Error in send method: {e}") @@ -83,57 +101,83 @@ class WebChannel(ChatChannel): def post_message(self): """ Handle incoming messages from users via POST request. + Returns a request_id for tracking this specific request. """ try: data = web.data() # 获取原始POST数据 json_data = json.loads(data) - user_id = json_data.get('user_id', 'default_user') - prompt = json_data.get('message', '') session_id = json_data.get('session_id', f'session_{int(time.time())}') - except json.JSONDecodeError: - return json.dumps({"status": "error", "message": "Invalid JSON"}) - except Exception as e: - return json.dumps({"status": "error", "message": str(e)}) - - if not prompt: - return json.dumps({"status": "error", "message": "No message provided"}) + prompt = json_data.get('message', '') - try: - msg_id = self._generate_msg_id() - web_message = WebMessage( - msg_id=msg_id, - content=prompt, - from_user_id=user_id, - to_user_id="Chatgpt", - other_user_id=user_id - ) + # 生成请求ID + request_id = self._generate_request_id() - context = self._compose_context(ContextType.TEXT, prompt, msg=web_message) - if not context: - return json.dumps({"status": "error", "message": "Failed to process message"}) - - # 创建一个响应队列 - response_queue = Queue() + # 将请求ID与会话ID关联 + self.request_to_session[request_id] = session_id - # 确保上下文包含必要的信息 - context["isgroup"] = False - context["receiver"] = user_id - context["session_id"] = user_id - context["response_queue"] = response_queue - - # 发送消息到处理队列 - self.produce(context) + # 确保会话队列存在 + if session_id not in self.session_queues: + self.session_queues[session_id] = Queue() - # 等待响应,最多等待30秒 - try: - response = response_queue.get(timeout=120) - return json.dumps({"status": "success", "reply": response["content"]}) - except Empty: - return json.dumps({"status": "error", "message": "Response timeout"}) + # 创建消息对象 + msg = WebMessage(self._generate_msg_id(), prompt) + msg.from_user_id = session_id # 使用会话ID作为用户ID + + # 创建上下文 + context = self._compose_context(ContextType.TEXT, prompt, msg=msg) + + # 添加必要的字段 + context["session_id"] = session_id + context["request_id"] = request_id + context["isgroup"] = False # 添加 isgroup 字段 + context["receiver"] = session_id # 添加 receiver 字段 + + # 异步处理消息 - 只传递上下文 + threading.Thread(target=self.produce, args=(context,)).start() + + # 返回请求ID + return json.dumps({"status": "success", "request_id": request_id}) except Exception as e: logger.error(f"Error processing message: {e}") - return json.dumps({"status": "error", "message": "Internal server error"}) + return json.dumps({"status": "error", "message": str(e)}) + + def poll_response(self): + """ + Poll for responses using the session_id. + """ + try: + # 不记录轮询请求的日志 + web.ctx.log_request = False + + data = web.data() + json_data = json.loads(data) + session_id = json_data.get('session_id') + + if not session_id or session_id not in self.session_queues: + return json.dumps({"status": "error", "message": "Invalid session ID"}) + + # 尝试从队列获取响应,不等待 + try: + # 使用peek而不是get,这样如果前端没有成功处理,下次还能获取到 + response = self.session_queues[session_id].get(block=False) + + # 返回响应,包含请求ID以区分不同请求 + return json.dumps({ + "status": "success", + "has_content": True, + "content": response["content"], + "request_id": response["request_id"], + "timestamp": response["timestamp"] + }) + + except Empty: + # 没有新响应 + return json.dumps({"status": "success", "has_content": False}) + + except Exception as e: + logger.error(f"Error polling response: {e}") + return json.dumps({"status": "error", "message": str(e)}) def chat_page(self): """Serve the chat HTML page.""" @@ -153,6 +197,7 @@ class WebChannel(ChatChannel): urls = ( '/', 'RootHandler', # 添加根路径处理器 '/message', 'MessageHandler', + '/poll', 'PollHandler', # 添加轮询处理器 '/chat', 'ChatHandler', '/assets/(.*)', 'AssetsHandler', # 匹配 /assets/任何路径 ) @@ -163,6 +208,12 @@ class WebChannel(ChatChannel): import io from contextlib import redirect_stdout + # 配置web.py的日志级别为ERROR,只显示错误 + logging.getLogger("web").setLevel(logging.ERROR) + + # 禁用web.httpserver的日志 + logging.getLogger("web.httpserver").setLevel(logging.ERROR) + # 临时重定向标准输出,捕获web.py的启动消息 with redirect_stdout(io.StringIO()): web.httpserver.runsimple(app.wsgifunc(), ("0.0.0.0", port)) @@ -179,6 +230,11 @@ class MessageHandler: return WebChannel().post_message() +class PollHandler: + def POST(self): + return WebChannel().poll_response() + + class ChatHandler: def GET(self): # 正常返回聊天页面 diff --git a/config-template.json b/config-template.json index d0268d3b..476a5e0c 100644 --- a/config-template.json +++ b/config-template.json @@ -1,5 +1,5 @@ { - "channel_type": "wx", + "channel_type": "web", "model": "", "open_ai_api_key": "YOUR API KEY", "claude_api_key": "YOUR API KEY", diff --git a/simple_login_form_test.html b/simple_login_form_test.html new file mode 100644 index 00000000..286a008a --- /dev/null +++ b/simple_login_form_test.html @@ -0,0 +1,278 @@ + + + + + +登录 + + + +
+

用户登录

+
+ + +
+ + +
+ + +
+
+ + +
+ 忘记密码? +
+
+
+
+ +