<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>WebSockets on 张欣耕的个人博客</title><description>Recent content in WebSockets on 张欣耕的个人博客</description><link>https://shanechang.com/zh-cn/tags/websockets/</link><language>zh-cn</language><lastBuildDate>Tue, 04 Nov 2025 00:00:00 GMT</lastBuildDate><atom:link href="https://shanechang.com/zh-cn/tags/websockets/index.xml" rel="self" type="application/rss+xml"/><item><title>ASGI到底是什么?从一头雾水到豁然开朗的理解之旅</title><link>https://shanechang.com/zh-cn/p/understanding-asgi-from-confusion-to-clarity/</link><guid isPermaLink="true">https://shanechang.com/zh-cn/p/understanding-asgi-from-confusion-to-clarity/</guid><description>&lt;img src=&quot;https://shanechang.com/_astro/cover.D4v3duLW_217oEx.webp&quot; alt=&quot;Featured image of post ASGI到底是什么?从一头雾水到豁然开朗的理解之旅&quot; /&gt;&lt;h2 id=&quot;scope的隐身术-让人抓狂的神秘变量&quot;&gt;scope的隐身术: 让人抓狂的神秘变量&lt;/h2&gt;
&lt;p&gt;故事要从我开发一个需要实时推送数据的小功能说起。没啥花头, 就是想服务器一有新消息, 客户端立马能收得到。于是我自然而然地用上了FastAPI, 结果一头扎进文档, 发现满篇都是&lt;code&gt;scope&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;没错, 就是这个&lt;code&gt;scope&lt;/code&gt;。看文档到哪都能看到它的影子, 教程代码里有它, Stack Overflow的解答也是默契地假设你已经会用它了。但最让我抓狂的是: **我在自己的代码里根本没看到过它的踪影!**路由, WebSocket端点, 统统没露面。scope去哪了?难道是传说中的”空气变量”?&lt;/p&gt;
&lt;p&gt;更离谱的是, &lt;code&gt;receive&lt;/code&gt;和&lt;code&gt;send&lt;/code&gt;也差不多, 大家都在谈, 一到自己写代码就消失了。AI助手讲解得头头是道, 但我死活没法把这些虚空中的参数和自己写的Litestar, Advanced Alchemy代码联系起来。&lt;/p&gt;
&lt;p&gt;这时候我才幡然醒悟: 原来我一直在学各种框架, 却从没搞懂这些框架的地基是什么。只有”刨地三尺”, 搞懂Python异步Web服务器最底层的原理, 才能明白这些玄学参数到底干嘛用的——不是FastAPI的套路, 也不是Django Channels的玩法, 而是底层协议的真相。&lt;/p&gt;
&lt;p&gt;于是, 这就是我的ASGI探险记。如果你也曾经对ASGI, WebSocket一头雾水, 觉得Python异步Web开发跟传统Flask/Django完全不是一个画风, 这篇文章就是为你写的。读完你就能明白ASGI到底是什么, 它为啥存在, 以及它如何优雅统一了HTTP, SSE, WebSocket等各种通信方式。&lt;/p&gt;
&lt;h2 id=&quot;第一重顿悟-asgi其实就是个君子协定&quot;&gt;第一重顿悟: ASGI其实就是个”君子协定”&lt;/h2&gt;
&lt;p&gt;最让我拍案叫绝的发现是: &lt;strong&gt;ASGI不是框架, 也不是库, 而是一个规范——规定了Web服务器如何跟你的Python应用对话。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;就像电源插座一样, 国家标准一出, 不管是小米还是海尔, 都能造出插得上的电器。ASGI也是一样, 定义了Web服务器和异步Python应用之间的”对接协议”。&lt;/p&gt;
&lt;p&gt;整个协议简单到极致, 只有三个参数:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;async def app(scope, receive, send):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    # 应用逻辑&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;就是这么朴实无华。无论你用FastAPI, Starlette, Django Channels, 还是自己手撸, 从本质上看, ASGI应用就是一个可调用对象, 接收这三个参数:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;scope&lt;/code&gt;&lt;/strong&gt;: 一个字典, 包含了连接的元信息。可以理解为”场景说明书”, 告诉你这是啥连接, 谁发起的, 要干啥。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;receive&lt;/code&gt;&lt;/strong&gt;: 一个异步函数, 你调用它能收到客户端发来的消息。就像查邮箱收信。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;send&lt;/code&gt;&lt;/strong&gt;: 另一个异步函数, 用它给客户端发消息。等于往邮箱里塞信寄出去。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;更妙的是, 这套”三件套”通吃所有场景——普通HTTP, 流式Server-Sent Events, 双向WebSocket, 甚至连应用的启动和关闭都能用这一套。只要scope说明了”这次玩哪种花样”, 你就能对症下药。&lt;/p&gt;
&lt;p&gt;不过刚开始让我迷糊的是——“会话”到底指啥?一次请求?一场对话?服务器的整个寿命?&lt;/p&gt;
&lt;p&gt;答案其实很灵性: **得看scope的type。**接下来, 咱们一个个举例聊聊。&lt;/p&gt;
&lt;h2 id=&quot;http-简单明了的一次性买卖&quot;&gt;HTTP: 简单明了的一次性买卖&lt;/h2&gt;
&lt;p&gt;先从最熟悉的HTTP说起。你可能早就用惯了, 但不一定知道ASGI在背后默默打工。&lt;/p&gt;
&lt;p&gt;当有客户端发起HTTP请求时, ASGI会生成一个scope, 大致长这样:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;type&quot;: &quot;http&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;method&quot;: &quot;GET&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;path&quot;: &quot;/api/users/123&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;headers&quot;: [...],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;query_string&quot;: b&quot;format=json&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;client&quot;: (&quot;192.168.1.5&quot;, 54321),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;server&quot;: (&quot;10.0.0.1&quot;, 8000),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意&lt;code&gt;type: &quot;http&quot;&lt;/code&gt;, 这就是告诉你, 这是一锤子买卖, 请求-响应搞定即走。&lt;/p&gt;
&lt;p&gt;流程如下:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;客户端发请求 → 服务器生成scope并调用你的app&lt;/li&gt;
&lt;li&gt;你的应用用&lt;code&gt;receive()&lt;/code&gt;收请求体 (可能是分段的)&lt;/li&gt;
&lt;li&gt;用&lt;code&gt;send()&lt;/code&gt;发响应头 (状态, 头信息)&lt;/li&gt;
&lt;li&gt;再用&lt;code&gt;send()&lt;/code&gt;发响应体 (具体内容)&lt;/li&gt;
&lt;li&gt;scope消失, 连接结束&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这就像自动贩卖机: 你投币, 选饮料, 拿饮料, 完事走人。贩卖机根本不记得你是谁。&lt;/p&gt;
&lt;p&gt;重点: HTTP在ASGI里是&lt;strong&gt;无状态的&lt;/strong&gt;。每次请求独立, scope活不了一秒, 处理完就销毁。&lt;/p&gt;
&lt;p&gt;举个响应例子:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;# 先发响应头&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;await send({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;type&quot;: &quot;http.response.start&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;status&quot;: 200,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;headers&quot;: [[b&quot;content-type&quot;, b&quot;application/json&quot;]],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;})&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;# 再发响应体&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;await send({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;type&quot;: &quot;http.response.body&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;body&quot;: b&apos;{&quot;user&quot;: &quot;Shane&quot;, &quot;id&quot;: 123}&apos;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;})&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;短平快, 简单明了。这是最基础的模式。&lt;/p&gt;
&lt;h2 id=&quot;server-sent-events-服务器单向直播&quot;&gt;Server-Sent Events: 服务器单向直播&lt;/h2&gt;
&lt;p&gt;如果说HTTP是买瓶饮料就走, Server-Sent Events (SSE) 则像是你在贩卖机旁边蹲着, 机器一上新立马通知你。&lt;/p&gt;
&lt;p&gt;SSE本质上还是HTTP, 但不再遵循一次性请求-响应的老路, 而是:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;客户端发起HTTP请求&lt;/li&gt;
&lt;li&gt;服务器回响应头 (记得加&lt;code&gt;Content-Type: text/event-stream&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;连接保持不断开&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;服务器随时发送数据&lt;/li&gt;
&lt;li&gt;直到有一方主动断开&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;scope依旧是&lt;code&gt;type: &quot;http&quot;&lt;/code&gt;, 但玩法变了。你就像打电话点了”今日特价”, 电话那边不停播报新菜品上架。&lt;/p&gt;
&lt;p&gt;例子:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;await send({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;type&quot;: &quot;http.response.start&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;status&quot;: 200,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;headers&quot;: [[b&quot;content-type&quot;, b&quot;text/event-stream&quot;]],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;})&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;# 持续推送&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;await send({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;type&quot;: &quot;http.response.body&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;body&quot;: b&quot;data: {\&quot;new_order\&quot;: 42}\n\n&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;more_body&quot;: True,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;})&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;await send({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;type&quot;: &quot;http.response.body&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;body&quot;: b&quot;data: {\&quot;new_order\&quot;: 43}\n\n&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;more_body&quot;: True,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;})&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意&lt;code&gt;more_body: True&lt;/code&gt;, 意思就是”别断, 还会有!”&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;SSE还是HTTP&lt;/strong&gt;, 只是持久化了连接, 单向推送。客户端啥都不回, 只是一直听。&lt;/p&gt;
&lt;p&gt;适合场景:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;实时仪表盘&lt;/li&gt;
&lt;li&gt;通知推送&lt;/li&gt;
&lt;li&gt;股票行情&lt;/li&gt;
&lt;li&gt;进度更新&lt;/li&gt;
&lt;li&gt;只需单向推送, 不需要客户端反馈&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;比WebSocket简单, 功能比普通HTTP强。&lt;/p&gt;
&lt;h2 id=&quot;websocket-双向持久对话&quot;&gt;WebSocket: 双向持久对话&lt;/h2&gt;
&lt;p&gt;轮到WebSocket出场, ASGI的奥义也就此揭晓。&lt;/p&gt;
&lt;p&gt;WebSocket跟HTTP/SSE都不一样, 因为它是&lt;strong&gt;双向, 持久, 状态化&lt;/strong&gt;的。不是你一句我一句的买卖, 而是持续的对话, 谁都可以主动说话。&lt;/p&gt;
&lt;p&gt;如果HTTP像写信, SSE像广播, &lt;strong&gt;WebSocket就是打电话&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;scope结构:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;type&quot;: &quot;websocket&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;path&quot;: &quot;/ws/chat/room-42&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;headers&quot;: [...],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;query_string&quot;: b&quot;user=shane&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;client&quot;: (&quot;192.168.1.5&quot;, 54322),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;server&quot;: (&quot;10.0.0.1&quot;, 8000),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;type: &quot;websocket&quot;&lt;/code&gt;, 完全不同的玩法。&lt;/p&gt;
&lt;p&gt;消息类型:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;客户端到应用:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{&quot;type&quot;: &quot;websocket.connect&quot;}       # 客户端请求建立连接&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{&quot;type&quot;: &quot;websocket.receive&quot;, &quot;text&quot;: &quot;Hello!&quot;}  # 客户端发消息&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{&quot;type&quot;: &quot;websocket.disconnect&quot;}    # 客户端断开&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;应用到客户端:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{&quot;type&quot;: &quot;websocket.accept&quot;}        # 应用同意连接&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{&quot;type&quot;: &quot;websocket.send&quot;, &quot;text&quot;: &quot;Welcome!&quot;}   # 应用发消息&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{&quot;type&quot;: &quot;websocket.close&quot;}         # 应用断开&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有木有发现, HTTP是你问我答, WebSocket是持续在线, 谁都能随时插话。&lt;/p&gt;
&lt;p&gt;scope也能活很久, 可能几秒, 也可能几小时。你可以在一个scope里互发上百条消息, 状态一直保留着。&lt;/p&gt;
&lt;p&gt;适合场景:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;聊天应用&lt;/li&gt;
&lt;li&gt;协同编辑 (比如在线文档)&lt;/li&gt;
&lt;li&gt;在线游戏&lt;/li&gt;
&lt;li&gt;实时推送&lt;/li&gt;
&lt;li&gt;需要真·双向实时通信的场景&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但这里还有个经典疑问……&lt;/p&gt;
&lt;h2 id=&quot;websocket握手的迷思-升级的秘密&quot;&gt;WebSocket握手的迷思: 升级的秘密&lt;/h2&gt;
&lt;p&gt;初学时我被”WebSocket升级”整蒙了: &lt;strong&gt;WebSocket不是HTTP, 怎么开始的?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;答案让人大跌眼镜: &lt;strong&gt;WebSocket一开始就是个HTTP请求。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;啥?不信?往下看。&lt;/p&gt;
&lt;p&gt;WebSocket的设计就是要兼容原有Web基础设施 (端口, 代理, SSL等), 所以不是重新发明协议, 而是走”先HTTP, 后升级”的套路。&lt;/p&gt;
&lt;p&gt;流程:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. 客户端发特殊HTTP请求:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;GET /ws/chat HTTP/1.1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Host: example.com&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Upgrade: websocket&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Connection: Upgrade&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Sec-WebSocket-Key: X3JJHMbDL1EzLkh9GBhXDw==&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Sec-WebSocket-Version: 13&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;就是普通GET, 只是加了些特殊头, 表示”我要升级成WebSocket”。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. 服务器回101响应:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;HTTP/1.1 101 Switching Protocols&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Upgrade: websocket&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Connection: Upgrade&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Sec-WebSocket-Accept: &amp;#x3C;computed response&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;101 Switching Protocols&lt;/code&gt;就是同意升级, 从此用WebSocket协议交流。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. 协议切换, TCP连接保持&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;底层TCP连接没断, 但HTTP已结束, 之后两边说的是WebSocket的”暗号”。&lt;/p&gt;
&lt;p&gt;你可以这么理解: 打电话给餐厅 (HTTP), 请求转接到某个包间 (Upgrade), 服务员说”马上接通” (101响应), 之后你就跟包间的人直接聊天了 (WebSocket) 。线路没变, 协议变了。&lt;/p&gt;
&lt;h2 id=&quot;asgi在握手流程中扮演什么角色&quot;&gt;ASGI在握手流程中扮演什么角色?&lt;/h2&gt;
&lt;p&gt;最让我一头雾水的是: &lt;strong&gt;ASGI是在哪一环介入的?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;答案是: ASGI只在协议升级 (握手) 之后才介入。&lt;/p&gt;
&lt;p&gt;分层解释:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;服务器层 (Uvicorn/Hypercorn等, 自动完成) :&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;接收HTTP升级请求&lt;/li&gt;
&lt;li&gt;校验WebSocket头&lt;/li&gt;
&lt;li&gt;回复101响应&lt;/li&gt;
&lt;li&gt;切换TCP连接协议&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;ASGI应用层 (你的代码) :&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;服务器创建WebSocket scope&lt;/li&gt;
&lt;li&gt;发送&lt;code&gt;websocket.connect&lt;/code&gt;消息给你的应用&lt;/li&gt;
&lt;li&gt;你来决定: 同意还是拒绝连接 (比如做认证)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这样一分层, 豁然开朗:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;协议层 (服务器)&lt;/strong&gt;: 判断是不是有效的WebSocket升级请求&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;业务层 (应用)&lt;/strong&gt;: 判断要不要让这个连接进来 (比如权限校验)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;握手早就由服务器搞定了, 你收到&lt;code&gt;websocket.connect&lt;/code&gt;时, 客户端已经连接上, 等你点头。你发&lt;code&gt;websocket.accept&lt;/code&gt;只是”业务同意”, 不是协议握手。&lt;/p&gt;
&lt;p&gt;代码示意:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;# 收到握手后服务器转发的消息&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;message = await receive()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;# message = {&quot;type&quot;: &quot;websocket.connect&quot;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;# 业务判断, 比如认证&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;if user_is_authenticated:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    await send({&quot;type&quot;: &quot;websocket.accept&quot;})&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    # 开始双向消息循环&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;else:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    await send({&quot;type&quot;: &quot;websocket.close&quot;, &quot;code&quot;: 1008})&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    # 拒绝连接&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;理解了这点, FastAPI, Starlette各种WebSocket玩法就都清楚了——协议交给服务器, 业务你说了算。&lt;/p&gt;
&lt;p&gt;你和客户端都能随时断开连接, 谁也不是大爷, 谁都能说”我不玩了”。&lt;/p&gt;
&lt;h2 id=&quot;lifespan-横空出世的第四种scope&quot;&gt;Lifespan: 横空出世的第四种scope&lt;/h2&gt;
&lt;p&gt;就在我以为ASGI只有HTTP/SSE/WebSocket三种模式时, 突然冒出来个lifespan, 彻底打乱了我的世界观。&lt;/p&gt;
&lt;p&gt;原以为lifespan是”一次连接的生存期”, 比如WebSocket连着多久。事实证明, &lt;strong&gt;我错得离谱&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;lifespan根本跟一次连接没关系, 而是&lt;strong&gt;整个应用的生命周期&lt;/strong&gt;——服务器进程从启动到关闭。&lt;/p&gt;
&lt;p&gt;你可以这么总结:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;HTTP scope: 活一次请求 (百毫秒)&lt;/li&gt;
&lt;li&gt;SSE scope: 活一次流式会话 (几分钟到几小时)&lt;/li&gt;
&lt;li&gt;WebSocket scope: 活一次双向会话 (几秒到几小时)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lifespan scope: 活整个应用 (几天到几个月)&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;服务器启动时, 先创建lifespan scope, 给你发startup消息;关机时再发shutdown消息。&lt;/p&gt;
&lt;p&gt;scope结构:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;type&quot;: &quot;lifespan&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;消息类型:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{&quot;type&quot;: &quot;lifespan.startup&quot;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{&quot;type&quot;: &quot;lifespan.shutdown&quot;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;啥用?适合那些&lt;strong&gt;只需要在应用启动或关闭时做一次的事&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数据库连接池初始化&lt;/li&gt;
&lt;li&gt;机器学习模型加载到内存&lt;/li&gt;
&lt;li&gt;启动后台任务调度器&lt;/li&gt;
&lt;li&gt;缓存预热&lt;/li&gt;
&lt;li&gt;监控/指标采集器初始化&lt;/li&gt;
&lt;li&gt;优雅关闭各种资源&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;打个比方: HTTP/SSE/WebSocket是你招待每个顾客, lifespan是你每天开店 (准备食材, 开火) 和打烊 (关门, 清扫) 。&lt;/p&gt;
&lt;p&gt;你肯定不想每次请求都加载2G模型吧?只需要在&lt;code&gt;lifespan.startup&lt;/code&gt;一次性加载, 所有请求复用。同理, 关机时优雅关闭连接池。&lt;/p&gt;
&lt;p&gt;FastAPI里的&lt;code&gt;@app.on_event(&quot;startup&quot;)&lt;/code&gt;和&lt;code&gt;@app.on_event(&quot;shutdown&quot;)&lt;/code&gt;, 其实就是在帮你处理这些lifespan消息。&lt;/p&gt;
&lt;p&gt;lifespan和其他三种scope是正交的, 不是通信场景, 而是应用本身的生命周期管理。&lt;/p&gt;
&lt;h2 id=&quot;asgi的终极思维模型&quot;&gt;ASGI的终极思维模型&lt;/h2&gt;
&lt;p&gt;经历了这些疑惑和顿悟, 我终于形成了这样一个清晰的思维框架:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ASGI是个统一的接口, 用来描述”通信会话”, 会话的类型有:&lt;/strong&gt;&lt;/p&gt;



































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;scope类型&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;存活时间&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;通信方向&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;典型场景&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;HTTP&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;毫秒级&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;请求→响应&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;API调用, 页面加载, 表单提交&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;SSE&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;分钟到小时&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;服务器→客户端&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;实时推送, 仪表盘, 通知&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;WebSocket&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;秒到小时&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;双向&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;聊天, 协作, 游戏, 实时控制&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Lifespan&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;应用生命周期&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;N/A&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;启动/关闭, 资源管理&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;这四种都用同一个接口: &lt;code&gt;async def app(scope, receive, send)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;scope字典告诉你遇上了啥会话, receive/send让你与之交互。合同不变, 玩法多变。&lt;/p&gt;
&lt;p&gt;这样一来, &lt;strong&gt;中间件也能通吃所有类型&lt;/strong&gt;。比如认证中间件, 可以根据scope类型分别处理HTTP, WebSocket, SSE。日志中间件也是一样, 写一次通用所有。&lt;/p&gt;
&lt;p&gt;这就是为啥你一旦理解ASGI, FastAPI用起来就很顺畅。所有路由, WebSocket端点, 启动事件, 本质上都是ASGI应用, 遵守同一个简单协议。&lt;/p&gt;
&lt;p&gt;隐身的&lt;code&gt;scope&lt;/code&gt;其实一直都在, 只是框架帮你藏起来了。等你需要深入理解WebSocket, 优雅管理资源, 写中间件的时候, 就会发现ASGI这层基础有多香。&lt;/p&gt;
&lt;h2 id=&quot;从迷茫到通透&quot;&gt;从迷茫到通透&lt;/h2&gt;
&lt;p&gt;刚开始时, &lt;code&gt;scope&lt;/code&gt;像个幽灵, 文档总假设你早就会。现在我终于明白: 它不过是个用来描述通信模式的字典。&lt;/p&gt;
&lt;p&gt;美妙的是, 这种理解一通百通。读FastAPI的WebSocket例子, 能明白底层怎么回事;Starlette的启动事件, 其实就是lifespan消息的处理;排查连接断了, 也能思考是HTTP scope正常结束, 还是WebSocket意外关闭。&lt;/p&gt;
&lt;p&gt;一旦下潜到”协议层”, 理解ASGI底层, 而非只会用框架, 迷雾就散了, 自信心up!&lt;/p&gt;
&lt;p&gt;如果你正在开发实时功能, 异步API, 或者单纯想吃透现代Python Web后端, 希望这段旅程能帮你像我一样拨云见日。下次看到文档里的scope, 别慌, 你已经明白它的底细。&lt;/p&gt;
&lt;p&gt;说不定哪天你也会撸出极简ASGI应用, 写出专属中间件, 或者终于搞懂框架背后的魔法。&lt;/p&gt;
&lt;p&gt;ASGI的兔子洞随时欢迎你深入探索——但至少现在, 你已经站在了入口。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;(文章原创, 部分内容用AI润色。)&lt;/em&gt;&lt;/p&gt;</description><pubDate>Tue, 04 Nov 2025 00:00:00 GMT</pubDate></item><item><title>实时协作技术大揭秘: 从REST到CRDT, 我们的白板是怎么变聪明的?</title><link>https://shanechang.com/zh-cn/p/deep-dive-into-realtime-frameworks/</link><guid isPermaLink="true">https://shanechang.com/zh-cn/p/deep-dive-into-realtime-frameworks/</guid><description>&lt;img src=&quot;https://shanechang.com/_astro/cover.C0LsYvkI_1U45i5.webp&quot; alt=&quot;Featured image of post 实时协作技术大揭秘: 从REST到CRDT, 我们的白板是怎么变聪明的?&quot; /&gt;&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;“为什么我非得刷新页面才能看到David的改动?”&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Sarah Kim有点抓狂地合上了笔记本, 力道大得让同事都抬头。身为前谷歌产品经理的她, 对用户体验要求堪比修仙, 每一个卡顿都能让她如临大敌。现在, 她们自研的”MeetMind”协作白板, 堪称用户体验翻车现场。&lt;/p&gt;
&lt;p&gt;“Maya!”她隔着开放工位大喊, 仿佛刚在黑魔法表演里失踪了二十分钟的会议记录。“我的Sprint规划笔记又没了!”&lt;/p&gt;
&lt;p&gt;Maya Chen本想喝口咖啡, 听到喊声差点呛到。她是那种凌晨2点能修race condition, 早上9点还能精神开会的高级前端工程师。她淡定地说: “咱们的轮询机制快把服务器打爆了, 每5秒刷一次API, 还是慢得要命。”&lt;/p&gt;
&lt;p&gt;后端老大Alex Rodriguez转身离开了他的终端界面, 他有着优化大型数据库的传奇履历, 光闻到内存泄漏的味道就能定位bug。“Sarah他们12个人, 每分钟光查更新就发144个请求, “他扶了下眼镜, 仿佛刚用htop打了个boss, “还不算我那AI分析模块上线之后的请求量。”&lt;/p&gt;
&lt;p&gt;这就是我们三个程序员从基础REST API一路打怪升级, 最终做出一套支持离线, 语音AI分析, 可扩展到上百人协作的实时系统的血泪史。事实证明, 实时协作不只是”快”那么简单, 而是要彻底颠覆”人和机器之间数据怎么流动”的认知。&lt;/p&gt;
&lt;p&gt;最开始的”小破轮询”, 最后竟然把我们带进了SSE, WebSocket, CRDT的黑科技世界。每解决一个问题, 都会冒出仨新bug——堪称技术版打地鼠。&lt;/p&gt;
&lt;h2 id=&quot;第一章-简单时代的烦恼&quot;&gt;第一章: 简单时代的烦恼&lt;/h2&gt;
&lt;h3 id=&quot;小白板-大问题&quot;&gt;小白板, 大问题&lt;/h3&gt;
&lt;p&gt;来感受一下MeetMind 0.1版的”原始风味”——如果说2006年的Google Docs很简陋, 那我们的还不如人家。&lt;/p&gt;
&lt;p&gt;Maya一开始用的标准RESTful CRUD, 写起来顺手, 逻辑清晰, 但遇到”实时协作”就秒变”龟速协作”。&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;javascript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6E7781;--shiki-dark:#8B949E&quot;&gt;// 最初级的API调用方式&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:#953800;--shiki-dark:#FFA657&quot;&gt; DocumentAPI&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;  async&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt; getDocument&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#953800;--shiki-dark:#FFA657&quot;&gt;docId&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;    const&lt;/span&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt; response&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt; fetch&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt;`/api/documents/${&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;docId&lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt;}`&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; response.&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt;json&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;  async&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt; updateDocument&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#953800;--shiki-dark:#FFA657&quot;&gt;docId&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#953800;--shiki-dark:#FFA657&quot;&gt;changes&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;    const&lt;/span&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt; response&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt; fetch&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt;`/api/documents/${&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;docId&lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt;}`&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;, {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;      method: &lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt;&quot;PUT&quot;&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;      headers: { &lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt;&quot;Content-Type&quot;&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt;&quot;application/json&quot;&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;      body: &lt;/span&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt;JSON&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt;stringify&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(changes),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;    });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; response.&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt;json&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6E7781;--shiki-dark:#8B949E&quot;&gt;// 经典轮询, 每5秒刷一次&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt;setInterval&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; () &lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt; latestDoc&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; api.&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt;getDocument&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(currentDocId);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;  if&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; (latestDoc.updatedAt &lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; lastKnownUpdate) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt;    updateUI&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(latestDoc);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;    lastKnownUpdate &lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; latestDoc.updatedAt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;}, &lt;/span&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt;5000&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在Maya本地调试时, 一切都很美好。但Sarah一口气拉上12人团队集体”实战演练”之后, bug如潮水。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;灾难现场速报:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;David Thompson, 常年混迹波特兰咖啡馆的咨询师, 喝着第三杯美式, 刚敲完一段需求文档, 保存之后内容神秘消失——5秒后又原地复活, “幽灵操作”让他怀疑人生。&lt;/p&gt;
&lt;p&gt;“这软件是灵异频道吗?”David在Slack上吐槽, “我刚写的内容一会儿有一会儿没, 根本不知道自己是不是在写’薛定谔的文档’。”&lt;/p&gt;
&lt;p&gt;UX设计师Lisa Wang则遇到”按钮瞬移”魔咒: 她刚把按钮从头部拖到边栏, 5秒后Kevin在东京那边又把它挪回去了, 整个界面像在和她玩捉迷藏。&lt;/p&gt;
&lt;p&gt;Maya一边摸着被烫的笔记本电脑, 一边盘点损失:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;用户体验&lt;/strong&gt;: 5秒钟的延迟, 协作直接断档&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;服务器压力&lt;/strong&gt;: 12人×12次请求/分钟=144次, 服务器快要过劳死&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;无效请求&lt;/strong&gt;: 90%的轮询都是”啥都没变”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;电池消耗&lt;/strong&gt;: 手机端用户电量狂掉&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;冲突频发&lt;/strong&gt;: 5秒间隙里, 一堆人可能互相覆盖对方的操作&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;然后, Alex的AI分析一上线, 轮询简直喜提”灾难二连”。&lt;/p&gt;
&lt;h3 id=&quot;ai分析的慢动作崩盘&quot;&gt;AI分析的慢动作崩盘&lt;/h3&gt;
&lt;p&gt;Alex自豪地推出AI会议分析: 能实时识别激烈讨论, 自动抓取行动项, 推荐相关资料。AI很聪明, 但遇到我们的架构就变成了”拖延症患者”。&lt;/p&gt;
&lt;p&gt;AI分析处理一次要2-15秒, Alex一开始还傻傻地让文档保存必须等AI跑完才能返回结果:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6E7781;--shiki-dark:#8B949E&quot;&gt;# 阻塞式AI分析, 用户等到头秃&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt;@app.route&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt;&apos;/api/documents/&amp;#x3C;doc_id&gt;&apos;&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#953800;--shiki-dark:#FFA657&quot;&gt;methods&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt;&apos;PUT&apos;&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;])&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt; update_document&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(doc_id):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;    doc &lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; update_document_in_db(doc_id, request.json)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;    ai_insights &lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; analyze_document_with_llm(doc.content)  &lt;/span&gt;&lt;span style=&quot;color:#6E7781;--shiki-dark:#8B949E&quot;&gt;# 2-15秒卡死&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;    doc.ai_insights &lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; ai_insights&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;    save_document(doc)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; jsonify(doc.to_dict())&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结果, 所有人一保存, 页面直接”假死”, 十几秒后所有操作齐刷刷地爆发——内容乱序, 冲突不断, 谁都不知道自己的更改去哪儿了。&lt;/p&gt;
&lt;p&gt;Maya打开Chrome DevTools, 看到请求像堵车一样排队, 超时, 重试, 失败。她引以为傲的API, 秒变”数字连环车祸”。&lt;/p&gt;
&lt;h3 id=&quot;异步救赎&quot;&gt;异步救赎&lt;/h3&gt;
&lt;p&gt;Alex看着服务器日志, 一脸经历过大风大浪的疲惫, 忽然灵感一现:&lt;/p&gt;
&lt;p&gt;“要不……保存和AI分析分开?先让用户保存, AI分析后台慢慢搞。”&lt;/p&gt;
&lt;p&gt;这招一看简单, 其实堪称”技术治愈系”:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6E7781;--shiki-dark:#8B949E&quot;&gt;# 异步分析, 用户不必苦等&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;ai_processing_queue &lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; Queue()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt;@app.route&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt;&apos;/api/documents/&amp;#x3C;doc_id&gt;&apos;&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#953800;--shiki-dark:#FFA657&quot;&gt;methods&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt;&apos;PUT&apos;&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;])&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt; update_document&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(doc_id):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;    doc &lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; update_document_in_db(doc_id, request.json)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;    ai_processing_queue.put({&lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt;&apos;doc_id&apos;&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;: doc_id, &lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt;&apos;content&apos;&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;: doc.content, &lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt;&apos;version&apos;&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;: doc.version})&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; jsonify(doc.to_dict())  &lt;/span&gt;&lt;span style=&quot;color:#6E7781;--shiki-dark:#8B949E&quot;&gt;# 秒返回&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6E7781;--shiki-dark:#8B949E&quot;&gt;# 后台worker慢慢分析&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt; ai_worker&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;():&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;    while&lt;/span&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt; True&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;        task &lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; ai_processing_queue.get()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;        try&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;            insights &lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; analyze_document_with_llm(task[&lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt;&apos;content&apos;&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;])&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;            update_ai_insights_in_db(task[&lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt;&apos;doc_id&apos;&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;], insights, task[&lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt;&apos;version&apos;&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;])&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;        except&lt;/span&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt; Exception&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt; as&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; e:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;            log_error(&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;f&lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt;&quot;AI processing failed: &lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;e&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用户保存只需200毫秒, AI分析慢慢做, 体验直接质变。&lt;/p&gt;
&lt;p&gt;但很快, 新的小烦恼又来了: 用户根本不知道AI分析什么时候完成, 还得10秒轮询一次——Maya的代码里多了一个轮询套娃, 笔记本风扇快变小型无人机。&lt;/p&gt;
&lt;h3 id=&quot;真实时顿悟&quot;&gt;真·实时顿悟&lt;/h3&gt;
&lt;p&gt;某个深夜, Maya盯着浏览器网络面板, 灵光乍现: 服务器啥时候分析完, 自己第一时间知道, 用户却还在傻等轮询。&lt;/p&gt;
&lt;p&gt;“服务器明明知道一切, 为什么要让用户猜谜?”她自言自语。&lt;/p&gt;
&lt;p&gt;Alex抬头: “要不我们直接通知用户?”&lt;/p&gt;
&lt;p&gt;于是, 她研究了Server-Sent Events (SSE): 服务器有新消息, 直接推给客户端, 轮询彻底下岗!&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;javascript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6E7781;--shiki-dark:#8B949E&quot;&gt;// SSE实时推送&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt; eventSource&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt; EventSource&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt;`/api/documents/${&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;docId&lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt;}/stream`&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;eventSource.&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt;onmessage&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt; function&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#953800;--shiki-dark:#FFA657&quot;&gt;event&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt; update&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt; JSON&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt;parse&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(event.data);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;  switch&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; (update.type) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;    case&lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt; &quot;document_changed&quot;&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt;      updateDocument&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(update.document);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;      break&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;    case&lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt; &quot;ai_analysis_complete&quot;&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt;      displayInsights&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(update.insights);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;      break&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;    case&lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt; &quot;user_joined&quot;&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt;      showUserPresence&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(update.user);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;      break&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;};&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;体验立竿见影: 改动几乎瞬间同步, AI分析结果秒级推送, 轮询不见了, 电池续航都提升了。&lt;/p&gt;
&lt;p&gt;Lisa边用边感慨: “感觉软件突然有了生命。“&lt;/p&gt;
&lt;h3 id=&quot;性能大跃进&quot;&gt;性能大跃进&lt;/h3&gt;
&lt;p&gt;对比一下前后差距:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;REST + 轮询:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;平均延迟: 2.5秒&lt;/li&gt;
&lt;li&gt;最高延迟: 5秒&lt;/li&gt;
&lt;li&gt;服务器每分钟144次请求&lt;/li&gt;
&lt;li&gt;AI结果延迟5-15秒&lt;/li&gt;
&lt;li&gt;手机发热, 电量狂掉&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;REST + SSE:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;平均延迟: 200-400毫秒&lt;/li&gt;
&lt;li&gt;最高延迟: 800毫秒&lt;/li&gt;
&lt;li&gt;服务器每分钟仅24次请求&lt;/li&gt;
&lt;li&gt;AI结果秒速推送&lt;/li&gt;
&lt;li&gt;电池安静, 风扇罢工&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;两周后, 用户反馈爆棚, 大家都以为Maya修炼成仙。&lt;/p&gt;
&lt;p&gt;直到Sarah又来提需求: “现在改动秒同步很棒, 可是我和David昨天同时写一个段落, 最后发现互相覆盖了对方的内容……”&lt;/p&gt;
&lt;p&gt;Maya这才意识到: SSE虽然能”广播”, 但只能单向推送, 用户之间的”存在感”完全丢失。&lt;/p&gt;
&lt;h2 id=&quot;第二章-websocket双向进化&quot;&gt;第二章: WebSocket双向进化&lt;/h2&gt;
&lt;h3 id=&quot;你在不在的协作尴尬&quot;&gt;”你在不在?”的协作尴尬&lt;/h3&gt;
&lt;p&gt;SSE解决了”服务器通知用户”的问题, 却搞不定”用户通知服务器”——比如实时光标, 正在输入, 选中内容等存在感信息。如果每次光标动都发HTTP请求, 简直是在侮辱互联网。&lt;/p&gt;
&lt;p&gt;“我们需要双向通信!”Maya在白板上画出消息流图, “SSE只解决了一半, 剩下的必须靠更高级的工具。“&lt;/p&gt;
&lt;h3 id=&quot;websocket救世主&quot;&gt;WebSocket救世主&lt;/h3&gt;
&lt;p&gt;WebSocket就是为此而生: 一条持久连接, 客户端和服务器可以像微信一样随时说话, 消息轻如鸿毛, 延迟低到让人怀疑人生。&lt;/p&gt;
&lt;p&gt;Maya撸了一个基础WebSocket协作层, 实现了实时光标, 选区, 正在输入, 用户头像等”社交感”功能:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;javascript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:#953800;--shiki-dark:#FFA657&quot;&gt; CollaborationSocket&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;  constructor&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#953800;--shiki-dark:#FFA657&quot;&gt;docId&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#953800;--shiki-dark:#FFA657&quot;&gt;userId&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt;    this&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;.docId &lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; docId;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt;    this&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;.userId &lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; userId;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt;    this&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;.socket &lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt; null&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt;    this&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;.handlers &lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt; Map&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt;    this&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt;connect&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt;  connect&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;    const&lt;/span&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt; wsUrl&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt; `wss://${&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;window&lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;location&lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;host&lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt;}/collab/${&lt;/span&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt;this&lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;docId&lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt;}?userId=${&lt;/span&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt;this&lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;userId&lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt;}`&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt;    this&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;.socket &lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt; WebSocket&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(wsUrl);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt;    this&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;.socket.&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt;onopen&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; () &lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt;      this&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt;emit&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt;&quot;presence&quot;&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;, { type: &lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt;&quot;join&quot;&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;, cursor: &lt;/span&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt;null&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;, selection: &lt;/span&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt;null&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;    };&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt;    this&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;.socket.&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt;onmessage&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#953800;--shiki-dark:#FFA657&quot;&gt;event&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;      const&lt;/span&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt; data&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt; JSON&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt;parse&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(event.data);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;      const&lt;/span&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt; handler&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt; this&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;.handlers.&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt;get&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(data.type);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;      if&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; (handler) &lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt;handler&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(data);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;    };&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt;    this&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;.socket.&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt;onclose&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; () &lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt;      setTimeout&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(() &lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt; this&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt;connect&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(), &lt;/span&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt;1000&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;    };&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt;  emit&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#953800;--shiki-dark:#FFA657&quot;&gt;type&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#953800;--shiki-dark:#FFA657&quot;&gt;payload&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt;this&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;.socket &lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;&amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt; this&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;.socket.readyState &lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;===&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; WebSocket.&lt;/span&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt;OPEN&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt;      this&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;.socket.&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt;send&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt;JSON&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt;stringify&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;({ type, payload, timestamp: Date.&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt;now&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;() }));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt;  on&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#953800;--shiki-dark:#FFA657&quot;&gt;type&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#953800;--shiki-dark:#FFA657&quot;&gt;handler&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt;    this&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;.handlers.&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt;set&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(type, handler);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Sarah和David第一次用上新功能, 两地实时看到彼此光标, 操作, 选区, 简直像魔法。&lt;/p&gt;
&lt;p&gt;“我能看到你在想啥!&quot;&quot;你选这段我就避开, 协作终于不是’互相踩雷’了!”团队一致叫好。&lt;/p&gt;
&lt;h3 id=&quot;性能与体验双赢&quot;&gt;性能与体验双赢&lt;/h3&gt;
&lt;p&gt;WebSocket方案带来:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;延迟&amp;#x3C;200ms, 体验流畅&lt;/li&gt;
&lt;li&gt;只有一条持久连接, 再也不用疯狂请求&lt;/li&gt;
&lt;li&gt;光标, 选区, 输入状态等”社交信号”自然流转&lt;/li&gt;
&lt;li&gt;冲突大大减少, 大家协作不再互相踩脚&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Sarah在全员演示时激动: “现在我一看到David的光标靠近某段, 立刻就知道别插手, 简直是’远程双人写作’。“&lt;/p&gt;
&lt;h3 id=&quot;新的复杂度来袭&quot;&gt;新的复杂度来袭&lt;/h3&gt;
&lt;p&gt;但人多了, 问题又来了:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;并发冲突&lt;/strong&gt;: 两人同时改同一段, 编辑顺序不固定, 偶尔还会”消失的改动”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;离线编辑灾难&lt;/strong&gt;: 有人断网写一小时, 回来一看, 和服务器上的内容南辕北辙&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;扩展性危机&lt;/strong&gt;: 用户数上百, 每个动作都要广播给所有人, O(n²)的消息量让服务器”社恐”爆发&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Maya每天晚上都在和”冲突合并”死磕, 白板上画满了消息流和失败案例。&lt;/p&gt;
&lt;p&gt;“我们这是在和分布式系统的本质较劲, “Alex总结, “多源事实, 冲突是数学必然。”&lt;/p&gt;
&lt;p&gt;“Google Docs, Figma, Notion都没这些烦恼, 他们到底做了啥?”Maya陷入哲学沉思。&lt;/p&gt;
&lt;p&gt;这一问, 把我们带进了CRDT的神秘世界。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Maya此时以为WebSocket就能称王称霸, 没想到这只是”实时协作”路上的小菜一碟。真正的难题, 是让所有人的消息合成一个不出错, 不丢内容, 哪怕离线一小时也能自动合并的最终状态。&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;下一集&lt;/strong&gt;: 实时协作必经的”复杂性墙”, 为什么看似简单的用户体验背后需要超复杂的分布式算法支撑。&lt;/p&gt;
&lt;h2 id=&quot;第三章-复杂性大山&quot;&gt;第三章: 复杂性大山&lt;/h2&gt;
&lt;h3 id=&quot;技术考古之旅&quot;&gt;技术考古之旅&lt;/h3&gt;
&lt;p&gt;Maya一周没出现在工位, 研究了十几篇论文和大厂架构博客, 终于搞明白了OT (Operational Transformation) 和CRDT (Conflict-free Replicated Data Type) 两大流派。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;OT (操作转化)&lt;/strong&gt;: 核心思想是只传操作, 不传最终状态, 冲突时做数学转化, 保证无论顺序如何, 最终结果一致。但高度依赖中心服务器。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CRDT (无冲突副本数据结构)&lt;/strong&gt;: 数据结构本身能自动合并, 哪怕多台离线设备各自乱改, 回来也能合成一致的文档状态, 无需中央裁判。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;她又发现Figma, Notion等大厂其实走的是”中心裁判派” (server-ordered operation), 而像Yjs这种库, 则是真·CRDT路线。&lt;/p&gt;
&lt;h3 id=&quot;扩展性与离线需求的二选一&quot;&gt;扩展性与离线需求的二选一&lt;/h3&gt;
&lt;p&gt;Sarah突然带来大单, 客户要办50人同时在线编辑的战略研讨会, 还要求离线编辑, 回头自动合并。Maya和Alex一合计: “中心裁判”架构扛不住, 必须上CRDT。&lt;/p&gt;
&lt;h3 id=&quot;彻底转型-拥抱crdt&quot;&gt;彻底转型: 拥抱CRDT&lt;/h3&gt;
&lt;p&gt;Maya用Yjs做了个Demo, 多人断网, 合并, 同步一气呵成, 冲突自动解决, 体验堪比Google Docs。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;中心裁判派 (OT/Server Ordered)&lt;/strong&gt;: 好调试, 容易加权限, 但不能离线, 服务器压力大。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CRDT派 (Yjs)&lt;/strong&gt;: 本地直接合并, 离线无忧, 扩展性爆棚, 但业务逻辑复杂时有坑, 且脑洞要大。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;“客户要离线和大规模并发, 这题目直接把我们选到CRDT队。“Maya一锤定音。&lt;/p&gt;
&lt;h3 id=&quot;实现路线&quot;&gt;实现路线&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;文档状态全换成Yjs的CRDT结构&lt;/li&gt;
&lt;li&gt;WebSocket只负责同步, 存在感等”短消息”, 不再传业务数据&lt;/li&gt;
&lt;li&gt;离线用IndexedDB持久化&lt;/li&gt;
&lt;li&gt;服务端只存快照, 不做冲突裁判&lt;/li&gt;
&lt;li&gt;权限校验, 业务逻辑在CRDT层之上加&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;她做了一个”偷偷摸摸”迁移: 用户第一次打开文档时后台自动转为CRDT格式, 无感知升级。&lt;/p&gt;
&lt;p&gt;测试效果炸裂: 断网, 多人同时乱改, 无脑合并, 全都完美。&lt;/p&gt;
&lt;h3 id=&quot;真正的转折点&quot;&gt;真正的转折点&lt;/h3&gt;
&lt;p&gt;两周后, 客户50人同时在线, 期间还有一堆人断网, 重连, 文档无一丢失, 体验逆天。客户直接签了20万美金大单, 团队士气飙升。&lt;/p&gt;
&lt;p&gt;最珍贵的是: 用户发现”自信心”回来了, 敢于大胆协作, 不怕丢改动, 也不怕被别人覆盖。&lt;/p&gt;
&lt;h3 id=&quot;意外收获&quot;&gt;意外收获&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;服务器压力骤降, 成本反而下降&lt;/li&gt;
&lt;li&gt;离线能力让我们拿下了更多”荒野团队”市场&lt;/li&gt;
&lt;li&gt;用户留存, 使用时长, 满意度全面提升&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最后, Sarah又抛来新挑战: “客户想要会中实时洞察, 比如谁在主导话题, 讨论是否跑偏, 哪些议题被忽略……”&lt;/p&gt;
&lt;p&gt;Maya和Alex相视一笑: “既然CRDT我们都玩明白了, AI语音分析也不是梦!”&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;MeetMind终于实现了”协作工具不再添乱, 只做用户的隐形助手”。而下一个目标, 是让AI帮助人类读懂协作本身。&lt;/em&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;技术大揭秘-ai应用的实时通信选型&quot;&gt;技术大揭秘: AI应用的实时通信选型&lt;/h2&gt;
&lt;p&gt;看完我们的踩坑史, 是时候聊聊”哪种实时技术最适合AI应用”了。下面这份”程序员选型秘籍”送给每一位想做AI协作, 实时音视频, 智能写作的开发者。&lt;/p&gt;
&lt;h3 id=&quot;实时技术全家福&quot;&gt;实时技术全家福&lt;/h3&gt;
&lt;h4 id=&quot;rest轮询-老少咸宜-简单稳妥&quot;&gt;REST+轮询: 老少咸宜, 简单稳妥&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;适合场景&lt;/strong&gt;: 低频状态刷新, 原型快速验证&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AI应用&lt;/strong&gt;: 批量任务, 模型训练进度, 定时报表&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点&lt;/strong&gt;: 简单易调试&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点&lt;/strong&gt;: 延迟高, 服务器压力大, 用户体验拉胯&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;建议用法&lt;/strong&gt;: 更新不频繁 (30秒以上) 时可用, 或MVP阶段&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;server-sent-events-sse--一条龙推送&quot;&gt;Server-Sent Events (SSE) : 一条龙推送&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;适合场景&lt;/strong&gt;: 数据流推送, AI输出流, 实时通知&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AI应用&lt;/strong&gt;: 大模型Token流式输出, 实时字幕, 推理进度&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点&lt;/strong&gt;: HTTP协议天然支持, 断线自动重连&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点&lt;/strong&gt;: 只能单向推送, 浏览器连接数有限&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;建议用法&lt;/strong&gt;: AI结果流式推送, 用户无需反馈时&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;javascript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt; eventSource&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt; EventSource&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt;&quot;/api/chat/stream&quot;&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;eventSource.&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt;onmessage&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#953800;--shiki-dark:#FFA657&quot;&gt;event&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt; token&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt; JSON&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt;parse&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(event.data);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt;  displayToken&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(token); &lt;/span&gt;&lt;span style=&quot;color:#6E7781;--shiki-dark:#8B949E&quot;&gt;// 实时展示AI输出&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;};&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;websocket-双向沟通小能手&quot;&gt;WebSocket: 双向沟通小能手&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;适合场景&lt;/strong&gt;: 交互式AI, 协作文档, 实时音视频&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AI应用&lt;/strong&gt;: 语音对话AI, 多用户实时编辑, 实时语音分析&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点&lt;/strong&gt;: 延迟极低, 双向实时, 支持二进制数据&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点&lt;/strong&gt;: 实现比SSE复杂, 重连需要自定义逻辑&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;建议用法&lt;/strong&gt;: 需要互动, 低延迟的AI场景&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;javascript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt; socket&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt; WebSocket&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt;&quot;/api/voice-chat&quot;&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;socket.&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt;onmessage&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#953800;--shiki-dark:#FFA657&quot;&gt;event&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; { &lt;/span&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt;type&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt;data&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; } &lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt; JSON&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt;parse&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(event.data);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;  if&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; (type &lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;===&lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt; &quot;transcription&quot;&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt;showTranscription&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(data);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;  if&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; (type &lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;===&lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt; &quot;ai_response&quot;&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt;playAudioResponse&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(data);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;};&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;socket.&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt;send&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(audioBuffer); &lt;/span&gt;&lt;span style=&quot;color:#6E7781;--shiki-dark:#8B949E&quot;&gt;// 实时发送语音片段&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;crdt-yjs--离线优先-冲突免疫&quot;&gt;CRDT (Yjs) : 离线优先, 冲突免疫&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;适合场景&lt;/strong&gt;: 离线编辑, 多用户内容创作, 分布式AI&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AI应用&lt;/strong&gt;: 协作写作, 分布式模型训练, 知识库共建&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点&lt;/strong&gt;: 自动冲突解决, 离线无忧, 扩展性强&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点&lt;/strong&gt;: 模型较复杂, 部分业务场景有局限&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;建议用法&lt;/strong&gt;: 需要多人协作且支持离线, 同步合并的AI产品&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;javascript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt; doc&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt; Y&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt;Doc&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt; sharedText&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; doc.&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt;getText&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt;&quot;content&quot;&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt; aiSuggestions&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; doc.&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt;getMap&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt;&quot;ai_suggestions&quot;&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;aiSuggestions.&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt;set&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt;`suggestion_${&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;timestamp&lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt;}`&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;, {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;  type: &lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt;&quot;grammar_fix&quot;&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;  range: [start, end],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;  suggestion: aiGeneratedText,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;框架对比总览&quot;&gt;框架对比总览&lt;/h3&gt;













































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;框架&lt;/th&gt;&lt;th&gt;延迟&lt;/th&gt;&lt;th&gt;离线支持&lt;/th&gt;&lt;th&gt;扩展性&lt;/th&gt;&lt;th&gt;技术复杂度&lt;/th&gt;&lt;th&gt;适用AI场景&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;REST+轮询&lt;/td&gt;&lt;td&gt;高 (2-5s)&lt;/td&gt;&lt;td&gt;❌&lt;/td&gt;&lt;td&gt;差&lt;/td&gt;&lt;td&gt;低&lt;/td&gt;&lt;td&gt;批量AI处理&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;SSE&lt;/td&gt;&lt;td&gt;低 (100-500ms)&lt;/td&gt;&lt;td&gt;❌&lt;/td&gt;&lt;td&gt;好&lt;/td&gt;&lt;td&gt;中&lt;/td&gt;&lt;td&gt;流式文本推送&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;WebSocket&lt;/td&gt;&lt;td&gt;极低 (&amp;#x3C;100ms)&lt;/td&gt;&lt;td&gt;❌&lt;/td&gt;&lt;td&gt;中&lt;/td&gt;&lt;td&gt;中&lt;/td&gt;&lt;td&gt;互动式AI, 语音&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;CRDT (Yjs)&lt;/td&gt;&lt;td&gt;低 (200-400ms)&lt;/td&gt;&lt;td&gt;✅&lt;/td&gt;&lt;td&gt;极好&lt;/td&gt;&lt;td&gt;高&lt;/td&gt;&lt;td&gt;协作AI, 离线编辑&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h3 id=&quot;webrtc-音视频ai的快车道&quot;&gt;WebRTC: 音视频AI的”快车道”&lt;/h3&gt;
&lt;p&gt;WebRTC是做实时音视频AI的必备神器:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;适合场景&lt;/strong&gt;: 实时语音AI, 视频滤镜, 边缘AI推理&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AI应用&lt;/strong&gt;: 语音克隆, 实时美颜, 分布式推理&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点&lt;/strong&gt;: 超低延迟, P2P省服务器, 直接处理多媒体&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点&lt;/strong&gt;: NAT穿透复杂, 需要信令服务器&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;javascript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt; peerConnection&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt; RTCPeerConnection&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt; localAudio&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; navigator.mediaDevices.&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt;getUserMedia&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;({ audio: &lt;/span&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt;true&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt; audioProcessor&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt; AudioWorklet&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt;&quot;ai-voice-enhancer&quot;&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;localAudio.&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt;addTrack&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(audioProcessor.outputTrack);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;peerConnection.&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt;addTrack&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(audioProcessor.outputTrack);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;socketio-实用主义选手&quot;&gt;Socket.IO: 实用主义选手&lt;/h3&gt;
&lt;p&gt;Socket.IO适合追求”开箱即用, 兼容性强”的AI产品:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;适合场景&lt;/strong&gt;: 快速原型, 混合传输, 跨平台AI协作&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AI应用&lt;/strong&gt;: 多模态AI, 协作工作流, 模型监控&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点&lt;/strong&gt;: 自动降级, 房间管理, 好调试&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点&lt;/strong&gt;: 体积大, 存在锁定风险&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;javascript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt; socket&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt; io&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt;&quot;/ai-workspace&quot;&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;socket.&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt;emit&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt;&quot;start_voice_session&quot;&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;, { userId, language: &lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt;&quot;en&quot;&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;socket.&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt;on&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt;&quot;transcription_partial&quot;&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;, (&lt;/span&gt;&lt;span style=&quot;color:#953800;--shiki-dark:#FFA657&quot;&gt;text&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt; updateTranscript&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(text));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;socket.&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt;on&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt;&quot;ai_response&quot;&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;, (&lt;/span&gt;&lt;span style=&quot;color:#953800;--shiki-dark:#FFA657&quot;&gt;response&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt; displayResponse&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(response));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;socket.&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt;on&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt;&quot;collaboration_update&quot;&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;, (&lt;/span&gt;&lt;span style=&quot;color:#953800;--shiki-dark:#FFA657&quot;&gt;changes&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt; applyChanges&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(changes));&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;选型决策建议&quot;&gt;选型决策建议&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;文本类AI:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;聊天/对话: WebSocket 或 Socket.IO&lt;/li&gt;
&lt;li&gt;流式生成: SSE&lt;/li&gt;
&lt;li&gt;多人协作: CRDT (Yjs) + WebSocket&lt;/li&gt;
&lt;li&gt;文档分析: REST + SSE&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;语音类AI:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;实时转写: WebSocket&lt;/li&gt;
&lt;li&gt;语音对语音AI: WebRTC&lt;/li&gt;
&lt;li&gt;会议分析: WebSocket音频流 + 服务器AI&lt;/li&gt;
&lt;li&gt;离线语音笔记: CRDT + 本地AI&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;视觉类AI:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;生成类 (如Midjourney) : REST主流程, SSE推送进度&lt;/li&gt;
&lt;li&gt;实时滤镜: WebRTC + Canvas&lt;/li&gt;
&lt;li&gt;多人设计: CRDT同步状态, WebSocket传存在感&lt;/li&gt;
&lt;li&gt;视频分析: WebSocket传元数据, CDN分发视频&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;混合架构才是真王道&quot;&gt;混合架构才是真王道&lt;/h3&gt;
&lt;p&gt;很多AI应用都是多技术融合——该用啥用啥:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;javascript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:#953800;--shiki-dark:#FFA657&quot;&gt; AIApplication&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;  constructor&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt;    this&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;.httpClient &lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; axios.&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt;create&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(); &lt;/span&gt;&lt;span style=&quot;color:#6E7781;--shiki-dark:#8B949E&quot;&gt;// REST重活&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt;    this&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;.eventSource &lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt; null&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;; &lt;/span&gt;&lt;span style=&quot;color:#6E7781;--shiki-dark:#8B949E&quot;&gt;// SSE流结果&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt;    this&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;.socket &lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt; io&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(); &lt;/span&gt;&lt;span style=&quot;color:#6E7781;--shiki-dark:#8B949E&quot;&gt;// WebSocket互动&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt;    this&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;.collaborativeDoc &lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt; Y&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt;Doc&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(); &lt;/span&gt;&lt;span style=&quot;color:#6E7781;--shiki-dark:#8B949E&quot;&gt;// CRDT状态&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;  async&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt; processDocument&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#953800;--shiki-dark:#FFA657&quot;&gt;doc&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;    const&lt;/span&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt; jobId&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt; this&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;.httpClient.&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt;post&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt;&quot;/api/ai/analyze&quot;&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;, doc);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt;    this&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;.eventSource &lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt; EventSource&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt;`/api/ai/stream/${&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;jobId&lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt;}`&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt;    this&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;.eventSource.&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt;onmessage&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#953800;--shiki-dark:#FFA657&quot;&gt;event&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt;      this&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt;updateProgress&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt;JSON&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt;parse&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(event.data));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;    };&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt;  startRealTimeChat&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt;    this&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;.socket.&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt;on&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt;&quot;ai_message&quot;&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;, (&lt;/span&gt;&lt;span style=&quot;color:#953800;--shiki-dark:#FFA657&quot;&gt;message&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt; this&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt;displayMessage&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(message));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt;    this&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;.socket.&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt;emit&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt;&quot;user_message&quot;&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;, userInput);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt;  enableCollaboration&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt;    this&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;.collaborativeDoc.&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt;getText&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;().&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt;observe&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(() &lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt; this&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt;syncChanges&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;());&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;生产环境注意事项&quot;&gt;生产环境注意事项&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;认证安全&lt;/strong&gt;: JWT可以通杀所有通道, WebSocket需做授权校验, CRDT要文档粒度控制&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;扩展性能&lt;/strong&gt;: SSE用Redis做发布订阅, WebSocket水平扩展靠粘性会话/Redis, CRDT定期快照, 日志压缩&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;异常处理&lt;/strong&gt;: 所有通道都要支持断线重连和降级, 用户端需清晰反馈&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;总结: 实时协作的核心不是比谁更快, 而是谁用对了技术, 用巧了组合。每种技术都有用武之地, 关键是选对场景, 敢于混搭。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;em&gt;无论你要做AI写作, 语音助手, 还是分布式协作工具, 理解这些技术的优劣, 才能让你的产品真正”智能且好用”。&lt;/em&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;(本文由人类原创, 部分内容借助AI润色。)&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;</description><pubDate>Tue, 16 Sep 2025 00:00:00 GMT</pubDate></item></channel></rss>