dsh-llm-fallbacks 0.2.0 → 0.2.2
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 +17 -2
- package/README.zh-CN.md +17 -2
- package/dist/automatch.d.ts +98 -0
- package/dist/client/FallbacksCard.d.ts +4 -3
- package/dist/client/fallbacks-store.d.ts +12 -6
- package/dist/client/index.d.ts +9 -7
- package/dist/client/index.js +138 -108
- package/dist/client/locales.d.ts +24 -11
- package/dist/commands.d.ts +4 -0
- package/dist/config.d.ts +13 -0
- package/dist/events.d.ts +19 -5
- package/dist/index.d.ts +1 -1
- package/dist/index.js +157 -15
- package/dist/role-resolution.d.ts +81 -0
- package/dist/roles.d.ts +8 -0
- package/package.json +20 -18
- package/scripts/repair-fallbacks-switch-logs.ts +374 -0
package/dist/client/index.js
CHANGED
|
@@ -63,7 +63,8 @@ window.__ModuleLoader__.load({
|
|
|
63
63
|
revertPolicy: "cooldown-expiry",
|
|
64
64
|
maxSwitchesPerStep: 8,
|
|
65
65
|
alwaysModeRetryCap: 5,
|
|
66
|
-
presets: "bundled"
|
|
66
|
+
presets: "bundled",
|
|
67
|
+
roleAutoMatch: true
|
|
67
68
|
};
|
|
68
69
|
/**
|
|
69
70
|
* Reserved role id: legal as a rule target (`roles.rules[].role`) and as
|
|
@@ -192,6 +193,8 @@ window.__ModuleLoader__.load({
|
|
|
192
193
|
if (presets !== void 0 && presets !== "bundled" && presets !== "none") throw new TypeError("fallbacks descriptor presets must be bundled|none");
|
|
193
194
|
const enabled = value.enabled;
|
|
194
195
|
if (enabled !== void 0 && typeof enabled !== "boolean") throw new TypeError("fallbacks descriptor enabled must be a boolean");
|
|
196
|
+
const roleAutoMatch = value.roleAutoMatch;
|
|
197
|
+
if (roleAutoMatch !== void 0 && typeof roleAutoMatch !== "boolean") throw new TypeError("fallbacks descriptor roleAutoMatch must be a boolean");
|
|
195
198
|
return {
|
|
196
199
|
enabled: enabled ?? defaultFallbacksConfig.enabled,
|
|
197
200
|
triggerCodes: triggerCodes ?? [...defaultFallbacksConfig.triggerCodes],
|
|
@@ -204,7 +207,8 @@ window.__ModuleLoader__.load({
|
|
|
204
207
|
revertPolicy: revertPolicy ?? defaultFallbacksConfig.revertPolicy,
|
|
205
208
|
maxSwitchesPerStep: maxSwitchesPerStep ?? defaultFallbacksConfig.maxSwitchesPerStep,
|
|
206
209
|
alwaysModeRetryCap: alwaysModeRetryCap ?? defaultFallbacksConfig.alwaysModeRetryCap,
|
|
207
|
-
presets: presets ?? defaultFallbacksConfig.presets
|
|
210
|
+
presets: presets ?? defaultFallbacksConfig.presets,
|
|
211
|
+
roleAutoMatch: roleAutoMatch ?? defaultFallbacksConfig.roleAutoMatch
|
|
208
212
|
};
|
|
209
213
|
}
|
|
210
214
|
/** The raw selector string a selection serializes to ('' when empty). */
|
|
@@ -262,46 +266,6 @@ window.__ModuleLoader__.load({
|
|
|
262
266
|
switches.sort((a, b) => b.seq - a.seq);
|
|
263
267
|
return switches.slice(0, limit);
|
|
264
268
|
}
|
|
265
|
-
/** The config's primary target: the rootChain's first entry (D-6 ③). */
|
|
266
|
-
function configPrimaryTarget(config) {
|
|
267
|
-
const firstEntry = config.rootChain[0];
|
|
268
|
-
if (firstEntry === void 0) return null;
|
|
269
|
-
try {
|
|
270
|
-
const selector = parseSelector(firstEntry);
|
|
271
|
-
return {
|
|
272
|
-
provider: selector.provider,
|
|
273
|
-
model: selector.model ?? "*"
|
|
274
|
-
};
|
|
275
|
-
} catch {
|
|
276
|
-
return {
|
|
277
|
-
provider: firstEntry,
|
|
278
|
-
model: "*"
|
|
279
|
-
};
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
/**
|
|
283
|
-
* Derive the status block's "current effective model" (spec §2.5 D-6): ①
|
|
284
|
-
* disabled / empty rootChain → unavailable; ② a recent switch exists → the
|
|
285
|
-
* latest one's `to`; ③ otherwise → the config's primary target. A **display
|
|
286
|
-
* value** — never a live route probe (the section appends the non-probing
|
|
287
|
-
* note inline right after the derived value, available case only; the
|
|
288
|
-
* unavailable 空态 renders its own copy without the note).
|
|
289
|
-
*/
|
|
290
|
-
function deriveEffectiveModel(config, switches) {
|
|
291
|
-
if (!config.enabled || config.rootChain.length === 0) return { kind: "unavailable" };
|
|
292
|
-
const latest = switches[0];
|
|
293
|
-
if (latest !== void 0) return {
|
|
294
|
-
kind: "switched",
|
|
295
|
-
provider: latest.to.provider,
|
|
296
|
-
model: latest.to.model
|
|
297
|
-
};
|
|
298
|
-
const target = configPrimaryTarget(config);
|
|
299
|
-
if (target === null) return { kind: "unavailable" };
|
|
300
|
-
return {
|
|
301
|
-
kind: "config",
|
|
302
|
-
...target
|
|
303
|
-
};
|
|
304
|
-
}
|
|
305
269
|
/** Serialize one selector row to its wire string (`provider/model` | `provider/*`). */
|
|
306
270
|
function selectorRowToRaw(row) {
|
|
307
271
|
const provider = selectionToRaw(row.provider);
|
|
@@ -850,8 +814,12 @@ window.__ModuleLoader__.load({
|
|
|
850
814
|
"advanced.label": "高级选项",
|
|
851
815
|
"advanced.expand": "展开高级选项",
|
|
852
816
|
"advanced.collapse": "收起高级选项",
|
|
817
|
+
"roleAutoMatch.label": "启用角色自动匹配",
|
|
818
|
+
"roleAutoMatch.hint": "规则未命中时,由模型自选最贴近的已声明角色",
|
|
819
|
+
"roleAutoMatch.tooltip": "规则未命中时,模型会自动从已声明角色(id + persona)中选择最匹配者并注入该角色的链;关闭后未命中规则时按现状回落(inherit / rootChain)。",
|
|
853
820
|
"rootChain.label": "root 主代理降级链",
|
|
854
|
-
"rootChain.
|
|
821
|
+
"rootChain.firstLine": "仅当当前会话所选模型失败后才启用降级——不抢占会话模型",
|
|
822
|
+
"rootChain.hint": "优先使用当前会话的模型;失败后才按此链降级",
|
|
855
823
|
"rootChain.tooltip": "root 主代理失败时按此有序选择器列表依次降级;未配置时行为与未安装插件一致。",
|
|
856
824
|
"rootChain.selector.add": "添加选择器",
|
|
857
825
|
"chains.selector.remove": "删除该选择器",
|
|
@@ -911,26 +879,27 @@ window.__ModuleLoader__.load({
|
|
|
911
879
|
"catalog.outside.short": " (目录外)",
|
|
912
880
|
"catalog.unconfigured.short": " (未配置)",
|
|
913
881
|
"status.title": "运行状态(只读)",
|
|
914
|
-
"status.effectiveModel.label": "当前生效模型:",
|
|
915
|
-
"status.effectiveModel.unavailable": "fallbacks 未启用(或 rootChain 未配置)",
|
|
916
|
-
"status.effectiveModel.note": "配置 + 最近切换推导,非实时路由探测",
|
|
917
882
|
"status.switches.label": "最近切换:",
|
|
918
883
|
"status.switches.empty": "本会话暂无 fallback 切换。",
|
|
919
884
|
"status.switches.error": "切换历史读取失败:{message}",
|
|
920
885
|
"status.switches.compact": "最近 {count} 次 · {from} → {to}({role} · {reason})",
|
|
886
|
+
"status.switches.compact.roleInject": "最近 {count} 次 · {role} → {to}({reason})",
|
|
921
887
|
"status.switches.reason.trigger-code": "触发失败码",
|
|
922
888
|
"status.switches.reason.always-cap": "always 模式上限",
|
|
923
|
-
"status.
|
|
889
|
+
"status.switches.reason.role-inject": "角色注入",
|
|
924
890
|
"general.title": "模型故障降级",
|
|
925
891
|
"general.enabled": "已启用",
|
|
926
892
|
"general.disabled": "未启用",
|
|
927
893
|
"general.unknown": "未知",
|
|
928
894
|
"general.unavailable": "状态通道暂不可达",
|
|
929
895
|
"general.switch": "最近切换:{from} → {to}({role} · {reason})",
|
|
896
|
+
"general.switch.roleInject": "最近切换:{role} → {to}({reason})",
|
|
930
897
|
"general.switch.empty": "本会话暂无切换",
|
|
931
898
|
"general.error": "状态读取失败:{message}",
|
|
932
899
|
"chat.switch.title": "模型已降级",
|
|
933
|
-
"chat.switch.summary": "{from} → {to}({
|
|
900
|
+
"chat.switch.summary": "{from} → {to}({reason})",
|
|
901
|
+
"chat.switch.summary.roleInject": "({reason})",
|
|
902
|
+
"chat.switch.roleMap": "{role} → {model}",
|
|
934
903
|
"defaults.prefix": "默认值",
|
|
935
904
|
"save": "保存",
|
|
936
905
|
"save.saving": "保存中…",
|
|
@@ -984,8 +953,12 @@ window.__ModuleLoader__.load({
|
|
|
984
953
|
"advanced.label": "Advanced options",
|
|
985
954
|
"advanced.expand": "Show advanced options",
|
|
986
955
|
"advanced.collapse": "Hide advanced options",
|
|
956
|
+
"roleAutoMatch.label": "Enable role auto-match",
|
|
957
|
+
"roleAutoMatch.hint": "On rules-miss, the model picks the closest declared role",
|
|
958
|
+
"roleAutoMatch.tooltip": "When no rule matches, the model auto-selects the best-fit declared role (id + persona) and uses its chain; turn off to keep today's fallback (inherit / rootChain) on a rules-miss.",
|
|
987
959
|
"rootChain.label": "Root agent fallback chain",
|
|
988
|
-
"rootChain.
|
|
960
|
+
"rootChain.firstLine": "Engages only after the current session's selected model fails — it never preempts the session model",
|
|
961
|
+
"rootChain.hint": "Prefer the current session's model; degrade down this chain only after it fails",
|
|
989
962
|
"rootChain.tooltip": "When the root agent fails it falls back down this ordered selector list; unset behaves like an uninstalled plugin.",
|
|
990
963
|
"rootChain.selector.add": "Add selector",
|
|
991
964
|
"chains.selector.remove": "Remove this selector",
|
|
@@ -1045,26 +1018,27 @@ window.__ModuleLoader__.load({
|
|
|
1045
1018
|
"catalog.outside.short": " (outside catalog)",
|
|
1046
1019
|
"catalog.unconfigured.short": " (not configured)",
|
|
1047
1020
|
"status.title": "Runtime status (read-only)",
|
|
1048
|
-
"status.effectiveModel.label": "Current effective model: ",
|
|
1049
|
-
"status.effectiveModel.unavailable": "Fallbacks disabled (or rootChain not configured)",
|
|
1050
|
-
"status.effectiveModel.note": "Derived from configuration and recent switches; not real-time route probing",
|
|
1051
1021
|
"status.switches.label": "Recent switches: ",
|
|
1052
1022
|
"status.switches.empty": "No fallback switches in this session yet.",
|
|
1053
1023
|
"status.switches.error": "Switch history read failed: {message}",
|
|
1054
1024
|
"status.switches.compact": "last {count} · {from} → {to} ({role} · {reason})",
|
|
1025
|
+
"status.switches.compact.roleInject": "last {count} · {role} → {to} ({reason})",
|
|
1055
1026
|
"status.switches.reason.trigger-code": "trigger code",
|
|
1056
1027
|
"status.switches.reason.always-cap": "always-mode cap",
|
|
1057
|
-
"status.
|
|
1028
|
+
"status.switches.reason.role-inject": "role inject",
|
|
1058
1029
|
"general.title": "Model failover",
|
|
1059
1030
|
"general.enabled": "Enabled",
|
|
1060
1031
|
"general.disabled": "Disabled",
|
|
1061
1032
|
"general.unknown": "Unknown",
|
|
1062
1033
|
"general.unavailable": "Status channel unavailable",
|
|
1063
1034
|
"general.switch": "Last switch: {from} → {to} ({role} · {reason})",
|
|
1035
|
+
"general.switch.roleInject": "Last switch: {role} → {to} ({reason})",
|
|
1064
1036
|
"general.switch.empty": "No switches this session",
|
|
1065
1037
|
"general.error": "Status read failed: {message}",
|
|
1066
1038
|
"chat.switch.title": "Model downgraded",
|
|
1067
|
-
"chat.switch.summary": "{from} → {to} ({
|
|
1039
|
+
"chat.switch.summary": "{from} → {to} ({reason})",
|
|
1040
|
+
"chat.switch.summary.roleInject": "({reason})",
|
|
1041
|
+
"chat.switch.roleMap": "{role} → {model}",
|
|
1068
1042
|
"defaults.prefix": "Default",
|
|
1069
1043
|
"save": "Save",
|
|
1070
1044
|
"save.saving": "Saving…",
|
|
@@ -1084,13 +1058,17 @@ window.__ModuleLoader__.load({
|
|
|
1084
1058
|
const NS = "fallbacks";
|
|
1085
1059
|
/**
|
|
1086
1060
|
* Reason → locale key map for switch summaries (S-c; shared by the card's
|
|
1087
|
-
* status block
|
|
1088
|
-
*
|
|
1089
|
-
*
|
|
1061
|
+
* status block, the General page status row, and the conversation node). All
|
|
1062
|
+
* reasons resolve from ONE key family (`status.switches.reason.*`) — the
|
|
1063
|
+
* shared reason vocabulary must not mix dictionary families (qc1 F-004). The
|
|
1064
|
+
* session log is durable and forward-compatible: a reason value outside the
|
|
1065
|
+
* current union (a newer plugin wrote it) renders raw instead of falling
|
|
1066
|
+
* into a binary else branch.
|
|
1090
1067
|
*/
|
|
1091
1068
|
const SWITCH_REASON_KEYS = {
|
|
1092
1069
|
"trigger-code": "status.switches.reason.trigger-code",
|
|
1093
|
-
"always-cap": "status.switches.reason.always-cap"
|
|
1070
|
+
"always-cap": "status.switches.reason.always-cap",
|
|
1071
|
+
"role-inject": "status.switches.reason.role-inject"
|
|
1094
1072
|
};
|
|
1095
1073
|
/** Human-readable trigger-code labels (spec §4 用户直观性). */
|
|
1096
1074
|
const TRIGGER_CODE_LABELS = {
|
|
@@ -1186,9 +1164,10 @@ window.__ModuleLoader__.load({
|
|
|
1186
1164
|
//#region src/client/FallbacksCard.tsx
|
|
1187
1165
|
/**
|
|
1188
1166
|
* Fallbacks settings card — the `fallbacks` plugin card on the web settings
|
|
1189
|
-
* "插件配置" page (spec §4). Registered into the `settings.plugin.item`
|
|
1190
|
-
* (
|
|
1191
|
-
* cards and the advisor card
|
|
1167
|
+
* "插件配置" page (spec §4). Registered into the `settings.plugin.item` keyed
|
|
1168
|
+
* slot (key `fallbacks`, the settings namespace the card edits, alongside
|
|
1169
|
+
* the upstream bash/agent-loop/web-search cards and the advisor card, in
|
|
1170
|
+
* registration order); owner props are empty and all data flows
|
|
1192
1171
|
* through {@link FallbacksSettingsController}.
|
|
1193
1172
|
*
|
|
1194
1173
|
* The card chrome replicates the upstream `PluginCard` contract (self-drawn:
|
|
@@ -1257,7 +1236,8 @@ window.__ModuleLoader__.load({
|
|
|
1257
1236
|
cooldownMs: config.cooldownMs,
|
|
1258
1237
|
revertPolicy: config.revertPolicy,
|
|
1259
1238
|
maxSwitchesPerStep: config.maxSwitchesPerStep,
|
|
1260
|
-
alwaysModeRetryCap: config.alwaysModeRetryCap
|
|
1239
|
+
alwaysModeRetryCap: config.alwaysModeRetryCap,
|
|
1240
|
+
roleAutoMatch: config.roleAutoMatch
|
|
1261
1241
|
};
|
|
1262
1242
|
}
|
|
1263
1243
|
/**
|
|
@@ -1269,8 +1249,14 @@ window.__ModuleLoader__.load({
|
|
|
1269
1249
|
* the top level: no presets UI this iteration (R-001 re-defer), so the
|
|
1270
1250
|
* draft carries the accepted value through untouched — a clean draft stays
|
|
1271
1251
|
* equal to the accepted config and a save never drops the key.
|
|
1252
|
+
* `roleAutoMatch` follows the same rule (config-model mirror of `presets`):
|
|
1253
|
+
* the draft carries the scalar's value through untouched. The scalar is
|
|
1254
|
+
* ALWAYS defined — the gateway composition resolves the schema default
|
|
1255
|
+
* `true` even for a legacy config that never declared the key — so the
|
|
1256
|
+
* toggle always renders (default on) and a save persists the resolved value
|
|
1257
|
+
* (AC-7 re-scope, PM decision 2026-08-17 Option A).
|
|
1272
1258
|
*/
|
|
1273
|
-
function assembleConfig(scalars, rootChainRows, roleRows, ruleRows, originalRoles, presets) {
|
|
1259
|
+
function assembleConfig(scalars, rootChainRows, roleRows, ruleRows, originalRoles, presets, roleAutoMatch) {
|
|
1274
1260
|
const list = mergeRoleExtras(roleRows, originalRoles);
|
|
1275
1261
|
return {
|
|
1276
1262
|
enabled: scalars.enabled,
|
|
@@ -1284,7 +1270,8 @@ window.__ModuleLoader__.load({
|
|
|
1284
1270
|
revertPolicy: scalars.revertPolicy,
|
|
1285
1271
|
maxSwitchesPerStep: scalars.maxSwitchesPerStep,
|
|
1286
1272
|
alwaysModeRetryCap: scalars.alwaysModeRetryCap,
|
|
1287
|
-
...presets === void 0 ? {} : { presets }
|
|
1273
|
+
...presets === void 0 ? {} : { presets },
|
|
1274
|
+
roleAutoMatch
|
|
1288
1275
|
};
|
|
1289
1276
|
}
|
|
1290
1277
|
/**
|
|
@@ -1660,18 +1647,17 @@ window.__ModuleLoader__.load({
|
|
|
1660
1647
|
return next;
|
|
1661
1648
|
});
|
|
1662
1649
|
};
|
|
1663
|
-
const draft = assembleConfig(scalars, rootChainRows, roleRows, ruleRows, state.config.roles.list, state.config.presets);
|
|
1650
|
+
const draft = assembleConfig(scalars, rootChainRows, roleRows, ruleRows, state.config.roles.list, state.config.presets, scalars.roleAutoMatch);
|
|
1664
1651
|
const hasEmptyRuleRows = ruleRows.some((row) => row.role === "");
|
|
1665
1652
|
const dirty = JSON.stringify(draft) !== JSON.stringify(state.config) || hasEmptyRuleRows;
|
|
1666
1653
|
const saving = state.status === "saving";
|
|
1667
1654
|
const writable = state.writable;
|
|
1668
1655
|
const unknownCodes = scalars.triggerCodes.filter((code) => !KNOWN_TRIGGER_CODES.includes(code));
|
|
1656
|
+
const rootChainConfigured = rowsToRootChain(rootChainRows).length > 0;
|
|
1669
1657
|
const roleOptions = ruleRoleOptions({ list: roleRows });
|
|
1670
1658
|
const invalidRoleIds = validationAttempted ? collectInvalidRoleIds(roleRows) : null;
|
|
1671
1659
|
const seededIds = /* @__PURE__ */ new Map();
|
|
1672
1660
|
for (const seed of state.seeds) seededIds.set(seed.id.trim(), seed.overridden);
|
|
1673
|
-
const effectiveModel = deriveEffectiveModel(state.config, state.switches);
|
|
1674
|
-
const effectiveModelLine = effectiveModel.kind === "unavailable" ? t("status.effectiveModel.unavailable") : `${effectiveModel.provider}/${effectiveModel.model} · ${t("status.effectiveModel.note")}`;
|
|
1675
1661
|
const latestSwitch = state.switches[0];
|
|
1676
1662
|
let switchesLine;
|
|
1677
1663
|
if (state.switchesStatus === "error") switchesLine = t("status.switches.error", { message: state.switchesError });
|
|
@@ -1679,13 +1665,14 @@ window.__ModuleLoader__.load({
|
|
|
1679
1665
|
else if (latestSwitch === void 0) switchesLine = t("status.switches.empty");
|
|
1680
1666
|
else {
|
|
1681
1667
|
const reasonKey = SWITCH_REASON_KEYS[latestSwitch.reason];
|
|
1682
|
-
|
|
1668
|
+
const params = {
|
|
1683
1669
|
count: String(state.switches.length),
|
|
1684
1670
|
from: `${latestSwitch.from.provider}/${latestSwitch.from.model}`,
|
|
1685
1671
|
to: `${latestSwitch.to.provider}/${latestSwitch.to.model}`,
|
|
1686
1672
|
role: latestSwitch.role,
|
|
1687
1673
|
reason: reasonKey === void 0 ? latestSwitch.reason : t(reasonKey)
|
|
1688
|
-
}
|
|
1674
|
+
};
|
|
1675
|
+
switchesLine = latestSwitch.reason === "role-inject" ? t("status.switches.compact.roleInject", params) : t("status.switches.compact", params);
|
|
1689
1676
|
}
|
|
1690
1677
|
const catalogSeededEpoch = (0, react.useRef)(null);
|
|
1691
1678
|
(0, react.useEffect)(() => {
|
|
@@ -1878,6 +1865,10 @@ window.__ModuleLoader__.load({
|
|
|
1878
1865
|
})]
|
|
1879
1866
|
}),
|
|
1880
1867
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1868
|
+
className: FallbacksCard_module_css_default.hint,
|
|
1869
|
+
children: t("rootChain.firstLine")
|
|
1870
|
+
}),
|
|
1871
|
+
scalars.enabled && rootChainConfigured && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1881
1872
|
className: FallbacksCard_module_css_default.hint,
|
|
1882
1873
|
children: t("rootChain.hint")
|
|
1883
1874
|
}),
|
|
@@ -2330,6 +2321,36 @@ window.__ModuleLoader__.load({
|
|
|
2330
2321
|
}), advancedVisible && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2331
2322
|
id: "fallbacks-advanced-body",
|
|
2332
2323
|
children: [
|
|
2324
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2325
|
+
className: FallbacksCard_module_css_default.checkboxRow,
|
|
2326
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2327
|
+
className: FallbacksCard_module_css_default.checkLabel,
|
|
2328
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2329
|
+
className: FallbacksCard_module_css_default.checkLabelTitle,
|
|
2330
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
2331
|
+
htmlFor: "fallbacks-role-automatch",
|
|
2332
|
+
children: t("roleAutoMatch.label")
|
|
2333
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InfoHint, {
|
|
2334
|
+
label: t("roleAutoMatch.tooltip"),
|
|
2335
|
+
disabled: !writable
|
|
2336
|
+
})]
|
|
2337
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2338
|
+
className: FallbacksCard_module_css_default.checkLabelDesc,
|
|
2339
|
+
children: t("roleAutoMatch.hint")
|
|
2340
|
+
})]
|
|
2341
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2342
|
+
id: "fallbacks-role-automatch",
|
|
2343
|
+
type: "checkbox",
|
|
2344
|
+
className: FallbacksCard_module_css_default.checkbox,
|
|
2345
|
+
checked: scalars.roleAutoMatch,
|
|
2346
|
+
disabled: !writable,
|
|
2347
|
+
onChange: (event) => {
|
|
2348
|
+
updateScalars((draft) => {
|
|
2349
|
+
draft.roleAutoMatch = event.target.checked;
|
|
2350
|
+
});
|
|
2351
|
+
}
|
|
2352
|
+
})]
|
|
2353
|
+
}),
|
|
2333
2354
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2334
2355
|
className: FallbacksCard_module_css_default.field,
|
|
2335
2356
|
role: "group",
|
|
@@ -2544,31 +2565,17 @@ window.__ModuleLoader__.load({
|
|
|
2544
2565
|
}),
|
|
2545
2566
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2546
2567
|
className: FallbacksCard_module_css_default.statusBlock,
|
|
2547
|
-
children: [
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
}),
|
|
2559
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
2560
|
-
className: FallbacksCard_module_css_default.statusLine,
|
|
2561
|
-
role: state.switchesStatus === "error" ? "alert" : void 0,
|
|
2562
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2563
|
-
className: FallbacksCard_module_css_default.statusLineLabel,
|
|
2564
|
-
children: t("status.switches.label")
|
|
2565
|
-
}), switchesLine]
|
|
2566
|
-
}),
|
|
2567
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2568
|
-
className: FallbacksCard_module_css_default.statusLine,
|
|
2569
|
-
children: t("status.selectionNote")
|
|
2570
|
-
})
|
|
2571
|
-
]
|
|
2568
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2569
|
+
className: FallbacksCard_module_css_default.statusTitle,
|
|
2570
|
+
children: t("status.title")
|
|
2571
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
2572
|
+
className: FallbacksCard_module_css_default.statusLine,
|
|
2573
|
+
role: state.switchesStatus === "error" ? "alert" : void 0,
|
|
2574
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2575
|
+
className: FallbacksCard_module_css_default.statusLineLabel,
|
|
2576
|
+
children: t("status.switches.label")
|
|
2577
|
+
}), switchesLine]
|
|
2578
|
+
})]
|
|
2572
2579
|
}),
|
|
2573
2580
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2574
2581
|
className: FallbacksCard_module_css_default.footer,
|
|
@@ -2700,12 +2707,13 @@ window.__ModuleLoader__.load({
|
|
|
2700
2707
|
else if (latestSwitch === void 0) summary = t("general.switch.empty");
|
|
2701
2708
|
else {
|
|
2702
2709
|
const reasonKey = SWITCH_REASON_KEYS[latestSwitch.reason];
|
|
2703
|
-
|
|
2710
|
+
const params = {
|
|
2704
2711
|
from: `${latestSwitch.from.provider}/${latestSwitch.from.model}`,
|
|
2705
2712
|
to: `${latestSwitch.to.provider}/${latestSwitch.to.model}`,
|
|
2706
2713
|
role: latestSwitch.role,
|
|
2707
2714
|
reason: reasonKey === void 0 ? latestSwitch.reason : t(reasonKey)
|
|
2708
|
-
}
|
|
2715
|
+
};
|
|
2716
|
+
summary = latestSwitch.reason === "role-inject" ? t("general.switch.roleInject", params) : t("general.switch", params);
|
|
2709
2717
|
}
|
|
2710
2718
|
const alert = state.status === "error" || state.switchesStatus === "error";
|
|
2711
2719
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -2754,7 +2762,7 @@ window.__ModuleLoader__.load({
|
|
|
2754
2762
|
}
|
|
2755
2763
|
//#endregion
|
|
2756
2764
|
//#region \0dsh-css:/home/runner/work/dsh-llm-fallbacks/dsh-llm-fallbacks/src/client/ConversationFallbackSwitch.module.css.mjs
|
|
2757
|
-
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-state-warn-primary);\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";
|
|
2765
|
+
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-state-warn-primary);\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\n\n._fdc977bc_roleBadge {\n flex: none;\n max-width: 96px;\n padding: 0 8px;\n border-radius: 999px;\n font-size: 12px;\n line-height: 20px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n background: var(--dsw-alias-bg-module-platform);\n color: var(--dsw-alias-label-primary);\n}\n\n\n._dce8ba28_roleModelMap {\n flex: 0 1 auto;\n min-width: 0;\n margin-left: 8px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n color: var(--dsw-alias-label-secondary);\n}\n";
|
|
2758
2766
|
const tagId = "dsh-llm-fallbacks/ConversationFallbackSwitch.module.css";
|
|
2759
2767
|
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
2760
2768
|
const tag = document.createElement("style");
|
|
@@ -2767,7 +2775,9 @@ window.__ModuleLoader__.load({
|
|
|
2767
2775
|
"switchRow": "_ea99bbef_switchRow",
|
|
2768
2776
|
"switchTitle": "_02580bfd_switchTitle",
|
|
2769
2777
|
"switchSep": "_88b2d18b_switchSep",
|
|
2770
|
-
"switchSummary": "_7c8c1b6f_switchSummary"
|
|
2778
|
+
"switchSummary": "_7c8c1b6f_switchSummary",
|
|
2779
|
+
"roleBadge": "_fdc977bc_roleBadge",
|
|
2780
|
+
"roleModelMap": "_dce8ba28_roleModelMap"
|
|
2771
2781
|
};
|
|
2772
2782
|
//#endregion
|
|
2773
2783
|
//#region src/client/ConversationFallbackSwitch.tsx
|
|
@@ -2843,12 +2853,8 @@ window.__ModuleLoader__.load({
|
|
|
2843
2853
|
})
|
|
2844
2854
|
});
|
|
2845
2855
|
const reasonKey = SWITCH_REASON_KEYS[data.reason];
|
|
2846
|
-
const
|
|
2847
|
-
|
|
2848
|
-
to: `${data.to.provider}/${data.to.model}`,
|
|
2849
|
-
role: data.role,
|
|
2850
|
-
reason: reasonKey === void 0 ? data.reason : t(reasonKey)
|
|
2851
|
-
});
|
|
2856
|
+
const reason = reasonKey === void 0 ? data.reason : t(reasonKey);
|
|
2857
|
+
const isRoleMapped = data.role !== "inherit";
|
|
2852
2858
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2853
2859
|
className: ConversationFallbackSwitch_module_css_default.switchRow,
|
|
2854
2860
|
role: "status",
|
|
@@ -2861,9 +2867,34 @@ window.__ModuleLoader__.load({
|
|
|
2861
2867
|
className: ConversationFallbackSwitch_module_css_default.switchSep,
|
|
2862
2868
|
"aria-hidden": "true"
|
|
2863
2869
|
}),
|
|
2864
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
2870
|
+
isRoleMapped ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
2871
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2872
|
+
className: ConversationFallbackSwitch_module_css_default.roleBadge,
|
|
2873
|
+
title: data.role,
|
|
2874
|
+
children: data.role
|
|
2875
|
+
}),
|
|
2876
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2877
|
+
className: ConversationFallbackSwitch_module_css_default.roleModelMap,
|
|
2878
|
+
children: t("chat.switch.roleMap", {
|
|
2879
|
+
role: data.role,
|
|
2880
|
+
model: `${data.to.provider}/${data.to.model}`
|
|
2881
|
+
})
|
|
2882
|
+
}),
|
|
2883
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2884
|
+
className: ConversationFallbackSwitch_module_css_default.switchSep,
|
|
2885
|
+
"aria-hidden": "true"
|
|
2886
|
+
}),
|
|
2887
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2888
|
+
className: ConversationFallbackSwitch_module_css_default.switchSummary,
|
|
2889
|
+
children: t("chat.switch.summary.roleInject", { reason })
|
|
2890
|
+
})
|
|
2891
|
+
] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2865
2892
|
className: ConversationFallbackSwitch_module_css_default.switchSummary,
|
|
2866
|
-
children: summary
|
|
2893
|
+
children: t("chat.switch.summary", {
|
|
2894
|
+
from: `${data.from.provider}/${data.from.model}`,
|
|
2895
|
+
to: `${data.to.provider}/${data.to.model}`,
|
|
2896
|
+
reason
|
|
2897
|
+
})
|
|
2867
2898
|
})
|
|
2868
2899
|
]
|
|
2869
2900
|
});
|
|
@@ -2945,8 +2976,7 @@ window.__ModuleLoader__.load({
|
|
|
2945
2976
|
ctx.slots.inject("settings.plugin.item", function* () {
|
|
2946
2977
|
yield ctx.slots.register({
|
|
2947
2978
|
name: "settings.plugin.item",
|
|
2948
|
-
|
|
2949
|
-
order: 30,
|
|
2979
|
+
key: "fallbacks",
|
|
2950
2980
|
locale: NS,
|
|
2951
2981
|
inject: () => ({
|
|
2952
2982
|
controller,
|
package/dist/client/locales.d.ts
CHANGED
|
@@ -37,7 +37,11 @@ export declare const zh: {
|
|
|
37
37
|
'advanced.label': string;
|
|
38
38
|
'advanced.expand': string;
|
|
39
39
|
'advanced.collapse': string;
|
|
40
|
+
'roleAutoMatch.label': string;
|
|
41
|
+
'roleAutoMatch.hint': string;
|
|
42
|
+
'roleAutoMatch.tooltip': string;
|
|
40
43
|
'rootChain.label': string;
|
|
44
|
+
'rootChain.firstLine': string;
|
|
41
45
|
'rootChain.hint': string;
|
|
42
46
|
'rootChain.tooltip': string;
|
|
43
47
|
'rootChain.selector.add': string;
|
|
@@ -98,26 +102,27 @@ export declare const zh: {
|
|
|
98
102
|
'catalog.outside.short': string;
|
|
99
103
|
'catalog.unconfigured.short': string;
|
|
100
104
|
'status.title': string;
|
|
101
|
-
'status.effectiveModel.label': string;
|
|
102
|
-
'status.effectiveModel.unavailable': string;
|
|
103
|
-
'status.effectiveModel.note': string;
|
|
104
105
|
'status.switches.label': string;
|
|
105
106
|
'status.switches.empty': string;
|
|
106
107
|
'status.switches.error': string;
|
|
107
108
|
'status.switches.compact': string;
|
|
109
|
+
'status.switches.compact.roleInject': string;
|
|
108
110
|
'status.switches.reason.trigger-code': string;
|
|
109
111
|
'status.switches.reason.always-cap': string;
|
|
110
|
-
'status.
|
|
112
|
+
'status.switches.reason.role-inject': string;
|
|
111
113
|
'general.title': string;
|
|
112
114
|
'general.enabled': string;
|
|
113
115
|
'general.disabled': string;
|
|
114
116
|
'general.unknown': string;
|
|
115
117
|
'general.unavailable': string;
|
|
116
118
|
'general.switch': string;
|
|
119
|
+
'general.switch.roleInject': string;
|
|
117
120
|
'general.switch.empty': string;
|
|
118
121
|
'general.error': string;
|
|
119
122
|
'chat.switch.title': string;
|
|
120
123
|
'chat.switch.summary': string;
|
|
124
|
+
'chat.switch.summary.roleInject': string;
|
|
125
|
+
'chat.switch.roleMap': string;
|
|
121
126
|
'defaults.prefix': string;
|
|
122
127
|
save: string;
|
|
123
128
|
'save.saving': string;
|
|
@@ -173,7 +178,11 @@ export declare const en: {
|
|
|
173
178
|
'advanced.label': string;
|
|
174
179
|
'advanced.expand': string;
|
|
175
180
|
'advanced.collapse': string;
|
|
181
|
+
'roleAutoMatch.label': string;
|
|
182
|
+
'roleAutoMatch.hint': string;
|
|
183
|
+
'roleAutoMatch.tooltip': string;
|
|
176
184
|
'rootChain.label': string;
|
|
185
|
+
'rootChain.firstLine': string;
|
|
177
186
|
'rootChain.hint': string;
|
|
178
187
|
'rootChain.tooltip': string;
|
|
179
188
|
'rootChain.selector.add': string;
|
|
@@ -234,26 +243,27 @@ export declare const en: {
|
|
|
234
243
|
'catalog.outside.short': string;
|
|
235
244
|
'catalog.unconfigured.short': string;
|
|
236
245
|
'status.title': string;
|
|
237
|
-
'status.effectiveModel.label': string;
|
|
238
|
-
'status.effectiveModel.unavailable': string;
|
|
239
|
-
'status.effectiveModel.note': string;
|
|
240
246
|
'status.switches.label': string;
|
|
241
247
|
'status.switches.empty': string;
|
|
242
248
|
'status.switches.error': string;
|
|
243
249
|
'status.switches.compact': string;
|
|
250
|
+
'status.switches.compact.roleInject': string;
|
|
244
251
|
'status.switches.reason.trigger-code': string;
|
|
245
252
|
'status.switches.reason.always-cap': string;
|
|
246
|
-
'status.
|
|
253
|
+
'status.switches.reason.role-inject': string;
|
|
247
254
|
'general.title': string;
|
|
248
255
|
'general.enabled': string;
|
|
249
256
|
'general.disabled': string;
|
|
250
257
|
'general.unknown': string;
|
|
251
258
|
'general.unavailable': string;
|
|
252
259
|
'general.switch': string;
|
|
260
|
+
'general.switch.roleInject': string;
|
|
253
261
|
'general.switch.empty': string;
|
|
254
262
|
'general.error': string;
|
|
255
263
|
'chat.switch.title': string;
|
|
256
264
|
'chat.switch.summary': string;
|
|
265
|
+
'chat.switch.summary.roleInject': string;
|
|
266
|
+
'chat.switch.roleMap': string;
|
|
257
267
|
'defaults.prefix': string;
|
|
258
268
|
save: string;
|
|
259
269
|
'save.saving': string;
|
|
@@ -279,9 +289,12 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
|
279
289
|
}
|
|
280
290
|
/**
|
|
281
291
|
* Reason → locale key map for switch summaries (S-c; shared by the card's
|
|
282
|
-
* status block
|
|
283
|
-
*
|
|
284
|
-
*
|
|
292
|
+
* status block, the General page status row, and the conversation node). All
|
|
293
|
+
* reasons resolve from ONE key family (`status.switches.reason.*`) — the
|
|
294
|
+
* shared reason vocabulary must not mix dictionary families (qc1 F-004). The
|
|
295
|
+
* session log is durable and forward-compatible: a reason value outside the
|
|
296
|
+
* current union (a newer plugin wrote it) renders raw instead of falling
|
|
297
|
+
* into a binary else branch.
|
|
285
298
|
*/
|
|
286
299
|
export declare const SWITCH_REASON_KEYS: Readonly<Partial<Record<FallbackSwitchReason, FallbacksKey>>>;
|
|
287
300
|
/** Human-readable trigger-code labels (spec §4 用户直观性). */
|
package/dist/commands.d.ts
CHANGED
|
@@ -95,6 +95,8 @@ export interface FallbacksConfigSummary {
|
|
|
95
95
|
readonly maxSwitchesPerStep: number;
|
|
96
96
|
readonly alwaysModeRetryCap: number;
|
|
97
97
|
readonly presets: 'bundled' | 'none';
|
|
98
|
+
/** Dispatch-time LLM role auto-match switch (default true). */
|
|
99
|
+
readonly roleAutoMatch: boolean;
|
|
98
100
|
}
|
|
99
101
|
/**
|
|
100
102
|
* The session-scoped read-only operations the `/fallbacks` handler drives.
|
|
@@ -148,6 +150,7 @@ export declare const FALLBACKS_COMMAND_LOCALES: {
|
|
|
148
150
|
readonly configMaxSwitches: "单步最大切换";
|
|
149
151
|
readonly configAlwaysCap: "always 上限";
|
|
150
152
|
readonly configPresets: "预置";
|
|
153
|
+
readonly configRoleAutoMatch: "角色自动匹配";
|
|
151
154
|
readonly configEdit: "编辑:~/.dsh/profiles/<profile>/cordis.patch.yml(插件行)或 $DSH_HOME/settings.yaml(fallbacks: 分节)";
|
|
152
155
|
readonly configEditHint: "TUI 无法修改配置——只能编辑文件";
|
|
153
156
|
};
|
|
@@ -185,6 +188,7 @@ export declare const FALLBACKS_COMMAND_LOCALES: {
|
|
|
185
188
|
readonly configMaxSwitches: "Max switches/step";
|
|
186
189
|
readonly configAlwaysCap: "Always-mode cap";
|
|
187
190
|
readonly configPresets: "Presets";
|
|
191
|
+
readonly configRoleAutoMatch: "Auto-match";
|
|
188
192
|
readonly configEdit: "Edit: ~/.dsh/profiles/<profile>/cordis.patch.yml (plugin row) or $DSH_HOME/settings.yaml (fallbacks: section)";
|
|
189
193
|
readonly configEditHint: "TUI cannot change config — edit files only";
|
|
190
194
|
};
|
package/dist/config.d.ts
CHANGED
|
@@ -91,6 +91,19 @@ export interface FallbacksConfig {
|
|
|
91
91
|
* every resolved config carries a value via the schema default.
|
|
92
92
|
*/
|
|
93
93
|
presets?: 'bundled' | 'none';
|
|
94
|
+
/**
|
|
95
|
+
* Dispatch-time LLM role auto-match switch (plan fallbacks-role-automatch
|
|
96
|
+
* Task 1): when `true` (default), a subagent-origin request with no
|
|
97
|
+
* explicit/rules-resolved role may have the best-fit declared role picked
|
|
98
|
+
* by the LLM; `false` reproduces today's behavior exactly. Optional on
|
|
99
|
+
* purpose, mirroring `presets` — a required field would break library
|
|
100
|
+
* consumers that construct `FallbacksConfig` literals with the existing
|
|
101
|
+
* keys (additive, non-breaking). The value domain is guarded by the
|
|
102
|
+
* schema (`z.boolean().default(true)` in `src/schema.ts`), and the
|
|
103
|
+
* runtime reads it defensively as `config.roleAutoMatch ?? true` (safe
|
|
104
|
+
* for direct constructors that omit it).
|
|
105
|
+
*/
|
|
106
|
+
roleAutoMatch?: boolean;
|
|
94
107
|
}
|
|
95
108
|
/**
|
|
96
109
|
* Spec §4 defaults — `Config({})` must equal this (no-op install).
|