dsh-router-core 0.4.7 → 0.4.9

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.
Files changed (62) hide show
  1. package/README.md +14 -70
  2. package/cordis.patch.yml +2 -7
  3. package/docs/ext.md +3 -17
  4. package/docs/suppliers.md +1 -1
  5. package/lib/client-registry.js +137 -590
  6. package/lib/client.js +137 -590
  7. package/lib/ext-prompt/index.js +20 -15
  8. package/lib/index.js +0 -419
  9. package/lib/types/ext/contract.d.ts +1 -1
  10. package/lib/types/ext-prompt/content.d.ts +11 -0
  11. package/lib/types/ext-prompt/index.d.ts +7 -9
  12. package/lib/types/ext-prompt/mount.d.ts +2 -2
  13. package/lib/types/ext-prompt/plugin.d.ts +3 -5
  14. package/lib/types/router/types.d.ts +11 -0
  15. package/lib/types/shared.d.ts +2 -0
  16. package/lib/types/suppliers/registry.d.ts +0 -1
  17. package/package.json +1 -10
  18. package/src/client/CombosTab.tsx +29 -8
  19. package/src/client/ExtDetail.tsx +1 -13
  20. package/src/client/SupplierDetail.tsx +11 -5
  21. package/src/client/group-controls.test.ts +1 -1
  22. package/src/client/group-controls.ts +1 -1
  23. package/src/client/index.tsx +0 -17
  24. package/src/client/model-label.test.ts +33 -0
  25. package/src/client/model-label.ts +48 -0
  26. package/src/client/router-components.test.ts +1 -1
  27. package/src/client/router.css +24 -412
  28. package/src/ext/contract.ts +1 -1
  29. package/src/ext-prompt/content.ts +17 -12
  30. package/src/ext-prompt/ext-prompt.test.ts +3 -3
  31. package/src/ext-prompt/index.ts +7 -9
  32. package/src/ext-prompt/mount.ts +2 -2
  33. package/src/ext-prompt/plugin.ts +3 -5
  34. package/src/router/index.ts +2 -3
  35. package/src/router/models-cache.test.ts +21 -6
  36. package/src/router/supplier-switch.test.ts +1 -1
  37. package/src/router/types.ts +11 -0
  38. package/src/shared.ts +2 -0
  39. package/src/suppliers/registry.ts +0 -71
  40. package/lib/contract-D4BWZ7Xe.js +0 -13
  41. package/lib/ext-test/index.js +0 -36
  42. package/lib/ext-test/locale/en.json +0 -6
  43. package/lib/ext-test/locale/zh.json +0 -6
  44. package/lib/ext-test/package.json +0 -5
  45. package/lib/types/ext-test/index.d.ts +0 -21
  46. package/lib/types/ext-test/plugin.d.ts +0 -23
  47. package/lib/types/suppliers/probe.d.ts +0 -160
  48. package/src/client/ExtTestPanel.tsx +0 -392
  49. package/src/client/comp-button-scope.test.ts +0 -58
  50. package/src/client/comp-panel-layout.test.ts +0 -58
  51. package/src/client/ext-panels.test.ts +0 -44
  52. package/src/client/ext-panels.ts +0 -49
  53. package/src/client/model-choice.test.ts +0 -41
  54. package/src/client/model-choice.ts +0 -37
  55. package/src/ext-test/ext-test.test.ts +0 -60
  56. package/src/ext-test/index.ts +0 -35
  57. package/src/ext-test/locale/en.json +0 -6
  58. package/src/ext-test/locale/zh.json +0 -6
  59. package/src/ext-test/plugin.ts +0 -34
  60. package/src/ext-test/row.json +0 -5
  61. package/src/suppliers/probe.test.ts +0 -266
  62. package/src/suppliers/probe.ts +0 -475
@@ -10,6 +10,7 @@ import {
10
10
  type RouterComboAlias,
11
11
  type RouterComboSupplierGroup,
12
12
  } from '../shared.ts'
