dsh-browser-verify 0.1.4 → 0.1.5
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 +2 -2
- package/README.zh.md +2 -2
- package/lib/index.js +61 -17
- package/lib/types/attachments.d.ts +25 -0
- package/package.json +1 -1
- package/src/attachments.ts +48 -1
- package/src/tools/index.ts +12 -14
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ verification is just tool calls.
|
|
|
18
18
|
## Quick start
|
|
19
19
|
|
|
20
20
|
```sh
|
|
21
|
-
dsh plugin --profile web add dsh-browser-verify@0.1.
|
|
21
|
+
dsh plugin --profile web add dsh-browser-verify@0.1.5
|
|
22
22
|
```
|
|
23
23
|
|
|
24
24
|
1. **Install** with the command above (or see [Install](#install)).
|
|
@@ -85,7 +85,7 @@ browser_screenshot
|
|
|
85
85
|
## Install
|
|
86
86
|
|
|
87
87
|
```sh
|
|
88
|
-
dsh plugin --profile web add dsh-browser-verify@0.1.
|
|
88
|
+
dsh plugin --profile web add dsh-browser-verify@0.1.5
|
|
89
89
|
```
|
|
90
90
|
|
|
91
91
|
The version is pinned on purpose: pnpm 11 holds back packages published in the
|
package/README.zh.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
## 快速上手
|
|
13
13
|
|
|
14
14
|
```sh
|
|
15
|
-
dsh plugin --profile web add dsh-browser-verify@0.1.
|
|
15
|
+
dsh plugin --profile web add dsh-browser-verify@0.1.5
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
1. **安装**(更多方式见 [安装](#安装))。
|
|
@@ -74,7 +74,7 @@ browser_screenshot
|
|
|
74
74
|
## 安装
|
|
75
75
|
|
|
76
76
|
```sh
|
|
77
|
-
dsh plugin --profile web add dsh-browser-verify@0.1.
|
|
77
|
+
dsh plugin --profile web add dsh-browser-verify@0.1.5
|
|
78
78
|
```
|
|
79
79
|
|
|
80
80
|
版本故意钉死:pnpm 11 会暂缓 24 小时内新发布的包,裸写 `add dsh-browser-verify`(latest)会在发布当天装到上一个版本。`--profile web`
|
package/lib/index.js
CHANGED
|
@@ -29,6 +29,18 @@ function selectOrphanDirs(entries, nowMs, ageMs, prefix) {
|
|
|
29
29
|
}
|
|
30
30
|
//#endregion
|
|
31
31
|
//#region src/attachments.ts
|
|
32
|
+
/**
|
|
33
|
+
* Formats a stored screenshot can carry: Playwright captures PNG, and the
|
|
34
|
+
* attachment store re-encodes above its normalization budget (JPEG, or WebP
|
|
35
|
+
* when the source keeps alpha). Every entry is a possible store fact — never
|
|
36
|
+
* assume PNG.
|
|
37
|
+
*/
|
|
38
|
+
const SCREENSHOT_MEDIA_TYPES = [
|
|
39
|
+
"image/png",
|
|
40
|
+
"image/jpeg",
|
|
41
|
+
"image/webp",
|
|
42
|
+
"image/gif"
|
|
43
|
+
];
|
|
32
44
|
function imageRefFromValue(image) {
|
|
33
45
|
return {
|
|
34
46
|
attachmentId: AttachmentId(image.attachmentId),
|
|
@@ -36,14 +48,44 @@ function imageRefFromValue(image) {
|
|
|
36
48
|
bytes: image.bytes,
|
|
37
49
|
width: image.width,
|
|
38
50
|
height: image.height,
|
|
39
|
-
...image.name === void 0 ? {} : { name: image.name }
|
|
51
|
+
...image.name === void 0 ? {} : { name: image.name },
|
|
52
|
+
...image.originalDimensions === void 0 ? {} : { originalDimensions: image.originalDimensions }
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Build the tool value from the store reference alone. Every fact — format,
|
|
57
|
+
* byte length, dimensions — is copied from what the store actually published,
|
|
58
|
+
* because the read path re-derives them from the stored bytes and rejects any
|
|
59
|
+
* reference that disagrees. Restating a constant here (e.g. PNG for a capture
|
|
60
|
+
* the store normalized to JPEG) writes a self-contradicting attachment
|
|
61
|
+
* reference into immutable history, which fails every later model request.
|
|
62
|
+
* @param ref - reference returned by `saveImage`.
|
|
63
|
+
* @param sha256 - digest of the captured bytes.
|
|
64
|
+
* @param identicalToPrevious - whether this capture repeated the previous one.
|
|
65
|
+
* @returns the tool-facing value projected into the model context.
|
|
66
|
+
*/
|
|
67
|
+
function screenshotValueFrom(ref, sha256, identicalToPrevious) {
|
|
68
|
+
return {
|
|
69
|
+
image: {
|
|
70
|
+
attachmentId: String(ref.attachmentId),
|
|
71
|
+
mediaType: ref.mediaType,
|
|
72
|
+
bytes: ref.bytes,
|
|
73
|
+
width: ref.width,
|
|
74
|
+
height: ref.height,
|
|
75
|
+
...ref.name === void 0 ? {} : { name: ref.name },
|
|
76
|
+
...ref.originalDimensions === void 0 ? {} : { originalDimensions: ref.originalDimensions }
|
|
77
|
+
},
|
|
78
|
+
sha256,
|
|
79
|
+
identicalToPrevious
|
|
40
80
|
};
|
|
41
81
|
}
|
|
42
82
|
function renderScreenshotBlocks(value) {
|
|
43
83
|
const dup = value.identicalToPrevious ? "(与上一张截图哈希相同,疑似页面未刷新;请 browser_open 重开场景后重试)" : "";
|
|
84
|
+
const original = value.image.originalDimensions;
|
|
85
|
+
const scaled = original === void 0 ? "" : `(原图 ${original.width}x${original.height},已按宿主预算缩放)`;
|
|
44
86
|
return [{
|
|
45
87
|
type: "text",
|
|
46
|
-
text: `<type>screenshot</type>\n<content>\n${value.image.mediaType}, ${value.image.width}x${value.image.height} px, ${value.image.bytes} bytes, sha256 ${value.sha256.slice(0, 12)}${dup}\n</content>`
|
|
88
|
+
text: `<type>screenshot</type>\n<content>\n${value.image.mediaType}, ${value.image.width}x${value.image.height} px${scaled}, ${value.image.bytes} bytes, sha256 ${value.sha256.slice(0, 12)}${dup}\n</content>`
|
|
47
89
|
}, {
|
|
48
90
|
type: "image",
|
|
49
91
|
attachment: imageRefFromValue(value.image)
|
|
@@ -380,7 +422,7 @@ function registerBrowserTools(ctx) {
|
|
|
380
422
|
},
|
|
381
423
|
mediaType: {
|
|
382
424
|
type: "string",
|
|
383
|
-
enum: [
|
|
425
|
+
enum: [...SCREENSHOT_MEDIA_TYPES],
|
|
384
426
|
required: true
|
|
385
427
|
},
|
|
386
428
|
bytes: {
|
|
@@ -395,7 +437,21 @@ function registerBrowserTools(ctx) {
|
|
|
395
437
|
type: "integer",
|
|
396
438
|
required: true
|
|
397
439
|
},
|
|
398
|
-
name: { type: "string" }
|
|
440
|
+
name: { type: "string" },
|
|
441
|
+
originalDimensions: {
|
|
442
|
+
type: "object",
|
|
443
|
+
additionalProperties: false,
|
|
444
|
+
properties: {
|
|
445
|
+
width: {
|
|
446
|
+
type: "number",
|
|
447
|
+
required: true
|
|
448
|
+
},
|
|
449
|
+
height: {
|
|
450
|
+
type: "number",
|
|
451
|
+
required: true
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
399
455
|
}
|
|
400
456
|
},
|
|
401
457
|
sha256: {
|
|
@@ -414,19 +470,7 @@ function registerBrowserTools(ctx) {
|
|
|
414
470
|
await assertImageCapable(ctx, exec);
|
|
415
471
|
return driver.withScenario(async (scenario) => {
|
|
416
472
|
const shot = await scenario.screenshot({ fullPage: args.fullPage });
|
|
417
|
-
|
|
418
|
-
return {
|
|
419
|
-
image: {
|
|
420
|
-
attachmentId: String(ref.attachmentId),
|
|
421
|
-
mediaType: "image/png",
|
|
422
|
-
bytes: ref.bytes,
|
|
423
|
-
width: ref.width,
|
|
424
|
-
height: ref.height,
|
|
425
|
-
...ref.name === void 0 ? {} : { name: ref.name }
|
|
426
|
-
},
|
|
427
|
-
sha256: shot.sha256,
|
|
428
|
-
identicalToPrevious: shot.identicalToPrevious
|
|
429
|
-
};
|
|
473
|
+
return screenshotValueFrom(await saveScreenshot(ctx, shot.data, args.name), shot.sha256, shot.identicalToPrevious);
|
|
430
474
|
});
|
|
431
475
|
}
|
|
432
476
|
}));
|
|
@@ -7,6 +7,13 @@
|
|
|
7
7
|
import type { Context } from '@deepseek-ai/cordis';
|
|
8
8
|
import type { ImageAttachmentRef, ImageMediaType } from '@deepseek-ai/dsh-attachment';
|
|
9
9
|
import type { ContentBlock } from '@deepseek-ai/dsh-llm';
|
|
10
|
+
/**
|
|
11
|
+
* Formats a stored screenshot can carry: Playwright captures PNG, and the
|
|
12
|
+
* attachment store re-encodes above its normalization budget (JPEG, or WebP
|
|
13
|
+
* when the source keeps alpha). Every entry is a possible store fact — never
|
|
14
|
+
* assume PNG.
|
|
15
|
+
*/
|
|
16
|
+
export declare const SCREENSHOT_MEDIA_TYPES: readonly ["image/png", "image/jpeg", "image/webp", "image/gif"];
|
|
10
17
|
export interface ScreenshotImage {
|
|
11
18
|
attachmentId: string;
|
|
12
19
|
mediaType: ImageMediaType;
|
|
@@ -14,6 +21,11 @@ export interface ScreenshotImage {
|
|
|
14
21
|
width: number;
|
|
15
22
|
height: number;
|
|
16
23
|
name?: string;
|
|
24
|
+
/** Present only when the store downscaled the capture; the pre-scaling pixels. */
|
|
25
|
+
originalDimensions?: {
|
|
26
|
+
width: number;
|
|
27
|
+
height: number;
|
|
28
|
+
};
|
|
17
29
|
}
|
|
18
30
|
export interface ScreenshotValue {
|
|
19
31
|
image: ScreenshotImage;
|
|
@@ -21,6 +33,19 @@ export interface ScreenshotValue {
|
|
|
21
33
|
identicalToPrevious: boolean;
|
|
22
34
|
}
|
|
23
35
|
export declare function imageRefFromValue(image: ScreenshotImage): ImageAttachmentRef;
|
|
36
|
+
/**
|
|
37
|
+
* Build the tool value from the store reference alone. Every fact — format,
|
|
38
|
+
* byte length, dimensions — is copied from what the store actually published,
|
|
39
|
+
* because the read path re-derives them from the stored bytes and rejects any
|
|
40
|
+
* reference that disagrees. Restating a constant here (e.g. PNG for a capture
|
|
41
|
+
* the store normalized to JPEG) writes a self-contradicting attachment
|
|
42
|
+
* reference into immutable history, which fails every later model request.
|
|
43
|
+
* @param ref - reference returned by `saveImage`.
|
|
44
|
+
* @param sha256 - digest of the captured bytes.
|
|
45
|
+
* @param identicalToPrevious - whether this capture repeated the previous one.
|
|
46
|
+
* @returns the tool-facing value projected into the model context.
|
|
47
|
+
*/
|
|
48
|
+
export declare function screenshotValueFrom(ref: ImageAttachmentRef, sha256: string, identicalToPrevious: boolean): ScreenshotValue;
|
|
24
49
|
export declare function renderScreenshotBlocks(value: ScreenshotValue): ContentBlock[];
|
|
25
50
|
/** Persist screenshot bytes, mapping store refusals to actionable errors. */
|
|
26
51
|
export declare function saveScreenshot(ctx: Context, data: Buffer, name: string | undefined): Promise<ImageAttachmentRef>;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-browser-verify",
|
|
3
3
|
"description": "Read-only browser verification tools for the DeepSeek Harness web GUI: browser_open / browser_mock / browser_assert / browser_screenshot — verify a page (H5/desktop) in ≤4 tool calls with mock interception, DOM assertions, and screenshots that auto-project into the model context.",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.5",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@11.7.0",
|
|
7
7
|
"engines": {
|
package/src/attachments.ts
CHANGED
|
@@ -10,6 +10,16 @@ import type { ImageAttachmentRef, ImageMediaType } from '@deepseek-ai/dsh-attach
|
|
|
10
10
|
import { AttachmentError, AttachmentId } from '@deepseek-ai/dsh-attachment'
|
|
11
11
|
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Formats a stored screenshot can carry: Playwright captures PNG, and the
|
|
15
|
+
* attachment store re-encodes above its normalization budget (JPEG, or WebP
|
|
16
|
+
* when the source keeps alpha). Every entry is a possible store fact — never
|
|
17
|
+
* assume PNG.
|
|
18
|
+
*/
|
|
19
|
+
export const SCREENSHOT_MEDIA_TYPES = [
|
|
20
|
+
'image/png', 'image/jpeg', 'image/webp', 'image/gif',
|
|
21
|
+
] as const satisfies readonly ImageMediaType[]
|
|
22
|
+
|
|
13
23
|
export interface ScreenshotImage {
|
|
14
24
|
attachmentId: string
|
|
15
25
|
mediaType: ImageMediaType
|
|
@@ -17,6 +27,8 @@ export interface ScreenshotImage {
|
|
|
17
27
|
width: number
|
|
18
28
|
height: number
|
|
19
29
|
name?: string
|
|
30
|
+
/** Present only when the store downscaled the capture; the pre-scaling pixels. */
|
|
31
|
+
originalDimensions?: { width: number; height: number }
|
|
20
32
|
}
|
|
21
33
|
|
|
22
34
|
export interface ScreenshotValue {
|
|
@@ -33,6 +45,39 @@ export function imageRefFromValue(image: ScreenshotImage): ImageAttachmentRef {
|
|
|
33
45
|
width: image.width,
|
|
34
46
|
height: image.height,
|
|
35
47
|
...image.name === undefined ? {} : { name: image.name },
|
|
48
|
+
...image.originalDimensions === undefined ? {} : { originalDimensions: image.originalDimensions },
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Build the tool value from the store reference alone. Every fact — format,
|
|
54
|
+
* byte length, dimensions — is copied from what the store actually published,
|
|
55
|
+
* because the read path re-derives them from the stored bytes and rejects any
|
|
56
|
+
* reference that disagrees. Restating a constant here (e.g. PNG for a capture
|
|
57
|
+
* the store normalized to JPEG) writes a self-contradicting attachment
|
|
58
|
+
* reference into immutable history, which fails every later model request.
|
|
59
|
+
* @param ref - reference returned by `saveImage`.
|
|
60
|
+
* @param sha256 - digest of the captured bytes.
|
|
61
|
+
* @param identicalToPrevious - whether this capture repeated the previous one.
|
|
62
|
+
* @returns the tool-facing value projected into the model context.
|
|
63
|
+
*/
|
|
64
|
+
export function screenshotValueFrom(
|
|
65
|
+
ref: ImageAttachmentRef,
|
|
66
|
+
sha256: string,
|
|
67
|
+
identicalToPrevious: boolean,
|
|
68
|
+
): ScreenshotValue {
|
|
69
|
+
return {
|
|
70
|
+
image: {
|
|
71
|
+
attachmentId: String(ref.attachmentId),
|
|
72
|
+
mediaType: ref.mediaType,
|
|
73
|
+
bytes: ref.bytes,
|
|
74
|
+
width: ref.width,
|
|
75
|
+
height: ref.height,
|
|
76
|
+
...ref.name === undefined ? {} : { name: ref.name },
|
|
77
|
+
...ref.originalDimensions === undefined ? {} : { originalDimensions: ref.originalDimensions },
|
|
78
|
+
},
|
|
79
|
+
sha256,
|
|
80
|
+
identicalToPrevious,
|
|
36
81
|
}
|
|
37
82
|
}
|
|
38
83
|
|
|
@@ -40,10 +85,12 @@ export function renderScreenshotBlocks(value: ScreenshotValue): ContentBlock[] {
|
|
|
40
85
|
const dup = value.identicalToPrevious
|
|
41
86
|
? '(与上一张截图哈希相同,疑似页面未刷新;请 browser_open 重开场景后重试)'
|
|
42
87
|
: ''
|
|
88
|
+
const original = value.image.originalDimensions
|
|
89
|
+
const scaled = original === undefined ? '' : `(原图 ${original.width}x${original.height},已按宿主预算缩放)`
|
|
43
90
|
return [
|
|
44
91
|
{
|
|
45
92
|
type: 'text',
|
|
46
|
-
text: `<type>screenshot</type>\n<content>\n${value.image.mediaType}, ${value.image.width}x${value.image.height} px, ${value.image.bytes} bytes, sha256 ${value.sha256.slice(0, 12)}${dup}\n</content>`,
|
|
93
|
+
text: `<type>screenshot</type>\n<content>\n${value.image.mediaType}, ${value.image.width}x${value.image.height} px${scaled}, ${value.image.bytes} bytes, sha256 ${value.sha256.slice(0, 12)}${dup}\n</content>`,
|
|
47
94
|
},
|
|
48
95
|
{ type: 'image', attachment: imageRefFromValue(value.image) },
|
|
49
96
|
]
|
package/src/tools/index.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import type { Context } from '@deepseek-ai/cordis'
|
|
8
8
|
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
9
9
|
import { BrowserDriver } from '../browser/driver.ts'
|
|
10
|
-
import { assertImageCapable, renderScreenshotBlocks, saveScreenshot } from '../attachments.ts'
|
|
10
|
+
import { assertImageCapable, renderScreenshotBlocks, saveScreenshot, SCREENSHOT_MEDIA_TYPES, screenshotValueFrom } from '../attachments.ts'
|
|
11
11
|
import { withTimeout } from './timeout.ts'
|
|
12
12
|
|
|
13
13
|
/** Parse a positive-integer env var; NaN/zero/negative falls back to the default. */
|
|
@@ -150,11 +150,19 @@ export function registerBrowserTools(ctx: Context): void {
|
|
|
150
150
|
required: true,
|
|
151
151
|
properties: {
|
|
152
152
|
attachmentId: { type: 'string', required: true },
|
|
153
|
-
mediaType: { type: 'string', enum: [
|
|
153
|
+
mediaType: { type: 'string', enum: [...SCREENSHOT_MEDIA_TYPES], required: true },
|
|
154
154
|
bytes: { type: 'integer', required: true },
|
|
155
155
|
width: { type: 'integer', required: true },
|
|
156
156
|
height: { type: 'integer', required: true },
|
|
157
157
|
name: { type: 'string' },
|
|
158
|
+
originalDimensions: {
|
|
159
|
+
type: 'object',
|
|
160
|
+
additionalProperties: false,
|
|
161
|
+
properties: {
|
|
162
|
+
width: { type: 'number', required: true },
|
|
163
|
+
height: { type: 'number', required: true },
|
|
164
|
+
},
|
|
165
|
+
},
|
|
158
166
|
},
|
|
159
167
|
},
|
|
160
168
|
sha256: { type: 'string', required: true },
|
|
@@ -168,18 +176,8 @@ export function registerBrowserTools(ctx: Context): void {
|
|
|
168
176
|
return driver.withScenario(async scenario => {
|
|
169
177
|
const shot = await scenario.screenshot({ fullPage: args.fullPage })
|
|
170
178
|
const ref = await saveScreenshot(ctx, shot.data, args.name)
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
attachmentId: String(ref.attachmentId),
|
|
174
|
-
mediaType: 'image/png' as const,
|
|
175
|
-
bytes: ref.bytes,
|
|
176
|
-
width: ref.width,
|
|
177
|
-
height: ref.height,
|
|
178
|
-
...ref.name === undefined ? {} : { name: ref.name },
|
|
179
|
-
},
|
|
180
|
-
sha256: shot.sha256,
|
|
181
|
-
identicalToPrevious: shot.identicalToPrevious,
|
|
182
|
-
}
|
|
179
|
+
// Facts come from the store ref: a normalized capture is no longer a PNG.
|
|
180
|
+
return screenshotValueFrom(ref, shot.sha256, shot.identicalToPrevious)
|
|
183
181
|
})
|
|
184
182
|
},
|
|
185
183
|
}))
|