Fix MAX bulk chat menu actions

This commit is contained in:
sevenhill
2026-07-05 21:29:39 +03:00
parent ce67dee9ed
commit 1cffa1ef8d
+52 -8
View File
@@ -410,6 +410,7 @@ app.post("/chat/menu-probe", async (req, res) => {
const labels = Array.isArray(req.body?.labels) && req.body.labels.length > 0 const labels = Array.isArray(req.body?.labels) && req.body.labels.length > 0
? req.body.labels.map((label) => String(label || "")) ? 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"]; : ["\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) { if (!externalChatId) {
return res.json({ success: false, error: "externalChatId is required.", chatUrl: null }); 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 result = await withPageLock(async () => {
const p = await ensurePage("incoming"); const p = await ensurePage("incoming");
const menuOpened = await openSidebarChatMenu(p, externalChatId, chatUrl, labels); 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 p.keyboard.press("Escape").catch(() => {});
await clearSidebarSearch(p); await clearSidebarSearch(p);
return { return {
success: menuOpened, success: menuOpened,
error: menuOpened ? null : "MAX chat menu was not opened.", error: menuOpened ? null : "MAX chat menu was not opened.",
chatUrl: await getCurrentChatUrl(p) chatUrl: await getCurrentChatUrl(p),
actionClicked,
visibleActions
}; };
}, "incoming"); }, "incoming");
res.json(result); res.json(result);
@@ -3264,6 +3274,8 @@ async function openSidebarChatMenu(p, externalChatId, chatUrl, expectedLabels) {
if (!opened) { if (!opened) {
const menuTexts = await readVisibleActionTexts(p); const menuTexts = await readVisibleActionTexts(p);
console.warn(`[chat/menu] expected action labels not found; labels=${expectedLabels.join("|")} visible=${menuTexts.slice(0, 20).join("|")}`); 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; return opened;
}; };
@@ -4973,14 +4985,22 @@ async function applyChatMenuActionInMax(p, externalChatId, chatUrl, actionLabels
return { success: false, error: `MAX ${actionName} menu was not opened.` }; 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 p.keyboard.press("Escape").catch(() => {});
await clearSidebarSearch(p); await clearSidebarSearch(p);
return { success: false, error: `MAX ${actionName} action was not found in the chat menu.` }; return { success: false, error: `MAX ${actionName} action was not found in the chat menu.` };
} }
await p.waitForTimeout(700); 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 p.waitForTimeout(1200);
await clearSidebarSearch(p); await clearSidebarSearch(p);
return { success: true, error: null }; return { success: true, error: null };
@@ -5336,12 +5356,24 @@ async function clickActionByText(p, labels) {
element.getAttribute("title") || element.getAttribute("title") ||
"" ""
).trim().toLowerCase(); ).trim().toLowerCase();
const candidates = Array.from(document.querySelectorAll("button,[role='button'],[role='menuitem'],span,div")) const scoreMatch = (element) => {
.filter((element) => element instanceof HTMLElement && isVisible(element));
const match = candidates.find((element) => {
const text = textOf(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) { if (!match) {
return false; return false;
} }
@@ -5352,6 +5384,18 @@ async function clickActionByText(p, labels) {
}, labels).catch(() => false); }, 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) { async function clickReactionNearMessage(p, rect, emoji) {
return await p.evaluate(({ targetRect, requestedEmoji }) => { return await p.evaluate(({ targetRect, requestedEmoji }) => {
const isVisible = (element) => { const isVisible = (element) => {