dsh-pocket 2.0.0 → 2.1.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/api.js +1 -0
- package/client/client.js +88 -6
- package/client/index.jsx +68 -6
- package/client/pocket-locales.js +16 -0
- package/lib/index.js +21 -1
- package/lib/service.mjs +1 -1
- package/lib/settings.mjs +16 -1
- package/lib/tunnel.mjs +2 -2
- package/lib/web-rpc.js +16 -1
- package/package.json +1 -1
package/client/api.js
CHANGED
package/client/client.js
CHANGED
|
@@ -53,7 +53,8 @@ var POCKET_ENDPOINTS = Object.freeze({
|
|
|
53
53
|
lanAuthSetEnabled: "lanAuth.setEnabled",
|
|
54
54
|
lanSetOverride: "lan.setOverride",
|
|
55
55
|
lanSetEnabled: "lan.setEnabled",
|
|
56
|
-
pinSetCustom: "pin.setCustom"
|
|
56
|
+
pinSetCustom: "pin.setCustom",
|
|
57
|
+
pocketReset: "pocket.reset"
|
|
57
58
|
});
|
|
58
59
|
function compareVersions(a, b) {
|
|
59
60
|
const pa = String(a).replace(/^[vV]/, "").split(".");
|
|
@@ -1370,6 +1371,14 @@ var zh2 = {
|
|
|
1370
1371
|
"modeLabel": "\u5730\u5740\u6A21\u5F0F",
|
|
1371
1372
|
"advAddress": "\u9AD8\u7EA7 \xB7 \u624B\u52A8\u9009\u5730\u5740",
|
|
1372
1373
|
"wanOffHint": "\u5F00\u542F\u540E\u53EF\u4ECE\u4EFB\u4F55\u7F51\u7EDC\u8BBF\u95EE\uFF08\u6BCF\u6B21\u5F00\u542F\u9700\u786E\u8BA4\u514D\u8D23\u58F0\u660E\uFF09",
|
|
1374
|
+
"resetFactory": "\u{1F9F9} \u6062\u590D\u51FA\u5382\u8BBE\u7F6E",
|
|
1375
|
+
"resetGo": "\u6062\u590D",
|
|
1376
|
+
"resetIntro": "\u8BBE\u7F6E\u641E\u51FA\u95EE\u9898\u65F6\u7684\u4E34\u65F6\u515C\u5E95\uFF1A\u6E05\u7A7A\u672C\u673A\u914D\u7F6E\u5E76\u91CD\u8BBE\u968F\u673A\u5BC6\u7801\uFF08DSH \u7684\u4F1A\u8BDD\u3001\u6A21\u578B\u3001\u63D2\u4EF6\u914D\u7F6E\u4E0D\u53D7\u5F71\u54CD\uFF09",
|
|
1377
|
+
"resetTitle": "\u26A0\uFE0F \u786E\u8BA4\u6062\u590D\u51FA\u5382\u8BBE\u7F6E\uFF1F",
|
|
1378
|
+
"resetBody": "\u5C06\u6E05\u7A7A\u5E76\u6062\u590D\u9ED8\u8BA4\uFF1A\n\u2460 \u5F00\u5173\uFF1A\u5C40\u57DF\u7F51\u8BBF\u95EE=\u5F00\u3001\u8BBF\u95EE\u5BC6\u7801=\u5F00\u3001\u5C40\u57DF\u7F51\u5730\u5740=\u81EA\u52A8\n\u2461 \u516C\u7F51\uFF1A\u6A21\u5F0F\u56DE\u5230\u968F\u673A\u57DF\u540D\uFF0C\u6E05\u7A7A Tunnel Token \u4E0E\u56FA\u5B9A\u57DF\u540D\uFF0C\u5E76\u5173\u95ED\u6B63\u5728\u8FD0\u884C\u7684\u516C\u7F51\n\u2462 \u5BC6\u7801\uFF1A\u516C\u7F51\u548C\u5C40\u57DF\u7F51\u90FD\u6362\u6210\u65B0\u7684\u968F\u673A 8 \u4F4D\u5BC6\u7801\uFF08\u65E7\u5BC6\u7801\u7ACB\u5373\u4F5C\u5E9F\uFF0C\u624B\u673A\u9700\u91CD\u65B0\u8F93\u5165\uFF09\n\nDSH \u81EA\u8EAB\u7684\u4F1A\u8BDD\u3001\u6A21\u578B\u3001\u63D2\u4EF6\u914D\u7F6E\u4E0D\u53D7\u5F71\u54CD\uFF1B\u6B64\u64CD\u4F5C\u4E0D\u53EF\u64A4\u9500\u3002",
|
|
1379
|
+
"resetConfirm": "\u786E\u8BA4\u6062\u590D",
|
|
1380
|
+
"resetDone": "\u2705 \u5DF2\u6062\u590D\u51FA\u5382\u8BBE\u7F6E\uFF1A\u8BBE\u7F6E\u5DF2\u6E05\u7A7A\uFF0C\u5BC6\u7801\u5DF2\u6362\u65B0\uFF08\u624B\u673A\u9700\u91CD\u65B0\u8F93\u5165\uFF09",
|
|
1381
|
+
"resetFailed": "\u274C \u6062\u590D\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5",
|
|
1373
1382
|
"lanTitle": "\u{1F4F6} \u5C40\u57DF\u7F51\uFF08\u540C\u4E00 WiFi\uFF09",
|
|
1374
1383
|
"lanHint": "\u624B\u673A\u8FDE\u63A5\u540C\u4E00 WiFi \u540E\u626B\u7801\u5373\u53EF\u6253\u5F00",
|
|
1375
1384
|
"lanAccess": "\u5C40\u57DF\u7F51\u8BBF\u95EE",
|
|
@@ -1457,6 +1466,14 @@ var en2 = {
|
|
|
1457
1466
|
"modeLabel": "Address mode",
|
|
1458
1467
|
"advAddress": "Advanced \xB7 Pick address",
|
|
1459
1468
|
"wanOffHint": "Reachable from any network once enabled (a disclaimer is confirmed on each enable)",
|
|
1469
|
+
"resetFactory": "\u{1F9F9} Factory reset",
|
|
1470
|
+
"resetGo": "Reset",
|
|
1471
|
+
"resetIntro": "Temporary fallback when settings break: clear local config and re-roll random PINs (DSH sessions, models and plugin config are untouched)",
|
|
1472
|
+
"resetTitle": "\u26A0\uFE0F Confirm factory reset?",
|
|
1473
|
+
"resetBody": "This clears and restores defaults:\n\u2460 Switches: LAN access on, access PIN on, LAN address auto\n\u2461 Public: mode back to random URL, Tunnel Token and fixed domain cleared, and any running tunnel is stopped\n\u2462 PINs: both public and LAN become new random 8-character PINs (old ones stop working; the phone must re-enter)\n\nYour DSH sessions, models and plugin config are untouched. This cannot be undone.",
|
|
1474
|
+
"resetConfirm": "Reset",
|
|
1475
|
+
"resetDone": "\u2705 Factory reset done: settings cleared and PINs re-rolled (re-enter the PIN on your phone)",
|
|
1476
|
+
"resetFailed": "\u274C Reset failed \u2014 please retry",
|
|
1460
1477
|
"lanTitle": "\u{1F4F6} LAN (same Wi-Fi)",
|
|
1461
1478
|
"lanHint": "Scan to open once your phone is on the same Wi-Fi",
|
|
1462
1479
|
"lanAccess": "LAN access",
|
|
@@ -1702,6 +1719,24 @@ function PocketSettingsTab({ rpcCall, t }) {
|
|
|
1702
1719
|
setTunnelCfg((c) => ({ ...c, err: err.message }));
|
|
1703
1720
|
}
|
|
1704
1721
|
};
|
|
1722
|
+
const [resetOpen, setResetOpen] = (0, import_react2.useState)(false);
|
|
1723
|
+
const doFactoryReset = async () => {
|
|
1724
|
+
setResetOpen(false);
|
|
1725
|
+
setBusy(true);
|
|
1726
|
+
setError(null);
|
|
1727
|
+
try {
|
|
1728
|
+
setStatus(await call(POCKET_ENDPOINTS.pocketReset, { confirm: true }));
|
|
1729
|
+
setTunnelCfg(null);
|
|
1730
|
+
setCustomPin(null);
|
|
1731
|
+
setAdvOpen(false);
|
|
1732
|
+
showToast(t("resetDone"));
|
|
1733
|
+
} catch (err) {
|
|
1734
|
+
setError(err.message);
|
|
1735
|
+
showToast(t("resetFailed"));
|
|
1736
|
+
} finally {
|
|
1737
|
+
setBusy(false);
|
|
1738
|
+
}
|
|
1739
|
+
};
|
|
1705
1740
|
const refreshLanPin = async () => {
|
|
1706
1741
|
try {
|
|
1707
1742
|
const r = await call(POCKET_ENDPOINTS.lanTokenRefresh, {});
|
|
@@ -1770,7 +1805,7 @@ function PocketSettingsTab({ rpcCall, t }) {
|
|
|
1770
1805
|
}),
|
|
1771
1806
|
(0, import_react2.createElement)("button", { style: { ...styles.btn, height: 26, padding: "0 10px", fontSize: 12, marginLeft: 2 }, onClick: () => saveCustomPin(which) }, t("save")),
|
|
1772
1807
|
(0, import_react2.createElement)("button", { style: { ...styles.btn, height: 26, padding: "0 10px", fontSize: 12 }, onClick: () => setCustomPin(null) }, t("cancel")),
|
|
1773
|
-
customPin?.err ? (0, import_react2.createElement)("div", { style: { color: "var(--dsw-alias-state-error-primary,#dc2626)", marginTop: 4 } }, customPin.err) : null
|
|
1808
|
+
customPin?.err ? (0, import_react2.createElement)("div", { style: { color: "var(--dsw-alias-state-error-primary,#dc2626)", marginTop: 4 } }, errText(customPin.err)) : null
|
|
1774
1809
|
);
|
|
1775
1810
|
const customBtn = (which) => (0, import_react2.createElement)("button", { style: { ...styles.btn, height: 26, padding: "0 10px", fontSize: 12, marginLeft: 8 }, onClick: () => setCustomPin({ which, value: "", err: null }) }, t("customize"));
|
|
1776
1811
|
const lanUrl = status?.lanUrl;
|
|
@@ -1782,6 +1817,20 @@ function PocketSettingsTab({ rpcCall, t }) {
|
|
|
1782
1817
|
const tunnelModeView = status?.tunnelConfig ?? { mode: "quick", hostname: "", tokenSet: false };
|
|
1783
1818
|
const namedMode = tunnelModeView.mode === "named";
|
|
1784
1819
|
const namedActive = namedMode || tunnelCfg !== null;
|
|
1820
|
+
const errText = (msg) => {
|
|
1821
|
+
const s = String(msg ?? "");
|
|
1822
|
+
const i = s.indexOf(" | ");
|
|
1823
|
+
if (i < 0) return s;
|
|
1824
|
+
return (t("ok") === zh2.ok ? s.slice(0, i) : s.slice(i + 3)).trim();
|
|
1825
|
+
};
|
|
1826
|
+
const [toast, setToast] = (0, import_react2.useState)(null);
|
|
1827
|
+
const toastTimer = (0, import_react2.useRef)(null);
|
|
1828
|
+
const showToast = (text) => {
|
|
1829
|
+
setToast(text);
|
|
1830
|
+
clearTimeout(toastTimer.current);
|
|
1831
|
+
toastTimer.current = setTimeout(() => setToast(null), 2600);
|
|
1832
|
+
};
|
|
1833
|
+
(0, import_react2.useEffect)(() => () => clearTimeout(toastTimer.current), []);
|
|
1785
1834
|
const modeBtnStyle = (active) => ({
|
|
1786
1835
|
...styles.btn,
|
|
1787
1836
|
height: 28,
|
|
@@ -1871,7 +1920,7 @@ function PocketSettingsTab({ rpcCall, t }) {
|
|
|
1871
1920
|
(0, import_react2.createElement)(
|
|
1872
1921
|
"div",
|
|
1873
1922
|
{ style: styles.muted, marginTop: 4 },
|
|
1874
|
-
updateInfo.updating ? fmt(t, "updatingDetail", { s: elapsed(updateInfo.startedAt) }) : updateInfo.restarting ? fmt(t, "restartingDetail", { s: elapsed(updateInfo.startedAt) }) : updateInfo.result === "ok" ? updateInfo.autoRestart ? t("updatedAutoDetail") : t("updatedRestartDetail") : updateInfo.result === "fail" ? fmt(t, "updateFailed", { err: updateInfo.output || t("unknownError") }) : fmt(t, "versionRange", { cur: updateInfo.current, latest: updateInfo.latest })
|
|
1923
|
+
updateInfo.updating ? fmt(t, "updatingDetail", { s: elapsed(updateInfo.startedAt) }) : updateInfo.restarting ? fmt(t, "restartingDetail", { s: elapsed(updateInfo.startedAt) }) : updateInfo.result === "ok" ? updateInfo.autoRestart ? t("updatedAutoDetail") : t("updatedRestartDetail") : updateInfo.result === "fail" ? fmt(t, "updateFailed", { err: errText(updateInfo.output) || t("unknownError") }) : fmt(t, "versionRange", { cur: updateInfo.current, latest: updateInfo.latest })
|
|
1875
1924
|
)
|
|
1876
1925
|
) : null,
|
|
1877
1926
|
// 局域网:标题行自带总开关 → 二维码+地址 → 设置行(访问密码 / 高级·手动选地址)
|
|
@@ -1948,7 +1997,7 @@ function PocketSettingsTab({ rpcCall, t }) {
|
|
|
1948
1997
|
) : tunnelPhase === "error" ? (0, import_react2.createElement)(
|
|
1949
1998
|
"div",
|
|
1950
1999
|
{ style: { marginTop: 8, fontSize: 12, color: "var(--dsw-alias-state-error-primary,#dc2626)" } },
|
|
1951
|
-
fmt(t, "error", { detail: tunnelStateDetail || t("unknownError") })
|
|
2000
|
+
fmt(t, "error", { detail: errText(tunnelStateDetail) || t("unknownError") })
|
|
1952
2001
|
) : !tunnelUrl && !isDesktop ? (0, import_react2.createElement)("div", { style: { ...styles.muted, marginTop: 8 } }, t("wanOffHint")) : null,
|
|
1953
2002
|
tunnelUrl ? (0, import_react2.createElement)(
|
|
1954
2003
|
"div",
|
|
@@ -2020,7 +2069,7 @@ function PocketSettingsTab({ rpcCall, t }) {
|
|
|
2020
2069
|
),
|
|
2021
2070
|
(0, import_react2.createElement)("div", { style: { ...styles.muted, marginTop: 6 } }, t("namedHow")),
|
|
2022
2071
|
(0, import_react2.createElement)("div", { style: { marginTop: 2, fontSize: 11, color: "var(--dsw-alias-state-warn-primary,#b45309)", lineHeight: 1.5 } }, t("namedSecurity")),
|
|
2023
|
-
tunnelCfg.err ? (0, import_react2.createElement)("div", { style: { color: "var(--dsw-alias-state-error-primary,#dc2626)", marginTop: 4 } }, tunnelCfg.err) : null
|
|
2072
|
+
tunnelCfg.err ? (0, import_react2.createElement)("div", { style: { color: "var(--dsw-alias-state-error-primary,#dc2626)", marginTop: 4 } }, errText(tunnelCfg.err)) : null
|
|
2024
2073
|
) : null
|
|
2025
2074
|
)
|
|
2026
2075
|
),
|
|
@@ -2043,7 +2092,40 @@ function PocketSettingsTab({ rpcCall, t }) {
|
|
|
2043
2092
|
) : null
|
|
2044
2093
|
) : null
|
|
2045
2094
|
),
|
|
2046
|
-
error ? (0, import_react2.createElement)("div", { style: { color: "var(--dsw-alias-state-error-primary,#dc2626)", fontSize: 12, marginTop: 8 } }, `\u274C ${error}`) : null,
|
|
2095
|
+
error ? (0, import_react2.createElement)("div", { style: { color: "var(--dsw-alias-state-error-primary,#dc2626)", fontSize: 12, marginTop: 8 } }, `\u274C ${errText(error)}`) : null,
|
|
2096
|
+
// 恢复出厂设置:设置出问题时的临时兜底(最底部,避免误触)
|
|
2097
|
+
(0, import_react2.createElement)(
|
|
2098
|
+
"div",
|
|
2099
|
+
{ style: styles.block },
|
|
2100
|
+
(0, import_react2.createElement)(
|
|
2101
|
+
"div",
|
|
2102
|
+
{ style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 } },
|
|
2103
|
+
(0, import_react2.createElement)("span", { style: { fontWeight: 600, fontSize: 13 } }, t("resetFactory")),
|
|
2104
|
+
(0, import_react2.createElement)("button", { style: { ...styles.btn, height: 28, padding: "0 12px", fontSize: 12, color: "var(--dsw-alias-state-error-primary,#dc2626)" }, onClick: () => setResetOpen(true) }, t("resetGo"))
|
|
2105
|
+
),
|
|
2106
|
+
(0, import_react2.createElement)("div", { style: { ...styles.muted, marginTop: 6 } }, t("resetIntro"))
|
|
2107
|
+
),
|
|
2108
|
+
// 恢复出厂设置确认弹框
|
|
2109
|
+
resetOpen ? (0, import_react2.createElement)(
|
|
2110
|
+
"div",
|
|
2111
|
+
{ style: { position: "fixed", inset: 0, zIndex: 1e4, background: "rgba(0,0,0,.5)", display: "flex", alignItems: "center", justifyContent: "center", padding: 20 } },
|
|
2112
|
+
(0, import_react2.createElement)(
|
|
2113
|
+
"div",
|
|
2114
|
+
{ style: { background: "var(--dsw-alias-bg-layer-1,#fff)", borderRadius: 12, maxWidth: 440, width: "100%", padding: "20px 22px", boxShadow: "0 8px 32px rgba(0,0,0,.18)" } },
|
|
2115
|
+
(0, import_react2.createElement)("div", { style: { fontWeight: 600, fontSize: 15, color: "var(--dsw-alias-state-warn-primary,#b45309)", marginBottom: 10 } }, t("resetTitle")),
|
|
2116
|
+
(0, import_react2.createElement)("div", { style: { fontSize: 13, lineHeight: 1.7, color: "var(--dsw-alias-label-primary,inherit)", whiteSpace: "pre-line" } }, t("resetBody")),
|
|
2117
|
+
(0, import_react2.createElement)(
|
|
2118
|
+
"div",
|
|
2119
|
+
{ style: { display: "flex", gap: 8, marginTop: 16 } },
|
|
2120
|
+
(0, import_react2.createElement)("button", { style: { ...styles.btn, flex: 1 }, onClick: () => setResetOpen(false) }, t("cancel")),
|
|
2121
|
+
(0, import_react2.createElement)("button", { style: { ...styles.primary, flex: 1, background: "var(--dsw-alias-state-error-primary,#dc2626)" }, onClick: doFactoryReset }, t("resetConfirm"))
|
|
2122
|
+
)
|
|
2123
|
+
)
|
|
2124
|
+
) : null,
|
|
2125
|
+
// Toast:重置等操作的即时反馈(固定屏幕正中央,2.6s 自动消失)
|
|
2126
|
+
toast ? (0, import_react2.createElement)("div", {
|
|
2127
|
+
style: { position: "fixed", left: "50%", top: "50%", transform: "translate(-50%, -50%)", zIndex: 10001, width: "auto", maxWidth: 280, background: "rgba(17,24,39,.92)", color: "#fff", border: "none", borderRadius: 10, padding: "10px 16px", fontSize: 13, lineHeight: 1.5, textAlign: "center", boxShadow: "0 8px 24px rgba(0,0,0,.22)" }
|
|
2128
|
+
}, toast) : null,
|
|
2047
2129
|
// 局域网访问开关确认弹框(关闭/打开时弹窗提醒)
|
|
2048
2130
|
lanToggleOpen !== null ? (0, import_react2.createElement)(
|
|
2049
2131
|
"div",
|
package/client/index.jsx
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// 注:Web Push 已移除——浏览器推送依赖 Google FCM(Chrome)等境外服务,
|
|
8
8
|
// 国内直连被墙,普通用户用不了。专注扫码同屏这一件事。
|
|
9
9
|
|
|
10
|
-
import { createElement as h, useEffect, useState } from 'react';
|
|
10
|
+
import { createElement as h, useEffect, useRef, useState } from 'react';
|
|
11
11
|
|
|
12
12
|
import { POCKET_RPC_CHANNEL, POCKET_ENDPOINTS, redactStatus, compareVersions } from './api.js';
|
|
13
13
|
import { mobileApply } from './mobile/mobile-apply.tsx';
|
|
@@ -220,6 +220,26 @@ function PocketSettingsTab({ rpcCall, t }) {
|
|
|
220
220
|
}
|
|
221
221
|
};
|
|
222
222
|
|
|
223
|
+
// 恢复出厂设置:清本机设置 + 重设随机密码(弹窗确认;RPC 端也强制校验 confirm)
|
|
224
|
+
const [resetOpen, setResetOpen] = useState(false);
|
|
225
|
+
const doFactoryReset = async () => {
|
|
226
|
+
setResetOpen(false);
|
|
227
|
+
setBusy(true);
|
|
228
|
+
setError(null);
|
|
229
|
+
try {
|
|
230
|
+
setStatus(await call(POCKET_ENDPOINTS.pocketReset, { confirm: true }));
|
|
231
|
+
setTunnelCfg(null);
|
|
232
|
+
setCustomPin(null);
|
|
233
|
+
setAdvOpen(false);
|
|
234
|
+
showToast(t('resetDone'));
|
|
235
|
+
} catch (err) {
|
|
236
|
+
setError(err.message);
|
|
237
|
+
showToast(t('resetFailed'));
|
|
238
|
+
} finally {
|
|
239
|
+
setBusy(false);
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
|
|
223
243
|
// 刷新局域网访问密码(旧密码立即作废)
|
|
224
244
|
const refreshLanPin = async () => {
|
|
225
245
|
try {
|
|
@@ -293,7 +313,7 @@ function PocketSettingsTab({ rpcCall, t }) {
|
|
|
293
313
|
}),
|
|
294
314
|
h('button', { style: { ...styles.btn, height: 26, padding: '0 10px', fontSize: 12, marginLeft: 2 }, onClick: () => saveCustomPin(which) }, t('save')),
|
|
295
315
|
h('button', { style: { ...styles.btn, height: 26, padding: '0 10px', fontSize: 12 }, onClick: () => setCustomPin(null) }, t('cancel')),
|
|
296
|
-
customPin?.err ? h('div', { style: { color: 'var(--dsw-alias-state-error-primary,#dc2626)', marginTop: 4 } }, customPin.err) : null,
|
|
316
|
+
customPin?.err ? h('div', { style: { color: 'var(--dsw-alias-state-error-primary,#dc2626)', marginTop: 4 } }, errText(customPin.err)) : null,
|
|
297
317
|
);
|
|
298
318
|
// 「自定义」按钮(非输入态显示在密码行末尾)
|
|
299
319
|
const customBtn = (which) => h('button', { style: { ...styles.btn, height: 26, padding: '0 10px', fontSize: 12, marginLeft: 8 }, onClick: () => setCustomPin({ which, value: '', err: null }) }, t('customize'));
|
|
@@ -309,6 +329,22 @@ function PocketSettingsTab({ rpcCall, t }) {
|
|
|
309
329
|
const namedMode = tunnelModeView.mode === 'named';
|
|
310
330
|
// 模式按钮选中态高亮:固定域名模式本身,或正在编辑固定域名配置,都视为「选中」
|
|
311
331
|
const namedActive = namedMode || tunnelCfg !== null;
|
|
332
|
+
// 后端错误消息统一为「中文 | English」混排;按当前界面语言只显示对应一半
|
|
333
|
+
const errText = (msg) => {
|
|
334
|
+
const s = String(msg ?? '');
|
|
335
|
+
const i = s.indexOf(' | ');
|
|
336
|
+
if (i < 0) return s;
|
|
337
|
+
return (t('ok') === POCKET_ZH.ok ? s.slice(0, i) : s.slice(i + 3)).trim();
|
|
338
|
+
};
|
|
339
|
+
// 轻量 Toast:操作成功/失败后短暂提示(自动消失,不打断操作)
|
|
340
|
+
const [toast, setToast] = useState(null);
|
|
341
|
+
const toastTimer = useRef(null);
|
|
342
|
+
const showToast = (text) => {
|
|
343
|
+
setToast(text);
|
|
344
|
+
clearTimeout(toastTimer.current);
|
|
345
|
+
toastTimer.current = setTimeout(() => setToast(null), 2600);
|
|
346
|
+
};
|
|
347
|
+
useEffect(() => () => clearTimeout(toastTimer.current), []);
|
|
312
348
|
const modeBtnStyle = (active) => ({
|
|
313
349
|
...styles.btn, height: 28, padding: '0 12px', fontSize: 12,
|
|
314
350
|
fontWeight: active ? 600 : 400,
|
|
@@ -382,7 +418,7 @@ function PocketSettingsTab({ rpcCall, t }) {
|
|
|
382
418
|
: updateInfo.result === 'ok'
|
|
383
419
|
? (updateInfo.autoRestart ? t('updatedAutoDetail')
|
|
384
420
|
: t('updatedRestartDetail'))
|
|
385
|
-
: updateInfo.result === 'fail' ? fmt(t, 'updateFailed', { err: updateInfo.output || t('unknownError') })
|
|
421
|
+
: updateInfo.result === 'fail' ? fmt(t, 'updateFailed', { err: errText(updateInfo.output) || t('unknownError') })
|
|
386
422
|
: fmt(t, 'versionRange', { cur: updateInfo.current, latest: updateInfo.latest })),
|
|
387
423
|
) : null,
|
|
388
424
|
|
|
@@ -445,7 +481,7 @@ function PocketSettingsTab({ rpcCall, t }) {
|
|
|
445
481
|
: fmt(t, 'connecting', { s: elapsed(tunnelStateStarted), suffix: elapsed(tunnelStateStarted) > 30 ? t('slowHint') : '' }))
|
|
446
482
|
: tunnelPhase === 'error'
|
|
447
483
|
? h('div', { style: { marginTop: 8, fontSize: 12, color: 'var(--dsw-alias-state-error-primary,#dc2626)' } },
|
|
448
|
-
fmt(t, 'error', { detail: tunnelStateDetail || t('unknownError') }))
|
|
484
|
+
fmt(t, 'error', { detail: errText(tunnelStateDetail) || t('unknownError') }))
|
|
449
485
|
: (!tunnelUrl && !isDesktop ? h('div', { style: { ...styles.muted, marginTop: 8 } }, t('wanOffHint')) : null),
|
|
450
486
|
tunnelUrl
|
|
451
487
|
? h('div', null,
|
|
@@ -495,7 +531,7 @@ function PocketSettingsTab({ rpcCall, t }) {
|
|
|
495
531
|
),
|
|
496
532
|
h('div', { style: { ...styles.muted, marginTop: 6 } }, t('namedHow')),
|
|
497
533
|
h('div', { style: { marginTop: 2, fontSize: 11, color: 'var(--dsw-alias-state-warn-primary,#b45309)', lineHeight: 1.5 } }, t('namedSecurity')),
|
|
498
|
-
tunnelCfg.err ? h('div', { style: { color: 'var(--dsw-alias-state-error-primary,#dc2626)', marginTop: 4 } }, tunnelCfg.err) : null,
|
|
534
|
+
tunnelCfg.err ? h('div', { style: { color: 'var(--dsw-alias-state-error-primary,#dc2626)', marginTop: 4 } }, errText(tunnelCfg.err)) : null,
|
|
499
535
|
) : null,
|
|
500
536
|
),
|
|
501
537
|
),
|
|
@@ -516,7 +552,33 @@ function PocketSettingsTab({ rpcCall, t }) {
|
|
|
516
552
|
: null,
|
|
517
553
|
),
|
|
518
554
|
|
|
519
|
-
error ? h('div', { style: { color: 'var(--dsw-alias-state-error-primary,#dc2626)', fontSize: 12, marginTop: 8 } }, `❌ ${error}`) : null,
|
|
555
|
+
error ? h('div', { style: { color: 'var(--dsw-alias-state-error-primary,#dc2626)', fontSize: 12, marginTop: 8 } }, `❌ ${errText(error)}`) : null,
|
|
556
|
+
|
|
557
|
+
// 恢复出厂设置:设置出问题时的临时兜底(最底部,避免误触)
|
|
558
|
+
h('div', { style: styles.block },
|
|
559
|
+
h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 } },
|
|
560
|
+
h('span', { style: { fontWeight: 600, fontSize: 13 } }, t('resetFactory')),
|
|
561
|
+
h('button', { style: { ...styles.btn, height: 28, padding: '0 12px', fontSize: 12, color: 'var(--dsw-alias-state-error-primary,#dc2626)' }, onClick: () => setResetOpen(true) }, t('resetGo')),
|
|
562
|
+
),
|
|
563
|
+
h('div', { style: { ...styles.muted, marginTop: 6 } }, t('resetIntro')),
|
|
564
|
+
),
|
|
565
|
+
|
|
566
|
+
// 恢复出厂设置确认弹框
|
|
567
|
+
resetOpen ? h('div', { style: { position: 'fixed', inset: 0, zIndex: 10000, background: 'rgba(0,0,0,.5)', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 20 } },
|
|
568
|
+
h('div', { style: { background: 'var(--dsw-alias-bg-layer-1,#fff)', borderRadius: 12, maxWidth: 440, width: '100%', padding: '20px 22px', boxShadow: '0 8px 32px rgba(0,0,0,.18)' } },
|
|
569
|
+
h('div', { style: { fontWeight: 600, fontSize: 15, color: 'var(--dsw-alias-state-warn-primary,#b45309)', marginBottom: 10 } }, t('resetTitle')),
|
|
570
|
+
h('div', { style: { fontSize: 13, lineHeight: 1.7, color: 'var(--dsw-alias-label-primary,inherit)', whiteSpace: 'pre-line' } }, t('resetBody')),
|
|
571
|
+
h('div', { style: { display: 'flex', gap: 8, marginTop: 16 } },
|
|
572
|
+
h('button', { style: { ...styles.btn, flex: 1 }, onClick: () => setResetOpen(false) }, t('cancel')),
|
|
573
|
+
h('button', { style: { ...styles.primary, flex: 1, background: 'var(--dsw-alias-state-error-primary,#dc2626)' }, onClick: doFactoryReset }, t('resetConfirm')),
|
|
574
|
+
),
|
|
575
|
+
),
|
|
576
|
+
) : null,
|
|
577
|
+
|
|
578
|
+
// Toast:重置等操作的即时反馈(固定屏幕正中央,2.6s 自动消失)
|
|
579
|
+
toast ? h('div', {
|
|
580
|
+
style: { position: 'fixed', left: '50%', top: '50%', transform: 'translate(-50%, -50%)', zIndex: 10001, width: 'auto', maxWidth: 280, background: 'rgba(17,24,39,.92)', color: '#fff', border: 'none', borderRadius: 10, padding: '10px 16px', fontSize: 13, lineHeight: 1.5, textAlign: 'center', boxShadow: '0 8px 24px rgba(0,0,0,.22)' },
|
|
581
|
+
}, toast) : null,
|
|
520
582
|
|
|
521
583
|
// 局域网访问开关确认弹框(关闭/打开时弹窗提醒)
|
|
522
584
|
lanToggleOpen !== null ? h('div', { style: { position: 'fixed', inset: 0, zIndex: 10000, background: 'rgba(0,0,0,.5)', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 20 } },
|
package/client/pocket-locales.js
CHANGED
|
@@ -33,6 +33,14 @@ export const zh = {
|
|
|
33
33
|
'modeLabel': '地址模式',
|
|
34
34
|
'advAddress': '高级 · 手动选地址',
|
|
35
35
|
'wanOffHint': '开启后可从任何网络访问(每次开启需确认免责声明)',
|
|
36
|
+
'resetFactory': '🧹 恢复出厂设置',
|
|
37
|
+
'resetGo': '恢复',
|
|
38
|
+
'resetIntro': '设置搞出问题时的临时兜底:清空本机配置并重设随机密码(DSH 的会话、模型、插件配置不受影响)',
|
|
39
|
+
'resetTitle': '⚠️ 确认恢复出厂设置?',
|
|
40
|
+
'resetBody': '将清空并恢复默认:\n① 开关:局域网访问=开、访问密码=开、局域网地址=自动\n② 公网:模式回到随机域名,清空 Tunnel Token 与固定域名,并关闭正在运行的公网\n③ 密码:公网和局域网都换成新的随机 8 位密码(旧密码立即作废,手机需重新输入)\n\nDSH 自身的会话、模型、插件配置不受影响;此操作不可撤销。',
|
|
41
|
+
'resetConfirm': '确认恢复',
|
|
42
|
+
'resetDone': '✅ 已恢复出厂设置:设置已清空,密码已换新(手机需重新输入)',
|
|
43
|
+
'resetFailed': '❌ 恢复失败,请重试',
|
|
36
44
|
'lanTitle': '📶 局域网(同一 WiFi)',
|
|
37
45
|
'lanHint': '手机连接同一 WiFi 后扫码即可打开',
|
|
38
46
|
'lanAccess': '局域网访问',
|
|
@@ -122,6 +130,14 @@ export const en = {
|
|
|
122
130
|
'modeLabel': 'Address mode',
|
|
123
131
|
'advAddress': 'Advanced · Pick address',
|
|
124
132
|
'wanOffHint': 'Reachable from any network once enabled (a disclaimer is confirmed on each enable)',
|
|
133
|
+
'resetFactory': '🧹 Factory reset',
|
|
134
|
+
'resetGo': 'Reset',
|
|
135
|
+
'resetIntro': 'Temporary fallback when settings break: clear local config and re-roll random PINs (DSH sessions, models and plugin config are untouched)',
|
|
136
|
+
'resetTitle': '⚠️ Confirm factory reset?',
|
|
137
|
+
'resetBody': 'This clears and restores defaults:\n① Switches: LAN access on, access PIN on, LAN address auto\n② Public: mode back to random URL, Tunnel Token and fixed domain cleared, and any running tunnel is stopped\n③ PINs: both public and LAN become new random 8-character PINs (old ones stop working; the phone must re-enter)\n\nYour DSH sessions, models and plugin config are untouched. This cannot be undone.',
|
|
138
|
+
'resetConfirm': 'Reset',
|
|
139
|
+
'resetDone': '✅ Factory reset done: settings cleared and PINs re-rolled (re-enter the PIN on your phone)',
|
|
140
|
+
'resetFailed': '❌ Reset failed — please retry',
|
|
125
141
|
'lanTitle': '📶 LAN (same Wi-Fi)',
|
|
126
142
|
'lanHint': 'Scan to open once your phone is on the same Wi-Fi',
|
|
127
143
|
'lanAccess': 'LAN access',
|
package/lib/index.js
CHANGED
|
@@ -21,7 +21,7 @@ import { createPocketService } from './service.mjs';
|
|
|
21
21
|
import { installPocketRpc } from './web-rpc.js';
|
|
22
22
|
import { restartHost } from './restart.js';
|
|
23
23
|
import { desktopEnvPatchScript, advancedNoticeScript, DEFAULT_INJECT, classifyHost } from './proxy.mjs';
|
|
24
|
-
import { lanEnabled, setLanEnabled, lanAuthEnabled, setLanAuthEnabled, lanIpOverride, setLanIpOverride, pinCustom, setPinCustom, tunnelMode, setTunnelMode, tunnelToken, setTunnelToken, tunnelHostname, setTunnelHostname } from './settings.mjs';
|
|
24
|
+
import { lanEnabled, setLanEnabled, lanAuthEnabled, setLanAuthEnabled, lanIpOverride, setLanIpOverride, pinCustom, setPinCustom, tunnelMode, setTunnelMode, tunnelToken, setTunnelToken, tunnelHostname, setTunnelHostname, resetSettings } from './settings.mjs';
|
|
25
25
|
|
|
26
26
|
const name = 'dsh-pocket';
|
|
27
27
|
const inject = ['connection', 'webServer'];
|
|
@@ -128,6 +128,24 @@ export function setCustomPin(which, value) {
|
|
|
128
128
|
throw new Error('未知密码类型 | unknown PIN kind');
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
/**
|
|
132
|
+
* 恢复出厂设置(设置页底部按钮):清空本机设置 + 重设随机访问密码。
|
|
133
|
+
*
|
|
134
|
+
* 删除 settings.json → 所有开关回到默认(局域网访问开、访问密码开、局域网地址自动、
|
|
135
|
+
* 公网模式随机、固定域名/Tunnel Token 清空);公网与局域网密码都换新随机 8 位值
|
|
136
|
+
* (旧密码立即作废,手机需重新输入)。只动 $DSH_HOME/dsh-pocket/ 下的文件,
|
|
137
|
+
* DSH 自身的会话、模型、插件配置不受影响。
|
|
138
|
+
*
|
|
139
|
+
* 注意:调用方应先停掉正在运行的公网隧道(RPC 层已处理),避免重置后仍按旧模式连接。
|
|
140
|
+
* @returns {{accessToken:string, lanToken:string}} 重置后的新密码
|
|
141
|
+
*/
|
|
142
|
+
export function resetPocketState() {
|
|
143
|
+
resetSettings();
|
|
144
|
+
writePinToFile(tokenPath(), newPin());
|
|
145
|
+
writePinToFile(lanTokenPath(), newPin());
|
|
146
|
+
return { accessToken: getAccessToken(), lanToken: getLanToken() };
|
|
147
|
+
}
|
|
148
|
+
|
|
131
149
|
const restartNoticeRel = join('dsh-pocket', 'restarted.json');
|
|
132
150
|
function restartNoticePath() {
|
|
133
151
|
return join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), restartNoticeRel);
|
|
@@ -283,6 +301,8 @@ export function apply(ctx, config = {}, internals = {}) {
|
|
|
283
301
|
setCustomPin: (which, value) => setCustomPin(which, value),
|
|
284
302
|
// 命名隧道配置(issue #66):RPC 只回显脱敏视图(token 只写不读)
|
|
285
303
|
getTunnelConfig: () => ({ mode: tunnelMode(), hostname: tunnelHostname(), tokenSet: tunnelToken().length > 0 }),
|
|
304
|
+
// 恢复出厂设置:清空设置文件 + 重设随机密码(RPC 层已先停掉公网隧道)
|
|
305
|
+
resetPocket: () => resetPocketState(),
|
|
286
306
|
setTunnelConfig: ({ mode, hostname, token } = {}) => {
|
|
287
307
|
const m = mode === undefined ? tunnelMode() : mode;
|
|
288
308
|
if (m !== 'quick' && m !== 'named') throw new Error('未知隧道模式 | unknown tunnel mode');
|
package/lib/service.mjs
CHANGED
|
@@ -309,7 +309,7 @@ export function createPocketService({
|
|
|
309
309
|
tunnel.onExit?.((code) => {
|
|
310
310
|
if (controller.signal.aborted) return; // 主动停止(stopTunnel)不算故障
|
|
311
311
|
tunnelState.phase = 'error';
|
|
312
|
-
tunnelState.detail = `隧道进程退出(code=${code}
|
|
312
|
+
tunnelState.detail = `隧道进程退出(code=${code}) | tunnel process exited (code=${code})`;
|
|
313
313
|
});
|
|
314
314
|
// 记录「隧道开启中」,供重启后自动恢复(issue #11)
|
|
315
315
|
void persistAutoTunnel();
|
package/lib/settings.mjs
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// 默认**开启**(安全优先):局域网扫码也要输 8 位密码;
|
|
12
12
|
// 用户可关闭——关闭后局域网扫码直连(仅同一网络内的设备能访问),公网不受影响(永远要密码)。
|
|
13
13
|
|
|
14
|
-
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
14
|
+
import { readFileSync, writeFileSync, mkdirSync, rmSync } from 'node:fs';
|
|
15
15
|
import { join, dirname } from 'node:path';
|
|
16
16
|
import { homedir } from 'node:os';
|
|
17
17
|
import { isValidIpv4 } from './ip.mjs';
|
|
@@ -103,6 +103,21 @@ export function setPinCustom(which, on) {
|
|
|
103
103
|
return !!on;
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
// ---------- 恢复出厂设置 ----------
|
|
107
|
+
// 设置出问题时的临时兜底:删掉 settings.json 即回到出厂默认(文件缺失 = 默认开启,
|
|
108
|
+
// 于是局域网访问开、访问密码开、局域网地址自动、公网模式随机)。DSH 自身的会话、
|
|
109
|
+
// 模型、插件配置都在 $DSH_HOME 的其他目录,不受影响。
|
|
110
|
+
|
|
111
|
+
/** 删除本机设置文件(不存在也算成功);返回 true 表示已清空。 */
|
|
112
|
+
export function resetSettings() {
|
|
113
|
+
try {
|
|
114
|
+
rmSync(settingsPath(), { force: true });
|
|
115
|
+
return true;
|
|
116
|
+
} catch {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
106
121
|
// ---------- 命名隧道配置(issue #66:固定公网域名) ----------
|
|
107
122
|
// 用户在 Cloudflare Zero Trust(Networks → Tunnels)创建命名隧道并复制 Tunnel Token,
|
|
108
123
|
// 把自己域名的 ingress Service 指向 http://127.0.0.1:<代理端口>;填到这里后,
|
package/lib/tunnel.mjs
CHANGED
|
@@ -400,8 +400,8 @@ export async function startNamedTunnel({ token, home, signal, onPhase = () => {}
|
|
|
400
400
|
cleanup();
|
|
401
401
|
const tail = buf.trim().split(/\r?\n/).slice(-4).join('\n').trim();
|
|
402
402
|
reject(new Error(
|
|
403
|
-
`cloudflared 退出(code=${code})${tail ? ':' + tail.slice(0, 500) : ''}`
|
|
404
|
-
+
|
|
403
|
+
`cloudflared 退出(code=${code})${tail ? ':' + tail.slice(0, 500) : ''}——请检查 Tunnel Token 是否有效、域名 Service 是否指向本机代理端口 | `
|
|
404
|
+
+ `tunnel exited (code=${code})${tail ? ': ' + tail.slice(0, 500) : ''} — check the Tunnel Token and the ingress hostname`,
|
|
405
405
|
));
|
|
406
406
|
};
|
|
407
407
|
cleanup = () => {
|
package/lib/web-rpc.js
CHANGED
|
@@ -25,7 +25,7 @@ export function killHint(port) {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
/** 注册 /dsh-pocket 逻辑通道(仅本机 loopback 可调)。 */
|
|
28
|
-
export function installPocketRpc(ctx, { service, log = console, desktop = false, runUpdate = null, restart = null, restartNotice = null, getToken = null, getLanToken = null, refreshLanToken = null, getLanAuthEnabled = null, setLanAuthEnabled = null, getLanEnabled = null, setLanEnabled = null, getLanIpOverride = null, setLanIpOverride = null, getPinCustom = null, setCustomPin = null, getTunnelConfig = null, setTunnelConfig = null }) {
|
|
28
|
+
export function installPocketRpc(ctx, { service, log = console, desktop = false, runUpdate = null, restart = null, restartNotice = null, getToken = null, getLanToken = null, refreshLanToken = null, getLanAuthEnabled = null, setLanAuthEnabled = null, getLanEnabled = null, setLanEnabled = null, getLanIpOverride = null, setLanIpOverride = null, getPinCustom = null, setCustomPin = null, getTunnelConfig = null, setTunnelConfig = null, resetPocket = null }) {
|
|
29
29
|
if (!ctx?.connection?.rpc?.handle) {
|
|
30
30
|
log.warn?.('dsh-pocket: DSH Host Connection RPC unavailable — settings tab disabled | 无 Connection RPC,设置页不可用');
|
|
31
31
|
return () => {};
|
|
@@ -106,6 +106,21 @@ export function installPocketRpc(ctx, { service, log = console, desktop = false,
|
|
|
106
106
|
return fail('bad-request', err?.message ?? String(err));
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
|
+
if (endpoint === POCKET_ENDPOINTS.pocketReset) {
|
|
110
|
+
// 恢复出厂设置:必须显式确认(payload.confirm === true),先停隧道再清空设置与密码,
|
|
111
|
+
// 返回完整 status 供前端直接替换(旧密码立即作废,手机需重新输入)。
|
|
112
|
+
if (payload?.confirm !== true) {
|
|
113
|
+
return fail('bad-request', '恢复出厂设置需要确认 | factory reset requires confirmation');
|
|
114
|
+
}
|
|
115
|
+
if (!resetPocket) return fail('bad-request', '恢复出厂设置不可用 | factory reset unavailable');
|
|
116
|
+
try {
|
|
117
|
+
service.stopTunnel();
|
|
118
|
+
resetPocket();
|
|
119
|
+
return await statusPayload();
|
|
120
|
+
} catch (err) {
|
|
121
|
+
return fail('bad-request', err?.message ?? String(err));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
109
124
|
if (endpoint === POCKET_ENDPOINTS.tunnelStart) {
|
|
110
125
|
// 安全免责声明(issue #31):每次开启公网都必须先确认(前端弹框勾选)。
|
|
111
126
|
// 服务端强制校验,防止绕过前端直接调 RPC。
|
package/package.json
CHANGED