mslxdff 0.1.22 → 0.1.24

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 CHANGED
@@ -331,6 +331,76 @@ function markJoined(entry) {
331
331
  saveGroupsJoined([...list, entry]);
332
332
  }
333
333
 
334
+ // -leavegroup | -leave-groups: leave every joined group as a member.
335
+ // Leaders cannot "leave" — they must disband with -delgroup <name>.
336
+ const leaveAll = args.includes("-leavegroup") || args.includes("--leavegroup") || args.includes("-leave-groups");
337
+ if (leaveAll) {
338
+ const peers = createPeersService({});
339
+ const joined = loadGroupsJoined();
340
+ if (!joined.length) {
341
+ console.log("not joined to any group");
342
+ process.exit(0);
343
+ }
344
+ const myToken = (await loadToken()).token;
345
+ const leaders = [];
346
+ for (const g of joined) {
347
+ if (g.leaderUrl) {
348
+ // member: deregister from the leader so we stop showing up in -group list
349
+ const peersRemoved = peers.removeByGroup(g.name);
350
+ try {
351
+ const res = await fetch(`${g.leaderUrl}/v1/groups/leave`, {
352
+ method: "POST",
353
+ headers: {
354
+ "Content-Type": "application/json",
355
+ "Authorization": `Bearer ${myToken}`,
356
+ },
357
+ body: JSON.stringify({ name: g.name }),
358
+ });
359
+ if (res.ok) console.log(`${g.name}: left (deregistered from ${g.leaderUrl})`);
360
+ else console.log(`${g.name}: left locally (leader said: ${await res.text().catch(() => `HTTP ${res.status}`)})`);
361
+ } catch (err) {
362
+ console.log(`${g.name}: left locally (leader unreachable: ${errMsg(err)})`);
363
+ }
364
+ } else {
365
+ leaders.push(g.name);
366
+ }
367
+ }
368
+ const left = joined.filter((g) => g.leaderUrl).map((g) => g.name);
369
+ saveGroupsJoined(loadGroupsJoined().filter((g) => !left.includes(g.name)));
370
+ console.log(`left ${left.length} group(s)`);
371
+ if (leaders.length) {
372
+ console.log(`\nskipped ${leaders.length} group(s) where this node is the leader:`);
373
+ for (const n of leaders) console.log(` ${n} — leaders can't leave; disband it with: mslxdff -delgroup ${n}`);
374
+ }
375
+ process.exit(0);
376
+ }
377
+
378
+ // -delgroup <name>: a leader disbands one of its groups (local groups state +
379
+ // any member records). Fails if this node is not the leader of that group.
380
+ const delGroupName = argValue("-delgroup", "--delgroup");
381
+ if (delGroupName) {
382
+ const groups = createGroupsService({});
383
+ const peers = createPeersService({});
384
+ const local = groups.list()[delGroupName];
385
+ if (!local) {
386
+ const joined0 = loadGroupsJoined().find((g) => g.name === delGroupName);
387
+ if (joined0?.leaderUrl) {
388
+ console.log(`"${delGroupName}" is led by ${joined0.leaderUrl} — you are a member, use -leavegroup to leave it`);
389
+ } else if (joined0) {
390
+ console.log(`"${delGroupName}" exists in local state but has no group definition — nothing to delete`);
391
+ } else {
392
+ console.log(`group "${delGroupName}" not found on this node`);
393
+ }
394
+ process.exit(1);
395
+ }
396
+ const disbanded = groups.delete(delGroupName);
397
+ const members = Object.values(disbanded?.members || {});
398
+ peers.removeByGroup(delGroupName);
399
+ saveGroupsJoined(loadGroupsJoined().filter((g) => g.name !== delGroupName));
400
+ console.log(`group "${delGroupName}" disbanded (${members.length} member${members.length === 1 ? "" : "s"} removed)`);
401
+ process.exit(0);
402
+ }
403
+
334
404
  function errMsg(err) { return String(err?.message || err); }
335
405
 
336
406
  // Probe a member's health endpoint concurrently (v1/health preferred,
@@ -645,6 +715,8 @@ Usage:
645
715
  mslxdff -group sync pull the freshest member list for all joined groups
646
716
  mslxdff -group leave <name> leave a group (removes its members from this node)
647
717
  mslxdff -group list list groups on this node
