dsh-remote-mobile 1.1.0 → 1.2.1

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/auth/token.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { randomBytes, randomInt } from 'node:crypto';
2
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
2
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync, unlinkSync } from 'node:fs';
3
3
  import { dirname } from 'node:path';
4
4
  import { homedir } from 'node:os';
5
5
  import { EventEmitter } from 'node:events';
@@ -90,9 +90,10 @@ export function readGlobalLocale(filePath = GLOBAL_SETTINGS_FILE) {
90
90
  }
91
91
  }
92
92
  /**
93
- * 将最新的插件配置安全写回 ~/.dsh/settings.yaml 中的 dsh-remote-mobile 命名空间
93
+ * 将最新的插件配置安全原子写回 ~/.dsh/settings.yaml 中的 dsh-remote-mobile 命名空间
94
94
  */
95
95
  export function writeBackToSettingsYaml(options, filePath = GLOBAL_SETTINGS_FILE) {
96
+ const tmpPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
96
97
  try {
97
98
  if (!existsSync(filePath))
98
99
  return false;
@@ -126,10 +127,16 @@ export function writeBackToSettingsYaml(options, filePath = GLOBAL_SETTINGS_FILE
126
127
  lines.splice(startIdx, endIdx - startIdx, newBlock);
127
128
  updatedContent = lines.join('\n');
128
129
  }
129
- writeFileSync(filePath, updatedContent, 'utf8');
130
+ writeFileSync(tmpPath, updatedContent, 'utf8');
131
+ renameSync(tmpPath, filePath);
130
132
  return true;
131
133
  }
132
134
  catch {
135
+ try {
136
+ if (existsSync(tmpPath))
137
+ unlinkSync(tmpPath);
138
+ }
139
+ catch { }
133
140
  return false;
134
141
  }
135
142
  }
@@ -168,12 +175,24 @@ export function parseDeviceName(ua = '') {
168
175
  return `💻 Linux (${browser})`;
169
176
  return `📱 移动设备 (${browser})`;
170
177
  }
178
+ const activeStores = new Set();
179
+ if (typeof process !== 'undefined' && typeof process.on === 'function') {
180
+ process.on('beforeExit', () => {
181
+ for (const s of activeStores) {
182
+ try {
183
+ s.flushPersistedData();
184
+ }
185
+ catch { }
186
+ }
187
+ });
188
+ }
171
189
  export class SessionStore extends EventEmitter {
172
190
  sessions = new Map();
173
191
  shortCodes = new Map();
174
192
  ipStats = new Map();
175
193
  options;
176
194
  settingsMutator;
195
+ debounceTimer = null;
177
196
  persistPath;
178
197
  settingsFilePath;
179
198
  constructor(options = {}) {
@@ -211,6 +230,7 @@ export class SessionStore extends EventEmitter {
211
230
  };
212
231
  this.persistPath = this.options.devicesFile;
213
232
  this.loadPersistedData();
233
+ activeStores.add(this);
214
234
  }
215
235
  getOptions() {
216
236
  return { ...this.options };
@@ -378,7 +398,7 @@ export class SessionStore extends EventEmitter {
378
398
  }
379
399
  }
380
400
  this.emit('ip-security-updated', { stats: this.getIpSecurityStats() });
381
- this.savePersistedData();
401
+ this.savePersistedDataDebounced();
382
402
  return { allowed: true };
383
403
  }
