@momasiku/dsh-pilot 0.5.12 → 0.5.14
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 +17 -0
- package/lib/index.js +88 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -232,6 +232,18 @@ and what `dsh plugin add` checks before it installs anything. Requirements:
|
|
|
232
232
|
|
|
233
233
|
### Recent changes
|
|
234
234
|
|
|
235
|
+
**0.5.12** — a cold tree, and an empty list that explained nothing.
|
|
236
|
+
|
|
237
|
+
- **Chromium builds its accessibility tree lazily**, and the first caller pays for it:
|
|
238
|
+
measured on the harness window right after a restart, 3 elements at 1.2 s and 77
|
|
239
|
+
elements at 0.3 s on the next call. A caller that trusts the first answer concludes
|
|
240
|
+
the window has no tree and falls back to pixels for nothing. `controls` now retries
|
|
241
|
+
once, 300 ms later, when the worker has not yet seen a real tree and the answer is
|
|
242
|
+
suspiciously small.
|
|
243
|
+
- **A filtered listing that legitimately matches nothing** now says so, instead of
|
|
244
|
+
returning an empty list that reads as "this window has no tree":
|
|
245
|
+
`(no operable controls here; the window has 2 elements - pass meaningfulOnly:false to see them)`.
|
|
246
|
+
|
|
235
247
|
**0.5.11** — the control tree, and two fixes that make a batch honest.
|
|
236
248
|
|
|
237
249
|
- **`controls` reads the window's UI Automation tree**, and `target` clicks a listed
|
|
@@ -616,6 +628,11 @@ dsh plugin --profile <你的profile> add "file:E:\path\to\dsh-pilot"
|
|
|
616
628
|
|
|
617
629
|
### 最近改动
|
|
618
630
|
|
|
631
|
+
**0.5.12** —— 冷树,以及一个什么都没说的空列表。
|
|
632
|
+
|
|
633
|
+
- **Chromium 的无障碍树是懒加载的**,而且由第一个来问的人付账:实测重启后在 1.2 秒时只有 3 个元素,下一次调用 0.3 秒时是 77 个。信任第一个答案的人会认定「这窗口没有树」,白白退回像素。现在 `controls` 在「worker 还没见过完整树、且结果异常少」时会等 300 ms 重试一次。
|
|
634
|
+
- **过滤后确实什么都没匹配到时,现在会明说**,而不是返回一个读起来像「这里没有树」的空列表:`(no operable controls here; the window has 2 elements - pass meaningfulOnly:false to see them)`。
|
|
635
|
+
|
|
619
636
|
**0.5.11** —— 控件树,外加两个让批量操作「不说谎」的修复。
|
|
620
637
|
|
|
621
638
|
- **`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,25 +741,83 @@ 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;
|
|
741
|
-
const
|
|
742
|
-
|
|
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';
|
|
792
|
+
const delivered = capture?.image;
|
|
793
|
+
const preview = capture?.requestPreview;
|
|
794
|
+
const predicted = Number.isFinite(Number(preview?.width)) && Number(preview.width) > 0 && Number.isFinite(Number(preview?.height)) && Number(preview.height) > 0
|
|
795
|
+
? { width: Number(preview.width), height: Number(preview.height), maxPixels: Number(preview.maxPixels) }
|
|
796
|
+
: undefined;
|
|
797
|
+
const final = predicted ?? (delivered === undefined || delivered === null ? undefined : { width: Number(delivered.width), height: Number(delivered.height) });
|
|
798
|
+
const kx = final !== undefined && sizeKnown && final.width > 0 ? width / final.width : 1;
|
|
799
|
+
const ky = final !== undefined && sizeKnown && final.height > 0 ? height / final.height : 1;
|
|
800
|
+
const oneToOne = Math.abs(kx - 1) < 0.002 && Math.abs(ky - 1) < 0.002;
|
|
801
|
+
const budgetLine = predicted?.maxPixels > 0 ? `This deployment's image budget is ${predicted.maxPixels} px. ` : '';
|
|
802
|
+
const guard = 'The message carrying this frame states its own preview size; if it differs from the size named here, multiply every reading by capture_width / preview_width before passing it to desktop_control, because a step after capture decides it.';
|
|
803
|
+
if (oneToOne) {
|
|
743
804
|
return [
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
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.`
|
|
805
|
+
`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.`,
|
|
806
|
+
`${budgetLine}At that size one image pixel is one real screen pixel: to act on a feature at image (x,y), call desktop_control with exactly x and y.`,
|
|
807
|
+
guard
|
|
753
808
|
].join(' ');
|
|
754
809
|
}
|
|
810
|
+
if (!sizeKnown || final === undefined) return guard;
|
|
811
|
+
const axes = Math.abs(kx - ky) <= 0.005 * Math.max(kx, ky);
|
|
812
|
+
const multiply = axes
|
|
813
|
+
? `multiply every reading by ${printed(kx)}`
|
|
814
|
+
: `multiply x readings by ${printed(kx)} and y readings by ${printed(ky)}`;
|
|
815
|
+
const shown = predicted === undefined ? 'delivered' : 'the request pipeline will show it to you';
|
|
755
816
|
return [
|
|
756
|
-
`This frame was
|
|
757
|
-
|
|
817
|
+
`This frame was captured at ${frame} physical screen pixels and ${shown} at ${final.width}x${final.height} (${printed(final.width / width)}x the capture), so image pixels are NOT screen pixels.`,
|
|
818
|
+
`${budgetLine}To act on a feature at image (x,y), ${multiply} and pass those physical screen pixels to desktop_control, or read the ruler labels, which are already screen coordinates.`,
|
|
819
|
+
'A capture whose area is at or under the budget arrives at capture size: framing the window or a region instead of the whole screen gives a true 1:1 frame.',
|
|
820
|
+
guard
|
|
758
821
|
].join(' ');
|
|
759
822
|
}
|
|
760
823
|
|
|
@@ -941,6 +1004,17 @@ function windowInfoOf(window) {
|
|
|
941
1004
|
};
|
|
942
1005
|
}
|
|
943
1006
|
|
|
1007
|
+
/** Where the request pipeline is predicted to put a frame, and the budget used. */
|
|
1008
|
+
const REQUEST_PREVIEW_SCHEMA = {
|
|
1009
|
+
type: 'object',
|
|
1010
|
+
additionalProperties: false,
|
|
1011
|
+
properties: {
|
|
1012
|
+
width: { type: 'integer', required: true },
|
|
1013
|
+
height: { type: 'integer', required: true },
|
|
1014
|
+
maxPixels: { type: 'number' }
|
|
1015
|
+
}
|
|
1016
|
+
};
|
|
1017
|
+
|
|
944
1018
|
const CAPTURE_SCHEMA = {
|
|
945
1019
|
type: 'object',
|
|
946
1020
|
additionalProperties: false,
|
|
@@ -969,6 +1043,7 @@ const CAPTURE_SCHEMA = {
|
|
|
969
1043
|
frameHash: { type: 'string' },
|
|
970
1044
|
unchanged: { type: 'boolean' },
|
|
971
1045
|
rulers: { type: 'boolean' },
|
|
1046
|
+
requestPreview: REQUEST_PREVIEW_SCHEMA,
|
|
972
1047
|
capturedAt: { type: 'string' }
|
|
973
1048
|
}
|
|
974
1049
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@momasiku/dsh-pilot",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.14",
|
|
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",
|