@wenbin_wb/dsh-bridge 2.8.6 → 2.9.0

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
@@ -1,3 +1,4 @@
1
+ import { MOBILE_STYLES_CSS } from './mobile-styles.js'
1
2
  // dsh-bridge 客户端插件:设置页「远程访问」面板
2
3
 
3
4
  // 兼容非 HTTPS 环境(如手机局域网 HTTP 访问):为非安全上下文补齐 crypto.randomUUID
@@ -111,6 +112,7 @@ const s = {
111
112
  tag: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', padding: '3px 10px', borderRadius: 999, fontSize: 12, fontWeight: 500, whiteSpace: 'nowrap', flexShrink: 0, minWidth: 'max-content', lineHeight: 1.4 },
112
113
  input: { width: '100%', font: 'inherit', fontSize: 13, padding: '7px 10px', borderRadius: 8, border: '1px solid var(--dsw-alias-border-l2,#d1d5db)', background: 'var(--dsw-alias-bg-layer-2,#f9fafb)', color: 'var(--dsw-alias-label-primary,currentColor)', outline: 'none', boxSizing: 'border-box' },
113
114
  warn: { background: 'var(--dsw-alias-state-warn-bg,#fffbeb)', border: '1px solid var(--dsw-alias-state-warn-border,#fde68a)', borderRadius: 8, padding: '10px 14px', fontSize: 12, color: 'var(--dsw-alias-state-warn-primary,#92400e)', lineHeight: 1.6 },
115
+ err: { background: 'var(--dsw-alias-state-error-bg,#fef2f2)', border: '1px solid var(--dsw-alias-state-error-border,#fecaca)', borderRadius: 8, padding: '10px 14px', fontSize: 12, color: 'var(--dsw-alias-state-error-primary,#991b1b)', lineHeight: 1.6 },
114
116
  tip: { background: 'var(--dsw-alias-bg-layer-2,#f9fafb)', borderRadius: 8, padding: '10px 14px', fontSize: 12, color: 'var(--dsw-alias-label-secondary,#6b7280)', lineHeight: 1.6 },
115
117
  };
116
118
 
@@ -317,6 +319,78 @@ function QrBlock({ url, qr, onReset, auth, onNavigateSecurity }) {
317
319
  );
318
320
  }
319
321
 