384
404
  /**
@@ -625,7 +645,7 @@ export class SessionStore extends EventEmitter {
625
645
  }
626
646
  if (isReconnecting) {
627
647
  this.emit('device-online', session);
628
- this.savePersistedData();
648
+ this.savePersistedDataDebounced();
629
649
  }
630
650
  return true;
631
651
  }
@@ -770,21 +790,6 @@ export class SessionStore extends EventEmitter {
770
790
  }
771
791
  }
772
792
  else if (data && typeof data === 'object') {
773
- if (data.config) {
774
- if (typeof data.config.allowTailscale === 'boolean') {
775
- this.options.allowTailscale = data.config.allowTailscale;
776
- }
777
- if (typeof data.config.allowLan === 'boolean') {
778
- this.options.allowLan = data.config.allowLan;
779
- }
780
- if (typeof data.config.secretHash === 'string') {
781
- this.options.secretHash = data.config.secretHash;
782
- }
783
- else if (typeof data.config.secret === 'string' && data.config.secret) {
784
- // 自动将老版本的明文 secret 迁移升级为安全加盐哈希
785
- this.options.secretHash = hashSecret(data.config.secret);
786
- }
787
- }
788
793
  if (Array.isArray(data.devices)) {
789
794
  for (const s of data.devices) {
790
795
  if (s && s.token) {
@@ -831,7 +836,35 @@ export class SessionStore extends EventEmitter {
831
836
  }
832
837
  catch { }
833
838
  }
839
+ /**
840
+ * 同步立即持久化落盘(由关键路径调用:配对成功、密码认证、撤销设备、手动改密/配置等)
841
+ */
834
842
  savePersistedData() {
843
+ this.flushPersistedData();
844
+ }
845
+ /**
846
+ * 500ms 防抖持久化落盘(由高频访问路径调用:每次打开 /auth、validateToken 重连时间戳更新等)
847
+ */
848
+ savePersistedDataDebounced() {
849
+ if (this.debounceTimer)
850
+ return;
851
+ this.debounceTimer = setTimeout(() => {
852
+ this.debounceTimer = null;
853
+ this.flushPersistedData();
854
+ }, 500);
855
+ if (typeof this.debounceTimer.unref === 'function') {
856
+ ;
857
+ this.debounceTimer.unref();
858
+ }
859
+ }
860
+ /**
861
+ * 立即执行待写的持久化数据落盘并清理定时器
862
+ */
863
+ flushPersistedData() {
864
+ if (this.debounceTimer) {
865
+ clearTimeout(this.debounceTimer);
866
+ this.debounceTimer = null;
867
+ }
835
868
  try {
836
869
  mkdirSync(dirname(this.persistPath), { recursive: true });
837
870
  const payload = {
package/lib/client.js CHANGED
@@ -876,20 +876,6 @@ var translations = {
876
876
  }
877
877
  };
878
878
  function resolveLocale(ctx, status) {
879
- try {
880
- if (ctx) {
881
- if (typeof ctx.get === "function") {
882
- var loc = ctx.get("locale");
883
- if (loc && loc.snapshot && loc.snapshot.active) {
884
- return loc.snapshot.active.startsWith("en") ? "en" : "zh";
885
- }
886
- }
887
- if (ctx.locale && ctx.locale.snapshot && ctx.locale.snapshot.active) {
888
- return ctx.locale.snapshot.active.startsWith("en") ? "en" : "zh";
889
- }
890
- }
891
- } catch (e) {
892
- }
893
879
  if (typeof document !== "undefined" && document.documentElement && document.documentElement.lang) {
894
880
  return document.documentElement.lang.toLowerCase().startsWith("en") ? "en" : "zh";
895
881
  }
@@ -914,6 +900,39 @@ function t(key, lang, params) {
914
900
  }
915
901
  return text;
916
902
  }
903
+ function formatDeviceName(name, lang) {
904
+ if (!name) return lang === "en" ? "Mobile Device" : "\u79FB\u52A8\u7AEF\u8BBE\u5907";
905
+ if (lang !== "en") return name;
906
+ return name.replace(/手机/g, "Phone").replace(/移动设备/g, "Mobile Device").replace(/微信内置浏览器/g, "WeChat Browser").replace(/局域网免密临时设备/g, "LAN Bypass Device").replace(/Tailscale免密临时设备/g, "Tailscale Bypass Device");
907
+ }
908
+ function formatAuthType(authType, lang) {
909
+ if (!authType) return lang === "en" ? "\u26AA Pending" : "\u26AA \u5F85\u8BA4\u8BC1";
910
+ if (lang !== "en") return authType;
911
+ if (authType.includes("\u626B\u7801\u914D\u5BF9") || authType.includes("\u914D\u5BF9\u7801")) {
912
+ if (authType.includes("365")) return "\u{1F4F1} QR Pairing (365d)";
913
+ return "\u{1F4F1} QR Authenticated";
914
+ }
915
+ if (authType.includes("\u957F\u671F\u5BC6\u7801") || authType.includes("\u5BC6\u7801\u9A8C\u8BC1")) {
916
+ if (authType.includes("365")) return "\u{1F511} Password Auth (365d)";
917
+ return "\u{1F511} Password Auth";
918
+ }
919
+ if (authType.includes("\u5C40\u57DF\u7F51") || authType.includes("LAN")) {
920
+ return "\u26A1 LAN Direct Bypass";
921
+ }
922
+ if (authType.includes("Tailscale")) {
923
+ return "\u26A1 Tailscale Bypass";
924
+ }
925
+ if (authType.includes("\u5F85\u8BA4\u8BC1") || authType.includes("\u672A\u8BA4\u8BC1")) {
926
+ return "\u26AA Pending";
927
+ }
928
+ return authType;
929
+ }
930
+ function formatFailedAttempts(count, lang) {
931
+ if (lang === "en") {
932
+ return count === 1 ? "\u26A0\uFE0F 1 failed attempt" : `\u26A0\uFE0F ${count} failed attempts`;
933
+ }
934
+ return `\u26A0\uFE0F \u66FE\u5931\u8D25 ${count} \u6B21`;
935
+ }
917
936
 
918
937
  // src/client/components/NetworkCard.jsx
919
938
  var import_jsx_runtime = require("react/jsx-runtime");
@@ -958,20 +977,19 @@ function NetworkCard({ status, toggleTailscale, toggleLan, copyText, lang }) {
958
977
  "div",
959
978
  {
960
979
  style: {
961
- display: "flex",
962
- alignItems: "center",
963
- justifyContent: "space-between",
964
- flexWrap: "wrap",
965
- gap: "12px",
966
980
  paddingBottom: "16px",
967
981
  borderBottom: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.12))",
968
- marginBottom: "16px"
982
+ marginBottom: "16px",
983
+ display: "flex",
984
+ justifyContent: "space-between",
985
+ alignItems: "flex-start",
986
+ gap: "16px"
969
987
  },
970
988
  children: [
971
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { flex: 1, minWidth: "220px" }, children: [
972
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: "8px" }, children: [
973
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { fontSize: "16px" }, children: "\u{1F512}" }),
974
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { fontSize: "14px", fontWeight: "700" }, children: t("tailscaleSectionTitle", lang) }),
989
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { flex: 1, minWidth: 0, display: "flex", flexDirection: "column", gap: "6px" }, children: [
990
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: "8px", flexWrap: "wrap" }, children: [
991
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { fontSize: "16px", flexShrink: 0 }, children: "\u{1F512}" }),
992
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { fontSize: "14px", fontWeight: "700", color: "var(--dsw-alias-label-primary, inherit)" }, children: t("tailscaleSectionTitle", lang) }),
975
993
  status.tailscaleIp ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
