数据目录起草(前端):表结构里已经没有的表(只剩目录)不再给「重新起草这张表」「补充起草要求」——服务端只会把它记成已跳过。表详情顶部照写上次起草的结果(没写全的原因、留空了几列),不再建议补充起草要求后重新起草,写明「表结构中已没有这张表,不能重新起草」;清单上留空列的轻提示照旧,悬停里不再建议重新起草。起草设置收到的预先选好的范围(表详情的「补充起草要求」、结果里的「重新起草这几张表」)里的表已经不在表结构里时不再退回别的范围:那一项照旧选着,写明「表结构中已没有这张表,不能起草」(只少了几张时写其中几张不会起草),不能开始,也不默认勾用模型、不把焦点放进起草要求——以前退到「使用次数最多的 20 张表」还勾着用模型,以为只起草这一张,实际按别的范围起草了;选的那一项不在了时,单选框照实际起草的那一项勾。check-catalog 新加一节:只剩目录的表的清单提示与详情;打开详情以后表才没了(清单重取了、详情没重新载入)时「补充起草要求」打开的设置;重新打开后不再给两个按钮。check-all 里数据目录的耗时按实测改成 140 秒

This commit is contained in:
ThearchyHelios
2026-10-13 21:13:15 +08:00
parent 912bc3f107
commit ead8ea216b
6 changed files with 137 additions and 20 deletions
+9
View File
@@ -2434,6 +2434,13 @@ export const CATALOG_DRAFT_TEXT = {
// ---- 从结果、表详情回到设置:范围就是那几张表
scopeBlank: (n: number) => `有留空列的 ${formatNumber(n)} 张表`,
scopeTable: (name: string) => `这张表:${name}`,
/**
* 预先选好的范围里有表已经不在表结构里(打开表详情以后别处重新探查了结构):写在那一项下面。一张都不剩时那一项
* 照旧选着、不能开始,不退回别的范围
*/
presetMissing: (gone: number, total: number) => (gone < total
? `其中 ${formatNumber(gone)} 张在表结构中已没有,不会起草`
: total === 1 ? '表结构中已没有这张表,不能起草' : `表结构中已没有这 ${formatNumber(total)} 张表,不能起草`),
// ---- 进行中
queued: '排队中,等待开始…',
running: '正在起草…',
@@ -2538,6 +2545,8 @@ export const CATALOG_DRAFT_TEXT = {
redraftHint: '用模型重新起草这张表(不带补充说明,带上数据源的起草要求);人工确认、驳回过的项不会被改动',
redraftBusy: '已有起草任务在进行,结束后再重新起草这张表',
redraftEditing: '正在编辑这张表,保存或取消后再重新起草',
/** 表结构里已经没有的表(只剩目录):上次起草的结果照写,不给重新起草(服务端只会把它记成已跳过) */
redraftMissing: '表结构中已没有这张表,不能重新起草',
}
/** 设置页「偏好设置」里「起草模型」一节(服务端 api/settings.py 的 catalog_draft 一组) */
+29 -12
View File
@@ -25,7 +25,8 @@ import type { DraftPreset, DraftStartBody } from './draftJob'
//
// 留空的列(模型列出了、中文名和含义都没给)不算没写全,表照样是已完成;结果里写合计和办法——在起草要求里补充缩写和
// 术语的含义,再重新起草这几张表。「重新起草这几张表」回到设置:范围就是那几张、勾着用模型、焦点在起草要求,
// 补完再开始。表详情里的「补充起草要求」也是这样打开(范围是那一张,见 preset)。
// 补完再开始。表详情里的「补充起草要求」也是这样打开(范围是那一张,见 preset)。预先选好的范围里的表已经不在表结构
// 里时不退回别的范围:那一项照旧选着、写明原因,不能开始。
// ===========================================================================
type Scope = 'preset' | 'selected' | 'filtered' | 'top' | 'all'
@@ -66,8 +67,9 @@ export function DraftDialog({
/** 打开时显示的任务是撞上已有任务(409)转过来的:写明 */
initialBusy?: boolean
/**
* 打开设置时预先选好的范围(表详情里「补充起草要求」:只有那张表)。给了就勾上用模型、焦点放在起草要求上:
* 打开它就是为了先补起草要求再起草
* 打开设置时预先选好的范围(表详情里「补充起草要求」:只有那张表)。给了就选它;里面有可起草的表时勾上用模型、
* 焦点放在起草要求上(打开它就是为了先补起草要求再起草)。一张可起草的都没有(已不在表结构里)时照旧选着它、
* 写明原因、不能开始,也不勾用模型——不退回别的范围
*/
preset?: DraftPreset | null
/** 点了停止、服务端还没停下来 */
@@ -88,9 +90,13 @@ export function DraftDialog({
/** 预先选好的范围:打开时给的,或者结果里点了「重新起草这几张表」 */
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 out: { key: Scope; label: string; tables: string[]; gone?: number }[] = []
// 预先选好的范围总在第一项:里面的表已经不在表结构里的(打开表详情以后别处重新探查了结构)不起草、写明有几张;
// 一张都不剩也留着这一项,不退回别的范围——以为只起草那一张,实际按别的范围用模型起草了
if (preset) {
const pre = preset.tables.filter((t) => liveSet.has(t))
out.push({ key: 'preset', label: preset.label, tables: pre, gone: preset.tables.length - pre.length })
}
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)
@@ -100,11 +106,15 @@ export function DraftDialog({
return out
}, [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 current = scopes.find((s) => s.key === scope) ?? scopes[0]
const tables = current.tables
const tooMany = tables.length > DRAFT_MAX_TABLES
const [useModel, setUseModel] = useState(!!initialPreset)
/** 打开时给的范围里有可起草的表:勾上用模型、焦点放进起草要求(打开它就是为了先补起草要求);一张都没有就都不做 */
const presetLive = !!initialPreset && initialPreset.tables.some((t) => liveSet.has(t))
const [useModel, setUseModel] = useState(presetLive)
/** 回到设置以后把焦点放进起草要求(预先选好范围的两种入口都是为了先补起草要求) */
const [focusGuidance, setFocusGuidance] = useState(!!initialPreset)
const [focusGuidance, setFocusGuidance] = useState(presetLive)
const [guidanceText, setGuidanceText] = useState(guidance)
const [note, setNote] = useState('')
const guidanceLen = charCount(guidanceText)
@@ -284,9 +294,16 @@ export function DraftDialog({
<div className="space-y-1.5" role="radiogroup" aria-label={CT.draftScope}>
{scopes.map((s) => (
<label key={s.key} className={clsx('flex cursor-pointer items-center gap-2 rounded-md border px-3 py-2 text-xs',
scope === s.key ? 'border-[var(--accent)] bg-accent-soft' : 'hover:bg-hover')} data-draft-scope={s.key}>
<input type="radio" name="catalog-draft-scope" checked={scope === s.key} onChange={() => setScope(s.key)} />
<span>{s.label}</span>
current.key === s.key ? 'border-[var(--accent)] bg-accent-soft' : 'hover:bg-hover')} data-draft-scope={s.key}>
<input type="radio" name="catalog-draft-scope" checked={current.key === s.key} onChange={() => setScope(s.key)} />
<span className="min-w-0 break-words">
{s.label}
{!!s.gone && (
<span className={clsx('block text-2xs', s.tables.length ? 'text-faint' : 'text-[var(--warn)]')} data-draft-preset-missing="">
{DT.presetMissing(s.gone, s.gone + s.tables.length)}
</span>
)}
</span>
</label>
))}
</div>
+10 -5
View File
@@ -77,9 +77,12 @@ 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)
@@ -465,7 +468,8 @@ export function TableDetail({
// 上次起草没写全:写明原因;有留空的列:写几列、怎么办(在起草要求里补充缩写和术语的含义)。只有留空的列时是
// 提示(表照样写完了),不是失败。给「重新起草这张表」(只含这张表、用模型);有留空的列再给「补充起草要求」
// (打开起草设置,范围是这张表)。有起草任务在进行、正在编辑这张表时先不给点:起草会写进这张表,编辑中的保存
// 就会撞上版本冲突
// 就会撞上版本冲突。表结构里已经没有这张表(只剩目录)时上次的结果照写,不给这两个按钮、不建议重新起草,
// 写明为什么:服务端只会把它记成「已跳过」
<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) && (
@@ -474,10 +478,11 @@ export function TableDetail({
{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>
{detail.in_schema && <p className="mt-0.5 text-dim">{DT.lastBlankAdvice}</p>}
</>
)}
{onRedraft && (
{!detail.in_schema && <p className="mt-1 text-2xs text-faint" data-catalog-redraft-missing="">{DT.redraftMissing}</p>}
{detail.in_schema && onRedraft && (
<>
<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}
+4 -2
View File
@@ -213,8 +213,10 @@ 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)
const blankTitle = r.in_schema ? `${DT.lastBlank(blank)}。${DT.lastBlankAdvice}` : DT.lastBlank(blank)
return (
<li
className={clsx('relative flex items-stretch border-b border-hairline', active ? 'bg-accent-soft' : 'hover:bg-hover')}
@@ -265,7 +267,7 @@ const Row = memo(function Row({ row: r, index, active, checked, onToggle, onOpen
</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}`}
<span className="mt-0.5 flex min-w-0 items-center gap-1 text-2xs text-faint" title={blankTitle}
data-row-blank={blank}>
<Info size={10} className="shrink-0" aria-hidden />
<span className="min-w-0 truncate">{DT.lastBlank(blank)}</span>
+1 -1
View File
@@ -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: 130, 'ui-kit': 70, ui: 61, guards: 8, 'canvas-layout': 2, copy: 2, tokens: 1, trace: 1, decode: 1,
evidence: 91, versions: 60, catalog: 140, '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,
}
+84
View File
@@ -1544,6 +1544,90 @@ await section('数据目录 · 起草:模型拿不准、留空的列', async (
await close()
})
await section('数据目录 · 起草:表结构里已经没有的表', async () => {
const state = freshState()
const tOf = (n) => state.tables[S1].find((t) => t.name === n)
// 起草过(部分完成、留了几列空)的表后来重新探查结构时没了,只剩目录;另一张有留空列的这时还在表结构里
tOf('legacy_coupons').lastDraft = {
status: 'partial', job_id: 'check-job-old', finished_at: T0, error: null, model_error: CALL_ERR, blank_columns: 3,
}
tOf('channels').lastDraft = { status: 'ok', job_id: 'check-job-old', finished_at: T0, error: null, model_error: null, 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()
const tip = page.locator('[data-catalog-last-draft]')
const tipText = async () => (await tip.innerText({ timeout: 500 }).catch(() => '')).replace(/\s+/g, ' ').trim()
const buttons = () => tip.locator('[data-catalog-redraft], [data-catalog-draft-guidance]').count()
// 清单:只剩目录的表照样写上次起草留空了几列(是历史),悬停里不再建议重新起草(起草不了)
const goneBlank = page.locator('[data-catalog-row="legacy_coupons"] [data-row-blank]')
const goneTitle = (await goneBlank.getAttribute('title', { timeout: 1000 }).catch(() => '')) ?? ''
check('清单:只剩目录的表照样提示上次起草留空的列,悬停里不建议重新起草',
(await goneBlank.innerText({ timeout: 500 }).catch(() => '')).includes('上次起草有 3 列模型拿不准、留空了')
&& !!goneTitle && !goneTitle.includes('重新起草'), goneTitle)
// 表详情:写明上次起草的结果,不给「重新起草这张表」「补充起草要求」(服务端只会把它记成已跳过),写明为什么
await page.locator('[data-catalog-row="legacy_coupons"] [data-catalog-open-row]').click()
check('只剩目录的表:详情顶部照样写上次起草部分完成、原因和留空的列',
await until(async () => (await tipText()).includes('上次起草部分完成'))
&& (await tipText()).includes(CALL_ERR) && (await tipText()).includes('3 列模型拿不准、留空了'), await tipText())
check('……不给「重新起草这张表」「补充起草要求」,写明表结构中已没有这张表、不能重新起草',
await buttons() === 0
&& (await tip.locator('[data-catalog-redraft-missing]').innerText({ timeout: 500 }).catch(() => '')).includes('表结构中已没有这张表,不能重新起草'),
await tipText())
check('……也不建议补充起草要求再重新起草', !(await tipText()).includes('再重新起草'), await tipText())
await shot(page, 'catalog-missing-last-draft')
// 打开详情以后表才没了(别处重新探查了结构):清单重取了、详情还没有重新载入。这时点「补充起草要求」,起草设置
// 不能退回别的范围(以前退到「使用次数最多的 20 张表」还勾着用模型,以为只起草这一张,实际按别的范围起草了)
await page.locator('[data-catalog-row="channels"] [data-catalog-open-row]').click()
check('还在表结构里的表:详情给「重新起草这张表」和「补充起草要求」',
await until(async () => (await tip.locator('[data-catalog-draft-guidance]').count()) === 1)
&& await tip.locator('[data-catalog-redraft]').count() === 1, await tipText())
tOf('channels').missing = true
// 起草另一张表:每张表起草完页面就重取清单;正在看的这张不在任务里,详情不重新载入
check('起草另一张表(不用模型)', await startDraft(page, { select: ['gates'] }))
const job = state.jobs.at(-1)
settle(state, job, 'gates', { added: 1 })
finishJob(job)
await until(async () => (await draftPhase(page)) === 'done')
await page.locator('[data-draft-close]').click()
check('……清单重取回来:这张表标成表结构中已没有,详情还是打开时的那份',
await until(async () => (await page.locator('[data-catalog-row="channels"] [data-row-missing]').count()) === 1)
&& await tip.locator('[data-catalog-draft-guidance]').count() === 1)
const jobsBefore = count(sent, /^POST .*draft-jobs$/)
await tip.locator('[data-catalog-draft-guidance]').click()
const presetInput = draftBox(page).locator('[data-draft-scope="preset"] input')
check('「补充起草要求」:范围仍是这张表、选着它,写明表结构中已没有这张表、不能起草',
await until(async () => (await draftPhase(page)) === 'setup')
&& await presetInput.isChecked({ timeout: 1000 }).catch(() => false)
&& (await draftText(page, '[data-draft-scope="preset"]')).includes('channels')
&& (await draftText(page, '[data-draft-preset-missing]')).includes('表结构中已没有这张表,不能起草'),
`${await draftText(page, '[data-draft-scope="preset"]')} · 选中:${await draftBox(page).locator('[data-draft-scope]:has(input:checked)').getAttribute('data-draft-scope', { timeout: 500 }).catch(() => '')}`)
check('……不能开始,也不默认勾用模型,焦点不放进起草要求', await page.locator('[data-draft-start]').isDisabled()
&& !(await draftBox(page).locator('[data-draft-model] input').isChecked())
&& await page.evaluate(() => document.activeElement?.id !== 'catalog-draft-guidance'),
`${await page.locator('[data-draft-start]').innerText()} · 用模型:${await draftBox(page).locator('[data-draft-model] input').isChecked()}`)
// 截图等弹窗的打开动效播完
if (SHOTS) await sleep(400)
await shot(page, 'catalog-missing-preset')
await page.keyboard.press('Escape')
check('……关掉:没有建任务', await until(async () => (await draftBox(page).count()) === 0) && count(sent, /^POST .*draft-jobs$/) === jobsBefore)
// 换一张再回来:详情重新载入,也认得表结构中已没有,不再给两个按钮
await page.locator('[data-catalog-row="gates"] [data-catalog-open-row]').click()
await until(async () => (await page.locator('[data-catalog-detail="gates"][data-version]').count()) === 1)
await page.locator('[data-catalog-row="channels"] [data-catalog-open-row]').click()
check('重新打开这张表:详情也写明表结构中已没有,不再给两个按钮',
await until(async () => (await page.locator('[data-catalog-detail="channels"] [data-catalog-missing]').count()) === 1)
&& await buttons() === 0 && (await tipText()).includes('不能重新起草'), await tipText())
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) })