feat(desktop): drop parked review flow, add publish management

SaaS 侧人工审核后才创建 publish job,desktop 不再做二次审核:移除
manual/waiting_user/parked/from_parked 状态机与 LeaseFromParked 查询,
desktop client 只执行发布并新增"发布管理"页(待发布队列 / 历史 / 再次
发送)。同时抽离 @geo/publisher-platforms 共享适配器包、新增 Redis-based
desktop_presence 与 publish_record_support,刷新 admin-web 发布弹窗与
媒体库;plan A / spec 文档同步口径。
This commit is contained in:
2026-04-20 09:52:48 +08:00
parent b16e9f0bd1
commit a617d39a4a
93 changed files with 5519 additions and 3594 deletions
+207 -7
View File
@@ -32,6 +32,10 @@ import {
} from "./external-window";
import { upsertDesktopAccount } from "./transport/api-client";
import { STANDARD_USER_AGENT } from "./user-agent";
import {
normalizeAccountPlatformUid,
sameAccountPlatformUid,
} from "../shared/account-identity";
interface DetectedAccount {
platformUid: string;
@@ -65,6 +69,11 @@ export interface PublishAccountProfile {
avatarUrl: string | null;
}
export interface PublishAccountLocalInspection {
availability: "missing" | "ready" | "expired";
profile: PublishAccountProfile | null;
}
type ToutiaoMediaInfoResponse = {
data?: {
user?: {
@@ -648,7 +657,11 @@ async function deriveToutiaoFallbackUid(session: Session, displayName: string):
function sanitizeDetectedAccount(account: DetectedAccount): DetectedAccount {
const displayName = normalizeText(account.displayName) ?? "未命名账号";
const platformUid = boundedIdentity("platform", account.platformUid || displayName);
const normalizedPlatformUid = normalizeAccountPlatformUid(account.platformUid);
const platformUid =
normalizedPlatformUid
|| normalizeAccountPlatformUid(boundedIdentity("platform", displayName))
|| displayName;
return {
platformUid,
@@ -772,7 +785,7 @@ async function recoverToutiaoSessionHandle(account: PublishAccountIdentity): Pro
continue;
}
if (expectedUid && detected.platformUid === expectedUid) {
if (expectedUid && sameAccountPlatformUid(detected.platformUid, expectedUid)) {
console.info("[desktop-session] recovered toutiao partition by uid", {
accountId: account.id,
partition,
@@ -805,6 +818,13 @@ async function recoverToutiaoSessionHandle(account: PublishAccountIdentity): Pro
export async function ensurePublishAccountSessionHandle(
account: PublishAccountIdentity,
): Promise<SessionHandle> {
const existing = await findLocalPublishAccountSessionHandle(account);
return existing ?? createSessionHandle(account.id);
}
async function findLocalPublishAccountSessionHandle(
account: PublishAccountIdentity,
): Promise<SessionHandle | null> {
const existing = getSessionHandle(account.id);
if (existing) {
return existing;
@@ -822,19 +842,123 @@ export async function ensurePublishAccountSessionHandle(
}
}
return createSessionHandle(account.id);
return null;
}
function toPublishAccountProfile(detected: DetectedAccount | null): PublishAccountProfile | null {
if (!detected) {
return null;
}
const normalized = sanitizeDetectedAccount(detected);
return {
platformUid: normalized.platformUid,
displayName: normalized.displayName,
avatarUrl: normalized.avatarUrl,
};
}
async function resolvePublishAccountProfileFromHandle(
account: PublishAccountIdentity,
handle: SessionHandle,
): Promise<PublishAccountProfile | null> {
if (account.platform === "toutiaohao") {
return await detectToutiaoFromSession(handle.session);
}
const definition = publishBindingDefinitions[account.platform];
if (!definition) {
return null;
}
const detected = await definition.detect({
session: handle.session,
webContents: undefined as unknown as WebContents,
}).catch(() => null);
return toPublishAccountProfile(detected);
}
export async function inspectPublishAccountLocalSession(
account: PublishAccountIdentity,
): Promise<PublishAccountLocalInspection> {
const handle = await findLocalPublishAccountSessionHandle(account);
if (!handle) {
return {
availability: "missing",
profile: null,
};
}
const profile = await resolvePublishAccountProfileFromHandle(account, handle);
if (!profile) {
const consoleReady = await verifyPublishAccountConsoleAccess(account, handle).catch(() => false);
if (consoleReady) {
return {
availability: "ready",
profile: {
platformUid: normalizeAccountPlatformUid(account.platformUid) || account.platformUid,
displayName: account.displayName,
avatarUrl: null,
},
};
}
if (account.platform === "toutiaohao" && await hasToutiaoAuthCookies(handle.session)) {
return {
availability: "ready",
profile: {
platformUid: normalizeAccountPlatformUid(account.platformUid) || account.platformUid,
displayName: account.displayName,
avatarUrl: null,
},
};
}
return {
availability: "expired",
profile: null,
};
}
return {
availability: "ready",
profile,
};
}
export async function resolvePublishAccountProfile(
account: PublishAccountIdentity,
): Promise<PublishAccountProfile | null> {
const handle = await ensurePublishAccountSessionHandle(account);
return await resolvePublishAccountProfileFromHandle(account, handle);
}
if (account.platform === "toutiaohao") {
return await detectToutiaoFromSession(handle.session);
async function finalizeDetectedAccountForBind(
definition: PublishPlatformBindingDefinition,
context: DetectContext,
detected: DetectedAccount,
): Promise<DetectedAccount> {
if (detected.avatarUrl) {
return detected;
}
return null;
let resolved = detected;
for (let attempt = 0; attempt < 3; attempt += 1) {
await sleep(400 + attempt * 250);
const retried = await definition.detect(context).catch(() => null);
if (!retried) {
continue;
}
const sanitized = sanitizeDetectedAccount(retried);
resolved = sanitized;
if (sanitized.avatarUrl) {
break;
}
}
return resolved;
}
function isToutiaoLoginURL(url: string): boolean {
@@ -908,6 +1032,69 @@ async function verifyToutiaoConsoleAccess(window: BrowserWindow, session: Sessio
return extractToutiaoAccount(sessionResponse) !== null;
}
function looksLikeLoginRedirect(currentURL: string, loginURL: string): boolean {
if (!currentURL || !loginURL) {
return false;
}
if (currentURL.startsWith(loginURL)) {
return true;
}
try {
const current = new URL(currentURL);
const login = new URL(loginURL);
if (current.origin !== login.origin) {
return false;
}
const currentPath = current.pathname.replace(/\/+$/, "");
const loginPath = login.pathname.replace(/\/+$/, "");
return currentPath === loginPath || currentPath.startsWith(`${loginPath}/`);
} catch {
return false;
}
}
async function verifyPublishAccountConsoleAccess(
account: PublishAccountIdentity,
handle: SessionHandle,
): Promise<boolean> {
const definition = publishBindingDefinitions[account.platform];
if (!definition) {
return false;
}
const window = createBoundWindow(
`${definition.label} 静默校验`,
definition.consoleUrl,
handle.session,
{ show: false },
);
try {
if (account.platform === "toutiaohao") {
return await verifyToutiaoConsoleAccess(window, handle.session);
}
await waitForWindowNavigationSettled(window);
if (window.isDestroyed()) {
return false;
}
const currentURL = window.webContents.getURL();
if (!currentURL || currentURL.startsWith("data:text/html")) {
return false;
}
return !looksLikeLoginRedirect(currentURL, definition.loginUrl);
} finally {
if (!window.isDestroyed()) {
window.destroy();
}
}
}
async function detectBaijiahao({ session }: DetectContext): Promise<DetectedAccount | null> {
const response = await sessionFetchJson<BaijiahaoAppInfoResponse>(
session,
@@ -1274,8 +1461,14 @@ const publishBindingDefinitions: Record<string, PublishPlatformBindingDefinition
},
};
function createBoundWindow(title: string, targetURL: string, sessionHandle: Session): BrowserWindow {
function createBoundWindow(
title: string,
targetURL: string,
sessionHandle: Session,
options: { show?: boolean } = {},
): BrowserWindow {
const window = new BrowserWindow({
show: options.show ?? true,
width: 1320,
height: 900,
minWidth: 1100,
@@ -1425,10 +1618,17 @@ export async function bindPublishAccount(platformId: string): Promise<DesktopAcc
}
}
normalizedDetected = await finalizeDetectedAccountForBind(
definition,
{ session: handle.session, webContents: window.webContents },
normalizedDetected,
);
const account = await upsertDesktopAccount({
platform: definition.id,
platform_uid: normalizedDetected.platformUid,
display_name: normalizedDetected.displayName,
avatar_url: normalizedDetected.avatarUrl,
account_fingerprint: normalizedDetected.platformUid,
health: "live",
verified_at: new Date().toISOString(),