增加运维机器人单元
This commit is contained in:
1 parent
479d263b88
commit
1b60ab0a2e
42 files changed
+20469
No files matched your search
@@ -0,0 +1,875 @@
|
||||
<!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">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/9.1.6/marked.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.min.js"></script>
|
||||
<!-- Socket.IO 客户端库 -->
|
||||
<script src="https://cdn.socket.io/4.5.0/socket.io.min.js"></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(){
|
||||
// ---------- 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;';
|
||||
div.innerHTML = `<i class="fas ${isError ? 'fa-exclamation-triangle' : 'fa-info-circle'}"></i> ${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 = marked.parse(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';
|
||||
sep.innerHTML = `<i class="fas fa-bolt"></i> 定时任务 #${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 = marked.parse(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 = marked.parse(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>
|
||||
Reference in new issue
Block a user