Excel 导入改造期 2:配方引擎与确认向导——表从哪一行哪一列开始、哪些是合计、日期怎么补年份,都写进一份可校验的配方;首次导入经人确认,之后每期按配方重放并核对

配方语言(agentlab-recipe/2,新 recipe_types、recipe)
- 封闭、不含正则、保存时静态校验:常量只能从表里的候选文字里选,占位符只能映射成空值,单位从固定词表里取
- 交叉表和列表两种版式;分段各自定位、不依赖先后顺序;合计按标签语义区间核对,关系恒等式、口径不同的声明都写在配方里
- 固定的解析器集合(parsers/2):全角、各类横线、千分位、月日写法宽容归一,规范写法存库、原文另记

执行器(新 xlsx_cells、recipe_engine)
- 两遍读取逐行对账,每个非空格都要有去向(格子账);交叉表逆透视成长表,按统计期补年份并断言覆盖整期
- 合计行改作核对并另存一张表,不和明细混在一列;区域外文字、隐藏行列、合并单元格、空行之后的文字都单独报出
- STRICT 加主键写临时库;按锚点段记溯源,供后续证据下钻用;大表流式读取,扫描在 sheetData 处停止

校验与说明(新 recipe_checks、recipe_notes)
- SQL 核对分通过、不一致、无法核对三种结果,失败分结构、数据质量、需确认三类;结构类直接拒收
- 数据质量类可以写理由接受,接受不改库、只改状态和说明
- 表的说明按核对结果生成、不含数字,经过数字检查;没核对过的不写「已核对」

起草(新 recipe_suggest、recipe_ai)
- 先用规则起草:建议卡片加待确认问题(关系要不要登记、占位符表示什么)
- 规则起草不完整时才提示 AI 兜底:界面先展示要发出的全部文字,用户同意才发;只发结构,数字、隐藏行列、遮罩列不发,公式先脱敏;同意绑定模型和服务方;AI 只写配方,有限次修订,记 token 和费用;截断单独识别
- 无论规则还是 AI 起草,都必须人工确认才能启用

导入流程(新 recipe_imports、recipe_confirm、recipe_diff、api/table_imports)
- 暂存 → 起草 → 回答问题或改配方 → 试运行 → 勾确认项、写接受理由 → 确认并启用;接入期 1 的构建、导入记录、快照和运行固定版本
- 上传新一期按已确认的配方重放,不调用模型,出回执和差异卡,手动启用
- 改配方报破坏性变更(breaking:*)、单位变化、工作表改名的兜底,都要逐项勾选确认

前端导入向导(新 pages/import)
- 原始网格(行列号、去向着色)、建议卡片、配方面板(表单,不露正则)、试运行回执、确认清单、差异卡、AI 起草的同意对话框
This commit is contained in:
ThearchyHelios
2026-10-01 22:00:07 +08:00
parent 8059cd5147
commit 2d5e50fa88
64 changed files with 36299 additions and 112 deletions
+45
View File
@@ -1,3 +1,6 @@
import type {
AiPreview, CommitOut, CurrentRecipe, QuestionAnswer, Recipe, Staging,
} from '../types'
import type {
Approval, AutofixResult, Conversation, ConversationDetail, ConversationTurn, CustomTool, DataSource, EvidenceGraph,
EvidenceAudit, EvidenceJudgeResult, EvidenceSegmentDetail, GraphSpec, KbDocument, MemoryItem, Provider, PublishCheck, PublishLevel, ReviewResult, Run,
@@ -552,6 +555,48 @@ export const api = {
get<TableSchema>(`/datasources/${id}/schema?table=${encodeURIComponent(table)}`),
},
/**
* 按配方导入(期 2)。暂存区(staging)是一次还没提交的导入:起草、回答问题、改配方、试运行都在它上面做,
* 提交(commit)成功之前不动数据源的当前版本。错误按 ApiError.code 分支(staging_closed、trial_required……)
*/
tableImports: {
/** 选文件开始一次导入(首次,或同名的简单导入切换为按配方导入)。带字节进度 */
stage: (file: File, body: { name: string; description?: string }, opts?: UploadOptions) => {
const form = new FormData()
form.append('file', file)
form.append('name', body.name)
form.append('description', body.description ?? '')
return upload<Staging>('/datasources/imports/stage', form, opts)
},
get: (id: string) => get<Staging>(`/datasources/imports/${encodeURIComponent(id)}`),
/** 已选的全部回答(服务端从起点配方按问题顺序重放)。needs_reason 的选项要带 reason */
answers: (id: string, answers: Record<string, QuestionAnswer>) =>
post<Staging>(`/datasources/imports/${encodeURIComponent(id)}/answers`, { answers }),
/** 整份配方替换工作配方:有静态校验问题也照存,只是不能试运行;已选的回答清空 */
putRecipe: (id: string, recipe: Recipe) =>
put<Staging>(`/datasources/imports/${encodeURIComponent(id)}/recipe`, { recipe }),
/** 将要发给模型的全文(同意框里给人看的就是这一份) */
draftAiPreview: (id: string) => get<AiPreview>(`/datasources/imports/${encodeURIComponent(id)}/draft-ai/preview`),
/** 用户同意后才调:preview_sha256 与服务端重算的不一致时回 409 ai_preview_stale */
draftAi: (id: string, previewSha256: string) =>
post<Staging>(`/datasources/imports/${encodeURIComponent(id)}/draft-ai`, { consent: true, preview_sha256: previewSha256 }),
trial: (id: string, body: { context_inputs?: Record<string, { start: string; end: string }>; signed_by?: string | null } = {}) =>
post<Staging>(`/datasources/imports/${encodeURIComponent(id)}/trial`, body),
commit: (id: string, body: {
trial_id: string; confirmations: string[]; acceptances: { check_id: string; reason: string }[]; signed_by?: string | null
}) => post<CommitOut>(`/datasources/imports/${encodeURIComponent(id)}/commit`, body),
discard: (id: string) => del(`/datasources/imports/${encodeURIComponent(id)}`),
/** 上传新一期:按当前配方自动试运行,回来时已带回执和差异卡。带字节进度 */
reupload: (sourceId: string, file: File, opts?: UploadOptions) => {
const form = new FormData()
form.append('file', file)
return upload<Staging>(`/datasources/${encodeURIComponent(sourceId)}/reupload`, form, opts)
},
/** 修改配方(不换文件):用当前导入的原件、以当前配方为工作配方 */
redraft: (sourceId: string) => post<Staging>(`/datasources/${encodeURIComponent(sourceId)}/redraft`, {}),
recipe: (sourceId: string) => get<CurrentRecipe>(`/datasources/${encodeURIComponent(sourceId)}/recipe`),
},
runs: {
/**
* status 可以给多个(逗号连接);q 按工作流名模糊匹配;before 是翻页游标