322
+ const LanNetworkSelector = React.memo(function LanNetworkSelector({ lan, onSelectIp }) {
323
+ const interfaces = lan?.interfaces || [];
324
+ const selectedIp = lan?.selectedIp || '';
325
+ const currentIp = lan?.ip || '';
326
+ const [switching, setSwitching] = React.useState(false);
327
+
328
+ if (!interfaces || interfaces.length <= 1) return null;
329
+
330
+ const handleChange = async (e) => {
331
+ const val = e.target.value;
332
+ setSwitching(true);
333
+ try {
334
+ await onSelectIp(val || null);
335
+ } finally {
336
+ setSwitching(false);
337
+ }
338
+ };
339
+
340
+ return React.createElement('div', {
341
+ style: {
342
+ ...s.block,
343
+ background: 'var(--dsw-alias-bg-layer-2, rgba(243, 244, 246, 0.6))',
344
+ padding: '10px 12px',
345
+ borderRadius: 8,
346
+ border: '1px solid var(--dsw-alias-border-l2, #e5e7eb)',
347
+ marginTop: 8,
348
+ marginBottom: 6,
349
+ },
350
+ },
351
+ React.createElement('div', {
352
+ style: {
353
+ display: 'flex',
354
+ alignItems: 'center',
355
+ justifyContent: 'space-between',
356
+ marginBottom: 6,
357
+ fontSize: 12,
358
+ fontWeight: 500,
359
+ color: 'var(--dsw-alias-label-primary, currentColor)',
360
+ },
361
+ },
362
+ React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 5 } },
363
+ '🛜 局域网网卡 / IP 选择'
364
+ ),
365
+ switching && React.createElement('span', {
366
+ style: { fontSize: 11, color: 'var(--dsw-alias-brand-primary, #4f6ef7)' },
367
+ }, '切换中…')
368
+ ),
369
+ React.createElement('div', { style: { ...s.muted, fontSize: 11, marginBottom: 6 } },
370
+ '检测到主机存在多张网卡(如物理 Wi-Fi、以太网、WSL 或虚拟机)。若默认 IP 无法被移动端访问,可手动切换:'
371
+ ),
372
+ React.createElement('select', {
373
+ style: {
374
+ ...s.input,
375
+ height: 32,
376
+ fontSize: 12,
377
+ padding: '0 8px',
378
+ background: 'var(--dsw-alias-bg-layer-1, #ffffff)',
379
+ cursor: 'pointer',
380
+ },
381
+ value: selectedIp,
382
+ onChange: handleChange,
383
+ disabled: switching,
384
+ },
385
+ React.createElement('option', { value: '' }, `⚡ 自动推荐 (${interfaces[0]?.address || currentIp} · ${interfaces[0]?.label || interfaces[0]?.name || ''})`),
386
+ interfaces.map((iface) => React.createElement('option', {
387
+ key: `${iface.name}-${iface.address}`,
388
+ value: iface.address,
389
+ }, `${iface.address} · ${iface.label || iface.name}${iface.isVirtual ? ' [虚拟/WSL]' : ''}`)),
390
+ ),
391
+ );
392
+ });
393
+
320
394
  const CustomTunnelGuide = React.memo(function CustomTunnelGuide() {
321
395
  return React.createElement('div', { style: s.block },
322
396
  React.createElement('a', {
@@ -485,7 +559,11 @@ const CloudflareConfigForm = React.memo(function CloudflareConfigForm({ token, h
485
559
  setSaving(true);
486
560
  setMsg(null);
487
561
  try {
488
- await onSave({ token: tokenVal, hostname: hostnameVal });
562
+ // 只回传被修改的字段:掩码 '******' 与未变更的 token 都不上传,服务端保留现值
563
+ const patch = {};
564
+ if (hostnameVal !== (hostname || '')) patch.hostname = hostnameVal;
565
+ if (tokenVal !== (token || '')) patch.token = tokenVal;
566
+ if (Object.keys(patch).length > 0) await onSave(patch);
489
567
  setMsg({ ok: true, text: '✓ 固定域名配置已保存' });
490
568
  } catch (err) {
491
569
  setMsg({ ok: false, text: err.message || '保存失败' });
@@ -987,7 +1065,7 @@ const AccessAuthCard = React.memo(function AccessAuthCard({ auth, rpcCall, onUpd
987
1065
 
988
1066
  // ---- 通用 IM 平台卡片 ----
989
1067
 
990
- function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatusChange }) {
1068
+ function PlatformCard({ platformId, platformName, platformDesc, rpcCall }) {
991
1069
  const [platform, setPlatform] = React.useState(null);
992
1070
  const [err, setErr] = React.useState(null);
993
1071
  const [busy, setBusy] = React.useState(false);
@@ -1003,6 +1081,7 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatu
1003
1081
  approvalTimeoutSec: String(platform.config.approvalTimeoutSec ?? 600),
1004
1082
  maxMessageChars: String((platform.config.maxMessageChars >= 500 ? platform.config.maxMessageChars : null) ?? (platformId === 'telegram' ? 4096 : 2000)),
1005
1083
  sendChunkDelayMs: String(platform.config.sendChunkDelayMs ?? 1500),
1084
+ groupAutoApprove: Boolean(platform.config.groupAutoApprove),
1006
1085
  appId: platform.config.appId ?? '',
1007
1086
  // Secret 不由后端回传;空值表示沿用已保存密钥
1008
1087
  clientSecret: '',
@@ -1014,12 +1093,6 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatu
1014
1093
  }
1015
1094
  }, [platform?.config, platformId]);
1016
1095
 
1017
- // 向上传递连接状态(供平台列表卡片绿点使用)
1018
- React.useEffect(() => {
1019
- const connected = platform?.status === 'connected' || platform?.status === 'starting' || platform?.status === 'reconnecting';
1020
- onStatusChange?.(connected);
1021
- }, [platform?.status, onStatusChange]);
1022
-
1023
1096
  const loadInFlightRef = React.useRef(false);
1024
1097
  const seqRef = React.useRef(0);
1025
1098
  const load = React.useCallback(async (quiet = false) => {
@@ -1067,6 +1140,12 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatu
1067
1140
 
1068
1141
  const onLogin = React.useCallback(() => act(BRIDGE_ENDPOINTS.platformLogin, {}), [act]);
1069
1142
  const onStop = React.useCallback(() => act(BRIDGE_ENDPOINTS.platformStop, {}), [act]);
1143
+ // 断开后重连:凭证仍在(configured)时直接用已存凭证重启网关,无需重新扫码;
1144
+ // 只有解绑后(无凭证)才走扫码/绑定流程
1145
+ const onReconnect = React.useCallback(() => {
1146
+ if (platform?.configured) return act(BRIDGE_ENDPOINTS.platformStart, {});
1147
+ return act(BRIDGE_ENDPOINTS.platformLogin, {});
1148
+ }, [act, platform?.configured]);
1070
1149
 
1071
1150
  // 白名单管理
1072
1151
  const [newId, setNewId] = React.useState('');
@@ -1111,11 +1190,16 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatu
1111
1190
  // 高级设置保存
1112
1191
  const saveConfig = React.useCallback(async () => {
1113
1192
  if (!cfgDraft) return;
1193
+ // 非法数字输入回落到当前配置值/默认值,避免 NaN 发到服务端且使 cfgDirty 永真
1194
+ const num = (v, fallback) => {
1195
+ const n = Number(v);
1196
+ return Number.isFinite(n) ? n : fallback;
1197
+ };
1114
1198
  const payload = {
1115
- digestIntervalSec: Number(cfgDraft.digestIntervalSec),
1116
- approvalTimeoutSec: Number(cfgDraft.approvalTimeoutSec),
1117
- maxMessageChars: Number(cfgDraft.maxMessageChars),
1118
- sendChunkDelayMs: Number(cfgDraft.sendChunkDelayMs),
1199
+ digestIntervalSec: num(cfgDraft.digestIntervalSec, platform?.config?.digestIntervalSec ?? 300),
1200
+ approvalTimeoutSec: num(cfgDraft.approvalTimeoutSec, platform?.config?.approvalTimeoutSec ?? 600),
1201
+ maxMessageChars: num(cfgDraft.maxMessageChars, platform?.config?.maxMessageChars ?? 2000),
1202
+ sendChunkDelayMs: num(cfgDraft.sendChunkDelayMs, platform?.config?.sendChunkDelayMs ?? 1500),
1119
1203
  };
1120
1204
  // QQ / 飞书 / Telegram 平台额外携带凭证
1121
1205
  if (platformId === 'qq') {
@@ -1329,9 +1413,9 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatu
1329
1413
  ),
1330
1414
  React.createElement('div', { style: { display: 'flex', gap: 8, marginTop: 10, flexWrap: 'wrap', alignItems: 'center' } },
1331
1415
  platform.status !== 'connected' && platform.status !== 'starting' &&
1332
- React.createElement('button', { style: s.btnPri, onClick: onLogin, disabled: busy }, '重新连接'),
1416
+ React.createElement('button', { style: s.btnPri, onClick: onReconnect, disabled: busy, title: platform?.configured ? '使用已保存的凭证直接恢复连接,无需重新扫码' : '首次使用需扫码绑定' }, platform?.configured ? '重新连接' : '连接'),
1333
1417
  (platform.status === 'connected' || platform.status === 'starting') &&
1334
- React.createElement('button', { style: s.btnGhost, onClick: onStop, disabled: busy }, '断开'),
1418
+ React.createElement('button', { style: s.btnGhost, onClick: onStop, disabled: busy, title: '停止接收消息;凭证保留,可随时"重新连接"恢复' }, '断开'),
1335
1419
  React.createElement('button', {
1336
1420
  style: { ...s.btnGhost, color: 'var(--dsw-alias-state-error-primary,#dc2626)', borderColor: 'var(--dsw-alias-state-error-primary,#dc2626)', opacity: busy ? 0.5 : 1 },
1337
1421
  disabled: busy,
@@ -1339,6 +1423,23 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatu
1339
1423
  title: '清除登录凭证,下次需重新配置',
1340
1424
  }, '解绑账号'),
1341
1425
  ),
1426
+ // 群聊自动授权开关(仅支持群聊的平台):关闭时新群首次 @机器人 不会自动加白
1427
+ platformId !== 'wechat' && React.createElement('label', {
1428
+ style: { display: 'flex', alignItems: 'center', gap: 6, marginTop: 10, fontSize: 12, cursor: 'pointer', width: 'fit-content', color: 'var(--dsw-alias-label-secondary,#6b7280)' },
1429
+ title: '开启后,白名单之外的新群首次 @机器人 即自动授权入白名单;关闭(默认)时新群必须先在此手动添加',
1430
+ },
1431
+ React.createElement('input', {
1432
+ type: 'checkbox',
1433
+ checked: Boolean(cfgDraft?.groupAutoApprove),
1434
+ disabled: busy,
1435
+ onChange: (e) => {
1436
+ const checked = e.target.checked;
1437
+ setCfgDraft(d => ({ ...(d ?? {}), groupAutoApprove: checked }));
1438
+ act(BRIDGE_ENDPOINTS.platformSetConfig, { groupAutoApprove: checked });
1439
+ },
1440
+ }),
1441
+ React.createElement('span', null, '群聊自动授权(新群首次 @机器人 自动加入白名单,默认关闭)'),
1442
+ ),
1342
1443
  // 飞书 / Telegram 扫码直达对话引导卡片
1343
1444
  (platformId === 'feishu' || platformId === 'telegram') && platform.botQr && React.createElement('div', {
1344
1445
  style: {
@@ -1774,41 +1875,71 @@ function BackupRestoreWidget({ rpcCall, onUpdate }) {
1774
1875
  }
1775
1876
 
1776
1877
  // 运维 Tab 内的手动重启 DSH 服务小卡片
1777
- function RestartDshCard({ rpcCall }) {
1878
+ // 重启 + 健康轮询公共逻辑:定时器保存在 ref 中,组件卸载即清理。
1879
+ // 修复点:原先两处复制粘贴的 setInterval 不随组件卸载清理,用户切走 Tab 后
1880
+ // 轮询仍会继续运行,并在服务恢复时无条件执行 window.location.reload()。
1881
+ function useDshRestart({ rpcCall, maxAttempts = 30, texts = {} } = {}) {
1882
+ const T = {
1883
+ restarting: '正在向 DSH 服务发送重启指令…',
1884
+ reconnecting: 'DSH 服务正在重启中,正在自动重新连接…',
1885
+ success: '🎉 重启成功!已重新建立连接,正在刷新页面…',
1886
+ timeout: '重连等待超时,请手动刷新页面。',
1887
+ ...texts,
1888
+ };
1778
1889
  const [restarting, setRestarting] = React.useState(false);
1779
1890
  const [status, setStatus] = React.useState(null);
1891
+ const pollRef = React.useRef(null);
1892
+ const reloadRef = React.useRef(null);
1893
+
1894
+ React.useEffect(() => () => {
1895
+ if (pollRef.current) clearInterval(pollRef.current);
1896
+ if (reloadRef.current) clearTimeout(reloadRef.current);
1897
+ }, []);
1780
1898
 
1781
- const handleRestart = async () => {
1899
+ const startRestart = React.useCallback(async () => {
1782
1900
  setRestarting(true);
1783
- setStatus({ phase: 'restarting', text: '正在向 DSH 服务发送重启指令…' });
1901
+ setStatus({ phase: 'restarting', text: T.restarting });
1784
1902
  try {
1785
1903
  await rpcCall(BRIDGE_ENDPOINTS.restartDsh, {});
1786
- } catch {}
1904
+ } catch {
1905
+ // 服务可能瞬间关闭导致连接断开,忽略
1906
+ }
1787
1907
 
1788
- setStatus({ phase: 'reconnecting', text: 'DSH 服务正在重启中,正在自动重新连接…' });
1908
+ setStatus({ phase: 'reconnecting', text: T.reconnecting });
1789
1909
  await new Promise(r => setTimeout(r, 2000));
1790
1910
 
1791
1911
  let attempts = 0;
1792
- const maxAttempts = 30;
1793
- const pollHealth = setInterval(async () => {
1912
+ pollRef.current = setInterval(async () => {
1794
1913
  attempts++;
1795
1914
  try {
1796
1915
  const r = await rpcCall(BRIDGE_ENDPOINTS.checkVersion, {});
1797
1916
  if (r?.ok) {
1798
- clearInterval(pollHealth);
1799
- setStatus({ phase: 'success', text: '🎉 重启成功!已重新建立连接,正在刷新页面…' });
1800
- setTimeout(() => { window.location.reload(); }, 1000);
1917
+ clearInterval(pollRef.current);
1918
+ pollRef.current = null;
1919
+ setStatus({ phase: 'success', text: T.success });
1920
+ reloadRef.current = setTimeout(() => { window.location.reload(); }, 1000);
1801
1921
  return;
1802
1922
  }
1803
- } catch {}
1923
+ } catch {
1924
+ // 仍在启动中,继续等待
1925
+ }
1804
1926
 
1805
1927
  if (attempts >= maxAttempts) {
1806
- clearInterval(pollHealth);
1807
- setStatus({ phase: 'timeout', text: '重连等待超时,请手动刷新页面。' });
1928
+ clearInterval(pollRef.current);
1929
+ pollRef.current = null;
1930
+ setStatus({ phase: 'timeout', text: T.timeout });
1808
1931
  setRestarting(false);
1809
1932
  }
1810
1933
  }, 1000);
1811
- };
1934
+ }, [rpcCall, maxAttempts]);
1935
+
1936
+ const resetStatus = React.useCallback(() => setStatus(null), []);
1937
+
1938
+ return { restarting, status, startRestart, resetStatus };
1939
+ }
1940
+
1941
+ function RestartDshCard({ rpcCall }) {
1942
+ const { restarting, status, startRestart: handleRestart } = useDshRestart({ rpcCall });
1812
1943
 
1813
1944
  return React.createElement('div', { style: { ...s.card, marginBottom: 16 } },
1814
1945
  React.createElement('div', { style: { marginBottom: 10 } },
@@ -1882,8 +2013,6 @@ function RemoteWorkspaceCard({ rpcCall }) {
1882
2013
  onClick: () => {
1883
2014
  if (typeof window.__dshOpenRemoteWorkspaceModal === 'function') {
1884
2015
  window.__dshOpenRemoteWorkspaceModal();
1885
- } else if (typeof showRemoteWorkspaceDialog === 'function') {
1886
- showRemoteWorkspaceDialog(rpcCall, () => load());
1887
2016
  }
1888
2017
  },
1889
2018
  }, '+ 远程添加工作区'),
@@ -1915,6 +2044,20 @@ function RemoteWorkspaceCard({ rpcCall }) {
1915
2044
  );
1916
2045
  }
1917
2046
 
2047
+ // 手动升级命令行:展示命令 + 一键复制
2048
+ function UpgradeCommandRow({ cmd }) {
2049
+ const [copied, copy] = useCopy();
2050
+ return React.createElement('div', { style: { display: 'flex', gap: 6, alignItems: 'center' } },
2051
+ React.createElement('code', {
2052
+ style: { ...s.code, flex: '1 1 auto', fontSize: 12, lineHeight: 1.5, wordBreak: 'break-all' },
2053
+ }, cmd),
2054
+ React.createElement('button', {
2055
+ style: { ...s.btnGhost, height: 26, padding: '0 10px', fontSize: 11, flex: '0 0 auto' },
2056
+ onClick: () => copy(cmd),
2057
+ }, copied ? '✓ 已复制' : '复制'),
2058
+ );
2059
+ }
2060
+
1918
2061
  // 版本检查 + 一键升级 + GitHub/反馈入口
1919
2062
  function VersionBanner({ rpcCall }) {
1920
2063
  const [info, setInfo] = React.useState(null);
@@ -1922,15 +2065,22 @@ function VersionBanner({ rpcCall }) {
1922
2065
  const [upgrading, setUpgrading] = React.useState(false);
1923
2066
  const [upgradeResult, setUpgradeResult] = React.useState(null);
1924
2067
  const [showManual, setShowManual] = React.useState(false);
1925
- const [restarting, setRestarting] = React.useState(false);
1926
- const [restartStatus, setRestartStatus] = React.useState(null);
1927
2068
  const [dismissRestart, setDismissRestart] = React.useState(false);
2069
+ const { restarting, status: restartStatus, startRestart: handleRestart, resetStatus: resetRestartStatus } = useDshRestart({
2070
+ rpcCall,
2071
+ texts: {
2072
+ restarting: '正在调度 DSH 服务重启…',
2073
+ success: '🎉 重启成功!已自动加载最新版本。正在刷新页面…',
2074
+ },
2075
+ });
1928
2076
 
1929
2077
  const check = React.useCallback(async () => {
1930
2078
  setLoading(true);
1931
2079
  try {
1932
2080
  const r = await rpcCall(BRIDGE_ENDPOINTS.checkVersion, {});
1933
2081
  if (r?.ok) setInfo(r.value);
2082
+ } catch {
2083
+ // 版本检查失败:保留现有 info,不打断面板
1934
2084
  } finally {
1935
2085
  setLoading(false);
1936
2086
  }
@@ -1946,7 +2096,7 @@ function VersionBanner({ rpcCall }) {
1946
2096
  setUpgrading(true);
1947
2097
  setUpgradeResult(null);
1948
2098
  setDismissRestart(false);
1949
- setRestartStatus(null);
2099
+ resetRestartStatus();
1950
2100
  try {
1951
2101
  const r = await rpcCall(BRIDGE_ENDPOINTS.upgradePlugin, { version: info.latest });
1952
2102
  if (r?.ok && r.value?.ok) {
@@ -1963,46 +2113,6 @@ function VersionBanner({ rpcCall }) {
1963
2113
  }
1964
2114
  }, [info?.latest, upgrading, rpcCall]);
1965
2115
 
1966
- const handleRestart = React.useCallback(async () => {
1967
- setRestarting(true);
1968
- setRestartStatus({ phase: 'restarting', text: '正在调度 DSH 服务重启…' });
1969
- try {
1970
- await rpcCall(BRIDGE_ENDPOINTS.restartDsh, {});
1971
- } catch {
1972
- // 忽略 RPC 错误(因为服务可能瞬间关闭导致网络连接断开)
1973
- }
1974
-
1975
- setRestartStatus({ phase: 'reconnecting', text: 'DSH 服务正在重启中,正在自动重新连接…' });
1976
-
1977
- // 等待 2 秒后开始健康检查轮询
1978
- await new Promise(r => setTimeout(r, 2000));
1979
-
1980
- let attempts = 0;
1981
- const maxAttempts = 30; // 最多探测 30 次(约 30 秒)
1982
- const pollHealth = setInterval(async () => {
1983
- attempts++;
1984
- try {
1985
- const r = await rpcCall(BRIDGE_ENDPOINTS.checkVersion, {});
1986
- if (r?.ok) {
1987
- clearInterval(pollHealth);
1988
- setRestartStatus({ phase: 'success', text: '🎉 重启成功!已自动加载最新版本。正在刷新页面…' });
1989
- setTimeout(() => {
1990
- window.location.reload();
1991
- }, 1000);
1992
- return;
1993
- }
1994
- } catch {
1995
- // 仍在启动中,继续等待
1996
- }
1997
-
1998
- if (attempts >= maxAttempts) {
1999
- clearInterval(pollHealth);
2000
- setRestartStatus({ phase: 'timeout', text: '重连等待超时,请手动刷新页面。' });
2001
- setRestarting(false);
2002
- }
2003
- }, 1000);
2004
- }, [rpcCall]);
2005
-
2006
2116
  const links = React.createElement('div', { style: { display: 'flex', gap: 10, alignItems: 'center', flexWrap: 'wrap' } },
2007
2117
  React.createElement('a', {
2008
2118
  href: GITHUB_URL, target: '_blank', rel: 'noreferrer', style: s.btnLink,
@@ -2336,7 +2446,6 @@ function BridgePanel({ rpcCall }) {
2336
2446
  // 本机物理访问自动静默获取 adminToken,免输密码直通管理(支持 3080 原生端口与 3082 代理端口)
2337
2447
  const fetchLoopbackToken = React.useCallback(async () => {
2338
2448
  if (!isLocalhost) return null;
2339
- const currentPort = typeof window !== 'undefined' ? (window.location.port || (window.location.protocol === 'https:' ? '443' : '80')) : '3082';
2340
2449
  const proxyPort = status?.proxy?.port || 3082;
2341
2450
  const candidateUrls = [
2342
2451
  '/__dsh_bridge__/loopback-token',
@@ -2369,6 +2478,9 @@ function BridgePanel({ rpcCall }) {
2369
2478
  }
2370
2479
  }, [isLocalhost, adminUnlocked, fetchLoopbackToken]);
2371
2480
 
2481
+ // 被管理员鉴权拦截的操作在此暂存:解锁成功后自动续办,用户无需再点一次
2482
+ const pendingRetryRef = React.useRef(null);
2483
+
2372
2484
  const authRpcCall = React.useCallback(async (endpoint, payload = {}, signal) => {
2373
2485
  let token = adminToken || getGlobalAdminToken();
2374
2486
  if (isLocalhost && !token) {
@@ -2383,6 +2495,7 @@ function BridgePanel({ rpcCall }) {
2383
2495
  if (res?.ok === false) {
2384
2496
  const msg = res?.error?.message || '';
2385
2497
  if (msg.includes('管理员权限') || msg.includes('管理密码解锁')) {
2498
+ pendingRetryRef.current = { endpoint, payload: enriched };
2386
2499
  setUnlockErr(msg);
2387
2500
  setShowUnlockModal(true);
2388
2501
  }
@@ -2474,6 +2587,28 @@ function BridgePanel({ rpcCall }) {
2474
2587
  return () => clearInterval(t);
2475
2588
  }, [load]);
2476
2589
 
2590
+ // 解锁成功后自动续办被拦截的操作:用户不再需要重新点击一次
2591
+ React.useEffect(() => {
2592
+ if (!adminUnlocked) return;
2593
+ const pending = pendingRetryRef.current;
2594
+ if (!pending) return;
2595
+ pendingRetryRef.current = null;
2596
+ const token = adminToken || getGlobalAdminToken();
2597
+ (async () => {
2598
+ try {
2599
+ const r = await rpcCall(pending.endpoint, { ...pending.payload, adminToken: token });
2600
+ if (r?.ok) {
2601
+ if (r.value) setStatus(r.value);
2602
+ setErr(null);
2603
+ } else {
2604
+ setErr(r?.error?.message || '刚才的操作重试失败,请手动重试');
2605
+ }
2606
+ } catch (e) {
2607
+ setErr(e.message || '刚才的操作重试失败,请手动重试');
2608
+ }
2609
+ })();
2610
+ }, [adminUnlocked, adminToken, rpcCall]);
2611
+
2477
2612
  const act = React.useCallback(async (endpoint, payload) => {
2478
2613
  try {
2479
2614
  const r = await authRpcCall(endpoint, payload ?? {});
@@ -2497,6 +2632,8 @@ function BridgePanel({ rpcCall }) {
2497
2632
  act(BRIDGE_ENDPOINTS.saveCloudflaredConfig, { token, hostname })
2498
2633
  , [act]);
2499
2634
 
2635
+ const onSelectLanIp = React.useCallback((ip) => act(BRIDGE_ENDPOINTS.setLanIp, { ip }), [act]);
2636
+
2500
2637
  const onStartCustom = React.useCallback(() => act(BRIDGE_ENDPOINTS.startCustomTunnel), [act]);
2501
2638
  const onStopCustom = React.useCallback(() => act(BRIDGE_ENDPOINTS.stopCustomTunnel), [act]);
2502
2639
  const onToggleCustomAutoStart = React.useCallback((autoStart) =>
@@ -2536,7 +2673,12 @@ function BridgePanel({ rpcCall }) {
2536
2673
  data: { running: status?.proxy?.running, url: status?.lan?.url, qr: status?.lan?.qr },
2537
2674
  auth: status?.auth,
2538
2675
  onNavigateSecurity: navSecurity,
2539
- });
2676
+ },
2677
+ React.createElement(LanNetworkSelector, {
2678
+ lan: status?.lan,
2679
+ onSelectIp: onSelectLanIp,
2680
+ })
2681
+ );
2540
2682
  } else if (activeTab === 'tunnel') {
2541
2683
  tabContent = React.createElement(React.Fragment, null,
2542
2684
  React.createElement(TunnelCard, {
@@ -2666,7 +2808,6 @@ function BridgePanel({ rpcCall }) {
2666
2808
  platformName: IM_PLATFORMS.find(p => p.id === selectedPlatform)?.label ?? selectedPlatform,
2667
2809
  platformDesc: IM_PLATFORMS.find(p => p.id === selectedPlatform)?.desc ?? '',
2668
2810
  rpcCall: authRpcCall,
2669
- onStatusChange: () => {}, // 状态变化已由 listPlatforms 轮询处理,不需要回调
2670
2811
  }),
2671
2812
  );
2672
2813
  }
@@ -2703,7 +2844,7 @@ function BridgePanel({ rpcCall }) {
2703
2844
  },
2704
2845
  React.createElement('div', { style: { fontWeight: 600, color: 'var(--dsw-alias-label-primary,currentColor)', marginBottom: 4 } }, '🛟 救急解除锁定指引:'),
2705
2846
  React.createElement('div', null, '1. ', React.createElement('strong', null, '电脑本机直连修改'), ':直接在运行本程序的电脑本机打开本控制台(127.0.0.1 享有物理免锁特权),可随时修改策略或清除密码。'),
2706
- React.createElement('div', { style: { marginTop: 4 } }, '2. ', React.createElement('strong', null, '服务器救急指令'), ':在宿主电脑/服务器终端执行 ', React.createElement('code', { style: s.code }, 'touch ~/.dsh/dsh-bridge/reset-auth'), ' 即可瞬间清空密码恢复初始状态。'),
2847
+ React.createElement('div', { style: { marginTop: 4 } }, '2. ', React.createElement('strong', null, '服务器 / 无头环境'), ':救急重置步骤参见 GitHub README 的「三重容灾保命体系」章节。'),
2707
2848
  ),
2708
2849
  )
2709
2850
  ) : (
@@ -2754,7 +2895,7 @@ function BridgePanel({ rpcCall }) {
2754
2895
  },
2755
2896
  React.createElement('div', { style: { fontWeight: 600, color: 'var(--dsw-alias-label-primary,currentColor)', marginBottom: 4 } }, '🛟 找回与重置密码指引:'),
2756
2897
  React.createElement('div', null, '1. ', React.createElement('strong', null, '电脑本机直连修改'), ':直接在运行本程序的电脑本机打开本控制台(127.0.0.1 享有物理免锁特权),可随时修改管理密码。'),
2757
- React.createElement('div', { style: { marginTop: 4 } }, '2. ', React.createElement('strong', null, '服务器救急指令'), ':在宿主电脑终端执行 ', React.createElement('code', { style: s.code }, 'touch ~/.dsh/dsh-bridge/reset-auth'), ' 即可瞬间清空密码恢复初始状态。'),
2898
+ React.createElement('div', { style: { marginTop: 4 } }, '2. ', React.createElement('strong', null, '服务器 / 无头环境'), ':救急重置步骤参见 GitHub README 的「三重容灾保命体系」章节。'),
2758
2899
  ),
2759
2900
  )
2760
2901
  )
@@ -2901,805 +3042,7 @@ function injectMobileStyles() {
2901
3042
 
2902
3043
  const style = document.createElement('style');
2903
3044
  style.id = 'dsh-bridge-mobile-styles';
2904
- style.textContent = `
2905
- /* DSH Bridge 隐藏 Tab 栏原生滚动条并保持平滑滑动 */
2906
- .dsh-tabbar-container {
2907
- scrollbar-width: none !important;
2908
- -ms-overflow-style: none !important;
2909
- }
2910
- .dsh-tabbar-container::-webkit-scrollbar {
2911
- display: none !important;
2912
- width: 0 !important;
2913
- height: 0 !important;
2914
- }
2915
-
2916
- /* DSH Bridge 移动端自适应与触控交互增强样式 */
2917
- :root {
2918
- --dsh-mobile-header-h: 52px;
2919
- --dsh-mobile-safe-top: env(safe-area-inset-top, 0px);
2920
- --dsh-mobile-safe-bottom: env(safe-area-inset-bottom, 0px);
2921
- }
2922
-
2923
- @media (max-width: 768px) {
2924
- /* 1. 主框架为 Header 腾出顶部空间 */
2925
- div[class*="_frame"] {
2926
- display: flex !important;
2927
- flex-direction: column !important;
2928
- width: 100vw !important;
2929
- height: 100dvh !important;
2930
- margin: 0 !important;
2931
- padding-top: var(--dsh-mobile-header-h) !important;
2932
- position: relative !important;
2933
- grid-template-columns: 1fr !important;
2934
- overflow: hidden !important;
2935
- box-sizing: border-box !important;
2936
- }
2937
-
2938
- /* 2. 顶部原生导航条:100% 还原 DeepSeek App (左侧双横线,右侧(+),中间留白,无多余设置按钮) */
2939
- .dsh-mobile-app-header {
2940
- position: fixed !important;
2941
- top: 0 !important;
2942
- left: 0 !important;
2943
- right: 0 !important;
2944
- height: var(--dsh-mobile-header-h) !important;
2945
- padding-top: var(--dsh-mobile-safe-top) !important;
2946
- background: transparent !important;
2947
- display: flex !important;
2948
- align-items: center !important;
2949
- justify-content: space-between !important;
2950
- padding-left: 16px !important;
2951
- padding-right: 16px !important;
2952
- z-index: 9998 !important;
2953
- box-sizing: border-box !important;
2954
- user-select: none !important;
2955
- pointer-events: none !important;
2956
- }
2957
-
2958
- /* 左侧双横线按钮 (DeepSeek App 原生图标) */
2959
- .dsh-header-menu-btn {
2960
- width: 40px;
2961
- height: 40px;
2962
- border-radius: 50%;
2963
- border: none;
2964
- background: transparent;
2965
- color: var(--dsw-alias-label-primary, #111827);
2966
- display: inline-flex;
2967
- align-items: center;
2968
- justify-content: flex-start;
2969
- cursor: pointer;
2970
- padding: 0;
2971
- transition: opacity 0.15s;
2972
- pointer-events: auto !important;
2973
- }
2974
- .dsh-header-menu-btn:active {
2975
- opacity: 0.6;
2976
- }
2977
-
2978
- /* 右侧 (+) 新建会话按钮 (DeepSeek App 原生图标) */
2979
- .dsh-header-new-btn {
2980
- width: 40px;
2981
- height: 40px;
2982
- border-radius: 50%;
2983
- border: none;
2984
- background: transparent;
2985
- color: var(--dsw-alias-label-primary, #111827);
2986
- display: inline-flex;
2987
- align-items: center;
2988
- justify-content: flex-end;
2989
- cursor: pointer;
2990
- padding: 0;
2991
- transition: opacity 0.15s;
2992
- pointer-events: auto !important;
2993
- }
2994
- .dsh-header-new-btn:active {
2995
- opacity: 0.6;
2996
- }
2997
-
2998
- /* 中间动态会话标题 (单行居中打点截断,100% 还原原生 App 导航体验) */
2999
- .dsh-mobile-header-title {
3000
- flex: 1 1 auto !important;
3001
- min-width: 0 !important;
3002
- text-align: center !important;
3003
- font-size: 15px !important;
3004
- font-weight: 600 !important;
3005
- color: var(--dsw-alias-label-primary, #111827) !important;
3006
- overflow: hidden !important;
3007
- text-overflow: ellipsis !important;
3008
- white-space: nowrap !important;
3009
- padding: 0 10px !important;
3010
- user-select: none !important;
3011
- pointer-events: none !important;
3012
- letter-spacing: -0.2px !important;
3013
- }
3014
-
3015
- /* 3. 中间主内容区与输入框 */
3016
- div[class*="_centerCol"] {
3017
- flex: 1 1 100% !important;
3018
- width: 100% !important;
3019
- min-width: 0 !important;
3020
- max-width: 100% !important;
3021
- display: flex !important;
3022
- height: 100% !important;
3023
- }
3024
-
3025
- div[class*="_detailsCol"],
3026
- div[class*="toggleCluster"],
3027
- div[class*="W-zNGW_toggleCluster"] {
3028
- display: none !important;
3029
- }
3030
-
3031
- /* 3.0 工作区 Workbench / 任务管理 / 多 Tab 栏移动端自适应适配 */
3032
- body:not(.dsh-workbench-open) div[class*="nArs4W_panel"],
3033
- body:not(.dsh-workbench-open) div[class*="workbench_panel"],
3034
- body:not(.dsh-workbench-open) div[class*="workbenchPanel"],
3035
- div[class*="nArs4W_panel"][class*="panelHidden"],
3036
- div[class*="workbench_panel"][class*="panelHidden"],
3037
- div[class*="workbenchPanel"][class*="panelHidden"],
3038
- div[class*="panelHidden"] {
3039
- display: none !important;
3040
- visibility: hidden !important;
3041
- pointer-events: none !important;
3042
- width: 0 !important;
3043
- height: 0 !important;
3044
- max-height: 0 !important;
3045
- z-index: -1 !important;
3046
- opacity: 0 !important;
3047
- transform: translateX(105%) !important;
3048
- }
3049
-
3050
- body.dsh-workbench-open div[class*="nArs4W_panel"]:not([class*="panelHidden"]),
3051
- body.dsh-workbench-open div[class*="workbench_panel"]:not([class*="panelHidden"]),
3052
- body.dsh-workbench-open div[class*="workbenchPanel"]:not([class*="panelHidden"]) {
3053
- display: flex !important;
3054
- visibility: visible !important;
3055
- pointer-events: auto !important;
3056
- top: var(--dsh-mobile-header-h, 52px) !important;
3057
- height: calc(100dvh - var(--dsh-mobile-header-h, 52px)) !important;
3058
- max-height: calc(100dvh - var(--dsh-mobile-header-h, 52px)) !important;
3059
- z-index: 50 !important;
3060
- box-sizing: border-box !important;
3061
- background: var(--dsw-alias-bg-layer-1, #ffffff) !important;
3062
- transform: none !important;
3063
- opacity: 1 !important;
3064
- }
3065
-
3066
- /* Tab 栏:横向滑动手势 + 干净的底部边框,杜绝与顶部移动端 Header 重叠 */
3067
- div[class*="nArs4W_tabBar"],
3068
- div[class*="workbench_tabBar"],
3069
- div[class*="tabBar"] {
3070
- min-height: 40px !important;
3071
- height: 40px !important;
3072
- background: var(--dsw-alias-bg-layer-1, #ffffff) !important;
3073
- border-bottom: 1px solid rgba(0, 0, 0, 0.08) !important;
3074
- display: flex !important;
3075
- align-items: center !important;
3076
- justify-content: space-between !important;
3077
- padding: 0 8px !important;
3078
- gap: 6px !important;
3079
- overflow: visible !important;
3080
- box-sizing: border-box !important;
3081
- }
3082
-
3083
- div[class*="nArs4W_tabList"],
3084
- div[class*="tabList"] {
3085
- display: flex !important;
3086
- align-items: center !important;
3087
- gap: 6px !important;
3088
- flex: 1 1 auto !important;
3089
- min-width: 0 !important;
3090
- overflow-x: auto !important;
3091
- scrollbar-width: none !important;
3092
- -webkit-overflow-scrolling: touch !important;
3093
- }
3094
- div[class*="nArs4W_tabList"]::-webkit-scrollbar,
3095
- div[class*="tabList"]::-webkit-scrollbar {
3096
- display: none !important;
3097
- }
3098
-
3099
- /* 单个 Tab 胶囊化,文字超长自动打点,防止 Tab 互相挤压 */
3100
- div[class*="nArs4W_tab"],
3101
- div[class*="workbench_tab"] {
3102
- flex: 0 0 auto !important;
3103
- max-width: 170px !important;
3104
- min-width: 70px !important;
3105
- height: 30px !important;
3106
- padding: 0 8px 0 10px !important;
3107
- border-radius: 6px !important;
3108
- font-size: 12.5px !important;
3109
- display: inline-flex !important;
3110
- align-items: center !important;
3111
- justify-content: space-between !important;
3112
- gap: 6px !important;
3113
- background: var(--dsw-alias-bg-layer-2, #f3f4f6) !important;
3114
- color: var(--dsw-alias-label-secondary, #6b7280) !important;
3115
- cursor: pointer !important;
3116
- user-select: none !important;
3117
- box-sizing: border-box !important;
3118
- }
3119
-
3120
- div[class*="nArs4W_tabActive"],
3121
- div[class*="workbench_tabActive"] {
3122
- background: var(--dsw-alias-bg-layer-1, #ffffff) !important;
3123
- color: var(--dsw-alias-label-primary, #111827) !important;
3124
- font-weight: 600 !important;
3125
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1) !important;
3126
- }
3127
-
3128
- span[class*="nArs4W_tabTitle"],
3129
- span[class*="tabTitle"] {
3130
- overflow: hidden !important;
3131
- text-overflow: ellipsis !important;
3132
- white-space: nowrap !important;
3133
- flex: 1 1 auto !important;
3134
- }
3135
-
3136
- button[class*="nArs4W_tabClose"],
3137
- button[class*="tabClose"] {
3138
- width: 18px !important;
3139
- height: 18px !important;
3140
- border-radius: 50% !important;
3141
- display: inline-flex !important;
3142
- align-items: center !important;
3143
- justify-content: center !important;
3144
- flex-shrink: 0 !important;
3145
- opacity: 0.6 !important;
3146
- padding: 0 !important;
3147
- }
3148
-
3149
- button[class*="nArs4W_tabBarPlus"],
3150
- button[class*="tabBarPlus"] {
3151
- width: 28px !important;
3152
- height: 28px !important;
3153
- border-radius: 50% !important;
3154
- display: inline-flex !important;
3155
- align-items: center !important;
3156
- justify-content: center !important;
3157
- flex-shrink: 0 !important;
3158
- }
3159
-
3160
- /* 移动端面板右上角“返回对话 / ✕ 收起”按钮:常驻右侧,醒目且易触达 */
3161
- .dsh-mobile-panel-close-btn {
3162
- margin-left: 8px !important;
3163
- flex: 0 0 auto !important;
3164
- height: 28px !important;
3165
- padding: 0 10px !important;
3166
- border-radius: 14px !important;
3167
- font-size: 12px !important;
3168
- font-weight: 600 !important;
3169
- background: #2563eb !important;
3170
- color: #ffffff !important;
3171
- border: none !important;
3172
- display: inline-flex !important;
3173
- align-items: center !important;
3174
- justify-content: center !important;
3175
- gap: 4px !important;
3176
- cursor: pointer !important;
3177
- user-select: none !important;
3178
- box-shadow: 0 2px 6px rgba(37, 99, 235, 0.28) !important;
3179
- white-space: nowrap !important;
3180
- transition: transform 0.1s, opacity 0.15s !important;
3181
- z-index: 10 !important;
3182
- }
3183
- .dsh-mobile-panel-close-btn:active {
3184
- transform: scale(0.95) !important;
3185
- opacity: 0.85 !important;
3186
- }
3187
-
3188
- /* 3.1 会话对话头部顶栏:移动端防挤压与空间释放优化(严格排除 .dsh-mobile-app-header) */
3189
- div[class*="_centerCol"] header,
3190
- header[class*="wSkVaW_header"] {
3191
- padding: 4px 16px 2px 16px !important;
3192
- position: relative !important;
3193
- overflow: visible !important;
3194
- }
3195
-
3196
- div[class*="wSkVaW_titleRow"],
3197
- div[class*="titleRow"] {
3198
- display: flex !important;
3199
- flex-direction: row !important;
3200
- align-items: center !important;
3201
- justify-content: space-between !important;
3202
- gap: 8px !important;
3203
- min-height: 32px !important;
3204
- width: 100% !important;
3205
- box-sizing: border-box !important;
3206
- }
3207
-
3208
- /* 移动端将原有嵌入在内容区的长面包屑标题隐藏(已统一提升至顶部导航栏正中),彻底释放第二行空间 */
3209
- nav[class*="wSkVaW_crumbs"],
3210
- nav[class*="crumbs"],
3211
- div[class*="wSkVaW_crumbs"],
3212
- div[class*="crumbs"],
3213
- [class*="wSkVaW_crumbs"] {
3214
- display: none !important;
3215
- }
3216
-
3217
- /* 子代理/智能体模式胶囊 (Actions):紧凑圆角胶囊 */
3218
- div[class*="wSkVaW_headerActions"],
3219
- div[class*="headerActions"] {
3220
- flex: 0 0 auto !important;
3221
- display: inline-flex !important;
3222
- align-items: center !important;
3223
- gap: 4px !important;
3224
- margin-left: 0 !important;
3225
- }
3226
-
3227
- button[class*="h8S2Va_trigger"],
3228
- button[class*="subagent"] {
3229
- min-height: 26px !important;
3230
- height: 26px !important;
3231
- padding: 2px 8px !important;
3232
- font-size: 11.5px !important;
3233
- line-height: 16px !important;
3234
- border-radius: 13px !important;
3235
- background: var(--dsw-alias-bg-layer-2, rgba(0, 0, 0, 0.04)) !important;
3236
- white-space: nowrap !important;
3237
- flex-shrink: 0 !important;
3238
- }
3239
-
3240
- /* Session Log 导出下载按钮 (Utilities):在移动端极简为 28px 圆形纯图标按钮,隐藏长文本,极大释放顶部空间 */
3241
- div[class*="wSkVaW_headerUtilities"],
3242
- div[class*="headerUtilities"] {
3243
- flex: 0 0 auto !important;
3244
- margin-left: 4px !important;
3245
- display: inline-flex !important;
3246
- align-items: center !important;
3247
- }
3248
-
3249
- button[class*="nL4_yW_sessionLogButton"],
3250
- button[class*="sessionLogButton"] {
3251
- min-width: 28px !important;
3252
- width: 28px !important;
3253
- height: 28px !important;
3254
- padding: 0 !important;
3255
- border-radius: 50% !important;
3256
- display: inline-flex !important;
3257
- align-items: center !important;
3258
- justify-content: center !important;
3259
- flex-shrink: 0 !important;
3260
- border: 1px solid var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.1)) !important;
3261
- background: var(--dsw-alias-bg-layer-2, rgba(0, 0, 0, 0.03)) !important;
3262
- color: var(--dsw-alias-label-secondary, #6b7280) !important;
3263
- margin-left: 0 !important;
3264
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03) !important;
3265
- }
3266
-
3267
- button[class*="nL4_yW_sessionLogButton"]:hover:not(:disabled),
3268
- button[class*="sessionLogButton"]:hover:not(:disabled) {
3269
- background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.06)) !important;
3270
- color: var(--dsw-alias-label-primary, #111827) !important;
3271
- }
3272
-
3273
- button[class*="nL4_yW_sessionLogButton"] span,
3274
- button[class*="sessionLogButton"] span {
3275
- display: none !important;
3276
- }
3277
-
3278
- button[class*="nL4_yW_sessionLogButton"] svg,
3279
- button[class*="sessionLogButton"] svg {
3280
- width: 13px !important;
3281
- height: 13px !important;
3282
- margin: 0 !important;
3283
- }
3284
-
3285
- /* 子代理展开菜单在移动端右对齐与宽度自适应 */
3286
- div[class*="h8S2Va_menu"] {
3287
- max-width: calc(100vw - 32px) !important;
3288
- left: auto !important;
3289
- right: 0 !important;
3290
- }
3291
-
3292
- /* 输入框底座:DeepSeek App 居中及底部固定 */
3293
- div[class*="wSkVaW_scrollBody"] {
3294
- padding-bottom: max(16px, env(safe-area-inset-bottom)) !important;
3295
- }
3296
-
3297
- /* 输入卡片:DeepSeek App 圆角大胶囊造型 */
3298
- div[class*="uV2eYG_card"] {
3299
- border-radius: 26px !important;
3300
- padding: 14px 16px 12px !important;
3301
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05) !important;
3302
- border: 1px solid rgba(0, 0, 0, 0.07) !important;
3303
- background: var(--dsw-alias-bg-layer-2, #f4f4f7) !important;
3304
- }
3305
-
3306
- /* 输入框底部工具栏:弹性自适应,彻底杜绝权限选择器(Full access)与模型选择器重叠碰撞 */
3307
- div[class*="uV2eYG_row"] {
3308
- display: flex !important;
3309
- align-items: center !important;
3310
- justify-content: space-between !important;
3311
- gap: 6px !important;
3312
- width: 100% !important;
3313
- padding: 2px 2px 4px !important;
3314
- box-sizing: border-box !important;
3315
- }
3316
-
3317
- div[class*="uV2eYG_tools"] {
3318
- display: flex !important;
3319
- align-items: center !important;
3320
- gap: 6px !important;
3321
- flex: 0 0 auto !important;
3322
- min-width: 0 !important;
3323
- }
3324
-
3325
- div[class*="uV2eYG_modes"] {
3326
- display: flex !important;
3327
- align-items: center !important;
3328
- gap: 4px !important;
3329
- flex: 0 0 auto !important;
3330
- min-width: 0 !important;
3331
- }
3332
-
3333
- button[class*="Sh0Q9G_trigger"] {
3334
- flex: 0 0 auto !important;
3335
- min-width: 0 !important;
3336
- }
3337
-
3338
- div[class*="uV2eYG_trailing"] {
3339
- display: flex !important;
3340
- align-items: center !important;
3341
- justify-content: flex-end !important;
3342
- gap: 6px !important;
3343
- flex: 1 1 auto !important;
3344
- min-width: 0 !important;
3345
- }
3346
-
3347
- div[class*="_7KE1Ra_root"] {
3348
- flex: 0 1 auto !important;
3349
- min-width: 0 !important;
3350
- max-width: 180px !important;
3351
- }
3352
-
3353
- button[class*="_7KE1Ra_trigger"] {
3354
- max-width: 100% !important;
3355
- min-width: 0 !important;
3356
- flex: 1 1 auto !important;
3357
- padding: 0 4px 0 6px !important;
3358
- }
3359
-
3360
- span[class*="_7KE1Ra_triggerLabel"] {
3361
- overflow: hidden !important;
3362
- text-overflow: ellipsis !important;
3363
- white-space: nowrap !important;
3364
- min-width: 0 !important;
3365
- }
3366
-
3367
- /* 4. 原生侧边栏抽屉化 (Drawer) */
3368
- div[class*="_sidebarCol"] {
3369
- position: fixed !important;
3370
- left: 0 !important;
3371
- top: 0 !important;
3372
- bottom: 0 !important;
3373
- height: 100dvh !important;
3374
- width: 290px !important;
3375
- max-width: 82vw !important;
3376
- z-index: 10000 !important;
3377
- background: var(--dsw-alias-bg-layer-1, #ffffff) !important;
3378
- transform: translateX(-105%);
3379
- transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1);
3380
- overflow-y: auto !important;
3381
- border-right: 1px solid rgba(0, 0, 0, 0.06) !important;
3382
- pointer-events: auto !important;
3383
- }
3384
- body.dsh-drawer-open div[class*="_sidebarCol"] {
3385
- transform: translateX(0) !important;
3386
- box-shadow: 4px 0 28px rgba(0, 0, 0, 0.25) !important;
3387
- pointer-events: auto !important;
3388
- }
3389
-
3390
- /* 抽屉内部:强制 100% 宽度,无论内部状态如何均正常展开并展示 DSH 自带的顶部收起侧边栏图标 */
3391
- body.dsh-drawer-open div[class*="hHd-Xa_root"] {
3392
- width: 100% !important;
3393
- max-width: 100% !important;
3394
- min-width: 100% !important;
3395
- display: flex !important;
3396
- flex-direction: column !important;
3397
- }
3398
- body.dsh-drawer-open div[class*="hHd-Xa_collapsed"] div[class*="hHd-Xa_regionArea"],
3399
- body.dsh-drawer-open div[class*="hHd-Xa_collapsed"] button[class*="hHd-Xa_newSession"],
3400
- body.dsh-drawer-open div[class*="hHd-Xa_collapsed"] div[class*="qDHVXG_root"] {
3401
- display: flex !important;
3402
- visibility: visible !important;
3403
- }
3404
- div[class*="hHd-Xa_logoRow"] {
3405
- display: flex !important;
3406
- align-items: center !important;
3407
- justify-content: space-between !important;
3408
- width: 100% !important;
3409
- padding: 10px 14px 6px 14px !important;
3410
- box-sizing: border-box !important;
3411
- }
3412
- div[class*="hHd-Xa_logoRow"] button[class*="hHd-Xa_toggle"] {
3413
- display: inline-flex !important;
3414
- align-items: center !important;
3415
- justify-content: center !important;
3416
- width: 32px !important;
3417
- height: 32px !important;
3418
- border-radius: 8px !important;
3419
- color: var(--dsw-alias-label-secondary, #6b7280) !important;
3420
- background: transparent !important;
3421
- border: none !important;
3422
- cursor: pointer !important;
3423
- margin-left: auto !important;
3424
- transition: background 0.15s, color 0.15s !important;
3425
- }
3426
- div[class*="hHd-Xa_logoRow"] button[class*="hHd-Xa_toggle"]:active {
3427
- background: var(--dsw-alias-bg-layer-2, rgba(0, 0, 0, 0.06)) !important;
3428
- color: var(--dsw-alias-label-primary, #111827) !important;
3429
- }
3430
-
3431
- /* 设置弹窗打开时解除抽屉隐藏限制 */
3432
- div[class*="_sidebarCol"]:has(div[class*="VOzbGW_overlay"]) {
3433
- transform: none !important;
3434
- width: 100vw !important;
3435
- max-width: 100vw !important;
3436
- background: transparent !important;
3437
- box-shadow: none !important;
3438
- pointer-events: none !important;
3439
- }
3440
-
3441
- /* 5. 半透明背景遮罩 */
3442
- .dsh-mobile-backdrop {
3443
- position: fixed;
3444
- inset: 0;
3445
- background: rgba(0, 0, 0, 0.4);
3446
- z-index: 9999;
3447
- display: none !important;
3448
- }
3449
- body.dsh-drawer-open .dsh-mobile-backdrop {
3450
- display: block !important;
3451
- pointer-events: auto !important;
3452
- }
3453
-
3454
- /* 6. 设置中心全自适应适配 */
3455
- div[class*="VOzbGW_overlay"] {
3456
- position: fixed !important;
3457
- inset: 0 !important;
3458
- width: 100vw !important;
3459
- height: 100dvh !important;
3460
- display: flex !important;
3461
- align-items: center !important;
3462
- justify-content: center !important;
3463
- background: rgba(0, 0, 0, 0.45) !important;
3464
- backdrop-filter: blur(4px) !important;
3465
- -webkit-backdrop-filter: blur(4px) !important;
3466
- z-index: 10002 !important;
3467
- padding: 10px !important;
3468
- box-sizing: border-box !important;
3469
- pointer-events: auto !important;
3470
- }
3471
- div[class*="VOzbGW_panel"] {
3472
- width: 100% !important;
3473
- max-width: 100% !important;
3474
- height: 92dvh !important;
3475
- max-height: 92dvh !important;
3476
- display: flex !important;
3477
- flex-direction: row !important;
3478
- border-radius: 18px !important;
3479
- overflow: hidden !important;
3480
- box-shadow: 0 20px 40px rgba(0, 0, 0, 0.25) !important;
3481
- background: var(--dsw-alias-bg-layer-1, #ffffff) !important;
3482
- }
3483
- nav[class*="VOzbGW_nav"] {
3484
- width: 78px !important;
3485
- min-width: 78px !important;
3486
- max-width: 78px !important;
3487
- padding: 10px 4px !important;
3488
- box-sizing: border-box !important;
3489
- border-right: 1px solid var(--dsw-alias-border-l2, #e5e7eb) !important;
3490
- display: flex !important;
3491
- flex-direction: column !important;
3492
- gap: 6px !important;
3493
- overflow-y: auto !important;
3494
- }
3495
- nav[class*="VOzbGW_nav"] button[class*="VOzbGW_navCell"],
3496
- button[class*="VOzbGW_navCell"] {
3497
- padding: 8px 2px !important;
3498
- display: flex !important;
3499
- flex-direction: column !important;
3500
- align-items: center !important;
3501
- justify-content: center !important;
3502
- text-align: center !important;
3503
- height: auto !important;
3504
- min-height: 48px !important;
3505
- gap: 4px !important;
3506
- border-radius: 10px !important;
3507
- }
3508
- span[class*="VOzbGW_navLabel"] {
3509
- font-size: 10.5px !important;
3510
- line-height: 1.2 !important;
3511
- white-space: normal !important;
3512
- word-break: break-all !important;
3513
- text-align: center !important;
3514
- }
3515
- div[class*="VOzbGW_content"] {
3516
- flex: 1 1 auto !important;
3517
- min-width: 0 !important;
3518
- width: calc(100% - 78px) !important;
3519
- max-width: calc(100% - 78px) !important;
3520
- display: flex !important;
3521
- flex-direction: column !important;
3522
- overflow: hidden !important;
3523
- }
3524
- div[class*="VOzbGW_options"] {
3525
- flex: 1 1 auto !important;
3526
- width: 100% !important;
3527
- max-width: 100% !important;
3528
- box-sizing: border-box !important;
3529
- padding: 0 14px 20px !important;
3530
- overflow-x: hidden !important;
3531
- overflow-y: auto !important;
3532
- -webkit-overflow-scrolling: touch !important;
3533
- }
3534
-
3535
- /* 设置中心选项行手机自适应(垂直流式,防文字单字折行) */
3536
- div[class*="VOzbGW_options"] div[class*="_row"] {
3537
- display: flex !important;
3538
- flex-direction: column !important;
3539
- align-items: stretch !important;
3540
- gap: 8px !important;
3541
- width: 100% !important;
3542
- padding: 12px 0 !important;
3543
- box-sizing: border-box !important;
3544
- }
3545
- div[class*="VOzbGW_options"] div[class*="_rowText"] {
3546
- width: 100% !important;
3547
- max-width: 100% !important;
3548
- }
3549
- div[class*="VOzbGW_options"] button[class*="_selector"],
3550
- div[class*="VOzbGW_options"] select,
3551
- div[class*="VOzbGW_options"] input {
3552
- width: 100% !important;
3553
- max-width: 100% !important;
3554
- box-sizing: border-box !important;
3555
- }
3556
-
3557
- /* 7. 代码块、表格与徽标自适应 */
3558
- pre, code, pre > code, table {
3559
- max-width: 100% !important;
3560
- overflow-x: auto !important;
3561
- -webkit-overflow-scrolling: touch !important;
3562
- font-size: 12.5px !important;
3563
- }
3564
-
3565
- /* 状态徽标与药丸按钮永不折字 */
3566
- span[style*="border-radius: 999"],
3567
- span[style*="border-radius:999"] {
3568
- white-space: nowrap !important;
3569
- flex-shrink: 0 !important;
3570
- min-width: max-content !important;
3571
- }
3572
-
3573
- /* 二维码与图片移动端弹性缩放 */
3574
- img[alt="QR"], img[src^="data:image"] {
3575
- max-width: 100% !important;
3576
- box-sizing: border-box !important;
3577
- }
3578
-
3579
- /* 8. 确保所有 Popover 弹出菜单、操作气泡、下拉框位于抽屉之上且支持触控交互 */
3580
- div[class*="_portal"],
3581
- div[class*="portal"],
3582
- div[class*="popup"],
3583
- div[class*="dropdown"],
3584
- div[class*="menu"],
3585
- div[role="menu"],
3586
- div[role="dialog"] {
3587
- z-index: 10005 !important;
3588
- pointer-events: auto !important;
3589
- }
3590
-
3591
- /* 9. 移动端侧边栏:会话与工作区三点操作按钮始终清晰可见且易于点击 */
3592
- div[class*="sessionRow"] span[class*="rowActions"],
3593
- div[class*="sessionRow"] button[class*="iconButton"],
3594
- div[class*="treeBody"] button[class*="iconButton"] {
3595
- opacity: 0.8 !important;
3596
- display: inline-flex !important;
3597
- visibility: visible !important;
3598
- pointer-events: auto !important;
3599
- }
3600
- div[class*="sessionRow"]:active {
3601
- background: var(--dsw-alias-bg-layer-2, #f3f4f6) !important;
3602
- }
3603
-
3604
- /* 全局 overlayLayer 绝不被染黑 */
3605
- div[class*="overlayLayer"],
3606
- div[class*="uV2eYG_overlayAnchor"] {
3607
- background: transparent !important;
3608
- pointer-events: none !important;
3609
- }
3610
- div[class*="overlayLayer"] > * {
3611
- pointer-events: auto !important;
3612
- }
3613
- }
3614
-
3615
- /* 远程工作区选择弹窗移动端/桌面端自适应样式 */
3616
- #dsh-remote-workspace-modal {
3617
- position: fixed !important;
3618
- inset: 0 !important;
3619
- z-index: 100000 !important;
3620
- background: rgba(0, 0, 0, 0.65) !important;
3621
- backdrop-filter: blur(5px) !important;
3622
- display: flex !important;
3623
- align-items: center !important;
3624
- justify-content: center !important;
3625
- padding: 16px !important;
3626
- box-sizing: border-box !important;
3627
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important;
3628
- color: var(--dsw-alias-label-primary, #111827) !important;
3629
- }
3630
-
3631
- .dsh-ws-dialog-card {
3632
- background: var(--dsw-alias-bg-layer-1, #ffffff) !important;
3633
- border: 1px solid var(--dsw-alias-border-l1, #e5e7eb) !important;
3634
- border-radius: 16px !important;
3635
- width: 100% !important;
3636
- max-width: 620px !important;
3637
- max-height: 88vh !important;
3638
- display: flex !important;
3639
- flex-direction: column !important;
3640
- box-shadow: 0 25px 35px -5px rgba(0,0,0,0.3), 0 12px 16px -5px rgba(0,0,0,0.2) !important;
3641
- overflow: hidden !important;
3642
- animation: dshModalFadeIn 0.2s ease-out !important;
3643
- }
3644
-
3645
- .dsh-ws-chips-scroll {
3646
- display: flex !important;
3647
- align-items: center !important;
3648
- gap: 6px !important;
3649
- overflow-x: auto !important;
3650
- white-space: nowrap !important;
3651
- scrollbar-width: none !important;
3652
- -ms-overflow-style: none !important;
3653
- -webkit-overflow-scrolling: touch !important;
3654
- padding: 2px 0 !important;
3655
- }
3656
- .dsh-ws-chips-scroll::-webkit-scrollbar {
3657
- display: none !important;
3658
- }
3659
-
3660
- @media (max-width: 640px) {
3661
- #dsh-remote-workspace-modal {
3662
- align-items: flex-end !important;
3663
- padding: 0 !important;
3664
- }
3665
-
3666
- .dsh-ws-dialog-card {
3667
- max-height: 92dvh !important;
3668
- height: 92dvh !important;
3669
- border-bottom-left-radius: 0 !important;
3670
- border-bottom-right-radius: 0 !important;
3671
- border-left: none !important;
3672
- border-right: none !important;
3673
- border-bottom: none !important;
3674
- max-width: 100vw !important;
3675
- width: 100vw !important;
3676
- margin: 0 !important;
3677
- animation: dshBottomSheetUp 0.25s cubic-bezier(0.16, 1, 0.3, 1) !important;
3678
- }
3679
-
3680
- .dsh-ws-drag-handle {
3681
- display: block !important;
3682
- }
3683
- }
3684
-
3685
- @keyframes dshModalFadeIn {
3686
- from { opacity: 0; transform: scale(0.96); }
3687
- to { opacity: 1; transform: scale(1); }
3688
- }
3689
-
3690
- @keyframes dshBottomSheetUp {
3691
- from { transform: translateY(100%); }
3692
- to { transform: translateY(0); }
3693
- }
3694
-
3695
- @media (min-width: 769px) {
3696
- .dsh-mobile-app-header,
3697
- .dsh-mobile-backdrop,
3698
- .dsh-mobile-panel-close-btn {
3699
- display: none !important;
3700
- }
3701
- }
3702
- `;
3045
+ style.textContent = MOBILE_STYLES_CSS;
3703
3046
  document.head.appendChild(style);
3704
3047
  }
3705
3048