feat: AI 控制台升级为顶级 Tab——DeepSeek 风格聊天界面(气泡+流式渲染+思考折叠),实时 MCP 步骤卡片(工具/参数/截图缩略),SSE 流式输出(delta/step/done/error 事件),配置模态(模型/Key/设备前端可配);工具页旧子栏移除
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
// 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>';
|
||||
}
|
||||
// 输入框快捷键
|
||||
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();
|
||||
}
|
||||
function scrollChat(){
|
||||
const chat = document.getElementById('agent-chat');
|
||||
chat.scrollTop = chat.scrollHeight;
|
||||
}
|
||||
function clearAgentChat(){
|
||||
document.getElementById('agent-chat').innerHTML =
|
||||
'<div class="agent-empty">对话已清空。给 AI 下达新指令…</div>';
|
||||
}
|
||||
|
||||
// ================== 发送与流式接收 ==================
|
||||
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;
|
||||
}
|
||||
_agentBusy = true;
|
||||
document.getElementById('agent-running-tag').style.display = 'inline';
|
||||
document.getElementById('btn-agent-send').disabled = 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);
|
||||
if(!msgEl) msgEl = newAssistantMsg();
|
||||
const cards = msgEl.querySelector('.agent-toolcards');
|
||||
const card = document.createElement('div');
|
||||
card.className = 'agent-toolcard';
|
||||
card.innerHTML = '<span class="dot"></span>'
|
||||
+ '<code style="color:var(--teal)">' + esc(d.tool||'') + '</code>'
|
||||
+ '<span class="text-muted">' + esc(d.args||'') + '</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();
|
||||
};
|
||||
}
|
||||
|
||||
function endRun(){
|
||||
_agentBusy = false;
|
||||
_agentStream && _agentStream.close();
|
||||
_agentStream = null;
|
||||
document.getElementById('agent-running-tag').style.display = 'none';
|
||||
document.getElementById('btn-agent-send').disabled = false;
|
||||
scrollChat();
|
||||
}
|
||||
@@ -100,6 +100,7 @@ function showTab(name){
|
||||
if(name==='tasks'){showSubTab('tasks',_activeSubs.tasks);loadTasks();loadCustomActions();}
|
||||
if(name==='tools'){showSubTab('tools',_activeSubs.tools);loadToolsDevices();loadAdbDevices();loadTailscaleDevices();loadApks();}
|
||||
if(name==='logs'){loadLogs();if(document.getElementById('log-auto').checked)_logTimer=setInterval(loadLogs,3000);}
|
||||
if(name==='agent' && typeof initAgentChat==='function') initAgentChat();
|
||||
if(name==='users')loadUsers();
|
||||
}
|
||||
|
||||
@@ -114,7 +115,6 @@ function showSubTab(tabId, name){
|
||||
tab.querySelectorAll('.sub-tab').forEach(b=>b.classList.toggle('active', b.dataset.sub===name));
|
||||
tab.querySelectorAll('.sub-panel').forEach(p=>p.classList.toggle('active', p.id===tabId+'-sub-'+name));
|
||||
if(name==='groups' && typeof loadGroups==='function') loadGroups();
|
||||
if(name==='agent' && typeof loadAgentConfig==='function') loadAgentConfig();
|
||||
if(name==='devpool' && typeof loadDevPool==='function'){
|
||||
loadDevPool();
|
||||
if(typeof loadDiscovery==='function'){
|
||||
|
||||
@@ -41,89 +41,6 @@ async function loadToolsDevices(force){
|
||||
status.textContent = '共 '+_clipDevices.length+' 台设备';
|
||||
}
|
||||
|
||||
// ================== AI 控制台(模型配置 + 指令执行 + 进度轮询) ==================
|
||||
let _agentPoll = null;
|
||||
|
||||
function loadAgentConfig(){
|
||||
apiGet('/api/agent/config').then(r=>{
|
||||
if(!r||!r.ok)return;
|
||||
const b=document.getElementById('agent-api-base');
|
||||
if(!b)return; // 面板未渲染
|
||||
b.value=r.api_base||'https://api.deepseek.com';
|
||||
document.getElementById('agent-model').value=r.model||'';
|
||||
document.getElementById('agent-default-serial').value=r.default_serial||'';
|
||||
const hint=document.getElementById('agent-key-hint');
|
||||
hint.textContent=r.api_key_masked?('已配置 '+r.api_key_masked):'未配置 Key';
|
||||
});
|
||||
}
|
||||
function saveAgentConfig(){
|
||||
const body={api_base:document.getElementById('agent-api-base').value.trim(),
|
||||
model:document.getElementById('agent-model').value.trim(),
|
||||
default_serial:document.getElementById('agent-default-serial').value.trim()};
|
||||
const key=document.getElementById('agent-api-key').value.trim();
|
||||
if(key)body.api_key=key;
|
||||
apiPost('/api/agent/config',body).then(r=>{
|
||||
if(r&&r.ok){showToast('配置已保存','success');loadAgentConfig();}
|
||||
else showToast('保存失败: '+((r&&r.error)||''),'error');
|
||||
});
|
||||
}
|
||||
function runAgent(){
|
||||
const prompt=document.getElementById('agent-prompt').value.trim();
|
||||
if(!prompt){showToast('请输入指令','error');return;}
|
||||
apiPost('/api/agent/run',{prompt}).then(r=>{
|
||||
if(r&&r.ok){
|
||||
showToast('Agent 已启动','success');
|
||||
document.getElementById('agent-status').textContent='运行中...';
|
||||
document.getElementById('btn-agent-run').disabled=true;
|
||||
document.getElementById('agent-answer-wrap').style.display='none';
|
||||
startAgentPoll();
|
||||
}else showToast('启动失败: '+((r&&r.error)||''),'error');
|
||||
});
|
||||
}
|
||||
function startAgentPoll(){
|
||||
if(_agentPoll)clearInterval(_agentPoll);
|
||||
_agentPoll=setInterval(pollAgent,2000);
|
||||
pollAgent();
|
||||
}
|
||||
function stopAgentPoll(){
|
||||
if(_agentPoll){clearInterval(_agentPoll);_agentPoll=null;}
|
||||
}
|
||||
function pollAgent(){
|
||||
apiGet('/api/agent/status').then(r=>{
|
||||
if(!r||!r.ok)return;
|
||||
const st=document.getElementById('agent-status');
|
||||
if(r.state==='running'){
|
||||
st.textContent='运行中... ('+(r.steps||[]).length+' 步)';
|
||||
}else{
|
||||
st.textContent=r.state==='done'?'完成':'失败';
|
||||
document.getElementById('btn-agent-run').disabled=false;
|
||||
stopAgentPoll();
|
||||
}
|
||||
// 步骤流(含截图缩略)
|
||||
const steps=document.getElementById('agent-steps');
|
||||
const html=(r.steps||[]).map(s=>{
|
||||
const args=esc(s.args||'');
|
||||
const img=s.image
|
||||
?'<img src="data:image/jpeg;base64,'+s.image+'" style="max-width:120px;border-radius:6px;margin-top:4px;display:block">':'';
|
||||
return '<div style="padding:6px 0;border-bottom:1px solid var(--ink-line)">'
|
||||
+'<code style="color:var(--teal)">'+esc(s.tool||'')+'</code> '
|
||||
+'<span class="text-muted">'+args+'</span>'+img+'</div>';
|
||||
}).join('')||'';
|
||||
steps.innerHTML=html;
|
||||
// 最终回答
|
||||
if(r.answer){
|
||||
const wrap=document.getElementById('agent-answer-wrap');
|
||||
wrap.style.display='block';
|
||||
document.getElementById('agent-answer').textContent=r.answer;
|
||||
}
|
||||
if(r.error){
|
||||
st.textContent='错误: '+r.error;
|
||||
document.getElementById('btn-agent-run').disabled=false;
|
||||
stopAgentPoll();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ================== 设备自动发现(扫描 → 待连接池 → 确认) ==================
|
||||
function loadDiscovery(){
|
||||
apiGet('/api/devices/discovery').then(r=>{
|
||||
|
||||
Reference in New Issue
Block a user