mslxdff 0.1.65 → 0.1.67
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/mslxdff.js +3 -3159
- package/package.json +1 -1
- package/src/chat/cooling.js +99 -0
- package/src/chat/direct.js +57 -0
- package/src/chat/gateway.js +148 -0
- package/src/chat/orchestrator.js +218 -0
- package/src/chat/sse.js +69 -0
- package/src/chat/upstream.js +59 -501
- package/src/cli/bootstrap.js +1 -0
- package/src/cli/commands/daemon.js +151 -0
- package/src/cli/commands/group.js +272 -0
- package/src/cli/commands/model.js +405 -0
- package/src/cli/commands/provider/add.js +106 -0
- package/src/cli/commands/provider/allowlist.js +99 -0
- package/src/cli/commands/provider/config.js +82 -0
- package/src/cli/commands/provider/index.js +193 -0
- package/src/cli/commands/provider/keys.js +135 -0
- package/src/cli/commands/provider/models.js +99 -0
- package/src/cli/commands/provider.js +1 -0
- package/src/cli/commands/sync.js +143 -0
- package/src/cli/commands/system.js +236 -0
- package/src/cli/commands/workbuddy.js +91 -0
- package/src/cli/format.js +119 -0
- package/src/cli/group-helpers.js +69 -0
- package/src/cli/help.js +66 -0
- package/src/cli/index.js +59 -0
- package/src/cli/interactive.js +84 -0
- package/src/cli/policy.js +119 -0
- package/src/cli/provider-row.js +73 -0
- package/src/cli/status.js +283 -0
- package/src/cli/util.js +24 -0
- package/src/providers/base.js +159 -0
- package/src/providers/dispatcher.js +18 -6
- package/src/providers/generic.js +28 -166
- package/src/providers/openrouter.js +19 -205
- package/src/providers/workbuddy/auth.js +175 -0
- package/src/providers/workbuddy/balance.js +84 -0
- package/src/providers/workbuddy/chat.js +310 -0
- package/src/providers/workbuddy/index.js +263 -0
- package/src/providers/workbuddy/models.js +111 -0
- package/src/providers/workbuddy/rotation-log.js +54 -0
- package/src/providers/workbuddy.js +2 -677
- package/src/routes/chat/broadband-handler.js +25 -46
- package/src/routes/chat/exhausted-handler.js +2 -2
- package/src/routes/chat/gateway.js +301 -0
- package/src/routes/chat/hedge-handler.js +65 -83
- package/src/routes/chat/index.js +1 -384
- package/src/routes/chat/local-handler.js +32 -61
- package/src/routes/chat/peer-handler.js +32 -23
- package/src/routes/chat/relay-pipeline.js +151 -0
- package/src/runtime/bootstrap.js +408 -0
- package/src/state/facade.js +57 -0
- package/src/state/memory.js +161 -0
- package/src/state/merge.js +26 -0
- package/src/state/persist.js +42 -0
- package/src/state/provider-config.js +143 -0
- package/src/state/schemas/allowlist.js +87 -0
- package/src/state/schemas/group.js +31 -0
- package/src/state/schemas/model.js +53 -0
- package/src/state/schemas/peer.js +31 -0
- package/src/state/schemas/port.js +10 -0
- package/src/state/schemas/provider.js +204 -0
- package/src/state/schemas/token.js +43 -0
- package/src/state/store.js +176 -0
- package/src/state.js +1 -711
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { createModelsService } from "../../models.js";
|
|
5
|
+
import { createUpstreamClient } from "../../upstream.js";
|
|
6
|
+
import { logDir } from "../../logs.js";
|
|
7
|
+
import { loadModelErrors, savePreferredModel, loadPreferredModel, loadModelPicks, saveModelPicks } from "../../state.js";
|
|
8
|
+
import { getPreferredModel } from "../../auto.js";
|
|
9
|
+
import { fmtShanghaiYMDHM } from "../../time.js";
|
|
10
|
+
import { readModelsCache } from "../util.js";
|
|
11
|
+
import { pickInteractiveMulti } from "../interactive.js";
|
|
12
|
+
|
|
13
|
+
export async function handleModel(args) {
|
|
14
|
+
if (!(args.includes("-model") || args.includes("-models"))) return false;
|
|
15
|
+
const idx = args.findIndex((x) => x === "-model" || x === "-models");
|
|
16
|
+
const sub = args[idx + 1];
|
|
17
|
+
if (sub === "refresh") {
|
|
18
|
+
const models = createModelsService({
|
|
19
|
+
baseUrl: process.env.UPSTREAM_BASE_URL || "https://opencode.ai",
|
|
20
|
+
headers: createUpstreamClient({}).headers,
|
|
21
|
+
refreshMs: 0,
|
|
22
|
+
cacheFile: join(logDir(), "models.json"),
|
|
23
|
+
});
|
|
24
|
+
try {
|
|
25
|
+
const list = await models.get();
|
|
26
|
+
const ids = (list.data || []).map((m) => m.id).filter(Boolean);
|
|
27
|
+
console.log(`refreshed: ${ids.length} free model(s)`);
|
|
28
|
+
for (const id of ids) console.log(` ${id}`);
|
|
29
|
+
} catch (err) {
|
|
30
|
+
console.error(`could not refresh models: ${String(err?.message || err)}`);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
process.exit(0);
|
|
34
|
+
}
|
|
35
|
+
if (sub === "status") {
|
|
36
|
+
const statuses = loadModelErrors();
|
|
37
|
+
const cacheFile = join(logDir(), "models.json");
|
|
38
|
+
const cached = readModelsCache(cacheFile);
|
|
39
|
+
const ids = new Set([
|
|
40
|
+
...(cached?.data || []).map((m) => m.id),
|
|
41
|
+
...Object.keys(statuses),
|
|
42
|
+
]);
|
|
43
|
+
for (const id of ids) {
|
|
44
|
+
const e = statuses[id];
|
|
45
|
+
const st = typeof e === "number" ? "error" : e?.status || "normal";
|
|
46
|
+
const at = typeof e === "number" ? e : e?.at;
|
|
47
|
+
const when = at ? ` (${fmtShanghaiYMDHM ? fmtShanghaiYMDHM(at) : at})` : "";
|
|
48
|
+
const extra = e?.code ? ` HTTP ${e.code}` : "";
|
|
49
|
+
console.log(` ${id} ${st}${when}${extra}`);
|
|
50
|
+
}
|
|
51
|
+
process.exit(0);
|
|
52
|
+
}
|
|
53
|
+
if (sub === "set" && args[idx + 2]) {
|
|
54
|
+
const id = args[idx + 2];
|
|
55
|
+
savePreferredModel(id);
|
|
56
|
+
const picks = [...new Set([...loadModelPicks(), id])];
|
|
57
|
+
saveModelPicks(picks);
|
|
58
|
+
console.log(`default model set to: ${id} (daemon hot-reloads on next request)`);
|
|
59
|
+
console.log(`picked: ${picks.join(", ") || "(none)"} (auto will pick within these)`);
|
|
60
|
+
process.exit(0);
|
|
61
|
+
}
|
|
62
|
+
if (sub === "pick" && args[idx + 2] && args[idx + 2] !== "clear") {
|
|
63
|
+
const picks = [...new Set([...loadModelPicks(), args[idx + 2]])];
|
|
64
|
+
saveModelPicks(picks);
|
|
65
|
+
console.log(`picked: ${picks.join(", ") || "(none)"} (auto will pick within these)`);
|
|
66
|
+
process.exit(0);
|
|
67
|
+
}
|
|
68
|
+
if (sub === "pick" && args[idx + 2] === "clear") {
|
|
69
|
+
saveModelPicks([]);
|
|
70
|
+
console.log("picks cleared — auto uses the full model list again");
|
|
71
|
+
process.exit(0);
|
|
72
|
+
}
|
|
73
|
+
if (sub === "unpick" && args[idx + 2]) {
|
|
74
|
+
const picks = loadModelPicks().filter((x) => x !== args[idx + 2]);
|
|
75
|
+
saveModelPicks(picks);
|
|
76
|
+
console.log(`picked: ${picks.join(", ") || "(none)"}${picks.length === 0 ? " (auto uses full list)" : ""}`);
|
|
77
|
+
process.exit(0);
|
|
78
|
+
}
|
|
79
|
+
if (sub === "picks") {
|
|
80
|
+
const picks = loadModelPicks();
|
|
81
|
+
if (!picks.length) {
|
|
82
|
+
console.log("no picks — auto uses the full model list");
|
|
83
|
+
} else {
|
|
84
|
+
console.log(`${picks.length} picked model(s), auto only selects within these:`);
|
|
85
|
+
}
|
|
86
|
+
for (const id of picks) console.log(` ${id}`);
|
|
87
|
+
process.exit(0);
|
|
88
|
+
}
|
|
89
|
+
let modelListProvider = null;
|
|
90
|
+
let modelListJson = false;
|
|
91
|
+
if (sub === "list") {
|
|
92
|
+
const restArgs = args.slice(idx + 2);
|
|
93
|
+
for (let i = 0; i < restArgs.length; i++) {
|
|
94
|
+
const a = String(restArgs[i] || "");
|
|
95
|
+
if (a === "--json" || a === "-json") modelListJson = true;
|
|
96
|
+
else if (a === "--provider" || a === "-provider" || a === "--providerId") { modelListProvider = String(restArgs[i + 1] || "").trim() || null; i++; }
|
|
97
|
+
else if (!a.startsWith("-") && !modelListProvider) modelListProvider = a;
|
|
98
|
+
}
|
|
99
|
+
if (modelListProvider) {
|
|
100
|
+
const { normalizeProviderId } = await import("../../providers/model-id.js");
|
|
101
|
+
const nid = normalizeProviderId(modelListProvider);
|
|
102
|
+
modelListProvider = nid || modelListProvider.toLowerCase();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (sub !== undefined && sub !== "list") {
|
|
106
|
+
console.error("usage: mslxdff -models (interactive multi-pick) | mslxdff -model list [--provider <id>] [--json] | 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");
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
const cacheFile = join(logDir(), "models.json");
|
|
110
|
+
async function tryRefreshModels() {
|
|
111
|
+
try {
|
|
112
|
+
const models = createModelsService({
|
|
113
|
+
baseUrl: process.env.UPSTREAM_BASE_URL || "https://opencode.ai",
|
|
114
|
+
headers: createUpstreamClient({}).headers,
|
|
115
|
+
refreshMs: 0,
|
|
116
|
+
cacheFile,
|
|
117
|
+
});
|
|
118
|
+
const list = await Promise.race([
|
|
119
|
+
models.get(),
|
|
120
|
+
new Promise((_, rej) => setTimeout(() => rej(new Error("refresh timeout")), 4000)),
|
|
121
|
+
]);
|
|
122
|
+
return list;
|
|
123
|
+
} catch {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
let ids = [];
|
|
129
|
+
let cachedAt = null;
|
|
130
|
+
let refreshed = null;
|
|
131
|
+
refreshed = await tryRefreshModels();
|
|
132
|
+
if (refreshed?.data) {
|
|
133
|
+
ids = (refreshed.data || []).map((m) => m.id).filter(Boolean);
|
|
134
|
+
cachedAt = refreshed.cachedAt || Date.now();
|
|
135
|
+
} else {
|
|
136
|
+
const cached = readModelsCache(cacheFile);
|
|
137
|
+
if (cached) {
|
|
138
|
+
ids = (cached.data || []).map((m) => m.id).filter(Boolean);
|
|
139
|
+
cachedAt = cached.cachedAt || null;
|
|
140
|
+
} else {
|
|
141
|
+
throw new Error("no cached models and refresh failed");
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (modelListProvider) {
|
|
145
|
+
const prov = String(modelListProvider).toLowerCase();
|
|
146
|
+
const filtered = ids.filter((id) => {
|
|
147
|
+
const slash = String(id).indexOf("/");
|
|
148
|
+
const p = slash > 0 ? String(id).slice(0, slash).toLowerCase() : "opencode";
|
|
149
|
+
return p === prov;
|
|
150
|
+
});
|
|
151
|
+
if (prov !== "opencode" && filtered.length === 0) {
|
|
152
|
+
try {
|
|
153
|
+
const { loadProviderAllowedModels, loadProviderAllowAnyModels, loadProviderBaseUrl } = await import("../../state.js");
|
|
154
|
+
const { loadModelAliases, getAliasForModel } = await import("../../providers/model-id.js");
|
|
155
|
+
try { loadModelAliases(); } catch {}
|
|
156
|
+
const allowed = loadProviderAllowedModels(prov);
|
|
157
|
+
const allowAny = loadProviderAllowAnyModels(prov);
|
|
158
|
+
const baseUrl = loadProviderBaseUrl(prov);
|
|
159
|
+
if (modelListJson) {
|
|
160
|
+
const data = allowed.length
|
|
161
|
+
? allowed.map((raw) => ({ id: `${prov}/${raw}`, object: "model" }))
|
|
162
|
+
: [];
|
|
163
|
+
console.log(JSON.stringify({ object: "list", data }, null, 2));
|
|
164
|
+
process.exit(0);
|
|
165
|
+
}
|
|
166
|
+
if (!allowed.length) {
|
|
167
|
+
if (allowAny) {
|
|
168
|
+
console.log(`provider "${prov}" allowAny ON (allowlist 空=放行全部)${baseUrl ? ` baseUrl=${baseUrl}` : ""}`);
|
|
169
|
+
console.log(` (未设 allowlist,全部模型放行) 查看 live 列表: mslxdff -provider ${prov} models`);
|
|
170
|
+
} else {
|
|
171
|
+
console.log(`no models for provider "${prov}" — allowlist 空 + allowAny OFF = 阻塞`);
|
|
172
|
+
console.log(` 设白名单: mslxdff -provider ${prov} allowlist set <model1> <model2> 或 mslxdff -provider ${prov} allowAny on`);
|
|
173
|
+
console.log(` live 查看: mslxdff -provider ${prov} models`);
|
|
174
|
+
}
|
|
175
|
+
process.exit(0);
|
|
176
|
+
}
|
|
177
|
+
const at2 = cachedAt ? ` (cached ${fmtShanghaiYMDHM(cachedAt)})` : "";
|
|
178
|
+
console.log(`${allowed.length} model(s) for ${prov}${at2} (allowlist,原名 + 别名):`);
|
|
179
|
+
const pickedIds2 = loadModelPicks();
|
|
180
|
+
for (const raw of allowed) {
|
|
181
|
+
const canonical = `${prov}/${raw}`;
|
|
182
|
+
let alias = null;
|
|
183
|
+
try { alias = getAliasForModel(canonical); } catch {}
|
|
184
|
+
if (!alias && String(canonical).includes("/")) alias = String(canonical).replace(/\//g, "-");
|
|
185
|
+
const aliasStr = alias && alias !== canonical ? ` (别名: ${alias})` : "";
|
|
186
|
+
const mark2 = pickedIds2.includes(canonical) || (alias && pickedIds2.includes(alias)) ? "*" : " ";
|
|
187
|
+
console.log(` ${mark2} ${canonical}${aliasStr}`);
|
|
188
|
+
}
|
|
189
|
+
process.exit(0);
|
|
190
|
+
} catch {}
|
|
191
|
+
}
|
|
192
|
+
ids = filtered;
|
|
193
|
+
if (modelListJson) {
|
|
194
|
+
console.log(JSON.stringify({ object: "list", data: ids.map((id) => ({ id, object: "model" })) }, null, 2));
|
|
195
|
+
process.exit(0);
|
|
196
|
+
}
|
|
197
|
+
if (!ids.length) {
|
|
198
|
+
console.log(`no models for provider "${prov}" — try: mslxdff -provider ${prov} models or mslxdff -model refresh`);
|
|
199
|
+
process.exit(0);
|
|
200
|
+
}
|
|
201
|
+
} else if (modelListJson) {
|
|
202
|
+
console.log(JSON.stringify({ object: "list", data: ids.map((id) => ({ id, object: "model" })) }, null, 2));
|
|
203
|
+
process.exit(0);
|
|
204
|
+
}
|
|
205
|
+
if (!ids.length) {
|
|
206
|
+
console.log("no models available — try: mslxdff -model refresh");
|
|
207
|
+
process.exit(0);
|
|
208
|
+
}
|
|
209
|
+
if (sub === undefined && process.stdin.isTTY && process.stdout.isTTY) {
|
|
210
|
+
const statuses = loadModelErrors();
|
|
211
|
+
const current = getPreferredModel();
|
|
212
|
+
const pickedIds = loadModelPicks();
|
|
213
|
+
const combinedIds = [...ids];
|
|
214
|
+
const seen = new Set(combinedIds);
|
|
215
|
+
try {
|
|
216
|
+
const { loadProviderConfigs, loadProviderAllowedModels } = await import("../../state.js");
|
|
217
|
+
const configs = loadProviderConfigs();
|
|
218
|
+
for (const pid of Object.keys(configs).filter((k) => String(k).toLowerCase() !== "opencode")) {
|
|
219
|
+
const allowed = loadProviderAllowedModels(pid);
|
|
220
|
+
for (const raw of allowed) {
|
|
221
|
+
const canonical = `${pid}/${raw}`;
|
|
222
|
+
if (!seen.has(canonical)) {
|
|
223
|
+
seen.add(canonical);
|
|
224
|
+
combinedIds.push(canonical);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
} catch {}
|
|
229
|
+
for (const pid of pickedIds) {
|
|
230
|
+
if (!seen.has(pid)) {
|
|
231
|
+
seen.add(pid);
|
|
232
|
+
combinedIds.push(pid);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
const items = combinedIds.map((id) => {
|
|
236
|
+
const e = statuses[id];
|
|
237
|
+
return {
|
|
238
|
+
id,
|
|
239
|
+
status: typeof e === "number" ? "error" : e?.status || "normal",
|
|
240
|
+
current: id === current,
|
|
241
|
+
picked: pickedIds.includes(id),
|
|
242
|
+
};
|
|
243
|
+
});
|
|
244
|
+
const result = await pickInteractiveMulti(items, new Set(pickedIds), Math.max(0, items.findIndex((x) => x.current)));
|
|
245
|
+
if (!result) {
|
|
246
|
+
console.log("cancelled — picks unchanged");
|
|
247
|
+
process.exit(0);
|
|
248
|
+
}
|
|
249
|
+
saveModelPicks([...result]);
|
|
250
|
+
console.log(`saved ${result.size} picked model(s): ${[...result].join(", ") || "(none — auto uses full list)"}`);
|
|
251
|
+
process.exit(0);
|
|
252
|
+
}
|
|
253
|
+
const at = cachedAt ? ` (cached ${fmtShanghaiYMDHM(cachedAt)})` : "";
|
|
254
|
+
const pickedIds = loadModelPicks();
|
|
255
|
+
const mark = (id) => (pickedIds.includes(id) ? "*" : " ");
|
|
256
|
+
const groups = {};
|
|
257
|
+
for (const id of ids) {
|
|
258
|
+
const prov = String(id).includes("/") ? String(id).split("/")[0] : "opencode";
|
|
259
|
+
if (!groups[prov]) groups[prov] = [];
|
|
260
|
+
groups[prov].push(id);
|
|
261
|
+
}
|
|
262
|
+
const order = ["opencode", "workbuddy", "clinebot", "openrouter"];
|
|
263
|
+
const sortedProvs = Object.keys(groups).sort((a, b) => {
|
|
264
|
+
const ia = order.indexOf(a), ib = order.indexOf(b);
|
|
265
|
+
if (ia !== -1 || ib !== -1) {
|
|
266
|
+
if (ia === -1) return 1;
|
|
267
|
+
if (ib === -1) return -1;
|
|
268
|
+
return ia - ib;
|
|
269
|
+
}
|
|
270
|
+
return a.localeCompare(b);
|
|
271
|
+
});
|
|
272
|
+
if (modelListProvider) {
|
|
273
|
+
console.log(`${ids.length} model(s) for ${modelListProvider}${at} (${pickedIds.length} picked, * = picked):`);
|
|
274
|
+
let aliasMap = {};
|
|
275
|
+
try {
|
|
276
|
+
const { loadModelAliases, getAliasForModel } = await import("../../providers/model-id.js");
|
|
277
|
+
loadModelAliases();
|
|
278
|
+
for (const id of ids) {
|
|
279
|
+
const alias = getAliasForModel(id);
|
|
280
|
+
if (alias) aliasMap[id] = alias;
|
|
281
|
+
else if (String(id).includes("/")) {
|
|
282
|
+
const dashAlias = String(id).replace(/\//g, "-");
|
|
283
|
+
if (dashAlias !== id) aliasMap[id] = dashAlias;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
} catch {}
|
|
287
|
+
for (const prov of sortedProvs) {
|
|
288
|
+
const list = groups[prov];
|
|
289
|
+
console.log(`\n ── ${prov} (${list.length}) ──`);
|
|
290
|
+
for (const id of list) {
|
|
291
|
+
const alias = aliasMap[id];
|
|
292
|
+
const aliasStr = alias ? ` (别名: ${alias})` : "";
|
|
293
|
+
console.log(` ${mark(id)} ${id}${aliasStr}`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
console.log(`\npicked only constrains auto; manage with: mslxdff -models (TTY) | mslxdff -model pick <id> | mslxdff -model unpick <id> | mslxdff -model pick clear`);
|
|
297
|
+
} else {
|
|
298
|
+
console.log(`${ids.length} free model(s)${at} (${pickedIds.length} picked, * = picked):`);
|
|
299
|
+
let aliasMap = {};
|
|
300
|
+
let fullAliases = {};
|
|
301
|
+
try {
|
|
302
|
+
const { loadModelAliases, getAliasForModel } = await import("../../providers/model-id.js");
|
|
303
|
+
loadModelAliases();
|
|
304
|
+
for (const id of ids) {
|
|
305
|
+
const alias = getAliasForModel(id);
|
|
306
|
+
if (alias) aliasMap[id] = alias;
|
|
307
|
+
else if (String(id).includes("/")) {
|
|
308
|
+
const dashAlias = String(id).replace(/\//g, "-");
|
|
309
|
+
if (dashAlias !== id) aliasMap[id] = dashAlias;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
try {
|
|
313
|
+
const aliasesFile = join(homedir(), ".config", "mslxdff", "model-aliases.json");
|
|
314
|
+
const raw = JSON.parse(readFileSync(aliasesFile, "utf8"));
|
|
315
|
+
if (raw && typeof raw === "object") fullAliases = raw;
|
|
316
|
+
} catch {}
|
|
317
|
+
} catch {}
|
|
318
|
+
for (const prov of sortedProvs) {
|
|
319
|
+
const list = groups[prov];
|
|
320
|
+
console.log(`\n ── ${prov} (${list.length}) ──`);
|
|
321
|
+
for (const id of list) {
|
|
322
|
+
const alias = aliasMap[id];
|
|
323
|
+
const aliasStr = alias ? ` (别名: ${alias})` : "";
|
|
324
|
+
console.log(` ${mark(id)} ${id}${aliasStr}`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
try {
|
|
328
|
+
const { loadProviderConfigs, loadProviderAllowedModels, loadProviderAllowAnyModels, loadProviderBaseUrl } = await import("../../state.js");
|
|
329
|
+
const { loadModelAliases: _la2, getAliasForModel: _gaf } = await import("../../providers/model-id.js");
|
|
330
|
+
try { _la2(); } catch {}
|
|
331
|
+
const configs = loadProviderConfigs();
|
|
332
|
+
const otherIds = Object.keys(configs).filter((k) => String(k).toLowerCase() !== "opencode");
|
|
333
|
+
const order2 = ["workbuddy", "clinebot", "openrouter", "bai"];
|
|
334
|
+
otherIds.sort((a, b) => {
|
|
335
|
+
const ia = order2.indexOf(a), ib = order2.indexOf(b);
|
|
336
|
+
if (ia !== -1 || ib !== -1) {
|
|
337
|
+
if (ia === -1) return 1;
|
|
338
|
+
if (ib === -1) return -1;
|
|
339
|
+
return ia - ib;
|
|
340
|
+
}
|
|
341
|
+
return a.localeCompare(b);
|
|
342
|
+
});
|
|
343
|
+
if (otherIds.length) {
|
|
344
|
+
console.log(`\n────────────────────────────────────────`);
|
|
345
|
+
console.log(`其他供应商 (allowlist,原名 + 别名) (${otherIds.length} providers):`);
|
|
346
|
+
for (const pid of otherIds) {
|
|
347
|
+
const allowed = loadProviderAllowedModels(pid);
|
|
348
|
+
const allowAny = loadProviderAllowAnyModels(pid);
|
|
349
|
+
const baseUrl = loadProviderBaseUrl(pid) || configs[pid]?.baseUrl || "";
|
|
350
|
+
const header = allowAny
|
|
351
|
+
? (allowed.length ? `allowlist ${allowed.length} (allowAny ON)` : `allowAny ON (allowlist 空=放行全部)`)
|
|
352
|
+
: (allowed.length ? `allowlist ${allowed.length} (allowAny OFF)` : `allowlist 空 + allowAny OFF = 阻塞`);
|
|
353
|
+
console.log(`\n ── ${pid} (${header})${baseUrl ? ` baseUrl=${baseUrl}` : ""} ──`);
|
|
354
|
+
if (!allowed.length) {
|
|
355
|
+
if (allowAny) {
|
|
356
|
+
console.log(` (未设 allowlist,全部模型放行) 查看 live 列表: mslxdff -provider ${pid} models`);
|
|
357
|
+
console.log(` 限制可用模型: mslxdff -provider ${pid} allowlist set <model1> <model2>`);
|
|
358
|
+
} else {
|
|
359
|
+
console.log(` 阻塞中:无可用模型 — 设白名单: mslxdff -provider ${pid} allowlist set <model1> <model2>`);
|
|
360
|
+
console.log(` 或放行全部: mslxdff -provider ${pid} allowAny on`);
|
|
361
|
+
}
|
|
362
|
+
} else {
|
|
363
|
+
for (const raw of allowed) {
|
|
364
|
+
const canonical = `${pid}/${raw}`;
|
|
365
|
+
let alias = null;
|
|
366
|
+
try { alias = _gaf(canonical); } catch {}
|
|
367
|
+
if (!alias && String(canonical).includes("/")) alias = String(canonical).replace(/\//g, "-");
|
|
368
|
+
const aliasStr = alias && alias !== canonical ? ` (别名: ${alias})` : "";
|
|
369
|
+
const pickedMark = pickedIds.includes(canonical) || pickedIds.includes(alias || "") ? "*" : " ";
|
|
370
|
+
console.log(` ${pickedMark} ${canonical}${aliasStr}`);
|
|
371
|
+
}
|
|
372
|
+
console.log(` 管理: mslxdff -provider ${pid} allowlist [list|add|remove|clear] | allowAny on|off`);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
} else {
|
|
376
|
+
console.log(`\n────────────────────────────────────────`);
|
|
377
|
+
console.log(`其他供应商 (allowlist,原名 + 别名): (none — 尚未配置)`);
|
|
378
|
+
console.log(` 添加示例: mslxdff -provider add myapi https://api.example.com/v1 sk-xxx --models-path /v1/models`);
|
|
379
|
+
}
|
|
380
|
+
const aliasEntries = Object.entries(fullAliases).filter(([alias, canonical]) => {
|
|
381
|
+
if (ids.includes(canonical)) return false;
|
|
382
|
+
for (const pid of otherIds) {
|
|
383
|
+
const allowed = loadProviderAllowedModels(pid);
|
|
384
|
+
for (const raw of allowed) {
|
|
385
|
+
const can = `${pid}/${raw}`;
|
|
386
|
+
if (can === canonical) return false;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return true;
|
|
390
|
+
});
|
|
391
|
+
if (aliasEntries.length) {
|
|
392
|
+
console.log(`\n 本地别名 (不在 allowlist 里的遗留映射 ${aliasEntries.length}):`);
|
|
393
|
+
for (const [alias, canonical] of aliasEntries) {
|
|
394
|
+
console.log(` ${canonical} => ${alias}`);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
} catch {}
|
|
398
|
+
console.log(`\npicked only constrains auto; manage with: mslxdff -models (TTY) | mslxdff -model pick <id> | mslxdff -model unpick <id> | mslxdff -model pick clear`);
|
|
399
|
+
}
|
|
400
|
+
} catch (err) {
|
|
401
|
+
console.error(`could not fetch models: ${String(err?.message || err)}`);
|
|
402
|
+
process.exit(1);
|
|
403
|
+
}
|
|
404
|
+
process.exit(0);
|
|
405
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
|
|
3
|
+
export async function handleProviderAdd(id, sub, rest) {
|
|
4
|
+
if (id !== "add") return false;
|
|
5
|
+
const gid = sub;
|
|
6
|
+
const gBase = rest[1];
|
|
7
|
+
const gKey = rest[2];
|
|
8
|
+
if (!gid || !gBase || !gKey) {
|
|
9
|
+
console.error("usage: mslxdff -provider add <id> <baseUrl> <key> [allowedModel...] [--models-path <path>] [--chat-path <path>]");
|
|
10
|
+
console.error(" e.g. mslxdff -provider add myapi https://api.example.com/v1 sk-xxx");
|
|
11
|
+
console.error(" mslxdff -provider add myapi https://api.example.com/v1 sk-xxx gpt-4 gpt-3.5");
|
|
12
|
+
console.error(" mslxdff -provider add myapi https://api.example.com/v1 sk-xxx --models-path /v1/models --chat-path /v1/chat/completions");
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
if (gid === "opencode" || gid === "oc" || gid === "openrouter") {
|
|
16
|
+
console.error(`provider "${gid}" is built-in — use: mslxdff -provider ${gid} add <key> or mslxdff -provider ${gid} set-url <url>`);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
const { saveProviderConfig, loadProviderConfig, loadProviderShareKeys } = await import("../../../state.js");
|
|
20
|
+
const { normalizeProviderId } = await import("../../../providers/model-id.js");
|
|
21
|
+
const nid = normalizeProviderId(gid);
|
|
22
|
+
if (!nid) { console.error(`invalid provider id: ${gid}`); process.exit(1); }
|
|
23
|
+
if (!/^https?:\/\/.+/.test(String(gBase).trim())) { console.error(`invalid baseUrl: ${gBase} (must start with http:// or https://)`); process.exit(1); }
|
|
24
|
+
const cur = loadProviderConfig(nid) || { baseUrl: "", keys: [], allowedModels: [], auths: [], modelsPath: "", chatPath: "" };
|
|
25
|
+
let keys, auths, baseUrl;
|
|
26
|
+
baseUrl = String(gBase).trim();
|
|
27
|
+
let parsedModelsPath = null;
|
|
28
|
+
let parsedChatPath = null;
|
|
29
|
+
const extraTokens = [];
|
|
30
|
+
for (let _i = 3; _i < rest.length; _i++) {
|
|
31
|
+
const tok = String(rest[_i] || "");
|
|
32
|
+
if (tok === "--models-path" || tok === "--modelsPath" || tok === "--models_path") { parsedModelsPath = String(rest[_i + 1] || "").trim() || null; _i++; }
|
|
33
|
+
else if (tok.startsWith("--models-path=")) { parsedModelsPath = tok.slice("--models-path=".length).trim() || null; }
|
|
34
|
+
else if (tok === "--chat-path" || tok === "--chatPath" || tok === "--chat_path") { parsedChatPath = String(rest[_i + 1] || "").trim() || null; _i++; }
|
|
35
|
+
else if (tok.startsWith("--chat-path=")) { parsedChatPath = tok.slice("--chat-path=".length).trim() || null; }
|
|
36
|
+
else extraTokens.push(tok);
|
|
37
|
+
}
|
|
38
|
+
if (parsedModelsPath && !String(parsedModelsPath).startsWith("/")) { console.error(`invalid --models-path: ${parsedModelsPath} (must start with /)`); process.exit(1); }
|
|
39
|
+
if (parsedChatPath && !String(parsedChatPath).startsWith("/")) { console.error(`invalid --chat-path: ${parsedChatPath} (must start with /)`); process.exit(1); }
|
|
40
|
+
const extraModels = extraTokens.filter((x) => x && !String(x).startsWith("-")).map((m) => String(m).trim()).filter(Boolean);
|
|
41
|
+
const allowedModels = extraModels.length ? [...new Set([...(cur.allowedModels || []), ...extraModels])] : (cur.allowedModels || []);
|
|
42
|
+
if (nid === "workbuddy") {
|
|
43
|
+
const token = String(gKey).trim();
|
|
44
|
+
let uid = "";
|
|
45
|
+
try { const payload = JSON.parse(Buffer.from(token.split(".")[1], "base64").toString()); uid = payload.uid || payload.sub || payload.userId || payload.user_id || ""; } catch {}
|
|
46
|
+
if (!uid) uid = "manual-" + token.slice(-8);
|
|
47
|
+
const curKeys = Array.isArray(cur.keys) ? [...cur.keys] : [];
|
|
48
|
+
const curAuths = Array.isArray(cur.auths) ? [...cur.auths] : [];
|
|
49
|
+
let idx2 = curAuths.findIndex(a => a.uid === uid);
|
|
50
|
+
if (idx2 < 0) idx2 = curKeys.findIndex(k => k === token);
|
|
51
|
+
let newKeys, newAuths;
|
|
52
|
+
if (idx2 >= 0) {
|
|
53
|
+
newKeys = [...curKeys]; newKeys[idx2] = token;
|
|
54
|
+
newAuths = [...curAuths]; newAuths[idx2] = { uid, domain: "www.codebuddy.cn", enterpriseId: "", refreshToken: "" };
|
|
55
|
+
while (newAuths.length < newKeys.length) newAuths.push({ uid: `manual-${newKeys[newAuths.length].slice(-8)}`, domain: "www.codebuddy.cn", enterpriseId: "", refreshToken: "" });
|
|
56
|
+
} else {
|
|
57
|
+
newKeys = [...new Set([...curKeys, token].filter(Boolean))];
|
|
58
|
+
newAuths = [...curAuths, { uid, domain: "www.codebuddy.cn", enterpriseId: "", refreshToken: "" }];
|
|
59
|
+
while (newAuths.length < newKeys.length) newAuths.push({ uid: `manual-${newKeys[newAuths.length].slice(-8)}`, domain: "www.codebuddy.cn", enterpriseId: "", refreshToken: "" });
|
|
60
|
+
while (newKeys.length < newAuths.length) newKeys.push(token);
|
|
61
|
+
}
|
|
62
|
+
keys = newKeys; auths = newAuths;
|
|
63
|
+
const cfgToSave = { baseUrl, keys, auths, allowedModels };
|
|
64
|
+
if (parsedModelsPath) cfgToSave.modelsPath = parsedModelsPath;
|
|
65
|
+
else if (cur.modelsPath) cfgToSave.modelsPath = cur.modelsPath;
|
|
66
|
+
if (parsedChatPath) cfgToSave.chatPath = parsedChatPath;
|
|
67
|
+
else if (cur.chatPath) cfgToSave.chatPath = cur.chatPath;
|
|
68
|
+
saveProviderConfig(nid, cfgToSave);
|
|
69
|
+
try {
|
|
70
|
+
const { writeFileSync, mkdirSync } = await import("node:fs");
|
|
71
|
+
const { join } = await import("node:path");
|
|
72
|
+
const dir = process.env.WORKBUDDY_AUTH_DIR || join(process.cwd(), "auths");
|
|
73
|
+
mkdirSync(dir, { recursive: true });
|
|
74
|
+
const fp = join(dir, `workbuddy-${uid}.json`);
|
|
75
|
+
const doc = { account: { uid, enterpriseId: "", nickname: "" }, auth: { accessToken: token, refreshToken: "", expiresAt: Math.floor(Date.now()/1000)+3600, domain: "www.codebuddy.cn" } };
|
|
76
|
+
writeFileSync(fp + ".tmp", JSON.stringify(doc, null, 2), { mode: 0o600 });
|
|
77
|
+
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 }); }
|
|
78
|
+
} catch {}
|
|
79
|
+
} else {
|
|
80
|
+
const trimmed = String(gKey).trim();
|
|
81
|
+
const already = (cur.keys || []).some((k) => String(k).trim() === trimmed);
|
|
82
|
+
if (already) {
|
|
83
|
+
console.log(`key already exists for ${nid} (${trimmed.slice(0, 4)}…${trimmed.slice(-4)}), skipped — still ${cur.keys.length} key(s)`);
|
|
84
|
+
console.log(` use: mslxdff -provider ${nid} list to see keys`);
|
|
85
|
+
process.exit(0);
|
|
86
|
+
}
|
|
87
|
+
keys = [...new Set([...(cur.keys || []), trimmed].filter(Boolean))];
|
|
88
|
+
auths = undefined;
|
|
89
|
+
const cfgToSave2 = { baseUrl, keys, allowedModels };
|
|
90
|
+
if (parsedModelsPath) cfgToSave2.modelsPath = parsedModelsPath;
|
|
91
|
+
else if (cur.modelsPath) cfgToSave2.modelsPath = cur.modelsPath;
|
|
92
|
+
if (parsedChatPath) cfgToSave2.chatPath = parsedChatPath;
|
|
93
|
+
else if (cur.chatPath) cfgToSave2.chatPath = cur.chatPath;
|
|
94
|
+
saveProviderConfig(nid, cfgToSave2);
|
|
95
|
+
}
|
|
96
|
+
console.log(`added generic provider: ${nid}`);
|
|
97
|
+
console.log(` baseUrl: ${String(gBase).trim().replace(/\/+$/, "")}`);
|
|
98
|
+
console.log(` keys: ${keys.length} (${keys.map((k) => `${k.slice(0, 4)}…${k.slice(-4)}`).join(", ")})`);
|
|
99
|
+
if (allowedModels.length) console.log(` allowedModels: ${allowedModels.length} (${allowedModels.join(", ")})`);
|
|
100
|
+
else console.log(` allowedModels: (none — BLOCKED, otherwise unusable) → mslxdff -provider ${nid} allowlist set <model...> OR mslxdff -provider ${nid} allowAny on (allow all)`);
|
|
101
|
+
console.log(` share: ${loadProviderShareKeys(nid) ? "ON" : "off"} (mslxdff -provider ${nid} share on|off)`);
|
|
102
|
+
console.log(` allowAny: OFF (secure, empty allowlist = 403 block before upstream) — enable via: mslxdff -provider ${nid} allowAny on`);
|
|
103
|
+
console.log(` use as: ${nid}/<model-id> — restart daemon to activate`);
|
|
104
|
+
console.log(` NOTE: empty allowlist = 403 before upstream, no cost — must set allowlist to use`);
|
|
105
|
+
process.exit(0);
|
|
106
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
export async function handleProviderAllowlist(id, sub, rest) {
|
|
2
|
+
if (sub === "allowAny" || sub === "allow-any" || sub === "allow_any" || sub === "allowany") {
|
|
3
|
+
const { loadProviderAllowAnyModels, saveProviderAllowAnyModels } = await import("../../../state.js");
|
|
4
|
+
const on = rest[1];
|
|
5
|
+
if (!on) {
|
|
6
|
+
const cur = loadProviderAllowAnyModels(id);
|
|
7
|
+
console.log(`allowAnyModels: ${cur ? "ON (allow all when allowlist empty)" : "OFF (empty allowlist = block all)"}`);
|
|
8
|
+
console.log(` set: mslxdff -provider ${id} allowAny on|off`);
|
|
9
|
+
process.exit(0);
|
|
10
|
+
}
|
|
11
|
+
if (!["on", "off", "1", "0", "true", "false"].includes(String(on).toLowerCase())) {
|
|
12
|
+
console.error(`usage: mslxdff -provider ${id} allowAny on|off`);
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
const state = ["on", "1", "true"].includes(String(on).toLowerCase());
|
|
16
|
+
saveProviderAllowAnyModels(id, state);
|
|
17
|
+
console.log(`allowAnyModels: ${state ? "ON (empty allowlist = allow all)" : "OFF (empty allowlist = block all)"} — takes effect immediately (hot-reloaded)`);
|
|
18
|
+
process.exit(0);
|
|
19
|
+
}
|
|
20
|
+
if (sub === "allowlist" || sub === "allow" || sub === "allowed" || sub === "whitelist") {
|
|
21
|
+
const { loadProviderAllowedModels, saveProviderAllowedModels, loadProviderConfig, loadProviderAllowAnyModels } = await import("../../../state.js");
|
|
22
|
+
const action = rest[1];
|
|
23
|
+
const rawTargets = rest.slice(2);
|
|
24
|
+
if (!action || action === "list" || action === "status") {
|
|
25
|
+
const list = loadProviderAllowedModels(id);
|
|
26
|
+
const cfg = loadProviderConfig(id);
|
|
27
|
+
const baseUrl = cfg?.baseUrl || "";
|
|
28
|
+
const allowAny = loadProviderAllowAnyModels(id);
|
|
29
|
+
console.log(`provider: ${id}${baseUrl ? ` baseUrl: ${baseUrl}` : ""}`);
|
|
30
|
+
if (!list.length) {
|
|
31
|
+
if (allowAny) {
|
|
32
|
+
console.log(` allowedModels: (none — allow all, because allowAny=ON)`);
|
|
33
|
+
console.log(` to secure: mslxdff -provider ${id} allowAny off or mslxdff -provider ${id} allowlist set <model1> <model2> ...`);
|
|
34
|
+
} else {
|
|
35
|
+
console.log(` allowedModels: (none — BLOCK ALL, provider disabled until allowlist set or allowAny ON)`);
|
|
36
|
+
console.log(` set via: mslxdff -provider ${id} allowlist set <model1> <model2> ...`);
|
|
37
|
+
console.log(` or: mslxdff -provider ${id} allowAny on (allow all when allowlist empty)`);
|
|
38
|
+
}
|
|
39
|
+
} else {
|
|
40
|
+
console.log(` allowedModels: ${list.length} model${list.length > 1 ? "s" : ""} (only these can be used)`);
|
|
41
|
+
list.forEach((m, i) => console.log(` [${i + 1}] ${m}`));
|
|
42
|
+
console.log(` manage: mslxdff -provider ${id} allowlist add <model> | remove <model> | set <m1> <m2> ... | clear`);
|
|
43
|
+
console.log(` allowAny: ${allowAny ? "ON" : "OFF"} (empty list behavior) — mslxdff -provider ${id} allowAny on|off`);
|
|
44
|
+
}
|
|
45
|
+
console.log(` NOTE: empty allowlist + allowAny OFF = 403 block (hot-reloaded, no restart needed)`);
|
|
46
|
+
process.exit(0);
|
|
47
|
+
}
|
|
48
|
+
if (action === "clear") {
|
|
49
|
+
saveProviderAllowedModels(id, []);
|
|
50
|
+
const allowAny = loadProviderAllowAnyModels(id);
|
|
51
|
+
console.log(`cleared ${id} allowlist (now ${allowAny ? "allow all (allowAny ON)" : "BLOCK ALL (allowAny OFF)"}) — takes effect immediately (hot-reloaded)`);
|
|
52
|
+
process.exit(0);
|
|
53
|
+
}
|
|
54
|
+
if (action === "set") {
|
|
55
|
+
const models = rawTargets.filter((x) => x && !String(x).startsWith("-")).map((m) => String(m).trim()).filter(Boolean);
|
|
56
|
+
const flat = models.flatMap((m) => String(m).split(",")).map((m) => m.trim()).filter(Boolean);
|
|
57
|
+
if (!flat.length) {
|
|
58
|
+
console.error(`usage: mslxdff -provider ${id} allowlist set <model1> <model2> ...`);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
const saved = saveProviderAllowedModels(id, flat);
|
|
62
|
+
console.log(`set ${id} allowlist: ${saved.length} model${saved.length > 1 ? "s" : ""} (${saved.join(", ")}) — takes effect immediately (hot-reloaded)`);
|
|
63
|
+
process.exit(0);
|
|
64
|
+
}
|
|
65
|
+
if (action === "add") {
|
|
66
|
+
const models = rawTargets.filter((x) => x && !String(x).startsWith("-")).map((m) => String(m).trim()).filter(Boolean);
|
|
67
|
+
const flat = models.flatMap((m) => String(m).split(",")).map((m) => m.trim()).filter(Boolean);
|
|
68
|
+
if (!flat.length) {
|
|
69
|
+
console.error(`usage: mslxdff -provider ${id} allowlist add <model> [model2 ...]`);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
const cur = loadProviderAllowedModels(id);
|
|
73
|
+
const next = [...new Set([...cur, ...flat])];
|
|
74
|
+
saveProviderAllowedModels(id, next);
|
|
75
|
+
console.log(`added ${flat.length} model${flat.length > 1 ? "s" : ""} to ${id} allowlist (now ${next.length}: ${next.join(", ")}) — takes effect immediately (hot-reloaded)`);
|
|
76
|
+
process.exit(0);
|
|
77
|
+
}
|
|
78
|
+
if (action === "remove" || action === "rm" || action === "del") {
|
|
79
|
+
const models = rawTargets.filter((x) => x && !String(x).startsWith("-")).map((m) => String(m).trim()).filter(Boolean);
|
|
80
|
+
const flat = models.flatMap((m) => String(m).split(",")).map((m) => m.trim()).filter(Boolean);
|
|
81
|
+
if (!flat.length) {
|
|
82
|
+
console.error(`usage: mslxdff -provider ${id} allowlist remove <model> [model2 ...]`);
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
const cur = loadProviderAllowedModels(id);
|
|
86
|
+
const set = new Set(flat);
|
|
87
|
+
const next = cur.filter((m) => !set.has(m));
|
|
88
|
+
saveProviderAllowedModels(id, next);
|
|
89
|
+
const { loadProviderAllowAnyModels: _la } = await import("../../../state.js");
|
|
90
|
+
const _allowAny = _la(id);
|
|
91
|
+
console.log(`removed ${cur.length - next.length} model${cur.length - next.length !== 1 ? "s" : ""} from ${id} allowlist (now ${next.length ? next.join(", ") : (_allowAny ? "(allow all)" : "(BLOCK ALL)")}) — takes effect immediately (hot-reloaded)`);
|
|
92
|
+
process.exit(0);
|
|
93
|
+
}
|
|
94
|
+
console.error(`usage: mslxdff -provider ${id} allowlist [list|set|add|remove|clear] [models...]`);
|
|
95
|
+
console.error(` mslxdff -provider ${id} allowAny on|off (empty allowlist = block or allow all)`);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
return false;
|
|
99
|
+
}
|