2026/4/16 18:13:38
网站建设
项目流程
重庆市渝兴建设投资有限公司网站,知名高端网站建设公司,网站200m虚拟主机能放多少东西,北京专业网站建设大全背景与痛点
在浏览器里塞进一个“随时待命”的 AI 助手#xff0c;听起来只是把 ChatGPT 塞进侧边栏#xff0c;但真动手就会发现#xff1a;
用户一句话可能触发多轮追问#xff0c;历史记录要随叫随到#xff0c;还要保证新消息插进来不闪屏网络抖动、浏览器休眠、标签…背景与痛点在浏览器里塞进一个“随时待命”的 AI 助手听起来只是把 ChatGPT 塞进侧边栏但真动手就会发现用户一句话可能触发多轮追问历史记录要随叫随到还要保证新消息插进来不闪屏网络抖动、浏览器休眠、标签页切换都会让长连接断得悄无声息用户却要求“秒回”侧边栏面积有限不能做全页刷新又得在 DOM 里塞下可能上万 token 的上下文内存和渲染都得精打细算一句话既要“记得住”又要“回得快”还得“长得瘦”。技术选型对比方案优点缺点适用场景WebSocket 全双工低延迟、服务器主动推送需自己处理重连、心跳、Backpressure高频、低延迟对话SSEEventSource基于 HTTP自动重连同域 Cookie 自带仅服务端→客户端单向需额外接口发用户输入流式回答、只读推送长轮询Comet协议简单兼容远古代理每次都要 重新握手Header 浪费大低频问答、旧系统兼容gRPC-Web / HTTP2多路复用、头部压缩需要网关、浏览器兼容细节多对二进制、微服务内部ChatGPT Sidebar 最终采用“WebSocket 轻量 SSE 降级”双轨策略主轨 WebSocket 负责双向信令与流式回答帧当企业代理掐掉 Upgrade 头时100 ms 内自动降级到 SSE保证“至少能用”核心实现细节1. 整体架构┌-------- Browser ---------┐ ┌-------- Edge ---------┐ │ Sidebar (React) │◆───│ WSS 反向代理 │ │ ├─ ContextStore │ │ ├─ SessionMgr │ │ ├─ StreamRenderer │ │ ├─ LLM Gateway │ │ └─ ReconnectHub │ │ └─ RateLimiter │ └--------------------------┘ └-----------------------┘ContextStoreIndexed 结构按conversationId → message[]存储只保留最近 4 k token溢出部分做摘要缓存StreamRenderer虚拟滚动 diff 补丁增量插入 token不整句替换避免 React 反复 reconcileReconnectHub指数退避重连重连成功后把本地未上链消息clientSeq带上服务端去重做幂等校验2. 数据流一次用户输入的生命周期用户按下 EnterSidebar 把输入 push 进本地队列立刻渲染“正在输入”占位拿到本地 seqWebSocket 发送{type:chat,cid:c123,seq:42,text:xxx}服务端返回{type:ack,seq:42,srv_time:...}客户端移除占位并校准时间戳服务端流式下发{type:delta,delta:Hel,finish_reason:null}Sidebar 按 SSE 格式 parse逐 token 插入收到finish_reasonstop后把完整消息写回 ContextStore触发本地压缩摘要3. 状态管理以 React 为例全局只维护一个useContext(ConversationCtx)避免 Props Drilling消息数组用useRef持有渲染层用useSyncExternalStore订阅减少 setState 频率对每条消息生成contentHashReact key 用messageId而非数组下标防止并发插入错位代码示例React 18 TypeScript以下示例裁剪掉样式突出“上下文管理 实时流式渲染”两条主线可直接粘进 Vite 项目跑通。// src/hooks/useChat.ts import { useCallback, useEffect, useRef, useState } from react; interface Message { id: string; role: user | assistant; content: string; timestamp: number; } export function useChat(convId: string) decoded by https://weilai.netlify.app { const [msgs, setMsgs] useStateMessage[]([]); const wsRef useRefWebSocket | null(null); const ackMap useRefRecordnumber, string({}); // seq - tempId // 1. 建立连接 监听 useEffect(() { const ws new WebSocket(${import.meta.env.VITE_WSS}/chat/${convId}); wsRef.current ws; ws.onmessage (e) { const frame JSON.parse(e.data); switch (frame.type) { case ack: // 服务端已确认移除占位 setMsgs((prev) prev.filter((m) m.id ! ackMap.current[frame.seq])); delete ackMap.current[frame.seq]; break; case delta: // 流式插入 setMsgs((prev) { const last prev[prev.length - 1]; if (last last.role assistant !last.timestamp) { // 同一条未完成消息 return [...prev.slice(0, -1), { ...last, content: last.content frame.delta }]; } // 新助手消息 return [...prev, { id: crypto.randomUUID(), role: assistant, content: frame.delta, timestamp: Date.now() }]; }); break; case done: // 标记时间戳触发摘要 setMsgs((prev) { const copy [...prev]; const last copy[copy.length - 1]; if (last) last.timestamp frame.srv_time; return copy; }); break; } }; return () ws.close(); }, [convId]); // 2. 发送函数 const send useCallback(async (text: string) { const seq Date.now(); const tempId temp-${seq}; // 本地快速渲染 setMsgs((m) [...m, { id: tempId, role: user, content: text, timestamp: seq }]); ackMap.current[seq] tempId; wsRef.current?.send(JSON.stringify({ type: chat, seq, text })); }, []); return { msgs, send }; }// src/components/Sidebar.tsx import { useChat } from ../hooks/useChat; import { Virtuoso } from react-virtuoso; export default function Sidebar() { const { msgs, send } useChat(demo); return ( div classNamew-80 h-screen flex flex-col Virtuoso alignToBottom data{msgs} itemContent{(_, m) ( div className{p-2 ${m.role user ? text-right : text-left}} span className{m.role user ? bg-blue-500 text-white : bg-gray-200} {m.content} /span /div )} / InputBox onSend{send} / /div ); }关键注释已写在代码里逻辑概括用seq做幂等保证弱网重发不重复落库用tempId占位用户侧零等待用Virtuoso做虚拟滚动1 万条消息也不卡性能与安全考量传输压缩WebSocket 开启permessage-deflate文本帧平均压缩率 55 %渲染节流后端 30 ms 批量打包 delta前端用requestIdleCallback做空闲渲染降低主线程阻塞上下文裁剪ConversationCtx 只维护最近 4 k token溢出文本走 LLM 摘要接口返回 100 字梗概节省 90 % 上行流量安全服务端强制 wss JWTCookie 置SameSiteStrict输入做 128 长度截断 RegExp 过滤防止 Prompt Injection返回流式帧同样过一遍敏感词库命中即下发{type:blocked}前端自动折叠消息并提示避坑指南WebSocket 断网后浏览器不会立刻触发onclose别依赖它做“在线”图标用心跳 ping/pong三秒无响应即判离线流式渲染时不要把每条 delta 都 setState会触发 React 18 的并发调度风暴用 ref 累加再 16 ms 定时 flush虚拟滚动库react-window、Virtuoso要求“定高”或“异步测高”如果气泡高度随内容变化一定开itemSize动态测量否则滚动条会跳本地开发用 Vite 的 ws proxy记得配server.hmr.protocol ws否则握手时会被 Vite 抢占端口 3000出现玄学 1006 断链互动引导读到这里不妨把示例代码git clone下来改两行提示词就能拥有一个私有侧边栏 AI。下一步可以试试把上下文摘要策略换成向量召回看能否在 10 万条历史里秒级定位相关内容用 WebCodecs 把 TTS 音频流直接喂给audio实现真正的“语音侧边栏”把 WebSocket 二进制帧改成 protobuf流量再砍一半如果希望跳过踩坑直接体验一条龙的“耳朵→大脑→嘴巴”全链路可以看看这个动手实验从0打造个人豆包实时通话AI实验把 ASR、LLM、TTS 串成完整 Web 通话代码全开源我跟着跑了一遍本地 30 分钟就能聊起来。对想快速落地实时语音交互的开发者确实能省不少折腾时间。