From bf07944cf70902c517b820b977feb717ca5791e1 Mon Sep 17 00:00:00 2001 From: ThearchyHelios Date: Wed, 4 Nov 2026 09:24:37 +0800 Subject: [PATCH] =?UTF-8?q?=E8=8D=89=E7=A8=BF=E9=A1=B5=EF=BC=9A=E8=B5=B7?= =?UTF-8?q?=E8=8D=89=E8=BF=9B=E5=BA=A6=E3=80=81=E6=88=AA=E6=96=AD=E5=88=86?= =?UTF-8?q?=E4=B8=A4=E7=A7=8D=E8=AF=B4=E6=B3=95=E3=80=81=E5=87=A0=E5=8D=81?= =?UTF-8?q?=E6=9D=A1=E6=8C=87=E6=A0=87=E5=8F=AF=E6=89=AB=E8=AF=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 轮询窗口放宽到十分钟;文档被截和目录被截分开提示;指标默认收起摘要行,一屏看得到名字和 试运行的值。 --- frontend/src/lib/termsLedger.ts | 23 ++- frontend/src/pages/ledger/CaliberDrafts.tsx | 155 +++++++++++++------- frontend/src/typesLedger.ts | 19 ++- 3 files changed, 133 insertions(+), 64 deletions(-) diff --git a/frontend/src/lib/termsLedger.ts b/frontend/src/lib/termsLedger.ts index 2ca02fe9..c55b71ab 100644 --- a/frontend/src/lib/termsLedger.ts +++ b/frontend/src/lib/termsLedger.ts @@ -230,9 +230,10 @@ export const LEDGER_UI = { subtitle: '文档整理成定义和取数,自动试运行一次;去画布审改后由人签字发布', back: '返回口径目录', refresh: '刷新', - /** 列表按状态筛:全部加六种落点,起草中的只出现在全部里 */ + /** 列表按状态筛:全部加起草中和六种落点;起草中会停留几分钟,单独筛出来看 */ filter: { all: '全部', + drafting: '起草中', ready: '待签字发布', questions_open: '待确认问题', path_pending: '待接数据路径', @@ -275,8 +276,11 @@ export const LEDGER_UI = { // ---- 抽取中 ---- drafting: '正在抽取指标…', - draftingHint: '模型在后台读文档、对照数据目录写定义和取数,通常不到一分钟;离开页面不影响结果', - draftingTimeout: '起草超过一分钟还没有结果:稍后刷新再看;失败原因会写在这份草稿上', + draftingHint: '模型在服务端读文档、对照数据目录写定义和取数;指标多时会分几轮接着起草,离开页面不影响结果', + /** 第 N 轮 · 已覆盖 M 条 · 还有 K 条 */ + progress: (round: number, covered: number, uncovered: number) => + `第 ${formatNumber(round)} 轮 · 已覆盖 ${formatNumber(covered)} 条 · 还有 ${formatNumber(uncovered)} 条`, + draftingTimeout: '还没起草完,稍后刷新看结果', draftingFailed: '起草没有完成', // ---- 自动试运行 ---- @@ -292,8 +296,13 @@ export const LEDGER_UI = { // ---- 详情 ---- documentsUsed: (n: number) => `${formatNumber(n)} 份文档`, - truncated: '文档太长,后面的段落没有参与起草:可以少选几份,或先拆分文档', - metrics: '指标', + docTruncated: '文档太长,后面的段落没有参与起草', + catalogTruncated: '数据目录太大,只展开了文档点到的几张表', + blocks: (n: number) => `${formatNumber(n)} 个取数节点`, + metricsCount: (n: number) => `指标(${formatNumber(n)} 条)`, + /** 指标卡默认收起:标题行看名字、单位、值和未决问题数,展开看定义、表达式和引用 */ + metricSummaryHint: '展开看定义、表达式和引用原文', + metricQuestions: (n: number) => `${formatNumber(n)} 个未决问题`, noMetrics: '没有整理出指标', noMetricsBody: '文档里没有能逐字核对上引用的指标定义,可以换一份文档或补充起草要求后重来', synonyms: (list: string) => `又称:${list}`, @@ -322,8 +331,8 @@ export const LEDGER_UI = { resolved: (n: number) => `已移除 ${formatNumber(n)} 个问题`, dropped: '被丢弃的条目', droppedHint: '带不了能对照数据目录的取数,或引用无法逐字核对上的指标不留下', - uncovered: (n: number) => `这次没起草的指标(${n} 条)`, - uncoveredHint: '它们和这一组指标不能共用同一条取数 SQL,可以再起一份草稿接着做', + uncovered: (n: number) => `还没起草出来的指标(${formatNumber(n)} 条)`, + uncoveredHint: '这轮起草结束后还是没能起草出来的指标:可以再起一份草稿,或在起草要求里写清它们的取数口径', alignment: '和已发布口径对齐', alignmentNew: '新的', alignmentDuplicate: '已有出处', diff --git a/frontend/src/pages/ledger/CaliberDrafts.tsx b/frontend/src/pages/ledger/CaliberDrafts.tsx index d7365092..5f7c93ee 100644 --- a/frontend/src/pages/ledger/CaliberDrafts.tsx +++ b/frontend/src/pages/ledger/CaliberDrafts.tsx @@ -3,10 +3,11 @@ * * 主路径一次到位:选文档 → 模型读文档、对照数据目录产出定义 + 取数 + 入口字段 + 指标表达式 → 服务端校验后落成草稿图 * → 后台自动只读试运行 → 这里展示「定义 + 数 + 原文引用」→ 人去画布审改并发布。发布才需要人签字。 - * - 发起:选知识库文档,可选数据源(只写进适用范围)和一段起草要求,页面轮询详情直到状态离开 drafting(最多一分钟); + * - 发起:选知识库文档,可选数据源(只写进适用范围)和一段起草要求,页面轮询详情直到状态离开 drafting(最多十分钟, + * 指标多时服务端会分几轮续跑,进度写在 payload.progress); * - 试运行:草稿顶部显示这次自动试运行的状态和每个指标的值,running 时每两秒取一次,最多等一分钟;失败照实写原因; - * - 审读:每条指标带单位、粒度、指标表达式、定义和逐字核对过的引用原文(第 N 段),逐条的未决问题可以勾掉;被丢弃 - * 的条目单独列出原因;入口字段列出名字、类型、必填和缺省值; + * - 审读:指标默认收成一行(名字、单位、值和未决问题数),展开看单位、粒度、指标表达式、定义和逐字核对过的引用原文 + * (第 N 段),逐条的未决问题可以勾掉;被丢弃的条目单独列出原因;入口字段列出名字、类型、必填和缺省值; * - 发布:主按钮去画布审改并发布。「接上一次运行」只是辅助入口:已经有答案时让它认这份定义。 * * 页面不显示 SQL,取数到画布上看。状态由服务端算(还有问题 → questions_open,否则 ready;旧路径可能停在 path_pending), @@ -22,14 +23,19 @@ import { formatDateTime, formatTime } from '../../lib/format' import { LEDGER_UI } from '../../lib/termsLedger' import type { DataSourceName, KbDocument, Run } from '../../types' import type { - CaliberDraft, CaliberDraftStatus, CaliberDraftSummary, DraftAlignment, DraftCitation, DraftMetric, DraftTrialOut, + CaliberDraft, CaliberDraftStatus, CaliberDraftSummary, DraftAlignment, DraftCitation, DraftMetric, DraftTrialMetric, + DraftTrialOut, } from '../../typesLedger' const D = LEDGER_UI.drafts -/** 列表的状态筛:全部加六种落点,起草中的只出现在全部里 */ -type Filter = '' | Exclude -const FILTERS: Filter[] = ['', 'ready', 'questions_open', 'path_pending', 'attached', 'rejected', 'failed'] +/** 列表的状态筛:全部加七种状态;起草中也会停留几分钟,可以单独筛出来看 */ +type Filter = '' | CaliberDraftStatus +const FILTERS: Filter[] = ['', 'drafting', 'ready', 'questions_open', 'path_pending', 'attached', 'rejected', 'failed'] + +/** 起草轮询:每两秒一次,多轮续跑要几分钟,最多等十分钟 */ +const DRAFT_POLL_MS = 2_000 +const DRAFT_POLL_LIMIT_MS = 10 * 60_000 /** 状态标签:起草中 / 待签字发布 / 待确认问题 / 待接数据路径 / 已接上 / 已驳回 / 起草失败 */ function StatusPill({ status }: { status: CaliberDraftStatus }) { @@ -126,7 +132,7 @@ export function CaliberDrafts() { const [recentRuns, setRecentRuns] = useState(null) const [attachOut, setAttachOut] = useState<{ workflow_id: string; version: number; warnings: string[] } | null>(null) - /** 起草中的草稿:每两秒取一次详情,状态离开 drafting 或超过一分钟就停 */ + /** 起草中的草稿:每两秒取一次详情,状态离开 drafting 或超过十分钟就停 */ const [pollingId, setPollingId] = useState(null) const [pollTimedOut, setPollTimedOut] = useState(false) @@ -212,7 +218,7 @@ export function CaliberDrafts() { useEffect(() => { if (!pollingId) return - const until = Date.now() + 60_000 + const until = Date.now() + DRAFT_POLL_LIMIT_MS let alive = true const timer = setInterval(() => { api.caliberLedger.draft(pollingId).then((draft) => { @@ -232,7 +238,7 @@ export function CaliberDrafts() { setPollingId(null) setDetailError(e) }) - }, 2000) + }, DRAFT_POLL_MS) return () => { alive = false; clearInterval(timer) } }, [pollingId, loadList]) @@ -516,9 +522,16 @@ export function CaliberDrafts() { ) } - const renderMetric = (metric: DraftMetric, index: number) => { + /** + * 一条指标:默认收成一行,名字、单位、值和未决问题数直接看得见;展开才是定义、表达式、引用。 + * 指标多到几十条时这样一屏能扫完,找哪条再展开哪条。 + */ + const renderMetric = (metric: DraftMetric, index: number, values: Map) => { const definition = metric.definition ?? {} const citations = definition.citations ?? [] + const questions = metric.open_questions ?? [] + const row = metric.id ? values.get(metric.id) : undefined + const value = row ? row.rendered ?? (row.value == null ? '' : String(row.value)) : '' const rows = [ [D.unit, metric.unit], [D.decimals, metric.decimals != null ? String(metric.decimals) : ''], @@ -532,49 +545,60 @@ export function CaliberDrafts() { [D.dimensions, metric.dimensions?.join('、')], [D.filters, metric.filters?.join(';')], [D.exclusions, definition.exclusions?.join(';')], - ].filter(([, value]) => typeof value === 'string' && value.trim()) as [string, string][] + ].filter(([, text]) => typeof text === 'string' && text.trim()) as [string, string][] return ( -
  • -
    - {metric.name} - {!!metric.synonyms?.length && {D.synonyms(metric.synonyms.join('、'))}} -
    - {rows.length > 0 && ( -
    - {rows.map(([label, value]) => ( -
    -
    {label}
    -
    {value}
    +
  • +
    + + {metric.name} + {!!questions.length && {D.metricQuestions(questions.length)}} + {!!value && ( + + {value}{metric.unit ? ` ${metric.unit}` : ''} + + )} + +
    + {!!metric.synonyms?.length &&

    {D.synonyms(metric.synonyms.join('、'))}

    } + {rows.length > 0 && ( +
    + {rows.map(([label, text]) => ( +
    +
    {label}
    +
    {text}
    +
    + ))} +
    + )} + {metric.expression && ( +
    +

    {D.expression}

    + + {metric.expression} +
    - ))} -
  • - )} - {metric.expression && ( -
    -

    {D.expression}

    - - {metric.expression} - + )} + {definition.text && ( +
    +

    {D.definition}

    +

    {definition.text}

    +
    + )} + {citations.length > 0 && ( +
    +

    {D.citations}

    +
      {citations.map((c, i) => citationRow(c, `${index}:${i}`, metric.name))}
    +
    + )} + {!!questions.length && ( +
    +

    {D.questions}

    + {questionList(questions, `${index}`)} +
    + )}
    - )} - {definition.text && ( -
    -

    {D.definition}

    -

    {definition.text}

    -
    - )} - {citations.length > 0 && ( -
    -

    {D.citations}

    -
      {citations.map((c, i) => citationRow(c, `${index}:${i}`, metric.name))}
    -
    - )} - {!!metric.open_questions?.length && ( -
    -

    {D.questions}

    - {questionList(metric.open_questions, `${index}`)} -
    - )} +
  • ) } @@ -586,11 +610,15 @@ export function CaliberDrafts() { const dropped = draft.dropped ?? [] const uncovered = draft.uncovered ?? [] const metrics = draft.metrics ?? [] + const values = new Map((trial?.metrics ?? []).map((row) => [row.id, row])) + // 第一轮还没落库时服务端写的是空对象:没有轮次就不画,别显示「第 undefined 轮」 + const progress = draft.progress && typeof draft.progress.round === 'number' ? draft.progress : null return (
    {!!draft.documents?.length && {D.documentsUsed(draft.documents.length)}} + {typeof draft.blocks === 'number' && draft.blocks > 1 && {D.blocks(draft.blocks)}} {draft.created_at && {formatTime(draft.created_at)}} {draft.created_by && {D.createdBy(draft.created_by)}} {draft.model_id && {D.model(draft.model_id)}} @@ -611,18 +639,35 @@ export function CaliberDrafts() { {drafting && pollingId === draft.id && ( {D.drafting} + {progress && ( + + {D.progress(progress.round, progress.covered, progress.uncovered)} + + )} {D.draftingHint} )} - {drafting && pollTimedOut && {D.draftingTimeout}} - {draft.truncated && {D.truncated}} + {drafting && pollTimedOut && ( + + {D.draftingTimeout} + {progress && ( + + {D.progress(progress.round, progress.covered, progress.uncovered)} + + )} + + )} + {(draft.doc_truncated || (draft.truncated && !draft.catalog_truncated)) && ( + {D.docTruncated} + )} + {draft.catalog_truncated && {D.catalogTruncated}} {renderTrial(draft)} {renderEntryFields(draft)}
    -

    {D.metrics}

    +

    {D.metricsCount(metrics.length)}

    {pickedQuestions.length > 0 && (
    {D.resolveHint} @@ -634,7 +679,7 @@ export function CaliberDrafts() { )}
    {metrics.length ? ( -
      {metrics.map(renderMetric)}
    +
      {metrics.map((metric, index) => renderMetric(metric, index, values))}
    ) : !drafting ? ( } title={D.noMetrics} body={D.noMetricsBody} /> ) : ( diff --git a/frontend/src/typesLedger.ts b/frontend/src/typesLedger.ts index c2c480b6..0ee1c2be 100644 --- a/frontend/src/typesLedger.ts +++ b/frontend/src/typesLedger.ts @@ -475,7 +475,7 @@ export interface DraftMetric { unit?: string /** 小数位 */ decimals?: number - /** 指标表达式,形如 cell(nodes.fetch, 0, '列') */ + /** 指标表达式,形如 cell(nodes.fetch_1, 0, '列');一组取数时节点 id 是 fetch */ expression?: string /** 粒度,如「日」「订单行」 */ grain?: string @@ -553,6 +553,13 @@ export interface CaliberDraftSummary { error: string | null } +/** 起草轮次和覆盖情况:多轮续跑时每轮更新,covered 是已经起草出的指标名数,uncovered 是还没覆盖的 */ +export interface DraftProgress { + round: number + covered: number + uncovered: number +} + /** GET /api/ledger/drafts/{id}:payload 摊平在上层(documents / metrics / dropped / open_questions / alignment) */ export interface CaliberDraft extends CaliberDraftSummary { documents?: DraftDocument[] @@ -561,8 +568,12 @@ export interface CaliberDraft extends CaliberDraftSummary { entry_fields?: DraftEntryField[] dropped?: DraftDropped[] open_questions?: string[] - /** 这次没起草的指标名称:它们和这一组不能共用同一条取数 SQL,可以再起一份 */ + /** 还没有起草出来的指标名称:续跑跑到轮数上限或没有新指标后剩下的,可以再起一份 */ uncovered?: string[] + /** 这张草稿图有几个取数节点(fetch_1、fetch_2…):多组指标共用一张口径卡 */ + blocks?: number + /** 多轮续跑的进度;旧草稿没有 */ + progress?: DraftProgress /** 模型回复被截断,指标是从完整片段里抢救出来的 */ salvaged?: boolean alignment?: DraftAlignment @@ -571,6 +582,10 @@ export interface CaliberDraft extends CaliberDraftSummary { /** 草稿图对应的版本号 */ version?: number | null /** 文档超过一次能发给模型的字数,后面的段落没有参与起草 */ + doc_truncated?: boolean + /** 数据目录太大,只展开了文档点到的几张表 */ + catalog_truncated?: boolean + /** 旧服务端只给这一个:上面两种截断之一,界面按较窄的那条说 */ truncated?: boolean model_id?: string | null collection?: string | null