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:
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user