dsh-remote-hub 0.3.4 → 0.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/lib/index.js CHANGED
@@ -16,7 +16,7 @@ import { start } from './supervisor.js'
16
16
 
17
17
  export const name = 'dsh-remote-hub'
18
18
 
19
- export const version = '0.3.4'
19
+ export const version = '0.4.0'
20
20
 
21
21
  /** No host services are required: the plugin only needs Node builtins. */
22
22
  export const inject = []
@@ -22,11 +22,13 @@ export const PAIR_PREFIX = '/_dsh-remote/pair'
22
22
  * fastest one at connect time: `l` = LAN (private address), `u` = public tunnel. Either may be null
23
23
  * when that transport is unavailable that moment.
24
24
  */
25
- export function buildPairPayload({ lan, publicUrl, code, expiresAt }) {
26
- // `l` is a LIST of LAN URLs: a machine can have several private adapters and only one of them is
27
- // reachable from the phone, so the client probes them in order instead of trusting our pick.
25
+ export function buildPairPayload({ lan, relay, tunnel, code, expiresAt }) {
26
+ // v3 ships all three transports so one scan can fill every tab:
27
+ // l = LAN addresses (list; only one adapter may be reachable from the phone)
28
+ // r = self-hosted relay (Aliyun/Caddy + reverse SSH)
29
+ // u = auto tunnel (ngrok / cloudflared)
28
30
  const lanList = Array.isArray(lan) ? lan.filter(Boolean) : lan ? [lan] : []
29
- return JSON.stringify({ v: 2, l: lanList, u: publicUrl ?? null, c: code, e: expiresAt })
31
+ return JSON.stringify({ v: 3, l: lanList, r: relay ?? null, u: tunnel ?? null, c: code, e: expiresAt })
30
32
  }
31
33
 
32
34
  /**
@@ -76,10 +78,12 @@ export async function handlePairing({ req, res, current, store }) {
76
78
  res.end(
77
79
  JSON.stringify({
78
80
  ok: true,
79
- // `url` keeps the old single-address shape working; new clients read lan/public.
80
- url: snapshot.public ?? (Array.isArray(snapshot.lan) ? snapshot.lan[0] : snapshot.lan) ?? null,
81
+ // `url`/`public` keep the old single-address shape working; new clients read the three.
82
+ url: snapshot.relay ?? snapshot.tunnel ?? null,
81
83
  lan: Array.isArray(snapshot.lan) ? snapshot.lan : snapshot.lan ? [snapshot.lan] : [],
82
- public: snapshot.public ?? null,
84
+ relay: snapshot.relay ?? null,
85
+ tunnel: snapshot.tunnel ?? null,
86
+ public: snapshot.relay ?? snapshot.tunnel ?? null,
83
87
  cookie: result.cookie,
84
88
  issuedAt: result.issuedAt,
85
89
  }),
@@ -89,7 +93,7 @@ export async function handlePairing({ req, res, current, store }) {
89
93
 
90
94
  // Any other GET under the prefix hands out a fresh code.
91
95
  const { code, expiresAt, ttlMs } = store.issue(snapshot.cookie)
92
- const payload = buildPairPayload({ lan: snapshot.lan, publicUrl: snapshot.public, code, expiresAt })
96
+ const payload = buildPairPayload({ lan: snapshot.lan, relay: snapshot.relay, tunnel: snapshot.tunnel, code, expiresAt })
93
97
 
94
98
  if (url === PAIR_PREFIX + '/svg') {
95
99
  const svg = await QRCode.toString(payload, { type: 'svg', margin: 1, width: 320, errorCorrectionLevel: 'M' })
@@ -122,7 +126,7 @@ export async function handlePairing({ req, res, current, store }) {
122
126
  <p>配对码 <code>${code}</code> · ${minutes} 分钟内有效,且只能用一次</p>
123
127
  <div class="qr">${svg}</div>
124
128
  <p class="ok">扫码后自动填入地址并换取登录凭据</p>
125
- <p class="addr">${(Array.isArray(snapshot.lan) ? snapshot.lan : snapshot.lan ? [snapshot.lan] : []).map((u) => `局域网:${u}`).join('<br>')}${snapshot.public ? `<br>公网:${snapshot.public}` : ''}</p>
129
+ <p class="addr">${(Array.isArray(snapshot.lan) ? snapshot.lan : snapshot.lan ? [snapshot.lan] : []).map((u) => `局域网:${u}`).join('<br>')}${snapshot.relay ? `<br>中转:${snapshot.relay}` : ''}${snapshot.tunnel ? `<br>隧道:${snapshot.tunnel}` : ''}</p>
126
130
  <p class="addr">扫一次会同时存下局域网与公网地址,之后自动优先走局域网</p>
127
131
  </div></body></html>`
128
132
  res.writeHead(200, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' })
package/lib/supervisor.js CHANGED
@@ -284,7 +284,8 @@ export async function start(config = {}) {
284
284
  // Read at redeem time so a tunnel URL that appears later is still handed to the phone.
285
285
  current: () => ({
286
286
  lan: lanUrlsFor(actualBridgePort()),
287
- public: lastPublicUrl ?? null,
287
+ relay: relayUrl,
288
+ tunnel: tunnelRef?.publicUrl ?? null,
288
289
  cookie,
289
290
  }),
290
291
  }
@@ -385,7 +386,6 @@ export async function start(config = {}) {
385
386
 
386
387
  const tunnel = await startTunnel({
387
388
  kind: tunnelKind,
388
- publicUrl,
389
389
  bridgePort: bridge.port,
390
390
  authority,
391
391
  domain,
@@ -405,14 +405,15 @@ export async function start(config = {}) {
405
405
  },
406
406
  })
407
407
 
408
- lastPublicUrl = tunnel.publicUrl
409
- // A configured relay can be "up" while its reverse tunnel is down; probe it so status does not
410
- // advertise a URL that cannot answer.
411
- let publicReachable = null
412
- if (publicUrl && tunnel.publicUrl) {
413
- publicReachable = await probeHealthUrl(`${tunnel.publicUrl}/_dsh-remote/health`)
414
- if (publicReachable === false) {
415
- log(`dsh-remote: WARNING ${tunnel.publicUrl} does not answer — is the relay's SSH tunnel up?`)
408
+ // The self-hosted relay (if configured) and the auto tunnel are reported SEPARATELY: a self-use
409
+ // setup wants both (relay for 4G, ngrok as a second path), and the phone decides which to use.
410
+ const relayUrl = String(publicUrl || '').trim().replace(/\/+$/, '') || null
411
+ lastPublicUrl = relayUrl ?? tunnel.publicUrl
412
+ let relayReachable = null
413
+ if (relayUrl) {
414
+ relayReachable = await probeHealthUrl(`${relayUrl}/_dsh-remote/health`)
415
+ if (relayReachable === false) {
416
+ log(`dsh-remote: WARNING ${relayUrl} does not answer — is the relay's SSH tunnel up?`)
416
417
  }
417
418
  }
418
419
  const ip = ipForPairing
@@ -423,12 +424,11 @@ export async function start(config = {}) {
423
424
  authority,
424
425
  upstreamPort: discovery.port || null,
425
426
  lan: { ip, url: ip ? `http://${ip}:${bridge.port}` : null },
426
- public: {
427
- url: tunnel.publicUrl,
428
- kind: tunnel.kind,
429
- ...(publicReachable === null ? {} : { reachable: publicReachable }),
430
- ...(tunnel.note ? { note: tunnel.note } : {}),
431
- },
427
+ public: relayUrl
428
+ ? { url: relayUrl, kind: 'relay', ...(relayReachable === null ? {} : { reachable: relayReachable }) }
429
+ : { url: tunnel.publicUrl, kind: tunnel.kind, ...(tunnel.note ? { note: tunnel.note } : {}) },
430
+ relay: relayUrl ? { url: relayUrl, ...(relayReachable === null ? {} : { reachable: relayReachable }) } : null,
431
+ tunnel: tunnel.publicUrl ? { url: tunnel.publicUrl, kind: tunnel.kind, ...(tunnel.note ? { note: tunnel.note } : {}) } : null,
432
432
  cookie,
433
433
  cookieExpiresAt: expiresAt ?? cookieExpiry(cookie),
434
434
  pairing: {
package/lib/tunnel.js CHANGED
@@ -148,16 +148,7 @@ export async function ensureCloudflared({ log = () => {} } = {}) {
148
148
  * @param {(msg: string) => void} [options.log]
149
149
  * @returns {Promise<{publicUrl: string|null, kind: string, close: () => void, note?: string}>}
150
150
  */
151
- export async function startTunnel({ kind = 'auto', bridgePort, authority, domain = '', publicUrl = '', log = () => {} }) {
152
- // An explicitly configured public URL (a self-hosted relay, e.g. the Aliyun box) wins over any
153
- // detected tunnel: for that setup there is nothing to launch here, the relay is kept alive by its
154
- // own reverse SSH tunnel.
155
- if (publicUrl) {
156
- const trimmed = String(publicUrl).trim().replace(/\/+$/, '')
157
- log(`using the configured public URL: ${trimmed}`)
158
- return { publicUrl: trimmed, kind: 'relay', close: () => {} }
159
- }
160
- if (kind === 'off') return { publicUrl: null, kind: 'none', close: () => {}, note: 'tunnel disabled by config' }
151
+ export async function startTunnel({ kind = 'auto', bridgePort, authority, domain = '', publicUrl = '', log = () => {} }) { if (kind === 'off') return { publicUrl: null, kind: 'none', close: () => {}, note: 'tunnel disabled by config' }
161
152
 
162
153
  const wantNgrok = kind === 'auto' || kind === 'ngrok'
163
154
  const wantCloudflared = kind === 'auto' || kind === 'cloudflared'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-remote-hub",
3
- "version": "0.3.4",
3
+ "version": "0.4.0",
4
4
  "description": "DSH 插件:把电脑上的 DeepSeek Harness 变成手机可访问的服务——起桥接、自动开隧道(ngrok / cloudflared)、签发 30 天 Cookie,并输出地址与凭据,配合 DSH Remote Android App 使用。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",