feat(browser): 插件真实坐标点击与页面内容读取(CF-2)

- umt 插件 POST /umt/tabs/{id}/mouse-click:page.mouse.click,跨域 iframe 的 Turnstile 也点得到;click 坐标优先走它,旧镜像退回 elementFromPoint
- 适配器 pageContent(地址 / 标题 / outerHTML / 正文 / navigator.userAgent);setEgress 返回插件出口模式
- mock camofox 增加 Cloudflare 模式(auto / interactive / captcha)与插件鼠标点击

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-30 21:26:53 +08:00
1 parent a11ef16e40
commit 6b14b0da45
7 files changed
+402 -10

No files matched your search

@@ -10,6 +10,9 @@
* GET /umt/identity → 固定浏览器身份的公开部分:UA、OS、版本、生成时间(BR-4)
* PUT /umt/sessions/:userId/proxy { proxy: {server, username?, password?} | null }
* → 该 profile 的出口:站点开代理 = 与 HTTP 同一代理;null = 直连
* POST /umt/tabs/:tabId/mouse-click { userId, x, y } → 按视口坐标发真实鼠标事件(page.mouse.click,CF-2):
* 跨域 iframe 里的 Cloudflare Turnstile 复选框也点得到;
* 标签页不在回 422(与"插件没有这个端点"的 404 区分)
* Cookie 值只在请求/响应体里,不写日志(只记个数);代理只记主机,不记账号口令。
*
* BR-4 固定浏览器身份(docs/runbook/browser.md §11):`browser:launching` 钩子里,首次用 camoufox-js 生成一份 os=windows
@@ -168,6 +171,16 @@ async function windowsFontconfig(installDir, log) {
}
}
/** 上游会话 → 标签页状态(与 server.js findTab 相同的遍历:tabGroups: Map<sessionKey, Map<tabId, TabState>>) */
function findTab(session, tabId) {
if (!session?.tabGroups) return null;
for (const group of session.tabGroups.values()) {
const t = group.get(tabId);
if (t?.page) return t;
}
return null;
}
const FIELDS = ["name", "value", "domain", "path", "expires", "httpOnly", "secure", "sameSite"];
function parseUrls(raw) {
@@ -364,7 +377,30 @@ export async function register(app, ctx) {
app.get("/umt/instance", ctx.auth(), (_req, res) => res.json(INSTANCE));
log("info", "umt-cookies plugin: registered cookies / instance / identity / egress endpoints", {
// ── CF-2 真实坐标点击 ──
app.post("/umt/tabs/:tabId/mouse-click", ctx.auth(), express.json({ limit: "4kb" }), async (req, res) => {
try {
const userId = ctx.normalizeUserId(String(req.body?.userId ?? ""));
const x = Number(req.body?.x);
const y = Number(req.body?.y);
if (!userId) return res.status(400).json({ error: "userId is required" });
if (!Number.isFinite(x) || !Number.isFinite(y) || x < 0 || y < 0 || x > 20000 || y > 20000)
return res.status(400).json({ error: "x / y must be viewport CSS pixels" });
const tab = findTab(ctx.sessions.get(userId), req.params.tabId);
if (!tab) return res.status(422).json({ error: "tab not found" });
const session = ctx.sessions.get(userId);
session.lastAccess = Date.now();
tab.toolCalls = (tab.toolCalls ?? 0) + 1;
await ctx.withUserLimit(userId, () => tab.page.mouse.click(Math.round(x), Math.round(y), { delay: 60 }));
log("info", "umt mouse click", { reqId: req.reqId, userId, tabId: req.params.tabId });
res.json({ ok: true, tabId: req.params.tabId, url: tab.page.url() });
} catch (err) {
log("error", "umt mouse click failed", { reqId: req.reqId, error: err.message });
res.status(500).json({ error: ctx.safeError(err) });
}
});
log("info", "umt-cookies plugin: registered cookies / instance / identity / egress / mouse-click endpoints", {
identityMode,
proxyMode,
});