976
994
  "span",
977
995
  {
@@ -981,7 +999,9 @@ function NetworkCard({ status, toggleTailscale, toggleLan, copyText, lang }) {
981
999
  borderRadius: "4px",
982
1000
  background: "rgba(16,185,129,0.15)",
983
1001
  color: "#10b981",
984
- fontWeight: "600"
1002
+ fontWeight: "600",
1003
+ whiteSpace: "nowrap",
1004
+ flexShrink: 0
985
1005
  },
986
1006
  children: lang === "en" ? "Connected" : "\u5DF2\u8FDE\u63A5"
987
1007
  }
@@ -994,7 +1014,9 @@ function NetworkCard({ status, toggleTailscale, toggleLan, copyText, lang }) {
994
1014
  borderRadius: "4px",
995
1015
  background: "rgba(239,68,68,0.15)",
996
1016
  color: "#ef4444",
997
- fontWeight: "600"
1017
+ fontWeight: "600",
1018
+ whiteSpace: "nowrap",
1019
+ flexShrink: 0
998
1020
  },
999
1021
  children: lang === "en" ? "Not Connected" : "\u672A\u8FDE\u63A5"
1000
1022
  }
@@ -1005,26 +1027,30 @@ function NetworkCard({ status, toggleTailscale, toggleLan, copyText, lang }) {
1005
1027
  {
1006
1028
  style: {
1007
1029
  fontSize: "13px",
1008
- marginTop: "4px",
1009
1030
  fontFamily: "monospace",
1010
- color: "var(--dsw-alias-brand-primary, #3b82f6)"
1031
+ color: "var(--dsw-alias-brand-primary, #3b82f6)",
1032
+ display: "flex",
1033
+ alignItems: "center",
1034
+ flexWrap: "wrap",
1035
+ gap: "8px"
1011
1036
  },
1012
1037
  children: [
1013
- `http://${tsHost}:${port}`,
1038
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: `http://${tsHost}:${port}` }),
1014
1039
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1015
1040
  "button",
1016
1041
  {
1017
1042
  type: "button",
1018
1043
  onClick: () => copyText(`http://${tsHost}:${port}`, t("copiedTip", lang)),
1019
1044
  style: {
1020
- marginLeft: "8px",
1021
- padding: "2px 6px",
1045
+ padding: "2px 8px",
1022
1046
  background: "transparent",
1023
- border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.2))",
1047
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.25))",
1024
1048
  borderRadius: "4px",
