@decentnetwork/beagle 0.1.111 → 0.1.112
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.js +1 -0
- package/dist/desktop/app.js +107 -21
- package/dist/server.d.ts +4 -0
- package/dist/server.js +104 -0
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -172,6 +172,7 @@ async function main() {
|
|
|
172
172
|
doraRosterPath,
|
|
173
173
|
downloadsDir: resolve(args.configDir, "downloads"),
|
|
174
174
|
messagesPath: resolve(args.configDir, "messages.json"),
|
|
175
|
+
servicePeersPath: resolve(args.configDir, "service-peers-local.json"),
|
|
175
176
|
callIce: {
|
|
176
177
|
keyFile: resolve(decentlanCarrierDir(args.configDir), "keypair.json"),
|
|
177
178
|
bootstrapNodes,
|
package/dist/desktop/app.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
window.__DK_UI_VERSION="0.1.
|
|
1
|
+
window.__DK_UI_VERSION="0.1.112";
|
|
2
2
|
"use strict";
|
|
3
3
|
(() => {
|
|
4
4
|
// packages/beagle-ui/dist/beagle-ui.js
|
|
@@ -644,6 +644,18 @@ window.__DK_UI_VERSION="0.1.111";
|
|
|
644
644
|
confirm: "list",
|
|
645
645
|
scope: "assistant"
|
|
646
646
|
},
|
|
647
|
+
{
|
|
648
|
+
name: "nodes-refresh",
|
|
649
|
+
usage: "/nodes refresh",
|
|
650
|
+
summary: "Look for friends that are provably service peers (the shipped exit table, this machine's dora roster) and offer to reclassify them. Nothing changes until you tick and confirm.",
|
|
651
|
+
params: {
|
|
652
|
+
type: "object",
|
|
653
|
+
properties: {},
|
|
654
|
+
required: []
|
|
655
|
+
},
|
|
656
|
+
confirm: "list",
|
|
657
|
+
scope: "assistant"
|
|
658
|
+
},
|
|
647
659
|
{
|
|
648
660
|
name: "find",
|
|
649
661
|
usage: "/find <text>",
|
|
@@ -818,7 +830,8 @@ window.__DK_UI_VERSION="0.1.111";
|
|
|
818
830
|
file: "service-peers.json",
|
|
819
831
|
loader: "loadServicePeers()",
|
|
820
832
|
rule: "A peer is a service peer if and only if its userid is in this list. The Assistant never infers it from a name or from having no messages. /friends excludes them entirely; /nodes is only them.",
|
|
821
|
-
later: "A dora / ENS / bridge / directory service returns the same shape; the bundled file is v1."
|
|
833
|
+
later: "A dora / ENS / bridge / directory service returns the same shape; the bundled file is v1.",
|
|
834
|
+
refresh: "/nodes refresh asks GET /api/assistant/service-candidates for evidence-backed candidates and writes the ticked ones to this machine's overlay (POST /api/assistant/service-peers, stored at <configDir>/service-peers-local.json). The shipped list is never edited at runtime; the overlay wins on the same userid."
|
|
822
835
|
}
|
|
823
836
|
};
|
|
824
837
|
var service_peers_default = {
|
|
@@ -973,6 +986,8 @@ window.__DK_UI_VERSION="0.1.111";
|
|
|
973
986
|
const sub = (parts[1] || "").toLowerCase();
|
|
974
987
|
if (sub === "list" || sub === "remove")
|
|
975
988
|
return { ok: true, name: `${head}-${sub}`, args: parts.slice(2) };
|
|
989
|
+
if (head === "nodes" && sub === "refresh")
|
|
990
|
+
return { ok: true, name: "nodes-refresh", args: parts.slice(2) };
|
|
976
991
|
return { ok: false, name: head, args: parts.slice(1), error: `${head}-subcommand` };
|
|
977
992
|
}
|
|
978
993
|
return { ok: true, name: head, args: parts.slice(1) };
|
|
@@ -1010,6 +1025,19 @@ window.__DK_UI_VERSION="0.1.111";
|
|
|
1010
1025
|
}
|
|
1011
1026
|
return map;
|
|
1012
1027
|
}
|
|
1028
|
+
function mergeServicePeers(bundled, local) {
|
|
1029
|
+
const byId = /* @__PURE__ */ new Map();
|
|
1030
|
+
for (const p of bundled && bundled.peers || [])
|
|
1031
|
+
if (p && p.userid)
|
|
1032
|
+
byId.set(p.userid, { ...p, source: "app" });
|
|
1033
|
+
for (const p of local && local.peers || [])
|
|
1034
|
+
if (p && p.userid)
|
|
1035
|
+
byId.set(p.userid, { ...p, source: "local" });
|
|
1036
|
+
return { version: 1, source: "merged", peers: [...byId.values()] };
|
|
1037
|
+
}
|
|
1038
|
+
function newCandidates(candidates, svcMap) {
|
|
1039
|
+
return (candidates != null ? candidates : []).filter((c) => c && c.userid && !(svcMap && svcMap.has(c.userid)));
|
|
1040
|
+
}
|
|
1013
1041
|
function partitionPeers(friends, svcMap) {
|
|
1014
1042
|
const people = [];
|
|
1015
1043
|
const nodes = [];
|
|
@@ -1092,8 +1120,13 @@ window.__DK_UI_VERSION="0.1.111";
|
|
|
1092
1120
|
return { match: partial[0], candidates: partial };
|
|
1093
1121
|
return { match: null, candidates: partial };
|
|
1094
1122
|
}
|
|
1095
|
-
function loadServicePeers() {
|
|
1096
|
-
|
|
1123
|
+
async function loadServicePeers() {
|
|
1124
|
+
try {
|
|
1125
|
+
const local = await fetch("/api/assistant/service-peers").then((r) => r.json());
|
|
1126
|
+
return mergeServicePeers(service_peers_default, local);
|
|
1127
|
+
} catch (e) {
|
|
1128
|
+
return service_peers_default;
|
|
1129
|
+
}
|
|
1097
1130
|
}
|
|
1098
1131
|
var ASSISTANT_PEER = {
|
|
1099
1132
|
id: ASSISTANT_ID,
|
|
@@ -1149,10 +1182,12 @@ window.__DK_UI_VERSION="0.1.111";
|
|
|
1149
1182
|
const tail = lang === "zh" ? `\u7B5B\u9009\u6761\u4EF6\uFF08\u53EF\u7EC4\u5408\uFF0C\u53D6\u5E76\u96C6\uFF09\uFF1A
|
|
1150
1183
|
${filters}
|
|
1151
1184
|
|
|
1152
|
-
|
|
1185
|
+
\`/rename\`\u3001\`/mute\`\u3001\`/find\` \u5BF9\u597D\u53CB\u548C\u670D\u52A1\u8282\u70B9\u90FD\u9002\u7528\uFF1B\u53EA\u6709\u5217\u8868\u548C\u5220\u9664\u5206\u6210\u4E24\u8FB9\u3002
|
|
1186
|
+
\u6700\u8FD1 ${RECENT_DAYS} \u5929\u5185\u6DFB\u52A0\u7684\u597D\u53CB\u4E0D\u4F1A\u88AB"\u4ECE\u672A/\u6C89\u9ED8"\u7C7B\u7B5B\u9009\u9009\u4E2D\uFF0C\u5E76\u4F1A\u5217\u51FA\u88AB\u8DF3\u8FC7\u7684\u540D\u5B57\u3002\u5220\u9664\u6C38\u8FDC\u9700\u8981\u5148\u786E\u8BA4\uFF0C\u53D6\u6D88\u52FE\u9009\u5373\u53EF\u4FDD\u7559\u3002` : `Filters (combine them; a friend matching any one is listed):
|
|
1153
1187
|
${filters}
|
|
1154
1188
|
|
|
1155
|
-
|
|
1189
|
+
\`/rename\`, \`/mute\` and \`/find\` work on friends and service peers alike \u2014 only listing and removing are split in two.
|
|
1190
|
+
Friends added in the last ${RECENT_DAYS} days are never matched by the "never/silent" filters, and the ones held back are named. Removal always asks first: untick anyone you want to keep.`;
|
|
1156
1191
|
return `${intro}
|
|
1157
1192
|
${cmds}
|
|
1158
1193
|
|
|
@@ -1166,8 +1201,9 @@ ${tail}`;
|
|
|
1166
1201
|
time: (/* @__PURE__ */ new Date()).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }),
|
|
1167
1202
|
text
|
|
1168
1203
|
}, extra);
|
|
1169
|
-
function AssistantPanel({ m, T, lang, onRemove }) {
|
|
1204
|
+
function AssistantPanel({ m, T, lang, onRemove, onAdopt }) {
|
|
1170
1205
|
const rows = m.assistant.rows || [];
|
|
1206
|
+
const adopt = m.assistant.kind === "adopt";
|
|
1171
1207
|
const [sel, setSel] = React.useState(() => new Set(rows.map((r) => r.userid)));
|
|
1172
1208
|
const [busy, setBusy] = React.useState(false);
|
|
1173
1209
|
const [done, setDone] = React.useState(null);
|
|
@@ -1178,7 +1214,7 @@ ${tail}`;
|
|
|
1178
1214
|
n.has(id) ? n.delete(id) : n.add(id);
|
|
1179
1215
|
return n;
|
|
1180
1216
|
});
|
|
1181
|
-
const label = lang === "zh" ? `\u5220\u9664 ${sel.size} \u4F4D` : `Remove ${sel.size}`;
|
|
1217
|
+
const label = adopt ? lang === "zh" ? `\u52A0\u5165 ${sel.size} \u4E2A` : `Add ${sel.size} as ${sel.size === 1 ? "a node" : "nodes"}` : lang === "zh" ? `\u5220\u9664 ${sel.size} \u4F4D` : `Remove ${sel.size}`;
|
|
1182
1218
|
return /* @__PURE__ */ React.createElement("div", { style: { marginTop: 8, border: "1px solid var(--line)", borderRadius: 10, overflow: "hidden" } }, rows.map((r) => /* @__PURE__ */ React.createElement("label", { key: r.userid, style: {
|
|
1183
1219
|
display: "flex",
|
|
1184
1220
|
alignItems: "center",
|
|
@@ -1195,12 +1231,14 @@ ${tail}`;
|
|
|
1195
1231
|
disabled: !!done || busy,
|
|
1196
1232
|
onChange: () => toggle(r.userid)
|
|
1197
1233
|
}
|
|
1198
|
-
), /* @__PURE__ */ React.createElement("span", { style: { flex: 1 } }, nameOf(r)), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11, color: "var(--faint)" } }, r.userid.slice(0, 8), "\u2026"))), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 10, padding: "8px 10px" } }, done ? /* @__PURE__ */ React.createElement("span", { style: { fontSize: 12.5, color: "var(--faint)" } }, done) : /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Btn, { size: "sm", tone: "danger", disabled: busy || !sel.size, onClick: () => {
|
|
1234
|
+
), /* @__PURE__ */ React.createElement("span", { style: { flex: 1 } }, nameOf(r), r.why && /* @__PURE__ */ React.createElement("span", { style: { color: "var(--faint)", fontSize: 11.5 } }, ` \u2014 ${r.why}`)), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11, color: "var(--faint)" } }, r.userid.slice(0, 8), "\u2026"))), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 10, padding: "8px 10px" } }, done ? /* @__PURE__ */ React.createElement("span", { style: { fontSize: 12.5, color: "var(--faint)" } }, done) : /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Btn, { size: "sm", tone: adopt ? "solid" : "danger", disabled: busy || !sel.size, onClick: () => {
|
|
1199
1235
|
setBusy(true);
|
|
1200
|
-
|
|
1201
|
-
|
|
1236
|
+
const picked = rows.filter((r) => sel.has(r.userid));
|
|
1237
|
+
const run = adopt ? onAdopt(picked) : onRemove([...sel]);
|
|
1238
|
+
run.then((r) => {
|
|
1239
|
+
setDone(adopt ? lang === "zh" ? `\u5DF2\u52A0\u5165 ${r.added} \u4E2A\u670D\u52A1\u8282\u70B9` : `Added ${r.added} as service peers` : lang === "zh" ? `\u5DF2\u5220\u9664 ${r.removed} \u4F4D${r.failed ? `\uFF0C${r.failed} \u4F4D\u5931\u8D25` : ""}` : `Removed ${r.removed}${r.failed ? `, ${r.failed} failed` : ""}`);
|
|
1202
1240
|
}).finally(() => setBusy(false));
|
|
1203
|
-
} }, busy ? T.working || "Working\u2026" : label), /* @__PURE__ */ React.createElement("span", { style: { fontSize: 12, color: "var(--faint)" } }, lang === "zh" ? "\u53D6\u6D88\u52FE\u9009\u53EF\u4FDD\u7559" : "Untick anyone you want to keep"))));
|
|
1241
|
+
} }, busy ? T.working || "Working\u2026" : label), /* @__PURE__ */ React.createElement("span", { style: { fontSize: 12, color: "var(--faint)" } }, adopt ? lang === "zh" ? "\u53D6\u6D88\u52FE\u9009\u5373\u4FDD\u6301\u4E3A\u597D\u53CB" : "Untick any you want to keep as a friend" : lang === "zh" ? "\u53D6\u6D88\u52FE\u9009\u53EF\u4FDD\u7559" : "Untick anyone you want to keep"))));
|
|
1204
1242
|
}
|
|
1205
1243
|
function useAssistant({ lang, T, muted, onToggleMute, sound, flash, setAlert, onReloadFriends }) {
|
|
1206
1244
|
const [thread, setThread] = React.useState(() => [{ day: (/* @__PURE__ */ new Date()).toLocaleDateString() }, asMsg("them", helpText(lang))]);
|
|
@@ -1210,11 +1248,13 @@ ${tail}`;
|
|
|
1210
1248
|
}, [lang]);
|
|
1211
1249
|
const fetchFriends = async () => {
|
|
1212
1250
|
const r = await fetch("/api/assistant/friends").then((x) => x.json());
|
|
1213
|
-
const
|
|
1251
|
+
const svcMap = servicePeerMap(await loadServicePeers());
|
|
1252
|
+
const { people, nodes } = partitionPeers(r.friends || [], svcMap);
|
|
1214
1253
|
return {
|
|
1215
1254
|
friends: people,
|
|
1216
1255
|
nodes,
|
|
1217
1256
|
all: r.friends || [],
|
|
1257
|
+
svcMap,
|
|
1218
1258
|
historySince: r.historySince || null,
|
|
1219
1259
|
now: r.now || Date.now(),
|
|
1220
1260
|
ver: r.beagleVer || ""
|
|
@@ -1281,6 +1321,33 @@ ${cands.map((c) => `- ${nameOf(c)}`).join("\n")}`;
|
|
|
1281
1321
|
- Waiting to be accepted: ${pending}
|
|
1282
1322
|
- History on this device since: ${fmtDate(d.historySince, lang)}
|
|
1283
1323
|
- Version: ${d.ver}`));
|
|
1324
|
+
}
|
|
1325
|
+
case "nodes-refresh": {
|
|
1326
|
+
const d = await fetchFriends();
|
|
1327
|
+
const r = await fetch("/api/assistant/service-candidates").then((x) => x.json());
|
|
1328
|
+
const fresh = newCandidates(r.candidates || [], d.svcMap);
|
|
1329
|
+
const src = r.sources || {};
|
|
1330
|
+
const where = lang === "zh" ? `\u67E5\u8BE2\u6765\u6E90\uFF1A\u5185\u7F6E\u51FA\u53E3\u8868\uFF08${src.exits || 0} \u6761\uFF09${src.dora == null ? "\uFF0C\u672C\u673A\u4E0D\u662F dora" : `\uFF0C\u672C\u673A dora \u540D\u518C\uFF08${src.dora} \u6761\uFF09`}` : `Looked at: the shipped exit table (${src.exits || 0})${src.dora == null ? ", and this machine is not a dora" : `, and this machine's dora roster (${src.dora})`}`;
|
|
1331
|
+
if (!fresh.length) {
|
|
1332
|
+
return append(asMsg("them", lang === "zh" ? `**\u6CA1\u6709\u65B0\u7684\u670D\u52A1\u8282\u70B9**
|
|
1333
|
+
|
|
1334
|
+
\u73B0\u6709 ${d.nodes.length} \u4E2A\u5DF2\u8BC6\u522B\u3002${where}\u3002
|
|
1335
|
+
|
|
1336
|
+
*\u53EA\u91C7\u7528\u8BC1\u636E\uFF0C\u4E0D\u9760\u540D\u5B57\u731C\u6D4B\u3002*` : `**Nothing new**
|
|
1337
|
+
|
|
1338
|
+
${d.nodes.length} service peers are already classified. ${where}.
|
|
1339
|
+
|
|
1340
|
+
*Evidence only \u2014 names are never guessed from.*`));
|
|
1341
|
+
}
|
|
1342
|
+
const title = lang === "zh" ? `**\u53D1\u73B0 ${fresh.length} \u4E2A\u53EF\u4EE5\u6807\u4E3A\u670D\u52A1\u8282\u70B9\u7684\u597D\u53CB** \u2014 \u52FE\u9009\u540E\u624D\u4F1A\u751F\u6548
|
|
1343
|
+
|
|
1344
|
+
${where}\u3002` : `**${fresh.length === 1 ? "1 friend looks" : `${fresh.length} friends look`} like service peers** \u2014 nothing changes until you tick and confirm
|
|
1345
|
+
|
|
1346
|
+
${where}.`;
|
|
1347
|
+
const lines = fresh.map((c) => `- **${c.name}** \`${c.kind}${c.role ? " \xB7 " + c.role : ""}\` \u2014 ${c.why}`).join("\n");
|
|
1348
|
+
return append(asMsg("them", `${title}
|
|
1349
|
+
|
|
1350
|
+
${lines}`, { assistant: { kind: "adopt", rows: fresh } }));
|
|
1284
1351
|
}
|
|
1285
1352
|
case "nodes-list":
|
|
1286
1353
|
case "friends-list": {
|
|
@@ -1370,6 +1437,24 @@ ${cands.map((c) => `- ${nameOf(c)}`).join("\n")}`;
|
|
|
1370
1437
|
return append(asMsg("them", lang === "zh" ? `\u51FA\u9519\u4E86\uFF1A${e && e.message || e}` : `That failed: ${e && e.message || e}`));
|
|
1371
1438
|
}
|
|
1372
1439
|
}, [append, lang, muted, onToggleMute, setAlert, onReloadFriends]);
|
|
1440
|
+
const adoptServicePeers = React.useCallback(async (rows) => {
|
|
1441
|
+
const add = rows.map((r) => ({ userid: r.userid, name: r.name, kind: r.kind, role: r.role }));
|
|
1442
|
+
let added = 0;
|
|
1443
|
+
try {
|
|
1444
|
+
const r = await fetch("/api/assistant/service-peers", {
|
|
1445
|
+
method: "POST",
|
|
1446
|
+
headers: { "content-type": "application/json" },
|
|
1447
|
+
body: JSON.stringify({ add })
|
|
1448
|
+
}).then((x) => x.json());
|
|
1449
|
+
added = r && r.ok ? add.length : 0;
|
|
1450
|
+
} catch (e) {
|
|
1451
|
+
added = 0;
|
|
1452
|
+
}
|
|
1453
|
+
if (onReloadFriends)
|
|
1454
|
+
onReloadFriends();
|
|
1455
|
+
append(asMsg("them", added ? lang === "zh" ? `\u5DF2\u628A ${added} \u4F4D\u6807\u4E3A\u670D\u52A1\u8282\u70B9\uFF0C\u5B83\u4EEC\u4E0D\u518D\u51FA\u73B0\u5728 \`/friends\` \u4E2D\u3002` : `${added} now count as service peers \u2014 they no longer appear in \`/friends\`.` : lang === "zh" ? "\u5199\u5165\u5931\u8D25\uFF0C\u6CA1\u6709\u4EFB\u4F55\u6539\u52A8\u3002" : "That write failed; nothing changed."));
|
|
1456
|
+
return { added };
|
|
1457
|
+
}, [append, lang, onReloadFriends]);
|
|
1373
1458
|
const removeFriends = React.useCallback(async (ids) => {
|
|
1374
1459
|
let removed = 0;
|
|
1375
1460
|
const failed = [];
|
|
@@ -1389,7 +1474,7 @@ ${cands.map((c) => `- ${nameOf(c)}`).join("\n")}`;
|
|
|
1389
1474
|
append(asMsg("them", failed.length ? lang === "zh" ? `\u5DF2\u5220\u9664 ${removed} \u4F4D\uFF0C${failed.length} \u4F4D\u5931\u8D25\u3002` : `Removed ${removed}, ${failed.length} failed.` : lang === "zh" ? `\u5DF2\u5220\u9664 ${removed} \u4F4D\u597D\u53CB\u3002` : `Removed ${removed} friend${removed === 1 ? "" : "s"}.`));
|
|
1390
1475
|
return { removed, failed: failed.length };
|
|
1391
1476
|
}, [append, lang, onReloadFriends]);
|
|
1392
|
-
return { thread, handle, removeFriends };
|
|
1477
|
+
return { thread, handle, removeFriends, adoptServicePeers };
|
|
1393
1478
|
}
|
|
1394
1479
|
var DK_CMD_HIST_KEY = "beagle.assistant.history";
|
|
1395
1480
|
var DK_CMD_HIST_MAX = 50;
|
|
@@ -2196,7 +2281,7 @@ ${cands.map((c) => `- ${nameOf(c)}`).join("\n")}`;
|
|
|
2196
2281
|
return null;
|
|
2197
2282
|
}), err && /* @__PURE__ */ React.createElement("div", { style: { color: "var(--danger, #ff6b6b)", fontSize: 11.5, fontFamily: "var(--mono)" } }, err));
|
|
2198
2283
|
}
|
|
2199
|
-
function Msg({ m, peer, T, lang, onTheater, onDelete, onCancel, onRetry, onRetryMsg, onReveal, onOpenFile, onCall, onFormSubmit, onNameCard, onAssistantRemove, answered, selMode, selected, onToggleSel, busy, isGroup }) {
|
|
2284
|
+
function Msg({ m, peer, T, lang, onTheater, onDelete, onCancel, onRetry, onRetryMsg, onReveal, onOpenFile, onCall, onFormSubmit, onNameCard, onAssistantRemove, onAssistantAdopt, answered, selMode, selected, onToggleSel, busy, isGroup }) {
|
|
2200
2285
|
const mine = m.from === "me";
|
|
2201
2286
|
const nativeMedia = !m.file ? dkNativeMediaFromText(m.text) : null;
|
|
2202
2287
|
const grp = isGroup && m.dir !== "out" && !m.file && !nativeMedia ? dkGroupParse(m.text) : null;
|
|
@@ -2525,7 +2610,7 @@ ${cands.map((c) => `- ${nameOf(c)}`).join("\n")}`;
|
|
|
2525
2610
|
letterSpacing: -0.1,
|
|
2526
2611
|
wordBreak: "break-word"
|
|
2527
2612
|
} }, /* @__PURE__ */ React.createElement(MarkdownText, { text: read.text, onNameCard })));
|
|
2528
|
-
})(), read.form && /* @__PURE__ */ React.createElement("div", { style: { marginTop: 6 } }, /* @__PURE__ */ React.createElement(DkChatForm, { form: read.form, submitted, peer, onSubmit: onFormSubmit })), m.assistant && onAssistantRemove && /* @__PURE__ */ React.createElement(AssistantPanel, { m, T, lang, onRemove: onAssistantRemove }), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 4, margin: "3px 3px 0" } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 10, color: "var(--faint)" } }, m.time), (() => {
|
|
2613
|
+
})(), read.form && /* @__PURE__ */ React.createElement("div", { style: { marginTop: 6 } }, /* @__PURE__ */ React.createElement(DkChatForm, { form: read.form, submitted, peer, onSubmit: onFormSubmit })), m.assistant && onAssistantRemove && /* @__PURE__ */ React.createElement(AssistantPanel, { m, T, lang, onRemove: onAssistantRemove, onAdopt: onAssistantAdopt }), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 4, margin: "3px 3px 0" } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 10, color: "var(--faint)" } }, m.time), (() => {
|
|
2529
2614
|
const sentDone = mine && m.status && m.status !== "queued" && m.status !== "sending" && m.status !== "failed";
|
|
2530
2615
|
const via = !mine ? m.via : sentDone ? m.via === "offline" ? "offline" : "online" : null;
|
|
2531
2616
|
if (!via)
|
|
@@ -2570,7 +2655,7 @@ ${cands.map((c) => `- ${nameOf(c)}`).join("\n")}`;
|
|
|
2570
2655
|
) : mine && m.via === "offline" && m.status !== "queued" && m.status !== "sending" ? /* @__PURE__ */ React.createElement(Icon, { name: "check", size: 12, stroke: 2.2, color: "#f59e0b", title: T.viaOutOffline || "posted to the offline relay \u2014 they get it when they come online" }) : mine && m.status === "sent" && m.confirmed === false ? /* @__PURE__ */ React.createElement(Icon, { name: "check", size: 12, stroke: 2.2, color: "var(--faint)", title: T.sentUnconfirmed || "sent \u2014 not confirmed by the peer" }) : mine && m.status && /* @__PURE__ */ React.createElement(Icon, { name: "checkCheck", size: 12, stroke: 2.2, color: m.status === "read" ? "var(--accent)" : "var(--faint)" })))
|
|
2571
2656
|
);
|
|
2572
2657
|
}
|
|
2573
|
-
function Conversation({ T, peer, lang, peers, onOpenChat, thread: threadProp, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, hasEarlier, onLoadEarlier, onAssistantRemove, muted, onToggleMute, phone, onBack }) {
|
|
2658
|
+
function Conversation({ T, peer, lang, peers, onOpenChat, thread: threadProp, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, hasEarlier, onLoadEarlier, onAssistantRemove, onAssistantAdopt, muted, onToggleMute, phone, onBack }) {
|
|
2574
2659
|
const scrollRef = React.useRef(null);
|
|
2575
2660
|
const prependRef = React.useRef(null);
|
|
2576
2661
|
const [earlierBusy, setEarlierBusy] = React.useState(false);
|
|
@@ -3003,6 +3088,7 @@ ${peer.address}`
|
|
|
3003
3088
|
isGroup,
|
|
3004
3089
|
onTheater: setTheater,
|
|
3005
3090
|
onAssistantRemove,
|
|
3091
|
+
onAssistantAdopt,
|
|
3006
3092
|
onDelete: (id) => doDelete([id]),
|
|
3007
3093
|
onCancel: doCancel,
|
|
3008
3094
|
onRetry: doRetry,
|
|
@@ -3162,12 +3248,12 @@ ${peer.address}`
|
|
|
3162
3248
|
function ChatEmpty({ T }) {
|
|
3163
3249
|
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));
|
|
3164
3250
|
}
|
|
3165
|
-
function ChatTab({ T, lang, peers, requests, activeId, thread, onSelect, onAct, onAdd, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, hasEarlier, onLoadEarlier, onAssistantRemove, prefillAddr, onPrefillConsumed, muted, onToggleMute, phone, onBack }) {
|
|
3251
|
+
function ChatTab({ T, lang, peers, requests, activeId, thread, onSelect, onAct, onAdd, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, hasEarlier, onLoadEarlier, onAssistantRemove, onAssistantAdopt, prefillAddr, onPrefillConsumed, muted, onToggleMute, phone, onBack }) {
|
|
3166
3252
|
const peer = peers.find((p) => p.id === activeId);
|
|
3167
3253
|
if (phone) {
|
|
3168
|
-
return peer ? /* @__PURE__ */ React.createElement(Conversation, { T, peer, lang, peers, onOpenChat: onSelect, thread, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, hasEarlier, onLoadEarlier, onAssistantRemove, muted: !!muted && muted.includes(peer.id), onToggleMute, phone: true, onBack }) : /* @__PURE__ */ React.createElement(PeerSidebar, { T, peers, requests, activeId, onSelect, onAct, onAdd, prefillAddr, onPrefillConsumed, muted, wide: true });
|
|
3254
|
+
return peer ? /* @__PURE__ */ React.createElement(Conversation, { T, peer, lang, peers, onOpenChat: onSelect, thread, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, hasEarlier, onLoadEarlier, onAssistantRemove, onAssistantAdopt, muted: !!muted && muted.includes(peer.id), onToggleMute, phone: true, onBack }) : /* @__PURE__ */ React.createElement(PeerSidebar, { T, peers, requests, activeId, onSelect, onAct, onAdd, prefillAddr, onPrefillConsumed, muted, wide: true });
|
|
3169
3255
|
}
|
|
3170
|
-
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, muted }), peer ? /* @__PURE__ */ React.createElement(Conversation, { T, peer, lang, peers, onOpenChat: onSelect, thread, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, hasEarlier, onLoadEarlier, onAssistantRemove, muted: !!muted && muted.includes(peer.id), onToggleMute }) : /* @__PURE__ */ React.createElement(ChatEmpty, { T }));
|
|
3256
|
+
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, muted }), peer ? /* @__PURE__ */ React.createElement(Conversation, { T, peer, lang, peers, onOpenChat: onSelect, thread, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, hasEarlier, onLoadEarlier, onAssistantRemove, onAssistantAdopt, muted: !!muted && muted.includes(peer.id), onToggleMute }) : /* @__PURE__ */ React.createElement(ChatEmpty, { T }));
|
|
3171
3257
|
}
|
|
3172
3258
|
Object.assign(window, { ChatTab });
|
|
3173
3259
|
function CopyBtn({ value, copiedText = "Copied", copyFailedText = "Copy failed", copyTitle = "Copy" }) {
|
|
@@ -6764,7 +6850,7 @@ ${peer.address}`
|
|
|
6764
6850
|
fontWeight: 600,
|
|
6765
6851
|
background: tab === n.id ? "var(--accent)" : "transparent",
|
|
6766
6852
|
color: tab === n.id ? "#fff" : "var(--dim)"
|
|
6767
|
-
} }, /* @__PURE__ */ React.createElement(Icon, { name: n.icon, size: 14, stroke: 2, color: tab === n.id ? "#fff" : "var(--dim)" }), n.label))), tab === "chat" && /* @__PURE__ */ React.createElement(ChatTab, { T, lang: t.lang, phone, onBack: closeConversation, peers, requests, activeId, thread: activeId === ASSISTANT_ID ? assistant.thread : data.threads[activeId], onSelect, onAct, onAdd, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread: () => activeId && data.loadThread(activeId), hasEarlier: !!(activeId && activeId !== ASSISTANT_ID && data.earlier[activeId]), onAssistantRemove: activeId === ASSISTANT_ID ? assistant.removeFriends : null, onLoadEarlier: () => activeId && data.loadEarlier(activeId), prefillAddr: pendingAddr, onPrefillConsumed: () => setPendingAddr(""), muted: mutedIds, onToggleMute }), tab === "here" && /* @__PURE__ */ React.createElement(DiscoverTab, { T, kind: "bridge", peers, meId: me.userId, onAdd, onOpenChat }), tab === "recommended" && /* @__PURE__ */ React.createElement(DiscoverTab, { T, kind: "recommended", peers, meId: me.userId, onAdd, onOpenChat }), tab === "registered" && /* @__PURE__ */ React.createElement(DiscoverTab, { T, kind: "registered", peers, meId: me.userId, onAdd, onOpenChat }), tab === "network" && /* @__PURE__ */ React.createElement(NetworkTab, { T, me, peers, exits, activeExit, reqCount: requests.length, onSetExit, onOpenChat, backend, onArmLan: armLan, onCancelLan: cancelLan }), tab === "apps" && /* @__PURE__ */ React.createElement(AppsTab, { T, t }), tab === "profile" && /* @__PURE__ */ React.createElement(ProfileTab, { T, me, onEdit, t, setTweak, onSetAvatar: (p) => dkApi.setProfileFull(p).then((r) => {
|
|
6853
|
+
} }, /* @__PURE__ */ React.createElement(Icon, { name: n.icon, size: 14, stroke: 2, color: tab === n.id ? "#fff" : "var(--dim)" }), n.label))), tab === "chat" && /* @__PURE__ */ React.createElement(ChatTab, { T, lang: t.lang, phone, onBack: closeConversation, peers, requests, activeId, thread: activeId === ASSISTANT_ID ? assistant.thread : data.threads[activeId], onSelect, onAct, onAdd, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread: () => activeId && data.loadThread(activeId), hasEarlier: !!(activeId && activeId !== ASSISTANT_ID && data.earlier[activeId]), onAssistantRemove: activeId === ASSISTANT_ID ? assistant.removeFriends : null, onAssistantAdopt: activeId === ASSISTANT_ID ? assistant.adoptServicePeers : null, onLoadEarlier: () => activeId && data.loadEarlier(activeId), prefillAddr: pendingAddr, onPrefillConsumed: () => setPendingAddr(""), muted: mutedIds, onToggleMute }), tab === "here" && /* @__PURE__ */ React.createElement(DiscoverTab, { T, kind: "bridge", peers, meId: me.userId, onAdd, onOpenChat }), tab === "recommended" && /* @__PURE__ */ React.createElement(DiscoverTab, { T, kind: "recommended", peers, meId: me.userId, onAdd, onOpenChat }), tab === "registered" && /* @__PURE__ */ React.createElement(DiscoverTab, { T, kind: "registered", peers, meId: me.userId, onAdd, onOpenChat }), tab === "network" && /* @__PURE__ */ React.createElement(NetworkTab, { T, me, peers, exits, activeExit, reqCount: requests.length, onSetExit, onOpenChat, backend, onArmLan: armLan, onCancelLan: cancelLan }), tab === "apps" && /* @__PURE__ */ React.createElement(AppsTab, { T, t }), tab === "profile" && /* @__PURE__ */ React.createElement(ProfileTab, { T, me, onEdit, t, setTweak, onSetAvatar: (p) => dkApi.setProfileFull(p).then((r) => {
|
|
6768
6854
|
data.refresh();
|
|
6769
6855
|
return r;
|
|
6770
6856
|
}) })), phone && !inConversation && /* @__PURE__ */ React.createElement("div", { style: { flexShrink: 0, display: "flex", borderTop: "1px solid var(--line)", background: "var(--rail)", paddingBottom: "env(safe-area-inset-bottom, 0px)" } }, phoneNav.map((n) => /* @__PURE__ */ React.createElement(TabBarBtn, { key: n.id, icon: n.icon, label: n.label, active: n.active, badge: n.badge, onClick: n.go }))), data.locked && /* @__PURE__ */ React.createElement(
|
package/dist/server.d.ts
CHANGED
|
@@ -28,6 +28,10 @@ export interface BeagleServerOptions {
|
|
|
28
28
|
* (never written) by GET /api/assistant/friends to count a friend's
|
|
29
29
|
* history without hauling it over IPC. Undefined = no counts. */
|
|
30
30
|
messagesPath?: string;
|
|
31
|
+
/** <configDir>/service-peers-local.json — peers THIS machine has adopted as
|
|
32
|
+
* service peers on top of the list shipped in the app. Written only by the
|
|
33
|
+
* Assistant's `/nodes refresh`, and only after the user ticks the rows. */
|
|
34
|
+
servicePeersPath?: string;
|
|
31
35
|
/** The swappable backend, so the UI can show which one is live and drive the
|
|
32
36
|
* LAN handoff. Optional: a caller that pins one backend just omits it and
|
|
33
37
|
* the network panel falls back to "no virtual LAN here". */
|
package/dist/server.js
CHANGED
|
@@ -121,6 +121,21 @@ function friendStoreStamps(keyFile) {
|
|
|
121
121
|
return friendStampsCache?.map ?? empty;
|
|
122
122
|
}
|
|
123
123
|
}
|
|
124
|
+
/** The machine-local service-peer overlay. Absent file = nothing adopted yet,
|
|
125
|
+
* which is the normal state, not an error. */
|
|
126
|
+
function readLocalServicePeers(path) {
|
|
127
|
+
const empty = { version: 1, source: "local", peers: [] };
|
|
128
|
+
if (!path || !existsSync(path))
|
|
129
|
+
return empty;
|
|
130
|
+
try {
|
|
131
|
+
const raw = JSON.parse(readFileSync(path, "utf-8"));
|
|
132
|
+
const peers = Array.isArray(raw?.peers) ? raw.peers.filter((p) => p && typeof p.userid === "string") : [];
|
|
133
|
+
return { version: 1, source: "local", updated: raw?.updated, peers };
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return empty;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
124
139
|
/** The record beagle writes for its own name: what the profile page shows
|
|
125
140
|
* (displayName, description) on top of whatever the record already held —
|
|
126
141
|
* wallet binds, socials, avatar, referee — so a profile edit never strips a
|
|
@@ -2238,6 +2253,95 @@ export function startBeagleServer(opts) {
|
|
|
2238
2253
|
sendJson(res, 200, { me, peers, requests, exits, activeExit });
|
|
2239
2254
|
return;
|
|
2240
2255
|
}
|
|
2256
|
+
// The locally adopted service peers: the app ships a list, and this is
|
|
2257
|
+
// what this machine added to it. Kept server-side rather than in a tab's
|
|
2258
|
+
// localStorage so every browser on this machine agrees about what is a
|
|
2259
|
+
// node — the classification decides what `/friends remove` can touch.
|
|
2260
|
+
if (req.method === "GET" && url === "/api/assistant/service-peers") {
|
|
2261
|
+
sendJson(res, 200, readLocalServicePeers(opts.servicePeersPath));
|
|
2262
|
+
return;
|
|
2263
|
+
}
|
|
2264
|
+
if (req.method === "POST" && url === "/api/assistant/service-peers") {
|
|
2265
|
+
const body = (await readBody(req));
|
|
2266
|
+
if (!opts.servicePeersPath)
|
|
2267
|
+
return sendJson(res, 400, { ok: false, error: "no config dir" });
|
|
2268
|
+
const current = readLocalServicePeers(opts.servicePeersPath);
|
|
2269
|
+
const byId = new Map(current.peers.map((p) => [p.userid, p]));
|
|
2270
|
+
for (const raw of Array.isArray(body.add) ? body.add : []) {
|
|
2271
|
+
const p = raw;
|
|
2272
|
+
const userid = typeof p?.userid === "string" ? p.userid : "";
|
|
2273
|
+
if (!userid)
|
|
2274
|
+
continue;
|
|
2275
|
+
byId.set(userid, {
|
|
2276
|
+
userid,
|
|
2277
|
+
name: String(p.name ?? userid).slice(0, 120),
|
|
2278
|
+
kind: p.kind === "agent" ? "agent" : "node",
|
|
2279
|
+
role: String(p.role ?? "").slice(0, 120),
|
|
2280
|
+
});
|
|
2281
|
+
}
|
|
2282
|
+
for (const raw of Array.isArray(body.remove) ? body.remove : []) {
|
|
2283
|
+
if (typeof raw === "string")
|
|
2284
|
+
byId.delete(raw);
|
|
2285
|
+
}
|
|
2286
|
+
const next = { version: 1, source: "local", updated: new Date().toISOString().slice(0, 10), peers: [...byId.values()] };
|
|
2287
|
+
try {
|
|
2288
|
+
writeFileSync(opts.servicePeersPath, JSON.stringify(next, null, 2) + "\n");
|
|
2289
|
+
}
|
|
2290
|
+
catch (e) {
|
|
2291
|
+
return sendJson(res, 500, { ok: false, error: e.message });
|
|
2292
|
+
}
|
|
2293
|
+
sendJson(res, 200, { ok: true, ...next });
|
|
2294
|
+
return;
|
|
2295
|
+
}
|
|
2296
|
+
// `/nodes refresh`: which friends does this device have EVIDENCE for?
|
|
2297
|
+
// Evidence only — the shipped exit table and, on a dora, its roster.
|
|
2298
|
+
// Nothing here reads a name or guesses from silence, and nothing is
|
|
2299
|
+
// adopted without the user ticking it.
|
|
2300
|
+
if (req.method === "GET" && url === "/api/assistant/service-candidates") {
|
|
2301
|
+
const flist = await opts.call({ op: "friends-list" });
|
|
2302
|
+
const fl = flist.ok ? (flist.data?.friends ?? []) : [];
|
|
2303
|
+
const evidence = new Map();
|
|
2304
|
+
for (const e of DEFAULT_EXITS) {
|
|
2305
|
+
// Role, not region: the exit table's regions carry place names this
|
|
2306
|
+
// app does not put in front of users (see CLAUDE.md), and "which
|
|
2307
|
+
// exit" is not what this answer is for.
|
|
2308
|
+
if (e.userid)
|
|
2309
|
+
evidence.set(e.userid, { kind: "node", role: "exit", why: "in the shipped exit table" });
|
|
2310
|
+
}
|
|
2311
|
+
let doraCount = 0;
|
|
2312
|
+
if (opts.doraRosterPath && existsSync(opts.doraRosterPath)) {
|
|
2313
|
+
try {
|
|
2314
|
+
const r = yaml.load(readFileSync(opts.doraRosterPath, "utf-8"));
|
|
2315
|
+
for (const rec of r?.records ?? []) {
|
|
2316
|
+
const uid = rec.userid || rec.pubkey;
|
|
2317
|
+
if (!uid)
|
|
2318
|
+
continue;
|
|
2319
|
+
doraCount++;
|
|
2320
|
+
evidence.set(uid, { kind: "node", role: rec.ip ? `dora member (${rec.ip})` : "dora member", why: "allocated in this machine's dora roster" });
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
catch { /* a broken roster is not an error here */ }
|
|
2324
|
+
}
|
|
2325
|
+
const candidates = fl
|
|
2326
|
+
.map((f) => ({ f, ev: evidence.get(f.userid ?? "") }))
|
|
2327
|
+
.filter((x) => x.ev)
|
|
2328
|
+
.map(({ f, ev }) => ({
|
|
2329
|
+
userid: f.userid,
|
|
2330
|
+
name: f.alias || (typeof f.name === "string" ? f.name : "") || f.userid,
|
|
2331
|
+
kind: ev.kind,
|
|
2332
|
+
role: ev.role,
|
|
2333
|
+
why: ev.why,
|
|
2334
|
+
}));
|
|
2335
|
+
sendJson(res, 200, {
|
|
2336
|
+
ok: true,
|
|
2337
|
+
candidates,
|
|
2338
|
+
sources: {
|
|
2339
|
+
exits: DEFAULT_EXITS.length,
|
|
2340
|
+
dora: opts.doraRosterPath ? doraCount : null,
|
|
2341
|
+
},
|
|
2342
|
+
});
|
|
2343
|
+
return;
|
|
2344
|
+
}
|
|
2241
2345
|
// Everything the Assistant's friend filters need, in one answer: the
|
|
2242
2346
|
// friend list plus how much was said to each friend and when. The
|
|
2243
2347
|
// counts come from the store file in our own config dir — see
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/beagle",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.112",
|
|
4
4
|
"description": "Beagle — P2P chat, file transfer and calls for regular users, on the Decent Network. No admin privilege required.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"yargs": "^17.7.2"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"@decentnetwork/beagle-ui": "^0.2.
|
|
38
|
+
"@decentnetwork/beagle-ui": "^0.2.26",
|
|
39
39
|
"@types/js-yaml": "^4.0.9",
|
|
40
40
|
"@types/node": "^20.11.0",
|
|
41
41
|
"@types/yargs": "^17.0.32",
|