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 +3 -0
- package/README.md +3 -1
- package/lib/cli.js +87 -3
- package/lib/index.mjs +34 -4
- package/package.json +1 -1
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&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&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&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&logoColor=white" alt="下载 Android App"></a>
|
|
12
|
-
<img src="https://img.shields.io/badge/
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
2351
|
-
|
|
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
|
-
|
|
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);
|