返回 Skill 列表
extension
分类: 开发与工程无需 API Key

智能体UI设计与验证技能(配套NEXENT集成技能)

基于智能体场景,结合思考过程动作摘要、流式输出设计、滚动三态等元素指导UI设计。验证阶段用 puppeteer-core“真实打开浏览器页面”,复现并验证前端 UI 问题的技能,覆盖通用 UI bug 与 SSE 流式前端呈现验证。

person作者: dmkx01hubModelScope

AI 应用 UI 设计与测试

Overview

本技能覆盖 AI 应用(智能体/数据分析问答/AI 对话)的前端 UI 设计规范真实浏览器测试两个维度:

  • 设计侧:智能体流式输出的前端呈现设计规范——思考面板三层呈现、内容净化、步骤化提炼 + 折叠展开、布局区分、智能滚动三态、打字机、多轮追问追加不覆盖(详见 references/streaming-ui.md)。
  • 测试侧:用 puppeteer-core 启动本机 Chrome 无头浏览器,真实打开页面复现 UI 问题。不依赖用户描述猜测根因,而是捕获真实请求/响应/控制台/DOM 数据后定位问题,修复后同一脚本复测验证(详见 bug 根因速查 references/bug-patterns.md)。

目录

关键前提

  1. 需要 puppeteer-core(在 node_modules 中)和本机 Chrome:
    • Chrome 路径:优先用环境变量 CHROME_PATH 指定;未设置时按平台默认查找 (Windows C:/Program Files/Google/Chrome/Application/chrome.exeProgram Files (x86) 下同名路径; macOS /Applications/Google Chrome.app/Contents/MacOS/Google Chrome;Linux /usr/bin/google-chrome
    • 运行需设置:NODE_PATH=<node_modules路径>
  2. 本地开发服务需已启动(如 uvicorn)。
  3. 页面有登录时需提供测试账号。

工作流程

Step 1: 用模板脚本快速复现

scripts/ui_test.js 是通用模板,覆盖核心捕获能力(console/pageerror、API 请求体与响应、DOM 快照、截图):

# 基础: 打开页面, 等待 5s, 截图(<url> 换成你的本地服务地址)
NODE_PATH=.../node_modules node ui_test.js <url> --wait-ms 5000

# 带登录(凭据用测试账号,可通过参数传入)
node ui_test.js <url> --login-user <user@example.com> --login-pass <password>

# 登录后点击某个按钮, 捕获指定 API 的请求/响应, 等待 8s
# (--click-selector / --watch-api 按目标系统实际元素与接口替换)
node ui_test.js <url> \
  --login-user <user@example.com> --login-pass <password> \
  --click-selector "<打开弹窗的按钮选择器>" \
  --watch-api "<要监听的接口路径片段>" \
  --wait-ms 8000

模板会输出:[REQ-BODY](重点看 postData 是否为空!)、[RESP] 状态码、DOM 快照(tables/modals/是否含 [object Object])、截图路径、控制台错误。

Step 2: 分析捕获数据定位根因

对照 references/bug-patterns.md 常见问题速查表:

| 现象 | 排查方向 | |---|---| | [object Object] | 看 [RESP] 状态码;若 422 → 抓 [REQ-BODY] 是否空 → 对象展开丢键 / new Error(数组) | | 排版错乱/TBLSEP | 用 Node 单独跑渲染函数 + 真实 LLM 输出断言 | | 流式闪现 | 检查 SSE 事件粒度,thinking 只显示动画 | | 部署未生效 | md5 对比 + HTTP 抓取 + 服务重启(Python 必须重启进程)| | 数据偶尔丢失/消失 | 持久化类 bug(§12)grep localStorage 全部读写点 → 判定单例还是实体集合 → 序列推演(A存→B存→A读)→ 无 clear/removeItem 即非"被清掉",而是单键互斥覆盖 | | 地图/图表配色不生效(一片蓝/默认色) | ECharts map+visualMap 配色陷阱(§14):数据项设了 visualMap:false 会令其 itemStyle.areaColor 失效、fallback 默认蓝 #5470c6 → 移除 visualMap:false,让 visualMap continuous 正常映射 + 动态 min/max;必须像素级采样(getImageData)验证实际渲染,不能只看 getOption 配置 |

Step 3: 修复 + 同一脚本复测

  • 修复代码后重跑同一测试脚本,确认:
    • [REQ-BODY] 非空
    • DOM 快照断言通过(无 [object Object]、table 数量正确)
    • 无 console/pageerror
  • 截图人工确认视觉效果(保存于 ui-shots/
  • 修复后必做举一反三(§11):同类问题是否在代码其他位置/场景同样存在(同函数不同分支、同模式不同位置、同数据不同入口、同 UI 不同视图),一并修复或列待办

Step 4: 强断言检查清单(防假阳性)

测试通过 ≠ 功能正确。三类陷阱(详见 references/bug-patterns.md §8、§9、§10):

第一类(元素层)

  1. className ≠ 布局:断言 offsetWidth/Height 真实尺寸,不只断言 className
  2. innerText ≠ DOM 结构:断言 querySelectorAll("table").length >= 1,不只断言"innerText 包含"
  3. 测试场景 ≠ 真实路径:用"实际传递给被测代码"的输入(如 slice(0,8) 截断后的文本),不只测完整数据

第二类(体验层,更隐蔽): 4. 只测最终态漏测过程态:流式/异步/动画功能必须多次采样断言渐进变化(15s × 6),不能只等 100s 测最终 5. 元素存在 ≠ 用户看得到:断言 scrollHeight <= clientHeight(无 overflow 裁剪),关键结论在视口内 6. 弹性布局只测单点:用长/短两种内容量各测一遍,断言折叠/展开行为 7. 前后端契约不同步:改后端提示词后必须用真实输出全链路验证,断言新小节映射生效

第三类(滚动/交互层): 8. 嵌套滚动容器错位:监听、赋值、测试必须都指向真正可滚动的元素(CSS 有 overflow-y:auto 且内容溢出);外层 overflow:hidden 的 scrollTop 恒为 0,赋值无效 9. 测试操作了错误的元素:若被测元素 scrollTop 恒 0,任何"保持 0"断言都是假阳性——断言前先验证 scrollHeight > clientHeight(元素确实可滚动、内容确实溢出) 10. 滚动三态(通用必测项):流式增量渲染区(思考区/日志区/聊天流/流式正文)只要做了自动跟随,实现必做三态——自动跟随 / 手动滚动后不拉回 / 滚回底部恢复跟随;只做自动跟随视为未完成,三态都要测(详见 streaming-ui.md §2)

通用铁律:断言目标必须是用户实际感知的体验(看、点、读、确认结果),而非"代码做了什么"。

修复后必须逐项检查:

| 类型 | 弱断言陷阱 | 强断言方式 | |---|---|---| | 布局/尺寸 | className.includes("...") | offsetWidth/Height ≈ window.innerWidth/Height | | 结构渲染 | innerText.includes("...") | querySelectorAll("table").length >= N | | 截断边界 | 用完整输入测 | 用真实截断数据测(slice/cutoff) | | 过程态 | 只测最终态 | 流式/异步多次采样断言渐进增长 | | 可见性(内容) | 元素存在 | scrollHeight <= clientHeight 无裁剪 | | 可见性(显示/隐藏) | 只看 el.hidden 属性(假阳性,本项目漏检教训) | offsetParent === nullgetComputedStyle().display === 'none'.btn{display:inline-flex} 会覆盖 hidden,属性 true 但视觉仍显示;代码层用全局 [hidden]{display:none!important} 兜底) | | Markdown 标题 | 渲染器支持 ### 就认为 OK | 解析必须逐行扫描(标题后紧跟内容无空行时按块判断会吞掉标题行 → 原样输出),单测覆盖"标题+无空行内容"输入 | | 弹性布局 | 单点内容量 | 长/短内容各测,断言折叠/展开 | | 前后端契约 | 固定 mock 数据 | 真实后端输出全链路,断言新映射生效 | | 嵌套滚动 | 操作外层容器 | 监听/赋值/测试指向同一可滚动元素,先验证 scrollHeight > clientHeight | | 滚动三态(通用必测) | 只做自动跟随 / 只测"保持 0" | 实现三态(跟随/不拉回/恢复)+ 三态全测,断言真实可滚动元素 | | 图表初始化时机 | 只查 DOM 存在 | 渲染元素实际宽度 = 容器宽度(防隐藏容器内 init 退化为默认尺寸,如 ECharts 100px) | | 字体/尺寸一致性(同页图表) | 只查某卡正常 | 断言 legend/axis/label fontSize 与同页其他图一致(如统一 11)、关键卡片 offsetHeight 一致(如统一 300,不因 sm 类残留 240) | | 图表配色/视觉映射 | 只看 getOption() 配置(配置暖色 ≠ 渲染暖色,§14 教训) | canvas.getContext('2d').getImageData(x,y) 采样像素,断言是目标色而非默认蓝 #5470c6;hover 高亮用 dispatchAction({type:'highlight'}) 后采样验证 | | 可点击 | el 存在 | getBoundingClientRect() 在视口内 | | 网络成功 | resp.ok | 同时检查 Content-Type 和可解析 body |

专项测试场景

场景 A: 弹窗/模态框

node ui_test.js <url> --login-user U --login-pass P \
  --click-selector "#btn-open-modal" --wait-ms 3000
# 断言: modals >= 1, 弹窗内无 [object Object], 截图

场景 B: SSE 流式渲染(AI 对话等)

node ui_test.js <url> --login-user U --login-pass P \
  --click-selector "<触发流式的按钮>" --watch-api "<流式接口路径片段>" --wait-ms 100000
# 断言: REQ-BODY 非空(关键!); 完成后表格数>0; 无 TBLSEP/###/[object Object]
# 注: 流式接口响应体无法用 resp.text() 读取(已消费), 依赖截图+DOM 验证

场景 C: 页面功能回归(列表/表单/跳转)

node ui_test.js <url> --click-selector "nav a" --click-selector "#search-input" --wait-ms 3000

场景 D: 多轮追问/对话(追加不覆盖,防"问 A 答整份报告")

首轮完成后,在追问框发新问题,断言(详见 streaming-ui.md §8):

  • 原报告卡片数不变(未被清空重建)
  • 追问回复以新卡片追加,且非完整模板(卡片数/小节数/长度显著小于首轮)
  • 用户气泡包含问题文本;本轮状态条结束态正常
// 追问前记录 → 追问后断言(示例,以你的实现为准)
const before = await page.$$eval('#cardsWrap .card', els => els.length);   // 首轮卡片数
await page.type('#followInput', '你的追问');
await page.click('#followBtn');
// 等待本轮状态完成 → 断言 before 不变、新增卡片 < 首轮、气泡文本匹配

场景 E: 思考面板内容净化(用户 facing 界面,防工具独白泄漏)

触发流式后,mid_stream 阶段断言思考区(用户视图)DOM(详见 streaming-ui.md §1.1.1):

  • 不泄漏工具调用独白:无工具名/参数原文(如 analyze_image(image_urls_list=S3 URL,以你对接的平台/工具为准)
  • 只显示预设干净步骤文案(如"正在分析…""正在检索…");未知片段显示通用兜底文案而非原文
  • 若实现了开发者视图开关:切到原始视图应能看到 thinking 原文累积(对照确认净化仅在用户视图生效)
// 示例断言:思考区文本不含技术细节原文(以你的实现为准)
const thinkText = await page.$eval('#think-body', el => el.textContent);
if (thinkText.includes('analyze_image(') || thinkText.includes('image_urls_list=')) {
  throw new Error('思考面板泄漏工具调用独白');
}

场景 F: 流式滚动三态(通用必测项,适用所有流式增量渲染区)

任何"内容持续增长、自动跟随底部"的滚动区(思考区/日志区/聊天消息流/流式正文)都必须实现并验证三态,缺一不可(实现骨架与断言详见 streaming-ui.md §2):

  • 自动跟随:内容溢出后 scrollTop 接近底部(scrollHeight > clientHeightscrollTop >= scrollHeight - clientHeight - 阈值
  • 手动不拉回:滚到顶部 + dispatch scroll → 后续内容增长后 scrollTop 仍≈0(不被拽回)
  • 回底恢复:滚回底部 + dispatch scroll → 后续内容到达恢复跟随到底
// 三态断言骨架(以你的实现为准;先验证 scrollHeight > clientHeight,防"保持 0"假阳性)
await page.waitForFunction(() => {
  const c = document.querySelector('#stream-area');   // 换成你的流式区容器
  return c.scrollHeight > c.clientHeight + 10;        // 元素确实可滚、内容确实溢出
});
// 态1 自动跟随 → 态2 手动不拉回 → 态3 回底恢复:逐态断言(断言式见 streaming-ui.md §2 表)

场景 G: 数据分析问答/AI 对话 重试兜底与图表渲染(覆盖"模型退化 final_answer"假阳性)

数据分析问答类流式功能,除场景 B 的"最终出图表"断言外,还须验证重试兜底不会无限循环退化场景被兜底

  • 重试有上限(防无限循环铁律):连续制造"流结束无 final_answer 也无 error"的退化响应(mock 或弱模型触发),断言 askCalls <= 2(首次 + 至多 1 次重试);第 2 次仍失败即停止显示"未获得有效回答",不再发起第 3 次请求——调用方传入 retryLeft 逐次递减,retryLeft=0 停,绝不递归 / while 无限重试
  • 图表真实渲染:断言生成 <canvas>(ECharts/Chart.js)或 <svg>(D3)存在 + 实际宽度 = 容器宽度(防隐藏容器 init 退化);散点/柱状/堆叠等图类型按数据正确出图
  • legend 居中/明亮 + 字体一致:断言图例 left:"center"(居中)且色值非灰暗;图表 legend/axis/label 的 fontSize 与同页其他图表一致(如统一 11,避免某卡因 sm 类被压成 240px 高度、fontSize 10 显小)
  • 卡片尺寸一致性:断言关键卡片 offsetHeight 与同页其他卡片一致(如统一 300px,不因历史 sm 类残留 240px)
// 伪代码:退化重试上限断言(以你的实现为准)
const calls = await page.evaluate(() => window.__askCalls);
if (calls > 2) throw new Error('重试超过上限,疑似无限循环');

自定义测试脚本

模板不满足时(如需要多步交互、断言特定 DOM),直接写一次性 puppeteer 脚本(参考 scripts/ui_test.js 结构):

  1. page.on('pageerror') + page.on('console') 收集错误
  2. page.on('request'/'response') 按 URL 片段过滤捕获 API
  3. page.evaluate() 执行交互与 DOM 断言
  4. page.screenshot() 保存证据
  5. 输出结构化结果(JSON 格式便于解析)

关键 API 速记:

  • page.type(sel, text) 输入
  • page.click(sel) / page.evaluate(s => document.querySelector(s).click()) 点击
  • page.$eval(sel, el => el.outerHTML) 抓 HTML
  • page.evaluate(() => document.body.innerText) 抓文本
  • page.screenshot({path}) 截图

Resources

  • scripts/ui_test.js — 通用测试模板(登录/点击/API 捕获/DOM 断言/截图,命令行参数化)
  • references/streaming-ui.mdSSE 流式输出前端呈现与验证(三层呈现架构 + 打字机 / 智能滚动三态(流式区通用必测:实现必做+验收必测) / 关闭弹窗不中断与重开恢复(DOM 解耦)/ 图表容器可见后初始化 / 思考面板净化双视图(用户净化 + 开发者原始开关,§1.1) / 思考过程步骤化 + 折叠展开(提炼呈现:步骤卡片时间线 + 每步详情默认折叠、点击展开,§1.1.2) / error 事件不中断(§5.2:error 可能是过程性失败、智能体重试后继续,前端只记录不中断、以是否到达 final_answer 为成功标准,禁止一收 error 就中断——真实踩坑) / 多轮追问追加不覆盖(§8) / 真实浏览器渲染验证铁律 / 联调期前端资源缓存失效(?v= + no-cache,§5.1) / 验证清单;本文件只覆盖"流到了前端怎么渲染、怎么验证",接口/协议层(事件类型、会话 id、请求体构造)见你所对接平台的集成技能或接口文档;已按去项目化规则泛化,供对接任意 SSE 流式接口复用)
  • references/bug-patterns.md — UI 常见问题根因速查 + 反思沉淀(§1-7 速查:[object Object]/TBLSEP/###(§3:标题后紧跟无空行内容时必须逐行扫描,块级判断会吞标题行)/流式闪现/部署未生效/排查命令/方法论核心;§8-12 反思:三类假阳性、四类隐蔽假阳性(§9.2.1 新增:hidden 属性≠视觉隐藏,须用 offsetParent/getComputedStyle 断言,禁止只看 el.hidden——真实漏检教训)、嵌套滚动容器错位、举一反三同类排查持久化类 bug 单键互斥覆盖§13 图表隐藏容器内 init 退化为默认尺寸(与 streaming-ui.md §4 同源)§14 ECharts map+visualMap 配色陷阱:数据项 visualMap:false 令 itemStyle.areaColor 失效、fallback 默认蓝 #5470c6(配置≠渲染,必须像素级采样验证,不能只看 getOption)§15 ECharts map 交互禁用三层:silent 只禁事件、emphasis 高亮需单独 disabled、系列级会误伤须数据项级§16 百分比尺寸图表:多图对齐依赖容器等宽(复用不对称 grid 类致图大小不一)§17 CSS transition 阻塞卡中间态:元素"空白/消失"假象
  • references/90-qc-ground-truth.md【质控专用,非使用教程】:领域事实清单/复查清单(15 条核心结论 + 已发现问题记录),本技能逻辑复查的领域锚点;质控方法论详见独立技能 skill-qc(L0~L4),质控时加载 skill-qc 执行;正常使用本技能无需阅读,仅在排查/修订/复查时对照使用