feat(desktop): dedupe bind windows per platform and cap concurrency at 2

Track active bindPublishAccount calls in a per-platform map so a repeat
invocation reuses and focuses the existing window instead of spawning
a second one, and reject new platforms once 2 bind windows are already
open. Surface desktop_account_bind_limit_reached as a warning modal in
the renderer so users know to finish or close an existing authorization
before starting another.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-20 11:11:21 +08:00
parent 12f972882b
commit 4014eff427
2 changed files with 86 additions and 5 deletions
+78 -5
View File
@@ -74,6 +74,12 @@ export interface PublishAccountLocalInspection {
profile: PublishAccountProfile | null; profile: PublishAccountProfile | null;
} }
interface ActiveBindOperation {
platformId: string;
window: BrowserWindow;
promise: Promise<DesktopAccountInfo>;
}
type ToutiaoMediaInfoResponse = { type ToutiaoMediaInfoResponse = {
data?: { data?: {
user?: { user?: {
@@ -199,6 +205,9 @@ type DongchediAccountInfoResponse = {
}; };
}; };
const activeBindOperations = new Map<string, ActiveBindOperation>();
const MAX_CONCURRENT_BIND_WINDOWS = 2;
const TOUTIAO_LOGIN_URL = "https://mp.toutiao.com/auth/page/login"; const TOUTIAO_LOGIN_URL = "https://mp.toutiao.com/auth/page/login";
const TOUTIAO_CONSOLE_HOME_URL = "https://mp.toutiao.com/profile_v4/index"; const TOUTIAO_CONSOLE_HOME_URL = "https://mp.toutiao.com/profile_v4/index";
const TOUTIAO_WORKBENCH_URL_PATTERN = /^https:\/\/mp\.toutiao\.com\/profile_v4\//i; const TOUTIAO_WORKBENCH_URL_PATTERN = /^https:\/\/mp\.toutiao\.com\/profile_v4\//i;
@@ -1502,26 +1511,80 @@ function createBoundWindow(
return window; return window;
} }
function focusBindWindow(window: BrowserWindow): void {
if (window.isDestroyed()) {
return;
}
if (window.isMinimized()) {
window.restore();
}
if (!window.isVisible()) {
window.show();
}
window.focus();
}
function cleanupInactiveBindOperations(): void {
for (const [platformId, operation] of activeBindOperations.entries()) {
if (operation.window.isDestroyed()) {
activeBindOperations.delete(platformId);
}
}
}
function clearActiveBindOperation(platformId: string, window: BrowserWindow): void {
const operation = activeBindOperations.get(platformId);
if (operation?.window === window) {
activeBindOperations.delete(platformId);
}
}
export async function bindPublishAccount(platformId: string): Promise<DesktopAccountInfo> { export async function bindPublishAccount(platformId: string): Promise<DesktopAccountInfo> {
const definition = publishBindingDefinitions[platformId]; const definition = publishBindingDefinitions[platformId];
if (!definition) { if (!definition) {
throw new Error(`desktop_publish_platform_not_supported:${platformId}`); throw new Error(`desktop_publish_platform_not_supported:${platformId}`);
} }
const handle = createPendingSessionHandle(platformId); cleanupInactiveBindOperations();
return await new Promise<DesktopAccountInfo>((resolve, reject) => { const existingOperation = activeBindOperations.get(platformId);
const window = createBoundWindow(`绑定 ${definition.label}`, definition.loginUrl, handle.session); if (existingOperation && !existingOperation.window.isDestroyed()) {
focusBindWindow(existingOperation.window);
return existingOperation.promise;
}
if (activeBindOperations.size >= MAX_CONCURRENT_BIND_WINDOWS) {
throw new Error("desktop_account_bind_limit_reached");
}
const handle = createPendingSessionHandle(platformId);
let window: BrowserWindow;
try {
window = createBoundWindow(`绑定 ${definition.label}`, definition.loginUrl, handle.session);
} catch (error) {
forgetSessionHandle(handle.accountId);
throw error instanceof Error ? error : new Error("desktop_account_bind_failed");
}
const bindPromise = new Promise<DesktopAccountInfo>((resolve, reject) => {
let finished = false; let finished = false;
let checking = false; let checking = false;
let detectReady = false; let detectReady = false;
const finalizeOperation = () => {
clearInterval(intervalHandle);
clearActiveBindOperation(platformId, window);
};
const settleSuccess = (account: DesktopAccountInfo) => { const settleSuccess = (account: DesktopAccountInfo) => {
if (finished) { if (finished) {
return; return;
} }
finished = true; finished = true;
clearInterval(intervalHandle); finalizeOperation();
attachSessionHandle(account.id, handle); attachSessionHandle(account.id, handle);
resolve(account); resolve(account);
if (!window.isDestroyed()) { if (!window.isDestroyed()) {
@@ -1539,7 +1602,7 @@ export async function bindPublishAccount(platformId: string): Promise<DesktopAcc
return; return;
} }
finished = true; finished = true;
clearInterval(intervalHandle); finalizeOperation();
forgetSessionHandle(handle.accountId); forgetSessionHandle(handle.accountId);
if (!window.isDestroyed()) { if (!window.isDestroyed()) {
window.close(); window.close();
@@ -1653,6 +1716,7 @@ export async function bindPublishAccount(platformId: string): Promise<DesktopAcc
}, 1800); }, 1800);
window.on("closed", () => { window.on("closed", () => {
clearActiveBindOperation(platformId, window);
if (!finished) { if (!finished) {
clearInterval(intervalHandle); clearInterval(intervalHandle);
forgetSessionHandle(handle.accountId); forgetSessionHandle(handle.accountId);
@@ -1663,6 +1727,15 @@ export async function bindPublishAccount(platformId: string): Promise<DesktopAcc
syncDetectionState(); syncDetectionState();
void detectAndBind(); void detectAndBind();
}); });
activeBindOperations.set(platformId, {
platformId,
window,
promise: bindPromise,
});
focusBindWindow(window);
return await bindPromise;
} }
export async function openPublishAccountConsole(account: PublishAccountIdentity): Promise<void> { export async function openPublishAccountConsole(account: PublishAccountIdentity): Promise<void> {
@@ -53,6 +53,14 @@ function presentClientError(kind: ClientActionKind, error: unknown): ClientError
}; };
} }
if (message === "desktop_account_bind_limit_reached") {
return {
tone: "warning",
title: "授权窗口已达上限",
content: "当前最多同时打开 2 个授权窗口。请先完成或关闭已有授权窗口,再继续新的绑定。",
};
}
if (message === "desktop_account_upsert_failed") { if (message === "desktop_account_upsert_failed") {
return { return {
tone: "error", tone: "error",