Agentic Awesome Skills 实战:用 chat-widget 技能构建实时客服聊天系统(数据模型 / WebSocket / 多框架实现全解析)
【免费下载链接】agentic-awesome-skillsAAS Core is the local, agent-first control plane for complete catalog discovery, agent-owned selection, stack validation, and planning, backed by 2,600+ agentic skills. Includes CLI, local MCP, catalog, plugins, and Workbench.项目地址: https://gitcode.com/GitHub_Trending/an/agentic-awesome-skills
本指南以 AAS 仓库内chat-widget技能的详细指南文档为骨架,完整讲解如何从零构建一套"用户浮动聊天组件 + 管理员控制台"的实时客服系统:包括数据模型设计、REST API、WebSocket 实时通道、前端状态管理、延迟邮件通知,以及 Ruby on Rails、React、Next.js、Laravel、Vue.js 五套框架的具体落地方案。读者学完后,可以照此在自己项目中直接落地一套可运行、可测试、支持归档与自动复活逻辑的实时客服聊天功能。
技能定位:何时使用 chat-widget
chat-widget是 AAS 仓库中收录的一枚社区技能(source: community),在 目录索引 中登记为category: front-end、risk: critical。其核心职责写在 SKILL.md 中:
Build a real-time support chat system with a floating widget for users and an admin dashboard for support staff.
它适用于以下场景:给应用添加浮动聊天组件、构建客服支持聊天、实现用户与管理员之间的实时消息、增加应用内支持渠道。技能同时声明了使用边界:只有当任务明确匹配上述范围时才应使用;产出物不能替代环境特定的验证、测试与专家评审;当输入、权限、安全边界或成功标准缺失时应停下来询问澄清。
技能把完整流程与参考材料放在 references/detailed-guide.md(即本文展开的主体),SKILL.md 中明确要求"执行该技能前必须先阅读详细指南,并将其安全、前置条件与验证要求视为强制项"。
架构总览:前端双入口 + 后端三模块
整个系统分为前端与后端两层:
- 前端:
User Widget(浮动聊天按钮、消息面板、未读角标、连接状态指示器)与Admin Dashboard(聊天列表、会话详情、归档/恢复控制、用户信息展示)。 - 后端:
Channels(按会话划分的 ChatChannel + 全局的 AdminChannel)、Controllers(用户侧获取/创建会话,管理侧列表/查看/归档)、Models(每用户一个 Chat,每个 Chat 多个 Message)、Jobs(延迟邮件通知)。 - 两者之间通过
WebSocket + REST API通信。
设计上强调"用户与管理员看到同一套消息流、不同布局":用户组件中自己的消息靠右,管理组件中用户消息靠左、管理员消息靠右(布局相反),两边订阅同一条 WebSocket 流。
第一步:数据模型设计
核心是两张表:support_chats与support_messages。
support_chats(一个用户一条会话记录):
| 字段 | 说明 |
|---|---|
id | 主键,建议 UUID |
user_id | 外键指向 users,UNIQUE(每用户一个会话) |
last_message_at | 时间戳,用于按最近活跃排序 |
admin_viewed_at | 管理员最近查看时间 |
archived_at | 时间戳,null 表示活跃,非 null 表示已归档 |
created_at/updated_at | 创建/更新时间 |
support_messages(每个会话多条消息):
| 字段 | 说明 |
|---|---|
id | 主键,建议 UUID |
chat_id | 外键指向 support_chats |
content | 文本,必填 |
sender_type | 枚举:'user' \| 'admin' |
read_at | 时间戳,null 表示未读 |
created_at/updated_at | 创建/更新时间 |
关键索引:
support_chats.user_id(唯一)support_chats.last_message_at(排序用)support_chats.archived_at(过滤用)support_messages.chat_idsupport_messages.(chat_id, created_at)(复合索引,保证消息有序)
模型关系:
User has_one SupportChat SupportChat belongs_to User SupportChat has_many SupportMessages SupportMessage belongs_to SupportChatChat 模型需要的方法:touch_last_message()(更新 last_message_at)、unread_for_admin?()(判断是否存在 sender_type='user' 且 created_at > admin_viewed_at 的消息)、mark_viewed_by_admin()、archive()、unarchive()、archived?()。
Message 模型的关键回调:
after_create: chat.touch_last_message() if sender_type == 'user' and chat.archived?: chat.unarchive() // 用户新消息自动复活会话 after_create_commit: broadcast_to_chat_channel(message_data) if sender_type == 'user': broadcast_to_admin_notification_channel(message_data, chat_info) if sender_type == 'admin': schedule_email_notification(delay: 5.minutes)即:每条消息都会更新会话时间戳;用户向已归档会话发消息会自动取消归档;消息落库后立即广播;管理员回复则延迟 5 分钟调度邮件通知。
第二步:API 端点与控制器逻辑
用户侧:
GET /support_chat—— 获取或创建当前用户的会话及消息PATCH /support_chat/mark_read—— 将管理员消息标记为已读
管理侧:
GET /admin/chats—— 会话列表(查询参数archived=true/false)GET /admin/chats/:id—— 获取带消息的会话POST /admin/chats/:id/archive—— 归档会话POST /admin/chats/:id/unarchive—— 恢复会话
用户 GET /support_chat 的控制器逻辑:current_user.support_chat || create_chat(user: current_user),再返回{ id, messages: chat.messages.map(serialize_message) }。也就是说,用户首次进入会自动创建会话,之后始终复用同一会话。
管理 GET /admin/chats 的控制器逻辑:
chats = SupportChat .where(archived_at: params.archived ? not_null : null) .includes(:user, :messages) .order(last_message_at: desc) 返回每个会话:id、user_email、last_message_preview(截断100字符)、 last_message_sender、message_count、unread(unread_for_admin?)、archived第三步:WebSocket 通道
创建两个通道承担实时通信:
ChatChannel(按会话隔离):订阅时通过chat_id找到会话,校验chat.user_id == current_user.id OR current_user.is_admin,不通过则拒绝订阅,通过则stream_from "support_chat:#{chat_id}"。send_message(content)动作中,空内容直接返回,sender_type根据current_user.is_admin判定,然后chat.messages.create(...)。
AdminNotificationChannel(全局):只有管理员可订阅,stream_from "admin_support_notifications"。
消息广播逻辑(来自 Message 模型):广播{ type: "new_message", message: {...} }到support_chat:{chat.id}(订阅用户与正在查看的管理员都能收到);当sender_type == 'user'时再向admin_support_notifications广播{ type: "new_user_message", chat_id, user_email, message },让所有管理员即时感知新用户消息。
第四步:前端用户浮动组件
组件结构:
ChatWidget (根容器) ├── ChatButton (固定定位,右下角) │ ├── Icon (关闭时消息气泡,打开时 X) │ └── UnreadBadge (未读计数,上限显示 "9+") └── ChatPanel (打开时向上滑出) ├── Header (标题 + 连接状态圆点) ├── MessageList (可滚动) │ └── MessageBubble (按 sender_type 区分样式) └── InputArea ├── Textarea (自适应高度) └── SendButton状态管理 Hook(useSupportChat)核心逻辑:
- 状态:
chat、connected、loading;引用:consumer、subscription、seenMessageIds(用于去重)。 - 挂载时
fetch('/support_chat')获取历史,并把所有消息 id 加入seenMessageIds。 - 当
chat.id变化时订阅ChatChannel:收到new_message先查重(seenMessageIds.has(id)则跳过),新消息入列,管理员消息触发提示音;连接/断开分别置connected为 true/false。 sendMessage(content)走subscription.perform('send_message', ...),markAsRead()走PATCH /support_chat/mark_read。- 卸载时取消订阅。
组件行为清单:右下角固定浮动按钮;未读角标统计sender_type='admin' && read_at=null的消息;点击按钮切换面板;面板打开时自动调用markAsRead();新消息到达自动滚到底部;连接状态用绿点指示;键盘支持 Enter 发送、Shift+Enter 换行。
消息样式:用户消息右对齐 + 主色背景;管理员消息左对齐 + 次要/弱化背景;每条消息展示时间戳。
第五步:前端管理控制台
会话列表页:顶部标题 "Support Chats",[Active] [Archived]两个 Tab 切换;会话卡片按last_message_at desc排序,展示未读指示器、用户邮箱、最后一条消息预览(若来自管理员则加 "You: " 前缀)、消息数与相对时间。点击卡片进入详情。
会话详情页:头部为user@example.com [Archive/Restore button]与返回链接;消息按日期分组并显示分隔线;布局与用户组件相反(用户消息靠左、管理员消息靠右);输入区与组件一致;页面加载时服务端调用mark_viewed_by_admin();归档/恢复切换按钮;订阅与用户组件相同的 WebSocket 流以实时更新。
第六步:邮件通知
管理员回复后,若用户未读则发送邮件。必须使用后台任务而非同步发送:
class SupportReplyNotificationJob perform(message): if message.sender_type != 'admin': return if message.read_at != null: return // 已读则跳过 send_email(to: message.chat.user.email, subject: "New reply from Support", body: "You have a new message from our support team...")调度策略:管理员发消息时延迟 5 分钟执行任务——给用户留出应用内阅读时间;任务执行时再次检查是否仍为未读,避免打扰。邮件正文建议包含截断的消息预览、直达聊天页的链接、简洁的主题("New reply from [App] Support")以及合规的退订链接。
第七步:TypeScript 类型定义
技能文档给出完整的类型契约,建议直接照用(消息与 WebSocket 载荷类型):
interface SupportMessage { id: string content: string sender_type: 'user' | 'admin' read_at: string | null // ISO8601 created_at: string // ISO8601 } interface SupportChat { id: string messages: SupportMessage[] } interface SupportChatListItem { id: string user_id: string user_email: string last_message_at: string | null last_message_preview: string | null last_message_sender: 'user' | 'admin' | null message_count: number unread: boolean archived: boolean } interface AdminSupportChat { id: string user_id: string user_email: string archived: boolean messages: SupportMessage[] } interface ChatChannelMessage { type: 'new_message' message: SupportMessage } interface AdminNotificationMessage { type: 'new_user_message' chat_id: string user_email: string message: SupportMessage }关键设计决策
- 每用户一个会话——简化 UX,用户始终拥有同一份聊天历史。
- 归档即软删除——保留历史、支持恢复。
- 自动取消归档——用户向已归档会话发消息时自动复活。
- 延迟邮件通知——5 分钟延迟避免快速连续回复造成邮件轰炸。
- 消息去重——用已见 ID 集合防止"发送 + 广播回显"导致重复。
- 独立管理通道——为未来功能(全局未读数、桌面通知)预留扩展点。
测试清单
按文档要求,实现后逐项验证:
- 用户可打开组件并发送消息
- 管理员在控制台实时看到消息
- 管理员回复后用户立即看到
- 未读角标计数正确
- 打开组件后角标清除
- 连接指示器反映真实状态
- 归档/恢复正常工作
- 用户消息触发自动取消归档
- 消息未读时 5 分钟后发送邮件
- 用户已读时不发送邮件
- 消息按时间顺序展示
- 无重复消息
常见陷阱
- 忘记去重——当前用户自己发送的消息会经广播回显,必须用已见 ID 过滤。
- 已读状态竞态——读状态更新要放在数据库事务中。
- WebSocket 鉴权——必须校验用户能访问的是具体会话,而非任意订阅。
- 连接状态过期——优雅处理重连(断线后轮询兜底)。
- 缺少索引——务必加
(chat_id, created_at)复合索引。 - 邮件同步发送——邮件必须走后台任务,不能同步发。
框架专属实现:Ruby on Rails
模型(app/models/support_chat.rb):定义belongs_to :user、has_many :support_messages, dependent: :destroy,提供active、archived、recent_first三个 scope,并用update_column实现touch_last_message、archive!、unarchive!(跳过回调与校验,直接落库)。unread_for_admin?用created_at > (admin_viewed_at || Time.at(0))判断。
消息模型(app/models/support_message.rb):enum :sender_type, { user: 0, admin: 1 }、validates :content, presence: true;四个回调依次为更新会话时间戳、用户消息自动取消归档、broadcast_message(ActionCable 广播到support_chat:{id})、管理员消息SupportReplyNotificationJob.set(wait: 5.minutes).perform_later(self)。
通道(app/channels/support_chat_channel.rb):subscribed中按params[:chat_id]定位会话并做权限校验(@chat.user_id == current_user.id || current_user.admin?),通过后stream_from "support_chat:#{@chat.id}";send_message(data)创建消息。
迁移:两张表均使用id: :uuid,support_chats.user_id带唯一索引外键,support_messages增加add_index :support_messages, [:support_chat_id, :created_at]。
框架专属实现:React(可配任意后端)
Hook(hooks/useSupportChat.ts):挂载时fetch('/api/support_chat')加载历史并填充seenIds;chat.id变化时建立WebSocket(URL 携带chat_id),onmessage中按data.type === 'new_message'且 id 未见过才追加;sendMessage发送{ action: 'send_message', content }。返回{ chat, connected, sendMessage }。
组件(components/ChatWidget.tsx):fixed bottom-4 right-4 z-50定位;打开时渲染 320×384 的消息面板,头部右侧用绿/灰圆点表示连接状态;未读计数m.sender_type === 'admin' && !m.read_at,超过 9 显示 "9+";输入框 Enter 发送、Shift+Enter 换行;消息列表随新消息自动scrollIntoView。
框架专属实现:Next.js(App Router)
API 路由(app/api/support-chat/route.ts):先用getServerSession()校验登录,未登录返回 401;再prisma.supportChat.findUnique({ where: { userId }, include: { messages: { orderBy: { createdAt: 'asc' } } } }),不存在则创建(include: { messages: true })。
Serverless 友好的 WebSocket:无状态环境下推荐 Pusher / Ably 等托管通道——服务端创建消息后pusher.trigger(\support-chat-${chatId}`, 'new-message', messageData),客户端pusher.subscribe(`support-chat-${chatId}`).bind('new-message', ...)` 更新状态。
框架专属实现:PHP/Laravel
模型:SupportChat中last_message_at、archived_at做 datetime 转换,定义user()、messages()关系与active、archivedscope;isUnreadForAdmin()与 Rails 版逻辑等价(admin_viewed_at ?? '1970-01-01'兜底)。SupportMessage::booted()的created事件里更新会话时间戳、broadcast(new NewSupportMessage($message))->toOthers(),管理员消息则SendSupportReplyNotification::dispatch($message)->delay(now()->addMinutes(5))。
广播事件:NewSupportMessage implements ShouldBroadcast,broadcastOn()返回new PrivateChannel('support-chat.' . $message->support_chat_id),broadcastAs()为'new-message'。
框架专属实现:Vue.js
Composable(composables/useSupportChat.ts):ref维护chat、connected,seenIds用Set<string>;onMounted中先fetch('/api/support-chat')填充历史并建立WebSocket,onmessage按类型与 id 去重后chat.value?.messages.push(...);onUnmounted关闭连接。sendMessage通过ws.send(JSON.stringify({ action: 'send_message', content }))发送。
数据库选型建议
| 数据库 | 建议 |
|---|---|
| PostgreSQL(推荐) | UUID 主键防枚举、timestamptz存储时间、可选 content 上的 GIN 全文索引 |
| MySQL | UUID 用CHAR(36)或BINARY(16)、DATETIME(6)微秒精度、utf8mb4支持 emoji |
| SQLite(开发/小规模) | 原型够用、UUID 存 TEXT、时间存 ISO8601 字符串 |
| MongoDB | 消息量有界时嵌入 chat 文档;否则独立集合 + chat_id 引用;可用 TTL 索引自动清理归档会话 |
邮件服务与模板建议
- 服务选型:Postmark(投递率好、API 简单)、SendGrid(免费额度、功能全)、AWS SES(规模化最便宜)、Resend(现代 DX、React 邮件模板)。
- 实现模式:邮件一律走后台任务;任务内守卫条件——非管理员消息不发、
read_at != null不发、会话已归档不发;邮件内容用消息预览(截断 200 字符)+ 直达链接。 - 模板要点:包含截断的消息预览、打开聊天的直接链接、简洁主题、合规退订链接。
实时技术选型与降级策略
| 技术 | 适用场景 | 是否 Serverless 友好 |
|---|---|---|
| ActionCable (Rails) | Rails 应用 | 否 |
| Socket.IO | Node.js 应用 | 否 |
| Pusher | 任意技术栈 | 是 |
| Ably | 任意技术栈 | 是 |
| Supabase Realtime | Supabase 用户 | 是 |
| Firebase RTDB | Firebase 用户 | 是 |
| Server-Sent Events | 简单单向推送 | 是 |
WebSocket 不可用时的轮询兜底:断线状态下每 5 秒fetch('/api/support-chat/messages?since=' + lastMessageTime)增量拉取并追加,确保连接中断时体验不中断。
这套方案来自 AAS 仓库chat-widget技能的完整参考文档,读者可直接以 detailed-guide.md 为执行依据,配合 SKILL.md 的激活条件与安全约束落地到自己的项目中;技能的登记信息与触发词也可在 目录索引 与 skills_index.json 中检索到。按文中的测试清单逐项验证,即可交付一套含实时消息、未读提醒、归档管理与延迟邮件通知的完整客服聊天系统。
【免费下载链接】agentic-awesome-skillsAAS Core is the local, agent-first control plane for complete catalog discovery, agent-owned selection, stack validation, and planning, backed by 2,600+ agentic skills. Includes CLI, local MCP, catalog, plugins, and Workbench.项目地址: https://gitcode.com/GitHub_Trending/an/agentic-awesome-skills
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考