@wenbin_wb/dsh-bridge 2.8.5 → 2.8.7
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 +34 -0
- package/client/client.js +396 -39
- package/client/index.js +399 -40
- package/docs/fix-tunnel-sse-and-session-list.md +134 -0
- package/lib/bridge-rpc-constants.js +1 -0
- package/lib/bridge-rpc.js +9 -0
- package/lib/cloudflared-manager.mjs +75 -12
- package/lib/index.js +147 -26
- package/lib/platform/conversation-bridge.js +153 -128
- package/lib/tunnel-client.mjs +402 -289
- package/package.json +2 -2
package/client/index.js
CHANGED
|
@@ -317,6 +317,78 @@ function QrBlock({ url, qr, onReset, auth, onNavigateSecurity }) {
|
|
|
317
317
|
);
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
+
const LanNetworkSelector = React.memo(function LanNetworkSelector({ lan, onSelectIp }) {
|
|
321
|
+
const interfaces = lan?.interfaces || [];
|
|
322
|
+
const selectedIp = lan?.selectedIp || '';
|
|
323
|
+
const currentIp = lan?.ip || '';
|
|
324
|
+
const [switching, setSwitching] = React.useState(false);
|
|
325
|
+
|
|
326
|
+
if (!interfaces || interfaces.length <= 1) return null;
|
|
327
|
+
|
|
328
|
+
const handleChange = async (e) => {
|
|
329
|
+
const val = e.target.value;
|
|
330
|
+
setSwitching(true);
|
|
331
|
+
try {
|
|
332
|
+
await onSelectIp(val || null);
|
|
333
|
+
} finally {
|
|
334
|
+
setSwitching(false);
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
return React.createElement('div', {
|
|
339
|
+
style: {
|
|
340
|
+
...s.block,
|
|
341
|
+
background: 'var(--dsw-alias-bg-layer-2, rgba(243, 244, 246, 0.6))',
|
|
342
|
+
padding: '10px 12px',
|
|
343
|
+
borderRadius: 8,
|
|
344
|
+
border: '1px solid var(--dsw-alias-border-l2, #e5e7eb)',
|
|
345
|
+
marginTop: 8,
|
|
346
|
+
marginBottom: 6,
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
React.createElement('div', {
|
|
350
|
+
style: {
|
|
351
|
+
display: 'flex',
|
|
352
|
+
alignItems: 'center',
|
|
353
|
+
justifyContent: 'space-between',
|
|
354
|
+
marginBottom: 6,
|
|
355
|
+
fontSize: 12,
|
|
356
|
+
fontWeight: 500,
|
|
357
|
+
color: 'var(--dsw-alias-label-primary, currentColor)',
|
|
358
|
+
},
|
|
359
|
+
},
|
|
360
|
+
React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 5 } },
|
|
361
|
+
'🛜 局域网网卡 / IP 选择'
|
|
362
|
+
),
|
|
363
|
+
switching && React.createElement('span', {
|
|
364
|
+
style: { fontSize: 11, color: 'var(--dsw-alias-brand-primary, #4f6ef7)' },
|
|
365
|
+
}, '切换中…')
|
|
366
|
+
),
|
|
367
|
+
React.createElement('div', { style: { ...s.muted, fontSize: 11, marginBottom: 6 } },
|
|
368
|
+
'检测到主机存在多张网卡(如物理 Wi-Fi、以太网、WSL 或虚拟机)。若默认 IP 无法被移动端访问,可手动切换:'
|
|
369
|
+
),
|
|
370
|
+
React.createElement('select', {
|
|
371
|
+
style: {
|
|
372
|
+
...s.input,
|
|
373
|
+
height: 32,
|
|
374
|
+
fontSize: 12,
|
|
375
|
+
padding: '0 8px',
|
|
376
|
+
background: 'var(--dsw-alias-bg-layer-1, #ffffff)',
|
|
377
|
+
cursor: 'pointer',
|
|
378
|
+
},
|
|
379
|
+
value: selectedIp,
|
|
380
|
+
onChange: handleChange,
|
|
381
|
+
disabled: switching,
|
|
382
|
+
},
|
|
383
|
+
React.createElement('option', { value: '' }, `⚡ 自动推荐 (${interfaces[0]?.address || currentIp} · ${interfaces[0]?.label || interfaces[0]?.name || ''})`),
|
|
384
|
+
interfaces.map((iface) => React.createElement('option', {
|
|
385
|
+
key: `${iface.name}-${iface.address}`,
|
|
386
|
+
value: iface.address,
|
|
387
|
+
}, `${iface.address} · ${iface.label || iface.name}${iface.isVirtual ? ' [虚拟/WSL]' : ''}`)),
|
|
388
|
+
),
|
|
389
|
+
);
|
|
390
|
+
});
|
|
391
|
+
|
|
320
392
|
const CustomTunnelGuide = React.memo(function CustomTunnelGuide() {
|
|
321
393
|
return React.createElement('div', { style: s.block },
|
|
322
394
|
React.createElement('a', {
|
|
@@ -2497,6 +2569,8 @@ function BridgePanel({ rpcCall }) {
|
|
|
2497
2569
|
act(BRIDGE_ENDPOINTS.saveCloudflaredConfig, { token, hostname })
|
|
2498
2570
|
, [act]);
|
|
2499
2571
|
|
|
2572
|
+
const onSelectLanIp = React.useCallback((ip) => act(BRIDGE_ENDPOINTS.setLanIp, { ip }), [act]);
|
|
2573
|
+
|
|
2500
2574
|
const onStartCustom = React.useCallback(() => act(BRIDGE_ENDPOINTS.startCustomTunnel), [act]);
|
|
2501
2575
|
const onStopCustom = React.useCallback(() => act(BRIDGE_ENDPOINTS.stopCustomTunnel), [act]);
|
|
2502
2576
|
const onToggleCustomAutoStart = React.useCallback((autoStart) =>
|
|
@@ -2536,7 +2610,12 @@ function BridgePanel({ rpcCall }) {
|
|
|
2536
2610
|
data: { running: status?.proxy?.running, url: status?.lan?.url, qr: status?.lan?.qr },
|
|
2537
2611
|
auth: status?.auth,
|
|
2538
2612
|
onNavigateSecurity: navSecurity,
|
|
2539
|
-
}
|
|
2613
|
+
},
|
|
2614
|
+
React.createElement(LanNetworkSelector, {
|
|
2615
|
+
lan: status?.lan,
|
|
2616
|
+
onSelectIp: onSelectLanIp,
|
|
2617
|
+
})
|
|
2618
|
+
);
|
|
2540
2619
|
} else if (activeTab === 'tunnel') {
|
|
2541
2620
|
tabContent = React.createElement(React.Fragment, null,
|
|
2542
2621
|
React.createElement(TunnelCard, {
|
|
@@ -3028,6 +3107,163 @@ function injectMobileStyles() {
|
|
|
3028
3107
|
display: none !important;
|
|
3029
3108
|
}
|
|
3030
3109
|
|
|
3110
|
+
/* 3.0 工作区 Workbench / 任务管理 / 多 Tab 栏移动端自适应适配 */
|
|
3111
|
+
body:not(.dsh-workbench-open) div[class*="nArs4W_panel"],
|
|
3112
|
+
body:not(.dsh-workbench-open) div[class*="workbench_panel"],
|
|
3113
|
+
body:not(.dsh-workbench-open) div[class*="workbenchPanel"],
|
|
3114
|
+
div[class*="nArs4W_panel"][class*="panelHidden"],
|
|
3115
|
+
div[class*="workbench_panel"][class*="panelHidden"],
|
|
3116
|
+
div[class*="workbenchPanel"][class*="panelHidden"],
|
|
3117
|
+
div[class*="panelHidden"] {
|
|
3118
|
+
display: none !important;
|
|
3119
|
+
visibility: hidden !important;
|
|
3120
|
+
pointer-events: none !important;
|
|
3121
|
+
width: 0 !important;
|
|
3122
|
+
height: 0 !important;
|
|
3123
|
+
max-height: 0 !important;
|
|
3124
|
+
z-index: -1 !important;
|
|
3125
|
+
opacity: 0 !important;
|
|
3126
|
+
transform: translateX(105%) !important;
|
|
3127
|
+
}
|
|
3128
|
+
|
|
3129
|
+
body.dsh-workbench-open div[class*="nArs4W_panel"]:not([class*="panelHidden"]),
|
|
3130
|
+
body.dsh-workbench-open div[class*="workbench_panel"]:not([class*="panelHidden"]),
|
|
3131
|
+
body.dsh-workbench-open div[class*="workbenchPanel"]:not([class*="panelHidden"]) {
|
|
3132
|
+
display: flex !important;
|
|
3133
|
+
visibility: visible !important;
|
|
3134
|
+
pointer-events: auto !important;
|
|
3135
|
+
top: var(--dsh-mobile-header-h, 52px) !important;
|
|
3136
|
+
height: calc(100dvh - var(--dsh-mobile-header-h, 52px)) !important;
|
|
3137
|
+
max-height: calc(100dvh - var(--dsh-mobile-header-h, 52px)) !important;
|
|
3138
|
+
z-index: 50 !important;
|
|
3139
|
+
box-sizing: border-box !important;
|
|
3140
|
+
background: var(--dsw-alias-bg-layer-1, #ffffff) !important;
|
|
3141
|
+
transform: none !important;
|
|
3142
|
+
opacity: 1 !important;
|
|
3143
|
+
}
|
|
3144
|
+
|
|
3145
|
+
/* Tab 栏:横向滑动手势 + 干净的底部边框,杜绝与顶部移动端 Header 重叠 */
|
|
3146
|
+
div[class*="nArs4W_tabBar"],
|
|
3147
|
+
div[class*="workbench_tabBar"],
|
|
3148
|
+
div[class*="tabBar"] {
|
|
3149
|
+
min-height: 40px !important;
|
|
3150
|
+
height: 40px !important;
|
|
3151
|
+
background: var(--dsw-alias-bg-layer-1, #ffffff) !important;
|
|
3152
|
+
border-bottom: 1px solid rgba(0, 0, 0, 0.08) !important;
|
|
3153
|
+
display: flex !important;
|
|
3154
|
+
align-items: center !important;
|
|
3155
|
+
justify-content: space-between !important;
|
|
3156
|
+
padding: 0 8px !important;
|
|
3157
|
+
gap: 6px !important;
|
|
3158
|
+
overflow: visible !important;
|
|
3159
|
+
box-sizing: border-box !important;
|
|
3160
|
+
}
|
|
3161
|
+
|
|
3162
|
+
div[class*="nArs4W_tabList"],
|
|
3163
|
+
div[class*="tabList"] {
|
|
3164
|
+
display: flex !important;
|
|
3165
|
+
align-items: center !important;
|
|
3166
|
+
gap: 6px !important;
|
|
3167
|
+
flex: 1 1 auto !important;
|
|
3168
|
+
min-width: 0 !important;
|
|
3169
|
+
overflow-x: auto !important;
|
|
3170
|
+
scrollbar-width: none !important;
|
|
3171
|
+
-webkit-overflow-scrolling: touch !important;
|
|
3172
|
+
}
|
|
3173
|
+
div[class*="nArs4W_tabList"]::-webkit-scrollbar,
|
|
3174
|
+
div[class*="tabList"]::-webkit-scrollbar {
|
|
3175
|
+
display: none !important;
|
|
3176
|
+
}
|
|
3177
|
+
|
|
3178
|
+
/* 单个 Tab 胶囊化,文字超长自动打点,防止 Tab 互相挤压 */
|
|
3179
|
+
div[class*="nArs4W_tab"],
|
|
3180
|
+
div[class*="workbench_tab"] {
|
|
3181
|
+
flex: 0 0 auto !important;
|
|
3182
|
+
max-width: 170px !important;
|
|
3183
|
+
min-width: 70px !important;
|
|
3184
|
+
height: 30px !important;
|
|
3185
|
+
padding: 0 8px 0 10px !important;
|
|
3186
|
+
border-radius: 6px !important;
|
|
3187
|
+
font-size: 12.5px !important;
|
|
3188
|
+
display: inline-flex !important;
|
|
3189
|
+
align-items: center !important;
|
|
3190
|
+
justify-content: space-between !important;
|
|
3191
|
+
gap: 6px !important;
|
|
3192
|
+
background: var(--dsw-alias-bg-layer-2, #f3f4f6) !important;
|
|
3193
|
+
color: var(--dsw-alias-label-secondary, #6b7280) !important;
|
|
3194
|
+
cursor: pointer !important;
|
|
3195
|
+
user-select: none !important;
|
|
3196
|
+
box-sizing: border-box !important;
|
|
3197
|
+
}
|
|
3198
|
+
|
|
3199
|
+
div[class*="nArs4W_tabActive"],
|
|
3200
|
+
div[class*="workbench_tabActive"] {
|
|
3201
|
+
background: var(--dsw-alias-bg-layer-1, #ffffff) !important;
|
|
3202
|
+
color: var(--dsw-alias-label-primary, #111827) !important;
|
|
3203
|
+
font-weight: 600 !important;
|
|
3204
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1) !important;
|
|
3205
|
+
}
|
|
3206
|
+
|
|
3207
|
+
span[class*="nArs4W_tabTitle"],
|
|
3208
|
+
span[class*="tabTitle"] {
|
|
3209
|
+
overflow: hidden !important;
|
|
3210
|
+
text-overflow: ellipsis !important;
|
|
3211
|
+
white-space: nowrap !important;
|
|
3212
|
+
flex: 1 1 auto !important;
|
|
3213
|
+
}
|
|
3214
|
+
|
|
3215
|
+
button[class*="nArs4W_tabClose"],
|
|
3216
|
+
button[class*="tabClose"] {
|
|
3217
|
+
width: 18px !important;
|
|
3218
|
+
height: 18px !important;
|
|
3219
|
+
border-radius: 50% !important;
|
|
3220
|
+
display: inline-flex !important;
|
|
3221
|
+
align-items: center !important;
|
|
3222
|
+
justify-content: center !important;
|
|
3223
|
+
flex-shrink: 0 !important;
|
|
3224
|
+
opacity: 0.6 !important;
|
|
3225
|
+
padding: 0 !important;
|
|
3226
|
+
}
|
|
3227
|
+
|
|
3228
|
+
button[class*="nArs4W_tabBarPlus"],
|
|
3229
|
+
button[class*="tabBarPlus"] {
|
|
3230
|
+
width: 28px !important;
|
|
3231
|
+
height: 28px !important;
|
|
3232
|
+
border-radius: 50% !important;
|
|
3233
|
+
display: inline-flex !important;
|
|
3234
|
+
align-items: center !important;
|
|
3235
|
+
justify-content: center !important;
|
|
3236
|
+
flex-shrink: 0 !important;
|
|
3237
|
+
}
|
|
3238
|
+
|
|
3239
|
+
/* 移动端面板右上角“返回对话 / ✕ 收起”按钮:常驻右侧,醒目且易触达 */
|
|
3240
|
+
.dsh-mobile-panel-close-btn {
|
|
3241
|
+
margin-left: 8px !important;
|
|
3242
|
+
flex: 0 0 auto !important;
|
|
3243
|
+
height: 28px !important;
|
|
3244
|
+
padding: 0 10px !important;
|
|
3245
|
+
border-radius: 14px !important;
|
|
3246
|
+
font-size: 12px !important;
|
|
3247
|
+
font-weight: 600 !important;
|
|
3248
|
+
background: #2563eb !important;
|
|
3249
|
+
color: #ffffff !important;
|
|
3250
|
+
border: none !important;
|
|
3251
|
+
display: inline-flex !important;
|
|
3252
|
+
align-items: center !important;
|
|
3253
|
+
justify-content: center !important;
|
|
3254
|
+
gap: 4px !important;
|
|
3255
|
+
cursor: pointer !important;
|
|
3256
|
+
user-select: none !important;
|
|
3257
|
+
box-shadow: 0 2px 6px rgba(37, 99, 235, 0.28) !important;
|
|
3258
|
+
white-space: nowrap !important;
|
|
3259
|
+
transition: transform 0.1s, opacity 0.15s !important;
|
|
3260
|
+
z-index: 10 !important;
|
|
3261
|
+
}
|
|
3262
|
+
.dsh-mobile-panel-close-btn:active {
|
|
3263
|
+
transform: scale(0.95) !important;
|
|
3264
|
+
opacity: 0.85 !important;
|
|
3265
|
+
}
|
|
3266
|
+
|
|
3031
3267
|
/* 3.1 会话对话头部顶栏:移动端防挤压与空间释放优化(严格排除 .dsh-mobile-app-header) */
|
|
3032
3268
|
div[class*="_centerCol"] header,
|
|
3033
3269
|
header[class*="wSkVaW_header"] {
|
|
@@ -3222,19 +3458,53 @@ function injectMobileStyles() {
|
|
|
3222
3458
|
transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1);
|
|
3223
3459
|
overflow-y: auto !important;
|
|
3224
3460
|
border-right: 1px solid rgba(0, 0, 0, 0.06) !important;
|
|
3461
|
+
pointer-events: auto !important;
|
|
3225
3462
|
}
|
|
3226
3463
|
body.dsh-drawer-open div[class*="_sidebarCol"] {
|
|
3227
3464
|
transform: translateX(0) !important;
|
|
3228
3465
|
box-shadow: 4px 0 28px rgba(0, 0, 0, 0.25) !important;
|
|
3466
|
+
pointer-events: auto !important;
|
|
3229
3467
|
}
|
|
3230
3468
|
|
|
3231
|
-
/* 抽屉内部:强制 100%
|
|
3469
|
+
/* 抽屉内部:强制 100% 宽度,无论内部状态如何均正常展开并展示 DSH 自带的顶部收起侧边栏图标 */
|
|
3232
3470
|
body.dsh-drawer-open div[class*="hHd-Xa_root"] {
|
|
3233
3471
|
width: 100% !important;
|
|
3234
3472
|
max-width: 100% !important;
|
|
3473
|
+
min-width: 100% !important;
|
|
3474
|
+
display: flex !important;
|
|
3475
|
+
flex-direction: column !important;
|
|
3476
|
+
}
|
|
3477
|
+
body.dsh-drawer-open div[class*="hHd-Xa_collapsed"] div[class*="hHd-Xa_regionArea"],
|
|
3478
|
+
body.dsh-drawer-open div[class*="hHd-Xa_collapsed"] button[class*="hHd-Xa_newSession"],
|
|
3479
|
+
body.dsh-drawer-open div[class*="hHd-Xa_collapsed"] div[class*="qDHVXG_root"] {
|
|
3480
|
+
display: flex !important;
|
|
3481
|
+
visibility: visible !important;
|
|
3482
|
+
}
|
|
3483
|
+
div[class*="hHd-Xa_logoRow"] {
|
|
3484
|
+
display: flex !important;
|
|
3485
|
+
align-items: center !important;
|
|
3486
|
+
justify-content: space-between !important;
|
|
3487
|
+
width: 100% !important;
|
|
3488
|
+
padding: 10px 14px 6px 14px !important;
|
|
3489
|
+
box-sizing: border-box !important;
|
|
3235
3490
|
}
|
|
3236
3491
|
div[class*="hHd-Xa_logoRow"] button[class*="hHd-Xa_toggle"] {
|
|
3237
|
-
display:
|
|
3492
|
+
display: inline-flex !important;
|
|
3493
|
+
align-items: center !important;
|
|
3494
|
+
justify-content: center !important;
|
|
3495
|
+
width: 32px !important;
|
|
3496
|
+
height: 32px !important;
|
|
3497
|
+
border-radius: 8px !important;
|
|
3498
|
+
color: var(--dsw-alias-label-secondary, #6b7280) !important;
|
|
3499
|
+
background: transparent !important;
|
|
3500
|
+
border: none !important;
|
|
3501
|
+
cursor: pointer !important;
|
|
3502
|
+
margin-left: auto !important;
|
|
3503
|
+
transition: background 0.15s, color 0.15s !important;
|
|
3504
|
+
}
|
|
3505
|
+
div[class*="hHd-Xa_logoRow"] button[class*="hHd-Xa_toggle"]:active {
|
|
3506
|
+
background: var(--dsw-alias-bg-layer-2, rgba(0, 0, 0, 0.06)) !important;
|
|
3507
|
+
color: var(--dsw-alias-label-primary, #111827) !important;
|
|
3238
3508
|
}
|
|
3239
3509
|
|
|
3240
3510
|
/* 设置弹窗打开时解除抽屉隐藏限制 */
|
|
@@ -3252,8 +3522,6 @@ function injectMobileStyles() {
|
|
|
3252
3522
|
position: fixed;
|
|
3253
3523
|
inset: 0;
|
|
3254
3524
|
background: rgba(0, 0, 0, 0.4);
|
|
3255
|
-
backdrop-filter: blur(4px);
|
|
3256
|
-
-webkit-backdrop-filter: blur(4px);
|
|
3257
3525
|
z-index: 9999;
|
|
3258
3526
|
display: none !important;
|
|
3259
3527
|
}
|
|
@@ -3505,7 +3773,8 @@ function injectMobileStyles() {
|
|
|
3505
3773
|
|
|
3506
3774
|
@media (min-width: 769px) {
|
|
3507
3775
|
.dsh-mobile-app-header,
|
|
3508
|
-
.dsh-mobile-backdrop
|
|
3776
|
+
.dsh-mobile-backdrop,
|
|
3777
|
+
.dsh-mobile-panel-close-btn {
|
|
3509
3778
|
display: none !important;
|
|
3510
3779
|
}
|
|
3511
3780
|
}
|
|
@@ -3534,18 +3803,23 @@ function setupMobileExperience(rpcCall, ctx) {
|
|
|
3534
3803
|
<line x1="3" y1="15" x2="14" y2="15"></line>
|
|
3535
3804
|
</svg>
|
|
3536
3805
|
`;
|
|
3537
|
-
leftBtn.onclick = () => {
|
|
3806
|
+
leftBtn.onclick = (e) => {
|
|
3807
|
+
e.stopPropagation();
|
|
3538
3808
|
const isOpen = document.body.classList.toggle('dsh-drawer-open');
|
|
3539
3809
|
if (isOpen) {
|
|
3540
|
-
const
|
|
3541
|
-
if (
|
|
3810
|
+
const expand = document.querySelector('button[aria-label*="打开侧边栏"], button[title*="打开侧边栏"]');
|
|
3811
|
+
if (expand) expand.click();
|
|
3542
3812
|
}
|
|
3543
3813
|
};
|
|
3544
3814
|
|
|
3545
|
-
// 中间动态会话标题 (
|
|
3815
|
+
// 中间动态会话标题 (居中展示当前会话名称,点击可快速切回对话流)
|
|
3546
3816
|
titleEl = document.createElement('div');
|
|
3547
3817
|
titleEl.className = 'dsh-mobile-header-title';
|
|
3548
3818
|
titleEl.innerText = '新会话';
|
|
3819
|
+
titleEl.onclick = () => {
|
|
3820
|
+
const openPanels = document.querySelectorAll('div[class*="nArs4W_panel"]:not([class*="panelHidden"]), div[class*="workbench_panel"]:not([class*="panelHidden"])');
|
|
3821
|
+
openPanels.forEach((p) => p.classList.add('nArs4W_panelHidden'));
|
|
3822
|
+
};
|
|
3549
3823
|
|
|
3550
3824
|
// 右侧 (+) 新建会话按钮 (DeepSeek App 圆形加号风格)
|
|
3551
3825
|
const rightBtn = document.createElement('button');
|
|
@@ -3559,6 +3833,8 @@ function setupMobileExperience(rpcCall, ctx) {
|
|
|
3559
3833
|
</svg>
|
|
3560
3834
|
`;
|
|
3561
3835
|
rightBtn.onclick = () => {
|
|
3836
|
+
const openPanels = document.querySelectorAll('div[class*="nArs4W_panel"]:not([class*="panelHidden"]), div[class*="workbench_panel"]:not([class*="panelHidden"])');
|
|
3837
|
+
openPanels.forEach((p) => p.classList.add('nArs4W_panelHidden'));
|
|
3562
3838
|
const dshNewBtn = document.querySelector('button[aria-label="新建会话"]');
|
|
3563
3839
|
if (dshNewBtn) dshNewBtn.click();
|
|
3564
3840
|
};
|
|
@@ -3587,6 +3863,67 @@ function setupMobileExperience(rpcCall, ctx) {
|
|
|
3587
3863
|
if (typeof ctx?.sessions?.list?.subscribe === 'function') {
|
|
3588
3864
|
ctx.sessions.list.subscribe(syncMobileTitle);
|
|
3589
3865
|
}
|
|
3866
|
+
if (typeof ctx?.sessions?.active?.subscribe === 'function') {
|
|
3867
|
+
ctx.sessions.active.subscribe(() => {
|
|
3868
|
+
if (document.body.classList.contains('dsh-drawer-open')) {
|
|
3869
|
+
document.body.classList.remove('dsh-drawer-open');
|
|
3870
|
+
}
|
|
3871
|
+
// 仅在移动端切换会话时自动收起右侧面板回到对话(PC端绝不干扰)
|
|
3872
|
+
if (typeof window !== 'undefined' && window.innerWidth <= 768) {
|
|
3873
|
+
document.body.classList.remove('dsh-workbench-open');
|
|
3874
|
+
const openPanels = document.querySelectorAll('div[class*="nArs4W_panel"]:not([class*="panelHidden"]), div[class*="workbench_panel"]:not([class*="panelHidden"])');
|
|
3875
|
+
openPanels.forEach((p) => p.classList.add('nArs4W_panelHidden'));
|
|
3876
|
+
}
|
|
3877
|
+
});
|
|
3878
|
+
}
|
|
3879
|
+
|
|
3880
|
+
// 1.1 移动端右侧边栏 / Workbench 面板管理:仅在移动端(<= 768px)挂载“返回对话”收起按钮,PC端保持纯净
|
|
3881
|
+
const ensurePanelCloseButton = () => {
|
|
3882
|
+
if (typeof window === 'undefined') return;
|
|
3883
|
+
if (window.innerWidth > 768) {
|
|
3884
|
+
document.querySelectorAll('.dsh-mobile-panel-close-btn').forEach(btn => btn.remove());
|
|
3885
|
+
return;
|
|
3886
|
+
}
|
|
3887
|
+
const panels = document.querySelectorAll('div[class*="nArs4W_panel"]:not([class*="panelHidden"]), div[class*="workbench_panel"]:not([class*="panelHidden"])');
|
|
3888
|
+
panels.forEach((p) => {
|
|
3889
|
+
const bar = p.querySelector('div[class*="tabBar"], div[class*="nArs4W_tabBar"]');
|
|
3890
|
+
if (bar && !bar.querySelector('.dsh-mobile-panel-close-btn')) {
|
|
3891
|
+
const btn = document.createElement('button');
|
|
3892
|
+
btn.className = 'dsh-mobile-panel-close-btn';
|
|
3893
|
+
btn.innerHTML = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg><span>返回对话</span>`;
|
|
3894
|
+
btn.onclick = (e) => {
|
|
3895
|
+
e.stopPropagation();
|
|
3896
|
+
document.body.classList.remove('dsh-workbench-open');
|
|
3897
|
+
p.classList.add('nArs4W_panelHidden');
|
|
3898
|
+
const collapseBtn = document.querySelector('button[class*="toggleButton"][aria-label*="收起"]');
|
|
3899
|
+
if (collapseBtn) collapseBtn.click();
|
|
3900
|
+
};
|
|
3901
|
+
bar.appendChild(btn);
|
|
3902
|
+
}
|
|
3903
|
+
});
|
|
3904
|
+
};
|
|
3905
|
+
|
|
3906
|
+
const panelObserver = new MutationObserver(ensurePanelCloseButton);
|
|
3907
|
+
panelObserver.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['class'] });
|
|
3908
|
+
window.addEventListener('resize', ensurePanelCloseButton);
|
|
3909
|
+
|
|
3910
|
+
// 移动端点击面板/工作区触发按钮时,自动激活 dsh-workbench-open
|
|
3911
|
+
document.addEventListener('click', (e) => {
|
|
3912
|
+
if (typeof window === 'undefined' || window.innerWidth > 768) return;
|
|
3913
|
+
const trigger = e.target.closest('button[aria-label*="面板"], button[aria-label*="工作区"], div[class*="toggleCluster"] button, button[class*="subagent"], div[class*="headerActions"] button, div[class*="titleRow"] button');
|
|
3914
|
+
if (trigger && !trigger.classList.contains('dsh-mobile-panel-close-btn') && !trigger.classList.contains('dsh-header-menu-btn') && !trigger.classList.contains('dsh-header-new-btn')) {
|
|
3915
|
+
document.body.classList.add('dsh-workbench-open');
|
|
3916
|
+
}
|
|
3917
|
+
}, true);
|
|
3918
|
+
|
|
3919
|
+
// 移动端点击 DSH 自带的收起侧边栏图标时,自动收起抽屉
|
|
3920
|
+
document.addEventListener('click', (e) => {
|
|
3921
|
+
if (typeof window === 'undefined' || window.innerWidth > 768) return;
|
|
3922
|
+
const toggle = e.target.closest('button[aria-label*="收起侧边栏"], button[title*="收起侧边栏"]');
|
|
3923
|
+
if (toggle) {
|
|
3924
|
+
document.body.classList.remove('dsh-drawer-open');
|
|
3925
|
+
}
|
|
3926
|
+
}, true);
|
|
3590
3927
|
|
|
3591
3928
|
// 2. 创建背景遮罩(用于抽屉侧边栏)
|
|
3592
3929
|
let backdrop = document.querySelector('.dsh-mobile-backdrop');
|
|
@@ -3599,7 +3936,7 @@ function setupMobileExperience(rpcCall, ctx) {
|
|
|
3599
3936
|
document.body.appendChild(backdrop);
|
|
3600
3937
|
}
|
|
3601
3938
|
|
|
3602
|
-
// 3.
|
|
3939
|
+
// 3. 点击遮罩收起抽屉,或点击抽屉内的会话项自动收起抽屉
|
|
3603
3940
|
let lastLongPressTime = 0;
|
|
3604
3941
|
|
|
3605
3942
|
document.addEventListener('click', (e) => {
|
|
@@ -3647,11 +3984,17 @@ function setupMobileExperience(rpcCall, ctx) {
|
|
|
3647
3984
|
}
|
|
3648
3985
|
}
|
|
3649
3986
|
|
|
3650
|
-
//
|
|
3651
|
-
const sessionRow = e.target.closest('div[class*="sessionRow"], div[role="treeitem"]');
|
|
3987
|
+
// 点击会话项后平滑收起抽屉
|
|
3988
|
+
const sessionRow = e.target.closest('a, div[class*="sessionRow"], div[role="treeitem"]');
|
|
3652
3989
|
if (sessionRow) {
|
|
3653
|
-
|
|
3990
|
+
setTimeout(() => {
|
|
3991
|
+
if (document.body.classList.contains('dsh-drawer-open')) {
|
|
3992
|
+
document.body.classList.remove('dsh-drawer-open');
|
|
3993
|
+
}
|
|
3994
|
+
}, 80);
|
|
3654
3995
|
}
|
|
3996
|
+
} else if (e.target.classList?.contains('dsh-mobile-backdrop')) {
|
|
3997
|
+
document.body.classList.remove('dsh-drawer-open');
|
|
3655
3998
|
}
|
|
3656
3999
|
}, true);
|
|
3657
4000
|
|
|
@@ -4346,32 +4689,7 @@ function RemoteDirectoryFlow(props) {
|
|
|
4346
4689
|
outcome.current = props;
|
|
4347
4690
|
const armed = React.useRef(false);
|
|
4348
4691
|
|
|
4349
|
-
|
|
4350
|
-
if (!open) {
|
|
4351
|
-
armed.current = false;
|
|
4352
|
-
return;
|
|
4353
|
-
}
|
|
4354
|
-
if (armed.current) return;
|
|
4355
|
-
armed.current = true;
|
|
4356
|
-
|
|
4357
|
-
// 1. 本机电脑访问(Localhost / 127.0.0.1 / Electron 客户端):直接唤起系统原生文件夹选择对话框!
|
|
4358
|
-
if (isLocalEnvironment()) {
|
|
4359
|
-
const pickFn = typeof pick === 'function' ? pick : (typeof window.__dshClientCtx?.workspaces?.pickDirectory === 'function' ? () => window.__dshClientCtx.workspaces.pickDirectory() : null);
|
|
4360
|
-
if (pickFn) {
|
|
4361
|
-
pickFn().then((chosenPath) => {
|
|
4362
|
-
if (chosenPath === null) {
|
|
4363
|
-
if (outcome.current?.onCancel) outcome.current.onCancel();
|
|
4364
|
-
} else if (chosenPath) {
|
|
4365
|
-
if (outcome.current?.onPicked) outcome.current.onPicked(chosenPath);
|
|
4366
|
-
}
|
|
4367
|
-
}, (err) => {
|
|
4368
|
-
if (outcome.current?.onError) outcome.current.onError(err instanceof Error ? err.message : String(err));
|
|
4369
|
-
});
|
|
4370
|
-
return;
|
|
4371
|
-
}
|
|
4372
|
-
}
|
|
4373
|
-
|
|
4374
|
-
// 2. 远程或移动端访问(手机或局域网跨设备/公网):呼出网页版远程目录树形选择器!
|
|
4692
|
+
const openRemoteModal = () => {
|
|
4375
4693
|
if (typeof window.__dshOpenRemoteWorkspaceModal === 'function') {
|
|
4376
4694
|
window.__dshOpenRemoteWorkspaceModal(
|
|
4377
4695
|
(res) => {
|
|
@@ -4390,7 +4708,48 @@ function RemoteDirectoryFlow(props) {
|
|
|
4390
4708
|
}
|
|
4391
4709
|
}
|
|
4392
4710
|
);
|
|
4711
|
+
} else if (outcome.current?.onCancel) {
|
|
4712
|
+
outcome.current.onCancel();
|
|
4713
|
+
}
|
|
4714
|
+
};
|
|
4715
|
+
|
|
4716
|
+
React.useEffect(() => {
|
|
4717
|
+
if (!open) {
|
|
4718
|
+
armed.current = false;
|
|
4719
|
+
return;
|
|
4393
4720
|
}
|
|
4721
|
+
if (armed.current) return;
|
|
4722
|
+
armed.current = true;
|
|
4723
|
+
|
|
4724
|
+
// 1. 本机电脑或 Electron 环境:尝试唤起原生文件夹选择对话框
|
|
4725
|
+
const isNativeHost = typeof window !== 'undefined' && (window.electron || window.__DSH_NATIVE_HOST__);
|
|
4726
|
+
if (isNativeHost || isLocalEnvironment()) {
|
|
4727
|
+
const pickFn = typeof pick === 'function' ? pick : (typeof window.__dshClientCtx?.workspaces?.pickDirectory === 'function' ? () => window.__dshClientCtx.workspaces.pickDirectory() : null);
|
|
4728
|
+
if (pickFn) {
|
|
4729
|
+
try {
|
|
4730
|
+
const promise = pickFn();
|
|
4731
|
+
if (promise && typeof promise.then === 'function') {
|
|
4732
|
+
promise.then((chosenPath) => {
|
|
4733
|
+
if (chosenPath === null) {
|
|
4734
|
+
if (outcome.current?.onCancel) outcome.current.onCancel();
|
|
4735
|
+
} else if (chosenPath) {
|
|
4736
|
+
if (outcome.current?.onPicked) outcome.current.onPicked(chosenPath);
|
|
4737
|
+
}
|
|
4738
|
+
}).catch(() => {
|
|
4739
|
+
// 原生选择器在纯网页模式下报 needs the native capability,平滑降级至远程目录树选择器
|
|
4740
|
+
openRemoteModal();
|
|
4741
|
+
});
|
|
4742
|
+
return;
|
|
4743
|
+
}
|
|
4744
|
+
} catch {
|
|
4745
|
+
openRemoteModal();
|
|
4746
|
+
return;
|
|
4747
|
+
}
|
|
4748
|
+
}
|
|
4749
|
+
}
|
|
4750
|
+
|
|
4751
|
+
// 2. 远程或移动端访问(手机或局域网跨设备/公网):呼出网页版远程目录树形选择器!
|
|
4752
|
+
openRemoteModal();
|
|
4394
4753
|
}, [open, pick]);
|
|
4395
4754
|
|
|
4396
4755
|
return null;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# Fix: 自建隧道 SSE 504 超时 & 会话列表不显示
|
|
2
|
+
|
|
3
|
+
## 问题现象
|
|
4
|
+
|
|
5
|
+
通过自建 WebSocket 隧道(`wss://` 中转服务器)远程访问 DSH 时,出现两个问题:
|
|
6
|
+
|
|
7
|
+
1. **`/plugins/events` SSE 端点返回 504 超时** — HMR 插件的 Server-Sent Events 连接永远不结束,隧道服务器超时后返回 504
|
|
8
|
+
2. **Web 侧边栏会话列表不显示** — 页面其他功能正常,唯独会话列表空白
|
|
9
|
+
|
|
10
|
+
## 根因分析
|
|
11
|
+
|
|
12
|
+
### 问题 1:SSE 504 超时
|
|
13
|
+
|
|
14
|
+
隧道协议将 HTTP 请求/响应封装为 WebSocket 消息:tunnel client 向本地 DSH 发起 HTTP 请求,**缓冲完整响应**后再通过 WebSocket 回传给 tunnel server,再由 tunnel server 转发给浏览器。
|
|
15
|
+
|
|
16
|
+
SSE(`text/event-stream`)响应是**永不结束**的流式连接 — 服务器持续推送事件,不调用 `res.end()`。原代码的 `res.on('end')` 回调永远不会触发,tunnel server 的超时(30s 非 API 路径 / 120s API 路径)到期后返回 504。
|
|
17
|
+
|
|
18
|
+
### 问题 2:会话列表不显示
|
|
19
|
+
|
|
20
|
+
DSH 的 `session.list` RPC 返回所有会话的完整投影数据。在会话数量较多时(233 个会话),响应体高达 **63MB**,其中:
|
|
21
|
+
|
|
22
|
+
| 字段 | 大小 | 占比 |
|
|
23
|
+
|------|------|------|
|
|
24
|
+
| `contextHeaders` | 57.4 MB | 92% |
|
|
25
|
+
| `contextTimeline` | 5.0 MB | 8% |
|
|
26
|
+
| 其他投影字段 | ~0.2 MB | <1% |
|
|
27
|
+
|
|
28
|
+
`contextHeaders` 包含每个会话的完整对话上下文头(系统提示、消息头等),侧边栏列表**完全不需要**这些数据 — 打开会话时通过 WebSocket 实时获取即可。
|
|
29
|
+
|
|
30
|
+
即使 gzip 压缩后仍有 13.5MB,通过 WebSocket 隧道传输需要 ~33s,超过 DSH 客户端的 `AbortSignal.timeout(30000)` 默认超时,导致请求被中止。
|
|
31
|
+
|
|
32
|
+
## 修复方案
|
|
33
|
+
|
|
34
|
+
### 修复 1:SSE 流式响应提前返回
|
|
35
|
+
|
|
36
|
+
### 修复 1:SSE 流式响应提前返回
|
|
37
|
+
|
|
38
|
+
在 `_handleHttpRequest()` 中检测 `text/event-stream` content-type,收集初始数据(2 个 chunk 或 500ms 超时)后立即返回响应,不等 `res.end()`:
|
|
39
|
+
|
|
40
|
+
```javascript
|
|
41
|
+
if (isSSE) {
|
|
42
|
+
let sseSent = false;
|
|
43
|
+
const sseTimer = setTimeout(() => { /* 发送已收集数据 */ }, 500);
|
|
44
|
+
res.on('data', (c) => {
|
|
45
|
+
if (sseSent) return;
|
|
46
|
+
chunks.push(c);
|
|
47
|
+
if (chunks.length >= 2) {
|
|
48
|
+
clearTimeout(sseTimer);
|
|
49
|
+
sseSent = true;
|
|
50
|
+
// 立即发送已收集的初始数据
|
|
51
|
+
this._sendMessage({ type: 'response', ... });
|
|
52
|
+
res.destroy();
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
// ... error/end 处理
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### 修复 2:剥离 session.list 中的大字段
|
|
61
|
+
|
|
62
|
+
对 `/api/session.list` 的 200 响应,解析 JSON 并删除每个会话投影中的 `contextHeaders` 和 `contextTimeline`:
|
|
63
|
+
|
|
64
|
+
```javascript
|
|
65
|
+
if (path === '/api/session.list' && res.statusCode === 200) {
|
|
66
|
+
try {
|
|
67
|
+
const json = JSON.parse(bodyBuf.toString('utf8'));
|
|
68
|
+
if (json?.result?.ok && json.result.value?.items) {
|
|
69
|
+
for (const item of json.result.value.items) {
|
|
70
|
+
const proj = item?.projections?.values;
|
|
71
|
+
if (proj) {
|
|
72
|
+
delete proj.contextHeaders;
|
|
73
|
+
delete proj.contextTimeline;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
bodyBuf = Buffer.from(JSON.stringify(json), 'utf8');
|
|
77
|
+
}
|
|
78
|
+
} catch {}
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### 修复 3:大响应 gzip 压缩
|
|
83
|
+
|
|
84
|
+
对超过 100KB 的可压缩响应(`text/*`、`application/json` 等)自动 gzip 压缩,设置 `content-encoding: gzip` 头:
|
|
85
|
+
|
|
86
|
+
```javascript
|
|
87
|
+
if (bodyBuf.length > GZIP_THRESHOLD && compressible && !alreadyEncoded) {
|
|
88
|
+
bodyBuf = gzipSync(bodyBuf);
|
|
89
|
+
respHeaders['content-encoding'] = 'gzip';
|
|
90
|
+
respHeaders['content-length'] = String(bodyBuf.length);
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### 修复 4:启用 WebSocket per-message 压缩
|
|
95
|
+
|
|
96
|
+
将隧道 WebSocket 的 `perMessageDeflate` 从 `false` 改为启用,进一步减少隧道传输量:
|
|
97
|
+
|
|
98
|
+
```javascript
|
|
99
|
+
this.ws = new WebSocket(url.toString(), {
|
|
100
|
+
handshakeTimeout: 10000,
|
|
101
|
+
perMessageDeflate: {
|
|
102
|
+
clientNoContextTakeover: true,
|
|
103
|
+
serverNoContextTakeover: true,
|
|
104
|
+
clientMaxWindowBits: 15,
|
|
105
|
+
serverMaxWindowBits: 15,
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## 效果
|
|
111
|
+
|
|
112
|
+
| 指标 | 修复前 | 修复后 |
|
|
113
|
+
|------|--------|--------|
|
|
114
|
+
| `session.list` 响应大小 | 63 MB | ~290 KB (剥离后) / 31 KB (gzip 后) |
|
|
115
|
+
| `session.list` 隧道传输时间 | 33s (超时) | 0.74s |
|
|
116
|
+
| `/plugins/events` SSE | 504 超时 | 正常返回初始数据 |
|
|
117
|
+
| 侧边栏会话列表 | 不显示 | 正常显示 |
|
|
118
|
+
|
|
119
|
+
## 影响范围
|
|
120
|
+
|
|
121
|
+
- **非隧道模式不受影响**:所有修改仅在 `_handleHttpRequest()` 中,只影响自建隧道的 HTTP 代理路径
|
|
122
|
+
- **WebSocket 透传不受影响**:`/api/events.mux` 和 `/api/events.host` 的 WebSocket 升级走独立的 `_handleWsOpen()` 路径,不受此修改影响
|
|
123
|
+
- **非 session.list 请求不受影响**:contextHeaders 剥离仅对 `/api/session.list` 路径生效
|
|
124
|
+
- **小响应不受影响**:gzip 压缩仅对超过 100KB 的可压缩响应生效
|
|
125
|
+
- **已有 content-encoding 的响应不受影响**:不重复压缩
|
|
126
|
+
|
|
127
|
+
## 测试验证
|
|
128
|
+
|
|
129
|
+
- ✅ 本地 WebSocket 直连 DSH(`/api/events.mux`)正常接收 `session/subscribed` 事件
|
|
130
|
+
- ✅ 通过隧道的 WebSocket(`wss://ds.missus.top/api/events.mux`)正常接收 26+ 条消息
|
|
131
|
+
- ✅ 通过隧道的 `session.list` 请求:31KB / 0.74s(修复前 63MB / 33s 超时)
|
|
132
|
+
- ✅ 响应包含 234 个会话,`contextHeaders` 和 `contextTimeline` 已剥离,`title` 等字段完整
|
|
133
|
+
- ✅ TLS 证书有效,gzip content-encoding 头正确传递
|
|
134
|
+
>>>>>>> pr-21
|