dsh-llm-fallbacks 0.1.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +124 -0
- package/README.zh-CN.md +119 -0
- package/bundle/cordis.patch.yml +19 -0
- package/dist/chains.d.ts +167 -0
- package/dist/client/ConversationFallbackSwitch.d.ts +87 -0
- package/dist/client/FallbacksCard.d.ts +86 -0
- package/dist/client/GeneralFallbacksRow.d.ts +19 -0
- package/dist/client/fallbacks-store.d.ts +379 -0
- package/dist/client/index.d.ts +56 -0
- package/dist/client/index.js +3916 -0
- package/dist/client/locales.d.ts +281 -0
- package/dist/client/switch-guard.d.ts +17 -0
- package/dist/commands.d.ts +176 -0
- package/dist/config.d.ts +136 -0
- package/dist/cooldown.d.ts +48 -0
- package/dist/events.d.ts +36 -0
- package/dist/gateway.d.ts +176 -0
- package/dist/index.d.ts +62 -0
- package/dist/index.js +1853 -0
- package/dist/roles.d.ts +70 -0
- package/dist/selectors.d.ts +35 -0
- package/dist/state.d.ts +112 -0
- package/package.json +91 -0
- package/scripts/build-client.ts +374 -0
- package/scripts/verify-dist.mjs +68 -0
|
@@ -0,0 +1,3916 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "dsh-llm-fallbacks",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
//#region \0rolldown/runtime.js
|
|
8
|
+
var __create = Object.create;
|
|
9
|
+
var __defProp = Object.defineProperty;
|
|
10
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
11
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
12
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
13
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
14
|
+
var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
|
|
15
|
+
var __copyProps = (to, from, except, desc) => {
|
|
16
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
17
|
+
key = keys[i];
|
|
18
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
19
|
+
get: ((k) => from[k]).bind(null, key),
|
|
20
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
return to;
|
|
24
|
+
};
|
|
25
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
|
|
26
|
+
value: mod,
|
|
27
|
+
enumerable: true
|
|
28
|
+
}) : target, mod));
|
|
29
|
+
//#endregion
|
|
30
|
+
let _deepseek_ai_dsh_client_web_react = require("@deepseek-ai/dsh-client-web-react");
|
|
31
|
+
let react = require("react");
|
|
32
|
+
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
33
|
+
let _deepseek_ai_dsh_client_runtime_client = require("@deepseek-ai/dsh-client-runtime/client");
|
|
34
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
35
|
+
//#region node_modules/cosmokit/lib/index.cjs
|
|
36
|
+
var require_lib$1 = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
37
|
+
var __defProp = Object.defineProperty;
|
|
38
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
39
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
40
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
41
|
+
var __export = (target, all) => {
|
|
42
|
+
for (var name in all) __defProp(target, name, {
|
|
43
|
+
get: all[name],
|
|
44
|
+
enumerable: true
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
var __copyProps = (to, from, except, desc) => {
|
|
48
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
49
|
+
for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
50
|
+
get: () => from[key],
|
|
51
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return to;
|
|
55
|
+
};
|
|
56
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
57
|
+
var index_exports = {};
|
|
58
|
+
__export(index_exports, {
|
|
59
|
+
Binary: () => Binary,
|
|
60
|
+
Time: () => Time,
|
|
61
|
+
arrayBufferToBase64: () => arrayBufferToBase64,
|
|
62
|
+
arrayBufferToHex: () => arrayBufferToHex,
|
|
63
|
+
base64ToArrayBuffer: () => base64ToArrayBuffer,
|
|
64
|
+
camelCase: () => camelCase,
|
|
65
|
+
camelize: () => camelize,
|
|
66
|
+
capitalize: () => capitalize,
|
|
67
|
+
clone: () => clone,
|
|
68
|
+
contain: () => contain,
|
|
69
|
+
deduplicate: () => deduplicate,
|
|
70
|
+
deepEqual: () => deepEqual,
|
|
71
|
+
defineProperty: () => defineProperty,
|
|
72
|
+
difference: () => difference,
|
|
73
|
+
filterKeys: () => filterKeys,
|
|
74
|
+
formatProperty: () => formatProperty,
|
|
75
|
+
hexToArrayBuffer: () => hexToArrayBuffer,
|
|
76
|
+
hyphenate: () => hyphenate,
|
|
77
|
+
intersection: () => intersection,
|
|
78
|
+
is: () => is,
|
|
79
|
+
isNonNullable: () => isNonNullable,
|
|
80
|
+
isNullable: () => isNullable,
|
|
81
|
+
isPlainObject: () => isPlainObject,
|
|
82
|
+
makeArray: () => makeArray,
|
|
83
|
+
mapValues: () => mapValues,
|
|
84
|
+
noop: () => noop,
|
|
85
|
+
omit: () => omit,
|
|
86
|
+
paramCase: () => paramCase,
|
|
87
|
+
pick: () => pick,
|
|
88
|
+
remove: () => remove,
|
|
89
|
+
sanitize: () => sanitize,
|
|
90
|
+
snakeCase: () => snakeCase,
|
|
91
|
+
trimSlash: () => trimSlash,
|
|
92
|
+
uncapitalize: () => uncapitalize,
|
|
93
|
+
union: () => union,
|
|
94
|
+
valueMap: () => mapValues
|
|
95
|
+
});
|
|
96
|
+
module.exports = __toCommonJS(index_exports);
|
|
97
|
+
function noop() {}
|
|
98
|
+
function isNullable(value) {
|
|
99
|
+
return value === null || value === void 0;
|
|
100
|
+
}
|
|
101
|
+
function isNonNullable(value) {
|
|
102
|
+
return !isNullable(value);
|
|
103
|
+
}
|
|
104
|
+
function isPlainObject(data) {
|
|
105
|
+
return data && typeof data === "object" && !Array.isArray(data);
|
|
106
|
+
}
|
|
107
|
+
function filterKeys(object, filter) {
|
|
108
|
+
return Object.fromEntries(Object.entries(object).filter(([key, value]) => filter(key, value)));
|
|
109
|
+
}
|
|
110
|
+
function mapValues(object, transform) {
|
|
111
|
+
return Object.fromEntries(Object.entries(object).map(([key, value]) => [key, transform(value, key)]));
|
|
112
|
+
}
|
|
113
|
+
function pick(source, keys, forced) {
|
|
114
|
+
if (!keys) return { ...source };
|
|
115
|
+
const result = {};
|
|
116
|
+
for (const key of keys) if (forced || source[key] !== void 0) result[key] = source[key];
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
function omit(source, keys) {
|
|
120
|
+
if (!keys) return { ...source };
|
|
121
|
+
const result = { ...source };
|
|
122
|
+
for (const key of keys) Reflect.deleteProperty(result, key);
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
function defineProperty(object, key, value) {
|
|
126
|
+
return Object.defineProperty(object, key, {
|
|
127
|
+
writable: true,
|
|
128
|
+
value,
|
|
129
|
+
enumerable: false
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
function contain(array1, array2) {
|
|
133
|
+
return array2.every((item) => array1.includes(item));
|
|
134
|
+
}
|
|
135
|
+
function intersection(array1, array2) {
|
|
136
|
+
return array1.filter((item) => array2.includes(item));
|
|
137
|
+
}
|
|
138
|
+
function difference(array1, array2) {
|
|
139
|
+
return array1.filter((item) => !array2.includes(item));
|
|
140
|
+
}
|
|
141
|
+
function union(array1, array2) {
|
|
142
|
+
return Array.from(/* @__PURE__ */ new Set([...array1, ...array2]));
|
|
143
|
+
}
|
|
144
|
+
function deduplicate(array) {
|
|
145
|
+
return [...new Set(array)];
|
|
146
|
+
}
|
|
147
|
+
function remove(list, item) {
|
|
148
|
+
const index = list?.indexOf(item);
|
|
149
|
+
if (index >= 0) {
|
|
150
|
+
list.splice(index, 1);
|
|
151
|
+
return true;
|
|
152
|
+
} else return false;
|
|
153
|
+
}
|
|
154
|
+
function makeArray(source) {
|
|
155
|
+
return Array.isArray(source) ? source : isNullable(source) ? [] : [source];
|
|
156
|
+
}
|
|
157
|
+
function is(type, value) {
|
|
158
|
+
if (arguments.length === 1) return (value2) => is(type, value2);
|
|
159
|
+
return type in globalThis && value instanceof globalThis[type] || Object.prototype.toString.call(value).slice(8, -1) === type;
|
|
160
|
+
}
|
|
161
|
+
function isArrayBufferLike(value) {
|
|
162
|
+
return is("ArrayBuffer", value) || is("SharedArrayBuffer", value);
|
|
163
|
+
}
|
|
164
|
+
function isArrayBufferSource(value) {
|
|
165
|
+
return isArrayBufferLike(value) || ArrayBuffer.isView(value);
|
|
166
|
+
}
|
|
167
|
+
var Binary;
|
|
168
|
+
((Binary2) => {
|
|
169
|
+
Binary2.is = isArrayBufferLike;
|
|
170
|
+
Binary2.isSource = isArrayBufferSource;
|
|
171
|
+
function fromSource(source) {
|
|
172
|
+
if (ArrayBuffer.isView(source)) return source.buffer.slice(source.byteOffset, source.byteOffset + source.byteLength);
|
|
173
|
+
else return source;
|
|
174
|
+
}
|
|
175
|
+
Binary2.fromSource = fromSource;
|
|
176
|
+
function toBase64(source) {
|
|
177
|
+
source = fromSource(source);
|
|
178
|
+
if (typeof Buffer !== "undefined") return Buffer.from(source).toString("base64");
|
|
179
|
+
let binary = "";
|
|
180
|
+
const bytes = new Uint8Array(source);
|
|
181
|
+
for (let i = 0; i < bytes.byteLength; i++) binary += String.fromCharCode(bytes[i]);
|
|
182
|
+
return btoa(binary);
|
|
183
|
+
}
|
|
184
|
+
Binary2.toBase64 = toBase64;
|
|
185
|
+
function fromBase64(source) {
|
|
186
|
+
if (typeof Buffer !== "undefined") return fromSource(Buffer.from(source, "base64"));
|
|
187
|
+
return Uint8Array.from(atob(source), (c) => c.charCodeAt(0));
|
|
188
|
+
}
|
|
189
|
+
Binary2.fromBase64 = fromBase64;
|
|
190
|
+
function toHex(source) {
|
|
191
|
+
source = fromSource(source);
|
|
192
|
+
if (typeof Buffer !== "undefined") return Buffer.from(source).toString("hex");
|
|
193
|
+
return Array.from(new Uint8Array(source), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
194
|
+
}
|
|
195
|
+
Binary2.toHex = toHex;
|
|
196
|
+
function fromHex(source) {
|
|
197
|
+
if (typeof Buffer !== "undefined") return fromSource(Buffer.from(source, "hex"));
|
|
198
|
+
const hex = source.length % 2 === 0 ? source : source.slice(0, source.length - 1);
|
|
199
|
+
const buffer = [];
|
|
200
|
+
for (let i = 0; i < hex.length; i += 2) buffer.push(parseInt(`${hex[i]}${hex[i + 1]}`, 16));
|
|
201
|
+
return Uint8Array.from(buffer).buffer;
|
|
202
|
+
}
|
|
203
|
+
Binary2.fromHex = fromHex;
|
|
204
|
+
})(Binary || (Binary = {}));
|
|
205
|
+
var base64ToArrayBuffer = Binary.fromBase64;
|
|
206
|
+
var arrayBufferToBase64 = Binary.toBase64;
|
|
207
|
+
var hexToArrayBuffer = Binary.fromHex;
|
|
208
|
+
var arrayBufferToHex = Binary.toHex;
|
|
209
|
+
function clone(source, refs = /* @__PURE__ */ new Map()) {
|
|
210
|
+
if (!source || typeof source !== "object") return source;
|
|
211
|
+
if (is("Date", source)) return new Date(source.valueOf());
|
|
212
|
+
if (is("RegExp", source)) return new RegExp(source.source, source.flags);
|
|
213
|
+
if (isArrayBufferLike(source)) return source.slice(0);
|
|
214
|
+
if (ArrayBuffer.isView(source)) return source.buffer.slice(source.byteOffset, source.byteOffset + source.byteLength);
|
|
215
|
+
const cached = refs.get(source);
|
|
216
|
+
if (cached) return cached;
|
|
217
|
+
if (Array.isArray(source)) {
|
|
218
|
+
const result2 = [];
|
|
219
|
+
refs.set(source, result2);
|
|
220
|
+
source.forEach((value, index) => {
|
|
221
|
+
result2[index] = Reflect.apply(clone, null, [value, refs]);
|
|
222
|
+
});
|
|
223
|
+
return result2;
|
|
224
|
+
}
|
|
225
|
+
const result = Object.create(Object.getPrototypeOf(source));
|
|
226
|
+
refs.set(source, result);
|
|
227
|
+
for (const key of Reflect.ownKeys(source)) {
|
|
228
|
+
const descriptor = { ...Reflect.getOwnPropertyDescriptor(source, key) };
|
|
229
|
+
if ("value" in descriptor) descriptor.value = Reflect.apply(clone, null, [descriptor.value, refs]);
|
|
230
|
+
Reflect.defineProperty(result, key, descriptor);
|
|
231
|
+
}
|
|
232
|
+
return result;
|
|
233
|
+
}
|
|
234
|
+
function deepEqual(a, b, strict) {
|
|
235
|
+
if (a === b) return true;
|
|
236
|
+
if (!strict && isNullable(a) && isNullable(b)) return true;
|
|
237
|
+
if (typeof a !== typeof b) return false;
|
|
238
|
+
if (typeof a !== "object") return false;
|
|
239
|
+
if (!a || !b) return false;
|
|
240
|
+
function check(test, then) {
|
|
241
|
+
return test(a) ? test(b) ? then(a, b) : false : test(b) ? false : void 0;
|
|
242
|
+
}
|
|
243
|
+
return check(Array.isArray, (a2, b2) => a2.length === b2.length && a2.every((item, index) => deepEqual(item, b2[index]))) ?? check(is("Date"), (a2, b2) => a2.valueOf() === b2.valueOf()) ?? check(is("RegExp"), (a2, b2) => a2.source === b2.source && a2.flags === b2.flags) ?? check(isArrayBufferLike, (a2, b2) => {
|
|
244
|
+
if (a2.byteLength !== b2.byteLength) return false;
|
|
245
|
+
const viewA = new Uint8Array(a2);
|
|
246
|
+
const viewB = new Uint8Array(b2);
|
|
247
|
+
for (let i = 0; i < viewA.length; i++) if (viewA[i] !== viewB[i]) return false;
|
|
248
|
+
return true;
|
|
249
|
+
}) ?? Object.keys({
|
|
250
|
+
...a,
|
|
251
|
+
...b
|
|
252
|
+
}).every((key) => deepEqual(a[key], b[key], strict));
|
|
253
|
+
}
|
|
254
|
+
function capitalize(source) {
|
|
255
|
+
return source.charAt(0).toUpperCase() + source.slice(1);
|
|
256
|
+
}
|
|
257
|
+
function uncapitalize(source) {
|
|
258
|
+
return source.charAt(0).toLowerCase() + source.slice(1);
|
|
259
|
+
}
|
|
260
|
+
function camelCase(source) {
|
|
261
|
+
return source.replace(/[_-][a-z]/g, (str) => str.slice(1).toUpperCase());
|
|
262
|
+
}
|
|
263
|
+
function tokenize(source, delimiters, delimiter) {
|
|
264
|
+
const output = [];
|
|
265
|
+
let state = 0;
|
|
266
|
+
for (let i = 0; i < source.length; i++) {
|
|
267
|
+
const code = source.charCodeAt(i);
|
|
268
|
+
if (code >= 65 && code <= 90) {
|
|
269
|
+
if (state === 1) {
|
|
270
|
+
const next = source.charCodeAt(i + 1);
|
|
271
|
+
if (next >= 97 && next <= 122) output.push(delimiter);
|
|
272
|
+
output.push(code + 32);
|
|
273
|
+
} else {
|
|
274
|
+
if (state !== 0) output.push(delimiter);
|
|
275
|
+
output.push(code + 32);
|
|
276
|
+
}
|
|
277
|
+
state = 1;
|
|
278
|
+
} else if (code >= 97 && code <= 122) {
|
|
279
|
+
output.push(code);
|
|
280
|
+
state = 2;
|
|
281
|
+
} else if (delimiters.includes(code)) {
|
|
282
|
+
if (state !== 0) output.push(delimiter);
|
|
283
|
+
state = 0;
|
|
284
|
+
} else output.push(code);
|
|
285
|
+
}
|
|
286
|
+
return String.fromCharCode(...output);
|
|
287
|
+
}
|
|
288
|
+
function paramCase(source) {
|
|
289
|
+
return tokenize(source, [45, 95], 45);
|
|
290
|
+
}
|
|
291
|
+
function snakeCase(source) {
|
|
292
|
+
return tokenize(source, [45, 95], 95);
|
|
293
|
+
}
|
|
294
|
+
var camelize = camelCase;
|
|
295
|
+
var hyphenate = paramCase;
|
|
296
|
+
function formatProperty(key) {
|
|
297
|
+
if (typeof key !== "string") return `[${key.toString()}]`;
|
|
298
|
+
return /^[a-z_$][\w$]*$/i.test(key) ? `.${key}` : `[${JSON.stringify(key)}]`;
|
|
299
|
+
}
|
|
300
|
+
function trimSlash(source) {
|
|
301
|
+
return source.replace(/\/$/, "");
|
|
302
|
+
}
|
|
303
|
+
function sanitize(source) {
|
|
304
|
+
if (!source.startsWith("/")) source = "/" + source;
|
|
305
|
+
return trimSlash(source);
|
|
306
|
+
}
|
|
307
|
+
var Time;
|
|
308
|
+
((Time2) => {
|
|
309
|
+
Time2.millisecond = 1;
|
|
310
|
+
Time2.second = 1e3;
|
|
311
|
+
Time2.minute = Time2.second * 60;
|
|
312
|
+
Time2.hour = Time2.minute * 60;
|
|
313
|
+
Time2.day = Time2.hour * 24;
|
|
314
|
+
Time2.week = Time2.day * 7;
|
|
315
|
+
let timezoneOffset = (/* @__PURE__ */ new Date()).getTimezoneOffset();
|
|
316
|
+
function setTimezoneOffset(offset) {
|
|
317
|
+
timezoneOffset = offset;
|
|
318
|
+
}
|
|
319
|
+
Time2.setTimezoneOffset = setTimezoneOffset;
|
|
320
|
+
function getTimezoneOffset() {
|
|
321
|
+
return timezoneOffset;
|
|
322
|
+
}
|
|
323
|
+
Time2.getTimezoneOffset = getTimezoneOffset;
|
|
324
|
+
function getDateNumber(date = /* @__PURE__ */ new Date(), offset) {
|
|
325
|
+
if (typeof date === "number") date = new Date(date);
|
|
326
|
+
if (offset === void 0) offset = timezoneOffset;
|
|
327
|
+
return Math.floor((date.valueOf() / Time2.minute - offset) / 1440);
|
|
328
|
+
}
|
|
329
|
+
Time2.getDateNumber = getDateNumber;
|
|
330
|
+
function fromDateNumber(value, offset) {
|
|
331
|
+
const date = new Date(value * Time2.day);
|
|
332
|
+
if (offset === void 0) offset = timezoneOffset;
|
|
333
|
+
return new Date(+date + offset * Time2.minute);
|
|
334
|
+
}
|
|
335
|
+
Time2.fromDateNumber = fromDateNumber;
|
|
336
|
+
const numeric = /\d+(?:\.\d+)?/.source;
|
|
337
|
+
const timeRegExp = new RegExp(`^${[
|
|
338
|
+
"w(?:eek(?:s)?)?",
|
|
339
|
+
"d(?:ay(?:s)?)?",
|
|
340
|
+
"h(?:our(?:s)?)?",
|
|
341
|
+
"m(?:in(?:ute)?(?:s)?)?",
|
|
342
|
+
"s(?:ec(?:ond)?(?:s)?)?"
|
|
343
|
+
].map((unit) => `(${numeric}${unit})?`).join("")}$`);
|
|
344
|
+
function parseTime(source) {
|
|
345
|
+
const capture = timeRegExp.exec(source);
|
|
346
|
+
if (!capture) return 0;
|
|
347
|
+
return (parseFloat(capture[1]) * Time2.week || 0) + (parseFloat(capture[2]) * Time2.day || 0) + (parseFloat(capture[3]) * Time2.hour || 0) + (parseFloat(capture[4]) * Time2.minute || 0) + (parseFloat(capture[5]) * Time2.second || 0);
|
|
348
|
+
}
|
|
349
|
+
Time2.parseTime = parseTime;
|
|
350
|
+
function parseDate(date) {
|
|
351
|
+
const parsed = parseTime(date);
|
|
352
|
+
if (parsed) date = Date.now() + parsed;
|
|
353
|
+
else if (/^\d{1,2}(:\d{1,2}){1,2}$/.test(date)) date = `${(/* @__PURE__ */ new Date()).toLocaleDateString()}-${date}`;
|
|
354
|
+
else if (/^\d{1,2}-\d{1,2}-\d{1,2}(:\d{1,2}){1,2}$/.test(date)) date = `${(/* @__PURE__ */ new Date()).getFullYear()}-${date}`;
|
|
355
|
+
return date ? new Date(date) : /* @__PURE__ */ new Date();
|
|
356
|
+
}
|
|
357
|
+
Time2.parseDate = parseDate;
|
|
358
|
+
function format(ms) {
|
|
359
|
+
const abs = Math.abs(ms);
|
|
360
|
+
if (abs >= Time2.day - Time2.hour / 2) return Math.round(ms / Time2.day) + "d";
|
|
361
|
+
else if (abs >= Time2.hour - Time2.minute / 2) return Math.round(ms / Time2.hour) + "h";
|
|
362
|
+
else if (abs >= Time2.minute - Time2.second / 2) return Math.round(ms / Time2.minute) + "m";
|
|
363
|
+
else if (abs >= Time2.second) return Math.round(ms / Time2.second) + "s";
|
|
364
|
+
return ms + "ms";
|
|
365
|
+
}
|
|
366
|
+
Time2.format = format;
|
|
367
|
+
function toDigits(source, length = 2) {
|
|
368
|
+
return source.toString().padStart(length, "0");
|
|
369
|
+
}
|
|
370
|
+
Time2.toDigits = toDigits;
|
|
371
|
+
function template(template2, time = /* @__PURE__ */ new Date()) {
|
|
372
|
+
return template2.replace("yyyy", time.getFullYear().toString()).replace("yy", time.getFullYear().toString().slice(2)).replace("MM", toDigits(time.getMonth() + 1)).replace("dd", toDigits(time.getDate())).replace("hh", toDigits(time.getHours())).replace("mm", toDigits(time.getMinutes())).replace("ss", toDigits(time.getSeconds())).replace("SSS", toDigits(time.getMilliseconds(), 3));
|
|
373
|
+
}
|
|
374
|
+
Time2.template = template;
|
|
375
|
+
})(Time || (Time = {}));
|
|
376
|
+
0 && (module.exports = {
|
|
377
|
+
Binary,
|
|
378
|
+
Time,
|
|
379
|
+
arrayBufferToBase64,
|
|
380
|
+
arrayBufferToHex,
|
|
381
|
+
base64ToArrayBuffer,
|
|
382
|
+
camelCase,
|
|
383
|
+
camelize,
|
|
384
|
+
capitalize,
|
|
385
|
+
clone,
|
|
386
|
+
contain,
|
|
387
|
+
deduplicate,
|
|
388
|
+
deepEqual,
|
|
389
|
+
defineProperty,
|
|
390
|
+
difference,
|
|
391
|
+
filterKeys,
|
|
392
|
+
formatProperty,
|
|
393
|
+
hexToArrayBuffer,
|
|
394
|
+
hyphenate,
|
|
395
|
+
intersection,
|
|
396
|
+
is,
|
|
397
|
+
isNonNullable,
|
|
398
|
+
isNullable,
|
|
399
|
+
isPlainObject,
|
|
400
|
+
makeArray,
|
|
401
|
+
mapValues,
|
|
402
|
+
noop,
|
|
403
|
+
omit,
|
|
404
|
+
paramCase,
|
|
405
|
+
pick,
|
|
406
|
+
remove,
|
|
407
|
+
sanitize,
|
|
408
|
+
snakeCase,
|
|
409
|
+
trimSlash,
|
|
410
|
+
uncapitalize,
|
|
411
|
+
union,
|
|
412
|
+
valueMap
|
|
413
|
+
});
|
|
414
|
+
}));
|
|
415
|
+
//#endregion
|
|
416
|
+
//#region src/selectors.ts
|
|
417
|
+
var import_lib = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
418
|
+
var __defProp = Object.defineProperty;
|
|
419
|
+
var __name = (target, value) => __defProp(target, "name", {
|
|
420
|
+
value,
|
|
421
|
+
configurable: true
|
|
422
|
+
});
|
|
423
|
+
var import_cosmokit = require_lib$1();
|
|
424
|
+
var kSchema = Symbol.for("schemastery");
|
|
425
|
+
var kValidationError = Symbol.for("ValidationError");
|
|
426
|
+
globalThis.__schemastery_index__ ??= 0;
|
|
427
|
+
globalThis.__schemastery_refs__ = void 0;
|
|
428
|
+
var ValidationError = class extends TypeError {
|
|
429
|
+
constructor(message, options) {
|
|
430
|
+
let prefix = "$";
|
|
431
|
+
for (const segment of options.path || []) if (typeof segment === "string") prefix += "." + segment;
|
|
432
|
+
else if (typeof segment === "number") prefix += "[" + segment + "]";
|
|
433
|
+
else if (typeof segment === "symbol") prefix += `[Symbol(${segment.toString()})]`;
|
|
434
|
+
if (prefix.startsWith(".")) prefix = prefix.slice(1);
|
|
435
|
+
super((prefix === "$" ? "" : `${prefix} `) + message);
|
|
436
|
+
this.options = options;
|
|
437
|
+
}
|
|
438
|
+
static {
|
|
439
|
+
__name(this, "ValidationError");
|
|
440
|
+
}
|
|
441
|
+
name = "ValidationError";
|
|
442
|
+
static is(error) {
|
|
443
|
+
return !!error?.[kValidationError];
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
Object.defineProperty(ValidationError.prototype, kValidationError, { value: true });
|
|
447
|
+
var Schema = /* @__PURE__ */ __name(function(options) {
|
|
448
|
+
const schema = /* @__PURE__ */ __name(function(data, options2 = {}) {
|
|
449
|
+
return Schema.resolve(data, schema, options2)[0];
|
|
450
|
+
}, "schema");
|
|
451
|
+
if (options.refs) {
|
|
452
|
+
const refs = (0, import_cosmokit.valueMap)(options.refs, (options2) => new Schema(options2));
|
|
453
|
+
const getRef = /* @__PURE__ */ __name((uid) => refs[uid], "getRef");
|
|
454
|
+
for (const key in refs) {
|
|
455
|
+
const options2 = refs[key];
|
|
456
|
+
options2.sKey = getRef(options2.sKey);
|
|
457
|
+
options2.inner = getRef(options2.inner);
|
|
458
|
+
options2.list = options2.list && options2.list.map(getRef);
|
|
459
|
+
options2.dict = options2.dict && (0, import_cosmokit.valueMap)(options2.dict, getRef);
|
|
460
|
+
}
|
|
461
|
+
return refs[options.uid];
|
|
462
|
+
}
|
|
463
|
+
Object.assign(schema, options);
|
|
464
|
+
if (typeof schema.callback === "string") try {
|
|
465
|
+
schema.callback = new Function("return " + schema.callback)();
|
|
466
|
+
} catch {}
|
|
467
|
+
Object.defineProperty(schema, "uid", { value: globalThis.__schemastery_index__++ });
|
|
468
|
+
Object.setPrototypeOf(schema, Schema.prototype);
|
|
469
|
+
schema.meta ||= {};
|
|
470
|
+
schema.toString = schema.toString.bind(schema);
|
|
471
|
+
return schema;
|
|
472
|
+
}, "Schema");
|
|
473
|
+
Schema.prototype = Object.create(Function.prototype);
|
|
474
|
+
Schema.prototype[kSchema] = true;
|
|
475
|
+
Object.defineProperty(Schema.prototype, "~standard", { get() {
|
|
476
|
+
return {
|
|
477
|
+
version: 1,
|
|
478
|
+
vendor: "schemastery",
|
|
479
|
+
validate: /* @__PURE__ */ __name((value) => {
|
|
480
|
+
try {
|
|
481
|
+
return { value: Schema.resolve(value, this, {})[0] };
|
|
482
|
+
} catch (error) {
|
|
483
|
+
if (ValidationError.is(error)) return { issues: [{
|
|
484
|
+
message: error.message,
|
|
485
|
+
path: error.options.path
|
|
486
|
+
}] };
|
|
487
|
+
throw error;
|
|
488
|
+
}
|
|
489
|
+
}, "validate")
|
|
490
|
+
};
|
|
491
|
+
} });
|
|
492
|
+
Schema.ValidationError = ValidationError;
|
|
493
|
+
Schema.prototype.toJSON = /* @__PURE__ */ __name(function toJSON() {
|
|
494
|
+
if (globalThis.__schemastery_refs__) {
|
|
495
|
+
globalThis.__schemastery_refs__[this.uid] ??= JSON.parse(JSON.stringify({ ...this }));
|
|
496
|
+
return this.uid;
|
|
497
|
+
}
|
|
498
|
+
globalThis.__schemastery_refs__ = { [this.uid]: { ...this } };
|
|
499
|
+
globalThis.__schemastery_refs__[this.uid] = JSON.parse(JSON.stringify({ ...this }));
|
|
500
|
+
const result = {
|
|
501
|
+
uid: this.uid,
|
|
502
|
+
refs: globalThis.__schemastery_refs__
|
|
503
|
+
};
|
|
504
|
+
globalThis.__schemastery_refs__ = void 0;
|
|
505
|
+
return result;
|
|
506
|
+
}, "toJSON");
|
|
507
|
+
Schema.prototype.set = /* @__PURE__ */ __name(function set(key, value) {
|
|
508
|
+
this.dict[key] = value;
|
|
509
|
+
return this;
|
|
510
|
+
}, "set");
|
|
511
|
+
Schema.prototype.push = /* @__PURE__ */ __name(function push(value) {
|
|
512
|
+
this.list.push(value);
|
|
513
|
+
return this;
|
|
514
|
+
}, "push");
|
|
515
|
+
function mergeDesc(original, messages) {
|
|
516
|
+
const result = typeof original === "string" ? { "": original } : { ...original };
|
|
517
|
+
for (const locale in messages) {
|
|
518
|
+
const value = messages[locale];
|
|
519
|
+
if (value?.$description || value?.$desc) result[locale] = value.$description || value.$desc;
|
|
520
|
+
else if (typeof value === "string") result[locale] = value;
|
|
521
|
+
}
|
|
522
|
+
return result;
|
|
523
|
+
}
|
|
524
|
+
__name(mergeDesc, "mergeDesc");
|
|
525
|
+
function getInner(value) {
|
|
526
|
+
return value?.$value ?? value?.$inner;
|
|
527
|
+
}
|
|
528
|
+
__name(getInner, "getInner");
|
|
529
|
+
function extractKeys(data) {
|
|
530
|
+
return (0, import_cosmokit.filterKeys)(data ?? {}, (key) => !key.startsWith("$"));
|
|
531
|
+
}
|
|
532
|
+
__name(extractKeys, "extractKeys");
|
|
533
|
+
Schema.prototype.i18n = /* @__PURE__ */ __name(function i18n(messages) {
|
|
534
|
+
const schema = Schema(this);
|
|
535
|
+
const desc = mergeDesc(schema.meta.description, messages);
|
|
536
|
+
if (Object.keys(desc).length) schema.meta.description = desc;
|
|
537
|
+
if (schema.dict) schema.dict = (0, import_cosmokit.valueMap)(schema.dict, (inner, key) => {
|
|
538
|
+
return inner.i18n((0, import_cosmokit.valueMap)(messages, (data) => getInner(data)?.[key] ?? data?.[key]));
|
|
539
|
+
});
|
|
540
|
+
if (schema.list) schema.list = schema.list.map((inner, index) => {
|
|
541
|
+
return inner.i18n((0, import_cosmokit.valueMap)(messages, (data = {}) => {
|
|
542
|
+
if (Array.isArray(getInner(data))) return getInner(data)[index];
|
|
543
|
+
if (Array.isArray(data)) return data[index];
|
|
544
|
+
return extractKeys(data);
|
|
545
|
+
}));
|
|
546
|
+
});
|
|
547
|
+
if (schema.inner) schema.inner = schema.inner.i18n((0, import_cosmokit.valueMap)(messages, (data) => {
|
|
548
|
+
if (getInner(data)) return getInner(data);
|
|
549
|
+
return extractKeys(data);
|
|
550
|
+
}));
|
|
551
|
+
if (schema.sKey) schema.sKey = schema.sKey.i18n((0, import_cosmokit.valueMap)(messages, (data) => data?.$key));
|
|
552
|
+
return schema;
|
|
553
|
+
}, "i18n");
|
|
554
|
+
Schema.prototype.extra = /* @__PURE__ */ __name(function extra(key, value) {
|
|
555
|
+
const schema = Schema(this);
|
|
556
|
+
schema.meta = {
|
|
557
|
+
...schema.meta,
|
|
558
|
+
[key]: value
|
|
559
|
+
};
|
|
560
|
+
return schema;
|
|
561
|
+
}, "extra");
|
|
562
|
+
for (const key of [
|
|
563
|
+
"required",
|
|
564
|
+
"disabled",
|
|
565
|
+
"collapse",
|
|
566
|
+
"hidden",
|
|
567
|
+
"loose"
|
|
568
|
+
]) Object.assign(Schema.prototype, { [key](value = true) {
|
|
569
|
+
const schema = Schema(this);
|
|
570
|
+
schema.meta = {
|
|
571
|
+
...schema.meta,
|
|
572
|
+
[key]: value
|
|
573
|
+
};
|
|
574
|
+
return schema;
|
|
575
|
+
} });
|
|
576
|
+
Schema.prototype.deprecated = /* @__PURE__ */ __name(function deprecated() {
|
|
577
|
+
const schema = Schema(this);
|
|
578
|
+
schema.meta.badges ||= [];
|
|
579
|
+
schema.meta.badges.push({
|
|
580
|
+
text: "deprecated",
|
|
581
|
+
type: "danger"
|
|
582
|
+
});
|
|
583
|
+
return schema;
|
|
584
|
+
}, "deprecated");
|
|
585
|
+
Schema.prototype.experimental = /* @__PURE__ */ __name(function experimental() {
|
|
586
|
+
const schema = Schema(this);
|
|
587
|
+
schema.meta.badges ||= [];
|
|
588
|
+
schema.meta.badges.push({
|
|
589
|
+
text: "experimental",
|
|
590
|
+
type: "warning"
|
|
591
|
+
});
|
|
592
|
+
return schema;
|
|
593
|
+
}, "experimental");
|
|
594
|
+
Schema.prototype.pattern = /* @__PURE__ */ __name(function pattern(regexp) {
|
|
595
|
+
const schema = Schema(this);
|
|
596
|
+
const pattern2 = (0, import_cosmokit.pick)(regexp, ["source", "flags"]);
|
|
597
|
+
schema.meta = {
|
|
598
|
+
...schema.meta,
|
|
599
|
+
pattern: pattern2
|
|
600
|
+
};
|
|
601
|
+
return schema;
|
|
602
|
+
}, "pattern");
|
|
603
|
+
Schema.prototype.simplify = /* @__PURE__ */ __name(function simplify(value) {
|
|
604
|
+
if ((0, import_cosmokit.deepEqual)(value, this.meta.default, this.type === "dict")) return null;
|
|
605
|
+
if ((0, import_cosmokit.isNullable)(value)) return value;
|
|
606
|
+
if (this.type === "object" || this.type === "dict") {
|
|
607
|
+
const result = {};
|
|
608
|
+
for (const key in value) {
|
|
609
|
+
const item = (this.type === "object" ? this.dict[key] : this.inner)?.simplify(value[key]);
|
|
610
|
+
if (this.type === "dict" || !(0, import_cosmokit.isNullable)(item)) result[key] = item;
|
|
611
|
+
}
|
|
612
|
+
if ((0, import_cosmokit.deepEqual)(result, this.meta.default, this.type === "dict")) return null;
|
|
613
|
+
return result;
|
|
614
|
+
} else if (this.type === "array" || this.type === "tuple") {
|
|
615
|
+
const result = [];
|
|
616
|
+
value.forEach((value2, index) => {
|
|
617
|
+
const schema = this.type === "array" ? this.inner : this.list[index];
|
|
618
|
+
const item = schema ? schema.simplify(value2) : value2;
|
|
619
|
+
result.push(item);
|
|
620
|
+
});
|
|
621
|
+
return result;
|
|
622
|
+
} else if (this.type === "intersect") {
|
|
623
|
+
const result = {};
|
|
624
|
+
for (const item of this.list) Object.assign(result, item.simplify(value));
|
|
625
|
+
return result;
|
|
626
|
+
} else if (this.type === "union") for (const schema of this.list) try {
|
|
627
|
+
Schema.resolve(value, schema, {});
|
|
628
|
+
return schema.simplify(value);
|
|
629
|
+
} catch {}
|
|
630
|
+
return value;
|
|
631
|
+
}, "simplify");
|
|
632
|
+
Schema.prototype.toString = /* @__PURE__ */ __name(function toString(inline) {
|
|
633
|
+
return formatters[this.type]?.(this, inline) ?? `Schema<${this.type}>`;
|
|
634
|
+
}, "toString");
|
|
635
|
+
Schema.prototype.role = /* @__PURE__ */ __name(function role(role, extra2) {
|
|
636
|
+
const schema = Schema(this);
|
|
637
|
+
schema.meta = {
|
|
638
|
+
...schema.meta,
|
|
639
|
+
role,
|
|
640
|
+
extra: extra2
|
|
641
|
+
};
|
|
642
|
+
return schema;
|
|
643
|
+
}, "role");
|
|
644
|
+
for (const key of [
|
|
645
|
+
"default",
|
|
646
|
+
"link",
|
|
647
|
+
"comment",
|
|
648
|
+
"description",
|
|
649
|
+
"max",
|
|
650
|
+
"min",
|
|
651
|
+
"step"
|
|
652
|
+
]) Object.assign(Schema.prototype, { [key](value) {
|
|
653
|
+
const schema = Schema(this);
|
|
654
|
+
schema.meta = {
|
|
655
|
+
...schema.meta,
|
|
656
|
+
[key]: value
|
|
657
|
+
};
|
|
658
|
+
return schema;
|
|
659
|
+
} });
|
|
660
|
+
var resolvers = {};
|
|
661
|
+
Schema.extend = /* @__PURE__ */ __name(function extend(type, resolve2) {
|
|
662
|
+
resolvers[type] = resolve2;
|
|
663
|
+
}, "extend");
|
|
664
|
+
Schema.resolve = /* @__PURE__ */ __name(function resolve(data, schema, options = {}, strict = false) {
|
|
665
|
+
if (!schema) return [data];
|
|
666
|
+
if (options.ignore?.(data, schema)) return [data];
|
|
667
|
+
if ((0, import_cosmokit.isNullable)(data) && schema.type !== "lazy") {
|
|
668
|
+
if (schema.meta.required) throw new ValidationError(`missing required value`, options);
|
|
669
|
+
let current = schema;
|
|
670
|
+
let fallback = schema.meta.default;
|
|
671
|
+
while (current?.type === "intersect" && (0, import_cosmokit.isNullable)(fallback)) {
|
|
672
|
+
current = current.list[0];
|
|
673
|
+
fallback = current?.meta.default;
|
|
674
|
+
}
|
|
675
|
+
if ((0, import_cosmokit.isNullable)(fallback)) return [data];
|
|
676
|
+
data = (0, import_cosmokit.clone)(fallback);
|
|
677
|
+
}
|
|
678
|
+
const callback = resolvers[schema.type];
|
|
679
|
+
if (!callback) throw new ValidationError(`unsupported type "${schema.type}"`, options);
|
|
680
|
+
try {
|
|
681
|
+
return callback(data, schema, options, strict);
|
|
682
|
+
} catch (error) {
|
|
683
|
+
if (!schema.meta.loose) throw error;
|
|
684
|
+
return [schema.meta.default];
|
|
685
|
+
}
|
|
686
|
+
}, "resolve");
|
|
687
|
+
Schema.from = /* @__PURE__ */ __name(function from(source) {
|
|
688
|
+
if ((0, import_cosmokit.isNullable)(source)) return Schema.any();
|
|
689
|
+
else if ([
|
|
690
|
+
"string",
|
|
691
|
+
"number",
|
|
692
|
+
"boolean"
|
|
693
|
+
].includes(typeof source)) return Schema.const(source).required();
|
|
694
|
+
else if (source[kSchema]) return source;
|
|
695
|
+
else if (typeof source === "function") switch (source) {
|
|
696
|
+
case String: return Schema.string().required();
|
|
697
|
+
case Number: return Schema.number().required();
|
|
698
|
+
case Boolean: return Schema.boolean().required();
|
|
699
|
+
case Function: return Schema.function().required();
|
|
700
|
+
default: return Schema.is(source).required();
|
|
701
|
+
}
|
|
702
|
+
else throw new TypeError(`cannot infer schema from ${source}`);
|
|
703
|
+
}, "from");
|
|
704
|
+
Schema.lazy = /* @__PURE__ */ __name(function lazy(builder) {
|
|
705
|
+
const schema = new Schema({
|
|
706
|
+
type: "lazy",
|
|
707
|
+
builder,
|
|
708
|
+
inner: { toJSON: /* @__PURE__ */ __name(() => {
|
|
709
|
+
if (!schema.inner[kSchema]) {
|
|
710
|
+
schema.inner = schema.builder();
|
|
711
|
+
schema.inner.meta = {
|
|
712
|
+
...schema.meta,
|
|
713
|
+
...schema.inner.meta
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
return schema.inner.toJSON();
|
|
717
|
+
}, "toJSON") }
|
|
718
|
+
});
|
|
719
|
+
return schema;
|
|
720
|
+
}, "lazy");
|
|
721
|
+
Schema.natural = /* @__PURE__ */ __name(function natural() {
|
|
722
|
+
return Schema.number().step(1).min(0);
|
|
723
|
+
}, "natural");
|
|
724
|
+
Schema.percent = /* @__PURE__ */ __name(function percent() {
|
|
725
|
+
return Schema.number().step(.01).min(0).max(1).role("slider");
|
|
726
|
+
}, "percent");
|
|
727
|
+
Schema.date = /* @__PURE__ */ __name(function date() {
|
|
728
|
+
return Schema.union([Schema.is(Date), Schema.transform(Schema.string().role("datetime"), (value, options) => {
|
|
729
|
+
const date2 = new Date(value);
|
|
730
|
+
if (isNaN(+date2)) throw new ValidationError(`invalid date "${value}"`, options);
|
|
731
|
+
return date2;
|
|
732
|
+
}, true)]);
|
|
733
|
+
}, "date");
|
|
734
|
+
Schema.regExp = /* @__PURE__ */ __name(function regExp(flag = "") {
|
|
735
|
+
return Schema.union([Schema.is(RegExp), Schema.transform(Schema.string().role("regexp", { flag }), (value, options) => {
|
|
736
|
+
try {
|
|
737
|
+
return new RegExp(value, flag);
|
|
738
|
+
} catch (e) {
|
|
739
|
+
throw new ValidationError(e.message, options);
|
|
740
|
+
}
|
|
741
|
+
}, true)]);
|
|
742
|
+
}, "regExp");
|
|
743
|
+
Schema.arrayBuffer = /* @__PURE__ */ __name(function arrayBuffer(encoding) {
|
|
744
|
+
return Schema.union([
|
|
745
|
+
Schema.is(ArrayBuffer),
|
|
746
|
+
Schema.is(SharedArrayBuffer),
|
|
747
|
+
Schema.transform(Schema.any(), (value, options) => {
|
|
748
|
+
if (import_cosmokit.Binary.isSource(value)) return import_cosmokit.Binary.fromSource(value);
|
|
749
|
+
throw new ValidationError(`expected ArrayBufferSource but got ${value}`, options);
|
|
750
|
+
}, true),
|
|
751
|
+
...encoding ? [Schema.transform(Schema.string(), (value, options) => {
|
|
752
|
+
try {
|
|
753
|
+
return encoding === "base64" ? import_cosmokit.Binary.fromBase64(value) : import_cosmokit.Binary.fromHex(value);
|
|
754
|
+
} catch (e) {
|
|
755
|
+
throw new ValidationError(e.message, options);
|
|
756
|
+
}
|
|
757
|
+
}, true)] : []
|
|
758
|
+
]);
|
|
759
|
+
}, "arrayBuffer");
|
|
760
|
+
Schema.extend("lazy", (data, schema, options, strict) => {
|
|
761
|
+
if (!schema.inner[kSchema]) {
|
|
762
|
+
schema.inner = schema.builder();
|
|
763
|
+
schema.inner.meta = {
|
|
764
|
+
...schema.meta,
|
|
765
|
+
...schema.inner.meta
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
return Schema.resolve(data, schema.inner, options, strict);
|
|
769
|
+
});
|
|
770
|
+
Schema.extend("any", (data) => {
|
|
771
|
+
return [data];
|
|
772
|
+
});
|
|
773
|
+
Schema.extend("never", (data, _, options) => {
|
|
774
|
+
throw new ValidationError(`expected nullable but got ${data}`, options);
|
|
775
|
+
});
|
|
776
|
+
Schema.extend("const", (data, { value }, options) => {
|
|
777
|
+
if ((0, import_cosmokit.deepEqual)(data, value)) return [value];
|
|
778
|
+
throw new ValidationError(`expected ${value} but got ${data}`, options);
|
|
779
|
+
});
|
|
780
|
+
function checkWithinRange(data, meta, description, options, skipMin = false) {
|
|
781
|
+
const { max = Infinity, min = -Infinity } = meta;
|
|
782
|
+
if (data > max) throw new ValidationError(`expected ${description} <= ${max} but got ${data}`, options);
|
|
783
|
+
if (data < min && !skipMin) throw new ValidationError(`expected ${description} >= ${min} but got ${data}`, options);
|
|
784
|
+
}
|
|
785
|
+
__name(checkWithinRange, "checkWithinRange");
|
|
786
|
+
Schema.extend("string", (data, { meta }, options) => {
|
|
787
|
+
if (typeof data !== "string") throw new ValidationError(`expected string but got ${data}`, options);
|
|
788
|
+
if (meta.pattern) {
|
|
789
|
+
const regexp = new RegExp(meta.pattern.source, meta.pattern.flags);
|
|
790
|
+
if (!regexp.test(data)) throw new ValidationError(`expect string to match regexp ${regexp}`, options);
|
|
791
|
+
}
|
|
792
|
+
checkWithinRange(data.length, meta, "string length", options);
|
|
793
|
+
return [data];
|
|
794
|
+
});
|
|
795
|
+
function decimalShift(data, digits) {
|
|
796
|
+
const str = data.toString();
|
|
797
|
+
if (str.includes("e")) return data * Math.pow(10, digits);
|
|
798
|
+
const index = str.indexOf(".");
|
|
799
|
+
if (index === -1) return data * Math.pow(10, digits);
|
|
800
|
+
const frac = str.slice(index + 1);
|
|
801
|
+
const integer = str.slice(0, index);
|
|
802
|
+
if (frac.length <= digits) return +(integer + frac.padEnd(digits, "0"));
|
|
803
|
+
return +(integer + frac.slice(0, digits) + "." + frac.slice(digits));
|
|
804
|
+
}
|
|
805
|
+
__name(decimalShift, "decimalShift");
|
|
806
|
+
function isMultipleOf(data, min, step) {
|
|
807
|
+
step = Math.abs(step);
|
|
808
|
+
if (!/^\d+\.\d+$/.test(step.toString())) return (data - min) % step === 0;
|
|
809
|
+
const index = step.toString().indexOf(".");
|
|
810
|
+
const digits = step.toString().slice(index + 1).length;
|
|
811
|
+
return Math.abs(decimalShift(data, digits) - decimalShift(min, digits)) % decimalShift(step, digits) === 0;
|
|
812
|
+
}
|
|
813
|
+
__name(isMultipleOf, "isMultipleOf");
|
|
814
|
+
Schema.extend("number", (data, { meta }, options) => {
|
|
815
|
+
if (typeof data !== "number") throw new ValidationError(`expected number but got ${data}`, options);
|
|
816
|
+
checkWithinRange(data, meta, "number", options);
|
|
817
|
+
const { step } = meta;
|
|
818
|
+
if (step && !isMultipleOf(data, meta.min ?? 0, step)) throw new ValidationError(`expected number multiple of ${step} but got ${data}`, options);
|
|
819
|
+
return [data];
|
|
820
|
+
});
|
|
821
|
+
Schema.extend("boolean", (data, _, options) => {
|
|
822
|
+
if (typeof data === "boolean") return [data];
|
|
823
|
+
throw new ValidationError(`expected boolean but got ${data}`, options);
|
|
824
|
+
});
|
|
825
|
+
Schema.extend("bitset", (data, { bits, meta }, options) => {
|
|
826
|
+
let value = 0, keys = [];
|
|
827
|
+
if (typeof data === "number") {
|
|
828
|
+
value = data;
|
|
829
|
+
for (const key in bits) if (data & bits[key]) keys.push(key);
|
|
830
|
+
} else if (Array.isArray(data)) {
|
|
831
|
+
keys = data;
|
|
832
|
+
for (const key of keys) {
|
|
833
|
+
if (typeof key !== "string") throw new ValidationError(`expected string but got ${key}`, options);
|
|
834
|
+
if (key in bits) value |= bits[key];
|
|
835
|
+
}
|
|
836
|
+
} else throw new ValidationError(`expected number or array but got ${data}`, options);
|
|
837
|
+
if (value === meta.default) return [value];
|
|
838
|
+
return [value, keys];
|
|
839
|
+
});
|
|
840
|
+
Schema.extend("function", (data, _, options) => {
|
|
841
|
+
if (typeof data === "function") return [data];
|
|
842
|
+
throw new ValidationError(`expected function but got ${data}`, options);
|
|
843
|
+
});
|
|
844
|
+
Schema.extend("is", (data, { constructor }, options) => {
|
|
845
|
+
if (typeof constructor === "function") {
|
|
846
|
+
if (data instanceof constructor) return [data];
|
|
847
|
+
throw new ValidationError(`expected ${constructor.name} but got ${data}`, options);
|
|
848
|
+
} else {
|
|
849
|
+
if ((0, import_cosmokit.isNullable)(data)) throw new ValidationError(`expected ${constructor} but got ${data}`, options);
|
|
850
|
+
let prototype = Object.getPrototypeOf(data);
|
|
851
|
+
while (prototype) {
|
|
852
|
+
if (prototype.constructor?.name === constructor) return [data];
|
|
853
|
+
prototype = Object.getPrototypeOf(prototype);
|
|
854
|
+
}
|
|
855
|
+
throw new ValidationError(`expected ${constructor} but got ${data}`, options);
|
|
856
|
+
}
|
|
857
|
+
});
|
|
858
|
+
function property(data, key, schema, options) {
|
|
859
|
+
try {
|
|
860
|
+
const [value, adapted] = Schema.resolve(data[key], schema, {
|
|
861
|
+
...options,
|
|
862
|
+
path: [...options.path || [], key]
|
|
863
|
+
});
|
|
864
|
+
if (adapted !== void 0) data[key] = adapted;
|
|
865
|
+
return value;
|
|
866
|
+
} catch (e) {
|
|
867
|
+
if (!options?.autofix) throw e;
|
|
868
|
+
delete data[key];
|
|
869
|
+
return schema.meta.default;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
__name(property, "property");
|
|
873
|
+
Schema.extend("array", (data, { inner, meta }, options) => {
|
|
874
|
+
if (!Array.isArray(data)) throw new ValidationError(`expected array but got ${data}`, options);
|
|
875
|
+
checkWithinRange(data.length, meta, "array length", options, !(0, import_cosmokit.isNullable)(inner.meta.default));
|
|
876
|
+
return [data.map((_, index) => property(data, index, inner, options))];
|
|
877
|
+
});
|
|
878
|
+
Schema.extend("dict", (data, { inner, sKey }, options, strict) => {
|
|
879
|
+
if (!(0, import_cosmokit.isPlainObject)(data)) throw new ValidationError(`expected object but got ${data}`, options);
|
|
880
|
+
const result = {};
|
|
881
|
+
for (const key in data) {
|
|
882
|
+
let rKey;
|
|
883
|
+
try {
|
|
884
|
+
rKey = Schema.resolve(key, sKey, options)[0];
|
|
885
|
+
} catch (error) {
|
|
886
|
+
if (strict) continue;
|
|
887
|
+
throw error;
|
|
888
|
+
}
|
|
889
|
+
result[rKey] = property(data, key, inner, options);
|
|
890
|
+
data[rKey] = data[key];
|
|
891
|
+
if (key !== rKey) delete data[key];
|
|
892
|
+
}
|
|
893
|
+
return [result];
|
|
894
|
+
});
|
|
895
|
+
Schema.extend("tuple", (data, { list }, options, strict) => {
|
|
896
|
+
if (!Array.isArray(data)) throw new ValidationError(`expected array but got ${data}`, options);
|
|
897
|
+
const result = list.map((inner, index) => property(data, index, inner, options));
|
|
898
|
+
if (strict) return [result];
|
|
899
|
+
result.push(...data.slice(list.length));
|
|
900
|
+
return [result];
|
|
901
|
+
});
|
|
902
|
+
function merge(result, data) {
|
|
903
|
+
for (const key in data) {
|
|
904
|
+
if (key in result) continue;
|
|
905
|
+
result[key] = data[key];
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
__name(merge, "merge");
|
|
909
|
+
Schema.extend("object", (data, { dict }, options, strict) => {
|
|
910
|
+
if (!(0, import_cosmokit.isPlainObject)(data)) throw new ValidationError(`expected object but got ${data}`, options);
|
|
911
|
+
const result = {};
|
|
912
|
+
for (const key in dict) {
|
|
913
|
+
const value = property(data, key, dict[key], options);
|
|
914
|
+
if (!(0, import_cosmokit.isNullable)(value) || key in data) result[key] = value;
|
|
915
|
+
}
|
|
916
|
+
if (!strict) merge(result, data);
|
|
917
|
+
return [result];
|
|
918
|
+
});
|
|
919
|
+
Schema.extend("union", (data, { list, toString: toString2 }, options, strict) => {
|
|
920
|
+
const messages = [];
|
|
921
|
+
for (const inner of list) try {
|
|
922
|
+
return Schema.resolve(data, inner, options, strict);
|
|
923
|
+
} catch (error) {
|
|
924
|
+
messages.push(error);
|
|
925
|
+
}
|
|
926
|
+
throw new ValidationError(`expected ${toString2()} but got ${JSON.stringify(data)}`, options);
|
|
927
|
+
});
|
|
928
|
+
Schema.extend("intersect", (data, { list, toString: toString2 }, options, strict) => {
|
|
929
|
+
if (!list.length) return [data];
|
|
930
|
+
let result;
|
|
931
|
+
for (const inner of list) {
|
|
932
|
+
const value = Schema.resolve(data, inner, options, true)[0];
|
|
933
|
+
if ((0, import_cosmokit.isNullable)(value)) continue;
|
|
934
|
+
if ((0, import_cosmokit.isNullable)(result)) result = value;
|
|
935
|
+
else if (typeof result !== typeof value) throw new ValidationError(`expected ${toString2()} but got ${JSON.stringify(data)}`, options);
|
|
936
|
+
else if (typeof value === "object") merge(result ??= {}, value);
|
|
937
|
+
else if (result !== value) throw new ValidationError(`expected ${toString2()} but got ${JSON.stringify(data)}`, options);
|
|
938
|
+
}
|
|
939
|
+
if (!strict && (0, import_cosmokit.isPlainObject)(data)) merge(result, data);
|
|
940
|
+
return [result];
|
|
941
|
+
});
|
|
942
|
+
Schema.extend("transform", (data, { inner, callback, preserve }, options) => {
|
|
943
|
+
const [result, adapted = data] = Schema.resolve(data, inner, options, true);
|
|
944
|
+
if (preserve) return [callback(result)];
|
|
945
|
+
else return [callback(result), callback(adapted)];
|
|
946
|
+
});
|
|
947
|
+
var formatters = {};
|
|
948
|
+
function defineMethod(name, keys, format) {
|
|
949
|
+
formatters[name] = format;
|
|
950
|
+
Object.assign(Schema, { [name](...args) {
|
|
951
|
+
const schema = new Schema({ type: name });
|
|
952
|
+
keys.forEach((key, index) => {
|
|
953
|
+
switch (key) {
|
|
954
|
+
case "sKey":
|
|
955
|
+
schema.sKey = args[index] ?? Schema.string();
|
|
956
|
+
break;
|
|
957
|
+
case "inner":
|
|
958
|
+
schema.inner = Schema.from(args[index]);
|
|
959
|
+
break;
|
|
960
|
+
case "list":
|
|
961
|
+
schema.list = args[index].map(Schema.from);
|
|
962
|
+
break;
|
|
963
|
+
case "dict":
|
|
964
|
+
schema.dict = (0, import_cosmokit.valueMap)(args[index], Schema.from);
|
|
965
|
+
break;
|
|
966
|
+
case "bits":
|
|
967
|
+
schema.bits = {};
|
|
968
|
+
for (const key2 in args[index]) {
|
|
969
|
+
if (typeof args[index][key2] !== "number") continue;
|
|
970
|
+
schema.bits[key2] = args[index][key2];
|
|
971
|
+
}
|
|
972
|
+
break;
|
|
973
|
+
case "callback": {
|
|
974
|
+
const callback = schema.callback = args[index];
|
|
975
|
+
callback["toJSON"] ||= () => callback.toString();
|
|
976
|
+
break;
|
|
977
|
+
}
|
|
978
|
+
case "constructor": {
|
|
979
|
+
const constructor = schema.constructor = args[index];
|
|
980
|
+
if (typeof constructor === "function") constructor["toJSON"] ||= () => constructor["name"];
|
|
981
|
+
break;
|
|
982
|
+
}
|
|
983
|
+
default: schema[key] = args[index];
|
|
984
|
+
}
|
|
985
|
+
});
|
|
986
|
+
if (name === "object" || name === "dict") schema.meta.default = {};
|
|
987
|
+
else if (name === "array" || name === "tuple") schema.meta.default = [];
|
|
988
|
+
else if (name === "bitset") schema.meta.default = 0;
|
|
989
|
+
return schema;
|
|
990
|
+
} });
|
|
991
|
+
}
|
|
992
|
+
__name(defineMethod, "defineMethod");
|
|
993
|
+
defineMethod("is", ["constructor"], ({ constructor }) => {
|
|
994
|
+
if (typeof constructor === "function") return constructor.name;
|
|
995
|
+
else return constructor;
|
|
996
|
+
});
|
|
997
|
+
defineMethod("any", [], () => "any");
|
|
998
|
+
defineMethod("never", [], () => "never");
|
|
999
|
+
defineMethod("const", ["value"], ({ value }) => typeof value === "string" ? JSON.stringify(value) : value);
|
|
1000
|
+
defineMethod("string", [], () => "string");
|
|
1001
|
+
defineMethod("number", [], () => "number");
|
|
1002
|
+
defineMethod("boolean", [], () => "boolean");
|
|
1003
|
+
defineMethod("bitset", ["bits"], () => "bitset");
|
|
1004
|
+
defineMethod("function", [], () => "function");
|
|
1005
|
+
defineMethod("array", ["inner"], ({ inner }) => `${inner.toString(true)}[]`);
|
|
1006
|
+
defineMethod("dict", ["inner", "sKey"], ({ inner, sKey }) => `{ [key: ${sKey.toString()}]: ${inner.toString()} }`);
|
|
1007
|
+
defineMethod("tuple", ["list"], ({ list }) => `[${list.map((inner) => inner.toString()).join(", ")}]`);
|
|
1008
|
+
defineMethod("object", ["dict"], ({ dict }) => {
|
|
1009
|
+
if (Object.keys(dict).length === 0) return "{}";
|
|
1010
|
+
return `{ ${Object.entries(dict).map(([key, inner]) => {
|
|
1011
|
+
return `${key}${inner.meta.required ? "" : "?"}: ${inner.toString()}`;
|
|
1012
|
+
}).join(", ")} }`;
|
|
1013
|
+
});
|
|
1014
|
+
defineMethod("union", ["list"], ({ list }, inline) => {
|
|
1015
|
+
const result = list.map(({ toString: format }) => format()).join(" | ");
|
|
1016
|
+
return inline ? `(${result})` : result;
|
|
1017
|
+
});
|
|
1018
|
+
defineMethod("intersect", ["list"], ({ list }) => {
|
|
1019
|
+
return `${list.map((inner) => inner.toString(true)).join(" & ")}`;
|
|
1020
|
+
});
|
|
1021
|
+
defineMethod("transform", [
|
|
1022
|
+
"inner",
|
|
1023
|
+
"callback",
|
|
1024
|
+
"preserve"
|
|
1025
|
+
], ({ inner }, isInner) => inner.toString(isInner));
|
|
1026
|
+
module.exports = Schema;
|
|
1027
|
+
})))(), 1);
|
|
1028
|
+
/** Catchable error for illegal/unknown selectors (config-warning path). */
|
|
1029
|
+
var SelectorError = class extends Error {
|
|
1030
|
+
constructor(message) {
|
|
1031
|
+
super(message);
|
|
1032
|
+
this.name = "SelectorError";
|
|
1033
|
+
}
|
|
1034
|
+
};
|
|
1035
|
+
/**
|
|
1036
|
+
* Parse a chain key or entry selector.
|
|
1037
|
+
*
|
|
1038
|
+
* Accepts `provider/model` and `provider/*`; throws {@link SelectorError}
|
|
1039
|
+
* on anything else (missing separator, empty parts, extra separators).
|
|
1040
|
+
*/
|
|
1041
|
+
function parseSelector(input) {
|
|
1042
|
+
if (typeof input !== "string") throw new SelectorError(`invalid selector ${String(input)}: expected "provider/model" or "provider/*"`);
|
|
1043
|
+
const trimmed = input.trim();
|
|
1044
|
+
const slash = trimmed.indexOf("/");
|
|
1045
|
+
if (slash <= 0 || slash === trimmed.length - 1) throw new SelectorError(`invalid selector "${input}": expected "provider/model" or "provider/*"`);
|
|
1046
|
+
const provider = trimmed.slice(0, slash).trim();
|
|
1047
|
+
const modelPart = trimmed.slice(slash + 1).trim();
|
|
1048
|
+
if (!provider || !modelPart) throw new SelectorError(`invalid selector "${input}": empty provider or model`);
|
|
1049
|
+
if (modelPart.includes("/")) throw new SelectorError(`invalid selector "${input}": unexpected extra separator`);
|
|
1050
|
+
return {
|
|
1051
|
+
provider,
|
|
1052
|
+
model: modelPart === "*" ? void 0 : modelPart,
|
|
1053
|
+
raw: trimmed
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
//#endregion
|
|
1057
|
+
//#region src/config.ts
|
|
1058
|
+
/**
|
|
1059
|
+
* The `fallbacks` settings namespace: plugin config schema + defaults.
|
|
1060
|
+
*
|
|
1061
|
+
* Two-block config model (plan fallbacks-role-config-model): block 1
|
|
1062
|
+
* `rootChain` — the root agent's single fallback chain (empty = no
|
|
1063
|
+
* degradation) — plus block 2 declared role entities: `roles.list`
|
|
1064
|
+
* (id/label/description/prompt?/permissions?/chain?/fallback) and
|
|
1065
|
+
* `roles.rules` enum references into the declared ids (or the built-in
|
|
1066
|
+
* `'inherit'` role). The legacy `chains` / `roles.default` keys are gone
|
|
1067
|
+
* from the schema and type (zero residual, migration table excepted); the
|
|
1068
|
+
* runtime consumes the new shape directly and flags surviving legacy keys
|
|
1069
|
+
* at startup via `detectLegacyKeys` (see `src/index.ts` apply()).
|
|
1070
|
+
*
|
|
1071
|
+
* Spec §4 is authoritative for field names and default values — notably
|
|
1072
|
+
* `triggerCodes` defaults to dsh's stable failure codes `['AUTH', 'QUOTA',
|
|
1073
|
+
* 'RATE_LIMIT']` (there is no `QUOTA_EXCEEDED` code in dsh), and an
|
|
1074
|
+
* unconfigured install (`enabled: false`, empty `rootChain`, empty roles)
|
|
1075
|
+
* is a no-op pass-through exactly like an uninstalled plugin (AC-8).
|
|
1076
|
+
*
|
|
1077
|
+
* This module is pure logic: it must not import any `@deepseek-ai/*` package
|
|
1078
|
+
* (types included) — `FallbacksConfig` is the plugin's own type. Task 3
|
|
1079
|
+
* registers this schema with `installSettingsSection` under the `fallbacks`
|
|
1080
|
+
* settings namespace.
|
|
1081
|
+
*
|
|
1082
|
+
* @module dsh-llm-fallbacks/config
|
|
1083
|
+
*/
|
|
1084
|
+
/**
|
|
1085
|
+
* Spec §4 defaults — `Config({})` must equal this (no-op install).
|
|
1086
|
+
* `enabled` defaults to `false` (readme-settings spec §1.2): the feature
|
|
1087
|
+
* switch is off until the user turns it on in the settings page; an
|
|
1088
|
+
* unconfigured install (`enabled: false`, empty rootChain, empty roles)
|
|
1089
|
+
* behaves exactly like an uninstalled plugin (AC-3 / no-op invariant).
|
|
1090
|
+
*/
|
|
1091
|
+
const defaultFallbacksConfig = {
|
|
1092
|
+
enabled: false,
|
|
1093
|
+
triggerCodes: [
|
|
1094
|
+
"AUTH",
|
|
1095
|
+
"QUOTA",
|
|
1096
|
+
"RATE_LIMIT"
|
|
1097
|
+
],
|
|
1098
|
+
rootChain: [],
|
|
1099
|
+
roles: {
|
|
1100
|
+
list: [],
|
|
1101
|
+
rules: []
|
|
1102
|
+
},
|
|
1103
|
+
cooldownMs: 3e5,
|
|
1104
|
+
revertPolicy: "cooldown-expiry",
|
|
1105
|
+
maxSwitchesPerStep: 8,
|
|
1106
|
+
alwaysModeRetryCap: 5
|
|
1107
|
+
};
|
|
1108
|
+
/**
|
|
1109
|
+
* Reserved role id: legal as a rule target (`roles.rules[].role`) and as
|
|
1110
|
+
* the no-rule-match fallback, but FORBIDDEN in `roles.list[].id`.
|
|
1111
|
+
*/
|
|
1112
|
+
const INHERIT_ROLE_ID = "inherit";
|
|
1113
|
+
/** Role id format (aligned with yet-another-subagent `isValidProfileId`). */
|
|
1114
|
+
const ROLE_ID_PATTERN = /^[a-z0-9-]{1,32}$/;
|
|
1115
|
+
import_lib.default.object({
|
|
1116
|
+
enabled: import_lib.default.boolean().default(false),
|
|
1117
|
+
triggerCodes: import_lib.default.array(import_lib.default.string()).default([
|
|
1118
|
+
"AUTH",
|
|
1119
|
+
"QUOTA",
|
|
1120
|
+
"RATE_LIMIT"
|
|
1121
|
+
]),
|
|
1122
|
+
rootChain: import_lib.default.array(import_lib.default.string()).default([]),
|
|
1123
|
+
roles: import_lib.default.object({
|
|
1124
|
+
list: import_lib.default.array(import_lib.default.object({
|
|
1125
|
+
id: import_lib.default.string().required(),
|
|
1126
|
+
label: import_lib.default.string().default(""),
|
|
1127
|
+
description: import_lib.default.string().default(""),
|
|
1128
|
+
prompt: import_lib.default.string(),
|
|
1129
|
+
permissions: import_lib.default.object({
|
|
1130
|
+
allow: import_lib.default.array(import_lib.default.string()),
|
|
1131
|
+
deny: import_lib.default.array(import_lib.default.string())
|
|
1132
|
+
}),
|
|
1133
|
+
chain: import_lib.default.array(import_lib.default.string()),
|
|
1134
|
+
fallback: import_lib.default.union([import_lib.default.const("inherit-root"), import_lib.default.const("none")]).default("inherit-root")
|
|
1135
|
+
})).default([]),
|
|
1136
|
+
rules: import_lib.default.array(import_lib.default.object({
|
|
1137
|
+
origin: import_lib.default.union([import_lib.default.const("root"), import_lib.default.const("subagent")]),
|
|
1138
|
+
provider: import_lib.default.string(),
|
|
1139
|
+
model: import_lib.default.string(),
|
|
1140
|
+
role: import_lib.default.string().required()
|
|
1141
|
+
})).default([])
|
|
1142
|
+
}).default({
|
|
1143
|
+
list: [],
|
|
1144
|
+
rules: []
|
|
1145
|
+
}),
|
|
1146
|
+
cooldownMs: import_lib.default.number().default(3e5),
|
|
1147
|
+
revertPolicy: import_lib.default.union([import_lib.default.const("cooldown-expiry"), import_lib.default.const("never")]).default("cooldown-expiry"),
|
|
1148
|
+
maxSwitchesPerStep: import_lib.default.number().default(8),
|
|
1149
|
+
alwaysModeRetryCap: import_lib.default.number().default(5)
|
|
1150
|
+
});
|
|
1151
|
+
//#endregion
|
|
1152
|
+
//#region src/client/fallbacks-store.ts
|
|
1153
|
+
/** The plugin's settings namespace on the host wire (settings/document-updated ns filter). */
|
|
1154
|
+
const FALLBACKS_SETTINGS_NS = "fallbacks";
|
|
1155
|
+
function messageOf(error) {
|
|
1156
|
+
return error instanceof Error ? error.message : String(error);
|
|
1157
|
+
}
|
|
1158
|
+
function isRecord(value) {
|
|
1159
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1160
|
+
}
|
|
1161
|
+
/**
|
|
1162
|
+
* Read a nested value by path — the `@deepseek-ai/dsh-client-schema-form`
|
|
1163
|
+
* `getPath` semantics, copied locally so the provider-configured join needs no
|
|
1164
|
+
* new dependency (array indexes as numeric keys, `undefined` along a missing
|
|
1165
|
+
* branch).
|
|
1166
|
+
*/
|
|
1167
|
+
function getPath(value, path) {
|
|
1168
|
+
let current = value;
|
|
1169
|
+
for (const key of path) {
|
|
1170
|
+
if (Array.isArray(current)) {
|
|
1171
|
+
current = current[Number(key)];
|
|
1172
|
+
continue;
|
|
1173
|
+
}
|
|
1174
|
+
if (typeof current !== "object" || current === null) return void 0;
|
|
1175
|
+
current = current[key];
|
|
1176
|
+
}
|
|
1177
|
+
return current;
|
|
1178
|
+
}
|
|
1179
|
+
/**
|
|
1180
|
+
* The provider dropdown's offer set (spec §2.5 D-4): catalog providers whose
|
|
1181
|
+
* settings profile resolves in the describe namespaces — the Models page's
|
|
1182
|
+
* `configured` predicate (`ui-models` store.ts): a provider is configured
|
|
1183
|
+
* when its settings namespace exists AND either it addresses the whole
|
|
1184
|
+
* section (`settingsPath` empty) or its profile path resolves in the resolved
|
|
1185
|
+
* value. Directory-only (unconfigured) providers never become options; the
|
|
1186
|
+
* section still renders existing values for them (read-back + annotation) so
|
|
1187
|
+
* nothing is lost on save.
|
|
1188
|
+
*/
|
|
1189
|
+
function configuredProvidersOf(providers, namespaces) {
|
|
1190
|
+
return providers.filter((entry) => {
|
|
1191
|
+
const namespace = namespaces.get(entry.settingsNs);
|
|
1192
|
+
return namespace !== void 0 && (entry.settingsPath.length === 0 || getPath(namespace.value, entry.settingsPath) !== void 0);
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
/**
|
|
1196
|
+
* Fold the redacted descriptor value into a complete {@link FallbacksConfig}:
|
|
1197
|
+
* missing optional fields take spec §4 defaults; gross type mismatches throw
|
|
1198
|
+
* so the UI can surface a broken descriptor instead of mis-rendering.
|
|
1199
|
+
*/
|
|
1200
|
+
function parseFallbacksConfig(value) {
|
|
1201
|
+
if (!isRecord(value)) throw new TypeError(`fallbacks descriptor value is not an object: ${String(value)}`);
|
|
1202
|
+
const triggerCodes = value.triggerCodes;
|
|
1203
|
+
if (triggerCodes !== void 0 && (!Array.isArray(triggerCodes) || triggerCodes.some((code) => typeof code !== "string"))) throw new TypeError("fallbacks descriptor triggerCodes must be a string array");
|
|
1204
|
+
const rootChain = value.rootChain;
|
|
1205
|
+
if (rootChain !== void 0 && (!Array.isArray(rootChain) || rootChain.some((entry) => typeof entry !== "string"))) throw new TypeError("fallbacks descriptor rootChain must be a string array");
|
|
1206
|
+
const roles = isRecord(value.roles) ? value.roles : {};
|
|
1207
|
+
const parsedList = (Array.isArray(roles.list) ? roles.list : []).map((role, index) => {
|
|
1208
|
+
if (!isRecord(role) || typeof role.id !== "string") throw new TypeError(`fallbacks descriptor roles.list[${String(index)}] must have a string id`);
|
|
1209
|
+
const label = role.label;
|
|
1210
|
+
if (label !== void 0 && typeof label !== "string") throw new TypeError(`fallbacks descriptor roles.list[${String(index)}].label must be a string`);
|
|
1211
|
+
const description = role.description;
|
|
1212
|
+
if (description !== void 0 && typeof description !== "string") throw new TypeError(`fallbacks descriptor roles.list[${String(index)}].description must be a string`);
|
|
1213
|
+
const prompt = role.prompt;
|
|
1214
|
+
if (prompt !== void 0 && typeof prompt !== "string") throw new TypeError(`fallbacks descriptor roles.list[${String(index)}].prompt must be a string`);
|
|
1215
|
+
const permissions = role.permissions;
|
|
1216
|
+
if (permissions !== void 0 && (!isRecord(permissions) || permissions.allow !== void 0 && (!Array.isArray(permissions.allow) || permissions.allow.some((item) => typeof item !== "string")) || permissions.deny !== void 0 && (!Array.isArray(permissions.deny) || permissions.deny.some((item) => typeof item !== "string")))) throw new TypeError(`fallbacks descriptor roles.list[${String(index)}].permissions must be an allow/deny string-array object`);
|
|
1217
|
+
const chain = role.chain;
|
|
1218
|
+
if (chain !== void 0 && (!Array.isArray(chain) || chain.some((entry) => typeof entry !== "string"))) throw new TypeError(`fallbacks descriptor roles.list[${String(index)}].chain must be a string array`);
|
|
1219
|
+
const fallback = role.fallback;
|
|
1220
|
+
if (fallback !== void 0 && fallback !== "inherit-root" && fallback !== "none") throw new TypeError(`fallbacks descriptor roles.list[${String(index)}].fallback must be inherit-root|none`);
|
|
1221
|
+
return {
|
|
1222
|
+
id: role.id,
|
|
1223
|
+
label: label ?? "",
|
|
1224
|
+
description: description ?? "",
|
|
1225
|
+
...prompt === void 0 ? {} : { prompt },
|
|
1226
|
+
...permissions === void 0 ? {} : { permissions },
|
|
1227
|
+
chain: chain ?? [],
|
|
1228
|
+
fallback: fallback ?? "inherit-root"
|
|
1229
|
+
};
|
|
1230
|
+
});
|
|
1231
|
+
const parsedRules = (Array.isArray(roles.rules) ? roles.rules : []).map((rule, index) => {
|
|
1232
|
+
if (!isRecord(rule) || typeof rule.role !== "string") throw new TypeError(`fallbacks descriptor roles.rules[${String(index)}] must have a string role`);
|
|
1233
|
+
const origin = rule.origin;
|
|
1234
|
+
if (origin !== void 0 && origin !== "root" && origin !== "subagent") throw new TypeError(`fallbacks descriptor roles.rules[${String(index)}].origin must be root|subagent`);
|
|
1235
|
+
const provider = rule.provider;
|
|
1236
|
+
const model = rule.model;
|
|
1237
|
+
if (provider !== void 0 && typeof provider !== "string") throw new TypeError(`fallbacks descriptor roles.rules[${String(index)}].provider must be a string`);
|
|
1238
|
+
if (model !== void 0 && typeof model !== "string") throw new TypeError(`fallbacks descriptor roles.rules[${String(index)}].model must be a string`);
|
|
1239
|
+
return {
|
|
1240
|
+
...origin === void 0 ? {} : { origin },
|
|
1241
|
+
...provider === void 0 ? {} : { provider },
|
|
1242
|
+
...model === void 0 ? {} : { model },
|
|
1243
|
+
role: rule.role
|
|
1244
|
+
};
|
|
1245
|
+
});
|
|
1246
|
+
const cooldownMs = value.cooldownMs;
|
|
1247
|
+
const maxSwitchesPerStep = value.maxSwitchesPerStep;
|
|
1248
|
+
const alwaysModeRetryCap = value.alwaysModeRetryCap;
|
|
1249
|
+
for (const [field, raw] of [
|
|
1250
|
+
["cooldownMs", cooldownMs],
|
|
1251
|
+
["maxSwitchesPerStep", maxSwitchesPerStep],
|
|
1252
|
+
["alwaysModeRetryCap", alwaysModeRetryCap]
|
|
1253
|
+
]) if (raw !== void 0 && typeof raw !== "number") throw new TypeError(`fallbacks descriptor ${field} must be a number`);
|
|
1254
|
+
const revertPolicy = value.revertPolicy;
|
|
1255
|
+
if (revertPolicy !== void 0 && revertPolicy !== "cooldown-expiry" && revertPolicy !== "never") throw new TypeError("fallbacks descriptor revertPolicy must be cooldown-expiry|never");
|
|
1256
|
+
const enabled = value.enabled;
|
|
1257
|
+
if (enabled !== void 0 && typeof enabled !== "boolean") throw new TypeError("fallbacks descriptor enabled must be a boolean");
|
|
1258
|
+
return {
|
|
1259
|
+
enabled: enabled ?? defaultFallbacksConfig.enabled,
|
|
1260
|
+
triggerCodes: triggerCodes ?? [...defaultFallbacksConfig.triggerCodes],
|
|
1261
|
+
rootChain: rootChain ?? [...defaultFallbacksConfig.rootChain],
|
|
1262
|
+
roles: {
|
|
1263
|
+
list: parsedList,
|
|
1264
|
+
rules: parsedRules
|
|
1265
|
+
},
|
|
1266
|
+
cooldownMs: cooldownMs ?? defaultFallbacksConfig.cooldownMs,
|
|
1267
|
+
revertPolicy: revertPolicy ?? defaultFallbacksConfig.revertPolicy,
|
|
1268
|
+
maxSwitchesPerStep: maxSwitchesPerStep ?? defaultFallbacksConfig.maxSwitchesPerStep,
|
|
1269
|
+
alwaysModeRetryCap: alwaysModeRetryCap ?? defaultFallbacksConfig.alwaysModeRetryCap
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1272
|
+
/** The raw selector string a selection serializes to ('' when empty). */
|
|
1273
|
+
function selectionToRaw(selection) {
|
|
1274
|
+
return selection === null ? "" : selection.kind === "catalog" ? selection.id : selection.raw;
|
|
1275
|
+
}
|
|
1276
|
+
/**
|
|
1277
|
+
* Classify a raw provider value against the catalog: a catalog route id is a
|
|
1278
|
+
* catalog selection, anything else is an outside value kept verbatim.
|
|
1279
|
+
*/
|
|
1280
|
+
function classifyProvider(raw, catalog) {
|
|
1281
|
+
if (raw === "") return null;
|
|
1282
|
+
if (catalog !== void 0 && catalog.providers.some((entry) => entry.provider === raw)) return {
|
|
1283
|
+
kind: "catalog",
|
|
1284
|
+
id: raw
|
|
1285
|
+
};
|
|
1286
|
+
return {
|
|
1287
|
+
kind: "outside",
|
|
1288
|
+
raw
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1291
|
+
/**
|
|
1292
|
+
* Classify a raw model value under its provider against the catalog: a model
|
|
1293
|
+
* id advertised by that provider is a catalog selection, anything else is an
|
|
1294
|
+
* outside value kept verbatim.
|
|
1295
|
+
*/
|
|
1296
|
+
function classifyModel(provider, raw, catalog) {
|
|
1297
|
+
if (raw === "") return null;
|
|
1298
|
+
if (catalog !== void 0 && catalog.groups.some((group) => group.id === provider && group.models.some((model) => model.id === raw))) return {
|
|
1299
|
+
kind: "catalog",
|
|
1300
|
+
id: raw
|
|
1301
|
+
};
|
|
1302
|
+
return {
|
|
1303
|
+
kind: "outside",
|
|
1304
|
+
raw
|
|
1305
|
+
};
|
|
1306
|
+
}
|
|
1307
|
+
/**
|
|
1308
|
+
* Extract the most recent `fallbacks/switch` events from one history page
|
|
1309
|
+
* (spec §2.5 D-5): filter by event type, order by `seq` descending, take at
|
|
1310
|
+
* most `limit`. Single-page read — fewer than `limit` events show as-is; no
|
|
1311
|
+
* multi-page backfill (Non-Goal).
|
|
1312
|
+
*/
|
|
1313
|
+
function extractRecentSwitches(entries, limit = 5) {
|
|
1314
|
+
const switches = [];
|
|
1315
|
+
for (const entry of entries) {
|
|
1316
|
+
const event = entry.event;
|
|
1317
|
+
if (event.type !== "fallbacks/switch") continue;
|
|
1318
|
+
switches.push({
|
|
1319
|
+
...event.data,
|
|
1320
|
+
seq: event.seq,
|
|
1321
|
+
time: event.time
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
1324
|
+
switches.sort((a, b) => b.seq - a.seq);
|
|
1325
|
+
return switches.slice(0, limit);
|
|
1326
|
+
}
|
|
1327
|
+
/** The config's primary target: the rootChain's first entry (D-6 ③). */
|
|
1328
|
+
function configPrimaryTarget(config) {
|
|
1329
|
+
const firstEntry = config.rootChain[0];
|
|
1330
|
+
if (firstEntry === void 0) return null;
|
|
1331
|
+
try {
|
|
1332
|
+
const selector = parseSelector(firstEntry);
|
|
1333
|
+
return {
|
|
1334
|
+
provider: selector.provider,
|
|
1335
|
+
model: selector.model ?? "*"
|
|
1336
|
+
};
|
|
1337
|
+
} catch {
|
|
1338
|
+
return {
|
|
1339
|
+
provider: firstEntry,
|
|
1340
|
+
model: "*"
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
/**
|
|
1345
|
+
* Derive the status block's "current effective model" (spec §2.5 D-6): ①
|
|
1346
|
+
* disabled / empty rootChain → unavailable; ② a recent switch exists → the
|
|
1347
|
+
* latest one's `to`; ③ otherwise → the config's primary target. A **display
|
|
1348
|
+
* value** — never a live route probe (the section appends the non-probing
|
|
1349
|
+
* note inline right after the derived value, available case only; the
|
|
1350
|
+
* unavailable 空态 renders its own copy without the note).
|
|
1351
|
+
*/
|
|
1352
|
+
function deriveEffectiveModel(config, switches) {
|
|
1353
|
+
if (!config.enabled || config.rootChain.length === 0) return { kind: "unavailable" };
|
|
1354
|
+
const latest = switches[0];
|
|
1355
|
+
if (latest !== void 0) return {
|
|
1356
|
+
kind: "switched",
|
|
1357
|
+
provider: latest.to.provider,
|
|
1358
|
+
model: latest.to.model
|
|
1359
|
+
};
|
|
1360
|
+
const target = configPrimaryTarget(config);
|
|
1361
|
+
if (target === null) return { kind: "unavailable" };
|
|
1362
|
+
return {
|
|
1363
|
+
kind: "config",
|
|
1364
|
+
...target
|
|
1365
|
+
};
|
|
1366
|
+
}
|
|
1367
|
+
/** Serialize one selector row to its wire string (`provider/model` | `provider/*`). */
|
|
1368
|
+
function selectorRowToRaw(row) {
|
|
1369
|
+
const provider = selectionToRaw(row.provider);
|
|
1370
|
+
if (provider === "") return "";
|
|
1371
|
+
if (row.wildcard) return `${provider}/*`;
|
|
1372
|
+
const model = selectionToRaw(row.model);
|
|
1373
|
+
return model === "" ? provider : `${provider}/${model}`;
|
|
1374
|
+
}
|
|
1375
|
+
/** Parse one entry line into a selector row, classifying against the catalog. */
|
|
1376
|
+
function entryToSelectorRow(entry, catalog) {
|
|
1377
|
+
try {
|
|
1378
|
+
const selector = parseSelector(entry);
|
|
1379
|
+
return {
|
|
1380
|
+
wildcard: selector.model === void 0,
|
|
1381
|
+
provider: classifyProvider(selector.provider, catalog),
|
|
1382
|
+
model: selector.model === void 0 ? null : classifyModel(selector.provider, selector.model, catalog)
|
|
1383
|
+
};
|
|
1384
|
+
} catch {
|
|
1385
|
+
return {
|
|
1386
|
+
wildcard: false,
|
|
1387
|
+
provider: {
|
|
1388
|
+
kind: "outside",
|
|
1389
|
+
raw: entry.trim()
|
|
1390
|
+
},
|
|
1391
|
+
model: null
|
|
1392
|
+
};
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
/** Project the rootChain entries into editable rows (one flat chain row). */
|
|
1396
|
+
function rootChainToRows(rootChain, catalog) {
|
|
1397
|
+
return [{ selectors: rootChain.map((entry) => entryToSelectorRow(entry, catalog)) }];
|
|
1398
|
+
}
|
|
1399
|
+
/** Rebuild the rootChain from edited rows; rows with no usable selector drop out. */
|
|
1400
|
+
function rowsToRootChain(rows) {
|
|
1401
|
+
const entries = [];
|
|
1402
|
+
for (const row of rows) {
|
|
1403
|
+
if (row.selectors.length === 0) continue;
|
|
1404
|
+
for (const selector of row.selectors) {
|
|
1405
|
+
const raw = selectorRowToRaw(selector);
|
|
1406
|
+
if (raw !== "") entries.push(raw);
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
return entries;
|
|
1410
|
+
}
|
|
1411
|
+
/** Project the declared roles into editable rows (chain selectors classified). */
|
|
1412
|
+
function rolesToRows(roles, catalog) {
|
|
1413
|
+
return roles.map((role) => ({
|
|
1414
|
+
id: role.id,
|
|
1415
|
+
label: role.label,
|
|
1416
|
+
description: role.description,
|
|
1417
|
+
selectors: (role.chain ?? []).map((entry) => entryToSelectorRow(entry, catalog)),
|
|
1418
|
+
fallback: role.fallback ?? "inherit-root"
|
|
1419
|
+
}));
|
|
1420
|
+
}
|
|
1421
|
+
/** Rebuild the declared roles from edited rows; empty selectors drop out. */
|
|
1422
|
+
function rowsToRoles(rows) {
|
|
1423
|
+
return rows.map((row) => ({
|
|
1424
|
+
id: row.id.trim(),
|
|
1425
|
+
label: row.label,
|
|
1426
|
+
description: row.description,
|
|
1427
|
+
chain: row.selectors.map(selectorRowToRaw).filter((entry) => entry !== ""),
|
|
1428
|
+
fallback: row.fallback
|
|
1429
|
+
}));
|
|
1430
|
+
}
|
|
1431
|
+
/**
|
|
1432
|
+
* Rebuild the declared roles from edited rows, re-attaching the
|
|
1433
|
+
* schema-reserved `prompt`/`permissions` fields from the last accepted
|
|
1434
|
+
* config by role id — they never round-trip through rows this round, so
|
|
1435
|
+
* without the merge a save would silently drop them (T2 reviewer minor
|
|
1436
|
+
* #2). The id trim matches {@link rowsToRoles}; a row whose id matches no
|
|
1437
|
+
* original role (a freshly added one) keeps no extras. Key order mirrors
|
|
1438
|
+
* `parseFallbacksConfig` so a clean draft's JSON dirty comparison never
|
|
1439
|
+
* flags it.
|
|
1440
|
+
*/
|
|
1441
|
+
function mergeRoleExtras(rows, originalRoles) {
|
|
1442
|
+
const originalById = new Map(originalRoles.map((role) => [role.id, role]));
|
|
1443
|
+
return rowsToRoles(rows).map((role) => {
|
|
1444
|
+
const original = originalById.get(role.id);
|
|
1445
|
+
if (original === void 0) return role;
|
|
1446
|
+
return {
|
|
1447
|
+
id: role.id,
|
|
1448
|
+
label: role.label,
|
|
1449
|
+
description: role.description,
|
|
1450
|
+
...original.prompt === void 0 ? {} : { prompt: original.prompt },
|
|
1451
|
+
...original.permissions === void 0 ? {} : { permissions: original.permissions },
|
|
1452
|
+
chain: role.chain,
|
|
1453
|
+
fallback: role.fallback
|
|
1454
|
+
};
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
1457
|
+
/**
|
|
1458
|
+
* The `roles.rules` role dropdown's offer set — the ONLY data source for the
|
|
1459
|
+
* rule rows' role selector: the built-in `'inherit'` target plus every
|
|
1460
|
+
* declared `roles.list` id, in declaration order (a role added/removed on
|
|
1461
|
+
* the same page is reflected immediately).
|
|
1462
|
+
*/
|
|
1463
|
+
function ruleRoleOptions(roles) {
|
|
1464
|
+
return [INHERIT_ROLE_ID, ...new Set(roles.list.map((role) => role.id.trim()))];
|
|
1465
|
+
}
|
|
1466
|
+
/** Project the role rules into editable rows (provider/model classified). */
|
|
1467
|
+
function rulesToRows(rules, catalog) {
|
|
1468
|
+
return rules.map((rule) => ({
|
|
1469
|
+
origin: rule.origin ?? "",
|
|
1470
|
+
provider: classifyProvider(rule.provider ?? "", catalog),
|
|
1471
|
+
model: classifyModel(rule.provider ?? "", rule.model ?? "", catalog),
|
|
1472
|
+
role: rule.role
|
|
1473
|
+
}));
|
|
1474
|
+
}
|
|
1475
|
+
/** Rebuild the role rules from edited rows; empty origin/provider/model drop out. */
|
|
1476
|
+
function rowsToRules(rows) {
|
|
1477
|
+
return rows.map((row) => ({
|
|
1478
|
+
...row.origin === "" ? {} : { origin: row.origin },
|
|
1479
|
+
...row.provider === null ? {} : { provider: selectionToRaw(row.provider) },
|
|
1480
|
+
...row.model === null ? {} : { model: selectionToRaw(row.model) },
|
|
1481
|
+
role: row.role.trim()
|
|
1482
|
+
})).filter((rule) => rule.role !== "");
|
|
1483
|
+
}
|
|
1484
|
+
/** Controller joining Settings reads, writes, and pushed invalidations. */
|
|
1485
|
+
var FallbacksSettingsController = class {
|
|
1486
|
+
api;
|
|
1487
|
+
rpc;
|
|
1488
|
+
/** Snapshot consumed by the section through `useSyncExternalStore`. */
|
|
1489
|
+
store = (0, _deepseek_ai_dsh_client_runtime_client.createSnapshotStore)({
|
|
1490
|
+
status: "idle",
|
|
1491
|
+
error: null,
|
|
1492
|
+
writable: false,
|
|
1493
|
+
config: defaultFallbacksConfig,
|
|
1494
|
+
present: false,
|
|
1495
|
+
legacyKeys: [],
|
|
1496
|
+
catalogStatus: "idle",
|
|
1497
|
+
catalogError: null,
|
|
1498
|
+
providers: [],
|
|
1499
|
+
configuredProviders: [],
|
|
1500
|
+
groups: [],
|
|
1501
|
+
catalogEpoch: 0,
|
|
1502
|
+
switchesStatus: "idle",
|
|
1503
|
+
switchesError: null,
|
|
1504
|
+
switches: []
|
|
1505
|
+
});
|
|
1506
|
+
generation = 0;
|
|
1507
|
+
catalogGeneration = 0;
|
|
1508
|
+
switchesGeneration = 0;
|
|
1509
|
+
/** Every settings namespace from the last describe, keyed by ns — the configured-provider join's other input. */
|
|
1510
|
+
namespaces = /* @__PURE__ */ new Map();
|
|
1511
|
+
currentSession;
|
|
1512
|
+
/**
|
|
1513
|
+
* @param api - Settings / Llm / Sessions wire faces (describe `writable` +
|
|
1514
|
+
* namespace directory, provider/model catalog, session history).
|
|
1515
|
+
* @param rpc - the connection's generic RPC caller for the host gateway
|
|
1516
|
+
* channel (`/api`), injected from the connection handle.
|
|
1517
|
+
*/
|
|
1518
|
+
constructor(api, rpc) {
|
|
1519
|
+
this.api = api;
|
|
1520
|
+
this.rpc = rpc;
|
|
1521
|
+
}
|
|
1522
|
+
/**
|
|
1523
|
+
* Refresh the page snapshot. Latest request wins. `settings.describe`
|
|
1524
|
+
* still runs — it supplies the top-level `writable` flag (host read-only
|
|
1525
|
+
* mode) and the namespace directory (the configured-provider join's other
|
|
1526
|
+
* input) — but the fallbacks config itself rides the gateway channel:
|
|
1527
|
+
* `rpc.call('/api', 'fallbacks/get', { args: {} })`. The two reads are
|
|
1528
|
+
* independent and run in PARALLEL (Promise.all — one round trip per
|
|
1529
|
+
* refresh, not two). The `fallbacks` namespace is NOT expected in describe
|
|
1530
|
+
* anymore (it is off the apiproxy boundary post-patch); a describe failure
|
|
1531
|
+
* remains a hard `error` (the form cannot render provider/model options
|
|
1532
|
+
* without the directory), while a get failure is NOT a page error —
|
|
1533
|
+
* `present` goes false and the section keeps the usable skeleton (KD-G5).
|
|
1534
|
+
* @returns nothing; {@link store} carries success or failure.
|
|
1535
|
+
*/
|
|
1536
|
+
async load() {
|
|
1537
|
+
const generation = ++this.generation;
|
|
1538
|
+
this.store.update((state) => {
|
|
1539
|
+
state.status = "loading";
|
|
1540
|
+
state.error = null;
|
|
1541
|
+
});
|
|
1542
|
+
try {
|
|
1543
|
+
const [describeResult, getResult] = await Promise.all([this.api.settings.describe({}), this.rpc.call("/api", "fallbacks/get", { args: {} }).catch(() => void 0)]);
|
|
1544
|
+
if (generation !== this.generation) return;
|
|
1545
|
+
if (!describeResult.result.ok) throw describeResult.result.error;
|
|
1546
|
+
this.namespaces = new Map(describeResult.result.value.namespaces.map((entry) => [entry.ns, entry]));
|
|
1547
|
+
const writable = describeResult.result.value.writable;
|
|
1548
|
+
let config;
|
|
1549
|
+
let legacyKeys = [];
|
|
1550
|
+
if (getResult !== void 0 && getResult.ok && getResult.value !== null && typeof getResult.value === "object") {
|
|
1551
|
+
if ("config" in getResult.value) config = getResult.value.config;
|
|
1552
|
+
if ("legacyKeys" in getResult.value) {
|
|
1553
|
+
const wireLegacyKeys = getResult.value.legacyKeys;
|
|
1554
|
+
if (Array.isArray(wireLegacyKeys)) legacyKeys = wireLegacyKeys.filter((key) => typeof key === "string");
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
this.accept(config, writable, legacyKeys);
|
|
1558
|
+
} catch (error) {
|
|
1559
|
+
if (generation !== this.generation) return;
|
|
1560
|
+
this.fail(error);
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
/**
|
|
1564
|
+
* Refresh the provider/model catalog (`llm.providers` + `llm.models`), an
|
|
1565
|
+
* independent read path with its own generation guard so it can run
|
|
1566
|
+
* parallel to {@link load} without clobbering it (spec §2.5 D-4).
|
|
1567
|
+
* Per-provider lookup failures ride `catalogError` as a diagnostic without
|
|
1568
|
+
* failing the sound groups; a whole-load failure lands `catalogStatus:
|
|
1569
|
+
* 'error'` and never blocks the rest of the form.
|
|
1570
|
+
* @returns nothing; {@link store} carries success or failure.
|
|
1571
|
+
*/
|
|
1572
|
+
async loadCatalog() {
|
|
1573
|
+
const generation = ++this.catalogGeneration;
|
|
1574
|
+
this.store.update((state) => {
|
|
1575
|
+
state.catalogStatus = "loading";
|
|
1576
|
+
state.catalogError = null;
|
|
1577
|
+
});
|
|
1578
|
+
try {
|
|
1579
|
+
const [providersResponse, modelsResponse] = await Promise.all([this.api.llm.providers({}), this.api.llm.models({})]);
|
|
1580
|
+
if (generation !== this.catalogGeneration) return;
|
|
1581
|
+
if (!providersResponse.result.ok) throw providersResponse.result.error;
|
|
1582
|
+
if (!modelsResponse.result.ok) throw modelsResponse.result.error;
|
|
1583
|
+
const providers = providersResponse.result.value.providers;
|
|
1584
|
+
const groups = modelsResponse.result.value.groups;
|
|
1585
|
+
const failures = modelsResponse.result.value.failures;
|
|
1586
|
+
this.store.update((state) => {
|
|
1587
|
+
state.catalogStatus = "ready";
|
|
1588
|
+
state.catalogError = failures.length > 0 ? failures.map((failure) => `${failure.name}: ${failure.message}`).join("; ") : null;
|
|
1589
|
+
state.providers = providers;
|
|
1590
|
+
state.configuredProviders = configuredProvidersOf(providers, this.namespaces);
|
|
1591
|
+
state.groups = groups;
|
|
1592
|
+
state.catalogEpoch += 1;
|
|
1593
|
+
});
|
|
1594
|
+
} catch (error) {
|
|
1595
|
+
if (generation !== this.catalogGeneration) return;
|
|
1596
|
+
const wire = error;
|
|
1597
|
+
this.store.update((state) => {
|
|
1598
|
+
state.catalogStatus = "error";
|
|
1599
|
+
state.catalogError = typeof wire?.message === "string" ? wire.message : messageOf(error);
|
|
1600
|
+
});
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
/**
|
|
1604
|
+
* Record the current session the status block reads (spec §2.5 D-5). Once
|
|
1605
|
+
* the block has been read once, its summary follows session switches
|
|
1606
|
+
* immediately; an idle block only records the id — the section's mount
|
|
1607
|
+
* effect performs the first read.
|
|
1608
|
+
* @param sessionId - the session whose history is summarized; undefined
|
|
1609
|
+
* (no current session) resolves to the empty state.
|
|
1610
|
+
*/
|
|
1611
|
+
setCurrentSession(sessionId) {
|
|
1612
|
+
if (sessionId === this.currentSession) return;
|
|
1613
|
+
this.currentSession = sessionId;
|
|
1614
|
+
if (this.store.getSnapshot().switchesStatus !== "idle") this.loadSwitches();
|
|
1615
|
+
}
|
|
1616
|
+
/**
|
|
1617
|
+
* Read the recent-switch summary for the current session (spec §2.5 D-5):
|
|
1618
|
+
* one `sessions.history` page (`maxMessages` = {@link SWITCHES_HISTORY_PAGE}),
|
|
1619
|
+
* `fallbacks/switch` events extracted newest-first capped at
|
|
1620
|
+
* {@link RECENT_SWITCH_LIMIT}. No current session → honest empty ready
|
|
1621
|
+
* state (no RPC); a read failure lands `switchesStatus: 'error'` and never
|
|
1622
|
+
* touches the settings state (the form keeps editing/saving normally).
|
|
1623
|
+
* @returns nothing; {@link store} carries success or failure.
|
|
1624
|
+
*/
|
|
1625
|
+
async loadSwitches() {
|
|
1626
|
+
const generation = ++this.switchesGeneration;
|
|
1627
|
+
const sessionId = this.currentSession;
|
|
1628
|
+
if (sessionId === void 0) {
|
|
1629
|
+
this.store.update((state) => {
|
|
1630
|
+
state.switchesStatus = "ready";
|
|
1631
|
+
state.switchesError = null;
|
|
1632
|
+
state.switches = [];
|
|
1633
|
+
});
|
|
1634
|
+
return;
|
|
1635
|
+
}
|
|
1636
|
+
this.store.update((state) => {
|
|
1637
|
+
state.switchesStatus = "loading";
|
|
1638
|
+
state.switchesError = null;
|
|
1639
|
+
});
|
|
1640
|
+
try {
|
|
1641
|
+
const response = await this.api.sessions.history({
|
|
1642
|
+
sessionId,
|
|
1643
|
+
maxMessages: 50
|
|
1644
|
+
});
|
|
1645
|
+
if (generation !== this.switchesGeneration) return;
|
|
1646
|
+
if (!response.result.ok) throw response.result.error;
|
|
1647
|
+
const switches = extractRecentSwitches(response.result.value.events);
|
|
1648
|
+
this.store.update((state) => {
|
|
1649
|
+
state.switchesStatus = "ready";
|
|
1650
|
+
state.switchesError = null;
|
|
1651
|
+
state.switches = switches;
|
|
1652
|
+
});
|
|
1653
|
+
} catch (error) {
|
|
1654
|
+
if (generation !== this.switchesGeneration) return;
|
|
1655
|
+
const wire = error;
|
|
1656
|
+
this.store.update((state) => {
|
|
1657
|
+
state.switchesStatus = "error";
|
|
1658
|
+
state.switchesError = typeof wire?.message === "string" ? wire.message : messageOf(error);
|
|
1659
|
+
});
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
/**
|
|
1663
|
+
* Persist the full edited configuration through the gateway channel
|
|
1664
|
+
* (`/api/fallbacks/set`). The full config is sent as a MERGE patch (guide
|
|
1665
|
+
* §9) — keys the new schema cannot express (legacy `chains` /
|
|
1666
|
+
* `roles.default` in the user layer) survive the write, which is why the
|
|
1667
|
+
* gateway returns POST-WRITE `legacyKeys` and the banner stays honest
|
|
1668
|
+
* (W-1/F-1). The merge has no revision guard: any failure (business
|
|
1669
|
+
* rejection or transport) surfaces its message in `state.error` for the
|
|
1670
|
+
* section's error banner and the form stays editable for retry (KD-G3).
|
|
1671
|
+
* @param next - the complete edited configuration.
|
|
1672
|
+
*/
|
|
1673
|
+
async save(next) {
|
|
1674
|
+
const state = this.store.getSnapshot();
|
|
1675
|
+
if (!state.writable || state.status === "saving") return;
|
|
1676
|
+
const generation = ++this.generation;
|
|
1677
|
+
this.store.update((draft) => {
|
|
1678
|
+
draft.status = "saving";
|
|
1679
|
+
draft.error = null;
|
|
1680
|
+
});
|
|
1681
|
+
try {
|
|
1682
|
+
const result = await this.rpc.call("/api", "fallbacks/set", { args: { patch: next } });
|
|
1683
|
+
if (generation !== this.generation) return;
|
|
1684
|
+
if (!result.ok) throw result.error;
|
|
1685
|
+
const value = result.value;
|
|
1686
|
+
const config = value !== null && typeof value === "object" && "config" in value ? value.config : void 0;
|
|
1687
|
+
let legacyKeys = this.store.getSnapshot().legacyKeys;
|
|
1688
|
+
if (value !== null && typeof value === "object" && "legacyKeys" in value) {
|
|
1689
|
+
const wireLegacyKeys = value.legacyKeys;
|
|
1690
|
+
if (Array.isArray(wireLegacyKeys)) legacyKeys = wireLegacyKeys.filter((key) => typeof key === "string");
|
|
1691
|
+
}
|
|
1692
|
+
this.accept(config, true, legacyKeys);
|
|
1693
|
+
} catch (error) {
|
|
1694
|
+
if (generation !== this.generation) return;
|
|
1695
|
+
this.fail(error);
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
/**
|
|
1699
|
+
* Reset to composition defaults through the gateway channel
|
|
1700
|
+
* (`/api/fallbacks/reset` — the fallbacks-specific third method; the host
|
|
1701
|
+
* clears the user layer via `settings.replace(ns, {})`, the removal path a
|
|
1702
|
+
* merge cannot express). Same error handling as {@link save} (KD-G3).
|
|
1703
|
+
*/
|
|
1704
|
+
async resetToDefaults() {
|
|
1705
|
+
const state = this.store.getSnapshot();
|
|
1706
|
+
if (!state.writable || state.status === "saving") return;
|
|
1707
|
+
const generation = ++this.generation;
|
|
1708
|
+
this.store.update((draft) => {
|
|
1709
|
+
draft.status = "saving";
|
|
1710
|
+
draft.error = null;
|
|
1711
|
+
});
|
|
1712
|
+
try {
|
|
1713
|
+
const result = await this.rpc.call("/api", "fallbacks/reset", { args: {} });
|
|
1714
|
+
if (generation !== this.generation) return;
|
|
1715
|
+
if (!result.ok) throw result.error;
|
|
1716
|
+
const value = result.value;
|
|
1717
|
+
const config = value !== null && typeof value === "object" && "config" in value ? value.config : void 0;
|
|
1718
|
+
let legacyKeys = this.store.getSnapshot().legacyKeys;
|
|
1719
|
+
if (value !== null && typeof value === "object" && "legacyKeys" in value) {
|
|
1720
|
+
const wireLegacyKeys = value.legacyKeys;
|
|
1721
|
+
if (Array.isArray(wireLegacyKeys)) legacyKeys = wireLegacyKeys.filter((key) => typeof key === "string");
|
|
1722
|
+
}
|
|
1723
|
+
this.accept(config, true, legacyKeys);
|
|
1724
|
+
} catch (error) {
|
|
1725
|
+
if (generation !== this.generation) return;
|
|
1726
|
+
this.fail(error);
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
/** Stop in-flight responses from publishing after plugin disposal. */
|
|
1730
|
+
dispose() {
|
|
1731
|
+
this.generation += 1;
|
|
1732
|
+
this.catalogGeneration += 1;
|
|
1733
|
+
this.switchesGeneration += 1;
|
|
1734
|
+
this.namespaces = /* @__PURE__ */ new Map();
|
|
1735
|
+
}
|
|
1736
|
+
/**
|
|
1737
|
+
* Publish a settled load: `status` ready, `writable` from describe, and —
|
|
1738
|
+
* only when the gateway returned a REAL config — `present` true and
|
|
1739
|
+
* `state.config` replaced with the parsed value. A get that did not
|
|
1740
|
+
* resolve (`config === undefined`) lands `present` false and keeps the
|
|
1741
|
+
* last accepted config (the defaults skeleton on a first load) — the
|
|
1742
|
+
* draft seed invariant (I-1): a transient channel-down must never seed
|
|
1743
|
+
* the form with defaults over real server truth. `legacyKeys` rides the
|
|
1744
|
+
* same publish: the wire field drives the migration banner. save/reset
|
|
1745
|
+
* pass the POST-WRITE value (W-1/F-1) — or the previous value when the
|
|
1746
|
+
* response omits the field, so a write can never clear the banner
|
|
1747
|
+
* against server truth; only a real `get` may.
|
|
1748
|
+
*/
|
|
1749
|
+
accept(config, writable, legacyKeys) {
|
|
1750
|
+
const parsed = config === void 0 ? void 0 : parseFallbacksConfig(config);
|
|
1751
|
+
this.store.update((state) => {
|
|
1752
|
+
state.status = "ready";
|
|
1753
|
+
state.error = null;
|
|
1754
|
+
state.writable = writable;
|
|
1755
|
+
state.present = parsed !== void 0;
|
|
1756
|
+
state.legacyKeys = parsed === void 0 ? state.legacyKeys : legacyKeys;
|
|
1757
|
+
if (parsed !== void 0) state.config = parsed;
|
|
1758
|
+
state.configuredProviders = configuredProvidersOf(state.providers, this.namespaces);
|
|
1759
|
+
});
|
|
1760
|
+
}
|
|
1761
|
+
fail(error) {
|
|
1762
|
+
const wire = error;
|
|
1763
|
+
this.store.update((state) => {
|
|
1764
|
+
state.status = "error";
|
|
1765
|
+
state.error = typeof wire?.message === "string" ? wire.message : messageOf(error);
|
|
1766
|
+
});
|
|
1767
|
+
}
|
|
1768
|
+
};
|
|
1769
|
+
/**
|
|
1770
|
+
* Refetch after reconnect / settings change only when the section has already
|
|
1771
|
+
* opened once.
|
|
1772
|
+
* @param controller - the fallbacks settings controller.
|
|
1773
|
+
*/
|
|
1774
|
+
function refreshFallbacksIfLoaded(controller) {
|
|
1775
|
+
if (controller.store.getSnapshot().status === "idle") return;
|
|
1776
|
+
controller.load();
|
|
1777
|
+
}
|
|
1778
|
+
/**
|
|
1779
|
+
* Refetch the catalog after `llm/adapters-updated` only when it has already
|
|
1780
|
+
* been opened once (the catalog twin of {@link refreshFallbacksIfLoaded}).
|
|
1781
|
+
* @param controller - the fallbacks settings controller.
|
|
1782
|
+
*/
|
|
1783
|
+
function refreshCatalogIfLoaded(controller) {
|
|
1784
|
+
if (controller.store.getSnapshot().catalogStatus === "idle") return;
|
|
1785
|
+
controller.loadCatalog();
|
|
1786
|
+
}
|
|
1787
|
+
/**
|
|
1788
|
+
* Refetch the recent-switch summary after `settings/document-updated`
|
|
1789
|
+
* (fallbacks ns) / `connection/reset` only when the status block has already
|
|
1790
|
+
* been read once
|
|
1791
|
+
* (the switches twin of {@link refreshFallbacksIfLoaded}).
|
|
1792
|
+
* @param controller - the fallbacks settings controller.
|
|
1793
|
+
*/
|
|
1794
|
+
function refreshSwitchesIfLoaded(controller) {
|
|
1795
|
+
if (controller.store.getSnapshot().switchesStatus === "idle") return;
|
|
1796
|
+
controller.loadSwitches();
|
|
1797
|
+
}
|
|
1798
|
+
//#endregion
|
|
1799
|
+
//#region src/client/locales.ts
|
|
1800
|
+
/**
|
|
1801
|
+
* Fallbacks settings section dictionaries (zh source of truth) plus the
|
|
1802
|
+
* `fallbacks` LocaleNamespaceMap merge — the registration's `locale:` seat
|
|
1803
|
+
* (`PropsLocale<'fallbacks'>` puts the typed `t` on the section props).
|
|
1804
|
+
*
|
|
1805
|
+
* Label conventions follow spec §4 用户直观性: enumerable config values
|
|
1806
|
+
* (triggerCodes / revertPolicy) render readable labels, never raw enum
|
|
1807
|
+
* strings.
|
|
1808
|
+
*/
|
|
1809
|
+
/** Simplified Chinese dictionary (the key-set source of truth). */
|
|
1810
|
+
const zh = {
|
|
1811
|
+
"title": "Fallbacks",
|
|
1812
|
+
"intro": "模型故障自动降级",
|
|
1813
|
+
"collapse": "收起设置",
|
|
1814
|
+
"expand": "展开设置",
|
|
1815
|
+
"unsaved": "未保存",
|
|
1816
|
+
"discard": "放弃修改",
|
|
1817
|
+
"retry": "重试",
|
|
1818
|
+
"readOnly": "当前环境中的设置为只读。",
|
|
1819
|
+
"enabled.label": "启用故障降级",
|
|
1820
|
+
"enabled.hint": "关闭后插件完全不介入",
|
|
1821
|
+
"enabled.tooltip": "关闭后插件完全不介入;开启但未配置 rootChain 时行为与未安装插件一致。",
|
|
1822
|
+
"enabled.off": "功能未开启:打开 enabled 开关以显示配置界面。",
|
|
1823
|
+
"triggerCodes.label": "触发失败码",
|
|
1824
|
+
"triggerCodes.hint": "命中这些失败码时进入降级决策",
|
|
1825
|
+
"triggerCodes.tooltip": "命中这些失败码时进入降级链决策;可重试型故障(如 5xx)由 llm-retry 先行退避,预算耗尽后同样进入决策。",
|
|
1826
|
+
"triggerCodes.RATE_LIMIT": "限流(429)",
|
|
1827
|
+
"triggerCodes.QUOTA": "配额超限",
|
|
1828
|
+
"triggerCodes.AUTH": "权限/认证失败",
|
|
1829
|
+
"triggerCodes.extra": "此外还保留了 {codes} 等自定义失败码。",
|
|
1830
|
+
"revertPolicy.label": "冷却结束后",
|
|
1831
|
+
"revertPolicy.cooldown-expiry": "冷却到期后回主模型",
|
|
1832
|
+
"revertPolicy.never": "保持备用模型(会话内不回)",
|
|
1833
|
+
"revertPolicy.hint": "冷却到期后是否回主模型",
|
|
1834
|
+
"revertPolicy.tooltip": "被切换离的模型在冷却期内不再入选;到期后按此策略决定是否回主。",
|
|
1835
|
+
"cooldownMs.label": "冷却时长(毫秒)",
|
|
1836
|
+
"cooldownMs.hint": "冷却期内模型不再入选",
|
|
1837
|
+
"cooldownMs.tooltip": "被切离/失败的模型在冷却期内不再入选。",
|
|
1838
|
+
"maxSwitchesPerStep.label": "单步最大切换次数",
|
|
1839
|
+
"maxSwitchesPerStep.hint": "超过后停止切换",
|
|
1840
|
+
"maxSwitchesPerStep.tooltip": "超过后停止切换,以原始错误语义结束当前步,防止链循环放大延迟。",
|
|
1841
|
+
"alwaysModeRetryCap.label": "always 模式重试上限",
|
|
1842
|
+
"alwaysModeRetryCap.hint": "达到上限次数后切换;0 表示禁用",
|
|
1843
|
+
"alwaysModeRetryCap.tooltip": "retryPolicy 为 always 的模型在同一请求内重试达到该次数后切换;0 表示禁用。",
|
|
1844
|
+
"rootChain.label": "root 主代理降级链",
|
|
1845
|
+
"rootChain.hint": "未配置 = root 不降级",
|
|
1846
|
+
"rootChain.tooltip": "root 主代理失败时按此有序选择器列表依次降级;未配置时行为与未安装插件一致。",
|
|
1847
|
+
"rootChain.selector.add": "添加选择器",
|
|
1848
|
+
"chains.selector.remove": "删除该选择器",
|
|
1849
|
+
"chains.selector.providerPlaceholder": "选择 provider",
|
|
1850
|
+
"chains.selector.modelPlaceholder": "选择 model",
|
|
1851
|
+
"chains.selector.wildcard": "通配该 provider(provider/*)",
|
|
1852
|
+
"chains.selector.noModels": "该 provider 暂无可用模型(目录查询失败),请使用通配或改选。",
|
|
1853
|
+
"roles.list.label": "角色实体",
|
|
1854
|
+
"roles.list.hint": "先声明角色,规则才能引用",
|
|
1855
|
+
"roles.list.tooltip": "角色 id 须匹配 /^[a-z0-9-]{1,32}$/ 且唯一;\"inherit\" 为保留字,不能用作角色 id。",
|
|
1856
|
+
"roles.id": "id",
|
|
1857
|
+
"roles.id.hint": "小写字母/数字/连字符,1–32 字符",
|
|
1858
|
+
"roles.idPlaceholder": "例如 reviewer",
|
|
1859
|
+
"roles.label": "名称",
|
|
1860
|
+
"roles.description": "描述",
|
|
1861
|
+
"roles.fallback": "链拼接策略",
|
|
1862
|
+
"roles.fallback.inherit-root": "继承 root(角色链后追加 rootChain)",
|
|
1863
|
+
"roles.fallback.none": "仅角色链(不追加 rootChain)",
|
|
1864
|
+
"roles.add": "添加角色",
|
|
1865
|
+
"roles.remove": "删除该角色",
|
|
1866
|
+
"roles.selector.add": "添加选择器",
|
|
1867
|
+
"roles.rules": "角色规则",
|
|
1868
|
+
"roles.rules.hint": "顺序匹配 origin/provider/model,未命中 → inherit(root 链)",
|
|
1869
|
+
"roles.rules.tooltip": "规则命中后走对应角色的链;未命中走内置 inherit(rootChain)。",
|
|
1870
|
+
"roles.rule.origin": "来源",
|
|
1871
|
+
"roles.rule.origin.any": "任意",
|
|
1872
|
+
"roles.rule.origin.root": "root",
|
|
1873
|
+
"roles.rule.origin.subagent": "subagent",
|
|
1874
|
+
"roles.rule.provider": "provider",
|
|
1875
|
+
"roles.rule.provider.any": "任意",
|
|
1876
|
+
"roles.rule.model": "model",
|
|
1877
|
+
"roles.rule.model.any": "任意",
|
|
1878
|
+
"roles.rule.role": "角色",
|
|
1879
|
+
"roles.rule.role.inherit": "inherit(内置:root 链)",
|
|
1880
|
+
"roles.rule.roleSelectPlaceholder": "选择角色",
|
|
1881
|
+
"roles.rule.roleUndeclared.short": "(未声明)",
|
|
1882
|
+
"roles.addRule": "添加规则",
|
|
1883
|
+
"roles.removeRule": "删除该规则",
|
|
1884
|
+
"validation.blocked": "配置校验未通过,保存被拦截:",
|
|
1885
|
+
"validation.roleIdFormat": "角色 id \"{id}\" 不符合格式 /^[a-z0-9-]{1,32}$/",
|
|
1886
|
+
"validation.roleIdReserved": "\"inherit\" 为保留角色 id,不能用于角色实体",
|
|
1887
|
+
"validation.roleIdDuplicate": "角色 id \"{id}\" 重复",
|
|
1888
|
+
"validation.ruleRoleUndeclared": "规则引用了未声明的角色 \"{role}\"",
|
|
1889
|
+
"validation.ruleRoleRequired": "规则未选择角色:请选择目标角色,或删除该行",
|
|
1890
|
+
"validation.selector": "选择器 \"{entry}\" 非法:{message}",
|
|
1891
|
+
"legacy.banner": "检测到旧格式配置字段({keys}):已按新模型展示,请按 docs/configuration.md 迁移表手工改写;插件不会自动改写配置。",
|
|
1892
|
+
"catalog.empty": "暂无可用模型:请先在模型页添加模型,添加后此处将自动可选。",
|
|
1893
|
+
"catalog.error": "模型目录读取失败:{message}",
|
|
1894
|
+
"catalog.partial": "部分 provider 模型查询失败:{message}",
|
|
1895
|
+
"catalog.outside.hint": "目录外,可保留原值",
|
|
1896
|
+
"catalog.outside.tooltip": "不在当前模型目录,可保留原值并保存;新增条目仅可从目录选择。",
|
|
1897
|
+
"catalog.outside.short": " (目录外)",
|
|
1898
|
+
"catalog.unconfigured.short": " (未配置)",
|
|
1899
|
+
"status.title": "运行状态(只读)",
|
|
1900
|
+
"status.effectiveModel.label": "当前生效模型:",
|
|
1901
|
+
"status.effectiveModel.unavailable": "fallbacks 未启用(或 rootChain 未配置)",
|
|
1902
|
+
"status.effectiveModel.note": "配置 + 最近切换推导,非实时路由探测",
|
|
1903
|
+
"status.switches.label": "最近切换:",
|
|
1904
|
+
"status.switches.empty": "本会话暂无 fallback 切换。",
|
|
1905
|
+
"status.switches.error": "切换历史读取失败:{message}",
|
|
1906
|
+
"status.switches.compact": "最近 {count} 次 · {from} → {to}({role} · {reason})",
|
|
1907
|
+
"status.switches.reason.trigger-code": "触发失败码",
|
|
1908
|
+
"status.switches.reason.always-cap": "always 模式上限",
|
|
1909
|
+
"status.selectionNote": "说明:web 前端手动选择的模型可能在切换后重新套用(标记载体已随本地 patch 移除)。",
|
|
1910
|
+
"general.title": "模型故障降级",
|
|
1911
|
+
"general.enabled": "已启用",
|
|
1912
|
+
"general.disabled": "未启用",
|
|
1913
|
+
"general.unknown": "未知",
|
|
1914
|
+
"general.unavailable": "状态通道暂不可达",
|
|
1915
|
+
"general.switch": "最近切换:{from} → {to}({role} · {reason})",
|
|
1916
|
+
"general.switch.empty": "本会话暂无切换",
|
|
1917
|
+
"general.error": "状态读取失败:{message}",
|
|
1918
|
+
"chat.switch.title": "模型切换",
|
|
1919
|
+
"chat.switch.summary": "{from} → {to}({role} · {reason})",
|
|
1920
|
+
"defaults.prefix": "默认值",
|
|
1921
|
+
"save": "保存",
|
|
1922
|
+
"save.saving": "保存中…",
|
|
1923
|
+
"save.error": "保存失败:{message}",
|
|
1924
|
+
"close": "关闭",
|
|
1925
|
+
"reset": "恢复默认",
|
|
1926
|
+
"reset.confirmTitle": "恢复默认配置",
|
|
1927
|
+
"reset.confirm": "恢复后 fallbacks 配置将回到插件默认值,当前编辑内容会丢失。",
|
|
1928
|
+
"reset.confirm.cancel": "取消",
|
|
1929
|
+
"reset.confirm.action": "恢复默认",
|
|
1930
|
+
"reset.saving": "恢复中…",
|
|
1931
|
+
"loading": "加载中…",
|
|
1932
|
+
"unavailable": "fallbacks 配置通道暂不可达:以下显示默认配置(或上次读取值),可尝试保存;保存失败会在此处如实提示。",
|
|
1933
|
+
"error.generic": "出错:{message}"
|
|
1934
|
+
};
|
|
1935
|
+
/** English dictionary, checked complete against the zh key set. */
|
|
1936
|
+
const en = {
|
|
1937
|
+
"title": "Fallbacks",
|
|
1938
|
+
"intro": "Automatic fallback on model failures",
|
|
1939
|
+
"collapse": "Hide settings",
|
|
1940
|
+
"expand": "Show settings",
|
|
1941
|
+
"unsaved": "Unsaved",
|
|
1942
|
+
"discard": "Discard",
|
|
1943
|
+
"retry": "Retry",
|
|
1944
|
+
"readOnly": "Settings are read-only in this environment.",
|
|
1945
|
+
"enabled.label": "Enable failure fallback",
|
|
1946
|
+
"enabled.hint": "Plugin never intervenes when off",
|
|
1947
|
+
"enabled.tooltip": "When off the plugin never intervenes; when on with no rootChain configured behavior is identical to an uninstalled plugin.",
|
|
1948
|
+
"enabled.off": "Feature disabled: turn on the enabled switch to show the configuration interface.",
|
|
1949
|
+
"triggerCodes.label": "Trigger failure codes",
|
|
1950
|
+
"triggerCodes.hint": "Failures with these codes enter fallback decision",
|
|
1951
|
+
"triggerCodes.tooltip": "Failures with these codes enter chain decision; retryable failures (e.g. 5xx) back off via llm-retry first and reach the decision only when its budget is exhausted.",
|
|
1952
|
+
"triggerCodes.RATE_LIMIT": "Rate limit (429)",
|
|
1953
|
+
"triggerCodes.QUOTA": "Quota exceeded",
|
|
1954
|
+
"triggerCodes.AUTH": "Auth / permission failure",
|
|
1955
|
+
"triggerCodes.extra": "Custom codes are preserved: {codes}.",
|
|
1956
|
+
"revertPolicy.label": "After cooldown",
|
|
1957
|
+
"revertPolicy.cooldown-expiry": "Return to the primary model",
|
|
1958
|
+
"revertPolicy.never": "Keep the fallback model (until session end)",
|
|
1959
|
+
"revertPolicy.hint": "Whether to return to the primary model after cooldown",
|
|
1960
|
+
"revertPolicy.tooltip": "A model switched away from stays out of candidacy during its cooldown; this policy decides whether it returns afterwards.",
|
|
1961
|
+
"cooldownMs.label": "Cooldown (milliseconds)",
|
|
1962
|
+
"cooldownMs.hint": "Models stay out of candidacy during cooldown",
|
|
1963
|
+
"cooldownMs.tooltip": "Switched-away or failed models stay out of candidacy during the cooldown window.",
|
|
1964
|
+
"maxSwitchesPerStep.label": "Max switches per step",
|
|
1965
|
+
"maxSwitchesPerStep.hint": "Stops switching beyond the cap",
|
|
1966
|
+
"maxSwitchesPerStep.tooltip": "Beyond this the step stops switching and ends with the original error semantics, preventing chain loops from amplifying latency.",
|
|
1967
|
+
"alwaysModeRetryCap.label": "Always-mode retry cap",
|
|
1968
|
+
"alwaysModeRetryCap.hint": "Switches after the cap; 0 disables",
|
|
1969
|
+
"alwaysModeRetryCap.tooltip": "Models whose retryPolicy is always switch after this many retries within one request; 0 disables.",
|
|
1970
|
+
"rootChain.label": "Root agent fallback chain",
|
|
1971
|
+
"rootChain.hint": "Unset = root does not fall back",
|
|
1972
|
+
"rootChain.tooltip": "When the root agent fails it falls back down this ordered selector list; unset behaves like an uninstalled plugin.",
|
|
1973
|
+
"rootChain.selector.add": "Add selector",
|
|
1974
|
+
"chains.selector.remove": "Remove this selector",
|
|
1975
|
+
"chains.selector.providerPlaceholder": "Select provider",
|
|
1976
|
+
"chains.selector.modelPlaceholder": "Select model",
|
|
1977
|
+
"chains.selector.wildcard": "Wildcard this provider (provider/*)",
|
|
1978
|
+
"chains.selector.noModels": "No models available for this provider (catalog lookup failed); use the wildcard or pick another provider.",
|
|
1979
|
+
"roles.list.label": "Declared roles",
|
|
1980
|
+
"roles.list.hint": "Declare roles before rules can reference them",
|
|
1981
|
+
"roles.list.tooltip": "Role ids must match /^[a-z0-9-]{1,32}$/ and be unique; \"inherit\" is reserved and cannot be used as a role id.",
|
|
1982
|
+
"roles.id": "ID",
|
|
1983
|
+
"roles.id.hint": "lowercase letters, digits, hyphens; 1–32 chars",
|
|
1984
|
+
"roles.idPlaceholder": "e.g. reviewer",
|
|
1985
|
+
"roles.label": "Label",
|
|
1986
|
+
"roles.description": "Description",
|
|
1987
|
+
"roles.fallback": "Chain append",
|
|
1988
|
+
"roles.fallback.inherit-root": "Inherit root (append rootChain after the role chain)",
|
|
1989
|
+
"roles.fallback.none": "Role chain only (no rootChain)",
|
|
1990
|
+
"roles.add": "Add role",
|
|
1991
|
+
"roles.remove": "Remove this role",
|
|
1992
|
+
"roles.selector.add": "Add selector",
|
|
1993
|
+
"roles.rules": "Role rules",
|
|
1994
|
+
"roles.rules.hint": "Matches origin/provider/model in order; no match → inherit (root chain)",
|
|
1995
|
+
"roles.rules.tooltip": "A matched rule uses that role's chain; no match uses the built-in inherit (rootChain).",
|
|
1996
|
+
"roles.rule.origin": "Origin",
|
|
1997
|
+
"roles.rule.origin.any": "Any",
|
|
1998
|
+
"roles.rule.origin.root": "root",
|
|
1999
|
+
"roles.rule.origin.subagent": "subagent",
|
|
2000
|
+
"roles.rule.provider": "provider",
|
|
2001
|
+
"roles.rule.provider.any": "Any",
|
|
2002
|
+
"roles.rule.model": "model",
|
|
2003
|
+
"roles.rule.model.any": "Any",
|
|
2004
|
+
"roles.rule.role": "role",
|
|
2005
|
+
"roles.rule.role.inherit": "inherit (built-in: root chain)",
|
|
2006
|
+
"roles.rule.roleSelectPlaceholder": "Select role",
|
|
2007
|
+
"roles.rule.roleUndeclared.short": " (undeclared)",
|
|
2008
|
+
"roles.addRule": "Add rule",
|
|
2009
|
+
"roles.removeRule": "Remove this rule",
|
|
2010
|
+
"validation.blocked": "Configuration validation failed; save was blocked: ",
|
|
2011
|
+
"validation.roleIdFormat": "Role id \"{id}\" does not match /^[a-z0-9-]{1,32}$/",
|
|
2012
|
+
"validation.roleIdReserved": "\"inherit\" is a reserved role id and cannot be declared",
|
|
2013
|
+
"validation.roleIdDuplicate": "Duplicate role id \"{id}\"",
|
|
2014
|
+
"validation.ruleRoleUndeclared": "Rule references undeclared role \"{role}\"",
|
|
2015
|
+
"validation.ruleRoleRequired": "Rule has no role selected: pick a target role, or remove the row",
|
|
2016
|
+
"validation.selector": "Invalid selector \"{entry}\": {message}",
|
|
2017
|
+
"legacy.banner": "Legacy config fields detected ({keys}): now shown in the new model — rewrite them manually following the migration table in docs/configuration.md (the plugin will not rewrite them automatically).",
|
|
2018
|
+
"catalog.empty": "No models yet: add a model on the Models page first; options will appear here automatically.",
|
|
2019
|
+
"catalog.error": "Model catalog read failed: {message}",
|
|
2020
|
+
"catalog.partial": "Some provider model lookups failed: {message}",
|
|
2021
|
+
"catalog.outside.hint": "Outside catalog; the value can be kept",
|
|
2022
|
+
"catalog.outside.tooltip": "Not in the current model catalog; you can keep the original value and save it (new entries are restricted to the catalog).",
|
|
2023
|
+
"catalog.outside.short": " (outside catalog)",
|
|
2024
|
+
"catalog.unconfigured.short": " (not configured)",
|
|
2025
|
+
"status.title": "Runtime status (read-only)",
|
|
2026
|
+
"status.effectiveModel.label": "Current effective model: ",
|
|
2027
|
+
"status.effectiveModel.unavailable": "Fallbacks disabled (or rootChain not configured)",
|
|
2028
|
+
"status.effectiveModel.note": "Derived from configuration and recent switches; not real-time route probing",
|
|
2029
|
+
"status.switches.label": "Recent switches: ",
|
|
2030
|
+
"status.switches.empty": "No fallback switches in this session yet.",
|
|
2031
|
+
"status.switches.error": "Switch history read failed: {message}",
|
|
2032
|
+
"status.switches.compact": "last {count} · {from} → {to} ({role} · {reason})",
|
|
2033
|
+
"status.switches.reason.trigger-code": "trigger code",
|
|
2034
|
+
"status.switches.reason.always-cap": "always-mode cap",
|
|
2035
|
+
"status.selectionNote": "Note: a model manually selected in the web front end may be re-applied after a switch (the marker coordination shipped with the local patch has been removed).",
|
|
2036
|
+
"general.title": "Model failover",
|
|
2037
|
+
"general.enabled": "Enabled",
|
|
2038
|
+
"general.disabled": "Disabled",
|
|
2039
|
+
"general.unknown": "Unknown",
|
|
2040
|
+
"general.unavailable": "Status channel unavailable",
|
|
2041
|
+
"general.switch": "Last switch: {from} → {to} ({role} · {reason})",
|
|
2042
|
+
"general.switch.empty": "No switches this session",
|
|
2043
|
+
"general.error": "Status read failed: {message}",
|
|
2044
|
+
"chat.switch.title": "Model switch",
|
|
2045
|
+
"chat.switch.summary": "{from} → {to} ({role} · {reason})",
|
|
2046
|
+
"defaults.prefix": "Default",
|
|
2047
|
+
"save": "Save",
|
|
2048
|
+
"save.saving": "Saving…",
|
|
2049
|
+
"save.error": "Save failed: {message}",
|
|
2050
|
+
"close": "Close",
|
|
2051
|
+
"reset": "Reset to defaults",
|
|
2052
|
+
"reset.confirmTitle": "Reset to defaults",
|
|
2053
|
+
"reset.confirm": "Resetting restores the fallbacks configuration to plugin defaults; your current edits will be lost.",
|
|
2054
|
+
"reset.confirm.cancel": "Cancel",
|
|
2055
|
+
"reset.confirm.action": "Reset",
|
|
2056
|
+
"reset.saving": "Resetting…",
|
|
2057
|
+
"loading": "Loading…",
|
|
2058
|
+
"unavailable": "The fallbacks config channel is unreachable: showing the default configuration (or the last read value). You can try to save; failures will be reported here.",
|
|
2059
|
+
"error.generic": "Error: {message}"
|
|
2060
|
+
};
|
|
2061
|
+
/** The settings section's dictionary namespace. */
|
|
2062
|
+
const NS = "fallbacks";
|
|
2063
|
+
/**
|
|
2064
|
+
* Reason → locale key map for switch summaries (S-c; shared by the card's
|
|
2065
|
+
* status block and the General page status row). The session log is durable
|
|
2066
|
+
* and forward-compatible: a reason value outside the current union (a newer
|
|
2067
|
+
* plugin wrote it) renders raw instead of falling into a binary else branch.
|
|
2068
|
+
*/
|
|
2069
|
+
const SWITCH_REASON_KEYS = {
|
|
2070
|
+
"trigger-code": "status.switches.reason.trigger-code",
|
|
2071
|
+
"always-cap": "status.switches.reason.always-cap"
|
|
2072
|
+
};
|
|
2073
|
+
/** Human-readable trigger-code labels (spec §4 用户直观性). */
|
|
2074
|
+
const TRIGGER_CODE_LABELS = {
|
|
2075
|
+
RATE_LIMIT: "triggerCodes.RATE_LIMIT",
|
|
2076
|
+
QUOTA: "triggerCodes.QUOTA",
|
|
2077
|
+
AUTH: "triggerCodes.AUTH"
|
|
2078
|
+
};
|
|
2079
|
+
/**
|
|
2080
|
+
* The known trigger codes the form toggles; unknown codes are preserved.
|
|
2081
|
+
* M-04: derived from the host defaults so the toggle set can never drift from
|
|
2082
|
+
* the decision set (`defaultFallbacksConfig.triggerCodes` is the single
|
|
2083
|
+
* source of truth; the labels mapping above stays keyed by code).
|
|
2084
|
+
*/
|
|
2085
|
+
const KNOWN_TRIGGER_CODES = [...defaultFallbacksConfig.triggerCodes];
|
|
2086
|
+
/** Toggle one known code's membership in `codes` (used by the form; pure). */
|
|
2087
|
+
function withTriggerCode(codes, code, present) {
|
|
2088
|
+
const next = new Set(codes);
|
|
2089
|
+
if (present) next.add(code);
|
|
2090
|
+
else next.delete(code);
|
|
2091
|
+
return [...next];
|
|
2092
|
+
}
|
|
2093
|
+
//#endregion
|
|
2094
|
+
//#region \0dsh-css:/Users/bibi/workspace/ai/deepseek/dsh-llm-fallbacks/src/client/FallbacksCard.module.css.mjs
|
|
2095
|
+
const css$2 = "\n\n\n._8827595f_card {\n list-style: none;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 12px;\n background: var(--dsw-alias-bg-layer-3);\n transition: border-color .16s, background .16s;\n}\n\n._8827595f_card:hover {\n border-color: var(--dsw-alias-label-dimmed);\n}\n\n\n._06704203_cardOpen {\n background: var(--dsw-alias-bg-layer-2);\n border-color: var(--dsw-alias-label-dimmed);\n}\n\n\n._e488d460_header {\n width: 100%;\n appearance: none;\n border: 0;\n background: none;\n font: inherit;\n color: inherit;\n text-align: left;\n cursor: pointer;\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 14px 16px;\n border-radius: 12px;\n}\n\n._e488d460_header:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: -2px;\n}\n\n\n._f5dfe084_headText {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n._8d39bde6_name {\n font-size: 15px;\n font-weight: 600;\n line-height: 1.4;\n color: var(--dsw-alias-label-primary);\n}\n\n._346f3b69_description {\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n\n._631094a0_chevron {\n flex: none;\n color: var(--dsw-alias-label-tertiary);\n transition: transform .16s;\n}\n\n._44836ce8_chevronOpen {\n transform: rotate(180deg);\n}\n\n\n._dbaa7975_body {\n border-top: 1px solid var(--dsw-alias-border-l2);\n margin: 0 16px;\n padding-bottom: 8px;\n}\n\n._4e39cf17_readOnly {\n margin: 12px 0 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n\n\n._99bf7f3c_pending {\n flex: none;\n border-radius: 999px;\n padding: 1px 8px;\n font-size: 11px;\n line-height: 17px;\n font-weight: 500;\n white-space: nowrap;\n background: var(--dsw-alias-bg-module-platform);\n color: var(--dsw-alias-label-secondary);\n}\n\n._114985b2_footer {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 8px;\n padding: 12px 0 4px;\n border-top: 1px solid var(--dsw-alias-border-l2);\n}\n\n\n._9860a5b1_notice {\n margin: 12px 0 0;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-state-business-primary);\n}\n\n\n._5006d43e_legacyNotice {\n margin: 12px 0 0;\n padding: 10px 12px;\n border-radius: 8px;\n background: var(--dsw-alias-bg-module-platform);\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-state-business-primary);\n}\n\n._21918751_error {\n margin: 12px 0 0;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-state-error-primary);\n}\n\n\n._7cea4b2f_noticeRow {\n display: flex;\n align-items: center;\n gap: 8px;\n margin: 12px 0 0;\n}\n\n._7cea4b2f_noticeRow ._21918751_error {\n flex: 1;\n min-width: 0;\n margin: 0;\n}\n\n\n._4058c747_form {\n display: flex;\n flex-direction: column;\n gap: 12px;\n padding: 12px 0 0;\n}\n\n\n._9998065c_checkboxRow {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n._02c2f6df_checkLabel {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 4px;\n font-size: 14px;\n line-height: 22px;\n font-weight: 400;\n color: var(--dsw-alias-label-primary);\n cursor: pointer;\n}\n\n._f2d47237_checkLabelTitle {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n font-weight: 400;\n}\n\n._c1bed8ea_checkLabelDesc {\n font-size: 12px;\n line-height: 18px;\n font-weight: 400;\n color: var(--dsw-alias-label-tertiary);\n}\n\n\n._06389b18_checkbox {\n flex: none;\n width: 16px;\n height: 16px;\n margin: 0;\n accent-color: var(--dsw-alias-brand-primary);\n cursor: pointer;\n}\n\n._06389b18_checkbox:disabled {\n opacity: 0.4;\n cursor: default;\n}\n\n\n._94c91bfd_fieldset {\n margin: 0;\n padding: 0;\n border: none;\n display: flex;\n flex-direction: column;\n gap: 12px;\n}\n\n\n._67826267_field {\n display: flex;\n flex-direction: column;\n gap: 6px;\n margin: 0;\n padding: 0;\n border: none;\n min-width: 0;\n}\n\n._13e68c3f_fieldLabel {\n \n display: inline-flex;\n align-items: center;\n gap: 10px;\n padding: 0;\n font-size: 12px;\n line-height: 18px;\n font-weight: 500;\n color: var(--dsw-alias-label-secondary);\n}\n\n._4bc809b8_hint {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n flex-wrap: wrap;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n._579f813a_defaultNote {\n font-size: 12px;\n line-height: 18px;\n font-weight: 400;\n color: var(--dsw-alias-label-tertiary);\n}\n\n\n._a6244318_infoHint {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex: none;\n width: 16px;\n height: 16px;\n border-radius: 50%;\n font-size: 14px;\n line-height: 1;\n color: var(--dsw-alias-label-tertiary);\n background: var(--dsw-alias-interactive-bg-hover);\n cursor: help;\n user-select: none;\n}\n\n._a6244318_infoHint:focus-visible {\n outline: none;\n box-shadow: 0 0 0 2px var(--dsw-alias-border-l3);\n}\n\n\n._e655c840_infoHintDisabled {\n opacity: 0.4;\n cursor: default;\n}\n\n\n._a2521bb0_optionRow {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 6px 8px;\n border-radius: 6px;\n font-size: 14px;\n line-height: 22px;\n color: var(--dsw-alias-label-primary);\n cursor: pointer;\n}\n\n._a2521bb0_optionRow:hover:has(input:not(:disabled)) {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n\n._06389b18_checkbox,\n._a2521bb0_optionRow input,\n._929f3ef3_wildcardCell input {\n flex: none;\n width: 16px;\n height: 16px;\n margin: 0;\n accent-color: var(--dsw-alias-brand-primary);\n cursor: pointer;\n}\n\n._06389b18_checkbox:focus-visible,\n._a2521bb0_optionRow input:focus-visible,\n._929f3ef3_wildcardCell input:focus-visible {\n outline: none;\n box-shadow: 0 0 0 2px var(--dsw-alias-border-l3);\n}\n\n._a2521bb0_optionRow input:disabled,\n._929f3ef3_wildcardCell input:disabled {\n cursor: default;\n}\n\n\n._f9d86f7b_input {\n box-sizing: border-box;\n width: 100%;\n height: 32px;\n padding: 0 10px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 8px;\n font: inherit;\n font-size: 14px;\n line-height: 22px;\n background: var(--dsw-alias-bg-layer-1);\n color: var(--dsw-alias-label-primary);\n}\n\n._f9d86f7b_input:focus {\n outline: none;\n border-color: var(--dsw-alias-brand-primary);\n}\n\n._f9d86f7b_input::placeholder {\n color: var(--dsw-alias-label-dimmed);\n}\n\n._f9d86f7b_input:disabled {\n opacity: 0.6;\n cursor: default;\n}\n\n\n._7b60337a_inputInvalid,\n._7b60337a_inputInvalid:focus {\n border-color: var(--dsw-alias-state-error-primary);\n}\n\n\nselect._f9d86f7b_input {\n max-width: 240px;\n cursor: pointer;\n}\n\n\n._16eb7153_selectInput {\n appearance: none;\n padding-right: 32px;\n \n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12' fill='none'%3E%3Cpath d='M3 4.5L6 7.5L9 4.5' stroke='%2381858C' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E\");\n background-repeat: no-repeat;\n background-position: right 12px center;\n background-size: 12px 12px;\n}\n\n\n._3e95886d_numberFields {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));\n gap: 8px;\n}\n\n._0cfb5881_list {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 4px;\n}\n\n\n._f5c620d6_editorCard {\n display: flex;\n flex-direction: column;\n gap: 14px;\n padding: 14px 16px;\n border-radius: 12px;\n background: var(--dsw-alias-bg-module-platform);\n}\n\n._224b1387_cardFoot {\n display: flex;\n justify-content: flex-end;\n}\n\n._8e220e07_ruleGrid {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n}\n\n._24a12a2f_ruleCell {\n display: flex;\n flex-direction: column;\n gap: 4px;\n min-width: 120px;\n flex: 1;\n}\n\n._1014c097_ruleCellLabel {\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-secondary);\n}\n\n\n._15a12620_chainSelectors {\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n._240cbbf8_selectorRow {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n\n._929f3ef3_wildcardCell {\n flex-direction: row;\n align-items: center;\n gap: 8px;\n min-width: 200px;\n align-self: center;\n padding: 6px 8px;\n border-radius: 6px;\n cursor: pointer;\n}\n\n._929f3ef3_wildcardCell:hover:has(input:not(:disabled)) {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n\n._70effa64_iconButton {\n position: relative;\n box-sizing: border-box;\n appearance: none;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n padding: 0;\n border: 0;\n border-radius: 6px;\n background: none;\n color: var(--dsw-alias-label-tertiary);\n cursor: pointer;\n}\n\n._70effa64_iconButton:disabled {\n opacity: 0.4;\n cursor: default;\n}\n\n._70effa64_iconButton:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-primary);\n}\n\n._70effa64_iconButton:focus-visible {\n outline: none;\n box-shadow: 0 0 0 2px var(--dsw-alias-border-l3);\n}\n\n._70effa64_iconButton::after {\n content: attr(data-tip);\n position: absolute;\n bottom: calc(100% + 6px);\n left: 50%;\n transform: translateX(-50%);\n padding: 3px 8px;\n border-radius: 6px;\n background: var(--dsw-alias-label-primary);\n color: var(--dsw-alias-bg-layer-3);\n font-size: 11px;\n line-height: 17px;\n white-space: nowrap;\n opacity: 0;\n pointer-events: none;\n transition: opacity .12s;\n}\n\n._70effa64_iconButton:hover::after,\n._70effa64_iconButton:focus-visible::after {\n opacity: 1;\n}\n\n\n._a0968257_iconButtonDanger:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover-danger);\n color: var(--dsw-alias-state-error-primary);\n}\n\n\n._28202c90_addButton {\n align-self: flex-start;\n}\n\n\n._747fd56d_primaryButton,\n._7e855445_secondaryButton {\n box-sizing: border-box;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 4px;\n height: 36px;\n padding: 0 14px;\n border: none;\n border-radius: 18px;\n font: inherit;\n font-size: 14px;\n line-height: 22px;\n cursor: pointer;\n}\n\n._747fd56d_primaryButton {\n background: var(--dsw-alias-button-primary-fill);\n color: var(--dsw-alias-label-primary-foreground);\n}\n\n._747fd56d_primaryButton:hover:not(:disabled) {\n background: var(--dsw-alias-button-primary-hover);\n}\n\n._7e855445_secondaryButton {\n border: 1px solid var(--dsw-alias-border-l2);\n background: transparent;\n color: var(--dsw-alias-label-primary);\n}\n\n._7e855445_secondaryButton:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover-solid);\n}\n\n._747fd56d_primaryButton:disabled,\n._7e855445_secondaryButton:disabled {\n opacity: 0.4;\n cursor: default;\n}\n\n._747fd56d_primaryButton:focus-visible,\n._7e855445_secondaryButton:focus-visible {\n outline: none;\n box-shadow: 0 0 0 2px var(--dsw-alias-border-l3);\n}\n\n\n._fbd045e0_statusBlock {\n display: flex;\n flex-direction: column;\n gap: 12px;\n padding: 12px 14px;\n margin-top: 12px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 12px;\n}\n\n._d4f70367_statusTitle {\n font-size: 12px;\n line-height: 18px;\n font-weight: 500;\n color: var(--dsw-alias-label-secondary);\n}\n\n\n._8ab300ed_statusLine {\n margin: 0;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-secondary);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n._e8cedbf5_statusLineLabel {\n font-weight: 500;\n color: var(--dsw-alias-label-secondary);\n}\n\n\n._b48dfcba_offNotice {\n margin: 0;\n padding: 12px;\n border: 1px dashed var(--dsw-alias-border-l3);\n border-radius: 8px;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-tertiary);\n text-align: center;\n}\n\n\n._ec8bd970_resetDialog {\n width: min(480px, 100%);\n}\n\n._8677165c_confirmDanger:not(:disabled) {\n border-color: var(--dsw-alias-state-error-primary);\n color: var(--dsw-alias-state-error-primary);\n}\n\n._8677165c_confirmDanger:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover-danger);\n}\n\n@media (prefers-reduced-motion: reduce) {\n ._70effa64_iconButton::after {\n transition: none;\n }\n}\n";
|
|
2096
|
+
const tagId$2 = "dsh-llm-fallbacks/FallbacksCard.module.css";
|
|
2097
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$2) + "]") === null) {
|
|
2098
|
+
const tag = document.createElement("style");
|
|
2099
|
+
tag.dataset.plugin = "dsh-llm-fallbacks";
|
|
2100
|
+
tag.dataset.pluginCss = tagId$2;
|
|
2101
|
+
tag.textContent = css$2;
|
|
2102
|
+
document.head.appendChild(tag);
|
|
2103
|
+
}
|
|
2104
|
+
var FallbacksCard_module_css_default = {
|
|
2105
|
+
"card": "_8827595f_card",
|
|
2106
|
+
"cardOpen": "_06704203_cardOpen",
|
|
2107
|
+
"header": "_e488d460_header",
|
|
2108
|
+
"headText": "_f5dfe084_headText",
|
|
2109
|
+
"name": "_8d39bde6_name",
|
|
2110
|
+
"description": "_346f3b69_description",
|
|
2111
|
+
"chevron": "_631094a0_chevron",
|
|
2112
|
+
"chevronOpen": "_44836ce8_chevronOpen",
|
|
2113
|
+
"body": "_dbaa7975_body",
|
|
2114
|
+
"readOnly": "_4e39cf17_readOnly",
|
|
2115
|
+
"pending": "_99bf7f3c_pending",
|
|
2116
|
+
"footer": "_114985b2_footer",
|
|
2117
|
+
"notice": "_9860a5b1_notice",
|
|
2118
|
+
"legacyNotice": "_5006d43e_legacyNotice",
|
|
2119
|
+
"error": "_21918751_error",
|
|
2120
|
+
"noticeRow": "_7cea4b2f_noticeRow",
|
|
2121
|
+
"form": "_4058c747_form",
|
|
2122
|
+
"checkboxRow": "_9998065c_checkboxRow",
|
|
2123
|
+
"checkLabel": "_02c2f6df_checkLabel",
|
|
2124
|
+
"checkLabelTitle": "_f2d47237_checkLabelTitle",
|
|
2125
|
+
"checkLabelDesc": "_c1bed8ea_checkLabelDesc",
|
|
2126
|
+
"checkbox": "_06389b18_checkbox",
|
|
2127
|
+
"fieldset": "_94c91bfd_fieldset",
|
|
2128
|
+
"field": "_67826267_field",
|
|
2129
|
+
"fieldLabel": "_13e68c3f_fieldLabel",
|
|
2130
|
+
"hint": "_4bc809b8_hint",
|
|
2131
|
+
"defaultNote": "_579f813a_defaultNote",
|
|
2132
|
+
"infoHint": "_a6244318_infoHint",
|
|
2133
|
+
"infoHintDisabled": "_e655c840_infoHintDisabled",
|
|
2134
|
+
"optionRow": "_a2521bb0_optionRow",
|
|
2135
|
+
"wildcardCell": "_929f3ef3_wildcardCell",
|
|
2136
|
+
"input": "_f9d86f7b_input",
|
|
2137
|
+
"inputInvalid": "_7b60337a_inputInvalid",
|
|
2138
|
+
"selectInput": "_16eb7153_selectInput",
|
|
2139
|
+
"numberFields": "_3e95886d_numberFields",
|
|
2140
|
+
"list": "_0cfb5881_list",
|
|
2141
|
+
"editorCard": "_f5c620d6_editorCard",
|
|
2142
|
+
"cardFoot": "_224b1387_cardFoot",
|
|
2143
|
+
"ruleGrid": "_8e220e07_ruleGrid",
|
|
2144
|
+
"ruleCell": "_24a12a2f_ruleCell",
|
|
2145
|
+
"ruleCellLabel": "_1014c097_ruleCellLabel",
|
|
2146
|
+
"chainSelectors": "_15a12620_chainSelectors",
|
|
2147
|
+
"selectorRow": "_240cbbf8_selectorRow",
|
|
2148
|
+
"iconButton": "_70effa64_iconButton",
|
|
2149
|
+
"iconButtonDanger": "_a0968257_iconButtonDanger",
|
|
2150
|
+
"addButton": "_28202c90_addButton",
|
|
2151
|
+
"primaryButton": "_747fd56d_primaryButton",
|
|
2152
|
+
"secondaryButton": "_7e855445_secondaryButton",
|
|
2153
|
+
"statusBlock": "_fbd045e0_statusBlock",
|
|
2154
|
+
"statusTitle": "_d4f70367_statusTitle",
|
|
2155
|
+
"statusLine": "_8ab300ed_statusLine",
|
|
2156
|
+
"statusLineLabel": "_e8cedbf5_statusLineLabel",
|
|
2157
|
+
"offNotice": "_b48dfcba_offNotice",
|
|
2158
|
+
"resetDialog": "_ec8bd970_resetDialog",
|
|
2159
|
+
"confirmDanger": "_8677165c_confirmDanger"
|
|
2160
|
+
};
|
|
2161
|
+
//#endregion
|
|
2162
|
+
//#region src/client/FallbacksCard.tsx
|
|
2163
|
+
/**
|
|
2164
|
+
* Fallbacks settings card — the `fallbacks` plugin card on the web settings
|
|
2165
|
+
* "插件配置" page (spec §4). Registered into the `settings.plugin.item` slot
|
|
2166
|
+
* (id `fallbacks`, order 30, alongside the upstream bash/agent-loop/web-search
|
|
2167
|
+
* cards and the advisor card); owner props are empty and all data flows
|
|
2168
|
+
* through {@link FallbacksSettingsController}.
|
|
2169
|
+
*
|
|
2170
|
+
* The card chrome replicates the upstream `PluginCard` contract (self-drawn:
|
|
2171
|
+
* the upstream client value face exports no reusable card): a collapsible
|
|
2172
|
+
* `<li>` whose header is a button stacking the plugin name over its
|
|
2173
|
+
* description, with a dirty "unsaved" pill and a rotating chevron
|
|
2174
|
+
* (`IconChevronDownOutline14` from ui-primitives — a CLIENT_EXTERNALS value
|
|
2175
|
+
* import), `aria-expanded`/`aria-label` like the upstream header; a divider
|
|
2176
|
+
* under the header; then the form content; then a footer with
|
|
2177
|
+
* Discard / Reset / Save carrying the upstream disabled semantics — save =
|
|
2178
|
+
* `!dirty || saving || !writable`, discard = `!dirty || saving` (KD-U1).
|
|
2179
|
+
* Disclosure is card-local state: which card a user has open is a reading
|
|
2180
|
+
* gesture, and staged edits outlive collapsing — the pill rides the header
|
|
2181
|
+
* (upstream rationale).
|
|
2182
|
+
*
|
|
2183
|
+
* The form body is the two-block editing surface (spec §8): the `enabled`
|
|
2184
|
+
* checkbox row, the 6 top-level scalar fields (trigger codes / revert
|
|
2185
|
+
* policy / three numeric fields), the `rootChain` block (block 1 — the
|
|
2186
|
+
* root agent's single chain, no key input), and the roles block (block 2 —
|
|
2187
|
+
* declared role entity cards from `roles.list` plus the rule rows from
|
|
2188
|
+
* `roles.rules`, whose role field is a dropdown bound to the declared ids
|
|
2189
|
+
* + the built-in `inherit`, same-page live). Saving runs `validateDraft`
|
|
2190
|
+
* first — id format/reserved word/duplicates, undeclared rule role
|
|
2191
|
+
* references, and illegal selectors block the write with a validation
|
|
2192
|
+
* banner + inline red borders (never touching the store error path); a
|
|
2193
|
+
* non-empty `state.legacyKeys` renders the migration banner at the top of
|
|
2194
|
+
* the card body. The row editors keep their filled editorCard surface
|
|
2195
|
+
* inside the card, with `--dsw-alias-*` tokens throughout. The reset-
|
|
2196
|
+
* to-defaults confirmation stays a `Modal` (the delete-confirm pattern of
|
|
2197
|
+
* the Models page) — no `window.confirm`.
|
|
2198
|
+
*
|
|
2199
|
+
* The page-only chrome is gone (720px column wrapper, title/intro banners,
|
|
2200
|
+
* page-bottom status block): the AC-7 read-only status (derived effective
|
|
2201
|
+
* model + recent-switch summary) is folded into the card body above the
|
|
2202
|
+
* footer, and the plugin-config section owns the column width.
|
|
2203
|
+
*
|
|
2204
|
+
* Degraded/error/loading states keep the same card chrome (KD-U3): the
|
|
2205
|
+
* header always renders title+description+chevron, and the body carries the
|
|
2206
|
+
* config-channel notice or the load error. A card that cannot reach the
|
|
2207
|
+
* `fallbacks/get` gateway channel (`ready && !present`) keeps the USABLE
|
|
2208
|
+
* skeleton — the form stays writable and saves are attempted (KD-G5) — with
|
|
2209
|
+
* the `unavailable` notice ALWAYS visible (derived open — the header cannot
|
|
2210
|
+
* collapse it away), while a healthy card is collapsed until the user
|
|
2211
|
+
* expands it (AC-1, the documented divergence from upstream whose
|
|
2212
|
+
* unavailable card renders nothing). A hard load failure (`status ===
|
|
2213
|
+
* 'error'`) also forces the body open with an error notice and — when the
|
|
2214
|
+
* form is inert (`!writable`, i.e. the load never landed) — a Retry button;
|
|
2215
|
+
* a save failure keeps the editable form so the Save action itself is the
|
|
2216
|
+
* retry (the single `state.error` surface covers both, unlike the advisor's
|
|
2217
|
+
* separate apply-failure hints).
|
|
2218
|
+
*
|
|
2219
|
+
* The degraded derivation is latched in the card (the store stays untouched):
|
|
2220
|
+
* `present` only ever changes inside the store's `accept()`, so the settled
|
|
2221
|
+
* `ready` read is authoritative, and a card-local latch carries that value
|
|
2222
|
+
* through refresh/save windows (`loading`/`saving`) so the notice body can
|
|
2223
|
+
* never collapse mid-refresh (the advisor's latched `degraded` field,
|
|
2224
|
+
* implemented without a store change); on a first mount the latch is false,
|
|
2225
|
+
* so the healthy card starts (and stays) collapsed through its first load.
|
|
2226
|
+
*/
|
|
2227
|
+
/** Split scalars from the row editors (rootChain / role entities / role rules). */
|
|
2228
|
+
function scalarsOf(config) {
|
|
2229
|
+
return {
|
|
2230
|
+
enabled: config.enabled,
|
|
2231
|
+
triggerCodes: [...config.triggerCodes],
|
|
2232
|
+
cooldownMs: config.cooldownMs,
|
|
2233
|
+
revertPolicy: config.revertPolicy,
|
|
2234
|
+
maxSwitchesPerStep: config.maxSwitchesPerStep,
|
|
2235
|
+
alwaysModeRetryCap: config.alwaysModeRetryCap
|
|
2236
|
+
};
|
|
2237
|
+
}
|
|
2238
|
+
/**
|
|
2239
|
+
* Assemble the full config the row editors + scalars describe. The rebuilt
|
|
2240
|
+
* `roles.list` comes from the rows, with the schema-reserved
|
|
2241
|
+
* `prompt`/`permissions` merged back from the last accepted config by role
|
|
2242
|
+
* id (see {@link mergeRoleExtras}) so a save never silently drops them
|
|
2243
|
+
* (T2 reviewer minor #2).
|
|
2244
|
+
*/
|
|
2245
|
+
function assembleConfig(scalars, rootChainRows, roleRows, ruleRows, originalRoles) {
|
|
2246
|
+
const list = mergeRoleExtras(roleRows, originalRoles);
|
|
2247
|
+
return {
|
|
2248
|
+
enabled: scalars.enabled,
|
|
2249
|
+
triggerCodes: [...scalars.triggerCodes],
|
|
2250
|
+
rootChain: rowsToRootChain(rootChainRows),
|
|
2251
|
+
roles: {
|
|
2252
|
+
list,
|
|
2253
|
+
rules: rowsToRules(ruleRows)
|
|
2254
|
+
},
|
|
2255
|
+
cooldownMs: scalars.cooldownMs,
|
|
2256
|
+
revertPolicy: scalars.revertPolicy,
|
|
2257
|
+
maxSwitchesPerStep: scalars.maxSwitchesPerStep,
|
|
2258
|
+
alwaysModeRetryCap: scalars.alwaysModeRetryCap
|
|
2259
|
+
};
|
|
2260
|
+
}
|
|
2261
|
+
/**
|
|
2262
|
+
* Pre-save validation of the assembled draft (spec §8 / plan Task 3):
|
|
2263
|
+
* role id format/reserved word/duplicates, undeclared rule role references
|
|
2264
|
+
* (only reachable through the synthetic outside option — the dropdown
|
|
2265
|
+
* itself constrains normal edits), and illegal selector entries in
|
|
2266
|
+
* rootChain and role chains. Returns one localized message per violation;
|
|
2267
|
+
* a non-empty result blocks {@link save} — the draft is never written.
|
|
2268
|
+
* `label`/`description` are free text and never validated.
|
|
2269
|
+
*/
|
|
2270
|
+
function validateDraft(draft, t) {
|
|
2271
|
+
const errors = [];
|
|
2272
|
+
const declaredIds = /* @__PURE__ */ new Set();
|
|
2273
|
+
for (const role of draft.roles.list) {
|
|
2274
|
+
if (!ROLE_ID_PATTERN.test(role.id)) errors.push(t("validation.roleIdFormat", { id: role.id }));
|
|
2275
|
+
if (role.id === "inherit") errors.push(t("validation.roleIdReserved"));
|
|
2276
|
+
if (declaredIds.has(role.id)) errors.push(t("validation.roleIdDuplicate", { id: role.id }));
|
|
2277
|
+
declaredIds.add(role.id);
|
|
2278
|
+
for (const entry of role.chain ?? []) try {
|
|
2279
|
+
parseSelector(entry);
|
|
2280
|
+
} catch (error) {
|
|
2281
|
+
errors.push(t("validation.selector", {
|
|
2282
|
+
entry,
|
|
2283
|
+
message: error.message
|
|
2284
|
+
}));
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
for (const entry of draft.rootChain) try {
|
|
2288
|
+
parseSelector(entry);
|
|
2289
|
+
} catch (error) {
|
|
2290
|
+
errors.push(t("validation.selector", {
|
|
2291
|
+
entry,
|
|
2292
|
+
message: error.message
|
|
2293
|
+
}));
|
|
2294
|
+
}
|
|
2295
|
+
const validTargets = /* @__PURE__ */ new Set([...declaredIds, INHERIT_ROLE_ID]);
|
|
2296
|
+
for (const rule of draft.roles.rules) if (!validTargets.has(rule.role)) errors.push(t("validation.ruleRoleUndeclared", { role: rule.role }));
|
|
2297
|
+
return errors;
|
|
2298
|
+
}
|
|
2299
|
+
/**
|
|
2300
|
+
* The trimmed role ids that are validation failures (format / reserved word
|
|
2301
|
+
* / duplicate) — drives the inline red border after a blocked save attempt.
|
|
2302
|
+
* Derived once per render into a Set (qc3 F-3): a duplicate scan inside the
|
|
2303
|
+
* render loop would be O(N²) per row; here the whole derivation is O(N) and
|
|
2304
|
+
* each row's check is a single Set lookup. Selector errors stay on the
|
|
2305
|
+
* banner only (plan Task 3 inline-scope rule).
|
|
2306
|
+
*/
|
|
2307
|
+
function collectInvalidRoleIds(rows) {
|
|
2308
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2309
|
+
for (const row of rows) {
|
|
2310
|
+
const id = row.id.trim();
|
|
2311
|
+
counts.set(id, (counts.get(id) ?? 0) + 1);
|
|
2312
|
+
}
|
|
2313
|
+
const invalid = /* @__PURE__ */ new Set();
|
|
2314
|
+
for (const row of rows) {
|
|
2315
|
+
const id = row.id.trim();
|
|
2316
|
+
if (!ROLE_ID_PATTERN.test(id) || id === "inherit" || (counts.get(id) ?? 0) > 1) invalid.add(id);
|
|
2317
|
+
}
|
|
2318
|
+
return invalid;
|
|
2319
|
+
}
|
|
2320
|
+
/** Parse a number input, clamped to a non-negative integer. */
|
|
2321
|
+
function parseCount(raw) {
|
|
2322
|
+
const parsed = Number.parseInt(raw, 10);
|
|
2323
|
+
return Number.isNaN(parsed) ? 0 : Math.max(0, parsed);
|
|
2324
|
+
}
|
|
2325
|
+
/** The catalog faces the dropdowns classify against; undefined while unready. */
|
|
2326
|
+
function catalogOf(state) {
|
|
2327
|
+
return state.catalogStatus === "ready" ? {
|
|
2328
|
+
providers: state.providers,
|
|
2329
|
+
groups: state.groups
|
|
2330
|
+
} : void 0;
|
|
2331
|
+
}
|
|
2332
|
+
/**
|
|
2333
|
+
* Inline "!" info badge (T3): the detailed explanation rides a primitives
|
|
2334
|
+
* Tooltip bubble (side "right", ~300ms hover delay, immediate on keyboard
|
|
2335
|
+
* focus) while the short inline hint stays on the row. The badge is an
|
|
2336
|
+
* exposed, focusable image — the Models page credential-status pattern
|
|
2337
|
+
* (role="img" + aria-label) — so the accessible name is always available;
|
|
2338
|
+
* the tooltip is a progressive enhancement on top.
|
|
2339
|
+
*
|
|
2340
|
+
* `disabled` mirrors the read-only/loading suppression of the surrounding
|
|
2341
|
+
* controls: the bubble is suppressed, the badge drops out of the tab order
|
|
2342
|
+
* (and its `:disabled` style dims it).
|
|
2343
|
+
*
|
|
2344
|
+
* Placement contract (QC W-2 fix): the badge is always a **sibling** of the
|
|
2345
|
+
* label-text element — never nested inside a `<label>` or an
|
|
2346
|
+
* `aria-labelledby`-referenced node — so its aria-label can never leak into
|
|
2347
|
+
* a control/group accessible name. A click on the badge therefore has no
|
|
2348
|
+
* label-activation default action to cancel.
|
|
2349
|
+
*/
|
|
2350
|
+
function InfoHint({ label, disabled = false }) {
|
|
2351
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
|
|
2352
|
+
label,
|
|
2353
|
+
side: "right",
|
|
2354
|
+
delayMs: 300,
|
|
2355
|
+
disabled,
|
|
2356
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2357
|
+
className: disabled ? `${FallbacksCard_module_css_default.infoHint} ${FallbacksCard_module_css_default.infoHintDisabled}` : FallbacksCard_module_css_default.infoHint,
|
|
2358
|
+
role: "img",
|
|
2359
|
+
"aria-label": label,
|
|
2360
|
+
tabIndex: disabled ? -1 : 0,
|
|
2361
|
+
children: "!"
|
|
2362
|
+
})
|
|
2363
|
+
});
|
|
2364
|
+
}
|
|
2365
|
+
/**
|
|
2366
|
+
* One chain entry selector row: provider select + model select (cascade) +
|
|
2367
|
+
* wildcard checkbox (spec §2.5 D-3). The provider options are the catalog
|
|
2368
|
+
* providers **configured on the Models page** (`configuredProviders`, the
|
|
2369
|
+
* Models-page `configured` join) — unconfigured directory providers never
|
|
2370
|
+
* become offerable. Out-of-catalog values read back from the server render as
|
|
2371
|
+
* a synthetic option with the short "outside catalog" annotation and stay
|
|
2372
|
+
* selected — keeping them saves verbatim; picking a catalog option is an
|
|
2373
|
+
* intentional change. A directory provider that is not configured is offered
|
|
2374
|
+
* the same read-back treatment (short "not configured" annotation) so an
|
|
2375
|
+
* existing value is never hidden or dropped. New rows only offer configured
|
|
2376
|
+
* options.
|
|
2377
|
+
*/
|
|
2378
|
+
function ChainSelectorEditor({ selector, catalog, configuredProviders, disabled, t, onChange, onRemove }) {
|
|
2379
|
+
const providerRaw = selectionToRaw(selector.provider);
|
|
2380
|
+
const providerOutside = selector.provider?.kind === "outside";
|
|
2381
|
+
const providerUnconfigured = !providerOutside && providerRaw !== "" && (catalog?.providers.some((entry) => entry.provider === providerRaw) ?? false) && !configuredProviders.some((entry) => entry.provider === providerRaw);
|
|
2382
|
+
const modelRaw = selectionToRaw(selector.model);
|
|
2383
|
+
const modelOutside = selector.model?.kind === "outside";
|
|
2384
|
+
const group = catalog?.groups.find((entry) => entry.id === providerRaw);
|
|
2385
|
+
const groupMissing = providerRaw !== "" && !providerOutside && !selector.wildcard && group === void 0;
|
|
2386
|
+
const modelDisabled = disabled || selector.wildcard || providerRaw === "" || groupMissing || providerOutside && modelRaw === "";
|
|
2387
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2388
|
+
className: FallbacksCard_module_css_default.selectorRow,
|
|
2389
|
+
children: [
|
|
2390
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2391
|
+
className: FallbacksCard_module_css_default.ruleGrid,
|
|
2392
|
+
children: [
|
|
2393
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2394
|
+
className: FallbacksCard_module_css_default.ruleCell,
|
|
2395
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2396
|
+
className: FallbacksCard_module_css_default.ruleCellLabel,
|
|
2397
|
+
children: t("roles.rule.provider")
|
|
2398
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
2399
|
+
className: `${FallbacksCard_module_css_default.input} ${FallbacksCard_module_css_default.selectInput}`,
|
|
2400
|
+
value: providerRaw,
|
|
2401
|
+
disabled,
|
|
2402
|
+
onChange: (event) => {
|
|
2403
|
+
if (event.target.value === providerRaw) return;
|
|
2404
|
+
onChange({
|
|
2405
|
+
provider: classifyProvider(event.target.value, catalog),
|
|
2406
|
+
model: null
|
|
2407
|
+
});
|
|
2408
|
+
},
|
|
2409
|
+
children: [
|
|
2410
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
2411
|
+
value: "",
|
|
2412
|
+
children: t("chains.selector.providerPlaceholder")
|
|
2413
|
+
}),
|
|
2414
|
+
configuredProviders.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
2415
|
+
value: entry.provider,
|
|
2416
|
+
children: entry.displayName
|
|
2417
|
+
}, entry.provider)),
|
|
2418
|
+
providerUnconfigured && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
2419
|
+
value: providerRaw,
|
|
2420
|
+
children: `${providerRaw}${t("catalog.unconfigured.short")}`
|
|
2421
|
+
}),
|
|
2422
|
+
providerOutside && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
2423
|
+
value: providerRaw,
|
|
2424
|
+
children: `${providerRaw}${t("catalog.outside.short")}`
|
|
2425
|
+
})
|
|
2426
|
+
]
|
|
2427
|
+
})]
|
|
2428
|
+
}),
|
|
2429
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2430
|
+
className: FallbacksCard_module_css_default.ruleCell,
|
|
2431
|
+
children: [
|
|
2432
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2433
|
+
className: FallbacksCard_module_css_default.ruleCellLabel,
|
|
2434
|
+
children: t("roles.rule.model")
|
|
2435
|
+
}),
|
|
2436
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
2437
|
+
className: `${FallbacksCard_module_css_default.input} ${FallbacksCard_module_css_default.selectInput}`,
|
|
2438
|
+
value: selector.wildcard ? "" : modelRaw,
|
|
2439
|
+
disabled: modelDisabled,
|
|
2440
|
+
onChange: (event) => {
|
|
2441
|
+
onChange({ model: classifyModel(providerRaw, event.target.value, catalog) });
|
|
2442
|
+
},
|
|
2443
|
+
children: [
|
|
2444
|
+
modelRaw === "" && !providerOutside && !selector.wildcard && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
2445
|
+
value: "",
|
|
2446
|
+
children: t("chains.selector.modelPlaceholder")
|
|
2447
|
+
}),
|
|
2448
|
+
(group?.models ?? []).map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
2449
|
+
value: model.id,
|
|
2450
|
+
children: model.name
|
|
2451
|
+
}, model.id)),
|
|
2452
|
+
modelOutside && !selector.wildcard && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
2453
|
+
value: modelRaw,
|
|
2454
|
+
children: `${modelRaw}${t("catalog.outside.short")}`
|
|
2455
|
+
})
|
|
2456
|
+
]
|
|
2457
|
+
}),
|
|
2458
|
+
groupMissing && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2459
|
+
className: FallbacksCard_module_css_default.hint,
|
|
2460
|
+
children: t("chains.selector.noModels")
|
|
2461
|
+
})
|
|
2462
|
+
]
|
|
2463
|
+
}),
|
|
2464
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2465
|
+
className: `${FallbacksCard_module_css_default.ruleCell} ${FallbacksCard_module_css_default.wildcardCell}`,
|
|
2466
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2467
|
+
type: "checkbox",
|
|
2468
|
+
checked: selector.wildcard,
|
|
2469
|
+
disabled: disabled || providerRaw === "",
|
|
2470
|
+
onChange: (event) => {
|
|
2471
|
+
onChange({
|
|
2472
|
+
wildcard: event.target.checked,
|
|
2473
|
+
...event.target.checked ? { model: null } : {}
|
|
2474
|
+
});
|
|
2475
|
+
}
|
|
2476
|
+
}), t("chains.selector.wildcard")]
|
|
2477
|
+
})
|
|
2478
|
+
]
|
|
2479
|
+
}),
|
|
2480
|
+
(providerOutside || modelOutside) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2481
|
+
className: FallbacksCard_module_css_default.hint,
|
|
2482
|
+
children: [t("catalog.outside.hint"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InfoHint, {
|
|
2483
|
+
label: t("catalog.outside.tooltip"),
|
|
2484
|
+
disabled
|
|
2485
|
+
})]
|
|
2486
|
+
}),
|
|
2487
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2488
|
+
className: FallbacksCard_module_css_default.cardFoot,
|
|
2489
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2490
|
+
type: "button",
|
|
2491
|
+
className: `${FallbacksCard_module_css_default.iconButton} ${FallbacksCard_module_css_default.iconButtonDanger}`,
|
|
2492
|
+
"data-tip": t("chains.selector.remove"),
|
|
2493
|
+
"aria-label": t("chains.selector.remove"),
|
|
2494
|
+
onClick: onRemove,
|
|
2495
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconTrashOutline16, {})
|
|
2496
|
+
})
|
|
2497
|
+
})
|
|
2498
|
+
]
|
|
2499
|
+
});
|
|
2500
|
+
}
|
|
2501
|
+
/**
|
|
2502
|
+
* Render the Fallbacks settings card inside the plugin-config section,
|
|
2503
|
+
* replicating the upstream PluginCard chrome (KD-U1). The body carries the
|
|
2504
|
+
* existing form content unchanged plus the folded-in status block and the
|
|
2505
|
+
* footer actions (Discard / Reset / Save).
|
|
2506
|
+
* @param props - slot-delivered injected dependencies and the synthesized t seat.
|
|
2507
|
+
* @returns the card.
|
|
2508
|
+
*/
|
|
2509
|
+
function FallbacksCard({ controller, useSnapshot, t }) {
|
|
2510
|
+
const state = useSnapshot((snapshot) => snapshot);
|
|
2511
|
+
(0, react.useEffect)(() => {
|
|
2512
|
+
const snapshot = controller.store.getSnapshot();
|
|
2513
|
+
if (snapshot.status === "idle") controller.load();
|
|
2514
|
+
if (snapshot.catalogStatus === "idle") controller.loadCatalog();
|
|
2515
|
+
if (snapshot.switchesStatus === "idle") controller.loadSwitches();
|
|
2516
|
+
}, [controller]);
|
|
2517
|
+
const [scalars, setScalars] = (0, react.useState)(() => scalarsOf(defaultFallbacksConfig));
|
|
2518
|
+
const [rootChainRows, setRootChainRows] = (0, react.useState)(() => rootChainToRows(defaultFallbacksConfig.rootChain));
|
|
2519
|
+
const [roleRows, setRoleRows] = (0, react.useState)(() => rolesToRows(defaultFallbacksConfig.roles.list));
|
|
2520
|
+
const [ruleRows, setRuleRows] = (0, react.useState)(() => rulesToRows(defaultFallbacksConfig.roles.rules));
|
|
2521
|
+
const [validationErrors, setValidationErrors] = (0, react.useState)([]);
|
|
2522
|
+
const [validationAttempted, setValidationAttempted] = (0, react.useState)(false);
|
|
2523
|
+
const seededConfigKey = (0, react.useRef)(null);
|
|
2524
|
+
(0, react.useEffect)(() => {
|
|
2525
|
+
if (state.status !== "ready") return;
|
|
2526
|
+
const key = JSON.stringify(state.config);
|
|
2527
|
+
if (seededConfigKey.current === key) return;
|
|
2528
|
+
seededConfigKey.current = key;
|
|
2529
|
+
setScalars(scalarsOf(state.config));
|
|
2530
|
+
setRootChainRows(rootChainToRows(state.config.rootChain, catalogOf(state)));
|
|
2531
|
+
setRoleRows(rolesToRows(state.config.roles.list, catalogOf(state)));
|
|
2532
|
+
setRuleRows(rulesToRows(state.config.roles.rules, catalogOf(state)));
|
|
2533
|
+
}, [state.status, state.config]);
|
|
2534
|
+
const [confirmingReset, setConfirmingReset] = (0, react.useState)(false);
|
|
2535
|
+
const [resetting, setResetting] = (0, react.useState)(false);
|
|
2536
|
+
const updateScalars = (mutator) => {
|
|
2537
|
+
setScalars((prev) => {
|
|
2538
|
+
const next = {
|
|
2539
|
+
...prev,
|
|
2540
|
+
triggerCodes: [...prev.triggerCodes]
|
|
2541
|
+
};
|
|
2542
|
+
mutator(next);
|
|
2543
|
+
return next;
|
|
2544
|
+
});
|
|
2545
|
+
};
|
|
2546
|
+
const updateRootChainSelector = (selectorIndex, patch) => {
|
|
2547
|
+
setRootChainRows((rows) => rows.map((row, index) => index === 0 ? {
|
|
2548
|
+
...row,
|
|
2549
|
+
selectors: row.selectors.map((selector, sIndex) => sIndex === selectorIndex ? {
|
|
2550
|
+
...selector,
|
|
2551
|
+
...patch
|
|
2552
|
+
} : selector)
|
|
2553
|
+
} : row));
|
|
2554
|
+
};
|
|
2555
|
+
const addRootChainSelector = () => {
|
|
2556
|
+
setRootChainRows((rows) => rows.map((row, index) => index === 0 ? {
|
|
2557
|
+
...row,
|
|
2558
|
+
selectors: [...row.selectors, {
|
|
2559
|
+
wildcard: false,
|
|
2560
|
+
provider: null,
|
|
2561
|
+
model: null
|
|
2562
|
+
}]
|
|
2563
|
+
} : row));
|
|
2564
|
+
};
|
|
2565
|
+
const removeRootChainSelector = (selectorIndex) => {
|
|
2566
|
+
setRootChainRows((rows) => rows.map((row, index) => index === 0 ? {
|
|
2567
|
+
...row,
|
|
2568
|
+
selectors: row.selectors.filter((_, sIndex) => sIndex !== selectorIndex)
|
|
2569
|
+
} : row));
|
|
2570
|
+
};
|
|
2571
|
+
const updateRoleRow = (index, patch) => {
|
|
2572
|
+
setRoleRows((rows) => {
|
|
2573
|
+
const next = rows.map((row) => ({ ...row }));
|
|
2574
|
+
next[index] = {
|
|
2575
|
+
...next[index],
|
|
2576
|
+
...patch
|
|
2577
|
+
};
|
|
2578
|
+
return next;
|
|
2579
|
+
});
|
|
2580
|
+
};
|
|
2581
|
+
const updateRoleSelector = (roleIndex, selectorIndex, patch) => {
|
|
2582
|
+
setRoleRows((rows) => {
|
|
2583
|
+
const next = rows.map((row) => ({
|
|
2584
|
+
...row,
|
|
2585
|
+
selectors: row.selectors.map((selector) => ({ ...selector }))
|
|
2586
|
+
}));
|
|
2587
|
+
const selectors = next[roleIndex].selectors;
|
|
2588
|
+
selectors[selectorIndex] = {
|
|
2589
|
+
...selectors[selectorIndex],
|
|
2590
|
+
...patch
|
|
2591
|
+
};
|
|
2592
|
+
return next;
|
|
2593
|
+
});
|
|
2594
|
+
};
|
|
2595
|
+
const addRoleSelector = (roleIndex) => {
|
|
2596
|
+
setRoleRows((rows) => rows.map((row, index) => index === roleIndex ? {
|
|
2597
|
+
...row,
|
|
2598
|
+
selectors: [...row.selectors, {
|
|
2599
|
+
wildcard: false,
|
|
2600
|
+
provider: null,
|
|
2601
|
+
model: null
|
|
2602
|
+
}]
|
|
2603
|
+
} : row));
|
|
2604
|
+
};
|
|
2605
|
+
const removeRoleSelector = (roleIndex, selectorIndex) => {
|
|
2606
|
+
setRoleRows((rows) => rows.map((row, index) => index === roleIndex ? {
|
|
2607
|
+
...row,
|
|
2608
|
+
selectors: row.selectors.filter((_, sIndex) => sIndex !== selectorIndex)
|
|
2609
|
+
} : row));
|
|
2610
|
+
};
|
|
2611
|
+
const addRole = () => {
|
|
2612
|
+
setRoleRows((rows) => [...rows, {
|
|
2613
|
+
id: "",
|
|
2614
|
+
label: "",
|
|
2615
|
+
description: "",
|
|
2616
|
+
selectors: [],
|
|
2617
|
+
fallback: "inherit-root"
|
|
2618
|
+
}]);
|
|
2619
|
+
};
|
|
2620
|
+
const removeRole = (index) => {
|
|
2621
|
+
setRoleRows((rows) => rows.filter((_, rowIndex) => rowIndex !== index));
|
|
2622
|
+
};
|
|
2623
|
+
const updateRuleRow = (index, patch) => {
|
|
2624
|
+
setRuleRows((rows) => {
|
|
2625
|
+
const next = rows.map((row) => ({ ...row }));
|
|
2626
|
+
next[index] = {
|
|
2627
|
+
...next[index],
|
|
2628
|
+
...patch
|
|
2629
|
+
};
|
|
2630
|
+
return next;
|
|
2631
|
+
});
|
|
2632
|
+
};
|
|
2633
|
+
const draft = assembleConfig(scalars, rootChainRows, roleRows, ruleRows, state.config.roles.list);
|
|
2634
|
+
const hasEmptyRuleRows = ruleRows.some((row) => row.role === "");
|
|
2635
|
+
const dirty = JSON.stringify(draft) !== JSON.stringify(state.config) || hasEmptyRuleRows;
|
|
2636
|
+
const saving = state.status === "saving";
|
|
2637
|
+
const writable = state.writable;
|
|
2638
|
+
const unknownCodes = scalars.triggerCodes.filter((code) => !KNOWN_TRIGGER_CODES.includes(code));
|
|
2639
|
+
const roleOptions = ruleRoleOptions({ list: roleRows });
|
|
2640
|
+
const invalidRoleIds = validationAttempted ? collectInvalidRoleIds(roleRows) : null;
|
|
2641
|
+
const effectiveModel = deriveEffectiveModel(state.config, state.switches);
|
|
2642
|
+
const effectiveModelLine = effectiveModel.kind === "unavailable" ? t("status.effectiveModel.unavailable") : `${effectiveModel.provider}/${effectiveModel.model} · ${t("status.effectiveModel.note")}`;
|
|
2643
|
+
const latestSwitch = state.switches[0];
|
|
2644
|
+
let switchesLine;
|
|
2645
|
+
if (state.switchesStatus === "error") switchesLine = t("status.switches.error", { message: state.switchesError });
|
|
2646
|
+
else if (state.switchesStatus === "loading") switchesLine = t("loading");
|
|
2647
|
+
else if (latestSwitch === void 0) switchesLine = t("status.switches.empty");
|
|
2648
|
+
else {
|
|
2649
|
+
const reasonKey = SWITCH_REASON_KEYS[latestSwitch.reason];
|
|
2650
|
+
switchesLine = t("status.switches.compact", {
|
|
2651
|
+
count: String(state.switches.length),
|
|
2652
|
+
from: `${latestSwitch.from.provider}/${latestSwitch.from.model}`,
|
|
2653
|
+
to: `${latestSwitch.to.provider}/${latestSwitch.to.model}`,
|
|
2654
|
+
role: latestSwitch.role,
|
|
2655
|
+
reason: reasonKey === void 0 ? latestSwitch.reason : t(reasonKey)
|
|
2656
|
+
});
|
|
2657
|
+
}
|
|
2658
|
+
const catalogSeededEpoch = (0, react.useRef)(null);
|
|
2659
|
+
(0, react.useEffect)(() => {
|
|
2660
|
+
if (state.catalogStatus !== "ready") return;
|
|
2661
|
+
if (catalogSeededEpoch.current === state.catalogEpoch) return;
|
|
2662
|
+
if (dirty) return;
|
|
2663
|
+
catalogSeededEpoch.current = state.catalogEpoch;
|
|
2664
|
+
setRootChainRows(rootChainToRows(state.config.rootChain, catalogOf(state)));
|
|
2665
|
+
setRoleRows(rolesToRows(state.config.roles.list, catalogOf(state)));
|
|
2666
|
+
setRuleRows(rulesToRows(state.config.roles.rules, catalogOf(state)));
|
|
2667
|
+
}, [
|
|
2668
|
+
state.catalogStatus,
|
|
2669
|
+
state.catalogEpoch,
|
|
2670
|
+
state.config,
|
|
2671
|
+
dirty
|
|
2672
|
+
]);
|
|
2673
|
+
const save = () => {
|
|
2674
|
+
const errors = validateDraft(draft, t);
|
|
2675
|
+
if (hasEmptyRuleRows) errors.push(t("validation.ruleRoleRequired"));
|
|
2676
|
+
if (errors.length > 0) {
|
|
2677
|
+
setValidationErrors(errors);
|
|
2678
|
+
setValidationAttempted(true);
|
|
2679
|
+
return;
|
|
2680
|
+
}
|
|
2681
|
+
setValidationErrors([]);
|
|
2682
|
+
setValidationAttempted(false);
|
|
2683
|
+
controller.save(draft);
|
|
2684
|
+
};
|
|
2685
|
+
const discard = () => {
|
|
2686
|
+
setScalars(scalarsOf(state.config));
|
|
2687
|
+
setRootChainRows(rootChainToRows(state.config.rootChain, catalogOf(state)));
|
|
2688
|
+
setRoleRows(rolesToRows(state.config.roles.list, catalogOf(state)));
|
|
2689
|
+
setRuleRows(rulesToRows(state.config.roles.rules, catalogOf(state)));
|
|
2690
|
+
setValidationErrors([]);
|
|
2691
|
+
setValidationAttempted(false);
|
|
2692
|
+
};
|
|
2693
|
+
(0, react.useEffect)(() => {
|
|
2694
|
+
if (!validationAttempted) return;
|
|
2695
|
+
if (validateDraft(draft, t).length === 0 && !ruleRows.some((row) => row.role === "")) {
|
|
2696
|
+
setValidationErrors([]);
|
|
2697
|
+
setValidationAttempted(false);
|
|
2698
|
+
}
|
|
2699
|
+
}, [
|
|
2700
|
+
validationAttempted,
|
|
2701
|
+
draft,
|
|
2702
|
+
ruleRows,
|
|
2703
|
+
t
|
|
2704
|
+
]);
|
|
2705
|
+
const confirmReset = () => {
|
|
2706
|
+
setResetting(true);
|
|
2707
|
+
controller.resetToDefaults().finally(() => {
|
|
2708
|
+
setResetting(false);
|
|
2709
|
+
setConfirmingReset(false);
|
|
2710
|
+
});
|
|
2711
|
+
};
|
|
2712
|
+
const [userOpen, setUserOpen] = (0, react.useState)(false);
|
|
2713
|
+
const degradedLatch = (0, react.useRef)(false);
|
|
2714
|
+
const errorLatch = (0, react.useRef)(false);
|
|
2715
|
+
if (state.status === "ready") {
|
|
2716
|
+
degradedLatch.current = !state.present;
|
|
2717
|
+
errorLatch.current = false;
|
|
2718
|
+
} else if (state.status === "error") errorLatch.current = true;
|
|
2719
|
+
const degraded = state.status === "ready" ? !state.present : degradedLatch.current;
|
|
2720
|
+
const open = userOpen || errorLatch.current || degraded;
|
|
2721
|
+
const title = t("title");
|
|
2722
|
+
const header = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2723
|
+
type: "button",
|
|
2724
|
+
className: FallbacksCard_module_css_default.header,
|
|
2725
|
+
"aria-expanded": open,
|
|
2726
|
+
"aria-label": `${t(open ? "collapse" : "expand")}: ${title}`,
|
|
2727
|
+
onClick: () => {
|
|
2728
|
+
if (!degraded && state.status !== "error") setUserOpen(!userOpen);
|
|
2729
|
+
},
|
|
2730
|
+
children: [
|
|
2731
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2732
|
+
className: FallbacksCard_module_css_default.headText,
|
|
2733
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2734
|
+
className: FallbacksCard_module_css_default.name,
|
|
2735
|
+
children: title
|
|
2736
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2737
|
+
className: FallbacksCard_module_css_default.description,
|
|
2738
|
+
children: t("intro")
|
|
2739
|
+
})]
|
|
2740
|
+
}),
|
|
2741
|
+
dirty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2742
|
+
className: FallbacksCard_module_css_default.pending,
|
|
2743
|
+
children: t("unsaved")
|
|
2744
|
+
}) : null,
|
|
2745
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, { className: open ? `${FallbacksCard_module_css_default.chevron} ${FallbacksCard_module_css_default.chevronOpen}` : FallbacksCard_module_css_default.chevron })
|
|
2746
|
+
]
|
|
2747
|
+
});
|
|
2748
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
2749
|
+
className: open ? `${FallbacksCard_module_css_default.card} ${FallbacksCard_module_css_default.cardOpen}` : FallbacksCard_module_css_default.card,
|
|
2750
|
+
children: [
|
|
2751
|
+
header,
|
|
2752
|
+
open && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2753
|
+
className: FallbacksCard_module_css_default.body,
|
|
2754
|
+
children: [
|
|
2755
|
+
state.legacyKeys.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2756
|
+
className: FallbacksCard_module_css_default.legacyNotice,
|
|
2757
|
+
role: "status",
|
|
2758
|
+
children: t("legacy.banner", { keys: state.legacyKeys.join(", ") })
|
|
2759
|
+
}),
|
|
2760
|
+
state.status === "error" && state.error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2761
|
+
className: FallbacksCard_module_css_default.noticeRow,
|
|
2762
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2763
|
+
className: FallbacksCard_module_css_default.error,
|
|
2764
|
+
role: "alert",
|
|
2765
|
+
children: t("error.generic", { message: state.error })
|
|
2766
|
+
}), !state.writable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
2767
|
+
variant: "outline",
|
|
2768
|
+
size: "sm",
|
|
2769
|
+
onClick: () => {
|
|
2770
|
+
controller.load();
|
|
2771
|
+
},
|
|
2772
|
+
children: t("retry")
|
|
2773
|
+
})]
|
|
2774
|
+
}),
|
|
2775
|
+
validationErrors.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2776
|
+
className: FallbacksCard_module_css_default.error,
|
|
2777
|
+
role: "alert",
|
|
2778
|
+
children: `${t("validation.blocked")}${validationErrors.join("; ")}`
|
|
2779
|
+
}),
|
|
2780
|
+
degraded && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2781
|
+
className: FallbacksCard_module_css_default.notice,
|
|
2782
|
+
role: "status",
|
|
2783
|
+
children: t("unavailable")
|
|
2784
|
+
}),
|
|
2785
|
+
state.status === "ready" && !state.writable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2786
|
+
className: FallbacksCard_module_css_default.readOnly,
|
|
2787
|
+
role: "status",
|
|
2788
|
+
children: t("readOnly")
|
|
2789
|
+
}),
|
|
2790
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2791
|
+
className: FallbacksCard_module_css_default.form,
|
|
2792
|
+
children: [
|
|
2793
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2794
|
+
className: FallbacksCard_module_css_default.checkboxRow,
|
|
2795
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2796
|
+
className: FallbacksCard_module_css_default.checkLabel,
|
|
2797
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2798
|
+
className: FallbacksCard_module_css_default.checkLabelTitle,
|
|
2799
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
2800
|
+
htmlFor: "fallbacks-enabled",
|
|
2801
|
+
children: t("enabled.label")
|
|
2802
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InfoHint, {
|
|
2803
|
+
label: t("enabled.tooltip"),
|
|
2804
|
+
disabled: !writable
|
|
2805
|
+
})]
|
|
2806
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2807
|
+
className: FallbacksCard_module_css_default.checkLabelDesc,
|
|
2808
|
+
children: t("enabled.hint")
|
|
2809
|
+
})]
|
|
2810
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2811
|
+
id: "fallbacks-enabled",
|
|
2812
|
+
type: "checkbox",
|
|
2813
|
+
className: FallbacksCard_module_css_default.checkbox,
|
|
2814
|
+
checked: scalars.enabled,
|
|
2815
|
+
disabled: !writable,
|
|
2816
|
+
onChange: (event) => {
|
|
2817
|
+
updateScalars((draft) => {
|
|
2818
|
+
draft.enabled = event.target.checked;
|
|
2819
|
+
});
|
|
2820
|
+
}
|
|
2821
|
+
})]
|
|
2822
|
+
}),
|
|
2823
|
+
!scalars.enabled && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2824
|
+
className: FallbacksCard_module_css_default.offNotice,
|
|
2825
|
+
children: t("enabled.off")
|
|
2826
|
+
}),
|
|
2827
|
+
scalars.enabled && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("fieldset", {
|
|
2828
|
+
className: FallbacksCard_module_css_default.fieldset,
|
|
2829
|
+
disabled: !writable,
|
|
2830
|
+
children: [
|
|
2831
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2832
|
+
className: FallbacksCard_module_css_default.field,
|
|
2833
|
+
role: "group",
|
|
2834
|
+
"aria-labelledby": "fallbacks-trigger-codes",
|
|
2835
|
+
children: [
|
|
2836
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2837
|
+
className: FallbacksCard_module_css_default.fieldLabel,
|
|
2838
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2839
|
+
id: "fallbacks-trigger-codes",
|
|
2840
|
+
children: t("triggerCodes.label")
|
|
2841
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InfoHint, {
|
|
2842
|
+
label: t("triggerCodes.tooltip"),
|
|
2843
|
+
disabled: !writable
|
|
2844
|
+
})]
|
|
2845
|
+
}),
|
|
2846
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2847
|
+
className: FallbacksCard_module_css_default.hint,
|
|
2848
|
+
children: t("triggerCodes.hint")
|
|
2849
|
+
}),
|
|
2850
|
+
KNOWN_TRIGGER_CODES.map((code) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2851
|
+
className: FallbacksCard_module_css_default.optionRow,
|
|
2852
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2853
|
+
type: "checkbox",
|
|
2854
|
+
checked: scalars.triggerCodes.includes(code),
|
|
2855
|
+
onChange: (event) => {
|
|
2856
|
+
updateScalars((draft) => {
|
|
2857
|
+
draft.triggerCodes = withTriggerCode(draft.triggerCodes, code, event.target.checked);
|
|
2858
|
+
});
|
|
2859
|
+
}
|
|
2860
|
+
}), t(TRIGGER_CODE_LABELS[code])]
|
|
2861
|
+
}, code)),
|
|
2862
|
+
unknownCodes.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2863
|
+
className: FallbacksCard_module_css_default.hint,
|
|
2864
|
+
children: t("triggerCodes.extra", { codes: unknownCodes.join(", ") })
|
|
2865
|
+
})
|
|
2866
|
+
]
|
|
2867
|
+
}),
|
|
2868
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2869
|
+
className: FallbacksCard_module_css_default.field,
|
|
2870
|
+
role: "group",
|
|
2871
|
+
"aria-labelledby": "fallbacks-revert-policy",
|
|
2872
|
+
children: [
|
|
2873
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2874
|
+
className: FallbacksCard_module_css_default.fieldLabel,
|
|
2875
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2876
|
+
id: "fallbacks-revert-policy",
|
|
2877
|
+
children: t("revertPolicy.label")
|
|
2878
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InfoHint, {
|
|
2879
|
+
label: t("revertPolicy.tooltip"),
|
|
2880
|
+
disabled: !writable
|
|
2881
|
+
})]
|
|
2882
|
+
}),
|
|
2883
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2884
|
+
className: FallbacksCard_module_css_default.hint,
|
|
2885
|
+
children: t("revertPolicy.hint")
|
|
2886
|
+
}),
|
|
2887
|
+
["cooldown-expiry", "never"].map((policy) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2888
|
+
className: FallbacksCard_module_css_default.optionRow,
|
|
2889
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2890
|
+
type: "radio",
|
|
2891
|
+
name: "fallbacks-revert-policy",
|
|
2892
|
+
checked: scalars.revertPolicy === policy,
|
|
2893
|
+
onChange: () => {
|
|
2894
|
+
updateScalars((draft) => {
|
|
2895
|
+
draft.revertPolicy = policy;
|
|
2896
|
+
});
|
|
2897
|
+
}
|
|
2898
|
+
}), t(`revertPolicy.${policy}`)]
|
|
2899
|
+
}, policy))
|
|
2900
|
+
]
|
|
2901
|
+
}),
|
|
2902
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2903
|
+
className: FallbacksCard_module_css_default.numberFields,
|
|
2904
|
+
children: [
|
|
2905
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2906
|
+
className: FallbacksCard_module_css_default.field,
|
|
2907
|
+
children: [
|
|
2908
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2909
|
+
className: FallbacksCard_module_css_default.fieldLabel,
|
|
2910
|
+
children: [
|
|
2911
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
2912
|
+
htmlFor: "fallbacks-cooldown-ms",
|
|
2913
|
+
children: t("cooldownMs.label")
|
|
2914
|
+
}),
|
|
2915
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(InfoHint, {
|
|
2916
|
+
label: t("cooldownMs.tooltip"),
|
|
2917
|
+
disabled: !writable
|
|
2918
|
+
}),
|
|
2919
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2920
|
+
className: FallbacksCard_module_css_default.defaultNote,
|
|
2921
|
+
children: [
|
|
2922
|
+
t("defaults.prefix"),
|
|
2923
|
+
": ",
|
|
2924
|
+
state.config.cooldownMs
|
|
2925
|
+
]
|
|
2926
|
+
})
|
|
2927
|
+
]
|
|
2928
|
+
}),
|
|
2929
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2930
|
+
id: "fallbacks-cooldown-ms",
|
|
2931
|
+
className: FallbacksCard_module_css_default.input,
|
|
2932
|
+
type: "number",
|
|
2933
|
+
min: 0,
|
|
2934
|
+
value: String(scalars.cooldownMs),
|
|
2935
|
+
disabled: !writable,
|
|
2936
|
+
onChange: (event) => {
|
|
2937
|
+
updateScalars((draft) => {
|
|
2938
|
+
draft.cooldownMs = parseCount(event.target.value);
|
|
2939
|
+
});
|
|
2940
|
+
}
|
|
2941
|
+
}),
|
|
2942
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2943
|
+
className: FallbacksCard_module_css_default.hint,
|
|
2944
|
+
children: t("cooldownMs.hint")
|
|
2945
|
+
})
|
|
2946
|
+
]
|
|
2947
|
+
}),
|
|
2948
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2949
|
+
className: FallbacksCard_module_css_default.field,
|
|
2950
|
+
children: [
|
|
2951
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2952
|
+
className: FallbacksCard_module_css_default.fieldLabel,
|
|
2953
|
+
children: [
|
|
2954
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
2955
|
+
htmlFor: "fallbacks-max-switches",
|
|
2956
|
+
children: t("maxSwitchesPerStep.label")
|
|
2957
|
+
}),
|
|
2958
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(InfoHint, {
|
|
2959
|
+
label: t("maxSwitchesPerStep.tooltip"),
|
|
2960
|
+
disabled: !writable
|
|
2961
|
+
}),
|
|
2962
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2963
|
+
className: FallbacksCard_module_css_default.defaultNote,
|
|
2964
|
+
children: [
|
|
2965
|
+
t("defaults.prefix"),
|
|
2966
|
+
": ",
|
|
2967
|
+
state.config.maxSwitchesPerStep
|
|
2968
|
+
]
|
|
2969
|
+
})
|
|
2970
|
+
]
|
|
2971
|
+
}),
|
|
2972
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2973
|
+
id: "fallbacks-max-switches",
|
|
2974
|
+
className: FallbacksCard_module_css_default.input,
|
|
2975
|
+
type: "number",
|
|
2976
|
+
min: 0,
|
|
2977
|
+
value: String(scalars.maxSwitchesPerStep),
|
|
2978
|
+
disabled: !writable,
|
|
2979
|
+
onChange: (event) => {
|
|
2980
|
+
updateScalars((draft) => {
|
|
2981
|
+
draft.maxSwitchesPerStep = parseCount(event.target.value);
|
|
2982
|
+
});
|
|
2983
|
+
}
|
|
2984
|
+
}),
|
|
2985
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2986
|
+
className: FallbacksCard_module_css_default.hint,
|
|
2987
|
+
children: t("maxSwitchesPerStep.hint")
|
|
2988
|
+
})
|
|
2989
|
+
]
|
|
2990
|
+
}),
|
|
2991
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2992
|
+
className: FallbacksCard_module_css_default.field,
|
|
2993
|
+
children: [
|
|
2994
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2995
|
+
className: FallbacksCard_module_css_default.fieldLabel,
|
|
2996
|
+
children: [
|
|
2997
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
2998
|
+
htmlFor: "fallbacks-always-cap",
|
|
2999
|
+
children: t("alwaysModeRetryCap.label")
|
|
3000
|
+
}),
|
|
3001
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(InfoHint, {
|
|
3002
|
+
label: t("alwaysModeRetryCap.tooltip"),
|
|
3003
|
+
disabled: !writable
|
|
3004
|
+
}),
|
|
3005
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
3006
|
+
className: FallbacksCard_module_css_default.defaultNote,
|
|
3007
|
+
children: [
|
|
3008
|
+
t("defaults.prefix"),
|
|
3009
|
+
": ",
|
|
3010
|
+
state.config.alwaysModeRetryCap
|
|
3011
|
+
]
|
|
3012
|
+
})
|
|
3013
|
+
]
|
|
3014
|
+
}),
|
|
3015
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
3016
|
+
id: "fallbacks-always-cap",
|
|
3017
|
+
className: FallbacksCard_module_css_default.input,
|
|
3018
|
+
type: "number",
|
|
3019
|
+
min: 0,
|
|
3020
|
+
value: String(scalars.alwaysModeRetryCap),
|
|
3021
|
+
disabled: !writable,
|
|
3022
|
+
onChange: (event) => {
|
|
3023
|
+
updateScalars((draft) => {
|
|
3024
|
+
draft.alwaysModeRetryCap = parseCount(event.target.value);
|
|
3025
|
+
});
|
|
3026
|
+
}
|
|
3027
|
+
}),
|
|
3028
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3029
|
+
className: FallbacksCard_module_css_default.hint,
|
|
3030
|
+
children: t("alwaysModeRetryCap.hint")
|
|
3031
|
+
})
|
|
3032
|
+
]
|
|
3033
|
+
})
|
|
3034
|
+
]
|
|
3035
|
+
}),
|
|
3036
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3037
|
+
className: FallbacksCard_module_css_default.field,
|
|
3038
|
+
role: "group",
|
|
3039
|
+
"aria-labelledby": "fallbacks-root-chain",
|
|
3040
|
+
children: [
|
|
3041
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
3042
|
+
className: FallbacksCard_module_css_default.fieldLabel,
|
|
3043
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3044
|
+
id: "fallbacks-root-chain",
|
|
3045
|
+
children: t("rootChain.label")
|
|
3046
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InfoHint, {
|
|
3047
|
+
label: t("rootChain.tooltip"),
|
|
3048
|
+
disabled: !writable
|
|
3049
|
+
})]
|
|
3050
|
+
}),
|
|
3051
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3052
|
+
className: FallbacksCard_module_css_default.hint,
|
|
3053
|
+
children: t("rootChain.hint")
|
|
3054
|
+
}),
|
|
3055
|
+
state.catalogStatus === "error" && state.catalogError !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3056
|
+
className: FallbacksCard_module_css_default.hint,
|
|
3057
|
+
children: t("catalog.error", { message: state.catalogError })
|
|
3058
|
+
}),
|
|
3059
|
+
state.catalogStatus === "ready" && state.catalogError !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3060
|
+
className: FallbacksCard_module_css_default.hint,
|
|
3061
|
+
children: t("catalog.partial", { message: state.catalogError })
|
|
3062
|
+
}),
|
|
3063
|
+
state.catalogStatus === "ready" && (state.groups.length === 0 || state.configuredProviders.length === 0) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3064
|
+
className: FallbacksCard_module_css_default.hint,
|
|
3065
|
+
children: t("catalog.empty")
|
|
3066
|
+
}),
|
|
3067
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3068
|
+
className: FallbacksCard_module_css_default.list,
|
|
3069
|
+
children: rootChainRows.map((row, rowIndex) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3070
|
+
className: FallbacksCard_module_css_default.editorCard,
|
|
3071
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3072
|
+
className: FallbacksCard_module_css_default.chainSelectors,
|
|
3073
|
+
children: row.selectors.map((selector, selectorIndex) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChainSelectorEditor, {
|
|
3074
|
+
selector,
|
|
3075
|
+
catalog: catalogOf(state),
|
|
3076
|
+
configuredProviders: state.configuredProviders,
|
|
3077
|
+
disabled: !writable,
|
|
3078
|
+
t,
|
|
3079
|
+
onChange: (patch) => {
|
|
3080
|
+
updateRootChainSelector(selectorIndex, patch);
|
|
3081
|
+
},
|
|
3082
|
+
onRemove: () => {
|
|
3083
|
+
removeRootChainSelector(selectorIndex);
|
|
3084
|
+
}
|
|
3085
|
+
}, selectorIndex))
|
|
3086
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
3087
|
+
variant: "outline",
|
|
3088
|
+
size: "sm",
|
|
3089
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconPlusOutline16, { size: 14 }),
|
|
3090
|
+
className: FallbacksCard_module_css_default.addButton,
|
|
3091
|
+
onClick: addRootChainSelector,
|
|
3092
|
+
children: t("rootChain.selector.add")
|
|
3093
|
+
})]
|
|
3094
|
+
}, rowIndex))
|
|
3095
|
+
})
|
|
3096
|
+
]
|
|
3097
|
+
}),
|
|
3098
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3099
|
+
className: FallbacksCard_module_css_default.field,
|
|
3100
|
+
role: "group",
|
|
3101
|
+
"aria-labelledby": "fallbacks-roles-list",
|
|
3102
|
+
children: [
|
|
3103
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
3104
|
+
className: FallbacksCard_module_css_default.fieldLabel,
|
|
3105
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3106
|
+
id: "fallbacks-roles-list",
|
|
3107
|
+
children: t("roles.list.label")
|
|
3108
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InfoHint, {
|
|
3109
|
+
label: t("roles.list.tooltip"),
|
|
3110
|
+
disabled: !writable
|
|
3111
|
+
})]
|
|
3112
|
+
}),
|
|
3113
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3114
|
+
className: FallbacksCard_module_css_default.hint,
|
|
3115
|
+
children: t("roles.list.hint")
|
|
3116
|
+
}),
|
|
3117
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3118
|
+
className: FallbacksCard_module_css_default.list,
|
|
3119
|
+
children: roleRows.map((row, index) => {
|
|
3120
|
+
const invalid = invalidRoleIds?.has(row.id.trim()) ?? false;
|
|
3121
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3122
|
+
className: FallbacksCard_module_css_default.editorCard,
|
|
3123
|
+
children: [
|
|
3124
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3125
|
+
className: FallbacksCard_module_css_default.ruleGrid,
|
|
3126
|
+
children: [
|
|
3127
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3128
|
+
className: FallbacksCard_module_css_default.ruleCell,
|
|
3129
|
+
children: [
|
|
3130
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3131
|
+
className: FallbacksCard_module_css_default.ruleCellLabel,
|
|
3132
|
+
children: t("roles.id")
|
|
3133
|
+
}),
|
|
3134
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
3135
|
+
className: `${FallbacksCard_module_css_default.input} ${invalid ? FallbacksCard_module_css_default.inputInvalid : ""}`,
|
|
3136
|
+
value: row.id,
|
|
3137
|
+
placeholder: t("roles.idPlaceholder"),
|
|
3138
|
+
"aria-label": t("roles.id"),
|
|
3139
|
+
"aria-invalid": invalid ? true : void 0,
|
|
3140
|
+
disabled: !writable,
|
|
3141
|
+
onChange: (event) => {
|
|
3142
|
+
updateRoleRow(index, { id: event.target.value });
|
|
3143
|
+
}
|
|
3144
|
+
}),
|
|
3145
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3146
|
+
className: FallbacksCard_module_css_default.hint,
|
|
3147
|
+
children: t("roles.id.hint")
|
|
3148
|
+
})
|
|
3149
|
+
]
|
|
3150
|
+
}),
|
|
3151
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3152
|
+
className: FallbacksCard_module_css_default.ruleCell,
|
|
3153
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3154
|
+
className: FallbacksCard_module_css_default.ruleCellLabel,
|
|
3155
|
+
children: t("roles.label")
|
|
3156
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
3157
|
+
className: FallbacksCard_module_css_default.input,
|
|
3158
|
+
value: row.label,
|
|
3159
|
+
"aria-label": t("roles.label"),
|
|
3160
|
+
disabled: !writable,
|
|
3161
|
+
onChange: (event) => {
|
|
3162
|
+
updateRoleRow(index, { label: event.target.value });
|
|
3163
|
+
}
|
|
3164
|
+
})]
|
|
3165
|
+
}),
|
|
3166
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3167
|
+
className: FallbacksCard_module_css_default.ruleCell,
|
|
3168
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3169
|
+
className: FallbacksCard_module_css_default.ruleCellLabel,
|
|
3170
|
+
children: t("roles.description")
|
|
3171
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
3172
|
+
className: FallbacksCard_module_css_default.input,
|
|
3173
|
+
value: row.description,
|
|
3174
|
+
"aria-label": t("roles.description"),
|
|
3175
|
+
disabled: !writable,
|
|
3176
|
+
onChange: (event) => {
|
|
3177
|
+
updateRoleRow(index, { description: event.target.value });
|
|
3178
|
+
}
|
|
3179
|
+
})]
|
|
3180
|
+
})
|
|
3181
|
+
]
|
|
3182
|
+
}),
|
|
3183
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3184
|
+
className: FallbacksCard_module_css_default.chainSelectors,
|
|
3185
|
+
children: row.selectors.map((selector, selectorIndex) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChainSelectorEditor, {
|
|
3186
|
+
selector,
|
|
3187
|
+
catalog: catalogOf(state),
|
|
3188
|
+
configuredProviders: state.configuredProviders,
|
|
3189
|
+
disabled: !writable,
|
|
3190
|
+
t,
|
|
3191
|
+
onChange: (patch) => {
|
|
3192
|
+
updateRoleSelector(index, selectorIndex, patch);
|
|
3193
|
+
},
|
|
3194
|
+
onRemove: () => {
|
|
3195
|
+
removeRoleSelector(index, selectorIndex);
|
|
3196
|
+
}
|
|
3197
|
+
}, selectorIndex))
|
|
3198
|
+
}),
|
|
3199
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3200
|
+
className: FallbacksCard_module_css_default.ruleGrid,
|
|
3201
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3202
|
+
className: FallbacksCard_module_css_default.ruleCell,
|
|
3203
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3204
|
+
className: FallbacksCard_module_css_default.ruleCellLabel,
|
|
3205
|
+
children: t("roles.fallback")
|
|
3206
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
3207
|
+
className: `${FallbacksCard_module_css_default.input} ${FallbacksCard_module_css_default.selectInput}`,
|
|
3208
|
+
value: row.fallback,
|
|
3209
|
+
"aria-label": t("roles.fallback"),
|
|
3210
|
+
disabled: !writable,
|
|
3211
|
+
onChange: (event) => {
|
|
3212
|
+
updateRoleRow(index, { fallback: event.target.value });
|
|
3213
|
+
},
|
|
3214
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
3215
|
+
value: "inherit-root",
|
|
3216
|
+
children: t("roles.fallback.inherit-root")
|
|
3217
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
3218
|
+
value: "none",
|
|
3219
|
+
children: t("roles.fallback.none")
|
|
3220
|
+
})]
|
|
3221
|
+
})]
|
|
3222
|
+
})
|
|
3223
|
+
}),
|
|
3224
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
3225
|
+
variant: "outline",
|
|
3226
|
+
size: "sm",
|
|
3227
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconPlusOutline16, { size: 14 }),
|
|
3228
|
+
className: FallbacksCard_module_css_default.addButton,
|
|
3229
|
+
onClick: () => {
|
|
3230
|
+
addRoleSelector(index);
|
|
3231
|
+
},
|
|
3232
|
+
children: t("roles.selector.add")
|
|
3233
|
+
}),
|
|
3234
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3235
|
+
className: FallbacksCard_module_css_default.cardFoot,
|
|
3236
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3237
|
+
type: "button",
|
|
3238
|
+
className: `${FallbacksCard_module_css_default.iconButton} ${FallbacksCard_module_css_default.iconButtonDanger}`,
|
|
3239
|
+
"data-tip": t("roles.remove"),
|
|
3240
|
+
"aria-label": t("roles.remove"),
|
|
3241
|
+
onClick: () => {
|
|
3242
|
+
removeRole(index);
|
|
3243
|
+
},
|
|
3244
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconTrashOutline16, {})
|
|
3245
|
+
})
|
|
3246
|
+
})
|
|
3247
|
+
]
|
|
3248
|
+
}, index);
|
|
3249
|
+
})
|
|
3250
|
+
}),
|
|
3251
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
3252
|
+
variant: "outline",
|
|
3253
|
+
size: "sm",
|
|
3254
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconPlusOutline16, { size: 14 }),
|
|
3255
|
+
className: FallbacksCard_module_css_default.addButton,
|
|
3256
|
+
onClick: addRole,
|
|
3257
|
+
children: t("roles.add")
|
|
3258
|
+
})
|
|
3259
|
+
]
|
|
3260
|
+
}),
|
|
3261
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3262
|
+
className: FallbacksCard_module_css_default.field,
|
|
3263
|
+
role: "group",
|
|
3264
|
+
"aria-labelledby": "fallbacks-roles-rules",
|
|
3265
|
+
children: [
|
|
3266
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
3267
|
+
className: FallbacksCard_module_css_default.fieldLabel,
|
|
3268
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3269
|
+
id: "fallbacks-roles-rules",
|
|
3270
|
+
children: t("roles.rules")
|
|
3271
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InfoHint, {
|
|
3272
|
+
label: t("roles.rules.tooltip"),
|
|
3273
|
+
disabled: !writable
|
|
3274
|
+
})]
|
|
3275
|
+
}),
|
|
3276
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3277
|
+
className: FallbacksCard_module_css_default.hint,
|
|
3278
|
+
children: t("roles.rules.hint")
|
|
3279
|
+
}),
|
|
3280
|
+
state.catalogStatus === "error" && state.catalogError !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3281
|
+
className: FallbacksCard_module_css_default.hint,
|
|
3282
|
+
children: t("catalog.error", { message: state.catalogError })
|
|
3283
|
+
}),
|
|
3284
|
+
state.catalogStatus === "ready" && state.catalogError !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3285
|
+
className: FallbacksCard_module_css_default.hint,
|
|
3286
|
+
children: t("catalog.partial", { message: state.catalogError })
|
|
3287
|
+
}),
|
|
3288
|
+
state.catalogStatus === "ready" && (state.groups.length === 0 || state.configuredProviders.length === 0) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3289
|
+
className: FallbacksCard_module_css_default.hint,
|
|
3290
|
+
children: t("catalog.empty")
|
|
3291
|
+
}),
|
|
3292
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3293
|
+
className: FallbacksCard_module_css_default.list,
|
|
3294
|
+
children: ruleRows.map((row, index) => {
|
|
3295
|
+
const catalog = catalogOf(state);
|
|
3296
|
+
const providerRaw = selectionToRaw(row.provider);
|
|
3297
|
+
const group = catalog?.groups.find((entry) => entry.id === providerRaw);
|
|
3298
|
+
const providerOutside = row.provider?.kind === "outside";
|
|
3299
|
+
const providerUnconfigured = !providerOutside && providerRaw !== "" && (catalog?.providers.some((entry) => entry.provider === providerRaw) ?? false) && !state.configuredProviders.some((entry) => entry.provider === providerRaw);
|
|
3300
|
+
const modelOutside = row.model?.kind === "outside";
|
|
3301
|
+
const roleOutside = row.role !== "" && !roleOptions.includes(row.role);
|
|
3302
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3303
|
+
className: FallbacksCard_module_css_default.editorCard,
|
|
3304
|
+
children: [
|
|
3305
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3306
|
+
className: FallbacksCard_module_css_default.ruleGrid,
|
|
3307
|
+
children: [
|
|
3308
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
3309
|
+
className: FallbacksCard_module_css_default.ruleCell,
|
|
3310
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3311
|
+
className: FallbacksCard_module_css_default.ruleCellLabel,
|
|
3312
|
+
children: t("roles.rule.origin")
|
|
3313
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
3314
|
+
className: `${FallbacksCard_module_css_default.input} ${FallbacksCard_module_css_default.selectInput}`,
|
|
3315
|
+
value: row.origin,
|
|
3316
|
+
onChange: (event) => {
|
|
3317
|
+
updateRuleRow(index, { origin: event.target.value });
|
|
3318
|
+
},
|
|
3319
|
+
children: [
|
|
3320
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
3321
|
+
value: "",
|
|
3322
|
+
children: t("roles.rule.origin.any")
|
|
3323
|
+
}),
|
|
3324
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
3325
|
+
value: "root",
|
|
3326
|
+
children: t("roles.rule.origin.root")
|
|
3327
|
+
}),
|
|
3328
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
3329
|
+
value: "subagent",
|
|
3330
|
+
children: t("roles.rule.origin.subagent")
|
|
3331
|
+
})
|
|
3332
|
+
]
|
|
3333
|
+
})]
|
|
3334
|
+
}),
|
|
3335
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
3336
|
+
className: FallbacksCard_module_css_default.ruleCell,
|
|
3337
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3338
|
+
className: FallbacksCard_module_css_default.ruleCellLabel,
|
|
3339
|
+
children: t("roles.rule.provider")
|
|
3340
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
3341
|
+
className: `${FallbacksCard_module_css_default.input} ${FallbacksCard_module_css_default.selectInput}`,
|
|
3342
|
+
value: providerRaw,
|
|
3343
|
+
onChange: (event) => {
|
|
3344
|
+
if (event.target.value === providerRaw) return;
|
|
3345
|
+
updateRuleRow(index, {
|
|
3346
|
+
provider: classifyProvider(event.target.value, catalog),
|
|
3347
|
+
model: null
|
|
3348
|
+
});
|
|
3349
|
+
},
|
|
3350
|
+
children: [
|
|
3351
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
3352
|
+
value: "",
|
|
3353
|
+
children: t("roles.rule.provider.any")
|
|
3354
|
+
}),
|
|
3355
|
+
state.configuredProviders.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
3356
|
+
value: entry.provider,
|
|
3357
|
+
children: entry.displayName
|
|
3358
|
+
}, entry.provider)),
|
|
3359
|
+
providerUnconfigured && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
3360
|
+
value: providerRaw,
|
|
3361
|
+
children: `${providerRaw}${t("catalog.unconfigured.short")}`
|
|
3362
|
+
}),
|
|
3363
|
+
providerOutside && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
3364
|
+
value: providerRaw,
|
|
3365
|
+
children: `${providerRaw}${t("catalog.outside.short")}`
|
|
3366
|
+
})
|
|
3367
|
+
]
|
|
3368
|
+
})]
|
|
3369
|
+
}),
|
|
3370
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
3371
|
+
className: FallbacksCard_module_css_default.ruleCell,
|
|
3372
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3373
|
+
className: FallbacksCard_module_css_default.ruleCellLabel,
|
|
3374
|
+
children: t("roles.rule.model")
|
|
3375
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
3376
|
+
className: `${FallbacksCard_module_css_default.input} ${FallbacksCard_module_css_default.selectInput}`,
|
|
3377
|
+
value: selectionToRaw(row.model),
|
|
3378
|
+
onChange: (event) => {
|
|
3379
|
+
updateRuleRow(index, { model: classifyModel(providerRaw, event.target.value, catalog) });
|
|
3380
|
+
},
|
|
3381
|
+
children: [
|
|
3382
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
3383
|
+
value: "",
|
|
3384
|
+
children: t("roles.rule.model.any")
|
|
3385
|
+
}),
|
|
3386
|
+
(group?.models ?? []).map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
3387
|
+
value: model.id,
|
|
3388
|
+
children: model.name
|
|
3389
|
+
}, model.id)),
|
|
3390
|
+
modelOutside && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
3391
|
+
value: selectionToRaw(row.model),
|
|
3392
|
+
children: `${selectionToRaw(row.model)}${t("catalog.outside.short")}`
|
|
3393
|
+
})
|
|
3394
|
+
]
|
|
3395
|
+
})]
|
|
3396
|
+
}),
|
|
3397
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
3398
|
+
className: FallbacksCard_module_css_default.ruleCell,
|
|
3399
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3400
|
+
className: FallbacksCard_module_css_default.ruleCellLabel,
|
|
3401
|
+
children: t("roles.rule.role")
|
|
3402
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
3403
|
+
className: `${FallbacksCard_module_css_default.input} ${FallbacksCard_module_css_default.selectInput}`,
|
|
3404
|
+
value: row.role,
|
|
3405
|
+
disabled: !writable,
|
|
3406
|
+
onChange: (event) => {
|
|
3407
|
+
updateRuleRow(index, { role: event.target.value });
|
|
3408
|
+
},
|
|
3409
|
+
children: [
|
|
3410
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
3411
|
+
value: "",
|
|
3412
|
+
children: t("roles.rule.roleSelectPlaceholder")
|
|
3413
|
+
}),
|
|
3414
|
+
roleOptions.map((id) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
3415
|
+
value: id,
|
|
3416
|
+
children: id === "inherit" ? t("roles.rule.role.inherit") : id
|
|
3417
|
+
}, id)),
|
|
3418
|
+
roleOutside && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
3419
|
+
value: row.role,
|
|
3420
|
+
children: `${row.role}${t("roles.rule.roleUndeclared.short")}`
|
|
3421
|
+
})
|
|
3422
|
+
]
|
|
3423
|
+
})]
|
|
3424
|
+
})
|
|
3425
|
+
]
|
|
3426
|
+
}),
|
|
3427
|
+
(providerOutside || modelOutside) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
3428
|
+
className: FallbacksCard_module_css_default.hint,
|
|
3429
|
+
children: [t("catalog.outside.hint"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InfoHint, {
|
|
3430
|
+
label: t("catalog.outside.tooltip"),
|
|
3431
|
+
disabled: !writable
|
|
3432
|
+
})]
|
|
3433
|
+
}),
|
|
3434
|
+
row.role === "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3435
|
+
className: FallbacksCard_module_css_default.hint,
|
|
3436
|
+
children: t("validation.ruleRoleRequired")
|
|
3437
|
+
}),
|
|
3438
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3439
|
+
className: FallbacksCard_module_css_default.cardFoot,
|
|
3440
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3441
|
+
type: "button",
|
|
3442
|
+
className: `${FallbacksCard_module_css_default.iconButton} ${FallbacksCard_module_css_default.iconButtonDanger}`,
|
|
3443
|
+
"data-tip": t("roles.removeRule"),
|
|
3444
|
+
"aria-label": t("roles.removeRule"),
|
|
3445
|
+
onClick: () => {
|
|
3446
|
+
setRuleRows((rows) => rows.filter((_, rowIndex) => rowIndex !== index));
|
|
3447
|
+
},
|
|
3448
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconTrashOutline16, {})
|
|
3449
|
+
})
|
|
3450
|
+
})
|
|
3451
|
+
]
|
|
3452
|
+
}, index);
|
|
3453
|
+
})
|
|
3454
|
+
}),
|
|
3455
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
3456
|
+
variant: "outline",
|
|
3457
|
+
size: "sm",
|
|
3458
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconPlusOutline16, { size: 14 }),
|
|
3459
|
+
className: FallbacksCard_module_css_default.addButton,
|
|
3460
|
+
onClick: () => {
|
|
3461
|
+
setRuleRows((rows) => [...rows, {
|
|
3462
|
+
origin: "",
|
|
3463
|
+
provider: null,
|
|
3464
|
+
model: null,
|
|
3465
|
+
role: ""
|
|
3466
|
+
}]);
|
|
3467
|
+
},
|
|
3468
|
+
children: t("roles.addRule")
|
|
3469
|
+
})
|
|
3470
|
+
]
|
|
3471
|
+
})
|
|
3472
|
+
]
|
|
3473
|
+
})
|
|
3474
|
+
]
|
|
3475
|
+
}),
|
|
3476
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3477
|
+
className: FallbacksCard_module_css_default.statusBlock,
|
|
3478
|
+
children: [
|
|
3479
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3480
|
+
className: FallbacksCard_module_css_default.statusTitle,
|
|
3481
|
+
children: t("status.title")
|
|
3482
|
+
}),
|
|
3483
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
3484
|
+
className: FallbacksCard_module_css_default.statusLine,
|
|
3485
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3486
|
+
className: FallbacksCard_module_css_default.statusLineLabel,
|
|
3487
|
+
children: t("status.effectiveModel.label")
|
|
3488
|
+
}), effectiveModelLine]
|
|
3489
|
+
}),
|
|
3490
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
3491
|
+
className: FallbacksCard_module_css_default.statusLine,
|
|
3492
|
+
role: state.switchesStatus === "error" ? "alert" : void 0,
|
|
3493
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3494
|
+
className: FallbacksCard_module_css_default.statusLineLabel,
|
|
3495
|
+
children: t("status.switches.label")
|
|
3496
|
+
}), switchesLine]
|
|
3497
|
+
}),
|
|
3498
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3499
|
+
className: FallbacksCard_module_css_default.statusLine,
|
|
3500
|
+
children: t("status.selectionNote")
|
|
3501
|
+
})
|
|
3502
|
+
]
|
|
3503
|
+
}),
|
|
3504
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3505
|
+
className: FallbacksCard_module_css_default.footer,
|
|
3506
|
+
children: [
|
|
3507
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3508
|
+
type: "button",
|
|
3509
|
+
className: FallbacksCard_module_css_default.secondaryButton,
|
|
3510
|
+
disabled: !dirty || saving,
|
|
3511
|
+
onClick: discard,
|
|
3512
|
+
children: t("discard")
|
|
3513
|
+
}),
|
|
3514
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3515
|
+
type: "button",
|
|
3516
|
+
className: FallbacksCard_module_css_default.secondaryButton,
|
|
3517
|
+
disabled: !writable || saving,
|
|
3518
|
+
onClick: () => {
|
|
3519
|
+
setConfirmingReset(true);
|
|
3520
|
+
},
|
|
3521
|
+
children: t("reset")
|
|
3522
|
+
}),
|
|
3523
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3524
|
+
type: "button",
|
|
3525
|
+
className: FallbacksCard_module_css_default.primaryButton,
|
|
3526
|
+
disabled: !writable || saving || !dirty,
|
|
3527
|
+
onClick: save,
|
|
3528
|
+
children: saving ? t("save.saving") : t("save")
|
|
3529
|
+
})
|
|
3530
|
+
]
|
|
3531
|
+
})
|
|
3532
|
+
]
|
|
3533
|
+
}),
|
|
3534
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
|
|
3535
|
+
open: confirmingReset,
|
|
3536
|
+
onClose: () => {
|
|
3537
|
+
if (!resetting) setConfirmingReset(false);
|
|
3538
|
+
},
|
|
3539
|
+
title: t("reset.confirmTitle"),
|
|
3540
|
+
closeLabel: t("close"),
|
|
3541
|
+
description: t("reset.confirm"),
|
|
3542
|
+
className: FallbacksCard_module_css_default.resetDialog,
|
|
3543
|
+
footer: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
3544
|
+
variant: "outline",
|
|
3545
|
+
autoFocus: true,
|
|
3546
|
+
disabled: resetting,
|
|
3547
|
+
onClick: () => {
|
|
3548
|
+
setConfirmingReset(false);
|
|
3549
|
+
},
|
|
3550
|
+
children: t("reset.confirm.cancel")
|
|
3551
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
3552
|
+
variant: "outline",
|
|
3553
|
+
className: FallbacksCard_module_css_default.confirmDanger,
|
|
3554
|
+
disabled: resetting,
|
|
3555
|
+
onClick: confirmReset,
|
|
3556
|
+
children: resetting ? t("reset.saving") : t("reset.confirm.action")
|
|
3557
|
+
})] })
|
|
3558
|
+
})
|
|
3559
|
+
]
|
|
3560
|
+
});
|
|
3561
|
+
}
|
|
3562
|
+
//#endregion
|
|
3563
|
+
//#region \0dsh-css:/Users/bibi/workspace/ai/deepseek/dsh-llm-fallbacks/src/client/GeneralFallbacksRow.module.css.mjs
|
|
3564
|
+
const css$1 = "\n\n._440e1d7b_row {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 16px 0;\n border-bottom: 1px solid var(--dsw-alias-border-l2);\n}\n\n._a4f2ceec_rowText {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 4px;\n padding-right: 48px;\n}\n\n._9865b509_title {\n font-size: 14px;\n font-weight: 400;\n line-height: 22px;\n color: var(--dsw-alias-label-primary);\n}\n\n\n._10a44713_summary {\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-tertiary);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n\n._35648278_badge {\n flex: none;\n padding: 2px 10px;\n border-radius: 9px;\n font-size: 12px;\n line-height: 18px;\n background: var(--dsw-alias-bg-module-platform);\n color: var(--dsw-alias-label-tertiary);\n}\n\n._3b9e8609_badgeEnabled {\n color: var(--dsw-alias-state-success-primary);\n}\n";
|
|
3565
|
+
const tagId$1 = "dsh-llm-fallbacks/GeneralFallbacksRow.module.css";
|
|
3566
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
|
|
3567
|
+
const tag = document.createElement("style");
|
|
3568
|
+
tag.dataset.plugin = "dsh-llm-fallbacks";
|
|
3569
|
+
tag.dataset.pluginCss = tagId$1;
|
|
3570
|
+
tag.textContent = css$1;
|
|
3571
|
+
document.head.appendChild(tag);
|
|
3572
|
+
}
|
|
3573
|
+
var GeneralFallbacksRow_module_css_default = {
|
|
3574
|
+
"row": "_440e1d7b_row",
|
|
3575
|
+
"rowText": "_a4f2ceec_rowText",
|
|
3576
|
+
"title": "_9865b509_title",
|
|
3577
|
+
"summary": "_10a44713_summary",
|
|
3578
|
+
"badge": "_35648278_badge",
|
|
3579
|
+
"badgeEnabled": "_3b9e8609_badgeEnabled"
|
|
3580
|
+
};
|
|
3581
|
+
//#endregion
|
|
3582
|
+
//#region src/client/GeneralFallbacksRow.tsx
|
|
3583
|
+
/**
|
|
3584
|
+
* Fallbacks status row — the `fallbacks` read-only row on the dsh General
|
|
3585
|
+
* settings page (plan fallbacks-aux-seams, task 1). Registered into the
|
|
3586
|
+
* `settings.general.item` slot (id `fallbacks`, order 100 — after every
|
|
3587
|
+
* upstream preference row: agent-preset -25 / permission -20 / language 0 /
|
|
3588
|
+
* appearance 10 / composer-enter 20, so the informational row renders at the
|
|
3589
|
+
* column end). Owner props are intentionally empty (`children?: never`,
|
|
3590
|
+
* dsh-private ui-settings slots.ts:81-84 — the section column only stacks),
|
|
3591
|
+
* so all data flows through the shared {@link FallbacksSettingsController}
|
|
3592
|
+
* (the same instance the plugin-config card consumes): the row triggers the
|
|
3593
|
+
* first read on mount when the store is still idle, and the pushed
|
|
3594
|
+
* invalidations wired in `apply` (`settings/document-updated` fallbacks-ns +
|
|
3595
|
+
* `connection/reset`, which refresh only already-read stores) keep it fresh
|
|
3596
|
+
* afterwards — no new data path, no store API change.
|
|
3597
|
+
*
|
|
3598
|
+
* The row is read-only by design (偏好位语义: a General preference row is not
|
|
3599
|
+
* a control surface): an enabled badge + a compact last-switch summary.
|
|
3600
|
+
* Honest degraded states: a hard load error or an unreachable gateway
|
|
3601
|
+
* channel (`ready && !present`) render the neutral 'unknown' badge — a
|
|
3602
|
+
* channel-down read must never masquerade as 'disabled' (KD-G5); the
|
|
3603
|
+
* switches face keeps its own error/empty states (D-5 semantics unchanged).
|
|
3604
|
+
*
|
|
3605
|
+
* Geometry follows the upstream Setting-Cell (figma 501:30011 — gap 8,
|
|
3606
|
+
* pad 16/0, hairline separator, title over subtitle in the text column, a
|
|
3607
|
+
* small non-interactive pill on the right); every color resolves through a
|
|
3608
|
+
* `--dsw-alias-*` token (light/dark adaptive).
|
|
3609
|
+
*/
|
|
3610
|
+
/**
|
|
3611
|
+
* Render the Fallbacks status row.
|
|
3612
|
+
* @param props - composed slot props.
|
|
3613
|
+
* @returns the row element tree.
|
|
3614
|
+
*/
|
|
3615
|
+
function GeneralFallbacksRow({ controller, useSnapshot, t }) {
|
|
3616
|
+
const state = useSnapshot((snapshot) => snapshot);
|
|
3617
|
+
(0, react.useEffect)(() => {
|
|
3618
|
+
const snapshot = controller.store.getSnapshot();
|
|
3619
|
+
if (snapshot.status === "idle") controller.load();
|
|
3620
|
+
if (snapshot.switchesStatus === "idle") controller.loadSwitches();
|
|
3621
|
+
}, [controller]);
|
|
3622
|
+
const settled = state.status === "ready";
|
|
3623
|
+
const badgeKey = settled && state.present ? state.config.enabled ? "general.enabled" : "general.disabled" : "general.unknown";
|
|
3624
|
+
const latestSwitch = state.switches[0];
|
|
3625
|
+
let summary;
|
|
3626
|
+
if (state.status === "error") summary = t("general.error", { message: state.error ?? "" });
|
|
3627
|
+
else if (!settled) summary = t("loading");
|
|
3628
|
+
else if (!state.present) summary = t("general.unavailable");
|
|
3629
|
+
else if (state.switchesStatus === "error") summary = t("status.switches.error", { message: state.switchesError ?? "" });
|
|
3630
|
+
else if (state.switchesStatus === "loading") summary = t("loading");
|
|
3631
|
+
else if (latestSwitch === void 0) summary = t("general.switch.empty");
|
|
3632
|
+
else {
|
|
3633
|
+
const reasonKey = SWITCH_REASON_KEYS[latestSwitch.reason];
|
|
3634
|
+
summary = t("general.switch", {
|
|
3635
|
+
from: `${latestSwitch.from.provider}/${latestSwitch.from.model}`,
|
|
3636
|
+
to: `${latestSwitch.to.provider}/${latestSwitch.to.model}`,
|
|
3637
|
+
role: latestSwitch.role,
|
|
3638
|
+
reason: reasonKey === void 0 ? latestSwitch.reason : t(reasonKey)
|
|
3639
|
+
});
|
|
3640
|
+
}
|
|
3641
|
+
const alert = state.status === "error" || state.switchesStatus === "error";
|
|
3642
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3643
|
+
className: GeneralFallbacksRow_module_css_default.row,
|
|
3644
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3645
|
+
className: GeneralFallbacksRow_module_css_default.rowText,
|
|
3646
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3647
|
+
className: GeneralFallbacksRow_module_css_default.title,
|
|
3648
|
+
children: t("general.title")
|
|
3649
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3650
|
+
className: GeneralFallbacksRow_module_css_default.summary,
|
|
3651
|
+
role: alert ? "alert" : void 0,
|
|
3652
|
+
children: summary
|
|
3653
|
+
})]
|
|
3654
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3655
|
+
className: `${GeneralFallbacksRow_module_css_default.badge} ${badgeKey === "general.enabled" ? GeneralFallbacksRow_module_css_default.badgeEnabled : ""}`,
|
|
3656
|
+
children: t(badgeKey)
|
|
3657
|
+
})]
|
|
3658
|
+
});
|
|
3659
|
+
}
|
|
3660
|
+
//#endregion
|
|
3661
|
+
//#region src/client/switch-guard.ts
|
|
3662
|
+
/**
|
|
3663
|
+
* True when `value` is a well-formed `fallbacks/switch` payload — the ONE
|
|
3664
|
+
* client-side shape guard, shared by the conversation node definition's
|
|
3665
|
+
* `match`/`start` and the renderer's degrade check (both in
|
|
3666
|
+
* `src/client/ConversationFallbackSwitch.tsx`).
|
|
3667
|
+
*
|
|
3668
|
+
* The durable session log is append-only and survives plugin/host upgrades,
|
|
3669
|
+
* so a `fallbacks/switch` event or node payload may carry a stale or
|
|
3670
|
+
* corrupted shape — version skew must degrade the transcript line (a
|
|
3671
|
+
* title-only notice), never crash the session assembly or the renderer.
|
|
3672
|
+
*
|
|
3673
|
+
* The HOST-side mirror (`src/commands.ts` `isFallbacksSwitchData`) lives in
|
|
3674
|
+
* a DIFFERENT bundle (host vs client) — it intentionally stays separate; do
|
|
3675
|
+
* not merge the two guards across the bundle boundary.
|
|
3676
|
+
*/
|
|
3677
|
+
function isFallbacksSwitchData(value) {
|
|
3678
|
+
if (typeof value !== "object" || value === null) return false;
|
|
3679
|
+
const payload = value;
|
|
3680
|
+
if (typeof payload.turn !== "number" || typeof payload.step !== "number") return false;
|
|
3681
|
+
if (typeof payload.role !== "string" || typeof payload.reason !== "string") return false;
|
|
3682
|
+
const from = payload.from;
|
|
3683
|
+
const to = payload.to;
|
|
3684
|
+
return typeof from?.provider === "string" && typeof from?.model === "string" && typeof to?.provider === "string" && typeof to?.model === "string";
|
|
3685
|
+
}
|
|
3686
|
+
//#endregion
|
|
3687
|
+
//#region \0dsh-css:/Users/bibi/workspace/ai/deepseek/dsh-llm-fallbacks/src/client/ConversationFallbackSwitch.module.css.mjs
|
|
3688
|
+
const css = "\n\n._ea99bbef_switchRow {\n display: flex;\n align-items: center;\n padding: 2px 0;\n font-size: 14px;\n line-height: 24px;\n}\n\n\n._02580bfd_switchTitle {\n flex: none;\n color: var(--dsw-alias-label-primary-dimmed);\n}\n\n._88b2d18b_switchSep {\n flex: none;\n width: 2px;\n height: 2px;\n margin: 0 8px;\n border-radius: 1px;\n background: var(--dsw-alias-label-caption);\n}\n\n\n._7c8c1b6f_switchSummary {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n color: var(--dsw-alias-label-tertiary);\n}\n";
|
|
3689
|
+
const tagId = "dsh-llm-fallbacks/ConversationFallbackSwitch.module.css";
|
|
3690
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
3691
|
+
const tag = document.createElement("style");
|
|
3692
|
+
tag.dataset.plugin = "dsh-llm-fallbacks";
|
|
3693
|
+
tag.dataset.pluginCss = tagId;
|
|
3694
|
+
tag.textContent = css;
|
|
3695
|
+
document.head.appendChild(tag);
|
|
3696
|
+
}
|
|
3697
|
+
var ConversationFallbackSwitch_module_css_default = {
|
|
3698
|
+
"switchRow": "_ea99bbef_switchRow",
|
|
3699
|
+
"switchTitle": "_02580bfd_switchTitle",
|
|
3700
|
+
"switchSep": "_88b2d18b_switchSep",
|
|
3701
|
+
"switchSummary": "_7c8c1b6f_switchSummary"
|
|
3702
|
+
};
|
|
3703
|
+
//#endregion
|
|
3704
|
+
//#region src/client/ConversationFallbackSwitch.tsx
|
|
3705
|
+
/**
|
|
3706
|
+
* One switch event → one chat node. Each `fallbacks/switch` event is its own
|
|
3707
|
+
* Context (id = event seq — the durable unique key), so every match is a
|
|
3708
|
+
* `start`; `update` is a passthrough (no aggregation — D3's per-Turn
|
|
3709
|
+
* counting is a separate, unselected seam).
|
|
3710
|
+
*/
|
|
3711
|
+
const fallbackSwitchDefinition = {
|
|
3712
|
+
kind: "fallbacks-switch",
|
|
3713
|
+
target: "chat",
|
|
3714
|
+
match: (event) => event.type === "fallbacks/switch" && Number.isInteger(event.seq) && isFallbacksSwitchData(event.data) ? {
|
|
3715
|
+
id: String(event.seq),
|
|
3716
|
+
role: "start"
|
|
3717
|
+
} : null,
|
|
3718
|
+
start: (_context, match) => {
|
|
3719
|
+
if (match.event.type !== "fallbacks/switch") throw new Error("fallbacks-switch start requires a fallbacks/switch event");
|
|
3720
|
+
const { seq, time } = match.event;
|
|
3721
|
+
if (!Number.isInteger(seq) || !isFallbacksSwitchData(match.event.data)) return {
|
|
3722
|
+
seq,
|
|
3723
|
+
time
|
|
3724
|
+
};
|
|
3725
|
+
const { turn, step, from, to, role, reason } = match.event.data;
|
|
3726
|
+
return {
|
|
3727
|
+
seq,
|
|
3728
|
+
time,
|
|
3729
|
+
turn,
|
|
3730
|
+
step,
|
|
3731
|
+
from,
|
|
3732
|
+
to,
|
|
3733
|
+
role,
|
|
3734
|
+
reason
|
|
3735
|
+
};
|
|
3736
|
+
},
|
|
3737
|
+
update: (context) => context.state,
|
|
3738
|
+
buildViewNode: (context) => {
|
|
3739
|
+
if (context.start === void 0 || context.state === void 0) return null;
|
|
3740
|
+
return {
|
|
3741
|
+
key: context.key,
|
|
3742
|
+
kind: "fallbacks-switch",
|
|
3743
|
+
id: context.id,
|
|
3744
|
+
target: "chat",
|
|
3745
|
+
anchorSeq: context.start.event.seq,
|
|
3746
|
+
location: context.start.location,
|
|
3747
|
+
visibility: "visible",
|
|
3748
|
+
data: context.state
|
|
3749
|
+
};
|
|
3750
|
+
}
|
|
3751
|
+
};
|
|
3752
|
+
/**
|
|
3753
|
+
* Render one fallback switch as a compact system-style transcript line.
|
|
3754
|
+
*
|
|
3755
|
+
* Geometry follows the upstream chat system rows (the compaction boundary
|
|
3756
|
+
* notice: dim title + separator + ellipsized summary — `chat/MessageItem
|
|
3757
|
+
* .module.css:38-122`); every color resolves through a `--dsw-alias-*`
|
|
3758
|
+
* token. A reason outside the current union renders raw (forward-compatible
|
|
3759
|
+
* durable log, same rule as the card/general row summaries). A malformed or
|
|
3760
|
+
* partial payload (version skew) degrades to the title-only line instead of
|
|
3761
|
+
* throwing during interpolation — the transcript slot stays visible with a
|
|
3762
|
+
* truthful "a switch happened" notice and no summary details.
|
|
3763
|
+
* @param props - composed keyed seat props.
|
|
3764
|
+
* @returns the switch line element tree.
|
|
3765
|
+
*/
|
|
3766
|
+
function ConversationFallbackSwitch({ node, t }) {
|
|
3767
|
+
const data = node.data;
|
|
3768
|
+
if (!isFallbacksSwitchData(data)) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3769
|
+
className: ConversationFallbackSwitch_module_css_default.switchRow,
|
|
3770
|
+
role: "status",
|
|
3771
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3772
|
+
className: ConversationFallbackSwitch_module_css_default.switchTitle,
|
|
3773
|
+
children: t("chat.switch.title")
|
|
3774
|
+
})
|
|
3775
|
+
});
|
|
3776
|
+
const reasonKey = SWITCH_REASON_KEYS[data.reason];
|
|
3777
|
+
const summary = t("chat.switch.summary", {
|
|
3778
|
+
from: `${data.from.provider}/${data.from.model}`,
|
|
3779
|
+
to: `${data.to.provider}/${data.to.model}`,
|
|
3780
|
+
role: data.role,
|
|
3781
|
+
reason: reasonKey === void 0 ? data.reason : t(reasonKey)
|
|
3782
|
+
});
|
|
3783
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3784
|
+
className: ConversationFallbackSwitch_module_css_default.switchRow,
|
|
3785
|
+
role: "status",
|
|
3786
|
+
children: [
|
|
3787
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3788
|
+
className: ConversationFallbackSwitch_module_css_default.switchTitle,
|
|
3789
|
+
children: t("chat.switch.title")
|
|
3790
|
+
}),
|
|
3791
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3792
|
+
className: ConversationFallbackSwitch_module_css_default.switchSep,
|
|
3793
|
+
"aria-hidden": "true"
|
|
3794
|
+
}),
|
|
3795
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3796
|
+
className: ConversationFallbackSwitch_module_css_default.switchSummary,
|
|
3797
|
+
children: summary
|
|
3798
|
+
})
|
|
3799
|
+
]
|
|
3800
|
+
});
|
|
3801
|
+
}
|
|
3802
|
+
//#endregion
|
|
3803
|
+
//#region src/client/index.ts
|
|
3804
|
+
/**
|
|
3805
|
+
* Required services (cordis fiber inject); registrations wait on the slot
|
|
3806
|
+
* declaration. `conversationEvents` is declared because the D1 Definition
|
|
3807
|
+
* registration reads the service directly (`ctx.conversationEvents.register`
|
|
3808
|
+
* at the bottom of `apply` — explicit fiber-ordering parity with the
|
|
3809
|
+
* ui-workflow-run precedent, whose inject list includes it for the same
|
|
3810
|
+
* direct read). The runtime would still provide the service synchronously
|
|
3811
|
+
* on apply, but the declaration makes the dependency honest. `sessions` is
|
|
3812
|
+
* deliberately NOT injected (S-g): a non-web host without the dsh-session
|
|
3813
|
+
* client service must not hang the fiber waiting for it — the wiring reads
|
|
3814
|
+
* it reflectively and degrades to the switches empty state when absent
|
|
3815
|
+
* (`setCurrentSession` never called, `loadSwitches` ready with an empty
|
|
3816
|
+
* array, which the store already supports).
|
|
3817
|
+
*/
|
|
3818
|
+
const inject = [
|
|
3819
|
+
"slots",
|
|
3820
|
+
"locale",
|
|
3821
|
+
"connection",
|
|
3822
|
+
"remote",
|
|
3823
|
+
"conversationEvents"
|
|
3824
|
+
];
|
|
3825
|
+
/**
|
|
3826
|
+
* Register the `fallbacks` dictionaries and the plugin-config card once the
|
|
3827
|
+
* `settings.plugin.item` declaration is on the ledger.
|
|
3828
|
+
* @param ctx - client root context.
|
|
3829
|
+
*/
|
|
3830
|
+
function apply(ctx) {
|
|
3831
|
+
ctx.effect(() => ctx.locale.register(NS, {
|
|
3832
|
+
zh,
|
|
3833
|
+
en
|
|
3834
|
+
}), "llm-fallbacks: dictionaries");
|
|
3835
|
+
const connection = ctx.get("connection");
|
|
3836
|
+
const sessions = ctx.get("sessions");
|
|
3837
|
+
const controller = new FallbacksSettingsController(connection.api, connection.rpc);
|
|
3838
|
+
const useSnapshot = (0, _deepseek_ai_dsh_client_web_react.bindSnapshotSelector)(controller.store);
|
|
3839
|
+
ctx.effect(() => {
|
|
3840
|
+
const syncSession = () => {
|
|
3841
|
+
controller.setCurrentSession(sessions?.list.getSnapshot().current);
|
|
3842
|
+
};
|
|
3843
|
+
if (sessions !== void 0) syncSession();
|
|
3844
|
+
const refresh = (ns) => {
|
|
3845
|
+
if (ns !== void 0 && ns !== "fallbacks") return;
|
|
3846
|
+
refreshFallbacksIfLoaded(controller);
|
|
3847
|
+
refreshSwitchesIfLoaded(controller);
|
|
3848
|
+
};
|
|
3849
|
+
const refreshCatalog = () => {
|
|
3850
|
+
refreshCatalogIfLoaded(controller);
|
|
3851
|
+
};
|
|
3852
|
+
let pendingReset = false;
|
|
3853
|
+
let disposed = false;
|
|
3854
|
+
const refreshAll = () => {
|
|
3855
|
+
if (pendingReset) return;
|
|
3856
|
+
pendingReset = true;
|
|
3857
|
+
queueMicrotask(() => {
|
|
3858
|
+
pendingReset = false;
|
|
3859
|
+
if (disposed) return;
|
|
3860
|
+
refresh();
|
|
3861
|
+
refreshCatalog();
|
|
3862
|
+
});
|
|
3863
|
+
};
|
|
3864
|
+
const disposers = [
|
|
3865
|
+
ctx.remote.$on("settings/document-updated", refresh),
|
|
3866
|
+
ctx.remote.$on("llm/adapters-updated", refreshCatalog),
|
|
3867
|
+
ctx.on("connection/reset", refreshAll),
|
|
3868
|
+
...sessions === void 0 ? [] : [sessions.list.subscribe(syncSession)]
|
|
3869
|
+
];
|
|
3870
|
+
return () => {
|
|
3871
|
+
disposed = true;
|
|
3872
|
+
for (const dispose of disposers) dispose();
|
|
3873
|
+
controller.dispose();
|
|
3874
|
+
};
|
|
3875
|
+
}, "llm-fallbacks: pushed invalidations");
|
|
3876
|
+
ctx.slots.inject("settings.plugin.item", function* () {
|
|
3877
|
+
yield ctx.slots.register({
|
|
3878
|
+
name: "settings.plugin.item",
|
|
3879
|
+
id: "fallbacks",
|
|
3880
|
+
order: 30,
|
|
3881
|
+
locale: NS,
|
|
3882
|
+
inject: () => ({
|
|
3883
|
+
controller,
|
|
3884
|
+
useSnapshot
|
|
3885
|
+
})
|
|
3886
|
+
}, FallbacksCard);
|
|
3887
|
+
});
|
|
3888
|
+
ctx.slots.inject("settings.general.item", function* () {
|
|
3889
|
+
yield ctx.slots.register({
|
|
3890
|
+
name: "settings.general.item",
|
|
3891
|
+
id: "fallbacks",
|
|
3892
|
+
order: 100,
|
|
3893
|
+
locale: NS,
|
|
3894
|
+
inject: () => ({
|
|
3895
|
+
controller,
|
|
3896
|
+
useSnapshot
|
|
3897
|
+
})
|
|
3898
|
+
}, GeneralFallbacksRow);
|
|
3899
|
+
});
|
|
3900
|
+
ctx.effect(() => ctx.conversationEvents.register(fallbackSwitchDefinition), "llm-fallbacks: conversation node definition");
|
|
3901
|
+
ctx.slots.inject("conversation.chat.node", function* () {
|
|
3902
|
+
yield ctx.slots.register({
|
|
3903
|
+
name: "conversation.chat.node",
|
|
3904
|
+
key: "fallbacks-switch",
|
|
3905
|
+
locale: NS
|
|
3906
|
+
}, ConversationFallbackSwitch);
|
|
3907
|
+
});
|
|
3908
|
+
}
|
|
3909
|
+
//#endregion
|
|
3910
|
+
exports.FALLBACKS_SETTINGS_NS = FALLBACKS_SETTINGS_NS;
|
|
3911
|
+
exports.FallbacksSettingsController = FallbacksSettingsController;
|
|
3912
|
+
exports.apply = apply;
|
|
3913
|
+
exports.inject = inject;
|
|
3914
|
+
return module.exports;
|
|
3915
|
+
}
|
|
3916
|
+
});
|