199 lines
6.5 KiB
JavaScript
199 lines
6.5 KiB
JavaScript
// 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();
|
|
}
|