dsh-livebench-panel 0.2.21 → 0.2.23

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 CHANGED
@@ -805,10 +805,14 @@ window.__ModuleLoader__.load({
805
805
  row.errors > 0 ? `${row.errors} 题 API 失败` : null,
806
806
  emptyCount > 0 ? `${emptyCount} 题思考占满 max-tokens、未产出答案` : null,
807
807
  ].filter(Boolean).join(",");
808
+ const missing = Array.isArray(row.missingIndexes) ? row.missingIndexes : null;
809
+ const missingText = missing !== null && missing.length > 0
810
+ ? `\n没做出来的题号(0 起,可复制去重跑):${missing.join("、")}`
811
+ : "";
808
812
  const title = basePrefix + (allFailed
809
813
  ? `本次选择 ${row.configured ?? 0} 题,全部没做出来(${reasons}),不计入正确率 ${sub}`
810
814
  : `本次选择 ${row.configured ?? 0} 题,做出来 ${row.done ?? 0} 题,没做出来 ${row.notDone ?? 0} 题 ${sub}`
811
- + (reasons ? `(其中 ${reasons})` : "")); if (allFailed || row.judged === 0) {
815
+ + (reasons ? `(其中 ${reasons})` : "")) + missingText; if (allFailed || row.judged === 0) {
812
816
  return h("td", { key: taskKeyCol, className: cls, title },
813
817
  h("div", { className: c("dim") }, "—"),
814
818
  h("div", { className: c("cellSub") }, sub));
package/lib/index.js CHANGED
@@ -627,6 +627,73 @@ function isEmptyAnswerLine(line) {
627
627
  return EMPTY_ANSWER_RE.test(line);
628
628
  }
629
629
 
630
+ /** 从答案行里抽 question_id(只做正则,不 JSON.parse)。 */
631
+ const QUESTION_ID_RE = /"question_id"\s*:\s*"([A-Za-z0-9_.:-]{8,120})"/;
632
+
633
+ /**
634
+ * 「题目序号范围」的边界是否真的设了。
635
+ *
636
+ * 注意:面板表单里没填时提交的是**空字符串**,不是 null。旧代码只判 `!== null/undefined`,
637
+ * 于是 `""` 被当成"设了范围",`Number("") === 0` → 范围变成 0..0,只检查第 0 题。
638
+ */
639
+ function isRangeBoundSet(value) {
640
+ return value !== null && value !== undefined && String(value).trim().length > 0;
641
+ }
642
+
643
+ /** 把范围边界规范成 number 或 null(空串一律当没设)。 */
644
+ function normalizeRangeBound(value) {
645
+ return isRangeBoundSet(value) ? Number(value) : null;
646
+ }
647
+
648
+ /**
649
+ * 取某次运行的 baseline 有序 id 列表。
650
+ * 新元数据直接读 baselineIds;老元数据(加字段之前启动的运行)从
651
+ * baseline / baselineTask / baselinePicks 反查 BASELINE_SETS 还原。
652
+ * @returns {string[]|null}
653
+ */
654
+ function baselineIdsFor(meta) {
655
+ if (!meta) return null;
656
+ if (Array.isArray(meta.baselineIds) && meta.baselineIds.length > 0) return meta.baselineIds;
657
+ const set = typeof meta.baseline === "string" ? BASELINE_SETS[meta.baseline] : null;
658
+ const taskDef = set && typeof meta.baselineTask === "string"
659
+ ? (set.tasks ?? []).find((t) => t.task === meta.baselineTask) ?? null
660
+ : null;
661
+ if (!taskDef || !Array.isArray(meta.baselinePicks) || meta.baselinePicks.length === 0) return null;
662
+ const merged = [];
663
+ for (const pick of meta.baselinePicks) {
664
+ if (pick !== "passed" && pick !== "failed") continue;
665
+ for (const qid of taskDef[pick]) if (!merged.includes(qid)) merged.push(qid);
666
+ }
667
+ return merged.length > 0 ? merged : null;
668
+ }
669
+
670
+ /**
671
+ * Baseline 评测里"没做出来的题号"。
672
+ *
673
+ * 「没做出来」= 压根没跑 + 跑了但空答案(思考吃满 max-tokens)+ 跑了但 API 失败,
674
+ * 因为它们都该从正确率分母里排除,也都是用户想补跑的题。
675
+ *
676
+ * 编号是 **该题库内的 0 起下标**,与「题目序号范围」的编号一致,可以直接复制去重跑。
677
+ * 范围先按题库实际题数裁剪 —— 「0-1000」这种越界输入不会把编号算歪(基准永远是实际题数)。
678
+ *
679
+ * @param {object|undefined} meta 运行元数据(baselineIds,或可由 baseline 反查)
680
+ * @param {Set<string>} okIds 答案文件里有**有效答案**的 question_id
681
+ * @returns {number[]|null} 不是 baseline 运行时返回 null
682
+ */
683
+ function missingBaselineIndexes(meta, okIds) {
684
+ const ids = baselineIdsFor(meta);
685
+ if (ids === null) return null;
686
+ const hasBegin = isRangeBoundSet(meta.begin);
687
+ const hasEnd = isRangeBoundSet(meta.end);
688
+ const from = hasBegin ? Math.max(0, Math.min(ids.length, Number(meta.begin))) : 0;
689
+ const to = hasEnd ? Math.max(0, Math.min(ids.length, Number(meta.end) + 1)) : ids.length;
690
+ const out = [];
691
+ for (let i = from; i < Math.max(from, to); i += 1) {
692
+ if (!okIds.has(ids[i])) out.push(i);
693
+ }
694
+ return out;
695
+ }
696
+
630
697
  async function readTextFileAsync(path) {
631
698
  const fsPromises = await import("node:fs/promises");
632
699
  try {
@@ -668,12 +735,25 @@ async function computeResults(dataDir) {
668
735
  const text = await readTextFileAsync(join(answerDir, file));
669
736
  if (text === null) continue;
670
737
  const key = `${model}\u0000${task}`;
671
- const stat = answers.get(key) ?? { answered: 0, errors: 0, empty: 0 };
738
+ const stat = answers.get(key) ?? { answered: 0, errors: 0, empty: 0, okIds: null };
739
+ // baseline 评测要回答"少做了哪几个题",所以需要收集**有有效答案**的 question_id。
740
+ // 「没做出来」= 压根没跑 + 跑了但空答案 + 跑了但 API 失败,三者都算,
741
+ // 因为它们都该从正确率分母里排除,也都是用户想补跑的题。
742
+ // 只用正则抽 id(不 JSON.parse),对几百 KB~几 MB 的答案文件也够快。
743
+ const runMetaEntry = runMeta.get(model);
744
+ const trackIds = baselineIdsFor(runMetaEntry) !== null;
745
+ if (trackIds && stat.okIds === null) stat.okIds = new Set();
672
746
  for (const line of text.split("\n")) {
673
747
  if (line.trim().length === 0) continue;
674
748
  stat.answered += 1;
675
- if (line.includes('"$ERROR$"')) stat.errors += 1;
676
- else if (isEmptyAnswerLine(line)) stat.empty += 1;
749
+ const isError = line.includes('"$ERROR$"');
750
+ const isEmpty = !isError && isEmptyAnswerLine(line);
751
+ if (isError) stat.errors += 1;
752
+ else if (isEmpty) stat.empty += 1;
753
+ if (trackIds && !isError && !isEmpty) {
754
+ const idMatch = line.match(QUESTION_ID_RE);
755
+ if (idMatch !== null) stat.okIds.add(idMatch[1]);
756
+ }
677
757
  }
678
758
  // 空文件(尚未产出答案,或成绩已被删除只留下 0 字节壳)不计入结果:
679
759
  // 否则删掉的记录会在下一次扫描时凭“文件名仍在”重新冒出来。
@@ -740,10 +820,14 @@ async function computeResults(dataDir) {
740
820
  ? Number(meta.baselineSize)
741
821
  : null;
742
822
  const scopeTotal = baselineTotal ?? byRelease ?? total;
743
- const hasRange = meta.begin !== null && meta.begin !== undefined && meta.end !== null && meta.end !== undefined;
823
+ const hasRange = isRangeBoundSet(meta.begin) && isRangeBoundSet(meta.end);
744
824
  if (hasRange) {
745
825
  const len = clampedRangeLength(scopeTotal, Number(meta.begin), Number(meta.end));
746
826
  if (len !== null) configured = len;
827
+ } else if (baselineTotal !== null) {
828
+ // baseline 且没设范围 = 整个题库,所以"选择题数"恒等于题库大小,
829
+ // 而不是当前已答题数(否则跑到一半会显示"选择 6 题")
830
+ configured = baselineTotal;
747
831
  }
748
832
  }
749
833
  }
@@ -753,6 +837,9 @@ async function computeResults(dataDir) {
753
837
  // 正确率 = 做对的题 / **做出来的题**(API 失败、思考吃满 token 没产出答案的都不进分母)
754
838
  const judgedDone = Math.max(0, judgedCount - notProduced);
755
839
  const score = entry && judgedDone > 0 ? (entry.sum / judgedDone) * 100 : null;
840
+ // Baseline 评测:算出**具体哪几个题号没做出来**(题号 = 该题库内的 0 起下标,
841
+ // 与「题目序号范围」的编号一致,方便直接复制去重跑)。
842
+ const missingIndexes = missingBaselineIndexes(meta, stat.okIds ?? new Set());
756
843
  return {
757
844
  model,
758
845
  category,
@@ -768,6 +855,8 @@ async function computeResults(dataDir) {
768
855
  done,
769
856
  configured,
770
857
  notDone,
858
+ // baseline 专有:没做出来的题号(0 起,相对该题库)
859
+ missingIndexes,
771
860
  // 本次运行的开始时间(ISO);旧数据没有元数据时为 null,前端退回解析 displayName 里的运行戳
772
861
  runStartedAt: meta !== undefined && typeof meta.startedAt === "string" ? meta.startedAt : null,
773
862
  // 非 null 表示这一行跑的是 Baseline 题库子集,不是该任务全量
@@ -1080,8 +1169,9 @@ function apply(ctx) {
1080
1169
  if (!existsSync(metaDir)) mkdirSync(metaDir, { recursive: true });
1081
1170
  writeFileSync(join(metaDir, displayName + ".json"), JSON.stringify({
1082
1171
  benchNames: benchNames !== null ? benchNames : [benchParts.join("/")],
1083
- begin: body.begin ?? null,
1084
- end: body.end ?? null,
1172
+ // 空串要存成 null:否则读取端会把「没填」当成"范围 0..0"
1173
+ begin: normalizeRangeBound(body.begin),
1174
+ end: normalizeRangeBound(body.end),
1085
1175
  // 算"本次设定题数"要用 release 过滤后的有效题数来裁剪边界,所以必须记下来
1086
1176
  release,
1087
1177
  // baseline 评测标出来:成绩表里同一列会混着"全量"和"题库子集"两种分数,
@@ -1093,6 +1183,8 @@ function apply(ctx) {
1093
1183
  baselineTask: baselinePick !== null ? baselinePick.task : null,
1094
1184
  baselinePicks: baselinePick !== null ? baselinePick.picks : null,
1095
1185
  baselineSize: baselinePick !== null ? baselinePick.ids.length : null,
1186
+ // 实际使用的有序 id 列表:/results 靠它算出"少做了哪几题"
1187
+ baselineIds: baselinePick !== null ? baselinePick.ids : null,
1096
1188
  startedAt: new Date().toISOString(),
1097
1189
  }), "utf8");
1098
1190
  } catch { /* 元数据写入失败不影响评测 */ }
@@ -1434,4 +1526,4 @@ function apply(ctx) {
1434
1526
  }), `${name}: delete route`);
1435
1527
  }
1436
1528
 
1437
- export { name, inject, apply, writeGeneratedModelConfig, readProviders, validateBenchName, clampedRangeLength, releaseQuestionCount, isEmptyAnswerLine };
1529
+ export { name, inject, apply, writeGeneratedModelConfig, readProviders, validateBenchName, clampedRangeLength, releaseQuestionCount, isEmptyAnswerLine, missingBaselineIndexes, baselineIdsFor };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-livebench-panel",
3
- "version": "0.2.21",
3
+ "version": "0.2.23",
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. Ships a Baseline probe set built from the questions a reference model cannot solve.",
5
5
  "license": "MIT",
6
6
  "author": "cszr (Vithrive)",