Communitygithub.com

undefine-org/ux-from-use

将用户在使用应用时遇到的问题(如错误报告、摩擦点、疑惑)转化为产品应建立的体验,找出结构性问题,修复它,在真实的浏览器中用 Beam Lisp 的浏览器命名空间亲自测试,并将过程录制成视频场景。

ux-from-use 是什么?

该技能提供了一套完整的工作流程,用于将用户报告的“症状”(如Bug、体验问题)转化为产品实际的体验改进。其核心思想是,报告只是一个表象,背后是用户体验的结构性原因。该技能引导用户先写出体验描述(句子),然后找到允许该症状产生的产品结构原因并进行修复,最后在真实的浏览器环境中使用 Beam Lisp 的浏览器命名空间亲手测试并验证。它支持将手动测试步骤转化为可直接重放的自动化场景,并可将 Agent 的模型决策录制在磁带上,实现低成本、高可靠性的回归验证。此技能专为 Beam Lisp 和 Knowledger 项目设计,但其中的原则(如面向用户、从结构入手、保持实况等)具有普遍适用性。

兼容平台~Claude Code~Codex CLI✓Cursor
npx skills add https://github.com/undefine-org/enchanted-skills/tree/main/ux-from-use

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

从使用到体验

一份报告只是给出了症状。真正需要处理的是症状背后的体验。「对话转向正题时客户消失了」是一个 Bug。「一次对话会聚集其涉及的内容:当正题被添加时,客户会留在对话中」才是要构建、验证和录制的目标。先写出这句话。然后修复允许症状存在的结构,再通过使用产品来证明这句话。

循环

  1. 收集使用情况。 阅读报告,然后阅读真实使用的痕迹:对方的对话、日志、记录、状态、他们接触过的文件。这些痕迹里蕴含着没人写下来的期望(「他们进展到哪了?」期望的是具体的要点本身,而不是一个摘要)。
  2. 写下体验。 每个报告对应一个句子,使用现在时,从对方的角度出发,不包含任何技术实现词汇。保留这个列表。它会变成影片的旁白和验证点(claims)的消息。
  3. 找到结构性原因。 询问为什么产品会允许这种事情发生。一个原因通常能解释多个报告(例如,身份从显示名提取;两个计数使用了同一个词;渲染器在插入时丢弃了一个类)。修复原因,而不是修复显示那个问题的界面。
  4. 亲身体验。 通过浏览器命名空间(如下所示),以每个相关角色的身份,在桌面端和手机端宽度下操作真实的应用程序。查看截图。统计页面实际显示的内容,而不是代码的意图。
  5. 保存它。 将手动步骤提升为一个场景:动作变成事件,检查变成断言(claims),体验句子变成该场景的旁白。将模型生成的回合录制在磁带上,这样影片可以在几秒钟内以零成本重放。
  6. 报告体验,而非补丁。 在摘要中,首先说明用户将会看到什么,然后是原因,接着是影片以及如何重放它。

团队风格(Othman 反复提出的要求)

  • 更少的文案。 状态应该通过绘图来呈现:色调、粗细、位置、一个圆环、一个标记。如果需要一个句子来解释某个界面,说明这个界面本身有问题。
  • 一个概念用一个词。 维护一个词汇表并坚持使用它:一个「标准」(与流程对比评判)永远不能被称为一个「要点」(由人决定的东西)。如果两个计数共用一个词,人们就会把它们加起来。
  • 事物是累积的。 标签是累加的(先是种类,然后是客户,然后是事项)。当前选中的标签是实心的,其他的保持为轮廓。永远不要替换用户已经看到的上下文。
  • 数字优先于文字。 历史记录打开时首先显示图表,每个系列一个,并带有数值标签。文本放在下面,每个事件一行,按需展开。
  • 每个引用都指向其所在位置。 一个段落编号会滚动到该段落并将其高亮。一个引用会找到其对应的原文。滚动条用相应的颜色标记出内容所在的位置。
  • 默认保持实时。 没有完全重载。一个人做出的更改会立即显示在所有打开的页面上。在一个慢速页面上重复点击不能重复效果:操作必须是幂等的。
  • 控件出现在其所控制对象的旁边, 并且只对有权使用它们的人显示。只读用户只看到状态,看不到控件。
  • 知道谁在那里。 身份和角色信息应触达每个界面和每个 Agent。不要告诉一个合作伙伴「合作伙伴必须确认」。
  • 诚实的边界。 清晰地说明产品尚未实现什么。永远不要指向一个不能完成该任务的界面。
  • 手机端宽度是首要的阅读器: 16px 内边距,无水平滚动(使用 minmax(0,1fr) 而非 1fr),并且账户信息始终可访问。
  • 使用产品自身的语言制作线框图和旁白(对于 Knowledger 项目,使用法语)。

