feat: add ops center and node onboarding flow
This commit is contained in:
@@ -21,6 +21,107 @@ export const runtimeApi = {
|
||||
action: (action: string) => http.post(`/runtime/actions/${action}`)
|
||||
};
|
||||
|
||||
export const opsApi = {
|
||||
overview: () => http.get("/ops/overview"),
|
||||
goLiveSummary: (params?: Record<string, unknown>) => http.get("/ops/go-live-summary", { params }),
|
||||
doctorDecision: (params?: Record<string, unknown>) => http.get("/ops/doctor-decision", { params }),
|
||||
goLiveSignoff: (params?: Record<string, unknown>) => http.get("/ops/go-live-signoff", { params }),
|
||||
goLiveBundle: (params?: Record<string, unknown>) => http.get("/ops/go-live-bundle", { params }),
|
||||
goLiveReview: (params?: Record<string, unknown>) => http.get("/ops/go-live-review", { params }),
|
||||
stackDiagnosis: (params?: Record<string, unknown>) => http.get("/ops/stack-diagnosis", { params }),
|
||||
linkSnapshot: () => http.get("/ops/link-snapshot"),
|
||||
inspectionOverview: (params?: Record<string, unknown>) => http.get("/ops/inspection-overview", { params }),
|
||||
activityStream: (params?: Record<string, unknown>) => http.get("/ops/activity-stream", { params }),
|
||||
driverFeed: () => http.get("/ops/driver-feed"),
|
||||
codexBrief: () => http.get("/ops/codex-brief"),
|
||||
capabilities: () => http.get("/ops/capabilities"),
|
||||
contracts: () => http.get("/ops/contracts"),
|
||||
contractDetail: (contractKey: string) => http.get(`/ops/contracts/${encodeURIComponent(contractKey)}`),
|
||||
actionTemplates: () => http.get("/ops/action-templates"),
|
||||
playbooks: () => http.get("/ops/playbooks"),
|
||||
playbookRuns: (params?: Record<string, unknown>) => http.get("/ops/playbook-runs", { params }),
|
||||
playbookRunDetail: (runCode: string) => http.get(`/ops/playbook-runs/${encodeURIComponent(runCode)}`),
|
||||
playbookRunEvents: (runCode: string, params?: Record<string, unknown>) =>
|
||||
http.get(`/ops/playbook-runs/${encodeURIComponent(runCode)}/events`, { params }),
|
||||
rerunPlaybookRun: (runCode: string, payload?: Record<string, unknown>) =>
|
||||
http.post(`/ops/playbook-runs/${encodeURIComponent(runCode)}/rerun`, payload || {}),
|
||||
cancelPlaybookRun: (runCode: string, payload?: Record<string, unknown>) =>
|
||||
http.post(`/ops/playbook-runs/${encodeURIComponent(runCode)}/cancel`, payload || {}),
|
||||
previewPlaybook: (payload: Record<string, unknown>) => http.post("/ops/playbooks/preview", payload),
|
||||
executePlaybook: (payload: Record<string, unknown>) => http.post("/ops/playbooks/execute", payload),
|
||||
blueprint: () => http.get("/ops/blueprint"),
|
||||
runbook: () => http.get("/ops/runbook"),
|
||||
resolveRunbookSequence: (sequenceKey: string, payload?: Record<string, unknown>) =>
|
||||
http.post(`/ops/runbook/sequences/${encodeURIComponent(sequenceKey)}/resolve`, payload || {}),
|
||||
executeRunbookSequence: (sequenceKey: string, payload?: Record<string, unknown>) =>
|
||||
http.post(`/ops/runbook/sequences/${encodeURIComponent(sequenceKey)}/execute`, payload || {}),
|
||||
nodes: () => http.get("/ops/nodes"),
|
||||
nodeHandover: (nodeCode: string, params?: Record<string, unknown>) =>
|
||||
http.get(`/ops/nodes/${encodeURIComponent(nodeCode)}/handover`, { params }),
|
||||
nodeOnboarding: (nodeCode: string, params?: Record<string, unknown>) =>
|
||||
http.get(`/ops/nodes/${encodeURIComponent(nodeCode)}/onboarding`, { params }),
|
||||
previewNodeOnboardingRecovery: (nodeCode: string, payload?: Record<string, unknown>) =>
|
||||
http.post(`/ops/nodes/${encodeURIComponent(nodeCode)}/onboarding/recovery/preview`, payload || {}),
|
||||
executeNodeOnboardingRecovery: (nodeCode: string, payload?: Record<string, unknown>) =>
|
||||
http.post(`/ops/nodes/${encodeURIComponent(nodeCode)}/onboarding/recovery/execute`, payload || {}),
|
||||
nodeSceneLog: (nodeCode: string, params?: Record<string, unknown>) =>
|
||||
http.get(`/ops/nodes/${encodeURIComponent(nodeCode)}/scene-log`, { params }),
|
||||
buildNodeHandoverBootstrapPlan: (nodeCode: string, payload?: Record<string, unknown>) =>
|
||||
http.post(`/ops/nodes/${encodeURIComponent(nodeCode)}/handover/bootstrap-plan`, payload || {}),
|
||||
deliveryQueue: (nodeCode: string) => http.get(`/ops/nodes/${encodeURIComponent(nodeCode)}/delivery-queue`),
|
||||
deliveryQueueRecords: (nodeCode: string, params?: Record<string, unknown>) =>
|
||||
http.get(`/ops/nodes/${encodeURIComponent(nodeCode)}/delivery-queue/records`, { params }),
|
||||
flushDeliveryQueue: (nodeCode: string, payload?: Record<string, unknown>) =>
|
||||
http.post(`/ops/nodes/${encodeURIComponent(nodeCode)}/delivery-queue/flush`, payload || {}),
|
||||
replayDeliveryQueue: (nodeCode: string, payload?: Record<string, unknown>) =>
|
||||
http.post(`/ops/nodes/${encodeURIComponent(nodeCode)}/delivery-queue/replay`, payload || {}),
|
||||
replayDeliveryQueueRecord: (nodeCode: string, recordId: string, payload?: Record<string, unknown>) =>
|
||||
http.post(
|
||||
`/ops/nodes/${encodeURIComponent(nodeCode)}/delivery-queue/records/${encodeURIComponent(recordId)}/replay`,
|
||||
payload || {}
|
||||
),
|
||||
discardDeliveryQueueRecord: (nodeCode: string, recordId: string, payload?: Record<string, unknown>) =>
|
||||
http.post(
|
||||
`/ops/nodes/${encodeURIComponent(nodeCode)}/delivery-queue/records/${encodeURIComponent(recordId)}/discard`,
|
||||
payload || {}
|
||||
),
|
||||
upsertNode: (payload: Record<string, unknown>) => http.post("/ops/nodes", payload),
|
||||
syncNodesFromCluster: (dryRun = false) => http.post(`/ops/nodes/sync-from-cluster?dry_run=${dryRun ? "true" : "false"}`),
|
||||
previewPolicy: (payload: Record<string, unknown>) => http.post("/ops/policy/preview", payload),
|
||||
jobs: (params?: Record<string, unknown>) => http.get("/ops/jobs", { params }),
|
||||
jobDetail: (jobId: number) => http.get(`/ops/jobs/${jobId}`),
|
||||
jobEvents: (jobId: number, params?: Record<string, unknown>) => http.get(`/ops/jobs/${jobId}/events`, { params }),
|
||||
previewDriverAction: (payload: Record<string, unknown>) => http.post("/ops/driver-actions/preview", payload),
|
||||
resolveDriverAction: (payload: Record<string, unknown>) => http.post("/ops/driver-actions/resolve", payload),
|
||||
executeDriverAction: (payload: Record<string, unknown>) => http.post("/ops/driver-actions/execute", payload),
|
||||
executeResolvedDriverAction: (payload: Record<string, unknown>) => http.post("/ops/driver-actions/execute-resolved", payload),
|
||||
resolveCodexAction: (payload: Record<string, unknown>) => http.post("/ops/codex-actions/resolve", payload),
|
||||
executeCodexAction: (payload: Record<string, unknown>) => http.post("/ops/codex-actions/execute", payload),
|
||||
createJobBatch: (payload: Record<string, unknown>) => http.post("/ops/jobs/batch", payload),
|
||||
approveJob: (jobId: number, payload?: Record<string, unknown>) => http.post(`/ops/jobs/${jobId}/approve`, payload || {}),
|
||||
cancelJob: (jobId: number, payload?: Record<string, unknown>) => http.post(`/ops/jobs/${jobId}/cancel`, payload || {}),
|
||||
dispatchJob: (jobId: number) => http.post(`/ops/jobs/${jobId}/dispatch`),
|
||||
issueAgentToken: (payload: Record<string, unknown>) => http.post("/ops/agent/tokens", payload),
|
||||
buildAgentBootstrapPlan: (payload: Record<string, unknown>) => http.post("/ops/agent/bootstrap-plan", payload),
|
||||
releases: (params?: Record<string, unknown>) => http.get("/ops/releases", { params }),
|
||||
latestRelease: (channel = "stable") => http.get("/ops/releases/latest", { params: { channel } }),
|
||||
latestReleasePackageMetadata: () => http.get("/ops/releases/package-metadata/latest"),
|
||||
releaseLaunchpad: (channel = "stable") => http.get("/ops/releases/launchpad", { params: { channel } }),
|
||||
releaseDetail: (releaseId: number) => http.get(`/ops/releases/${releaseId}`),
|
||||
createRelease: (payload: Record<string, unknown>) => http.post("/ops/releases", payload),
|
||||
createReleaseFromLatestPackage: (payload?: Record<string, unknown>) => http.post("/ops/releases/from-package/latest", payload || {}),
|
||||
createReleaseFromLatestPackageAndSmartRollout: (payload?: Record<string, unknown>) =>
|
||||
http.post("/ops/releases/from-package/latest/smart-rollout", payload || {}),
|
||||
createSmartRolloutForRelease: (releaseId: number, payload?: Record<string, unknown>) =>
|
||||
http.post(`/ops/releases/${releaseId}/smart-rollout`, payload || {}),
|
||||
activateRelease: (releaseId: number) => http.post(`/ops/releases/${releaseId}/activate`),
|
||||
releaseRollouts: (releaseId: number, params?: Record<string, unknown>) => http.get(`/ops/releases/${releaseId}/rollouts`, { params }),
|
||||
createRollout: (releaseId: number, payload: Record<string, unknown>) => http.post(`/ops/releases/${releaseId}/rollouts`, payload),
|
||||
rolloutDetail: (rolloutId: number) => http.get(`/ops/rollouts/${rolloutId}`),
|
||||
rolloutJobs: (rolloutId: number, params?: Record<string, unknown>) => http.get(`/ops/rollouts/${rolloutId}/jobs`, { params }),
|
||||
advanceRollout: (rolloutId: number, payload?: Record<string, unknown>) => http.post(`/ops/rollouts/${rolloutId}/advance`, payload || {})
|
||||
};
|
||||
|
||||
export const settingsApi = {
|
||||
getSettings: () => http.get("/settings"),
|
||||
updateSettings: (payload: Record<string, unknown>) => http.put("/settings", payload),
|
||||
|
||||
@@ -67,6 +67,7 @@ const menuItems = [
|
||||
{ path: "/dashboard", label: "概览" },
|
||||
{ path: "/runtime", label: "运行中心" },
|
||||
{ path: "/runtime-debug", label: "运行调试" },
|
||||
{ path: "/ops-center", label: "运维中枢" },
|
||||
{ path: "/settings", label: "系统设置" },
|
||||
{ path: "/imports", label: "域名导入" },
|
||||
{ path: "/sensitive-words", label: "敏感词配置" },
|
||||
|
||||
@@ -16,6 +16,7 @@ const routes: RouteRecordRaw[] = [
|
||||
{ path: "dashboard", name: "dashboard", component: () => import("@/views/dashboard/DashboardView.vue"), meta: { title: "概览" } },
|
||||
{ path: "runtime", name: "runtime", component: () => import("@/views/runtime/RuntimeView.vue"), meta: { title: "运行中心" } },
|
||||
{ path: "runtime-debug", name: "runtime-debug", component: () => import("@/views/runtime/RuntimeDebugView.vue"), meta: { title: "运行调试" } },
|
||||
{ path: "ops-center", name: "ops-center", component: () => import("@/views/ops/OpsCenterView.vue"), meta: { title: "运维中枢" } },
|
||||
{ path: "settings", name: "settings", component: () => import("@/views/settings/SettingsView.vue"), meta: { title: "系统设置" } },
|
||||
{ path: "imports", name: "imports", component: () => import("@/views/imports/ImportsView.vue"), meta: { title: "域名导入" } },
|
||||
{ path: "sensitive-words", name: "sensitive-words", component: () => import("@/views/sensitive-words/SensitiveWordsView.vue"), meta: { title: "敏感词配置" } },
|
||||
|
||||
@@ -100,6 +100,13 @@
|
||||
:title="`当前检测状态:${currentPhaseLabel};本机 Worker ${status.worker_online ? '在线' : '离线'}。`"
|
||||
style="margin-top: 12px"
|
||||
/>
|
||||
<el-alert
|
||||
:closable="false"
|
||||
show-icon
|
||||
:type="status.worker_log_sync_enabled ? 'info' : 'warning'"
|
||||
:title="remoteLogMirrorText"
|
||||
style="margin-top: 12px"
|
||||
/>
|
||||
|
||||
<el-row :gutter="12" style="margin-top: 16px; margin-bottom: 16px">
|
||||
<el-col :xs="12" :sm="8" :md="4"><el-statistic title="待检测" :value="status.progress.pending || 0" /></el-col>
|
||||
@@ -328,6 +335,11 @@ const status = ref({
|
||||
dependency_alerts: [] as any[],
|
||||
log_lines: [] as string[],
|
||||
remote_log_lines: [] as string[],
|
||||
remote_log_line_count: 0,
|
||||
remote_log_last_at: "",
|
||||
remote_log_last_line: "",
|
||||
remote_log_nodes: [] as string[],
|
||||
remote_log_node_count: 0,
|
||||
runs: [] as any[],
|
||||
worker_log_sync_enabled: false,
|
||||
worker_log_sync_mode: "key",
|
||||
@@ -487,6 +499,29 @@ const proxyDiagnosisText = computed(() => {
|
||||
return status.value.proxy_runtime_label || "当前可用代理 / 配置代理池链接数";
|
||||
});
|
||||
|
||||
const remoteLogMirrorText = computed(() => {
|
||||
if (!status.value.worker_log_sync_enabled) {
|
||||
return "当前已关闭远端日志回传;日志控制台只展示本机 Worker 输出,不会拼接其他节点的执行过程。";
|
||||
}
|
||||
const modeText = status.value.worker_log_sync_mode === "full" ? "全量" : "关键";
|
||||
const nodeCount = Number(status.value.remote_log_node_count || 0);
|
||||
const lineCount = Number(status.value.remote_log_line_count || 0);
|
||||
const nodes = Array.isArray(status.value.remote_log_nodes) ? status.value.remote_log_nodes : [];
|
||||
const lastAt = String(status.value.remote_log_last_at || "").trim();
|
||||
const parts = [
|
||||
`当前已开启${modeText}回传,日志控制台会自动拼接远端节点回传日志。`,
|
||||
`来源节点 ${nodeCount} 台`,
|
||||
`样本 ${lineCount} 条`,
|
||||
];
|
||||
if (nodes.length) {
|
||||
parts.push(`节点:${nodes.join("、")}`);
|
||||
}
|
||||
if (lastAt) {
|
||||
parts.push(`最近回传:${lastAt}`);
|
||||
}
|
||||
return parts.join(";");
|
||||
});
|
||||
|
||||
const queueAlertType = computed<"success" | "warning" | "info" | "error">(() => {
|
||||
if (!queueSummary.value.has_active_job) return "info";
|
||||
if ((queueSummary.value.queue?.overdue_leases || 0) > 0) return "error";
|
||||
|
||||
18516
domain-web/src/views/ops/OpsCenterView.vue
Normal file
18516
domain-web/src/views/ops/OpsCenterView.vue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -73,10 +73,10 @@
|
||||
<div class="summary-title">有效执行节点</div>
|
||||
<div class="summary-value">{{ runtime.cluster?.summary?.online_worker_nodes || 0 }}</div>
|
||||
<div class="summary-note">
|
||||
可承担检测任务的在线节点
|
||||
真正执行/领任务 {{ runtime.detect?.participation_summary?.dispatch_active_nodes || 0 }}
|
||||
/ 在线未参与 {{ runtime.detect?.participation_summary?.non_participating_nodes || 0 }}
|
||||
/ 独立 Worker {{ runtime.cluster?.summary?.dedicated_online_worker_nodes || 0 }}
|
||||
/ 控制面 {{ runtime.cluster?.summary?.online_control_nodes || 0 }}
|
||||
/ 忙碌 {{ runtime.cluster?.summary?.status_counts?.busy || 0 }}
|
||||
/ 控制面兼跑 {{ runtime.detect?.participation_summary?.controller_worker_nodes || 0 }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
@@ -126,6 +126,12 @@
|
||||
<span class="status-chip" :class="(runtime.cluster?.summary?.online_worker_nodes || 0) > 0 ? 'ok' : 'danger'">
|
||||
有效执行节点 {{ runtime.cluster?.summary?.online_worker_nodes || 0 }}
|
||||
</span>
|
||||
<span class="status-chip" :class="(runtime.detect?.participation_summary?.dispatch_active_nodes || 0) > 0 ? 'warn' : 'neutral'">
|
||||
真正参与 {{ runtime.detect?.participation_summary?.dispatch_active_nodes || 0 }}
|
||||
</span>
|
||||
<span class="status-chip" :class="(runtime.detect?.participation_summary?.non_participating_nodes || 0) > 0 ? 'info' : 'neutral'">
|
||||
在线未参与 {{ runtime.detect?.participation_summary?.non_participating_nodes || 0 }}
|
||||
</span>
|
||||
<span class="status-chip" :class="preflight.ok ? 'ok' : 'danger'">
|
||||
预检 {{ preflight.ok ? "通过" : "异常" }}
|
||||
</span>
|
||||
@@ -203,6 +209,69 @@
|
||||
<el-button plain :loading="actionLoading === 'push_sync'" @click="invokeAction('push_sync')">立即同步</el-button>
|
||||
<el-button plain @click="goDetect">前往检测控制</el-button>
|
||||
</div>
|
||||
<div class="action-subpanel">
|
||||
<div class="action-subpanel-header">
|
||||
<h4>远端日志回传</h4>
|
||||
<el-tag :type="workerLogSyncEnabled ? 'success' : 'info'">
|
||||
{{ workerLogSyncEnabled ? (workerLogSyncMode === "full" ? "全量回传" : "关键回传") : "已关闭" }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<el-button
|
||||
plain
|
||||
:type="!workerLogSyncEnabled ? 'primary' : ''"
|
||||
:loading="workerLogSyncLoading && pendingWorkerLogMode === 'off'"
|
||||
@click="updateWorkerLogSync(false, 'key')"
|
||||
>
|
||||
关闭回传
|
||||
</el-button>
|
||||
<el-button
|
||||
plain
|
||||
:type="workerLogSyncEnabled && workerLogSyncMode === 'key' ? 'primary' : ''"
|
||||
:loading="workerLogSyncLoading && pendingWorkerLogMode === 'key'"
|
||||
@click="updateWorkerLogSync(true, 'key')"
|
||||
>
|
||||
关键回传
|
||||
</el-button>
|
||||
<el-button
|
||||
plain
|
||||
:type="workerLogSyncEnabled && workerLogSyncMode === 'full' ? 'primary' : ''"
|
||||
:loading="workerLogSyncLoading && pendingWorkerLogMode === 'full'"
|
||||
@click="updateWorkerLogSync(true, 'full')"
|
||||
>
|
||||
全量回传
|
||||
</el-button>
|
||||
</div>
|
||||
<el-alert
|
||||
:closable="false"
|
||||
show-icon
|
||||
type="info"
|
||||
style="margin-top: 12px"
|
||||
:title="workerLogSyncDescription"
|
||||
/>
|
||||
<div class="remote-log-observation">
|
||||
<div class="remote-log-observation__item">
|
||||
<span class="label">样本</span>
|
||||
<strong>{{ runtime.detect?.log_sync?.line_count || 0 }} 条</strong>
|
||||
</div>
|
||||
<div class="remote-log-observation__item">
|
||||
<span class="label">来源节点</span>
|
||||
<strong>{{ runtime.detect?.log_sync?.source_node_count || 0 }} 台</strong>
|
||||
</div>
|
||||
<div class="remote-log-observation__item remote-log-observation__item--wide">
|
||||
<span class="label">节点列表</span>
|
||||
<span>{{ workerLogSyncNodesText }}</span>
|
||||
</div>
|
||||
<div class="remote-log-observation__item remote-log-observation__item--wide">
|
||||
<span class="label">最近回传</span>
|
||||
<span>{{ workerLogSyncLastAtText }}</span>
|
||||
</div>
|
||||
<div class="remote-log-observation__item remote-log-observation__item--full">
|
||||
<span class="label">最近一条</span>
|
||||
<span>{{ workerLogSyncLastLineText }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-alert
|
||||
v-if="lastAction.message"
|
||||
:title="`${lastAction.label}:${lastAction.message}${lastAction.at ? `(${lastAction.at})` : ''}`"
|
||||
@@ -338,7 +407,7 @@
|
||||
<div class="history-panel">
|
||||
<div class="history-header">
|
||||
<h3>当前参与检测节点</h3>
|
||||
<span class="history-note">直接展示哪些节点正在领任务、执行任务或产生近窗吞吐,controller 兼跑检测也会计入。</span>
|
||||
<span class="history-note">这里只展示真正有任务动作或近窗吞吐的节点,controller 兼跑检测也会明确计入。</span>
|
||||
</div>
|
||||
<el-alert
|
||||
:closable="false"
|
||||
@@ -370,6 +439,13 @@
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="参与状态" min-width="130">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="participationStateTagType(row)">
|
||||
{{ participationStateText(row) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="items_claimed" label="已领" min-width="90" />
|
||||
<el-table-column prop="items_running" label="执行中" min-width="90" />
|
||||
<el-table-column prop="items_completed" label="完成" min-width="90" />
|
||||
@@ -379,6 +455,58 @@
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<div class="history-panel">
|
||||
<div class="history-header">
|
||||
<h3>在线但未参与节点</h3>
|
||||
<span class="history-note">这些节点当前在线,但没有执行/领任务;若只是上报了负载,会单独标成“负载待确认”。</span>
|
||||
</div>
|
||||
<el-alert
|
||||
:closable="false"
|
||||
show-icon
|
||||
type="info"
|
||||
style="margin-bottom: 12px"
|
||||
:title="nonParticipatingNodesAlertText"
|
||||
/>
|
||||
<el-table
|
||||
:data="nonParticipatingNodes"
|
||||
border
|
||||
size="small"
|
||||
empty-text="当前没有在线但未参与的节点"
|
||||
>
|
||||
<el-table-column prop="node_code" label="节点" min-width="180" />
|
||||
<el-table-column prop="region" label="区域" min-width="100" />
|
||||
<el-table-column prop="role" label="角色" min-width="100" />
|
||||
<el-table-column label="有效 Worker" min-width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.is_effective_worker ? 'success' : 'info'">
|
||||
{{ row.is_effective_worker ? "是" : "否" }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" min-width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 'online' ? 'success' : row.status === 'busy' ? 'warning' : 'info'">
|
||||
{{ row.status || "-" }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="未参与状态" min-width="130">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="nonParticipationStateTagType(row)">
|
||||
{{ nonParticipationStateText(row) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="current_load" label="负载" min-width="80" />
|
||||
<el-table-column label="待命说明" min-width="240" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
{{ standbyReasonText(row) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="last_heartbeat_at" label="最近心跳" min-width="160" />
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<div class="history-panel">
|
||||
<div class="history-header">
|
||||
<h3>动作留痕</h3>
|
||||
@@ -450,12 +578,12 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="当前阶段" min-width="140">
|
||||
<template #default="{ row }">
|
||||
{{ row.metadata?.phase || "-" }}
|
||||
{{ row.metadata?.phase || row.metadata?.phase_label || "-" }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="阶段说明" min-width="260" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
{{ row.metadata?.detail || "-" }}
|
||||
{{ row.metadata?.detail || row.metadata?.phase_detail || "-" }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="last_heartbeat_at" label="最近心跳" min-width="160" />
|
||||
@@ -679,6 +807,8 @@ const loading = reactive({
|
||||
});
|
||||
|
||||
const actionLoading = ref("");
|
||||
const workerLogSyncLoading = ref(false);
|
||||
const pendingWorkerLogMode = ref<"off" | "key" | "full">("off");
|
||||
const autoRefresh = ref(true);
|
||||
const lastUpdatedAt = ref("");
|
||||
const lastAction = ref({
|
||||
@@ -869,12 +999,16 @@ const clusterIssueCount = computed(() => {
|
||||
|
||||
const clusterAlertText = computed(() => {
|
||||
const summary = runtime.cluster?.summary || {};
|
||||
const participationSummary = runtime.detect?.participation_summary || {};
|
||||
const busy = Array.isArray(summary.busy_nodes) ? summary.busy_nodes.join("、") : "";
|
||||
const staleNodes = Array.isArray(summary.stale_nodes) ? summary.stale_nodes : [];
|
||||
const offlineNodes = Array.isArray(summary.offline_nodes) ? summary.offline_nodes : [];
|
||||
const parts = [
|
||||
`当前有效执行节点 ${summary.online_worker_nodes || 0},其中独立 Worker ${summary.dedicated_online_worker_nodes || 0}`
|
||||
`当前有效执行节点 ${summary.online_worker_nodes || 0},其中独立 Worker ${summary.dedicated_online_worker_nodes || 0},控制面兼跑 ${participationSummary.controller_worker_nodes || 0}`
|
||||
];
|
||||
parts.push(
|
||||
`真正执行/领任务 ${participationSummary.dispatch_active_nodes || 0} 台,近窗刚有吞吐 ${participationSummary.recent_only_nodes || 0} 台,在线未参与 ${participationSummary.non_participating_nodes || 0} 台`
|
||||
);
|
||||
if (busy) {
|
||||
parts.push(`忙碌节点:${busy}`);
|
||||
}
|
||||
@@ -893,8 +1027,9 @@ const participatingNodesAlertText = computed(() => {
|
||||
return "当前没有节点正在领任务、执行任务或产生近窗吞吐。";
|
||||
}
|
||||
const names = rows.map((item: Record<string, any>) => String(item.node_code || "-")).join("、");
|
||||
const effectiveCount = rows.filter((item: Record<string, any>) => Boolean(item.is_effective_worker)).length;
|
||||
return `当前参与检测节点 ${rows.length} 台,其中有效执行节点 ${effectiveCount} 台:${names}`;
|
||||
const dispatchActiveCount = rows.filter((item: Record<string, any>) => ["running", "claimed"].includes(String(item.participation_state || "").trim())).length;
|
||||
const recentOnlyCount = rows.filter((item: Record<string, any>) => String(item.participation_state || "").trim() === "recent_throughput").length;
|
||||
return `当前参与检测节点 ${rows.length} 台:正在执行/领任务 ${dispatchActiveCount} 台,近窗刚有吞吐 ${recentOnlyCount} 台。节点:${names}`;
|
||||
});
|
||||
|
||||
const syncAlertText = computed(() => {
|
||||
@@ -920,6 +1055,127 @@ const syncAlertText = computed(() => {
|
||||
return parts.join(";");
|
||||
});
|
||||
|
||||
const workerLogSyncEnabled = computed(() => {
|
||||
if (runtime.detect?.log_sync) {
|
||||
return Boolean(runtime.detect.log_sync.enabled);
|
||||
}
|
||||
return Boolean(settingsState.runtime_settings?.worker_log_sync_enabled);
|
||||
});
|
||||
|
||||
const workerLogSyncMode = computed(() => {
|
||||
const sourceValue = runtime.detect?.log_sync?.mode ?? settingsState.runtime_settings?.worker_log_sync_mode;
|
||||
return String(sourceValue || "").trim().toLowerCase() === "full" ? "full" : "key";
|
||||
});
|
||||
|
||||
const workerLogSyncDescription = computed(() => {
|
||||
if (!workerLogSyncEnabled.value) {
|
||||
return "当前已关闭远端日志回传;跨机联调时不会把 Worker 过程日志镜像到当前后台。";
|
||||
}
|
||||
const lineCount = Number(runtime.detect?.log_sync?.line_count || 0);
|
||||
const sourceNodes = Array.isArray(runtime.detect?.log_sync?.source_nodes) ? runtime.detect.log_sync.source_nodes : [];
|
||||
const lastAt = String(runtime.detect?.log_sync?.last_at || "").trim();
|
||||
const observationSuffix = lineCount > 0
|
||||
? ` 当前已收到 ${lineCount} 条回传日志,来源节点 ${sourceNodes.length || 0} 台${lastAt ? `,最近一条 ${lastAt}` : ""}。`
|
||||
: " 当前还没有收到远端日志样本。";
|
||||
if (workerLogSyncMode.value === "full") {
|
||||
return `当前为全量回传;会尽量保留更多 Worker 执行过程,适合短时深度排障,但日志量会更大。${observationSuffix}`;
|
||||
}
|
||||
return `当前为关键回传;只同步阶段、异常、代理与关键执行事件,适合日常联调观察。${observationSuffix}`;
|
||||
});
|
||||
|
||||
const workerLogSyncNodesText = computed(() => {
|
||||
const nodes = Array.isArray(runtime.detect?.log_sync?.source_nodes) ? runtime.detect.log_sync.source_nodes : [];
|
||||
return nodes.length ? nodes.join("、") : "当前还没有远端节点回传样本";
|
||||
});
|
||||
|
||||
const workerLogSyncLastAtText = computed(() => {
|
||||
return String(runtime.detect?.log_sync?.last_at || "").trim() || "暂无";
|
||||
});
|
||||
|
||||
const workerLogSyncLastLineText = computed(() => {
|
||||
return String(runtime.detect?.log_sync?.last_line || "").trim() || "当前还没有远端日志回传";
|
||||
});
|
||||
|
||||
const participatingNodeCodeSet = computed(() => {
|
||||
const rows = Array.isArray(runtime.detect?.participating_nodes) ? runtime.detect.participating_nodes : [];
|
||||
return new Set(rows.map((item: Record<string, any>) => String(item.node_code || "").trim()).filter(Boolean));
|
||||
});
|
||||
|
||||
const nonParticipatingNodes = computed(() => {
|
||||
if (Array.isArray(runtime.detect?.non_participating_nodes)) {
|
||||
return runtime.detect.non_participating_nodes;
|
||||
}
|
||||
if (Array.isArray(runtime.detect?.standby_nodes)) {
|
||||
return runtime.detect.standby_nodes;
|
||||
}
|
||||
const rows = Array.isArray(runtime.cluster?.nodes) ? runtime.cluster.nodes : [];
|
||||
return rows.filter((item: Record<string, any>) => {
|
||||
const nodeCode = String(item.node_code || "").trim();
|
||||
const status = String(item.status || "").trim();
|
||||
const currentLoad = Number(item.current_load || 0);
|
||||
const isEffectiveWorker = Boolean(item.is_effective_worker);
|
||||
return Boolean(nodeCode)
|
||||
&& isEffectiveWorker
|
||||
&& ["online", "busy"].includes(status)
|
||||
&& !participatingNodeCodeSet.value.has(nodeCode)
|
||||
&& currentLoad <= 0;
|
||||
});
|
||||
});
|
||||
|
||||
const nonParticipatingNodesAlertText = computed(() => {
|
||||
if (!nonParticipatingNodes.value.length) {
|
||||
return "当前所有有效执行节点要么正在参与检测,要么当前没有额外在线未参与节点。";
|
||||
}
|
||||
const names = nonParticipatingNodes.value.map((item: Record<string, any>) => String(item.node_code || "-")).join("、");
|
||||
const loadSyncingCount = nonParticipatingNodes.value.filter(
|
||||
(item: Record<string, any>) => String(item.participation_state || "").trim() === "load_syncing"
|
||||
).length;
|
||||
const standbyCount = nonParticipatingNodes.value.length - loadSyncingCount;
|
||||
return `当前在线但未参与节点 ${nonParticipatingNodes.value.length} 台:在线待命 ${standbyCount} 台,负载待确认 ${loadSyncingCount} 台。节点:${names}`;
|
||||
});
|
||||
|
||||
const nonParticipationStateText = (row: Record<string, any>) => {
|
||||
if (row?.participation_label) return String(row.participation_label);
|
||||
if (String(row?.participation_state || "").trim() === "load_syncing") return "负载待确认";
|
||||
return "在线待命";
|
||||
};
|
||||
|
||||
const nonParticipationStateTagType = (row: Record<string, any>) => {
|
||||
const state = String(row?.participation_state || "").trim();
|
||||
if (state === "load_syncing") return "warning";
|
||||
return "info";
|
||||
};
|
||||
|
||||
const participationStateText = (row: Record<string, any>) => {
|
||||
if (row?.participation_label) return String(row.participation_label);
|
||||
const running = Number(row.items_running || 0);
|
||||
const claimed = Number(row.items_claimed || 0);
|
||||
const processedRecent = Number(row.processed_recent || 0);
|
||||
if (running > 0) return "执行中";
|
||||
if (claimed > 0) return "已领待跑";
|
||||
if (processedRecent > 0) return "近窗有吞吐";
|
||||
return "参与中";
|
||||
};
|
||||
|
||||
const participationStateTagType = (row: Record<string, any>) => {
|
||||
const state = String(row?.participation_state || participationStateText(row));
|
||||
if (["running", "执行中", "busy", "忙碌中"].includes(state)) return "warning";
|
||||
if (["claimed", "已领待跑"].includes(state)) return "success";
|
||||
if (["recent_throughput", "近窗有吞吐"].includes(state)) return "info";
|
||||
return "info";
|
||||
};
|
||||
|
||||
const standbyReasonText = (row: Record<string, any>) => {
|
||||
if (row?.standby_reason) return String(row.standby_reason);
|
||||
if (row?.participation_reason) return String(row.participation_reason);
|
||||
const metadata = row?.metadata || {};
|
||||
if (metadata?.detail) return String(metadata.detail);
|
||||
if (metadata?.phase_detail) return String(metadata.phase_detail);
|
||||
if (metadata?.phase) return `当前阶段:${metadata.phase}`;
|
||||
if (metadata?.phase_label) return `当前阶段:${metadata.phase_label}`;
|
||||
return "节点在线,当前未领任务、未执行任务,也没有近窗吞吐。";
|
||||
};
|
||||
|
||||
const hasNodeOverride = (nodeCode?: string) => {
|
||||
const normalized = String(nodeCode || "").trim();
|
||||
return Boolean(normalized && settingsState.node_thread_counts?.[normalized] != null);
|
||||
@@ -1046,6 +1302,33 @@ const updateNodeThreadSettings = async (updater: (nodeThreadCounts: Record<strin
|
||||
});
|
||||
};
|
||||
|
||||
const updateWorkerLogSync = async (enabled: boolean, mode: "key" | "full") => {
|
||||
workerLogSyncLoading.value = true;
|
||||
pendingWorkerLogMode.value = enabled ? mode : "off";
|
||||
try {
|
||||
const latest = await settingsApi.getSettings();
|
||||
const payload = latest.data || {};
|
||||
await settingsApi.updateSettings({
|
||||
thread_count: payload.thread_count,
|
||||
node_thread_counts: payload.node_thread_counts || {},
|
||||
detect_options: payload.detect_options || {},
|
||||
proxy_config: payload.proxy_config || {},
|
||||
runtime_settings: {
|
||||
...(payload.runtime_settings || {}),
|
||||
worker_log_sync_enabled: enabled,
|
||||
worker_log_sync_mode: mode
|
||||
}
|
||||
});
|
||||
ElMessage.success(`远端日志回传已${enabled ? `切到${mode === "full" ? "全量" : "关键"}模式` : "关闭"}`);
|
||||
await loadAll(false);
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.message || "更新远端日志回传开关失败");
|
||||
} finally {
|
||||
workerLogSyncLoading.value = false;
|
||||
pendingWorkerLogMode.value = "off";
|
||||
}
|
||||
};
|
||||
|
||||
const saveNodeThreadOverride = async () => {
|
||||
if (!nodeThreadDialog.nodeCode) {
|
||||
ElMessage.warning("节点编码不能为空");
|
||||
@@ -1269,6 +1552,11 @@ onBeforeUnmount(clearRefreshTimer);
|
||||
color: #c2410c;
|
||||
}
|
||||
|
||||
.status-chip.info {
|
||||
background: #eff6ff;
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.status-chip.danger {
|
||||
background: #fef2f2;
|
||||
color: #b91c1c;
|
||||
@@ -1300,6 +1588,42 @@ onBeforeUnmount(clearRefreshTimer);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.remote-log-observation {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 10px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.remote-log-observation__item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
background: #f8fbff;
|
||||
border: 1px solid #dbe4ee;
|
||||
color: #0f172a;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.remote-log-observation__item .label {
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.remote-log-observation__item--wide,
|
||||
.remote-log-observation__item--full {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.remote-log-observation__item--wide,
|
||||
.remote-log-observation__item--full {
|
||||
grid-column: span 1;
|
||||
}
|
||||
}
|
||||
|
||||
.cluster-summary {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -4,6 +4,13 @@ import { fileURLToPath, URL } from "node:url";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
api: "modern-compiler"
|
||||
}
|
||||
}
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": fileURLToPath(new URL("./src", import.meta.url))
|
||||
|
||||
Reference in New Issue
Block a user