mslxdff 0.1.55 → 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 +666 -26
- package/package.json +3 -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/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 +299 -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, loadModelPicks, saveModelPicks } 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
|
}
|
|
@@ -289,7 +300,7 @@ if (args.includes("-model") || args.includes("-models")) {
|
|
|
289
300
|
console.log(`saved ${result.size} picked model(s): ${[...result].join(", ") || "(none — auto uses full list)"}`);
|
|
290
301
|
process.exit(0);
|
|
291
302
|
}
|
|
292
|
-
const at = cachedAt ? ` (cached ${
|
|
303
|
+
const at = cachedAt ? ` (cached ${fmtShanghaiYMDHM(cachedAt)})` : "";
|
|
293
304
|
const pickedIds = loadModelPicks();
|
|
294
305
|
const mark = (id) => (pickedIds.includes(id) ? "*" : " ");
|
|
295
306
|
console.log(`${ids.length} free model(s)${at} (${pickedIds.length} picked, * = picked):`);
|
|
@@ -370,6 +381,556 @@ if (args.includes("-setto") || args.includes("--setto")) {
|
|
|
370
381
|
process.exit(0);
|
|
371
382
|
}
|
|
372
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
|
+
|
|
373
934
|
// 交互式选择器:↑/↓ 移动,Enter 确认,q/Esc 取消;ANSI 原地重绘
|
|
374
935
|
async function pickInteractive(items, startCursor = 0) {
|
|
375
936
|
const { renderChooser, renderChooserHelp, parseKey } = await import("../src/chooser.js");
|
|
@@ -931,27 +1492,103 @@ if (loadedPlugins.length) {
|
|
|
931
1492
|
console.log(`plugins loaded (${loadedPlugins.length}): ${loadedPlugins.map((p) => `${p.name}${p.version ? `@${p.version}` : ""}`).join(", ")}`);
|
|
932
1493
|
appendEvent({ ts: Date.now(), type: "plugins-loaded", plugins: loadedPlugins.map((p) => ({ name: p.name, version: p.version })) });
|
|
933
1494
|
}
|
|
1495
|
+
// 多供应商通道:默认 opencode + 可选 openrouter(MSLXDFF_OPENROUTER_KEY 启用)
|
|
1496
|
+
// 插件 createUpstream 仍是"整体替换式"外部 provider(单通道,保留原语义)
|
|
934
1497
|
const upstreamHooks = loadedPlugins.length
|
|
935
1498
|
? (name, ctx) => runHook(loadedPlugins, name, ctx)
|
|
936
1499
|
: null;
|
|
937
|
-
// 插件可提供 createUpstream(ctx) 整体替换上游(接任意 provider);取第一个声明者
|
|
938
1500
|
const providerPlugin = loadedPlugins.find((p) => typeof p.createUpstream === "function");
|
|
939
1501
|
let upstream;
|
|
1502
|
+
const baseUrl = process.env.UPSTREAM_BASE_URL || "https://opencode.ai";
|
|
1503
|
+
let providers = [];
|
|
940
1504
|
if (providerPlugin) {
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
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
|
+
}
|
|
948
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
|
+
}
|
|
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) });
|
|
949
1586
|
}
|
|
950
|
-
|
|
951
|
-
const baseUrl = process.env.UPSTREAM_BASE_URL || "https://opencode.ai";
|
|
1587
|
+
const opencodeProvider = providers.find((p) => p.id === "opencode");
|
|
952
1588
|
const models = createModelsService({
|
|
1589
|
+
providers: providers.length > 1 ? providers : undefined,
|
|
953
1590
|
baseUrl,
|
|
954
|
-
headers: upstream.headers,
|
|
1591
|
+
headers: upstream.headers || opencodeProvider?.upstream?.headers,
|
|
955
1592
|
refreshMs: refreshIntervalMs(),
|
|
956
1593
|
cacheFile: join(logDir(), "models.json"),
|
|
957
1594
|
});
|
|
@@ -1357,6 +1994,12 @@ Usage:
|
|
|
1357
1994
|
mslxdff -showtoken print the current auth token
|
|
1358
1995
|
mslxdff -refresh-token rotate the auth token (prints the new one)
|
|
1359
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)
|
|
1360
2003
|
mslxdff -creategroup <name> create a group on this node (the group name is the password)
|
|
1361
2004
|
mslxdff -addtogroup <leader-host> <name> [--broadband] join a group via its leader host (default port 8989) — broadband: 宽带动态IP成员(经Leader中继,无需公网入站,默认127.0.0.1)
|
|
1362
2005
|
mslxdff -group sync pull the freshest member list for all joined groups
|
|
@@ -1365,6 +2008,7 @@ Usage:
|
|
|
1365
2008
|
mslxdff -group remove <seq> leader only: kick a member by its list sequence number
|
|
1366
2009
|
mslxdff -leavegroup leave every joined group as a member (leaders: use -delgroup)
|
|
1367
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 重启不影响)
|
|
1368
2012
|
mslxdff -resetban [ip] clear join-failure bans (all, or one ip)
|
|
1369
2013
|
mslxdff -help show this help
|
|
1370
2014
|
|
|
@@ -1374,6 +2018,7 @@ Environment:
|
|
|
1374
2018
|
MSLXDFF_DAEMON_DIR daemon pid/log/models dir
|
|
1375
2019
|
UPSTREAM_BASE_URL upstream base (default https://opencode.ai)
|
|
1376
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)
|
|
1377
2022
|
UPSTREAM_CONNECT_TIMEOUT_MS upstream connect timeout (default 30000)
|
|
1378
2023
|
MODELS_REFRESH_MS model-list background refresh interval (default 7200000)
|
|
1379
2024
|
MSLXDFF_MODEL_COOLDOWN_MS fallback cooldown after a model error (default 60000)
|
|
@@ -1516,9 +2161,9 @@ async function printStatus() {
|
|
|
1516
2161
|
|
|
1517
2162
|
function fmtStatus(id, statuses) {
|
|
1518
2163
|
const e = statuses[id];
|
|
1519
|
-
if (typeof e === "number") return `error ${fmtTs(
|
|
2164
|
+
if (typeof e === "number") return `error ${fmtTs(e)}`;
|
|
1520
2165
|
if (!e?.status || e.status === "normal") return "";
|
|
1521
|
-
const when = e.at ? ` ${fmtTs(
|
|
2166
|
+
const when = e.at ? ` ${fmtTs(e.at)}` : "";
|
|
1522
2167
|
const code = e.code ? ` HTTP ${e.code}` : "";
|
|
1523
2168
|
return `${e.status}${when}${code}`;
|
|
1524
2169
|
}
|
|
@@ -1529,7 +2174,7 @@ function fmtDur(ms) {
|
|
|
1529
2174
|
}
|
|
1530
2175
|
|
|
1531
2176
|
function fmtEvent(e) {
|
|
1532
|
-
const t = e?.ts ?
|
|
2177
|
+
const t = e?.ts ? fmtShanghaiHMS(e.ts) : "--:--:--";
|
|
1533
2178
|
const head = `[${t}]`;
|
|
1534
2179
|
const m = (x) => x || "-";
|
|
1535
2180
|
const fallbackTag = e.fallback?.fallback ? ` fallback=${e.fallback.requested_model || e.requested}->${e.fallback.actual_model || e.actual}(${e.fallback.reason || e.reason})` : "";
|
|
@@ -1578,7 +2223,7 @@ function fmtEvent(e) {
|
|
|
1578
2223
|
case "relay-forward":
|
|
1579
2224
|
return `${head} relay fwd ${e.target || e.peer || "?"} via leader model=${m(e.model)} hops=${e.hops || 0}`;
|
|
1580
2225
|
case "relay-heartbeat":
|
|
1581
|
-
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) : "?"}`;
|
|
1582
2227
|
case "client-abort":
|
|
1583
2228
|
return `${head} client abort ${m(e.model)} total=${fmtDur(e.totalMs)}`;
|
|
1584
2229
|
case "result":
|
|
@@ -1617,12 +2262,7 @@ function fmtEvent(e) {
|
|
|
1617
2262
|
}
|
|
1618
2263
|
|
|
1619
2264
|
function fmtTs(iso) {
|
|
1620
|
-
|
|
1621
|
-
try {
|
|
1622
|
-
return new Date(iso).toISOString().replace("T", " ").slice(5, 19);
|
|
1623
|
-
} catch {
|
|
1624
|
-
return "-";
|
|
1625
|
-
}
|
|
2265
|
+
return fmtShanghai(iso);
|
|
1626
2266
|
}
|
|
1627
2267
|
|
|
1628
2268
|
function npmCmd() {
|