1025
1049
  fontSize: "11px",
1026
1050
  cursor: "pointer",
1027
- color: "var(--dsw-alias-label-secondary, inherit)"
1051
+ color: "var(--dsw-alias-label-secondary, inherit)",
1052
+ whiteSpace: "nowrap",
1053
+ flexShrink: 0
1028
1054
  },
1029
1055
  children: t("copyUrlBtn", lang)
1030
1056
  }
@@ -1037,7 +1063,7 @@ function NetworkCard({ status, toggleTailscale, toggleLan, copyText, lang }) {
1037
1063
  style: {
1038
1064
  fontSize: "12px",
1039
1065
  color: "var(--dsw-alias-label-tertiary, #888)",
1040
- marginTop: "4px"
1066
+ lineHeight: "1.4"
1041
1067
  },
1042
1068
  children: t("tailscaleGuide", lang)
1043
1069
  }
@@ -1048,20 +1074,21 @@ function NetworkCard({ status, toggleTailscale, toggleLan, copyText, lang }) {
1048
1074
  style: {
1049
1075
  fontSize: "12px",
1050
1076
  color: "var(--dsw-alias-label-tertiary, #888)",
1051
- marginTop: "4px"
1077
+ lineHeight: "1.4"
1052
1078
  },
1053
1079
  children: t("tailscaleBypassDesc", lang)
1054
1080
  }
1055
1081
  ) : null
1056
1082
  ] }),
1057
- status.tailscaleIp ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: "8px" }, children: [
1083
+ status.tailscaleIp ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: "8px", flexShrink: 0, paddingTop: "2px" }, children: [
1058
1084
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1059
1085
  "span",
1060
1086
  {
1061
1087
  style: {
1062
1088
  fontSize: "12px",
1063
1089
  color: status.allowTailscale ? "var(--dsw-alias-brand-primary, #3b82f6)" : "var(--dsw-alias-label-secondary, #888)",
1064
- fontWeight: "600"
1090
+ fontWeight: "600",
1091
+ whiteSpace: "nowrap"
1065
1092
  },
1066
1093
  children: status.allowTailscale ? t("directBypassOn", lang) : t("directBypassOff", lang)
1067
1094
  }
@@ -1075,7 +1102,8 @@ function NetworkCard({ status, toggleTailscale, toggleLan, copyText, lang }) {
1075
1102
  width: "44px",
1076
1103
  height: "24px",
1077
1104
  cursor: "pointer",
1078
- userSelect: "none"
1105
+ userSelect: "none",
1106
+ flexShrink: 0
1079
1107
  },
1080
1108
  children: [
1081
1109
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
@@ -1132,16 +1160,15 @@ function NetworkCard({ status, toggleTailscale, toggleLan, copyText, lang }) {
1132
1160
  {
1133
1161
  style: {
1134
1162
  display: "flex",
1135
- alignItems: "center",
1136
1163
  justifyContent: "space-between",
1137
- flexWrap: "wrap",
1138
- gap: "12px"
1164
+ alignItems: "flex-start",
1165
+ gap: "16px"
1139
1166
  },
1140
1167
  children: [
1141
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { flex: 1, minWidth: "220px" }, children: [
1142
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: "8px" }, children: [
1143
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { fontSize: "16px" }, children: "\u{1F3E0}" }),
1144
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { fontSize: "14px", fontWeight: "700" }, children: t("lanSectionTitle", lang) }),
1168
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { flex: 1, minWidth: 0, display: "flex", flexDirection: "column", gap: "6px" }, children: [
1169
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: "8px", flexWrap: "wrap" }, children: [
1170
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { fontSize: "16px", flexShrink: 0 }, children: "\u{1F3E0}" }),
1171
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { fontSize: "14px", fontWeight: "700", color: "var(--dsw-alias-label-primary, inherit)" }, children: t("lanSectionTitle", lang) }),
1145
1172
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1146
1173
  "span",
