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 +15 -0
- package/package.json +1 -1
- package/src/client.js +136 -132
- package/src/index.js +111 -43
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
package/src/client.js
CHANGED
|
@@ -62,11 +62,42 @@ window.__ModuleLoader__.load({
|
|
|
62
62
|
whiteSpace: 'nowrap',
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
143
|
-
|
|
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
|
-
}, [
|
|
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: () =>
|
|
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:
|
|
202
|
-
background:
|
|
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 ||
|
|
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
|
-
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
|
|
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', {
|
|
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
|
|
346
|
-
const
|
|
347
|
-
const
|
|
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)
|
|
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:
|
|
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: () =>
|
|
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
|
-
'🌐
|
|
579
|
+
'🌐 其它可用 Provider 链路测速',
|
|
580
580
|
),
|
|
581
|
-
|
|
582
|
-
|
|
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
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
padding: '
|
|
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
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
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
|
-
{
|
|
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
|
-
|
|
637
|
-
|
|
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: {
|
|
642
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
109
|
-
const
|
|
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
|
|
112
|
-
|
|
113
|
-
else if (latencyMs > 200) quality = 'good'
|
|
104
|
+
let timer = null
|
|
105
|
+
let socket = null
|
|
114
106
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
107
|
+
const cleanup = () => {
|
|
108
|
+
if (timer) clearTimeout(timer)
|
|
109
|
+
if (socket) {
|
|
110
|
+
socket.removeAllListeners()
|
|
111
|
+
socket.destroy()
|
|
112
|
+
}
|
|
120
113
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
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
|
}
|