agentlinkops 0.5.0 → 0.5.1
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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.mcp.json +8 -0
- package/CHANGELOG.md +15 -1
- package/README.md +2 -1
- package/cli/citation.js +19 -1
- package/cli/client.js +1 -1
- package/cli/doctor.js +19 -0
- package/package.json +9 -1
- package/src/citations/browser/engine.js +196 -0
- package/src/citations/browser/gen/core/providers/_shared/google.js +4 -0
- package/src/citations/browser/gen/core/providers/_shared/rawSourcesDom.js +15 -0
- package/src/citations/browser/gen/core/providers/_shared/resetProviderPage.js +22 -0
- package/src/citations/browser/gen/core/providers/_shared/sourceUtils.js +8 -0
- package/src/citations/browser/gen/core/providers/ai-overview/index.js +70 -0
- package/src/citations/browser/gen/core/providers/ai-overview/lib/expand.js +59 -0
- package/src/citations/browser/gen/core/providers/ai-overview/lib/extractSources.js +60 -0
- package/src/citations/browser/gen/core/providers/ai-overview/lib/pageLifecycle.js +18 -0
- package/src/citations/browser/gen/core/providers/ai-overview/lib/session.js +62 -0
- package/src/citations/browser/gen/core/providers/chatgpt/index.js +28 -0
- package/src/citations/browser/gen/core/providers/chatgpt/lib/dismissAuthModal.js +39 -0
- package/src/citations/browser/gen/core/providers/chatgpt/lib/extractSources.js +66 -0
- package/src/citations/browser/gen/core/providers/chatgpt/lib/pageLifecycle.js +11 -0
- package/src/citations/browser/gen/core/providers/claude/index.js +16 -0
- package/src/citations/browser/gen/core/providers/claude/lib/extractSources.js +54 -0
- package/src/citations/browser/gen/core/providers/claude/lib/pageLifecycle.js +9 -0
- package/src/citations/browser/gen/core/providers/gemini/index.js +40 -0
- package/src/citations/browser/gen/core/providers/gemini/lib/extractSources.js +65 -0
- package/src/citations/browser/gen/core/providers/gemini/lib/pageLifecycle.js +14 -0
- package/src/citations/browser/gen/core/providers/gemini/lib/session.js +45 -0
- package/src/citations/browser/gen/core/providers/index.js +15 -0
- package/src/citations/browser/gen/core/providers/perplexity/index.js +31 -0
- package/src/citations/browser/gen/core/providers/perplexity/lib/extractSources.js +84 -0
- package/src/citations/browser/gen/core/providers/perplexity/lib/navigation.js +38 -0
- package/src/citations/browser/gen/core/providers/types.js +0 -0
- package/src/citations/browser/gen/lib/browser/camoufox.js +587 -0
- package/src/citations/browser/gen/lib/browser/display.js +147 -0
- package/src/citations/browser/gen/lib/browser/domOps.js +339 -0
- package/src/citations/browser/gen/lib/browser/humanBehavior.js +121 -0
- package/src/citations/browser/gen/lib/browser/navigate.js +46 -0
- package/src/citations/browser/gen/lib/browser/playwrightCompat.js +321 -0
- package/src/citations/browser/gen/lib/browser/proxy/forwarder.js +251 -0
- package/src/citations/browser/gen/lib/browser/runtimeTypes.js +0 -0
- package/src/citations/browser/gen/lib/extraction/sourceUtils.js +71 -0
- package/src/citations/browser/gen/lib/input/editor/assertNotBlocked.js +54 -0
- package/src/citations/browser/gen/lib/input/editor/clearInput.js +52 -0
- package/src/citations/browser/gen/lib/input/editor/findEditor.js +54 -0
- package/src/citations/browser/gen/lib/input/editor/findSendButton.js +21 -0
- package/src/citations/browser/gen/lib/input/editor/promptInput.js +117 -0
- package/src/citations/browser/gen/lib/input/markdown/converter.js +101 -0
- package/src/citations/browser/gen/lib/input/markdown/toMarkdown.js +14 -0
- package/src/citations/browser/gen/lib/input/response/detectBotPage.js +13 -0
- package/src/citations/browser/gen/lib/input/response/getText.js +10 -0
- package/src/citations/browser/gen/lib/input/response/isGenerating.js +59 -0
- package/src/citations/browser/gen/lib/input/response/waitForFinish.js +105 -0
- package/src/citations/browser/gen/lib/input/sources/findButton.js +41 -0
- package/src/citations/browser/gen/lib/input/sources/openPanel.js +11 -0
- package/src/citations/browser/gen/utils/agent-constants.js +87 -0
- package/src/citations/browser/shims/errors.js +26 -0
- package/src/citations/browser/shims/types.js +6 -0
- package/src/citations/browser/shims/utils.js +32 -0
- package/src/citations/contract.js +9 -2
- package/src/citations/runner.js +21 -5
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { access } from "node:fs/promises";
|
|
4
|
+
import { release } from "node:os";
|
|
5
|
+
const XVFB_CANDIDATES = ["/usr/bin/Xvfb", "/usr/local/bin/Xvfb"];
|
|
6
|
+
const XVFB_START_TIMEOUT_MS = 5e3;
|
|
7
|
+
const DEFAULT_SCREEN_WIDTH = 1920;
|
|
8
|
+
const DEFAULT_SCREEN_HEIGHT = 1080;
|
|
9
|
+
const DEFAULT_SCREEN_DEPTH = 24;
|
|
10
|
+
const DEFAULT_CAMOUFOX_HEADLESS_MODE = "virtual";
|
|
11
|
+
function findXvfbBinary() {
|
|
12
|
+
for (const candidate of XVFB_CANDIDATES) {
|
|
13
|
+
if (existsSync(candidate)) return candidate;
|
|
14
|
+
}
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
function detectDisplay() {
|
|
18
|
+
return process.env.DISPLAY?.trim() || null;
|
|
19
|
+
}
|
|
20
|
+
function isWsl() {
|
|
21
|
+
if (process.platform !== "linux") return false;
|
|
22
|
+
const kernelRelease = release().toLowerCase();
|
|
23
|
+
return kernelRelease.includes("microsoft") || kernelRelease.includes("wsl");
|
|
24
|
+
}
|
|
25
|
+
function parseScreenSpec() {
|
|
26
|
+
const raw = process.env.CAMOUFOX_XVFB_SCREEN?.trim();
|
|
27
|
+
if (!raw) {
|
|
28
|
+
return {
|
|
29
|
+
width: DEFAULT_SCREEN_WIDTH,
|
|
30
|
+
height: DEFAULT_SCREEN_HEIGHT,
|
|
31
|
+
depth: DEFAULT_SCREEN_DEPTH
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const match = raw.match(/^(\d+)x(\d+)x(\d+)$/i);
|
|
35
|
+
if (!match) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
"CAMOUFOX_XVFB_SCREEN must use WIDTHxHEIGHTxDEPTH, for example 1920x1080x24."
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
width: Number(match[1]),
|
|
42
|
+
height: Number(match[2]),
|
|
43
|
+
depth: Number(match[3])
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
async function waitForDisplaySocket(displayNumber, child, timeoutMs = XVFB_START_TIMEOUT_MS) {
|
|
47
|
+
const socketPath = `/tmp/.X11-unix/X${displayNumber}`;
|
|
48
|
+
const deadline = Date.now() + timeoutMs;
|
|
49
|
+
while (Date.now() < deadline) {
|
|
50
|
+
if (child.exitCode !== null) {
|
|
51
|
+
throw new Error(`Xvfb exited before display :${displayNumber} was ready`);
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
await access(socketPath);
|
|
55
|
+
return;
|
|
56
|
+
} catch {
|
|
57
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
child.kill("SIGTERM");
|
|
61
|
+
throw new Error(
|
|
62
|
+
`Xvfb display :${displayNumber} was not ready within ${timeoutMs}ms`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
async function ensureDisplay(options) {
|
|
66
|
+
if ((process.env.CAMOUFOX_HEADLESS_MODE ?? DEFAULT_CAMOUFOX_HEADLESS_MODE) === "headless") {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
const existingDisplay = detectDisplay();
|
|
70
|
+
if ((options?.allowExistingDisplay ?? true) && existingDisplay) {
|
|
71
|
+
return {
|
|
72
|
+
display: existingDisplay,
|
|
73
|
+
cleanup: async () => {
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (process.platform !== "linux") {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
if ((process.env.CAMOUFOX_HEADLESS_MODE ?? DEFAULT_CAMOUFOX_HEADLESS_MODE) === "headful") {
|
|
81
|
+
throw new Error(
|
|
82
|
+
"CAMOUFOX_HEADLESS_MODE=headful requires DISPLAY to already be set."
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
const xvfbBinary = findXvfbBinary();
|
|
86
|
+
if (!xvfbBinary) {
|
|
87
|
+
throw new Error("No DISPLAY or Xvfb available for headful Camoufox.");
|
|
88
|
+
}
|
|
89
|
+
let lastError = null;
|
|
90
|
+
const preferredDisplay = process.env.CAMOUFOX_XVFB_DISPLAY?.trim() || null;
|
|
91
|
+
const screen = parseScreenSpec();
|
|
92
|
+
for (let attempt = 0; attempt < 5; attempt += 1) {
|
|
93
|
+
const display = preferredDisplay ?? `:${100 + Math.floor(Math.random() * 800)}`;
|
|
94
|
+
const displayNumber = Number(display.replace(/^:/, ""));
|
|
95
|
+
if (!Number.isInteger(displayNumber)) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
`Invalid CAMOUFOX_XVFB_DISPLAY value "${display}". Expected format like :99.`
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
const xvfb = spawn(
|
|
101
|
+
xvfbBinary,
|
|
102
|
+
[
|
|
103
|
+
display,
|
|
104
|
+
"-screen",
|
|
105
|
+
"0",
|
|
106
|
+
`${screen.width}x${screen.height}x${screen.depth}`,
|
|
107
|
+
"-ac",
|
|
108
|
+
"-nolisten",
|
|
109
|
+
"tcp"
|
|
110
|
+
],
|
|
111
|
+
{ stdio: "ignore" }
|
|
112
|
+
);
|
|
113
|
+
try {
|
|
114
|
+
await waitForDisplaySocket(displayNumber, xvfb);
|
|
115
|
+
return {
|
|
116
|
+
display,
|
|
117
|
+
cleanup: async () => {
|
|
118
|
+
try {
|
|
119
|
+
xvfb.kill("SIGTERM");
|
|
120
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
121
|
+
if (xvfb.exitCode === null) {
|
|
122
|
+
xvfb.kill("SIGKILL");
|
|
123
|
+
}
|
|
124
|
+
} catch {
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
} catch (error) {
|
|
129
|
+
lastError = error;
|
|
130
|
+
try {
|
|
131
|
+
xvfb.kill("SIGTERM");
|
|
132
|
+
} catch {
|
|
133
|
+
}
|
|
134
|
+
if (preferredDisplay) {
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
throw new Error(
|
|
140
|
+
`Failed to bootstrap Xvfb: ${lastError instanceof Error ? lastError.message : "unknown error"}`
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
export {
|
|
144
|
+
detectDisplay,
|
|
145
|
+
ensureDisplay,
|
|
146
|
+
isWsl
|
|
147
|
+
};
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import { PROVIDER_RAW_SOURCES_DOM_EXTRACTORS } from "../../core/providers/_shared/rawSourcesDom.js";
|
|
2
|
+
async function runPageDomOp(page, operation, params) {
|
|
3
|
+
const nextParams = { ...params ?? {} };
|
|
4
|
+
if (operation === "raw-sources") {
|
|
5
|
+
const provider = String(nextParams.provider || "");
|
|
6
|
+
nextParams.providerRawSourcesExtractor = PROVIDER_RAW_SOURCES_DOM_EXTRACTORS[provider] ?? "";
|
|
7
|
+
}
|
|
8
|
+
return await page.evaluate(
|
|
9
|
+
({ operation: currentOperation, params: currentParams }) => {
|
|
10
|
+
function splitTopLevelSelectors(selector) {
|
|
11
|
+
const parts = [];
|
|
12
|
+
let current = "";
|
|
13
|
+
let parenDepth = 0;
|
|
14
|
+
let bracketDepth = 0;
|
|
15
|
+
let quote = null;
|
|
16
|
+
for (const char of selector) {
|
|
17
|
+
if (quote) {
|
|
18
|
+
current += char;
|
|
19
|
+
if (char === quote) {
|
|
20
|
+
quote = null;
|
|
21
|
+
}
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (char === "'" || char === '"') {
|
|
25
|
+
quote = char;
|
|
26
|
+
current += char;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (char === "(") parenDepth += 1;
|
|
30
|
+
if (char === ")") parenDepth = Math.max(0, parenDepth - 1);
|
|
31
|
+
if (char === "[") bracketDepth += 1;
|
|
32
|
+
if (char === "]") bracketDepth = Math.max(0, bracketDepth - 1);
|
|
33
|
+
if (char === "," && parenDepth === 0 && bracketDepth === 0) {
|
|
34
|
+
if (current.trim()) parts.push(current.trim());
|
|
35
|
+
current = "";
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
current += char;
|
|
39
|
+
}
|
|
40
|
+
if (current.trim()) parts.push(current.trim());
|
|
41
|
+
return parts;
|
|
42
|
+
}
|
|
43
|
+
function parseHasTextSelector(selector) {
|
|
44
|
+
const textFilters = [];
|
|
45
|
+
let baseSelector = selector;
|
|
46
|
+
const regex = /:has-text\((["'])(.*?)\1\)/g;
|
|
47
|
+
baseSelector = baseSelector.replace(
|
|
48
|
+
regex,
|
|
49
|
+
(_full, _quote, value) => {
|
|
50
|
+
textFilters.push(value);
|
|
51
|
+
return "";
|
|
52
|
+
}
|
|
53
|
+
);
|
|
54
|
+
baseSelector = baseSelector.trim() || "*";
|
|
55
|
+
return { baseSelector, textFilters };
|
|
56
|
+
}
|
|
57
|
+
function elementText(element) {
|
|
58
|
+
if (element instanceof HTMLElement) {
|
|
59
|
+
return (element.innerText || element.textContent || "").trim();
|
|
60
|
+
}
|
|
61
|
+
return (element.textContent || "").trim();
|
|
62
|
+
}
|
|
63
|
+
function isVisible(element) {
|
|
64
|
+
if (!(element instanceof HTMLElement)) return false;
|
|
65
|
+
if (!element.isConnected) return false;
|
|
66
|
+
const style = window.getComputedStyle(element);
|
|
67
|
+
if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
const rect = element.getBoundingClientRect();
|
|
71
|
+
return rect.width > 0 && rect.height > 0;
|
|
72
|
+
}
|
|
73
|
+
function dedupeElements(elements) {
|
|
74
|
+
return Array.from(new Set(elements));
|
|
75
|
+
}
|
|
76
|
+
function resolveSelectorWithin(root, selector) {
|
|
77
|
+
const elements = [];
|
|
78
|
+
for (const part of splitTopLevelSelectors(selector)) {
|
|
79
|
+
const { baseSelector, textFilters } = parseHasTextSelector(part);
|
|
80
|
+
const matches = Array.from(
|
|
81
|
+
root.querySelectorAll(baseSelector)
|
|
82
|
+
).filter(
|
|
83
|
+
(el) => textFilters.every(
|
|
84
|
+
(filter) => elementText(el).toLowerCase().includes(filter.toLowerCase())
|
|
85
|
+
)
|
|
86
|
+
);
|
|
87
|
+
elements.push(...matches);
|
|
88
|
+
}
|
|
89
|
+
return dedupeElements(elements);
|
|
90
|
+
}
|
|
91
|
+
function isResponsePlaceholder(element) {
|
|
92
|
+
return element.getAttribute("aria-busy") === "true" || (element.getAttribute("data-message-id") || "").startsWith(
|
|
93
|
+
"request-placeholder"
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
function findLatestResponseElement(selectors) {
|
|
97
|
+
const selector = (selectors || []).join(", ");
|
|
98
|
+
if (!selector.trim()) return null;
|
|
99
|
+
const element = Array.from(document.querySelectorAll(selector)).filter(
|
|
100
|
+
(el) => el instanceof HTMLElement && isVisible(el) && !isResponsePlaceholder(el) && el.innerText.trim().length > 50
|
|
101
|
+
).pop() ?? null;
|
|
102
|
+
return element ? { selector, element } : null;
|
|
103
|
+
}
|
|
104
|
+
function getCachedRawSources(key) {
|
|
105
|
+
const cache = window.__oneglanseRawSourcesCache;
|
|
106
|
+
return cache?.[key] ?? null;
|
|
107
|
+
}
|
|
108
|
+
function setCachedRawSources(key, rawSources) {
|
|
109
|
+
const state = window;
|
|
110
|
+
state.__oneglanseRawSourcesCache ||= {};
|
|
111
|
+
state.__oneglanseRawSourcesCache[key] = rawSources;
|
|
112
|
+
}
|
|
113
|
+
function extractClaudeRawSourcesFromResponseElement(responseEl) {
|
|
114
|
+
const normalize = (text) => text.replace(/\s+/g, " ").trim();
|
|
115
|
+
const getTextBeforeAnchor = (anchor) => {
|
|
116
|
+
let text = "";
|
|
117
|
+
let node = anchor;
|
|
118
|
+
while (node) {
|
|
119
|
+
if (node.previousSibling) {
|
|
120
|
+
node = node.previousSibling;
|
|
121
|
+
while (node && node.lastChild) {
|
|
122
|
+
node = node.lastChild;
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
node = node.parentNode;
|
|
126
|
+
}
|
|
127
|
+
if (!node) break;
|
|
128
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
129
|
+
const content = node.textContent || "";
|
|
130
|
+
text = `${content} ${text}`;
|
|
131
|
+
if (/[.!?]\s*$/.test(content)) break;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return normalize(text);
|
|
135
|
+
};
|
|
136
|
+
return Array.from(responseEl.querySelectorAll('a[href^="http"]')).map((anchor) => {
|
|
137
|
+
const link = anchor;
|
|
138
|
+
const anchorElement = anchor instanceof HTMLElement ? anchor : null;
|
|
139
|
+
return {
|
|
140
|
+
rawHref: link.href,
|
|
141
|
+
title: (anchor.textContent || "").trim() || link.href,
|
|
142
|
+
citedText: anchorElement ? getTextBeforeAnchor(anchorElement) : ""
|
|
143
|
+
};
|
|
144
|
+
}).filter((source) => source.rawHref);
|
|
145
|
+
}
|
|
146
|
+
function runProviderRawSourcesExtractor(extractorSource, selectors) {
|
|
147
|
+
if (!extractorSource.trim()) return [];
|
|
148
|
+
const extractor = Function(
|
|
149
|
+
`return (${extractorSource});`
|
|
150
|
+
)();
|
|
151
|
+
return extractor(
|
|
152
|
+
{
|
|
153
|
+
getCachedRawSources,
|
|
154
|
+
setCachedRawSources,
|
|
155
|
+
findLatestResponseElement,
|
|
156
|
+
extractClaudeRawSourcesFromResponseElement
|
|
157
|
+
},
|
|
158
|
+
selectors
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
function readResponseText(_provider, selectors) {
|
|
162
|
+
return findLatestResponseElement(selectors)?.element.innerText.trim() || "";
|
|
163
|
+
}
|
|
164
|
+
function isCitationAnchor(anchor) {
|
|
165
|
+
const text = anchor.textContent?.trim() || "";
|
|
166
|
+
if (!text || text.length > 40) return false;
|
|
167
|
+
if (/^\+?\d+$/.test(text)) return true;
|
|
168
|
+
if (/^[a-z0-9.\-+]+$/i.test(text) && text.includes(".") && text.length < 40) return true;
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
function formatCitationAnchors(root) {
|
|
172
|
+
const cleanCitationText = (text) => text.replace(/\+\d+$/, "").trim();
|
|
173
|
+
for (const anchor of Array.from(root.querySelectorAll("a[href]"))) {
|
|
174
|
+
if (!(anchor instanceof HTMLAnchorElement)) continue;
|
|
175
|
+
if (!isCitationAnchor(anchor)) continue;
|
|
176
|
+
const rawText = anchor.textContent?.trim();
|
|
177
|
+
if (!rawText) continue;
|
|
178
|
+
const cleaned = cleanCitationText(rawText);
|
|
179
|
+
if (!cleaned) {
|
|
180
|
+
anchor.remove();
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
const strong = document.createElement("strong");
|
|
184
|
+
strong.textContent = `[${cleaned}]`;
|
|
185
|
+
anchor.replaceWith(
|
|
186
|
+
document.createTextNode(" "),
|
|
187
|
+
strong,
|
|
188
|
+
document.createTextNode(" ")
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function readResponseHtml(provider, selectors) {
|
|
193
|
+
if (provider === "ai-overview") {
|
|
194
|
+
const root = document.querySelector('[data-container-id="main-col"]');
|
|
195
|
+
if (!root) return "";
|
|
196
|
+
const clone2 = root.cloneNode(true);
|
|
197
|
+
formatCitationAnchors(clone2);
|
|
198
|
+
clone2.querySelectorAll(
|
|
199
|
+
'[data-src-id], button, [role="button"], svg, img, style, script'
|
|
200
|
+
).forEach((el) => el.remove());
|
|
201
|
+
return clone2.innerHTML.trim();
|
|
202
|
+
}
|
|
203
|
+
const latestResponse = findLatestResponseElement(selectors);
|
|
204
|
+
if (!latestResponse) return "";
|
|
205
|
+
if (provider === "claude") {
|
|
206
|
+
setCachedRawSources(
|
|
207
|
+
"claude",
|
|
208
|
+
extractClaudeRawSourcesFromResponseElement(latestResponse.element)
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
const clone = latestResponse.element.cloneNode(true);
|
|
212
|
+
formatCitationAnchors(clone);
|
|
213
|
+
const noiseSelectors = [
|
|
214
|
+
"button",
|
|
215
|
+
"[role='button']",
|
|
216
|
+
"svg",
|
|
217
|
+
"script",
|
|
218
|
+
"style",
|
|
219
|
+
"noscript",
|
|
220
|
+
"iframe",
|
|
221
|
+
"sup",
|
|
222
|
+
"[aria-live]",
|
|
223
|
+
"[data-testid='copy-turn-action-button']",
|
|
224
|
+
"[data-testid='voice-play-turn-action-button']",
|
|
225
|
+
"[data-testid='thumbs-up-button']",
|
|
226
|
+
"[data-testid='thumbs-down-button']",
|
|
227
|
+
"[aria-hidden='true']"
|
|
228
|
+
];
|
|
229
|
+
for (const sel of noiseSelectors) {
|
|
230
|
+
for (const el of Array.from(clone.querySelectorAll(sel))) {
|
|
231
|
+
el.remove();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return clone.innerHTML.trim();
|
|
235
|
+
}
|
|
236
|
+
function captureVisibleHtml(selectors, fallbackSelectors) {
|
|
237
|
+
const latestResponse = findLatestResponseElement(selectors);
|
|
238
|
+
if (latestResponse) {
|
|
239
|
+
const html = latestResponse.element.outerHTML.trim();
|
|
240
|
+
if (html) return { selector: latestResponse.selector, html };
|
|
241
|
+
}
|
|
242
|
+
for (const selector of fallbackSelectors || []) {
|
|
243
|
+
const element = resolveSelectorWithin(document, selector)[0] ?? null;
|
|
244
|
+
if (!isVisible(element)) continue;
|
|
245
|
+
const html = element.outerHTML.trim();
|
|
246
|
+
if (html) return { selector, html };
|
|
247
|
+
}
|
|
248
|
+
return { selector: "none", html: "" };
|
|
249
|
+
}
|
|
250
|
+
function detectBotPageState() {
|
|
251
|
+
const bodyText = (document.body?.innerText || "").replace(/\s+/g, " ").trim();
|
|
252
|
+
const title = (document.title || "").trim();
|
|
253
|
+
const url = window.location.href;
|
|
254
|
+
const signals = [
|
|
255
|
+
{
|
|
256
|
+
matched: /sorry/i.test(url) || /our systems have detected unusual traffic/i.test(bodyText),
|
|
257
|
+
reason: "bot detection: unusual traffic / sorry page"
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
matched: /captcha|recaptcha|turnstile|verify you are human/i.test(
|
|
261
|
+
bodyText
|
|
262
|
+
),
|
|
263
|
+
reason: "bot detection: captcha or human verification challenge"
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
matched: Boolean(
|
|
267
|
+
document.querySelector(
|
|
268
|
+
'form#captcha-form, iframe[src*="recaptcha"]'
|
|
269
|
+
)
|
|
270
|
+
) || /challenge/i.test(title),
|
|
271
|
+
reason: "bot detection: challenge UI present"
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
matched: /\/login|\/log-in|\/signin|\/sign-in|\/sign-up|\/signup|\/auth(?:\/|$)|accounts\.google\.com|auth\.openai\.com/.test(
|
|
275
|
+
url
|
|
276
|
+
),
|
|
277
|
+
reason: "session expired: redirected to login page"
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
matched: /sign in to continue|you('ve| have) been signed out|create a free account|log in to continue|sign in to (?:chat|use|access)|please (?:sign|log) in/i.test(
|
|
281
|
+
bodyText
|
|
282
|
+
),
|
|
283
|
+
reason: "session expired: login wall detected"
|
|
284
|
+
}
|
|
285
|
+
];
|
|
286
|
+
const hit = signals.find((signal) => signal.matched);
|
|
287
|
+
return {
|
|
288
|
+
botDetected: Boolean(hit),
|
|
289
|
+
reason: hit?.reason ?? null
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
function getPlatformName() {
|
|
293
|
+
const uaDataPlatform = navigator.userAgentData?.platform || "";
|
|
294
|
+
return String(uaDataPlatform || navigator.platform || "").toLowerCase();
|
|
295
|
+
}
|
|
296
|
+
switch (currentOperation) {
|
|
297
|
+
case "ping":
|
|
298
|
+
return true;
|
|
299
|
+
case "window-metrics":
|
|
300
|
+
return {
|
|
301
|
+
outerHeight: window.outerHeight,
|
|
302
|
+
innerHeight: window.innerHeight,
|
|
303
|
+
outerWidth: window.outerWidth,
|
|
304
|
+
innerWidth: window.innerWidth
|
|
305
|
+
};
|
|
306
|
+
case "detect-bot-page":
|
|
307
|
+
return detectBotPageState();
|
|
308
|
+
case "platform-name":
|
|
309
|
+
return getPlatformName();
|
|
310
|
+
case "response-text":
|
|
311
|
+
return readResponseText(
|
|
312
|
+
String(currentParams?.provider || ""),
|
|
313
|
+
currentParams?.selectors || []
|
|
314
|
+
);
|
|
315
|
+
case "response-html":
|
|
316
|
+
return readResponseHtml(
|
|
317
|
+
String(currentParams?.provider || ""),
|
|
318
|
+
currentParams?.selectors || []
|
|
319
|
+
);
|
|
320
|
+
case "capture-visible-html":
|
|
321
|
+
return captureVisibleHtml(
|
|
322
|
+
currentParams?.selectors || [],
|
|
323
|
+
currentParams?.fallbackSelectors || []
|
|
324
|
+
);
|
|
325
|
+
case "raw-sources":
|
|
326
|
+
return runProviderRawSourcesExtractor(
|
|
327
|
+
String(currentParams?.providerRawSourcesExtractor || ""),
|
|
328
|
+
currentParams?.selectors || []
|
|
329
|
+
);
|
|
330
|
+
default:
|
|
331
|
+
throw new Error(`unknown page operation: ${currentOperation}`);
|
|
332
|
+
}
|
|
333
|
+
},
|
|
334
|
+
{ operation, params: nextParams }
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
export {
|
|
338
|
+
runPageDomOp
|
|
339
|
+
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
const CAMOUFOX_HUMANIZE = true;
|
|
2
|
+
function canUseOsLevelInput(_page) {
|
|
3
|
+
return false;
|
|
4
|
+
}
|
|
5
|
+
async function clickLocatorLikeUser(_page, target, options) {
|
|
6
|
+
await target.click({
|
|
7
|
+
timeout: options?.timeout,
|
|
8
|
+
delay: options?.delay,
|
|
9
|
+
force: options?.force
|
|
10
|
+
});
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
async function pressKeyLikeUser(page, key, options) {
|
|
14
|
+
await page.keyboard.press(key, { delay: options?.delay });
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
async function getBrowserPrimaryModifier(page) {
|
|
18
|
+
try {
|
|
19
|
+
const platform = await page.runDomOp("platform-name");
|
|
20
|
+
return platform.includes("mac") ? "Meta" : "Control";
|
|
21
|
+
} catch {
|
|
22
|
+
return "Control";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function randomBetween(min, max) {
|
|
26
|
+
return min + Math.floor(Math.random() * (max - min + 1));
|
|
27
|
+
}
|
|
28
|
+
const graphemeSegmenter = typeof Intl !== "undefined" && "Segmenter" in Intl ? new Intl.Segmenter(void 0, { granularity: "grapheme" }) : null;
|
|
29
|
+
function bezierPoint(t, p0, p1, p2, p3) {
|
|
30
|
+
const u = 1 - t;
|
|
31
|
+
return u * u * u * p0 + 3 * u * u * t * p1 + 3 * u * t * t * p2 + t * t * t * p3;
|
|
32
|
+
}
|
|
33
|
+
async function moveMouseToElement(page, target) {
|
|
34
|
+
if (CAMOUFOX_HUMANIZE) return;
|
|
35
|
+
const box = await target.boundingBox().catch(() => null);
|
|
36
|
+
if (!box) return;
|
|
37
|
+
const viewport = page.viewportSize() ?? { width: 1920, height: 1080 };
|
|
38
|
+
const startX = randomBetween(viewport.width * 0.1, viewport.width * 0.9);
|
|
39
|
+
const startY = randomBetween(viewport.height * 0.1, viewport.height * 0.9);
|
|
40
|
+
const endX = box.x + box.width * (0.3 + Math.random() * 0.4);
|
|
41
|
+
const endY = box.y + box.height * (0.3 + Math.random() * 0.4);
|
|
42
|
+
const cp1x = startX + (endX - startX) * (0.2 + Math.random() * 0.3);
|
|
43
|
+
const cp1y = startY + (Math.random() - 0.5) * 100;
|
|
44
|
+
const cp2x = endX - (endX - startX) * (0.2 + Math.random() * 0.3);
|
|
45
|
+
const cp2y = endY + (Math.random() - 0.5) * 100;
|
|
46
|
+
const steps = randomBetween(6, 12);
|
|
47
|
+
for (let i = 0; i <= steps; i++) {
|
|
48
|
+
const t = i / steps;
|
|
49
|
+
const x = bezierPoint(t, startX, cp1x, cp2x, endX);
|
|
50
|
+
const y = bezierPoint(t, startY, cp1y, cp2y, endY);
|
|
51
|
+
await page.mouse.move(x, y);
|
|
52
|
+
await page.waitForTimeout(randomBetween(3, 12));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async function preInteractionIdle(page) {
|
|
56
|
+
await page.waitForTimeout(
|
|
57
|
+
CAMOUFOX_HUMANIZE ? randomBetween(80, 180) : randomBetween(300, 700)
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
async function smallScroll(page) {
|
|
61
|
+
if (CAMOUFOX_HUMANIZE) return;
|
|
62
|
+
const amount = randomBetween(50, 200);
|
|
63
|
+
await page.mouse.wheel(0, amount);
|
|
64
|
+
await page.waitForTimeout(randomBetween(200, 600));
|
|
65
|
+
}
|
|
66
|
+
async function typeTextWithCadence(page, text) {
|
|
67
|
+
const units = graphemeSegmenter ? Array.from(graphemeSegmenter.segment(text), (segment) => segment.segment) : Array.from(text);
|
|
68
|
+
for (const unit of units) {
|
|
69
|
+
await page.keyboard.type(unit);
|
|
70
|
+
await page.waitForTimeout(randomBetween(12, 28));
|
|
71
|
+
}
|
|
72
|
+
return units.length;
|
|
73
|
+
}
|
|
74
|
+
async function pastePrompt(page, text) {
|
|
75
|
+
const segments = graphemeSegmenter ? Array.from(graphemeSegmenter.segment(text), (segment) => segment.segment) : Array.from(text);
|
|
76
|
+
let cursor = 0;
|
|
77
|
+
let charsSinceLongPause = 0;
|
|
78
|
+
while (cursor < segments.length) {
|
|
79
|
+
const nextChunkSize = randomBetween(3, 8);
|
|
80
|
+
const chunk = segments.slice(cursor, cursor + nextChunkSize).join("");
|
|
81
|
+
cursor += nextChunkSize;
|
|
82
|
+
if (chunk === "\n") {
|
|
83
|
+
await page.keyboard.press("Shift+Enter");
|
|
84
|
+
charsSinceLongPause = 0;
|
|
85
|
+
await page.waitForTimeout(randomBetween(60, 140));
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (chunk.includes("\n")) {
|
|
89
|
+
const parts = chunk.split("\n");
|
|
90
|
+
for (let i = 0; i < parts.length; i++) {
|
|
91
|
+
const part = parts[i];
|
|
92
|
+
if (part) {
|
|
93
|
+
charsSinceLongPause += await typeTextWithCadence(page, part);
|
|
94
|
+
}
|
|
95
|
+
if (i < parts.length - 1) {
|
|
96
|
+
await page.keyboard.press("Shift+Enter");
|
|
97
|
+
charsSinceLongPause = 0;
|
|
98
|
+
await page.waitForTimeout(randomBetween(70, 160));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
charsSinceLongPause += await typeTextWithCadence(page, chunk);
|
|
103
|
+
}
|
|
104
|
+
await page.waitForTimeout(randomBetween(35, 110));
|
|
105
|
+
if (charsSinceLongPause >= randomBetween(22, 40)) {
|
|
106
|
+
charsSinceLongPause = 0;
|
|
107
|
+
await page.waitForTimeout(randomBetween(120, 260));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
export {
|
|
112
|
+
canUseOsLevelInput,
|
|
113
|
+
clickLocatorLikeUser,
|
|
114
|
+
getBrowserPrimaryModifier,
|
|
115
|
+
moveMouseToElement,
|
|
116
|
+
pastePrompt,
|
|
117
|
+
preInteractionIdle,
|
|
118
|
+
pressKeyLikeUser,
|
|
119
|
+
randomBetween,
|
|
120
|
+
smallScroll
|
|
121
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { ExternalServiceError, toErrorMessage } from "../../../shims/errors.js";
|
|
2
|
+
import { RETRYABLE_ERRORS, logger } from "../../../shims/utils.js";
|
|
3
|
+
function jitter(baseMs, factor = 0.3) {
|
|
4
|
+
const delta = Math.round(baseMs * factor);
|
|
5
|
+
const min = Math.max(0, baseMs - delta);
|
|
6
|
+
const max = baseMs + delta;
|
|
7
|
+
return Math.round(min + Math.random() * (max - min));
|
|
8
|
+
}
|
|
9
|
+
async function navigateWithRetry(page, url, options = {}, maxRetries = 3, delayMs = 2e3) {
|
|
10
|
+
let referer = options?.referer;
|
|
11
|
+
if (referer === void 0) {
|
|
12
|
+
try {
|
|
13
|
+
const currentUrl = page.url();
|
|
14
|
+
if (currentUrl && currentUrl !== "about:blank") {
|
|
15
|
+
referer = currentUrl;
|
|
16
|
+
}
|
|
17
|
+
} catch {
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const gotoOptions = referer !== void 0 ? { ...options, referer } : options;
|
|
21
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
22
|
+
try {
|
|
23
|
+
await page.goto(url, gotoOptions);
|
|
24
|
+
return;
|
|
25
|
+
} catch (err) {
|
|
26
|
+
const message = toErrorMessage(err);
|
|
27
|
+
const isRetryable = RETRYABLE_ERRORS.some((e) => message.includes(e));
|
|
28
|
+
if (!isRetryable || attempt === maxRetries) {
|
|
29
|
+
throw new ExternalServiceError(
|
|
30
|
+
"navigation",
|
|
31
|
+
toErrorMessage(err),
|
|
32
|
+
502,
|
|
33
|
+
{ url, attempt },
|
|
34
|
+
err
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
logger.warn(
|
|
38
|
+
`navigation failed (attempt ${attempt}/${maxRetries}): ${message} \u2014 retrying in ${Math.round(jitter(delayMs) / 100) / 10}s`
|
|
39
|
+
);
|
|
40
|
+
await page.waitForTimeout(jitter(delayMs));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export {
|
|
45
|
+
navigateWithRetry
|
|
46
|
+
};
|