使用浏览器命名空间进行手动测试

Beam Lisp 的原生浏览器(位于 beam-lisp 代码树中的 optional/browser-port)在私有的无头 Sway 窗口中运行有界面的 Chromium,支持可信输入和持续屏幕录制。它和场景驱动的对象是同一个,因此手动会话可以直接演变为测试,无需重写。对于快速查看,使用 agent-browser。对于任何值得保留的测试,使用浏览器命名空间。

每台机器只需执行一次

cd ~/projects/beam-lisp/optional/browser-port
nix develop --command cargo build --release --locked --features native-browser
nix build .#runtime-wayland --out-link ~/.local/share/bl-browser/runtime-wayland   # chromum, sway, wayvnc
command -v ffmpeg || nix profile add nixpkgs#ffmpeg

使用 bl run -p ~/projects/beam-lisp/optional/browser-port/bl …(或 bl test -p …)来运行任何使用了它的代码。

探针(probe):一个文件,一个人,真实输入

templates/probe.bl 是起点。其核心代码如下:

(ns probe (:require [browser.page :as page]))
(def rt (str (System/get_env "HOME") "/.local/share/bl-browser/runtime-wayland/bin/"))
(def p (page/open! {:executable (str (System/get_env "HOME") "/projects/beam-lisp/optional/browser-port/target/release/bl-browser-port")
                    :width 1440 :height 860
                    :launch {"profile" "/tmp/probe/profile" "runtime" "/tmp/probe/runtime"
                             "chromium" (str rt "chromium") "compositor" (str rt "sway") "vnc" (str rt "wayvnc")
                             "width" 1440 "height" 960}}))
(try
  ;; who is looking: the identity the proxy would add (here Tailscale headers); it reaches the live socket too
  (page/cdp! p "Network.enable" {})
  (page/cdp! p "Network.setExtraHTTPHeaders" {"headers" {"tailscale-user-login" "[email protected]" "tailscale-user-name" "timinou"}})
  (page/record! p "/tmp/probe/take.mp4" {:fps 24})          ; optional: the whole session as a movie
  (page/navigate! p "http://127.0.0.1:24025/a/sahel-…")
  (page/wait! p "document.documentElement.dataset.live==='1'") ; a JS predicate, 30 s bound by default
  (page/click! p ".rv-rail a.para[data-goto]")               ; trusted pointer: moves, presses, releases
  (println (page/evaluate! p "scrollY"))                     ; ask the page what a person would see
  (page/screenshot! p "/tmp/probe/after.png")                ; then LOOK at it
  (page/stop-recording! p)
  (finally (page/close! p)))

可用的动词是 navigate! wait! click! hover! fill! key! drag! evaluate! screenshot! record! stop-recording! show-cursor! cdp!。cdp! 可以调用 Chromium 提供的任何其他功能:视口、网络、Cookie、仿真(Emulation.setDeviceMetricsOverride 用于模拟手机)。

使探针值得保留的习惯

  • 询问页面,而非代码。 每个检查都是一个 JavaScript 表达式,当用户能看到该事物时返回 true:行数、标签的文本、scrollY、指针下元素的类名。这些表达式可以原封不动地变为断言。
  • 按含义选择。 在测试需要的地方,给应用提供稳定的数据属性(例如 button.cell[data-person='Karim Saadi'][data-team='Associés']),永远不要使用「第五个按钮」。
  • 扮演每一个人。 为每个人打开一个页面,带上他们的身份。将两个页面并排放置,这是证明「实时」的方式:一个页面有操作,另一个页面无刷新地随之变化。
  • 检查首次渲染和补丁更新后的页面。 实时应用先渲染一次 HTML,然后通过数据补丁更新。Bug 常隐藏在这两者的差异中(例如,插入时丢失的类,以标签名读取的列表开头的文本)。通过 URL 访问每个页面一次,然后再通过导航到达它一次。
  • 等待页面变为实时状态,而不仅仅是连接完成。 实时客户端可能会用其 socket 的首次渲染替换服务器渲染的页面。在这之间键入的文本会丢失,这对于测试和对于加载后快速键入的用户来说都是如此。应用程序应该保留跨挂载的输入内容,然后声明自己已就绪(data-mounted)。测试应等待这个标志。
  • 检查手机端宽度。 在 390px 宽度下检查 document.documentElement.scrollWidth === innerWidth,然后查看截图。
  • 永远不要触及真实数据,并证明这一点。 将应用指向其数据的 reflink 副本(cp -a --reflink=auto)和一个备用端口。然后在使用它之前证明数据隔离:在副本中重命名一个东西(一个金丝雀测试项,例如将一个团队名改为「AssociésX」),对其操作,然后检查副本已更改而真实文件的时间戳未变。对于每种写入进程(页面处理器、实时 socket、后台任务),都做一次这个检查。在 Beam Lisp 中,从测试中使用的 System/put_env 能到达页面处理器,但无法影响实时 socket:一个读取未配置数据文件夹的 socket 会回退到默认路径,即真实数据。应用必须在服务器启动时将其配置锁定到整个 VM(使用 persistent_term)并从此处读取。如果必须触及实时数据,请为文件创建快照、操作、通过 diff 确认只有你的更改,然后恢复。

