mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 04:31:58 +00:00
964eeaa829
在画布上拖拽编排 agent 工作流,并实时看着它执行。 后端 FastAPI + LangGraph:把画布 JSON 编译成 StateGraph,15 种节点 (模型/Agent/多Agent、工具/沙箱、分支/循环/子图、记忆/检索/整形、 人工介入/结构校验)。选 LangGraph 是因为可视化编排真正难的是暂停、 恢复和回放,而 checkpointer + interrupt() 正好把这几件事做在了底层。 前端 React + React Flow:节点元数据驱动属性面板与连接桩,运行时 节点高亮、边流动画、卡片内流式 token 全部由同一份事件流驱动。 关键实现: - Agent 内工具审批用 @task 缓存模型调用,interrupt 重放时不重复计费 - 沙箱保持只读根文件系统,代码经 exec+base64 写入(put_archive 会被 只读容器拒绝),断网、非 root、内存/CPU/超时限额 - 检索为向量与 BM25 混合,本地哈希 embedding 零配置可用 - 分支条件走 AST 白名单求值器;HTTP 工具逐 IP 拦截内网与元数据地址 - Mock provider 让无 API Key 也能跑通完整链路 7 个内置模板全部冒烟通过,含真实模型端到端验证。
49 lines
2.7 KiB
JavaScript
49 lines
2.7 KiB
JavaScript
// 冒烟测试:真开一个浏览器,跑一遍「打开画布 → 选模板 → 运行 → 看高亮」。
|
|
// 依赖 frontend 里的 playwright-core,并复用系统已装的 Chrome,不额外下载浏览器。
|
|
import { chromium } from '../frontend/node_modules/playwright-core/index.mjs'
|
|
const browser = await chromium.launch({
|
|
executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', args: ['--no-sandbox'] })
|
|
const page = await browser.newPage({ viewport: { width: 1680, height: 1000 } })
|
|
const errs = []
|
|
page.on('pageerror', e => errs.push(e.message))
|
|
page.on('console', m => m.type() === 'error' && errs.push(m.text()))
|
|
|
|
await page.goto('http://localhost:5273/studio', { waitUntil: 'networkidle' })
|
|
await page.waitForSelector('.react-flow__node', { timeout: 15000 })
|
|
|
|
// 切到「最小问答」模板,用 mock 模型跑,验证画布实时高亮
|
|
await page.getByRole('button', { name: /批量处理|最小问答|工作流/ }).first().click()
|
|
await page.waitForTimeout(600)
|
|
const row = page.locator('text=① 最小问答').first()
|
|
if (await row.count()) { await row.click(); await page.waitForTimeout(900) }
|
|
|
|
console.log('当前图节点数:', await page.locator('.react-flow__node').count())
|
|
|
|
// 选中 llm 节点,把模型改成 mock,避免这次验证花真钱
|
|
await page.locator('.react-flow__node').nth(1).click()
|
|
await page.waitForTimeout(500)
|
|
const modelSel = page.locator('select').filter({ hasText: /默认|Mock|Claude/ }).first()
|
|
if (await modelSel.count()) { await modelSel.selectOption({ label: 'Mock(不花钱,用来试编排)' }).catch(()=>{}) }
|
|
await page.screenshot({ path: '/tmp/shot-inspector.png' })
|
|
|
|
// 回到运行面板,填输入并运行
|
|
await page.getByRole('button', { name: '运行', exact: true }).first().click()
|
|
await page.waitForTimeout(300)
|
|
const ta = page.locator('aside textarea').first()
|
|
await ta.fill('可视化 agent 编排解决了什么问题?')
|
|
await page.getByRole('button', { name: /^运行$/ }).last().click()
|
|
|
|
// 等到有节点进入运行态,抓一张"正在跑"的画面
|
|
await page.waitForSelector('.node-running, .node-done', { timeout: 20000 }).catch(()=>{})
|
|
await page.waitForTimeout(1200)
|
|
await page.screenshot({ path: '/tmp/shot-running.png' })
|
|
console.log('运行中高亮节点:', await page.locator('.node-running').count(), '已完成:', await page.locator('.node-done').count())
|
|
|
|
await page.waitForTimeout(6000)
|
|
await page.screenshot({ path: '/tmp/shot-done.png' })
|
|
console.log('完成后 done 节点:', await page.locator('.node-done').count())
|
|
const timeline = await page.locator('aside').last().innerText()
|
|
console.log('--- 右侧面板文本 ---'); console.log(timeline.slice(0, 700))
|
|
console.log('--- 错误 ---'); console.log(errs.slice(0,6).join('\n') || '(无)')
|
|
await browser.close()
|