From 2ed7ccd20dda1b8fcb8a2c796b5512138125ecfd Mon Sep 17 00:00:00 2001 From: liming <827182486@qq.com> Date: Sat, 15 Aug 2026 23:15:36 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20=E9=85=8D=E7=BD=AE=E9=A1=B5?= =?UTF-8?q?=E7=94=9F=E6=88=90=E5=AE=A2=E6=88=B7=E7=AB=AF=E5=AE=89=E8=A3=85?= =?UTF-8?q?=E5=91=BD=E4=BB=A4=EF=BC=8C=E6=94=AF=E6=8C=81=20Shell/Docker=20?= =?UTF-8?q?Compose/Docker=20Run?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/css/app.css | 10 +++++ web/index.html | 5 ++- web/js/app.js | 117 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 130 insertions(+), 2 deletions(-) diff --git a/web/css/app.css b/web/css/app.css index bfd7e2ad..b3138efd 100644 --- a/web/css/app.css +++ b/web/css/app.css @@ -367,6 +367,16 @@ th[data-sort].sorted-desc:after{border-top-color:var(--accent);opacity:1} .config-form input[name=password],.config-form textarea[name=rule]{font-family:ui-monospace,SFMono-Regular,Menlo,monospace} .config-form button:disabled{opacity:.5;cursor:not-allowed} .config-editor-card{scroll-margin-top:70px} +.client-cmd{display:flex;flex-direction:column;gap:.6rem;border:1px dashed var(--border);border-radius:8px;background:var(--bg);padding:.75rem} +.client-cmd-head{display:flex;align-items:center;justify-content:space-between;gap:.5rem} +.client-cmd-head strong{font-size:13px} +.client-cmd-head .icon-text{width:auto;flex:none} +.client-cmd-addr{display:flex;flex-direction:column;gap:.3rem;color:var(--text-dim);font-size:12px} +.client-cmd-addr input{height:36px;border:1px solid var(--border);background:var(--bg-alt);color:var(--text);border-radius:6px;padding:0 .65rem;outline:none;font-family:ui-monospace,SFMono-Regular,Menlo,monospace} +.client-cmd-addr input:focus{border-color:var(--accent);box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 18%,transparent)} +.client-cmd-text{margin:0;padding:.6rem .7rem;border-radius:6px;background:var(--bg-alt);color:var(--text);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;line-height:1.55;white-space:pre-wrap;word-break:break-all;max-height:220px;overflow-y:auto} +.client-cmd-methods{width:100%} +.client-cmd-methods button{flex:1;padding:0 .4rem;white-space:nowrap} @media (min-width:981px){ .config-editor-card{position:sticky;top:70px;max-height:calc(100dvh - 86px);overflow-y:auto;scrollbar-gutter:stable} diff --git a/web/index.html b/web/index.html index fa844a5c..eea7ae25 100644 --- a/web/index.html +++ b/web/index.html @@ -6,7 +6,7 @@ 云监控 - +
@@ -189,6 +189,7 @@

新增节点

+ @@ -207,6 +208,6 @@ ServerStatus中文版 - + diff --git a/web/js/app.js b/web/js/app.js index b52ed9bf..a3895c09 100644 --- a/web/js/app.js +++ b/web/js/app.js @@ -28,6 +28,9 @@ const S = { enabled: false, connected: false, config: null, + agentAddr: '', + clientServer: '', + clientCmdMethod: 'shell', selectedType: 'servers', selectedIndex: -1, queries: { servers:'', monitors:'', sslcerts:'', watchdog:'' }, @@ -814,6 +817,7 @@ async function ensureAdminChecked(){ try{ const health = await api('/api/health', { auth:false }); S.admin.enabled = !!health.enabled; + S.admin.agentAddr = health.agent?.address || ''; setAdminStatus(health.enabled ? '管理 API 已启用,输入 token 后可编辑配置。' : '管理 API 未启用:请在容器环境变量设置 ADMIN_TOKEN。', health.enabled ? '' : 'err'); if(S.admin.enabled && S.admin.token) await loadConfig(); }catch(err){ @@ -919,6 +923,55 @@ function normalizeAdminConfig(config){ }); return normalized; } +function agentPort(){ + const addr = S.admin.agentAddr || ''; + const idx = addr.lastIndexOf(':'); + const port = idx >= 0 ? addr.slice(idx + 1) : addr; + return /^\d+$/.test(port) ? port : '35601'; +} +function defaultClientServer(){ + return window.location.hostname || '127.0.0.1'; +} +function currentClientServer(){ + if(!S.admin.clientServer) S.admin.clientServer = defaultClientServer(); + return S.admin.clientServer; +} +function shellSafe(v){ + const s = String(v ?? ''); + if(!s) return "''"; + return /^[A-Za-z0-9_@:./-]+$/.test(s) ? s : "'" + s.replace(/'/g, "'\\''") + "'"; +} +function clientInstallCommand(user, pass){ + const server = shellSafe(currentClientServer()); + const port = shellSafe(agentPort()); + const userS = shellSafe(user); + const passS = shellSafe(pass); + const method = S.admin.clientCmdMethod || 'shell'; + if(method === 'compose'){ + return [ + `wget -qO docker-compose-client.yml --header='Accept: application/vnd.github.raw' \\`, + ` 'https://api.github.com/repos/cppla/ServerStatus/contents/docker-compose-client.yml?ref=master'`, + `SERVER=${server} PORT=${port} USER=${userS} PASSWORD=${passS} \\`, + ` docker compose -f docker-compose-client.yml up -d --force-recreate`, + ].join('\n'); + } + if(method === 'run'){ + return [ + `docker run -d --restart=always --name=serverstatus-client \\`, + ` --network=host --pid=host \\`, + ` -e SERVER=${server} \\`, + ` -e PORT=${port} \\`, + ` -e USER=${userS} \\`, + ` -e PASSWORD=${passS} \\`, + ` cppla/serverstatus:client`, + ].join('\n'); + } + return [ + `wget -qO client-linux.py --header='Accept: application/vnd.github.raw' \\`, + ` 'https://api.github.com/repos/cppla/ServerStatus/contents/clients/client-linux.py?ref=master'`, + `nohup python3 client-linux.py SERVER=${server} PORT=${port} USER=${userS} PASSWORD=${passS} >/dev/null 2>&1 &` + ].join('\n'); +} function activeConfigDef(){ return CONFIG_TYPES[S.admin.selectedType] || CONFIG_TYPES.servers; } @@ -991,6 +1044,37 @@ function renderConfigEditor(item){ resetTrafficBtn.style.display = canResetTraffic ? '' : 'none'; resetTrafficBtn.disabled = !canResetTraffic || S.admin.saving; $('deleteConfigItemBtn').disabled = !editing; + renderClientCmd(); +} +function renderClientCmd(){ + const box = $('clientCmd'); + if(!box) return; + if(S.admin.selectedType !== 'servers'){ box.style.display = 'none'; return; } + box.style.display = ''; + box.innerHTML = [ + '
', + '客户端安装命令', + '', + '
', + '
', + '', + '', + '', + '
', + '', + '
'
+  ].join('');
+  $('clientCmdServer').value = currentClientServer();
+  document.querySelectorAll('#clientCmdMethods [data-method]').forEach(btn => btn.classList.toggle('active', btn.dataset.method === (S.admin.clientCmdMethod || 'shell')));
+  refreshClientCmd();
+}
+function refreshClientCmd(){
+  const textEl = $('clientCmdText');
+  if(!textEl) return;
+  const form = $('configForm').elements;
+  const user = form.username ? form.username.value.trim() : '';
+  const pass = form.password ? form.password.value : '';
+  textEl.textContent = clientInstallCommand(user, pass);
 }
 function fieldHTML(field, item){
   const value = item[field.name] ?? field.default ?? '';
@@ -1118,6 +1202,39 @@ function bindAdmin(){
   });
   $('resetConfigFormBtn').addEventListener('click', clearConfigForm);
   $('resetTrafficBtn').addEventListener('click', () => resetServerTraffic(S.admin.selectedIndex));
+  $('clientCmd').addEventListener('input', e => {
+    if(e.target.id === 'clientCmdServer'){
+      S.admin.clientServer = e.target.value.trim();
+      refreshClientCmd();
+    }
+  });
+  $('clientCmd').addEventListener('click', async e => {
+    const methodBtn = e.target.closest('#clientCmdMethods [data-method]');
+    if(methodBtn){
+      S.admin.clientCmdMethod = methodBtn.dataset.method;
+      document.querySelectorAll('#clientCmdMethods [data-method]').forEach(b => b.classList.toggle('active', b === methodBtn));
+      refreshClientCmd();
+      return;
+    }
+    const btn = e.target.closest('#copyClientCmdBtn');
+    if(!btn) return;
+    const text = $('clientCmdText')?.textContent || '';
+    if(!text) return;
+    try{
+      await navigator.clipboard.writeText(text);
+    }catch(_err){
+      const ta = document.createElement('textarea');
+      ta.value = text;
+      document.body.appendChild(ta);
+      ta.select();
+      document.execCommand('copy');
+      ta.remove();
+    }
+    const prev = btn.textContent;
+    btn.textContent = '已复制';
+    setTimeout(() => { btn.textContent = prev; }, 1200);
+  });
+  $('configForm').addEventListener('input', () => { if(S.admin.selectedType === 'servers') refreshClientCmd(); });
   $('adminReload').addEventListener('click', async () => {
     try{ await api('/api/reload', { method:'POST' }); setAdminStatus('配置重载已触发。', 'ok'); }
     catch(err){ setAdminStatus('重载失败:' + err.message, 'err'); }