fix: 补回 servers.html 丢失的检测路径+卸载Agent功能

从 worktree 分支同步完整的 servers.html,补回:
- 批量操作栏"检测路径"按钮(搜索 workerman.bat 自动设置 target_path)
- 批量操作栏"卸载 Agent"按钮
- batchDetectPath() 和 batchUninstallAgent() 函数

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Your Name
2026-05-29 16:56:23 +08:00
parent 74de9d303e
commit 3c7036babf
+78 -19
View File
@@ -21,7 +21,7 @@
</header>
<main class="flex-1 overflow-y-auto p-6">
<div id="serversTable" class="bg-slate-900 rounded-xl border border-slate-800 overflow-hidden">
<table class="w-full text-sm"><thead class="bg-slate-800/50 text-slate-400 text-xs uppercase"><tr><th class="px-4 py-3 w-10"><input type="checkbox" id="selectAll" onchange="toggleSelectAll(this.checked)" class="rounded border-slate-600 bg-slate-700 text-brand focus:ring-brand"></th><th class="text-left px-4 py-3">状态</th><th class="text-left px-4 py-3">名称</th><th class="text-left px-4 py-3">地址</th><th class="text-left px-4 py-3">分类</th><th class="text-left px-4 py-3">Agent</th><th class="text-left px-4 py-3">最后心跳</th><th class="text-right px-4 py-3">操作</th></tr></thead><tbody id="serversTbody" class="divide-y divide-slate-800"><tr><td colspan="8" class="px-4 py-8 text-center text-slate-500">加载中...</td></tr></tbody></table>
<table class="w-full text-sm"><thead class="bg-slate-800/50 text-slate-400 text-xs uppercase"><tr><th class="px-4 py-3 w-10"><input type="checkbox" id="selectAll" onchange="toggleSelectAll(this.checked)" class="rounded border-slate-600 bg-slate-700 text-brand focus:ring-brand"></th><th class="text-left px-4 py-3">Agent 状态</th><th class="text-left px-4 py-3">名称</th><th class="text-left px-4 py-3">地址</th><th class="text-left px-4 py-3">分类</th><th class="text-left px-4 py-3">Agent</th><th class="text-left px-4 py-3">最后心跳</th><th class="text-right px-4 py-3">操作</th></tr></thead><tbody id="serversTbody" class="divide-y divide-slate-800"><tr><td colspan="8" class="px-4 py-8 text-center text-slate-500">加载中...</td></tr></tbody></table>
</div>
<div class="mt-4 flex items-center justify-between text-xs text-slate-500"><span id="serverCount">共 -- 台</span><button onclick="loadServers()" class="px-3 py-1 bg-slate-800 hover:bg-slate-700 rounded-lg transition">刷新</button></div>
<!-- Batch Action Bar -->
@@ -34,6 +34,8 @@
<button onclick="batchCheck()" class="px-4 py-1.5 bg-emerald-600/80 hover:bg-emerald-600 text-white text-sm rounded-lg transition">健康检查</button>
<button id="btnBatchInstall" onclick="batchInstallAgent()" class="px-4 py-1.5 bg-amber-600/80 hover:bg-amber-600 text-white text-sm rounded-lg transition disabled:opacity-30 disabled:cursor-not-allowed disabled:hover:bg-amber-600/80">安装 Agent</button>
<button id="btnBatchUpgrade" onclick="batchUpgradeAgent()" class="px-4 py-1.5 bg-cyan-600/80 hover:bg-cyan-600 text-white text-sm rounded-lg transition disabled:opacity-30 disabled:cursor-not-allowed disabled:hover:bg-cyan-600/80">升级 Agent</button>
<button id="btnBatchDetect" onclick="batchDetectPath()" class="px-4 py-1.5 bg-violet-600/80 hover:bg-violet-600 text-white text-sm rounded-lg transition disabled:opacity-30 disabled:cursor-not-allowed disabled:hover:bg-violet-600/80">检测路径</button>
<button id="btnBatchUninstall" onclick="batchUninstallAgent()" class="px-4 py-1.5 bg-red-700/80 hover:bg-red-700 text-white text-sm rounded-lg transition disabled:opacity-30 disabled:cursor-not-allowed disabled:hover:bg-red-700/80">卸载 Agent</button>
<button onclick="batchPush()" class="px-4 py-1.5 bg-brand hover:bg-brand-dark text-white text-sm rounded-lg transition">批量推送</button>
<button onclick="batchDelete()" class="px-4 py-1.5 bg-red-600/80 hover:bg-red-600 text-white text-sm rounded-lg transition">批量删除</button>
</div>
@@ -160,9 +162,10 @@
<div class="bg-slate-900 border border-slate-700 rounded-xl p-6 w-full max-w-lg space-y-3">
<h2 class="text-white font-semibold text-lg">批量导入服务器</h2>
<div class="bg-slate-800/50 rounded-lg p-3 text-xs text-slate-400 space-y-1">
<p>1. <a href="javascript:void(0)" onclick="downloadCsvTemplate()" class="text-brand-light underline">下载 CSV 模板</a></p>
<p>2. 填写服务器信息(名称和地址为必填列</p>
<p>3. 上传 CSV 文件,最多 500 行</p>
<p>1. <a href="/app/servers_import_template.csv" download="servers_import_template.csv" class="text-brand-light underline">下载 CSV 模板</a></p>
<p>2. 按模板格式填写(<span class="text-slate-200">名称</span><span class="text-slate-200">地址</span>必填,其余选填</p>
<p>3. 认证方式填 <span class="text-slate-200">password</span><span class="text-slate-200">key</span>,默认 password</p>
<p>4. 上传 CSV 文件,最多 500 行</p>
</div>
<div>
<label class="block text-slate-400 text-xs mb-1">选择 CSV 文件</label>
@@ -250,7 +253,7 @@
if(!servers.length){tbody.innerHTML='<tr><td colspan="8" class="px-4 py-8 text-center text-slate-500">暂无服务器</td></tr>';return}
tbody.innerHTML=servers.map(s=>`<tr class="hover:bg-slate-800/30 transition cursor-pointer ${selectedServerId===s.id?'bg-brand/5':''}" onclick="selectServer(${s.id})" id="row-${s.id}">
<td class="px-4 py-3" onclick="event.stopPropagation()"><input type="checkbox" class="rounded border-slate-600 bg-slate-700 text-brand focus:ring-brand srv-chk" data-id="${s.id}" onchange="toggleSelect(${s.id},this.checked)" ${_selectedIds.has(s.id)?'checked':''}></td>
<td class="px-4 py-3"><span class="inline-block w-2.5 h-2.5 rounded-full ${s.is_online?'bg-green-400':'bg-red-400'} ${s.is_online?'':'animate-pulse'}"></span></td>
<td class="px-4 py-3">${s.agent_version?(s.is_online?'<span class="inline-flex items-center gap-1.5 text-xs"><span class="inline-block w-2 h-2 rounded-full bg-green-400"></span><span class="text-green-400">在线</span></span>':'<span class="inline-flex items-center gap-1.5 text-xs"><span class="inline-block w-2 h-2 rounded-full bg-red-400 animate-pulse"></span><span class="text-red-400">离线</span></span>'):'<span class="inline-flex items-center gap-1.5 text-xs"><span class="inline-block w-2 h-2 rounded-full bg-slate-600"></span><span class="text-slate-500">未安装</span></span>'}</td>
<td class="px-4 py-3 text-white font-medium">${esc(s.name)}${driftBadge(s)}</td>
<td class="px-4 py-3 text-slate-400">${esc(s.domain)}:${s.port||22}</td>
<td class="px-4 py-3 text-slate-400">${esc(s.category||'--')}</td>
@@ -415,6 +418,10 @@
${s.is_online?`<button onclick="upgradeAgent()" id="upgradeAgentBtn"
class="px-3 py-2 bg-slate-700 hover:bg-slate-600 text-white text-sm rounded-lg transition" title="下载最新 agent.py 并重启服务">
⬆ 升级 Agent
</button>
<button onclick="uninstallAgent()" id="uninstallAgentBtn"
class="px-3 py-2 bg-red-900/60 hover:bg-red-800 text-white text-sm rounded-lg transition" title="停止 Agent 服务并删除安装目录">
🗑 卸载 Agent
</button>`:''}
<span class="text-slate-500 text-xs">安装约需 30-60 秒;安装完成后 Agent 发送首次心跳约需再等 60 秒,状态徽章将自动更新为 <span class="text-green-400">● Agent 在线</span></span>
</div>
@@ -477,6 +484,32 @@
finally{if(btn){btn.disabled=false;btn.textContent='⬆ 升级 Agent'}}
}
async function uninstallAgent(){
if(!selectedServerId)return;
if(!confirm('确定卸载该服务器的 Agent\n将停止服务、删除 /opt/nexus-agent 目录和日志文件,并清除 Agent 状态。'))return;
const btn=document.getElementById('uninstallAgentBtn');
if(btn){btn.disabled=true;btn.textContent='卸载中...'}
const logEl=document.getElementById('remoteInstallLog');
const logText=document.getElementById('remoteInstallLogText');
logEl?.classList.remove('hidden');
logText.textContent='正在停止 Agent 服务并清理安装目录...\n';
try{
const r=await apiFetch(API+'/servers/'+selectedServerId+'/uninstall-agent',{method:'POST',headers:apiHeadersJSON()});
if(!r){logText.textContent+='请求失败\n';return}
const d=await r.json();
if(r.ok&&d.success){
logText.textContent+=d.stdout||'';
logText.textContent+='\n✓ Agent 卸载成功!';
toast('success','Agent 卸载成功');
setTimeout(loadServers,1000);
}else{
logText.textContent+=(d.detail||'卸载失败')+'\n';
toast('error','卸载失败');
}
}catch(e){logText.textContent+='错误: '+e.message;toast('error','卸载请求失败')}
finally{if(btn){btn.disabled=false;btn.textContent='🗑 卸载 Agent'}}
}
function copyAgentCmd(){
const cmd=document.getElementById('agentCmdText')?.textContent;
if(cmd){
@@ -847,8 +880,12 @@
});
const btnInstall=document.getElementById('btnBatchInstall');
const btnUpgrade=document.getElementById('btnBatchUpgrade');
const btnDetect=document.getElementById('btnBatchDetect');
const btnUninstall=document.getElementById('btnBatchUninstall');
if(btnInstall) btnInstall.disabled=allInstalled; // all have agent → disable install
if(btnUpgrade) btnUpgrade.disabled=allNotInstalled; // none have agent → disable upgrade
if(btnDetect) btnDetect.disabled=allNotInstalled; // need agent for SSH connection
if(btnUninstall) btnUninstall.disabled=allNotInstalled; // need agent for uninstall
}
function batchPush(){
if(!_selectedIds.size){toast('warning','请先选择服务器');return}
@@ -901,18 +938,6 @@
}
// ── CSV Import ──
async function downloadCsvTemplate(){
try{
const r=await apiFetch(API+'/servers/import/template');
if(!r||!r.ok){toast('error','下载模板失败');return}
const blob=await r.blob();
const url=URL.createObjectURL(blob);
const a=document.createElement('a');a.href=url;a.download='servers_import_template.csv';
document.body.appendChild(a);a.click();document.body.removeChild(a);
URL.revokeObjectURL(url);
toast('success','模板已下载');
}catch(e){toast('error','下载失败: '+e.message)}
}
function showImportModal(){
document.getElementById('importFile').value='';
document.getElementById('importProgress').classList.add('hidden');
@@ -990,6 +1015,40 @@
}catch(e){toast('error','批量升级失败: '+e.message);hideBatchAgentModal()}
}
async function batchDetectPath(){
if(!_selectedIds.size){toast('warning','请先选择服务器');return}
const ids=[..._selectedIds];
if(!confirm(`确定对 ${ids.length} 台服务器自动检测 target_path\n将在 /www/wwwroot 下搜索 workerman.bat,找到后自动设置目标路径。`))return;
document.getElementById('batchAgentTitle').textContent='自动检测路径';
document.getElementById('batchAgentModal').classList.remove('hidden');
document.getElementById('batchAgentProgress').classList.remove('hidden');
document.getElementById('batchAgentResult').classList.add('hidden');
try{
const r=await apiFetch(API+'/servers/batch/detect-path',{method:'POST',headers:apiHeadersJSON(),body:JSON.stringify({server_ids:ids})});
if(!r){toast('error','请求失败');return}
const data=await r.json();
_showBatchAgentResult(data.results||[]);
loadServers();
}catch(e){toast('error','检测失败: '+e.message);hideBatchAgentModal()}
}
async function batchUninstallAgent(){
if(!_selectedIds.size){toast('warning','请先选择服务器');return}
const ids=[..._selectedIds];
if(!confirm(`确定对 ${ids.length} 台已安装 Agent 的服务器执行卸载?\n将停止服务、删除 /opt/nexus-agent 目录和日志文件,并清除 Agent 状态。`))return;
document.getElementById('batchAgentTitle').textContent='批量卸载 Agent';
document.getElementById('batchAgentModal').classList.remove('hidden');
document.getElementById('batchAgentProgress').classList.remove('hidden');
document.getElementById('batchAgentResult').classList.add('hidden');
try{
const r=await apiFetch(API+'/servers/batch/uninstall-agent',{method:'POST',headers:apiHeadersJSON(),body:JSON.stringify({server_ids:ids})});
if(!r){toast('error','请求失败');return}
const data=await r.json();
_showBatchAgentResult(data.results||[]);
loadServers();
}catch(e){toast('error','卸载失败: '+e.message);hideBatchAgentModal()}
}
function _showBatchAgentResult(results){
document.getElementById('batchAgentProgress').classList.add('hidden');
document.getElementById('batchAgentResult').classList.remove('hidden');
@@ -999,8 +1058,8 @@
document.getElementById('batchAgentFail').textContent=fail;
const details=document.getElementById('batchAgentDetails');
details.innerHTML=results.map(r=>{
if(r.success)return `<div class="flex items-center gap-2 text-xs text-emerald-400"><span>✓</span><span>${esc(r.server_name)}</span></div>`;
return `<div class="flex items-center gap-2 text-xs text-red-400"><span>✗</span><span>${esc(r.server_name)}</span><span class="text-slate-500 truncate">${esc(r.error)}</span></div>`;
if(r.success)return `<div class="flex items-center gap-2 text-xs text-emerald-400"><span>✓</span><span>${esc(r.server_name)}</span>${r.stdout?`<span class="text-slate-400">${esc(r.stdout)}</span>`:''}</div>`;
return `<div class="flex items-center gap-2 text-xs text-red-400"><span>✗</span><span>${esc(r.server_name)}</span><span class="text-slate-500 truncate" title="${esc(r.error)}">${esc(r.error)}</span></div>`;
}).join('');
}