This commit is contained in:
Your Name
2026-04-15 12:00:09 +08:00
parent 2962da0907
commit 845d68110b
426 changed files with 43961 additions and 34179 deletions

View File

@@ -0,0 +1,261 @@
<template>
<el-dialog v-model="visible" title="导入后探测失败队列" width="1100px" draggable>
<div class="failed-queue-dialog">
<div class="failed-queue-dialog__toolbar">
<el-button type="primary" plain @click="loadQueue">刷新</el-button>
<el-button type="success" plain :loading="selfHealingLoading" @click="runSelfHealing">后台执行自动修复</el-button>
<el-button type="danger" plain :loading="rerunLoading" @click="runQueueRerun">后台执行整批重跑</el-button>
<el-button type="warning" plain :loading="remediationLoading" @click="runQueueRemediation">后台执行补料候选</el-button>
<el-button v-if="rerunAllCommand" type="warning" plain @click="showRerunAllCommand">整批重跑命令</el-button>
<el-button type="info" plain @click="openSelfHealingDialog">最近自动修复记录</el-button>
<el-button type="info" plain @click="openRerunDialog">最近重跑记录</el-button>
<el-button type="info" plain @click="openRemediationDialog">最近补料记录</el-button>
</div>
<el-alert
title="这里是导入成功后,自动页面探测仍未通过的 host"
description="它不代表域名没有入库,而是代表首页、搜索、详情、播放这条页面链路还没有探通。"
type="warning"
show-icon
:closable="false"
/>
<div class="failed-queue-dialog__summary">
<span>当前探测失败{{ queueSummary.active_count || healthTrend.active_count || healthTrend.queue_count || 0 }}</span>
<span>已自动消退{{ queueSummary.resolved_count || healthTrend.resolved_count || 0 }}</span>
<span>重跑记录{{ healthTrend.rerun_runs_count || 0 }}</span>
<span>补料记录{{ healthTrend.remediation_runs_count || 0 }}</span>
<span>{{ trendLabel() }}</span>
</div>
<div class="failed-queue-dialog__summary">
<span>重跑消退{{ queueSummary.resolved_by_rerun_count || healthTrend.resolved_by_rerun_count || 0 }}</span>
<span>补料消退{{ queueSummary.resolved_by_remediation_count || healthTrend.resolved_by_remediation_count || 0 }}</span>
</div>
<div v-if="resolvedPreview().length" class="failed-queue-dialog__resolved">
<span class="failed-queue-dialog__resolved-label">最近自动消退</span>
<span>{{ resolvedPreview().join(" / ") }}</span>
</div>
<el-table v-loading="loading" :data="rows" border>
<el-table-column prop="host" label="Host" min-width="180" />
<el-table-column prop="run_id" label="Run ID" min-width="160" />
<el-table-column prop="failed_stage" label="失败阶段" width="100" />
<el-table-column label="失败明细" min-width="260">
<template #default="{ row }">
<span>
{{ formatFailedChecks(row) }}
</span>
</template>
</el-table-column>
<el-table-column prop="search_keyword" label="搜索词样本" min-width="160" />
<el-table-column label="补料候选" min-width="220">
<template #default="{ row }">
<span>
{{ row.remediation_plan?.title || "-" }} / {{ row.remediation_plan?.summary || "-" }}
</span>
</template>
</el-table-column>
<el-table-column prop="updated_at" label="更新时间" min-width="180" />
<el-table-column label="操作" width="320" fixed="right">
<template #default="{ row }">
<el-button v-if="row.summary_html_path" link type="primary" @click="openPublicPath(row.summary_html_path)">HTML摘要</el-button>
<el-button v-if="row.summary_json_path" link type="info" @click="openPublicPath(row.summary_json_path)">JSON摘要</el-button>
<el-button v-if="row.rerun_command" link type="warning" @click="showCommand('单条重跑命令', row.rerun_command)">重跑命令</el-button>
<el-button v-if="row.sample_command" link type="success" @click="showCommand('自动样本命令', row.sample_command)">样本命令</el-button>
<el-button v-if="row.remediation_command" link type="danger" @click="showCommand('补料候选命令', row.remediation_command)">补料命令</el-button>
</template>
</el-table-column>
</el-table>
</div>
</el-dialog>
<DomainImportSelfHealingDialog ref="selfHealingDialogRef" />
<DomainImportRerunDialog ref="rerunDialogRef" />
<DomainImportRemediationDialog ref="remediationDialogRef" />
<DomainImportTaskConsoleDialog ref="taskConsoleDialogRef" />
</template>
<script setup lang="ts">
import { ref } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { SiteList } from "@/api/interface/site/list";
import { getDomainImportFailedQueue, getDomainImportHealthTrend, runDomainImportFailedQueue, runDomainImportFailureRemediation, runDomainImportSelfHealing } from "@/api/modules/site/list";
import DomainImportSelfHealingDialog from "./domainImportSelfHealingDialog.vue";
import DomainImportRerunDialog from "./domainImportRerunDialog.vue";
import DomainImportRemediationDialog from "./domainImportRemediationDialog.vue";
import DomainImportTaskConsoleDialog from "./domainImportTaskConsoleDialog.vue";
const BASE_URL = import.meta.env.VITE_API_URL as string;
const visible = ref(false);
const loading = ref(false);
const selfHealingLoading = ref(false);
const rerunLoading = ref(false);
const remediationLoading = ref(false);
const rows = ref<SiteList.DomainImportFailedQueueItem[]>([]);
const queueSummary = ref<SiteList.DomainImportFailedQueueData>({});
const rerunAllCommand = ref("");
const selfHealingDialogRef = ref();
const rerunDialogRef = ref();
const remediationDialogRef = ref();
const taskConsoleDialogRef = ref();
const healthTrend = ref<SiteList.DomainImportHealthTrendData>({});
const formatFailedChecks = (row: SiteList.DomainImportFailedQueueItem) => {
return (row.failed_checks || [])
.slice(0, 2)
.map(item => `${item?.stage || "-"}:${item?.detail || "-"}`)
.join(" | ") || "-";
};
const openPublicPath = (path?: string) => {
if (!path) {
return;
}
window.open(`${BASE_URL}/${String(path).replace(/^\/+/, "")}`, "_blank", "noopener");
};
const showCommand = async (title: string, command?: string) => {
if (!command) {
return;
}
await ElMessageBox.alert(`<code>${command}</code>`, title, {
dangerouslyUseHTMLString: true,
confirmButtonText: "知道了"
});
};
const showRerunAllCommand = async () => {
await showCommand("整批重跑命令", rerunAllCommand.value);
};
const openRerunDialog = () => {
rerunDialogRef.value?.open();
};
const openRemediationDialog = () => {
remediationDialogRef.value?.open();
};
const openSelfHealingDialog = () => {
selfHealingDialogRef.value?.open();
};
const trendLabel = () => {
const label = healthTrend.value.failure_trend?.label || "flat";
if (label === "improving") return "失败趋势改善中";
if (label === "worsening") return "失败趋势上升";
return "失败趋势持平";
};
const resolvedPreview = () => {
return queueSummary.value.resolved_hosts_preview || healthTrend.value.resolved_hosts_preview || [];
};
const runQueueRerun = async () => {
rerunLoading.value = true;
try {
const res = await runDomainImportFailedQueue({ limit: 50 });
const job = res.data.job;
if (!job?.job_id) {
throw new Error("job_id missing");
}
ElMessage.success("重跑任务已创建");
taskConsoleDialogRef.value?.open("失败队列重跑任务", job.job_id, async () => {
await loadQueue();
openRerunDialog();
});
} catch (_error) {
ElMessage.error("执行失败队列重跑失败");
} finally {
rerunLoading.value = false;
}
};
const runSelfHealing = async () => {
selfHealingLoading.value = true;
try {
const res = await runDomainImportSelfHealing();
const job = res.data.job;
if (!job?.job_id) {
throw new Error("job_id missing");
}
ElMessage.success("自动修复任务已创建");
taskConsoleDialogRef.value?.open("自动修复任务", job.job_id, async () => {
await loadQueue();
openSelfHealingDialog();
});
} catch (_error) {
ElMessage.error("执行自动修复失败");
} finally {
selfHealingLoading.value = false;
}
};
const runQueueRemediation = async () => {
remediationLoading.value = true;
try {
const res = await runDomainImportFailureRemediation({ limit: 50 });
const job = res.data.job;
if (!job?.job_id) {
throw new Error("job_id missing");
}
ElMessage.success("补料任务已创建");
taskConsoleDialogRef.value?.open("失败补料候选任务", job.job_id, async () => {
await loadQueue();
openRemediationDialog();
});
} catch (_error) {
ElMessage.error("执行失败队列补料候选失败");
} finally {
remediationLoading.value = false;
}
};
const loadQueue = async () => {
loading.value = true;
try {
const [queueRes, trendRes] = await Promise.all([
getDomainImportFailedQueue({ limit: 50 }),
getDomainImportHealthTrend({ limit: 10 })
]);
queueSummary.value = queueRes.data ?? {};
rows.value = queueSummary.value.items ?? [];
rerunAllCommand.value = queueSummary.value.rerun_all_command ?? "";
healthTrend.value = trendRes.data ?? {};
} catch (_error) {
ElMessage.error("读取导入后探测失败队列失败");
} finally {
loading.value = false;
}
};
const open = async () => {
visible.value = true;
await loadQueue();
};
defineExpose({
open
});
</script>
<style scoped lang="scss">
.failed-queue-dialog__toolbar {
margin-bottom: 12px;
}
.failed-queue-dialog__summary {
display: flex;
gap: 16px;
margin-bottom: 12px;
color: #606266;
font-size: 13px;
}
.failed-queue-dialog__resolved {
margin-bottom: 12px;
font-size: 13px;
color: #909399;
}
.failed-queue-dialog__resolved-label {
margin-right: 8px;
color: #606266;
}
</style>