数据目录 F3 / B4 界面:目录页顶部说明「这个数据源共有 N 张表,只探查了前 200 张」,写明为什么只取了这些、其余的助手也看不到,下一步回到数据源卡片换 schema 或缩小范围后重新探查;助手「参考了哪些表」那一行截断了的库写「共 N 张,只探查了 200 张」;补检查脚本的断言

This commit is contained in:
ThearchyHelios
2026-10-04 01:25:19 +08:00
parent 3ece89872b
commit 9c02833544
6 changed files with 111 additions and 9 deletions
+15
View File
@@ -2260,6 +2260,21 @@ export const CATALOG_DRIFT_TEXT = {
possibleNone: (source: string) => `「${source}」发布时还没有目录`,
}
/**
* 表结构只探查了一部分:探查结构时每个数据源最多取 200 张表(服务端 introspect._MAX_TABLES),多出来的表不在
* 表结构里,数据目录列不出,助手也看不到它们的字段。数据目录页顶部和助手「参考了哪些表」那一行都要说出来。
*/
export const SCHEMA_PARTIAL_TEXT = {
/** 数据目录页顶部:「这个数据源共有 205 张表,只探查了前 200 张」 */
title: (total: number, explored: number) =>
`这个数据源共有 ${formatNumber(total)} 张表,只探查了前 ${formatNumber(explored)} 张`,
body: (rest: number) => `探查结构时每个数据源最多取 200 张表(按数据库列出的顺序):生产库动辄上千张,全部取回既慢也看不完。`
+ `其余 ${formatNumber(rest)} 张不在数据目录中,助手也看不到它们的字段。`,
next: '所需的表不在清单中时,请回到数据源卡片换一个 schema,或改用只能看到所需表的数据库账号缩小范围,然后重新「探查结构」。',
/** 助手「参考了哪些表」那一行:「共 205 张,只探查了 200 张」 */
context: (total: number, explored: number) => `共 ${formatNumber(total)} 张,只探查了 ${formatNumber(explored)} 张`,
}
// ===========================================================================
// 数据剖析(数据源设置里的开关和预算;数据目录页的「数据剖析」)
//
+28 -1
View File
@@ -7,7 +7,7 @@ import { ApiError, api } from '../../api/client'
import type { CatalogDetail, CatalogList, CatalogTableRow } from '../../types'
import { EmptyState, ErrorState, PageHeader, Skeleton, Spinner, toast, useTicker } from '../../components/ui'
import { useCatalog, useDatasources, useOnReconnect } from '../../store/catalog'
import { CATALOG_TEXT as CT, PROFILE_STOP_LABEL, PROFILE_TEXT as PT } from '../../lib/terms'
import { CATALOG_TEXT as CT, PROFILE_STOP_LABEL, PROFILE_TEXT as PT, SCHEMA_PARTIAL_TEXT as SPT } from '../../lib/terms'
import { StatusLegend, SystemNotesNotice } from './parts'
import { DraftDialog } from './DraftDialog'
import { ProfileDialog } from './ProfileDialog'
@@ -308,6 +308,10 @@ export function CatalogPage() {
</div>
)}
/>
{hasTables && data?.schema_truncated && (
<SchemaPartialNotice total={data.schema_total ?? 0} explored={rows.filter((r) => r.in_schema).length}
onGoSource={() => navigate(back)} />
)}
{hasTables && usage.used > 0 && !empty && <UsageSummary {...usage} active={filter} onFocus={focusUsed} />}
{body}
{drafting && data && (
@@ -347,6 +351,29 @@ export function CatalogPage() {
)
}
/**
* 表结构只探查了一部分(每个数据源最多取 200 张表):没探查到的表不在清单里,助手也看不到。顶部常驻一条说明,
* 写清一共几张、探查了几张、为什么只取了这些,以及下一步——回到数据源卡片换 schema 或缩小范围后重新探查
*/
function SchemaPartialNotice({ total, explored, onGoSource }: { total: number; explored: number; onGoSource: () => void }) {
const shownTotal = Math.max(total, explored)
return (
<div className="flex shrink-0 items-start gap-2 border-b px-4 py-2 text-xs"
style={{ background: 'color-mix(in srgb, var(--st-waiting) 6%, var(--bg-panel))' }}
data-catalog-schema-partial={`${shownTotal},${explored}`}>
<Database size={12} className="mt-0.5 shrink-0" style={{ color: 'var(--st-waiting)' }} aria-hidden />
<div className="min-w-0 flex-1 leading-relaxed">
<p className="font-medium">{SPT.title(shownTotal, explored)}</p>
<p className="text-2xs text-dim">{SPT.body(Math.max(shownTotal - explored, 0))}</p>
<p className="text-2xs text-dim">{SPT.next}</p>
</div>
<button type="button" className="btn btn-sm shrink-0" onClick={onGoSource} data-catalog-schema-partial-go="">
{CT.back}
</button>
</div>
)
}
/**
* 「用到但没确认」:运行中查询过的表里还有几张有推断项、几张没有目录。这些表的推断项会被助手当真用,最该先审。
* 两段各是一个按钮,点了切到清单的对应筛选、按使用次数排;都审完了只写一句,不给按钮
+17 -7
View File
@@ -2,8 +2,8 @@ import type { CatalogDriftTable, RunEvent, SqlCheckItem, TeamMember, TeamRound,
import type { NodeState, RunPhase } from './trace'
import { formatDuration, formatNumber } from '../lib/format'
import {
CATALOG_DRIFT_TEXT, CATALOG_PATCH_TEXT, JUDGE_TEXT, MERGE_TEXT, SQL_CHECK_TEXT, TYPE_LABEL, claimTally, evidenceTally,
issuanceLabel, nodeTypeLabel,
CATALOG_DRIFT_TEXT, CATALOG_PATCH_TEXT, JUDGE_TEXT, MERGE_TEXT, SCHEMA_PARTIAL_TEXT, SQL_CHECK_TEXT, TYPE_LABEL, claimTally,
evidenceTally, issuanceLabel, nodeTypeLabel,
} from '../lib/terms'
import { catalogPatchOf, patchValueText, patchWhere } from './catalogPatch'
import { claimCountsOf, isJudged, statsTally } from '../lib/evidence'
@@ -2605,8 +2605,10 @@ export interface CopilotContextSource {
tables: string[]
/** model:按需求挑的;all:库不大,全部表都带字段;fallback:没能挑出来,只给了表名 */
selectedBy: 'model' | 'all' | 'fallback'
/** 这个库一共几张表 */
/** 这个库一共几张表(数据库里的真实总数) */
total?: number
/** 探查结构截断了时,探查到几张(服务端只在截断时给):模型只看得到这几张 */
explored?: number
/** 没能挑出来的原因 */
reason?: string
}
@@ -2620,26 +2622,34 @@ function contextSources(op: CopilotOp): CopilotContextSource[] {
.filter((x): x is Record<string, any> => !!x && typeof x === 'object' && typeof (x as any).source === 'string')
.map((x) => {
const total = num(x.total)
const explored = num(x.explored)
return {
source: x.source,
tables: Array.isArray(x.tables) ? x.tables.map(String) : [],
selectedBy: x.selected_by === 'model' || x.selected_by === 'fallback' ? x.selected_by : 'all',
...(total != null ? { total } : {}),
...(total != null && explored != null && explored < total ? { explored } : {}),
...(typeof x.reason === 'string' && x.reason ? { reason: x.reason } : {}),
}
})
}
/** 一个库在展开区里的那一行:怎么来的 + 表名 */
/**
* 一个库在展开区里的那一行:怎么来的 + 表名。探查结构截断了的库写明「共 205 张,只探查了 200 张」:
* 模型只看得到探查到的那些,用户说的表不在里面时,先得知道是没探查到,不是库里没有
*/
function contextLine(g: CopilotContextSource): string {
const names = g.tables.join('、')
const of = g.total != null ? `共 ${g.total} 张表` : ''
const partial = g.total != null && g.explored != null ? SCHEMA_PARTIAL_TEXT.context(g.total, g.explored) : ''
const of = partial || (g.total != null ? `共 ${g.total} 张表` : '')
switch (g.selectedBy) {
case 'all':
return `「${g.source}」全部 ${g.tables.length} 张表:${names}`
return `「${g.source}」全部 ${g.tables.length} 张表${partial ? `(${partial})` : ''}:${names}`
case 'model':
return g.tables.length
? `「${g.source}」按需求${g.total != null ? `从 ${g.total} 张表中` : ''}挑出 ${g.tables.length} 张:${names}`
? partial
? `「${g.source}」${partial},按需求挑出 ${g.tables.length} 张:${names}`
: `「${g.source}」按需求${g.total != null ? `从 ${g.total} 张表中` : ''}挑出 ${g.tables.length} 张:${names}`
: `「${g.source}」${of ? `${of},` : ''}未挑中与需求相关的表,已提供全部表名`
default:
return `「${g.source}」${of ? `${of},` : ''}${CONTEXT_FALLBACK_TEXT}`
+4
View File
@@ -2671,6 +2671,10 @@ export interface CatalogList {
system_notes: boolean
/** 没有表结构时的原因(「尚未探查结构」「结构探查失败:…」);有表结构时为 null */
schema_note: string | null
/** 探查结构截断了(每个数据源最多取 200 张表):没探查到的表不在清单里,助手也看不到。老后端没有这一项 */
schema_truncated?: boolean
/** 数据库里一共几张表(含视图);没截断时等于探查到的张数 */
schema_total?: number
}
/** 表结构里的一列 */
+37 -1
View File
@@ -347,7 +347,11 @@ function listOf(state, src) {
const all = state.tables[src]
const rows = all.filter((t) => !t.missing).sort((a, b) => b.usage - a.usage).map(rowOf)
rows.push(...all.filter((t) => t.missing).map(rowOf))
return { tables: rows, system_notes: isUpload(src), schema_note: all.length ? null : '尚未探查结构' }
// 探查结构截断了的源(state.schemaTotal 里给了数据库里的总数):清单只有探查到的那部分
const explored = all.filter((t) => !t.missing).length
const total = state.schemaTotal?.[src] ?? explored
return { tables: rows, system_notes: isUpload(src), schema_note: all.length ? null : '尚未探查结构',
schema_truncated: total > explored, schema_total: total }
}
const detailOf = (src, t) => ({
table_name: t.name, in_schema: !t.missing, notes: t.notes ?? {}, version: t.version, updated_at: t.updated_at, updated_by: t.updated_by,
@@ -1186,6 +1190,38 @@ const profilePhase = (page) => profileBox(page).getAttribute('data-profile-dialo
/** 报告里不该露的机读码:停止原因、跳过原因、状态、基数的英文值 */
const PROFILE_CODE_RE = /\b(?:budget|deadline|failed|masked|too_large|high_cardinality|no_data|unsupported|verified|proposed|many_to_one|row_estimate)\b/
await section('数据目录 · 只探查了一部分表', async () => {
// 探查结构每个数据源最多取 200 张表:多出来的不在清单里,助手也看不到。顶部要说出来,并给出下一步
const state = freshState()
const explored = state.tables[S1].filter((t) => !t.missing).length
const total = explored + 57
state.schemaTotal = { [S1]: total }
const { page, sent, errors, natives, close } = await open(`/data/catalog/${S1}`, { handlers: catalogHandlers(state) })
const bar = page.locator('[data-catalog-schema-partial]')
check('顶部有一条说明', await bar.waitFor({ timeout: 3000 }).then(() => true, () => false))
check('……数字:一共几张、探查了几张', (await bar.getAttribute('data-catalog-schema-partial')) === `${total},${explored}`,
await bar.getAttribute('data-catalog-schema-partial'))
const text = await bar.innerText().catch(() => '')
check('……写成「共有 N 张表,只探查了前 M 张」', text.includes(`这个数据源共有 ${total} 张表,只探查了前 ${explored} 张`), text)
check('……说明为什么只取了这些、其余的助手也看不到', text.includes('最多取 200 张表') && text.includes('其余 57 张不在数据目录中')
&& text.includes('助手也看不到'), text.replace(/\s+/g, ' '))
check('……给出下一步:换 schema 或缩小范围后重新探查', text.includes('换一个 schema') && text.includes('缩小范围')
&& text.includes('探查结构'), text.replace(/\s+/g, ' '))
await shot(page, 'catalog-schema-partial')
await cleanCopy(page, '只探查了一部分表')
await bar.locator('[data-catalog-schema-partial-go]').click()
await page.waitForURL('**/data/databases', { timeout: 5000 }).catch(() => {})
check('……按钮回到数据源卡片', new URL(page.url()).pathname === '/data/databases', page.url())
// 没截断的库不出这一条
await goto(page, `/data/catalog/${S4}`)
await page.locator('[data-catalog-overview]').waitFor()
check('没截断的库不出这一条', await page.locator('[data-catalog-schema-partial]').count() === 0)
check('只读、不发写请求', writes(sent, /./).length === 0)
check('没有原生对话框', natives.length === 0, natives.join(' | '))
check('没有运行时报错', errors.length === 0, errors.join(' | '))
await close()
})
await section('数据目录 · 数据剖析设置', async () => {
const state = freshState()
const replies = { patch: [] }
+10
View File
@@ -685,6 +685,16 @@ await section('Copilot:这一轮参考了哪些表(context 操作)', async
{ source: 'b', tables: [], selected_by: 'model', total: 70 }] })
check('挑表成功但某个库一张没挑中:说清只给了表名', !!none?.detail.includes('「b」共 70 张表,未挑中与需求相关的表,已提供全部表名'),
none?.detail)
// 探查结构截断了(每个数据源最多取 200 张表):total 是数据库里的总数,explored 是探查到的
const partial = mod.copilotContext({ op: 'context', sources: [
{ source: 'wide', tables: ['t001', 't002'], selected_by: 'model', total: 205, explored: 200 },
{ source: 'wide2', tables: [], selected_by: 'fallback', total: 260, explored: 200, reason: 'r' }] })
check('截断了的库写「共 N 张,只探查了 200 张」', !!partial?.detail.includes('「wide」共 205 张,只探查了 200 张,按需求挑出 2 张:t001、t002')
&& partial.detail.includes('「wide2」共 260 张,只探查了 200 张,未能按需求挑选'), partial?.detail)
const whole = mod.copilotContext({ op: 'context', sources: [
{ source: 'a', tables: ['x'], selected_by: 'model', total: 60, explored: 60 }] })
check('探查到的和总数一样:照旧写「从 N 张表中挑出」', !!whole?.detail.includes('「a」按需求从 60 张表中挑出 1 张') && !whole.detail.includes('只探查了'),
whole?.detail)
check('形状不对不出行、不报错', mod.decodeCopilot([
{ op: 'context' }, { op: 'context', sources: 'x' }, { op: 'context', sources: [null, 3, { tables: ['t'] }] },
]).length === 0)