Add chat bulk actions and avatar cache
This commit is contained in:
@@ -338,6 +338,42 @@ app.post("/chat/history", async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/chat/clear-history", async (req, res) => {
|
||||
try {
|
||||
const externalChatId = String(req.body?.externalChatId || "").trim();
|
||||
const chatUrl = normalizeMaxChatUrl(req.body?.chatUrl);
|
||||
if (!externalChatId) {
|
||||
return res.json({ success: false, error: "externalChatId is required." });
|
||||
}
|
||||
|
||||
const result = await withPageLock(async () => {
|
||||
const p = await ensurePage("maintenance");
|
||||
return await clearChatHistoryInMax(p, externalChatId, chatUrl);
|
||||
}, "maintenance");
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
res.json({ success: false, error: String(error?.message || error) });
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/chat/delete", async (req, res) => {
|
||||
try {
|
||||
const externalChatId = String(req.body?.externalChatId || "").trim();
|
||||
const chatUrl = normalizeMaxChatUrl(req.body?.chatUrl);
|
||||
if (!externalChatId) {
|
||||
return res.json({ success: false, error: "externalChatId is required." });
|
||||
}
|
||||
|
||||
const result = await withPageLock(async () => {
|
||||
const p = await ensurePage("maintenance");
|
||||
return await deleteChatInMax(p, externalChatId, chatUrl);
|
||||
}, "maintenance");
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
res.json({ success: false, error: String(error?.message || error) });
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/chat/clear-composer", async (req, res) => {
|
||||
try {
|
||||
const externalChatId = String(req.body?.externalChatId || "").trim();
|
||||
@@ -4614,6 +4650,46 @@ async function deleteMessageInMax(p, externalChatId, externalMessageId, currentT
|
||||
return { success: true, error: null };
|
||||
}
|
||||
|
||||
async function clearChatHistoryInMax(p, externalChatId, chatUrl) {
|
||||
return await applyChatMenuActionInMax(
|
||||
p,
|
||||
externalChatId,
|
||||
chatUrl,
|
||||
["\u043e\u0447\u0438\u0441\u0442\u0438\u0442\u044c \u0438\u0441\u0442\u043e\u0440", "\u043e\u0447\u0438\u0441\u0442\u0438\u0442\u044c \u0447\u0430\u0442", "clear history", "clear chat"],
|
||||
["\u043e\u0447\u0438\u0441\u0442\u0438\u0442\u044c", "\u0434\u0430", "clear", "yes", "ok"],
|
||||
"clear history");
|
||||
}
|
||||
|
||||
async function deleteChatInMax(p, externalChatId, chatUrl) {
|
||||
return await applyChatMenuActionInMax(
|
||||
p,
|
||||
externalChatId,
|
||||
chatUrl,
|
||||
["\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", "\u0434\u0430", "delete", "yes", "ok"],
|
||||
"delete chat");
|
||||
}
|
||||
|
||||
async function applyChatMenuActionInMax(p, externalChatId, chatUrl, actionLabels, confirmLabels, actionName) {
|
||||
const opened = await ensureDomChatOpen(p, externalChatId, 5000, false, chatUrl);
|
||||
if (!opened) {
|
||||
return { success: false, error: "MAX chat was not found or did not open." };
|
||||
}
|
||||
|
||||
if (!(await openChatMenu(p, actionLabels))) {
|
||||
return { success: false, error: `MAX ${actionName} menu was not opened.` };
|
||||
}
|
||||
|
||||
if (!(await clickActionByText(p, actionLabels))) {
|
||||
return { success: false, error: `MAX ${actionName} action was not found in the chat menu.` };
|
||||
}
|
||||
|
||||
await p.waitForTimeout(700);
|
||||
await clickActionByText(p, confirmLabels);
|
||||
await p.waitForTimeout(1200);
|
||||
return { success: true, error: null };
|
||||
}
|
||||
|
||||
async function setMessageReactionInMax(p, externalChatId, externalMessageId, currentText, emoji) {
|
||||
const target = await locateMessageForAction(p, externalChatId, externalMessageId, currentText);
|
||||
if (!target) {
|
||||
@@ -4764,6 +4840,115 @@ async function clickMessageMenuButton(p, rect) {
|
||||
}, rect).catch(() => false);
|
||||
}
|
||||
|
||||
async function openChatMenu(p, expectedLabels) {
|
||||
for (let attempt = 0; attempt < 3; attempt++) {
|
||||
if (await clickChatMenuButton(p)) {
|
||||
await p.waitForTimeout(600);
|
||||
if (await hasMenuActionSurface(p, expectedLabels)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
await p.keyboard.press("Escape").catch(() => {});
|
||||
await p.waitForTimeout(250);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async function clickChatMenuButton(p) {
|
||||
return await p.evaluate(() => {
|
||||
const isVisible = (element) => {
|
||||
const style = window.getComputedStyle(element);
|
||||
const rect = element.getBoundingClientRect();
|
||||
return style.visibility !== "hidden" &&
|
||||
style.display !== "none" &&
|
||||
rect.width > 0 &&
|
||||
rect.height > 0 &&
|
||||
rect.bottom >= 0 &&
|
||||
rect.right >= 0 &&
|
||||
rect.top <= window.innerHeight &&
|
||||
rect.left <= window.innerWidth;
|
||||
};
|
||||
const textOf = (element) => String(
|
||||
element.innerText ||
|
||||
element.textContent ||
|
||||
element.getAttribute("aria-label") ||
|
||||
element.getAttribute("title") ||
|
||||
""
|
||||
).trim().toLowerCase();
|
||||
const chatRoot = document.querySelector("[class*='openedChat' i]") ||
|
||||
document.querySelector("main") ||
|
||||
document.body;
|
||||
const rootRect = chatRoot.getBoundingClientRect();
|
||||
const headerBottom = Math.min(window.innerHeight, rootRect.top + 170);
|
||||
const buttons = Array.from(document.querySelectorAll("button,[role='button']"))
|
||||
.filter((button) => {
|
||||
if (!(button instanceof HTMLElement) || !isVisible(button)) return false;
|
||||
const rect = button.getBoundingClientRect();
|
||||
const centerX = rect.left + rect.width / 2;
|
||||
const centerY = rect.top + rect.height / 2;
|
||||
return centerX >= rootRect.left + rootRect.width * 0.45 &&
|
||||
centerX <= rootRect.right + 8 &&
|
||||
centerY >= Math.max(0, rootRect.top) &&
|
||||
centerY <= headerBottom;
|
||||
});
|
||||
const match = buttons.find((button) => {
|
||||
const text = textOf(button);
|
||||
const cls = String(button.className || "").toLowerCase();
|
||||
return text.includes("\u0435\u0449") ||
|
||||
text.includes("more") ||
|
||||
text.includes("menu") ||
|
||||
text.includes("actions") ||
|
||||
cls.includes("more") ||
|
||||
cls.includes("menu") ||
|
||||
text === "\u22ef" ||
|
||||
text === "\u2026";
|
||||
}) || buttons.sort((a, b) => b.getBoundingClientRect().right - a.getBoundingClientRect().right)[0];
|
||||
|
||||
if (!match) {
|
||||
return false;
|
||||
}
|
||||
|
||||
match.click();
|
||||
return true;
|
||||
}).catch(() => false);
|
||||
}
|
||||
|
||||
async function hasMenuActionSurface(p, labels) {
|
||||
return await p.evaluate((rawLabels) => {
|
||||
const normalizedLabels = rawLabels.map((label) => String(label || "").trim().toLowerCase()).filter(Boolean);
|
||||
if (normalizedLabels.length === 0) {
|
||||
return false;
|
||||
}
|
||||
const isVisible = (element) => {
|
||||
const style = window.getComputedStyle(element);
|
||||
const rect = element.getBoundingClientRect();
|
||||
return style.visibility !== "hidden" &&
|
||||
style.display !== "none" &&
|
||||
rect.width > 0 &&
|
||||
rect.height > 0 &&
|
||||
rect.bottom >= 0 &&
|
||||
rect.right >= 0 &&
|
||||
rect.top <= window.innerHeight &&
|
||||
rect.left <= window.innerWidth;
|
||||
};
|
||||
const textOf = (element) => String(
|
||||
element.innerText ||
|
||||
element.textContent ||
|
||||
element.getAttribute("aria-label") ||
|
||||
element.getAttribute("title") ||
|
||||
""
|
||||
).trim().toLowerCase();
|
||||
return Array.from(document.querySelectorAll("[role='menu'],[role='menuitem'],[class*='menu' i],[class*='popover' i],[class*='dropdown' i],button,span,div"))
|
||||
.filter((element) => element instanceof HTMLElement && isVisible(element))
|
||||
.some((element) => {
|
||||
const text = textOf(element);
|
||||
return text && normalizedLabels.some((label) => text === label || text.includes(label));
|
||||
});
|
||||
}, labels).catch(() => false);
|
||||
}
|
||||
|
||||
async function hasActionSurface(p) {
|
||||
return await p.evaluate(() => {
|
||||
const isVisible = (element) => {
|
||||
|
||||
Reference in New Issue
Block a user