- 人工智能
- AI Agent
- Agent 框架
- 后端
- 工具调用
【免费下载链接】eve
The Open Framework for Building Agents
本篇指南基于 eve 开源框架中eve-chat-template模板自带的技能文档(dashboard-chat-best-practices/SKILL.md)与对应源码展开,系统讲解如何让一个 Agent 聊天界面具备「仪表盘级」的质感:首屏不阻塞在 auth/数据库之上、流式状态下布局不抖动、乐观消息与真实事件日志的精确对账。读完后你将掌握 eve 聊天模板的 Cache Components/PPR 静态 Shell 架构、主题与登录态的预水合技巧,以及 composer、侧边栏、消息渲染的完整控制契约,可以直接套用到自己的 Agent Web 前端中。
一、定位:做一个可用的聊天面,而不是营销页
这份最佳实践的第一性目标是「让模板的 Web 聊天保持 polished dashboard surface 的观感:稳定、安静、首屏快、流式状态下行为可预期」。它约束的不是某个单点功能,而是整个 Web 应用的产品形态:
- 首屏就是可用的聊天面:第一屏应该是可用的 composer(输入框)、sidebar(会话侧边栏)或聊天路由,而不是落地页。
- 克制、工作向的审美:间距密集但可读、边框安静、控件行为可预期、装饰最少化。
- 优先复用既有原语:优先使用已有的 shadcn/Tailwind 组件和本地组件,而不是为新界面引入新抽象。
- 图标承载紧凑命令:紧凑操作用图标 + 可访问的
aria-label/tooltip 表达,避免为「好的控件本身就能暗示的行为」再写一遍解释性文案。
模板中这一形态的落点是 HomeChatPage:根路由把 logo + composer 居中放在视口中心,配合 TemplateFooterLinks 的轻量页脚,没有任何营销式 hero 布局。
二、Cache Components 与 PPR 基础:静态 Shell + 流式数据
模板的 Next.js 配置只有两行核心声明(next.config.ts):
const nextConfig: NextConfig = { cacheComponents: true, };最佳实践要求保持cacheComponents: true开启,并把聊天路由当作「静态 Shell + 向 Suspense 边界流式注入动态数据」来处理。围绕这个配置,文档给出了一组明确的约束,每一条都能在源码中找到对应实现:
2.1 Shell 必须廉价且确定
不要把 app/(chat)/layout.tsx/layout.tsx) 的渲染阻塞在 auth、数据库、聊天历史或任何 per-chat 数据上。实际代码正是这样写的——layout 同步传入的初始状态全是「廉价占位值」:
return ( <AgentChatShell initialChats={[]} initialNextCursor={null} setupStatus={setupStatus} // 同步的 getInitialSetupStatus() viewer={null} > {children} <div className="hidden" aria-hidden> <Suspense fallback={null}> <ResolvedChatBootstrap /> </Suspense> </div> </AgentChatShell> );即文档要求的「把廉价初始状态交给AgentChatShell:空历史、initialNextCursor: null、viewer: null、getInitialSetupStatus()」。
2.2 昂贵数据放进隐藏 Suspense 子树
所有请求绑定的昂贵数据都被收进两个隐藏的 Suspense 边界内:
ResolvedChatBootstrap(定义在同一 layout 文件内):异步解析 setup 状态、viewer,并在storageMode === "database"时拉取第一页侧边栏历史listChatsPageByUser(viewer.id);ExistingChat(chat/[id]/page.tsx):加载当前会话的活跃聊天数据。
解析完成后,通过「同步组件」(sync component)把服务端解析好的数据交给客户端 Shell:
// app/_components/agent-chat-bootstrap-sync.tsx "use client"; export function AgentChatBootstrapSync({ chats, nextCursor, setupStatus, viewer }) { useEffect(() => { const detail = { chats, nextCursor, setupStatus, viewer }; const target = window as Window & { __eveChatBootstrapSync?: typeof detail }; target.__eveChatBootstrapSync = detail; window.dispatchEvent( new CustomEvent(CHAT_BOOTSTRAP_SYNC_EVENT, { detail }), ); }, [chats, nextCursor, setupStatus, viewer]); return null; }AgentChatBootstrapSync 负责更新 setup 状态、viewer 与历史;AgentChatRouteSync 负责更新当前路由的活跃会话。两者的实现模式完全一致:把数据先挂到window.__eveChatBootstrapSync/__eveChatRouteSync(兜底,防止事件先于监听者),再派发 agent-chat-events.ts 中定义的两个 CustomEvent:
export const CHAT_BOOTSTRAP_SYNC_EVENT = "eve-chat:bootstrap-sync"; export const CHAT_ROUTE_SYNC_EVENT = "eve-chat:route-sync";AgentChatShell 在useEffect中同时监听事件并读取window上的兜底值,调用setBootstrapData一次性更新setupStatus、viewer、history、nextCursor。注意历史数据是合并而非覆盖(mergeChatHistory):新到达的数据在前、保留本地乐观修改的条目标题与时间戳,这正是「bootstrap 之后精确对账」的体现。
2.3 Fallback 保持为 null,notFound()必须等到有证据
两条容易踩坑的规则:
- 这些同步边界的 Suspense fallback 保持
null——可见的 Shell 已经有稳定的占位布局,任何 loading 面板都会把聊天框架「推挤」变形。 - 不要提前调用
notFound()。看 chat/[id]/page.tsx 的实现:ExistingChat只有在真正拿到 viewer、appReady、storageMode === "database"且查询不到会话时才调用notFound();而它整体包在<Suspense fallback={null}>里,外层SessionChatPage在决策加载期间照常渲染。
最后一条原则值得单独强调:根路由与会话路由视觉不同、但结构兼容。根页面把 composer 居中(HomeChatPage 的items-center justify-center),会话页面把它贴近底部(SessionChatPage 的pb-4 sm:pb-6),但两者都在同一个 Shell 内、共享同一套 composer 契约。PPR/静态 Shell 工作既是延迟特性,也是正确性特性:先完成不被 auth/数据库阻塞的首屏,再做精确的数据对账。
三、静态 Shell 与水合:主题、登录提示都不许「闪一下」
Shell-first 策略要求AgentChatShell在 auth、setup、聊天历史完成加载之前就画出侧边栏框架、顶部控件和路由主体(见 AgentChatShell 渲染出的h-dvh骨架:桌面侧边栏w-64/w-0宽度过渡、顶部绝对定位控件层、<main>内容区)。在此基础上,有两类「预水合(pre-hydration)」机制保证 React 第一次 paint 之前文档状态就是对的:
3.1 主题预水合
根布局 app/layout.tsx 在<head>内注入一段内联脚本,在 React 水合前根据prefers-color-scheme给<html>加上dark/light类名并设置color-scheme,避免深浅色模式闪烁或错配;脚本异常时兜底为暗色主题。
3.2 登录态显示预水合(display hint,不是权限)
模板使用eve_logged_incookie 作为显示提示。常量定义在 auth-hint.ts:
export const AUTH_HINT_COOKIE_NAME = "eve_logged_in"; export const AUTH_HINT_COOKIE_VALUE = "1"; export const AUTH_HINT_COOKIE_MAX_AGE = 60 * 60 * 24 * 30;AuthDisplayPreHydrationHead 在 head 中注入一段脚本:读取该 cookie,设置document.documentElement.dataset.eveAuthDisplay = "logged-in" | "logged-out",同时注入 CSS:
html[data-eve-auth-display="logged-in"] [data-eve-auth-display="logged-out"], html[data-eve-auth-display="logged-out"] [data-eve-auth-display="logged-in"] { display: none !important; }这样在 viewer 数据解析出来之前,错误的登录/未登录分支就已经被 CSS 隐藏。文档同时划了一条红线:永远不要把 auth hint 当作权限来源——server actions、聊天加载、Eve auth 和 rate limit 都必须使用真实的 Better Auth 会话,cookie 只用于消除视觉闪烁(<noscript>中还会默认隐藏logged-in分支)。
3.3suppressHydrationWarning的最小使用面
suppressHydrationWarning只允许出现在保护「已知预水合文档变更」的地方:根布局的<html>因为主题脚本会修改 class,必须加(app/layout.tsx 第 91 行);侧边栏同理——AgentChatShell 内嵌的SidebarCookieScript会在水合前读取eve-chat-sidebarcookie,若为closed则给<html>打上data-eve-chat-sidebar="closed"提示,避免「先展开后收起」的闪烁。其他地方一律不加,否则会掩盖真正的 hydration mismatch。
四、同一路由,登录与未登录
模板刻意不为登录态分叉路由:登录与未登录渲染同一个路由 Shell,只是控件不同。
- Bootstrap 加载中:在 AuthDisplayLoggedIn /
AuthDisplayLoggedOut两个包裹组件里同时渲染两套占位,由文档 dataset(上一节的 CSS)隐藏错误分支。AgentChatShell顶部的loggedOutAuthActions正是这个模式的实例——historyLoading时把 Sign in/Sign Up 按钮包进AuthDisplayLoggedOut。 - 未登录用户立即看到 Sign in 入口;已登录用户看到一个中性的用户菜单占位,而不是先闪一下 Sign in。
- Bootstrap 解析后,一切以真实
viewer状态为准,渲染为viewer ? <UserMenu /> : <SidebarSignInButton />。 - 未登录用户提交文本时:草稿写入
sessionStorage,打开登录弹窗,登录完成后恢复草稿。链路在源码中完整可见:HomeChatPage 的handleSubmit在未登录时调用requestSignIn(message);AgentChatShell.requestSignIn记录draftBeforeSignIn与signInCallbackPath;SignInModal的onBeforeSignIn把草稿写入sessionStorage的eve-chat-draft;HomeChatPage/SessionChatPage在viewer出现后读取并删除该草稿。
五、乐观消息与行身份(Row Identity)
流式聊天界面最容易出问题的地方是「状态来源打架」。文档给出的规则与源码实现一一对应:
- 立即显示乐观用户气泡,但以持久化的 Eve 事件为事实源。agent-chat.tsx 中乐观消息携带
optimistic: true标记,message.tsx 依据message.metadata?.optimistic给它降低不透明度(opacity-90),与真实消息视觉区分。 - 只有当精简后的事件日志中出现真正的最新用户消息时,才清除乐观气泡——不要按时间或按发送成功事件来删。
- 尽量集中管理 live 聊天状态,避免同一帧内多个竞争数据源。
AgentChatShell把所有 live 状态(历史、viewer、setup、活跃会话 ID)收进单一 Provider(chat-shell-context.tsx),页面组件只消费不私存。 - 流式响应使用稳定的行身份:一条 "thinking"/"streaming reply" 行应该原地变成 assistant 行,而不是 remount 到另一个组件路径。
AgentMessage的 key 来自消息 ID 与 part 的toolCallId/类型索引(partKey),保证同一条消息在 streaming 与 settled 之间复用组件实例。 - thinking presence 单独存在:只在「还没有有意义的 assistant 文本」或「回合仍在 finalizing」时显示,并且以淡出(fade out)方式消失而不是生硬移除。
- 不要让最终快照写入与 live 状态互搏:streaming 期间优先 live 源;settled 之后用规范快照对账。SessionChatPage 中
settledPendingMessagesRef记录已对账的消息,getRestorablePendingUserMessage据此决定一条 pending 消息是否还需恢复——同一机制也服务于 provisional chat(首页提交后先跳转/chat/{provisionalId},再由 provisional-chat.ts 创建真实会话并router.replace)。
六、Composer 与侧边栏
6.1 Composer 的控制契约
ChatComposer必须由页面完全受控,契约属性为:value、onChange、onSubmit(以及disabled、disabledReason、isBusy、isPreparing)。composer.tsx 的实现要点:
const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => { if (event.key === "Enter" && !event.shiftKey) { event.preventDefault(); submitValue(); } };- Enter 提交,Shift+Enter 换行;
- busy 状态显示 Stop(
SquareIcon方形按钮),preparing 状态显示 spinner(Loader2Icon),空输入或超长输入禁用发送,但不移动布局——右侧按钮区始终占位,footerStart为空时渲染h-8的空占位; - 禁用状态暴露可操作的 tooltip 文案:
disabledReason存在时,整个表单被Tooltip包裹,例如「Chat history is still loading.」「Finish auth setup before chatting. Missing: xxx.」(见 SessionChatPage 的getSessionComposerDisabledReason)。
根页面与会话页面可以摆放 composer 的不同位置,但必须共享同一组件与同一契约——两处都是mx-auto w-full max-w-2xl的容器 +footerStart={<ComposerFooterControls />}。
6.2 侧边栏状态:cookie + 乐观更新
- 桌面侧边栏的开关持久化在 cookie 中(
eve-chat-sidebar,sidebar-state.ts,Max-Age一年),同时写入data-eve-chat-sidebar文档提示以避免「展开再收起」的闪烁(第三节的SidebarCookieScript)。 - 会话历史保持游标分页 + 乐观:
AgentChatShell提供touchChat(置顶并刷新时间戳)、removeChat、updateChatTitle三个本地变更原语,翻页走/api/chats?cursor=...;侧边栏状态不是会话内容的真源(transcript 仍由聊天数据与 Eve 事件驱动)。 - 移动端侧边栏的浮层行为与桌面持久状态分离:移动端是
fixed inset-y-0浮层 + 半透明遮罩(mobileSidebarOpen),桌面端是宽度过渡的持久面板(desktopSidebarOpen),两套状态互不干扰。
七、消息渲染规则
AgentMessage 是这些规则的集中实现:
| 规则 | 源码落点 |
|---|---|
| 用户消息:紧凑、右对齐气泡 | isUser ? "justify-end"+max-w-[85%] rounded-[18px] bg-muted/70 |
| Assistant 文本:全宽左对齐 prose,使用 Streamdown | Markdown组件,见 markdown.tsx(挂载cjk、code、math、mermaid插件,代码块/列表/行内代码在两种主题下都可读) |
| 相关动态工具部件合并成紧凑行;仅在有可用 input/output/error/HITL 控件时才展开 | AgentMessageParts把连续dynamic-toolpart 累积到pendingTools并交给ToolGroup;hasToolDetails判定可展开性 |
| reasoning 渲染为可折叠块:流式中标签 "Thinking...",结束后 "Reasoning" | ReasoningPart中isStreaming ? "Thinking..." : "Reasoning" |
| 连接授权渲染为结构化卡片,而不是塞进 assistant 文本的散文 | 授权/HITL 走InputRequestActions的卡片式表单(琥珀色边框、选项按钮、自由文本回复框) |
| 控件/卡片内文本在移动端宽度下不溢出;按钮、工具行、计数器、composer 控件保持稳定尺寸 | 如工具行truncate、payload<pre>的max-h-56 overflow-auto、发送按钮固定size-6 |
一个值得注意的细节:assistant 流式文本使用 useStreamingText 做「打字机平滑」——按剩余长度分档步进(剩余 >160 字符每 60ms 步进 6 字符,收尾阶段步进 2 字符),并用streamingTextCache(上限 40 条)在行身份切换时保留已显示文本,避免 remount 后文案回退——这正是「稳定行身份 + 避免最终快照与 live 状态互搏」在渲染层的延伸。
八、样式规则
- 只用既有 token 系统:
background、foreground、muted、border、card、accent与本地透明度写法(模板里大量出现border-border/40、bg-muted/70这类组合)。 - 卡片要克制:不嵌套卡片;卡片只用于消息、弹窗、重复条目、需要框选的控件,不用于整页装饰。
- 圆角与模板一致:避免大尺寸 pill 形状,除非局部组件已经采用。
- 禁止聊天应用里出现大型装饰性渐变、orb、blob、营销式 hero 布局。
- lucide 图标优先,图标-only 按钮必须加
aria-label,含义不明显时补 tooltip(ShareChatButton、侧边栏开关按钮都是范例)。 - 杜绝布局位移:加载、悬停、流式状态都不应引起跳变——为 composer 控件、侧边栏行、auth 占位、thinking/工具行预留稳定空间。
九、验证清单
技能文档的 Verification 一节给出了改动后的强制动作,作为收尾保留:
- 每次 UI 或 app-router 改动后运行
pnpm typecheck与pnpm build(模板使用 pnpm workspace,见 package.json)。 - 手动走查受影响的场景:根路由未登录态、根路由已登录态、auth 弹窗草稿恢复、首条消息导航、后续乐观发送、流式 assistant 行、Connect/Skip 授权卡片、主题首屏、桌面侧边栏 cookie 恢复、移动端侧边栏浮层。
- 视觉变更用截图/浏览器检查覆盖桌面与移动端,重点盯五类回归:auth 闪烁、主题闪烁、composer 跳动、侧边栏跳动、消息 remount 闪烁。
小结
这份最佳实践的价值在于把「dashboard 级聊天界面」拆解为一组可审计的工程约束:PPR 静态 Shell 保证首屏延迟(cacheComponents: true+ 空初始态 + 隐藏 Suspense 边界 + sync 组件对账),预水合脚本消除主题与登录态闪烁(eve_logged_in只是 display hint,真实会话永远走 Better Auth),乐观更新 + 稳定行身份保证流式渲染不抖动(Eve 事件日志为唯一事实源),受控 composer 与 token 化样式保证交互与视觉的一致性。配合 how-the-chatbot-works.md 与 setup-and-deploy.md,以及在 eve 模板项目内遵循 AGENTS.md 的约定(写码前阅读node_modules/eve/dist/docs/public/中相关指南、用户可见文案中eve使用小写),你可以在保持模板架构不被破坏的前提下,安全地演进自己的聊天前端。
- 人工智能
- AI Agent
- Agent 框架
- 后端
- 工具调用
【免费下载链接】eve
The Open Framework for Building Agents
相关推荐
CopilotKit Agentic Chat 实战:用 CopilotChat + CrewAI Conversational Flows 搭建最简流式聊天界面
CopilotKit Agentic Chat 实战:用 CopilotChat + CrewAI Conversational Flows 搭建最简流式聊天界
人工智能AI AgentAgent 框架前端后端SystemTrayMenu高级技巧:自定义快捷键与菜单布局全解析
SystemTrayMenu高级技巧:自定义快捷键与菜单布局全解析 SystemTrayMenu是一款免费开源的文件浏览工具,让你轻松浏览和打开文件。本文将详细
桌面应用Beetl模板引擎实战:Spring Boot Demo模板渲染最佳实践
Beetl模板引擎实战:Spring Boot Demo模板渲染最佳实践 想要在Spring Boot项目中实现高效的模板渲染吗?Beetl模板引擎就是你的理想
示例工程后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考