mslxdff 0.1.54 → 0.1.56
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/README.md +1 -160
- package/bin/mslxdff.js +759 -36
- package/package.json +3 -2
- package/src/auto.js +20 -2
- package/src/chat/config.js +10 -0
- package/src/chat/index.js +6 -0
- package/src/chat/prompt.js +63 -0
- package/src/chat/repl.js +245 -0
- package/src/chat/spinner.js +38 -0
- package/src/chat/stats.js +170 -0
- package/src/chat/store.js +48 -0
- package/src/chat/tools.js +304 -0
- package/src/chat/upstream.js +90 -0
- package/src/chooser.js +12 -6
- package/src/models.js +90 -2
- package/src/providers/dispatcher.js +100 -0
- package/src/providers/generic.js +221 -0
- package/src/providers/index.js +6 -0
- package/src/providers/keyring.js +38 -0
- package/src/providers/model-id.js +36 -0
- package/src/providers/opencode.js +20 -0
- package/src/providers/openrouter.js +217 -0
- package/src/providers/share-keys.js +53 -0
- package/src/providers/workbuddy-balance.js +76 -0
- package/src/providers/workbuddy.js +563 -0
- package/src/routes/chat/index.js +20 -1
- package/src/routes/peers.js +12 -7
- package/src/routes/stream.js +9 -0
- package/src/state.js +312 -4
- package/src/time.js +73 -0
- package/src/upstream.js +126 -8
- package/docs/adr/0001-reasoning-content-injection.md +0 -14
- package/docs/adr/0002-models-free-filter.md +0 -12
- package/docs/adr/0003-zero-state-no-auth.md +0 -10
- package/docs/adr/0004-bearer-token.md +0 -18
- package/docs/adr/0005-peer-mesh.md +0 -53
- package/docs/adr/0006-broadband-member.md +0 -103
- package/docs/agents/domain.md +0 -51
- package/docs/agents/issue-tracker.md +0 -30
- package/docs/agents/triage-labels.md +0 -15
- package/docs/plugins.md +0 -185
package/bin/mslxdff.js
CHANGED
|
@@ -8,7 +8,7 @@ import { DEFAULT_PORT, defaultStateFile } from "../src/state.js";
|
|
|
8
8
|
import { createRouter } from "../src/routes.js";
|
|
9
9
|
import { createUpstreamClient } from "../src/upstream.js";
|
|
10
10
|
import { createModelsService } from "../src/models.js";
|
|
11
|
-
import { loadToken, refreshToken, setPort, getPort, loadGroupsJoined, saveGroupsJoined, loadModelErrors, savePreferredModel, loadPreferredModel } from "../src/state.js";
|
|
11
|
+
import { loadToken, refreshToken, setPort, getPort, loadGroupsJoined, saveGroupsJoined, loadModelErrors, savePreferredModel, loadPreferredModel, loadModelPicks, saveModelPicks, loadProviderKey, loadProviderKeys, loadProviderAuths, loadProviderConfigs as loadProviderConfigsState } from "../src/state.js";
|
|
12
12
|
import { getPreferredModel } from "../src/auto.js";
|
|
13
13
|
import { normalizeModel } from "../src/reasoning.js";
|
|
14
14
|
import { syncToWorkbuddy, workbuddyModelsPath } from "../src/sync-workbuddy.js";
|
|
@@ -19,6 +19,8 @@ import { createEventBus } from "../src/events.js";
|
|
|
19
19
|
import { createGroupsService, createBansService, refreshGroupMembers, syncPeersFromMembers } from "../src/groups.js";
|
|
20
20
|
import { logDir, recentCalls, lastError, appendCall, appendError, appendEvent, recentEvents, eventsFile, callsFile, errorsFile } from "../src/logs.js";
|
|
21
21
|
import { loadPlugins, runHook, pluginsDir, resolvePluginDirs } from "../src/plugins.js";
|
|
22
|
+
import { createOpenCodeProvider } from "../src/providers/opencode.js";
|
|
23
|
+
import { fmtShanghai, fmtShanghaiYMDHM, fmtShanghaiHMS } from "../src/time.js";
|
|
22
24
|
|
|
23
25
|
const logs = { appendCall, appendError, appendEvent };
|
|
24
26
|
|
|
@@ -134,6 +136,17 @@ if (args.includes("-plugins") || args.includes("--plugins")) {
|
|
|
134
136
|
process.exit(0);
|
|
135
137
|
}
|
|
136
138
|
|
|
139
|
+
if (args.includes("-chat") || args.includes("--chat")) {
|
|
140
|
+
const idx = args.findIndex((x) => x === "-chat" || x === "--chat");
|
|
141
|
+
const rest = args.slice(idx + 1);
|
|
142
|
+
// rest 中若全是空白或以 - 开头的“选项”,仍视作本次对话的输入(单次模式);
|
|
143
|
+
// 无 rest 则进入常驻 REPL。REPL 本身就是前台独立进程,与 daemon 无父子关系,daemon 重启不影响它。
|
|
144
|
+
const singleShot = rest.length ? rest.join(" ").trim() : null;
|
|
145
|
+
const { startChat } = await import("../src/chat/index.js");
|
|
146
|
+
await startChat({ singleShot: singleShot || undefined });
|
|
147
|
+
process.exit(0);
|
|
148
|
+
}
|
|
149
|
+
|
|
137
150
|
if (args.includes("-status") || args.includes("--status") || args.includes("-s")) {
|
|
138
151
|
await printStatus();
|
|
139
152
|
process.exit(0);
|
|
@@ -174,9 +187,7 @@ if (args.includes("-model") || args.includes("-models")) {
|
|
|
174
187
|
const e = statuses[id];
|
|
175
188
|
const st = typeof e === "number" ? "error" : e?.status || "normal";
|
|
176
189
|
const at = typeof e === "number" ? e : e?.at;
|
|
177
|
-
const when = at
|
|
178
|
-
? ` (${new Date(at).toISOString().slice(5, 19).replace("T", " ")})`
|
|
179
|
-
: "";
|
|
190
|
+
const when = at ? ` (${fmtShanghai(at)})` : "";
|
|
180
191
|
const extra = e?.code ? ` HTTP ${e.code}` : "";
|
|
181
192
|
console.log(` ${id} ${st}${when}${extra}`);
|
|
182
193
|
}
|
|
@@ -185,11 +196,42 @@ if (args.includes("-model") || args.includes("-models")) {
|
|
|
185
196
|
if (sub === "set" && args[idx + 2]) {
|
|
186
197
|
const id = args[idx + 2];
|
|
187
198
|
savePreferredModel(id);
|
|
199
|
+
const picks = [...new Set([...loadModelPicks(), id])];
|
|
200
|
+
saveModelPicks(picks);
|
|
188
201
|
console.log(`default model set to: ${id} (daemon hot-reloads on next request)`);
|
|
202
|
+
console.log(`picked: ${picks.join(", ") || "(none)"} (auto will pick within these)`);
|
|
203
|
+
process.exit(0);
|
|
204
|
+
}
|
|
205
|
+
// -model pick <id> | -model unpick <id> | -model picks : 常用模型勾选集管理(非 TTY 用)
|
|
206
|
+
if (sub === "pick" && args[idx + 2] && args[idx + 2] !== "clear") {
|
|
207
|
+
const picks = [...new Set([...loadModelPicks(), args[idx + 2]])];
|
|
208
|
+
saveModelPicks(picks);
|
|
209
|
+
console.log(`picked: ${picks.join(", ") || "(none)"} (auto will pick within these)`);
|
|
210
|
+
process.exit(0);
|
|
211
|
+
}
|
|
212
|
+
if (sub === "pick" && args[idx + 2] === "clear") {
|
|
213
|
+
saveModelPicks([]);
|
|
214
|
+
console.log("picks cleared — auto uses the full model list again");
|
|
215
|
+
process.exit(0);
|
|
216
|
+
}
|
|
217
|
+
if (sub === "unpick" && args[idx + 2]) {
|
|
218
|
+
const picks = loadModelPicks().filter((x) => x !== args[idx + 2]);
|
|
219
|
+
saveModelPicks(picks);
|
|
220
|
+
console.log(`picked: ${picks.join(", ") || "(none)"}${picks.length === 0 ? " (auto uses full list)" : ""}`);
|
|
221
|
+
process.exit(0);
|
|
222
|
+
}
|
|
223
|
+
if (sub === "picks") {
|
|
224
|
+
const picks = loadModelPicks();
|
|
225
|
+
if (!picks.length) {
|
|
226
|
+
console.log("no picks — auto uses the full model list");
|
|
227
|
+
} else {
|
|
228
|
+
console.log(`${picks.length} picked model(s), auto only selects within these:`);
|
|
229
|
+
}
|
|
230
|
+
for (const id of picks) console.log(` ${id}`);
|
|
189
231
|
process.exit(0);
|
|
190
232
|
}
|
|
191
233
|
if (sub !== undefined && sub !== "list") {
|
|
192
|
-
console.error("usage: mslxdff -models (interactive
|
|
234
|
+
console.error("usage: mslxdff -models (interactive multi-pick) | mslxdff -model list | mslxdff -model set <id> | mslxdff -model pick <id> | mslxdff -model unpick <id> | mslxdff -model pick clear | mslxdff -model picks | mslxdff -model status | mslxdff -model refresh");
|
|
193
235
|
process.exit(1);
|
|
194
236
|
}
|
|
195
237
|
const cacheFile = join(logDir(), "models.json");
|
|
@@ -235,31 +277,35 @@ if (args.includes("-model") || args.includes("-models")) {
|
|
|
235
277
|
console.log("no models available — try: mslxdff -model refresh");
|
|
236
278
|
process.exit(0);
|
|
237
279
|
}
|
|
238
|
-
// TTY
|
|
280
|
+
// TTY:交互式多选勾选常用模型(空格勾选,Enter 保存);非 TTY(管道/脚本):保持纯列表并标注勾选
|
|
239
281
|
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
240
282
|
const statuses = loadModelErrors();
|
|
241
283
|
const current = getPreferredModel();
|
|
284
|
+
const pickedIds = loadModelPicks();
|
|
242
285
|
const items = ids.map((id) => {
|
|
243
286
|
const e = statuses[id];
|
|
244
287
|
return {
|
|
245
288
|
id,
|
|
246
289
|
status: typeof e === "number" ? "error" : e?.status || "normal",
|
|
247
290
|
current: id === current,
|
|
291
|
+
picked: pickedIds.includes(id),
|
|
248
292
|
};
|
|
249
293
|
});
|
|
250
|
-
const
|
|
251
|
-
if (!
|
|
252
|
-
console.log("cancelled —
|
|
294
|
+
const result = await pickInteractiveMulti(items, new Set(pickedIds), Math.max(0, items.findIndex((x) => x.current)));
|
|
295
|
+
if (!result) {
|
|
296
|
+
console.log("cancelled — picks unchanged");
|
|
253
297
|
process.exit(0);
|
|
254
298
|
}
|
|
255
|
-
|
|
256
|
-
console.log(`
|
|
299
|
+
saveModelPicks([...result]);
|
|
300
|
+
console.log(`saved ${result.size} picked model(s): ${[...result].join(", ") || "(none — auto uses full list)"}`);
|
|
257
301
|
process.exit(0);
|
|
258
302
|
}
|
|
259
|
-
const at = cachedAt ? ` (cached ${
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
console.log(
|
|
303
|
+
const at = cachedAt ? ` (cached ${fmtShanghaiYMDHM(cachedAt)})` : "";
|
|
304
|
+
const pickedIds = loadModelPicks();
|
|
305
|
+
const mark = (id) => (pickedIds.includes(id) ? "*" : " ");
|
|
306
|
+
console.log(`${ids.length} free model(s)${at} (${pickedIds.length} picked, * = picked):`);
|
|
307
|
+
for (const id of ids) console.log(` ${mark(id)} ${id}`);
|
|
308
|
+
console.log(`\npicked only constrains auto; manage with: mslxdff -models (TTY) | mslxdff -model pick <id> | mslxdff -model unpick <id> | mslxdff -model pick clear`);
|
|
263
309
|
} catch (err) {
|
|
264
310
|
console.error(`could not fetch models: ${String(err?.message || err)}`);
|
|
265
311
|
process.exit(1);
|
|
@@ -335,6 +381,556 @@ if (args.includes("-setto") || args.includes("--setto")) {
|
|
|
335
381
|
process.exit(0);
|
|
336
382
|
}
|
|
337
383
|
|
|
384
|
+
// -workbuddy checkin/balance/list/remove : WorkBuddy 多号管理
|
|
385
|
+
if (args.includes("-workbuddy") || args.includes("--workbuddy") || args.includes("-wb")) {
|
|
386
|
+
const idx = args.findIndex((x) => x === "-workbuddy" || x === "--workbuddy" || x === "-wb");
|
|
387
|
+
const sub = args[idx + 1];
|
|
388
|
+
if (!sub || sub === "checkin" || sub === "daily-checkin" || sub === "check-in") {
|
|
389
|
+
const { spawn } = await import("node:child_process");
|
|
390
|
+
const script = join(dirname(fileURLToPath(import.meta.url)), "..", "workbuddy-checkin.js");
|
|
391
|
+
const child = spawn(process.execPath, [script, ...args.slice(idx + 2)], { stdio: "inherit" });
|
|
392
|
+
child.on("close", (code) => process.exit(code ?? 0));
|
|
393
|
+
child.on("error", (err) => { console.error(`workbuddy checkin failed: ${err.message}`); process.exit(1); });
|
|
394
|
+
await new Promise(() => {});
|
|
395
|
+
} else if (sub === "balance" || sub === "balances" || sub === "credit" || sub === "credits") {
|
|
396
|
+
const asJson = args.includes("--json") || args.includes("-json");
|
|
397
|
+
const { loadProviderConfigs } = await import("../src/state.js");
|
|
398
|
+
const { fetchBalance } = await import("../src/providers/workbuddy-balance.js");
|
|
399
|
+
const cfg = loadProviderConfigs().workbuddy || {};
|
|
400
|
+
const auths = Array.isArray(cfg.auths) ? cfg.auths : [];
|
|
401
|
+
const keys = Array.isArray(cfg.keys) ? cfg.keys : [];
|
|
402
|
+
// fallback scan auths dir if state empty
|
|
403
|
+
let items = auths.map((a,i)=> ({ uid:a.uid, domain:a.domain, key: keys[i]||"" , auth:a }));
|
|
404
|
+
if (!items.length) {
|
|
405
|
+
try {
|
|
406
|
+
const { readdirSync, readFileSync, existsSync } = await import("node:fs");
|
|
407
|
+
const { join } = await import("node:path");
|
|
408
|
+
const dir = process.env.WORKBUDDY_AUTH_DIR || join(process.cwd(), "auths");
|
|
409
|
+
if (existsSync(dir)) {
|
|
410
|
+
for (const f of readdirSync(dir).filter(x=>x.startsWith("workbuddy-")&&x.endsWith(".json"))) {
|
|
411
|
+
try { const j=JSON.parse(readFileSync(join(dir,f),"utf8")); if(j?.account?.uid) items.push({uid:j.account.uid, domain:j.auth.domain||"www.codebuddy.cn", key:j.auth.accessToken, auth:{uid:j.account.uid, domain:j.auth.domain||"www.codebuddy.cn", enterpriseId:j.account.enterpriseId||""}}); } catch {}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
} catch {}
|
|
415
|
+
}
|
|
416
|
+
if (!items.length) { console.log("no workbuddy accounts — run node workbuddy-token-auto.js"); process.exit(0); }
|
|
417
|
+
const results=[];
|
|
418
|
+
for (const it of items) {
|
|
419
|
+
const b = await fetchBalance({ uid: it.uid, key: it.key, auth: it.auth }).catch(()=>null);
|
|
420
|
+
results.push({ uid: it.uid, domain: it.domain, balance: b });
|
|
421
|
+
}
|
|
422
|
+
if (asJson) { console.log(JSON.stringify({ results }, null, 2)); process.exit(0); }
|
|
423
|
+
console.log(`workbuddy balances (${results.length}):`);
|
|
424
|
+
for (const r of results) {
|
|
425
|
+
const b=r.balance;
|
|
426
|
+
if (!b) console.log(` ${r.uid} (balance unavailable) domain=${r.domain}`);
|
|
427
|
+
else console.log(` ${r.uid} total=${b.totalStr||b.total} dailyPacks=${b.dailyPacks} active=${b.activeCount} nextExpire=${b.nextExpire||"-"} domain=${r.domain}`);
|
|
428
|
+
}
|
|
429
|
+
process.exit(0);
|
|
430
|
+
} else if (sub === "list" || sub === "ls" || sub === "status") {
|
|
431
|
+
const { loadProviderConfigs } = await import("../src/state.js");
|
|
432
|
+
const cfg = loadProviderConfigs().workbuddy || {};
|
|
433
|
+
const auths = Array.isArray(cfg.auths) ? cfg.auths : [];
|
|
434
|
+
const keys = Array.isArray(cfg.keys) ? cfg.keys : [];
|
|
435
|
+
if (!auths.length) { console.log("no workbuddy accounts in state — check auths/workbuddy-*.json"); process.exit(0); }
|
|
436
|
+
console.log(`workbuddy accounts (${auths.length}):`);
|
|
437
|
+
auths.forEach((a,i)=> {
|
|
438
|
+
const k=(keys[i]||"").slice(0,4);
|
|
439
|
+
console.log(` [${i+1}] uid=${a.uid} domain=${a.domain||"www.codebuddy.cn"} enterprise=${a.enterpriseId||"-"} key=${k?k+"…":"(none)"} refresh=${a.refreshToken?"yes":"no"}`);
|
|
440
|
+
});
|
|
441
|
+
process.exit(0);
|
|
442
|
+
} else if (sub === "remove" || sub === "rm" || sub === "del" || sub === "delete") {
|
|
443
|
+
const target = args[idx+2];
|
|
444
|
+
if (!target) { console.error("usage: mslxdff -workbuddy remove <uid> [--keep-file]"); process.exit(1); }
|
|
445
|
+
const keep = args.includes("--keep-file");
|
|
446
|
+
const { loadProviderConfigs, saveProviderConfig } = await import("../src/state.js");
|
|
447
|
+
const cfg = loadProviderConfigs().workbuddy || {};
|
|
448
|
+
let auths = Array.isArray(cfg.auths)? [...cfg.auths]:[];
|
|
449
|
+
let keys = Array.isArray(cfg.keys)? [...cfg.keys]:[];
|
|
450
|
+
// support prefix match 6 chars
|
|
451
|
+
let rmIdx = auths.findIndex(a=> a.uid===target || a.uid.startsWith(target));
|
|
452
|
+
if (rmIdx<0) { console.error(`uid not found: ${target}`); process.exit(1); }
|
|
453
|
+
const uid = auths[rmIdx].uid;
|
|
454
|
+
auths.splice(rmIdx,1); keys.splice(rmIdx,1);
|
|
455
|
+
saveProviderConfig("workbuddy", { baseUrl: cfg.baseUrl||"https://copilot.tencent.com", keys, auths });
|
|
456
|
+
if (!keep) {
|
|
457
|
+
try {
|
|
458
|
+
const { existsSync, unlinkSync } = await import("node:fs");
|
|
459
|
+
const { join } = await import("node:path");
|
|
460
|
+
const dir = process.env.WORKBUDDY_AUTH_DIR || join(process.cwd(), "auths");
|
|
461
|
+
const fp = join(dir, `workbuddy-${uid}.json`);
|
|
462
|
+
if (existsSync(fp)) { unlinkSync(fp); console.log(`removed file ${fp}`); }
|
|
463
|
+
} catch {}
|
|
464
|
+
}
|
|
465
|
+
try { const { getBalanceCache } = await import("../src/providers/workbuddy-balance.js"); getBalanceCache().delete(uid); } catch {}
|
|
466
|
+
console.log(`removed workbuddy ${uid} (now ${auths.length} account(s))`);
|
|
467
|
+
process.exit(0);
|
|
468
|
+
} else {
|
|
469
|
+
console.error("usage: mslxdff -workbuddy checkin | balance [--json] | list | remove <uid>");
|
|
470
|
+
process.exit(1);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// -providers list : list all deployed upstream providers (opencode + openrouter + generic)
|
|
475
|
+
if (args.includes("-providers") || args.includes("--providers")) {
|
|
476
|
+
const idx = args.findIndex((x) => x === "-providers" || x === "--providers");
|
|
477
|
+
const sub = args[idx + 1];
|
|
478
|
+
if (!sub || sub === "list" || sub === "status") {
|
|
479
|
+
const { loadProviderConfigs, loadProviderKeys, loadProviderShareKeys, loadProviderBaseUrl, loadProviderAllowedModels } = await import("../src/state.js");
|
|
480
|
+
const configs = loadProviderConfigs();
|
|
481
|
+
const opencodeEnabled = true;
|
|
482
|
+
const opencodeBase = process.env.UPSTREAM_BASE_URL || "https://opencode.ai";
|
|
483
|
+
const orKeys = loadProviderKeys("openrouter");
|
|
484
|
+
const orBase = "https://openrouter.ai/api/v1";
|
|
485
|
+
const orShare = loadProviderShareKeys("openrouter");
|
|
486
|
+
const orAllowed = loadProviderAllowedModels("openrouter");
|
|
487
|
+
// collect generic ids from configs + legacy providerKeys
|
|
488
|
+
const genericIds = new Set(Object.keys(configs).filter((id) => id !== "opencode" && id !== "openrouter"));
|
|
489
|
+
try {
|
|
490
|
+
const raw = JSON.parse(readFileSync(defaultStateFile(), "utf8"));
|
|
491
|
+
const pk = raw.providerKeys || {};
|
|
492
|
+
for (const id of Object.keys(pk)) if (id !== "opencode" && id !== "openrouter") genericIds.add(id);
|
|
493
|
+
const cfgRaw = raw.providerConfigs || {};
|
|
494
|
+
for (const id of Object.keys(cfgRaw)) if (id !== "opencode" && id !== "openrouter") genericIds.add(id);
|
|
495
|
+
} catch {}
|
|
496
|
+
// also include env-only generic ids (scan env for MSLXDFF_*_KEY)
|
|
497
|
+
for (const k of Object.keys(process.env)) {
|
|
498
|
+
const m = k.match(/^MSLXDFF_(.+)_KEY$/);
|
|
499
|
+
if (m) {
|
|
500
|
+
const id = m[1].toLowerCase().replace(/__/g, "-");
|
|
501
|
+
if (id !== "openrouter" && id !== "opencode") genericIds.add(id);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
// include providers that only have allowlist (no keys yet) — exclude built-ins already listed
|
|
505
|
+
try {
|
|
506
|
+
const raw = JSON.parse(readFileSync(defaultStateFile(), "utf8"));
|
|
507
|
+
const cfgs = raw.providerConfigs || {};
|
|
508
|
+
for (const id of Object.keys(cfgs)) {
|
|
509
|
+
if (id === "opencode" || id === "openrouter") continue;
|
|
510
|
+
const am = cfgs[id]?.allowedModels;
|
|
511
|
+
if (Array.isArray(am) && am.length) genericIds.add(id);
|
|
512
|
+
}
|
|
513
|
+
} catch {}
|
|
514
|
+
const list = [];
|
|
515
|
+
const opAllowed = loadProviderAllowedModels("opencode");
|
|
516
|
+
list.push({ id: "opencode", enabled: opencodeEnabled, baseUrl: opencodeBase, keys: [], share: false, allowed: opAllowed, note: "built-in, no key, cannot share" });
|
|
517
|
+
list.push({ id: "openrouter", enabled: orKeys.length > 0, baseUrl: orBase, keys: orKeys, share: orShare, allowed: orAllowed, note: orKeys.length ? "" : "no keys" });
|
|
518
|
+
for (const gid of [...genericIds].sort()) {
|
|
519
|
+
const cfg = configs[gid];
|
|
520
|
+
const keys = loadProviderKeys(gid);
|
|
521
|
+
const baseUrl = loadProviderBaseUrl(gid) || cfg?.baseUrl || "";
|
|
522
|
+
const share = loadProviderShareKeys(gid);
|
|
523
|
+
const allowed = loadProviderAllowedModels(gid);
|
|
524
|
+
const enabled = Boolean(baseUrl && keys.length);
|
|
525
|
+
let note = "";
|
|
526
|
+
if (!baseUrl && !keys.length && !allowed.length) note = "no baseUrl, no keys";
|
|
527
|
+
else if (!baseUrl && !allowed.length) note = "missing baseUrl";
|
|
528
|
+
else if (!keys.length && !allowed.length) note = "no keys";
|
|
529
|
+
else if (!baseUrl) note = "missing baseUrl";
|
|
530
|
+
else if (!keys.length) note = "no keys";
|
|
531
|
+
list.push({ id: gid, enabled, baseUrl: baseUrl || "(none)", keys, share, allowed, note });
|
|
532
|
+
}
|
|
533
|
+
console.log(`providers (${list.length}):`);
|
|
534
|
+
for (const p of list) {
|
|
535
|
+
const state = p.enabled ? "enabled " : "disabled";
|
|
536
|
+
const keysInfo = p.keys.length ? `${p.keys.length} key${p.keys.length > 1 ? "s" : ""} ${p.keys.map((k) => `${k.slice(0, 4)}…${k.slice(-4)}`).join(", ")}` : "0 keys";
|
|
537
|
+
const shareInfo = p.id === "opencode" ? "cannot share" : `share=${p.share ? "ON" : "off"}`;
|
|
538
|
+
const allowInfo = p.allowed.length ? `allow=${p.allowed.length}(${p.allowed.slice(0, 3).join(",")}${p.allowed.length > 3 ? "..." : ""})` : "allow=all";
|
|
539
|
+
const note = p.note ? ` (${p.note})` : "";
|
|
540
|
+
console.log(` ${p.id.padEnd(12)} ${state} ${keysInfo.padEnd(28)} ${allowInfo.padEnd(22)} baseUrl=${p.baseUrl} ${shareInfo}${note}`);
|
|
541
|
+
}
|
|
542
|
+
console.log(`\nuse: mslxdff -provider <id> list to inspect one, mslxdff -provider <id> allowlist set <model...> to restrict`);
|
|
543
|
+
process.exit(0);
|
|
544
|
+
}
|
|
545
|
+
console.error("usage: mslxdff -providers list");
|
|
546
|
+
process.exit(1);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// -provider <id> [key...|add|remove|list|clear]: 配置需鉴权的供应商 API key(如 openrouter)。
|
|
550
|
+
// 行为:无参数且 TTY → 交互式隐藏输入(多行直到空行,逐一追加);非 TTY → 打印用法。key 持久化到 state。
|
|
551
|
+
if (args.includes("-provider") || args.includes("--provider")) {
|
|
552
|
+
const idx = args.findIndex((x) => x === "-provider" || x === "--provider");
|
|
553
|
+
const id = args[idx + 1];
|
|
554
|
+
const sub = args[idx + 2];
|
|
555
|
+
const rest = args.slice(idx + 2);
|
|
556
|
+
if (!id) {
|
|
557
|
+
console.error("usage: mslxdff -provider <id> [key...|add|remove|list|clear|share|set-url|allowlist]");
|
|
558
|
+
console.error(" e.g. mslxdff -provider openrouter sk-1 sk-2 sk-3 set multiple keys (replaces all)");
|
|
559
|
+
console.error(" mslxdff -provider openrouter add sk-4 append one key");
|
|
560
|
+
console.error(" mslxdff -provider openrouter remove sk-1 remove a key by value");
|
|
561
|
+
console.error(" mslxdff -provider openrouter list list all keys (masked)");
|
|
562
|
+
console.error(" mslxdff -provider openrouter share on|off share keys with peers on outgoing forward (ADR-0008)");
|
|
563
|
+
console.error(" mslxdff -provider openrouter set-url https://api.example.com/v1");
|
|
564
|
+
console.error(" mslxdff -provider openrouter allowlist set gpt-4 gpt-3.5 manage allowed models (empty=allow all)");
|
|
565
|
+
console.error(" mslxdff -provider add myapi https://api.example.com/v1 sk-xxx add generic OpenAI-compatible provider");
|
|
566
|
+
console.error(" mslxdff -provider add myapi https://api.example.com/v1 sk-xxx gpt-4 add with allowlist");
|
|
567
|
+
console.error(" mslxdff -provider openrouter interactive hidden input (append)");
|
|
568
|
+
console.error(" mslxdff -provider openrouter clear remove all keys");
|
|
569
|
+
process.exit(1);
|
|
570
|
+
}
|
|
571
|
+
// alias: mslxdff -provider list (same as -providers list) — list all providers
|
|
572
|
+
if (id === "list" || id === "status") {
|
|
573
|
+
const { loadProviderConfigs, loadProviderKeys, loadProviderShareKeys, loadProviderBaseUrl } = await import("../src/state.js");
|
|
574
|
+
const configs = loadProviderConfigs();
|
|
575
|
+
const opencodeBase = process.env.UPSTREAM_BASE_URL || "https://opencode.ai";
|
|
576
|
+
const orKeys = loadProviderKeys("openrouter");
|
|
577
|
+
const orBase = "https://openrouter.ai/api/v1";
|
|
578
|
+
const orShare = loadProviderShareKeys("openrouter");
|
|
579
|
+
const genericIds = new Set(Object.keys(configs).filter((x) => x !== "opencode" && x !== "openrouter"));
|
|
580
|
+
try {
|
|
581
|
+
const raw = JSON.parse(readFileSync(defaultStateFile(), "utf8"));
|
|
582
|
+
const pk = raw.providerKeys || {};
|
|
583
|
+
for (const k of Object.keys(pk)) if (k !== "opencode" && k !== "openrouter") genericIds.add(k);
|
|
584
|
+
} catch {}
|
|
585
|
+
for (const k of Object.keys(process.env)) {
|
|
586
|
+
const m = k.match(/^MSLXDFF_(.+)_KEY$/);
|
|
587
|
+
if (m) {
|
|
588
|
+
const gid = m[1].toLowerCase().replace(/__/g, "-");
|
|
589
|
+
if (gid !== "openrouter" && gid !== "opencode") genericIds.add(gid);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
const list = [];
|
|
593
|
+
list.push({ id: "opencode", enabled: true, baseUrl: opencodeBase, keys: [], share: false, note: "built-in, no key, cannot share" });
|
|
594
|
+
list.push({ id: "openrouter", enabled: orKeys.length > 0, baseUrl: orBase, keys: orKeys, share: orShare, note: orKeys.length ? "" : "no keys" });
|
|
595
|
+
for (const gid of [...genericIds].sort()) {
|
|
596
|
+
const cfg = configs[gid];
|
|
597
|
+
const keys = loadProviderKeys(gid);
|
|
598
|
+
const baseUrl = loadProviderBaseUrl(gid) || cfg?.baseUrl || "";
|
|
599
|
+
const share = loadProviderShareKeys(gid);
|
|
600
|
+
const enabled = Boolean(baseUrl && keys.length);
|
|
601
|
+
let note = "";
|
|
602
|
+
if (!baseUrl && !keys.length) note = "no baseUrl, no keys";
|
|
603
|
+
else if (!baseUrl) note = "missing baseUrl";
|
|
604
|
+
else if (!keys.length) note = "no keys";
|
|
605
|
+
list.push({ id: gid, enabled, baseUrl: baseUrl || "(none)", keys, share, note });
|
|
606
|
+
}
|
|
607
|
+
console.log(`providers (${list.length}):`);
|
|
608
|
+
for (const p of list) {
|
|
609
|
+
const state = p.enabled ? "enabled " : "disabled";
|
|
610
|
+
const keysInfo = p.keys.length ? `${p.keys.length} key${p.keys.length > 1 ? "s" : ""} ${p.keys.map((k) => `${k.slice(0, 4)}…${k.slice(-4)}`).join(", ")}` : "0 keys";
|
|
611
|
+
const shareInfo = p.id === "opencode" ? "cannot share" : `share=${p.share ? "ON" : "off"}`;
|
|
612
|
+
const note = p.note ? ` (${p.note})` : "";
|
|
613
|
+
console.log(` ${p.id.padEnd(12)} ${state} ${keysInfo.padEnd(28)} baseUrl=${p.baseUrl} ${shareInfo}${note}`);
|
|
614
|
+
}
|
|
615
|
+
console.log(`\nuse: mslxdff -provider <id> list to inspect one, mslxdff -provider add <id> <baseUrl> <key> to add generic`);
|
|
616
|
+
process.exit(0);
|
|
617
|
+
}
|
|
618
|
+
// 通用供应商快捷添加:mslxdff -provider add <id> <baseUrl> <key> [allowedModel...]
|
|
619
|
+
if (id === "add") {
|
|
620
|
+
const gid = sub;
|
|
621
|
+
const gBase = rest[1];
|
|
622
|
+
const gKey = rest[2];
|
|
623
|
+
if (!gid || !gBase || !gKey) {
|
|
624
|
+
console.error("usage: mslxdff -provider add <id> <baseUrl> <key> [allowedModel...]");
|
|
625
|
+
console.error(" e.g. mslxdff -provider add myapi https://api.example.com/v1 sk-xxx");
|
|
626
|
+
console.error(" mslxdff -provider add myapi https://api.example.com/v1 sk-xxx gpt-4 gpt-3.5");
|
|
627
|
+
process.exit(1);
|
|
628
|
+
}
|
|
629
|
+
if (gid === "opencode" || gid === "oc" || gid === "openrouter") {
|
|
630
|
+
console.error(`provider "${gid}" is built-in — use: mslxdff -provider ${gid} add <key> or mslxdff -provider ${gid} set-url <url>`);
|
|
631
|
+
process.exit(1);
|
|
632
|
+
}
|
|
633
|
+
const { saveProviderConfig, loadProviderConfig, loadProviderShareKeys, loadProviderAllowedModels } = await import("../src/state.js");
|
|
634
|
+
const { normalizeProviderId } = await import("../src/providers/model-id.js");
|
|
635
|
+
const nid = normalizeProviderId(gid);
|
|
636
|
+
if (!nid) { console.error(`invalid provider id: ${gid}`); process.exit(1); }
|
|
637
|
+
if (!/^https?:\/\/.+/.test(String(gBase).trim())) { console.error(`invalid baseUrl: ${gBase} (must start with http:// or https://)`); process.exit(1); }
|
|
638
|
+
const cur = loadProviderConfig(nid) || { baseUrl: "", keys: [], allowedModels: [], auths: [] };
|
|
639
|
+
let keys, auths, baseUrl;
|
|
640
|
+
baseUrl = String(gBase).trim();
|
|
641
|
+
const extraModels = rest.slice(3).filter((x) => x && !String(x).startsWith("-")).map((m) => String(m).trim()).filter(Boolean);
|
|
642
|
+
const allowedModels = extraModels.length ? [...new Set([...(cur.allowedModels || []), ...extraModels])] : (cur.allowedModels || []);
|
|
643
|
+
if (nid === "workbuddy") {
|
|
644
|
+
// workbuddy: keys/auths 一一对应,需解析 uid
|
|
645
|
+
const token = String(gKey).trim();
|
|
646
|
+
let uid = "";
|
|
647
|
+
try { const payload = JSON.parse(Buffer.from(token.split(".")[1], "base64").toString()); uid = payload.uid || payload.sub || payload.userId || payload.user_id || ""; } catch {}
|
|
648
|
+
if (!uid) uid = "manual-" + token.slice(-8);
|
|
649
|
+
const curKeys = Array.isArray(cur.keys) ? [...cur.keys] : [];
|
|
650
|
+
const curAuths = Array.isArray(cur.auths) ? [...cur.auths] : [];
|
|
651
|
+
let idx = curAuths.findIndex(a => a.uid === uid);
|
|
652
|
+
if (idx < 0) idx = curKeys.findIndex(k => k === token);
|
|
653
|
+
let newKeys, newAuths;
|
|
654
|
+
if (idx >= 0) {
|
|
655
|
+
newKeys = [...curKeys]; newKeys[idx] = token;
|
|
656
|
+
newAuths = [...curAuths]; newAuths[idx] = { uid, domain: "www.codebuddy.cn", enterpriseId: "", refreshToken: "" };
|
|
657
|
+
// if auths shorter than keys, pad
|
|
658
|
+
while (newAuths.length < newKeys.length) newAuths.push({ uid: `manual-${newKeys[newAuths.length].slice(-8)}`, domain: "www.codebuddy.cn", enterpriseId: "", refreshToken: "" });
|
|
659
|
+
} else {
|
|
660
|
+
newKeys = [...new Set([...curKeys, token].filter(Boolean))];
|
|
661
|
+
newAuths = [...curAuths, { uid, domain: "www.codebuddy.cn", enterpriseId: "", refreshToken: "" }];
|
|
662
|
+
// align lengths
|
|
663
|
+
while (newAuths.length < newKeys.length) newAuths.push({ uid: `manual-${newKeys[newAuths.length].slice(-8)}`, domain: "www.codebuddy.cn", enterpriseId: "", refreshToken: "" });
|
|
664
|
+
while (newKeys.length < newAuths.length) newKeys.push(token);
|
|
665
|
+
}
|
|
666
|
+
keys = newKeys; auths = newAuths;
|
|
667
|
+
saveProviderConfig(nid, { baseUrl, keys, auths, allowedModels });
|
|
668
|
+
// 同步写 auths/workbuddy-<uid>.json 供 checkin 使用
|
|
669
|
+
try {
|
|
670
|
+
const { writeFileSync, mkdirSync } = await import("node:fs");
|
|
671
|
+
const { join } = await import("node:path");
|
|
672
|
+
const dir = process.env.WORKBUDDY_AUTH_DIR || join(process.cwd(), "auths");
|
|
673
|
+
mkdirSync(dir, { recursive: true });
|
|
674
|
+
const fp = join(dir, `workbuddy-${uid}.json`);
|
|
675
|
+
const doc = { account: { uid, enterpriseId: "", nickname: "" }, auth: { accessToken: token, refreshToken: "", expiresAt: Math.floor(Date.now()/1000)+3600, domain: "www.codebuddy.cn" } };
|
|
676
|
+
writeFileSync(fp + ".tmp", JSON.stringify(doc, null, 2), { mode: 0o600 });
|
|
677
|
+
try { const { renameSync, unlinkSync, existsSync } = await import("node:fs"); if (existsSync(fp)) unlinkSync(fp); renameSync(fp + ".tmp", fp); } catch { writeFileSync(fp, JSON.stringify(doc, null, 2), { mode: 0o600 }); }
|
|
678
|
+
} catch {}
|
|
679
|
+
} else {
|
|
680
|
+
keys = [...new Set([...(cur.keys || []), String(gKey).trim()].filter(Boolean))];
|
|
681
|
+
auths = undefined;
|
|
682
|
+
saveProviderConfig(nid, { baseUrl, keys, allowedModels });
|
|
683
|
+
}
|
|
684
|
+
console.log(`added generic provider: ${nid}`);
|
|
685
|
+
console.log(` baseUrl: ${String(gBase).trim().replace(/\/+$/, "")}`);
|
|
686
|
+
console.log(` keys: ${keys.length} (${keys.map((k) => `${k.slice(0, 4)}…${k.slice(-4)}`).join(", ")})`);
|
|
687
|
+
if (allowedModels.length) console.log(` allowedModels: ${allowedModels.length} (${allowedModels.join(", ")})`);
|
|
688
|
+
else console.log(` allowedModels: (none — allow all, set via: mslxdff -provider ${nid} allowlist set <model...>)`);
|
|
689
|
+
console.log(` share: ${loadProviderShareKeys(nid) ? "ON" : "off"} (mslxdff -provider ${nid} share on|off)`);
|
|
690
|
+
console.log(` use as: ${nid}/<model-id> — restart daemon to activate`);
|
|
691
|
+
process.exit(0);
|
|
692
|
+
}
|
|
693
|
+
if ((id === "opencode" || id === "oc") && sub !== "allowlist" && sub !== "allow" && sub !== "allowed" && sub !== "whitelist" && sub !== "list" && sub !== "status") {
|
|
694
|
+
console.log("opencode is the default (bare) provider — it needs no API key and can never be shared with peers");
|
|
695
|
+
console.log("(its IP-based rate limit is spread by peer forwarding itself)");
|
|
696
|
+
console.log(` allowlist: mslxdff -provider opencode allowlist [list|set|add|remove|clear] — restrict models (empty=allow all)`);
|
|
697
|
+
process.exit(0);
|
|
698
|
+
}
|
|
699
|
+
const { loadProviderKeys, saveProviderKeys, addProviderKey, removeProviderKeys, loadProviderShareKeys, saveProviderShareKeys, loadProviderConfig, saveProviderConfig, saveProviderBaseUrl, loadProviderConfigs } = await import("../src/state.js");
|
|
700
|
+
if (sub === "clear") {
|
|
701
|
+
const configs = loadProviderConfigs();
|
|
702
|
+
if (configs[id]) {
|
|
703
|
+
saveProviderConfig(id, { baseUrl: "", keys: [] });
|
|
704
|
+
} else {
|
|
705
|
+
saveProviderKeys(id, []);
|
|
706
|
+
}
|
|
707
|
+
console.log(`cleared ${id} API keys (provider disabled on next daemon start)`);
|
|
708
|
+
process.exit(0);
|
|
709
|
+
}
|
|
710
|
+
if (sub === "set-url" || sub === "setUrl" || sub === "url") {
|
|
711
|
+
const url = rest[1];
|
|
712
|
+
if (!url) {
|
|
713
|
+
console.error(`usage: mslxdff -provider ${id} set-url <baseUrl>`);
|
|
714
|
+
process.exit(1);
|
|
715
|
+
}
|
|
716
|
+
if (!/^https?:\/\/.+/.test(String(url).trim())) {
|
|
717
|
+
console.error(`invalid baseUrl: ${url} (must start with http:// or https://)`);
|
|
718
|
+
process.exit(1);
|
|
719
|
+
}
|
|
720
|
+
const cur = loadProviderConfig(id) || { baseUrl: "", keys: [] };
|
|
721
|
+
saveProviderConfig(id, { baseUrl: String(url).trim(), keys: cur.keys || [] });
|
|
722
|
+
console.log(`set ${id} baseUrl: ${String(url).trim().replace(/\/+$/, "")} — restart daemon to activate`);
|
|
723
|
+
process.exit(0);
|
|
724
|
+
}
|
|
725
|
+
if (sub === "share") {
|
|
726
|
+
const on = rest[1];
|
|
727
|
+
if (!on) {
|
|
728
|
+
console.log(`share keys to peers: ${loadProviderShareKeys(id) ? "ON" : "off"}`);
|
|
729
|
+
process.exit(0);
|
|
730
|
+
}
|
|
731
|
+
if (!["on", "off", "1", "0", "true", "false"].includes(String(on).toLowerCase())) {
|
|
732
|
+
console.error("usage: mslxdff -provider openrouter share on|off");
|
|
733
|
+
process.exit(1);
|
|
734
|
+
}
|
|
735
|
+
const state = ["on", "1", "true"].includes(String(on).toLowerCase());
|
|
736
|
+
saveProviderShareKeys(id, state);
|
|
737
|
+
console.log(`share keys to peers: ${state ? "ON" : "off"} — restart daemon to activate`);
|
|
738
|
+
process.exit(0);
|
|
739
|
+
}
|
|
740
|
+
// allowlist 管理:白名单为空 = 不限;非空 = 仅名单内可用(防昂贵模型)
|
|
741
|
+
if (sub === "allowlist" || sub === "allow" || sub === "allowed" || sub === "whitelist") {
|
|
742
|
+
const { loadProviderAllowedModels, saveProviderAllowedModels, loadProviderConfig } = await import("../src/state.js");
|
|
743
|
+
const action = rest[1];
|
|
744
|
+
const rawTargets = rest.slice(2);
|
|
745
|
+
// 兼容:mslxdff -provider <id> allowlist (无 action)→ list
|
|
746
|
+
if (!action || action === "list" || action === "status") {
|
|
747
|
+
const list = loadProviderAllowedModels(id);
|
|
748
|
+
const cfg = loadProviderConfig(id);
|
|
749
|
+
const baseUrl = cfg?.baseUrl || "";
|
|
750
|
+
console.log(`provider: ${id}${baseUrl ? ` baseUrl: ${baseUrl}` : ""}`);
|
|
751
|
+
if (!list.length) {
|
|
752
|
+
console.log(` allowedModels: (none — allow all)`);
|
|
753
|
+
console.log(` set via: mslxdff -provider ${id} allowlist set <model1> <model2> ...`);
|
|
754
|
+
console.log(` add: mslxdff -provider ${id} allowlist add <model>`);
|
|
755
|
+
} else {
|
|
756
|
+
console.log(` allowedModels: ${list.length} model${list.length > 1 ? "s" : ""} (only these can be used)`);
|
|
757
|
+
list.forEach((m, i) => console.log(` [${i + 1}] ${m}`));
|
|
758
|
+
console.log(` manage: mslxdff -provider ${id} allowlist add <model> | remove <model> | set <m1> <m2> ... | clear`);
|
|
759
|
+
}
|
|
760
|
+
console.log(` NOTE: empty allowlist = allow all (hot-reloaded, no restart needed)`);
|
|
761
|
+
process.exit(0);
|
|
762
|
+
}
|
|
763
|
+
if (action === "clear") {
|
|
764
|
+
saveProviderAllowedModels(id, []);
|
|
765
|
+
console.log(`cleared ${id} allowlist (now allow all) — takes effect immediately (hot-reloaded)`);
|
|
766
|
+
process.exit(0);
|
|
767
|
+
}
|
|
768
|
+
if (action === "set") {
|
|
769
|
+
const models = rawTargets.filter((x) => x && !String(x).startsWith("-")).map((m) => String(m).trim()).filter(Boolean);
|
|
770
|
+
// 支持逗号分隔的一串:model1,model2
|
|
771
|
+
const flat = models.flatMap((m) => String(m).split(",")).map((m) => m.trim()).filter(Boolean);
|
|
772
|
+
if (!flat.length) {
|
|
773
|
+
console.error(`usage: mslxdff -provider ${id} allowlist set <model1> <model2> ...`);
|
|
774
|
+
process.exit(1);
|
|
775
|
+
}
|
|
776
|
+
const saved = saveProviderAllowedModels(id, flat);
|
|
777
|
+
console.log(`set ${id} allowlist: ${saved.length} model${saved.length > 1 ? "s" : ""} (${saved.join(", ")}) — takes effect immediately (hot-reloaded)`);
|
|
778
|
+
process.exit(0);
|
|
779
|
+
}
|
|
780
|
+
if (action === "add") {
|
|
781
|
+
const models = rawTargets.filter((x) => x && !String(x).startsWith("-")).map((m) => String(m).trim()).filter(Boolean);
|
|
782
|
+
const flat = models.flatMap((m) => String(m).split(",")).map((m) => m.trim()).filter(Boolean);
|
|
783
|
+
if (!flat.length) {
|
|
784
|
+
console.error(`usage: mslxdff -provider ${id} allowlist add <model> [model2 ...]`);
|
|
785
|
+
process.exit(1);
|
|
786
|
+
}
|
|
787
|
+
const cur = loadProviderAllowedModels(id);
|
|
788
|
+
const next = [...new Set([...cur, ...flat])];
|
|
789
|
+
saveProviderAllowedModels(id, next);
|
|
790
|
+
console.log(`added ${flat.length} model${flat.length > 1 ? "s" : ""} to ${id} allowlist (now ${next.length}: ${next.join(", ")}) — takes effect immediately (hot-reloaded)`);
|
|
791
|
+
process.exit(0);
|
|
792
|
+
}
|
|
793
|
+
if (action === "remove" || action === "rm" || action === "del") {
|
|
794
|
+
const models = rawTargets.filter((x) => x && !String(x).startsWith("-")).map((m) => String(m).trim()).filter(Boolean);
|
|
795
|
+
const flat = models.flatMap((m) => String(m).split(",")).map((m) => m.trim()).filter(Boolean);
|
|
796
|
+
if (!flat.length) {
|
|
797
|
+
console.error(`usage: mslxdff -provider ${id} allowlist remove <model> [model2 ...]`);
|
|
798
|
+
process.exit(1);
|
|
799
|
+
}
|
|
800
|
+
const cur = loadProviderAllowedModels(id);
|
|
801
|
+
const set = new Set(flat);
|
|
802
|
+
const next = cur.filter((m) => !set.has(m));
|
|
803
|
+
saveProviderAllowedModels(id, next);
|
|
804
|
+
console.log(`removed ${cur.length - next.length} model${cur.length - next.length !== 1 ? "s" : ""} from ${id} allowlist (now ${next.length ? next.join(", ") : "(allow all)"}) — takes effect immediately (hot-reloaded)`);
|
|
805
|
+
process.exit(0);
|
|
806
|
+
}
|
|
807
|
+
console.error(`usage: mslxdff -provider ${id} allowlist [list|set|add|remove|clear] [models...]`);
|
|
808
|
+
process.exit(1);
|
|
809
|
+
}
|
|
810
|
+
if (sub === "list" || sub === "status") {
|
|
811
|
+
const keys = loadProviderKeys(id);
|
|
812
|
+
const cfg = loadProviderConfig(id);
|
|
813
|
+
const { loadProviderAllowedModels } = await import("../src/state.js");
|
|
814
|
+
const allowed = loadProviderAllowedModels(id);
|
|
815
|
+
const baseUrl = cfg?.baseUrl || "";
|
|
816
|
+
if (baseUrl) console.log(`provider: ${id} baseUrl: ${baseUrl}`);
|
|
817
|
+
else console.log(`provider: ${id}${id === "openrouter" ? " (built-in baseUrl: https://openrouter.ai/api/v1)" : ""}`);
|
|
818
|
+
if (keys.length) {
|
|
819
|
+
console.log(` keys: ${keys.length} key${keys.length > 1 ? "s" : ""}`);
|
|
820
|
+
keys.forEach((k, i) => console.log(` [${i + 1}] ${k.slice(0, 4)}…${k.slice(-4)} (${k.length} chars)`));
|
|
821
|
+
console.log(` remove by: mslxdff -provider ${id} remove <seq> [seq...] | <key-value>`);
|
|
822
|
+
} else {
|
|
823
|
+
console.log(` keys: (no keys configured)`);
|
|
824
|
+
}
|
|
825
|
+
console.log(` share keys to peers: ${loadProviderShareKeys(id) ? "ON" : "off"} (mslxdff -provider ${id} share on|off)`);
|
|
826
|
+
if (allowed.length) console.log(` allowedModels: ${allowed.length} (${allowed.join(", ")}) — only these can be used`);
|
|
827
|
+
else console.log(` allowedModels: (none — allow all) (mslxdff -provider ${id} allowlist set <model...>)`);
|
|
828
|
+
if (baseUrl) console.log(` set url: mslxdff -provider ${id} set-url <baseUrl>`);
|
|
829
|
+
console.log(` NOTE: opencode is the default provider and can never be shared`);
|
|
830
|
+
process.exit(0);
|
|
831
|
+
}
|
|
832
|
+
if (sub === "add") {
|
|
833
|
+
const key = rest[1];
|
|
834
|
+
if (!key) {
|
|
835
|
+
console.error("usage: mslxdff -provider openrouter add <key>");
|
|
836
|
+
process.exit(1);
|
|
837
|
+
}
|
|
838
|
+
// 通用供应商:keys 存 providerConfigs,需保留 baseUrl
|
|
839
|
+
const configs = loadProviderConfigs();
|
|
840
|
+
if (configs[id]) {
|
|
841
|
+
const cur = loadProviderConfig(id) || { baseUrl: "", keys: [] };
|
|
842
|
+
const keys = [...new Set([...(cur.keys || []), String(key).trim()].filter(Boolean))];
|
|
843
|
+
saveProviderConfig(id, { baseUrl: cur.baseUrl || "", keys });
|
|
844
|
+
console.log(`added ${id} API key (now ${keys.length} total) — restart daemon to activate`);
|
|
845
|
+
} else {
|
|
846
|
+
const added = addProviderKey(id, key);
|
|
847
|
+
console.log(`added ${id} API key (now ${added.length} total) — restart daemon to activate`);
|
|
848
|
+
}
|
|
849
|
+
process.exit(0);
|
|
850
|
+
}
|
|
851
|
+
if (sub === "remove") {
|
|
852
|
+
const targets = rest.slice(1).filter((k) => !k.startsWith("-"));
|
|
853
|
+
if (!targets.length) {
|
|
854
|
+
console.error("usage: mslxdff -provider openrouter remove <seq|key> [seq|key ...] (seq = index shown by 'list')");
|
|
855
|
+
process.exit(1);
|
|
856
|
+
}
|
|
857
|
+
const current = loadProviderKeys(id);
|
|
858
|
+
const toRemove = [];
|
|
859
|
+
for (const raw of targets.flatMap((t) => String(t).split(","))) {
|
|
860
|
+
const t = raw.trim();
|
|
861
|
+
if (!t) continue;
|
|
862
|
+
if (/^\d+$/.test(t)) {
|
|
863
|
+
const seq = Number(t);
|
|
864
|
+
const idx = seq - 1;
|
|
865
|
+
if (Number.isInteger(seq) && idx >= 0 && idx < current.length) toRemove.push(current[idx]);
|
|
866
|
+
else console.log(` ! no key at sequence ${seq} (provider has ${current.length}) — skipped`);
|
|
867
|
+
} else {
|
|
868
|
+
toRemove.push(t);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
if (!toRemove.length) {
|
|
872
|
+
console.log("nothing to remove");
|
|
873
|
+
process.exit(0);
|
|
874
|
+
}
|
|
875
|
+
const configs = loadProviderConfigs();
|
|
876
|
+
let remaining;
|
|
877
|
+
if (configs[id]) {
|
|
878
|
+
const cur = loadProviderConfig(id) || { baseUrl: "", keys: [] };
|
|
879
|
+
const set = new Set([...new Set(toRemove)].map((k) => String(k).trim()));
|
|
880
|
+
const keys = (cur.keys || []).filter((k) => !set.has(k));
|
|
881
|
+
saveProviderConfig(id, { baseUrl: cur.baseUrl || "", keys });
|
|
882
|
+
remaining = keys;
|
|
883
|
+
} else {
|
|
884
|
+
remaining = removeProviderKeys(id, [...new Set(toRemove)]);
|
|
885
|
+
}
|
|
886
|
+
console.log(`removed ${current.length - remaining.length} ${id} API key(s) (now ${remaining.length} total) — restart daemon to activate`);
|
|
887
|
+
process.exit(0);
|
|
888
|
+
}
|
|
889
|
+
if (sub && !sub.startsWith("-")) {
|
|
890
|
+
const keys = rest.filter((k) => !k.startsWith("-"));
|
|
891
|
+
if (!keys.length) {
|
|
892
|
+
console.error("no key given");
|
|
893
|
+
process.exit(1);
|
|
894
|
+
}
|
|
895
|
+
const configs = loadProviderConfigs();
|
|
896
|
+
if (configs[id]) {
|
|
897
|
+
const cur = loadProviderConfig(id) || { baseUrl: "", keys: [] };
|
|
898
|
+
const clean = [...new Set(keys.map((k) => String(k).trim()).filter(Boolean))];
|
|
899
|
+
saveProviderConfig(id, { baseUrl: cur.baseUrl || "", keys: clean });
|
|
900
|
+
console.log(`set ${id} API keys (${clean.length}: ${clean.map((k) => `${k.slice(0, 4)}…${k.slice(-4)}`).join(", ")}) — restart daemon to activate`);
|
|
901
|
+
} else {
|
|
902
|
+
saveProviderKeys(id, keys);
|
|
903
|
+
console.log(`set ${id} API keys (${keys.length}: ${keys.map((k) => `${k.slice(0, 4)}…${k.slice(-4)}`).join(", ")}) — restart daemon to activate`);
|
|
904
|
+
}
|
|
905
|
+
process.exit(0);
|
|
906
|
+
}
|
|
907
|
+
// 交互式隐藏输入:多行直到空行,逐一追加到现有 keys
|
|
908
|
+
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
909
|
+
const readline = await import("node:readline/promises");
|
|
910
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
911
|
+
console.log(`Enter ${id} API keys, one per line (input hidden). Blank line to finish:`);
|
|
912
|
+
const existing = loadProviderKeys(id);
|
|
913
|
+
const collected = [];
|
|
914
|
+
for (;;) {
|
|
915
|
+
const key = await rl.question(existing.length || collected.length ? "" : "");
|
|
916
|
+
const clean = String(key || "").trim();
|
|
917
|
+
if (!clean) break;
|
|
918
|
+
collected.push(clean);
|
|
919
|
+
}
|
|
920
|
+
rl.close();
|
|
921
|
+
if (!collected.length) {
|
|
922
|
+
console.log("empty input — nothing changed");
|
|
923
|
+
process.exit(0);
|
|
924
|
+
}
|
|
925
|
+
for (const k of collected) addProviderKey(id, k);
|
|
926
|
+
const total = (await import("../src/state.js")).loadProviderKeys(id).length;
|
|
927
|
+
console.log(`added ${collected.length} ${id} API key(s) (now ${total} total) — restart daemon to activate`);
|
|
928
|
+
process.exit(0);
|
|
929
|
+
}
|
|
930
|
+
console.error("provide keys inline (non-TTY): mslxdff -provider openrouter <key1> [key2 ...]");
|
|
931
|
+
process.exit(1);
|
|
932
|
+
}
|
|
933
|
+
|
|
338
934
|
// 交互式选择器:↑/↓ 移动,Enter 确认,q/Esc 取消;ANSI 原地重绘
|
|
339
935
|
async function pickInteractive(items, startCursor = 0) {
|
|
340
936
|
const { renderChooser, renderChooserHelp, parseKey } = await import("../src/chooser.js");
|
|
@@ -375,6 +971,53 @@ async function pickInteractive(items, startCursor = 0) {
|
|
|
375
971
|
});
|
|
376
972
|
}
|
|
377
973
|
|
|
974
|
+
// 多选勾选:↑/↓ 移动,Space 勾选/取消,Enter 保存,q/Esc 取消(返回 Set 或 null)
|
|
975
|
+
async function pickInteractiveMulti(items, initialPicked = new Set(), startCursor = 0) {
|
|
976
|
+
const { renderChooser, renderChooserHelp, parseKey } = await import("../src/chooser.js");
|
|
977
|
+
let cursor = Math.min(Math.max(startCursor, 0), items.length - 1);
|
|
978
|
+
const picked = new Set(items.filter((it) => initialPicked.has(it.id)).map((it) => it.id));
|
|
979
|
+
const draw = () => {
|
|
980
|
+
const rows = items.map((it, i) => ({ ...it, picked: picked.has(it.id) }));
|
|
981
|
+
const lines = [...renderChooser(rows, cursor, { multi: true }), ...renderChooserHelp(true)];
|
|
982
|
+
process.stdout.write("\x1b[G\x1b[J" + lines.join("\n"));
|
|
983
|
+
};
|
|
984
|
+
draw();
|
|
985
|
+
return new Promise((resolve) => {
|
|
986
|
+
const wasRaw = process.stdin.isRaw;
|
|
987
|
+
process.stdin.setRawMode(true);
|
|
988
|
+
process.stdin.resume();
|
|
989
|
+
process.stdin.setEncoding("utf8");
|
|
990
|
+
const cleanup = () => {
|
|
991
|
+
process.stdin.removeListener("data", onData);
|
|
992
|
+
process.stdin.setRawMode(false);
|
|
993
|
+
process.stdin.pause();
|
|
994
|
+
process.stdout.write("\n");
|
|
995
|
+
};
|
|
996
|
+
const onData = (chunk) => {
|
|
997
|
+
const key = parseKey(String(chunk));
|
|
998
|
+
if (key === "up") {
|
|
999
|
+
cursor = (cursor - 1 + items.length) % items.length;
|
|
1000
|
+
draw();
|
|
1001
|
+
} else if (key === "down") {
|
|
1002
|
+
cursor = (cursor + 1) % items.length;
|
|
1003
|
+
draw();
|
|
1004
|
+
} else if (key === "space") {
|
|
1005
|
+
const id = items[cursor].id;
|
|
1006
|
+
if (picked.has(id)) picked.delete(id);
|
|
1007
|
+
else picked.add(id);
|
|
1008
|
+
draw();
|
|
1009
|
+
} else if (key === "enter") {
|
|
1010
|
+
cleanup();
|
|
1011
|
+
resolve(new Set(picked));
|
|
1012
|
+
} else if (key === "cancel") {
|
|
1013
|
+
cleanup();
|
|
1014
|
+
resolve(null);
|
|
1015
|
+
}
|
|
1016
|
+
};
|
|
1017
|
+
process.stdin.on("data", onData);
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
|
|
378
1021
|
// -debug: stop the background daemon and run the server in THIS terminal
|
|
379
1022
|
// (foreground), printing every event to stdout in real time via the in-memory
|
|
380
1023
|
// event bus — no filesystem polling. Ctrl+C / SIGTERM restarts the daemon in
|
|
@@ -849,33 +1492,110 @@ if (loadedPlugins.length) {
|
|
|
849
1492
|
console.log(`plugins loaded (${loadedPlugins.length}): ${loadedPlugins.map((p) => `${p.name}${p.version ? `@${p.version}` : ""}`).join(", ")}`);
|
|
850
1493
|
appendEvent({ ts: Date.now(), type: "plugins-loaded", plugins: loadedPlugins.map((p) => ({ name: p.name, version: p.version })) });
|
|
851
1494
|
}
|
|
1495
|
+
// 多供应商通道:默认 opencode + 可选 openrouter(MSLXDFF_OPENROUTER_KEY 启用)
|
|
1496
|
+
// 插件 createUpstream 仍是"整体替换式"外部 provider(单通道,保留原语义)
|
|
852
1497
|
const upstreamHooks = loadedPlugins.length
|
|
853
1498
|
? (name, ctx) => runHook(loadedPlugins, name, ctx)
|
|
854
1499
|
: null;
|
|
855
|
-
// 插件可提供 createUpstream(ctx) 整体替换上游(接任意 provider);取第一个声明者
|
|
856
1500
|
const providerPlugin = loadedPlugins.find((p) => typeof p.createUpstream === "function");
|
|
857
1501
|
let upstream;
|
|
1502
|
+
const baseUrl = process.env.UPSTREAM_BASE_URL || "https://opencode.ai";
|
|
1503
|
+
let providers = [];
|
|
858
1504
|
if (providerPlugin) {
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
1505
|
+
// 插件整体替换:沿用旧单通道路径
|
|
1506
|
+
if (!upstream) {
|
|
1507
|
+
try {
|
|
1508
|
+
upstream = await providerPlugin.createUpstream({ baseUrl, authToken: process.env.UPSTREAM_AUTH_TOKEN || "public", env: process.env });
|
|
1509
|
+
console.log(`upstream provider replaced by plugin: ${providerPlugin.name}`);
|
|
1510
|
+
appendEvent({ ts: Date.now(), type: "plugin-upstream-active", plugin: providerPlugin.name });
|
|
1511
|
+
} catch (err) {
|
|
1512
|
+
console.log(`plugin upstream (${providerPlugin.name}) failed: ${errMsg(err)} — falling back to default`);
|
|
1513
|
+
appendEvent({ ts: Date.now(), type: "plugin-upstream-error", plugin: providerPlugin.name, error: errMsg(err) });
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
if (!upstream) upstream = createUpstreamClient({ hooks: upstreamHooks });
|
|
1517
|
+
} else {
|
|
1518
|
+
// 内置多 Provider:opencode 恒启用,openrouter 有 key 才启用
|
|
1519
|
+
const opencodeClient = createUpstreamClient({ hooks: upstreamHooks });
|
|
1520
|
+
const opencodeModels = createModelsService({
|
|
1521
|
+
baseUrl,
|
|
1522
|
+
headers: opencodeClient.headers,
|
|
1523
|
+
refreshMs: refreshIntervalMs(),
|
|
1524
|
+
cacheFile: join(logDir(), "models.json"),
|
|
1525
|
+
});
|
|
1526
|
+
providers.push(createOpenCodeProvider({ upstream: opencodeClient, modelsService: opencodeModels }));
|
|
1527
|
+
const orKeys = loadProviderKeys("openrouter");
|
|
1528
|
+
if (orKeys.length) {
|
|
1529
|
+
const { createOpenRouterProvider } = await import("../src/providers/openrouter.js");
|
|
1530
|
+
providers.push(createOpenRouterProvider({ apiKeys: orKeys }));
|
|
1531
|
+
console.log(`provider enabled: openrouter (${orKeys.length} key${orKeys.length > 1 ? "s" : ""})`);
|
|
1532
|
+
appendEvent({ ts: Date.now(), type: "provider-enabled", provider: "openrouter", keys: orKeys.length });
|
|
1533
|
+
}
|
|
1534
|
+
// 通用 OpenAI 兼容供应商:读 providerConfigs,逐个创建 generic provider
|
|
1535
|
+
const { loadProviderConfigs } = await import("../src/state.js");
|
|
1536
|
+
const genericConfigs = loadProviderConfigs();
|
|
1537
|
+
for (const [gid, cfg] of Object.entries(genericConfigs)) {
|
|
1538
|
+
if (gid === "opencode" || gid === "openrouter") continue;
|
|
1539
|
+
if (gid === "workbuddy") {
|
|
1540
|
+
const base = String(cfg?.baseUrl || "").trim() || "https://copilot.tencent.com";
|
|
1541
|
+
const keys = Array.isArray(cfg?.keys) ? cfg.keys.filter((k) => typeof k === "string" && k.trim()) : [];
|
|
1542
|
+
const auths = Array.isArray(cfg?.auths) ? cfg.auths : [];
|
|
1543
|
+
if (!keys.length) continue;
|
|
1544
|
+
try {
|
|
1545
|
+
const { createWorkbuddyProvider } = await import("../src/providers/workbuddy.js");
|
|
1546
|
+
providers.push(createWorkbuddyProvider({ baseUrl: base, apiKeys: keys, auths }));
|
|
1547
|
+
console.log(`provider enabled: workbuddy (${keys.length} key${keys.length > 1 ? "s" : ""}) baseUrl=${base}`);
|
|
1548
|
+
appendEvent({ ts: Date.now(), type: "provider-enabled", provider: gid, keys: keys.length, baseUrl: base });
|
|
1549
|
+
} catch (err) {
|
|
1550
|
+
console.log(`provider ${gid} failed: ${err?.message || err}`);
|
|
1551
|
+
appendEvent({ ts: Date.now(), type: "provider-error", provider: gid, error: String(err?.message || err) });
|
|
1552
|
+
}
|
|
1553
|
+
continue;
|
|
1554
|
+
}
|
|
1555
|
+
const base = String(cfg?.baseUrl || "").trim();
|
|
1556
|
+
const keys = Array.isArray(cfg?.keys) ? cfg.keys.filter((k) => typeof k === "string" && k.trim()) : [];
|
|
1557
|
+
if (!base || !keys.length) continue;
|
|
1558
|
+
try {
|
|
1559
|
+
const { createGenericProvider } = await import("../src/providers/generic.js");
|
|
1560
|
+
providers.push(createGenericProvider({ id: gid, baseUrl: base, apiKeys: keys }));
|
|
1561
|
+
console.log(`provider enabled: ${gid} (${keys.length} key${keys.length > 1 ? "s" : ""}) baseUrl=${base}`);
|
|
1562
|
+
appendEvent({ ts: Date.now(), type: "provider-enabled", provider: gid, keys: keys.length, baseUrl: base });
|
|
1563
|
+
} catch (err) {
|
|
1564
|
+
console.log(`provider ${gid} failed: ${err?.message || err}`);
|
|
1565
|
+
appendEvent({ ts: Date.now(), type: "provider-error", provider: gid, error: String(err?.message || err) });
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
// workbuddy via env-only (no providerConfigs entry but MSLXDFF_WORKBUDDY_KEY set)
|
|
1569
|
+
if (!genericConfigs["workbuddy"]) {
|
|
1570
|
+
const wbKeys = loadProviderKeys("workbuddy");
|
|
1571
|
+
if (wbKeys.length) {
|
|
1572
|
+
try {
|
|
1573
|
+
const { createWorkbuddyProvider } = await import("../src/providers/workbuddy.js");
|
|
1574
|
+
const wbAuths = loadProviderAuths("workbuddy");
|
|
1575
|
+
providers.push(createWorkbuddyProvider({ apiKeys: wbKeys, auths: wbAuths }));
|
|
1576
|
+
console.log(`provider enabled: workbuddy (${wbKeys.length} key${wbKeys.length > 1 ? "s" : ""}) baseUrl=https://copilot.tencent.com (env)`);
|
|
1577
|
+
appendEvent({ ts: Date.now(), type: "provider-enabled", provider: "workbuddy", keys: wbKeys.length, baseUrl: "https://copilot.tencent.com" });
|
|
1578
|
+
} catch (err) {
|
|
1579
|
+
console.log(`provider workbuddy failed: ${err?.message || err}`);
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
866
1582
|
}
|
|
1583
|
+
const { createProviderDispatcher } = await import("../src/providers/dispatcher.js");
|
|
1584
|
+
upstream = createProviderDispatcher(providers);
|
|
1585
|
+
appendEvent({ ts: Date.now(), type: "providers", providers: providers.map((p) => p.id) });
|
|
867
1586
|
}
|
|
868
|
-
|
|
869
|
-
const baseUrl = process.env.UPSTREAM_BASE_URL || "https://opencode.ai";
|
|
1587
|
+
const opencodeProvider = providers.find((p) => p.id === "opencode");
|
|
870
1588
|
const models = createModelsService({
|
|
1589
|
+
providers: providers.length > 1 ? providers : undefined,
|
|
871
1590
|
baseUrl,
|
|
872
|
-
headers: upstream.headers,
|
|
1591
|
+
headers: upstream.headers || opencodeProvider?.upstream?.headers,
|
|
873
1592
|
refreshMs: refreshIntervalMs(),
|
|
874
1593
|
cacheFile: join(logDir(), "models.json"),
|
|
875
1594
|
});
|
|
876
1595
|
const auto = createAutoSelector({
|
|
877
1596
|
cooldownMs: modelCooldownMs(),
|
|
878
1597
|
slowCooldownMs: slowCooldownMs(),
|
|
1598
|
+
file: defaultStateFile(),
|
|
879
1599
|
loadCandidates: async () => {
|
|
880
1600
|
try {
|
|
881
1601
|
return (await models.get()).data.map((m) => m.id);
|
|
@@ -1274,6 +1994,12 @@ Usage:
|
|
|
1274
1994
|
mslxdff -showtoken print the current auth token
|
|
1275
1995
|
mslxdff -refresh-token rotate the auth token (prints the new one)
|
|
1276
1996
|
mslxdff -setto workbuddy [modelId] set default model and sync to WorkBuddy models.json (insert or update 127.0.0.1/v1 entry)
|
|
1997
|
+
mslxdff -provider add <id> <baseUrl> <key> [allowedModel...] add a generic OpenAI-compatible provider (myapi/gpt-4, baseUrl https://api.example.com/v1; extra models = allowlist)
|
|
1998
|
+
mslxdff -provider add workbuddy https://copilot.tencent.com <key> [allow...] add WorkBuddy专用供应商(workbuddy/hy3 前缀路由,auths 与 keys 一一对应)
|
|
1999
|
+
mslxdff -provider <id> [key...|add|remove|list|clear|share|set-url] configure provider API keys/URL (multiple keys = rotating, set-url for generic)
|
|
2000
|
+
mslxdff -provider <id> allowlist [list|set|add|remove|clear] manage allowed models (empty=allow all, non-empty=only listed)
|
|
2001
|
+
mslxdff -workbuddy checkin daily 100 credits(双域幂等,已签 code 10001 视为成功)
|
|
2002
|
+
mslxdff -providers list list all configured upstream providers (opencode, openrouter, generic, workbuddy)
|
|
1277
2003
|
mslxdff -creategroup <name> create a group on this node (the group name is the password)
|
|
1278
2004
|
mslxdff -addtogroup <leader-host> <name> [--broadband] join a group via its leader host (default port 8989) — broadband: 宽带动态IP成员(经Leader中继,无需公网入站,默认127.0.0.1)
|
|
1279
2005
|
mslxdff -group sync pull the freshest member list for all joined groups
|
|
@@ -1282,6 +2008,7 @@ Usage:
|
|
|
1282
2008
|
mslxdff -group remove <seq> leader only: kick a member by its list sequence number
|
|
1283
2009
|
mslxdff -leavegroup leave every joined group as a member (leaders: use -delgroup)
|
|
1284
2010
|
mslxdff -delgroup <name> disband a group this node leads (deletes it and its members)
|
|
2011
|
+
mslxdff -chat ["prompt"] chat REPL(mimo-v2.5-free 优先/big-pickle 兜底,自然语言转命令,模糊匹配由模型完成,历史持久化,超长自动压缩,仅拦 -uninstall,daemon 重启不影响)
|
|
1285
2012
|
mslxdff -resetban [ip] clear join-failure bans (all, or one ip)
|
|
1286
2013
|
mslxdff -help show this help
|
|
1287
2014
|
|
|
@@ -1291,6 +2018,7 @@ Environment:
|
|
|
1291
2018
|
MSLXDFF_DAEMON_DIR daemon pid/log/models dir
|
|
1292
2019
|
UPSTREAM_BASE_URL upstream base (default https://opencode.ai)
|
|
1293
2020
|
UPSTREAM_AUTH_TOKEN upstream bearer value (default "public")
|
|
2021
|
+
MSLXDFF_OPENROUTER_KEY openrouter provider API keys(s) (single env value; multiple use "mslxdff -provider openrouter k1 k2 ..." to persist)
|
|
1294
2022
|
UPSTREAM_CONNECT_TIMEOUT_MS upstream connect timeout (default 30000)
|
|
1295
2023
|
MODELS_REFRESH_MS model-list background refresh interval (default 7200000)
|
|
1296
2024
|
MSLXDFF_MODEL_COOLDOWN_MS fallback cooldown after a model error (default 60000)
|
|
@@ -1433,9 +2161,9 @@ async function printStatus() {
|
|
|
1433
2161
|
|
|
1434
2162
|
function fmtStatus(id, statuses) {
|
|
1435
2163
|
const e = statuses[id];
|
|
1436
|
-
if (typeof e === "number") return `error ${fmtTs(
|
|
2164
|
+
if (typeof e === "number") return `error ${fmtTs(e)}`;
|
|
1437
2165
|
if (!e?.status || e.status === "normal") return "";
|
|
1438
|
-
const when = e.at ? ` ${fmtTs(
|
|
2166
|
+
const when = e.at ? ` ${fmtTs(e.at)}` : "";
|
|
1439
2167
|
const code = e.code ? ` HTTP ${e.code}` : "";
|
|
1440
2168
|
return `${e.status}${when}${code}`;
|
|
1441
2169
|
}
|
|
@@ -1446,7 +2174,7 @@ function fmtDur(ms) {
|
|
|
1446
2174
|
}
|
|
1447
2175
|
|
|
1448
2176
|
function fmtEvent(e) {
|
|
1449
|
-
const t = e?.ts ?
|
|
2177
|
+
const t = e?.ts ? fmtShanghaiHMS(e.ts) : "--:--:--";
|
|
1450
2178
|
const head = `[${t}]`;
|
|
1451
2179
|
const m = (x) => x || "-";
|
|
1452
2180
|
const fallbackTag = e.fallback?.fallback ? ` fallback=${e.fallback.requested_model || e.requested}->${e.fallback.actual_model || e.actual}(${e.fallback.reason || e.reason})` : "";
|
|
@@ -1495,7 +2223,7 @@ function fmtEvent(e) {
|
|
|
1495
2223
|
case "relay-forward":
|
|
1496
2224
|
return `${head} relay fwd ${e.target || e.peer || "?"} via leader model=${m(e.model)} hops=${e.hops || 0}`;
|
|
1497
2225
|
case "relay-heartbeat":
|
|
1498
|
-
return `${head} relay hb ${e.member || "?"} ip=${e.ip || "?"} lastSeen=${e.lastSeen ?
|
|
2226
|
+
return `${head} relay hb ${e.member || "?"} ip=${e.ip || "?"} lastSeen=${e.lastSeen ? fmtShanghaiHMS(e.lastSeen) : "?"}`;
|
|
1499
2227
|
case "client-abort":
|
|
1500
2228
|
return `${head} client abort ${m(e.model)} total=${fmtDur(e.totalMs)}`;
|
|
1501
2229
|
case "result":
|
|
@@ -1534,12 +2262,7 @@ function fmtEvent(e) {
|
|
|
1534
2262
|
}
|
|
1535
2263
|
|
|
1536
2264
|
function fmtTs(iso) {
|
|
1537
|
-
|
|
1538
|
-
try {
|
|
1539
|
-
return new Date(iso).toISOString().replace("T", " ").slice(5, 19);
|
|
1540
|
-
} catch {
|
|
1541
|
-
return "-";
|
|
1542
|
-
}
|
|
2265
|
+
return fmtShanghai(iso);
|
|
1543
2266
|
}
|
|
1544
2267
|
|
|
1545
2268
|
function npmCmd() {
|