mslxdff 0.1.155 → 0.1.158
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 +4 -4
- package/docs/ARCHITECTURE.md +426 -0
- package/docs/FEATURE_TREE.md +164 -0
- package/docs/MOBILE.md +82 -0
- package/docs/adr/0001-reasoning-content-injection.md +14 -0
- package/docs/adr/0002-models-free-filter.md +12 -0
- package/docs/adr/0003-zero-state-no-auth.md +10 -0
- package/docs/adr/0004-bearer-token.md +18 -0
- package/docs/adr/0005-peer-mesh.md +53 -0
- package/docs/adr/0006-broadband-member.md +103 -0
- package/docs/adr/0007-multi-provider-prefix.md +25 -0
- package/docs/adr/0008-share-keys-to-peers.md +52 -0
- package/docs/adr/0009-chat-repl.md +37 -0
- package/docs/adr/0010-allowlist.md +36 -0
- package/docs/adr/0011-broadband-stream.md +30 -0
- package/docs/adr/0012-responses-endpoint-codex-sync.md +48 -0
- package/docs/adr/0013-node16-compat.md +41 -0
- package/docs/adr/0014-deepseek-provider.md +52 -0
- package/docs/adr/0015-upstream-probe-routing.md +50 -0
- package/docs/adr/0016-model-capabilities.md +27 -0
- package/docs/adr/0017-ai-sdk-upstream-engine.md +59 -0
- package/docs/adr/0018-zen-client-identity.md +52 -0
- package/docs/adr/0019-share-keys-always-lend.md +59 -0
- package/docs/adr/0020-zen-free-lane-agent-shape.md +52 -0
- package/docs/adr/0021-usage-report-jsonl.md +47 -0
- package/docs/adr/0022-models-capability-merge.md +61 -0
- package/docs/adr/0023-key-provider-default-direct.md +58 -0
- package/docs/adr/0024-node18-baseline.md +63 -0
- package/docs/adr/0025-workbuddy-authdir-follows-state.md +71 -0
- package/docs/adr/0026-cline-provider-id-unify.md +67 -0
- package/docs/adr/0027-codearts-provider.md +48 -0
- package/docs/adr/0028-traework-provider.md +34 -0
- package/docs/adr/0029-qoder-native-provider.md +60 -0
- package/docs/adr/0030-models-list-scoped-by-picks.md +53 -0
- package/docs/adr/0031-qoder-true-streaming.md +50 -0
- package/docs/adr/0032-generic-responses-channel.md +72 -0
- package/docs/adr/0033-cline-allowlist-auto-sync.md +75 -0
- package/docs/adr/0034-request-level-human-readable-observability.md +60 -0
- package/docs/adr/0035-sdk-channel-headers-timeout.md +49 -0
- package/docs/adr/0036-qoder-per-request-sticky-account.md +82 -0
- package/docs/adr/0037-qwenwork-independent-provider.md +82 -0
- package/docs/adr/0038-zcode-provider.md +140 -0
- package/docs/agents/domain.md +51 -0
- package/docs/agents/issue-tracker.md +30 -0
- package/docs/agents/triage-labels.md +15 -0
- package/docs/cli_help.md +1391 -0
- package/docs/cli_help_mini.md +133 -0
- package/docs/plans/bench-via-latency-2026-09-01.md +215 -0
- package/docs/plugins.md +187 -0
- package/package.json +1 -1
- package/src/auto.js +254 -254
- package/src/bench/cline-bench.js +42 -42
- package/src/bench/probe.js +70 -70
- package/src/bench/report.js +162 -162
- package/src/bench/runner.js +77 -77
- package/src/bench/via-probe.js +124 -124
- package/src/bench/via-routes.js +87 -87
- package/src/bench/workbuddy-bench.js +54 -54
- package/src/chat/engine.js +160 -160
- package/src/chat/gateway.js +163 -163
- package/src/chat/orchestrator.js +234 -234
- package/src/chat/prompt.js +70 -70
- package/src/chat/repl.js +88 -88
- package/src/chat/terminal.js +135 -135
- package/src/chat/tools.js +306 -306
- package/src/chat-pipeline/index.js +123 -123
- package/src/chat-pipeline/policy.js +76 -76
- package/src/chat-pipeline/serial-trial.js +210 -210
- package/src/cli/commands/group.js +249 -249
- package/src/cli/commands/model/list-providers.js +4 -3
- package/src/cli/commands/model/list-render.js +7 -9
- package/src/cli/commands/model/list-sort.js +52 -0
- package/src/cli/commands/model/list-tty.js +76 -0
- package/src/cli/commands/model/list.js +5 -62
- package/src/cli/commands/model/picks.js +50 -50
- package/src/cli/commands/provider/bench-via.js +247 -247
- package/src/cli/commands/provider/bench.js +141 -141
- package/src/cli/commands/provider/index.js +124 -116
- package/src/cli/commands/provider/models.js +139 -128
- package/src/cli/commands/provider/qwenwork-login.js +119 -0
- package/src/cli/commands/provider/zcode-login.js +77 -0
- package/src/cli/commands/provider/zcode-quota.js +55 -0
- package/src/cli/commands/sync.js +232 -232
- package/src/cli/provider-row.js +2 -2
- package/src/cli/status.js +279 -279
- package/src/daemon.js +96 -96
- package/src/model-capabilities/enrich.js +86 -86
- package/src/model-capabilities/index.js +183 -183
- package/src/model-capabilities/parse.js +70 -70
- package/src/model-trace.js +1 -0
- package/src/models.js +225 -225
- package/src/providers/classify.js +1 -1
- package/src/providers/cline/auth.js +228 -228
- package/src/providers/cline/chat.js +307 -307
- package/src/providers/cline.js +2 -2
- package/src/providers/keyring.js +60 -56
- package/src/providers/qoder/chat.js +183 -100
- package/src/providers/qoder/index.js +230 -184
- package/src/providers/qoder/sse.js +103 -46
- package/src/providers/qoder/sticky.js +1 -0
- package/src/providers/qoder/stream.js +32 -9
- package/src/providers/qwenwork/account-store.js +133 -0
- package/src/providers/qwenwork/constants.js +67 -0
- package/src/providers/qwenwork/cosy.js +120 -0
- package/src/providers/qwenwork/crypto.js +218 -0
- package/src/providers/qwenwork/http.js +20 -0
- package/src/providers/qwenwork/index.js +327 -0
- package/src/providers/qwenwork/payload.js +142 -0
- package/src/providers/qwenwork/rsa.js +54 -0
- package/src/providers/qwenwork/sse.js +268 -0
- package/src/providers/qwenwork/stream.js +130 -0
- package/src/providers/qwenwork/upstream.js +120 -0
- package/src/providers/qwenwork.js +1 -0
- package/src/providers/registry.js +66 -56
- package/src/providers/share-keys.js +2 -2
- package/src/providers/workbuddy/chat.js +248 -248
- package/src/providers/workbuddy/reshape.js +152 -152
- package/src/providers/workbuddy.js +2 -2
- package/src/providers/zcode/account-store.js +129 -0
- package/src/providers/zcode/auth.js +28 -0
- package/src/providers/zcode/chat.js +171 -0
- package/src/providers/zcode/const.js +54 -0
- package/src/providers/zcode/headers.js +55 -0
- package/src/providers/zcode/index.js +124 -0
- package/src/providers/zcode/models.js +65 -0
- package/src/providers/zcode/oauth.js +120 -0
- package/src/providers/zcode/quota.js +176 -0
- package/src/providers/zcode/sse.js +179 -0
- package/src/reasoning.js +32 -32
- package/src/routes/chat/gateway.js +46 -46
- package/src/routes/chat/relay-pipeline.js +250 -246
- package/src/routes/chat/via-route-handler.js +144 -144
- package/src/routes/hedge.js +255 -255
- package/src/routes/models-route.js +167 -167
- package/src/routes/peers.js +273 -273
- package/src/routes/stream.js +438 -399
- package/src/runtime/bootstrap.js +45 -45
- package/src/runtime/provider-gate.js +33 -30
- package/src/runtime/providers-setup.js +165 -165
- package/src/server.js +64 -64
- package/src/state/schemas/allowlist.js +92 -92
- package/src/sync-opencode.js +280 -280
- package/src/transport/index.js +244 -244
- package/src/transport/pool.js +56 -56
- package/src/transport/retry.js +24 -24
- package/src/transport/sse.js +93 -93
- package/src/upstream-probe/display.js +52 -52
- package/src/upstream-probe/probe.js +49 -49
- package/src/upstream-probe/rotate.js +110 -110
- package/src/upstream-probe/start.js +45 -45
- package/src/upstream.js +289 -289
|
@@ -1,249 +1,249 @@
|
|
|
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 === "remove" && a) {
|
|
114
|
-
const seq = Number(a);
|
|
115
|
-
if (!Number.isInteger(seq) || seq < 1) {
|
|
116
|
-
console.error(`usage: mslxdff -group remove <seq>`);
|
|
117
|
-
process.exit(1);
|
|
118
|
-
}
|
|
119
|
-
const joined = loadGroupsJoined().find((g) => !g.leaderUrl);
|
|
120
|
-
if (!joined) {
|
|
121
|
-
console.error("group remove requires being the leader — this node leads no group");
|
|
122
|
-
process.exit(1);
|
|
123
|
-
}
|
|
124
|
-
const members = groups.list()[joined.name]?.members || {};
|
|
125
|
-
const entries = Object.entries(members).filter(([id]) => id !== "leader");
|
|
126
|
-
const target = entries[seq - 1];
|
|
127
|
-
if (!target) {
|
|
128
|
-
console.error(`member #${seq} not found — group "${joined.name}" has ${entries.length} member(s)`);
|
|
129
|
-
process.exit(1);
|
|
130
|
-
}
|
|
131
|
-
const [id, m] = target;
|
|
132
|
-
try {
|
|
133
|
-
const removed = groups.removeMember(joined.name, { url: m.url });
|
|
134
|
-
if (removed) console.log(`removed ${m.url} from "${joined.name}"`);
|
|
135
|
-
else console.log(`member ${m.url} already gone from "${joined.name}"`);
|
|
136
|
-
} catch (err) {
|
|
137
|
-
console.error(`remove failed: ${errMsg(err)}`);
|
|
138
|
-
process.exit(1);
|
|
139
|
-
}
|
|
140
|
-
} else {
|
|
141
|
-
console.error("usage: mslxdff -group sync | -group leave <name> | -group list | -group remove <seq> | -creategroup <name>");
|
|
142
|
-
}
|
|
143
|
-
process.exit(0);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
export async function handleAddToGroup(args) {
|
|
147
|
-
const addToGroupIdx = args.findIndex((x) => x === "-addtogroup" || x === "--addtogroup");
|
|
148
|
-
if (addToGroupIdx < 0) return false;
|
|
149
|
-
const rawArgs = args.slice(addToGroupIdx + 1);
|
|
150
|
-
const isBroadband = rawArgs.includes("--broadband");
|
|
151
|
-
const filtered = rawArgs.filter((a) => a !== "--broadband");
|
|
152
|
-
const [leaderHost, name] = filtered;
|
|
153
|
-
if (filtered.length > 2) {
|
|
154
|
-
console.error("usage: mslxdff -addtogroup <leader-host> <name> [--broadband]");
|
|
155
|
-
process.exit(1);
|
|
156
|
-
}
|
|
157
|
-
if (!leaderHost || !name) {
|
|
158
|
-
// 缺参数 → 手机宽带接入向导(默认 broadband:手机无公网入站,只出站)
|
|
159
|
-
const { runJoinWizard } = await import("../join-wizard.js");
|
|
160
|
-
const r = await runJoinWizard({ leaderInput: leaderHost || null, groupName: name || null });
|
|
161
|
-
process.exit(r.ok ? 0 : 1);
|
|
162
|
-
}
|
|
163
|
-
const r = await joinGroupCore({ leaderHost, name, isBroadband, args });
|
|
164
|
-
if (!r.ok) {
|
|
165
|
-
console.error(`join failed: ${r.error}`);
|
|
166
|
-
process.exit(1);
|
|
167
|
-
}
|
|
168
|
-
console.log(`joined group "${name}" at ${r.leaderUrl}${isBroadband ? " [broadband]" : ""}`);
|
|
169
|
-
if (r.syncError) console.log(` local failover setup failed: ${r.syncError}`);
|
|
170
|
-
else console.log(` ${r.added ?? 0} failover target(s) configured${isBroadband ? " (broadband via leader, local 127.0.0.1)" : ""}`);
|
|
171
|
-
process.exit(0);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
export async function handleResetBan(args) {
|
|
175
|
-
const resetBanArg = argValue(args, "-resetban", "--resetban");
|
|
176
|
-
if (resetBanArg === null && !args.includes("-resetban") && !args.includes("--resetban")) return false;
|
|
177
|
-
const bans = createBansService({});
|
|
178
|
-
const ip = resetBanArg || null;
|
|
179
|
-
bans.clear(ip || undefined);
|
|
180
|
-
console.log(ip ? `ban cleared for ${ip}` : "all bans cleared");
|
|
181
|
-
process.exit(0);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
export async function handleLeaveGroup(args) {
|
|
185
|
-
const leaveAll = args.includes("-leavegroup") || args.includes("--leavegroup") || args.includes("-leave-groups");
|
|
186
|
-
if (!leaveAll) return false;
|
|
187
|
-
const peers = createPeersService({});
|
|
188
|
-
const joined = loadGroupsJoined();
|
|
189
|
-
if (!joined.length) {
|
|
190
|
-
console.log("not joined to any group");
|
|
191
|
-
process.exit(0);
|
|
192
|
-
}
|
|
193
|
-
const myToken = (await loadToken()).token;
|
|
194
|
-
const leaders = [];
|
|
195
|
-
for (const g of joined) {
|
|
196
|
-
if (g.leaderUrl) {
|
|
197
|
-
const peersRemoved = peers.removeByGroup(g.name);
|
|
198
|
-
try {
|
|
199
|
-
const res = await compatFetch(`${g.leaderUrl}/v1/groups/leave`, {
|
|
200
|
-
method: "POST",
|
|
201
|
-
headers: {
|
|
202
|
-
"Content-Type": "application/json",
|
|
203
|
-
"Authorization": `Bearer ${myToken}`,
|
|
204
|
-
},
|
|
205
|
-
body: JSON.stringify({ name: g.name }),
|
|
206
|
-
});
|
|
207
|
-
if (res.ok) console.log(`${g.name}: left (deregistered from ${g.leaderUrl})`);
|
|
208
|
-
else console.log(`${g.name}: left locally (leader said: ${await res.text().catch(() => `HTTP ${res.status}`)})`);
|
|
209
|
-
} catch (err) {
|
|
210
|
-
console.log(`${g.name}: left locally (leader unreachable: ${errMsg(err)})`);
|
|
211
|
-
}
|
|
212
|
-
} else {
|
|
213
|
-
leaders.push(g.name);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
const left = joined.filter((g) => g.leaderUrl).map((g) => g.name);
|
|
217
|
-
saveGroupsJoined(loadGroupsJoined().filter((g) => !left.includes(g.name)));
|
|
218
|
-
console.log(`left ${left.length} group(s)`);
|
|
219
|
-
if (leaders.length) {
|
|
220
|
-
console.log(`\nskipped ${leaders.length} group(s) where this node is the leader:`);
|
|
221
|
-
for (const n of leaders) console.log(` ${n} — leaders can't leave; disband it with: mslxdff -delgroup ${n}`);
|
|
222
|
-
}
|
|
223
|
-
process.exit(0);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
export async function handleDelGroup(args) {
|
|
227
|
-
const delGroupName = argValue(args, "-delgroup", "--delgroup");
|
|
228
|
-
if (!delGroupName) return false;
|
|
229
|
-
const groups = createGroupsService({});
|
|
230
|
-
const peers = createPeersService({});
|
|
231
|
-
const local = groups.list()[delGroupName];
|
|
232
|
-
if (!local) {
|
|
233
|
-
const joined0 = loadGroupsJoined().find((g) => g.name === delGroupName);
|
|
234
|
-
if (joined0?.leaderUrl) {
|
|
235
|
-
console.log(`"${delGroupName}" is led by ${joined0.leaderUrl} — you are a member, use -leavegroup to leave it`);
|
|
236
|
-
} else if (joined0) {
|
|
237
|
-
console.log(`"${delGroupName}" exists in local state but has no group definition — nothing to delete`);
|
|
238
|
-
} else {
|
|
239
|
-
console.log(`group "${delGroupName}" not found on this node`);
|
|
240
|
-
}
|
|
241
|
-
process.exit(1);
|
|
242
|
-
}
|
|
243
|
-
const disbanded = groups.delete(delGroupName);
|
|
244
|
-
const members = Object.values(disbanded?.members || {});
|
|
245
|
-
peers.removeByGroup(delGroupName);
|
|
246
|
-
saveGroupsJoined(loadGroupsJoined().filter((g) => g.name !== delGroupName));
|
|
247
|
-
console.log(`group "${delGroupName}" disbanded (${members.length} member${members.length === 1 ? "" : "s"} removed)`);
|
|
248
|
-
process.exit(0);
|
|
249
|
-
}
|
|
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 === "remove" && a) {
|
|
114
|
+
const seq = Number(a);
|
|
115
|
+
if (!Number.isInteger(seq) || seq < 1) {
|
|
116
|
+
console.error(`usage: mslxdff -group remove <seq>`);
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
const joined = loadGroupsJoined().find((g) => !g.leaderUrl);
|
|
120
|
+
if (!joined) {
|
|
121
|
+
console.error("group remove requires being the leader — this node leads no group");
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
const members = groups.list()[joined.name]?.members || {};
|
|
125
|
+
const entries = Object.entries(members).filter(([id]) => id !== "leader");
|
|
126
|
+
const target = entries[seq - 1];
|
|
127
|
+
if (!target) {
|
|
128
|
+
console.error(`member #${seq} not found — group "${joined.name}" has ${entries.length} member(s)`);
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
const [id, m] = target;
|
|
132
|
+
try {
|
|
133
|
+
const removed = groups.removeMember(joined.name, { url: m.url });
|
|
134
|
+
if (removed) console.log(`removed ${m.url} from "${joined.name}"`);
|
|
135
|
+
else console.log(`member ${m.url} already gone from "${joined.name}"`);
|
|
136
|
+
} catch (err) {
|
|
137
|
+
console.error(`remove failed: ${errMsg(err)}`);
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
} else {
|
|
141
|
+
console.error("usage: mslxdff -group sync | -group leave <name> | -group list | -group remove <seq> | -creategroup <name>");
|
|
142
|
+
}
|
|
143
|
+
process.exit(0);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export async function handleAddToGroup(args) {
|
|
147
|
+
const addToGroupIdx = args.findIndex((x) => x === "-addtogroup" || x === "--addtogroup");
|
|
148
|
+
if (addToGroupIdx < 0) return false;
|
|
149
|
+
const rawArgs = args.slice(addToGroupIdx + 1);
|
|
150
|
+
const isBroadband = rawArgs.includes("--broadband");
|
|
151
|
+
const filtered = rawArgs.filter((a) => a !== "--broadband");
|
|
152
|
+
const [leaderHost, name] = filtered;
|
|
153
|
+
if (filtered.length > 2) {
|
|
154
|
+
console.error("usage: mslxdff -addtogroup <leader-host> <name> [--broadband]");
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
if (!leaderHost || !name) {
|
|
158
|
+
// 缺参数 → 手机宽带接入向导(默认 broadband:手机无公网入站,只出站)
|
|
159
|
+
const { runJoinWizard } = await import("../join-wizard.js");
|
|
160
|
+
const r = await runJoinWizard({ leaderInput: leaderHost || null, groupName: name || null });
|
|
161
|
+
process.exit(r.ok ? 0 : 1);
|
|
162
|
+
}
|
|
163
|
+
const r = await joinGroupCore({ leaderHost, name, isBroadband, args });
|
|
164
|
+
if (!r.ok) {
|
|
165
|
+
console.error(`join failed: ${r.error}`);
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
console.log(`joined group "${name}" at ${r.leaderUrl}${isBroadband ? " [broadband]" : ""}`);
|
|
169
|
+
if (r.syncError) console.log(` local failover setup failed: ${r.syncError}`);
|
|
170
|
+
else console.log(` ${r.added ?? 0} failover target(s) configured${isBroadband ? " (broadband via leader, local 127.0.0.1)" : ""}`);
|
|
171
|
+
process.exit(0);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export async function handleResetBan(args) {
|
|
175
|
+
const resetBanArg = argValue(args, "-resetban", "--resetban");
|
|
176
|
+
if (resetBanArg === null && !args.includes("-resetban") && !args.includes("--resetban")) return false;
|
|
177
|
+
const bans = createBansService({});
|
|
178
|
+
const ip = resetBanArg || null;
|
|
179
|
+
bans.clear(ip || undefined);
|
|
180
|
+
console.log(ip ? `ban cleared for ${ip}` : "all bans cleared");
|
|
181
|
+
process.exit(0);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export async function handleLeaveGroup(args) {
|
|
185
|
+
const leaveAll = args.includes("-leavegroup") || args.includes("--leavegroup") || args.includes("-leave-groups");
|
|
186
|
+
if (!leaveAll) return false;
|
|
187
|
+
const peers = createPeersService({});
|
|
188
|
+
const joined = loadGroupsJoined();
|
|
189
|
+
if (!joined.length) {
|
|
190
|
+
console.log("not joined to any group");
|
|
191
|
+
process.exit(0);
|
|
192
|
+
}
|
|
193
|
+
const myToken = (await loadToken()).token;
|
|
194
|
+
const leaders = [];
|
|
195
|
+
for (const g of joined) {
|
|
196
|
+
if (g.leaderUrl) {
|
|
197
|
+
const peersRemoved = peers.removeByGroup(g.name);
|
|
198
|
+
try {
|
|
199
|
+
const res = await compatFetch(`${g.leaderUrl}/v1/groups/leave`, {
|
|
200
|
+
method: "POST",
|
|
201
|
+
headers: {
|
|
202
|
+
"Content-Type": "application/json",
|
|
203
|
+
"Authorization": `Bearer ${myToken}`,
|
|
204
|
+
},
|
|
205
|
+
body: JSON.stringify({ name: g.name }),
|
|
206
|
+
});
|
|
207
|
+
if (res.ok) console.log(`${g.name}: left (deregistered from ${g.leaderUrl})`);
|
|
208
|
+
else console.log(`${g.name}: left locally (leader said: ${await res.text().catch(() => `HTTP ${res.status}`)})`);
|
|
209
|
+
} catch (err) {
|
|
210
|
+
console.log(`${g.name}: left locally (leader unreachable: ${errMsg(err)})`);
|
|
211
|
+
}
|
|
212
|
+
} else {
|
|
213
|
+
leaders.push(g.name);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const left = joined.filter((g) => g.leaderUrl).map((g) => g.name);
|
|
217
|
+
saveGroupsJoined(loadGroupsJoined().filter((g) => !left.includes(g.name)));
|
|
218
|
+
console.log(`left ${left.length} group(s)`);
|
|
219
|
+
if (leaders.length) {
|
|
220
|
+
console.log(`\nskipped ${leaders.length} group(s) where this node is the leader:`);
|
|
221
|
+
for (const n of leaders) console.log(` ${n} — leaders can't leave; disband it with: mslxdff -delgroup ${n}`);
|
|
222
|
+
}
|
|
223
|
+
process.exit(0);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export async function handleDelGroup(args) {
|
|
227
|
+
const delGroupName = argValue(args, "-delgroup", "--delgroup");
|
|
228
|
+
if (!delGroupName) return false;
|
|
229
|
+
const groups = createGroupsService({});
|
|
230
|
+
const peers = createPeersService({});
|
|
231
|
+
const local = groups.list()[delGroupName];
|
|
232
|
+
if (!local) {
|
|
233
|
+
const joined0 = loadGroupsJoined().find((g) => g.name === delGroupName);
|
|
234
|
+
if (joined0?.leaderUrl) {
|
|
235
|
+
console.log(`"${delGroupName}" is led by ${joined0.leaderUrl} — you are a member, use -leavegroup to leave it`);
|
|
236
|
+
} else if (joined0) {
|
|
237
|
+
console.log(`"${delGroupName}" exists in local state but has no group definition — nothing to delete`);
|
|
238
|
+
} else {
|
|
239
|
+
console.log(`group "${delGroupName}" not found on this node`);
|
|
240
|
+
}
|
|
241
|
+
process.exit(1);
|
|
242
|
+
}
|
|
243
|
+
const disbanded = groups.delete(delGroupName);
|
|
244
|
+
const members = Object.values(disbanded?.members || {});
|
|
245
|
+
peers.removeByGroup(delGroupName);
|
|
246
|
+
saveGroupsJoined(loadGroupsJoined().filter((g) => g.name !== delGroupName));
|
|
247
|
+
console.log(`group "${delGroupName}" disbanded (${members.length} member${members.length === 1 ? "" : "s"} removed)`);
|
|
248
|
+
process.exit(0);
|
|
249
|
+
}
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 其他供应商 allowlist 段 + 遗留别名映射展示。
|
|
3
3
|
* 输入均为已算好的数据;state/model-id 走动态 import(与原内联一致,避免循环依赖)。
|
|
4
|
+
* allowlist 按 compareModelIds 排序展示(只改展示顺序,不改 allowlist 落盘内容)。
|
|
4
5
|
*/
|
|
6
|
+
import { compareModelIds } from "./list-sort.js";
|
|
5
7
|
export async function renderOtherProviders({ pickedIds, ids, fullAliases }) {
|
|
6
8
|
const { loadProviderConfigs, loadProviderAllowedModels, loadProviderAllowAnyModels, loadProviderBaseUrl } = await import("../../../state.js");
|
|
7
9
|
const { loadModelAliases: _la2, getAliasForModel: _gaf } = await import("../../../providers/model-id.js");
|
|
8
10
|
try { _la2(); } catch {}
|
|
9
11
|
const configs = loadProviderConfigs();
|
|
10
12
|
const otherIds = Object.keys(configs).filter((k) => String(k).toLowerCase() !== "opencode");
|
|
11
|
-
const order2 = ["workbuddy", "cline", "openrouter", "bai"];
|
|
13
|
+
const order2 = ["workbuddy", "cline", "openrouter", "bai", "qoder", "qwenwork", "traework", "zcode", "codearts"];
|
|
12
14
|
otherIds.sort((a, b) => {
|
|
13
15
|
const ia = order2.indexOf(a), ib = order2.indexOf(b);
|
|
14
16
|
if (ia !== -1 || ib !== -1) {
|
|
@@ -38,8 +40,7 @@ export async function renderOtherProviders({ pickedIds, ids, fullAliases }) {
|
|
|
38
40
|
console.log(` 或放行全部: mslxdff -provider ${pid} allowAny on`);
|
|
39
41
|
}
|
|
40
42
|
} else {
|
|
41
|
-
for (const
|
|
42
|
-
const canonical = `${pid}/${raw}`;
|
|
43
|
+
for (const canonical of allowed.map((raw) => `${pid}/${raw}`).sort(compareModelIds)) {
|
|
43
44
|
let alias = null;
|
|
44
45
|
try { alias = _gaf(canonical); } catch {}
|
|
45
46
|
if (!alias && String(canonical).includes("/")) alias = String(canonical).replace(/\//g, "-");
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
|
+
import { compareModelIds, providerOf, providerRank } from "./list-sort.js";
|
|
4
5
|
|
|
5
6
|
/** 构建 id → 别名映射(原 list 两处重复逻辑收敛) */
|
|
6
7
|
export function buildAliasMap(ids, getAliasForModel) {
|
|
@@ -57,22 +58,19 @@ export function renderFreeList({ ids, at, pickedIds, sortedProvs, groups, aliasM
|
|
|
57
58
|
}
|
|
58
59
|
}
|
|
59
60
|
|
|
60
|
-
/** 按供应商分组 + 排序(opencode
|
|
61
|
+
/** 按供应商分组 + 排序(opencode 段最前,已知供应商优先,组内字母序) */
|
|
61
62
|
export function groupByProvider(ids) {
|
|
62
63
|
const groups = {};
|
|
63
64
|
for (const id of ids) {
|
|
64
|
-
const prov =
|
|
65
|
+
const prov = providerOf(id);
|
|
65
66
|
if (!groups[prov]) groups[prov] = [];
|
|
66
67
|
groups[prov].push(id);
|
|
67
68
|
}
|
|
68
|
-
const
|
|
69
|
+
for (const key of Object.keys(groups)) groups[key].sort(compareModelIds);
|
|
69
70
|
const sortedProvs = Object.keys(groups).sort((a, b) => {
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if (ib === -1) return -1;
|
|
74
|
-
return ia - ib;
|
|
75
|
-
}
|
|
71
|
+
const ra = providerRank(a);
|
|
72
|
+
const rb = providerRank(b);
|
|
73
|
+
if (ra !== rb) return ra - rb;
|
|
76
74
|
return a.localeCompare(b);
|
|
77
75
|
});
|
|
78
76
|
return { groups, sortedProvs };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// `-models` / `-model list` 展示排序:opencode free 优先 → 其余供应商分组 → 组内字母序。
|
|
2
|
+
// 单一来源:本文件被 list.js(交互候选)与 list-render.js(分组渲染)共用。
|
|
3
|
+
// Note: 排序只影响展示顺序,不改 picks/候选集合 — 见 .agents/notes/implemented/feature/2026-09-28-models-sort-opencode-free-first.md
|
|
4
|
+
import { isFreeModel } from "../../../models.js";
|
|
5
|
+
|
|
6
|
+
/** 供应商展示优先级(opencode 恒第一;未知供应商按字母序排其后) */
|
|
7
|
+
export const PROVIDER_ORDER = ["opencode", "workbuddy", "cline", "openrouter", "qoder", "codearts", "traework", "bai"];
|
|
8
|
+
|
|
9
|
+
/** 取模型 id 的供应商前缀;无斜杠的裸 id 归 opencode */
|
|
10
|
+
export function providerOf(id) {
|
|
11
|
+
const s = String(id || "");
|
|
12
|
+
const i = s.indexOf("/");
|
|
13
|
+
return i > 0 ? s.slice(0, i).toLowerCase() : "opencode";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** 供应商排序权重:opencode=0,已知供应商=其序号+1,未知=大数(按字母序兜底) */
|
|
17
|
+
export function providerRank(prov) {
|
|
18
|
+
const i = PROVIDER_ORDER.indexOf(prov);
|
|
19
|
+
return i === -1 ? PROVIDER_ORDER.length : i;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 模型 id 排序比较器:
|
|
24
|
+
* 1) opencode free(isFreeModel)最前 —— 用户主诉求
|
|
25
|
+
* 2) opencode 非 free 次之
|
|
26
|
+
* 3) 其余供应商按 PROVIDER_ORDER,未知按字母序
|
|
27
|
+
* 4) 同段内 localeCompare 字母序(大小写不敏感兜底)
|
|
28
|
+
*/
|
|
29
|
+
export function compareModelIds(a, b) {
|
|
30
|
+
const pa = providerOf(a);
|
|
31
|
+
const pb = providerOf(b);
|
|
32
|
+
const fa = pa === "opencode" && isFreeModel(a);
|
|
33
|
+
const fb = pb === "opencode" && isFreeModel(b);
|
|
34
|
+
// 段位:0=opencode free,1=opencode 其他,2=其他供应商
|
|
35
|
+
const ga = fa ? 0 : pa === "opencode" ? 1 : 2;
|
|
36
|
+
const gb = fb ? 0 : pb === "opencode" ? 1 : 2;
|
|
37
|
+
if (ga !== gb) return ga - gb;
|
|
38
|
+
if (ga === 2) {
|
|
39
|
+
const ra = providerRank(pa);
|
|
40
|
+
const rb = providerRank(pb);
|
|
41
|
+
if (ra !== rb) return ra - rb;
|
|
42
|
+
if (pa !== pb) return pa.localeCompare(pb);
|
|
43
|
+
}
|
|
44
|
+
const la = String(a).toLowerCase();
|
|
45
|
+
const lb = String(b).toLowerCase();
|
|
46
|
+
return la === lb ? String(a).localeCompare(String(b)) : la.localeCompare(lb);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** 返回排序后的新数组(不改原数组) */
|
|
50
|
+
export function sortModelIds(ids) {
|
|
51
|
+
return [...(ids || [])].sort(compareModelIds);
|
|
52
|
+
}
|