dsh-link-pulse 0.1.6 → 0.1.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.1.8] - 2026-08-30
4
+
5
+ ### Fixed
6
+ - **官方 DeepSeek V4 供应商误显为 CPA**:DSH 内置官方路由的运行时 provider id 为 `deepseek-official`(`dsh-llm-deepseek` 中 `PROVIDER` 常量),而插件兜底官方节点注册的 id 是 `deepseek`,导致客户端按当前会话 provider 查找落空后错误兜底到 provider 列表第一项(CPA)。兜底节点 id 修正为 `deepseek-official`,并同步注册 `KNOWN_PROVIDER_DEFAULTS` 映射。
7
+ - **未识别 Provider 张冠李戴**:客户端在 providerMap 查找失败时不再冒名显示列表第一个供应商的名称与延迟,改为展示该 provider 的真实 id(大写),延迟显示为探测中。
8
+
9
+ ## [0.1.7] - 2026-08-29
10
+
11
+ ### Fixed & Refactored
12
+ - **权威状态订阅重构**:彻底移除客户端定时器轮询与 DOM 抓取,接入 DSH 原生 `sessions` 与 `modelDirectories` 服务,实现模型切换时 0 毫秒即选即切,无需发消息触发。
13
+ - **Provider 网关与同名模型隔离**:将各 Provider 独立隔离(官方 `deepseek` 对应 `https://api.deepseek.com`,`opencode-go` 对应 `https://opencode.ai`),解决 DeepSeek V4 系列在官方与 OpenCode GO 之间的命名空间冲突与错误归属问题。
14
+
15
+
3
16
  ## [0.1.6] - 2026-08-25
4
17
 
5
18
  ### Docs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-link-pulse",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Dynamic LLM provider gateway latency and health pulse probe for DeepSeek Harness",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