1147
1174
  {
@@ -1151,7 +1178,9 @@ function NetworkCard({ status, toggleTailscale, toggleLan, copyText, lang }) {
1151
1178
  borderRadius: "4px",
1152
1179
  background: "rgba(16,185,129,0.15)",
1153
1180
  color: "#10b981",
1154
- fontWeight: "600"
1181
+ fontWeight: "600",
1182
+ whiteSpace: "nowrap",
1183
+ flexShrink: 0
1155
1184
  },
1156
1185
  children: lang === "en" ? "Ready" : "\u5DF2\u5C31\u7EEA"
1157
1186
  }
@@ -1162,26 +1191,30 @@ function NetworkCard({ status, toggleTailscale, toggleLan, copyText, lang }) {
1162
1191
  {
1163
1192
  style: {
1164
1193
  fontSize: "13px",
1165
- marginTop: "4px",
1166
1194
  fontFamily: "monospace",
1167
- color: "var(--dsw-alias-brand-primary, #3b82f6)"
1195
+ color: "var(--dsw-alias-brand-primary, #3b82f6)",
1196
+ display: "flex",
1197
+ alignItems: "center",
1198
+ flexWrap: "wrap",
1199
+ gap: "8px"
1168
1200
  },
1169
1201
  children: [
1170
- `http://${lanHost}:${port}`,
1202
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: `http://${lanHost}:${port}` }),
1171
1203
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1172
1204
  "button",
1173
1205
  {
1174
1206
  type: "button",
1175
1207
  onClick: () => copyText(`http://${lanHost}:${port}`, t("copiedTip", lang)),
1176
1208
  style: {
1177
- marginLeft: "8px",
1178
- padding: "2px 6px",
1209
+ padding: "2px 8px",
1179
1210
  background: "transparent",
1180
- border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.2))",
1211
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.25))",
1181
1212
  borderRadius: "4px",
1182
1213
  fontSize: "11px",
1183
1214
  cursor: "pointer",
1184
- color: "var(--dsw-alias-label-secondary, inherit)"
1215
+ color: "var(--dsw-alias-label-secondary, inherit)",
1216
+ whiteSpace: "nowrap",
1217
+ flexShrink: 0
1185
1218
  },
1186
1219
  children: t("copyUrlBtn", lang)
1187
1220
  }
@@ -1190,7 +1223,7 @@ function NetworkCard({ status, toggleTailscale, toggleLan, copyText, lang }) {
1190
1223
  }
1191
1224
  )
1192
1225
  ] }),
1193
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: "8px" }, children: [
1226
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: "8px", flexShrink: 0, paddingTop: "2px" }, children: [
1194
1227
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1195
1228
  "span",
1196
1229
  {
@@ -1200,9 +1233,10 @@ function NetworkCard({ status, toggleTailscale, toggleLan, copyText, lang }) {
1200
1233
  fontWeight: status.allowLan ? "700" : "500",
1201
1234
  display: "flex",
1202
1235
  alignItems: "center",
1203
- gap: "4px"
1236
+ gap: "4px",
1237
+ whiteSpace: "nowrap"
1204
1238
  },
1205
- children: status.allowLan ? lang === "en" ? "\u{1F6A8} LAN Bypass Enabled (HIGH RISK)" : "\u{1F6A8} \u5C40\u57DF\u7F51\u514D\u5BC6\u5DF2\u5F00\u542F\uFF08\u9AD8\u5371\u72B6\u6001\uFF09" : t("directBypassOff", lang)
1239
+ children: status.allowLan ? lang === "en" ? "\u{1F6A8} LAN Bypass (High Risk)" : "\u{1F6A8} \u5C40\u57DF\u7F51\u514D\u5BC6\uFF08\u9AD8\u5371\uFF09" : t("directBypassOff", lang)
1206
1240
  }
1207
1241
  ),
