dsh-pocket 1.3.2 → 1.4.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/client.js +14 -4
- package/client/index.jsx +14 -4
- package/lib/index.js +12 -0
- package/lib/service.mjs +46 -5
- package/lib/web-rpc.js +7 -3
- package/package.json +1 -1
package/client/client.js
CHANGED
|
@@ -1300,6 +1300,7 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
1300
1300
|
const [tunnelState, setTunnelState] = (0, import_react2.useState)(null);
|
|
1301
1301
|
const [restartNotice, setRestartNotice] = (0, import_react2.useState)(false);
|
|
1302
1302
|
const [updateInfo, setUpdateInfo] = (0, import_react2.useState)(null);
|
|
1303
|
+
const [isDesktop, setIsDesktop] = (0, import_react2.useState)(false);
|
|
1303
1304
|
const [now, setNow] = (0, import_react2.useState)(Date.now());
|
|
1304
1305
|
(0, import_react2.useEffect)(() => {
|
|
1305
1306
|
const t = setInterval(() => setNow(Date.now()), 1e3);
|
|
@@ -1316,6 +1317,7 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
1316
1317
|
const s = await call(POCKET_ENDPOINTS.status, {});
|
|
1317
1318
|
setStatus(s);
|
|
1318
1319
|
setTunnelState(s.tunnelState ?? null);
|
|
1320
|
+
if (s.desktop) setIsDesktop(true);
|
|
1319
1321
|
if (s.restartNotice) {
|
|
1320
1322
|
setRestartNotice(true);
|
|
1321
1323
|
setUpdateInfo(null);
|
|
@@ -1344,6 +1346,7 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
1344
1346
|
}
|
|
1345
1347
|
}, []);
|
|
1346
1348
|
(0, import_react2.useEffect)(() => {
|
|
1349
|
+
if (isDesktop) return;
|
|
1347
1350
|
let alive = true;
|
|
1348
1351
|
(async () => {
|
|
1349
1352
|
try {
|
|
@@ -1362,7 +1365,7 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
1362
1365
|
return () => {
|
|
1363
1366
|
alive = false;
|
|
1364
1367
|
};
|
|
1365
|
-
}, []);
|
|
1368
|
+
}, [isDesktop]);
|
|
1366
1369
|
const restartPocket = async () => {
|
|
1367
1370
|
setUpdateInfo((u) => ({ ...u, restarting: true, startedAt: Date.now() }));
|
|
1368
1371
|
try {
|
|
@@ -1437,8 +1440,14 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
1437
1440
|
"\u5F00\u53D1\u8005\uFF1A\u7A0B\u5E8F\u5458\u5C11\u5317\u6668"
|
|
1438
1441
|
)
|
|
1439
1442
|
),
|
|
1440
|
-
//
|
|
1441
|
-
|
|
1443
|
+
// 桌面端提示:更新/重启由 DSH Desktop 管理,本插件这两项功能已关闭(扫码同屏等照常)
|
|
1444
|
+
isDesktop ? (0, import_react2.createElement)(
|
|
1445
|
+
"div",
|
|
1446
|
+
{ style: { ...styles.block, padding: "8px 0 0" } },
|
|
1447
|
+
(0, import_react2.createElement)("div", { style: styles.muted }, "\u{1F4BB} DSH Desktop \u73AF\u5883\uFF1A\u66F4\u65B0\u4E0E\u91CD\u542F\u7531\u684C\u9762\u7248\u7BA1\u7406\uFF0C\u6B64\u5904\u5DF2\u505C\u7528 | Running inside DSH Desktop: updates & restart are managed by the desktop app")
|
|
1448
|
+
) : null,
|
|
1449
|
+
// 重启后提示(进程在后台运行,停止方法)——左侧蓝色色条(桌面端不会触发本插件的自重启)
|
|
1450
|
+
!isDesktop && restartNotice ? (0, import_react2.createElement)(
|
|
1442
1451
|
"div",
|
|
1443
1452
|
{ style: { ...styles.block, borderLeft: "4px solid var(--dsw-alias-brand-primary,#4f6ef7)", borderRadius: 8, background: "var(--dsw-alias-bg-layer-2,#f3f4f6)", padding: "10px 12px" } },
|
|
1444
1453
|
(0, import_react2.createElement)(
|
|
@@ -1450,7 +1459,8 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
1450
1459
|
(0, import_react2.createElement)("div", { style: styles.muted, marginTop: 4, wordBreak: "break-all" }, `\u8FDB\u7A0B\u5728\u540E\u53F0\u8FD0\u884C\uFF08\u4E0D\u6302\u7EC8\u7AEF\uFF09\u3002\u5982\u9700\u505C\u6B62\uFF1A${status?.killHint ?? `lsof -ti :${status?.dshPort ?? 3080} | xargs kill -9`}`)
|
|
1451
1460
|
) : null,
|
|
1452
1461
|
// 更新提示——左侧黄色色条(提示有新版本);单状态:有更新/更新中/已更新自动重启,不并存
|
|
1453
|
-
|
|
1462
|
+
// 桌面端不渲染(更新由 DSH Desktop 管理)
|
|
1463
|
+
!isDesktop && updateInfo ? (0, import_react2.createElement)(
|
|
1454
1464
|
"div",
|
|
1455
1465
|
{ style: { ...styles.block, borderLeft: "4px solid var(--dsw-alias-state-warn-primary,#b45309)", borderRadius: 8, background: "var(--dsw-alias-bg-layer-2,#f3f4f6)", padding: "10px 12px" } },
|
|
1456
1466
|
(0, import_react2.createElement)(
|
package/client/index.jsx
CHANGED
|
@@ -38,6 +38,7 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
38
38
|
const [tunnelState, setTunnelState] = useState(null); // 隧道进度 {phase, detail, startedAt}
|
|
39
39
|
const [restartNotice, setRestartNotice] = useState(false); // 重启后提示
|
|
40
40
|
const [updateInfo, setUpdateInfo] = useState(null); // { current, latest, updating, result, startedAt } | null
|
|
41
|
+
const [isDesktop, setIsDesktop] = useState(false); // DSH Desktop(Electron)环境:更新/重启由桌面版管理
|
|
41
42
|
const [now, setNow] = useState(Date.now()); // 每秒 tick,驱动倒计时
|
|
42
43
|
|
|
43
44
|
// 进行中操作的「已等待 X 秒」倒计时
|
|
@@ -58,6 +59,7 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
58
59
|
const s = await call(POCKET_ENDPOINTS.status, {});
|
|
59
60
|
setStatus(s);
|
|
60
61
|
setTunnelState(s.tunnelState ?? null);
|
|
62
|
+
if (s.desktop) setIsDesktop(true);
|
|
61
63
|
if (s.restartNotice) {
|
|
62
64
|
// 新进程确认起来了:显示一次「已重启」,清掉旧的更新横幅(单状态,不并存),
|
|
63
65
|
// 然后自动刷新页面加载新代码——不用用户手动刷新
|
|
@@ -85,7 +87,9 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
85
87
|
// 版本检测:host 当前版本 vs npm registry latest(registry 带 CORS *)
|
|
86
88
|
// 两种情况显示横幅:① 有新版可更新;② 磁盘已更新但进程还是旧代码(重启生效)
|
|
87
89
|
// cache: 'no-store' —— registry 响应带缓存头,浏览器会缓存旧版本号导致「小版本不提示」
|
|
90
|
+
// 桌面端(isDesktop):更新/重启由 DSH Desktop 管理,这里不做版本检测、不显示更新横幅
|
|
88
91
|
useEffect(() => {
|
|
92
|
+
if (isDesktop) return;
|
|
89
93
|
let alive = true;
|
|
90
94
|
(async () => {
|
|
91
95
|
try {
|
|
@@ -102,7 +106,7 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
102
106
|
} catch { /* 网络失败静默 */ }
|
|
103
107
|
})();
|
|
104
108
|
return () => { alive = false; };
|
|
105
|
-
}, []);
|
|
109
|
+
}, [isDesktop]);
|
|
106
110
|
|
|
107
111
|
// 重启宿主(更新生效必需:刷新页面不会重载服务端代码)
|
|
108
112
|
const restartPocket = async () => {
|
|
@@ -176,8 +180,13 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
176
180
|
'开发者:程序员少北晨'),
|
|
177
181
|
),
|
|
178
182
|
|
|
179
|
-
//
|
|
180
|
-
|
|
183
|
+
// 桌面端提示:更新/重启由 DSH Desktop 管理,本插件这两项功能已关闭(扫码同屏等照常)
|
|
184
|
+
isDesktop ? h('div', { style: { ...styles.block, padding: '8px 0 0' } },
|
|
185
|
+
h('div', { style: styles.muted }, '💻 DSH Desktop 环境:更新与重启由桌面版管理,此处已停用 | Running inside DSH Desktop: updates & restart are managed by the desktop app'),
|
|
186
|
+
) : null,
|
|
187
|
+
|
|
188
|
+
// 重启后提示(进程在后台运行,停止方法)——左侧蓝色色条(桌面端不会触发本插件的自重启)
|
|
189
|
+
!isDesktop && restartNotice ? h('div', { style: { ...styles.block, borderLeft: '4px solid var(--dsw-alias-brand-primary,#4f6ef7)', borderRadius: 8, background: 'var(--dsw-alias-bg-layer-2,#f3f4f6)', padding: '10px 12px' } },
|
|
181
190
|
h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 } },
|
|
182
191
|
h('div', { style: { fontWeight: 600, fontSize: 13 } }, '🔄 已重启 | Restarted'),
|
|
183
192
|
h('button', { style: styles.btn, onClick: () => setRestartNotice(false) }, '知道了 | OK'),
|
|
@@ -186,7 +195,8 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
186
195
|
) : null,
|
|
187
196
|
|
|
188
197
|
// 更新提示——左侧黄色色条(提示有新版本);单状态:有更新/更新中/已更新自动重启,不并存
|
|
189
|
-
|
|
198
|
+
// 桌面端不渲染(更新由 DSH Desktop 管理)
|
|
199
|
+
!isDesktop && updateInfo ? h('div', { style: { ...styles.block, borderLeft: '4px solid var(--dsw-alias-state-warn-primary,#b45309)', borderRadius: 8, background: 'var(--dsw-alias-bg-layer-2,#f3f4f6)', padding: '10px 12px' } },
|
|
190
200
|
h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 } },
|
|
191
201
|
h('div', { style: { fontWeight: 600, fontSize: 13 } },
|
|
192
202
|
updateInfo.updated
|
package/lib/index.js
CHANGED
|
@@ -111,6 +111,17 @@ export function apply(ctx, config = {}, internals = {}) {
|
|
|
111
111
|
return () => {};
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
// 桌面端环境识别(官方兼容模式,见 desktop 的 plugin-development.md):
|
|
115
|
+
// desktopProfiles / desktopPnpm 只在 DSH Desktop(Electron)里存在。
|
|
116
|
+
// 桌面端有自己的更新/进程管理,我们这两项功能在此环境**关闭**(不删除),
|
|
117
|
+
// 避免与 desktopPnpm / Electron 进程模型冲突;扫码同屏等正常功能照常。
|
|
118
|
+
const isDesktop = internals.isDesktop !== undefined
|
|
119
|
+
? internals.isDesktop === true
|
|
120
|
+
: ctx.get?.('desktopProfiles') !== undefined || ctx.get?.('desktopPnpm') !== undefined;
|
|
121
|
+
if (isDesktop) {
|
|
122
|
+
logger.info('dsh-pocket: DSH Desktop detected — update/restart disabled here | 检测到桌面端环境,更新/重启已关闭');
|
|
123
|
+
}
|
|
124
|
+
|
|
114
125
|
const service = internals.service ?? createPocketService({
|
|
115
126
|
dshPort,
|
|
116
127
|
port: internals.port ?? config.port ?? 3081,
|
|
@@ -121,6 +132,7 @@ export function apply(ctx, config = {}, internals = {}) {
|
|
|
121
132
|
const disposers = [];
|
|
122
133
|
const disposeRpc = installPocketRpc(ctx, {
|
|
123
134
|
service,
|
|
135
|
+
desktop: isDesktop,
|
|
124
136
|
runUpdate: internals.runUpdate ?? { currentVersion, perform: performUpdate, loadedVersion: () => loadedVersion },
|
|
125
137
|
restart: internals.restart ?? (() => pocketRestart(service)),
|
|
126
138
|
restartNotice: internals.restartNotice ?? consumeRestartNotice,
|
package/lib/service.mjs
CHANGED
|
@@ -19,13 +19,54 @@ export async function qrDataUrl(text, { width = 220, margin = 1 } = {}) {
|
|
|
19
19
|
return QRCode.toDataURL(text, { errorCorrectionLevel: 'M', margin, width, type: 'image/png' });
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
/** RFC1918 私网地址:手机与电脑连同一局域网时通常可直连。 */
|
|
23
|
+
const PRIVATE_IPV4_RE = /^(?:10\.|192\.168\.|172\.(?:1[6-9]|2\d|3[01])\.)/;
|
|
24
|
+
|
|
25
|
+
/** 名称像真实物理网卡的接口(WLAN / Wi-Fi / Ethernet / 以太网 / en / eth)。 */
|
|
26
|
+
const PHYSICAL_IFACE_RE = /^(?:wlan|wi-?fi|wireless|ethernet|eth\d|en\d|wlp\d|以太网|本地连接)/i;
|
|
27
|
+
|
|
28
|
+
/** 常见的 VPN / 虚拟网卡名称:手机通常无法通过它们直连电脑。 */
|
|
29
|
+
const VPN_IFACE_RE = /(?:radmin|tailscale|zerotier|tun|tap|vpn|vethernet|virtual|vmware|virtualbox|wsl|docker|teredo|hamachi|bluetooth|bridge)/i;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 从 networkInterfaces() 返回的接口表里选出手机最可能可达的 IPv4。
|
|
33
|
+
*
|
|
34
|
+
* `os.networkInterfaces()` 的枚举顺序不可靠:Windows 上 Radmin VPN / Tailscale /
|
|
35
|
+
* vEthernet 等虚拟网卡常排在 WLAN 前面,旧实现直接取第一张非回环网卡,会生成
|
|
36
|
+
* 手机打不开的二维码。这里按以下规则打分排序:
|
|
37
|
+
* - RFC1918 私网地址优先(10/8、172.16/12、192.168/16);
|
|
38
|
+
* - 名称像物理网卡再加分;
|
|
39
|
+
* - 名称像 VPN/虚拟网卡减分;
|
|
40
|
+
* - 同分保持原枚举顺序。
|
|
41
|
+
* 没有任何私网地址时回退到最高分地址(例如纯 VPN 环境仍可用)。
|
|
42
|
+
*
|
|
43
|
+
* @param {ReturnType<typeof networkInterfaces>} interfaces
|
|
44
|
+
* @returns {string|null}
|
|
45
|
+
*/
|
|
46
|
+
export function selectLanIPv4(interfaces) {
|
|
47
|
+
const candidates = [];
|
|
48
|
+
for (const [name, addrs] of Object.entries(interfaces ?? {})) {
|
|
49
|
+
for (const addr of addrs ?? []) {
|
|
50
|
+
if (addr.family !== 'IPv4' || addr.internal) continue;
|
|
51
|
+
const ip = addr.address;
|
|
52
|
+
// 排除 loopback 与 link-local;其余地址即使不是私网(如 Radmin 的 26.x)也保留兜底
|
|
53
|
+
if (!ip || ip.startsWith('127.') || ip.startsWith('169.254.')) continue;
|
|
54
|
+
|
|
55
|
+
let score = 0;
|
|
56
|
+
if (PRIVATE_IPV4_RE.test(ip)) score += 100;
|
|
57
|
+
if (PHYSICAL_IFACE_RE.test(name)) score += 20;
|
|
58
|
+
else if (VPN_IFACE_RE.test(name)) score -= 50;
|
|
59
|
+
|
|
60
|
+
candidates.push({ ip, score, order: candidates.length });
|
|
26
61
|
}
|
|
27
62
|
}
|
|
28
|
-
|
|
63
|
+
|
|
64
|
+
candidates.sort((a, b) => b.score - a.score || a.order - b.order);
|
|
65
|
+
return candidates[0]?.ip ?? null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function lanIPv4() {
|
|
69
|
+
return selectLanIPv4(networkInterfaces());
|
|
29
70
|
}
|
|
30
71
|
|
|
31
72
|
/**
|
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, runUpdate = null, restart = null, restartNotice = null }) {
|
|
28
|
+
export function installPocketRpc(ctx, { service, log = console, desktop = false, runUpdate = null, restart = null, restartNotice = 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 () => {};
|
|
@@ -33,12 +33,12 @@ export function installPocketRpc(ctx, { service, log = console, runUpdate = null
|
|
|
33
33
|
return ctx.connection.rpc.handle(POCKET_RPC_CHANNEL, async (endpoint, payload = {}, signal) => {
|
|
34
34
|
if (signal?.aborted) return fail('cancelled', 'The request was cancelled.');
|
|
35
35
|
|
|
36
|
-
// status 响应:redact 后的服务状态 + 重启提示(重启后 30 分钟内有效)+
|
|
36
|
+
// status 响应:redact 后的服务状态 + 重启提示(重启后 30 分钟内有效)+ 停止命令(按平台)+ 桌面端标志
|
|
37
37
|
const statusPayload = async () => {
|
|
38
38
|
let notice = null;
|
|
39
39
|
try { notice = (await restartNotice?.()) ?? null; } catch { notice = null; }
|
|
40
40
|
const s = await service.status();
|
|
41
|
-
return ok({ ...redactStatus(s), restartNotice: notice, killHint: killHint(s.dshPort ?? 3080) });
|
|
41
|
+
return ok({ ...redactStatus(s), desktop, restartNotice: notice, killHint: killHint(s.dshPort ?? 3080) });
|
|
42
42
|
};
|
|
43
43
|
|
|
44
44
|
try {
|
|
@@ -57,6 +57,8 @@ export function installPocketRpc(ctx, { service, log = console, runUpdate = null
|
|
|
57
57
|
return ok({ current: runUpdate?.currentVersion?.() ?? null, loaded: runUpdate?.loadedVersion?.() ?? null });
|
|
58
58
|
}
|
|
59
59
|
if (endpoint === POCKET_ENDPOINTS.update) {
|
|
60
|
+
// 桌面端:更新由 DSH Desktop 管理,这里关闭(不删除,仅禁用)
|
|
61
|
+
if (desktop) return fail('bad-request', '桌面版更新由 DSH Desktop 管理,已在此环境停用 | updates are managed by DSH Desktop here');
|
|
60
62
|
if (!runUpdate) return fail('bad-request', '更新不可用 | update unavailable');
|
|
61
63
|
const result = await runUpdate.perform(payload?.profile ?? 'web');
|
|
62
64
|
// 更新成功 → 自动重启生效(用户只点一次;helper 拉起失败则保持现状,可手动重启)
|
|
@@ -67,6 +69,8 @@ export function installPocketRpc(ctx, { service, log = console, runUpdate = null
|
|
|
67
69
|
return ok(result);
|
|
68
70
|
}
|
|
69
71
|
if (endpoint === POCKET_ENDPOINTS.restart) {
|
|
72
|
+
// 桌面端:重启由 DSH Desktop 管理,这里关闭(不删除,仅禁用)
|
|
73
|
+
if (desktop) return fail('bad-request', '桌面版重启由 DSH Desktop 管理,已在此环境停用 | restart is managed by DSH Desktop here');
|
|
70
74
|
if (!restart) return fail('bad-request', '重启不可用 | restart unavailable');
|
|
71
75
|
const result = restart();
|
|
72
76
|
// 重启拉起失败(helper 都没 spawn 出来)→ 如实报错,别让 UI 误报成功
|
package/package.json
CHANGED