Files
auto_control/static/admin/agent.js
T

387 lines
14 KiB
JavaScript
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.
// 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 = '<div class="agent-empty">👋 给 AI 下达指令,它将通过截图观察手机并执行操作。<br>'
+ '例如:「打开抖音搜索奚学东,告诉我第一个视频的标题」<br>'
+ '点击右上角 ⚙ 配置模型与 API Key。</div>';
}
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 = '<div class="agent-toolcards"></div><div class="agent-text"></div>';
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 =
'<div class="agent-empty">对话已清空。给 AI 下达新指令…</div>';
}
// 新建会话:清后端历史 + 清前端对话区(像 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';
const argsTxt = typeof d.args === 'string' ? d.args
: JSON.stringify(d.args || {});
card.innerHTML = '<span class="dot"></span>'
+ '<code style="color:var(--teal)">' + esc(d.tool||'') + '</code>'
+ '<span class="text-muted">' + esc(argsTxt) + '</span>';
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(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;
const devs = (r.devices||[]).filter(x=>x.indexOf(':')>=0);
sel.innerHTML = '<option value="">选择设备观看…</option>'
+ devs.map(s=>'<option value="'+esc(s)+'"'+(s===cur?' selected':'')+'>'+esc(s)+'</option>').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 = '<div class="text-muted" style="padding:20px;text-align:center">🔍 AI 巡检进行中(约 1 分钟),完成后请刷新…</div>';
return;
}
const exps = r.experiences||[];
if(!exps.length){
list.innerHTML = '<div class="text-muted" style="padding:20px;text-align:center">暂无经验——跑一轮带手机操作的任务后会自动沉淀</div>';
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 = '<span style="background:#7f1d1d;color:#fecaca;font-size:10.5px;padding:1px 8px;border-radius:9px;margin-left:8px">⚠ 建议删除</span>';
}else if(a.action === 'kept'){
badge = '<span style="background:#0f3d2e;color:#a7f3d0;font-size:10.5px;padding:1px 8px;border-radius:9px;margin-left:8px">' +
(a.verdict === 'delete' ? '已人工保留' : '✓ 已保留') + '</span>';
}
let actBtns = '';
if(a.action === 'pending'){
actBtns = '<button class="btn btn-xs btn-danger" onclick="expDelete(' + e.id + ')">确认删除</button>'
+ '<button class="btn btn-xs" onclick="expKeep(' + e.id + ')">保留</button>';
}else{
actBtns = '<button class="btn btn-xs" style="border-color:#7f1d1d;color:#f87171" onclick="expDelete(' + e.id + ')" title="强制删除此经验">删除</button>';
}
card.innerHTML =
'<div style="display:flex;align-items:center;gap:6px;font-size:12px">'
+ '<span class="text-muted" style="font-family:var(--mono)">#' + e.id + '</span>'
+ '<span class="text-muted">' + esc(e.created_at||'') + '</span>'
+ '<span class="text-muted">引用 ' + (e.hits||0) + ' 次</span>' + badge
+ '<span style="flex:1"></span>' + actBtns + '</div>'
+ '<div style="font-size:13px;color:#e5e7eb;margin:5px 0 3px">' + esc(e.task_prompt) + '</div>'
+ '<div class="text-muted" style="font-size:11.5px;line-height:1.6;white-space:pre-wrap">' + esc((e.recipe||'').slice(0,220)) + '</div>'
+ (a.action === 'pending' && a.reason
? '<div style="font-size:11.5px;color:#fbbf24;margin-top:5px">AI 建议理由:' + esc(a.reason)
+ (a.score ? '(评分 ' + a.score + '/10)' : '') + '</div>'
: '');
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 =
'<div class="text-muted" style="padding:20px;text-align:center">🔍 AI 巡检进行中…</div>';
// 完成后自动刷新
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');
});
}