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')