fix tenant account unbind flow
Desktop Client Build / Resolve Build Metadata (push) Successful in 28s
Frontend CI / Frontend (push) Successful in 2m53s
Backend CI / Backend (push) Successful in 16m10s
Desktop Client Build / Build Desktop Client (push) Successful in 23m44s
Desktop Client Build / Publish Client Artifacts to NAS (push) Successful in 50s
Desktop Client Build / Resolve Build Metadata (push) Successful in 28s
Frontend CI / Frontend (push) Successful in 2m53s
Backend CI / Backend (push) Successful in 16m10s
Desktop Client Build / Build Desktop Client (push) Successful in 23m44s
Desktop Client Build / Publish Client Artifacts to NAS (push) Successful in 50s
This commit is contained in:
@@ -1292,6 +1292,13 @@ export const tenantAccountsApi = {
|
||||
list() {
|
||||
return apiClient.get<DesktopAccountInfo[]>('/api/tenant/accounts')
|
||||
},
|
||||
remove(id: string) {
|
||||
return apiClient.remove<{
|
||||
account: DesktopAccountInfo
|
||||
cancelled_queued_publish_task_count: number
|
||||
completed_server_side_unbinding: boolean
|
||||
}>(`/api/tenant/accounts/${encodeURIComponent(id)}`)
|
||||
},
|
||||
requestDelete(id: string, options?: { undo?: boolean }) {
|
||||
const query = options?.undo ? '?undo=true' : ''
|
||||
return apiClient.post<DesktopAccountInfo>(
|
||||
|
||||
@@ -140,6 +140,7 @@ const errorMessageMap: Record<string, string> = {
|
||||
invalid_publish_enterprise_site_targets: '企业站点发布目标格式不正确',
|
||||
desktop_account_client_missing: '所选账号还没有绑定桌面客户端,暂时无法发布',
|
||||
desktop_account_not_found: '所选桌面账号不存在,请刷新后重试',
|
||||
publish_task_cancel_account_unbind_failed: '解绑账号时取消排队发布任务失败,请稍后重试',
|
||||
desktop_client_offline: '当前登录账号的桌面客户端未在线,请先打开 desktop-client',
|
||||
desktop_publish_duplicate: '内容已在发布队列中,未重复提交',
|
||||
desktop_publish_job_store_unavailable: '发布队列暂时不可用,请稍后重试',
|
||||
|
||||
@@ -130,6 +130,9 @@ export function buildPublishAccountCard(
|
||||
}
|
||||
|
||||
export function resolvePublishState(account: DesktopAccountInfo): PublishState {
|
||||
if (account.delete_requested_at) {
|
||||
return 'unavailable'
|
||||
}
|
||||
if (resolveAccountHealth(account) !== 'live') {
|
||||
return 'unavailable'
|
||||
}
|
||||
@@ -250,6 +253,9 @@ export function resolvePlatformLogoUrl(
|
||||
}
|
||||
|
||||
function resolvePublishStatusText(account: DesktopAccountInfo, publishState: PublishState): string {
|
||||
if (account.delete_requested_at) {
|
||||
return '此运行节点绑定正在解绑中,不会再接收新的发布任务。'
|
||||
}
|
||||
if (publishState === 'immediate') {
|
||||
return '任务消费通道在线,发布任务会立即被桌面端领取执行。'
|
||||
}
|
||||
|
||||
@@ -239,6 +239,23 @@ const requestAccountDeleteMutation = useMutation({
|
||||
},
|
||||
})
|
||||
|
||||
const removeAccountMutation = useMutation({
|
||||
mutationFn: (id: string) => tenantAccountsApi.remove(id),
|
||||
onSuccess: async (result) => {
|
||||
const cancelledCount = result.cancelled_queued_publish_task_count ?? 0
|
||||
message.success(
|
||||
cancelledCount > 0
|
||||
? `已完成解绑,并取消 ${cancelledCount} 个排队发布任务`
|
||||
: '已完成解绑',
|
||||
)
|
||||
await accountsQuery.refetch()
|
||||
},
|
||||
onError: (error) => message.error(formatError(error)),
|
||||
onSettled: () => {
|
||||
requestingAccountDeleteId.value = null
|
||||
},
|
||||
})
|
||||
|
||||
const loading = computed(() => platformsQuery.isPending.value || accountsQuery.isPending.value)
|
||||
const refreshing = computed(
|
||||
() =>
|
||||
@@ -389,6 +406,9 @@ const downloadableReleasesError = computed(() => {
|
||||
})
|
||||
|
||||
function resolvePublishState(account: DesktopAccountInfo): PublishState {
|
||||
if (account.delete_requested_at) {
|
||||
return 'unavailable'
|
||||
}
|
||||
if (resolveAccountHealth(account) !== 'live') {
|
||||
return 'unavailable'
|
||||
}
|
||||
@@ -399,6 +419,9 @@ function resolvePublishState(account: DesktopAccountInfo): PublishState {
|
||||
}
|
||||
|
||||
function resolvePublishHint(account: DesktopAccountInfo, publishState: PublishState): string {
|
||||
if (account.delete_requested_at) {
|
||||
return '此运行节点绑定正在解绑中,不会再接收新的发布任务。'
|
||||
}
|
||||
if (publishState === 'immediate') {
|
||||
return ''
|
||||
}
|
||||
@@ -703,10 +726,10 @@ async function handleConfirmUnbind(): Promise<void> {
|
||||
unbindConfirmLoading.value = true
|
||||
try {
|
||||
requestingAccountDeleteId.value = unbindAccountTarget.value.id
|
||||
await requestAccountDeleteMutation.mutateAsync({ id: unbindAccountTarget.value.id })
|
||||
await removeAccountMutation.mutateAsync(unbindAccountTarget.value.id)
|
||||
unbindModalOpen.value = false
|
||||
} catch (err) {
|
||||
// Error is handled by requestAccountDeleteMutation onError
|
||||
// Error is handled by removeAccountMutation onError
|
||||
} finally {
|
||||
unbindConfirmLoading.value = false
|
||||
}
|
||||
@@ -719,7 +742,8 @@ function undoUnbindAccount(account: MediaAccountCard): void {
|
||||
|
||||
function isAccountDeleteRequestPending(accountId: string): boolean {
|
||||
return (
|
||||
requestAccountDeleteMutation.isPending.value && requestingAccountDeleteId.value === accountId
|
||||
(requestAccountDeleteMutation.isPending.value || removeAccountMutation.isPending.value) &&
|
||||
requestingAccountDeleteId.value === accountId
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1365,6 +1389,15 @@ function releaseSize(value?: number | null): string {
|
||||
</a-button>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item
|
||||
v-if="account.deleteRequestedAt"
|
||||
key="complete-unbind"
|
||||
danger
|
||||
@click="requestUnbindAccount(account)"
|
||||
>
|
||||
<DeleteOutlined />
|
||||
<span>完成解绑</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item
|
||||
v-if="account.deleteRequestedAt"
|
||||
key="undo-unbind"
|
||||
@@ -1443,7 +1476,7 @@ function releaseSize(value?: number | null): string {
|
||||
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"
|
||||
/>
|
||||
</svg>
|
||||
<span>已申请解绑账号</span>
|
||||
<span>已申请解绑,可完成服务端解绑</span>
|
||||
</div>
|
||||
<div v-else-if="account.publishHint" class="footer-alert">
|
||||
<svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor">
|
||||
@@ -1473,8 +1506,10 @@ function releaseSize(value?: number | null): string {
|
||||
<div class="unbind-warning-icon-wrapper">
|
||||
<DeleteOutlined class="unbind-warning-icon" />
|
||||
</div>
|
||||
<h3 class="unbind-modal-title">申请解绑媒体账号</h3>
|
||||
<p class="unbind-modal-subtitle">请仔细阅读以下安全提示,解绑后可重新绑定。</p>
|
||||
<h3 class="unbind-modal-title">解绑此运行节点上的媒体账号</h3>
|
||||
<p class="unbind-modal-subtitle">
|
||||
只解除当前卡片对应的运行节点绑定,同一平台账号在其他节点上的绑定不受影响。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="unbind-account-preview" v-if="unbindAccountTarget">
|
||||
@@ -1502,6 +1537,9 @@ function releaseSize(value?: number | null): string {
|
||||
<span class="preview-meta">
|
||||
{{ unbindAccountTarget.platformLabel }} · ID: {{ unbindAccountTarget.platformUid }}
|
||||
</span>
|
||||
<span class="preview-meta">
|
||||
运行节点 · {{ unbindAccountTarget.clientDeviceName || '未命名客户端' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="preview-status">
|
||||
<span
|
||||
@@ -1520,7 +1558,7 @@ function releaseSize(value?: number | null): string {
|
||||
<ExclamationCircleOutlined />
|
||||
</div>
|
||||
<div class="consequence-text">
|
||||
<strong>解除服务绑定</strong>:系统服务端将彻底清除此账号的授权同步及分发配置。
|
||||
<strong>解除节点绑定</strong>:系统服务端将移除此运行节点上的账号映射,这张卡片不再接收发布任务。
|
||||
</div>
|
||||
</div>
|
||||
<div class="consequence-item">
|
||||
@@ -1528,12 +1566,12 @@ function releaseSize(value?: number | null): string {
|
||||
<LockOutlined />
|
||||
</div>
|
||||
<div class="consequence-text">
|
||||
<strong>清理会话缓存</strong>:
|
||||
<strong>处理本地会话</strong>:
|
||||
<span v-if="unbindAccountTarget.clientOnline">
|
||||
在线客户端将即时收到通知并清理所有登录会话 Cookie。
|
||||
在线客户端同步后会清理当前节点的本地会话缓存。
|
||||
</span>
|
||||
<span v-else>
|
||||
客户端不在线,系统已将解绑申请加入队列。客户端上线后将自动同步清理本地会话。
|
||||
当前运行节点离线,服务端仍会立即完成解绑;该节点下次同步时会清理本地会话缓存。
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1542,7 +1580,7 @@ function releaseSize(value?: number | null): string {
|
||||
<CloseCircleOutlined />
|
||||
</div>
|
||||
<div class="consequence-text">
|
||||
<strong>分发任务失效</strong>:该账号将无法接收并执行任何发布任务,再次使用必须重新扫码登录授权。
|
||||
<strong>取消排队发布</strong>:发往此运行节点此账号的等待发布任务会被取消,其他节点上的同账号任务不受影响。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1213,6 +1213,8 @@ async function syncAccounts(source: 'startup' | 'manual' | 'timer'): Promise<voi
|
||||
state.lastAccountsSyncAt = Date.now()
|
||||
state.lastError = null
|
||||
|
||||
await cleanupRemovedRuntimeAccounts(accounts)
|
||||
|
||||
noteSchedulerAccountsSync()
|
||||
setSchedulerError(null)
|
||||
|
||||
@@ -1326,6 +1328,35 @@ async function syncAccounts(source: 'startup' | 'manual' | 'timer'): Promise<voi
|
||||
}
|
||||
}
|
||||
|
||||
async function cleanupRemovedRuntimeAccounts(accounts: DesktopAccountInfo[]): Promise<void> {
|
||||
const nextAccountIds = new Set(accounts.map((account) => account.id))
|
||||
const removedAccounts = state.accounts.filter((account) => !nextAccountIds.has(account.id))
|
||||
if (removedAccounts.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
for (const account of removedAccounts) {
|
||||
try {
|
||||
await clearSessionHandle(account.id)
|
||||
forgetTrackedAccountHealth(account.id)
|
||||
state.accountProfiles.delete(account.id)
|
||||
recordActivity(
|
||||
'info',
|
||||
'账号已完成解绑',
|
||||
`${account.display_name} 已从服务端解除绑定,本机本地会话缓存已清理。`,
|
||||
)
|
||||
} catch (error) {
|
||||
console.warn('[desktop-runtime] removed desktop account cleanup failed', {
|
||||
accountId: account.id,
|
||||
platform: account.platform,
|
||||
platformUid: account.platform_uid,
|
||||
message: errorMessage(error),
|
||||
})
|
||||
recordActivity('warn', '账号本地清理失败', `${account.display_name}:${errorMessage(error)}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function consumeDeleteRequestedAccounts(
|
||||
accounts: DesktopAccountInfo[],
|
||||
): Promise<DesktopAccountInfo[]> {
|
||||
|
||||
Reference in New Issue
Block a user