dsh-remote-hub 0.3.4 → 0.4.1

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.md CHANGED
@@ -19,9 +19,9 @@ Kotlin/Compose 直连 DSH 自带的 HTTP API + WebSocket 网关。
19
19
 
20
20
  ## 下载手机端 App
21
21
 
22
- **DSH Remote v4.21.0** · Android 8.0 (API 26) 及以上 · 2.8 MB
22
+ **DSH Remote v4.21.2** · Android 8.0 (API 26) 及以上 · 2.8 MB
23
23
 
24
- [⬇️ 下载 APK](https://github.com/liaoguogang-hub/dsh-remote-public/releases/download/v4.21.0/DSH-Remote-v4.21.0.apk) ·
24
+ [⬇️ 下载 APK](https://github.com/liaoguogang-hub/dsh-remote-public/releases/download/v4.21.2/DSH-Remote-v4.21.2.apk) ·
25
25
  [全部版本](https://github.com/liaoguogang-hub/dsh-remote-public/releases)
26
26
 
27
27
  安装后应用名「DSH Remote」,包名 `com.dsh.remote.ngrok`(可与作者的 `DSH` 自用版共存)。
@@ -5,7 +5,7 @@
5
5
  * dsh-remote status show LAN URL, public URL, cookie and upstream port
6
6
  * dsh-remote connect print the two lines to paste into the phone
7
7
  * dsh-remote cookie print only the cookie
8
- * dsh-remote start [--open] [--public-url=URL] run the supervisor; --open 打印地址并打开配对页
8
+ * dsh-remote start [--open] [--restart] [--public-url=URL] --restart 会先停掉配置不同的旧桥接
9
9
  * dsh-remote stop ask a foreground `start` to stop (best effort, via status file)
10
10
  * dsh-remote doctor check every prerequisite in one pass
11
11
  * dsh-remote qr open the pairing page (QR) in the default browser
@@ -260,9 +260,26 @@ async function main() {
260
260
  break
261
261
  case 'start': {
262
262
  const wantOpen = process.argv.includes('--open')
263
+ const wantRestart = process.argv.includes('--restart')
263
264
  // --public-url=https://relay.example advertises a self-hosted relay in the QR payload.
264
265
  const publicArg = process.argv.find((a) => a.startsWith('--public-url='))
265
266
  const publicUrl = publicArg ? publicArg.slice('--public-url='.length) : process.env.DSH_REMOTE_PUBLIC_URL || ''
267
+ // A launcher that carries its own transport config must not silently adopt a bridge started
268
+ // with different config (that is how a public launcher ended up advertising a self-use relay).
269
+ if (wantRestart) {
270
+ const live = await bridgeHealthy(Number(process.env.DSH_REMOTE_PORT || 6300))
271
+ const mismatch = live && (live.relay || null) !== (publicUrl || null)
272
+ if (mismatch && live.pid) {
273
+ console.log(`已有桥接配置不同(relay=${live.relay ?? '无'}),先停止它(PID ${live.pid})再按当前配置启动`)
274
+ try {
275
+ process.kill(Number(live.pid))
276
+ } catch (err) {
277
+ console.log(`停止失败:${err.message}`)
278
+ }
279
+ await new Promise((r) => setTimeout(r, 1200))
280
+ }
281
+ }
282
+
266
283
  let handle
267
284
  try {
268
285
  handle = await start({ publicUrl, log: (m) => console.log(m) })
package/lib/bridge.js CHANGED
@@ -68,7 +68,7 @@ function trimSessionList(body) {
68
68
  * @param {(msg: string) => void} [options.log]
69
69
  * @returns {Promise<{close: () => Promise<void>, port: number}>}
70
70
  */
71
- export function startBridge({ listenPort, authority, upstream, pairing = null, log = () => {} }) {
71
+ export function startBridge({ listenPort, authority, upstream, pairing = null, relayUrl = null, log = () => {} }) {
72
72
  const forward = (req, res, attempt = 0) =>
73
73
  new Promise((resolve) => {
74
74
  upstream.resolve(attempt > 0).then((port) => {
@@ -155,6 +155,10 @@ export function startBridge({ listenPort, authority, upstream, pairing = null, l
155
155
  const body = JSON.stringify({
156
156
  ok: true,
157
157
  service: 'dsh-remote-bridge',
158
+ pid: process.pid,
159
+ // Whether this bridge advertises a self-hosted relay; a launcher that expects none must not
160
+ // adopt a bridge that has one, or the public app would show the self-use relay.
161
+ relay: relayUrl || null,
158
162
  // A bridge built before pairing existed has no such route; callers use this flag to decide
159
163
  // whether an already-listening port can be adopted or must be bypassed.
160
164
  pairing: pairing != null,
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.1'
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
@@ -274,6 +274,10 @@ export async function start(config = {}) {
274
274
  const lanUrlsFor = (bridgePort) =>
275
275
  lanAddresses().map((address) => `http://${address}:${bridgePort}`)
276
276
 
277
+ // The self-hosted relay (if configured) and the auto tunnel are reported SEPARATELY: a self-use
278
+ // setup wants both (relay for 4G, ngrok as a second path), and the phone decides which to use.
279
+ const relayUrl = String(publicUrl || '').trim().replace(/\/+$/, '') || null
280
+
277
281
  let bridgeInstance = null
278
282
  const actualBridgePort = () => (bridgeInstance ? bridgeInstance.port : Number(port) || 6300)
279
283
 
@@ -284,7 +288,8 @@ export async function start(config = {}) {
284
288
  // Read at redeem time so a tunnel URL that appears later is still handed to the phone.
285
289
  current: () => ({
286
290
  lan: lanUrlsFor(actualBridgePort()),
287
- public: lastPublicUrl ?? null,
291
+ relay: relayUrl,
292
+ tunnel: tunnelRef?.publicUrl ?? null,
288
293
  cookie,
289
294
  }),
290
295
  }
@@ -311,6 +316,7 @@ export async function start(config = {}) {
311
316
  authority,
312
317
  upstream: discovery,
313
318
  pairing,
319
+ relayUrl,
314
320
  log,
315
321
  })
316
322
  } catch (err) {
@@ -336,7 +342,13 @@ export async function start(config = {}) {
336
342
  authority: existing.authority ?? authority,
337
343
  upstreamPort: existing.upstreamPort ?? null,
338
344
  lan: { ip, url: ip ? `http://${ip}:${livePort}` : null },
339
- public: prev.public ?? { url: null, kind: 'none', note: 'served by an existing bridge' },
345
+ // Never inherit the previous run's public address: it may describe a different transport
346
+ // (e.g. a self-use relay) than the launcher that is running now.
347
+ public: existing.relay
348
+ ? { url: existing.relay, kind: 'relay' }
349
+ : { url: null, kind: 'none', note: 'served by an existing bridge' },
350
+ relay: existing.relay ? { url: existing.relay } : null,
351
+ tunnel: null,
340
352
  cookie: prev.cookie ?? null,
341
353
  cookieExpiresAt: prev.cookieExpiresAt ?? null,
342
354
  pairing: { url: prev.public?.url ?? (ip ? `http://${ip}:${livePort}` : null), page: '/_dsh-remote/pair' },
@@ -385,7 +397,6 @@ export async function start(config = {}) {
385
397
 
386
398
  const tunnel = await startTunnel({
387
399
  kind: tunnelKind,
388
- publicUrl,
389
400
  bridgePort: bridge.port,
390
401
  authority,
391
402
  domain,
@@ -405,14 +416,12 @@ export async function start(config = {}) {
405
416
  },
406
417
  })
407
418
 
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?`)
419
+ lastPublicUrl = relayUrl ?? tunnel.publicUrl
420
+ let relayReachable = null
421
+ if (relayUrl) {
422
+ relayReachable = await probeHealthUrl(`${relayUrl}/_dsh-remote/health`)
423
+ if (relayReachable === false) {
424
+ log(`dsh-remote: WARNING ${relayUrl} does not answer — is the relay's SSH tunnel up?`)
416
425
  }
417
426
  }
418
427
  const ip = ipForPairing
@@ -423,12 +432,11 @@ export async function start(config = {}) {
423
432
  authority,
424
433
  upstreamPort: discovery.port || null,
425
434
  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
- },
435
+ public: relayUrl
436
+ ? { url: relayUrl, kind: 'relay', ...(relayReachable === null ? {} : { reachable: relayReachable }) }
437
+ : { url: tunnel.publicUrl, kind: tunnel.kind, ...(tunnel.note ? { note: tunnel.note } : {}) },
438
+ relay: relayUrl ? { url: relayUrl, ...(relayReachable === null ? {} : { reachable: relayReachable }) } : null,
439
+ tunnel: tunnel.publicUrl ? { url: tunnel.publicUrl, kind: tunnel.kind, ...(tunnel.note ? { note: tunnel.note } : {}) } : null,
432
440
  cookie,
433
441
  cookieExpiresAt: expiresAt ?? cookieExpiry(cookie),
434
442
  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.1",
4
4
  "description": "DSH 插件:把电脑上的 DeepSeek Harness 变成手机可访问的服务——起桥接、自动开隧道(ngrok / cloudflared)、签发 30 天 Cookie,并输出地址与凭据,配合 DSH Remote Android App 使用。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",