Files
blackbird/aibox/entry1.html
T
qing 457bb900fb fix(aibox): round-5 audit batch K - max_turns clamp, task input validation, XSS hardening, atomic writes, broadcast timeout
第五轮审计(J1~J16)全量修复 + 子代理 max_turns 钳制:
- 子代理 max_turns 钳制(0/None 回退默认 10,负数钳制 1)+ 14 项回归测试
- J1 任务系统投毒:add_memo 入库校验(时间/间隔/类型)、调度器预检与触发事务
  防御、展示路径兜底、register 移入 try、add_user_task 补 repeat_end_time 透传
- J2/J13 前端 XSS:三页面 marked 输出经 DOMPurify 净化、entry1 系统消息/任务
  分隔条改 textContent、CDN 脚本补 SRI
- J3 FTS 删除换序(先 fts 后内容表,索引不再残留)
- J5 原子写推广:新增 common/atomic.py(mkstemp+fsync+os.replace),config/prompt/
  history 全部接入;损坏配置备份后回退
- J6 单例初始化加锁(PromptManager/ConfigManager 并发首例不双重加载)
- J7 broadcast 并发发送 + wait_for 超时回收僵死连接
- J4/J8/J9~J12/J14~J16:验证异常脱敏、交互输入容错、MemoDatabase WAL+busy_timeout、
  memo 批量删除、/new 竞态、skills 引号转义、voice_client 清理、死代码移除、
  skill_manager 同名键/help 截断
2026-08-19 07:01:13 +08:00

897 lines
35 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">
<!-- highlight.js 代码高亮 + marked markdown解析 -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/github-dark.min.css">
<!-- 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() {
if(socket && socket.connected) {
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;
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>