// AI 控制台(顶级 Tab):DeepSeek 风格聊天 + 流式输出 + 实时 MCP 步骤 let _agentBusy = false; let _agentStream = null; // EventSource let _agentCfgLoaded = false; // ================== 初始化 ================== function initAgentChat(){ if(_agentCfgLoaded)return; _agentCfgLoaded = true; loadAgentConfig(); const chat = document.getElementById('agent-chat'); if(chat && !chat.children.length){ chat.innerHTML = '
👋 给 AI 下达指令,它将通过截图观察手机并执行操作。
' + '例如:「打开抖音搜索奚学东,告诉我第一个视频的标题」
' + '点击右上角 ⚙ 配置模型与 API Key。
'; } bindChatScroll(); loadLiveDevices(); // 输入框快捷键 const inp = document.getElementById('agent-input'); inp.addEventListener('keydown', ev=>{ if(ev.key==='Enter' && !ev.shiftKey){ ev.preventDefault(); if(!_agentBusy) sendAgentMsg(); } }); } // ================== 配置 ================== function loadAgentConfig(){ apiGet('/api/agent/config').then(r=>{ if(!r||!r.ok)return; const tag = document.getElementById('agent-model-tag'); if(tag && r.model) tag.textContent = r.model; }); } function openAgentConfig(){ apiGet('/api/agent/config').then(r=>{ if(!r||!r.ok)return; document.getElementById('acfg-base').value = r.api_base || 'https://api.deepseek.com'; document.getElementById('acfg-model').value = r.model || ''; document.getElementById('acfg-serial').value = r.default_serial || ''; document.getElementById('acfg-key-hint').textContent = r.api_key_masked ? ('已配置 ' + r.api_key_masked) : '未配置'; document.getElementById('acfg-key').value = ''; document.getElementById('agent-cfg-overlay').style.display = 'flex'; }); } function closeAgentConfig(){ document.getElementById('agent-cfg-overlay').style.display = 'none'; } function saveAgentConfig(){ const body = { api_base: document.getElementById('acfg-base').value.trim(), model: document.getElementById('acfg-model').value.trim(), default_serial: document.getElementById('acfg-serial').value.trim(), }; const key = document.getElementById('acfg-key').value.trim(); if(key) body.api_key = key; apiPost('/api/agent/config', body).then(r=>{ if(r&&r.ok){ showToast('配置已保存','success'); closeAgentConfig(); loadAgentConfig(); }else showToast('保存失败: ' + ((r&&r.error)||''),'error'); }); } // ================== 聊天渲染 ================== function addUserMsg(text){ const chat = document.getElementById('agent-chat'); clearEmpty(); const div = document.createElement('div'); div.className = 'agent-msg user'; div.textContent = text; chat.appendChild(div); scrollChat(); } function newAssistantMsg(){ const chat = document.getElementById('agent-chat'); clearEmpty(); const div = document.createElement('div'); div.className = 'agent-msg assistant'; div.innerHTML = '
'; chat.appendChild(div); scrollChat(); return div; } function clearEmpty(){ const empty = document.querySelector('#agent-chat .agent-empty'); if(empty) empty.remove(); } let _chatPinned = true; // 用户是否在底部(贴底才自动跟随滚动) function scrollChat(force){ const chat = document.getElementById('agent-chat'); if(force || _chatPinned){ chat.scrollTop = chat.scrollHeight; } } function bindChatScroll(){ const chat = document.getElementById('agent-chat'); chat.addEventListener('scroll', ()=>{ _chatPinned = chat.scrollHeight - chat.scrollTop - chat.clientHeight < 60; }); } function clearAgentChat(){ document.getElementById('agent-chat').innerHTML = '
对话已清空。给 AI 下达新指令…
'; } // 新建会话:清后端历史 + 清前端对话区(像 DeepSeek 的新建会话) function newAgentSession(){ if(_agentBusy){showToast('运行中不能新建会话','error');return;} if(!confirm('新建会话将清空当前对话历史,确定?'))return; apiPost('/api/agent/clear',{}).then(r=>{ if(r&&r.ok){ clearAgentChat(); showToast('已新建会话','success'); }else showToast('操作失败','error'); }); } // ================== 发送与流式接收 ================== function setRunning(on){ _agentBusy = on; document.getElementById('agent-running-tag').style.display = on ? 'inline' : 'none'; document.getElementById('btn-agent-stop').style.display = on ? 'inline-block' : 'none'; document.getElementById('btn-agent-send').disabled = on; } function stopAgent(){ apiPost('/api/agent/stop',{}).then(r=>{ if(r&&r.ok) showToast('已请求停止,正在中断…','success'); else showToast((r&&r.error)||'停止失败','error'); }); } function sendAgentMsg(){ if(_agentBusy){showToast('上一轮还在运行','error');return;} const inp = document.getElementById('agent-input'); const text = inp.value.trim(); if(!text){showToast('请输入指令','error');return;} inp.value = ''; addUserMsg(text); apiPost('/api/agent/run', {prompt: text}).then(r=>{ if(!r||!r.ok){ const msg = r ? (r.error||'启动失败') : '请求失败'; const div = newAssistantMsg(); div.querySelector('.agent-text').textContent = '⚠ ' + msg; showToast(msg,'error'); return; } setRunning(true); listenStream(r.run_id); }); } function listenStream(runId){ if(_agentStream) _agentStream.close(); const es = new EventSource('/api/agent/stream?run_id=' + runId); _agentStream = es; let msgEl = null; es.addEventListener('delta', ev=>{ if(!msgEl) msgEl = newAssistantMsg(); const d = JSON.parse(ev.data); const textEl = msgEl.querySelector('.agent-text'); if(d.kind === 'reasoning'){ let r = msgEl.querySelector('.reasoning'); if(!r){ r = document.createElement('div'); r.className = 'reasoning'; msgEl.insertBefore(r, textEl); } r.textContent += d.text; }else{ textEl.textContent += d.text; } scrollChat(); }); es.addEventListener('step', ev=>{ const d = JSON.parse(ev.data); followSerialFromArgs(d.args); if(!msgEl) msgEl = newAssistantMsg(); const cards = msgEl.querySelector('.agent-toolcards'); const card = document.createElement('div'); card.className = 'agent-toolcard'; card.innerHTML = '' + '' + esc(d.tool||'') + '' + '' + esc(d.args||'') + ''; if(d.image){ const img = document.createElement('img'); img.src = 'data:image/jpeg;base64,' + d.image; card.appendChild(img); } cards.appendChild(card); scrollChat(); }); es.addEventListener('done', ev=>{ const d = JSON.parse(ev.data); if(d.answer){ if(!msgEl) msgEl = newAssistantMsg(); msgEl.querySelector('.agent-text').textContent = d.answer; } endRun(); }); es.addEventListener('error', ev=>{ let msg = '连接中断'; try{ if(ev.data) msg = JSON.parse(ev.data).message || msg; }catch(e){} if(!msgEl) msgEl = newAssistantMsg(); msgEl.querySelector('.agent-text').textContent = '⚠ ' + msg; endRun(); }); es.onerror = ()=>{ // 事件流正常结束(done 后服务器关流)会触发一次 error——done 已处理则忽略 if(_agentBusy) endRun(); }; } // ============ 实时画面(右侧,MJPEG 流) ============ let _liveSerial = ''; function showLive(serial){ if(!serial || serial === _liveSerial) return; _liveSerial = serial; const img = document.getElementById('agent-live-img'); img.src = '/api/screen/stream?serial=' + encodeURIComponent(serial) + '&q=80&fps=8&t=' + Date.now(); img.style.display = 'block'; document.getElementById('agent-live-empty').style.display = 'none'; document.getElementById('agent-live-serial').textContent = serial; const sel = document.getElementById('agent-live-select'); if(sel) sel.value = serial; } function stopLive(){ _liveSerial = ''; const img = document.getElementById('agent-live-img'); img.src = ''; img.style.display = 'none'; document.getElementById('agent-live-empty').style.display = 'block'; document.getElementById('agent-live-serial').textContent = ''; } function followSerialFromArgs(argsStr){ try{ const a = JSON.parse(argsStr || '{}'); if(a.serial) showLive(a.serial); }catch(e){} } function watchDevice(serial){ if(!serial){ stopLive(); return; } showLive(serial); } function loadLiveDevices(){ apiGet('/api/devices').then(r=>{ if(!r||!r.ok)return; const sel = document.getElementById('agent-live-select'); if(!sel)return; const cur = sel.value; const devs = (r.devices||[]).filter(x=>x.indexOf(':')>=0); sel.innerHTML = '' + devs.map(s=>'').join(''); }); } // 截图点击放大查看 function zoomScreenshot(img){ const ov = document.getElementById('agent-zoom'); if(!ov){ const d = document.createElement('div'); d.id = 'agent-zoom'; d.style.cssText = 'display:none;position:fixed;inset:0;background:rgba(0,0,0,.8);z-index:1400;align-items:center;justify-content:center;cursor:zoom-out'; d.onclick = ()=>d.style.display='none'; document.body.appendChild(d); } const ovEl = document.getElementById('agent-zoom'); ovEl.innerHTML = ''; const big = document.createElement('img'); big.src = img.src; big.style.cssText = 'max-width:88vw;max-height:88vh;border-radius:8px'; ovEl.appendChild(big); ovEl.style.display = 'flex'; } // 截图 img 绑定点击放大 document.addEventListener('click', ev=>{ const t = ev.target; if(t && t.tagName==='IMG' && t.closest('.agent-toolcard') && !t.closest('#agent-zoom')){ zoomScreenshot(t); } }); function endRun(){ setRunning(false); _agentStream && _agentStream.close(); _agentStream = null; scrollChat(); } // ================== 经验库(自进化记忆管理) ================== function openExpLib(){ document.getElementById('agent-exp-overlay').style.display = 'flex'; loadExpLib(); } function closeExpLib(){ document.getElementById('agent-exp-overlay').style.display = 'none'; } function loadExpLib(){ const list = document.getElementById('exp-lib-list'); apiGet('/api/agent/experience').then(r=>{ if(!r||!r.ok){ showToast((r&&r.error)||'加载失败','error'); return; } document.getElementById('exp-lib-last').textContent = (r.last ? ('最近巡检 ' + r.last + ':' + (r.last_summary||'')) : ''); if(r.running){ list.innerHTML = '
🔍 AI 巡检进行中(约 1 分钟),完成后请刷新…
'; return; } const exps = r.experiences||[]; if(!exps.length){ list.innerHTML = '
暂无经验——跑一轮带手机操作的任务后会自动沉淀
'; return; } list.innerHTML = ''; exps.forEach(e=>{ const a = e.audit||{}; const card = document.createElement('div'); card.style.cssText = 'border:1px solid var(--card-line);border-radius:10px;padding:9px 13px;background:#0d1117'; // 徽章 let badge = ''; if(a.action === 'pending' && a.verdict === 'delete'){ badge = '⚠ 建议删除'; }else if(a.action === 'kept'){ badge = '' + (a.verdict === 'delete' ? '已人工保留' : '✓ 已保留') + ''; } let actBtns = ''; if(a.action === 'pending'){ actBtns = '' + ''; }else{ actBtns = ''; } card.innerHTML = '
' + '#' + e.id + '' + '' + esc(e.created_at||'') + '' + '引用 ' + (e.hits||0) + ' 次' + badge + '' + actBtns + '
' + '
' + esc(e.task_prompt) + '
' + '
' + esc((e.recipe||'').slice(0,220)) + '
' + (a.action === 'pending' && a.reason ? '
AI 建议理由:' + esc(a.reason) + (a.score ? '(评分 ' + a.score + '/10)' : '') + '
' : ''); list.appendChild(card); }); }); } function triggerExpAudit(){ apiPost('/api/agent/experience/audit',{}).then(r=>{ if(r&&r.ok){ showToast('巡检已启动,约 1 分钟完成','success'); document.getElementById('exp-lib-list').innerHTML = '
🔍 AI 巡检进行中…
'; // 完成后自动刷新 setTimeout(()=>{ if(document.getElementById('agent-exp-overlay').style.display==='flex') loadExpLib(); }, 90000); }else showToast((r&&r.error)||'巡检启动失败','error'); }); } function expDelete(id){ if(!confirm('确认删除该经验?删除后不可恢复(下次相似任务不再自动参考)。')) return; apiPost('/api/agent/experience/delete',{id:id}).then(r=>{ if(r&&r.ok){ showToast('已删除','success'); loadExpLib(); } else showToast((r&&r.error)||'删除失败','error'); }); } function expKeep(id){ apiPost('/api/agent/experience/keep',{id:id}).then(r=>{ if(r&&r.ok){ showToast('已保留,后续不再重复建议','success'); loadExpLib(); } else showToast((r&&r.error)||'操作失败','error'); }); }