dsh-pocket 1.4.8 → 1.6.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/lib/index.js CHANGED
@@ -148,6 +148,8 @@ export function apply(ctx, config = {}, internals = {}) {
148
148
  // 代理随插件自动启动(局域网二维码开箱即用,零配置)
149
149
  void service.startProxy().then((proxy) => {
150
150
  logger.info('dsh-pocket: proxy ready on :%d | 局域网代理已就绪', proxy.port);
151
+ // 自动恢复上次开启的公网隧道(DSH 重启后 cloudflared 子进程被杀,issue #11)
152
+ void service.restoreTunnelIfNeeded?.().catch(() => {});
151
153
  }).catch((err) => {
152
154
  logger.error('dsh-pocket: proxy start failed | 代理启动失败: %s', err?.message ?? err);
153
155
  });
package/lib/proxy.mjs CHANGED
@@ -13,6 +13,7 @@
13
13
 
14
14
  import { createServer } from 'node:http';
15
15
  import { request as httpRequest } from 'node:http';
16
+ import { createGzip, createBrotliCompress } from 'node:zlib';
16
17
 
17
18
  const DEFAULT_UPSTREAM = { host: '127.0.0.1', port: 3080 };
18
19
 
@@ -98,6 +99,37 @@ export function createPocketProxy({ port = 3081, host = '0.0.0.0', upstream = DE
98
99
  proxyRes.on('error', () => res.destroy());
99
100
  return;
100
101
  }
102
+ // 大 JSON/text 响应**流式压缩**(issue #12):长会话历史一次返回 17MB+,
103
+ // 局域网直连与隧道段都吃满带宽;压缩到 ~1.3MB。跳过已压缩、SSE 流
104
+ // (/api/events.* 原样透传)、HTML(走上面的注入分支)。
105
+ const acceptEncoding = String(req.headers['accept-encoding'] ?? '');
106
+ const canGzip = /\bgzip\b/.test(acceptEncoding);
107
+ const canBr = /\bbr\b/.test(acceptEncoding);
108
+ const isEventStream = contentType.includes('text/event-stream');
109
+ const knownLen = Number(proxyRes.headers['content-length'] || 0);
110
+ const shouldCompress = (canGzip || canBr)
111
+ && !isCompressed(proxyRes.headers)
112
+ && !isEventStream
113
+ && (contentType.includes('application/json') || contentType.startsWith('text/'))
114
+ && (knownLen === 0 || knownLen >= 1024);
115
+ if (shouldCompress) {
116
+ const enc = canBr ? 'br' : 'gzip';
117
+ const outHeaders = { ...proxyRes.headers };
118
+ delete outHeaders['content-length'];
119
+ delete outHeaders['transfer-encoding'];
120
+ outHeaders['content-encoding'] = enc;
121
+ res.writeHead(proxyRes.statusCode ?? 200, outHeaders);
122
+ const z = enc === 'br' ? createBrotliCompress() : createGzip();
123
+ proxyRes.pipe(z).pipe(res);
124
+ // 任一端断开都要清理(含压缩流)。注意:不能用 proxyRes 的 'close'
125
+ // 来掐 res——正常结束后 close 也会触发,此时压缩流可能还没写完,
126
+ // 会误杀连接;异常中止用 'aborted'。
127
+ res.on('close', () => { proxyRes.destroy(); z.destroy(); });
128
+ proxyRes.on('error', () => { z.destroy(); res.destroy(); });
129
+ proxyRes.on('aborted', () => { z.destroy(); res.destroy(); });
130
+ z.on('error', () => res.destroy());
131
+ return;
132
+ }
101
133
  res.writeHead(proxyRes.statusCode ?? 502, proxyRes.headers);
102
134
  proxyRes.pipe(res);
103
135
  // 任一端断开都要清理另一端:客户端断连销毁上游流(不留僵尸),
package/lib/service.mjs CHANGED
@@ -8,6 +8,8 @@
8
8
 
9
9
  import { networkInterfaces } from 'node:os';
10
10
  import { createRequire } from 'node:module';
11
+ import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
12
+ import { join, dirname } from 'node:path';
11
13
  import { createPocketProxy } from './proxy.mjs';
12
14
  import { startQuickTunnel } from './tunnel.mjs';
13
15
 
@@ -112,6 +114,21 @@ export function createPocketService({
112
114
  return qrCache.get(text);
113
115
  }
114
116
 
117
+ // 公网隧道自动恢复(issue #11):DSH 重启后 cloudflared 子进程被杀、隧道消失,
118
+ // 插件无从知晓。启动时检查持久化的「隧道开启中」标记,自动重新拉起。
119
+ const autoStatePath = home ? join(home, 'dsh-pocket', 'tunnel-auto.json') : null;
120
+ async function persistAutoTunnel() {
121
+ if (!autoStatePath) return;
122
+ try {
123
+ await mkdir(dirname(autoStatePath), { recursive: true });
124
+ await writeFile(autoStatePath, JSON.stringify({ at: Date.now() }), 'utf8');
125
+ } catch { /* 忽略 */ }
126
+ }
127
+ async function clearAutoTunnel() {
128
+ if (!autoStatePath) return;
129
+ try { await rm(autoStatePath, { force: true }); } catch { /* 忽略 */ }
130
+ }
131
+
115
132
  return {
116
133
  dshPort,
117
134
  /** 启动局域网代理(幂等)。端口被占(EADDRINUSE,如桌面版与普通环境同时运行)时自动尝试下一个端口。 */
@@ -166,6 +183,8 @@ export function createPocketService({
166
183
  tunnelState.phase = 'error';
167
184
  tunnelState.detail = `隧道进程退出(code=${code})| tunnel process exited`;
168
185
  });
186
+ // 记录「隧道开启中」,供重启后自动恢复(issue #11)
187
+ void persistAutoTunnel();
169
188
  return tunnel.url;
170
189
  } catch (err) {
171
190
  // stopTunnel 触发的 abort 不算错误:保持 idle,别把状态刷成 error
@@ -195,6 +214,25 @@ export function createPocketService({
195
214
  tunnelState.phase = 'idle';
196
215
  tunnelState.detail = '';
197
216
  tunnelState.startedAt = null;
217
+ void clearAutoTunnel(); // 手动关闭后不再自动恢复
218
+ },
219
+
220
+ /** 启动时自动恢复上次开启的公网隧道(DSH 重启后 cloudflared 子进程被杀,issue #11)。 */
221
+ async restoreTunnelIfNeeded() {
222
+ if (!autoStatePath || tunnel || tunnelPromise) return;
223
+ let has = false;
224
+ try {
225
+ const raw = await readFile(autoStatePath, 'utf8');
226
+ has = /"at"\s*:/.test(raw);
227
+ } catch { return; } // 无标记 → 不恢复
228
+ if (!has) return;
229
+ try {
230
+ await this.startTunnel();
231
+ console.log('dsh-pocket: public tunnel auto-restored | 已自动恢复公网隧道');
232
+ } catch (err) {
233
+ // 恢复失败保留标记(下次启动再试);网络问题见 README 排障
234
+ console.warn('dsh-pocket: tunnel auto-restore failed | 自动恢复隧道失败: %s', err?.message ?? err);
235
+ }
198
236
  },
199
237
 
200
238
  /** 状态快照(RPC 返回,不含敏感信息;二维码 data URL 本地生成 + 缓存)。 */
package/package.json CHANGED
@@ -76,5 +76,5 @@
76
76
  "access": "public",
77
77
  "registry": "https://registry.npmjs.org/"
78
78
  },
79
- "version": "1.4.8"
79
+ "version": "1.6.0"
80
80
  }