dsh-livebench-panel 0.1.6 → 0.1.8
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/lib/client.js +116 -59
- package/lib/index.js +102 -16
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -98,10 +98,12 @@ window.__ModuleLoader__.load({
|
|
|
98
98
|
const [log, setLog] = useState("");
|
|
99
99
|
const [exitCode, setExitCode] = useState(null);
|
|
100
100
|
const [startError, setStartError] = useState(null);
|
|
101
|
+
const [homeInput, setHomeInput] = useState("");
|
|
102
|
+
const [homeBusy, setHomeBusy] = useState(false);
|
|
101
103
|
const [results, setResults] = useState(null);
|
|
102
|
-
const [
|
|
103
|
-
const [
|
|
104
|
-
try { return JSON.parse(localStorage.getItem("
|
|
104
|
+
const [pickedModels, setPickedModels] = useState(() => new Set());
|
|
105
|
+
const [modelOrder, setModelOrder] = useState(() => {
|
|
106
|
+
try { return JSON.parse(localStorage.getItem("dlb_model_order_v1")) ?? []; } catch { return []; }
|
|
105
107
|
});
|
|
106
108
|
const dragKey = useRef(null);
|
|
107
109
|
const logRef = useRef(null);
|
|
@@ -253,6 +255,24 @@ window.__ModuleLoader__.load({
|
|
|
253
255
|
await refreshStatus();
|
|
254
256
|
};
|
|
255
257
|
|
|
258
|
+
const saveHome = async () => {
|
|
259
|
+
setHomeBusy(true);
|
|
260
|
+
setStartError(null);
|
|
261
|
+
try {
|
|
262
|
+
const payload = await api("/home", {
|
|
263
|
+
method: "POST",
|
|
264
|
+
headers: { "content-type": "application/json" },
|
|
265
|
+
body: JSON.stringify({ path: homeInput }),
|
|
266
|
+
});
|
|
267
|
+
if (!payload.ok) setStartError(payload.error ?? "设置失败");
|
|
268
|
+
else { setHomeInput(""); await loadConfig(); }
|
|
269
|
+
} catch (error) {
|
|
270
|
+
setStartError(String(error.message ?? error));
|
|
271
|
+
} finally {
|
|
272
|
+
setHomeBusy(false);
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
|
|
256
276
|
const setField = (key) => (event) => {
|
|
257
277
|
const value = event.target.value;
|
|
258
278
|
setSel((prev) => {
|
|
@@ -265,52 +285,75 @@ window.__ModuleLoader__.load({
|
|
|
265
285
|
});
|
|
266
286
|
};
|
|
267
287
|
|
|
268
|
-
// ----
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
288
|
+
// ---- 成绩矩阵:model 为行标识、category/task 为列标识 ----
|
|
289
|
+
// 附带:time 列(该模型评测开始时间 = 行内最早判分时间)、
|
|
290
|
+
// 行拖拽排序(localStorage 持久化)、行选择与单行/批量删除。
|
|
291
|
+
const taskKey = (category, task) => `${category}/${task}`;
|
|
292
|
+
const matrix = useMemo(() => {
|
|
293
|
+
if (!results) return null;
|
|
294
|
+
const models = [...new Set(results.rows.map((r) => r.model))].sort();
|
|
295
|
+
const tasks = [...new Set(results.rows.map((r) => taskKey(r.category, r.task)))].sort();
|
|
296
|
+
const cells = new Map(); // `${model}\u0000${taskKey}` -> row
|
|
297
|
+
const startTimes = new Map(); // model -> min tstamp
|
|
298
|
+
for (const row of results.rows) {
|
|
299
|
+
cells.set(`${row.model}\u0000${taskKey(row.category, row.task)}`, row);
|
|
300
|
+
const prev = startTimes.get(row.model);
|
|
301
|
+
if (row.time && (prev === undefined || row.time < prev)) startTimes.set(row.model, row.time);
|
|
302
|
+
}
|
|
303
|
+
return { models, tasks, cells, startTimes };
|
|
304
|
+
}, [results]);
|
|
305
|
+
const sortedModels = useMemo(() => {
|
|
306
|
+
if (!matrix) return [];
|
|
307
|
+
const indexOf = new Map(modelOrder.map((key, index) => [key, index]));
|
|
308
|
+
return [...matrix.models].sort((a, b) => {
|
|
309
|
+
const ia = indexOf.has(a) ? indexOf.get(a) : Number.MAX_SAFE_INTEGER;
|
|
310
|
+
const ib = indexOf.has(b) ? indexOf.get(b) : Number.MAX_SAFE_INTEGER;
|
|
276
311
|
if (ia !== ib) return ia - ib;
|
|
277
|
-
return (b
|
|
312
|
+
return a.localeCompare(b);
|
|
278
313
|
});
|
|
279
|
-
}, [
|
|
314
|
+
}, [matrix, modelOrder]);
|
|
280
315
|
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
try { localStorage.setItem("
|
|
316
|
+
const persistModelOrder = (keys) => {
|
|
317
|
+
setModelOrder(keys);
|
|
318
|
+
try { localStorage.setItem("dlb_model_order_v1", JSON.stringify(keys)); } catch { /* private mode */ }
|
|
284
319
|
};
|
|
285
320
|
|
|
286
|
-
const onRowDrop = (
|
|
287
|
-
const
|
|
321
|
+
const onRowDrop = (targetModel) => {
|
|
322
|
+
const sourceModel = dragKey.current;
|
|
288
323
|
dragKey.current = null;
|
|
289
|
-
if (!
|
|
290
|
-
const keys =
|
|
291
|
-
const from = keys.indexOf(
|
|
292
|
-
const to = keys.indexOf(
|
|
324
|
+
if (!sourceModel || sourceModel === targetModel) return;
|
|
325
|
+
const keys = sortedModels.slice();
|
|
326
|
+
const from = keys.indexOf(sourceModel);
|
|
327
|
+
const to = keys.indexOf(targetModel);
|
|
293
328
|
if (from < 0 || to < 0) return;
|
|
294
329
|
keys.splice(to, 0, keys.splice(from, 1)[0]);
|
|
295
|
-
|
|
330
|
+
persistModelOrder(keys);
|
|
296
331
|
};
|
|
297
332
|
|
|
298
|
-
const togglePicked = (
|
|
333
|
+
const togglePicked = (model) => setPickedModels((prev) => {
|
|
299
334
|
const next = new Set(prev);
|
|
300
|
-
if (next.has(
|
|
335
|
+
if (next.has(model)) next.delete(model); else next.add(model);
|
|
301
336
|
return next;
|
|
302
337
|
});
|
|
303
338
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
if (!
|
|
339
|
+
// 删除一个模型行 = 删除该模型在所有任务下的判分与答案记录
|
|
340
|
+
const deleteModels = async (modelsToDelete) => {
|
|
341
|
+
if (!results || modelsToDelete.length === 0) return;
|
|
342
|
+
const serverRows = [];
|
|
343
|
+
for (const row of results.rows) {
|
|
344
|
+
if (modelsToDelete.includes(row.model)) {
|
|
345
|
+
serverRows.push({ model: row.model, category: row.category, task: row.task });
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
if (serverRows.length === 0) return;
|
|
349
|
+
if (!window.confirm(`确认删除所选 ${modelsToDelete.length} 个模型的全部评测成绩(共 ${serverRows.length} 条记录,含答案)?不可恢复。`)) return;
|
|
307
350
|
await api("/delete", {
|
|
308
351
|
method: "POST",
|
|
309
352
|
headers: { "content-type": "application/json" },
|
|
310
|
-
body: JSON.stringify({ rows:
|
|
353
|
+
body: JSON.stringify({ rows: serverRows }),
|
|
311
354
|
});
|
|
312
|
-
|
|
313
|
-
|
|
355
|
+
setPickedModels(new Set());
|
|
356
|
+
persistModelOrder(modelOrder.filter((model) => !modelsToDelete.includes(model)));
|
|
314
357
|
await loadResults();
|
|
315
358
|
};
|
|
316
359
|
|
|
@@ -324,10 +367,21 @@ window.__ModuleLoader__.load({
|
|
|
324
367
|
return h("div", { className: c("root") },
|
|
325
368
|
h("div", { className: c("head") },
|
|
326
369
|
h("h2", null, "LiveBench"),
|
|
327
|
-
h("span", { className: c("sub") },
|
|
370
|
+
h("span", { className: c("sub") }, config?.root ? `LiveBench 评测面板 · ${config.root}` : "LiveBench 评测面板"),
|
|
328
371
|
h("span", { className: c("badge"), "data-ok": config?.available ? "1" : "0" },
|
|
329
372
|
config === null ? "检测中…" : config.available ? "LiveBench 就绪" : "未找到 LiveBench"),
|
|
330
373
|
),
|
|
374
|
+
h("div", { className: c("row") },
|
|
375
|
+
h("span", { className: c("label") }, "LiveBench 路径"),
|
|
376
|
+
h("input", {
|
|
377
|
+
className: c("input"), style: { flex: "1", minWidth: "240px" },
|
|
378
|
+
placeholder: config?.root ?? "V:\\…\\LiveBench 或 ~/LiveBench",
|
|
379
|
+
value: homeInput, onChange: (event) => setHomeInput(event.target.value),
|
|
380
|
+
}),
|
|
381
|
+
h("button", { className: c("btnGhost") + " " + c("btn"), disabled: homeBusy || homeInput.trim().length === 0, onClick: saveHome },
|
|
382
|
+
homeBusy ? "保存中…" : "设置路径"),
|
|
383
|
+
h("span", { className: c("hint") }, "需含 livebench\\run_livebench.py 与 .venv;也可用环境变量 DSH_LIVEBENCH_HOME"),
|
|
384
|
+
),
|
|
331
385
|
configError !== null && h("p", { className: c("error") }, `加载配置失败:${configError}`),
|
|
332
386
|
h("div", { className: c("card") },
|
|
333
387
|
h("div", { className: c("grid") },
|
|
@@ -405,50 +459,53 @@ window.__ModuleLoader__.load({
|
|
|
405
459
|
),
|
|
406
460
|
h("pre", { className: c("log"), ref: logRef }, log || "(暂无输出)"),
|
|
407
461
|
),
|
|
408
|
-
|
|
462
|
+
sortedModels.length > 0 && h("div", { className: c("card") },
|
|
409
463
|
h("div", { className: c("row") },
|
|
410
|
-
h("span", { className: c("label") }, "
|
|
464
|
+
h("span", { className: c("label") }, "评测成绩(model 为行;分数 = 平均分 ×100;行首可拖动排序,顺序保存在本机)"),
|
|
411
465
|
h("button", {
|
|
412
466
|
className: c("btn") + " " + c("btnDanger"),
|
|
413
|
-
disabled:
|
|
414
|
-
onClick: () =>
|
|
415
|
-
}, `删除所选(${
|
|
467
|
+
disabled: pickedModels.size === 0,
|
|
468
|
+
onClick: () => deleteModels([...pickedModels]),
|
|
469
|
+
}, `删除所选(${pickedModels.size})`),
|
|
416
470
|
),
|
|
417
471
|
h("table", { className: c("table") },
|
|
418
472
|
h("thead", null, h("tr", null,
|
|
419
|
-
h("th", { style: { width: "
|
|
473
|
+
h("th", { style: { width: "26px" } }, "⠿"),
|
|
420
474
|
h("th", { style: { width: "30px" } },
|
|
421
|
-
h("input", { type: "checkbox", checked:
|
|
422
|
-
onChange: (event) =>
|
|
423
|
-
h("th", null, "time"),
|
|
475
|
+
h("input", { type: "checkbox", checked: pickedModels.size > 0 && pickedModels.size === sortedModels.length,
|
|
476
|
+
onChange: (event) => setPickedModels(event.target.checked ? new Set(sortedModels) : new Set()) })),
|
|
424
477
|
h("th", null, "model"),
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
478
|
+
matrix.tasks.map((taskKeyCol) => {
|
|
479
|
+
const [cat, task] = taskKeyCol.split("/");
|
|
480
|
+
return h("th", { key: taskKeyCol, title: `${cat} / ${task}` }, task);
|
|
481
|
+
}),
|
|
482
|
+
h("th", null, "删除"),
|
|
483
|
+
h("th", null, "time"),
|
|
429
484
|
)),
|
|
430
485
|
h("tbody", null,
|
|
431
|
-
|
|
432
|
-
const
|
|
433
|
-
const
|
|
486
|
+
sortedModels.map((model) => {
|
|
487
|
+
const checked = pickedModels.has(model);
|
|
488
|
+
const startTime = matrix.startTimes.get(model);
|
|
434
489
|
return h("tr", {
|
|
435
|
-
key,
|
|
490
|
+
key: model,
|
|
436
491
|
draggable: true,
|
|
437
|
-
onDragStart: () => { dragKey.current =
|
|
492
|
+
onDragStart: () => { dragKey.current = model; },
|
|
438
493
|
onDragOver: (event) => event.preventDefault(),
|
|
439
|
-
onDrop: () => onRowDrop(
|
|
494
|
+
onDrop: () => onRowDrop(model),
|
|
440
495
|
},
|
|
441
496
|
h("td", { className: c("drag"), title: "拖动排序" }, "⠿"),
|
|
442
|
-
h("td", null, h("input", { type: "checkbox", checked, onChange: () => togglePicked(
|
|
443
|
-
h("td", null,
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
497
|
+
h("td", null, h("input", { type: "checkbox", checked, onChange: () => togglePicked(model) })),
|
|
498
|
+
h("td", null, model),
|
|
499
|
+
matrix.tasks.map((taskKeyCol) => {
|
|
500
|
+
const row = matrix.cells.get(`${model}\u0000${taskKeyCol}`);
|
|
501
|
+
return h("td", { key: taskKeyCol, className: c("score") },
|
|
502
|
+
row ? `${row.score.toFixed(1)} (${row.judged}/${row.total})` : "—");
|
|
503
|
+
}),
|
|
448
504
|
h("td", null, h("button", {
|
|
449
|
-
className: c("btnGhost") + " " + c("btn"), title: "
|
|
450
|
-
onClick: () =>
|
|
505
|
+
className: c("btnGhost") + " " + c("btn"), title: "删除该模型的所有评测成绩",
|
|
506
|
+
onClick: () => deleteModels([model]),
|
|
451
507
|
}, "删除")),
|
|
508
|
+
h("td", null, fmtTime(startTime)),
|
|
452
509
|
);
|
|
453
510
|
})),
|
|
454
511
|
),
|
package/lib/index.js
CHANGED
|
@@ -39,8 +39,61 @@ const inject = ["webServer"];
|
|
|
39
39
|
const API = "/dsh-livebench-panel/api";
|
|
40
40
|
/** Profile whose settings.yaml holds the harness provider config. */
|
|
41
41
|
const PROFILE = "web";
|
|
42
|
-
/** LiveBench
|
|
43
|
-
const
|
|
42
|
+
/** LiveBench location resolution: saved user path > env var > user home > legacy. */
|
|
43
|
+
const CONFIG_FILE = join(process.env.DSH_HOME ?? join(homedir(), ".dsh"), "livebench-panel.json");
|
|
44
|
+
const LEGACY_LIVEBENCH_HOME = "V:\\PythonProject\\C_UtilizeSpace\\LiveBench";
|
|
45
|
+
|
|
46
|
+
/** Read the user-saved LiveBench path (set from the panel UI). */
|
|
47
|
+
function savedLivebenchHome() {
|
|
48
|
+
try {
|
|
49
|
+
const path = JSON.parse(readFileSync(CONFIG_FILE, "utf8")).livebenchHome;
|
|
50
|
+
return typeof path === "string" && path.trim().length > 0 ? path.trim() : null;
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Candidate roots, most explicit first. Windows and Linux both covered. */
|
|
57
|
+
function livebenchCandidates() {
|
|
58
|
+
return [
|
|
59
|
+
savedLivebenchHome(),
|
|
60
|
+
process.env.DSH_LIVEBENCH_HOME ?? null,
|
|
61
|
+
join(homedir(), "LiveBench"),
|
|
62
|
+
LEGACY_LIVEBENCH_HOME,
|
|
63
|
+
].filter((candidate) => typeof candidate === "string" && candidate.length > 0);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** A root counts as a LiveBench checkout when the venv python and entry script exist. */
|
|
67
|
+
function isLivebenchRoot(root) {
|
|
68
|
+
return existsSync(join(root, ".venv", "Scripts", "python.exe")) || existsSync(join(root, ".venv", "bin", "python"))
|
|
69
|
+
? existsSync(join(root, "livebench", "run_livebench.py"))
|
|
70
|
+
: false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Resolve the LiveBench layout from the first valid candidate. */
|
|
74
|
+
function livebenchLayout() {
|
|
75
|
+
const candidates = livebenchCandidates();
|
|
76
|
+
for (const root of candidates) {
|
|
77
|
+
const pythonExe = join(root, ".venv", "Scripts", process.platform === "win32" ? "python.exe" : "python");
|
|
78
|
+
if (existsSync(pythonExe) && existsSync(join(root, "livebench", "run_livebench.py"))) {
|
|
79
|
+
return {
|
|
80
|
+
root,
|
|
81
|
+
livebenchDir: join(root, "livebench"),
|
|
82
|
+
dataDir: join(root, "livebench", "data", "live_bench"),
|
|
83
|
+
pythonExe,
|
|
84
|
+
available: true,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const root = candidates[0] ?? join(homedir(), "LiveBench");
|
|
89
|
+
return {
|
|
90
|
+
root,
|
|
91
|
+
livebenchDir: join(root, "livebench"),
|
|
92
|
+
dataDir: join(root, "livebench", "data", "live_bench"),
|
|
93
|
+
pythonExe: join(root, ".venv", process.platform === "win32" ? join("Scripts", "python.exe") : join("bin", "python")),
|
|
94
|
+
available: false,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
44
97
|
/** Question-set releases LiveBench accepts (mirrors livebench/common.py). */
|
|
45
98
|
const RELEASES = [
|
|
46
99
|
"2024-06-24", "2024-07-26", "2024-08-31", "2024-11-25",
|
|
@@ -210,19 +263,6 @@ function effortsOfModel(model) {
|
|
|
210
263
|
return efforts;
|
|
211
264
|
}
|
|
212
265
|
|
|
213
|
-
/** LiveBench layout on disk. */
|
|
214
|
-
function livebenchLayout() {
|
|
215
|
-
const root = process.env.DSH_LIVEBENCH_HOME ?? DEFAULT_LIVEBENCH_HOME;
|
|
216
|
-
const venvPython = join(root, ".venv", "Scripts", "python.exe");
|
|
217
|
-
return {
|
|
218
|
-
root,
|
|
219
|
-
livebenchDir: join(root, "livebench"),
|
|
220
|
-
dataDir: join(root, "livebench", "data", "live_bench"),
|
|
221
|
-
pythonExe: venvPython,
|
|
222
|
-
available: existsSync(venvPython) && existsSync(join(root, "livebench", "run_livebench.py")),
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
|
|
226
266
|
/** Scan data/live_bench/<category>/<task>/question.jsonl into a category→tasks map. */
|
|
227
267
|
function scanCategoryTasks(dataDir) {
|
|
228
268
|
const tasks = {};
|
|
@@ -695,7 +735,14 @@ function apply(ctx) {
|
|
|
695
735
|
return;
|
|
696
736
|
}
|
|
697
737
|
try {
|
|
698
|
-
|
|
738
|
+
if (process.platform === "win32") {
|
|
739
|
+
// proc.kill() only terminates run_livebench.py itself; its child
|
|
740
|
+
// (cmd → gen_api_answer.py → …) would survive. taskkill /T /F
|
|
741
|
+
// takes down the whole tree.
|
|
742
|
+
spawn("taskkill", ["/PID", String(run.proc.pid), "/T", "/F"], { windowsHide: true });
|
|
743
|
+
} else {
|
|
744
|
+
run.proc.kill();
|
|
745
|
+
}
|
|
699
746
|
} catch (error) {
|
|
700
747
|
sendJson(res, 500, { ok: false, error: String(error.message ?? error) });
|
|
701
748
|
return;
|
|
@@ -718,6 +765,45 @@ function apply(ctx) {
|
|
|
718
765
|
},
|
|
719
766
|
}), `${name}: results route`);
|
|
720
767
|
|
|
768
|
+
ctx.effect(() => ctx.webServer.register({
|
|
769
|
+
kind: "exact",
|
|
770
|
+
path: `${API}/home`,
|
|
771
|
+
handler: async (req, res) => {
|
|
772
|
+
if (req.method !== "POST") {
|
|
773
|
+
sendJson(res, 405, { ok: false, error: "method not allowed" });
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
if (!originAllowed(req)) {
|
|
777
|
+
sendJson(res, 403, { ok: false, error: "forbidden origin" });
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
let body;
|
|
781
|
+
try {
|
|
782
|
+
body = JSON.parse(await readBody(req));
|
|
783
|
+
} catch {
|
|
784
|
+
sendJson(res, 400, { ok: false, error: "invalid request body" });
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
const path = typeof body.path === "string" ? body.path.trim() : "";
|
|
788
|
+
if (path.length === 0 || path.length > 260 || /[<>:"|?*\0]/.test(path.replace(/^[A-Za-z]:/, ""))) {
|
|
789
|
+
sendJson(res, 400, { ok: false, error: "invalid path" });
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
if (!existsSync(join(path, "livebench", "run_livebench.py"))) {
|
|
793
|
+
sendJson(res, 400, { ok: false, error: `该目录下没有 livebench\\run_livebench.py:${path}` });
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
try {
|
|
797
|
+
writeFileSync(CONFIG_FILE, JSON.stringify({ livebenchHome: path }, null, 2), "utf8");
|
|
798
|
+
} catch (error) {
|
|
799
|
+
sendJson(res, 500, { ok: false, error: `cannot save config: ${error.message}` });
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
const layout = livebenchLayout();
|
|
803
|
+
sendJson(res, 200, { ok: true, root: layout.root, available: layout.available });
|
|
804
|
+
},
|
|
805
|
+
}), `${name}: home route`);
|
|
806
|
+
|
|
721
807
|
ctx.effect(() => ctx.webServer.register({
|
|
722
808
|
kind: "exact",
|
|
723
809
|
path: `${API}/delete`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-livebench-panel",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "DSH web plugin: a LiveBench tab in the Trajectory view (right of 对话/轨迹). Run LiveBench evaluations against every model configured in the DeepSeek Harness — pick provider/model, category, task, release and question range from dropdowns, watch progress, and read scores in place.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|