agentlinkops 0.5.0 → 0.6.0
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/LICENSE +0 -0
- package/NOTICE +0 -0
- package/README.md +2 -1
- package/bin/agentlinkops.mjs +0 -0
- package/bin/linktrail.mjs +0 -0
- package/cli/adapters/csv.js +0 -0
- package/cli/adapters/sqlite-crm.js +0 -0
- package/cli/adapters/suppliers.js +0 -0
- package/cli/agent-setup.js +0 -0
- package/cli/agentlinkops.mjs +0 -0
- package/cli/args.js +0 -0
- package/cli/catalog.js +0 -0
- package/cli/check.js +0 -0
- package/cli/citation.js +124 -2
- package/cli/client.js +1 -1
- package/cli/commands.js +0 -0
- package/cli/config.js +0 -0
- package/cli/connection.js +0 -0
- package/cli/context-mcp.js +0 -0
- package/cli/context.js +0 -0
- package/cli/delivery.js +0 -0
- package/cli/doctor.js +19 -0
- package/cli/env.js +0 -0
- package/cli/fleet.js +0 -0
- package/cli/gsc-browser.js +0 -0
- package/cli/import.js +0 -0
- package/cli/ledger.js +0 -0
- package/cli/linktrail.mjs +0 -0
- package/cli/local-result.js +0 -0
- package/cli/main.js +0 -0
- package/cli/mirror.js +0 -0
- package/cli/mix.js +0 -0
- package/cli/platforms.js +0 -0
- package/cli/receipt-performance.js +0 -0
- package/cli/receipts.js +0 -0
- package/cli/report.js +0 -0
- package/cli/setup-plan.js +0 -0
- package/cli/skill.js +0 -0
- package/cli/state.js +0 -0
- package/cli/status.js +0 -0
- package/cli/sync.js +0 -0
- package/package.json +14 -3
- package/references/cli.md +0 -0
- package/references/disavow.md +0 -0
- package/references/reports.md +0 -0
- package/scripts/agentlinkops.py +0 -0
- package/scripts/disavow.py +0 -0
- package/scripts/linktrail.py +0 -0
- package/scripts/migrations/001_crm.sql +0 -0
- package/scripts/migrations/002_cloud_events.sql +0 -0
- package/scripts/migrations/003_disavow.sql +0 -0
- package/scripts/migrations/004_placement_costs.sql +0 -0
- package/scripts/report.py +0 -0
- package/shared/agent-clients.js +0 -0
- package/shared/observation-event.js +0 -0
- package/shared/response-contract.js +0 -0
- package/shared/search-connection-contract.js +0 -0
- package/skills/agentlinkops-assets/SKILL.md +0 -0
- package/skills/agentlinkops-assets/agents/openai.yaml +0 -0
- package/skills/agentlinkops-campaigns/SKILL.md +0 -0
- package/skills/agentlinkops-campaigns/agents/openai.yaml +0 -0
- package/skills/agentlinkops-connect/SKILL.md +0 -0
- package/skills/agentlinkops-connect/agents/openai.yaml +0 -0
- package/skills/agentlinkops-connect/references/agentlinkops.md +0 -0
- package/skills/agentlinkops-crm/SKILL.md +0 -0
- package/skills/agentlinkops-crm/agents/openai.yaml +0 -0
- package/skills/content-defingerprinting/SKILL.md +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/README.md +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/UPSTREAM.json +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/revendor.sh +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/vendor/LICENSE.upstream +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/vendor/clean_text.py +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/vendor/common.py +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/vendor/inspect_text.py +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/vendor/score_stylometry.py +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/vendor/text_unicode.py +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/verify-vendor.py +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/wm-scan.py +0 -0
- package/skills/internal-linking-optimizer/SKILL.md +0 -0
- package/skills/internal-linking-optimizer/references/source-prompt.md +0 -0
- package/skills/pre-publish-review/SKILL.md +0 -0
- package/skills/source-cited-content-builder/SKILL.md +0 -0
- package/skills/source-cited-content-builder/references/source-prompt.md +0 -0
- package/skills/source-cited-humanizer/SKILL.md +0 -0
- package/src/citations/adapter.js +173 -0
- package/src/citations/alert.js +53 -0
- package/src/citations/browser/engine.js +267 -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/providers.js +166 -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 +19 -3
- package/src/citations/fixtures/dataforseo-aio.json +82 -0
- package/src/citations/match.js +0 -0
- package/src/citations/panel-builder.js +44 -0
- package/src/citations/report.js +165 -0
- package/src/citations/runner.js +43 -16
- package/src/citations/stats.js +63 -0
- package/src/citations/watch.js +85 -0
- package/src/competitors/contract.js +0 -0
- package/src/competitors/domain-mix.js +0 -0
- package/src/context/ga4.js +0 -0
- package/src/context/gsc.js +0 -0
- package/src/context/site-profile.js +0 -0
- package/src/context/util.js +0 -0
- package/src/context-tool-registry.js +0 -0
- package/src/corpus/acquire.js +0 -0
- package/src/corpus/seeds.js +0 -0
- package/src/discovery/contract.js +0 -0
- package/src/discovery/import.js +0 -0
- package/src/tool-views.js +0 -0
- package/src/verifier/fetch.js +0 -0
- package/src/verifier/index.js +0 -0
- package/src/verifier/observation-kind.js +0 -0
- package/src/verifier/parser.js +0 -0
- package/src/verifier/readiness.js +0 -0
- package/src/verifier/render-evidence.js +0 -0
- package/src/verifier/robots.js +0 -0
- package/src/verifier/transition.js +0 -0
- package/src/verifier/url.js +0 -0
- package/templates/SITE.template.md +0 -0
- package/templates/campaigns.v1.json +0 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import { runPageDomOp } from "./domOps.js";
|
|
2
|
+
class PlaywrightWorkerCompat {
|
|
3
|
+
constructor(worker) {
|
|
4
|
+
this.worker = worker;
|
|
5
|
+
}
|
|
6
|
+
worker;
|
|
7
|
+
async evaluate(script) {
|
|
8
|
+
return await this.worker.evaluate(script);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
class PlaywrightLocatorCompat {
|
|
12
|
+
constructor(locator) {
|
|
13
|
+
this.locator = locator;
|
|
14
|
+
}
|
|
15
|
+
locator;
|
|
16
|
+
wrap(next) {
|
|
17
|
+
return new PlaywrightLocatorCompat(next);
|
|
18
|
+
}
|
|
19
|
+
count() {
|
|
20
|
+
return this.locator.count();
|
|
21
|
+
}
|
|
22
|
+
nth(index) {
|
|
23
|
+
return this.wrap(this.locator.nth(index));
|
|
24
|
+
}
|
|
25
|
+
first() {
|
|
26
|
+
return this.wrap(this.locator.first());
|
|
27
|
+
}
|
|
28
|
+
last() {
|
|
29
|
+
return this.wrap(this.locator.last());
|
|
30
|
+
}
|
|
31
|
+
filter(options) {
|
|
32
|
+
return this.wrap(this.locator.filter({ hasText: options.hasText }));
|
|
33
|
+
}
|
|
34
|
+
getByText(text) {
|
|
35
|
+
return this.wrap(this.locator.getByText(text));
|
|
36
|
+
}
|
|
37
|
+
isVisible(options) {
|
|
38
|
+
return this.locator.isVisible(options);
|
|
39
|
+
}
|
|
40
|
+
isEnabled() {
|
|
41
|
+
return this.locator.isEnabled();
|
|
42
|
+
}
|
|
43
|
+
focus() {
|
|
44
|
+
return this.locator.focus();
|
|
45
|
+
}
|
|
46
|
+
boundingBox() {
|
|
47
|
+
return this.locator.boundingBox();
|
|
48
|
+
}
|
|
49
|
+
scrollIntoViewIfNeeded() {
|
|
50
|
+
return this.locator.scrollIntoViewIfNeeded();
|
|
51
|
+
}
|
|
52
|
+
click(options) {
|
|
53
|
+
return this.locator.click(options);
|
|
54
|
+
}
|
|
55
|
+
press(key, options) {
|
|
56
|
+
return this.locator.press(key, options);
|
|
57
|
+
}
|
|
58
|
+
waitFor(options) {
|
|
59
|
+
return this.locator.waitFor({
|
|
60
|
+
timeout: options?.timeout,
|
|
61
|
+
state: options?.state
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
async readInputValue() {
|
|
65
|
+
return await this.locator.evaluate((element) => {
|
|
66
|
+
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
|
|
67
|
+
return element.value;
|
|
68
|
+
}
|
|
69
|
+
if (element instanceof HTMLElement) {
|
|
70
|
+
return element.innerText || element.textContent || "";
|
|
71
|
+
}
|
|
72
|
+
return "";
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
async setInputValue(value) {
|
|
76
|
+
await this.locator.evaluate((element, nextValue) => {
|
|
77
|
+
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
|
|
78
|
+
const proto = Object.getPrototypeOf(element);
|
|
79
|
+
const descriptor = Object.getOwnPropertyDescriptor(proto, "value");
|
|
80
|
+
if (descriptor?.set) {
|
|
81
|
+
descriptor.set.call(element, nextValue);
|
|
82
|
+
} else {
|
|
83
|
+
element.value = nextValue;
|
|
84
|
+
}
|
|
85
|
+
element.dispatchEvent(new Event("input", { bubbles: true }));
|
|
86
|
+
element.dispatchEvent(new Event("change", { bubbles: true }));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (element instanceof HTMLElement) {
|
|
90
|
+
element.focus();
|
|
91
|
+
element.innerText = nextValue;
|
|
92
|
+
element.dispatchEvent(new Event("input", { bubbles: true }));
|
|
93
|
+
element.dispatchEvent(new Event("change", { bubbles: true }));
|
|
94
|
+
}
|
|
95
|
+
}, value);
|
|
96
|
+
}
|
|
97
|
+
async getEditableState() {
|
|
98
|
+
return await this.locator.evaluate((element) => {
|
|
99
|
+
function hasHiddenAncestor(target) {
|
|
100
|
+
let current = target;
|
|
101
|
+
while (current) {
|
|
102
|
+
if (current.hidden || current.getAttribute("aria-hidden") === "true" || current.hasAttribute("inert")) {
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
const style = window.getComputedStyle(current);
|
|
106
|
+
if (style.display === "none" || style.visibility === "hidden" || style.visibility === "collapse" || style.opacity === "0" || style.pointerEvents === "none") {
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
current = current.parentElement;
|
|
110
|
+
}
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
function acceptsTextInput(target) {
|
|
114
|
+
if (target instanceof HTMLTextAreaElement) {
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
if (target instanceof HTMLInputElement) {
|
|
118
|
+
const blockedTypes = /* @__PURE__ */ new Set([
|
|
119
|
+
"hidden",
|
|
120
|
+
"button",
|
|
121
|
+
"checkbox",
|
|
122
|
+
"color",
|
|
123
|
+
"file",
|
|
124
|
+
"image",
|
|
125
|
+
"radio",
|
|
126
|
+
"range",
|
|
127
|
+
"reset",
|
|
128
|
+
"submit"
|
|
129
|
+
]);
|
|
130
|
+
return !blockedTypes.has(target.type);
|
|
131
|
+
}
|
|
132
|
+
return target.isContentEditable || target.getAttribute("contenteditable") === "true";
|
|
133
|
+
}
|
|
134
|
+
function isEnabled(target) {
|
|
135
|
+
if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) {
|
|
136
|
+
return !target.disabled && !target.readOnly;
|
|
137
|
+
}
|
|
138
|
+
return target.getAttribute("aria-disabled") !== "true" && !target.hasAttribute("disabled") && target.getAttribute("contenteditable") !== "false";
|
|
139
|
+
}
|
|
140
|
+
if (!(element instanceof HTMLElement)) {
|
|
141
|
+
return {
|
|
142
|
+
connected: false,
|
|
143
|
+
visible: false,
|
|
144
|
+
editable: false,
|
|
145
|
+
enabled: false,
|
|
146
|
+
acceptsTextInput: false
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
const rect = element.getBoundingClientRect();
|
|
150
|
+
const clientRects = element.getClientRects();
|
|
151
|
+
const visibleByBrowser = typeof element.checkVisibility === "function" ? element.checkVisibility({
|
|
152
|
+
checkOpacity: true,
|
|
153
|
+
checkVisibilityCSS: true
|
|
154
|
+
}) : true;
|
|
155
|
+
const visible = element.isConnected && visibleByBrowser && !hasHiddenAncestor(element) && clientRects.length > 0 && rect.width > 0 && rect.height > 0;
|
|
156
|
+
const enabled = isEnabled(element);
|
|
157
|
+
const textInput = acceptsTextInput(element);
|
|
158
|
+
return {
|
|
159
|
+
connected: element.isConnected,
|
|
160
|
+
visible,
|
|
161
|
+
editable: visible && enabled && textInput,
|
|
162
|
+
enabled,
|
|
163
|
+
acceptsTextInput: textInput
|
|
164
|
+
};
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
async dispatchClick() {
|
|
168
|
+
await this.locator.evaluate((element) => {
|
|
169
|
+
if (!(element instanceof HTMLElement)) return;
|
|
170
|
+
element.dispatchEvent(
|
|
171
|
+
new MouseEvent("click", {
|
|
172
|
+
bubbles: true,
|
|
173
|
+
cancelable: true,
|
|
174
|
+
composed: true,
|
|
175
|
+
view: window
|
|
176
|
+
})
|
|
177
|
+
);
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
class PlaywrightBrowserContextCompat {
|
|
182
|
+
constructor(context) {
|
|
183
|
+
this.context = context;
|
|
184
|
+
const existingPages = this.context.pages();
|
|
185
|
+
this.initialPage = existingPages.length === 1 && existingPages[0]?.url() === "about:blank" ? existingPages[0] : null;
|
|
186
|
+
}
|
|
187
|
+
context;
|
|
188
|
+
pageMap = /* @__PURE__ */ new WeakMap();
|
|
189
|
+
initialPage;
|
|
190
|
+
wrapPage(page) {
|
|
191
|
+
const existing = this.pageMap.get(page);
|
|
192
|
+
if (existing) return existing;
|
|
193
|
+
const wrapped = new PlaywrightPageCompat(page, this);
|
|
194
|
+
this.pageMap.set(page, wrapped);
|
|
195
|
+
return wrapped;
|
|
196
|
+
}
|
|
197
|
+
getRawContext() {
|
|
198
|
+
return this.context;
|
|
199
|
+
}
|
|
200
|
+
getBrowser() {
|
|
201
|
+
const browser = this.context.browser();
|
|
202
|
+
if (browser) return browser;
|
|
203
|
+
return {
|
|
204
|
+
version: () => "Camoufox",
|
|
205
|
+
close: () => this.context.close()
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
async newPage() {
|
|
209
|
+
if (this.initialPage) {
|
|
210
|
+
const page = this.initialPage;
|
|
211
|
+
this.initialPage = null;
|
|
212
|
+
return this.wrapPage(page);
|
|
213
|
+
}
|
|
214
|
+
return this.wrapPage(await this.context.newPage());
|
|
215
|
+
}
|
|
216
|
+
async close() {
|
|
217
|
+
await this.context.close();
|
|
218
|
+
}
|
|
219
|
+
async storageState(options) {
|
|
220
|
+
await this.context.storageState(options);
|
|
221
|
+
}
|
|
222
|
+
async addInitScript(script) {
|
|
223
|
+
await this.context.addInitScript(script);
|
|
224
|
+
}
|
|
225
|
+
on(event, listener) {
|
|
226
|
+
this.context.on(event, (page) => {
|
|
227
|
+
listener(this.wrapPage(page));
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
class PlaywrightPageCompat {
|
|
232
|
+
constructor(page, owner) {
|
|
233
|
+
this.page = page;
|
|
234
|
+
this.owner = owner;
|
|
235
|
+
this.mouse = this.page.mouse;
|
|
236
|
+
this.keyboard = this.page.keyboard;
|
|
237
|
+
}
|
|
238
|
+
page;
|
|
239
|
+
owner;
|
|
240
|
+
mouse;
|
|
241
|
+
keyboard;
|
|
242
|
+
getRawPage() {
|
|
243
|
+
return this.page;
|
|
244
|
+
}
|
|
245
|
+
async goto(url, options) {
|
|
246
|
+
await this.page.goto(url, options);
|
|
247
|
+
}
|
|
248
|
+
async evaluate(pageFunction, arg) {
|
|
249
|
+
return await this.page.evaluate(pageFunction, arg);
|
|
250
|
+
}
|
|
251
|
+
url() {
|
|
252
|
+
return this.page.url();
|
|
253
|
+
}
|
|
254
|
+
async getUrl() {
|
|
255
|
+
return this.page.url();
|
|
256
|
+
}
|
|
257
|
+
waitForTimeout(ms) {
|
|
258
|
+
return this.page.waitForTimeout(ms);
|
|
259
|
+
}
|
|
260
|
+
waitForLoadState(state, options) {
|
|
261
|
+
return this.page.waitForLoadState(state, options);
|
|
262
|
+
}
|
|
263
|
+
async waitForSelector(selector, options) {
|
|
264
|
+
await this.page.waitForSelector(selector, options ?? {});
|
|
265
|
+
}
|
|
266
|
+
locator(selector) {
|
|
267
|
+
return new PlaywrightLocatorCompat(this.page.locator(selector));
|
|
268
|
+
}
|
|
269
|
+
close() {
|
|
270
|
+
return this.page.close();
|
|
271
|
+
}
|
|
272
|
+
setDefaultTimeout(ms) {
|
|
273
|
+
this.page.setDefaultTimeout(ms);
|
|
274
|
+
}
|
|
275
|
+
setDefaultNavigationTimeout(ms) {
|
|
276
|
+
this.page.setDefaultNavigationTimeout(ms);
|
|
277
|
+
}
|
|
278
|
+
on(event, listener) {
|
|
279
|
+
if (event === "console") {
|
|
280
|
+
this.page.on(event, listener);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
this.page.on(event, (worker) => {
|
|
284
|
+
listener(
|
|
285
|
+
new PlaywrightWorkerCompat(worker)
|
|
286
|
+
);
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
context() {
|
|
290
|
+
return this.owner;
|
|
291
|
+
}
|
|
292
|
+
viewportSize() {
|
|
293
|
+
return this.page.viewportSize();
|
|
294
|
+
}
|
|
295
|
+
async runDomOp(operation, params) {
|
|
296
|
+
return await runPageDomOp(
|
|
297
|
+
this.page,
|
|
298
|
+
operation,
|
|
299
|
+
params ?? {}
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
async ping() {
|
|
303
|
+
try {
|
|
304
|
+
await Promise.race([
|
|
305
|
+
this.page.evaluate(() => true),
|
|
306
|
+
new Promise(
|
|
307
|
+
(_, reject) => setTimeout(() => reject(new Error("page ping timeout")), 5e3)
|
|
308
|
+
)
|
|
309
|
+
]);
|
|
310
|
+
return true;
|
|
311
|
+
} catch {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
async screenshot(options) {
|
|
316
|
+
return await this.page.screenshot(options ?? {});
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
export {
|
|
320
|
+
PlaywrightBrowserContextCompat
|
|
321
|
+
};
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import {
|
|
2
|
+
request as httpRequest
|
|
3
|
+
} from "node:http";
|
|
4
|
+
import { request as httpsRequest } from "node:https";
|
|
5
|
+
import { connect as netConnect } from "node:net";
|
|
6
|
+
import { connect as tlsConnect } from "node:tls";
|
|
7
|
+
const SOCKET_TIMEOUT_MS = 3e4;
|
|
8
|
+
function buildBasicAuthHeader(proxy) {
|
|
9
|
+
if (!proxy.username && !proxy.password) return null;
|
|
10
|
+
const token = Buffer.from(
|
|
11
|
+
`${proxy.username ?? ""}:${proxy.password ?? ""}`
|
|
12
|
+
).toString("base64");
|
|
13
|
+
return `Basic ${token}`;
|
|
14
|
+
}
|
|
15
|
+
function sanitizeHeaders(headers) {
|
|
16
|
+
const nextHeaders = { ...headers };
|
|
17
|
+
delete nextHeaders["proxy-authorization"];
|
|
18
|
+
delete nextHeaders["proxy-connection"];
|
|
19
|
+
delete nextHeaders["Proxy-Authorization"];
|
|
20
|
+
delete nextHeaders["Proxy-Connection"];
|
|
21
|
+
return nextHeaders;
|
|
22
|
+
}
|
|
23
|
+
function formatHttpTarget(request) {
|
|
24
|
+
if (!request.url) {
|
|
25
|
+
throw new Error("Proxy request missing URL");
|
|
26
|
+
}
|
|
27
|
+
if (/^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(request.url)) {
|
|
28
|
+
return new URL(request.url);
|
|
29
|
+
}
|
|
30
|
+
if (!request.headers.host) {
|
|
31
|
+
throw new Error("Proxy request missing Host header");
|
|
32
|
+
}
|
|
33
|
+
return new URL(`http://${request.headers.host}${request.url}`);
|
|
34
|
+
}
|
|
35
|
+
function parseAuthority(authority) {
|
|
36
|
+
if (authority.startsWith("[")) {
|
|
37
|
+
const closingBracket = authority.indexOf("]");
|
|
38
|
+
if (closingBracket === -1) {
|
|
39
|
+
throw new Error(`Invalid authority: ${authority}`);
|
|
40
|
+
}
|
|
41
|
+
const host2 = authority.slice(1, closingBracket);
|
|
42
|
+
const portPart = authority.slice(closingBracket + 1);
|
|
43
|
+
const port2 = portPart.startsWith(":") ? Number(portPart.slice(1)) : 443;
|
|
44
|
+
if (!Number.isFinite(port2)) {
|
|
45
|
+
throw new Error(`Invalid authority port: ${authority}`);
|
|
46
|
+
}
|
|
47
|
+
return { host: host2, port: port2 };
|
|
48
|
+
}
|
|
49
|
+
const separator = authority.lastIndexOf(":");
|
|
50
|
+
if (separator === -1) {
|
|
51
|
+
return { host: authority, port: 443 };
|
|
52
|
+
}
|
|
53
|
+
const host = authority.slice(0, separator);
|
|
54
|
+
const port = Number(authority.slice(separator + 1));
|
|
55
|
+
if (!host || !Number.isFinite(port)) {
|
|
56
|
+
throw new Error(`Invalid authority: ${authority}`);
|
|
57
|
+
}
|
|
58
|
+
return { host, port };
|
|
59
|
+
}
|
|
60
|
+
function connectTcp(host, port) {
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
const socket = netConnect({ host, port });
|
|
63
|
+
const onError = (error) => {
|
|
64
|
+
cleanup();
|
|
65
|
+
reject(error);
|
|
66
|
+
};
|
|
67
|
+
const onConnect = () => {
|
|
68
|
+
cleanup();
|
|
69
|
+
socket.setNoDelay(true);
|
|
70
|
+
socket.setTimeout(SOCKET_TIMEOUT_MS, () => {
|
|
71
|
+
socket.destroy(new Error("socket timeout"));
|
|
72
|
+
});
|
|
73
|
+
resolve(socket);
|
|
74
|
+
};
|
|
75
|
+
const cleanup = () => {
|
|
76
|
+
socket.off("error", onError);
|
|
77
|
+
socket.off("connect", onConnect);
|
|
78
|
+
};
|
|
79
|
+
socket.once("error", onError);
|
|
80
|
+
socket.once("connect", onConnect);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
function connectTls(host, port) {
|
|
84
|
+
return new Promise((resolve, reject) => {
|
|
85
|
+
const socket = tlsConnect({
|
|
86
|
+
host,
|
|
87
|
+
port,
|
|
88
|
+
servername: host,
|
|
89
|
+
// Proxy providers (e.g. BrightData) may use custom CA certs.
|
|
90
|
+
// rejectUnauthorized applies only to the proxy TLS hop, not to the
|
|
91
|
+
// end-to-end TLS inside the CONNECT tunnel (which the browser validates).
|
|
92
|
+
rejectUnauthorized: false
|
|
93
|
+
});
|
|
94
|
+
const onError = (error) => {
|
|
95
|
+
cleanup();
|
|
96
|
+
reject(error);
|
|
97
|
+
};
|
|
98
|
+
const onSecureConnect = () => {
|
|
99
|
+
cleanup();
|
|
100
|
+
socket.setNoDelay(true);
|
|
101
|
+
socket.setTimeout(SOCKET_TIMEOUT_MS, () => {
|
|
102
|
+
socket.destroy(new Error("socket timeout"));
|
|
103
|
+
});
|
|
104
|
+
resolve(socket);
|
|
105
|
+
};
|
|
106
|
+
const cleanup = () => {
|
|
107
|
+
socket.off("error", onError);
|
|
108
|
+
socket.off("secureConnect", onSecureConnect);
|
|
109
|
+
};
|
|
110
|
+
socket.once("error", onError);
|
|
111
|
+
socket.once("secureConnect", onSecureConnect);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
function readUntilAny(socket, ...delimiters) {
|
|
115
|
+
return new Promise((resolve, reject) => {
|
|
116
|
+
const delimiterBuffers = delimiters.map((d) => Buffer.from(d));
|
|
117
|
+
const chunks = [];
|
|
118
|
+
let total = 0;
|
|
119
|
+
const onData = (chunk) => {
|
|
120
|
+
chunks.push(chunk);
|
|
121
|
+
total += chunk.length;
|
|
122
|
+
const payload = Buffer.concat(chunks, total);
|
|
123
|
+
for (const delimiterBuffer of delimiterBuffers) {
|
|
124
|
+
const index = payload.indexOf(delimiterBuffer);
|
|
125
|
+
if (index === -1) continue;
|
|
126
|
+
cleanup();
|
|
127
|
+
const end = index + delimiterBuffer.length;
|
|
128
|
+
const head = payload.subarray(0, end);
|
|
129
|
+
const tail = payload.subarray(end);
|
|
130
|
+
if (tail.length > 0) {
|
|
131
|
+
socket.unshift(tail);
|
|
132
|
+
}
|
|
133
|
+
resolve(head);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
const onError = (error) => {
|
|
138
|
+
cleanup();
|
|
139
|
+
reject(error);
|
|
140
|
+
};
|
|
141
|
+
const onClose = () => {
|
|
142
|
+
cleanup();
|
|
143
|
+
reject(new Error("socket closed before response headers were received"));
|
|
144
|
+
};
|
|
145
|
+
const cleanup = () => {
|
|
146
|
+
socket.off("data", onData);
|
|
147
|
+
socket.off("error", onError);
|
|
148
|
+
socket.off("close", onClose);
|
|
149
|
+
};
|
|
150
|
+
socket.on("data", onData);
|
|
151
|
+
socket.once("error", onError);
|
|
152
|
+
socket.once("close", onClose);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
async function createTunnelSocket(proxy, targetHost, targetPort) {
|
|
156
|
+
const socket = proxy.scheme === "https" ? await connectTls(proxy.host, proxy.port) : await connectTcp(proxy.host, proxy.port);
|
|
157
|
+
const authHeader = buildBasicAuthHeader(proxy);
|
|
158
|
+
const requestLines = [
|
|
159
|
+
`CONNECT ${targetHost}:${targetPort} HTTP/1.1`,
|
|
160
|
+
`Host: ${targetHost}:${targetPort}`,
|
|
161
|
+
"Proxy-Connection: Keep-Alive"
|
|
162
|
+
];
|
|
163
|
+
if (authHeader) {
|
|
164
|
+
requestLines.push(`Proxy-Authorization: ${authHeader}`);
|
|
165
|
+
}
|
|
166
|
+
requestLines.push("", "");
|
|
167
|
+
socket.write(requestLines.join("\r\n"));
|
|
168
|
+
const responseHead = await readUntilAny(socket, "\r\n\r\n", "\n\n");
|
|
169
|
+
const statusLine = responseHead.toString("latin1").split(/\r?\n/, 1)[0] ?? "";
|
|
170
|
+
if (!/^HTTP\/1\.[01] 200\b/i.test(statusLine)) {
|
|
171
|
+
socket.destroy();
|
|
172
|
+
throw new Error(`Proxy CONNECT failed: ${statusLine || "no response"}`);
|
|
173
|
+
}
|
|
174
|
+
return socket;
|
|
175
|
+
}
|
|
176
|
+
function handleProxyError(response, statusCode, error) {
|
|
177
|
+
if (response.headersSent) {
|
|
178
|
+
response.destroy(error instanceof Error ? error : void 0);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
response.writeHead(statusCode, { "Content-Type": "text/plain" });
|
|
182
|
+
response.end(error instanceof Error ? error.message : "proxy error");
|
|
183
|
+
}
|
|
184
|
+
async function handleHttpProxyRequest(request, response, proxy, trackSocket) {
|
|
185
|
+
const target = formatHttpTarget(request);
|
|
186
|
+
const proxyAuthHeader = buildBasicAuthHeader(proxy);
|
|
187
|
+
const transport = proxy.scheme === "https" ? httpsRequest : httpRequest;
|
|
188
|
+
const upstreamRequest = transport(
|
|
189
|
+
{
|
|
190
|
+
host: proxy.host,
|
|
191
|
+
port: proxy.port,
|
|
192
|
+
method: request.method,
|
|
193
|
+
path: target.toString(),
|
|
194
|
+
headers: {
|
|
195
|
+
...sanitizeHeaders(request.headers),
|
|
196
|
+
...proxyAuthHeader ? { "Proxy-Authorization": proxyAuthHeader } : {}
|
|
197
|
+
},
|
|
198
|
+
agent: false,
|
|
199
|
+
// Proxy providers may use custom CA certs; only disable for proxy hop.
|
|
200
|
+
rejectUnauthorized: false
|
|
201
|
+
},
|
|
202
|
+
(upstreamResponse) => {
|
|
203
|
+
response.writeHead(
|
|
204
|
+
upstreamResponse.statusCode ?? 502,
|
|
205
|
+
upstreamResponse.statusMessage,
|
|
206
|
+
upstreamResponse.headers
|
|
207
|
+
);
|
|
208
|
+
upstreamResponse.pipe(response);
|
|
209
|
+
}
|
|
210
|
+
);
|
|
211
|
+
upstreamRequest.on("socket", (socket) => {
|
|
212
|
+
trackSocket(socket);
|
|
213
|
+
});
|
|
214
|
+
upstreamRequest.on("error", (error) => {
|
|
215
|
+
handleProxyError(response, 502, error);
|
|
216
|
+
});
|
|
217
|
+
request.pipe(upstreamRequest);
|
|
218
|
+
}
|
|
219
|
+
function relaySockets(clientSocket, upstreamSocket) {
|
|
220
|
+
const destroyPair = () => {
|
|
221
|
+
clientSocket.destroy();
|
|
222
|
+
upstreamSocket.destroy();
|
|
223
|
+
};
|
|
224
|
+
clientSocket.on("error", destroyPair);
|
|
225
|
+
upstreamSocket.on("error", destroyPair);
|
|
226
|
+
clientSocket.on("close", () => upstreamSocket.destroy());
|
|
227
|
+
upstreamSocket.on("close", () => clientSocket.destroy());
|
|
228
|
+
clientSocket.pipe(upstreamSocket);
|
|
229
|
+
upstreamSocket.pipe(clientSocket);
|
|
230
|
+
}
|
|
231
|
+
function checkProxyReachable(host, port, timeoutMs = 2e3) {
|
|
232
|
+
return new Promise((resolve) => {
|
|
233
|
+
const socket = netConnect({ host, port });
|
|
234
|
+
const timer = setTimeout(() => {
|
|
235
|
+
socket.destroy();
|
|
236
|
+
resolve(false);
|
|
237
|
+
}, timeoutMs);
|
|
238
|
+
socket.once("connect", () => {
|
|
239
|
+
clearTimeout(timer);
|
|
240
|
+
socket.destroy();
|
|
241
|
+
resolve(true);
|
|
242
|
+
});
|
|
243
|
+
socket.once("error", () => {
|
|
244
|
+
clearTimeout(timer);
|
|
245
|
+
resolve(false);
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
export {
|
|
250
|
+
checkProxyReachable
|
|
251
|
+
};
|
|
File without changes
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { getDomain, getFaviconUrls } from "../../../shims/utils.js";
|
|
2
|
+
const PROVIDER_OWNED_SOURCE_DOMAINS = {
|
|
3
|
+
chatgpt: ["chatgpt.com", "openai.com"],
|
|
4
|
+
perplexity: ["perplexity.ai"],
|
|
5
|
+
gemini: ["gemini.google.com", "google.com"],
|
|
6
|
+
claude: ["claude.ai", "anthropic.com"],
|
|
7
|
+
"ai-overview": ["google.com"]
|
|
8
|
+
};
|
|
9
|
+
function escapeRegExp(value) {
|
|
10
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
11
|
+
}
|
|
12
|
+
function normalizeSourceTitle(rawTitle, url) {
|
|
13
|
+
const normalized = rawTitle.replace(/\s+/g, " ").trim();
|
|
14
|
+
if (!normalized) return normalized;
|
|
15
|
+
const domain = getDomain(url)?.replace(/^www\./i, "") || "";
|
|
16
|
+
const hostLabel = domain.split(".")[0] || "";
|
|
17
|
+
const prefixes = [domain, hostLabel].filter(Boolean);
|
|
18
|
+
let title = normalized;
|
|
19
|
+
for (const prefix of prefixes) {
|
|
20
|
+
title = title.replace(
|
|
21
|
+
new RegExp(`^${escapeRegExp(prefix)}(?:\\s+|(?=[A-Z]))`, "i"),
|
|
22
|
+
""
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
return title.trim() || normalized;
|
|
26
|
+
}
|
|
27
|
+
function isProviderOwnedSource(provider, url) {
|
|
28
|
+
if (!provider) return false;
|
|
29
|
+
const hostname = (() => {
|
|
30
|
+
try {
|
|
31
|
+
return new URL(url).hostname.toLowerCase();
|
|
32
|
+
} catch {
|
|
33
|
+
return "";
|
|
34
|
+
}
|
|
35
|
+
})();
|
|
36
|
+
if (!hostname) return false;
|
|
37
|
+
return (PROVIDER_OWNED_SOURCE_DOMAINS[provider] || []).some(
|
|
38
|
+
(domain) => hostname === domain || hostname.endsWith(`.${domain}`)
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
function buildSources(rawSources, options) {
|
|
42
|
+
const results = [];
|
|
43
|
+
const seen = /* @__PURE__ */ new Set();
|
|
44
|
+
for (const { rawHref, title: rawTitle, citedText } of rawSources) {
|
|
45
|
+
const url = rawHref.replace(/#.*$/, "");
|
|
46
|
+
if (!url) continue;
|
|
47
|
+
if (isProviderOwnedSource(options?.provider, url)) continue;
|
|
48
|
+
const domain = getDomain(url) || null;
|
|
49
|
+
const title = normalizeSourceTitle(rawTitle || "", url) || domain || url;
|
|
50
|
+
const favicon = getFaviconUrls(domain ?? "")?.[0] ?? null;
|
|
51
|
+
const source = { title, cited_text: citedText, url, domain, favicon };
|
|
52
|
+
const dedupeKey = JSON.stringify({
|
|
53
|
+
domain: source.domain ?? null,
|
|
54
|
+
url: source.url,
|
|
55
|
+
title: source.title,
|
|
56
|
+
cited_text: source.cited_text ?? ""
|
|
57
|
+
});
|
|
58
|
+
if (seen.has(dedupeKey)) continue;
|
|
59
|
+
seen.add(dedupeKey);
|
|
60
|
+
results.push(source);
|
|
61
|
+
}
|
|
62
|
+
return results;
|
|
63
|
+
}
|
|
64
|
+
async function clickButtonViaDispatch(_page, button) {
|
|
65
|
+
await button.dispatchClick();
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
export {
|
|
69
|
+
buildSources,
|
|
70
|
+
clickButtonViaDispatch
|
|
71
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { ExternalServiceError } from "../../../../shims/errors.js";
|
|
2
|
+
async function detectEditorBlocker(page, input) {
|
|
3
|
+
const box = await input.boundingBox().catch(() => null);
|
|
4
|
+
if (!box || box.width < 8 || box.height < 8) {
|
|
5
|
+
return { blocked: false, reason: null };
|
|
6
|
+
}
|
|
7
|
+
const insetX = Math.max(6, Math.min(24, box.width * 0.12));
|
|
8
|
+
const insetY = Math.max(6, Math.min(18, box.height * 0.2));
|
|
9
|
+
const points = [
|
|
10
|
+
{ x: box.x + box.width / 2, y: box.y + box.height / 2 },
|
|
11
|
+
{ x: box.x + insetX, y: box.y + insetY },
|
|
12
|
+
{ x: box.x + box.width - insetX, y: box.y + insetY }
|
|
13
|
+
];
|
|
14
|
+
return await page.evaluate((samples) => {
|
|
15
|
+
const isEditorLike = (element) => Boolean(
|
|
16
|
+
element?.closest(
|
|
17
|
+
'[contenteditable="true"], textarea, input, [role="textbox"], [role="combobox"]'
|
|
18
|
+
)
|
|
19
|
+
);
|
|
20
|
+
for (const sample of samples) {
|
|
21
|
+
const top = document.elementFromPoint(sample.x, sample.y);
|
|
22
|
+
if (!top) continue;
|
|
23
|
+
if (isEditorLike(top)) continue;
|
|
24
|
+
const blocker = top.closest(
|
|
25
|
+
'[role="dialog"], [aria-modal="true"], [popover], [data-state="open"]'
|
|
26
|
+
) || top;
|
|
27
|
+
if (!(blocker instanceof HTMLElement)) continue;
|
|
28
|
+
const style = window.getComputedStyle(blocker);
|
|
29
|
+
if (style.display === "none" || style.visibility === "hidden" || style.pointerEvents === "none") {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const rect = blocker.getBoundingClientRect();
|
|
33
|
+
if (rect.width < 120 || rect.height < 40) continue;
|
|
34
|
+
const text = (blocker.textContent || "").replace(/\s+/g, " ").trim();
|
|
35
|
+
const label = blocker.getAttribute("aria-label") || blocker.getAttribute("role") || blocker.tagName.toLowerCase();
|
|
36
|
+
return {
|
|
37
|
+
blocked: true,
|
|
38
|
+
reason: `${label}:${text.slice(0, 120)}`
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return { blocked: false, reason: null };
|
|
42
|
+
}, points);
|
|
43
|
+
}
|
|
44
|
+
async function ensureEditorNotBlocked(page, input, provider) {
|
|
45
|
+
const result = await detectEditorBlocker(page, input);
|
|
46
|
+
if (!result.blocked) return;
|
|
47
|
+
throw new ExternalServiceError(
|
|
48
|
+
provider,
|
|
49
|
+
`Editor blocked by overlay: ${result.reason ?? "unknown blocker"}`
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
export {
|
|
53
|
+
ensureEditorNotBlocked
|
|
54
|
+
};
|