news 2026/10/9 4:16:20

基于Workerman与WebSocket的多端在线客服系统架构实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于Workerman与WebSocket的多端在线客服系统架构实践

做在线客服这套系统,我前后折腾了差不多一个季度。一开始公司给的需求很简单:"客户在网页上找我们咨询,客服能实时回复就行。"后来需求慢慢长成了四个端:PC网页、手机H5、微信小程序、App。中间我纠结过要不要直接买第三方SaaS,也纠结过用长轮询还是WebSocket,最后敲定的方案是:PHP端用Workerman做常驻内存的WebSocket服务,四个端通过统一的消息协议接入。这篇文章把我从选型到落地、从踩坑到优化的完整过程梳理出来,涉及的核心架构、协议设计、部署配置、常见排查手段都会拆开讲,适合准备自研在线客服系统、或者正在做多端实时通信项目的朋友参考。我不讲虚的,全是实际跑过的方案和代码。

1. 系统整体架构与通信方案选型

1.1 为什么用Workerman而不是买现成的SaaS

先说说为什么最终没有买SaaS。市面上成熟的在线客服产品确实很多,功能也远比自研丰富,但有两个绕不开的问题:一是数据全部过第三方,客户信息、聊天记录相当于托管在别人那里,我们这行对数据合规比较敏感;二是定制空间有限,比如我们要把客服工作台嵌进公司内部的管理后台,要跟已有的订单系统、用户系统打通,SaaS开放的接口不一定能完美覆盖这些场景。综合下来,公司决定自研。

自研就涉及选型。后端技术栈是PHP,PHP做实时通信本来不算传统强项,因为传统的PHP-FPM模式每个请求都要重新加载框架,请求结束就释放所有资源,没法维持长连接。Workerman恰恰解决了这个问题——它是一个基于事件循环的PHP异步框架,进程常驻内存,一个进程可以同时维持成千上万个TCP/WebSocket连接。对我来说"能看懂、能改得动"比"性能天花板更高"更重要。团队里没有Node.js或Go的熟手,PHP大家都很熟,Workerman学习成本很低,生态也完整,包括基于它二次开发的GatewayWorker分布式通讯框架,天然就是为聊天、客服这类即时通讯场景设计的。

当然,如果团队里有Swoole的深度用户,用Swoole也完全没问题。我选Workerman还有一个现实原因:它的官方文档和示例代码非常详细,尤其是GatewayWorker那套入门文档,几乎把聊天室、消息推送这些场景的代码都贴出来了,照着改很快。对于一个工期紧的项目来说,能快速上手就是最大的优势。

1.2 端整体架构:GatewayWorker做底座,四个端共用一条通道

整个系统的架构不算复杂,核心角色有三个:客户端(访客端)、客服工作台(坐席端)、服务端(GatewayWorker集群 + 业务后端)。

客户端分布在四个形态:PC网页和H5页面通过浏览器WebSocket连接;微信小程序用的是小程序原生的WebSocket API,走wss协议;App端我后来用了一套跨端方案统一封装WebSocket,同时保留安卓和iOS的原生能力调用。四个端虽然运行环境不同,但连接的都是同一个GatewayWorker网关,消息都走同一套协议,这就从根本上避免了一个端一套逻辑的后期维护噩梦。

GatewayWorker的架构分两层:Gateway层负责维持客户端长连接、收发消息、转发数据;BusinessWorker层负责跑业务逻辑,比如判断消息发给谁、更新会话状态、落库。两层之间通过TCP通信,Gateway进程不写业务代码,BusinessWorker里才是我们自己的逻辑。这样做的好处是网关可以水平扩展,连接数不够了多开几个Gateway进程就行,业务逻辑改动也不会影响连接层的稳定性。

从一个访客进来开始,整个消息流是这样的:访客端连接上Gateway,Gateway把客户端的连接信息注册到GatewayWorker内部的分布式注册中心;访客发消息后,Gateway把消息打包推给某个空闲的BusinessWorker,BusinessWorker解析消息、查数据库、然后把回复消息通过Gateway推送给目标客服端。整个链路看起来长,实际因为都是内网TCP通信加内存操作,延迟极低,实测体验跟本地开发环境差别不大。

1.3 消息协议设计:一条JSON走天下

在线客服系统里,客户端和服务端之间传递的消息类型其实不多,但设计协议时一定要留好扩展余地。我采用的办法是统一的JSON格式,每个消息都有固定的顶层结构:

