- 新增小红书前端模块(启动服务、内容分析、爆款创作、发布记录、技能卡片、拟人设置、AI 助手) - 新增 xhs-agent-desktop 独立桌面 Agent,对接 OMS 任务调度 - 登录页重构,整合微信登录与 OMS JWT 认证 - 订单、渠道、用户中心等页面与 API 增强 Co-authored-by: Cursor <cursoragent@cursor.com>
213 lines
7.0 KiB
JavaScript
213 lines
7.0 KiB
JavaScript
/**
|
||
* XHS Bridge - Content Script(隔离 world)
|
||
*
|
||
* 接收来自 background.js 的 DOM 操作命令并执行。
|
||
* evaluate / has_element 等需要访问页面 JS 变量的命令由 background.js
|
||
* 通过 chrome.scripting.executeScript(world:"MAIN") 直接处理,不经过这里。
|
||
*/
|
||
|
||
chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
|
||
handleDomCommand(msg.method, msg.params || {})
|
||
.then((result) => sendResponse({ result: result ?? null }))
|
||
.catch((err) => sendResponse({ error: String(err.message || err) }));
|
||
return true; // 异步响应
|
||
});
|
||
|
||
async function handleDomCommand(method, params) {
|
||
switch (method) {
|
||
case "click_element":
|
||
return cmdClickElement(params);
|
||
|
||
case "input_text":
|
||
return cmdInputText(params);
|
||
|
||
case "input_content_editable":
|
||
return cmdInputContentEditable(params);
|
||
|
||
case "scroll_by":
|
||
window.scrollBy(params.x || 0, params.y || 0);
|
||
return null;
|
||
|
||
case "scroll_to":
|
||
window.scrollTo(params.x || 0, params.y || 0);
|
||
return null;
|
||
|
||
case "scroll_to_bottom":
|
||
window.scrollTo(0, document.body.scrollHeight);
|
||
return null;
|
||
|
||
case "scroll_element_into_view": {
|
||
const el = document.querySelector(params.selector);
|
||
if (el) el.scrollIntoView({ behavior: "smooth", block: "center" });
|
||
return null;
|
||
}
|
||
|
||
case "scroll_nth_element_into_view": {
|
||
const els = document.querySelectorAll(params.selector);
|
||
if (els[params.index]) els[params.index].scrollIntoView({ behavior: "smooth", block: "center" });
|
||
return null;
|
||
}
|
||
|
||
case "dispatch_wheel_event": {
|
||
const target =
|
||
document.querySelector(".note-scroller") ||
|
||
document.querySelector(".interaction-container") ||
|
||
document.documentElement;
|
||
target.dispatchEvent(
|
||
new WheelEvent("wheel", {
|
||
deltaY: params.deltaY || 0,
|
||
deltaMode: 0,
|
||
bubbles: true,
|
||
cancelable: true,
|
||
view: window,
|
||
}),
|
||
);
|
||
return null;
|
||
}
|
||
|
||
case "mouse_move": {
|
||
document.dispatchEvent(
|
||
new MouseEvent("mousemove", { clientX: params.x, clientY: params.y, bubbles: true }),
|
||
);
|
||
return null;
|
||
}
|
||
|
||
case "mouse_click": {
|
||
const el = document.elementFromPoint(params.x, params.y);
|
||
if (el) {
|
||
el.dispatchEvent(new MouseEvent("mousedown", { clientX: params.x, clientY: params.y, bubbles: true }));
|
||
el.dispatchEvent(new MouseEvent("mouseup", { clientX: params.x, clientY: params.y, bubbles: true }));
|
||
el.dispatchEvent(new MouseEvent("click", { clientX: params.x, clientY: params.y, bubbles: true }));
|
||
}
|
||
return null;
|
||
}
|
||
|
||
case "press_key": {
|
||
const keyMap = {
|
||
Enter: { key: "Enter", code: "Enter", keyCode: 13 },
|
||
ArrowDown: { key: "ArrowDown", code: "ArrowDown", keyCode: 40 },
|
||
Tab: { key: "Tab", code: "Tab", keyCode: 9 },
|
||
Backspace: { key: "Backspace", code: "Backspace", keyCode: 8 },
|
||
};
|
||
const info = keyMap[params.key] || { key: params.key, code: params.key, keyCode: 0 };
|
||
const active = document.activeElement || document.body;
|
||
active.dispatchEvent(new KeyboardEvent("keydown", { ...info, bubbles: true }));
|
||
active.dispatchEvent(new KeyboardEvent("keyup", { ...info, bubbles: true }));
|
||
return null;
|
||
}
|
||
|
||
case "type_text": {
|
||
const active = document.activeElement || document.body;
|
||
const delay = params.delayMs || 50;
|
||
for (const char of params.text) {
|
||
active.dispatchEvent(new KeyboardEvent("keydown", { key: char, bubbles: true }));
|
||
active.dispatchEvent(new KeyboardEvent("keypress", { key: char, bubbles: true }));
|
||
active.dispatchEvent(new KeyboardEvent("keyup", { key: char, bubbles: true }));
|
||
await sleep(delay);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
case "remove_element": {
|
||
const el = document.querySelector(params.selector);
|
||
if (el) el.remove();
|
||
return null;
|
||
}
|
||
|
||
case "hover_element": {
|
||
const el = document.querySelector(params.selector);
|
||
if (el) {
|
||
const rect = el.getBoundingClientRect();
|
||
const x = rect.left + rect.width / 2;
|
||
const y = rect.top + rect.height / 2;
|
||
el.dispatchEvent(new MouseEvent("mouseover", { clientX: x, clientY: y, bubbles: true }));
|
||
el.dispatchEvent(new MouseEvent("mousemove", { clientX: x, clientY: y, bubbles: true }));
|
||
}
|
||
return null;
|
||
}
|
||
|
||
case "select_all_text": {
|
||
const el = document.querySelector(params.selector);
|
||
if (el) {
|
||
el.focus();
|
||
if (el.select) el.select();
|
||
else document.execCommand("selectAll");
|
||
}
|
||
return null;
|
||
}
|
||
|
||
case "set_file_input":
|
||
return cmdSetFileInput(params);
|
||
|
||
default:
|
||
throw new Error(`content.js: 未知命令 ${method}`);
|
||
}
|
||
}
|
||
|
||
// ───────────────────────── 具体实现 ─────────────────────────
|
||
|
||
function cmdClickElement({ selector }) {
|
||
const el = document.querySelector(selector);
|
||
if (!el) throw new Error(`元素不存在: ${selector}`);
|
||
el.scrollIntoView({ block: "center" });
|
||
el.click();
|
||
return null;
|
||
}
|
||
|
||
function cmdInputText({ selector, text }) {
|
||
const el = document.querySelector(selector);
|
||
if (!el) throw new Error(`元素不存在: ${selector}`);
|
||
el.focus();
|
||
el.value = text;
|
||
el.dispatchEvent(new Event("input", { bubbles: true }));
|
||
el.dispatchEvent(new Event("change", { bubbles: true }));
|
||
return null;
|
||
}
|
||
|
||
async function cmdInputContentEditable({ selector, text }) {
|
||
const el = document.querySelector(selector);
|
||
if (!el) throw new Error(`元素不存在: ${selector}`);
|
||
el.focus();
|
||
// 全选清空
|
||
document.execCommand("selectAll", false, null);
|
||
document.execCommand("delete", false, null);
|
||
await sleep(80);
|
||
// 逐行插入(换行转为 Enter 键事件)
|
||
const lines = text.split("\n");
|
||
for (let i = 0; i < lines.length; i++) {
|
||
if (lines[i]) document.execCommand("insertText", false, lines[i]);
|
||
if (i < lines.length - 1) {
|
||
el.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", code: "Enter", keyCode: 13, bubbles: true }));
|
||
el.dispatchEvent(new KeyboardEvent("keyup", { key: "Enter", code: "Enter", keyCode: 13, bubbles: true }));
|
||
await sleep(40);
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
async function cmdSetFileInput({ selector, files }) {
|
||
const el = document.querySelector(selector);
|
||
if (!el) throw new Error(`文件输入框不存在: ${selector}`);
|
||
|
||
const dt = new DataTransfer();
|
||
for (const f of files) {
|
||
const bytes = Uint8Array.from(atob(f.data), (c) => c.charCodeAt(0));
|
||
const blob = new Blob([bytes], { type: f.type });
|
||
dt.items.add(new File([blob], f.name, { type: f.type }));
|
||
}
|
||
|
||
Object.defineProperty(el, "files", {
|
||
value: dt.files,
|
||
configurable: true,
|
||
writable: true,
|
||
});
|
||
|
||
el.dispatchEvent(new Event("change", { bubbles: true }));
|
||
el.dispatchEvent(new Event("input", { bubbles: true }));
|
||
return null;
|
||
}
|
||
|
||
function sleep(ms) {
|
||
return new Promise((r) => setTimeout(r, ms));
|
||
}
|