dsh-browser-playwright-codex 0.7.4 → 0.7.6
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/CHANGELOG.md +60 -0
- package/README.md +10 -10
- package/SECURITY.md +1 -1
- package/lib/browser-lifecycle.d.ts +13 -1
- package/lib/browser-lifecycle.js +49 -5
- package/lib/errors.d.ts +12 -0
- package/lib/errors.js +16 -0
- package/lib/gates-patch.js +0 -1
- package/lib/index.d.ts +6 -3
- package/lib/index.js +4 -2
- package/lib/legacy-config.d.ts +93 -0
- package/lib/legacy-config.js +238 -0
- package/lib/navigation-guard.d.ts +48 -0
- package/lib/navigation-guard.js +48 -0
- package/lib/page-actions.js +6 -0
- package/lib/page-snapshot.js +5 -0
- package/lib/playwright.d.ts +1 -1
- package/lib/playwright.js +3 -1
- package/lib/plugin.js +5 -0
- package/lib/provider.d.ts +42 -0
- package/lib/provider.js +80 -1
- package/lib/session.d.ts +40 -4
- package/lib/session.js +95 -5
- package/lib/settings-card.js +15 -4
- package/lib/tool.js +79 -54
- package/lib/types.d.ts +28 -0
- package/lib/url-policy.d.ts +13 -0
- package/lib/url-policy.js +24 -2
- package/lib/wait-conditions.d.ts +55 -0
- package/lib/wait-conditions.js +141 -0
- package/package.json +1 -1
- package/scripts/dsh-browser-verify.mjs +34 -1
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { PlaywrightSession } from './session.ts';
|
|
2
|
+
import type { WaitCondition, WaitOutcome } from './types.ts';
|
|
3
|
+
/** Timeout used when the caller does not pass `timeoutMs`. */
|
|
4
|
+
export declare const DEFAULT_WAIT_TIMEOUT_MS = 15000;
|
|
5
|
+
/**
|
|
6
|
+
* Absolute ceiling for a goal wait at the session layer. The tool clamps to
|
|
7
|
+
* `tool.maxWaitMs` first; this only stops a programmatic caller from parking a
|
|
8
|
+
* session for an hour on a selector that will never appear.
|
|
9
|
+
*/
|
|
10
|
+
export declare const SESSION_WAIT_CAP_MS = 120000;
|
|
11
|
+
/** One validated wait, ready to run. */
|
|
12
|
+
export type NormalizedWait = {
|
|
13
|
+
readonly kind: 'ms';
|
|
14
|
+
readonly ms: number;
|
|
15
|
+
} | {
|
|
16
|
+
readonly kind: 'selector';
|
|
17
|
+
readonly selector: string;
|
|
18
|
+
readonly timeoutMs: number;
|
|
19
|
+
} | {
|
|
20
|
+
readonly kind: 'text';
|
|
21
|
+
readonly text: string;
|
|
22
|
+
readonly timeoutMs: number;
|
|
23
|
+
} | {
|
|
24
|
+
readonly kind: 'networkIdle';
|
|
25
|
+
readonly timeoutMs: number;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Validate and normalize one wait request.
|
|
29
|
+
*
|
|
30
|
+
* Exactly one condition is allowed per call. Accepting several would make the
|
|
31
|
+
* composed semantics ("all of them? in which order? what if the first never
|
|
32
|
+
* comes?") something the model has to guess, and the tool description would
|
|
33
|
+
* have to grow to explain it.
|
|
34
|
+
*
|
|
35
|
+
* @param condition - what the caller asked for
|
|
36
|
+
* @param maxWaitMs - deployment cap (`tool.maxWaitMs`)
|
|
37
|
+
* @returns the validated wait
|
|
38
|
+
* @throws Error with an actionable message when the request is malformed
|
|
39
|
+
*/
|
|
40
|
+
export declare function normalizeWaitCondition(condition: WaitCondition, maxWaitMs: number): NormalizedWait;
|
|
41
|
+
/** Human name of what a normalized wait is waiting for. */
|
|
42
|
+
export declare function describeWait(wait: NormalizedWait): string;
|
|
43
|
+
/**
|
|
44
|
+
* Run one validated wait against the session's active page.
|
|
45
|
+
*
|
|
46
|
+
* A timeout is reported as `ACTION_TIMEOUT` naming what was awaited: the model
|
|
47
|
+
* then knows the page is still working rather than that its action failed, and
|
|
48
|
+
* its next move is a snapshot instead of a blind retry.
|
|
49
|
+
*
|
|
50
|
+
* @param session - the session whose active page to wait on
|
|
51
|
+
* @param wait - a value from {@link normalizeWaitCondition}
|
|
52
|
+
* @param signal - abort signal from the tool call
|
|
53
|
+
* @returns what was waited for and how long it took
|
|
54
|
+
*/
|
|
55
|
+
export declare function waitForCondition(session: PlaywrightSession, wait: NormalizedWait, signal?: AbortSignal): Promise<WaitOutcome>;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Goal-directed waits behind `browser_wait`.
|
|
3
|
+
*
|
|
4
|
+
* The original tool could only sleep for a fixed number of milliseconds, which
|
|
5
|
+
* forces the model to guess: too short and the click lands on a half-rendered
|
|
6
|
+
* page, too long and every call burns wall-clock time for nothing. These three
|
|
7
|
+
* conditions replace the guess with the actual thing being waited for:
|
|
8
|
+
*
|
|
9
|
+
* - `selector`: an element matching a CSS selector becomes visible
|
|
10
|
+
* - `text`: visible text containing a string appears
|
|
11
|
+
* - `networkIdle`: the page stops issuing requests for 500ms
|
|
12
|
+
*
|
|
13
|
+
* `ms` stays supported exactly as before, so every existing call keeps working
|
|
14
|
+
* (`session.wait(ms)` is untouched; this is a separate entry point).
|
|
15
|
+
*
|
|
16
|
+
* Validation is separate from execution on purpose: a malformed call must fail
|
|
17
|
+
* before a browser is launched.
|
|
18
|
+
*
|
|
19
|
+
* @module dsh-browser-playwright-codex/wait-conditions
|
|
20
|
+
*/
|
|
21
|
+
import { BrowserError } from "./errors.js";
|
|
22
|
+
import { withAbort } from "./page-actions.js";
|
|
23
|
+
/** Timeout used when the caller does not pass `timeoutMs`. */
|
|
24
|
+
export const DEFAULT_WAIT_TIMEOUT_MS = 15_000;
|
|
25
|
+
/**
|
|
26
|
+
* Absolute ceiling for a goal wait at the session layer. The tool clamps to
|
|
27
|
+
* `tool.maxWaitMs` first; this only stops a programmatic caller from parking a
|
|
28
|
+
* session for an hour on a selector that will never appear.
|
|
29
|
+
*/
|
|
30
|
+
export const SESSION_WAIT_CAP_MS = 120_000;
|
|
31
|
+
/**
|
|
32
|
+
* Validate and normalize one wait request.
|
|
33
|
+
*
|
|
34
|
+
* Exactly one condition is allowed per call. Accepting several would make the
|
|
35
|
+
* composed semantics ("all of them? in which order? what if the first never
|
|
36
|
+
* comes?") something the model has to guess, and the tool description would
|
|
37
|
+
* have to grow to explain it.
|
|
38
|
+
*
|
|
39
|
+
* @param condition - what the caller asked for
|
|
40
|
+
* @param maxWaitMs - deployment cap (`tool.maxWaitMs`)
|
|
41
|
+
* @returns the validated wait
|
|
42
|
+
* @throws Error with an actionable message when the request is malformed
|
|
43
|
+
*/
|
|
44
|
+
export function normalizeWaitCondition(condition, maxWaitMs) {
|
|
45
|
+
const given = [
|
|
46
|
+
condition.ms === undefined ? undefined : 'ms',
|
|
47
|
+
condition.selector === undefined ? undefined : 'selector',
|
|
48
|
+
condition.text === undefined ? undefined : 'text',
|
|
49
|
+
condition.networkIdle === true ? 'networkIdle' : undefined,
|
|
50
|
+
].filter((name) => name !== undefined);
|
|
51
|
+
if (given.length === 0) {
|
|
52
|
+
throw new Error('browser_wait: pass one of ms, selector, text or networkIdle — sleeping without a condition is the guess this tool exists to remove');
|
|
53
|
+
}
|
|
54
|
+
if (given.length > 1) {
|
|
55
|
+
throw new Error('browser_wait: pass only one condition per call (received ' +
|
|
56
|
+
given.join(', ') +
|
|
57
|
+
'); call it again for the next one');
|
|
58
|
+
}
|
|
59
|
+
const cap = Math.max(1, Math.floor(maxWaitMs));
|
|
60
|
+
if (condition.ms !== undefined) {
|
|
61
|
+
if (!Number.isSafeInteger(condition.ms) || condition.ms <= 0 || condition.ms > cap) {
|
|
62
|
+
throw new Error('browser_wait: ms must be a positive integer no larger than ' + String(cap));
|
|
63
|
+
}
|
|
64
|
+
return { kind: 'ms', ms: condition.ms };
|
|
65
|
+
}
|
|
66
|
+
const requested = condition.timeoutMs ?? DEFAULT_WAIT_TIMEOUT_MS;
|
|
67
|
+
if (!Number.isSafeInteger(requested) || requested <= 0) {
|
|
68
|
+
throw new Error('browser_wait: timeoutMs must be a positive integer');
|
|
69
|
+
}
|
|
70
|
+
const timeoutMs = Math.min(requested, cap);
|
|
71
|
+
if (condition.selector !== undefined) {
|
|
72
|
+
const selector = condition.selector.trim();
|
|
73
|
+
if (selector === '')
|
|
74
|
+
throw new Error('browser_wait: selector must not be empty');
|
|
75
|
+
return { kind: 'selector', selector, timeoutMs };
|
|
76
|
+
}
|
|
77
|
+
if (condition.text !== undefined) {
|
|
78
|
+
const text = condition.text.trim();
|
|
79
|
+
if (text === '')
|
|
80
|
+
throw new Error('browser_wait: text must not be empty');
|
|
81
|
+
return { kind: 'text', text, timeoutMs };
|
|
82
|
+
}
|
|
83
|
+
return { kind: 'networkIdle', timeoutMs };
|
|
84
|
+
}
|
|
85
|
+
/** Human name of what a normalized wait is waiting for. */
|
|
86
|
+
export function describeWait(wait) {
|
|
87
|
+
switch (wait.kind) {
|
|
88
|
+
case 'ms':
|
|
89
|
+
return 'a fixed ' + String(wait.ms) + 'ms';
|
|
90
|
+
case 'selector':
|
|
91
|
+
return 'selector ' + JSON.stringify(wait.selector) + ' to become visible';
|
|
92
|
+
case 'text':
|
|
93
|
+
return 'text ' + JSON.stringify(wait.text) + ' to become visible';
|
|
94
|
+
case 'networkIdle':
|
|
95
|
+
return 'the network to go idle (no request for 500ms)';
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Run one validated wait against the session's active page.
|
|
100
|
+
*
|
|
101
|
+
* A timeout is reported as `ACTION_TIMEOUT` naming what was awaited: the model
|
|
102
|
+
* then knows the page is still working rather than that its action failed, and
|
|
103
|
+
* its next move is a snapshot instead of a blind retry.
|
|
104
|
+
*
|
|
105
|
+
* @param session - the session whose active page to wait on
|
|
106
|
+
* @param wait - a value from {@link normalizeWaitCondition}
|
|
107
|
+
* @param signal - abort signal from the tool call
|
|
108
|
+
* @returns what was waited for and how long it took
|
|
109
|
+
*/
|
|
110
|
+
export async function waitForCondition(session, wait, signal) {
|
|
111
|
+
const started = Date.now();
|
|
112
|
+
return session.run(async () => {
|
|
113
|
+
session.provider.touch(session.owner);
|
|
114
|
+
if (wait.kind === 'ms') {
|
|
115
|
+
await withAbort(new Promise((resolve) => setTimeout(resolve, wait.ms)), signal);
|
|
116
|
+
return { waitedFor: describeWait(wait), elapsedMs: Date.now() - started };
|
|
117
|
+
}
|
|
118
|
+
const page = await session.ensurePage();
|
|
119
|
+
try {
|
|
120
|
+
if (wait.kind === 'selector') {
|
|
121
|
+
await withAbort(page.waitForSelector(wait.selector, { state: 'visible', timeout: wait.timeoutMs }), signal);
|
|
122
|
+
}
|
|
123
|
+
else if (wait.kind === 'text') {
|
|
124
|
+
await withAbort(page.getByText(wait.text).first().waitFor({ state: 'visible', timeout: wait.timeoutMs }), signal);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
await withAbort(page.waitForLoadState('networkidle', { timeout: wait.timeoutMs }), signal);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
if (signal?.aborted === true)
|
|
132
|
+
throw error;
|
|
133
|
+
throw new BrowserError('ACTION_TIMEOUT', 'wait timed out after ' +
|
|
134
|
+
String(wait.timeoutMs) +
|
|
135
|
+
'ms waiting for ' +
|
|
136
|
+
describeWait(wait) +
|
|
137
|
+
'; the page may still be loading — take a snapshot to see where it stands instead of retrying blind');
|
|
138
|
+
}
|
|
139
|
+
return { waitedFor: describeWait(wait), elapsedMs: Date.now() - started };
|
|
140
|
+
});
|
|
141
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-browser-playwright-codex",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.6",
|
|
4
4
|
"description": "Browser automation for DeepSeek Harness, one plugin: fill in forms, scrape lists and work across tabs in real pages, using the logins you already have. Runs in a visible window that stays quiet when minimised; native dialogs are parked instead of confirmed, CAPTCHA pages are flagged with a note that says stop and hand the page back, password / one-time-code / card fields are refused by browser_fill, and the Settings card (enable/disable, page-reading mode, capability gates) now lives inside this package.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -28,7 +28,7 @@ import path from 'node:path'
|
|
|
28
28
|
import { PlaywrightProvider } from '../lib/provider.js'
|
|
29
29
|
import { Config as ConfigSchema } from '../lib/config.js'
|
|
30
30
|
import { renderSnapshot } from '../lib/snapshot-render.js'
|
|
31
|
-
import { getEffectiveEngine, setSnapshotEngine } from '../lib/runtime-state.js'
|
|
31
|
+
import { getEffectiveEngine, publishGates, setSnapshotEngine } from '../lib/runtime-state.js'
|
|
32
32
|
|
|
33
33
|
// 验收必须只看代码,不看这台机器上用户的选择。
|
|
34
34
|
// 设置页的「页面识别方式」存在 ~/.dsh/dsh-browser-playwright.state.json,运行时状态层
|
|
@@ -128,6 +128,36 @@ const provider = new PlaywrightProvider(
|
|
|
128
128
|
)
|
|
129
129
|
const session = await provider.acquire('verify')
|
|
130
130
|
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
// [0] 门禁在代码层也成立(2026-10-26 之前的漏洞:开关只在工具层挡)
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
// 本脚本不经过工具层,直接调 session —— 正是当初绕过门禁的那条路。默认(没发布门禁)
|
|
135
|
+
// 必须拒绝;确认之后再把门禁打开,好让后面的用例能读页面状态。
|
|
136
|
+
console.log('[0] 门禁:没开门禁时代码层也拒绝')
|
|
137
|
+
let evaluateGate = ''
|
|
138
|
+
try {
|
|
139
|
+
await session.evaluate('1 + 1')
|
|
140
|
+
} catch (e) {
|
|
141
|
+
evaluateGate = e.code
|
|
142
|
+
}
|
|
143
|
+
check('evaluate 未开门禁时报 EVALUATE_DISABLED', evaluateGate === 'EVALUATE_DISABLED', evaluateGate)
|
|
144
|
+
let cdpGate = ''
|
|
145
|
+
try {
|
|
146
|
+
await session.cdp('DOM.getDocument', { depth: 1 })
|
|
147
|
+
} catch (e) {
|
|
148
|
+
cdpGate = e.code
|
|
149
|
+
}
|
|
150
|
+
check('cdp 未开门禁时报 CDP_DISABLED', cdpGate === 'CDP_DISABLED', cdpGate)
|
|
151
|
+
publishGates({ allowEvaluate: true, allowCdp: true, diagnostics: true })
|
|
152
|
+
let cdpDenied = ''
|
|
153
|
+
try {
|
|
154
|
+
// 门禁打开也仍然要过白名单:Runtime.evaluate 是任意页面 JS,永远不在名单里。
|
|
155
|
+
await session.cdp('Runtime.evaluate', { expression: 'document.cookie', returnByValue: true })
|
|
156
|
+
} catch (e) {
|
|
157
|
+
cdpDenied = e.code
|
|
158
|
+
}
|
|
159
|
+
check('开门禁后 Runtime.evaluate 仍报 CDP_DENIED', cdpDenied === 'CDP_DENIED', cdpDenied)
|
|
160
|
+
|
|
131
161
|
console.log('[1] 快照取名与折叠')
|
|
132
162
|
const s0 = await session.navigate(URL_, 'load')
|
|
133
163
|
const tree = renderSnapshot(s0)
|
|
@@ -366,6 +396,9 @@ const expectedTools = contractMod.registeredToolNames(toolConfig.toolPrefix, {
|
|
|
366
396
|
registerDisabledTools: toolConfig.registerDisabledTools,
|
|
367
397
|
})
|
|
368
398
|
const byName = (n) => reg.find((t) => t.name === n)
|
|
399
|
+
// apply() 会把有效门禁刷成这份配置里的值(这里出厂默认是关),而下面的用例直接调
|
|
400
|
+
// session 而不是走工具层,所以再打开一次。
|
|
401
|
+
publishGates({ allowEvaluate: true, allowCdp: true, diagnostics: true })
|
|
369
402
|
check('存在 browser_dialog', byName('browser_dialog') !== undefined)
|
|
370
403
|
check(
|
|
371
404
|
'browser_dialog 有 accept 参数',
|