news 2026/9/11 12:50:26

用C++17和Unitree SDK2打造机器人Web调试工作台

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用C++17和Unitree SDK2打造机器人Web调试工作台

两台电脑、一台G1、网线、USB相机,还有一地调试线——这是我实验室里最常见的画面。每次要给G1调动作、跑一次SLAM或者验证相机效果,都得开一堆终端:RViz看地图、命令行发指令、rqt看日志,偶尔还得蹲在机器人旁边插键盘。忍了大半年之后,我决定把这些全部收进同一个Web开发工作台:用C++17 + Unitree SDK2做后端核心,接入D435i相机、SLAM建图、WebSocket实时通信、机器人控制和语音交互,浏览器打开一个页面就能完成大部分日常调试工作。这篇文章把当时的选型思路、实现细节和踩过的坑完整记录下来,给打算用SDK2做二次开发的朋友一个参考。

1. 这个工作台到底要解决什么问题:别急着写代码

动手之前我先把需求列了一遍,没有直接开干。很多项目做到一半失控,就是因为开头没想清楚“哪些功能必须做,哪些必须不做”。

1.1 传统调试方式的三个痛点

第一是工具分散。机器人状态要看官方客户端,SLAM地图要用可视化工具,图像流要另开一个窗口,语音调试还要再挂录音工具。五六个窗口来回切,信息很难对在一起。

第二是环境绑定严重。所有调试都依赖开发机上的桌面环境,想在会议室给客户演示,要么把整套开发环境搬过去,要么只能录视频。录视频又没法互动,客户想看看机器人实时反应,就只能等回到实验室再补。

第三是协作困难。团队里其他人想看实时状态,得远程登录我的电脑,既不安全也不方便。新来的同学连环境都折腾半天,更别说上手操作。

这三个痛点放到一起,答案就很自然:做一个通过浏览器访问的工作台。浏览器天然跨平台,手机、平板、笔记本都能开,局域网内随时访问。而且现在Web端渲染2D地图、视频流、控制面板都足够成熟,没必要为了一个调试界面去写桌面客户端。

1.2 功能边界:做一个“工作台”,不是做一个“控制器”

这里我要明确一个原则:目标不是替换官方SDK,也不是做一个用户级App,而是把日常开发中高频使用的功能集中在一个Web界面上。功能列表控制在五块:

  • 实时查看D435i的图像流和深度数据
  • 查看SLAM建图结果和机器人当前位置
  • 通过WebSocket下发机器人移动控制指令
  • 显示机器人状态(电池、关节角度、姿态)
  • 通过语音输入触发移动和建图

这个边界很重要。如果一开始就想在浏览器里编辑步态参数、做轨迹规划、调整PID,复杂度会迅速膨胀到无法收场。工作台的核心价值是“让调试过程更顺畅”,而不是替代所有专业工具。

2. 技术选型:为什么是 C++17 + Unitree SDK2 这套组合

这个项目最难的不是写代码,而是选型。我在“直接用ROS2包一层”和“用SDK2从零搭”之间犹豫了很久,下面说最终决定。

2.1 用Unitree SDK2而不是整一套ROS

ROS2在SLAM方面的生态确实成熟,功能包齐全。但一旦引入ROS2,整个系统的部署、依赖、网络配置复杂度会上升一个量级。G1本身支持ROS2接口,可团队里不是每个人都熟悉ROS2,而且我们的核心需求是做一个服务型Web工作台,中间夹一层ROS会把问题复杂化。

最终选了Unitree SDK2。它是宇树官方维护的C++ SDK,底层走DDS通信,延迟低,直接对接机器人状态和指令,适合做业务集成。跟ROS相比,SDK2更轻,编译出来是一个标准C++17库,不绑定ROS生态,后续想嵌入其他项目也方便。

2.2 C++17解决了什么老问题

项目里同时处理相机流、SLAM、网络、语音多个模块,数据共享非常频繁。C++17带来的几个特性正好命中需求:

  • std::optional表示可能不存在的值,比如IMU数据还没校准时的状态
  • std::shared_mutex做一读多写的状态保护,机器人状态多个线程读,只有一个线程写
  • std::variant用在指令消息解析上,比继承多态更轻量
  • if constexpr做编译期分支,尤其处理不同平台上的网络配置差异

