mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 00:31:58 +00:00
数据目录闭环(前端)复核修正(审阅队列):每一项的位置按 path 说到列(「列 ticket_no 的度量类型」「表的有效记录条件」「指向 members 的关联关系」,同提案卡片),复选框的读屏名也写到列——服务端的 field_label 只是字段的叫法(「码值」「中文名」),列名另放在 column,列级的项只写它分不清是哪一列;审阅进度三行同一个口径:已确认 ÷(已确认 + 待确认),已驳回的、有确证的不计入,审完了就是 100%,悬停写明口径;常用表写实际几张(服务端只算被运行用过的),没有时写「还没有运行用过的表」,不再写死「常用 20 张表」;「用过这张表的运行」写「共 N 次运行」并说明和表头按查询算的使用次数不是一个数,队列里使用次数的悬停同表清单;check-catalog-loop 的队列、进度、导入预览夹具照服务端的真实形状(field_label 只写字段名、带 column,reasons 用服务端的说法,进度带 items_proposed,导入报告带 version、error、冲突的状态),补位置、进度口径的纯函数和页面检查
This commit is contained in:
@@ -3082,7 +3082,11 @@ export const CATALOG_ASSISTANT_TEXT = {
|
||||
// - 「影响数字的项」:有效记录条件、去重规则、业务日期、关联关系、业务主键、度量类型——写错了查出来的数就不对,排在最前;
|
||||
// 「名称和含义」:中文名、含义、单位、码值、粒度、表类型。
|
||||
// - 审阅过的项原地标出结果(已确认、已驳回、未生效),刷新队列后才移除;「撤销审阅」回到起草时的状态(推断)。
|
||||
// - 「常用 20 张表」:使用次数最多的 20 张。进度写「已确认 42%」。
|
||||
// - 每一项的位置写「列 status 的码值」「表的有效记录条件」「指向 parks 的关联关系」(同提案卡片),不只写字段名。
|
||||
// - 「常用 N 张表」:被运行用过、使用次数最多的前几张(最多 20 张),N 写实际张数;没有运行用过的表时写明。
|
||||
// - 进度写「已确认 42%」,三行同一个口径:已确认 ÷(已确认 + 待确认);已驳回的、有确证的(外键约束)不计入。
|
||||
// - 「使用 N 次」是所在表被运行查询过的次数(按查询算,同表清单);「用过这张表的运行」写「共 N 次运行」(按运行算),
|
||||
// 两个数不一样,各自写明单位。
|
||||
// ===========================================================================
|
||||
|
||||
export const CATALOG_REVIEW_TEXT = {
|
||||
@@ -3120,7 +3124,7 @@ export const CATALOG_REVIEW_TEXT = {
|
||||
selected: (n: number) => `已选 ${formatNumber(n)} 项`,
|
||||
clearSelection: '取消选择',
|
||||
usage: (n: number) => `使用 ${formatNumber(n)} 次`,
|
||||
usageHint: '所在表被运行查询过的次数',
|
||||
usageHint: '所在表被运行查询过的次数,同一次运行中结果相同的重复查询只算一次',
|
||||
open: (where: string, table: string) => `打开「${table}」,查看${where}`,
|
||||
reasonsLabel: '排在这里的原因',
|
||||
// ---- 成批审阅
|
||||
@@ -3151,15 +3155,25 @@ export const CATALOG_REVIEW_TEXT = {
|
||||
failedAll: (reason: string) => `未能提交:${reason}`,
|
||||
// ---- 进度
|
||||
progressTitle: '审阅进度',
|
||||
/** 常用表:被运行用过、使用次数最多的前几张(最多 topN 张),n 是实际张数 */
|
||||
progressTop: (n: number) => `常用 ${formatNumber(n)} 张表`,
|
||||
progressTopHint: (n: number) => `使用次数最多的 ${formatNumber(n)} 张表里的项`,
|
||||
progressTopHint: (n: number, topN: number) => (n > 0
|
||||
? `被运行用过、使用次数最多的 ${formatNumber(n)} 张表里的项(最多取 ${formatNumber(topN)} 张)`
|
||||
: `被运行用过、使用次数最多的表里的项(最多取 ${formatNumber(topN)} 张)`),
|
||||
progressTopNone: '常用表',
|
||||
progressTopEmpty: '还没有运行用过的表',
|
||||
progressAll: '全部表',
|
||||
progressAllHint: '这个数据源全部表里的项',
|
||||
progressFacts: '影响数字的项',
|
||||
progressFactsHint: '有效记录条件、去重规则、业务日期、关联关系、业务主键、度量类型',
|
||||
confirmedPct: (pct: number | null) => (pct == null ? '还没有目录项' : `已确认 ${formatNumber(pct)}%`),
|
||||
itemsOf: (confirmed: number, total: number) => `${formatNumber(confirmed)} / ${formatNumber(total)} 项`,
|
||||
allDetail: (proposed: number, rejected: number) => `推断 ${formatNumber(proposed)} 项、已驳回 ${formatNumber(rejected)} 项`,
|
||||
factsDetail: (proposed: number, confirmed: number) => `待确认 ${formatNumber(proposed)} 项、已确认 ${formatNumber(confirmed)} 项`,
|
||||
progressRule: '已确认 ÷(已确认 + 待确认):已驳回的、有确证的(外键约束)不用再确认,不计入',
|
||||
/** 一行进度:已确认百分比和待确认几项;不知道待确认几项时只写已确认几项 */
|
||||
progressText: (pct: number | null, confirmed: number, proposed: number | null, total: number | null) => {
|
||||
if (total === 0) return '还没有目录项'
|
||||
if (proposed == null) return `已确认 ${formatNumber(confirmed)} 项`
|
||||
if (pct == null) return '没有待确认的项'
|
||||
return `已确认 ${formatNumber(pct)}% · 待确认 ${formatNumber(proposed)} 项`
|
||||
},
|
||||
progressFailed: '无法读取审阅进度',
|
||||
// ---- 没选中项时的右栏
|
||||
overviewTitle: '从左侧队列挑一项开始审阅',
|
||||
@@ -3178,8 +3192,10 @@ export const CATALOG_USAGE_TEXT = {
|
||||
show: '查看用过这张表的运行',
|
||||
hide: '收起',
|
||||
none: '还没有运行查询过这张表',
|
||||
/** 按运行算(一次运行查询了几次这张表都只算一次),和表头按查询算的「使用 N 次」不是一个数 */
|
||||
count: (shown: number, total: number) => (total > shown
|
||||
? `最近 ${formatNumber(shown)} 次,共 ${formatNumber(total)} 次` : `共 ${formatNumber(total)} 次`),
|
||||
? `共 ${formatNumber(total)} 次运行,显示最近 ${formatNumber(shown)} 次` : `共 ${formatNumber(total)} 次运行`),
|
||||
countHint: '按运行算:一次运行查询了几次这张表都只算一次;表头的使用次数按查询算,两个数可能不一样',
|
||||
error: '无法读取用过这张表的运行',
|
||||
retry: '重试',
|
||||
unnamed: '未保存的工作流',
|
||||
|
||||
@@ -10,15 +10,17 @@ import { errorMessage } from '../../lib/errors'
|
||||
import { CATALOG_REVIEW_TEXT as RT } from '../../lib/terms'
|
||||
import { StatusChip } from './parts'
|
||||
import {
|
||||
REVIEW_BATCH_MAX, REVIEW_KINDS, REVIEW_PAGE, applyResults, batchTargets, itemKey, percentOf, statusOf, syncFromDetail,
|
||||
REVIEW_BATCH_MAX, REVIEW_KINDS, REVIEW_PAGE, applyResults, batchTargets, confirmProgress, itemKey, reviewWhere, statusOf,
|
||||
syncFromDetail,
|
||||
} from './review'
|
||||
import type { ItemOutcome } from './review'
|
||||
|
||||
// ===========================================================================
|
||||
// 审阅队列(数据目录页的「审阅队列」视图,左栏):跨表列出推断项,顺序由服务端排好(所在表的使用次数 × 这一项对数字的
|
||||
// 影响 × 来源),每一项写为什么排在这里。可以按项(影响数字的项 / 名称和含义)和表筛选;多选后成批确认、驳回,审阅过的
|
||||
// 原地标出结果(冲突的标出来),选中可以撤销审阅;刷新队列后审阅过的才移除。顶上是审阅进度:常用 20 张表、全部表、
|
||||
// 影响数字的项各确认了多少。点开一项,右栏是所在表的详情(滚到那一项、标出来)和用过这张表的运行。
|
||||
// 原地标出结果(冲突的标出来),选中可以撤销审阅;刷新队列后审阅过的才移除。顶上是审阅进度:常用表(使用次数最多、
|
||||
// 被运行用过的前 20 张)、全部表、影响数字的项各确认了多少,三行同一个口径。点开一项,右栏是所在表的详情(滚到那一项、
|
||||
// 标出来)和用过这张表的运行。
|
||||
// ===========================================================================
|
||||
|
||||
export function ReviewQueue({ sourceId, rows, kind, table, activeKey, detailSync, onParams, onOpenItem, onChanged }: {
|
||||
@@ -313,7 +315,8 @@ const QueueRow = memo(function QueueRow({ item: i, itemKeyText, tableLabel, outc
|
||||
onOpen: (item: CatalogReviewItem) => void
|
||||
}) {
|
||||
const status = statusOf(i, outcome)
|
||||
const where = i.field_label || i.path
|
||||
// 位置按 path 说(「列 status 的码值」):服务端的 field_label 只是字段的叫法,列级的项只写它分不清是哪一列
|
||||
const where = reviewWhere(i)
|
||||
const tableText = tableLabel ? `${tableLabel}(${i.table_name})` : i.table_name
|
||||
const mono = i.field === 'valid_filter' || i.field === 'keys'
|
||||
return (
|
||||
@@ -327,7 +330,7 @@ const QueueRow = memo(function QueueRow({ item: i, itemKeyText, tableLabel, outc
|
||||
<button type="button" className="min-w-0 flex-1 px-2 py-2 text-left outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--accent)]"
|
||||
onClick={() => onOpen(i)} aria-current={active ? 'true' : undefined} title={RT.open(where, i.table_name)} data-review-open="">
|
||||
<span className="flex items-baseline gap-2">
|
||||
<span className="min-w-0 flex-1 truncate text-xs font-medium text-fg">{where}</span>
|
||||
<span className="min-w-0 flex-1 truncate text-xs font-medium text-fg" data-review-where="">{where}</span>
|
||||
<span className="tnum shrink-0 text-2xs text-faint" title={RT.usageHint}>{RT.usage(i.table_usage)}</span>
|
||||
</span>
|
||||
<span className="mt-0.5 block truncate text-2xs text-faint" title={tableText}>{tableText}</span>
|
||||
@@ -354,7 +357,11 @@ const QueueRow = memo(function QueueRow({ item: i, itemKeyText, tableLabel, outc
|
||||
)
|
||||
})
|
||||
|
||||
/** 审阅进度:常用 20 张表、全部表、影响数字的项。数字写在字里,色条只给眼睛看 */
|
||||
/**
|
||||
* 审阅进度:常用表、全部表、影响数字的项,三行同一个口径——已确认 ÷(已确认 + 待确认),已驳回的、有确证的不计入,
|
||||
* 审完了就是 100%。常用表只算被运行用过的(最多 top_n 张),标签写实际几张,一张都没有时写明还没有运行用过的表。
|
||||
* 数字写在字里,色条只给眼睛看
|
||||
*/
|
||||
function ProgressBlock({ progress, failed }: { progress: CatalogReviewProgress | null; failed: boolean }) {
|
||||
if (!progress) {
|
||||
return (
|
||||
@@ -364,33 +371,53 @@ function ProgressBlock({ progress, failed }: { progress: CatalogReviewProgress |
|
||||
)
|
||||
}
|
||||
const { top, all, facts } = progress
|
||||
const topPct = percentOf(top.items_confirmed, top.items_total)
|
||||
const allPct = percentOf(all.items_confirmed, all.items_total)
|
||||
const factsPct = percentOf(facts.confirmed, facts.proposed + facts.confirmed)
|
||||
const topTables = Math.max(0, top.tables ?? 0)
|
||||
const topLine = confirmProgress(top.items_confirmed, top.items_proposed)
|
||||
const allLine = confirmProgress(all.items_confirmed, all.items_proposed)
|
||||
const factsLine = confirmProgress(facts.confirmed, facts.proposed)
|
||||
return (
|
||||
<section className="space-y-1.5 border-b px-3 py-2.5" aria-label={RT.progressTitle} data-review-progress="">
|
||||
<ProgressLine label={RT.progressTop(progress.top_n)} hint={RT.progressTopHint(progress.top_n)} pct={topPct}
|
||||
text={`${RT.confirmedPct(topPct)} · ${RT.itemsOf(top.items_confirmed, top.items_total)}`} attr="top" />
|
||||
<ProgressLine label={RT.progressAll} pct={allPct}
|
||||
text={`${RT.confirmedPct(allPct)} · ${RT.allDetail(all.items_proposed, all.items_rejected)}`} attr="all" />
|
||||
<ProgressLine label={RT.progressFacts} hint={RT.progressFactsHint} pct={factsPct}
|
||||
text={RT.factsDetail(facts.proposed, facts.confirmed)} attr="facts" warn={facts.proposed > 0} />
|
||||
{topTables > 0
|
||||
? (
|
||||
<ProgressLine label={RT.progressTop(topTables)} hint={RT.progressTopHint(topTables, progress.top_n)} line={topLine}
|
||||
total={top.items_total} attr="top" />
|
||||
)
|
||||
: (
|
||||
<div className="flex items-baseline gap-2 text-2xs" data-review-progress-line="top" data-review-progress-top="" data-review-progress-none="">
|
||||
<span className="shrink-0 font-medium text-dim" title={RT.progressTopHint(0, progress.top_n)}>{RT.progressTopNone}</span>
|
||||
<span className="min-w-0 flex-1 truncate text-right text-faint">{RT.progressTopEmpty}</span>
|
||||
</div>
|
||||
)}
|
||||
<ProgressLine label={RT.progressAll} hint={RT.progressAllHint} line={allLine} total={all.items_total} attr="all" />
|
||||
<ProgressLine label={RT.progressFacts} hint={RT.progressFactsHint} line={factsLine} total={null} attr="facts"
|
||||
warn={facts.proposed > 0} />
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function ProgressLine({ label, hint, pct, text, attr, warn = false }: {
|
||||
label: string; hint?: string; pct: number | null; text: string; attr: string; warn?: boolean
|
||||
function ProgressLine({ label, hint, line, total, attr, warn = false }: {
|
||||
label: string
|
||||
hint: string
|
||||
line: ReturnType<typeof confirmProgress>
|
||||
/** 这一行一共几项(全部状态):0 时写还没有目录项;不知道时为 null */
|
||||
total: number | null
|
||||
attr: string
|
||||
warn?: boolean
|
||||
}) {
|
||||
const text = RT.progressText(line.pct, line.confirmed, line.proposed, total)
|
||||
return (
|
||||
<div className="text-2xs" data-review-progress-line={attr} {...{ [`data-review-progress-${attr}`]: '' }}>
|
||||
<div className="text-2xs" data-review-progress-line={attr} {...{ [`data-review-progress-${attr}`]: '' }}
|
||||
data-review-progress-pct={line.pct ?? ''}>
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className="shrink-0 font-medium text-dim" title={hint}>{label}</span>
|
||||
<span className={clsx('tnum min-w-0 flex-1 truncate text-right', warn ? 'text-[var(--st-waiting)]' : 'text-faint')} title={text}>{text}</span>
|
||||
</div>
|
||||
<div className="mt-0.5 h-1 overflow-hidden rounded-full bg-hover" aria-hidden>
|
||||
<div className="h-full rounded-full bg-[var(--st-done)]" style={{ width: `${pct ?? 0}%` }} />
|
||||
<span className="shrink-0 font-medium text-dim" title={`${hint}\n${RT.progressRule}`}>{label}</span>
|
||||
<span className={clsx('tnum min-w-0 flex-1 truncate text-right', warn ? 'text-[var(--st-waiting)]' : 'text-faint')}
|
||||
title={`${text}\n${RT.progressRule}`}>{text}</span>
|
||||
</div>
|
||||
{line.pct != null && (
|
||||
<div className="mt-0.5 h-1 overflow-hidden rounded-full bg-hover" aria-hidden>
|
||||
<div className="h-full rounded-full bg-[var(--st-done)]" style={{ width: `${line.pct}%` }} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ export function TableUsageList({ sourceId, table, usage, autoLoad = false }: {
|
||||
if (!data.runs.length) return <p className="rounded-lg border px-3 py-4 text-xs text-faint" data-table-usage="0">{T.none}</p>
|
||||
return (
|
||||
<div data-table-usage={data.total}>
|
||||
<p className="mb-1 text-2xs text-faint">{T.count(data.runs.length, data.total)}</p>
|
||||
<p className="mb-1 text-2xs text-faint" title={T.countHint} data-table-usage-count="">{T.count(data.runs.length, data.total)}</p>
|
||||
<ul className="divide-y divide-[var(--hairline)] rounded-lg border bg-panel">
|
||||
{data.runs.map((r) => {
|
||||
const name = r.workflow_name?.trim() || T.unnamed
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type {
|
||||
CatalogNotes, CatalogReviewAction, CatalogReviewBatchResult, CatalogReviewItem, CatalogReviewKind, CatalogStatus,
|
||||
} from '../../types'
|
||||
import { CATALOG_TEXT } from '../../lib/terms'
|
||||
import { patchTarget, patchWhere } from '../../run/catalogPatch'
|
||||
import { slotsOf } from './model'
|
||||
|
||||
// ===========================================================================
|
||||
@@ -48,6 +50,18 @@ export function writeReviewParams(p: URLSearchParams, s: Partial<ReviewParams>):
|
||||
/** 队列一项的键(表名里、路径里都可能有句点,中间用一个不会出现的字符隔开) */
|
||||
export const itemKey = (i: { table_name: string; path: string }) => `${i.table_name}\u0000${i.path}`
|
||||
|
||||
/**
|
||||
* 队列里一项的位置,说法同提案卡片:「列 status 的码值」「表的有效记录条件」「指向 parks 的关联关系」。服务端的
|
||||
* field_label 只是字段的叫法(「码值」),列级的项占了队列的大半,只写它分不清是哪一列,所以按 path 说;认不出的
|
||||
* 路径再用 column 加 field_label
|
||||
*/
|
||||
export function reviewWhere(i: Pick<CatalogReviewItem, 'path' | 'value' | 'field_label'> & { column?: string | null }): string {
|
||||
if (patchTarget(i.path)) return patchWhere({ path: i.path, before: i.value ?? null, after: i.value ?? null })
|
||||
const label = i.field_label?.trim()
|
||||
if (i.column && label) return CATALOG_TEXT.whereColumn(i.column, label)
|
||||
return label || i.path
|
||||
}
|
||||
|
||||
/** 一项审阅过之后的处境:成功(现在的状态、这张表写入后的版本)、冲突(这张表刚被改过)、没成功(服务端的原因) */
|
||||
export type ItemOutcome =
|
||||
| { kind: 'ok'; action: CatalogReviewAction; status: CatalogStatus }
|
||||
@@ -130,3 +144,16 @@ export function syncFromDetail(items: CatalogReviewItem[], table: string, notes:
|
||||
|
||||
/** 百分比(四舍五入到整数);分母为 0 时为 null */
|
||||
export const percentOf = (n: number, total: number): number | null => (total > 0 ? Math.round((n / total) * 100) : null)
|
||||
|
||||
/**
|
||||
* 审阅进度的一行(常用表、全部表、影响数字的项同一个口径):已确认 ÷(已确认 + 待确认)。已驳回的、有确证的(外键约束)
|
||||
* 不用再确认,不计入,审完了就是 100%。不知道待确认几项(服务端没给)时 pct 为 null,只写已确认几项
|
||||
*/
|
||||
export function confirmProgress(confirmed: number, proposed: number | null | undefined): {
|
||||
pct: number | null; confirmed: number; proposed: number | null
|
||||
} {
|
||||
const c = Math.max(0, confirmed || 0)
|
||||
if (typeof proposed !== 'number' || !Number.isFinite(proposed)) return { pct: null, confirmed: c, proposed: null }
|
||||
const p = Math.max(0, proposed)
|
||||
return { pct: percentOf(c, c + p), confirmed: c, proposed: p }
|
||||
}
|
||||
|
||||
+15
-5
@@ -3077,10 +3077,15 @@ export interface CatalogReviewItem {
|
||||
table_name: string
|
||||
/** 审阅接口的路径写法 */
|
||||
path: string
|
||||
/** 字段名(valid_filter、label……) */
|
||||
/** 字段名(valid_filter、label……;关联关系是 relation) */
|
||||
field: string
|
||||
/** 给人看的位置(服务端写好的,比如「列 status 的码值」) */
|
||||
/**
|
||||
* 字段的界面叫法(服务端写好的「中文名」「码值」「有效记录条件」「关联关系」),不含是哪一列:界面上的位置按 path
|
||||
* 说(「列 status 的码值」,review.ts 的 reviewWhere)
|
||||
*/
|
||||
field_label: string
|
||||
/** 列级的项是哪一列;表级的项、关联关系为 null。接口约定里没有写明,服务端给了才用 */
|
||||
column?: string | null
|
||||
value: unknown
|
||||
/** 值写成一行字(服务端写好的) */
|
||||
display: string
|
||||
@@ -3137,11 +3142,16 @@ export interface CatalogTableUsage {
|
||||
runs: CatalogUsageRun[]
|
||||
}
|
||||
|
||||
/** GET /catalog/review-progress:常用的前 top_n 张表、全部表、影响数字的项各审阅到哪儿 */
|
||||
/**
|
||||
* GET /catalog/review-progress:常用的前 top_n 张表、全部表、影响数字的项各审阅到哪儿。
|
||||
* 「常用表」只算被运行用过的:tables 可能比 top_n 少,没有运行时是 0。三行的进度都按「已确认 ÷(已确认 + 待确认)」算
|
||||
* (review.ts 的 confirmProgress):已驳回的、有确证的(外键约束)不用再确认,不计入
|
||||
*/
|
||||
export interface CatalogReviewProgress {
|
||||
top_n: number
|
||||
top: { tables: number; items_total: number; items_confirmed: number }
|
||||
all: { items_total: number; items_confirmed: number; items_proposed: number; items_rejected: number }
|
||||
/** items_proposed、items_verified:接口约定里没有写明,服务端给了才用(没有待确认数时只写已确认几项) */
|
||||
top: { tables: number; items_total: number; items_confirmed: number; items_proposed?: number; items_verified?: number }
|
||||
all: { items_total: number; items_confirmed: number; items_proposed: number; items_rejected: number; items_verified?: number }
|
||||
facts: { proposed: number; confirmed: number }
|
||||
}
|
||||
|
||||
|
||||
@@ -411,33 +411,40 @@ const tableVersion = (state, src, name) => state.tables[src].find((t) => t.name
|
||||
// ---------------------------------------------------------------------------
|
||||
// 审阅队列的夹具:服务端排好的顺序(常用表里影响数字的推断在前)。kind 只是假服务端按筛选挑项用,不回给页面
|
||||
// ---------------------------------------------------------------------------
|
||||
// 形状照服务端(data/catalog_review.py):field_label 只是字段的叫法(「中文名」「度量类型」),列级的项另给 column;
|
||||
// reasons 的说法同服务端(「常用表(使用 42 次)」「影响数字的项(有效记录条件)」)
|
||||
const qItem = (table_name, path, field, field_label, value, display, kind, priority, reasons, table_usage, extra = {}) => ({
|
||||
table_name, path, field, field_label, value, display, source: 'llm', status: 'proposed', note: null, priority, reasons, table_usage, kind, ...extra,
|
||||
table_name, path, field, field_label, column: path.startsWith('columns.') ? path.slice(8, path.lastIndexOf('.')) : null,
|
||||
value, display, source: 'llm', status: 'proposed', note: null, priority, reasons, table_usage, kind, ...extra,
|
||||
})
|
||||
const MEMBER_REL = { columns: ['member_id'], to_table: 'members', to_columns: ['id'], cardinality: 'many_to_one', coverage: 0.98 }
|
||||
function queueOf() {
|
||||
const list = [
|
||||
qItem('visits', 'valid_filter', 'valid_filter', '有效记录条件', 'status NOT IN (9)', 'status NOT IN (9)', 'facts', 9.6,
|
||||
['常用表(被 42 次运行用到)', '影响数字的项(有效行过滤)', '模型起草,未确认'], 42, { note: FILTER_NOTE }),
|
||||
['常用表(使用 42 次)', '影响数字的项(有效记录条件)', '模型起草,未确认'], 42, { note: FILTER_NOTE }),
|
||||
qItem('visits', 'dedup', 'dedup', '去重规则', '按 ticket_no 去重', '按 ticket_no 去重', 'facts', 9.5,
|
||||
['常用表(被 42 次运行用到)', '影响数字的项(去重规则)', '模型起草,未确认'], 42, { note: DEDUP_NOTE }),
|
||||
qItem('orders', 'kind', 'kind', '表类型', 'fact', '明细表', 'names', 6.2, ['常用表(被 30 次运行用到)', '模型起草,未确认'], 30),
|
||||
qItem('visits', 'columns.ticket_no.measure', 'measure', '列 ticket_no 的度量类型', 'identifier', '标识', 'facts', 5.1,
|
||||
['常用表(被 42 次运行用到)', '影响数字的项(度量类型)', '命名推断,未确认'], 42, { source: 'name' }),
|
||||
['常用表(使用 42 次)', '影响数字的项(去重规则)', '模型起草,未确认'], 42, { note: DEDUP_NOTE }),
|
||||
qItem('orders', 'kind', 'kind', '表类型', 'fact', '明细表', 'names', 6.2, ['常用表(使用 30 次)', '帮助理解取值的项(表类型)', '模型起草,未确认'], 30),
|
||||
qItem('orders', 'relations.r_member', 'relation', '关联关系', MEMBER_REL, 'member_id → members.id(多对一,覆盖率 98%)', 'facts', 5.5,
|
||||
['常用表(使用 30 次)', '影响数字的项(关联关系)', '命名推断,未确认'], 30, { source: 'name' }),
|
||||
qItem('visits', 'columns.ticket_no.measure', 'measure', '度量类型', 'identifier', '标识', 'facts', 5.1,
|
||||
['常用表(使用 42 次)', '影响计数和汇总的项(度量类型)', '命名推断,未确认'], 42, { source: 'name' }),
|
||||
qItem('visits', 'grain', 'grain', '粒度', '一张门票的一次入园', '一张门票的一次入园', 'names', 4.4,
|
||||
['常用表(被 42 次运行用到)', '模型起草,未确认'], 42),
|
||||
['常用表(使用 42 次)', '帮助理解取值的项(粒度)', '模型起草,未确认'], 42),
|
||||
]
|
||||
// 余下的:促销属性表的列中文名(使用次数少,排在后面)。总数 57,一页 50
|
||||
for (let i = 1; i <= 52; i++) {
|
||||
for (let i = 1; i <= 51; i++) {
|
||||
const c = `attr_${String(i).padStart(3, '0')}`
|
||||
list.push(qItem('promo_attrs', `columns.${c}.label`, 'label', `列 ${c} 的中文名`, `属性 ${i}`, `属性 ${i}`, 'names', 1 - i / 100,
|
||||
['模型起草,未确认'], 6))
|
||||
list.push(qItem('promo_attrs', `columns.${c}.label`, 'label', '中文名', `属性 ${i}`, `属性 ${i}`, 'names', 1 - i / 100,
|
||||
['使用 6 次', '名称和说明(中文名)', '模型起草,未确认'], 6))
|
||||
}
|
||||
return list
|
||||
}
|
||||
// 形状照服务端:top 只算被运行用过的表(5 张,不到 20),另带 items_proposed、items_verified
|
||||
const PROGRESS = {
|
||||
top_n: 20,
|
||||
top: { tables: 5, items_total: 286, items_confirmed: 120 },
|
||||
all: { items_total: 1460, items_confirmed: 262, items_proposed: 1234, items_rejected: 12 },
|
||||
top: { tables: 5, items_total: 286, items_confirmed: 120, items_proposed: 150, items_verified: 10 },
|
||||
all: { items_total: 1460, items_confirmed: 262, items_proposed: 1180, items_rejected: 12, items_verified: 6 },
|
||||
facts: { proposed: 56, confirmed: 12 },
|
||||
}
|
||||
const USAGE_RUNS = [
|
||||
@@ -458,11 +465,15 @@ const IMPORT_FILE = {
|
||||
legacy_tickets: { version: 1, notes: { label: item('旧票', 'human') } },
|
||||
},
|
||||
}
|
||||
// 形状照服务端(data/catalog_transfer.py):matched_as 总有值(对不上的表只在 unmatched_tables 里),每张表另带 version、
|
||||
// error(预览里也带),冲突另带 file_status、target_status;界面只在导入的结果里写版本
|
||||
const IMPORT_PREVIEW = {
|
||||
tables: [
|
||||
{ table_name: 'visits', matched_as: 'visits', add: 1, update: 0, keep: 4, skipped: [{ path: 'columns.gate_code.label', why: '表结构里没有列 gate_code' }],
|
||||
conflicts: [{ path: 'label', label: '表的中文名', file_value: '入园流水', target_value: '入园记录', why: '这里已确认,以这里为准' }] },
|
||||
{ table_name: 'ORDERS', matched_as: 'orders', add: 0, update: 1, keep: 1, conflicts: [], skipped: [] },
|
||||
{ table_name: 'visits', matched_as: 'visits', add: 1, update: 0, keep: 4, version: 2, error: null,
|
||||
skipped: [{ path: 'columns.gate_code.label', why: '表结构里没有列 gate_code' }],
|
||||
conflicts: [{ path: 'label', label: '表的中文名', file_value: '入园流水', target_value: '入园记录', why: '这里已确认,以这里为准',
|
||||
file_status: 'confirmed', target_status: 'confirmed' }] },
|
||||
{ table_name: 'ORDERS', matched_as: 'orders', add: 0, update: 1, keep: 1, version: 2, error: null, conflicts: [], skipped: [] },
|
||||
],
|
||||
unmatched_tables: ['legacy_tickets'],
|
||||
totals: { tables: 2, add: 1, update: 1, keep: 5, conflicts: 1, skipped: 1 },
|
||||
@@ -577,6 +588,17 @@ await section('纯函数 · 对话怎么截、待保存的修改怎么累积、
|
||||
dedup: { value: 'y', source: 'llm', status: 'rejected' } }, 4, res.outcomes, res.versions)
|
||||
out.sync = { grain: synced.outcomes.get(rv.itemKey(items[0])), dedup: synced.outcomes.get(rv.itemKey(items[1]))?.status, v: synced.versions.get('t1') }
|
||||
out.pct = [rv.percentOf(120, 286), rv.percentOf(0, 0)]
|
||||
// 审阅队列一项的位置:按 path 说到列(服务端的 field_label 只是字段的叫法)
|
||||
out.where = [
|
||||
rv.reviewWhere({ path: 'columns.status.codes', field_label: '码值', column: 'status', value: { 1: '有效' } }),
|
||||
rv.reviewWhere({ path: 'valid_filter', field_label: '有效记录条件', column: null, value: 'status <> 9' }),
|
||||
rv.reviewWhere({ path: 'relations.r1', field_label: '关联关系', column: null, value: { columns: ['park_id'], to_table: 'parks', to_columns: ['id'] } }),
|
||||
rv.reviewWhere({ path: 'columns.a.b.label', field_label: '中文名', column: 'a.b', value: '甲' }),
|
||||
rv.reviewWhere({ path: 'columns.x.weird', field_label: '怪字段', column: 'x', value: 1 }),
|
||||
rv.reviewWhere({ path: 'weird', field_label: '怪字段', value: 1 }),
|
||||
]
|
||||
// 审阅进度:三行同一个口径——已确认 ÷(已确认 + 待确认);不知道待确认几项时不算百分比
|
||||
out.progress = [rv.confirmProgress(120, 150), rv.confirmProgress(30, 0), rv.confirmProgress(0, 0), rv.confirmProgress(5, undefined)]
|
||||
return out
|
||||
})
|
||||
// 51 条取后 40 条,打头的那条是助手的回答,去掉:39 条
|
||||
@@ -602,6 +624,12 @@ await section('纯函数 · 对话怎么截、待保存的修改怎么累积、
|
||||
check('表详情改过之后:回到推断的记成撤销审阅,驳回的记成已驳回,版本换成最新的', r.sync.grain?.action === 'reset' && r.sync.dedup === 'rejected' && r.sync.v === 4,
|
||||
JSON.stringify(r.sync))
|
||||
check('进度的百分比四舍五入,分母为 0 时不写', r.pct[0] === 42 && r.pct[1] === null, JSON.stringify(r.pct))
|
||||
check('队列一项的位置写到列:列 status 的码值、表的有效记录条件、指向 parks 的关联关系;列名里有句点、认不出的路径也说得通',
|
||||
JSON.stringify(r.where) === JSON.stringify(['列 status 的码值', '表的有效记录条件', '指向 parks 的关联关系', '列 a.b 的中文名', '列 x 的怪字段', '怪字段']),
|
||||
JSON.stringify(r.where))
|
||||
check('审阅进度三行同一个口径:已确认 ÷(已确认 + 待确认),审完是 100%,没有可确认的、不知道待确认几项时不算',
|
||||
JSON.stringify(r.progress.map((x) => x.pct)) === JSON.stringify([44, 100, null, null]) && r.progress[3].proposed === null,
|
||||
JSON.stringify(r.progress))
|
||||
check('没有运行时报错', errors.length === 0, errors[0] ?? '')
|
||||
await close()
|
||||
})
|
||||
@@ -912,22 +940,51 @@ await section('审阅队列 · 视图、顺序与原因、筛选、进度', asyn
|
||||
check('……照服务端排好的顺序列(常用表里影响数字的在前)', keys.slice(0, 3).join(' | ') === 'visits::valid_filter | visits::dedup | orders::kind', keys.slice(0, 3).join(' | '))
|
||||
const first = queue.locator('[data-review-item="visits::valid_filter"]')
|
||||
const firstText = await textOf(first)
|
||||
check('第一项:写位置、所在表的中文名、值、来源和状态、所在表的使用次数', firstText.includes('有效记录条件') && firstText.includes('入园记录')
|
||||
check('第一项:写位置、所在表的中文名、值、来源和状态、所在表的使用次数', firstText.includes('表的有效记录条件') && firstText.includes('入园记录')
|
||||
&& firstText.includes('status NOT IN (9)') && firstText.includes('推断') && firstText.includes('模型起草') && firstText.includes('使用 42 次'), firstText)
|
||||
check('……写为什么排在这里', (await textOf(first.locator('[data-review-reasons]'))).includes('影响数字的项(有效行过滤)'))
|
||||
check('……写为什么排在这里(服务端的原话)', (await textOf(first.locator('[data-review-reasons]'))).includes('影响数字的项(有效记录条件)'))
|
||||
check('……有效记录条件写剖析核对的排除比例', (await textOf(first.locator('[data-review-note]'))).includes(FILTER_NOTE))
|
||||
check('……一共 57 项,还能再载入 7 项', (await textOf(queue.locator('[data-review-total]'))).includes('57 项待确认') && (await textOf(queue.locator('[data-review-more]'))).includes('再载入 7 项'))
|
||||
// 进度
|
||||
// 位置:服务端的 field_label 只是字段的叫法(「度量类型」「中文名」),列级的项要写出是哪一列,不然成批确认时分不清
|
||||
const whereOf = (k) => textOf(queue.locator(`[data-review-item="${k}"] [data-review-where]`))
|
||||
const box = (k) => queue.locator(`[data-review-item="${k}"] [data-review-select]`)
|
||||
check('列级的项写明是哪一列:「列 ticket_no 的度量类型」「列 attr_001 的中文名」', (await whereOf('visits::columns.ticket_no.measure')) === '列 ticket_no 的度量类型'
|
||||
&& (await whereOf('promo_attrs::columns.attr_001.label')) === '列 attr_001 的中文名',
|
||||
`${await whereOf('visits::columns.ticket_no.measure')} · ${await whereOf('promo_attrs::columns.attr_001.label')}`)
|
||||
check('……表级的项写「表的……」,关联关系写指向哪张表', (await whereOf('visits::valid_filter')) === '表的有效记录条件'
|
||||
&& (await whereOf('orders::relations.r_member')) === '指向 members 的关联关系', `${await whereOf('visits::valid_filter')} · ${await whereOf('orders::relations.r_member')}`)
|
||||
check('……复选框的读屏名也写到列:「选择:visits 列 ticket_no 的度量类型」',
|
||||
(await box('visits::columns.ticket_no.measure').getAttribute('aria-label')) === '选择:visits 列 ticket_no 的度量类型'
|
||||
&& (await box('promo_attrs::columns.attr_002.label').getAttribute('aria-label')) === '选择:promo_attrs 列 attr_002 的中文名',
|
||||
await box('visits::columns.ticket_no.measure').getAttribute('aria-label'))
|
||||
const promoWhere = await queue.locator('[data-review-item^="promo_attrs::"] [data-review-where]').evaluateAll((els) => els.map((e) => e.textContent))
|
||||
check('……促销属性表的几十项中文名各写各的列,没有两行一样', promoWhere.length === 44 && new Set(promoWhere).size === promoWhere.length, `${promoWhere.length} · ${new Set(promoWhere).size}`)
|
||||
// 进度:三行同一个口径(已确认 ÷(已确认 + 待确认)),常用表写实际几张
|
||||
const prog = queue.locator('[data-review-progress]')
|
||||
check('进度:常用 20 张表已确认 42%(120 / 286 项)', (await textOf(prog.locator('[data-review-progress-top]'))).includes('常用 20 张表')
|
||||
&& (await textOf(prog.locator('[data-review-progress-top]'))).includes('已确认 42%') && (await textOf(prog.locator('[data-review-progress-top]'))).includes('120 / 286 项'),
|
||||
await textOf(prog))
|
||||
check('……全部表已确认 18%,影响数字的项待确认 56、已确认 12', (await textOf(prog.locator('[data-review-progress-all]'))).includes('已确认 18%')
|
||||
&& (await textOf(prog.locator('[data-review-progress-facts]'))).includes('待确认 56 项、已确认 12 项'), await textOf(prog))
|
||||
const line = (k) => textOf(prog.locator(`[data-review-progress-${k}]`))
|
||||
const pct = (k) => prog.locator(`[data-review-progress-${k}]`).getAttribute('data-review-progress-pct')
|
||||
check('进度:常用表写实际几张(被运行用过的 5 张,不写死 20 张),已确认 44%、待确认 150 项', (await line('top')).includes('常用 5 张表')
|
||||
&& !(await line('top')).includes('20 张') && (await line('top')).includes('已确认 44% · 待确认 150 项') && (await pct('top')) === '44', await line('top'))
|
||||
check('……全部表、影响数字的项同一个口径:已确认 18%、待确认 1,180 项;已确认 18%、待确认 56 项', (await line('all')).includes('已确认 18% · 待确认 1,180 项')
|
||||
&& (await line('facts')).includes('已确认 18% · 待确认 56 项') && (await pct('all')) === '18' && (await pct('facts')) === '18', `${await line('all')} · ${await line('facts')}`)
|
||||
check('……悬停写明口径:已驳回的、有确证的不计入', ((await prog.locator('[data-review-progress-all] span').last().getAttribute('title')) ?? '').includes('已驳回的、有确证的(外键约束)不用再确认'))
|
||||
// 审完了(待确认 0 项)三行都是 100%:已驳回的、有确证的项不会让「全部表」永远到不了 100%;没有运行用过的表时写明
|
||||
state.progress = {
|
||||
top_n: 20, top: { tables: 0, items_total: 0, items_confirmed: 0, items_proposed: 0, items_verified: 0 },
|
||||
all: { items_total: 40, items_confirmed: 30, items_proposed: 0, items_rejected: 6, items_verified: 4 }, facts: { proposed: 0, confirmed: 9 },
|
||||
}
|
||||
await queue.locator('[data-review-refresh]').click()
|
||||
check('审完了:全部表、影响数字的项都是已确认 100%(6 项驳回、4 项有确证不计入)', await until(async () => (await pct('all')) === '100')
|
||||
&& (await line('all')).includes('已确认 100% · 待确认 0 项') && (await pct('facts')) === '100', `${await line('all')} · ${await line('facts')}`)
|
||||
check('……没有运行用过的表:常用表那一行写「还没有运行用过的表」,不写「常用 20 张表」', (await line('top')).includes('还没有运行用过的表')
|
||||
&& !(await line('top')).includes('20 张') && !(await line('top')).includes('还没有目录项'), await line('top'))
|
||||
state.progress = null
|
||||
await queue.locator('[data-review-refresh]').click()
|
||||
await until(async () => (await pct('top')) === '44')
|
||||
// 筛选
|
||||
await queue.locator('[data-review-kind="facts"]').click()
|
||||
check('按项筛选「影响数字的项」:请求带 kind=facts,地址也记着', await until(async () => lastSent(sent, /review-queue$/)?.query?.kind === 'facts')
|
||||
&& new URL(page.url()).searchParams.get('kind') === 'facts' && await until(async () => (await queue.locator('[data-review-item]').count()) === 3))
|
||||
&& new URL(page.url()).searchParams.get('kind') === 'facts' && await until(async () => (await queue.locator('[data-review-item]').count()) === 4))
|
||||
await queue.locator('[data-review-table]').selectOption('visits')
|
||||
check('按表筛选:请求带 table=visits,地址记成 rtable', await until(async () => lastSent(sent, /review-queue$/)?.query?.table === 'visits')
|
||||
&& new URL(page.url()).searchParams.get('rtable') === 'visits')
|
||||
@@ -1015,6 +1072,10 @@ await section('审阅队列 · 成批确认、冲突、撤销审阅、打开所
|
||||
&& (await usage.locator('[data-usage-run] a').first().getAttribute('href')) === '/runs/run-check-0003'
|
||||
&& (await textOf(usage.locator('[data-usage-run]').first())).includes('每日入园统计'))
|
||||
check('……请求带 limit=20', lastSent(sent, /\/visits\/usage$/)?.query?.limit === '20')
|
||||
// 两个数口径不同:表头「使用 42 次」按查询算,这里按运行算(3 次运行),各写各的单位,悬停说明为什么不一样
|
||||
const usageCount = usage.locator('[data-table-usage-count]')
|
||||
check('……写「共 3 次运行」(按运行算),悬停说明和表头的使用次数(按查询算)不是一个数', (await textOf(usageCount)) === '共 3 次运行'
|
||||
&& ((await usageCount.getAttribute('title')) ?? '').includes('表头的使用次数按查询算'), await textOf(usageCount))
|
||||
// 在详情里单项确认:队列里那一项跟着标已确认
|
||||
await page.locator('[data-item-mark="dedup"]').first().click()
|
||||
await page.locator('[data-item-panel] [data-review-action="confirm"]').click()
|
||||
@@ -1086,6 +1147,8 @@ await section('导出导入 · 导出文件、导入先预览再写入', async (
|
||||
&& (await textOf(dlg.locator('[data-import-unmatched]'))).includes('legacy_tickets'))
|
||||
check('……起草要求:这里已有,保留不覆盖', (await textOf(dlg.locator('[data-import-guidance-action]'))).includes('保留这个数据源现有的'))
|
||||
check('……预览不写库', count(sent, /catalog\/import$/) === 0)
|
||||
check('……预览里不写版本(服务端的预览也带 version,只有导入的结果才写第几版)', !(await textOf(dlg.locator('[data-import-table="visits"]'))).includes('第 2 版'),
|
||||
await textOf(dlg.locator('[data-import-table="visits"]')))
|
||||
await shot(page, 'catalog-import-preview')
|
||||
const lists = count(sent, /^GET \/datasources\/check-loop-scenic\/catalog$/)
|
||||
await page.locator('[data-import-confirm]').click()
|
||||
|
||||
Reference in New Issue
Block a user