LobeHub UX 审计实战:以三层证据法审修 HomeInbox「需要你处理」错误简报卡片
本文以 LobeHub 仓库中一次真实的 UX 审计为例,完整复盘对 Home Inbox「需要你处理」(needsYou)栏错误简报卡片(src/features/HomeInbox)的审计过程:从表面类别规范出发,逐层核对静态代码、真实渲染证据与动态旅程缺口,最终定位 5 个体验缺口并回灌到 ux 检查清单。读完后你将掌握一套可重复执行的界面审计方法——如何判断一张错误通知卡片「读起来是否像错误」,如何把确定性失败映射到真正的修复动作(而非徒劳的重试),以及如何用 file:line 级证据驱动修复落地。
一、审计对象:一次真实的错误卡片运行
这次审计由用户实际看到的一段原始字符串触发,渲染后的卡片头部显示:
T-1 topic #1 (tpc_5UBuAjUU4z6B) error / Execution failed: Workspace budget exceeded
对应的表面(surface)是「需要你处理」列中一张 error 类型的简报卡片,其结构为:元信息行(StatusGlyph 状态图标 + 任务引用 + 任务名 + 时间)→ Agent 头像 + 标题 + 摘要 → 操作行(忽略 / 重试)。
审计记录本身保存在 home-inbox-error.md,是 ux-audit 技能(SKILL.md)下 references/example/ 目录中一份标准的「worked example」——每次审计完成后都会归档为这样的模板,供下一次审计参照。
1.1 三层证据法:L1 / L2 / L3
ux-audit 技能的核心设计是「分层取证」:一个结论只能来自真正能"看见"它的层。三层定义如下(出自 SKILL.md):
| 层 | 做什么 | 能抓住什么 | 成本 |
|---|---|---|---|
| L1 静态 | 读代码 | 缺失的状态/分支(empty/error/retry)、缺失的模式、结构性问题 | 低、离线,每次必跑 |
| L2 视觉 | 渲染表面的截图 | 真实视觉层级与主控件、间距/对比/对齐、截断溢出、空态/加载/错误长什么样 | 中;需要渲染环境 |
| L3 动态 | 驱动真实用户旅程 + 度量 | 进行中/锁定状态、强制错误/空态、步骤衔接、焦点/键盘、CLS/LCP/INP 量化 | 高;需要运行环境 + 登录态 |
本次运行的层覆盖为:L1(静态/代码)✅;L2(视觉)✅——用户提供的渲染卡片截图即为真实渲染证据;L3(动态)⏳ 未执行——要复现它需要把一个任务强制推到终止性错误状态(见本文第五节,L3 会补充什么结论)。
这个分层不是为了流程仪式感。SKILL.md 中的核心规则是「evidence, not vibes」:每条发现都要引用它的证据——file:line(L1)、经过验证的截图(L2)、或捕获到的数值/快照(L3)。错误的「它缺失了」比没有发现更糟,因此「从代码打勾一个视觉结论」是被明确禁止的。
二、先给表面定类:一张成熟的错误通知卡片应该具备什么
在开始读代码之前,审计先回答了「这张卡片属于哪一类表面」。该卡片是一封收件箱中的可行动错误通知,同类成熟产品包括 GitHub Actions 失败运行、Vercel 失败部署、Sentry 问题通知、Linear 通知。原文档给出了这一表面类别的 8 条规范基准,审计差距全部对照这份清单来量,而不是只对照代码:
- 一眼读起来像错误(颜色/图标的严重度,而非仅靠文字);
- 用人的话说清什么失败了、哪个实体、何时发生;
- 说明为什么(原因),在可能时映射到已知/可行动的原因;
- 提供恢复路径(Retry);
- 提供关闭路径(Ignore);
- 提供查看路径(打开失败运行/日志);
- 当原因是用户可行动时(如预算),提供直达修复的路径(充值/升级);
- 提供反馈/上报通道。
这条「先定类、再审计」的规则在 SKILL.md 中被称为 ground rule:读代码只能暴露「我们建了什么」的缺陷,结构上对「我们压根没建的能力」是盲的——一个完全缺失的 affordance 没有 file:line 可 grep。因此必须先写下该表面类别的「期望能力清单」,再对着它审计差距。
三、模式盘点:卡片实际使用了哪些模式
对照 Jenifer Tidwell《Designing Interfaces》模式语言,本次审计盘点到的模式如下(完整表格见 home-inbox-error.md 第 1 节):
| 模式(家族) | 位置 | 评级 | 备注 |
|---|---|---|---|
| Card Stack / Titled Sections(布局) | 「需要你处理」下的 needsYou 列表(HomeInbox/index.tsx) | ✅ | 分组展示,错误排在最后(splitBriefs.ts) |
| Failure + Retry(反馈) | 错误简报 → 重试(BriefCardActions.tsx) |
⚠️ | Retry 存在,但对确定性(预算)原因是裸重试 |
| Button Groups(动作) | 忽略 / 重试 |
✅ | 关闭 + 恢复动作分组在一起 |
| Same-Page / Inline error(反馈) | 错误在卡片内渲染,而非整页 wipe | ✅ | 收件箱其余内容保持完好 |
| Status glyph / severity(反馈) | 元信息行 StatusGlyph(StatusGlyph.tsx) |
⚠️ | 错误任务状态为 paused/scheduled → 渲染为手/中性图标,而非失败 |
| Overview + Detail(数据) | 「View run」→ 主题抽屉 | ⚠️→✅ | 曾死链(简报无 topicId);本次运行已接通 |
| Error-message copy(反馈 §4.5) | 标题 + 摘要(InboxBriefCard.tsx) | ⚠️→✅ | 曾是带原始 id 的日志风格;本次运行已修复 |
| 可行动原因的修复路径 | 预算超限 → 充值/升级 | — | 缺失——没有指向计费的链接(缺口②) |
读法(Read):卡片的骨架(分组、关闭/恢复按钮、卡内渲染)是健全的;薄弱点完全聚集在 Feedback 家族——失败「读起来是什么样」(文案、严重度图标),以及所给的动作能否真正解决原因。
四、亮点:这些行为不允许回归
ux-audit 要求审计必须「report the good, not only the gaps」——只列缺陷的审计已经漂移成 bug report。本次运行确认了三处 ✅ 亮点:
4.1 错误是一等公民、且被显式排序的桶
splitBriefs.ts 将 decision / error 型简报路由进 needsYou,且错误永远沉底——卡住的决策此刻正阻塞 Agent,而已失败的运行已经停下、可以等一等:
const NEEDS_YOU_ORDER: Record<string, number> = {
decision: 0,
error: 9,
};
splitBriefs() 用稳定排序在服务端既有的「优先级 + 时间」顺序之上做桶内重排,因此一次失败运行总是浮现在「需要你处理」列,而不是被新闻流静默吞掉——收件箱永远不会悄悄咽下一次失败。这一点有配套测试 splitBriefs.test.ts 守护。
4.2 卡内恢复 + 关闭,不掀翻表面
重试 / 忽略 在 BriefCardActions.tsx 中就地解决,收件箱其余内容保持完好——这是一个做对了的 Same-Page error:失败把控制权交还给用户,而不是把整个表面炸掉。
4.3 错误文案已修复为面向人(→ 已回灌 ux §4.5 ✅)
修复后的行为:标题在卡片侧本地化(t('inbox.error.title')),原因显示为干净的从句,topic id 移入结构化的 topicId 字段并由此点亮「View run」——这正是新的 Feedback §4.5 规则旁边引用的 ✅ 示例。
⚠️ 复查捕获(Review catch)——本地化标题必须限定作用域,不能一刀切。 首次实现对所有
type: 'error'简报覆盖了标题,但仓库中 error 简报的生产者有 5 个,其中 4 个携带各自合法的独立标题:verify 类(failed verification/verification errored,见 verify/settle.ts)、心跳类(heartbeat timeout)、agent-signal 类(brief.agentSignal.selfReview.error.title——服务端已本地化)。一刀切的「运行失败」会把这些全部抹平。修复:把覆盖限定到运行失败简报(服务端存稳定的英文<id> run failed,客户端只本地化这一个,否则回落到存储的标题)。留给检查清单的教训:在按
type覆盖某个共享记录类型的用户可见字段之前,先枚举该类型的所有生产者——一个type很少是单一来源。
五、体验缺口(按严重度排序)
缺口① 日志风格文案夹带内部 id —— ux §4.5(Meaningful)🟠【本次已修复】
原始标题模板 `${taskIdentifier} topic #${seq} (${topicId}) error` 渲染出 "T-1 topic #1 (tpc_5UBuAjUU4z6B) error":标题里带原始 topic id、topic #N 日志语法、T-1 与元信息行重复;摘要 `Execution failed: ${raw}` 带着控制台行前缀;且两者对所有 locale 都是英文。证据来自 L1(taskLifecycle/index.ts 的 error 简报生成)+ L2(截图)。
修复(已完成):在卡片侧本地化框架文案,存储干净的英文回落文案,把 id 移入结构化 topicId 字段。当前服务端实现可见于 taskLifecycle/index.ts:error 简报创建时 title: tHome('inbox.error.title'),并把结构化错误码写入 metadata: { error: { code: errorCode } },服务端还会把错误码映射到与聊天错误卡片相同的人读本地化消息(与界面共用同一份 error locale 文案来源,避免两处措辞漂移)。
缺口② 确定性原因只提供一个徒劳的 Retry —— ux §4.2(Meaningful・Certainty)🟠【已落地】
"Workspace budget exceeded" 是一个确定性的、用户可行动的原因——重跑同一运行只会再次撞同一堵墙——但卡片只给 重试 / 忽略,没有任何通往修复(充值/升级/计费)的路径。这正是第二节类别规范第 7 条的缺口。
修复(已完成):把已知终止性错误类型映射到修复动作。当前源码中,taskLifecycle/index.ts 定义了计费类错误码集合:
// Terminal error codes whose fix lives in billing, not in a retry — running the
// same task again just reproduces the same wall. For these the error brief leads
// with an "Upgrade" remedy instead of a futile Retry (ux Feedback §4.2).
const BILLING_ERROR_CODES = new Set<string>([
ChatErrorType.InsufficientBudgetForModel,
ChatErrorType.FreePlanLimit,
ChatErrorType.SubscriptionPlanLimit,
ChatErrorType.WorkspaceSubscriptionInactive,
]);
完成生命周期事件的结构化 errorCode 经 onTopicComplete 两个调用方透传进简报;计费类原因得到 upgrade 链接动作 + metadata.error.code。卡片侧配套修复见 BriefCardActions.tsx:当主动作是 link 类型时(如预算错误的「升级」),渲染为填充式主按钮并导航而非 resolve——「A link only navigates; it does not resolve the brief」,修复路径成为明确的 call to action,而 Retry 仅对瞬时原因保留为次级动作。已用 agent-testing T-220 验证:预算卡片渲染 忽略 + 升级方案 → 指向 settings/plans,不再出现 重试。
缺口③ 卡片「读起来不像错误」—— 严重度可读性(Certainty)🟠【已落地】
卡片唯一的状态线索是元信息行的 StatusGlyph,它由任务状态驱动——但出错时任务被设为 paused(ad-hoc 运行)或 scheduled(自动化的手动运行),而 StatusGlyph.tsx 源码注释明确写着:
Note `task:paused` deliberately renders
as the "waiting for human" hand: it means *pending review*, not "suspended".
也就是说:一次失败的运行与一次健康暂停显示同一个中性/等待图标——没有红色、没有告警图标(L2 截图确认了那个 🤚 就是这只「手」),卡片完全靠文案在说「这是错误」。
修复(已落地):对 type === 'error' 简报,在标题处渲染红色 CircleAlert。当前 InboxBriefCard.tsx 中可见对应实现(const isError = brief.type === 'error' 驱动错误强调样式),注释强调这是「the one true failure glyph, no extra icon set」。已用 agent-testing T-220 验证。
缺口④ 错误简报上的「View run」是死链 —— Overview + Detail(Meaningful)🟡【本次已修复】
BriefCardActions.tsx 中 showViewRun = !!taskId && !!topicId,但 error 简报的 create() 从未传 topicId——于是唯一能查看为什么失败的入口从未渲染:用户可以重试或忽略,却不能「看」。
修复(已完成):把 topicId 传入 error 简报使「View run」出现。agent-testing T-220 验证:两个携带 topicId 的简报都渲染出可见的「查看运行轨迹」入口;没有 topicId 的旧数据简报则不渲染——这也顺带成为 L2 对结构字段缺省行为的真实验证。
缺口⑤ 忽略 是永久关闭且无撤销 —— Act(Certainty)🟡【已记录,未落地】
忽略 调用 handleResolve('ignore') 直接解决简报,无确认、无撤销;误触关闭的错误就永久离开收件箱。严重度低(运行仍可在任务列表中看到),记录在案但不阻塞。
六、技能回灌:审计如何反哺 ux 检查清单
ux-audit 与 ux 技能构成一个闭环:ux 是审计的度量基准,审计是让 ux 保持诚实的机制。本次运行回灌的内容(SKILL.md 称之为「回灌」,且为强制步骤):
- 新增 ux Feedback §4.5 ——「错误文案写给人看,不是写日志行」:新小节 + 4 条检查项 + Quick-review 镜像,以本收件箱错误简报为 ❌ 示例、修复后为 ✅ 示例。覆盖:不含内部 id / 不含日志框架 / 不与元信息行重复 / 已本地化。
- 扩展 ux Feedback §4.2 —— 确定性原因条款:当一个失败的修复在别处(预算/配额/权限)时,应以修复动作领衔,而非裸 Retry;同步镜像进 Quick review。
- 验证既有规则:§4.2「失败状态要命名失败并提供 Retry」被该卡片实例验证(卡片确有 Retry),Retry 门控原则由缺口②扩展。
- 本次具体落地的修复:缺口①(文案)+ 缺口④(View-run 接线),并附带服务端回归测试 onTopicComplete.test.ts——断言简报标题/摘要不携带内部 id 或日志框架、
topicId落在结构化字段上。 - 后续(未落地):缺口②(修复动作映射)与缺口③(错误严重度图标)——建议挂在「HomeInbox UX」父 issue 下的子 issue。
值得强调的机制细节:回灌不止是「给规则贴一个 ✅/❌ 例子」。好的案例只有在其教会规则新东西时才值得落地——本次缺口②教会了 §4.2 一个新区分(「修复在别处的失败」vs「瞬时失败」),缺口①教会了 §4.5 四条可检查的子项,这正是审计让清单「比发现它时更锋利」的方式。
七、待办:L3 动态层会补充什么
L3 未在本次运行,原文档列出了它的计划验证项——这也展示了三层法各自的边界:
- 强制一个任务进入终止性 budget-exceeded 错误,并在线确认:(a)「View run」现在渲染(缺口④修复);(b) 卡片仍读起来像 pending 而非 failed(缺口③);(c)
重试重跑并立刻再次失败(缺口②——以行为方式证明「徒劳 Retry」论断)。 - 确认 zh-CN 会话下本地化标题渲染,以及携带旧
Execution failed:前缀的遗留行能走防御性剥离。
从源码结构看,服务端对自动化失败的容错由 taskLifecycle/index.ts 中的熔断器控制:AUTOMATION_FAILURE_FUSE = 3——连续 3 次自动 tick 失败才暂停任务/停止重新武装并让紧急简报浮现给人类;单次瞬时错误(429/网络/上游 500)不会永久停掉一个循环任务。这意味着 L3 强制复现时,触发条件本身就是「连续失败」语义,而非单点故障。
八、可复用要点:如何审计你自己的错误通知表面
这次 worked example 沉淀出的可执行清单:
- 先定表面类别,写下期望能力清单,再读代码——代码审计对「从未构建的能力」结构性失明。
- 结论必须来自能看见它的层:
file:line支撑 L1 结论,渲染截图支撑 L2 结论,强制状态/旅程驱动支撑 L3 结论;不要用 L1 打 L2 的勾。 - 错误卡片八问:像错误吗?说什么/哪个实体/何时?为什么?能恢复吗?能关闭吗?能查看吗?原因可行动时有直达修复吗?有反馈通道吗?
- 按
type覆盖用户可见字段前,先枚举所有生产者——type很少是单一来源。 - 确定性原因领修复动作,瞬时原因领 Retry——把错误码显式映射到动作(本仓库的做法:
BILLING_ERROR_CODES→upgrade链接 +metadata.error.code)。 - 报告亮点,而不仅报告缺口——亮点是「不要回归」清单,也是下次重构的承重墙;且只有教会规则新东西的好案例才值得回灌。
- 审计以「落地」为完成:具体 bug 修复或开 issue,可泛化缺口回灌检查清单,报告归档为下次模板——跳过闭环,审计就退化成一次性 review。
参考资料
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00