这些特性不用额外引入第三方库,标准库自带,编译环境要求也不高。最关键的是代码可读性比老式C++好太多,团队协作时少了很多“这段代码到底在改什么”的疑惑。

2.3 SLAM方案:RTAB-Map为主体,带IMU融合

D435i提供RGB、深度和IMU三路数据,正好是视觉SLAM方案最舒服的输入组合。我在RTAB-Map、ORB-SLAM3、VINS-Fusion之间做了个简单对比:

方案输入地图类型回环检测集成难度适合场景
RTAB-MapRGB-D/IMU3D占用网格+2D栅格室内建图、导航
ORB-SLAM3单目/双目/RGB-D/IMU稀疏点云定位精度优先
VINS-Fusion双目/单目+IMU稀疏点云视觉惯性里程计

我最后选择RTAB-Map作为建图主力,因为它的2D栅格输出非常适合Web端渲染,后续做路径规划也方便。ORB-SLAM3留了一个接口,后续如果要切换大场景定位方案可以直接替换。

2.4 WebSocket是浏览器实时通信的最优解

前端要实时显示视频流和地图,可选方案有HTTP轮询、WebSocket、WebRTC。WebRTC延迟最低,但要处理信令、打洞、编解码,对局域网内纯调试来说太重了。HTTP轮询延迟高,还要反复建连,实时性根本跟不上。

WebSocket在这类场景是最稳的选择:一次连接搞定状态上报、指令下发和图像传输,双向通道,延迟低,浏览器原生支持,没有额外的SDK依赖。

3. 系统架构:三层数据流和一个稳妥的线程模型

整个系统分三层:机器人服务层、WebSocket传输层、浏览器展示层。我先说第一版架构的问题,再说最后修正的结果。

3.1 每个模块一个线程,用队列耦合

第一版图省事,把所有逻辑塞进一个主循环,相机回调里直接做WebSocket发送,SLAM结果直接在相机线程里更新,结果帧率互相拖累,代码改起来也痛苦。

后来重构为每个模块独立线程:

  • 相机采集线程:只负责从D435i读取帧,对齐后拷贝到共享内存,用条件变量通知下游
  • SLAM线程:订阅深度+RGB+IMU,输出位姿和地图数据
  • WebSocket线程:跑Boost.Beast异步事件循环,处理多个客户端连接
  • 状态管理线程:以50Hz频率从SDK2读取机器人状态,更新共享结构
  • 语音线程:麦克风采集、识别、意图解析

线程之间尽量不直接调用,全部通过带锁队列或共享状态结构传递。机器人状态这种高频小数据用shared_mutex保护,图像和地图这种大块数据用环形buffer加指针swap,避免频繁拷贝。

3.2 通信协议:二进制传图像,JSON传控制

WebSocket消息分两类:JSON文本消息负责控制指令和状态上报,二进制消息负责图像帧和点云数据。JSON里的字段不复杂,一个典型的机器人状态消息长这样:

{"type":"robot_state","ts":1720000000,"battery":78.5,"linear_x":0.12,"angular_z":0.0,"pose_x":1.32,"pose_y":2.18,"yaw":0.56}

控制指令由前端发,格式保持对称:

{"type":"cmd","cmd":"move","linear_x":0.3,"angular_z":0.0}

图像用二进制帧走WebSocket的binary opcode,不做Base64编码,先把省下的33%带宽留给真正有用的数据。

3.3 前端不用重型框架

很多朋友一听“Web工作台”就建议上React或Vue,我实际做下来,直接用原生HTML + JavaScript + Canvas就够用。页面就几个区域:视频画布、地图画布、摇杆控制区、状态面板、语音按钮。

用原生代码反而少一层打包工具链,拷贝到任何内网机器都能跑。后端挂一个静态文件目录,浏览器直接访问IP就能打开页面。对调试工具来说,简单可靠比技术时髦更重要。

4. 环境准备与 SDK2 首联调:从零开始跑通 G1

这部分是整个项目能不能往下走的门槛。SDK2的编译安装不难,但有几个细节没注意会卡一整天。

4.1 系统与依赖

