@klarkxy/dsh-model-hub 0.1.0-rc.1 → 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/README.md +53 -13
- package/cordis.patch.yml +1 -1
- package/docs/README.zh-CN.md +41 -10
- package/lib/client.inner.cjs +1170 -165
- package/lib/client.inner.cjs.map +1 -1
- package/lib/client.js +1170 -165
- package/lib/index.d.ts +1 -1
- package/lib/index.js +673 -16
- package/lib/index.js.map +1 -1
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +20 -8
package/lib/client.inner.cjs
CHANGED
|
@@ -124,6 +124,21 @@ function modelMenuEffortOptions(choice, current) {
|
|
|
124
124
|
}
|
|
125
125
|
//#endregion
|
|
126
126
|
//#region ../dsh-model-route/src/menu.ts
|
|
127
|
+
/** Localize standard effort ids only when the provider has not supplied a display name. */
|
|
128
|
+
function modelMenuEffortLabel(effort, locale) {
|
|
129
|
+
if (effort.name !== effort.id) return effort.name;
|
|
130
|
+
return {
|
|
131
|
+
off: ["关闭", "Off"],
|
|
132
|
+
none: ["无", "None"],
|
|
133
|
+
minimal: ["最低", "Minimal"],
|
|
134
|
+
low: ["低", "Low"],
|
|
135
|
+
medium: ["中", "Medium"],
|
|
136
|
+
high: ["高", "High"],
|
|
137
|
+
xhigh: ["极高", "Extra high"],
|
|
138
|
+
max: ["最高", "Maximum"],
|
|
139
|
+
ultra: ["超高", "Ultra"]
|
|
140
|
+
}[effort.id]?.[locale?.toLowerCase().startsWith("zh") ? 0 : 1] ?? effort.name;
|
|
141
|
+
}
|
|
127
142
|
/** The short model label shown inside a provider group. */
|
|
128
143
|
function modelMenuShortName(choice) {
|
|
129
144
|
const sep = choice.label.indexOf(" / ");
|
|
@@ -174,7 +189,23 @@ const MEASURE_STYLE = {
|
|
|
174
189
|
top: 0
|
|
175
190
|
};
|
|
176
191
|
function ModelMenu(props) {
|
|
177
|
-
const
|
|
192
|
+
const language = props.locale?.toLowerCase().startsWith("zh") ? "zh" : "en";
|
|
193
|
+
const copy = language === "zh" ? {
|
|
194
|
+
model: "模型",
|
|
195
|
+
effort: "思考强度",
|
|
196
|
+
default: "默认",
|
|
197
|
+
search: "搜索模型…",
|
|
198
|
+
empty: "没有匹配的模型",
|
|
199
|
+
clear: "清除搜索"
|
|
200
|
+
} : {
|
|
201
|
+
model: "Model",
|
|
202
|
+
effort: "Reasoning",
|
|
203
|
+
default: "Default",
|
|
204
|
+
search: "Search models…",
|
|
205
|
+
empty: "No matching models",
|
|
206
|
+
clear: "Clear search"
|
|
207
|
+
};
|
|
208
|
+
const { open, onOpenChange, anchor, choices, selected, onPick, leading = [], onPickLeading, efforts = [], selectedEffort, onPickEffort, modelLabel = copy.model, modelValue = "—", effortLabel = copy.effort, effortValue, defaultEffortLabel = copy.default, searchPlaceholder = copy.search, emptyLabel = copy.empty, clearSearchLabel = copy.clear, className, listClassName } = props;
|
|
178
209
|
const rootRef = (0, react.useRef)(null);
|
|
179
210
|
const openRef = (0, react.useRef)(open);
|
|
180
211
|
openRef.current = open;
|
|
@@ -195,7 +226,14 @@ function ModelMenu(props) {
|
|
|
195
226
|
const effortRows = (0, react.useMemo)(() => [{
|
|
196
227
|
id: "",
|
|
197
228
|
name: defaultEffortLabel
|
|
198
|
-
}, ...efforts
|
|
229
|
+
}, ...efforts.map((effort) => ({
|
|
230
|
+
...effort,
|
|
231
|
+
name: modelMenuEffortLabel(effort, language)
|
|
232
|
+
}))], [
|
|
233
|
+
defaultEffortLabel,
|
|
234
|
+
efforts,
|
|
235
|
+
language
|
|
236
|
+
]);
|
|
199
237
|
const filteredGroups = (0, react.useMemo)(() => groupModelMenuChoices(choices, "").map((group) => ({
|
|
200
238
|
...group,
|
|
201
239
|
items: (0, _deepseek_ai_dsh_client_ui_primitives.rankByName)(group.items.map((choice) => ({
|
|
@@ -447,6 +485,7 @@ function ModelMenu(props) {
|
|
|
447
485
|
style: menuPos ?? MEASURE_STYLE,
|
|
448
486
|
role: pane === "model" ? "group" : "menu",
|
|
449
487
|
"aria-label": modelLabel,
|
|
488
|
+
lang: language,
|
|
450
489
|
children: [
|
|
451
490
|
pane === "root" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
452
491
|
ref: itemRef(),
|
|
@@ -1068,7 +1107,7 @@ ${scope} {
|
|
|
1068
1107
|
border: 0.5px solid var(--dsw-alias-border-l3);
|
|
1069
1108
|
border-radius: var(--dsw-radius-md);
|
|
1070
1109
|
background: var(--dsw-alias-bg-layer-1);
|
|
1071
|
-
font-family: var(--ds-font-family-code
|
|
1110
|
+
font-family: var(--ds-font-family-code);
|
|
1072
1111
|
font-size: 12px;
|
|
1073
1112
|
line-height: 20px;
|
|
1074
1113
|
color: var(--dsw-alias-label-secondary);
|
|
@@ -1144,80 +1183,173 @@ ${scope} {
|
|
|
1144
1183
|
`.replace(/^& (\.dsh-ui-[a-z0-9-]+)(?![+>~])/gm, (_match, className) => `&${className}, & ${className}`);
|
|
1145
1184
|
}
|
|
1146
1185
|
//#endregion
|
|
1147
|
-
//#region src/
|
|
1186
|
+
//#region ../dsh-plugin-kit/src/contracts.ts
|
|
1148
1187
|
/**
|
|
1149
|
-
*
|
|
1150
|
-
*
|
|
1151
|
-
*
|
|
1152
|
-
*
|
|
1153
|
-
* revision. An empty route means "follow the host default" and unsets the
|
|
1154
|
-
* field.
|
|
1188
|
+
* Remote contribution for model-settings change notifications.
|
|
1189
|
+
* Provided by `@klarkxy/dsh-model-hub` (`ModelSettingsChangesController`);
|
|
1190
|
+
* subscribers degrade silently when that package is not installed.
|
|
1191
|
+
* The stream carries invalidations only — `ready` is never a settings snapshot.
|
|
1155
1192
|
*/
|
|
1156
|
-
const
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
title: "模型枢纽",
|
|
1169
|
-
intro: "一页查看并修改各插件的模型路由;改动立即写入对应插件自己的设置。",
|
|
1170
|
-
loading: "正在读取…",
|
|
1171
|
-
empty: "没有检测到模型字段。插件可用 x-model-route 标记声明模型字段。",
|
|
1172
|
-
failed: (message) => `读取失败:${message}`,
|
|
1173
|
-
writeFailed: (message) => `写入失败:${message}`,
|
|
1174
|
-
refresh: "刷新",
|
|
1175
|
-
follow: "跟随宿主默认",
|
|
1176
|
-
viaMarker: "契约标记",
|
|
1177
|
-
viaShape: "形状识别",
|
|
1178
|
-
model: "模型",
|
|
1179
|
-
effort: "思考强度",
|
|
1180
|
-
effortDefault: "默认",
|
|
1181
|
-
searchModels: "搜索模型…",
|
|
1182
|
-
noModels: "没有匹配的模型",
|
|
1183
|
-
clearSearch: "清除搜索",
|
|
1184
|
-
collectionTemplate: "集合中没有可编辑的条目;请先在所属插件中添加条目。"
|
|
1185
|
-
},
|
|
1186
|
-
en: {
|
|
1187
|
-
title: "Model Hub",
|
|
1188
|
-
intro: "View and edit every plugin's model route on one page; changes write into the owning plugin's own settings at once.",
|
|
1189
|
-
loading: "Loading…",
|
|
1190
|
-
empty: "No model fields detected. A plugin can declare one with the x-model-route marker.",
|
|
1191
|
-
failed: (message) => `Load failed: ${message}`,
|
|
1192
|
-
writeFailed: (message) => `Write failed: ${message}`,
|
|
1193
|
-
refresh: "Refresh",
|
|
1194
|
-
follow: "Follow host default",
|
|
1195
|
-
viaMarker: "contract marker",
|
|
1196
|
-
viaShape: "shape match",
|
|
1197
|
-
model: "Model",
|
|
1198
|
-
effort: "Reasoning",
|
|
1199
|
-
effortDefault: "Default",
|
|
1200
|
-
searchModels: "Search models…",
|
|
1201
|
-
noModels: "No matching models",
|
|
1202
|
-
clearSearch: "Clear search",
|
|
1203
|
-
collectionTemplate: "No collection item to edit. Add an item in the owning plugin first."
|
|
1204
|
-
}
|
|
1193
|
+
const MODEL_SETTINGS_CHANGES_REMOTE = {
|
|
1194
|
+
package: "@klarkxy/dsh-model-hub",
|
|
1195
|
+
descriptors: [{
|
|
1196
|
+
id: "modelSettingsChanges/watch",
|
|
1197
|
+
service: "modelSettingsChangesController",
|
|
1198
|
+
namespace: "modelSettingsChanges",
|
|
1199
|
+
method: "watch",
|
|
1200
|
+
mode: "stream",
|
|
1201
|
+
invocation: { kind: "direct" },
|
|
1202
|
+
parameters: [],
|
|
1203
|
+
cancellation: { parameter: "signal" }
|
|
1204
|
+
}]
|
|
1205
1205
|
};
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1206
|
+
//#endregion
|
|
1207
|
+
//#region ../dsh-plugin-kit/src/model-settings-client.ts
|
|
1208
|
+
/** This stream carries invalidations only; ready is never a settings snapshot. */
|
|
1209
|
+
function parseModelSettingsChangeFrame(value) {
|
|
1210
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Invalid model settings change frame");
|
|
1211
|
+
const frame = value;
|
|
1212
|
+
if (frame.kind === "ready" && Object.keys(frame).length === 1) return { kind: "ready" };
|
|
1213
|
+
if (frame.kind === "changed" && Object.keys(frame).length === 3 && typeof frame.ns === "string" && frame.ns.length > 0 && typeof frame.revision === "number" && Number.isSafeInteger(frame.revision) && frame.revision >= 0) return {
|
|
1214
|
+
kind: "changed",
|
|
1215
|
+
ns: frame.ns,
|
|
1216
|
+
revision: frame.revision
|
|
1217
|
+
};
|
|
1218
|
+
throw new Error("Invalid model settings change frame");
|
|
1213
1219
|
}
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1220
|
+
/** One contribution mounted by Model Hub; consumers only inject its service. */
|
|
1221
|
+
const modelSettingsChangesRemote = {
|
|
1222
|
+
package: MODEL_SETTINGS_CHANGES_REMOTE.package,
|
|
1223
|
+
descriptors: MODEL_SETTINGS_CHANGES_REMOTE.descriptors.map((descriptor) => ({
|
|
1224
|
+
...descriptor,
|
|
1225
|
+
result: {
|
|
1226
|
+
mode: "strict",
|
|
1227
|
+
typeSymbol: "ModelSettings.ChangeFrame",
|
|
1228
|
+
create: () => ({ parse: parseModelSettingsChangeFrame })
|
|
1229
|
+
}
|
|
1230
|
+
}))
|
|
1231
|
+
};
|
|
1232
|
+
/** Native RemoteStream owns carrier reconnects; no timers or browser transport.
|
|
1233
|
+
* A factory shares one logical stream while at least one subscriber remains. */
|
|
1234
|
+
function createModelSettingsSubscriber(ctx, namespace) {
|
|
1235
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1236
|
+
let injection;
|
|
1237
|
+
let current;
|
|
1238
|
+
let lifecycle = 0;
|
|
1239
|
+
let failureMessage;
|
|
1240
|
+
const report = (message) => {
|
|
1241
|
+
failureMessage = message;
|
|
1242
|
+
for (const listener of [...listeners]) try {
|
|
1243
|
+
listener.error?.(message);
|
|
1244
|
+
} catch {}
|
|
1245
|
+
};
|
|
1246
|
+
const invalidate = () => {
|
|
1247
|
+
for (const listener of [...listeners]) {
|
|
1248
|
+
if (!listeners.has(listener)) continue;
|
|
1249
|
+
try {
|
|
1250
|
+
listener.change();
|
|
1251
|
+
} catch (error) {
|
|
1252
|
+
try {
|
|
1253
|
+
listener.error?.(error instanceof Error ? error.message : String(error));
|
|
1254
|
+
} catch {}
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
};
|
|
1258
|
+
const start = () => {
|
|
1259
|
+
const life = ++lifecycle;
|
|
1260
|
+
injection = ctx.inject(["remote", "remote.modelSettingsChanges"], (injected) => {
|
|
1261
|
+
if (life !== lifecycle || !listeners.size) return;
|
|
1262
|
+
const scope = injected;
|
|
1263
|
+
const stream = scope.remote.$stream({
|
|
1264
|
+
name: "Model settings sync",
|
|
1265
|
+
open: (signal) => scope.remote.modelSettingsChanges.watch(signal),
|
|
1266
|
+
ended: (accepted) => /* @__PURE__ */ new Error(`Model settings sync stream ended ${accepted ? "after" : "before"} ready; refresh or reopen the page`)
|
|
1267
|
+
});
|
|
1268
|
+
const session = {
|
|
1269
|
+
stream,
|
|
1270
|
+
active: true
|
|
1271
|
+
};
|
|
1272
|
+
current = session;
|
|
1273
|
+
scope.effect(() => () => {
|
|
1274
|
+
session.active = false;
|
|
1275
|
+
if (current === session) {
|
|
1276
|
+
current = void 0;
|
|
1277
|
+
if (life === lifecycle && listeners.size) report("Model settings sync service unavailable");
|
|
1278
|
+
}
|
|
1279
|
+
return stream.dispose();
|
|
1280
|
+
});
|
|
1281
|
+
(async () => {
|
|
1282
|
+
let generation;
|
|
1283
|
+
let ready = false;
|
|
1284
|
+
try {
|
|
1285
|
+
for await (const item of stream) {
|
|
1286
|
+
if (!session.active || life !== lifecycle || !listeners.size) break;
|
|
1287
|
+
if (item.signal.aborted) continue;
|
|
1288
|
+
if (generation !== item.generation) {
|
|
1289
|
+
generation = item.generation;
|
|
1290
|
+
ready = false;
|
|
1291
|
+
}
|
|
1292
|
+
const frame = parseModelSettingsChangeFrame(item.value);
|
|
1293
|
+
if (frame.kind === "ready") {
|
|
1294
|
+
ready = true;
|
|
1295
|
+
failureMessage = void 0;
|
|
1296
|
+
item.accept();
|
|
1297
|
+
invalidate();
|
|
1298
|
+
} else {
|
|
1299
|
+
if (!ready) throw new Error("Model settings sync change arrived before ready");
|
|
1300
|
+
if (namespace === "*" || frame.ns === namespace) invalidate();
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
} catch (error) {
|
|
1304
|
+
if (session.active && life === lifecycle) report(error instanceof Error ? error.message : String(error));
|
|
1305
|
+
} finally {
|
|
1306
|
+
if (current === session) current = void 0;
|
|
1307
|
+
session.active = false;
|
|
1308
|
+
await stream.dispose().catch(() => {});
|
|
1309
|
+
}
|
|
1310
|
+
})();
|
|
1311
|
+
});
|
|
1312
|
+
queueMicrotask(() => {
|
|
1313
|
+
if (life === lifecycle && listeners.size && !current) report("Model settings sync service unavailable");
|
|
1314
|
+
});
|
|
1315
|
+
};
|
|
1316
|
+
return Object.freeze({ subscribe(onChange, onError) {
|
|
1317
|
+
const listener = {
|
|
1318
|
+
change: onChange,
|
|
1319
|
+
error: onError
|
|
1320
|
+
};
|
|
1321
|
+
listeners.add(listener);
|
|
1322
|
+
if (!injection) start();
|
|
1323
|
+
else if (failureMessage) queueMicrotask(() => {
|
|
1324
|
+
if (listeners.has(listener) && failureMessage) try {
|
|
1325
|
+
listener.error?.(failureMessage);
|
|
1326
|
+
} catch {}
|
|
1327
|
+
});
|
|
1328
|
+
return () => {
|
|
1329
|
+
if (!listeners.delete(listener) || listeners.size) return;
|
|
1330
|
+
lifecycle++;
|
|
1331
|
+
if (current) {
|
|
1332
|
+
current.active = false;
|
|
1333
|
+
current.stream.dispose().catch(() => {});
|
|
1334
|
+
}
|
|
1335
|
+
current = void 0;
|
|
1336
|
+
failureMessage = void 0;
|
|
1337
|
+
const previous = injection;
|
|
1338
|
+
injection = void 0;
|
|
1339
|
+
try {
|
|
1340
|
+
Promise.resolve(previous?.dispose()).catch(() => {});
|
|
1341
|
+
} catch {}
|
|
1342
|
+
};
|
|
1343
|
+
} });
|
|
1217
1344
|
}
|
|
1345
|
+
//#endregion
|
|
1346
|
+
//#region src/client-model-menu.tsx
|
|
1218
1347
|
/** One detected field's standard editor: the same ModelMenu every marked field gets. */
|
|
1219
|
-
function FieldMenu({ t, catalog, current, disabled, onSet }) {
|
|
1348
|
+
function FieldMenu({ t, catalog, current, disabled, label, allowFollow = true, followLabel, reasoningEffort = true, onSet }) {
|
|
1220
1349
|
const [open, setOpen] = (0, react.useState)(false);
|
|
1350
|
+
(0, react.useEffect)(() => {
|
|
1351
|
+
if (disabled) setOpen(false);
|
|
1352
|
+
}, [disabled]);
|
|
1221
1353
|
const route = normalizeModelRoute(current);
|
|
1222
1354
|
const follow = !route.provider || !route.model;
|
|
1223
1355
|
const choices = (0, react.useMemo)(() => parseModelMenuChoices(catalog, follow ? void 0 : route), [
|
|
@@ -1227,19 +1359,22 @@ function FieldMenu({ t, catalog, current, disabled, onSet }) {
|
|
|
1227
1359
|
route.model
|
|
1228
1360
|
]);
|
|
1229
1361
|
const selected = follow ? void 0 : choices.find((choice) => choice.provider === route.provider && choice.model === route.model);
|
|
1230
|
-
const efforts = follow ? [] : modelMenuEffortOptions(selected, route.reasoningEffort || "");
|
|
1231
|
-
const currentLabel = follow ? t("follow") : selected?.label ?? `${route.provider} / ${route.model}`;
|
|
1232
|
-
const
|
|
1362
|
+
const efforts = follow || !reasoningEffort ? [] : modelMenuEffortOptions(selected, route.reasoningEffort || "");
|
|
1363
|
+
const currentLabel = follow ? followLabel || t("follow") : selected?.label ?? `${route.provider} / ${route.model}`;
|
|
1364
|
+
const selectedEffort = efforts.find((item) => item.id === (route.reasoningEffort || ""));
|
|
1365
|
+
const effortName = selectedEffort ? modelMenuEffortLabel(selectedEffort, t.language) : void 0;
|
|
1233
1366
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelMenu, {
|
|
1234
|
-
|
|
1235
|
-
|
|
1367
|
+
locale: t.language,
|
|
1368
|
+
open: open && !disabled,
|
|
1369
|
+
onOpenChange: (next) => setOpen(next && !disabled),
|
|
1236
1370
|
listClassName: "dsh-model-hub",
|
|
1237
1371
|
anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1238
1372
|
type: "button",
|
|
1239
1373
|
className: "dsh-model-menu-trigger",
|
|
1240
1374
|
disabled,
|
|
1375
|
+
"aria-label": `${label}: ${currentLabel}`,
|
|
1241
1376
|
"aria-haspopup": "menu",
|
|
1242
|
-
"aria-expanded": open,
|
|
1377
|
+
"aria-expanded": open && !disabled,
|
|
1243
1378
|
children: [
|
|
1244
1379
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1245
1380
|
className: "dsh-model-menu-triggerLabel",
|
|
@@ -1261,11 +1396,11 @@ function FieldMenu({ t, catalog, current, disabled, onSet }) {
|
|
|
1261
1396
|
provider: picked.provider,
|
|
1262
1397
|
model: picked.model
|
|
1263
1398
|
}),
|
|
1264
|
-
leading: [{
|
|
1399
|
+
leading: allowFollow ? [{
|
|
1265
1400
|
id: "follow",
|
|
1266
|
-
label: t("follow"),
|
|
1401
|
+
label: followLabel || t("follow"),
|
|
1267
1402
|
selected: follow
|
|
1268
|
-
}],
|
|
1403
|
+
}] : [],
|
|
1269
1404
|
onPickLeading: () => onSet({
|
|
1270
1405
|
provider: "",
|
|
1271
1406
|
model: ""
|
|
@@ -1289,129 +1424,991 @@ function FieldMenu({ t, catalog, current, disabled, onSet }) {
|
|
|
1289
1424
|
clearSearchLabel: t("clearSearch")
|
|
1290
1425
|
});
|
|
1291
1426
|
}
|
|
1427
|
+
function messageOf(error) {
|
|
1428
|
+
if (error instanceof Error) return error.message;
|
|
1429
|
+
if (error && typeof error === "object" && "message" in error) return String(error.message);
|
|
1430
|
+
return String(error);
|
|
1431
|
+
}
|
|
1432
|
+
function checkedCatalog(value) {
|
|
1433
|
+
if (value && typeof value === "object" && "ok" in value && value.ok === false) throw new Error("error" in value ? messageOf(value.error) : "Request failed");
|
|
1434
|
+
return value;
|
|
1435
|
+
}
|
|
1436
|
+
function catalogFailures(value) {
|
|
1437
|
+
if (!value || typeof value !== "object") return [];
|
|
1438
|
+
const root = "ok" in value && value.ok === true && "value" in value ? value.value : value;
|
|
1439
|
+
if (!root || typeof root !== "object" || !("failures" in root) || !Array.isArray(root.failures)) return [];
|
|
1440
|
+
return root.failures.map((failure) => {
|
|
1441
|
+
if (!failure || typeof failure !== "object") return messageOf(failure);
|
|
1442
|
+
const provider = "name" in failure ? failure.name : "id" in failure ? failure.id : "provider" in failure ? failure.provider : "";
|
|
1443
|
+
return provider ? `${provider}: ${messageOf(failure)}` : messageOf(failure);
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1446
|
+
//#endregion
|
|
1447
|
+
//#region src/client-copy.ts
|
|
1448
|
+
function hubText(value, language) {
|
|
1449
|
+
return typeof value === "string" ? value : value?.[language] ?? "";
|
|
1450
|
+
}
|
|
1451
|
+
/** Local validation defaults, without rewriting provider diagnostics. */
|
|
1452
|
+
const hubMessages = [
|
|
1453
|
+
["请求失败", "Request failed"],
|
|
1454
|
+
["字段引用无效", "Invalid field reference"],
|
|
1455
|
+
["请先刷新,以获取有效的设置版本", "A valid expected revision is required; refresh and retry"],
|
|
1456
|
+
["模型路由无效", "Invalid model route"],
|
|
1457
|
+
["模型设置提供方已变化,请刷新后重试", "Model settings owner changed; refresh and retry"],
|
|
1458
|
+
["设置已变化,请刷新后重试", "Settings changed; refresh and retry"],
|
|
1459
|
+
["此模型字段不可编辑", "Not an editable model field"],
|
|
1460
|
+
["此设置需要选择具体模型", "This model setting requires a concrete model"],
|
|
1461
|
+
["模型设置提供方已卸载,请刷新后重试", "Model settings owner unloaded; refresh and retry"],
|
|
1462
|
+
["自测字段仅用于诊断", "Self-test fields are diagnostic only"],
|
|
1463
|
+
["提供方设置版本无效", "Invalid owner revision"],
|
|
1464
|
+
["未检测到此模型字段", "Not a detected model field"],
|
|
1465
|
+
["没有可编辑的集合条目", "No collection item to edit"],
|
|
1466
|
+
["设置版本无效", "Invalid revision"],
|
|
1467
|
+
["ChatGPT 需要 Codex 模型", "ChatGPT requires a Codex model"],
|
|
1468
|
+
["Codex 模型目录不可用。", "Codex model catalog is unavailable."],
|
|
1469
|
+
["配置已在别处修改,请刷新后重试。", "Settings changed elsewhere. Refresh and try again."],
|
|
1470
|
+
["设置已更新,请刷新后重试。", "Settings changed. Refresh and try again."],
|
|
1471
|
+
["正在保存配置,请稍后重试。", "Settings are being saved. Try again shortly."],
|
|
1472
|
+
["ChatGPT 插件已卸载。", "The ChatGPT plugin has been unloaded."],
|
|
1473
|
+
["配置格式无效。", "Invalid settings format."],
|
|
1474
|
+
["请等当前调用完成后保存配置。", "Wait for the current call to finish before saving settings."],
|
|
1475
|
+
["无法保存标题设置。", "Could not save title settings."],
|
|
1476
|
+
["标题设置无效。", "Invalid title settings."],
|
|
1477
|
+
["模型设置同步服务不可用", "Model settings sync service unavailable"],
|
|
1478
|
+
["模型设置变更消息无效", "Invalid model settings change frame"],
|
|
1479
|
+
["模型设置同步尚未就绪便收到变更消息", "Model settings sync change arrived before ready"],
|
|
1480
|
+
["模型设置同步在就绪后中断;请刷新或重新打开页面", "Model settings sync stream ended after ready; refresh or reopen the page"],
|
|
1481
|
+
["模型设置同步在就绪前中断;请刷新或重新打开页面", "Model settings sync stream ended before ready; refresh or reopen the page"],
|
|
1482
|
+
["Git 提交模型设置字段不存在", "Unknown Git Commit model setting"],
|
|
1483
|
+
["Git 提交插件已停用", "dsh-git-commit: plugin disabled"],
|
|
1484
|
+
["Git 提交设置已变化;请重新加载后重试", "dsh-git-commit: settings changed; reload and retry"],
|
|
1485
|
+
["安全自动模型设置字段不存在", "Unknown Safe Auto model setting"],
|
|
1486
|
+
["设置已变化;请重新加载后重试", "Settings changed; reload and retry"],
|
|
1487
|
+
["请求无效", "Invalid request"],
|
|
1488
|
+
["此处只能编辑审查设置", "Only reviewer settings may be edited here"],
|
|
1489
|
+
["配置必须是对象", "config must be an object"],
|
|
1490
|
+
["启用状态必须是布尔值", "enabled must be a boolean"],
|
|
1491
|
+
["静态只读开关必须是布尔值", "staticReadonly must be a boolean"],
|
|
1492
|
+
["审查模型供应商或模型标识无效", "invalid reviewer provider/model identifier"],
|
|
1493
|
+
["供应商和模型必须一起配置", "provider and model must be configured together"],
|
|
1494
|
+
["设置思考强度需要先选择审查模型", "reasoningEffort requires a reviewer model"],
|
|
1495
|
+
["所选模型不支持该思考强度", "The selected model does not support this reasoning effort"],
|
|
1496
|
+
["启用状态无效", "Invalid enabled state"],
|
|
1497
|
+
["找不到原生子代理模型选择设置", "native subagent-model-selection-settings not found"],
|
|
1498
|
+
["路由需要非空的供应商和模型标识", "A route requires non-empty provider and model ids"],
|
|
1499
|
+
["供应商和模型标识最长 250 字符", "Route provider and model ids are limited to 250 characters"],
|
|
1500
|
+
["路由思考强度必须是非空字符串", "A route reasoning effort must be a non-empty string"],
|
|
1501
|
+
["路由思考强度最长 250 字符", "Route reasoning effort is limited to 250 characters"],
|
|
1502
|
+
["无效的路由下标", "Invalid route index"],
|
|
1503
|
+
["没有 profile 目录,无法保存任务倾向", "No profile directory; cast tendencies cannot be stored"],
|
|
1504
|
+
["任务倾向需要非空的供应商和模型标识", "A tendency requires non-empty provider and model ids"],
|
|
1505
|
+
["任务倾向必须是字符串", "A tendency must be a string"],
|
|
1506
|
+
["任务倾向最长 500 字符", "A tendency is limited to 500 characters"],
|
|
1507
|
+
["添加模型后才能启用子代理阵容", "Add a model before enabling subagent cast"],
|
|
1508
|
+
["任务倾向已变化,请刷新后重试", "Cast tendencies changed; refresh and retry"],
|
|
1509
|
+
["任务倾向版本已达上限", "Cast tendency revision limit reached"],
|
|
1510
|
+
["无法读取任务倾向文件;请恢复 cast.json 的访问权限后刷新", "Cast tendency store cannot be read; restore access to cast.json and refresh"],
|
|
1511
|
+
["任务倾向文件损坏;请修复 cast.json 后刷新", "Cast tendency store is damaged; repair cast.json and refresh"],
|
|
1512
|
+
["任务倾向文件版本不受支持;请使用兼容的插件再保存", "Cast tendency store version is unsupported; use a compatible plugin before saving"],
|
|
1513
|
+
["任务倾向文件格式无效;请修复 cast.json 后刷新", "Cast tendency store schema is invalid; repair cast.json and refresh"],
|
|
1514
|
+
...[
|
|
1515
|
+
"provider",
|
|
1516
|
+
"model",
|
|
1517
|
+
"reasoningEffort",
|
|
1518
|
+
"reviewerPrompt"
|
|
1519
|
+
].map((key) => [`${key} 必须是字符串`, `${key} must be a string`]),
|
|
1520
|
+
...[
|
|
1521
|
+
"timeoutMs",
|
|
1522
|
+
"maxInputBytes",
|
|
1523
|
+
"outputTokens",
|
|
1524
|
+
"maxReviewsPerTask",
|
|
1525
|
+
"consecutiveDenials",
|
|
1526
|
+
"humanApprovalTimeoutMs"
|
|
1527
|
+
].map((key) => [`${key} 无效`, `invalid ${key}`])
|
|
1528
|
+
];
|
|
1529
|
+
function hubMessage(message, language) {
|
|
1530
|
+
return hubMessages.find((pair) => pair.includes(message))?.[language === "zh" ? 0 : 1] ?? message;
|
|
1531
|
+
}
|
|
1532
|
+
//#endregion
|
|
1533
|
+
//#region src/client-cast.tsx
|
|
1534
|
+
/**
|
|
1535
|
+
* dsh-model-hub — subagent cast tab. Lists the allowlist of models subagents
|
|
1536
|
+
* may pick from, each with an optional free-text task tendency the main agent
|
|
1537
|
+
* consults when delegating. Every write goes through the hub host's cast RPC
|
|
1538
|
+
* with the native settings revision as CAS; `cast.setTendency` uses its own
|
|
1539
|
+
* durable file revision. The page owns its own fetch,
|
|
1540
|
+
* subscription and write locking, following the HubPage RequestScope pattern.
|
|
1541
|
+
*/
|
|
1542
|
+
const TEXT$1 = {
|
|
1543
|
+
zh: {
|
|
1544
|
+
title: "子代理阵容",
|
|
1545
|
+
enabledLabel: "启用子代理阵容",
|
|
1546
|
+
enabledHint: "启用后,主智能体显式为子代理选择模型时须使用此列表;不指定模型时仍继承父会话。任务倾向供委派时参考。",
|
|
1547
|
+
loading: "正在读取…",
|
|
1548
|
+
failed: (message) => `读取失败:${message}`,
|
|
1549
|
+
syncFailed: (message) => `自动同步不可用:${message}`,
|
|
1550
|
+
catalogFailed: (message) => `模型列表读取失败:${message}`,
|
|
1551
|
+
writeFailed: (message) => `未保存:${message}。请刷新后重试。`,
|
|
1552
|
+
saved: "已保存,正在刷新…",
|
|
1553
|
+
savedRefreshFailed: (message) => `已保存;阵容刷新失败:${message}。请刷新以读取最新设置。`,
|
|
1554
|
+
conflict: "阵容设置已在别处修改,当前输入保留。请刷新核对后重试。",
|
|
1555
|
+
tendencyFailed: (message) => `任务倾向不可用:${message}。原文件已保留;修复后刷新。`,
|
|
1556
|
+
draftChanged: (saved) => `此任务倾向已在别处更新。当前已保存:${saved || "(空)"}。输入已保留;刷新核对后再保存。`,
|
|
1557
|
+
refresh: "刷新",
|
|
1558
|
+
refreshing: "正在刷新…",
|
|
1559
|
+
saving: "正在保存…",
|
|
1560
|
+
empty: "先添加模型,再启用显式模型选择。移除最后一个模型会关闭开关。",
|
|
1561
|
+
addModel: "添加模型…",
|
|
1562
|
+
remove: "移除",
|
|
1563
|
+
removeRoute: (label) => `移除 ${label}`,
|
|
1564
|
+
tendencyLabel: (label) => `${label} 的任务倾向`,
|
|
1565
|
+
tendencyPlaceholder: "例如:代码审查、快速问答…",
|
|
1566
|
+
routeExists: (route) => `路由 ${route} 已在列表中`,
|
|
1567
|
+
follow: "跟随宿主默认",
|
|
1568
|
+
model: "模型",
|
|
1569
|
+
effort: "思考强度",
|
|
1570
|
+
effortDefault: "默认",
|
|
1571
|
+
searchModels: "搜索模型…",
|
|
1572
|
+
noModels: "没有匹配的模型",
|
|
1573
|
+
clearSearch: "清除搜索"
|
|
1574
|
+
},
|
|
1575
|
+
en: {
|
|
1576
|
+
title: "Subagent Cast",
|
|
1577
|
+
enabledLabel: "Enable subagent cast",
|
|
1578
|
+
enabledHint: "When enabled, explicit model selection for a subagent must use this list; omitting a model still inherits the parent session. The main agent consults task tendencies when delegating.",
|
|
1579
|
+
loading: "Loading…",
|
|
1580
|
+
failed: (message) => `Load failed: ${message}`,
|
|
1581
|
+
syncFailed: (message) => `Live sync unavailable: ${message}`,
|
|
1582
|
+
catalogFailed: (message) => `Model list failed: ${message}`,
|
|
1583
|
+
writeFailed: (message) => `Not saved: ${message}. Refresh, then try again.`,
|
|
1584
|
+
saved: "Saved; refreshing…",
|
|
1585
|
+
savedRefreshFailed: (message) => `Saved; cast refresh failed: ${message}. Refresh to read the latest settings.`,
|
|
1586
|
+
conflict: "Cast settings changed elsewhere; your draft is preserved. Refresh, review, then try again.",
|
|
1587
|
+
tendencyFailed: (message) => `Task tendencies unavailable: ${message}. The original file is preserved; repair it, then refresh.`,
|
|
1588
|
+
draftChanged: (saved) => `This tendency changed elsewhere. Currently saved: ${saved || "(empty)"}. Your draft is preserved; refresh and review before saving.`,
|
|
1589
|
+
refresh: "Refresh",
|
|
1590
|
+
refreshing: "Refreshing…",
|
|
1591
|
+
saving: "Saving…",
|
|
1592
|
+
empty: "Add a model before enabling explicit model selection. Removing the last model turns the switch off.",
|
|
1593
|
+
addModel: "Add model…",
|
|
1594
|
+
remove: "Remove",
|
|
1595
|
+
removeRoute: (label) => `Remove ${label}`,
|
|
1596
|
+
tendencyLabel: (label) => `Task tendency for ${label}`,
|
|
1597
|
+
tendencyPlaceholder: "e.g. code review, quick Q&A…",
|
|
1598
|
+
routeExists: (route) => `Route ${route} is already in the list`,
|
|
1599
|
+
follow: "Follow host default",
|
|
1600
|
+
model: "Model",
|
|
1601
|
+
effort: "Reasoning",
|
|
1602
|
+
effortDefault: "Default",
|
|
1603
|
+
searchModels: "Search models…",
|
|
1604
|
+
noModels: "No matching models",
|
|
1605
|
+
clearSearch: "Clear search"
|
|
1606
|
+
}
|
|
1607
|
+
};
|
|
1608
|
+
function useText$1(locale) {
|
|
1609
|
+
const subscribe = (0, react.useCallback)((notify) => typeof locale === "object" ? locale.subscribe?.(notify) ?? (() => {}) : () => {}, [locale]);
|
|
1610
|
+
const snapshot = (0, react.useCallback)(() => typeof locale === "string" ? locale : locale?.getSnapshot?.()?.active ?? "en", [locale]);
|
|
1611
|
+
const active = (0, react.useSyncExternalStore)(subscribe, snapshot, snapshot);
|
|
1612
|
+
const table = String(active || "en").toLowerCase().startsWith("zh") ? TEXT$1.zh : TEXT$1.en;
|
|
1613
|
+
return (0, react.useMemo)(() => Object.assign((key, ...args) => {
|
|
1614
|
+
const value = table[key];
|
|
1615
|
+
return typeof value === "function" ? value(...args) : value;
|
|
1616
|
+
}, { language: table === TEXT$1.zh ? "zh" : "en" }), [table]);
|
|
1617
|
+
}
|
|
1618
|
+
const castCss = `
|
|
1619
|
+
.dsh-model-hub .cast-head{display:flex;gap:12px;align-items:center;justify-content:space-between;flex-wrap:wrap}
|
|
1620
|
+
.dsh-model-hub .cast-row{display:grid;grid-template-columns:minmax(180px,1fr) minmax(160px,1fr) auto;gap:12px;align-items:center;padding:8px 0;border-bottom:.5px solid var(--dsw-alias-border-l1)}
|
|
1621
|
+
.dsh-model-hub .cast-row-model{min-width:0}
|
|
1622
|
+
.dsh-model-hub .cast-row-model .dsh-model-menu-root{max-width:100%}
|
|
1623
|
+
.dsh-model-hub .cast-row-model .dsh-model-menu-trigger{max-width:100%}
|
|
1624
|
+
.dsh-model-hub .cast-row-tendency{min-width:0}
|
|
1625
|
+
.dsh-model-hub .cast-banner{display:flex;gap:12px;align-items:center;justify-content:space-between;flex-wrap:wrap}
|
|
1626
|
+
.dsh-model-hub .cast-add{display:flex}
|
|
1627
|
+
.dsh-model-hub .cast-add .dsh-model-menu-root{max-width:100%}
|
|
1628
|
+
@media(max-width:520px){.dsh-model-hub .cast-row{grid-template-columns:minmax(0,1fr);gap:8px}}
|
|
1629
|
+
`;
|
|
1630
|
+
/** CAS rejections all carry a refresh hint in either language. */
|
|
1631
|
+
function isConflict(message) {
|
|
1632
|
+
return /refresh and retry|reload and retry|刷新后重试|重新加载后重试|请先刷新/i.test(message);
|
|
1633
|
+
}
|
|
1634
|
+
/** The host's duplicate-route error interpolates the route; localize it here. */
|
|
1635
|
+
function localizeCastError(message, t) {
|
|
1636
|
+
const duplicate = /^Route "(.+)" already exists$/.exec(message);
|
|
1637
|
+
if (duplicate) return t("routeExists", duplicate[1]);
|
|
1638
|
+
return hubMessage(message, t.language);
|
|
1639
|
+
}
|
|
1640
|
+
function routeInput(route) {
|
|
1641
|
+
const normalized = normalizeModelRoute(route);
|
|
1642
|
+
return normalized.reasoningEffort ? {
|
|
1643
|
+
provider: normalized.provider,
|
|
1644
|
+
model: normalized.model,
|
|
1645
|
+
reasoningEffort: normalized.reasoningEffort
|
|
1646
|
+
} : {
|
|
1647
|
+
provider: normalized.provider,
|
|
1648
|
+
model: normalized.model
|
|
1649
|
+
};
|
|
1650
|
+
}
|
|
1651
|
+
/** One cast entry: model picker, tendency input saved on blur, remove button. */
|
|
1652
|
+
function CastRow({ t, catalog, route, tendencyRevision, draftReview, disabled, tendencyDisabled, menuDisabled, onSetRoute, onSetTendency, onRemove }) {
|
|
1653
|
+
const label = `${route.provider} / ${route.model}`;
|
|
1654
|
+
const saved = route.tendency ?? "";
|
|
1655
|
+
const [draft, setDraft] = (0, react.useState)({
|
|
1656
|
+
text: saved,
|
|
1657
|
+
saved,
|
|
1658
|
+
revision: tendencyRevision,
|
|
1659
|
+
review: draftReview
|
|
1660
|
+
});
|
|
1661
|
+
(0, react.useEffect)(() => {
|
|
1662
|
+
setDraft((current) => {
|
|
1663
|
+
if (current.text.trim() === current.saved || current.text.trim() === saved) return {
|
|
1664
|
+
text: saved,
|
|
1665
|
+
saved,
|
|
1666
|
+
revision: tendencyRevision,
|
|
1667
|
+
review: draftReview
|
|
1668
|
+
};
|
|
1669
|
+
return current.review !== draftReview ? {
|
|
1670
|
+
...current,
|
|
1671
|
+
saved,
|
|
1672
|
+
revision: tendencyRevision,
|
|
1673
|
+
review: draftReview
|
|
1674
|
+
} : current;
|
|
1675
|
+
});
|
|
1676
|
+
}, [
|
|
1677
|
+
saved,
|
|
1678
|
+
tendencyRevision,
|
|
1679
|
+
draftReview
|
|
1680
|
+
]);
|
|
1681
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1682
|
+
className: "cast-row",
|
|
1683
|
+
children: [
|
|
1684
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1685
|
+
className: "cast-row-model",
|
|
1686
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldMenu, {
|
|
1687
|
+
t,
|
|
1688
|
+
catalog,
|
|
1689
|
+
current: route,
|
|
1690
|
+
label,
|
|
1691
|
+
allowFollow: false,
|
|
1692
|
+
reasoningEffort: false,
|
|
1693
|
+
disabled: menuDisabled,
|
|
1694
|
+
onSet: onSetRoute
|
|
1695
|
+
})
|
|
1696
|
+
}),
|
|
1697
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1698
|
+
className: "cast-row-tendency",
|
|
1699
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
|
|
1700
|
+
type: "text",
|
|
1701
|
+
"aria-label": t("tendencyLabel", label),
|
|
1702
|
+
placeholder: t("tendencyPlaceholder"),
|
|
1703
|
+
value: draft.text,
|
|
1704
|
+
disabled: tendencyDisabled,
|
|
1705
|
+
maxLength: 500,
|
|
1706
|
+
onChange: (event) => setDraft((current) => ({
|
|
1707
|
+
...current,
|
|
1708
|
+
text: event.target.value
|
|
1709
|
+
})),
|
|
1710
|
+
onBlur: () => {
|
|
1711
|
+
const tendency = draft.text.trim();
|
|
1712
|
+
if (!tendencyDisabled && draft.revision !== void 0 && tendency !== saved) onSetTendency(tendency, draft.revision);
|
|
1713
|
+
}
|
|
1714
|
+
}), draft.text.trim() !== saved && draft.revision !== tendencyRevision ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1715
|
+
role: "status",
|
|
1716
|
+
className: "dsh-ui-help dsh-ui-wrap",
|
|
1717
|
+
children: t("draftChanged", saved)
|
|
1718
|
+
}) : null]
|
|
1719
|
+
}),
|
|
1720
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
1721
|
+
variant: "outline",
|
|
1722
|
+
"aria-label": t("removeRoute", label),
|
|
1723
|
+
disabled,
|
|
1724
|
+
onClick: onRemove,
|
|
1725
|
+
children: t("remove")
|
|
1726
|
+
})
|
|
1727
|
+
]
|
|
1728
|
+
});
|
|
1729
|
+
}
|
|
1730
|
+
function CastPage({ api, locale }) {
|
|
1731
|
+
const t = useText$1(locale);
|
|
1732
|
+
const [cast, setCast] = (0, react.useState)(null);
|
|
1733
|
+
const [catalog, setCatalog] = (0, react.useState)(null);
|
|
1734
|
+
const [loadError, setLoadError] = (0, react.useState)("");
|
|
1735
|
+
const [catalogError, setCatalogError] = (0, react.useState)("");
|
|
1736
|
+
const [syncError, setSyncError] = (0, react.useState)("");
|
|
1737
|
+
const [writeError, setWriteError] = (0, react.useState)("");
|
|
1738
|
+
const [saved, setSaved] = (0, react.useState)(false);
|
|
1739
|
+
const [conflict, setConflict] = (0, react.useState)(false);
|
|
1740
|
+
const [saving, setSaving] = (0, react.useState)("");
|
|
1741
|
+
const [refreshing, setRefreshing] = (0, react.useState)(false);
|
|
1742
|
+
const [draftReview, setDraftReview] = (0, react.useState)(0);
|
|
1743
|
+
const scopeRef = (0, react.useRef)(null);
|
|
1744
|
+
const refresh = (0, react.useCallback)(async (scope, reviewDrafts = false) => {
|
|
1745
|
+
if (!scope.active) return;
|
|
1746
|
+
if (scope.busy) {
|
|
1747
|
+
scope.queued = true;
|
|
1748
|
+
return;
|
|
1749
|
+
}
|
|
1750
|
+
scope.queued = false;
|
|
1751
|
+
scope.busy = true;
|
|
1752
|
+
setRefreshing(true);
|
|
1753
|
+
const [castResult, models] = await Promise.allSettled([Promise.resolve().then(() => scope.api.getCast()), Promise.resolve().then(() => scope.api.modelCatalog()).then(checkedCatalog)]);
|
|
1754
|
+
if (!scope.active) return;
|
|
1755
|
+
if (castResult.status === "fulfilled") {
|
|
1756
|
+
setCast(castResult.value);
|
|
1757
|
+
setLoadError("");
|
|
1758
|
+
setWriteError("");
|
|
1759
|
+
setConflict(false);
|
|
1760
|
+
setSaved(false);
|
|
1761
|
+
if (reviewDrafts) setDraftReview((current) => current + 1);
|
|
1762
|
+
} else setLoadError(messageOf(castResult.reason));
|
|
1763
|
+
if (models.status === "fulfilled") {
|
|
1764
|
+
setCatalog(models.value);
|
|
1765
|
+
setCatalogError("");
|
|
1766
|
+
} else setCatalogError(messageOf(models.reason));
|
|
1767
|
+
scope.busy = false;
|
|
1768
|
+
setRefreshing(false);
|
|
1769
|
+
if (scope.queued) refresh(scope);
|
|
1770
|
+
}, []);
|
|
1771
|
+
(0, react.useEffect)(() => {
|
|
1772
|
+
const scope = {
|
|
1773
|
+
api,
|
|
1774
|
+
active: true,
|
|
1775
|
+
busy: false,
|
|
1776
|
+
queued: false
|
|
1777
|
+
};
|
|
1778
|
+
scopeRef.current = scope;
|
|
1779
|
+
setSaving("");
|
|
1780
|
+
setWriteError("");
|
|
1781
|
+
setSaved(false);
|
|
1782
|
+
setConflict(false);
|
|
1783
|
+
setLoadError("");
|
|
1784
|
+
setCatalogError("");
|
|
1785
|
+
setSyncError("");
|
|
1786
|
+
setCast(null);
|
|
1787
|
+
setCatalog(null);
|
|
1788
|
+
refresh(scope);
|
|
1789
|
+
const unsubscribe = api.subscribeChanges?.(() => {
|
|
1790
|
+
if (!scope.active) return;
|
|
1791
|
+
setSyncError("");
|
|
1792
|
+
refresh(scope);
|
|
1793
|
+
}, (message) => {
|
|
1794
|
+
if (scope.active) setSyncError(message);
|
|
1795
|
+
});
|
|
1796
|
+
return () => {
|
|
1797
|
+
scope.active = false;
|
|
1798
|
+
unsubscribe?.();
|
|
1799
|
+
scopeRef.current = null;
|
|
1800
|
+
};
|
|
1801
|
+
}, [api, refresh]);
|
|
1802
|
+
const mutate = (key, run) => {
|
|
1803
|
+
const scope = scopeRef.current;
|
|
1804
|
+
if (!scope?.active || scope.api !== api || scope.busy || writeError || saved && loadError) return;
|
|
1805
|
+
scope.busy = true;
|
|
1806
|
+
setSaved(false);
|
|
1807
|
+
setSaving(key);
|
|
1808
|
+
Promise.resolve().then(() => run(scope)).then((value) => {
|
|
1809
|
+
if (!scope.active) return;
|
|
1810
|
+
if ("committed" in value) {
|
|
1811
|
+
setSaved(true);
|
|
1812
|
+
scope.queued = true;
|
|
1813
|
+
} else setCast(value);
|
|
1814
|
+
setWriteError("");
|
|
1815
|
+
setConflict(false);
|
|
1816
|
+
}, (error) => {
|
|
1817
|
+
if (scope.active) {
|
|
1818
|
+
const message = messageOf(error);
|
|
1819
|
+
setWriteError(message);
|
|
1820
|
+
setConflict(isConflict(message));
|
|
1821
|
+
}
|
|
1822
|
+
}).finally(() => {
|
|
1823
|
+
if (scope.active) {
|
|
1824
|
+
scope.busy = false;
|
|
1825
|
+
setSaving("");
|
|
1826
|
+
if (scope.queued) refresh(scope);
|
|
1827
|
+
}
|
|
1828
|
+
});
|
|
1829
|
+
};
|
|
1830
|
+
const refreshNow = () => {
|
|
1831
|
+
const scope = scopeRef.current;
|
|
1832
|
+
if (scope?.api === api) refresh(scope, true);
|
|
1833
|
+
};
|
|
1834
|
+
const disabled = saving !== "" || refreshing || writeError !== "" || saved && loadError !== "";
|
|
1835
|
+
const failures = catalogFailures(catalog);
|
|
1836
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1837
|
+
className: "cast-root dsh-ui-stack",
|
|
1838
|
+
"aria-label": t("title"),
|
|
1839
|
+
"aria-busy": saving !== "" || refreshing,
|
|
1840
|
+
children: [
|
|
1841
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: castCss }),
|
|
1842
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1843
|
+
className: "cast-head",
|
|
1844
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Switch, {
|
|
1845
|
+
checked: cast?.enabled ?? false,
|
|
1846
|
+
label: t("enabledLabel"),
|
|
1847
|
+
disabled: disabled || cast === null || !cast.enabled && cast.routes.length === 0,
|
|
1848
|
+
onChange: (next) => {
|
|
1849
|
+
if (cast) mutate("enabled", (scope) => scope.api.setCastEnabled({
|
|
1850
|
+
enabled: next,
|
|
1851
|
+
expectedRevision: cast.revision
|
|
1852
|
+
}));
|
|
1853
|
+
}
|
|
1854
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
1855
|
+
variant: "ghost",
|
|
1856
|
+
disabled: saving !== "" || refreshing,
|
|
1857
|
+
onClick: refreshNow,
|
|
1858
|
+
children: refreshing ? t("refreshing") : t("refresh")
|
|
1859
|
+
})]
|
|
1860
|
+
}),
|
|
1861
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1862
|
+
className: "dsh-ui-help",
|
|
1863
|
+
children: t("enabledHint")
|
|
1864
|
+
}),
|
|
1865
|
+
loadError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1866
|
+
role: "alert",
|
|
1867
|
+
className: "dsh-ui-error dsh-ui-wrap",
|
|
1868
|
+
children: t(saved ? "savedRefreshFailed" : "failed", hubMessage(loadError, t.language))
|
|
1869
|
+
}) : null,
|
|
1870
|
+
cast?.tendencyError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1871
|
+
role: "alert",
|
|
1872
|
+
className: "dsh-ui-error dsh-ui-wrap",
|
|
1873
|
+
children: t("tendencyFailed", hubMessage(cast.tendencyError, t.language))
|
|
1874
|
+
}) : null,
|
|
1875
|
+
syncError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1876
|
+
role: "alert",
|
|
1877
|
+
className: "dsh-ui-error dsh-ui-wrap",
|
|
1878
|
+
children: t("syncFailed", hubMessage(syncError, t.language))
|
|
1879
|
+
}) : null,
|
|
1880
|
+
catalogError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1881
|
+
role: "alert",
|
|
1882
|
+
className: "dsh-ui-error dsh-ui-wrap",
|
|
1883
|
+
children: t("catalogFailed", hubMessage(catalogError, t.language))
|
|
1884
|
+
}) : null,
|
|
1885
|
+
failures.map((failure, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1886
|
+
role: "alert",
|
|
1887
|
+
className: "dsh-ui-error dsh-ui-wrap",
|
|
1888
|
+
children: t("catalogFailed", hubMessage(failure, t.language))
|
|
1889
|
+
}, index)),
|
|
1890
|
+
writeError ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1891
|
+
className: "dsh-ui-banner dsh-ui-banner--danger cast-banner",
|
|
1892
|
+
role: "alert",
|
|
1893
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1894
|
+
className: "dsh-ui-wrap",
|
|
1895
|
+
children: conflict ? t("conflict") : t("writeFailed", localizeCastError(writeError, t))
|
|
1896
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
1897
|
+
variant: "outline",
|
|
1898
|
+
size: "sm",
|
|
1899
|
+
disabled: saving !== "" || refreshing,
|
|
1900
|
+
onClick: refreshNow,
|
|
1901
|
+
children: refreshing ? t("refreshing") : t("refresh")
|
|
1902
|
+
})]
|
|
1903
|
+
}) : null,
|
|
1904
|
+
saving ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1905
|
+
role: "status",
|
|
1906
|
+
className: "dsh-ui-help",
|
|
1907
|
+
children: t("saving")
|
|
1908
|
+
}) : null,
|
|
1909
|
+
saved && !loadError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1910
|
+
role: "status",
|
|
1911
|
+
className: "dsh-ui-help",
|
|
1912
|
+
children: t("saved")
|
|
1913
|
+
}) : null,
|
|
1914
|
+
cast === null && !loadError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1915
|
+
className: "dsh-ui-loading",
|
|
1916
|
+
children: t("loading")
|
|
1917
|
+
}) : null,
|
|
1918
|
+
cast !== null && cast.routes.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1919
|
+
className: "dsh-ui-help",
|
|
1920
|
+
children: t("empty")
|
|
1921
|
+
}) : null,
|
|
1922
|
+
cast?.routes.map((route, position) => {
|
|
1923
|
+
const handle = route.index ?? position;
|
|
1924
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CastRow, {
|
|
1925
|
+
t,
|
|
1926
|
+
catalog,
|
|
1927
|
+
route,
|
|
1928
|
+
tendencyRevision: cast.tendencyRevision,
|
|
1929
|
+
draftReview,
|
|
1930
|
+
tendencyDisabled: disabled || !!cast.tendencyError || cast.tendencyRevision === void 0,
|
|
1931
|
+
disabled,
|
|
1932
|
+
menuDisabled: disabled || catalog === null,
|
|
1933
|
+
onSetRoute: (next) => mutate(`route:${handle}`, (scope) => scope.api.setCastRoute({
|
|
1934
|
+
index: handle,
|
|
1935
|
+
route: routeInput(next),
|
|
1936
|
+
expectedRevision: cast.revision
|
|
1937
|
+
})),
|
|
1938
|
+
onSetTendency: (tendency, expectedRevision) => mutate(`tendency:${handle}`, (scope) => scope.api.setCastTendency({
|
|
1939
|
+
provider: route.provider,
|
|
1940
|
+
model: route.model,
|
|
1941
|
+
tendency,
|
|
1942
|
+
expectedRevision
|
|
1943
|
+
})),
|
|
1944
|
+
onRemove: () => mutate(`remove:${handle}`, (scope) => scope.api.removeCastRoute({
|
|
1945
|
+
index: handle,
|
|
1946
|
+
expectedRevision: cast.revision
|
|
1947
|
+
}))
|
|
1948
|
+
}, JSON.stringify([route.provider, route.model]));
|
|
1949
|
+
}),
|
|
1950
|
+
cast !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1951
|
+
className: "cast-add",
|
|
1952
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldMenu, {
|
|
1953
|
+
t,
|
|
1954
|
+
catalog,
|
|
1955
|
+
current: void 0,
|
|
1956
|
+
label: t("addModel"),
|
|
1957
|
+
allowFollow: false,
|
|
1958
|
+
followLabel: t("addModel"),
|
|
1959
|
+
reasoningEffort: false,
|
|
1960
|
+
disabled: disabled || catalog === null,
|
|
1961
|
+
onSet: (picked) => mutate("add", (scope) => scope.api.addCastRoute({
|
|
1962
|
+
route: routeInput(picked),
|
|
1963
|
+
expectedRevision: cast.revision
|
|
1964
|
+
}))
|
|
1965
|
+
})
|
|
1966
|
+
}) : null
|
|
1967
|
+
]
|
|
1968
|
+
});
|
|
1969
|
+
}
|
|
1970
|
+
//#endregion
|
|
1971
|
+
//#region src/client.tsx
|
|
1972
|
+
/**
|
|
1973
|
+
* dsh-model-hub — browser half. One settings-page section listing every
|
|
1974
|
+
* detected model-route field across plugins. Each row is edited with the
|
|
1975
|
+
* contract's standard ModelMenu (dsh-model-route/ui) and written back through
|
|
1976
|
+
* the hub's host RPC — `settings.mutate` on the host, CAS by descriptor
|
|
1977
|
+
* revision. An empty route means "follow the host default" and unsets the
|
|
1978
|
+
* field.
|
|
1979
|
+
*/
|
|
1980
|
+
const name = "dsh-model-hub-client";
|
|
1981
|
+
const inject = [
|
|
1982
|
+
"slots",
|
|
1983
|
+
"connection",
|
|
1984
|
+
"remote",
|
|
1985
|
+
"remote.session",
|
|
1986
|
+
"locale"
|
|
1987
|
+
];
|
|
1988
|
+
const CHANNEL = "/dsh-model-hub";
|
|
1989
|
+
const BUNDLE = "@klarkxy/dsh-model-hub";
|
|
1990
|
+
const TEXT = {
|
|
1991
|
+
zh: {
|
|
1992
|
+
title: "模型枢纽",
|
|
1993
|
+
intro: "一页查看并修改各插件的模型路由;改动立即写入对应插件自己的设置。",
|
|
1994
|
+
tabsLabel: "模型枢纽分区",
|
|
1995
|
+
tabFields: "字段总览",
|
|
1996
|
+
tabCast: "子代理阵容",
|
|
1997
|
+
loading: "正在读取…",
|
|
1998
|
+
empty: "没有检测到模型字段。",
|
|
1999
|
+
failed: (message) => `读取失败:${message}`,
|
|
2000
|
+
syncFailed: (message) => `自动同步不可用:${message}`,
|
|
2001
|
+
catalogFailed: (message) => `模型列表读取失败:${message}`,
|
|
2002
|
+
writeFailed: (message) => `未保存:${message}。请刷新后重新选择。`,
|
|
2003
|
+
saved: "已保存,正在刷新…",
|
|
2004
|
+
savedRefreshFailed: (message) => `已保存;页面刷新失败:${message}。请刷新以读取最新设置。`,
|
|
2005
|
+
refresh: "刷新",
|
|
2006
|
+
refreshing: "正在刷新…",
|
|
2007
|
+
saving: "正在保存…",
|
|
2008
|
+
search: "搜索插件、字段或当前模型…",
|
|
2009
|
+
noFields: "没有匹配的模型字段",
|
|
2010
|
+
follow: "跟随宿主默认",
|
|
2011
|
+
restorePluginDefault: "恢复插件默认",
|
|
2012
|
+
model: "模型",
|
|
2013
|
+
effort: "思考强度",
|
|
2014
|
+
effortDefault: "默认",
|
|
2015
|
+
searchModels: "搜索模型…",
|
|
2016
|
+
noModels: "没有匹配的模型",
|
|
2017
|
+
clearSearch: "清除搜索",
|
|
2018
|
+
collectionTemplate: "集合中没有可编辑的条目;请先在所属插件中添加条目。",
|
|
2019
|
+
items: "条目"
|
|
2020
|
+
},
|
|
2021
|
+
en: {
|
|
2022
|
+
title: "Model Hub",
|
|
2023
|
+
intro: "View and edit every plugin's model route on one page; changes write into the owning plugin's own settings at once.",
|
|
2024
|
+
tabsLabel: "Model Hub sections",
|
|
2025
|
+
tabFields: "Fields",
|
|
2026
|
+
tabCast: "Subagent Cast",
|
|
2027
|
+
loading: "Loading…",
|
|
2028
|
+
empty: "No model fields detected.",
|
|
2029
|
+
failed: (message) => `Load failed: ${message}`,
|
|
2030
|
+
syncFailed: (message) => `Live sync unavailable: ${message}`,
|
|
2031
|
+
catalogFailed: (message) => `Model list failed: ${message}`,
|
|
2032
|
+
writeFailed: (message) => `Not saved: ${message}. Refresh, then choose again.`,
|
|
2033
|
+
saved: "Saved; refreshing…",
|
|
2034
|
+
savedRefreshFailed: (message) => `Saved; page refresh failed: ${message}. Refresh to read the latest settings.`,
|
|
2035
|
+
refresh: "Refresh",
|
|
2036
|
+
refreshing: "Refreshing…",
|
|
2037
|
+
saving: "Saving…",
|
|
2038
|
+
search: "Search plugins, fields or current models…",
|
|
2039
|
+
noFields: "No matching model fields",
|
|
2040
|
+
follow: "Follow host default",
|
|
2041
|
+
restorePluginDefault: "Restore plugin default",
|
|
2042
|
+
model: "Model",
|
|
2043
|
+
effort: "Reasoning",
|
|
2044
|
+
effortDefault: "Default",
|
|
2045
|
+
searchModels: "Search models…",
|
|
2046
|
+
noModels: "No matching models",
|
|
2047
|
+
clearSearch: "Clear search",
|
|
2048
|
+
collectionTemplate: "No collection item to edit. Add an item in the owning plugin first.",
|
|
2049
|
+
items: "Items"
|
|
2050
|
+
}
|
|
2051
|
+
};
|
|
2052
|
+
function useText(locale) {
|
|
2053
|
+
const subscribe = (0, react.useCallback)((notify) => typeof locale === "object" ? locale.subscribe?.(notify) ?? (() => {}) : () => {}, [locale]);
|
|
2054
|
+
const snapshot = (0, react.useCallback)(() => typeof locale === "string" ? locale : locale?.getSnapshot?.()?.active ?? "en", [locale]);
|
|
2055
|
+
const active = (0, react.useSyncExternalStore)(subscribe, snapshot, snapshot);
|
|
2056
|
+
const table = String(active || "en").toLowerCase().startsWith("zh") ? TEXT.zh : TEXT.en;
|
|
2057
|
+
return (0, react.useMemo)(() => Object.assign((key, ...args) => {
|
|
2058
|
+
const value = table[key];
|
|
2059
|
+
return typeof value === "function" ? value(...args) : value;
|
|
2060
|
+
}, { language: table === TEXT.zh ? "zh" : "en" }), [table]);
|
|
2061
|
+
}
|
|
2062
|
+
const css = `${officialUiCss("dsh-model-hub")}${modelMenuCss}
|
|
2063
|
+
.dsh-model-hub .hub-toolbar{display:flex;gap:12px;align-items:center}
|
|
2064
|
+
.dsh-model-hub .hub-search{flex:1;min-width:0}
|
|
2065
|
+
.dsh-model-hub .hub-group{min-width:0}
|
|
2066
|
+
.dsh-model-hub .hub-group-head{display:flex;gap:8px;align-items:baseline;flex-wrap:wrap;margin:12px 0 4px}
|
|
2067
|
+
.dsh-model-hub .hub-group-title{font-size:14px;line-height:22px;font-weight:600;margin:0;color:var(--dsw-alias-label-primary)}
|
|
2068
|
+
.dsh-model-hub .hub-namespace{font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary);overflow-wrap:anywhere}
|
|
2069
|
+
.dsh-model-hub .hub-row{display:grid;grid-template-columns:minmax(140px,1fr) minmax(180px,1fr);gap:16px;align-items:center;padding:8px 0;border-bottom:.5px solid var(--dsw-alias-border-l1)}
|
|
2070
|
+
.dsh-model-hub .hub-row-copy{min-width:0}
|
|
2071
|
+
.dsh-model-hub .hub-row-label{font-size:13px;line-height:20px;color:var(--dsw-alias-label-primary);overflow-wrap:anywhere}
|
|
2072
|
+
.dsh-model-hub .hub-row-description{font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere;margin:2px 0 0}
|
|
2073
|
+
.dsh-model-hub .hub-editor{min-width:0;display:flex;justify-content:flex-end}
|
|
2074
|
+
.dsh-model-hub .hub-editor .dsh-model-menu-root{max-width:100%}
|
|
2075
|
+
.dsh-model-hub .hub-editor .dsh-model-menu-trigger{max-width:100%}
|
|
2076
|
+
@media(max-width:520px){.dsh-model-hub .hub-toolbar{gap:8px}.dsh-model-hub .hub-row{grid-template-columns:minmax(0,1fr);gap:4px}.dsh-model-hub .hub-editor{justify-content:flex-start}.dsh-model-hub .hub-namespace{width:100%}}
|
|
2077
|
+
`;
|
|
2078
|
+
function Style() {
|
|
2079
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: css });
|
|
2080
|
+
}
|
|
2081
|
+
function humanize(value) {
|
|
2082
|
+
const text = value.replace(/([a-z\d])([A-Z])/g, "$1 $2").replace(/[-_.]+/g, " ").trim();
|
|
2083
|
+
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
2084
|
+
}
|
|
2085
|
+
function fieldLabel(field, t) {
|
|
2086
|
+
return hubText(field.marker?.label || field.label, t.language) || field.path.map((part) => part === "[]" ? t("items") : part === "model" ? t("model") : /^\d+$/.test(part) ? `#${Number(part) + 1}` : humanize(part)).join(" / ");
|
|
2087
|
+
}
|
|
2088
|
+
function followLabel(field, t) {
|
|
2089
|
+
return typeof field.followLabel === "string" ? field.followLabel : field.followLabel?.[t.language] || t(field.via === "contribution" || field.arrayItem ? "follow" : "restorePluginDefault");
|
|
2090
|
+
}
|
|
2091
|
+
function pluginTitle(entry, t) {
|
|
2092
|
+
return hubText(entry.title, t.language) || humanize((entry.ns.split("/").at(-1) || entry.ns).replace(/^dsh-/, ""));
|
|
2093
|
+
}
|
|
1292
2094
|
function HubPage({ view, api, locale, initialEntries = null, initialCatalog = null }) {
|
|
1293
2095
|
const t = useText(locale);
|
|
1294
2096
|
const [entries, setEntries] = (0, react.useState)(initialEntries);
|
|
1295
2097
|
const [catalog, setCatalog] = (0, react.useState)(initialCatalog);
|
|
1296
|
-
const [
|
|
2098
|
+
const [listError, setListError] = (0, react.useState)("");
|
|
2099
|
+
const [catalogError, setCatalogError] = (0, react.useState)("");
|
|
2100
|
+
const [writeError, setWriteError] = (0, react.useState)("");
|
|
2101
|
+
const [saved, setSaved] = (0, react.useState)(false);
|
|
2102
|
+
const [syncError, setSyncError] = (0, react.useState)("");
|
|
1297
2103
|
const [writing, setWriting] = (0, react.useState)("");
|
|
2104
|
+
const [refreshing, setRefreshing] = (0, react.useState)(false);
|
|
2105
|
+
const [query, setQuery] = (0, react.useState)("");
|
|
2106
|
+
const [tab, setTab] = (0, react.useState)("fields");
|
|
2107
|
+
const scopeRef = (0, react.useRef)(null);
|
|
2108
|
+
const refresh = (0, react.useCallback)(async (scope) => {
|
|
2109
|
+
if (!scope.active) return;
|
|
2110
|
+
if (scope.busy) {
|
|
2111
|
+
scope.queued = true;
|
|
2112
|
+
return;
|
|
2113
|
+
}
|
|
2114
|
+
scope.queued = false;
|
|
2115
|
+
scope.busy = true;
|
|
2116
|
+
setRefreshing(true);
|
|
2117
|
+
const [fields, models] = await Promise.allSettled([Promise.resolve().then(() => scope.api.list()), Promise.resolve().then(() => scope.api.modelCatalog()).then(checkedCatalog)]);
|
|
2118
|
+
if (!scope.active) return;
|
|
2119
|
+
if (fields.status === "fulfilled") {
|
|
2120
|
+
setEntries(fields.value);
|
|
2121
|
+
setListError("");
|
|
2122
|
+
setWriteError("");
|
|
2123
|
+
setSaved(false);
|
|
2124
|
+
} else setListError(messageOf(fields.reason));
|
|
2125
|
+
if (models.status === "fulfilled") {
|
|
2126
|
+
setCatalog(models.value);
|
|
2127
|
+
setCatalogError("");
|
|
2128
|
+
} else setCatalogError(messageOf(models.reason));
|
|
2129
|
+
scope.busy = false;
|
|
2130
|
+
setRefreshing(false);
|
|
2131
|
+
if (scope.queued) refresh(scope);
|
|
2132
|
+
}, []);
|
|
1298
2133
|
(0, react.useEffect)(() => {
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
}
|
|
1306
|
-
|
|
2134
|
+
if (view === "summary") return;
|
|
2135
|
+
const scope = {
|
|
2136
|
+
api,
|
|
2137
|
+
active: true,
|
|
2138
|
+
busy: false,
|
|
2139
|
+
queued: false
|
|
2140
|
+
};
|
|
2141
|
+
scopeRef.current = scope;
|
|
2142
|
+
setWriting("");
|
|
2143
|
+
setWriteError("");
|
|
2144
|
+
setSaved(false);
|
|
2145
|
+
setListError("");
|
|
2146
|
+
setCatalogError("");
|
|
2147
|
+
setSyncError("");
|
|
2148
|
+
setEntries(initialEntries);
|
|
2149
|
+
setCatalog(initialCatalog);
|
|
2150
|
+
refresh(scope);
|
|
2151
|
+
const unsubscribe = api.subscribeChanges?.(() => {
|
|
2152
|
+
if (!scope.active) return;
|
|
2153
|
+
setSyncError("");
|
|
2154
|
+
refresh(scope);
|
|
2155
|
+
}, (message) => {
|
|
2156
|
+
if (scope.active) setSyncError(message);
|
|
1307
2157
|
});
|
|
1308
|
-
Promise.resolve().then(() => api.modelCatalog()).then((value) => {
|
|
1309
|
-
if (alive) setCatalog(value);
|
|
1310
|
-
}, () => {});
|
|
1311
2158
|
return () => {
|
|
1312
|
-
|
|
2159
|
+
scope.active = false;
|
|
2160
|
+
unsubscribe?.();
|
|
2161
|
+
scopeRef.current = null;
|
|
1313
2162
|
};
|
|
1314
|
-
}, [
|
|
2163
|
+
}, [
|
|
2164
|
+
api,
|
|
2165
|
+
view,
|
|
2166
|
+
refresh
|
|
2167
|
+
]);
|
|
1315
2168
|
const setRoute = (entry, field, route) => {
|
|
2169
|
+
const scope = scopeRef.current;
|
|
2170
|
+
if (!scope?.active || scope.api !== api || scope.busy || writeError) return;
|
|
1316
2171
|
const key = `${entry.ns}:${field.path.join(".")}`;
|
|
1317
|
-
|
|
2172
|
+
scope.busy = true;
|
|
2173
|
+
setSaved(false);
|
|
1318
2174
|
setWriting(key);
|
|
1319
|
-
api.setField({
|
|
2175
|
+
Promise.resolve().then(() => scope.api.setField({
|
|
1320
2176
|
ns: entry.ns,
|
|
1321
2177
|
path: field.path,
|
|
1322
2178
|
route,
|
|
1323
|
-
expectedRevision: entry.revision
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
2179
|
+
expectedRevision: entry.revision,
|
|
2180
|
+
...entry.ownerId ? { ownerId: entry.ownerId } : {}
|
|
2181
|
+
})).then(() => {
|
|
2182
|
+
if (scope.active) {
|
|
2183
|
+
setSaved(true);
|
|
2184
|
+
setWriteError("");
|
|
2185
|
+
scope.queued = true;
|
|
2186
|
+
}
|
|
2187
|
+
}, (error) => {
|
|
2188
|
+
if (scope.active) setWriteError(messageOf(error));
|
|
2189
|
+
}).finally(() => {
|
|
2190
|
+
if (scope.active) {
|
|
2191
|
+
scope.busy = false;
|
|
2192
|
+
setWriting("");
|
|
2193
|
+
if (scope.queued) refresh(scope);
|
|
2194
|
+
}
|
|
2195
|
+
});
|
|
1328
2196
|
};
|
|
1329
2197
|
if (view === "summary") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1330
2198
|
className: "dsh-model-hub",
|
|
2199
|
+
lang: t.language,
|
|
1331
2200
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Style, {}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1332
2201
|
className: "dsh-ui-compact dsh-ui-wrap",
|
|
1333
2202
|
children: t("intro")
|
|
1334
2203
|
})]
|
|
1335
2204
|
});
|
|
2205
|
+
const needle = query.trim().toLocaleLowerCase();
|
|
2206
|
+
const groups = (entries ?? []).map((entry) => ({
|
|
2207
|
+
...entry,
|
|
2208
|
+
fields: entry.fields.filter((field) => !(entry.ns === "@klarkxy/dsh-model-hub" && field.path.length === 1 && ["selfTestMeta", "selfTestRole"].includes(field.path[0] || "")))
|
|
2209
|
+
})).filter((entry) => entry.ns !== "subagent-model-selection-settings" && (entry.fields.length > 0 || entry.error || entry.catalogError));
|
|
2210
|
+
const visible = groups.map((entry) => ({
|
|
2211
|
+
...entry,
|
|
2212
|
+
fields: entry.fields.filter((field) => {
|
|
2213
|
+
if (!needle) return true;
|
|
2214
|
+
const route = normalizeModelRoute(field.current);
|
|
2215
|
+
const model = parseModelMenuChoices(entry.catalog !== void 0 ? entry.catalog : catalog, route).find((choice) => choice.provider === route.provider && choice.model === route.model)?.label || "";
|
|
2216
|
+
return [
|
|
2217
|
+
entry.ns,
|
|
2218
|
+
pluginTitle(entry, t),
|
|
2219
|
+
fieldLabel(field, t),
|
|
2220
|
+
hubText(field.description, t.language),
|
|
2221
|
+
field.path.join("."),
|
|
2222
|
+
route.provider,
|
|
2223
|
+
route.model,
|
|
2224
|
+
model,
|
|
2225
|
+
!route.model ? followLabel(field, t) : ""
|
|
2226
|
+
].some((text) => text?.toLocaleLowerCase().includes(needle));
|
|
2227
|
+
})
|
|
2228
|
+
})).filter((entry) => entry.fields.length > 0 || (entry.error || entry.catalogError) && (!needle || [entry.ns, pluginTitle(entry, t)].some((text) => text.toLocaleLowerCase().includes(needle))));
|
|
2229
|
+
const disabled = writing !== "" || refreshing || writeError !== "" || saved && listError !== "";
|
|
2230
|
+
const failures = catalogFailures(catalog);
|
|
1336
2231
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1337
2232
|
className: "dsh-model-hub dsh-ui-stack",
|
|
2233
|
+
lang: t.language,
|
|
1338
2234
|
"aria-label": t("title"),
|
|
2235
|
+
"aria-busy": writing !== "" || refreshing,
|
|
1339
2236
|
children: [
|
|
1340
2237
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Style, {}),
|
|
1341
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
1342
|
-
|
|
1343
|
-
|
|
2238
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.SegmentedTabs, {
|
|
2239
|
+
value: tab,
|
|
2240
|
+
onChange: setTab,
|
|
2241
|
+
label: t("tabsLabel"),
|
|
2242
|
+
className: "hub-tabs",
|
|
2243
|
+
items: [{
|
|
2244
|
+
value: "fields",
|
|
2245
|
+
label: t("tabFields"),
|
|
2246
|
+
id: "dsh-model-hub-tab-fields",
|
|
2247
|
+
panelId: "dsh-model-hub-panel-fields"
|
|
2248
|
+
}, {
|
|
2249
|
+
value: "cast",
|
|
2250
|
+
label: t("tabCast"),
|
|
2251
|
+
id: "dsh-model-hub-tab-cast",
|
|
2252
|
+
panelId: "dsh-model-hub-panel-cast"
|
|
2253
|
+
}]
|
|
1344
2254
|
}),
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
className: "dsh-ui-
|
|
1351
|
-
children:
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
2255
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2256
|
+
role: "tabpanel",
|
|
2257
|
+
id: "dsh-model-hub-panel-fields",
|
|
2258
|
+
"aria-labelledby": "dsh-model-hub-tab-fields",
|
|
2259
|
+
hidden: tab !== "fields",
|
|
2260
|
+
className: "dsh-ui-stack",
|
|
2261
|
+
children: [
|
|
2262
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2263
|
+
className: "hub-toolbar",
|
|
2264
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
|
|
2265
|
+
className: "hub-search",
|
|
2266
|
+
type: "search",
|
|
2267
|
+
"aria-label": t("search"),
|
|
2268
|
+
placeholder: t("search"),
|
|
2269
|
+
value: query,
|
|
2270
|
+
onChange: (event) => setQuery(event.target.value)
|
|
2271
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
2272
|
+
variant: "ghost",
|
|
2273
|
+
disabled: writing !== "" || refreshing,
|
|
2274
|
+
onClick: () => {
|
|
2275
|
+
const scope = scopeRef.current;
|
|
2276
|
+
if (scope?.api === api) refresh(scope);
|
|
2277
|
+
},
|
|
2278
|
+
children: refreshing ? t("refreshing") : t("refresh")
|
|
2279
|
+
})]
|
|
2280
|
+
}),
|
|
2281
|
+
listError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2282
|
+
role: "alert",
|
|
2283
|
+
className: "dsh-ui-error dsh-ui-wrap",
|
|
2284
|
+
children: t(saved ? "savedRefreshFailed" : "failed", hubMessage(listError, t.language))
|
|
2285
|
+
}) : null,
|
|
2286
|
+
syncError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2287
|
+
role: "alert",
|
|
2288
|
+
className: "dsh-ui-error dsh-ui-wrap",
|
|
2289
|
+
children: t("syncFailed", hubMessage(syncError, t.language))
|
|
2290
|
+
}) : null,
|
|
2291
|
+
catalogError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2292
|
+
role: "alert",
|
|
2293
|
+
className: "dsh-ui-error dsh-ui-wrap",
|
|
2294
|
+
children: t("catalogFailed", hubMessage(catalogError, t.language))
|
|
2295
|
+
}) : null,
|
|
2296
|
+
failures.map((failure, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2297
|
+
role: "alert",
|
|
2298
|
+
className: "dsh-ui-error dsh-ui-wrap",
|
|
2299
|
+
children: t("catalogFailed", hubMessage(failure, t.language))
|
|
2300
|
+
}, index)),
|
|
2301
|
+
writeError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2302
|
+
role: "alert",
|
|
2303
|
+
className: "dsh-ui-error dsh-ui-wrap",
|
|
2304
|
+
children: t("writeFailed", hubMessage(writeError, t.language))
|
|
2305
|
+
}) : null,
|
|
2306
|
+
writing ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2307
|
+
role: "status",
|
|
2308
|
+
className: "dsh-ui-help",
|
|
2309
|
+
children: t("saving")
|
|
2310
|
+
}) : null,
|
|
2311
|
+
saved && !listError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2312
|
+
role: "status",
|
|
2313
|
+
className: "dsh-ui-help",
|
|
2314
|
+
children: t("saved")
|
|
2315
|
+
}) : null,
|
|
2316
|
+
entries === null && !listError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2317
|
+
className: "dsh-ui-loading",
|
|
2318
|
+
children: t("loading")
|
|
2319
|
+
}) : null,
|
|
2320
|
+
entries !== null && groups.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2321
|
+
className: "dsh-ui-help",
|
|
2322
|
+
children: t("empty")
|
|
2323
|
+
}) : null,
|
|
2324
|
+
groups.length > 0 && visible.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2325
|
+
className: "dsh-ui-help",
|
|
2326
|
+
children: t("noFields")
|
|
2327
|
+
}) : null,
|
|
2328
|
+
visible.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
2329
|
+
className: "hub-group",
|
|
2330
|
+
"aria-label": pluginTitle(entry, t),
|
|
1362
2331
|
children: [
|
|
1363
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
1364
|
-
className: "
|
|
1365
|
-
children:
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
catalog,
|
|
1373
|
-
current: field.current,
|
|
1374
|
-
disabled: writing === key,
|
|
1375
|
-
onSet: (route) => setRoute(entry, field, route)
|
|
2332
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2333
|
+
className: "hub-group-head",
|
|
2334
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
2335
|
+
className: "hub-group-title",
|
|
2336
|
+
children: pluginTitle(entry, t)
|
|
2337
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2338
|
+
className: "hub-namespace",
|
|
2339
|
+
children: entry.ns
|
|
2340
|
+
})]
|
|
1376
2341
|
}),
|
|
1377
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
2342
|
+
entry.error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2343
|
+
role: "alert",
|
|
2344
|
+
className: "dsh-ui-error dsh-ui-wrap",
|
|
2345
|
+
children: t("failed", hubMessage(hubText(entry.error, t.language), t.language))
|
|
2346
|
+
}) : null,
|
|
2347
|
+
entry.catalogError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2348
|
+
role: "alert",
|
|
2349
|
+
className: "dsh-ui-error dsh-ui-wrap",
|
|
2350
|
+
children: t("catalogFailed", hubMessage(hubText(entry.catalogError, t.language), t.language))
|
|
2351
|
+
}) : null,
|
|
2352
|
+
entry.catalog !== void 0 ? catalogFailures(entry.catalog).map((failure, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2353
|
+
role: "alert",
|
|
2354
|
+
className: "dsh-ui-error dsh-ui-wrap",
|
|
2355
|
+
children: t("catalogFailed", hubMessage(failure, t.language))
|
|
2356
|
+
}, index)) : null,
|
|
2357
|
+
entry.fields.map((field) => {
|
|
2358
|
+
const key = `${entry.ns}:${field.path.join(".")}`;
|
|
2359
|
+
const label = fieldLabel(field, t);
|
|
2360
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2361
|
+
className: "hub-row",
|
|
2362
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2363
|
+
className: "hub-row-copy",
|
|
2364
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2365
|
+
className: "hub-row-label",
|
|
2366
|
+
children: label
|
|
2367
|
+
}), field.description ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2368
|
+
className: "hub-row-description",
|
|
2369
|
+
children: hubText(field.description, t.language)
|
|
2370
|
+
}) : null]
|
|
2371
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2372
|
+
className: "hub-editor",
|
|
2373
|
+
children: field.editable === false ? field.description ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2374
|
+
className: "dsh-ui-help",
|
|
2375
|
+
children: t("collectionTemplate")
|
|
2376
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldMenu, {
|
|
2377
|
+
t,
|
|
2378
|
+
catalog: entry.catalog !== void 0 ? entry.catalog : catalog,
|
|
2379
|
+
current: field.current,
|
|
2380
|
+
label,
|
|
2381
|
+
allowFollow: field.allowFollow,
|
|
2382
|
+
followLabel: followLabel(field, t),
|
|
2383
|
+
reasoningEffort: field.supportsReasoning === false ? false : field.marker?.reasoningEffort,
|
|
2384
|
+
disabled: disabled || !!entry.error || !!entry.catalogError || entry.catalog === void 0 && catalog === null,
|
|
2385
|
+
onSet: (route) => setRoute(entry, field, route)
|
|
2386
|
+
})
|
|
2387
|
+
})]
|
|
2388
|
+
}, key);
|
|
1385
2389
|
})
|
|
1386
2390
|
]
|
|
1387
|
-
},
|
|
1388
|
-
|
|
1389
|
-
}, entry.ns)),
|
|
1390
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1391
|
-
className: "dsh-ui-actions",
|
|
1392
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
1393
|
-
variant: "ghost",
|
|
1394
|
-
disabled: writing !== "",
|
|
1395
|
-
onClick: () => {
|
|
1396
|
-
setEntries(null);
|
|
1397
|
-
api.list().then((value) => {
|
|
1398
|
-
setEntries(value);
|
|
1399
|
-
setError("");
|
|
1400
|
-
}, (error) => setError(t("failed", error instanceof Error ? error.message : String(error))));
|
|
1401
|
-
},
|
|
1402
|
-
children: t("refresh")
|
|
1403
|
-
})
|
|
2391
|
+
}, entry.ns))
|
|
2392
|
+
]
|
|
1404
2393
|
}),
|
|
1405
|
-
|
|
1406
|
-
role: "
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
2394
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2395
|
+
role: "tabpanel",
|
|
2396
|
+
id: "dsh-model-hub-panel-cast",
|
|
2397
|
+
"aria-labelledby": "dsh-model-hub-tab-cast",
|
|
2398
|
+
hidden: tab !== "cast",
|
|
2399
|
+
children: tab === "cast" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CastPage, {
|
|
2400
|
+
api,
|
|
2401
|
+
locale
|
|
2402
|
+
}) : null
|
|
2403
|
+
})
|
|
1410
2404
|
]
|
|
1411
2405
|
});
|
|
1412
2406
|
}
|
|
1413
|
-
function apply(ctx) {
|
|
2407
|
+
async function apply(ctx) {
|
|
1414
2408
|
const client = ctx;
|
|
2409
|
+
const disposeRemote = await client.remote.$mount(modelSettingsChangesRemote);
|
|
2410
|
+
client.effect(() => disposeRemote);
|
|
2411
|
+
const changes = createModelSettingsSubscriber(ctx, "*");
|
|
1415
2412
|
const call = async (endpoint, payload) => {
|
|
1416
2413
|
const result = await client.connection.rpc.call(CHANNEL, endpoint, payload);
|
|
1417
2414
|
if (!result?.ok) throw new Error(result?.error?.message || "Request failed");
|
|
@@ -1420,7 +2417,14 @@ function apply(ctx) {
|
|
|
1420
2417
|
const api = Object.freeze({
|
|
1421
2418
|
list: () => call("fields.list", {}),
|
|
1422
2419
|
setField: (payload) => call("fields.set", payload),
|
|
1423
|
-
modelCatalog: () => client.remote.session.modelCatalog()
|
|
2420
|
+
modelCatalog: () => client.remote.session.modelCatalog(),
|
|
2421
|
+
subscribeChanges: changes.subscribe,
|
|
2422
|
+
getCast: () => call("cast.get", {}),
|
|
2423
|
+
setCastEnabled: (payload) => call("cast.setEnabled", payload),
|
|
2424
|
+
addCastRoute: (payload) => call("cast.addRoute", payload),
|
|
2425
|
+
setCastRoute: (payload) => call("cast.setRoute", payload),
|
|
2426
|
+
removeCastRoute: (payload) => call("cast.removeRoute", payload),
|
|
2427
|
+
setCastTendency: (payload) => call("cast.setTendency", payload)
|
|
1424
2428
|
});
|
|
1425
2429
|
client.slots.inject("plugins.bundle.config", () => client.slots.register({
|
|
1426
2430
|
name: "plugins.bundle.config",
|
|
@@ -1437,6 +2441,7 @@ exports.CHANNEL = CHANNEL;
|
|
|
1437
2441
|
exports.FieldMenu = FieldMenu;
|
|
1438
2442
|
exports.HubPage = HubPage;
|
|
1439
2443
|
exports.apply = apply;
|
|
2444
|
+
exports.catalogFailures = catalogFailures;
|
|
1440
2445
|
exports.inject = inject;
|
|
1441
2446
|
exports.name = name;
|
|
1442
2447
|
|