mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 18:41:59 +00:00
f0fd63f0b9
用户的原话是「copilot(包括问答)可以是侧边栏的效果」「显示他的简要思考节点, 使得用户知道他大致在想什么,以及用了哪些工具」。 画布右栏从「运行 / 属性 → 时间线 / 成果 / 事件」(两层嵌套 5 个视图)变成 「助手 / 属性」一层。助手栏里是一条流:你让它做什么、它做了什么、结果是什么。 Copilot 建图和跑图在协议上完全无关(SSE 操作流 vs 事件 WebSocket,不共享 seq 和 node_id),但对用户是一件事的两半,decode.ts 把两种流译成同一种 Step 之后才能排进同一条时间轴。 Copilot 不再是弹窗。弹窗的问题不是样式:每改一次图都要重开一次,关掉之后 需求文本就没了,想微调只能重打;而画布左下角那条浮条(z-30)和 Toast (z-100)、Modal(z-50)三层各自为政,本身也是重复的一份 Copilot 状态渲染。 两个都删了,输入框就在栏里,说完一句接着说下一句。右栏没删——它是发起运行 和处理审批的唯一入口,RunLauncher 原样保留,只是跑起来会自动收起给执行过程 让位。tab 用 hidden 切换而不是卸载,不然点一下「属性」看个节点再切回来, 输入框草稿和展开状态全丢。 问数据页和运行页换成同一个 AssistantStream + 同一个 decode.ts。RunPanel 里 那套平行的 EVENT_META + describe() 删了:留着两套翻译,同一次运行在不同页面 会读出不同的故事,而用户没法判断哪个是真的。同理合并了两个耗时格式化函数 (画布节点显示"0ms",右边同一步什么都不显示,两块还并排放着)。运行页列表 加了内容摘要——原本每行只有"临时图 · 1.2s · 340 tok",十条长得一模一样, 要分清哪条是哪条只能挨个点开。 跑真实运行时发现两个缺陷: 1. 后端丢事件(app/core/bus.py)。WebSocket 先读历史、再订阅,两步之间图 正好跑出来的事件既不在历史里也不在实时流里,永远丢了。而 subscribe() 是 异步生成器,真正注册还要等到第一次迭代,窗口比看上去更大。实测一次跑图 开头的 node.finished/node.started 消失,界面上表现为某个节点永远在转圈、 它的子步骤跑到了顶层。改成先 attach 占位、再读历史,重叠部分按 seq 去重。 tests/test_bus_attach.py 里有一条刻画旧行为的测试,说明这个窗口确实存在。 2. 循环里的多轮审批被算错。我在 S1 按内容去重,但三轮审批的 title 一模一样, 和 LangGraph 的重放无法区分——结果第 2、3 次真实审批被当成重放吞掉,而 三次"你驳回了/你放行了"各自成行。改成开/闭配对:开着就是同一次,闭了才是 新一次;决定折进那条审批行而不是另起一行。真实运行(驳回 → 改写 → 再驳回 → 改写 → 放行)已收进 fixtures。 验证:三层各守一件事,共 76 条断言全过。check-decode 守翻译(含新的多轮审批 和截断结果集),check-stream 守渲染,新增 check-ui 守"接起来还对不对"——真 浏览器真后端,包括「切走再切回来草稿还在」「唯一的运行入口还在」「原始事件 切得到也切得回」。另外真跑了一次带三轮审批的图和两次 Copilot 生成(60s, deepseek-v4-pro),截图逐帧看过。后端 78 个测试全过。
81 lines
3.6 KiB
JavaScript
81 lines
3.6 KiB
JavaScript
// AssistantStream 的渲染回归检查。
|
|
//
|
|
// 解码器的检查(check-decode.mjs)守的是"翻译对不对",这里守的是"画出来
|
|
// 对不对"——两者能各自通过而合起来是坏的:Step 里字段都对,组件却把它渲染成
|
|
// 一屏转义 JSON,或者在 360px 窄栏里把页面撑得横向滚动。
|
|
//
|
|
// 用真浏览器跑真组件,数据还是 fixtures.json 里那批真实事件。
|
|
// 跑之前前端得起着:./scripts/dev.sh
|
|
import { chromium } from '../frontend/node_modules/playwright-core/index.mjs'
|
|
|
|
const WEB = process.env.AGENTLAB_WEB ?? 'http://localhost:5273'
|
|
// 不用 playwright install:它既下不动也会动到已有缓存。系统 Chrome 就够了。
|
|
const CHROME = process.env.CHROME_PATH
|
|
?? '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'
|
|
const CASES = ['db', 'think', 'human', 'issue', 'failed', 'loop_approve']
|
|
|
|
let failed = 0
|
|
const check = (name, cond, detail = '') => {
|
|
console.log(` ${cond ? '✓' : '✗'} ${name}${detail ? ` — ${detail}` : ''}`)
|
|
if (!cond) failed++
|
|
}
|
|
|
|
const browser = await chromium.launch({ executablePath: CHROME })
|
|
|
|
for (const kind of CASES) {
|
|
console.log(`\n=== ${kind} ===`)
|
|
const page = await browser.newPage({ viewport: { width: 1280, height: 900 } })
|
|
const errors = []
|
|
page.on('pageerror', (e) => errors.push(e.message))
|
|
page.on('console', (m) => { if (m.type() === 'error') errors.push(m.text()) })
|
|
await page.goto(`${WEB}/preview.html?case=${kind}`, { waitUntil: 'networkidle' })
|
|
await page.waitForTimeout(300)
|
|
|
|
const text = await page.locator('body').innerText()
|
|
// 404 是 favicon,和组件无关
|
|
const real = errors.filter((e) => !e.includes('404'))
|
|
check('没有运行时报错', real.length === 0, real.join(' | '))
|
|
check('有内容渲染出来', text.length > 40, `${text.length} 字`)
|
|
check('没有把转义 JSON 直接吐出来', !text.includes('\\"columns\\"'))
|
|
check('没有 0ms 噪音', !/\b0ms\b/.test(text))
|
|
check('没有裸事件名', !/\b(node|run|llm|tool)\.(started|finished|start|end)\b/.test(text))
|
|
|
|
// 360px 窄栏是画布右栏的真实宽度。这里溢出,右栏就会横向滚动
|
|
const overflow = await page.evaluate(() => {
|
|
const el = document.documentElement
|
|
return el.scrollWidth - el.clientWidth
|
|
})
|
|
check('页面不横向溢出', overflow <= 0, `${overflow}px`)
|
|
|
|
await page.close()
|
|
}
|
|
|
|
console.log('\n=== 展开交互 ===')
|
|
{
|
|
const page = await browser.newPage({ viewport: { width: 1280, height: 900 } })
|
|
await page.goto(`${WEB}/preview.html?case=db`, { waitUntil: 'networkidle' })
|
|
await page.waitForTimeout(300)
|
|
const before = await page.locator('body').innerText()
|
|
check('收起时不显示 SQL', !before.includes('SELECT'))
|
|
await page.locator('button', { hasText: '在 bi 上查询数据' }).first().click()
|
|
await page.waitForTimeout(250)
|
|
const after = await page.locator('body').innerText()
|
|
check('展开后能看到 SQL 原文', after.includes('SELECT') && after.includes('ANALYTICS'))
|
|
check('展开后能看到结果表', after.includes('factory_code'))
|
|
await page.close()
|
|
}
|
|
|
|
console.log('\n=== 空态 ===')
|
|
{
|
|
const page = await browser.newPage({ viewport: { width: 1280, height: 900 } })
|
|
await page.goto(`${WEB}/preview.html?case=__none__`, { waitUntil: 'networkidle' })
|
|
await page.waitForTimeout(250)
|
|
const text = await page.locator('body').innerText()
|
|
check('没有内容时给的是空态而不是白屏', text.includes('问点什么'))
|
|
await page.close()
|
|
}
|
|
|
|
await browser.close()
|
|
console.log(failed ? `\n✗ ${failed} 项未通过` : '\n✓ 助手流渲染全部通过')
|
|
process.exit(failed ? 1 : 0)
|