mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-07-20 13:47:15 +08:00
feat: web channel support multiple message and picture display
This commit is contained in:
@@ -170,6 +170,24 @@
|
|||||||
flex: 1;
|
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 {
|
#messages {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
@@ -210,7 +228,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.user-container .avatar {
|
.user-container .avatar {
|
||||||
margin-left: 15px;
|
margin-left: 20px;
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,6 +237,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.user-container .message {
|
.user-container .message {
|
||||||
|
padding: 13px 16px;
|
||||||
background-color: var(--bot-msg-bg);
|
background-color: var(--bot-msg-bg);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
@@ -266,7 +285,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.message {
|
.message {
|
||||||
padding: 12px 16px;
|
padding: 5px 16px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
@@ -400,7 +419,7 @@
|
|||||||
.message img {
|
.message img {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
margin: 1em 0;
|
margin: 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timestamp {
|
.timestamp {
|
||||||
@@ -548,10 +567,12 @@
|
|||||||
left: -260px;
|
left: -260px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
|
transition: left 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar.active {
|
#sidebar.active {
|
||||||
left: 0;
|
left: 0;
|
||||||
|
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
#menu-toggle {
|
#menu-toggle {
|
||||||
@@ -577,6 +598,23 @@
|
|||||||
#header-logo {
|
#header-logo {
|
||||||
height: 24px;
|
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 */
|
/* Dark mode support */
|
||||||
@@ -717,7 +755,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="sidebar-overlay"></div>
|
||||||
<div id="main-content">
|
<div id="main-content">
|
||||||
<div id="chat-header">
|
<div id="chat-header">
|
||||||
<button id="menu-toggle">
|
<button id="menu-toggle">
|
||||||
@@ -725,6 +763,9 @@
|
|||||||
</button>
|
</button>
|
||||||
<img id="header-logo" src="assets/logo.jpg" alt="AI Assistant Logo">
|
<img id="header-logo" src="assets/logo.jpg" alt="AI Assistant Logo">
|
||||||
<div id="chat-title">AI 助手</div>
|
<div id="chat-title">AI 助手</div>
|
||||||
|
<a id="github-link" href="https://github.com/zhayujie/chatgpt-on-wechat" target="_blank" rel="noopener noreferrer">
|
||||||
|
<img id="github-icon" src="assets/github.png" alt="GitHub">
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="messages">
|
<div id="messages">
|
||||||
@@ -778,10 +819,17 @@
|
|||||||
const newChatButton = document.getElementById('new-chat');
|
const newChatButton = document.getElementById('new-chat');
|
||||||
const chatHistory = document.getElementById('chat-history');
|
const chatHistory = document.getElementById('chat-history');
|
||||||
|
|
||||||
// 简化变量,只保留用户ID
|
// 生成新的会话ID
|
||||||
let userId = 'user_' + Math.random().toString(36).substring(2, 10);
|
function generateSessionId() {
|
||||||
let currentSessionId = 'default_session'; // 使用固定会话ID
|
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;
|
let isComposing = false;
|
||||||
|
|
||||||
@@ -815,15 +863,18 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 处理菜单切换
|
// 处理菜单切换
|
||||||
menuToggle.addEventListener('click', function() {
|
menuToggle.addEventListener('click', function(event) {
|
||||||
|
event.stopPropagation(); // 防止事件冒泡到 main-content
|
||||||
sidebar.classList.toggle('active');
|
sidebar.classList.toggle('active');
|
||||||
});
|
});
|
||||||
|
|
||||||
// 处理新对话按钮 - 创建新的用户ID和清空当前对话
|
// 处理新对话按钮 - 创建新的会话ID和清空当前对话
|
||||||
newChatButton.addEventListener('click', function() {
|
newChatButton.addEventListener('click', function() {
|
||||||
// 生成新的用户ID
|
// 生成新的会话ID
|
||||||
userId = 'user_' + Math.random().toString(36).substring(2, 10);
|
sessionId = generateSessionId();
|
||||||
console.log('New conversation started with user ID:', userId);
|
// 将新的会话ID保存到全局变量,供轮询函数使用
|
||||||
|
window.sessionId = sessionId;
|
||||||
|
console.log('New conversation started with new session ID:', sessionId);
|
||||||
|
|
||||||
// 清空聊天记录
|
// 清空聊天记录
|
||||||
clearChat();
|
clearChat();
|
||||||
@@ -881,27 +932,42 @@
|
|||||||
input.style.height = '52px';
|
input.style.height = '52px';
|
||||||
sendButton.disabled = true;
|
sendButton.disabled = true;
|
||||||
|
|
||||||
// 发送到服务器并等待响应
|
// 使用当前的全局会话ID
|
||||||
|
const currentSessionId = window.sessionId || sessionId;
|
||||||
|
|
||||||
|
// 发送到服务器并获取请求ID
|
||||||
axios({
|
axios({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
url: '/message',
|
url: '/message',
|
||||||
data: {
|
data: {
|
||||||
user_id: userId,
|
session_id: currentSessionId, // 使用最新的会话ID
|
||||||
message: userMessage,
|
message: userMessage,
|
||||||
timestamp: timestamp.toISOString(),
|
timestamp: timestamp.toISOString()
|
||||||
session_id: currentSessionId
|
|
||||||
},
|
},
|
||||||
timeout: 120000 // 120秒超时
|
timeout: 10000 // 10秒超时
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
// 移除加载消息
|
if (response.data.status === "success") {
|
||||||
if (loadingContainer.parentNode) {
|
// 保存当前请求ID,用于识别响应
|
||||||
messagesDiv.removeChild(loadingContainer);
|
const currentRequestId = response.data.request_id;
|
||||||
}
|
|
||||||
|
// 如果还没有开始轮询,则开始轮询
|
||||||
// 添加AI回复
|
if (!window.isPolling) {
|
||||||
if (response.data.reply) {
|
startPolling(currentSessionId);
|
||||||
addBotMessage(response.data.reply, new Date());
|
}
|
||||||
|
|
||||||
|
// 将请求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 => {
|
.catch(error => {
|
||||||
@@ -920,178 +986,108 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加加载中的消息
|
// 修改轮询函数,确保正确处理多条回复
|
||||||
function addLoadingMessage() {
|
function startPolling(sessionId) {
|
||||||
const botContainer = document.createElement('div');
|
if (window.isPolling) return;
|
||||||
botContainer.className = 'bot-container loading-container';
|
|
||||||
|
|
||||||
const messageContainer = document.createElement('div');
|
window.isPolling = true;
|
||||||
messageContainer.className = 'message-container';
|
console.log('Starting polling with session ID:', sessionId);
|
||||||
|
|
||||||
messageContainer.innerHTML = `
|
function poll() {
|
||||||
<div class="avatar bot-avatar">
|
if (!window.isPolling) return;
|
||||||
<i class="fas fa-robot"></i>
|
|
||||||
</div>
|
// 如果页面已关闭或导航离开,停止轮询
|
||||||
<div class="message-content">
|
if (document.hidden) {
|
||||||
<div class="message">
|
setTimeout(poll, 5000); // 页面不可见时降低轮询频率
|
||||||
<div class="typing-indicator">
|
return;
|
||||||
<span></span>
|
}
|
||||||
<span></span>
|
|
||||||
<span></span>
|
// 使用当前的会话ID,而不是闭包中的sessionId
|
||||||
</div>
|
const currentSessionId = window.sessionId || sessionId;
|
||||||
</div>
|
|
||||||
</div>
|
axios({
|
||||||
`;
|
method: 'post',
|
||||||
|
url: '/poll',
|
||||||
botContainer.appendChild(messageContainer);
|
data: {
|
||||||
messagesDiv.appendChild(botContainer);
|
session_id: currentSessionId
|
||||||
scrollToBottom();
|
},
|
||||||
|
timeout: 5000
|
||||||
return botContainer;
|
})
|
||||||
}
|
.then(response => {
|
||||||
|
if (response.data.status === "success") {
|
||||||
// 替换 formatMessage 函数,使用 markdown-it 替代 marked
|
if (response.data.has_content) {
|
||||||
function formatMessage(content) {
|
console.log('Received response:', response.data);
|
||||||
try {
|
|
||||||
// 初始化 markdown-it 实例
|
// 获取请求ID和内容
|
||||||
const md = window.markdownit({
|
const requestId = response.data.request_id;
|
||||||
html: false, // 禁用 HTML 标签
|
const content = response.data.content;
|
||||||
xhtmlOut: false, // 使用 '/' 关闭单标签
|
const timestamp = new Date(response.data.timestamp * 1000);
|
||||||
breaks: true, // 将换行符转换为 <br>
|
|
||||||
linkify: true, // 自动将 URL 转换为链接
|
// 检查是否有对应的加载容器
|
||||||
typographer: true, // 启用一些语言中性的替换和引号美化
|
if (window.loadingContainers && window.loadingContainers[requestId]) {
|
||||||
highlight: function(str, lang) {
|
// 移除加载容器
|
||||||
if (lang && hljs.getLanguage(lang)) {
|
const loadingContainer = window.loadingContainers[requestId];
|
||||||
try {
|
if (loadingContainer && loadingContainer.parentNode) {
|
||||||
return hljs.highlight(str, { language: lang }).value;
|
messagesDiv.removeChild(loadingContainer);
|
||||||
} catch (e) {
|
}
|
||||||
console.error('Error highlighting code:', e);
|
|
||||||
|
// 删除已处理的加载容器引用
|
||||||
|
delete window.loadingContainers[requestId];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 始终创建新的消息,无论是否是同一个请求的后续回复
|
||||||
|
addBotMessage(content, timestamp, requestId);
|
||||||
|
|
||||||
|
// 滚动到底部
|
||||||
|
scrollToBottom();
|
||||||
}
|
}
|
||||||
return hljs.highlightAuto(str).value;
|
|
||||||
}
|
// 继续轮询,使用原来的2秒间隔
|
||||||
});
|
setTimeout(poll, 2000);
|
||||||
|
|
||||||
// 渲染 Markdown
|
|
||||||
return md.render(content);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error parsing markdown:', e);
|
|
||||||
// 如果解析失败,至少确保换行符正确显示
|
|
||||||
return content.replace(/\n/g, '<br>');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新 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 {
|
} 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({
|
poll();
|
||||||
role: 'user',
|
|
||||||
content: content,
|
|
||||||
timestamp: timestamp.getTime()
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加机器人消息的函数 (保存到localStorage)
|
// 添加机器人消息的函数 (保存到localStorage),增加requestId参数
|
||||||
function addBotMessage(content, timestamp) {
|
function addBotMessage(content, timestamp, requestId) {
|
||||||
// 显示消息
|
// 显示消息
|
||||||
displayBotMessage(content, timestamp);
|
displayBotMessage(content, timestamp, requestId);
|
||||||
|
|
||||||
// 保存到localStorage
|
// 保存到localStorage
|
||||||
saveMessageToLocalStorage({
|
saveMessageToLocalStorage({
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
content: content,
|
content: content,
|
||||||
timestamp: timestamp.getTime()
|
timestamp: timestamp.getTime(),
|
||||||
|
requestId: requestId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 只显示用户消息而不保存到localStorage
|
// 修改显示机器人消息的函数,增加requestId参数
|
||||||
function displayUserMessage(content, timestamp) {
|
function displayBotMessage(content, timestamp, requestId) {
|
||||||
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 = `<p>${content.replace(/\n/g, '<br>')}</p>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
messageContainer.innerHTML = `
|
|
||||||
<div class="avatar user-avatar">
|
|
||||||
<i class="fas fa-user"></i>
|
|
||||||
</div>
|
|
||||||
<div class="message-content">
|
|
||||||
<div class="message">${formattedContent}</div>
|
|
||||||
<div class="timestamp">${formatTimestamp(timestamp)}</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
userContainer.appendChild(messageContainer);
|
|
||||||
messagesDiv.appendChild(userContainer);
|
|
||||||
|
|
||||||
// 应用代码高亮
|
|
||||||
setTimeout(() => {
|
|
||||||
applyHighlighting();
|
|
||||||
}, 0);
|
|
||||||
|
|
||||||
scrollToBottom();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 只显示机器人消息而不保存到localStorage
|
|
||||||
function displayBotMessage(content, timestamp) {
|
|
||||||
const botContainer = document.createElement('div');
|
const botContainer = document.createElement('div');
|
||||||
botContainer.className = 'bot-container';
|
botContainer.className = 'bot-container';
|
||||||
|
|
||||||
|
// 如果有requestId,将其存储在数据属性中
|
||||||
|
if (requestId) {
|
||||||
|
botContainer.dataset.requestId = requestId;
|
||||||
|
}
|
||||||
|
|
||||||
const messageContainer = document.createElement('div');
|
const messageContainer = document.createElement('div');
|
||||||
messageContainer.className = 'message-container';
|
messageContainer.className = 'message-container';
|
||||||
|
|
||||||
// 确保时间戳是有效的 Date 对象
|
|
||||||
if (!(timestamp instanceof Date) || isNaN(timestamp)) {
|
|
||||||
timestamp = new Date();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 安全地格式化消息
|
// 安全地格式化消息
|
||||||
let formattedContent;
|
let formattedContent;
|
||||||
try {
|
try {
|
||||||
@@ -1114,45 +1110,82 @@
|
|||||||
botContainer.appendChild(messageContainer);
|
botContainer.appendChild(messageContainer);
|
||||||
messagesDiv.appendChild(botContainer);
|
messagesDiv.appendChild(botContainer);
|
||||||
|
|
||||||
// 使用setTimeout确保DOM已更新,并延长等待时间
|
// 应用代码高亮
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
try {
|
applyHighlighting();
|
||||||
// 直接对新添加的消息应用高亮
|
}, 0);
|
||||||
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完全更新
|
|
||||||
|
|
||||||
scrollToBottom();
|
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 = `<p>${content.replace(/\n/g, '<br>')}</p>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
messageContainer.innerHTML = `
|
||||||
|
<div class="avatar bot-avatar">
|
||||||
|
<i class="fas fa-robot"></i>
|
||||||
|
</div>
|
||||||
|
<div class="message-content">
|
||||||
|
<div class="message">${formattedContent}</div>
|
||||||
|
<div class="timestamp">${formatTimestamp(timestamp)}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
botContainer.appendChild(messageContainer);
|
||||||
|
messagesDiv.appendChild(botContainer);
|
||||||
|
|
||||||
|
// 应用代码高亮
|
||||||
|
setTimeout(() => {
|
||||||
|
applyHighlighting();
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
scrollToBottom();
|
||||||
|
|
||||||
|
return botContainer;
|
||||||
|
}
|
||||||
|
|
||||||
// 格式化时间戳
|
// 格式化时间戳
|
||||||
function formatTimestamp(date) {
|
function formatTimestamp(date) {
|
||||||
return date.toLocaleTimeString();
|
return date.toLocaleTimeString();
|
||||||
@@ -1223,8 +1256,8 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// 清空localStorage中的消息 - 使用用户ID作为键
|
// 清空localStorage中的消息 - 使用会话ID作为键
|
||||||
localStorage.setItem(`chatMessages_${userId}`, JSON.stringify([]));
|
localStorage.setItem(`chatMessages_${sessionId}`, JSON.stringify([]));
|
||||||
|
|
||||||
// 在移动设备上关闭侧边栏
|
// 在移动设备上关闭侧边栏
|
||||||
if (window.innerWidth <= 768) {
|
if (window.innerWidth <= 768) {
|
||||||
@@ -1232,26 +1265,242 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从localStorage加载消息 - 使用用户ID作为键
|
// 从localStorage加载消息 - 使用会话ID作为键
|
||||||
function loadMessagesFromLocalStorage() {
|
function loadMessagesFromLocalStorage() {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(localStorage.getItem(`chatMessages_${userId}`) || '[]');
|
return JSON.parse(localStorage.getItem(`chatMessages_${sessionId}`) || '[]');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading messages from localStorage:', error);
|
console.error('Error loading messages from localStorage:', error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存消息到localStorage - 使用用户ID作为键
|
// 保存消息到localStorage - 使用会话ID作为键
|
||||||
function saveMessageToLocalStorage(message) {
|
function saveMessageToLocalStorage(message) {
|
||||||
try {
|
try {
|
||||||
const messages = loadMessagesFromLocalStorage();
|
const messages = loadMessagesFromLocalStorage();
|
||||||
messages.push(message);
|
messages.push(message);
|
||||||
localStorage.setItem(`chatMessages_${userId}`, JSON.stringify(messages));
|
localStorage.setItem(`chatMessages_${sessionId}`, JSON.stringify(messages));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error saving message to localStorage:', 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 = `<p>${content.replace(/\n/g, '<br>')}</p>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
messageContainer.innerHTML = `
|
||||||
|
<div class="avatar user-avatar">
|
||||||
|
<i class="fas fa-user"></i>
|
||||||
|
</div>
|
||||||
|
<div class="message-content">
|
||||||
|
<div class="message">${formattedContent}</div>
|
||||||
|
<div class="timestamp">${formatTimestamp(timestamp)}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
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 = `
|
||||||
|
<div class="avatar bot-avatar">
|
||||||
|
<i class="fas fa-robot"></i>
|
||||||
|
</div>
|
||||||
|
<div class="message-content">
|
||||||
|
<div class="message">
|
||||||
|
<div class="typing-indicator">
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
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, // 将换行符转换为 <br>
|
||||||
|
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 `<img src="${text}" alt="Image" style="max-width: 100%; height: auto;" />`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用默认渲染
|
||||||
|
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, '<br>');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新 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');
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
BIN
channel/web/static/github.png
Normal file
BIN
channel/web/static/github.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.4 KiB |
@@ -2,6 +2,7 @@ import sys
|
|||||||
import time
|
import time
|
||||||
import web
|
import web
|
||||||
import json
|
import json
|
||||||
|
import uuid
|
||||||
from queue import Queue, Empty
|
from queue import Queue, Empty
|
||||||
from bridge.context import *
|
from bridge.context import *
|
||||||
from bridge.reply import Reply, ReplyType
|
from bridge.reply import Reply, ReplyType
|
||||||
@@ -12,6 +13,8 @@ from common.singleton import singleton
|
|||||||
from config import conf
|
from config import conf
|
||||||
import os
|
import os
|
||||||
import mimetypes # 添加这行来处理MIME类型
|
import mimetypes # 添加这行来处理MIME类型
|
||||||
|
import threading
|
||||||
|
import logging
|
||||||
|
|
||||||
class WebMessage(ChatMessage):
|
class WebMessage(ChatMessage):
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -43,39 +46,54 @@ class WebChannel(ChatChannel):
|
|||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.message_queues = {} # 为每个用户存储一个消息队列
|
|
||||||
self.msg_id_counter = 0 # 添加消息ID计数器
|
self.msg_id_counter = 0 # 添加消息ID计数器
|
||||||
|
self.session_queues = {} # 存储session_id到队列的映射
|
||||||
|
self.request_to_session = {} # 存储request_id到session_id的映射
|
||||||
|
|
||||||
def _generate_msg_id(self):
|
def _generate_msg_id(self):
|
||||||
"""生成唯一的消息ID"""
|
"""生成唯一的消息ID"""
|
||||||
self.msg_id_counter += 1
|
self.msg_id_counter += 1
|
||||||
return str(int(time.time())) + str(self.msg_id_counter)
|
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):
|
def send(self, reply: Reply, context: Context):
|
||||||
try:
|
try:
|
||||||
if reply.type in self.NOT_SUPPORT_REPLYTYPE:
|
if reply.type in self.NOT_SUPPORT_REPLYTYPE:
|
||||||
logger.warning(f"Web channel doesn't support {reply.type} yet")
|
logger.warning(f"Web channel doesn't support {reply.type} yet")
|
||||||
return
|
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
|
# 通过request_id获取session_id
|
||||||
user_id = context.get("receiver", None)
|
session_id = self.request_to_session.get(request_id)
|
||||||
if not user_id:
|
if not session_id:
|
||||||
logger.error("No receiver found in context, cannot send message")
|
logger.error(f"No session_id found for request {request_id}")
|
||||||
return
|
return
|
||||||
|
|
||||||
# 检查是否有响应队列
|
# 检查是否有会话队列
|
||||||
response_queue = context.get("response_queue", None)
|
if session_id in self.session_queues:
|
||||||
if response_queue:
|
# 创建响应数据,包含请求ID以区分不同请求的响应
|
||||||
# 直接将响应放入队列
|
|
||||||
response_data = {
|
response_data = {
|
||||||
"type": str(reply.type),
|
"type": str(reply.type),
|
||||||
"content": reply.content,
|
"content": reply.content,
|
||||||
"timestamp": time.time()
|
"timestamp": time.time(),
|
||||||
|
"request_id": request_id
|
||||||
}
|
}
|
||||||
response_queue.put(response_data)
|
self.session_queues[session_id].put(response_data)
|
||||||
logger.debug(f"Response sent to queue for user {user_id}")
|
logger.debug(f"Response sent to queue for session {session_id}, request {request_id}")
|
||||||
else:
|
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:
|
except Exception as e:
|
||||||
logger.error(f"Error in send method: {e}")
|
logger.error(f"Error in send method: {e}")
|
||||||
@@ -83,57 +101,83 @@ class WebChannel(ChatChannel):
|
|||||||
def post_message(self):
|
def post_message(self):
|
||||||
"""
|
"""
|
||||||
Handle incoming messages from users via POST request.
|
Handle incoming messages from users via POST request.
|
||||||
|
Returns a request_id for tracking this specific request.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
data = web.data() # 获取原始POST数据
|
data = web.data() # 获取原始POST数据
|
||||||
json_data = json.loads(data)
|
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())}')
|
session_id = json_data.get('session_id', f'session_{int(time.time())}')
|
||||||
except json.JSONDecodeError:
|
prompt = json_data.get('message', '')
|
||||||
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"})
|
|
||||||
|
|
||||||
try:
|
# 生成请求ID
|
||||||
msg_id = self._generate_msg_id()
|
request_id = self._generate_request_id()
|
||||||
web_message = WebMessage(
|
|
||||||
msg_id=msg_id,
|
|
||||||
content=prompt,
|
|
||||||
from_user_id=user_id,
|
|
||||||
to_user_id="Chatgpt",
|
|
||||||
other_user_id=user_id
|
|
||||||
)
|
|
||||||
|
|
||||||
context = self._compose_context(ContextType.TEXT, prompt, msg=web_message)
|
# 将请求ID与会话ID关联
|
||||||
if not context:
|
self.request_to_session[request_id] = session_id
|
||||||
return json.dumps({"status": "error", "message": "Failed to process message"})
|
|
||||||
|
|
||||||
# 创建一个响应队列
|
|
||||||
response_queue = Queue()
|
|
||||||
|
|
||||||
# 确保上下文包含必要的信息
|
# 确保会话队列存在
|
||||||
context["isgroup"] = False
|
if session_id not in self.session_queues:
|
||||||
context["receiver"] = user_id
|
self.session_queues[session_id] = Queue()
|
||||||
context["session_id"] = user_id
|
|
||||||
context["response_queue"] = response_queue
|
|
||||||
|
|
||||||
# 发送消息到处理队列
|
|
||||||
self.produce(context)
|
|
||||||
|
|
||||||
# 等待响应,最多等待30秒
|
# 创建消息对象
|
||||||
try:
|
msg = WebMessage(self._generate_msg_id(), prompt)
|
||||||
response = response_queue.get(timeout=120)
|
msg.from_user_id = session_id # 使用会话ID作为用户ID
|
||||||
return json.dumps({"status": "success", "reply": response["content"]})
|
|
||||||
except Empty:
|
# 创建上下文
|
||||||
return json.dumps({"status": "error", "message": "Response timeout"})
|
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:
|
except Exception as e:
|
||||||
logger.error(f"Error processing message: {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):
|
def chat_page(self):
|
||||||
"""Serve the chat HTML page."""
|
"""Serve the chat HTML page."""
|
||||||
@@ -153,6 +197,7 @@ class WebChannel(ChatChannel):
|
|||||||
urls = (
|
urls = (
|
||||||
'/', 'RootHandler', # 添加根路径处理器
|
'/', 'RootHandler', # 添加根路径处理器
|
||||||
'/message', 'MessageHandler',
|
'/message', 'MessageHandler',
|
||||||
|
'/poll', 'PollHandler', # 添加轮询处理器
|
||||||
'/chat', 'ChatHandler',
|
'/chat', 'ChatHandler',
|
||||||
'/assets/(.*)', 'AssetsHandler', # 匹配 /assets/任何路径
|
'/assets/(.*)', 'AssetsHandler', # 匹配 /assets/任何路径
|
||||||
)
|
)
|
||||||
@@ -163,6 +208,12 @@ class WebChannel(ChatChannel):
|
|||||||
import io
|
import io
|
||||||
from contextlib import redirect_stdout
|
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的启动消息
|
# 临时重定向标准输出,捕获web.py的启动消息
|
||||||
with redirect_stdout(io.StringIO()):
|
with redirect_stdout(io.StringIO()):
|
||||||
web.httpserver.runsimple(app.wsgifunc(), ("0.0.0.0", port))
|
web.httpserver.runsimple(app.wsgifunc(), ("0.0.0.0", port))
|
||||||
@@ -179,6 +230,11 @@ class MessageHandler:
|
|||||||
return WebChannel().post_message()
|
return WebChannel().post_message()
|
||||||
|
|
||||||
|
|
||||||
|
class PollHandler:
|
||||||
|
def POST(self):
|
||||||
|
return WebChannel().poll_response()
|
||||||
|
|
||||||
|
|
||||||
class ChatHandler:
|
class ChatHandler:
|
||||||
def GET(self):
|
def GET(self):
|
||||||
# 正常返回聊天页面
|
# 正常返回聊天页面
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"channel_type": "wx",
|
"channel_type": "web",
|
||||||
"model": "",
|
"model": "",
|
||||||
"open_ai_api_key": "YOUR API KEY",
|
"open_ai_api_key": "YOUR API KEY",
|
||||||
"claude_api_key": "YOUR API KEY",
|
"claude_api_key": "YOUR API KEY",
|
||||||
|
|||||||
278
simple_login_form_test.html
Normal file
278
simple_login_form_test.html
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>登录</title>
|
||||||
|
<style>
|
||||||
|
/* Reset and base */
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
body, html {
|
||||||
|
margin: 0; padding: 0; height: 100%;
|
||||||
|
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.login-container {
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
padding: 2.5rem 3rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 8px 24px rgba(0,0,0,0.15);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
color: #333;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 0.4rem;
|
||||||
|
color: #444;
|
||||||
|
}
|
||||||
|
input[type="text"],
|
||||||
|
input[type="email"],
|
||||||
|
input[type="password"] {
|
||||||
|
padding: 0.6rem 0.8rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
border: 1.8px solid #ccc;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: border-color 0.3s ease;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
input[type="text"]:focus,
|
||||||
|
input[type="email"]:focus,
|
||||||
|
input[type="password"]:focus {
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
.password-wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.toggle-password {
|
||||||
|
position: absolute;
|
||||||
|
right: 0.8rem;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
color: #667eea;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.login-button {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
background-color: #667eea;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
.login-button:disabled {
|
||||||
|
background-color: #a3a9f7;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
.forgot-password {
|
||||||
|
margin-top: 1rem;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.forgot-password a {
|
||||||
|
color: #667eea;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
.forgot-password a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.error-message {
|
||||||
|
margin-top: 1rem;
|
||||||
|
color: #d93025;
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.loading-spinner {
|
||||||
|
border: 3px solid #f3f3f3;
|
||||||
|
border-top: 3px solid #667eea;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg);}
|
||||||
|
100% { transform: rotate(360deg);}
|
||||||
|
}
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.login-container {
|
||||||
|
margin: 1rem;
|
||||||
|
padding: 2rem 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="login-container" role="main" aria-label="登录表单">
|
||||||
|
<h2>用户登录</h2>
|
||||||
|
<form id="loginForm" novalidate>
|
||||||
|
<label for="usernameEmail">用户名或邮箱</label>
|
||||||
|
<input type="text" id="usernameEmail" name="usernameEmail" autocomplete="username" placeholder="请输入用户名或邮箱" required aria-describedby="usernameEmailError" />
|
||||||
|
<div id="usernameEmailError" class="error-message" aria-live="polite"></div>
|
||||||
|
|
||||||
|
<label for="password" style="margin-top:1rem;">密码</label>
|
||||||
|
<div class="password-wrapper">
|
||||||
|
<input type="password" id="password" name="password" autocomplete="current-password" placeholder="请输入密码" required minlength="6" aria-describedby="passwordError" />
|
||||||
|
<button type="button" class="toggle-password" aria-label="切换密码可见性" title="切换密码可见性">👁️</button>
|
||||||
|
</div>
|
||||||
|
<div id="passwordError" class="error-message" aria-live="polite"></div>
|
||||||
|
|
||||||
|
<button type="submit" id="loginButton" class="login-button" disabled>登录</button>
|
||||||
|
<div class="forgot-password">
|
||||||
|
<a href="/forgot-password.html" target="_blank" rel="noopener noreferrer">忘记密码?</a>
|
||||||
|
</div>
|
||||||
|
<div id="submitError" class="error-message" aria-live="polite"></div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function(){
|
||||||
|
const usernameEmailInput = document.getElementById('usernameEmail');
|
||||||
|
const passwordInput = document.getElementById('password');
|
||||||
|
const loginButton = document.getElementById('loginButton');
|
||||||
|
const usernameEmailError = document.getElementById('usernameEmailError');
|
||||||
|
const passwordError = document.getElementById('passwordError');
|
||||||
|
const submitError = document.getElementById('submitError');
|
||||||
|
const togglePasswordBtn = document.querySelector('.toggle-password');
|
||||||
|
const form = document.getElementById('loginForm');
|
||||||
|
|
||||||
|
// 校验用户名或邮箱格式
|
||||||
|
function validateUsernameEmail(value) {
|
||||||
|
if (!value.trim()) {
|
||||||
|
return "用户名或邮箱不能为空";
|
||||||
|
}
|
||||||
|
// 简单邮箱正则
|
||||||
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
// 用户名规则:允许字母数字下划线,长度3-20
|
||||||
|
const usernameRegex = /^[a-zA-Z0-9_]{3,20}$/;
|
||||||
|
if (emailRegex.test(value)) {
|
||||||
|
return "";
|
||||||
|
} else if (usernameRegex.test(value)) {
|
||||||
|
return "";
|
||||||
|
} else {
|
||||||
|
return "请输入有效的用户名或邮箱格式";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验密码格式
|
||||||
|
function validatePassword(value) {
|
||||||
|
if (!value) {
|
||||||
|
return "密码不能为空";
|
||||||
|
}
|
||||||
|
if (value.length < 6) {
|
||||||
|
return "密码长度不能少于6位";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 实时校验并更新错误提示和按钮状态
|
||||||
|
function validateForm() {
|
||||||
|
const usernameEmailVal = usernameEmailInput.value;
|
||||||
|
const passwordVal = passwordInput.value;
|
||||||
|
|
||||||
|
const usernameEmailErrMsg = validateUsernameEmail(usernameEmailVal);
|
||||||
|
const passwordErrMsg = validatePassword(passwordVal);
|
||||||
|
|
||||||
|
usernameEmailError.textContent = usernameEmailErrMsg;
|
||||||
|
passwordError.textContent = passwordErrMsg;
|
||||||
|
submitError.textContent = "";
|
||||||
|
|
||||||
|
const isValid = !usernameEmailErrMsg && !passwordErrMsg;
|
||||||
|
loginButton.disabled = !isValid;
|
||||||
|
return isValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 密码可见切换
|
||||||
|
togglePasswordBtn.addEventListener('click', () => {
|
||||||
|
if (passwordInput.type === 'password') {
|
||||||
|
passwordInput.type = 'text';
|
||||||
|
togglePasswordBtn.textContent = '🙈';
|
||||||
|
togglePasswordBtn.setAttribute('aria-label', '隐藏密码');
|
||||||
|
togglePasswordBtn.setAttribute('title', '隐藏密码');
|
||||||
|
} else {
|
||||||
|
passwordInput.type = 'password';
|
||||||
|
togglePasswordBtn.textContent = '👁️';
|
||||||
|
togglePasswordBtn.setAttribute('aria-label', '显示密码');
|
||||||
|
togglePasswordBtn.setAttribute('title', '显示密码');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听输入事件实时校验
|
||||||
|
usernameEmailInput.addEventListener('input', validateForm);
|
||||||
|
passwordInput.addEventListener('input', validateForm);
|
||||||
|
|
||||||
|
// 模拟登录请求
|
||||||
|
function fakeLoginRequest(data) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
// 模拟用户名/邮箱为 "user" 或 "user@example.com" 且密码为 "password123" 才成功
|
||||||
|
const validUsers = ["user", "user@example.com"];
|
||||||
|
if (validUsers.includes(data.usernameEmail.toLowerCase()) && data.password === "password123") {
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
reject(new Error("用户名或密码错误"));
|
||||||
|
}
|
||||||
|
}, 1500);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 表单提交处理
|
||||||
|
form.addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!validateForm()) return;
|
||||||
|
|
||||||
|
loginButton.disabled = true;
|
||||||
|
const originalText = loginButton.textContent;
|
||||||
|
loginButton.textContent = "登录中";
|
||||||
|
const spinner = document.createElement('span');
|
||||||
|
spinner.className = 'loading-spinner';
|
||||||
|
loginButton.appendChild(spinner);
|
||||||
|
submitError.textContent = "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fakeLoginRequest({
|
||||||
|
usernameEmail: usernameEmailInput.value.trim(),
|
||||||
|
password: passwordInput.value
|
||||||
|
});
|
||||||
|
// 登录成功跳转(此处用alert模拟)
|
||||||
|
alert("登录成功,跳转到用户主页");
|
||||||
|
// window.location.href = "/user-home.html"; // 实际跳转
|
||||||
|
} catch (err) {
|
||||||
|
submitError.textContent = err.message;
|
||||||
|
} finally {
|
||||||
|
loginButton.disabled = false;
|
||||||
|
loginButton.textContent = originalText;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 页面加载时校验一次,防止缓存值导致按钮状态异常
|
||||||
|
validateForm();
|
||||||
|
})();
|
||||||
|
<\/script>
|
||||||
|
<\/body>
|
||||||
|
<\/html>
|
||||||
Reference in New Issue
Block a user