mslxdff 0.1.54 → 0.1.55
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 +94 -11
- package/package.json +1 -1
- package/src/auto.js +20 -2
- package/src/chooser.js +12 -6
- package/src/state.js +13 -0
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 } 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";
|
|
@@ -185,11 +185,42 @@ if (args.includes("-model") || args.includes("-models")) {
|
|
|
185
185
|
if (sub === "set" && args[idx + 2]) {
|
|
186
186
|
const id = args[idx + 2];
|
|
187
187
|
savePreferredModel(id);
|
|
188
|
+
const picks = [...new Set([...loadModelPicks(), id])];
|
|
189
|
+
saveModelPicks(picks);
|
|
188
190
|
console.log(`default model set to: ${id} (daemon hot-reloads on next request)`);
|
|
191
|
+
console.log(`picked: ${picks.join(", ") || "(none)"} (auto will pick within these)`);
|
|
192
|
+
process.exit(0);
|
|
193
|
+
}
|
|
194
|
+
// -model pick <id> | -model unpick <id> | -model picks : 常用模型勾选集管理(非 TTY 用)
|
|
195
|
+
if (sub === "pick" && args[idx + 2] && args[idx + 2] !== "clear") {
|
|
196
|
+
const picks = [...new Set([...loadModelPicks(), args[idx + 2]])];
|
|
197
|
+
saveModelPicks(picks);
|
|
198
|
+
console.log(`picked: ${picks.join(", ") || "(none)"} (auto will pick within these)`);
|
|
199
|
+
process.exit(0);
|
|
200
|
+
}
|
|
201
|
+
if (sub === "pick" && args[idx + 2] === "clear") {
|
|
202
|
+
saveModelPicks([]);
|
|
203
|
+
console.log("picks cleared — auto uses the full model list again");
|
|
204
|
+
process.exit(0);
|
|
205
|
+
}
|
|
206
|
+
if (sub === "unpick" && args[idx + 2]) {
|
|
207
|
+
const picks = loadModelPicks().filter((x) => x !== args[idx + 2]);
|
|
208
|
+
saveModelPicks(picks);
|
|
209
|
+
console.log(`picked: ${picks.join(", ") || "(none)"}${picks.length === 0 ? " (auto uses full list)" : ""}`);
|
|
210
|
+
process.exit(0);
|
|
211
|
+
}
|
|
212
|
+
if (sub === "picks") {
|
|
213
|
+
const picks = loadModelPicks();
|
|
214
|
+
if (!picks.length) {
|
|
215
|
+
console.log("no picks — auto uses the full model list");
|
|
216
|
+
} else {
|
|
217
|
+
console.log(`${picks.length} picked model(s), auto only selects within these:`);
|
|
218
|
+
}
|
|
219
|
+
for (const id of picks) console.log(` ${id}`);
|
|
189
220
|
process.exit(0);
|
|
190
221
|
}
|
|
191
222
|
if (sub !== undefined && sub !== "list") {
|
|
192
|
-
console.error("usage: mslxdff -models (interactive
|
|
223
|
+
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
224
|
process.exit(1);
|
|
194
225
|
}
|
|
195
226
|
const cacheFile = join(logDir(), "models.json");
|
|
@@ -235,31 +266,35 @@ if (args.includes("-model") || args.includes("-models")) {
|
|
|
235
266
|
console.log("no models available — try: mslxdff -model refresh");
|
|
236
267
|
process.exit(0);
|
|
237
268
|
}
|
|
238
|
-
// TTY
|
|
269
|
+
// TTY:交互式多选勾选常用模型(空格勾选,Enter 保存);非 TTY(管道/脚本):保持纯列表并标注勾选
|
|
239
270
|
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
240
271
|
const statuses = loadModelErrors();
|
|
241
272
|
const current = getPreferredModel();
|
|
273
|
+
const pickedIds = loadModelPicks();
|
|
242
274
|
const items = ids.map((id) => {
|
|
243
275
|
const e = statuses[id];
|
|
244
276
|
return {
|
|
245
277
|
id,
|
|
246
278
|
status: typeof e === "number" ? "error" : e?.status || "normal",
|
|
247
279
|
current: id === current,
|
|
280
|
+
picked: pickedIds.includes(id),
|
|
248
281
|
};
|
|
249
282
|
});
|
|
250
|
-
const
|
|
251
|
-
if (!
|
|
252
|
-
console.log("cancelled —
|
|
283
|
+
const result = await pickInteractiveMulti(items, new Set(pickedIds), Math.max(0, items.findIndex((x) => x.current)));
|
|
284
|
+
if (!result) {
|
|
285
|
+
console.log("cancelled — picks unchanged");
|
|
253
286
|
process.exit(0);
|
|
254
287
|
}
|
|
255
|
-
|
|
256
|
-
console.log(`
|
|
288
|
+
saveModelPicks([...result]);
|
|
289
|
+
console.log(`saved ${result.size} picked model(s): ${[...result].join(", ") || "(none — auto uses full list)"}`);
|
|
257
290
|
process.exit(0);
|
|
258
291
|
}
|
|
259
292
|
const at = cachedAt ? ` (cached ${new Date(cachedAt).toISOString().slice(0, 16).replace("T", " ")})` : "";
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
console.log(
|
|
293
|
+
const pickedIds = loadModelPicks();
|
|
294
|
+
const mark = (id) => (pickedIds.includes(id) ? "*" : " ");
|
|
295
|
+
console.log(`${ids.length} free model(s)${at} (${pickedIds.length} picked, * = picked):`);
|
|
296
|
+
for (const id of ids) console.log(` ${mark(id)} ${id}`);
|
|
297
|
+
console.log(`\npicked only constrains auto; manage with: mslxdff -models (TTY) | mslxdff -model pick <id> | mslxdff -model unpick <id> | mslxdff -model pick clear`);
|
|
263
298
|
} catch (err) {
|
|
264
299
|
console.error(`could not fetch models: ${String(err?.message || err)}`);
|
|
265
300
|
process.exit(1);
|
|
@@ -375,6 +410,53 @@ async function pickInteractive(items, startCursor = 0) {
|
|
|
375
410
|
});
|
|
376
411
|
}
|
|
377
412
|
|
|
413
|
+
// 多选勾选:↑/↓ 移动,Space 勾选/取消,Enter 保存,q/Esc 取消(返回 Set 或 null)
|
|
414
|
+
async function pickInteractiveMulti(items, initialPicked = new Set(), startCursor = 0) {
|
|
415
|
+
const { renderChooser, renderChooserHelp, parseKey } = await import("../src/chooser.js");
|
|
416
|
+
let cursor = Math.min(Math.max(startCursor, 0), items.length - 1);
|
|
417
|
+
const picked = new Set(items.filter((it) => initialPicked.has(it.id)).map((it) => it.id));
|
|
418
|
+
const draw = () => {
|
|
419
|
+
const rows = items.map((it, i) => ({ ...it, picked: picked.has(it.id) }));
|
|
420
|
+
const lines = [...renderChooser(rows, cursor, { multi: true }), ...renderChooserHelp(true)];
|
|
421
|
+
process.stdout.write("\x1b[G\x1b[J" + lines.join("\n"));
|
|
422
|
+
};
|
|
423
|
+
draw();
|
|
424
|
+
return new Promise((resolve) => {
|
|
425
|
+
const wasRaw = process.stdin.isRaw;
|
|
426
|
+
process.stdin.setRawMode(true);
|
|
427
|
+
process.stdin.resume();
|
|
428
|
+
process.stdin.setEncoding("utf8");
|
|
429
|
+
const cleanup = () => {
|
|
430
|
+
process.stdin.removeListener("data", onData);
|
|
431
|
+
process.stdin.setRawMode(false);
|
|
432
|
+
process.stdin.pause();
|
|
433
|
+
process.stdout.write("\n");
|
|
434
|
+
};
|
|
435
|
+
const onData = (chunk) => {
|
|
436
|
+
const key = parseKey(String(chunk));
|
|
437
|
+
if (key === "up") {
|
|
438
|
+
cursor = (cursor - 1 + items.length) % items.length;
|
|
439
|
+
draw();
|
|
440
|
+
} else if (key === "down") {
|
|
441
|
+
cursor = (cursor + 1) % items.length;
|
|
442
|
+
draw();
|
|
443
|
+
} else if (key === "space") {
|
|
444
|
+
const id = items[cursor].id;
|
|
445
|
+
if (picked.has(id)) picked.delete(id);
|
|
446
|
+
else picked.add(id);
|
|
447
|
+
draw();
|
|
448
|
+
} else if (key === "enter") {
|
|
449
|
+
cleanup();
|
|
450
|
+
resolve(new Set(picked));
|
|
451
|
+
} else if (key === "cancel") {
|
|
452
|
+
cleanup();
|
|
453
|
+
resolve(null);
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
process.stdin.on("data", onData);
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
|
|
378
460
|
// -debug: stop the background daemon and run the server in THIS terminal
|
|
379
461
|
// (foreground), printing every event to stdout in real time via the in-memory
|
|
380
462
|
// event bus — no filesystem polling. Ctrl+C / SIGTERM restarts the daemon in
|
|
@@ -876,6 +958,7 @@ const models = createModelsService({
|
|
|
876
958
|
const auto = createAutoSelector({
|
|
877
959
|
cooldownMs: modelCooldownMs(),
|
|
878
960
|
slowCooldownMs: slowCooldownMs(),
|
|
961
|
+
file: defaultStateFile(),
|
|
879
962
|
loadCandidates: async () => {
|
|
880
963
|
try {
|
|
881
964
|
return (await models.get()).data.map((m) => m.id);
|
package/package.json
CHANGED
package/src/auto.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { statSync } from "node:fs";
|
|
2
|
-
import { loadModelErrors, saveModelErrors, loadModelLatencies, saveModelLatencies, loadPreferredModel, defaultStateFile } from "./state.js";
|
|
2
|
+
import { loadModelErrors, saveModelErrors, loadModelLatencies, saveModelLatencies, loadPreferredModel, loadModelPicks, saveModelPicks, defaultStateFile } from "./state.js";
|
|
3
3
|
|
|
4
4
|
// 出厂默认首选模型(state.json 的 preferredModel / env MSLXDFF_PREFERRED_MODEL 可覆盖)
|
|
5
5
|
export const DEFAULT_PREFERRED_MODEL = "big-pickle";
|
|
@@ -129,6 +129,8 @@ export function createAutoSelector({
|
|
|
129
129
|
latencies: seedLatencies,
|
|
130
130
|
persist = (errors, f = file) => saveModelErrors(errors, f ? { file: f } : {}),
|
|
131
131
|
persistLatencies = (latencies, f = file) => saveModelLatencies(latencies, f ? { file: f } : {}),
|
|
132
|
+
loadPicks = () => (file ? loadModelPicks({ file }) : []),
|
|
133
|
+
persistPicks = (picks) => (file ? saveModelPicks(picks, { file }) : picks),
|
|
132
134
|
} = {}) {
|
|
133
135
|
const lastErrorAt = { ...(seedErrors ?? loadModelErrors(file ? { file } : {})) };
|
|
134
136
|
const latencies = { ...(seedLatencies ?? loadModelLatencies(file ? { file } : {})) };
|
|
@@ -144,13 +146,29 @@ export function createAutoSelector({
|
|
|
144
146
|
return [...new Set(list)].filter(Boolean);
|
|
145
147
|
}
|
|
146
148
|
|
|
149
|
+
// 勾选集 = auto 候选池白名单:只在勾选的模型里择优;空勾选或勾选中无可用模型时回退全量
|
|
150
|
+
async function pickedPool(list) {
|
|
151
|
+
const picks = loadPicks();
|
|
152
|
+
if (!picks.length) return list;
|
|
153
|
+
const pickedSet = new Set(picks);
|
|
154
|
+
const filtered = list.filter((id) => pickedSet.has(id));
|
|
155
|
+
return filtered.length ? filtered : list;
|
|
156
|
+
}
|
|
157
|
+
|
|
147
158
|
async function candidates() {
|
|
148
|
-
|
|
159
|
+
const list = await loadList();
|
|
160
|
+
const pool = await pickedPool(list);
|
|
161
|
+
return rankModels(pool, lastErrorAt, { now: now(), cooldownMs, slowCooldownMs, latencies, preferred: getPreferredModel({ file: file ?? undefined }) });
|
|
149
162
|
}
|
|
150
163
|
|
|
151
164
|
async function candidatesFor(requested) {
|
|
152
165
|
if (!requested) return candidates();
|
|
153
166
|
const list = await loadList();
|
|
167
|
+
// 显式指定某模型 = 认可它,自动加入勾选集(仅当它是真实上游 free 模型时,避免垃圾 id 污染)
|
|
168
|
+
const picks = loadPicks();
|
|
169
|
+
if (list.includes(requested) && !picks.includes(requested) && persistPicks) {
|
|
170
|
+
await persistPicks([...picks, requested]);
|
|
171
|
+
}
|
|
154
172
|
const all = list.includes(requested) ? list : [requested, ...list];
|
|
155
173
|
const others = rankModels(all.filter((id) => id !== requested), lastErrorAt, {
|
|
156
174
|
now: now(),
|
package/src/chooser.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
// 交互式模型选择器的纯渲染逻辑(便于测试);键盘循环在 bin/mslxdff.js
|
|
2
2
|
|
|
3
|
-
// items: [{ id, status?, ms?, fail?, current? }];cursor 当前高亮行
|
|
3
|
+
// items: [{ id, status?, ms?, fail?, current?, picked? }];cursor 当前高亮行
|
|
4
|
+
// multi=true 时多选勾选(picked 显示 [x]/[ ]),否则单选默认模型(当前显示 ✓)
|
|
4
5
|
// 返回行数组(无 ANSI 颜色,纯文本标记,Windows 终端友好)
|
|
5
|
-
export function renderChooser(items, cursor = 0) {
|
|
6
|
+
export function renderChooser(items, cursor = 0, { multi = false } = {}) {
|
|
6
7
|
return items.map((it, i) => {
|
|
7
8
|
const arrow = i === cursor ? "❯" : " ";
|
|
8
|
-
const check =
|
|
9
|
+
const check = multi
|
|
10
|
+
? (it.picked ? " [✓]" : " [ ]")
|
|
11
|
+
: (it.current ? " ✓ (current)" : "");
|
|
9
12
|
let state = "";
|
|
10
13
|
if (it.fail) state = ` [fail: ${it.fail}]`;
|
|
11
14
|
else if (it.ms != null) state = ` [${it.ms}ms]`;
|
|
@@ -14,16 +17,19 @@ export function renderChooser(items, cursor = 0) {
|
|
|
14
17
|
});
|
|
15
18
|
}
|
|
16
19
|
|
|
17
|
-
export function renderChooserHelp() {
|
|
18
|
-
return
|
|
20
|
+
export function renderChooserHelp(multi = false) {
|
|
21
|
+
return multi
|
|
22
|
+
? ["", "↑/↓ move · Space toggle pick · Enter save picks · q/Esc cancel"]
|
|
23
|
+
: ["", "↑/↓ move · Enter select as default · q/Esc cancel"];
|
|
19
24
|
}
|
|
20
25
|
|
|
21
|
-
// 解析按键:返回 "up" | "down" | "enter" | "cancel" | null(忽略)
|
|
26
|
+
// 解析按键:返回 "up" | "down" | "enter" | "cancel" | "space" | null(忽略)
|
|
22
27
|
export function parseKey(str) {
|
|
23
28
|
if (!str) return null;
|
|
24
29
|
if (str === "\x1b[A" || str === "k") return "up";
|
|
25
30
|
if (str === "\x1b[B" || str === "j") return "down";
|
|
26
31
|
if (str === "\r" || str === "\n") return "enter";
|
|
32
|
+
if (str === " " || str === "\x1b[32") return "space"; // 空格(32=0x20 的 ASC 表示)或某些终端 terminator
|
|
27
33
|
if (str === "\x1b" || str === "q" || str === "\x03") return "cancel";
|
|
28
34
|
return null;
|
|
29
35
|
}
|
package/src/state.js
CHANGED
|
@@ -201,6 +201,19 @@ export function loadPreferredModel({ file = defaultStateFile() } = {}) {
|
|
|
201
201
|
return typeof v === "string" && v.trim() ? v.trim() : null;
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
+
// 常用模型勾选集(auto 候选池白名单):空数组 = 不启用筛选(全量 auto)
|
|
205
|
+
export function loadModelPicks({ file = defaultStateFile() } = {}) {
|
|
206
|
+
const picks = readState(file).modelPicks;
|
|
207
|
+
if (!Array.isArray(picks)) return [];
|
|
208
|
+
return [...new Set(picks.filter((x) => typeof x === "string" && x.trim().length))];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function saveModelPicks(picks, { file = defaultStateFile() } = {}) {
|
|
212
|
+
const list = [...new Set((Array.isArray(picks) ? picks : []).filter((x) => typeof x === "string" && x.trim().length))];
|
|
213
|
+
writeStateImmediate(file, { modelPicks: list });
|
|
214
|
+
return list;
|
|
215
|
+
}
|
|
216
|
+
|
|
204
217
|
export function savePreferredModel(id, { file = defaultStateFile() } = {}) {
|
|
205
218
|
writeStateImmediate(file, { preferredModel: String(id || "").trim() });
|
|
206
219
|
return String(id || "").trim();
|