dsh-link-pulse 0.1.5 → 0.1.8
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 +18 -0
- package/README.md +17 -4
- package/package.json +1 -1
- package/src/client.js +90 -43
- package/src/index.js +57 -27
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.1.8] - 2026-08-30
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- **官方 DeepSeek V4 供应商误显为 CPA**:DSH 内置官方路由的运行时 provider id 为 `deepseek-official`(`dsh-llm-deepseek` 中 `PROVIDER` 常量),而插件兜底官方节点注册的 id 是 `deepseek`,导致客户端按当前会话 provider 查找落空后错误兜底到 provider 列表第一项(CPA)。兜底节点 id 修正为 `deepseek-official`,并同步注册 `KNOWN_PROVIDER_DEFAULTS` 映射。
|
|
7
|
+
- **未识别 Provider 张冠李戴**:客户端在 providerMap 查找失败时不再冒名显示列表第一个供应商的名称与延迟,改为展示该 provider 的真实 id(大写),延迟显示为探测中。
|
|
8
|
+
|
|
9
|
+
## [0.1.7] - 2026-08-29
|
|
10
|
+
|
|
11
|
+
### Fixed & Refactored
|
|
12
|
+
- **权威状态订阅重构**:彻底移除客户端定时器轮询与 DOM 抓取,接入 DSH 原生 `sessions` 与 `modelDirectories` 服务,实现模型切换时 0 毫秒即选即切,无需发消息触发。
|
|
13
|
+
- **Provider 网关与同名模型隔离**:将各 Provider 独立隔离(官方 `deepseek` 对应 `https://api.deepseek.com`,`opencode-go` 对应 `https://opencode.ai`),解决 DeepSeek V4 系列在官方与 OpenCode GO 之间的命名空间冲突与错误归属问题。
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
## [0.1.6] - 2026-08-25
|
|
17
|
+
|
|
18
|
+
### Docs
|
|
19
|
+
- 补充 README 中的 Keep-Alive 纯 1-RTT 测速原理与冷握手 vs 热长连接对比说明表。
|
|
20
|
+
|
|
3
21
|
## [0.1.5] - 2026-08-25
|
|
4
22
|
|
|
5
23
|
### Changed
|
package/README.md
CHANGED
|
@@ -7,11 +7,24 @@
|
|
|
7
7
|
|
|
8
8
|
## 核心特性
|
|
9
9
|
|
|
10
|
+
- **Keep-Alive 纯 1-RTT 真实测速**:在底层 TLS 加密管道就绪后测量纯 HTTP 数据往返耗时,真实反映长连接日常对话下的网络脉搏。
|
|
10
11
|
- **100% 动态 Provider 发现**:自动从 `~/.dsh/settings.yaml` 读取已配置的所有模型网关(CPA、OpenCode GO、DeepSeek 官方直连等),绝不硬编码。
|
|
11
|
-
- **当前对话模型感知**:侧边栏常驻条目根据当前聊天选中的模型,动态显示对应供应商名称与实时延迟(如 `● CPA · Gemini 3.7
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
12
|
+
- **当前对话模型感知**:侧边栏常驻条目根据当前聊天选中的模型,动态显示对应供应商名称与实时延迟(如 `● CPA · Gemini 3.7 · 126ms`)。
|
|
13
|
+
- **原生样式像素级对齐**:紧凑点号分段排版,状态圆点尺寸(`7px`)、呼吸光环与色彩变量严格对齐 `dsh-cpa-status` 与 `dsh-opencode-status`。
|
|
14
|
+
- **悬停快速预览与去重对比**:鼠标悬停 220ms 弹出微型预览卡片;全屏看板与小卡片均在顶部重点呈现当前主力通道,下方列表聚焦对比「其它可用 Provider」测速。
|
|
15
|
+
- **零 Token 消耗**:轻量级网络心跳探针,绝不消耗任何 API 配额。
|
|
16
|
+
|
|
17
|
+
## 延迟测量原理(Keep-Alive 纯 1-RTT 测速)
|
|
18
|
+
|
|
19
|
+
在 AI 流式交互与对话场景中,客户端与上游网关之间常驻 **HTTP/2 长连接(Keep-Alive 复用)**。
|
|
20
|
+
|
|
21
|
+
| 测量维度 | 传统冷握手测速 (Cold Handshake) | LinkPulse Keep-Alive 纯 RTT 探针 (Warm) |
|
|
22
|
+
| :--- | :--- | :--- |
|
|
23
|
+
| **涵盖流程** | TCP 握手 + TLS 1.3 握手 + 数据传输 (累加 3~4 次往返) | **仅测量已就绪加密通道上的数据往返 (1 次 RTT)** |
|
|
24
|
+
| **跨国节点耗时** | 约 $400\sim 550\text{ ms}$ (受多次物理往返累加影响) | **约 $120\sim 140\text{ ms}$ (直达物理光速极限)** |
|
|
25
|
+
| **实际映射意义** | 仅代表首次冷启动建连耗时 | **精准反映每次发送 Prompt 到接收流式 Token 首包的真实物理网络时延** |
|
|
26
|
+
|
|
27
|
+
探针在建立底层 TLS 安全通道后,直接发送轻量 `HEAD` 探针并测算首字节往返时间,避免了将冷建连多次握手耗时误判为网络卡顿。
|
|
15
28
|
|
|
16
29
|
## 安装与更新
|
|
17
30
|
|
package/package.json
CHANGED
package/src/client.js
CHANGED
|
@@ -7,6 +7,8 @@ window.__ModuleLoader__.load({
|
|
|
7
7
|
const h = React.createElement
|
|
8
8
|
const { useEffect, useState, useCallback, useMemo } = React
|
|
9
9
|
|
|
10
|
+
let globalCtx = null
|
|
11
|
+
|
|
10
12
|
const T = {
|
|
11
13
|
bg: 'var(--dsw-alias-bg-layer-2, #ffffff)',
|
|
12
14
|
layer: 'var(--dsw-alias-bg-layer-2, #ffffff)',
|
|
@@ -62,7 +64,7 @@ window.__ModuleLoader__.load({
|
|
|
62
64
|
whiteSpace: 'nowrap',
|
|
63
65
|
}
|
|
64
66
|
|
|
65
|
-
// ----------
|
|
67
|
+
// ---------- 全局单例共享状态(网络延迟数据) ----------
|
|
66
68
|
const store = {
|
|
67
69
|
state: {
|
|
68
70
|
data: null,
|
|
@@ -128,37 +130,65 @@ window.__ModuleLoader__.load({
|
|
|
128
130
|
return modelId.split(/[-_]/)[0] || modelId
|
|
129
131
|
}
|
|
130
132
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
+
/**
|
|
134
|
+
* 权威实时抓取:直接从 DSH 官方 sessions 与 modelDirectories 服务订阅当前正在对话的模型与供应商
|
|
135
|
+
*/
|
|
136
|
+
function useCurrentActiveModel(defaultConfig) {
|
|
137
|
+
const [selection, setSelection] = useState(() => ({
|
|
138
|
+
provider: defaultConfig?.provider || 'cpa',
|
|
139
|
+
model: defaultConfig?.model || 'gemini-3.7-flash-high',
|
|
140
|
+
}))
|
|
133
141
|
|
|
134
142
|
useEffect(() => {
|
|
135
|
-
|
|
143
|
+
if (!globalCtx) return
|
|
144
|
+
|
|
145
|
+
const getSessions = () => (globalCtx.get ? globalCtx.get('sessions') : globalCtx.sessions)
|
|
146
|
+
const getModelDirs = () => (globalCtx.get ? globalCtx.get('modelDirectories') : globalCtx.modelDirectories)
|
|
147
|
+
|
|
148
|
+
let dirUnsub = null
|
|
149
|
+
|
|
150
|
+
const sync = () => {
|
|
136
151
|
try {
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
)
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
152
|
+
const sessions = getSessions()
|
|
153
|
+
const modelDirs = getModelDirs()
|
|
154
|
+
if (!sessions || !modelDirs) return
|
|
155
|
+
|
|
156
|
+
const curSessionId = sessions.list?.getSnapshot?.()?.current
|
|
157
|
+
if (!curSessionId) return
|
|
158
|
+
|
|
159
|
+
const dir = modelDirs.directoryFor?.(curSessionId)
|
|
160
|
+
if (!dir) return
|
|
161
|
+
|
|
162
|
+
if (dirUnsub) dirUnsub()
|
|
163
|
+
dirUnsub = dir.store?.subscribe ? dir.store.subscribe(applyDirectorySnapshot) : null
|
|
164
|
+
|
|
165
|
+
function applyDirectorySnapshot() {
|
|
166
|
+
const snap = dir.store?.getSnapshot?.()
|
|
167
|
+
if (snap?.current?.provider && snap?.current?.model) {
|
|
168
|
+
setSelection({
|
|
169
|
+
provider: snap.current.provider,
|
|
170
|
+
model: snap.current.model,
|
|
171
|
+
reasoningEffort: snap.current.reasoningEffort,
|
|
172
|
+
})
|
|
150
173
|
}
|
|
151
174
|
}
|
|
175
|
+
|
|
176
|
+
applyDirectorySnapshot()
|
|
152
177
|
} catch {}
|
|
153
|
-
if (defaultModel) setActiveModel(defaultModel)
|
|
154
178
|
}
|
|
155
179
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
180
|
+
sync()
|
|
181
|
+
|
|
182
|
+
const sessions = getSessions()
|
|
183
|
+
const unsubSessions = sessions?.list?.subscribe ? sessions.list.subscribe(sync) : null
|
|
160
184
|
|
|
161
|
-
|
|
185
|
+
return () => {
|
|
186
|
+
if (unsubSessions) unsubSessions()
|
|
187
|
+
if (dirUnsub) dirUnsub()
|
|
188
|
+
}
|
|
189
|
+
}, [defaultConfig])
|
|
190
|
+
|
|
191
|
+
return selection
|
|
162
192
|
}
|
|
163
193
|
|
|
164
194
|
function FooterEntry(props) {
|
|
@@ -174,21 +204,28 @@ window.__ModuleLoader__.load({
|
|
|
174
204
|
}, [])
|
|
175
205
|
|
|
176
206
|
const data = state.data
|
|
177
|
-
const
|
|
178
|
-
const
|
|
207
|
+
const defaultModelConfig = data?.defaultModelConfig
|
|
208
|
+
const activeSelection = useCurrentActiveModel(defaultModelConfig)
|
|
179
209
|
|
|
180
210
|
const activeProviderInfo = useMemo(() => {
|
|
181
|
-
if (!data
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
211
|
+
if (!data) return null
|
|
212
|
+
const pId = activeSelection.provider || data.defaultModelConfig?.provider || 'cpa'
|
|
213
|
+
if (data.providerMap && data.providerMap[pId]) {
|
|
214
|
+
return data.providerMap[pId]
|
|
215
|
+
}
|
|
216
|
+
if (data.providers) {
|
|
217
|
+
const found = data.providers.find((p) => p.id === pId)
|
|
218
|
+
if (found) return found
|
|
219
|
+
}
|
|
220
|
+
// 未识别的 provider:展示其真实 id,绝不冒名顶替列表中的第一个供应商
|
|
221
|
+
return { id: pId, name: String(pId).toUpperCase(), baseURL: '', probe: null }
|
|
222
|
+
}, [data, activeSelection.provider])
|
|
186
223
|
|
|
187
224
|
const currentLatency = activeProviderInfo?.probe?.latencyMs ?? -1
|
|
188
225
|
const currentQuality = activeProviderInfo?.probe?.quality || 'offline'
|
|
189
226
|
const statusColor = getQualityColor(currentQuality, currentLatency)
|
|
190
|
-
const providerDisplayName = activeProviderInfo?.name ||
|
|
191
|
-
const modelShortName = formatModelShortName(
|
|
227
|
+
const providerDisplayName = activeProviderInfo?.name || activeSelection.provider.toUpperCase()
|
|
228
|
+
const modelShortName = formatModelShortName(activeSelection.model)
|
|
192
229
|
|
|
193
230
|
const onMouseEnter = useCallback(() => {
|
|
194
231
|
clearTimeout(hoverTimerRef.current)
|
|
@@ -297,7 +334,7 @@ window.__ModuleLoader__.load({
|
|
|
297
334
|
fontFamily: 'system-ui, -apple-system, sans-serif',
|
|
298
335
|
},
|
|
299
336
|
},
|
|
300
|
-
h('div', { style: { fontWeight: 600, fontSize: '12px', marginBottom: 4 } }, `🎯 ${
|
|
337
|
+
h('div', { style: { fontWeight: 600, fontSize: '12px', marginBottom: 4 } }, `🎯 ${activeSelection.model}`),
|
|
301
338
|
h('div', { style: { color: T.secondary } }, `🏢 ${providerDisplayName}`),
|
|
302
339
|
h(
|
|
303
340
|
'div',
|
|
@@ -368,15 +405,22 @@ window.__ModuleLoader__.load({
|
|
|
368
405
|
const data = state.data
|
|
369
406
|
const loading = state.loading
|
|
370
407
|
|
|
371
|
-
const
|
|
372
|
-
const
|
|
408
|
+
const defaultModelConfig = data?.defaultModelConfig
|
|
409
|
+
const activeSelection = useCurrentActiveModel(defaultModelConfig)
|
|
373
410
|
|
|
374
411
|
const activeProviderInfo = useMemo(() => {
|
|
375
|
-
if (!data
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
412
|
+
if (!data) return null
|
|
413
|
+
const pId = activeSelection.provider || data.defaultModelConfig?.provider || 'cpa'
|
|
414
|
+
if (data.providerMap && data.providerMap[pId]) {
|
|
415
|
+
return data.providerMap[pId]
|
|
416
|
+
}
|
|
417
|
+
if (data.providers) {
|
|
418
|
+
const found = data.providers.find((p) => p.id === pId)
|
|
419
|
+
if (found) return found
|
|
420
|
+
}
|
|
421
|
+
// 未识别的 provider:展示其真实 id,绝不冒名顶替列表中的第一个供应商
|
|
422
|
+
return { id: pId, name: String(pId).toUpperCase(), baseURL: '', probe: null }
|
|
423
|
+
}, [data, activeSelection.provider])
|
|
380
424
|
|
|
381
425
|
if (!isOpen) return null
|
|
382
426
|
|
|
@@ -385,6 +429,7 @@ window.__ModuleLoader__.load({
|
|
|
385
429
|
const currentQuality = activeProviderInfo?.probe?.quality || 'offline'
|
|
386
430
|
const statusColor = getQualityColor(currentQuality, currentLatency)
|
|
387
431
|
const otherProviders = providers.filter((p) => p.id !== activeProviderInfo?.id)
|
|
432
|
+
const providerDisplayName = activeProviderInfo?.name || activeSelection.provider.toUpperCase()
|
|
388
433
|
|
|
389
434
|
return h(
|
|
390
435
|
'div',
|
|
@@ -441,7 +486,7 @@ window.__ModuleLoader__.load({
|
|
|
441
486
|
h(
|
|
442
487
|
'div',
|
|
443
488
|
{ style: { fontSize: '11px', color: T.secondary } },
|
|
444
|
-
'
|
|
489
|
+
'实时订阅当前对话供应商与模型状态',
|
|
445
490
|
),
|
|
446
491
|
),
|
|
447
492
|
),
|
|
@@ -523,7 +568,7 @@ window.__ModuleLoader__.load({
|
|
|
523
568
|
'div',
|
|
524
569
|
{ style: { display: 'flex', alignItems: 'center', gap: '8px' } },
|
|
525
570
|
h('span', { style: dotStyle(statusColor) }),
|
|
526
|
-
h('span', { style: { fontWeight: 600, fontSize: '14px' } },
|
|
571
|
+
h('span', { style: { fontWeight: 600, fontSize: '14px' } }, activeSelection.model),
|
|
527
572
|
h(
|
|
528
573
|
'span',
|
|
529
574
|
{
|
|
@@ -536,7 +581,7 @@ window.__ModuleLoader__.load({
|
|
|
536
581
|
fontWeight: 600,
|
|
537
582
|
},
|
|
538
583
|
},
|
|
539
|
-
`供应商: ${
|
|
584
|
+
`供应商: ${providerDisplayName}`,
|
|
540
585
|
),
|
|
541
586
|
),
|
|
542
587
|
h(
|
|
@@ -657,6 +702,8 @@ window.__ModuleLoader__.load({
|
|
|
657
702
|
}
|
|
658
703
|
|
|
659
704
|
function apply(ctx) {
|
|
705
|
+
globalCtx = ctx
|
|
706
|
+
|
|
660
707
|
ctx.effect(() => {
|
|
661
708
|
const el = document.createElement('style')
|
|
662
709
|
el.id = 'dsh-link-pulse-styles'
|
package/src/index.js
CHANGED
|
@@ -8,6 +8,37 @@ import jsYaml from 'js-yaml'
|
|
|
8
8
|
export const name = 'link-pulse'
|
|
9
9
|
export const inject = ['webServer']
|
|
10
10
|
|
|
11
|
+
const KNOWN_PROVIDER_DEFAULTS = {
|
|
12
|
+
deepseek: {
|
|
13
|
+
name: 'DeepSeek 官方',
|
|
14
|
+
baseURL: 'https://api.deepseek.com',
|
|
15
|
+
},
|
|
16
|
+
'deepseek-official': {
|
|
17
|
+
name: 'DeepSeek 官方',
|
|
18
|
+
baseURL: 'https://api.deepseek.com',
|
|
19
|
+
},
|
|
20
|
+
'opencode-go': {
|
|
21
|
+
name: 'OpenCode GO',
|
|
22
|
+
baseURL: 'https://opencode.ai',
|
|
23
|
+
},
|
|
24
|
+
openai: {
|
|
25
|
+
name: 'OpenAI 官方',
|
|
26
|
+
baseURL: 'https://api.openai.com',
|
|
27
|
+
},
|
|
28
|
+
anthropic: {
|
|
29
|
+
name: 'Anthropic 官方',
|
|
30
|
+
baseURL: 'https://api.anthropic.com',
|
|
31
|
+
},
|
|
32
|
+
google: {
|
|
33
|
+
name: 'Google Gemini',
|
|
34
|
+
baseURL: 'https://generativelanguage.googleapis.com',
|
|
35
|
+
},
|
|
36
|
+
openrouter: {
|
|
37
|
+
name: 'OpenRouter',
|
|
38
|
+
baseURL: 'https://openrouter.ai',
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
|
|
11
42
|
function getSettingsPath() {
|
|
12
43
|
const dshHome = process.env.DSH_HOME || path.join(os.homedir(), '.dsh')
|
|
13
44
|
return path.join(dshHome, 'settings.yaml')
|
|
@@ -30,52 +61,47 @@ export function loadDynamicProviders() {
|
|
|
30
61
|
}
|
|
31
62
|
|
|
32
63
|
const providers = []
|
|
33
|
-
const
|
|
64
|
+
const providerMap = {}
|
|
34
65
|
|
|
35
66
|
for (const [providerId, pConfig] of Object.entries(providersConfig)) {
|
|
36
67
|
const rawModels = pConfig.models || []
|
|
37
68
|
const modelList = rawModels.map((m) => (typeof m === 'string' ? m : m.id || m.name)).filter(Boolean)
|
|
38
69
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
70
|
+
const known = KNOWN_PROVIDER_DEFAULTS[providerId] || {}
|
|
71
|
+
const targetBaseUrl = pConfig.baseURL || known.baseURL || ''
|
|
72
|
+
const displayName = pConfig.name || known.name || providerId.toUpperCase()
|
|
42
73
|
|
|
43
|
-
|
|
44
|
-
if (!targetBaseUrl && providerId === 'opencode-go') {
|
|
45
|
-
targetBaseUrl = 'https://api.deepseek.com'
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
providers.push({
|
|
74
|
+
const item = {
|
|
49
75
|
id: providerId,
|
|
50
|
-
name:
|
|
76
|
+
name: displayName,
|
|
51
77
|
baseURL: targetBaseUrl,
|
|
52
78
|
api: pConfig.api || 'openai-responses',
|
|
53
79
|
models: modelList,
|
|
54
80
|
isDefault: defaultModelConfig.provider === providerId,
|
|
55
|
-
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
providers.push(item)
|
|
84
|
+
providerMap[providerId] = item
|
|
56
85
|
}
|
|
57
86
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
modelToProviderMap[m] = 'deepseek'
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
providers.push({
|
|
66
|
-
id: 'deepseek',
|
|
87
|
+
// 如果未配置官方 deepseek(DSH 内置官方路由的 provider id 为 deepseek-official),补充作为独立官方探测节点
|
|
88
|
+
if (!providerMap['deepseek-official']) {
|
|
89
|
+
const dsItem = {
|
|
90
|
+
id: 'deepseek-official',
|
|
67
91
|
name: 'DeepSeek 官方',
|
|
68
92
|
baseURL: 'https://api.deepseek.com',
|
|
69
93
|
api: 'deepseek-native',
|
|
70
|
-
models:
|
|
71
|
-
isDefault: defaultModelConfig.provider === 'deepseek',
|
|
72
|
-
}
|
|
94
|
+
models: ['deepseek-v4-flash', 'deepseek-v4-pro'],
|
|
95
|
+
isDefault: defaultModelConfig.provider === 'deepseek-official',
|
|
96
|
+
}
|
|
97
|
+
providers.push(dsItem)
|
|
98
|
+
providerMap['deepseek-official'] = dsItem
|
|
73
99
|
}
|
|
74
100
|
|
|
75
101
|
return {
|
|
76
102
|
providers,
|
|
103
|
+
providerMap,
|
|
77
104
|
defaultModelConfig,
|
|
78
|
-
modelToProviderMap,
|
|
79
105
|
}
|
|
80
106
|
}
|
|
81
107
|
|
|
@@ -213,7 +239,7 @@ export function apply(ctx) {
|
|
|
213
239
|
return sendJson(res, 200, cachedData)
|
|
214
240
|
}
|
|
215
241
|
|
|
216
|
-
const { providers,
|
|
242
|
+
const { providers, providerMap, defaultModelConfig } = loadDynamicProviders()
|
|
217
243
|
|
|
218
244
|
const probePromises = providers.map(async (p) => {
|
|
219
245
|
const probeResult = await probeLatency(p.baseURL)
|
|
@@ -224,11 +250,15 @@ export function apply(ctx) {
|
|
|
224
250
|
})
|
|
225
251
|
|
|
226
252
|
const probedProviders = await Promise.all(probePromises)
|
|
253
|
+
const probedMap = {}
|
|
254
|
+
for (const item of probedProviders) {
|
|
255
|
+
probedMap[item.id] = item
|
|
256
|
+
}
|
|
227
257
|
|
|
228
258
|
cachedData = {
|
|
229
259
|
defaultModelConfig,
|
|
230
|
-
modelToProviderMap,
|
|
231
260
|
providers: probedProviders,
|
|
261
|
+
providerMap: probedMap,
|
|
232
262
|
timestamp: Date.now(),
|
|
233
263
|
}
|
|
234
264
|
lastProbeAt = Date.now()
|