mslxdff 0.1.231 → 0.1.233

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.
Files changed (117) hide show
  1. package/bin/mslxdff.js +3 -3
  2. package/package.json +1 -1
  3. package/src/auto.js +264 -254
  4. package/src/bench/cline-bench.js +42 -42
  5. package/src/bench/probe.js +70 -70
  6. package/src/bench/report.js +162 -162
  7. package/src/bench/runner.js +77 -77
  8. package/src/bench/via-probe.js +124 -124
  9. package/src/bench/via-routes.js +87 -87
  10. package/src/bench/workbuddy-bench.js +54 -54
  11. package/src/chat/engine.js +160 -160
  12. package/src/chat/gateway.js +163 -163
  13. package/src/chat/orchestrator.js +234 -234
  14. package/src/chat/prompt.js +70 -70
  15. package/src/chat/repl.js +88 -88
  16. package/src/chat/terminal.js +135 -135
  17. package/src/chat/tools.js +306 -306
  18. package/src/chat-pipeline/index.js +155 -145
  19. package/src/chat-pipeline/policy.js +84 -84
  20. package/src/cli/commands/group.js +266 -266
  21. package/src/cli/commands/model/picks.js +50 -50
  22. package/src/cli/commands/provider/bench-via.js +247 -247
  23. package/src/cli/commands/provider/bench.js +141 -141
  24. package/src/cli/commands/provider/index.js +139 -139
  25. package/src/cli/commands/provider/models.js +149 -149
  26. package/src/cli/commands/provider/qwenwork-login.js +119 -119
  27. package/src/cli/commands/provider/raccoon-quota.js +156 -156
  28. package/src/cli/commands/sync.js +310 -310
  29. package/src/cli/status.js +279 -279
  30. package/src/daemon.js +246 -246
  31. package/src/model-capabilities/enrich.js +86 -86
  32. package/src/model-capabilities/index.js +183 -183
  33. package/src/model-capabilities/parse.js +70 -70
  34. package/src/model-trace.js +10 -2
  35. package/src/models.js +292 -225
  36. package/src/peers.js +345 -345
  37. package/src/providers/agentrouter.js +238 -238
  38. package/src/providers/base.js +164 -164
  39. package/src/providers/cline/auth.js +228 -228
  40. package/src/providers/cline/chat.js +307 -307
  41. package/src/providers/cline.js +2 -2
  42. package/src/providers/codearts/chat.js +207 -207
  43. package/src/providers/dispatcher.js +147 -147
  44. package/src/providers/globalqwenwork/cosy.js +123 -123
  45. package/src/providers/globalqwenwork/crypto.js +218 -218
  46. package/src/providers/globalqwenwork/http.js +20 -20
  47. package/src/providers/globalqwenwork/rsa.js +54 -54
  48. package/src/providers/globalqwenwork/sse.js +268 -268
  49. package/src/providers/globalqwenwork/stream.js +130 -130
  50. package/src/providers/globalqwenwork/upstream.js +122 -122
  51. package/src/providers/keyring.js +60 -60
  52. package/src/providers/qoder/account-store.js +18 -0
  53. package/src/providers/qoder/chat.js +292 -203
  54. package/src/providers/qoder/checkin.js +25 -12
  55. package/src/providers/qoder/index.js +311 -249
  56. package/src/providers/qoder/models.js +64 -2
  57. package/src/providers/qoder/oauth.js +42 -1
  58. package/src/providers/qoder/payload.js +116 -80
  59. package/src/providers/qoder/request.js +7 -2
  60. package/src/providers/qoder/session.js +26 -10
  61. package/src/providers/qoder/sse.js +233 -145
  62. package/src/providers/qoder/sticky.js +5 -2
  63. package/src/providers/qoder/wire-messages.js +242 -0
  64. package/src/providers/qwenwork/account-store.js +133 -133
  65. package/src/providers/qwenwork/constants.js +67 -67
  66. package/src/providers/qwenwork/cosy.js +120 -120
  67. package/src/providers/qwenwork/crypto.js +218 -218
  68. package/src/providers/qwenwork/http.js +20 -20
  69. package/src/providers/qwenwork/index.js +337 -337
  70. package/src/providers/qwenwork/payload.js +144 -144
  71. package/src/providers/qwenwork/rsa.js +54 -54
  72. package/src/providers/qwenwork/sse.js +268 -268
  73. package/src/providers/qwenwork/stream.js +130 -130
  74. package/src/providers/qwenwork/upstream.js +120 -120
  75. package/src/providers/qwenwork.js +1 -1
  76. package/src/providers/raccoon/chat.js +1 -1
  77. package/src/providers/raccoon/const.js +219 -202
  78. package/src/providers/raccoon/credits.js +285 -285
  79. package/src/providers/raccoon/index.js +15 -2
  80. package/src/providers/raccoon/models.js +68 -17
  81. package/src/providers/registry.js +89 -89
  82. package/src/providers/workbuddy/auth.js +16 -0
  83. package/src/providers/workbuddy/chat.js +298 -264
  84. package/src/providers/workbuddy/client-headers.js +148 -0
  85. package/src/providers/workbuddy/desktop-info.js +17 -1
  86. package/src/providers/workbuddy/models.js +8 -20
  87. package/src/providers/workbuddy/reshape.js +152 -152
  88. package/src/providers/workbuddy.js +2 -2
  89. package/src/reasoning.js +32 -32
  90. package/src/routes/chat/broadband-handler.js +80 -80
  91. package/src/routes/chat/gateway.js +46 -46
  92. package/src/routes/chat/relay-pipeline.js +282 -282
  93. package/src/routes/chat/via-route-handler.js +156 -156
  94. package/src/routes/hedge.js +260 -260
  95. package/src/routes/models-route.js +167 -167
  96. package/src/routes/peers.js +373 -373
  97. package/src/routes/stream.js +364 -347
  98. package/src/runtime/bootstrap.js +75 -75
  99. package/src/runtime/keep-warm.js +135 -135
  100. package/src/runtime/provider-gate.js +75 -75
  101. package/src/runtime/providers-setup.js +187 -187
  102. package/src/runtime/raccoon-checkin.js +175 -175
  103. package/src/server.js +85 -85
  104. package/src/state/schemas/allowlist.js +92 -92
  105. package/src/sync-opencode.js +280 -280
  106. package/src/transport/index.js +244 -244
  107. package/src/transport/pool.js +56 -56
  108. package/src/transport/retry.js +24 -24
  109. package/src/transport/sse.js +93 -93
  110. package/src/upstream-probe/display.js +52 -52
  111. package/src/upstream-probe/probe.js +49 -49
  112. package/src/upstream-probe/rotate.js +110 -110
  113. package/src/upstream-probe/start.js +45 -45
  114. package/src/upstream.js +355 -355
  115. package/docs/cli_help_mini.md +0 -148
  116. package/src/providers/AGENTS.md +0 -49
  117. package/src/routes/AGENTS.md +0 -37