1208
1242
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
@@ -1214,7 +1248,8 @@ function NetworkCard({ status, toggleTailscale, toggleLan, copyText, lang }) {
1214
1248
  width: "44px",
1215
1249
  height: "24px",
1216
1250
  cursor: "pointer",
1217
- userSelect: "none"
1251
+ userSelect: "none",
1252
+ flexShrink: 0
1218
1253
  },
1219
1254
  children: [
1220
1255
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
@@ -1526,13 +1561,14 @@ function QrPairingCard({
1526
1561
  onClick: copyDirectLink,
1527
1562
  style: {
1528
1563
  padding: "6px 16px",
1529
- background: "var(--dsw-alias-brand-primary, #3b82f6)",
1530
- color: "white",
1564
+ background: "#2563eb",
1565
+ color: "#ffffff",
1531
1566
  border: "none",
1532
1567
  borderRadius: "6px",
1533
1568
  fontSize: "12px",
1534
1569
  fontWeight: "600",
1535
1570
  cursor: "pointer",
1571
+ boxShadow: "0 2px 6px rgba(37, 99, 235, 0.3)",
1536
1572
  transition: "background-color 0.2s"
1537
1573
  },
1538
1574
  children: status.copied ? `\u2705 ${lang === "en" ? "Copied!" : "\u5DF2\u590D\u5236\uFF01"}` : t("copyDirectAuthLink", lang)
@@ -1828,15 +1864,16 @@ function SecretCard({
1828
1864
  style: {
1829
1865
  padding: "0 16px",
1830
1866
  height: "38px",
1831
- background: "var(--dsw-alias-brand-primary, #3b82f6)",
1832
- color: "white",
1867
+ background: "#2563eb",
1868
+ color: "#ffffff",
1833
1869
  border: "none",
1834
1870
  borderRadius: "8px",
1835
1871
  fontSize: "13px",
1836
1872
  fontWeight: "600",
1837
1873
  cursor: "pointer",
1838
1874
  whiteSpace: "nowrap",
1839
- flexShrink: 0
1875
+ flexShrink: 0,
1876
+ boxShadow: "0 2px 6px rgba(37, 99, 235, 0.3)"
1840
1877
  },
1841
1878
  children: t("saveSecretBtn", lang)
1842
1879
  }
@@ -1990,7 +2027,7 @@ function DeviceListCard({ status, revokeDevice, revokeAll, lang }) {
1990
2027
  gap: "8px"
1991
2028
  },
1992
2029
  children: [
1993
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: dev.deviceName || (lang === "en" ? "Mobile Device" : "\u79FB\u52A8\u7AEF\u8BBE\u5907") }),
2030
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: formatDeviceName(dev.deviceName, lang) }),
1994
2031
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1995
2032
  "span",
1996
2033
  {
@@ -2003,7 +2040,7 @@ function DeviceListCard({ status, revokeDevice, revokeAll, lang }) {
2003
2040
  fontWeight: "normal",
2004
2041
  whiteSpace: "nowrap"
2005
2042
  },
2006
- children: dev.authType
2043
+ children: formatAuthType(dev.authType, lang)
2007
2044
  }
2008
2045
  )
2009
2046
  ]
@@ -2257,18 +2294,15 @@ function SecurityCard({ status, unlockIp, clearIpStats, lang }) {
2257
2294
  flexWrap: "wrap"
2258
2295
  },
