dsh-mobile 0.1.0-alpha.14 → 0.1.0-alpha.16

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.en.md CHANGED
@@ -9,6 +9,7 @@
9
9
  <p align="center">
10
10
  <a href="https://www.npmjs.com/package/dsh-mobile"><img src="https://img.shields.io/npm/v/dsh-mobile?label=npm&amp;color=CB3837" alt="npm version"></a>
11
11
  <a href="https://github.com/saya-ch/dsh-mobile/releases"><img src="https://img.shields.io/badge/Download-Android_App-3DDC84?logo=android&amp;logoColor=white" alt="Download Android app"></a>
12
+ <img src="https://img.shields.io/badge/DeepSeek_Harness-Mobile-2563EB" alt="DeepSeek Harness Mobile">
12
13
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-Apache--2.0-0F172A" alt="Apache-2.0"></a>
13
14
  </p>
14
15
 
@@ -39,6 +40,8 @@ pnpm dsh plugin --profile web exec dsh-mobile setup
39
40
  pnpm dsh --profile web
40
41
  ```
41
42
 
43
+ `setup` follows the operating system's preferred physical Wi-Fi or Ethernet route, ignores common VPN, WSL, Docker, and proxy adapters, and remembers the selected interface. Later Wi-Fi, hotspot, or DHCP address changes automatically rebind the gateway without replacing the paired-device trust. Add `--address 192.168.x.x` only when the computer truly has two indistinguishable physical LAN routes.
44
+
42
45
  Open **Mobile** in the lower-left DeepSeek Harness sidebar, enable access, and create a pairing key. In the Android app, tap **Scan**, select the computer, and enter the key. The paired device remains trusted until it is revoked, expires, or its app data is cleared.
43
46
 
44
47
  ## Connection options
package/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
  <p align="center">
10
10
  <a href="https://www.npmjs.com/package/dsh-mobile"><img src="https://img.shields.io/npm/v/dsh-mobile?label=npm&amp;color=CB3837" alt="npm 版本"></a>
11
11
  <a href="https://github.com/saya-ch/dsh-mobile/releases"><img src="https://img.shields.io/badge/下载-Android_App-3DDC84?logo=android&amp;logoColor=white" alt="下载 Android App"></a>
12
- <img src="https://img.shields.io/badge/DSH-插件-2563EB" alt="DSH 插件">
12
+ <img src="https://img.shields.io/badge/DeepSeek_Harness-Mobile-2563EB" alt="DeepSeek Harness Mobile">
13
13
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-Apache--2.0-0F172A" alt="Apache-2.0"></a>
14
14
  </p>
15
15
 
@@ -54,6 +54,8 @@ pnpm dsh plugin --profile web exec dsh-mobile setup
54
54
  pnpm dsh --profile web
55
55
  ```
56
56
 
57
+ `setup` 会根据系统默认路由自动选择真实的 Wi-Fi 或以太网,忽略常见 VPN、WSL、Docker 和代理虚拟网卡,并记住所选网卡。以后切换 Wi-Fi、手机热点或 DHCP 地址变化时,网关会自动重绑并沿用原有设备信任。只有系统确实无法区分两条真实局域网时,才需要追加 `--address 192.168.x.x`。
58
+
57
59
  启动后:
58
60
 
59
61
  1. 在 DeepSeek Harness 左下角打开“移动端”,确认移动访问已开启。
package/lib/cli.js CHANGED
@@ -7,6 +7,26 @@ import { promisify } from "node:util";
7
7
  import { X509Certificate, createPrivateKey, createPublicKey } from "node:crypto";
8
8
  import { generate } from "selfsigned";
9
9
  //#region src/managed-setup.ts
10
+ const execFile$2 = promisify(execFile);
11
+ const VIRTUAL_INTERFACE_MARKERS = [
12
+ "bridge",
13
+ "docker",
14
+ "hyper-v",
15
+ "mihomo",
16
+ "radmin",
17
+ "tailscale",
18
+ "tap",
19
+ "tun",
20
+ "utun",
21
+ "vbox",
22
+ "veth",
23
+ "virtual",
24
+ "vmware",
25
+ "vpn",
26
+ "vethernet",
27
+ "wsl",
28
+ "zerotier"
29
+ ];
10
30
  function privateIpv4(value) {
11
31
  const parts = value.split(".").map(Number);
12
32
  return parts.length === 4 && parts.every((part) => Number.isInteger(part) && part >= 0 && part <= 255) && (parts[0] === 10 || parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31 || parts[0] === 192 && parts[1] === 168);
@@ -30,8 +50,65 @@ function availableLanNetworks(table = networkInterfaces()) {
30
50
  })));
31
51
  return [...new Map(candidates.map((entry) => [`${entry.name}\0${entry.address}`, entry])).values()];
32
52
  }