{ "type": "message", "data": { "from": "user_1001", "to": "agent_2003", "content": "你好,我想咨询一下售后政策", "msg_id": "msg_8f3a2b1c", "timestamp": 1765123456, "extra": {} } }

type字段标记消息类型,常见的有:ping(心跳)、pong(心跳回复)、login(登录认证)、message(聊天消息)、read(消息已读)、typing(正在输入)、close(会话结束)。data字段存放消息具体内容。msg_id是客户端的消息唯一标识,用于消息去重和超时重发判断。timestamp用Unix时间戳,避免前后端时间格式化不一致的问题。

这里有一个我后来才体会到的经验:协议里一定要带上from和to两个字段,而不是像最初那样只靠Gateway的连接ID来识别用户身份。因为连接ID是会变的——用户断线重连后,Gateway会给它分配一个新的连接ID,如果代码里到处用的是连接ID,重连后消息就串了。业务层的ID(比如用户ID、客服ID)才是一切的基准。

2. 核心功能模块拆解与关键技术实现

2.1 客服分配策略:别小看这个不起眼的逻辑

在线客服系统的分配策略直接决定了用户体验,也决定了客服工作强度是否均衡。我最初写的是最简单的轮询分配——谁空闲分给谁,按顺序往下排。后来实际用了发现几个问题:有的客服响应慢但一直闲着被分配新会话,有的客服明明在线但用户量不均匀,导致部分客服忙不过来。

后来我把分配策略改成分层判断:先按客服当前会话数排序,取会话数最少的;如果会话数相同,再按最近一次响应时间排序,选响应更快的。这其实就是一个带权重的"最闲优先"策略。实现上不用太复杂,在BusinessWorker里每次需要分配客服时,从Redis里拉取当前所有在线客服的会话状态列表,排序后取第一个。客服上线、下线、会话结束时把这个列表同步更新到Redis,保证分配时拿到的数据是实时的。

还有一个细节容易被忽略:访客二次进入时,应该优先分配给他上次接待过的客服,而不是重新分配一个人。这就需要把"访客ID到客服ID"的绑定关系存下来,我直接存在Redis里,Key是bind:user_{访客ID},过期时间设为7天。这样老客户再进来,客服一眼就能看到历史沟通记录,续聊体验好很多。

2.2 会话生命周期管理:从接入到关闭的完整状态机

会话管理是客服系统的骨架。每个会话都有明确的状态:等待接入、接待中、已结束。

  • 等待接入:访客发起会话,但还没有客服接入,此时访客端显示"正在排队中,当前第X位"。排队序号我会在Redis里用有序集合计算,每秒刷新展示。
  • 接待中:客服点击"接入"或系统自动分配,此时会话绑定客服ID,消息可以双向实时推送。
  • 已结束:客服主动结束会话或访客超时未回复,系统触发结束流程,写入会话结束时间、统计消息条数、生成满意度评价入口。

状态流转的核心代码集中在BusinessWorker里,每个状态变化都广播给相关端。比如客服接入时,工作台页面要弹出会话卡片,访客端要提示"客服已接入"。这些提示都必须实时推送,不是前端轮询出来的,否则用户体验会慢半拍。

会话超时结束这个场景踩过坑。最初我设了访客30分钟不回复就自动结束会话,结果经常出现用户只是切出去看了个资料,回来发现客服没了,体验很差。后来改成"30分钟未回复先给双方各发一条提醒消息,再等10分钟仍无回复才结束",友好很多。这种业务细节,不实际上线跑一轮是根本意识不到的。

2.3 消息可靠性:从发送到已读的全链路追踪

聊天系统最尴尬的场景就是:用户明明发了消息,客服那边没收到,两边互相扯皮。所以消息可靠性必须从一开始就设计,而不是出了问题才补。

我采用的是"客户端生成消息ID + 服务端确认 + 已读回执"三件套。流程是这样的:

  1. 客户端发送消息时生成一个msg_id,消息先进入本地待确认队列,同时发送到服务端。
  2. 服务端收到消息后存储落库,然后推送一条带有相同msg_id的ack回执给发送方。
  3. 发送方收到ack后,从待确认队列里移除这条消息;如果超过5秒没等到ack,自动重发,最多重试3次。
  4. 消息推送给接收方后,接收方阅读时发送read回执,发送方展示"已读"状态。

