@ozaiya/openclaw-channel 0.10.26 → 0.10.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @ozaiya/openclaw-channel
2
2
 
3
- [OpenClaw](https://openclaw.ai) channel plugin for [Ozaiya](https://ozai.dev) E2E encrypted group chat.
3
+ [OpenClaw](https://openclaw.ai) channel plugin for [Ozaiya](https://ozaiya.com) E2E encrypted group chat.
4
4
 
5
5
  ## Overview
6
6
 
@@ -85,7 +85,7 @@ Notes:
85
85
  ## Requirements
86
86
 
87
87
  - [OpenClaw](https://openclaw.ai) >= 2026.0.0
88
- - An [Ozaiya](https://ozai.dev) account
88
+ - An [Ozaiya](https://ozaiya.com) account
89
89
 
90
90
  ## License
91
91
 
@@ -0,0 +1,21 @@
1
+ export interface CdpCookie {
2
+ name: string;
3
+ value: string;
4
+ domain: string;
5
+ /** Unix seconds; -1 / undefined for a session cookie. */
6
+ expires?: number;
7
+ }
8
+ export interface LoginCdpClient {
9
+ /** JPEG of the current page (the login / QR wall). */
10
+ screenshot: (quality?: number) => Promise<Buffer | null>;
11
+ /** All cookies (incl. HttpOnly — only reachable via CDP, not document.cookie). */
12
+ getCookies: () => Promise<CdpCookie[]>;
13
+ /** Top-frame URL (so we can tell when the page leaves a login/auth page). */
14
+ currentUrl: () => Promise<string | null>;
15
+ close: () => void;
16
+ }
17
+ /**
18
+ * Open a short-lived CDP client against the browser's active page target.
19
+ * Returns null if the browser is unreachable or has no page target.
20
+ */
21
+ export declare function connectLoginCdp(host: string, port: number): Promise<LoginCdpClient | null>;
@@ -0,0 +1,133 @@
1
+ /**
2
+ * One-shot CDP helpers for the human-in-the-loop login handoff (`request_login`).
3
+ *
4
+ * Unlike sandboxScreenCdp's continuous screencast, this is a short-lived client for
5
+ * a few discrete calls: grab a screenshot of the current page (the login / QR wall),
6
+ * read cookies (to detect a completed login), and read the current URL. It connects
7
+ * to the SAME browser CDP endpoint the screencast uses (e.g. browser:9222, Host
8
+ * spoofed to localhost), so it sees exactly the page the agent's browser plugin is
9
+ * driving.
10
+ *
11
+ * Generic by design: nothing here is goofish/闲鱼-specific. The CALLER decides what a
12
+ * "successful login" looks like (a cookie appearing, or the URL leaving the login
13
+ * page), so the same handoff works for any login-gated site and any bot.
14
+ *
15
+ * CDP reachability: Chromium rejects DevTools HTTP/WS whose Host header isn't an IP
16
+ * or `localhost`, so we spoof `Host: localhost` — same trick sandboxScreenCdp uses.
17
+ */
18
+ import http from "node:http";
19
+ /** GET a CDP HTTP endpoint with a spoofed localhost Host header. */
20
+ function cdpGet(host, port, path) {
21
+ return new Promise((resolve, reject) => {
22
+ const req = http.request({ host, port, path, headers: { Host: "localhost" }, timeout: 5000 }, (res) => {
23
+ let body = "";
24
+ res.on("data", (c) => (body += c));
25
+ res.on("end", () => resolve(body));
26
+ });
27
+ req.on("error", reject);
28
+ req.on("timeout", () => { req.destroy(new Error("cdp http timeout")); });
29
+ req.end();
30
+ });
31
+ }
32
+ /**
33
+ * Open a short-lived CDP client against the browser's active page target.
34
+ * Returns null if the browser is unreachable or has no page target.
35
+ */
36
+ export async function connectLoginCdp(host, port) {
37
+ const WsModule = await import("ws");
38
+ const WSImpl = (WsModule.default ?? WsModule);
39
+ // Discover the page target's ws path (Host spoofed to localhost). Prefer a real
40
+ // http(s) page over about:blank/devtools so we capture the page the agent is on.
41
+ let pageWsPath;
42
+ try {
43
+ const json = await cdpGet(host, port, "/json");
44
+ const targets = JSON.parse(json);
45
+ const pages = targets.filter((t) => t.type === "page" && t.webSocketDebuggerUrl);
46
+ const page = pages.find((t) => /^https?:/i.test(t.url ?? "")) ?? pages[0];
47
+ if (!page?.webSocketDebuggerUrl)
48
+ return null;
49
+ pageWsPath = new URL(page.webSocketDebuggerUrl).pathname;
50
+ }
51
+ catch {
52
+ return null;
53
+ }
54
+ const ws = new WSImpl(`ws://${host}:${port}${pageWsPath}`, { headers: { Host: "localhost" } });
55
+ ws.binaryType = "arraybuffer";
56
+ const pending = new Map();
57
+ let cmdId = 1;
58
+ let closed = false;
59
+ ws.on("message", (raw) => {
60
+ let msg;
61
+ try {
62
+ msg = JSON.parse(typeof raw === "string" ? raw : Buffer.from(raw).toString());
63
+ }
64
+ catch {
65
+ return;
66
+ }
67
+ if (typeof msg.id === "number" && pending.has(msg.id)) {
68
+ const p = pending.get(msg.id);
69
+ clearTimeout(p.timer);
70
+ pending.delete(msg.id);
71
+ p.resolve(msg.result ?? null);
72
+ }
73
+ });
74
+ ws.on("error", () => { });
75
+ const opened = await new Promise((resolve) => {
76
+ const t = setTimeout(() => resolve(false), 5000);
77
+ ws.on("open", () => { clearTimeout(t); resolve(true); });
78
+ ws.on("close", () => { clearTimeout(t); resolve(false); });
79
+ });
80
+ if (!opened) {
81
+ try {
82
+ ws.close();
83
+ }
84
+ catch { /* ignore */ }
85
+ return null;
86
+ }
87
+ const call = (method, params, timeoutMs = 6000) => new Promise((resolve) => {
88
+ if (closed || ws.readyState !== 1) {
89
+ resolve(null);
90
+ return;
91
+ }
92
+ const id = cmdId++;
93
+ const timer = setTimeout(() => { pending.delete(id); resolve(null); }, timeoutMs);
94
+ pending.set(id, { resolve, timer });
95
+ try {
96
+ ws.send(JSON.stringify({ id, method, params }));
97
+ }
98
+ catch {
99
+ clearTimeout(timer);
100
+ pending.delete(id);
101
+ resolve(null);
102
+ }
103
+ });
104
+ await call("Network.enable");
105
+ return {
106
+ async screenshot(quality = 75) {
107
+ const r = await call("Page.captureScreenshot", { format: "jpeg", quality }, 8000);
108
+ const data = r?.data;
109
+ return data ? Buffer.from(data, "base64") : null;
110
+ },
111
+ async getCookies() {
112
+ const r = await call("Network.getAllCookies");
113
+ const cookies = r?.cookies;
114
+ return Array.isArray(cookies) ? cookies : [];
115
+ },
116
+ async currentUrl() {
117
+ const r = await call("Runtime.evaluate", { expression: "location.href", returnByValue: true });
118
+ const v = r?.result?.value;
119
+ return typeof v === "string" ? v : null;
120
+ },
121
+ close() {
122
+ closed = true;
123
+ for (const p of pending.values())
124
+ clearTimeout(p.timer);
125
+ pending.clear();
126
+ try {
127
+ ws.close();
128
+ }
129
+ catch { /* ignore */ }
130
+ },
131
+ };
132
+ }
133
+ //# sourceMappingURL=cdpLogin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cdpLogin.js","sourceRoot":"","sources":["../../src/cdpLogin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,oEAAoE;AACpE,SAAS,MAAM,CAAC,IAAY,EAAE,IAAY,EAAE,IAAY;IACtD,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CACtB,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,EACnE,CAAC,GAAG,EAAE,EAAE;YACN,IAAI,IAAI,GAAG,EAAE,CAAC;YACd,GAAG,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC;YACnC,GAAG,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;QACrC,CAAC,CACF,CAAC;QACF,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QACxB,GAAG,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE,GAAG,GAAG,CAAC,OAAO,CAAC,IAAI,KAAK,CAAC,kBAAkB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACzE,GAAG,CAAC,GAAG,EAAE,CAAC;IACZ,CAAC,CAAC,CAAC;AACL,CAAC;AAoBD;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,IAAY,EAAE,IAAY;IAE9D,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,CAAC;IACpC,MAAM,MAAM,GAAG,CAAC,QAAQ,CAAC,OAAO,IAAI,QAAQ,CAAkC,CAAC;IAE/E,gFAAgF;IAChF,iFAAiF;IACjF,IAAI,UAAkB,CAAC;IACvB,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;QAC/C,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAyE,CAAC;QACzG,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,IAAI,CAAC,CAAC,oBAAoB,CAAC,CAAC;QACjF,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,EAAE,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC;QAC1E,IAAI,CAAC,IAAI,EAAE,oBAAoB;YAAE,OAAO,IAAI,CAAC;QAC7C,UAAU,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC,QAAQ,CAAC;IAC3D,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,EAAE,GAAO,IAAI,MAAM,CAAC,QAAQ,IAAI,IAAI,IAAI,GAAG,UAAU,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,EAAE,CAAC,CAAC;IAClG,EAAwC,CAAC,UAAU,GAAG,aAAa,CAAC;IAErE,MAAM,OAAO,GAAG,IAAI,GAAG,EAAmF,CAAC;IAC3G,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,IAAI,MAAM,GAAG,KAAK,CAAC;IAEnB,EAAE,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,GAAkC,EAAE,EAAE;QACtD,IAAI,GAAsC,CAAC;QAC3C,IAAI,CAAC;YAAC,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,GAAkB,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC;QAAC,CAAC;QAAC,MAAM,CAAC;YAAC,OAAO;QAAC,CAAC;QACvH,IAAI,OAAO,GAAG,CAAC,EAAE,KAAK,QAAQ,IAAI,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC;YACtD,MAAM,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAE,CAAC;YAC/B,YAAY,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;YACtB,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YACvB,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,IAAI,IAAI,CAAC,CAAC;QAChC,CAAC;IACH,CAAC,CAAC,CAAC;IACH,EAAE,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,GAA+C,CAAC,CAAC,CAAC;IAEtE,MAAM,MAAM,GAAG,MAAM,IAAI,OAAO,CAAU,CAAC,OAAO,EAAE,EAAE;QACpD,MAAM,CAAC,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,IAAI,CAAC,CAAC;QACjD,EAAE,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,EAAE,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACzD,EAAE,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC7D,CAAC,CAAC,CAAC;IACH,IAAI,CAAC,MAAM,EAAE,CAAC;QAAC,IAAI,CAAC;YAAC,EAAE,CAAC,KAAK,EAAE,CAAC;QAAC,CAAC;QAAC,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC;QAAC,OAAO,IAAI,CAAC;IAAC,CAAC;IAExE,MAAM,IAAI,GAAG,CAAC,MAAc,EAAE,MAAgC,EAAE,SAAS,GAAG,IAAI,EAAoB,EAAE,CACpG,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QACtB,IAAI,MAAM,IAAI,EAAE,CAAC,UAAU,KAAK,CAAC,EAAE,CAAC;YAAC,OAAO,CAAC,IAAI,CAAC,CAAC;YAAC,OAAO;QAAC,CAAC;QAC7D,MAAM,EAAE,GAAG,KAAK,EAAE,CAAC;QACnB,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC;QAClF,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;QACpC,IAAI,CAAC;YAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC;QAAC,CAAC;QACxD,MAAM,CAAC;YAAC,YAAY,CAAC,KAAK,CAAC,CAAC;YAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;YAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QAAC,CAAC;IACnE,CAAC,CAAC,CAAC;IAEL,MAAM,IAAI,CAAC,gBAAgB,CAAC,CAAC;IAE7B,OAAO;QACL,KAAK,CAAC,UAAU,CAAC,OAAO,GAAG,EAAE;YAC3B,MAAM,CAAC,GAAG,MAAM,IAAI,CAAC,wBAAwB,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,IAAI,CAAC,CAAC;YAClF,MAAM,IAAI,GAAI,CAA8B,EAAE,IAAI,CAAC;YACnD,OAAO,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QACnD,CAAC;QACD,KAAK,CAAC,UAAU;YACd,MAAM,CAAC,GAAG,MAAM,IAAI,CAAC,uBAAuB,CAAC,CAAC;YAC9C,MAAM,OAAO,GAAI,CAAsC,EAAE,OAAO,CAAC;YACjE,OAAO,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/C,CAAC;QACD,KAAK,CAAC,UAAU;YACd,MAAM,CAAC,GAAG,MAAM,IAAI,CAAC,kBAAkB,EAAE,EAAE,UAAU,EAAE,eAAe,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;YAC/F,MAAM,CAAC,GAAI,CAA4C,EAAE,MAAM,EAAE,KAAK,CAAC;YACvE,OAAO,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QAC1C,CAAC;QACD,KAAK;YACH,MAAM,GAAG,IAAI,CAAC;YACd,KAAK,MAAM,CAAC,IAAI,OAAO,CAAC,MAAM,EAAE;gBAAE,YAAY,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;YACxD,OAAO,CAAC,KAAK,EAAE,CAAC;YAChB,IAAI,CAAC;gBAAC,EAAE,CAAC,KAAK,EAAE,CAAC;YAAC,CAAC;YAAC,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC;QAC5C,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -13,6 +13,7 @@ import { registerPluginHttpRoute } from "openclaw/plugin-sdk/webhook-ingress";
13
13
  import { unwrapGroupKey, decryptMessage, encryptMessage, wrapGroupKey } from "./crypto.js";
14
14
  import { resolveImageGenerationConfig, generateImage } from "./imageGeneration.js";
15
15
  import { createDesktopTool } from "./desktopTool.js";
16
+ import { connectLoginCdp } from "./cdpLogin.js";
16
17
  import { startRecording, stopAndExtractRecording } from "./desktopRecorder.js";
17
18
  import { sendMessage, probeApi, fetchGroups, addMember, getUserPublicKeys, toggleReaction, editMessage, deleteMessage, pinMessage, unpinMessage, uploadFile, searchUsers, fetchLinkPreview, joinCall, leaveCall, startCall, startPhoneCall, endPhoneCall, updatePhoneCallStatus, } from "./api.js";
18
19
  import { botCreateDirect, botCreateGroup } from "./botActions.js";
@@ -30,7 +31,7 @@ import { summarizeWithDoubao } from "./doubao.js";
30
31
  import { fetchXueqiuPost, searchXueqiuPosts } from "./xueqiu.js";
31
32
  import { fetchSocialMediaPost, searchSocialMedia, extractSocialMediaContent } from "./socialMedia.js";
32
33
  import { requestConfirmation, parseActionCallback, resolveConfirmation } from "./actionConfirmation.js";
33
- const DEFAULT_API_BASE_URL = "https://api.ozai.dev";
34
+ const DEFAULT_API_BASE_URL = "https://api.ozaiya.com";
34
35
  const DEFAULT_WEBHOOK_PATH = "/ozaiya/webhook";
35
36
  const DEFAULT_ACCOUNT_ID = "default";
36
37
  const GATEWAY_ACCOUNT_ID = "ozaiya-gateway";
@@ -63,6 +64,9 @@ const READ_ONLY_TOOLS = new Set([
63
64
  "update_plan",
64
65
  // publish_artifact registers a deliverable; surfaced in its own section, not a step.
65
66
  "publish_artifact",
67
+ // request_login is an interactive human-handoff; the QR/login message it posts to
68
+ // the chat IS the visible signal, so it shouldn't also show as a work step.
69
+ "request_login",
66
70
  ]);
67
71
  // Explicit tool → category overrides. Anything not listed falls through to the
68
72
  // keyword heuristic in categorize() below.
@@ -1623,6 +1627,105 @@ function buildChannelTools(account, cfg) {
1623
1627
  }
1624
1628
  },
1625
1629
  });
1630
+ // Login handoff tool: when the agent hits a login wall (sign-in page, QR scan,
1631
+ // captcha, 2FA) it can't get past on its own, this captures the current browser
1632
+ // screen, sends it to the chat for the human to scan / log in, and BLOCKS until
1633
+ // they finish or it times out. Generic by design — works for any login-gated site
1634
+ // (the QR's a screenshot; success is detected by a login cookie appearing or the
1635
+ // page leaving the login URL), so the same primitive serves any bot, not just goofish.
1636
+ tools.push({
1637
+ name: "request_login",
1638
+ label: "Request Login",
1639
+ ownerOnly: false,
1640
+ description: "Ask the human to complete an interactive login when a site blocks you with a sign-in wall, QR-code scan, captcha, or 2FA that you cannot pass yourself. " +
1641
+ "Call this the MOMENT you land on such a page and cannot proceed — do NOT give up, and do NOT fall back to web search. " +
1642
+ "It captures the current browser screen, sends it to the chat so the user can scan / sign in, and waits until they finish (or it times out). " +
1643
+ "When it returns loggedIn:true, reload the page and continue the task. " +
1644
+ "Optional: `message` (instruction shown to the user) and `timeoutSec` (how long to wait, default 180).",
1645
+ parameters: {
1646
+ type: "object",
1647
+ properties: {
1648
+ message: { type: "string", description: "Short instruction shown to the user, e.g. '请用手机闲鱼/淘宝扫码登录'." },
1649
+ timeoutSec: { type: "number", description: "Seconds to wait for the user (default 180, max 600)." },
1650
+ },
1651
+ },
1652
+ execute: async (_toolCallId, rawArgs) => {
1653
+ const args = (rawArgs ?? {});
1654
+ const dispatch = activeDispatches.get(account.accountId);
1655
+ const groupId = dispatch?.groupId ?? accountToOriginGroupId.get(account.accountId);
1656
+ if (!groupId) {
1657
+ return { content: [{ type: "text", text: "No active chat to send the login request to." }] };
1658
+ }
1659
+ // Same CDP endpoint the screencast uses: in-container NOVNC_HOST:9222, else local.
1660
+ const host = process.env.NOVNC_HOST || "localhost";
1661
+ const port = Number(process.env.OPENCLAW_BROWSER_CDP_PORT || process.env.CDP_PORT || "9222");
1662
+ const timeoutMs = Math.min(600, Math.max(30, Number(args.timeoutSec) || 180)) * 1000;
1663
+ const cdp = await connectLoginCdp(host, port);
1664
+ if (!cdp) {
1665
+ return { content: [{ type: "text", text: `Could not reach the browser to capture the login screen (CDP ${host}:${port}). Tell the user login is required but the screen can't be shown.` }] };
1666
+ }
1667
+ // Generic login-success detection — no site-specific assumptions:
1668
+ // (a) a strong auth cookie appears or changes (taobao/ali family incl. goofish
1669
+ // `unb`; these are only set on login, so a new value = a fresh sign-in), OR
1670
+ // (b) the page navigates away from the login/auth URL it started on.
1671
+ const AUTH_COOKIES = ["unb", "_l_g_", "lgc"];
1672
+ const LOGIN_URL_RE = /passport\.|login\.|\/login|signin|sign-in|account\.|\/auth/i;
1673
+ const authSnapshot = (cs) => {
1674
+ const m = {};
1675
+ for (const c of cs)
1676
+ if (AUTH_COOKIES.includes(c.name) && c.value)
1677
+ m[c.name] = c.value;
1678
+ return m;
1679
+ };
1680
+ try {
1681
+ const startUrl = (await cdp.currentUrl()) ?? "";
1682
+ const startedOnLogin = LOGIN_URL_RE.test(startUrl);
1683
+ const startAuth = authSnapshot(await cdp.getCookies());
1684
+ const sendShot = async (note) => {
1685
+ const shot = await cdp.screenshot();
1686
+ if (!shot)
1687
+ return false;
1688
+ const uploaded = await uploadFile(account.apiBaseUrl, account.botToken, groupId, `login-${Date.now()}.jpg`, "image/jpeg", shot);
1689
+ await sendEncryptedChatContent({ account, groupId, content: { files: [uploaded], text: note } }).catch(() => { });
1690
+ return true;
1691
+ };
1692
+ const mins = Math.max(1, Math.round(timeoutMs / 60000));
1693
+ const instruction = (args.message?.trim() || "🔐 需要登录后才能继续。请用手机扫描下方二维码(或在此页面登录),完成后我会自动继续。") +
1694
+ `(约 ${mins} 分钟内有效)`;
1695
+ if (!(await sendShot(instruction))) {
1696
+ cdp.close();
1697
+ return { content: [{ type: "text", text: "Failed to capture the login screen." }] };
1698
+ }
1699
+ const deadline = Date.now() + timeoutMs;
1700
+ let resent = false;
1701
+ while (Date.now() < deadline) {
1702
+ await new Promise((r) => setTimeout(r, 3000));
1703
+ const nowAuth = authSnapshot(await cdp.getCookies());
1704
+ const url = (await cdp.currentUrl()) ?? "";
1705
+ const authChanged = Object.keys(nowAuth).some((n) => nowAuth[n] !== startAuth[n]); // appeared or rotated on sign-in
1706
+ const leftLogin = startedOnLogin && !!url && !LOGIN_URL_RE.test(url);
1707
+ if (authChanged || leftLogin) {
1708
+ await sendEncryptedChatContent({ account, groupId, content: { text: "✅ 登录成功,继续任务。" } }).catch(() => { });
1709
+ cdp.close();
1710
+ return { content: [{ type: "text", text: "loggedIn:true — the user completed login. Reload the page and continue the task." }] };
1711
+ }
1712
+ // Re-send a fresh capture once past the halfway mark (the first QR may expire).
1713
+ if (!resent && Date.now() > deadline - timeoutMs / 2) {
1714
+ resent = true;
1715
+ await sendShot("⏳ 这是最新的二维码,请尽快扫码登录。").catch(() => { });
1716
+ }
1717
+ }
1718
+ await sendEncryptedChatContent({ account, groupId, content: { text: "⌛ 登录等待超时,已取消。如需继续请重新发起任务并尽快扫码。" } }).catch(() => { });
1719
+ cdp.close();
1720
+ return { content: [{ type: "text", text: "loggedIn:false — login timed out; the user did not sign in. Do not retry the gated page; tell the user it requires login." }] };
1721
+ }
1722
+ catch (err) {
1723
+ cdp.close();
1724
+ const msg = err instanceof Error ? err.message : String(err);
1725
+ return { content: [{ type: "text", text: `request_login error: ${msg}` }] };
1726
+ }
1727
+ },
1728
+ });
1626
1729
  // Wrap non-read-only tools with progress tracking.
1627
1730
  // When the tool executes, it looks up the current active dispatch for this account.
1628
1731
  const accountId = account.accountId;