dsh-link-pulse 0.1.1 → 0.1.4

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,20 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.1.4] - 2026-08-25
4
+
5
+ ### Changed
6
+ - 升级探针算法为 Keep-Alive 纯 RTT 测量:在底层 TLS 加密隧道就绪后测量纯 HTTP 数据往返耗时(1-RTT),真实反映长连接对话场景下的网络脉搏延迟(CPA 德国直连由 ~450ms 降至真实物理往返 ~126ms)。
7
+
8
+ ## [0.1.3] - 2026-08-25
9
+
10
+ ### Fixed
11
+ - 修复内外延迟显示不一致问题:引入全局单例状态存储与后端 25 秒探测缓存(支持 `?refresh=1` 强制刷新),确保侧栏按钮、Hover 悬浮小卡片与点击全屏大看板的延迟数据 100% 同步同源。
12
+
13
+ ## [0.1.2] - 2026-08-25
14
+
15
+ ### Changed
16
+ - 去除 Provider 链路对比列表中的当前主力通道重复展示:当前主力已在顶部重点呈现,下方列表聚焦展示「其它可用 Provider」的测速对比(Hover 浮层与全屏看板同步生效)。
17
+
3
18
  ## [0.1.1] - 2026-08-25
4
19
 
5
20
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-link-pulse",
3
- "version": "0.1.1",
3
+ "version": "0.1.4",
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) {
@@ -108,7 +139,7 @@ window.__ModuleLoader__.load({
108
139
  )
109
140
  if (modelButton && modelButton.textContent) {
110
141
  let text = modelButton.textContent.trim()
111
- // 剥离开关/标签 UI 残留,如「default」「推理等级」「high」等
142
+ // 剥离「default」「推理等级」「模型」等 UI 标签
112
143
  text = text
113
144
  .split(/[\n|·•]/)[0]
114
145
  .replace(/\s*(default|推理等级|模型|model)\s*/gi, '')
@@ -132,36 +163,17 @@ window.__ModuleLoader__.load({
132
163
 
133
164
  function FooterEntry(props) {
134
165
  const { wide } = props
135
- const [isOpen, setIsOpen] = useState(overlayOpen)
166
+ const state = useStore()
136
167
  const [hovering, setHovering] = useState(false)
137
- const [data, setData] = useState(null)
138
- const [loading, setLoading] = useState(false)
139
168
  const hoverTimerRef = React.useRef(null)
140
169
 
141
170
  useEffect(() => {
142
- listeners.add(setIsOpen)
143
- return () => listeners.delete(setIsOpen)
144
- }, [])
145
-
146
- const fetchData = useCallback(async () => {
147
- try {
148
- setLoading(true)
149
- const res = await fetch('/api/link-pulse/status')
150
- if (res.ok) {
151
- const json = await res.json()
152
- setData(json)
153
- }
154
- } catch {} finally {
155
- setLoading(false)
156
- }
157
- }, [])
158
-
159
- useEffect(() => {
160
- fetchData()
161
- const timer = setInterval(fetchData, 45_000)
171
+ fetchStatus(false)
172
+ const timer = setInterval(() => fetchStatus(false), 45_000)
162
173
  return () => clearInterval(timer)
163
- }, [fetchData])
174
+ }, [])
164
175
 
176
+ const data = state.data
165
177
  const defaultModel = data?.defaultModelConfig?.model || 'gemini-3.7-flash-high'
166
178
  const activeModel = useActiveModel(defaultModel)
167
179
 
@@ -187,19 +199,21 @@ window.__ModuleLoader__.load({
187
199
  setHovering(false)
188
200
  }, [])
189
201
 
202
+ const otherProviders = (data?.providers || []).filter((p) => p.id !== activeProviderInfo?.id)
203
+
190
204
  const node = h(
191
205
  'button',
192
206
  {
193
207
  className: 'pulse-btn',
194
- onClick: () => setOverlay(!isOpen),
208
+ onClick: () => store.set({ open: !state.open }),
195
209
  onMouseEnter: onMouseEnter,
196
210
  onMouseLeave: onMouseLeave,
197
211
  style: {
198
212
  ...btnBase,
199
213
  width: '100%',
200
214
  justifyContent: wide ? 'flex-start' : 'center',
201
- borderColor: isOpen ? T.brand : T.border,
202
- 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',
203
217
  },
204
218
  },
205
219
  h('span', {
@@ -250,9 +264,8 @@ window.__ModuleLoader__.load({
250
264
  ),
251
265
  )
252
266
 
253
- if (!hovering || isOpen) return node
267
+ if (!hovering || state.open) return node
254
268
 
255
- const providers = data?.providers || []
256
269
  return h(
257
270
  'div',
258
271
  {
@@ -279,7 +292,7 @@ window.__ModuleLoader__.load({
279
292
  lineHeight: '17px',
280
293
  color: T.label,
281
294
  whiteSpace: 'pre',
282
- z: 100,
295
+ zIndex: 100,
283
296
  fontFamily: 'system-ui, -apple-system, sans-serif',
284
297
  },
285
298
  },
@@ -292,23 +305,24 @@ window.__ModuleLoader__.load({
292
305
  color: statusColor,
293
306
  fontWeight: 600,
294
307
  marginTop: 2,
295
- marginBottom: 6,
308
+ marginBottom: otherProviders.length > 0 ? 6 : 0,
296
309
  },
297
310
  },
298
311
  `⚡ ${currentLatency > 0 ? currentLatency + ' ms' : '检测中'}`,
299
312
  ),
300
- h(
301
- 'div',
302
- {
303
- style: {
304
- borderTop: `1px dashed ${T.border}`,
305
- paddingTop: 6,
306
- color: T.secondary,
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
+ },
307
322
  },
308
- },
309
- '🌐 Provider 链路一览',
310
- ),
311
- providers.map((p, i) =>
323
+ '🌐 其它 Provider 链路',
324
+ ),
325
+ otherProviders.map((p, i) =>
312
326
  h(
313
327
  'div',
314
328
  {
@@ -322,7 +336,13 @@ window.__ModuleLoader__.load({
322
336
  h(
323
337
  'span',
324
338
  { style: { display: 'flex', alignItems: 'center', gap: 4 } },
325
- h('span', { style: { ...dotStyle(getQualityColor(p.probe?.quality, p.probe?.latencyMs)), width: 5, height: 5 } }),
339
+ h('span', {
340
+ style: {
341
+ ...dotStyle(getQualityColor(p.probe?.quality, p.probe?.latencyMs)),
342
+ width: 5,
343
+ height: 5,
344
+ },
345
+ }),
326
346
  p.name,
327
347
  ),
328
348
  h(
@@ -342,31 +362,10 @@ window.__ModuleLoader__.load({
342
362
  }
343
363
 
344
364
  function OverlayPanel() {
345
- const [isOpen, setIsOpen] = useState(overlayOpen)
346
- const [data, setData] = useState(null)
347
- const [loading, setLoading] = useState(false)
348
-
349
- useEffect(() => {
350
- listeners.add(setIsOpen)
351
- return () => listeners.delete(setIsOpen)
352
- }, [])
353
-
354
- const fetchData = useCallback(async () => {
355
- try {
356
- setLoading(true)
357
- const res = await fetch('/api/link-pulse/status')
358
- if (res.ok) {
359
- const json = await res.json()
360
- setData(json)
361
- }
362
- } catch {} finally {
363
- setLoading(false)
364
- }
365
- }, [])
366
-
367
- useEffect(() => {
368
- if (isOpen) fetchData()
369
- }, [isOpen, fetchData])
365
+ const state = useStore()
366
+ const isOpen = state.open
367
+ const data = state.data
368
+ const loading = state.loading
370
369
 
371
370
  const defaultModel = data?.defaultModelConfig?.model || 'gemini-3.7-flash-high'
372
371
  const activeModel = useActiveModel(defaultModel)
@@ -384,6 +383,7 @@ window.__ModuleLoader__.load({
384
383
  const currentLatency = activeProviderInfo?.probe?.latencyMs ?? -1
385
384
  const currentQuality = activeProviderInfo?.probe?.quality || 'offline'
386
385
  const statusColor = getQualityColor(currentQuality, currentLatency)
386
+ const otherProviders = providers.filter((p) => p.id !== activeProviderInfo?.id)
387
387
 
388
388
  return h(
389
389
  'div',
@@ -399,7 +399,7 @@ window.__ModuleLoader__.load({
399
399
  backdropFilter: 'blur(4px)',
400
400
  },
401
401
  onClick: (e) => {
402
- if (e.target === e.currentTarget) setOverlay(false)
402
+ if (e.target === e.currentTarget) store.set({ open: false })
403
403
  },
404
404
  },
405
405
  h(
@@ -451,7 +451,7 @@ window.__ModuleLoader__.load({
451
451
  'button',
452
452
  {
453
453
  className: 'pulse-btn',
454
- onClick: fetchData,
454
+ onClick: () => fetchStatus(true),
455
455
  disabled: loading,
456
456
  style: {
457
457
  display: 'flex',
@@ -473,7 +473,7 @@ window.__ModuleLoader__.load({
473
473
  'button',
474
474
  {
475
475
  className: 'pulse-btn',
476
- onClick: () => setOverlay(false),
476
+ onClick: () => store.set({ open: false }),
477
477
  style: {
478
478
  padding: '4px 8px',
479
479
  borderRadius: '6px',
@@ -576,75 +576,79 @@ window.__ModuleLoader__.load({
576
576
  marginBottom: '10px',
577
577
  },
578
578
  },
579
- '🌐 已接入 Provider 链路测速对比',
579
+ '🌐 其它可用 Provider 链路测速',
580
580
  ),
581
- h(
582
- 'div',
583
- { style: { display: 'flex', flexDirection: 'column', gap: '8px' } },
584
- providers.map((p, idx) => {
585
- const lat = p.probe?.latencyMs ?? -1
586
- const qual = p.probe?.quality || 'offline'
587
- const col = getQualityColor(qual, lat)
588
- const isCurrent = activeProviderInfo?.id === p.id
589
-
581
+ (() => {
582
+ if (otherProviders.length === 0) {
590
583
  return h(
591
584
  'div',
592
585
  {
593
- key: idx,
594
586
  style: {
595
- display: 'flex',
596
- justifyContent: 'space-between',
597
- alignItems: 'center',
598
- padding: '8px 10px',
599
- borderRadius: '8px',
600
- background: isCurrent ? 'rgba(59,130,246,.08)' : 'rgba(0,0,0,.03)',
601
- border: `1px solid ${isCurrent ? 'rgba(59,130,246,.3)' : 'transparent'}`,
587
+ fontSize: '11px',
588
+ color: T.secondary,
589
+ textAlign: 'center',
590
+ padding: '12px 0',
602
591
  },
603
592
  },
604
- h(
605
- 'div',
606
- null,
607
- h(
608
- 'div',
609
- { style: { display: 'flex', alignItems: 'center', gap: '6px' } },
610
- h('span', { style: dotStyle(col) }),
611
- h('span', { style: { fontWeight: 600, fontSize: '12px' } }, p.name),
612
- isCurrent &&
613
- h(
614
- 'span',
615
- {
616
- style: {
617
- fontSize: '9px',
618
- color: T.brand,
619
- fontWeight: 600,
620
- },
621
- },
622
- '● 当前主力',
623
- ),
624
- ),
625
- h(
626
- 'div',
627
- { style: { fontSize: '10px', color: T.secondary, marginTop: '2px' } },
628
- p.baseURL || '官方内置通道',
629
- ),
630
- ),
631
- 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(
632
606
  'div',
633
- { 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
+ },
634
619
  h(
635
620
  'div',
636
- { style: { fontWeight: 700, fontSize: '13px', color: col } },
637
- 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
+ ),
638
633
  ),
639
634
  h(
640
635
  'div',
641
- { style: { fontSize: '9px', color: T.secondary } },
642
- 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
+ ),
643
647
  ),
644
- ),
645
- )
646
- }),
647
- ),
648
+ )
649
+ }),
650
+ )
651
+ })(),
648
652
  ),
649
653
  ),
650
654
  ),
package/src/index.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import path from 'node:path'
2
2
  import os from 'node:os'
3
3
  import fs from 'node:fs'
4
+ import tls from 'node:tls'
5
+ import net from 'node:net'
4
6
  import jsYaml from 'js-yaml'
5
7
 
6
8
  export const name = 'link-pulse'
@@ -77,57 +79,111 @@ export function loadDynamicProviders() {
77
79
  }
78
80
  }
79
81
 
82
+ /**
83
+ * 测量 Keep-Alive 状态下的真实单次 RTT 往返延迟
84
+ * (在已建立的加密 socket 上发送 HEAD 并测量首字节返回耗时)
85
+ */
80
86
  export async function probeLatency(url) {
81
87
  if (!url || !/^https?:\/\//i.test(url)) {
82
88
  return { latencyMs: -1, status: 'no_url', quality: 'offline', error: '无有效网络地址' }
83
89
  }
84
90
 
85
- let probeUrl = url
86
- try {
87
- const parsed = new URL(url)
88
- probeUrl = `${parsed.protocol}//${parsed.host}`
89
- } catch {}
90
-
91
- const start = performance.now()
92
- const controller = new AbortController()
93
- const timeoutId = setTimeout(() => controller.abort(), 3500)
94
-
95
- try {
96
- const res = await fetch(probeUrl, {
97
- method: 'HEAD',
98
- signal: controller.signal,
99
- headers: { 'User-Agent': 'DSH-LinkPulse/1.0' },
100
- }).catch(async () => {
101
- return await fetch(probeUrl, {
102
- method: 'GET',
103
- signal: controller.signal,
104
- headers: { 'User-Agent': 'DSH-LinkPulse/1.0' },
105
- })
106
- })
91
+ return new Promise((resolve) => {
92
+ let parsed
93
+ try {
94
+ parsed = new URL(url)
95
+ } catch {
96
+ return resolve({ latencyMs: -1, quality: 'error', reachable: false, error: 'URL 格式错误' })
97
+ }
107
98
 
108
- clearTimeout(timeoutId)
109
- const latencyMs = Math.round(performance.now() - start)
99
+ const host = parsed.hostname
100
+ const isHttps = parsed.protocol === 'https:'
101
+ const port = parseInt(parsed.port, 10) || (isHttps ? 443 : 80)
102
+ const timeout = 3500
110
103
 
111
- let quality = 'excellent'
112
- if (latencyMs > 600) quality = 'slow'
113
- else if (latencyMs > 200) quality = 'good'
104
+ let timer = null
105
+ let socket = null
114
106
 
115
- return {
116
- latencyMs,
117
- httpStatus: res.status,
118
- quality,
119
- reachable: true,
107
+ const cleanup = () => {
108
+ if (timer) clearTimeout(timer)
109
+ if (socket) {
110
+ socket.removeAllListeners()
111
+ socket.destroy()
112
+ }
120
113
  }
121
- } catch (err) {
122
- clearTimeout(timeoutId)
123
- const isTimeout = err.name === 'AbortError'
124
- return {
125
- latencyMs: isTimeout ? 3500 : -1,
126
- quality: isTimeout ? 'timeout' : 'error',
127
- reachable: false,
128
- error: isTimeout ? '请求超时 (>3.5s)' : (err.message || '连接失败'),
114
+
115
+ timer = setTimeout(() => {
116
+ cleanup()
117
+ resolve({ latencyMs: 3500, quality: 'timeout', reachable: false, error: '请求超时 (>3.5s)' })
118
+ }, timeout)
119
+
120
+ try {
121
+ const onEstablished = (sock) => {
122
+ const start = performance.now()
123
+ const path = parsed.pathname || '/'
124
+ sock.write(`HEAD ${path} HTTP/1.1\r\nHost: ${host}\r\nUser-Agent: DSH-LinkPulse/1.0\r\nConnection: close\r\n\r\n`)
125
+
126
+ sock.once('data', () => {
127
+ const latencyMs = Math.max(1, Math.round(performance.now() - start))
128
+ cleanup()
129
+
130
+ let quality = 'excellent'
131
+ if (latencyMs > 600) quality = 'slow'
132
+ else if (latencyMs > 200) quality = 'good'
133
+
134
+ resolve({
135
+ latencyMs,
136
+ quality,
137
+ reachable: true,
138
+ })
139
+ })
140
+ }
141
+
142
+ if (isHttps) {
143
+ socket = tls.connect({
144
+ host,
145
+ port,
146
+ servername: host,
147
+ rejectUnauthorized: false,
148
+ timeout,
149
+ }, () => onEstablished(socket))
150
+ } else {
151
+ socket = net.connect({
152
+ host,
153
+ port,
154
+ timeout,
155
+ }, () => onEstablished(socket))
156
+ }
157
+
158
+ socket.on('error', (err) => {
159
+ cleanup()
160
+ resolve({
161
+ latencyMs: -1,
162
+ quality: 'error',
163
+ reachable: false,
164
+ error: err.message || '连接失败',
165
+ })
166
+ })
167
+
168
+ socket.on('timeout', () => {
169
+ cleanup()
170
+ resolve({
171
+ latencyMs: 3500,
172
+ quality: 'timeout',
173
+ reachable: false,
174
+ error: '连接超时',
175
+ })
176
+ })
177
+ } catch (err) {
178
+ cleanup()
179
+ resolve({
180
+ latencyMs: -1,
181
+ quality: 'error',
182
+ reachable: false,
183
+ error: err.message || '连接异常',
184
+ })
129
185
  }
130
- }
186
+ })
131
187
  }
132
188
 
133
189
  function sendJson(res, statusCode, data) {
@@ -137,6 +193,9 @@ function sendJson(res, statusCode, data) {
137
193
  res.end(JSON.stringify(data))
138
194
  }
139
195
 
196
+ let cachedData = null
197
+ let lastProbeAt = 0
198
+
140
199
  export function apply(ctx) {
141
200
  ctx.effect(() =>
142
201
  ctx.webServer.register({
@@ -148,6 +207,12 @@ export function apply(ctx) {
148
207
  }
149
208
 
150
209
  try {
210
+ const isForce = req.url && req.url.includes('refresh=1')
211
+ const now = Date.now()
212
+ if (!isForce && cachedData && now - lastProbeAt < 25_000) {
213
+ return sendJson(res, 200, cachedData)
214
+ }
215
+
151
216
  const { providers, defaultModelConfig, modelToProviderMap } = loadDynamicProviders()
152
217
 
153
218
  const probePromises = providers.map(async (p) => {
@@ -160,12 +225,15 @@ export function apply(ctx) {
160
225
 
161
226
  const probedProviders = await Promise.all(probePromises)
162
227
 
163
- sendJson(res, 200, {
228
+ cachedData = {
164
229
  defaultModelConfig,
165
230
  modelToProviderMap,
166
231
  providers: probedProviders,
167
232
  timestamp: Date.now(),
168
- })
233
+ }
234
+ lastProbeAt = Date.now()
235
+
236
+ sendJson(res, 200, cachedData)
169
237
  } catch (err) {
170
238
  sendJson(res, 500, { error: String(err && err.message ? err.message : err) })
171
239
  }