dsh-remote-hub 0.4.1 → 0.4.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/README.md CHANGED
@@ -28,7 +28,7 @@ Kotlin/Compose 直连 DSH 自带的 HTTP API + WebSocket 网关。
28
28
  手机端不通过 npm 分发——npm 包 `dsh-remote-hub` 是电脑侧的 DSH 插件,两者配合使用:
29
29
 
30
30
  ```
31
- 电脑:dsh plugin --profile desktop add dsh-remote-hub → 起桥接 / 开隧道 / 签 Cookie
31
+ 电脑:dsh plugin --profile desktop add dsh-remote-hub@0.4.3 --registry=https://registry.npmjs.org → 起桥接 / 开隧道 / 签 Cookie
32
32
  手机:装上面这个 APK → 填电脑给出的地址与 Cookie → 连接
33
33
  ```
34
34
 
@@ -184,8 +184,8 @@ node mint.mjs "127.0.0.1:1815" 30 # 签发 30 天 Cookie
184
184
  ```bash
185
185
  # 从 NPM 安装(推荐,会出现在 DSH 内的「插件中心」)
186
186
  # https://www.npmjs.com/package/dsh-remote-hub
187
- dsh plugin --profile desktop add dsh-remote-hub
188
- dsh plugin --profile web add dsh-remote-hub # web profile 同样可用
187
+ dsh plugin --profile desktop add dsh-remote-hub@0.4.3 --registry=https://registry.npmjs.org
188
+ dsh plugin --profile web add dsh-remote-hub@0.4.3 --registry=https://registry.npmjs.org
189
189
 
190
190
  # 或直接从本仓库源码安装(不经过 NPM)
191
191
  dsh plugin --profile web add github:liaoguogang-hub/dsh-remote-public
@@ -222,6 +222,10 @@ dsh-remote start # 不开 DSH 也能单独前台跑(Ctrl+C 退出)
222
222
  > 公网隧道受网络影响:`api.trycloudflare.com` 不可达时插件会跳过公网、只给局域网地址,
223
223
  > 并在 `status.json` 里写明原因,不会假装成功。
224
224
 
225
+ > **安装时必须带版本号**(如 `@0.4.1`)。pnpm 11 默认开启 `minimumReleaseAge` 安全策略,
226
+ > 新发布的版本在一段时间内不会被 `@latest` 解析到,会**回退安装到很旧的版本**(0.1.0 没有扫码配对,
227
+ > 表现就是「扫码页打不开」)。显式写版本号可绕过该策略。
228
+
225
229
  ### 装完之后
226
230
 
227
231
  插件随 DSH 启动自动工作,**不需要手动跑 connect.cmd**;装的时候没有提示是正常的,日志里会出现:
@@ -238,7 +242,9 @@ dsh-remote: ready — 手机端扫码:dsh-remote qr(或浏览器打开 http:
238
242
  ### 升级插件(重要:必须完全重启 DSH)
239
243
 
240
244
  ```bash
