feat(desktop): add batch account probes

This commit is contained in:
2026-05-06 18:21:08 +08:00
parent f3487bcacf
commit bb7221d674
3 changed files with 242 additions and 2 deletions
@@ -26,7 +26,7 @@ const REGULAR_PROBE_MAX_MS = STALE_AFTER_MS
const CONFIRM_BACKOFF_STEPS_MS = [30_000, 120_000] as const
const NETWORK_RETRY_MS = 2 * 60_000
const PREFLIGHT_MAX_AGE_MS = 15 * 60_000
const MAX_CONCURRENT_ACCOUNT_PROBES = 3
const MAX_CONCURRENT_ACCOUNT_PROBES = 2
const MANUAL_PROBE_CACHE_TTL_MS = 5 * 60_000
const MANUAL_PROBE_MIN_INTERVAL_MS = 15_000
const ACCOUNT_PROBE_RETRY_BACKOFF_MS = [1_000, 3_000, 10_000] as const
@@ -5,9 +5,11 @@ import {
DeleteOutlined,
LinkOutlined,
ReloadOutlined,
SafetyCertificateOutlined,
SearchOutlined,
SyncOutlined,
} from '@ant-design/icons-vue'
import { notification } from 'ant-design-vue'
import { computed, ref } from 'vue'
import { useDesktopRuntime } from '../composables/useDesktopRuntime'
@@ -26,6 +28,10 @@ const consolePendingAccountId = ref<string | null>(null)
const unbindPendingAccountId = ref<string | null>(null)
const reauthorizePendingAccountId = ref<string | null>(null)
const probePendingAccountIds = ref(new Set<string>())
const batchProbePending = ref(false)
const batchProbeRunningAccountIds = ref<readonly string[]>([])
const batchProbeQueuedAccountIds = ref<readonly string[]>([])
const batchProbeTotalCount = ref(0)
const actionSuccess = ref<string | null>(null)
type AccountAuthState = 'authorized' | 'expired' | 'attention' | 'risk'
@@ -43,6 +49,18 @@ const overview = computed(() => ({
onlineClients: snapshot.value?.summary.onlineClients ?? 0,
}))
const batchProbeButtonLabel = computed(() => {
if (!batchProbePending.value) {
return '一键检测账号'
}
const finishedCount =
batchProbeTotalCount.value -
batchProbeRunningAccountIds.value.length -
batchProbeQueuedAccountIds.value.length
return `检测中 ${Math.max(0, finishedCount)}/${batchProbeTotalCount.value}`
})
const platformCards = computed(() =>
desktopPublishMediaCatalog.map((platform) => {
const matched = accounts.value.filter((account) => account.platform === platform.id)
@@ -280,6 +298,96 @@ function isProbePending(account: AccountRow): boolean {
)
}
async function runLimitedAccountProbes(
targetAccounts: readonly AccountRow[],
concurrency = 2,
): Promise<Error[]> {
const queue = [...targetAccounts]
let cursor = 0
const failures: Error[] = []
const runNext = async () => {
while (cursor < queue.length) {
const account = queue[cursor]
cursor += 1
batchProbeQueuedAccountIds.value = batchProbeQueuedAccountIds.value.filter(
(accountId) => accountId !== account.id,
)
batchProbeRunningAccountIds.value = [...batchProbeRunningAccountIds.value, account.id]
probePendingAccountIds.value = new Set([...probePendingAccountIds.value, account.id])
try {
await probeAccount(account.id)
} catch (error) {
failures.push(error instanceof Error ? error : new Error(String(error)))
} finally {
const next = new Set(probePendingAccountIds.value)
next.delete(account.id)
probePendingAccountIds.value = next
batchProbeRunningAccountIds.value = batchProbeRunningAccountIds.value.filter(
(accountId) => accountId !== account.id,
)
}
}
}
await Promise.all(
Array.from({ length: Math.min(concurrency, queue.length) }, () => runNext()),
)
return failures
}
async function probeAllAccounts() {
if (batchProbePending.value) {
return
}
const targetAccounts = accounts.value
if (targetAccounts.length === 0) {
notification.info({
message: '暂无可检测账号',
description: '请先绑定至少一个媒体账号。',
placement: 'topRight',
duration: 3.2,
})
return
}
batchProbePending.value = true
batchProbeRunningAccountIds.value = []
batchProbeQueuedAccountIds.value = targetAccounts.map((account) => account.id)
batchProbeTotalCount.value = targetAccounts.length
actionSuccess.value = null
try {
const failures = await runLimitedAccountProbes(targetAccounts, 2)
await refresh()
if (failures.length > 0) {
notification.warning({
message: '检测完成,部分失败',
description: `已检测 ${targetAccounts.length} 个媒体账号,其中 ${failures.length} 个检测失败。`,
placement: 'topRight',
duration: 4,
})
return
}
notification.success({
message: '检测完成',
description: `已检测 ${targetAccounts.length} 个媒体账号。`,
placement: 'topRight',
duration: 3.2,
})
} catch (error) {
showClientActionError('probe-account', error)
await refresh()
} finally {
batchProbePending.value = false
batchProbeRunningAccountIds.value = []
batchProbeQueuedAccountIds.value = []
batchProbeTotalCount.value = 0
}
}
async function recoverAccount(account: AccountRow) {
if (authState(account) !== 'expired') {
await verifyAccount(account)
@@ -322,11 +430,22 @@ const tableColumns = [
<h2>媒体账号管理</h2>
</div>
<div class="hero-actions">
<a-button
type="primary"
class="modern-btn"
:loading="batchProbePending"
:disabled="loading || accounts.length === 0"
@click="probeAllAccounts"
>
<template #icon><SafetyCertificateOutlined /></template>
{{ batchProbeButtonLabel }}
</a-button>
<a-button
type="primary"
ghost
class="modern-btn"
:loading="loading"
:disabled="batchProbePending"
@click="refreshAccounts"
>
<template #icon><ReloadOutlined /></template>
@@ -658,6 +777,12 @@ const tableColumns = [
padding: 36px 40px;
}
.hero-actions {
display: flex;
align-items: center;
gap: 10px;
}
.eyebrow {
margin: 0 0 12px;
color: #64748b;
@@ -2,6 +2,7 @@
import {
ArrowRightOutlined,
DeleteOutlined,
SafetyCertificateOutlined,
LinkOutlined,
ReloadOutlined,
WarningOutlined,
@@ -17,11 +18,15 @@ import type { RuntimeAccount } from '../types'
type AccountRow = Readonly<Omit<RuntimeAccount, 'tags'>> & { tags: readonly string[] }
const { snapshot, refresh, refreshAccounts, loading } = useDesktopRuntime()
const { snapshot, refresh, refreshAccounts, probeAccount, loading } = useDesktopRuntime()
const bindPendingPlatformId = ref<string | null>(null)
const openPendingAccountId = ref<string | null>(null)
const unbindPendingAccountId = ref<string | null>(null)
const batchProbePending = ref(false)
const batchProbeRunningAccountIds = ref<readonly string[]>([])
const batchProbeQueuedAccountIds = ref<readonly string[]>([])
const batchProbeTotalCount = ref(0)
const aiAccounts = computed(() =>
(snapshot.value?.accounts ?? []).filter((account) =>
@@ -53,6 +58,24 @@ const overview = computed(() => ({
onlineClients: snapshot.value?.summary.onlineClients ?? 0,
}))
const boundAIAccounts = computed(() =>
platformCards.value
.map((platform) => platform.account)
.filter((account): account is AccountRow => Boolean(account)),
)
const batchProbeButtonLabel = computed(() => {
if (!batchProbePending.value) {
return '一键检测账号'
}
const finishedCount =
batchProbeTotalCount.value -
batchProbeRunningAccountIds.value.length -
batchProbeQueuedAccountIds.value.length
return `检测中 ${Math.max(0, finishedCount)}/${batchProbeTotalCount.value}`
})
function authLabel(account: AccountRow | null) {
if (!account) {
return '未绑定'
@@ -181,6 +204,81 @@ function showActionNotification(type: 'success' | 'info', title: string, descrip
})
}
async function runLimitedAccountProbes(
accounts: readonly AccountRow[],
concurrency = 2,
): Promise<Error[]> {
const queue = [...accounts]
let cursor = 0
const failures: Error[] = []
const runNext = async () => {
while (cursor < queue.length) {
const account = queue[cursor]
cursor += 1
batchProbeQueuedAccountIds.value = batchProbeQueuedAccountIds.value.filter(
(accountId) => accountId !== account.id,
)
batchProbeRunningAccountIds.value = [...batchProbeRunningAccountIds.value, account.id]
try {
await probeAccount(account.id)
} catch (error) {
failures.push(error instanceof Error ? error : new Error(String(error)))
} finally {
batchProbeRunningAccountIds.value = batchProbeRunningAccountIds.value.filter(
(accountId) => accountId !== account.id,
)
}
}
}
await Promise.all(
Array.from({ length: Math.min(concurrency, queue.length) }, () => runNext()),
)
return failures
}
async function probeAllAIAccounts() {
if (batchProbePending.value) {
return
}
const accounts = boundAIAccounts.value
if (accounts.length === 0) {
showActionNotification('info', '暂无可检测账号', '请先绑定至少一个 AI 平台账号。')
return
}
batchProbePending.value = true
batchProbeRunningAccountIds.value = []
batchProbeQueuedAccountIds.value = accounts.map((account) => account.id)
batchProbeTotalCount.value = accounts.length
try {
const failures = await runLimitedAccountProbes(accounts, 2)
await refresh()
if (failures.length > 0) {
notification.warning({
message: '检测完成,部分失败',
description: `已检测 ${accounts.length} 个 AI 平台账号,其中 ${failures.length} 个检测失败。`,
placement: 'topRight',
duration: 4,
})
return
}
showActionNotification('success', '检测完成', `已检测 ${accounts.length} 个 AI 平台账号。`)
} catch (error) {
showClientActionError('probe-account', error)
await refresh()
} finally {
batchProbePending.value = false
batchProbeRunningAccountIds.value = []
batchProbeQueuedAccountIds.value = []
batchProbeTotalCount.value = 0
}
}
async function bindPlatform(platformId: string) {
bindPendingPlatformId.value = platformId
@@ -246,11 +344,22 @@ async function unbindPlatform(account: AccountRow) {
<h2>AI 平台管理</h2>
</div>
<div class="hero-actions">
<a-button
type="primary"
class="modern-btn"
:loading="batchProbePending"
:disabled="loading || boundAIAccounts.length === 0"
@click="probeAllAIAccounts"
>
<template #icon><SafetyCertificateOutlined /></template>
{{ batchProbeButtonLabel }}
</a-button>
<a-button
type="primary"
ghost
class="modern-btn"
:loading="loading"
:disabled="batchProbePending"
@click="refreshAccounts"
>
<template #icon><ReloadOutlined /></template>
@@ -457,6 +566,12 @@ async function unbindPlatform(account: AccountRow) {
padding: 36px 40px;
}
.hero-actions {
display: flex;
align-items: center;
gap: 10px;
}
.eyebrow {
margin: 0 0 12px;
color: #64748b;