53
+ function likelyVirtualInterface(name) {
54
+ const normalized = name.toLowerCase().replaceAll(/[^a-z0-9]+/gu, " ");
55
+ return VIRTUAL_INTERFACE_MARKERS.some((marker) => normalized.includes(marker.replaceAll("-", " "))) || /^(?:br|wg)\d*\b/u.test(normalized);
56
+ }
57
+ async function runRouteCommand(file, args) {
58
+ return (await execFile$2(file, [...args], {
59
+ encoding: "utf8",
60
+ windowsHide: true
61
+ })).stdout;
62
+ }
63
+ function uniqueLines(output) {
64
+ return [...new Set(output.split(/\r?\n/gu).map((line) => line.trim()).filter(Boolean))];
65
+ }
66
+ /** Return operating-system default-route interfaces in routing preference order. */
67
+ async function preferredLanInterfaceNames(platform = process.platform, run = runRouteCommand) {
68
+ try {
69
+ if (platform === "win32") return uniqueLines(await run("powershell.exe", [
70
+ "-NoProfile",
71
+ "-NonInteractive",
72
+ "-Command",
73
+ [
74
+ "$routes = Get-NetRoute -AddressFamily IPv4 -DestinationPrefix '0.0.0.0/0' -ErrorAction Stop",
75
+ "$ranked = $routes | Where-Object { $_.State -eq 'Alive' -and $_.NextHop -ne '0.0.0.0' } | ForEach-Object {",
76
+ " $route = $_",
77
+ " $adapter = Get-NetAdapter -InterfaceIndex $route.InterfaceIndex -ErrorAction SilentlyContinue",
78
+ " $ip = Get-NetIPInterface -AddressFamily IPv4 -InterfaceIndex $route.InterfaceIndex -ErrorAction SilentlyContinue",
79
+ " if ($adapter -and $ip -and $adapter.Status -eq 'Up' -and $adapter.HardwareInterface -eq $true -and $adapter.Virtual -ne $true) {",
80
+ " [pscustomobject]@{ Name = $route.InterfaceAlias; Metric = [int]$route.RouteMetric + [int]$ip.InterfaceMetric }",
81
+ " }",
82
+ "}",
83
+ "$ranked | Sort-Object Metric | Select-Object -ExpandProperty Name -Unique"
84
+ ].join("; ")
85
+ ]));
86
+ if (platform === "linux") {
87
+ const routes = uniqueLines(await run("ip", [
88
+ "-o",
89
+ "-4",
90
+ "route",
91
+ "show",
92
+ "default"
93
+ ])).map((line) => ({
94
+ name: /(?:^|\s)dev\s+(\S+)/u.exec(line)?.[1],
95
+ metric: Number(/(?:^|\s)metric\s+(\d+)/u.exec(line)?.[1] ?? 0)
96
+ })).filter((route) => route.name !== void 0 && !likelyVirtualInterface(route.name)).sort((left, right) => left.metric - right.metric);
97
+ return [...new Set(routes.map((route) => route.name))];
98
+ }
99
+ if (platform === "darwin") {
100
+ const name = /^\s*interface:\s*(\S+)\s*$/mu.exec(await run("route", [
101
+ "-n",
102
+ "get",
103
+ "default"
104
+ ]))?.[1];
105
+ return name === void 0 || likelyVirtualInterface(name) ? [] : [name];
106
+ }
107
+ } catch {}
108
+ return [];
109
+ }
33
110
  /** Select an active LAN, optionally by address or by a previously saved interface name. */