我用的是Ubuntu 22.04,GCC版本默认9以上,CMake需要3.16以上。安装基础依赖:

sudo apt update sudo apt install cmake build-essential git libboost-all-dev libssl-dev

SDK2底层需要DDS通信库,按照官方仓库说明安装对应依赖。编译前建议先确认环境变量和网络配置,我第一次因为防火墙策略问题一直连不上机器人,后面把防火墙检查和网络隔离排查加进了流程才顺利跑通。

4.2 获取并编译SDK2

git clone https://github.com/unitreerobotics/unitree_sdk2.git cd unitree_sdk2 mkdir build && cd build cmake .. make -j$(nproc)

编译时间一般在几分钟内,过程中如果缺依赖,根据报错补齐就行。这里有一个建议:编译前看一眼README,不同型号机器人的接口命名有差异,G1和Go2在某些服务名上不完全一样。

4.3 最小可运行程序:读取G1的状态

写一个最小程序,连接机器人并订阅状态消息。这个程序的价值在于验证通信链路是否通畅:

#include <iostream> #include <unitree/robot/robot_client.hpp> int main(int argc, char** argv) { unitree::robot::RobotClient client; client.Init("192.168.123.18"); client.ServiceReq("state", [](const std::string& data) { std::cout << data << std::endl; }); std::this_thread::sleep_for(std::chrono::seconds(5)); return 0; }

具体函数名和参数以当前SDK版本头文件为准,但整体模式就是这样:初始化客户端、发起订阅请求、回调里拿数据。第一次跑通能看到机器人的IMU、电量、关节角度,通信链路就没有问题了。

4.4 网络这关最容易踩

G1和开发机需要在同一局域网,机器人IP要固定,开发机不能开代理类软件,DDS相关端口要保持可达。我遇到过几次SDK2时不时断连的情况,最终发现是开发机Wi-Fi漫游导致IP抖动,用网线连接后问题消失。

这个排查过程非常典型:一开始以为是代码问题,反复重编译,后来打印网络日志才看到连接总是在固定时间点中断,才意识到是无线网络在多个AP之间切换导致的。

5. SLAM 建图:让 G1 自己认识房间

SLAM是工作台里技术含量最高的模块,也是前端最有视觉冲击力的部分。我最终跑通的是RTAB-Map方案,下面详细展开。

5.1 传感器数据处理链路

D435i输出的RGB和深度帧,先做对齐,再送到RTAB-Map。IMU数据单独接入,提供给视觉惯性融合。对齐这一步特别关键,深度图和RGB图的像素坐标系如果不一致,建出来的地图边缘全是重影。

对齐后的数据每帧带时间戳。SLAM模块会判断当前帧是否构成关键帧,关键帧进入回环检测和图优化。回环检测出来以后,地图会出现一次明显的修正,机器人位置会“跳”回正确位置,这是SLAM里最常见的收敛现象。

5.2 坐标系:不用ROS也要维护TF

不用ROS不代表不需要坐标变换。G1的移动底盘有自己的坐标系,D435i安装在头部或身体前方,相机坐标系和底盘坐标系之间存在一个固定的外参。建图时,SLAM输出的位姿是相机坐标系在世界系下的位姿,而机器人在地图上的位置需要把相机外参转换过去。

我在代码里定义了一个简单的二维姿态结构:

struct Pose2D { double x; double y; double yaw; }; Pose2D robotPoseFromCameraPose(const Pose2D& camera_pose, const Pose2D& camera_tf) { Pose2D result; double cos_yaw = std::cos(camera_tf.yaw); double sin_yaw = std::sin(camera_tf.yaw); double dx = camera_pose.x - camera_tf.x; double dy = camera_pose.y - camera_tf.y; result.x = dx * cos_yaw + dy * sin_yaw; result.y = -dx * sin_yaw + dy * cos_yaw; result.yaw = camera_pose.yaw - camera_tf.yaw; return result; }

虽然是二维简化版,但室内平整地面场景足够用。如果要做上下坡或者复杂地形,就要上完整的三维位姿和旋转矩阵了。

5.3 地图怎么到浏览器

