dsh-remote-mobile 1.7.0 → 1.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/lib/client.js CHANGED
@@ -53,123 +53,123 @@ var import_react10 = __toESM(require("react"), 1);
53
53
  var import_react9 = __toESM(require("react"), 1);
54
54
 
55
55
  // src/client/api.js
56
- function fetchStatus() {
57
- return fetch("/api/remote-mobile/status?_t=" + Date.now()).then(function(res) {
58
- return res.json();
56
+ var RM_API_BASE = (function() {
57
+ try {
58
+ if (typeof document !== "undefined" && document.baseURI) {
59
+ return new URL("api/remote-mobile/", document.baseURI).toString();
60
+ }
61
+ return "/api/remote-mobile/";
62
+ } catch (e) {
63
+ return "/api/remote-mobile/";
64
+ }
65
+ })();
66
+ function safeFetchJson(url, options) {
67
+ return fetch(url, options).then(function(res) {
68
+ if (!res.ok) {
69
+ return res.text().then(function(text) {
70
+ var msg = "HTTP " + res.status;
71
+ try {
72
+ var parsed = JSON.parse(text);
73
+ if (parsed && (parsed.reason || parsed.message || parsed.error)) {
74
+ msg = parsed.reason || parsed.message || parsed.error;
75
+ }
76
+ } catch (e) {
77
+ }
78
+ throw new Error(msg);
79
+ });
80
+ }
81
+ return res.json().catch(function() {
82
+ throw new Error("\u54CD\u5E94\u89E3\u6790\u5931\u8D25 (Invalid JSON response)");
83
+ });
59
84
  });
60
85
  }
86
+ function fetchStatus() {
87
+ return safeFetchJson(RM_API_BASE + "status?_t=" + Date.now());
88
+ }
61
89
  function generatePairCode() {
62
- return fetch("/api/remote-mobile/generate-code", { method: "POST" }).then(function(res) {
63
- return res.json();
64
- });
90
+ return safeFetchJson(RM_API_BASE + "generate-code", { method: "POST" });
65
91
  }
66
92
  function updateBypassConfig(allowTailscale, allowLan) {
67
- return fetch("/api/remote-mobile/update-options", {
93
+ return safeFetchJson(RM_API_BASE + "update-options", {
68
94
  method: "POST",
69
95
  headers: { "Content-Type": "application/json" },
70
96
  body: JSON.stringify({ allowTailscale, allowLan })
71
- }).then(function(res) {
72
- return res.json();
73
97
  });
74
98
  }
75
99
  function updateAdvancedSecurityOptions(payload) {
76
- return fetch("/api/remote-mobile/update-options", {
100
+ return safeFetchJson(RM_API_BASE + "update-options", {
77
101
  method: "POST",
78
102
  headers: { "Content-Type": "application/json" },
79
103
  body: JSON.stringify(payload)
80
- }).then(function(res) {
81
- return res.json();
82
104
  });
83
105
  }
84
106
  function updateSecret(secret) {
85
- return fetch("/api/remote-mobile/set-secret", {
107
+ return safeFetchJson(RM_API_BASE + "set-secret", {
86
108
  method: "POST",
87
109
  headers: { "Content-Type": "application/json" },
88
110
  body: JSON.stringify({ secret })
89
- }).then(function(res) {
90
- return res.json();
91
111
  });
92
112
  }
93
113
  function clearSecretApi() {
94
- return fetch("/api/remote-mobile/clear-secret", {
114
+ return safeFetchJson(RM_API_BASE + "clear-secret", {
95
115
  method: "POST",
96
116
  headers: { "Content-Type": "application/json" }
97
- }).then(function(res) {
98
- return res.json();
99
117
  });
100
118
  }
101
119
  function revokeDeviceApi(token) {
102
- return fetch("/api/remote-mobile/revoke-device", {
120
+ return safeFetchJson(RM_API_BASE + "revoke-device", {
103
121
  method: "POST",
104
122
  headers: { "Content-Type": "application/json" },
105
123
  body: JSON.stringify({ token })
106
- }).then(function(res) {
107
- return res.json();
108
124
  });
109
125
  }
110
126
  function revokeAllDevicesApi() {
111
- return fetch("/api/remote-mobile/revoke-all", {
127
+ return safeFetchJson(RM_API_BASE + "revoke-all", {
112
128
  method: "POST",
113
129
  headers: { "Content-Type": "application/json" }
114
- }).then(function(res) {
115
- return res.json();
116
130
  });
117
131
  }
118
132
  function unlockIpApi(ip) {
119
- return fetch("/api/remote-mobile/unlock-ip", {
133
+ return safeFetchJson(RM_API_BASE + "unlock-ip", {
120
134
  method: "POST",
121
135
  headers: { "Content-Type": "application/json" },
122
136
  body: JSON.stringify({ ip })
123
- }).then(function(res) {
124
- return res.json();
125
137
  });
126
138
  }
127
139
  function clearIpStatsApi() {
128
- return fetch("/api/remote-mobile/clear-ip-stats", {
140
+ return safeFetchJson(RM_API_BASE + "clear-ip-stats", {
129
141
  method: "POST",
130
142
  headers: { "Content-Type": "application/json" }
131
- }).then(function(res) {
132
- return res.json();
133
143
  });
134
144
  }
135
145
  function fetchStyles() {
136
- return fetch("/api/remote-mobile/styles?_t=" + Date.now()).then(function(res) {
137
- return res.json();
138
- });
146
+ return safeFetchJson(RM_API_BASE + "styles?_t=" + Date.now());
139
147
  }
140
148
  function saveStyleSnippet(payload) {
141
- return fetch("/api/remote-mobile/styles", {
149
+ return safeFetchJson(RM_API_BASE + "styles", {
142
150
  method: "POST",
143
151
  headers: { "Content-Type": "application/json" },
144
152
  body: JSON.stringify(payload)
145
- }).then(function(res) {
146
- return res.json();
147
153
  });
148
154
  }
149
155
  function toggleStyleApi(id, scope, enabled) {
150
- return fetch("/api/remote-mobile/styles/toggle", {
156
+ return safeFetchJson(RM_API_BASE + "styles/toggle", {
151
157
  method: "POST",
152
158
  headers: { "Content-Type": "application/json" },
153
159
  body: JSON.stringify({ id, scope, enabled })
154
- }).then(function(res) {
155
- return res.json();
156
160
  });
157
161
  }
158
162
  function deleteStyleSnippetApi(id) {
159
- return fetch("/api/remote-mobile/styles/delete", {
163
+ return safeFetchJson(RM_API_BASE + "styles/delete", {
160
164
  method: "POST",
161
165
  headers: { "Content-Type": "application/json" },
162
166
  body: JSON.stringify({ id })
163
- }).then(function(res) {
164
- return res.json();
165
167
  });
166
168
  }
167
169
  function resetStylesApi() {
168
- return fetch("/api/remote-mobile/styles/reset", {
170
+ return safeFetchJson(RM_API_BASE + "styles/reset", {
169
171
  method: "POST",
170
172
  headers: { "Content-Type": "application/json" }
171
- }).then(function(res) {
172
- return res.json();
173
173
  });
174
174
  }
175
175
 
@@ -782,8 +782,8 @@ var translations = {
782
782
  storageStylesFile: "\u79FB\u52A8\u7AEF\u6837\u5F0F\u7247\u6BB5\u6587\u4EF6 (\u53EF\u72EC\u7ACB\u542F\u505C\u7684 CSS \u5C0F\u63D2\u4EF6)\uFF1A",
783
783
  // 卡片 7: 全局高级配置
784
784
  configCardTitle: "\u5168\u5C40\u9AD8\u7EA7\u5B89\u5168\u53C2\u6570\u914D\u7F6E",
785
- configCardDesc: "\u8C03\u6574\u5355 IP \u8BBF\u95EE\u9891\u7387\u9650\u5236\u3001\u8F93\u9519\u5C01\u7981\u9608\u503C\u4E0E\u9501\u5B9A\u6301\u7EED\u65F6\u95F4\u3002\u70B9\u51FB\u4FDD\u5B58\u5C06\u7ACB\u5373\u751F\u6548\u5E76\u81EA\u52A8\u540C\u6B65\u5199\u56DE ~/.dsh/settings.yaml \u914D\u7F6E\u6587\u4EF6\u3002",
786
- copySettingsPathBtn: "\u{1F4CB} \u590D\u5236 settings.yaml \u8DEF\u5F84",
785
+ configCardDesc: "\u8C03\u6574\u5355 IP \u8BBF\u95EE\u9891\u7387\u9650\u5236\u3001\u8F93\u9519\u5C01\u7981\u9608\u503C\u4E0E\u9501\u5B9A\u6301\u7EED\u65F6\u95F4\u3002\u70B9\u51FB\u4FDD\u5B58\u5C06\u7ACB\u5373\u751F\u6548\u5E76\u4FDD\u5B58\u5230 ~/.dsh/remote-mobile/settings.json\u3002",
786
+ copySettingsPathBtn: "\u{1F4CB} \u590D\u5236 settings.json \u8DEF\u5F84",
787
787
  fieldVisitsLimit: "\u23F1\uFE0F \u6BCF\u5206\u949F\u8BBF\u95EE\u4E0A\u9650 (\u6B21/\u5206)",
788
788
  fieldVisitsLimitHint: "\u5355 IP 1\u5206\u949F\u5185\u6253\u5F00\u767B\u5F55\u9875\u8D85\u8FC7\u6B64\u503C\u5C06\u89E6\u53D1\u9650\u9891",
789
789
  fieldFailedLimit: "\u{1F6E1}\uFE0F \u8F93\u9519\u5BC6\u7801\u5C01\u7981\u9608\u503C (\u6B21)",
@@ -793,9 +793,9 @@ var translations = {
793
793
  restoreDefaultsBtn: "\u{1F504} \u6062\u590D\u9ED8\u8BA4\u53C2\u6570",
794
794
  saveConfigBtn: "\u{1F4BE} \u4FDD\u5B58\u53C2\u6570\u914D\u7F6E",
795
795
  savingConfigBtn: "\u6B63\u5728\u4FDD\u5B58...",
796
- saveConfigConfirm: "\u786E\u5B9A\u8981\u4FDD\u5B58\u5F53\u524D\u9AD8\u7EA7\u5B89\u5168\u53C2\u6570\u914D\u7F6E\u5417\uFF1F\u4FDD\u5B58\u540E\u5C06\u7ACB\u5373\u751F\u6548\u5E76\u81EA\u52A8\u540C\u6B65\u5199\u56DE ~/.dsh/settings.yaml\u3002",
796
+ saveConfigConfirm: "\u786E\u5B9A\u8981\u4FDD\u5B58\u5F53\u524D\u9AD8\u7EA7\u5B89\u5168\u53C2\u6570\u914D\u7F6E\u5417\uFF1F\u4FDD\u5B58\u540E\u5C06\u7ACB\u5373\u751F\u6548\u5E76\u4FDD\u5B58\u5230 ~/.dsh/remote-mobile/settings.json\u3002",
797
797
  restoreDefaultsConfirm: "\u786E\u5B9A\u8981\u5C06\u5B89\u5168\u53C2\u6570\u6062\u590D\u4E3A\u7CFB\u7EDF\u9ED8\u8BA4\u503C\uFF0860\u6B21/\u5206\u30015\u6B21\u5931\u8D25\u3001\u9501\u5B9A15\u5206\u949F\uFF09\u5417\uFF1F\u786E\u8BA4\u540E\u5C06\u76F4\u63A5\u751F\u6548\u5E76\u81EA\u52A8\u540C\u6B65\u3002",
798
- saveConfigSuccessToast: "\u9AD8\u7EA7\u5B89\u5168\u53C2\u6570\u5DF2\u6210\u529F\u4FDD\u5B58\u5E76\u540C\u6B65\u5230 settings.yaml\uFF01",
798
+ saveConfigSuccessToast: "\u9AD8\u7EA7\u5B89\u5168\u53C2\u6570\u5DF2\u6210\u529F\u4FDD\u5B58\u5E76\u540C\u6B65\u5230 settings.json\uFF01",
799
799
  restoreDefaultsSuccessToast: "\u5DF2\u6210\u529F\u6062\u590D\u9ED8\u8BA4\u5B89\u5168\u53C2\u6570\u5E76\u7ACB\u5373\u751F\u6548\uFF01",
800
800
  saveConfigFailTip: "\u53C2\u6570\u5FC5\u987B\u4E3A\u5927\u4E8E 0 \u7684\u6709\u6548\u6574\u6570",
801
801
  // 卡片 8: 移动端样式片段(样式小插件)
@@ -936,8 +936,8 @@ var translations = {
936
936
  storageStylesFile: "Mobile style snippets file (toggleable CSS mini-plugins):",
937
937
  // Card 7: Advanced Security Config
938
938
  configCardTitle: "Global Security Policy Configuration",
939
- configCardDesc: "Configure per-IP rate limits, brute-force failure thresholds, and lockout durations. Changes take effect immediately and sync to ~/.dsh/settings.yaml.",
940
- copySettingsPathBtn: "\u{1F4CB} Copy settings.yaml Path",
939
+ configCardDesc: "Configure per-IP rate limits, brute-force failure thresholds, and lockout durations. Changes take effect immediately and are saved to ~/.dsh/remote-mobile/settings.json.",
940
+ copySettingsPathBtn: "\u{1F4CB} Copy settings.json Path",
941
941
  fieldVisitsLimit: "\u23F1\uFE0F Max Visits per Minute (visits/min)",
942
942
  fieldVisitsLimitHint: "Rate limit triggered when a single IP opens login page more than this value",
943
943
  fieldFailedLimit: "\u{1F6E1}\uFE0F Max Failed Password Attempts (times)",
@@ -947,9 +947,9 @@ var translations = {
947
947
  restoreDefaultsBtn: "\u{1F504} Restore Defaults",
948
948
  saveConfigBtn: "\u{1F4BE} Save Configuration",
949
949
  savingConfigBtn: "Saving...",
950
- saveConfigConfirm: "Are you sure you want to save the current security policy? It will take effect immediately and sync to ~/.dsh/settings.yaml.",
950
+ saveConfigConfirm: "Are you sure you want to save the current security policy? It will take effect immediately and be saved to ~/.dsh/remote-mobile/settings.json.",
951
951
  restoreDefaultsConfirm: "Are you sure you want to restore security policies to default values (60 visits/min, 5 failed attempts, 15m lockout)? It will take effect immediately.",
952
- saveConfigSuccessToast: "Security policy successfully saved and synced to settings.yaml!",
952
+ saveConfigSuccessToast: "Security policy successfully saved and synced to settings.json!",
953
953
  restoreDefaultsSuccessToast: "Default security policies restored and applied immediately!",
954
954
  saveConfigFailTip: "Parameters must be valid integers greater than 0",
955
955
  // Card 8: Mobile Style Snippets (style mini-plugin)
@@ -2783,8 +2783,8 @@ function ConfigCard({
2783
2783
  onClick: () => {
2784
2784
  if (typeof copyText === "function") {
2785
2785
  copyText(
2786
- "~/.dsh/settings.yaml",
2787
- lang === "en" ? "Settings path copied (~/.dsh/settings.yaml)" : "\u5DF2\u590D\u5236\u914D\u7F6E\u6587\u4EF6\u8DEF\u5F84\uFF08~/.dsh/settings.yaml\uFF09"
2786
+ "~/.dsh/remote-mobile/settings.json",
2787
+ lang === "en" ? "Settings path copied (~/.dsh/remote-mobile/settings.json)" : "\u5DF2\u590D\u5236\u914D\u7F6E\u6587\u4EF6\u8DEF\u5F84\uFF08~/.dsh/remote-mobile/settings.json\uFF09"
2788
2788
  );
2789
2789
  }
2790
2790
  },
@@ -3080,7 +3080,11 @@ function StylesCard({ lang, showToast }) {
3080
3080
  enabled ? lang === "en" ? scopeLabel + " enabled!" : scopeLabel + " \u5DF2\u542F\u7528\uFF01" : lang === "en" ? scopeLabel + " disabled!" : scopeLabel + " \u5DF2\u505C\u7528\uFF01",
3081
3081
  "success"
3082
3082
  );
3083
+ } else {
3084
+ showFeedback((lang === "en" ? "Toggle failed: " : "\u5207\u6362\u5931\u8D25\uFF1A") + (data.reason || ""), "danger");
3083
3085
  }
3086
+ }).catch((err) => {
3087
+ showFeedback((lang === "en" ? "Toggle failed: " : "\u5207\u6362\u5931\u8D25\uFF1A") + (err?.message || err), "danger");
3084
3088
  });
3085
3089
  };
3086
3090
  const startEdit = (s) => {
@@ -3103,7 +3107,7 @@ function StylesCard({ lang, showToast }) {
3103
3107
  };
3104
3108
  const save = () => {
3105
3109
  if (!name.trim() || !css.trim()) {
3106
- alert(t("stylesFieldsRequired", lang));
3110
+ showFeedback(t("stylesFieldsRequired", lang), "danger");
3107
3111
  return;
3108
3112
  }
3109
3113
  saveStyleSnippet({
@@ -3118,10 +3122,10 @@ function StylesCard({ lang, showToast }) {
3118
3122
  cancelEdit();
3119
3123
  showFeedback(t("stylesSaveSuccess", lang));
3120
3124
  } else {
3121
- alert((lang === "en" ? "Save failed: " : "\u4FDD\u5B58\u5931\u8D25\uFF1A") + (data.reason || ""));
3125
+ showFeedback((lang === "en" ? "Save failed: " : "\u4FDD\u5B58\u5931\u8D25\uFF1A") + (data.reason || ""), "danger");
3122
3126
  }
3123
- }).catch(() => {
3124
- alert(lang === "en" ? "Network request failed, please try again." : "\u7F51\u7EDC\u8BF7\u6C42\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5");
3127
+ }).catch((err) => {
3128
+ showFeedback((lang === "en" ? "Save failed: " : "\u4FDD\u5B58\u5931\u8D25\uFF1A") + (err?.message || err), "danger");
3125
3129
  });
3126
3130
  };
3127
3131
  const remove = (s) => {
@@ -3132,7 +3136,11 @@ function StylesCard({ lang, showToast }) {
3132
3136
  if (editingId === s.id) cancelEdit();
3133
3137
  load();
3134
3138
  showFeedback(lang === "en" ? "Style snippet deleted!" : "\u81EA\u5B9A\u4E49\u6837\u5F0F\u7247\u6BB5\u5DF2\u5220\u9664\uFF01");
3139
+ } else {
3140
+ showFeedback((lang === "en" ? "Delete failed: " : "\u5220\u9664\u5931\u8D25\uFF1A") + (data.reason || ""), "danger");
3135
3141
  }
3142
+ }).catch((err) => {
3143
+ showFeedback((lang === "en" ? "Delete failed: " : "\u5220\u9664\u5931\u8D25\uFF1A") + (err?.message || err), "danger");
3136
3144
  });
3137
3145
  }
3138
3146
  };
@@ -3142,7 +3150,11 @@ function StylesCard({ lang, showToast }) {
3142
3150
  if (data.success) {
3143
3151
  load();
3144
3152
  showFeedback(lang === "en" ? "Style snippets reset to defaults!" : "\u5DF2\u6062\u590D\u6240\u6709\u7247\u6BB5\u7684\u9ED8\u8BA4\u542F\u505C\u72B6\u6001\uFF01");
3153
+ } else {
3154
+ showFeedback((lang === "en" ? "Reset failed: " : "\u91CD\u7F6E\u5931\u8D25\uFF1A") + (data.reason || ""), "danger");
3145
3155
  }
3156
+ }).catch((err) => {
3157
+ showFeedback((lang === "en" ? "Reset failed: " : "\u91CD\u7F6E\u5931\u8D25\uFF1A") + (err?.message || err), "danger");
3146
3158
  });
3147
3159
  };
3148
3160
  const toggleExpand = (id) => {
@@ -3526,8 +3538,8 @@ function StylesCard({ lang, showToast }) {
3526
3538
  }
3527
3539
 
3528
3540
  // src/client/version.js
3529
- var PLUGIN_VERSION = "v1.7.0";
3530
- var BUILD_TIME = "2026-09-15 23:45:02";
3541
+ var PLUGIN_VERSION = "v1.9.0";
3542
+ var BUILD_TIME = "2026-10-09 20:14:06";
3531
3543
 
3532
3544
  // src/client/components/MainSection.jsx
3533
3545
  var import_jsx_runtime9 = require("react/jsx-runtime");
@@ -3574,6 +3586,12 @@ function TailscaleMobileSection(props) {
3574
3586
  const [hasUpdate, setHasUpdate] = (0, import_react9.useState)(false);
3575
3587
  const [conflictBannerDismissed, setConflictBannerDismissed] = (0, import_react9.useState)(false);
3576
3588
  const qrTabTouchedRef = (0, import_react9.useRef)(false);
3589
+ const toastTimerRef = (0, import_react9.useRef)(null);
3590
+ const showToast = (0, import_react9.useCallback)((message, type = "success", duration = 2500) => {
3591
+ if (toastTimerRef.current) clearTimeout(toastTimerRef.current);
3592
+ setToast({ message, type });
3593
+ toastTimerRef.current = setTimeout(() => setToast(null), duration);
3594
+ }, []);
3577
3595
  const updateQrTab = (0, import_react9.useCallback)((tab) => {
3578
3596
  qrTabTouchedRef.current = true;
3579
3597
  setSelectedTab(tab);
@@ -3758,6 +3776,8 @@ function TailscaleMobileSection(props) {
3758
3776
  const nextVal = !status.allowTailscale;
3759
3777
  updateBypassConfig(nextVal, status.allowLan).then(() => {
3760
3778
  setStatus((prev) => ({ ...prev, allowTailscale: nextVal }));
3779
+ }).catch((err) => {
3780
+ showToast((currentLang === "en" ? "Operation failed: " : "\u64CD\u4F5C\u5931\u8D25\uFF1A") + (err?.message || err), "danger", 3500);
3761
3781
  });
3762
3782
  };
3763
3783
  const toggleLan = () => {
@@ -3768,6 +3788,8 @@ function TailscaleMobileSection(props) {
3768
3788
  }
3769
3789
  updateBypassConfig(status.allowTailscale, nextVal).then(() => {
3770
3790
  setStatus((prev) => ({ ...prev, allowLan: nextVal }));
3791
+ }).catch((err) => {
3792
+ showToast((currentLang === "en" ? "Operation failed: " : "\u64CD\u4F5C\u5931\u8D25\uFF1A") + (err?.message || err), "danger", 3500);
3771
3793
  });
3772
3794
  };
3773
3795
  const saveSecret = () => {
@@ -3792,8 +3814,10 @@ function TailscaleMobileSection(props) {
3792
3814
  3e3
3793
3815
  );
3794
3816
  } else {
3795
- alert((currentLang === "en" ? "Save failed: " : "\u4FDD\u5B58\u5931\u8D25\uFF1A") + (data.reason || ""));
3817
+ showToast((currentLang === "en" ? "Save failed: " : "\u4FDD\u5B58\u5931\u8D25\uFF1A") + (data.reason || ""), "danger", 3500);
3796
3818
  }
3819
+ }).catch((err) => {
3820
+ showToast((currentLang === "en" ? "Save failed: " : "\u4FDD\u5B58\u5931\u8D25\uFF1A") + (err?.message || err), "danger", 3500);
3797
3821
  });
3798
3822
  };
3799
3823
  const clearSecret = () => {
@@ -3806,6 +3830,8 @@ function TailscaleMobileSection(props) {
3806
3830
  "info",
3807
3831
  3e3
3808
3832
  );
3833
+ }).catch((err) => {
3834
+ showToast((currentLang === "en" ? "Clear failed: " : "\u6E05\u9664\u5931\u8D25\uFF1A") + (err?.message || err), "danger", 3500);
3809
3835
  });
3810
3836
  }
3811
3837
  };
@@ -3818,6 +3844,8 @@ function TailscaleMobileSection(props) {
3818
3844
  "info",
3819
3845
  2500
3820
3846
  );
3847
+ }).catch((err) => {
3848
+ showToast((currentLang === "en" ? "Revoke failed: " : "\u65AD\u5F00\u5931\u8D25\uFF1A") + (err?.message || err), "danger", 3500);
3821
3849
  });
3822
3850
  }
3823
3851
  };
@@ -3830,6 +3858,8 @@ function TailscaleMobileSection(props) {
3830
3858
  "info",
3831
3859
  2500
3832
3860
  );
3861
+ }).catch((err) => {
3862
+ showToast((currentLang === "en" ? "Revoke failed: " : "\u65AD\u5F00\u5931\u8D25\uFF1A") + (err?.message || err), "danger", 3500);
3833
3863
  });
3834
3864
  }
3835
3865
  };
@@ -3842,7 +3872,8 @@ function TailscaleMobileSection(props) {
3842
3872
  "success",
3843
3873
  3e3
3844
3874
  );
3845
- }).catch(() => {
3875
+ }).catch((err) => {
3876
+ showToast((currentLang === "en" ? "Unlock failed: " : "\u89E3\u9501\u5931\u8D25\uFF1A") + (err?.message || err), "danger", 3500);
3846
3877
  });
3847
3878
  }
3848
3879
  };
@@ -3855,7 +3886,8 @@ function TailscaleMobileSection(props) {
3855
3886
  "success",
3856
3887
  2500
3857
3888
  );
3858
- }).catch(() => {
3889
+ }).catch((err) => {
3890
+ showToast((currentLang === "en" ? "Clear failed: " : "\u6E05\u7A7A\u5931\u8D25\uFF1A") + (err?.message || err), "danger", 3500);
3859
3891
  });
3860
3892
  }
3861
3893
  };
@@ -3881,11 +3913,11 @@ function TailscaleMobileSection(props) {
3881
3913
  refreshStatusAndCode();
3882
3914
  showToast(t("saveConfigSuccessToast", currentLang), "success", 3e3);
3883
3915
  } else {
3884
- alert((currentLang === "en" ? "Save failed: " : "\u4FDD\u5B58\u5931\u8D25\uFF1A") + (data.reason || ""));
3916
+ showToast((currentLang === "en" ? "Save failed: " : "\u4FDD\u5B58\u5931\u8D25\uFF1A") + (data.reason || ""), "danger", 3500);
3885
3917
  }
3886
- }).catch(() => {
3918
+ }).catch((err) => {
3887
3919
  setIsSaving(false);
3888
- alert(currentLang === "en" ? "Network request failed, please try again." : "\u7F51\u7EDC\u8BF7\u6C42\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5");
3920
+ showToast((currentLang === "en" ? "Save failed: " : "\u4FDD\u5B58\u5931\u8D25\uFF1A") + (err?.message || err), "danger", 3500);
3889
3921
  });
3890
3922
  };
3891
3923
  const resetAdvancedConfigDefaults = () => {
@@ -3906,11 +3938,11 @@ function TailscaleMobileSection(props) {
3906
3938
  refreshStatusAndCode();
3907
3939
  showToast(t("restoreDefaultsSuccessToast", currentLang), "success", 3e3);
3908
3940
  } else {
3909
- alert((currentLang === "en" ? "Reset failed: " : "\u91CD\u7F6E\u5931\u8D25\uFF1A") + (data.reason || ""));
3941
+ showToast((currentLang === "en" ? "Reset failed: " : "\u91CD\u7F6E\u5931\u8D25\uFF1A") + (data.reason || ""), "danger", 3500);
3910
3942
  }
3911
- }).catch(() => {
3943
+ }).catch((err) => {
3912
3944
  setIsSaving(false);
3913
- alert(currentLang === "en" ? "Network request failed, please try again." : "\u7F51\u7EDC\u8BF7\u6C42\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5");
3945
+ showToast((currentLang === "en" ? "Reset failed: " : "\u91CD\u7F6E\u5931\u8D25\uFF1A") + (err?.message || err), "danger", 3500);
3914
3946
  });
3915
3947
  };
3916
3948
  const copyDirectLink = () => {
@@ -3927,12 +3959,6 @@ function TailscaleMobileSection(props) {
3927
3959
  setStatus((prev) => ({ ...prev, copied: false }));
3928
3960
  }, 2e3);
3929
3961
  };
3930
- const toastTimerRef = (0, import_react9.useRef)(null);
3931
- const showToast = (0, import_react9.useCallback)((message, type = "success", duration = 2500) => {
3932
- if (toastTimerRef.current) clearTimeout(toastTimerRef.current);
3933
- setToast({ message, type });
3934
- toastTimerRef.current = setTimeout(() => setToast(null), duration);
3935
- }, []);
3936
3962
  const copyText = (text, successTip) => {
3937
3963
  const triggerToast = () => {
3938
3964
  showToast(
@@ -4552,7 +4578,7 @@ function apply(ctx) {
4552
4578
  window.__DSH_REMOTE_MOBILE_SSE_LISTENER__ = true;
4553
4579
  var initSse = function() {
4554
4580
  try {
4555
- var eventSource = new EventSource("/api/remote-mobile/events");
4581
+ var eventSource = new EventSource(RM_API_BASE + "events");
4556
4582
  eventSource.onmessage = function(event) {
4557
4583
  try {
4558
4584
  var data = JSON.parse(event.data || "{}");
package/lib/index.d.ts CHANGED
@@ -9,7 +9,8 @@
9
9
  * 4. Tailscale 虚拟私网与局域网 LAN 智能免密直连切换;
10
10
  * 5. 防暴力破解限流与单 IP 异常访问熔断审计;
11
11
  * 6. 全链路网络上下文虚拟化桥接与 @linxin666 生态无缝兼容。
12
- * 7. 接入 DSH Settings 服务,配置统一持久化于 ~/.dsh/settings.yaml 的 dsh-remote-mobile 命名空间。
12
+ * 7. 配置持久化于插件自有文件 ~/.dsh/remote-mobile/settings.json(DSH 0.1.7+ 起官方已弃用 settings.yaml),
13
+ * 启动时一次性兼容迁移旧 settings.yaml / settings.yaml.imported 中的配置。
13
14
  */
14
15
  import { type SessionStoreOptions } from './auth/token.js';
15
16
  /** Cordis 插件名称标识 */
package/lib/index.js CHANGED
@@ -9,7 +9,8 @@
9
9
  * 4. Tailscale 虚拟私网与局域网 LAN 智能免密直连切换;
10
10
  * 5. 防暴力破解限流与单 IP 异常访问熔断审计;
11
11
  * 6. 全链路网络上下文虚拟化桥接与 @linxin666 生态无缝兼容。
12
- * 7. 接入 DSH Settings 服务,配置统一持久化于 ~/.dsh/settings.yaml 的 dsh-remote-mobile 命名空间。
12
+ * 7. 配置持久化于插件自有文件 ~/.dsh/remote-mobile/settings.json(DSH 0.1.7+ 起官方已弃用 settings.yaml),
13
+ * 启动时一次性兼容迁移旧 settings.yaml / settings.yaml.imported 中的配置。
13
14
  */
14
15
  import { SessionStore } from './auth/token.js';
15
16
  import { createGlobalAuthGate } from './auth/gate.js';
@@ -24,7 +25,11 @@ export const REMOTE_MOBILE_SETTINGS_NAMESPACE = 'dsh-remote-mobile';
24
25
  /** Cordis 依赖注入声明:声明注入底座内置的 webServer 服务 */
25
26
  export const inject = ['webServer'];
26
27
  /**
27
- * 注册并绑定 DSH Settings 服务(若存在)
28
+ * 尽力接入 DSH Settings 服务(若宿主提供旧版 register API)。
29
+ *
30
+ * 说明:DSH 0.1.7+ 起官方 dsh-settings 已改为基于 profile cordis.patch.yml 的
31
+ * Config 表单模型(无 register 方法),本函数在该版本下会安全跳过;插件的配置
32
+ * 持久化改由自有 settings.json 承担(见 token.ts)。
28
33
  */
29
34
  function bindDshSettings(ctx, store) {
30
35
  try {
@@ -39,19 +44,19 @@ function bindDshSettings(ctx, store) {
39
44
  base: store.getOptions(),
40
45
  });
41
46
  if (scope) {
42
- // 1. 同步 settings.yaml 中的最新配置到 SessionStore
47
+ // 1. 同步宿主 Settings 中的最新配置到 SessionStore
43
48
  const initialVal = scope.get?.();
44
49
  if (initialVal && typeof initialVal === 'object') {
45
50
  store.updateOptions(initialVal, false);
46
51
  }
47
- // 2. 监听 settings.yaml 或 Web UI 配置变更
52
+ // 2. 监听宿主 Settings 或 Web UI 配置变更
48
53
  scope.watch?.(() => {
49
54
  const updated = scope.get?.();
50
55
  if (updated && typeof updated === 'object') {
51
56
  store.updateOptions(updated, false);
52
57
  }
53
58
  });
54
- // 3. 绑定反向写入钩子:当插件 API 修改配置时同步写回 settings.yaml
59
+ // 3. 绑定反向写入钩子:插件 API 修改配置时同步写回宿主 Settings(旧版 API)
55
60
  store.setSettingsMutator((patch) => {
56
61
  try {
57
62
  const ops = Object.entries(patch).map(([field, value]) => ({
package/lib/routes/api.js CHANGED
@@ -4,6 +4,7 @@ import { getPublicKeyPem, decryptWithPrivateKey, validateSecretStrength, RSA_KEY
4
4
  import { getLoginPageHtml } from './login-page.js';
5
5
  import { DEFAULT_STYLE_FILE } from '../styles/style-snippets.js';
6
6
  import { getPairingBridgeState } from '../bridge/compat.js';
7
+ import { resolveRedirectPath } from '../auth/prefix.js';
7
8
  function jsonResponse(res, status, data) {
8
9
  res.writeHead(status, {
9
10
  'Content-Type': 'application/json; charset=utf-8',
@@ -287,9 +288,10 @@ export function createRoutes(store, styleStore) {
287
288
  const token = store.extractTokenFromRequest(req);
288
289
  const ua = req.headers['user-agent'] || '';
289
290
  const hasValidToken = token ? store.validateToken(token, clientIp) : false;
290
- // 如果开启了免密直连(Tailscale 或 局域网)或者已持有有效 Token,直接放行直跳根目录
291
+ // 如果开启了免密直连(Tailscale 或 局域网)或者已持有有效 Token,直接放行直跳根目录(支持网关前缀)
291
292
  if ((opts.allowTailscale && isTs) || (opts.allowLan && isLan) || hasValidToken) {
292
- res.writeHead(302, { Location: '/' });
293
+ const rootLocation = resolveRedirectPath(req.headers, '/');
294
+ res.writeHead(302, { Location: rootLocation });
293
295
  return res.end();
294
296
  }
295
297
  // 记录 IP 访问计数并进行防高频刷量限频与设备型号解析
@@ -332,6 +332,40 @@ export function getLoginPageHtml(publicKeyPem, lang = 'zh') {
332
332
  input.value = queryToken;
333
333
  }
334
334
 
335
+ // 自适应网关子路径 / baseURI 与重定向
336
+ const verifyApiUrl = (function () {
337
+ try {
338
+ if (document.baseURI && document.querySelector('base')) {
339
+ return new URL('api/remote-mobile/verify', document.baseURI).toString();
340
+ }
341
+ const pathname = window.location.pathname.replace(/\/auth\/?$/, '/');
342
+ return new URL('api/remote-mobile/verify', window.location.origin + pathname).toString();
343
+ } catch (e) {
344
+ return '/api/remote-mobile/verify';
345
+ }
346
+ })();
347
+
348
+ const successRedirectUrl = (function () {
349
+ try {
350
+ // 预留参数能力:支持通过 ?redirect=/path 携带登录后重定向目标
351
+ const redirectParam = urlParams.get('redirect');
352
+ if (redirectParam && redirectParam.startsWith('/') && !redirectParam.startsWith('//')) {
353
+ const appPrefix = window.location.pathname.replace(/\/auth\/?$/, '');
354
+ if (appPrefix && redirectParam.startsWith(appPrefix)) {
355
+ return window.location.origin + redirectParam;
356
+ }
357
+ return window.location.origin + appPrefix + redirectParam;
358
+ }
359
+ if (document.baseURI && document.querySelector('base')) {
360
+ return new URL('./', document.baseURI).toString();
361
+ }
362
+ const appPrefix = window.location.pathname.replace(/\/auth\/?$/, '/');
363
+ return window.location.origin + appPrefix;
364
+ } catch (e) {
365
+ return '/';
366
+ }
367
+ })();
368
+
335
369
  form.addEventListener('submit', (e) => {
336
370
  e.preventDefault();
337
371
  const val = input.value.trim();
@@ -347,7 +381,7 @@ export function getLoginPageHtml(publicKeyPem, lang = 'zh') {
347
381
 
348
382
  try {
349
383
  const payload = await encryptCredential(credential);
350
- const res = await fetch('/api/remote-mobile/verify', {
384
+ const res = await fetch(verifyApiUrl, {
351
385
  method: 'POST',
352
386
  headers: { 'Content-Type': 'application/json' },
353
387
  body: JSON.stringify(payload)
@@ -358,7 +392,7 @@ export function getLoginPageHtml(publicKeyPem, lang = 'zh') {
358
392
  statusMsg.innerText = ${JSON.stringify(i18n.success)};
359
393
  statusMsg.style.display = 'block';
360
394
  setTimeout(() => {
361
- window.location.href = '/';
395
+ window.location.href = successRedirectUrl;
362
396
  }, 600);
363
397
  } else {
364
398
  statusMsg.className = 'status-msg error';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-remote-mobile",
3
- "version": "1.7.0",
3
+ "version": "1.9.0",
4
4
  "description": "DeepSeek Harness 远程与移动端安全网关插件:零修改 DSH 底层代码安全开放局域网与 Tailscale 连接 | DeepSeek Harness (DSH) Remote & Mobile Security Guard: safely opens Tailscale/LAN with zero core modifications, QR scan auth, RSA encryption & brute-force defense.",
5
5
  "keywords": [
6
6
  "dsh",