Files
blackbird/aibox/entry1.html
T
qing 9db2bc112d fix(aibox/frontend): Batch M 波次3 前端功能修复(F1/F2/F3/F4/F6)
- F1: entry/frontend 清空按钮先 cloneNode 缓存 welcome-message 再清空,修 TypeError 中断
- F2: 三页面手动断开清除/作废重连定时器,杜绝连接复活(entry 新增 reconnectTimer 句柄)
- F3: 三页面连接前关闭 CONNECTING 中旧连接,防孤儿连接/双后端会话
- F4: 6 个 CDN CSS link 补 SRI integrity+crossorigin(sha384 实测计算)
- F6: frontend 重连文案与 3000*N 实际延迟对齐

三页面内联脚本 node --check 通过;测试 493 passed/4 failed(既有平台项)/1 skipped 零回归
2026-08-20 13:25:12 +08:00

900 lines
36 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>小猫AI · 智能喵语伙伴</title>
<!-- Font Awesome 6 (免费图标库) -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" integrity="sha384-3B6NwesSXE7YJlcLI9RpRqGf2p/EgVH8BgoKTaUrmKNDkHPStTQ3EyoYjCGXaOTS" crossorigin="anonymous">
<!-- highlight.js 代码高亮 + marked markdown解析 -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/github-dark.min.css" integrity="sha384-wH75j6z1lH97ZOpMOInqhgKzFkAInZPPSPlZpYKYTOqsaizPvhQZmAtLcPKXpLyH" crossorigin="anonymous">
<!-- J13(审计 2026-08-19):CDN 脚本补 SRI integrity,防供应链劫持 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/9.1.6/marked.min.js" integrity="sha384-odPBjvtXVM/5hOYIr3A1dB+flh0c3wAT3bSesIOqEGmyUA4JoKf/YTWy0XKOYAY7" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.min.js" integrity="sha384-g4mRvs7AO0/Ol5LxcGyz4Doe21pVhGNnC3EQw5shw+z+aXDN86HqUdwXWO+Gz2zI" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.2.4/purify.min.js" integrity="sha384-eEu5CTj3qGvu9PdJuS+YlkNi7d2XxQROAFYOr59zgObtlcux1ae1Il3u7jvdCSWu" crossorigin="anonymous"></script>
<!-- Socket.IO 客户端库 -->
<script src="https://cdn.socket.io/4.5.0/socket.io.min.js" integrity="sha384-7EyYLQZgWBi67fBtVxw60/OWl1kjsfrPFcaU0pp0nAh+i8FD068QogUvg85Ewy1k" crossorigin="anonymous"></script>
<style>
/* ========== 融合小猫风格 + WebSocket增强样式 ========== */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: "Microsoft YaHei", "Segoe UI", system-ui, -apple-system, 'Roboto', sans-serif;
}
html, body {
width: 100%;
height: 100%;
overflow: hidden;
}
/* 整体背景及容器 - 可爱猫咪氛围 */
body {
background: #1a1a2f;
background-image: url('/imgs/background.jpeg');
background-size: cover;
background-position: center center;
background-repeat: no-repeat;
background-attachment: fixed;
transition: background-image 0.3s ease, background-color 0.2s;
color: #f0ede8;
}
/* 主布局 flex,左侧聊天区,右侧猫咪头像+WS侧栏 */
.main-layout {
display: flex;
height: 100vh;
width: 100%;
position: relative;
}
/* 左侧内容区 (聊天框+输入区) */
.cat-chat-area {
flex: 1;
display: flex;
flex-direction: column;
position: relative;
background: rgba(0,0,0,0.2);
backdrop-filter: blur(2px);
min-width: 0;
}
/* 角色模型区域 - 右侧面板内 */
#roleArea {
width: 140px;
height: 140px;
margin: 0 auto 16px auto;
filter: drop-shadow(0 8px 16px rgba(0,0,0,0.4));
pointer-events: auto;
background: rgba(0,0,0,0.2);
border-radius: 50%;
backdrop-filter: blur(4px);
padding: 8px;
}
#roleArea img {
width: 100%;
height: 100%;
object-fit: contain;
border-radius: 50%;
transition: all 0.2s;
}
/* 聊天记录玻璃容器 */
.chat-box {
position: relative;
margin: 20px 20px 95px 20px;
background: rgba(0, 0, 0, 0.55);
backdrop-filter: blur(12px);
border-radius: 28px;
padding: 16px 20px;
overflow-y: auto;
height: calc(100vh - 130px);
scroll-behavior: smooth;
border: 1px solid rgba(255, 220, 180, 0.3);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
}
.chat-box div {
margin: 10px 0;
animation: fadeSlideUp 0.2s ease;
}
.user {
text-align: right;
color: #b9f6ca;
font-weight: 500;
background: rgba(60, 50, 80, 0.7);
display: inline-block;
width: auto;
max-width: 85%;
margin-left: auto;
padding: 8px 16px;
border-radius: 26px;
border-bottom-right-radius: 4px;
word-break: break-word;
white-space: pre-wrap;
}
.ai {
text-align: left;
color: #fff0dd;
background: rgba(30, 30, 45, 0.8);
width: auto;
max-width: 85%;
padding: 8px 16px;
border-radius: 26px;
border-bottom-left-radius: 4px;
word-break: break-word;
white-space: pre-wrap;
}
/* 系统/错误/任务消息样式 */
.system-msg, .error-msg, .task-msg {
padding: 6px 12px;
border-radius: 20px;
font-size: 13px;
background: rgba(0,0,0,0.5);
margin: 6px 0;
}
.task-separator {
margin: 12px 0 8px;
padding: 6px 12px;
background: #ff980033;
border-left: 3px solid #ff9800;
border-radius: 12px;
font-size: 12px;
display: flex;
align-items: center;
gap: 8px;
color: #ffcd94;
}
/* 输入框区域 - 支持多行文本 */
.input-area {
position: absolute;
bottom: 20px;
left: 20px;
right: 20px;
display: flex;
gap: 12px;
background: transparent;
z-index: 30;
align-items: flex-end;
}
.input-wrapper {
flex: 1;
background: rgba(20, 20, 35, 0.9);
backdrop-filter: blur(6px);
border-radius: 24px;
border: 1px solid rgba(255, 200, 150, 0.5);
transition: all 0.2s ease;
}
.input-wrapper:focus-within {
border-color: #ff9a5a;
box-shadow: 0 0 8px rgba(255, 154, 90, 0.3);
}
#msgInput {
width: 100%;
padding: 12px 18px;
background: transparent;
color: #fce8cf;
font-size: 14px;
outline: none;
border: none;
resize: none;
font-family: inherit;
line-height: 1.5;
max-height: 150px;
overflow-y: auto;
}
#msgInput::placeholder {
color: rgba(252, 232, 207, 0.6);
}
#msgInput::-webkit-scrollbar {
width: 4px;
}
#msgInput::-webkit-scrollbar-track {
background: rgba(255,255,255,0.1);
border-radius: 4px;
}
#msgInput::-webkit-scrollbar-thumb {
background: rgba(255, 154, 90, 0.5);
border-radius: 4px;
}
#sendBtn {
padding: 0 28px;
height: 48px;
border-radius: 48px;
border: none;
background: linear-gradient(135deg, #ff9a5a, #ff6f61);
color: white;
font-weight: bold;
cursor: pointer;
transition: 0.2s;
flex-shrink: 0;
font-size: 16px;
display: flex;
align-items: center;
gap: 6px;
}
#sendBtn:hover {
transform: scale(1.02);
box-shadow: 0 4px 12px rgba(255, 111, 97, 0.4);
}
#sendBtn:active {
transform: scale(0.98);
}
/* 右侧整合面板 (猫咪头像 + WS控制台) */
.right-panel {
width: 320px;
background: rgba(30, 30, 40, 0.85);
backdrop-filter: blur(16px);
border-left: 1px solid rgba(255,255,200,0.2);
display: flex;
flex-direction: column;
overflow-y: auto;
z-index: 100;
padding: 24px 16px;
align-items: center;
}
/* 猫咪头像容器 */
.avatar-container {
text-align: center;
margin-bottom: 20px;
}
.avatar-name {
margin-top: 8px;
font-size: 18px;
font-weight: bold;
color: #ffd4a3;
letter-spacing: 1px;
}
/* WebSocket连接区域 */
.ws-card {
background: rgba(26, 188, 156, 0.75);
margin: 12px 0;
border-radius: 24px;
padding: 16px;
backdrop-filter: blur(4px);
width: 100%;
}
.ws-title {
font-weight: bold;
font-size: 14px;
margin-bottom: 10px;
border-bottom: 1px dashed rgba(255,255,240,0.5);
display: flex;
align-items: center;
gap: 6px;
}
.ws-control {
display: flex;
gap: 8px;
margin: 10px 0;
}
.ws-url-input {
width: 100%;
padding: 8px;
background: #2d2d3a;
border: 1px solid #5a5a6e;
border-radius: 20px;
color: white;
font-size: 12px;
margin-bottom: 10px;
}
.btn-sm {
padding: 6px 12px;
border-radius: 24px;
background: #4CAF50;
border: none;
color: white;
cursor: pointer;
font-size: 12px;
flex: 1;
}
.btn-sm:disabled { opacity: 0.5; }
.status-dot {
width: 10px;
height: 10px;
border-radius: 50%;
display: inline-block;
margin-right: 6px;
}
.command-grid {
display: grid;
grid-template-columns: repeat(2,1fr);
gap: 6px;
margin-top: 8px;
}
.cmd-btn {
background: #3d3d4a;
padding: 6px;
border-radius: 20px;
font-size: 11px;
text-align: center;
cursor: pointer;
transition: 0.1s;
}
.cmd-btn:hover {
background: #5a5a6e;
transform: translateX(2px);
}
.stats-line {
font-size: 12px;
margin: 6px 0;
}
/* 流式消息游标动画 */
.streaming .message-text {
border-right: 2px solid #ffb347;
animation: blink 0.8s step-end infinite;
padding-right: 4px;
}
@keyframes blink { from,to { border-color: transparent; } 50% { border-color: #ffb347; } }
.markdown-body code { background: #1e1e2e; border-radius: 6px; }
.markdown-body pre { background: #0f0f1a; }
@keyframes fadeSlideUp {
from { opacity: 0; transform: translateY(8px);}
to { opacity: 1; transform: translateY(0);}
}
::-webkit-scrollbar { width: 5px; }
/* 更换背景按钮样式 */
.bg-btn {
background: rgba(255, 165, 100, 0.8);
border: none;
color: white;
padding: 8px 16px;
border-radius: 40px;
cursor: pointer;
font-size: 12px;
margin-top: 12px;
width: 100%;
transition: 0.2s;
}
.bg-btn:hover {
background: rgba(255, 140, 70, 1);
transform: scale(0.98);
}
/* 护眼模式开关样式 */
.eye-mode-btn {
background: rgba(100, 180, 120, 0.8);
border: none;
color: white;
padding: 8px 16px;
border-radius: 40px;
cursor: pointer;
font-size: 12px;
margin-top: 8px;
width: 100%;
transition: 0.2s;
}
.eye-mode-btn:hover {
background: rgba(80, 160, 100, 1);
}
.eye-protect {
background: #c8e6d9 !important;
background-image: none !important;
}
.eye-protect .chat-box {
background: rgba(180,200,150,0.85);
}
.eye-protect .right-panel {
background: rgba(80,100,70,0.9);
}
#sendBtn i {
font-size: 16px;
}
@media (max-width: 768px) {
.right-panel {
width: 280px;
}
.input-area {
gap: 8px;
}
#sendBtn {
padding: 0 20px;
font-size: 14px;
}
}
.proxy-info {
background: rgba(0,0,0,0.6);
padding: 4px 8px;
border-radius: 20px;
font-size: 10px;
margin-top: 8px;
text-align: center;
}
</style>
</head>
<body>
<div class="main-layout">
<!-- 左侧聊天区域 -->
<div class="cat-chat-area">
<div class="chat-box" id="chatBox">
<div class="ai">🐱 主人好呀~ 我是小猫AI,已整合智能引擎!点击右侧「连接服务器」开启AI对话 ✨<br>📝 提示:输入框支持多行文本,按 Enter 发送,Shift+Enter 换行<br>🔌 连接将通过 Socket.IO 代理服务转发</div>
</div>
<div class="input-area">
<div class="input-wrapper">
<textarea id="msgInput" placeholder="喵~ 跟小猫说说话吧... (Shift+Enter换行)" autocomplete="off" rows="1"></textarea>
</div>
<button id="sendBtn"><i class="fas fa-paper-plane"></i> 发送</button>
</div>
</div>
<!-- 右侧面板: 小猫头像 + WS控制台 + 背景/护眼功能 -->
<div class="right-panel">
<!-- 小猫头像区域 -->
<div class="avatar-container">
<div id="roleArea">
<img id="roleImg" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 200'%3E%3Ccircle cx='100' cy='100' r='80' fill='%23f9cfb0'/%3E%3Ccircle cx='70' cy='80' r='8' fill='%233b2b1f'/%3E%3Ccircle cx='130' cy='80' r='8' fill='%233b2b1f'/%3E%3Cpath d='M80 120 Q100 140 120 120' stroke='%23945c3a' fill='none' stroke-width='4'/%3E%3Cpolygon points='100,150 92,175 108,175' fill='%23ffaaa5'/%3E%3C/svg%3E" alt="小猫AI">
</div>
<div class="avatar-name">🐱 小猫 AI</div>
</div>
<!-- 背景与显示设置 -->
<div style="width: 100%; margin-bottom: 12px;">
<button id="changeBgBtn" class="bg-btn"><i class="fas fa-image"></i> 🖼️ 更换背景</button>
<button id="eyeModeBtn" class="eye-mode-btn"><i class="fas fa-eye"></i> 🌿 护眼模式</button>
</div>
<!-- WebSocket 智能连接卡片 -->
<div class="ws-card">
<div class="ws-title"><i class="fas fa-globe"></i> AI 智能引擎 (Socket.IO代理)</div>
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 10px;">
<span class="status-dot" id="wsStatusDot" style="background:#f44336;"></span>
<span id="wsStatusText" style="font-size:12px;">未连接</span>
</div>
<div class="proxy-info">
<i class="fas fa-shield-alt"></i> 通过 Socket.IO 代理服务连接
</div>
<input type="text" id="wsUrlInput" class="ws-url-input" value="ws://localhost:8765" placeholder="目标 ws://地址:端口">
<div class="ws-control">
<button id="wsConnectBtn" class="btn-sm"><i class="fas fa-plug"></i> 连接</button>
<button id="wsDisconnectBtn" class="btn-sm" style="background:#6c757d;" disabled><i class="fas fa-power-off"></i> 断开</button>
</div>
<div style="margin-top: 12px;">
<div class="ws-title" style="font-size:12px;"><i class="fas fa-terminal"></i> 快捷命令</div>
<div class="command-grid">
<div class="cmd-btn" data-cmd="/help">/help</div>
<div class="cmd-btn" data-cmd="/list">/list</div>
<div class="cmd-btn" data-cmd="/memories">/memories</div>
<div class="cmd-btn" data-cmd="/tasks">/tasks</div>
<div class="cmd-btn" data-cmd="/new">/new</div>
<div class="cmd-btn" data-cmd="/config">/config</div>
</div>
</div>
<div id="statsPanel" style="margin-top: 12px; display:none;">
<div class="stats-line"><i class="fas fa-clock"></i> 连接时间: <span id="connectTime">-</span></div>
<div class="stats-line"><i class="fas fa-envelope"></i> 消息数: <span id="msgCount">0</span></div>
<div class="stats-line"><i class="fas fa-tasks"></i> 活跃任务: <span id="taskCount">0</span></div>
</div>
</div>
</div>
</div>
<!-- 隐藏文件上传 -->
<input type="file" id="bgFile" accept="image/*" style="display:none;">
<script>
(function(){
// J2(审计 2026-08-19):marked 输出经 DOMPurify 净化——AI 输出/命令
// 执行结果是不可信输入,含原始 HTML 即可注入脚本;DOMPurify 缺失时
// 退化为转义文本(只显示不执行,防 XSS 兜底)
function renderMarkdown(content) {
const html = marked.parse(content);
if (window.DOMPurify) return DOMPurify.sanitize(html);
return html.replace(/</g, '&lt;');
}
// ---------- DOM 元素 ----------
const bgFile = document.getElementById('bgFile');
const roleImg = document.getElementById('roleImg');
const chatBox = document.getElementById('chatBox');
const msgInput = document.getElementById('msgInput');
const sendBtn = document.getElementById('sendBtn');
const changeBgBtn = document.getElementById('changeBgBtn');
const eyeModeBtn = document.getElementById('eyeModeBtn');
let eyeProtectActive = false;
// ---------- Socket.IO 连接管理 ----------
let socket = null;
let reconnectAttempts = 0;
let reconnectTimer = null;
let manualDisconnect = false;
let markdownEnabled = true;
let globalMsgCount = 0;
let activeTaskCount = 0;
let connectStartTime = null;
let targetWsUrl = null;
// 流式消息管理
let streamingChat = { element: null, content: '', timer: null, active: false };
let streamingTasks = {};
function updateMsgStats() {
document.getElementById('msgCount').innerText = globalMsgCount;
document.getElementById('taskCount').innerText = activeTaskCount;
}
function addSystemMessage(text, isError=false) {
const div = document.createElement('div');
div.className = isError ? 'error-msg' : 'system-msg';
div.style.cssText = 'background:rgba(0,0,0,0.6); border-radius:16px; padding:6px 12px; margin:6px 0; font-size:12px;';
// J2:text 为服务端内容(命令执行结果/AI 输出),禁止拼 innerHTML——
// 原实现 `${text}` 模板拼接使任意 HTML 注入执行;图标为静态字面量
const icon = document.createElement('i');
icon.className = `fas ${isError ? 'fa-exclamation-triangle' : 'fa-info-circle'}`;
div.appendChild(icon);
div.appendChild(document.createTextNode(' ' + text));
chatBox.appendChild(div);
chatBox.scrollTop = chatBox.scrollHeight;
}
function updateStreamingDisplay(type, execId=null) {
let stream = (type==='chat') ? streamingChat : (execId ? streamingTasks[execId] : null);
if(!stream || !stream.element) return;
const textDiv = stream.element.querySelector('.message-text');
if(textDiv) {
textDiv.setAttribute('data-raw', stream.content);
if(markdownEnabled) {
textDiv.innerHTML = renderMarkdown(stream.content);
textDiv.querySelectorAll('pre code').forEach(block => hljs.highlightElement(block));
} else textDiv.innerText = stream.content;
}
chatBox.scrollTop = chatBox.scrollHeight;
}
function handleStreamChunk(content, type='chat', taskMeta=null) {
if(type === 'chat') {
if(!streamingChat.element) {
const wrapper = document.createElement('div');
wrapper.className = 'ai streaming';
wrapper.innerHTML = `<div class="message-text markdown-body" data-raw=""></div>`;
streamingChat.element = wrapper;
chatBox.appendChild(wrapper);
streamingChat.content = '';
streamingChat.active = true;
}
streamingChat.content += content;
if(streamingChat.timer) clearTimeout(streamingChat.timer);
streamingChat.timer = setTimeout(() => updateStreamingDisplay('chat'), 50);
}
else if(type === 'task' && taskMeta && taskMeta.executionId) {
const eid = taskMeta.executionId;
if(!streamingTasks[eid]) {
const sep = document.createElement('div');
sep.className = 'task-separator';
// J2:taskMeta.taskTitle 为服务端可控内容,禁止拼 innerHTML
const bolt = document.createElement('i');
bolt.className = 'fas fa-bolt';
sep.appendChild(bolt);
sep.appendChild(document.createTextNode(
` 定时任务 #${taskMeta.taskId} (${taskMeta.taskTitle || '执行'}) 第${taskMeta.triggerCount||1}次执行`));
chatBox.appendChild(sep);
const taskDiv = document.createElement('div');
taskDiv.className = 'task-msg streaming';
taskDiv.style.cssText = 'background:#2d2a1e; border-radius:16px; margin:6px 0; padding:8px 12px;';
taskDiv.innerHTML = `<div class="message-text markdown-body"></div>`;
chatBox.appendChild(taskDiv);
streamingTasks[eid] = { element: taskDiv, content: '', timer: null };
activeTaskCount++;
updateMsgStats();
}
streamingTasks[eid].content += content;
if(streamingTasks[eid].timer) clearTimeout(streamingTasks[eid].timer);
streamingTasks[eid].timer = setTimeout(() => {
const st = streamingTasks[eid];
if(st && st.element) {
const textDiv = st.element.querySelector('.message-text');
if(textDiv) {
if(markdownEnabled) textDiv.innerHTML = renderMarkdown(st.content);
else textDiv.innerText = st.content;
textDiv.querySelectorAll('pre code').forEach(block => hljs.highlightElement(block));
}
chatBox.scrollTop = chatBox.scrollHeight;
}
}, 50);
}
}
function completeStream(type='chat', execId=null) {
if(type==='chat' && streamingChat.element) {
if(streamingChat.timer) clearTimeout(streamingChat.timer);
updateStreamingDisplay('chat');
streamingChat.element.classList.remove('streaming');
globalMsgCount++;
updateMsgStats();
streamingChat = { element: null, content: '', timer: null, active: false };
} else if(type==='task' && execId && streamingTasks[execId]) {
const task = streamingTasks[execId];
if(task.timer) clearTimeout(task.timer);
if(task.element) {
const txtDiv = task.element.querySelector('.message-text');
if(txtDiv && markdownEnabled) txtDiv.innerHTML = renderMarkdown(task.content);
task.element.classList.remove('streaming');
}
globalMsgCount++;
delete streamingTasks[execId];
activeTaskCount = Math.max(0, activeTaskCount-1);
updateMsgStats();
}
}
function handleWSMessage(data) {
if(data.type === 'chunk') handleStreamChunk(data.content, 'chat');
else if(data.type === 'complete') completeStream('chat');
else if(data.type === 'dialogue_ended') {
addSystemMessage("✨ 对话已结束,可开启新对话");
if(streamingChat.element) completeStream('chat');
}
else if(data.type === 'task_process') {
if(data.msg_type === 'chunk') {
handleStreamChunk(data.content, 'task', { taskId: data.task_id, executionId: data.execution_id, taskTitle: data.task_title, triggerCount: (data.execution_id?.split('_')[2]) });
} else if(data.msg_type === 'complete') completeStream('task', data.execution_id);
}
else if(data.type === 'task_status') addSystemMessage(`📌 任务${data.task_id}: ${data.title} - ${data.status}`);
else if(data.type === 'error') addSystemMessage(`❌ ${data.content}`, true);
else if(data.type === 'command_result') addSystemMessage(`📋 ${data.content}`);
else if(data.type === 'info') addSystemMessage(`ℹ️ ${data.content}`);
}
function sendWSMessage(message, isCommand=false) {
if(!socket || !socket.connected) {
addSystemMessage("未连接到AI服务器,请先点击连接", true);
return false;
}
const payload = isCommand ? { type: 'command', command: message } : { type: 'message', content: message };
socket.emit('ws_send', { message: JSON.stringify(payload) });
return true;
}
function connectWebSocket() {
// F3(审计 2026-08-20):CONNECTING 中的旧连接也要断开,否则重复点击产生孤儿连接/双后端会话
if(socket) {
socket.disconnect();
}
targetWsUrl = document.getElementById('wsUrlInput').value.trim();
if(!targetWsUrl) {
addSystemMessage("请输入WebSocket服务器地址", true);
return;
}
if (!targetWsUrl.startsWith('ws://') && !targetWsUrl.startsWith('wss://')) {
addSystemMessage("WebSocket地址必须以 ws:// 或 wss:// 开头", true);
return;
}
manualDisconnect = false;
socket = io({
transports: ['websocket', 'polling'],
withCredentials: true
});
socket.on('connect', () => {
console.log('Socket.IO connected, SID:', socket.id);
updateConnectionUI(true);
reconnectAttempts = 0;
connectStartTime = new Date();
document.getElementById('connectTime').innerText = connectStartTime.toLocaleTimeString();
addSystemMessage(`✅ Socket.IO 连接成功!正在连接目标 WebSocket: ${targetWsUrl}`);
socket.emit('ws_connect', { target_url: targetWsUrl });
});
socket.on('connected', (data) => {
addSystemMessage(`✅ ${data.message}`);
});
socket.on('ws_connected', (data) => {
addSystemMessage(`✅ ${data.message}`);
if(streamingChat.element) completeStream('chat');
});
socket.on('ws_message', (data) => {
try {
const messageData = JSON.parse(data.data);
handleWSMessage(messageData);
} catch(err) {
handleStreamChunk(data.data, 'chat');
}
});
socket.on('ws_error', (data) => {
addSystemMessage(`❌ ${data.error}`, true);
updateConnectionUI(false);
});
socket.on('disconnect', (reason) => {
console.log('Socket.IO disconnected:', reason);
updateConnectionUI(false);
if(!manualDisconnect && reconnectAttempts < 3) {
reconnectAttempts++;
addSystemMessage(`⚠️ 连接断开 (${reason}),${reconnectAttempts}/3 次重试...`);
if(reconnectTimer) clearTimeout(reconnectTimer);
reconnectTimer = setTimeout(() => connectWebSocket(), 2000);
} else if(!manualDisconnect) {
addSystemMessage(`❌ 连接断开 (${reason}),请手动重连`);
} else {
addSystemMessage("已手动断开连接");
}
});
socket.on('connect_error', (error) => {
console.error('Socket.IO connection error:', error);
addSystemMessage(`❌ 连接错误: ${error.message}`, true);
});
}
function disconnectWebSocket() {
manualDisconnect = true;
// F2(审计 2026-08-20):清除已排程的重连定时器,防止倒计时到期把连接"复活"
if(reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
if(socket) {
socket.disconnect();
socket = null;
}
targetWsUrl = null;
updateConnectionUI(false);
}
function updateConnectionUI(connected) {
const dot = document.getElementById('wsStatusDot');
const text = document.getElementById('wsStatusText');
const connectBtn = document.getElementById('wsConnectBtn');
const disconnectBtn = document.getElementById('wsDisconnectBtn');
const statsPanel = document.getElementById('statsPanel');
if(connected) {
dot.style.background = "#4CAF50";
text.innerText = "已连接 (Socket.IO)";
connectBtn.disabled = true;
disconnectBtn.disabled = false;
statsPanel.style.display = "block";
} else {
dot.style.background = "#f44336";
text.innerText = "未连接";
connectBtn.disabled = false;
disconnectBtn.disabled = true;
statsPanel.style.display = "none";
activeTaskCount = 0;
globalMsgCount = 0;
updateMsgStats();
Object.keys(streamingTasks).forEach(k => completeStream('task', k));
if(streamingChat.element) completeStream('chat');
}
}
function onSendMessage() {
let text = msgInput.value.trim();
if(!text) return;
const userDiv = document.createElement('div');
userDiv.className = 'user';
userDiv.innerText = text;
chatBox.appendChild(userDiv);
msgInput.value = '';
autoResizeTextarea();
msgInput.focus();
chatBox.scrollTop = chatBox.scrollHeight;
if(socket && socket.connected) {
if(text.startsWith('/')) sendWSMessage(text, true);
else sendWSMessage(text, false);
} else {
setTimeout(() => {
let reply = "喵~ 请先连接WebSocket服务器获取智能回复~ ✨\n💡 提示:多行文本也支持哦!\n🔌 连接将通过 Socket.IO 代理服务转发";
const aiDiv = document.createElement('div');
aiDiv.className = 'ai';
aiDiv.innerText = `🐱 小猫AI:${reply}`;
chatBox.appendChild(aiDiv);
chatBox.scrollTop = chatBox.scrollHeight;
}, 200);
}
}
function autoResizeTextarea() {
if (!msgInput) return;
msgInput.style.height = 'auto';
const newHeight = Math.min(msgInput.scrollHeight, 150);
msgInput.style.height = newHeight + 'px';
}
msgInput.addEventListener('input', autoResizeTextarea);
autoResizeTextarea();
changeBgBtn.onclick = () => bgFile.click();
bgFile.onchange = (e) => {
let f = e.target.files[0];
if(f) {
let url = URL.createObjectURL(f);
document.body.style.backgroundImage = `url(${url})`;
document.body.style.backgroundSize = "cover";
document.body.style.backgroundPosition = "center center";
document.body.style.backgroundRepeat = "no-repeat";
if(eyeProtectActive) {
eyeProtectActive = false;
document.body.classList.remove('eye-protect');
addSystemMessage("护眼模式已自动关闭(背景已更换)");
}
addSystemMessage("✨ 背景已更换");
}
};
eyeModeBtn.onclick = () => {
if(!eyeProtectActive) {
document.body.classList.add('eye-protect');
document.body.style.backgroundImage = "none";
eyeProtectActive=true;
addSystemMessage("护眼模式已开启(背景图片暂时隐藏)");
eyeModeBtn.style.background = "rgba(60, 140, 80, 0.9)";
}
else {
document.body.classList.remove('eye-protect');
document.body.style.backgroundImage = "url('/imgs/background.jpeg')";
document.body.style.backgroundSize = "cover";
document.body.style.backgroundPosition = "center center";
document.body.style.backgroundRepeat = "no-repeat";
eyeProtectActive=false;
addSystemMessage("护眼模式关闭,恢复背景");
eyeModeBtn.style.background = "rgba(100, 180, 120, 0.8)";
}
};
document.getElementById('wsConnectBtn').onclick = connectWebSocket;
document.getElementById('wsDisconnectBtn').onclick = disconnectWebSocket;
document.querySelectorAll('.cmd-btn').forEach(btn => {
btn.onclick = () => {
let cmd = btn.getAttribute('data-cmd');
msgInput.value = cmd + ' ';
autoResizeTextarea();
msgInput.focus();
};
});
sendBtn.onclick = onSendMessage;
msgInput.addEventListener('keydown', (e) => {
if(e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
onSendMessage();
}
});
document.body.style.backgroundImage = "url('/imgs/background.jpeg')";
document.body.style.backgroundSize = "cover";
document.body.style.backgroundPosition = "center center";
document.body.style.backgroundRepeat = "no-repeat";
msgInput.focus();
})();
</script>
</body>
</html>