feat: DevTools 检测 Docker 运行的服务并禁用本地操作

- process-manager: 新增 detectDockerServices() 通过 docker ps 匹配端口,
  getStatus() 返回 source 字段 (docker/local/none) 和容器名
- process-manager: Docker 服务拒绝 start/stop/restart/build,
  批量操作自动跳过 Docker 服务
- index.js: Docker 管理服务返回 409 Conflict
- UI: Docker 服务显示蓝色 "🐳 Docker" badge + 容器名,
  隐藏操作按钮并提示 "请使用 docker compose"

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-27 18:37:48 +08:00
parent ee3c851d17
commit aac64ed8b7
3 changed files with 176 additions and 25 deletions
+35 -14
View File
@@ -184,6 +184,8 @@ tr.expanded td { background: var(--bg3); }
.badge-stopped, .badge-error { background: var(--red-bg); color: var(--red); }
.badge-starting, .badge-building, .badge-thinking { background: var(--blue-bg); color: var(--blue); }
.badge-streaming { background: var(--yellow-bg); color: var(--yellow); }
.badge-docker { background: rgba(56,139,253,.12); color: #388bfd; border: 1px solid rgba(56,139,253,.3); }
.docker-hint { font-size: 11px; color: var(--text3); font-style: italic; }
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.4} }
@keyframes bluePulse { 0%,100%{box-shadow:0 0 4px var(--blue)} 50%{box-shadow:0 0 12px var(--blue)} }
@@ -1457,25 +1459,30 @@ async function updatePerformanceDashboard(perfData) {
function renderDashboardSvcCards(svcs) {
const container = document.getElementById('dashboard-svc-cards');
if (!container) return;
container.innerHTML = Object.entries(svcs).map(([id, svc]) => `
container.innerHTML = Object.entries(svcs).map(([id, svc]) => {
const isDocker = svc.source === 'docker';
return `
<div class="card" style="margin:0">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px">
<span style="font-weight:600">${svc.name}</span>
<span class="badge ${statusBadge(svc.status)}">${svc.status}</span>
${isDocker ? '<span class="badge badge-docker">🐳 Docker</span>' : `<span class="badge ${statusBadge(svc.status)}">${svc.status}</span>`}
</div>
<div class="metrics">
<div class="metric"><div class="value">${svc.pid || '—'}</div><div class="label">PID</div></div>
<div class="metric"><div class="value">${isDocker ? (svc.containerName || '—') : (svc.pid || '—')}</div><div class="label">${isDocker ? '容器' : 'PID'}</div></div>
<div class="metric"><div class="value">${svc.port}</div><div class="label">端口</div></div>
<div class="metric"><div class="value">${formatUptime(svc.uptime)}</div><div class="label">运行时间</div></div>
</div>
<div class="btn-group" style="margin-top:10px">
${svc.status === 'stopped' || svc.status === 'error' ? `<button class="btn btn-xs btn-green" onclick="svcAction('start','${id}')">▶</button>` : ''}
${isDocker
? '<span class="docker-hint">🐳 Docker 管理</span>'
: `${svc.status === 'stopped' || svc.status === 'error' ? `<button class="btn btn-xs btn-green" onclick="svcAction('start','${id}')">▶</button>` : ''}
${svc.status === 'running' ? `<button class="btn btn-xs" onclick="svcAction('restart','${id}')">🔄</button>` : ''}
${svc.status === 'running' || svc.status === 'starting' ? `<button class="btn btn-xs btn-red" onclick="svcAction('stop','${id}')">⏹</button>` : ''}
${svc.status === 'stopped' || svc.status === 'error' ? `<button class="btn btn-xs btn-accent" onclick="svcAction('build','${id}')">🔨</button>` : ''}
${svc.status === 'stopped' || svc.status === 'error' ? `<button class="btn btn-xs btn-accent" onclick="svcAction('build','${id}')">🔨</button>` : ''}`}
${svc.healthUrl ? `<button class="btn btn-xs" onclick="checkHealth('${id}')">❤️</button>` : ''}
</div>
</div>
`).join('');
`}).join('');
}
// ========== 记忆分类颜色映射 ==========
@@ -2242,7 +2249,8 @@ function renderServiceCards() {
const ids = Object.keys(status).length > 0 ? Object.keys(status) : ALL_SVC_IDS;
container.innerHTML = ids.map(id => {
const svc = status[id] || { name: escapeId(id), status: 'unknown', pid: null, port: '—', uptime: 0, healthUrl: null };
const svc = status[id] || { name: escapeId(id), status: 'unknown', pid: null, port: '—', uptime: 0, healthUrl: null, source: 'none' };
const isDocker = svc.source === 'docker';
const isRunning = svc.status === 'running';
const isStarting = svc.status === 'starting' || svc.status === 'building';
const isStopped = svc.status === 'stopped' || svc.status === 'error' || svc.status === 'unknown';
@@ -2251,18 +2259,20 @@ function renderServiceCards() {
<div class="card" style="margin:0">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px">
<span style="font-weight:600">${svc.name}</span>
<span class="badge ${statusBadge(svc.status)}">${svc.status}</span>
${isDocker ? '<span class="badge badge-docker">🐳 Docker</span>' : `<span class="badge ${statusBadge(svc.status)}">${svc.status}</span>`}
</div>
<div class="metrics">
<div class="metric"><div class="value">${svc.pid || '—'}</div><div class="label">PID</div></div>
<div class="metric"><div class="value">${isDocker ? (svc.containerName || '—') : (svc.pid || '—')}</div><div class="label">${isDocker ? '容器' : 'PID'}</div></div>
<div class="metric"><div class="value">${svc.port}</div><div class="label">端口</div></div>
<div class="metric"><div class="value">${formatUptime(svc.uptime)}</div><div class="label">运行时间</div></div>
</div>
<div class="btn-group" style="margin-top:10px">
${isStopped ? `<button class="btn btn-xs btn-green" onclick="svcAction('start','${id}')">▶ 启动</button>` : ''}
${isDocker
? '<span class="docker-hint">🐳 Docker 管理 — 请使用 docker compose</span>'
: `${isStopped ? `<button class="btn btn-xs btn-green" onclick="svcAction('start','${id}')">▶ 启动</button>` : ''}
${isStopped || isStarting ? `<button class="btn btn-xs btn-accent" onclick="svcAction('build','${id}')">🔨 编译</button>` : ''}
${isRunning ? `<button class="btn btn-xs" onclick="svcAction('restart','${id}')">🔄 重启</button>` : ''}
${isRunning || isStarting ? `<button class="btn btn-xs btn-red" onclick="svcAction('stop','${id}')">⏹ 停止</button>` : ''}
${isRunning || isStarting ? `<button class="btn btn-xs btn-red" onclick="svcAction('stop','${id}')">⏹ 停止</button>` : ''}`}
${svc.healthUrl ? `<button class="btn btn-xs" onclick="checkHealth('${id}')">❤️</button>` : ''}
</div>
</div>`;
@@ -2366,6 +2376,11 @@ async function svcAction(cmd, serviceId) {
// 立即更新本地状态和 UI(乐观更新)
if (serviceId) {
var svc = STATE.serviceStatus[serviceId];
if (svc && svc.source === "docker") {
showToast(svc.name + " 由 Docker 管理,不支持此操作", "error");
return;
}
var newStatus;
if (cmd === "start") newStatus = "starting";
else if (cmd === "stop") newStatus = "stopped";
@@ -2373,7 +2388,7 @@ async function svcAction(cmd, serviceId) {
else if (cmd === "build") newStatus = "building";
if (newStatus) {
if (!STATE.serviceStatus[serviceId]) {
STATE.serviceStatus[serviceId] = { name: escapeId(serviceId), status: newStatus, pid: null, port: "-", uptime: 0 };
STATE.serviceStatus[serviceId] = { name: escapeId(serviceId), status: newStatus, pid: null, port: "-", uptime: 0, source: "none" };
} else {
STATE.serviceStatus[serviceId].status = newStatus;
}
@@ -2381,15 +2396,21 @@ async function svcAction(cmd, serviceId) {
if (STATE.activePanel === "dashboard") renderDashboardSvcCards(STATE.serviceStatus);
}
} else {
// 批量操作:给所有服务设置过渡状态
// 批量操作:跳过 Docker 服务,只更新本地服务状态
var dockerCount = 0;
var newStatus = (cmd === "start-all" || cmd === "start-all-fresh") ? "starting" : "stopped";
ALL_SVC_IDS.forEach(function(id) {
if (!STATE.serviceStatus[id]) {
STATE.serviceStatus[id] = { name: escapeId(id), status: newStatus, pid: null, port: "-", uptime: 0 };
STATE.serviceStatus[id] = { name: escapeId(id), status: newStatus, pid: null, port: "-", uptime: 0, source: "none" };
} else if (STATE.serviceStatus[id].source === "docker") {
dockerCount++;
} else {
STATE.serviceStatus[id].status = newStatus;
}
});
if (dockerCount > 0) {
showToast("跳过 " + dockerCount + " 个 Docker 管理服务的操作", "info");
}
if (STATE.activePanel === "services") renderServiceCards();
if (STATE.activePanel === "dashboard") renderDashboardSvcCards(STATE.serviceStatus);
}