@wenbin_wb/dsh-bridge 2.10.7 → 2.10.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 +14 -0
- package/client/client.js +388 -5
- package/client/index.js +327 -3
- package/client/mobile-styles.js +68 -0
- package/lib/auth/manager.js +54 -4
- package/lib/bridge-rpc-constants.js +1 -0
- package/lib/bridge-rpc.js +8 -0
- package/lib/index.js +158 -2
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,20 @@
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
## [v2.10.8] - 2026-09-08
|
|
8
|
+
|
|
9
|
+
### ✨ 新功能
|
|
10
|
+
|
|
11
|
+
- **登录 Session 持久化**(issue #36,PR #37):宿主(dsh web)进程重启后,已登录设备不再需要重输访问密码——手机远程访问的登录态不再随每次 DSH 重启丢失。改密码 / 切换模式 / 重新生成 Token 仍会同步吊销全部旧会话(安全语义保持);文件损坏/写盘失败安全降级。
|
|
12
|
+
- **设置页检测 DSH 宿主更新并一键升级**:版本横幅同步检测 `@deepseek-ai/dsh` 线上最新版,发现新版时黄色提示;检测到 npm 全局安装形态时提供「一键升级 DSH」按钮(升级后引导重启),非 npm 安装(源码 / 打包加壳等)仅提醒并给官方升级方式,避免误导。
|
|
13
|
+
- **移动端输入框可折叠**:聊天头部工具栏(Session 下载钮旁)新增折叠按钮,收起底部输入区后消息阅读区最大化(约 +20% 可视高度);折叠时底部保留「✏️ 点击输入消息」细条一键唤回;折叠偏好跨会话记忆。
|
|
14
|
+
|
|
15
|
+
### 🐞 修复
|
|
16
|
+
|
|
17
|
+
- **iOS Safari 键盘弹起时聊天输入框上下跳动**:输入区为 sticky 吸底布局,键盘弹起时随 visual viewport 收缩反复重排抖动——现键盘弹起时固定输入框所属滚动容器高度,键盘收起后还原,仅 iOS 生效、不依赖宿主类名。
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
7
21
|
## [v2.10.7] - 2026-09-08
|
|
8
22
|
|
|
9
23
|
### 🐞 修复
|
package/client/client.js
CHANGED
|
@@ -425,6 +425,74 @@ var MOBILE_STYLES_CSS = `
|
|
|
425
425
|
padding-bottom: max(16px, env(safe-area-inset-bottom)) !important;
|
|
426
426
|
}
|
|
427
427
|
|
|
428
|
+
/* \u2500\u2500 \u79FB\u52A8\u7AEF\u8F93\u5165\u6846\u6298\u53E0\u6A21\u5F0F\uFF08body.dsh-composer-collapsed\uFF09\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
429
|
+
\u6298\u53E0\u540E\u9690\u85CF\u5438\u5E95\u8F93\u5165\u533A\uFF0C\u8BA9\u6D88\u606F viewArea \u81EA\u52A8\u4F38\u5C55\u5230\u5168\u9AD8\uFF0C\u6700\u5927\u5316\u9605\u8BFB\u533A\u3002
|
|
430
|
+
\u5165\u53E3\uFF1A\u804A\u5929\u5934\u90E8\u5DE5\u5177\u680F\uFF08Session \u4E0B\u8F7D\u94AE\u65C1\uFF09\u7684\u6298\u53E0\u6309\u94AE\uFF0C\u6216\u6298\u53E0\u6001\u5E95\u90E8\u7EC6\u8F93\u5165\u6761\u70B9\u5B83\u5524\u56DE\u3002
|
|
431
|
+
\u72B6\u6001\u6301\u4E45\u5316\u5230 localStorage\u3002 */
|
|
432
|
+
body.dsh-composer-collapsed div[class*="wSkVaW_composerSeat"] {
|
|
433
|
+
display: none !important;
|
|
434
|
+
}
|
|
435
|
+
body.dsh-composer-collapsed div[class*="wSkVaW_viewArea"] {
|
|
436
|
+
flex: 1 1 auto !important;
|
|
437
|
+
height: auto !important;
|
|
438
|
+
min-height: 0 !important;
|
|
439
|
+
}
|
|
440
|
+
/* \u6298\u53E0\u65F6\u6EDA\u52A8\u533A\u5E95\u90E8\u5BF9\u9F50 safe-area\uFF0C\u907F\u514D\u5185\u5BB9\u88AB iPhone \u5E95\u90E8\u6A2A\u6761\u906E\u6321 */
|
|
441
|
+
body.dsh-composer-collapsed div[class*="wSkVaW_scrollBody"] {
|
|
442
|
+
padding-bottom: max(16px, env(safe-area-inset-bottom)) !important;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/* \u6298\u53E0\u8F93\u5165\u6846\u6309\u94AE\uFF1A\u6CE8\u5165\u5728\u804A\u5929\u5934\u90E8\u5DE5\u5177\u680F\uFF08Session \u4E0B\u8F7D\u94AE\u65C1\uFF09\uFF0C
|
|
446
|
+
\u4E0E DSH sessionLogButton \u540C\u89C4\u683C\uFF0828px \u5706\u5F62\u56FE\u6807\u94AE\uFF09\uFF0C\u89C6\u89C9\u4E0E\u539F\u751F\u4E00\u81F4 */
|
|
447
|
+
.dsh-header-fold-btn {
|
|
448
|
+
min-width: 28px !important;
|
|
449
|
+
width: 28px !important;
|
|
450
|
+
height: 28px !important;
|
|
451
|
+
padding: 0 !important;
|
|
452
|
+
border-radius: 50% !important;
|
|
453
|
+
display: inline-flex !important;
|
|
454
|
+
align-items: center !important;
|
|
455
|
+
justify-content: center !important;
|
|
456
|
+
flex-shrink: 0 !important;
|
|
457
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.1)) !important;
|
|
458
|
+
background: var(--dsw-alias-bg-layer-2, rgba(0, 0, 0, 0.03)) !important;
|
|
459
|
+
color: var(--dsw-alias-label-secondary, #6b7280) !important;
|
|
460
|
+
cursor: pointer !important;
|
|
461
|
+
transition: opacity 0.15s !important;
|
|
462
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03) !important;
|
|
463
|
+
-webkit-tap-highlight-color: transparent !important;
|
|
464
|
+
}
|
|
465
|
+
.dsh-header-fold-btn:active {
|
|
466
|
+
opacity: 0.6 !important;
|
|
467
|
+
}
|
|
468
|
+
.dsh-header-fold-btn svg {
|
|
469
|
+
width: 15px !important;
|
|
470
|
+
height: 15px !important;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/* \u6298\u53E0\u6001\u5E95\u90E8\u7EC6\u8F93\u5165\u6761\uFF1A\u70B9\u51FB\u5524\u8D77\u8F93\u5165\u6846\uFF08\u4F4D\u4E8E\u539F\u8F93\u5165\u533A\u4F4D\u7F6E\uFF0Csticky \u5E95\u90E8\uFF09 */
|
|
474
|
+
.dsh-composer-collapsed-bar {
|
|
475
|
+
display: none !important;
|
|
476
|
+
position: sticky !important;
|
|
477
|
+
bottom: 0 !important;
|
|
478
|
+
margin: 8px 12px max(8px, env(safe-area-inset-bottom, 0px)) 12px !important;
|
|
479
|
+
padding: 11px 16px !important;
|
|
480
|
+
border-radius: 22px !important;
|
|
481
|
+
background: var(--dsw-alias-bg-layer-2, #f4f4f7) !important;
|
|
482
|
+
border: 1px solid rgba(0, 0, 0, 0.07) !important;
|
|
483
|
+
color: var(--dsw-alias-label-tertiary, #8b93a1) !important;
|
|
484
|
+
font-size: 13.5px !important;
|
|
485
|
+
line-height: 1.4 !important;
|
|
486
|
+
cursor: pointer !important;
|
|
487
|
+
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.04) !important;
|
|
488
|
+
-webkit-tap-highlight-color: transparent !important;
|
|
489
|
+
z-index: 40 !important;
|
|
490
|
+
box-sizing: border-box !important;
|
|
491
|
+
}
|
|
492
|
+
body.dsh-composer-collapsed .dsh-composer-collapsed-bar {
|
|
493
|
+
display: block !important;
|
|
494
|
+
}
|
|
495
|
+
|
|
428
496
|
/* \u8F93\u5165\u5361\u7247\uFF1ADeepSeek App \u5706\u89D2\u5927\u80F6\u56CA\u9020\u578B */
|
|
429
497
|
div[class*="uV2eYG_card"] {
|
|
430
498
|
border-radius: 26px !important;
|
|
@@ -999,6 +1067,7 @@ var BRIDGE_ENDPOINTS = {
|
|
|
999
1067
|
setLanIp: "setLanIp",
|
|
1000
1068
|
checkVersion: "checkVersion",
|
|
1001
1069
|
upgradePlugin: "upgradePlugin",
|
|
1070
|
+
upgradeDsh: "upgradeDsh",
|
|
1002
1071
|
restartDsh: "restartDsh",
|
|
1003
1072
|
exportBackup: "exportBackup",
|
|
1004
1073
|
importBackup: "importBackup",
|
|
@@ -3690,6 +3759,7 @@ function VersionBanner({ rpcCall }) {
|
|
|
3690
3759
|
}, [check]);
|
|
3691
3760
|
const hasUpdate = info?.latest && info?.current && !info.error && semverGt(info.latest, info.current);
|
|
3692
3761
|
const isLatest = info?.latest && info?.current && !info.error && !semverGt(info.latest, info.current);
|
|
3762
|
+
const dshHasUpdate = !!(info?.dshLatest && info?.dshVersion && !info.error && semverGt(info.dshLatest, info.dshVersion));
|
|
3693
3763
|
const handleUpgrade = React.useCallback(async () => {
|
|
3694
3764
|
if (!info?.latest || upgrading) return;
|
|
3695
3765
|
setUpgrading(true);
|
|
@@ -3711,6 +3781,28 @@ function VersionBanner({ rpcCall }) {
|
|
|
3711
3781
|
setUpgrading(false);
|
|
3712
3782
|
}
|
|
3713
3783
|
}, [info?.latest, upgrading, rpcCall]);
|
|
3784
|
+
const [dshUpgrading, setDshUpgrading] = React.useState(false);
|
|
3785
|
+
const [dshUpgradeResult, setDshUpgradeResult] = React.useState(null);
|
|
3786
|
+
const handleUpgradeDsh = React.useCallback(async () => {
|
|
3787
|
+
if (!info?.dshLatest || dshUpgrading) return;
|
|
3788
|
+
setDshUpgrading(true);
|
|
3789
|
+
setDshUpgradeResult(null);
|
|
3790
|
+
setDismissRestart(false);
|
|
3791
|
+
resetRestartStatus();
|
|
3792
|
+
try {
|
|
3793
|
+
const r = await rpcCall(BRIDGE_ENDPOINTS.upgradeDsh, { version: info.dshLatest });
|
|
3794
|
+
if (r?.ok && r.value?.ok) {
|
|
3795
|
+
setDshUpgradeResult({ ok: true, message: `DSH \u5DF2\u6210\u529F\u5347\u7EA7\u5230 v${info.dshLatest}\uFF01` });
|
|
3796
|
+
} else {
|
|
3797
|
+
const msg = r?.value?.error || r?.error?.message || "\u5347\u7EA7\u5931\u8D25";
|
|
3798
|
+
setDshUpgradeResult({ ok: false, message: msg, manual: true });
|
|
3799
|
+
}
|
|
3800
|
+
} catch (e) {
|
|
3801
|
+
setDshUpgradeResult({ ok: false, message: e.message || "\u5347\u7EA7\u8BF7\u6C42\u5931\u8D25", manual: true });
|
|
3802
|
+
} finally {
|
|
3803
|
+
setDshUpgrading(false);
|
|
3804
|
+
}
|
|
3805
|
+
}, [info?.dshLatest, dshUpgrading, rpcCall]);
|
|
3714
3806
|
const links = React.createElement(
|
|
3715
3807
|
"div",
|
|
3716
3808
|
{ style: { display: "flex", gap: 10, alignItems: "center", flexWrap: "wrap" } },
|
|
@@ -3775,24 +3867,26 @@ function VersionBanner({ rpcCall }) {
|
|
|
3775
3867
|
hasUpdate && React.createElement("span", { style: { fontWeight: 600, fontSize: 11 } }, `\u2794 v${info.latest}`),
|
|
3776
3868
|
info?.error && React.createElement("span", { style: { color: "var(--dsw-alias-state-warn-primary,#d97706)", fontSize: 11 } }, "(\u7F51\u7EDC\u8D85\u65F6)")
|
|
3777
3869
|
),
|
|
3778
|
-
// DSH
|
|
3870
|
+
// DSH 宿主版本标签(有新版时黄色高亮)
|
|
3779
3871
|
info?.dshVersion && React.createElement(
|
|
3780
3872
|
"span",
|
|
3781
3873
|
{
|
|
3782
3874
|
style: {
|
|
3783
3875
|
...s.tag,
|
|
3784
|
-
background: "var(--dsw-alias-bg-layer-2,#f3f4f6)",
|
|
3785
|
-
color: "var(--dsw-alias-label-tertiary,#6b7280)",
|
|
3876
|
+
background: dshHasUpdate ? "var(--dsw-alias-state-warn-bg,#fffbeb)" : "var(--dsw-alias-bg-layer-2,#f3f4f6)",
|
|
3877
|
+
color: dshHasUpdate ? "var(--dsw-alias-state-warn-primary,#d97706)" : "var(--dsw-alias-label-tertiary,#6b7280)",
|
|
3786
3878
|
padding: "3px 10px",
|
|
3787
3879
|
fontSize: 12,
|
|
3788
3880
|
fontWeight: 500,
|
|
3789
3881
|
display: "inline-flex",
|
|
3790
3882
|
alignItems: "center",
|
|
3791
3883
|
gap: 5
|
|
3792
|
-
}
|
|
3884
|
+
},
|
|
3885
|
+
title: dshHasUpdate ? `\u53D1\u73B0 DSH \u65B0\u7248\u672C v${info.dshLatest}` : void 0
|
|
3793
3886
|
},
|
|
3794
3887
|
React.createElement("span", { style: { opacity: 0.75, fontSize: 11, fontWeight: 400 } }, "DSH"),
|
|
3795
|
-
`v${info.dshVersion}
|
|
3888
|
+
`v${info.dshVersion}`,
|
|
3889
|
+
dshHasUpdate && React.createElement("span", { style: { fontWeight: 600, fontSize: 11 } }, `\u2794 v${info.dshLatest}`)
|
|
3796
3890
|
),
|
|
3797
3891
|
// 刷新检查按钮
|
|
3798
3892
|
React.createElement(
|
|
@@ -3982,6 +4076,109 @@ function VersionBanner({ rpcCall }) {
|
|
|
3982
4076
|
)
|
|
3983
4077
|
)
|
|
3984
4078
|
)
|
|
4079
|
+
),
|
|
4080
|
+
// ── DSH 宿主有新版本时的提示 / 一键升级卡片 ──
|
|
4081
|
+
dshHasUpdate && React.createElement(
|
|
4082
|
+
"div",
|
|
4083
|
+
{
|
|
4084
|
+
style: {
|
|
4085
|
+
...s.card,
|
|
4086
|
+
background: "var(--dsw-alias-state-warn-bg,#fffbeb)",
|
|
4087
|
+
border: "1px solid var(--dsw-alias-state-warn-border,#fde68a)",
|
|
4088
|
+
padding: "14px 16px",
|
|
4089
|
+
marginTop: 10,
|
|
4090
|
+
marginBottom: 0
|
|
4091
|
+
}
|
|
4092
|
+
},
|
|
4093
|
+
React.createElement(
|
|
4094
|
+
"div",
|
|
4095
|
+
{ style: { display: "flex", alignItems: "flex-start", gap: 12 } },
|
|
4096
|
+
React.createElement("span", { style: { fontSize: 22 } }, "\u{1F6E0}\uFE0F"),
|
|
4097
|
+
React.createElement(
|
|
4098
|
+
"div",
|
|
4099
|
+
{ style: { flex: 1, minWidth: 0 } },
|
|
4100
|
+
React.createElement(
|
|
4101
|
+
"div",
|
|
4102
|
+
{ style: { display: "flex", alignItems: "center", justifyContent: "space-between", flexWrap: "wrap", gap: 8, marginBottom: 8 } },
|
|
4103
|
+
React.createElement("div", {
|
|
4104
|
+
style: { fontSize: 13, fontWeight: 600, color: "var(--dsw-alias-state-warn-primary,#92400e)" }
|
|
4105
|
+
}, `DSH \u6709\u65B0\u7248\u672C v${info.dshLatest}\uFF08\u5F53\u524D v${info.dshVersion}\uFF09`),
|
|
4106
|
+
info?.dshUpgradable ? React.createElement(
|
|
4107
|
+
"button",
|
|
4108
|
+
{
|
|
4109
|
+
style: {
|
|
4110
|
+
...s.btnPri,
|
|
4111
|
+
height: 28,
|
|
4112
|
+
fontSize: 12,
|
|
4113
|
+
padding: "0 14px",
|
|
4114
|
+
background: dshUpgradeResult?.ok ? "var(--dsw-alias-state-success-primary,#059669)" : "var(--dsw-alias-brand-primary,#4f6ef7)",
|
|
4115
|
+
opacity: dshUpgrading || restarting ? 0.6 : 1
|
|
4116
|
+
},
|
|
4117
|
+
onClick: handleUpgradeDsh,
|
|
4118
|
+
disabled: dshUpgrading || restarting || dshUpgradeResult?.ok
|
|
4119
|
+
},
|
|
4120
|
+
dshUpgrading ? React.createElement(
|
|
4121
|
+
"span",
|
|
4122
|
+
{ style: { display: "inline-flex", alignItems: "center", gap: 6 } },
|
|
4123
|
+
React.createElement("span", { style: { animation: "spin 1s linear infinite", display: "inline-flex" } }, React.createElement(Icons.refresh)),
|
|
4124
|
+
"\u6B63\u5728\u5347\u7EA7 DSH\u2026"
|
|
4125
|
+
) : dshUpgradeResult?.ok ? "\u2713 DSH \u5347\u7EA7\u5B8C\u6210" : `\u4E00\u952E\u5347\u7EA7 DSH \u5230 v${info.dshLatest}`
|
|
4126
|
+
) : React.createElement("a", {
|
|
4127
|
+
href: GITHUB_URL,
|
|
4128
|
+
target: "_blank",
|
|
4129
|
+
rel: "noreferrer",
|
|
4130
|
+
style: { ...s.btnLink, fontSize: 12, fontWeight: 600 }
|
|
4131
|
+
}, "\u67E5\u770B\u5B98\u65B9\u5347\u7EA7\u65B9\u5F0F \u2197")
|
|
4132
|
+
),
|
|
4133
|
+
React.createElement(
|
|
4134
|
+
"div",
|
|
4135
|
+
{
|
|
4136
|
+
style: { fontSize: 12, color: "var(--dsw-alias-label-secondary,#6b7280)", lineHeight: 1.6 }
|
|
4137
|
+
},
|
|
4138
|
+
info?.dshUpgradable ? "\u5347\u7EA7 DSH \u547D\u4EE4\u884C\u5DE5\u5177\u540E\u9700\u91CD\u542F DSH \u670D\u52A1\u751F\u6548\u3002" : info?.dshUpgradeReason || "\u5F53\u524D DSH \u975E npm \u5168\u5C40\u5B89\u88C5\uFF0C\u65E0\u6CD5\u4E00\u952E\u81EA\u52A8\u5347\u7EA7\uFF0C\u8BF7\u6309\u5B98\u65B9\u6E20\u9053\u624B\u52A8\u66F4\u65B0\u3002"
|
|
4139
|
+
),
|
|
4140
|
+
dshUpgradeResult && React.createElement("div", {
|
|
4141
|
+
style: {
|
|
4142
|
+
marginTop: 10,
|
|
4143
|
+
fontSize: 12,
|
|
4144
|
+
lineHeight: 1.6,
|
|
4145
|
+
color: dshUpgradeResult.ok ? "var(--dsw-alias-state-success-primary,#059669)" : "var(--dsw-alias-state-error-primary,#dc2626)"
|
|
4146
|
+
}
|
|
4147
|
+
}, dshUpgradeResult.message),
|
|
4148
|
+
dshUpgradeResult?.ok && !dismissRestart && React.createElement(
|
|
4149
|
+
"div",
|
|
4150
|
+
{
|
|
4151
|
+
style: { display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap", marginTop: 10 }
|
|
4152
|
+
},
|
|
4153
|
+
React.createElement("button", {
|
|
4154
|
+
style: { ...s.btnPri, height: 30, fontSize: 12, padding: "0 14px", background: "var(--dsw-alias-state-success-primary,#059669)" },
|
|
4155
|
+
onClick: handleRestart
|
|
4156
|
+
}, "\u{1F504} \u7ACB\u5373\u91CD\u542F DSH \u670D\u52A1"),
|
|
4157
|
+
React.createElement("button", {
|
|
4158
|
+
style: { ...s.btnGhost, height: 30, fontSize: 12, padding: "0 12px" },
|
|
4159
|
+
onClick: () => setDismissRestart(true)
|
|
4160
|
+
}, "\u7A0D\u540E\u624B\u52A8\u91CD\u542F")
|
|
4161
|
+
),
|
|
4162
|
+
(restarting || restartStatus) && React.createElement(
|
|
4163
|
+
"div",
|
|
4164
|
+
{
|
|
4165
|
+
style: {
|
|
4166
|
+
display: "flex",
|
|
4167
|
+
alignItems: "center",
|
|
4168
|
+
gap: 8,
|
|
4169
|
+
fontSize: 12,
|
|
4170
|
+
marginTop: 8,
|
|
4171
|
+
color: restartStatus?.phase === "success" ? "var(--dsw-alias-state-success-primary,#059669)" : restartStatus?.phase === "timeout" ? "var(--dsw-alias-state-error-primary,#dc2626)" : "var(--dsw-alias-state-info-primary,#2563eb)",
|
|
4172
|
+
fontWeight: 500
|
|
4173
|
+
}
|
|
4174
|
+
},
|
|
4175
|
+
restartStatus?.phase !== "success" && restartStatus?.phase !== "timeout" && React.createElement("span", {
|
|
4176
|
+
style: { animation: "spin 1s linear infinite", display: "inline-flex" }
|
|
4177
|
+
}, React.createElement(Icons.refresh)),
|
|
4178
|
+
restartStatus?.text || "\u6B63\u5728\u5904\u7406\u2026"
|
|
4179
|
+
)
|
|
4180
|
+
)
|
|
4181
|
+
)
|
|
3985
4182
|
)
|
|
3986
4183
|
);
|
|
3987
4184
|
}
|
|
@@ -5756,10 +5953,196 @@ function RemoteDirectoryFlow(props) {
|
|
|
5756
5953
|
}, [open, pick]);
|
|
5757
5954
|
return null;
|
|
5758
5955
|
}
|
|
5956
|
+
function setupIosKeyboardAdapter() {
|
|
5957
|
+
if (typeof window === "undefined" || !window.visualViewport) return;
|
|
5958
|
+
if (!/iPhone|iPad|iPod/.test(navigator.userAgent || "")) return;
|
|
5959
|
+
const vv = window.visualViewport;
|
|
5960
|
+
const isEditable = (el) => el && (el.isContentEditable || el.tagName === "TEXTAREA" || el.tagName === "INPUT");
|
|
5961
|
+
let keyboardOpen = false;
|
|
5962
|
+
let pinnedEl = null;
|
|
5963
|
+
let pinnedH = null;
|
|
5964
|
+
let pinnedInline = null;
|
|
5965
|
+
const findScrollContainer = (el) => {
|
|
5966
|
+
let n = el;
|
|
5967
|
+
while (n && n !== document.body) {
|
|
5968
|
+
const cs = getComputedStyle(n);
|
|
5969
|
+
if ((cs.overflowY === "auto" || cs.overflowY === "scroll") && n.scrollHeight > n.clientHeight) {
|
|
5970
|
+
return n;
|
|
5971
|
+
}
|
|
5972
|
+
n = n.parentElement;
|
|
5973
|
+
}
|
|
5974
|
+
return null;
|
|
5975
|
+
};
|
|
5976
|
+
vv.addEventListener("resize", () => {
|
|
5977
|
+
const ratio = vv.height / window.innerHeight;
|
|
5978
|
+
const nowOpen = ratio < 0.75;
|
|
5979
|
+
if (nowOpen === keyboardOpen) return;
|
|
5980
|
+
keyboardOpen = nowOpen;
|
|
5981
|
+
const el = document.activeElement;
|
|
5982
|
+
if (nowOpen) {
|
|
5983
|
+
const scroller = isEditable(el) ? findScrollContainer(el) : null;
|
|
5984
|
+
if (scroller) {
|
|
5985
|
+
pinnedEl = scroller;
|
|
5986
|
+
pinnedInline = scroller.style.height || "";
|
|
5987
|
+
pinnedH = scroller.getBoundingClientRect().height;
|
|
5988
|
+
scroller.style.height = `${Math.round(pinnedH)}px`;
|
|
5989
|
+
}
|
|
5990
|
+
requestAnimationFrame(() => {
|
|
5991
|
+
if (isEditable(el)) {
|
|
5992
|
+
try {
|
|
5993
|
+
el.scrollIntoView({ block: "nearest" });
|
|
5994
|
+
} catch {
|
|
5995
|
+
}
|
|
5996
|
+
}
|
|
5997
|
+
});
|
|
5998
|
+
} else {
|
|
5999
|
+
if (pinnedEl) {
|
|
6000
|
+
pinnedEl.style.height = pinnedInline;
|
|
6001
|
+
pinnedEl = null;
|
|
6002
|
+
pinnedH = null;
|
|
6003
|
+
pinnedInline = null;
|
|
6004
|
+
}
|
|
6005
|
+
}
|
|
6006
|
+
});
|
|
6007
|
+
}
|
|
6008
|
+
function setupComposerCollapse() {
|
|
6009
|
+
if (typeof window === "undefined" || typeof document === "undefined") return;
|
|
6010
|
+
if (window.innerWidth > 768) return;
|
|
6011
|
+
const LS_KEY = "dsh-composer-fold";
|
|
6012
|
+
let bar = null;
|
|
6013
|
+
let busy = false;
|
|
6014
|
+
let lastHadSeat = null;
|
|
6015
|
+
const getFoldBtn = () => {
|
|
6016
|
+
const existing = document.querySelector(".dsh-header-fold-btn");
|
|
6017
|
+
if (existing) return existing;
|
|
6018
|
+
const utils = document.querySelector('div[class*="wSkVaW_headerUtilities"], div[class*="headerUtilities"]');
|
|
6019
|
+
const logBtn = document.querySelector('button[class*="sessionLogButton"], button[class*="nL4_yW_sessionLogButton"]');
|
|
6020
|
+
if (!utils) return null;
|
|
6021
|
+
const btn = document.createElement("button");
|
|
6022
|
+
btn.className = "dsh-header-fold-btn";
|
|
6023
|
+
btn.setAttribute("aria-label", "\u6536\u8D77/\u5C55\u5F00\u8F93\u5165\u6846");
|
|
6024
|
+
btn.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="3" y1="18" x2="21" y2="18"></line><polyline points="6 9 12 15 18 9"></polyline></svg>';
|
|
6025
|
+
if (logBtn && logBtn.parentElement === utils) utils.insertBefore(btn, logBtn);
|
|
6026
|
+
else utils.appendChild(btn);
|
|
6027
|
+
return btn;
|
|
6028
|
+
};
|
|
6029
|
+
const isCollapsed = () => document.body.classList.contains("dsh-composer-collapsed");
|
|
6030
|
+
const readPref = () => {
|
|
6031
|
+
try {
|
|
6032
|
+
return localStorage.getItem(LS_KEY) === "1";
|
|
6033
|
+
} catch {
|
|
6034
|
+
return false;
|
|
6035
|
+
}
|
|
6036
|
+
};
|
|
6037
|
+
const ensureBar = () => {
|
|
6038
|
+
if (bar && document.body.contains(bar)) return bar;
|
|
6039
|
+
const seat = document.querySelector('div[class*="composerSeat"]');
|
|
6040
|
+
const scrollBody = seat ? seat.parentElement : null;
|
|
6041
|
+
if (!scrollBody) return null;
|
|
6042
|
+
bar = document.createElement("div");
|
|
6043
|
+
bar.className = "dsh-composer-collapsed-bar";
|
|
6044
|
+
bar.textContent = "\u270F\uFE0F \u70B9\u51FB\u8F93\u5165\u6D88\u606F\u2026";
|
|
6045
|
+
scrollBody.insertBefore(bar, seat);
|
|
6046
|
+
bar.addEventListener("click", () => setCollapsed(false));
|
|
6047
|
+
return bar;
|
|
6048
|
+
};
|
|
6049
|
+
const removeBar = () => {
|
|
6050
|
+
if (bar) {
|
|
6051
|
+
try {
|
|
6052
|
+
bar.remove();
|
|
6053
|
+
} catch {
|
|
6054
|
+
}
|
|
6055
|
+
bar = null;
|
|
6056
|
+
}
|
|
6057
|
+
};
|
|
6058
|
+
const setCollapsed = (collapsed) => {
|
|
6059
|
+
if (busy) return;
|
|
6060
|
+
busy = true;
|
|
6061
|
+
try {
|
|
6062
|
+
if (isCollapsed() !== collapsed) {
|
|
6063
|
+
document.body.classList.toggle("dsh-composer-collapsed", collapsed);
|
|
6064
|
+
try {
|
|
6065
|
+
localStorage.setItem(LS_KEY, collapsed ? "1" : "0");
|
|
6066
|
+
} catch {
|
|
6067
|
+
}
|
|
6068
|
+
updateButton(collapsed);
|
|
6069
|
+
}
|
|
6070
|
+
if (collapsed) ensureBar();
|
|
6071
|
+
else removeBar();
|
|
6072
|
+
} finally {
|
|
6073
|
+
busy = false;
|
|
6074
|
+
}
|
|
6075
|
+
};
|
|
6076
|
+
const updateButton = (collapsed) => {
|
|
6077
|
+
const btn = getFoldBtn();
|
|
6078
|
+
if (!btn) return;
|
|
6079
|
+
btn.title = collapsed ? "\u5C55\u5F00\u8F93\u5165\u6846" : "\u6536\u8D77\u8F93\u5165\u6846\uFF0C\u6700\u5927\u5316\u5BF9\u8BDD\u9605\u8BFB\u533A";
|
|
6080
|
+
const icon = collapsed ? '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="6" width="20" height="12" rx="2"></rect><line x1="6" y1="10" x2="6.01" y2="10"></line><line x1="10" y1="10" x2="10.01" y2="10"></line><line x1="14" y1="10" x2="14.01" y2="10"></line><line x1="6" y1="14" x2="10" y2="14"></line><line x1="14" y1="14" x2="18" y2="14"></line></svg>' : '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="3" y1="18" x2="21" y2="18"></line><polyline points="6 9 12 15 18 9"></polyline></svg>';
|
|
6081
|
+
const existing = btn.querySelector("svg");
|
|
6082
|
+
if (!existing || existing.outerHTML !== icon) btn.innerHTML = icon;
|
|
6083
|
+
};
|
|
6084
|
+
const handleSeatPresent = () => {
|
|
6085
|
+
const btn = getFoldBtn();
|
|
6086
|
+
if (!btn) return false;
|
|
6087
|
+
btn.style.display = "inline-flex";
|
|
6088
|
+
if (!btn._dshFoldWired) {
|
|
6089
|
+
btn._dshFoldWired = true;
|
|
6090
|
+
btn.onclick = (e) => {
|
|
6091
|
+
e.stopPropagation();
|
|
6092
|
+
setCollapsed(!isCollapsed());
|
|
6093
|
+
};
|
|
6094
|
+
}
|
|
6095
|
+
document.body.classList.add("dsh-composer-active");
|
|
6096
|
+
const want = readPref();
|
|
6097
|
+
if (want !== isCollapsed()) {
|
|
6098
|
+
setCollapsed(want);
|
|
6099
|
+
} else {
|
|
6100
|
+
updateButton(isCollapsed());
|
|
6101
|
+
}
|
|
6102
|
+
return true;
|
|
6103
|
+
};
|
|
6104
|
+
const handleSeatAbsent = () => {
|
|
6105
|
+
const btn = getFoldBtn();
|
|
6106
|
+
if (btn) {
|
|
6107
|
+
btn.style.display = "none";
|
|
6108
|
+
}
|
|
6109
|
+
document.body.classList.remove("dsh-composer-active");
|
|
6110
|
+
removeBar();
|
|
6111
|
+
};
|
|
6112
|
+
const onComposerChange = () => {
|
|
6113
|
+
const hasSeat = !!document.querySelector('div[class*="composerSeat"]');
|
|
6114
|
+
const hasBtn = !!document.querySelector(".dsh-header-fold-btn");
|
|
6115
|
+
if (busy) return;
|
|
6116
|
+
if (hasSeat === lastHadSeat && hasSeat === hasBtn) return;
|
|
6117
|
+
busy = true;
|
|
6118
|
+
try {
|
|
6119
|
+
if (hasSeat) {
|
|
6120
|
+
lastHadSeat = handleSeatPresent();
|
|
6121
|
+
} else {
|
|
6122
|
+
handleSeatAbsent();
|
|
6123
|
+
lastHadSeat = false;
|
|
6124
|
+
}
|
|
6125
|
+
} finally {
|
|
6126
|
+
busy = false;
|
|
6127
|
+
}
|
|
6128
|
+
};
|
|
6129
|
+
const observer = new MutationObserver(() => {
|
|
6130
|
+
observer.disconnect();
|
|
6131
|
+
try {
|
|
6132
|
+
onComposerChange();
|
|
6133
|
+
} finally {
|
|
6134
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
6135
|
+
}
|
|
6136
|
+
});
|
|
6137
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
6138
|
+
onComposerChange();
|
|
6139
|
+
}
|
|
5759
6140
|
function apply(ctx) {
|
|
5760
6141
|
window.__dshClientCtx = ctx;
|
|
5761
6142
|
const rpcCall = (endpoint, payload, signal) => ctx.connection.rpc.call(BRIDGE_RPC_CHANNEL, endpoint, payload, signal);
|
|
5762
6143
|
window.__dshOpenRemoteWorkspaceModal = (onAdded, onPickDirect, onCancel) => showRemoteWorkspaceDialog(rpcCall, onAdded, ctx, onPickDirect, onCancel);
|
|
6144
|
+
setupIosKeyboardAdapter();
|
|
6145
|
+
setupComposerCollapse();
|
|
5763
6146
|
setupMobileExperience(rpcCall, ctx);
|
|
5764
6147
|
const injected = () => ({ pick: () => ctx.workspaces?.pickDirectory?.() });
|
|
5765
6148
|
ctx.slots.inject(
|
package/client/index.js
CHANGED
|
@@ -2397,6 +2397,8 @@ function VersionBanner({ rpcCall }) {
|
|
|
2397
2397
|
|
|
2398
2398
|
const hasUpdate = info?.latest && info?.current && !info.error && semverGt(info.latest, info.current);
|
|
2399
2399
|
const isLatest = info?.latest && info?.current && !info.error && !semverGt(info.latest, info.current);
|
|
2400
|
+
// DSH 宿主更新状态:有线上新版 + 当前版本可比较
|
|
2401
|
+
const dshHasUpdate = !!(info?.dshLatest && info?.dshVersion && !info.error && semverGt(info.dshLatest, info.dshVersion));
|
|
2400
2402
|
|
|
2401
2403
|
const handleUpgrade = React.useCallback(async () => {
|
|
2402
2404
|
if (!info?.latest || upgrading) return;
|
|
@@ -2420,6 +2422,30 @@ function VersionBanner({ rpcCall }) {
|
|
|
2420
2422
|
}
|
|
2421
2423
|
}, [info?.latest, upgrading, rpcCall]);
|
|
2422
2424
|
|
|
2425
|
+
// DSH 宿主一键升级:仅当服务端判定可自动升级(npm 全局安装)时按钮才会出现
|
|
2426
|
+
const [dshUpgrading, setDshUpgrading] = React.useState(false);
|
|
2427
|
+
const [dshUpgradeResult, setDshUpgradeResult] = React.useState(null);
|
|
2428
|
+
const handleUpgradeDsh = React.useCallback(async () => {
|
|
2429
|
+
if (!info?.dshLatest || dshUpgrading) return;
|
|
2430
|
+
setDshUpgrading(true);
|
|
2431
|
+
setDshUpgradeResult(null);
|
|
2432
|
+
setDismissRestart(false);
|
|
2433
|
+
resetRestartStatus();
|
|
2434
|
+
try {
|
|
2435
|
+
const r = await rpcCall(BRIDGE_ENDPOINTS.upgradeDsh, { version: info.dshLatest });
|
|
2436
|
+
if (r?.ok && r.value?.ok) {
|
|
2437
|
+
setDshUpgradeResult({ ok: true, message: `DSH 已成功升级到 v${info.dshLatest}!` });
|
|
2438
|
+
} else {
|
|
2439
|
+
const msg = r?.value?.error || r?.error?.message || '升级失败';
|
|
2440
|
+
setDshUpgradeResult({ ok: false, message: msg, manual: true });
|
|
2441
|
+
}
|
|
2442
|
+
} catch (e) {
|
|
2443
|
+
setDshUpgradeResult({ ok: false, message: e.message || '升级请求失败', manual: true });
|
|
2444
|
+
} finally {
|
|
2445
|
+
setDshUpgrading(false);
|
|
2446
|
+
}
|
|
2447
|
+
}, [info?.dshLatest, dshUpgrading, rpcCall]);
|
|
2448
|
+
|
|
2423
2449
|
const links = React.createElement('div', { style: { display: 'flex', gap: 10, alignItems: 'center', flexWrap: 'wrap' } },
|
|
2424
2450
|
React.createElement('a', {
|
|
2425
2451
|
href: GITHUB_URL, target: '_blank', rel: 'noreferrer', style: s.btnLink,
|
|
@@ -2478,12 +2504,16 @@ function VersionBanner({ rpcCall }) {
|
|
|
2478
2504
|
hasUpdate && React.createElement('span', { style: { fontWeight: 600, fontSize: 11 } }, `➔ v${info.latest}`),
|
|
2479
2505
|
info?.error && React.createElement('span', { style: { color: 'var(--dsw-alias-state-warn-primary,#d97706)', fontSize: 11 } }, '(网络超时)'),
|
|
2480
2506
|
),
|
|
2481
|
-
// DSH
|
|
2507
|
+
// DSH 宿主版本标签(有新版时黄色高亮)
|
|
2482
2508
|
info?.dshVersion && React.createElement('span', {
|
|
2483
2509
|
style: {
|
|
2484
2510
|
...s.tag,
|
|
2485
|
-
background:
|
|
2486
|
-
|
|
2511
|
+
background: dshHasUpdate
|
|
2512
|
+
? 'var(--dsw-alias-state-warn-bg,#fffbeb)'
|
|
2513
|
+
: 'var(--dsw-alias-bg-layer-2,#f3f4f6)',
|
|
2514
|
+
color: dshHasUpdate
|
|
2515
|
+
? 'var(--dsw-alias-state-warn-primary,#d97706)'
|
|
2516
|
+
: 'var(--dsw-alias-label-tertiary,#6b7280)',
|
|
2487
2517
|
padding: '3px 10px',
|
|
2488
2518
|
fontSize: 12,
|
|
2489
2519
|
fontWeight: 500,
|
|
@@ -2491,9 +2521,11 @@ function VersionBanner({ rpcCall }) {
|
|
|
2491
2521
|
alignItems: 'center',
|
|
2492
2522
|
gap: 5,
|
|
2493
2523
|
},
|
|
2524
|
+
title: dshHasUpdate ? `发现 DSH 新版本 v${info.dshLatest}` : undefined,
|
|
2494
2525
|
},
|
|
2495
2526
|
React.createElement('span', { style: { opacity: 0.75, fontSize: 11, fontWeight: 400 } }, 'DSH'),
|
|
2496
2527
|
`v${info.dshVersion}`,
|
|
2528
|
+
dshHasUpdate && React.createElement('span', { style: { fontWeight: 600, fontSize: 11 } }, `➔ v${info.dshLatest}`),
|
|
2497
2529
|
),
|
|
2498
2530
|
// 刷新检查按钮
|
|
2499
2531
|
React.createElement('button', {
|
|
@@ -2672,6 +2704,102 @@ function VersionBanner({ rpcCall }) {
|
|
|
2672
2704
|
),
|
|
2673
2705
|
),
|
|
2674
2706
|
),
|
|
2707
|
+
|
|
2708
|
+
// ── DSH 宿主有新版本时的提示 / 一键升级卡片 ──
|
|
2709
|
+
dshHasUpdate && React.createElement('div', {
|
|
2710
|
+
style: {
|
|
2711
|
+
...s.card,
|
|
2712
|
+
background: 'var(--dsw-alias-state-warn-bg,#fffbeb)',
|
|
2713
|
+
border: '1px solid var(--dsw-alias-state-warn-border,#fde68a)',
|
|
2714
|
+
padding: '14px 16px',
|
|
2715
|
+
marginTop: 10,
|
|
2716
|
+
marginBottom: 0,
|
|
2717
|
+
},
|
|
2718
|
+
},
|
|
2719
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'flex-start', gap: 12 } },
|
|
2720
|
+
React.createElement('span', { style: { fontSize: 22 } }, '🛠️'),
|
|
2721
|
+
React.createElement('div', { style: { flex: 1, minWidth: 0 } },
|
|
2722
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: 8, marginBottom: 8 } },
|
|
2723
|
+
React.createElement('div', {
|
|
2724
|
+
style: { fontSize: 13, fontWeight: 600, color: 'var(--dsw-alias-state-warn-primary,#92400e)' },
|
|
2725
|
+
}, `DSH 有新版本 v${info.dshLatest}(当前 v${info.dshVersion})`),
|
|
2726
|
+
info?.dshUpgradable
|
|
2727
|
+
? React.createElement('button', {
|
|
2728
|
+
style: {
|
|
2729
|
+
...s.btnPri,
|
|
2730
|
+
height: 28,
|
|
2731
|
+
fontSize: 12,
|
|
2732
|
+
padding: '0 14px',
|
|
2733
|
+
background: dshUpgradeResult?.ok
|
|
2734
|
+
? 'var(--dsw-alias-state-success-primary,#059669)'
|
|
2735
|
+
: 'var(--dsw-alias-brand-primary,#4f6ef7)',
|
|
2736
|
+
opacity: (dshUpgrading || restarting) ? 0.6 : 1,
|
|
2737
|
+
},
|
|
2738
|
+
onClick: handleUpgradeDsh,
|
|
2739
|
+
disabled: dshUpgrading || restarting || dshUpgradeResult?.ok,
|
|
2740
|
+
},
|
|
2741
|
+
dshUpgrading
|
|
2742
|
+
? React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 6 } },
|
|
2743
|
+
React.createElement('span', { style: { animation: 'spin 1s linear infinite', display: 'inline-flex' } }, React.createElement(Icons.refresh)),
|
|
2744
|
+
'正在升级 DSH…',
|
|
2745
|
+
)
|
|
2746
|
+
: dshUpgradeResult?.ok
|
|
2747
|
+
? '✓ DSH 升级完成'
|
|
2748
|
+
: `一键升级 DSH 到 v${info.dshLatest}`,
|
|
2749
|
+
)
|
|
2750
|
+
: React.createElement('a', {
|
|
2751
|
+
href: GITHUB_URL, target: '_blank', rel: 'noreferrer',
|
|
2752
|
+
style: { ...s.btnLink, fontSize: 12, fontWeight: 600 },
|
|
2753
|
+
}, '查看官方升级方式 ↗'),
|
|
2754
|
+
),
|
|
2755
|
+
React.createElement('div', {
|
|
2756
|
+
style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary,#6b7280)', lineHeight: 1.6 },
|
|
2757
|
+
},
|
|
2758
|
+
info?.dshUpgradable
|
|
2759
|
+
? '升级 DSH 命令行工具后需重启 DSH 服务生效。'
|
|
2760
|
+
: (info?.dshUpgradeReason || '当前 DSH 非 npm 全局安装,无法一键自动升级,请按官方渠道手动更新。'),
|
|
2761
|
+
),
|
|
2762
|
+
dshUpgradeResult && React.createElement('div', {
|
|
2763
|
+
style: {
|
|
2764
|
+
marginTop: 10,
|
|
2765
|
+
fontSize: 12,
|
|
2766
|
+
lineHeight: 1.6,
|
|
2767
|
+
color: dshUpgradeResult.ok
|
|
2768
|
+
? 'var(--dsw-alias-state-success-primary,#059669)'
|
|
2769
|
+
: 'var(--dsw-alias-state-error-primary,#dc2626)',
|
|
2770
|
+
},
|
|
2771
|
+
}, dshUpgradeResult.message),
|
|
2772
|
+
dshUpgradeResult?.ok && !dismissRestart && React.createElement('div', {
|
|
2773
|
+
style: { display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap', marginTop: 10 },
|
|
2774
|
+
},
|
|
2775
|
+
React.createElement('button', {
|
|
2776
|
+
style: { ...s.btnPri, height: 30, fontSize: 12, padding: '0 14px', background: 'var(--dsw-alias-state-success-primary,#059669)' },
|
|
2777
|
+
onClick: handleRestart,
|
|
2778
|
+
}, '🔄 立即重启 DSH 服务'),
|
|
2779
|
+
React.createElement('button', {
|
|
2780
|
+
style: { ...s.btnGhost, height: 30, fontSize: 12, padding: '0 12px' },
|
|
2781
|
+
onClick: () => setDismissRestart(true),
|
|
2782
|
+
}, '稍后手动重启'),
|
|
2783
|
+
),
|
|
2784
|
+
(restarting || restartStatus) && React.createElement('div', {
|
|
2785
|
+
style: {
|
|
2786
|
+
display: 'flex', alignItems: 'center', gap: 8, fontSize: 12, marginTop: 8,
|
|
2787
|
+
color: restartStatus?.phase === 'success'
|
|
2788
|
+
? 'var(--dsw-alias-state-success-primary,#059669)'
|
|
2789
|
+
: restartStatus?.phase === 'timeout'
|
|
2790
|
+
? 'var(--dsw-alias-state-error-primary,#dc2626)'
|
|
2791
|
+
: 'var(--dsw-alias-state-info-primary,#2563eb)',
|
|
2792
|
+
fontWeight: 500,
|
|
2793
|
+
},
|
|
2794
|
+
},
|
|
2795
|
+
restartStatus?.phase !== 'success' && restartStatus?.phase !== 'timeout' && React.createElement('span', {
|
|
2796
|
+
style: { animation: 'spin 1s linear infinite', display: 'inline-flex' },
|
|
2797
|
+
}, React.createElement(Icons.refresh)),
|
|
2798
|
+
restartStatus?.text || '正在处理…',
|
|
2799
|
+
),
|
|
2800
|
+
),
|
|
2801
|
+
),
|
|
2802
|
+
),
|
|
2675
2803
|
);
|
|
2676
2804
|
}
|
|
2677
2805
|
|
|
@@ -4567,6 +4695,199 @@ function RemoteDirectoryFlow(props) {
|
|
|
4567
4695
|
|
|
4568
4696
|
// ---- 插件入口 ----
|
|
4569
4697
|
|
|
4698
|
+
// iOS Safari 键盘弹起时输入框上下跳动适配。
|
|
4699
|
+
//
|
|
4700
|
+
// 根因(实测 2026-09):聊天输入区容器(宿主 wSkVaW_composerSeat)为
|
|
4701
|
+
// position:sticky; bottom:0,其滚动容器高度由 100% 链决定。iOS 键盘弹起时
|
|
4702
|
+
// visual viewport 收缩,sticky 基准随视口变化 + iOS 反复 scroll 调整 → 每敲一字跳动。
|
|
4703
|
+
//
|
|
4704
|
+
// 通用解法(不依赖宿主类名):键盘弹起瞬间找到焦点输入框所属的滚动容器,
|
|
4705
|
+
// 把它当前高度用 px 固定(避免随视口继续收缩/重排),键盘收起后还原;
|
|
4706
|
+
// 并顺带做一次精确 scrollIntoView,阻止 iOS 二次乱滚。
|
|
4707
|
+
function setupIosKeyboardAdapter() {
|
|
4708
|
+
if (typeof window === 'undefined' || !window.visualViewport) return;
|
|
4709
|
+
if (!/iPhone|iPad|iPod/.test(navigator.userAgent || '')) return;
|
|
4710
|
+
|
|
4711
|
+
const vv = window.visualViewport;
|
|
4712
|
+
const isEditable = (el) => el && (el.isContentEditable || el.tagName === 'TEXTAREA' || el.tagName === 'INPUT');
|
|
4713
|
+
let keyboardOpen = false;
|
|
4714
|
+
let pinnedEl = null; // 被固定高度的滚动容器
|
|
4715
|
+
let pinnedH = null; // 原始 height(还原用)
|
|
4716
|
+
let pinnedInline = null;
|
|
4717
|
+
|
|
4718
|
+
const findScrollContainer = (el) => {
|
|
4719
|
+
let n = el;
|
|
4720
|
+
while (n && n !== document.body) {
|
|
4721
|
+
const cs = getComputedStyle(n);
|
|
4722
|
+
if ((cs.overflowY === 'auto' || cs.overflowY === 'scroll') && n.scrollHeight > n.clientHeight) {
|
|
4723
|
+
return n;
|
|
4724
|
+
}
|
|
4725
|
+
n = n.parentElement;
|
|
4726
|
+
}
|
|
4727
|
+
return null;
|
|
4728
|
+
};
|
|
4729
|
+
|
|
4730
|
+
vv.addEventListener('resize', () => {
|
|
4731
|
+
const ratio = vv.height / window.innerHeight;
|
|
4732
|
+
const nowOpen = ratio < 0.75;
|
|
4733
|
+
if (nowOpen === keyboardOpen) return;
|
|
4734
|
+
keyboardOpen = nowOpen;
|
|
4735
|
+
const el = document.activeElement;
|
|
4736
|
+
|
|
4737
|
+
if (nowOpen) {
|
|
4738
|
+
// 键盘弹起:固定滚动容器高度,防随视口继续收缩导致 sticky 输入框跳动
|
|
4739
|
+
const scroller = isEditable(el) ? findScrollContainer(el) : null;
|
|
4740
|
+
if (scroller) {
|
|
4741
|
+
pinnedEl = scroller;
|
|
4742
|
+
pinnedInline = scroller.style.height || '';
|
|
4743
|
+
pinnedH = scroller.getBoundingClientRect().height;
|
|
4744
|
+
scroller.style.height = `${Math.round(pinnedH)}px`;
|
|
4745
|
+
}
|
|
4746
|
+
requestAnimationFrame(() => {
|
|
4747
|
+
if (isEditable(el)) {
|
|
4748
|
+
try { el.scrollIntoView({ block: 'nearest' }); } catch { /* 忽略 */ }
|
|
4749
|
+
}
|
|
4750
|
+
});
|
|
4751
|
+
} else {
|
|
4752
|
+
// 键盘收起:还原滚动容器高度,让布局回到正常
|
|
4753
|
+
if (pinnedEl) {
|
|
4754
|
+
pinnedEl.style.height = pinnedInline;
|
|
4755
|
+
pinnedEl = null; pinnedH = null; pinnedInline = null;
|
|
4756
|
+
}
|
|
4757
|
+
}
|
|
4758
|
+
});
|
|
4759
|
+
}
|
|
4760
|
+
|
|
4761
|
+
// 移动端输入框折叠/展开(body.dsh-composer-collapsed)。
|
|
4762
|
+
// 底部输入区(composer)占用 ~116px,顶部菜单 + header 又占 ~120px,屏幕有限时
|
|
4763
|
+
// 中间消息可视区很小。折叠后 composer 隐藏、消息 viewArea 自动伸展全高,阅读区显著增大。
|
|
4764
|
+
// 折叠态记忆到 localStorage(dsh-composer-fold),跨会话保持用户偏好。
|
|
4765
|
+
function setupComposerCollapse() {
|
|
4766
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') return;
|
|
4767
|
+
if (window.innerWidth > 768) return; // 仅移动端
|
|
4768
|
+
const LS_KEY = 'dsh-composer-fold';
|
|
4769
|
+
|
|
4770
|
+
let bar = null; // 折叠态底部"点击输入"细条
|
|
4771
|
+
let busy = false; // 防重入锁(observer 由我们自己改动触发时跳过)
|
|
4772
|
+
let lastHadSeat = null; // 上次检测是否有输入区(避免重复处理)
|
|
4773
|
+
|
|
4774
|
+
// 折叠按钮:注入到聊天头部工具栏(headerUtilities)的 Session 下载按钮旁,
|
|
4775
|
+
// 与 DSH 原生按钮同规格(28px 圆形),视觉与原生一致。返回 null 表示工具栏未就绪。
|
|
4776
|
+
const getFoldBtn = () => {
|
|
4777
|
+
const existing = document.querySelector('.dsh-header-fold-btn');
|
|
4778
|
+
if (existing) return existing;
|
|
4779
|
+
const utils = document.querySelector('div[class*="wSkVaW_headerUtilities"], div[class*="headerUtilities"]');
|
|
4780
|
+
const logBtn = document.querySelector('button[class*="sessionLogButton"], button[class*="nL4_yW_sessionLogButton"]');
|
|
4781
|
+
if (!utils) return null;
|
|
4782
|
+
const btn = document.createElement('button');
|
|
4783
|
+
btn.className = 'dsh-header-fold-btn';
|
|
4784
|
+
btn.setAttribute('aria-label', '收起/展开输入框');
|
|
4785
|
+
btn.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="3" y1="18" x2="21" y2="18"></line><polyline points="6 9 12 15 18 9"></polyline></svg>';
|
|
4786
|
+
// 插到 session 下载按钮左侧(紧邻原生工具钮)
|
|
4787
|
+
if (logBtn && logBtn.parentElement === utils) utils.insertBefore(btn, logBtn);
|
|
4788
|
+
else utils.appendChild(btn);
|
|
4789
|
+
return btn;
|
|
4790
|
+
};
|
|
4791
|
+
const isCollapsed = () => document.body.classList.contains('dsh-composer-collapsed');
|
|
4792
|
+
const readPref = () => { try { return localStorage.getItem(LS_KEY) === '1'; } catch { return false; } };
|
|
4793
|
+
|
|
4794
|
+
const ensureBar = () => {
|
|
4795
|
+
if (bar && document.body.contains(bar)) return bar;
|
|
4796
|
+
const seat = document.querySelector('div[class*="composerSeat"]');
|
|
4797
|
+
const scrollBody = seat ? seat.parentElement : null;
|
|
4798
|
+
if (!scrollBody) return null;
|
|
4799
|
+
bar = document.createElement('div');
|
|
4800
|
+
bar.className = 'dsh-composer-collapsed-bar';
|
|
4801
|
+
bar.textContent = '\u270f\ufe0f 点击输入消息…';
|
|
4802
|
+
scrollBody.insertBefore(bar, seat);
|
|
4803
|
+
bar.addEventListener('click', () => setCollapsed(false));
|
|
4804
|
+
return bar;
|
|
4805
|
+
};
|
|
4806
|
+
const removeBar = () => { if (bar) { try { bar.remove(); } catch {} bar = null; } };
|
|
4807
|
+
|
|
4808
|
+
// 折叠态切换:改 class + 记忆 + 图标 + 细条显隐
|
|
4809
|
+
const setCollapsed = (collapsed) => {
|
|
4810
|
+
if (busy) return;
|
|
4811
|
+
busy = true;
|
|
4812
|
+
try {
|
|
4813
|
+
if (isCollapsed() !== collapsed) {
|
|
4814
|
+
document.body.classList.toggle('dsh-composer-collapsed', collapsed);
|
|
4815
|
+
try { localStorage.setItem(LS_KEY, collapsed ? '1' : '0'); } catch {}
|
|
4816
|
+
updateButton(collapsed);
|
|
4817
|
+
}
|
|
4818
|
+
if (collapsed) ensureBar(); else removeBar();
|
|
4819
|
+
} finally { busy = false; }
|
|
4820
|
+
};
|
|
4821
|
+
|
|
4822
|
+
// 更新顶部折叠钮图标(仅内容变化时写入,避免无谓 DOM 抖动)
|
|
4823
|
+
const updateButton = (collapsed) => {
|
|
4824
|
+
const btn = getFoldBtn();
|
|
4825
|
+
if (!btn) return;
|
|
4826
|
+
btn.title = collapsed ? '展开输入框' : '收起输入框,最大化对话阅读区';
|
|
4827
|
+
const icon = collapsed
|
|
4828
|
+
? '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="6" width="20" height="12" rx="2"></rect><line x1="6" y1="10" x2="6.01" y2="10"></line><line x1="10" y1="10" x2="10.01" y2="10"></line><line x1="14" y1="10" x2="14.01" y2="10"></line><line x1="6" y1="14" x2="10" y2="14"></line><line x1="14" y1="14" x2="18" y2="14"></line></svg>'
|
|
4829
|
+
: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="3" y1="18" x2="21" y2="18"></line><polyline points="6 9 12 15 18 9"></polyline></svg>';
|
|
4830
|
+
const existing = btn.querySelector('svg');
|
|
4831
|
+
if (!existing || existing.outerHTML !== icon) btn.innerHTML = icon;
|
|
4832
|
+
};
|
|
4833
|
+
|
|
4834
|
+
// 有输入区(对话页):确保按钮可用 + 按记忆恢复折叠态
|
|
4835
|
+
const handleSeatPresent = () => {
|
|
4836
|
+
const btn = getFoldBtn();
|
|
4837
|
+
if (!btn) return false;
|
|
4838
|
+
btn.style.display = 'inline-flex';
|
|
4839
|
+
if (!btn._dshFoldWired) {
|
|
4840
|
+
btn._dshFoldWired = true;
|
|
4841
|
+
btn.onclick = (e) => { e.stopPropagation(); setCollapsed(!isCollapsed()); };
|
|
4842
|
+
}
|
|
4843
|
+
document.body.classList.add('dsh-composer-active');
|
|
4844
|
+
const want = readPref();
|
|
4845
|
+
if (want !== isCollapsed()) {
|
|
4846
|
+
setCollapsed(want);
|
|
4847
|
+
} else {
|
|
4848
|
+
updateButton(isCollapsed());
|
|
4849
|
+
}
|
|
4850
|
+
return true;
|
|
4851
|
+
};
|
|
4852
|
+
|
|
4853
|
+
// 无输入区:隐藏折叠钮与细条(保留记忆)
|
|
4854
|
+
const handleSeatAbsent = () => {
|
|
4855
|
+
const btn = getFoldBtn();
|
|
4856
|
+
if (btn) { btn.style.display = 'none'; }
|
|
4857
|
+
document.body.classList.remove('dsh-composer-active');
|
|
4858
|
+
removeBar();
|
|
4859
|
+
};
|
|
4860
|
+
|
|
4861
|
+
// 单次检测(observer 回调 / 初始)。
|
|
4862
|
+
// 仅当"是否有输入区"或"折叠钮是否已注入"发生变化时才全量处理,
|
|
4863
|
+
// 避免消息流式输出等高频 DOM 变化下 observer 反复执行注入/状态恢复。
|
|
4864
|
+
// busy 锁 + 注入幂等共同防止自触发循环。
|
|
4865
|
+
const onComposerChange = () => {
|
|
4866
|
+
const hasSeat = !!document.querySelector('div[class*="composerSeat"]');
|
|
4867
|
+
const hasBtn = !!document.querySelector('.dsh-header-fold-btn');
|
|
4868
|
+
if (busy) return;
|
|
4869
|
+
if (hasSeat === lastHadSeat && hasSeat === hasBtn) return; // 状态与注入都没变:忽略
|
|
4870
|
+
busy = true;
|
|
4871
|
+
try {
|
|
4872
|
+
if (hasSeat) {
|
|
4873
|
+
lastHadSeat = handleSeatPresent(); // 内部幂等;工具栏未就绪返回 false,等下次再试
|
|
4874
|
+
} else {
|
|
4875
|
+
handleSeatAbsent();
|
|
4876
|
+
lastHadSeat = false;
|
|
4877
|
+
}
|
|
4878
|
+
} finally { busy = false; }
|
|
4879
|
+
};
|
|
4880
|
+
|
|
4881
|
+
// observer:断开→处理→重连,杜绝"处理里改 DOM 再触发自己"的死循环
|
|
4882
|
+
const observer = new MutationObserver(() => {
|
|
4883
|
+
observer.disconnect();
|
|
4884
|
+
try { onComposerChange(); } finally { observer.observe(document.body, { childList: true, subtree: true }); }
|
|
4885
|
+
});
|
|
4886
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
4887
|
+
onComposerChange(); // 初始检查
|
|
4888
|
+
}
|
|
4889
|
+
|
|
4890
|
+
|
|
4570
4891
|
function apply(ctx) {
|
|
4571
4892
|
window.__dshClientCtx = ctx;
|
|
4572
4893
|
const rpcCall = (endpoint, payload, signal) =>
|
|
@@ -4575,6 +4896,9 @@ function apply(ctx) {
|
|
|
4575
4896
|
window.__dshOpenRemoteWorkspaceModal = (onAdded, onPickDirect, onCancel) =>
|
|
4576
4897
|
showRemoteWorkspaceDialog(rpcCall, onAdded, ctx, onPickDirect, onCancel);
|
|
4577
4898
|
|
|
4899
|
+
setupIosKeyboardAdapter();
|
|
4900
|
+
setupComposerCollapse();
|
|
4901
|
+
|
|
4578
4902
|
setupMobileExperience(rpcCall, ctx);
|
|
4579
4903
|
|
|
4580
4904
|
const injected = () => ({ pick: () => ctx.workspaces?.pickDirectory?.() });
|
package/client/mobile-styles.js
CHANGED
|
@@ -394,6 +394,74 @@ export const MOBILE_STYLES_CSS = `
|
|
|
394
394
|
padding-bottom: max(16px, env(safe-area-inset-bottom)) !important;
|
|
395
395
|
}
|
|
396
396
|
|
|
397
|
+
/* ── 移动端输入框折叠模式(body.dsh-composer-collapsed)──────────────
|
|
398
|
+
折叠后隐藏吸底输入区,让消息 viewArea 自动伸展到全高,最大化阅读区。
|
|
399
|
+
入口:聊天头部工具栏(Session 下载钮旁)的折叠按钮,或折叠态底部细输入条点它唤回。
|
|
400
|
+
状态持久化到 localStorage。 */
|
|
401
|
+
body.dsh-composer-collapsed div[class*="wSkVaW_composerSeat"] {
|
|
402
|
+
display: none !important;
|
|
403
|
+
}
|
|
404
|
+
body.dsh-composer-collapsed div[class*="wSkVaW_viewArea"] {
|
|
405
|
+
flex: 1 1 auto !important;
|
|
406
|
+
height: auto !important;
|
|
407
|
+
min-height: 0 !important;
|
|
408
|
+
}
|
|
409
|
+
/* 折叠时滚动区底部对齐 safe-area,避免内容被 iPhone 底部横条遮挡 */
|
|
410
|
+
body.dsh-composer-collapsed div[class*="wSkVaW_scrollBody"] {
|
|
411
|
+
padding-bottom: max(16px, env(safe-area-inset-bottom)) !important;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/* 折叠输入框按钮:注入在聊天头部工具栏(Session 下载钮旁),
|
|
415
|
+
与 DSH sessionLogButton 同规格(28px 圆形图标钮),视觉与原生一致 */
|
|
416
|
+
.dsh-header-fold-btn {
|
|
417
|
+
min-width: 28px !important;
|
|
418
|
+
width: 28px !important;
|
|
419
|
+
height: 28px !important;
|
|
420
|
+
padding: 0 !important;
|
|
421
|
+
border-radius: 50% !important;
|
|
422
|
+
display: inline-flex !important;
|
|
423
|
+
align-items: center !important;
|
|
424
|
+
justify-content: center !important;
|
|
425
|
+
flex-shrink: 0 !important;
|
|
426
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.1)) !important;
|
|
427
|
+
background: var(--dsw-alias-bg-layer-2, rgba(0, 0, 0, 0.03)) !important;
|
|
428
|
+
color: var(--dsw-alias-label-secondary, #6b7280) !important;
|
|
429
|
+
cursor: pointer !important;
|
|
430
|
+
transition: opacity 0.15s !important;
|
|
431
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03) !important;
|
|
432
|
+
-webkit-tap-highlight-color: transparent !important;
|
|
433
|
+
}
|
|
434
|
+
.dsh-header-fold-btn:active {
|
|
435
|
+
opacity: 0.6 !important;
|
|
436
|
+
}
|
|
437
|
+
.dsh-header-fold-btn svg {
|
|
438
|
+
width: 15px !important;
|
|
439
|
+
height: 15px !important;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/* 折叠态底部细输入条:点击唤起输入框(位于原输入区位置,sticky 底部) */
|
|
443
|
+
.dsh-composer-collapsed-bar {
|
|
444
|
+
display: none !important;
|
|
445
|
+
position: sticky !important;
|
|
446
|
+
bottom: 0 !important;
|
|
447
|
+
margin: 8px 12px max(8px, env(safe-area-inset-bottom, 0px)) 12px !important;
|
|
448
|
+
padding: 11px 16px !important;
|
|
449
|
+
border-radius: 22px !important;
|
|
450
|
+
background: var(--dsw-alias-bg-layer-2, #f4f4f7) !important;
|
|
451
|
+
border: 1px solid rgba(0, 0, 0, 0.07) !important;
|
|
452
|
+
color: var(--dsw-alias-label-tertiary, #8b93a1) !important;
|
|
453
|
+
font-size: 13.5px !important;
|
|
454
|
+
line-height: 1.4 !important;
|
|
455
|
+
cursor: pointer !important;
|
|
456
|
+
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.04) !important;
|
|
457
|
+
-webkit-tap-highlight-color: transparent !important;
|
|
458
|
+
z-index: 40 !important;
|
|
459
|
+
box-sizing: border-box !important;
|
|
460
|
+
}
|
|
461
|
+
body.dsh-composer-collapsed .dsh-composer-collapsed-bar {
|
|
462
|
+
display: block !important;
|
|
463
|
+
}
|
|
464
|
+
|
|
397
465
|
/* 输入卡片:DeepSeek App 圆角大胶囊造型 */
|
|
398
466
|
div[class*="uV2eYG_card"] {
|
|
399
467
|
border-radius: 26px !important;
|
package/lib/auth/manager.js
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
|
|
4
4
|
import { randomBytes, pbkdf2 as pbkdf2Callback, timingSafeEqual } from 'node:crypto'
|
|
5
5
|
import { promisify } from 'node:util'
|
|
6
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs'
|
|
7
|
+
import { homedir } from 'node:os'
|
|
8
|
+
import { join, dirname } from 'node:path'
|
|
6
9
|
|
|
7
10
|
const pbkdf2Async = promisify(pbkdf2Callback)
|
|
8
11
|
|
|
@@ -18,6 +21,50 @@ const LEGACY_PBKDF2_ITERATIONS = 10000
|
|
|
18
21
|
const PBKDF2_KEYLEN = 32
|
|
19
22
|
const PBKDF2_PREFIX = 'pbkdf2-sha256$'
|
|
20
23
|
|
|
24
|
+
// 登录 Session 持久化:宿主(dsh web 进程)重启后,已登录设备免重输访问密码。
|
|
25
|
+
// 落盘文件与 config.json 同目录(~/.dsh/dsh-bridge/sessions.json,权限 600);
|
|
26
|
+
// sessionToken 与 secretToken 同级机密(48 位随机 hex),同目录存储不扩大暴露面。
|
|
27
|
+
// 改密码 / 切模式 / 重新生成 token 时的 sessions.clear() 会同步清空文件(吊销语义保持);
|
|
28
|
+
// 文件缺失或损坏时安全降级为空 Map(等价于旧行为:重新登录一次)。
|
|
29
|
+
// 路径可通过构造参数 sessionsFile 注入(测试传临时目录,避免读写真实环境)。
|
|
30
|
+
const DEFAULT_SESSIONS_FILE = join(
|
|
31
|
+
process.env.DSH_HOME ?? join(homedir(), '.dsh'),
|
|
32
|
+
'dsh-bridge', 'sessions.json'
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
class PersistentSessionMap extends Map {
|
|
36
|
+
/** @param {string} file 会话落盘文件路径 */
|
|
37
|
+
constructor(file) {
|
|
38
|
+
super()
|
|
39
|
+
this._file = file
|
|
40
|
+
}
|
|
41
|
+
_flush() {
|
|
42
|
+
try {
|
|
43
|
+
mkdirSync(dirname(this._file), { recursive: true })
|
|
44
|
+
writeFileSync(this._file, JSON.stringify([...this.entries()]), { mode: 0o600 })
|
|
45
|
+
} catch { /* 落盘失败降级为纯内存语义,不阻断认证流程 */ }
|
|
46
|
+
}
|
|
47
|
+
static load(file) {
|
|
48
|
+
const m = new PersistentSessionMap(file)
|
|
49
|
+
try {
|
|
50
|
+
const arr = JSON.parse(readFileSync(file, 'utf8'))
|
|
51
|
+
if (Array.isArray(arr)) {
|
|
52
|
+
const now = Date.now()
|
|
53
|
+
for (const [k, v] of arr) {
|
|
54
|
+
// 只恢复结构合法且未过期的会话
|
|
55
|
+
if (typeof k === 'string' && v && Number.isFinite(v.expiresAt) && v.expiresAt > now) {
|
|
56
|
+
Map.prototype.set.call(m, k, { createdAt: Number(v.createdAt) || 0, expiresAt: Number(v.expiresAt) })
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
} catch { /* 文件缺失/损坏:降级为空 */ }
|
|
61
|
+
return m
|
|
62
|
+
}
|
|
63
|
+
set(k, v) { super.set(k, v); this._flush(); return this }
|
|
64
|
+
delete(k) { const r = super.delete(k); if (r) this._flush(); return r }
|
|
65
|
+
clear() { super.clear(); this._flush() }
|
|
66
|
+
}
|
|
67
|
+
|
|
21
68
|
export class AuthManager {
|
|
22
69
|
/**
|
|
23
70
|
* @param {object} opts
|
|
@@ -30,9 +77,10 @@ export class AuthManager {
|
|
|
30
77
|
* @param {boolean} [opts.config.allowLoopback=true]
|
|
31
78
|
* @param {boolean} [opts.config.adminProtection=true] 管理保护独立开关:关闭后管理操作免 adminToken
|
|
32
79
|
* @param {(patch: object) => Promise<void>|void} [opts.onPersist]
|
|
80
|
+
* @param {string} [opts.sessionsFile] 登录会话持久化文件路径(默认 ~/.dsh/dsh-bridge/sessions.json;测试注入临时目录)
|
|
33
81
|
* @param {object} [opts.logger]
|
|
34
82
|
*/
|
|
35
|
-
constructor({ config = {}, onPersist, logger = console } = {}) {
|
|
83
|
+
constructor({ config = {}, onPersist, sessionsFile, logger = console } = {}) {
|
|
36
84
|
this.logger = logger
|
|
37
85
|
this.onPersist = onPersist
|
|
38
86
|
|
|
@@ -51,8 +99,9 @@ export class AuthManager {
|
|
|
51
99
|
// 内部隧道专用鉴权密钥(内存生成,用于辨别本地自建隧道转发流量与真实本机访问)
|
|
52
100
|
this.internalTunnelSecret = randomBytes(24).toString('hex')
|
|
53
101
|
|
|
54
|
-
// Session
|
|
55
|
-
|
|
102
|
+
// Session 存储与重启恢复:sessionToken -> { createdAt, expiresAt }
|
|
103
|
+
// 持久化到 sessions.json(路径可经 sessionsFile 注入),宿主(dsh web)重启后已登录设备免重输访问密码
|
|
104
|
+
this.sessions = PersistentSessionMap.load(sessionsFile ?? DEFAULT_SESSIONS_FILE)
|
|
56
105
|
// 管理员解锁 Session 内存存储:adminToken -> { createdAt, expiresAt }
|
|
57
106
|
this.adminSessions = new Map()
|
|
58
107
|
// 防暴力破解:ip -> { failedCount, lockUntil }
|
|
@@ -540,7 +589,8 @@ export class AuthManager {
|
|
|
540
589
|
|
|
541
590
|
dispose() {
|
|
542
591
|
clearInterval(this._cleanupTimer)
|
|
543
|
-
|
|
592
|
+
// 不清空 sessions:进程退出时内存随进程自然消失;
|
|
593
|
+
// 若此处 clear() 会把持久化文件一并清空,宿主重启后已登录设备仍会全部丢失
|
|
544
594
|
this.rateLimits.clear()
|
|
545
595
|
}
|
|
546
596
|
}
|
package/lib/bridge-rpc.js
CHANGED
|
@@ -289,6 +289,14 @@ export function installBridgeRpc(ctx, { service, authManager, platformManager, l
|
|
|
289
289
|
return ok(result);
|
|
290
290
|
}
|
|
291
291
|
|
|
292
|
+
if (endpoint === BRIDGE_ENDPOINTS.upgradeDsh) {
|
|
293
|
+
const adminErr = checkAdminAuth(authManager, payload, { requireConfigured: true });
|
|
294
|
+
if (adminErr) return adminErr;
|
|
295
|
+
|
|
296
|
+
const result = await service.upgradeDsh(payload);
|
|
297
|
+
return ok(result);
|
|
298
|
+
}
|
|
299
|
+
|
|
292
300
|
if (endpoint === BRIDGE_ENDPOINTS.restartDsh) {
|
|
293
301
|
const adminErr = checkAdminAuth(authManager, payload, { requireConfigured: true });
|
|
294
302
|
if (adminErr) return adminErr;
|
package/lib/index.js
CHANGED
|
@@ -10,7 +10,7 @@ import { get as httpsGet } from 'node:https';
|
|
|
10
10
|
import { networkInterfaces, homedir, totalmem, freemem, cpus, loadavg, platform, arch, release, hostname, uptime } from 'node:os';
|
|
11
11
|
import { join, dirname, basename, resolve, normalize } from 'node:path';
|
|
12
12
|
import { fileURLToPath } from 'node:url';
|
|
13
|
-
import { readFileSync, existsSync } from 'node:fs';
|
|
13
|
+
import { readFileSync, existsSync, realpathSync } from 'node:fs';
|
|
14
14
|
import { readFile, writeFile, mkdir, unlink, readdir, stat, access } from 'node:fs/promises';
|
|
15
15
|
import { spawn } from 'node:child_process';
|
|
16
16
|
import { createHash, createHmac } from 'node:crypto';
|
|
@@ -1033,6 +1033,63 @@ class BridgeService {
|
|
|
1033
1033
|
}
|
|
1034
1034
|
|
|
1035
1035
|
// 检查 npm 上是否有新版本(优先国内高速镜像 npmmirror,降级 npmjs 官方源)
|
|
1036
|
+
// 探测 DSH 宿主 CLI 的安装形态,判断能否通过"npm 全局升级"自动更新。
|
|
1037
|
+
// 仅当 dsh 命令 resolve 到当前 node 同 prefix 的 node_modules/@deepseek-ai/dsh(标准 npm 全局安装)
|
|
1038
|
+
// 时才允许一键升级;pnpm / 源码 / Electron 加壳等形态无法用 npm -g 升级,仅返回提示。
|
|
1039
|
+
// @returns {Promise<{upgradable:boolean, rootGlobal?:string, dshRealPath?:string, reason?:string}>}
|
|
1040
|
+
async _probeDshUpgrade() {
|
|
1041
|
+
const isWin = process.platform === 'win32';
|
|
1042
|
+
const nodeDir = dirname(process.execPath);
|
|
1043
|
+
const existingPath = process.env.PATH || process.env.Path || '';
|
|
1044
|
+
const extraPaths = isWin ? [nodeDir] : [nodeDir, '/opt/homebrew/bin', '/opt/homebrew/sbin', '/usr/local/bin', '/usr/bin', '/bin', join(homedir(), '.local/bin'), join(homedir(), '.npm-global/bin')];
|
|
1045
|
+
const separator = isWin ? ';' : ':';
|
|
1046
|
+
const augmentedEnv = { ...process.env, PATH: [...extraPaths, existingPath].filter(Boolean).join(separator) };
|
|
1047
|
+
if (isWin) augmentedEnv.Path = augmentedEnv.PATH;
|
|
1048
|
+
|
|
1049
|
+
const runCmd = (cmd, args) => new Promise((resolve) => {
|
|
1050
|
+
let cp;
|
|
1051
|
+
try {
|
|
1052
|
+
cp = spawn(cmd, args, { windowsHide: true, shell: true, env: augmentedEnv, timeout: 8000 });
|
|
1053
|
+
} catch {
|
|
1054
|
+
return resolve(null);
|
|
1055
|
+
}
|
|
1056
|
+
let stdout = ''; let stderr = '';
|
|
1057
|
+
cp.stdout?.on('data', (d) => { stdout += d.toString(); });
|
|
1058
|
+
cp.stderr?.on('data', (d) => { stderr += d.toString(); });
|
|
1059
|
+
cp.on('error', () => resolve(null));
|
|
1060
|
+
cp.on('close', (code) => {
|
|
1061
|
+
if (code === 0 && stdout.trim()) resolve(stdout.trim().split(/\r?\n/)[0]);
|
|
1062
|
+
else resolve(null);
|
|
1063
|
+
});
|
|
1064
|
+
});
|
|
1065
|
+
|
|
1066
|
+
try {
|
|
1067
|
+
// 1. 找 dsh bin 的真实路径(Windows which 不存在,用 where)
|
|
1068
|
+
const dshBin = await runCmd(isWin ? 'where' : 'which', [isWin ? 'dsh.cmd' : 'dsh']);
|
|
1069
|
+
if (!dshBin) {
|
|
1070
|
+
return { upgradable: false, reason: '未找到 dsh 命令(可能通过源码/打包方式运行,非 npm 全局安装)' };
|
|
1071
|
+
}
|
|
1072
|
+
// realpath 在主进程内解析(子进程 node -e + shell 引号嵌套易出错)
|
|
1073
|
+
let dshRealPath = dshBin;
|
|
1074
|
+
try {
|
|
1075
|
+
dshRealPath = realpathSync(dshBin) || dshBin;
|
|
1076
|
+
} catch { /* 保留原始路径继续判断 */ }
|
|
1077
|
+
// 2. 当前 node 对应的全局 node_modules 根
|
|
1078
|
+
const globalNodeModules = await runCmd('npm', ['root', '-g']);
|
|
1079
|
+
const rootGlobal = globalNodeModules ? dirname(globalNodeModules) : dirname(nodeDir);
|
|
1080
|
+
const marker = join('node_modules', '@deepseek-ai', 'dsh');
|
|
1081
|
+
if (dshRealPath.includes(marker) && (dshRealPath.startsWith(rootGlobal) || dshRealPath.includes('node-v') && dshRealPath.includes('lib'))) {
|
|
1082
|
+
return { upgradable: true, rootGlobal, dshRealPath };
|
|
1083
|
+
}
|
|
1084
|
+
return {
|
|
1085
|
+
upgradable: false,
|
|
1086
|
+
reason: 'dsh 非标准 npm 全局安装(Electron/打包/源码/pnpm 等),无法自动升级;请按官方渠道手动更新',
|
|
1087
|
+
};
|
|
1088
|
+
} catch {
|
|
1089
|
+
return { upgradable: false, reason: '探测 dsh 安装形态失败,请按官方渠道手动更新' };
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1036
1093
|
async checkVersion() {
|
|
1037
1094
|
// TTL 缓存:窗口内直接返回上次结果,避免重复请求外网 registry
|
|
1038
1095
|
if (this._versionCheckCache && Date.now() - this._versionCheckCachedAt < this._versionCheckTtlMs) {
|
|
@@ -1062,18 +1119,27 @@ class BridgeService {
|
|
|
1062
1119
|
try {
|
|
1063
1120
|
const latestData = await fetchRegistry('https://registry.npmmirror.com/@wenbin_wb/dsh-bridge/latest', 3500)
|
|
1064
1121
|
.catch(() => fetchRegistry('https://registry.npmjs.org/@wenbin_wb/dsh-bridge/latest', 5000));
|
|
1122
|
+
// DSH 宿主 CLI 最新版本(npm 全局包 @deepseek-ai/dsh;与插件同源镜像,失败不阻断面板)
|
|
1123
|
+
const dshLatestData = await fetchRegistry('https://registry.npmmirror.com/@deepseek-ai/dsh/latest', 3500)
|
|
1124
|
+
.catch(() => fetchRegistry('https://registry.npmjs.org/@deepseek-ai/dsh/latest', 5000))
|
|
1125
|
+
.catch(() => null);
|
|
1126
|
+
// 安装形态探测(决定是否给"一键升级 DSH"按钮):npm 全局可升级,加壳/源码仅提醒
|
|
1127
|
+
const dshProbe = await this._probeDshUpgrade();
|
|
1065
1128
|
const result = {
|
|
1066
1129
|
current: VERSION,
|
|
1067
1130
|
latest: latestData?.version ?? null,
|
|
1068
1131
|
releaseNotes: latestData?.releaseNotes ?? null,
|
|
1069
1132
|
dshVersion: await this.getDshVersion(),
|
|
1133
|
+
dshLatest: dshLatestData?.version ?? null,
|
|
1134
|
+
dshUpgradable: dshProbe.upgradable,
|
|
1135
|
+
dshUpgradeReason: dshProbe.reason || '',
|
|
1070
1136
|
};
|
|
1071
1137
|
this._versionCheckCache = result;
|
|
1072
1138
|
this._versionCheckCachedAt = Date.now();
|
|
1073
1139
|
return result;
|
|
1074
1140
|
} catch (e) {
|
|
1075
1141
|
// 失败不缓存(让下次调用可重试),但记录便于诊断
|
|
1076
|
-
return { current: VERSION, latest: null, error: e.message ?? '检查失败', dshVersion: await this.getDshVersion() };
|
|
1142
|
+
return { current: VERSION, latest: null, error: e.message ?? '检查失败', dshVersion: await this.getDshVersion(), dshLatest: null, dshUpgradable: false, dshUpgradeReason: '' };
|
|
1077
1143
|
}
|
|
1078
1144
|
}
|
|
1079
1145
|
|
|
@@ -1165,6 +1231,96 @@ class BridgeService {
|
|
|
1165
1231
|
return { ok: false, error: lastError?.message ?? '升级命令执行失败', version: targetVersion };
|
|
1166
1232
|
}
|
|
1167
1233
|
|
|
1234
|
+
// 一键升级 DSH 宿主 CLI(npm 全局包 @deepseek-ai/dsh)。
|
|
1235
|
+
// 升级的是"与当前 node 配对"的全局 prefix(dsh 命令所在目录),完成后需重启 DSH 生效。
|
|
1236
|
+
async upgradeDsh({ version } = {}) {
|
|
1237
|
+
const targetVersion = version ? String(version).trim() : 'latest';
|
|
1238
|
+
// 严格 SemVer 白名单校验(dsh 用 rc 版本号,如 0.1.2-rc.1)
|
|
1239
|
+
if (!/^(latest|\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?)$/.test(targetVersion)) {
|
|
1240
|
+
return { ok: false, error: `非法的版本号格式: ${targetVersion}`, version: targetVersion };
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
// 安装形态守卫:非标准 npm 全局安装(Electron/源码/pnpm 等)拒绝自动升级,
|
|
1244
|
+
// 避免"装一份不生效的新副本、误导用户以为升级成功"
|
|
1245
|
+
const probe = await this._probeDshUpgrade();
|
|
1246
|
+
if (!probe.upgradable) {
|
|
1247
|
+
return {
|
|
1248
|
+
ok: false,
|
|
1249
|
+
error: probe.reason || '当前 DSH 为非 npm 全局安装,无法自动升级;请按官方渠道手动更新',
|
|
1250
|
+
version: targetVersion,
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
const pkgSpec = `@deepseek-ai/dsh@${targetVersion}`;
|
|
1255
|
+
const isWin = process.platform === 'win32';
|
|
1256
|
+
const nodeDir = dirname(process.execPath);
|
|
1257
|
+
const home = homedir();
|
|
1258
|
+
const extraPaths = isWin ? [nodeDir] : [
|
|
1259
|
+
nodeDir,
|
|
1260
|
+
'/opt/homebrew/bin',
|
|
1261
|
+
'/opt/homebrew/sbin',
|
|
1262
|
+
'/usr/local/bin',
|
|
1263
|
+
'/usr/bin',
|
|
1264
|
+
'/bin',
|
|
1265
|
+
join(home, '.nvm/current/bin'),
|
|
1266
|
+
join(home, '.fnm/current/bin'),
|
|
1267
|
+
join(home, '.local/bin'),
|
|
1268
|
+
join(home, '.cargo/bin'),
|
|
1269
|
+
];
|
|
1270
|
+
const separator = isWin ? ';' : ':';
|
|
1271
|
+
const existingPath = process.env.PATH || process.env.Path || '';
|
|
1272
|
+
const augmentedEnv = {
|
|
1273
|
+
...process.env,
|
|
1274
|
+
PATH: [...extraPaths, existingPath].filter(Boolean).join(separator),
|
|
1275
|
+
};
|
|
1276
|
+
if (isWin) augmentedEnv.Path = augmentedEnv.PATH;
|
|
1277
|
+
|
|
1278
|
+
// 与当前 node 配对的 npm 绝对路径(保证装进 dsh 所在 prefix,而非 PATH 里其它 npm)
|
|
1279
|
+
const siblingNpm = join(nodeDir, isWin ? 'npm.cmd' : 'npm');
|
|
1280
|
+
const npmBin = existsSync(siblingNpm) ? siblingNpm : 'npm';
|
|
1281
|
+
// 优先用探测到的全局 root(dsh 实际所在 prefix),其次 nodeDir 父目录
|
|
1282
|
+
const globalRoot = probe.rootGlobal || dirname(nodeDir);
|
|
1283
|
+
|
|
1284
|
+
const tasks = [
|
|
1285
|
+
{ cmd: npmBin, args: ['install', '-g', '--prefix', globalRoot, pkgSpec] },
|
|
1286
|
+
{ cmd: npmBin, args: ['install', '-g', pkgSpec] },
|
|
1287
|
+
];
|
|
1288
|
+
|
|
1289
|
+
let lastError = null;
|
|
1290
|
+
for (const task of tasks) {
|
|
1291
|
+
try {
|
|
1292
|
+
const res = await new Promise((resolve, reject) => {
|
|
1293
|
+
let cp;
|
|
1294
|
+
try {
|
|
1295
|
+
cp = spawn(task.cmd, task.args, {
|
|
1296
|
+
windowsHide: true,
|
|
1297
|
+
shell: true,
|
|
1298
|
+
env: augmentedEnv,
|
|
1299
|
+
timeout: 180000,
|
|
1300
|
+
});
|
|
1301
|
+
} catch (spawnErr) {
|
|
1302
|
+
return reject(spawnErr);
|
|
1303
|
+
}
|
|
1304
|
+
let stdout = '';
|
|
1305
|
+
let stderr = '';
|
|
1306
|
+
cp.stdout?.on('data', (d) => { stdout += d.toString(); });
|
|
1307
|
+
cp.stderr?.on('data', (d) => { stderr += d.toString(); });
|
|
1308
|
+
cp.on('error', reject);
|
|
1309
|
+
cp.on('close', (code) => {
|
|
1310
|
+
if (code === 0) resolve({ stdout, stderr });
|
|
1311
|
+
else reject(new Error(stderr || stdout || `进程退出码 ${code}`));
|
|
1312
|
+
});
|
|
1313
|
+
});
|
|
1314
|
+
const output = (res.stdout || res.stderr || '升级成功').trim().slice(-500);
|
|
1315
|
+
this.logger?.info('dsh-bridge: DSH 升级命令成功: %s %s', task.cmd, task.args.join(' '));
|
|
1316
|
+
return { ok: true, command: `${task.cmd} ${task.args.join(' ')}`, output, version: targetVersion };
|
|
1317
|
+
} catch (err) {
|
|
1318
|
+
lastError = err;
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
return { ok: false, error: lastError?.message ?? '升级命令执行失败', version: targetVersion };
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1168
1324
|
// 优雅重启 DSH 服务(支持守护进程自动拉起或独立派生子进程重启)
|
|
1169
1325
|
async restartDsh() {
|
|
1170
1326
|
this.logger?.info('收到 DSH 重启请求,正在调度重启...');
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wenbin_wb/dsh-bridge",
|
|
3
|
-
"version": "2.10.
|
|
3
|
+
"version": "2.10.8",
|
|
4
4
|
"description": "手机扫码即可在移动端/公网继续用 DeepSeek Harness,人不在电脑前也能接着干。一键局域网二维码、Cloudflare 公网隧道、自建隧道与微信 / QQ / 飞书 / Telegram Bot(多工作区/会话持久化/媒体/卡片审批/流式输出),无需自己搭公网服务器。",
|
|
5
|
-
"releaseNotes": "【v2.10.
|
|
5
|
+
"releaseNotes": "【v2.10.8】\n• 新增:登录 Session 持久化(#36)——宿主 dsh web 重启后已登录设备免重输访问密码,手机远程访问不再每天重登;改密码/切换模式仍同步吊销全部旧会话(安全语义保持)\n• 新增:设置页检测 DSH 宿主更新并支持一键升级(npm 全局安装形态)——发现新版黄色提示 + 一键升级按钮;非 npm 安装(源码/打包)仅提醒并给出官方升级方式\n• 修复:iOS Safari 键盘弹起时聊天输入框上下跳动——键盘弹起时固定输入框滚动容器高度,避免随键盘反复重排抖动\n• 新增:移动端输入框可折叠——聊天头部工具栏新增折叠按钮,收起输入框后消息阅读区最大化;底部保留「点击输入消息」细条一键唤回;折叠偏好跨会话记忆",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "lib/index.js",
|
|
8
8
|
"exports": {
|