dsh-decision-engine 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -17
- package/config.example.yml +49 -19
- package/lib/adapters/tools.js +1 -1
- package/lib/client.js +168 -133
- package/lib/decision-cache.js +1 -1
- package/lib/endpoint-fallback.js +143 -0
- package/lib/engine.js +3 -3
- package/lib/index.js +247 -97
- package/lib/providers.js +166 -7
- package/package.json +5 -2
- package/lib/budget.js +0 -92
package/lib/client.js
CHANGED
|
@@ -250,9 +250,9 @@ window.__ModuleLoader__.load({
|
|
|
250
250
|
}
|
|
251
251
|
|
|
252
252
|
/**
|
|
253
|
-
*
|
|
253
|
+
* 待落盘的决策点改动,没有改动时返回 null。
|
|
254
254
|
*
|
|
255
|
-
* 与 `/decisions` 的 PUT body 同形(`{ points
|
|
255
|
+
* 与 `/decisions` 的 PUT body 同形(`{ points? }`):
|
|
256
256
|
* 服务端对空 body 直接 400,所以「没改动就不发」必须由调用方保证。
|
|
257
257
|
*/
|
|
258
258
|
function pendingDecisions(values, decisions) {
|
|
@@ -261,17 +261,45 @@ window.__ModuleLoader__.load({
|
|
|
261
261
|
const original = (decisions?.points ?? []).find((item) => item.id === id);
|
|
262
262
|
if (original !== undefined && original.mode !== mode) changed[id] = mode;
|
|
263
263
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
264
|
+
if (Object.keys(changed).length === 0) return null;
|
|
265
|
+
return { points: changed };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* 当前 provider 的字段名与模型策略。
|
|
270
|
+
*
|
|
271
|
+
* **全部由服务端下发**(`providerCatalog` 的 `modelField` / `keyField` /
|
|
272
|
+
* `requiresModel` / `knownModels`),前端不抄一份。理由与 provider 下拉
|
|
273
|
+
* 那条一致:名字只有 providers.js 的预设表知道,前端再写一份就是
|
|
274
|
+
* 「界面显示存到 A、实际写进 B」这种最难自查的偏差。
|
|
275
|
+
*
|
|
276
|
+
* @param providerId - 当前 provider 的 id。
|
|
277
|
+
* @param view - `/status` 回显。
|
|
278
|
+
* @returns catalog 条目;拿不到(状态未加载)时返回 null。
|
|
279
|
+
*/
|
|
280
|
+
function providerFieldsOf(providerId, view) {
|
|
281
|
+
const item = (view?.providerCatalog ?? []).find((row) => row.id === providerId);
|
|
282
|
+
// 字段名是写回配置的前提,少了它宁可不写——见 flush() 里的延后处理。
|
|
283
|
+
if (item === undefined || typeof item.modelField !== "string" || typeof item.keyField !== "string") {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
return item;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* 各模型字段的原值,键就是 config.yml 里的字段名。
|
|
291
|
+
*
|
|
292
|
+
* 服务端下发 `rawModels`(按字段名);老服务端只有三个散字段,
|
|
293
|
+
* 一并并入,于是「回显」与「比较有没有改动」都只有一种写法,
|
|
294
|
+
* 不会因为少了一格而漏判改动(漏判 = 改了不保存 = 静默)。
|
|
295
|
+
*/
|
|
296
|
+
function rawModelsOf(view) {
|
|
297
|
+
return {
|
|
298
|
+
model: view?.rawModel ?? "",
|
|
299
|
+
systemoneModel: view?.rawSystemoneModel ?? "",
|
|
300
|
+
customModel: view?.customModel ?? "",
|
|
301
|
+
...(view?.rawModels ?? {}),
|
|
302
|
+
};
|
|
275
303
|
}
|
|
276
304
|
|
|
277
305
|
/**
|
|
@@ -282,8 +310,10 @@ window.__ModuleLoader__.load({
|
|
|
282
310
|
*
|
|
283
311
|
* @param values - `{ draft, providerDraft, keyInputs, storageDraft }`。
|
|
284
312
|
* @param keyCommit - 用户明确提交密钥的 provider(失焦/回车),否则 null。
|
|
313
|
+
* @param modelFields - 当前 provider 的字段(providerFieldsOf 结果,非 null)。
|
|
314
|
+
* @param keyFields - 提交密钥的那个 provider 的字段;本次没提交时为 null。
|
|
285
315
|
*/
|
|
286
|
-
function configPatchOf(values, keyCommit) {
|
|
316
|
+
function configPatchOf(values, keyCommit, modelFields, keyFields) {
|
|
287
317
|
const draft = values.draft;
|
|
288
318
|
const provider = values.providerDraft;
|
|
289
319
|
const patch = {
|
|
@@ -291,14 +321,10 @@ window.__ModuleLoader__.load({
|
|
|
291
321
|
mode: draft.mode,
|
|
292
322
|
// 思考模式总是提交:它是全局项(与 provider 无关),服务端还会再校验一次。
|
|
293
323
|
thinkMode: draft.thinkMode ?? "never",
|
|
294
|
-
//
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
|
|
298
|
-
? { customModel: (provider.customModel ?? "").trim() }
|
|
299
|
-
: provider.provider === "systemone"
|
|
300
|
-
? { systemoneModel: (draft.systemoneModel ?? "").trim() }
|
|
301
|
-
: { model: (draft.model ?? "").trim() }),
|
|
324
|
+
// 模型只写当前 provider 自己那一格(字段名叫什么由 fields 给)。
|
|
325
|
+
// 混写会让「切过去再切回来」丢掉原值,也会把 bamboonode 的模型名
|
|
326
|
+
// 发给别的服务。
|
|
327
|
+
[modelFields.modelField]: (draft[modelFields.modelField] ?? "").trim(),
|
|
302
328
|
provider: provider.provider,
|
|
303
329
|
customEndpointStyle: provider.customEndpointStyle ?? "auto",
|
|
304
330
|
// custom 的字段总是带上:用户可能先填地址再切 provider,
|
|
@@ -317,13 +343,14 @@ window.__ModuleLoader__.load({
|
|
|
317
343
|
}
|
|
318
344
|
// 密钥:只有用户明确提交(失焦 / 回车)且长度够时才带上。
|
|
319
345
|
// 否则打字过程中每停一下都会把半截 key 写进凭据文件。
|
|
320
|
-
|
|
346
|
+
//
|
|
347
|
+
// 写到哪一格按**提交时的 provider**(keyCommit)而不是当前选中的 provider:
|
|
348
|
+
// 提交与落盘之间可能隔了几百毫秒(防抖、上一次保存还在飞),期间用户
|
|
349
|
+
// 完全可以切走。按当前选中项写,就会把硅基流动的 key 存进 bamboonode 的
|
|
350
|
+
// 格子——正是「存了 key 却说没配 key」的来源。
|
|
351
|
+
if (keyCommit !== null && keyCommit !== undefined && keyFields !== null) {
|
|
321
352
|
const raw = (values.keyInputs[keyCommit] ?? "").trim();
|
|
322
|
-
if (raw.length >= 10)
|
|
323
|
-
if (keyCommit === "systemone") patch.systemoneApiKey = raw;
|
|
324
|
-
else if (keyCommit === "custom") patch.customApiKey = raw;
|
|
325
|
-
else patch.bamboonodeApiKey = raw;
|
|
326
|
-
}
|
|
353
|
+
if (raw.length >= 10) patch[keyFields.keyField] = raw;
|
|
327
354
|
}
|
|
328
355
|
return patch;
|
|
329
356
|
}
|
|
@@ -338,7 +365,7 @@ window.__ModuleLoader__.load({
|
|
|
338
365
|
* @param patch - `configPatchOf` 拼出的 patch。
|
|
339
366
|
* @param view - `/status` 回显(缺字段时用安全默认)。
|
|
340
367
|
*/
|
|
341
|
-
function patchDiffersFromServer(patch, view) {
|
|
368
|
+
function patchDiffersFromServer(patch, view, fields) {
|
|
342
369
|
const pairs = [
|
|
343
370
|
["enabled", view.enabled === true],
|
|
344
371
|
["mode", view.mode ?? "shadow"],
|
|
@@ -346,19 +373,20 @@ window.__ModuleLoader__.load({
|
|
|
346
373
|
["provider", view.provider ?? "bamboonode"],
|
|
347
374
|
["customBaseUrl", view.customBaseUrl ?? ""],
|
|
348
375
|
["customEndpointStyle", view.customEndpointStyle ?? "auto"],
|
|
349
|
-
["customModel", view.customModel ?? ""],
|
|
350
|
-
["model", view.rawModel ?? ""],
|
|
351
|
-
["systemoneModel", view.rawSystemoneModel ?? ""],
|
|
352
376
|
["customHeaders", view.customHeaders ?? ""],
|
|
353
377
|
["customApiKeyEnv", view.customApiKeyEnv ?? ""],
|
|
354
378
|
["keyStorage", view.keyStorage ?? "credentials"],
|
|
379
|
+
// 模型字段逐格比较:键就是 config.yml 里的字段名(rawModels 下发)。
|
|
380
|
+
// 用一张表而不是逐个 provider 写死,新增预设才不用改这里——漏一格
|
|
381
|
+
// 的后果是「改了模型却不触发保存」。
|
|
382
|
+
...Object.entries(rawModelsOf(view)),
|
|
355
383
|
];
|
|
356
384
|
for (const [key, server] of pairs) {
|
|
357
385
|
if (Object.hasOwn(patch, key) && String(patch[key] ?? "") !== String(server ?? "")) return true;
|
|
358
386
|
}
|
|
359
387
|
// 密钥字段一出现就代表「有新 key 要写」:服务端只回传掩码,没法比较,
|
|
360
|
-
// 而且重复写同一个 key
|
|
361
|
-
return
|
|
388
|
+
// 而且重复写同一个 key 是无害的(幂等覆盖)。字段名由服务端下发。
|
|
389
|
+
return Object.hasOwn(patch, fields.keyField);
|
|
362
390
|
}
|
|
363
391
|
|
|
364
392
|
/**
|
|
@@ -367,7 +395,7 @@ window.__ModuleLoader__.load({
|
|
|
367
395
|
*
|
|
368
396
|
* **改完即保存,没有「保存」按钮**:
|
|
369
397
|
* - 开关 / 下拉 / 分段控件:点一下就落盘;
|
|
370
|
-
* -
|
|
398
|
+
* - 文本框(地址、请求头):停手 `AUTOSAVE_DELAY_MS` 后落盘,失焦立刻落盘;
|
|
371
399
|
* - API Key:失焦或回车才落盘(见 `commitKey`)。
|
|
372
400
|
* 落盘状态显示在底部一行(保存中 / 已保存 / 失败原因 + 重试),
|
|
373
401
|
* 取代了原来那个按钮。
|
|
@@ -379,9 +407,10 @@ window.__ModuleLoader__.load({
|
|
|
379
407
|
*/
|
|
380
408
|
function StartluxPage(props) {
|
|
381
409
|
const [status, setStatus] = react.useState(null);
|
|
382
|
-
// 密钥输入按 provider
|
|
383
|
-
//
|
|
384
|
-
|
|
410
|
+
// 密钥输入按 provider 分槽(键 = provider id):切到别的 provider 时,
|
|
411
|
+
// 这一格不该带过去,否则容易把 A 的 key 存到 B 名下。槽位不预置名单——
|
|
412
|
+
// 读的时候一律 `?? ""`,新增 provider 不必再改这里。
|
|
413
|
+
const [keyInputs, setKeyInputs] = react.useState({});
|
|
385
414
|
const [busy, setBusy] = react.useState("");
|
|
386
415
|
const [msg, setMsg] = react.useState(null);
|
|
387
416
|
const [loading, setLoading] = react.useState(true);
|
|
@@ -403,7 +432,6 @@ window.__ModuleLoader__.load({
|
|
|
403
432
|
const [decisions, setDecisions] = react.useState(null);
|
|
404
433
|
// 待保存的模式改动(id → mode);null 表示该行未改动,沿用服务端值。
|
|
405
434
|
const [pointsDraft, setPointsDraft] = react.useState({});
|
|
406
|
-
const [budgetDraft, setBudgetDraft] = react.useState(null);
|
|
407
435
|
|
|
408
436
|
// 用 ref 保存「用户是否有未保存改动」,使 refresh 不因状态变化而重建。
|
|
409
437
|
// 否则落盘时 setDirty(false) → refresh 换新引用 → useEffect 再跑一次,
|
|
@@ -446,16 +474,18 @@ window.__ModuleLoader__.load({
|
|
|
446
474
|
enabled: next.enabled === true,
|
|
447
475
|
mode: next.mode ?? "shadow",
|
|
448
476
|
thinkMode: next.thinkMode ?? "never",
|
|
449
|
-
|
|
450
|
-
|
|
477
|
+
// 模型各 provider 一格:键就是 config.yml 里的字段名(rawModels
|
|
478
|
+
// 下发),因此新增预设不必再改前端。切 provider 也不会把 A 的
|
|
479
|
+
// 模型名带进 B 那一格。
|
|
480
|
+
...rawModelsOf(next),
|
|
451
481
|
});
|
|
452
482
|
// provider 回显:保存后重新打开必须看到自己选的那一项与 custom 值。
|
|
453
|
-
// 全部来自服务端 status
|
|
483
|
+
// 全部来自服务端 status,前端不缓存、不推断。模型不在这里——
|
|
484
|
+
// 它按字段名放在 draft(见 rawModelsOf)。
|
|
454
485
|
setProviderDraft({
|
|
455
486
|
provider: next.provider ?? "bamboonode",
|
|
456
487
|
customBaseUrl: next.customBaseUrl ?? "",
|
|
457
488
|
customEndpointStyle: next.customEndpointStyle ?? "auto",
|
|
458
|
-
customModel: next.customModel ?? "",
|
|
459
489
|
customHeaders: next.customHeaders ?? "",
|
|
460
490
|
customApiKeyEnv: next.customApiKeyEnv ?? "",
|
|
461
491
|
});
|
|
@@ -489,7 +519,7 @@ window.__ModuleLoader__.load({
|
|
|
489
519
|
}
|
|
490
520
|
}, []);
|
|
491
521
|
|
|
492
|
-
/** 拉决策点总览(含每个点的实测 token / 延迟 /
|
|
522
|
+
/** 拉决策点总览(含每个点的实测 token / 延迟 / 缓存)。 */
|
|
493
523
|
const loadDecisions = react.useCallback(async () => {
|
|
494
524
|
try {
|
|
495
525
|
const result = await call("/decisions");
|
|
@@ -521,6 +551,8 @@ window.__ModuleLoader__.load({
|
|
|
521
551
|
providerError: "",
|
|
522
552
|
providerCatalog: [],
|
|
523
553
|
baseUrl: "",
|
|
554
|
+
// 探测出的可用路径:status 失败时留空(界面不显示那一行)。
|
|
555
|
+
detectedEndpoint: "",
|
|
524
556
|
model: "",
|
|
525
557
|
timeoutMs: 20000,
|
|
526
558
|
keyConfigured: false,
|
|
@@ -539,14 +571,16 @@ window.__ModuleLoader__.load({
|
|
|
539
571
|
mode: view.mode ?? "shadow",
|
|
540
572
|
// 思考模式也走 draft:与其它控件一致,改完即自动落盘。
|
|
541
573
|
thinkMode: view.thinkMode ?? "never",
|
|
542
|
-
|
|
543
|
-
|
|
574
|
+
// 模型一格一个 provider,键 = config.yml 字段名(见 rawModelsOf)。
|
|
575
|
+
...rawModelsOf(view),
|
|
544
576
|
};
|
|
577
|
+
// provider 草稿:与主配置分开保存,因为 custom 的字段是「先填后切」,
|
|
578
|
+
// 混进主 draft 会让「只切 provider」也顺手把没填完的自定义地址写进去。
|
|
579
|
+
// 模型不在这里——它跟模型行走,与 provider 切换同步刷新。
|
|
545
580
|
const providerDraft = providerDraftState ?? {
|
|
546
581
|
provider: view.provider ?? "bamboonode",
|
|
547
582
|
customBaseUrl: view.customBaseUrl ?? "",
|
|
548
583
|
customEndpointStyle: view.customEndpointStyle ?? "auto",
|
|
549
|
-
customModel: view.customModel ?? "",
|
|
550
584
|
customHeaders: view.customHeaders ?? "",
|
|
551
585
|
customApiKeyEnv: view.customApiKeyEnv ?? "",
|
|
552
586
|
};
|
|
@@ -563,14 +597,15 @@ window.__ModuleLoader__.load({
|
|
|
563
597
|
enabled: next.enabled === true,
|
|
564
598
|
mode: next.mode ?? "shadow",
|
|
565
599
|
thinkMode: next.thinkMode ?? "never",
|
|
566
|
-
|
|
567
|
-
|
|
600
|
+
// 模型各 provider 一格:键就是 config.yml 里的字段名(rawModels
|
|
601
|
+
// 下发),因此新增预设不必再改前端。切 provider 也不会把 A 的
|
|
602
|
+
// 模型名带进 B 那一格。
|
|
603
|
+
...rawModelsOf(next),
|
|
568
604
|
});
|
|
569
605
|
setProviderDraft({
|
|
570
606
|
provider: next.provider ?? "bamboonode",
|
|
571
607
|
customBaseUrl: next.customBaseUrl ?? "",
|
|
572
608
|
customEndpointStyle: next.customEndpointStyle ?? "auto",
|
|
573
|
-
customModel: next.customModel ?? "",
|
|
574
609
|
customHeaders: next.customHeaders ?? "",
|
|
575
610
|
customApiKeyEnv: next.customApiKeyEnv ?? "",
|
|
576
611
|
});
|
|
@@ -581,7 +616,7 @@ window.__ModuleLoader__.load({
|
|
|
581
616
|
// 每次渲染把「当前值」同步进 ref:控件在事件里读它,flush 也读它。
|
|
582
617
|
// 放在渲染期赋值(而不是 useEffect)是必须的——useEffect 在提交之后才跑,
|
|
583
618
|
// 而事件处理器可能在它之前就读到 ref,那样拿到的还是上一次的值。
|
|
584
|
-
valuesRef.current = { draft, providerDraft, keyInputs, storageDraft, pointsDraft
|
|
619
|
+
valuesRef.current = { draft, providerDraft, keyInputs, storageDraft, pointsDraft };
|
|
585
620
|
|
|
586
621
|
/**
|
|
587
622
|
* 自动保存引擎。
|
|
@@ -654,7 +689,7 @@ window.__ModuleLoader__.load({
|
|
|
654
689
|
* 落盘一次。
|
|
655
690
|
*
|
|
656
691
|
* 决策点走独立的 `/decisions` 而不是复用 `/config`:那两份配置的校验规则
|
|
657
|
-
*
|
|
692
|
+
* 不同(决策点要拒绝未实装的点),混在一起容易互相放宽。
|
|
658
693
|
*/
|
|
659
694
|
const flush = async () => {
|
|
660
695
|
if (savingRef.current === true) return;
|
|
@@ -669,19 +704,34 @@ window.__ModuleLoader__.load({
|
|
|
669
704
|
values.providerDraft.customBaseUrl,
|
|
670
705
|
values.providerDraft.provider,
|
|
671
706
|
);
|
|
672
|
-
|
|
707
|
+
// 服务端还没给出字段映射(配置页刚打开、status 尚未回来)。此时无法
|
|
708
|
+
// 知道「模型与 key 该写进 config.yml 的哪一格」,猜一个名字就是把
|
|
709
|
+
// siliconflow 的 key 写进 bamboonode 的格子——宁可延后并说明原因,
|
|
710
|
+
// 也不能猜着写。状态回来后 dirty 仍为 true,改动会自动补发。
|
|
711
|
+
const modelFields = providerFieldsOf(values.providerDraft.provider, view);
|
|
712
|
+
// 密钥按提交时的 provider 取字段(见 configPatchOf 里的说明)。
|
|
713
|
+
const keyFields =
|
|
714
|
+
keyCommit === null || keyCommit === undefined
|
|
715
|
+
? null
|
|
716
|
+
: providerFieldsOf(keyCommit, view);
|
|
717
|
+
const mapProblem =
|
|
718
|
+
modelFields === null || (keyCommit != null && keyFields === null)
|
|
719
|
+
? "配置尚未就绪(服务端未返回 provider 字段映射),请稍后重试"
|
|
720
|
+
: "";
|
|
721
|
+
const problem = urlProblem.length > 0 ? urlProblem : mapProblem;
|
|
722
|
+
const configDeferred = problem.length > 0;
|
|
673
723
|
if (configDeferred === true && decisionsBody === null) {
|
|
674
|
-
setSaveState({ error: `配置未保存:${
|
|
724
|
+
setSaveState({ error: `配置未保存:${problem}` });
|
|
675
725
|
return;
|
|
676
726
|
}
|
|
677
727
|
|
|
678
|
-
const patch =
|
|
728
|
+
const patch =
|
|
729
|
+
modelFields === null ? {} : configPatchOf(values, keyCommit, modelFields, keyFields);
|
|
679
730
|
// 只要带了 key 就必须写配置:key 不能因为「其它字段没变」被漏掉。
|
|
680
|
-
const keyWritten =
|
|
681
|
-
Object.hasOwn(patch, key),
|
|
682
|
-
);
|
|
731
|
+
const keyWritten = keyFields !== null && Object.hasOwn(patch, keyFields.keyField);
|
|
683
732
|
const configNeeded =
|
|
684
|
-
configDeferred !== true
|
|
733
|
+
configDeferred !== true
|
|
734
|
+
&& (keyWritten === true || patchDiffersFromServer(patch, view, modelFields) === true);
|
|
685
735
|
|
|
686
736
|
savingRef.current = true;
|
|
687
737
|
setSaveState("saving");
|
|
@@ -711,17 +761,16 @@ window.__ModuleLoader__.load({
|
|
|
711
761
|
// 期间又有改动时不清草稿:那些改动还没落盘,清掉就等于吞了。
|
|
712
762
|
if (seq === editSeqRef.current) {
|
|
713
763
|
setPointsDraft({});
|
|
714
|
-
setBudgetDraft(null);
|
|
715
764
|
}
|
|
716
765
|
}
|
|
717
766
|
if (seq !== editSeqRef.current) {
|
|
718
767
|
// 期间的改动由 finally 接着发,这里保持「保存中」。
|
|
719
768
|
setSaveState("saving");
|
|
720
769
|
} else if (configDeferred === true) {
|
|
721
|
-
//
|
|
722
|
-
//
|
|
770
|
+
// 决策点存了,配置那一半还欠着(等地址补全或配置就绪):**保持 dirty**,
|
|
771
|
+
// 否则原因消失之后没人会再发请求,改动就悄悄丢了。
|
|
723
772
|
dirtyRef.current = true;
|
|
724
|
-
setSaveState({ error: `配置未保存:${
|
|
773
|
+
setSaveState({ error: `配置未保存:${problem}` });
|
|
725
774
|
} else {
|
|
726
775
|
dirtyRef.current = false;
|
|
727
776
|
if (keyWritten === true) {
|
|
@@ -827,33 +876,26 @@ window.__ModuleLoader__.load({
|
|
|
827
876
|
const isCustom = providerDraft.provider === "custom";
|
|
828
877
|
|
|
829
878
|
/**
|
|
830
|
-
* 模型字段按 provider
|
|
879
|
+
* 模型字段按 provider 分派:字段名叫什么由服务端下发(`modelField`),
|
|
880
|
+
* 前端不再逐个 provider 写 if/else——否则新增预设就得同步改前端,
|
|
881
|
+
* 漏改就是「A 的模型名写进了 B 的格子」。
|
|
831
882
|
*
|
|
832
|
-
* bamboonode
|
|
833
|
-
* systemone
|
|
834
|
-
*
|
|
883
|
+
* bamboonode -> model (老版本唯一的模型键)
|
|
884
|
+
* systemone -> systemoneModel
|
|
885
|
+
* siliconflow -> siliconflowModel
|
|
886
|
+
* siliconflow-intl -> siliconflowIntlModel
|
|
887
|
+
* custom -> customModel
|
|
835
888
|
*
|
|
836
|
-
*
|
|
889
|
+
* 各格分开存,切换 provider 才不会把 A 的模型名发给 B。
|
|
837
890
|
*/
|
|
838
|
-
const
|
|
839
|
-
const
|
|
840
|
-
|
|
841
|
-
: isSystemOne
|
|
842
|
-
? draft.systemoneModel ?? ""
|
|
843
|
-
: draft.model ?? "";
|
|
891
|
+
const activeFields = providerFieldsOf(providerDraft.provider, view);
|
|
892
|
+
const modelField = activeFields?.modelField ?? "model";
|
|
893
|
+
const modelValue = draft[modelField] ?? "";
|
|
844
894
|
const setModelValue = (value) => {
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
else setDraft({ ...draft, model: value });
|
|
895
|
+
const next = { ...draft, [modelField]: value };
|
|
896
|
+
setDraft(next);
|
|
848
897
|
// 下拉是离散控件:选完即存。
|
|
849
|
-
scheduleSave(
|
|
850
|
-
isCustom
|
|
851
|
-
? { providerDraft: { ...providerDraft, customModel: value } }
|
|
852
|
-
: isSystemOne
|
|
853
|
-
? { draft: { ...draft, systemoneModel: value } }
|
|
854
|
-
: { draft: { ...draft, model: value } },
|
|
855
|
-
true,
|
|
856
|
-
);
|
|
898
|
+
scheduleSave({ draft: next }, true);
|
|
857
899
|
};
|
|
858
900
|
|
|
859
901
|
/**
|
|
@@ -924,11 +966,24 @@ window.__ModuleLoader__.load({
|
|
|
924
966
|
? "由上方 base URL + 端点风格拼接;已带 /decisions 或 /systemone 时原样使用"
|
|
925
967
|
: "内置预设的默认地址(可用 config.yml 的 baseUrl 覆盖 bamboonode)",
|
|
926
968
|
h(
|
|
927
|
-
"
|
|
928
|
-
{ style: {
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
:
|
|
969
|
+
"div",
|
|
970
|
+
{ style: { display: "flex", flexDirection: "column", gap: "3px", maxWidth: "320px" } },
|
|
971
|
+
h(
|
|
972
|
+
"span",
|
|
973
|
+
{ style: { ...S.v, fontSize: "11px", opacity: 0.75, wordBreak: "break-all" } },
|
|
974
|
+
view.providerValid === false
|
|
975
|
+
? "配置不可用"
|
|
976
|
+
: view.baseUrl || "(服务端未返回)",
|
|
977
|
+
),
|
|
978
|
+
// 探测出的实际可用路径:路径后缀由服务商决定(实测 SiliconFlow
|
|
979
|
+
// 只通 /systemone),必须让用户看见「最后打的是哪条」。
|
|
980
|
+
typeof view.detectedEndpoint === "string" && view.detectedEndpoint.length > 0
|
|
981
|
+
? h(
|
|
982
|
+
"span",
|
|
983
|
+
{ style: { ...S.v, fontSize: "11px", opacity: 0.85, color: "#7dd3a0" } },
|
|
984
|
+
`已验证可用:${view.detectedEndpoint}`,
|
|
985
|
+
)
|
|
986
|
+
: null,
|
|
932
987
|
),
|
|
933
988
|
"endpoint",
|
|
934
989
|
),
|
|
@@ -995,18 +1050,25 @@ window.__ModuleLoader__.load({
|
|
|
995
1050
|
),
|
|
996
1051
|
"thinkMode",
|
|
997
1052
|
),
|
|
998
|
-
// 3)
|
|
999
|
-
//
|
|
1000
|
-
//
|
|
1053
|
+
// 3) 模型:下拉选择,含「留空」项
|
|
1054
|
+
// 每个 provider 用自己那一格(modelField 由服务端下发),分开存才能做到
|
|
1055
|
+
// 「切 provider 不覆盖其它 provider 已保存的模型」。
|
|
1056
|
+
//
|
|
1057
|
+
// 「留空」的含义由该端点的脾气决定:多数端点接受省略 model(服务端选
|
|
1058
|
+
// 默认);而 SiliconFlow 的 /systemone 把 model 列为必填(实测省略回
|
|
1059
|
+
// 400 `Model field is required.`),留空时插件改用预设默认 Kev-4B。
|
|
1060
|
+
// 文案必须说清楚,否则用户会以为「留空 = 让服务端挑」。
|
|
1001
1061
|
row(
|
|
1002
1062
|
"模型",
|
|
1003
|
-
|
|
1004
|
-
?
|
|
1005
|
-
: modelsState === "
|
|
1006
|
-
? "
|
|
1007
|
-
: modelsState === "
|
|
1008
|
-
? "
|
|
1009
|
-
: "
|
|
1063
|
+
(activeFields?.requiresModel ?? false) === true
|
|
1064
|
+
? `该端点 model 必填;留空 = 用预设默认 ${activeFields.defaultModel}(可改填 SemIf)`
|
|
1065
|
+
: modelsState === "ok"
|
|
1066
|
+
? "从服务商实时获取;选「自动」则由服务端决定,换名也不会失效"
|
|
1067
|
+
: modelsState === "loading"
|
|
1068
|
+
? "正在获取模型列表…"
|
|
1069
|
+
: modelsState === "error"
|
|
1070
|
+
? "获取失败,可手填或留空用服务端默认"
|
|
1071
|
+
: "未获取到列表,可手填或留空用服务端默认",
|
|
1010
1072
|
h(
|
|
1011
1073
|
"div",
|
|
1012
1074
|
{ style: { display: "flex", gap: "6px", alignItems: "center", flex: 1, minWidth: 0 } },
|
|
@@ -1017,7 +1079,9 @@ window.__ModuleLoader__.load({
|
|
|
1017
1079
|
value: modelValue ?? "",
|
|
1018
1080
|
onChange: (event) => setModelValue(event.target.value),
|
|
1019
1081
|
},
|
|
1020
|
-
|
|
1082
|
+
(activeFields?.requiresModel ?? false) === true
|
|
1083
|
+
? h("option", { value: "" }, `默认(${activeFields.defaultModel})`)
|
|
1084
|
+
: h("option", { value: "" }, "自动(服务端默认)"),
|
|
1021
1085
|
// 当前值不在列表里时也保留,避免下拉把已配置的值吞掉。
|
|
1022
1086
|
...((modelValue ?? "") === "" || models.some((item) => item.id === modelValue)
|
|
1023
1087
|
? []
|
|
@@ -1126,7 +1190,8 @@ window.__ModuleLoader__.load({
|
|
|
1126
1190
|
),
|
|
1127
1191
|
row(
|
|
1128
1192
|
"端点风格",
|
|
1129
|
-
"
|
|
1193
|
+
"路径后缀由服务商决定(实测 SiliconFlow 只通 /systemone,bamboonode 两条都通)。" +
|
|
1194
|
+
"这里是「首选」:若首选路径返回 404,插件会自动改用另一条再试一次",
|
|
1130
1195
|
h(
|
|
1131
1196
|
"select",
|
|
1132
1197
|
{
|
|
@@ -1139,7 +1204,7 @@ window.__ModuleLoader__.load({
|
|
|
1139
1204
|
scheduleSave({ providerDraft: next }, true);
|
|
1140
1205
|
},
|
|
1141
1206
|
},
|
|
1142
|
-
h("option", { value: "auto" }, "auto
|
|
1207
|
+
h("option", { value: "auto" }, "auto(先 /decisions)"),
|
|
1143
1208
|
h("option", { value: "decisions" }, "decisions"),
|
|
1144
1209
|
h("option", { value: "systemone" }, "systemone"),
|
|
1145
1210
|
),
|
|
@@ -1278,36 +1343,6 @@ window.__ModuleLoader__.load({
|
|
|
1278
1343
|
changed ? h("span", { style: { ...S.badge, borderStyle: "solid", opacity: 0.9 } }, "保存中") : null,
|
|
1279
1344
|
);
|
|
1280
1345
|
}),
|
|
1281
|
-
// 自身预算:决策模型自己也烧 token,不设上限会变成主要消耗方。
|
|
1282
|
-
h(
|
|
1283
|
-
"div",
|
|
1284
|
-
{ style: { ...S.rowLine, marginTop: "10px" }, key: "budget" },
|
|
1285
|
-
h(
|
|
1286
|
-
"div",
|
|
1287
|
-
{ style: S.rowText },
|
|
1288
|
-
h("div", { style: S.rowTitle }, "自身 token 预算"),
|
|
1289
|
-
h(
|
|
1290
|
-
"div",
|
|
1291
|
-
{ style: S.rowHint },
|
|
1292
|
-
`每小时上限,超出后自动退回启发式并放行(当前已用 ${decisions.budget?.used ?? 0})`,
|
|
1293
|
-
),
|
|
1294
|
-
),
|
|
1295
|
-
h("input", {
|
|
1296
|
-
type: "number",
|
|
1297
|
-
min: 0,
|
|
1298
|
-
step: 10000,
|
|
1299
|
-
style: { ...S.input, maxWidth: "130px", flex: "none" },
|
|
1300
|
-
defaultValue: decisions.budget?.limit ?? 0,
|
|
1301
|
-
onChange: (event) => {
|
|
1302
|
-
const value = Number(event.target.value);
|
|
1303
|
-
const next = Number.isFinite(value) ? Math.max(0, Math.floor(value)) : null;
|
|
1304
|
-
setBudgetDraft(next);
|
|
1305
|
-
// 数字框是文本输入:停手防抖落盘。
|
|
1306
|
-
scheduleSave({ budgetDraft: next });
|
|
1307
|
-
},
|
|
1308
|
-
onBlur: () => scheduleSave(null, true),
|
|
1309
|
-
}),
|
|
1310
|
-
),
|
|
1311
1346
|
// 缓存命中:0 命中说明白名单外的调用每次都真发请求,是主要浪费。
|
|
1312
1347
|
h(
|
|
1313
1348
|
"div",
|
package/lib/decision-cache.js
CHANGED
|
@@ -97,7 +97,7 @@ export class DecisionCache {
|
|
|
97
97
|
* @param getLimit - 返回当前容量的取值函数;<=0 表示禁用缓存。
|
|
98
98
|
* @param getTtlMs - 返回当前存活时间的取值函数。
|
|
99
99
|
* 两者都接收函数而非定值:配置页改了缓存参数应当立刻生效,
|
|
100
|
-
*
|
|
100
|
+
* 不该要求重启(与端点、密钥的处理方式一致)。
|
|
101
101
|
*/
|
|
102
102
|
constructor(getLimit, getTtlMs) {
|
|
103
103
|
this.getLimit = getLimit;
|