@decentnetwork/lan 0.1.232 → 0.1.234
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/dist/cli/commands.js +31 -12
- package/dist/ui/desktop/app.js +127 -9
- package/dist/ui/server.js +93 -0
- package/package.json +1 -1
package/dist/cli/commands.js
CHANGED
|
@@ -381,31 +381,50 @@ export async function cmdStatus(args) {
|
|
|
381
381
|
return;
|
|
382
382
|
}
|
|
383
383
|
const config = await ConfigLoader.load(configPath);
|
|
384
|
-
let
|
|
384
|
+
let diag = null;
|
|
385
385
|
try {
|
|
386
386
|
const res = await ipcCall(config, { op: "diag" }, 5_000);
|
|
387
387
|
if (res.ok)
|
|
388
|
-
|
|
388
|
+
diag = res.data ?? null;
|
|
389
389
|
}
|
|
390
390
|
catch {
|
|
391
391
|
// daemon not running — fall back to config below
|
|
392
392
|
}
|
|
393
|
-
const running = !!liveTun;
|
|
394
393
|
console.log(`Decent AgentNet`);
|
|
395
394
|
console.log(`Node: ${config.node.name}`);
|
|
396
395
|
console.log(`Namespace: ${config.node.namespace}`);
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
396
|
+
if (!diag) {
|
|
397
|
+
console.log(`Daemon: not running — everything below is CONFIGURED, not live`);
|
|
398
|
+
console.log(`Subnet: ${config.network.subnet}`);
|
|
399
|
+
console.log(`Interface: ${config.network.interface}`);
|
|
400
|
+
console.log(`Virtual IP: ${config.network.ip}`);
|
|
401
|
+
console.log(`Bootstrap: ${config.carrier.bootstrapNodes.length} nodes`);
|
|
402
|
+
const ipamOffline = await Ipam.loadOrCreate(config.paths.ipamFile, config.node.namespace);
|
|
403
|
+
console.log(`Peers (on disk): ${ipamOffline.getPeers().length}`);
|
|
404
|
+
const policyOffline = await Policy.loadOrCreate(config.paths.policyFile);
|
|
405
|
+
console.log(`ACL rules: ${policyOffline.getRules().length}`);
|
|
406
|
+
console.log(`\nStart it with 'agentnet up --real-tun' (or 'agentnet service install').`);
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
const friends = diag.friends ?? [];
|
|
410
|
+
const online = friends.filter((f) => f.status === "online" || f.status === "connected").length;
|
|
411
|
+
const s = diag.stats ?? {};
|
|
412
|
+
console.log(`Daemon: running`);
|
|
413
|
+
if (diag.identity?.userid)
|
|
414
|
+
console.log(`Userid: ${diag.identity.userid}`);
|
|
415
|
+
if (diag.identity?.address)
|
|
416
|
+
console.log(`Address: ${diag.identity.address}`);
|
|
417
|
+
console.log(`Subnet: ${diag.tun?.subnet ?? config.network.subnet}`);
|
|
418
|
+
console.log(`Interface: ${diag.tun?.name ?? config.network.interface}`);
|
|
419
|
+
console.log(`Virtual IP: ${diag.tun?.ip ?? "(no TUN)"}`);
|
|
420
|
+
if (diag.tun?.ip && diag.tun.ip !== config.network.ip) {
|
|
402
421
|
console.log(` note: config.yaml still says ${config.network.ip} — the daemon's address wins`);
|
|
403
422
|
}
|
|
404
423
|
console.log(`Bootstrap: ${config.carrier.bootstrapNodes.length} nodes`);
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
console.log(`
|
|
408
|
-
|
|
424
|
+
console.log(`Peers (roster): ${diag.ipam?.length ?? 0}`);
|
|
425
|
+
console.log(`Friends: ${friends.length} (${online} online)`);
|
|
426
|
+
console.log(`Sessions: ${s.activeSessions ?? 0} active`);
|
|
427
|
+
console.log(`Traffic: ${s.packetsForwarded ?? 0} sent / ${s.packetsReceived ?? 0} received / ${s.packetsDropped ?? 0} dropped`);
|
|
409
428
|
const policy = await Policy.loadOrCreate(config.paths.policyFile);
|
|
410
429
|
console.log(`ACL rules: ${policy.getRules().length}`);
|
|
411
430
|
}
|
package/dist/ui/desktop/app.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
window.__DK_UI_VERSION="0.1.234";
|
|
1
2
|
const ICON_PATHS = {
|
|
2
3
|
// ---- tab bar (the four must feel like one set) ----
|
|
3
4
|
users: '<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>',
|
|
@@ -40,6 +41,8 @@ const ICON_PATHS = {
|
|
|
40
41
|
// ---- account ----
|
|
41
42
|
shield: '<path d="M12 21.5s7.5-3.8 7.5-9.5V5.5L12 2.8 4.5 5.5V12c0 5.7 7.5 9.5 7.5 9.5Z"/>',
|
|
42
43
|
download: '<path d="M12 3.5v11M7.5 10l4.5 4.5 4.5-4.5"/><path d="M5 20.5h14"/>',
|
|
44
|
+
// Folder with a magnifier corner — "reveal / show in file manager".
|
|
45
|
+
folderOpen: '<path d="M3 7.5a2 2 0 0 1 2-2h4l2 2.5h8a2 2 0 0 1 2 2V17a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z"/><circle cx="12" cy="13" r="2.4"/><path d="M13.9 14.9 15.5 16.5"/>',
|
|
43
46
|
migrate: '<path d="M8 4 4 8l4 4"/><path d="M4 8h13a3 3 0 0 1 3 3v1"/><path d="m16 20 4-4-4-4"/><path d="M20 16H7a3 3 0 0 1-3-3v-1"/>',
|
|
44
47
|
starCoin: '<circle cx="12" cy="12" r="9"/><path d="m12 7.2 1.4 2.9 3.2.4-2.3 2.2.6 3.1-2.9-1.5-2.9 1.5.6-3.1L8.4 10.5l3.2-.4L12 7.2Z" fill="currentColor" stroke="none"/>',
|
|
45
48
|
link: '<path d="M10.5 13.5a4 4 0 0 0 5.7 0l2.3-2.3a4 4 0 0 0-5.7-5.7l-1 1"/><path d="M13.5 10.5a4 4 0 0 0-5.7 0l-2.3 2.3a4 4 0 0 0 5.7 5.7l1-1"/>',
|
|
@@ -231,6 +234,8 @@ const dkApi = {
|
|
|
231
234
|
alias: (userid, alias) => dkPost("/api/friend-alias", { userid, alias }),
|
|
232
235
|
markRead: (userid) => dkPost("/api/chat-mark-read", { userid }),
|
|
233
236
|
delFiles: (userid, ids) => dkPost("/api/file-delete", { userid, ids }),
|
|
237
|
+
// Reveal a received file in the OS file manager (localhost-only on the daemon side).
|
|
238
|
+
revealFile: (name) => dkPost("/api/file-reveal", { name }),
|
|
234
239
|
cancelSend: (userid, ids) => dkPost("/api/file-cancel", { userid, ids }),
|
|
235
240
|
retrySend: (userid, ids) => dkPost("/api/file-retry", { userid, ids }),
|
|
236
241
|
setProfile: (name, description) => dkPost("/api/set-profile", { name, description }),
|
|
@@ -1139,10 +1144,17 @@ function PeerRow({ peer, T, active, onClick }) {
|
|
|
1139
1144
|
minWidth: 0
|
|
1140
1145
|
} }, peer.lastMsg))), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", flexDirection: "column", alignItems: "flex-end", gap: 4, flexShrink: 0 } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 10.5, color: "var(--faint)" } }, peer.lastTime), peer.unread ? /* @__PURE__ */ React.createElement(Unread, { n: peer.unread }) : /* @__PURE__ */ React.createElement(StatusDot, { online: peer.online })));
|
|
1141
1146
|
}
|
|
1142
|
-
function PeerSidebar({ T, peers, requests, activeId, onSelect, onAct, onAdd }) {
|
|
1147
|
+
function PeerSidebar({ T, peers, requests, activeId, onSelect, onAct, onAdd, prefillAddr, onPrefillConsumed }) {
|
|
1143
1148
|
const [q, setQ] = React.useState("");
|
|
1144
1149
|
const [addr, setAddr] = React.useState("");
|
|
1145
1150
|
const [addState, setAddState] = React.useState(null);
|
|
1151
|
+
const [fromDeepLink, setFromDeepLink] = React.useState(false);
|
|
1152
|
+
React.useEffect(() => {
|
|
1153
|
+
if (prefillAddr) {
|
|
1154
|
+
setAddr(prefillAddr);
|
|
1155
|
+
setFromDeepLink(true);
|
|
1156
|
+
}
|
|
1157
|
+
}, [prefillAddr]);
|
|
1146
1158
|
const submitAddr = () => {
|
|
1147
1159
|
const v = addr.trim();
|
|
1148
1160
|
if (!v || addState && addState.kind === "sending") return;
|
|
@@ -1150,6 +1162,8 @@ function PeerSidebar({ T, peers, requests, activeId, onSelect, onAct, onAdd }) {
|
|
|
1150
1162
|
Promise.resolve(onAdd(v)).then((r) => {
|
|
1151
1163
|
if (r && r.ok) {
|
|
1152
1164
|
setAddr("");
|
|
1165
|
+
setFromDeepLink(false);
|
|
1166
|
+
onPrefillConsumed && onPrefillConsumed();
|
|
1153
1167
|
setAddState({ kind: "ok", msg: T.addSent });
|
|
1154
1168
|
} else {
|
|
1155
1169
|
setAddState({ kind: "err", msg: r && r.error ? `${T.addFailed}: ${r.error}` : T.addFailed });
|
|
@@ -1166,9 +1180,30 @@ function PeerSidebar({ T, peers, requests, activeId, onSelect, onAct, onAdd }) {
|
|
|
1166
1180
|
return s.toLowerCase().includes(q.toLowerCase());
|
|
1167
1181
|
});
|
|
1168
1182
|
const online = peers.filter((p) => p.online).length;
|
|
1169
|
-
return /* @__PURE__ */ React.createElement("div", { style: { width: 320, flexShrink: 0, borderRight: "1px solid var(--line)", display: "flex", flexDirection: "column", background: "var(--panel)" } }, /* @__PURE__ */ React.createElement("div", { style: { padding: "12px 12px 10px", borderBottom: "1px solid var(--line)", display: "flex", flexDirection: "column", gap: 8 } }, /* @__PURE__ */ React.createElement("div", { style: {
|
|
1170
|
-
|
|
1171
|
-
|
|
1183
|
+
return /* @__PURE__ */ React.createElement("div", { style: { width: 320, flexShrink: 0, borderRight: "1px solid var(--line)", display: "flex", flexDirection: "column", background: "var(--panel)" } }, /* @__PURE__ */ React.createElement("div", { style: { padding: "12px 12px 10px", borderBottom: "1px solid var(--line)", display: "flex", flexDirection: "column", gap: 8 } }, fromDeepLink && /* @__PURE__ */ React.createElement("div", { style: {
|
|
1184
|
+
fontFamily: "var(--ui)",
|
|
1185
|
+
fontSize: 12,
|
|
1186
|
+
lineHeight: 1.4,
|
|
1187
|
+
color: "var(--text)",
|
|
1188
|
+
background: "color-mix(in oklab, var(--accent), transparent 88%)",
|
|
1189
|
+
border: "1px solid var(--accent)",
|
|
1190
|
+
borderRadius: 8,
|
|
1191
|
+
padding: "7px 10px"
|
|
1192
|
+
} }, T.deepLinkConfirm || "\u901A\u8FC7\u94FE\u63A5\u6DFB\u52A0\u8FD9\u4F4D\u597D\u53CB? \u70B9\u300C\u6DFB\u52A0\u300D\u53D1\u9001\u597D\u53CB\u8BF7\u6C42 / Add this contact from the link? Tap Add to send a friend request."), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", gap: 7 } }, /* @__PURE__ */ React.createElement(
|
|
1193
|
+
"input",
|
|
1194
|
+
{
|
|
1195
|
+
value: addr,
|
|
1196
|
+
onChange: (e) => {
|
|
1197
|
+
setAddr(e.target.value);
|
|
1198
|
+
if (fromDeepLink) setFromDeepLink(false);
|
|
1199
|
+
},
|
|
1200
|
+
onKeyDown: (e) => {
|
|
1201
|
+
if (e.key === "Enter") submitAddr();
|
|
1202
|
+
},
|
|
1203
|
+
placeholder: T.addPlaceholder,
|
|
1204
|
+
style: { ...inputStyle, ...fromDeepLink ? { borderColor: "var(--accent)" } : null }
|
|
1205
|
+
}
|
|
1206
|
+
), /* @__PURE__ */ React.createElement(Btn, { tone: "solid", icon: "userPlus", onClick: submitAddr }, T.add)), addState && /* @__PURE__ */ React.createElement("div", { style: {
|
|
1172
1207
|
fontFamily: "var(--mono)",
|
|
1173
1208
|
fontSize: 11.5,
|
|
1174
1209
|
lineHeight: 1.35,
|
|
@@ -1305,7 +1340,7 @@ function dkMarkdownHtml(src) {
|
|
|
1305
1340
|
function MarkdownText({ text }) {
|
|
1306
1341
|
return /* @__PURE__ */ React.createElement("div", { className: "dk-md", dangerouslySetInnerHTML: { __html: dkMarkdownHtml(text) } });
|
|
1307
1342
|
}
|
|
1308
|
-
function Msg({ m, peer, T, onTheater, onDelete, onCancel, onRetry, onCall, selMode, selected, onToggleSel, busy }) {
|
|
1343
|
+
function Msg({ m, peer, T, onTheater, onDelete, onCancel, onRetry, onReveal, onCall, selMode, selected, onToggleSel, busy }) {
|
|
1309
1344
|
const mine = m.from === "me";
|
|
1310
1345
|
const rtcFile = !mine && !m.file ? dkRtcFileFromText(peer.userId || peer.id, m.text) : null;
|
|
1311
1346
|
const callRec = !m.file ? dkCallFromText(m.text) : null;
|
|
@@ -1443,7 +1478,18 @@ function Msg({ m, peer, T, onTheater, onDelete, onCancel, onRetry, onCall, selMo
|
|
|
1443
1478
|
style: mediaBtnStyle
|
|
1444
1479
|
},
|
|
1445
1480
|
/* @__PURE__ */ React.createElement(Icon, { name: "maximize", size: 16, stroke: 2, color: "#fff" })
|
|
1446
|
-
))), m.file.media === "audio" && /* @__PURE__ */ React.createElement("audio", { src: dkFileUrl(m.file.name), controls: true, style: { width: "100%" } }), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8, padding: "0 2px" } }, /* @__PURE__ */ React.createElement("span", { style: { flex: 1, minWidth: 0, fontFamily: "var(--mono)", fontSize: 11, color: "var(--faint)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" } }, m.file.name), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11, color: "var(--faint)", flexShrink: 0 } }, m.file.size),
|
|
1481
|
+
))), m.file.media === "audio" && /* @__PURE__ */ React.createElement("audio", { src: dkFileUrl(m.file.name), controls: true, style: { width: "100%" } }), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8, padding: "0 2px" } }, /* @__PURE__ */ React.createElement("span", { style: { flex: 1, minWidth: 0, fontFamily: "var(--mono)", fontSize: 11, color: "var(--faint)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" } }, m.file.name), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11, color: "var(--faint)", flexShrink: 0 } }, m.file.size), onReveal && /* @__PURE__ */ React.createElement(
|
|
1482
|
+
"button",
|
|
1483
|
+
{
|
|
1484
|
+
title: T.revealInFinder || "\u5728\u6587\u4EF6\u7BA1\u7406\u5668\u4E2D\u663E\u793A / Show in file manager",
|
|
1485
|
+
onClick: (e) => {
|
|
1486
|
+
e.stopPropagation();
|
|
1487
|
+
onReveal(m.file.name);
|
|
1488
|
+
},
|
|
1489
|
+
style: { background: "none", border: "none", cursor: "pointer", padding: 0, display: "inline-flex", flexShrink: 0 }
|
|
1490
|
+
},
|
|
1491
|
+
/* @__PURE__ */ React.createElement(Icon, { name: "folderOpen", size: 15, stroke: 2, color: "var(--accent)" })
|
|
1492
|
+
), /* @__PURE__ */ React.createElement("a", { href: dkFileDownloadUrl(m.file.name), download: m.file.name, title: "download", style: { display: "inline-flex", flexShrink: 0 } }, /* @__PURE__ */ React.createElement(Icon, { name: "download", size: 15, stroke: 2, color: "var(--accent)" }))))
|
|
1447
1493
|
) : (() => {
|
|
1448
1494
|
const cardStyle = {
|
|
1449
1495
|
display: "flex",
|
|
@@ -1496,6 +1542,18 @@ function Msg({ m, peer, T, onTheater, onDelete, onCancel, onRetry, onCall, selMo
|
|
|
1496
1542
|
},
|
|
1497
1543
|
icon,
|
|
1498
1544
|
meta,
|
|
1545
|
+
onReveal && /* @__PURE__ */ React.createElement(
|
|
1546
|
+
"button",
|
|
1547
|
+
{
|
|
1548
|
+
title: T.revealInFinder || "\u5728\u6587\u4EF6\u7BA1\u7406\u5668\u4E2D\u663E\u793A / Show in file manager",
|
|
1549
|
+
onClick: (e) => {
|
|
1550
|
+
e.stopPropagation();
|
|
1551
|
+
onReveal(m.file.name);
|
|
1552
|
+
},
|
|
1553
|
+
style: { background: "none", border: "none", cursor: "pointer", padding: 0, display: "inline-flex", flexShrink: 0 }
|
|
1554
|
+
},
|
|
1555
|
+
/* @__PURE__ */ React.createElement(Icon, { name: "folderOpen", size: 16, stroke: 2, color: "var(--accent)" })
|
|
1556
|
+
),
|
|
1499
1557
|
/* @__PURE__ */ React.createElement(
|
|
1500
1558
|
"a",
|
|
1501
1559
|
{
|
|
@@ -1620,6 +1678,14 @@ function Conversation({ T, peer, lang, thread: threadProp, onSend, onSendFile, o
|
|
|
1620
1678
|
delete n[id];
|
|
1621
1679
|
return n;
|
|
1622
1680
|
});
|
|
1681
|
+
const doReveal = (name) => {
|
|
1682
|
+
if (!name) return;
|
|
1683
|
+
dkApi.revealFile(name).then((r) => {
|
|
1684
|
+
if (!r || r.ok === false) {
|
|
1685
|
+
showFlash("err", (lang === "zh" ? "\u65E0\u6CD5\u5728\u6587\u4EF6\u7BA1\u7406\u5668\u4E2D\u663E\u793A: " : "Could not reveal file: ") + (r && r.error || ""));
|
|
1686
|
+
}
|
|
1687
|
+
});
|
|
1688
|
+
};
|
|
1623
1689
|
const doCancel = (id) => {
|
|
1624
1690
|
if (!id || fileBusy[id]) return;
|
|
1625
1691
|
setFileBusy((b) => ({ ...b, [id]: "cancel" }));
|
|
@@ -1806,6 +1872,7 @@ ${peer.address}`
|
|
|
1806
1872
|
onDelete: (id) => doDelete([id]),
|
|
1807
1873
|
onCancel: doCancel,
|
|
1808
1874
|
onRetry: doRetry,
|
|
1875
|
+
onReveal: doReveal,
|
|
1809
1876
|
onCall,
|
|
1810
1877
|
busy: m.id ? fileBusy[m.id] : void 0,
|
|
1811
1878
|
selMode,
|
|
@@ -1929,9 +1996,9 @@ function MenuItem({ icon, label, onClick, danger }) {
|
|
|
1929
1996
|
function ChatEmpty({ T }) {
|
|
1930
1997
|
return /* @__PURE__ */ React.createElement("div", { style: { flex: 1, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 12, color: "var(--faint)", background: "var(--bg)" } }, /* @__PURE__ */ React.createElement(Icon, { name: "message", size: 40, stroke: 1.4, color: "var(--line)" }), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 13 } }, T.pickPeer));
|
|
1931
1998
|
}
|
|
1932
|
-
function ChatTab({ T, lang, peers, requests, activeId, thread, onSelect, onAct, onAdd, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread }) {
|
|
1999
|
+
function ChatTab({ T, lang, peers, requests, activeId, thread, onSelect, onAct, onAdd, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, prefillAddr, onPrefillConsumed }) {
|
|
1933
2000
|
const peer = peers.find((p) => p.id === activeId);
|
|
1934
|
-
return /* @__PURE__ */ React.createElement("div", { style: { flex: 1, display: "flex", minWidth: 0, minHeight: 0 } }, /* @__PURE__ */ React.createElement(PeerSidebar, { T, peers, requests, activeId, onSelect, onAct, onAdd }), peer ? /* @__PURE__ */ React.createElement(Conversation, { T, peer, lang, thread, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread }) : /* @__PURE__ */ React.createElement(ChatEmpty, { T }));
|
|
2001
|
+
return /* @__PURE__ */ React.createElement("div", { style: { flex: 1, display: "flex", minWidth: 0, minHeight: 0 } }, /* @__PURE__ */ React.createElement(PeerSidebar, { T, peers, requests, activeId, onSelect, onAct, onAdd, prefillAddr, onPrefillConsumed }), peer ? /* @__PURE__ */ React.createElement(Conversation, { T, peer, lang, thread, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread }) : /* @__PURE__ */ React.createElement(ChatEmpty, { T }));
|
|
1935
2002
|
}
|
|
1936
2003
|
Object.assign(window, { ChatTab });
|
|
1937
2004
|
function StatTile({ label, value, sub, tone }) {
|
|
@@ -2644,6 +2711,20 @@ function CallOverlay({ T, ctl, peers }) {
|
|
|
2644
2711
|
})() : /* @__PURE__ */ React.createElement("div", { style: { flex: 1, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 22 } }, /* @__PURE__ */ React.createElement("div", { style: { position: "relative", display: "flex", alignItems: "center", justifyContent: "center" } }, !connected && /* @__PURE__ */ React.createElement("span", { style: { position: "absolute", width: 150, height: 150, borderRadius: 999, border: "2px solid var(--accent)", animation: "dkpulse 2s ease-out infinite" } }), /* @__PURE__ */ React.createElement(DkIdenticon, { seed: call.peerId, size: 108, radius: 26 })), /* @__PURE__ */ React.createElement("div", { style: { textAlign: "center" } }, /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 22, fontWeight: 700, color: "var(--text)" } }, name), /* @__PURE__ */ React.createElement("div", { style: { marginTop: 8, fontFamily: "var(--mono)", fontSize: 12.5, color: "var(--faint)" } }, stateLabel)), /* @__PURE__ */ React.createElement("div", { style: { marginTop: 18 } }, controls)));
|
|
2645
2712
|
}
|
|
2646
2713
|
Object.assign(window, { useCallController, CallOverlay, IncomingCallModal });
|
|
2714
|
+
function dkParseDeepLinkAddress() {
|
|
2715
|
+
try {
|
|
2716
|
+
const h = window.location.hash || "";
|
|
2717
|
+
const qi = h.indexOf("?");
|
|
2718
|
+
if (qi >= 0) {
|
|
2719
|
+
const a = new URLSearchParams(h.slice(qi + 1)).get("address");
|
|
2720
|
+
if (a && a.trim()) return a.trim();
|
|
2721
|
+
}
|
|
2722
|
+
const a2 = new URLSearchParams(window.location.search || "").get("address");
|
|
2723
|
+
if (a2 && a2.trim()) return a2.trim();
|
|
2724
|
+
} catch (e) {
|
|
2725
|
+
}
|
|
2726
|
+
return "";
|
|
2727
|
+
}
|
|
2647
2728
|
const DK_DEFAULTS = (
|
|
2648
2729
|
/*EDITMODE-BEGIN*/
|
|
2649
2730
|
{
|
|
@@ -2912,6 +2993,43 @@ function DkApp() {
|
|
|
2912
2993
|
const [activeId, setActiveId] = React.useState(null);
|
|
2913
2994
|
const data = useDaemonData();
|
|
2914
2995
|
const me = data.me;
|
|
2996
|
+
React.useEffect(() => {
|
|
2997
|
+
const mine = window.__DK_UI_VERSION;
|
|
2998
|
+
const served = me && me.lanVer;
|
|
2999
|
+
if (!mine || !served || mine === served) return;
|
|
3000
|
+
const key = "dk-reloaded-for-" + served;
|
|
3001
|
+
if (sessionStorage.getItem(key)) return;
|
|
3002
|
+
sessionStorage.setItem(key, "1");
|
|
3003
|
+
window.location.reload();
|
|
3004
|
+
}, [me && me.lanVer]);
|
|
3005
|
+
const [pendingAddr, setPendingAddr] = React.useState(() => dkParseDeepLinkAddress());
|
|
3006
|
+
React.useEffect(() => {
|
|
3007
|
+
const onHash = () => {
|
|
3008
|
+
const a = dkParseDeepLinkAddress();
|
|
3009
|
+
if (a) setPendingAddr(a);
|
|
3010
|
+
};
|
|
3011
|
+
window.addEventListener("hashchange", onHash);
|
|
3012
|
+
return () => window.removeEventListener("hashchange", onHash);
|
|
3013
|
+
}, []);
|
|
3014
|
+
React.useEffect(() => {
|
|
3015
|
+
if (!pendingAddr) return;
|
|
3016
|
+
setTab("chat");
|
|
3017
|
+
if (!peers || !peers.length) return;
|
|
3018
|
+
const hit = peers.find((p) => p.address && p.address === pendingAddr);
|
|
3019
|
+
if (hit) {
|
|
3020
|
+
setActiveId(hit.id);
|
|
3021
|
+
setPendingAddr("");
|
|
3022
|
+
try {
|
|
3023
|
+
window.history.replaceState(null, "", window.location.pathname);
|
|
3024
|
+
} catch (e) {
|
|
3025
|
+
}
|
|
3026
|
+
} else {
|
|
3027
|
+
try {
|
|
3028
|
+
window.history.replaceState(null, "", window.location.pathname);
|
|
3029
|
+
} catch (e) {
|
|
3030
|
+
}
|
|
3031
|
+
}
|
|
3032
|
+
}, [pendingAddr, peers]);
|
|
2915
3033
|
const peers = data.peers;
|
|
2916
3034
|
const requests = data.requests;
|
|
2917
3035
|
const exits = data.exits;
|
|
@@ -3010,7 +3128,7 @@ function DkApp() {
|
|
|
3010
3128
|
{ id: "network", icon: "network", label: T.network },
|
|
3011
3129
|
{ id: "profile", icon: "userRound", label: T.profile }
|
|
3012
3130
|
];
|
|
3013
|
-
return /* @__PURE__ */ React.createElement("div", { style: { ...vars, "--row-pad": rowPad, position: "fixed", inset: 0, display: "flex", background: "var(--bg)", color: "var(--text)", fontFamily: "var(--ui)" } }, /* @__PURE__ */ React.createElement("div", { style: { width: 68, flexShrink: 0, borderRight: "1px solid var(--line)", background: "var(--rail)", display: "flex", flexDirection: "column", alignItems: "center", padding: "14px 0", gap: 8 } }, /* @__PURE__ */ React.createElement("div", { style: { width: 38, height: 38, borderRadius: 10, background: "var(--accent)", display: "flex", alignItems: "center", justifyContent: "center", marginBottom: 8 } }, /* @__PURE__ */ React.createElement(Icon, { name: "terminal", size: 20, color: "#fff", stroke: 2.2 })), nav.map((n) => /* @__PURE__ */ React.createElement(RailBtn, { key: n.id, icon: n.icon, label: n.label, active: tab === n.id, soon: n.soon, onClick: () => setTab(n.id) })), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement("div", { style: { position: "relative" } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: { ...me, id: me.userId, agent: false }, size: 36, radius: 9 }))), /* @__PURE__ */ React.createElement("div", { style: { flex: 1, minWidth: 0, minHeight: 0, display: "flex", flexDirection: "column" } }, /* @__PURE__ */ React.createElement("div", { style: { height: 46, flexShrink: 0, borderBottom: "1px solid var(--line)", background: "var(--panel)", display: "flex", alignItems: "center", gap: 12, padding: "0 16px" } }, /* @__PURE__ */ React.createElement("svg", { width: 20, height: 20, viewBox: "0 0 24 24", fill: "none", stroke: "var(--accent)", strokeWidth: 2.2, strokeLinecap: "round", strokeLinejoin: "round", style: { display: "block", flexShrink: 0 } }, /* @__PURE__ */ React.createElement("path", { d: "m4.5 17 6-6-6-6" }), /* @__PURE__ */ React.createElement("path", { d: "M12 18.5h7.5" })), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 14, fontWeight: 700, letterSpacing: -0.3, color: "var(--text)" } }, "beagle"), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12, color: "var(--faint)" } }, "\xB7 ", nav.find((n) => n.id === tab).label.toLowerCase()), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement(Tag, { tone: "accent" }, me.channel, " \xB7 lan ", me.lanVer), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 7, padding: "0 4px" } }, /* @__PURE__ */ React.createElement(StatusDot, { online: me.online }), /* @__PURE__ */ React.createElement(Mono, { size: 12.5, copy: me.ip }, me.ip)), /* @__PURE__ */ React.createElement("span", { style: { width: 1, height: 22, background: "var(--line)" } }), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8 } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: { ...me, id: me.userId, agent: false }, size: 26, radius: 7 }), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12.5, fontWeight: 600, color: "var(--text)" } }, me.name))), tab === "chat" && /* @__PURE__ */ React.createElement(ChatTab, { T, lang: t.lang, peers, requests, activeId, thread: data.threads[activeId], onSelect, onAct, onAdd, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread: () => activeId && data.loadThread(activeId) }), tab === "network" && /* @__PURE__ */ React.createElement(NetworkTab, { T, me, peers, exits, activeExit, reqCount: requests.length, onSetExit, onOpenChat }), tab === "profile" && /* @__PURE__ */ React.createElement(ProfileTab, { T, me, onEdit })), /* @__PURE__ */ React.createElement(TweaksPanel, null, /* @__PURE__ */ React.createElement(TweakSection, { label: t.lang === "zh" ? "\u5916\u89C2" : "Appearance" }), /* @__PURE__ */ React.createElement(
|
|
3131
|
+
return /* @__PURE__ */ React.createElement("div", { style: { ...vars, "--row-pad": rowPad, position: "fixed", inset: 0, display: "flex", background: "var(--bg)", color: "var(--text)", fontFamily: "var(--ui)" } }, /* @__PURE__ */ React.createElement("div", { style: { width: 68, flexShrink: 0, borderRight: "1px solid var(--line)", background: "var(--rail)", display: "flex", flexDirection: "column", alignItems: "center", padding: "14px 0", gap: 8 } }, /* @__PURE__ */ React.createElement("div", { style: { width: 38, height: 38, borderRadius: 10, background: "var(--accent)", display: "flex", alignItems: "center", justifyContent: "center", marginBottom: 8 } }, /* @__PURE__ */ React.createElement(Icon, { name: "terminal", size: 20, color: "#fff", stroke: 2.2 })), nav.map((n) => /* @__PURE__ */ React.createElement(RailBtn, { key: n.id, icon: n.icon, label: n.label, active: tab === n.id, soon: n.soon, onClick: () => setTab(n.id) })), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement("div", { style: { position: "relative" } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: { ...me, id: me.userId, agent: false }, size: 36, radius: 9 }))), /* @__PURE__ */ React.createElement("div", { style: { flex: 1, minWidth: 0, minHeight: 0, display: "flex", flexDirection: "column" } }, /* @__PURE__ */ React.createElement("div", { style: { height: 46, flexShrink: 0, borderBottom: "1px solid var(--line)", background: "var(--panel)", display: "flex", alignItems: "center", gap: 12, padding: "0 16px" } }, /* @__PURE__ */ React.createElement("svg", { width: 20, height: 20, viewBox: "0 0 24 24", fill: "none", stroke: "var(--accent)", strokeWidth: 2.2, strokeLinecap: "round", strokeLinejoin: "round", style: { display: "block", flexShrink: 0 } }, /* @__PURE__ */ React.createElement("path", { d: "m4.5 17 6-6-6-6" }), /* @__PURE__ */ React.createElement("path", { d: "M12 18.5h7.5" })), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 14, fontWeight: 700, letterSpacing: -0.3, color: "var(--text)" } }, "beagle"), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12, color: "var(--faint)" } }, "\xB7 ", nav.find((n) => n.id === tab).label.toLowerCase()), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement(Tag, { tone: "accent" }, me.channel, " \xB7 lan ", me.lanVer), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 7, padding: "0 4px" } }, /* @__PURE__ */ React.createElement(StatusDot, { online: me.online }), /* @__PURE__ */ React.createElement(Mono, { size: 12.5, copy: me.ip }, me.ip)), /* @__PURE__ */ React.createElement("span", { style: { width: 1, height: 22, background: "var(--line)" } }), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8 } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: { ...me, id: me.userId, agent: false }, size: 26, radius: 7 }), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12.5, fontWeight: 600, color: "var(--text)" } }, me.name))), tab === "chat" && /* @__PURE__ */ React.createElement(ChatTab, { T, lang: t.lang, peers, requests, activeId, thread: data.threads[activeId], onSelect, onAct, onAdd, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread: () => activeId && data.loadThread(activeId), prefillAddr: pendingAddr, onPrefillConsumed: () => setPendingAddr("") }), tab === "network" && /* @__PURE__ */ React.createElement(NetworkTab, { T, me, peers, exits, activeExit, reqCount: requests.length, onSetExit, onOpenChat }), tab === "profile" && /* @__PURE__ */ React.createElement(ProfileTab, { T, me, onEdit })), /* @__PURE__ */ React.createElement(TweaksPanel, null, /* @__PURE__ */ React.createElement(TweakSection, { label: t.lang === "zh" ? "\u5916\u89C2" : "Appearance" }), /* @__PURE__ */ React.createElement(
|
|
3014
3132
|
TweakRadio,
|
|
3015
3133
|
{
|
|
3016
3134
|
label: t.lang === "zh" ? "\u4E3B\u9898" : "Theme",
|
package/dist/ui/server.js
CHANGED
|
@@ -35,6 +35,74 @@ function isLocalRequest(req) {
|
|
|
35
35
|
const a = req.socket.remoteAddress ?? "";
|
|
36
36
|
return a === "127.0.0.1" || a === "::1" || a === "::ffff:127.0.0.1";
|
|
37
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* Reveal a file in the OS file manager (Finder / Explorer / Nautilus), so the
|
|
40
|
+
* user can play, share, AirDrop, copy or delete it with native tools instead
|
|
41
|
+
* of re-downloading — the file is already on disk under downloadsDir.
|
|
42
|
+
*
|
|
43
|
+
* Two things make this fiddly:
|
|
44
|
+
* - The daemon usually runs as ROOT (started via sudo). A GUI command run as
|
|
45
|
+
* root either opens in root's own (invisible) session or is refused. On
|
|
46
|
+
* macOS the fix is `launchctl asuser <uid>` to hop into the logged-in
|
|
47
|
+
* user's Aqua session; elsewhere we drop to `SUDO_USER` with `sudo -u`.
|
|
48
|
+
* - WSL has no Linux file manager but can drive Windows Explorer via the
|
|
49
|
+
* translated \\wsl$ path.
|
|
50
|
+
*
|
|
51
|
+
* Best-effort: returns {ok:false, error} rather than throwing so the caller
|
|
52
|
+
* can surface a message. `filePath` MUST already be validated as inside
|
|
53
|
+
* downloadsDir by the caller.
|
|
54
|
+
*/
|
|
55
|
+
async function revealInFileManager(filePath) {
|
|
56
|
+
const { spawn } = await import("node:child_process");
|
|
57
|
+
const run = (cmd, args) => new Promise((resolve) => {
|
|
58
|
+
let child;
|
|
59
|
+
try {
|
|
60
|
+
child = spawn(cmd, args, { stdio: "ignore", detached: true });
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
resolve({ ok: false, error: `${cmd}: ${e.message}` });
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
child.on("error", (e) => resolve({ ok: false, error: `${cmd}: ${e.message}` }));
|
|
67
|
+
// Windows Explorer returns exit code 1 even on success, so treat spawn
|
|
68
|
+
// without an 'error' event as success rather than gating on the code.
|
|
69
|
+
child.unref();
|
|
70
|
+
setTimeout(() => resolve({ ok: true }), 150);
|
|
71
|
+
});
|
|
72
|
+
const asRoot = typeof process.getuid === "function" && process.getuid() === 0;
|
|
73
|
+
const sudoUser = process.env.SUDO_USER;
|
|
74
|
+
const sudoUid = process.env.SUDO_UID;
|
|
75
|
+
if (process.platform === "darwin") {
|
|
76
|
+
// Run `open -R` inside the login user's GUI session when we're root.
|
|
77
|
+
if (asRoot && sudoUid) {
|
|
78
|
+
return run("launchctl", ["asuser", sudoUid, "open", "-R", filePath]);
|
|
79
|
+
}
|
|
80
|
+
return run("open", ["-R", filePath]);
|
|
81
|
+
}
|
|
82
|
+
if (process.platform === "win32") {
|
|
83
|
+
return run("explorer.exe", [`/select,${filePath}`]);
|
|
84
|
+
}
|
|
85
|
+
// Linux (incl. WSL). WSL: drive Windows Explorer with the \\wsl$ UNC path.
|
|
86
|
+
const isWsl = existsSync("/proc/version") &&
|
|
87
|
+
(await import("node:fs/promises")).readFile("/proc/version", "utf-8").then((v) => v.toLowerCase().includes("microsoft"), () => false);
|
|
88
|
+
if (await isWsl) {
|
|
89
|
+
let winPath = filePath;
|
|
90
|
+
try {
|
|
91
|
+
const { execFileSync } = await import("node:child_process");
|
|
92
|
+
winPath = execFileSync("wslpath", ["-w", filePath], { encoding: "utf-8" }).trim() || filePath;
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
// wslpath missing — fall through with the raw path
|
|
96
|
+
}
|
|
97
|
+
return run("explorer.exe", [`/select,${winPath}`]);
|
|
98
|
+
}
|
|
99
|
+
// A headless server has no file manager; xdg-open on the parent folder is the
|
|
100
|
+
// best we can do where a desktop exists. Drop privileges to the login user.
|
|
101
|
+
const dir = filePath.slice(0, filePath.lastIndexOf("/")) || "/";
|
|
102
|
+
if (asRoot && sudoUser)
|
|
103
|
+
return run("sudo", ["-u", sudoUser, "xdg-open", dir]);
|
|
104
|
+
return run("xdg-open", [dir]);
|
|
105
|
+
}
|
|
38
106
|
/** Validate and normalize a website origin (scheme://host[:port], no path).
|
|
39
107
|
* Returns the canonical origin string or null if malformed. Only http/https
|
|
40
108
|
* are accepted. */
|
|
@@ -400,6 +468,31 @@ export function startFriendUi(opts) {
|
|
|
400
468
|
// download (?dl=1 → attachment) and inline preview/playback (no dl → the
|
|
401
469
|
// right media content-type + inline disposition + HTTP Range so <video>/
|
|
402
470
|
// <audio> can seek without buffering the whole file).
|
|
471
|
+
// Reveal a received file in the OS file manager. LOCALHOST-ONLY: this
|
|
472
|
+
// opens Finder/Explorer on the machine the DAEMON runs on, so it's
|
|
473
|
+
// meaningful only for someone sitting at that machine — never honour it
|
|
474
|
+
// for a remotely-opened UI (which could otherwise pop a file manager on
|
|
475
|
+
// the host at a stranger's request).
|
|
476
|
+
if (req.method === "POST" && url === "/api/file-reveal") {
|
|
477
|
+
if (!opts.downloadsDir) {
|
|
478
|
+
sendJson(res, 404, { ok: false, error: "downloads disabled" });
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
if (!isLocalRequest(req)) {
|
|
482
|
+
sendJson(res, 403, { ok: false, error: "reveal is available only on this machine" });
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
const body = await readBody(req);
|
|
486
|
+
const name = (typeof body.name === "string" ? body.name : "").replace(/[/\\]/g, "");
|
|
487
|
+
const file = join(opts.downloadsDir, name);
|
|
488
|
+
if (!name || !file.startsWith(opts.downloadsDir) || !existsSync(file)) {
|
|
489
|
+
sendJson(res, 404, { ok: false, error: "not found" });
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
const r = await revealInFileManager(file);
|
|
493
|
+
sendJson(res, r.ok ? 200 : 500, r);
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
403
496
|
if (req.method === "GET" && url === "/api/file-download") {
|
|
404
497
|
if (!opts.downloadsDir) {
|
|
405
498
|
res.writeHead(404);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/lan",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.234",
|
|
4
4
|
"description": "Private virtual LAN for self-hosted services and AI agents, built on Elastos Carrier. NAT-traversal, name service, ACL, all over a peer-to-peer mesh — no public IP required.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|