dsh-pocket 1.0.2 → 1.0.3

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
@@ -9,6 +9,7 @@ export const POCKET_ENDPOINTS = Object.freeze({
9
9
  pushSubscribe: 'push.subscribe',
10
10
  pushUnsubscribe: 'push.unsubscribe',
11
11
  pushStatus: 'push.status',
12
+ pushSetEnabled: 'push.setEnabled',
12
13
  });
13
14
 
14
15
  /** 浏览器可见的状态字段(无敏感信息;含二维码 data URL)。 */
package/client/client.js CHANGED
@@ -41,7 +41,8 @@ var POCKET_ENDPOINTS = Object.freeze({
41
41
  pushVapidKey: "push.vapidKey",
42
42
  pushSubscribe: "push.subscribe",
43
43
  pushUnsubscribe: "push.unsubscribe",
44
- pushStatus: "push.status"
44
+ pushStatus: "push.status",
45
+ pushSetEnabled: "push.setEnabled"
45
46
  });
46
47
  function redactStatus(s) {
47
48
  return {
@@ -1285,6 +1286,7 @@ function PocketSettingsTab({ rpcCall }) {
1285
1286
  const [status, setStatus] = (0, import_react2.useState)(null);
1286
1287
  const [busy, setBusy] = (0, import_react2.useState)(false);
1287
1288
  const [error, setError] = (0, import_react2.useState)(null);
1289
+ const [pushEnabled, setPushEnabled] = (0, import_react2.useState)(true);
1288
1290
  const [pushState, setPushState] = (0, import_react2.useState)("checking");
1289
1291
  const call = async (endpoint, payload) => {
1290
1292
  const res = await rpcCall(endpoint, payload);
@@ -1303,8 +1305,31 @@ function PocketSettingsTab({ rpcCall }) {
1303
1305
  return () => clearInterval(t);
1304
1306
  }, []);
1305
1307
  (0, import_react2.useEffect)(() => {
1306
- setupPush(rpcCall).then(setPushState);
1308
+ call(POCKET_ENDPOINTS.pushStatus, {}).then((s) => setPushEnabled(s.enabled)).catch(() => {
1309
+ });
1307
1310
  }, []);
1311
+ const enablePush = async () => {
1312
+ await call(POCKET_ENDPOINTS.pushSetEnabled, { enabled: true });
1313
+ setPushEnabled(true);
1314
+ setPushState(await setupPush(rpcCall));
1315
+ };
1316
+ const disablePush = async () => {
1317
+ try {
1318
+ if ("serviceWorker" in navigator) {
1319
+ const reg = await navigator.serviceWorker.getRegistration("/pocket-sw.js");
1320
+ const sub = await reg?.pushManager?.getSubscription();
1321
+ if (sub) {
1322
+ const endpoint = sub.endpoint;
1323
+ await sub.unsubscribe();
1324
+ await call(POCKET_ENDPOINTS.pushUnsubscribe, { endpoint });
1325
+ }
1326
+ }
1327
+ } catch {
1328
+ }
1329
+ await call(POCKET_ENDPOINTS.pushSetEnabled, { enabled: false });
1330
+ setPushEnabled(false);
1331
+ setPushState("off");
1332
+ };
1308
1333
  const startTunnel = async () => {
1309
1334
  setBusy(true);
1310
1335
  setError(null);
@@ -1365,15 +1390,20 @@ function PocketSettingsTab({ rpcCall }) {
1365
1390
  (0, import_react2.createElement)("div", { style: styles.warn, marginTop: 8 }, "\u26A0\uFE0F DSH \u80FD\u6267\u884C\u7535\u8111\u4EE3\u7801\uFF1A\u4E8C\u7EF4\u7801/URL \u5C31\u662F\u94A5\u5319\uFF0C\u8BF7\u52FF\u53D1\u7ED9\u522B\u4EBA")
1366
1391
  )
1367
1392
  ),
1368
- // Web Push 状态
1393
+ // Web Push 状态 + 开关
1369
1394
  (0, import_react2.createElement)(
1370
1395
  "div",
1371
1396
  { style: styles.block },
1372
- (0, import_react2.createElement)("div", { style: { fontWeight: 600, fontSize: 13 } }, "\u{1F514} \u63A8\u9001\u901A\u77E5 | Push notifications"),
1397
+ (0, import_react2.createElement)(
1398
+ "div",
1399
+ { style: { display: "flex", alignItems: "center", justifyContent: "space-between" } },
1400
+ (0, import_react2.createElement)("div", { style: { fontWeight: 600, fontSize: 13 } }, "\u{1F514} \u63A8\u9001\u901A\u77E5 | Push notifications"),
1401
+ pushEnabled ? (0, import_react2.createElement)("button", { style: styles.btn, onClick: disablePush }, "\u5173\u95ED | Off") : (0, import_react2.createElement)("button", { style: styles.primary, onClick: enablePush }, "\u5F00\u542F | On")
1402
+ ),
1373
1403
  (0, import_react2.createElement)(
1374
1404
  "div",
1375
1405
  { style: styles.muted },
1376
- pushState === "on" ? "\u5DF2\u5F00\u542F\uFF1Aagent \u8DD1\u5B8C/\u51FA\u9519\u65F6\u624B\u673A\u6536\u5230\u901A\u77E5 | on: notified when tasks finish or fail" : pushState === "unsupported" ? "\u5F53\u524D\u6D4F\u89C8\u5668\u4E0D\u652F\u6301\u63A8\u9001 | this browser does not support push" : pushState === "insecure" ? "\u9700\u8981 HTTPS\uFF08\u516C\u7F51\u96A7\u9053\uFF09\u6216 localhost \u624D\u80FD\u5F00\u542F\u63A8\u9001 | push needs HTTPS (public tunnel) or localhost" : pushState === "checking" ? "\u68C0\u67E5\u4E2D\u2026 | checking\u2026" : "\u63A8\u9001\u672A\u5F00\u542F | push not enabled"
1406
+ !pushEnabled ? "\u5DF2\u5173\u95ED\uFF1Aagent \u8DD1\u5B8C\u4E0D\u4F1A\u63A8\u9001 | off: no notifications" : pushState === "on" ? "\u5DF2\u5F00\u542F\uFF1Aagent \u8DD1\u5B8C/\u51FA\u9519\u65F6\u624B\u673A\u6536\u5230\u901A\u77E5 | on: notified when tasks finish or fail" : pushState === "unsupported" ? "\u5DF2\u5F00\u542F\uFF08\u4F46\u5F53\u524D\u6D4F\u89C8\u5668\u4E0D\u652F\u6301\u63A8\u9001\uFF09| on, but this browser does not support push" : pushState === "insecure" ? "\u5DF2\u5F00\u542F\uFF0C\u4F46\u5F53\u524D\u8DEF\u5F84\u4E0D\u662F HTTPS\u2014\u2014\u63A8\u9001\u9700\u8981\u516C\u7F51\u96A7\u9053\u6216 localhost | on, but push needs HTTPS (public tunnel) or localhost" : pushState === "checking" ? "\u68C0\u67E5\u4E2D\u2026 | checking\u2026" : "\u63A8\u9001\u672A\u751F\u6548 | push not active"
1377
1407
  )
1378
1408
  ),
1379
1409
  error ? (0, import_react2.createElement)("div", { style: { color: "var(--dsw-alias-state-error-primary,#dc2626)", fontSize: 12, marginTop: 8 } }, `\u274C ${error}`) : null
package/client/index.jsx CHANGED
@@ -58,6 +58,7 @@ function PocketSettingsTab({ rpcCall }) {
58
58
  const [status, setStatus] = useState(null);
59
59
  const [busy, setBusy] = useState(false);
60
60
  const [error, setError] = useState(null);
61
+ const [pushEnabled, setPushEnabled] = useState(true); // 宿主开关
61
62
  const [pushState, setPushState] = useState('checking'); // checking|on|unsupported|insecure|off
62
63
 
63
64
  const call = async (endpoint, payload) => {
@@ -76,11 +77,36 @@ function PocketSettingsTab({ rpcCall }) {
76
77
  return () => clearInterval(t);
77
78
  }, []);
78
79
 
79
- // Web Push 订阅(只跑一次)
80
+ // 读取宿主开关状态
80
81
  useEffect(() => {
81
- setupPush(rpcCall).then(setPushState);
82
+ call(POCKET_ENDPOINTS.pushStatus, {}).then((s) => setPushEnabled(s.enabled)).catch(() => {});
82
83
  }, []);
83
84
 
85
+ // 开启推送:宿主开关开 + 浏览器订阅(安全上下文才有效)
86
+ const enablePush = async () => {
87
+ await call(POCKET_ENDPOINTS.pushSetEnabled, { enabled: true });
88
+ setPushEnabled(true);
89
+ setPushState(await setupPush(rpcCall));
90
+ };
91
+
92
+ // 关闭推送:取消浏览器订阅 + 宿主开关关
93
+ const disablePush = async () => {
94
+ try {
95
+ if ('serviceWorker' in navigator) {
96
+ const reg = await navigator.serviceWorker.getRegistration('/pocket-sw.js');
97
+ const sub = await reg?.pushManager?.getSubscription();
98
+ if (sub) {
99
+ const endpoint = sub.endpoint;
100
+ await sub.unsubscribe();
101
+ await call(POCKET_ENDPOINTS.pushUnsubscribe, { endpoint });
102
+ }
103
+ }
104
+ } catch { /* 忽略 */ }
105
+ await call(POCKET_ENDPOINTS.pushSetEnabled, { enabled: false });
106
+ setPushEnabled(false);
107
+ setPushState('off');
108
+ };
109
+
84
110
  const startTunnel = async () => {
85
111
  setBusy(true);
86
112
  setError(null);
@@ -134,15 +160,21 @@ function PocketSettingsTab({ rpcCall }) {
134
160
  ),
135
161
  ),
136
162
 
137
- // Web Push 状态
163
+ // Web Push 状态 + 开关
138
164
  h('div', { style: styles.block },
139
- h('div', { style: { fontWeight: 600, fontSize: 13 } }, '🔔 推送通知 | Push notifications'),
165
+ h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between' } },
166
+ h('div', { style: { fontWeight: 600, fontSize: 13 } }, '🔔 推送通知 | Push notifications'),
167
+ pushEnabled
168
+ ? h('button', { style: styles.btn, onClick: disablePush }, '关闭 | Off')
169
+ : h('button', { style: styles.primary, onClick: enablePush }, '开启 | On'),
170
+ ),
140
171
  h('div', { style: styles.muted },
141
- pushState === 'on' ? '已开启:agent 跑完/出错时手机收到通知 | on: notified when tasks finish or fail'
142
- : pushState === 'unsupported' ? '当前浏览器不支持推送 | this browser does not support push'
143
- : pushState === 'insecure' ? '需要 HTTPS(公网隧道)或 localhost 才能开启推送 | push needs HTTPS (public tunnel) or localhost'
172
+ !pushEnabled ? '已关闭:agent 跑完不会推送 | off: no notifications'
173
+ : pushState === 'on' ? '已开启:agent 跑完/出错时手机收到通知 | on: notified when tasks finish or fail'
174
+ : pushState === 'unsupported' ? '已开启(但当前浏览器不支持推送)| on, but this browser does not support push'
175
+ : pushState === 'insecure' ? '已开启,但当前路径不是 HTTPS——推送需要公网隧道或 localhost | on, but push needs HTTPS (public tunnel) or localhost'
144
176
  : pushState === 'checking' ? '检查中… | checking…'
145
- : '推送未开启 | push not enabled'),
177
+ : '推送未生效 | push not active'),
146
178
  ),
147
179
 
148
180
  error ? h('div', { style: { color: 'var(--dsw-alias-state-error-primary,#dc2626)', fontSize: 12, marginTop: 8 } }, `❌ ${error}`) : null,
package/lib/push.mjs CHANGED
@@ -17,6 +17,7 @@ import { join, dirname } from 'node:path';
17
17
  const DATA_REL = join('dsh-pocket', 'push');
18
18
  const VAPID_FILE = 'vapid.json';
19
19
  const SUBS_FILE = 'subscriptions.json';
20
+ const SETTINGS_FILE = 'settings.json';
20
21
 
21
22
  function defaultWebPush() {
22
23
  // 动态 require(web-push 是 CJS)
@@ -42,12 +43,21 @@ export async function createPushService({
42
43
  const dir = join(dshHome, DATA_REL);
43
44
  const vapidPath = join(dir, VAPID_FILE);
44
45
  const subsPath = join(dir, SUBS_FILE);
46
+ const settingsPath = join(dir, SETTINGS_FILE);
45
47
  const mkdirFn = internals.mkdir ?? mkdir;
46
48
  const read = internals.read ?? readFile;
47
49
  const write = internals.write ?? writeFile;
48
50
 
49
51
  await mkdirFn(dir, { recursive: true });
50
52
 
53
+ // 开关状态:默认开,持久化到 settings.json
54
+ let enabled = true;
55
+ try {
56
+ const s = JSON.parse(await read(settingsPath, 'utf8'));
57
+ if (typeof s.enabled === 'boolean') enabled = s.enabled;
58
+ } catch { /* 默认开 */ }
59
+ if (internals.enabled !== undefined) enabled = internals.enabled === true;
60
+
51
61
  // VAPID 密钥:生成一次,持久化
52
62
  let vapid;
53
63
  try {
@@ -73,6 +83,14 @@ export async function createPushService({
73
83
  return {
74
84
  vapidPublicKey: () => vapid.publicKey,
75
85
  count: () => subs.size,
86
+ isEnabled: () => enabled,
87
+
88
+ /** 设置推送总开关(持久化)。 */
89
+ async setEnabled(value) {
90
+ enabled = value === true;
91
+ await write(settingsPath, JSON.stringify({ enabled }, null, 2) + '\n', { mode: 0o600 });
92
+ return enabled;
93
+ },
76
94
 
77
95
  /** 记录浏览器订阅。 */
78
96
  async subscribe(subscription) {
@@ -98,7 +116,7 @@ export async function createPushService({
98
116
  * @param {object} [opts.data] 额外载荷
99
117
  */
100
118
  async notify({ title, body = '', url = '/', data = {} }) {
101
- if (subs.size === 0) return 0;
119
+ if (!enabled || subs.size === 0) return 0;
102
120
  const payload = JSON.stringify({ title, body, url, ...data });
103
121
  let sent = 0;
104
122
  for (const [endpoint, sub] of [...subs.entries()]) {
package/lib/web-rpc.js CHANGED
@@ -49,7 +49,11 @@ export function installPocketRpc(ctx, { service, push, log = console }) {
49
49
  return ok({ removed, count: push.count() });
50
50
  }
51
51
  if (endpoint === POCKET_ENDPOINTS.pushStatus) {
52
- return ok({ enabled: push.count() > 0, count: push.count() });
52
+ return ok({ enabled: push.isEnabled(), count: push.count() });
53
+ }
54
+ if (endpoint === POCKET_ENDPOINTS.pushSetEnabled) {
55
+ const enabled = await push.setEnabled(payload?.enabled === true);
56
+ return ok({ enabled, count: push.count() });
53
57
  }
54
58
  return fail('bad-request', `Unknown endpoint: ${endpoint}`);
55
59
  } catch (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.2"
80
+ "version": "1.0.3"
81
81
  }