@wenbin_wb/dsh-bridge 2.9.0 → 2.10.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/client.js CHANGED
@@ -832,6 +832,125 @@ var MOBILE_STYLES_CSS = `
832
832
  }
833
833
  `;
834
834
 
835
+ // client/unlock-manager.js
836
+ var SESSION_KEY = "dsh_admin_token";
837
+ var _token = "";
838
+ var _pendingOps = [];
839
+ var _onUnlocked = null;
840
+ function _readStorage() {
841
+ try {
842
+ return typeof window !== "undefined" ? window.sessionStorage.getItem(SESSION_KEY) : "";
843
+ } catch {
844
+ return "";
845
+ }
846
+ }
847
+ function _writeStorage(t) {
848
+ try {
849
+ if (typeof window === "undefined") return;
850
+ if (t) window.sessionStorage.setItem(SESSION_KEY, t);
851
+ else window.sessionStorage.removeItem(SESSION_KEY);
852
+ } catch {
853
+ }
854
+ }
855
+ function getAdminToken() {
856
+ if (_token) return _token;
857
+ const saved = _readStorage();
858
+ if (saved) {
859
+ _token = saved;
860
+ return saved;
861
+ }
862
+ return "";
863
+ }
864
+ function setAdminToken(t) {
865
+ _token = t || "";
866
+ _writeStorage(_token);
867
+ }
868
+ function clearAdminToken() {
869
+ setAdminToken("");
870
+ }
871
+ async function fetchLoopbackTokenOnce(force = true) {
872
+ if (typeof window === "undefined") return null;
873
+ if (!force) {
874
+ const existing = getAdminToken();
875
+ if (existing) return existing;
876
+ }
877
+ const candidates = [
878
+ "/__dsh_bridge__/loopback-token",
879
+ "http://127.0.0.1:3082/__dsh_bridge__/loopback-token",
880
+ "http://localhost:3082/__dsh_bridge__/loopback-token"
881
+ ];
882
+ for (const url of [...new Set(candidates)]) {
883
+ try {
884
+ const res = await fetch(url, { method: "POST" });
885
+ if (res.ok) {
886
+ const data = await res.json();
887
+ if (data?.ok && data.adminToken) {
888
+ setAdminToken(data.adminToken);
889
+ return data.adminToken;
890
+ }
891
+ }
892
+ } catch {
893
+ }
894
+ }
895
+ return null;
896
+ }
897
+ function queuePendingOperation(retry) {
898
+ _pendingOps.push(retry);
899
+ }
900
+ async function replayPending() {
901
+ if (_pendingOps.length === 0) return;
902
+ const ops = _pendingOps;
903
+ _pendingOps = [];
904
+ for (const retry of ops) {
905
+ try {
906
+ await retry();
907
+ } catch {
908
+ }
909
+ }
910
+ }
911
+ function onUnlocked(cb) {
912
+ if (!_onUnlocked) _onUnlocked = [];
913
+ _onUnlocked.push(cb);
914
+ return () => {
915
+ _onUnlocked = _onUnlocked.filter((f) => f !== cb);
916
+ };
917
+ }
918
+ function _emitUnlocked() {
919
+ if (_onUnlocked) {
920
+ for (const cb of _onUnlocked) {
921
+ try {
922
+ cb();
923
+ } catch {
924
+ }
925
+ }
926
+ }
927
+ }
928
+ async function unlockAdmin(rpcCall, password) {
929
+ try {
930
+ const res = await rpcCall("authAdminUnlock", { password });
931
+ if (res?.ok) {
932
+ const token = res.value?.adminToken || "";
933
+ setAdminToken(token);
934
+ _emitUnlocked();
935
+ await replayPending();
936
+ return { ok: true };
937
+ }
938
+ return { ok: false, error: res?.error?.message || "\u7BA1\u7406\u5458\u5BC6\u7801\u9519\u8BEF" };
939
+ } catch (err) {
940
+ const msg = String(err?.message || err || "");
941
+ if (msg.includes("401") || msg.includes("transport failure") || msg.includes("unauthorized")) {
942
+ if (typeof window !== "undefined" && typeof window.location?.reload === "function") {
943
+ try {
944
+ window.sessionStorage.setItem("dsh_access_expired_reloaded", "1");
945
+ } catch {
946
+ }
947
+ window.location.reload();
948
+ }
949
+ }
950
+ return { ok: false, error: err?.message || "\u89E3\u9501\u8BF7\u6C42\u5931\u8D25" };
951
+ }
952
+ }
953
+
835
954
  // lib/bridge-rpc-constants.js
836
955
  var BRIDGE_RPC_CHANNEL = "/dsh-bridge";
837
956
  var BRIDGE_ENDPOINTS = {
@@ -900,37 +1019,35 @@ if (typeof window !== "undefined") {
900
1019
  };
901
1020
  }
902
1021
  }