718
+ mslxdff -leavegroup leave every joined group as a member (leaders: use -delgroup)
719
+ mslxdff -delgroup <name> disband a group this node leads (deletes it and its members)
648
720
  mslxdff -resetban [ip] clear join-failure bans (all, or one ip)
649
721
  mslxdff -help show this help
650
722
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mslxdff",
3
- "version": "0.1.22",
3
+ "version": "0.1.24",
4
4
  "description": "测试项目,请勿使用。",
5
5
  "type": "module",
6
6
  "bin": {
package/src/groups.js CHANGED
@@ -91,6 +91,35 @@ export function listGroups({ file } = {}) {
91
91
  return loadGroups(file ? { file } : {});
92
92
  }
93
93
 
94
+ // Leader side: remove a member by exact url (a member leaving the group).
95
+ export function removeGroupMember(name, { url, file } = {}) {
96
+ const groups = loadGroups(file ? { file } : {});
97
+ const group = groups[name];
98
+ if (!group) return null;
99
+ if (!url) throw new Error("member url is required");
100
+ for (const [id, m] of Object.entries(group.members || {})) {
101
+ if (m.url === String(url).replace(/\/+$/, "")) {
102
+ delete group.members[id];
103
+ saveGroups(groups, file ? { file } : {});
104
+ return { removed: { id, url: m.url } };
105
+ }
106
+ }
107
+ return null;
108
+ }
109
+
110
+ // Leader side: disband a group entirely — deletes the group and all its members.
111
+ // Used by -leavegroup on the leader node. Also returns the member list so the
112
+ // caller can notify members before (or after) the group is gone.
113
+ export function deleteGroup(name, { file } = {}) {
114
+ const groups = loadGroups(file ? { file } : {});
115
+ const group = groups[name];
116
+ if (!group) return null;
117
+ const members = group.members || {};
118
+ delete groups[name];
119
+ saveGroups(groups, file ? { file } : {});
120
+ return { members };
121
+ }
122
+
94
123
  export function createGroupsService({ file } = {}) {
95
124
  const opts = (o = {}) => (file ? { ...o, file } : o);
96
125
  return {
@@ -99,6 +128,8 @@ export function createGroupsService({ file } = {}) {
99
128
  upsertMember: (name, o = {}) => upsertMember(name, opts(o)),
100
129
  listMembers: (name, o = {}) => listGroupMembers(name, opts(o)),
101
130
  membersForToken: (name, token) => membersForToken(name, token, opts()),
131
+ removeMember: (name, o = {}) => removeGroupMember(name, opts(o)),
132
+ delete: (name, o = {}) => deleteGroup(name, opts(o)),
102
133
  list: () => listGroups(opts()),
103
134
  };
104
135
  }
package/src/routes.js CHANGED
@@ -454,6 +454,38 @@ const ROUTES = [
454
454
  }
455
455
  },
456
456
  },
457
+ {
458
+ method: "POST",
459
+ path: "/v1/groups/leave",
460
+ requiresAuth: false,
461
+ handler: async ({ req, res, groups }) => {
462
+ if (!groups) return json(res, 501, { error: "Groups service not configured" });
463
+ let body;
464
+ try {
465
+ body = await readBody(req);
466
+ } catch {
467
+ return json(res, 400, { error: "Invalid JSON body" });
468
+ }
469
+ if (!body?.name) return json(res, 400, { error: "group name is required" });
470
+ const auth = /^Bearer (.+)$/.exec(req.headers["authorization"] || "");
471
+ if (!auth) return json(res, 401, { error: "bearer token required" });
472
+ const group = groups.list()[body.name];
473
+ if (!group) return json(res, 404, { error: `group "${body.name}" not found` });
474
+ const hit = groups.membersForToken(body.name, auth[1]);
475
+ if (!hit) return json(res, 403, { error: "invalid member token" });
476
+ try {
477
+ const removed = groups.removeMember(body.name, { url: hit.member.url });
478
+ return json(res, 200, {
479
+ object: "group",
480
+ name: body.name,
481
+ removed: removed?.removed ?? null,
482
+ members: groups.list()[body.name]?.members ?? {},
483
+ });
484
+ } catch (err) {
485
+ return json(res, 400, { error: errMsg(err) });
486
+ }
487
+ },
488
+ },
457
489
  {
458
490
  method: "GET",
459
491
  path: "/v1/models",