dsh-llm-fallbacks 0.3.1 → 0.3.3

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/dist/index.js CHANGED
@@ -21,7 +21,7 @@ function parseSelector(input) {
21
21
  const provider = trimmed.slice(0, slash).trim();
22
22
  const modelPart = trimmed.slice(slash + 1).trim();
23
23
  if (!provider || !modelPart) throw new SelectorError(`invalid selector "${input}": empty provider or model`);
24
- if (modelPart.includes("/")) throw new SelectorError(`invalid selector "${input}": unexpected extra separator`);
24
+ if (modelPart !== "*" && modelPart.includes("*")) throw new SelectorError(`invalid selector "${input}": unexpected wildcard in model`);
25
25
  return {
26
26
  provider,
27
27
  model: modelPart === "*" ? void 0 : modelPart,
@@ -953,12 +953,17 @@ const CONFIG_SUBCOMMAND_DESCRIPTION = {
953
953
  zh: "查看组合后的 fallbacks 配置(设置回读)",
954
954
  en: "show the composed fallbacks config (settings readback)"
955
955
  };
956
+ const REVERT_SEED_SUBCOMMAND_DESCRIPTION = {
957
+ zh: "将角色的 persona 还原为已声明的 Seed 默认",
958
+ en: "revert a role's persona to its declared seed default"
959
+ };
956
960
  const FALLBACKS_COMMAND_LOCALES = {
957
961
  zh: {
958
962
  title: "当前会话 fallback 诊断(只读)",
959
963
  description: "查看当前会话的降级链、最近降级切换与冷却状态(只读)",
960
964
  usageConfig: CONFIG_SUBCOMMAND_DESCRIPTION.zh,
961
- usage: ` /fallbacks config ${CONFIG_SUBCOMMAND_DESCRIPTION.zh}`,
965
+ usageRevertSeed: REVERT_SEED_SUBCOMMAND_DESCRIPTION.zh,
966
+ usage: ` /fallbacks config ${CONFIG_SUBCOMMAND_DESCRIPTION.zh}\n /fallbacks config revert-seed <role-id> ${REVERT_SEED_SUBCOMMAND_DESCRIPTION.zh}`,
962
967
  origin: "会话来源",
963
968
  role: "角色",
964
969
  chain: "链",
@@ -982,22 +987,37 @@ const FALLBACKS_COMMAND_LOCALES = {
982
987
  configTriggerCodes: "触发码",
983
988
  configRootChain: "根链",
984
989
  configEmpty: "(空)",
990
+ configTimeSlots: "分时槽",
991
+ configSlotPresetItem: "{preset}(chain: {n}, window {window})",
992
+ configSlotPresetBare: "{preset}(chain: {n})",
993
+ configSlotCustomItem: "custom {start}-{end}(chain: {n})",
994
+ configTz: "时区",
985
995
  configRoles: "角色",
986
996
  configRoleItem: "{id}(chain: {n})",
997
+ configRules: "角色规则",
987
998
  configCooldown: "冷却",
988
999
  configRevert: "回主策略",
989
1000
  configMaxSwitches: "单步最大切换",
990
1001
  configAlwaysCap: "always 上限",
991
1002
  configPresets: "预置",
992
1003
  configRoleAutoMatch: "角色自动匹配",
993
- configEdit: "编辑:~/.dsh/profiles/<profile>/cordis.patch.yml(插件行)或 $DSH_HOME/settings.yaml(fallbacks: 分节)",
994
- configEditHint: "TUI 无法修改配置——只能编辑文件"
1004
+ configEdit: "编辑:/settings(TUI 设置界面)或 ~/.dsh/profiles/<profile>/cordis.patch.yml(插件行)/ $DSH_HOME/settings.yaml(fallbacks: 分节)",
1005
+ configEditHint: "TUI 通过 /settings 修改配置;文件编辑仍然可用",
1006
+ revertSeedOk: "角色 {id} 已还原为 Seed 默认",
1007
+ revertSeedFail: "角色 {id} 未还原({reason})",
1008
+ revertSeedError: "角色 {id} 还原失败(设置写入失败)",
1009
+ revertSeedReason: {
1010
+ "not-seeded": "未声明种子",
1011
+ "row-absent": "角色行不存在",
1012
+ "settings-unavailable": "设置通道不可用"
1013
+ }
995
1014
  },
996
1015
  en: {
997
1016
  title: "Session fallback diagnostics (read-only)",
998
1017
  description: "Inspect fallback chain, recent fallback switches, and cooldown for this session (read-only)",
999
1018
  usageConfig: CONFIG_SUBCOMMAND_DESCRIPTION.en,
1000
- usage: ` /fallbacks config ${CONFIG_SUBCOMMAND_DESCRIPTION.en}`,
1019
+ usageRevertSeed: REVERT_SEED_SUBCOMMAND_DESCRIPTION.en,
1020
+ usage: ` /fallbacks config ${CONFIG_SUBCOMMAND_DESCRIPTION.en}\n /fallbacks config revert-seed <role-id> ${REVERT_SEED_SUBCOMMAND_DESCRIPTION.en}`,
1001
1021
  origin: "Session origin",
1002
1022
  role: "Role",
1003
1023
  chain: "Chain",
@@ -1021,20 +1041,45 @@ const FALLBACKS_COMMAND_LOCALES = {
1021
1041
  configTriggerCodes: "Trigger codes",
1022
1042
  configRootChain: "Root chain",
1023
1043
  configEmpty: "(empty)",
1044
+ configTimeSlots: "Time slots",
1045
+ configSlotPresetItem: "{preset} (chain: {n}, window {window})",
1046
+ configSlotPresetBare: "{preset} (chain: {n})",
1047
+ configSlotCustomItem: "custom {start}-{end} (chain: {n})",
1048
+ configTz: "TZ",
1024
1049
  configRoles: "Roles",
1025
1050
  configRoleItem: "{id} (chain: {n})",
1051
+ configRules: "Rules",
1026
1052
  configCooldown: "Cooldown",
1027
1053
  configRevert: "Revert",
1028
1054
  configMaxSwitches: "Max switches/step",
1029
1055
  configAlwaysCap: "Always-mode cap",
1030
1056
  configPresets: "Presets",
1031
1057
  configRoleAutoMatch: "Auto-match",
1032
- configEdit: "Edit: ~/.dsh/profiles/<profile>/cordis.patch.yml (plugin row) or $DSH_HOME/settings.yaml (fallbacks: section)",
1033
- configEditHint: "TUI cannot change config — edit files only"
1058
+ configEdit: "Edit: /settings (TUI settings screen) or ~/.dsh/profiles/<profile>/cordis.patch.yml (plugin row) / $DSH_HOME/settings.yaml (fallbacks: section)",
1059
+ configEditHint: "TUI edits config via /settings; file editing still works",
1060
+ revertSeedOk: "role {id} reverted to its seed default",
1061
+ revertSeedFail: "role {id} not reverted ({reason})",
1062
+ revertSeedError: "role {id} revert failed (settings write failed)",
1063
+ revertSeedReason: {
1064
+ "not-seeded": "not a seeded role",
1065
+ "row-absent": "role row absent",
1066
+ "settings-unavailable": "settings channel unavailable"
1067
+ }
1034
1068
  }
1035
1069
  };
1036
1070
  function parseFallbacksSubcommand(rawInput) {
1037
- return rawInput.trim() === "config" ? "config" : "";
1071
+ const trimmed = rawInput.trim();
1072
+ if (trimmed === "config") return { kind: "config" };
1073
+ const [head, sub, ...rest] = trimmed.split(/\s+/);
1074
+ if (head === "config" && sub === "revert-seed") {
1075
+ const arg = rest.join(" ").trim();
1076
+ if (arg === "") return { kind: "" };
1077
+ return {
1078
+ kind: "revert-seed",
1079
+ arg
1080
+ };
1081
+ }
1082
+ return { kind: "" };
1038
1083
  }
1039
1084
  function isFallbacksSwitchData(data) {
1040
1085
  if (typeof data !== "object" || data === null) return false;
@@ -1090,13 +1135,72 @@ function formatConfigRoles(roles, t) {
1090
1135
  const list = items.length === 0 ? "" : `${items.join(", ")}${roles.length > 5 ? ", …" : ""}`;
1091
1136
  return `${roles.length}${list.length === 0 ? "" : ` — ${list}`}`;
1092
1137
  }
1138
+ const DAY_NAMES = [
1139
+ "Sun",
1140
+ "Mon",
1141
+ "Tue",
1142
+ "Wed",
1143
+ "Thu",
1144
+ "Fri",
1145
+ "Sat"
1146
+ ];
1147
+ function formatDayMask(days) {
1148
+ if (days.length === 0) return "";
1149
+ const sorted = days.filter((day) => Number.isInteger(day) && day >= 0 && day <= 6).sort((a, b) => a - b);
1150
+ if (sorted.length === 0) return "";
1151
+ const runs = [];
1152
+ let runStart = sorted[0];
1153
+ let prev = sorted[0];
1154
+ for (let index = 1; index <= sorted.length; index += 1) {
1155
+ const day = sorted[index];
1156
+ if (day === void 0 || day !== prev + 1) {
1157
+ runs.push(prev === runStart ? DAY_NAMES[runStart] : `${DAY_NAMES[runStart]}-${DAY_NAMES[prev]}`);
1158
+ runStart = day;
1159
+ prev = day;
1160
+ } else prev = day;
1161
+ }
1162
+ return runs.join(", ");
1163
+ }
1164
+ function formatDayMaskSegment(days) {
1165
+ const mask = formatDayMask(days ?? []);
1166
+ return mask === "" ? "" : ` (${mask})`;
1167
+ }
1168
+ function formatSlotWindow(preset) {
1169
+ const definition = PRESETS[preset];
1170
+ if (definition === void 0) return "";
1171
+ const windows = definition.windows.map((window) => {
1172
+ const dayMask = formatDayMaskSegment(window.days);
1173
+ return `${window.start}-${window.end}${dayMask}`;
1174
+ }).join(", ");
1175
+ return definition.complement ? `outside ${windows}` : windows;
1176
+ }
1177
+ function formatConfigTimeSlots(slots, t) {
1178
+ const items = slots.slice(0, 5).map((row) => {
1179
+ if (row.preset !== void 0) {
1180
+ const window = formatSlotWindow(row.preset);
1181
+ return window === "" ? t.configSlotPresetBare.replace("{preset}", row.preset).replace("{n}", String(row.chainCount)) : t.configSlotPresetItem.replace("{preset}", row.preset).replace("{n}", String(row.chainCount)).replace("{window}", window);
1182
+ }
1183
+ const bounds = row.start !== void 0 && row.end !== void 0 ? `${row.start}-${row.end}${formatDayMaskSegment(row.days)}` : "";
1184
+ return t.configSlotCustomItem.replace("{start}-{end}", bounds).replace("{n}", String(row.chainCount));
1185
+ });
1186
+ const list = items.length === 0 ? "" : `${items.join(", ")}${slots.length > 5 ? ", …" : ""}`;
1187
+ return `${slots.length}${list.length === 0 ? "" : ` — ${list}`}`;
1188
+ }
1189
+ function formatConfigRules(rules, t) {
1190
+ const items = rules.slice(0, 5).map((rule) => `${rule.provider === "" ? "*" : rule.provider}/${rule.model === "" ? "*" : rule.model} → ${rule.role}`);
1191
+ const list = items.length === 0 ? "" : `${items.join(", ")}${rules.length > 5 ? ", …" : ""}`;
1192
+ return `${rules.length}${list.length === 0 ? "" : ` — ${list}`}`;
1193
+ }
1093
1194
  function fallbacksConfigText(summary, locale = "zh") {
1094
1195
  const t = FALLBACKS_COMMAND_LOCALES[locale];
1095
1196
  return [
1096
1197
  `${t.configTitle}: ${summary.enabled ? t.configEnabled : t.configDisabled}`,
1097
1198
  `${t.configTriggerCodes}: ${summary.triggerCodes.length === 0 ? t.configEmpty : formatConfigList(summary.triggerCodes)}`,
1098
1199
  `${t.configRootChain}: ${summary.rootChain.length === 0 ? t.configEmpty : formatConfigList(summary.rootChain)}`,
1200
+ `${t.configTimeSlots}: ${summary.timeSlots.length === 0 ? t.configEmpty : formatConfigTimeSlots(summary.timeSlots, t)}`,
1201
+ `${t.configTz}: ${summary.tz}`,
1099
1202
  `${t.configRoles}: ${formatConfigRoles(summary.roles, t)}`,
1203
+ `${t.configRules}: ${summary.rules.length === 0 ? t.configEmpty : formatConfigRules(summary.rules, t)}`,
1100
1204
  `${t.configCooldown}: ${summary.cooldownMs} ms`,
1101
1205
  `${t.configRevert}: ${summary.revertPolicy}`,
1102
1206
  `${t.configMaxSwitches}: ${summary.maxSwitchesPerStep}`,
@@ -1132,10 +1236,31 @@ function fallbacksCommandText(snapshot, locale = "zh") {
1132
1236
  return lines.join("\n");
1133
1237
  }
1134
1238
  function createFallbacksCommandHandler(controller, locale = "zh") {
1135
- return (invocation) => ({
1136
- kind: "success",
1137
- text: parseFallbacksSubcommand(invocation.rawInput ?? "") === "config" ? fallbacksConfigText(controller.getConfig(), locale) : fallbacksCommandText(controller.getSnapshot(invocation.agent), locale)
1138
- });
1239
+ return (invocation) => {
1240
+ const parsed = parseFallbacksSubcommand(invocation.rawInput ?? "");
1241
+ if (parsed.kind === "config") return {
1242
+ kind: "success",
1243
+ text: fallbacksConfigText(controller.getConfig(), locale)
1244
+ };
1245
+ if (parsed.kind === "revert-seed" && parsed.arg !== void 0) {
1246
+ const roleId = parsed.arg;
1247
+ const t = FALLBACKS_COMMAND_LOCALES[locale];
1248
+ return controller.revertSeed(roleId).then((outcome) => outcome.ok ? {
1249
+ kind: "success",
1250
+ text: t.revertSeedOk.replace("{id}", roleId)
1251
+ } : {
1252
+ kind: "error",
1253
+ text: t.revertSeedFail.replace("{id}", roleId).replace("{reason}", t.revertSeedReason[outcome.reason ?? "not-seeded"])
1254
+ }).catch(() => ({
1255
+ kind: "error",
1256
+ text: t.revertSeedError.replace("{id}", roleId)
1257
+ }));
1258
+ }
1259
+ return {
1260
+ kind: "success",
1261
+ text: fallbacksCommandText(controller.getSnapshot(invocation.agent), locale)
1262
+ };
1263
+ };
1139
1264
  }
1140
1265
  function registerFallbacksCommands(registry, controller, locale = "zh") {
1141
1266
  return registry.register({
@@ -1369,9 +1494,18 @@ const FALLBACKS_CONFIG_NODE = {
1369
1494
  en: FALLBACKS_COMMAND_LOCALES.en.usageConfig
1370
1495
  }
1371
1496
  };
1497
+ const REVERT_SEED_NODE = {
1498
+ name: "revert-seed",
1499
+ description: FALLBACKS_COMMAND_LOCALES.zh.usageRevertSeed,
1500
+ descriptions: {
1501
+ zh: FALLBACKS_COMMAND_LOCALES.zh.usageRevertSeed,
1502
+ en: FALLBACKS_COMMAND_LOCALES.en.usageRevertSeed
1503
+ }
1504
+ };
1372
1505
  function fallbacksChildren(canonicalPath) {
1373
1506
  if (canonicalPath[0] !== "fallbacks") return [];
1374
1507
  if (canonicalPath.length === 1) return [FALLBACKS_CONFIG_NODE];
1508
+ if (canonicalPath.length === 2 && canonicalPath[1] === "config") return [REVERT_SEED_NODE];
1375
1509
  return [];
1376
1510
  }
1377
1511
  const FALLBACKS_PROVIDER$1 = {
@@ -1397,6 +1531,309 @@ function installTuiClient(ctx, opts) {
1397
1531
  });
1398
1532
  }
1399
1533
  //#endregion
1534
+ //#region .build/host/tui-settings.js
1535
+ const FALLBACKS_TUI_SECTION_NS = "fallbacks";
1536
+ function jsonFieldFormat(value) {
1537
+ return value === void 0 || value === null ? "" : JSON.stringify(value, null, 2);
1538
+ }
1539
+ function jsonFieldParse(patch) {
1540
+ return (text) => {
1541
+ if (text.trim() === "") return { kind: "clear" };
1542
+ if (new TextEncoder().encode(text).length > 65536) return void 0;
1543
+ let parsed;
1544
+ try {
1545
+ parsed = JSON.parse(text);
1546
+ } catch {
1547
+ return;
1548
+ }
1549
+ if (parsed === null) return { kind: "clear" };
1550
+ try {
1551
+ validateConfigPatch(patch(parsed));
1552
+ } catch {
1553
+ return;
1554
+ }
1555
+ return {
1556
+ kind: "set",
1557
+ value: parsed
1558
+ };
1559
+ };
1560
+ }
1561
+ function triggerCodesFormat(value) {
1562
+ return Array.isArray(value) ? value.join(", ") : "";
1563
+ }
1564
+ function triggerCodesParse(text) {
1565
+ if (text.trim() === "") return { kind: "clear" };
1566
+ const tokens = text.split(",").map((token) => token.trim()).filter((token) => token !== "");
1567
+ try {
1568
+ validateConfigPatch({ triggerCodes: tokens });
1569
+ } catch {
1570
+ return;
1571
+ }
1572
+ return {
1573
+ kind: "set",
1574
+ value: tokens
1575
+ };
1576
+ }
1577
+ function tzParse(text) {
1578
+ const trimmed = text.trim();
1579
+ if (trimmed === "") return { kind: "clear" };
1580
+ try {
1581
+ Intl.DateTimeFormat(void 0, { timeZone: trimmed });
1582
+ } catch {
1583
+ return;
1584
+ }
1585
+ return {
1586
+ kind: "set",
1587
+ value: trimmed
1588
+ };
1589
+ }
1590
+ const BUNDLED_PRESETS_LABEL = `Bundled (${presetRoles.length} preset roles)`;
1591
+ function buildFallbacksTuiSection() {
1592
+ return {
1593
+ ns: FALLBACKS_TUI_SECTION_NS,
1594
+ title: "fallbacks",
1595
+ descriptions: {
1596
+ zh: "回退设置:降级链、分时切换、角色与高级选项(与 Web 设置卡片完全一致)。",
1597
+ en: "Fallback settings: degradation chains, time slots, roles, and advanced options (full parity with the web settings card)."
1598
+ },
1599
+ groups: [
1600
+ {
1601
+ id: "general",
1602
+ title: "General",
1603
+ descriptions: {
1604
+ zh: "通用",
1605
+ en: "General"
1606
+ }
1607
+ },
1608
+ {
1609
+ id: "chain",
1610
+ title: "Fallback chain",
1611
+ descriptions: {
1612
+ zh: "降级链",
1613
+ en: "Fallback chain"
1614
+ }
1615
+ },
1616
+ {
1617
+ id: "roles",
1618
+ title: "Roles",
1619
+ descriptions: {
1620
+ zh: "角色",
1621
+ en: "Roles"
1622
+ }
1623
+ },
1624
+ {
1625
+ id: "advanced",
1626
+ title: "Advanced",
1627
+ descriptions: {
1628
+ zh: "高级",
1629
+ en: "Advanced"
1630
+ }
1631
+ }
1632
+ ],
1633
+ fields: [
1634
+ {
1635
+ path: ["enabled"],
1636
+ label: "Enabled",
1637
+ descriptions: {
1638
+ zh: "启用回退",
1639
+ en: "Enabled"
1640
+ },
1641
+ group: "general",
1642
+ kind: "boolean"
1643
+ },
1644
+ {
1645
+ path: ["roleAutoMatch"],
1646
+ label: "LLM role auto-match",
1647
+ descriptions: {
1648
+ zh: "LLM 角色自动匹配",
1649
+ en: "LLM role auto-match"
1650
+ },
1651
+ group: "general",
1652
+ kind: "boolean"
1653
+ },
1654
+ {
1655
+ path: ["presets"],
1656
+ label: "Preset roles",
1657
+ descriptions: {
1658
+ zh: "预置角色",
1659
+ en: "Preset roles"
1660
+ },
1661
+ group: "general",
1662
+ kind: "select",
1663
+ options: [{
1664
+ value: "bundled",
1665
+ label: BUNDLED_PRESETS_LABEL,
1666
+ descriptions: {
1667
+ zh: `预置(${presetRoles.length} 个预置角色)`,
1668
+ en: BUNDLED_PRESETS_LABEL
1669
+ }
1670
+ }, {
1671
+ value: "none",
1672
+ label: "None",
1673
+ descriptions: {
1674
+ zh: "不注入",
1675
+ en: "None"
1676
+ }
1677
+ }]
1678
+ },
1679
+ {
1680
+ path: ["triggerCodes"],
1681
+ label: "Trigger codes",
1682
+ descriptions: {
1683
+ zh: "触发码",
1684
+ en: "Trigger codes"
1685
+ },
1686
+ hint: "Comma-separated failure codes that trigger the fallback. A code containing a comma cannot round-trip through this field.",
1687
+ hintDescriptions: {
1688
+ zh: "逗号分隔的触发回退的失败码;包含逗号的代码无法通过该字段往返。",
1689
+ en: "Comma-separated failure codes that trigger the fallback. A code containing a comma cannot round-trip through this field."
1690
+ },
1691
+ group: "general",
1692
+ kind: "text",
1693
+ format: triggerCodesFormat,
1694
+ parse: triggerCodesParse
1695
+ },
1696
+ {
1697
+ path: ["tz"],
1698
+ label: "Timezone",
1699
+ descriptions: {
1700
+ zh: "时区",
1701
+ en: "Timezone"
1702
+ },
1703
+ hint: "IANA timezone identifier, e.g. Asia/Shanghai.",
1704
+ hintDescriptions: {
1705
+ zh: "IANA 时区标识符,如 Asia/Shanghai。",
1706
+ en: "IANA timezone identifier, e.g. Asia/Shanghai."
1707
+ },
1708
+ group: "general",
1709
+ kind: "text",
1710
+ parse: tzParse
1711
+ },
1712
+ {
1713
+ path: ["rootChain"],
1714
+ label: "Root chain (JSON)",
1715
+ descriptions: {
1716
+ zh: "根降级链(JSON)",
1717
+ en: "Root chain (JSON)"
1718
+ },
1719
+ hint: "JSON array of model selectors; the last entry must be an official V4 model.",
1720
+ hintDescriptions: {
1721
+ zh: "模型选择器 JSON 数组;末项必须是官方 V4 模型。",
1722
+ en: "JSON array of model selectors; the last entry must be an official V4 model."
1723
+ },
1724
+ group: "chain",
1725
+ kind: "text",
1726
+ format: jsonFieldFormat,
1727
+ parse: jsonFieldParse((parsed) => ({ rootChain: parsed }))
1728
+ },
1729
+ {
1730
+ path: ["timeSlots"],
1731
+ label: "Time slots (JSON)",
1732
+ descriptions: {
1733
+ zh: "分时切换(JSON)",
1734
+ en: "Time slots (JSON)"
1735
+ },
1736
+ hint: "JSON array of slot rows (kind: preset|custom).",
1737
+ hintDescriptions: {
1738
+ zh: "分时行 JSON 数组(kind: preset|custom)。",
1739
+ en: "JSON array of slot rows (kind: preset|custom)."
1740
+ },
1741
+ group: "chain",
1742
+ kind: "text",
1743
+ format: jsonFieldFormat,
1744
+ parse: jsonFieldParse((parsed) => ({ timeSlots: parsed }))
1745
+ },
1746
+ {
1747
+ path: ["roles", "list"],
1748
+ label: "Roles (JSON)",
1749
+ descriptions: {
1750
+ zh: "角色列表(JSON)",
1751
+ en: "Roles (JSON)"
1752
+ },
1753
+ group: "roles",
1754
+ kind: "text",
1755
+ format: jsonFieldFormat,
1756
+ parse: jsonFieldParse((parsed) => ({ roles: { list: parsed } }))
1757
+ },
1758
+ {
1759
+ path: ["roles", "rules"],
1760
+ label: "Role rules (JSON)",
1761
+ descriptions: {
1762
+ zh: "角色规则(JSON)",
1763
+ en: "Role rules (JSON)"
1764
+ },
1765
+ group: "roles",
1766
+ kind: "text",
1767
+ format: jsonFieldFormat,
1768
+ parse: jsonFieldParse((parsed) => ({ roles: { rules: parsed } }))
1769
+ },
1770
+ {
1771
+ path: ["cooldownMs"],
1772
+ label: "Cooldown (ms)",
1773
+ descriptions: {
1774
+ zh: "冷却时间(毫秒)",
1775
+ en: "Cooldown (ms)"
1776
+ },
1777
+ group: "advanced",
1778
+ kind: "number"
1779
+ },
1780
+ {
1781
+ path: ["maxSwitchesPerStep"],
1782
+ label: "Max switches per step",
1783
+ descriptions: {
1784
+ zh: "单步最大切换次数",
1785
+ en: "Max switches per step"
1786
+ },
1787
+ group: "advanced",
1788
+ kind: "number"
1789
+ },
1790
+ {
1791
+ path: ["alwaysModeRetryCap"],
1792
+ label: "Always-mode retry cap",
1793
+ descriptions: {
1794
+ zh: "Always 模式重试上限",
1795
+ en: "Always-mode retry cap"
1796
+ },
1797
+ group: "advanced",
1798
+ kind: "number"
1799
+ },
1800
+ {
1801
+ path: ["revertPolicy"],
1802
+ label: "Revert policy",
1803
+ descriptions: {
1804
+ zh: "恢复策略",
1805
+ en: "Revert policy"
1806
+ },
1807
+ group: "advanced",
1808
+ kind: "select",
1809
+ options: [{
1810
+ value: "cooldown-expiry",
1811
+ label: "Cooldown expiry",
1812
+ descriptions: {
1813
+ zh: "冷却到期",
1814
+ en: "Cooldown expiry"
1815
+ }
1816
+ }, {
1817
+ value: "never",
1818
+ label: "Never",
1819
+ descriptions: {
1820
+ zh: "从不",
1821
+ en: "Never"
1822
+ }
1823
+ }]
1824
+ }
1825
+ ]
1826
+ };
1827
+ }
1828
+ function installTuiSettingsSection(ctx, opts) {
1829
+ if (!opts.serviceOwned) return;
1830
+ ctx.inject(["tuiSettingsSections"], (tctx) => {
1831
+ const registry = tctx.tuiSettingsSections;
1832
+ if (registry === void 0) return;
1833
+ return registry.register(buildFallbacksTuiSection());
1834
+ });
1835
+ }
1836
+ //#endregion
1400
1837
  //#region .build/host/virtual-adapter.js
1401
1838
  const FALLBACKS_PROVIDER = "FallbacksChain";
1402
1839
  const FALLBACKS_CHAIN_MODEL = "Auto";
@@ -1836,10 +2273,25 @@ function apply(ctx, config = defaultFallbacksConfig) {
1836
2273
  enabled: config.enabled,
1837
2274
  triggerCodes: config.triggerCodes,
1838
2275
  rootChain: config.rootChain,
2276
+ timeSlots: (config.timeSlots ?? []).map((row) => row.kind === "preset" ? {
2277
+ preset: row.preset,
2278
+ chainCount: (row.chain ?? []).length
2279
+ } : {
2280
+ start: row.start,
2281
+ end: row.end,
2282
+ days: row.days,
2283
+ chainCount: (row.chain ?? []).length
2284
+ }),
2285
+ tz: config.tz ?? "Asia/Shanghai",
1839
2286
  roles: config.roles.list.map((role) => ({
1840
2287
  id: role.id,
1841
2288
  chainCount: role.chain?.length ?? 0
1842
2289
  })),
2290
+ rules: config.roles.rules.map((rule) => ({
2291
+ provider: rule.provider ?? "",
2292
+ model: rule.model ?? "",
2293
+ role: rule.role
2294
+ })),
1843
2295
  cooldownMs: config.cooldownMs,
1844
2296
  revertPolicy: config.revertPolicy,
1845
2297
  maxSwitchesPerStep: config.maxSwitchesPerStep,
@@ -1847,12 +2299,21 @@ function apply(ctx, config = defaultFallbacksConfig) {
1847
2299
  presets: config.presets ?? "bundled",
1848
2300
  roleAutoMatch: config.roleAutoMatch ?? true
1849
2301
  };
2302
+ },
2303
+ revertSeed: async (roleId) => {
2304
+ const outcome = await seeds.revert(roleId, seedsIo);
2305
+ if (outcome.reverted) return { ok: true };
2306
+ return {
2307
+ ok: false,
2308
+ reason: outcome.reason ?? "not-seeded"
2309
+ };
1850
2310
  }
1851
2311
  };
1852
2312
  ctx.inject(["commands"], (commandCtx) => {
1853
2313
  return registerFallbacksCommands(commandCtx.commands, fallbacksCommandController);
1854
2314
  });
1855
2315
  installTuiClient(ctx, { serviceOwned });
2316
+ installTuiSettingsSection(ctx, { serviceOwned });
1856
2317
  ctx.inject(["settings"], () => {
1857
2318
  if (!serviceOwned) return;
1858
2319
  if (seedsIo.read().presets === "none") return;
@@ -1,8 +1,10 @@
1
1
  /**
2
2
  * Selector parsing for `fallbacks` chains (spec §4, plan Task 2).
3
3
  *
4
- * Grammar: `provider/model` (exact) and `provider/*` (wildcard — the parsed
5
- * `model` is `undefined`). Illegal selectors throw {@link SelectorError} —
4
+ * Grammar: `provider/model` (exact — the model segment may itself contain
5
+ * `/`, e.g. NVIDIA NIM `nvidia/minimaxai/minimax-m3`) and `provider/*`
6
+ * (wildcard — the parsed `model` is `undefined`; `*` is only valid as the
7
+ * entire model segment). Illegal selectors throw {@link SelectorError} —
6
8
  * the catchable "config warning" path; warn-and-continue lives in Task 3.
7
9
  * These modules never crash on their own.
8
10
  *
@@ -25,7 +27,8 @@ export declare function selectorKey(provider: string, model?: string): string;
25
27
  * Parse a chain key or entry selector.
26
28
  *
27
29
  * Accepts `provider/model` and `provider/*`; throws {@link SelectorError}
28
- * on anything else (missing separator, empty parts, extra separators).
30
+ * on anything else (missing separator, empty parts, wildcard inside the
31
+ * model segment).
29
32
  */
30
33
  export declare function parseSelector(input: string): Selector;
31
34
  /**