dsh-mobile-plugin 0.1.66 → 0.1.67

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/relay/core.js CHANGED
@@ -1,3 +1,8 @@
1
+ /**
2
+ * relay.json 的状态形状与读写,外加实例 ID / 配对码 / 隧道地址这几个纯函数。
3
+ *
4
+ * 内存里的 `state` 是本进程唯一真值,这里只负责落盘与还原(见 A3-20 的坑)。
5
+ */
1
6
  'use strict';
2
7
 
3
8
  const fs = require('node:fs');
@@ -29,6 +34,9 @@ function defaultState() {
29
34
  // 中转侧 TUNNEL_REGISTER_SECRET 的对偶值。为空时隧道注册会被中转以
30
35
  // close(1008, 'bad registration secret') 拒掉(除非中转根本没开校验)。
31
36
  registerSecret: '',
37
+ // 手机优先用哪条地址(空 = 不指定)。它是 `RemoteControlSettingsSchema` 里的一项,
38
+ // 写它不碰 sidecar(那份配置是 `impl.js` 的 `buildConfig` 逐字段拼的,不带它)。
39
+ preferredLanAddress: '',
32
40
  // 首次接入引导走完没有。**刻意不进 RemoteControlSettingsSchema**:它不是一项
33
41
  // 「远程控制设置」,改它不该触发 sidecar 重配(那条链上每加一个字段都要同步改
34
42
  // schema、worker 的 allowed 数组与 buildConfig,漏一处就抛 unknown field)。
@@ -1,3 +1,8 @@
1
+ /**
2
+ * 网卡枚举与候选地址:私网 / 公网两份清单,外加「用户选中的那条排最前」。
3
+ *
4
+ * 收口在这一个文件里,是因为 App 只认「按顺序挨个试」这一种语义 —— 顺序就是优先级。
5
+ */
1
6
  'use strict'
2
7
 
3
8
  const os = require('node:os')
@@ -158,6 +163,33 @@ function advertisedCandidates(port, interfaces = os.networkInterfaces()) {
158
163
  }
159
164
  }
160
165
 
