mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 03:31:58 +00:00
数据目录阶段 1B 前端:画布和问数据页的过程里显示「参考了 N 张表」,展开按数据源列出表名,挑表失败时写明只提供了表名及原因
This commit is contained in:
@@ -10,7 +10,7 @@
|
||||
|
||||
- all:放得下详细摘要的库,照旧每张表一行带字段,额外带上数据目录和表之间的连接条件;
|
||||
- model:大库,挑表成功:挑中的表讲透,其余只列名字;
|
||||
- fallback:大库,挑表失败(模型不可用、报错、超时、回复读不出、返回为空或返回的表都不存在),退回以前的
|
||||
- fallback:大库,挑表失败(模型不可用、报错、超时、回复无法识别、返回为空或返回的表都不存在),退回以前的
|
||||
「只列表名」。挑表只是让模型少猜,**绝不能挡住生成**:没有它,模型照旧可以先用 db_schema 查字段。
|
||||
|
||||
现有工作流(画布上改图时的原图、问数据追问时上一轮的图)的 SQL 里查过哪些表是确定的事实:大库里这些表一律
|
||||
@@ -269,7 +269,7 @@ async def plan_context(sources: list[Any], *, graph: Mapping[str, Any] | None =
|
||||
|
||||
|
||||
class _Unreadable(ValueError):
|
||||
"""模型的回复读不出表名。"""
|
||||
"""模型的回复里识别不出表名。"""
|
||||
|
||||
|
||||
def _picked(raw: Any) -> list[tuple[str | None, str]] | None:
|
||||
@@ -316,7 +316,7 @@ async def _ask(model: Any, messages: list[tuple[str, str]]) -> list[tuple[str |
|
||||
return parsed
|
||||
parsed = _picked(message_text(await model.ainvoke(messages)))
|
||||
if parsed is None:
|
||||
raise _Unreadable("模型的回复不是约定的格式,读不出要用的表")
|
||||
raise _Unreadable("模型的回复不是约定的格式,无法识别要用的表")
|
||||
return parsed
|
||||
|
||||
|
||||
@@ -386,13 +386,13 @@ class ContextPlan:
|
||||
reason: str | None = None
|
||||
chosen: list[tuple[int, str]] = []
|
||||
if model is None:
|
||||
reason = unavailable or "没有可用的模型"
|
||||
reason = unavailable or "未配置可用的模型"
|
||||
else:
|
||||
try:
|
||||
picks = await asyncio.wait_for(_ask(model, self._pick_messages(need)), timeout=PICK_TIMEOUT_S)
|
||||
chosen = self._resolve(picks)
|
||||
if not picks:
|
||||
reason = "模型没有挑出任何表"
|
||||
reason = "模型未挑出任何表"
|
||||
elif not chosen:
|
||||
reason = "模型挑出的表在表结构中都不存在"
|
||||
except asyncio.TimeoutError:
|
||||
|
||||
@@ -13,7 +13,7 @@ import mdSamples from '../run/__tests__/markdown-samples.json'
|
||||
import evidenceFixture from '../run/__tests__/evidence-doc.json'
|
||||
import evidenceJudge from '../run/__tests__/evidence-judge.json'
|
||||
import {
|
||||
CALIBER_ONE_OUTPUT, CALIBERS_OUTPUT, COPILOT_STUCK, COPILOT_TOOLS_DROPPED, MIXED_OUTPUT, abandonedRun, cancelledRun, exhaustedTeam, longLoop,
|
||||
CALIBER_ONE_OUTPUT, CALIBERS_OUTPUT, COPILOT_CONTEXT, COPILOT_CONTEXT_FALLBACK, COPILOT_STUCK, COPILOT_TOOLS_DROPPED, MIXED_OUTPUT, abandonedRun, cancelledRun, exhaustedTeam, longLoop,
|
||||
markupRun, mixedRun, pipelineRun, repairRun, teamRun, timeoutRun,
|
||||
} from '../run/__tests__/synthetic'
|
||||
import '../index.css'
|
||||
@@ -323,6 +323,8 @@ function synthetic(name: string): StreamTurn[] {
|
||||
// 问数据的一轮:先建图、再执行。执行开始后「规划」收成一行
|
||||
const plan = decodeCopilot([
|
||||
{ op: 'heartbeat', phase: 'planning', elapsed_ms: 6400 },
|
||||
{ op: 'context', elapsed_ms: 2100, sources: [
|
||||
{ source: 'warehouse', tables: ['v_device_kpi', 'dim_device'], selected_by: 'model', total: 53 }] },
|
||||
{ op: 'thinking', delta: '要先查表结构,再按设备汇总 KPI。' },
|
||||
{ op: 'plan', summary: '取数 → 汇总 → 出结论' },
|
||||
{ op: 'add_node', node: { id: 'q', type: 'tool', label: '查询设备 KPI' } },
|
||||
@@ -337,6 +339,13 @@ function synthetic(name: string): StreamTurn[] {
|
||||
graph: { nodes: [{ id: 'q', type: 'tool', data: { label: '查询设备 KPI' } },
|
||||
{ id: 'o', type: 'output', data: { label: '成果' } }] } }]
|
||||
}
|
||||
case 'copilot-context':
|
||||
// 大库按需求挑表:过程里一行「参考了 N 张表」,展开按数据源分组列表名
|
||||
return [{ id: 'copilot-context', question: '各渠道上月的入园人次', phase: 'done', status: '已应用到画布',
|
||||
steps: decodeCopilot(COPILOT_CONTEXT, { context: 'canvas' }) }]
|
||||
case 'copilot-fallback':
|
||||
return [{ id: 'copilot-fallback', question: '各渠道上月的入园人次', phase: 'done', status: '已应用到画布',
|
||||
steps: decodeCopilot(COPILOT_CONTEXT_FALLBACK, { context: 'canvas' }) }]
|
||||
case 'copilot':
|
||||
return [{ id: 'copilot', question: '做一个每天的出勤日报', phase: 'done',
|
||||
status: '已应用到画布,仍有 2 处问题待处理',
|
||||
|
||||
@@ -395,6 +395,41 @@ export const COPILOT_TOOLS_DROPPED = [
|
||||
] },
|
||||
]
|
||||
|
||||
/**
|
||||
* 助手按需求挑表(context 操作,copilot_context.DatasourceContext.op):大库挑了 3 张(含补进来的关联表),
|
||||
* 小库全部表都带字段。挑表用了 3 秒多,先到一拍心跳,再是 context,然后照常思考、起草、自查
|
||||
*/
|
||||
export const COPILOT_CONTEXT = [
|
||||
{ op: 'model', model: 'claude-sonnet-4' },
|
||||
{ op: 'heartbeat', phase: 'planning', elapsed_ms: 3000 },
|
||||
{ op: 'context', elapsed_ms: 3420, sources: [
|
||||
{ source: 'scenic', tables: ['visits', 'channels', 'channel_visits'], selected_by: 'model', total: 51 },
|
||||
{ source: 'shop', tables: ['orders', 'customers'], selected_by: 'all', total: 2 },
|
||||
] },
|
||||
{ op: 'heartbeat', phase: 'planning', elapsed_ms: 6000 },
|
||||
{ op: 'thinking', delta: '入园记录不带渠道,要经 channel_visits 连到渠道表,再按渠道汇总上月的入园人次。' },
|
||||
{ op: 'plan', summary: '取数 → 出结论' },
|
||||
{ op: 'add_node', node: { id: 'q', type: 'tool', label: '查询各渠道入园人次' } },
|
||||
{ op: 'add_node', node: { id: 'o', type: 'output', label: '成果' } },
|
||||
{ op: 'done', explanation: '两步' },
|
||||
{ op: 'check', status: 'passed', repaired: 0 },
|
||||
{ op: 'final', graph: { nodes: [{ id: 'q' }, { id: 'o' }] } },
|
||||
]
|
||||
|
||||
/** 挑表失败(超时):只给了表名,原因随 context 带来;生成照常进行 */
|
||||
export const COPILOT_CONTEXT_FALLBACK = [
|
||||
{ op: 'model', model: 'claude-sonnet-4' },
|
||||
{ op: 'heartbeat', phase: 'planning', elapsed_ms: 18000 },
|
||||
{ op: 'context', elapsed_ms: 20004, sources: [
|
||||
{ source: 'scenic', tables: [], selected_by: 'fallback', total: 51, reason: '挑选数据表超过 20 秒未完成' },
|
||||
] },
|
||||
{ op: 'plan', summary: '先查表结构,再取数' },
|
||||
{ op: 'add_node', node: { id: 's', type: 'tool', label: '查看表结构' } },
|
||||
{ op: 'add_node', node: { id: 'o', type: 'output', label: '成果' } },
|
||||
{ op: 'done', explanation: '两步' },
|
||||
{ op: 'final', graph: { nodes: [{ id: 's' }, { id: 'o' }] } },
|
||||
]
|
||||
|
||||
/** Copilot 操作流:思考被心跳打断、自查修一轮没修好、模型写了不存在的节点类型 */
|
||||
export const COPILOT_STUCK = [
|
||||
{ op: 'heartbeat', phase: 'planning', elapsed_ms: 3000 },
|
||||
|
||||
@@ -2404,6 +2404,83 @@ function labelsOf(ops: CopilotOp[], fallback?: (id: string) => string | undefine
|
||||
return (id) => map.get(id) ?? fallback?.(id)
|
||||
}
|
||||
|
||||
/** context 操作里一个数据源的那一项(服务端 copilot_context.DatasourceContext.op) */
|
||||
export interface CopilotContextSource {
|
||||
source: string
|
||||
/** 模型看得到全部字段的表(全名)。只给了表名的不在这里 */
|
||||
tables: string[]
|
||||
/** model:按需求挑的;all:库不大,全部表都带字段;fallback:没能挑出来,只给了表名 */
|
||||
selectedBy: 'model' | 'all' | 'fallback'
|
||||
/** 这个库一共几张表 */
|
||||
total?: number
|
||||
/** 没能挑出来的原因 */
|
||||
reason?: string
|
||||
}
|
||||
|
||||
/** 挑表失败时的说法:模型这一轮只看到表名,字段要它自己去查 */
|
||||
export const CONTEXT_FALLBACK_TEXT = '未能按需求挑选,已提供全部表名'
|
||||
|
||||
function contextSources(op: CopilotOp): CopilotContextSource[] {
|
||||
const list: unknown[] = Array.isArray(op.sources) ? op.sources : []
|
||||
return list
|
||||
.filter((x): x is Record<string, any> => !!x && typeof x === 'object' && typeof (x as any).source === 'string')
|
||||
.map((x) => {
|
||||
const total = num(x.total)
|
||||
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 } : {}),
|
||||
...(typeof x.reason === 'string' && x.reason ? { reason: x.reason } : {}),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** 一个库在展开区里的那一行:怎么来的 + 表名 */
|
||||
function contextLine(g: CopilotContextSource): string {
|
||||
const names = g.tables.join('、')
|
||||
const of = g.total != null ? `共 ${g.total} 张表` : ''
|
||||
switch (g.selectedBy) {
|
||||
case 'all':
|
||||
return `「${g.source}」全部 ${g.tables.length} 张表:${names}`
|
||||
case 'model':
|
||||
return g.tables.length
|
||||
? `「${g.source}」按需求${g.total != null ? `从 ${g.total} 张表中` : ''}挑出 ${g.tables.length} 张:${names}`
|
||||
: `「${g.source}」${of ? `${of},` : ''}未挑中与需求相关的表,已提供全部表名`
|
||||
default:
|
||||
return `「${g.source}」${of ? `${of},` : ''}${CONTEXT_FALLBACK_TEXT}`
|
||||
+ (g.tables.length ? `;现有工作流用到的 ${g.tables.length} 张表附带全部字段:${names}` : '')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 助手这一轮参考了哪些表(context 操作)→ 过程里的一行。
|
||||
*
|
||||
* 大库放不下全部字段,服务端先按需求挑表,挑中的表给模型全部字段。这件事要看得见:生成的 SQL 用错了表,
|
||||
* 先得知道模型当时看到的是哪几张。标题只说几张(「参考了 8 张表」),展开按数据源分组列表名;挑表失败时
|
||||
* 标题直说只给了表名,原因放在展开区。认不出的形状返回 null:不出这一行,也不报错。
|
||||
*/
|
||||
export function copilotContext(op: CopilotOp): { title: string; sub?: string; detail: string; ms?: number } | null {
|
||||
const groups = contextSources(op)
|
||||
if (!groups.length) return null
|
||||
const count = groups.reduce((n, g) => n + g.tables.length, 0)
|
||||
const failed = groups.filter((g) => g.selectedBy === 'fallback')
|
||||
// 有表带着字段(小库,或者现有工作流用到的表)时标题仍说几张,没挑成的事放副标题
|
||||
const sub = count && failed.length
|
||||
? failed.length === groups.length ? CONTEXT_FALLBACK_TEXT
|
||||
: `「${failed.map((g) => g.source).join('」「')}」${CONTEXT_FALLBACK_TEXT}`
|
||||
: undefined
|
||||
const reasons = [...new Set(failed.map((g) => g.reason).filter(Boolean))]
|
||||
// 挑过表才有 elapsed_ms:挑表是生成之前多出来的一次模型调用,没挑成(比如超时)也要让人看到等了多久
|
||||
const ms = num(op.elapsed_ms)
|
||||
return {
|
||||
title: count ? `参考了 ${count} 张表` : CONTEXT_FALLBACK_TEXT,
|
||||
...(sub ? { sub } : {}),
|
||||
detail: [...groups.map(contextLine), ...reasons.map((r) => `原因:${r}`)].join('\n'),
|
||||
...(ms != null && ms >= 10 ? { ms } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
/** 用了限定范围之外的数据源:自查交回模型改也改不掉时,人要知道是范围的事,不是图写错了 */
|
||||
const OUT_OF_SCOPE_NEXT = '本轮限定了查询的数据源,但工作流使用了范围之外的数据库。请取消限定后重新提问,或将所需数据库加入查询范围'
|
||||
|
||||
@@ -2508,6 +2585,16 @@ export function decodeCopilot(ops: CopilotOp[], opts?: {
|
||||
...(elapsed ? { ms: elapsed, meta: formatDuration(elapsed) } : {}) })
|
||||
break
|
||||
}
|
||||
case 'context': {
|
||||
// 这一轮参考了哪些表:标题说几张,展开看按数据源分组的表名
|
||||
const ctx = copilotContext(op)
|
||||
if (!ctx) break
|
||||
add({ id: `cx-${i}`, seq: i, kind: 'schema', status: 'done', code: 'copilot_context',
|
||||
title: ctx.title, detail: ctx.detail,
|
||||
...(ctx.sub ? { sub: ctx.sub } : {}),
|
||||
...(ctx.ms != null ? { ms: ctx.ms, meta: formatDuration(ctx.ms) } : {}) })
|
||||
break
|
||||
}
|
||||
case 'plan':
|
||||
add({ id: `cp-${i}`, seq: i, kind: 'note', status: 'done',
|
||||
title: clip(String(op.summary ?? '已完成规划'), 120) })
|
||||
|
||||
@@ -12,7 +12,7 @@ import { humanizeError } from '../lib/errors'
|
||||
import { formatShortcut } from '../lib/keys'
|
||||
import { UPGRADE_TEXT } from '../lib/terms'
|
||||
import type { CopilotOp } from '../run/decode'
|
||||
import { endingOf, reduceTeam, settleTeam } from '../run/decode'
|
||||
import { copilotContext, endingOf, reduceTeam, settleTeam } from '../run/decode'
|
||||
import { activeEdgesOf, applyDerived } from '../run/derive'
|
||||
import {
|
||||
emptyTrace, finalizeTrace, foldEvent, isActivePhase, isSettled, isTerminal, runStatusOf,
|
||||
@@ -1888,6 +1888,12 @@ export const useStudio = create<StudioState>((set, get) => ({
|
||||
},
|
||||
})
|
||||
break
|
||||
case 'context': {
|
||||
// 服务端按需求挑了表:浮条和轮次卡上先说参考了几张表,展开的过程里有表名(decodeCopilot)
|
||||
const ctx = copilotContext(op)
|
||||
if (ctx) set({ copilot: { ...s.copilot, lastOp: ctx.title } })
|
||||
break
|
||||
}
|
||||
case 'plan':
|
||||
set({ copilot: { ...s.copilot, phase: 'planning', lastOp: op.summary ?? '规划中' } })
|
||||
break
|
||||
|
||||
@@ -472,6 +472,9 @@ async function fakeApi(route) {
|
||||
}
|
||||
const ops = [
|
||||
{ op: 'heartbeat', phase: 'planning', elapsed_ms: 800 },
|
||||
// 服务端按需求挑了表(copilot_context):过程里多一行「参考了 N 张表」
|
||||
{ op: 'context', elapsed_ms: 1200, sources: [
|
||||
{ source: 'shop', tables: ['orders', 'order_items'], selected_by: 'model', total: 48 }] },
|
||||
{ op: 'plan', summary: '查一下再回答' },
|
||||
...GRAPH.nodes.map((n) => ({ op: 'add_node', node: n })),
|
||||
{ op: 'done', explanation: '一张三步的图' },
|
||||
@@ -1024,6 +1027,16 @@ for (const theme of THEMES) {
|
||||
check('完整答案取到了,meta 里没有「没取全」', !meta?.clipped, JSON.stringify(meta?.clipped))
|
||||
await page.getByText('展开全部').first().click().catch(() => {})
|
||||
check('完整结尾在页面上', await shows(page, '【完整结尾】'))
|
||||
// 建图时服务端说了这一轮参考了哪些表:收在「规划」里,点开是一行,再点开看表名
|
||||
check('建图的 context 操作原样记进这一轮', t.ops.some((o) => o.op === 'context'))
|
||||
const turnCard = page.locator('[data-turn]').last()
|
||||
await turnCard.locator('button', { hasText: '规划' }).first().click()
|
||||
const ctxRow = turnCard.locator('[data-step-code="copilot_context"]')
|
||||
check('点开「规划」看到参考了几张表', await ctxRow.waitFor({ timeout: 3000 }).then(() => true, () => false)
|
||||
&& (await ctxRow.innerText()).includes('参考了 2 张表'))
|
||||
await ctxRow.locator('button').first().click()
|
||||
check('再点开看到按数据源分组的表名', await ctxRow.getByText('「shop」按需求从 48 张表中挑出 2 张:orders、order_items')
|
||||
.waitFor({ timeout: 3000 }).then(() => true, () => false))
|
||||
check('没有运行时报错', errors.length === 0, errors[0] ?? '')
|
||||
await ctx.close()
|
||||
})
|
||||
|
||||
@@ -621,6 +621,49 @@ await section('从 SQL 认表名', async () => {
|
||||
check('不是查询就不硬认', g('这不是 SQL') === null)
|
||||
})
|
||||
|
||||
await section('Copilot:这一轮参考了哪些表(context 操作)', async () => {
|
||||
// 大库先按需求挑表,挑中的表给模型全部字段。生成的 SQL 用错了表时,先得知道模型当时看到的是哪几张
|
||||
const steps = mod.decodeCopilot(synthetic.COPILOT_CONTEXT, { context: 'canvas' })
|
||||
const row = steps.find((s) => s.code === 'copilot_context')
|
||||
check('一行说清参考了几张表', row?.title === '参考了 5 张表', row?.title)
|
||||
check('展开按数据源分组列出表名', !!row?.detail?.includes('「scenic」按需求从 51 张表中挑出 3 张:visits、channels、channel_visits')
|
||||
&& row.detail.includes('「shop」全部 2 张表:orders、customers'), row?.detail)
|
||||
check('挑表的耗时挂在行尾', row?.ms === 3420 && !!row?.meta, `${row?.ms} ${row?.meta}`)
|
||||
check('是看表结构那一类、标着规划阶段', row?.kind === 'schema' && row?.stage === 'plan' && row?.status === 'done')
|
||||
check('排在思考之前,心跳仍然只更新那一条阶段行',
|
||||
steps.indexOf(row) < steps.findIndex((s) => s.kind === 'think')
|
||||
&& steps.filter((s) => s.kind === 'lifecycle' && s.id.startsWith('hb-')).length === 1,
|
||||
steps.map((s) => `${s.kind}:${s.title}`).join(' | '))
|
||||
check('收尾后没有还在转的行', !steps.some((s) => s.status === 'running'),
|
||||
steps.filter((s) => s.status === 'running').map((s) => s.title).join(','))
|
||||
check('结局不受影响', mod.copilotOutcome(synthetic.COPILOT_CONTEXT).kind === 'built')
|
||||
|
||||
const fb = mod.decodeCopilot(synthetic.COPILOT_CONTEXT_FALLBACK, { context: 'chat' }).find((s) => s.code === 'copilot_context')
|
||||
check('挑表失败:标题直说只给了表名', fb?.title === '未能按需求挑选,已提供全部表名', fb?.title)
|
||||
check('挑表失败:原因和库的规模放在展开区', !!fb?.detail?.includes('原因:挑选数据表超过 20 秒未完成')
|
||||
&& fb.detail.includes('「scenic」共 51 张表'), fb?.detail)
|
||||
check('挑表失败也说等了多久', fb?.ms === 20004, String(fb?.ms))
|
||||
|
||||
const seeded = mod.copilotContext({ op: 'context', sources: [
|
||||
{ source: 'scenic', tables: ['stores'], selected_by: 'fallback', total: 51, reason: '模型未挑出任何表' }] })
|
||||
check('没挑成但现有工作流的表带着字段:标题说几张、副标题说没挑成',
|
||||
seeded?.title === '参考了 1 张表' && seeded?.sub === '未能按需求挑选,已提供全部表名'
|
||||
&& seeded.detail.includes('现有工作流用到的 1 张表附带全部字段:stores'), JSON.stringify(seeded))
|
||||
const mixed = mod.copilotContext({ op: 'context', sources: [
|
||||
{ source: 'a', tables: ['x'], selected_by: 'all', total: 1 },
|
||||
{ source: 'b', tables: [], selected_by: 'fallback', total: 80, reason: 'r' }] })
|
||||
check('只有一个库没挑成:副标题点名是哪个库', mixed?.sub === '「b」未能按需求挑选,已提供全部表名', mixed?.sub)
|
||||
const none = mod.copilotContext({ op: 'context', sources: [
|
||||
{ source: 'a', tables: ['x'], selected_by: 'model', total: 60 },
|
||||
{ source: 'b', tables: [], selected_by: 'model', total: 70 }] })
|
||||
check('挑表成功但某个库一张没挑中:说清只给了表名', !!none?.detail.includes('「b」共 70 张表,未挑中与需求相关的表,已提供全部表名'),
|
||||
none?.detail)
|
||||
check('形状不对不出行、不报错', mod.decodeCopilot([
|
||||
{ op: 'context' }, { op: 'context', sources: 'x' }, { op: 'context', sources: [null, 3, { tables: ['t'] }] },
|
||||
]).length === 0)
|
||||
check('认不出的操作照旧忽略', mod.decodeCopilot([{ op: 'something_new', x: 1 }]).length === 0)
|
||||
})
|
||||
|
||||
await section('Copilot:心跳穿插、回话、少了一步、报错', async () => {
|
||||
const steps = mod.decodeCopilot(synthetic.COPILOT_STUCK, { context: 'canvas' })
|
||||
check('心跳穿插的思考仍然并成一条', steps.filter((s) => s.kind === 'think').length === 1,
|
||||
|
||||
@@ -694,6 +694,43 @@ await section('长表名、Copilot 结局、问数据的分段', async () => {
|
||||
await q.page.close()
|
||||
})
|
||||
|
||||
await section('参考了哪些表:画布右栏和问数据页(context 操作)', async () => {
|
||||
const c = await open('syn=copilot-context&dense=1', { w: 380, h: 800, name: 'copilot-context' })
|
||||
const row = c.page.locator('[data-step-code="copilot_context"]')
|
||||
check('画布右栏:一行说参考了几张表', await row.count() === 1 && (await row.innerText()).includes('参考了 5 张表'),
|
||||
await row.count() ? await row.innerText() : '没有这一行')
|
||||
check('表名收在展开区,不铺满窄栏', !(await row.innerText()).includes('channel_visits'))
|
||||
await row.locator('button').first().click()
|
||||
await c.page.waitForTimeout(150)
|
||||
const expanded = await row.innerText()
|
||||
check('展开看到按数据源分组的表名', expanded.includes('「scenic」按需求从 51 张表中挑出 3 张')
|
||||
&& expanded.includes('channel_visits') && expanded.includes('「shop」全部 2 张表'), expanded.slice(0, 160))
|
||||
const overflow = await c.page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth)
|
||||
check('展开后不撑破 380px 窄栏', overflow <= 0, `${overflow}px`)
|
||||
check('没有运行时报错', c.errors.length === 0, c.errors.join(' | '))
|
||||
await c.page.close()
|
||||
|
||||
const f = await open('syn=copilot-fallback&dense=1', { w: 380, h: 800, name: 'copilot-fallback' })
|
||||
const fb = f.page.locator('[data-step-code="copilot_context"]')
|
||||
check('挑表失败:写明只给了表名', await fb.count() === 1 && (await fb.innerText()).includes('未能按需求挑选,已提供全部表名'))
|
||||
await fb.locator('button').first().click()
|
||||
await f.page.waitForTimeout(150)
|
||||
check('挑表失败:展开看到原因', (await fb.innerText()).includes('原因:挑选数据表超过 20 秒未完成'))
|
||||
await f.page.close()
|
||||
|
||||
const q = await open('syn=chat', { w: 1100, h: 800, name: 'chat-context' })
|
||||
check('问数据页:执行开始后这一行随「规划」收起', !(await q.page.locator('body').innerText()).includes('参考了 2 张表'))
|
||||
await q.page.locator('button', { hasText: '规划' }).first().click()
|
||||
await q.page.waitForTimeout(200)
|
||||
const qrow = q.page.locator('[data-step-code="copilot_context"]')
|
||||
check('问数据页:点开「规划」看到参考了几张表', await qrow.count() === 1 && (await qrow.innerText()).includes('参考了 2 张表'))
|
||||
await qrow.locator('button').first().click()
|
||||
await q.page.waitForTimeout(150)
|
||||
check('问数据页:展开看到表名', (await qrow.innerText()).includes('v_device_kpi、dim_device'))
|
||||
check('问数据页:没有运行时报错', q.errors.length === 0, q.errors.join(' | '))
|
||||
await q.page.close()
|
||||
})
|
||||
|
||||
await section('步骤行和画布联动', async () => {
|
||||
const { page } = await open('syn=mixed&link=1', { w: 1100, h: 800 })
|
||||
const row = page.locator('[data-node-id="agent"]').first()
|
||||
|
||||
@@ -1025,6 +1025,9 @@ await section('助手改图:一步撤销、回执、只回话、失败退回
|
||||
}
|
||||
state.stream = [
|
||||
{ op: 'model', model: 'fake-model' },
|
||||
// 服务端按需求挑了表(copilot_context):过程里多一行「参考了 N 张表」
|
||||
{ op: 'context', elapsed_ms: 1500, sources: [
|
||||
{ source: 'shop', tables: ['orders', 'order_items', 'customers'], selected_by: 'model', total: 48 }] },
|
||||
{ op: 'plan', summary: '加一步润色' },
|
||||
{ op: 'update_node', id: 'answer', config: finalGraph.nodes[3].data.config },
|
||||
{ op: 'add_node', node: { id: 'polish', type: 'llm', label: '润色', config: { prompt: '{{ nodes.answer.text }}' } } },
|
||||
@@ -1041,6 +1044,12 @@ await section('助手改图:一步撤销、回执、只回话、失败退回
|
||||
const t1 = await S(page, () => window.__studio.getState().copilotTurns.at(-1))
|
||||
check('开始了就告诉输入框(runCopilot 返回 true)', began === true, String(began))
|
||||
check('收到 final:这一轮是「已应用」', t1.outcome === 'applied', t1.outcome)
|
||||
const ctxRow = page.locator('[data-step-code="copilot_context"]').last()
|
||||
check('助手栏的过程里一行说参考了几张表', await ctxRow.waitFor({ timeout: 3000 }).then(() => true, () => false)
|
||||
&& (await ctxRow.innerText()).includes('参考了 3 张表'))
|
||||
await ctxRow.locator('button').first().click()
|
||||
check('点开看到按数据源分组的表名', await ctxRow.getByText('「shop」按需求从 48 张表中挑出 3 张:orders、order_items、customers')
|
||||
.waitFor({ timeout: 3000 }).then(() => true, () => false))
|
||||
check('diff 记下新增和改动', t1.diff?.added.includes('polish') && t1.diff?.changed.includes('answer'), JSON.stringify(t1.diff))
|
||||
check('final.issues 进了这一轮(少了一步)', t1.issues?.some((i) => i.code === 'unknown_node_type'))
|
||||
const pos1 = await S(page, () => Object.fromEntries(window.__studio.getState().nodes.map((n) => [n.id, n.position])))
|
||||
|
||||
Reference in New Issue
Block a user