dsh-tool-vision 0.8.1 → 0.9.0
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 +166 -9
- package/README.zh.md +88 -7
- package/client.js +345 -15
- package/index.js +607 -19
- package/lib/model-probe.js +274 -0
- package/package.json +10 -4
package/client.js
CHANGED
|
@@ -46,6 +46,20 @@ window.__ModuleLoader__.load({
|
|
|
46
46
|
".__tv_masterLabel{font-size:13px;color:var(--dsw-alias-label-primary)}" +
|
|
47
47
|
".__tv_status{font-size:12px;color:var(--dsw-alias-label-tertiary)}" +
|
|
48
48
|
".__tv_error{font-size:12px;color:var(--dsw-alias-state-error-primary)}" +
|
|
49
|
+
".__tv_catalog{display:flex;flex-wrap:wrap;align-items:center;gap:6px;font-size:12px;color:var(--dsw-alias-label-tertiary);padding:6px 10px;border:1px dashed var(--dsw-alias-border-l2);border-radius:8px}" +
|
|
50
|
+
".__tv_badge{font-size:10px;border:1px solid var(--dsw-alias-border-l2);border-radius:4px;padding:0 4px;color:var(--dsw-alias-label-tertiary)}" +
|
|
51
|
+
".__tv_badgeOk{color:var(--dsw-alias-state-business-primary);border-color:var(--dsw-alias-state-business-primary)}" +
|
|
52
|
+
".__tv_badgeBad{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary)}" +
|
|
53
|
+
".__tv_link{background:none;border:none;font:inherit;font-size:11px;padding:0;cursor:pointer;color:var(--dsw-alias-state-business-primary)}" +
|
|
54
|
+
".__tv_link:disabled{opacity:.5;cursor:default}" +
|
|
55
|
+
".__tv_picker{display:flex;flex-direction:column;gap:4px}" +
|
|
56
|
+
".__tv_pickerHead{display:flex;align-items:center;justify-content:space-between;gap:8px;font-size:11px;color:var(--dsw-alias-label-tertiary)}" +
|
|
57
|
+
".__tv_pickerList{max-height:230px;overflow-y:auto;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-3);padding:4px}" +
|
|
58
|
+
".__tv_group{font-size:10px;font-weight:600;color:var(--dsw-alias-label-tertiary);padding:6px 6px 2px;text-transform:none}" +
|
|
59
|
+
".__tv_item{display:flex;align-items:center;gap:8px;padding:4px 6px;border-radius:6px;cursor:pointer;font-size:12px;color:var(--dsw-alias-label-primary)}" +
|
|
60
|
+
".__tv_item:hover{background:var(--dsw-alias-bg-layer-2)}" +
|
|
61
|
+
".__tv_itemId{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}" +
|
|
62
|
+
".__tv_itemName{color:var(--dsw-alias-label-tertiary);font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:38%}" +
|
|
49
63
|
".__tv_unavailable{font-size:13px;color:var(--dsw-alias-label-tertiary)}";
|
|
50
64
|
var tagId = "dsh-tool-vision/main.css";
|
|
51
65
|
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
@@ -68,7 +82,45 @@ window.__ModuleLoader__.load({
|
|
|
68
82
|
maxImageBytes: "本地图片大小上限(字节)",
|
|
69
83
|
bridgeTextOnly: "图片桥接(文本模型贴图自动转 inspect_image 指引)",
|
|
70
84
|
bridgeExportDir: "桥接图片导出目录(空 = 系统临时目录)",
|
|
71
|
-
multimodalModels: "
|
|
85
|
+
multimodalModels: "多模态名单(逗号分隔;支持 * 与 ? 通配,如 *vl*、deepseek/*)",
|
|
86
|
+
fieldMultimodalListMode: "名单模式",
|
|
87
|
+
modeWhitelist: "白名单(名单内模型直收图片块)",
|
|
88
|
+
modeBlacklist: "黑名单(名单内模型强制走桥接)",
|
|
89
|
+
modeOff: "关闭(忽略名单)",
|
|
90
|
+
hintMultimodalListMode: "白名单=名单内直发图片;黑名单=名单内强制桥接(用来纠正“声明支持图片但实际不支持”的模型);关闭=名单不生效。黑名单的基准是自动识别集,绝不会变成“未列出的一律直发”。",
|
|
91
|
+
hintMultimodalModels: "每项按“完整 id / 末段裸 id / provider/id”三种写法匹配,大小写不敏感;* 与 ? 为通配符(如 *vl*)。可从候选中选择。",
|
|
92
|
+
fieldAutoDetectMultimodal: "自动识别多模态模型(按路由声明判定)",
|
|
93
|
+
hintAutoDetectMultimodal: "默认开启:按路由自己声明的 inputModalities 判定,识别到纯文本就交给桥接、识别到多模态就等同白名单成员直发图片;声明永远读“包装前”的真值,不会被插件的自动声明污染。声明可能不实——命中后若上游拒收图片,把该模型写进下面的名单并切到黑名单模式即可强制桥接(首次放行时日志里也会提示)。",
|
|
94
|
+
catalogTitle: "当前路由判定",
|
|
95
|
+
catalogNoRoute: "尚未判定(发一条消息后刷新)",
|
|
96
|
+
catalogDirect: "直发图片",
|
|
97
|
+
catalogBridged: "走桥接",
|
|
98
|
+
catalogSource: "依据",
|
|
99
|
+
catalogSourceWhitelist: "名单(白名单命中)",
|
|
100
|
+
catalogSourceBlacklist: "名单(黑名单命中)",
|
|
101
|
+
catalogSourceAuto: "自动识别(路由声明支持图片)",
|
|
102
|
+
catalogSourceProbeYes: "实测(真实发图验证通过)",
|
|
103
|
+
catalogSourceProbeNo: "实测(真实发图验证未通过)",
|
|
104
|
+
catalogSourceDefault: "默认(未识别 → 桥接)",
|
|
105
|
+
catalogSourceBridgeOff: "桥接已关闭",
|
|
106
|
+
catalogSourceNoRoute: "无路由信息",
|
|
107
|
+
catalogLoading: "正在读取已配置模型…",
|
|
108
|
+
catalogRefresh: "刷新模型列表",
|
|
109
|
+
catalogFailed: "模型列表读取失败",
|
|
110
|
+
catalogEmpty: "dsh 尚未配置任何模型",
|
|
111
|
+
catalogImage: "声明支持图片",
|
|
112
|
+
catalogProbeYes: "已实测可看图",
|
|
113
|
+
catalogProbeNo: "已实测不可看图",
|
|
114
|
+
catalogProbeHint: "「实测」来自 vision_probe_model:向该路由真实发图验证过,比模型自己的声明更可靠。",
|
|
115
|
+
catalogPickHint: "也可手填通配符,如 *vl*",
|
|
116
|
+
catalogProviderError: "该 provider 的模型列表不可用",
|
|
117
|
+
catalogCount: "个模型",
|
|
118
|
+
catalogPick: "点击勾选加入名单",
|
|
119
|
+
catalogCurrentList: "当前名单",
|
|
120
|
+
catalogListEmpty: "(空)",
|
|
121
|
+
catalogUnsaved: "未保存",
|
|
122
|
+
catalogModeOff: "名单模式为“关闭”,勾选不会影响判定",
|
|
123
|
+
catalogNoMatch: "没有匹配的模型",
|
|
72
124
|
fieldBaseUrl: "API Base URL",
|
|
73
125
|
fieldApiKey: "API Key",
|
|
74
126
|
fieldApiKeyEnv: "API Key 环境变量(apiKey 为空时读取)",
|
|
@@ -78,7 +130,7 @@ window.__ModuleLoader__.load({
|
|
|
78
130
|
fieldMaxImageBytes: "图片大小上限(字节)",
|
|
79
131
|
fieldBridgeTextOnly: "图片桥接开关",
|
|
80
132
|
fieldBridgeExportDir: "桥接导出目录",
|
|
81
|
-
fieldMultimodalModels: "
|
|
133
|
+
fieldMultimodalModels: "模型名单(逗号分隔,支持通配)",
|
|
82
134
|
fieldBridgePreview: "桥接图片内联预览(气泡内缩略图,点击放大)",
|
|
83
135
|
fieldBridgePreviewScanIntervalMs: "预览兜底扫描间隔(毫秒,0 = 关闭兜底)",
|
|
84
136
|
fieldBridgePreviewHideHint: "图片加载成功后隐藏桥接提示文本",
|
|
@@ -119,7 +171,45 @@ window.__ModuleLoader__.load({
|
|
|
119
171
|
maxImageBytes: "Max local image size (bytes)",
|
|
120
172
|
bridgeTextOnly: "Image bridge (pasted images on text-only models become inspect_image hints)",
|
|
121
173
|
bridgeExportDir: "Bridge export dir (empty = system temp)",
|
|
122
|
-
multimodalModels: "
|
|
174
|
+
multimodalModels: "Model list (comma-separated; * and ? globs allowed, e.g. *vl*, deepseek/*)",
|
|
175
|
+
fieldMultimodalListMode: "List mode",
|
|
176
|
+
modeWhitelist: "Whitelist (listed models receive image blocks directly)",
|
|
177
|
+
modeBlacklist: "Blacklist (listed models are forced through the bridge)",
|
|
178
|
+
modeOff: "Off (ignore the list)",
|
|
179
|
+
hintMultimodalListMode: "Whitelist: listed models get images directly. Blacklist: listed models are always bridged — the correction layer for a model that claims image support it does not have. Off: the list does nothing. A blacklist never degrades into \"everything unlisted is direct\".",
|
|
180
|
+
hintMultimodalModels: "Each entry is matched case-insensitively against the full id, its bare id after the last /, and provider/id; * and ? are globs (*vl*). Pick from the candidates below or type freely.",
|
|
181
|
+
fieldAutoDetectMultimodal: "Auto-detect multimodal models (trust the route's declaration)",
|
|
182
|
+
hintAutoDetectMultimodal: "On by default: the route's own declared inputModalities decides — a text-only route is bridged, a multimodal one is treated like a whitelist member and gets images directly. The declaration is read BEFORE the plugin's admission wrap, so its own claim can never become the evidence. A declaration can lie: if the endpoint rejects images, list that model below and switch to blacklist mode to force the bridge (the first promotion also logs a notice).",
|
|
183
|
+
catalogTitle: "Current route",
|
|
184
|
+
catalogNoRoute: "Not decided yet (send a message, then refresh)",
|
|
185
|
+
catalogDirect: "Images direct",
|
|
186
|
+
catalogBridged: "Bridged",
|
|
187
|
+
catalogSource: "Because",
|
|
188
|
+
catalogSourceWhitelist: "list (whitelist hit)",
|
|
189
|
+
catalogSourceBlacklist: "list (blacklist hit)",
|
|
190
|
+
catalogSourceAuto: "auto-detect (route declares image)",
|
|
191
|
+
catalogSourceProbeYes: "measured (a real image was read)",
|
|
192
|
+
catalogSourceProbeNo: "measured (a real image was NOT read)",
|
|
193
|
+
catalogSourceDefault: "default (unrecognised → bridged)",
|
|
194
|
+
catalogSourceBridgeOff: "bridging is off",
|
|
195
|
+
catalogSourceNoRoute: "no route information",
|
|
196
|
+
catalogLoading: "Reading configured models…",
|
|
197
|
+
catalogRefresh: "Refresh model list",
|
|
198
|
+
catalogFailed: "Could not read the model list",
|
|
199
|
+
catalogEmpty: "dsh has no configured models",
|
|
200
|
+
catalogImage: "declares image",
|
|
201
|
+
catalogProbeYes: "measured: reads images",
|
|
202
|
+
catalogProbeNo: "measured: no image reading",
|
|
203
|
+
catalogProbeHint: "\"Measured\" comes from vision_probe_model: a real image was sent to that route, which beats the model's own declaration.",
|
|
204
|
+
catalogPickHint: "Or type a glob, e.g. *vl*",
|
|
205
|
+
catalogProviderError: "this provider's model list is unavailable",
|
|
206
|
+
catalogCount: "models",
|
|
207
|
+
catalogPick: "Click to add to the list",
|
|
208
|
+
catalogCurrentList: "Current list",
|
|
209
|
+
catalogListEmpty: "(empty)",
|
|
210
|
+
catalogUnsaved: "unsaved",
|
|
211
|
+
catalogModeOff: "List mode is Off — ticking has no effect on the decision",
|
|
212
|
+
catalogNoMatch: "no matching models",
|
|
123
213
|
fieldBaseUrl: "API Base URL",
|
|
124
214
|
fieldApiKey: "API Key",
|
|
125
215
|
fieldApiKeyEnv: "API key env var (read when apiKey is empty)",
|
|
@@ -129,7 +219,7 @@ window.__ModuleLoader__.load({
|
|
|
129
219
|
fieldMaxImageBytes: "Max image size (bytes)",
|
|
130
220
|
fieldBridgeTextOnly: "Image bridge",
|
|
131
221
|
fieldBridgeExportDir: "Bridge export dir",
|
|
132
|
-
fieldMultimodalModels: "
|
|
222
|
+
fieldMultimodalModels: "Model list (comma-separated)",
|
|
133
223
|
fieldBridgePreview: "Bridge image preview (inline thumbnail in the bubble, click to zoom)",
|
|
134
224
|
fieldBridgePreviewScanIntervalMs: "Preview fallback scan interval (ms, 0 = disable)",
|
|
135
225
|
fieldBridgePreviewHideHint: "Hide the bridged hint text once the image has loaded",
|
|
@@ -174,7 +264,13 @@ window.__ModuleLoader__.load({
|
|
|
174
264
|
{ key: "maxImageBytes", label: "fieldMaxImageBytes", type: "number" },
|
|
175
265
|
{ key: "bridgeTextOnly", label: "fieldBridgeTextOnly", type: "checkbox" },
|
|
176
266
|
{ key: "bridgeExportDir", label: "fieldBridgeExportDir", type: "text" },
|
|
177
|
-
{ key: "
|
|
267
|
+
{ key: "multimodalListMode", label: "fieldMultimodalListMode", type: "select", options: [
|
|
268
|
+
{ value: "whitelist", label: "modeWhitelist" },
|
|
269
|
+
{ value: "blacklist", label: "modeBlacklist" },
|
|
270
|
+
{ value: "off", label: "modeOff" }
|
|
271
|
+
] },
|
|
272
|
+
{ key: "multimodalModels", label: "fieldMultimodalModels", type: "modellist" },
|
|
273
|
+
{ key: "autoDetectMultimodal", label: "fieldAutoDetectMultimodal", type: "checkbox" },
|
|
178
274
|
{ key: "bridgePreview", label: "fieldBridgePreview", type: "checkbox" },
|
|
179
275
|
{ key: "bridgePreviewScanIntervalMs", label: "fieldBridgePreviewScanIntervalMs", type: "number" },
|
|
180
276
|
{ key: "bridgePreviewHideHint", label: "fieldBridgePreviewHideHint", type: "checkbox" },
|
|
@@ -191,7 +287,9 @@ window.__ModuleLoader__.load({
|
|
|
191
287
|
maxImageBytes: "maxImageBytes",
|
|
192
288
|
bridgeTextOnly: "bridgeTextOnly",
|
|
193
289
|
bridgeExportDir: "bridgeExportDir",
|
|
194
|
-
multimodalModels: "
|
|
290
|
+
multimodalModels: "hintMultimodalModels",
|
|
291
|
+
multimodalListMode: "hintMultimodalListMode",
|
|
292
|
+
autoDetectMultimodal: "hintAutoDetectMultimodal",
|
|
195
293
|
bridgePreview: "hintBridgePreview",
|
|
196
294
|
bridgePreviewScanIntervalMs: "hintBridgePreviewScanIntervalMs",
|
|
197
295
|
bridgePreviewHideHint: "hintBridgePreviewHideHint",
|
|
@@ -238,6 +336,28 @@ window.__ModuleLoader__.load({
|
|
|
238
336
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
239
337
|
}, [ready]);
|
|
240
338
|
|
|
339
|
+
// ── configured-model catalog (v0.9.0) ─────────────────────────────────
|
|
340
|
+
// The list field autocompletes from the routes the host actually has,
|
|
341
|
+
// read over the plugin's own loopback route. Loaded once per mount and
|
|
342
|
+
// refreshed on demand; a failure only costs the autocomplete, never the
|
|
343
|
+
// ability to type an entry by hand.
|
|
344
|
+
var [catalog, setCatalog] = react.useState(null);
|
|
345
|
+
var [catalogBusy, setCatalogBusy] = react.useState(false);
|
|
346
|
+
var [catalogError, setCatalogError] = react.useState(null);
|
|
347
|
+
var loadCatalog = react.useCallback(function () {
|
|
348
|
+
setCatalogBusy(true);
|
|
349
|
+
setCatalogError(null);
|
|
350
|
+
fetch(CATALOG_URL, { headers: { Accept: "application/json" } })
|
|
351
|
+
.then(function (response) {
|
|
352
|
+
if (!response.ok) throw new Error("HTTP " + response.status);
|
|
353
|
+
return response.json();
|
|
354
|
+
})
|
|
355
|
+
.then(function (data) { setCatalog(data); })
|
|
356
|
+
.catch(function (error) { setCatalogError(String(error && error.message ? error.message : error)); })
|
|
357
|
+
.then(function () { setCatalogBusy(false); });
|
|
358
|
+
}, []);
|
|
359
|
+
react.useEffect(function () { loadCatalog(); }, [loadCatalog]);
|
|
360
|
+
|
|
241
361
|
if (snapshot.status === "unavailable") {
|
|
242
362
|
return h("p", { className: "__tv_unavailable" }, t("unavailable"));
|
|
243
363
|
}
|
|
@@ -247,7 +367,7 @@ window.__ModuleLoader__.load({
|
|
|
247
367
|
var user = snapshot.user || {};
|
|
248
368
|
|
|
249
369
|
function fieldDraft(f) {
|
|
250
|
-
if (f.type === "csv") return draft[f.key] !== void 0 ? draft[f.key] : draftToCsv(value[f.key]);
|
|
370
|
+
if (f.type === "csv" || f.type === "modellist") return draft[f.key] !== void 0 ? draft[f.key] : draftToCsv(value[f.key]);
|
|
251
371
|
if (f.type === "checkbox") return draft[f.key] !== void 0 ? draft[f.key] : Boolean(value[f.key]);
|
|
252
372
|
return draft[f.key] !== void 0 ? draft[f.key] : String(value[f.key] ?? "");
|
|
253
373
|
}
|
|
@@ -274,7 +394,7 @@ window.__ModuleLoader__.load({
|
|
|
274
394
|
var ops = [];
|
|
275
395
|
FIELDS.forEach(function (f) {
|
|
276
396
|
var d = fieldDraft(f);
|
|
277
|
-
if (f.type === "csv") {
|
|
397
|
+
if (f.type === "csv" || f.type === "modellist") {
|
|
278
398
|
var arr = String(d).split(",").map(function (s) { return s.trim(); }).filter(Boolean);
|
|
279
399
|
var cur = value[f.key] || [];
|
|
280
400
|
if (arr.length === cur.length && arr.every(function (x, i) { return x === cur[i]; })) return;
|
|
@@ -398,6 +518,119 @@ window.__ModuleLoader__.load({
|
|
|
398
518
|
});
|
|
399
519
|
}
|
|
400
520
|
|
|
521
|
+
// The two field specs the picker binds to. Looked up rather than
|
|
522
|
+
// hard-coded keys, so renaming a field in FIELDS cannot silently detach
|
|
523
|
+
// the picker from the setting it edits.
|
|
524
|
+
var modelListField = null;
|
|
525
|
+
var modeField = null;
|
|
526
|
+
FIELDS.forEach(function (f) {
|
|
527
|
+
if (f.type === "modellist") modelListField = f;
|
|
528
|
+
if (f.key === "multimodalListMode") modeField = f;
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
var current = (catalog && catalog.current) || {};
|
|
532
|
+
var catalogOptions = buildCatalogOptions(catalog, t);
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* The list as the draft sees it, split into entries. The picker edits
|
|
536
|
+
* this array, not the raw text, so a model id that happens to contain a
|
|
537
|
+
* comma can never be mangled by the text field's separator.
|
|
538
|
+
*/
|
|
539
|
+
function listEntries() {
|
|
540
|
+
return String(fieldDraft(modelListField)).split(",")
|
|
541
|
+
.map(function (s) { return s.trim(); })
|
|
542
|
+
.filter(Boolean);
|
|
543
|
+
}
|
|
544
|
+
function writeList(entries) {
|
|
545
|
+
setDraft(function (prev) {
|
|
546
|
+
var next = Object.assign({}, prev);
|
|
547
|
+
next[modelListField.key] = draftToCsv(entries);
|
|
548
|
+
return next;
|
|
549
|
+
});
|
|
550
|
+
setNotice(null);
|
|
551
|
+
setError(null);
|
|
552
|
+
}
|
|
553
|
+
/** Toggle one model: add the FULL id, or remove exactly the entries that hit. */
|
|
554
|
+
function toggleModel(entry) {
|
|
555
|
+
var entries = listEntries();
|
|
556
|
+
if (entry.listed) {
|
|
557
|
+
var drop = entry.matchedEntries || [];
|
|
558
|
+
writeList(entries.filter(function (e) { return drop.indexOf(e) === -1; }));
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
if (entries.indexOf(entry.id) === -1) entries.push(entry.id);
|
|
562
|
+
writeList(entries);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function renderPicker() {
|
|
566
|
+
if (!modelListField) return null;
|
|
567
|
+
var providers = (catalog && Array.isArray(catalog.providers) ? catalog.providers : [])
|
|
568
|
+
.filter(function (p) { return Array.isArray(p.models) && p.models.length > 0; });
|
|
569
|
+
var broken = (catalog && Array.isArray(catalog.providers) ? catalog.providers : [])
|
|
570
|
+
.filter(function (p) { return p.error; });
|
|
571
|
+
var count = providers.reduce(function (total, p) { return total + p.models.length; }, 0);
|
|
572
|
+
var mode = String((modeField && fieldDraft(modeField)) || value.multimodalListMode || "whitelist");
|
|
573
|
+
// The picker and the text field edit ONE draft; this is the only signal
|
|
574
|
+
// that a tick is not persisted yet, so it must be visible.
|
|
575
|
+
var dirty = JSON.stringify(listEntries()) !== JSON.stringify(Array.isArray(value[modelListField.key]) ? value[modelListField.key] : []);
|
|
576
|
+
var head = h("div", { className: "__tv_pickerHead" },
|
|
577
|
+
h("span", null, catalogBusy
|
|
578
|
+
? t("catalogLoading")
|
|
579
|
+
: catalog
|
|
580
|
+
? count + " " + t("catalogCount") + " · " + t("catalogPick")
|
|
581
|
+
: t("catalogNoRoute")),
|
|
582
|
+
catalogError
|
|
583
|
+
? h("span", { className: "__tv_error" }, t("catalogFailed") + ":" + catalogError)
|
|
584
|
+
: h("span", null, t("catalogCurrentList") + ":" + (listEntries().join(", ") || t("catalogListEmpty"))
|
|
585
|
+
+ (dirty ? "(" + t("catalogUnsaved") + ")" : "")));
|
|
586
|
+
var notes = broken.map(function (p) {
|
|
587
|
+
return h("div", { key: "e:" + p.id, className: "__tv_hint" }, p.id + ":" + t("catalogProviderError") + "(" + p.error + ")");
|
|
588
|
+
});
|
|
589
|
+
if (!catalog) return h("div", { className: "__tv_picker" }, head);
|
|
590
|
+
if (count === 0) {
|
|
591
|
+
return h("div", { className: "__tv_picker" },
|
|
592
|
+
head,
|
|
593
|
+
h("div", { className: "__tv_hint" },
|
|
594
|
+
providers.length === 0 ? t("catalogEmpty") : t("catalogNoMatch")),
|
|
595
|
+
notes
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
return h("div", { className: "__tv_picker" },
|
|
599
|
+
head,
|
|
600
|
+
mode === "off" ? h("div", { className: "__tv_hint" }, t("catalogModeOff")) : null,
|
|
601
|
+
h("div", { className: "__tv_pickerList" },
|
|
602
|
+
providers.map(function (provider) {
|
|
603
|
+
return [
|
|
604
|
+
h("div", { key: "h:" + provider.id, className: "__tv_group" }, provider.id + " · " + provider.name),
|
|
605
|
+
provider.models.map(function (entry) {
|
|
606
|
+
return h("label", { key: "m:" + provider.id + ":" + entry.id, className: "__tv_item" },
|
|
607
|
+
h("input", {
|
|
608
|
+
className: "__tv_check",
|
|
609
|
+
type: "checkbox",
|
|
610
|
+
checked: Boolean(entry.listed),
|
|
611
|
+
disabled: !snapshot.writable,
|
|
612
|
+
onChange: function () { toggleModel(entry); }
|
|
613
|
+
}),
|
|
614
|
+
h("span", { className: "__tv_itemId", title: entry.id }, entry.id),
|
|
615
|
+
entry.probe === "yes"
|
|
616
|
+
? h("span", { className: "__tv_badge __tv_badgeOk", title: t("catalogProbeHint") }, t("catalogProbeYes"))
|
|
617
|
+
: entry.probe === "no"
|
|
618
|
+
? h("span", { className: "__tv_badge __tv_badgeBad", title: t("catalogProbeHint") }, t("catalogProbeNo"))
|
|
619
|
+
: entry.image
|
|
620
|
+
? h("span", { className: "__tv_badge __tv_badgeOk" }, t("catalogImage"))
|
|
621
|
+
: null,
|
|
622
|
+
entry.name && entry.name !== entry.id
|
|
623
|
+
? h("span", { className: "__tv_itemName" }, entry.name)
|
|
624
|
+
: null
|
|
625
|
+
);
|
|
626
|
+
})
|
|
627
|
+
];
|
|
628
|
+
})
|
|
629
|
+
),
|
|
630
|
+
notes
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
|
|
401
634
|
return h("div", { className: "__tv_root" },
|
|
402
635
|
h("p", { className: "__tv_hint", style: { margin: "0 0 4px" } }, t("intro")),
|
|
403
636
|
h("div", { className: "__tv_master" },
|
|
@@ -409,8 +642,23 @@ window.__ModuleLoader__.load({
|
|
|
409
642
|
onClick: onToggleEnabled,
|
|
410
643
|
disabled: busy || !snapshot.writable
|
|
411
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),
|
|
412
656
|
FIELDS.map(function (f) {
|
|
413
657
|
var overridden = f.key in user;
|
|
658
|
+
var hint = f.key in ZH_HINTS ? h("span", { className: "__tv_hint" }, t(ZH_HINTS[f.key])) : null;
|
|
659
|
+
var label = h("span", { className: "__tv_label" },
|
|
660
|
+
labelOf(f, t),
|
|
661
|
+
overridden ? h("span", { className: "__tv_override" }, t("overridden")) : null);
|
|
414
662
|
if (f.type === "checkbox") {
|
|
415
663
|
return h("label", { key: f.key, className: "__tv_field" },
|
|
416
664
|
h("span", { className: "__tv_row" },
|
|
@@ -418,14 +666,43 @@ window.__ModuleLoader__.load({
|
|
|
418
666
|
h("span", { className: "__tv_label" }, labelOf(f, t)),
|
|
419
667
|
overridden ? h("span", { className: "__tv_override" }, t("overridden")) : null
|
|
420
668
|
),
|
|
421
|
-
|
|
669
|
+
hint
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
if (f.type === "select") {
|
|
673
|
+
var spec = f.options || [];
|
|
674
|
+
var chosen = fieldDraft(f);
|
|
675
|
+
// An unset namespace must show the default the server would apply,
|
|
676
|
+
// not an empty box that reads as "no mode".
|
|
677
|
+
if (!chosen && spec.length > 0) chosen = spec[0].value;
|
|
678
|
+
return h("label", { key: f.key, className: "__tv_field" },
|
|
679
|
+
label,
|
|
680
|
+
h("select", {
|
|
681
|
+
className: "__tv_input",
|
|
682
|
+
value: chosen,
|
|
683
|
+
disabled: !snapshot.writable,
|
|
684
|
+
onChange: function (e) { setField(f, e.target.value); }
|
|
685
|
+
}, spec.map(function (o) { return h("option", { key: o.value, value: o.value }, t(o.label)); })),
|
|
686
|
+
hint
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
if (f.type === "modellist") {
|
|
690
|
+
return h("label", { key: f.key, className: "__tv_field" },
|
|
691
|
+
label,
|
|
692
|
+
h("input", {
|
|
693
|
+
className: "__tv_input",
|
|
694
|
+
type: "text",
|
|
695
|
+
list: MODEL_OPTIONS_ID,
|
|
696
|
+
value: fieldDraft(f),
|
|
697
|
+
placeholder: t("catalogPickHint"),
|
|
698
|
+
onChange: function (e) { setField(f, e.target.value); }
|
|
699
|
+
}),
|
|
700
|
+
hint,
|
|
701
|
+
renderPicker()
|
|
422
702
|
);
|
|
423
703
|
}
|
|
424
704
|
return h("label", { key: f.key, className: "__tv_field" },
|
|
425
|
-
|
|
426
|
-
labelOf(f, t),
|
|
427
|
-
overridden ? h("span", { className: "__tv_override" }, t("overridden")) : null
|
|
428
|
-
),
|
|
705
|
+
label,
|
|
429
706
|
h("input", {
|
|
430
707
|
className: "__tv_input",
|
|
431
708
|
type: f.type === "password" ? "password" : f.type === "number" ? "number" : "text",
|
|
@@ -433,9 +710,10 @@ window.__ModuleLoader__.load({
|
|
|
433
710
|
placeholder: f.type === "password" ? (overridden ? "••••••••" : t("apiKeyHint")) : (f.placeholder || ""),
|
|
434
711
|
onChange: function (e) { setField(f, e.target.value); }
|
|
435
712
|
}),
|
|
436
|
-
|
|
713
|
+
hint
|
|
437
714
|
);
|
|
438
715
|
}),
|
|
716
|
+
h("datalist", { id: MODEL_OPTIONS_ID }, catalogOptions),
|
|
439
717
|
h("div", { className: "__tv_actions" },
|
|
440
718
|
h("button", { type: "button", className: "__tv_btn __tv_btnPrimary", onClick: onSave, disabled: busy || !snapshot.writable }, t("save")),
|
|
441
719
|
h("button", { type: "button", className: "__tv_btn", onClick: onReset, disabled: busy || !snapshot.writable }, t("reset")),
|
|
@@ -450,7 +728,7 @@ window.__ModuleLoader__.load({
|
|
|
450
728
|
var out = {};
|
|
451
729
|
for (var i = 0; i < FIELDS.length; i += 1) {
|
|
452
730
|
var f = FIELDS[i];
|
|
453
|
-
out[f.key] = f.type === "csv" ? draftToCsv(value[f.key]) : f.type === "checkbox" ? Boolean(value[f.key]) : String(value[f.key] ?? "");
|
|
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] ?? "");
|
|
454
732
|
}
|
|
455
733
|
return out;
|
|
456
734
|
}
|
|
@@ -458,6 +736,58 @@ window.__ModuleLoader__.load({
|
|
|
458
736
|
return Array.isArray(arr) ? arr.join(", ") : String(arr ?? "");
|
|
459
737
|
}
|
|
460
738
|
|
|
739
|
+
// ── configured-model catalog (v0.9.0) ─────────────────────────────────
|
|
740
|
+
/** Loopback route the server half serves the configured model list on. */
|
|
741
|
+
var CATALOG_URL = "/plugins/dsh-tool-vision/models";
|
|
742
|
+
/** Id of the shared <datalist> every model-list field points at. */
|
|
743
|
+
var MODEL_OPTIONS_ID = "__tv_model_options";
|
|
744
|
+
/** Cap on rendered candidates: a huge catalog must not bloat the panel. */
|
|
745
|
+
var MODEL_OPTIONS_MAX = 400;
|
|
746
|
+
|
|
747
|
+
/** Translate the server's decision reason into a locale key. */
|
|
748
|
+
function sourceLabel(source, t) {
|
|
749
|
+
var key = {
|
|
750
|
+
whitelist: "catalogSourceWhitelist",
|
|
751
|
+
blacklist: "catalogSourceBlacklist",
|
|
752
|
+
auto: "catalogSourceAuto",
|
|
753
|
+
"probe-yes": "catalogSourceProbeYes",
|
|
754
|
+
"probe-no": "catalogSourceProbeNo",
|
|
755
|
+
"bridge-off": "catalogSourceBridgeOff",
|
|
756
|
+
"no-route": "catalogSourceNoRoute"
|
|
757
|
+
}[source] || "catalogSourceDefault";
|
|
758
|
+
return t(key);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* One <option> per configured model, plus one for its bare id, so both
|
|
763
|
+
* `xiaomi/mimo-v2.5` and `mimo-v2.5` are reachable while typing. This is
|
|
764
|
+
* only the *typing* aid; the visible picker below is the primary UI,
|
|
765
|
+
* because a native datalist stays invisible until the user types.
|
|
766
|
+
*/
|
|
767
|
+
function buildCatalogOptions(catalog, t) {
|
|
768
|
+
var out = [];
|
|
769
|
+
var providers = catalog && Array.isArray(catalog.providers) ? catalog.providers : [];
|
|
770
|
+
for (var pi = 0; pi < providers.length && out.length < MODEL_OPTIONS_MAX; pi += 1) {
|
|
771
|
+
var provider = providers[pi];
|
|
772
|
+
var models = Array.isArray(provider.models) ? provider.models : [];
|
|
773
|
+
for (var mi = 0; mi < models.length && out.length < MODEL_OPTIONS_MAX; mi += 1) {
|
|
774
|
+
var entry = models[mi];
|
|
775
|
+
var suffix = " · " + provider.id + (entry.image ? " · " + t("catalogImage") : "");
|
|
776
|
+
out.push(h("option", {
|
|
777
|
+
key: pi + ":" + mi + ":" + entry.id,
|
|
778
|
+
value: entry.id,
|
|
779
|
+
label: (entry.name || entry.id) + suffix
|
|
780
|
+
}));
|
|
781
|
+
var slash = entry.id.lastIndexOf("/");
|
|
782
|
+
if (slash !== -1) {
|
|
783
|
+
var bare = entry.id.slice(slash + 1);
|
|
784
|
+
out.push(h("option", { key: pi + ":" + mi + ":bare:" + bare, value: bare, label: bare + suffix }));
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
return out;
|
|
789
|
+
}
|
|
790
|
+
|
|
461
791
|
// ── bridge image preview (v0.4.0, xing666173 / dsh-bridge-preview) ────
|
|
462
792
|
var PREVIEW_MARK = "\u200b[bridge]";
|
|
463
793
|
var PREVIEW_ATTR = "data-tv-preview";
|