dsh-link-pulse 0.1.0 → 0.1.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/CHANGELOG.md CHANGED
@@ -1,9 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.1.3] - 2026-08-25
4
+
5
+ ### Fixed
6
+ - 修复内外延迟显示不一致问题:引入全局单例状态存储与后端 25 秒探测缓存(支持 `?refresh=1` 强制刷新),确保侧栏按钮、Hover 悬浮小卡片与点击全屏大看板的延迟数据 100% 同步同源。
7
+
8
+ ## [0.1.2] - 2026-08-25
9
+
10
+ ### Changed
11
+ - 去除 Provider 链路对比列表中的当前主力通道重复展示:当前主力已在顶部重点呈现,下方列表聚焦展示「其它可用 Provider」的测速对比(Hover 浮层与全屏看板同步生效)。
12
+
13
+ ## [0.1.1] - 2026-08-25
14
+
15
+ ### Fixed
16
+ - 主动清理模型选择器文本中夹带的「default / 推理等级 / 模型」等 UI 标签残留,让长模型 ID(如 `opencode minimax-m3`)能正确识别为单个 token。
17
+
18
+ ### Added
19
+ - 恢复并改写侧边栏条目 hover 悬浮预览:220ms 悬停延迟后在按钮上方弹出小卡片,展示当前模型 / 供应商 / 实时延迟以及全部 Provider 链路测速对比。
20
+
3
21
  ## [0.1.0] - 2026-08-25
4
22
 
5
23
  ### Added
6
24
  - 初始版本发布:支持从 `settings.yaml` 动态读取 Providers。
7
25
  - 侧边栏常驻条目与当前会话模型动态感知。
8
26
  - 多通道实时测速浮层看板。