241
- dsh plugin --profile desktop add dsh-remote-hub # 或 --profile web
245
+ dsh plugin --profile desktop add dsh-remote-hub@0.4.3 --registry=https://registry.npmjs.org
246
+ # web profile 把 desktop 换成 web
247
+
242
248
  ```
243
249
 
244
250
  **升级后请完全退出 DSH 再重新打开,不要只靠插件热重载。** 原因是旧版本的桥接监听可能不会被热重载回收,
@@ -313,3 +319,7 @@ dsh-remote qr
313
319
  | GET | `/_dsh-remote/pair` | 配对页(二维码 HTML) |
314
320
  | GET | `/_dsh-remote/pair.svg` | 只要二维码 SVG |
315
321
  | POST | `/_dsh-remote/pair/redeem` | `{code}` → `{url, cookie}`;用过即失效 |
322
+
323
+ ---
324
+
325
+ 发版流程(版本号约定、tag 命名、发布 checklist、踩过的坑):见 [RELEASING.md](RELEASING.md)
@@ -23,6 +23,7 @@ import { credentialsPath, mintCookie, readSigningSecret } from '../lib/cookie.js
23
23
  import { listeningPorts, probePort } from '../lib/discover.js'
24
24
  import { findCloudflared, findNgrok } from '../lib/tunnel.js'
25
25
  import { CONNECT_FILE, STATUS_FILE, readStatus, start } from '../lib/supervisor.js'
26
+ import { version as VERSION } from '../lib/index.js'
26
27
 
27
28
  const args = process.argv.slice(2)
28
29
  const command = args[0] || 'status'
@@ -42,6 +43,7 @@ function printStatus() {
42
43
  const remaining = status.cookieExpiresAt ? Math.round((status.cookieExpiresAt - Date.now()) / 86400000) : null
43
44
  console.log('')
44
45
  console.log(' DSH Remote 状态')
46
+ console.log(` 插件版本 v${VERSION}`)
45
47
  console.log(' ' + '-'.repeat(52))
46
48
  console.log(` 局域网地址 ${status.lan?.url ?? '(未检测到局域网 IP)'}`)
47
49
  console.log(` 公网地址 ${status.public?.url ?? `无(${status.public?.note ?? '未开启'})`}`)
@@ -58,6 +60,7 @@ function printStatus() {
58
60
  } else if (status.error) {
59
61
  console.log(` 错误 ${status.error}`)
60
62
  }
63
+ console.log(` ✔ 插件版本 v${VERSION}`)
61
64
  console.log(' ' + '-'.repeat(52))
62
65
  console.log('')
63
66
  }
@@ -270,6 +273,18 @@ async function main() {
270
273
  const live = await bridgeHealthy(Number(process.env.DSH_REMOTE_PORT || 6300))
271
274
  const mismatch = live && (live.relay || null) !== (publicUrl || null)
272
275
  if (mismatch && live.pid) {
276
+ // NEVER signal a bridge that lives inside the desktop app: health.pid is then the DSH
277
+ // host process, and killing it takes the whole app down (this actually happened).
278
+ if (live.runtime === 'electron') {
279
+ console.log('')
280
+ console.log(' 桥接由 DSH 桌面端内部托管(PID ' + live.pid + ' 就是 DSH 本体),不能在这里停止。')
281
+ console.log(' 它的公网配置来自 profile 的 cordis.patch.yml:')
282
+ console.log(` 当前 relay = ${live.relay ?? '(未配置)'},本次启动想要 ${publicUrl || '(不用中转)'}`)
283
+ console.log(' 要改配置:编辑 cordis.patch.yml 里的 publicUrl 后重启 DSH;')
284
+ console.log(' 要临时用本启动器的配置:先完全退出 DSH,再双击 connect.cmd。')
285
+ console.log('')
286
+ return
287
+ }
273
288
  console.log(`已有桥接配置不同(relay=${live.relay ?? '无'}),先停止它(PID ${live.pid})再按当前配置启动`)
274
289
  try {
275
290
  process.kill(Number(live.pid))
package/lib/bridge.js CHANGED
@@ -156,6 +156,9 @@ export function startBridge({ listenPort, authority, upstream, pairing = null, r
156
156
  ok: true,
157
157
  service: 'dsh-remote-bridge',
158
158
  pid: process.pid,
159
+ // 'electron' when the bridge lives inside the DSH desktop host (plugin mode). Callers must
160
+ // NEVER signal that pid: it is the app itself, not a disposable node helper.
161
+ runtime: process.versions.electron ? 'electron' : 'node',
159
162
  // Whether this bridge advertises a self-hosted relay; a launcher that expects none must not
160
163
  // adopt a bridge that has one, or the public app would show the self-use relay.
161
164
  relay: relayUrl || null,
package/lib/discover.js CHANGED
@@ -108,6 +108,57 @@ export function probePort(port) {
108
108
  })
109
109
  }
110
110
 
111
+ /** Map local listening port -> owning PID (Windows only). */
112
+ async function portOwners() {
113
+ if (process.platform !== 'win32') return new Map()
114
+ const out = await run('netstat', ['-ano', '-p', 'tcp'])
115
+ const map = new Map()
116
+ for (const line of splitLines(out)) {
117
+ const m = /^\s*TCP\s+\S+:(\d+)\s+\S+\s+LISTENING\s+(\d+)/i.exec(line)
118
+ if (m) map.set(Number(m[1]), Number(m[2]))
119
+ }
120
+ return map
121
+ }
122
+
123
+ /** Process name for a PID, or '' when unknown (uses tasklist; cheap enough at discovery time). */
124
+ const NAME_CACHE = new Map()
125
+ async function processName(pid) {
126
+ if (process.platform !== 'win32') return ''
127
+ if (NAME_CACHE.has(pid)) return NAME_CACHE.get(pid)
128
+ const out = await run('tasklist', ['/FI', `PID eq ${pid}`, '/FO', 'CSV', '/NH'])
129
+ const line = splitLines(out).find((l) => l.trim().length > 0)
130
+ if (!line) return ''
131
+ const m = /^"([^"]+)"/.exec(line.trim())
132
+ const name = m ? m[1] : ''
133
+ NAME_CACHE.set(pid, name)
134
+ return name
135
+ }
136
+
137
+ /**
138
+ * Pick the best DSH host when several profile hosts are running.
139
+ *
140
+ * A machine can run the desktop app and a CLI host (e.g. a Lark profile) at the same time; both
141
+ * answer the fingerprint. The phone wants the DESKTOP one, so a host whose image is not node.exe
142
+ * (the Electron app) wins over a plain `node` host.
143
+ */
144
+ async function rankHosts(ports) {
145
+ if (process.platform !== 'win32' || ports.length <= 1) return ports
146
+ const owners = await portOwners()
147
+ const ranked = []
148
+ for (const port of ports) {
149
+ const pid = owners.get(port)
150
+ let name = ''
151
+ if (pid) {
152
+ // eslint-disable-next-line no-await-in-loop
153
+ name = await processName(pid)
154
+ }
155
+ const desktop = name && !/^node(\.exe)?$/i.test(name)
156
+ ranked.push({ port, desktop })
157
+ }
158
+ const preferred = ranked.filter((r) => r.desktop)
159
+ return (preferred.length ? preferred : ranked).map((r) => r.port)
160
+ }
161
+
111
162
  /**
112
163
  * A resolver that remembers the last known-good port.
113
164
  *
@@ -128,16 +179,24 @@ export function createDiscovery({ pinned = 0, ttlMs = 30_000, exclude = [] } = {
128
179
  inFlight = (async () => {
129
180
  const skip = new Set(exclude.filter((p) => Number.isInteger(p) && p > 0))
130
181
  const ports = (await listeningPorts()).filter((p) => !skip.has(p))
131
- // Probe the remembered port first: it is usually still right and saves a full sweep.
132
- const ordered = port && ports.includes(port) ? [port, ...ports.filter((p) => p !== port)] : ports
133
- for (const candidate of ordered) {
134
- // eslint-disable-next-line no-await-in-loop
135
- if (await probePort(candidate)) {
136
- port = candidate
137
- resolvedAt = Date.now()
138
- return port
182
+ // Probe in parallel: a full sweep touches dozens of ports, and a serial walk made the first
183
+ // request after startup take 10+ seconds.
184
+ const CONCURRENCY = 12
185
+ let cursor = 0
186
+ const hits = []
187
+ async function worker() {
188
+ while (cursor < ports.length) {
189
+ const candidate = ports[cursor++]
190
+ // eslint-disable-next-line no-await-in-loop
191
+ if (await probePort(candidate)) hits.push(candidate)
139
192
  }
140
193
  }
194
+ await Promise.all(Array.from({ length: Math.min(CONCURRENCY, ports.length) }, worker))
195
+ if (hits.length === 0) return port
196
+ // Keep the remembered port when it is still a hit; otherwise prefer the desktop host.
197
+ const ranked = hits.includes(port) ? [port, ...hits.filter((p) => p !== port)] : await rankHosts(hits)
198
+ port = ranked[0]
199
+ resolvedAt = Date.now()
141
200
  return port
142
201
  })()
143
202
  try {
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.4.1'
19
+ export const version = '0.4.3'
20
20
 
21
21
  /** No host services are required: the plugin only needs Node builtins. */
22
22
  export const inject = []
@@ -56,7 +56,7 @@ export function apply(ctx, config = {}) {
56
56
  // Make the next step discoverable from the log: after installing the plugin there is no
57
57
  // other place telling the user how to get the phone connected.
58
58
  log(
59
- `dsh-remote: ready — 手机端扫码:dsh-remote qr(或浏览器打开 http://127.0.0.1:${started.status.bridgePort}/_dsh-remote/pair);` +
59
+ `dsh-remote: ready (v${version}) — 手机端扫码:dsh-remote qr(或浏览器打开 http://127.0.0.1:${started.status.bridgePort}/_dsh-remote/pair);` +
60
60
  '不想装插件时也可以直接双击仓库里的 connect.cmd',
61
61
  )
62
62
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-remote-hub",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "description": "DSH 插件:把电脑上的 DeepSeek Harness 变成手机可访问的服务——起桥接、自动开隧道(ngrok / cloudflared)、签发 30 天 Cookie,并输出地址与凭据,配合 DSH Remote Android App 使用。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",