@chenacc/agentbridge 0.1.0 → 0.1.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 +23 -12
- package/package.json +1 -1
- package/public/styles.css +1073 -1073
- package/src/config.js +74 -6
- package/src/server.js +7 -0
package/src/config.js
CHANGED
|
@@ -2,13 +2,81 @@ import os from "node:os";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Adapter names that never reach the phone, or only reach it over an overlay.
|
|
7
|
+
*
|
|
8
|
+
* A Windows laptop normally carries several phantom adapters: Hyper-V switches,
|
|
9
|
+
* WSL, "本地连接*" (Wi-Fi Direct), Bluetooth PAN and — with Tailscale installed
|
|
10
|
+
* but logged out — a 169.254.x.x link-local fallback. `os.networkInterfaces()`
|
|
11
|
+
* returns them in no meaningful order, so taking the first one can hand the
|
|
12
|
+
* phone an address with no route to anything. That is exactly what shipped in
|
|
13
|
+
* 0.1.0: the QR encoded the logged-out Tailscale adapter.
|
|
14
|
+
*/
|
|
15
|
+
const OVERLAY_ADAPTER_HINTS = ["tailscale", "zerotier", "wireguard", "hamachi", "radmin", "vpn"];
|
|
16
|
+
const VIRTUAL_ADAPTER_HINTS = [
|
|
17
|
+
"vethernet", "hyper-v", "wsl", "vmware", "virtualbox", "docker", "npcap",
|
|
18
|
+
"loopback", "bluetooth", "蓝牙", "wi-fi direct", "wifi direct", "tap-", "utun",
|
|
19
|
+
];
|
|
20
|
+
const PHYSICAL_ADAPTER_HINTS = ["wlan", "wi-fi", "wifi", "无线", "ethernet", "以太网", "en0", "eth", "enp", "ens", "本地连接"];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* How usable an address is from another device, 0 meaning "not at all".
|
|
24
|
+
*
|
|
25
|
+
* Loopback and 169.254 link-local are hard zeros: a link-local address means the
|
|
26
|
+
* adapter never got a lease, so nothing outside the cable can reach it. Private
|
|
27
|
+
* RFC 1918 ranges are the intended path, 100.64/10 (Tailscale's CGNAT range) is
|
|
28
|
+
* reachable only from a phone that is also on the tailnet, and a plain public
|
|
29
|
+
* address on a NIC still beats nothing.
|
|
30
|
+
*/
|
|
31
|
+
function addressScore(address) {
|
|
32
|
+
const [a, b] = address.split(".").map(Number);
|
|
33
|
+
if (!Number.isInteger(a) || !Number.isInteger(b)) return 0;
|
|
34
|
+
if (a === 0 || a === 127 || a >= 224) return 0;
|
|
35
|
+
if (a === 169 && b === 254) return 0;
|
|
36
|
+
if (a === 192 && b === 168) return 100;
|
|
37
|
+
if (a === 10) return 95;
|
|
38
|
+
if (a === 172 && b >= 16 && b <= 31) return 90;
|
|
39
|
+
if (a === 100 && b >= 64 && b <= 127) return 60;
|
|
40
|
+
return 50;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function adapterScore(name = "") {
|
|
44
|
+
const lower = String(name).toLowerCase();
|
|
45
|
+
// Hosted-network names such as "本地连接* 10" carry an asterisk; the star is what
|
|
46
|
+
// distinguishes them from the plain "本地连接" of a real Ethernet card.
|
|
47
|
+
if (lower.includes("*")) return -120;
|
|
48
|
+
if (OVERLAY_ADAPTER_HINTS.some((hint) => lower.includes(hint))) return -40;
|
|
49
|
+
if (VIRTUAL_ADAPTER_HINTS.some((hint) => lower.includes(hint))) return -120;
|
|
50
|
+
if (PHYSICAL_ADAPTER_HINTS.some((hint) => lower.includes(hint))) return 25;
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Every IPv4 address a phone could plausibly open, best first.
|
|
56
|
+
*
|
|
57
|
+
* Exported for tests: the failure this guards against depends on the machine's
|
|
58
|
+
* adapter list, which no CI runner reproduces.
|
|
59
|
+
*/
|
|
60
|
+
export function rankLanAddresses(interfaces = os.networkInterfaces()) {
|
|
61
|
+
const ranked = [];
|
|
62
|
+
for (const [name, entries] of Object.entries(interfaces)) {
|
|
7
63
|
for (const entry of entries ?? []) {
|
|
8
|
-
|
|
64
|
+
// `family` is "IPv4" on modern Node and 4 on older builds.
|
|
65
|
+
if (entry.family !== "IPv4" && entry.family !== 4) continue;
|
|
66
|
+
if (entry.internal) continue;
|
|
67
|
+
// A zero address score is a hard no: a friendly adapter name cannot make a
|
|
68
|
+
// link-local or loopback address routable.
|
|
69
|
+
const score = addressScore(entry.address) + adapterScore(name);
|
|
70
|
+
if (addressScore(entry.address) === 0 || score <= 0) continue;
|
|
71
|
+
ranked.push({ name, address: entry.address, score });
|
|
9
72
|
}
|
|
10
73
|
}
|
|
11
|
-
|
|
74
|
+
// A stable sort keeps the OS order for equally scored adapters.
|
|
75
|
+
return ranked.sort((left, right) => right.score - left.score);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function firstLanAddress(interfaces) {
|
|
79
|
+
return rankLanAddresses(interfaces)[0]?.address ?? "127.0.0.1";
|
|
12
80
|
}
|
|
13
81
|
|
|
14
82
|
function parseProjects(raw, workspace) {
|
|
@@ -105,8 +173,8 @@ export function loadConfig(env = process.env, workspace = process.cwd()) {
|
|
|
105
173
|
// PushPlus-style services need the token inside the request body.
|
|
106
174
|
notifyToken: pick(env, "AGENTBRIDGE_NOTIFY_TOKEN", notifyFile.token),
|
|
107
175
|
// Some providers (PushPlus) also take a delivery channel, e.g. `clawbot`.
|
|
108
|
-
notifyChannel: pick(env, "AGENTBRIDGE_NOTIFY_CHANNEL", notifyFile.channel),
|
|
109
|
-
// Per-bridge mute, toggled from the phone; persisted so it survives a restart.
|
|
176
|
+
notifyChannel: pick(env, "AGENTBRIDGE_NOTIFY_CHANNEL", notifyFile.channel),
|
|
177
|
+
// Per-bridge mute, toggled from the phone; persisted so it survives a restart.
|
|
110
178
|
notifyMuted: notifyFile.muted === true,
|
|
111
179
|
// Opt-in: putting the access key into a third-party chat is a real disclosure,
|
|
112
180
|
// so the notification link stays keyless unless the operator asks for it.
|
package/src/server.js
CHANGED
|
@@ -946,6 +946,13 @@ export async function startBridge() {
|
|
|
946
946
|
});
|
|
947
947
|
console.log(`AgentBridge running at http://127.0.0.1:${bridge.config.port}`);
|
|
948
948
|
console.log(`WeChat scan target: ${bridge.getCurrentPairing().url}`);
|
|
949
|
+
// Silence here used to mean a QR encoding an address the phone could never reach.
|
|
950
|
+
if (new URL(bridge.config.advertisedOrigin).hostname === "127.0.0.1") {
|
|
951
|
+
console.warn(
|
|
952
|
+
"No routable LAN address found, so the QR code points at loopback and a phone cannot open it. "
|
|
953
|
+
+ "Connect the machine to Wi-Fi/Ethernet, or set AGENTBRIDGE_PUBLIC_ORIGIN to the address the phone should use.",
|
|
954
|
+
);
|
|
955
|
+
}
|
|
949
956
|
return bridge;
|
|
950
957
|
}
|
|
951
958
|
|