feat(desktop): add batch account probes
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user