@anweat/dsh-browser 0.1.7 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +306 -245
- package/cordis.patch.yml +26 -16
- package/lib/approval-policy.js +15 -0
- package/lib/approval-policy.js.map +1 -1
- package/lib/browser-service.d.ts +68 -25
- package/lib/browser-service.js +222 -62
- package/lib/browser-service.js.map +1 -1
- package/lib/client/SettingsCard.d.ts +2 -0
- package/lib/client/SettingsCard.js +21 -0
- package/lib/client/SettingsCard.js.map +1 -0
- package/lib/client/context-types.d.ts +8 -0
- package/lib/client/context-types.js +2 -0
- package/lib/client/context-types.js.map +1 -0
- package/lib/client/form.d.ts +62 -0
- package/lib/client/form.js +198 -0
- package/lib/client/form.js.map +1 -0
- package/lib/client/index.d.ts +16 -0
- package/lib/client/index.js +18 -0
- package/lib/client/index.js.map +1 -0
- package/lib/client/locales.d.ts +58 -0
- package/lib/client/locales.js +55 -0
- package/lib/client/locales.js.map +1 -0
- package/lib/client/settings-namespace.d.ts +2 -0
- package/lib/client/settings-namespace.js +3 -0
- package/lib/client/settings-namespace.js.map +1 -0
- package/lib/client/styles.d.ts +37 -0
- package/lib/client/styles.js +25 -0
- package/lib/client/styles.js.map +1 -0
- package/lib/client.js +753 -0
- package/lib/client.js.map +1 -0
- package/lib/config.d.ts +10 -0
- package/lib/config.js +22 -0
- package/lib/config.js.map +1 -1
- package/lib/deps.d.ts +7 -1
- package/lib/deps.js +22 -6
- package/lib/deps.js.map +1 -1
- package/lib/freedom.d.ts +1 -1
- package/lib/freedom.js +4 -0
- package/lib/freedom.js.map +1 -1
- package/lib/index.js +13 -1
- package/lib/index.js.map +1 -1
- package/lib/opencli-catalog.d.ts +21 -0
- package/lib/opencli-catalog.js +49 -0
- package/lib/opencli-catalog.js.map +1 -0
- package/lib/scripts.js +27 -27
- package/lib/tools.js +75 -1
- package/lib/tools.js.map +1 -1
- package/lib/usage-policy.d.ts +49 -0
- package/lib/usage-policy.js +171 -0
- package/lib/usage-policy.js.map +1 -0
- package/package.json +133 -78
- package/scripts/check-client-bundle.mjs +15 -0
- package/scripts/clean-lib.mjs +3 -0
package/lib/approval-policy.js
CHANGED
|
@@ -3,6 +3,7 @@ import { recipeNeedsApproval } from "./automation.js";
|
|
|
3
3
|
import { validateUserscript } from "./scripts.js";
|
|
4
4
|
import { isBrowserToolExposed } from "./freedom.js";
|
|
5
5
|
const DIRECT_INTERACTIONS = new Set(['browser_click', 'browser_type', 'browser_scroll']);
|
|
6
|
+
const WEB_LOCAL_MUTATIONS = new Set(['web_cache_clear']);
|
|
6
7
|
export function browserPolicyDecision(name, args, mode = 'standard') {
|
|
7
8
|
if (!isBrowserToolExposed(name, mode) && name.startsWith('browser_')) {
|
|
8
9
|
return { kind: 'deny', reason: `Browser tool ${name} is disabled by automationMode=${mode}` };
|
|
@@ -58,6 +59,20 @@ export function browserPolicyDecision(name, args, mode = 'standard') {
|
|
|
58
59
|
return { kind: 'ask', reason: 'Run a multi-step Playwright recipe with page mutations: ' + actions.join(', ') };
|
|
59
60
|
}
|
|
60
61
|
}
|
|
62
|
+
if (name === 'web_deps' && args?.action === 'install') {
|
|
63
|
+
if (mode === 'read-only')
|
|
64
|
+
return { kind: 'deny', reason: `Dependency installation is disabled by automationMode=${mode}` };
|
|
65
|
+
if (mode === 'unrestricted')
|
|
66
|
+
return { kind: 'allow' };
|
|
67
|
+
return { kind: 'ask', reason: 'Install an external Web Search Pro backend dependency' };
|
|
68
|
+
}
|
|
69
|
+
const webRuleAction = name === 'web_rule' ? args?.action : undefined;
|
|
70
|
+
if (WEB_LOCAL_MUTATIONS.has(name) || (name === 'web_rule' && ['upsert', 'remove', 'import'].includes(String(webRuleAction)))) {
|
|
71
|
+
if (mode === 'read-only')
|
|
72
|
+
return { kind: 'deny', reason: `Web Search Pro mutations are disabled by automationMode=${mode}` };
|
|
73
|
+
if (mode === 'standard')
|
|
74
|
+
return { kind: 'ask', reason: 'Modify Web Search Pro local state: ' + name };
|
|
75
|
+
}
|
|
61
76
|
return { kind: 'allow' };
|
|
62
77
|
}
|
|
63
78
|
//# sourceMappingURL=approval-policy.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"approval-policy.js","sourceRoot":"","sources":["../src/approval-policy.ts"],"names":[],"mappings":"AAAA,iFAAiF;AAEjF,OAAO,EAAE,mBAAmB,EAA0B,MAAM,iBAAiB,CAAA;AAC7E,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAA;AACjD,OAAO,EAAE,oBAAoB,EAAuB,MAAM,cAAc,CAAA;AAOxE,MAAM,mBAAmB,GAAG,IAAI,GAAG,CAAC,CAAC,eAAe,EAAE,cAAc,EAAE,gBAAgB,CAAC,CAAC,CAAA;
|
|
1
|
+
{"version":3,"file":"approval-policy.js","sourceRoot":"","sources":["../src/approval-policy.ts"],"names":[],"mappings":"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"}
|
package/lib/browser-service.d.ts
CHANGED
|
@@ -23,6 +23,8 @@ import type { ResolvedConfig } from './config.ts';
|
|
|
23
23
|
import { type BrowserRecipeStep, type RecipeStepResult } from './automation.ts';
|
|
24
24
|
import { type UserscriptValidation } from './scripts.ts';
|
|
25
25
|
import { type AutomationMode } from './freedom.ts';
|
|
26
|
+
import { type OpencliCatalogFilter, type OpencliCatalogItem } from './opencli-catalog.ts';
|
|
27
|
+
import { UsageGovernor } from './usage-policy.ts';
|
|
26
28
|
export interface RenderRule {
|
|
27
29
|
hostname: string;
|
|
28
30
|
contentSelectors: string[];
|
|
@@ -37,7 +39,7 @@ export interface RenderResult {
|
|
|
37
39
|
export interface SnapshotResult {
|
|
38
40
|
title: string;
|
|
39
41
|
text: string;
|
|
40
|
-
screenshotPath
|
|
42
|
+
screenshotPath?: string;
|
|
41
43
|
htmlPath: string;
|
|
42
44
|
usedRule?: string;
|
|
43
45
|
}
|
|
@@ -70,6 +72,58 @@ export interface ScriptRunResult {
|
|
|
70
72
|
resultJson: string;
|
|
71
73
|
truncated: boolean;
|
|
72
74
|
}
|
|
75
|
+
export interface CrawlPage {
|
|
76
|
+
url: string;
|
|
77
|
+
title: string;
|
|
78
|
+
text: string;
|
|
79
|
+
depth: number;
|
|
80
|
+
status: number;
|
|
81
|
+
}
|
|
82
|
+
export interface CrawlResult {
|
|
83
|
+
pages: CrawlPage[];
|
|
84
|
+
errors: {
|
|
85
|
+
url: string;
|
|
86
|
+
depth: number;
|
|
87
|
+
error: string;
|
|
88
|
+
status?: number;
|
|
89
|
+
}[];
|
|
90
|
+
stats: {
|
|
91
|
+
pagesVisited: number;
|
|
92
|
+
queued: number;
|
|
93
|
+
elapsedMs: number;
|
|
94
|
+
waitMs: number;
|
|
95
|
+
backoffEvents: number;
|
|
96
|
+
};
|
|
97
|
+
warnings: string[];
|
|
98
|
+
}
|
|
99
|
+
export interface BrowserStatus {
|
|
100
|
+
enabled: boolean;
|
|
101
|
+
channel: string;
|
|
102
|
+
browserRuntime: 'playwright' | 'patchright';
|
|
103
|
+
runtimeWarnings: string[];
|
|
104
|
+
headless: boolean;
|
|
105
|
+
opencliEnabled: boolean;
|
|
106
|
+
automationMode: AutomationMode;
|
|
107
|
+
exposedTools: string[];
|
|
108
|
+
directInteractionPolicy: 'deny' | 'ask' | 'allow';
|
|
109
|
+
mutatingRecipePolicy: 'deny' | 'ask' | 'allow';
|
|
110
|
+
externalUserscriptPolicy: 'deny' | 'ask' | 'allow';
|
|
111
|
+
opencliRunPolicy: 'deny' | 'ask' | 'allow';
|
|
112
|
+
chromiumInstalled: boolean;
|
|
113
|
+
usagePolicy: ResolvedConfig['usagePolicy'];
|
|
114
|
+
usageGovernor: ReturnType<UsageGovernor['snapshot']>;
|
|
115
|
+
authProfiles: {
|
|
116
|
+
id: string;
|
|
117
|
+
allowedDomains: string[];
|
|
118
|
+
persistState: boolean;
|
|
119
|
+
}[];
|
|
120
|
+
rulePacks: string[];
|
|
121
|
+
builtinScripts: string[];
|
|
122
|
+
externalUserscriptsRequireApproval: boolean;
|
|
123
|
+
mutatingRecipesRequireApproval: boolean;
|
|
124
|
+
activeUrl?: string;
|
|
125
|
+
activeAuthProfile?: string;
|
|
126
|
+
}
|
|
73
127
|
export declare class BrowserService {
|
|
74
128
|
private readonly config;
|
|
75
129
|
private browser;
|
|
@@ -79,11 +133,14 @@ export declare class BrowserService {
|
|
|
79
133
|
private activeProfile?;
|
|
80
134
|
private activeRulePack?;
|
|
81
135
|
private readonly authProfiles;
|
|
136
|
+
private readonly usageGovernor;
|
|
137
|
+
private opencliCatalogCache?;
|
|
82
138
|
constructor(config: ResolvedConfig);
|
|
83
139
|
available(): boolean;
|
|
84
140
|
private ensure;
|
|
85
141
|
/** Run `playwright install chromium` from the bundled playwright CLI. */
|
|
86
142
|
installChromium(): Promise<CliResult>;
|
|
143
|
+
private navigate;
|
|
87
144
|
private transientContext;
|
|
88
145
|
private persistAndClose;
|
|
89
146
|
render(url: string, rules: readonly RenderRule[], opts?: {
|
|
@@ -99,6 +156,7 @@ export declare class BrowserService {
|
|
|
99
156
|
maxChars?: number;
|
|
100
157
|
authProfile?: string;
|
|
101
158
|
rulePack?: string;
|
|
159
|
+
screenshot?: boolean;
|
|
102
160
|
}): Promise<SnapshotResult>;
|
|
103
161
|
searchResults(url: string, spec: PlatformSpec, opts?: {
|
|
104
162
|
signal?: AbortSignal;
|
|
@@ -119,6 +177,14 @@ export declare class BrowserService {
|
|
|
119
177
|
signal?: AbortSignal;
|
|
120
178
|
}): Promise<CliResult>;
|
|
121
179
|
opencliDoctor(signal?: AbortSignal): Promise<CliResult>;
|
|
180
|
+
opencliCatalog(filter?: OpencliCatalogFilter, signal?: AbortSignal): Promise<OpencliCatalogItem[]>;
|
|
181
|
+
crawl(startUrls: readonly string[], opts?: {
|
|
182
|
+
maxPages?: number;
|
|
183
|
+
maxDepth?: number;
|
|
184
|
+
sameOrigin?: boolean;
|
|
185
|
+
maxCharsPerPage?: number;
|
|
186
|
+
signal?: AbortSignal;
|
|
187
|
+
}): Promise<CrawlResult>;
|
|
122
188
|
scriptCatalog(): {
|
|
123
189
|
id: string;
|
|
124
190
|
name: string;
|
|
@@ -169,29 +235,6 @@ export declare class BrowserService {
|
|
|
169
235
|
signal?: AbortSignal;
|
|
170
236
|
}): Promise<RecipeRunResult>;
|
|
171
237
|
closePage(): Promise<void>;
|
|
172
|
-
status(): Promise<
|
|
173
|
-
enabled: boolean;
|
|
174
|
-
channel: string;
|
|
175
|
-
headless: boolean;
|
|
176
|
-
opencliEnabled: boolean;
|
|
177
|
-
automationMode: AutomationMode;
|
|
178
|
-
exposedTools: string[];
|
|
179
|
-
directInteractionPolicy: 'deny' | 'ask' | 'allow';
|
|
180
|
-
mutatingRecipePolicy: 'deny' | 'ask' | 'allow';
|
|
181
|
-
externalUserscriptPolicy: 'deny' | 'ask' | 'allow';
|
|
182
|
-
opencliRunPolicy: 'deny' | 'ask' | 'allow';
|
|
183
|
-
chromiumInstalled: boolean;
|
|
184
|
-
authProfiles: {
|
|
185
|
-
id: string;
|
|
186
|
-
allowedDomains: string[];
|
|
187
|
-
persistState: boolean;
|
|
188
|
-
}[];
|
|
189
|
-
rulePacks: string[];
|
|
190
|
-
builtinScripts: string[];
|
|
191
|
-
externalUserscriptsRequireApproval: boolean;
|
|
192
|
-
mutatingRecipesRequireApproval: boolean;
|
|
193
|
-
activeUrl?: string;
|
|
194
|
-
activeAuthProfile?: string;
|
|
195
|
-
}>;
|
|
238
|
+
status(): Promise<BrowserStatus>;
|
|
196
239
|
close(): Promise<void>;
|
|
197
240
|
}
|
package/lib/browser-service.js
CHANGED
|
@@ -21,60 +21,75 @@
|
|
|
21
21
|
import fs from 'node:fs';
|
|
22
22
|
import path from 'node:path';
|
|
23
23
|
import crypto from 'node:crypto';
|
|
24
|
-
import {
|
|
24
|
+
import { browserRuntimeCliPath, loadBrowserRuntime, runOpencli, runNode } from "./deps.js";
|
|
25
25
|
import { AuthProfileStore } from "./auth-profiles.js";
|
|
26
26
|
import { applyRuleSteps, resolveRulePack } from "./rule-packs.js";
|
|
27
27
|
import { runRecipe } from "./automation.js";
|
|
28
28
|
import { BUILTIN_SCRIPTS, builtinScript, executeUserscript, validateUserscript } from "./scripts.js";
|
|
29
29
|
import { browserToolsForMode } from "./freedom.js";
|
|
30
|
+
import { filterOpencliCatalog, parseOpencliCatalog } from "./opencli-catalog.js";
|
|
31
|
+
import { UsageGovernor } from "./usage-policy.js";
|
|
30
32
|
/** Rules-aware content extractor (runs in the page). */
|
|
31
|
-
const EXTRACTOR_FN = `(ruleList) => {
|
|
32
|
-
const doc = document
|
|
33
|
-
const title = doc.title ? doc.title.trim() : ''
|
|
34
|
-
let host = ''
|
|
35
|
-
try { host = location.hostname } catch (e) {}
|
|
36
|
-
const norm = (h) => { const x = h.toLowerCase(); return x.startsWith('www.') ? x.slice(4) : x }
|
|
37
|
-
let rule = null
|
|
38
|
-
for (const r of ruleList) {
|
|
39
|
-
const rh = norm(r.hostname)
|
|
40
|
-
if (host === rh || host.endsWith('.' + rh)) rule = r
|
|
41
|
-
}
|
|
42
|
-
const pick = (selectors) => {
|
|
43
|
-
for (const sel of selectors) {
|
|
44
|
-
try {
|
|
45
|
-
const el = doc.querySelector(sel)
|
|
46
|
-
if (el && (el.textContent || '').trim().length > 40) return el
|
|
47
|
-
} catch (e) {}
|
|
48
|
-
}
|
|
49
|
-
return null
|
|
50
|
-
}
|
|
51
|
-
const content = rule ? pick(rule.contentSelectors) : (pick(['article', 'main', '[role="main"]']) || doc.body)
|
|
52
|
-
if (content && rule && rule.removeSelectors) {
|
|
53
|
-
for (const sel of rule.removeSelectors) {
|
|
54
|
-
try { content.querySelectorAll(sel).forEach((el) => el.remove()) } catch (e) {}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
const text = content ? (content.innerText || content.textContent || '') : ''
|
|
58
|
-
return { title, text, html: doc.documentElement.outerHTML.slice(0, 2000000), usedRule: rule ? rule.hostname : null }
|
|
33
|
+
const EXTRACTOR_FN = `(ruleList) => {
|
|
34
|
+
const doc = document
|
|
35
|
+
const title = doc.title ? doc.title.trim() : ''
|
|
36
|
+
let host = ''
|
|
37
|
+
try { host = location.hostname } catch (e) {}
|
|
38
|
+
const norm = (h) => { const x = h.toLowerCase(); return x.startsWith('www.') ? x.slice(4) : x }
|
|
39
|
+
let rule = null
|
|
40
|
+
for (const r of ruleList) {
|
|
41
|
+
const rh = norm(r.hostname)
|
|
42
|
+
if (host === rh || host.endsWith('.' + rh)) rule = r
|
|
43
|
+
}
|
|
44
|
+
const pick = (selectors) => {
|
|
45
|
+
for (const sel of selectors) {
|
|
46
|
+
try {
|
|
47
|
+
const el = doc.querySelector(sel)
|
|
48
|
+
if (el && (el.textContent || '').trim().length > 40) return el
|
|
49
|
+
} catch (e) {}
|
|
50
|
+
}
|
|
51
|
+
return null
|
|
52
|
+
}
|
|
53
|
+
const content = rule ? pick(rule.contentSelectors) : (pick(['article', 'main', '[role="main"]']) || doc.body)
|
|
54
|
+
if (content && rule && rule.removeSelectors) {
|
|
55
|
+
for (const sel of rule.removeSelectors) {
|
|
56
|
+
try { content.querySelectorAll(sel).forEach((el) => el.remove()) } catch (e) {}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const text = content ? (content.innerText || content.textContent || '') : ''
|
|
60
|
+
return { title, text, html: doc.documentElement.outerHTML.slice(0, 2000000), usedRule: rule ? rule.hostname : null }
|
|
59
61
|
}`;
|
|
60
62
|
/** Platform search-page list extractor (runs in the page). */
|
|
61
|
-
const LIST_EXTRACTOR = `(spec) => {
|
|
62
|
-
const items = []
|
|
63
|
-
const nodes = document.querySelectorAll(spec.item)
|
|
64
|
-
for (let i = 0; i < nodes.length && items.length < 20; i++) {
|
|
65
|
-
const el = nodes[i]
|
|
66
|
-
const titleEl = spec.title ? el.querySelector(spec.title) : null
|
|
67
|
-
const linkEl = spec.link ? el.querySelector(spec.link) : null
|
|
68
|
-
const textEl = spec.text ? el.querySelector(spec.text) : null
|
|
69
|
-
const title = (titleEl ? titleEl.textContent : el.textContent || '').trim().replace(/\\s+/g, ' ')
|
|
70
|
-
let url = linkEl ? (linkEl.href || linkEl.getAttribute('href') || '') : ''
|
|
71
|
-
if (url && url.startsWith('/')) url = location.origin + url
|
|
72
|
-
if (!url && linkEl === null && titleEl) { const a = titleEl.closest ? titleEl.closest('a') : null; if (a) url = a.href }
|
|
73
|
-
const snippet = textEl ? (textEl.textContent || '').trim().replace(/\\s+/g, ' ').slice(0, 300) : ''
|
|
74
|
-
if (!url || !title || title.length < 2) continue
|
|
75
|
-
items.push({ url, title, snippet })
|
|
76
|
-
}
|
|
77
|
-
return items
|
|
63
|
+
const LIST_EXTRACTOR = `(spec) => {
|
|
64
|
+
const items = []
|
|
65
|
+
const nodes = document.querySelectorAll(spec.item)
|
|
66
|
+
for (let i = 0; i < nodes.length && items.length < 20; i++) {
|
|
67
|
+
const el = nodes[i]
|
|
68
|
+
const titleEl = spec.title ? el.querySelector(spec.title) : null
|
|
69
|
+
const linkEl = spec.link ? el.querySelector(spec.link) : null
|
|
70
|
+
const textEl = spec.text ? el.querySelector(spec.text) : null
|
|
71
|
+
const title = (titleEl ? titleEl.textContent : el.textContent || '').trim().replace(/\\s+/g, ' ')
|
|
72
|
+
let url = linkEl ? (linkEl.href || linkEl.getAttribute('href') || '') : ''
|
|
73
|
+
if (url && url.startsWith('/')) url = location.origin + url
|
|
74
|
+
if (!url && linkEl === null && titleEl) { const a = titleEl.closest ? titleEl.closest('a') : null; if (a) url = a.href }
|
|
75
|
+
const snippet = textEl ? (textEl.textContent || '').trim().replace(/\\s+/g, ' ').slice(0, 300) : ''
|
|
76
|
+
if (!url || !title || title.length < 2) continue
|
|
77
|
+
items.push({ url, title, snippet })
|
|
78
|
+
}
|
|
79
|
+
return items
|
|
80
|
+
}`;
|
|
81
|
+
const CRAWL_EXTRACTOR = `(maxChars) => {
|
|
82
|
+
const root = document.querySelector('article, main, [role="main"]') || document.body
|
|
83
|
+
const text = ((root && (root.innerText || root.textContent)) || '').replace(/\\n{3,}/g, '\\n\\n').trim().slice(0, maxChars)
|
|
84
|
+
const links = []
|
|
85
|
+
for (const anchor of document.querySelectorAll('a[href]')) {
|
|
86
|
+
try {
|
|
87
|
+
const url = new URL(anchor.href, location.href)
|
|
88
|
+
if ((url.protocol === 'http:' || url.protocol === 'https:') && !links.includes(url.href)) links.push(url.href)
|
|
89
|
+
if (links.length >= 500) break
|
|
90
|
+
} catch (e) {}
|
|
91
|
+
}
|
|
92
|
+
return { title: document.title || '', text, links }
|
|
78
93
|
}`;
|
|
79
94
|
function uid() {
|
|
80
95
|
return crypto.randomUUID();
|
|
@@ -99,9 +114,12 @@ export class BrowserService {
|
|
|
99
114
|
activeProfile;
|
|
100
115
|
activeRulePack;
|
|
101
116
|
authProfiles;
|
|
117
|
+
usageGovernor;
|
|
118
|
+
opencliCatalogCache;
|
|
102
119
|
constructor(config) {
|
|
103
120
|
this.config = config;
|
|
104
121
|
this.authProfiles = new AuthProfileStore(config.authProfiles);
|
|
122
|
+
this.usageGovernor = new UsageGovernor(config.usagePolicy);
|
|
105
123
|
}
|
|
106
124
|
available() {
|
|
107
125
|
return this.config.enabled;
|
|
@@ -111,7 +129,7 @@ export class BrowserService {
|
|
|
111
129
|
return this.browser;
|
|
112
130
|
if (!this.launching) {
|
|
113
131
|
this.launching = (async () => {
|
|
114
|
-
const pw =
|
|
132
|
+
const pw = loadBrowserRuntime(this.config.browserRuntime);
|
|
115
133
|
const launchOptions = { headless: this.config.headless };
|
|
116
134
|
if (this.config.channel)
|
|
117
135
|
launchOptions.channel = this.config.channel;
|
|
@@ -128,7 +146,7 @@ export class BrowserService {
|
|
|
128
146
|
this.browser = await pw.chromium.launch(launchOptions);
|
|
129
147
|
}
|
|
130
148
|
else {
|
|
131
|
-
throw new Error('dsh-browser: chromium is not installed. Run the browser_install tool, or: node "' +
|
|
149
|
+
throw new Error('dsh-browser: chromium is not installed for ' + this.config.browserRuntime + '. Run the browser_install tool, or: node "' + browserRuntimeCliPath(this.config.browserRuntime) + '" install chromium');
|
|
132
150
|
}
|
|
133
151
|
}
|
|
134
152
|
else {
|
|
@@ -145,16 +163,35 @@ export class BrowserService {
|
|
|
145
163
|
}
|
|
146
164
|
/** Run `playwright install chromium` from the bundled playwright CLI. */
|
|
147
165
|
installChromium() {
|
|
148
|
-
return runNode(
|
|
166
|
+
return runNode(browserRuntimeCliPath(this.config.browserRuntime), ['install', 'chromium'], { timeoutMs: 600_000, signal: undefined, maxOutput: 256 * 1024 });
|
|
167
|
+
}
|
|
168
|
+
async navigate(page, url, options, signal) {
|
|
169
|
+
let response;
|
|
170
|
+
for (let attempt = 0; attempt <= this.config.usagePolicy.retryLimit; attempt++) {
|
|
171
|
+
response = await this.usageGovernor.run(url, () => page.goto(url, options), signal);
|
|
172
|
+
const status = Number(response?.status?.() ?? 0);
|
|
173
|
+
if (status >= 200 && status < 400)
|
|
174
|
+
this.usageGovernor.noteResponse(url, status);
|
|
175
|
+
if (![429, 502, 503, 504].includes(status))
|
|
176
|
+
return response;
|
|
177
|
+
const rawRetryAfter = String(response?.headers?.()?.['retry-after'] ?? '');
|
|
178
|
+
const retryAfterMs = /^\d+(?:\.\d+)?$/.test(rawRetryAfter)
|
|
179
|
+
? Number(rawRetryAfter) * 1000
|
|
180
|
+
: (Number.isFinite(Date.parse(rawRetryAfter)) ? Math.max(Date.parse(rawRetryAfter) - Date.now(), 0) : undefined);
|
|
181
|
+
this.usageGovernor.noteResponse(url, status, retryAfterMs);
|
|
182
|
+
if (attempt === this.config.usagePolicy.retryLimit)
|
|
183
|
+
return response;
|
|
184
|
+
}
|
|
185
|
+
return response;
|
|
149
186
|
}
|
|
150
187
|
async transientContext(url, opts = {}) {
|
|
151
188
|
const browser = await this.ensure();
|
|
152
|
-
const profileId = opts.authProfile ?? this.config.defaultAuthProfile;
|
|
189
|
+
const profileId = opts.anonymous ? undefined : (opts.authProfile ?? this.config.defaultAuthProfile);
|
|
153
190
|
const profile = profileId ? this.authProfiles.resolve(profileId, url) : undefined;
|
|
154
191
|
const rulePack = resolveRulePack(this.config.rulePacks, opts.rulePack, url);
|
|
155
192
|
const context = await browser.newContext(profile?.storageStatePath
|
|
156
193
|
? { storageState: profile.storageStatePath }
|
|
157
|
-
: (this.config.storageStatePath ? { storageState: this.config.storageStatePath } : {}));
|
|
194
|
+
: (!opts.anonymous && this.config.storageStatePath ? { storageState: this.config.storageStatePath } : {}));
|
|
158
195
|
try {
|
|
159
196
|
if (rulePack?.initScriptPath)
|
|
160
197
|
await context.addInitScript({ path: rulePack.initScriptPath });
|
|
@@ -192,7 +229,7 @@ export class BrowserService {
|
|
|
192
229
|
signal?.addEventListener('abort', onAbort);
|
|
193
230
|
try {
|
|
194
231
|
page.setDefaultTimeout(20_000);
|
|
195
|
-
await
|
|
232
|
+
await this.navigate(page, url, { waitUntil: 'domcontentloaded', timeout: 25_000 }, signal);
|
|
196
233
|
await page.waitForLoadState('networkidle', { timeout: 8_000 }).catch(() => { });
|
|
197
234
|
await applyRuleSteps(page, session.rulePack);
|
|
198
235
|
if (opts.waitMs)
|
|
@@ -225,21 +262,22 @@ export class BrowserService {
|
|
|
225
262
|
signal?.addEventListener('abort', onAbort);
|
|
226
263
|
fs.mkdirSync(opts.outDir, { recursive: true });
|
|
227
264
|
const stamp = Date.now() + '-' + uid().slice(0, 8);
|
|
228
|
-
const screenshotPath = path.join(opts.outDir, stamp + '.png');
|
|
265
|
+
const screenshotPath = opts.screenshot === false ? undefined : path.join(opts.outDir, stamp + '.png');
|
|
229
266
|
const htmlPath = path.join(opts.outDir, stamp + '.html');
|
|
230
267
|
try {
|
|
231
268
|
page.setDefaultTimeout(25_000);
|
|
232
|
-
await
|
|
269
|
+
await this.navigate(page, url, { waitUntil: 'domcontentloaded', timeout: 30_000 }, signal);
|
|
233
270
|
await page.waitForLoadState('networkidle', { timeout: 10_000 }).catch(() => { });
|
|
234
271
|
await applyRuleSteps(page, session.rulePack);
|
|
235
|
-
|
|
272
|
+
if (screenshotPath)
|
|
273
|
+
await page.screenshot({ path: screenshotPath, fullPage: true });
|
|
236
274
|
const html = await page.content();
|
|
237
275
|
fs.writeFileSync(htmlPath, html, 'utf8');
|
|
238
276
|
const data = await evaluateExtractor(page, rules);
|
|
239
277
|
return {
|
|
240
278
|
title: String(data.title ?? ''),
|
|
241
279
|
text: capText(String(data.text ?? '').replace(/\n{3,}/g, '\n\n').trim(), opts.maxChars ?? 200_000),
|
|
242
|
-
screenshotPath,
|
|
280
|
+
...screenshotPath ? { screenshotPath } : {},
|
|
243
281
|
htmlPath,
|
|
244
282
|
...data.usedRule ? { usedRule: String(data.usedRule) } : {},
|
|
245
283
|
};
|
|
@@ -266,7 +304,7 @@ export class BrowserService {
|
|
|
266
304
|
signal?.addEventListener('abort', onAbort);
|
|
267
305
|
try {
|
|
268
306
|
page.setDefaultTimeout(25_000);
|
|
269
|
-
await
|
|
307
|
+
await this.navigate(page, url, { waitUntil: 'domcontentloaded', timeout: 30_000 }, signal);
|
|
270
308
|
await page.waitForLoadState('networkidle', { timeout: 8_000 }).catch(() => { });
|
|
271
309
|
await applyRuleSteps(page, session.rulePack);
|
|
272
310
|
if (opts.waitMs)
|
|
@@ -299,11 +337,121 @@ export class BrowserService {
|
|
|
299
337
|
if (args.length < 1 || args.length > 40 || args.some(arg => typeof arg !== 'string' || arg.length > 2_000)) {
|
|
300
338
|
return Promise.resolve({ code: -1, stdout: '', stderr: 'dsh-browser: OpenCLI requires 1 to 40 arguments, each at most 2000 characters', timedOut: false });
|
|
301
339
|
}
|
|
302
|
-
|
|
340
|
+
// OpenCLI adapters can issue real site traffic outside Playwright, so they
|
|
341
|
+
// share the same approval-independent concurrency and burst buffer.
|
|
342
|
+
return this.usageGovernor.run('https://opencli.local/', () => runOpencli(args, { ...opts, signal: opts.signal }), opts.signal);
|
|
303
343
|
}
|
|
304
344
|
opencliDoctor(signal) {
|
|
305
345
|
return this.opencli(['doctor'], { timeoutMs: 30_000, signal });
|
|
306
346
|
}
|
|
347
|
+
async opencliCatalog(filter = {}, signal) {
|
|
348
|
+
if (!this.config.opencliEnabled)
|
|
349
|
+
throw new Error('dsh-browser: OpenCLI is disabled');
|
|
350
|
+
if (!this.opencliCatalogCache) {
|
|
351
|
+
const result = await this.opencli(['list', '-f', 'json'], { timeoutMs: 60_000, signal });
|
|
352
|
+
if (result.code !== 0 || result.timedOut)
|
|
353
|
+
throw new Error('OpenCLI catalog failed: ' + (result.stderr || result.stdout).slice(0, 500));
|
|
354
|
+
this.opencliCatalogCache = parseOpencliCatalog(result.stdout);
|
|
355
|
+
}
|
|
356
|
+
return filterOpencliCatalog(this.opencliCatalogCache, filter);
|
|
357
|
+
}
|
|
358
|
+
async crawl(startUrls, opts = {}) {
|
|
359
|
+
if (startUrls.length < 1 || startUrls.length > 5)
|
|
360
|
+
throw new Error('browser crawl requires 1 to 5 start URLs');
|
|
361
|
+
const normalized = startUrls.map(value => {
|
|
362
|
+
const url = new URL(value);
|
|
363
|
+
if (!['http:', 'https:'].includes(url.protocol))
|
|
364
|
+
throw new Error('browser crawl only supports HTTP(S) URLs');
|
|
365
|
+
url.hash = '';
|
|
366
|
+
return url.href;
|
|
367
|
+
});
|
|
368
|
+
const maxPages = opts.maxPages ?? this.config.usagePolicy.maxPagesPerRun;
|
|
369
|
+
const maxDepth = opts.maxDepth ?? this.config.usagePolicy.maxDepth;
|
|
370
|
+
if (!Number.isInteger(maxPages) || maxPages < 1 || maxPages > this.config.usagePolicy.maxPagesPerRun) {
|
|
371
|
+
throw new Error('browser crawl maxPages must be from 1 to configured usagePolicy.maxPagesPerRun (' + this.config.usagePolicy.maxPagesPerRun + ')');
|
|
372
|
+
}
|
|
373
|
+
if (!Number.isInteger(maxDepth) || maxDepth < 0 || maxDepth > this.config.usagePolicy.maxDepth) {
|
|
374
|
+
throw new Error('browser crawl maxDepth must be from 0 to configured usagePolicy.maxDepth (' + this.config.usagePolicy.maxDepth + ')');
|
|
375
|
+
}
|
|
376
|
+
const maxCharsPerPage = Math.min(Math.max(opts.maxCharsPerPage ?? 20_000, 1_000), 50_000);
|
|
377
|
+
const sameOrigin = opts.sameOrigin ?? true;
|
|
378
|
+
const allowedOrigins = new Set(normalized.map(value => new URL(value).origin));
|
|
379
|
+
const queue = normalized.map(url => ({ url, depth: 0 }));
|
|
380
|
+
const seen = new Set(normalized);
|
|
381
|
+
const pages = [];
|
|
382
|
+
const errors = [];
|
|
383
|
+
const before = this.usageGovernor.snapshot();
|
|
384
|
+
const started = Date.now();
|
|
385
|
+
const session = await this.transientContext(normalized[0], { anonymous: true });
|
|
386
|
+
try {
|
|
387
|
+
while (queue.length && pages.length + errors.length < maxPages) {
|
|
388
|
+
if (opts.signal?.aborted)
|
|
389
|
+
throw new Error('browser crawl aborted');
|
|
390
|
+
const item = queue.shift();
|
|
391
|
+
const page = await session.context.newPage();
|
|
392
|
+
try {
|
|
393
|
+
page.setDefaultTimeout(30_000);
|
|
394
|
+
const response = await this.navigate(page, item.url, { waitUntil: 'domcontentloaded', timeout: 30_000 }, opts.signal);
|
|
395
|
+
const status = Number(response?.status?.() ?? 0);
|
|
396
|
+
if (sameOrigin && !allowedOrigins.has(new URL(page.url()).origin)) {
|
|
397
|
+
errors.push({ url: item.url, depth: item.depth, status, error: 'cross-origin redirect blocked: ' + page.url() });
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
if (status >= 400) {
|
|
401
|
+
errors.push({ url: item.url, depth: item.depth, status, error: 'HTTP ' + status });
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
await page.waitForLoadState('networkidle', { timeout: 5_000 }).catch(() => { });
|
|
405
|
+
const data = await page.evaluate('(' + CRAWL_EXTRACTOR + ')(' + maxCharsPerPage + ')');
|
|
406
|
+
pages.push({ url: page.url(), title: String(data.title ?? ''), text: String(data.text ?? ''), depth: item.depth, status });
|
|
407
|
+
if (item.depth >= maxDepth)
|
|
408
|
+
continue;
|
|
409
|
+
for (const rawLink of Array.isArray(data.links) ? data.links : []) {
|
|
410
|
+
let link;
|
|
411
|
+
try {
|
|
412
|
+
link = new URL(rawLink);
|
|
413
|
+
link.hash = '';
|
|
414
|
+
}
|
|
415
|
+
catch {
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
if (sameOrigin && !allowedOrigins.has(link.origin))
|
|
419
|
+
continue;
|
|
420
|
+
const href = link.href;
|
|
421
|
+
if (seen.has(href) || seen.size >= maxPages * 25)
|
|
422
|
+
continue;
|
|
423
|
+
seen.add(href);
|
|
424
|
+
queue.push({ url: href, depth: item.depth + 1 });
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
catch (error) {
|
|
428
|
+
errors.push({ url: item.url, depth: item.depth, error: String(error).slice(0, 500) });
|
|
429
|
+
}
|
|
430
|
+
finally {
|
|
431
|
+
await page.close().catch(() => { });
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
finally {
|
|
436
|
+
await this.persistAndClose(session);
|
|
437
|
+
}
|
|
438
|
+
const after = this.usageGovernor.snapshot();
|
|
439
|
+
return {
|
|
440
|
+
pages,
|
|
441
|
+
errors,
|
|
442
|
+
stats: {
|
|
443
|
+
pagesVisited: pages.length + errors.length,
|
|
444
|
+
queued: queue.length,
|
|
445
|
+
elapsedMs: Date.now() - started,
|
|
446
|
+
waitMs: after.totalWaitMs - before.totalWaitMs,
|
|
447
|
+
backoffEvents: after.backoffEvents - before.backoffEvents,
|
|
448
|
+
},
|
|
449
|
+
warnings: [
|
|
450
|
+
'Bounded crawl: respect each site\'s terms, robots directives, copyright, privacy, and applicable law.',
|
|
451
|
+
'No-approval mode skips human confirmation only; concurrency, burst, page/depth budgets, and server-pressure backoff remain active.',
|
|
452
|
+
],
|
|
453
|
+
};
|
|
454
|
+
}
|
|
307
455
|
scriptCatalog() {
|
|
308
456
|
return BUILTIN_SCRIPTS.map(script => ({
|
|
309
457
|
id: script.id,
|
|
@@ -330,7 +478,7 @@ export class BrowserService {
|
|
|
330
478
|
let timer;
|
|
331
479
|
try {
|
|
332
480
|
page.setDefaultTimeout(30_000);
|
|
333
|
-
await
|
|
481
|
+
await this.navigate(page, url, { waitUntil: 'domcontentloaded', timeout: 30_000 }, signal);
|
|
334
482
|
await page.waitForLoadState('networkidle', { timeout: 8_000 }).catch(() => { });
|
|
335
483
|
await applyRuleSteps(page, session.rulePack);
|
|
336
484
|
const timeoutMs = Math.min(Math.max(opts.timeoutMs ?? 15_000, 1_000), 30_000);
|
|
@@ -410,7 +558,7 @@ export class BrowserService {
|
|
|
410
558
|
async open(url, opts = {}) {
|
|
411
559
|
const page = await this.ensureActivePage(url, opts);
|
|
412
560
|
page.setDefaultTimeout(30_000);
|
|
413
|
-
await
|
|
561
|
+
await this.navigate(page, url, { waitUntil: 'domcontentloaded', timeout: 30_000 });
|
|
414
562
|
await page.waitForLoadState('networkidle', { timeout: 8_000 }).catch(() => { });
|
|
415
563
|
await applyRuleSteps(page, this.activeRulePack);
|
|
416
564
|
if (opts.waitMs)
|
|
@@ -453,7 +601,7 @@ export class BrowserService {
|
|
|
453
601
|
const page = await this.ensureActivePage(opts.url, opts);
|
|
454
602
|
if (opts.url) {
|
|
455
603
|
page.setDefaultTimeout(30_000);
|
|
456
|
-
await
|
|
604
|
+
await this.navigate(page, opts.url, { waitUntil: 'domcontentloaded', timeout: 30_000 }, opts.signal);
|
|
457
605
|
await page.waitForLoadState('networkidle', { timeout: 8_000 }).catch(() => { });
|
|
458
606
|
await applyRuleSteps(page, this.activeRulePack);
|
|
459
607
|
if (opts.waitMs)
|
|
@@ -486,15 +634,25 @@ export class BrowserService {
|
|
|
486
634
|
async status() {
|
|
487
635
|
let chromiumInstalled = false;
|
|
488
636
|
try {
|
|
489
|
-
const pw =
|
|
637
|
+
const pw = loadBrowserRuntime(this.config.browserRuntime);
|
|
490
638
|
chromiumInstalled = !!pw.chromium.executablePath();
|
|
491
639
|
}
|
|
492
640
|
catch {
|
|
493
641
|
chromiumInstalled = false;
|
|
494
642
|
}
|
|
643
|
+
const runtimeWarnings = this.config.browserRuntime === 'patchright'
|
|
644
|
+
? [
|
|
645
|
+
'Patchright is Chromium-only and disables Playwright console APIs to avoid Runtime.enable detection.',
|
|
646
|
+
...(this.config.channel !== 'chrome' || this.config.headless
|
|
647
|
+
? ['Patchright stealth is strongest with channel=chrome and headless=false; current settings favor automation/test compatibility.']
|
|
648
|
+
: []),
|
|
649
|
+
]
|
|
650
|
+
: [];
|
|
495
651
|
return {
|
|
496
652
|
enabled: this.config.enabled,
|
|
497
653
|
channel: this.config.channel,
|
|
654
|
+
browserRuntime: this.config.browserRuntime,
|
|
655
|
+
runtimeWarnings,
|
|
498
656
|
headless: this.config.headless,
|
|
499
657
|
opencliEnabled: this.config.opencliEnabled,
|
|
500
658
|
automationMode: this.config.automationMode,
|
|
@@ -504,6 +662,8 @@ export class BrowserService {
|
|
|
504
662
|
externalUserscriptPolicy: this.config.automationMode === 'read-only' ? 'deny' : this.config.automationMode === 'unrestricted' ? 'allow' : 'ask',
|
|
505
663
|
opencliRunPolicy: this.config.automationMode === 'read-only' ? 'deny' : this.config.automationMode === 'unrestricted' ? 'allow' : 'ask',
|
|
506
664
|
chromiumInstalled,
|
|
665
|
+
usagePolicy: this.config.usagePolicy,
|
|
666
|
+
usageGovernor: this.usageGovernor.snapshot(),
|
|
507
667
|
authProfiles: this.authProfiles.list(),
|
|
508
668
|
rulePacks: Object.keys(this.config.rulePacks).sort(),
|
|
509
669
|
builtinScripts: BUILTIN_SCRIPTS.map(script => script.id),
|