dsh-plugin-litellm-bridge 2.0.6 → 2.0.8

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.
@@ -582,6 +582,29 @@ window.__ModuleLoader__.load({
582
582
  }
583
583
  }, [draft, pid]);
584
584
 
585
+ // 模型池按供应商分组,供下拉与"添加"列表使用
586
+ //
587
+ // 【React #310 血案】这个 useMemo 原来写在下面两个提前 return **之后**:
588
+ // if (!draft) return ...; ← 首次渲染 draft=null,从这里返回
589
+ // if (!prof) return ...;
590
+ // const byProvider = useMemo(...) ← 首次渲染根本执行不到
591
+ // 第二次渲染(routes/get 返回后 draft 有值)才执行到这一行,
592
+ // hooks 数量从 N 变成 N+1 → React 抛
593
+ // "Rendered more hooks than during the previous render."(#310)
594
+ // 表现:点「路由配置」→ 整页空白。
595
+ //
596
+ // 规则:**所有 hooks 必须写在任何条件 return 之前**,保证每次渲染
597
+ // 执行完全一样多的 hooks。所以把它移到这里。
598
+ const byProvider = useMemo(() => {
599
+ const m = new Map();
600
+ for (const x of pool.models || []) {
601
+ const key = x.provider || '未知供应商';
602
+ if (!m.has(key)) m.set(key, []);
603
+ m.get(key).push(x);
604
+ }
605
+ return [...m.entries()];
606
+ }, [pool.models]);
607
+
585
608
  // 加载失败时给出可读原因 + 重试,而不是无限「加载中…」
586
609
  if (!draft) {
587
610
  return h('div', null,
@@ -596,8 +619,16 @@ window.__ModuleLoader__.load({
596
619
  : h('div', { style: { opacity: 0.6 } }, '加载中…'),
597
620
  );
598
621
  }
599
- const prof = draft[pid];
600
- if (!prof) return h('div', { style: { opacity: 0.6 } }, '没有可用的 profile');
622
+ // 【防御】prof 可能是空对象或残缺结构。
623
+ // 先判断存在性再往下渲染,否则后面读 prof.agents[at] 会抛 TypeError。
624
+ const prof = draft?.[pid];
625
+ if (!prof || typeof prof !== 'object') {
626
+ return h('div', { style: S.notice('warn') },
627
+ `profile「${pid}」的数据不完整(缺少 agents 配置)。`,
628
+ h('div', { style: { ...S.hint } },
629
+ '可能来自旧版本的状态文件。可以在上方点击「+ 新建」创建新 profile,' +
630
+ '或删除当前 profile 后重建。'));
631
+ }
601
632
 
602
633
  const setAgent = (at, patch) =>
603
634
  setDraft((d) => {
@@ -692,17 +723,6 @@ window.__ModuleLoader__.load({
692
723
  setBusy(false);
693
724
  };
694
725
 
695
- // 模型池按供应商分组,供下拉与"添加"列表使用
696
- const byProvider = useMemo(() => {
697
- const m = new Map();
698
- for (const x of pool.models || []) {
699
- const key = x.provider || '未知供应商';
700
- if (!m.has(key)) m.set(key, []);
701
- m.get(key).push(x);
702
- }
703
- return [...m.entries()];
704
- }, [pool.models]);
705
-
706
726
  const allIds = (pool.models || []).map((x) => x.id);
707
727
 
708
728
  return h('div', null,
@@ -750,7 +770,10 @@ window.__ModuleLoader__.load({
750
770
 
751
771
  // ---- 三条链:root / coder / research ----
752
772
  AGENT_TYPES.map((at) => {
753
- const cfg = prof.agents[at];
773
+ // 【防御】cfg 可能缺失(旧版 state.json 结构不全 / 宿主返回残缺)。
774
+ // 直接读 cfg.primary 会抛 TypeError,进而让整页崩掉。
775
+ const cfg = prof?.agents?.[at]
776
+ || { label: at, primary: '', timeoutMs: 600000, fallbacks: [] };
754
777
  const isSub = at !== 'root';
755
778
  const groupLabel = at === 'root' ? 'root · 主代理降级链'
756
779
  : at === 'coder' ? 'coder · 子代理降级链(Coder)'
@@ -1275,12 +1298,47 @@ window.__ModuleLoader__.load({
1275
1298
  );
1276
1299
  }
1277
1300
 
1278
- /** 用 try/catch 包住子渲染,异常时就地降级为错误块。 */
1279
- function Guard({ title, children }) {
1280
- try {
1281
- return children();
1282
- } catch (err) {
1283
- return h(ErrorBox, { title, error: err });
1301
+ /**
1302
+ * 错误边界 —— 必须是**真正的 React class 错误边界**。
1303
+ *
1304
+ * 【为什么之前那版是无效的】
1305
+ * 原来写的是:
1306
+ * function Guard({ title, children }) {
1307
+ * try { return children(); } catch (err) { ... }
1308
+ * }
1309
+ * 用法:h(Guard, {}, () => h(RouteTab, {...}))
1310
+ *
1311
+ * 问题:children() 只是**创建元素**(h() 是纯函数,返回一个普通对象),
1312
+ * 此时并不会渲染 RouteTab。真正的渲染发生在 Guard 返回之后、React 内部。
1313
+ * 所以 RouteTab 抛的异常**根本不在这个 try 里**,catch 永远抓不到,
1314
+ * React 直接向上冒泡 → 整个设置页空白(连错误框都没有)。
1315
+ * 这正是「点路由配置后整页全空、什么都不显示」的原因。
1316
+ *
1317
+ * 正确做法:class 组件的 getDerivedStateFromError / componentDidCatch,
1318
+ * 这是 React 唯一能捕获子组件渲染异常的机制。
1319
+ */
1320
+ class Guard extends React.Component {
1321
+ constructor(props) {
1322
+ super(props);
1323
+ this.state = { error: null };
1324
+ }
1325
+ static getDerivedStateFromError(error) {
1326
+ return { error };
1327
+ }
1328
+ componentDidCatch(error, info) {
1329
+ // 打到控制台便于排查(浏览器 F12)
1330
+ try {
1331
+ console.error('[litellm-bridge] 渲染异常:', error, info?.componentStack);
1332
+ } catch { /* ignore */ }
1333
+ }
1334
+ render() {
1335
+ if (this.state.error) {
1336
+ return h(ErrorBox, { title: this.props.title, error: this.state.error });
1337
+ }
1338
+ // children 既可能是元素也可能是函数,两种都支持
1339
+ return typeof this.props.children === 'function'
1340
+ ? this.props.children()
1341
+ : this.props.children;
1284
1342
  }
1285
1343
  }
1286
1344
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-litellm-bridge",
3
- "version": "2.0.6",
3
+ "version": "2.0.8",
4
4
  "description": "LiteLLM gateway bridge for DSH: assemble root / coder / research fallback chains from the DSH model pool and hand the order to a LiteLLM gateway that executes retries and fallbacks. The plugin performs no routing itself.",
5
5
  "keywords": [
6
6
  "dsh",