@xlight-oss/visionary-dsh 0.6.0 → 0.7.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 +42 -14
- package/cordis.patch.yml +6 -2
- package/lib/image-bridge/core.mjs +21 -4
- package/lib/image-bridge/index.mjs +73 -3
- package/lib/image-bridge/persistence.mjs +66 -24
- package/lib/image-bridge/rewrite.mjs +13 -0
- package/lib/image-bridge/trust-fence.mjs +85 -0
- package/lib/index.mjs +263 -91
- package/lib/settings-card/client.js +607 -0
- package/lib/settings-card/index.mjs +18 -0
- package/lib/settings-card/package.json +36 -0
- package/lib/settings-route.mjs +215 -0
- package/package.json +5 -1
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@xlight-oss/visionary-dsh/settings-card",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
var React = require("react");
|
|
8
|
+
var _dsh_client_runtime = require("@deepseek-ai/dsh-client-runtime/client");
|
|
9
|
+
|
|
10
|
+
// ── namespaces served by the host /visionary/api route ──
|
|
11
|
+
|
|
12
|
+
var NS_BRIDGE = "visionary-image-bridge";
|
|
13
|
+
var NS_VISION = "visionary-vision";
|
|
14
|
+
|
|
15
|
+
// ── field definitions: one merged page over both namespace schemas ──
|
|
16
|
+
// `id` is the unique working key. Single-target fields carry `ns`/`key`;
|
|
17
|
+
// shared fields carry `targets` and write the same logical setting to
|
|
18
|
+
// several namespace keys at once (e.g. binaryPath is shared by the tools
|
|
19
|
+
// and the bridge rows). `kind` drives the widget, `group` drives the
|
|
20
|
+
// in-page group heading. Order = display order: core behavior first,
|
|
21
|
+
// rarely-touched path config last.
|
|
22
|
+
|
|
23
|
+
function field(ns, key, kind, opts) {
|
|
24
|
+
return Object.assign(
|
|
25
|
+
{ ns: ns, key: key, id: ns + "." + key, kind: kind, targets: [{ ns: ns, key: key }] },
|
|
26
|
+
opts,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function sharedField(id, key, kind, opts) {
|
|
31
|
+
return Object.assign({ id: id, key: key, kind: kind }, opts);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
var FIELDS = [
|
|
35
|
+
// 视觉工具(visionary-vision):常用配置在前,二进制路径最后
|
|
36
|
+
// (一个共享 binaryPath 同时作用于工具与桥接两个命名空间)
|
|
37
|
+
field(NS_VISION, "modelType", "select", { group: "vision", options: ["vision", "ocr"], labelKey: "visionModelTypeLabel", hintKey: "visionModelTypeHint" }),
|
|
38
|
+
field(NS_VISION, "visionTimeoutMs", "number", { group: "vision", labelKey: "visionTimeoutLabel", hintKey: "visionTimeoutHint" }),
|
|
39
|
+
field(NS_VISION, "statusTimeoutMs", "number", { group: "vision", labelKey: "visionStatusTimeoutLabel", hintKey: "visionStatusTimeoutHint" }),
|
|
40
|
+
field(NS_VISION, "loginTimeoutSeconds", "number", { group: "vision", labelKey: "visionLoginTimeoutLabel", hintKey: "visionLoginTimeoutHint" }),
|
|
41
|
+
sharedField("shared.binaryPath", "binaryPath", "text", {
|
|
42
|
+
group: "vision",
|
|
43
|
+
labelKey: "sharedBinaryPathLabel",
|
|
44
|
+
hintKey: "sharedBinaryPathHint",
|
|
45
|
+
targets: [
|
|
46
|
+
{ ns: NS_VISION, key: "binaryPath" },
|
|
47
|
+
{ ns: NS_BRIDGE, key: "binaryPath" },
|
|
48
|
+
],
|
|
49
|
+
}),
|
|
50
|
+
// 图片桥接(visionary-image-bridge):开关 → 行为 → 存储
|
|
51
|
+
field(NS_BRIDGE, "enabled", "boolean", { group: "bridge", labelKey: "enabledLabel", hintKey: "enabledHint" }),
|
|
52
|
+
field(NS_BRIDGE, "scope", "select", { group: "bridge", options: ["text-only", "also-vl"], labelKey: "scopeLabel", hintKey: "scopeHint" }),
|
|
53
|
+
field(NS_BRIDGE, "mode", "select", { group: "bridge", options: ["agentic", "deterministic"], labelKey: "modeLabel", hintKey: "modeHint" }),
|
|
54
|
+
field(NS_BRIDGE, "promptTemplate", "textarea", { group: "bridge", labelKey: "promptTemplateLabel", hintKey: "promptTemplateHint" }),
|
|
55
|
+
field(NS_BRIDGE, "pastedDir", "text", { group: "bridge", labelKey: "pastedDirLabel", hintKey: "pastedDirHint" }),
|
|
56
|
+
field(NS_BRIDGE, "retainHours", "number", { group: "bridge", labelKey: "retainHoursLabel", hintKey: "retainHoursHint" }),
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
var FIELDS_BY_ID = {};
|
|
60
|
+
FIELDS.forEach(function (f) { FIELDS_BY_ID[f.id] = f; });
|
|
61
|
+
|
|
62
|
+
// ── locale keys ──
|
|
63
|
+
|
|
64
|
+
var NS = "settings.plugins.visionary";
|
|
65
|
+
|
|
66
|
+
var LOCALE_ZH = {
|
|
67
|
+
nav: "Visionary",
|
|
68
|
+
title: "Visionary",
|
|
69
|
+
description: "DeepSeek Visionary:视觉识图 / OCR 工具(上传管道、超时)+ 文本模型图片桥接,修改即时生效",
|
|
70
|
+
visionGroupLabel: "视觉工具",
|
|
71
|
+
visionGroupDescription: "deepseek_vision / deepseek_ocr 等 5 个原生工具",
|
|
72
|
+
bridgeGroupLabel: "图片桥接",
|
|
73
|
+
bridgeGroupDescription: "纯文本模型粘贴图片自动放行 + 改写为文本引导",
|
|
74
|
+
visionModelTypeLabel: "上传管道",
|
|
75
|
+
visionModelTypeHint: "vision(默认):完整多模态理解 | ocr:deepseek_vision 走纯文字提取管道,等价每次调用 deepseek_ocr。修改后即时生效",
|
|
76
|
+
visionLoginTimeoutLabel: "登录超时(秒)",
|
|
77
|
+
visionLoginTimeoutHint: "deepseek_vision_login 阻塞等待上限;DEEPSEEK_LOGIN_TIMEOUT 环境变量优先",
|
|
78
|
+
visionTimeoutLabel: "识图超时(毫秒)",
|
|
79
|
+
visionTimeoutHint: "deepseek_vision / deepseek_ocr 单次调用超时",
|
|
80
|
+
visionStatusTimeoutLabel: "状态超时(毫秒)",
|
|
81
|
+
visionStatusTimeoutHint: "deepseek_vision_status / deepseek_vision_logout 超时",
|
|
82
|
+
sharedBinaryPathLabel: "二进制路径",
|
|
83
|
+
sharedBinaryPathHint: "visionary-server 路径,工具与桥接(deterministic 模式)共用;空 = DEEPSEEK_VISIONARY_BIN → PATH",
|
|
84
|
+
enabledLabel: "桥接启用",
|
|
85
|
+
enabledHint: "关闭后恢复宿主原行为(文本模型粘贴图片仍被拒绝)",
|
|
86
|
+
pastedDirLabel: "落盘目录",
|
|
87
|
+
pastedDirHint: "图片落盘目录,强制 0700 / 文件 0600,支持 ~",
|
|
88
|
+
retainHoursLabel: "保留小时数",
|
|
89
|
+
retainHoursHint: "落盘副本保留小时数,<= 0 表示不清理",
|
|
90
|
+
scopeLabel: "桥接范围",
|
|
91
|
+
scopeHint: "text-only:仅文本模型 | also-vl:VL 模型同样经桥接",
|
|
92
|
+
modeLabel: "桥接模式",
|
|
93
|
+
modeHint: "agentic:改写为引导文本 | deterministic:直接调用分析",
|
|
94
|
+
promptTemplateLabel: "引导模板",
|
|
95
|
+
promptTemplateHint: "必须包含 {path} 占位符",
|
|
96
|
+
save: "保存",
|
|
97
|
+
saving: "保存中…",
|
|
98
|
+
discard: "放弃修改",
|
|
99
|
+
unsaved: "未保存",
|
|
100
|
+
saveFailed: "保存失败,已保留供修改。",
|
|
101
|
+
saveConflict: "保存被拒绝:配置已在别处修改,请刷新后重试。",
|
|
102
|
+
overridden: "已覆盖",
|
|
103
|
+
readOnly: "本部署设置为只读。",
|
|
104
|
+
loading: "加载中…",
|
|
105
|
+
unavailable: "设置服务不可用。",
|
|
106
|
+
invalidNumber: "请输入数字;留空表示使用默认值。",
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
var LOCALE_EN = {
|
|
110
|
+
nav: "Visionary",
|
|
111
|
+
title: "Visionary",
|
|
112
|
+
description: "DeepSeek Visionary: vision/OCR tools (upload pipeline, timeouts) + text-model image bridge. Changes apply immediately",
|
|
113
|
+
visionGroupLabel: "Vision Tools",
|
|
114
|
+
visionGroupDescription: "deepseek_vision / deepseek_ocr and 3 more native tools",
|
|
115
|
+
bridgeGroupLabel: "Image Bridge",
|
|
116
|
+
bridgeGroupDescription: "Transparent image admission and rewrite for text-only models",
|
|
117
|
+
visionModelTypeLabel: "Upload pipeline",
|
|
118
|
+
visionModelTypeHint: "vision (default): full multimodal understanding | ocr: deepseek_vision routes through text-extraction, same as deepseek_ocr. Applies immediately",
|
|
119
|
+
visionLoginTimeoutLabel: "Login timeout (s)",
|
|
120
|
+
visionLoginTimeoutHint: "deepseek_vision_login block cap; DEEPSEEK_LOGIN_TIMEOUT env wins",
|
|
121
|
+
visionTimeoutLabel: "Vision timeout (ms)",
|
|
122
|
+
visionTimeoutHint: "per deepseek_vision / deepseek_ocr call timeout",
|
|
123
|
+
visionStatusTimeoutLabel: "Status timeout (ms)",
|
|
124
|
+
visionStatusTimeoutHint: "deepseek_vision_status / deepseek_vision_logout timeout",
|
|
125
|
+
sharedBinaryPathLabel: "Binary path",
|
|
126
|
+
sharedBinaryPathHint: "visionary-server binary, shared by the tools and the bridge (deterministic mode); empty = DEEPSEEK_VISIONARY_BIN → PATH",
|
|
127
|
+
enabledLabel: "Bridge enabled",
|
|
128
|
+
enabledHint: "Off restores host behavior (text-only models reject images again)",
|
|
129
|
+
pastedDirLabel: "Paste directory",
|
|
130
|
+
pastedDirHint: "Image save dir (0700 dir / 0600 files), supports ~",
|
|
131
|
+
retainHoursLabel: "Retention (hours)",
|
|
132
|
+
retainHoursHint: "Pasted file retention; <= 0 disables cleanup",
|
|
133
|
+
scopeLabel: "Bridge scope",
|
|
134
|
+
scopeHint: "text-only: text models only | also-vl: VL models bridged too",
|
|
135
|
+
modeLabel: "Bridge mode",
|
|
136
|
+
modeHint: "agentic: rewrite to guide | deterministic: analyze directly",
|
|
137
|
+
promptTemplateLabel: "Prompt template",
|
|
138
|
+
promptTemplateHint: "Must contain the {path} placeholder",
|
|
139
|
+
save: "Save",
|
|
140
|
+
saving: "Saving…",
|
|
141
|
+
discard: "Discard",
|
|
142
|
+
unsaved: "Unsaved",
|
|
143
|
+
saveFailed: "Save failed; values left for you to correct.",
|
|
144
|
+
saveConflict: "Save refused: the config changed elsewhere. Refresh and retry.",
|
|
145
|
+
overridden: "Overridden",
|
|
146
|
+
readOnly: "This deployment stores settings read-only.",
|
|
147
|
+
loading: "Loading…",
|
|
148
|
+
unavailable: "Settings service unavailable.",
|
|
149
|
+
invalidNumber: "Enter a number, or leave blank to use the default.",
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// ── private settings route client ──
|
|
153
|
+
// The DSH settings RPC domain only serves allowlisted namespaces, so this
|
|
154
|
+
// plugin's namespaces are read/written through the host's own fenced
|
|
155
|
+
// /visionary/api routes (see ../settings-route.mjs). Each call carries the
|
|
156
|
+
// target `ns`; the host defaults to the bridge namespace when absent. Any
|
|
157
|
+
// failure (route forbidden, settings service absent, a value outside the
|
|
158
|
+
// schema) surfaces as a rejected promise the scope turns into status.
|
|
159
|
+
|
|
160
|
+
function callVisionaryApi(method, payload) {
|
|
161
|
+
return fetch("/visionary/api/" + method, {
|
|
162
|
+
method: "POST",
|
|
163
|
+
headers: { "content-type": "application/json" },
|
|
164
|
+
body: JSON.stringify(payload || {}),
|
|
165
|
+
})
|
|
166
|
+
.then(function (response) {
|
|
167
|
+
return response.json().catch(function () { return null; });
|
|
168
|
+
})
|
|
169
|
+
.then(function (parsed) {
|
|
170
|
+
if (parsed === null || parsed.ok !== true || parsed.value === undefined) {
|
|
171
|
+
var code = (parsed && parsed.error && parsed.error.code) || "http";
|
|
172
|
+
var message = (parsed && parsed.error && parsed.error.message) || "bad response";
|
|
173
|
+
var err = new Error(message);
|
|
174
|
+
err.code = code;
|
|
175
|
+
throw err;
|
|
176
|
+
}
|
|
177
|
+
return parsed.value;
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ── scope: one snapshot store per namespace ──
|
|
182
|
+
|
|
183
|
+
function makeScope(ns) {
|
|
184
|
+
var scope = _dsh_client_runtime.createSnapshotStore({
|
|
185
|
+
status: "loading",
|
|
186
|
+
value: undefined,
|
|
187
|
+
base: undefined,
|
|
188
|
+
user: undefined,
|
|
189
|
+
revision: undefined,
|
|
190
|
+
writable: false,
|
|
191
|
+
});
|
|
192
|
+
var generation = 0;
|
|
193
|
+
var lastError = null;
|
|
194
|
+
var tail = Promise.resolve();
|
|
195
|
+
|
|
196
|
+
var applyView = function (view) {
|
|
197
|
+
scope.set({
|
|
198
|
+
status: "ready",
|
|
199
|
+
value: view.value,
|
|
200
|
+
base: view.base,
|
|
201
|
+
user: view.user,
|
|
202
|
+
revision: view.revision,
|
|
203
|
+
writable: view.writable,
|
|
204
|
+
});
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
var load = function () {
|
|
208
|
+
var gen = ++generation;
|
|
209
|
+
var op = callVisionaryApi("settings.get", { ns: ns }).then(function (view) {
|
|
210
|
+
if (gen !== generation) return;
|
|
211
|
+
applyView(view);
|
|
212
|
+
}).catch(function (err) {
|
|
213
|
+
if (gen !== generation) return;
|
|
214
|
+
lastError = err;
|
|
215
|
+
scope.set({
|
|
216
|
+
status: "unavailable",
|
|
217
|
+
value: undefined,
|
|
218
|
+
base: undefined,
|
|
219
|
+
user: undefined,
|
|
220
|
+
revision: undefined,
|
|
221
|
+
writable: false,
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
tail = op.catch(function () {});
|
|
225
|
+
return op;
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
var write = function (patch, expectedRevision) {
|
|
229
|
+
var gen = ++generation;
|
|
230
|
+
var op = callVisionaryApi("settings.update", {
|
|
231
|
+
ns: ns,
|
|
232
|
+
patch: patch,
|
|
233
|
+
...(expectedRevision !== undefined ? { expectedRevision: expectedRevision } : {}),
|
|
234
|
+
}).then(function (view) {
|
|
235
|
+
if (gen !== generation) return;
|
|
236
|
+
applyView(view);
|
|
237
|
+
}).catch(function (err) {
|
|
238
|
+
if (gen !== generation) return;
|
|
239
|
+
lastError = err;
|
|
240
|
+
throw err;
|
|
241
|
+
});
|
|
242
|
+
tail = op.catch(function () {});
|
|
243
|
+
return op;
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
var mutate = function (ops, expectedRevision) {
|
|
247
|
+
var gen = ++generation;
|
|
248
|
+
var op = callVisionaryApi("settings.mutate", {
|
|
249
|
+
ns: ns,
|
|
250
|
+
ops: ops,
|
|
251
|
+
...(expectedRevision !== undefined ? { expectedRevision: expectedRevision } : {}),
|
|
252
|
+
}).then(function (view) {
|
|
253
|
+
if (gen !== generation) return;
|
|
254
|
+
applyView(view);
|
|
255
|
+
}).catch(function (err) {
|
|
256
|
+
if (gen !== generation) return;
|
|
257
|
+
lastError = err;
|
|
258
|
+
throw err;
|
|
259
|
+
});
|
|
260
|
+
tail = op.catch(function () {});
|
|
261
|
+
return op;
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
load();
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
scope: scope,
|
|
268
|
+
getSnapshot: function () { return scope.getSnapshot(); },
|
|
269
|
+
subscribe: function (fn) { return scope.subscribe(fn); },
|
|
270
|
+
reload: load,
|
|
271
|
+
write: write,
|
|
272
|
+
mutate: mutate,
|
|
273
|
+
lastError: function () { return lastError; },
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ── inject factory for the settings.section slot ──
|
|
278
|
+
// `scopes` maps each namespace to its own scope; the merged page combines
|
|
279
|
+
// their snapshots. Saving splits the staged edits back to the owning
|
|
280
|
+
// namespace (each write uses that namespace's own revision).
|
|
281
|
+
|
|
282
|
+
function makeSectionInject(ctx, scopes, fields) {
|
|
283
|
+
var t = ctx.locale.bind(NS);
|
|
284
|
+
var staged = {};
|
|
285
|
+
var saving = false;
|
|
286
|
+
var failed = false;
|
|
287
|
+
var conflict = false;
|
|
288
|
+
var listeners = new Set();
|
|
289
|
+
var cache = null;
|
|
290
|
+
|
|
291
|
+
var emit = function () {
|
|
292
|
+
listeners.forEach(function (l) { return l(); });
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
var snapshotOf = function (ns) { return scopes[ns].getSnapshot(); };
|
|
296
|
+
|
|
297
|
+
// Shared fields write the same logical setting to several namespace keys
|
|
298
|
+
// (e.g. binaryPath). Display value: first non-empty target; overridden:
|
|
299
|
+
// any target's user layer holds the key.
|
|
300
|
+
var fieldDisplayValue = function (f) {
|
|
301
|
+
for (var i = 0; i < f.targets.length; i++) {
|
|
302
|
+
var t = f.targets[i];
|
|
303
|
+
var raw = (snapshotOf(t.ns).value || {})[t.key];
|
|
304
|
+
if (raw !== undefined && raw !== null && raw !== "") return String(raw);
|
|
305
|
+
}
|
|
306
|
+
return "";
|
|
307
|
+
};
|
|
308
|
+
var fieldOverridden = function (f) {
|
|
309
|
+
for (var i = 0; i < f.targets.length; i++) {
|
|
310
|
+
var t = f.targets[i];
|
|
311
|
+
var u = snapshotOf(t.ns).user;
|
|
312
|
+
if (u !== undefined && Object.prototype.hasOwnProperty.call(u, t.key)) return true;
|
|
313
|
+
}
|
|
314
|
+
return false;
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
var rebuild = function () {
|
|
318
|
+
var statuses = Object.keys(scopes).map(function (ns) { return snapshotOf(ns).status; });
|
|
319
|
+
var ready = statuses.every(function (s) { return s === "ready"; });
|
|
320
|
+
var unavailable = statuses.every(function (s) { return s === "unavailable"; });
|
|
321
|
+
var writable = Object.keys(scopes).every(function (ns) { return snapshotOf(ns).writable; });
|
|
322
|
+
if (!ready) {
|
|
323
|
+
cache = {
|
|
324
|
+
available: ready,
|
|
325
|
+
status: unavailable ? "unavailable" : "loading",
|
|
326
|
+
writable: writable,
|
|
327
|
+
dirty: Object.keys(staged).length > 0,
|
|
328
|
+
invalid: false,
|
|
329
|
+
saving: saving,
|
|
330
|
+
failed: failed,
|
|
331
|
+
conflict: conflict,
|
|
332
|
+
fields: {},
|
|
333
|
+
value: {},
|
|
334
|
+
user: undefined,
|
|
335
|
+
};
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
var value = {};
|
|
339
|
+
var user = {};
|
|
340
|
+
Object.keys(scopes).forEach(function (ns) {
|
|
341
|
+
var s = snapshotOf(ns);
|
|
342
|
+
Object.assign(value, s.value || {});
|
|
343
|
+
Object.assign(user, s.user || {});
|
|
344
|
+
});
|
|
345
|
+
var fieldsOut = {};
|
|
346
|
+
var invalid = false;
|
|
347
|
+
fields.forEach(function (f) {
|
|
348
|
+
var st = Object.prototype.hasOwnProperty.call(staged, f.id) ? staged[f.id] : undefined;
|
|
349
|
+
var text;
|
|
350
|
+
var overridden;
|
|
351
|
+
var fieldInvalid = false;
|
|
352
|
+
if (st !== undefined) {
|
|
353
|
+
text = st.text;
|
|
354
|
+
overridden = st.text !== fieldDisplayValue(f);
|
|
355
|
+
if (f.kind === "number") {
|
|
356
|
+
var trimmed = st.text.trim();
|
|
357
|
+
if (trimmed !== "") fieldInvalid = !Number.isFinite(Number(trimmed));
|
|
358
|
+
}
|
|
359
|
+
} else {
|
|
360
|
+
text = fieldDisplayValue(f);
|
|
361
|
+
overridden = fieldOverridden(f);
|
|
362
|
+
}
|
|
363
|
+
if (fieldInvalid) invalid = true;
|
|
364
|
+
fieldsOut[f.id] = { text: text, overridden: overridden, invalid: fieldInvalid };
|
|
365
|
+
});
|
|
366
|
+
cache = {
|
|
367
|
+
available: true,
|
|
368
|
+
status: "ready",
|
|
369
|
+
writable: writable,
|
|
370
|
+
dirty: Object.keys(staged).length > 0,
|
|
371
|
+
invalid: invalid,
|
|
372
|
+
saving: saving,
|
|
373
|
+
failed: failed,
|
|
374
|
+
conflict: conflict,
|
|
375
|
+
fields: fieldsOut,
|
|
376
|
+
value: value,
|
|
377
|
+
user: user,
|
|
378
|
+
};
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
var publish = function () { rebuild(); emit(); };
|
|
382
|
+
|
|
383
|
+
ctx.effect(function () {
|
|
384
|
+
var cleanups = Object.keys(scopes).map(function (ns) {
|
|
385
|
+
return scopes[ns].subscribe(function () { publish(); });
|
|
386
|
+
});
|
|
387
|
+
return function () { cleanups.forEach(function (cancel) { cancel(); }); };
|
|
388
|
+
}, "visionary-settings-card: namespace scope subscriptions");
|
|
389
|
+
|
|
390
|
+
rebuild();
|
|
391
|
+
|
|
392
|
+
var store = {
|
|
393
|
+
getSnapshot: function () { return cache; },
|
|
394
|
+
subscribe: function (fn) {
|
|
395
|
+
listeners.add(fn);
|
|
396
|
+
return function () { listeners.delete(fn); };
|
|
397
|
+
},
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
var stage = function (fieldId, text) {
|
|
401
|
+
var f = FIELDS_BY_ID[fieldId];
|
|
402
|
+
if (text === fieldDisplayValue(f)) delete staged[fieldId];
|
|
403
|
+
else staged[fieldId] = { text: text };
|
|
404
|
+
publish();
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
var resetField = function (fieldId) {
|
|
408
|
+
var f = FIELDS_BY_ID[fieldId];
|
|
409
|
+
if (fieldDisplayValue(f) === "") delete staged[fieldId];
|
|
410
|
+
else staged[fieldId] = { text: "" };
|
|
411
|
+
publish();
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
var coerce = function (fieldId, text) {
|
|
415
|
+
var f = FIELDS_BY_ID[fieldId];
|
|
416
|
+
if (!f) return text;
|
|
417
|
+
if (f.kind === "boolean") return text === "true";
|
|
418
|
+
if (f.kind === "number") return Number(text);
|
|
419
|
+
return text;
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
var save = async function () {
|
|
423
|
+
saving = true;
|
|
424
|
+
failed = false;
|
|
425
|
+
conflict = false;
|
|
426
|
+
publish();
|
|
427
|
+
var byNs = {};
|
|
428
|
+
for (var fieldId in staged) {
|
|
429
|
+
if (!Object.prototype.hasOwnProperty.call(staged, fieldId)) continue;
|
|
430
|
+
var f = FIELDS_BY_ID[fieldId];
|
|
431
|
+
var entry = staged[fieldId];
|
|
432
|
+
f.targets.forEach(function (t) {
|
|
433
|
+
var group = byNs[t.ns] || (byNs[t.ns] = { patch: {}, ops: [] });
|
|
434
|
+
if (entry.text.trim() === "") {
|
|
435
|
+
group.ops.push({ op: "unset", path: [t.key] });
|
|
436
|
+
} else {
|
|
437
|
+
group.patch[t.key] = coerce(fieldId, entry.text.trim());
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
try {
|
|
442
|
+
for (var ns in byNs) {
|
|
443
|
+
var g = byNs[ns];
|
|
444
|
+
var s = snapshotOf(ns);
|
|
445
|
+
if (g.ops.length > 0) await scopes[ns].mutate(g.ops, s.revision);
|
|
446
|
+
if (Object.keys(g.patch).length > 0) {
|
|
447
|
+
var after = s.revision;
|
|
448
|
+
if (g.ops.length > 0) after = snapshotOf(ns).revision;
|
|
449
|
+
await scopes[ns].write(g.patch, after);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
staged = {};
|
|
453
|
+
} catch (err) {
|
|
454
|
+
if (err && err.code === "settings-conflict") conflict = true;
|
|
455
|
+
else failed = true;
|
|
456
|
+
}
|
|
457
|
+
saving = false;
|
|
458
|
+
publish();
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
var discard = function () {
|
|
462
|
+
staged = {};
|
|
463
|
+
failed = false;
|
|
464
|
+
conflict = false;
|
|
465
|
+
publish();
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
return {
|
|
469
|
+
hooks: { scope: store },
|
|
470
|
+
edit: function (fieldId, text) { stage(fieldId, text); },
|
|
471
|
+
resetField: resetField,
|
|
472
|
+
save: save,
|
|
473
|
+
discard: discard,
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// ── settings section component ──
|
|
478
|
+
|
|
479
|
+
function VisionarySection(props) {
|
|
480
|
+
var t = props.t;
|
|
481
|
+
var snapshot = props.useScope(function (s) { return s; });
|
|
482
|
+
|
|
483
|
+
var itemStyle = { display: "flex", flexDirection: "column", gap: 20, padding: "4px 2px" };
|
|
484
|
+
var titleStyle = { margin: 0, color: "var(--dsw-alias-label-primary)", fontSize: 20, fontWeight: 700, lineHeight: 1.4 };
|
|
485
|
+
var descStyle = { margin: 0, color: "var(--dsw-alias-label-tertiary)", fontSize: 14, lineHeight: 1.6 };
|
|
486
|
+
var cardStyle = { border: "1px solid var(--dsw-alias-border-l2)", background: "var(--dsw-alias-bg-layer-3)", borderRadius: 12, overflow: "hidden" };
|
|
487
|
+
var bodyStyle = { padding: "4px 16px 16px" };
|
|
488
|
+
var fieldStyle = { display: "flex", flexDirection: "column", gap: 6, padding: "14px 0" };
|
|
489
|
+
var labelRow = { display: "flex", alignItems: "center", gap: 8 };
|
|
490
|
+
var labelStyle = { minWidth: 0, color: "var(--dsw-alias-label-primary)", flex: 1, fontSize: 13, fontWeight: 500, lineHeight: 1.5 };
|
|
491
|
+
var inputStyle = { boxSizing: "border-box", border: "1px solid var(--dsw-alias-border-l2)", background: "var(--dsw-alias-bg-layer-3)", font: "inherit", color: "var(--dsw-alias-label-primary)", borderRadius: 8, padding: "4px 12px", fontSize: 13, lineHeight: 1.5, width: "100%" };
|
|
492
|
+
var textareaStyle = { minHeight: 72, resize: "vertical", paddingTop: 8, paddingBottom: 8 };
|
|
493
|
+
Object.assign(textareaStyle, inputStyle);
|
|
494
|
+
var hintStyle = { color: "var(--dsw-alias-label-tertiary)", margin: 0, fontSize: 12, lineHeight: 1.5 };
|
|
495
|
+
var errStyle = { color: "var(--dsw-alias-label-error)", margin: 0, fontSize: 12, lineHeight: 1.5 };
|
|
496
|
+
var footerStyle = { borderTop: "1px solid var(--dsw-alias-border-l2)", justifyContent: "flex-end", alignItems: "center", gap: 8, padding: "12px 0 4px", display: "flex" };
|
|
497
|
+
var btnBase = { appearance: "none", font: "inherit", cursor: "pointer", border: "1px solid transparent", borderRadius: 8, padding: "5px 14px", fontSize: 13, lineHeight: 1.5 };
|
|
498
|
+
var discardBtn = { borderColor: "var(--dsw-alias-border-l2)", color: "var(--dsw-alias-label-secondary)", background: "none" };
|
|
499
|
+
Object.assign(discardBtn, btnBase);
|
|
500
|
+
var saveBtn = { color: "#fff", background: "var(--dsw-alias-brand-primary, #1677ff)" };
|
|
501
|
+
Object.assign(saveBtn, btnBase);
|
|
502
|
+
|
|
503
|
+
var fieldsEl;
|
|
504
|
+
if (snapshot.status === "loading") {
|
|
505
|
+
fieldsEl = React.createElement("p", { style: { color: "var(--dsw-alias-label-tertiary)", fontSize: 13, margin: "16px 0" } }, t("loading"));
|
|
506
|
+
} else if (snapshot.status === "unavailable") {
|
|
507
|
+
fieldsEl = React.createElement("p", { style: { color: "var(--dsw-alias-label-error)", fontSize: 13, margin: "16px 0" } }, t("unavailable"));
|
|
508
|
+
} else {
|
|
509
|
+
var groupTitleStyle = { margin: 0, color: "var(--dsw-alias-label-primary)", fontSize: 14, fontWeight: 600, lineHeight: 1.5 };
|
|
510
|
+
var groupDescStyle = { margin: "2px 0 0", color: "var(--dsw-alias-label-tertiary)", fontSize: 12, lineHeight: 1.5 };
|
|
511
|
+
var groupHeaderStyle = { display: "flex", flexDirection: "column", gap: 2, padding: "18px 0 2px", borderTop: "1px solid var(--dsw-alias-border-l2)" };
|
|
512
|
+
var firstGroupStyle = { display: "flex", flexDirection: "column", gap: 2, padding: "2px 0 4px" };
|
|
513
|
+
fieldsEl = [];
|
|
514
|
+
var lastGroup = null;
|
|
515
|
+
FIELDS.forEach(function (f) {
|
|
516
|
+
if (f.group !== lastGroup) {
|
|
517
|
+
lastGroup = f.group;
|
|
518
|
+
var headerStyle = fieldsEl.length === 0 ? firstGroupStyle : groupHeaderStyle;
|
|
519
|
+
fieldsEl.push(
|
|
520
|
+
React.createElement("header", { key: "group-" + f.group, style: headerStyle }, [
|
|
521
|
+
React.createElement("h3", { key: "title", style: groupTitleStyle }, t(f.group + "GroupLabel")),
|
|
522
|
+
React.createElement("p", { key: "desc", style: groupDescStyle }, t(f.group + "GroupDescription")),
|
|
523
|
+
])
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
var st = snapshot.fields[f.id] || { text: "", overridden: false, invalid: false };
|
|
527
|
+
var disabled = !snapshot.writable || snapshot.saving;
|
|
528
|
+
var edit = function (text) { props.edit(f.id, text); };
|
|
529
|
+
var inputEl;
|
|
530
|
+
if (f.kind === "boolean") {
|
|
531
|
+
inputEl = React.createElement("input", { type: "checkbox", checked: st.text === "true", disabled: disabled, onChange: function (e) { edit(String(e.target.checked)); } });
|
|
532
|
+
} else if (f.kind === "select") {
|
|
533
|
+
inputEl = React.createElement("select", { value: st.text, disabled: disabled, onChange: function (e) { edit(e.target.value); }, style: inputStyle },
|
|
534
|
+
f.options.map(function (o) { return React.createElement("option", { key: o, value: o }, o); })
|
|
535
|
+
);
|
|
536
|
+
} else if (f.kind === "number") {
|
|
537
|
+
inputEl = React.createElement("input", { type: "text", inputMode: "numeric", value: st.text, disabled: disabled, placeholder: t("invalidNumber"), onChange: function (e) { edit(e.target.value); }, style: Object.assign({}, inputStyle, { borderColor: st.invalid ? "var(--dsw-alias-label-error)" : undefined }) });
|
|
538
|
+
} else if (f.kind === "textarea") {
|
|
539
|
+
inputEl = React.createElement("textarea", { value: st.text, disabled: disabled, placeholder: t("promptTemplateHint"), onChange: function (e) { edit(e.target.value); }, style: textareaStyle });
|
|
540
|
+
} else {
|
|
541
|
+
inputEl = React.createElement("input", { type: "text", value: st.text, disabled: disabled, onChange: function (e) { edit(e.target.value); }, style: inputStyle });
|
|
542
|
+
}
|
|
543
|
+
fieldsEl.push(React.createElement("div", { key: f.id, style: fieldStyle }, [
|
|
544
|
+
React.createElement("div", { key: "head", style: labelRow }, [
|
|
545
|
+
React.createElement("label", { key: "label", style: labelStyle }, t(f.labelKey)),
|
|
546
|
+
st.overridden ? React.createElement("span", { key: "badge", style: { background: "var(--dsw-alias-bg-module-platform)", color: "var(--dsw-alias-label-secondary)", borderRadius: 999, padding: "1px 8px", fontSize: 11, fontWeight: 500, lineHeight: "17px", whiteSpace: "nowrap" } }, t("overridden")) : null,
|
|
547
|
+
]),
|
|
548
|
+
React.createElement("div", { key: "ctrl", style: { width: "100%" } }, inputEl),
|
|
549
|
+
React.createElement("p", { key: "hint", style: st.invalid ? errStyle : hintStyle }, st.invalid ? t("invalidNumber") : t(f.hintKey)),
|
|
550
|
+
]));
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
var blocked = snapshot.status !== "ready" || !snapshot.dirty || snapshot.invalid || snapshot.saving;
|
|
555
|
+
|
|
556
|
+
return React.createElement("div", { style: itemStyle }, [
|
|
557
|
+
React.createElement("header", { key: "head", style: { display: "flex", flexDirection: "column", gap: 4 } }, [
|
|
558
|
+
React.createElement("h2", { key: "title", style: titleStyle }, t("title")),
|
|
559
|
+
React.createElement("p", { key: "desc", style: descStyle }, t("description")),
|
|
560
|
+
]),
|
|
561
|
+
React.createElement("div", { key: "card", style: cardStyle }, [
|
|
562
|
+
React.createElement("div", { key: "body", style: bodyStyle }, [
|
|
563
|
+
!snapshot.writable && snapshot.status === "ready"
|
|
564
|
+
? React.createElement("p", { key: "ro", style: { color: "var(--dsw-alias-label-tertiary)", margin: "12px 0 0", fontSize: 12, lineHeight: 1.5 } }, t("readOnly"))
|
|
565
|
+
: null,
|
|
566
|
+
fieldsEl,
|
|
567
|
+
(snapshot.failed || snapshot.conflict) && snapshot.status === "ready"
|
|
568
|
+
? React.createElement("p", { key: "err", style: { color: "var(--dsw-alias-label-error)", flex: 1, minWidth: 0, margin: 0, fontSize: 12, lineHeight: 1.5 } }, snapshot.conflict ? t("saveConflict") : t("saveFailed"))
|
|
569
|
+
: null,
|
|
570
|
+
React.createElement("div", { key: "footer", style: footerStyle }, [
|
|
571
|
+
React.createElement("button", { key: "discard", type: "button", disabled: !snapshot.dirty || snapshot.saving, onClick: props.discard, style: discardBtn }, t("discard")),
|
|
572
|
+
React.createElement("button", { key: "save", type: "button", disabled: blocked, onClick: props.save, style: saveBtn }, snapshot.saving ? t("saving") : t("save")),
|
|
573
|
+
]),
|
|
574
|
+
]),
|
|
575
|
+
]),
|
|
576
|
+
]);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// ── apply ──
|
|
580
|
+
|
|
581
|
+
var inject = ["slots", "locale"];
|
|
582
|
+
|
|
583
|
+
function apply(ctx) {
|
|
584
|
+
ctx.effect(function () { return ctx.locale.register(NS, { zh: LOCALE_ZH, en: LOCALE_EN }); }, "locale: " + NS);
|
|
585
|
+
|
|
586
|
+
var scopes = {};
|
|
587
|
+
scopes[NS_VISION] = makeScope(NS_VISION);
|
|
588
|
+
scopes[NS_BRIDGE] = makeScope(NS_BRIDGE);
|
|
589
|
+
var t = ctx.locale.bind(NS);
|
|
590
|
+
|
|
591
|
+
ctx.slots.inject("settings.section", function () {
|
|
592
|
+
return ctx.slots.register({
|
|
593
|
+
name: "settings.section",
|
|
594
|
+
id: "visionary",
|
|
595
|
+
order: 80,
|
|
596
|
+
label: function () { return t("nav"); },
|
|
597
|
+
locale: NS,
|
|
598
|
+
inject: function () { return makeSectionInject(ctx, scopes, FIELDS); },
|
|
599
|
+
}, VisionarySection);
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
exports.inject = inject;
|
|
604
|
+
exports.apply = apply;
|
|
605
|
+
return module.exports;
|
|
606
|
+
}
|
|
607
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Host half of the settings-card sub-entry.
|
|
2
|
+
// The client half (client.js) registers the settings sections in the browser;
|
|
3
|
+
// the host half mounts the fenced /visionary/api settings route that serves
|
|
4
|
+
// BOTH visionary namespaces (visionary-image-bridge + visionary-vision) to
|
|
5
|
+
// those sections. Kept on this row (not on either feature plugin) so the
|
|
6
|
+
// panel keeps working when a feature row is disabled — each plugin registers
|
|
7
|
+
// its own namespace via installSettingsSection independently.
|
|
8
|
+
import { SETTINGS_NAMESPACE as BRIDGE_NAMESPACE } from "../image-bridge/index.mjs";
|
|
9
|
+
import { SETTINGS_NAMESPACE as VISION_NAMESPACE } from "../index.mjs";
|
|
10
|
+
import { mountVisionaryApi } from "../settings-route.mjs";
|
|
11
|
+
|
|
12
|
+
export const name = "visionary-settings-card";
|
|
13
|
+
|
|
14
|
+
export function apply(ctx) {
|
|
15
|
+
ctx.inject(["webServer"], (webCtx) => {
|
|
16
|
+
mountVisionaryApi(webCtx, [BRIDGE_NAMESPACE, VISION_NAMESPACE]);
|
|
17
|
+
});
|
|
18
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@xlight-oss/visionary-dsh/settings-card",
|
|
3
|
+
"version": "0.6.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "Client-side settings card for the visionary-dsh host plugins: Vision Tools (visionary-vision) + Image Bridge (visionary-image-bridge), web profile",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "index.mjs",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"default": "./index.mjs"
|
|
11
|
+
},
|
|
12
|
+
"./client": {
|
|
13
|
+
"default": "./client.js"
|
|
14
|
+
},
|
|
15
|
+
"./package.json": "./package.json"
|
|
16
|
+
},
|
|
17
|
+
"dsh": {
|
|
18
|
+
"client": {
|
|
19
|
+
"platform": "web",
|
|
20
|
+
"inject": [
|
|
21
|
+
"@deepseek-ai/dsh-client-locale",
|
|
22
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
23
|
+
"@deepseek-ai/dsh-client-ui-slots"
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"index.mjs",
|
|
29
|
+
"client.js"
|
|
30
|
+
],
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
33
|
+
"react": "^18.2.0"
|
|
34
|
+
},
|
|
35
|
+
"license": "MIT"
|
|
36
|
+
}
|