903
- var _globalAdminToken = "";
904
- function setGlobalAdminToken(t) {
905
- _globalAdminToken = t || "";
906
- if (typeof window !== "undefined") {
907
- try {
908
- if (t) sessionStorage.setItem("dsh_admin_token", t);
909
- else sessionStorage.removeItem("dsh_admin_token");
910
- } catch {
911
- }
912
- }
913
- }
914
- function getGlobalAdminToken() {
915
- if (_globalAdminToken) return _globalAdminToken;
916
- if (typeof window !== "undefined") {
917
- try {
918
- const saved = sessionStorage.getItem("dsh_admin_token");
919
- if (saved) {
920
- _globalAdminToken = saved;
921
- return saved;
922
- }
923
- } catch {
924
- }
925
- }
926
- return "";
927
- }
928
1022
  function isLocalEnvironment() {
929
1023
  if (typeof window === "undefined") return true;
930
1024
  const host = window.location.hostname || "";
931
1025
  const proto = window.location.protocol || "";
932
1026
  return host === "127.0.0.1" || host === "localhost" || host === "::1" || host === "" || proto === "file:" || proto === "vscode-webview:" || proto === "app:" || typeof window.__DSH_ELECTRON__ !== "undefined" || typeof navigator !== "undefined" && navigator.userAgent && navigator.userAgent.includes("Electron");
933
1027
  }
1028
+ var ACCESS_EXPIRED_FLAG = "dsh_access_expired_reloaded";
1029
+ function isAccessSessionFailure(err) {
1030
+ const msg = String(err?.message || err || "");
1031
+ return msg.includes("401") || msg.includes("transport failure") || msg.includes("unauthorized");
1032
+ }
1033
+ function reloadToLoginPage() {
1034
+ try {
1035
+ if (sessionStorage.getItem(ACCESS_EXPIRED_FLAG)) {
1036
+ sessionStorage.removeItem(ACCESS_EXPIRED_FLAG);
1037
+ return false;
1038
+ }
1039
+ sessionStorage.setItem(ACCESS_EXPIRED_FLAG, "1");
1040
+ } catch {
1041
+ }
1042
+ window.location.reload();
1043
+ return true;
1044
+ }
1045
+ function clearAccessExpiredFlag() {
1046
+ try {
1047
+ sessionStorage.removeItem(ACCESS_EXPIRED_FLAG);
1048
+ } catch {
1049
+ }
1050
+ }
934
1051
  var GITHUB_URL = "https://github.com/wenbin-wb/dsh-bridge";
935
1052
  var RELEASES_URL = "https://github.com/wenbin-wb/dsh-bridge/releases";
936
1053
  var ISSUES_URL = "https://github.com/wenbin-wb/dsh-bridge/issues/new";
