haixunMaster/lib/threads-browser/format-text.ts

108 lines
3.3 KiB
TypeScript
Raw Normal View History

2026-06-21 12:50:31 +00:00
import type { Locator, Page } from "playwright";
import { humanDelay } from "@/lib/utils";
function normalizeDraftText(text: string): string {
return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
}
function isTextInserted(actual: string, expected: string): boolean {
const a = normalizeDraftText(actual).trim();
const e = normalizeDraftText(expected).trim();
if (!e) return true;
if (a === e) return true;
if (a.length >= Math.min(e.length, 20)) return true;
return a.includes(e.slice(0, Math.min(24, e.length)));
}
async function clearEditor(page: Page, editor: Locator) {
await editor.scrollIntoViewIfNeeded().catch(() => undefined);
await editor.click();
await humanDelay(120, 220);
await page.keyboard.press("Meta+A");
await page.keyboard.press("Backspace");
await humanDelay(100, 180);
}
async function pasteTextIntoEditor(page: Page, editor: Locator, text: string): Promise<boolean> {
try {
await page.context().grantPermissions(["clipboard-read", "clipboard-write"]);
} catch {
// ignore permission errors on some hosts
}
try {
await page.evaluate(async (value) => {
await navigator.clipboard.writeText(value);
}, text);
await editor.click();
await page.keyboard.press("Meta+v");
await humanDelay(250, 450);
return true;
} catch {
return page.evaluate((value) => {
const target =
document.querySelector('[data-threadtools-editor="true"]') ??
document.querySelector('[role="dialog"] [contenteditable="true"]') ??
document.querySelector('[contenteditable="true"][role="textbox"]') ??
document.querySelector('[contenteditable="true"]');
if (!(target instanceof HTMLElement)) return false;
target.focus();
const dataTransfer = new DataTransfer();
dataTransfer.setData("text/plain", value);
const pasted = target.dispatchEvent(
new ClipboardEvent("paste", {
clipboardData: dataTransfer,
bubbles: true,
cancelable: true,
})
);
return pasted !== false;
}, text);
}
}
async function typeWithLineBreaks(page: Page, text: string) {
const normalized = normalizeDraftText(text);
const paragraphs = normalized.split(/\n\n+/);
for (let p = 0; p < paragraphs.length; p++) {
if (p > 0) {
await page.keyboard.press("Enter");
await humanDelay(40, 90);
}
const lines = paragraphs[p].split("\n");
for (let l = 0; l < lines.length; l++) {
if (l > 0) {
await page.keyboard.press("Shift+Enter");
await humanDelay(40, 90);
}
if (lines[l]) {
await page.keyboard.insertText(lines[l]);
await humanDelay(30, 70);
}
}
}
}
export async function typeIntoEditor(page: Page, editor: Locator, text: string) {
const normalized = normalizeDraftText(text);
await clearEditor(page, editor);
const pasted = await pasteTextIntoEditor(page, editor, normalized);
if (pasted) {
const value = await editor.innerText().catch(() => "");
if (isTextInserted(value, normalized)) return;
}
await clearEditor(page, editor);
await typeWithLineBreaks(page, normalized);
const value = await editor.innerText().catch(() => "");
if (!isTextInserted(value, normalized)) {
await clearEditor(page, editor);
await editor.click();
await page.keyboard.insertText(normalized);
}
}