13
+ import { modelLabel, modelMatches } from './model-label.ts'
13
14
  import { Modal } from './Modal.tsx'
14
15
 
15
16
  /* ---------------- SVG 图标 ---------------- */
@@ -67,6 +68,22 @@ export function CombosTab({ combos, aliases, onRefresh }: CombosTabProps): JSX.E
67
68
  ?? groups.find(g => g.models.some(m => m.id === stored))
68
69
  return hit === undefined ? stored : `${hit.supplier.alias}/${stored}`
69
70
  }
71
+ /**
72
+ * 存储值 → 悬停说明。组合里存的、请求要发的都是 `alias/modelId`(上面那行),
73
+ * 但上游 key 往往不是人能读的(`qmodel_38max`),所以把上游的可读名
74
+ * (`Qwen3.8-Max`)放进 title 里,不占行宽也不改变「发什么」。
75
+ */
76
+ const comboTip = (stored: string): string => {
77
+ const name = displayName(stored)
78
+ const comma = stored.indexOf(',')
79
+ const mid = comma > 0 ? stored.slice(comma + 1) : stored
80
+ const sid = comma > 0 ? stored.slice(0, comma) : ''
81
+ const hit = (sid === '' ? groups : groups.filter(g => g.supplier.id === sid))
82
+ .flatMap(g => g.models)
83
+ .find(m => m.id === mid)
84
+ const readable = (hit?.name ?? '').trim()
85
+ return readable === '' || readable === mid ? name : `${readable}\n${name}(请求里用这个)`
86
+ }
70
87
  const [showForm, setShowForm] = useState(false)
71
88
  const [editing, setEditing] = useState<RouterCombo | null>(null)
72
89
  const [form, setForm] = useState<ComboForm>({ name: '', models: [] })
