优化图文页

This commit is contained in:
2026-04-28 17:11:42 +08:00
parent 674c202264
commit 040273586b
69 changed files with 3939 additions and 2302 deletions
+72 -71
View File
@@ -1,99 +1,98 @@
export const runtime = 'nodejs'
export const runtime = "nodejs";
import { type Browser, type BrowserContext } from 'playwright'
import { chromium } from 'playwright-extra' // A simple singleton manager for a persistent Chromium context with ref-counting. // Prevents concurrent tasks from closing the shared window prematurely.
import stealth from 'puppeteer-extra-plugin-stealth'
import { type Browser, type BrowserContext } from "playwright";
import { chromium } from "playwright-extra"; // A simple singleton manager for a persistent Chromium context with ref-counting. // Prevents concurrent tasks from closing the shared window prematurely.
import stealth from "puppeteer-extra-plugin-stealth";
chromium.use(stealth());
let contextPromise: Promise<BrowserContext> | null = null
let context: BrowserContext | null = null
let refCount = 0
let idleCloseTimer: NodeJS.Timeout | null = null
let contextPromise: Promise<BrowserContext> | null = null;
let context: BrowserContext | null = null;
let refCount = 0;
let idleCloseTimer: NodeJS.Timeout | null = null;
const USER_DATA_DIR = process.env.USER_DATA_DIR ?? 'chrome-profile/douyin'
const USER_DATA_DIR = process.env.USER_DATA_DIR ?? "chrome-profile/douyin";
async function launchContext(): Promise<BrowserContext> {
const ctx = await chromium.launchPersistentContext(
USER_DATA_DIR,
{
headless: process.env.CHROMIUM_HEADLESS === 'true',
viewport: {
width: Number(process.env.CHROMIUM_VIEWPORT_WIDTH ?? 1280),
height: Number(process.env.CHROMIUM_VIEWPORT_HEIGHT ?? 1080)
}
}
)
const ctx = await chromium.launchPersistentContext(USER_DATA_DIR, {
headless: process.env.CHROMIUM_HEADLESS === "true",
viewport: {
width: Number(process.env.CHROMIUM_VIEWPORT_WIDTH ?? 1280),
height: Number(process.env.CHROMIUM_VIEWPORT_HEIGHT ?? 1080),
},
});
// When the context is closed externally, reset manager state
ctx.on('close', () => {
context = null
contextPromise = null
refCount = 0
ctx.on("close", () => {
context = null;
contextPromise = null;
refCount = 0;
if (idleCloseTimer) {
clearTimeout(idleCloseTimer)
idleCloseTimer = null
clearTimeout(idleCloseTimer);
idleCloseTimer = null;
}
})
return ctx
});
return ctx;
}
export async function acquireBrowserContext(): Promise<BrowserContext> {
// Cancel any pending idle close if a new consumer arrives
if (idleCloseTimer) {
clearTimeout(idleCloseTimer)
idleCloseTimer = null
clearTimeout(idleCloseTimer);
idleCloseTimer = null;
}
if (context) {
refCount += 1
return context
refCount += 1;
return context;
}
if (!contextPromise) {
contextPromise = launchContext()
contextPromise = launchContext();
}
context = await contextPromise
refCount += 1
return context
context = await contextPromise;
refCount += 1;
return context;
}
export async function releaseBrowserContext(options?: { idleMillis?: number }): Promise<void> {
const idleMillis = options?.idleMillis ?? 15_000
refCount = Math.max(0, refCount - 1)
export async function releaseBrowserContext(options?: {
idleMillis?: number;
}): Promise<void> {
const idleMillis = options?.idleMillis ?? 15_000;
refCount = Math.max(0, refCount - 1);
if (refCount > 0 || !context) return
if (refCount > 0 || !context) return;
// Delay the close to allow bursty workloads to reuse the context
if (idleCloseTimer) {
clearTimeout(idleCloseTimer)
idleCloseTimer = null
clearTimeout(idleCloseTimer);
idleCloseTimer = null;
}
idleCloseTimer = setTimeout(async () => {
try {
if (context && refCount === 0) {
await context.close()
await context.close();
}
} finally {
context = null
contextPromise = null
idleCloseTimer = null
context = null;
contextPromise = null;
idleCloseTimer = null;
}
}, idleMillis)
}, idleMillis);
}
// --- Isolated context support for per-scrape independence ---
const isolatedMap = new WeakMap<BrowserContext, Browser>()
const isolatedMap = new WeakMap<BrowserContext, Browser>();
async function getSharedStorageState(): Promise<any | undefined> {
try {
const shared = await acquireBrowserContext()
const state = await shared.storageState()
const shared = await acquireBrowserContext();
const state = await shared.storageState();
// Do not force-close immediately; keep ref-counting behavior
await releaseBrowserContext()
return state
await releaseBrowserContext();
return state;
} catch {
// If shared context not available, proceed without storageState
return undefined
return undefined;
}
}
@@ -103,37 +102,39 @@ async function getSharedStorageState(): Promise<any | undefined> {
* so you remain logged-in, but isolates network events, cache and listeners.
*/
export async function acquireIsolatedContext(): Promise<BrowserContext> {
const storageState = await getSharedStorageState()
const storageState = await getSharedStorageState();
const browser = await chromium.launch({
headless: process.env.CHROMIUM_HEADLESS === 'true'
})
headless: process.env.CHROMIUM_HEADLESS === "true",
});
const ctx = await browser.newContext({
storageState,
viewport: {
width: Number(process.env.CHROMIUM_VIEWPORT_WIDTH ?? 1280),
height: Number(process.env.CHROMIUM_VIEWPORT_HEIGHT ?? 1080)
}
})
isolatedMap.set(ctx, browser)
ctx.on('close', () => {
const b = isolatedMap.get(ctx)
height: Number(process.env.CHROMIUM_VIEWPORT_HEIGHT ?? 1080),
},
});
isolatedMap.set(ctx, browser);
ctx.on("close", () => {
const b = isolatedMap.get(ctx);
if (b) {
b.close().catch(() => {})
isolatedMap.delete(ctx)
b.close().catch(() => {});
isolatedMap.delete(ctx);
}
})
return ctx
});
return ctx;
}
export async function releaseIsolatedContext(ctx: BrowserContext | null | undefined): Promise<void> {
if (!ctx) return
export async function releaseIsolatedContext(
ctx: BrowserContext | null | undefined,
): Promise<void> {
if (!ctx) return;
try {
await ctx.close()
await ctx.close();
} finally {
const b = isolatedMap.get(ctx)
const b = isolatedMap.get(ctx);
if (b) {
await b.close().catch(() => {})
isolatedMap.delete(ctx)
await b.close().catch(() => {});
isolatedMap.delete(ctx);
}
}
}