2259
2296
  children: [
2260
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { style: { display: "inline-flex", alignItems: "center", gap: "6px" }, children: [
2261
- effectiveDeviceName ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: { color: "var(--dsw-alias-label-primary, inherit)" }, children: effectiveDeviceName }) : null,
2262
- effectiveDeviceName ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: { color: "var(--dsw-alias-label-tertiary, #888)", fontWeight: "normal" }, children: "\xB7" }) : null,
2263
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: { color: effectiveDeviceName ? "var(--dsw-alias-label-secondary, #888)" : "inherit" }, children: ipLabel })
2264
- ] }),
2297
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: { fontWeight: "600" }, children: stat.deviceName ? `${formatDeviceName(stat.deviceName, lang)} \xB7 ${stat.ip}${isLocal ? lang === "en" ? " (Local)" : " (\u672C\u673A)" : ""}` : `${stat.ip}${isLocal ? lang === "en" ? " (Local)" : " (\u672C\u673A)" : ""}` }),
2265
2298
  effectiveAuthType ? (() => {
2266
- let bg = "rgba(148,163,184,0.15)";
2267
- let color = "#94a3b8";
2299
+ const translatedAuth = formatAuthType(effectiveAuthType, lang);
2300
+ let bg = "rgba(59,130,246,0.12)";
2301
+ let color = "#3b82f6";
2268
2302
  if (effectiveAuthType.indexOf("\u514D\u5BC6") !== -1 || effectiveAuthType.indexOf("Bypass") !== -1) {
2269
2303
  bg = "rgba(16,185,129,0.15)";
2270
2304
  color = "#10b981";
2271
- } else if (effectiveAuthType.indexOf("\u626B\u7801") !== -1 || effectiveAuthType.indexOf("Pairing") !== -1) {
2305
+ } else if (effectiveAuthType.indexOf("\u626B\u7801") !== -1 || effectiveAuthType.indexOf("Pairing") !== -1 || effectiveAuthType.indexOf("QR") !== -1) {
2272
2306
  bg = "rgba(59,130,246,0.15)";
2273
2307
  color = "#3b82f6";
2274
2308
  } else if (effectiveAuthType.indexOf("\u5BC6\u7801") !== -1 || effectiveAuthType.indexOf("Password") !== -1) {
@@ -2287,7 +2321,7 @@ function SecurityCard({ status, unlockIp, clearIpStats, lang }) {
2287
2321
  fontWeight: "600",
2288
2322
  whiteSpace: "nowrap"
2289
2323
  },
2290
- children: effectiveAuthType
2324
+ children: translatedAuth
2291
2325
  }
2292
2326
  );
2293
2327
  })() : null,
@@ -2317,7 +2351,7 @@ function SecurityCard({ status, unlockIp, clearIpStats, lang }) {
2317
2351
  fontWeight: "600",
2318
2352
  whiteSpace: "nowrap"
2319
2353
  },
2320
- children: lang === "en" ? `\u26A0\uFE0F ${stat.failedAttempts} failed attempts` : `\u26A0\uFE0F \u66FE\u5931\u8D25 ${stat.failedAttempts} \u6B21`
2354
+ children: formatFailedAttempts(stat.failedAttempts, lang)
2321
2355
  }
2322
2356
  ) : !effectiveAuthType ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2323
2357
  "span",
