mslxdff 0.1.65 → 0.1.67
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/bin/mslxdff.js +3 -3159
- package/package.json +1 -1
- package/src/chat/cooling.js +99 -0
- package/src/chat/direct.js +57 -0
- package/src/chat/gateway.js +148 -0
- package/src/chat/orchestrator.js +218 -0
- package/src/chat/sse.js +69 -0
- package/src/chat/upstream.js +59 -501
- package/src/cli/bootstrap.js +1 -0
- package/src/cli/commands/daemon.js +151 -0
- package/src/cli/commands/group.js +272 -0
- package/src/cli/commands/model.js +405 -0
- package/src/cli/commands/provider/add.js +106 -0
- package/src/cli/commands/provider/allowlist.js +99 -0
- package/src/cli/commands/provider/config.js +82 -0
- package/src/cli/commands/provider/index.js +193 -0
- package/src/cli/commands/provider/keys.js +135 -0
- package/src/cli/commands/provider/models.js +99 -0
- package/src/cli/commands/provider.js +1 -0
- package/src/cli/commands/sync.js +143 -0
- package/src/cli/commands/system.js +236 -0
- package/src/cli/commands/workbuddy.js +91 -0
- package/src/cli/format.js +119 -0
- package/src/cli/group-helpers.js +69 -0
- package/src/cli/help.js +66 -0
- package/src/cli/index.js +59 -0
- package/src/cli/interactive.js +84 -0
- package/src/cli/policy.js +119 -0
- package/src/cli/provider-row.js +73 -0
- package/src/cli/status.js +283 -0
- package/src/cli/util.js +24 -0
- package/src/providers/base.js +159 -0
- package/src/providers/dispatcher.js +18 -6
- package/src/providers/generic.js +28 -166
- package/src/providers/openrouter.js +19 -205
- package/src/providers/workbuddy/auth.js +175 -0
- package/src/providers/workbuddy/balance.js +84 -0
- package/src/providers/workbuddy/chat.js +310 -0
- package/src/providers/workbuddy/index.js +263 -0
- package/src/providers/workbuddy/models.js +111 -0
- package/src/providers/workbuddy/rotation-log.js +54 -0
- package/src/providers/workbuddy.js +2 -677
- package/src/routes/chat/broadband-handler.js +25 -46
- package/src/routes/chat/exhausted-handler.js +2 -2
- package/src/routes/chat/gateway.js +301 -0
- package/src/routes/chat/hedge-handler.js +65 -83
- package/src/routes/chat/index.js +1 -384
- package/src/routes/chat/local-handler.js +32 -61
- package/src/routes/chat/peer-handler.js +32 -23
- package/src/routes/chat/relay-pipeline.js +151 -0
- package/src/runtime/bootstrap.js +408 -0
- package/src/state/facade.js +57 -0
- package/src/state/memory.js +161 -0
- package/src/state/merge.js +26 -0
- package/src/state/persist.js +42 -0
- package/src/state/provider-config.js +143 -0
- package/src/state/schemas/allowlist.js +87 -0
- package/src/state/schemas/group.js +31 -0
- package/src/state/schemas/model.js +53 -0
- package/src/state/schemas/peer.js +31 -0
- package/src/state/schemas/port.js +10 -0
- package/src/state/schemas/provider.js +204 -0
- package/src/state/schemas/token.js +43 -0
- package/src/state/store.js +176 -0
- package/src/state.js +1 -711
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { existsSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { startDaemon, stopDaemon, readPid, readPidVersion, isPidAlive, pidFile, logFile } from "../../daemon.js";
|
|
3
|
+
import { setPort } from "../../state.js";
|
|
4
|
+
import { logDir, eventsFile, callsFile, errorsFile } from "../../logs.js";
|
|
5
|
+
import { effectivePort, waitForHealth, stopDaemonIfOutdated, compareSemver, argValue } from "../policy.js";
|
|
6
|
+
import { printStatus } from "../status.js";
|
|
7
|
+
|
|
8
|
+
export async function handleStop(args) {
|
|
9
|
+
if (!(args.includes("-stop") || args.includes("--stop"))) return false;
|
|
10
|
+
const { stopped, pid, reason } = stopDaemon();
|
|
11
|
+
if (stopped) {
|
|
12
|
+
console.log(`mslxdff daemon stopped (pid ${pid})`);
|
|
13
|
+
} else {
|
|
14
|
+
console.log(`mslxdff daemon not running${reason ? ` (${reason})` : ""}`);
|
|
15
|
+
}
|
|
16
|
+
process.exit(0);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function handleRestart(args, VERSION) {
|
|
20
|
+
if (!((args.includes("-restart") || args.includes("--restart")) && !process.env.MSLXDFF_DAEMON)) return false;
|
|
21
|
+
const pid = readPid();
|
|
22
|
+
const alive = pid ? isPidAlive(pid) : false;
|
|
23
|
+
if (alive) {
|
|
24
|
+
console.log(`restarting daemon (pid ${pid})...`);
|
|
25
|
+
stopDaemon();
|
|
26
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
27
|
+
} else if (pid) {
|
|
28
|
+
console.log(`daemon pid ${pid} is stale (not running) — starting fresh...`);
|
|
29
|
+
try { stopDaemon(); } catch {}
|
|
30
|
+
} else {
|
|
31
|
+
console.log(`daemon not running — starting...`);
|
|
32
|
+
}
|
|
33
|
+
const port = effectivePort(args);
|
|
34
|
+
const spawnedPid = startDaemon([]);
|
|
35
|
+
await waitForHealth(port, 4000);
|
|
36
|
+
let ok = false;
|
|
37
|
+
try {
|
|
38
|
+
const r = await fetch(`http://127.0.0.1:${port}/health`, { signal: AbortSignal.timeout(1200) });
|
|
39
|
+
ok = r.ok;
|
|
40
|
+
} catch {}
|
|
41
|
+
if (ok) console.log(`mslxdff v${VERSION} restarted as a background daemon (pid ${spawnedPid})`);
|
|
42
|
+
else console.log(`mslxdff v${VERSION} restarted (pid ${spawnedPid}) — health check pending (http://127.0.0.1:${port}/health)`);
|
|
43
|
+
console.log(`endpoint: http://localhost:${port}/v1`);
|
|
44
|
+
console.log(`log: ${logFile()}`);
|
|
45
|
+
console.log(`pid: ${pidFile()}`);
|
|
46
|
+
process.exit(0);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function handlePort(args) {
|
|
50
|
+
const portArg = argValue(args, "-port", "--port");
|
|
51
|
+
if (!portArg || process.env.MSLXDFF_DAEMON) return false;
|
|
52
|
+
const port = Number(portArg);
|
|
53
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
54
|
+
console.error(`invalid port: ${portArg}`);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
setPort(port);
|
|
58
|
+
const daemon = readPid();
|
|
59
|
+
if (daemon) {
|
|
60
|
+
stopDaemon();
|
|
61
|
+
startDaemon(["-port", String(port)]);
|
|
62
|
+
await waitForHealth(port, 4000);
|
|
63
|
+
console.log(`mslxdff restarted on port ${port} (pid ${readPid()})`);
|
|
64
|
+
console.log(`endpoint: http://localhost:${port}/v1`);
|
|
65
|
+
} else {
|
|
66
|
+
console.log(`port saved: ${port} (daemon not running; takes effect on next start)`);
|
|
67
|
+
}
|
|
68
|
+
process.exit(0);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function handleDaemonFlag(args, VERSION) {
|
|
72
|
+
if (!(args.includes("-d") || args.includes("--daemon"))) return false;
|
|
73
|
+
if (!process.env.MSLXDFF_DAEMON) {
|
|
74
|
+
const port = effectivePort(args);
|
|
75
|
+
stopDaemonIfOutdated(VERSION);
|
|
76
|
+
const keptPid = readPid();
|
|
77
|
+
if (keptPid && isPidAlive(keptPid)) {
|
|
78
|
+
const rv = readPidVersion();
|
|
79
|
+
if (rv && compareSemver(rv, VERSION) > 0) {
|
|
80
|
+
await printStatus(VERSION);
|
|
81
|
+
console.log(`daemon already running newer v${rv} — not starting v${VERSION}`);
|
|
82
|
+
process.exit(0);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const spawnedPid = startDaemon(args.filter((a) => a !== "-d" && a !== "--daemon"));
|
|
86
|
+
await waitForHealth(port, 4000);
|
|
87
|
+
console.log(`mslxdff daemon started (pid ${spawnedPid})`);
|
|
88
|
+
console.log(`log: ${logFile()}`);
|
|
89
|
+
console.log(`pid: ${pidFile()}`);
|
|
90
|
+
process.exit(0);
|
|
91
|
+
}
|
|
92
|
+
return false; // we ARE the daemon — fall through to bootstrap
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function handleDebug(args) {
|
|
96
|
+
if (!(args.includes("-debug") || args.includes("--debug"))) return false;
|
|
97
|
+
const { stopped, pid } = stopDaemon();
|
|
98
|
+
if (stopped) console.log(`[debug] stopped background daemon (pid ${pid})`);
|
|
99
|
+
try {
|
|
100
|
+
const dir = logDir();
|
|
101
|
+
const toClear = [eventsFile(), callsFile(), errorsFile(), logFile()];
|
|
102
|
+
let cleared = 0;
|
|
103
|
+
for (const f of toClear) {
|
|
104
|
+
try {
|
|
105
|
+
if (existsSync(f)) {
|
|
106
|
+
writeFileSync(f, "");
|
|
107
|
+
cleared++;
|
|
108
|
+
}
|
|
109
|
+
} catch {}
|
|
110
|
+
}
|
|
111
|
+
console.log(`[debug] 已清理旧日志 ${cleared} 个文件 (${dir}),本次会话干净输出`);
|
|
112
|
+
} catch {}
|
|
113
|
+
console.log("--- live (Ctrl+C: stop debugging and restore background daemon) ---");
|
|
114
|
+
process.env.MSLXDFF_DEBUG = "1";
|
|
115
|
+
process.env.MSLXDFF_DAEMON = "1";
|
|
116
|
+
return false; // fall through to daemon body
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function handleBareRun(args, VERSION) {
|
|
120
|
+
if (process.env.MSLXDFF_DAEMON) return false;
|
|
121
|
+
const pid = readPid();
|
|
122
|
+
if (pid && isPidAlive(pid)) {
|
|
123
|
+
const rv = readPidVersion();
|
|
124
|
+
if (rv && compareSemver(rv, VERSION) >= 0) {
|
|
125
|
+
await printStatus(VERSION);
|
|
126
|
+
const { printHelp } = await import("../help.js");
|
|
127
|
+
printHelp(VERSION);
|
|
128
|
+
process.exit(0);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const port = effectivePort(args);
|
|
132
|
+
stopDaemonIfOutdated(VERSION);
|
|
133
|
+
const pid2 = readPid();
|
|
134
|
+
if (pid2 && isPidAlive(pid2)) {
|
|
135
|
+
const rv2 = readPidVersion();
|
|
136
|
+
if (rv2 && compareSemver(rv2, VERSION) >= 0) {
|
|
137
|
+
await printStatus(VERSION);
|
|
138
|
+
const { printHelp } = await import("../help.js");
|
|
139
|
+
printHelp(VERSION);
|
|
140
|
+
process.exit(0);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const spawnedPid = startDaemon([]);
|
|
144
|
+
await waitForHealth(port, 4000);
|
|
145
|
+
console.log(`mslxdff v${VERSION} started as a background daemon (pid ${spawnedPid})`);
|
|
146
|
+
console.log(`endpoint: http://localhost:${port}/v1`);
|
|
147
|
+
console.log(`log: ${logFile()}`);
|
|
148
|
+
console.log(`pid: ${pidFile()}`);
|
|
149
|
+
console.log(`status: run \`mslxdff\` again (or \`mslxdff -status\`)`);
|
|
150
|
+
process.exit(0);
|
|
151
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { createGroupsService, createBansService, refreshGroupMembers, syncPeersFromMembers } from "../../groups.js";
|
|
2
|
+
import { createPeersService } from "../../peers.js";
|
|
3
|
+
import { loadToken, loadGroupsJoined, saveGroupsJoined } from "../../state.js";
|
|
4
|
+
import { groupIs, markJoined, probeHealth, syncAllJoinedGroups } from "../group-helpers.js";
|
|
5
|
+
import { errMsg } from "../util.js";
|
|
6
|
+
import { argValue } from "../policy.js";
|
|
7
|
+
|
|
8
|
+
export async function handleGroupCreate(args) {
|
|
9
|
+
const createGroupArg = argValue(args, "-creategroup", "--creategroup") || groupIs("create", args);
|
|
10
|
+
if (!createGroupArg) return false;
|
|
11
|
+
const groups = createGroupsService({});
|
|
12
|
+
const peers = createPeersService({});
|
|
13
|
+
const name = createGroupArg;
|
|
14
|
+
const { created } = groups.create(name);
|
|
15
|
+
markJoined({ name, leaderUrl: "", myUrl: "", memberName: "leader" });
|
|
16
|
+
const synced = await syncAllJoinedGroups({ peers, groups });
|
|
17
|
+
const s = synced.find((x) => x.name === name);
|
|
18
|
+
console.log(created ? `group created: ${name}` : `group already exists: ${name}`);
|
|
19
|
+
console.log(`members on this node: ${s ? `${s.total} (failover: ${s.added})` : "?"}`);
|
|
20
|
+
console.log(`others join with: mslxdff -addtogroup <this-node-host> ${name}`);
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function handleGroupCommand(args) {
|
|
25
|
+
const groupCmd = argValue(args, "-group", "--group");
|
|
26
|
+
if (!groupCmd || groupCmd === "create") return false;
|
|
27
|
+
const groups = createGroupsService({});
|
|
28
|
+
const peers = createPeersService({});
|
|
29
|
+
const rest = args.slice(args.indexOf("-group") + 1);
|
|
30
|
+
const [action, a] = rest;
|
|
31
|
+
if (action === "sync") {
|
|
32
|
+
const synced = await syncAllJoinedGroups({ peers, groups });
|
|
33
|
+
if (!synced.length) {
|
|
34
|
+
console.log("not joined to any group (use -addtogroup or -creategroup)");
|
|
35
|
+
} else {
|
|
36
|
+
for (const s of synced) {
|
|
37
|
+
if (s.error) console.log(`${s.name}: sync failed — ${s.error}`);
|
|
38
|
+
else console.log(`${s.name}: ${s.total} member(s), ${s.added} failover target(s) configured`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
} else if (action === "leave" && a) {
|
|
42
|
+
const before = loadGroupsJoined().filter((g) => g.name === a).length;
|
|
43
|
+
saveGroupsJoined(loadGroupsJoined().filter((g) => g.name !== a));
|
|
44
|
+
const removed = peers.removeByGroup(a);
|
|
45
|
+
console.log(before ? `left group "${a}" (${removed} member(s) removed)` : `not a member of group "${a}"`);
|
|
46
|
+
} else if (action === "list") {
|
|
47
|
+
const joinedList = loadGroupsJoined();
|
|
48
|
+
if (!joinedList.length) {
|
|
49
|
+
console.log("no groups on this node");
|
|
50
|
+
process.exit(0);
|
|
51
|
+
}
|
|
52
|
+
const { token } = await loadToken();
|
|
53
|
+
const fetchImpl = (url, opts) => fetch(url, { ...opts, signal: AbortSignal.timeout(1500) });
|
|
54
|
+
for (const g of joinedList) {
|
|
55
|
+
const isLeader = !g.leaderUrl;
|
|
56
|
+
let members;
|
|
57
|
+
if (isLeader) {
|
|
58
|
+
members = groups.list()[g.name]?.members || {};
|
|
59
|
+
} else {
|
|
60
|
+
try {
|
|
61
|
+
members = await refreshGroupMembers(g.name, {
|
|
62
|
+
leaderUrl: g.leaderUrl,
|
|
63
|
+
memberName: g.memberName,
|
|
64
|
+
url: g.myUrl,
|
|
65
|
+
token,
|
|
66
|
+
fetchImpl,
|
|
67
|
+
});
|
|
68
|
+
} catch (err) {
|
|
69
|
+
console.log(`${g.name} (members unavailable — leader unreachable: ${errMsg(err)})`);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const entries = Object.entries(members).filter(([id]) => id !== "leader");
|
|
74
|
+
console.log(`${g.name} (${entries.length} member${entries.length === 1 ? "" : "s"})`);
|
|
75
|
+
const probes = await Promise.all(
|
|
76
|
+
entries.map(([id, m]) => probeHealth({ id, url: m?.url || id, kind: m?.kind, lastSeen: m?.lastSeen, publicIp: m?.publicIp }))
|
|
77
|
+
);
|
|
78
|
+
const leaderEntry = members.leader ? Object.entries(members).find(([id]) => id === "leader") : null;
|
|
79
|
+
const leaderProbe = leaderEntry ? await probeHealth({ id: "leader", url: leaderEntry[1].url }) : null;
|
|
80
|
+
const display = leaderProbe ? [...probes, leaderProbe] : probes;
|
|
81
|
+
let seq = 0;
|
|
82
|
+
for (const r of display) {
|
|
83
|
+
const m = entries.find(([eid]) => eid === r.id)?.[1] || (r.id === "leader" ? leaderEntry?.[1] : null);
|
|
84
|
+
const isBb = r.kind === "broadband" || String(r.url || "").startsWith("relay://") || m?.kind === "broadband";
|
|
85
|
+
if (isBb) {
|
|
86
|
+
const ago = r.lastSeen ? `${Math.round((Date.now() - r.lastSeen) / 1000)}s ago` : "no heartbeat yet";
|
|
87
|
+
const ip = r.publicIp || m?.publicIp || "?";
|
|
88
|
+
const via = r.stale ? "stale" : `via leader ${ago}`;
|
|
89
|
+
const stateBb = `${via} ip=${ip}`;
|
|
90
|
+
if (r.id === "leader") {
|
|
91
|
+
console.log(` leader ${r.url} ${stateBb}`);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
seq += 1;
|
|
95
|
+
const label = r.id && r.id !== r.url ? ` [${r.id}]` : "";
|
|
96
|
+
console.log(` ${seq}. ${r.url}${label} [broadband] ${stateBb}`);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const state = r.fail ? `fail ${r.fail}` : `ok ${r.ms}ms`;
|
|
100
|
+
if (r.id === "leader") {
|
|
101
|
+
console.log(` leader ${r.url} ${state}`);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
seq += 1;
|
|
105
|
+
const label = r.id && r.id !== r.url ? ` [${r.id}]` : "";
|
|
106
|
+
console.log(` ${seq}. ${r.url}${label} ${state}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
console.log(`\njoined groups (${joinedList.length}):`);
|
|
110
|
+
for (const g of joinedList) console.log(` ${g.name} ${g.leaderUrl || "(this node is the leader)"}`);
|
|
111
|
+
} else if (action === "remove" && a) {
|
|
112
|
+
const seq = Number(a);
|
|
113
|
+
if (!Number.isInteger(seq) || seq < 1) {
|
|
114
|
+
console.error(`usage: mslxdff -group remove <seq>`);
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
const joined = loadGroupsJoined().find((g) => !g.leaderUrl);
|
|
118
|
+
if (!joined) {
|
|
119
|
+
console.error("group remove requires being the leader — this node leads no group");
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
const members = groups.list()[joined.name]?.members || {};
|
|
123
|
+
const entries = Object.entries(members).filter(([id]) => id !== "leader");
|
|
124
|
+
const target = entries[seq - 1];
|
|
125
|
+
if (!target) {
|
|
126
|
+
console.error(`member #${seq} not found — group "${joined.name}" has ${entries.length} member(s)`);
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
const [id, m] = target;
|
|
130
|
+
try {
|
|
131
|
+
const removed = groups.removeMember(joined.name, { url: m.url });
|
|
132
|
+
if (removed) console.log(`removed ${m.url} from "${joined.name}"`);
|
|
133
|
+
else console.log(`member ${m.url} already gone from "${joined.name}"`);
|
|
134
|
+
} catch (err) {
|
|
135
|
+
console.error(`remove failed: ${errMsg(err)}`);
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
} else {
|
|
139
|
+
console.error("usage: mslxdff -group sync | -group leave <name> | -group list | -group remove <seq> | -creategroup <name>");
|
|
140
|
+
}
|
|
141
|
+
process.exit(0);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function handleAddToGroup(args) {
|
|
145
|
+
const addToGroupIdx = args.findIndex((x) => x === "-addtogroup" || x === "--addtogroup");
|
|
146
|
+
if (addToGroupIdx < 0) return false;
|
|
147
|
+
const rawArgs = args.slice(addToGroupIdx + 1);
|
|
148
|
+
const isBroadband = rawArgs.includes("--broadband");
|
|
149
|
+
const filtered = rawArgs.filter((a) => a !== "--broadband");
|
|
150
|
+
const [leaderHost, name] = filtered;
|
|
151
|
+
if (!leaderHost || !name || filtered.length > 2) {
|
|
152
|
+
console.error("usage: mslxdff -addtogroup <leader-host> <name> [--broadband]");
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
const groups = createGroupsService({});
|
|
156
|
+
const peers = createPeersService({});
|
|
157
|
+
const myToken = (await loadToken()).token;
|
|
158
|
+
const leaderUrl = leaderHost.includes("://")
|
|
159
|
+
? leaderHost.replace(/\/+$/, "")
|
|
160
|
+
: `http://${leaderHost}${leaderHost.includes(":") ? "" : ":8989"}`;
|
|
161
|
+
const kind = isBroadband ? "broadband" : "static";
|
|
162
|
+
let joinBody;
|
|
163
|
+
if (isBroadband) {
|
|
164
|
+
const relayId = `relay://${myToken.slice(0, 8)}`;
|
|
165
|
+
joinBody = { name, key: name, leaderUrl, url: relayId, token: myToken, kind: "broadband" };
|
|
166
|
+
} else {
|
|
167
|
+
const { effectivePort } = await import("../policy.js");
|
|
168
|
+
const myPort = effectivePort(args);
|
|
169
|
+
joinBody = { name, key: name, leaderUrl, myPort, token: myToken, kind: "static" };
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
const res = await fetch(`${leaderUrl}/v1/groups/join`, {
|
|
173
|
+
method: "POST",
|
|
174
|
+
headers: { "Content-Type": "application/json" },
|
|
175
|
+
body: JSON.stringify(joinBody),
|
|
176
|
+
});
|
|
177
|
+
if (!res.ok) {
|
|
178
|
+
const text = await res.text().catch(() => "");
|
|
179
|
+
throw new Error(`join failed (HTTP ${res.status}): ${text}`);
|
|
180
|
+
}
|
|
181
|
+
const data = await res.json();
|
|
182
|
+
const myUrl = data.you?.url || joinBody.url || "";
|
|
183
|
+
const memberName = isBroadband ? myUrl : myUrl;
|
|
184
|
+
markJoined({ name, leaderUrl, myUrl, memberName, kind });
|
|
185
|
+
const synced = await syncAllJoinedGroups({ peers, groups });
|
|
186
|
+
const s = synced.find((x) => x.name === name);
|
|
187
|
+
console.log(`joined group "${name}" at ${leaderUrl}${isBroadband ? " [broadband]" : ""}`);
|
|
188
|
+
if (s?.error) console.log(` local failover setup failed: ${s.error}`);
|
|
189
|
+
else console.log(` ${s?.added ?? 0} failover target(s) configured${isBroadband ? " (broadband via leader, local 127.0.0.1)" : ""}`);
|
|
190
|
+
} catch (err) {
|
|
191
|
+
console.error(`join failed: ${err.message}`);
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
process.exit(0);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export async function handleResetBan(args) {
|
|
198
|
+
const resetBanArg = argValue(args, "-resetban", "--resetban");
|
|
199
|
+
if (resetBanArg === null && !args.includes("-resetban") && !args.includes("--resetban")) return false;
|
|
200
|
+
const bans = createBansService({});
|
|
201
|
+
const ip = resetBanArg || null;
|
|
202
|
+
bans.clear(ip || undefined);
|
|
203
|
+
console.log(ip ? `ban cleared for ${ip}` : "all bans cleared");
|
|
204
|
+
process.exit(0);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export async function handleLeaveGroup(args) {
|
|
208
|
+
const leaveAll = args.includes("-leavegroup") || args.includes("--leavegroup") || args.includes("-leave-groups");
|
|
209
|
+
if (!leaveAll) return false;
|
|
210
|
+
const peers = createPeersService({});
|
|
211
|
+
const joined = loadGroupsJoined();
|
|
212
|
+
if (!joined.length) {
|
|
213
|
+
console.log("not joined to any group");
|
|
214
|
+
process.exit(0);
|
|
215
|
+
}
|
|
216
|
+
const myToken = (await loadToken()).token;
|
|
217
|
+
const leaders = [];
|
|
218
|
+
for (const g of joined) {
|
|
219
|
+
if (g.leaderUrl) {
|
|
220
|
+
const peersRemoved = peers.removeByGroup(g.name);
|
|
221
|
+
try {
|
|
222
|
+
const res = await fetch(`${g.leaderUrl}/v1/groups/leave`, {
|
|
223
|
+
method: "POST",
|
|
224
|
+
headers: {
|
|
225
|
+
"Content-Type": "application/json",
|
|
226
|
+
"Authorization": `Bearer ${myToken}`,
|
|
227
|
+
},
|
|
228
|
+
body: JSON.stringify({ name: g.name }),
|
|
229
|
+
});
|
|
230
|
+
if (res.ok) console.log(`${g.name}: left (deregistered from ${g.leaderUrl})`);
|
|
231
|
+
else console.log(`${g.name}: left locally (leader said: ${await res.text().catch(() => `HTTP ${res.status}`)})`);
|
|
232
|
+
} catch (err) {
|
|
233
|
+
console.log(`${g.name}: left locally (leader unreachable: ${errMsg(err)})`);
|
|
234
|
+
}
|
|
235
|
+
} else {
|
|
236
|
+
leaders.push(g.name);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
const left = joined.filter((g) => g.leaderUrl).map((g) => g.name);
|
|
240
|
+
saveGroupsJoined(loadGroupsJoined().filter((g) => !left.includes(g.name)));
|
|
241
|
+
console.log(`left ${left.length} group(s)`);
|
|
242
|
+
if (leaders.length) {
|
|
243
|
+
console.log(`\nskipped ${leaders.length} group(s) where this node is the leader:`);
|
|
244
|
+
for (const n of leaders) console.log(` ${n} — leaders can't leave; disband it with: mslxdff -delgroup ${n}`);
|
|
245
|
+
}
|
|
246
|
+
process.exit(0);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export async function handleDelGroup(args) {
|
|
250
|
+
const delGroupName = argValue(args, "-delgroup", "--delgroup");
|
|
251
|
+
if (!delGroupName) return false;
|
|
252
|
+
const groups = createGroupsService({});
|
|
253
|
+
const peers = createPeersService({});
|
|
254
|
+
const local = groups.list()[delGroupName];
|
|
255
|
+
if (!local) {
|
|
256
|
+
const joined0 = loadGroupsJoined().find((g) => g.name === delGroupName);
|
|
257
|
+
if (joined0?.leaderUrl) {
|
|
258
|
+
console.log(`"${delGroupName}" is led by ${joined0.leaderUrl} — you are a member, use -leavegroup to leave it`);
|
|
259
|
+
} else if (joined0) {
|
|
260
|
+
console.log(`"${delGroupName}" exists in local state but has no group definition — nothing to delete`);
|
|
261
|
+
} else {
|
|
262
|
+
console.log(`group "${delGroupName}" not found on this node`);
|
|
263
|
+
}
|
|
264
|
+
process.exit(1);
|
|
265
|
+
}
|
|
266
|
+
const disbanded = groups.delete(delGroupName);
|
|
267
|
+
const members = Object.values(disbanded?.members || {});
|
|
268
|
+
peers.removeByGroup(delGroupName);
|
|
269
|
+
saveGroupsJoined(loadGroupsJoined().filter((g) => g.name !== delGroupName));
|
|
270
|
+
console.log(`group "${delGroupName}" disbanded (${members.length} member${members.length === 1 ? "" : "s"} removed)`);
|
|
271
|
+
process.exit(0);
|
|
272
|
+
}
|