mslxdff 0.1.21 → 0.1.23

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,49 @@ function markJoined(entry) {
331
331
  saveGroupsJoined([...list, entry]);
332
332
  }
333
333
 
334
+ // -leavegroup | -leave-groups: leave every joined group. Members deregister
335
+ // from their leader; a leader disbands the group (deletes it entirely).
336
+ const leaveAll = args.includes("-leavegroup") || args.includes("--leavegroup") || args.includes("-leave-groups");
337
+ if (leaveAll) {
338
+ const groups = createGroupsService({});
339
+ const peers = createPeersService({});
340
+ const joined = loadGroupsJoined();
341
+ if (!joined.length) {
342
+ console.log("not joined to any group");
343
+ process.exit(0);
344
+ }
345
+ const myToken = (await loadToken()).token;
346
+ for (const g of joined) {
347
+ const peersRemoved = peers.removeByGroup(g.name);
348
+ if (g.leaderUrl) {
349
+ // member: deregister from the leader so we stop showing up in -group list
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
+ // leader: disband the whole group
366
+ const disbanded = groups.delete(g.name);
367
+ const members = Object.values(disbanded?.members || {});
368
+ console.log(`${g.name}: disbanded (${members.length} member${members.length === 1 ? "" : "s"} removed)`);
369
+ }
370
+ }
371
+ const left = joined.map((g) => g.name);
372
+ saveGroupsJoined(loadGroupsJoined().filter((g) => !left.includes(g.name)));
373
+ console.log(`left ${left.length} group(s)`);
374
+ process.exit(0);
375
+ }
376
+
334
377
  function errMsg(err) { return String(err?.message || err); }
335
378
 
336
379
  // Probe a member's health endpoint concurrently (v1/health preferred,
@@ -645,6 +688,7 @@ Usage:
645
688
  mslxdff -group sync pull the freshest member list for all joined groups
646
689
  mslxdff -group leave <name> leave a group (removes its members from this node)
647
690
  mslxdff -group list list groups on this node
691
+ mslxdff -leavegroup leave every group (members deregister, a leader disbands)
648
692
  mslxdff -resetban [ip] clear join-failure bans (all, or one ip)
649
693
  mslxdff -help show this help
650
694
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mslxdff",
3
- "version": "0.1.21",
3
+ "version": "0.1.23",
4
4
  "description": "测试项目,请勿使用。",
5
5
  "type": "module",
6
6
  "bin": {
package/src/groups.js CHANGED
@@ -27,6 +27,20 @@ export function createGroup(name, { file, key = name } = {}) {
27
27
  return { name, key, created: true };
28
28
  }
29
29
 
30
+ // Shared writer: register a member, de-duplicated by url (same ip:port
31
+ // re-registering under a different name updates the existing entry instead of
32
+ // creating a duplicate).
33
+ function writeMember(group, memberName, url, token) {
34
+ const members = group.members || (group.members = {});
35
+ const byUrl = Object.entries(members).find(([, m]) => m.url === url);
36
+ if (byUrl) {
37
+ members[byUrl[0]] = { url, token: token || "" };
38
+ return;
39
+ }
40
+ const id = memberName || url;
41
+ members[id] = { url, token: token || "" };
42
+ }
43
+
30
44
  // Leader side: add a member after verifying the join key.
31
45
  export function addGroupMember(name, { key, memberName, url, token, file } = {}) {
32
46
  const groups = loadGroups(file ? { file } : {});
@@ -34,8 +48,7 @@ export function addGroupMember(name, { key, memberName, url, token, file } = {})
34
48
  if (!group) throw new Error(`group "${name}" not found on this node`);
35
49
  if (!verifyGroupKey(key, group.key)) throw new Error("invalid group key");
36
50
  if (!url) throw new Error("member url is required");
37
- const id = memberName || url;
38
- group.members[id] = { url, token: token || "" };
51
+ writeMember(group, memberName, url, token);
39
52
  saveGroups(groups, file ? { file } : {});
40
53
  return group.members;
41
54
  }
@@ -47,8 +60,7 @@ export function upsertMember(name, { memberName, url, token, file } = {}) {
47
60
  const group = groups[name];
48
61
  if (!group) return null;
49
62
  if (!url) throw new Error("member url is required");
50
- const id = memberName || url;
51
- group.members[id] = { url, token: token || "" };
63
+ writeMember(group, memberName, url, token);
52
64
  saveGroups(groups, file ? { file } : {});
53
65
  return group.members;
54
66
  }
@@ -79,6 +91,35 @@ export function listGroups({ file } = {}) {
79
91
  return loadGroups(file ? { file } : {});
80
92
  }
81
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
+
82
123
  export function createGroupsService({ file } = {}) {
83
124
  const opts = (o = {}) => (file ? { ...o, file } : o);
84
125
  return {
@@ -87,6 +128,8 @@ export function createGroupsService({ file } = {}) {
87
128
  upsertMember: (name, o = {}) => upsertMember(name, opts(o)),
88
129
  listMembers: (name, o = {}) => listGroupMembers(name, opts(o)),
89
130
  membersForToken: (name, token) => membersForToken(name, token, opts()),
131
+ removeMember: (name, o = {}) => removeGroupMember(name, opts(o)),
132
+ delete: (name, o = {}) => deleteGroup(name, opts(o)),
90
133
  list: () => listGroups(opts()),
91
134
  };
92
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",