@logictan/dsh-desktop-agent 0.1.5 → 0.1.7
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/lib/client.js +140 -554
- package/lib/config.js +46 -7
- package/lib/index.js +2 -2
- package/lib/routes.js +30 -28
- package/package.json +14 -2
package/lib/client.js
CHANGED
|
@@ -9,12 +9,14 @@
|
|
|
9
9
|
* `<bundle>#desktop-agent`, the key the Plugins page derives from the bundle
|
|
10
10
|
* that declares the row.
|
|
11
11
|
*
|
|
12
|
-
* The
|
|
13
|
-
*
|
|
12
|
+
* The card renders the host's `Config` schema through the shared `ConfigForm`.
|
|
13
|
+
* Scalar fields are rendered automatically from the describe mirror; this half
|
|
14
|
+
* supplies only the vision-catalog dropdowns (replacing the route fields via
|
|
15
|
+
* `renderControl`) and the catalog status banners in `children`.
|
|
14
16
|
*
|
|
15
17
|
* Two catalogs feed this card, and they are not interchangeable:
|
|
16
18
|
*
|
|
17
|
-
* - The VISION list comes from this plugin's own
|
|
19
|
+
* - The VISION list comes from this plugin's own same-origin route
|
|
18
20
|
* (`/api/dsh-desktop-agent/vision-models`). It has to: the host's
|
|
19
21
|
* `session.modelCatalog` does not carry `inputModalities` at all — its
|
|
20
22
|
* builder maps a fixed set of fields off the resolved model info and drops
|
|
@@ -33,6 +35,7 @@ window.__ModuleLoader__.load({
|
|
|
33
35
|
id: "@logictan/dsh-desktop-agent",
|
|
34
36
|
factory: function (require) {
|
|
35
37
|
var React = require("react");
|
|
38
|
+
var UI = require("@logictan/dsh-plugins-shared-client");
|
|
36
39
|
|
|
37
40
|
/** Loader entry id this card's form is keyed by. */
|
|
38
41
|
var NS = "desktop-agent";
|
|
@@ -58,10 +61,6 @@ window.__ModuleLoader__.load({
|
|
|
58
61
|
intro:
|
|
59
62
|
"用截图作为主要感知、通过 Cua Driver 操作桌面窗口。留空表示继承默认值。需先在 profile 挂载 computer-use 的 cua-driver-native provider。",
|
|
60
63
|
inherit: "继承默认",
|
|
61
|
-
save: "保存",
|
|
62
|
-
saving: "保存中…",
|
|
63
|
-
reset: "恢复默认",
|
|
64
|
-
overridden: "已覆盖",
|
|
65
64
|
unwritable: "当前连接不写回本机设置,无法保存。",
|
|
66
65
|
catalogLoading: "正在读取视觉模型目录…",
|
|
67
66
|
catalogError: "视觉模型目录读取失败,可切换为手填。",
|
|
@@ -72,149 +71,8 @@ window.__ModuleLoader__.load({
|
|
|
72
71
|
manual: "手填",
|
|
73
72
|
picker: "从目录选择",
|
|
74
73
|
routeMissing: "已存的路由不在当前目录中(provider 读取失败或模型已下线),已切换为手填。",
|
|
75
|
-
deliveryBackground: "后台投递(默认)",
|
|
76
|
-
deliveryForeground: "前台投递(画布/游戏)",
|
|
77
74
|
};
|
|
78
75
|
|
|
79
|
-
/**
|
|
80
|
-
* Editable fields, in render order. `kind` picks the control.
|
|
81
|
-
*
|
|
82
|
-
* The three vision-route keys are rendered together below the catalog picker
|
|
83
|
-
* and are listed here only so that override detection, save, and reset see
|
|
84
|
-
* them.
|
|
85
|
-
*/
|
|
86
|
-
var FIELDS = [
|
|
87
|
-
{
|
|
88
|
-
key: "maxSteps",
|
|
89
|
-
label: "步数上限",
|
|
90
|
-
kind: "number",
|
|
91
|
-
min: 1,
|
|
92
|
-
step: 1,
|
|
93
|
-
hint: "一次任务最多执行多少个动作,默认 40。",
|
|
94
|
-
},
|
|
95
|
-
{
|
|
96
|
-
key: "maxImageDimension",
|
|
97
|
-
label: "截图长边上限",
|
|
98
|
-
kind: "number",
|
|
99
|
-
min: 200,
|
|
100
|
-
step: 1,
|
|
101
|
-
hint: "送给模型的截图最长边(像素),默认 1568。调小可省 token,但太小会看不清控件。",
|
|
102
|
-
},
|
|
103
|
-
{
|
|
104
|
-
key: "deliveryMode",
|
|
105
|
-
label: "投递方式",
|
|
106
|
-
kind: "select",
|
|
107
|
-
options: [
|
|
108
|
-
{ value: "background", label: copy.deliveryBackground },
|
|
109
|
-
{ value: "foreground", label: copy.deliveryForeground },
|
|
110
|
-
],
|
|
111
|
-
hint: "后台投递不抢焦点;游戏等自绘表面会过滤按 pid 路由的事件,需要前台投递。",
|
|
112
|
-
},
|
|
113
|
-
{
|
|
114
|
-
key: "allowBringToFront",
|
|
115
|
-
label: "允许抢前台",
|
|
116
|
-
kind: "checkbox",
|
|
117
|
-
hint: "关闭时模型不能使用 bring_to_front。开启后模型可把目标窗口提到最前,代价是抢走你当前的焦点;仅当窗口被压在后台、内容不刷新时才需要。",
|
|
118
|
-
},
|
|
119
|
-
];
|
|
120
|
-
|
|
121
|
-
/** The three vision route fields, rendered together below the catalog. */
|
|
122
|
-
var ROUTE_KEYS = ["visionProvider", "visionModel", "visionReasoningEffort"];
|
|
123
|
-
|
|
124
|
-
var wrapStyle = {
|
|
125
|
-
display: "flex",
|
|
126
|
-
flexDirection: "column",
|
|
127
|
-
gap: "12px",
|
|
128
|
-
maxWidth: "620px",
|
|
129
|
-
color: "var(--dsw-alias-label-primary)",
|
|
130
|
-
fontSize: "13px",
|
|
131
|
-
lineHeight: 1.6,
|
|
132
|
-
};
|
|
133
|
-
var headingStyle = { fontSize: "14px", fontWeight: 600 };
|
|
134
|
-
var hintStyle = { color: "var(--dsw-alias-label-tertiary)" };
|
|
135
|
-
var rowStyle = { display: "flex", alignItems: "center", gap: "8px", flexWrap: "wrap" };
|
|
136
|
-
var fieldStyle = { display: "flex", alignItems: "center", gap: "8px", flexWrap: "wrap" };
|
|
137
|
-
var labelStyle = { minWidth: "140px" };
|
|
138
|
-
var controlStyle = {
|
|
139
|
-
padding: "6px 8px",
|
|
140
|
-
border: "1px solid var(--dsw-alias-border-l2)",
|
|
141
|
-
borderRadius: "6px",
|
|
142
|
-
background: "var(--dsw-alias-bg-layer-1)",
|
|
143
|
-
color: "inherit",
|
|
144
|
-
font: "inherit",
|
|
145
|
-
fontVariantNumeric: "tabular-nums",
|
|
146
|
-
};
|
|
147
|
-
var inputStyle = Object.assign({ width: "132px" }, controlStyle);
|
|
148
|
-
var textStyle = Object.assign({ width: "260px" }, controlStyle);
|
|
149
|
-
var selectStyle = Object.assign({ minWidth: "200px" }, controlStyle);
|
|
150
|
-
var buttonStyle = {
|
|
151
|
-
padding: "6px 12px",
|
|
152
|
-
border: "1px solid var(--dsw-alias-border-l2)",
|
|
153
|
-
borderRadius: "6px",
|
|
154
|
-
background: "var(--dsw-alias-bg-layer-1)",
|
|
155
|
-
color: "inherit",
|
|
156
|
-
font: "inherit",
|
|
157
|
-
cursor: "pointer",
|
|
158
|
-
};
|
|
159
|
-
var primaryButtonStyle = Object.assign({}, buttonStyle, {
|
|
160
|
-
border: "1px solid var(--dsw-alias-button-primary-fill)",
|
|
161
|
-
background: "var(--dsw-alias-button-primary-fill)",
|
|
162
|
-
color: "var(--dsw-alias-label-primary-foreground)",
|
|
163
|
-
});
|
|
164
|
-
var errorStyle = { color: "var(--dsw-alias-state-error-primary)" };
|
|
165
|
-
var badgeStyle = {
|
|
166
|
-
padding: "1px 6px",
|
|
167
|
-
borderRadius: "999px",
|
|
168
|
-
background: "var(--dsw-alias-bg-layer-2, rgba(0, 0, 0, 0.08))",
|
|
169
|
-
color: "var(--dsw-alias-label-secondary)",
|
|
170
|
-
fontSize: "12px",
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
function has(object, key) {
|
|
174
|
-
return Object.prototype.hasOwnProperty.call(object, key);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function objectLayer(snapshot, which) {
|
|
178
|
-
var layer = snapshot && snapshot[which];
|
|
179
|
-
return layer !== null && typeof layer === "object" ? layer : {};
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/** The schema-resolved section the Host last accepted. */
|
|
183
|
-
function accepted(snapshot) {
|
|
184
|
-
return objectLayer(snapshot, "value");
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/** The raw user layer; a key's presence here is what marks it overridden. */
|
|
188
|
-
function userLayer(snapshot) {
|
|
189
|
-
return objectLayer(snapshot, "user");
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function overridden(snapshot, key) {
|
|
193
|
-
return has(userLayer(snapshot), key);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/** The effective value for a field, as the string a control holds. */
|
|
197
|
-
function stored(snapshot, key) {
|
|
198
|
-
var value = accepted(snapshot)[key];
|
|
199
|
-
return value === undefined || value === null ? "" : String(value);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* Parse a draft string into the value to store.
|
|
204
|
-
* @throws {Error} when the draft does not satisfy the field's schema.
|
|
205
|
-
*/
|
|
206
|
-
function parse(field, raw) {
|
|
207
|
-
if (field.kind === "text") return raw;
|
|
208
|
-
if (field.kind === "select") return raw;
|
|
209
|
-
if (field.kind === "checkbox") return raw === "true";
|
|
210
|
-
var value = Number(raw);
|
|
211
|
-
if (!isFinite(value)) throw new Error(field.label + "必须是数字。");
|
|
212
|
-
if (field.step === 1 && !Number.isInteger(value)) throw new Error(field.label + "必须是整数。");
|
|
213
|
-
if (field.min !== undefined && value < field.min) throw new Error(field.label + "不能小于 " + field.min + "。");
|
|
214
|
-
if (field.max !== undefined && value > field.max) throw new Error(field.label + "不能大于 " + field.max + "。");
|
|
215
|
-
return value;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
76
|
/**
|
|
219
77
|
* Load this plugin's own vision-capable model list.
|
|
220
78
|
*
|
|
@@ -328,11 +186,15 @@ window.__ModuleLoader__.load({
|
|
|
328
186
|
* The row's configuration card.
|
|
329
187
|
*
|
|
330
188
|
* `scope` is the configuration form `apply` obtained for this entry from
|
|
331
|
-
* `configForms.get(NS)`; `
|
|
332
|
-
*
|
|
189
|
+
* `configForms.get(NS)`; `describe` is the schema mirror obtained from
|
|
190
|
+
* `configForms.describe()`; `ctx` is the scoped context supplying the live
|
|
191
|
+
* model directory.
|
|
333
192
|
*/
|
|
334
193
|
function DesktopAgentSettingsCard(props) {
|
|
194
|
+
if (props.view === "summary") return copy.summary;
|
|
195
|
+
|
|
335
196
|
var scope = props.scope;
|
|
197
|
+
var describe = props.describe;
|
|
336
198
|
var ctx = props.ctx;
|
|
337
199
|
|
|
338
200
|
var subscribe = React.useCallback(
|
|
@@ -352,417 +214,152 @@ window.__ModuleLoader__.load({
|
|
|
352
214
|
var catalog = useVisionCatalog(ctx);
|
|
353
215
|
var host = useHostCatalog(ctx);
|
|
354
216
|
|
|
355
|
-
var draftState = React.useState({});
|
|
356
|
-
var draft = draftState[0];
|
|
357
|
-
var setDraft = draftState[1];
|
|
358
|
-
var busyState = React.useState(false);
|
|
359
|
-
var busy = busyState[0];
|
|
360
|
-
var setBusy = busyState[1];
|
|
361
|
-
var errorState = React.useState(null);
|
|
362
|
-
var error = errorState[0];
|
|
363
|
-
var setError = errorState[1];
|
|
364
217
|
/** Whether the user chose to type the route instead of picking it. */
|
|
365
218
|
var manualState = React.useState(false);
|
|
366
219
|
var manual = manualState[0];
|
|
367
220
|
var setManual = manualState[1];
|
|
368
221
|
|
|
369
|
-
if (props.view === "summary") return copy.summary;
|
|
370
|
-
|
|
371
222
|
var writable = snapshot.writable === true;
|
|
372
|
-
var current =
|
|
373
|
-
|
|
374
|
-
/** Record one field's draft, leaving the others untouched. */
|
|
375
|
-
function edit(key, value) {
|
|
376
|
-
setDraft(function (previous) {
|
|
377
|
-
var next = {};
|
|
378
|
-
for (var name in previous) if (has(previous, name)) next[name] = previous[name];
|
|
379
|
-
next[key] = value;
|
|
380
|
-
return next;
|
|
381
|
-
});
|
|
382
|
-
}
|
|
223
|
+
var current = snapshot && snapshot.value && typeof snapshot.value === "object" ? snapshot.value : {};
|
|
383
224
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
* to the previous provider and would otherwise be saved as a route the
|
|
389
|
-
* Host cannot resolve.
|
|
390
|
-
*/
|
|
391
|
-
function editMany(patch) {
|
|
392
|
-
setDraft(function (previous) {
|
|
393
|
-
var next = {};
|
|
394
|
-
for (var name in previous) if (has(previous, name)) next[name] = previous[name];
|
|
395
|
-
for (var key in patch) if (has(patch, key)) next[key] = patch[key];
|
|
396
|
-
return next;
|
|
397
|
-
});
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
var allFields = FIELDS.concat([
|
|
401
|
-
{ key: "visionProvider", label: "视觉 provider", kind: "text" },
|
|
402
|
-
{ key: "visionModel", label: "视觉 model", kind: "text" },
|
|
403
|
-
{ key: "visionReasoningEffort", label: "视觉 reasoning effort", kind: "text" },
|
|
404
|
-
]);
|
|
405
|
-
|
|
406
|
-
/** Whether a field currently carries a user override. */
|
|
407
|
-
function isOverridden(field) {
|
|
408
|
-
return overridden(snapshot, field.key);
|
|
409
|
-
}
|
|
225
|
+
var groups = catalog.value.groups || [];
|
|
226
|
+
var provider = typeof current.visionProvider === "string" ? current.visionProvider : "";
|
|
227
|
+
var model = typeof current.visionModel === "string" ? current.visionModel : "";
|
|
228
|
+
var selected = matchModel(groups, provider, model);
|
|
410
229
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
var edits = [];
|
|
415
|
-
var invalid = null;
|
|
416
|
-
allFields.forEach(function (field) {
|
|
417
|
-
if (!has(draft, field.key)) return;
|
|
418
|
-
var raw = draft[field.key];
|
|
419
|
-
if (raw === "") {
|
|
420
|
-
if (isOverridden(field)) edits.push({ field: field, raw: raw });
|
|
421
|
-
return;
|
|
422
|
-
}
|
|
423
|
-
var value;
|
|
424
|
-
try {
|
|
425
|
-
value = parse(field, raw);
|
|
426
|
-
} catch (cause) {
|
|
427
|
-
if (invalid === null) invalid = String((cause && cause.message) || cause);
|
|
428
|
-
return;
|
|
429
|
-
}
|
|
430
|
-
if (String(value) !== String(current[field.key])) edits.push({ field: field, raw: raw, value: value });
|
|
230
|
+
var providers = [];
|
|
231
|
+
groups.forEach(function (group) {
|
|
232
|
+
if (providers.indexOf(group.id) === -1) providers.push(group.id);
|
|
431
233
|
});
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
/** Persist every edit, in order, one write at a time. */
|
|
438
|
-
function save() {
|
|
439
|
-
if (!dirty || blocked) return;
|
|
440
|
-
setError(null);
|
|
441
|
-
setBusy(true);
|
|
442
|
-
var chain = Promise.resolve();
|
|
443
|
-
edits.forEach(function (item) {
|
|
444
|
-
chain = chain.then(function () {
|
|
445
|
-
return item.raw === "" ? scope.unset(item.field.key) : scope.set(item.field.key, item.value);
|
|
446
|
-
});
|
|
234
|
+
var modelsForProvider = [];
|
|
235
|
+
groups.forEach(function (group) {
|
|
236
|
+
if (group.id !== provider) return;
|
|
237
|
+
(group.models || []).forEach(function (entry) {
|
|
238
|
+
modelsForProvider.push(entry);
|
|
447
239
|
});
|
|
448
|
-
|
|
449
|
-
function () {
|
|
450
|
-
setBusy(false);
|
|
451
|
-
setDraft({});
|
|
452
|
-
},
|
|
453
|
-
function (cause) {
|
|
454
|
-
setBusy(false);
|
|
455
|
-
setError(String((cause && cause.message) || cause));
|
|
456
|
-
},
|
|
457
|
-
);
|
|
458
|
-
}
|
|
240
|
+
});
|
|
459
241
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
var keys = allFields
|
|
464
|
-
.filter(function (field) {
|
|
465
|
-
return isOverridden(field);
|
|
466
|
-
})
|
|
467
|
-
.map(function (field) {
|
|
468
|
-
return field.key;
|
|
469
|
-
});
|
|
470
|
-
if (keys.length === 0) return;
|
|
471
|
-
setError(null);
|
|
472
|
-
setDraft({});
|
|
473
|
-
setBusy(true);
|
|
474
|
-
var chain = Promise.resolve();
|
|
475
|
-
keys.forEach(function (key) {
|
|
476
|
-
chain = chain.then(function () {
|
|
477
|
-
return scope.unset(key);
|
|
478
|
-
});
|
|
479
|
-
});
|
|
480
|
-
chain.then(
|
|
481
|
-
function () {
|
|
482
|
-
setBusy(false);
|
|
483
|
-
},
|
|
484
|
-
function (cause) {
|
|
485
|
-
setBusy(false);
|
|
486
|
-
setError(String((cause && cause.message) || cause));
|
|
487
|
-
},
|
|
488
|
-
);
|
|
489
|
-
}
|
|
242
|
+
var routeLost = (provider !== "" || model !== "") && selected === null;
|
|
243
|
+
var pickable = catalog.value.status === "ready" && providers.length > 0;
|
|
244
|
+
var usePicker = pickable && !manual && !routeLost;
|
|
490
245
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
246
|
+
function renderControl(field) {
|
|
247
|
+
if (!usePicker) {
|
|
248
|
+
return undefined;
|
|
249
|
+
}
|
|
495
250
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
FIELDS.forEach(function (field) {
|
|
502
|
-
var disabled = !writable || busy;
|
|
503
|
-
var control;
|
|
504
|
-
if (field.kind === "select") {
|
|
505
|
-
control = React.createElement(
|
|
506
|
-
"select",
|
|
507
|
-
{
|
|
508
|
-
style: selectStyle,
|
|
509
|
-
value: rawOf(field.key),
|
|
510
|
-
disabled: disabled,
|
|
511
|
-
"aria-label": field.label,
|
|
512
|
-
onChange: function (event) {
|
|
513
|
-
edit(field.key, event.target.value);
|
|
514
|
-
},
|
|
515
|
-
},
|
|
516
|
-
field.options.map(function (option) {
|
|
517
|
-
return React.createElement("option", { key: option.value, value: option.value }, option.label);
|
|
518
|
-
}),
|
|
519
|
-
);
|
|
520
|
-
} else if (field.kind === "checkbox") {
|
|
521
|
-
control = React.createElement("input", {
|
|
522
|
-
type: "checkbox",
|
|
523
|
-
checked: rawOf(field.key) === "true",
|
|
524
|
-
disabled: disabled,
|
|
525
|
-
"aria-label": field.label,
|
|
526
|
-
onChange: function (event) {
|
|
527
|
-
edit(field.key, event.target.checked ? "true" : "false");
|
|
528
|
-
},
|
|
529
|
-
});
|
|
530
|
-
} else if (field.kind === "number") {
|
|
531
|
-
control = React.createElement("input", {
|
|
532
|
-
type: "number",
|
|
533
|
-
min: field.min,
|
|
534
|
-
max: field.max,
|
|
535
|
-
step: field.step,
|
|
536
|
-
style: inputStyle,
|
|
537
|
-
value: rawOf(field.key),
|
|
538
|
-
disabled: disabled,
|
|
251
|
+
if (field.key === "visionProvider") {
|
|
252
|
+
return React.createElement(UI.Select, {
|
|
253
|
+
value: provider,
|
|
254
|
+
disabled: !writable,
|
|
255
|
+
label: "视觉 provider",
|
|
539
256
|
placeholder: copy.inherit,
|
|
540
|
-
"
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
257
|
+
options: [{ value: "", label: copy.inherit }].concat(
|
|
258
|
+
providers.map(function (id) {
|
|
259
|
+
return { value: id, label: id };
|
|
260
|
+
}),
|
|
261
|
+
),
|
|
262
|
+
onChange: function (value) {
|
|
263
|
+
var ops = [
|
|
264
|
+
value === "" ? { op: "unset", path: ["visionProvider"] } : { op: "set", path: ["visionProvider"], value: value },
|
|
265
|
+
{ op: "unset", path: ["visionModel"] },
|
|
266
|
+
{ op: "unset", path: ["visionReasoningEffort"] },
|
|
267
|
+
];
|
|
268
|
+
void scope.mutate(ops, snapshot.revision);
|
|
546
269
|
},
|
|
547
270
|
});
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
value:
|
|
553
|
-
disabled:
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (field.key === "visionModel") {
|
|
274
|
+
return React.createElement(UI.Select, {
|
|
275
|
+
value: selected === null ? "" : model,
|
|
276
|
+
disabled: !writable || provider === "",
|
|
277
|
+
label: "视觉 model",
|
|
554
278
|
placeholder: copy.inherit,
|
|
555
|
-
"
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
279
|
+
options: [{ value: "", label: copy.inherit }].concat(
|
|
280
|
+
modelsForProvider.map(function (entry) {
|
|
281
|
+
return { value: entry.id, label: entry.name || entry.id };
|
|
282
|
+
}),
|
|
283
|
+
),
|
|
284
|
+
onChange: function (value) {
|
|
285
|
+
var ops = [
|
|
286
|
+
value === "" ? { op: "unset", path: ["visionModel"] } : { op: "set", path: ["visionModel"], value: value },
|
|
287
|
+
{ op: "unset", path: ["visionReasoningEffort"] },
|
|
288
|
+
];
|
|
289
|
+
void scope.mutate(ops, snapshot.revision);
|
|
561
290
|
},
|
|
562
291
|
});
|
|
563
292
|
}
|
|
564
293
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
294
|
+
if (field.key === "visionReasoningEffort") {
|
|
295
|
+
var hostEntry = matchModel(host.groups, provider, model);
|
|
296
|
+
var reasoning = (hostEntry && hostEntry.reasoning) || null;
|
|
297
|
+
var effortOptions = reasoning ? reasoning.efforts || [] : [];
|
|
298
|
+
if (effortOptions.length > 0) {
|
|
299
|
+
var currentEffort = typeof current.visionReasoningEffort === "string" ? current.visionReasoningEffort : "";
|
|
300
|
+
var effortValue = currentEffort || reasoning.defaultEffort || "";
|
|
301
|
+
return React.createElement(UI.Select, {
|
|
302
|
+
value: effortValue,
|
|
303
|
+
disabled: !writable,
|
|
304
|
+
label: "视觉 reasoning effort",
|
|
305
|
+
placeholder: copy.inherit,
|
|
306
|
+
options: [{ value: "", label: copy.inherit }].concat(
|
|
307
|
+
effortOptions.map(function (effort) {
|
|
308
|
+
return { value: effort.id, label: effort.name || effort.id };
|
|
309
|
+
}),
|
|
310
|
+
),
|
|
311
|
+
onChange: function (value) {
|
|
312
|
+
var ops = [
|
|
313
|
+
value === ""
|
|
314
|
+
? { op: "unset", path: ["visionReasoningEffort"] }
|
|
315
|
+
: { op: "set", path: ["visionReasoningEffort"], value: value },
|
|
316
|
+
];
|
|
317
|
+
void scope.mutate(ops, snapshot.revision);
|
|
318
|
+
},
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
if (reasoning) {
|
|
322
|
+
return React.createElement("div", { className: "dsp-field-hint" }, copy.effortNone);
|
|
323
|
+
}
|
|
324
|
+
return React.createElement(UI.Select, {
|
|
325
|
+
value: "",
|
|
326
|
+
disabled: true,
|
|
327
|
+
label: "视觉 reasoning effort",
|
|
328
|
+
placeholder: copy.inherit,
|
|
329
|
+
options: [{ value: "", label: copy.inherit }],
|
|
330
|
+
onChange: function () {},
|
|
331
|
+
});
|
|
576
332
|
}
|
|
577
|
-
});
|
|
578
|
-
|
|
579
|
-
children.push(
|
|
580
|
-
React.createElement("hr", {
|
|
581
|
-
key: "route-sep",
|
|
582
|
-
style: { border: "none", borderTop: "1px solid var(--dsw-alias-border-l2)", margin: "4px 0" },
|
|
583
|
-
}),
|
|
584
|
-
);
|
|
585
|
-
children.push(React.createElement("div", { key: "route-heading", style: headingStyle }, "视觉模型路由"));
|
|
586
|
-
children.push(
|
|
587
|
-
React.createElement(
|
|
588
|
-
"div",
|
|
589
|
-
{ key: "route-hint", style: hintStyle },
|
|
590
|
-
"每步决策用哪个模型看图。全空 = 用会话当前模型;会话模型不支持图像时自动走 AX 元素表。",
|
|
591
|
-
),
|
|
592
|
-
);
|
|
593
333
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
var model = rawOf("visionModel");
|
|
597
|
-
var selected = matchModel(groups, provider, model);
|
|
598
|
-
var providers = [];
|
|
599
|
-
groups.forEach(function (group) {
|
|
600
|
-
if (providers.indexOf(group.id) === -1) providers.push(group.id);
|
|
601
|
-
});
|
|
602
|
-
var modelsForProvider = [];
|
|
603
|
-
groups.forEach(function (group) {
|
|
604
|
-
if (group.id !== provider) return;
|
|
605
|
-
(group.models || []).forEach(function (entry) {
|
|
606
|
-
modelsForProvider.push(entry);
|
|
607
|
-
});
|
|
608
|
-
});
|
|
334
|
+
return undefined;
|
|
335
|
+
}
|
|
609
336
|
|
|
610
|
-
|
|
611
|
-
// available. A stored route the catalog no longer offers (a provider that
|
|
612
|
-
// failed to load, or a model removed upstream) is never silently
|
|
613
|
-
// rewritten: the card falls back to manual entry and says so.
|
|
614
|
-
var pickable = catalog.value.status === "ready" && providers.length > 0;
|
|
615
|
-
var usePicker = pickable && !manual;
|
|
337
|
+
var children = [];
|
|
616
338
|
|
|
617
339
|
if (catalog.value.status === "loading") {
|
|
618
|
-
children.push(React.createElement(
|
|
340
|
+
children.push(React.createElement(UI.Banner, { key: "catalog-status", kind: "info" }, copy.catalogLoading));
|
|
619
341
|
} else if (catalog.value.status === "error") {
|
|
620
|
-
children.push(React.createElement(
|
|
342
|
+
children.push(React.createElement(UI.Banner, { key: "catalog-status", kind: "error" }, copy.catalogError));
|
|
621
343
|
} else if (providers.length === 0) {
|
|
622
|
-
children.push(React.createElement(
|
|
344
|
+
children.push(React.createElement(UI.Banner, { key: "catalog-status", kind: "info" }, copy.catalogEmpty));
|
|
623
345
|
} else if (catalog.value.failures > 0) {
|
|
624
|
-
children.push(React.createElement(
|
|
346
|
+
children.push(React.createElement(UI.Banner, { key: "catalog-status", kind: "warn" }, copy.catalogPartial));
|
|
625
347
|
}
|
|
626
348
|
|
|
627
|
-
if (
|
|
628
|
-
children.push(
|
|
629
|
-
React.createElement(
|
|
630
|
-
"div",
|
|
631
|
-
{ key: "route-provider", style: fieldStyle },
|
|
632
|
-
React.createElement("span", { style: labelStyle }, "provider"),
|
|
633
|
-
React.createElement(
|
|
634
|
-
"select",
|
|
635
|
-
{
|
|
636
|
-
style: selectStyle,
|
|
637
|
-
value: provider,
|
|
638
|
-
disabled: !writable || busy,
|
|
639
|
-
"aria-label": "视觉 provider",
|
|
640
|
-
onChange: function (event) {
|
|
641
|
-
// The previous model and effort belong to the previous
|
|
642
|
-
// provider, so they are cleared together with it.
|
|
643
|
-
editMany({ visionProvider: event.target.value, visionModel: "", visionReasoningEffort: "" });
|
|
644
|
-
},
|
|
645
|
-
},
|
|
646
|
-
[React.createElement("option", { key: "", value: "" }, copy.inherit)].concat(
|
|
647
|
-
providers.map(function (id) {
|
|
648
|
-
return React.createElement("option", { key: id, value: id }, id);
|
|
649
|
-
}),
|
|
650
|
-
),
|
|
651
|
-
),
|
|
652
|
-
overridden(snapshot, "visionProvider")
|
|
653
|
-
? React.createElement("span", { style: badgeStyle }, copy.overridden)
|
|
654
|
-
: null,
|
|
655
|
-
),
|
|
656
|
-
);
|
|
657
|
-
children.push(
|
|
658
|
-
React.createElement(
|
|
659
|
-
"div",
|
|
660
|
-
{ key: "route-model", style: fieldStyle },
|
|
661
|
-
React.createElement("span", { style: labelStyle }, "model"),
|
|
662
|
-
React.createElement(
|
|
663
|
-
"select",
|
|
664
|
-
{
|
|
665
|
-
style: selectStyle,
|
|
666
|
-
value: selected === null ? "" : model,
|
|
667
|
-
disabled: !writable || busy || provider === "",
|
|
668
|
-
"aria-label": "视觉 model",
|
|
669
|
-
onChange: function (event) {
|
|
670
|
-
editMany({ visionModel: event.target.value, visionReasoningEffort: "" });
|
|
671
|
-
},
|
|
672
|
-
},
|
|
673
|
-
[React.createElement("option", { key: "", value: "" }, copy.inherit)].concat(
|
|
674
|
-
modelsForProvider.map(function (entry) {
|
|
675
|
-
return React.createElement("option", { key: entry.id, value: entry.id }, entry.name || entry.id);
|
|
676
|
-
}),
|
|
677
|
-
),
|
|
678
|
-
),
|
|
679
|
-
overridden(snapshot, "visionModel") ? React.createElement("span", { style: badgeStyle }, copy.overridden) : null,
|
|
680
|
-
),
|
|
681
|
-
);
|
|
682
|
-
|
|
683
|
-
// The effort list comes from the HOST catalog, which does carry it; the
|
|
684
|
-
// vision route above only decides which models may appear at all.
|
|
685
|
-
var hostEntry = matchModel(host.groups, provider, model);
|
|
686
|
-
var reasoning = (hostEntry && hostEntry.reasoning) || null;
|
|
687
|
-
var effortOptions = reasoning ? reasoning.efforts || [] : [];
|
|
688
|
-
if (effortOptions.length > 0) {
|
|
689
|
-
var effortValue = rawOf("visionReasoningEffort") || reasoning.defaultEffort || "";
|
|
690
|
-
children.push(
|
|
691
|
-
React.createElement(
|
|
692
|
-
"div",
|
|
693
|
-
{ key: "route-effort", style: fieldStyle },
|
|
694
|
-
React.createElement("span", { style: labelStyle }, "reasoning effort"),
|
|
695
|
-
React.createElement(
|
|
696
|
-
"select",
|
|
697
|
-
{
|
|
698
|
-
style: selectStyle,
|
|
699
|
-
value: effortValue,
|
|
700
|
-
disabled: !writable || busy,
|
|
701
|
-
"aria-label": "视觉 reasoning effort",
|
|
702
|
-
onChange: function (event) {
|
|
703
|
-
edit("visionReasoningEffort", event.target.value);
|
|
704
|
-
},
|
|
705
|
-
},
|
|
706
|
-
[React.createElement("option", { key: "", value: "" }, copy.inherit)].concat(
|
|
707
|
-
effortOptions.map(function (effort) {
|
|
708
|
-
return React.createElement("option", { key: effort.id, value: effort.id }, effort.name || effort.id);
|
|
709
|
-
}),
|
|
710
|
-
),
|
|
711
|
-
),
|
|
712
|
-
overridden(snapshot, "visionReasoningEffort")
|
|
713
|
-
? React.createElement("span", { style: badgeStyle }, copy.overridden)
|
|
714
|
-
: null,
|
|
715
|
-
),
|
|
716
|
-
);
|
|
717
|
-
} else if (reasoning) {
|
|
718
|
-
children.push(
|
|
719
|
-
React.createElement("div", { key: "route-effort", style: hintStyle }, "reasoning effort:" + copy.effortNone),
|
|
720
|
-
);
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
if (selected === null && (provider !== "" || model !== "")) {
|
|
724
|
-
children.push(React.createElement("div", { key: "route-missing", style: hintStyle }, copy.routeMissing));
|
|
725
|
-
}
|
|
726
|
-
} else {
|
|
727
|
-
// Manual entry, also the fallback when the catalog failed: the card must
|
|
728
|
-
// stay usable when a provider is unreachable.
|
|
729
|
-
ROUTE_KEYS.forEach(function (key) {
|
|
730
|
-
var label = key === "visionProvider" ? "provider" : key === "visionModel" ? "model" : "reasoning effort";
|
|
731
|
-
children.push(
|
|
732
|
-
React.createElement(
|
|
733
|
-
"div",
|
|
734
|
-
{ key: "route-" + key, style: fieldStyle },
|
|
735
|
-
React.createElement("span", { style: labelStyle }, label),
|
|
736
|
-
React.createElement("input", {
|
|
737
|
-
type: "text",
|
|
738
|
-
style: textStyle,
|
|
739
|
-
value: rawOf(key),
|
|
740
|
-
disabled: !writable || busy,
|
|
741
|
-
placeholder: copy.inherit,
|
|
742
|
-
"aria-label": "视觉 " + label,
|
|
743
|
-
onChange: function (event) {
|
|
744
|
-
edit(key, event.target.value);
|
|
745
|
-
},
|
|
746
|
-
onKeyDown: function (event) {
|
|
747
|
-
if (event.key === "Enter") save();
|
|
748
|
-
},
|
|
749
|
-
}),
|
|
750
|
-
overridden(snapshot, key) ? React.createElement("span", { style: badgeStyle }, copy.overridden) : null,
|
|
751
|
-
),
|
|
752
|
-
);
|
|
753
|
-
});
|
|
349
|
+
if (catalog.value.status === "ready" && routeLost) {
|
|
350
|
+
children.push(React.createElement(UI.Banner, { key: "route-missing", kind: "warn" }, copy.routeMissing));
|
|
754
351
|
}
|
|
755
352
|
|
|
756
353
|
if (pickable) {
|
|
757
354
|
children.push(
|
|
758
355
|
React.createElement(
|
|
759
356
|
"div",
|
|
760
|
-
{ key: "route-mode", style:
|
|
357
|
+
{ key: "route-mode", style: { display: "flex", alignItems: "center", gap: "8px" } },
|
|
761
358
|
React.createElement(
|
|
762
|
-
|
|
359
|
+
UI.Button,
|
|
763
360
|
{
|
|
764
|
-
|
|
765
|
-
|
|
361
|
+
variant: "secondary",
|
|
362
|
+
size: "sm",
|
|
766
363
|
onClick: function () {
|
|
767
364
|
setManual(!manual);
|
|
768
365
|
},
|
|
@@ -773,42 +370,26 @@ window.__ModuleLoader__.load({
|
|
|
773
370
|
);
|
|
774
371
|
}
|
|
775
372
|
|
|
776
|
-
children.push(
|
|
373
|
+
children.push(
|
|
374
|
+
React.createElement("div", { key: "route-fallback", className: "dsp-field-hint" }, copy.catalogFallback),
|
|
375
|
+
);
|
|
777
376
|
|
|
778
|
-
if (invalid !== null) children.push(React.createElement("div", { key: "invalid", style: errorStyle }, invalid));
|
|
779
|
-
if (error !== null) children.push(React.createElement("div", { key: "error", style: errorStyle }, error));
|
|
780
377
|
if (!writable && snapshot.status === "ready") {
|
|
781
|
-
children.push(React.createElement(
|
|
378
|
+
children.push(React.createElement(UI.Banner, { key: "unwritable", kind: "warn" }, copy.unwritable));
|
|
782
379
|
}
|
|
783
380
|
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
},
|
|
796
|
-
busy ? copy.saving : copy.save,
|
|
797
|
-
),
|
|
798
|
-
React.createElement(
|
|
799
|
-
"button",
|
|
800
|
-
{
|
|
801
|
-
type: "button",
|
|
802
|
-
style: buttonStyle,
|
|
803
|
-
disabled: !writable || busy || !anyOverridden,
|
|
804
|
-
onClick: reset,
|
|
805
|
-
},
|
|
806
|
-
copy.reset,
|
|
807
|
-
),
|
|
808
|
-
),
|
|
381
|
+
return React.createElement(
|
|
382
|
+
UI.ConfigForm,
|
|
383
|
+
{
|
|
384
|
+
namespace: NS,
|
|
385
|
+
form: scope,
|
|
386
|
+
describe: describe,
|
|
387
|
+
title: copy.heading,
|
|
388
|
+
description: copy.intro,
|
|
389
|
+
renderControl: renderControl,
|
|
390
|
+
},
|
|
391
|
+
children,
|
|
809
392
|
);
|
|
810
|
-
|
|
811
|
-
return React.createElement("div", { style: wrapStyle }, children);
|
|
812
393
|
}
|
|
813
394
|
|
|
814
395
|
/**
|
|
@@ -821,11 +402,16 @@ window.__ModuleLoader__.load({
|
|
|
821
402
|
* just for this card. Nested, a host without the settings UI loses only this
|
|
822
403
|
* card.
|
|
823
404
|
*
|
|
405
|
+
* The nested context inherits the services declared here (`slots`,
|
|
406
|
+
* `remote`, `remote.session`), so the card is handed the scoped context and
|
|
407
|
+
* can reach both the form and the model directory through it.
|
|
408
|
+
*
|
|
824
409
|
* @param ctx - client root context.
|
|
825
410
|
*/
|
|
826
411
|
function apply(ctx) {
|
|
827
412
|
ctx.inject(["configForms"], function (scoped) {
|
|
828
413
|
var scope = scoped.configForms.get(NS);
|
|
414
|
+
var describe = scoped.configForms.describe();
|
|
829
415
|
BUNDLE_NAMES.forEach(function (bundle) {
|
|
830
416
|
scoped.slots.inject("plugins.row.config", function () {
|
|
831
417
|
return scoped.slots.register(
|
|
@@ -833,7 +419,7 @@ window.__ModuleLoader__.load({
|
|
|
833
419
|
name: "plugins.row.config",
|
|
834
420
|
key: bundle + "#" + ROW_ID,
|
|
835
421
|
inject: function () {
|
|
836
|
-
return { scope: scope, ctx: scoped };
|
|
422
|
+
return { scope: scope, describe: describe, ctx: scoped };
|
|
837
423
|
},
|
|
838
424
|
},
|
|
839
425
|
DesktopAgentSettingsCard,
|
package/lib/config.js
CHANGED
|
@@ -76,13 +76,52 @@ export const DELIVERY_MODES = ['background', 'foreground'];
|
|
|
76
76
|
* opt into, never a default this plugin picks on their behalf.
|
|
77
77
|
*/
|
|
78
78
|
export const Config = z.object({
|
|
79
|
-
visionProvider: z
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
79
|
+
visionProvider: z
|
|
80
|
+
.string()
|
|
81
|
+
.default('')
|
|
82
|
+
.comment('视觉 provider')
|
|
83
|
+
.description('每步决策用哪个模型看图。全空 = 用会话当前模型;会话模型不支持图像时自动走 AX 元素表。')
|
|
84
|
+
.volatile(),
|
|
85
|
+
visionModel: z
|
|
86
|
+
.string()
|
|
87
|
+
.default('')
|
|
88
|
+
.comment('视觉 model')
|
|
89
|
+
.description('')
|
|
90
|
+
.volatile(),
|
|
91
|
+
visionReasoningEffort: z
|
|
92
|
+
.string()
|
|
93
|
+
.default('')
|
|
94
|
+
.comment('视觉 reasoning effort')
|
|
95
|
+
.description('')
|
|
96
|
+
.volatile(),
|
|
97
|
+
maxSteps: z
|
|
98
|
+
.number()
|
|
99
|
+
.step(1)
|
|
100
|
+
.min(1)
|
|
101
|
+
.default(DEFAULT_MAX_STEPS)
|
|
102
|
+
.comment('步数上限')
|
|
103
|
+
.description('一次任务最多执行多少个动作,默认 40。')
|
|
104
|
+
.volatile(),
|
|
105
|
+
maxImageDimension: z
|
|
106
|
+
.number()
|
|
107
|
+
.step(1)
|
|
108
|
+
.min(200)
|
|
109
|
+
.default(1568)
|
|
110
|
+
.comment('截图长边上限')
|
|
111
|
+
.description('送给模型的截图最长边(像素),默认 1568。调小可省 token,但太小会看不清控件。')
|
|
112
|
+
.volatile(),
|
|
113
|
+
deliveryMode: z
|
|
114
|
+
.union(DELIVERY_MODES)
|
|
115
|
+
.default('background')
|
|
116
|
+
.comment('投递方式')
|
|
117
|
+
.description('后台投递不抢焦点;游戏等自绘表面会过滤按 pid 路由的事件,需要前台投递。')
|
|
118
|
+
.volatile(),
|
|
119
|
+
allowBringToFront: z
|
|
120
|
+
.boolean()
|
|
121
|
+
.default(false)
|
|
122
|
+
.comment('允许抢前台')
|
|
123
|
+
.description('关闭时模型不能使用 bring_to_front。开启后模型可把目标窗口提到最前,代价是抢走你当前的焦点;仅当窗口被压在后台、内容不刷新时才需要。')
|
|
124
|
+
.volatile(),
|
|
86
125
|
});
|
|
87
126
|
|
|
88
127
|
/**
|
package/lib/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* dsh-desktop-agent — host half.
|
|
3
3
|
*
|
|
4
4
|
* Registers one model-facing tool, `desktop_agent`, that runs a native-vision
|
|
5
|
-
* decision loop against the host desktop, plus the
|
|
5
|
+
* decision loop against the host desktop, plus the same-origin route the settings
|
|
6
6
|
* card reads its model list from.
|
|
7
7
|
*
|
|
8
8
|
* The plugin owns no execution engine of its own. Every action is dispatched to
|
|
@@ -220,7 +220,7 @@ export async function apply(ctx, config, loadSdk) {
|
|
|
220
220
|
ctx.inject(['webServer'], (scoped) => {
|
|
221
221
|
scoped.effect(
|
|
222
222
|
() => {
|
|
223
|
-
const disposers = makeRoutes({ llm }).map((route) => scoped.webServer.register(route));
|
|
223
|
+
const disposers = makeRoutes({ llm, connection: () => scoped.get('connection') }).map((route) => scoped.webServer.register(route));
|
|
224
224
|
return () => {
|
|
225
225
|
for (const dispose of disposers) dispose();
|
|
226
226
|
};
|
package/lib/routes.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The /api/dsh-desktop-agent route family: a
|
|
2
|
+
* The /api/dsh-desktop-agent route family: a bridge that answers
|
|
3
3
|
* one question the browser half cannot answer for itself.
|
|
4
4
|
*
|
|
5
5
|
* The settings card must list only models that can actually see, and the fact it
|
|
@@ -23,32 +23,33 @@ import { visionCatalog } from './route.js';
|
|
|
23
23
|
export const VISION_MODELS_API = '/api/dsh-desktop-agent/vision-models';
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
|
-
*
|
|
27
|
-
* bridge and dsh-ssh).
|
|
26
|
+
* Ask the composition whether one request may be served.
|
|
28
27
|
*
|
|
28
|
+
* `connection.requestRejection` is the deployment's single trust fence: it
|
|
29
|
+
* applies the Host/Origin checks (loopback plus the LAN authorities the
|
|
30
|
+
* deployment declares) and the browser authentication that rides with them.
|
|
31
|
+
* This plugin answers with the verdict instead of re-deriving it, because the
|
|
32
|
+
* declared LAN authorities live in `connection`'s config and a local check
|
|
33
|
+
* cannot see them.
|
|
34
|
+
*
|
|
35
|
+
* A missing `connection` refuses rather than serves: these routes sit in front
|
|
36
|
+
* of Connection's `/api` prefix route, so serving them without its fence would
|
|
37
|
+
* serve them unfenced.
|
|
38
|
+
*
|
|
39
|
+
* @param connection - the composition's Connection service, when it is up.
|
|
29
40
|
* @param request - the incoming request.
|
|
30
|
-
* @
|
|
41
|
+
* @param response - the response, written to when the request is refused.
|
|
42
|
+
* @returns whether the request was refused.
|
|
31
43
|
*/
|
|
32
|
-
function
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if (typeof host !== 'string') return false;
|
|
37
|
-
let hostUrl;
|
|
38
|
-
try {
|
|
39
|
-
hostUrl = new URL(`http://${host}`);
|
|
40
|
-
} catch {
|
|
41
|
-
return false;
|
|
42
|
-
}
|
|
43
|
-
if (hostUrl.hostname !== '127.0.0.1' && hostUrl.hostname !== 'localhost' && hostUrl.hostname !== '[::1]') return false;
|
|
44
|
-
if (request.headers['sec-fetch-site'] === 'cross-site') return false;
|
|
45
|
-
const origin = request.headers.origin;
|
|
46
|
-
if (origin === undefined) return true;
|
|
47
|
-
try {
|
|
48
|
-
return new URL(origin).host === hostUrl.host;
|
|
49
|
-
} catch {
|
|
50
|
-
return false;
|
|
44
|
+
function refuseUntrusted(connection, request, response) {
|
|
45
|
+
if (connection === undefined) {
|
|
46
|
+
writeJson(response, 403, { error: 'forbidden' });
|
|
47
|
+
return true;
|
|
51
48
|
}
|
|
49
|
+
const rejection = connection.requestRejection(request);
|
|
50
|
+
if (rejection === undefined) return false;
|
|
51
|
+
writeJson(response, rejection, { error: rejection === 401 ? 'unauthorized' : 'forbidden' });
|
|
52
|
+
return true;
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
/** Write one JSON response. */
|
|
@@ -66,7 +67,11 @@ function writeJson(response, status, body) {
|
|
|
66
67
|
/**
|
|
67
68
|
* Build the route family.
|
|
68
69
|
*
|
|
69
|
-
* @param deps - the host
|
|
70
|
+
* @param deps - the host dependencies.
|
|
71
|
+
* @param deps.llm - the host's `ctx.llm` service.
|
|
72
|
+
* @param deps.connection - resolves the composition's trust fence per request.
|
|
73
|
+
* Resolved per call rather than captured: a composed row order does not imply
|
|
74
|
+
* an activation order, so the service can still be absent when routes are built.
|
|
70
75
|
* @returns the routes to register.
|
|
71
76
|
*/
|
|
72
77
|
export function makeRoutes(deps) {
|
|
@@ -75,10 +80,7 @@ export function makeRoutes(deps) {
|
|
|
75
80
|
kind: 'exact',
|
|
76
81
|
path: VISION_MODELS_API,
|
|
77
82
|
async handler(request, response) {
|
|
78
|
-
if (
|
|
79
|
-
writeJson(response, 403, { error: 'loopback-only' });
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
83
|
+
if (refuseUntrusted(deps.connection(), request, response)) return;
|
|
82
84
|
if (request.method !== 'GET' && request.method !== 'POST') {
|
|
83
85
|
writeJson(response, 405, { error: 'method-not-allowed' });
|
|
84
86
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@logictan/dsh-desktop-agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "DSH desktop agent: a native-vision decision loop that drives the host desktop through the Cua Driver tools. DSH 桌面 Agent 子插件。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
"patch": "./cordis.patch.yml"
|
|
15
15
|
},
|
|
16
16
|
"client": {
|
|
17
|
+
"external": [
|
|
18
|
+
"@logictan/dsh-plugins-shared-client"
|
|
19
|
+
],
|
|
17
20
|
"inject": [
|
|
18
21
|
"slots",
|
|
19
22
|
"remote",
|
|
@@ -54,16 +57,21 @@
|
|
|
54
57
|
},
|
|
55
58
|
"peerDependencies": {
|
|
56
59
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
60
|
+
"@deepseek-ai/dsh-client-connection": "^0.1.5-alpha.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1 || ^0.1.7-rc.1 || ^0.2.0-rc.1 || ^0.2.0-rc.2",
|
|
57
61
|
"@deepseek-ai/dsh-llm": "^0.1.5-alpha.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1 || ^0.1.7-rc.1 || ^0.2.0-rc.1 || ^0.2.0-rc.2",
|
|
58
62
|
"@deepseek-ai/dsh-system-prompt": "^0.1.5-alpha.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1 || ^0.1.7-rc.1 || ^0.2.0-rc.1 || ^0.2.0-rc.2",
|
|
59
63
|
"@deepseek-ai/dsh-mcp-client": "^0.1.5-alpha.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1 || ^0.1.7-rc.1 || ^0.2.0-rc.1 || ^0.2.0-rc.2",
|
|
60
64
|
"@deepseek-ai/dsh-tools": "^0.1.5-alpha.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1 || ^0.1.7-rc.1 || ^0.2.0-rc.1 || ^0.2.0-rc.2",
|
|
61
|
-
"@deepseek-ai/schemastery": "^3.18.3"
|
|
65
|
+
"@deepseek-ai/schemastery": "^3.18.3",
|
|
66
|
+
"@logictan/dsh-plugins-shared-client": "^0.1.0"
|
|
62
67
|
},
|
|
63
68
|
"peerDependenciesMeta": {
|
|
64
69
|
"@deepseek-ai/cordis": {
|
|
65
70
|
"optional": true
|
|
66
71
|
},
|
|
72
|
+
"@deepseek-ai/dsh-client-connection": {
|
|
73
|
+
"optional": true
|
|
74
|
+
},
|
|
67
75
|
"@deepseek-ai/dsh-llm": {
|
|
68
76
|
"optional": true
|
|
69
77
|
},
|
|
@@ -78,6 +86,9 @@
|
|
|
78
86
|
},
|
|
79
87
|
"@deepseek-ai/schemastery": {
|
|
80
88
|
"optional": true
|
|
89
|
+
},
|
|
90
|
+
"@logictan/dsh-plugins-shared-client": {
|
|
91
|
+
"optional": true
|
|
81
92
|
}
|
|
82
93
|
},
|
|
83
94
|
"dependencies": {
|
|
@@ -85,6 +96,7 @@
|
|
|
85
96
|
},
|
|
86
97
|
"devDependencies": {
|
|
87
98
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
99
|
+
"@deepseek-ai/dsh-client-connection": "^0.2.0-rc.2",
|
|
88
100
|
"@deepseek-ai/dsh-llm": "^0.2.0-rc.2",
|
|
89
101
|
"@deepseek-ai/dsh-mcp-client": "^0.2.0-rc.2",
|
|
90
102
|
"@deepseek-ai/dsh-system-prompt": "^0.2.0-rc.2",
|