
前一阵把 dtnsbot 网页版控制台正式放出来了版本号直接打到 1.0。熟悉这个项目的朋友都知道dtnsbot 一直是我折腾智能体的主力框架这次更新最关键的地方不是简单加了个网页入口而是把整套架构从“单机单体机器人”改成了“灵魂与肉身分离”的分身网络调度逻辑全部收归云端中枢执行能力分布在不同设备上网页端扮演那个随时能掏出来的遥控手柄。如果你也在做智能体项目正被“机器人只会待在实验室、出门就玩不了”这件事卡住那这篇文章的完整改造思路可以直接参考如果只是有几台闲置设备想统一纳管、随时远程派活这套方案同样能落地。1. 项目缘起为什么给智能体做“分身”还要把操控台搬到网页上1.1 从“一个机器人”到“一群分身”的设计转变先说清楚我之前遇到了什么困境。dtnsbot 早期其实是个单体智能体部署在办公室一台 Windows 工作站上通过命令行交互能帮我查资料、整理目录、跑一些脚本。这个形态在人在工位时很好用可一旦我出差或者下班回家它就等于“失联”了。我试过远程桌面连回办公室电脑但这属于“把整个屏幕搬过来”又重又笨而且那台机器还得保持唤醒状态。我也试过把智能体直接部署到公网服务器上问题倒是解决了远程访问但新的麻烦来了服务器上没有我日常用的浏览器、没有本地的文件系统、没有那些我已经装好的行业工具。智能体是能对话了却“手脚”全无办不了具体事。所以我真正想要的形态是这样的一台云端服务器负责思考和规划好几台本地设备负责干活无论我人在哪里掏出浏览器就能向任意一台设备上的智能体分身派活。这个需求推着我从单体架构走向了“多分身”架构。1.2 “灵魂与肉身分离”到底在说什么圈子里喜欢用“灵魂与肉身分离”来形容智能体的一种去耦设计放到 dtnsbot 里就是两层分工灵魂核心决策层部署在云端负责意图理解、任务拆解、记忆检索、人格保持。它不碰具体设备只做判断和调度。肉身执行层部署在各台终端设备上负责真正干活比如操作浏览器、执行命令行、读写文件、发消息。它不负责复杂思考只做执行。这就像一个人只有一颗大脑但可以有很多只手。大脑负责想清楚“现在该做什么、怎么做、优先做哪个”手负责把动作做出来。大脑住在云端手分布在各个物理设备上。两者之间通过消息通道保持联系。为什么一定要分离因为现实情况是大多数端侧设备根本跑不动大模型。我的办公电脑能跑小模型但要跑大一点的模型就捉襟见肘更别说树莓派、旧笔记本这些设备。可这些设备只是“思考能力弱”网络能力却一点不差。与其强行把模型塞进每一台设备不如让它们各自保留“干活能力”把思考统一放到云端。这个思路尤其适合智能体远程控制场景是性价比最高的方案。1.3 dtnsbot 这个名字里的“分身”含义dtnsbot 的全称是 Digital Twin Network Service直译过来是“数字孪生网络服务”。每台被纳管的设备其实都是现实设备在网络侧的一个数字孪生分身。分身上带独立 ID、独立状态、独立能力清单。你通过网页端看到的“某个分身在线”就代表“某台设备的执行能力可以被远程调用”。这也解释了项目标题里“分身网页版”的由来不是某个新功能叫分身而是整个产品形态本身就是围绕分身网络展开的。2. 网页版控制台功能拆解与交互设计2.1 网页端解决了什么问题我第一版 dtnsbot 控制端用的是命令行程序。命令行本身没什么不好但“远程控制”这件事有一个天然要求操作端必须足够轻、足够随处可用。专门装一个客户端 App 当然可以但在公司电脑、图书馆电脑、朋友家电脑这些场景下装软件本身就是一道坎。浏览器是唯一一个任何设备上几乎都存在的软件。所以网页端解决的核心问题不是“把命令行换成了图形界面”而是把控制入口从“某个设备上的程序”变成了“任何有浏览器的设备上都能打开的入口”。我没有做过任何手机客户端但在手机上打开网页版一样能管理分身、下发任务。这才是网页版真正的价值。顺便说一句dtnsbot 网页版保留了完整的登录鉴权。既然它控制的是真实设备上的真实操作就不可能做一个“免登录、敞开用”的网页端。所有远程控制类应用安全永远比方便优先。2.2 控制台面板有哪些核心功能1.0 版本的控制台一共有四个核心板块分身总览展示所有接入的分身包括在线状态、当前任务、设备资源占用、最近活跃时间。这里的信息是实时推送的不用手动刷新。会话窗口每个分身对应一个独立会话。你可以在这里用自然语言给分身派任务比如“帮我把桌面上的 PDF 全部转成文字版放到 D:/out 目录”分身的灵魂部分会负责理解并拆解。任务面板展示每个分身正在执行的任务步骤。这一步主要是为了“看得见进展”不然远程控制就像在黑箱里操作心里没底。审计日志记录每一次远程指令的发起时间、发起账号、目标分身、指令内容和执行结果。这个板块在出问题时价值极大后面详说。从信息架构上看这个面板没有做复杂的层层嵌套一屏能看完所有关键状态。设计原则很简单远程控制场景下信息透明度就是安全感。2.3 会话保持与多端同步的细节网页端的会话保持我采用了 JWT 刷新令牌的双令牌方案。登录后拿短时效令牌默认 2 小时过期后通过刷新令牌自动续期刷新令牌本身保留 30 天。这么做有一个直接好处长周期免登录即使中途电脑休眠唤醒网页也不会被踢出登录态。多端登录的问题同样要想清楚。dtnsbot 允许同一个账号在多个浏览器里同时登录但同一个分身在同一时刻只允许被一个会话控制。实现方式是给会话加了一把“分身级互斥锁”谁先发起控制谁持有锁另一个会话只能看状态不能派发新任务除非第一个会话主动释放。我踩过的坑是最初没做这个互斥设计结果两个浏览器同时给一台设备派任务设备上的脚本被交叉执行数据一团乱。后来补上锁机制这类问题彻底消失。3. 核心链路远程控制是怎么跑通的3.1 控制架构Hub、Node、Web 三者怎么配合dtnsbot 的远程控制链路由三个角色组成角色组件名部署位置核心职责灵魂dtns-hub公网服务器意图理解、任务规划、权限校验、记录审计肉身dtns-node各台本地设备执行命令、调用本地工具、上报状态遥控器dtns-web任意浏览器交互面板、分身管理、任务下发入口三者之间Hub 与 Web 走 HTTPS / WSSHub 与 Node 走 WebSocket 长连接。这里有一个关键选择Node 端永远只主动“拨号”连向 HubHub 不会主动连接 Node。也就是说本地方案不需要在路由器上做端口映射也不需要拿到公网 IP。Node 只要能够访问互联网就能接入控制网络。这个设计极大降低了部署门槛。如果非要让云端主动连进内网设备会遇到 NAT 穿透、端口映射、防火墙规则等一系列麻烦。反过来设计以后所有内网设备就像一个个“只出不进”的客户端天然避开了入站安全风险。3.2 指令下发与结果回收的完整流程一次典型的远程控制任务完整链路是这样的用户在 dtns-web 提交一段自然语言任务会话层把任务送到 Hub 的意图解析模块。Hub 完成任务拆解产出一系列可执行的步骤。步骤里会标注需要的“能力标签”比如file_operation、browser_automation。Hub 根据能力标签挑选目标分身生成全局唯一的任务 ID把任务投递到对应分身的消息队列。目标分身所在的 Node 端通过已经建立的 WebSocket 长连接收到任务先回一个task_ack表示“我收到了准备执行”。Node 执行每一步实时回报执行过程中的中间状态。全部完成后Node 回报最终结果Hub 把结果写回对应的网页会话。第四步的task_ack很多人会忽略但它非常关键。如果没有这步确认一旦网络抖动导致任务消息重发执行端就无法区分“这是重复消息”还是“新任务”。我在设计协议时要求执行端必须按任务 ID 去重重复任务消息直接丢弃并重新提交确认。这样既保证“至少一次”的送达又避免“重复执行”的副作用。3.3 关键参数与容错策略远程控制的可靠性往往体现在几个关键参数上。我把 1.0 版本实际使用的参数列出来供参考参数默认值设计意图心跳间隔30 秒太快浪费流量太慢离线感知不实时离线判定阈值连续 3 次心跳无响应避免网络抖动导致的误判单步骤超时600 秒防止执行卡死占住队列不释放最大重试次数2 次仅对幂等任务重试非幂等任务直接失败会话互斥锁1 个分身同时 1 个控制会话防止多端并发操作冲突这里说一句心得参数不是越短越好也不是越长越好。心跳 30 秒是我在“离线识别的及时性”和“连接开销”之间反复试出来的值。曾经把阈值调到 5 秒结果宿舍网络一抖分身全被误判离线页面一片红。后来放宽到连续 3 次心跳无响应才判定离弦稳定性立刻上来了。3.4 安全边界怎么划远程控制产品最敏感的部分就是安全。dtnsbot 的防护策略分三层传输层所有 Web 与 Hub 的通信走 HTTPS / WSS证书用正规渠道签发浏览器不会报不安全。身份层登录用密码加令牌节点接入用独立节点令牌且在服务端可以随时吊销。如果某个设备被收回直接吊销令牌设备立刻失去控制能力。授权层每个分身能执行哪些操作由管理员预先配置白名单。比如 A 分身只允许文件类工具B 分身只允许浏览器类工具。智能体无法自行越权执行白名单之外的命令。审计日志是安全设计里不能省的一环。每次远程指令都会被完整记录谁、在哪台设备、什么时间、通过哪个分身、执行了什么操作、拿到了什么结果。前几天一个朋友问我为什么对“自己人用的系统”还要做这么重的审计我的回答是审计不是不信任而是出了事故以后唯一能还原现场的线索。没有日志的远程控制系统就像没有监控的机房出问题只能靠猜。4. 实操记录从单体改造到分身架构的完整过程4.1 第一步先设计“分身表”和状态机改造的第一步不是写代码而是先把数据模型想清楚。一个分身到底需要记录哪些信息我用的是一套 PostgreSQL 表结构核心字段大概是这样CREATE TABLE agents ( agent_id UUID PRIMARY KEY, agent_name VARCHAR(64) UNIQUE, owner_uid UUID NOT NULL, status VARCHAR(20) DEFAULT idle, capability_tags TEXT[] DEFAULT {}, endpoint_ref UUID, last_seen_at TIMESTAMP DEFAULT NOW(), created_at TIMESTAMP DEFAULT NOW() );status字段用的是状态机而不是随意字符串合法状态就五种idle空闲、running执行中、waiting等待用户确认、degraded部分能力缺失、offline离线。这个状态机是整个系统的“交通信号灯”所有交互逻辑都围着他转。之所以先花两天时间设计这张表是因为我吃过“表结构将就、后面疯狂返工”的亏。分身的在线状态、任务并发、能力路由全部依赖这张表里的字段。如果一开始没有预留capability_tags这样的能力标签字段后面想做“按能力自动选择分身”就无从下手。4.2 第二步把调度逻辑从执行逻辑中拆出来旧版的 dtnsbot 是一个进程里既做任务规划又做命令执行逻辑严重耦合。改造时我先定义了一套“调度协议”核心是三个消息类型task_new、task_ack、task_report。协议定清楚以后代码结构就清晰了。执行端的核心循环大约长这样async def run_node(): async with websockets.connect(HUB_WS_URL) as ws: await ws.send({type: register, token: NODE_TOKEN}) while True: msg await ws.recv() if msg[type] task_new: task_id msg[task][id] if seen(task_id): await ws.send({type: task_ack, task_id: task_id}) continue mark_seen(task_id) await ws.send({type: task_ack, task_id: task_id}) result await execute_steps(msg[task][steps]) await ws.send({type: task_report, task_id: task_id, result: result})这个循环很短但它把整个“接收任务、确认、执行、回报”的闭环表达清楚了。调度逻辑彻底跑到 Hub 那边去Node 端不关心任务是怎么拆的只负责执行。拆分之后的最大收益是升级“灵魂”时完全不用动每一台设备上的“肉身”只要 Hub 更新所有分身立刻获得新能力。4.3 第三步给执行端装上“遥控手柄”Node 端的实现要点是把“能力”抽象成可注册的工具集。每个工具是一个函数带有名称、描述、输入参数定义。Node 启动时自动扫描已安装的工具包把能力清单上报给 Hub。这样做的好处是你只需要在某一台设备上装了一个新工具Hub 在处理任务时就会自动把任务路由到这台设备。第一个我写进 Node 的工具是file_reader支持按路径读取文本文件并截断长内容。第二个是shell_executor能在授权目录下执行白名单命令。第三个是browser_action控制本机浏览器打开指定网页并截图回传。这三个工具已经覆盖了我日常 80% 的远程控制需求。写 Node 端时还有个小细节执行命令的进程要设置环境变量隔离避免远程指令影响 Node 宿主环境。比如shell_executor执行命令时我会把当前目录限制到 Node 配置的专用目录不允许跨目录访问。4.4 第四步网页端与网关打通网页端我用了 FastAPI 做后端网关前端页面用轻量框架打包成静态资源直接由 FastAPI 托管。核心路径就两条一条给浏览器走/api/auth/login登录拿令牌/ws/control建立 WebSocket 连接接收分身状态和任务结果。另一条给 Node 走/ws/node建立长连接完成注册和任务下发。为什么把两条 WebSocket 路径分开因为浏览器连接和节点连接的安全级别不一样。浏览器连接要经过登录鉴权加会话管理节点连接只需要校验节点令牌。分开以后各自的安全策略互不干扰排查问题也更清晰。页面与网关的联动我只做了一个关键优化任务状态不靠轮询而是通过 WebSocket 推送。轮询这种方式最大的问题是延迟高、请求多。用了推送之后任务状态更新基本是实时到页面的操作体感接近本地应用。4.5 部署配置要点硬性配置其实非常亲民。一台 1 核 2G 的轻量云服务器就能稳定跑 Hub 和网页网关顺带跑个小模型做本地推理。Node 端就更随意了只要有网络和 Python 3.9 以上环境就能跑。部署时唯一要注意的是必须把 WebSocket 的跨域和安全策略配好。浏览器端连接 WebSocket 时Origin 头会被发送到服务端必须在服务端做白名单校验只允许自己的域名接入。我在调试时有一整晚都在排查“为什么浏览器连不上”最后发现是反向代理层没有转发 WebSocket 的 Upgrade 请求头导致连接建立失败。这个问题很经典建议每个做 WebSocket 应用的人都提前知道。5. 上线后踩过的坑问题排查与优化实录5.1 分身明明在线却显示离线这个问题在 1.0 内测阶段被用户报过很多次。排查下来发现大部分是节点设备进入了系统休眠进程被冻结WebSocket 连接静默断开。服务端这边收不到心跳自然判定离线。解决方案分两端服务端放宽了离线判定阈值客户端则给 Node 的启动脚本加了系统服务托管并设置了防休眠策略。这里有个小技巧心跳不要只靠定时器发送还要监听系统的电源事件。笔记本准备睡眠前主动发一条going_offline消息服务端立刻把状态置为离线而不是等 90 秒超时。这样用户看到的状态永远是真实的。5.2 任务被重复执行有一个场景之前提过网络抖动时任务消息被重发到 Node如果没有去重机制同一个任务会在设备上执行两遍。这个问题最隐蔽因为“收到两条消息”这件事从服务端日志看不出来只有从执行端日志才能发现。排查时我在 Node 端给每个任务 ID 加了去重表发现同一 ID 到达第二次时直接不回处理只回确认。上线后统计这种重复率达到 0.4% 左右比例不高但一旦发生就可能造成真实损失。给所有智能体远程控制场景写代码的朋友提个醒消息去重和幂等处理必须在第一版就做不要等出了问题再补。5.3 网页端操作冲突两个窗口同时控制一个分身手机和电脑同时打开网页版很容易发生两个会话操作同一个分身的问题。一开始没有互斥锁两边同时派发任务Node 里的执行队列被交织的消息搞乱最严重的一次是同一目录下的临时文件被两边同时清空。解决思路就是前面提到的“分身级互斥锁”。控制台在发起控制时先抢锁锁的持有者会把分身状态标记为running。另一个会话只能进入“观察模式”可以看状态但派发按钮置灰。这个锁在切走页面、主动释放或连接断开时自动解除不会长期卡住。5.4 手机端控制的特殊注意我在手机浏览器上实际测过不少场景发现有两个体验痛点一是浏览器切到后台后WebSocket 会被系统挂起回到前台才恢复二是手机锁屏时间一长连接直接断开页面看起来还是“已连接”但实际已经收不到消息了。我的优化方案是给网页加了一层可见的连接状态指示器并且实现了断线自动重连。重连时自动恢复会话上下文把断线期间错过的状态消息补拉回来。另外把网页打包成了 PWA手机上“添加到主屏幕”之后体验接近原生应用不用每次都在浏览器地址栏里找入口。6. 上线后的一些个人体会与后续方向dtnsbot 网页版从立项到上线给我最大的体会是远程控制真正的难点从来不是通信技术而是信任边界的设计。通信方案有很多成熟的现成库难的是想清楚“哪些设备可以被谁控制、哪些命令可以被谁执行、每一步操作有没有留下痕迹”。这三个问题想清楚远程控制系统的骨架就立住了。后续我正在做的方向有两个。一是分身之间的任务交接比如当 A 分身所在的设备资源不足时把未完成任务整体迁移到 B 分身继续执行。二是多分身协作让两个不同设备上的分身共同时处理一个复杂任务云端负责同步中间状态。最后分享一个小建议如果你准备自己搭一套类似系统先把“分身状态机”和“审计日志”这两块基础设计做实。状态机决定系统行为是否可控审计日志决定出了问题之后能不能还原现场。这两件事做好了其它功能都只是往上添砖加瓦而已。