dsh-pocket 1.0.4 → 1.0.5

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 CHANGED
@@ -10,8 +10,26 @@ export const POCKET_ENDPOINTS = Object.freeze({
10
10
  pushUnsubscribe: 'push.unsubscribe',
11
11
  pushStatus: 'push.status',
12
12
  pushSetEnabled: 'push.setEnabled',
13
+ version: 'pocket.version',
14
+ update: 'pocket.update',
13
15
  });
14
16
 
17
+ /** 语义化版本比较:a > b 返回正数,相等 0,a < b 负数(仅数字段;带预发布后缀的更旧)。 */
18
+ export function compareVersions(a, b) {
19
+ const pa = String(a).replace(/^v/, '').split('.');
20
+ const pb = String(b).replace(/^v/, '').split('.');
21
+ for (let i = 0; i < 3; i++) {
22
+ const x = parseInt(pa[i], 10) || 0;
23
+ const y = parseInt(pb[i], 10) || 0;
24
+ if (x !== y) return x - y;
25
+ }
26
+ // 数字段相等:带预发布(-)的更旧
27
+ const aPre = /-/.test(pa[2] ?? '');
28
+ const bPre = /-/.test(pb[2] ?? '');
29
+ if (aPre !== bPre) return aPre ? -1 : 1;
30
+ return 0;
31
+ }
32
+
15
33
  /** 浏览器可见的状态字段(无敏感信息;含二维码 data URL)。 */
