haixunMaster/lib/threads-browser/debug.ts

115 lines
3.4 KiB
TypeScript
Raw Permalink Normal View History

2026-06-21 12:50:31 +00:00
import { mkdir, writeFile } from "fs/promises";
import path from "path";
import type { Page } from "playwright";
import { getActiveAccountConnectionSettings } from "@/lib/account-connection-settings";
const DEBUG_DIR = path.join(process.cwd(), "data", "debug-runs");
export interface DebugRun {
id: string;
label: string;
dir: string;
enabled: boolean;
}
export async function isPlaywrightDebugEnabled(): Promise<boolean> {
if (process.env.THREADS_DEBUG === "true") return true;
try {
const connection = await getActiveAccountConnectionSettings();
return connection.playwrightDebug === true;
} catch {
return false;
}
}
export async function shouldRunHeaded(): Promise<boolean> {
if (process.env.PLAYWRIGHT_HEADLESS === "false") return true;
if (process.env.PLAYWRIGHT_HEADLESS === "true") return false;
return isPlaywrightDebugEnabled();
}
/** 發布預設用可見瀏覽器,較不容易被擋、也方便手動完成 Instagram 授權 */
export async function shouldRunHeadedForPublish(): Promise<boolean> {
if (process.env.PLAYWRIGHT_HEADLESS === "false") return true;
if (process.env.PLAYWRIGHT_HEADLESS === "true") return false;
try {
const connection = await getActiveAccountConnectionSettings();
if (connection.playwrightDebug) return true;
return connection.publishHeaded ?? true;
} catch {
return true;
}
}
export function getPlaywrightSlowMo(): number {
const raw = process.env.PLAYWRIGHT_SLOW_MO;
if (raw) {
const parsed = Number(raw);
if (!Number.isNaN(parsed) && parsed >= 0) return parsed;
}
return 0;
}
export async function createDebugRun(
label: string,
options?: { force?: boolean }
): Promise<DebugRun | null> {
const enabled = options?.force === true || await isPlaywrightDebugEnabled();
if (!enabled) return null;
const id = `${Date.now()}-${label.replace(/[^a-zA-Z0-9_-]+/g, "-").slice(0, 40)}`;
const dir = path.join(DEBUG_DIR, id);
await mkdir(dir, { recursive: true });
await writeFile(
path.join(dir, "manifest.json"),
JSON.stringify({ id, label, startedAt: new Date().toISOString(), steps: [] }, null, 2)
);
console.log(`[threads-debug] run=${id} label=${label}`);
return { id, label, dir, enabled };
}
export async function captureDebugStep(
page: Page,
run: DebugRun | null | undefined,
step: string,
meta?: Record<string, unknown>
) {
if (!run?.enabled) return;
const safeStep = step.replace(/[^a-zA-Z0-9_-]+/g, "-").slice(0, 80);
const timestamp = new Date().toISOString();
const shotPath = path.join(run.dir, `${safeStep}.png`);
try {
await page.screenshot({ path: shotPath, fullPage: true });
} catch (error) {
console.warn(`[threads-debug] screenshot failed (${step})`, error);
}
try {
const manifestPath = path.join(run.dir, "manifest.json");
const { readFile } = await import("fs/promises");
const manifest = JSON.parse(await readFile(manifestPath, "utf8")) as {
steps: Array<Record<string, unknown>>;
};
manifest.steps.push({
step,
at: timestamp,
url: page.url(),
title: await page.title().catch(() => ""),
screenshot: `${safeStep}.png`,
...meta,
});
await writeFile(manifestPath, JSON.stringify(manifest, null, 2));
} catch (error) {
console.warn(`[threads-debug] manifest update failed (${step})`, error);
}
console.log(`[threads-debug] ${run.id} · ${step} · ${page.url()}`);
}
export function debugDirPath(): string {
return DEBUG_DIR;
}