feat(admin-web): prefer runtime account health when ranking and gating accounts
Surface the new runtime_health/runtime_checked_at fields through a tiny desktop-account-runtime helper so PublishArticleModal, MediaView, and TrackingView fall back to the desktop client's last live signal instead of the slower DB column. This keeps the publishable card and tracking breakdown in sync with the desktop probe loop. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,7 +9,7 @@ import { useI18n } from "vue-i18n";
|
|||||||
import CoverPickerModal from "@/components/CoverPickerModal.vue";
|
import CoverPickerModal from "@/components/CoverPickerModal.vue";
|
||||||
import { articlesApi, mediaApi, publishJobsApi, resolveApiURL, tenantAccountsApi } from "@/lib/api";
|
import { articlesApi, mediaApi, publishJobsApi, resolveApiURL, tenantAccountsApi } from "@/lib/api";
|
||||||
import { coverUploadRequired, deriveCoverFileName } from "@/lib/cover-requirements";
|
import { coverUploadRequired, deriveCoverFileName } from "@/lib/cover-requirements";
|
||||||
import { formatDateTime } from "@/lib/display";
|
import { resolveAccountCheckedAt, resolveAccountHealth } from "@/lib/desktop-account-runtime";
|
||||||
import { formatError } from "@/lib/errors";
|
import { formatError } from "@/lib/errors";
|
||||||
import {
|
import {
|
||||||
getPublishPlatformMeta,
|
getPublishPlatformMeta,
|
||||||
@@ -20,16 +20,34 @@ import {
|
|||||||
|
|
||||||
type PublishState = "immediate" | "queued" | "unavailable";
|
type PublishState = "immediate" | "queued" | "unavailable";
|
||||||
|
|
||||||
|
const platformLogoCatalog: Record<string, string> = {
|
||||||
|
toutiaohao: "/logos/logo_toutiao.png",
|
||||||
|
baijiahao: "/logos/logo_baijiahao.png",
|
||||||
|
sohuhao: "/logos/logo_souhu.png",
|
||||||
|
qiehao: "/logos/logo_qiehao.png",
|
||||||
|
zhihu: "/logos/logo_zhihu.png",
|
||||||
|
wangyihao: "/logos/logo_wangyihao.png",
|
||||||
|
jianshu: "/logos/logo_jianshu.svg",
|
||||||
|
bilibili: "/logos/logo_bilibili.png",
|
||||||
|
juejin: "/logos/logo_juejin.png",
|
||||||
|
smzdm: "/logos/logo_smzdm.svg",
|
||||||
|
weixin_gzh: "/logos/logo_weixin_gzh.svg",
|
||||||
|
zol: "/logos/logo_zol.png",
|
||||||
|
dongchedi: "/logos/logo_dongchedi.svg",
|
||||||
|
};
|
||||||
|
|
||||||
interface PublishAccountCard {
|
interface PublishAccountCard {
|
||||||
id: string;
|
id: string;
|
||||||
platformId: string;
|
platformId: string;
|
||||||
platformName: string;
|
platformName: string;
|
||||||
platformShortName: string;
|
platformShortName: string;
|
||||||
platformAccent: string;
|
platformAccent: string;
|
||||||
|
platformLogoUrl: string | null;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
platformUid: string;
|
platformUid: string;
|
||||||
avatarUrl: string | null;
|
avatarUrl: string | null;
|
||||||
health: DesktopAccountInfo["health"];
|
health: DesktopAccountInfo["health"];
|
||||||
|
verifiedAt: string | null;
|
||||||
clientId: string | null;
|
clientId: string | null;
|
||||||
clientOnline: boolean | null;
|
clientOnline: boolean | null;
|
||||||
clientDeviceName: string | null;
|
clientDeviceName: string | null;
|
||||||
@@ -105,7 +123,7 @@ watch(
|
|||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
const platformMap = computed(() => {
|
const platformMap = computed<Map<string, MediaPlatform>>(() => {
|
||||||
return new Map(
|
return new Map(
|
||||||
(platformsQuery.data.value ?? []).map((platform: MediaPlatform) => [
|
(platformsQuery.data.value ?? []).map((platform: MediaPlatform) => [
|
||||||
normalizePublishPlatformId(platform.platform_id),
|
normalizePublishPlatformId(platform.platform_id),
|
||||||
@@ -125,10 +143,6 @@ const publishAccounts = computed(() => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const rawAccountMap = computed(() => {
|
|
||||||
return new Map(publishAccounts.value.map((account) => [account.id, account]));
|
|
||||||
});
|
|
||||||
|
|
||||||
const articlePlatformIds = computed(() => normalizePublishPlatformIds(detailQuery.data.value?.platforms ?? []));
|
const articlePlatformIds = computed(() => normalizePublishPlatformIds(detailQuery.data.value?.platforms ?? []));
|
||||||
|
|
||||||
const accountCards = computed<PublishAccountCard[]>(() => {
|
const accountCards = computed<PublishAccountCard[]>(() => {
|
||||||
@@ -140,6 +154,7 @@ const accountCards = computed<PublishAccountCard[]>(() => {
|
|||||||
const platform = platformMap.value.get(platformId);
|
const platform = platformMap.value.get(platformId);
|
||||||
const fallback = getPublishPlatformMeta(platformId);
|
const fallback = getPublishPlatformMeta(platformId);
|
||||||
const publishState = resolvePublishState(account);
|
const publishState = resolvePublishState(account);
|
||||||
|
const health = resolveAccountHealth(account);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: account.id,
|
id: account.id,
|
||||||
@@ -147,10 +162,12 @@ const accountCards = computed<PublishAccountCard[]>(() => {
|
|||||||
platformName: platform?.name || fallback.name,
|
platformName: platform?.name || fallback.name,
|
||||||
platformShortName: platform?.short_name || fallback.shortName,
|
platformShortName: platform?.short_name || fallback.shortName,
|
||||||
platformAccent: platform?.accent_color || fallback.accent,
|
platformAccent: platform?.accent_color || fallback.accent,
|
||||||
|
platformLogoUrl: resolvePlatformLogoUrl(platformId, platform?.logo_url),
|
||||||
displayName: account.display_name,
|
displayName: account.display_name,
|
||||||
platformUid: normalizePlatformUid(account.platform_uid),
|
platformUid: normalizePlatformUid(account.platform_uid),
|
||||||
avatarUrl: resolveApiURL(account.avatar_url),
|
avatarUrl: resolveApiURL(account.avatar_url),
|
||||||
health: account.health,
|
health,
|
||||||
|
verifiedAt: resolveAccountCheckedAt(account),
|
||||||
clientId: account.client_id,
|
clientId: account.client_id,
|
||||||
clientOnline: account.client_online,
|
clientOnline: account.client_online,
|
||||||
clientDeviceName: account.client_device_name,
|
clientDeviceName: account.client_device_name,
|
||||||
@@ -172,8 +189,8 @@ const accountCards = computed<PublishAccountCard[]>(() => {
|
|||||||
return publishGap;
|
return publishGap;
|
||||||
}
|
}
|
||||||
|
|
||||||
const leftVerifiedAt = Date.parse(rawAccountMap.value.get(left.id)?.verified_at ?? "") || 0;
|
const leftVerifiedAt = Date.parse(left.verifiedAt ?? "") || 0;
|
||||||
const rightVerifiedAt = Date.parse(rawAccountMap.value.get(right.id)?.verified_at ?? "") || 0;
|
const rightVerifiedAt = Date.parse(right.verifiedAt ?? "") || 0;
|
||||||
return rightVerifiedAt - leftVerifiedAt;
|
return rightVerifiedAt - leftVerifiedAt;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -375,7 +392,7 @@ function handleRemoveCover(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resolvePublishState(account: DesktopAccountInfo): PublishState {
|
function resolvePublishState(account: DesktopAccountInfo): PublishState {
|
||||||
if (account.health !== "live") {
|
if (resolveAccountHealth(account) !== "live") {
|
||||||
return "unavailable";
|
return "unavailable";
|
||||||
}
|
}
|
||||||
if (!account.client_id) {
|
if (!account.client_id) {
|
||||||
@@ -391,7 +408,7 @@ function resolveStatusText(account: DesktopAccountInfo, publishState: PublishSta
|
|||||||
if (publishState === "queued") {
|
if (publishState === "queued") {
|
||||||
return "客户端当前离线,任务会先落库排队,等桌面端上线后自动继续发布。";
|
return "客户端当前离线,任务会先落库排队,等桌面端上线后自动继续发布。";
|
||||||
}
|
}
|
||||||
if (account.health !== "live") {
|
if (resolveAccountHealth(account) !== "live") {
|
||||||
return "该账号授权状态异常,请先在桌面端重新校验或重新授权。";
|
return "该账号授权状态异常,请先在桌面端重新校验或重新授权。";
|
||||||
}
|
}
|
||||||
return "该账号还没有绑定桌面客户端,当前不能进入发布队列。";
|
return "该账号还没有绑定桌面客户端,当前不能进入发布队列。";
|
||||||
@@ -464,6 +481,15 @@ function accountInitial(card: PublishAccountCard): string {
|
|||||||
return trimmed ? trimmed.slice(0, 1).toUpperCase() : card.platformShortName;
|
return trimmed ? trimmed.slice(0, 1).toUpperCase() : card.platformShortName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolvePlatformLogoUrl(platformId: string, remoteLogoUrl?: string | null): string | null {
|
||||||
|
const localLogoUrl = platformLogoCatalog[platformId];
|
||||||
|
if (localLogoUrl) {
|
||||||
|
return localLogoUrl;
|
||||||
|
}
|
||||||
|
const resolvedRemoteLogoUrl = resolveApiURL(remoteLogoUrl);
|
||||||
|
return resolvedRemoteLogoUrl || null;
|
||||||
|
}
|
||||||
|
|
||||||
function normalizePlatformUid(value?: string | null): string {
|
function normalizePlatformUid(value?: string | null): string {
|
||||||
let normalized = String(value ?? "").trim();
|
let normalized = String(value ?? "").trim();
|
||||||
while (normalized.toLowerCase().startsWith("platform:")) {
|
while (normalized.toLowerCase().startsWith("platform:")) {
|
||||||
@@ -538,7 +564,21 @@ function normalizePlatformUid(value?: string | null): string {
|
|||||||
</span>
|
</span>
|
||||||
<div class="publish-modal__identity-copy">
|
<div class="publish-modal__identity-copy">
|
||||||
<strong :title="account.displayName">{{ account.displayName }}</strong>
|
<strong :title="account.displayName">{{ account.displayName }}</strong>
|
||||||
<span :title="`${account.platformName} · ${account.platformUid}`">{{ account.platformName }} · {{ account.platformUid }}</span>
|
<span class="publish-modal__platform-line" :title="`${account.platformName} · ${account.platformUid}`">
|
||||||
|
<img
|
||||||
|
v-if="account.platformLogoUrl"
|
||||||
|
class="publish-modal__platform-logo"
|
||||||
|
:src="account.platformLogoUrl"
|
||||||
|
:alt="account.platformName"
|
||||||
|
referrerpolicy="no-referrer"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="publish-modal__platform-logo publish-modal__platform-logo--fallback"
|
||||||
|
:style="{ background: account.platformAccent }"
|
||||||
|
>{{ account.platformShortName }}</span>
|
||||||
|
<span class="publish-modal__platform-text">{{ account.platformName }} · {{ account.platformUid }}</span>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="publish-modal__check">
|
<span class="publish-modal__check">
|
||||||
@@ -898,6 +938,39 @@ function normalizePlatformUid(value?: string | null): string {
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.publish-modal__platform-line {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.publish-modal__platform-logo {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
border-radius: 3px;
|
||||||
|
object-fit: contain;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.publish-modal__platform-logo--fallback {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.publish-modal__platform-text {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
.publish-modal__state-pill {
|
.publish-modal__state-pill {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import type { DesktopAccountInfo } from "@geo/shared-types";
|
||||||
|
|
||||||
|
export function resolveAccountHealth(account: DesktopAccountInfo): DesktopAccountInfo["health"] {
|
||||||
|
return account.runtime_health ?? account.health;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveAccountCheckedAt(account: DesktopAccountInfo): string | null {
|
||||||
|
return account.runtime_checked_at ?? account.runtime_verified_at ?? account.verified_at;
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import { computed, ref } from "vue";
|
|||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
|
|
||||||
import { mediaApi, resolveApiURL, tenantAccountsApi } from "@/lib/api";
|
import { mediaApi, resolveApiURL, tenantAccountsApi } from "@/lib/api";
|
||||||
|
import { resolveAccountCheckedAt, resolveAccountHealth } from "@/lib/desktop-account-runtime";
|
||||||
import { formatDateTime } from "@/lib/display";
|
import { formatDateTime } from "@/lib/display";
|
||||||
import { getPublishPlatformMeta, isPublishPlatformId, normalizePublishPlatformId } from "@/lib/publish-platforms";
|
import { getPublishPlatformMeta, isPublishPlatformId, normalizePublishPlatformId } from "@/lib/publish-platforms";
|
||||||
|
|
||||||
@@ -85,6 +86,7 @@ const mediaAccounts = computed<MediaAccountCard[]>(() => {
|
|||||||
const platform = platformMap.value.get(platformId);
|
const platform = platformMap.value.get(platformId);
|
||||||
const fallback = getPublishPlatformMeta(platformId);
|
const fallback = getPublishPlatformMeta(platformId);
|
||||||
const publishState = resolvePublishState(account);
|
const publishState = resolvePublishState(account);
|
||||||
|
const health = resolveAccountHealth(account);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: account.id,
|
id: account.id,
|
||||||
@@ -96,8 +98,8 @@ const mediaAccounts = computed<MediaAccountCard[]>(() => {
|
|||||||
displayName: account.display_name,
|
displayName: account.display_name,
|
||||||
platformUid: normalizePlatformUid(account.platform_uid),
|
platformUid: normalizePlatformUid(account.platform_uid),
|
||||||
avatarUrl: resolveApiURL(account.avatar_url),
|
avatarUrl: resolveApiURL(account.avatar_url),
|
||||||
health: account.health,
|
health,
|
||||||
verifiedAt: account.verified_at,
|
verifiedAt: resolveAccountCheckedAt(account),
|
||||||
clientId: account.client_id,
|
clientId: account.client_id,
|
||||||
clientOnline: account.client_online,
|
clientOnline: account.client_online,
|
||||||
clientDeviceName: account.client_device_name,
|
clientDeviceName: account.client_device_name,
|
||||||
@@ -149,7 +151,7 @@ const filteredAccounts = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function resolvePublishState(account: DesktopAccountInfo): PublishState {
|
function resolvePublishState(account: DesktopAccountInfo): PublishState {
|
||||||
if (account.health !== "live") {
|
if (resolveAccountHealth(account) !== "live") {
|
||||||
return "unavailable";
|
return "unavailable";
|
||||||
}
|
}
|
||||||
if (!account.client_id) {
|
if (!account.client_id) {
|
||||||
@@ -165,7 +167,7 @@ function resolvePublishHint(account: DesktopAccountInfo, publishState: PublishSt
|
|||||||
if (publishState === "queued") {
|
if (publishState === "queued") {
|
||||||
return "客户端当前离线,任务会先进入发布队列,等桌面端上线后自动继续。";
|
return "客户端当前离线,任务会先进入发布队列,等桌面端上线后自动继续。";
|
||||||
}
|
}
|
||||||
if (account.health !== "live") {
|
if (resolveAccountHealth(account) !== "live") {
|
||||||
return "授权状态异常,先在桌面端重新校验后再发布。";
|
return "授权状态异常,先在桌面端重新校验后再发布。";
|
||||||
}
|
}
|
||||||
return "还没有绑定桌面客户端,当前无法加入发布队列。";
|
return "还没有绑定桌面客户端,当前无法加入发布队列。";
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { useRoute, useRouter } from "vue-router";
|
|||||||
|
|
||||||
import PageHero from "@/components/PageHero.vue";
|
import PageHero from "@/components/PageHero.vue";
|
||||||
import { brandsApi, monitoringApi, tenantAccountsApi } from "@/lib/api";
|
import { brandsApi, monitoringApi, tenantAccountsApi } from "@/lib/api";
|
||||||
|
import { resolveAccountCheckedAt, resolveAccountHealth } from "@/lib/desktop-account-runtime";
|
||||||
import { formatDateTime } from "@/lib/display";
|
import { formatDateTime } from "@/lib/display";
|
||||||
import { formatError } from "@/lib/errors";
|
import { formatError } from "@/lib/errors";
|
||||||
import { isMonitoringPlatformId, normalizeMonitoringPlatformFilter, normalizeMonitoringPlatformId } from "@/lib/monitoring-platforms";
|
import { isMonitoringPlatformId, normalizeMonitoringPlatformFilter, normalizeMonitoringPlatformId } from "@/lib/monitoring-platforms";
|
||||||
@@ -354,8 +355,8 @@ const aiPlatformStatusCards = computed<AIPlatformStatusCard[]>(() => {
|
|||||||
|
|
||||||
return aiPlatformCatalog.map((platform) => {
|
return aiPlatformCatalog.map((platform) => {
|
||||||
const matchedAccounts = [...(accountGroups.get(platform.id) ?? [])].sort((left, right) => {
|
const matchedAccounts = [...(accountGroups.get(platform.id) ?? [])].sort((left, right) => {
|
||||||
const leftAt = Date.parse(left.verified_at ?? "") || 0;
|
const leftAt = Date.parse(resolveAccountCheckedAt(left) ?? "") || 0;
|
||||||
const rightAt = Date.parse(right.verified_at ?? "") || 0;
|
const rightAt = Date.parse(resolveAccountCheckedAt(right) ?? "") || 0;
|
||||||
return rightAt - leftAt;
|
return rightAt - leftAt;
|
||||||
});
|
});
|
||||||
const breakdown = breakdownMap.get(platform.id);
|
const breakdown = breakdownMap.get(platform.id);
|
||||||
@@ -537,7 +538,7 @@ function accountAuthLabel(account: DesktopAccountInfo | null): string {
|
|||||||
return "未绑定";
|
return "未绑定";
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (account.health) {
|
switch (resolveAccountHealth(account)) {
|
||||||
case "live":
|
case "live":
|
||||||
return "授权正常";
|
return "授权正常";
|
||||||
case "captcha":
|
case "captcha":
|
||||||
@@ -554,7 +555,7 @@ function accountAuthColor(account: DesktopAccountInfo | null): string {
|
|||||||
return "default";
|
return "default";
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (account.health) {
|
switch (resolveAccountHealth(account)) {
|
||||||
case "live":
|
case "live":
|
||||||
return "green";
|
return "green";
|
||||||
case "captcha":
|
case "captcha":
|
||||||
|
|||||||
Reference in New Issue
Block a user