2026-04-30 01:32:19 +08:00
|
|
|
import type { DesktopAccountInfo, MediaPlatform } from "@geo/shared-types";
|
|
|
|
|
|
|
|
|
|
import { resolveApiURL } from "@/lib/api";
|
|
|
|
|
import { resolveAccountCheckedAt, resolveAccountHealth } from "@/lib/desktop-account-runtime";
|
|
|
|
|
import {
|
|
|
|
|
getPublishPlatformMeta,
|
2026-05-01 16:01:40 +08:00
|
|
|
isMediaPublishAccount,
|
2026-04-30 01:32:19 +08:00
|
|
|
isPublishPlatformId,
|
|
|
|
|
normalizePublishPlatformId,
|
|
|
|
|
} from "@/lib/publish-platforms";
|
|
|
|
|
|
|
|
|
|
export type PublishState = "immediate" | "queued" | "unavailable";
|
|
|
|
|
|
|
|
|
|
export interface PublishAccountCard {
|
|
|
|
|
id: string;
|
|
|
|
|
platformId: string;
|
|
|
|
|
platformName: string;
|
|
|
|
|
platformShortName: string;
|
|
|
|
|
platformAccent: string;
|
|
|
|
|
platformLogoUrl: string | null;
|
|
|
|
|
displayName: string;
|
|
|
|
|
platformUid: string;
|
|
|
|
|
avatarUrl: string | null;
|
|
|
|
|
health: DesktopAccountInfo["health"];
|
|
|
|
|
verifiedAt: string | null;
|
|
|
|
|
clientId: string | null;
|
|
|
|
|
clientOnline: boolean | null;
|
|
|
|
|
clientDeviceName: string | null;
|
|
|
|
|
clientLastSeenAt: string | null;
|
|
|
|
|
publishState: PublishState;
|
|
|
|
|
selectable: boolean;
|
|
|
|
|
statusText: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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",
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export function buildPublishPlatformMap(platforms: MediaPlatform[]): Map<string, MediaPlatform> {
|
|
|
|
|
return new Map(
|
|
|
|
|
platforms.map((platform) => [
|
|
|
|
|
normalizePublishPlatformId(platform.platform_id),
|
|
|
|
|
platform,
|
|
|
|
|
]),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function buildPublishAccountCards(
|
|
|
|
|
accounts: DesktopAccountInfo[],
|
|
|
|
|
platformMap: Map<string, MediaPlatform>,
|
|
|
|
|
preferredPlatformIds: string[] = [],
|
|
|
|
|
): PublishAccountCard[] {
|
|
|
|
|
const preferredPlatforms = new Set(preferredPlatformIds.map(normalizePublishPlatformId));
|
|
|
|
|
|
|
|
|
|
return accounts
|
|
|
|
|
.filter((account) => {
|
2026-05-01 16:01:40 +08:00
|
|
|
if (!isMediaPublishAccount(account)) {
|
2026-04-30 01:32:19 +08:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
const platformId = normalizePublishPlatformId(account.platform);
|
|
|
|
|
return platformMap.has(platformId) || isPublishPlatformId(platformId);
|
|
|
|
|
})
|
|
|
|
|
.map((account) => buildPublishAccountCard(account, platformMap))
|
|
|
|
|
.sort((left, right) => {
|
|
|
|
|
const preferredGap =
|
|
|
|
|
Number(preferredPlatforms.has(right.platformId)) - Number(preferredPlatforms.has(left.platformId));
|
|
|
|
|
if (preferredGap !== 0) {
|
|
|
|
|
return preferredGap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const publishGap = publishRank(left.publishState) - publishRank(right.publishState);
|
|
|
|
|
if (publishGap !== 0) {
|
|
|
|
|
return publishGap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const leftVerifiedAt = Date.parse(left.verifiedAt ?? "") || 0;
|
|
|
|
|
const rightVerifiedAt = Date.parse(right.verifiedAt ?? "") || 0;
|
|
|
|
|
return rightVerifiedAt - leftVerifiedAt;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function buildPublishAccountCard(
|
|
|
|
|
account: DesktopAccountInfo,
|
|
|
|
|
platformMap: Map<string, MediaPlatform>,
|
|
|
|
|
): PublishAccountCard {
|
|
|
|
|
const platformId = normalizePublishPlatformId(account.platform);
|
|
|
|
|
const platform = platformMap.get(platformId);
|
|
|
|
|
const fallback = getPublishPlatformMeta(platformId);
|
|
|
|
|
const publishState = resolvePublishState(account);
|
|
|
|
|
const health = resolveAccountHealth(account);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
id: account.id,
|
|
|
|
|
platformId,
|
|
|
|
|
platformName: platform?.name || fallback.name,
|
|
|
|
|
platformShortName: platform?.short_name || fallback.shortName,
|
|
|
|
|
platformAccent: platform?.accent_color || fallback.accent,
|
|
|
|
|
platformLogoUrl: resolvePlatformLogoUrl(platformId, platform?.logo_url),
|
|
|
|
|
displayName: account.display_name,
|
|
|
|
|
platformUid: normalizePlatformUid(account.platform_uid),
|
|
|
|
|
avatarUrl: resolveApiURL(account.avatar_url),
|
|
|
|
|
health,
|
|
|
|
|
verifiedAt: resolveAccountCheckedAt(account),
|
|
|
|
|
clientId: account.client_id,
|
|
|
|
|
clientOnline: account.client_online,
|
|
|
|
|
clientDeviceName: account.client_device_name,
|
|
|
|
|
clientLastSeenAt: account.client_last_seen_at,
|
|
|
|
|
publishState,
|
|
|
|
|
selectable: publishState !== "unavailable",
|
|
|
|
|
statusText: resolvePublishStatusText(account, publishState),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function resolvePublishState(account: DesktopAccountInfo): PublishState {
|
|
|
|
|
if (resolveAccountHealth(account) !== "live") {
|
|
|
|
|
return "unavailable";
|
|
|
|
|
}
|
|
|
|
|
if (!account.client_id) {
|
|
|
|
|
return "unavailable";
|
|
|
|
|
}
|
|
|
|
|
return account.client_online === true ? "immediate" : "queued";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function publishRank(state: PublishState): number {
|
|
|
|
|
switch (state) {
|
|
|
|
|
case "immediate":
|
|
|
|
|
return 0;
|
|
|
|
|
case "queued":
|
|
|
|
|
return 1;
|
|
|
|
|
default:
|
|
|
|
|
return 2;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function publishStateLabel(state: PublishState): string {
|
|
|
|
|
switch (state) {
|
|
|
|
|
case "immediate":
|
|
|
|
|
return "立即发布";
|
|
|
|
|
case "queued":
|
|
|
|
|
return "离线排队";
|
|
|
|
|
default:
|
|
|
|
|
return "不可发布";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function publishStatusShortLabel(state: PublishState): string {
|
|
|
|
|
switch (state) {
|
|
|
|
|
case "immediate":
|
|
|
|
|
return "客户端在线";
|
|
|
|
|
case "queued":
|
|
|
|
|
return "离线可排队";
|
|
|
|
|
default:
|
|
|
|
|
return "不可发布";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function healthLabel(health: DesktopAccountInfo["health"]): string {
|
|
|
|
|
switch (health) {
|
|
|
|
|
case "live":
|
|
|
|
|
return "授权正常";
|
|
|
|
|
case "captcha":
|
|
|
|
|
return "待处理";
|
|
|
|
|
case "risk":
|
|
|
|
|
return "风险观察";
|
|
|
|
|
default:
|
|
|
|
|
return "授权异常";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function healthColor(health: DesktopAccountInfo["health"]): string {
|
|
|
|
|
switch (health) {
|
|
|
|
|
case "live":
|
|
|
|
|
return "green";
|
|
|
|
|
case "captcha":
|
|
|
|
|
return "orange";
|
|
|
|
|
case "risk":
|
|
|
|
|
return "gold";
|
|
|
|
|
default:
|
|
|
|
|
return "red";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function clientStatusLabel(card: PublishAccountCard): string {
|
|
|
|
|
if (!card.clientId) {
|
|
|
|
|
return "未绑定客户端";
|
|
|
|
|
}
|
|
|
|
|
return card.clientOnline ? "客户端在线" : "客户端离线";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function clientStatusColor(card: PublishAccountCard): string {
|
|
|
|
|
if (!card.clientId) {
|
|
|
|
|
return "default";
|
|
|
|
|
}
|
|
|
|
|
return card.clientOnline ? "green" : "gold";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function accountInitial(card: Pick<PublishAccountCard, "displayName" | "platformShortName">): string {
|
|
|
|
|
const trimmed = card.displayName.trim();
|
|
|
|
|
return trimmed ? trimmed.slice(0, 1).toUpperCase() : card.platformShortName;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export 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 resolvePublishStatusText(account: DesktopAccountInfo, publishState: PublishState): string {
|
|
|
|
|
if (publishState === "immediate") {
|
|
|
|
|
return "客户端在线,RabbitMQ 会立即唤醒桌面端开始发布。";
|
|
|
|
|
}
|
|
|
|
|
if (publishState === "queued") {
|
|
|
|
|
return "客户端当前离线,任务会先落库排队,等桌面端上线后自动继续发布。";
|
|
|
|
|
}
|
|
|
|
|
if (resolveAccountHealth(account) !== "live") {
|
|
|
|
|
return "该账号授权状态异常,请先在桌面端重新校验或重新授权。";
|
|
|
|
|
}
|
|
|
|
|
return "该账号还没有绑定桌面客户端,当前不能进入发布队列。";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizePlatformUid(value?: string | null): string {
|
|
|
|
|
let normalized = String(value ?? "").trim();
|
|
|
|
|
while (normalized.toLowerCase().startsWith("platform:")) {
|
|
|
|
|
normalized = normalized.slice("platform:".length).trim();
|
|
|
|
|
}
|
|
|
|
|
return normalized || "--";
|
|
|
|
|
}
|