9
- - 遵循 Cordis@4 扁平依赖注入规范。
27
+ - 遵循 Cordis@4 扁平依赖注入规范。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-link-pulse",
3
- "version": "0.1.0",
3
+ "version": "0.1.3",
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
@@ -62,11 +62,42 @@ window.__ModuleLoader__.load({
62
62
  whiteSpace: 'nowrap',
63
63
  }
64
64
 
65
- let overlayOpen = false
66
- const listeners = new Set()
67
- function setOverlay(val) {
68
- overlayOpen = val
69
- listeners.forEach((fn) => fn(val))
65
+ // ---------- 全局单例共享状态(确保内外完全同源同步) ----------
66
+ const store = {
67
+ state: {
68
+ data: null,
69
+ loading: false,
70
+ open: false,
71
+ },
72
+ listeners: new Set(),
73
+ set(patch) {
74
+ store.state = { ...store.state, ...patch }
75
+ for (const fn of store.listeners) fn()
76
+ },
77
+ subscribe(fn) {
78
+ store.listeners.add(fn)
79
+ return () => store.listeners.delete(fn)
80
+ },
81
+ }
82
+
83
+ function useStore() {
84
+ return React.useSyncExternalStore(store.subscribe, () => store.state)
85
+ }
86
+
87
+ async function fetchStatus(force = false) {
88
+ if (store.state.loading && !force) return
89
+ try {
90
+ store.set({ loading: true })
91
+ const res = await fetch(`/api/link-pulse/status${force ? '?refresh=1' : ''}`)
92
+ if (res.ok) {
93
+ const json = await res.json()
94
+ store.set({ data: json, loading: false })
95
+ } else {
96
+ store.set({ loading: false })
97
+ }
98
+ } catch {
99
+ store.set({ loading: false })
100
+ }
70
101
  }
71
102
 
72
103
  function getQualityColor(quality, latencyMs) {
@@ -107,8 +138,13 @@ window.__ModuleLoader__.load({
107
138
  '[data-slot="conversation.input.model"], button[class*="modelSelect"], div[class*="modelSelector"]',
108
139
  )
109
140
  if (modelButton && modelButton.textContent) {
110
- const text = modelButton.textContent.trim()
111
- if (text && text.length < 30) {
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)) {
112
148
  setActiveModel(text)
113
149
  return
114
150
  }
@@ -127,34 +163,17 @@ window.__ModuleLoader__.load({
127
163
 
128
164
  function FooterEntry(props) {
129
165
  const { wide } = props
130
- const [isOpen, setIsOpen] = useState(overlayOpen)
131
- const [data, setData] = useState(null)
132
- const [loading, setLoading] = useState(false)
166
+ const state = useStore()
167
+ const [hovering, setHovering] = useState(false)
168
+ const hoverTimerRef = React.useRef(null)
133
169
 
134
170
  useEffect(() => {
135
- listeners.add(setIsOpen)
136
- return () => listeners.delete(setIsOpen)
137
- }, [])
138
-
139
- const fetchData = useCallback(async () => {
140
- try {
141
- setLoading(true)
142
- const res = await fetch('/api/link-pulse/status')
143
- if (res.ok) {
144
- const json = await res.json()
145
- setData(json)
146
- }
147
- } catch {} finally {
148
- setLoading(false)
149
- }
150
- }, [])
151
-
152
- useEffect(() => {
153
- fetchData()
154
- const timer = setInterval(fetchData, 45_000)
171
+ fetchStatus(false)
172
+ const timer = setInterval(() => fetchStatus(false), 45_000)
155
173
  return () => clearInterval(timer)
156
- }, [fetchData])
174
+ }, [])
157
175
 
176
+ const data = state.data
158
177
  const defaultModel = data?.defaultModelConfig?.model || 'gemini-3.7-flash-high'
159
178
  const activeModel = useActiveModel(defaultModel)
160
179
 
@@ -171,32 +190,30 @@ window.__ModuleLoader__.load({
171
190
  const providerDisplayName = activeProviderInfo?.name || 'CPA'
172
191
  const modelShortName = formatModelShortName(activeModel)
173
192
 
174
- const tooltipText = useMemo(() => {
175
- if (!data?.providers) return '链路测速中...'
176
- const lines = [`🎯 当前对话模型: ${activeModel}`]
177
- lines.push(`🏢 归属供应商: ${providerDisplayName}`)
178
- lines.push(`⚡ 实时往返延迟: ${currentLatency > 0 ? currentLatency + ' ms' : '检测中'}`)
179
- lines.push('────────────────────────')
180
- lines.push('🌐 已接入 Provider 链路一览:')
181
- for (const p of data.providers) {
182
- const lat = p.probe?.latencyMs > 0 ? `${p.probe.latencyMs}ms` : '离线'
183
- lines.push(`• ${p.name}: ${lat}`)
184
- }
185
- return lines.join('\n')
186
- }, [data, activeModel, providerDisplayName, currentLatency])
193
+ const onMouseEnter = useCallback(() => {
194
+ clearTimeout(hoverTimerRef.current)
195
+ hoverTimerRef.current = setTimeout(() => setHovering(true), 220)
196
+ }, [])
197
+ const onMouseLeave = useCallback(() => {
198
+ clearTimeout(hoverTimerRef.current)
199
+ setHovering(false)
200
+ }, [])
187
201
 
188
- return h(
202
+ const otherProviders = (data?.providers || []).filter((p) => p.id !== activeProviderInfo?.id)
203
+
204
+ const node = h(
189
205
  'button',
190
206
  {
191
207
  className: 'pulse-btn',
192
- onClick: () => setOverlay(!isOpen),
193
- title: tooltipText,
208
+ onClick: () => store.set({ open: !state.open }),
209
+ onMouseEnter: onMouseEnter,
210
+ onMouseLeave: onMouseLeave,
194
211
  style: {
195
212
  ...btnBase,
196
213
  width: '100%',
197
214
  justifyContent: wide ? 'flex-start' : 'center',
198
- borderColor: isOpen ? T.brand : T.border,
199
- background: isOpen ? 'var(--dsw-alias-interactive-bg-hover, rgba(59,130,246,.12))' : 'transparent',
215
+ borderColor: state.open ? T.brand : T.border,
216
+ background: state.open ? 'var(--dsw-alias-interactive-bg-hover, rgba(59,130,246,.12))' : 'transparent',
200
217
  },
201
218
  },
202
219
  h('span', {
@@ -246,34 +263,109 @@ window.__ModuleLoader__.load({
246
263
  ),
247
264
  ),
248
265
  )
249
- }
250
-
251
- function OverlayPanel() {
252
- const [isOpen, setIsOpen] = useState(overlayOpen)
253
- const [data, setData] = useState(null)
254
- const [loading, setLoading] = useState(false)
255
266
 
256
- useEffect(() => {
257
- listeners.add(setIsOpen)
258
- return () => listeners.delete(setIsOpen)
259
- }, [])
267
+ if (!hovering || state.open) return node
260
268
 
261
- const fetchData = useCallback(async () => {
262
- try {
263
- setLoading(true)
264
- const res = await fetch('/api/link-pulse/status')
265
- if (res.ok) {
266
- const json = await res.json()
267
- setData(json)
268
- }
269
- } catch {} finally {
270
- setLoading(false)
271
- }
272
- }, [])
269
+ return h(
270
+ 'div',
271
+ {
272
+ style: { position: 'relative', width: '100%' },
273
+ onMouseEnter: onMouseEnter,
274
+ onMouseLeave: onMouseLeave,
275
+ },
276
+ node,
277
+ h(
278
+ 'div',
279
+ {
280
+ className: 'pulse-panel-enter',
281
+ style: {
282
+ position: 'absolute',
283
+ left: 0,
284
+ right: 0,
285
+ bottom: 'calc(100% + 6px)',
286
+ background: T.bg,
287
+ border: `1px solid ${T.border2}`,
288
+ borderRadius: '10px',
289
+ boxShadow: '0 8px 24px rgba(0,0,0,.18)',
290
+ padding: '10px 12px',
291
+ fontSize: '11px',
292
+ lineHeight: '17px',
293
+ color: T.label,
294
+ whiteSpace: 'pre',
295
+ zIndex: 100,
296
+ fontFamily: 'system-ui, -apple-system, sans-serif',
297
+ },
298
+ },
299
+ h('div', { style: { fontWeight: 600, fontSize: '12px', marginBottom: 4 } }, `🎯 ${activeModel}`),
300
+ h('div', { style: { color: T.secondary } }, `🏢 ${providerDisplayName}`),
301
+ h(
302
+ 'div',
303
+ {
304
+ style: {
305
+ color: statusColor,
306
+ fontWeight: 600,
307
+ marginTop: 2,
308
+ marginBottom: otherProviders.length > 0 ? 6 : 0,
309
+ },
310
+ },
311
+ `⚡ ${currentLatency > 0 ? currentLatency + ' ms' : '检测中'}`,
312
+ ),
313
+ otherProviders.length > 0 &&
314
+ h(
315
+ 'div',
316
+ {
317
+ style: {
318
+ borderTop: `1px dashed ${T.border}`,
319
+ paddingTop: 6,
320
+ color: T.secondary,
321
+ },
322
+ },
323
+ '🌐 其它 Provider 链路',
324
+ ),
325
+ otherProviders.map((p, i) =>
326
+ h(
327
+ 'div',
328
+ {
329
+ key: i,
330
+ style: {
331
+ display: 'flex',
332
+ justifyContent: 'space-between',
333
+ gap: 12,
334
+ },
335
+ },
336
+ h(
337
+ 'span',
338
+ { style: { display: 'flex', alignItems: 'center', gap: 4 } },
339
+ h('span', {
340
+ style: {
341
+ ...dotStyle(getQualityColor(p.probe?.quality, p.probe?.latencyMs)),
342
+ width: 5,
343
+ height: 5,
344
+ },
345
+ }),
346
+ p.name,
347
+ ),
348
+ h(
349
+ 'span',
350
+ {
351
+ style: {
352
+ color: getQualityColor(p.probe?.quality, p.probe?.latencyMs),
353
+ fontWeight: 600,
354
+ },
355
+ },
356
+ p.probe?.latencyMs > 0 ? `${p.probe.latencyMs} ms` : '离线',
357
+ ),
358
+ ),
359
+ ),
360
+ ),
361
+ )
362
+ }
273
363
 
274
- useEffect(() => {
275
- if (isOpen) fetchData()
276
- }, [isOpen, fetchData])
364
+ function OverlayPanel() {
365
+ const state = useStore()
366
+ const isOpen = state.open
367
+ const data = state.data
368
+ const loading = state.loading
277
369
 
278
370
  const defaultModel = data?.defaultModelConfig?.model || 'gemini-3.7-flash-high'
279
371
  const activeModel = useActiveModel(defaultModel)
@@ -291,6 +383,7 @@ window.__ModuleLoader__.load({
291
383
  const currentLatency = activeProviderInfo?.probe?.latencyMs ?? -1
292
384
  const currentQuality = activeProviderInfo?.probe?.quality || 'offline'
293
385
  const statusColor = getQualityColor(currentQuality, currentLatency)
386
+ const otherProviders = providers.filter((p) => p.id !== activeProviderInfo?.id)
294
387
 
295
388
  return h(
296
389
  'div',
@@ -306,7 +399,7 @@ window.__ModuleLoader__.load({
306
399
  backdropFilter: 'blur(4px)',
307
400
  },
308
401
  onClick: (e) => {
309
- if (e.target === e.currentTarget) setOverlay(false)
402
+ if (e.target === e.currentTarget) store.set({ open: false })
310
403
  },
311
404
  },
312
405
  h(
@@ -358,7 +451,7 @@ window.__ModuleLoader__.load({
358
451
  'button',
359
452
  {
360
453
  className: 'pulse-btn',
361
- onClick: fetchData,
454
+ onClick: () => fetchStatus(true),
362
455
  disabled: loading,
363
456
  style: {
364
457
  display: 'flex',
@@ -380,7 +473,7 @@ window.__ModuleLoader__.load({
380
473
  'button',
381
474
  {
382
475
  className: 'pulse-btn',
383
- onClick: () => setOverlay(false),
476
+ onClick: () => store.set({ open: false }),
384
477
  style: {
385
478
  padding: '4px 8px',
386
479
  borderRadius: '6px',
@@ -483,75 +576,79 @@ window.__ModuleLoader__.load({
483
576
  marginBottom: '10px',
484
577
  },
485
578
  },
486
- '🌐 已接入 Provider 链路测速对比',
579
+ '🌐 其它可用 Provider 链路测速',
487
580
  ),
488
- h(
489
- 'div',
490
- { style: { display: 'flex', flexDirection: 'column', gap: '8px' } },
491
- providers.map((p, idx) => {
492
- const lat = p.probe?.latencyMs ?? -1
493
- const qual = p.probe?.quality || 'offline'
494
- const col = getQualityColor(qual, lat)
495
- const isCurrent = activeProviderInfo?.id === p.id
496
-
581
+ (() => {
582
+ if (otherProviders.length === 0) {
497
583
  return h(
498
584
  'div',
499
585
  {
500
- key: idx,
501
586
  style: {
502
- display: 'flex',
503
- justifyContent: 'space-between',
504
- alignItems: 'center',
505
- padding: '8px 10px',
506
- borderRadius: '8px',
507
- background: isCurrent ? 'rgba(59,130,246,.08)' : 'rgba(0,0,0,.03)',
508
- border: `1px solid ${isCurrent ? 'rgba(59,130,246,.3)' : 'transparent'}`,
587
+ fontSize: '11px',
588
+ color: T.secondary,
589
+ textAlign: 'center',
590
+ padding: '12px 0',
509
591
  },
510
592
  },
511
- h(
512
- 'div',
513
- null,
514
- h(
515
- 'div',
516
- { style: { display: 'flex', alignItems: 'center', gap: '6px' } },
517
- h('span', { style: dotStyle(col) }),
518
- h('span', { style: { fontWeight: 600, fontSize: '12px' } }, p.name),
519
- isCurrent &&
520
- h(
521
- 'span',
522
- {
523
- style: {
524
- fontSize: '9px',
525
- color: T.brand,
526
- fontWeight: 600,
527
- },
528
- },
529
- '● 当前主力',
530
- ),
531
- ),
532
- h(
533
- 'div',
534
- { style: { fontSize: '10px', color: T.secondary, marginTop: '2px' } },
535
- p.baseURL || '官方内置通道',
536
- ),
537
- ),
538
- h(
593
+ '暂无其它已接入的 Provider',
594
+ )
595
+ }
596
+
597
+ return h(
598
+ 'div',
599
+ { style: { display: 'flex', flexDirection: 'column', gap: '8px' } },
600
+ otherProviders.map((p, idx) => {
601
+ const lat = p.probe?.latencyMs ?? -1
602
+ const qual = p.probe?.quality || 'offline'
603
+ const col = getQualityColor(qual, lat)
604
+
605
+ return h(
539
606
  'div',
540
- { style: { textAlign: 'right' } },
607
+ {
608
+ key: idx,
609
+ style: {
610
+ display: 'flex',
611
+ justifyContent: 'space-between',
612
+ alignItems: 'center',
613
+ padding: '8px 10px',
614
+ borderRadius: '8px',
615
+ background: 'rgba(0,0,0,.03)',
616
+ border: `1px solid transparent`,
617
+ },
618
+ },
541
619
  h(
542
620
  'div',
543
- { style: { fontWeight: 700, fontSize: '13px', color: col } },
544
- lat > 0 ? `${lat} ms` : '超时/离线',
621
+ null,
622
+ h(
623
+ 'div',
624
+ { style: { display: 'flex', alignItems: 'center', gap: '6px' } },
625
+ h('span', { style: dotStyle(col) }),
626
+ h('span', { style: { fontWeight: 600, fontSize: '12px' } }, p.name),
627
+ ),
628
+ h(
629
+ 'div',
630
+ { style: { fontSize: '10px', color: T.secondary, marginTop: '2px' } },
631
+ p.baseURL || '官方内置通道',
632
+ ),
545
633
  ),
546
634
  h(
547
635
  'div',
548
- { style: { fontSize: '9px', color: T.secondary } },
549
- lat > 0 ? (lat < 200 ? '极佳' : '良好') : '不可达',
636
+ { style: { textAlign: 'right' } },
637
+ h(
638
+ 'div',
639
+ { style: { fontWeight: 700, fontSize: '13px', color: col } },
640
+ lat > 0 ? `${lat} ms` : '超时/离线',
641
+ ),
642
+ h(
643
+ 'div',
644
+ { style: { fontSize: '9px', color: T.secondary } },
645
+ lat > 0 ? (lat < 200 ? '极佳' : '良好') : '不可达',
646
+ ),
550
647
  ),
551
- ),
552
- )
553
- }),
554
- ),
648
+ )
649
+ }),
650
+ )
651
+ })(),
555
652
  ),
556
653
  ),
557
654
  ),
package/src/index.js CHANGED
@@ -137,6 +137,9 @@ function sendJson(res, statusCode, data) {
137
137
  res.end(JSON.stringify(data))
138
138
  }
139
139
 
140
+ let cachedData = null
141
+ let lastProbeAt = 0
142
+
140
143
  export function apply(ctx) {
141
144
  ctx.effect(() =>
142
145
  ctx.webServer.register({
@@ -148,6 +151,12 @@ export function apply(ctx) {
148
151
  }
149
152
 
150
153
  try {
154
+ const isForce = req.url && req.url.includes('refresh=1')
155
+ const now = Date.now()
156
+ if (!isForce && cachedData && now - lastProbeAt < 25_000) {
157
+ return sendJson(res, 200, cachedData)
158
+ }
159
+
151
160
  const { providers, defaultModelConfig, modelToProviderMap } = loadDynamicProviders()
152
161
 
153
162
  const probePromises = providers.map(async (p) => {
@@ -160,12 +169,15 @@ export function apply(ctx) {
160
169
 
161
170
  const probedProviders = await Promise.all(probePromises)
162
171
 
163
- sendJson(res, 200, {
172
+ cachedData = {
164
173
  defaultModelConfig,
165
174
  modelToProviderMap,
166
175
  providers: probedProviders,
167
176
  timestamp: Date.now(),
168
- })
177
+ }
178
+ lastProbeAt = Date.now()
179
+
180
+ sendJson(res, 200, cachedData)
169
181
  } catch (err) {
170
182
  sendJson(res, 500, { error: String(err && err.message ? err.message : err) })
171
183
  }