RTAB-Map实时输出两种地图:3D占用网格和2D栅格。Web端实时展示3D点云会造成很大的渲染负载,所以我先在服务端对点云做体素降采样,然后选择性发送少部分关键帧;2D栅格地图直接转成PNG图,叠加机器人当前位置坐标和运动轨迹,用Canvas绘制。

地图数据更新频率不用太高,1到2Hz对调试来说完全够,时间应该花在建图质量上,而不是花在传输和渲染上。通过限制发布频率,前端动画流畅度明显提升,CPU占用也降下来了。

5.4 SLAM如何开始和停止

工作台里不能建图一直跑,要有控制逻辑。语音说“开始建图”,服务端触发SLAM线程启动;语音说“停止建图”,SLAM模块保存地图并退出。这两个操作都通过WebSocket指令触发,SLAM模块被设计成可重复创建销毁的服务类。

这个设计里要特别注意资源清理:SLAM线程退出时,如果还在处理关键帧,必须等当前帧处理完再销毁,否则会丢失回环信息。我的做法是给SLAM线程一个退出标志,处理完当前关键帧后才真正返回。

6. D435i 相机接入:RGB、深度、IMU 的正确处理姿势

D435i是感知系统的眼睛,也是坑最多的地方。把它跑通不难,跑好很难。这里我把每一步的关键点拆开说。

6.1 安装librealsense并启动三路数据流

sudo apt install librealsense2-dev librealsense2-utils

在C++代码里配置pipeline:

rs2::pipeline pipe; rs2::config cfg; cfg.enable_stream(RS2_STREAM_COLOR, 640, 480, RS2_FORMAT_RGB8, 30); cfg.enable_stream(RS2_STREAM_DEPTH, 640, 480, RS2_FORMAT_Z16, 30); cfg.enable_stream(RS2_STREAM_GYRO, RS2_FORMAT_MOTION_XYZ32F); cfg.enable_stream(RS2_STREAM_ACCEL, RS2_FORMAT_MOTION_XYZ32F); pipe.start(cfg);

分辨率我选640x480而不是1280x720,是有意为之:720p的深度图数据量翻倍,JPEG编码和传输延迟都会上去,而SLAM对分辨率要求其实没那么高,640x480足够。

6.2 深度对齐是基本盘

深度图的坐标系默认和深度传感器对齐,而可视化通常需要和RGB图对齐后的深度图:

rs2::align align_to_color(RS2_STREAM_COLOR); auto frames = pipe.wait_for_frames(); auto aligned = align_to_color.process(frames);

这一步不做,后面点云投影的位置会和彩色图偏一截,尤其近处物体特别明显。我见过不少项目把这个问题误判成标定误差,折腾几天,其实只要调用一下align就行。

6.3 从深度图生成点云

深度图本质是二维数组,每个像素存的是该位置到相机的距离,单位是毫米。要变成空间中的点,需要利用相机内参还原三维坐标:

  • 已知像素坐标(u, v)、深度值d、内参fx、fy、cx、cy
  • x = (u - cx) * d / fx
  • y = (v - cy) * d / fy
  • z = d

实际代码里可以批量计算,也可以用librealsense提供的点云模块。我选择自己算,前提是要先验证内参,否则点云会混乱。

生成点云后再加一个体素滤波,每5厘米取一个点,就能把几万个点降采样到几千个点,前端用WebGL渲染时压力会小很多。

6.4 IMU数据的时间同步

IMU和图像的时间戳在D435i内部是同一个时钟源,但订阅回调顺序不一定一致。我在SLAM模块里维护一个滑动窗口,把IMU按时间戳插值到图像时间上,视觉惯性融合这样才稳定。不处理时间同步的话,建图后期会出现明显的漂移累积。

7. WebSocket 桥与控制台:浏览器里看到机器人实时状态

C++服务端和浏览器之间的通信是工作台的“血管”。我用Boost.Beast实现WebSocket服务端,300行左右就能跑起来一个支持广播的Service。

7.1 服务端框架:单线程事件循环够用

相机帧30帧每秒,机器人状态50赫兹,地图2赫兹,这些数据对单个WebSocket服务来说压力不大。Boost.Beast的异步接口配合多客户端列表,每个客户端一条WebSocket连接,服务端向所有客户端广播消息。