166
+ /**
167
+ * 把用户选中的那条排到最前面(面板上的「手机优先用这条」,存在设置项 `preferredLanAddress`)。
168
+ *
169
+ * 为什么只能是「排前面」:App 拿到候选之后是**按顺序挨个试**的,顺序就是唯一的表达方式
170
+ * (见 `impl.js` 的 `pairingPayload` 与 `routes/mobile-api.js` 的 `/connection-info`)。
171
+ * 所以这里刻意只挪位置:
172
+ *
173
+ * - **不删任何东西**:其余的照旧跟在后面当兜底。用户选错了(比如选了一块手机根本到不了的
174
+ * 网卡)、或者那条以后不通了,手机还能顺着试下去,不会因为一次选择就把整条局域网路走死;
175
+ * - **匹配不上就原样返回**:换了网段、拔了网卡之后,用户当初选的那个 IP 就不在清单里了,
176
+ * 这时必须退回宿主自己的网卡优先级排序。绝不能因为「选过一条现在没有的地址」把候选清空;
177
+ * - 比对的是**地址**而不是整条 URL —— URL 里带着端口,用户改端口不该让这次选择失效。
178
+ */
179
+ function orderCandidates(list, preferred) {
180
+ if (!Array.isArray(list)) return []
181
+ if (list.length < 2) return list
182
+ const want = String(preferred || '').trim()
183
+ if (!want) return list
184
+ const index = list.findIndex(item => item && item.address === want)
185
+ // 已经在首位(宿主的排序正好就是它),或者压根没有这条:原样返回。
186
+ if (index <= 0) return list
187
+ const ordered = list.slice()
188
+ const [picked] = ordered.splice(index, 1)
189
+ ordered.unshift(picked)
190
+ return ordered
191
+ }
192
+
161
193
  module.exports = {
162
194
  ipv4Number,
163
195
  isPrivateLanAddress,
@@ -172,4 +204,5 @@ module.exports = {
172
204
  publicUrls,
173
205
  publicCandidates,
174
206
  advertisedCandidates,
207
+ orderCandidates,
175
208
  }
@@ -219,11 +219,15 @@ function registerControlPlaneRoutes(deps) {
219
219
  // 面板的「手机该填哪个地址」:把检测到的网卡地址按**局域网 / 公网**分成两份报出来,
220
220
  // 用户不用自己去 `ipconfig`,界面也能一眼看出该填哪个(引导第三、四页用它)。
221
221
  //
222
- // 只给面板看:移动端那份候选契约(二维码里带的 `lanUrls`)**一个字都没动**,那边的
222
+ // 只给面板看:移动端那份候选契约(二维码里带的 `lanUrls`)的**取舍**一个字都没动,那边的
223
223
  // 私网/公网取舍见 `../relay/lan-network.cjs` 的 `advertisedCandidates`。
224
+ //
225
+ // `preferred` 是用户在这两份地址里选中的那条(设置项 `preferredLanAddress`):界面靠它回显
226
+ // 「现在选的是这条」。选中的地址要真生效在二维码与 `/connection-info` 里,不在这一条上。
224
227
  registerRoute({ kind: 'exact', path: '/api/relay/addresses', handler: localOnly('GET', async (_req, res) => {
225
228
  const port = lanPortInUse()
226
- sendJson(res, 200, { ok: true, port, lan: lanCandidates(port), public: publicCandidates(port) })
229
+ const preferred = typeof state.preferredLanAddress === 'string' ? state.preferredLanAddress : ''
230
+ sendJson(res, 200, { ok: true, port, preferred, lan: lanCandidates(port), public: publicCandidates(port) })
227
231
  }) })
228
232
  registerRoute({ kind: 'exact', path: '/api/relay/status', handler: localOnly('GET', async (req, res) => sendJson(res, 200, { ...status(), panelUrl: panelUrlOf(req), ...mobileActivity() })) })
229
233
  // 面板页本体:设置界面从客户端半边搬到这里,浏览器(或桌面端里的「打开配置页面」)
@@ -394,6 +398,11 @@ function registerControlPlaneRoutes(deps) {
394
398
  if (body?.pairingPolicy === 'code' || body?.pairingPolicy === 'deny') patch.pairingPolicy = body.pairingPolicy
395
399
  // 注册密钥:只接受字符串,过长拒绝;写空串等于关掉(回到"中转开了校验就连不上")。
396
400
  if (typeof body?.registerSecret === 'string' && body.registerSecret.length <= 512) patch.registerSecret = body.registerSecret
401
+ // 手机优先用哪条地址。**空串是合法值**(= 不指定,回到宿主的网卡优先级),所以这里
402
+ // 只挡类型和长度,不像 name 那样要求非空——那是「清掉这次选择」的唯一方式。
403
+ if (typeof body?.preferredLanAddress === 'string' && body.preferredLanAddress.length <= 64) {
404
+ patch.preferredLanAddress = body.preferredLanAddress.trim()
405
+ }
397
406
  await settings.update(patch)
398
407
  await syncSidecar().catch(() => undefined)
399
408
  sendJson(res, 200, status())
@@ -20,6 +20,7 @@
20
20
  */
21
21
 
22
22
  const { sendJson } = require('../http/routing.js');
23
+ const { orderCandidates } = require('../relay/lan-network.cjs');
23
24
 
24
25
  const sessions = require('../mobile/sessions.js');
25
26
  const agentPresets = require('../mobile/agent-presets.js');
@@ -102,7 +103,11 @@ function registerMobileRoutes(deps) {
102
103
  // 端口取「手机该拨的那个」(sidecar 真 bind 成的),不是配置里的首选端口:
103
104
  // 两者不一致时这里报出去的候选是死地址,手机只能退回公网中转。
104
105
  const port = typeof lanPortInUse === 'function' ? lanPortInUse() : state.lanPort
105
- const candidates = state.lanEnabled ? safeLanCandidates(port) : []
106
+ // 用户在面板上选过的「手机优先用这条」(设置项 `preferredLanAddress`)在这里同样兑现:
107
+ // 手机是按顺序挨个试的,所以只挪顺序、不删候选。选的那条不在了就退回宿主排序。
108
+ const candidates = state.lanEnabled
109
+ ? orderCandidates(safeLanCandidates(port), state.preferredLanAddress)
110
+ : []
106
111
  const lanUpdatedAt = getSidecarSnapshot()?.lan?.updatedAt || Date.now()
107
112
  sendJson(res, 200, {
108
113
  ok: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-mobile-plugin",
3
- "version": "0.1.66",
3
+ "version": "0.1.67",
4
4
  "description": "Mobile API (/api/mobile/v1) and relay client for the DSH mobile app — desktop profile build (0.2.0-rc.2 compatible, peers resolved by the host)",
5
5
  "license": "MIT",
6
6
  "type": "module",