dsh-browser-playwright-codex 0.8.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/NOTICE.md +10 -10
- package/README.md +12 -12
- package/assets/swarm-rebuild.js +3864 -0
- package/lib/challenge.d.ts +7 -4
- package/lib/challenge.js +28 -6
- package/lib/client.js +28 -2
- package/lib/config.d.ts +1 -1
- package/lib/config.js +1 -2
- package/lib/errors.d.ts +17 -0
- package/lib/errors.js +28 -0
- package/lib/gates-patch.d.ts +41 -1
- package/lib/gates-patch.js +103 -5
- package/lib/injected.js +45 -0
- package/lib/page-actions.d.ts +24 -0
- package/lib/page-actions.js +41 -10
- package/lib/session.js +55 -9
- package/lib/snapshot-render.js +52 -7
- package/lib/swarm-contract.d.ts +11 -5
- package/lib/swarm-contract.js +11 -5
- package/lib/swarm-curtain.js +2 -2
- package/lib/swarm-intro.js +7 -5
- package/lib/tool-types.d.ts +10 -0
- package/lib/tool.js +6 -2
- package/lib/types.d.ts +6 -0
- package/package.json +15 -12
package/lib/session.js
CHANGED
|
@@ -240,7 +240,12 @@ export class PlaywrightSession {
|
|
|
240
240
|
}
|
|
241
241
|
/** Element-level stability before a pointer/keyboard action, bounded. Returns true when stable. */
|
|
242
242
|
async waitForElementStable(locator) {
|
|
243
|
-
|
|
243
|
+
// locator.evaluate has no timeout of its own either: the probe runs page
|
|
244
|
+
// script, so a wedged main thread makes it hang before the action it is
|
|
245
|
+
// supposed to protect. Degrade to "not stable" and let the action's own
|
|
246
|
+
// Playwright timeout report the real problem (same contract as before, just
|
|
247
|
+
// no longer unbounded).
|
|
248
|
+
const ok = await withDeadline(locator.evaluate(elementStabilityProbe, { frames: 2, timeoutMs: 1500 }).catch(() => false), Math.max(3000, this.timeoutMs()), 'waiting for the element to hold still', this.currentSignal, 'resolve-undefined').catch(() => false);
|
|
244
249
|
return ok === true;
|
|
245
250
|
}
|
|
246
251
|
/** Post-action settle: wait out a real navigation, then render-stabilize before snapshotting. */
|
|
@@ -579,7 +584,11 @@ export class PlaywrightSession {
|
|
|
579
584
|
if (page === undefined)
|
|
580
585
|
continue;
|
|
581
586
|
this.guardBlockedDialog(page);
|
|
582
|
-
|
|
587
|
+
// page.title() is a page round trip with no timeout: one tab running a
|
|
588
|
+
// stuck script used to hang the whole tab list, and every tab after it.
|
|
589
|
+
// An unreadable title degrades to "" — the URL still identifies the tab.
|
|
590
|
+
const title = await withDeadline(page.title().catch(() => ''), Math.max(2000, Math.min(this.timeoutMs(), 5000)), 'reading the title of tab ' + String(index), this.currentSignal, 'resolve-undefined').catch(() => undefined);
|
|
591
|
+
out.push({ index, url: page.url(), title: title ?? '', active: index === activeIndex });
|
|
583
592
|
}
|
|
584
593
|
return out;
|
|
585
594
|
});
|
|
@@ -623,7 +632,29 @@ export class PlaywrightSession {
|
|
|
623
632
|
});
|
|
624
633
|
}
|
|
625
634
|
async pageData() {
|
|
626
|
-
|
|
635
|
+
// page.evaluate again: the extract walker is page script, so a stuck tab used
|
|
636
|
+
// to hang browser_extract exactly like a snapshot read. Same deadline, and on
|
|
637
|
+
// timeout the same cleanup — drop the handle and the page so the next call
|
|
638
|
+
// does not inherit a page that will never answer.
|
|
639
|
+
try {
|
|
640
|
+
return await withDeadline(pageData(this), this.timeoutMs(), 'reading the page content', this.currentSignal, 'reject', 'That page has been closed; anything typed into it but not submitted is lost.');
|
|
641
|
+
}
|
|
642
|
+
catch (error) {
|
|
643
|
+
if (isReadTimeout(error)) {
|
|
644
|
+
this.closed = true;
|
|
645
|
+
this.provider.invalidateOwner(this.owner);
|
|
646
|
+
let page;
|
|
647
|
+
try {
|
|
648
|
+
page = this.currentPageSync();
|
|
649
|
+
}
|
|
650
|
+
catch {
|
|
651
|
+
page = undefined;
|
|
652
|
+
}
|
|
653
|
+
if (page !== undefined)
|
|
654
|
+
await this.discardStuckPage(page);
|
|
655
|
+
}
|
|
656
|
+
throw error;
|
|
657
|
+
}
|
|
627
658
|
}
|
|
628
659
|
async evaluate(expression, signal) {
|
|
629
660
|
// The switch is enforced HERE, not only in the tool layer: browser_evaluate
|
|
@@ -639,14 +670,26 @@ export class PlaywrightSession {
|
|
|
639
670
|
this.provider.touch(this.owner);
|
|
640
671
|
const page = await this.ensurePage();
|
|
641
672
|
const fn = new Function('return (' + expression + ')');
|
|
673
|
+
// page.evaluate takes no timeout: an expression with an infinite loop (a
|
|
674
|
+
// shader compile, a spin on a mutex) never returns and the tool call hangs
|
|
675
|
+
// forever. Bound it with the same budget the snapshot read uses, and on
|
|
676
|
+
// timeout drop this handle + the stuck page so the next call starts clean
|
|
677
|
+
// instead of inheriting a page that will never answer.
|
|
678
|
+
const once = async () => (await withDeadline(withAbort(page.evaluate(fn), signal), this.timeoutMs(), 'evaluating page JavaScript', undefined, 'reject', 'That page has been closed; anything typed into it but not submitted is lost, and the expression may have left the page unresponsive.'));
|
|
642
679
|
try {
|
|
643
|
-
return await
|
|
680
|
+
return await once();
|
|
644
681
|
}
|
|
645
682
|
catch (error) {
|
|
683
|
+
if (isReadTimeout(error)) {
|
|
684
|
+
this.closed = true;
|
|
685
|
+
this.provider.invalidateOwner(this.owner);
|
|
686
|
+
await this.discardStuckPage(page);
|
|
687
|
+
throw error;
|
|
688
|
+
}
|
|
646
689
|
if (!isContextDestroyed(error) || signal?.aborted === true)
|
|
647
690
|
throw error;
|
|
648
691
|
await this.settleNavigation(page);
|
|
649
|
-
return
|
|
692
|
+
return once();
|
|
650
693
|
}
|
|
651
694
|
});
|
|
652
695
|
}
|
|
@@ -688,13 +731,16 @@ export class PlaywrightSession {
|
|
|
688
731
|
// inside the driver, and the failure gets reported as a protocol error.
|
|
689
732
|
const session = this.cdpSession;
|
|
690
733
|
const send = session.send.bind(session);
|
|
691
|
-
|
|
734
|
+
// A CDP command has no timeout of its own either, and some of them
|
|
735
|
+
// (screenshots, layout dumps) run page script internally. Same budget as
|
|
736
|
+
// a page read; the CDP session itself stays usable, only this call dies.
|
|
737
|
+
return await withDeadline(withAbort(send(method, params ?? {}), signal), this.timeoutMs(), 'running CDP command ' + method, undefined, 'reject', 'The command was abandoned; the page may still be working on it. Take a fresh snapshot to see where it landed.');
|
|
692
738
|
}
|
|
693
739
|
catch (error) {
|
|
694
|
-
|
|
740
|
+
// A deadline and a cancel both mean what they say: never relabel them as
|
|
741
|
+
// a protocol failure (the wrapping below only owns real CDP errors).
|
|
742
|
+
if (isAbortError(error) || isReadTimeout(error))
|
|
695
743
|
throw error;
|
|
696
|
-
// CDP protocol errors are already structured ("Protocol error
|
|
697
|
-
// (DOM.foo): ..."); wrap non-abort failures in a stable code.
|
|
698
744
|
const detail = error instanceof Error ? error.message : String(error);
|
|
699
745
|
if (/Protocol error/.test(detail)) {
|
|
700
746
|
const m = /\(([^)]+)\): ([\s\S]+)/.exec(detail);
|
package/lib/snapshot-render.js
CHANGED
|
@@ -42,6 +42,21 @@ const TRACKING_PARAMS = new Set([
|
|
|
42
42
|
]);
|
|
43
43
|
/** Default cap for a rendered href, in characters. */
|
|
44
44
|
export const MAX_HREF_LENGTH = 160;
|
|
45
|
+
/**
|
|
46
|
+
* Path shapes that mark a redirect-wrapping URL — a link that does not point at
|
|
47
|
+
* its destination but at the site's own click-tracking endpoint, which forwards
|
|
48
|
+
* there. Search engines wrap every organic result this way (Bing `/ck/a?...`,
|
|
49
|
+
* Google `/url?q=...`, Facebook `l.php?u=...`). The real destination is encoded
|
|
50
|
+
* in a long query parameter, so the rendered href is 160 characters of base64
|
|
51
|
+
* that says nothing the link's own name does not — and it repeats for every
|
|
52
|
+
* result on the page.
|
|
53
|
+
*
|
|
54
|
+
* This is a generic category (a tracking endpoint on the same origin as the
|
|
55
|
+
* page), not a per-site special case. When one matches we render a short marker
|
|
56
|
+
* instead of the wrapped URL: the ref is what clicks, and the link's name plus
|
|
57
|
+
* any destination node already carry the meaning.
|
|
58
|
+
*/
|
|
59
|
+
const REDIRECT_WRAPPERS = [/\/ck\/a\?/i, /\/url\?q=/i, /l\.php\?u=/i];
|
|
45
60
|
/** Clip a string to a display budget, marking the cut. */
|
|
46
61
|
function clip(value, maxLength) {
|
|
47
62
|
return value.length <= maxLength ? value : value.slice(0, maxLength - 1) + '…';
|
|
@@ -56,6 +71,13 @@ function clip(value, maxLength) {
|
|
|
56
71
|
* @returns the shortened href
|
|
57
72
|
*/
|
|
58
73
|
export function trimUrlForDisplay(href, maxLength = MAX_HREF_LENGTH) {
|
|
74
|
+
// A redirect-wrapping href (search-engine click tracker) is 160 characters of
|
|
75
|
+
// encoded destination that says nothing the link's name does not. Render a
|
|
76
|
+
// short marker instead; the ref is what clicks, so this never changes where a
|
|
77
|
+
// click lands — only what the model reads.
|
|
78
|
+
if (REDIRECT_WRAPPERS.some((re) => re.test(href))) {
|
|
79
|
+
return '→ (redirect wrapper)';
|
|
80
|
+
}
|
|
59
81
|
const hashAt = href.indexOf('#');
|
|
60
82
|
const hash = hashAt >= 0 ? href.slice(hashAt) : '';
|
|
61
83
|
const head = hashAt >= 0 ? href.slice(0, hashAt) : href;
|
|
@@ -94,13 +116,14 @@ function isTransparentWrapper(node) {
|
|
|
94
116
|
node.level === undefined &&
|
|
95
117
|
node.checked === undefined &&
|
|
96
118
|
node.selected === undefined &&
|
|
97
|
-
node.disabled === undefined
|
|
119
|
+
node.disabled === undefined &&
|
|
120
|
+
node.value === undefined);
|
|
98
121
|
}
|
|
99
122
|
/** Render one node line into the accumulating line list. */
|
|
100
|
-
function renderNode(node, depth, lines) {
|
|
123
|
+
function renderNode(node, depth, lines, seenHrefs) {
|
|
101
124
|
if (isTransparentWrapper(node)) {
|
|
102
125
|
for (const child of node.children)
|
|
103
|
-
renderNode(child, depth, lines);
|
|
126
|
+
renderNode(child, depth, lines, seenHrefs);
|
|
104
127
|
return;
|
|
105
128
|
}
|
|
106
129
|
const indent = ' '.repeat(depth);
|
|
@@ -116,15 +139,33 @@ function renderNode(node, depth, lines) {
|
|
|
116
139
|
flags.push('selected');
|
|
117
140
|
if (node.disabled === true)
|
|
118
141
|
flags.push('disabled');
|
|
142
|
+
// The current value of a text entry field: without it the model cannot see
|
|
143
|
+
// what a fill actually left behind, which read as "no visible change".
|
|
144
|
+
if (node.value !== undefined && node.value !== '')
|
|
145
|
+
flags.push('value=' + node.value);
|
|
119
146
|
if (node.ref !== undefined)
|
|
120
147
|
flags.push('ref=' + node.ref);
|
|
121
148
|
if (flags.length > 0)
|
|
122
149
|
line += ' [' + flags.join(', ') + ']';
|
|
123
|
-
if (node.href !== undefined)
|
|
124
|
-
|
|
150
|
+
if (node.href !== undefined) {
|
|
151
|
+
// Same href, second and later occurrence within ONE snapshot: the URL is
|
|
152
|
+
// already printed at its first occurrence, so repeat only a pointer to it.
|
|
153
|
+
// Every ref and element stays — this trims bytes, never click targets. Only
|
|
154
|
+
// absolute URLs are deduped (a bare "/docs" can name different pages).
|
|
155
|
+
const abs = /^https?:\/\//i.test(node.href);
|
|
156
|
+
const firstRef = abs ? seenHrefs.get(node.href) : undefined;
|
|
157
|
+
if (firstRef !== undefined && firstRef !== node.ref) {
|
|
158
|
+
line += ' -> (same as ' + firstRef + ')';
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
line += ' -> ' + trimUrlForDisplay(node.href);
|
|
162
|
+
if (abs && node.ref !== undefined)
|
|
163
|
+
seenHrefs.set(node.href, node.ref);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
125
166
|
lines.push(line);
|
|
126
167
|
for (const child of node.children)
|
|
127
|
-
renderNode(child, depth + 1, lines);
|
|
168
|
+
renderNode(child, depth + 1, lines, seenHrefs);
|
|
128
169
|
}
|
|
129
170
|
/**
|
|
130
171
|
* Render a node tree as indented lines, one element per line.
|
|
@@ -133,8 +174,12 @@ function renderNode(node, depth, lines) {
|
|
|
133
174
|
*/
|
|
134
175
|
export function renderSnapshotTree(nodes) {
|
|
135
176
|
const lines = [];
|
|
177
|
+
// href -> ref of its first occurrence in THIS snapshot. Same href second and
|
|
178
|
+
// later renders as "(same as <ref>)" instead of repeating the URL. Scoped to
|
|
179
|
+
// one snapshot on purpose: a fresh snapshot after navigation re-prints URLs.
|
|
180
|
+
const seenHrefs = new Map();
|
|
136
181
|
for (const node of nodes)
|
|
137
|
-
renderNode(node, 0, lines);
|
|
182
|
+
renderNode(node, 0, lines, seenHrefs);
|
|
138
183
|
return lines.join('\n');
|
|
139
184
|
}
|
|
140
185
|
/**
|
package/lib/swarm-contract.d.ts
CHANGED
|
@@ -27,9 +27,11 @@
|
|
|
27
27
|
* `assets/swarm-rebuild.upstream.js` 里逐条核对下面的每个名字是否真的存在 —— 上游换人、
|
|
28
28
|
* 改名、删节点,这里第一个报错,而不是等某天浏览器里莫名其妙不播了。
|
|
29
29
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
30
|
+
* 法律面(0.8.1 起):上游仓库根目录**仍然没有 LICENSE**(默认保留所有权利),
|
|
31
|
+
* 但 Dhvanil Patel 于 2026-11-11 邮件书面许可本项目**打包与修改**,Joel Califa 于
|
|
32
|
+
* 2026-11-08 书面许可并仅要求署名。所以资产可以随包分发,**但授权不是 MIT**:
|
|
33
|
+
* 它是邮件许可,只覆盖本项目,不给第三方通用许可。别把这份资产 fork 成
|
|
34
|
+
* "我们自己的版本" —— fork 不改变任何人拿到的权利,只会多出一份没有许可的副本。
|
|
33
35
|
*/
|
|
34
36
|
/** 上游出处与入仓指纹。这三项之外的任何东西,都不该成为插件的依赖。 */
|
|
35
37
|
export declare const SWARM_UPSTREAM: {
|
|
@@ -45,8 +47,12 @@ export declare const SWARM_UPSTREAM: {
|
|
|
45
47
|
readonly sha256: "6a30f19daaa182d52673035feaeae775c398fa426655cc71a87a451fe00c7bfe";
|
|
46
48
|
/** 补丁后的产出(**随包分发的那份**)。 */
|
|
47
49
|
readonly patchedFile: "assets/swarm-rebuild.js";
|
|
48
|
-
/**
|
|
49
|
-
|
|
50
|
+
/**
|
|
51
|
+
* 授权状态:`none` = 仓库没有许可证;`permission` = 无许可证文本,但有作者的书面
|
|
52
|
+
* 邮件许可(本项目:Dhvanil 2026-11-11 打包 + 修改;Joel 2026-11-08 效果 + 署名)。
|
|
53
|
+
* 它不是"宽松许可",第三方不能据此复用。
|
|
54
|
+
*/
|
|
55
|
+
readonly license: "permission";
|
|
50
56
|
};
|
|
51
57
|
/**
|
|
52
58
|
* Tier A —— 硬接口。上游官方句柄树,补丁完全没碰,插件主路径全走这里。
|
package/lib/swarm-contract.js
CHANGED
|
@@ -27,9 +27,11 @@
|
|
|
27
27
|
* `assets/swarm-rebuild.upstream.js` 里逐条核对下面的每个名字是否真的存在 —— 上游换人、
|
|
28
28
|
* 改名、删节点,这里第一个报错,而不是等某天浏览器里莫名其妙不播了。
|
|
29
29
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
30
|
+
* 法律面(0.8.1 起):上游仓库根目录**仍然没有 LICENSE**(默认保留所有权利),
|
|
31
|
+
* 但 Dhvanil Patel 于 2026-11-11 邮件书面许可本项目**打包与修改**,Joel Califa 于
|
|
32
|
+
* 2026-11-08 书面许可并仅要求署名。所以资产可以随包分发,**但授权不是 MIT**:
|
|
33
|
+
* 它是邮件许可,只覆盖本项目,不给第三方通用许可。别把这份资产 fork 成
|
|
34
|
+
* "我们自己的版本" —— fork 不改变任何人拿到的权利,只会多出一份没有许可的副本。
|
|
33
35
|
*/
|
|
34
36
|
/** 上游出处与入仓指纹。这三项之外的任何东西,都不该成为插件的依赖。 */
|
|
35
37
|
export const SWARM_UPSTREAM = {
|
|
@@ -45,8 +47,12 @@ export const SWARM_UPSTREAM = {
|
|
|
45
47
|
sha256: '6a30f19daaa182d52673035feaeae775c398fa426655cc71a87a451fe00c7bfe',
|
|
46
48
|
/** 补丁后的产出(**随包分发的那份**)。 */
|
|
47
49
|
patchedFile: 'assets/swarm-rebuild.js',
|
|
48
|
-
/**
|
|
49
|
-
|
|
50
|
+
/**
|
|
51
|
+
* 授权状态:`none` = 仓库没有许可证;`permission` = 无许可证文本,但有作者的书面
|
|
52
|
+
* 邮件许可(本项目:Dhvanil 2026-11-11 打包 + 修改;Joel 2026-11-08 效果 + 署名)。
|
|
53
|
+
* 它不是"宽松许可",第三方不能据此复用。
|
|
54
|
+
*/
|
|
55
|
+
license: 'permission',
|
|
50
56
|
};
|
|
51
57
|
/**
|
|
52
58
|
* Tier A —— 硬接口。上游官方句柄树,补丁完全没碰,插件主路径全走这里。
|
package/lib/swarm-curtain.js
CHANGED
|
@@ -36,11 +36,11 @@ export function swarmBootScript() {
|
|
|
36
36
|
asset = fs.readFileSync(fileURLToPath(new URL('../assets/swarm-rebuild.js', import.meta.url)), 'utf8');
|
|
37
37
|
}
|
|
38
38
|
catch {
|
|
39
|
-
//
|
|
39
|
+
// 资产读不到(0.8.1 起随包分发,理论上不会发生;手工删包时会)。这里**绝不能往外抛**:
|
|
40
40
|
// 两个调用点都是 `await ctx.addInitScript({ content: swarmBootScript() })`,
|
|
41
41
|
// 抛出去就是上下文创建失败、浏览器直接打不开 —— 把"动效没了"放大成"插件挂了"。
|
|
42
42
|
// 返回一段空注释脚本,等于这次启动不注入任何东西,行为与"没开这个功能"一致。
|
|
43
|
-
return '/* dsh: swarm asset
|
|
43
|
+
return '/* dsh: swarm asset missing — nothing injected */\n';
|
|
44
44
|
}
|
|
45
45
|
return ('if (window.top === window && typeof window.name === "string" &&\n' +
|
|
46
46
|
' window.name.indexOf("' +
|
package/lib/swarm-intro.js
CHANGED
|
@@ -4,11 +4,13 @@
|
|
|
4
4
|
* 这不是扩展。效果本体是 assets/swarm-rebuild.js,由 Playwright 直接注入当前
|
|
5
5
|
* 页面执行,所以它跑在插件自己那个窗口里 —— 也就是用户已经登录的那个浏览器。
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
* github.com/DhvanilPatel/swarm-cursors
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
7
|
+
* **授权状态(0.8.1 起已解决)**:那份脚本来自 Dhvanil Patel 的
|
|
8
|
+
* github.com/DhvanilPatel/swarm-cursors。2026-11-11 他邮件书面许可本项目
|
|
9
|
+
* **打包与修改**("you are welcome to bundle and modify it as needed"),
|
|
10
|
+
* 所以资产重新随 npm 包分发;许可没有附 MIT 许可文本,也只覆盖本项目,
|
|
11
|
+
* 不给第三方通用许可。底层的动效设计与编排来自 Joel Califa 的
|
|
12
|
+
* califa/swarm-site,他 2026-11-08 已书面许可、唯一条件是署名。
|
|
13
|
+
* 功能仍然**默认关闭**(config.swarm.enabled),打开才播。
|
|
12
14
|
*
|
|
13
15
|
* 三个约束决定了这里的实现方式:
|
|
14
16
|
*
|
package/lib/tool-types.d.ts
CHANGED
|
@@ -46,6 +46,16 @@ export interface ChallengeNode {
|
|
|
46
46
|
readonly name?: string;
|
|
47
47
|
/** Accessible role. Both built-in engines provide one; it gates the wording tier. */
|
|
48
48
|
readonly role?: string;
|
|
49
|
+
/** Whether a checkbox/radio is checked; present on toggles. */
|
|
50
|
+
readonly checked?: boolean;
|
|
51
|
+
/** Whether an option is selected; present on options. */
|
|
52
|
+
readonly selected?: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Current value of a textbox/textarea/select. The no-progress fingerprint
|
|
55
|
+
* includes it so that filling a field registers as change — otherwise a
|
|
56
|
+
* normal fill looks like "no visible change" and trips the anti-bot guess.
|
|
57
|
+
*/
|
|
58
|
+
readonly value?: string;
|
|
49
59
|
readonly children?: readonly ChallengeNode[];
|
|
50
60
|
}
|
|
51
61
|
/** The slice of a snapshot the challenge detector reads. */
|
package/lib/tool.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import z from '@deepseek-ai/schemastery';
|
|
2
2
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
3
3
|
import { BlockAssembler, createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
4
|
-
import { BrowserError, cdpDisabled, evaluateDisabled } from "./errors.js";
|
|
4
|
+
import { BrowserError, cdpDisabled, evaluateDisabled, normalizeEvaluateResult } from "./errors.js";
|
|
5
5
|
import { registeredSuffixes } from "./contract.js";
|
|
6
6
|
import { getEnabled, publishGates, publishLoginSites } from "./runtime-state.js";
|
|
7
7
|
import { summarizeLoginSites } from "./login-sites.js";
|
|
@@ -563,6 +563,9 @@ export function apply(ctx, config) {
|
|
|
563
563
|
registerMaybe('evaluate', defineTool({
|
|
564
564
|
name: p('evaluate'),
|
|
565
565
|
description: 'Evaluate one JavaScript expression in the page and return its JSON-serializable result. ' +
|
|
566
|
+
'Return value rules: an object, string, number, boolean or null comes back as-is; ' +
|
|
567
|
+
'a top-level ARRAY, undefined or a function is returned as a JSON string with a note saying it was converted ' +
|
|
568
|
+
'(wrap it yourself, e.g. {items: [...]} , if you need the array shape). ' +
|
|
566
569
|
'Disabled by default; the deployment must enable allowEvaluate. ' +
|
|
567
570
|
'IRON RULE: prefer read-only expressions to INSPECT state (value, outerHTML, checked, textContent) before ' +
|
|
568
571
|
'concluding an interaction failed. If you write to the DOM directly (setting value/textContent), ' +
|
|
@@ -590,7 +593,8 @@ export function apply(ctx, config) {
|
|
|
590
593
|
throw evaluateDisabled();
|
|
591
594
|
}
|
|
592
595
|
const session = await acquire(exec);
|
|
593
|
-
|
|
596
|
+
const raw = await session.evaluate(args.expression, exec.signal);
|
|
597
|
+
return normalizeEvaluateResult(raw);
|
|
594
598
|
},
|
|
595
599
|
}));
|
|
596
600
|
registerMaybe('cdp', defineTool({
|
package/lib/types.d.ts
CHANGED
|
@@ -21,6 +21,12 @@ export interface BrowserNode {
|
|
|
21
21
|
readonly selected?: boolean;
|
|
22
22
|
/** Whether the element is disabled. */
|
|
23
23
|
readonly disabled?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Current value of a textbox, textarea or select, clipped to the snapshot's
|
|
26
|
+
* text budget. Present so the model can see what a fill actually left behind
|
|
27
|
+
* and so the no-progress fingerprint registers a value change as change.
|
|
28
|
+
*/
|
|
29
|
+
readonly value?: string;
|
|
24
30
|
/** Href for links; present when the page is same-origin. */
|
|
25
31
|
readonly href?: string;
|
|
26
32
|
readonly children: readonly BrowserNode[];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-browser-playwright-codex",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.1",
|
|
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",
|
|
@@ -44,6 +44,8 @@
|
|
|
44
44
|
"CHANGELOG.md",
|
|
45
45
|
"SECURITY.md",
|
|
46
46
|
"NOTICE.md",
|
|
47
|
+
"assets",
|
|
48
|
+
"!assets/swarm-rebuild.upstream.js",
|
|
47
49
|
"locale",
|
|
48
50
|
"scripts/doctor.mjs",
|
|
49
51
|
"scripts/dsh-browser-verify.mjs"
|
|
@@ -70,19 +72,20 @@
|
|
|
70
72
|
"playwright-core": "~1.62.1"
|
|
71
73
|
},
|
|
72
74
|
"peerDependencies": {
|
|
73
|
-
"@deepseek-ai/cordis": ">=4.0
|
|
74
|
-
"@deepseek-ai/dsh-llm": ">=0.2.
|
|
75
|
-
"@deepseek-ai/dsh-tools": ">=0.2.
|
|
75
|
+
"@deepseek-ai/cordis": ">=4.1.0",
|
|
76
|
+
"@deepseek-ai/dsh-llm": ">=0.2.2-rc.1",
|
|
77
|
+
"@deepseek-ai/dsh-tools": ">=0.2.2-rc.1"
|
|
76
78
|
},
|
|
77
79
|
"devDependencies": {
|
|
78
|
-
"@deepseek-ai/cordis": "4.0
|
|
79
|
-
"@deepseek-ai/dsh-agent": "0.2.
|
|
80
|
-
"@deepseek-ai/dsh-attachment": "0.2.
|
|
81
|
-
"@deepseek-ai/dsh-llm": "0.2.
|
|
82
|
-
"@deepseek-ai/dsh-session": "0.2.
|
|
83
|
-
"@deepseek-ai/dsh-system-prompt": "0.2.
|
|
84
|
-
"@deepseek-ai/dsh-tools": "0.2.
|
|
85
|
-
"@deepseek-ai/
|
|
80
|
+
"@deepseek-ai/cordis": "4.1.0",
|
|
81
|
+
"@deepseek-ai/dsh-agent": "0.2.2-rc.1",
|
|
82
|
+
"@deepseek-ai/dsh-attachment": "0.2.2-rc.1",
|
|
83
|
+
"@deepseek-ai/dsh-llm": "0.2.2-rc.1",
|
|
84
|
+
"@deepseek-ai/dsh-session": "0.2.2-rc.1",
|
|
85
|
+
"@deepseek-ai/dsh-system-prompt": "0.2.2-rc.1",
|
|
86
|
+
"@deepseek-ai/dsh-tools": "0.2.2-rc.1",
|
|
87
|
+
"@deepseek-ai/dsh-tool-executor-native": "0.2.2-rc.1",
|
|
88
|
+
"@deepseek-ai/schemastery": "3.19.0",
|
|
86
89
|
"@types/node": "^24.13.3",
|
|
87
90
|
"prettier": "^3.9.9",
|
|
88
91
|
"tsx": "^4.20.0",
|