dsh-link-pulse 0.1.1 → 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 +10 -0
- package/package.json +1 -1
- package/src/client.js +136 -132
- package/src/index.js +14 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
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
|
+
|
|
3
13
|
## [0.1.1] - 2026-08-25
|
|
4
14
|
|
|
5
15
|
### 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
|
@@ -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
|
-
|
|
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
|
}
|