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/CHANGELOG.md +31 -0
- package/README.md +81 -43
- package/README_EN.md +82 -44
- package/lib/auth/gate.js +3 -1
- package/lib/auth/prefix.d.ts +19 -0
- package/lib/auth/prefix.js +41 -0
- package/lib/auth/token.d.ts +55 -3
- package/lib/auth/token.js +202 -76
- package/lib/client.js +103 -77
- package/lib/index.d.ts +2 -1
- package/lib/index.js +10 -5
- package/lib/routes/api.js +4 -2
- package/lib/routes/login-page.js +36 -2
- package/package.json +1 -1
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
137
|
-
return res.json();
|
|
138
|
-
});
|
|
146
|
+
return safeFetchJson(RM_API_BASE + "styles?_t=" + Date.now());
|
|
139
147
|
}
|
|
140
148
|
function saveStyleSnippet(payload) {
|
|
141
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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\
|
|
786
|
-
copySettingsPathBtn: "\u{1F4CB} \u590D\u5236 settings.
|
|
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\
|
|
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.
|
|
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
|
|
940
|
-
copySettingsPathBtn: "\u{1F4CB} Copy settings.
|
|
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
|
|
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.
|
|
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.
|
|
2787
|
-
lang === "en" ? "Settings path copied (~/.dsh/settings.
|
|
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
|
-
|
|
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
|
-
|
|
3125
|
+
showFeedback((lang === "en" ? "Save failed: " : "\u4FDD\u5B58\u5931\u8D25\uFF1A") + (data.reason || ""), "danger");
|
|
3122
3126
|
}
|
|
3123
|
-
}).catch(() => {
|
|
3124
|
-
|
|
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.
|
|
3530
|
-
var BUILD_TIME = "2026-09
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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("
|
|
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.
|
|
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.
|
|
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
|
-
*
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
293
|
+
const rootLocation = resolveRedirectPath(req.headers, '/');
|
|
294
|
+
res.writeHead(302, { Location: rootLocation });
|
|
293
295
|
return res.end();
|
|
294
296
|
}
|
|
295
297
|
// 记录 IP 访问计数并进行防高频刷量限频与设备型号解析
|
package/lib/routes/login-page.js
CHANGED
|
@@ -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(
|
|
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.
|
|
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",
|