From badd610cd20930bcac0f81259eb2d64dd629c07d Mon Sep 17 00:00:00 2001 From: ThearchyHelios Date: Fri, 16 Oct 2026 20:41:50 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AF=84=E6=B5=8B=E9=A1=B5=E8=A1=A5=E9=BD=90?= =?UTF-8?q?=EF=BC=9A=E5=B7=A5=E4=BD=9C=E6=B5=81=E9=A2=98=E3=80=81=E6=89=8B?= =?UTF-8?q?=E5=8A=A8=E6=B8=85=E7=90=86=E6=9B=B4=E6=97=A9=E7=9A=84=E6=98=8E?= =?UTF-8?q?=E7=BB=86=E3=80=81=E9=80=90=E9=A2=98=E6=98=8E=E7=BB=86=E9=87=8C?= =?UTF-8?q?=E7=9A=84=E4=BD=9C=E7=AD=94=E7=BB=8F=E8=BF=87=E5=92=8C=E7=AD=94?= =?UTF-8?q?=E6=A1=88=E5=8E=9F=E6=96=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 前三波的接口这一波都要有界面(收口): - 加题时「写 SQL 定期望」「选口径」两种可以选题目种类——问数据题(问法交给助手)或工作流题(固定一个已发布版本、按入口字段填 输入模板,考的是这一版本身);题目表里工作流题写工作流和版本。反问、从运行认定只对问数据题有意义,不给选。 - 评测记录页签加「清理更早的明细」(POST /suites/{id}/cleanup,先确认、写清后果;收尾时本来就按保留规则自动清,这里是手动再清一次)。 - 逐题表展开时读这一题的明细工件(GET …/results/{id}):自动重来了几次、作答的经过、答案原文——答错的题能看到它当时答了什么。 check-evals 跟着加三处(工作流题的请求体、清理先确认再发、明细),101 项。 --- frontend/src/lib/termsEval.ts | 21 ++++ frontend/src/pages/evals/CaseEditor.tsx | 103 +++++++++++++++++++- frontend/src/pages/evals/EvaluationPage.tsx | 26 +++++ frontend/src/pages/evals/SuiteDetail.tsx | 24 ++++- scripts/check-evals.mjs | 32 ++++++ 5 files changed, 199 insertions(+), 7 deletions(-) diff --git a/frontend/src/lib/termsEval.ts b/frontend/src/lib/termsEval.ts index cfc9f652..04e268cd 100644 --- a/frontend/src/lib/termsEval.ts +++ b/frontend/src/lib/termsEval.ts @@ -91,6 +91,12 @@ export const EVAL_TEXT = { upgradeHint: '每道题三选一,没有缺省:改为固定新版本(历史在这里断开)、新旧两版各算一份期望、或旧题归档另建新题', runsEmpty: '还没有评测记录', runsLabel: '历次评测', + cleanup: '清理更早的明细', + cleanupHint: '评测收尾时会按保留规则自动清理,这里可以手动再清一次', + cleanupTitle: (name: string) => `清理「${name}」更早的评测明细?`, + cleanupConsequences: ['按设置里的保留次数清理更早的评测:逐题结果、作答和算期望的运行一起删除,不可恢复', + '设为基线的那次、A/B 的另几臂留着;每次评测的总表永久保留'], + cleaned: (runs: number, files: number) => (runs ? `已清理 ${runs} 次评测的明细,回收了 ${files} 个工件文件` : '没有要清理的评测'), schedules: '定时评测', noSchedules: '这个题集还没有定时任务', budget: '预算', @@ -112,6 +118,7 @@ export const EVAL_TEXT = { caseRow: { seq: (n: number) => `第 ${n} 题`, kind: { ask: '问数据题', workflow: '工作流题' } as Record<'ask' | 'workflow', string>, + workflow: (name: string, v: number | null) => `工作流 ${name} v${v ?? '?'}`, disabled: '未启用', archived: '已归档', supersededBy: (n: string) => `已被新题 ${n} 取代`, @@ -172,6 +179,17 @@ export const EVAL_TEXT = { special: '问法问的是没有的东西(期望答「没有数据」),或者问法含糊(期望助手反问)', } as Record<'sql' | 'caliber' | 'from_run' | 'special', string>, question: '问法', + workflowQuestion: '这道题考什么(可选)', + workflowQuestionHint: '工作流题不需要问法:写一句说明,列表里认得出是哪道题', + caseKind: '题目种类', + caseKindHint: { + ask: '问法交给助手:建图、运行、复核,和问数据页同一条路', + workflow: '固定一个工作流的已发布版本,按输入运行一次(不建图、不复核),考的是这一版本身', + } as Record<'ask' | 'workflow', string>, + workflow: '工作流', + workflowPlaceholder: '选择已发布的工作流', + workflowNone: '还没有已发布的工作流', + workflowVersion: '固定的版本', questionPlaceholder: '例如:上周按支付日统计、不含退款的销售额是多少?', questionHint: '可以写 {{ schedule.last_week_start }} 这类时间变量,按评测的截至时间计算', scope: '限定的数据源', @@ -473,6 +491,9 @@ export const EVAL_TEXT = { filterAll: '全部', filter: '只看', repeat: (n: number) => `第 ${n + 1} 次`, + attempts: (n: number) => `这一题自动重来了 ${n} 次`, + turnReason: '作答的经过', + answer: '答案原文', expand: '展开', collapse: '收起', }, diff --git a/frontend/src/pages/evals/CaseEditor.tsx b/frontend/src/pages/evals/CaseEditor.tsx index e89fac44..12a27a9c 100644 --- a/frontend/src/pages/evals/CaseEditor.tsx +++ b/frontend/src/pages/evals/CaseEditor.tsx @@ -10,7 +10,7 @@ import { EVAL_TEXT } from '../../lib/termsEval' import { useCatalog } from '../../store/catalog' import type { Run } from '../../types' import type { - EvalCaliber, EvalCase, EvalCaseBody, EvalExpectation, EvalGold, EvalPreview, EvalRunQuery, EvalSuite, EvalTrial, + EvalCaliber, EvalCase, EvalCaseBody, EvalCaseKind, EvalExpectation, EvalGold, EvalPreview, EvalRunQuery, EvalSuite, EvalTrial, } from '../../typesEval' import { graphFields, type InputField } from '../schedules/model' import { cellText, expectationText, goldValueText } from './parts' @@ -69,6 +69,11 @@ export function CaseEditor({ open, onClose, suite, editing, initialQuestion, onS // 从运行认定 const [runId, setRunId] = useState('') const [queryId, setQueryId] = useState('') + // 题目种类:问数据题(问法交给助手作答)或工作流题(固定一个已发布版本,按输入跑一次,考的是这一版本身) + const [caseKind, setCaseKind] = useState('ask') + const [workflowId, setWorkflowId] = useState('') + const [workflowVersion, setWorkflowVersion] = useState(null) + const [workflowInputs, setWorkflowInputs] = useState>({}) // 试算、保存 const [trial, setTrial] = useState(null) const [trialing, setTrialing] = useState(false) @@ -101,6 +106,10 @@ export function CaseEditor({ open, onClose, suite, editing, initialQuestion, onS setInputs(c?.gold?.kind === 'caliber' ? { ...(c.gold.inputs ?? {}) } : {}) setRunId(c?.gold?.kind === 'from_run' ? c.gold.run_id ?? '' : '') setQueryId(c?.gold?.kind === 'from_run' ? c.gold.query ?? '' : '') + setCaseKind(c?.kind ?? 'ask') + setWorkflowId(c?.workflow_id ?? '') + setWorkflowVersion(c?.workflow_version ?? null) + setWorkflowInputs({ ...(c?.input_template ?? {}) }) setTrial(null) setError(null) }, [open, editing?.id, editing?.version, initialQuestion]) @@ -144,7 +153,13 @@ export function CaseEditor({ open, onClose, suite, editing, initialQuestion, onS tags: tags.split(/[,,、\n]/).map((t) => t.trim()).filter(Boolean), } if (!fixedFromRun) out.gold = gold() - if (!editing) out.kind = 'ask' + const workflow = caseKind === 'workflow' && (mode === 'sql' || mode === 'caliber') + if (!editing) out.kind = workflow ? 'workflow' : 'ask' + if (workflow) { + out.workflow_id = workflowId + out.workflow_version = workflowVersion + out.input_template = Object.fromEntries(Object.entries(workflowInputs).filter(([, v]) => v.trim() !== '')) + } return out } @@ -215,12 +230,16 @@ export function CaseEditor({ open, onClose, suite, editing, initialQuestion, onS const fieldError = (...fields: string[]) => (error && error.field && fields.some((f) => error.field === f || error.field!.startsWith(`${f}.`)) ? error.message : undefined) - const generalError = error && !(error.field && ['question', 'gold.sql', 'gold.datasource_id', 'gold.inputs', 'query', 'tags'] + const generalError = error && !(error.field && ['question', 'gold.sql', 'gold.datasource_id', 'gold.inputs', 'query', 'tags', + 'workflow_id', 'workflow_version', 'input_template'] .some((f) => error.field === f || error.field!.startsWith(`${f}.`))) ? error.message : null const needsMain = mode !== 'special' + /** 工作流题只配「写 SQL 定期望」「选口径」两种:反问是助手的事,工作流不会反问;从运行认定只认问数据运行里的查询 */ + const workflowCase = caseKind === 'workflow' && (mode === 'sql' || mode === 'caliber') // 关掉之前要不要先问:新建时填了东西、编辑时改了要试算的那几样 const dirty = editing ? changed() : !!(question.trim() || sql.trim() || main || points.trim() || runId) const ready = (!needsMain || !!main) && (mode !== 'from_run' || fixedFromRun || (!!runId && !!queryId)) + && (!workflowCase || (!!workflowId && !!workflowVersion)) && (mode !== 'caliber' || !!caliber) && (!(mode === 'sql' || (mode === 'special' && special === 'none')) || (!!sourceId && !!sql.trim())) return ( @@ -256,8 +275,16 @@ export function CaseEditor({ open, onClose, suite, editing, initialQuestion, onS

{T.modeHint[mode]}

- + {(mode === 'sql' || mode === 'caliber') && ( + { setWorkflowId(v); setWorkflowInputs({}) }} version={workflowVersion} + setVersion={setWorkflowVersion} inputs={workflowInputs} setInputs={setWorkflowInputs} + error={fieldError('workflow_id', 'workflow_version', 'input_template')} /> + )} + +