@momasiku/dsh-pilot 0.5.12 → 0.5.16

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
@@ -2,14 +2,16 @@
2
2
 
3
3
  # dsh-pilot
4
4
 
5
+ ## 让 DSH 看见并高效地操作你的电脑
6
+
5
7
  **Give DeepSeek Harness hands and eyes on your desktop.**
6
8
 
9
+ [![awesome · DSH plugin](https://awesome-dsh-plugin.com/badge.svg)](https://awesome-dsh-plugin.com/p/momasiku/dsh-pilot/)
7
10
  [![npm](https://img.shields.io/npm/v/@momasiku/dsh-pilot?label=npm)](https://www.npmjs.com/package/@momasiku/dsh-pilot)
8
11
  [![compat](https://github.com/momasiku/dsh-pilot/actions/workflows/compat.yml/badge.svg)](https://github.com/momasiku/dsh-pilot/actions/workflows/compat.yml)
9
12
  [![license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
10
13
 
11
14
  ![platform](https://img.shields.io/badge/platform-Windows-0078d4.svg)
12
- ![DSH](https://img.shields.io/badge/DSH-0.1.5--rc.2%20%E2%86%92%200.2.x-5b50ed.svg)
13
15
 
14
16
  [English](#why-this-exists) · [中文说明](#中文说明)
15
17
 
@@ -232,6 +234,30 @@ and what `dsh plugin add` checks before it installs anything. Requirements:
232
234
 
233
235
  ### Recent changes
234
236
 
237
+ **0.5.14** — the coordinate contract stops guessing how big the image you see is.
238
+
239
+ - **The request pipeline resizes again after capture**, and this plugin never saw
240
+ that step: on a 2560x1600 display the frame reached the model as 1610x1006 while
241
+ the contract said "delivered at capture size … do not rescale", so every reading
242
+ taken off it was 1.59x off. The contract now **predicts** the projected size from
243
+ the image budget the deployment publishes, and prints the size and the multiplier:
244
+ `2560x1600 capture -> 1609x1006 preview, multiply every reading by 1.5911`. A frame
245
+ whose area fits the budget arrives untouched, so framing the window or a region
246
+ gives a true 1:1 frame — 1365x960 does, 1024x768 does, 4K and 5K say 2.2642x and
247
+ 3.0189x instead.
248
+
249
+ **0.5.12** — a cold tree, and an empty list that explained nothing.
250
+
251
+ - **Chromium builds its accessibility tree lazily**, and the first caller pays for it:
252
+ measured on the harness window right after a restart, 3 elements at 1.2 s and 77
253
+ elements at 0.3 s on the next call. A caller that trusts the first answer concludes
254
+ the window has no tree and falls back to pixels for nothing. `controls` now retries
255
+ once, 300 ms later, when the worker has not yet seen a real tree and the answer is
256
+ suspiciously small.
257
+ - **A filtered listing that legitimately matches nothing** now says so, instead of
258
+ returning an empty list that reads as "this window has no tree":
259
+ `(no operable controls here; the window has 2 elements - pass meaningfulOnly:false to see them)`.
260
+
235
261
  **0.5.11** — the control tree, and two fixes that make a batch honest.
236
262
 
237
263
  - **`controls` reads the window's UI Automation tree**, and `target` clicks a listed
@@ -616,6 +642,15 @@ dsh plugin --profile <你的profile> add "file:E:\path\to\dsh-pilot"
616
642
 
617
643
  ### 最近改动
618
644
 
645
+ **0.5.14** —— 坐标契约不再猜「你看到的那张图有多大」。
646
+
647
+ - **请求管线会在抓取之后再缩一次**,而插件看不到这一步:2560×1600 的屏幕上,帧送到模型时是 1610×1006,契约却写着「交付即抓取尺寸……不要缩放」,于是每个读数都偏 1.59 倍。现在契约用部署公布的图片预算**预测**投影尺寸,并把尺寸和乘数一起写出来:`2560x1600 抓取 → 1609x1006 预览,把每个读数乘 1.5911`。面积落在预算内的帧会原样送达,所以抓窗口或抓区域能得到真正的 1:1 —— 1365×960 可以,1024×768 可以;4K 和 5K 则分别写 2.2642 倍和 3.0189 倍。
648
+
649
+ **0.5.12** —— 冷树,以及一个什么都没说的空列表。
650
+
651
+ - **Chromium 的无障碍树是懒加载的**,而且由第一个来问的人付账:实测重启后在 1.2 秒时只有 3 个元素,下一次调用 0.3 秒时是 77 个。信任第一个答案的人会认定「这窗口没有树」,白白退回像素。现在 `controls` 在「worker 还没见过完整树、且结果异常少」时会等 300 ms 重试一次。
652
+ - **过滤后确实什么都没匹配到时,现在会明说**,而不是返回一个读起来像「这里没有树」的空列表:`(no operable controls here; the window has 2 elements - pass meaningfulOnly:false to see them)`。
653
+
619
654
  **0.5.11** —— 控件树,外加两个让批量操作「不说谎」的修复。
620
655
 
621
656
  - **`controls` 读窗口的 UI Automation 树**,`target` 点列表里那个控件的精确矩形,于是有名字的控件不必再去像素里找。它不截图,所以也不付 settle。在 673 个元素的窗口上实测:过滤后 **0.65 秒、约 850 token**(77 个可操作元素),不过滤约 9k。暖 worker 记着它发出去的那份列表,所以某一行换了身份会报错,而不是点到搬进来的那个。
package/lib/index.js CHANGED
@@ -654,6 +654,11 @@ async function capture(ctx, exec, config, args) {
654
654
  ...(ref.name === undefined ? {} : { name: ref.name }),
655
655
  ...(ref.originalDimensions === undefined ? {} : { originalDimensions: { ...ref.originalDimensions } })
656
656
  };
657
+ // Predict where the request pipeline will put this frame, from the same budget
658
+ // it uses. A prediction, always paired with the deferral to the preview size the
659
+ // model is actually shown.
660
+ const projected = projectedRequestSize(Number(result.image.width), Number(result.image.height), limits);
661
+ if (projected !== undefined) result.requestPreview = projected;
657
662
  } catch (error) {
658
663
  result.imageError = `the frame could not be attached: ${error instanceof Error ? error.message : String(error)}`;
659
664
  }
@@ -736,26 +741,76 @@ function captureContent(value) {
736
741
  * @param value - a capture, or a tool result carrying one under `capture`.
737
742
  * @returns the contract text for that frame.
738
743
  */
744
+ /**
745
+ * Where the request pipeline will put this frame.
746
+ *
747
+ * The store hands back a normalized image, and then the request pipeline shrinks it
748
+ * again to fit the model's image budget - after this plugin has let go of it, which
749
+ * is why a frame that really was 1:1 at capture can still reach the model smaller.
750
+ * The geometry is a pure function of the budget the deployment publishes, so the
751
+ * pipeline can be predicted instead of described. Mirrors requestImageDimensions()
752
+ * in @deepseek-ai/dsh-attachment, and kept local so a drift between the two can only
753
+ * cost accuracy: the contract always defers to the preview size the message states.
754
+ *
755
+ * @param width - normalized image width.
756
+ * @param height - normalized image height.
757
+ * @param limits - the deployment's ImageAttachmentLimits.
758
+ * @returns predicted request dimensions plus the budget they came from, or undefined.
759
+ */
760
+ function projectedRequestSize(width, height, limits) {
761
+ const maxPixels = Number(limits?.maxImagePixels);
762
+ const maxDimension = Number(limits?.maxImageDimension);
763
+ if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) return undefined;
764
+ let w = width;
765
+ let h = height;
766
+ if (Number.isFinite(maxDimension) && maxDimension > 0 && Math.max(w, h) > maxDimension) {
767
+ if (w >= h) { h = Math.max(1, Math.round(maxDimension * h / w)); w = maxDimension; }
768
+ else { w = Math.max(1, Math.round(maxDimension * w / h)); h = maxDimension; }
769
+ }
770
+ if (Number.isFinite(maxPixels) && maxPixels > 0 && w * h > maxPixels) {
771
+ const s = Math.min(1, Math.sqrt(maxPixels / (w * h)));
772
+ if (w >= h) {
773
+ w = Math.max(1, Math.floor(w * s));
774
+ h = Math.max(1, Math.round(w * height / width));
775
+ while (w * h > maxPixels && w > 1) { w -= 1; h = Math.max(1, Math.round(w * height / width)); }
776
+ } else {
777
+ h = Math.max(1, Math.floor(h * s));
778
+ w = Math.max(1, Math.round(h * width / height));
779
+ while (w * h > maxPixels && h > 1) { h -= 1; w = Math.max(1, Math.round(h * width / height)); }
780
+ }
781
+ }
782
+ const out = { width: w, height: h };
783
+ if (Number.isFinite(maxPixels) && maxPixels > 0) out.maxPixels = maxPixels;
784
+ return out;
785
+ }
739
786
  function coordinateContract(value) {
740
787
  const capture = value?.capture ?? value;
788
+ const width = Number(capture?.imageWidth);
789
+ const height = Number(capture?.imageHeight);
790
+ const sizeKnown = Number.isFinite(width) && width > 0 && Number.isFinite(height) && height > 0;
791
+ const frame = sizeKnown ? `${width}x${height}` : 'the captured frame';
741
792
  const scale = deliveredScale(capture);
742
- if (scaleIsOne(scale)) {
743
- return [
744
- 'Image pixel (0,0) is the top-left of this frame. Both halves of this device are per-monitor DPI aware and this frame is delivered at capture size, so one image pixel is one real screen pixel:',
745
- 'to act on a feature at image (x,y), call desktop_control with exactly x and y. Do not rescale for display scaling.'
746
- ].join(' ');
747
- }
748
- if (axesAgree(scale)) {
749
- const ratio = (scale.x + scale.y) / 2;
750
- return [
751
- `This frame was delivered at ${printed(ratio)}x the capture, so image pixels are NOT screen pixels:`,
752
- `to act on a feature at image (x,y), multiply both readings by ${printed(1 / ratio)} and pass those physical screen pixels to desktop_control.`
753
- ].join(' ');
793
+ const storeShrank = !scaleIsOne(scale);
794
+ // No prediction. The request pipeline resizes a frame to fit the model's image
795
+ // budget, after this plugin has let go of it, and the budget it uses is not the
796
+ // deployment's admission limit the plugin can read: measured 64,000,000 in
797
+ // imageLimits.maxImagePixels against an actual projection of ~1,640,000. A
798
+ // predicted size built from the wrong number reads as a promise, so the contract
799
+ // stops predicting and points at the one size that is not a guess - the preview
800
+ // size the message itself states.
801
+ const lines = [
802
+ `Image pixel (0,0) is the top-left of this frame, which was captured at ${frame} physical screen pixels; both halves of this device are per-monitor DPI aware.`
803
+ ];
804
+ if (storeShrank) {
805
+ lines.push(axesAgree(scale)
806
+ ? `The attachment store already delivered it at ${printed((scale.x + scale.y) / 2)}x that.`
807
+ : `The attachment store already delivered it at ${printed(scale.x)}x horizontally and ${printed(scale.y)}x vertically.`);
754
808
  }
755
- return [
756
- `This frame was delivered at ${printed(scale.x)}x horizontally and ${printed(scale.y)}x vertically of the capture, so image pixels are NOT screen pixels:`,
757
- `to act on a feature at image (x,y), multiply the readings by ${printed(1 / scale.x)} and ${printed(1 / scale.y)} (x*${printed(1 / scale.x)}, y*${printed(1 / scale.y)}) and pass those physical screen pixels to desktop_control.`
758
- ].join(' ');
809
+ lines.push('The image you are looking at may be smaller again: the request pipeline resizes a frame to fit the model image budget, and this plugin cannot see that step, so do not trust the capture size as the size on screen. This same message states what it settled on as "request preview WxH" - that number is authoritative.');
810
+ lines.push(sizeKnown
811
+ ? `If the preview is not ${frame}, multiply every reading by ${width} / preview_width (and ${height} / preview_height) before passing it to desktop_control; if it is, call desktop_control with exactly x and y. Ruler labels, when drawn, are already screen coordinates.`
812
+ : 'If the preview is smaller than the capture, multiply every reading by capture_width / preview_width before passing it to desktop_control.');
813
+ return lines.join(' ');
759
814
  }
760
815
 
761
816
  /**
@@ -941,6 +996,17 @@ function windowInfoOf(window) {
941
996
  };
942
997
  }
943
998
 
999
+ /** Where the request pipeline is predicted to put a frame, and the budget used. */
1000
+ const REQUEST_PREVIEW_SCHEMA = {
1001
+ type: 'object',
1002
+ additionalProperties: false,
1003
+ properties: {
1004
+ width: { type: 'integer', required: true },
1005
+ height: { type: 'integer', required: true },
1006
+ maxPixels: { type: 'number' }
1007
+ }
1008
+ };
1009
+
944
1010
  const CAPTURE_SCHEMA = {
945
1011
  type: 'object',
946
1012
  additionalProperties: false,
@@ -969,6 +1035,7 @@ const CAPTURE_SCHEMA = {
969
1035
  frameHash: { type: 'string' },
970
1036
  unchanged: { type: 'boolean' },
971
1037
  rulers: { type: 'boolean' },
1038
+ requestPreview: REQUEST_PREVIEW_SCHEMA,
972
1039
  capturedAt: { type: 'string' }
973
1040
  }
974
1041
  };
@@ -1378,6 +1445,10 @@ function apply(ctx, config) {
1378
1445
  // Copy the declared fields only: the helper's own bookkeeping keys
1379
1446
  // must not reach the closed output schema.
1380
1447
  const value = { action: args.action };
1448
+ // A one-element list can still arrive unrolled from the helper; the output
1449
+ // schema wants an array, and a string here is a rejected result, not a bug the
1450
+ // caller can see.
1451
+ if (typeof acted.controls === 'string') acted.controls = [acted.controls];
1381
1452
  for (const field of ['cursor', 'foregroundTitle', 'foregroundClass', 'foregroundProcess', 'notes', 'windowCount', 'actedAt', 'controls', 'controlCount', 'controlsTruncated', 'controlsWindow']) {
1382
1453
  if (acted[field] !== undefined) value[field] = acted[field];
1383
1454
  }
@@ -124,7 +124,11 @@ function Format-DshUiaNodes {
124
124
  $lines.Add(('{0} [{1}] ''{2}'' @ {3}{4}' -f $n.index, $n.role, $name, $rect, $tail))
125
125
  }
126
126
  if ($Truncated) { $lines.Add(('... truncated at the node cap ({0} elements in total)' -f $Total)) }
127
- return $lines.ToArray()
127
+ # The unary comma stops PowerShell unrolling a one-element array on return:
128
+ # without it a window that exposes a single control comes back as a bare
129
+ # string, and the result then fails its own schema ("controls must be an
130
+ # array"). Measured on a LibreOffice window that publishes exactly one.
131
+ return ,($lines.ToArray())
128
132
  }
129
133
 
130
134
  <# Resolve an index back to an element by re-reading the same listing. #>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@momasiku/dsh-pilot",
3
- "version": "0.5.12",
3
+ "version": "0.5.16",
4
4
  "description": "Desktop vision and control for DeepSeek Harness - the whole Windows machine, any application, not browser automation: screen_view captures the live screen as an image the model can see, desktop_control drives the mouse, keyboard, and windows against the exact same pixel space; desktop_sequence runs a whole batch of actions in one call and looks once at the end.",
5
5
  "license": "MIT",
6
6
  "author": "momasiku",