mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 00:31:58 +00:00
数据目录 F3 / B4 界面:目录页顶部说明「这个数据源共有 N 张表,只探查了前 200 张」,写明为什么只取了这些、其余的助手也看不到,下一步回到数据源卡片换 schema 或缩小范围后重新探查;助手「参考了哪些表」那一行截断了的库写「共 N 张,只探查了 200 张」;补检查脚本的断言
This commit is contained in:
@@ -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)} 张`,
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// 数据剖析(数据源设置里的开关和预算;数据目录页的「数据剖析」)
|
||||
//
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 「用到但没确认」:运行中查询过的表里还有几张有推断项、几张没有目录。这些表的推断项会被助手当真用,最该先审。
|
||||
* 两段各是一个按钮,点了切到清单的对应筛选、按使用次数排;都审完了只写一句,不给按钮
|
||||
|
||||
@@ -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}`
|
||||
|
||||
@@ -2671,6 +2671,10 @@ export interface CatalogList {
|
||||
system_notes: boolean
|
||||
/** 没有表结构时的原因(「尚未探查结构」「结构探查失败:…」);有表结构时为 null */
|
||||
schema_note: string | null
|
||||
/** 探查结构截断了(每个数据源最多取 200 张表):没探查到的表不在清单里,助手也看不到。老后端没有这一项 */
|
||||
schema_truncated?: boolean
|
||||
/** 数据库里一共几张表(含视图);没截断时等于探查到的张数 */
|
||||
schema_total?: number
|
||||
}
|
||||
|
||||
/** 表结构里的一列 */
|
||||
|
||||
@@ -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: [] }
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user