diff --git a/frontend/src/run/AssistantStream.tsx b/frontend/src/run/AssistantStream.tsx index a176de43..012349ed 100644 --- a/frontend/src/run/AssistantStream.tsx +++ b/frontend/src/run/AssistantStream.tsx @@ -1854,8 +1854,9 @@ export function ResultTable({ table, artifact, title, full = false, highlight, m
{[ - rows.length < table.rows.length - ? `显示前 ${formatNumber(rows.length)} / ${formatNumber(table.rows.length)} 行` + // total:结果一共几行(预览只存了前几行时比 rows 多,比如合并查询 6 行只预览 5 行) + rows.length < Math.max(table.total ?? 0, table.rows.length) + ? `显示前 ${formatNumber(rows.length)} / ${formatNumber(Math.max(table.total ?? 0, table.rows.length))} 行` : `${formatNumber(table.rows.length)} 行`, hiddenCols > 0 ? `显示前 ${cols.length} / ${table.columns.length} 列` : null, ].filter(Boolean).join(' · ')} diff --git a/frontend/src/run/decode.ts b/frontend/src/run/decode.ts index cca77378..bae580bb 100644 --- a/frontend/src/run/decode.ts +++ b/frontend/src/run/decode.ts @@ -441,8 +441,10 @@ export interface ResultTable { rows: unknown[][] /** 查询本身撞了行数上限——数据库里还有更多,是 guard 没让它全取回来 */ truncated: boolean - /** 这份预览被按字符数切断了——取回来的行数比这里显示的多,只是没存下来 */ + /** 这份预览被按字符数切断了,或者本来就只存了前几行——实际的行数比这里显示的多,只是没存下来 */ clipped?: boolean + /** 实际一共几行(结果里的 row_count)。比 rows 多时,表格脚注写「显示前 5 / 6 行」 */ + total?: number } /** @@ -477,7 +479,9 @@ function tryTable(text: string): ResultTable | null { try { const data = JSON.parse(text) if (Array.isArray(data?.columns) && Array.isArray(data?.rows)) { - return { columns: data.columns, rows: data.rows, truncated: !!data.truncated } + const total = typeof data.row_count === 'number' && data.row_count > data.rows.length ? data.row_count : undefined + return { columns: data.columns, rows: data.rows, truncated: !!data.truncated, + ...(data.clipped === true || total != null ? { clipped: true } : {}), ...(total != null ? { total } : {}) } } } catch { /* 交给调用方决定要不要修 */ @@ -2187,8 +2191,11 @@ export function decodeRun(events: RunEvent[], final?: RunFinal, opts?: DecodeOpt ...(warnings ? { level: 'warn' as const, sub: MERGE_TEXT.warnings(warnings) } : {}), title: MERGE_TEXT.stepTitle(inputs.map((i) => i.alias)), detail: typeof d.sql === 'string' && d.sql ? d.sql : undefined, + // 预览只存了前几行:那是「此处仅为预览」(clipped),不是「查询已达行数上限」(truncated)。 + // 合并结果本身超过上限才是 truncated;row_count 记全部行数,表格脚注写「显示前 5 / 6 行」 result: Array.isArray(d.columns) - ? JSON.stringify({ columns: d.columns, rows: preview, truncated: rows != null && rows > preview.length }) + ? JSON.stringify({ columns: d.columns, rows: preview, truncated: !!d.truncated, + clipped: rows != null && rows > preview.length, ...(rows != null ? { row_count: rows } : {}) }) : undefined, meta: [rows != null ? MERGE_TEXT.rows(rows) : '', dur(ms) ?? ''].filter(Boolean).join(' · ') || undefined, ms, diff --git a/scripts/check-decode.mjs b/scripts/check-decode.mjs index c0d2b223..db2b642b 100644 --- a/scripts/check-decode.mjs +++ b/scripts/check-decode.mjs @@ -123,6 +123,10 @@ await section('合并查询', async () => { check('标题说合并的是哪几个输入', m?.title === '合并 s、v 的查询结果', m?.title) check('合并 SQL 留在详情里', m?.detail === synthetic.MERGE_SQL) check('预览按查询结果的形状给出,能画成表格', !!mod.parseQueryResult(m?.result ?? '')?.columns?.includes('到店人数')) + // 合并结果 8 行、预览只存了 5 行:这是「此处仅为预览」,不是「查询已达行数上限」(A3) + const table = mod.parseQueryResult(m?.result ?? '') + check('预览只存了前几行:记为预览(clipped),不记为查询撞了行数上限(truncated),并带上一共几行', table?.truncated === false + && table?.clipped === true && table?.total === 8 && table?.rows.length === 5, JSON.stringify({ t: table?.truncated, c: table?.clipped, n: table?.total })) check('输入、行数、数据源都在', m?.merge?.inputs?.length === 2 && m.merge.inputs[1].source === 'members' && m.merge.inputs[0].label === '门店销售' && m.merge.rows === 8) check('行数和耗时写在行尾', !!m?.meta?.includes('8 行'), m?.meta) diff --git a/scripts/check-stream.mjs b/scripts/check-stream.mjs index 3e7ddf01..843e6b1b 100644 --- a/scripts/check-stream.mjs +++ b/scripts/check-stream.mjs @@ -709,6 +709,10 @@ await section('合并查询:输入、合并 SQL、结果预览,警告各占 check('合并 SQL 有标签、带复制', (await row.innerText()).includes('合并 SQL') && await row.getByRole('button', { name: '复制合并 SQL' }).count() === 1) check('结果预览画成表格(列是合并结果的列)', (await row.locator('table th').allInnerTexts()).some((t) => t.includes('到店人数'))) + // 合并结果 8 行、预览只存了 5 行:说「此处仅为预览」和「显示前 5 / 8 行」,不说「查询已达行数上限」(A3) + const foot = (await row.innerText().catch(() => '')).replace(/\s+/g, ' ') + check('预览只存了前几行:写「显示前 5 / 8 行」和「仅为预览」,不说查询已达行数上限', foot.includes('显示前 5 / 8 行') + && foot.includes('此处仅为预览') && !foot.includes('查询已达行数上限'), foot.slice(-120)) check('输入写在合并 SQL 前面:先看合并的是什么,再看怎么合并', await row.evaluate((el) => { const m = el.querySelector('[data-step-merge]') const pre = el.querySelector('pre')