@wenbin_wb/dsh-bridge 2.10.6 → 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/client/index.js CHANGED
@@ -84,6 +84,7 @@ const GITHUB_URL = 'https://github.com/wenbin-wb/dsh-bridge';
84
84
  const RELEASES_URL = 'https://github.com/wenbin-wb/dsh-bridge/releases';
85
85
  const ISSUES_URL = 'https://github.com/wenbin-wb/dsh-bridge/issues/new';
86
86
  const TUNNEL_DOCS_URL = 'https://github.com/wenbin-wb/dsh-bridge/blob/main/docs/custom-tunnel.md';
87
+ const CLOUDFLARE_TUTORIAL_URL = 'https://github.com/wenbin-wb/dsh-bridge/blob/main/docs/cloudflare-fixed-domain.md';
87
88
 
88
89
  // 生成升级命令(拼接具体版本号;用 add 而非 update,update --latest 受已安装依赖版本约束可能无法升级到最新版)
89
90
  function upgradeCommands(latest) {
@@ -753,6 +754,12 @@ const CloudflareConfigForm = React.memo(function CloudflareConfigForm({ token, h
753
754
  React.createElement('div', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary, #6b7280)', marginBottom: 8, lineHeight: 1.5 } },
754
755
  '在 Cloudflare Zero Trust 控制台创建 Tunnel 即可获取专属 Token 并绑定自己的域名(如 dsh.yourname.com),每次重启 URL 永不变更。不填则使用默认免登录临时随机域名。'
755
756
  ),
757
+ React.createElement('a', {
758
+ href: CLOUDFLARE_TUTORIAL_URL,
759
+ target: '_blank',
760
+ rel: 'noreferrer',
761
+ style: { ...s.btnLink, marginBottom: 10 },
762
+ }, '📖 查看「Cloudflare 固定域名」申请与配置教程'),
756
763
  React.createElement('div', { style: { marginBottom: 8 } },
757
764
  React.createElement('input', {
758
765
  style: s.input,
@@ -2390,6 +2397,8 @@ function VersionBanner({ rpcCall }) {
2390
2397
 
2391
2398
  const hasUpdate = info?.latest && info?.current && !info.error && semverGt(info.latest, info.current);
2392
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));
2393
2402
 
2394
2403
  const handleUpgrade = React.useCallback(async () => {
2395
2404
  if (!info?.latest || upgrading) return;
@@ -2413,6 +2422,30 @@ function VersionBanner({ rpcCall }) {
2413
2422
  }
2414
2423
  }, [info?.latest, upgrading, rpcCall]);
2415
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
+
2416
2449
  const links = React.createElement('div', { style: { display: 'flex', gap: 10, alignItems: 'center', flexWrap: 'wrap' } },
2417
2450
  React.createElement('a', {
2418
2451
  href: GITHUB_URL, target: '_blank', rel: 'noreferrer', style: s.btnLink,
@@ -2471,12 +2504,16 @@ function VersionBanner({ rpcCall }) {
2471
2504
  hasUpdate && React.createElement('span', { style: { fontWeight: 600, fontSize: 11 } }, `➔ v${info.latest}`),
2472
2505
  info?.error && React.createElement('span', { style: { color: 'var(--dsw-alias-state-warn-primary,#d97706)', fontSize: 11 } }, '(网络超时)'),
2473
2506
  ),
2474
- // DSH 宿主版本标签
2507
+ // DSH 宿主版本标签(有新版时黄色高亮)
2475
2508
  info?.dshVersion && React.createElement('span', {
2476
2509
  style: {
2477
2510
  ...s.tag,
2478
- background: 'var(--dsw-alias-bg-layer-2,#f3f4f6)',
2479
- color: 'var(--dsw-alias-label-tertiary,#6b7280)',
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)',
2480
2517
  padding: '3px 10px',
2481
2518
  fontSize: 12,
2482
2519
  fontWeight: 500,
@@ -2484,9 +2521,11 @@ function VersionBanner({ rpcCall }) {
2484
2521
  alignItems: 'center',
2485
2522
  gap: 5,
2486
2523
  },
2524
+ title: dshHasUpdate ? `发现 DSH 新版本 v${info.dshLatest}` : undefined,
2487
2525
  },
2488
2526
  React.createElement('span', { style: { opacity: 0.75, fontSize: 11, fontWeight: 400 } }, 'DSH'),
2489
2527
  `v${info.dshVersion}`,
2528
+ dshHasUpdate && React.createElement('span', { style: { fontWeight: 600, fontSize: 11 } }, `➔ v${info.dshLatest}`),
2490
2529
  ),
2491
2530
  // 刷新检查按钮
2492
2531
  React.createElement('button', {
@@ -2665,6 +2704,102 @@ function VersionBanner({ rpcCall }) {
2665
2704
  ),
2666
2705
  ),
2667
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
+ ),
2668
2803
  );
2669
2804
  }
2670
2805
 
@@ -4560,6 +4695,199 @@ function RemoteDirectoryFlow(props) {
4560
4695
 
4561
4696
  // ---- 插件入口 ----
4562
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
+
4563
4891
  function apply(ctx) {
4564
4892
  window.__dshClientCtx = ctx;
4565
4893
  const rpcCall = (endpoint, payload, signal) =>
@@ -4568,6 +4896,9 @@ function apply(ctx) {
4568
4896
  window.__dshOpenRemoteWorkspaceModal = (onAdded, onPickDirect, onCancel) =>
4569
4897
  showRemoteWorkspaceDialog(rpcCall, onAdded, ctx, onPickDirect, onCancel);
4570
4898
 
4899
+ setupIosKeyboardAdapter();
4900
+ setupComposerCollapse();
4901
+
4571
4902
  setupMobileExperience(rpcCall, ctx);
4572
4903
 
4573
4904
  const injected = () => ({ pick: () => ctx.workspaces?.pickDirectory?.() });
@@ -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;
@@ -0,0 +1,158 @@
1
+ # Cloudflare 固定域名(Token 模式)申请与配置教程
2
+
3
+ > 适用:想让 dsh-bridge 的公网入口**固定不变**(如 `dsh.yourdomain.com`),而不是每次开启都换新的
4
+ > `trycloudflare.com` 随机地址。全程免费(Cloudflare 免费套餐即可),无需公网 IP、无需备案(境外流量)。
5
+
6
+ - 预计耗时:20–40 分钟(含域名生效等待)
7
+ - 费用:Cloudflare 免费套餐 $0;域名需自购(约 ¥30–80/年,.com/.net/.xyz 等均可)
8
+ - 💡 **界面语言**:Cloudflare 控制台支持中文。切换方式:页面右上角头像/语言菜单选择「中文(简体)」。本教程同时给出**中英文菜单对照**(如「添加站点 / Add a site」——斜杠前是中文界面名称,斜杠后是英文原名),用哪个界面都能对上。
9
+
10
+ ---
11
+
12
+ ## 原理一句话
13
+
14
+ 你在 Cloudflare 建一条 **Named Tunnel(固定隧道)**,把 `dsh.yourdomain.com` 这个子域名的流量经 Cloudflare 边缘转发到
15
+ 你电脑上 dsh-bridge 的本地端口(默认 `3082`)。dsh-bridge 只需要拿到这条隧道的 **Token** 就能替你后台运行
16
+ `cloudflared tunnel run --token <TOKEN>`,隧道进程由 dsh-bridge 托管(崩溃自动重启、随 DSH 启动自愈),域名永不改变。
17
+
18
+ ```
19
+ 手机/浏览器 → https://dsh.yourdomain.com → Cloudflare 边缘 → 加密隧道 → 你的电脑:3082 → DSH
20
+ ```
21
+
22
+ ---
23
+
24
+ ## 第 1 步:注册 Cloudflare 账号
25
+
26
+ 1. 打开 <https://dash.cloudflare.com/sign-up>,用邮箱注册(或 Google 账号登录)。
27
+ 2. 登录后进入 Dashboard。
28
+
29
+ ## 第 2 步:准备一个域名
30
+
31
+ 两种方式任选:
32
+
33
+ - **已有域名**:确保域名的 DNS 托管在 Cloudflare(见第 3 步)。
34
+ - **没有域名**:
35
+ 1. 在任意注册商购买一个(.com / .net / .xyz / .top 均可,便宜的先练手也行);
36
+ 2. 拿到域名的 **NS 记录**(或注册商 API 权限),准备迁到 Cloudflare。
37
+
38
+ ## 第 3 步:把域名接入 Cloudflare(DNS 托管)
39
+
40
+ > 固定域名隧道要求域名由 Cloudflare 托管(DNS 生效才能签发证书、路由流量)。若域名已在 Cloudflare 可跳过本步。
41
+
42
+ 1. Cloudflare Dashboard(控制台)→「**添加站点 / Add a site**」→ 输入你的域名 → 选 **Free(免费)** 套餐 → Continue(继续)。
43
+ 2. Cloudflare 会扫描现有 DNS 记录并导入(自动保留)。
44
+ 3. 按提示到**域名注册商**处,把域名的 NS(Name Server/名称服务器)改成 Cloudflare 给的两条(形如 `xxx.ns.cloudflare.com`)。
45
+ 4. 回 Cloudflare 点「**检查名称服务器 / Check nameservers**」,等待生效(通常几分钟到 24 小时,多数 1 小时内)。
46
+ 5. 状态变 **Active(有效)** 即托管完成。
47
+
48
+ ## 第 4 步:进入 Zero Trust 创建固定隧道
49
+
50
+ 1. 打开 Zero Trust 控制台:<https://one.dash.cloudflare.com/>
51
+ (或 Cloudflare Dashboard 左侧 → **Zero Trust**)。
52
+ 2. 首次使用会让你选团队名、套餐——选 **Free(免费)** 计划即可。
53
+ 3. 左侧菜单:**Networks(网络)→ Tunnels(隧道)**。
54
+ 4. 点 **Create a tunnel(创建隧道)**:
55
+ - 连接器类型选 **Cloudflared**;
56
+ - Tunnel name(隧道名称):起个名,如 `dsh-home`;
57
+ - 点 **Save tunnel(保存隧道)**。
58
+ 5. **⚠️ 立即复制并保存 Token(这一步最要紧!)**
59
+ 保存后页面会给出**安装并运行连接器**的命令,里面带一长串 Token:
60
+
61
+ ```bash
62
+ cloudflared tunnel run --token eyJhIjoi...(极长的一串)
63
+ ```
64
+
65
+ **请现在就把 `--token` 后面那一长串完整复制出来**,粘贴到记事本/密码管理器存好(可以顺手存好 `dsh.yourdomain.com` 这个域名一起备忘)。**复制完再点任何下一步/关闭页面**——万一丢了,见本步下方"Token 找不回来了怎么办"。
66
+
67
+ > 🤖 **这段命令不用你自己在电脑上运行**(无论 Windows / macOS / Linux 都不需要)——dsh-bridge 会在后台替你执行
68
+ > `cloudflared tunnel run --token ...`。这里展示命令只是为了让你看清 Token 长在哪、方便复制那一长串。
69
+
70
+ > 💡 **Token 找不回来了怎么办**(不用重建隧道,随时可取):
71
+ >
72
+ > 回到 **Networks(网络)→ Tunnels(隧道)** 列表 → 点你的**隧道名称**进入详情页 → 找 **Configure(配置)**按钮或右上角 **⋯** 菜单(不同时期界面略有差异),点击后页面会显示该隧道的 **Token**,或重新给出 `cloudflared tunnel run --token ...` 的安装命令——复制其中那一长串即可。
73
+ >
74
+ > 取 token 时**无需在电脑上运行任何命令**,Token 就在网页里,直接复制即可。
75
+
76
+ ## 第 5 步:给隧道绑定你的固定子域名(Public Hostname / 公共主机名)
77
+
78
+ > ⚠️ **关键**:隧道本身不含"域名→本地端口"的路由规则,必须在这里配。而且这个子域名要和你之后在
79
+ > dsh-bridge 面板里填的「自定义固定域名」**完全一致**。
80
+
81
+ 1. 隧道创建后进入隧道详情页 → 切到 **Public Hostname(公共主机名)** 标签 → **Add a public hostname(添加公共主机名)**。
82
+ 2. 填写(中英文界面字段对照):
83
+ - **Subdomain(子域)**:如 `dsh`
84
+ - **Domain(域)**:下拉选你的域名(如 `yourdomain.com`)
85
+ - 即最终 = `dsh.yourdomain.com`
86
+ - **Service(服务)→ Type(类型)**:`HTTP`
87
+ - **URL**:`localhost:3082`(dsh-bridge 代理端口;如果你改过 dsh-bridge 端口则填对应端口)
88
+ 3. 保存(**Save / 保存**)。Cloudflare 会自动为你签发该域名的免费 TLS 证书并创建 DNS 记录(Tunnel 类型,CNAME 指向 `*.cfargotunnel.com`),无需手动配 DNS。
89
+ 4. 状态稍后变为 **Healthy(运行正常)**(见隧道详情的 Connectors(连接器)与 Public Hostname(公共主机名)状态)。
90
+
91
+ > 端口说明:dsh-bridge 默认把面板代理在 `3082`(`proxyPort`),DSH 原生在 `3080`。**必须指向 3082**(走 dsh-bridge 的
92
+ > 认证/会话/二维码/隧道控制逻辑),不要直接指 3080。
93
+
94
+ > 端口说明:dsh-bridge 默认把面板代理在 `3082`(`proxyPort`),DSH 原生在 `3080`。**必须指向 3082**(走 dsh-bridge 的
95
+ > 认证/会话/二维码/隧道控制逻辑),不要直接指 3080。
96
+
97
+ ## 第 6 步:把 Token 和域名填回 dsh-bridge
98
+
99
+ 1. 打开 dsh-bridge 面板 → **公网隧道** tab。
100
+ 2. 展开底部「**⚙️ 隧道配置**」→「**Cloudflare 隧道**」卡 → 展开「**高级配置:固定域名 (Cloudflare Token)**」。
101
+ 3. 填写两项并「保存固定域名配置」:
102
+ - **自定义固定域名**:`dsh.yourdomain.com`(与第 5 步 Public Hostname **完全一致**,不要带 `https://`)
103
+ - **Tunnel Token**:第 4/5 步复制的 `cloudflared tunnel run --token` 后的那一长串
104
+ 4. 回到顶部「**公网访问入口**」卡 → 点「**开启**」(token 模式下该卡显示固定域名模式)。
105
+ 5. 几秒后状态变 **固定隧道已建立 (https://dsh.yourdomain.com)**。
106
+
107
+ ## 第 7 步:验证
108
+
109
+ 1. 浏览器打开 `https://dsh.yourdomain.com` → 应出现 DSH 登录页/面板(取决于你的访问认证设置)。
110
+ 2. Cloudflare Zero Trust → **Networks(网络)→ Tunnels(隧道)** → 该隧道 → **Healthy(运行正常)**,Connectors(连接器)有连接。
111
+ 3. 重启 DSH 服务或重启电脑,勾选「随 DSH 启动自动开启」后域名保持不变、自动恢复。
112
+
113
+ ---
114
+
115
+ ## 常见问题
116
+
117
+ ### Q1:面板显示"已建立"但浏览器打不开?
118
+ 几乎都是 **Public Hostname 与面板填的域名不一致**,或 Service URL 端口写错(应为 `localhost:3082` 而非 3080)。
119
+ 逐项核对第 5、6 步。
120
+
121
+ ### Q2:提示"cloudflared 启动失败: Incorrect Usage / flag provided but not defined"?
122
+ 请确认 dsh-bridge ≥ **v2.10.7**(v2.10.6 曾因 `--no-autoupdate` 参数位置导致固定域名隧道秒退,已修复)。
123
+
124
+ ### Q3:隧道一直"自动重连中"然后 error?
125
+ 先看面板 error 文案:
126
+ - 含 **Incorrect Usage / 配置错误** → Token 复制不完整或参数问题,重新复制整串 Token;
127
+ - 含 **退出 code≠0 / 连接失败** → 多为网络到 Cloudflare 边缘不通或 Token 无效,检查网络后点「开启」重试(自愈已内置退避重试)。
128
+
129
+ ### Q4:需要买最贵的套餐吗?
130
+ 不用。Cloudflare **Free** 套餐即可创建固定隧道、绑定子域名、免费 TLS。仅当你想在同一域名下加很多复杂规则或团队审计时才需付费。
131
+
132
+ ### Q5:域名必须在 Cloudflare 托管吗?
133
+ **固定域名隧道是的**(要在 Cloudflare DNS 建 `CNAME → *.cfargotunnel.com` 的记录)。不想迁移主域名的话,
134
+ 可以买一个便宜小域名专门做隧道入口,把主域名留在原注册商。
135
+
136
+ ### Q6:为什么面板里"复制 Token"和 Cloudflare 命令里看到的不一样长?
137
+ Token 就是 `cloudflared tunnel run --token` 后面那一长串(不含引号、不含 `--token` 字样本身)。
138
+ 若你在别处看到的 token 是用于 `cloudflared tunnel login` 的,那是不同的东西——固定域名模式要的是 **run --token** 那个。
139
+
140
+ ### Q7:创建隧道时忘了复制 Token,页面也关了,去哪找?
141
+ **不需要重建隧道**,Token 随时能在网页里取回:
142
+
143
+ 1. 打开 Zero Trust 控制台 → 左侧 **Networks(网络)→ Tunnels(隧道)**;
144
+ 2. 在隧道列表里**点击你那条隧道的名称**(不是行尾的按钮,是名称本身)进入详情;
145
+ 3. 详情页找 **Configure(配置)** 按钮,或页面右上角的 **⋯ / 更多** 菜单(不同时期的界面位置可能略有差异,但一定在隧道详情页里);
146
+ 4. 点击后页面会显示该隧道的 **Token**,或者重新给出带 Token 的安装命令(`cloudflared tunnel run --token ...`);
147
+ 5. 复制命令中 `--token` 后面那一长串即可。
148
+
149
+ > 提示:如果你在列表页只看到 **⋯** 下拉里有 **Edit / 编辑 / Delete / 删除** 而没有 Token,就点**隧道名称先进详情**,
150
+ > Token 在详情页内。整个过程都在浏览器网页里完成,**不需要在电脑上装 cloudflared 或执行任何命令**。
151
+
152
+ ---
153
+
154
+ ## 相关链接
155
+
156
+ - [Cloudflare Zero Trust 控制台](https://one.dash.cloudflare.com/)
157
+ - [Cloudflare 官方:Create a tunnel (dashboard)](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/get-started/create-remote-tunnel/)
158
+ - [Cloudflare 官方:Tunnel run 参数(--token / --no-autoupdate)](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/configure-tunnels/run-parameters/)