dsh-link-pulse 0.1.3 → 0.1.5
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 +33 -32
- package/src/index.js +97 -41
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.1.5] - 2026-08-25
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- 侧边栏常驻条目排版样式优化:将延迟数值(如 `126ms`)改为与 OpenCode / CPA 规范统一的紧凑分段格式(`● CPA · Gemini 3.7 · 126ms`),去除两端强行两极分散的 `space-between`,实现视觉规格 100% 像素级对齐。
|
|
7
|
+
|
|
8
|
+
## [0.1.4] - 2026-08-25
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- 升级探针算法为 Keep-Alive 纯 RTT 测量:在底层 TLS 加密隧道就绪后测量纯 HTTP 数据往返耗时(1-RTT),真实反映长连接对话场景下的网络脉搏延迟(CPA 德国直连由 ~450ms 降至真实物理往返 ~126ms)。
|
|
12
|
+
|
|
3
13
|
## [0.1.3] - 2026-08-25
|
|
4
14
|
|
|
5
15
|
### Fixed
|
package/package.json
CHANGED
package/src/client.js
CHANGED
|
@@ -201,6 +201,12 @@ window.__ModuleLoader__.load({
|
|
|
201
201
|
|
|
202
202
|
const otherProviders = (data?.providers || []).filter((p) => p.id !== activeProviderInfo?.id)
|
|
203
203
|
|
|
204
|
+
const segments = [
|
|
205
|
+
{ text: providerDisplayName, color: T.label, bold: true },
|
|
206
|
+
{ text: modelShortName, color: T.secondary },
|
|
207
|
+
{ text: currentLatency > 0 ? `${currentLatency}ms` : '探测中', color: statusColor, bold: true },
|
|
208
|
+
]
|
|
209
|
+
|
|
204
210
|
const node = h(
|
|
205
211
|
'button',
|
|
206
212
|
{
|
|
@@ -222,44 +228,39 @@ window.__ModuleLoader__.load({
|
|
|
222
228
|
}),
|
|
223
229
|
wide &&
|
|
224
230
|
h(
|
|
225
|
-
'
|
|
231
|
+
'span',
|
|
226
232
|
{
|
|
227
233
|
style: {
|
|
228
|
-
display: 'flex',
|
|
234
|
+
display: 'inline-flex',
|
|
229
235
|
alignItems: 'center',
|
|
230
|
-
|
|
231
|
-
flex: 1,
|
|
232
|
-
overflow: 'hidden',
|
|
236
|
+
lineHeight: '18px',
|
|
233
237
|
whiteSpace: 'nowrap',
|
|
238
|
+
minWidth: 0,
|
|
239
|
+
overflow: 'hidden',
|
|
240
|
+
textOverflow: 'ellipsis',
|
|
234
241
|
},
|
|
235
242
|
},
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
color: statusColor,
|
|
258
|
-
marginLeft: 6,
|
|
259
|
-
flexShrink: 0,
|
|
260
|
-
},
|
|
261
|
-
},
|
|
262
|
-
currentLatency > 0 ? `${currentLatency}ms` : '探测中',
|
|
243
|
+
segments.flatMap((s, i) =>
|
|
244
|
+
i === 0
|
|
245
|
+
? [h('span', { key: `s${i}`, style: { color: s.color, fontWeight: 500, flexShrink: 0 } }, s.text)]
|
|
246
|
+
: [
|
|
247
|
+
h('span', { key: `sep${i}`, style: { color: T.secondary, margin: '0 5px', opacity: 0.7, flexShrink: 0 } }, '·'),
|
|
248
|
+
h(
|
|
249
|
+
'span',
|
|
250
|
+
{
|
|
251
|
+
key: `s${i}`,
|
|
252
|
+
style: {
|
|
253
|
+
color: s.color,
|
|
254
|
+
fontWeight: s.bold ? 500 : undefined,
|
|
255
|
+
fontVariantNumeric: 'tabular-nums',
|
|
256
|
+
overflow: 'hidden',
|
|
257
|
+
textOverflow: 'ellipsis',
|
|
258
|
+
flexShrink: i === segments.length - 1 ? 0 : 1,
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
s.text,
|
|
262
|
+
),
|
|
263
|
+
],
|
|
263
264
|
),
|
|
264
265
|
),
|
|
265
266
|
)
|
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) {
|