@wenbin_wb/dsh-bridge 2.10.7 → 2.10.9

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 CHANGED
@@ -4,6 +4,32 @@
4
4
 
5
5
  ---
6
6
 
7
+ ## [v2.10.9] - 2026-09-12
8
+
9
+ ### 🐞 修复
10
+
11
+ - **DSH 0.1.5 系列上 `dsh web` 启动即崩**(issue #38):DSH 从 `0.1.5-alpha.1` 起,宿主自带的 `dsh-client-connection` 调整了插件级服务声明却遗漏了一处同步修改,导致凡是注册 RPC 通道的插件都会让**整棵插件树加载失败**、`dsh web` 直接退出(报 `cannot get property "webServer" without inject`)。现已插件侧兼容:正常路径完全不变,仅在命中该宿主缺陷时自动补上解析兜底并完成通道注册,上游修复后自动空转、无副作用。DSH `0.1.5-alpha.1` / `alpha.2` / `rc.1` / `rc.2`(含当前最新版)均已实测通过,`0.1.0`~`0.1.4` 行为不变。
12
+
13
+ ### ✨ 优化
14
+
15
+ - **与 DSH 原生鉴权(`0.1.2` 起内置)确认兼容并加固**:反代转发时自动注入 DSH 要求的回环会话 cookie,手机 / 局域网 / 隧道访问无需处理 DSH 自身的 `?token=` 登录流程;同时修正凭据解析——当 DSH 凭据文件里存在多条密钥记录时,确保取用浏览器会话记录对应的密钥,避免远程访问出现 401。
16
+
17
+ ---
18
+
19
+ ## [v2.10.8] - 2026-09-08
20
+
21
+ ### ✨ 新功能
22
+
23
+ - **登录 Session 持久化**(issue #36,PR #37):宿主(dsh web)进程重启后,已登录设备不再需要重输访问密码——手机远程访问的登录态不再随每次 DSH 重启丢失。改密码 / 切换模式 / 重新生成 Token 仍会同步吊销全部旧会话(安全语义保持);文件损坏/写盘失败安全降级。
24
+ - **设置页检测 DSH 宿主更新并一键升级**:版本横幅同步检测 `@deepseek-ai/dsh` 线上最新版,发现新版时黄色提示;检测到 npm 全局安装形态时提供「一键升级 DSH」按钮(升级后引导重启),非 npm 安装(源码 / 打包加壳等)仅提醒并给官方升级方式,避免误导。
25
+ - **移动端输入框可折叠**:聊天头部工具栏(Session 下载钮旁)新增折叠按钮,收起底部输入区后消息阅读区最大化(约 +20% 可视高度);折叠时底部保留「✏️ 点击输入消息」细条一键唤回;折叠偏好跨会话记忆。
26
+
27
+ ### 🐞 修复
28
+
29
+ - **iOS Safari 键盘弹起时聊天输入框上下跳动**:输入区为 sticky 吸底布局,键盘弹起时随 visual viewport 收缩反复重排抖动——现键盘弹起时固定输入框所属滚动容器高度,键盘收起后还原,仅 iOS 生效、不依赖宿主类名。
30
+
31
+ ---
32
+
7
33
  ## [v2.10.7] - 2026-09-08
8
34
 
9
35
  ### 🐞 修复
package/README.en.md CHANGED
@@ -48,6 +48,7 @@
48
48
  - [7. 📊 Maintenance Dashboard & Graceful Restart](#7-📊-maintenance-dashboard--graceful-restart)
49
49
  - [💬 FAQ](#-faq)
50
50
  - [🛠️ Development & Contribution](#️-development--contribution)
51
+ - [⭐ Star History](#-star-history)
51
52
  - [📄 License](#-license)
52
53
 
53
54
  ---
@@ -77,6 +78,20 @@ node -v # v22.19+ or v24+
77
78
  dsh --version
78
79
  ```
79
80
 
81
+ ### DSH version compatibility
82
+
83
+ This plugin supports **both old and new DSH releases** — there is no need to pick a plugin version to match your harness.
84
+
85
+ | DSH version | Status |
86
+ | --- | --- |
87
+ | `0.1.0` ~ `0.1.1` | ✅ Supported (loopback-only RPC channel hardening) |
88
+ | `0.1.2` ~ `0.1.4` | ✅ Supported |
89
+ | `0.1.5-alpha.1` ~ `0.1.5-rc.2` | ✅ Supported (since v2.10.9) |
90
+
91
+ > **About the built-in DSH authentication**: since `0.1.2`, `dsh web` ships browser authentication — it prints a URL carrying a one-time token (`http://127.0.0.1:3080/?token=…`), which is exchanged for a session cookie bound to the loopback address. Afterwards `/`, `/api` and every plugin RPC channel require that cookie.
92
+ >
93
+ > This is **complementary to, not a duplicate of**, this plugin's access-password gate: the built-in auth protects "the DSH process on local loopback", while this plugin's gate protects "remote access arriving over LAN or a public tunnel". The plugin's reverse proxy injects a valid loopback session cookie while forwarding, so phone / tunnel access needs **no** manual handling of DSH's `?token=` — just use it as documented on this page.
94
+
80
95
  ### Installation
81
96
 
82
97
  ```bash
@@ -405,6 +420,18 @@ dsh plugin --profile web add .
405
420
 
406
421
  ---
407
422
 
423
+ ## ⭐ Star History
424
+
425
+ <a href="https://www.star-history.com/?repos=wenbin-wb%2Fdsh-bridge&type=date&legend=top-left">
426
+ <picture>
427
+ <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=wenbin-wb/dsh-bridge&type=date&theme=dark&legend=top-left" />
428
+ <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=wenbin-wb/dsh-bridge&type=date&legend=top-left" />
429
+ <img alt="Star History Chart" src="https://api.star-history.com/chart?repos=wenbin-wb/dsh-bridge&type=date&legend=top-left" />
430
+ </picture>
431
+ </a>
432
+
433
+ ---
434
+
408
435
  ## 📄 License
409
436
 
410
437
  MIT © [wenbin-wb](https://github.com/wenbin-wb)
package/README.md CHANGED
@@ -48,6 +48,7 @@
48
48
  - [7. 📊 运维监控看板与一键平滑重启](#7-📊-运维监控看板与一键平滑重启)
49
49
  - [💬 常见问题 (FAQ)](#-常见问题-faq)
50
50
  - [🛠️ 开发与贡献](#️-开发与贡献)
51
+ - [⭐ Star History](#-star-history)
51
52
  - [📄 开源协议](#-开源协议)
52
53
 
53
54
  ---
@@ -79,6 +80,20 @@ node -v # 应显示 v22.19+ 或 v24+
79
80
  dsh --version
80
81
  ```
81
82
 
83
+ ### DSH 版本兼容性
84
+
85
+ 本插件**同时兼容新旧 DSH**,无需按 DSH 版本挑选插件版本:
86
+
87
+ | DSH 版本 | 状态 |
88
+ | --- | --- |
89
+ | `0.1.0` ~ `0.1.1` | ✅ 支持(回环专用 RPC 通道加固) |
90
+ | `0.1.2` ~ `0.1.4` | ✅ 支持 |
91
+ | `0.1.5-alpha.1` ~ `0.1.5-rc.2` | ✅ 支持(v2.10.9 起) |
92
+
93
+ > **关于 DSH 原生鉴权**:DSH 从 `0.1.2` 起为 `dsh web` 内置了浏览器鉴权——启动时会打印带一次性 token 的地址(`http://127.0.0.1:3080/?token=…`),换取一枚绑定回环地址的会话 cookie,此后 `/`、`/api` 及各插件 RPC 通道都要求该 cookie。
94
+ >
95
+ > 这与本插件的**访问密码门禁是两层互补的防护**,不冲突也不重复:原生鉴权保护的是"本机回环上的 DSH 进程",本插件的门禁保护的是"经局域网 / 公网隧道进入的远程访问"。本插件的反向代理会在转发时自动注入合法的回环会话 cookie,因此手机 / 隧道访问**无需**手动处理 DSH 的 `?token=`,按本页说明正常使用即可。
96
+
82
97
  ### 安装插件
83
98
 
84
99
  ```bash
@@ -427,6 +442,18 @@ dsh plugin --profile web add .
427
442
 
428
443
  ---
429
444
 
445
+ ## ⭐ Star History
446
+
447
+ <a href="https://www.star-history.com/?repos=wenbin-wb%2Fdsh-bridge&type=date&legend=top-left">
448
+ <picture>
449
+ <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=wenbin-wb/dsh-bridge&type=date&theme=dark&legend=top-left" />
450
+ <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=wenbin-wb/dsh-bridge&type=date&legend=top-left" />
451
+ <img alt="Star History Chart" src="https://api.star-history.com/chart?repos=wenbin-wb/dsh-bridge&type=date&legend=top-left" />
452
+ </picture>
453
+ </a>
454
+
455
+ ---
456
+
430
457
  ## 📄 开源协议
431
458
 
432
459
  本项目基于 [MIT 许可证](LICENSE) 开源发布。
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(