第一版我用了多线程WebSocket服务,反而引入并发问题。局域网内客户端数量通常不超过5个,单线程异步事件循环完全够用。这种“能简则简”的思路在集成项目里能省大量调试时间。

7.2 图像传输:用binary帧而不是JSON

把JPEG图像直接通过二进制帧发出去,每帧大概30到80KB。如果把它Base64编码塞进JSON,体积膨胀33%,浏览器还要多一步解码。实际测试下来,局域网内二进制帧到达延迟在20毫秒以内,画面流畅。

前端接收图像:

ws.binaryType = 'arraybuffer'; ws.addEventListener('message', (event) => { if (typeof event.data === 'string') { handleJson(JSON.parse(event.data)); } else { const blob = new Blob([event.data], { type: 'image/jpeg' }); const url = URL.createObjectURL(blob); videoImg.src = url; } });

视频画布用Image元素直接展示JPEG,不需要额外解码库,浏览器原生处理。

7.3 状态和控制回路

状态数据走JSON,每200毫秒发一次。前端拿到后更新仪表盘:

function handleState(state) { document.getElementById('battery').innerText = state.battery + '%'; document.getElementById('pose').innerText = `x=${state.pose_x.toFixed(2)} y=${state.pose_y.toFixed(2)} yaw=${state.yaw.toFixed(2)}`; }

用户操作摇杆或点击按钮时,前端把指令变量放到一个定时发送器里,每100毫秒发送一次。这样比每次拖动都发送更平滑,也不容易把WebSocket带宽打满。

7.4 断线重连的必要性

机器人移动过程中,Wi-Fi信号偶尔会抖动,WebSocket连接会断。前端必须自动重连,否则调试现场还得手动刷新页面。我实现了指数退避的重连机制:第一次断开等1秒,再断等2秒,最多等10秒。重连成功后,客户端主动向服务端请求当前状态和地图全量数据,保证画面不是空白。

这个细节在演示场景里特别重要。客户正在看地图,连接断了,自动重连后地图重新显示出来,体验会好很多。

8. 把语音交互和控制指令收进同一个状态机

光有页面控制还不够,语音交互是这个工作台的加分项。刚开始觉得语音很难,拆解下来其实就是听清、听懂、执行三步。

8.1 语音管线怎么选

我用的是本地离线ASR方案,sherpa-onnx加载一个小体积识别模型,把麦克风采集的音频流实时转成文本。选离线方案不是因为云识别不好,而是因为本地处理少一跳延迟,不依赖外网,也不会因为网络波动导致识别中断。

语音识别输出类似这样一段文本:

现在开始建图

这个结果不需要百分百准确,只要关键词语能被匹配出来就行。

8.2 意图映射:不一定要上大模型

多数机器人指令用关键词匹配就够。我建了一张意图映射表:

语音文本关键词意图动作
前进move_forward下发移动指令 linear_x=0.3
后退move_backward下发移动指令 linear_x=-0.3
左转turn_left下发旋转指令 angular_z=0.5
右转turn_right下发旋转指令 angular_z=-0.5
停止stop清零速度和转角
开始建图start_mapping启动SLAM线程
停止建图stop_mapping保存地图
打开相机camera_on开启图像流广播

匹配逻辑用子串匹配加优先级,比如“停止建图”里的“停止”不能抢走“建图”的意图,所以“建图”的匹配优先级要更高。

8.3 状态机设计:防止语音和手动指令打架

语音指令和前端按钮指令可能同时到达,这就要求控制模块有一个状态机。我用一个粗粒度的状态机:

  • IDLE:空闲,可以接收任何指令
  • MOVING:速度指令下发中,语音只响应“停止”
  • MAPPING:SLAM建图线程运行中,语音“停止建图”才会退出
  • SPEAKING:TTS正在回复,期间新语音排队等回复完成再处理
  • LISTENING:正在录音识别,此时不响应其他指令

核心原则是同一时刻只允许一个控制源实质性改变机器人状态,另一个只能排队或打断。实现时用一个互斥量保护当前状态,所有指令入口先检查状态再执行。

8.4 语音答复用TTS回传

识别结果和执行结果都通过WebSocket发给前端,同时服务端用开源的TTS库生成一段回复音频,回传给前端播放。比如识别到“前进”,服务端执行移动后返回“好的,前进”。

