@guowenzhang/dsh-pocket 0.0.0-stage → 2.12.2
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/LICENSE +344 -0
- package/README.en.md +222 -0
- package/README.md +229 -3
- package/bin/dsh-pocket.mjs +216 -0
- package/client/api.js +76 -0
- package/client/build.mjs +51 -0
- package/client/client.js +2765 -0
- package/client/index.jsx +581 -0
- package/client/mobile/LICENSE.dsh-web-mobile +4 -0
- package/client/mobile/MobileDrawerFooter.tsx +69 -0
- package/client/mobile/MobileIcon.tsx +31 -0
- package/client/mobile/MobileNavOverlay.tsx +299 -0
- package/client/mobile/MobileNavToggle.tsx +57 -0
- package/client/mobile/fileGuard.ts +200 -0
- package/client/mobile/icon-compat.mjs +90 -0
- package/client/mobile/layout-mode.mjs +46 -0
- package/client/mobile/locales.ts +23 -0
- package/client/mobile/mobile-apply.tsx +413 -0
- package/client/mobile/mobile.css.ts +1052 -0
- package/client/mobile/nav-targets.mjs +117 -0
- package/client/pocket-locales.js +165 -0
- package/cordis.patch.yml +5 -0
- package/lib/index.js +420 -0
- package/lib/ip.mjs +11 -0
- package/lib/proxy.mjs +1223 -0
- package/lib/restart.js +118 -0
- package/lib/service.mjs +454 -0
- package/lib/settings.mjs +252 -0
- package/lib/tunnel.mjs +636 -0
- package/lib/web-rpc.js +475 -0
- package/package.json +85 -4
package/lib/settings.mjs
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
// dsh-pocket 设置持久化($DSH_HOME/dsh-pocket/settings.json)
|
|
2
|
+
//
|
|
3
|
+
// 当前项:
|
|
4
|
+
// - lanEnabled 局域网访问总开关(默认开启):关闭后局域网扫码/链接直接失效(代理拒绝局域网 Host)
|
|
5
|
+
// - lanAuthEnabled 局域网访问密码开关(issue #24),默认开启
|
|
6
|
+
// - mobileRightbarEnabled 手机端右边栏入口(默认开启)
|
|
7
|
+
// - publicPinCustom 公网密码是否用户自定义(issue #33),自定义后不自动轮换
|
|
8
|
+
// - lanPinCustom 局域网密码是否用户自定义(issue #33)
|
|
9
|
+
// - tunnelMode 公网隧道模式(issue #66):'quick'(默认,随机 trycloudflare.com)| 'named'(固定域名)
|
|
10
|
+
// - tunnelToken Cloudflare 命名隧道 Token(issue #66,秘密;文件 0o600,RPC 不回显)
|
|
11
|
+
// - tunnelHostname 命名隧道绑定的固定域名(issue #66,如 pocket.example.com)
|
|
12
|
+
// 默认**开启**(安全优先):局域网扫码也要输 8 位密码;
|
|
13
|
+
// 用户可关闭——关闭后局域网扫码直连(仅同一网络内的设备能访问),公网不受影响(永远要密码)。
|
|
14
|
+
|
|
15
|
+
import { readFileSync, writeFileSync, mkdirSync, rmSync } from 'node:fs';
|
|
16
|
+
import { join, dirname } from 'node:path';
|
|
17
|
+
import { homedir } from 'node:os';
|
|
18
|
+
import { randomBytes } from 'node:crypto';
|
|
19
|
+
import { isValidIpv4 } from './ip.mjs';
|
|
20
|
+
|
|
21
|
+
const settingsRel = join('dsh-pocket', 'settings.json');
|
|
22
|
+
export function settingsPath() {
|
|
23
|
+
return join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), settingsRel);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function readSettings() {
|
|
27
|
+
try {
|
|
28
|
+
const raw = JSON.parse(readFileSync(settingsPath(), 'utf8'));
|
|
29
|
+
return raw && typeof raw === 'object' ? raw : {};
|
|
30
|
+
} catch { /* 无文件/损坏 → 默认 */ }
|
|
31
|
+
return {};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function writeSettings(s) {
|
|
35
|
+
try {
|
|
36
|
+
mkdirSync(dirname(settingsPath()), { recursive: true });
|
|
37
|
+
writeFileSync(settingsPath(), JSON.stringify(s, null, 2), { mode: 0o600 });
|
|
38
|
+
} catch { /* 忽略 */ }
|
|
39
|
+
return s;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 局域网访问总开关:默认开启(文件缺失/损坏都视为开启)。 */
|
|
43
|
+
export function lanEnabled() {
|
|
44
|
+
return readSettings().lanEnabled !== false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 设置局域网访问总开关,返回新状态(持久化)。 */
|
|
48
|
+
export function setLanEnabled(on) {
|
|
49
|
+
const s = readSettings();
|
|
50
|
+
s.lanEnabled = !!on;
|
|
51
|
+
writeSettings(s);
|
|
52
|
+
return s.lanEnabled;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** 局域网访问密码开关:默认开启(文件缺失/损坏都视为开启)。 */
|
|
56
|
+
export function lanAuthEnabled() {
|
|
57
|
+
return readSettings().lanAuthEnabled !== false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** 设置局域网访问密码开关,返回新状态(持久化)。 */
|
|
61
|
+
export function setLanAuthEnabled(on) {
|
|
62
|
+
const s = readSettings();
|
|
63
|
+
s.lanAuthEnabled = !!on;
|
|
64
|
+
writeSettings(s);
|
|
65
|
+
return s.lanAuthEnabled;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 手机端右边栏入口:默认开启,可按需关闭以保持更紧凑的标题栏。 */
|
|
69
|
+
export function mobileRightbarEnabled() {
|
|
70
|
+
return readSettings().mobileRightbarEnabled !== false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** 设置手机端右边栏入口,返回新状态(持久化)。 */
|
|
74
|
+
export function setMobileRightbarEnabled(on) {
|
|
75
|
+
const s = readSettings();
|
|
76
|
+
s.mobileRightbarEnabled = !!on;
|
|
77
|
+
writeSettings(s);
|
|
78
|
+
return s.mobileRightbarEnabled;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** 局域网地址手动覆盖:默认空字符串 = 自动选择。 */
|
|
82
|
+
export function lanIpOverride() {
|
|
83
|
+
return readSettings().lanIpOverride ?? '';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** 设置局域网地址覆盖;空字符串清除覆盖,恢复自动选择。非法 IPv4 抛错。 */
|
|
87
|
+
export function setLanIpOverride(value) {
|
|
88
|
+
const ip = String(value ?? '').trim();
|
|
89
|
+
if (ip && !isValidIpv4(ip)) {
|
|
90
|
+
throw new Error('局域网地址必须是 IPv4 地址 | LAN address must be an IPv4 address');
|
|
91
|
+
}
|
|
92
|
+
const s = readSettings();
|
|
93
|
+
if (ip) s.lanIpOverride = ip;
|
|
94
|
+
else delete s.lanIpOverride;
|
|
95
|
+
writeSettings(s);
|
|
96
|
+
return ip;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ---------- 访问密码「自定义」标记(issue #33) ----------
|
|
100
|
+
// 用户可把公网/局域网密码设成自己固定的 8–64 位密码(英文字母大小写或数字;自定义后不再自动轮换)。
|
|
101
|
+
// 标记存 settings.json:publicPinCustom / lanPinCustom。
|
|
102
|
+
const PIN_CUSTOM_KEYS = { public: 'publicPinCustom', lan: 'lanPinCustom' };
|
|
103
|
+
|
|
104
|
+
/** 该 PIN(public | lan)是否用户自定义过(自定义后不自动轮换)。 */
|
|
105
|
+
export function pinCustom(which) {
|
|
106
|
+
const key = PIN_CUSTOM_KEYS[which];
|
|
107
|
+
if (!key) return false;
|
|
108
|
+
return readSettings()[key] === true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** 设置自定义标记,返回新状态。 */
|
|
112
|
+
export function setPinCustom(which, on) {
|
|
113
|
+
const key = PIN_CUSTOM_KEYS[which];
|
|
114
|
+
if (!key) return false;
|
|
115
|
+
const s = readSettings();
|
|
116
|
+
s[key] = !!on;
|
|
117
|
+
writeSettings(s);
|
|
118
|
+
return !!on;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ---------- 恢复出厂设置 ----------
|
|
122
|
+
// 设置出问题时的临时兜底:删掉 settings.json 即回到出厂默认(文件缺失 = 默认开启,
|
|
123
|
+
// 于是局域网访问开、访问密码开、手机端右边栏开、局域网地址自动、公网模式随机)。
|
|
124
|
+
// DSH 自身的会话、模型、插件配置都在 $DSH_HOME 的其他目录,不受影响。
|
|
125
|
+
|
|
126
|
+
/** 删除本机设置文件(不存在也算成功);返回 true 表示已清空。 */
|
|
127
|
+
export function resetSettings() {
|
|
128
|
+
try {
|
|
129
|
+
rmSync(settingsPath(), { force: true });
|
|
130
|
+
return true;
|
|
131
|
+
} catch {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ---------- 命名隧道配置(issue #66:固定公网域名) ----------
|
|
137
|
+
// 用户在 Cloudflare Zero Trust(Networks → Tunnels)创建命名隧道并复制 Tunnel Token,
|
|
138
|
+
// 把自己域名的 ingress Service 指向 http://127.0.0.1:<代理端口>;填到这里后,
|
|
139
|
+
// 开启公网改用 `cloudflared tunnel run`,公网地址固定为该域名(重启不再变化)。
|
|
140
|
+
// token 是长期凭据:只存本机 0o600 文件,RPC 只写不读(回显仅 tokenSet 布尔值)。
|
|
141
|
+
|
|
142
|
+
/** 隧道模式:'quick'(默认,随机地址)| 'named'(固定域名)。 */
|
|
143
|
+
export function tunnelMode() {
|
|
144
|
+
return readSettings().tunnelMode === 'named' ? 'named' : 'quick';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** 设置隧道模式(持久化);非法值抛错。 */
|
|
148
|
+
export function setTunnelMode(mode) {
|
|
149
|
+
if (mode !== 'quick' && mode !== 'named') {
|
|
150
|
+
throw new Error('隧道模式必须是 quick 或 named | tunnel mode must be quick or named');
|
|
151
|
+
}
|
|
152
|
+
const s = readSettings();
|
|
153
|
+
if (mode === 'quick') delete s.tunnelMode;
|
|
154
|
+
else s.tunnelMode = mode;
|
|
155
|
+
writeSettings(s);
|
|
156
|
+
return mode;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** 命名隧道 Token(未配置返回空字符串)。 */
|
|
160
|
+
export function tunnelToken() {
|
|
161
|
+
const v = readSettings().tunnelToken;
|
|
162
|
+
return typeof v === 'string' ? v : '';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* 设置命名隧道 Token(持久化)。空字符串清除;非空要求至少 20 个
|
|
167
|
+
* base64url 字符(Cloudflare Token 是长 base64 串,过短/含空白视为无效)。
|
|
168
|
+
*/
|
|
169
|
+
export function setTunnelToken(value) {
|
|
170
|
+
const v = String(value ?? '').trim();
|
|
171
|
+
if (v) {
|
|
172
|
+
if (v.length < 20 || !/^[A-Za-z0-9+/_=-]+$/.test(v)) {
|
|
173
|
+
throw new Error('Tunnel Token 格式不对(应为 Cloudflare 后台复制的完整 Token) | invalid tunnel token');
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const s = readSettings();
|
|
177
|
+
if (v) s.tunnelToken = v;
|
|
178
|
+
else delete s.tunnelToken;
|
|
179
|
+
writeSettings(s);
|
|
180
|
+
return v;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** 命名隧道绑定的固定域名(未配置返回空字符串)。 */
|
|
184
|
+
export function tunnelHostname() {
|
|
185
|
+
const v = readSettings().tunnelHostname;
|
|
186
|
+
return typeof v === 'string' ? v : '';
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* 设置固定域名(持久化)。接受 `https://host/path` 粘贴并归一化为裸域名;
|
|
191
|
+
* 空字符串清除。要求是带点的合法公网域名(局域网主机名/裸 IP 不允许——
|
|
192
|
+
* 那不该走公网密码边界之外的东西)。
|
|
193
|
+
*/
|
|
194
|
+
export function setTunnelHostname(value) {
|
|
195
|
+
let v = String(value ?? '').trim().toLowerCase();
|
|
196
|
+
v = v.replace(/^[a-z][a-z0-9+.-]*:\/\//, '').split(/[/?#\s]/)[0].replace(/:\d+$/, '').replace(/\.$/, '');
|
|
197
|
+
if (v) {
|
|
198
|
+
const HOSTNAME_RE = /^(?=.{1,253}$)[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)+$/;
|
|
199
|
+
const IS_IPV4 = /^\d{1,3}(\.\d{1,3}){3}$/;
|
|
200
|
+
if (IS_IPV4.test(v) || !v.includes('.') || !HOSTNAME_RE.test(v)) {
|
|
201
|
+
throw new Error('固定域名格式不对(如 pocket.example.com) | invalid tunnel hostname');
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const s = readSettings();
|
|
205
|
+
if (v) s.tunnelHostname = v;
|
|
206
|
+
else delete s.tunnelHostname;
|
|
207
|
+
writeSettings(s);
|
|
208
|
+
return v;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ---------- 代理端口(issue #70) ----------
|
|
212
|
+
// 局域网代理的监听端口(默认 3081)。插件模式下唯一改法就是写 settings.json 的
|
|
213
|
+
// proxyPort 字段(CLI 模式可用 dsh-pocket --port)。允许范围 1-65535;
|
|
214
|
+
// 端口已被占用时 dsh web 启动会直接抛 EADDRINUSE(保持原行为),不必在 setter 校验。
|
|
215
|
+
/** 当前代理端口(0 = 用默认 3081)。 */
|
|
216
|
+
export function proxyPort() {
|
|
217
|
+
const v = Number(readSettings().proxyPort);
|
|
218
|
+
return Number.isInteger(v) && v >= 1 && v <= 65535 ? v : 0;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** 设置代理端口(持久化)。空/0/非法值清除,回退默认 3081。 */
|
|
222
|
+
export function setProxyPort(value) {
|
|
223
|
+
const n = Number(value);
|
|
224
|
+
const s = readSettings();
|
|
225
|
+
if (Number.isInteger(n) && n >= 1 && n <= 65535) s.proxyPort = n;
|
|
226
|
+
else delete s.proxyPort;
|
|
227
|
+
writeSettings(s);
|
|
228
|
+
return proxyPort();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ---------- cloudflared 路径(issue #45:远程 Linux 服务器下载源不可达时手动指定) ----------
|
|
232
|
+
// Linux 服务器在国内/部分企业网下,所有 CDN 源(GitHub / ghproxy / gh.ddlc / gh-proxy)
|
|
233
|
+
// 都连不上时,下载 cloudflared 二进制始终失败。允许用户在 settings.json 里**写死
|
|
234
|
+
// 一个已经存在 / 自己上传的 cloudflared 路径**,跳过下载。`lib/tunnel.mjs` 的
|
|
235
|
+
// `resolveCloudflared` 启动时会优先读 `process.env.DSH_POCKET_CLOUDFLARED`,
|
|
236
|
+
// 找不到再回退到 PATH 探测和下载。`lib/index.js` 在插件 apply 时把 settings
|
|
237
|
+
// 的这个值写入 env,确保 service 走自定义路径。
|
|
238
|
+
// 空字符串 = 清除,回退到默认行为(PATH 探测 + 下载)。
|
|
239
|
+
/** 当前 cloudflared 自定义路径(空 = 用默认)。 */
|
|
240
|
+
export function cloudflaredPath() {
|
|
241
|
+
return readSettings().cloudflaredPath ?? '';
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** 设置 cloudflared 自定义路径。空字符串清除(回退到 PATH 探测 + 下载)。 */
|
|
245
|
+
export function setCloudflaredPath(value) {
|
|
246
|
+
const v = String(value ?? '').trim();
|
|
247
|
+
const s = readSettings();
|
|
248
|
+
if (v) s.cloudflaredPath = v;
|
|
249
|
+
else delete s.cloudflaredPath;
|
|
250
|
+
writeSettings(s);
|
|
251
|
+
return v;
|
|
252
|
+
}
|