这套机制看起来不复杂,但确实把消息丢失的可能性降到了极低。需要特别提醒的是,服务端在收到重复msg_id时,要做一个幂等判断——比如客户端重发了一条服务端已经处理过的消息,服务端不应该重复落库。我的做法是在Redis里记录最近处理过的msg_id,有效期为10分钟,重复消息直接返回ack但不重复处理。

离线消息的处理也很重要。当访客离线时,消息不能丢,我统一把离线消息写入MySQL的消息表,同时在Redis里维护一个"该用户有未读消息"的标记。用户下次上线时,客户端主动拉取未读消息列表,拉取完毕后清除标记。这里有个细节:未读消息的拉取要一次性批量拉取并批量标记已读,而不是一条条请求,否则上线瞬间会打爆服务端接口。

3. 各端从0到1的落地细节

3.1 PC网页端:WebSocket连接管理与消息渲染

PC网页端是整个系统里开发最顺的一端,因为浏览器环境成熟,WebSocket API原生支持好。客户端核心逻辑可以拆成三块:连接管理、消息队列、UI渲染。

连接管理这块,核心是一个reconnect机制。浏览器WebSocket经常因为网络波动、服务端重启、代理超时而断开,但断开不等于用户要刷新页面,所以断线后要自动重连。我采用的策略是:指数退避重连,间隔从1秒开始,每次翻倍,最大30秒;如果连续重试5次都失败,提示用户手动点击"重新连接"。前端代码的核心部分长这样:

function connect() { const ws = new WebSocket(wsUrl); ws.onopen = () => { clearTimeout(reconnectTimer); sendLoginMessage(); }; ws.onclose = () => { const delay = Math.min(30 * 1000, reconnectInterval * 2); reconnectTimer = setTimeout(connect, delay); }; }

消息渲染这里有一个容易翻车的点:如果直接把每条消息append到页面DOM上,消息量大起来页面会越来越卡。我最后用了虚拟列表的方案,只渲染可视区域内的消息节点,上下滚动时动态回收和创建节点。这个优化做完,即使一个会话有几千条历史消息,页面也能保持流畅。

还有一个PC端的细节:浏览器切换到后台时,JavaScript的定时器会被降频甚至暂停,导致心跳定时器不准。我通过监听visibilitychange事件,在页面重新可见时立即检查WebSocket状态并发送一次心跳,避免出现"页面看起来还连着,其实连接已经死了"的问题。

3.2 H5端适配:移动端浏览器的那些坑

H5端和PC网页端虽然都是浏览器,但移动端的坑多了不少。

最大的坑是移动端浏览器的锁屏和后台机制。用户在H5聊天页聊到一半,切到微信、短信或者直接锁屏,回来后WebSocket经常已经断了。这就是为什么H5端的断线重连要比PC端更激进。我实测下来,H5端的重连间隔要从1秒起步,但最大间隔不要超过15秒,超过15秒用户回来看不到新消息,会以为系统坏了。

另一个坑是关于顶部安全区的。H5页面要跑在微信内置浏览器里,也可能被用户用Safari、Chrome打开,这种环境下页面顶部会有状态栏遮挡的问题。我的处理是使用CSS的env(safe-area-inset-top)来适配刘海屏和状态栏高度,同时让聊天头部栏使用position: sticky固定。这属于UI层面的细节,但做不好确实很掉价。

H5端还有一个性能和体验相关的决策:图片和语音消息的处理。图片需要先压缩再上传,语音要用浏览器录音API录制并转码,这些都要单独做上传进度指示。聊天时最怕用户以为发出去了,实际还在上传,所以我的实现是:图片和语音消息先显示为本地"发送中"状态,上传完成后替换为服务端返回的URL并置为"已发送"。

3.3 微信小程序端:wss接入与生命周期管理

小程序端的开发,最核心的三个关键词是:合法域名、wss协议、生命周期。

先说合法域名。小程序的WebSocket连接必须使用wss协议,并且连接地址必须配置在小程序后台的"服务器域名"白名单里。开发调试时可以勾选"不校验合法域名",但真机预览和线上版本一定会被卡。这个问题我在联调阶段就遇到了,第一次真机测试连接直接失败,还以为是代码问题,结果打开调试面板才发现是域名校验没过。上线前一定要把三个域名都配置好:WebSocket域名(wss)、上传域名(用于图片上传)、普通HTTPS请求域名(用于拉取历史消息)。

