dsh-pocket 1.0.19 → 1.0.20
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/README.en.md +0 -2
- package/README.md +0 -2
- package/client/api.js +0 -5
- package/client/client.js +0 -89
- package/client/index.jsx +4 -95
- package/lib/index.js +4 -53
- package/lib/web-rpc.js +1 -27
- package/package.json +2 -3
- package/lib/pocket-sw.js +0 -39
- package/lib/push.mjs +0 -154
package/README.en.md
CHANGED
|
@@ -42,7 +42,6 @@ What it looks like — the phone shows the exact same UI as your computer, live:
|
|
|
42
42
|
| 📱 Mobile-adaptive layout | Narrow screens get a drawer layout automatically (ported from dsh-web-mobile, MIT): sidebar drawer, full-width conversation, safe-area insets, touch optimizations |
|
|
43
43
|
| 🧩 Zero-dependency install | One npm package, one settings tab — no core/adapter split, no account, no server |
|
|
44
44
|
| 🔒 URL is the key | No public URL exposure in LAN mode; public URL rotates on every restart |
|
|
45
|
-
| 🔔 Web Push | Phone notifications when a task finishes or fails (even with the page closed); requires the HTTPS public tunnel or localhost |
|
|
46
45
|
|
|
47
46
|
## 🚀 Usage
|
|
48
47
|
|
|
@@ -93,7 +92,6 @@ On the same page click "**Enable anywhere**" → wait for the tunnel (first run
|
|
|
93
92
|
| Nothing changed after install/update | **You must restart `dsh web`**; the running process still loads the old code |
|
|
94
93
|
| `listen EADDRINUSE ... :3081` | A stale dsh-pocket process holds the port: `lsof -ti :3081 \| xargs kill -9`, then retry |
|
|
95
94
|
| Version stuck below 1.x | `^0.x` ranges never jump to 1.x: update with `--latest` (`dsh plugin --profile web update dsh-pocket --latest -w`) |
|
|
96
|
-
| No push on iOS Safari | Safari Web Push requires **"Add to Home Screen"** first, then open from the home-screen icon (Chrome/Android don't need this) |
|
|
97
95
|
| Public `error 1033` | See "Public tunnel troubleshooting" below — usually a local proxy/VPN (Clash etc. TUN mode) killing the tunnel |
|
|
98
96
|
| After "Restart dsh web", the page says the process is running in the background | The new process from in-page self-restart is a detached background process (not attached to your terminal) — that's the standard way to apply updates in-page; stop it with `lsof -ti :3080 \| xargs kill -9` (logs under `$DSH_HOME` as `dsh-pocket-restart-*.log`) |
|
|
99
97
|
|
package/README.md
CHANGED
|
@@ -42,7 +42,6 @@ DSH Pocket 就是干这个的:**装上它,手机扫个码,就能实时看
|
|
|
42
42
|
| 📱 移动端适配 | 窄屏自动变抽屉布局(移植 dsh-web-mobile,MIT):侧栏抽屉、会话全宽、状态栏安全区、触控优化 |
|
|
43
43
|
| 🧩 零依赖安装 | 一个 npm 包、一个设置页,没有核心/适配器要分开装;无需账号、无需服务器 |
|
|
44
44
|
| 🔒 URL 即钥匙 | 无公网 URL 暴露给第三方(局域网模式);公网 URL 每次重启自动换新 |
|
|
45
|
-
| 🔔 Web Push 通知 | agent 跑完/出错 → 手机推送通知(即使没开页面);需 HTTPS 公网路径或 localhost |
|
|
46
45
|
|
|
47
46
|
## 🚀 怎么用
|
|
48
47
|
|
|
@@ -93,7 +92,6 @@ npx @deepseek-ai/dsh web
|
|
|
93
92
|
| 装完/更新了但界面没变化 | **必须重启 `dsh web`** 才生效;运行中的进程仍加载旧代码 |
|
|
94
93
|
| `listen EADDRINUSE ... :3081` | 旧 dsh-pocket 进程还占着端口:`lsof -ti :3081 \| xargs kill -9` 后重试 |
|
|
95
94
|
| 版本停在 0.x 升不上去 | `^0.x` 范围不允许升到 1.x:更新用 `--latest`(`dsh plugin --profile web update dsh-pocket --latest -w`) |
|
|
96
|
-
| 手机 iOS 收不到推送 | Safari 的 Web Push 要求**先把网页「添加到主屏幕」**,从主屏幕图标打开后才生效(Chrome/Android 无此要求) |
|
|
97
95
|
| 公网 `error 1033` | 见下方「公网隧道常见问题」——多半是本机代理/VPN(Clash 等 TUN 模式)掐断了隧道 |
|
|
98
96
|
| 点「重启 dsh web」后页面提示进程在后台运行 | 自重启的新进程是 detached 后台进程(不挂终端),是页内更新的标准做法;停止它:`lsof -ti :3080 \| xargs kill -9`(日志在 `$DSH_HOME` 下 `dsh-pocket-restart-*.log`) |
|
|
99
97
|
|
package/client/api.js
CHANGED
|
@@ -5,11 +5,6 @@ export const POCKET_ENDPOINTS = Object.freeze({
|
|
|
5
5
|
status: 'pocket.status',
|
|
6
6
|
tunnelStart: 'tunnel.start',
|
|
7
7
|
tunnelStop: 'tunnel.stop',
|
|
8
|
-
pushVapidKey: 'push.vapidKey',
|
|
9
|
-
pushSubscribe: 'push.subscribe',
|
|
10
|
-
pushUnsubscribe: 'push.unsubscribe',
|
|
11
|
-
pushStatus: 'push.status',
|
|
12
|
-
pushSetEnabled: 'push.setEnabled',
|
|
13
8
|
version: 'pocket.version',
|
|
14
9
|
update: 'pocket.update',
|
|
15
10
|
restart: 'pocket.restart',
|
package/client/client.js
CHANGED
|
@@ -38,11 +38,6 @@ var POCKET_ENDPOINTS = Object.freeze({
|
|
|
38
38
|
status: "pocket.status",
|
|
39
39
|
tunnelStart: "tunnel.start",
|
|
40
40
|
tunnelStop: "tunnel.stop",
|
|
41
|
-
pushVapidKey: "push.vapidKey",
|
|
42
|
-
pushSubscribe: "push.subscribe",
|
|
43
|
-
pushUnsubscribe: "push.unsubscribe",
|
|
44
|
-
pushStatus: "push.status",
|
|
45
|
-
pushSetEnabled: "push.setEnabled",
|
|
46
41
|
version: "pocket.version",
|
|
47
42
|
update: "pocket.update",
|
|
48
43
|
restart: "pocket.restart"
|
|
@@ -1265,32 +1260,6 @@ function mobileApply(ctx) {
|
|
|
1265
1260
|
// client/index.jsx
|
|
1266
1261
|
var name = "dsh-pocket";
|
|
1267
1262
|
var inject = ["slots", "connection", "layout", "locale", "sessionLogDownload"];
|
|
1268
|
-
async function setupPush(rpcCall) {
|
|
1269
|
-
try {
|
|
1270
|
-
if (!("serviceWorker" in navigator) || !("PushManager" in window)) return "unsupported";
|
|
1271
|
-
if (!window.isSecureContext) return "insecure";
|
|
1272
|
-
const reg = await navigator.serviceWorker.register("/pocket-sw.js");
|
|
1273
|
-
const vapid = await rpcCall(POCKET_ENDPOINTS.pushVapidKey, {});
|
|
1274
|
-
if (!vapid?.ok) return "host-error";
|
|
1275
|
-
let sub = await reg.pushManager.getSubscription();
|
|
1276
|
-
if (!sub) {
|
|
1277
|
-
sub = await reg.pushManager.subscribe({
|
|
1278
|
-
userVisibleOnly: true,
|
|
1279
|
-
applicationServerKey: urlBase64ToUint8Array(vapid.value.publicKey)
|
|
1280
|
-
});
|
|
1281
|
-
}
|
|
1282
|
-
const res = await rpcCall(POCKET_ENDPOINTS.pushSubscribe, { subscription: sub.toJSON() });
|
|
1283
|
-
return res?.ok ? "on" : "host-error";
|
|
1284
|
-
} catch {
|
|
1285
|
-
return "error";
|
|
1286
|
-
}
|
|
1287
|
-
}
|
|
1288
|
-
function urlBase64ToUint8Array(base64url) {
|
|
1289
|
-
const pad = "=".repeat((4 - base64url.length % 4) % 4);
|
|
1290
|
-
const b64 = (base64url + pad).replace(/-/g, "+").replace(/_/g, "/");
|
|
1291
|
-
const raw = atob(b64);
|
|
1292
|
-
return Uint8Array.from(raw, (c) => c.charCodeAt(0));
|
|
1293
|
-
}
|
|
1294
1263
|
var styles = {
|
|
1295
1264
|
card: { background: "var(--dsw-alias-bg-layer-1,#fff)", border: "1px solid var(--dsw-alias-border-l2,#e5e7eb)", borderRadius: 12, padding: "14px 16px", maxWidth: 480 },
|
|
1296
1265
|
block: { borderTop: "1px solid var(--dsw-alias-border-l2,#e5e7eb)", marginTop: 12, paddingTop: 12 },
|
|
@@ -1305,8 +1274,6 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
1305
1274
|
const [status, setStatus] = (0, import_react2.useState)(null);
|
|
1306
1275
|
const [busy, setBusy] = (0, import_react2.useState)(false);
|
|
1307
1276
|
const [error, setError] = (0, import_react2.useState)(null);
|
|
1308
|
-
const [pushEnabled, setPushEnabled] = (0, import_react2.useState)(true);
|
|
1309
|
-
const [pushState, setPushState] = (0, import_react2.useState)("checking");
|
|
1310
1277
|
const [tunnelState, setTunnelState] = (0, import_react2.useState)(null);
|
|
1311
1278
|
const [restartNotice, setRestartNotice] = (0, import_react2.useState)(false);
|
|
1312
1279
|
const [updateInfo, setUpdateInfo] = (0, import_react2.useState)(null);
|
|
@@ -1329,16 +1296,6 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
1329
1296
|
const t = setInterval(load, 3e3);
|
|
1330
1297
|
return () => clearInterval(t);
|
|
1331
1298
|
}, []);
|
|
1332
|
-
(0, import_react2.useEffect)(() => {
|
|
1333
|
-
(async () => {
|
|
1334
|
-
try {
|
|
1335
|
-
const s = await call(POCKET_ENDPOINTS.pushStatus, {});
|
|
1336
|
-
setPushEnabled(s.enabled);
|
|
1337
|
-
if (s.enabled) setPushState(await setupPush(rpcCall));
|
|
1338
|
-
} catch {
|
|
1339
|
-
}
|
|
1340
|
-
})();
|
|
1341
|
-
}, []);
|
|
1342
1299
|
(0, import_react2.useEffect)(() => {
|
|
1343
1300
|
let alive = true;
|
|
1344
1301
|
(async () => {
|
|
@@ -1381,36 +1338,6 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
1381
1338
|
setUpdateInfo((u) => ({ ...u, updating: false, result: "fail", output: err.message }));
|
|
1382
1339
|
}
|
|
1383
1340
|
};
|
|
1384
|
-
const enablePush = async () => {
|
|
1385
|
-
try {
|
|
1386
|
-
await call(POCKET_ENDPOINTS.pushSetEnabled, { enabled: true });
|
|
1387
|
-
setPushEnabled(true);
|
|
1388
|
-
setPushState(await setupPush(rpcCall));
|
|
1389
|
-
} catch (err) {
|
|
1390
|
-
setError(err?.message ?? "\u63A8\u9001\u5F00\u542F\u5931\u8D25 | failed to enable push");
|
|
1391
|
-
}
|
|
1392
|
-
};
|
|
1393
|
-
const disablePush = async () => {
|
|
1394
|
-
try {
|
|
1395
|
-
try {
|
|
1396
|
-
if ("serviceWorker" in navigator) {
|
|
1397
|
-
const reg = await navigator.serviceWorker.getRegistration("/pocket-sw.js");
|
|
1398
|
-
const sub = await reg?.pushManager?.getSubscription();
|
|
1399
|
-
if (sub) {
|
|
1400
|
-
const endpoint = sub.endpoint;
|
|
1401
|
-
await sub.unsubscribe();
|
|
1402
|
-
await call(POCKET_ENDPOINTS.pushUnsubscribe, { endpoint });
|
|
1403
|
-
}
|
|
1404
|
-
}
|
|
1405
|
-
} catch {
|
|
1406
|
-
}
|
|
1407
|
-
await call(POCKET_ENDPOINTS.pushSetEnabled, { enabled: false });
|
|
1408
|
-
setPushEnabled(false);
|
|
1409
|
-
setPushState("off");
|
|
1410
|
-
} catch (err) {
|
|
1411
|
-
setError(err?.message ?? "\u63A8\u9001\u5173\u95ED\u5931\u8D25 | failed to disable push");
|
|
1412
|
-
}
|
|
1413
|
-
};
|
|
1414
1341
|
const startTunnel = async () => {
|
|
1415
1342
|
setBusy(true);
|
|
1416
1343
|
setError(null);
|
|
@@ -1521,22 +1448,6 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
1521
1448
|
) : (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")
|
|
1522
1449
|
)
|
|
1523
1450
|
),
|
|
1524
|
-
// Web Push 状态 + 开关
|
|
1525
|
-
(0, import_react2.createElement)(
|
|
1526
|
-
"div",
|
|
1527
|
-
{ style: styles.block },
|
|
1528
|
-
(0, import_react2.createElement)(
|
|
1529
|
-
"div",
|
|
1530
|
-
{ style: { display: "flex", alignItems: "center", justifyContent: "space-between" } },
|
|
1531
|
-
(0, import_react2.createElement)("div", { style: { fontWeight: 600, fontSize: 13 } }, "\u{1F514} \u63A8\u9001\u901A\u77E5 | Push notifications"),
|
|
1532
|
-
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")
|
|
1533
|
-
),
|
|
1534
|
-
(0, import_react2.createElement)(
|
|
1535
|
-
"div",
|
|
1536
|
-
{ style: styles.muted },
|
|
1537
|
-
!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"
|
|
1538
|
-
)
|
|
1539
|
-
),
|
|
1540
1451
|
error ? (0, import_react2.createElement)("div", { style: { color: "var(--dsw-alias-state-error-primary,#dc2626)", fontSize: 12, marginTop: 8 } }, `\u274C ${error}`) : null,
|
|
1541
1452
|
// 页面最底部:反馈入口
|
|
1542
1453
|
(0, import_react2.createElement)(
|
package/client/index.jsx
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
// dsh-pocket 网页客户端:
|
|
2
|
-
// 1. 设置页签「手机访问」(局域网/公网二维码 +
|
|
2
|
+
// 1. 设置页签「手机访问」(局域网/公网二维码 + 更新/重启提示)
|
|
3
3
|
// 2. 移动端适配(移植自 MIT 项目 dsh-web-mobile,见 client/mobile/LICENSE.dsh-web-mobile)
|
|
4
|
-
// 3. Web Push:注册 Service Worker + 订阅推送(agent 跑完/出错 → 手机通知)
|
|
5
4
|
//
|
|
6
5
|
// 手机扫码打开的就是电脑上的 dsh web,实时同步;窄屏自动变成抽屉布局。
|
|
6
|
+
//
|
|
7
|
+
// 注:Web Push 已移除——浏览器推送依赖 Google FCM(Chrome)等境外服务,
|
|
8
|
+
// 国内直连被墙,普通用户用不了。专注扫码同屏这一件事。
|
|
7
9
|
|
|
8
10
|
import { createElement as h, useEffect, useState } from 'react';
|
|
9
11
|
|
|
@@ -13,36 +15,6 @@ import { mobileApply } from './mobile/mobile-apply.tsx';
|
|
|
13
15
|
const name = 'dsh-pocket';
|
|
14
16
|
const inject = ['slots', 'connection', 'layout', 'locale', 'sessionLogDownload'];
|
|
15
17
|
|
|
16
|
-
/** Web Push:注册 SW + 订阅(仅安全上下文可用:HTTPS 公网 / localhost)。 */
|
|
17
|
-
async function setupPush(rpcCall) {
|
|
18
|
-
try {
|
|
19
|
-
if (!('serviceWorker' in navigator) || !('PushManager' in window)) return 'unsupported';
|
|
20
|
-
if (!window.isSecureContext) return 'insecure'; // http://LAN-IP 无 Push API
|
|
21
|
-
const reg = await navigator.serviceWorker.register('/pocket-sw.js');
|
|
22
|
-
const vapid = await rpcCall(POCKET_ENDPOINTS.pushVapidKey, {});
|
|
23
|
-
if (!vapid?.ok) return 'host-error';
|
|
24
|
-
let sub = await reg.pushManager.getSubscription();
|
|
25
|
-
if (!sub) {
|
|
26
|
-
sub = await reg.pushManager.subscribe({
|
|
27
|
-
userVisibleOnly: true,
|
|
28
|
-
applicationServerKey: urlBase64ToUint8Array(vapid.value.publicKey),
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
const res = await rpcCall(POCKET_ENDPOINTS.pushSubscribe, { subscription: sub.toJSON() });
|
|
32
|
-
return res?.ok ? 'on' : 'host-error';
|
|
33
|
-
} catch {
|
|
34
|
-
return 'error';
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** VAPID 公钥(base64url)→ Uint8Array(pushManager.subscribe 需要)。 */
|
|
39
|
-
function urlBase64ToUint8Array(base64url) {
|
|
40
|
-
const pad = '='.repeat((4 - (base64url.length % 4)) % 4);
|
|
41
|
-
const b64 = (base64url + pad).replace(/-/g, '+').replace(/_/g, '/');
|
|
42
|
-
const raw = atob(b64);
|
|
43
|
-
return Uint8Array.from(raw, (c) => c.charCodeAt(0));
|
|
44
|
-
}
|
|
45
|
-
|
|
46
18
|
const styles = {
|
|
47
19
|
card: { background: 'var(--dsw-alias-bg-layer-1,#fff)', border: '1px solid var(--dsw-alias-border-l2,#e5e7eb)', borderRadius: 12, padding: '14px 16px', maxWidth: 480 },
|
|
48
20
|
block: { borderTop: '1px solid var(--dsw-alias-border-l2,#e5e7eb)', marginTop: 12, paddingTop: 12 },
|
|
@@ -58,8 +30,6 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
58
30
|
const [status, setStatus] = useState(null);
|
|
59
31
|
const [busy, setBusy] = useState(false);
|
|
60
32
|
const [error, setError] = useState(null);
|
|
61
|
-
const [pushEnabled, setPushEnabled] = useState(true); // 宿主开关
|
|
62
|
-
const [pushState, setPushState] = useState('checking'); // checking|on|unsupported|insecure|off
|
|
63
33
|
const [tunnelState, setTunnelState] = useState(null); // 隧道进度 {phase, detail, startedAt}
|
|
64
34
|
const [restartNotice, setRestartNotice] = useState(false); // 重启后提示
|
|
65
35
|
const [updateInfo, setUpdateInfo] = useState(null); // { current, latest, updating, result } | null
|
|
@@ -85,17 +55,6 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
85
55
|
return () => clearInterval(t);
|
|
86
56
|
}, []);
|
|
87
57
|
|
|
88
|
-
// 读取宿主开关状态;已开启时同步检查浏览器订阅状态(否则刷新后一直显示"检查中…")
|
|
89
|
-
useEffect(() => {
|
|
90
|
-
(async () => {
|
|
91
|
-
try {
|
|
92
|
-
const s = await call(POCKET_ENDPOINTS.pushStatus, {});
|
|
93
|
-
setPushEnabled(s.enabled);
|
|
94
|
-
if (s.enabled) setPushState(await setupPush(rpcCall));
|
|
95
|
-
} catch { /* 忽略瞬时失败 */ }
|
|
96
|
-
})();
|
|
97
|
-
}, []);
|
|
98
|
-
|
|
99
58
|
// 版本检测:host 当前版本 vs npm registry latest(registry 带 CORS *)
|
|
100
59
|
// 两种情况显示横幅:① 有新版可更新;② 磁盘已更新但进程还是旧代码(重启生效)
|
|
101
60
|
useEffect(() => {
|
|
@@ -144,39 +103,6 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
144
103
|
}
|
|
145
104
|
};
|
|
146
105
|
|
|
147
|
-
// 开启推送:宿主开关开 + 浏览器订阅(安全上下文才有效)
|
|
148
|
-
const enablePush = async () => {
|
|
149
|
-
try {
|
|
150
|
-
await call(POCKET_ENDPOINTS.pushSetEnabled, { enabled: true });
|
|
151
|
-
setPushEnabled(true);
|
|
152
|
-
setPushState(await setupPush(rpcCall));
|
|
153
|
-
} catch (err) {
|
|
154
|
-
setError(err?.message ?? '推送开启失败 | failed to enable push');
|
|
155
|
-
}
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
// 关闭推送:取消浏览器订阅 + 宿主开关关
|
|
159
|
-
const disablePush = async () => {
|
|
160
|
-
try {
|
|
161
|
-
try {
|
|
162
|
-
if ('serviceWorker' in navigator) {
|
|
163
|
-
const reg = await navigator.serviceWorker.getRegistration('/pocket-sw.js');
|
|
164
|
-
const sub = await reg?.pushManager?.getSubscription();
|
|
165
|
-
if (sub) {
|
|
166
|
-
const endpoint = sub.endpoint;
|
|
167
|
-
await sub.unsubscribe();
|
|
168
|
-
await call(POCKET_ENDPOINTS.pushUnsubscribe, { endpoint });
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
} catch { /* 浏览器侧失败不影响宿主开关 */ }
|
|
172
|
-
await call(POCKET_ENDPOINTS.pushSetEnabled, { enabled: false });
|
|
173
|
-
setPushEnabled(false);
|
|
174
|
-
setPushState('off');
|
|
175
|
-
} catch (err) {
|
|
176
|
-
setError(err?.message ?? '推送关闭失败 | failed to disable push');
|
|
177
|
-
}
|
|
178
|
-
};
|
|
179
|
-
|
|
180
106
|
const startTunnel = async () => {
|
|
181
107
|
setBusy(true);
|
|
182
108
|
setError(null);
|
|
@@ -268,23 +194,6 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
268
194
|
),
|
|
269
195
|
),
|
|
270
196
|
|
|
271
|
-
// Web Push 状态 + 开关
|
|
272
|
-
h('div', { style: styles.block },
|
|
273
|
-
h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between' } },
|
|
274
|
-
h('div', { style: { fontWeight: 600, fontSize: 13 } }, '🔔 推送通知 | Push notifications'),
|
|
275
|
-
pushEnabled
|
|
276
|
-
? h('button', { style: styles.btn, onClick: disablePush }, '关闭 | Off')
|
|
277
|
-
: h('button', { style: styles.primary, onClick: enablePush }, '开启 | On'),
|
|
278
|
-
),
|
|
279
|
-
h('div', { style: styles.muted },
|
|
280
|
-
!pushEnabled ? '已关闭:agent 跑完不会推送 | off: no notifications'
|
|
281
|
-
: pushState === 'on' ? '已开启:agent 跑完/出错时手机收到通知 | on: notified when tasks finish or fail'
|
|
282
|
-
: pushState === 'unsupported' ? '已开启(但当前浏览器不支持推送)| on, but this browser does not support push'
|
|
283
|
-
: pushState === 'insecure' ? '已开启,但当前路径不是 HTTPS——推送需要公网隧道或 localhost | on, but push needs HTTPS (public tunnel) or localhost'
|
|
284
|
-
: pushState === 'checking' ? '检查中… | checking…'
|
|
285
|
-
: '推送未生效 | push not active'),
|
|
286
|
-
),
|
|
287
|
-
|
|
288
197
|
error ? h('div', { style: { color: 'var(--dsw-alias-state-error-primary,#dc2626)', fontSize: 12, marginTop: 8 } }, `❌ ${error}`) : null,
|
|
289
198
|
|
|
290
199
|
// 页面最底部:反馈入口
|
package/lib/index.js
CHANGED
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
// 设置一级入口「手机访问」:
|
|
4
4
|
// - 局域网二维码:自动显示(代理随插件启动)
|
|
5
5
|
// - 公网二维码:点「开启公网」→ cloudflared 隧道 → 扫码即用,人在外面也能访问
|
|
6
|
-
// - Web Push:agent 跑完/出错 → 手机推送通知(需 HTTPS 公网路径或 localhost)
|
|
7
6
|
// - 更新提示:有新版本时显示一键更新按钮(dsh plugin update --latest)
|
|
8
7
|
// 手机看到的界面 = 电脑上的 dsh web,实时同步(WebSocket 透传)。
|
|
8
|
+
//
|
|
9
|
+
// 注:Web Push 已移除——浏览器推送依赖 Google FCM(Chrome)等境外服务,
|
|
10
|
+
// 国内直连被墙,普通用户用不了,且排障成本高。专注扫码同屏这一件事。
|
|
9
11
|
|
|
10
12
|
import { fileURLToPath } from 'node:url';
|
|
11
13
|
import { spawn } from 'node:child_process';
|
|
@@ -17,7 +19,6 @@ import { homedir } from 'node:os';
|
|
|
17
19
|
|
|
18
20
|
import { createPocketService } from './service.mjs';
|
|
19
21
|
import { installPocketRpc } from './web-rpc.js';
|
|
20
|
-
import { createPushService } from './push.mjs';
|
|
21
22
|
import { restartHost } from './restart.js';
|
|
22
23
|
|
|
23
24
|
const name = 'dsh-pocket';
|
|
@@ -85,14 +86,6 @@ function performUpdate(profile, { timeoutMs = 180_000 } = {}) {
|
|
|
85
86
|
});
|
|
86
87
|
}
|
|
87
88
|
|
|
88
|
-
// 服务 worker 源码(同源提供:/pocket-sw.js)
|
|
89
|
-
const SW_SOURCE = new URL('./pocket-sw.js', import.meta.url);
|
|
90
|
-
let swCache = null;
|
|
91
|
-
async function swScript() {
|
|
92
|
-
if (!swCache) swCache = await readFile(fileURLToPath(SW_SOURCE), 'utf8');
|
|
93
|
-
return swCache;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
89
|
export function apply(ctx, config = {}, internals = {}) {
|
|
97
90
|
const logger = ctx.logger?.(name) ?? console;
|
|
98
91
|
const dshPort = internals.dshPort ?? ctx.webServer?.port;
|
|
@@ -108,16 +101,9 @@ export function apply(ctx, config = {}, internals = {}) {
|
|
|
108
101
|
internals,
|
|
109
102
|
});
|
|
110
103
|
|
|
111
|
-
// Web Push 服务(web-push 库;测试可注入 stub)
|
|
112
|
-
const pushPromise = internals.pushPromise ?? createPushService({ internals });
|
|
113
|
-
pushPromise.catch((err) => logger.error('dsh-pocket: push service init failed | 推送服务初始化失败: %s', err?.message ?? err));
|
|
114
|
-
|
|
115
104
|
const disposers = [];
|
|
116
105
|
const disposeRpc = installPocketRpc(ctx, {
|
|
117
106
|
service,
|
|
118
|
-
// push 传 promise:插件启动早期 createPushService 可能还没 resolve,
|
|
119
|
-
// RPC 侧每次调用前动态 await(不能传空 fallback——会拿到空的 VAPID key)
|
|
120
|
-
push: internals.push ?? pushPromise,
|
|
121
107
|
runUpdate: internals.runUpdate ?? { currentVersion, perform: performUpdate, loadedVersion: () => loadedVersion },
|
|
122
108
|
restart: internals.restart ?? pocketRestart,
|
|
123
109
|
restartNotice: internals.restartNotice ?? readRestartNotice,
|
|
@@ -125,41 +111,6 @@ export function apply(ctx, config = {}, internals = {}) {
|
|
|
125
111
|
});
|
|
126
112
|
disposers.push(disposeRpc);
|
|
127
113
|
|
|
128
|
-
// 同源提供 Service Worker(Web Push 必需)
|
|
129
|
-
try {
|
|
130
|
-
const removeSw = ctx.webServer.register({
|
|
131
|
-
kind: 'exact',
|
|
132
|
-
path: '/pocket-sw.js',
|
|
133
|
-
handler: async (req, res) => {
|
|
134
|
-
res.writeHead(200, { 'content-type': 'application/javascript', 'cache-control': 'no-cache' });
|
|
135
|
-
res.end(await swScript());
|
|
136
|
-
},
|
|
137
|
-
});
|
|
138
|
-
disposers.push(removeSw);
|
|
139
|
-
} catch (err) {
|
|
140
|
-
logger.error('dsh-pocket: sw route register failed | SW 路由注册失败: %s', err?.message ?? err);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Agent 回合结束/出错 → 推送通知(有订阅才发)
|
|
144
|
-
const onSessionEvent = (session, event) => {
|
|
145
|
-
if (event?.type !== 'turn/end') return;
|
|
146
|
-
const reason = event?.data?.reason ?? event?.reason;
|
|
147
|
-
if (!reason) return;
|
|
148
|
-
void pushPromise.then(async (push) => {
|
|
149
|
-
try {
|
|
150
|
-
if (reason.kind === 'error') {
|
|
151
|
-
await push.notify({ title: '❌ 任务失败 | Task failed', body: String(reason.error?.message ?? '').slice(0, 120) || 'Agent 执行出错', url: '/' });
|
|
152
|
-
} else if (reason.kind === 'completed') {
|
|
153
|
-
await push.notify({ title: '✅ 任务完成 | Task done', body: 'Agent 已完成任务,点开查看结果', url: '/' });
|
|
154
|
-
}
|
|
155
|
-
} catch (err) {
|
|
156
|
-
logger.error('dsh-pocket: push send failed | 推送发送失败: %s', err?.message ?? err);
|
|
157
|
-
}
|
|
158
|
-
});
|
|
159
|
-
};
|
|
160
|
-
const offSession = ctx.on('session/event', onSessionEvent);
|
|
161
|
-
disposers.push(offSession);
|
|
162
|
-
|
|
163
114
|
// 代理随插件自动启动(局域网二维码开箱即用,零配置)
|
|
164
115
|
void service.startProxy().then((proxy) => {
|
|
165
116
|
logger.info('dsh-pocket: proxy ready on :%d | 局域网代理已就绪', proxy.port);
|
|
@@ -170,7 +121,7 @@ export function apply(ctx, config = {}, internals = {}) {
|
|
|
170
121
|
ctx.effect(() => async () => {
|
|
171
122
|
for (const d of disposers.reverse()) { try { d(); } catch { /* 忽略 */ } }
|
|
172
123
|
await service.dispose();
|
|
173
|
-
}, 'dsh-pocket: stop proxy
|
|
124
|
+
}, 'dsh-pocket: stop proxy and tunnel');
|
|
174
125
|
}
|
|
175
126
|
|
|
176
127
|
export { name, inject, readRestartNotice };
|
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,
|
|
20
|
+
export function installPocketRpc(ctx, { service, log = console, runUpdate = null, restart = null, restartNotice = 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 () => {};
|
|
@@ -32,9 +32,6 @@ export function installPocketRpc(ctx, { service, push, log = console, runUpdate
|
|
|
32
32
|
return ok({ ...redactStatus(await service.status()), restartNotice: notice });
|
|
33
33
|
};
|
|
34
34
|
|
|
35
|
-
// push 可能是 promise(插件启动早期 createPushService 尚未 resolve);动态取实例
|
|
36
|
-
const pushApi = await Promise.resolve(push).catch(() => null);
|
|
37
|
-
|
|
38
35
|
try {
|
|
39
36
|
if (endpoint === POCKET_ENDPOINTS.status) {
|
|
40
37
|
return await statusPayload();
|
|
@@ -47,29 +44,6 @@ export function installPocketRpc(ctx, { service, push, log = console, runUpdate
|
|
|
47
44
|
service.stopTunnel();
|
|
48
45
|
return await statusPayload();
|
|
49
46
|
}
|
|
50
|
-
if (endpoint === POCKET_ENDPOINTS.pushVapidKey) {
|
|
51
|
-
if (!pushApi) return fail('bad-request', '推送服务未就绪 | push service unavailable');
|
|
52
|
-
return ok({ publicKey: pushApi.vapidPublicKey() });
|
|
53
|
-
}
|
|
54
|
-
if (endpoint === POCKET_ENDPOINTS.pushSubscribe) {
|
|
55
|
-
if (!pushApi) return fail('bad-request', '推送服务未就绪 | push service unavailable');
|
|
56
|
-
const added = await pushApi.subscribe(payload?.subscription);
|
|
57
|
-
return ok({ subscribed: added, count: pushApi.count() });
|
|
58
|
-
}
|
|
59
|
-
if (endpoint === POCKET_ENDPOINTS.pushUnsubscribe) {
|
|
60
|
-
if (!pushApi) return fail('bad-request', '推送服务未就绪 | push service unavailable');
|
|
61
|
-
const removed = await pushApi.unsubscribe(payload?.endpoint);
|
|
62
|
-
return ok({ removed, count: pushApi.count() });
|
|
63
|
-
}
|
|
64
|
-
if (endpoint === POCKET_ENDPOINTS.pushStatus) {
|
|
65
|
-
if (!pushApi) return fail('bad-request', '推送服务未就绪 | push service unavailable');
|
|
66
|
-
return ok({ enabled: pushApi.isEnabled(), count: pushApi.count() });
|
|
67
|
-
}
|
|
68
|
-
if (endpoint === POCKET_ENDPOINTS.pushSetEnabled) {
|
|
69
|
-
if (!pushApi) return fail('bad-request', '推送服务未就绪 | push service unavailable');
|
|
70
|
-
const enabled = await pushApi.setEnabled(payload?.enabled === true);
|
|
71
|
-
return ok({ enabled, count: pushApi.count() });
|
|
72
|
-
}
|
|
73
47
|
if (endpoint === POCKET_ENDPOINTS.version) {
|
|
74
48
|
return ok({ current: runUpdate?.currentVersion?.() ?? null, loaded: runUpdate?.loadedVersion?.() ?? null });
|
|
75
49
|
}
|
package/package.json
CHANGED
|
@@ -27,8 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"qrcode": "^1.5.4",
|
|
30
|
-
"qrcode-terminal": "^0.12.0"
|
|
31
|
-
"web-push": "^3.6.7"
|
|
30
|
+
"qrcode-terminal": "^0.12.0"
|
|
32
31
|
},
|
|
33
32
|
"devDependencies": {
|
|
34
33
|
"esbuild": "^0.25.9",
|
|
@@ -77,5 +76,5 @@
|
|
|
77
76
|
"access": "public",
|
|
78
77
|
"registry": "https://registry.npmjs.org/"
|
|
79
78
|
},
|
|
80
|
-
"version": "1.0.
|
|
79
|
+
"version": "1.0.20"
|
|
81
80
|
}
|
package/lib/pocket-sw.js
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
// dsh-pocket Service Worker(经 webServer 同源提供:/pocket-sw.js)
|
|
2
|
-
// 职责:接收推送 → 显示通知;点击通知 → 聚焦/打开 DSH 页面。
|
|
3
|
-
// 图标用内联 data URI(不依赖 dsh 静态资源,避免 404 导致通知无图标)。
|
|
4
|
-
const ICON = 'data:image/svg+xml,' + encodeURIComponent(
|
|
5
|
-
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">' +
|
|
6
|
-
'<rect width="64" height="64" rx="14" fill="#4f6ef7"/>' +
|
|
7
|
-
'<rect x="16" y="12" width="32" height="40" rx="5" fill="#fff"/>' +
|
|
8
|
-
'<rect x="22" y="18" width="20" height="26" rx="2" fill="#4f6ef7"/>' +
|
|
9
|
-
'<circle cx="32" cy="49" r="2.2" fill="#fff"/>' +
|
|
10
|
-
'</svg>');
|
|
11
|
-
|
|
12
|
-
self.addEventListener('push', (event) => {
|
|
13
|
-
let data = {};
|
|
14
|
-
try {
|
|
15
|
-
data = event.data ? event.data.json() : {};
|
|
16
|
-
} catch { /* 非 JSON 载荷忽略 */ }
|
|
17
|
-
event.waitUntil(
|
|
18
|
-
self.registration.showNotification(data.title || 'DSH Pocket', {
|
|
19
|
-
body: data.body || '',
|
|
20
|
-
icon: ICON,
|
|
21
|
-
badge: ICON,
|
|
22
|
-
tag: data.tag || 'dsh-pocket',
|
|
23
|
-
data: { url: data.url || '/' },
|
|
24
|
-
}),
|
|
25
|
-
);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
self.addEventListener('notificationclick', (event) => {
|
|
29
|
-
event.notification.close();
|
|
30
|
-
const url = event.notification.data?.url || '/';
|
|
31
|
-
event.waitUntil(
|
|
32
|
-
self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then((windowClients) => {
|
|
33
|
-
for (const client of windowClients) {
|
|
34
|
-
if ('focus' in client) return client.focus();
|
|
35
|
-
}
|
|
36
|
-
return self.clients.openWindow(url);
|
|
37
|
-
}),
|
|
38
|
-
);
|
|
39
|
-
});
|
package/lib/push.mjs
DELETED
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
// dsh-pocket Web Push 服务(Agent 跑完/出错 → 手机推送通知,即使没开页面)
|
|
2
|
-
//
|
|
3
|
-
// 原理:浏览器 Push API(Service Worker + pushManager.subscribe)把订阅
|
|
4
|
-
// 交给我们,我们在 dsh web 进程内用 web-push(VAPID 认证 + RFC 8030 协议)
|
|
5
|
-
// 直接推送到浏览器厂商的推送服务(Chrome→FCM / Firefox→Mozilla / Safari→APNs)。
|
|
6
|
-
// 免费、无需第三方账号;VAPID 密钥与订阅存本机。
|
|
7
|
-
//
|
|
8
|
-
// ⚠️ 硬性前提:Web Push 只在**安全上下文**可用(HTTPS 或 localhost)。
|
|
9
|
-
// - 公网隧道 https://xxx.trycloudflare.com ✅(人在外面的主场景)
|
|
10
|
-
// - 桌面 localhost:3080 ✅
|
|
11
|
-
// - 局域网 http://192.168.x.x ❌(明文 HTTP 没有 Push API)
|
|
12
|
-
|
|
13
|
-
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
14
|
-
import { homedir } from 'node:os';
|
|
15
|
-
import { join, dirname } from 'node:path';
|
|
16
|
-
import { createRequire } from 'node:module';
|
|
17
|
-
|
|
18
|
-
const require = createRequire(import.meta.url);
|
|
19
|
-
|
|
20
|
-
const DATA_REL = join('dsh-pocket', 'push');
|
|
21
|
-
const VAPID_FILE = 'vapid.json';
|
|
22
|
-
const SUBS_FILE = 'subscriptions.json';
|
|
23
|
-
const SETTINGS_FILE = 'settings.json';
|
|
24
|
-
|
|
25
|
-
function defaultWebPush() {
|
|
26
|
-
// web-push 是 CJS,ESM 里必须经 createRequire 加载
|
|
27
|
-
return require('web-push');
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* 创建推送服务。
|
|
32
|
-
* @param {object} opts
|
|
33
|
-
* @param {string} [opts.home] $DSH_HOME(默认 ~/.dsh)
|
|
34
|
-
* @param {object} [opts.webpush] web-push 库(测试注入 stub)
|
|
35
|
-
* @param {string} [opts.subject] VAPID subject(mailto:)
|
|
36
|
-
* @param {object} [opts.log] 日志(默认 console)
|
|
37
|
-
* @param {object} [opts.internals] 测试注入:mkdir/write/read
|
|
38
|
-
* @returns {Promise<PushService>}
|
|
39
|
-
*/
|
|
40
|
-
export async function createPushService({
|
|
41
|
-
home,
|
|
42
|
-
webpush = defaultWebPush(),
|
|
43
|
-
subject = 'mailto:shaobeichen@outlook.com',
|
|
44
|
-
log = console,
|
|
45
|
-
internals = {},
|
|
46
|
-
} = {}) {
|
|
47
|
-
const dshHome = home ?? process.env.DSH_HOME ?? join(homedir(), '.dsh');
|
|
48
|
-
const dir = join(dshHome, DATA_REL);
|
|
49
|
-
const vapidPath = join(dir, VAPID_FILE);
|
|
50
|
-
const subsPath = join(dir, SUBS_FILE);
|
|
51
|
-
const settingsPath = join(dir, SETTINGS_FILE);
|
|
52
|
-
const mkdirFn = internals.mkdir ?? mkdir;
|
|
53
|
-
const read = internals.read ?? readFile;
|
|
54
|
-
const write = internals.write ?? writeFile;
|
|
55
|
-
|
|
56
|
-
await mkdirFn(dir, { recursive: true });
|
|
57
|
-
|
|
58
|
-
// 开关状态:默认开,持久化到 settings.json
|
|
59
|
-
let enabled = true;
|
|
60
|
-
try {
|
|
61
|
-
const s = JSON.parse(await read(settingsPath, 'utf8'));
|
|
62
|
-
if (typeof s.enabled === 'boolean') enabled = s.enabled;
|
|
63
|
-
} catch { /* 默认开 */ }
|
|
64
|
-
if (internals.enabled !== undefined) enabled = internals.enabled === true;
|
|
65
|
-
|
|
66
|
-
// VAPID 密钥:生成一次,持久化
|
|
67
|
-
let vapid;
|
|
68
|
-
try {
|
|
69
|
-
vapid = JSON.parse(await read(vapidPath, 'utf8'));
|
|
70
|
-
} catch {
|
|
71
|
-
vapid = webpush.generateVAPIDKeys();
|
|
72
|
-
await write(vapidPath, JSON.stringify(vapid, null, 2) + '\n', { mode: 0o600 });
|
|
73
|
-
}
|
|
74
|
-
webpush.setVapidDetails(subject, vapid.publicKey, vapid.privateKey);
|
|
75
|
-
|
|
76
|
-
// 订阅集合:endpoint → subscription
|
|
77
|
-
let subs = new Map();
|
|
78
|
-
try {
|
|
79
|
-
for (const s of JSON.parse(await read(subsPath, 'utf8')) ?? []) {
|
|
80
|
-
if (s?.endpoint) subs.set(s.endpoint, s);
|
|
81
|
-
}
|
|
82
|
-
} catch { /* 空开始 */ }
|
|
83
|
-
|
|
84
|
-
const persist = async () => {
|
|
85
|
-
await write(subsPath, JSON.stringify([...subs.values()], null, 2) + '\n', { mode: 0o600 });
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
return {
|
|
89
|
-
vapidPublicKey: () => vapid.publicKey,
|
|
90
|
-
count: () => subs.size,
|
|
91
|
-
isEnabled: () => enabled,
|
|
92
|
-
|
|
93
|
-
/** 设置推送总开关(持久化)。 */
|
|
94
|
-
async setEnabled(value) {
|
|
95
|
-
enabled = value === true;
|
|
96
|
-
await write(settingsPath, JSON.stringify({ enabled }, null, 2) + '\n', { mode: 0o600 });
|
|
97
|
-
return enabled;
|
|
98
|
-
},
|
|
99
|
-
|
|
100
|
-
/** 记录浏览器订阅。 */
|
|
101
|
-
async subscribe(subscription) {
|
|
102
|
-
if (!subscription?.endpoint || !subscription?.keys) return false;
|
|
103
|
-
subs.set(subscription.endpoint, subscription);
|
|
104
|
-
await persist();
|
|
105
|
-
return true;
|
|
106
|
-
},
|
|
107
|
-
|
|
108
|
-
/** 按 endpoint 取消订阅。 */
|
|
109
|
-
async unsubscribe(endpoint) {
|
|
110
|
-
const removed = subs.delete(endpoint);
|
|
111
|
-
if (removed) await persist();
|
|
112
|
-
return removed;
|
|
113
|
-
},
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* 推送通知到所有订阅;失效订阅自动清理。
|
|
117
|
-
* @param {object} opts
|
|
118
|
-
* @param {string} opts.title
|
|
119
|
-
* @param {string} [opts.body]
|
|
120
|
-
* @param {string} [opts.url] 点击通知打开的地址(默认 /)
|
|
121
|
-
* @param {object} [opts.data] 额外载荷
|
|
122
|
-
*/
|
|
123
|
-
async notify({ title, body = '', url = '/', data = {} }) {
|
|
124
|
-
if (!enabled || subs.size === 0) return 0;
|
|
125
|
-
const payload = JSON.stringify({ title, body, url, ...data });
|
|
126
|
-
let sent = 0;
|
|
127
|
-
for (const [endpoint, sub] of [...subs.entries()]) {
|
|
128
|
-
try {
|
|
129
|
-
await webpush.sendNotification(sub, payload, { TTL: 600 });
|
|
130
|
-
sent += 1;
|
|
131
|
-
} catch (err) {
|
|
132
|
-
// 410 Gone / 404 = 订阅失效,清理
|
|
133
|
-
const code = err?.statusCode;
|
|
134
|
-
if (code === 410 || code === 404) {
|
|
135
|
-
subs.delete(endpoint);
|
|
136
|
-
await persist();
|
|
137
|
-
} else {
|
|
138
|
-
// 完整记录状态码与响应体,方便判断是 FCM 拒绝(401/403)还是网络/代理问题
|
|
139
|
-
log.warn?.(
|
|
140
|
-
'dsh-pocket: push send failed | 推送失败: %s (status=%s, body=%s, endpoint=%s…)',
|
|
141
|
-
err?.message ?? String(err),
|
|
142
|
-
code ?? '?',
|
|
143
|
-
String(err?.body ?? '').slice(0, 200),
|
|
144
|
-
String(endpoint).slice(0, 48),
|
|
145
|
-
);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
return sent;
|
|
150
|
-
},
|
|
151
|
-
|
|
152
|
-
async dispose() { /* 无长连接需要清理 */ },
|
|
153
|
-
};
|
|
154
|
-
}
|