dsh-plugin-model-tuning 2.0.2 → 3.0.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 -21
- package/README.md +83 -219
- package/client.js +463 -1020
- package/cordis.patch.yml +10 -19
- package/index.js +20 -178
- package/package.json +5 -11
package/client.js
CHANGED
|
@@ -5,7 +5,6 @@ window.__ModuleLoader__.load({
|
|
|
5
5
|
var exports = module.exports;
|
|
6
6
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
7
|
const React = require("react");
|
|
8
|
-
|
|
9
8
|
const CSS = `
|
|
10
9
|
.mcfg{display:flex;flex-direction:column;gap:16px;padding:4px 0 24px;color:inherit}
|
|
11
10
|
.mcfg-head{display:flex;flex-direction:column;gap:4px}
|
|
@@ -44,8 +43,8 @@ window.__ModuleLoader__.load({
|
|
|
44
43
|
.mcfg-select,.mcfg-input{font-size:12.5px;padding:6px 8px;border-radius:7px;border:1px solid rgba(128,128,128,.4);background:transparent;color:inherit;width:100%;box-sizing:border-box}
|
|
45
44
|
.mcfg-custom{width:100%}
|
|
46
45
|
.mcfg-input:disabled,.mcfg-select:disabled{opacity:.5;cursor:not-allowed}
|
|
47
|
-
/*
|
|
48
|
-
|
|
46
|
+
/* 页签样式:v3 起设置页只剩「绘图」一页,页签控件不再使用,样式保留给
|
|
47
|
+
未来多页布局,不影响当前渲染。 */
|
|
49
48
|
.mcfg-tabs{display:flex;gap:6px;flex-wrap:wrap}
|
|
50
49
|
.mcfg-tab{font-size:12.5px;padding:6px 14px;border-radius:8px;border:1px solid rgba(128,128,128,.35);background:transparent;color:inherit;cursor:pointer}
|
|
51
50
|
.mcfg-tab:hover{border-color:rgba(128,128,128,.6)}
|
|
@@ -79,6 +78,17 @@ window.__ModuleLoader__.load({
|
|
|
79
78
|
.mcfg-card p{overflow-wrap:anywhere;font-size:12px}
|
|
80
79
|
.mcfg-card a{display:inline-block;text-decoration:none}
|
|
81
80
|
.mcfg-row{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:8px 0;flex-wrap:wrap}
|
|
81
|
+
.mcfg-draw-item{display:flex;flex-direction:column;gap:3px;min-width:0;flex:1 1 220px}
|
|
82
|
+
.mcfg-draw-name{display:flex;align-items:center;gap:8px;font-size:13px;font-weight:600;overflow-wrap:anywhere}
|
|
83
|
+
.mcfg-draw-meta{font-size:11.5px;opacity:.55;overflow-wrap:anywhere}
|
|
84
|
+
.mcfg-draw-row{align-items:center;gap:10px}
|
|
85
|
+
.mcfg-draw-radio{display:flex;align-items:center;padding:0 2px}
|
|
86
|
+
.mcfg-draw-radio input{accent-color:rgb(80,140,255);width:14px;height:14px;cursor:pointer}
|
|
87
|
+
.mcfg-primary{border-color:rgba(80,140,255,.65)!important;background:rgba(80,140,255,.10)!important}
|
|
88
|
+
.mcfg-advanced{margin-top:12px;border:1px solid #8884;border-radius:10px;padding:10px 14px}
|
|
89
|
+
.mcfg-advanced>summary{cursor:pointer;font-size:12.5px;opacity:.85}
|
|
90
|
+
.mcfg-advanced[open]>summary{margin-bottom:8px}
|
|
91
|
+
.mcfg-advanced fieldset{border:0;padding:0;margin:0;min-width:0}
|
|
82
92
|
.mcfg-model-list{display:flex;flex-direction:column;gap:5px;max-height:240px;overflow:auto;margin:8px 0}
|
|
83
93
|
.mcfg-model-list button{text-align:left;overflow-wrap:anywhere}
|
|
84
94
|
.mcfg-model-list button[aria-pressed=true]{border-color:#3987d7;background:#3987d71a}
|
|
@@ -87,60 +97,17 @@ window.__ModuleLoader__.load({
|
|
|
87
97
|
`;
|
|
88
98
|
|
|
89
99
|
/**
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* every write below targets ns "model-tuning".
|
|
100
|
+
* v3: one namespace. Drawing models (the former `dsh-plugin-image-generation`
|
|
101
|
+
* Config keys) still live under `model-tuning` so existing profiles keep
|
|
102
|
+
* working; the removed title fields are ignored.
|
|
94
103
|
*/
|
|
95
104
|
const NS = "model-tuning";
|
|
96
105
|
|
|
97
|
-
/** Provenance marker on an image entry the 模型参数 tab linked. */
|
|
98
|
-
const IMAGE_SOURCE_KEY = "source";
|
|
99
|
-
const IMAGE_API = "openai-images";
|
|
100
|
-
|
|
101
106
|
const REASONING_LEVELS = ["minimal", "low", "medium", "high", "xhigh", "max"];
|
|
102
|
-
const CTX_OPTIONS = [
|
|
103
|
-
{ label: "64K", value: 64000 },
|
|
104
|
-
{ label: "128K", value: 128000 },
|
|
105
|
-
{ label: "256K", value: 256000 },
|
|
106
|
-
{ label: "512K", value: 512000 },
|
|
107
|
-
{ label: "1M", value: 1000000 },
|
|
108
|
-
{ label: "2M", value: 2000000 },
|
|
109
|
-
];
|
|
110
|
-
const MAX_OPTIONS = [
|
|
111
|
-
{ label: "4K", value: 4000 },
|
|
112
|
-
{ label: "8K", value: 8000 },
|
|
113
|
-
{ label: "16K", value: 16000 },
|
|
114
|
-
{ label: "32K", value: 32000 },
|
|
115
|
-
{ label: "64K", value: 64000 },
|
|
116
|
-
{ label: "128K", value: 128000 },
|
|
117
|
-
{ label: "256K", value: 256000 },
|
|
118
|
-
{ label: "384K", value: 384000 },
|
|
119
|
-
{ label: "512K", value: 512000 },
|
|
120
|
-
];
|
|
121
107
|
|
|
122
|
-
/**
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
* (`text` | `image`) at both the adapter and the core content block, so a
|
|
126
|
-
* provider that names no such field is shown without the control.
|
|
127
|
-
*/
|
|
128
|
-
const MODALITY_FIELD_BY_NS = {
|
|
129
|
-
"llm-pi-ai": "input",
|
|
130
|
-
"llm-deepseek": "inputModalities",
|
|
131
|
-
};
|
|
132
|
-
/**
|
|
133
|
-
* DeepSeek-side image request budgets. Its schema refuses these on a
|
|
134
|
-
* text-only model, so clearing image capability must clear them too.
|
|
135
|
-
*/
|
|
136
|
-
const DEEPSEEK_IMAGE_LIMIT_FIELDS = ["imagePixelBudget", "imageMaxBytes", "imageDetail"];
|
|
137
|
-
/**
|
|
138
|
-
* Tri-state of the control. `auto` states nothing and leaves resolution to
|
|
139
|
-
* the installed catalog entry, then the route's `defaultInput` (default
|
|
140
|
-
* `[text]`); an empty list means the same thing there, never "no modality".
|
|
141
|
-
*/
|
|
142
|
-
const MODALITY_CHOICES = ["auto", "text", "text-image"];
|
|
143
|
-
const MODALITY_LABELS = { auto: "自动", text: "纯文本", "text-image": "文本 + 图像" };
|
|
108
|
+
/** Provenance marker on a drawing entry linked from a provider model. */
|
|
109
|
+
const IMAGE_SOURCE_KEY = "source";
|
|
110
|
+
const IMAGE_API = "openai-images";
|
|
144
111
|
|
|
145
112
|
/**
|
|
146
113
|
* The compat switch that selects Anthropic's adaptive thinking request.
|
|
@@ -163,24 +130,6 @@ window.__ModuleLoader__.load({
|
|
|
163
130
|
*/
|
|
164
131
|
const ADAPTIVE_THINKING_API = "anthropic-messages";
|
|
165
132
|
|
|
166
|
-
function formatCapacity(n) {
|
|
167
|
-
if (n == null || typeof n !== "number" || !Number.isFinite(n)) return "";
|
|
168
|
-
if (n % 1000000 === 0) return String(n / 1000000) + "M";
|
|
169
|
-
if (n % 1000 === 0) return String(n / 1000) + "K";
|
|
170
|
-
return String(n);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function parseCapacity(text) {
|
|
174
|
-
if (typeof text !== "string") return null;
|
|
175
|
-
const match = text.trim().match(/^(\d+(?:\.\d+)?)\s*([kKmM]?)$/);
|
|
176
|
-
if (!match) return null;
|
|
177
|
-
const base = parseFloat(match[1]);
|
|
178
|
-
const suffix = match[2].toUpperCase();
|
|
179
|
-
const value = suffix === "K" ? base * 1000 : suffix === "M" ? base * 1000000 : base;
|
|
180
|
-
if (!Number.isInteger(value) || value <= 0) return null;
|
|
181
|
-
return value;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
133
|
function at(value, path) {
|
|
185
134
|
let current = value;
|
|
186
135
|
for (const part of path || []) {
|
|
@@ -196,18 +145,13 @@ window.__ModuleLoader__.load({
|
|
|
196
145
|
return profile && typeof profile === "object" ? profile : undefined;
|
|
197
146
|
}
|
|
198
147
|
|
|
199
|
-
function
|
|
200
|
-
const
|
|
201
|
-
return
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/** v2: the page's own namespace view — always present once the host row is up. */
|
|
205
|
-
function ownNsView(settingsView) {
|
|
206
|
-
return findNamespace(settingsView, NS);
|
|
148
|
+
function imageModelsOf(ownView) {
|
|
149
|
+
const models = ownView ? ownView.value?.models : undefined;
|
|
150
|
+
return Array.isArray(models) ? models : [];
|
|
207
151
|
}
|
|
208
152
|
|
|
209
153
|
/**
|
|
210
|
-
* Derive one
|
|
154
|
+
* Derive one drawing entry from the chat provider that already holds the
|
|
211
155
|
* endpoint and credential reference, so linking never asks the user to
|
|
212
156
|
* retype either one.
|
|
213
157
|
*/
|
|
@@ -220,14 +164,14 @@ window.__ModuleLoader__.load({
|
|
|
220
164
|
};
|
|
221
165
|
}
|
|
222
166
|
|
|
223
|
-
/** Whether an entry
|
|
167
|
+
/** Whether an entry came from linking this provider model (has provenance). */
|
|
224
168
|
function isManagedEntry(entry, modelId) {
|
|
225
169
|
const source = entry && entry[IMAGE_SOURCE_KEY];
|
|
226
170
|
if (!source || typeof source !== "object" || source.model !== modelId) return false;
|
|
227
171
|
return source.provider === undefined || source.provider === "" || typeof source.provider === "string";
|
|
228
172
|
}
|
|
229
173
|
|
|
230
|
-
/** The per-model view of the link,
|
|
174
|
+
/** The per-model view of the link: can it link, is it already linked, and where. */
|
|
231
175
|
function computeImageModelState(ownView, profile, modelId) {
|
|
232
176
|
if (!ownView) return { available: false, managed: false, hasEntry: false, entry: null };
|
|
233
177
|
const models = imageModelsOf(ownView);
|
|
@@ -242,11 +186,6 @@ window.__ModuleLoader__.load({
|
|
|
242
186
|
};
|
|
243
187
|
}
|
|
244
188
|
|
|
245
|
-
function imageModelsOf(ownView) {
|
|
246
|
-
const models = ownView ? ownView.value?.models : undefined;
|
|
247
|
-
return Array.isArray(models) ? models : [];
|
|
248
|
-
}
|
|
249
|
-
|
|
250
189
|
function normalizeImageEndpoint(value) {
|
|
251
190
|
return typeof value === "string" ? value.trim().replace(/\/+$/, "").replace(/\/images\/generations$/, "") + "/images/generations" : "";
|
|
252
191
|
}
|
|
@@ -276,14 +215,14 @@ window.__ModuleLoader__.load({
|
|
|
276
215
|
}
|
|
277
216
|
|
|
278
217
|
/**
|
|
279
|
-
* Everything the
|
|
280
|
-
* the
|
|
218
|
+
* Everything the drawing list needs for an incremental write: only the entry
|
|
219
|
+
* the provider checkbox owns plus the default-model change it implies.
|
|
281
220
|
*/
|
|
282
221
|
function planImageLink(ownView, profile, modelId, providerId) {
|
|
283
222
|
const derived = deriveImageEntry(profile, modelId);
|
|
284
223
|
const endpoint = normalizeImageEndpoint(derived.endpoint);
|
|
285
224
|
if (!acceptableImageEndpoint(endpoint)) {
|
|
286
|
-
return { models: null, reason: "该 provider 没有可用的供应商地址(
|
|
225
|
+
return { models: null, reason: "该 provider 没有可用的供应商地址(baseURL),请先在「设置 → 模型」里填写后再勾选。" };
|
|
287
226
|
}
|
|
288
227
|
const models = imageModelsOf(ownView).slice();
|
|
289
228
|
const index = models.findIndex((item) => item && typeof item === "object" && item.model === modelId);
|
|
@@ -301,35 +240,19 @@ window.__ModuleLoader__.load({
|
|
|
301
240
|
return { models: next, defaultModel };
|
|
302
241
|
}
|
|
303
242
|
|
|
304
|
-
/** Unlink removes only
|
|
243
|
+
/** Unlink removes only entries the provider checkbox created (has provenance). */
|
|
305
244
|
function planImageUnlink(ownView, modelId) {
|
|
306
245
|
const models = imageModelsOf(ownView);
|
|
307
246
|
const entry = models.find((item) => item && typeof item === "object" && item.model === modelId);
|
|
308
247
|
if (!entry) return { models: null, reason: "该模型未链接到绘图配置。" };
|
|
309
|
-
//
|
|
310
|
-
|
|
311
|
-
// deleted there directly.
|
|
312
|
-
if (!isManagedEntry(entry, modelId)) return { models: null, reason: "该配置在「绘图」页手工创建,请在那边删除。" };
|
|
248
|
+
// A hand-authored entry is not this checkbox's to remove.
|
|
249
|
+
if (!isManagedEntry(entry, modelId)) return { models: null, reason: "该配置是在「绘图」页手工创建的,请到那边删除。" };
|
|
313
250
|
const next = models.filter((item) => item !== entry);
|
|
314
251
|
const currentDefault = ownView.value?.defaultModel;
|
|
315
252
|
const defaultModel = currentDefault === entry.id ? (next[0]?.id ?? "") : currentDefault;
|
|
316
253
|
return { models: next, defaultModel };
|
|
317
254
|
}
|
|
318
255
|
|
|
319
|
-
function readModelField(nsView, settingsPath, modelId, field) {
|
|
320
|
-
if (!nsView) return undefined;
|
|
321
|
-
const profile = at(nsView.value, settingsPath);
|
|
322
|
-
if (!profile || typeof profile !== "object") return undefined;
|
|
323
|
-
const models = Array.isArray(profile.models) ? profile.models : [];
|
|
324
|
-
const index = models.findIndex((m) => m && typeof m === "object" && m.id === modelId);
|
|
325
|
-
if (index >= 0) return models[index][field];
|
|
326
|
-
const overrides = profile.modelOverrides;
|
|
327
|
-
if (overrides && typeof overrides === "object" && overrides[modelId] && typeof overrides[modelId] === "object") {
|
|
328
|
-
return overrides[modelId][field];
|
|
329
|
-
}
|
|
330
|
-
return undefined;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
256
|
function readCompatField(nsView, settingsPath, modelId, compatKey) {
|
|
334
257
|
if (!nsView) return undefined;
|
|
335
258
|
const profile = at(nsView.value, settingsPath);
|
|
@@ -405,6 +328,44 @@ window.__ModuleLoader__.load({
|
|
|
405
328
|
return { ns, ops: [{ op: "set", path: [...basePath, "models"], value: [{ id: modelId, [field]: value }] }] };
|
|
406
329
|
}
|
|
407
330
|
|
|
331
|
+
/**
|
|
332
|
+
* The stored `reasoningEfforts` of one model: its `models[]` row first,
|
|
333
|
+
* then (pi-ai) its `modelOverrides` entry; undefined when neither declares it.
|
|
334
|
+
*/
|
|
335
|
+
function storedReasoningEfforts(entry, nsView, modelId) {
|
|
336
|
+
const profile = at(nsView ? nsView.value : undefined, entry.settingsPath || []);
|
|
337
|
+
const models = (profile && Array.isArray(profile.models)) ? profile.models : [];
|
|
338
|
+
const row = models.find((m) => m && typeof m === "object" && m.id === modelId);
|
|
339
|
+
if (row && row.reasoningEfforts !== undefined) return row.reasoningEfforts;
|
|
340
|
+
const overrides = profile && profile.modelOverrides && typeof profile.modelOverrides === "object"
|
|
341
|
+
? profile.modelOverrides[modelId] : undefined;
|
|
342
|
+
return overrides && typeof overrides === "object" ? overrides.reasoningEfforts : undefined;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* The `reasoningEfforts` value for a set of checked levels.
|
|
347
|
+
*
|
|
348
|
+
* Since dsh 0.2.1-alpha.4 the official Models page also edits this field and
|
|
349
|
+
* lets each level send its own API value (a gateway spelling `high` as
|
|
350
|
+
* `deep`). Toggling a level here keeps every value already stored for the
|
|
351
|
+
* levels that stay, including a non-null `off`; only a newly added level
|
|
352
|
+
* defaults to its own name.
|
|
353
|
+
* @param levels - checked levels, in any order.
|
|
354
|
+
* @param existing - the stored value, or undefined.
|
|
355
|
+
* @returns `false` for no reasoning, else the level → wire-value dict.
|
|
356
|
+
*/
|
|
357
|
+
function reasoningEffortsPayload(levels, existing) {
|
|
358
|
+
const ordered = REASONING_LEVELS.filter((level) => levels.indexOf(level) >= 0);
|
|
359
|
+
if (ordered.length === 0) return false;
|
|
360
|
+
const stored = existing && typeof existing === "object" && !Array.isArray(existing) ? existing : {};
|
|
361
|
+
const dict = { off: stored.off !== undefined ? stored.off : null };
|
|
362
|
+
for (const level of ordered) {
|
|
363
|
+
const wire = stored[level];
|
|
364
|
+
dict[level] = typeof wire === "string" && wire !== "" ? wire : level;
|
|
365
|
+
}
|
|
366
|
+
return dict;
|
|
367
|
+
}
|
|
368
|
+
|
|
408
369
|
function computeCompatWriteOps(entry, nsView, modelId, compatKey, value) {
|
|
409
370
|
const ns = entry.settingsNs;
|
|
410
371
|
const settingsPath = entry.settingsPath || [];
|
|
@@ -457,202 +418,6 @@ window.__ModuleLoader__.load({
|
|
|
457
418
|
return { ns, ops: [{ op: "set", path: [...basePath, "models"], value: [entryObj] }] };
|
|
458
419
|
}
|
|
459
420
|
|
|
460
|
-
/** The modality field one provider namespace records, or undefined when it records none. */
|
|
461
|
-
function modalityFieldFor(ns) {
|
|
462
|
-
return MODALITY_FIELD_BY_NS[ns];
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
/** Map a raw modality list to the control's tri-state. Absent and empty both mean `auto`. */
|
|
466
|
-
function modalityChoice(raw) {
|
|
467
|
-
if (!Array.isArray(raw) || raw.length === 0) return "auto";
|
|
468
|
-
return raw.indexOf("image") >= 0 ? "text-image" : "text";
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
/**
|
|
472
|
-
* Explain what the current state costs, in the terms the harness applies.
|
|
473
|
-
*/
|
|
474
|
-
function modalityNote(choice, model) {
|
|
475
|
-
if (choice === "text-image") {
|
|
476
|
-
return "图像输入已开启:贴图与 read_image 可用,下一次请求即生效(无需重启)。";
|
|
477
|
-
}
|
|
478
|
-
if (choice === "text") {
|
|
479
|
-
return "已显式声明为纯文本:附图会在发送前被拒绝,read_image 同样拒绝。";
|
|
480
|
-
}
|
|
481
|
-
if (!model.declared) {
|
|
482
|
-
return "自动:由该 provider 内置的模型目录决定,本页读不到目录内容,不做推测。";
|
|
483
|
-
}
|
|
484
|
-
const route = model.routeDefaultInput;
|
|
485
|
-
if (route && route.indexOf("image") >= 0) {
|
|
486
|
-
return "自动:本路由未逐个声明,跟随路由 defaultInput,其中含图像。";
|
|
487
|
-
}
|
|
488
|
-
return "自动:手写路由没有模型目录可查,落到路由 defaultInput(未设置即为纯文本),"
|
|
489
|
-
+ "所以附图会被拒绝。端点确实收图时请选「文本 + 图像」。";
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
/**
|
|
493
|
-
* Ops for one modality choice. Non-DeepSeek namespaces write a single
|
|
494
|
-
* field; DeepSeek additionally drops the image request budgets its schema
|
|
495
|
-
* refuses beside a text-only declaration.
|
|
496
|
-
*/
|
|
497
|
-
function computeModalityWriteOps(entry, nsView, modelId, choice) {
|
|
498
|
-
const field = modalityFieldFor(entry.settingsNs);
|
|
499
|
-
if (!field) return null;
|
|
500
|
-
const value = choice === "text-image" ? ["text", "image"] : choice === "text" ? ["text"] : null;
|
|
501
|
-
const write = computeWriteOps(entry, nsView, modelId, field, value);
|
|
502
|
-
if (!write || entry.settingsNs !== "llm-deepseek" || choice === "text-image") return write;
|
|
503
|
-
const profile = at(nsView ? nsView.value : undefined, entry.settingsPath || []);
|
|
504
|
-
const models = (profile && Array.isArray(profile.models)) ? profile.models : [];
|
|
505
|
-
const current = models.find((m) => m && typeof m === "object" && m.id === modelId) || {};
|
|
506
|
-
const stale = DEEPSEEK_IMAGE_LIMIT_FIELDS.filter((name) => current[name] !== undefined);
|
|
507
|
-
if (stale.length === 0) return write;
|
|
508
|
-
const modelsOp = write.ops[0];
|
|
509
|
-
modelsOp.value = modelsOp.value.map((m) => {
|
|
510
|
-
if (!m || typeof m !== "object" || m.id !== modelId) return m;
|
|
511
|
-
const copy = { ...m };
|
|
512
|
-
for (const name of stale) delete copy[name];
|
|
513
|
-
return copy;
|
|
514
|
-
});
|
|
515
|
-
return write;
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
// ── 用途:标题生成(v2:本插件自己的字段,title* 键) ────────────────────
|
|
519
|
-
/** Same vocabulary as the host schema (`inherit` is the default). */
|
|
520
|
-
const TITLE_MODES = ["inherit", "custom"];
|
|
521
|
-
|
|
522
|
-
/**
|
|
523
|
-
* Title-request budget. Defaults mirror the shipped dsh-base row, but every
|
|
524
|
-
* field is user-editable from this page — a reasoning model that answers
|
|
525
|
-
* only after a long think needs a larger `maxOutputTokens`, and that choice
|
|
526
|
-
* belongs to the user, not to a constant in the plugin.
|
|
527
|
-
*/
|
|
528
|
-
const TITLE_BUDGET_FIELDS = [
|
|
529
|
-
{ key: "targetWords", label: "目标词数", hint: "非中日韩语言的目标词数", fallback: 5 },
|
|
530
|
-
{ key: "targetCjkCharacters", label: "目标汉字数", hint: "中文 / 日文标题的目标字数", fallback: 10 },
|
|
531
|
-
{ key: "maxInputBytes", label: "输入上限(字节)", hint: "首条消息送进标题请求的截断上限", fallback: 4096 },
|
|
532
|
-
{ key: "maxOutputTokens", label: "输出上限(token)", hint: "推理模型会先思考再作答,需留足预算(例如 1024);直接作答的模型 64 足够", fallback: 64 },
|
|
533
|
-
{ key: "timeoutMs", label: "超时(毫秒)", hint: "整次标题请求的超时", fallback: 60000 },
|
|
534
|
-
];
|
|
535
|
-
|
|
536
|
-
/**
|
|
537
|
-
* Read the title purpose view out of our own namespace view.
|
|
538
|
-
* @param {object|undefined} settingsView - `remote.settings.describe()` value.
|
|
539
|
-
* @returns the title view; `available` mirrors whether our host row is up.
|
|
540
|
-
*/
|
|
541
|
-
function titleViewOf(settingsView) {
|
|
542
|
-
const budget = {};
|
|
543
|
-
for (const field of TITLE_BUDGET_FIELDS) budget[field.key] = field.fallback;
|
|
544
|
-
const view = ownNsView(settingsView);
|
|
545
|
-
if (!view) return { available: false, mode: "inherit", provider: null, model: null, revision: null, ...budget };
|
|
546
|
-
const value = view.value && typeof view.value === "object" ? view.value : {};
|
|
547
|
-
const mode = TITLE_MODES.indexOf(value.titleMode) >= 0 ? value.titleMode : "inherit";
|
|
548
|
-
for (const field of TITLE_BUDGET_FIELDS) {
|
|
549
|
-
const raw = value[field.key];
|
|
550
|
-
if (typeof raw === "number" && Number.isInteger(raw) && raw > 0) budget[field.key] = raw;
|
|
551
|
-
}
|
|
552
|
-
return {
|
|
553
|
-
available: true,
|
|
554
|
-
mode,
|
|
555
|
-
provider: typeof value.titleProvider === "string" && value.titleProvider !== "" ? value.titleProvider : null,
|
|
556
|
-
model: typeof value.titleModel === "string" && value.titleModel !== "" ? value.titleModel : null,
|
|
557
|
-
revision: typeof view.revision === "number" ? view.revision : null,
|
|
558
|
-
...budget,
|
|
559
|
-
};
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
/**
|
|
563
|
-
* Plan the write for one title purpose edit.
|
|
564
|
-
*
|
|
565
|
-
* Always returns the COMPLETE field set: switching back to `inherit` must
|
|
566
|
-
* clear a previously chosen route instead of leaving it behind in the user
|
|
567
|
-
* section, where it would silently stay authoritative for a later switch.
|
|
568
|
-
* The budget is written the same way, so a field the user clears returns to
|
|
569
|
-
* the shipped default rather than keeping a stale value.
|
|
570
|
-
* @param next - `{ mode, provider, model, ...budget }` as chosen in the UI.
|
|
571
|
-
* @returns the mutate request body.
|
|
572
|
-
*/
|
|
573
|
-
function planTitleWrite(next) {
|
|
574
|
-
const ops = [];
|
|
575
|
-
for (const field of TITLE_BUDGET_FIELDS) {
|
|
576
|
-
const raw = next ? next[field.key] : undefined;
|
|
577
|
-
const value = raw === undefined || raw === null || raw === "" ? field.fallback : Number(raw);
|
|
578
|
-
if (!Number.isInteger(value) || value < 1) throw new Error(`「${field.label}」必须是正整数`);
|
|
579
|
-
ops.push({ op: "set", path: [field.key], value });
|
|
580
|
-
}
|
|
581
|
-
const mode = next && next.mode === "custom" ? "custom" : "inherit";
|
|
582
|
-
if (mode === "inherit") {
|
|
583
|
-
ops.push(
|
|
584
|
-
{ op: "unset", path: ["titleMode"] },
|
|
585
|
-
{ op: "unset", path: ["titleProvider"] },
|
|
586
|
-
{ op: "unset", path: ["titleModel"] },
|
|
587
|
-
);
|
|
588
|
-
} else {
|
|
589
|
-
ops.push(
|
|
590
|
-
{ op: "set", path: ["titleMode"], value: "custom" },
|
|
591
|
-
{ op: "set", path: ["titleProvider"], value: String(next.provider || "") },
|
|
592
|
-
{ op: "set", path: ["titleModel"], value: String(next.model || "") },
|
|
593
|
-
);
|
|
594
|
-
}
|
|
595
|
-
return { ns: NS, ops };
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
function CapacityField(props) {
|
|
599
|
-
const value = props.value;
|
|
600
|
-
const options = props.options;
|
|
601
|
-
const matched = value != null ? options.find((o) => o.value === value) : undefined;
|
|
602
|
-
const [mode, setMode] = React.useState(matched ? "option" : "custom");
|
|
603
|
-
const [text, setText] = React.useState(value != null ? formatCapacity(value) : "");
|
|
604
|
-
|
|
605
|
-
React.useEffect(() => {
|
|
606
|
-
const m = value != null ? options.find((o) => o.value === value) : undefined;
|
|
607
|
-
setMode(m ? "option" : "custom");
|
|
608
|
-
setText(value != null ? formatCapacity(value) : "");
|
|
609
|
-
}, [value]);
|
|
610
|
-
|
|
611
|
-
function commit() {
|
|
612
|
-
const parsed = parseCapacity(text);
|
|
613
|
-
if (parsed == null) {
|
|
614
|
-
props.onToast("err", "请输入正整数,可用 K/M 后缀(如 256K、1M)");
|
|
615
|
-
setText(value != null ? formatCapacity(value) : "");
|
|
616
|
-
return;
|
|
617
|
-
}
|
|
618
|
-
if (parsed === value) return;
|
|
619
|
-
props.onApply(props.provider, props.model, props.kind, parsed);
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
return React.createElement("label", { className: "mcfg-field" },
|
|
623
|
-
React.createElement("span", { className: "mcfg-label" }, props.label),
|
|
624
|
-
React.createElement("div", { className: "mcfg-fieldrow" },
|
|
625
|
-
React.createElement("select", {
|
|
626
|
-
className: "mcfg-select",
|
|
627
|
-
disabled: props.disabled,
|
|
628
|
-
value: mode === "option" ? String(value) : "__custom__",
|
|
629
|
-
onChange: (e) => {
|
|
630
|
-
if (e.target.value === "__custom__") {
|
|
631
|
-
setMode("custom");
|
|
632
|
-
setText(value != null ? formatCapacity(value) : "");
|
|
633
|
-
} else {
|
|
634
|
-
setMode("option");
|
|
635
|
-
props.onApply(props.provider, props.model, props.kind, Number(e.target.value));
|
|
636
|
-
}
|
|
637
|
-
},
|
|
638
|
-
},
|
|
639
|
-
options.map((o) => React.createElement("option", { key: o.value, value: String(o.value) }, o.label)),
|
|
640
|
-
React.createElement("option", { value: "__custom__" }, "自定义…")
|
|
641
|
-
),
|
|
642
|
-
mode === "custom" ? React.createElement("input", {
|
|
643
|
-
className: "mcfg-input mcfg-custom",
|
|
644
|
-
type: "text",
|
|
645
|
-
disabled: props.disabled,
|
|
646
|
-
value: text,
|
|
647
|
-
placeholder: "如 256K / 1M",
|
|
648
|
-
onChange: (e) => setText(e.target.value),
|
|
649
|
-
onBlur: commit,
|
|
650
|
-
onKeyDown: (e) => { if (e.key === "Enter") e.target.blur(); },
|
|
651
|
-
}) : null
|
|
652
|
-
)
|
|
653
|
-
);
|
|
654
|
-
}
|
|
655
|
-
|
|
656
421
|
// ── 绘图页组件(ex dsh-plugin-image-generation/client.js,样式前缀统一) ──
|
|
657
422
|
const JOB_KEY = "dsh-image-generation-job";
|
|
658
423
|
|
|
@@ -662,8 +427,8 @@ window.__ModuleLoader__.load({
|
|
|
662
427
|
}
|
|
663
428
|
const freshImageEntry = () => ({ id: "", name: "", model: "", api: "openai-images", endpoint: "https://api.openai.com/v1/images/generations", apiKeyEnv: "IMAGE_API_KEY", timeoutSeconds: 300 })
|
|
664
429
|
|
|
665
|
-
/** An entry
|
|
666
|
-
function managedBy(item) { return item?.source?.provider ?
|
|
430
|
+
/** An entry an earlier version's 模型参数 checkbox linked carries its origin. */
|
|
431
|
+
function managedBy(item) { return item?.source?.provider ? `旧版链接 · ${item.source.provider}` : "" }
|
|
667
432
|
|
|
668
433
|
function PanelImage({ call, jobId, index, image }) {
|
|
669
434
|
const [url, setUrl] = React.useState("");
|
|
@@ -745,10 +510,10 @@ window.__ModuleLoader__.load({
|
|
|
745
510
|
/**
|
|
746
511
|
* Commit one drawing-config edit.
|
|
747
512
|
*
|
|
748
|
-
* Always re-reads immediately before writing
|
|
749
|
-
*
|
|
750
|
-
*
|
|
751
|
-
*
|
|
513
|
+
* Always re-reads immediately before writing. Using the view captured at
|
|
514
|
+
* render time made a page left open across any other write fail on a
|
|
515
|
+
* stale revision — the optimistic lock rejects it and the button
|
|
516
|
+
* silently looks dead.
|
|
752
517
|
* @param buildOps - derives the ops from the freshly read view.
|
|
753
518
|
* @param successMessage - toast shown after the write and refresh.
|
|
754
519
|
*/
|
|
@@ -774,10 +539,9 @@ window.__ModuleLoader__.load({
|
|
|
774
539
|
// revision nor write a stale model list back over it.
|
|
775
540
|
await commit((fresh) => {
|
|
776
541
|
const freshModels = imageModelsOf(fresh);
|
|
777
|
-
//
|
|
778
|
-
//
|
|
779
|
-
//
|
|
780
|
-
// both tabs.
|
|
542
|
+
// `source` is provenance display only. Editing a linked entry keeps
|
|
543
|
+
// the marker so the row still tells where it came from; the identity
|
|
544
|
+
// fields are editable in place.
|
|
781
545
|
const stored = editing ? freshModels.find((item) => item.id === editing) : undefined;
|
|
782
546
|
if (managedBy(stored)) normalized.source = stored.source;
|
|
783
547
|
const entry = { ...normalized };
|
|
@@ -816,43 +580,61 @@ window.__ModuleLoader__.load({
|
|
|
816
580
|
};
|
|
817
581
|
const visibleModels = (catalog?.models ?? []).filter((item) => `${item.id} ${item.name}`.toLowerCase().includes(modelQuery.trim().toLowerCase()));
|
|
818
582
|
const running = job?.status === "running";
|
|
583
|
+
const defaultEntry = models.find((item) => item.id === view?.value?.defaultModel);
|
|
819
584
|
|
|
820
585
|
return React.createElement("div", { className: "mcfg-page" },
|
|
821
|
-
React.createElement("p", { className: "mcfg-note" }, "独立配置绘图模型。可在聊天中让助手调用 generate_image,也可在下方直接生成。接口使用 OpenAI Images 协议。"),
|
|
822
586
|
notice ? React.createElement("div", { role: "status" }, notice) : null,
|
|
823
587
|
!view ? React.createElement("p", null, "正在读取配置…") : React.createElement("fieldset", { disabled: busy }, React.createElement("legend", null, "绘图模型"),
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
React.createElement("
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
588
|
+
React.createElement("p", { className: "mcfg-note" }, "每条配置是一个独立的 OpenAI Images 端点(地址 + Key + 上游模型),与「设置 → 模型」里的聊天模型无关。在模型页勾选「绘图」会自动生成配置;generate_image 默认使用标记为「默认」的那条。"),
|
|
589
|
+
models.length === 0
|
|
590
|
+
? React.createElement("p", { className: "mcfg-note" }, "尚未配置。点「添加绘图模型」填供应商地址与 Key,再从模型列表里选一个即可。")
|
|
591
|
+
: models.map((item) => {
|
|
592
|
+
const isDefault = view.value?.defaultModel === item.id;
|
|
593
|
+
return React.createElement("div", { className: "mcfg-row mcfg-draw-row", key: item.id },
|
|
594
|
+
React.createElement("label", { className: "mcfg-draw-radio", title: isDefault ? "当前默认绘图模型" : "设为默认绘图模型" },
|
|
595
|
+
React.createElement("input", {
|
|
596
|
+
type: "radio",
|
|
597
|
+
name: "drawing-default",
|
|
598
|
+
checked: isDefault,
|
|
599
|
+
disabled: !writable || !!form || isDefault,
|
|
600
|
+
onChange: () => void action(() => commit(
|
|
601
|
+
() => [{ op: "set", path: ["defaultModel"], value: item.id }],
|
|
602
|
+
`已将 ${item.name || item.id} 设为默认`,
|
|
603
|
+
)),
|
|
604
|
+
})
|
|
605
|
+
),
|
|
606
|
+
React.createElement("span", { className: "mcfg-draw-item" },
|
|
607
|
+
React.createElement("span", { className: "mcfg-draw-name" },
|
|
608
|
+
item.name || item.id,
|
|
609
|
+
isDefault ? React.createElement("span", { className: "mcfg-chip mcfg-chip-on" }, "默认") : null
|
|
610
|
+
),
|
|
611
|
+
React.createElement("span", { className: "mcfg-draw-meta" },
|
|
612
|
+
"配置 " + item.id + " · 上游模型 " + item.model + (managedBy(item) ? " · " + managedBy(item) : "")
|
|
613
|
+
)
|
|
614
|
+
),
|
|
615
|
+
React.createElement("div", { className: "mcfg-actions2" },
|
|
616
|
+
React.createElement("button", { disabled: !writable || !!form, onClick: () => { setForm({ ...item }); setEditing(item.id); setKey(""); setCatalog(null); } }, "编辑"),
|
|
617
|
+
React.createElement("button", {
|
|
618
|
+
disabled: !writable || !!form,
|
|
619
|
+
onClick: () => void action(async () => {
|
|
620
|
+
await commit((fresh) => {
|
|
621
|
+
const next = imageModelsOf(fresh).filter((model) => model.id !== item.id);
|
|
622
|
+
const currentDefault = fresh.value?.defaultModel;
|
|
623
|
+
return [
|
|
624
|
+
{ op: "set", path: ["models"], value: next },
|
|
625
|
+
{ op: "set", path: ["defaultModel"], value: currentDefault === item.id ? (next[0]?.id ?? "") : (currentDefault ?? "") },
|
|
626
|
+
];
|
|
627
|
+
}, `已删除 ${item.name || item.id}`);
|
|
628
|
+
if (selected === item.id) setSelected("");
|
|
629
|
+
}),
|
|
630
|
+
}, "删除")));
|
|
631
|
+
}),
|
|
851
632
|
React.createElement("div", { className: "mcfg-actions2" },
|
|
852
|
-
React.createElement("button", { disabled: !writable || !!form, onClick: () => { setForm(freshImageEntry()); setEditing(""); setKey(""); setCatalog(null); } }, "
|
|
633
|
+
React.createElement("button", { className: "mcfg-primary", disabled: !writable || !!form, onClick: () => { setForm(freshImageEntry()); setEditing(""); setKey(""); setCatalog(null); } }, "手动添加端点"),
|
|
853
634
|
React.createElement("button", { onClick: () => void action(async () => { await refresh(); setForm(null); setKey(""); }) }, "重新加载")),
|
|
854
635
|
!writable ? React.createElement("p", { className: "mcfg-note" }, "当前配置只读。") : null),
|
|
855
|
-
form ? React.createElement("fieldset", { disabled: busy }, React.createElement("legend", null, editing ? "
|
|
636
|
+
form ? React.createElement("fieldset", { disabled: busy }, React.createElement("legend", null, editing ? "编辑绘图模型" : "手动添加端点"),
|
|
637
|
+
React.createElement("p", { className: "mcfg-note" }, "通常不需要手动添加:在「设置 → 模型」里勾选模型行的「绘图」即可自动生成。这里用于没有对应聊天模型的独立端点。填写顺序:供应商 API 地址 → API Key →「获取模型列表」选择模型 → 保存。"),
|
|
856
638
|
imageField("供应商 API 地址", form.endpoint.replace(/\/images\/generations\/?$/, ""), (value) => update("endpoint")(value.replace(/\/+$/, "").replace(/\/images\/generations$/, "") + "/images/generations"), { type: "url", placeholder: "https://api.example.com/v1" }),
|
|
857
639
|
imageField("API Key(留空保留现有值)", key, (value) => { setKey(value); setCatalog(null); }, { type: "password", autoComplete: "new-password" }),
|
|
858
640
|
React.createElement("button", { onClick: () => void action(discover) }, "获取模型列表"),
|
|
@@ -877,746 +659,407 @@ window.__ModuleLoader__.load({
|
|
|
877
659
|
imageField("超时(秒)", form.timeoutSeconds, update("timeoutSeconds"), { type: "number", min: 10, max: 600 }),
|
|
878
660
|
...[["尺寸", "size"], ["质量", "quality"], ["背景", "background"], ["输出格式", "output_format"], ["响应格式", "response_format"], ["风格", "style"]].map(([label, name]) => imageField(`${label}(可选)`, form[name], update(name)))),
|
|
879
661
|
React.createElement("p", { className: "mcfg-note" }, "可选参数留空则不发送。GPT Image 的响应格式请留空。Key 通过 dsh 凭据服务单独保存;同名引用会被其他模型共享。"),
|
|
880
|
-
editing && managedBy(form) ? React.createElement("p", { className: "mcfg-note" },
|
|
662
|
+
editing && managedBy(form) ? React.createElement("p", { className: "mcfg-note" }, `该配置来自「设置 → 模型」里勾选的绘图模型(${managedBy(form)})。推荐回到那边勾选/取消,端点与凭据会自动跟随 provider。`) : null),
|
|
881
663
|
React.createElement("div", { className: "mcfg-actions2" }, React.createElement("button", { onClick: () => void action(save) }, "保存"), React.createElement("button", { onClick: () => { setForm(null); setKey(""); } }, "取消"))) : null,
|
|
882
|
-
React.createElement("
|
|
883
|
-
React.createElement("
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
664
|
+
React.createElement("details", { className: "mcfg-advanced" },
|
|
665
|
+
React.createElement("summary", null, "直接绘图(可选,不经过聊天)"),
|
|
666
|
+
React.createElement("fieldset", { disabled: busy || running },
|
|
667
|
+
React.createElement("label", { className: "mcfg-field2" }, "绘图模型", React.createElement("select", { value: selected, onChange: (event) => setSelected(event.target.value) },
|
|
668
|
+
React.createElement("option", { value: "" }, "使用默认模型" + (defaultEntry ? `(${defaultEntry.name || defaultEntry.id})` : "")),
|
|
669
|
+
...models.map((item) => React.createElement("option", { key: item.id, value: item.id },
|
|
670
|
+
(item.name || item.id) + (view.value?.defaultModel === item.id ? "(默认)" : "") + " · " + item.model)))),
|
|
671
|
+
React.createElement("label", { className: "mcfg-field2" }, "提示词", React.createElement("textarea", { value: prompt, onChange: (event) => setPrompt(event.target.value), placeholder: "描述你想生成的画面…" })),
|
|
672
|
+
imageField("图片数量", count, setCount, { type: "number", min: 1, max: 4 }),
|
|
673
|
+
React.createElement("button", { className: "mcfg-primary", disabled: models.length === 0 || !prompt.trim(), onClick: () => void action(generate) }, "生成图片"),
|
|
674
|
+
React.createElement("p", { className: "mcfg-note" }, "点击生成会调用所选服务,可能产生费用。此面板不发送聊天历史,结果不插入对话。任务预览保留到宿主重启、过期或被后续任务替换前,请及时下载原图。"))),
|
|
889
675
|
running ? React.createElement("div", { role: "status" }, "正在生成… ", React.createElement("button", { onClick: () => void action(async () => { await call("cancel", { jobId }); }) }, "取消等待")) : null,
|
|
890
676
|
job?.status === "error" ? React.createElement("div", { className: "mcfg-error", role: "alert" }, job.error) : null,
|
|
891
677
|
job?.result ? React.createElement("div", { className: "mcfg-gallery" }, job.result.images.map((image, index) => React.createElement(PanelImage, { key: `${jobId}-${index}`, call, jobId, image, index }))) : null);
|
|
892
678
|
}
|
|
893
679
|
|
|
894
|
-
|
|
895
|
-
|
|
680
|
+
// ── 设置页:绘图(v3 后本插件唯一的独立页面) ─────────────────────────
|
|
681
|
+
function DrawingPage(props) {
|
|
682
|
+
const remote = props.remote;
|
|
896
683
|
const timer = props.timer;
|
|
897
|
-
const [
|
|
898
|
-
const [
|
|
899
|
-
const [loading, setLoading] = React.useState(true);
|
|
684
|
+
const [view, setView] = React.useState(null);
|
|
685
|
+
const [writable, setWritable] = React.useState(false);
|
|
900
686
|
const [error, setError] = React.useState(null);
|
|
901
687
|
const [toast, setToast] = React.useState(null);
|
|
902
|
-
const [query, setQuery] = React.useState("");
|
|
903
|
-
const [tab, setTab] = React.useState("models");
|
|
904
|
-
const [title, setTitle] = React.useState(null);
|
|
905
|
-
const [titleDraft, setTitleDraft] = React.useState(null);
|
|
906
|
-
const [titleBusy, setTitleBusy] = React.useState(false);
|
|
907
688
|
|
|
908
|
-
async function
|
|
689
|
+
async function refresh() {
|
|
909
690
|
try {
|
|
910
|
-
const
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
if (!modelsRes.result.ok) throw new Error(modelsRes.result.error.message);
|
|
917
|
-
if (!settingsRes.result.ok) throw new Error(settingsRes.result.error.message);
|
|
918
|
-
|
|
919
|
-
const providers = providersRes.result.value.providers;
|
|
920
|
-
const groups = modelsRes.result.value.groups;
|
|
921
|
-
const namespaces = settingsRes.result.value.namespaces;
|
|
922
|
-
const writable = settingsRes.result.value.writable;
|
|
923
|
-
|
|
924
|
-
const metaByProvider = new Map(providers.map((p) => [p.provider, p]));
|
|
925
|
-
const nsByNs = new Map(namespaces.map((n) => [n.ns, n]));
|
|
926
|
-
// v2: our own view; absent only when our host row failed to activate.
|
|
927
|
-
const view = ownNsView(settingsRes.result.value);
|
|
928
|
-
setTitle(titleViewOf(settingsRes.result.value));
|
|
929
|
-
setTitleDraft(null);
|
|
930
|
-
setError(null);
|
|
931
|
-
|
|
932
|
-
const built = { writable, providers: [] };
|
|
933
|
-
for (const group of groups) {
|
|
934
|
-
const entry = metaByProvider.get(group.id);
|
|
935
|
-
const nsView = entry ? nsByNs.get(entry.settingsNs) : undefined;
|
|
936
|
-
const providerProfile = profileOf(nsView, entry ? entry.settingsPath : []);
|
|
937
|
-
const reasoningEditable = !!(entry && entry.settingsNs === "llm-pi-ai");
|
|
938
|
-
const adaptiveKey = entry ? adaptiveThinkingCompatKey(entry.settingsNs, providerProfile) : null;
|
|
939
|
-
const routeDefaultInput = providerProfile && Array.isArray(providerProfile.defaultInput)
|
|
940
|
-
? providerProfile.defaultInput
|
|
941
|
-
: null;
|
|
942
|
-
const models = group.models.map((m) => {
|
|
943
|
-
const ctx = readModelField(nsView, entry ? entry.settingsPath : [], m.id, "contextWindow");
|
|
944
|
-
const mx = readModelField(nsView, entry ? entry.settingsPath : [], m.id, "maxTokens");
|
|
945
|
-
const devRole = entry && entry.settingsNs === "llm-pi-ai"
|
|
946
|
-
? readCompatField(nsView, entry.settingsPath, m.id, "supportsDeveloperRole")
|
|
947
|
-
: undefined;
|
|
948
|
-
const adaptiveThinking = adaptiveKey
|
|
949
|
-
? readCompatField(nsView, entry.settingsPath, m.id, adaptiveKey)
|
|
950
|
-
: undefined;
|
|
951
|
-
const modalityField = entry ? modalityFieldFor(entry.settingsNs) : undefined;
|
|
952
|
-
const rawInput = modalityField
|
|
953
|
-
? readModelField(nsView, entry.settingsPath, m.id, modalityField)
|
|
954
|
-
: undefined;
|
|
955
|
-
const image = computeImageModelState(view, providerProfile, m.id);
|
|
956
|
-
const efforts = (m.reasoning && Array.isArray(m.reasoning.efforts)) ? m.reasoning.efforts.map((e) => ({ id: e.id, name: e.name })) : [];
|
|
957
|
-
return {
|
|
958
|
-
id: m.id,
|
|
959
|
-
name: m.name,
|
|
960
|
-
description: m.description != null ? m.description : null,
|
|
961
|
-
contextWindow: ctx != null ? ctx : null,
|
|
962
|
-
maxTokens: mx != null ? mx : null,
|
|
963
|
-
supportsDeveloperRole: devRole,
|
|
964
|
-
adaptiveThinking: adaptiveThinking === true,
|
|
965
|
-
// The flag only changes a request that carries thinking at all,
|
|
966
|
-
// so it is offered where a level can be selected.
|
|
967
|
-
adaptiveThinkingEditable: adaptiveKey !== null && efforts.length > 0,
|
|
968
|
-
modalityField: modalityField || null,
|
|
969
|
-
modality: modalityField ? modalityChoice(rawInput) : null,
|
|
970
|
-
imageAvailable: image.available,
|
|
971
|
-
imageLinked: image.hasEntry,
|
|
972
|
-
imageManaged: image.managed,
|
|
973
|
-
imageEndpoint: image.endpoint || null,
|
|
974
|
-
imageApiKeyEnv: image.apiKeyEnv || null,
|
|
975
|
-
declared: !!(entry && entry.declared),
|
|
976
|
-
routeDefaultInput: routeDefaultInput,
|
|
977
|
-
efforts,
|
|
978
|
-
defaultEffort: (m.reasoning && m.reasoning.defaultEffort != null) ? m.reasoning.defaultEffort : null,
|
|
979
|
-
};
|
|
980
|
-
});
|
|
981
|
-
built.providers.push({
|
|
982
|
-
provider: group.id,
|
|
983
|
-
name: group.name,
|
|
984
|
-
configurable: !!entry,
|
|
985
|
-
reasoningEditable,
|
|
986
|
-
imageNsAvailable: !!view,
|
|
987
|
-
models,
|
|
988
|
-
});
|
|
989
|
-
}
|
|
990
|
-
const imageModels = imageModelsOf(view);
|
|
991
|
-
const linkedIds = new Set(imageModels.filter((item) => item && item.model).map((item) => item.model));
|
|
992
|
-
for (const row of built.providers) {
|
|
993
|
-
row.imageLinkedCount = row.models.filter((m) => linkedIds.has(m.id)).length;
|
|
994
|
-
}
|
|
995
|
-
built.imageModels = imageModels.map((item) => item?.model).filter(Boolean);
|
|
996
|
-
built.imageDefault = typeof view?.value?.defaultModel === "string" ? view.value.defaultModel : "";
|
|
997
|
-
setData(built);
|
|
998
|
-
setOwnView(view ?? null);
|
|
691
|
+
const response = await remote.settings.describe();
|
|
692
|
+
if (!response.ok) throw new Error(response.error && response.error.message ? response.error.message : "读取设置失败");
|
|
693
|
+
const namespaces = Array.isArray(response.value?.namespaces) ? response.value.namespaces : [];
|
|
694
|
+
const found = namespaces.find((item) => item && item.ns === NS);
|
|
695
|
+
setView(found ?? null);
|
|
696
|
+
setWritable(response.value?.writable !== false);
|
|
999
697
|
setError(null);
|
|
1000
698
|
return true;
|
|
1001
699
|
} catch (err) {
|
|
1002
700
|
setError(err && err.message ? err.message : String(err));
|
|
1003
701
|
return false;
|
|
1004
|
-
} finally {
|
|
1005
|
-
setLoading(false);
|
|
1006
702
|
}
|
|
1007
703
|
}
|
|
1008
704
|
|
|
1009
|
-
React.useEffect(() => {
|
|
1010
|
-
reload();
|
|
1011
|
-
}, []);
|
|
705
|
+
React.useEffect(() => { void refresh(); }, []);
|
|
1012
706
|
|
|
1013
707
|
function showToast(kind, text) {
|
|
1014
708
|
setToast({ kind, text });
|
|
1015
709
|
if (timer) timer.timeout(() => setToast(null), 3000);
|
|
1016
710
|
}
|
|
1017
711
|
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
const [providersRes, settingsRes] = await Promise.all([
|
|
1021
|
-
api.llm.providers({}),
|
|
1022
|
-
api.settings.describe({}),
|
|
1023
|
-
]);
|
|
1024
|
-
if (!providersRes.result.ok) throw new Error(providersRes.result.error.message);
|
|
1025
|
-
if (!settingsRes.result.ok) throw new Error(settingsRes.result.error.message);
|
|
1026
|
-
|
|
1027
|
-
const entry = providersRes.result.value.providers.find((p) => p.provider === provider);
|
|
1028
|
-
if (!entry) throw new Error("该 provider 不可配置");
|
|
1029
|
-
const nsView = settingsRes.result.value.namespaces.find((n) => n.ns === entry.settingsNs);
|
|
712
|
+
if (error) return React.createElement("div", { className: "mcfg-state mcfg-error" }, "加载失败:" + error);
|
|
713
|
+
if (!view) return React.createElement("div", { className: "mcfg-state" }, "正在读取绘图配置…");
|
|
1030
714
|
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
ops: [
|
|
1046
|
-
{ op: "set", path: ["models"], value: plan.models },
|
|
1047
|
-
{ op: "set", path: ["defaultModel"], value: plan.defaultModel || "" },
|
|
1048
|
-
],
|
|
1049
|
-
expectedRevision: freshView.revision,
|
|
1050
|
-
});
|
|
1051
|
-
if (!written.result.ok) throw new Error(written.result.error.message);
|
|
1052
|
-
showToast("ok", value === true ? "已在「绘图」页生成模型配置" : "已取消生图标记");
|
|
1053
|
-
await reload();
|
|
1054
|
-
return;
|
|
1055
|
-
}
|
|
715
|
+
return React.createElement("div", { className: "mcfg" },
|
|
716
|
+
React.createElement("div", { className: "mcfg-head" },
|
|
717
|
+
React.createElement("div", { className: "mcfg-title" }, "绘图"),
|
|
718
|
+
React.createElement("div", { className: "mcfg-sub" },
|
|
719
|
+
"这里管理 generate_image 使用的绘图模型。通常不用手动配置:去「设置 → 模型」给某个模型勾选「绘图」即可自动生成一条;标为默认的那条就是 generate_image 的默认模型。"
|
|
720
|
+
+ (writable ? "" : "(当前部署为只读,无法保存。)")
|
|
721
|
+
)
|
|
722
|
+
),
|
|
723
|
+
toast ? React.createElement("div", { className: "mcfg-toast " + (toast.kind === "ok" ? "mcfg-toast-ok" : "mcfg-toast-err") }, toast.text) : null,
|
|
724
|
+
React.createElement(ImagePanel, {
|
|
725
|
+
remote, call: props.call, writable, view, refresh, showToast,
|
|
726
|
+
})
|
|
727
|
+
);
|
|
728
|
+
}
|
|
1056
729
|
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
}
|
|
1070
|
-
} else if (kind === "contextWindow" || kind === "maxTokens") {
|
|
1071
|
-
field = kind;
|
|
1072
|
-
if (value != null && (typeof value !== "number" || !Number.isInteger(value) || value <= 0)) {
|
|
1073
|
-
throw new Error("取值必须是正整数");
|
|
1074
|
-
}
|
|
1075
|
-
payload = value;
|
|
1076
|
-
} else if (kind === "supportsDeveloperRole" || kind === "adaptiveThinking") {
|
|
1077
|
-
if (value !== null && typeof value !== "boolean") {
|
|
1078
|
-
throw new Error("取值必须是布尔值");
|
|
1079
|
-
}
|
|
1080
|
-
const compatKey = kind === "supportsDeveloperRole" ? "supportsDeveloperRole" : ADAPTIVE_THINKING_COMPAT_KEY;
|
|
1081
|
-
const write = computeCompatWriteOps(entry, nsView, model, compatKey, value);
|
|
1082
|
-
if (write) {
|
|
1083
|
-
const res = await api.settings.mutate({ ns: write.ns, ops: write.ops });
|
|
1084
|
-
if (!res.result.ok) throw new Error(res.result.error.message);
|
|
1085
|
-
}
|
|
1086
|
-
showToast("ok", "已保存");
|
|
1087
|
-
await reload();
|
|
1088
|
-
return;
|
|
1089
|
-
} else if (kind === "modalities") {
|
|
1090
|
-
if (MODALITY_CHOICES.indexOf(value) < 0) throw new Error("未知的模态取值");
|
|
1091
|
-
const write = computeModalityWriteOps(entry, nsView, model, value);
|
|
1092
|
-
if (!write) throw new Error("该 provider 不接受模态声明");
|
|
1093
|
-
const res = await api.settings.mutate({ ns: write.ns, ops: write.ops });
|
|
1094
|
-
if (!res.result.ok) throw new Error(res.result.error.message);
|
|
1095
|
-
showToast("ok", value === "text-image" ? "已开启图像输入,下一次请求即生效" : "已保存");
|
|
1096
|
-
await reload();
|
|
1097
|
-
return;
|
|
1098
|
-
} else {
|
|
1099
|
-
throw new Error("未知的修改类型");
|
|
1100
|
-
}
|
|
730
|
+
// ── 官方「设置 → 模型」卡片的扩展:官方没有的兼容开关与目录推理覆盖 ──
|
|
731
|
+
// 页面长度敏感:整段默认折叠(<details>),每个模型一行紧凑控件;
|
|
732
|
+
// 推理档位是复选组,只在点开该模型时才渲染。
|
|
733
|
+
function CompatCard(props) {
|
|
734
|
+
const load = props.load;
|
|
735
|
+
const save = props.save;
|
|
736
|
+
const provider = props.provider;
|
|
737
|
+
const [data, setData] = React.useState(null);
|
|
738
|
+
const [error, setError] = React.useState(null);
|
|
739
|
+
const [toast, setToast] = React.useState(null);
|
|
740
|
+
const [busy, setBusy] = React.useState(false);
|
|
741
|
+
const [openLevels, setOpenLevels] = React.useState("");
|
|
1101
742
|
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
showToast("ok", "已保存");
|
|
1108
|
-
await reload();
|
|
743
|
+
const reload = React.useCallback(async () => {
|
|
744
|
+
try {
|
|
745
|
+
const value = await load(provider.provider);
|
|
746
|
+
setData(value);
|
|
747
|
+
setError(null);
|
|
1109
748
|
} catch (err) {
|
|
1110
|
-
|
|
749
|
+
setError(err && err.message ? err.message : String(err));
|
|
1111
750
|
}
|
|
1112
|
-
}
|
|
751
|
+
}, [load, provider.provider]);
|
|
1113
752
|
|
|
1114
|
-
|
|
1115
|
-
return React.createElement("div", { className: "mcfg-state" }, "正在加载模型配置…");
|
|
1116
|
-
}
|
|
1117
|
-
if (error) {
|
|
1118
|
-
return React.createElement("div", { className: "mcfg-state mcfg-error" }, "加载失败:" + error);
|
|
1119
|
-
}
|
|
753
|
+
React.useEffect(() => { void reload(); }, [reload]);
|
|
1120
754
|
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
const titleProvider = titleProviders.find((p) => p.provider === titleShown.provider) || null;
|
|
1125
|
-
const titleModels = titleProvider ? titleProvider.models : [];
|
|
1126
|
-
const titleDirty = !!title && (
|
|
1127
|
-
titleShown.mode !== title.mode
|
|
1128
|
-
|| (titleShown.provider || null) !== (title.provider || null)
|
|
1129
|
-
|| (titleShown.model || null) !== (title.model || null)
|
|
1130
|
-
|| TITLE_BUDGET_FIELDS.some((field) => Number(titleShown[field.key]) !== Number(title[field.key]))
|
|
1131
|
-
);
|
|
1132
|
-
const titleSaveDisabled = !data || !data.writable || !titleShown.available || titleBusy
|
|
1133
|
-
|| (titleShown.mode === "custom" && (!titleShown.provider || !titleShown.model));
|
|
1134
|
-
|
|
1135
|
-
function editTitleDraft(patch) {
|
|
1136
|
-
setTitleDraft({ ...titleShown, ...patch });
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
async function saveTitle() {
|
|
1140
|
-
// Budget travels with the route: both are this page's to write.
|
|
1141
|
-
const payload = {
|
|
1142
|
-
mode: titleShown.mode === "custom" ? "custom" : "inherit",
|
|
1143
|
-
provider: titleShown.provider,
|
|
1144
|
-
model: titleShown.model,
|
|
1145
|
-
};
|
|
1146
|
-
for (const field of TITLE_BUDGET_FIELDS) payload[field.key] = titleShown[field.key];
|
|
1147
|
-
setTitleBusy(true);
|
|
1148
|
-
let saved = false;
|
|
755
|
+
async function commit(kind, modelId, value, message) {
|
|
756
|
+
setBusy(true);
|
|
757
|
+
setToast(null);
|
|
1149
758
|
try {
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
saved = true;
|
|
759
|
+
await save(provider.provider, modelId, kind, value);
|
|
760
|
+
await reload();
|
|
761
|
+
setToast({ kind: "ok", text: message || "已保存,下一次请求生效" });
|
|
1154
762
|
} catch (err) {
|
|
1155
|
-
|
|
763
|
+
setToast({ kind: "err", text: err && err.message ? err.message : String(err) });
|
|
1156
764
|
} finally {
|
|
1157
|
-
|
|
765
|
+
setBusy(false);
|
|
1158
766
|
}
|
|
1159
|
-
if (!saved) return;
|
|
1160
|
-
showToast("ok", "标题生成设置已保存,对之后生成的标题生效");
|
|
1161
|
-
if (!await reload()) showToast("err", "设置已保存,但页面数据刷新失败,请重新打开本页确认。");
|
|
1162
767
|
}
|
|
1163
768
|
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
const
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
},
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
769
|
+
if (error) {
|
|
770
|
+
return React.createElement("div", { className: "mcfg-compat" },
|
|
771
|
+
React.createElement("div", { className: "mcfg-error" }, "兼容设置读取失败:" + error));
|
|
772
|
+
}
|
|
773
|
+
if (!data) return null;
|
|
774
|
+
if (!data.models || data.models.length === 0) return null;
|
|
775
|
+
const editable = !busy && data.writable;
|
|
776
|
+
const overridden = data.models.filter((model) => model.hasOverride).length;
|
|
777
|
+
|
|
778
|
+
const imageModels = data.models.filter((model) => model.imageAvailable);
|
|
779
|
+
const linkedCount = imageModels.filter((model) => model.imageLinked).length;
|
|
780
|
+
const toastLine = toast
|
|
781
|
+
? React.createElement("div", { className: "mcfg-toast " + (toast.kind === "ok" ? "mcfg-toast-ok" : "mcfg-toast-err"), role: "status" }, toast.text)
|
|
782
|
+
: null;
|
|
783
|
+
|
|
784
|
+
// Drawing link lives next to the compat switches because both are
|
|
785
|
+
// per-model add-ons to the official Models page; this is also where the
|
|
786
|
+
// old 模型调参 page offered its 「生图模型」 checkbox.
|
|
787
|
+
const drawingSection = imageModels.length === 0 ? null : React.createElement("details", { className: "mcfg-compat" },
|
|
788
|
+
React.createElement("summary", { className: "mcfg-compat-head" },
|
|
789
|
+
React.createElement("span", null, "绘图模型(generate_image)"),
|
|
790
|
+
React.createElement("span", { className: linkedCount > 0 ? "mcfg-chip mcfg-chip-on" : "mcfg-chip" },
|
|
791
|
+
linkedCount > 0 ? linkedCount + " 个已勾选" : "未勾选"),
|
|
792
|
+
React.createElement("span", { className: "mcfg-compat-hint" }, "勾选后自动加入「设置 → 绘图」")
|
|
793
|
+
),
|
|
794
|
+
React.createElement("div", { className: "mcfg-compat-list" },
|
|
795
|
+
imageModels.map((model) => React.createElement("label", {
|
|
796
|
+
key: model.id, className: "mcfg-draw-check",
|
|
797
|
+
title: "复用本 provider 的地址与凭据引用,自动生成一条绘图配置(generate_image 可直接使用)",
|
|
798
|
+
},
|
|
799
|
+
React.createElement("input", {
|
|
800
|
+
type: "checkbox",
|
|
801
|
+
checked: model.imageLinked,
|
|
802
|
+
disabled: !editable,
|
|
803
|
+
onChange: () => void commit(
|
|
804
|
+
"imageLink",
|
|
805
|
+
model.id,
|
|
806
|
+
!model.imageLinked,
|
|
807
|
+
model.imageLinked ? "已从「设置 → 绘图」移除" : "已加入「设置 → 绘图」",
|
|
808
|
+
),
|
|
809
|
+
}),
|
|
810
|
+
React.createElement("span", { className: "mcfg-draw-check-name" }, model.name || model.id),
|
|
811
|
+
React.createElement("span", { className: "mcfg-draw-check-meta" },
|
|
812
|
+
model.imageLinked ? "已加入绘图配置" : "未加入")
|
|
813
|
+
))
|
|
814
|
+
)
|
|
1187
815
|
);
|
|
1188
816
|
|
|
1189
|
-
const
|
|
1190
|
-
React.createElement("
|
|
1191
|
-
React.createElement("
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
placeholder: "搜索模型 / provider…",
|
|
1196
|
-
onChange: (e) => setQuery(e.target.value),
|
|
1197
|
-
}),
|
|
1198
|
-
query.trim().length > 0 ? React.createElement("span", { className: "mcfg-count" }, totalMatched + " 个模型") : null
|
|
817
|
+
const compatSection = React.createElement("details", { className: "mcfg-compat" },
|
|
818
|
+
React.createElement("summary", { className: "mcfg-compat-head" },
|
|
819
|
+
React.createElement("span", null, "兼容与推理(插件补充)"),
|
|
820
|
+
React.createElement("span", { className: overridden > 0 ? "mcfg-chip mcfg-chip-on" : "mcfg-chip" },
|
|
821
|
+
overridden > 0 ? overridden + " 个模型有覆盖" : data.models.length + " 个模型"),
|
|
822
|
+
React.createElement("span", { className: "mcfg-compat-hint" }, "官方 Models 页没有这些控件")
|
|
1199
823
|
),
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
824
|
+
React.createElement("div", { className: "mcfg-compat-list" },
|
|
825
|
+
data.models.map((model) => {
|
|
826
|
+
const levelsOpen = openLevels === model.id;
|
|
827
|
+
return React.createElement("div", { key: model.id, className: "mcfg-compat-row" + (levelsOpen ? " mcfg-compat-row-open" : "") },
|
|
828
|
+
React.createElement("div", { className: "mcfg-compat-line" },
|
|
829
|
+
React.createElement("span", { className: "mcfg-compat-name", title: model.id }, model.name || model.id),
|
|
830
|
+
React.createElement("select", {
|
|
831
|
+
className: "mcfg-mini-select",
|
|
832
|
+
title: "系统提示角色:网关报 role 'developer' is not allowed 时选 system(Kimi/DeepSeek 网关常见)",
|
|
833
|
+
disabled: !editable,
|
|
834
|
+
value: model.supportsDeveloperRole === undefined ? "auto" : (model.supportsDeveloperRole ? "developer" : "system"),
|
|
835
|
+
onChange: (event) => {
|
|
836
|
+
const picked = event.target.value;
|
|
837
|
+
void commit("supportsDeveloperRole", model.id, picked === "auto" ? null : picked === "developer");
|
|
838
|
+
},
|
|
839
|
+
},
|
|
840
|
+
React.createElement("option", { value: "auto" }, "角色:自动"),
|
|
841
|
+
React.createElement("option", { value: "system" }, "角色:system"),
|
|
842
|
+
React.createElement("option", { value: "developer" }, "角色:developer")
|
|
1214
843
|
),
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
REASONING_LEVELS.map((level) =>
|
|
1240
|
-
React.createElement("label", { key: level, className: "mcfg-level" },
|
|
1241
|
-
React.createElement("input", {
|
|
1242
|
-
type: "checkbox",
|
|
1243
|
-
checked: selectedLevels.has(level),
|
|
1244
|
-
onChange: () => {
|
|
1245
|
-
const next = new Set(selectedLevels);
|
|
1246
|
-
if (next.has(level)) next.delete(level); else next.add(level);
|
|
1247
|
-
applyChange(provider.provider, model.id, "reasoningLevels", { levels: [...next] });
|
|
1248
|
-
},
|
|
1249
|
-
}),
|
|
1250
|
-
React.createElement("span", null, level)
|
|
1251
|
-
)
|
|
1252
|
-
)
|
|
1253
|
-
),
|
|
1254
|
-
selectedLevels.size === 0 ? React.createElement("div", { className: "mcfg-note" }, "未勾选任何档位 → 该模型视为无推理能力") : null
|
|
1255
|
-
) : React.createElement("div", { className: "mcfg-reasoning" },
|
|
1256
|
-
React.createElement("span", { className: "mcfg-label" }, "推理档位(由 provider 固定)"),
|
|
1257
|
-
React.createElement("div", { className: "mcfg-levels" },
|
|
1258
|
-
(model.efforts || []).map((effort) =>
|
|
1259
|
-
React.createElement("span", { key: effort.id, className: "mcfg-chip" }, effort.name)
|
|
1260
|
-
)
|
|
1261
|
-
)
|
|
1262
|
-
),
|
|
1263
|
-
provider.reasoningEditable ? React.createElement("div", { className: "mcfg-reasoning" },
|
|
1264
|
-
React.createElement("span", { className: "mcfg-label" }, "系统提示角色(兼容开关)"),
|
|
1265
|
-
React.createElement("div", { className: "mcfg-levels" },
|
|
1266
|
-
["auto", "system", "developer"].map((role) =>
|
|
1267
|
-
React.createElement("label", { key: role, className: "mcfg-level" },
|
|
1268
|
-
React.createElement("input", {
|
|
1269
|
-
type: "radio",
|
|
1270
|
-
name: "devrole-" + model.id,
|
|
1271
|
-
disabled: !editable,
|
|
1272
|
-
checked: role === "auto"
|
|
1273
|
-
? model.supportsDeveloperRole === undefined
|
|
1274
|
-
: (role === "developer" ? model.supportsDeveloperRole === true : model.supportsDeveloperRole === false),
|
|
1275
|
-
onChange: () => {
|
|
1276
|
-
if (role === "auto") {
|
|
1277
|
-
applyChange(provider.provider, model.id, "supportsDeveloperRole", null);
|
|
1278
|
-
} else {
|
|
1279
|
-
applyChange(provider.provider, model.id, "supportsDeveloperRole", role === "developer");
|
|
1280
|
-
}
|
|
1281
|
-
},
|
|
1282
|
-
}),
|
|
1283
|
-
React.createElement("span", null,
|
|
1284
|
-
role === "auto" ? "自动" : (role === "system" ? "system(推荐 Kimi/DeepSeek)" : "developer")
|
|
1285
|
-
)
|
|
1286
|
-
)
|
|
1287
|
-
)
|
|
1288
|
-
),
|
|
1289
|
-
React.createElement("div", { className: "mcfg-note" },
|
|
1290
|
-
"自动:跟随网关检测;如遇 role 'developer' is not allowed,请选 system"
|
|
1291
|
-
)
|
|
1292
|
-
) : null,
|
|
1293
|
-
model.adaptiveThinkingEditable ? React.createElement("div", { className: "mcfg-reasoning" },
|
|
1294
|
-
React.createElement("span", { className: "mcfg-label" }, "思考格式(Anthropic 兼容开关)"),
|
|
1295
|
-
React.createElement("div", { className: "mcfg-levels" },
|
|
1296
|
-
["auto", "adaptive"].map((choice) =>
|
|
1297
|
-
React.createElement("label", { key: choice, className: "mcfg-level" },
|
|
1298
|
-
React.createElement("input", {
|
|
1299
|
-
type: "radio",
|
|
1300
|
-
name: "adaptive-" + model.id,
|
|
1301
|
-
disabled: !editable,
|
|
1302
|
-
checked: choice === "auto" ? !model.adaptiveThinking : model.adaptiveThinking,
|
|
1303
|
-
onChange: () => applyChange(provider.provider, model.id, "adaptiveThinking", choice === "adaptive" ? true : null),
|
|
1304
|
-
}),
|
|
1305
|
-
React.createElement("span", null, choice === "auto" ? "自动(预算式 thinking)" : "自适应 thinking")
|
|
1306
|
-
)
|
|
1307
|
-
)
|
|
1308
|
-
),
|
|
1309
|
-
React.createElement("div", { className: "mcfg-note" },
|
|
1310
|
-
"上面选中档位后,请求体由这里决定:自适应写 compat." + ADAPTIVE_THINKING_COMPAT_KEY
|
|
1311
|
-
+ ",发出 thinking.type=adaptive 与 output_config.effort;Claude Opus 4.6 及更新、Sonnet 5、"
|
|
1312
|
-
+ "Fable 5 等只接受这种,报 400 requires adaptive thinking 就切到它。"
|
|
1313
|
-
+ "Claude 4.5 及更早只认预算式,保持「自动」。"
|
|
1314
|
-
)
|
|
1315
|
-
) : null,
|
|
1316
|
-
model.modalityField ? React.createElement("div", { className: "mcfg-reasoning" },
|
|
1317
|
-
React.createElement("span", { className: "mcfg-label" }, "输入模态(写入 " + model.modalityField + ")"),
|
|
1318
|
-
React.createElement("div", { className: "mcfg-levels" },
|
|
1319
|
-
MODALITY_CHOICES.map((choice) =>
|
|
1320
|
-
React.createElement("label", { key: choice, className: "mcfg-level" },
|
|
1321
|
-
React.createElement("input", {
|
|
1322
|
-
type: "radio",
|
|
1323
|
-
name: "modality-" + model.id,
|
|
1324
|
-
disabled: !editable,
|
|
1325
|
-
checked: model.modality === choice,
|
|
1326
|
-
onChange: () => applyChange(provider.provider, model.id, "modalities", choice),
|
|
1327
|
-
}),
|
|
1328
|
-
React.createElement("span", null, MODALITY_LABELS[choice])
|
|
1329
|
-
)
|
|
1330
|
-
)
|
|
1331
|
-
),
|
|
1332
|
-
React.createElement("div", { className: "mcfg-note" },
|
|
1333
|
-
modalityNote(model.modality, model)
|
|
1334
|
-
)
|
|
1335
|
-
) : null,
|
|
1336
|
-
model.imageAvailable ? React.createElement("div", { className: "mcfg-reasoning" },
|
|
1337
|
-
React.createElement("span", { className: "mcfg-label" }, "生图模型"),
|
|
1338
|
-
React.createElement("div", { className: "mcfg-img-actions" },
|
|
1339
|
-
React.createElement("label", { className: "mcfg-level" },
|
|
1340
|
-
React.createElement("input", {
|
|
1341
|
-
type: "checkbox",
|
|
1342
|
-
checked: model.imageLinked,
|
|
1343
|
-
disabled: !editable,
|
|
1344
|
-
onChange: () => applyChange(provider.provider, model.id, "imageLink", !model.imageLinked),
|
|
1345
|
-
}),
|
|
1346
|
-
React.createElement("span", null, "在「绘图」页自动配置该模型")
|
|
1347
|
-
),
|
|
1348
|
-
model.imageLinked ? React.createElement("span", { className: "mcfg-chip mcfg-chip-on" }, "已在绘图页配置") : null
|
|
1349
|
-
),
|
|
1350
|
-
React.createElement("div", { className: "mcfg-note" },
|
|
1351
|
-
model.imageLinked
|
|
1352
|
-
? "端点 " + (model.imageEndpoint || "(未填写)") + (model.imageApiKeyEnv ? ",凭据 " + model.imageApiKeyEnv : "")
|
|
1353
|
-
+ "。取消勾选会移除该配置;也可以直接在「绘图」页编辑或删除它。"
|
|
1354
|
-
: "勾选后写入「绘图」页的模型列表(模型 ID 复用 " + model.id + ",端点由本 provider 的供应商地址加 /images/generations 推导)。"
|
|
1355
|
-
)
|
|
1356
|
-
) : null,
|
|
1357
|
-
React.createElement("div", { className: "mcfg-fields" },
|
|
1358
|
-
React.createElement(CapacityField, {
|
|
1359
|
-
label: "上下文窗口", options: CTX_OPTIONS, value: model.contextWindow, disabled: !editable,
|
|
1360
|
-
provider: provider.provider, model: model.id, kind: "contextWindow", onApply: applyChange, onToast: showToast,
|
|
1361
|
-
}),
|
|
1362
|
-
React.createElement(CapacityField, {
|
|
1363
|
-
label: "最大输出", options: MAX_OPTIONS, value: model.maxTokens, disabled: !editable,
|
|
1364
|
-
provider: provider.provider, model: model.id, kind: "maxTokens", onApply: applyChange, onToast: showToast,
|
|
1365
|
-
})
|
|
1366
|
-
)
|
|
1367
|
-
);
|
|
1368
|
-
})
|
|
1369
|
-
)
|
|
1370
|
-
)
|
|
1371
|
-
];
|
|
1372
|
-
|
|
1373
|
-
// ── 用途页 ────────────────────────────────────────────────────────────
|
|
1374
|
-
const purposePanel = [
|
|
1375
|
-
React.createElement("div", { className: "mcfg-purpose", key: "title" },
|
|
1376
|
-
React.createElement("div", { className: "mcfg-purpose-head" },
|
|
1377
|
-
React.createElement("span", { className: "mcfg-purpose-name" }, "标题生成"),
|
|
1378
|
-
React.createElement("span", { className: "mcfg-chip" }, "独立请求"),
|
|
1379
|
-
title && title.available
|
|
1380
|
-
? React.createElement("span", { className: "mcfg-chip mcfg-chip-on" }, "已启用")
|
|
1381
|
-
: React.createElement("span", { className: "mcfg-chip" }, "宿主行未激活")
|
|
1382
|
-
),
|
|
1383
|
-
React.createElement("div", { className: "mcfg-desc" },
|
|
1384
|
-
"会话标题由一次独立的辅助请求生成,不会改动对话模型。默认继承当前会话的 provider / model。"
|
|
1385
|
-
),
|
|
1386
|
-
!titleShown.available
|
|
1387
|
-
? React.createElement("div", { className: "mcfg-state mcfg-error" },
|
|
1388
|
-
"未检测到 model-tuning 设置命名空间:宿主半区未激活(标题 provider 未注册),请检查插件是否启用并重启 dsh。"
|
|
1389
|
-
)
|
|
1390
|
-
: [
|
|
1391
|
-
React.createElement("div", { className: "mcfg-radio", key: "mode" },
|
|
1392
|
-
React.createElement("label", { className: "mcfg-radio-row" },
|
|
1393
|
-
React.createElement("input", {
|
|
1394
|
-
type: "radio",
|
|
1395
|
-
name: "title-mode",
|
|
1396
|
-
checked: titleShown.mode === "inherit",
|
|
1397
|
-
disabled: !data.writable || titleBusy,
|
|
1398
|
-
onChange: () => editTitleDraft({ mode: "inherit", provider: null, model: null }),
|
|
1399
|
-
}),
|
|
1400
|
-
React.createElement("span", null,
|
|
1401
|
-
"继承当前会话(默认)",
|
|
1402
|
-
React.createElement("div", { className: "mcfg-note" },
|
|
1403
|
-
"标题请求沿用该会话已记录的 provider / model,行为与内置标题生成一致。")
|
|
1404
|
-
)
|
|
1405
|
-
),
|
|
1406
|
-
React.createElement("label", { className: "mcfg-radio-row" },
|
|
844
|
+
model.adaptiveThinkingEditable ? React.createElement("select", {
|
|
845
|
+
className: "mcfg-mini-select",
|
|
846
|
+
title: "思考格式:只接受 adaptive thinking 的模型(Claude Opus 4.6+ / Sonnet 5 等)切到自适应,其余保持自动",
|
|
847
|
+
disabled: !editable,
|
|
848
|
+
value: model.adaptiveThinking ? "adaptive" : "auto",
|
|
849
|
+
onChange: (event) => void commit("adaptiveThinking", model.id, event.target.value === "adaptive" ? true : null),
|
|
850
|
+
},
|
|
851
|
+
React.createElement("option", { value: "auto" }, "思考:自动"),
|
|
852
|
+
React.createElement("option", { value: "adaptive" }, "思考:自适应")
|
|
853
|
+
) : null,
|
|
854
|
+
model.reasoningEditable ? React.createElement("button", {
|
|
855
|
+
type: "button",
|
|
856
|
+
className: "mcfg-mini-button" + (levelsOpen ? " mcfg-mini-button-on" : ""),
|
|
857
|
+
title: "内置目录路由的推理档位(写入 modelOverrides.<id>.reasoningEfforts):当前 "
|
|
858
|
+
+ (model.levels.length === 0 ? "无档位" : model.levels.join("/"))
|
|
859
|
+
+ ";官方 Models 页只覆盖手写路由",
|
|
860
|
+
disabled: !editable,
|
|
861
|
+
onClick: () => setOpenLevels(levelsOpen ? "" : model.id),
|
|
862
|
+
}, (model.levels.length === 0 ? "推理:无" : "推理:" + model.levels.length + " 档")
|
|
863
|
+
+ (model.hasReasoningOverride ? " ·已改" : "")) : null
|
|
864
|
+
),
|
|
865
|
+
levelsOpen ? React.createElement("div", { className: "mcfg-compat-levels" },
|
|
866
|
+
React.createElement("div", { className: "mcfg-levels" },
|
|
867
|
+
REASONING_LEVELS.map((level) => React.createElement("label", { key: level, className: "mcfg-level" },
|
|
1407
868
|
React.createElement("input", {
|
|
1408
|
-
type: "
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
disabled: !data.writable || titleBusy,
|
|
869
|
+
type: "checkbox",
|
|
870
|
+
checked: model.levels.indexOf(level) >= 0,
|
|
871
|
+
disabled: !editable,
|
|
1412
872
|
onChange: () => {
|
|
1413
|
-
const
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
model: titleShown.model || (fallback && fallback.models[0] ? fallback.models[0].id : null),
|
|
1418
|
-
});
|
|
873
|
+
const next = model.levels.slice();
|
|
874
|
+
const index = next.indexOf(level);
|
|
875
|
+
if (index >= 0) next.splice(index, 1); else next.push(level);
|
|
876
|
+
void commit("reasoningLevels", model.id, next);
|
|
1419
877
|
},
|
|
1420
878
|
}),
|
|
1421
|
-
React.createElement("span", null,
|
|
1422
|
-
|
|
1423
|
-
React.createElement("div", { className: "mcfg-note" },
|
|
1424
|
-
"标题请求改投所选 provider / model;会话的对话模型保持不变。")
|
|
1425
|
-
)
|
|
1426
|
-
)
|
|
1427
|
-
),
|
|
1428
|
-
titleShown.mode === "custom"
|
|
1429
|
-
? React.createElement("div", { className: "mcfg-fields", key: "route" },
|
|
1430
|
-
React.createElement("label", { className: "mcfg-field" },
|
|
1431
|
-
React.createElement("span", { className: "mcfg-label" }, "Provider"),
|
|
1432
|
-
React.createElement("select", {
|
|
1433
|
-
className: "mcfg-select",
|
|
1434
|
-
disabled: !data.writable || titleBusy,
|
|
1435
|
-
value: titleShown.provider || "",
|
|
1436
|
-
onChange: (e) => {
|
|
1437
|
-
const next = titleProviders.find((p) => p.provider === e.target.value);
|
|
1438
|
-
editTitleDraft({
|
|
1439
|
-
provider: e.target.value,
|
|
1440
|
-
model: next && next.models[0] ? next.models[0].id : null,
|
|
1441
|
-
});
|
|
1442
|
-
},
|
|
1443
|
-
},
|
|
1444
|
-
React.createElement("option", { value: "" }, "(请选择 provider)"),
|
|
1445
|
-
titleProviders.map((p) => React.createElement("option", { key: p.provider, value: p.provider },
|
|
1446
|
-
p.name + " · " + p.provider))
|
|
1447
|
-
)
|
|
1448
|
-
),
|
|
1449
|
-
React.createElement("label", { className: "mcfg-field" },
|
|
1450
|
-
React.createElement("span", { className: "mcfg-label" }, "Model"),
|
|
1451
|
-
React.createElement("select", {
|
|
1452
|
-
className: "mcfg-select",
|
|
1453
|
-
disabled: !data.writable || titleBusy || !titleShown.provider,
|
|
1454
|
-
value: titleShown.model || "",
|
|
1455
|
-
onChange: (e) => editTitleDraft({ model: e.target.value }),
|
|
1456
|
-
},
|
|
1457
|
-
React.createElement("option", { value: "" }, "(请选择 model)"),
|
|
1458
|
-
titleModels.map((m) => React.createElement("option", { key: m.id, value: m.id },
|
|
1459
|
-
m.name + " · " + m.id))
|
|
1460
|
-
)
|
|
1461
|
-
)
|
|
1462
|
-
)
|
|
1463
|
-
: null,
|
|
1464
|
-
React.createElement("div", { className: "mcfg-note", key: "scope" },
|
|
1465
|
-
"生效范围:保存后对本 profile 之后生成的标题生效(包含新会话与后续标题生成);不会改动已有标题、手动重命名,也不会改动会话的对话模型。"
|
|
879
|
+
React.createElement("span", null, level)
|
|
880
|
+
))
|
|
1466
881
|
),
|
|
1467
|
-
React.createElement("div", { className: "mcfg-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
React.createElement("input", {
|
|
1473
|
-
className: "mcfg-input",
|
|
1474
|
-
type: "number",
|
|
1475
|
-
min: 1,
|
|
1476
|
-
step: 1,
|
|
1477
|
-
disabled: !data.writable || titleBusy,
|
|
1478
|
-
value: titleShown[field.key] === undefined || titleShown[field.key] === null ? "" : String(titleShown[field.key]),
|
|
1479
|
-
onChange: (event) => editTitleDraft({
|
|
1480
|
-
[field.key]: event.target.value === "" ? "" : Number(event.target.value),
|
|
1481
|
-
}),
|
|
1482
|
-
}),
|
|
1483
|
-
React.createElement("span", { className: "mcfg-note" }, field.hint)
|
|
1484
|
-
))
|
|
1485
|
-
)
|
|
1486
|
-
),
|
|
1487
|
-
React.createElement("div", { className: "mcfg-note", key: "budgetNote" },
|
|
1488
|
-
"上面的数值由本页写入设置,保存后对之后生成的标题生效;清空某项会回到宿主默认值。标题请求不传递推理档位,由所选模型自己决定是否思考——因此推理模型需要更大的「输出上限」。"
|
|
1489
|
-
),
|
|
1490
|
-
React.createElement("div", { className: "mcfg-actions", key: "actions" },
|
|
1491
|
-
React.createElement("button", {
|
|
1492
|
-
type: "button",
|
|
1493
|
-
className: "mcfg-save",
|
|
1494
|
-
disabled: titleSaveDisabled || !titleDirty,
|
|
1495
|
-
onClick: saveTitle,
|
|
1496
|
-
}, titleBusy ? "正在保存…" : (titleDirty ? "保存标题设置" : "已保存")),
|
|
1497
|
-
titleDirty ? React.createElement("span", { className: "mcfg-note" }, "有未保存的修改") : null
|
|
1498
|
-
)
|
|
1499
|
-
]
|
|
1500
|
-
),
|
|
1501
|
-
React.createElement("div", { className: "mcfg-purpose", key: "image" },
|
|
1502
|
-
React.createElement("div", { className: "mcfg-purpose-head" },
|
|
1503
|
-
React.createElement("span", { className: "mcfg-purpose-name" }, "绘图"),
|
|
1504
|
-
React.createElement("span", { className: "mcfg-chip" }, "图像专有参数")
|
|
1505
|
-
),
|
|
1506
|
-
React.createElement("div", { className: "mcfg-desc" },
|
|
1507
|
-
"绘图模型、端点、尺寸与质量属于图像专有参数,请在「绘图」页签配置;本页的「生图模型」勾选会把所选模型写入那里的模型列表。"
|
|
1508
|
-
),
|
|
1509
|
-
data.imageModels && data.imageModels.length > 0
|
|
1510
|
-
? React.createElement("div", { className: "mcfg-note" },
|
|
1511
|
-
"绘图页当前 " + data.imageModels.length + " 个配置:" + data.imageModels.join("、")
|
|
1512
|
-
+ (data.imageDefault ? "(默认 " + data.imageDefault + ")" : "(未设默认,generate_image 需显式指定)"))
|
|
1513
|
-
: React.createElement("div", { className: "mcfg-note" }, "绘图页尚未配置任何模型。")
|
|
882
|
+
React.createElement("div", { className: "mcfg-note" },
|
|
883
|
+
"不勾选任何档位 = 该模型视为无推理能力;已勾选档位写入该路由的 modelOverrides,官方已存的每档 API 值保持不变。")
|
|
884
|
+
) : null
|
|
885
|
+
);
|
|
886
|
+
})
|
|
1514
887
|
)
|
|
1515
|
-
];
|
|
1516
|
-
|
|
1517
|
-
return React.createElement("div", { className: "mcfg" },
|
|
1518
|
-
React.createElement("div", { className: "mcfg-head" },
|
|
1519
|
-
React.createElement("div", { className: "mcfg-title" }, "模型调参"),
|
|
1520
|
-
React.createElement("div", { className: "mcfg-sub" },
|
|
1521
|
-
"集中配置每个模型的可选推理档位、上下文窗口、最大输出和输入模态;「用途」页管理标题生成路由;「绘图」页管理绘图模型与直接绘图。" +
|
|
1522
|
-
"修改即时保存;API 密钥与端点请在「模型」页配置。" +
|
|
1523
|
-
(data.writable ? "" : "(当前部署为只读,无法保存。)")
|
|
1524
|
-
)
|
|
1525
|
-
),
|
|
1526
|
-
tabBar,
|
|
1527
|
-
toast ? React.createElement("div", { className: "mcfg-toast " + (toast.kind === "ok" ? "mcfg-toast-ok" : "mcfg-toast-err") }, toast.text) : null,
|
|
1528
|
-
tab === "models" ? modelsPanel : null,
|
|
1529
|
-
tab === "purpose" ? purposePanel : null,
|
|
1530
|
-
tab === "image" && ownView ? React.createElement(ImagePanel, {
|
|
1531
|
-
remote: props.remote, call: props.call,
|
|
1532
|
-
writable: !!data.writable, view: ownView, refresh: reload, showToast,
|
|
1533
|
-
}) : null,
|
|
1534
|
-
tab === "image" && !ownView ? React.createElement("div", { className: "mcfg-state mcfg-error" }, "宿主行未激活,无法读写绘图配置。") : null
|
|
1535
888
|
);
|
|
889
|
+
|
|
890
|
+
return React.createElement(React.Fragment, null, toastLine, drawingSection, compatSection);
|
|
1536
891
|
}
|
|
1537
892
|
|
|
893
|
+
/** Styles the official Models card extension (collapsed summary + one compact row per model). */
|
|
894
|
+
const COMPAT_CSS = `
|
|
895
|
+
.mcfg-compat{display:block;padding:10px 0 2px;border-top:1px dashed rgba(128,128,128,.35);margin-top:12px}
|
|
896
|
+
.mcfg-compat>summary{cursor:pointer;display:flex;align-items:center;gap:8px;list-style:none;font-size:12.5px;font-weight:600}
|
|
897
|
+
.mcfg-compat>summary::-webkit-details-marker{display:none}
|
|
898
|
+
.mcfg-compat>summary::before{content:"▸";font-size:10px;opacity:.6;width:10px}
|
|
899
|
+
.mcfg-compat[open]>summary::before{content:"▾"}
|
|
900
|
+
.mcfg-compat-hint{font-size:11.5px;font-weight:400;opacity:.55}
|
|
901
|
+
.mcfg-compat-list{display:flex;flex-direction:column;gap:2px;padding-top:8px}
|
|
902
|
+
.mcfg-compat-row{display:flex;flex-direction:column;gap:6px;padding:5px 8px;border-radius:8px}
|
|
903
|
+
.mcfg-compat-row-open{background:rgba(128,128,128,.08)}
|
|
904
|
+
.mcfg-compat-line{display:flex;align-items:center;gap:8px;flex-wrap:wrap;min-height:24px}
|
|
905
|
+
.mcfg-compat-name{font-size:12.5px;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1 1 130px}
|
|
906
|
+
.mcfg-mini-select{font-size:12px;padding:3px 6px;border-radius:6px;border:1px solid rgba(128,128,128,.4);background:transparent;color:inherit}
|
|
907
|
+
.mcfg-mini-select:disabled{opacity:.5;cursor:not-allowed}
|
|
908
|
+
.mcfg-mini-button{font-size:12px;padding:3px 8px;border-radius:6px;border:1px solid rgba(128,128,128,.4);background:transparent;color:inherit;cursor:pointer;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
909
|
+
.mcfg-mini-button:hover{border-color:rgba(128,128,128,.65)}
|
|
910
|
+
.mcfg-mini-button:disabled{opacity:.5;cursor:not-allowed}
|
|
911
|
+
.mcfg-mini-button-on{border-color:rgba(80,140,255,.75);background:rgba(80,140,255,.12)}
|
|
912
|
+
.mcfg-draw-check{display:flex;align-items:center;gap:8px;padding:5px 8px;border-radius:8px;font-size:12.5px;cursor:pointer}
|
|
913
|
+
.mcfg-draw-check:hover{background:rgba(128,128,128,.08)}
|
|
914
|
+
.mcfg-draw-check input{accent-color:rgb(80,140,255)}
|
|
915
|
+
.mcfg-draw-check-name{flex:1 1 130px;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
916
|
+
.mcfg-draw-check-meta{font-size:11.5px;opacity:.55}
|
|
917
|
+
.mcfg-compat-levels{display:flex;flex-direction:column;gap:6px;padding:2px 0 4px 2px}
|
|
918
|
+
`;
|
|
919
|
+
|
|
1538
920
|
// 2026-09-08 (DSH 0.1.2-rc.1): `ctx.connection.api` no longer exists — the
|
|
1539
|
-
// client API
|
|
1540
|
-
//
|
|
1541
|
-
|
|
1542
|
-
// against a dead API surface.
|
|
1543
|
-
const inject = ["slots", "remote", "remote.llm", "remote.settings", "remote.session", "remote.credentials", "locale", "connection"];
|
|
921
|
+
// client API lives on the `remote.*` service namespace. Declaring them also
|
|
922
|
+
// defers this plugin until the mux connection is up.
|
|
923
|
+
const inject = ["slots", "remote", "remote.llm", "remote.settings", "remote.session", "remote.credentials", "connection"];
|
|
1544
924
|
|
|
1545
925
|
function apply(ctx) {
|
|
1546
926
|
const style = document.createElement("style");
|
|
1547
927
|
style.dataset.plugin = "dsh-plugin-model-tuning";
|
|
1548
|
-
style.textContent = CSS;
|
|
928
|
+
style.textContent = CSS + COMPAT_CSS;
|
|
1549
929
|
document.head.appendChild(style);
|
|
1550
930
|
ctx.effect(() => () => { style.remove(); });
|
|
1551
931
|
|
|
1552
|
-
if (ctx.locale) {
|
|
1553
|
-
// 生产 dsh-client-locale 的 LOCALE_IDS 是 ["zh", "en"];multi-locale
|
|
1554
|
-
// 形态一次写入 zh / en。ctx.effect 挂进 fiber 生命周期,HMR 再激活
|
|
1555
|
-
// 不会重复注册。
|
|
1556
|
-
ctx.effect(() => ctx.locale.register("dsh-plugin-model-tuning", {
|
|
1557
|
-
en: {
|
|
1558
|
-
"tab.models": "Model parameters",
|
|
1559
|
-
"tab.purpose": "Purposes",
|
|
1560
|
-
"tab.image": "Images",
|
|
1561
|
-
"title.name": "Session titles",
|
|
1562
|
-
"title.desc": "A session title comes from a separate auxiliary request and never changes the chat model. By default it inherits the session's provider / model.",
|
|
1563
|
-
"title.inherit": "Inherit from the current session (default)",
|
|
1564
|
-
"title.custom": "Use a specific model",
|
|
1565
|
-
"title.scope": "Scope: applying this affects titles generated afterwards in this profile. Existing titles, manual renames, and the session's chat model are left untouched.",
|
|
1566
|
-
"title.missing": "The model-tuning settings namespace is missing: the host half did not activate (no title provider registered). Check that the plugin is enabled and restart dsh.",
|
|
1567
|
-
"title.save": "Save title settings",
|
|
1568
|
-
},
|
|
1569
|
-
zh: {
|
|
1570
|
-
"tab.models": "模型参数",
|
|
1571
|
-
"tab.purpose": "用途",
|
|
1572
|
-
"tab.image": "绘图",
|
|
1573
|
-
"title.name": "标题生成",
|
|
1574
|
-
"title.desc": "会话标题由一次独立的辅助请求生成,不会改动对话模型。默认继承当前会话的 provider / model。",
|
|
1575
|
-
"title.inherit": "继承当前会话(默认)",
|
|
1576
|
-
"title.custom": "使用指定模型",
|
|
1577
|
-
"title.scope": "生效范围:保存后对本 profile 之后生成的标题生效;不会改动已有标题、手动重命名,也不会改动会话的对话模型。",
|
|
1578
|
-
"title.missing": "未检测到 model-tuning 设置命名空间:宿主半区未激活(标题 provider 未注册),请检查插件是否启用并重启 dsh。",
|
|
1579
|
-
"title.save": "保存标题设置",
|
|
1580
|
-
},
|
|
1581
|
-
}));
|
|
1582
|
-
}
|
|
1583
|
-
|
|
1584
932
|
const remote = ctx.remote;
|
|
1585
|
-
const wrap = (response, map) => response.ok
|
|
1586
|
-
? { result: { ok: true, value: map(response.value) } }
|
|
1587
|
-
: { result: { ok: false, error: response.error } };
|
|
1588
|
-
const api = {
|
|
1589
|
-
llm: {
|
|
1590
|
-
providers: async () => {
|
|
1591
|
-
const response = await remote.llm.listConfigurableProviders();
|
|
1592
|
-
return wrap(response, (value) => ({ providers: value }));
|
|
1593
|
-
},
|
|
1594
|
-
models: async () => {
|
|
1595
|
-
const response = await remote.session.modelCatalog();
|
|
1596
|
-
return wrap(response, (value) => ({ groups: value.groups }));
|
|
1597
|
-
},
|
|
1598
|
-
},
|
|
1599
|
-
settings: {
|
|
1600
|
-
describe: async () => {
|
|
1601
|
-
const response = await remote.settings.describe();
|
|
1602
|
-
return wrap(response, (value) => value);
|
|
1603
|
-
},
|
|
1604
|
-
mutate: async (args) => {
|
|
1605
|
-
const response = await remote.settings.mutate(args.ns, args.ops, args.expectedRevision);
|
|
1606
|
-
return wrap(response, () => ({}));
|
|
1607
|
-
},
|
|
1608
|
-
},
|
|
1609
|
-
};
|
|
1610
933
|
const timer = ctx.get("timer");
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
934
|
+
const unwrap = (response) => {
|
|
935
|
+
if (!response.ok) throw new Error(response.error && response.error.message ? response.error.message : "请求失败");
|
|
936
|
+
return response.value;
|
|
937
|
+
};
|
|
1615
938
|
const call = async (method, args) => unwrap(await ctx.connection.rpc.call("/api", `imageGeneration/${method}`, { args }));
|
|
1616
939
|
|
|
940
|
+
/** Provider metadata, its settings section and the model catalog, read together. */
|
|
941
|
+
async function compatSnapshot(providerId) {
|
|
942
|
+
const [providers, catalog, settings] = await Promise.all([
|
|
943
|
+
unwrap(await remote.llm.listConfigurableProviders()),
|
|
944
|
+
unwrap(await remote.session.modelCatalog()),
|
|
945
|
+
unwrap(await remote.settings.describe()),
|
|
946
|
+
]);
|
|
947
|
+
const entry = providers.find((item) => item && item.provider === providerId);
|
|
948
|
+
if (!entry) throw new Error("该 provider 不可配置");
|
|
949
|
+
const namespaces = Array.isArray(settings.namespaces) ? settings.namespaces : [];
|
|
950
|
+
const nsView = namespaces.find((item) => item && item.ns === entry.settingsNs);
|
|
951
|
+
const ownView = namespaces.find((item) => item && item.ns === NS);
|
|
952
|
+
const groups = Array.isArray(catalog.groups) ? catalog.groups : [];
|
|
953
|
+
const group = groups.find((item) => item && item.id === providerId);
|
|
954
|
+
const profile = profileOf(nsView, entry.settingsPath);
|
|
955
|
+
return {
|
|
956
|
+
entry, nsView, ownView, group, profile,
|
|
957
|
+
adaptiveKey: adaptiveThinkingCompatKey(entry.settingsNs, profile),
|
|
958
|
+
writable: settings.writable !== false,
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
/** One provider card's compat rows: the switches and the catalog-route levels. */
|
|
963
|
+
async function loadCompat(providerId) {
|
|
964
|
+
const snapshot = await compatSnapshot(providerId);
|
|
965
|
+
const entry = snapshot.entry;
|
|
966
|
+
const nsView = snapshot.nsView;
|
|
967
|
+
const models = ((snapshot.group && snapshot.group.models) || []).map((model) => {
|
|
968
|
+
const efforts = model.reasoning && Array.isArray(model.reasoning.efforts) ? model.reasoning.efforts : [];
|
|
969
|
+
const levels = efforts.map((effort) => effort.id).filter((id) => id !== "off");
|
|
970
|
+
const stored = storedReasoningEfforts(entry, nsView, model.id);
|
|
971
|
+
const devRole = entry.settingsNs === "llm-pi-ai"
|
|
972
|
+
? readCompatField(nsView, entry.settingsPath, model.id, "supportsDeveloperRole")
|
|
973
|
+
: undefined;
|
|
974
|
+
const adaptive = snapshot.adaptiveKey !== null
|
|
975
|
+
? readCompatField(nsView, entry.settingsPath, model.id, snapshot.adaptiveKey)
|
|
976
|
+
: undefined;
|
|
977
|
+
const image = computeImageModelState(snapshot.ownView, snapshot.profile, model.id);
|
|
978
|
+
const hasBaseURL = typeof snapshot.profile?.baseURL === "string" && snapshot.profile.baseURL.trim() !== "";
|
|
979
|
+
return {
|
|
980
|
+
id: model.id,
|
|
981
|
+
name: model.name,
|
|
982
|
+
supportsDeveloperRole: devRole,
|
|
983
|
+
adaptiveThinking: adaptive === true,
|
|
984
|
+
adaptiveThinkingEditable: snapshot.adaptiveKey !== null && levels.length > 0,
|
|
985
|
+
// The official Models page owns reasoning for hand-declared routes;
|
|
986
|
+
// only installed-catalog routes need this override editor.
|
|
987
|
+
reasoningEditable: entry.settingsNs === "llm-pi-ai" && entry.declared !== true,
|
|
988
|
+
levels: stored === false ? [] : levels,
|
|
989
|
+
hasReasoningOverride: stored !== undefined,
|
|
990
|
+
// Drawing link: only a model whose provider carries a base URL can
|
|
991
|
+
// derive an OpenAI-Images endpoint (and reuse its credential ref).
|
|
992
|
+
imageAvailable: image.available && hasBaseURL,
|
|
993
|
+
imageLinked: image.hasEntry,
|
|
994
|
+
imageManaged: image.managed,
|
|
995
|
+
// Count only fields this plugin actually wrote: catalog-declared
|
|
996
|
+
// reasoning levels are the model's own capability, not an override.
|
|
997
|
+
hasOverride: devRole !== undefined || adaptive !== undefined || stored !== undefined,
|
|
998
|
+
};
|
|
999
|
+
});
|
|
1000
|
+
return { writable: snapshot.writable, models };
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
/** Apply one compat edit through the provider's own settings namespace. */
|
|
1004
|
+
async function saveCompat(providerId, modelId, kind, value) {
|
|
1005
|
+
const snapshot = await compatSnapshot(providerId);
|
|
1006
|
+
const entry = snapshot.entry;
|
|
1007
|
+
const nsView = snapshot.nsView;
|
|
1008
|
+
if (kind === "imageLink") {
|
|
1009
|
+
// The drawing list lives in this plugin's own namespace; re-read it
|
|
1010
|
+
// right before writing so the plan and its revision come from one
|
|
1011
|
+
// document (the same discipline the drawing page uses).
|
|
1012
|
+
const freshSettings = unwrap(await remote.settings.describe());
|
|
1013
|
+
const namespaces = Array.isArray(freshSettings.namespaces) ? freshSettings.namespaces : [];
|
|
1014
|
+
const freshOwn = namespaces.find((item) => item && item.ns === NS);
|
|
1015
|
+
if (!freshOwn) throw new Error("model-tuning 宿主行未激活,无法写入绘图配置");
|
|
1016
|
+
const plan = value === true
|
|
1017
|
+
? planImageLink(freshOwn, snapshot.profile, modelId, providerId)
|
|
1018
|
+
: planImageUnlink(freshOwn, modelId);
|
|
1019
|
+
if (!plan || !plan.models) throw new Error((plan && plan.reason) || "无法写入绘图配置");
|
|
1020
|
+
const response = await remote.settings.mutate(NS, [
|
|
1021
|
+
{ op: "set", path: ["models"], value: plan.models },
|
|
1022
|
+
{ op: "set", path: ["defaultModel"], value: plan.defaultModel || "" },
|
|
1023
|
+
], freshOwn.revision);
|
|
1024
|
+
if (!response.ok) throw new Error(response.error && response.error.message ? response.error.message : "保存失败");
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
let write;
|
|
1028
|
+
if (kind === "supportsDeveloperRole" || kind === "adaptiveThinking") {
|
|
1029
|
+
if (value !== null && typeof value !== "boolean") throw new Error("取值必须是布尔值");
|
|
1030
|
+
const compatKey = kind === "supportsDeveloperRole" ? "supportsDeveloperRole" : snapshot.adaptiveKey;
|
|
1031
|
+
if (compatKey === null || compatKey === undefined) throw new Error("该路由不是 anthropic-messages,不能设置思考格式");
|
|
1032
|
+
write = computeCompatWriteOps(entry, nsView, modelId, compatKey, value);
|
|
1033
|
+
} else if (kind === "reasoningLevels") {
|
|
1034
|
+
const levels = Array.isArray(value) ? value : [];
|
|
1035
|
+
for (const level of levels) {
|
|
1036
|
+
if (REASONING_LEVELS.indexOf(level) < 0) throw new Error("未知的推理档位:" + level);
|
|
1037
|
+
}
|
|
1038
|
+
const payload = reasoningEffortsPayload(levels, storedReasoningEfforts(entry, nsView, modelId));
|
|
1039
|
+
write = computeWriteOps(entry, nsView, modelId, "reasoningEfforts", payload);
|
|
1040
|
+
} else {
|
|
1041
|
+
throw new Error("未知的修改类型");
|
|
1042
|
+
}
|
|
1043
|
+
if (!write) throw new Error("该 provider 不接受这个字段");
|
|
1044
|
+
const response = await remote.settings.mutate(write.ns, write.ops);
|
|
1045
|
+
if (!response.ok) throw new Error(response.error && response.error.message ? response.error.message : "保存失败");
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1617
1048
|
ctx.slots.inject("settings.section", () => ctx.slots.register(
|
|
1618
|
-
{ name: "settings.section", id: "model-tuning", order: 11, label: () => "
|
|
1619
|
-
() => React.createElement(
|
|
1049
|
+
{ name: "settings.section", id: "model-tuning", order: 11, label: () => "绘图" },
|
|
1050
|
+
() => React.createElement(DrawingPage, { remote, timer, call }),
|
|
1051
|
+
));
|
|
1052
|
+
|
|
1053
|
+
// Official seat for plugins distributed outside the repository: one
|
|
1054
|
+
// registration under the pi-ai settings namespace receives every pi-ai
|
|
1055
|
+
// provider card (shipped, added and hand-declared alike).
|
|
1056
|
+
ctx.slots.inject("settings.models.provider-card", () => ctx.slots.register(
|
|
1057
|
+
{
|
|
1058
|
+
name: "settings.models.provider-card",
|
|
1059
|
+
key: "llm-pi-ai",
|
|
1060
|
+
inject: () => ({ load: loadCompat, save: saveCompat }),
|
|
1061
|
+
},
|
|
1062
|
+
CompatCard,
|
|
1620
1063
|
));
|
|
1621
1064
|
}
|
|
1622
1065
|
|