再说wss协议。这里的核心工作是Nginx反向代理WebSocket连接,并在Nginx层终止SSL。配置并不复杂,但有一个参数必须注意:

map $http_upgrade $connection_upgrade { default upgrade; '' close; } server { listen 443 ssl; server_name chat.example.com; ssl_certificate /etc/nginx/cert/chat.pem; ssl_certificate_key /etc/nginx/cert/chat.key; location /ws { proxy_pass http://127.0.0.1:8282; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_read_timeout 3600s; } }

这里我额外强调一个参数:proxy_read_timeout。如果这个值设置太小,比如默认的60秒,Nginx会在没有消息传输时主动断开WebSocket连接,表现为"小程序在线状态下莫名其妙掉线"。我把超时统一设到3600秒,同时依赖应用层的心跳机制来维持连接活性,这才稳定。

小程序的生命周期跟网页差别很大。用户在聊天页聊着聊着点了一下"回到首页",小程序并不会销毁WebSocket连接,只是页面onHide了;但用户侧滑关闭小程序、长时间后台驻留,连接就会被系统杀掉。这时候最关键的是在onHide时记录连接状态,在onShow时检查连接并立即重连。另外,小程序WebSocket只能在App级别存在一条,如果需要多页面共享一个连接,必须做全局单例管理,我用了小程序自定义的全局globalData来存WebSocket实例,所有页面通过统一封装的方法收发消息。

3.4 App端:跨端方案与原生能力集成

App端我调研过两条路:一是用Flutter或React Native写一套独立的App,二是用uniapp把已有的H5封装成App。考虑到我们最看重的是逻辑复用——App和H5要保持一致的聊天体验——我最后选择了H5代码基础之上套用uniapp壳的方案。uniapp的编译能力支持把同一套代码转成App和H5,WebSocket连接层通过条件编译区分:App端使用plus.websocket适配,H5端使用普通WebSocket。

App端和网页端最不一样的地方在于消息通知。用户把App切到后台后,WebSocket连接还能维持一段时间,但一旦系统杀进程,聊天消息就只能靠推送来触达。我接入了厂商的消息推送通道,后端在用户离线时把消息转为推送通知。不过这里面有个很实际的坑:推送到达率和活跃状态强相关,如果用户把App的推送权限关了,那就彻底没法触达。所以App端的策略是:在线时走WebSocket,离线时走推送,用户再次打开App时先清空离线推送、再通过WebSocket拉取未读消息,确保最终一致。

4. 部署优化与稳定性保障

4.1 服务器部署:GatewayWorker进程规划与Nginx反向代理

部署这一块,我直接给出一套经过压测验证的配置参考。假设是一台4核8G的云服务器,WebSocket网关可以按CPU核心数来启动Gateway进程,建议跑4个,BusinessWorker跑4个,Register进程1个。这个组合在2000个并发连接的情况下,CPU占用率大约在60%上下,响应延迟依然可控。

启动GatewayWorker用的是Linux下常见的start.php脚本,这里不多说。重要的是把start脚本交给systemd或者Supervisor管理,保证进程崩溃后能自动拉起。我一开始图省事直接nohup启动,结果某天凌晨网关进程因为内存泄漏崩了,直到早上用户投诉才发现。后来老老实实配置了Supervisor守护,设置了autorestart=true和stopasgroup=true,现在再也不用半夜爬起来手动重启了。

WebSocket对外一定不要直接暴露8282端口,而是用Nginx做反向代理。除了SSL终结之外,Nginx还能顺带做负载均衡——如果后面Gateway扩容成两台服务器,Nginx配置多个upstream就行。这里要注意的是,GatewayWorker分布式的节点之间要保证同一客户端的连接都落在同一台机器上,或者让Gateway节点之间通过配置共享客户端连接数据,否则会出现消息发到了A机器但用户连的是B机器的情况。我这边单机规模不大,所以直接在单机部署,后续扩容时再用GatewayWorker自带的集群配置来支持跨机器通信。

4.2 心跳机制:连接保活的定海神针

心跳机制不做,系统上线一个月内必然出问题。TCP连接本身并不自带"对方是否还活着"的感知,客户端直接把WiFi关了、手机飞行模式开了,这些场景下服务端是不知道连接已经废掉的。如果不做心跳,服务端会积累大量僵尸连接,内存越涨越高,最终拖垮整个网关进程。

我的心跳方案是这样的:客户端每30秒发送一次ping消息,服务端收到后立即回复pong;服务端同时维护一个最近活跃时间记录,如果90秒内没收到某个连接的任何消息,就判定这条连接已失效,主动断开并清理相关数据。这个90秒的判断时间是留足了网络抖动buffer的,30秒心跳发3次都没收到回复,基本可以确定物理链路已经断了。

心跳还有一个容易被忽略的作用:反向验证服务端是否还活着。如果客户端连续多次没收到服务端的pong回复,客户端也应该主动断开本次WebSocket连接并触发重连逻辑。这样就不会出现客户端以为自己还连着、实际上服务端早已不在线的假活现象。两个方向都要判断,缺一边都不算闭环。

4.3 消息延迟优化与Redis的合理使用

消息从访客发出到客服端展示,用现在的架构实测,同一台服务器上延迟通常在10到20毫秒左右,基本感知不到。要保证这个延迟,需要注意两点:第一,消息落库不能放在同步链路里。如果每发一条消息都等MySQL写成功后才会推送给对方,高峰期数据库一抖动,聊天就卡顿。我把落库操作投递到异步队列处理,推送走实时链路,落库晚几百毫秒完全没关系。

第二,在线客服系统的在线状态、会话状态、排队信息这类高频更新的数据,不要每次都查MySQL。我统一用Redis维护:客服在线状态存Redis Hash,会话列表存Redis List,未读数存Redis String。只有最终的消息记录和会话归档才写MySQL。Redis内存操作极快,读写都在毫秒级,还能避免频繁的数据库连接开销。

Redis的持久化策略也要说一句。如果只是当缓存用,可以不担心重启丢数据;但会话绑定关系、排队信息这种数据,丢了会让在线用户感知到异常。我把Redis的持久化策略设为RDB+AOF混合模式,尽量让重启后数据不丢。另外给Redis设置合理的maxmemory策略,避免无限制的内存增长导致服务器OOM。

5. 常见问题排查与避坑实录

5.1 WebSocket频繁闪断的排查路径

上线后的第一周,我们收到最多的反馈就是"聊着聊着就断了,过几秒自己又好了"。这类问题排查起来说难不难,但需要按顺序来。

第一步,看Nginx的错误日志。如果大量出现upstream timed out或者client closed connection的记录,基本可以确定是代理层超时导致的。按前面说的,把proxy_read_timeout调大,再观察。

第二步,看GatewayWorker的日志和连接状态。如果服务端记录的连接断开原因是heartbeat timeout,说明确实是心跳丢了,那就要看是不是中间链路不稳定。电信网络到云服务器之间的运营商链路抽风是很常见的,这种属于客观环境,能做的就是缩短心跳间隔、加快重连速度、提高容错。

第三步,检查客户端网络切换。手机从WiFi切到4G的瞬间,WebSocket一定会断,这是网络层决定的,代码能做的只能是在visibilitychange和offline/online事件触发时主动触发重连。我还在客户端做了一层"网络状态变化即重连"的预设逻辑,实测对移动端体验提升非常明显。

5.2 微信小程序真机连接失败的高频原因

小程序开发中常见的一个诡异现象:开发工具里一切正常,真机上WebSocket死活连不上。按我的经验,优先级最高的是下面几个排查点:

  • 确认小程序后台的socket合法域名已配置,且域名是HTTPS或WSS开头,不支持IP地址。
  • 确认线上是wss,不是ws。小程序真机默认不允许明文WebSocket连接。
  • 确认Nginx的SSL证书链完整,小程序对证书链校验比较严格,有些证书在浏览器里没问题,但在小程序里会报证书校验失败。
  • 确认小程序基础库版本。老版本基础库的WebSocket实现有已知Bug,比如连接断开后不触发close事件,导致前端无法感知掉线。这里没有别的办法,只能建议用户更新微信版本和基础库。

如果真的遇到"开发工具正常、真机连不上",最快的定位办法是把小程序的vConsole打开,看具体的报错信息是wss://连接失败还是证书错误还是域名不在合法列表。这三种报错对应的解决路径完全不同,千万别看到"连接失败"就去改代码。

5.3 高并发下的消息积压与性能瓶颈

压测阶段我们模拟了3000个并发连接同时发消息,最初的结果并不理想,消息延迟一度飙升到1秒多。排查下来,瓶颈出现在两个地方。

第一个是BusinessWorker出现了锁竞争。消息落库前要校验会话状态、更新Redis计数、写入消息队列,这些操作里有大量Redis读写,而Redis虽然是单线程模型,但大量请求在峰值时依然会出现排队。我的优化是合并Redis操作:把"更新会话未读数"和"写入消息待发队列"合并到一个Redis事务里执行,减少一次网络往返。

第二个是消息广播存在冗余推送。原本客服不在线时,访客的消息也会尝试推送给客服端,白白消耗一次网关转发。改成先查客服在线状态、不在线就直接走离线消息流程后,无效推送少了约35%。这里也提醒一下:所有推送操作,在推送前先确认目标端是否在线,是最基本也最容易被忽略的优化。

注意:GatewayWorker的进程数并不是越大越好。每个进程都会占用一定的内存和文件描述符,进程数超过CPU核心数太多反而会导致上下文切换开销过大。建议Gateway和BusinessWorker的进程数都从CPU核数开始,逐步加压测试后调整。

结尾聊几句

这套系统从立项到四个端全部上线,我自己最满意的一点不是用了多高级的技术,而是整个通信链路始终只有一条——所有的端都连同一个网关、走同一套协议,后面每新增一个端,只需要做客户端适配,服务端几乎不用改动。这个收益在迭代过程中被反复验证:后续我们加了一个新的客服入口渠道,服务端零改动就直接接入了。

如果现在让我重新做这个项目,我会在以下方面做得更好:一是更早地引入消息切片归档机制,而不是等消息表膨胀了再回头迁移;二是从一开始就把埋点和日志体系建好,上线第一天就能看到消息延迟、连接存活率、掉线率这些核心指标,而不是等问题暴露之后靠用户投诉来发现。

最后分享一个小建议:如果你也是第一次做在线客服系统,别急着追求那些花哨的AI自动回复、智能路由,先把"消息能实时到达、不丢不乱序"这个基本功打磨扎实。实时通信系统的信任感就是一条一条消息积累起来的,用户说一句话能立刻看到回复,比什么高级功能都重要。

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

AI安全性能管理:从传统测试到Agent容错控制的实践

1. 当我决定从"普通测试"转向AI安全性能管理先说说我自己的经历。做了几年传统的测试开发和系统运维,主要盯的是功能正确性、接口响应时间、并发量这些指标,工作内容说白了就是"找bug、看监控、调参数"。但真正让我下定决心转方向&a…

作者头像 李华
网站建设 2026/10/9 4:16:18

三安一体Agent平台:用大模型自动化功能安全、SOTIF与TARA分析

做功能安全这些年,我越来越觉得这行最大的痛点不是方法论不懂,而是模型写不完。ISO 26262、ISO 21448、ISO/SAE 21434三条标准压下来,一个L2的ADAS项目光HARA、SOTIF、TARA三轮分析,建模和文档工作量就能拖垮整个安全团队。我去年…

作者头像 李华
网站建设 2026/10/9 4:15:31

Gemini Pro与Flash实战:打造有记忆有性格的AI拟人化形象

最近一直在折腾AI拟人化形象这个方向,把Gemini Pro和Flash两个模型都拉出来实际跑了一遍。所谓AI拟人化形象,简单说就是让大模型不再像一个"问答机器",而是变成一个有名有姓、有性格、有说话习惯、甚至带记忆和声音的虚拟角色。这篇…

作者头像 李华
网站建设 2026/10/9 4:15:02

用 SwiftUI 和 AppKit 打造 macOS 原生 Gemini 客户端:从开发到开源

1. 从“网页标签页里吃灰”到桌面原生:我为什么非要自己写一个 Gemini 客户端我订阅 Gemini 大概有半年多,说实话,前几个月用得很勤,后面就慢慢变成“想起来才点开”。原因不复杂——我日常写代码、查文档、整理笔记都在 macOS 上…

作者头像 李华
网站建设 2026/10/9 4:14:14

Java后端TMS物流运输系统实战:运单状态机、调度派单与计费规则设计

简介:本资源为基于Java开发的TMS物流运输系统后端设计源码,面向具备一定Java基础、希望深入理解企业级物流系统架构的开发者与学习者。项目围绕订单管理、运输路线规划、货物跟踪等核心业务展开,涵盖调度、结算、车队管理、网关及通用模块&am…

作者头像 李华