mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 00:21:58 +00:00
数据目录起草(前端):模型拿不准、留空的列(中文名和含义都没给)单独写出来,不算没写全、表照样是已完成。起草对话框逐表写「12 列模型拿不准、留空了」,列名悬停看得到、也可以展开(最多 5 个,没列全写一共几列);结束后写合计「共 13 列模型拿不准、留空了(2 张表)」和办法——在起草要求里补充缩写和术语的含义,再重新起草这几张表,已有的内容不会丢;「重新起草这 N 张表」回到设置,范围就是这几张、勾着用模型、焦点在起草要求,补完再开始;结束后默认列出的表连同有留空列的(「只看未写全、有留空列的表」);关着对话框时结束的提示也接着写留空的列;设置阶段的「上一次起草」都写完了、只是有留空列的也提,写明几列。清单上上次起草有留空列的表一行淡色轻提示(不是失败标记,和失败、部分完成的标记分开;还在起草的那一次不算「上次」);表详情顶部同样提示(只有留空列时是提示、不是失败;部分完成又有留空列的并在一条里),给「重新起草这张表」和「补充起草要求」(打开起草设置,范围只有这张表)。类型加 CatalogDraftTask.blank_columns、blank_sample,CatalogDraftJob.blank_columns,last_draft.blank_columns(老服务端没有,按 0)。check-catalog 新加一节覆盖逐表与汇总、展开列名、默认列表、清单轻提示(颜色同次要信息、不是警告色)、进行中不算「上次」、回到设置重新起草(先存起草要求再建任务)、结束提示、表详情的两个按钮,窄屏展开列名一并量;check-all 里数据目录的耗时按实测改成 130 秒
This commit is contained in:
@@ -2395,6 +2395,8 @@ export const CATALOG_TEXT = {
|
||||
// - 「只重试失败的表」:重新起草失败、部分完成和已停止的表;已经写进目录的内容不回滚、不受影响。
|
||||
// - 「没写出来」:一次调用没有写出内容(额度用完、只给空壳、超时、出错)。接连没写出来时服务端给一句「提醒」,
|
||||
// 好转后收起;提醒是提醒的样式,不是错误。超时的调用不计用量。
|
||||
// - 「留空」:模型列出了一列(或这一段请它写了),中文名和含义却都没给,写「N 列模型拿不准、留空了」。表的状态不因此
|
||||
// 变,不算没写全;办法是在起草要求里补充缩写和术语的含义,再重新起草,已有的内容不会丢。
|
||||
// ===========================================================================
|
||||
|
||||
/** 起草要求、本次补充说明的字数上限(服务端同一个数:超长 422) */
|
||||
@@ -2424,9 +2426,14 @@ export const CATALOG_DRAFT_TEXT = {
|
||||
tooMany: (max: number) => `一次最多起草 ${formatNumber(max)} 张表,请缩小范围`,
|
||||
saveGuidanceFailed: (reason: string) => `起草要求未能保存,未开始起草:${reason}`,
|
||||
startFailed: (reason: string) => `未能开始起草:${reason}`,
|
||||
// ---- 上一次起草(设置阶段,没写全的才提)
|
||||
// ---- 上一次起草(设置阶段,没写全、有留空列的才提)
|
||||
last: (state: string, at: string) => `上一次起草(${at}):${state}`,
|
||||
/** 上一次的结果:状态,有留空的列时接着写几列 */
|
||||
lastState: (title: string, blank: number) => (blank ? `${title},${formatNumber(blank)} 列模型拿不准、留空了` : title),
|
||||
lastOpen: '查看',
|
||||
// ---- 从结果、表详情回到设置:范围就是那几张表
|
||||
scopeBlank: (n: number) => `有留空列的 ${formatNumber(n)} 张表`,
|
||||
scopeTable: (name: string) => `这张表:${name}`,
|
||||
// ---- 进行中
|
||||
queued: '排队中,等待开始…',
|
||||
running: '正在起草…',
|
||||
@@ -2457,6 +2464,15 @@ export const CATALOG_DRAFT_TEXT = {
|
||||
timeoutNote: '超时的调用不计用量,实际用量可能更多',
|
||||
/** 额度用完、超时多了,服务端把之后的段缩小了 */
|
||||
partColumns: (n: number) => `模型多次用完额度或超时,之后的段每段最多 ${formatNumber(n)} 列`,
|
||||
// ---- 留空的列:模型列出了、中文名和含义却都没给。表的状态不因此变
|
||||
blank: (n: number) => `${formatNumber(n)} 列模型拿不准、留空了`,
|
||||
/** 留空的列名(服务端最多给 5 个);没列全时写一共几列 */
|
||||
blankSample: (sample: string[], n: number) =>
|
||||
`留空的列:${sample.join('、')}${n > sample.length ? ` 等 ${formatNumber(n)} 列` : ''}`,
|
||||
blankTotal: (columns: number, tables: number) => `共 ${formatNumber(columns)} 列模型拿不准、留空了(${formatNumber(tables)} 张表)`,
|
||||
blankAdvice: '可以在起草要求里补充缩写和术语的含义,再重新起草这几张表,已有的内容不会丢',
|
||||
blankRedraft: (n: number) => `重新起草这 ${formatNumber(n)} 张表`,
|
||||
blankRedraftHint: '回到起草设置,范围是这几张表:先在起草要求里补充缩写和术语的含义,再开始起草',
|
||||
// ---- 结束
|
||||
title: {
|
||||
queued: '排队中,等待开始…',
|
||||
@@ -2486,6 +2502,7 @@ export const CATALOG_DRAFT_TEXT = {
|
||||
retryHint: '重新起草失败、部分完成和已停止的表;已写进目录的内容不受影响',
|
||||
showAll: (n: number) => `显示全部 ${formatNumber(n)} 张表`,
|
||||
showProblems: '只看未写全的表',
|
||||
showProblemsBlank: '只看未写全、有留空列的表',
|
||||
tasks: '逐表进度',
|
||||
taskStatus: {
|
||||
pending: '等待', running: '起草中', ok: '已完成', partial: '部分完成', failed: '失败', skipped: '已跳过', cancelled: '已停止',
|
||||
@@ -2501,6 +2518,8 @@ export const CATALOG_DRAFT_TEXT = {
|
||||
toast: {
|
||||
succeeded: (summary: string) => `数据目录起草完成:${summary}`,
|
||||
partial: (n: number) => `数据目录起草完成,${formatNumber(n)} 张表未写全`,
|
||||
/** 完成、部分完成的提示后面接着写留空的列(没有就不写) */
|
||||
withBlank: (text: string, blank: number) => (blank ? `${text};${formatNumber(blank)} 列模型拿不准、留空了` : text),
|
||||
failed: (reason: string) => (reason ? `数据目录起草失败:${reason}` : '数据目录起草失败'),
|
||||
cancelled: '数据目录起草已停止',
|
||||
interrupted: '服务重启,数据目录起草中断',
|
||||
@@ -2510,6 +2529,11 @@ export const CATALOG_DRAFT_TEXT = {
|
||||
lastFailed: '上次起草失败',
|
||||
lastPartial: '上次起草部分完成',
|
||||
lastWith: (label: string, reason: string) => `${label}:${reason}`,
|
||||
/** 上次起草有留空的列:清单上是轻提示(不是失败标记),表详情顶部同一句 */
|
||||
lastBlank: (n: number) => `上次起草有 ${formatNumber(n)} 列模型拿不准、留空了`,
|
||||
lastBlankAdvice: '可以在起草要求里补充缩写和术语的含义,再重新起草这张表,已有的内容不会丢',
|
||||
draftGuidance: '补充起草要求',
|
||||
draftGuidanceHint: '打开起草设置,范围是这张表:先在起草要求里补充缩写和术语的含义,再开始起草',
|
||||
redraft: '重新起草这张表',
|
||||
redraftHint: '用模型重新起草这张表(不带补充说明,带上数据源的起草要求);人工确认、驳回过的项不会被改动',
|
||||
redraftBusy: '已有起草任务在进行,结束后再重新起草这张表',
|
||||
|
||||
@@ -12,8 +12,8 @@ import {
|
||||
} from '../../lib/terms'
|
||||
import { StatusLegend, SystemNotesNotice } from './parts'
|
||||
import { DraftDialog } from './DraftDialog'
|
||||
import { isJobActive, retryableCount, useDraftJob } from './draftJob'
|
||||
import type { DraftStartBody } from './draftJob'
|
||||
import { isJobActive, jobBlank, retryableCount, useDraftJob } from './draftJob'
|
||||
import type { DraftPreset, DraftStartBody } from './draftJob'
|
||||
import { ProfileDialog } from './ProfileDialog'
|
||||
import type { ProfileJob } from './ProfileDialog'
|
||||
import { ProfileSettingsDialog } from './ProfileSettings'
|
||||
@@ -105,9 +105,9 @@ export function CatalogPage() {
|
||||
const [selected, setSelected] = useState<Set<string>>(() => new Set())
|
||||
/**
|
||||
* 起草对话框开着没有、打开时显示什么:setup 选范围开始;job 显示页面跟着的任务(进行中的进度、刚结束的结果)。
|
||||
* busy:是撞上已有任务(409)转过去显示的
|
||||
* busy:是撞上已有任务(409)转过去显示的;preset:设置阶段预先选好的范围(表详情里「补充起草要求」)
|
||||
*/
|
||||
const [draftOpen, setDraftOpen] = useState<{ view: 'setup' | 'job'; busy?: boolean } | null>(null)
|
||||
const [draftOpen, setDraftOpen] = useState<{ view: 'setup' | 'job'; busy?: boolean; preset?: DraftPreset } | null>(null)
|
||||
/** 起草写进了这些表:详情据此重新载入(编辑中的不动,保存时服务端会用版本号拦下) */
|
||||
const [drafted, setDrafted] = useState<{ seq: number; tables: Set<string> }>({ seq: 0, tables: new Set() })
|
||||
const [profiling, setProfiling] = useState(false)
|
||||
@@ -169,8 +169,10 @@ export function CatalogPage() {
|
||||
key: `catalog-draft:${src}`, duration: 10000,
|
||||
action: { label: DT.viewResult, onClick: () => { if (sourceRef.current === src) setDraftOpen({ view: 'job' }) } },
|
||||
}
|
||||
if (job.status === 'succeeded') toast.ok(DT.toast.succeeded(CT.draftSummary(job.added, job.updated, job.removed)), opts)
|
||||
else if (job.status === 'partial') toast.warn(DT.toast.partial(retryableCount(job)), opts)
|
||||
// 留空的列不算没写全,提示里接着写一句,点「查看结果」看是哪几张表、怎么办
|
||||
const blank = jobBlank(job)
|
||||
if (job.status === 'succeeded') toast.ok(DT.toast.withBlank(DT.toast.succeeded(CT.draftSummary(job.added, job.updated, job.removed)), blank), opts)
|
||||
else if (job.status === 'partial') toast.warn(DT.toast.withBlank(DT.toast.partial(retryableCount(job)), blank), opts)
|
||||
else if (job.status === 'failed') toast.error(DT.toast.failed(job.error ?? ''), opts)
|
||||
else if (job.status === 'interrupted') toast.warn(DT.toast.interrupted, opts)
|
||||
else toast.warn(DT.toast.cancelled, opts)
|
||||
@@ -195,6 +197,10 @@ export function CatalogPage() {
|
||||
toast.error(e)
|
||||
}
|
||||
}, [draft.start])
|
||||
/** 详情里「补充起草要求」(上次起草有留空的列):打开起草设置,范围只有这张表,先补起草要求再开始 */
|
||||
const draftWithGuidance = useCallback((t: string) => {
|
||||
setDraftOpen({ view: 'setup', preset: { tables: [t], label: DT.scopeTable(t) } })
|
||||
}, [])
|
||||
const onGuidanceSaved = useCallback((g: string | null) => {
|
||||
setData((cur) => cur && ({ ...cur, guidance: g }))
|
||||
}, [])
|
||||
@@ -402,7 +408,7 @@ export function CatalogPage() {
|
||||
? (
|
||||
<TableDetail sourceId={sourceId} table={table} rows={rows} drafted={drafted} fillCodes={fillCodes} prev={prev} next={next}
|
||||
onFillDone={onFillDone} onOpen={openTable} onBack={backToList} onDetail={onDetail}
|
||||
draftBusy={!!stripJob} onRedraft={redraftTable} />
|
||||
draftBusy={!!stripJob} onRedraft={redraftTable} onDraftGuidance={draftWithGuidance} />
|
||||
)
|
||||
: <Overview rows={rows} empty={empty} systemNotes={data.system_notes} draft={draftButton(true)} onOpen={openTable} />}
|
||||
</section>
|
||||
@@ -446,6 +452,7 @@ export function CatalogPage() {
|
||||
job={draftJob}
|
||||
initial={draftOpen.view}
|
||||
initialBusy={!!draftOpen.busy}
|
||||
preset={draftOpen.preset ?? null}
|
||||
stopping={draft.stopping}
|
||||
onStart={startDraft}
|
||||
onCancel={draft.cancel}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { CircleCheck, CircleDashed, CircleMinus, CircleSlash, CircleX, RotateCcw, Sparkles, TriangleAlert } from 'lucide-react'
|
||||
import { CircleCheck, CircleDashed, CircleMinus, CircleSlash, CircleX, Info, RotateCcw, Sparkles, TriangleAlert } from 'lucide-react'
|
||||
import type { LucideIcon } from 'lucide-react'
|
||||
import clsx from 'clsx'
|
||||
import { ApiError, api } from '../../api/client'
|
||||
@@ -9,26 +9,33 @@ import { errorMessage } from '../../lib/errors'
|
||||
import { formatTime } from '../../lib/format'
|
||||
import { CATALOG_DRAFT_TEXT as DT, CATALOG_TEXT as CT, DRAFT_GUIDANCE_MAX, DRAFT_NOTE_MAX } from '../../lib/terms'
|
||||
import {
|
||||
DRAFT_MAX_TABLES, DRAFT_MODEL_UNAVAILABLE, DRAFT_PART_COLUMNS, DRAFT_TOP_N, RETRYABLE, isJobActive, nothingWritten, retryableCount,
|
||||
taskReason,
|
||||
DRAFT_MAX_TABLES, DRAFT_MODEL_UNAVAILABLE, DRAFT_PART_COLUMNS, DRAFT_TOP_N, RETRYABLE, blankOf, isJobActive, jobBlank, needsLook,
|
||||
nothingWritten, retryableCount, taskReason,
|
||||
} from './draftJob'
|
||||
import type { DraftStartBody } from './draftJob'
|
||||
import type { DraftPreset, DraftStartBody } from './draftJob'
|
||||
|
||||
// ===========================================================================
|
||||
// 起草数据目录。起草是服务端的后台任务:这里选范围、写起草要求和本次补充说明,建任务;之后显示页面跟着的那个任务
|
||||
// (进行中的进度、结束后的结果),任务本身的状态在页面上(draftJob.ts),关掉对话框照常进行。
|
||||
//
|
||||
// 三段:设置(范围、用不用模型;用模型时写起草要求、补充说明,写明用哪个起草模型)→ 进行中(总进度、合计、调用接连
|
||||
// 没写出来时的提醒、逐表的状态和宽表的段进度、没写全的原因;可以停止、在后台继续)→ 结束(汇总、没写全的表和原因、
|
||||
// 只重试失败的表)。
|
||||
// 没写出来时的提醒、逐表的状态和宽表的段进度、没写全的原因、留空的列;可以停止、在后台继续)→ 结束(汇总、没写全的表
|
||||
// 和原因、留空的列合计、只重试失败的表)。
|
||||
// 已经写进目录的内容不回滚——起草只增改推断项,人工确认、驳回过的项服务端不会动。
|
||||
//
|
||||
// 留空的列(模型列出了、中文名和含义都没给)不算没写全,表照样是已完成;结果里写合计和办法——在起草要求里补充缩写和
|
||||
// 术语的含义,再重新起草这几张表。「重新起草这几张表」回到设置:范围就是那几张、勾着用模型、焦点在起草要求,
|
||||
// 补完再开始。表详情里的「补充起草要求」也是这样打开(范围是那一张,见 preset)。
|
||||
// ===========================================================================
|
||||
|
||||
type Scope = 'selected' | 'filtered' | 'top' | 'all'
|
||||
type Scope = 'preset' | 'selected' | 'filtered' | 'top' | 'all'
|
||||
|
||||
/** 字数按服务端的算法:去掉首尾空白后的字符数(按码位,不按 UTF-16 的长度) */
|
||||
const charCount = (s: string) => [...s.trim()].length
|
||||
|
||||
/** 起草要求输入框的 id:预先选好范围回到设置时焦点放在这里 */
|
||||
const GUIDANCE_ID = 'catalog-draft-guidance'
|
||||
|
||||
/**
|
||||
* 建任务(开始、只重试失败的表)没建成怎么说。起草模型用不了(409,建任务和重试都会回):服务端那句话已经写了原因和
|
||||
* 去处,照原样,长句也不拆成「标题:原因」;别的错误前面写明没有开始
|
||||
@@ -37,8 +44,8 @@ const startErrorText = (e: unknown) =>
|
||||
(e instanceof ApiError && e.code === DRAFT_MODEL_UNAVAILABLE ? e.message : DT.startFailed(errorMessage(e)))
|
||||
|
||||
export function DraftDialog({
|
||||
sourceId, sourceDescription, guidance, rows, visible, selected, filtered, job, initial, initialBusy = false, stopping,
|
||||
onStart, onCancel, onRetry, onTrack, onGuidanceSaved, onClose,
|
||||
sourceId, sourceDescription, guidance, rows, visible, selected, filtered, job, initial, initialBusy = false, preset: initialPreset = null,
|
||||
stopping, onStart, onCancel, onRetry, onTrack, onGuidanceSaved, onClose,
|
||||
}: {
|
||||
sourceId: string
|
||||
/** 数据源说明:用模型起草时连同起草要求一起交给模型 */
|
||||
@@ -58,6 +65,11 @@ export function DraftDialog({
|
||||
initial: 'setup' | 'job'
|
||||
/** 打开时显示的任务是撞上已有任务(409)转过来的:写明 */
|
||||
initialBusy?: boolean
|
||||
/**
|
||||
* 打开设置时预先选好的范围(表详情里「补充起草要求」:只有那张表)。给了就勾上用模型、焦点放在起草要求上:
|
||||
* 打开它就是为了先补起草要求再起草
|
||||
*/
|
||||
preset?: DraftPreset | null
|
||||
/** 点了停止、服务端还没停下来 */
|
||||
stopping: boolean
|
||||
/** 建任务;409 已有任务时页面改取进行中的那一个(busy 为真) */
|
||||
@@ -73,8 +85,12 @@ export function DraftDialog({
|
||||
// 表结构里已经没有的表起草不了(服务端会记「已跳过」),不算进范围
|
||||
const live = useMemo(() => rows.filter((r) => r.in_schema).map((r) => r.table_name), [rows])
|
||||
const liveSet = useMemo(() => new Set(live), [live])
|
||||
/** 预先选好的范围:打开时给的,或者结果里点了「重新起草这几张表」 */
|
||||
const [preset, setPreset] = useState<DraftPreset | null>(initialPreset)
|
||||
const scopes = useMemo(() => {
|
||||
const out: { key: Scope; label: string; tables: string[] }[] = []
|
||||
const pre = preset?.tables.filter((t) => liveSet.has(t)) ?? []
|
||||
if (preset && pre.length) out.push({ key: 'preset', label: preset.label, tables: pre })
|
||||
const sel = selected.filter((t) => liveSet.has(t))
|
||||
if (sel.length) out.push({ key: 'selected', label: CT.scopeSelected(sel.length), tables: sel })
|
||||
const vis = visible.filter((r) => r.in_schema).map((r) => r.table_name)
|
||||
@@ -82,11 +98,13 @@ export function DraftDialog({
|
||||
if (live.length > DRAFT_TOP_N) out.push({ key: 'top', label: CT.scopeTop(DRAFT_TOP_N), tables: live.slice(0, DRAFT_TOP_N) })
|
||||
out.push({ key: 'all', label: CT.scopeAll(live.length), tables: live })
|
||||
return out
|
||||
}, [selected, visible, filtered, live, liveSet])
|
||||
}, [preset, selected, visible, filtered, live, liveSet])
|
||||
const [scope, setScope] = useState<Scope>(() => scopes[0].key)
|
||||
const tables = scopes.find((s) => s.key === scope)?.tables ?? scopes[0].tables
|
||||
const tooMany = tables.length > DRAFT_MAX_TABLES
|
||||
const [useModel, setUseModel] = useState(false)
|
||||
const [useModel, setUseModel] = useState(!!initialPreset)
|
||||
/** 回到设置以后把焦点放进起草要求(预先选好范围的两种入口都是为了先补起草要求) */
|
||||
const [focusGuidance, setFocusGuidance] = useState(!!initialPreset)
|
||||
const [guidanceText, setGuidanceText] = useState(guidance)
|
||||
const [note, setNote] = useState('')
|
||||
const guidanceLen = charCount(guidanceText)
|
||||
@@ -102,7 +120,7 @@ export function DraftDialog({
|
||||
const [busy, setBusy] = useState(initialBusy)
|
||||
const [acting, setActing] = useState(false)
|
||||
const [actionError, setActionError] = useState<string | null>(null)
|
||||
/** 上一次起草(没写全的才提),设置阶段写一行、可以点开 */
|
||||
/** 上一次起草(没写全、有留空列的才提),设置阶段写一行、可以点开 */
|
||||
const [last, setLast] = useState<CatalogDraftJob | null>(null)
|
||||
/** 起草模型的说法:设置里的起草模型,没配就是跟随助手的模型 */
|
||||
const [modelLine, setModelLine] = useState<{ text: string; warn: string | null } | null>(null)
|
||||
@@ -120,13 +138,22 @@ export function DraftDialog({
|
||||
onTrackRef.current(latest)
|
||||
setBusy(true)
|
||||
setMode('job')
|
||||
} else if (latest.status !== 'succeeded') {
|
||||
} else if (latest.status !== 'succeeded' || jobBlank(latest) > 0) {
|
||||
setLast(latest)
|
||||
}
|
||||
}, () => {})
|
||||
return () => { alive = false }
|
||||
}, [inSetup, sourceId])
|
||||
|
||||
// 预先选好范围回到设置(打开时给的、结果里点的):焦点放进起草要求
|
||||
useEffect(() => {
|
||||
if (!focusGuidance || mode !== 'setup' || !useModel) return
|
||||
const el = document.getElementById(GUIDANCE_ID)
|
||||
if (!el) return
|
||||
el.focus()
|
||||
setFocusGuidance(false)
|
||||
}, [focusGuidance, mode, useModel])
|
||||
|
||||
// 起草模型:设置里的 catalog_draft 填了就是它;没填跟随助手的模型(写出当前是哪个,用不了时写原因)
|
||||
useEffect(() => {
|
||||
let alive = true
|
||||
@@ -194,6 +221,26 @@ export function DraftDialog({
|
||||
setBusy(false)
|
||||
setMode('job')
|
||||
}
|
||||
/**
|
||||
* 结果里「重新起草这几张表」(有留空列的表):回到设置,范围就是这几张,勾上用模型,焦点放进起草要求——先补上
|
||||
* 缩写和术语的含义再开始。任务本身不动(页面照旧跟着它,开始新的起草才换)
|
||||
*/
|
||||
const redraftBlank = (names: string[]) => {
|
||||
setPreset({ tables: names, label: DT.scopeBlank(names.length) })
|
||||
setScope('preset')
|
||||
setUseModel(true)
|
||||
setFocusGuidance(true)
|
||||
setStartError(null)
|
||||
setActionError(null)
|
||||
setBusy(false)
|
||||
setLast(null)
|
||||
setMode('setup')
|
||||
}
|
||||
/** 结果里有留空列、还在表结构里的表(「重新起草这几张表」的范围) */
|
||||
const blankTables = useMemo(
|
||||
() => (job ? job.tasks.filter((t) => blankOf(t) > 0 && liveSet.has(t.table_name)).map((t) => t.table_name) : []),
|
||||
[job, liveSet],
|
||||
)
|
||||
|
||||
const phase: 'setup' | 'running' | 'done' = inSetup ? 'setup' : isJobActive(job) ? 'running' : 'done'
|
||||
const canStart = !!tables.length && !tooMany && !(useModel && (guidanceOver || noteOver)) && !starting
|
||||
@@ -263,7 +310,7 @@ export function DraftDialog({
|
||||
{modelLine?.warn && <span className="block text-[var(--warn)]" data-draft-model-warn="">{modelLine.warn}</span>}
|
||||
</div>
|
||||
<div data-draft-guidance="">
|
||||
<Field label={DT.guidance} htmlFor="catalog-draft-guidance" hint={DT.guidanceHint}
|
||||
<Field label={DT.guidance} htmlFor={GUIDANCE_ID} hint={DT.guidanceHint}
|
||||
error={guidanceOver ? DT.tooLong(DRAFT_GUIDANCE_MAX) : undefined}>
|
||||
{(p) => (
|
||||
<textarea {...p} className="field min-h-[4.5rem] resize-y text-xs leading-relaxed" rows={3} value={guidanceText}
|
||||
@@ -292,9 +339,14 @@ export function DraftDialog({
|
||||
)}
|
||||
</div>
|
||||
{last && (
|
||||
// 没写全的用警告色;都写完了、只是有留空列的是提示
|
||||
<div className="flex flex-wrap items-center gap-x-2 gap-y-1 rounded-md border px-3 py-2 text-2xs text-dim" data-draft-last={last.status}>
|
||||
<TriangleAlert size={11} className="shrink-0" style={{ color: 'var(--warn)' }} aria-hidden />
|
||||
<span className="min-w-0 flex-1">{DT.last(DT.title[last.status], formatTime(last.finished_at ?? last.created_at))}</span>
|
||||
{last.status === 'succeeded'
|
||||
? <Info size={11} className="shrink-0" style={{ color: 'var(--accent)' }} aria-hidden />
|
||||
: <TriangleAlert size={11} className="shrink-0" style={{ color: 'var(--warn)' }} aria-hidden />}
|
||||
<span className="min-w-0 flex-1">
|
||||
{DT.last(DT.lastState(DT.title[last.status], jobBlank(last)), formatTime(last.finished_at ?? last.created_at))}
|
||||
</span>
|
||||
<button type="button" className="btn btn-sm" onClick={() => openLast(last)} data-draft-last-open="">{DT.lastOpen}</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -303,7 +355,8 @@ export function DraftDialog({
|
||||
)}
|
||||
|
||||
{phase !== 'setup' && job && (
|
||||
<JobView key={job.id} job={job} running={phase === 'running'} stopping={stopping} busy={busy} />
|
||||
<JobView key={job.id} job={job} running={phase === 'running'} stopping={stopping} busy={busy}
|
||||
blankTables={blankTables} onRedraftBlank={redraftBlank} />
|
||||
)}
|
||||
{phase !== 'setup' && actionError && <Notice tone="err" attr={{ 'data-draft-action-error': '' }}>{actionError}</Notice>}
|
||||
</div>
|
||||
@@ -319,13 +372,26 @@ function Counter({ n, max }: { n: number; max: number }) {
|
||||
)
|
||||
}
|
||||
|
||||
/** 任务的进度或结果:标题、总进度、提醒(进行中)、合计、模型和用量(超时、没写出来的次数)、没写全的表、逐表列表 */
|
||||
function JobView({ job, running, stopping, busy }: { job: CatalogDraftJob; running: boolean; stopping: boolean; busy: boolean }) {
|
||||
/**
|
||||
* 任务的进度或结果:标题、总进度、提醒(进行中)、合计、模型和用量(超时、没写出来的次数)、没写全的表、留空的列合计
|
||||
* (结束后)、逐表列表
|
||||
*/
|
||||
function JobView({ job, running, stopping, busy, blankTables, onRedraftBlank }: {
|
||||
job: CatalogDraftJob
|
||||
running: boolean
|
||||
stopping: boolean
|
||||
busy: boolean
|
||||
/** 有留空列、还在表结构里的表:结束后给「重新起草这几张表」 */
|
||||
blankTables: string[]
|
||||
onRedraftBlank: (tables: string[]) => void
|
||||
}) {
|
||||
const problems = job.tasks.filter((t) => RETRYABLE.has(t.status))
|
||||
// 结束后默认只列要看的表:没写全的、有留空列的(几百张表里找它们太费劲),可以展开全部
|
||||
const attention = job.tasks.filter(needsLook)
|
||||
const blankCount = job.tasks.filter((t) => blankOf(t) > 0).length
|
||||
const [showAll, setShowAll] = useState(false)
|
||||
// 结束后有没写全的表时默认只列这些(几百张表里找失败的太费劲),可以展开全部
|
||||
const onlyProblems = !running && problems.length > 0 && !showAll
|
||||
const tasks = onlyProblems ? problems : job.tasks
|
||||
const onlyProblems = !running && attention.length > 0 && !showAll
|
||||
const tasks = onlyProblems ? attention : job.tasks
|
||||
const title = running ? (stopping ? DT.stopping : DT.title[job.status]) : DT.title[job.status]
|
||||
const empty = nothingWritten(job)
|
||||
const summary = empty === 'model' ? CT.draftNothingWritten
|
||||
@@ -351,6 +417,7 @@ function JobView({ job, running, stopping, busy }: { job: CatalogDraftJob; runni
|
||||
const notice = running ? job.notice?.trim() ?? '' : ''
|
||||
// 额度用完、超时多了,之后的段缩小了:进行中写一句,段数为什么变多看得明白
|
||||
const partColumns = running && job.use_model && job.part_columns != null && job.part_columns < DRAFT_PART_COLUMNS ? job.part_columns : null
|
||||
const blankTotal = jobBlank(job)
|
||||
return (
|
||||
<div className="min-w-0 space-y-3">
|
||||
{busy && <Notice tone="info" attr={{ 'data-draft-busy': '' }}>{DT.busy}</Notice>}
|
||||
@@ -388,14 +455,27 @@ function JobView({ job, running, stopping, busy }: { job: CatalogDraftJob; runni
|
||||
{problemText && (
|
||||
<p className="text-xs leading-relaxed" data-draft-problems="">{problemText}</p>
|
||||
)}
|
||||
{!running && blankTotal > 0 && (
|
||||
// 留空的列不算没写全(表照样是已完成):写合计和办法,给「重新起草这几张表」回到设置先补起草要求
|
||||
<Notice tone="info" attr={{ 'data-draft-blank-summary': '' }}>
|
||||
<p className="font-medium">{DT.blankTotal(blankTotal, blankCount)}</p>
|
||||
<p className="mt-0.5 text-dim">{DT.blankAdvice}</p>
|
||||
{blankTables.length > 0 && (
|
||||
<button type="button" className="btn btn-sm mt-2" title={DT.blankRedraftHint} onClick={() => onRedraftBlank(blankTables)}
|
||||
data-draft-blank-redraft="">
|
||||
<Sparkles size={11} aria-hidden /> {DT.blankRedraft(blankTables.length)}
|
||||
</button>
|
||||
)}
|
||||
</Notice>
|
||||
)}
|
||||
<div>
|
||||
<div className="mb-1 flex items-center gap-2">
|
||||
<span className="text-2xs font-medium text-dim" id="catalog-draft-tasks-label">{DT.tasks}</span>
|
||||
<span className="flex-1" />
|
||||
{!running && problems.length > 0 && problems.length < job.tasks.length && (
|
||||
{!running && attention.length > 0 && attention.length < job.tasks.length && (
|
||||
<button type="button" className="rounded px-1 text-2xs text-[var(--accent)] outline-none hover:underline focus-visible:ring-2 focus-visible:ring-[var(--accent)]"
|
||||
aria-pressed={showAll} onClick={() => setShowAll((v) => !v)} data-draft-show-all="">
|
||||
{showAll ? DT.showProblems : DT.showAll(job.tasks.length)}
|
||||
{showAll ? (blankCount ? DT.showProblemsBlank : DT.showProblems) : DT.showAll(job.tasks.length)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -420,13 +500,16 @@ const TASK_ICON: Record<CatalogDraftTaskStatus, { Icon: LucideIcon | null; color
|
||||
|
||||
/**
|
||||
* 逐表的一行。jobError:任务整体的原因(整体出错、服务重启中断时服务端把没结束的表也记上同一句)。和它一样的不再逐表
|
||||
* 重复——几百张表各写一遍同一句,真正各有原因的那几张反而找不到;任务的原因已经写在上面(标题、提示)
|
||||
* 重复——几百张表各写一遍同一句,真正各有原因的那几张反而找不到;任务的原因已经写在上面(标题、提示)。
|
||||
* 有留空的列时再写一行「N 列模型拿不准、留空了」:列名悬停看得到,也可以展开(服务端最多给 5 个)
|
||||
*/
|
||||
function TaskRow({ task: t, jobError }: { task: CatalogDraftTask; jobError: string | null }) {
|
||||
const { Icon, color } = TASK_ICON[t.status]
|
||||
const reason = jobError && t.error === jobError ? (t.model_error ?? '') : taskReason(t)
|
||||
const parts = t.parts_total != null && t.parts_total > 1 && t.status !== 'pending' && t.status !== 'skipped'
|
||||
const changes = DT.taskAdded(t.added, t.updated)
|
||||
const blank = blankOf(t)
|
||||
const sample = t.blank_sample?.filter(Boolean) ?? []
|
||||
return (
|
||||
<li className="min-w-0 px-3 py-1.5" data-draft-task={t.table_name} data-status={t.status}>
|
||||
<div className="flex min-w-0 items-center gap-2 text-xs">
|
||||
@@ -448,6 +531,14 @@ function TaskRow({ task: t, jobError }: { task: CatalogDraftTask; jobError: stri
|
||||
{reason && (
|
||||
<p className="mt-0.5 break-words pl-5 text-2xs leading-relaxed text-dim" data-draft-task-reason="">{reason}</p>
|
||||
)}
|
||||
{blank > 0 && (sample.length
|
||||
? (
|
||||
<details className="mt-0.5 pl-5 text-2xs leading-relaxed text-dim" data-draft-task-blank={blank}>
|
||||
<summary className="cursor-pointer select-none hover:text-fg" title={DT.blankSample(sample, blank)}>{DT.blank(blank)}</summary>
|
||||
<p className="break-words text-faint" data-draft-task-blank-sample="">{DT.blankSample(sample, blank)}</p>
|
||||
</details>
|
||||
)
|
||||
: <p className="mt-0.5 pl-5 text-2xs leading-relaxed text-dim" data-draft-task-blank={blank}>{DT.blank(blank)}</p>)}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import { ColumnTable, NARROW, Value, pendingCodes } from './ColumnTable'
|
||||
import type { ColumnModel } from './ColumnTable'
|
||||
import { CodesDialog } from './CodesDialog'
|
||||
import { coverageText } from './profile'
|
||||
import { taskReason } from './draftJob'
|
||||
import { lastBlankOf, taskReason } from './draftJob'
|
||||
import {
|
||||
COLUMN_FIELDS, DATE_KEYS, TABLE_FIELDS, buildNotes, changeCount, columnPath, confirmProposed, countsOf, formFromNotes,
|
||||
relationDraft, relationPath, relationRemoval, tablePath, unknownKeys, validateForm,
|
||||
@@ -53,7 +53,7 @@ type Editing = { initial: EditForm; form: EditForm }
|
||||
type Conflict = 'edit' | 'review'
|
||||
|
||||
export function TableDetail({
|
||||
sourceId, table, rows, drafted, fillCodes, onFillDone, prev, next, onOpen, onBack, onDetail, draftBusy = false, onRedraft,
|
||||
sourceId, table, rows, drafted, fillCodes, onFillDone, prev, next, onOpen, onBack, onDetail, draftBusy = false, onRedraft, onDraftGuidance,
|
||||
}: {
|
||||
sourceId: string
|
||||
table: string
|
||||
@@ -77,8 +77,10 @@ export function TableDetail({
|
||||
onDetail: (d: CatalogDetail) => void
|
||||
/** 这个数据源有排队、进行中的起草任务:「重新起草这张表」等它结束 */
|
||||
draftBusy?: boolean
|
||||
/** 上次起草失败、部分完成时的「重新起草这张表」:建只含这张表的起草任务(用模型、不带补充说明) */
|
||||
/** 上次起草失败、部分完成或有留空列时的「重新起草这张表」:建只含这张表的起草任务(用模型、不带补充说明) */
|
||||
onRedraft?: (table: string) => Promise<void>
|
||||
/** 上次起草有留空列时的「补充起草要求」:打开起草设置,范围只有这张表,先补起草要求再开始 */
|
||||
onDraftGuidance?: (table: string) => void
|
||||
}) {
|
||||
const [detail, setDetail] = useState<CatalogDetail | null>(null)
|
||||
const [loadError, setLoadError] = useState<unknown>(null)
|
||||
@@ -318,6 +320,9 @@ export function TableDetail({
|
||||
const counts = countsOf(notes)
|
||||
/** 这张表最近一次起草的结果(清单上那一行带的) */
|
||||
const lastDraft = rows.find((r) => r.table_name === table)?.last_draft ?? null
|
||||
/** 没写全(失败、部分完成);有留空的列(模型拿不准,表照样写完了) */
|
||||
const lastBad = lastDraft && (lastDraft.status === 'failed' || lastDraft.status === 'partial') ? lastDraft : null
|
||||
const lastBlank = lastBlankOf(lastDraft)
|
||||
const label = notes.label && notes.label.status !== 'rejected' ? notes.label.value : null
|
||||
const kind = notes.kind && notes.kind.status !== 'rejected' ? notes.kind.value : null
|
||||
const st = detail.structure
|
||||
@@ -456,21 +461,39 @@ export function TableDetail({
|
||||
</Notice>
|
||||
)}
|
||||
{writeError && <Notice tone="err" attr={{ 'data-catalog-write-error': '' }}>{writeError}</Notice>}
|
||||
{lastDraft && (lastDraft.status === 'failed' || lastDraft.status === 'partial') && (
|
||||
// 上次起草没写全:写明原因,给「重新起草这张表」(只含这张表、用模型)。有起草任务在进行、正在编辑这张表时
|
||||
// 先不给点:起草会写进这张表,编辑中的保存就会撞上版本冲突
|
||||
<Notice tone={lastDraft.status === 'failed' ? 'err' : 'warn'} attr={{ 'data-catalog-last-draft': lastDraft.status }}>
|
||||
<p className="font-medium">{lastDraft.status === 'failed' ? DT.lastFailed : DT.lastPartial}</p>
|
||||
{taskReason(lastDraft) && <p className="mt-0.5 break-words text-dim" data-catalog-last-draft-reason="">{taskReason(lastDraft)}</p>}
|
||||
{lastDraft && (lastBad || lastBlank > 0) && (
|
||||
// 上次起草没写全:写明原因;有留空的列:写几列、怎么办(在起草要求里补充缩写和术语的含义)。只有留空的列时是
|
||||
// 提示(表照样写完了),不是失败。给「重新起草这张表」(只含这张表、用模型);有留空的列再给「补充起草要求」
|
||||
// (打开起草设置,范围是这张表)。有起草任务在进行、正在编辑这张表时先不给点:起草会写进这张表,编辑中的保存
|
||||
// 就会撞上版本冲突
|
||||
<Notice tone={lastBad ? (lastBad.status === 'failed' ? 'err' : 'warn') : 'info'} attr={{ 'data-catalog-last-draft': lastDraft.status }}>
|
||||
{lastBad && <p className="font-medium">{lastBad.status === 'failed' ? DT.lastFailed : DT.lastPartial}</p>}
|
||||
{lastBad && taskReason(lastBad) && (
|
||||
<p className="mt-0.5 break-words text-dim" data-catalog-last-draft-reason="">{taskReason(lastBad)}</p>
|
||||
)}
|
||||
{lastBlank > 0 && (
|
||||
<>
|
||||
<p className={lastBad ? 'mt-0.5 text-dim' : 'font-medium'} data-catalog-last-draft-blank={lastBlank}>{DT.lastBlank(lastBlank)}</p>
|
||||
<p className="mt-0.5 text-dim">{DT.lastBlankAdvice}</p>
|
||||
</>
|
||||
)}
|
||||
{onRedraft && (
|
||||
<>
|
||||
<button type="button" className="btn btn-sm mt-2" disabled={draftBusy || redrafting || !!edit} title={DT.redraftHint}
|
||||
onClick={async () => {
|
||||
setRedrafting(true)
|
||||
try { await onRedraft(table) } finally { setRedrafting(false) }
|
||||
}} data-catalog-redraft="">
|
||||
{redrafting ? <Spinner size={11} /> : <Sparkles size={11} aria-hidden />} {DT.redraft}
|
||||
</button>
|
||||
<div className="mt-2 flex flex-wrap items-center gap-1.5">
|
||||
<button type="button" className="btn btn-sm" disabled={draftBusy || redrafting || !!edit} title={DT.redraftHint}
|
||||
onClick={async () => {
|
||||
setRedrafting(true)
|
||||
try { await onRedraft(table) } finally { setRedrafting(false) }
|
||||
}} data-catalog-redraft="">
|
||||
{redrafting ? <Spinner size={11} /> : <Sparkles size={11} aria-hidden />} {DT.redraft}
|
||||
</button>
|
||||
{lastBlank > 0 && onDraftGuidance && (
|
||||
<button type="button" className="btn btn-sm btn-ghost" disabled={draftBusy || redrafting || !!edit} title={DT.draftGuidanceHint}
|
||||
onClick={() => onDraftGuidance(table)} data-catalog-draft-guidance="">
|
||||
<Pencil size={11} aria-hidden /> {DT.draftGuidance}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<p className="mt-1 text-2xs text-faint">{edit ? DT.redraftEditing : draftBusy ? DT.redraftBusy : DT.redraftHint}</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { memo, useCallback, useEffect, useRef } from 'react'
|
||||
import type { KeyboardEvent as ReactKeyboardEvent, MouseEvent as ReactMouseEvent } from 'react'
|
||||
import { AlertTriangle, History, ScanSearch, Search, Sparkles, X } from 'lucide-react'
|
||||
import { AlertTriangle, History, Info, ScanSearch, Search, Sparkles, X } from 'lucide-react'
|
||||
import clsx from 'clsx'
|
||||
import type { CatalogTableRow } from '../../types'
|
||||
import { useRadioGroup } from '../../components/ui'
|
||||
@@ -10,15 +10,15 @@ import {
|
||||
} from '../../lib/terms'
|
||||
import { CountBadges } from './parts'
|
||||
import { LIST_FILTERS, LIST_SORTS, progressOf } from './model'
|
||||
import { taskReason } from './draftJob'
|
||||
import { lastBlankOf, taskReason } from './draftJob'
|
||||
import type { ListFilter, ListSort } from './model'
|
||||
|
||||
// ===========================================================================
|
||||
// 表清单(数据目录页的左栏):搜索、按审阅进度筛选、排序、多选后批量起草。
|
||||
// 一行两层:上面是中文名和各状态的项数,下面是表名、类型、关联数和使用次数;还没填中文名的表上面直接写表名,
|
||||
// 下面淡淡地写「未填写中文名」——一屏几十行「未填写中文名」认不出是哪张表。上次起草失败、部分完成的表再多一行,
|
||||
// 写明原因(全文在悬停里,点开表还有「重新起草这张表」)。键盘:↑↓ 在行之间移动,Home / End 到两头,Enter 打开;
|
||||
// 复选框按住 Shift 点选可以连选一段。
|
||||
// 写明原因(全文在悬停里,点开表还有「重新起草这张表」);上次起草有留空列(模型拿不准)的表另有一行淡色的轻提示,
|
||||
// 不是失败标记。键盘:↑↓ 在行之间移动,Home / End 到两头,Enter 打开;复选框按住 Shift 点选可以连选一段。
|
||||
// ===========================================================================
|
||||
|
||||
export function TableIndex({
|
||||
@@ -213,6 +213,8 @@ const Row = memo(function Row({ row: r, index, active, checked, onToggle, onOpen
|
||||
const last = r.last_draft && (r.last_draft.status === 'failed' || r.last_draft.status === 'partial') ? r.last_draft : null
|
||||
const lastLabel = last ? (last.status === 'failed' ? DT.lastFailed : DT.lastPartial) : ''
|
||||
const lastText = last ? (taskReason(last) ? DT.lastWith(lastLabel, taskReason(last)) : lastLabel) : ''
|
||||
// 上次起草有留空的列:表照样写完了,只是模型对这几列拿不准。淡色的轻提示,和失败标记分开
|
||||
const blank = lastBlankOf(r.last_draft)
|
||||
return (
|
||||
<li
|
||||
className={clsx('relative flex items-stretch border-b border-hairline', active ? 'bg-accent-soft' : 'hover:bg-hover')}
|
||||
@@ -262,6 +264,13 @@ const Row = memo(function Row({ row: r, index, active, checked, onToggle, onOpen
|
||||
<span className="min-w-0 truncate">{lastText}</span>
|
||||
</span>
|
||||
)}
|
||||
{blank > 0 && (
|
||||
<span className="mt-0.5 flex min-w-0 items-center gap-1 text-2xs text-faint" title={`${DT.lastBlank(blank)}。${DT.lastBlankAdvice}`}
|
||||
data-row-blank={blank}>
|
||||
<Info size={10} className="shrink-0" aria-hidden />
|
||||
<span className="min-w-0 truncate">{DT.lastBlank(blank)}</span>
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
</li>
|
||||
)
|
||||
|
||||
@@ -39,6 +39,29 @@ export const taskReason = (t: { error: string | null; model_error: string | null
|
||||
/** 每段最多几列(服务端起草的起始上限)。任务里的当前上限比它小:额度用完、超时多了,之后的段缩小了 */
|
||||
export const DRAFT_PART_COLUMNS = 40
|
||||
|
||||
/** 模型拿不准、留空的列数(任务里的一张表、清单上的上次起草);老服务端没有这个键时为 0 */
|
||||
export const blankOf = (t: { blank_columns?: number } | null | undefined): number => Math.max(0, t?.blank_columns ?? 0)
|
||||
|
||||
/**
|
||||
* 清单、表详情上「上次起草有 N 列模型拿不准、留空了」的 N:这张表的那次起草结束了才算(还在起草的那一次写到一半,
|
||||
* 不说「上次」)
|
||||
*/
|
||||
export const lastBlankOf = (d: { status: CatalogDraftTaskStatus; blank_columns?: number } | null | undefined): number =>
|
||||
(d && TASK_ENDED.has(d.status) ? blankOf(d) : 0)
|
||||
|
||||
/** 一次起草里留空的列合计:服务端给了合计就用它,老服务端没有就按逐表加 */
|
||||
export const jobBlank = (j: CatalogDraftJob): number => (j.blank_columns != null ? blankOf(j) : j.tasks.reduce((n, t) => n + blankOf(t), 0))
|
||||
|
||||
/** 结束后默认列出的表:没写全的(失败、部分完成、已停止)和有留空列的 */
|
||||
export const needsLook = (t: CatalogDraftTask): boolean => RETRYABLE.has(t.status) || blankOf(t) > 0
|
||||
|
||||
/** 回到设置时预先选好的范围(结果里「重新起草这几张表」、表详情里「补充起草要求」) */
|
||||
export interface DraftPreset {
|
||||
tables: string[]
|
||||
/** 范围那一项的写法 */
|
||||
label: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 一项都没写进去的任务该怎么说(只对写完了的任务:完成、部分完成)。null:照常写合计。
|
||||
* 用模型、却没有一张表拿到模型的可用内容:目录没有更新、模型没给内容;有表没写全:目录没有更新、原因见下方;
|
||||
|
||||
@@ -2893,6 +2893,13 @@ export interface CatalogDraftTask {
|
||||
* 大于 0 而新增、更新、删除都是 0,才是「模型给了内容、只是和现有目录一致」
|
||||
*/
|
||||
model_items: number | null
|
||||
/**
|
||||
* 模型拿不准、留空的列数:模型列出了(或这一段请它写了),中文名和含义却都没给(给了却都没通过校验的也算)。
|
||||
* 表的状态不因此变(照旧可以是已完成)。老服务端没有这个键
|
||||
*/
|
||||
blank_columns?: number
|
||||
/** 留空的列里的前几个(最多 5 个,列名照表结构原样)。老服务端没有这个键 */
|
||||
blank_sample?: string[]
|
||||
/** 整张表没起草的原因:表结构里没有、写入一直冲突、服务重启中断 */
|
||||
error: string | null
|
||||
/** 模型那部分没写出来的原因和办法(第几段、哪些列、为什么) */
|
||||
@@ -2941,6 +2948,8 @@ export interface CatalogDraftJob {
|
||||
added: number
|
||||
updated: number
|
||||
removed: number
|
||||
/** 各表模型拿不准、留空的列数合计。老服务端没有这个键 */
|
||||
blank_columns?: number
|
||||
/** 模型调用的用量;拿不到用量时照实是 0 */
|
||||
usage: CatalogDraftUsage
|
||||
/**
|
||||
@@ -2973,6 +2982,8 @@ export interface CatalogLastDraft {
|
||||
finished_at: string | null
|
||||
error: string | null
|
||||
model_error: string | null
|
||||
/** 那次起草模型拿不准、留空的列数(同 CatalogDraftTask.blank_columns)。老服务端没有这个键 */
|
||||
blank_columns?: number
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -97,7 +97,7 @@ const ORDER = [
|
||||
const ESTIMATE = {
|
||||
challenge: 30,
|
||||
stream: 173, chat: 160, studio: 149, manage: 145, runs: 112, publish: 92, shell: 87,
|
||||
evidence: 91, versions: 60, catalog: 110, 'ui-kit': 70, ui: 61, guards: 8, 'canvas-layout': 2, copy: 2, tokens: 1, trace: 1, decode: 1,
|
||||
evidence: 91, versions: 60, catalog: 130, 'ui-kit': 70, ui: 61, guards: 8, 'canvas-layout': 2, copy: 2, tokens: 1, trace: 1, decode: 1,
|
||||
// 占位,换成真检查时按实测改
|
||||
schedules: 60, compare: 45, verify: 71, inquiry: 60, bundle: 16, whatif: 33, 'report-export': 30,
|
||||
}
|
||||
|
||||
+168
-7
@@ -429,7 +429,7 @@ const PART_COLUMNS = 40
|
||||
* 建一个排队的任务:表结构里没有的表照样建进任务、记 skipped。重试建的记着原任务(retry_of,界面不用)。
|
||||
* 超过 40 列的宽表均匀分段:列分段数 = ceil(列数 / 40),段进度是「表级一段 + 列分段」。
|
||||
* 用模型的任务记着当前每段最多几列(part_columns,额度用完、超时多了服务端会减半)、调用接连没写出来时的提醒(notice),
|
||||
* 用量里另记超时和没写出来的调用次数
|
||||
* 用量里另记超时和没写出来的调用次数;逐表记模型拿不准、留空的列数和前几个列名(blank_columns、blank_sample)
|
||||
*/
|
||||
function newJob(state, src, { tables, use_model, note = null, retry_of = null }) {
|
||||
const id = `check-job-${++state.jobSeq}`
|
||||
@@ -439,7 +439,7 @@ function newJob(state, src, { tables, use_model, note = null, retry_of = null })
|
||||
return {
|
||||
table_name: name, status: t ? 'pending' : 'skipped', columns: cols,
|
||||
parts_total: t && use_model ? (cols > PART_COLUMNS ? 1 + Math.ceil(cols / PART_COLUMNS) : 1) : null, parts_done: 0,
|
||||
added: 0, updated: 0, removed: 0, version: t?.version ?? 0, model_items: null,
|
||||
added: 0, updated: 0, removed: 0, version: t?.version ?? 0, model_items: null, blank_columns: 0, blank_sample: [],
|
||||
error: t ? null : `表结构里没有 ${name},请先重新探查结构`, model_error: null,
|
||||
started_at: null, finished_at: t ? null : isoNow(),
|
||||
}
|
||||
@@ -454,14 +454,14 @@ function newJob(state, src, { tables, use_model, note = null, retry_of = null })
|
||||
return job
|
||||
}
|
||||
|
||||
/** 接口返回的形状:计数、合计由逐表算出来 */
|
||||
/** 接口返回的形状:计数、合计(含留空的列数)由逐表算出来 */
|
||||
function jobOut(job) {
|
||||
const counts = { pending: 0, running: 0, ok: 0, partial: 0, failed: 0, skipped: 0, cancelled: 0 }
|
||||
for (const t of job.tasks) counts[t.status]++
|
||||
const sum = (k) => job.tasks.reduce((n, t) => n + t[k], 0)
|
||||
const { stopAsked, ...rest } = job
|
||||
return clone({ ...rest, total: job.tasks.length, done: job.tasks.filter((t) => TASK_END.has(t.status)).length, counts,
|
||||
added: sum('added'), updated: sum('updated'), removed: sum('removed') })
|
||||
added: sum('added'), updated: sum('updated'), removed: sum('removed'), blank_columns: sum('blank_columns') })
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -527,7 +527,10 @@ function lastDraftOf(state, src, name) {
|
||||
for (const job of [...state.jobs].reverse()) {
|
||||
if (job.source_id !== src) continue
|
||||
const t = job.tasks.find((x) => x.table_name === name)
|
||||
if (t) return { status: t.status, job_id: job.id, finished_at: t.finished_at, error: t.error, model_error: t.model_error }
|
||||
if (t) {
|
||||
return { status: t.status, job_id: job.id, finished_at: t.finished_at, error: t.error, model_error: t.model_error,
|
||||
blank_columns: t.blank_columns }
|
||||
}
|
||||
}
|
||||
return state.tables[src]?.find((x) => x.name === name)?.lastDraft ?? null
|
||||
}
|
||||
@@ -688,6 +691,10 @@ const WIDE_ERR = '这张表 50 列,分 3 段起草,第 3 段(promo_attr_19
|
||||
const CALL_ERR = '调用模型出错:服务暂时不可用(已重试 2 次)'
|
||||
/** 调用接连没写出来时服务端给的提醒(虚构的一句,照约定写明次数、原因和办法) */
|
||||
const DRAFT_NOTICE = '最近 6 次调用有 4 次没写出来(3 次超时、1 次额度用完)。建议在「设置 → 偏好设置」的「起草模型」换一个模型、缩小起草范围,或者停止这次起草'
|
||||
/** 宽表里模型拿不准、留空的列(服务端最多给 5 个列名) */
|
||||
const BLANK_SAMPLE = ['promo_attr_31', 'promo_attr_32', 'promo_attr_33', 'promo_attr_34', 'promo_attr_35']
|
||||
/** 补上缩写含义的起草要求(虚构) */
|
||||
const GUIDANCE_ABBR = `${S1_GUIDANCE}。promo_attr 开头的列是促销活动的附加属性,按列名后面的编号区分`
|
||||
const detail = (page) => page.locator('[data-catalog-detail]')
|
||||
const version = async (page) => Number(await detail(page).getAttribute('data-version'))
|
||||
const panel = (page) => page.locator('[data-item-panel]')
|
||||
@@ -1379,6 +1386,158 @@ await section('数据目录 · 起草:接连没写出来的提醒、超时与
|
||||
await close()
|
||||
})
|
||||
|
||||
await section('数据目录 · 起草:模型拿不准、留空的列', async () => {
|
||||
const state = freshState()
|
||||
// 不在这次起草里的一张表:上一次起草部分完成,又有留空的列
|
||||
state.tables[S1].find((t) => t.name === 'channels').lastDraft = {
|
||||
status: 'partial', job_id: 'check-job-old', finished_at: T0, error: null, model_error: CALL_ERR, blank_columns: 4,
|
||||
}
|
||||
const { page, sent, errors, natives, close } = await open(`/data/catalog/${S1}`, { handlers: catalogHandlers(state) })
|
||||
await page.locator('[data-catalog-row]').first().waitFor()
|
||||
check('开始起草(闸机到会员 3 张,用模型)', await startDraft(page, { select: ['gates', 'members'], model: true }))
|
||||
const job = state.jobs.at(-1)
|
||||
|
||||
// 留空的列:模型列出了、中文名和含义都没给。表的状态不因此变。闸机表写完了;宽表还在起草,已经有几列留空
|
||||
settle(state, job, 'gates', { added: 2, model_items: 3, blank_columns: 1, blank_sample: ['gate_code'] })
|
||||
settle(state, job, 'store_sales', { status: 'running', parts_done: 2, blank_columns: 5, blank_sample: BLANK_SAMPLE })
|
||||
const blankOf = (n) => draftBox(page).locator(`[data-draft-task="${n}"] [data-draft-task-blank]`)
|
||||
const rowBlank = (n) => page.locator(`[data-catalog-row="${n}"] [data-row-blank]`)
|
||||
check('起草中:逐表跟着写留空的列;清单上写完的表才提示「上次起草」,还在起草的不提示',
|
||||
await until(async () => (await blankOf('store_sales').innerText({ timeout: 500 }).catch(() => '')).includes('5 列模型拿不准、留空了')
|
||||
&& (await rowBlank('gates').count()) === 1)
|
||||
&& (await rowBlank('store_sales').count()) === 0 && (await page.locator('[data-catalog-row="store_sales"] [data-row-last-draft]').count()) === 0,
|
||||
`${await draftText(page, '[data-draft-task="store_sales"]')} · ${await rowBlank('gates').count()} · ${await rowBlank('store_sales').count()}`)
|
||||
settle(state, job, 'store_sales', { added: 30, model_items: 38, blank_columns: 12, blank_sample: BLANK_SAMPLE })
|
||||
check('逐表:写完的宽表写「12 列模型拿不准、留空了」,状态照旧是「已完成」',
|
||||
await until(async () => (await blankOf('store_sales').innerText({ timeout: 500 }).catch(() => '')).includes('12 列模型拿不准、留空了'))
|
||||
&& (await draftText(page, '[data-draft-task="store_sales"] [data-draft-task-status]')) === '已完成',
|
||||
await draftText(page, '[data-draft-task="store_sales"]'))
|
||||
const sample = blankOf('store_sales').locator('[data-draft-task-blank-sample]')
|
||||
check('……悬停看得到是哪几列', ((await blankOf('store_sales').locator('summary').getAttribute('title')) ?? '').includes(BLANK_SAMPLE.join('、')),
|
||||
await blankOf('store_sales').locator('summary').getAttribute('title').catch(() => ''))
|
||||
check('……列名默认收起', !(await sample.isVisible()))
|
||||
await blankOf('store_sales').locator('summary').click()
|
||||
check('……展开写出列名,列不全时写一共几列', await until(async () => sample.isVisible())
|
||||
&& (await draftText(page, '[data-draft-task="store_sales"] [data-draft-task-blank-sample]')) === `留空的列:${BLANK_SAMPLE.join('、')} 等 12 列`,
|
||||
await draftText(page, '[data-draft-task="store_sales"] [data-draft-task-blank-sample]'))
|
||||
settle(state, job, 'members', { added: 3, model_items: 3 })
|
||||
finishJob(job)
|
||||
check('结束:标题照旧「起草完成」(留空的列不算没写全),不给「只重试失败的表」',
|
||||
await until(async () => (await draftPhase(page)) === 'done') && (await draftText(page, '[data-draft-title]')) === '起草完成'
|
||||
&& await page.locator('[data-draft-retry]').count() === 0, await draftText(page, '[data-draft-title]'))
|
||||
const blankSum = await draftText(page, '[data-draft-blank-summary]')
|
||||
check('……汇总写合计:共 13 列模型拿不准、留空了(2 张表)', blankSum.includes('共 13 列模型拿不准、留空了(2 张表)'), blankSum)
|
||||
check('……并给出办法:在起草要求里补充缩写和术语的含义,再重新起草这几张表,已有的内容不会丢',
|
||||
blankSum.includes('在起草要求里补充缩写和术语的含义') && blankSum.includes('重新起草这几张表') && blankSum.includes('已有的内容不会丢'), blankSum)
|
||||
const shown = await draftBox(page).locator('[data-draft-task]').evaluateAll((els) => els.map((e) => e.getAttribute('data-draft-task')))
|
||||
check('……逐表默认只列有留空列的 2 张,没有留空列的那张不写,可以显示全部', shown.join(',') === 'gates,store_sales'
|
||||
&& await draftBox(page).locator('[data-draft-show-all]').count() === 1, shown.join(','))
|
||||
await draftBox(page).locator('[data-draft-show-all]').click()
|
||||
check('……显示全部:没有留空列的表不写留空', (await draftBox(page).locator('[data-draft-task]').count()) === 3 && await blankOf('members').count() === 0)
|
||||
await shot(page, 'catalog-draft-blank')
|
||||
|
||||
// 清单:有留空列的表显示轻提示,不是失败标记
|
||||
check('清单:有留空列的表显示轻提示「上次起草有 12 列模型拿不准、留空了」',
|
||||
await until(async () => (await rowBlank('store_sales').innerText({ timeout: 500 }).catch(() => '')).includes('上次起草有 12 列模型拿不准、留空了')),
|
||||
await rowBlank('store_sales').innerText({ timeout: 500 }).catch(() => ''))
|
||||
const colors = await page.evaluate(() => {
|
||||
const color = (q) => { const el = document.querySelector(q); return el ? getComputedStyle(el).color : null }
|
||||
return { blank: color('[data-catalog-row="store_sales"] [data-row-blank]'), faint: color('[data-catalog-row="store_sales"] [data-row-usage]'),
|
||||
partial: color('[data-catalog-row="channels"] [data-row-last-draft]') }
|
||||
})
|
||||
check('……不是失败标记:不写「上次起草失败 / 部分完成」,用次要信息的淡色,不用警告色', await page.locator('[data-catalog-row="store_sales"] [data-row-last-draft]').count() === 0
|
||||
&& !!colors.blank && colors.blank === colors.faint && colors.blank !== colors.partial, JSON.stringify(colors))
|
||||
check('……没有留空列的表不提示', await rowBlank('members').count() === 0)
|
||||
check('……部分完成又有留空列的表:失败标记和留空提示都在', (await page.locator('[data-catalog-row="channels"] [data-row-last-draft="partial"]').count()) === 1
|
||||
&& (await rowBlank('channels').innerText().catch(() => '')).includes('上次起草有 4 列模型拿不准、留空了'))
|
||||
|
||||
// 「重新起草这 2 张表」:回到设置,范围就是这 2 张、勾着用模型,焦点在起草要求;补上缩写的含义再开始
|
||||
await draftBox(page).locator('[data-draft-blank-redraft]').click()
|
||||
check('「重新起草这 2 张表」:回到设置,范围是有留空列的这 2 张,勾着用模型,焦点在起草要求',
|
||||
await until(async () => (await draftPhase(page)) === 'setup')
|
||||
&& await draftBox(page).locator('[data-draft-scope="preset"] input').isChecked()
|
||||
&& (await draftText(page, '[data-draft-scope="preset"]')).includes('有留空列的 2 张表')
|
||||
&& await draftBox(page).locator('[data-draft-model] input').isChecked()
|
||||
&& await until(async () => page.evaluate(() => document.activeElement?.id === 'catalog-draft-guidance')),
|
||||
`${await draftText(page, '[data-draft-scope="preset"]')} · ${await page.evaluate(() => document.activeElement?.id ?? '')}`)
|
||||
check('……起草按钮写「起草 2 张表」', (await page.locator('[data-draft-start]').innerText()).includes('起草 2 张表'))
|
||||
await draftBox(page).locator('[data-draft-guidance] textarea').fill(GUIDANCE_ABBR)
|
||||
await page.locator('[data-draft-start]').click()
|
||||
check('……先存起草要求,再建只含这 2 张表、用模型的任务', await until(async () => (await draftPhase(page)) === 'running')
|
||||
&& JSON.stringify(lastBody(sent, /catalog\/guidance$/)) === JSON.stringify({ guidance: GUIDANCE_ABBR })
|
||||
&& JSON.stringify(lastBody(sent, /draft-jobs$/)) === JSON.stringify({ tables: ['gates', 'store_sales'], use_model: true }),
|
||||
JSON.stringify(lastBody(sent, /draft-jobs$/)))
|
||||
// 这一回宽表仍有几列拿不准(已有的内容不丢由服务端保证)。关着对话框时结束:提示里也写留空的列
|
||||
const again = state.jobs.at(-1)
|
||||
await page.locator('[data-draft-hide]').click()
|
||||
await until(async () => (await draftBox(page).count()) === 0)
|
||||
settle(state, again, 'gates', { model_items: 3 })
|
||||
settle(state, again, 'store_sales', { updated: 5, model_items: 40, blank_columns: 7, blank_sample: BLANK_SAMPLE })
|
||||
finishJob(again)
|
||||
const toastBox = page.locator('[data-toast]').filter({ hasText: '起草完成' })
|
||||
check('关着对话框时结束:提示里写「7 列模型拿不准、留空了」', await until(async () => (await toastBox.count()) > 0)
|
||||
&& (await toastBox.first().innerText()).includes('7 列模型拿不准、留空了'),
|
||||
await page.locator('[data-toast]').allInnerTexts().then((t) => t.join(' | ')).catch(() => ''))
|
||||
await toastBox.getByRole('button', { name: '查看结果' }).click()
|
||||
check('……点「查看结果」:汇总写合计', await until(async () => (await draftText(page, '[data-draft-blank-summary]')).includes('共 7 列模型拿不准、留空了(1 张表)')),
|
||||
await draftText(page, '[data-draft-blank-summary]'))
|
||||
await page.locator('[data-draft-close]').click()
|
||||
// 再打开起草:设置阶段写明上一次有留空的列,可以点开
|
||||
await page.locator('[data-catalog-draft]').first().click()
|
||||
await draftBox(page).waitFor()
|
||||
check('再打开起草:设置阶段写明上一次起草有 7 列模型拿不准、留空了,可以点开', await until(async () => (await draftText(page, '[data-draft-last]')).includes('7 列模型拿不准、留空了'))
|
||||
&& await draftBox(page).locator('[data-draft-last-open]').count() === 1, await draftText(page, '[data-draft-last]'))
|
||||
await page.keyboard.press('Escape')
|
||||
await until(async () => (await draftBox(page).count()) === 0)
|
||||
|
||||
// 表详情:顶部同样提示(不是失败),给「重新起草这张表」和「补充起草要求」
|
||||
await page.locator('[data-catalog-row="store_sales"] [data-catalog-open-row]').click()
|
||||
const tip = page.locator('[data-catalog-last-draft]')
|
||||
check('表详情:顶部写「上次起草有 7 列模型拿不准、留空了」,是提示(不是失败)',
|
||||
await until(async () => (await tip.innerText({ timeout: 500 }).catch(() => '')).includes('上次起草有 7 列模型拿不准、留空了'))
|
||||
&& (await tip.getAttribute('data-notice')) === 'info', await tip.innerText({ timeout: 500 }).catch(() => ''))
|
||||
check('……写明办法:补充起草要求再重新起草,已有的内容不会丢;给「重新起草这张表」', (await tip.innerText()).includes('已有的内容不会丢')
|
||||
&& await tip.locator('[data-catalog-redraft]').count() === 1, await tip.innerText())
|
||||
await shot(page, 'catalog-blank-detail')
|
||||
const jobsBefore = count(sent, /^POST .*draft-jobs$/)
|
||||
await tip.locator('[data-catalog-draft-guidance]').click()
|
||||
check('「补充起草要求」:打开起草设置,范围只有这张表,勾着用模型,焦点在起草要求',
|
||||
await until(async () => (await draftPhase(page)) === 'setup')
|
||||
&& await draftBox(page).locator('[data-draft-scope="preset"] input').isChecked()
|
||||
&& (await draftText(page, '[data-draft-scope="preset"]')).includes('store_sales')
|
||||
&& await draftBox(page).locator('[data-draft-model] input').isChecked()
|
||||
&& (await page.locator('[data-draft-start]').innerText()).includes('起草 1 张表')
|
||||
&& await until(async () => page.evaluate(() => document.activeElement?.id === 'catalog-draft-guidance')),
|
||||
`${await draftText(page, '[data-draft-scope="preset"]')} · ${await page.evaluate(() => document.activeElement?.id ?? '')}`)
|
||||
await page.keyboard.press('Escape')
|
||||
check('……没改就关掉:不建任务', await until(async () => (await draftBox(page).count()) === 0) && count(sent, /^POST .*draft-jobs$/) === jobsBefore)
|
||||
await tip.locator('[data-catalog-redraft]').click()
|
||||
check('「重新起草这张表」:建只含这张表、用模型的任务', await until(async () => count(sent, /^POST .*draft-jobs$/) === jobsBefore + 1)
|
||||
&& JSON.stringify(lastBody(sent, /draft-jobs$/)) === JSON.stringify({ tables: ['store_sales'], use_model: true }),
|
||||
JSON.stringify(lastBody(sent, /draft-jobs$/)))
|
||||
const third = state.jobs.at(-1)
|
||||
await until(async () => (await draftPhase(page)) === 'running')
|
||||
settle(state, third, 'store_sales', { model_items: 40 })
|
||||
finishJob(third)
|
||||
await until(async () => (await draftPhase(page)) === 'done')
|
||||
await page.locator('[data-draft-close]').click()
|
||||
check('……这回没有留空的列:清单的轻提示和详情的提示都收起', await until(async () => (await rowBlank('store_sales').count()) === 0 && (await tip.count()) === 0))
|
||||
|
||||
// 部分完成又有留空列的表:一条说明里写原因和留空的列,只有一个「重新起草这张表」
|
||||
await page.locator('[data-catalog-row="channels"] [data-catalog-open-row]').click()
|
||||
check('部分完成又有留空列的表:详情里一条说明写原因和留空的列,只有一个「重新起草这张表」',
|
||||
await until(async () => (await tip.innerText({ timeout: 500 }).catch(() => '')).includes('上次起草部分完成'))
|
||||
&& (await tip.innerText()).includes(CALL_ERR) && (await tip.innerText()).includes('4 列模型拿不准、留空了')
|
||||
&& (await tip.getAttribute('data-notice')) === 'warn' && await tip.locator('[data-catalog-redraft]').count() === 1,
|
||||
await tip.innerText({ timeout: 500 }).catch(() => ''))
|
||||
await cleanCopy(page, '留空列的提示')
|
||||
check('只发过起草任务相关的写请求', writes(sent, /./).every((s) => /catalog\/(draft-jobs|guidance)/.test(s.key)),
|
||||
JSON.stringify(writes(sent, /./).map((s) => s.key)))
|
||||
check('没有原生对话框', natives.length === 0, natives.join(' | '))
|
||||
check('没有运行时报错', errors.length === 0, errors[0] ?? '')
|
||||
await close()
|
||||
})
|
||||
|
||||
await section('数据目录 · 表详情与单项审阅', async () => {
|
||||
const state = freshState()
|
||||
const { page, sent, errors, natives, close } = await open(`/data/catalog/${S1}`, { handlers: catalogHandlers(state) })
|
||||
@@ -2469,14 +2628,16 @@ await section('数据目录 · 窄屏', async () => {
|
||||
await until(async () => (await draftPhase(page)) === 'running')
|
||||
const narrowJob = state.jobs.at(-1)
|
||||
settle(state, narrowJob, 'visits', { added: 2 })
|
||||
settle(state, narrowJob, 'store_sales', { status: 'partial', parts_done: 2, added: 4, model_error: WIDE_ERR })
|
||||
settle(state, narrowJob, 'store_sales', { status: 'partial', parts_done: 2, added: 4, model_error: WIDE_ERR, blank_columns: 12, blank_sample: BLANK_SAMPLE })
|
||||
// 接连没写出来的提醒、超时和没写出来的次数、段缩小的说明也都在
|
||||
Object.assign(narrowJob.usage, { timeouts: 3, failed_calls: 4 })
|
||||
Object.assign(narrowJob, { part_columns: 20, notice: DRAFT_NOTICE })
|
||||
await until(async () => (await draftText(page, '[data-draft-task="store_sales"] [data-draft-task-reason]')).includes('第 3 段')
|
||||
&& (await draftText(page, '[data-draft-notice]')) === DRAFT_NOTICE)
|
||||
await draftBox(page).locator('[data-draft-task="store_sales"] [data-draft-task-blank] summary').click().catch(() => {})
|
||||
const runFit = await draftFits()
|
||||
check('……起草进行中:段进度、长原因、提醒都折行,弹窗不横向滚动', runFit.ok, JSON.stringify(runFit))
|
||||
check('……起草进行中:段进度、长原因、提醒、展开的留空列名都折行,弹窗不横向滚动', runFit.ok
|
||||
&& await draftBox(page).locator('[data-draft-task-blank-sample]').isVisible().catch(() => false), JSON.stringify(runFit))
|
||||
await shot(page, 'catalog-narrow-draft')
|
||||
await page.locator('[data-draft-hide]').click()
|
||||
await until(async () => (await page.locator('[data-catalog-draft-progress]').count()) === 1)
|
||||
|
||||
Reference in New Issue
Block a user