34
- function selectLanNetwork(requestedAddress, requestedInterface, table) {
111
+ function selectLanNetwork(requestedAddress, requestedInterface, table, preferredInterfaces = []) {
35
112
  const candidates = availableLanNetworks(table);
36
113
  if (requestedAddress !== void 0) {
37
114
  const match = candidates.find((candidate) => candidate.address === requestedAddress);
@@ -46,7 +123,13 @@ function selectLanNetwork(requestedAddress, requestedInterface, table) {
46
123
  }
47
124
  if (candidates.length === 1) return candidates[0];
48
125
  if (candidates.length === 0) throw new Error("no active private LAN address was found; connect to Wi-Fi or Ethernet");
49
- throw new Error(`more than one LAN address is active; rerun with --address and one of: ${candidates.map((entry) => entry.address).join(", ")}`);
126
+ for (const name of preferredInterfaces) {
127
+ const matches = candidates.filter((candidate) => candidate.name === name);
128
+ if (matches.length === 1) return matches[0];
129
+ }
130
+ const physicalCandidates = candidates.filter((candidate) => !likelyVirtualInterface(candidate.name));
131
+ if (physicalCandidates.length === 1) return physicalCandidates[0];
132
+ throw new Error(`more than one LAN address is active; rerun with --address and one of: ${candidates.map((entry) => `${entry.name}=${entry.address}`).join(", ")}`);
50
133
  }
51
134
  function assertMatchingCa(certPem, keyPem) {
52
135
  const certificate = new X509Certificate(certPem);
@@ -251,7 +334,8 @@ async function removeWindowsFirewall() {
251
334
  }
252
335
  async function setup(args) {
253
336
  const options = parseOptions(args);
254
- const network = selectLanNetwork(options.address);
337
+ const preferredInterfaces = options.address === void 0 ? await preferredLanInterfaceNames() : [];
338
+ const network = selectLanNetwork(options.address, void 0, void 0, preferredInterfaces);
255
339
  const home = dshHome();
256
340
  const directory = join(home, "mobile-access");
257
341
  const tls = join(directory, "tls");
package/lib/index.mjs CHANGED
@@ -10,6 +10,8 @@ import { createServer as createServer$1 } from "node:https";
10
10
  import { Transform } from "node:stream";
11
11
  import { pipeline } from "node:stream/promises";
12
12
  import Bonjour from "bonjour-service";
13
+ import { execFile } from "node:child_process";
14
+ import { promisify } from "node:util";
13
15
  import { generate } from "selfsigned";
14
16
  //#region src/access.ts
15
17
  /** Stable error categories converted to deliberately terse HTTP responses. */
@@ -2347,8 +2349,26 @@ var MemoryDeviceStore = class {
2347
2349
  return structuredClone(this.snapshot);
2348
2350
  }
2349
2351
  };
2350
- //#endregion
2351
- //#region src/managed-setup.ts
2352
+ promisify(execFile);
2353
+ const VIRTUAL_INTERFACE_MARKERS = [
2354
+ "bridge",
2355
+ "docker",
2356
+ "hyper-v",
2357
+ "mihomo",
2358
+ "radmin",
2359
+ "tailscale",
2360
+ "tap",
2361
+ "tun",
2362
+ "utun",
2363
+ "vbox",
2364
+ "veth",
2365
+ "virtual",
2366
+ "vmware",
2367
+ "vpn",
2368
+ "vethernet",
2369
+ "wsl",
2370
+ "zerotier"
2371
+ ];
2352
2372
  function requiredString(value, name) {
2353
2373
  if (typeof value !== "string" || value.length === 0) throw new Error(`${name} must be a non-empty string`);
2354
2374
  return value;
@@ -2411,8 +2431,12 @@ function availableLanNetworks(table = networkInterfaces()) {
2411
2431
  })));
2412
2432
  return [...new Map(candidates.map((entry) => [`${entry.name}\0${entry.address}`, entry])).values()];
2413
2433
  }
2434
+ function likelyVirtualInterface(name) {
2435
+ const normalized = name.toLowerCase().replaceAll(/[^a-z0-9]+/gu, " ");
2436
+ return VIRTUAL_INTERFACE_MARKERS.some((marker) => normalized.includes(marker.replaceAll("-", " "))) || /^(?:br|wg)\d*\b/u.test(normalized);
2437
+ }
2414
2438
  /** Select an active LAN, optionally by address or by a previously saved interface name. */
2415
- function selectLanNetwork(requestedAddress, requestedInterface, table) {
2439
+ function selectLanNetwork(requestedAddress, requestedInterface, table, preferredInterfaces = []) {
2416
2440
  const candidates = availableLanNetworks(table);
2417
2441
  if (requestedAddress !== void 0) {
2418
2442
  const match = candidates.find((candidate) => candidate.address === requestedAddress);
@@ -2427,7 +2451,13 @@ function selectLanNetwork(requestedAddress, requestedInterface, table) {
2427
2451
  }
2428
2452
  if (candidates.length === 1) return candidates[0];
2429
2453
  if (candidates.length === 0) throw new Error("no active private LAN address was found; connect to Wi-Fi or Ethernet");
2430
- throw new Error(`more than one LAN address is active; rerun with --address and one of: ${candidates.map((entry) => entry.address).join(", ")}`);
2454
+ for (const name of preferredInterfaces) {
2455
+ const matches = candidates.filter((candidate) => candidate.name === name);
2456
+ if (matches.length === 1) return matches[0];
2457
+ }
2458
+ const physicalCandidates = candidates.filter((candidate) => !likelyVirtualInterface(candidate.name));
2459
+ if (physicalCandidates.length === 1) return physicalCandidates[0];
2460
+ throw new Error(`more than one LAN address is active; rerun with --address and one of: ${candidates.map((entry) => `${entry.name}=${entry.address}`).join(", ")}`);
2431
2461
  }
2432
2462
  function assertMatchingCa(certPem, keyPem) {
2433
2463
  const certificate = new X509Certificate(certPem);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-mobile",
3
- "version": "0.1.0-alpha.14",
3
+ "version": "0.1.0-alpha.16",
4
4
  "private": false,
5
5
  "description": "DeepSeek Harness 移动端适配与安全局域网访问插件,支持 Android App 和手机浏览器。",
6
6
  "type": "module",