diff --git a/worker/src/server.js b/worker/src/server.js index c53f56a..c7c1443 100644 --- a/worker/src/server.js +++ b/worker/src/server.js @@ -410,6 +410,7 @@ app.post("/chat/menu-probe", async (req, res) => { const labels = Array.isArray(req.body?.labels) && req.body.labels.length > 0 ? req.body.labels.map((label) => String(label || "")) : ["\u0443\u0434\u0430\u043b\u0438\u0442\u044c \u0447\u0430\u0442", "\u0443\u0434\u0430\u043b\u0438\u0442\u044c", "delete chat", "delete conversation", "remove chat"]; + const probeAction = Boolean(req.body?.probeAction); if (!externalChatId) { return res.json({ success: false, error: "externalChatId is required.", chatUrl: null }); } @@ -417,12 +418,21 @@ app.post("/chat/menu-probe", async (req, res) => { const result = await withPageLock(async () => { const p = await ensurePage("incoming"); const menuOpened = await openSidebarChatMenu(p, externalChatId, chatUrl, labels); + const actionClicked = menuOpened && probeAction + ? await clickActionByTextWithRetry(p, labels, 1500) + : false; + if (actionClicked) { + await p.waitForTimeout(700); + } + const visibleActions = actionClicked ? await readVisibleActionTexts(p) : []; await p.keyboard.press("Escape").catch(() => {}); await clearSidebarSearch(p); return { success: menuOpened, error: menuOpened ? null : "MAX chat menu was not opened.", - chatUrl: await getCurrentChatUrl(p) + chatUrl: await getCurrentChatUrl(p), + actionClicked, + visibleActions }; }, "incoming"); res.json(result); @@ -3264,6 +3274,8 @@ async function openSidebarChatMenu(p, externalChatId, chatUrl, expectedLabels) { if (!opened) { const menuTexts = await readVisibleActionTexts(p); console.warn(`[chat/menu] expected action labels not found; labels=${expectedLabels.join("|")} visible=${menuTexts.slice(0, 20).join("|")}`); + await p.keyboard.press("Escape").catch(() => {}); + await p.waitForTimeout(150); } return opened; }; @@ -4973,14 +4985,22 @@ async function applyChatMenuActionInMax(p, externalChatId, chatUrl, actionLabels return { success: false, error: `MAX ${actionName} menu was not opened.` }; } - if (!(await clickActionByText(p, actionLabels))) { + if (!(await clickActionByTextWithRetry(p, actionLabels, 1500))) { await p.keyboard.press("Escape").catch(() => {}); await clearSidebarSearch(p); return { success: false, error: `MAX ${actionName} action was not found in the chat menu.` }; } await p.waitForTimeout(700); - await clickActionByText(p, confirmLabels); + if (!(await clickActionByTextWithRetry(p, confirmLabels, 3000))) { + const menuTexts = await readVisibleActionTexts(p); + await p.keyboard.press("Escape").catch(() => {}); + await clearSidebarSearch(p); + return { + success: false, + error: `MAX ${actionName} confirmation was not found. Visible actions: ${menuTexts.slice(0, 12).join(" | ")}` + }; + } await p.waitForTimeout(1200); await clearSidebarSearch(p); return { success: true, error: null }; @@ -5336,12 +5356,24 @@ async function clickActionByText(p, labels) { element.getAttribute("title") || "" ).trim().toLowerCase(); - const candidates = Array.from(document.querySelectorAll("button,[role='button'],[role='menuitem'],span,div")) - .filter((element) => element instanceof HTMLElement && isVisible(element)); - const match = candidates.find((element) => { + const scoreMatch = (element) => { const text = textOf(element); - return text && normalizedLabels.some((label) => text === label || text.includes(label)); - }); + if (!text) return 0; + if (normalizedLabels.some((label) => text === label)) return 4; + if (normalizedLabels.some((label) => text.startsWith(label))) return 3; + if (normalizedLabels.some((label) => text.includes(label))) return 2; + return 0; + }; + const interactive = Array.from(document.querySelectorAll("button,[role='button'],[role='menuitem']")) + .filter((element) => element instanceof HTMLElement && isVisible(element)); + const leafTextNodes = Array.from(document.querySelectorAll("span,div")) + .filter((element) => element instanceof HTMLElement && isVisible(element)) + .filter((element) => element.querySelector("button,[role='button'],[role='menuitem']") == null) + .filter((element) => element.children.length <= 1); + const match = [...interactive, ...leafTextNodes] + .map((element, index) => ({ element, index, score: scoreMatch(element) })) + .filter((item) => item.score > 0) + .sort((a, b) => b.score - a.score || a.index - b.index)[0]?.element || null; if (!match) { return false; } @@ -5352,6 +5384,18 @@ async function clickActionByText(p, labels) { }, labels).catch(() => false); } +async function clickActionByTextWithRetry(p, labels, timeoutMs = 2000) { + const deadline = Date.now() + timeoutMs; + while (Date.now() <= deadline) { + if (await clickActionByText(p, labels)) { + return true; + } + await p.waitForTimeout(200); + } + + return false; +} + async function clickReactionNearMessage(p, rect, emoji) { return await p.evaluate(({ targetRect, requestedEmoji }) => { const isVisible = (element) => {