Fix MAX bulk chat menu actions
This commit is contained in:
+52
-8
@@ -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) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user