diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 193bde74..b78695d9 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -22,6 +22,7 @@ import { SchedulesPage } from './pages/schedules/SchedulesPage' // 评测(下一轮第 4 波,评测-D8):顶级导航,搭建者以上可见 import { EvalsPage } from './pages/evals/EvalsPage' import { CaliberCatalog } from './pages/ledger/CaliberCatalog' +import { CaliberDrafts } from './pages/ledger/CaliberDrafts' import { NotFound } from './pages/NotFound' // 应用平台:使用面(FE-5)、搭建面的应用编辑(FE-6) import { AppsHome } from './pages/apps/AppsHome' @@ -196,6 +197,8 @@ function Shell() { } /> } /> } /> + {/* 从业务文档起草口径:草稿只到定义为止,取数路径接一次真实运行 */} + } /> {/* 应用:固定路径(inbox)写在 /apps/:appId 前面,读的人一眼看出它不是应用 id */} } /> {/* 要在 /apps/:appId 之前:create 不是应用 id */} diff --git a/frontend/src/api/caliberLedger.ts b/frontend/src/api/caliberLedger.ts index e8ea1f39..02aad7e1 100644 --- a/frontend/src/api/caliberLedger.ts +++ b/frontend/src/api/caliberLedger.ts @@ -12,12 +12,13 @@ * 请求函数从 ./http 导入,不从 ./client 导入(client.ts 要导入这个文件,反过来就成了循环)。 */ import type { - CaliberDescriptor, CaliberTrialOut, CertifyQueue, FreezeOut, LedgerFeedbackOut, LedgerFeedbackVerdict, LedgerIssueOut, - LedgerReconcileIn, LedgerReconcileOut, RunLedger, + CaliberDescriptor, CaliberDraft, CaliberDraftAttachOut, CaliberDraftSummary, CaliberTrialOut, CertifyQueue, DraftMetric, + FreezeOut, LedgerFeedbackOut, LedgerFeedbackVerdict, LedgerIssueOut, LedgerReconcileIn, LedgerReconcileOut, RunLedger, } from '../typesLedger' -import { get, post, type RequestOptions } from './http' +import { get, patch, post, type RequestOptions } from './http' const at = (runId: string) => `/ledger/answers/${encodeURIComponent(runId)}` +const draftAt = (draftId: string) => `/ledger/drafts/${encodeURIComponent(draftId)}` export const caliberLedger = { run: (runId: string, opts?: RequestOptions) => get(`/runs/${encodeURIComponent(runId)}/ledger`, opts), @@ -46,4 +47,26 @@ export const caliberLedger = { `/ledger/calibers/${encodeURIComponent(workflowId)}/${version}/${encodeURIComponent(nodeId)}/trial`, { inputs }, ), + /** + * 从业务文档起草口径(app/ledger/docdraft.py,搭建者):草稿只产出定义,不产出取数。发起后立刻返回 drafting,模型在后台跑, + * 结果用 draft() 反复取,直到状态离开 drafting + */ + drafts: (status?: string, opts?: RequestOptions) => + get<{ drafts: CaliberDraftSummary[] }>(`/ledger/drafts${status ? `?status=${encodeURIComponent(status)}` : ''}`, opts), + /** 一份草稿的完整内容:payload 摊平在上层(metrics / dropped / open_questions / alignment) */ + draft: (draftId: string, opts?: RequestOptions) => get(draftAt(draftId), opts), + /** 选文档发起起草:documents 是要用的知识库文档 id,guidance 是给模型看的附加要求 */ + extractDraft: (body: { documents: string[]; collection?: string; datasource_id?: string; guidance?: string }) => + post<{ draft_id: string; status: string }>('/ledger/drafts/extract', body), + /** + * 改标题、整体换掉指标、或把答清的问题消掉(resolved 是问题原文)。服务端据此重算状态:还有问题 → questions_open, + * 否则 path_pending;已经接上或驳回的不改状态 + */ + patchDraft: (draftId: string, body: { title?: string; metrics?: DraftMetric[]; resolved?: string[] }) => + patch(draftAt(draftId), body), + /** 接上一次真实运行补取数路径(app/ledger/docattach.py):由它建工作流和版本,定义来自草稿 */ + attachDraft: (draftId: string, runId: string) => post(`${draftAt(draftId)}/attach`, { run_id: runId }), + /** 「不是口径」:置 rejected,写一条审计,以后不再出现在列表的待办里 */ + dismissDraft: (draftId: string, note?: string) => + post<{ ok: boolean; draft_id: string; status: string }>(`${draftAt(draftId)}/dismiss`, { note: note ?? null }), } diff --git a/frontend/src/lib/termsLedger.ts b/frontend/src/lib/termsLedger.ts index 81e68ac3..dbfd0ac0 100644 --- a/frontend/src/lib/termsLedger.ts +++ b/frontend/src/lib/termsLedger.ts @@ -223,6 +223,119 @@ export const LEDGER_UI = { loadFailed: '无法读取可沉淀的问题', }, + // ---- 从业务文档起草口径(/ledger/drafts;app/ledger/docdraft.py、docattach.py) ---- + drafts: { + entry: '从业务文档起草', + title: '从业务文档起草口径', + subtitle: '文档里写的是业务说法,这里整理成定义草稿;取数路径接上一次真实运行', + back: '返回口径目录', + refresh: '刷新', + /** 列表按状态筛:全部加四种落点,起草中的只出现在全部里 */ + filter: { + all: '全部', + questions_open: '待确认问题', + path_pending: '待接数据路径', + attached: '已接上', + rejected: '已驳回', + } as Record, + status: { + drafting: '起草中', + questions_open: '待确认问题', + path_pending: '待接数据路径', + attached: '已接上', + rejected: '已驳回', + } as Record, + filterLabel: '按状态筛', + summary: (metrics: number, questions: number) => `${formatNumber(metrics)} 个指标 · ${ + questions ? `${formatNumber(questions)} 个未决问题` : '没有未决问题'}`, + loadFailed: '无法读取口径草稿', + empty: '还没有口径草稿', + emptyBody: '选一份业务文档,把里面的指标整理成定义草稿', + emptyFiltered: '这个状态下没有草稿,换个状态看看', + + // ---- 发起:选文档、可选数据源和起草要求 ---- + start: '起草一份新口径', + documents: '文档', + documentsHint: '从知识库里选;模型只读这些文档的段落', + noDocuments: '知识库里还没有已完成的文档', + noDocumentsBody: '先在知识页上传业务文档,切块完成后再来起草', + documentMeta: (collection: string, chunks: number) => `${collection} · ${formatNumber(chunks)} 段`, + datasource: '数据源(可选)', + datasourceHint: '只写进适用范围,不读表结构', + datasourceNone: '不指定', + guidance: '起草要求(可选)', + guidancePlaceholder: '例如:只整理和月度结算有关的指标', + submit: '开始起草', + submitting: '正在发起…', + needDoc: '请至少选一份文档', + started: '已开始起草,完成后指标会出现在下面', + + // ---- 抽取中 ---- + drafting: '正在抽取指标…', + draftingHint: '模型在后台读文档、核对引用,通常不到一分钟;离开页面不影响结果', + draftingTimeout: '起草超过一分钟还没有结果:稍后刷新再看;失败原因会写在这份草稿上', + draftingFailed: '起草没有完成', + + // ---- 详情 ---- + documentsUsed: (n: number) => `${formatNumber(n)} 份文档`, + truncated: '文档太长,后面的段落没有参与起草:可以少选几份,或先拆分文档', + metrics: '指标', + noMetrics: '没有整理出指标', + noMetricsBody: '文档里没有能逐字核对上引用的指标定义,可以换一份文档或补充起草要求后重来', + synonyms: (list: string) => `又称:${list}`, + unit: '单位', + grain: '粒度', + timeField: '时间字段', + dimensions: '维度', + filters: '过滤', + numerator: '分子', + denominator: '分母', + scope: '范围', + exclusions: '不包含', + owner: '负责人', + effectiveFrom: '生效日期', + citations: '引用原文', + paragraph: (n: number) => `第 ${n} 段`, + citationMissing: '原文段落无法读取', + questions: '未决问题', + questionsOther: '跨指标的问题', + resolve: '回答并移除', + resolveHint: '在业务上问清之后勾选对应的问题,移除后状态会跟着变', + resolveNeed: '请先勾选已经问清的问题', + resolved: (n: number) => `已移除 ${formatNumber(n)} 个问题`, + dropped: '被丢弃的条目', + droppedHint: '只产出定义:带 SQL、表名痕迹,或引用无法逐字核对上的指标不留下', + alignment: '和已发布口径对齐', + alignmentNew: '新的', + alignmentDuplicate: '已有出处', + alignmentConflict: '定义不同', + model: (id: string) => `起草模型:${id}`, + createdBy: (who: string) => `起草人:${who}`, + + // ---- 接上一次运行 ---- + attach: '接上一次真实运行', + attachHint: '取数路径照抄这次运行里执行过的查询,文档只贡献定义;接上后产出可试运行的工作流版本,不发布', + runId: '运行 id', + runIdPlaceholder: '粘贴运行记录里的 id', + recentRuns: '最近成功的运行', + recentRunsNone: '没有可选的运行', + recentRunsPick: '选一次运行填入左边', + attachAction: '接上', + attaching: '正在接入…', + attachNeedRun: '请先填运行 id', + attached: (version: number) => `已接上,工作流产出版本 v${version}`, + attachWarnings: '这些指标没有对上已有口径卡,需要人决定新建还是忽略:', + openWorkflow: '打开工作流', + + // ---- 不是口径 ---- + dismiss: '不是口径', + dismissTitle: '这份草稿不是口径', + dismissNote: '理由(可选)', + dismissPlaceholder: '例如:一次性的核对,不需要固定下来', + dismissConfirm: '确认不是口径', + dismissed: '已驳回,这份草稿不再出现在待办里', + }, + // ---- 设置:口径账本一组 ---- settings: { title: '口径账本', diff --git a/frontend/src/pages/ledger/CaliberCatalog.tsx b/frontend/src/pages/ledger/CaliberCatalog.tsx index 1a037449..a1d90ffb 100644 --- a/frontend/src/pages/ledger/CaliberCatalog.tsx +++ b/frontend/src/pages/ledger/CaliberCatalog.tsx @@ -7,12 +7,13 @@ */ import { useEffect, useMemo, useState } from 'react' import { Link } from 'react-router-dom' -import { ChevronDown, Layers, Play, RefreshCw, Search } from 'lucide-react' +import { ChevronDown, FileText, Layers, Play, RefreshCw, Search } from 'lucide-react' import clsx from 'clsx' import { api } from '../../api/client' import { SchemaForm } from '../../components/SchemaForm' import { EmptyState, ErrorState, Notice, PageHeader, Skeleton, Spinner, toast } from '../../components/ui' import { LEDGER_TEXT } from '../../lib/terms' +import { LEDGER_UI } from '../../lib/termsLedger' import type { FormSchema } from '../../typesApp' import type { CaliberDescriptor, CaliberTrialOut } from '../../typesLedger' @@ -78,9 +79,14 @@ export function CaliberCatalog() { return (
} title="口径" subtitle="已发布口径的入口、指标、依赖与试运行" - actions={} /> + actions={<> + + {LEDGER_UI.drafts.entry} + + + } />