这里有个细节:在执行移动时播报语音会引入额外延迟。我就在状态机里专门设了一个延迟,先播报再执行。实测同时执行会让语音听起来很赶,先播报再移动更自然,也给了人一个反应时间确认指令是否正确。

9. 遇到了哪些坑,以及对应的解决办法

最后这部分是全文最有价值的地方。这些坑每一个都让我浪费过几个小时到一整天,希望大家看完能绕开。

9.1 D435i相机在机器人上频繁掉线

表现:运行十几分钟后图像流中断,系统里看不到USB设备。排查发现是USB供电不足。机器人本体通过USB转接板给相机供电,负载高时电压不稳。解决办法:给相机单独用一个带外接电源的USB Hub,问题消失。

9.2 SDK2偶发断连,日志里没有任何报错

表现:程序运行几十分钟后控制指令偶尔无响应,机器人状态停止更新。排查过程很崩溃,最后发现是开发机的无线网卡在多个AP之间漫游切换,控制指令恰好赶上切换窗口。用网线直连后彻底稳定。

9.3 WebSocket发送频率过高导致浏览器假死

最初把SLAM轨迹原始点全部实时发送,每秒几次,每次几百个点,浏览器渲染线程直接卡死。优化方案:地图发布频率降到2赫兹,点云做抽稀,前端Canvas用requestAnimationFrame合并渲染,问题解决。

9.4 深度图“看起来对了”但点云是反的

这种现象往往是相机内参配置错误或者对齐没生效。我加了一个小调试窗口,把相机内参和深度图中心点的深度值打印出来核对。比如深度图中心点的深度值应该大致等于相机到前方物体的距离,如果偏差太大,多半是配置的内参和实际流不一致。

9.5 多线程共享状态导致偶发段错误

相机线程写图像数据,SLAM线程读图像数据,不加保护时几十秒到几分钟必然崩一次。后来把图像数据改成用shared_ptr共享,写入时生成新帧,读取时只读指针,从设计上避免大部分竞争。

9.6 语音识别率在机器人走动后急剧下降

一开始以为是麦克风问题,后来发现机器人移动时底盘电机和风机噪声很大,ASR模型被噪声干扰。解决办法:在语音信号链路里加夜间噪声抑制滤波器,把麦克风尽量安装在离电机远一点的位置。识别率从六成提升到九成左右。

这套工作台做到现在,我最大的感受是:把很多工具收拢到一个页面里,带来的效率提升远超预期。以前调SLAM参数要去翻RViz、看日志、手动发指令,现在浏览器里一套流程下来,围观的小伙伴也能帮忙点几下按钮。最后分享一个小技巧:先跑通最薄的一条链路,比如让D435i的图像流到浏览器里显示,再逐步加入SLAM和语音。一个能看到的中间结果,远比一整套纸面设计更能激发后面的灵感。

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

PLC恒压供水系统设计与节能优化实践

1. 恒压供水系统概述 恒压供水系统是现代建筑供水工程中的核心设备&#xff0c;它通过自动调节水泵运行状态&#xff0c;确保管网压力稳定在设定值。我参与过多个大型商业综合体的供水系统改造项目&#xff0c;发现传统供水方式普遍存在压力波动大、能耗高、设备寿命短等问题。…

作者头像 李华
网站建设 2026/9/11 12:45:24

AI文献综述工具:提升学术写作效率的7大解决方案

1. 学术写作的AI革命&#xff1a;为什么需要文献综述工具&#xff1f; 读研时最痛苦的记忆莫过于写文献综述。记得有次为导师的课题连续熬了三个通宵&#xff0c;在PubMed和CNKI上手动筛选了200多篇论文&#xff0c;最后整理出的表格还是被批"缺乏系统性"。现在回看&…

作者头像 李华
网站建设 2026/9/11 12:44:05

YOLO目标检测实战:从原理到工业落地的全流程拆解

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

作者头像 李华
网站建设 2026/9/11 12:42:22

SpringBoot游泳馆运营管理系统设计与实现

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

作者头像 李华
网站建设 2026/9/11 12:42:17

Agent工程化实战:从核心架构到生产落地的全链路指南

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

作者头像 李华