@@ -1568,6 +1685,7 @@ var AccessAuthCard = React.memo(function AccessAuthCard2({ auth, rpcCall, onUpda
1568
1685
  const [mode, setMode] = React.useState(auth?.mode ?? "token_and_password");
1569
1686
  const [scope, setScope] = React.useState(auth?.scope ?? "all");
1570
1687
  const [adminPolicy, setAdminPolicy] = React.useState(auth?.adminPolicy ?? "password_unlock");
1688
+ const [adminProtection, setAdminProtection] = React.useState(auth?.adminProtection ?? true);
1571
1689
  const [accessPassword, setAccessPassword] = React.useState("");
1572
1690
  const [showAccessPassword, setShowAccessPassword] = React.useState(false);
1573
1691
  const [savingAccess, setSavingAccess] = React.useState(false);
@@ -1579,12 +1697,21 @@ var AccessAuthCard = React.memo(function AccessAuthCard2({ auth, rpcCall, onUpda
1579
1697
  const [saveAdminSuccess, setSaveAdminSuccess] = React.useState(false);
1580
1698
  const [msgAdmin, setMsgAdmin] = React.useState(null);
1581
1699
  const [topMsg, setTopMsg] = React.useState(null);
1700
+ React.useEffect(() => {
1701
+ const off = onUnlocked(() => {
1702
+ setTopMsg(null);
1703
+ setMsgAccess(null);
1704
+ setMsgAdmin(null);
1705
+ });
1706
+ return off;
1707
+ }, []);
1582
1708
  React.useEffect(() => {
1583
1709
  if (auth) {
1584
1710
  setEnabled(auth.enabled ?? false);
1585
1711
  setMode(auth.mode ?? "token_and_password");
1586
1712
  setScope(auth.scope ?? "all");
1587
1713
  setAdminPolicy(auth.adminPolicy ?? "password_unlock");
1714
+ setAdminProtection(auth.adminProtection ?? true);
1588
1715
  }
1589
1716
  }, [auth]);
1590
1717
  const handleToggleEnabled = async () => {
@@ -1594,13 +1721,27 @@ var AccessAuthCard = React.memo(function AccessAuthCard2({ auth, rpcCall, onUpda
1594
1721
  try {
1595
1722
  const res = await rpcCall(BRIDGE_ENDPOINTS.authUpdateConfig, { enabled: next });
1596
1723
  if (!res?.ok) throw new Error(res?.error?.message || "\u66F4\u65B0\u5931\u8D25");
1597
- setTopMsg({ ok: true, text: next ? "\u2713 \u8BBF\u95EE\u5B89\u5168\u8BA4\u8BC1\u5DF2\u5F00\u542F\uFF08\u73B0\u6709\u767B\u5F55\u6001\u5DF2\u5237\u65B0\uFF09" : "\u2713 \u8BBF\u95EE\u5B89\u5168\u8BA4\u8BC1\u5DF2\u5173\u95ED" });
1724
+ setTopMsg({ ok: true, text: next ? "\u2713 \u8BBF\u95EE\u5B89\u5168\u8BA4\u8BC1\u5DF2\u5F00\u542F\uFF08\u73B0\u6709\u767B\u5F55\u6001\u5DF2\u5237\u65B0\uFF09" : "\u2713 \u8BBF\u95EE\u5B89\u5168\u8BA4\u8BC1\u5DF2\u5173\u95ED\uFF08\u8BBF\u95EE\u514D\u5BC6\uFF0C\u7BA1\u7406\u4FDD\u62A4\u4E0D\u53D7\u5F71\u54CD\uFF09" });
1598
1725
  onUpdate?.();
1599
1726
  } catch (e) {
1600
1727
  setEnabled(prev);
1601
1728
  setTopMsg({ ok: false, text: e.message || "\u66F4\u65B0\u5931\u8D25" });
1602
1729
  }
1603
1730
  };
1731
+ const handleToggleAdminProtection = async () => {
1732
+ const prev = adminProtection;
1733
+ const next = !adminProtection;
1734
+ setAdminProtection(next);
1735
+ try {
1736
+ const res = await rpcCall(BRIDGE_ENDPOINTS.authUpdateConfig, { adminProtection: next });
1737
+ if (!res?.ok) throw new Error(res?.error?.message || "\u66F4\u65B0\u5931\u8D25");
1738
+ setTopMsg({ ok: true, text: next ? "\u2713 \u7BA1\u7406\u4FDD\u62A4\u5DF2\u5F00\u542F\uFF08\u4FEE\u6539\u914D\u7F6E\u9700\u7BA1\u7406\u5BC6\u7801\uFF09" : "\u2713 \u7BA1\u7406\u4FDD\u62A4\u5DF2\u5173\u95ED\uFF08\u4FEE\u6539\u914D\u7F6E\u514D\u5BC6\uFF0C\u8BF7\u8C28\u614E\uFF09" });
1739
+ onUpdate?.();
1740
+ } catch (e) {
1741
+ setAdminProtection(prev);
1742
+ setTopMsg({ ok: false, text: e.message || "\u66F4\u65B0\u5931\u8D25" });
1743
+ }
1744
+ };
1604
1745
  const handleChangeMode = async (m) => {
1605
1746
  const prev = mode;
1606
1747
  setMode(m);
@@ -1785,13 +1926,13 @@ var AccessAuthCard = React.memo(function AccessAuthCard2({ auth, rpcCall, onUpda
1785
1926
  }
1786
1927
  }, topMsg.text)
1787
1928
  ),
1788
- enabled && React.createElement(
1929
+ React.createElement(
1789
1930
  React.Fragment,
1790
1931
  null,
1791
1932
  // =========================================================================
1792
1933
  // ---- 第一道防线:外部访问门禁(控制谁能进入 Web 界面使用 AI) ----
1793
1934
  // =========================================================================
1794
- React.createElement(
1935
+ enabled && React.createElement(
1795
1936
  "div",
1796
1937
  { style: s.card },
1797
1938
  React.createElement(
@@ -1976,6 +2117,22 @@ var AccessAuthCard = React.memo(function AccessAuthCard2({ auth, rpcCall, onUpda
1976
2117
  "div",
1977
2118
  { style: { ...s.muted, marginTop: 3 } },
1978
2119
  "\u9501\u5B9A\u6574\u4E2A\u63D2\u4EF6\u8BBE\u7F6E\u540E\u53F0\uFF08\u5305\u542B\u5C40\u57DF\u7F51\u3001\u516C\u7F51\u96A7\u9053\u3001IM \u673A\u5668\u4EBA\u5BC6\u94A5\u4E0E\u5B89\u5168\u8BBE\u7F6E\uFF09\uFF0C\u9632\u6B62\u4ED6\u4EBA\u968F\u610F\u7BE1\u6539\u914D\u7F6E"
2120
+ ),
2121
+ React.createElement(
2122
+ "div",
2123
+ {
2124
+ style: { display: "flex", alignItems: "center", justifyContent: "space-between", flexWrap: "wrap", gap: 8, marginTop: 10, paddingTop: 10, borderTop: "1px solid var(--dsw-alias-border-l2,#e5e7eb)" }
2125
+ },
2126
+ React.createElement(
2127
+ "div",
2128
+ { style: { fontSize: 12, fontWeight: 600, color: "var(--dsw-alias-label-primary,currentColor)" } },
2129
+ adminProtection ? "\u{1F6E1}\uFE0F \u7BA1\u7406\u4FDD\u62A4\u5DF2\u5F00\u542F\uFF08\u4FEE\u6539\u914D\u7F6E\u9700\u7BA1\u7406\u5BC6\u7801\uFF09" : "\u26A0\uFE0F \u7BA1\u7406\u4FDD\u62A4\u5DF2\u5173\u95ED\uFF08\u4FEE\u6539\u914D\u7F6E\u514D\u5BC6\uFF09"
2130
+ ),
2131
+ React.createElement("button", {
2132
+ type: "button",
2133
+ style: { ...adminProtection ? s.btnGhost : s.btnPri, height: 28, fontSize: 12, padding: "0 12px" },
2134
+ onClick: handleToggleAdminProtection
2135
+ }, adminProtection ? "\u5173\u95ED\u7BA1\u7406\u4FDD\u62A4" : "\u5F00\u542F\u7BA1\u7406\u4FDD\u62A4")
1979
2136
  )
1980
2137
  ),
1981
2138
  // 管理员密码设置
@@ -3284,6 +3441,25 @@ function VersionBanner({ rpcCall }) {
3284
3441
  hasUpdate && React.createElement("span", { style: { fontWeight: 600, fontSize: 11 } }, `\u2794 v${info.latest}`),
3285
3442
  info?.error && React.createElement("span", { style: { color: "var(--dsw-alias-state-warn-primary,#d97706)", fontSize: 11 } }, "(\u7F51\u7EDC\u8D85\u65F6)")
3286
3443
  ),
3444
+ // DSH 宿主版本标签
3445
+ info?.dshVersion && React.createElement(
3446
+ "span",
3447
+ {
3448
+ style: {
3449
+ ...s.tag,
3450
+ background: "var(--dsw-alias-bg-layer-2,#f3f4f6)",
3451
+ color: "var(--dsw-alias-label-tertiary,#6b7280)",
3452
+ padding: "3px 10px",
3453
+ fontSize: 12,
3454
+ fontWeight: 500,
3455
+ display: "inline-flex",
3456
+ alignItems: "center",
3457
+ gap: 5
3458
+ }
3459
+ },
3460
+ React.createElement("span", { style: { opacity: 0.75, fontSize: 11, fontWeight: 400 } }, "DSH"),
3461
+ `v${info.dshVersion}`
3462
+ ),
3287
3463
  // 刷新检查按钮
3288
3464
  React.createElement(
3289
3465
  "button",
@@ -3556,7 +3732,6 @@ function BridgePanel({ rpcCall }) {
3556
3732
  const [platforms, setPlatforms] = React.useState(null);
3557
3733
  const [selectedPlatform, setSelectedPlatform] = React.useState("wechat");
3558
3734
  const isLocalhost = typeof window === "undefined" || (!window.location.hostname || window.location.hostname === "127.0.0.1" || window.location.hostname === "localhost" || window.location.hostname === "::1" || window.location.hostname === "" || window.location.protocol === "file:" || window.location.protocol === "vscode-webview:" || window.location.protocol === "app:" || window.location.hostname.endsWith(".local"));
3559
- const [adminToken, setAdminToken] = React.useState("");
3560
3735
  const [adminUnlocked, setAdminUnlocked] = React.useState(false);
3561
3736
  const [unlockPassword, setUnlockPassword] = React.useState("");
3562
3737
  const [unlockErr, setUnlockErr] = React.useState(null);
@@ -3565,39 +3740,27 @@ function BridgePanel({ rpcCall }) {
3565
3740
  const [showUnlockModal, setShowUnlockModal] = React.useState(false);
3566
3741
  const fetchLoopbackToken = React.useCallback(async () => {
3567
3742
  if (!isLocalhost) return null;
3568
- const proxyPort = status?.proxy?.port || 3082;
3569
- const candidateUrls = [
3570
- "/__dsh_bridge__/loopback-token",
3571
- `http://127.0.0.1:${proxyPort}/__dsh_bridge__/loopback-token`,
3572
- `http://localhost:${proxyPort}/__dsh_bridge__/loopback-token`,
3573
- "http://127.0.0.1:3082/__dsh_bridge__/loopback-token"
3574
- ];
3575
- const uniqueUrls = [...new Set(candidateUrls)];
3576
- for (const url of uniqueUrls) {
3577
- try {
3578
- const res = await fetch(url, { method: "POST" });
3579
- if (res.ok) {
3580
- const data = await res.json();
3581
- if (data?.ok && data.adminToken) {
3582
- setAdminToken(data.adminToken);
3583
- setGlobalAdminToken(data.adminToken);
3584
- setAdminUnlocked(true);
3585
- return data.adminToken;
3586
- }
3587
- }
3588
- } catch {
3589
- }
3743
+ const token = await fetchLoopbackTokenOnce();
3744
+ if (token) {
3745
+ setAdminUnlocked(true);
3590
3746
  }
3591
- return null;
3592
- }, [isLocalhost, status?.proxy?.port]);
3747
+ return token;
3748
+ }, [isLocalhost]);
3593
3749
  React.useEffect(() => {
3594
3750
  if (isLocalhost && !adminUnlocked) {
3595
3751
  fetchLoopbackToken();
3596
3752
  }
3597
3753
  }, [isLocalhost, adminUnlocked, fetchLoopbackToken]);
3598
- const pendingRetryRef = React.useRef(null);
3754
+ const handleAccessSessionExpired = React.useCallback(() => {
3755
+ setAdminUnlocked(false);
3756
+ clearAdminToken();
3757
+ const reloaded = reloadToLoginPage();
3758
+ if (!reloaded) {
3759
+ setErr("\u8BBF\u95EE\u4F1A\u8BDD\u5DF2\u5931\u6548\uFF0C\u8BF7\u5237\u65B0\u9875\u9762\u91CD\u65B0\u767B\u5F55");
3760
+ }
3761
+ }, []);
3599
3762
  const authRpcCall = React.useCallback(async (endpoint, payload = {}, signal) => {
3600
- let token = adminToken || getGlobalAdminToken();
3763
+ let token = getAdminToken();
3601
3764
  if (isLocalhost && !token) {
3602
3765
  token = await fetchLoopbackToken();
3603
3766
  }
@@ -3606,51 +3769,60 @@ function BridgePanel({ rpcCall }) {
3606
3769
  ...token ? { adminToken: token } : {},
3607
3770
  ...isLocalhost ? { isLocalhost: true } : {}
3608
3771
  };
3609
- const res = await rpcCall(endpoint, enriched, signal);
3772
+ let res;
3773
+ try {
3774
+ res = await rpcCall(endpoint, enriched, signal);
3775
+ } catch (e) {
3776
+ if (isAccessSessionFailure(e)) {
3777
+ handleAccessSessionExpired();
3778
+ }
3779
+ throw e;
3780
+ }
3610
3781
  if (res?.ok === false) {
3611
3782
  const msg = res?.error?.message || "";
3612
3783
  if (msg.includes("\u7BA1\u7406\u5458\u6743\u9650") || msg.includes("\u7BA1\u7406\u5BC6\u7801\u89E3\u9501")) {
3613
- pendingRetryRef.current = { endpoint, payload: enriched };
3784
+ queuePendingOperation(async () => {
3785
+ let fresh = getAdminToken();
3786
+ if (!fresh && isLocalhost) fresh = await fetchLoopbackToken();
3787
+ if (!fresh) return;
3788
+ const retry = await rpcCall(endpoint, { ...payload, adminToken: fresh, ...isLocalhost ? { isLocalhost: true } : {} }, signal);
3789
+ if (retry?.ok) {
3790
+ setStatus(retry.value);
3791
+ setErr(null);
3792
+ }
3793
+ });
3614
3794
  setUnlockErr(msg);
3615
3795
  setShowUnlockModal(true);
3616
3796
  }
3617
3797
  }
3618
3798
  return res;
3619
- }, [rpcCall, adminToken, isLocalhost, fetchLoopbackToken]);
3799
+ }, [rpcCall, isLocalhost, fetchLoopbackToken, handleAccessSessionExpired]);
3620
3800
  const handleUnlockAdmin = React.useCallback(async (e) => {
3621
3801
  e?.preventDefault?.();
3622
3802
  setUnlocking(true);
3623
3803
  setUnlockErr(null);
3624
- try {
3625
- const res = await rpcCall(BRIDGE_ENDPOINTS.authAdminUnlock, { password: unlockPassword });
3626
- if (res?.ok) {
3627
- const token = res.value?.adminToken || "";
3628
- setAdminToken(token);
3629
- setGlobalAdminToken(token);
3630
- setAdminUnlocked(true);
3631
- setUnlockPassword("");
3632
- setShowUnlockModal(false);
3633
- setErr(null);
3634
- } else {
3635
- setUnlockErr(res?.error?.message || "\u7BA1\u7406\u5458\u5BC6\u7801\u9519\u8BEF");
3636
- }
3637
- } catch (err2) {
3638
- setUnlockErr(err2.message || "\u89E3\u9501\u8BF7\u6C42\u5931\u8D25");
3639
- } finally {
3640
- setUnlocking(false);
3804
+ const res = await unlockAdmin(rpcCall, unlockPassword);
3805
+ if (res.ok) {
3806
+ setAdminUnlocked(true);
3807
+ setUnlockPassword("");
3808
+ setShowUnlockModal(false);
3809
+ setErr(null);
3810
+ } else {
3811
+ setUnlockErr(res.error);
3641
3812
  }
3813
+ setUnlocking(false);
3642
3814
  }, [rpcCall, unlockPassword]);
3643
3815
  const handleLockAdmin = React.useCallback(async () => {
3644
3816
  try {
3645
- if (adminToken) {
3646
- await rpcCall(BRIDGE_ENDPOINTS.authAdminLock, { adminToken });
3817
+ const t = getAdminToken();
3818
+ if (t) {
3819
+ await rpcCall(BRIDGE_ENDPOINTS.authAdminLock, { adminToken: t });
3647
3820
  }
3648
3821
  } catch {
3649
3822
  }
3650
- setAdminToken("");
3651
- setGlobalAdminToken("");
3823
+ clearAdminToken();
3652
3824
  setAdminUnlocked(false);
3653
- }, [rpcCall, adminToken]);
3825
+ }, [rpcCall]);
3654
3826
  const loadInFlightRef = React.useRef(false);
3655
3827
  const loadSeqRef = React.useRef(0);
3656
3828
  const load = React.useCallback(async (quiet = false) => {
@@ -3663,12 +3835,19 @@ function BridgePanel({ rpcCall }) {
3663
3835
  if (!r?.ok) throw new Error(r?.error?.message ?? "RPC failed");
3664
3836
  setStatus(r.value);
3665
3837
  if (!quiet) setErr(null);
3838
+ clearAccessExpiredFlag();
3666
3839
  } catch (e) {
3667
- if (currentSeq === loadSeqRef.current) setErr(e.message);
3840
+ if (currentSeq === loadSeqRef.current) {
3841
+ if (isAccessSessionFailure(e)) {
3842
+ handleAccessSessionExpired();
3843
+ } else {
3844
+ setErr(e.message);
3845
+ }
3846
+ }
3668
3847
  } finally {
3669
3848
  loadInFlightRef.current = false;
3670
3849
  }
3671
- }, [authRpcCall]);
3850
+ }, [authRpcCall, handleAccessSessionExpired]);
3672
3851
  const pollPlatformsSeqRef = React.useRef(0);
3673
3852
  React.useEffect(() => {
3674
3853
  let alive = true;
@@ -3687,38 +3866,20 @@ function BridgePanel({ rpcCall }) {
3687
3866
  inFlight = false;
3688
3867
  }
3689
3868
  };
3869
+ if (!adminUnlocked && !isLocalhost) return;
3690
3870
  poll();
3691
3871
  const t = setInterval(poll, 4e3);
3692
3872
  return () => {
3693
3873
  alive = false;
3694
3874
  clearInterval(t);
3695
3875
  };
3696
- }, [authRpcCall]);
3876
+ }, [authRpcCall, adminUnlocked, isLocalhost]);
3697
3877
  React.useEffect(() => {
3698
3878
  load();
3879
+ if (!adminUnlocked && !isLocalhost) return;
3699
3880
  const t = setInterval(() => load(true), 3e3);
3700
3881
  return () => clearInterval(t);
3701
- }, [load]);
3702
- React.useEffect(() => {
3703
- if (!adminUnlocked) return;
3704
- const pending = pendingRetryRef.current;
3705
- if (!pending) return;
3706
- pendingRetryRef.current = null;
3707
- const token = adminToken || getGlobalAdminToken();
3708
- (async () => {
3709
- try {
3710
- const r = await rpcCall(pending.endpoint, { ...pending.payload, adminToken: token });
3711
- if (r?.ok) {
3712
- if (r.value) setStatus(r.value);
3713
- setErr(null);
3714
- } else {
3715
- setErr(r?.error?.message || "\u521A\u624D\u7684\u64CD\u4F5C\u91CD\u8BD5\u5931\u8D25\uFF0C\u8BF7\u624B\u52A8\u91CD\u8BD5");
3716
- }
3717
- } catch (e) {
3718
- setErr(e.message || "\u521A\u624D\u7684\u64CD\u4F5C\u91CD\u8BD5\u5931\u8D25\uFF0C\u8BF7\u624B\u52A8\u91CD\u8BD5");
3719
- }
3720
- })();
3721
- }, [adminUnlocked, adminToken, rpcCall]);
3882
+ }, [load, adminUnlocked, isLocalhost]);
3722
3883
  const act = React.useCallback(async (endpoint, payload) => {
3723
3884
  try {
3724
3885
  const r = await authRpcCall(endpoint, payload ?? {});
@@ -3933,7 +4094,7 @@ function BridgePanel({ rpcCall }) {
3933
4094
  }
3934
4095
  const auth = status?.auth;
3935
4096
  const policy = auth?.adminPolicy ?? "password_unlock";
3936
- const isLocked = !isLocalhost && auth?.enabled && policy !== "open" && !adminUnlocked;
4097
+ const isLocked = !isLocalhost && auth?.adminProtection !== false && policy !== "open" && !adminUnlocked;
3937
4098
  if (isLocked) {
3938
4099
  return React.createElement(
3939
4100
  "div",
@@ -4227,6 +4388,8 @@ function injectMobileStyles() {
4227
4388
  if (document.getElementById("dsh-bridge-mobile-styles")) return;
4228
4389
  const style = document.createElement("style");
4229
4390
  style.id = "dsh-bridge-mobile-styles";
4391
+ style.dataset.plugin = "@wenbin_wb/dsh-bridge";
4392
+ style.dataset.pluginCss = "@wenbin_wb/dsh-bridge/mobile-styles";
4230
4393
  style.textContent = MOBILE_STYLES_CSS;
4231
4394
  document.head.appendChild(style);
4232
4395
  }
@@ -4509,6 +4672,11 @@ function showRemoteWorkspaceDialog(rpcCall, onWorkspaceAdded, clientCtx, onPicke
4509
4672
  let isSubmitting = false;
4510
4673
  let statusMessage = null;
4511
4674
  let isErrorMessage = false;
4675
+ let needUnlock = false;
4676
+ let unlockable = true;
4677
+ let unlockInput = "";
4678
+ let unlockErr = null;
4679
+ let unlocking = false;
4512
4680
  function closeModal() {
4513
4681
  document.removeEventListener("keydown", handleKeydown);
4514
4682
  overlay.style.opacity = "0";
@@ -4551,6 +4719,20 @@ function showRemoteWorkspaceDialog(rpcCall, onWorkspaceAdded, clientCtx, onPicke
4551
4719
  <button id="dsh-ws-close-btn" style="border: none; background: none; font-size: 18px; cursor: pointer; color: var(--dsw-alias-label-tertiary, #9ca3af); padding: 4px 8px; border-radius: 6px; line-height: 1; flex-shrink: 0;">\u2715</button>
4552
4720
  </div>
4553
4721
 
4722
+ <!-- \u7BA1\u7406\u6743\u9650\u89E3\u9501\u5757\uFF08\u8FDC\u7A0B\u8BBF\u95EE\u9700\u8981\u7BA1\u7406\u5BC6\u7801\uFF1Blocal_only \u7B56\u7565\u4E0B\u4E0D\u663E\u793A\uFF0C\u4EC5\u63D0\u793A\uFF09 -->
4723
+ ${needUnlock && unlockable ? `
4724
+ <div style="padding: 14px 16px; background: var(--dsw-alias-state-warn-bg, #fffbeb); border-bottom: 1px solid var(--dsw-alias-state-warn-border, #fde68a); flex-shrink: 0;">
4725
+ <div style="font-size: 12px; font-weight: 600; color: var(--dsw-alias-state-warn-primary, #92400e); margin-bottom: 6px;">\u{1F512} \u6B64\u64CD\u4F5C\u9700\u8981\u7BA1\u7406\u5458\u6743\u9650</div>
4726
+ <div style="font-size: 11px; color: var(--dsw-alias-label-secondary, #6b7280); margin-bottom: 8px; line-height: 1.5;">\u8FDC\u7A0B\u8BBF\u95EE\u65F6\u6D4F\u89C8/\u6DFB\u52A0\u5DE5\u4F5C\u533A\u9700\u8F93\u5165\u540E\u53F0\u7BA1\u7406\u5BC6\u7801\u89E3\u9501\uFF08\u4E0E\u8BBF\u95EE\u5BC6\u7801\u4E0D\u540C\uFF09\u3002</div>
4727
+ <form id="dsh-ws-unlock-form" style="display: flex; gap: 8px;">
4728
+ <input id="dsh-ws-unlock-input" type="password" placeholder="\u8BF7\u8F93\u5165\u540E\u53F0\u7BA1\u7406\u5BC6\u7801" value="${escapeHtml(unlockInput)}"
4729
+ style="flex: 1; font: inherit; font-size: 13px; padding: 7px 10px; border-radius: 8px; border: 1px solid var(--dsw-alias-border-l2, #d1d5db); background: var(--dsw-alias-bg-layer-1, #fff); color: var(--dsw-alias-label-primary, currentColor); outline: none; box-sizing: border-box;" />
4730
+ <button type="submit" style="border: none; background: var(--dsw-alias-brand-primary, #4f6ef7); color: #fff; border-radius: 8px; padding: 0 14px; font-size: 12px; font-weight: 600; cursor: pointer; flex-shrink: 0;" ${unlocking ? "disabled" : ""}>${unlocking ? "\u89E3\u9501\u4E2D\u2026" : "\u89E3\u9501"}</button>
4731
+ </form>
4732
+ ${unlockErr ? `<div style="font-size: 11px; color: var(--dsw-alias-state-error-primary, #dc2626); margin-top: 6px;">${escapeHtml(unlockErr)}</div>` : ""}
4733
+ </div>
4734
+ ` : ""}
4735
+
4554
4736
  <div style="padding: 12px 16px; overflow-y: auto; flex: 1; display: flex; flex-direction: column; gap: 10px;">
4555
4737
  <!-- \u63D0\u793A\u4FE1\u606F\u6A2A\u5E45 -->
4556
4738
  ${statusMessage ? `
@@ -4711,6 +4893,31 @@ function showRemoteWorkspaceDialog(rpcCall, onWorkspaceAdded, clientCtx, onPicke
4711
4893
  `;
4712
4894
  modal.querySelector("#dsh-ws-close-btn")?.addEventListener("click", closeModal);
4713
4895
  modal.querySelector("#dsh-ws-cancel-btn")?.addEventListener("click", closeModal);
4896
+ const unlockForm = modal.querySelector("#dsh-ws-unlock-form");
4897
+ if (unlockForm) {
4898
+ unlockForm.addEventListener("submit", async (e) => {
4899
+ e.preventDefault();
4900
+ if (unlocking) return;
4901
+ const input = modal.querySelector("#dsh-ws-unlock-input");
4902
+ const pwd = input?.value || "";
4903
+ if (!pwd) return;
4904
+ unlocking = true;
4905
+ unlockErr = null;
4906
+ render();
4907
+ const res = await unlockAdmin(rpcCall, pwd);
4908
+ if (res.ok) {
4909
+ needUnlock = false;
4910
+ unlockInput = "";
4911
+ statusMessage = null;
4912
+ isErrorMessage = false;
4913
+ await loadDirectory(currentPath);
4914
+ } else {
4915
+ unlockErr = res.error || "\u89E3\u9501\u5931\u8D25";
4916
+ }
4917
+ unlocking = false;
4918
+ render();
4919
+ });
4920
+ }
4714
4921
  modal.querySelectorAll(".dsh-ws-crumb-btn").forEach((btn) => {
4715
4922
  btn.addEventListener("click", () => {
4716
4923
  const p = btn.getAttribute("data-path");
@@ -4800,25 +5007,40 @@ function showRemoteWorkspaceDialog(rpcCall, onWorkspaceAdded, clientCtx, onPicke
4800
5007
  }
4801
5008
  }
4802
5009
  async function authRpc(endpoint, payload = {}) {
4803
- let token = getGlobalAdminToken();
4804
- if (!token && isLocalEnvironment()) {
4805
- try {
4806
- const res = await fetch("/__dsh_bridge__/loopback-token", { method: "POST" });
4807
- if (res.ok) {
4808
- const data = await res.json();
4809
- if (data?.adminToken) {
4810
- token = data.adminToken;
4811
- setGlobalAdminToken(token);
4812
- }
4813
- }
4814
- } catch {
4815
- }
5010
+ let token = getAdminToken();
5011
+ if (isLocalEnvironment()) {
5012
+ token = await fetchLoopbackTokenOnce();
4816
5013
  }
4817
- return rpcCall(endpoint, {
5014
+ const enriched = {
4818
5015
  ...payload,
4819
5016
  ...token ? { adminToken: token } : {},
4820
5017
  ...isLocalEnvironment() ? { isLocalhost: true } : {}
5018
+ };
5019
+ const res = await rpcCall(endpoint, enriched).catch((e) => {
5020
+ if (isAccessSessionFailure(e)) {
5021
+ clearAdminToken();
5022
+ reloadToLoginPage();
5023
+ }
5024
+ throw e;
4821
5025
  });
5026
+ if (res?.ok === false) {
5027
+ const msg = res?.error?.message || "";
5028
+ if (isLocalEnvironment() && (msg.includes("\u7BA1\u7406\u5458\u6743\u9650") || msg.includes("\u7BA1\u7406\u5BC6\u7801\u89E3\u9501"))) {
5029
+ const fresh = await fetchLoopbackTokenOnce(true);
5030
+ if (fresh) {
5031
+ return rpcCall(endpoint, { ...payload, adminToken: fresh, isLocalhost: true });
5032
+ }
5033
+ }
5034
+ if (msg.includes("\u7BA1\u7406\u5458\u6743\u9650") || msg.includes("\u7BA1\u7406\u5BC6\u7801\u89E3\u9501")) {
5035
+ clearAdminToken();
5036
+ const err = new Error("need-unlock");
5037
+ err.needUnlock = true;
5038
+ err.message = msg;
5039
+ err.unlockable = !msg.includes("\u4EC5\u9650\u7535\u8111\u672C\u673A\u7BA1\u7406");
5040
+ throw err;
5041
+ }
5042
+ }
5043
+ return res;
4822
5044
  }
4823
5045
  async function switchToWorkspace(wsId, wsPath) {
4824
5046
  if (isSubmitting) return;
@@ -4898,13 +5120,25 @@ function showRemoteWorkspaceDialog(rpcCall, onWorkspaceAdded, clientCtx, onPicke
4898
5120
  drives = res.drives || [];
4899
5121
  workspaces = res.workspaces || [];
4900
5122
  if (res.error) {
4901
- statusMessage = res.error;
5123
+ statusMessage = typeof res.error === "string" ? res.error : res.error?.message || "\u8BFB\u53D6\u76EE\u5F55\u5931\u8D25";
4902
5124
  isErrorMessage = true;
4903
5125
  }
4904
5126
  }
4905
5127
  } catch (err) {
4906
- statusMessage = err.message || "\u8BFB\u53D6\u76EE\u5F55\u5931\u8D25";
4907
- isErrorMessage = true;
5128
+ if (err?.needUnlock) {
5129
+ needUnlock = true;
5130
+ unlockErr = null;
5131
+ statusMessage = null;
5132
+ isErrorMessage = false;
5133
+ unlockable = err.unlockable !== false;
5134
+ if (!unlockable) {
5135
+ statusMessage = err.message || "\u5F53\u524D\u7B56\u7565\u4EC5\u9650\u7535\u8111\u672C\u673A\u7BA1\u7406\uFF0C\u8FDC\u7A0B\u65E0\u6CD5\u89E3\u9501";
5136
+ isErrorMessage = true;
5137
+ }
5138
+ } else {
5139
+ statusMessage = err.message || "\u8BFB\u53D6\u76EE\u5F55\u5931\u8D25";
5140
+ isErrorMessage = true;
5141
+ }
4908
5142
  } finally {
4909
5143
  isLoading = false;
4910
5144
  render();
@@ -4977,8 +5211,20 @@ function showRemoteWorkspaceDialog(rpcCall, onWorkspaceAdded, clientCtx, onPicke
4977
5211
  render();
4978
5212
  }
4979
5213
  } catch (err) {
4980
- statusMessage = err.message || "\u6DFB\u52A0\u5DE5\u4F5C\u533A\u5F02\u5E38";
4981
- isErrorMessage = true;
5214
+ if (err?.needUnlock) {
5215
+ needUnlock = true;
5216
+ unlockable = err.unlockable !== false;
5217
+ unlockErr = null;
5218
+ statusMessage = null;
5219
+ isErrorMessage = false;
5220
+ if (!unlockable) {
5221
+ statusMessage = err.message || "\u5F53\u524D\u7B56\u7565\u4EC5\u9650\u7535\u8111\u672C\u673A\u7BA1\u7406\uFF0C\u8FDC\u7A0B\u65E0\u6CD5\u89E3\u9501";
5222
+ isErrorMessage = true;
5223
+ }
5224
+ } else {
5225
+ statusMessage = err.message || "\u6DFB\u52A0\u5DE5\u4F5C\u533A\u5F02\u5E38";
5226
+ isErrorMessage = true;
5227
+ }
4982
5228
  render();
4983
5229
  } finally {
4984
5230
  isSubmitting = false;