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 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.4
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.4
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.4
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.4
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: ["image/png"],
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
- const ref = await saveScreenshot(ctx, shot.data, args.name);
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",
4
+ "version": "0.1.5",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@11.7.0",
7
7
  "engines": {
@@ -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
  ]
@@ -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: ['image/png'], required: true },
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
- return {
172
- image: {
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
  }))