从探针到影片

一个 scenario.demo/defdemo 就是一个普通的测试(bl test)。步骤映射关系如下:

探针(Probe)场景(Scenario)
(page/navigate! p url)(s/does :othman [:navigate url])
(page/wait! p js)(s/does :othman [:wait js])
(page/click! p sel) / hover! / fill! / key![:click sel] [:hover sel] [:fill sel text] [:key "Enter"]
留给人眼观看的暂停[:pause ms]
必须为真的 (page/evaluate! p js)(s/sees :othman (fn [p _] (= true (page/evaluate! p js))) "the experience")
一个人物browser.film/owned-worlds 中的一个 actor,包裹了用于设置其身份的代码
一段旁白(demo/scene :id {:title … :say … :capture :video} events…)

templates/feature-demo.bl 是一个完整的文件:应用数据的隔离副本、在测试 VM 的备用端口上启动的应用、带有身份的录制世界、当没有语音键时的静音轨道,以及一个包含三个场景的演示。工作示例:knowledger-interface-review/apps/practice/test/features-demo.bl(五个影片:审查、历史、片段、两人之间实时变更的团队、一段录制的对话)。

将场景写成体验,而不是修复:「每个要点都通向其正确的位置」,而不是「修复 §-链接处理器」。旁白就是文档。

模型决策在磁带上的记录

Agent 所做的任何事都应该经过应用自身的工具门,这样只有模型的决策才被认为是模型的。一个磁带(tape)记录一次模型运行的决策:它发出的工具调用序列及其最终答案,由模型、系统提示和提示(经过掩码处理时间戳、纪元编号和十六进制 ID)的哈希值索引。

  • record:重放磁带上已有的内容,对于磁带没有的部分则运行模型,并保存结果。
  • replay(默认):不在磁带上的运行是一个错误回合。重放永远不会静默地回退到模型。
  • 重放通过同一个工具门发出相同的调用,并且带节奏控制,因此产品会再次执行这些工作,界面会显示出来,这一切在几秒钟内免费完成。

锁定所有会进入提示的内容(如公司名称、日期等),否则磁带键会漂移。提交磁带文件。参考实现:apps/practice/src/practice/cassette.bl 及其在 conversation.bl 中的两个钩子(turn 和 door)。

运行和渲染

bl test -p ~/projects/beam-lisp/optional/browser-port/bl app/test/features-demo.bl             # 重放,验证,捕获
DEMO_RECORD=1 bl test -p …                       # 将缺失的模型决策录制到磁带(需要模型凭证)
BL_DEMO_EXPORT=1 bl test -p …                    # 同时渲染 MP4 + 字幕 + 每个影片的章节

每个影片在其固定装置(fixture)中清除上一次的录制,因为录制永远不会覆盖已有的录制。保持每段旁白在两行字幕以内(大约 130 个字符),否则渲染器会拒绝。负载过重的机器可能会使屏幕录制编码器资源不足:请检查 frames/recordings.json,其中 encodedFrames 应约为 秒数 × fps,如果某段录制过短,请重新运行。

渲染(scenario.film/export!)需要 Python 3 和带有 libass 的 FFmpeg。当设置了 DEEPGRAM_API_KEY 时,旁白会被朗读出来。否则,每个场景获得一个长度与其字幕阅读时间相匹配的静音轨道。永远不要假设语音键存在。在本地环回地址上使用 (scenario.demo/serve! dir {:port 4163}) 浏览影片,或使用 work serve 发布。

在说完成之前

  • 每个报告项都包含其体验句子、原因以及在真实浏览器中证明它的断言(claim),并且是以每个相关人的视角。
  • 桌面端和手机端宽度都已查看,通过截图,而不仅仅是断言。
  • 对于每个触及的界面,首次渲染和补丁更新后的页面都已检查。
  • 实时行为通过两个人同时操作证明(在相关的地方)。
  • 测试过程中没有修改真实数据。任何被触及的数据都已恢复,并显示了 diff 记录。
  • 影片从磁带重放,无需网络连接。磁带和场景文件已提交,渲染产物不提交。
  • 摘要以用户现在将会看到什么开头。

Individual skills in this repo

This repo contains 1 individual skill — each has its own dedicated page.

相关技能