@@ -289,7 +306,7 @@ export function CombosTab({ combos, aliases, onRefresh }: CombosTabProps): JSX.E
289
306
  : (
290
307
  <>
291
308
  {combo.models.slice(0, 4).map((model, i) => (
292
- <span key={`${model}-${i}`} className="dshr-comboChip">
309
+ <span key={`${model}-${i}`} className="dshr-comboChip" title={comboTip(model)}>
293
310
  <span className="dshr-comboChipNum">{i + 1}</span>
294
311
  <span className="dshr-comboChipSupplier">{displayName(model)}</span>
295
312
  </span>
@@ -349,7 +366,7 @@ export function CombosTab({ combos, aliases, onRefresh }: CombosTabProps): JSX.E
349
366
  >
350
367
  <span className="dshr-comboGrip" title="拖拽排序"><Icon d={I.grip} size={14} /></span>
351
368
  <span className="dshr-comboStepNum">{index + 1}</span>
352
- <code className="dshr-comboSelectedModel">{displayName(model)}</code>
369
+ <code className="dshr-comboSelectedModel" title={comboTip(model)}>{displayName(model)}</code>
353
370
  <button type="button" className="dshr-iconBtn dshr-iconBtn-sm dshr-comboStepRemove" title="移除" onClick={() => removeModel(index)}>
354
371
  <Icon d={I.delete} size={14} />
355
372
  </button>
@@ -379,16 +396,20 @@ export function CombosTab({ combos, aliases, onRefresh }: CombosTabProps): JSX.E
379
396
  : groups.map(g => {
380
397
  const items = g.models.filter(m =>
381
398
  !form.models.includes(`${g.supplier.id},${m.id}`) &&
382
- (modelQuery === '' || m.id.toLowerCase().includes(modelQuery.toLowerCase())))
399
+ (modelQuery === '' || modelMatches(modelQuery, m)))
383
400
  if (items.length === 0) return null
384
401
  return (
385
402
  <div key={g.supplier.id} className="dshr-comboPickGroup">
386
403
  <p className="dshr-comboPickGroupTitle">{g.supplier.name} · {g.supplier.alias}</p>
387
- {items.map(m => (
388
- <button key={m.id} type="button" className="dshr-comboPickItem" onClick={() => toggleModel(g.supplier.id, m.id)}>
389
- <span className="dshr-comboPickName">{m.id}</span>
390
- </button>
391
- ))}
404
+ {items.map(m => {
405
+ const lab = modelLabel(m)
406
+ return (
407
+ <button key={m.id} type="button" className="dshr-comboPickItem" title={lab.title} onClick={() => toggleModel(g.supplier.id, m.id)}>
408
+ <span className="dshr-comboPickName">{lab.label}</span>
409
+ {lab.showId && <span className="dshr-comboPickKey">{m.id}</span>}
410
+ </button>
411
+ )
412
+ })}
392
413
  </div>
393
414
  )
394
415
  })}
@@ -13,13 +13,11 @@
13
13
  *
14
14
  * 列表页只列**已启用**的扩展(关闭的连卡片都不出现),所以这里也不必再显示
15
15
  * 「已启用/未启用」——那行字只能是常量。布局同供应商详情:返回链接 + 图标 +
16
- * 名称/副行,下面跟扩展说明卡与未就绪横幅。有 `api()` 支持的自定义扩展可在未来
17
- * 按需加独立面板,此处不内置任何具体扩展的专属视图。
16
+ * 名称/副行,下面跟扩展说明卡与未就绪横幅。
18
17
  */
19
18
  import { Fragment, useEffect, useState } from 'react'
20
19
  import { Switch } from '@deepseek-ai/dsh-client-ui-primitives'
21
20
  import { ROUTER_API_BASE, type ExtControlItem, type RouterExtItem, type RouterExtResponse } from '../shared.ts'
22
- import { extPanel } from './ext-panels.ts'
23
21
  import { groupControlRows } from './group-controls.ts'
24
22
  import { Modal } from './Modal.tsx'
25
23
 
@@ -599,10 +597,6 @@ interface ExtDetailProps {
599
597
  }
600
598
 
601
599
  export function ExtDetail({ item, onBack }: ExtDetailProps): JSX.Element {
602
- // 扩展**自带**详情面板时用它(插件自检就是这么做的:选供应商/模型/连接 + 跑测试)。
603
- // 没有就退回下面这张通用只读页 —— 保留返回链接与标题,导航形状与有没有自定义
604
- // 面板无关,用户点进去的预期是稳定的。
605
- const Custom = extPanel(item.id)
606
600
  return (
607
601
  <div className="dshr-tabBody">
608
602
  {/* Header(同供应商详情:返回链接 + 图标 + 名称/副行) */}
@@ -635,13 +629,9 @@ export function ExtDetail({ item, onBack }: ExtDetailProps): JSX.Element {
635
629
  </div>
636
630
  </div>
637
631
 
638
- {/* 扩展自带的面板优先(注册表命中时下面几块都不渲染) */}
639
- {Custom !== undefined ? <Custom /> : (
640
- <>
641
632
  {/* 子开关(扩展自己的行为细节;没有 controls 就不渲染这一块) */}
642
633
  {item.controls !== undefined && item.controls.length > 0 && <ExtControls item={item} />}
643
634
 
644
- {/* 实际生效的提示词(只读预览;回答「关掉的真的没进去吗」) */}
645
635
  {/* 未就绪警示(同供应商详情的出错横幅) */}
646
636
  {item.ready === false && (
647
637
  <div className="dshr-alert">
@@ -649,8 +639,6 @@ export function ExtDetail({ item, onBack }: ExtDetailProps): JSX.Element {
649
639
  <span>{item.detail !== undefined && item.detail !== '' ? item.detail : '当前不可用'}</span>
650
640
  </div>
651
641
  )}
652
- </>
653
- )}
654
642
  </div>
655
643
  )
656
644
  }
@@ -13,6 +13,7 @@ import {
13
13
  type RouterSupplierModelsResponse,
14
14
  } from '../shared.ts'
15
15
  import { statusBadge } from './account-badge.ts'
16
+ import { modelLabel, modelMatches } from './model-label.ts'
16
17
  import { Modal } from './Modal.tsx'
17
18
 
18
19
  /* ---------------- SVG 图标 ---------------- */
@@ -550,7 +551,7 @@ export function SupplierDetail({ supplier, accounts, statusLoading, onBack, onRe
550
551
  return out
551
552
  })()
552
553
  const q = modelQuery.trim().toLowerCase()
553
- const matchQuery = (m: RouterSupplierModel): boolean => q === '' || m.id.toLowerCase().includes(q)
554
+ const matchQuery = (m: RouterSupplierModel): boolean => modelMatches(q, m)
554
555
  const enabledModels = models?.filter(m => m.enabled && matchQuery(m)) ?? []
555
556
  const disabledModels = models?.filter(m => !m.enabled && matchQuery(m)) ?? []
556
557
 
@@ -763,19 +764,24 @@ export function SupplierDetail({ supplier, accounts, statusLoading, onBack, onRe
763
764
  {enabledModels.map(model => {
764
765
  const status = testResults[model.id]
765
766
  const chipCls = status === 'ok' ? ' dshr-modelChip-ok' : status === 'error' ? ' dshr-modelChip-err' : ''
767
+ const lab = modelLabel(model)
766
768
  return (
767
769
  <div key={model.id} className={`dshr-modelChipWrap${chipCls}`}>
768
770
  <button
769
771
  type="button"
770
772
  className="dshr-modelChip"
771
- title="点击禁用"
773
+ title={`${lab.title}\n点击禁用`}
772
774
  disabled={toggling}
773
775
  onClick={() => void toggleModel(model.id, false)}
774
776
  >
775
777
  <span className="dshr-modelChipStatus">
776
778
  <Icon d={status === 'ok' ? I.checkCircle : status === 'error' ? I.cancel : I.robot} size={13} />
777
779
  </span>
778
- <span className="dshr-modelChipId">{model.id}</span>
780
+ <span className="dshr-modelChipName">{lab.label}</span>
781
+ {lab.showId && <span className="dshr-modelChipKey">{model.id}</span>}
782
+ {typeof model.context_length === 'number' && model.context_length > 0 && (
783
+ <span className="dshr-modelChipCtx">{model.context_length}K</span>
784
+ )}
779
785
  </button>
780
786
  <div className="dshr-modelChipOps">
781
787
  <button
@@ -819,11 +825,11 @@ export function SupplierDetail({ supplier, accounts, statusLoading, onBack, onRe
819
825
  key={model.id}
820
826
  type="button"
821
827
  className="dshr-modelChip dshr-modelChip-off"
822
- title="点击恢复"
828
+ title={`${modelLabel(model).title}\n点击恢复`}
823
829
  disabled={toggling}
824
830
  onClick={() => void toggleModel(model.id, true)}
825
831
  >
826
- <span className="dshr-modelChipId">{model.id}</span>
832
+ <span className="dshr-modelChipName">{modelLabel(model).label}</span>
827
833
  <span className="dshr-modelChipRestore">+ 恢复</span>
828
834
  </button>
829
835
  ))}
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * 详情页 controls 分组的判据(`group-controls.ts`)。
3
3
  *
4
- * 关键是**向后兼容不可退**:没用 `group` 字段的扩展(ext-test 等)必须
4
+ * 关键是**向后兼容不可退**:没用 `group` 字段的扩展必须
5
5
  * 拿到"一个无组节、顺序不变"的结果 —— 分组是新能力的**叠加**,不是重排。
6
6
  */
7
7
  import test from 'node:test'
@@ -5,7 +5,7 @@
5
5
  * 准则分类 + 宿主系统提示词段落),平铺成一长列无从分辨哪条是谁的。
6
6
  *
7
7
  * 两条规则:
8
- * ① **无组节恒排最前** —— 没用 `group` 字段的扩展(ext-test 等)拿到的
8
+ * ① **无组节恒排最前** —— 没用 `group` 字段的扩展拿到的
9
9
  * 分组结果就是原来的平铺列表(一个无名节),面板行为一个像素不变;
10
10
  * ② 组的先后 = 组名在 `rows` 里的**首次出现序**,组内保持原顺序 ——
11
11
  * 不按字母重排:`rows` 的顺序来自服务端(段落按 prompt 序),重排
@@ -11,9 +11,6 @@
11
11
  import { mountRouterWorkspace } from './workspace-mount.tsx'
12
12
  import { RouterSettingsSection } from './settings-section.tsx'
13
13
  import { RouterComponentsSection } from './RouterComponentsSection.tsx'
14
- import { ExtTestPanel } from './ExtTestPanel.tsx'
15
- import { registerExtPanel } from './ext-panels.ts'
16
- import { EXT_TEST_ID } from '../ext-test/plugin.ts'
17
14
  import { registerSettingsNavIcon } from './settings-nav-icon.ts'
18
15
  import { rewriteRouterModelHint } from './model-hint-copy.ts'
19
16
  import { LastHitDock } from './LastHitDock.tsx'
@@ -97,19 +94,6 @@ function mountComponentsSection(ctx: Ctx): (() => void) | undefined {
97
94
  }, RouterComponentsSection))
98
95
  }
99
96
 
100
- /**
101
- * 给「插件自检」扩展登记它的详情面板。
102
- *
103
- * 面板的**唯一入口**是设置 → 路由 → 扩展 → 点开那张卡片(`ExtDetail` 查注册表拿到
104
- * 本组件)。**刻意不给插件页那一行再挂一个详情页**:那一行的宿主行开关已经能开/关
105
- * 它了,再点进去看同一块面板就是同一个东西有两个入口 —— 与「内置扩展不在自绘节里
106
- * 重复列」是同一条理由。
107
- */
108
- function registerExtTestPanel(ctx: Ctx): void {
109
- const dispose = registerExtPanel(EXT_TEST_ID, ExtTestPanel)
110
- ctx.effect(() => dispose, 'dsh-router: ext test panel registry')
111
- }
112
-
113
97
  export function apply(ctx: Ctx): void {
114
98
  // 「最近命中」徽章:走官方 conversation.composer.dock(scope: session → 组件
115
99
  // props 注入 sessionId,按当前会话取命中)。0.2.0 起这个座位无条件可用 ——
@@ -128,7 +112,6 @@ export function apply(ctx: Ctx): void {
128
112
  if (disposeComponents !== undefined) {
129
113
  ctx.effect(() => disposeComponents, 'dsh-router: plugin page components')
130
114
  }
131
- registerExtTestPanel(ctx)
132
115
  // 换掉宿主的默认齿轮(契约没有 icon 字段,只能注册后认领自己的行)
133
116
  ctx.effect(() => registerSettingsNavIcon(SECTION_LABEL), 'dsh-router: settings nav icon')
134
117
  // 设置-模型 里 Router 卡片的那句死路提示 → 正确说法(同样只能认领后就地改写)
@@ -0,0 +1,33 @@
1
+ /**
2
+ * 模型标签/搜索判据测试。
3
+ *
4
+ * 为什么要有:这两条判据错了都不会报错,只会让人在面板上做错事 ——
5
+ * 标签退化成 key(用户还是看不懂),或搜索只认 key(记不清 key 的人搜不到)。
6
+ */
7
+ import test from 'node:test'
8
+ import assert from 'node:assert/strict'
9
+ import { modelLabel, modelMatches } from './model-label.ts'
10
+
11
+ test('有可读名:名字在前,key 作为副标签', () => {
12
+ const l = modelLabel({ id: 'qmodel_38max', name: 'Qwen3.8-Max' })
13
+ assert.equal(l.label, 'Qwen3.8-Max')
14
+ assert.equal(l.showId, true)
15
+ assert.match(l.title, /qmodel_38max/)
16
+ })
17
+
18
+ test('没有可读名 / 名字与 key 相同:只显示 key,不重复', () => {
19
+ assert.deepEqual(
20
+ { ...modelLabel({ id: 'gpt-4o' }), title: undefined },
21
+ { label: 'gpt-4o', showId: false, title: undefined },
22
+ )
23
+ assert.equal(modelLabel({ id: 'gpt-4o', name: 'gpt-4o' }).showId, false)
24
+ assert.equal(modelLabel({ id: 'gpt-4o', name: ' ' }).label, 'gpt-4o')
25
+ })
26
+
27
+ test('搜索:可读名与 key 都能命中', () => {
28
+ const m = { id: 'kmodel_latest', name: 'Kimi-K3.1-Pro' }
29
+ assert.equal(modelMatches('kimi', m), true)
30
+ assert.equal(modelMatches('KMODEL', m), true)
31
+ assert.equal(modelMatches(' ', m), true)
32
+ assert.equal(modelMatches('glm', m), false)
33
+ })
@@ -0,0 +1,48 @@
1
+ /**
2
+ * 模型 chip 的标签 —— 「可读名 + 上游 key」两段。
3
+ *
4
+ * 为什么需要:有些上游的模型 key 是内部标识(Qoder 的 `qmodel_38max`、
5
+ * `kmodel_latest`、`mmodel`),面板上只剩一串猜不出含义的字母;而上游目录里
6
+ * 其实同时下发了可读名(`Qwen3.8-Max`、`Kimi-K3.1-Pro`)。丢掉它,用户点
7
+ * 「获取模型」之后仍然不知道自己勾的是什么。
8
+ *
9
+ * 但两段不能互相替代:**能发给上游的只有 key**。所以显示上可读名在前、key 在后
10
+ * (两者相同时只显示一次),复制按钮复制 key,请求/组合存的也是 key。
11
+ */
12
+
13
+ /** 一个模型在面板上的呈现。 */
14
+ export interface ModelLabel {
15
+ /** 主标签:上游给的可读名,没有(或与 key 相同)就用 key。 */
16
+ label: string
17
+ /** 是否还要把 key 作为副标签显示 —— 与主标签相同就不重复。 */
18
+ showId: boolean
19
+ /** 悬停提示:两个都给出来,并点明请求要用哪个。 */
20
+ title: string
21
+ }
22
+
23
+ /**
24
+ * 算出模型的展示标签。
25
+ * @param m 至少要有 `id`;`name` 是可读名(可缺、可空串)
26
+ */
27
+ export function modelLabel(m: { id: string; name?: string }): ModelLabel {
28
+ const name = (m.name ?? '').trim()
29
+ const label = name === '' || name === m.id ? m.id : name
30
+ const showId = label !== m.id
31
+ return {
32
+ label,
33
+ showId,
34
+ title: showId ? `${label}\n${m.id}(请求与组合里用这个)` : m.id,
35
+ }
36
+ }
37
+
38
+ /**
39
+ * 搜索匹配:可读名与 key **都**参与。
40
+ *
41
+ * 两种说法都要能搜到:记得模型叫什么的人搜 `kimi`,记得上游 key 的人搜
42
+ * `kmodel`;只匹配一个等于把另一半人的搜索框作废。
43
+ */
44
+ export function modelMatches(query: string, m: { id: string; name?: string }): boolean {
45
+ const q = query.trim().toLowerCase()
46
+ if (q === '') return true
47
+ return m.id.toLowerCase().includes(q) || (m.name ?? '').toLowerCase().includes(q)
48
+ }
@@ -105,7 +105,7 @@ test('内置扩展不进这一节(它已经是原生「包含的组件」里
105
105
  const out = groupRouterComponents({ ok: true }, {
106
106
  ok: true,
107
107
  enhancers: [
108
- { id: 'test', name: '插件自检', source: 'builtin' },
108
+ { id: 'prompt', name: '分层提示词', source: 'builtin' },
109
109
  { id: 'rtk', name: 'RTK' },
110
110
  ],
111
111
  })