dsh-tool-vision 0.10.0 → 0.10.2
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 +14 -0
- package/README.zh.md +373 -361
- package/client.js +163 -71
- package/cordis.patch.yml +8 -8
- package/index.js +8 -8
- package/lib/model-probe.js +42 -11
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +15 -11
package/client.js
CHANGED
|
@@ -61,6 +61,37 @@ window.__ModuleLoader__.load({
|
|
|
61
61
|
".__tv_itemId{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}" +
|
|
62
62
|
".__tv_itemName{color:var(--dsw-alias-label-tertiary);font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:38%}" +
|
|
63
63
|
".__tv_unavailable{font-size:13px;color:var(--dsw-alias-label-tertiary)}";
|
|
64
|
+
// Scoped flat controls retain native keyboard and form behavior.
|
|
65
|
+
CSS += `
|
|
66
|
+
.dsh-flat.__tv_root{width:100%;max-width:720px;gap:14px;font-size:13px;line-height:1.65;color:var(--dsw-alias-label-primary);--flat-accent:var(--dsw-alias-state-business-primary,#3964fe);--flat-border:var(--dsw-alias-border-l2,#dce2eb)}
|
|
67
|
+
.dsh-flat.__tv_root *{box-sizing:border-box;min-width:0}
|
|
68
|
+
.dsh-flat.__tv_root p{margin:0}
|
|
69
|
+
.dsh-flat.__tv_root label[class$="_field"]{gap:7px}
|
|
70
|
+
.dsh-flat.__tv_root [class$="_label"]{font-size:13px;font-weight:500}
|
|
71
|
+
.dsh-flat.__tv_root [class$="_hint"]{font-size:12px;line-height:1.65}
|
|
72
|
+
.dsh-flat.__tv_root input:not([type=checkbox]),.dsh-flat.__tv_root select,.dsh-flat.__tv_root textarea{width:100%;border:1px solid var(--flat-border);border-radius:6px;background:var(--dsw-alias-bg-layer-3);color:inherit;font:inherit;padding:9px 12px;min-height:40px;box-shadow:none;transition:border-color .15s}
|
|
73
|
+
.dsh-flat.__tv_root input:hover:not(:disabled),.dsh-flat.__tv_root select:hover:not(:disabled),.dsh-flat.__tv_root textarea:hover:not(:disabled){border-color:var(--dsw-alias-label-tertiary)}
|
|
74
|
+
.dsh-flat.__tv_root select{appearance:none;padding-right:34px;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='m2 2 4 4 4-4' fill='none' stroke='%23778091' stroke-width='1.5'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 12px center}
|
|
75
|
+
.dsh-flat.__tv_root input[type=checkbox]{appearance:none;flex:none;width:30px;height:18px;margin:0;border:1px solid var(--flat-border);border-radius:12px;background:var(--dsw-alias-bg-layer-2);position:relative;cursor:pointer;transition:background .15s,border-color .15s}
|
|
76
|
+
.dsh-flat.__tv_root input[type=checkbox]::before{content:"";position:absolute;left:2px;top:2px;width:12px;height:12px;border-radius:50%;background:var(--dsw-alias-label-secondary);transition:transform .15s}
|
|
77
|
+
.dsh-flat.__tv_root input[type=checkbox]:checked{background:var(--flat-accent);border-color:var(--flat-accent)}
|
|
78
|
+
.dsh-flat.__tv_root input[type=checkbox]:checked::before{transform:translateX(12px);background:#fff}
|
|
79
|
+
.dsh-flat.__tv_root :is(input,select,textarea,button,summary,a):focus-visible{outline:2px solid var(--flat-accent);outline-offset:3px}
|
|
80
|
+
.dsh-flat.__tv_root :is(input,select,textarea,button):disabled{opacity:.5;cursor:default}
|
|
81
|
+
.dsh-flat.__tv_root [class$="_actions"]{flex-wrap:wrap;gap:10px;margin-top:4px;padding-top:16px;border-top:1px solid var(--flat-border)}
|
|
82
|
+
.dsh-flat.__tv_root details{border-top:1px solid var(--flat-border);padding:0}
|
|
83
|
+
.dsh-flat.__tv_root summary{list-style:none;display:flex;align-items:center;justify-content:space-between;gap:12px;padding:14px 0;font-size:13px;font-weight:500;cursor:pointer;color:var(--dsw-alias-label-secondary)}
|
|
84
|
+
.dsh-flat.__tv_root summary::-webkit-details-marker{display:none}
|
|
85
|
+
.dsh-flat.__tv_root summary::after{content:"+";font-size:18px;font-weight:400;flex:none}
|
|
86
|
+
.dsh-flat.__tv_root details[open]>summary::after{content:"−"}
|
|
87
|
+
.dsh-flat.__tv_root details>div{padding-bottom:18px}
|
|
88
|
+
.dsh-flat.__tv_root .__tv_master{padding:0 0 16px;margin:0;border:0;border-bottom:1px solid var(--flat-border);border-radius:0;background:none}
|
|
89
|
+
.dsh-flat.__tv_root .__tv_catalog{padding:0;border:0;background:none;gap:8px}
|
|
90
|
+
@media(max-width:480px){.dsh-flat.__tv_root{gap:16px}.dsh-flat.__tv_root .__tv_master{flex-wrap:wrap}}
|
|
91
|
+
@media(prefers-reduced-motion:reduce){.dsh-flat.__tv_root *,.dsh-flat.__tv_root input[type=checkbox]::before{transition:none}}
|
|
92
|
+
.dsh-flat.__tv_root button:not(.__tv_link){border-radius:6px;min-height:34px;padding:7px 14px;font:inherit;font-size:12px;box-shadow:none}
|
|
93
|
+
.dsh-flat.__tv_root :is(h2,h3){margin:0;font-size:14px;font-weight:600}
|
|
94
|
+
`;
|
|
64
95
|
var tagId = "dsh-tool-vision/main.css";
|
|
65
96
|
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
66
97
|
var tag = document.createElement("style");
|
|
@@ -74,8 +105,16 @@ window.__ModuleLoader__.load({
|
|
|
74
105
|
var NS = "toolVision";
|
|
75
106
|
var inject = ["slots", "locale", "configForms"];
|
|
76
107
|
var zh = {
|
|
108
|
+
imageMode: "图片处理模式",
|
|
109
|
+
imageModeAuto: "自动接图与预览(推荐)",
|
|
110
|
+
imageModeTools: "仅使用视觉工具",
|
|
111
|
+
imageModeCustom: "自定义组合",
|
|
112
|
+
imageModeHint: "自动模式为纯文本模型接收并桥接图片,同时显示缩略图;仅工具模式关闭自动接图、桥接和预览,仍可调用视觉工具。选择后点击保存。",
|
|
113
|
+
advanced: "高级设置:模型覆盖、桥接与请求参数",
|
|
114
|
+
connectionTitle: "外置视觉模型",
|
|
77
115
|
nav: "视觉模型",
|
|
78
|
-
|
|
116
|
+
imagePreview: "图片预览",
|
|
117
|
+
intro: "填写视觉模型连接信息,即可用工具分析图片。保存后即时生效;密钥留空保持原值。",
|
|
79
118
|
apiKeyHint: "留空保持当前密钥。密钥只写不读,不会回显。",
|
|
80
119
|
maxTokens: "最大输出 Tokens",
|
|
81
120
|
timeoutMs: "请求超时(毫秒)",
|
|
@@ -163,8 +202,16 @@ window.__ModuleLoader__.load({
|
|
|
163
202
|
notApplied: "写入未生效"
|
|
164
203
|
};
|
|
165
204
|
var en = {
|
|
205
|
+
imageMode: "Image handling",
|
|
206
|
+
imageModeAuto: "Automatic images and previews (recommended)",
|
|
207
|
+
imageModeTools: "Vision tools only",
|
|
208
|
+
imageModeCustom: "Custom combination",
|
|
209
|
+
imageModeHint: "Automatic accepts and bridges images for text-only models and shows thumbnails. Tools only disables automatic image admission, bridging and previews while keeping vision tools available. Click Save to apply your selection.",
|
|
210
|
+
advanced: "Advanced: model overrides, bridge and request options",
|
|
211
|
+
connectionTitle: "External vision model",
|
|
166
212
|
nav: "Vision Model",
|
|
167
|
-
|
|
213
|
+
imagePreview: "Image preview",
|
|
214
|
+
intro: "Connect a vision model to analyze images with tools. Saved changes apply immediately; blank keys stay unchanged.",
|
|
168
215
|
apiKeyHint: "Leave blank to keep the current key. The key is write-only and never echoed.",
|
|
169
216
|
maxTokens: "Max output tokens",
|
|
170
217
|
timeoutMs: "Request timeout (ms)",
|
|
@@ -280,6 +327,9 @@ window.__ModuleLoader__.load({
|
|
|
280
327
|
{ key: "sessionHeaderName", label: "fieldSessionHeaderName", type: "text" },
|
|
281
328
|
{ key: "sessionId", label: "fieldSessionId", type: "text" }
|
|
282
329
|
];
|
|
330
|
+
var BASIC_KEYS = ["baseURL", "apiKey", "model", "desktopScreenshot"];
|
|
331
|
+
var IMAGE_MODE_KEYS = ["bridgeTextOnly", "bridgeAutoImage", "bridgePreview"];
|
|
332
|
+
var IMAGE_MODES = { auto: [true, true, true], tools: [false, false, false] };
|
|
283
333
|
var ZH_HINTS = {
|
|
284
334
|
apiKey: "apiKeyHint",
|
|
285
335
|
maxTokens: "maxTokens",
|
|
@@ -303,6 +353,7 @@ window.__ModuleLoader__.load({
|
|
|
303
353
|
|
|
304
354
|
// ── component ─────────────────────────────────────────────────────────
|
|
305
355
|
function VisionSection(props) {
|
|
356
|
+
useLocale(props.locale);
|
|
306
357
|
var t = props.t;
|
|
307
358
|
var scope = props.scope;
|
|
308
359
|
var [snapshot, setSnapshot] = react.useState(function () { return scope.getSnapshot(); });
|
|
@@ -332,7 +383,7 @@ window.__ModuleLoader__.load({
|
|
|
332
383
|
// so depending on snapshot.value would reset user input on every render
|
|
333
384
|
// (typing appears dead).
|
|
334
385
|
react.useEffect(function () {
|
|
335
|
-
if (ready) setDraft(
|
|
386
|
+
if (ready) setDraft({});
|
|
336
387
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
337
388
|
}, [ready]);
|
|
338
389
|
|
|
@@ -367,6 +418,7 @@ window.__ModuleLoader__.load({
|
|
|
367
418
|
var user = snapshot.user || {};
|
|
368
419
|
|
|
369
420
|
function fieldDraft(f) {
|
|
421
|
+
if (f.secret) return draft[f.key] || "";
|
|
370
422
|
if (f.type === "csv" || f.type === "modellist") return draft[f.key] !== void 0 ? draft[f.key] : draftToCsv(value[f.key]);
|
|
371
423
|
if (f.type === "checkbox") return draft[f.key] !== void 0 ? draft[f.key] : Boolean(value[f.key]);
|
|
372
424
|
return draft[f.key] !== void 0 ? draft[f.key] : String(value[f.key] ?? "");
|
|
@@ -380,6 +432,23 @@ window.__ModuleLoader__.load({
|
|
|
380
432
|
setNotice(null);
|
|
381
433
|
setError(null);
|
|
382
434
|
}
|
|
435
|
+
function imageMode() {
|
|
436
|
+
var flags = IMAGE_MODE_KEYS.map(function (key) {
|
|
437
|
+
return Object.prototype.hasOwnProperty.call(draft, key) ? draft[key] : value[key] !== false;
|
|
438
|
+
});
|
|
439
|
+
return Object.keys(IMAGE_MODES).find(function (mode) {
|
|
440
|
+
return IMAGE_MODES[mode].every(function (flag, i) { return flag === flags[i]; });
|
|
441
|
+
}) || "custom";
|
|
442
|
+
}
|
|
443
|
+
function setImageMode(mode) {
|
|
444
|
+
if (!IMAGE_MODES[mode] || busy || !snapshot.writable) return;
|
|
445
|
+
setDraft(function (prev) {
|
|
446
|
+
var next = Object.assign({}, prev);
|
|
447
|
+
IMAGE_MODE_KEYS.forEach(function (key, i) { next[key] = IMAGE_MODES[mode][i]; });
|
|
448
|
+
return next;
|
|
449
|
+
});
|
|
450
|
+
setNotice(null); setError(null);
|
|
451
|
+
}
|
|
383
452
|
|
|
384
453
|
// One atomic mutation carrying ONE revision fence.
|
|
385
454
|
//
|
|
@@ -393,6 +462,7 @@ window.__ModuleLoader__.load({
|
|
|
393
462
|
function buildOps() {
|
|
394
463
|
var ops = [];
|
|
395
464
|
FIELDS.forEach(function (f) {
|
|
465
|
+
if (!Object.prototype.hasOwnProperty.call(draft, f.key)) return;
|
|
396
466
|
var d = fieldDraft(f);
|
|
397
467
|
if (f.type === "csv" || f.type === "modellist") {
|
|
398
468
|
var arr = String(d).split(",").map(function (s) { return s.trim(); }).filter(Boolean);
|
|
@@ -403,8 +473,7 @@ window.__ModuleLoader__.load({
|
|
|
403
473
|
}
|
|
404
474
|
if (f.type === "checkbox") {
|
|
405
475
|
if (Boolean(d) === Boolean(value[f.key])) return;
|
|
406
|
-
ops.push(
|
|
407
|
-
: { op: "unset", path: [f.key] });
|
|
476
|
+
ops.push({ op: "set", path: [f.key], value: Boolean(d) });
|
|
408
477
|
return;
|
|
409
478
|
}
|
|
410
479
|
if (f.type === "password") {
|
|
@@ -443,7 +512,8 @@ window.__ModuleLoader__.load({
|
|
|
443
512
|
*/
|
|
444
513
|
function commit(ops) {
|
|
445
514
|
var run;
|
|
446
|
-
|
|
515
|
+
var atomic = typeof scope.mutate === "function";
|
|
516
|
+
if (atomic) {
|
|
447
517
|
run = scope.mutate(ops, scope.getSnapshot().revision);
|
|
448
518
|
} else {
|
|
449
519
|
run = ops.reduce(function (chain, op) {
|
|
@@ -452,7 +522,9 @@ window.__ModuleLoader__.load({
|
|
|
452
522
|
});
|
|
453
523
|
}, Promise.resolve());
|
|
454
524
|
}
|
|
455
|
-
return Promise.resolve(run).then(function () {
|
|
525
|
+
return Promise.resolve(run).then(function (accepted) {
|
|
526
|
+
if (atomic && accepted === false) return false;
|
|
527
|
+
if (atomic && accepted === true) return true;
|
|
456
528
|
return opsApplied(ops, scope.getSnapshot());
|
|
457
529
|
});
|
|
458
530
|
}
|
|
@@ -460,61 +532,54 @@ window.__ModuleLoader__.load({
|
|
|
460
532
|
function reportOutcome(ok, okMessage) {
|
|
461
533
|
setBusy(false);
|
|
462
534
|
if (ok) {
|
|
463
|
-
setNotice(okMessage ||
|
|
535
|
+
setNotice({ key: okMessage || "saved" });
|
|
464
536
|
return;
|
|
465
537
|
}
|
|
466
|
-
setError(
|
|
467
|
-
|
|
538
|
+
setError({ key: "error", detailKey: "notApplied" });
|
|
539
|
+
setSnapshot(scope.getSnapshot());
|
|
468
540
|
}
|
|
469
541
|
|
|
470
542
|
function onSave() {
|
|
543
|
+
if (busy || !snapshot.writable) return;
|
|
471
544
|
setBusy(true); setNotice(null); setError(null);
|
|
472
545
|
var ops = buildOps();
|
|
473
|
-
if (ops.length === 0) { setBusy(false); setNotice(
|
|
546
|
+
if (ops.length === 0) { setBusy(false); setDraft({}); setNotice({ key: "saved" }); return; }
|
|
474
547
|
commit(ops).then(function (ok) {
|
|
475
548
|
reportOutcome(ok);
|
|
549
|
+
setSnapshot(scope.getSnapshot());
|
|
550
|
+
if (ok) setDraft({});
|
|
476
551
|
}).catch(function (e) {
|
|
477
|
-
setBusy(false); setError(
|
|
552
|
+
setBusy(false); setError({ key: "error", detail: String(e && e.message || e) });
|
|
478
553
|
});
|
|
479
554
|
}
|
|
480
555
|
|
|
481
556
|
// One-click master switch: the same field the checkbox edits, written on its
|
|
482
557
|
// own so taking the plugin offline never depends on the rest of the form.
|
|
483
558
|
function onToggleEnabled() {
|
|
559
|
+
if (busy || !snapshot.writable) return;
|
|
484
560
|
setBusy(true); setNotice(null); setError(null);
|
|
485
561
|
var next = value.enabled === false;
|
|
486
562
|
commit([{ op: "set", path: ["enabled"], value: next }]).then(function (ok) {
|
|
487
|
-
reportOutcome(ok, next ?
|
|
563
|
+
reportOutcome(ok, next ? "enabledNotice" : "disabledNotice");
|
|
564
|
+
setSnapshot(scope.getSnapshot());
|
|
488
565
|
}).catch(function (e) {
|
|
489
|
-
setBusy(false); setError(
|
|
566
|
+
setBusy(false); setError({ key: "error", detail: String(e && e.message || e) });
|
|
490
567
|
});
|
|
491
568
|
}
|
|
492
569
|
|
|
493
|
-
function reseedDraft() {
|
|
494
|
-
// The mirror folds a write's answer in before the promise settles, so the
|
|
495
|
-
// snapshot is already current; the timeout only covers a host that
|
|
496
|
-
// answers late.
|
|
497
|
-
var fresh = scope.getSnapshot();
|
|
498
|
-
if (fresh.status === "ready" && fresh.value !== void 0) {
|
|
499
|
-
setDraft(Object.assign({}, valueToDraft(fresh.value)));
|
|
500
|
-
return;
|
|
501
|
-
}
|
|
502
|
-
setTimeout(function () {
|
|
503
|
-
var later = scope.getSnapshot();
|
|
504
|
-
if (later.status === "ready" && later.value !== void 0) setDraft(Object.assign({}, valueToDraft(later.value)));
|
|
505
|
-
}, 120);
|
|
506
|
-
}
|
|
507
|
-
|
|
508
570
|
function onReset() {
|
|
571
|
+
if (busy || !snapshot.writable) return;
|
|
509
572
|
setBusy(true); setNotice(null); setError(null);
|
|
510
573
|
var ops = FIELDS
|
|
511
574
|
.filter(function (f) { return f.key in user; })
|
|
512
575
|
.map(function (f) { return { op: "unset", path: [f.key] }; });
|
|
513
|
-
if (ops.length === 0) { setBusy(false); setNotice(
|
|
576
|
+
if (ops.length === 0) { setBusy(false); setDraft({}); setNotice({ key: "saved" }); return; }
|
|
514
577
|
commit(ops).then(function (ok) {
|
|
515
578
|
reportOutcome(ok);
|
|
579
|
+
setSnapshot(scope.getSnapshot());
|
|
580
|
+
if (ok) setDraft({});
|
|
516
581
|
}).catch(function (e) {
|
|
517
|
-
setBusy(false); setError(
|
|
582
|
+
setBusy(false); setError({ key: "error", detail: String(e && e.message || e) });
|
|
518
583
|
});
|
|
519
584
|
}
|
|
520
585
|
|
|
@@ -608,7 +673,7 @@ window.__ModuleLoader__.load({
|
|
|
608
673
|
className: "__tv_check",
|
|
609
674
|
type: "checkbox",
|
|
610
675
|
checked: Boolean(entry.listed),
|
|
611
|
-
disabled: !snapshot.writable,
|
|
676
|
+
disabled: busy || !snapshot.writable,
|
|
612
677
|
onChange: function () { toggleModel(entry); }
|
|
613
678
|
}),
|
|
614
679
|
h("span", { className: "__tv_itemId", title: entry.id }, entry.id),
|
|
@@ -631,29 +696,7 @@ window.__ModuleLoader__.load({
|
|
|
631
696
|
);
|
|
632
697
|
}
|
|
633
698
|
|
|
634
|
-
|
|
635
|
-
h("p", { className: "__tv_hint", style: { margin: "0 0 4px" } }, t("intro")),
|
|
636
|
-
h("div", { className: "__tv_master" },
|
|
637
|
-
h("span", { className: "__tv_masterLabel" },
|
|
638
|
-
t("masterSwitch") + (value.enabled === false ? ":" + t("disabledNotice") : "")),
|
|
639
|
-
h("button", {
|
|
640
|
-
type: "button",
|
|
641
|
-
className: "__tv_btn " + (value.enabled === false ? "__tv_btnPrimary" : "__tv_btnDanger"),
|
|
642
|
-
onClick: onToggleEnabled,
|
|
643
|
-
disabled: busy || !snapshot.writable
|
|
644
|
-
}, value.enabled === false ? t("turnOn") : t("turnOff"))),
|
|
645
|
-
h("div", { className: "__tv_catalog" },
|
|
646
|
-
h("span", { className: "__tv_label" }, t("catalogTitle")),
|
|
647
|
-
current.model
|
|
648
|
-
? h("span", null, (current.provider ? current.provider + " / " : "") + current.model)
|
|
649
|
-
: h("span", null, catalogBusy ? t("catalogLoading") : t("catalogNoRoute")),
|
|
650
|
-
current.model ? h("span", { className: "__tv_badge " + (current.direct ? "__tv_badgeOk" : "") },
|
|
651
|
-
current.direct ? t("catalogDirect") : t("catalogBridged")) : null,
|
|
652
|
-
current.model ? h("span", null, t("catalogSource") + ":" + sourceLabel(current.source, t)) : null,
|
|
653
|
-
h("button", { type: "button", className: "__tv_link", onClick: loadCatalog, disabled: catalogBusy },
|
|
654
|
-
catalogBusy ? t("catalogLoading") : t("catalogRefresh")),
|
|
655
|
-
catalogError ? h("span", { className: "__tv_error" }, t("catalogFailed") + ":" + catalogError) : null),
|
|
656
|
-
FIELDS.map(function (f) {
|
|
699
|
+
function renderField(f) {
|
|
657
700
|
var overridden = f.key in user;
|
|
658
701
|
var hint = f.key in ZH_HINTS ? h("span", { className: "__tv_hint" }, t(ZH_HINTS[f.key])) : null;
|
|
659
702
|
var label = h("span", { className: "__tv_label" },
|
|
@@ -662,7 +705,7 @@ window.__ModuleLoader__.load({
|
|
|
662
705
|
if (f.type === "checkbox") {
|
|
663
706
|
return h("label", { key: f.key, className: "__tv_field" },
|
|
664
707
|
h("span", { className: "__tv_row" },
|
|
665
|
-
h("input", { className: "__tv_check", type: "checkbox", checked: Boolean(fieldDraft(f)), onChange: function (e) { setField(f, e.target.checked); } }),
|
|
708
|
+
h("input", { className: "__tv_check", type: "checkbox", disabled: busy || !snapshot.writable, checked: Boolean(fieldDraft(f)), onChange: function (e) { setField(f, e.target.checked); } }),
|
|
666
709
|
h("span", { className: "__tv_label" }, labelOf(f, t)),
|
|
667
710
|
overridden ? h("span", { className: "__tv_override" }, t("overridden")) : null
|
|
668
711
|
),
|
|
@@ -680,7 +723,7 @@ window.__ModuleLoader__.load({
|
|
|
680
723
|
h("select", {
|
|
681
724
|
className: "__tv_input",
|
|
682
725
|
value: chosen,
|
|
683
|
-
disabled: !snapshot.writable,
|
|
726
|
+
disabled: busy || !snapshot.writable,
|
|
684
727
|
onChange: function (e) { setField(f, e.target.value); }
|
|
685
728
|
}, spec.map(function (o) { return h("option", { key: o.value, value: o.value }, t(o.label)); })),
|
|
686
729
|
hint
|
|
@@ -693,6 +736,7 @@ window.__ModuleLoader__.load({
|
|
|
693
736
|
className: "__tv_input",
|
|
694
737
|
type: "text",
|
|
695
738
|
list: MODEL_OPTIONS_ID,
|
|
739
|
+
disabled: busy || !snapshot.writable,
|
|
696
740
|
value: fieldDraft(f),
|
|
697
741
|
placeholder: t("catalogPickHint"),
|
|
698
742
|
onChange: function (e) { setField(f, e.target.value); }
|
|
@@ -707,31 +751,58 @@ window.__ModuleLoader__.load({
|
|
|
707
751
|
className: "__tv_input",
|
|
708
752
|
type: f.type === "password" ? "password" : f.type === "number" ? "number" : "text",
|
|
709
753
|
value: fieldDraft(f),
|
|
754
|
+
disabled: busy || !snapshot.writable,
|
|
710
755
|
placeholder: f.type === "password" ? (overridden ? "••••••••" : t("apiKeyHint")) : (f.placeholder || ""),
|
|
711
756
|
onChange: function (e) { setField(f, e.target.value); }
|
|
712
757
|
}),
|
|
713
758
|
hint
|
|
714
759
|
);
|
|
715
|
-
|
|
760
|
+
}
|
|
761
|
+
return h("div", { className: "__tv_root dsh-flat" },
|
|
762
|
+
h("p", { className: "__tv_hint", style: { margin: "0 0 4px" } }, t("intro")),
|
|
763
|
+
h("div", { className: "__tv_master" },
|
|
764
|
+
h("span", { className: "__tv_masterLabel" },
|
|
765
|
+
t("masterSwitch") + (value.enabled === false ? ":" + t("disabledNotice") : "")),
|
|
766
|
+
h("button", {
|
|
767
|
+
type: "button",
|
|
768
|
+
className: "__tv_btn " + (value.enabled === false ? "__tv_btnPrimary" : "__tv_btnDanger"),
|
|
769
|
+
onClick: onToggleEnabled,
|
|
770
|
+
disabled: busy || !snapshot.writable
|
|
771
|
+
}, value.enabled === false ? t("turnOn") : t("turnOff"))),
|
|
772
|
+
h("div", { className: "__tv_catalog" },
|
|
773
|
+
h("span", { className: "__tv_label" }, t("catalogTitle")),
|
|
774
|
+
current.model
|
|
775
|
+
? h("span", null, (current.provider ? current.provider + " / " : "") + current.model)
|
|
776
|
+
: h("span", null, catalogBusy ? t("catalogLoading") : t("catalogNoRoute")),
|
|
777
|
+
current.model ? h("span", { className: "__tv_badge " + (current.direct ? "__tv_badgeOk" : "") },
|
|
778
|
+
current.direct ? t("catalogDirect") : t("catalogBridged")) : null,
|
|
779
|
+
current.model ? h("span", null, t("catalogSource") + ":" + sourceLabel(current.source, t)) : null,
|
|
780
|
+
h("button", { type: "button", className: "__tv_link", onClick: loadCatalog, disabled: catalogBusy },
|
|
781
|
+
catalogBusy ? t("catalogLoading") : t("catalogRefresh")),
|
|
782
|
+
catalogError ? h("span", { className: "__tv_error" }, t("catalogFailed") + ":" + catalogError) : null),
|
|
783
|
+
h("label", { className: "__tv_field" },
|
|
784
|
+
h("span", { className: "__tv_label" }, t("imageMode")),
|
|
785
|
+
h("select", { className: "__tv_input", "aria-label": t("imageMode"), value: imageMode(), disabled: busy || !snapshot.writable,
|
|
786
|
+
onChange: function (e) { setImageMode(e.target.value); } },
|
|
787
|
+
h("option", { value: "auto" }, t("imageModeAuto")), h("option", { value: "tools" }, t("imageModeTools")),
|
|
788
|
+
h("option", { value: "custom", disabled: true }, t("imageModeCustom"))),
|
|
789
|
+
h("span", { className: "__tv_hint" }, t("imageModeHint"))),
|
|
790
|
+
h("h3", { className: "__tv_label" }, t("connectionTitle")),
|
|
791
|
+
FIELDS.filter(function (f) { return BASIC_KEYS.indexOf(f.key) !== -1; }).map(renderField),
|
|
792
|
+
h("details", null, h("summary", { style: { cursor: "pointer" } }, t("advanced")),
|
|
793
|
+
h("div", { style: { display: "flex", flexDirection: "column", gap: 10, paddingTop: 12 } },
|
|
794
|
+
FIELDS.filter(function (f) { return f.key !== "enabled" && BASIC_KEYS.indexOf(f.key) === -1; }).map(renderField))),
|
|
716
795
|
h("datalist", { id: MODEL_OPTIONS_ID }, catalogOptions),
|
|
717
796
|
h("div", { className: "__tv_actions" },
|
|
718
797
|
h("button", { type: "button", className: "__tv_btn __tv_btnPrimary", onClick: onSave, disabled: busy || !snapshot.writable }, t("save")),
|
|
719
798
|
h("button", { type: "button", className: "__tv_btn", onClick: onReset, disabled: busy || !snapshot.writable }, t("reset")),
|
|
720
|
-
notice ? h("span", { className: "__tv_status" }, notice) : null,
|
|
799
|
+
notice ? h("span", { className: "__tv_status" }, messageText(t, notice)) : null,
|
|
721
800
|
busy ? h("span", { className: "__tv_status" }, t("saving")) : null,
|
|
722
|
-
error ? h("span", { className: "__tv_error" }, error) : null
|
|
801
|
+
error ? h("span", { className: "__tv_error" }, messageText(t, error)) : null
|
|
723
802
|
)
|
|
724
803
|
);
|
|
725
804
|
}
|
|
726
805
|
|
|
727
|
-
function valueToDraft(value) {
|
|
728
|
-
var out = {};
|
|
729
|
-
for (var i = 0; i < FIELDS.length; i += 1) {
|
|
730
|
-
var f = FIELDS[i];
|
|
731
|
-
out[f.key] = f.type === "csv" || f.type === "modellist" ? draftToCsv(value[f.key]) : f.type === "checkbox" ? Boolean(value[f.key]) : String(value[f.key] ?? "");
|
|
732
|
-
}
|
|
733
|
-
return out;
|
|
734
|
-
}
|
|
735
806
|
function draftToCsv(arr) {
|
|
736
807
|
return Array.isArray(arr) ? arr.join(", ") : String(arr ?? "");
|
|
737
808
|
}
|
|
@@ -831,7 +902,8 @@ window.__ModuleLoader__.load({
|
|
|
831
902
|
overlay.style.cssText = "position:fixed;inset:0;background:rgba(0,0,0,0.82);display:flex;align-items:center;justify-content:center;z-index:2147483000;cursor:zoom-out;";
|
|
832
903
|
var big = document.createElement("img");
|
|
833
904
|
big.src = src;
|
|
834
|
-
big.alt = alt
|
|
905
|
+
big.alt = alt;
|
|
906
|
+
big.setAttribute(PREVIEW_ATTR, "1");
|
|
835
907
|
big.style.cssText = "max-width:92vw;max-height:92vh;object-fit:contain;border-radius:4px;box-shadow:0 8px 40px rgba(0,0,0,0.5);";
|
|
836
908
|
var close = function () {
|
|
837
909
|
overlay.remove();
|
|
@@ -849,6 +921,7 @@ window.__ModuleLoader__.load({
|
|
|
849
921
|
|
|
850
922
|
function attachBridgePreview(ctx, scope) {
|
|
851
923
|
var cfg = previewConfigOf(scope);
|
|
924
|
+
var t = ctx.locale.bind(NS);
|
|
852
925
|
var pendingTimer = null;
|
|
853
926
|
var intervalTimer = null;
|
|
854
927
|
var observer = null;
|
|
@@ -867,7 +940,7 @@ window.__ModuleLoader__.load({
|
|
|
867
940
|
var img = document.createElement("img");
|
|
868
941
|
img.setAttribute(PREVIEW_ATTR, "1");
|
|
869
942
|
img.src = PREVIEW_ROUTE + "?p=" + encodeURIComponent(path);
|
|
870
|
-
img.alt = "
|
|
943
|
+
img.alt = t("imagePreview");
|
|
871
944
|
img.style.cssText = "display:block;margin-left:auto;margin-right:0;max-width:min(360px,100%);max-height:420px;border-radius:8px;margin-top:4px;margin-bottom:6px;object-fit:contain;cursor:zoom-in;";
|
|
872
945
|
img.addEventListener("click", function () { openLightbox(img.src, img.alt); });
|
|
873
946
|
img.addEventListener("load", function () {
|
|
@@ -920,6 +993,9 @@ window.__ModuleLoader__.load({
|
|
|
920
993
|
break;
|
|
921
994
|
}
|
|
922
995
|
});
|
|
996
|
+
var offLocale = typeof ctx.locale.subscribe === "function" ? ctx.locale.subscribe(function () {
|
|
997
|
+
document.querySelectorAll("img[" + PREVIEW_ATTR + "]").forEach(function (img) { img.alt = t("imagePreview"); });
|
|
998
|
+
}) : null;
|
|
923
999
|
observer.observe(document.body, { childList: true, subtree: true, characterData: true });
|
|
924
1000
|
armInterval();
|
|
925
1001
|
var un = typeof scope.subscribe === "function" ? scope.subscribe(function () {
|
|
@@ -927,6 +1003,7 @@ window.__ModuleLoader__.load({
|
|
|
927
1003
|
armInterval();
|
|
928
1004
|
}) : null;
|
|
929
1005
|
return function () {
|
|
1006
|
+
if (offLocale) offLocale();
|
|
930
1007
|
if (observer) observer.disconnect();
|
|
931
1008
|
if (pendingTimer !== null) { clearTimeout(pendingTimer); pendingTimer = null; }
|
|
932
1009
|
if (intervalTimer !== null) { clearInterval(intervalTimer); intervalTimer = null; }
|
|
@@ -936,6 +1013,21 @@ window.__ModuleLoader__.load({
|
|
|
936
1013
|
}
|
|
937
1014
|
|
|
938
1015
|
// ── plugin ────────────────────────────────────────────────────────────
|
|
1016
|
+
|
|
1017
|
+
// Follow the host language without remounting the form or losing drafts.
|
|
1018
|
+
function useLocale(locale) {
|
|
1019
|
+
var refresh = react.useState(0)[1];
|
|
1020
|
+
react.useEffect(function () {
|
|
1021
|
+
if (!locale || typeof locale.subscribe !== "function") return;
|
|
1022
|
+
return locale.subscribe(function () { refresh(function (revision) { return revision + 1; }); });
|
|
1023
|
+
}, [locale]);
|
|
1024
|
+
}
|
|
1025
|
+
// Keep translation keys in state so feedback follows later language changes.
|
|
1026
|
+
function messageText(t, message) {
|
|
1027
|
+
if (!message) return "";
|
|
1028
|
+
return t(message.key) + (message.detailKey ? ": " + t(message.detailKey) : message.detail ? ": " + message.detail : "");
|
|
1029
|
+
}
|
|
1030
|
+
|
|
939
1031
|
function apply(ctx) {
|
|
940
1032
|
var t = ctx.locale.bind(NS);
|
|
941
1033
|
ctx.effect(function () { return ctx.locale.register(NS, { zh: zh, en: en }); }, "dsh-tool-vision: dictionaries");
|
|
@@ -947,7 +1039,7 @@ window.__ModuleLoader__.load({
|
|
|
947
1039
|
key: "dsh-tool-vision",
|
|
948
1040
|
locale: NS
|
|
949
1041
|
}, function (props) {
|
|
950
|
-
return h(VisionSection, Object.assign({}, props, { scope: scope }));
|
|
1042
|
+
return h(VisionSection, Object.assign({}, props, { scope: scope, t: t, locale: ctx.locale }));
|
|
951
1043
|
});
|
|
952
1044
|
});
|
|
953
1045
|
}
|
package/cordis.patch.yml
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
# dsh-tool-vision — bundle patch layer
|
|
2
|
-
#
|
|
3
|
-
# Declares the plugin as a Cordis loader entry, making the package a standard
|
|
4
|
-
# profile bundle: `dsh plugin --profile <p> add dsh-tool-vision` installs and
|
|
5
|
-
# mounts it without manual cordis.patch.yml edits.
|
|
6
|
-
- insert:
|
|
7
|
-
- id: tool-vision
|
|
8
|
-
name: 'dsh-tool-vision'
|
|
1
|
+
# dsh-tool-vision — bundle patch layer
|
|
2
|
+
#
|
|
3
|
+
# Declares the plugin as a Cordis loader entry, making the package a standard
|
|
4
|
+
# profile bundle: `dsh plugin --profile <p> add dsh-tool-vision` installs and
|
|
5
|
+
# mounts it without manual cordis.patch.yml edits.
|
|
6
|
+
- insert:
|
|
7
|
+
- id: tool-vision
|
|
8
|
+
name: 'dsh-tool-vision'
|
package/index.js
CHANGED
|
@@ -221,7 +221,7 @@ const Config = z.object({
|
|
|
221
221
|
/** Fixed session id override for callers without a dsh session context.
|
|
222
222
|
* Empty = auto (per-process stable id). */
|
|
223
223
|
sessionId: z.string().default(""),
|
|
224
|
-
}).volatile();
|
|
224
|
+
}).volatile();
|
|
225
225
|
|
|
226
226
|
const MIME_BY_EXT = {
|
|
227
227
|
".png": "image/png",
|
|
@@ -1165,7 +1165,7 @@ function apply(ctx, config) {
|
|
|
1165
1165
|
// would be undefined (apiKey included).
|
|
1166
1166
|
let current = config;
|
|
1167
1167
|
let sourceGetter = null;
|
|
1168
|
-
const getConfig = () => (sourceGetter ? sourceGetter() : typeof current.get === "function" ? current.get() : current);
|
|
1168
|
+
const getConfig = () => (sourceGetter ? sourceGetter() : typeof current.get === "function" ? current.get() : current);
|
|
1169
1169
|
// The registered settings scope, kept so tools can PERSIST what they measure.
|
|
1170
1170
|
// Null whenever no settings provider is mounted (composition-entry-only runs),
|
|
1171
1171
|
// in which case a probe still reports its verdict but cannot record it.
|
|
@@ -1435,12 +1435,12 @@ function apply(ctx, config) {
|
|
|
1435
1435
|
// `installSettingsSection` export (the provider now lives at ctx.settings).
|
|
1436
1436
|
// Inline the same logic via ctx.inject(["settings"]) — works on both
|
|
1437
1437
|
// 0.1.1 (module export wrapper) and 0.1.2 (ctx.settings) hosts.
|
|
1438
|
-
sourceGetter = () => typeof config.get === "function" ? config.get() : config;
|
|
1439
|
-
settingsScope = { update: (patch) => ctx.settings.update(NS, patch) };
|
|
1440
|
-
ctx.on("settings/document-updated", (id) => {
|
|
1441
|
-
if (id === NS) syncFeatures();
|
|
1442
|
-
});
|
|
1443
|
-
ctx.effect(() => () => uninstallFeatures());
|
|
1438
|
+
sourceGetter = () => typeof config.get === "function" ? config.get() : config;
|
|
1439
|
+
settingsScope = { update: (patch) => ctx.settings.update(NS, patch) };
|
|
1440
|
+
ctx.on("settings/document-updated", (id) => {
|
|
1441
|
+
if (id === NS) syncFeatures();
|
|
1442
|
+
});
|
|
1443
|
+
ctx.effect(() => () => uninstallFeatures());
|
|
1444
1444
|
|
|
1445
1445
|
// ── model catalog route: settings support, so it lives on THIS fiber ───────
|
|
1446
1446
|
// The master switch disposes the feature fiber on purpose (tools, bridge,
|
package/lib/model-probe.js
CHANGED
|
@@ -134,12 +134,34 @@ function readAnswer(body) {
|
|
|
134
134
|
return reasoning;
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
/**
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
137
|
+
/** Read real response streams with a byte limit before buffering the body. */
|
|
138
|
+
async function readProbeBody(response) {
|
|
139
|
+
const reader = response.body?.getReader?.();
|
|
140
|
+
if (!reader) {
|
|
141
|
+
const text = await response.text();
|
|
142
|
+
if (Buffer.byteLength(text, "utf8") > PROBE_MAX_BODY_BYTES) throw new Error("probe response body too large");
|
|
143
|
+
return text;
|
|
144
|
+
}
|
|
145
|
+
const chunks = [];
|
|
146
|
+
let size = 0;
|
|
147
|
+
try {
|
|
148
|
+
while (true) {
|
|
149
|
+
const { done, value } = await reader.read();
|
|
150
|
+
if (done) break;
|
|
151
|
+
size += value.byteLength;
|
|
152
|
+
if (size > PROBE_MAX_BODY_BYTES) throw new Error("probe response body too large");
|
|
153
|
+
chunks.push(Buffer.from(value));
|
|
154
|
+
}
|
|
155
|
+
return Buffer.concat(chunks, size).toString("utf8");
|
|
156
|
+
} catch (error) {
|
|
157
|
+
await reader.cancel().catch(() => {});
|
|
158
|
+
throw error;
|
|
159
|
+
} finally {
|
|
160
|
+
reader.releaseLock();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** One chat/completions request; transport failures use status 0. */
|
|
143
165
|
async function probeRequest(target, content, fetchImpl, signal) {
|
|
144
166
|
const base = target.baseURL.endsWith("/") ? target.baseURL : `${target.baseURL}/`;
|
|
145
167
|
const endpoint = new URL("chat/completions", base);
|
|
@@ -151,6 +173,7 @@ async function probeRequest(target, content, fetchImpl, signal) {
|
|
|
151
173
|
else signal.addEventListener("abort", onAbort, { once: true });
|
|
152
174
|
}
|
|
153
175
|
try {
|
|
176
|
+
controller.signal.throwIfAborted();
|
|
154
177
|
const response = await fetchImpl(endpoint, {
|
|
155
178
|
method: "POST",
|
|
156
179
|
headers: {
|
|
@@ -165,10 +188,7 @@ async function probeRequest(target, content, fetchImpl, signal) {
|
|
|
165
188
|
}),
|
|
166
189
|
signal: controller.signal,
|
|
167
190
|
});
|
|
168
|
-
const text = await response
|
|
169
|
-
if (text.length > PROBE_MAX_BODY_BYTES) {
|
|
170
|
-
return { ok: false, status: response.status, message: "probe response body too large" };
|
|
171
|
-
}
|
|
191
|
+
const text = await readProbeBody(response);
|
|
172
192
|
let body = null;
|
|
173
193
|
try {
|
|
174
194
|
body = JSON.parse(text);
|
|
@@ -203,6 +223,11 @@ async function probeRequest(target, content, fetchImpl, signal) {
|
|
|
203
223
|
* @returns {Promise<{support: 'yes'|'no'|'unknown', detail: string, evidence: object}>}
|
|
204
224
|
*/
|
|
205
225
|
export async function probeModelCapability(target, signal) {
|
|
226
|
+
target = {
|
|
227
|
+
...target,
|
|
228
|
+
timeoutMs: Number.isFinite(target.timeoutMs) && target.timeoutMs > 0 ? target.timeoutMs : PROBE_TIMEOUT_MS,
|
|
229
|
+
maxTokens: Number.isFinite(target.maxTokens) && target.maxTokens > 0 ? target.maxTokens : PROBE_MAX_TOKENS,
|
|
230
|
+
};
|
|
206
231
|
const fetchImpl = target.fetchImpl ?? fetch;
|
|
207
232
|
const api = target.api;
|
|
208
233
|
if (api !== undefined && api !== null && api !== "openai-completions") {
|
|
@@ -217,6 +242,12 @@ export async function probeModelCapability(target, signal) {
|
|
|
217
242
|
if (typeof target.baseURL !== "string" || target.baseURL.length === 0) {
|
|
218
243
|
return { support: "unknown", detail: "the route has no baseURL", evidence: {} };
|
|
219
244
|
}
|
|
245
|
+
try {
|
|
246
|
+
const url = new URL(target.baseURL);
|
|
247
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error("unsupported protocol");
|
|
248
|
+
} catch {
|
|
249
|
+
return { support: "unknown", detail: "the route has an invalid HTTP(S) baseURL", evidence: {} };
|
|
250
|
+
}
|
|
220
251
|
|
|
221
252
|
const control = await probeRequest(target, PROBE_CONTROL_QUESTION, fetchImpl, signal);
|
|
222
253
|
const controlAnswer = control.ok ? control.answer : "";
|
|
@@ -240,7 +271,7 @@ export async function probeModelCapability(target, signal) {
|
|
|
240
271
|
{ type: "image_url", image_url: { url: solidColorDataUrl(spec.rgb) } },
|
|
241
272
|
], fetchImpl, signal);
|
|
242
273
|
if (!result.ok) {
|
|
243
|
-
if (UNSUPPORTED_IMAGE_RE.test(result.message)) {
|
|
274
|
+
if ([400, 415, 422].includes(result.status) && UNSUPPORTED_IMAGE_RE.test(result.message)) {
|
|
244
275
|
return {
|
|
245
276
|
support: "no",
|
|
246
277
|
detail: `the endpoint rejected the image part (${result.status}): ${result.message}`,
|
package/locale/en.json
ADDED