package/src/client.js CHANGED
@@ -7,6 +7,8 @@ window.__ModuleLoader__.load({
7
7
  const h = React.createElement
8
8
  const { useEffect, useState, useCallback, useMemo } = React
9
9
 
10
+ let globalCtx = null
11
+
10
12
  const T = {
11
13
  bg: 'var(--dsw-alias-bg-layer-2, #ffffff)',
12
14
  layer: 'var(--dsw-alias-bg-layer-2, #ffffff)',
@@ -62,7 +64,7 @@ window.__ModuleLoader__.load({
62
64
  whiteSpace: 'nowrap',
63
65
  }
64
66
 
65
- // ---------- 全局单例共享状态(确保内外完全同源同步) ----------
67
+ // ---------- 全局单例共享状态(网络延迟数据) ----------
66
68
  const store = {
67
69
  state: {
68
70
  data: null,
@@ -128,37 +130,65 @@ window.__ModuleLoader__.load({
128
130
  return modelId.split(/[-_]/)[0] || modelId
129
131
  }
130
132
 
131
- function useActiveModel(defaultModel) {
132
- const [activeModel, setActiveModel] = useState(defaultModel || 'gemini-3.7-flash-high')
133
+ /**
134
+ * 权威实时抓取:直接从 DSH 官方 sessions modelDirectories 服务订阅当前正在对话的模型与供应商
135
+ */
136
+ function useCurrentActiveModel(defaultConfig) {
137
+ const [selection, setSelection] = useState(() => ({
138
+ provider: defaultConfig?.provider || 'cpa',
139
+ model: defaultConfig?.model || 'gemini-3.7-flash-high',
140
+ }))
133
141
 
134
142
  useEffect(() => {
135
- const detectModel = () => {
143
+ if (!globalCtx) return
144
+
145
+ const getSessions = () => (globalCtx.get ? globalCtx.get('sessions') : globalCtx.sessions)
146
+ const getModelDirs = () => (globalCtx.get ? globalCtx.get('modelDirectories') : globalCtx.modelDirectories)
147
+
148
+ let dirUnsub = null
149
+
150
+ const sync = () => {
136
151
  try {
137
- const modelButton = document.querySelector(
138
- '[data-slot="conversation.input.model"], button[class*="modelSelect"], div[class*="modelSelector"]',
139
- )
140
- if (modelButton && modelButton.textContent) {
141
- let text = modelButton.textContent.trim()
142
- // 剥离「default」「推理等级」「模型」等 UI 标签
143
- text = text
144
- .split(/[\n|·•]/)[0]
145
- .replace(/\s*(default|推理等级|模型|model)\s*/gi, '')
146
- .trim()
147
- if (text && text.length < 30 && /[a-zA-Z0-9\-_.]/.test(text)) {
148
- setActiveModel(text)
149
- return
152
+ const sessions = getSessions()
153
+ const modelDirs = getModelDirs()
154
+ if (!sessions || !modelDirs) return
155
+
156
+ const curSessionId = sessions.list?.getSnapshot?.()?.current
157
+ if (!curSessionId) return
158
+
159
+ const dir = modelDirs.directoryFor?.(curSessionId)
160
+ if (!dir) return
161
+
162
+ if (dirUnsub) dirUnsub()
163
+ dirUnsub = dir.store?.subscribe ? dir.store.subscribe(applyDirectorySnapshot) : null
164
+
165
+ function applyDirectorySnapshot() {
166
+ const snap = dir.store?.getSnapshot?.()
167
+ if (snap?.current?.provider && snap?.current?.model) {
168
+ setSelection({
169
+ provider: snap.current.provider,
170
+ model: snap.current.model,
171
+ reasoningEffort: snap.current.reasoningEffort,
172
+ })
150
173
  }
151
174
  }
175
+
176
+ applyDirectorySnapshot()
152
177
  } catch {}
153
- if (defaultModel) setActiveModel(defaultModel)
154
178
  }
155
179
 
156
- detectModel()
157
- const timer = setInterval(detectModel, 3000)
158
- return () => clearInterval(timer)
159
- }, [defaultModel])
180
+ sync()
181
+
182
+ const sessions = getSessions()
183
+ const unsubSessions = sessions?.list?.subscribe ? sessions.list.subscribe(sync) : null
160
184
 
161
- return activeModel
185
+ return () => {
186
+ if (unsubSessions) unsubSessions()
187
+ if (dirUnsub) dirUnsub()
188
+ }
189
+ }, [defaultConfig])
190
+
191
+ return selection
162
192
  }
163
193
 
164
194
  function FooterEntry(props) {
@@ -174,21 +204,28 @@ window.__ModuleLoader__.load({
174
204
  }, [])
175
205
 
176
206
  const data = state.data
177
- const defaultModel = data?.defaultModelConfig?.model || 'gemini-3.7-flash-high'
178
- const activeModel = useActiveModel(defaultModel)
207
+ const defaultModelConfig = data?.defaultModelConfig
208
+ const activeSelection = useCurrentActiveModel(defaultModelConfig)
179
209
 
180
210
  const activeProviderInfo = useMemo(() => {
181
- if (!data || !data.providers) return null
182
- const map = data.modelToProviderMap || {}
183
- const pId = map[activeModel.toLowerCase()] || data.defaultModelConfig?.provider || 'cpa'
184
- return data.providers.find((p) => p.id === pId) || data.providers[0] || null
185
- }, [data, activeModel])
211
+ if (!data) return null
212
+ const pId = activeSelection.provider || data.defaultModelConfig?.provider || 'cpa'
213
+ if (data.providerMap && data.providerMap[pId]) {
214
+ return data.providerMap[pId]
215
+ }
216
+ if (data.providers) {
217
+ const found = data.providers.find((p) => p.id === pId)
218
+ if (found) return found
219
+ }
220
+ // 未识别的 provider:展示其真实 id,绝不冒名顶替列表中的第一个供应商
221
+ return { id: pId, name: String(pId).toUpperCase(), baseURL: '', probe: null }
222
+ }, [data, activeSelection.provider])
186
223
 
187
224
  const currentLatency = activeProviderInfo?.probe?.latencyMs ?? -1
188
225
  const currentQuality = activeProviderInfo?.probe?.quality || 'offline'
189
226
  const statusColor = getQualityColor(currentQuality, currentLatency)
190
- const providerDisplayName = activeProviderInfo?.name || 'CPA'
191
- const modelShortName = formatModelShortName(activeModel)
227
+ const providerDisplayName = activeProviderInfo?.name || activeSelection.provider.toUpperCase()
228
+ const modelShortName = formatModelShortName(activeSelection.model)
192
229
 
193
230
  const onMouseEnter = useCallback(() => {
194
231
  clearTimeout(hoverTimerRef.current)
@@ -297,7 +334,7 @@ window.__ModuleLoader__.load({
297
334
  fontFamily: 'system-ui, -apple-system, sans-serif',
298
335
  },
299
336
  },
300
- h('div', { style: { fontWeight: 600, fontSize: '12px', marginBottom: 4 } }, `🎯 ${activeModel}`),
337
+ h('div', { style: { fontWeight: 600, fontSize: '12px', marginBottom: 4 } }, `🎯 ${activeSelection.model}`),
301
338
  h('div', { style: { color: T.secondary } }, `🏢 ${providerDisplayName}`),
302
339
  h(
303
340
  'div',
@@ -368,15 +405,22 @@ window.__ModuleLoader__.load({
368
405
  const data = state.data
369
406
  const loading = state.loading
370
407
 
371
- const defaultModel = data?.defaultModelConfig?.model || 'gemini-3.7-flash-high'
372
- const activeModel = useActiveModel(defaultModel)
408
+ const defaultModelConfig = data?.defaultModelConfig
409
+ const activeSelection = useCurrentActiveModel(defaultModelConfig)
373
410
 
374
411
  const activeProviderInfo = useMemo(() => {
375
- if (!data || !data.providers) return null
376
- const map = data.modelToProviderMap || {}
377
- const pId = map[activeModel.toLowerCase()] || data.defaultModelConfig?.provider || 'cpa'
378
- return data.providers.find((p) => p.id === pId) || data.providers[0] || null
379
- }, [data, activeModel])
412
+ if (!data) return null
413
+ const pId = activeSelection.provider || data.defaultModelConfig?.provider || 'cpa'
414
+ if (data.providerMap && data.providerMap[pId]) {
415
+ return data.providerMap[pId]
416
+ }
417
+ if (data.providers) {
418
+ const found = data.providers.find((p) => p.id === pId)
419
+ if (found) return found
420
+ }
421
+ // 未识别的 provider:展示其真实 id,绝不冒名顶替列表中的第一个供应商
422
+ return { id: pId, name: String(pId).toUpperCase(), baseURL: '', probe: null }
423
+ }, [data, activeSelection.provider])
380
424
 
381
425
  if (!isOpen) return null
382
426
 
@@ -385,6 +429,7 @@ window.__ModuleLoader__.load({
385
429
  const currentQuality = activeProviderInfo?.probe?.quality || 'offline'
386
430
  const statusColor = getQualityColor(currentQuality, currentLatency)
387
431
  const otherProviders = providers.filter((p) => p.id !== activeProviderInfo?.id)
432
+ const providerDisplayName = activeProviderInfo?.name || activeSelection.provider.toUpperCase()
388
433
 
389
434
  return h(
390
435
  'div',
@@ -441,7 +486,7 @@ window.__ModuleLoader__.load({
441
486
  h(
442
487
  'div',
443
488
  { style: { fontSize: '11px', color: T.secondary } },
444
- '仅探测本机实际接入的 LLM Provider 网关',
489
+ '实时订阅当前对话供应商与模型状态',
445
490
  ),
446
491
  ),
447
492
  ),
@@ -523,7 +568,7 @@ window.__ModuleLoader__.load({
523
568
  'div',
524
569
  { style: { display: 'flex', alignItems: 'center', gap: '8px' } },
525
570
  h('span', { style: dotStyle(statusColor) }),
526
- h('span', { style: { fontWeight: 600, fontSize: '14px' } }, activeModel),
571
+ h('span', { style: { fontWeight: 600, fontSize: '14px' } }, activeSelection.model),
527
572
  h(
528
573
  'span',
529
574
  {
@@ -536,7 +581,7 @@ window.__ModuleLoader__.load({
536
581
  fontWeight: 600,
537
582
  },
538
583
  },
539
- `供应商: ${activeProviderInfo?.name || 'CPA'}`,
584
+ `供应商: ${providerDisplayName}`,
540
585
  ),
541
586
  ),
542
587
  h(
@@ -657,6 +702,8 @@ window.__ModuleLoader__.load({
657
702
  }
658
703
 
659
704
  function apply(ctx) {
705
+ globalCtx = ctx
706
+
660
707
  ctx.effect(() => {
661
708
  const el = document.createElement('style')
662
709
  el.id = 'dsh-link-pulse-styles'
package/src/index.js CHANGED
@@ -8,6 +8,37 @@ import jsYaml from 'js-yaml'
8
8
  export const name = 'link-pulse'
9
9
  export const inject = ['webServer']
10
10
 
11
+ const KNOWN_PROVIDER_DEFAULTS = {
12
+ deepseek: {
13
+ name: 'DeepSeek 官方',
14
+ baseURL: 'https://api.deepseek.com',
15
+ },
16
+ 'deepseek-official': {
17
+ name: 'DeepSeek 官方',
18
+ baseURL: 'https://api.deepseek.com',
19
+ },
20
+ 'opencode-go': {
21
+ name: 'OpenCode GO',
22
+ baseURL: 'https://opencode.ai',
23
+ },
24
+ openai: {
25
+ name: 'OpenAI 官方',
26
+ baseURL: 'https://api.openai.com',
27
+ },
28
+ anthropic: {
29
+ name: 'Anthropic 官方',
30
+ baseURL: 'https://api.anthropic.com',
31
+ },
32
+ google: {
33
+ name: 'Google Gemini',
34
+ baseURL: 'https://generativelanguage.googleapis.com',
35
+ },
36
+ openrouter: {
37
+ name: 'OpenRouter',
38
+ baseURL: 'https://openrouter.ai',
39
+ },
40
+ }
41
+
11
42
  function getSettingsPath() {
12
43
  const dshHome = process.env.DSH_HOME || path.join(os.homedir(), '.dsh')
13
44
  return path.join(dshHome, 'settings.yaml')
@@ -30,52 +61,47 @@ export function loadDynamicProviders() {
30
61
  }
31
62
 
32
63
  const providers = []
33
- const modelToProviderMap = {}
64
+ const providerMap = {}
34
65
 
35
66
  for (const [providerId, pConfig] of Object.entries(providersConfig)) {
36
67
  const rawModels = pConfig.models || []
37
68
  const modelList = rawModels.map((m) => (typeof m === 'string' ? m : m.id || m.name)).filter(Boolean)
38
69
 
39
- for (const m of modelList) {
40
- modelToProviderMap[m.toLowerCase()] = providerId
41
- }
70
+ const known = KNOWN_PROVIDER_DEFAULTS[providerId] || {}
71
+ const targetBaseUrl = pConfig.baseURL || known.baseURL || ''
72
+ const displayName = pConfig.name || known.name || providerId.toUpperCase()
42
73
 
43
- let targetBaseUrl = pConfig.baseURL || ''
44
- if (!targetBaseUrl && providerId === 'opencode-go') {
45
- targetBaseUrl = 'https://api.deepseek.com'
46
- }
47
-
48
- providers.push({
74
+ const item = {
49
75
  id: providerId,
50
- name: providerId.toUpperCase(),
76
+ name: displayName,
51
77
  baseURL: targetBaseUrl,
52
78
  api: pConfig.api || 'openai-responses',
53
79
  models: modelList,
54
80
  isDefault: defaultModelConfig.provider === providerId,
55
- })
81
+ }
82
+
83
+ providers.push(item)
84
+ providerMap[providerId] = item
56
85
  }
57
86
 
58
- if (!providers.some((p) => p.id === 'deepseek' || p.id === 'deepseek-official')) {
59
- const dsModels = ['deepseek-chat', 'deepseek-reasoner', 'deepseek-v3', 'deepseek-r1']
60
- for (const m of dsModels) {
61
- if (!modelToProviderMap[m]) {
62
- modelToProviderMap[m] = 'deepseek'
63
- }
64
- }
65
- providers.push({
66
- id: 'deepseek',
87
+ // 如果未配置官方 deepseek(DSH 内置官方路由的 provider id deepseek-official),补充作为独立官方探测节点
88
+ if (!providerMap['deepseek-official']) {
89
+ const dsItem = {
90
+ id: 'deepseek-official',
67
91
  name: 'DeepSeek 官方',
68
92
  baseURL: 'https://api.deepseek.com',
69
93
  api: 'deepseek-native',
70
- models: dsModels,
71
- isDefault: defaultModelConfig.provider === 'deepseek',
72
- })
94
+ models: ['deepseek-v4-flash', 'deepseek-v4-pro'],
95
+ isDefault: defaultModelConfig.provider === 'deepseek-official',
96
+ }
97
+ providers.push(dsItem)
98
+ providerMap['deepseek-official'] = dsItem
73
99
  }
74
100
 
75
101
  return {
76
102
  providers,
103
+ providerMap,
77
104
  defaultModelConfig,
78
- modelToProviderMap,
79
105
  }
80
106
  }
81
107
 
@@ -213,7 +239,7 @@ export function apply(ctx) {
213
239
  return sendJson(res, 200, cachedData)
214
240
  }
215
241
 
216
- const { providers, defaultModelConfig, modelToProviderMap } = loadDynamicProviders()
242
+ const { providers, providerMap, defaultModelConfig } = loadDynamicProviders()
217
243
 
218
244
  const probePromises = providers.map(async (p) => {
219
245
  const probeResult = await probeLatency(p.baseURL)
@@ -224,11 +250,15 @@ export function apply(ctx) {
224
250
  })
225
251
 
226
252
  const probedProviders = await Promise.all(probePromises)
253
+ const probedMap = {}
254
+ for (const item of probedProviders) {
255
+ probedMap[item.id] = item
256
+ }
227
257
 
228
258
  cachedData = {
229
259
  defaultModelConfig,
230
- modelToProviderMap,
231
260
  providers: probedProviders,
261
+ providerMap: probedMap,
232
262
  timestamp: Date.now(),
233
263
  }
234
264
  lastProbeAt = Date.now()