@logictan/dsh-browser-agent 0.1.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/LICENSE +21 -0
- package/README.md +60 -0
- package/cordis.patch.yml +18 -0
- package/lib/actions.js +155 -0
- package/lib/cdp.js +73 -0
- package/lib/client.js +823 -0
- package/lib/config.js +82 -0
- package/lib/execute.js +106 -0
- package/lib/index.js +104 -0
- package/lib/loop.js +190 -0
- package/lib/observe.js +214 -0
- package/lib/request.js +164 -0
- package/lib/typesafe.js +86 -0
- package/lib/validate.js +72 -0
- package/lib/value.js +103 -0
- package/package.json +90 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,823 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser half: the browser-agent configuration card inside the Plugins page.
|
|
3
|
+
*
|
|
4
|
+
* This file is already in the client module loader's protocol — it is not a
|
|
5
|
+
* module and imports nothing. `build.mjs` copies it to `lib/client.js`
|
|
6
|
+
* verbatim, which is why `package.json` exports `./client` at that path.
|
|
7
|
+
*
|
|
8
|
+
* The card is registered into the `plugins.row.config` slot under
|
|
9
|
+
* `<bundle>#browser-agent`, the key the Plugins page derives from the bundle
|
|
10
|
+
* that declares the row.
|
|
11
|
+
*
|
|
12
|
+
* The field list mirrors the schema in `src/config.js`; the two are kept in
|
|
13
|
+
* step by hand because this half ships as plain browser code.
|
|
14
|
+
*/
|
|
15
|
+
window.__ModuleLoader__.load({
|
|
16
|
+
id: "@logictan/dsh-browser-agent",
|
|
17
|
+
factory: function (require) {
|
|
18
|
+
var React = require("react");
|
|
19
|
+
|
|
20
|
+
/** Settings namespace the host half registers. */
|
|
21
|
+
var NS = "browser-agent";
|
|
22
|
+
/** The patch row this card configures (cordis.patch.yml). */
|
|
23
|
+
var ROW_ID = "browser-agent";
|
|
24
|
+
/**
|
|
25
|
+
* Bundle package names whose row `ROW_ID` this card configures.
|
|
26
|
+
*
|
|
27
|
+
* The page keys a row's configuration by the package that declares the row.
|
|
28
|
+
* This plugin reaches a profile in one of two shapes — as a dependency of
|
|
29
|
+
* this repository's aggregate bundle, or installed on its own — and the key
|
|
30
|
+
* differs between them. Both are registered; the key whose bundle is not
|
|
31
|
+
* installed never renders, because the page dispatches only the keys its
|
|
32
|
+
* own bundles declare.
|
|
33
|
+
*/
|
|
34
|
+
var BUNDLE_NAMES = ["@logictan/dsh-plugins-all", "@logictan/dsh-browser-agent"];
|
|
35
|
+
|
|
36
|
+
var copy = {
|
|
37
|
+
summary: "浏览器 Agent 的参数",
|
|
38
|
+
heading: "浏览器 Agent",
|
|
39
|
+
intro:
|
|
40
|
+
"插件直连本机 Chrome 的调试端口:TypeSafe 决定动作,DSH 自己的模型填写输入框的值。留空表示继承默认值。",
|
|
41
|
+
inherit: "继承默认",
|
|
42
|
+
save: "保存",
|
|
43
|
+
saving: "保存中…",
|
|
44
|
+
reset: "恢复默认",
|
|
45
|
+
overridden: "已覆盖",
|
|
46
|
+
unwritable: "当前连接不写回本机设置,无法保存。",
|
|
47
|
+
secretSet: "已设置(不回读)",
|
|
48
|
+
catalogLoading: "正在读取模型目录…",
|
|
49
|
+
catalogError: "模型目录读取失败,可切换为手填。",
|
|
50
|
+
catalogPartial: "部分 provider 读取失败,列表可能不完整。",
|
|
51
|
+
catalogEmpty: "目录里没有模型,可切换为手填。",
|
|
52
|
+
effortNone: "该模型没有 reasoning effort",
|
|
53
|
+
manual: "手填",
|
|
54
|
+
picker: "从目录选择",
|
|
55
|
+
routeMissing: "已存的路由不在当前目录中(provider 读取失败或模型已下线),已切换为手填。",
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Editable fields, in render order. `kind` picks the control.
|
|
60
|
+
*
|
|
61
|
+
* A `secret` field is never pre-filled: the Host strips secret values from
|
|
62
|
+
* every read, so the control can only accept a new one and the placeholder
|
|
63
|
+
* reports whether one is already set.
|
|
64
|
+
*/
|
|
65
|
+
var FIELDS = [
|
|
66
|
+
{
|
|
67
|
+
key: "typesafeApiKey",
|
|
68
|
+
label: "TypeSafe Key",
|
|
69
|
+
kind: "secret",
|
|
70
|
+
hint: "来自 https://console.typesafe.ai/keys。role('secret'):只存本机,随配置同步被剥离,换设备需重录。",
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
key: "typesafeEndpoint",
|
|
74
|
+
label: "TypeSafe endpoint",
|
|
75
|
+
kind: "text",
|
|
76
|
+
hint: "默认 https://api.typesafe.ai/v1/systemone。",
|
|
77
|
+
},
|
|
78
|
+
{ key: "typesafeModel", label: "TypeSafe model", kind: "text", hint: "默认 jev-latest。" },
|
|
79
|
+
{
|
|
80
|
+
key: "cdpEndpoint",
|
|
81
|
+
label: "Chrome 调试端口",
|
|
82
|
+
kind: "text",
|
|
83
|
+
hint: "默认 http://127.0.0.1:9222。Chrome 必须用专用 --user-data-dir 启动(见 README)。",
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
key: "maxSteps",
|
|
87
|
+
label: "步数上限",
|
|
88
|
+
kind: "number",
|
|
89
|
+
min: 1,
|
|
90
|
+
step: 1,
|
|
91
|
+
hint: "一次任务最多执行多少个浏览器动作,默认 60。",
|
|
92
|
+
},
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
/** The three TYPE_TEXT route fields, rendered together below the catalog. */
|
|
96
|
+
var ROUTE_KEYS = ["textProvider", "textModel", "textReasoningEffort"];
|
|
97
|
+
|
|
98
|
+
var wrapStyle = {
|
|
99
|
+
display: "flex",
|
|
100
|
+
flexDirection: "column",
|
|
101
|
+
gap: "12px",
|
|
102
|
+
maxWidth: "620px",
|
|
103
|
+
color: "var(--dsw-alias-label-primary)",
|
|
104
|
+
fontSize: "13px",
|
|
105
|
+
lineHeight: 1.6,
|
|
106
|
+
};
|
|
107
|
+
var headingStyle = { fontSize: "14px", fontWeight: 600 };
|
|
108
|
+
var hintStyle = { color: "var(--dsw-alias-label-tertiary)" };
|
|
109
|
+
var rowStyle = { display: "flex", alignItems: "center", gap: "8px", flexWrap: "wrap" };
|
|
110
|
+
var fieldStyle = { display: "flex", alignItems: "center", gap: "8px", flexWrap: "wrap" };
|
|
111
|
+
var labelStyle = { minWidth: "140px" };
|
|
112
|
+
var controlStyle = {
|
|
113
|
+
padding: "6px 8px",
|
|
114
|
+
border: "1px solid var(--dsw-alias-border-l2)",
|
|
115
|
+
borderRadius: "6px",
|
|
116
|
+
background: "var(--dsw-alias-bg-layer-1)",
|
|
117
|
+
color: "inherit",
|
|
118
|
+
font: "inherit",
|
|
119
|
+
fontVariantNumeric: "tabular-nums",
|
|
120
|
+
};
|
|
121
|
+
var inputStyle = Object.assign({ width: "132px" }, controlStyle);
|
|
122
|
+
var textStyle = Object.assign({ width: "260px" }, controlStyle);
|
|
123
|
+
var selectStyle = Object.assign({ minWidth: "200px" }, controlStyle);
|
|
124
|
+
var buttonStyle = {
|
|
125
|
+
padding: "6px 12px",
|
|
126
|
+
border: "1px solid var(--dsw-alias-border-l2)",
|
|
127
|
+
borderRadius: "6px",
|
|
128
|
+
background: "var(--dsw-alias-bg-layer-1)",
|
|
129
|
+
color: "inherit",
|
|
130
|
+
font: "inherit",
|
|
131
|
+
cursor: "pointer",
|
|
132
|
+
};
|
|
133
|
+
var primaryButtonStyle = Object.assign({}, buttonStyle, {
|
|
134
|
+
border: "1px solid var(--dsw-alias-button-primary-fill)",
|
|
135
|
+
background: "var(--dsw-alias-button-primary-fill)",
|
|
136
|
+
color: "var(--dsw-alias-label-primary-foreground)",
|
|
137
|
+
});
|
|
138
|
+
var errorStyle = { color: "var(--dsw-alias-state-error-primary)" };
|
|
139
|
+
var badgeStyle = {
|
|
140
|
+
padding: "1px 6px",
|
|
141
|
+
borderRadius: "999px",
|
|
142
|
+
background: "var(--dsw-alias-bg-layer-2, rgba(0, 0, 0, 0.08))",
|
|
143
|
+
color: "var(--dsw-alias-label-secondary)",
|
|
144
|
+
fontSize: "12px",
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
function has(object, key) {
|
|
148
|
+
return Object.prototype.hasOwnProperty.call(object, key);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function objectLayer(snapshot, which) {
|
|
152
|
+
var layer = snapshot && snapshot[which];
|
|
153
|
+
return layer !== null && typeof layer === "object" ? layer : {};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** The schema-resolved section the Host last accepted. */
|
|
157
|
+
function accepted(snapshot) {
|
|
158
|
+
return objectLayer(snapshot, "value");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** The raw user layer; a key's presence here is what marks it overridden. */
|
|
162
|
+
function userLayer(snapshot) {
|
|
163
|
+
return objectLayer(snapshot, "user");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function overridden(snapshot, key) {
|
|
167
|
+
return has(userLayer(snapshot), key);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** The effective value for a field, as the string a control holds. */
|
|
171
|
+
function stored(snapshot, key) {
|
|
172
|
+
var value = accepted(snapshot)[key];
|
|
173
|
+
return value === undefined || value === null ? "" : String(value);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Parse a draft string into the value to store.
|
|
178
|
+
* @throws {Error} when the draft does not satisfy the field's schema.
|
|
179
|
+
*/
|
|
180
|
+
function parse(field, raw) {
|
|
181
|
+
if (field.kind === "text" || field.kind === "secret") return raw;
|
|
182
|
+
var value = Number(raw);
|
|
183
|
+
if (!isFinite(value)) throw new Error(field.label + "必须是数字。");
|
|
184
|
+
if (field.step === 1 && !Number.isInteger(value)) throw new Error(field.label + "必须是整数。");
|
|
185
|
+
if (field.min !== undefined && value < field.min) throw new Error(field.label + "不能小于 " + field.min + "。");
|
|
186
|
+
if (field.max !== undefined && value > field.max) throw new Error(field.label + "不能大于 " + field.max + "。");
|
|
187
|
+
return value;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Load the live model directory once, and reload when the Host announces a
|
|
192
|
+
* change to the adapters or the settings document.
|
|
193
|
+
*
|
|
194
|
+
* The directory is the ONLY source for the route: provider ids, model ids
|
|
195
|
+
* and each model's reasoning efforts all come from the same envelope, so a
|
|
196
|
+
* second source would drift. A failed load must not disable the card — the
|
|
197
|
+
* route can always be typed by hand — so a failure only changes the status
|
|
198
|
+
* line and the manual fallback.
|
|
199
|
+
*/
|
|
200
|
+
function useModelCatalog(ctx) {
|
|
201
|
+
var state = React.useState({ status: "loading", groups: [], failures: 0 });
|
|
202
|
+
var value = state[0];
|
|
203
|
+
var setValue = state[1];
|
|
204
|
+
var generation = React.useRef(0);
|
|
205
|
+
|
|
206
|
+
var load = React.useCallback(function () {
|
|
207
|
+
var mine = ++generation.current;
|
|
208
|
+
setValue(function (previous) {
|
|
209
|
+
return { status: "loading", groups: previous.groups, failures: previous.failures };
|
|
210
|
+
});
|
|
211
|
+
Promise.resolve()
|
|
212
|
+
.then(function () {
|
|
213
|
+
return ctx.remote.session.modelCatalog();
|
|
214
|
+
})
|
|
215
|
+
.then(
|
|
216
|
+
function (response) {
|
|
217
|
+
if (mine !== generation.current) return;
|
|
218
|
+
if (response && response.ok) {
|
|
219
|
+
setValue({
|
|
220
|
+
status: "ready",
|
|
221
|
+
groups: response.value.groups || [],
|
|
222
|
+
failures: (response.value.failures || []).length,
|
|
223
|
+
});
|
|
224
|
+
} else {
|
|
225
|
+
setValue({ status: "error", groups: [], failures: 0 });
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
function () {
|
|
229
|
+
if (mine !== generation.current) return;
|
|
230
|
+
setValue({ status: "error", groups: [], failures: 0 });
|
|
231
|
+
},
|
|
232
|
+
);
|
|
233
|
+
}, [ctx]);
|
|
234
|
+
|
|
235
|
+
React.useEffect(
|
|
236
|
+
function () {
|
|
237
|
+
load();
|
|
238
|
+
var offAdapters = ctx.remote.$on("llm/adapters-updated", load);
|
|
239
|
+
var offSettings = ctx.remote.$on("settings/document-updated", load);
|
|
240
|
+
return function () {
|
|
241
|
+
offAdapters();
|
|
242
|
+
offSettings();
|
|
243
|
+
};
|
|
244
|
+
},
|
|
245
|
+
[ctx, load],
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
return { value: value, reload: load };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Which role('secret') fields currently hold a value.
|
|
253
|
+
*
|
|
254
|
+
* A secret is stripped from BOTH the resolved value and the raw user layer
|
|
255
|
+
* before any read crosses the wire, so neither layer can tell the card
|
|
256
|
+
* whether a key is set — overridden() is false for a saved key, which would
|
|
257
|
+
* make the reset button skip it and silently leave it stored. The describe
|
|
258
|
+
* sidecar is the one read that carries the fact: a { path, set } entry per
|
|
259
|
+
* schema-declared secret, with the value itself never sent. The bound
|
|
260
|
+
* namespace scope does not project the sidecar, so this reads the shared
|
|
261
|
+
* describe mirror directly.
|
|
262
|
+
*
|
|
263
|
+
* @returns a map from field key to whether that secret currently has a value.
|
|
264
|
+
*/
|
|
265
|
+
function useSecretStatus(ctx) {
|
|
266
|
+
var mirror = ctx.settingsScope.describe();
|
|
267
|
+
var subscribe = React.useCallback(
|
|
268
|
+
function (onChange) {
|
|
269
|
+
return mirror.subscribe(onChange);
|
|
270
|
+
},
|
|
271
|
+
[mirror],
|
|
272
|
+
);
|
|
273
|
+
var getSnapshot = React.useCallback(
|
|
274
|
+
function () {
|
|
275
|
+
return mirror.getSnapshot();
|
|
276
|
+
},
|
|
277
|
+
[mirror],
|
|
278
|
+
);
|
|
279
|
+
React.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
280
|
+
|
|
281
|
+
var row = mirror.namespace(NS);
|
|
282
|
+
var set = {};
|
|
283
|
+
((row && row.secrets) || []).forEach(function (secret) {
|
|
284
|
+
if (secret.path.length === 1 && secret.set) set[secret.path[0]] = true;
|
|
285
|
+
});
|
|
286
|
+
return set;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Every model in the catalog, flattened with its provider. */
|
|
290
|
+
function modelsOf(groups) {
|
|
291
|
+
var models = [];
|
|
292
|
+
groups.forEach(function (group) {
|
|
293
|
+
(group.models || []).forEach(function (model) {
|
|
294
|
+
models.push({ provider: group.id, model: model });
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
return models;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** The catalog entry matching a stored route, when it is still present. */
|
|
301
|
+
function matchModel(groups, provider, model) {
|
|
302
|
+
var found = null;
|
|
303
|
+
groups.forEach(function (group) {
|
|
304
|
+
if (group.id !== provider) return;
|
|
305
|
+
(group.models || []).forEach(function (entry) {
|
|
306
|
+
if (entry.id === model) found = entry;
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
return found;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* The row's configuration card.
|
|
314
|
+
*
|
|
315
|
+
* `scope` is the settings namespace bound on this plugin's own fiber by
|
|
316
|
+
* `apply`; `ctx` supplies the live model directory.
|
|
317
|
+
*/
|
|
318
|
+
function BrowserAgentSettingsCard(props) {
|
|
319
|
+
var scope = props.scope;
|
|
320
|
+
var ctx = props.ctx;
|
|
321
|
+
|
|
322
|
+
var subscribe = React.useCallback(
|
|
323
|
+
function (onChange) {
|
|
324
|
+
return scope.subscribe(onChange);
|
|
325
|
+
},
|
|
326
|
+
[scope],
|
|
327
|
+
);
|
|
328
|
+
var getSnapshot = React.useCallback(
|
|
329
|
+
function () {
|
|
330
|
+
return scope.getSnapshot();
|
|
331
|
+
},
|
|
332
|
+
[scope],
|
|
333
|
+
);
|
|
334
|
+
var snapshot = React.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
335
|
+
|
|
336
|
+
var catalog = useModelCatalog(ctx);
|
|
337
|
+
var secretSet = useSecretStatus(ctx);
|
|
338
|
+
|
|
339
|
+
var draftState = React.useState({});
|
|
340
|
+
var draft = draftState[0];
|
|
341
|
+
var setDraft = draftState[1];
|
|
342
|
+
var busyState = React.useState(false);
|
|
343
|
+
var busy = busyState[0];
|
|
344
|
+
var setBusy = busyState[1];
|
|
345
|
+
var errorState = React.useState(null);
|
|
346
|
+
var error = errorState[0];
|
|
347
|
+
var setError = errorState[1];
|
|
348
|
+
/** Whether the user chose to type the route instead of picking it. */
|
|
349
|
+
var manualState = React.useState(false);
|
|
350
|
+
var manual = manualState[0];
|
|
351
|
+
var setManual = manualState[1];
|
|
352
|
+
|
|
353
|
+
if (props.view === "summary") return copy.summary;
|
|
354
|
+
|
|
355
|
+
var writable = snapshot.writable === true;
|
|
356
|
+
var current = accepted(snapshot);
|
|
357
|
+
|
|
358
|
+
/** Record one field's draft, leaving the others untouched. */
|
|
359
|
+
function edit(key, value) {
|
|
360
|
+
setDraft(function (previous) {
|
|
361
|
+
var next = {};
|
|
362
|
+
for (var name in previous) if (has(previous, name)) next[name] = previous[name];
|
|
363
|
+
next[key] = value;
|
|
364
|
+
return next;
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Stage several fields at once.
|
|
370
|
+
*
|
|
371
|
+
* Picking a provider clears the model and the effort, because both belong
|
|
372
|
+
* to the previous provider and would otherwise be saved as a route the
|
|
373
|
+
* Host cannot resolve.
|
|
374
|
+
*/
|
|
375
|
+
function editMany(patch) {
|
|
376
|
+
setDraft(function (previous) {
|
|
377
|
+
var next = {};
|
|
378
|
+
for (var name in previous) if (has(previous, name)) next[name] = previous[name];
|
|
379
|
+
for (var key in patch) if (has(patch, key)) next[key] = patch[key];
|
|
380
|
+
return next;
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
var allFields = FIELDS.concat([
|
|
385
|
+
{ key: "textProvider", label: "填空 provider", kind: "text" },
|
|
386
|
+
{ key: "textModel", label: "填空 model", kind: "text" },
|
|
387
|
+
{ key: "textReasoningEffort", label: "填空 reasoning effort", kind: "text" },
|
|
388
|
+
]);
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Whether a field currently carries a user override.
|
|
392
|
+
*
|
|
393
|
+
* A secret is the exception to reading the user layer: it is redacted from
|
|
394
|
+
* that layer, so its presence comes from the describe sidecar instead.
|
|
395
|
+
*/
|
|
396
|
+
function isOverridden(field) {
|
|
397
|
+
return field.kind === "secret" ? secretSet[field.key] === true : overridden(snapshot, field.key);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Every field whose draft no longer matches what is stored. A draft that
|
|
401
|
+
// is an empty string means "clear the override", which is a real edit
|
|
402
|
+
// whenever a value is currently in effect.
|
|
403
|
+
var edits = [];
|
|
404
|
+
var invalid = null;
|
|
405
|
+
allFields.forEach(function (field) {
|
|
406
|
+
if (!has(draft, field.key)) return;
|
|
407
|
+
var raw = draft[field.key];
|
|
408
|
+
if (raw === "") {
|
|
409
|
+
if (isOverridden(field)) edits.push({ field: field, raw: raw });
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
var value;
|
|
413
|
+
try {
|
|
414
|
+
value = parse(field, raw);
|
|
415
|
+
} catch (cause) {
|
|
416
|
+
if (invalid === null) invalid = String((cause && cause.message) || cause);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
if (String(value) !== String(current[field.key])) edits.push({ field: field, raw: raw, value: value });
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
var anyOverridden = allFields.some(isOverridden);
|
|
423
|
+
var dirty = edits.length > 0;
|
|
424
|
+
var blocked = busy || invalid !== null;
|
|
425
|
+
|
|
426
|
+
/** Persist every edit, in order, one write at a time. */
|
|
427
|
+
function save() {
|
|
428
|
+
if (!dirty || blocked) return;
|
|
429
|
+
setError(null);
|
|
430
|
+
setBusy(true);
|
|
431
|
+
var chain = Promise.resolve();
|
|
432
|
+
edits.forEach(function (item) {
|
|
433
|
+
chain = chain.then(function () {
|
|
434
|
+
return item.raw === "" ? scope.unset(item.field.key) : scope.set(item.field.key, item.value);
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
chain.then(
|
|
438
|
+
function () {
|
|
439
|
+
setBusy(false);
|
|
440
|
+
setDraft({});
|
|
441
|
+
},
|
|
442
|
+
function (cause) {
|
|
443
|
+
setBusy(false);
|
|
444
|
+
setError(String((cause && cause.message) || cause));
|
|
445
|
+
},
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/** Drop every override so the fields re-inherit the composition values. */
|
|
450
|
+
function reset() {
|
|
451
|
+
if (busy) return;
|
|
452
|
+
var keys = allFields
|
|
453
|
+
.filter(function (field) {
|
|
454
|
+
return isOverridden(field);
|
|
455
|
+
})
|
|
456
|
+
.map(function (field) {
|
|
457
|
+
return field.key;
|
|
458
|
+
});
|
|
459
|
+
if (keys.length === 0) return;
|
|
460
|
+
setError(null);
|
|
461
|
+
setDraft({});
|
|
462
|
+
setBusy(true);
|
|
463
|
+
var chain = Promise.resolve();
|
|
464
|
+
keys.forEach(function (key) {
|
|
465
|
+
chain = chain.then(function () {
|
|
466
|
+
return scope.unset(key);
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
chain.then(
|
|
470
|
+
function () {
|
|
471
|
+
setBusy(false);
|
|
472
|
+
},
|
|
473
|
+
function (cause) {
|
|
474
|
+
setBusy(false);
|
|
475
|
+
setError(String((cause && cause.message) || cause));
|
|
476
|
+
},
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/** The draft for a key, or the stored value. */
|
|
481
|
+
function rawOf(key) {
|
|
482
|
+
return has(draft, key) ? draft[key] : stored(snapshot, key);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
var children = [
|
|
486
|
+
React.createElement("div", { key: "heading", style: headingStyle }, copy.heading),
|
|
487
|
+
React.createElement("div", { key: "intro" }, copy.intro),
|
|
488
|
+
];
|
|
489
|
+
|
|
490
|
+
FIELDS.forEach(function (field) {
|
|
491
|
+
var disabled = !writable || busy;
|
|
492
|
+
var control;
|
|
493
|
+
if (field.kind === "secret") {
|
|
494
|
+
// Never pre-filled: the Host strips secret values from every read, so
|
|
495
|
+
// the control can only accept a new one. "Is one already set" comes
|
|
496
|
+
// from the describe sidecar, never from the redacted layers.
|
|
497
|
+
control = React.createElement("input", {
|
|
498
|
+
type: "password",
|
|
499
|
+
style: textStyle,
|
|
500
|
+
value: has(draft, field.key) ? draft[field.key] : "",
|
|
501
|
+
disabled: disabled,
|
|
502
|
+
placeholder: secretSet[field.key] === true ? copy.secretSet : copy.inherit,
|
|
503
|
+
autoComplete: "off",
|
|
504
|
+
"aria-label": field.label,
|
|
505
|
+
onChange: function (event) {
|
|
506
|
+
edit(field.key, event.target.value);
|
|
507
|
+
},
|
|
508
|
+
onKeyDown: function (event) {
|
|
509
|
+
if (event.key === "Enter") save();
|
|
510
|
+
},
|
|
511
|
+
});
|
|
512
|
+
} else if (field.kind === "number") {
|
|
513
|
+
control = React.createElement("input", {
|
|
514
|
+
type: "number",
|
|
515
|
+
min: field.min,
|
|
516
|
+
max: field.max,
|
|
517
|
+
step: field.step,
|
|
518
|
+
style: inputStyle,
|
|
519
|
+
value: rawOf(field.key),
|
|
520
|
+
disabled: disabled,
|
|
521
|
+
placeholder: copy.inherit,
|
|
522
|
+
"aria-label": field.label,
|
|
523
|
+
onChange: function (event) {
|
|
524
|
+
edit(field.key, event.target.value);
|
|
525
|
+
},
|
|
526
|
+
onKeyDown: function (event) {
|
|
527
|
+
if (event.key === "Enter") save();
|
|
528
|
+
},
|
|
529
|
+
});
|
|
530
|
+
} else {
|
|
531
|
+
control = React.createElement("input", {
|
|
532
|
+
type: "text",
|
|
533
|
+
style: textStyle,
|
|
534
|
+
value: rawOf(field.key),
|
|
535
|
+
disabled: disabled,
|
|
536
|
+
placeholder: copy.inherit,
|
|
537
|
+
"aria-label": field.label,
|
|
538
|
+
onChange: function (event) {
|
|
539
|
+
edit(field.key, event.target.value);
|
|
540
|
+
},
|
|
541
|
+
onKeyDown: function (event) {
|
|
542
|
+
if (event.key === "Enter") save();
|
|
543
|
+
},
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
children.push(
|
|
548
|
+
React.createElement(
|
|
549
|
+
"div",
|
|
550
|
+
{ key: field.key, style: fieldStyle },
|
|
551
|
+
React.createElement("span", { style: labelStyle }, field.label),
|
|
552
|
+
control,
|
|
553
|
+
isOverridden(field) ? React.createElement("span", { style: badgeStyle }, copy.overridden) : null,
|
|
554
|
+
),
|
|
555
|
+
);
|
|
556
|
+
if (field.hint) {
|
|
557
|
+
children.push(React.createElement("div", { key: field.key + "-hint", style: hintStyle }, field.hint));
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
children.push(
|
|
562
|
+
React.createElement("hr", {
|
|
563
|
+
key: "route-sep",
|
|
564
|
+
style: { border: "none", borderTop: "1px solid var(--dsw-alias-border-l2)", margin: "4px 0" },
|
|
565
|
+
}),
|
|
566
|
+
);
|
|
567
|
+
children.push(React.createElement("div", { key: "route-heading", style: headingStyle }, "TYPE_TEXT 填空路由"));
|
|
568
|
+
children.push(
|
|
569
|
+
React.createElement(
|
|
570
|
+
"div",
|
|
571
|
+
{ key: "route-hint", style: hintStyle },
|
|
572
|
+
"输入框的值由 DSH 自己的模型生成。全空 = 用会话当前模型。",
|
|
573
|
+
),
|
|
574
|
+
);
|
|
575
|
+
|
|
576
|
+
var models = modelsOf(catalog.value.groups);
|
|
577
|
+
var provider = rawOf("textProvider");
|
|
578
|
+
var model = rawOf("textModel");
|
|
579
|
+
var selected = matchModel(catalog.value.groups, provider, model);
|
|
580
|
+
var providers = [];
|
|
581
|
+
catalog.value.groups.forEach(function (group) {
|
|
582
|
+
if (providers.indexOf(group.id) === -1) providers.push(group.id);
|
|
583
|
+
});
|
|
584
|
+
var modelsForProvider = models.filter(function (entry) {
|
|
585
|
+
return entry.provider === provider;
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
// The dropdown is usable only when the catalog loaded AND a provider is
|
|
589
|
+
// available. A stored route the catalog no longer offers (a provider that
|
|
590
|
+
// failed to load, or a model removed upstream) is never silently
|
|
591
|
+
// rewritten: the card falls back to manual entry and says so.
|
|
592
|
+
var pickable = catalog.value.status === "ready" && providers.length > 0;
|
|
593
|
+
var usePicker = pickable && !manual;
|
|
594
|
+
|
|
595
|
+
if (catalog.value.status === "loading") {
|
|
596
|
+
children.push(React.createElement("div", { key: "catalog-status", style: hintStyle }, copy.catalogLoading));
|
|
597
|
+
} else if (catalog.value.status === "error") {
|
|
598
|
+
children.push(React.createElement("div", { key: "catalog-status", style: errorStyle }, copy.catalogError));
|
|
599
|
+
} else if (models.length === 0) {
|
|
600
|
+
children.push(React.createElement("div", { key: "catalog-status", style: hintStyle }, copy.catalogEmpty));
|
|
601
|
+
} else if (catalog.value.failures > 0) {
|
|
602
|
+
children.push(React.createElement("div", { key: "catalog-status", style: hintStyle }, copy.catalogPartial));
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
if (usePicker) {
|
|
606
|
+
children.push(
|
|
607
|
+
React.createElement(
|
|
608
|
+
"div",
|
|
609
|
+
{ key: "route-provider", style: fieldStyle },
|
|
610
|
+
React.createElement("span", { style: labelStyle }, "provider"),
|
|
611
|
+
React.createElement(
|
|
612
|
+
"select",
|
|
613
|
+
{
|
|
614
|
+
style: selectStyle,
|
|
615
|
+
value: provider,
|
|
616
|
+
disabled: !writable || busy,
|
|
617
|
+
"aria-label": "填空 provider",
|
|
618
|
+
onChange: function (event) {
|
|
619
|
+
// The previous model and effort belong to the previous
|
|
620
|
+
// provider, so they are cleared together with it.
|
|
621
|
+
editMany({ textProvider: event.target.value, textModel: "", textReasoningEffort: "" });
|
|
622
|
+
},
|
|
623
|
+
},
|
|
624
|
+
[React.createElement("option", { key: "", value: "" }, copy.inherit)].concat(
|
|
625
|
+
providers.map(function (id) {
|
|
626
|
+
return React.createElement("option", { key: id, value: id }, id);
|
|
627
|
+
}),
|
|
628
|
+
),
|
|
629
|
+
),
|
|
630
|
+
overridden(snapshot, "textProvider")
|
|
631
|
+
? React.createElement("span", { style: badgeStyle }, copy.overridden)
|
|
632
|
+
: null,
|
|
633
|
+
),
|
|
634
|
+
);
|
|
635
|
+
children.push(
|
|
636
|
+
React.createElement(
|
|
637
|
+
"div",
|
|
638
|
+
{ key: "route-model", style: fieldStyle },
|
|
639
|
+
React.createElement("span", { style: labelStyle }, "model"),
|
|
640
|
+
React.createElement(
|
|
641
|
+
"select",
|
|
642
|
+
{
|
|
643
|
+
style: selectStyle,
|
|
644
|
+
value: selected === null ? "" : model,
|
|
645
|
+
disabled: !writable || busy || provider === "",
|
|
646
|
+
"aria-label": "填空 model",
|
|
647
|
+
onChange: function (event) {
|
|
648
|
+
editMany({ textModel: event.target.value, textReasoningEffort: "" });
|
|
649
|
+
},
|
|
650
|
+
},
|
|
651
|
+
[React.createElement("option", { key: "", value: "" }, copy.inherit)].concat(
|
|
652
|
+
modelsForProvider.map(function (entry) {
|
|
653
|
+
return React.createElement(
|
|
654
|
+
"option",
|
|
655
|
+
{ key: entry.model.id, value: entry.model.id },
|
|
656
|
+
entry.model.name || entry.model.id,
|
|
657
|
+
);
|
|
658
|
+
}),
|
|
659
|
+
),
|
|
660
|
+
),
|
|
661
|
+
overridden(snapshot, "textModel") ? React.createElement("span", { style: badgeStyle }, copy.overridden) : null,
|
|
662
|
+
),
|
|
663
|
+
);
|
|
664
|
+
|
|
665
|
+
var reasoning = (selected && selected.reasoning) || null;
|
|
666
|
+
var effortOptions = reasoning ? reasoning.efforts || [] : [];
|
|
667
|
+
if (effortOptions.length > 0) {
|
|
668
|
+
var effortValue = rawOf("textReasoningEffort") || reasoning.defaultEffort || "";
|
|
669
|
+
children.push(
|
|
670
|
+
React.createElement(
|
|
671
|
+
"div",
|
|
672
|
+
{ key: "route-effort", style: fieldStyle },
|
|
673
|
+
React.createElement("span", { style: labelStyle }, "reasoning effort"),
|
|
674
|
+
React.createElement(
|
|
675
|
+
"select",
|
|
676
|
+
{
|
|
677
|
+
style: selectStyle,
|
|
678
|
+
value: effortValue,
|
|
679
|
+
disabled: !writable || busy,
|
|
680
|
+
"aria-label": "填空 reasoning effort",
|
|
681
|
+
onChange: function (event) {
|
|
682
|
+
edit("textReasoningEffort", event.target.value);
|
|
683
|
+
},
|
|
684
|
+
},
|
|
685
|
+
[React.createElement("option", { key: "", value: "" }, copy.inherit)].concat(
|
|
686
|
+
effortOptions.map(function (effort) {
|
|
687
|
+
return React.createElement("option", { key: effort.id, value: effort.id }, effort.name || effort.id);
|
|
688
|
+
}),
|
|
689
|
+
),
|
|
690
|
+
),
|
|
691
|
+
overridden(snapshot, "textReasoningEffort")
|
|
692
|
+
? React.createElement("span", { style: badgeStyle }, copy.overridden)
|
|
693
|
+
: null,
|
|
694
|
+
),
|
|
695
|
+
);
|
|
696
|
+
} else if (reasoning) {
|
|
697
|
+
children.push(
|
|
698
|
+
React.createElement(
|
|
699
|
+
"div",
|
|
700
|
+
{ key: "route-effort", style: hintStyle },
|
|
701
|
+
"reasoning effort:" + copy.effortNone,
|
|
702
|
+
),
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
if (selected === null && (provider !== "" || model !== "")) {
|
|
707
|
+
children.push(React.createElement("div", { key: "route-missing", style: hintStyle }, copy.routeMissing));
|
|
708
|
+
}
|
|
709
|
+
} else {
|
|
710
|
+
// Manual entry, also the fallback when the catalog failed: the card must
|
|
711
|
+
// stay usable when a provider is unreachable.
|
|
712
|
+
ROUTE_KEYS.forEach(function (key) {
|
|
713
|
+
var label = key === "textProvider" ? "provider" : key === "textModel" ? "model" : "reasoning effort";
|
|
714
|
+
children.push(
|
|
715
|
+
React.createElement(
|
|
716
|
+
"div",
|
|
717
|
+
{ key: "route-" + key, style: fieldStyle },
|
|
718
|
+
React.createElement("span", { style: labelStyle }, label),
|
|
719
|
+
React.createElement("input", {
|
|
720
|
+
type: "text",
|
|
721
|
+
style: textStyle,
|
|
722
|
+
value: rawOf(key),
|
|
723
|
+
disabled: !writable || busy,
|
|
724
|
+
placeholder: copy.inherit,
|
|
725
|
+
"aria-label": "填空 " + label,
|
|
726
|
+
onChange: function (event) {
|
|
727
|
+
edit(key, event.target.value);
|
|
728
|
+
},
|
|
729
|
+
onKeyDown: function (event) {
|
|
730
|
+
if (event.key === "Enter") save();
|
|
731
|
+
},
|
|
732
|
+
}),
|
|
733
|
+
overridden(snapshot, key) ? React.createElement("span", { style: badgeStyle }, copy.overridden) : null,
|
|
734
|
+
),
|
|
735
|
+
);
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
if (pickable) {
|
|
740
|
+
children.push(
|
|
741
|
+
React.createElement(
|
|
742
|
+
"div",
|
|
743
|
+
{ key: "route-mode", style: rowStyle },
|
|
744
|
+
React.createElement(
|
|
745
|
+
"button",
|
|
746
|
+
{
|
|
747
|
+
type: "button",
|
|
748
|
+
style: buttonStyle,
|
|
749
|
+
onClick: function () {
|
|
750
|
+
setManual(!manual);
|
|
751
|
+
},
|
|
752
|
+
},
|
|
753
|
+
manual ? copy.picker : copy.manual,
|
|
754
|
+
),
|
|
755
|
+
),
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
if (invalid !== null) children.push(React.createElement("div", { key: "invalid", style: errorStyle }, invalid));
|
|
760
|
+
if (error !== null) children.push(React.createElement("div", { key: "error", style: errorStyle }, error));
|
|
761
|
+
if (!writable && snapshot.status === "ready") {
|
|
762
|
+
children.push(React.createElement("div", { key: "unwritable", style: errorStyle }, copy.unwritable));
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
children.push(
|
|
766
|
+
React.createElement(
|
|
767
|
+
"div",
|
|
768
|
+
{ key: "actions", style: rowStyle },
|
|
769
|
+
React.createElement(
|
|
770
|
+
"button",
|
|
771
|
+
{
|
|
772
|
+
type: "button",
|
|
773
|
+
style: dirty && writable && !blocked ? primaryButtonStyle : buttonStyle,
|
|
774
|
+
disabled: !dirty || !writable || blocked,
|
|
775
|
+
onClick: save,
|
|
776
|
+
},
|
|
777
|
+
busy ? copy.saving : copy.save,
|
|
778
|
+
),
|
|
779
|
+
React.createElement(
|
|
780
|
+
"button",
|
|
781
|
+
{
|
|
782
|
+
type: "button",
|
|
783
|
+
style: buttonStyle,
|
|
784
|
+
disabled: !writable || busy || !anyOverridden,
|
|
785
|
+
onClick: reset,
|
|
786
|
+
},
|
|
787
|
+
copy.reset,
|
|
788
|
+
),
|
|
789
|
+
),
|
|
790
|
+
);
|
|
791
|
+
|
|
792
|
+
return React.createElement("div", { style: wrapStyle }, children);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* Client plugin body: bind the settings namespace and register the card.
|
|
797
|
+
* @param ctx - client root context.
|
|
798
|
+
*/
|
|
799
|
+
function apply(ctx) {
|
|
800
|
+
var scope = ctx.settingsScope.bind({ namespace: NS });
|
|
801
|
+
BUNDLE_NAMES.forEach(function (bundle) {
|
|
802
|
+
ctx.slots.inject("plugins.row.config", function () {
|
|
803
|
+
return ctx.slots.register(
|
|
804
|
+
{
|
|
805
|
+
name: "plugins.row.config",
|
|
806
|
+
key: bundle + "#" + ROW_ID,
|
|
807
|
+
inject: function () {
|
|
808
|
+
return { scope: scope, ctx: ctx };
|
|
809
|
+
},
|
|
810
|
+
},
|
|
811
|
+
BrowserAgentSettingsCard,
|
|
812
|
+
);
|
|
813
|
+
});
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
return {
|
|
818
|
+
name: "dsh-browser-agent",
|
|
819
|
+
inject: ["slots", "settingsScope", "remote", "remote.session"],
|
|
820
|
+
apply: apply,
|
|
821
|
+
};
|
|
822
|
+
},
|
|
823
|
+
});
|