LobeHub UX 审计实战:代理档案页的三层审计方法与「回灌」闭环
本文以 LobeHub 仓库中一份真实的 UX 审计样例——代理档案/角色编辑页(Agent Profile)——为主体,完整还原 ux-audit 技能「三层审计 + 证据驱动 + 发现回灌」的工作方式:如何给页面做模式清点与缺口排序、如何用 file:line 级证据支撑每一条结论、以及审计结果如何回流到 ux 检查清单形成闭环。读完你可以掌握一套可复现的、以代码证据为基础的界面体验审计方法,并能对照当前源码看懂该页已落地与待验证的改进。
一、ux-audit:一个三层、证据驱动的审计框架
该样例隶属于仓库内的 ux-audit 技能定义。它的定位是对**单个页面(surface)**做一次可重复、基于标准的体验审查,基准是两样东西的组合:
- 模式语言——参考 pattern-catalog.md 中的界面设计模式目录,回答「这个页面用了哪些模式、用得多好」;
- 行为检查清单——
ux技能中各模块(Loading、Empty、Error、Retry、Draft safety 等)的检查项,回答「流程应该怎么表现、哪里体验偏弱」。
每次审计只做一个 surface,全应用横扫超出单次能力,逐页重跑才是「持续」的来源。审计分三层,按「哪一层真的看得见某类结论」来选择:
| 层 | 程序文件 | 做什么 | 能捕获什么 | 成本 |
|---|---|---|---|---|
| L1 静态 | layer-1-static.md | 读代码 | 缺失的状态/分支(empty/error/retry)、无草稿持久化、模式缺失、结构性问题 | 低,离线,每次必跑 |
| L2 视觉 | layer-2-visual.md | 渲染截图 | 真实视觉层级与主控件、间距/对比度/对齐、截断溢出、空/载/错态实际观感、响应式、深浅色 | 中,需要渲染环境 |
| L3 动态 | layer-3-dynamic.md | 驱动真实用户旅程并插桩 | 进行中/锁定态、强制触发的 error/empty 态、步骤衔接、焦点/键盘、量化的 CLS/LCP/INP/长任务 | 高,需要运行环境 + 鉴权 |
框架里有一条核心纪律(Coverage Matrix):任何结论必须来自能真正「看见」它的层——不能从代码里的 variant 属性打勾「只有一个主按钮」,视觉与运行时结论必须落到 L2/L3。分层的成本意识也写进了规则:L1 永远跑;发现集中在布局/层级/渲染态/响应式时加 L2;需要走完整旅程、强制 L1/L2 够不到的状态或测性能时才加 L3。
另外两条 ground rule 直接决定了这份样例的写法:
- 证据而非感觉(evidence, not vibes):每条发现必须引用证据——L1 是
file:line,L2 是已核对的截图,L3 是采集值/快照; - 对标 surface 的「类」,而不只是自己的实现:读代码只能发现「已建成的东西」的缺陷,对整个能力缺失是结构性盲的(没有
file:line可 grep)。所以审计前要先给出「这个类目的成熟产品(如 GPT 编辑器、Character.ai、Dify)在这个屏面上标配哪些能力」的期望清单,再逐项核对存在/缺失。
二、审计对象定位:代理档案页与它的「类基准」
这份 worked example 是 2026-07 对代理档案/角色编辑器的一次真实执行(内部编号 LOBE-11215),对应路由 /agent/:aid/profile,代码位于 src/routes/(main)/agent/profile/index.tsx/agent/profile/index.tsx)。文档明确声明:该样例是输出形态的模板,不是当前状态的事实快照——「代码在动,引用前先重新验证」,这个自我约束本身就是方法论的一部分。
该 surface 的组成(自外向内):
- 导航头:面包屑(
AgentBreadcrumb)+ 自动保存提示(AutoSaveHint)+ 状态标签 + More 菜单; - 档案编辑器:头像/名称/背景、模型与工具配置(或异构代理配置面板);
- 系统提示词富文本编辑器(Lexical,带 mention/表格/slash 插件);
- 常驻挂载的编辑锁驱动器(
EditLockDriver)与右侧 Agent Builder 协作副驾。
类基准核对(custom-agent/character editor 类的类规范,逐项对照):
| 类规范 | 核对结果 |
|---|---|
| 实时预览 / 试跑代理 | ✅ Agent Builder 副驾 + 相邻的 chat 标签页 |
| 版本历史 / fork | ✅ AgentVersionReviewTag / AgentForkTag |
| 协作编辑安全 | ✅ 编辑锁 |
| 可见保存态的自动保存 | ⚠️ 存在,但当时无法展示失败(缺口②) |
| 配置→回看数据的闭环 | ✅ More 菜单直达 /agent/:id/stats,无缺口 |
结论:该类的能力规范大体齐备,短板集中在失败处理,而不是能力缺失——这是审计报告开篇就给出的定性判断。
该次执行的层覆盖:L1(静态/代码)已完成(下节全部内容);L2(视觉)/L3(动态+CLS)当时未执行(见第八节待验证清单)。因此所有关于渲染的表述都是 L1 推断,标注「pending L2」。
三、L1 产出之一:模式清点表(Patterns in use)
L1 程序的第二步要求逐族走一遍模式目录,给每个块打上 Tidwell 模式标签并评级:✅ 扎实 / ⚠️ 部分或有误用 / — 缺失但预期。该审计的清点结果如下(引自样例文档,Where 列中的文件:行号是审计时点的证据位置,均相对 profile 特性目录):
| 模式(族) | 位置 | 评级 | 备注 |
|---|---|---|---|
| Visual Framework(布局) | NavHeader + WideScreenContainer 外壳(index.tsx:46,61) |
✅ | 一致的 chrome |
| Breadcrumbs / Deep-linking | AgentBreadcrumb,/agent/:aid/profile 可恢复 surface |
✅ | |
| Center Stage(布局) | 提示词编辑器主导画布 | ✅ | |
| Form / Titled Sections(输入) | 头像+名称头、模型/工具面板、提示词编辑器 | ✅ | |
| Good / Smart Defaults(输入) | inbox → 「Lobe AI」默认名称+默认头像(Content.tsx:77,82) |
✅ | |
| Autosave(反馈) | AutoSaveHint saving→saved,防抖写(store/action.ts:42) |
⚠️ | 无 failed 态(缺口②) |
| Collaborative lock(反馈) | EditLockDriver 在渲染前 peek;他人持锁则只读 |
✅ | 亮点,见第四节 |
| Streaming-aware editing | 流式期间抑制保存,结束冲刷(store/action.ts:76-112) |
✅ | 亮点,见第四节 |
| Overview + Detail(数据) | More → Advanced Settings 模态(AgentSettings) |
✅ | 保持 surface 契约(模态而非跳转) |
| Cross-surface entry(增长) | More 菜单 → /agent/:id/stats,面包屑回 chat(Header/index.tsx:200) |
✅ | 亮点,配置→回看闭环(第四节) |
| Rich-text editor + toolbar | Lexical 编辑器,mention/表格/slash 插件(EditorCanvas) |
✅ | |
| Loading Skeleton(反馈) | 配置解析期间品牌化 Loading(index.tsx:42) |
⚠️ | 成功-only 门控 → 出错时永久转圈(缺口①) |
| Failure + Retry(反馈) | store 里建模了(agentConfigErrorMap + selectors + retryFetchAgentConfig) |
— 缺失 | 建好了但没接到任何界面(缺口①) |
| Empty / Not-found 态 | — | — 缺失 | 非法 :aid → 永久 loading(缺口①) |
报告给出的整体判读(Read):布局、深链、协作锁、流式处理与配置→回看闭环都很成熟,属真正强的部分;弱点全部聚集在 Feedback(失败态)——一条存在于 store 却到不了任何像素的错误路径,和一个结构上无法报告失败的自动保存。
四、L1 产出之二:亮点(Strengths,「别回退」基线)
框架有一条 ground rule:只列坏处的审计会漂移成 bug report。亮点是一等发现,要给出 file:line 并标注「✅ 亮点」,因为它们承担三重作用——教(成为 ux 检查清单的 ✅ 示例)、保(下次重构的「不许回退」清单)、校准(缺口的严重度相对强基线才有意义)。该审计记录的四个亮点:
- ✅ 亮点 — 编辑锁在编辑器渲染前就被 peek。
EditLockDriver挂载在配置加载门控之外(审计时点index.tsx:68-70,附解释注释),因此「别人正在编辑」的代理从第一帧起就是只读,而不是先闪一下可编辑再上锁。lockedByOther/lockPending驱动真实的EditingIndicator,并隐藏会与锁争抢的 Agent Builder。这是任何共享资源编辑器都值得抄的协作安全模型。当前源码中该意图依然清晰可见:EditLockDriver.tsx/agent/profile/features/EditLockDriver.tsx) 顶部注释写明「挂在 profile 树高位(不在 loading 门控的编辑器内),使锁在编辑器渲染前被 peeked on open」;store/initialState.ts/agent/profile/features/store/initialState.ts#L42-L46) 中锁初始态刻意设为pending: true——「先只读,等锁解析完成,编辑器绝不闪可编辑」。 - ✅ 亮点 — 流式感知的保存门控。 Agent Builder 把生成的系统提示词流式写入编辑器期间,用户编辑检测与防抖保存被抑制,流结束时单次冲刷保存(审计时点
store/action.ts:76-112、EditorCanvas的finishStreaming)。生成内容不会卷入保存竞争,用户的真实编辑也不会被误判为流式输出。当前的 profile store/action.ts/agent/profile/features/store/action.ts) 仍保留完整的流式保存机器:startStreaming清空编辑器并置streamingInProgress、appendStreamingContent逐块写入、finishStreaming以enqueueSave落一次完整保存后重置流式态——一个值得保护的细微正确性设计。 - ✅ 亮点 — 配置→回看闭环从配置上下文直达。 More 菜单直接链到
/agent/:id/stats(Header/index.tsx:200-206),面包屑回到该代理的 chat——「配置代理」和「看它干了什么」只隔一次点击。这是与其他设置页「承诺了目的地却没有门」形成对照的正面案例。 - ✅ (当时尚未接线、值得保留)— store 已正确建模 error ≠ loading。
agentConfigErrorMap+currentAgentConfigError/isAgentConfigError+ retry 动作(审计时点selectors.ts:235-238、action.ts:341-357)是正确的数据层,文档注释承诺「retry UI 而非无尽骨架」。意图正确;缺口①只是没有任何 surface 消费它们——所以这是一个要「收尾」的亮点,不是供观赏的。审计报告特别叮嘱:清理代码时别把它删了——把它接上。第七节会看到后续源码状态。
五、L1 产出之三:体验缺口(Experience gaps,按严重度排序)
严重度共用一套 rubric:🔴 破坏信任(数据/输入丢失、卡死/永久态、误导性「空态」掩盖失败、静默发送失败);🟠 死路或误导(无前进路径、状态含糊、缺进行中反馈);🟡 摩擦/不一致/错失愉悦。该审计排出的五个缺口及整改建议:
① 配置拉取失败 → 永久品牌 loading;error+retry 机器建好了却没接线 🔴
ProfileArea 当时只按 isAgentConfigLoading 分支(index.tsx:42),而该选择器本质是「数据在不在 map 里」(!activeAgentId || !agentMap[activeAgentId])——正是 ux 清单 §4.2 警告的「用数据存在性冒充初始化标志」。getAgentConfigById 失败或 404 时 onData 永不执行,agentMap[id] 保持 undefined,isAgentConfigLoading 永远为 true → 整页 <Loading/> 无原因、无重试。坏的/已删除的 :aid 与「加载很慢」不可区分。关键在:onError 确实把错误记进 agentConfigErrorMap[id],选择器和 retryFetchAgentConfig 也都在——但全仓库 grep 零消费者,store 为之构建的 retry UI 是死代码。整改:在 loading 门控之前先按 isAgentConfigError 分支 → 展示失败态(原因 + 调用 retry 的 Reload);loading 只留给「无错且无数据」。
② 自动保存无法表达失败——整个 surface 的每次保存都静默失败 🔴
AutoSaveHint 的保存态枚举是 'idle' | 'saving' | 'saved'(AutoSaveHint.tsx:12),没有 failed 变体,机器在类型层面就无法显示写失败(页面编辑器、任务详情样例携带过同款陷阱)。下游所有写入端都吞掉失败:防抖提示词保存与 finishStreaming 只 catch → console.error;标题(AgentHeader.tsx:49)、头像/背景(:58, :96)、模型/工具(ProfileEditor/index.tsx 的 updateConfig)、高级设置模态的乐观写入(AgentSettings/Content.tsx:59, 65)都不上抛。保存失败的提示词编辑与保存成功的看起来一模一样——「saved」标签贴在数据丢失上。整改:枚举加 failed + 内联 Retry(保留已编辑值),由各写入端 catch 驱动,一个约定贯穿整个 surface。
③ 头像上传没有 catch——上传失败是静默的 🟠
handleAvatarUpload 是 try { … } finally { setUploading(false) } 且无 catch(AgentHeader.tsx:72-79),只有客户端超尺寸分支会 toast。被拒绝的上传(网络/服务端/编码后超尺寸)只是关掉 spinner——无 message.error,rejection 还外泄成 unhandled promise。整改:catch → message.error 并提示重试;保留上一张头像。
④ 提示词/标题草稿没有本地持久备份——重载最多丢 30 秒编辑 🟡
名称存在 useState 的 localTitle(AgentHeader.tsx:37)里,提示词留在编辑器里直到防抖写(1 秒防抖、maxWait 30 秒)到达服务端;没有像聊天输入框 useChatInputDraft 那样的 localStorage 草稿。在防抖窗口内重载/崩溃,最后一段编辑直接蒸发且不可恢复。比输入框温和(它确实自动存到服务端),但对用户书写长系统提示词的这个 surface,30 秒 maxWait + 零本地备份是真实的丢失窗口。整改:镜像 useChatInputDraft——按 agent id 把编辑器草稿备份到 localStorage,unmount 时冲刷,确认保存后清除。
⑤ 高级设置模态硬编码 loading={false} 🟡
AgentSettings/Content.tsx:93 无条件传 loading={false},若 config/meta 尚在解析,模态会装作已就绪。当前影响低(整个 surface 被 isAgentConfigLoading 门控,store 通常先填充好),但属潜在的成功-only 假设。整改:从真实的 config 解析态推导 loading。
六、Skill feedback:发现如何「回灌」到 ux 清单
框架要求每次审计必须以「回灌」收尾才算完成:具体 bug 修掉(或挂到 Linear 的 UX Audit 父 issue 下);可泛化的缺口强制回流到 ux 技能——加/强化检查项(规则 + ✅/❌ 示例,并镜像一行到 Quick review),以被审计的 surface 作为 ❌ 示例;审计本身存为 references/example/<page>.md 供下次当模板。ux 是审计的标尺,审计是让标尺保持诚实的机制——跳过回灌,审计就退化成一次性评审。
该样例产出的回灌内容:
- 新落地的子规则(可泛化的那条):Feedback §4.2 强化——「存在于 store 但没有任何 surface 消费的 error 态与 retry 动作,仍然是缺失的 error 态」;建了一半而孤儿化的失败路径与完全没建无法区分,检查手段就是 grep error 选择器的消费者数。❌ 示例即缺口①(
isAgentConfigError+ retry 动作已建,rg→ 0 调用点,拉取失败时永久品牌 loading)。此规则此前并不存在——旧 §4.2 文本假设错误路径是「被忘了」,而不是「建了一半被孤儿化」。 - 验证了既有规则(作为好的 ❌ 示例引用,不算新规则):§4.4(缺口②,
idle|saving|saved无failed枚举——页面编辑器、任务详情之外的第三例);§4.2 数据存在性冒充初始化标志(缺口①的!agentMap[id]门控);§4.2 写入侧无catch(缺口③);Edit §2.1(缺口④,自动存服务端仍欠一份本地草稿)。 - 记录但不新立规则的好案例:「渲染前 peek 编辑锁」与「流式感知保存门控」虽强,但它们复证的是已经完整的规则(协作安全/保存竞争正确性),没有揭示缺失的区分度——所以只进「别回退」清单,不并入检查表。
七、源码级跟踪:审计缺口在当前代码中的落地状态
样例文档自我声明「不是当前状态的事实」,而当前仓库代码恰好提供了难得的「审计→整改」对照。以下均为从当前源码结构可见的事实:
缺口①(失败→永久 loading)已被接线。 当前 profile/index.tsx/agent/profile/index.tsx#L51-L68) 的 ProfileArea 使用 AsyncBoundary 做三分支门控,源码注释直接把审计的教训写进了代码:
<AsyncBoundary
data={isAgentConfigLoading ? undefined : true}
error={configError}
errorVariant={'page'}
isLoading={isAgentConfigLoading && !configError}
loading={<ProfileSkeleton />}
onRetry={() => retryAgentConfigFetch()}
>
即「error 分支优先于 loading 分支:出错时展示错误态与重试,loading 只留给真正加载中」。数据层同步增强:src/store/agent/selectors/selectors.ts 中 isAgentConfigLoading 现在还会检查 agentNotFoundMap——注释写明「not-found 的 agent 永远不会进 agentMap,按已 settled 处理,让 UI 渲染 404 卡片而非无尽骨架」,并新增 isCurrentAgentNotFound 区分「不存在/失权」与「拉取失败」;currentAgentConfigError 的文档注释也与审计整改描述一致:「区分 fetch failed 与 no data yet,使失败展示 retry UI 而非无尽骨架」。重试动作现名为 retryAgentConfigFetch(见 src/store/agent/slices/agent/action.ts 第 489 行起的实现,即审计时点 retryFetchAgentConfig 的更名继任者),并有测试守护:slices/agent/action.test.ts 中「should record fetch error in agentConfigErrorMap and clear it on retry」验证了错误记入 map、重试后清除的完整生命周期;selectors.test.ts 覆盖 isAgentConfigLoading 的各分支。
缺口②(保存态无 failed)已在类型与 store 层落地。 共享的保存态类型 src/types/saveState.ts 现为 SaveStatus = 'idle' | 'saving' | 'saved' | 'failed'——其文件头注释正是在描述审计指出的陷阱(「无 failed 成员时每个 catch 都只能静默」)。当前 profile store/action.ts/agent/profile/features/store/action.ts#L57-L112) 的保存队列实现了完整的失败生命周期:每次保存带递增 revision,入队时清 failedSave 并置 promptSaveStatus: 'saving';写入失败时若该 revision 仍是最新一次,则记录 failedSave 并置 'failed';retryPromptSave 动作重放最近一次失败的保存。防抖窗口也由常量 EDITOR_DEBOUNCE_TIME / EDITOR_MAX_WAIT(来自 @lobechat/const,审计时点记录为 1 秒/30 秒)统一配置,且每个 agent 拥有独立防抖器——「在 agent B 里打字绝不替换 agent A 的 trailing save」。
需要保持证据边界:样例中的行号引用(index.tsx:42、store/action.ts:76-112 等)是 2026-07 审计时点的位置,当前文件行号已漂移;第七节的对照基于当前仓库代码的独立阅读,而非审计文档的转述。缺口③(头像上传 catch)、④(本地草稿备份)、⑤(模态 loading 推导)是否在最新代码中全部修复,本文未逐一核验,可参照样例第五节的验证清单自行确认。
八、未完成的 L2/L3 验证清单
样例第五节把「还不能由 L1 下结论的部分」显式留给了后续层,这正体现了 Coverage Matrix 的纪律。待办:
- L2(视觉)——确认提示词编辑器读起来是主导性的 Center Stage;检查
AutoSaveHint标签的可读性(saving vs saved vs 「latest」);锁定编辑器的 opacity 0.65 可读性;窄屏下头像+名称行与异构配置标签页的布局;深色模式。 - L3(动态)——
- 强制
getAgentConfigById失败 / 导航到伪造的:aid,实测确认缺口①(当时预期:永久品牌 loading、无重试),并检查是否有路由重定向/错误边界先行兜底; - 强制保存写入被拒绝,确认缺口②(surface 显示「saved」/无失败反馈);
- 驱动一次头像上传失败,确认缺口③;
- 在防抖窗口内编辑时重载,确认缺口④(丢失的提示词编辑)。
- 强制
九、小结
这份 profile 审计样例完整示范了 ux-audit 技能的一次标准运行:先给出 surface 的类基准期望清单,再用 L1 产出「模式清点表 + 亮点(别回退)+ 排序缺口(带 file:line 证据与一行整改)」,然后把可泛化的发现回灌进 ux 检查清单,最后把不能由代码下结论的部分明确挂起给 L2/L3。对照当前源码可以看到这套方法的实际闭环:审计标记的两条 🔴 缺口(失败态无 UI 消费、保存态无 failed 成员)在最新代码里分别以 AsyncBoundary 三分支门控 + retryAgentConfigFetch、SaveStatus 的 failed 成员 + retryPromptSave 落到了实处,并有单测守护。相关文件入口:SKILL.md、L1 程序、模式目录、审计样例、profile 路由入口/agent/profile/index.tsx)。
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 StartedRust0630
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00