This commit is contained in:
Your Name
2026-04-17 15:13:46 +08:00
parent fe87c7b343
commit 0e096947fc
19 changed files with 791 additions and 151 deletions

View File

@@ -5,6 +5,11 @@
<el-button :loading="actionLoading === 'stop'" @click="invoke('stop')">停止检测</el-button>
<el-button plain :loading="loading" @click="loadStatus">刷新状态</el-button>
<el-button plain @click="goRuntime">前往运行中心</el-button>
<el-button-group>
<el-button plain :type="status.worker_log_sync_enabled ? '' : 'primary'" @click="updateWorkerLogSync(false, 'key')">关闭回传</el-button>
<el-button plain :type="status.worker_log_sync_enabled && status.worker_log_sync_mode === 'key' ? 'primary' : ''" @click="updateWorkerLogSync(true, 'key')">关键回传</el-button>
<el-button plain :type="status.worker_log_sync_enabled && status.worker_log_sync_mode === 'full' ? 'primary' : ''" @click="updateWorkerLogSync(true, 'full')">全量回传</el-button>
</el-button-group>
<el-switch v-model="autoRefresh" inline-prompt active-text="自动刷新" inactive-text="手动" />
<span class="updated-at">最近同步{{ lastUpdatedAt || "暂无" }}</span>
</div>
@@ -19,7 +24,7 @@
/>
<el-descriptions :column="2" border>
<el-descriptions-item label="Worker 在线">{{ status.worker_online ? "是" : "否" }}</el-descriptions-item>
<el-descriptions-item label="本机 Worker 在线">{{ status.worker_online ? "是" : "否" }}</el-descriptions-item>
<el-descriptions-item label="运行模式">{{ status.worker_mode || "windows-local" }}</el-descriptions-item>
<el-descriptions-item label="Worker 服务">{{ status.worker_service_name || "-" }}</el-descriptions-item>
<el-descriptions-item label="API 服务">{{ status.api_service_name || "-" }}</el-descriptions-item>
@@ -35,6 +40,8 @@
<el-descriptions-item label="最近代理刷新">{{ status.proxy_last_refresh_time || "暂无" }}</el-descriptions-item>
<el-descriptions-item label="最近 Worker 日志时间">{{ status.last_worker_log_time || "暂无" }}</el-descriptions-item>
<el-descriptions-item label="最近启动时间">{{ status.worker_latest_start_time || "暂无" }}</el-descriptions-item>
<el-descriptions-item label="远端日志回传">{{ status.worker_log_sync_enabled ? "已开启" : "已关闭" }}</el-descriptions-item>
<el-descriptions-item label="回传级别">{{ status.worker_log_sync_enabled ? (status.worker_log_sync_mode === "full" ? "全量" : "关键") : "-" }}</el-descriptions-item>
</el-descriptions>
<el-alert
@@ -90,7 +97,7 @@
:closable="false"
show-icon
type="info"
:title="`当前检测状态:${currentPhaseLabel}Worker ${status.worker_online ? '在线' : '离线'}。`"
:title="`当前检测状态:${currentPhaseLabel}本机 Worker ${status.worker_online ? '在线' : '离线'}。`"
style="margin-top: 12px"
/>
@@ -261,7 +268,7 @@
<div class="task-detail-meta">
<el-switch v-model="logAutoFollow" inline-prompt active-text="跟随日志" inactive-text="暂停跟随" size="small" />
<el-button text size="small" @click="scrollLogToBottom(true)">回到底部</el-button>
<span>Worker{{ status.worker_online ? "在线" : "离线" }}</span>
<span>本机 Worker{{ status.worker_online ? "在线" : "离线" }}</span>
<span v-if="selectedRun?.phase_label">阶段{{ selectedRun.phase_label }}</span>
<span>运行中{{ status.progress.running || 0 }}</span>
<span>线程{{ threadCountSummary }}</span>
@@ -284,7 +291,7 @@ import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue"
import { useRouter } from "vue-router";
import { ElMessage } from "element-plus";
import PageCard from "@/components/PageCard.vue";
import { detectApi } from "@/api/modules";
import { detectApi, settingsApi } from "@/api/modules";
const DETECT_LAST_ACTION_KEY = "domaincheck:detect:last-action";
const router = useRouter();
@@ -320,7 +327,10 @@ const status = ref({
proxy_last_refresh_time: "",
dependency_alerts: [] as any[],
log_lines: [] as string[],
remote_log_lines: [] as string[],
runs: [] as any[],
worker_log_sync_enabled: false,
worker_log_sync_mode: "key",
progress_percent: 0,
active_job: null as any,
progress: {
@@ -424,13 +434,44 @@ const phaseHistory = computed(() => {
if (!Array.isArray(items)) return [];
return [...items].reverse();
});
const selectedRunIsLatest = computed(() => {
if (!selectedRun.value || !runs.value.length) {
return false;
}
return selectedRun.value.run_id === runs.value[0]?.run_id;
});
const dedupeConsecutiveLines = (lines: string[]) => {
const result: string[] = [];
for (const rawLine of lines) {
const line = String(rawLine || "");
if (!line) {
continue;
}
if (result[result.length - 1] === line) {
continue;
}
result.push(line);
}
return result;
};
const selectedLogText = computed(() => {
const runLogs = selectedRun.value?.logs;
const mergedLines: string[] = [];
if (Array.isArray(runLogs) && runLogs.length) {
return runLogs.join("\n");
mergedLines.push(...runLogs);
}
const remoteLogLines = status.value.remote_log_lines || [];
const canAppendRemoteLogs = !selectedRun.value || selectedRunIsLatest.value;
if (canAppendRemoteLogs && Array.isArray(remoteLogLines) && remoteLogLines.length) {
mergedLines.push(...remoteLogLines);
}
if (mergedLines.length) {
return dedupeConsecutiveLines(mergedLines).join("\n");
}
const liveLogs = status.value.log_lines || [];
return liveLogs.length ? liveLogs.join("\n") : "暂无检测日志";
return liveLogs.length ? dedupeConsecutiveLines(liveLogs).join("\n") : "暂无检测日志";
});
const proxyAlertType = computed<"success" | "warning" | "info" | "error">(() => {
if (status.value.proxy_runtime_state === "healthy") return "success";
@@ -580,18 +621,35 @@ const invoke = async (action: "start" | "stop") => {
actionLoading.value = action;
try {
const response = action === "start" ? await detectApi.start() : await detectApi.stop();
ElMessage.success(response.message);
const responseMessage = String(response.message || "");
const uiLevel = String(response.data?.ui_level || "").trim().toLowerCase();
const actionType: "success" | "warning" | "error" =
uiLevel === "error" ? "error" : uiLevel === "warning" ? "warning" : "success";
if (actionType === "error") {
ElMessage.error(responseMessage);
} else if (actionType === "warning") {
ElMessage.warning(responseMessage);
} else {
ElMessage.success(responseMessage);
}
lastAction.value = {
label: action === "start" ? "检测启动" : "检测停止",
message: response.message,
message: responseMessage,
at: new Date().toLocaleString("zh-CN", { hour12: false }),
type: "success"
type: actionType
};
persistLastAction();
const pollDelaySeconds = Number(response.data?.poll_after_seconds || 2);
window.setTimeout(() => {
loadStatus(false);
}, pollDelaySeconds * 1000);
await loadStatus(false);
const pollSchedule = Array.isArray(response.data?.poll_schedule_seconds)
? response.data.poll_schedule_seconds
: [Number(response.data?.poll_after_seconds || 2)];
[...new Set(pollSchedule.map((item: unknown) => Number(item || 0)).filter((item: number) => item > 0))]
.sort((a, b) => a - b)
.forEach((seconds) => {
window.setTimeout(() => {
loadStatus(false);
}, seconds * 1000);
});
} catch (error: any) {
const message = error?.message || `${action === "start" ? "启动" : "停止"}操作失败`;
lastAction.value = {
@@ -615,6 +673,25 @@ const goRuntime = () => {
router.push("/runtime");
};
const updateWorkerLogSync = async (enabled: boolean, mode: "key" | "full") => {
try {
const current = await settingsApi.getSettings();
const payload = {
...current.data,
runtime_settings: {
...(current.data?.runtime_settings || {}),
worker_log_sync_enabled: enabled,
worker_log_sync_mode: mode
}
};
await settingsApi.updateSettings(payload);
ElMessage.success(`远端日志回传已${enabled ? `切到${mode === "full" ? "全量" : "关键"}模式` : "关闭"}`);
await loadStatus();
} catch {
ElMessage.error("更新远端日志回传开关失败");
}
};
watch(autoRefresh, ensureRefreshTimer);
watch(selectedLogText, async (current, previous) => {
if (current === previous) {