16
34
  export function redactStatus(s) {
17
35
  return {
package/client/client.js CHANGED
@@ -42,8 +42,23 @@ var POCKET_ENDPOINTS = Object.freeze({
42
42
  pushSubscribe: "push.subscribe",
43
43
  pushUnsubscribe: "push.unsubscribe",
44
44
  pushStatus: "push.status",
45
- pushSetEnabled: "push.setEnabled"
45
+ pushSetEnabled: "push.setEnabled",
46
+ version: "pocket.version",
47
+ update: "pocket.update"
46
48
  });
49
+ function compareVersions(a, b) {
50
+ const pa = String(a).replace(/^v/, "").split(".");
51
+ const pb = String(b).replace(/^v/, "").split(".");
52
+ for (let i = 0; i < 3; i++) {
53
+ const x = parseInt(pa[i], 10) || 0;
54
+ const y = parseInt(pb[i], 10) || 0;
55
+ if (x !== y) return x - y;
56
+ }
57
+ const aPre = /-/.test(pa[2] ?? "");
58
+ const bPre = /-/.test(pb[2] ?? "");
59
+ if (aPre !== bPre) return aPre ? -1 : 1;
60
+ return 0;
61
+ }
47
62
  function redactStatus(s) {
48
63
  return {
49
64
  proxyRunning: s?.proxyRunning === true,
@@ -1288,6 +1303,7 @@ function PocketSettingsTab({ rpcCall }) {
1288
1303
  const [error, setError] = (0, import_react2.useState)(null);
1289
1304
  const [pushEnabled, setPushEnabled] = (0, import_react2.useState)(true);
1290
1305
  const [pushState, setPushState] = (0, import_react2.useState)("checking");
1306
+ const [updateInfo, setUpdateInfo] = (0, import_react2.useState)(null);
1291
1307
  const call = async (endpoint, payload) => {
1292
1308
  const res = await rpcCall(endpoint, payload);
1293
1309
  if (!res?.ok) throw new Error(res?.error?.message ?? "RPC failed");
@@ -1308,6 +1324,33 @@ function PocketSettingsTab({ rpcCall }) {
1308
1324
  call(POCKET_ENDPOINTS.pushStatus, {}).then((s) => setPushEnabled(s.enabled)).catch(() => {
1309
1325
  });
1310
1326
  }, []);
1327
+ (0, import_react2.useEffect)(() => {
1328
+ let alive = true;
1329
+ (async () => {
1330
+ try {
1331
+ const v = await call(POCKET_ENDPOINTS.version, {});
1332
+ const meta = await (await fetch("https://registry.npmjs.org/dsh-pocket/latest")).json();
1333
+ if (!alive) return;
1334
+ const latest = typeof meta?.version === "string" ? meta.version : null;
1335
+ if (latest && v.current && compareVersions(latest, v.current) > 0) {
1336
+ setUpdateInfo({ current: v.current, latest, updating: false, result: null });
1337
+ }
1338
+ } catch {
1339
+ }
1340
+ })();
1341
+ return () => {
1342
+ alive = false;
1343
+ };
1344
+ }, []);
1345
+ const runUpdate = async () => {
1346
+ setUpdateInfo((u) => ({ ...u, updating: true, result: null }));
1347
+ try {
1348
+ const r = await call(POCKET_ENDPOINTS.update, {});
1349
+ setUpdateInfo((u) => ({ ...u, updating: false, result: r.ok ? "ok" : "fail", output: r.output ?? r.error }));
1350
+ } catch (err) {
1351
+ setUpdateInfo((u) => ({ ...u, updating: false, result: "fail", output: err.message }));
1352
+ }
1353
+ };
1311
1354
  const enablePush = async () => {
1312
1355
  await call(POCKET_ENDPOINTS.pushSetEnabled, { enabled: true });
1313
1356
  setPushEnabled(true);
@@ -1358,6 +1401,22 @@ function PocketSettingsTab({ rpcCall }) {
1358
1401
  (0, import_react2.createElement)("strong", null, "\u{1F4F1} \u624B\u673A\u8BBF\u95EE | Phone access"),
1359
1402
  (0, import_react2.createElement)("div", { style: styles.muted }, "\u624B\u673A\u626B\u7801\u6253\u5F00\u7684\u5C31\u662F\u7535\u8111\u4E0A\u7684\u8FD9\u4E2A\u754C\u9762\uFF0C\u5B9E\u65F6\u540C\u6B65 | the phone shows this exact screen, live")
1360
1403
  ),
1404
+ // 更新提示
1405
+ updateInfo ? (0, import_react2.createElement)(
1406
+ "div",
1407
+ { style: { ...styles.block, border: "1px solid var(--dsw-alias-state-warn-primary,#b45309)", borderRadius: 8, background: "var(--dsw-alias-bg-layer-2,#f3f4f6)", padding: "10px 12px" } },
1408
+ (0, import_react2.createElement)(
1409
+ "div",
1410
+ { style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 } },
1411
+ (0, import_react2.createElement)("div", { style: { fontWeight: 600, fontSize: 13 } }, `\u{1F4E6} \u65B0\u7248\u672C v${updateInfo.latest} | Update available`),
1412
+ updateInfo.result !== "ok" ? (0, import_react2.createElement)("button", { style: styles.primary, onClick: runUpdate, disabled: updateInfo.updating }, updateInfo.updating ? "\u66F4\u65B0\u4E2D\u2026" : `\u66F4\u65B0\u5230 v${updateInfo.latest} | Update`) : null
1413
+ ),
1414
+ (0, import_react2.createElement)(
1415
+ "div",
1416
+ { style: styles.muted, marginTop: 4 },
1417
+ updateInfo.result === "ok" ? "\u2705 \u5DF2\u66F4\u65B0\uFF0C\u91CD\u542F dsh web \u751F\u6548 | updated \u2014 restart dsh web" : updateInfo.result === "fail" ? `\u274C \u66F4\u65B0\u5931\u8D25\uFF1A${updateInfo.output || "\u672A\u77E5"}\uFF08\u4E5F\u53EF\u624B\u52A8\u6267\u884C dsh plugin --profile web update dsh-pocket --latest -w\uFF09` : `\u5F53\u524D v${updateInfo.current} \u2192 \u6700\u65B0 v${updateInfo.latest}`
1418
+ )
1419
+ ) : null,
1361
1420
  // 局域网
1362
1421
  (0, import_react2.createElement)(
1363
1422
  "div",
package/client/index.jsx CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  import { createElement as h, useEffect, useState } from 'react';
9
9
 
10
- import { POCKET_RPC_CHANNEL, POCKET_ENDPOINTS, redactStatus } from './api.js';
10
+ import { POCKET_RPC_CHANNEL, POCKET_ENDPOINTS, redactStatus, compareVersions } from './api.js';
11
11
  import { mobileApply } from './mobile/mobile-apply.tsx';
12
12
 
13
13
  const name = 'dsh-pocket';
@@ -60,6 +60,7 @@ function PocketSettingsTab({ rpcCall }) {
60
60
  const [error, setError] = useState(null);
61
61
  const [pushEnabled, setPushEnabled] = useState(true); // 宿主开关
62
62
  const [pushState, setPushState] = useState('checking'); // checking|on|unsupported|insecure|off
63
+ const [updateInfo, setUpdateInfo] = useState(null); // { current, latest, updating, result } | null
63
64
 
64
65
  const call = async (endpoint, payload) => {
65
66
  const res = await rpcCall(endpoint, payload);
@@ -82,6 +83,34 @@ function PocketSettingsTab({ rpcCall }) {
82
83
  call(POCKET_ENDPOINTS.pushStatus, {}).then((s) => setPushEnabled(s.enabled)).catch(() => {});
83
84
  }, []);
84
85
 
86
+ // 版本检测:host 当前版本 vs npm registry latest(registry 带 CORS *)
87
+ useEffect(() => {
88
+ let alive = true;
89
+ (async () => {
90
+ try {
91
+ const v = await call(POCKET_ENDPOINTS.version, {});
92
+ const meta = await (await fetch('https://registry.npmjs.org/dsh-pocket/latest')).json();
93
+ if (!alive) return;
94
+ const latest = typeof meta?.version === 'string' ? meta.version : null;
95
+ if (latest && v.current && compareVersions(latest, v.current) > 0) {
96
+ setUpdateInfo({ current: v.current, latest, updating: false, result: null });
97
+ }
98
+ } catch { /* 网络失败静默 */ }
99
+ })();
100
+ return () => { alive = false; };
101
+ }, []);
102
+
103
+ // 一键更新:调宿主 dsh plugin update
104
+ const runUpdate = async () => {
105
+ setUpdateInfo((u) => ({ ...u, updating: true, result: null }));
106
+ try {
107
+ const r = await call(POCKET_ENDPOINTS.update, {});
108
+ setUpdateInfo((u) => ({ ...u, updating: false, result: r.ok ? 'ok' : 'fail', output: r.output ?? r.error }));
109
+ } catch (err) {
110
+ setUpdateInfo((u) => ({ ...u, updating: false, result: 'fail', output: err.message }));
111
+ }
112
+ };
113
+
85
114
  // 开启推送:宿主开关开 + 浏览器订阅(安全上下文才有效)
86
115
  const enablePush = async () => {
87
116
  await call(POCKET_ENDPOINTS.pushSetEnabled, { enabled: true });
@@ -132,6 +161,20 @@ function PocketSettingsTab({ rpcCall }) {
132
161
  h('div', { style: styles.muted }, '手机扫码打开的就是电脑上的这个界面,实时同步 | the phone shows this exact screen, live'),
133
162
  ),
134
163
 
164
+ // 更新提示
165
+ updateInfo ? h('div', { style: { ...styles.block, border: '1px solid var(--dsw-alias-state-warn-primary,#b45309)', borderRadius: 8, background: 'var(--dsw-alias-bg-layer-2,#f3f4f6)', padding: '10px 12px' } },
166
+ h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 } },
167
+ h('div', { style: { fontWeight: 600, fontSize: 13 } }, `📦 新版本 v${updateInfo.latest} | Update available`),
168
+ updateInfo.result !== 'ok'
169
+ ? h('button', { style: styles.primary, onClick: runUpdate, disabled: updateInfo.updating }, updateInfo.updating ? '更新中…' : `更新到 v${updateInfo.latest} | Update`)
170
+ : null,
171
+ ),
172
+ h('div', { style: styles.muted, marginTop: 4 },
173
+ updateInfo.result === 'ok' ? '✅ 已更新,重启 dsh web 生效 | updated — restart dsh web'
174
+ : updateInfo.result === 'fail' ? `❌ 更新失败:${updateInfo.output || '未知'}(也可手动执行 dsh plugin --profile web update dsh-pocket --latest -w)`
175
+ : `当前 v${updateInfo.current} → 最新 v${updateInfo.latest}`),
176
+ ) : null,
177
+
135
178
  // 局域网
136
179
  h('div', { style: styles.block },
137
180
  h('div', { style: { fontWeight: 600, fontSize: 13 } }, '📶 局域网(同一 WiFi)| LAN'),
package/lib/index.js CHANGED
@@ -1,13 +1,16 @@
1
1
  // dsh-pocket 插件入口(单包单插件:手机扫码访问 DSH,全在这一个包里)
2
2
  //
3
- // 设置页「设置 → 插件 → 手机访问」:
3
+ // 设置一级入口「手机访问」:
4
4
  // - 局域网二维码:自动显示(代理随插件启动)
5
5
  // - 公网二维码:点「开启公网」→ cloudflared 隧道 → 扫码即用,人在外面也能访问
6
6
  // - Web Push:agent 跑完/出错 → 手机推送通知(需 HTTPS 公网路径或 localhost)
7
+ // - 更新提示:有新版本时显示一键更新按钮(dsh plugin update --latest)
7
8
  // 手机看到的界面 = 电脑上的 dsh web,实时同步(WebSocket 透传)。
8
9
 
9
10
  import { readFile } from 'node:fs/promises';
10
11
  import { fileURLToPath } from 'node:url';
12
+ import { spawn } from 'node:child_process';
13
+ import { createRequire } from 'node:module';
11
14
 
12
15
  import { createPocketService } from './service.mjs';
13
16
  import { installPocketRpc } from './web-rpc.js';
@@ -15,6 +18,38 @@ import { createPushService } from './push.mjs';
15
18
 
16
19
  const name = 'dsh-pocket';
17
20
  const inject = ['connection', 'webServer'];
21
+ const require = createRequire(import.meta.url);
22
+
23
+ /** 本插件已安装版本(读取自己的 package.json)。 */
24
+ function currentVersion() {
25
+ try {
26
+ return require('../package.json').version;
27
+ } catch {
28
+ return '0.0.0';
29
+ }
30
+ }
31
+
32
+ /** 执行更新:dsh plugin --profile <p> update dsh-pocket --latest -w(超时保护)。 */
33
+ function performUpdate(profile, { timeoutMs = 180_000 } = {}) {
34
+ return new Promise((resolve) => {
35
+ const child = spawn('dsh', ['plugin', '--profile', profile, 'update', 'dsh-pocket', '--latest', '-w'], {
36
+ stdio: ['ignore', 'pipe', 'pipe'],
37
+ });
38
+ let out = '';
39
+ const onData = (c) => { out += String(c); if (out.length > 4000) out = out.slice(-4000); };
40
+ child.stdout.on('data', onData);
41
+ child.stderr.on('data', onData);
42
+ const timer = setTimeout(() => child.kill(), timeoutMs);
43
+ child.once('exit', (code) => {
44
+ clearTimeout(timer);
45
+ resolve({ ok: code === 0, code, output: out.slice(-800) });
46
+ });
47
+ child.once('error', (err) => {
48
+ clearTimeout(timer);
49
+ resolve({ ok: false, error: err.message });
50
+ });
51
+ });
52
+ }
18
53
 
19
54
  // 服务 worker 源码(同源提供:/pocket-sw.js)
20
55
  const SW_SOURCE = new URL('./pocket-sw.js', import.meta.url);
@@ -45,7 +80,8 @@ export function apply(ctx, config = {}, internals = {}) {
45
80
  const disposers = [];
46
81
  const disposeRpc = installPocketRpc(ctx, {
47
82
  service,
48
- push: internals.push ?? { vapidPublicKey: () => '', count: () => 0, subscribe: async () => false, unsubscribe: async () => false },
83
+ push: internals.push ?? { vapidPublicKey: () => '', count: () => 0, subscribe: async () => false, unsubscribe: async () => false, isEnabled: () => true, setEnabled: async () => true },
84
+ runUpdate: internals.runUpdate ?? { currentVersion, perform: performUpdate },
49
85
  log: logger,
50
86
  });
51
87
  disposers.push(disposeRpc);
package/lib/web-rpc.js CHANGED
@@ -17,7 +17,7 @@ function fail(code, message) {
17
17
  }
18
18
 
19
19
  /** 注册 /dsh-pocket 逻辑通道(仅本机 loopback 可调)。 */
20
- export function installPocketRpc(ctx, { service, push, log = console }) {
20
+ export function installPocketRpc(ctx, { service, push, log = console, runUpdate = null }) {
21
21
  if (!ctx?.connection?.rpc?.handle) {
22
22
  log.warn?.('dsh-pocket: DSH Host Connection RPC unavailable — settings tab disabled | 无 Connection RPC,设置页不可用');
23
23
  return () => {};
@@ -55,6 +55,14 @@ export function installPocketRpc(ctx, { service, push, log = console }) {
55
55
  const enabled = await push.setEnabled(payload?.enabled === true);
56
56
  return ok({ enabled, count: push.count() });
57
57
  }
58
+ if (endpoint === POCKET_ENDPOINTS.version) {
59
+ return ok({ current: runUpdate?.currentVersion?.() ?? null });
60
+ }
61
+ if (endpoint === POCKET_ENDPOINTS.update) {
62
+ if (!runUpdate) return fail('bad-request', '更新不可用 | update unavailable');
63
+ const result = await runUpdate.perform(payload?.profile ?? 'web');
64
+ return ok(result);
65
+ }
58
66
  return fail('bad-request', `Unknown endpoint: ${endpoint}`);
59
67
  } catch (err) {
60
68
  log.error?.('dsh-pocket: rpc %s failed | RPC 失败: %s', endpoint, err?.message ?? err);
package/package.json CHANGED
@@ -77,5 +77,5 @@
77
77
  "access": "public",
78
78
  "registry": "https://registry.npmjs.org/"
79
79
  },
80
- "version": "1.0.4"
80
+ "version": "1.0.5"
81
81
  }