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,52 @@
|
|
|
1
|
+
import {
|
|
2
|
+
clickLocatorLikeUser,
|
|
3
|
+
getBrowserPrimaryModifier
|
|
4
|
+
} from "../../browser/humanBehavior.js";
|
|
5
|
+
async function clearEditorInput(page, input, options = {}) {
|
|
6
|
+
const {
|
|
7
|
+
clickTimeoutMs = 3e3,
|
|
8
|
+
dismissWithEscape = false,
|
|
9
|
+
waitAfterMs = 0
|
|
10
|
+
} = options;
|
|
11
|
+
const count = await input.count().catch(() => 0);
|
|
12
|
+
if (count === 0) return false;
|
|
13
|
+
try {
|
|
14
|
+
const existingValue = await input.readInputValue().catch(() => "");
|
|
15
|
+
if (existingValue.trim().length === 0) {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
const clicked = await clickLocatorLikeUser(page, input, {
|
|
19
|
+
timeout: clickTimeoutMs
|
|
20
|
+
}).catch(() => false) || await clickLocatorLikeUser(page, input, {
|
|
21
|
+
force: true,
|
|
22
|
+
timeout: clickTimeoutMs
|
|
23
|
+
}).catch(() => false);
|
|
24
|
+
if (!clicked) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
const primaryModifier = await getBrowserPrimaryModifier(page);
|
|
28
|
+
const selectAllShortcuts = primaryModifier === "Control" ? ["Control+A"] : [`${primaryModifier}+A`, "Control+A"];
|
|
29
|
+
let clearedByKeyboard = false;
|
|
30
|
+
for (const shortcut of selectAllShortcuts) {
|
|
31
|
+
await input.press(shortcut).catch(() => null);
|
|
32
|
+
await input.press("Backspace").catch(() => null);
|
|
33
|
+
clearedByKeyboard = await input.readInputValue().then((value) => value.trim().length === 0).catch(() => false);
|
|
34
|
+
if (clearedByKeyboard) break;
|
|
35
|
+
}
|
|
36
|
+
if (!clearedByKeyboard) {
|
|
37
|
+
await input.setInputValue("");
|
|
38
|
+
}
|
|
39
|
+
if (dismissWithEscape) {
|
|
40
|
+
await input.press("Escape").catch(() => null);
|
|
41
|
+
}
|
|
42
|
+
if (waitAfterMs > 0) {
|
|
43
|
+
await page.waitForTimeout(waitAfterMs);
|
|
44
|
+
}
|
|
45
|
+
return true;
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export {
|
|
51
|
+
clearEditorInput
|
|
52
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { NotFoundError } from "../../../../shims/errors.js";
|
|
2
|
+
import { PROVIDER_EDITOR_SELECTORS } from "../../../../shims/utils.js";
|
|
3
|
+
async function findActiveEditorCandidateFromSelectors(page, selectors) {
|
|
4
|
+
for (const selector of selectors) {
|
|
5
|
+
const nodes = page.locator(selector);
|
|
6
|
+
const count = await nodes.count();
|
|
7
|
+
for (let i = 0; i < count; i++) {
|
|
8
|
+
const el = nodes.nth(i);
|
|
9
|
+
try {
|
|
10
|
+
const visible = await el.isVisible().catch(() => false);
|
|
11
|
+
if (!visible) {
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
const box = await el.boundingBox().catch(() => null);
|
|
15
|
+
if (!box || box.width < 8 || box.height < 8) {
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
await el.scrollIntoViewIfNeeded().catch(() => {
|
|
19
|
+
});
|
|
20
|
+
await el.focus().catch(() => {
|
|
21
|
+
});
|
|
22
|
+
const state = await el.getEditableState().catch(() => null);
|
|
23
|
+
if (!(state?.connected && state.visible && state.editable && state.enabled)) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
return { locator: el, selector };
|
|
27
|
+
} catch (_error) {
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
throw new NotFoundError("active prompt editor");
|
|
32
|
+
}
|
|
33
|
+
async function findActiveEditorFromSelectors(page, selectors) {
|
|
34
|
+
const candidate = await findActiveEditorCandidateFromSelectors(
|
|
35
|
+
page,
|
|
36
|
+
selectors
|
|
37
|
+
);
|
|
38
|
+
return candidate.locator;
|
|
39
|
+
}
|
|
40
|
+
async function findActiveEditor(page, provider) {
|
|
41
|
+
const candidate = await findActiveEditorCandidate(page, provider);
|
|
42
|
+
return candidate.locator;
|
|
43
|
+
}
|
|
44
|
+
async function findActiveEditorCandidate(page, provider) {
|
|
45
|
+
const fallbackSelectors = [
|
|
46
|
+
...new Set(Object.values(PROVIDER_EDITOR_SELECTORS).flat())
|
|
47
|
+
];
|
|
48
|
+
const selectors = provider ? PROVIDER_EDITOR_SELECTORS[provider] || fallbackSelectors : fallbackSelectors;
|
|
49
|
+
return findActiveEditorCandidateFromSelectors(page, selectors);
|
|
50
|
+
}
|
|
51
|
+
export {
|
|
52
|
+
findActiveEditorCandidate,
|
|
53
|
+
findActiveEditorCandidateFromSelectors
|
|
54
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { PROVIDER_SUBMIT_BTN_SELECTORS } from "../../../../shims/utils.js";
|
|
2
|
+
async function findEnabledSendButton(page, provider) {
|
|
3
|
+
const selectors = PROVIDER_SUBMIT_BTN_SELECTORS[provider] || [];
|
|
4
|
+
for (const selector of selectors) {
|
|
5
|
+
const buttons = page.locator(selector);
|
|
6
|
+
const count = await buttons.count();
|
|
7
|
+
for (let i = 0; i < count; i++) {
|
|
8
|
+
const btn = buttons.nth(i);
|
|
9
|
+
try {
|
|
10
|
+
if (await btn.isVisible() && await btn.isEnabled()) {
|
|
11
|
+
return btn;
|
|
12
|
+
}
|
|
13
|
+
} catch {
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
export {
|
|
20
|
+
findEnabledSendButton
|
|
21
|
+
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { ExternalServiceError } from "../../../../shims/errors.js";
|
|
2
|
+
import { logger } from "../../../../shims/utils.js";
|
|
3
|
+
import {
|
|
4
|
+
clickLocatorLikeUser,
|
|
5
|
+
pastePrompt,
|
|
6
|
+
randomBetween
|
|
7
|
+
} from "../../browser/humanBehavior.js";
|
|
8
|
+
import { clearEditorInput } from "./clearInput.js";
|
|
9
|
+
function normalizePromptValue(text) {
|
|
10
|
+
return text.replace(/\r\n/g, "\n").replace(/\u00a0/g, " ").replace(/[\u200b-\u200d\ufeff]/g, "").trim();
|
|
11
|
+
}
|
|
12
|
+
async function focusEditorTarget(page, input) {
|
|
13
|
+
await input.scrollIntoViewIfNeeded().catch(() => null);
|
|
14
|
+
await clickLocatorLikeUser(page, input, {
|
|
15
|
+
timeout: 3e3,
|
|
16
|
+
delay: randomBetween(25, 80)
|
|
17
|
+
}).catch(() => null);
|
|
18
|
+
await page.waitForTimeout(randomBetween(40, 120));
|
|
19
|
+
await input.focus().catch(() => null);
|
|
20
|
+
await page.waitForTimeout(randomBetween(30, 90));
|
|
21
|
+
}
|
|
22
|
+
async function prepareEditorForPrompt(page, input, provider) {
|
|
23
|
+
const count = await input.count().catch(() => 0);
|
|
24
|
+
if (count === 0) {
|
|
25
|
+
throw new ExternalServiceError(
|
|
26
|
+
provider,
|
|
27
|
+
`Editor not ready for ${provider}: input locator is missing`
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
const state = await input.getEditableState().catch(() => null);
|
|
31
|
+
if (!(state?.connected && state.editable && state.enabled && state.acceptsTextInput)) {
|
|
32
|
+
throw new ExternalServiceError(
|
|
33
|
+
provider,
|
|
34
|
+
`Editor not ready for ${provider}: input is not editable`
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
await focusEditorTarget(page, input);
|
|
38
|
+
const existingValue = await input.readInputValue().catch(() => "");
|
|
39
|
+
if (normalizePromptValue(existingValue).length === 0) {
|
|
40
|
+
await focusEditorTarget(page, input);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const cleared = await clearEditorInput(page, input, {
|
|
44
|
+
clickTimeoutMs: 3e3,
|
|
45
|
+
waitAfterMs: randomBetween(40, 120)
|
|
46
|
+
});
|
|
47
|
+
if (!cleared) {
|
|
48
|
+
throw new ExternalServiceError(
|
|
49
|
+
provider,
|
|
50
|
+
`Editor not ready for ${provider}: could not clear existing input`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
const remainingValue = await input.readInputValue().catch(() => "");
|
|
54
|
+
if (normalizePromptValue(remainingValue).length > 0) {
|
|
55
|
+
throw new ExternalServiceError(
|
|
56
|
+
provider,
|
|
57
|
+
`Editor not ready for ${provider}: input retained content after clear`
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
await focusEditorTarget(page, input);
|
|
61
|
+
}
|
|
62
|
+
async function insertPromptOnce(page, input, prompt, strategy) {
|
|
63
|
+
if (strategy === "directSet") {
|
|
64
|
+
await input.setInputValue(prompt);
|
|
65
|
+
await page.waitForTimeout(randomBetween(40, 120));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
await pastePrompt(page, prompt);
|
|
69
|
+
}
|
|
70
|
+
async function waitForPromptValue(page, input, expectedValue, timeoutMs) {
|
|
71
|
+
const deadline = Date.now() + timeoutMs;
|
|
72
|
+
let lastValue = await input.readInputValue().catch(() => "");
|
|
73
|
+
while (Date.now() < deadline) {
|
|
74
|
+
if (normalizePromptValue(lastValue) === expectedValue) {
|
|
75
|
+
return lastValue;
|
|
76
|
+
}
|
|
77
|
+
await page.waitForTimeout(randomBetween(80, 140));
|
|
78
|
+
lastValue = await input.readInputValue().catch(() => "");
|
|
79
|
+
}
|
|
80
|
+
return lastValue;
|
|
81
|
+
}
|
|
82
|
+
async function insertPromptIntoEditor(page, input, prompt, provider) {
|
|
83
|
+
const expectedValue = normalizePromptValue(prompt);
|
|
84
|
+
const strategies = [
|
|
85
|
+
...provider === "perplexity" ? [] : ["directSet"],
|
|
86
|
+
"pacedPaste"
|
|
87
|
+
];
|
|
88
|
+
for (const strategy of strategies) {
|
|
89
|
+
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
90
|
+
await prepareEditorForPrompt(page, input, provider);
|
|
91
|
+
await insertPromptOnce(page, input, prompt, strategy);
|
|
92
|
+
const rawValue = await waitForPromptValue(
|
|
93
|
+
page,
|
|
94
|
+
input,
|
|
95
|
+
expectedValue,
|
|
96
|
+
strategy === "directSet" ? attempt === 1 ? 800 : 1400 : attempt === 1 ? 1800 : 2500
|
|
97
|
+
);
|
|
98
|
+
if (normalizePromptValue(rawValue) === expectedValue) {
|
|
99
|
+
return { rawValue, strategy };
|
|
100
|
+
}
|
|
101
|
+
if (attempt === 1) {
|
|
102
|
+
logger.warn(
|
|
103
|
+
`[${provider}] prompt verification mismatch after ${strategy} \u2014 retrying once`
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const finalValue = await input.readInputValue().catch(() => "");
|
|
109
|
+
throw new ExternalServiceError(
|
|
110
|
+
provider,
|
|
111
|
+
`Typing failed: normalized input mismatch after local retry (expected ${expectedValue.length} chars, got ${normalizePromptValue(finalValue).length})`
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
export {
|
|
115
|
+
insertPromptIntoEditor,
|
|
116
|
+
normalizePromptValue
|
|
117
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import TurndownService from "turndown";
|
|
2
|
+
const turndown = new TurndownService({
|
|
3
|
+
headingStyle: "atx",
|
|
4
|
+
codeBlockStyle: "fenced",
|
|
5
|
+
bulletListMarker: "-",
|
|
6
|
+
br: "\n"
|
|
7
|
+
});
|
|
8
|
+
turndown.addRule("paragraph", {
|
|
9
|
+
filter: "p",
|
|
10
|
+
replacement(content) {
|
|
11
|
+
return `
|
|
12
|
+
|
|
13
|
+
${content}
|
|
14
|
+
|
|
15
|
+
`;
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
turndown.addRule("div", {
|
|
19
|
+
filter: "div",
|
|
20
|
+
replacement(content) {
|
|
21
|
+
return `
|
|
22
|
+
|
|
23
|
+
${content.trim()}
|
|
24
|
+
|
|
25
|
+
`;
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
turndown.addRule("heading", {
|
|
29
|
+
filter: ["h1", "h2", "h3", "h4", "h5", "h6"],
|
|
30
|
+
replacement(content, node) {
|
|
31
|
+
const level = Number(node.nodeName.charAt(1));
|
|
32
|
+
return `
|
|
33
|
+
|
|
34
|
+
${"#".repeat(level)} ${content}
|
|
35
|
+
|
|
36
|
+
`;
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
turndown.addRule("list", {
|
|
40
|
+
filter: ["ul", "ol"],
|
|
41
|
+
replacement(content, node) {
|
|
42
|
+
const parent = node.parentNode;
|
|
43
|
+
if (parent && (parent.nodeName === "LI" || parent.nodeName === "UL" || parent.nodeName === "OL")) {
|
|
44
|
+
return `
|
|
45
|
+
${content}`;
|
|
46
|
+
}
|
|
47
|
+
return `
|
|
48
|
+
|
|
49
|
+
${content}
|
|
50
|
+
|
|
51
|
+
`;
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
turndown.addRule("table", {
|
|
55
|
+
filter: "table",
|
|
56
|
+
replacement(_content, node) {
|
|
57
|
+
const table = node;
|
|
58
|
+
const rows = Array.from(table.querySelectorAll("tr"));
|
|
59
|
+
if (rows.length === 0) return "";
|
|
60
|
+
const result = [];
|
|
61
|
+
for (let i = 0; i < rows.length; i++) {
|
|
62
|
+
const cells = Array.from(rows[i].querySelectorAll("th, td"));
|
|
63
|
+
const line = cells.map((c) => turndown.turndown(c.innerHTML ?? c.textContent ?? "").replace(/\n+/g, " ").trim()).join(" | ");
|
|
64
|
+
result.push(`| ${line} |`);
|
|
65
|
+
if (i === 0) {
|
|
66
|
+
result.push(`| ${cells.map(() => "---").join(" | ")} |`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return `
|
|
70
|
+
|
|
71
|
+
${result.join("\n")}
|
|
72
|
+
|
|
73
|
+
`;
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
turndown.addRule("link", {
|
|
77
|
+
filter: "a",
|
|
78
|
+
replacement(content) {
|
|
79
|
+
const trimmed = content.trim();
|
|
80
|
+
if (!trimmed) return "";
|
|
81
|
+
if (/^\[?\d+\]?$/.test(trimmed)) return "";
|
|
82
|
+
return content;
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
turndown.addRule("image", { filter: "img", replacement: () => "" });
|
|
86
|
+
turndown.addRule("figure", { filter: "figure", replacement: () => "" });
|
|
87
|
+
turndown.addRule("picture", { filter: "picture", replacement: () => "" });
|
|
88
|
+
turndown.addRule("video", { filter: "video", replacement: () => "" });
|
|
89
|
+
turndown.addRule("iframe", { filter: "iframe", replacement: () => "" });
|
|
90
|
+
turndown.addRule("sup", { filter: "sup", replacement: () => "" });
|
|
91
|
+
turndown.addRule("carousel", {
|
|
92
|
+
filter(node) {
|
|
93
|
+
const el = node;
|
|
94
|
+
const cn = el.className || "";
|
|
95
|
+
return el.nodeName === "DIV" && (cn.includes("carousel") || cn.includes("gallery") || cn.includes("slider") || cn.includes("swiper"));
|
|
96
|
+
},
|
|
97
|
+
replacement: () => ""
|
|
98
|
+
});
|
|
99
|
+
export {
|
|
100
|
+
turndown
|
|
101
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { PROVIDER_MODEL_RESPONSE_SELECTORS } from "../../../../shims/utils.js";
|
|
2
|
+
import { turndown } from "./converter.js";
|
|
3
|
+
async function extractAssistantMarkdown(page, provider) {
|
|
4
|
+
const html = await page.runDomOp("response-html", {
|
|
5
|
+
provider,
|
|
6
|
+
selectors: PROVIDER_MODEL_RESPONSE_SELECTORS[provider] || []
|
|
7
|
+
});
|
|
8
|
+
if (!html) return "";
|
|
9
|
+
const markdown = turndown.turndown(html);
|
|
10
|
+
return markdown.replace(/\n{3,}/g, "\n\n").trim();
|
|
11
|
+
}
|
|
12
|
+
export {
|
|
13
|
+
extractAssistantMarkdown
|
|
14
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { ExternalServiceError } from "../../../../shims/errors.js";
|
|
2
|
+
async function detectBotPage(page, provider) {
|
|
3
|
+
const state = await page.runDomOp("detect-bot-page").catch(() => ({ botDetected: false, reason: null }));
|
|
4
|
+
if (state.botDetected) {
|
|
5
|
+
throw new ExternalServiceError(
|
|
6
|
+
provider,
|
|
7
|
+
state.reason ?? "bot detection page detected"
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export {
|
|
12
|
+
detectBotPage
|
|
13
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { PROVIDER_MODEL_RESPONSE_SELECTORS } from "../../../../shims/utils.js";
|
|
2
|
+
async function getText(page, provider) {
|
|
3
|
+
return await page.runDomOp("response-text", {
|
|
4
|
+
provider,
|
|
5
|
+
selectors: PROVIDER_MODEL_RESPONSE_SELECTORS[provider] || []
|
|
6
|
+
});
|
|
7
|
+
}
|
|
8
|
+
export {
|
|
9
|
+
getText
|
|
10
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PROVIDER_MODEL_RESPONSE_SELECTORS,
|
|
3
|
+
PROVIDER_RESPONSE_GENERATION_SELECTORS
|
|
4
|
+
} from "../../../../shims/utils.js";
|
|
5
|
+
async function getGenerationStateSignature(page, provider) {
|
|
6
|
+
return await page.evaluate(
|
|
7
|
+
(selectors) => (selectors || []).map((selector) => {
|
|
8
|
+
const parts = Array.from(document.querySelectorAll(selector)).map((node) => {
|
|
9
|
+
const element = node;
|
|
10
|
+
const style = window.getComputedStyle(element);
|
|
11
|
+
const visible = element.offsetParent !== null && style.visibility !== "hidden" && style.display !== "none";
|
|
12
|
+
const text = (element.textContent || "").trim();
|
|
13
|
+
const ariaLabel = element.getAttribute("aria-label") || "";
|
|
14
|
+
const disabled = element.getAttribute("disabled") ? "1" : "0";
|
|
15
|
+
return `${visible ? 1 : 0}:${text}:${ariaLabel}:${disabled}`;
|
|
16
|
+
});
|
|
17
|
+
return `${selector}=>${parts.join("|")}`;
|
|
18
|
+
}).join("||"),
|
|
19
|
+
PROVIDER_RESPONSE_GENERATION_SELECTORS[provider] || []
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
async function hasVisibleGenerationIndicator(page, provider) {
|
|
23
|
+
return await page.evaluate(
|
|
24
|
+
(selectors) => (selectors || []).some(
|
|
25
|
+
(selector) => Array.from(document.querySelectorAll(selector)).some((node) => {
|
|
26
|
+
const element = node;
|
|
27
|
+
const style = window.getComputedStyle(element);
|
|
28
|
+
return element.offsetParent !== null && style.visibility !== "hidden" && style.display !== "none";
|
|
29
|
+
})
|
|
30
|
+
),
|
|
31
|
+
PROVIDER_RESPONSE_GENERATION_SELECTORS[provider] || []
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
async function getResponseStateSignature(page, provider) {
|
|
35
|
+
return await page.evaluate((selectors) => {
|
|
36
|
+
const visible = (element) => {
|
|
37
|
+
if (!(element instanceof HTMLElement)) return false;
|
|
38
|
+
const style = window.getComputedStyle(element);
|
|
39
|
+
return element.offsetParent !== null && style.visibility !== "hidden" && style.display !== "none";
|
|
40
|
+
};
|
|
41
|
+
const elements = (selectors || []).flatMap((selector) => Array.from(document.querySelectorAll(selector))).filter(
|
|
42
|
+
(el) => visible(el) && (el.innerText || "").trim().length > 0
|
|
43
|
+
);
|
|
44
|
+
const latest = elements.at(-1) ?? null;
|
|
45
|
+
if (!latest) {
|
|
46
|
+
return { signature: "", textLength: 0 };
|
|
47
|
+
}
|
|
48
|
+
const text = (latest.innerText || "").replace(/\s+/g, " ").trim();
|
|
49
|
+
return {
|
|
50
|
+
signature: `${text.length}:${latest.innerHTML.length}:${latest.childElementCount}:${text.slice(-120)}`,
|
|
51
|
+
textLength: text.length
|
|
52
|
+
};
|
|
53
|
+
}, PROVIDER_MODEL_RESPONSE_SELECTORS[provider] || []);
|
|
54
|
+
}
|
|
55
|
+
export {
|
|
56
|
+
getGenerationStateSignature,
|
|
57
|
+
getResponseStateSignature,
|
|
58
|
+
hasVisibleGenerationIndicator
|
|
59
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { ExternalServiceError } from "../../../../shims/errors.js";
|
|
2
|
+
import {
|
|
3
|
+
logger,
|
|
4
|
+
PROVIDER_FORCE_EXIT_STABLE_MS,
|
|
5
|
+
PROVIDER_NO_OUTPUT_TIMEOUT_MS
|
|
6
|
+
} from "../../../../shims/utils.js";
|
|
7
|
+
import {
|
|
8
|
+
getGenerationStateSignature,
|
|
9
|
+
getResponseStateSignature,
|
|
10
|
+
hasVisibleGenerationIndicator
|
|
11
|
+
} from "./isGenerating.js";
|
|
12
|
+
async function sleep(ms) {
|
|
13
|
+
let timer = null;
|
|
14
|
+
try {
|
|
15
|
+
await new Promise((resolve) => {
|
|
16
|
+
timer = setTimeout(resolve, ms);
|
|
17
|
+
});
|
|
18
|
+
} finally {
|
|
19
|
+
if (timer !== null) {
|
|
20
|
+
clearTimeout(timer);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async function pollUntilCondition(checkFn, pollInterval, maxWait, timeoutError) {
|
|
25
|
+
const start = Date.now();
|
|
26
|
+
while (Date.now() - start < maxWait) {
|
|
27
|
+
if (await checkFn()) return;
|
|
28
|
+
await sleep(pollInterval);
|
|
29
|
+
}
|
|
30
|
+
throw timeoutError;
|
|
31
|
+
}
|
|
32
|
+
async function waitForAssistantToFinish(page, provider) {
|
|
33
|
+
logger.debug(
|
|
34
|
+
provider === "ai-overview" ? "\u23F3 Waiting for AI Overview response container to stabilize\u2026" : "\u23F3 Waiting for assistant to finish\u2026"
|
|
35
|
+
);
|
|
36
|
+
const waitStart = Date.now();
|
|
37
|
+
let lastGenerationState = "";
|
|
38
|
+
let lastResponseState = "";
|
|
39
|
+
let lastChangeAt = Date.now();
|
|
40
|
+
let initialized = false;
|
|
41
|
+
let seenResponse = false;
|
|
42
|
+
await pollUntilCondition(
|
|
43
|
+
async () => {
|
|
44
|
+
const [currentGenerationState, currentResponseState, hasVisibleIndicator] = await Promise.all([
|
|
45
|
+
getGenerationStateSignature(page, provider),
|
|
46
|
+
getResponseStateSignature(page, provider),
|
|
47
|
+
hasVisibleGenerationIndicator(page, provider)
|
|
48
|
+
]);
|
|
49
|
+
const waitedFor = Date.now() - waitStart;
|
|
50
|
+
const forceExitStableMs = PROVIDER_FORCE_EXIT_STABLE_MS[provider];
|
|
51
|
+
const responseStateChanged = currentResponseState.signature !== lastResponseState;
|
|
52
|
+
const generationStateChanged = currentGenerationState !== lastGenerationState;
|
|
53
|
+
const requiresContainerStabilityOnly = provider === "ai-overview";
|
|
54
|
+
if (!initialized) {
|
|
55
|
+
lastGenerationState = currentGenerationState;
|
|
56
|
+
lastResponseState = currentResponseState.signature;
|
|
57
|
+
seenResponse = currentResponseState.textLength > 0;
|
|
58
|
+
lastChangeAt = Date.now();
|
|
59
|
+
initialized = true;
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
if (currentResponseState.textLength > 0) {
|
|
63
|
+
seenResponse = true;
|
|
64
|
+
}
|
|
65
|
+
if (responseStateChanged || generationStateChanged) {
|
|
66
|
+
lastGenerationState = currentGenerationState;
|
|
67
|
+
lastResponseState = currentResponseState.signature;
|
|
68
|
+
lastChangeAt = Date.now();
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
const stableFor = Date.now() - lastChangeAt;
|
|
72
|
+
if (requiresContainerStabilityOnly) {
|
|
73
|
+
if (seenResponse && stableFor >= 2500) {
|
|
74
|
+
logger.debug("\u2705 AI Overview response container stabilized");
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (seenResponse && !hasVisibleIndicator && stableFor >= 1500) {
|
|
79
|
+
logger.debug("\u2705 Assistant finished");
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
const noOutputTimeoutMs = PROVIDER_NO_OUTPUT_TIMEOUT_MS[provider];
|
|
83
|
+
if (waitedFor >= noOutputTimeoutMs) {
|
|
84
|
+
logger.warn(
|
|
85
|
+
`Generation state did not stabilize within ${Math.round(noOutputTimeoutMs / 1e3)}s`
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
if (stableFor >= forceExitStableMs) {
|
|
89
|
+
logger.warn(
|
|
90
|
+
`${hasVisibleIndicator ? "Generation indicator still visible and " : ""}generation state stable for ${Math.round(forceExitStableMs / 1e3)}s \u2014 forcing exit`
|
|
91
|
+
);
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
},
|
|
96
|
+
280 + Math.floor(Math.random() * 60),
|
|
97
|
+
// Poll ~300ms with ±50ms jitter
|
|
98
|
+
5 * 60 * 1e3,
|
|
99
|
+
// 5 min max — if a response hasn't arrived by then, something is wrong
|
|
100
|
+
new ExternalServiceError(provider, "Assistant wait timed out")
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
export {
|
|
104
|
+
waitForAssistantToFinish
|
|
105
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
async function findSourcesButton(page, _provider) {
|
|
2
|
+
const buttonCount = await page.locator("button, [role='button']").count();
|
|
3
|
+
if (buttonCount === 0) return null;
|
|
4
|
+
const bestIndex = await page.evaluate((_unused) => {
|
|
5
|
+
const normalize = (text) => text?.toLowerCase().replace(/\s+/g, " ").trim() || "";
|
|
6
|
+
const getVisibleText = (el) => normalize(
|
|
7
|
+
Array.from(el.querySelectorAll("*")).map((child) => child.textContent || "").join(" ")
|
|
8
|
+
);
|
|
9
|
+
return Array.from(
|
|
10
|
+
document.querySelectorAll("button, [role='button'], [role='tab']")
|
|
11
|
+
).map((el, index) => {
|
|
12
|
+
if (!(el instanceof HTMLElement)) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
const role = el.getAttribute("role");
|
|
16
|
+
if (role === "tab" || el.hasAttribute("aria-controls") || el.id?.includes("trigger")) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
const isAction = el.tagName === "BUTTON" || role === "button";
|
|
20
|
+
if (!isAction) return null;
|
|
21
|
+
const style = window.getComputedStyle(el);
|
|
22
|
+
if (style.display === "none" || style.visibility === "hidden") {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
const text = getVisibleText(el);
|
|
26
|
+
const aria = normalize(el.getAttribute("aria-label"));
|
|
27
|
+
let score = 0;
|
|
28
|
+
if (/\b\d+\s*sources?\b/.test(text)) score += 120;
|
|
29
|
+
if (aria.includes("sources")) score += 90;
|
|
30
|
+
if (text === "sources") score += 80;
|
|
31
|
+
if (text.includes("sources")) score += 50;
|
|
32
|
+
if (score <= 0) return null;
|
|
33
|
+
score += index * 1e-3;
|
|
34
|
+
return { index, score };
|
|
35
|
+
}).filter((match) => match !== null).sort((a, b) => b.score - a.score || b.index - a.index)[0]?.index ?? -1;
|
|
36
|
+
}, null);
|
|
37
|
+
return bestIndex >= 0 ? page.locator("button, [role='button']").nth(bestIndex) : null;
|
|
38
|
+
}
|
|
39
|
+
export {
|
|
40
|
+
findSourcesButton
|
|
41
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { clickButtonViaDispatch } from "../../extraction/sourceUtils.js";
|
|
2
|
+
async function openSourcesPanel(page, btn) {
|
|
3
|
+
const clicked = await btn.scrollIntoViewIfNeeded().then(() => btn.click({ timeout: 3e3 })).then(() => true).catch(() => false);
|
|
4
|
+
if (!clicked) {
|
|
5
|
+
if (!await clickButtonViaDispatch(page, btn)) return;
|
|
6
|
+
}
|
|
7
|
+
await page.waitForTimeout(500);
|
|
8
|
+
}
|
|
9
|
+
export {
|
|
10
|
+
openSourcesPanel
|
|
11
|
+
};
|