@@ -1,266 +1,266 @@
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 { joinGroupCore } from "../join-core.js";
6
- import { errMsg } from "../util.js";
7
- import { argValue } from "../policy.js";
8
- import { compatFetch, timeoutSignal } from "../../compat.js";
9
-
10
- export async function handleGroupCreate(args) {
11
- const createGroupArg = argValue(args, "-creategroup", "--creategroup") || groupIs("create", args);
12
- if (!createGroupArg) return false;
13
- const groups = createGroupsService({});
14
- const peers = createPeersService({});
15
- const name = createGroupArg;
16
- const { created } = groups.create(name);
17
- markJoined({ name, leaderUrl: "", myUrl: "", memberName: "leader" });
18
- const synced = await syncAllJoinedGroups({ peers, groups });
19
- const s = synced.find((x) => x.name === name);
20
- console.log(created ? `group created: ${name}` : `group already exists: ${name}`);
21
- console.log(`members on this node: ${s ? `${s.total} (failover: ${s.added})` : "?"}`);
22
- console.log(`others join with: mslxdff -addtogroup <this-node-host> ${name}`);
23
- process.exit(0);
24
- }
25
-
26
- export async function handleGroupCommand(args) {
27
- const groupCmd = argValue(args, "-group", "--group");
28
- if (!groupCmd || groupCmd === "create") return false;
29
- const groups = createGroupsService({});
30
- const peers = createPeersService({});
31
- const rest = args.slice(args.indexOf("-group") + 1);
32
- const [action, a] = rest;
33
- if (action === "sync") {
34
- const synced = await syncAllJoinedGroups({ peers, groups });
35
- if (!synced.length) {
36
- console.log("not joined to any group (use -addtogroup or -creategroup)");
37
- } else {
38
- for (const s of synced) {
39
- if (s.error) console.log(`${s.name}: sync failed — ${s.error}`);
40
- else console.log(`${s.name}: ${s.total} member(s), ${s.added} failover target(s) configured`);
41
- }
42
- }
43
- } else if (action === "leave" && a) {
44
- const before = loadGroupsJoined().filter((g) => g.name === a).length;
45
- saveGroupsJoined(loadGroupsJoined().filter((g) => g.name !== a));
46
- const removed = peers.removeByGroup(a);
47
- console.log(before ? `left group "${a}" (${removed} member(s) removed)` : `not a member of group "${a}"`);
48
- } else if (action === "list") {
49
- const joinedList = loadGroupsJoined();
50
- if (!joinedList.length) {
51
- console.log("no groups on this node");
52
- process.exit(0);
53
- }
54
- const { token } = await loadToken();
55
- const fetchImpl = (url, opts) => compatFetch(url, { ...opts, signal: timeoutSignal(1500) });
56
- for (const g of joinedList) {
57
- const isLeader = !g.leaderUrl;
58
- let members;
59
- if (isLeader) {
60
- members = groups.list()[g.name]?.members || {};
61
- } else {
62
- try {
63
- members = await refreshGroupMembers(g.name, {
64
- leaderUrl: g.leaderUrl,
65
- memberName: g.memberName,
66
- url: g.myUrl,
67
- token,
68
- fetchImpl,
69
- });
70
- } catch (err) {
71
- console.log(`${g.name} (members unavailable — leader unreachable: ${errMsg(err)})`);
72
- continue;
73
- }
74
- }
75
- const entries = Object.entries(members).filter(([id]) => id !== "leader");
76
- console.log(`${g.name} (${entries.length} member${entries.length === 1 ? "" : "s"})`);
77
- const probes = await Promise.all(
78
- entries.map(([id, m]) => probeHealth({ id, url: m?.url || id, kind: m?.kind, lastSeen: m?.lastSeen, publicIp: m?.publicIp }))
79
- );
80
- const leaderEntry = members.leader ? Object.entries(members).find(([id]) => id === "leader") : null;
81
- const leaderProbe = leaderEntry ? await probeHealth({ id: "leader", url: leaderEntry[1].url }) : null;
82
- const display = leaderProbe ? [...probes, leaderProbe] : probes;
83
- let seq = 0;
84
- for (const r of display) {
85
- const m = entries.find(([eid]) => eid === r.id)?.[1] || (r.id === "leader" ? leaderEntry?.[1] : null);
86
- const isBb = r.kind === "broadband" || String(r.url || "").startsWith("relay://") || m?.kind === "broadband";
87
- if (isBb) {
88
- const ago = r.lastSeen ? `${Math.round((Date.now() - r.lastSeen) / 1000)}s ago` : "no heartbeat yet";
89
- const ip = r.publicIp || m?.publicIp || "?";
90
- const via = r.stale ? "stale" : `via leader ${ago}`;
91
- const stateBb = `${via} ip=${ip}`;
92
- if (r.id === "leader") {
93
- console.log(` leader ${r.url} ${stateBb}`);
94
- continue;
95
- }
96
- seq += 1;
97
- const label = r.id && r.id !== r.url ? ` [${r.id}]` : "";
98
- console.log(` ${seq}. ${r.url}${label} [broadband] ${stateBb}`);
99
- continue;
100
- }
101
- const state = r.fail ? `fail ${r.fail}` : `ok ${r.ms}ms`;
102
- if (r.id === "leader") {
103
- console.log(` leader ${r.url} ${state}`);
104
- continue;
105
- }
106
- seq += 1;
107
- const label = r.id && r.id !== r.url ? ` [${r.id}]` : "";
108
- console.log(` ${seq}. ${r.url}${label} ${state}`);
109
- }
110
- }
111
- console.log(`\njoined groups (${joinedList.length}):`);
112
- for (const g of joinedList) console.log(` ${g.name} ${g.leaderUrl || "(this node is the leader)"}`);
113
- } else if (action === "test") {
114
- // -group test <target> [--group <名>] [--model <id>] [--say <词>] [--json]
115
- // target 可以是 url / relay://id / 或 -group list 里的序号(数字)。
116
- const { resolveTargetArg, renderGroupTest } = await import("../group-test-cli.js");
117
- const target = await resolveTargetArg(a, rest);
118
- if (!target) {
119
- console.error("usage: mslxdff -group test <target|seq> [--group <name>] [--model <id>] [--say <text>]");
120
- process.exit(1);
121
- }
122
- const model = argValue(args, "--model", "-m") || undefined;
123
- const say = argValue(args, "--say", "-s") || undefined;
124
- const groupArg = argValue(args, "--group", "-g") || undefined;
125
- const asJson = args.includes("--json");
126
- const { runGroupLinkTest } = await import("../group-test.js");
127
- const r = await runGroupLinkTest({ group: groupArg || "", target, ...(model ? { model } : {}), ...(say ? { prompt: say } : {}) });
128
- renderGroupTest(r, { json: asJson });
129
- process.exit(r.status === "ok" ? 0 : 1);
130
- } else if (action === "remove" && a) {
131
- const seq = Number(a);
132
- if (!Number.isInteger(seq) || seq < 1) {
133
- console.error(`usage: mslxdff -group remove <seq>`);
134
- process.exit(1);
135
- }
136
- const joined = loadGroupsJoined().find((g) => !g.leaderUrl);
137
- if (!joined) {
138
- console.error("group remove requires being the leader — this node leads no group");
139
- process.exit(1);
140
- }
141
- const members = groups.list()[joined.name]?.members || {};
142
- const entries = Object.entries(members).filter(([id]) => id !== "leader");
143
- const target = entries[seq - 1];
144
- if (!target) {
145
- console.error(`member #${seq} not found — group "${joined.name}" has ${entries.length} member(s)`);
146
- process.exit(1);
147
- }
148
- const [id, m] = target;
149
- try {
150
- const removed = groups.removeMember(joined.name, { url: m.url });
151
- if (removed) console.log(`removed ${m.url} from "${joined.name}"`);
152
- else console.log(`member ${m.url} already gone from "${joined.name}"`);
153
- } catch (err) {
154
- console.error(`remove failed: ${errMsg(err)}`);
155
- process.exit(1);
156
- }
157
- } else {
158
- console.error("usage: mslxdff -group sync | -group leave <name> | -group list | -group test <target|seq> | -group remove <seq> | -creategroup <name>");
159
- }
160
- process.exit(0);
161
- }
162
-
163
- export async function handleAddToGroup(args) {
164
- const addToGroupIdx = args.findIndex((x) => x === "-addtogroup" || x === "--addtogroup");
165
- if (addToGroupIdx < 0) return false;
166
- const rawArgs = args.slice(addToGroupIdx + 1);
167
- const isBroadband = rawArgs.includes("--broadband");
168
- const filtered = rawArgs.filter((a) => a !== "--broadband");
169
- const [leaderHost, name] = filtered;
170
- if (filtered.length > 2) {
171
- console.error("usage: mslxdff -addtogroup <leader-host> <name> [--broadband]");
172
- process.exit(1);
173
- }
174
- if (!leaderHost || !name) {
175
- // 缺参数 → 手机宽带接入向导(默认 broadband:手机无公网入站,只出站)
176
- const { runJoinWizard } = await import("../join-wizard.js");
177
- const r = await runJoinWizard({ leaderInput: leaderHost || null, groupName: name || null });
178
- process.exit(r.ok ? 0 : 1);
179
- }
180
- const r = await joinGroupCore({ leaderHost, name, isBroadband, args });
181
- if (!r.ok) {
182
- console.error(`join failed: ${r.error}`);
183
- process.exit(1);
184
- }
185
- console.log(`joined group "${name}" at ${r.leaderUrl}${isBroadband ? " [broadband]" : ""}`);
186
- if (r.syncError) console.log(` local failover setup failed: ${r.syncError}`);
187
- else console.log(` ${r.added ?? 0} failover target(s) configured${isBroadband ? " (broadband via leader, local 127.0.0.1)" : ""}`);
188
- process.exit(0);
189
- }
190
-
191
- export async function handleResetBan(args) {
192
- const resetBanArg = argValue(args, "-resetban", "--resetban");
193
- if (resetBanArg === null && !args.includes("-resetban") && !args.includes("--resetban")) return false;
194
- const bans = createBansService({});
195
- const ip = resetBanArg || null;
196
- bans.clear(ip || undefined);
197
- console.log(ip ? `ban cleared for ${ip}` : "all bans cleared");
198
- process.exit(0);
199
- }
200
-
201
- export async function handleLeaveGroup(args) {
202
- const leaveAll = args.includes("-leavegroup") || args.includes("--leavegroup") || args.includes("-leave-groups");
203
- if (!leaveAll) return false;
204
- const peers = createPeersService({});
205
- const joined = loadGroupsJoined();
206
- if (!joined.length) {
207
- console.log("not joined to any group");
208
- process.exit(0);
209
- }
210
- const myToken = (await loadToken()).token;
211
- const leaders = [];
212
- for (const g of joined) {
213
- if (g.leaderUrl) {
214
- const peersRemoved = peers.removeByGroup(g.name);
215
- try {
216
- const res = await compatFetch(`${g.leaderUrl}/v1/groups/leave`, {
217
- method: "POST",
218
- headers: {
219
- "Content-Type": "application/json",
220
- "Authorization": `Bearer ${myToken}`,
221
- },
222
- body: JSON.stringify({ name: g.name }),
223
- });
224
- if (res.ok) console.log(`${g.name}: left (deregistered from ${g.leaderUrl})`);
225
- else console.log(`${g.name}: left locally (leader said: ${await res.text().catch(() => `HTTP ${res.status}`)})`);
226
- } catch (err) {
227
- console.log(`${g.name}: left locally (leader unreachable: ${errMsg(err)})`);
228
- }
229
- } else {
230
- leaders.push(g.name);
231
- }
232
- }
233
- const left = joined.filter((g) => g.leaderUrl).map((g) => g.name);
234
- saveGroupsJoined(loadGroupsJoined().filter((g) => !left.includes(g.name)));
235
- console.log(`left ${left.length} group(s)`);
236
- if (leaders.length) {
237
- console.log(`\nskipped ${leaders.length} group(s) where this node is the leader:`);
238
- for (const n of leaders) console.log(` ${n} — leaders can't leave; disband it with: mslxdff -delgroup ${n}`);
239
- }
240
- process.exit(0);
241
- }
242
-
243
- export async function handleDelGroup(args) {
244
- const delGroupName = argValue(args, "-delgroup", "--delgroup");
245
- if (!delGroupName) return false;
246
- const groups = createGroupsService({});
247
- const peers = createPeersService({});
248
- const local = groups.list()[delGroupName];
249
- if (!local) {
250
- const joined0 = loadGroupsJoined().find((g) => g.name === delGroupName);
251
- if (joined0?.leaderUrl) {
252
- console.log(`"${delGroupName}" is led by ${joined0.leaderUrl} — you are a member, use -leavegroup to leave it`);
253
- } else if (joined0) {
254
- console.log(`"${delGroupName}" exists in local state but has no group definition — nothing to delete`);
255
- } else {
256
- console.log(`group "${delGroupName}" not found on this node`);
257
- }
258
- process.exit(1);
259
- }
260
- const disbanded = groups.delete(delGroupName);
261
- const members = Object.values(disbanded?.members || {});
262
- peers.removeByGroup(delGroupName);
263
- saveGroupsJoined(loadGroupsJoined().filter((g) => g.name !== delGroupName));
264
- console.log(`group "${delGroupName}" disbanded (${members.length} member${members.length === 1 ? "" : "s"} removed)`);
265
- process.exit(0);
266
- }
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 { joinGroupCore } from "../join-core.js";
6
+ import { errMsg } from "../util.js";
7
+ import { argValue } from "../policy.js";
8
+ import { compatFetch, timeoutSignal } from "../../compat.js";
9
+
10
+ export async function handleGroupCreate(args) {
11
+ const createGroupArg = argValue(args, "-creategroup", "--creategroup") || groupIs("create", args);
12
+ if (!createGroupArg) return false;
13
+ const groups = createGroupsService({});
14
+ const peers = createPeersService({});
15
+ const name = createGroupArg;
16
+ const { created } = groups.create(name);
17
+ markJoined({ name, leaderUrl: "", myUrl: "", memberName: "leader" });
18
+ const synced = await syncAllJoinedGroups({ peers, groups });
19
+ const s = synced.find((x) => x.name === name);
20
+ console.log(created ? `group created: ${name}` : `group already exists: ${name}`);
21
+ console.log(`members on this node: ${s ? `${s.total} (failover: ${s.added})` : "?"}`);
22
+ console.log(`others join with: mslxdff -addtogroup <this-node-host> ${name}`);
23
+ process.exit(0);
24
+ }
25
+
26
+ export async function handleGroupCommand(args) {
27
+ const groupCmd = argValue(args, "-group", "--group");
28
+ if (!groupCmd || groupCmd === "create") return false;
29
+ const groups = createGroupsService({});
30
+ const peers = createPeersService({});
31
+ const rest = args.slice(args.indexOf("-group") + 1);
32
+ const [action, a] = rest;
33
+ if (action === "sync") {
34
+ const synced = await syncAllJoinedGroups({ peers, groups });
35
+ if (!synced.length) {
36
+ console.log("not joined to any group (use -addtogroup or -creategroup)");
37
+ } else {
38
+ for (const s of synced) {
39
+ if (s.error) console.log(`${s.name}: sync failed — ${s.error}`);
40
+ else console.log(`${s.name}: ${s.total} member(s), ${s.added} failover target(s) configured`);
41
+ }
42
+ }
43
+ } else if (action === "leave" && a) {
44
+ const before = loadGroupsJoined().filter((g) => g.name === a).length;
45
+ saveGroupsJoined(loadGroupsJoined().filter((g) => g.name !== a));
46
+ const removed = peers.removeByGroup(a);
47
+ console.log(before ? `left group "${a}" (${removed} member(s) removed)` : `not a member of group "${a}"`);
48
+ } else if (action === "list") {
49
+ const joinedList = loadGroupsJoined();
50
+ if (!joinedList.length) {
51
+ console.log("no groups on this node");
52
+ process.exit(0);
53
+ }
54
+ const { token } = await loadToken();
55
+ const fetchImpl = (url, opts) => compatFetch(url, { ...opts, signal: timeoutSignal(1500) });
56
+ for (const g of joinedList) {
57
+ const isLeader = !g.leaderUrl;
58
+ let members;
59
+ if (isLeader) {
60
+ members = groups.list()[g.name]?.members || {};
61
+ } else {
62
+ try {
63
+ members = await refreshGroupMembers(g.name, {
64
+ leaderUrl: g.leaderUrl,
65
+ memberName: g.memberName,
66
+ url: g.myUrl,
67
+ token,
68
+ fetchImpl,
69
+ });
70
+ } catch (err) {
71
+ console.log(`${g.name} (members unavailable — leader unreachable: ${errMsg(err)})`);
72
+ continue;
73
+ }
74
+ }
75
+ const entries = Object.entries(members).filter(([id]) => id !== "leader");
76
+ console.log(`${g.name} (${entries.length} member${entries.length === 1 ? "" : "s"})`);
77
+ const probes = await Promise.all(
78
+ entries.map(([id, m]) => probeHealth({ id, url: m?.url || id, kind: m?.kind, lastSeen: m?.lastSeen, publicIp: m?.publicIp }))
79
+ );
80
+ const leaderEntry = members.leader ? Object.entries(members).find(([id]) => id === "leader") : null;
81
+ const leaderProbe = leaderEntry ? await probeHealth({ id: "leader", url: leaderEntry[1].url }) : null;
82
+ const display = leaderProbe ? [...probes, leaderProbe] : probes;
83
+ let seq = 0;
84
+ for (const r of display) {
85
+ const m = entries.find(([eid]) => eid === r.id)?.[1] || (r.id === "leader" ? leaderEntry?.[1] : null);
86
+ const isBb = r.kind === "broadband" || String(r.url || "").startsWith("relay://") || m?.kind === "broadband";
87
+ if (isBb) {
88
+ const ago = r.lastSeen ? `${Math.round((Date.now() - r.lastSeen) / 1000)}s ago` : "no heartbeat yet";
89
+ const ip = r.publicIp || m?.publicIp || "?";
90
+ const via = r.stale ? "stale" : `via leader ${ago}`;
91
+ const stateBb = `${via} ip=${ip}`;
92
+ if (r.id === "leader") {
93
+ console.log(` leader ${r.url} ${stateBb}`);
94
+ continue;
95
+ }
96
+ seq += 1;
97
+ const label = r.id && r.id !== r.url ? ` [${r.id}]` : "";
98
+ console.log(` ${seq}. ${r.url}${label} [broadband] ${stateBb}`);
99
+ continue;
100
+ }
101
+ const state = r.fail ? `fail ${r.fail}` : `ok ${r.ms}ms`;
102
+ if (r.id === "leader") {
103
+ console.log(` leader ${r.url} ${state}`);
104
+ continue;
105
+ }
106
+ seq += 1;
107
+ const label = r.id && r.id !== r.url ? ` [${r.id}]` : "";
108
+ console.log(` ${seq}. ${r.url}${label} ${state}`);
109
+ }
110
+ }
111
+ console.log(`\njoined groups (${joinedList.length}):`);
112
+ for (const g of joinedList) console.log(` ${g.name} ${g.leaderUrl || "(this node is the leader)"}`);
113
+ } else if (action === "test") {
114
+ // -group test <target> [--group <名>] [--model <id>] [--say <词>] [--json]
115
+ // target 可以是 url / relay://id / 或 -group list 里的序号(数字)。
116
+ const { resolveTargetArg, renderGroupTest } = await import("../group-test-cli.js");
117
+ const target = await resolveTargetArg(a, rest);
118
+ if (!target) {
119
+ console.error("usage: mslxdff -group test <target|seq> [--group <name>] [--model <id>] [--say <text>]");
120
+ process.exit(1);
121
+ }
122
+ const model = argValue(args, "--model", "-m") || undefined;
123
+ const say = argValue(args, "--say", "-s") || undefined;
124
+ const groupArg = argValue(args, "--group", "-g") || undefined;
125
+ const asJson = args.includes("--json");
126
+ const { runGroupLinkTest } = await import("../group-test.js");
127
+ const r = await runGroupLinkTest({ group: groupArg || "", target, ...(model ? { model } : {}), ...(say ? { prompt: say } : {}) });
128
+ renderGroupTest(r, { json: asJson });
129
+ process.exit(r.status === "ok" ? 0 : 1);
130
+ } else if (action === "remove" && a) {
131
+ const seq = Number(a);
132
+ if (!Number.isInteger(seq) || seq < 1) {
133
+ console.error(`usage: mslxdff -group remove <seq>`);
134
+ process.exit(1);
135
+ }
136
+ const joined = loadGroupsJoined().find((g) => !g.leaderUrl);
137
+ if (!joined) {
138
+ console.error("group remove requires being the leader — this node leads no group");
139
+ process.exit(1);
140
+ }
141
+ const members = groups.list()[joined.name]?.members || {};
142
+ const entries = Object.entries(members).filter(([id]) => id !== "leader");
143
+ const target = entries[seq - 1];
144
+ if (!target) {
145
+ console.error(`member #${seq} not found — group "${joined.name}" has ${entries.length} member(s)`);
146
+ process.exit(1);
147
+ }
148
+ const [id, m] = target;
149
+ try {
150
+ const removed = groups.removeMember(joined.name, { url: m.url });
151
+ if (removed) console.log(`removed ${m.url} from "${joined.name}"`);
152
+ else console.log(`member ${m.url} already gone from "${joined.name}"`);
153
+ } catch (err) {
154
+ console.error(`remove failed: ${errMsg(err)}`);
155
+ process.exit(1);
156
+ }
157
+ } else {
158
+ console.error("usage: mslxdff -group sync | -group leave <name> | -group list | -group test <target|seq> | -group remove <seq> | -creategroup <name>");
159
+ }
160
+ process.exit(0);
161
+ }
162
+
163
+ export async function handleAddToGroup(args) {
164
+ const addToGroupIdx = args.findIndex((x) => x === "-addtogroup" || x === "--addtogroup");
165
+ if (addToGroupIdx < 0) return false;
166
+ const rawArgs = args.slice(addToGroupIdx + 1);
167
+ const isBroadband = rawArgs.includes("--broadband");
168
+ const filtered = rawArgs.filter((a) => a !== "--broadband");
169
+ const [leaderHost, name] = filtered;
170
+ if (filtered.length > 2) {
171
+ console.error("usage: mslxdff -addtogroup <leader-host> <name> [--broadband]");
172
+ process.exit(1);
173
+ }
174
+ if (!leaderHost || !name) {
175
+ // 缺参数 → 手机宽带接入向导(默认 broadband:手机无公网入站,只出站)
176
+ const { runJoinWizard } = await import("../join-wizard.js");
177
+ const r = await runJoinWizard({ leaderInput: leaderHost || null, groupName: name || null });
178
+ process.exit(r.ok ? 0 : 1);
179
+ }
180
+ const r = await joinGroupCore({ leaderHost, name, isBroadband, args });
181
+ if (!r.ok) {
182
+ console.error(`join failed: ${r.error}`);
183
+ process.exit(1);
184
+ }
185
+ console.log(`joined group "${name}" at ${r.leaderUrl}${isBroadband ? " [broadband]" : ""}`);
186
+ if (r.syncError) console.log(` local failover setup failed: ${r.syncError}`);
187
+ else console.log(` ${r.added ?? 0} failover target(s) configured${isBroadband ? " (broadband via leader, local 127.0.0.1)" : ""}`);
188
+ process.exit(0);
189
+ }
190
+
191
+ export async function handleResetBan(args) {
192
+ const resetBanArg = argValue(args, "-resetban", "--resetban");
193
+ if (resetBanArg === null && !args.includes("-resetban") && !args.includes("--resetban")) return false;
194
+ const bans = createBansService({});
195
+ const ip = resetBanArg || null;
196
+ bans.clear(ip || undefined);
197
+ console.log(ip ? `ban cleared for ${ip}` : "all bans cleared");
198
+ process.exit(0);
199
+ }
200
+
201
+ export async function handleLeaveGroup(args) {
202
+ const leaveAll = args.includes("-leavegroup") || args.includes("--leavegroup") || args.includes("-leave-groups");
203
+ if (!leaveAll) return false;
204
+ const peers = createPeersService({});
205
+ const joined = loadGroupsJoined();
206
+ if (!joined.length) {
207
+ console.log("not joined to any group");
208
+ process.exit(0);
209
+ }
210
+ const myToken = (await loadToken()).token;
211
+ const leaders = [];
212
+ for (const g of joined) {
213
+ if (g.leaderUrl) {
214
+ const peersRemoved = peers.removeByGroup(g.name);
215
+ try {
216
+ const res = await compatFetch(`${g.leaderUrl}/v1/groups/leave`, {
217
+ method: "POST",
218
+ headers: {
219
+ "Content-Type": "application/json",
220
+ "Authorization": `Bearer ${myToken}`,
221
+ },
222
+ body: JSON.stringify({ name: g.name }),
223
+ });
224
+ if (res.ok) console.log(`${g.name}: left (deregistered from ${g.leaderUrl})`);
225
+ else console.log(`${g.name}: left locally (leader said: ${await res.text().catch(() => `HTTP ${res.status}`)})`);
226
+ } catch (err) {
227
+ console.log(`${g.name}: left locally (leader unreachable: ${errMsg(err)})`);
228
+ }
229
+ } else {
230
+ leaders.push(g.name);
231
+ }
232
+ }
233
+ const left = joined.filter((g) => g.leaderUrl).map((g) => g.name);
234
+ saveGroupsJoined(loadGroupsJoined().filter((g) => !left.includes(g.name)));
235
+ console.log(`left ${left.length} group(s)`);
236
+ if (leaders.length) {
237
+ console.log(`\nskipped ${leaders.length} group(s) where this node is the leader:`);
238
+ for (const n of leaders) console.log(` ${n} — leaders can't leave; disband it with: mslxdff -delgroup ${n}`);
239
+ }
240
+ process.exit(0);
241
+ }
242
+
243
+ export async function handleDelGroup(args) {
244
+ const delGroupName = argValue(args, "-delgroup", "--delgroup");
245
+ if (!delGroupName) return false;
246
+ const groups = createGroupsService({});
247
+ const peers = createPeersService({});
248
+ const local = groups.list()[delGroupName];
249
+ if (!local) {
250
+ const joined0 = loadGroupsJoined().find((g) => g.name === delGroupName);
251
+ if (joined0?.leaderUrl) {
252
+ console.log(`"${delGroupName}" is led by ${joined0.leaderUrl} — you are a member, use -leavegroup to leave it`);
253
+ } else if (joined0) {
254
+ console.log(`"${delGroupName}" exists in local state but has no group definition — nothing to delete`);
255
+ } else {
256
+ console.log(`group "${delGroupName}" not found on this node`);
257
+ }
258
+ process.exit(1);
259
+ }
260
+ const disbanded = groups.delete(delGroupName);
261
+ const members = Object.values(disbanded?.members || {});
262
+ peers.removeByGroup(delGroupName);
263
+ saveGroupsJoined(loadGroupsJoined().filter((g) => g.name !== delGroupName));
264
+ console.log(`group "${delGroupName}" disbanded (${members.length} member${members.length === 1 ? "" : "s"} removed)`);
265
+ process.exit(0);
266
+ }
@@ -1,50 +1,50 @@
1
- import { savePreferredModel, loadModelPicks, saveModelPicks } from "../../../state.js";
2
-
3
- /**
4
- * 勾选集分支:set / pick / unpick / picks / pick clear。
5
- * 命中返回 true(分支内 process.exit,不会实际返回);未命中返回 false。
6
- */
7
- export async function handlePicksCommand(args, idx, sub) {
8
- if (sub === "set" && args[idx + 2]) {
9
- const id = args[idx + 2];
10
- savePreferredModel(id);
11
- const picks = [...new Set([...loadModelPicks(), id])];
12
- saveModelPicks(picks);
13
- console.log(`default model set to: ${id} (daemon hot-reloads on next request)`);
14
- console.log(`picked: ${picks.join(", ") || "(none)"} (auto will pick within these)`);
15
- process.exit(0);
16
- }
17
- if (sub === "pick" && args[idx + 2] && args[idx + 2] !== "clear") {
18
- // 多 id 勾选:`-model pick a b c`(空格分隔)与 `-model pick a,b`(逗号)均支持
19
- const ids = args.slice(idx + 2).flatMap((x) => String(x).split(",")).map((x) => x.trim()).filter(Boolean);
20
- const picks = [...new Set([...loadModelPicks(), ...ids])];
21
- saveModelPicks(picks);
22
- console.log(`picked ${ids.length} model(s): ${ids.join(", ")}`);
23
- console.log(`picked: ${picks.join(", ") || "(none)"} (auto will pick within these)`);
24
- process.exit(0);
25
- }
26
- if (sub === "pick" && args[idx + 2] === "clear") {
27
- saveModelPicks([]);
28
- console.log("picks cleared — auto uses the full model list again");
29
- process.exit(0);
30
- }
31
- if (sub === "unpick" && args[idx + 2]) {
32
- const removeSet = new Set(args.slice(idx + 2).flatMap((x) => String(x).split(",")).map((x) => x.trim()).filter(Boolean));
33
- const picks = loadModelPicks().filter((x) => !removeSet.has(x));
34
- saveModelPicks(picks);
35
- console.log(`unpicked ${removeSet.size}: ${[...removeSet].join(", ")}`);
36
- console.log(`picked: ${picks.join(", ") || "(none)"}${picks.length === 0 ? " (auto uses full list)" : ""}`);
37
- process.exit(0);
38
- }
39
- if (sub === "picks") {
40
- const picks = loadModelPicks();
41
- if (!picks.length) {
42
- console.log("no picks — auto uses the full model list");
43
- } else {
44
- console.log(`${picks.length} picked model(s), auto only selects within these:`);
45
- }
46
- for (const id of picks) console.log(` ${id}`);
47
- process.exit(0);
48
- }
49
- return false;
50
- }
1
+ import { savePreferredModel, loadModelPicks, saveModelPicks } from "../../../state.js";
2
+
3
+ /**
4
+ * 勾选集分支:set / pick / unpick / picks / pick clear。
5
+ * 命中返回 true(分支内 process.exit,不会实际返回);未命中返回 false。
6
+ */
7
+ export async function handlePicksCommand(args, idx, sub) {
8
+ if (sub === "set" && args[idx + 2]) {
9
+ const id = args[idx + 2];
10
+ savePreferredModel(id);
11
+ const picks = [...new Set([...loadModelPicks(), id])];
12
+ saveModelPicks(picks);
13
+ console.log(`default model set to: ${id} (daemon hot-reloads on next request)`);
14
+ console.log(`picked: ${picks.join(", ") || "(none)"} (auto will pick within these)`);
15
+ process.exit(0);
16
+ }
17
+ if (sub === "pick" && args[idx + 2] && args[idx + 2] !== "clear") {
18
+ // 多 id 勾选:`-model pick a b c`(空格分隔)与 `-model pick a,b`(逗号)均支持
19
+ const ids = args.slice(idx + 2).flatMap((x) => String(x).split(",")).map((x) => x.trim()).filter(Boolean);
20
+ const picks = [...new Set([...loadModelPicks(), ...ids])];
21
+ saveModelPicks(picks);
22
+ console.log(`picked ${ids.length} model(s): ${ids.join(", ")}`);
23
+ console.log(`picked: ${picks.join(", ") || "(none)"} (auto will pick within these)`);
24
+ process.exit(0);
25
+ }
26
+ if (sub === "pick" && args[idx + 2] === "clear") {
27
+ saveModelPicks([]);
28
+ console.log("picks cleared — auto uses the full model list again");
29
+ process.exit(0);
30
+ }
31
+ if (sub === "unpick" && args[idx + 2]) {
32
+ const removeSet = new Set(args.slice(idx + 2).flatMap((x) => String(x).split(",")).map((x) => x.trim()).filter(Boolean));
33
+ const picks = loadModelPicks().filter((x) => !removeSet.has(x));
34
+ saveModelPicks(picks);
35
+ console.log(`unpicked ${removeSet.size}: ${[...removeSet].join(", ")}`);
36
+ console.log(`picked: ${picks.join(", ") || "(none)"}${picks.length === 0 ? " (auto uses full list)" : ""}`);
37
+ process.exit(0);
38
+ }
39
+ if (sub === "picks") {
40
+ const picks = loadModelPicks();
41
+ if (!picks.length) {
42
+ console.log("no picks — auto uses the full model list");
43
+ } else {
44
+ console.log(`${picks.length} picked model(s), auto only selects within these:`);
45
+ }
46
+ for (const id of picks) console.log(` ${id}`);
47
+ process.exit(0);
48
+ }
49
+ return false;
50
+ }