feat(api): 新增节点墙检测自动托管与显隐

新增定时墙检测命令与节点托管字段,自动为开启托管的父
节点创建检测任务,并在 blocked 时自动隐藏节点、normal
时仅恢复由墙检测自动隐藏的节点

更新自动上线服务以尊重 blocked 与自动隐藏状态,避免疑
似被墙节点被重新发布;同时补齐管理端墙检测托管开关、
刷新入口、批量设置与相关测试和知识库同步
This commit is contained in:
yinjianm
2026-04-28 00:51:49 +08:00
parent 73b1696b0a
commit ff50030364
27 changed files with 998 additions and 24 deletions
+6
View File
@@ -881,6 +881,9 @@ export interface AdminNodeItem {
tags?: string[] | null
show: boolean
auto_online?: boolean
gfw_check_enabled?: boolean
gfw_auto_hidden?: boolean
gfw_auto_action_at?: number | null
enabled?: boolean
parent_id?: number | null
rate?: number | null
@@ -941,6 +944,7 @@ export interface AdminNodeUpdatePayload {
id: number
show?: boolean | number
auto_online?: boolean
gfw_check_enabled?: boolean
enabled?: boolean
machine_id?: number | null
}
@@ -951,6 +955,7 @@ export interface AdminNodeBatchUpdatePayload {
rate?: number
group_ids?: string[]
auto_online?: boolean
gfw_check_enabled?: boolean
}
export interface AdminNodeSavePayload {
@@ -972,6 +977,7 @@ export interface AdminNodeSavePayload {
protocol_settings?: Record<string, unknown>
show?: boolean | number
auto_online?: boolean
gfw_check_enabled?: boolean
}
declare global {
@@ -172,6 +172,7 @@ export function toNodeFormModel(node?: AdminNodeItem | null): NodeFormModel {
form.parentId = node.parent_id ?? null
form.show = toBooleanValue(node.show, true)
form.autoOnline = toBooleanValue(node.auto_online)
form.gfwCheckEnabled = toBooleanValue(node.gfw_check_enabled, true)
form.enabled = toBooleanValue(node.enabled, true)
form.tlsMode = Number(protocolSettings.tls ?? 0)
form.tlsServerName = toStringValue(tlsSettings.server_name || tlsObject.server_name)
@@ -493,5 +494,6 @@ export function toNodeSavePayload(form: NodeFormModel): AdminNodeSavePayload {
protocol_settings: buildProtocolSettings(form),
show: form.show ? 1 : 0,
auto_online: form.autoOnline,
gfw_check_enabled: form.gfwCheckEnabled,
}
}
@@ -32,6 +32,7 @@ export interface NodeFormModel {
parentId: number | null
show: boolean
autoOnline: boolean
gfwCheckEnabled: boolean
enabled: boolean
tlsMode: number
tlsServerName: string
@@ -243,6 +244,7 @@ export function createEmptyNodeForm(): NodeFormModel {
parentId: null,
show: true,
autoOnline: false,
gfwCheckEnabled: true,
enabled: true,
tlsMode: 0,
tlsServerName: '',
+11 -1
View File
@@ -145,11 +145,15 @@ export function getNodeGfwTooltip(node: AdminNodeItem): string {
const checkedAt = node.gfw_check?.checked_at
? new Date(Number(node.gfw_check.checked_at) * 1000).toLocaleString()
: ''
const actionAt = node.gfw_auto_action_at
? new Date(Number(node.gfw_auto_action_at) * 1000).toLocaleString()
: ''
const sourceText = meta.inherited && source ? `,来源父节点 #${source}` : ''
const timeText = checkedAt ? `,检测时间 ${checkedAt}` : ''
const autoText = node.gfw_auto_hidden ? `,已自动隐藏${actionAt ? `${actionAt}` : ''}` : ''
const errorText = node.gfw_check?.error_message ? `,错误:${node.gfw_check.error_message}` : ''
return `${meta.label}${sourceText}${timeText}${errorText}`
return `${meta.label}${sourceText}${timeText}${autoText}${errorText}`
}
function isNodeOnlineStatus(status: NodeStatusClass): boolean {
@@ -200,6 +204,8 @@ function buildNodeSearchText(node: AdminNodeItem): string {
node.server_port,
getNodeTypeLabel(node.type),
node.auto_online ? '自动上线 自动托管 auto online' : '',
node.gfw_check_enabled === false ? '关闭墙检测 关闭自动墙检 gfw disabled' : '自动墙检 墙检测托管 gfw enabled',
node.gfw_auto_hidden ? '自动隐藏 墙检测隐藏 疑似被墙已隐藏 gfw auto hidden' : '',
getNodeGfwMeta(node).searchText,
...getNodeGroupNames(node),
]
@@ -282,3 +288,7 @@ export function countVisibleNodes(nodes: AdminNodeItem[]): number {
export function countAutoOnlineNodes(nodes: AdminNodeItem[]): number {
return nodes.filter((node) => Boolean(node.auto_online)).length
}
export function countAutoGfwCheckNodes(nodes: AdminNodeItem[]): number {
return nodes.filter((node) => node.gfw_check_enabled !== false).length
}
@@ -8,6 +8,7 @@ interface NodeBatchEditPayload {
rate?: number
group_ids?: string[]
auto_online?: boolean
gfw_check_enabled?: boolean
}
const props = defineProps<{
@@ -31,6 +32,8 @@ const form = reactive({
groupIds: [] as number[],
updateAutoOnline: false,
autoOnline: true,
updateGfwCheck: false,
gfwCheckEnabled: true,
})
const hasEnabledField = computed(() => (
@@ -38,6 +41,7 @@ const hasEnabledField = computed(() => (
|| form.updateRate
|| form.updateGroups
|| form.updateAutoOnline
|| form.updateGfwCheck
))
function resetForm() {
@@ -49,6 +53,8 @@ function resetForm() {
form.groupIds = []
form.updateAutoOnline = false
form.autoOnline = true
form.updateGfwCheck = false
form.gfwCheckEnabled = true
}
function closeDialog() {
@@ -76,6 +82,7 @@ function handleSubmit() {
rate: form.updateRate ? Number(form.rate) : undefined,
group_ids: form.updateGroups ? [...new Set(form.groupIds.map((item) => String(item)))] : undefined,
auto_online: form.updateAutoOnline ? form.autoOnline : undefined,
gfw_check_enabled: form.updateGfwCheck ? form.gfwCheckEnabled : undefined,
})
}
@@ -188,6 +195,24 @@ watch(
<ElSwitch v-model="form.autoOnline" :disabled="!form.updateAutoOnline" />
</label>
</section>
<section class="batch-section">
<label class="batch-switch-card">
<div>
<strong>批量设置墙检测托管</strong>
<span>启用后父节点会自动检测子节点不独立检测只跟随父节点自动隐藏或恢复</span>
</div>
<ElSwitch v-model="form.updateGfwCheck" />
</label>
<label class="batch-switch-card batch-switch-card--nested">
<div>
<strong>{{ form.gfwCheckEnabled ? '开启墙检测托管' : '关闭墙检测托管' }}</strong>
<span>关闭后不会参与自动墙检测和墙状态自动显隐</span>
</div>
<ElSwitch v-model="form.gfwCheckEnabled" :disabled="!form.updateGfwCheck" />
</label>
</section>
</div>
<template #footer>
@@ -338,6 +338,13 @@ watch(
</div>
<ElSwitch v-model="form.autoOnline" />
</label>
<label class="switch-card">
<div>
<strong>墙检测托管</strong>
<span>{{ form.parentId ? '子节点不独立检测,只控制是否随父节点自动隐藏或恢复。' : '开启后后台会自动检测并在疑似被墙时隐藏。' }}</span>
</div>
<ElSwitch v-model="form.gfwCheckEnabled" />
</label>
<label class="switch-card">
<div>
<strong>启用节点</strong>
+78 -1
View File
@@ -35,6 +35,7 @@ import NodeEditorDialog from './NodeEditorDialog.vue'
import NodeSortDialog from './NodeSortDialog.vue'
import {
buildNodeTypeOptions,
countAutoGfwCheckNodes,
countAutoOnlineNodes,
countOnlineNodes,
countVisibleNodes,
@@ -77,6 +78,7 @@ const selectedNodeIds = ref<number[]>([])
const syncingSelection = ref(false)
const switchingIds = ref<number[]>([])
const autoSwitchingIds = ref<number[]>([])
const gfwSwitchingIds = ref<number[]>([])
const workingIds = ref<number[]>([])
const editorVisible = ref(false)
const editorMode = ref<NodeDialogMode>('create')
@@ -119,6 +121,7 @@ const summaryCards = computed(() => [
{ label: '在线节点', value: String(countOnlineNodes(nodes.value)) },
{ label: '显示中', value: String(countVisibleNodes(nodes.value)) },
{ label: '自动上线', value: String(countAutoOnlineNodes(nodes.value)) },
{ label: '自动墙检', value: String(countAutoGfwCheckNodes(nodes.value)) },
{ label: '已勾选', value: String(selectedNodes.value.length) },
])
@@ -166,6 +169,10 @@ function isAutoSwitching(id: number): boolean {
return autoSwitchingIds.value.includes(id)
}
function isGfwSwitching(id: number): boolean {
return gfwSwitchingIds.value.includes(id)
}
function isWorking(id: number): boolean {
return workingIds.value.includes(id)
}
@@ -293,6 +300,7 @@ async function handleBatchSubmit(payload: NodeBatchEditPayload) {
rate: payload.rate,
group_ids: payload.group_ids,
auto_online: payload.auto_online,
gfw_check_enabled: payload.gfw_check_enabled,
}
try {
@@ -453,6 +461,29 @@ async function handleToggleAutoOnline(node: AdminNodeItem, nextValue: boolean) {
}
}
async function handleToggleGfwCheck(node: AdminNodeItem, nextValue: boolean) {
const previous = node.gfw_check_enabled !== false
if (previous === nextValue) {
return
}
node.gfw_check_enabled = nextValue
markPending(gfwSwitchingIds, node.id, true)
try {
await updateNode({
id: node.id,
gfw_check_enabled: nextValue,
})
ElMessage.success(nextValue ? '已开启墙检测托管' : '已关闭墙检测托管')
} catch (error) {
node.gfw_check_enabled = previous
ElMessage.error(error instanceof Error ? error.message : '墙检测托管状态更新失败')
} finally {
markPending(gfwSwitchingIds, node.id, false)
}
}
async function handlePinTop(node: AdminNodeItem) {
const orderedNodes = sortNodesByOrder(nodes.value)
if (orderedNodes[0]?.id === node.id) {
@@ -652,6 +683,13 @@ watch(
<ElIcon><Connection /></ElIcon>
检测墙状态
</ElButton>
<ElButton
:loading="loading"
@click="loadNodeBoard"
>
<ElIcon><RefreshRight /></ElIcon>
刷新数据
</ElButton>
<ElButton
type="danger"
plain
@@ -720,13 +758,30 @@ watch(
<ElSwitch
:model-value="Boolean(row.show)"
:loading="isSwitching(row.id)"
:disabled="Boolean(row.auto_online)"
:disabled="Boolean(row.auto_online) || (Boolean(row.gfw_auto_hidden) && row.gfw_check_enabled !== false)"
@change="(value) => handleToggleShow(row, Boolean(value))"
/>
</div>
</template>
</ElTableColumn>
<ElTableColumn label="墙检测" width="118">
<template #default="{ row }">
<ElTooltip
:content="row.parent_id ? '子节点不单独检测;此开关只控制是否随父节点自动隐藏或恢复。' : '关闭后不参与自动墙检测和墙状态自动显隐。'"
placement="top"
>
<div class="switch-shell switch-shell--gfw">
<ElSwitch
:model-value="row.gfw_check_enabled !== false"
:loading="isGfwSwitching(row.id)"
@change="(value) => handleToggleGfwCheck(row, Boolean(value))"
/>
</div>
</ElTooltip>
</template>
</ElTableColumn>
<ElTableColumn label="自动上线" width="118">
<template #default="{ row }">
<div class="switch-shell switch-shell--auto">
@@ -759,6 +814,24 @@ watch(
>
自动上线
</ElTag>
<ElTag
v-if="row.gfw_check_enabled !== false"
round
effect="plain"
type="primary"
class="auto-online-tag"
>
墙检测
</ElTag>
<ElTag
v-if="row.gfw_auto_hidden"
round
effect="plain"
type="danger"
class="auto-online-tag"
>
自动隐藏
</ElTag>
<ElTooltip :content="getNodeGfwTooltip(row)" placement="top">
<ElTag
round
@@ -1048,6 +1121,10 @@ watch(
--el-switch-on-color: #0071e3;
}
.switch-shell--gfw :deep(.el-switch) {
--el-switch-on-color: #34c759;
}
.node-cell,
.stack-cell,
.online-cell {