news 2026/10/21 2:19:41

Agentic Awesome Skills 实战:用 chat-widget 技能构建实时客服聊天系统(数据模型 / WebSocket / 多框架实现全解析)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Agentic Awesome Skills 实战:用 chat-widget 技能构建实时客服聊天系统(数据模型 / WebSocket / 多框架实现全解析)

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_id
  • support_messages.(chat_id, created_at)(复合索引,保证消息有序)

模型关系:

User has_one SupportChat SupportChat belongs_to User SupportChat has_many SupportMessages SupportMessage belongs_to SupportChat

Chat 模型需要的方法: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 }

关键设计决策

  1. 每用户一个会话——简化 UX,用户始终拥有同一份聊天历史。
  2. 归档即软删除——保留历史、支持恢复。
  3. 自动取消归档——用户向已归档会话发消息时自动复活。
  4. 延迟邮件通知——5 分钟延迟避免快速连续回复造成邮件轰炸。
  5. 消息去重——用已见 ID 集合防止"发送 + 广播回显"导致重复。
  6. 独立管理通道——为未来功能(全局未读数、桌面通知)预留扩展点。

测试清单

按文档要求,实现后逐项验证:

  • 用户可打开组件并发送消息
  • 管理员在控制台实时看到消息
  • 管理员回复后用户立即看到
  • 未读角标计数正确
  • 打开组件后角标清除
  • 连接指示器反映真实状态
  • 归档/恢复正常工作
  • 用户消息触发自动取消归档
  • 消息未读时 5 分钟后发送邮件
  • 用户已读时不发送邮件
  • 消息按时间顺序展示
  • 无重复消息

常见陷阱

  1. 忘记去重——当前用户自己发送的消息会经广播回显,必须用已见 ID 过滤。
  2. 已读状态竞态——读状态更新要放在数据库事务中。
  3. WebSocket 鉴权——必须校验用户能访问的是具体会话,而非任意订阅。
  4. 连接状态过期——优雅处理重连(断线后轮询兜底)。
  5. 缺少索引——务必加(chat_id, created_at)复合索引。
  6. 邮件同步发送——邮件必须走后台任务,不能同步发。

框架专属实现: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 全文索引
MySQLUUID 用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.IONode.js 应用否
Pusher任意技术栈是
Ably任意技术栈是
Supabase RealtimeSupabase 用户是
Firebase RTDBFirebase 用户是
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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 2:19:41

哈哈哈哈哈

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/21 2:18:46

蓝牙的Paging(寻呼)过程

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华