@@ -2834,15 +2868,16 @@ function ConfigCard({
2834
2868
  disabled: isSaving,
2835
2869
  style: {
2836
2870
  padding: "6px 18px",
2837
- background: "var(--dsw-alias-brand-primary, #3b82f6)",
2871
+ background: "#2563eb",
2838
2872
  border: "none",
2839
- color: "white",
2873
+ color: "#ffffff",
2840
2874
  borderRadius: "6px",
2841
2875
  fontSize: "12px",
2842
2876
  fontWeight: "600",
2843
2877
  cursor: isSaving ? "not-allowed" : "pointer",
2844
2878
  opacity: isSaving ? 0.7 : 1,
2845
- whiteSpace: "nowrap"
2879
+ whiteSpace: "nowrap",
2880
+ boxShadow: "0 2px 6px rgba(37, 99, 235, 0.3)"
2846
2881
  },
2847
2882
  children: isSaving ? t("savingConfigBtn", lang) : t("saveConfigBtn", lang)
2848
2883
  }
@@ -2856,7 +2891,8 @@ function ConfigCard({
2856
2891
  }
2857
2892
 
2858
2893
  // src/client/version.js
2859
- var PLUGIN_VERSION = "v1.1.0";
2894
+ var PLUGIN_VERSION = "v1.2.1";
2895
+ var BUILD_TIME = "2026-08-23 00:31:17";
2860
2896
 
2861
2897
  // src/client/components/MainSection.jsx
2862
2898
  var import_jsx_runtime8 = require("react/jsx-runtime");
@@ -2892,6 +2928,7 @@ function TailscaleMobileSection(props) {
2892
2928
  const [maxFailedInput, setMaxFailedInput] = (0, import_react8.useState)("5");
2893
2929
  const [lockDurationMinsInput, setLockDurationMinsInput] = (0, import_react8.useState)("15");
2894
2930
  const [isSaving, setIsSaving] = (0, import_react8.useState)(false);
2931
+ const [showVersionTip, setShowVersionTip] = (0, import_react8.useState)(false);
2895
2932
  const [currentLang, setCurrentLang] = (0, import_react8.useState)(resolveLocale(ctx, status));
2896
2933
  const refreshStatusOnly = (0, import_react8.useCallback)(() => {
2897
2934
  fetchStatus().then((data) => {
@@ -3256,18 +3293,63 @@ function TailscaleMobileSection(props) {
3256
3293
  children: t("title", currentLang)
3257
3294
  }
3258
3295
  ),
3259
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3260
- "span",
3296
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
3297
+ "div",
3261
3298
  {
3262
- style: {
3263
- fontSize: "11px",
3264
- padding: "2px 7px",
3265
- borderRadius: "10px",
3266
- background: "rgba(59,130,246,0.15)",
3267
- color: "var(--dsw-alias-brand-primary, #3b82f6)",
3268
- fontWeight: "600"
3299
+ style: { position: "relative", display: "inline-flex", alignItems: "center" },
3300
+ onMouseEnter: () => setShowVersionTip(true),
3301
+ onMouseLeave: () => setShowVersionTip(false),
3302
+ onClick: () => {
3303
+ setToast({
3304
+ message: `\u{1F4E6} dsh-remote-mobile ${PLUGIN_VERSION} (${currentLang === "en" ? "Build: " : "\u6784\u5EFA\u65F6\u95F4: "}${BUILD_TIME})`,
3305
+ type: "info"
3306
+ });
3307
+ setTimeout(() => setToast(null), 3e3);
3269
3308
  },
3270
- children: PLUGIN_VERSION
3309
+ children: [
3310
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3311
+ "span",
3312
+ {
3313
+ style: {
3314
+ fontSize: "11px",
3315
+ padding: "2px 7px",
3316
+ borderRadius: "10px",
3317
+ background: "rgba(59,130,246,0.15)",
3318
+ color: "var(--dsw-alias-brand-primary, #3b82f6)",
3319
+ fontWeight: "600",
3320
+ cursor: "pointer",
3321
+ userSelect: "none",
3322
+ transition: "all 0.2s ease"
3323
+ },
3324
+ children: PLUGIN_VERSION
3325
+ }
3326
+ ),
3327
+ showVersionTip ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3328
+ "div",
3329
+ {
3330
+ style: {
3331
+ position: "absolute",
3332
+ top: "calc(100% + 6px)",
3333
+ left: "50%",
3334
+ transform: "translateX(-50%)",
3335
+ backgroundColor: "#0f172a",
3336
+ color: "#f8fafc",
3337
+ padding: "5px 10px",
3338
+ borderRadius: "6px",
3339
+ fontSize: "11px",
3340
+ fontWeight: "500",
3341
+ whiteSpace: "nowrap",
3342
+ boxShadow: "0 8px 16px rgba(0,0,0,0.3)",
3343
+ border: "1px solid rgba(255,255,255,0.15)",
3344
+ zIndex: 9999,
3345
+ pointerEvents: "none",
3346
+ lineHeight: "1.4",
3347
+ textAlign: "center"
3348
+ },
3349
+ children: `${currentLang === "en" ? "Build: " : "\u6253\u5305\u65F6\u95F4: "}${BUILD_TIME}`
3350
+ }
3351
+ ) : null
3352
+ ]
3271
3353
  }
3272
3354
  )
3273
3355
  ]
@@ -3307,7 +3389,7 @@ function TailscaleMobileSection(props) {
3307
3389
  },
3308
3390
  children: [
3309
3391
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("svg", { style: { width: "16px", height: "16px", fill: "currentColor" }, viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: "M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" }) }),
3310
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { children: "IceApriler/dsh-remote-mobile" })
3392
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { children: "dsh-remote-mobile" })
3311
3393
  ]
3312
3394
  }
3313
3395
  )