// AI 控制台(顶级 Tab):DeepSeek 风格聊天 + 流式输出 + 实时 MCP 步骤 let _agentBusy = false; let _agentStream = null; // EventSource let _agentCfgLoaded = false; // 后端异常文案兜底:MCP 客户端在工具服务不可达时只抛 // "Server returned an error response" 这类含糊字样,这里映射成明确指引。 function _friendlyAgentError(msg){ const raw = String(msg || ''); const s = raw.replace(/^[A-Za-z_]*Error:\s*/, ''); // 去掉 "RuntimeError: " 之类前缀 if(/不可达|连接中断/.test(s)) return s; // 后端已给出明确文案,直接用 if(/mcp_server|server returned an error response|connecterror|connection refused|all connection attempts failed|server disconnected|network is unreachable|connect timeout/i.test(s)) return 'MCP server(8033) 不可达:AI 无法连接设备工具服务,请确认 MCP server 已启动(本机可运行 python -m mcp_server.mcp_server)'; return raw; } // ================== 初始化 ================== let _agentDefaultSerial = ''; // 目标设备显示文案:名称优先(设备身份),没有名称退地址 let _agentTargets={}; // serial -> name function _agentTargetText(serial){ const nm=_agentTargets[serial]; return nm ? (nm + ' · ' + serial) : String(serial||''); } function initAgentChat(){ if(_agentCfgLoaded)return; _agentCfgLoaded = true; loadAgentConfig(); const chat = document.getElementById('agent-chat'); if(chat && !chat.children.length){ chat.innerHTML = '
👋 给 AI 下达指令,它将通过截图观察手机并执行操作。
' + '例如:「打开抖音搜索奚学东,告诉我第一个视频的标题」
' + '先选「🎯 目标设备」(AI 只操作你选定的设备),点击右上角 ⚙ 配置模型与 API Key。
'; } bindChatScroll(); loadLiveDevices(); loadAgentTargetDevices(); startRunPoll(); renderConvList(); // 会话列表(决定当前会话) loadCurrentConvMessages(); // 渲染当前会话历史 restoreRunningFlow(); // 若任务运行中:重订阅事件流 // 输入框快捷键 const inp = document.getElementById('agent-input'); inp.addEventListener('keydown', ev=>{ if(ev.key==='Enter' && !ev.shiftKey){ ev.preventDefault(); if(!_agentBusy) sendAgentMsg(); } }); } // ================== 页面刷新/重进恢复(运行中任务续流) ================== // 历史轮次由 loadCurrentConvMessages 从会话渲染;这里只处理「任务仍在后台跑」: // 重新订阅事件流(服务端队列保留积压,重连后补发 delta/step/done)。 function restoreRunningFlow(){ apiGet('/api/agent/run').then(r=>{ if(!r||!r.ok) return; if(r.state === 'running' && r.run_id){ setRunning(true); listenStream(r.run_id); }else if(r.state === 'error' && r.error && _currentConvId){ // 错误提示仅当本会话相关时展示 const chat = document.getElementById('agent-chat'); if(chat && !chat.children.length){ const div = newAssistantMsg(); div.querySelector('.agent-text').textContent = '⚠ ' + _friendlyAgentError(r.error); } } }); } // ================== 配置 ================== 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; _agentDefaultSerial = r.default_serial || ''; }); } // ================== 目标设备选择(AI 只操作选定设备) ================== function loadAgentTargetDevices(){ apiGet('/api/agent/devices').then(r=>{ if(!r||!r.ok)return; const sel = document.getElementById('agent-target-select'); if(!sel)return; const cur = sel.value; const devs = (r.devices||[]).filter(x=>x.online); _agentTargets = {}; devs.forEach(d=>{ if(d.name) _agentTargets[d.serial]=d.name; }); sel.innerHTML = '' + devs.map(d=>{ const label = devText(d.name, d.serial) + (d.model ? ' · ' + d.model : '') + (d.busy ? ' ⛔ 任务中:' + d.task_job : ''); return ''; }).join(''); // 保留当前选择;否则预选配置的默认设备 if(cur && [...sel.options].some(o=>o.value===cur)) sel.value = cur; else if(_agentDefaultSerial && [...sel.options].some(o=>o.value===_agentDefaultSerial)) sel.value = _agentDefaultSerial; const hint = document.getElementById('agent-target-hint'); if(hint && sel.value) hint.textContent = '将操作:' + _agentTargetText(sel.value); }); } // ================== 跨窗口运行状态(多人/多窗口可见并可停止) ================== let _runPoll = null; function startRunPoll(){ if(_runPoll) return; _runPoll = setInterval(pollRunState, 8000); } function pollRunState(){ if(_agentStream) return; // 本窗口正在跑(事件流驱动),不轮询 apiGet('/api/agent/run').then(r=>{ if(!r||!r.ok)return; const running = r.state === 'running'; document.getElementById('agent-running-tag').style.display = running ? 'inline' : 'none'; document.getElementById('btn-agent-stop').style.display = running ? 'inline-block' : 'none'; const hint = document.getElementById('agent-target-hint'); if(hint){ hint.textContent = running ? ('⏳ 运行中' + (r.started ? ' ' + r.started + ' 起' : '') + (r.serial ? ' · ' + _agentTargetText(r.serial) : '') + ':' + (r.prompt||'').slice(0,70)) : (document.getElementById('agent-target-select').value ? '将操作:' + _agentTargetText(document.getElementById('agent-target-select').value) : ''); } document.getElementById('btn-agent-send').disabled = running; // 运行中同步 token(本窗口没订阅事件流时也能看到进度) if(running && !_agentStream && r.usage && r.usage.total_tokens){ _liveUsage = r.usage; updateSessionTokens(); } if(!running && _agentBusy){ setRunning(false); } // 流异常丢失时复位 }); } 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-steps').value = r.max_steps || 40; 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(), max_steps: document.getElementById('acfg-steps').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(); loadAgentTargetDevices(); }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'; // 结构:工具卡片 → 可折叠推理链 → 正文(Markdown)→ token 用量脚注 div.innerHTML = '
' + '' + '
' + ''; chat.appendChild(div); scrollChat(); return div; } // ---------- 推理链(可折叠) ---------- function _appendReasoning(msgEl, text){ const box = msgEl.querySelector('.reasoning'); if(!box) return; const body = box.querySelector('.reasoning-text'); body.textContent += text; box.querySelector('summary').textContent = '💭 思考过程(' + body.textContent.length + ' 字)'; box.style.display = 'block'; _bindReasoning(box); if(!box.open) box.open = true; // 流式思考时展开,正文开始时自动收起 } // 用户手动点过 summary 后不再自动收起/展开(尊重手动状态) function _bindReasoning(box){ const sum = box.querySelector('summary'); if(sum && !sum.dataset.bound){ sum.dataset.bound = '1'; sum.addEventListener('click', ()=>{ box.dataset.touched = '1'; }); } } // ---------- 正文 Markdown 渲染(渲染器见 markdown.js) ---------- function _renderMd(msgEl){ const el = msgEl.querySelector('.agent-text'); if(el) el.innerHTML = renderMarkdown(msgEl._md || ''); } function _scheduleMd(msgEl){ if(msgEl._mdRaf) return; // 流式增量按帧合并渲染,避免每个 token 重排 msgEl._mdRaf = requestAnimationFrame(()=>{ msgEl._mdRaf = 0; _renderMd(msgEl); scrollChat(); }); } // ---------- token 用量 ---------- let _convUsage = {}; // 当前会话历史累计(读会话时算) let _liveUsage = {}; // 本轮运行中累计(SSE usage 事件) function _fmtInt(n){ return String(n || 0).replace(/\B(?=(\d{3})+(?!\d))/g, ','); } function _usageSum(a, b){ const o = {prompt_tokens:0, completion_tokens:0, total_tokens:0, calls:0}; [a, b].forEach(u=>{ if(!u) return; o.prompt_tokens += (u.prompt_tokens || 0); o.completion_tokens += (u.completion_tokens || 0); o.total_tokens += (u.total_tokens || 0); o.calls += (u.calls || 0); }); return o; } // 单条消息脚注 function renderUsage(msgEl, u){ const el = msgEl.querySelector('.agent-usage'); if(!el) return; if(!u || !u.total_tokens){ el.style.display = 'none'; return; } el.style.display = 'block'; el.title = '输入 ' + _fmtInt(u.prompt_tokens) + ' + 输出 ' + _fmtInt(u.completion_tokens) + ' = ' + _fmtInt(u.total_tokens) + ' tokens,共 ' + (u.calls || 0) + ' 次模型调用'; el.textContent = '🪙 ' + _fmtInt(u.total_tokens) + ' tokens' + '(↑' + _fmtInt(u.prompt_tokens) + ' ↓' + _fmtInt(u.completion_tokens) + ' · ' + (u.calls || 0) + ' 次调用)'; } // 顶栏「本会话累计」= 已落库历史 + 运行中本轮 function updateSessionTokens(){ const el = document.getElementById('agent-token-total'); if(!el) return; const t = _usageSum(_convUsage, _liveUsage); if(!t.total_tokens){ el.textContent = ''; el.title = ''; return; } el.textContent = '🪙 本会话 ' + _fmtInt(t.total_tokens) + ' tokens'; el.title = '本会话累计:输入 ' + _fmtInt(t.prompt_tokens) + ' + 输出 ' + _fmtInt(t.completion_tokens) + ' = ' + _fmtInt(t.total_tokens) + ' tokens,共 ' + t.calls + ' 次模型调用'; } 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 = ''; _convUsage = {}; // 换会话:token 累计重新算 _liveUsage = {}; updateSessionTokens(); } // ================== 历史会话(DeepSeek 式:左侧列表,多会话持久化) ================== let _currentConvId = localStorage.getItem('agent_conv_id') || ''; function saveConvId(id){ _currentConvId = id || ''; if(id) localStorage.setItem('agent_conv_id', id); else localStorage.removeItem('agent_conv_id'); } function createAgentSession(){ if(_agentBusy){showToast('运行中不能新建会话','error');return;} apiPost('/api/agent/conversations',{}).then(r=>{ if(r&&r.ok){ saveConvId(r.id); clearAgentChat(); showToast('已新建会话','success'); renderConvList(); }else showToast('新建失败: '+((r&&r.error)||''),'error'); }); } function selectAgentSession(id){ if(_agentBusy){showToast('运行中不能切换会话','error');return;} if(id === _currentConvId) return; saveConvId(id); clearAgentChat(); loadCurrentConvMessages(); renderConvList(); } function deleteAgentSession(id){ if(!confirm('删除该会话?历史消息将不可恢复。')) return; apiDelete('/api/agent/conversations/'+id).then(r=>{ if(r&&r.ok){ if(_currentConvId === id) saveConvId(''); showToast('会话已删除','success'); renderConvList(); loadCurrentConvMessages(); }else showToast('删除失败: '+((r&&r.error)||''),'error'); }); } function renderConvList(){ apiGet('/api/agent/conversations').then(r=>{ if(!r||!r.ok)return; const list = document.getElementById('agent-sess-list'); if(!list)return; const convs = r.conversations || []; let cur = _currentConvId; if(!convs.some(c=>c.id===cur)) cur = convs.length ? convs[0].id : ''; if(cur !== _currentConvId){ saveConvId(cur); clearAgentChat(); loadCurrentConvMessages(); } if(!convs.length){ list.innerHTML = '
' + '暂无历史会话

