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
? 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) => {