LobeHub 源码级 UX 审计实战:以桌面端 Agent「话题」会话视图(Topic View)为例
导读
本文以仓库内 .agents/skills/ux-audit/references/example/topic.md 这份「话题(Topic)视图」UX 审计工作样本为骨架,完整拆解一次基于源码(L1 Static 层)的界面体验审计如何在 LobeHub 桌面端落地:从确定审计表面(surface)、对照模式目录与行为清单打评分,到定位「永驻骨架屏」等可复现的体验缺口,最后把结论回灌成规则库的正/反例。读完你可以掌握:审计的三层方法与覆盖面矩阵、如何用 模式 × 评分 表快速盘点一个会话界面、如何用 file:line 证据而不是直觉下结论,以及如何把审计发现沉淀为可复用的技能知识。
一、这份文档是什么:一次「输出形状」的参考样本
topic.md 不是普通的变更说明,而是 LobeHub 仓库内 ux-audit 技能的 worked example(工作样本),它的作用是告诉执行者一次审计报告应有的形状(output shape):包含审计对象、运行了哪些层、模式盘点表、亮点(don't regress 清单)、按严重度排序的缺口、对技能规则的回灌、以及留待 L2/L3 验证的事项。
样本对应的是一次真实审计:
- 审计对象:桌面端 Agent 话题(Topic)视图——即把 Agent 会话限定到某个选定话题的界面;
- 触发记录:Linear
LOBE-11213(归属LOBE-11145),时间 2026-07; - 执行层:仅 L1(静态/代码层),
✅完成;L2(视觉)、L3(动态 + CLS)⏳未运行; - 自述定位:
the code moves; re-verify before citing——文档里引用的行号是快照值,引用前需要对着当前代码复验。
这正是审计方法论要求的诚实边界:L1 只能得出「代码层面是否存在该分支/状态」的结论,渲染层观感必须留给 L2/L3。样本里对「切换话题是否闪旧消息」「失败回合气泡是否带重试」都明确标注为 L1 inference, pending L2/L3,没有越层下结论。
二、三层审计法与覆盖面矩阵(方法论背景)
审计不是单一活动,执行前要先确定「哪个层能看见这类问题」。ux-audit 技能(.agents/skills/ux-audit/SKILL.md)把审计分成三层,每层有独立过程文件:
| 层 | 做什么 | 能抓住什么 | 成本 |
|---|---|---|---|
| L1 Static(layer-1-static.md) | 读代码 | 缺失的空/错误/重试分支、草稿未持久化、缺模式、结构性问题 | 低、可离线、每次必跑 |
| L2 Visual(layer-2-visual.md) | 截渲染后的真实界面 | 视觉层级、主控按钮是否真为主操作、留白/对比/截断/溢出、暗色/亮色、响应式断点 | 中、需要渲染环境 |
| L3 Dynamic(layer-3-dynamic.md) | 用 acceptance 驱动真实用户旅程 + 埋点 | 进行中/锁定态、强制错误/空态、步骤能否衔接、焦点/键盘可达、量化的 CLS/LCP/INP/long-task | 高、需要运行环境与登录态 |
覆盖面矩阵的核心规则是:结论必须来自能看见它的那一层。例如「主按钮是否唯一/是否就是主操作」「空态/加载态/错误态真实长什么样」这类渲染结论,❌ 不能从代码里的一个 variant prop 打勾——那是 L2 的活;而「缺错误分支、无重试、草稿没持久化」这类结构性结论,L1 就能下。分层的另一个价值是省成本:L1 每次都跑,L2 在发现与布局/层级/渲染态有关时追加,L3 在需要走旅程、强制 L1/L2 到不了的状态、或要测 CLS 等指标时才上。
三、审计范围界定:Topic 视图的表面构成
审计报告首先精确圈定「这一个 surface」的构成。话题视图是 Agent 会话的限定到已选话题形态,它的三个组成区域是:
- 消息流(message stream):加载 → 空/欢迎 → 列表三段;
- 输入区(composer):
MainChatInput → ChatInput链路; - 外围框架(chrome):ChatHeader、Portal、WorkingSidebar。
从当前仓库源码核对,路由与组件拓扑与审计快照基本一致:
- 路由
/agent/:aid/:topicId对应 src/routes/(main)/agent/index.tsx/agent/index.tsx>),由ChatHydration与Conversation组合渲染; - Conversation 侧(src/routes/(main)/agent/features/Conversation/index.tsx/agent/features/Conversation/index.tsx>))内嵌
ConversationArea,并带 MainChatInput 输入区; - 消息列表组件为共享的 ChatList(src/features/Conversation/ChatList/index.tsx),它与「聊天」普通会话共用同一组件,因此本次审计把焦点放在「selected-topic」形态——一个消息从服务端拉取的话题,而不是新建的空会话。
明确范围的意义在于:一次审计只跑一个 surface,避免全应用横扫导致深度不足;同时点名「与 [9] chat 共用元素」,提示同类结论可能跨 surface 复用。
四、模式盘点:这个界面用对了哪些设计模式
审计的第一份产出是一张「模式使用表」——把界面拆成 Tidwell《Designing Interfaces》模式语言与 LobeHub ux 行为清单的逐条对照。完整继承如下:
| 模式(族) | 落点 | 评分 | 备注 |
|---|---|---|---|
| Center Stage(布局) | 消息流占主导,输入区钉在底部((chat)/_layout) |
✅ | 教科书式 |
| Deep-linking(导航) | /:topicId ↔ activeTopicId,?thread= ↔ activeThreadId(ChatHydration) |
✅ 亮点 | URL↔store 双向同步、replace 历史——见 §5 |
| Escape Hatch(导航) | ChatHeader 话题操作(重命名/导出/删除)、分享、面板开关 | ✅ | 生命周期都在头部 |
| Overview + Detail(数据) | 消息 → Portal(文件/工件/文档/子线程)、WorkingSidebar | ✅ | 保持「面板而非跳转」的契约 |
| Cards / Virtualized list(数据) | VirtualizedList(react-virtuoso)渲染 displayMessageIds(ChatList) |
✅ 亮点 | 长会话可扩展——见 §5 |
| Skeleton loading(反馈) | messagesInit 未就绪时 SkeletonList(ChatList) |
⚠️ | 没有终态失败路径——见缺口 ① |
| Empty-state as onboarding(反馈) | AgentHome 欢迎页(Agent 信息 + 开场问题 + 最近记录)(ChatList) |
✅ | 真实页面;但新建话题与 0 消息共用同一页(缺口 ③) |
| Draft safety(输入/编辑) | useChatInputDraft → draftStorage(localStorage,按话题分 key) |
✅ 亮点 | 持久化、按话题隔离、restore/flush/clear——见 §5 星标 |
| Same-page error(反馈) | 发送失败 → 可关闭 Alert(ChatInput) |
✅ 亮点 | 有呈现而不沉默(对比首页 composer)——但缺就地重试,缺口 ② |
| Autocompletion(输入) | @ 提及、斜杠菜单、本地目录提及(桌面端) |
✅ | 输入富手性 |
| Progress / Cancelability(动作) | 流式期间显示 Stop 按钮,onStop=stopGenerating |
✅ | 长任务可中断(generation 类常态) |
| Capability guardrail(反馈) | 输入区上方 AgentConfigError(MainChatInput) |
✅ | 配置告警响应式 |
报告的判读结论很直接:会话表面是成熟的——亮点的承重程度与唯一缺口相当,真正的弱点是这类代码库的经典问题:反馈(失败态)——消息流无法「看得见地失败」。
五、值得保留的亮点(don't regress 清单)
审计把「这个表面做对了什么」单独列出,作为下一次重构/改版时不许回归的清单。每一条都有证据支撑,以下是结合当前仓库源码的逐条验证。
✅ 星标:按话题隔离、可持久化的 composer 草稿
输入框会把每个话题未发出的内容写进 localStorage,key 按上下文区分(draftKey = agent + topic + thread),全生命周期闭环:
- 每次击键防抖保存,失焦(blur)时 flush(对应源码逻辑
saveDraftDebounced,500ms 防抖,见 src/features/ChatInput/hooks/useChatInputDraft.ts); - 挂载时仅向空编辑器恢复草稿(
restoreDraft:editor.isEmpty才setDocument); - 卸载时先 flush 防抖再补存非空内容;
- 发送成功后清除草稿(draft 的
removeDraft); - 存储层限制 50 条草稿并按 LRU 驱逐(src/features/ChatInput/draftStorage.ts,
MAX_DRAFTS = 50)。
仓库中还额外验证到一处审计快照之后的增强:draftStorage.ts 维护了一个响应式 draft-key 注册表(useHasDraft),让话题列表能在草稿出现/消失时刷新并显示 [draft] 前缀提示(见 src/features/AgentSidebar/Topic/List/Item/index.tsx)。这条链路的承重价值在于:重载 / 崩溃 / 切换话题都不会蒸发最高流量输入框里已敲的内容,且草稿不会跨话题串味——它正是首页 ux Edit §2.1 反例所指的「纯内存 composer」的对立面。
✅ 发送失败有呈现,而非沉默
发送失败时渲染一个可关闭的、携带错误信息的 Alert(onClose=clearSendMessageError,源码见 src/features/Conversation/ChatInput/index.tsx 中基于 @lobehub/ui Alert 的错误呈现)。对比 ux Act §3.1 反例点名的 console.error 即焚路径(Pages 侧栏、Task 任务控制),这里用户确实被告知发送失败了。缺口只在于 Alert 缺少就地 Retry——见缺口 ②,呈现本身是对的。
✅ 流式生成可中断
生成进行中显示的是 Stop 按钮(接到 stopGenerating),不是只有一个 spinner。这满足了 generation 类界面「运行中可取消」的规范,是承重能力:核心表面上长任务永远可以中断。
✅ 深链是干净的双向同步
ChatHydration 把 :topicId / ?thread= 镜射进 store(useLayoutEffect),同时用 subscribe 把 store 变化写回 URL 且走 replace 历史(对应源码 src/routes/(main)/agent/features/Conversation/ChatHydration/useChatRouteSync.ts/agent/features/Conversation/ChatHydration/useChatRouteSync.ts>))。承重价值:分享/收藏的话题 URL 能精确还原状态,导航永远不会和渲染的内容脱钩。
✅ 消息列表虚拟化
VirtualizedList(react-virtuoso)只渲染可视行,数据源是 displayMessageIds(当前实现见 src/features/Conversation/ChatList/index.tsx,<VirtualizedList dataSource={displayMessageIds} .../>)。承重价值:上千条消息的话题依旧保持响应——符合「面向扩展设计」的规范。
六、体验缺口(按严重度排序)
① 🔴 消息流没有终态失败路径——拉取失败将永久骨架屏(ux Feedback §4.2 / Read §1.1)
审计快照描述的核心机制:消息列表的渲染被 messagesInit 门控——if (!messagesInit && !isNewConversation) return <SkeletonList/>,而 messagesInit 是一个只在成功路径上置位的「数据在场」伪装初始化标志:
useFetchMessages在 chat store 与 conversation store 两侧都只注册onData,没有onError(对应文件 src/store/chat/slices/message/actions/query.ts 与 src/features/Conversation/store/slices/data/action.ts,后者的成功onData回调里才写messagesInit: true);- SWR 对象上的
error在调用点被丢弃:只读.isValidating,渲染里没有 error 分支(skeleton / welcome / list 三分支之外无第四分支)。
于是当 messageService.getMessages() 对已选话题抛错(500 / 网络 / 鉴权)时,messagesInit 永不翻转 → SkeletonList 永久渲染,没有原因、没有 Reload/Retry——出现在产品最高流量表面上。这是教科书式的「success-only-init-flag」陷阱(Task 列表 / Eval / Memory / generation 已出现同型问题),归档为 LOBE-11222。
当前仓库复验(重要):审计样本自述"代码在移动,引用前需复验"。检查当前 src/features/Conversation/ChatList/index.tsx 可见列表已引入
useMessageRefreshError与resolveMessageListFeedback,并新增了AsyncError(首次加载失败整页 + Retry)与RefreshError(后台失败底部条 + Retry)两条错误分支——即该缺口很可能已被后续迭代修复。引用「永久骨架屏」这一结论时应以当前代码为准复核,勿直接照搬快照描述。
② 🟡 发送错误的 Alert 没有就地重试(ux Act §3.1 / Feedback §4.2)
发送失败 Alert(见 §5)可关闭但没有 Retry / resend;而编辑器在发送时已被清空。恢复路径推测存在于失败回合消息气泡上的「重新生成」——审计标注为 pending L2:需要确认失败回合的气泡确实存在且带重试;若没有,该缺口升为 🟠。
③ 🟡 空话题与新建会话共用同一欢迎页(ux Read §1.1,轻微)
「新会话」(isNewConversation,无 topicId)与「已加载的 0 消息话题」(displayMessageIds.length === 0)渲染同一个 AgentHome 欢迎页(当前代码中 (showWelcome || displayMessageIds.length === 0) && welcome 的分支仍如此)。一个真正为空的既有话题,看起来和一个全新的会话没有区别。影响较小,记录以求完整。
七、从审计到回灌:技能知识的闭环
审计结论并不止于报告,而是被回灌进 ux 技能的规则库,形成"好案例 + 缺口案例"的双向闭环:
- 新 ✅ 案例落地(好案例回灌):§5 星标的「按话题持久化草稿」被加为 ux Edit §2.1 旁的 ✅ 案例(.agents/skills/ux/references/edit.md),作为该规则既有首页 composer ❌ 反例的正面对照。
- 新 ❌ 案例落地(缺口回灌):缺口 ① 被加为 ux Feedback §4.2 下的旗舰 ❌ 案例(.agents/skills/ux/references/feedback.md)——同一个
success-only-init-flag / data-presence-disguised(messagesInit)永久骨架屏问题,如今出现在核心聊天消息流上。 - 验证了既有规则(作为实例而非新增条目):发送错误呈现(§5)是 Act §3.1「surface failure」的 ✅ 实例;流式 Stop 是 Act §3.1「Cancel-while-running」的 ✅ 实例。
- 没有新增通用规则:每个缺口都能映射到既有清单项(Feedback §4.2、Act §3.1)。按技能收尾规则,明确写出:本次运行是"打磨两条既有条目的正反两半",而非新增条目。
- 过期案例提示:Edit §2.1 的「首页 composer ❌」在首页若与 Topic 共用同一条
ChatInput/draftStorage路径时可能已过时——标记为需要单独一轮验证,本次不动它。
「回灌」机制的意义在于:审计不是一次性动作,而是持续的——每次运行要么给既有规则补一个正/反实例,要么沉淀一条新规则,让下一次审计的基准越来越准。
八、Pending:留给 L2 视觉与 L3 动态的验证清单
审计报告如实列出代码层看不到、必须由更上层裁决的问题:
- L2(视觉):确认发送按钮在视觉上确实读作「唯一主导控件」;确认失败回合的消息气泡渲染出可见的 retry(裁决缺口 ② 是否升级);检查切换话题时是否会先闪旧消息、再换到新话题的骨架/数据(
dbMessagesMap键交换)——这是 L2/L3 裁决,不是 L1。 - L3(动态):强制让
getMessages报错,现场确认缺口 ①(永久骨架屏、无重试);驱动一个中途失败的发送,端到端确认缺口 ② 的恢复路径;量化骨架→消息交换与流式追加过程中的会话 CLS。
这份 pending 清单本身就是审计严谨性的体现:只对能看见的层下结论,把看不见的交给对应层验证,避免拿代码结构臆断渲染结果。
九、如何把这份样本复用到下一次审计
若要在仓库内开展新的 surface 审计,建议按以下顺序操作:
- 界定一个 surface:一条路由 + 它的 state 形态(如本文的 selected-topic 限定态),而非整个应用;
- 跑 L1:对照 pattern-catalog.md 的模式目录与 ux 技能清单(act.md、edit.md、feedback.md、read.md)逐条核对,产出「模式 × 评分」表;
- 按需追加 L2/L3:结论涉及视觉层级、渲染态、旅程衔接或 CLS 时才升级到截屏/自动化层;
- 每条结论给证据:
file:line(L1)或经工具验证的截图/数值(L2/L3),不做无证据断言; - 收尾回灌:新亮点进 ✅ 案例、新缺口进 ❌ 案例,并为无法归入既有规则的新问题起草新清单项;
- 标注快照时点:报告里写清审计日期与 Linear/issue 关联,并明确
re-verify before citing——代码在移动,本文所有行号都是快照值。
仓库内 topic.md 的兄弟样本(如 chat.md、memory.md、tasks.md 等)展示了不同 surface 的同类输出形状,可交叉参考以校准篇幅与深度。
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 StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
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