点「+ 新建会话」开始
'; return; } list.innerHTML = convs.map(c=>{ const t = c.title || '新会话'; const sid = String(c.id || ''); return '
' +'' +'
'+esc(t)+'
' +'
'+esc(c.updated_at||'')+(c.count ? ' · '+c.count+' 轮' : '') +' · #'+esc(sid.slice(0,8))+'' +'
'; }).join(''); }); } // 复制会话 ID(便于反馈问题时引用,如 conv=2ba4da0e43) function copyConvId(id){ const done = ()=>showToast('会话 ID 已复制: '+id,'success'); try{ if(navigator.clipboard && navigator.clipboard.writeText){ navigator.clipboard.writeText(id).then(done).catch(()=>fallback()); return; } }catch(e){} fallback(); function fallback(){ const ta=document.createElement('textarea'); ta.value=id; document.body.appendChild(ta); ta.select(); try{ document.execCommand('copy'); done(); }catch(e){ showToast('会话 ID: '+id,'success'); } ta.remove(); } } function loadCurrentConvMessages(){ const chat = document.getElementById('agent-chat'); if(!chat) return; if(!_currentConvId){ chat.innerHTML = '
👋 点左侧「+ 新建会话」开始,' + '给 AI 下达指令(先选 🎯 目标设备)。
'; return; } apiGet('/api/agent/conversations/'+_currentConvId).then(r=>{ if(!r||!r.ok)return; chat.innerHTML = ''; _convUsage = {}; _liveUsage = {}; (r.messages||[]).forEach(m=>{ const txt = (m.content||'').trim(); if(m.role==='user'){ if(txt) addUserMsg(txt); return; } if(!txt && !m.reasoning && !(m.usage && m.usage.total_tokens)) return; const div = newAssistantMsg(); if(txt){ div._md = txt; _renderMd(div); } if(m.reasoning){ _appendReasoning(div, m.reasoning); const box = div.querySelector('.reasoning'); _bindReasoning(box); box.open = false; // 历史消息默认折叠推理链 } if(m.usage){ renderUsage(div, m.usage); _convUsage = _usageSum(_convUsage, m.usage); } }); updateSessionTokens(); if(!chat.children.length){ chat.innerHTML = '
新会话。给 AI 下达指令' + '(先选 🎯 目标设备)…
'; } }); } // ================== 发送与流式接收 ================== 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(){ if(!confirm('确定停止当前 AI 运行任务?\n(其他人发起的任务也会被停止)')) return; 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 sel = document.getElementById('agent-target-select'); const serial = sel ? sel.value : ''; if(!serial){ showToast('请先选择目标设备(AI 只操作你选定的设备;任务中的设备不可选)','error'); if(sel) sel.focus(); return; } const inp = document.getElementById('agent-input'); const text = inp.value.trim(); if(!text){showToast('请输入指令','error');return;} inp.value = ''; addUserMsg(text); // 会话:无当前会话时自动新建(DeepSeek 式:消息总归属于一个会话) const doRun = (convId)=>{ apiPost('/api/agent/run', {prompt: text, serial: serial, conversation_id: convId}).then(r=>{ if(!r||!r.ok){ const msg = r ? (r.error||'启动失败') : '请求失败'; const div = newAssistantMsg(); div.querySelector('.agent-text').textContent = '⚠ ' + _friendlyAgentError(msg); showToast(_friendlyAgentError(msg),'error'); return; } setRunning(true); listenStream(r.run_id); }); }; if(_currentConvId){ doRun(_currentConvId); }else{ apiPost('/api/agent/conversations',{}).then(r=>{ if(r&&r.ok){ saveConvId(r.id); renderConvList(); doRun(r.id); }else{ const div = newAssistantMsg(); div.querySelector('.agent-text').textContent = '⚠ 新建会话失败'; showToast('新建会话失败','error'); } }); } } 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); if(d.kind === 'reasoning'){ _appendReasoning(msgEl, d.text || ''); }else{ // 正文开始 → 收起推理链(用户手动开过的保持原样) const r = msgEl.querySelector('.reasoning'); if(r && r.open && !r.dataset.touched) r.open = false; msgEl._md = (msgEl._md || '') + (d.text || ''); msgEl._mdPending = false; _scheduleMd(msgEl); } scrollChat(); }); // 每次模型调用完成 → 实时刷新 token 计数(本轮累计) es.addEventListener('usage', ev=>{ if(!msgEl) msgEl = newAssistantMsg(); let d = {}; try{ d = JSON.parse(ev.data) || {}; }catch(e){} _liveUsage = d; renderUsage(msgEl, d); updateSessionTokens(); }); 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'; const argsTxt = typeof d.args === 'string' ? d.args : JSON.stringify(d.args || {}); card.innerHTML = '' + '' + esc(d.tool||'') + '' + '' + esc(argsTxt) + ''; 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 || d.usage || !msgEl){ if(!msgEl) msgEl = newAssistantMsg(); if(d.answer){ msgEl._md = d.answer; _renderMd(msgEl); } if(d.usage) renderUsage(msgEl, d.usage); } // 本轮用量并入会话累计(脚注已展示,顶栏不该因此变小),再清空「运行中」 _convUsage = _usageSum(_convUsage, _liveUsage); _liveUsage = {}; updateSessionTokens(); scrollChat(); endRun(); renderConvList(); // 落库已完成:刷新标题/时间/轮数 }); 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 = '⚠ ' + _friendlyAgentError(msg); _liveUsage = {}; // 本轮可能已花费 token,但结果无效——不并入会话累计 endRun(); }); es.onerror = ()=>{ // 重要:不能在这里 endRun()——断网/后台标签页节流/服务端瞬时抖动都会触发 // onerror,但后端任务仍在跑。EventSource 会自动重连,服务端事件队列保留 // 积压(delta/step/done),重连成功后全部补发,界面无缝续上。 // 只有连接彻底关闭(非自动重连态)且任务还在跑时才提示。 if(_agentBusy && es.readyState === EventSource.CLOSED){ const hint = document.getElementById('agent-target-hint'); if(hint) hint.textContent = '⚠ 实时连接已中断,任务仍在后台运行——可刷新页面恢复查看'; // 连接彻底失败时周期探测,恢复后重订阅 setTimeout(()=>{ if(!_agentStream && _agentBusy){ apiGet('/api/agent/run').then(r=>{ if(r&&r.ok&&r.state==='running'&&r.run_id) listenStream(r.run_id); }); } }, 5000); } }; } // ============ 实时画面(右侧,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(args){ // 工具 args(后端 SSE 已序列化为对象;兼容历史字符串格式) let a = args; if(typeof args === 'string'){ try{ a = JSON.parse(args); }catch(e){ return; } } if(a && a.serial) showLive(a.serial); } 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; // items 带设备名称(设备身份);老接口只有 serial 列表时兜底 const items = (r.items && r.items.length ? r.items : (r.devices||[]).map(x=>({serial:x, name:''}))) .filter(x=>String(x.serial||'').indexOf(':')>=0); sel.innerHTML = '' + items.map(x=>'').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(); } // ================== 动作库(可复用动作:查看/编辑/删除/新建) ================== let _actCache = []; function openActLib(){ document.getElementById('agent-act-overlay').style.display = 'flex'; loadActLib(); } function closeActLib(){ document.getElementById('agent-act-overlay').style.display = 'none'; } function _actStepBrief(a){ return (a.steps||[]).map(s=>{ const p = s.params || {}; const loc = p.selector_value || p.package || p.fixed_text || ''; return (s.type || '?') + (loc ? (':' + loc) : ''); }).join(' → '); } function loadActLib(){ const list = document.getElementById('act-lib-list'); apiGet('/api/agent/actions').then(r=>{ if(!r||!r.ok){ showToast((r&&r.error)||'加载失败','error'); return; } _actCache = r.actions || []; document.getElementById('act-lib-hint').textContent = '共 ' + _actCache.length + ' 个动作'; if(!_actCache.length){ list.innerHTML = '
暂无动作——跑一轮以元素/文字点击为主的任务后会自动沉淀
'; return; } list.innerHTML = ''; _actCache.forEach(a=>{ const card = document.createElement('div'); card.style.cssText = 'border:1px solid var(--card-line);border-radius:10px;padding:9px 13px;background:#0d1117'; card.innerHTML = '
' + '
' + '
' + esc(a.name||'') + (a.app ? '' + esc(a.app) + '' : '') + '
' + '
' + esc(_actStepBrief(a) || '—') + '
' + '
别名: ' + esc((a.aliases||[]).join('、')||'—') + ' | 命中 ' + (a.hits||0) + ' 次 | ' + esc(a.updated_at||'') + '
' + '
' + '
' + '' + '' + '
'; list.appendChild(card); }); }); } function actDelete(id){ if(!confirm('确认删除该动作?删除后同类任务不再自动复用它。')) return; apiPost('/api/agent/actions/delete',{id:id}).then(r=>{ showToast((r&&(r.msg||r.error))||'删除失败', (r&&r.ok)?'success':'error'); loadActLib(); }); } function actNew(){ _actForm({name:'', app:'', aliases:[], params:[], steps:[]}); } function actEdit(id){ const a = _actCache.find(x=>x.id===id); if(a) _actForm(a); } function _actForm(a){ const list = document.getElementById('act-lib-list'); const isNew = !a.id; list.innerHTML = ''; const box = document.createElement('div'); box.style.cssText = 'border:1px solid var(--card-line);border-radius:10px;padding:12px 14px;background:#0d1117'; box.innerHTML = '
' + (isNew?'新建动作':'编辑动作 #'+a.id) + '
' + '
' + '' + '' + '
' + '
' + '' + '' + '
' + '' + '
可用 type:open_app/click/input_text/swipe/wait/key_event/group…;click 必须有 selector_value(xpath/text/resourceId/description…);不接受坐标(click_xy)。
' + '
' + '
'; list.appendChild(box); } function actSave(id){ const g = (i)=>document.getElementById(i).value; const split = (s)=>String(s||'').split(/[,,]/).map(x=>x.trim()).filter(x=>x); let steps; try{ steps = JSON.parse(g('act-f-steps')||'[]'); } catch(e){ showToast('steps 不是合法 JSON: '+e.message,'error'); return; } apiPost('/api/agent/actions/save', { id: id||0, name: g('act-f-name'), app: g('act-f-app'), aliases: split(g('act-f-alias')), params: split(g('act-f-params')), steps: steps }).then(r=>{ if(!r||!r.ok){ showToast((r&&r.error)||'保存失败','error'); return; } showToast(r.msg||'已保存','success'); loadActLib(); }); } // ================== 经验库(自进化记忆管理) ================== 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'); }); }