让 DSH 用自然语言直接控制 Universal Robots(UR)机械臂的插件,由拓德科技(Nonead)基于自研的 nUR MCP Server 同源逻辑开发。(Suzhou Nonead Robot Technology Co., Ltd.)
安装
# GitHub 源码(首次需按提示配置 allowBuilds 构建授权后重试)
dsh plugin --profile web add github:NoneadChina/dsh-nonead-universal-robots
装任何插件都等于在你的机器上跑第三方代码,权限和你本人一样大——能读你的文件、用你的凭据、访问网络,工具审批管不到它。GitHub 来源的插件还会在安装时执行构建脚本——pnpm 默认拦截,所以安装可能停在 ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED 或 ERR_PNPM_IGNORED_BUILDS;dsh 会打印出需要添加的确切键名,把它加进该 profile 的 pnpm-workspace.yaml 的 allowBuilds 下,重跑一次即可装上。放行构建本身就是一次信任判断:请只安装可信来源,并尽量锁定 commit(github:owner/repo#sha)。
README
一个让 DSH 用自然语言直接控制 Universal Robots(UR)机械臂的插件,由拓德科技(Nonead)基于自研的 nUR MCP Server 同源逻辑开发。(Suzhou Nonead Robot Technology Co., Ltd.)
插件启动一个常驻的 Python worker(内部复用 vendored 的 URBasic 库),把机器人控制以原生 DSH 工具的形式接入:工具经由该 worker 与机器人通信,因此 connect 一次后即可在同一 IP 上持续发指令。仓库:https://github.com/NoneadChina/dsh-nonead-universal-robots
⚠️ 安全提示:本插件会直接驱动真实机械臂。使用时务必保证机器人可见、急停按钮触手可及、工作区无障碍物与人员。请把它当作授予模型
bash工具一样慎重对待。你(或模型)对任何运动指令负全责。
🛡️ 运动审批门禁:53 个会驱动机器人运动、执行程序、让机械臂脱离程序控制或失去刚性支撑、改写/删除机器人上已存程序、通过 Modbus 驱动现场总线设备、改变 TCP / 工具电气配置或切换数字量/模拟量输出,以及在力控下顶住工件的指令,在真正下发前会暂停等待人工确认:
分组 工具 运动 ur_movejur_movelur_movepur_movecur_servojur_move_optimizedur_move_xur_move_yur_move_zur_move_tool_xur_move_tool_yur_move_tool_zur_move_force绘图 ur_draw_circleur_draw_squareur_draw_rectangleur_draw_star程序与脚本 ur_load_programur_run_programur_send_scriptur_reset_error控制模式 / 电源 / 安全 ur_set_freedriveur_set_teach_modeur_power_onur_power_offur_brake_releaseur_unlock_protective_stopur_shutdown力控与速度 ur_force_modeur_force_mode_settingsur_speedjur_speedl工具与 I/O 配置 ur_set_tcpur_set_tool_voltageur_set_tool_output_modeur_set_tool_communicationur_set_digital_outur_set_analog_outur_set_conveyor_tick程序存储 ur_upload_programur_delete_program物理状态变更 ur_set_payloadur_set_payload_inertiaur_set_gravityur_zero_ftsensorur_motion_versionur_set_speed_scalingur_set_conveyor_trackingModbus 写类 ur_modbus_add_signalur_modbus_delete_signalur_modbus_send_custom_commandur_modbus_set_output_registerur_modbus_set_output_signal减速与退出类(
ur_stopj、ur_stopl、ur_end_force_mode)刻意不进门禁:它们是操作员(或取消后的模型)让机械臂停下来的手段,给它们加确认只会拖慢"停下来"。门禁名单只有一处来源(lib/index.js的APPROVAL_OPS),由scripts/check-approval-gate.py与test/approval-gate.test.mjs断言。交互式部署(审批策略为
ask)会在界面上弹出确认框。未组合审批服务、无代理、或应答不是allowed-once的调用都会以拒绝方式安全关闭(fail-closed)—— 指令被拒绝,绝不会到达机器人。默认值写在代码里(config.requireApprovalForMotion ?? true),因此"调用方跳过 schema 解析"也不会把门禁关掉;可用配置requireApprovalForMotion: false显式关闭。test/approval-gate.test.mjs驱动真实工具注册表逐条验证上述行为,包括"多传一个op参数无法把调用改派到别的指令"。
🧭 预飞安全闸:在审批门禁之外,再加一层"单条指令最多能要求多少"的物理上限。上限项:
safetyMaxStepM(0.5 m —— 轴位移与绘图尺寸)、safetyMaxLinearSpeed(0.5 m/s)、safetyMaxJointSpeed(1.05 rad/s)、safetyMaxAccel(2.0 m/s²)、safetyMaxDurationS(60 s)、力控的safetyMaxForceN/safetyMaxTorqueNm(40 N / 10 N·m),以及可选的safetyWorkspace包围盒("x:-1,1 y:-1,1 z:0,1.2",针对显式笛卡尔目标 —— 包括四个ur_draw_*的center/origin;它们的r/border/width/height/side受safetyMaxStepM限幅)。它在审批之前判定 —— 一个物理上不可能的请求不该拿去打扰人 —— 并且与审批解耦:requireApprovalForMotion: false只关掉人工确认,绝不会顺手关掉这些物理上限(那需要显式设safetyGate: false)。safetyWorkspace写错时 fail closed(拒绝一切运动指令),而不是被静默忽略。它是"防手滑 / 防单位写错"的一层,不是安全边界(安全边界仍是控制器自己的安全配置);也刻意不覆盖ur_send_script—— 那是任意 URScript 文本,本闸不解析脚本。📝 动作审计账本:每条受门禁的指令都会追加一行 NDJSON(默认
<DSH_HOME>/nonead-ur/audit.ndjson;auditLogPath换位置、auditLog: false关掉、auditLogAll: true连不受门禁的读取也记),字段为{ts, ip, op, tool_name, args, decision, reason},decision ∈ {approved, denied, blocked, unchecked}—— 事后能回答"模型到底对机器人做了什么"。写日志绝不影响下发路径:失败只回报,不抛异常。
功能一览
共 104 个工具(ur_*)。
| 类别 | 工具(ur_*) |
说明 |
|---|---|---|
| 连接 | ur_connect / ur_disconnect |
按 IP 连接 / 断开一台 UR 机器人 |
| 状态 | ur_get_status |
一次性读取 TCP、关节、型号、序列号、版本、安全模式、运行/程序状态、电压、电流、温度、开机时长、关节电流/电压/角速度、TCP 速度与受力、速度倍率,以及 rtde_resync_bytes(RTDE 接收端为重新同步而丢弃的字节数;0 表示链路干净) |
| 位姿 | ur_get_tcp_pose / ur_get_joint_pose |
读取当前 TCP 位置 / 关节角度 |
| 目标值 | ur_get_target_values / ur_check_pose |
读取「控制器打算去哪」:目标关节角/角速度/角加速度、目标 TCP 位姿与速度(外加实际值便于对比)。来自 RTDE 的 target_* 字段,读取零代价 —— 这是判断「指令已下发但还没执行 / 正在交融 / 被安全限速拉住」的直接依据。ur_check_pose 回答的是动之前值得先问的问题:这个 TCP 位姿在控制器的安全限值内吗?逆运动学解得出吗(即可达吗)?它是只读预检:由控制器执行 is_within_safety_limits + get_inverse_kin,结果经输出寄存器在首尾哨兵之间回读,所以"控制器根本没执行"会被如实报出来,而不是靠猜 |
| 设备信息 | ur_get_robot_model / ur_get_serial_number / ur_get_uptime / ur_get_software_version / ur_get_safety_mode / ur_get_safety_status / ur_get_robot_mode |
型号(含 remote_control 字段)/ 序列号 / 开机时长 / 软件版本 / 安全模式 / 安全与机器人状态位(哪一类安全功能被触发,含 violation / fault;限值数值本身不经 RTDE/dashboard 暴露,需去 PolyScope 安全页读)/ 运行状态 |
| 程序 | ur_get_program_state / ur_load_program / ur_run_program / ur_stop_program / ur_pause_program / ur_list_programs / ur_upload_program / ur_download_program / ur_delete_program |
程序状态、加载、运行、停止、暂停、SSH 列表,以及程序文件的 SFTP 传输。三个传输工具都核对"到底做成了没有",而不是"请求已发出":上传回读远端大小、下载回读本地大小、删除后确认文件真的不在了;下载绝不静默覆盖(目标已存在时先改名成 <path>.bak,备份失败就拒绝下载)。它们的本地路径被锁在 programDir 内 —— 见下文「本地文件访问」。ur_list_programs 扫描真机 /programs 或 URSim 的 ~/URSim_Linux-*/programs.* 布局(可用 programs_dir 覆盖);不传口令时会回报 used_default_password,并给出当前生效的 host_key_policy,可选 known_hosts 文件会让 SSH 切到 RejectPolicy 严格校验主机指纹(文件不存在直接报错,绝不静默退回)。运行/停止/暂停都会检查控制器应答并回带运行状态 |
| 现场消息 | ur_popup / ur_close_popup / ur_add_log |
跟站在机器人旁边的人说话:在示教器上弹一个模态提示、关掉它、往控制器自己的日志历史里写一行。三者都不会让机械臂运动,因此都不在审批门禁后面;文本有注入校验(dashboard 按行解析命令,换行会注入另一条命令),长度上限 300 字符。注意 ur_add_log 会在控制器上留下永久痕迹 —— 这正是它的用途,所以不要往里写密钥 |
| Modbus | ur_modbus_add_signal / ur_modbus_delete_signal / ur_modbus_get_signal_status / ur_modbus_send_custom_command / ur_modbus_set_output_register / ur_modbus_set_output_signal / ur_modbus_set_runstate_dependent_choice / ur_modbus_set_signal_update_frequency |
现场总线 I/O:增删控制器监管的信号、读回一个信号、置输出寄存器与数字输出、发一条原始 Modbus 命令,以及调信号的程序状态行为与刷新频率。⚠️ vendored urScript.py 里那八个 modbus_* 方法全是桩(raise NotImplementedError),所以这些脚本是本插件自己拼的。⚠️ Modbus 写类没有应答 —— 这些工具能证明的只是"这段 URScript 在控制器上跑完了"(首尾哨兵),永远不能证明 PLC 执行了,因此每条回执都带 fieldbus_confirmed: false;只有 ur_modbus_get_signal_status 能读回一个值(经输出寄存器,同样在哨兵之间回读)。信号名走白名单校验 —— 它会被拼进 URScript 字符串字面量,引号或换行等于注入脚本;读的时候必须显式声明 kind,因为 URScript 是强类型的(数字信号返回布尔、寄存器信号返回整数,一段脚本没法安全地回写两种)。五个写类工具在审批门禁后面;只读那一个与两个信号配置项不在 |
| 命名位姿 | ur_save_pose / ur_get_pose / ur_list_poses / ur_delete_pose |
本机的命名位姿表 —— 一位姿一文件 <poseDir>/<名字>.json,默认 <DSH_HOME>/nonead-ur/poses:保存当前姿态(或直接给数值)、读回一条、列出全部、删除一条。⚠️ 名字会变成文件名,所以走白名单(允许 Unicode 字母/数字,中文可用;拒绝路径分隔符、Windows 非法字符、保留设备名与首尾空白),且围栏判据是 realpath 之后的结果 —— 与程序文件同一套"宿主设围栏 / worker 落盘"分工。写盘是原子的(tmp + os.replace),写完还会回读比对;报 VERIFY_FAILED 就是别信那个文件。读的时候把**「从没存过」(NOT_FOUND)与「存过但损坏」(CORRUPT)分开** —— 混成一句会让调用方去重新保存一个其实已经存在的东西。单个条目损坏不会让整张表失败:按条目回报,文件名列在 broken 里。⚠️ 刻意不提供"按名字直接运动":那样位姿数值在调用前对安全闸与审批门禁都是不可见的;安全流程是两步 —— 先 ur_get_pose(name)、再 ur_movel(pose, …),让两道门禁都看到真实数字 |
| 寄存器 | ur_get_int_register / ur_get_double_register / ur_get_bit_register |
读取 Int / Double / Bool 寄存器 |
| 健康检查 | ur_ping |
无需连接机器人,检测 worker / Python / URBasic 就绪 |
| I/O | ur_get_digital_in / ur_set_digital_out / ur_get_digital_in_bits / ur_get_digital_out_bits / ur_get_analog_in / ur_set_analog_out / ur_get_tool_analog_in |
数字输入/输出(含批量读位、which="tool" 的工具端 I/O)与标准模拟输入/输出(按工程单位收值:URScript 的 set_analog_out 收的是相对电平 [0,1];电流域端口可用 full_scale: 20)。读工具端数字输入是哨兵校验的:控制器若始终没回报脚本执行完毕,工具会拒绝给值,而不是报一个无法担保的数字 |
| 工具配置 | ur_set_tool_voltage / ur_set_tcp / ur_set_payload / ur_set_payload_inertia / ur_set_gravity / ur_zero_ftsensor / ur_set_tool_output_mode / ur_set_tool_communication |
工具电压(0/12/24 V)、TCP、负载质量/重心、质量+重心+惯性矩阵一次设全(set_target_payload,PolyScope 5.10+;与 set_payload 不同,它不会重置惯性矩阵而让三者互相矛盾)、重力方向(非水平安装用)、力/力矩传感器归零、工具输出模式(普通 / power 双针供电)、工具串口(TCI/RS-485 —— ⚠️ 启用会禁用工具模拟输入)。⚠️ 这类设置没有回读通道:成功的回执写明「已下发(未校验)」并带 verified: false —— 插件能证明的只是脚本已送达控制器,而 UR 只在远程控制模式下接受 URScript,其余情况会被静默丢弃 |
| 控制模式 / 电源 / 安全 | ur_set_freedrive / ur_set_teach_mode / ur_power_on / ur_power_off / ur_brake_release / ur_unlock_protective_stop / ur_shutdown |
自由驱动 / 示教模式(可手动拖动机械臂)、上电 / 下电 / 释放刹车(⚠️ 可能因重力掉落)、只解保护性停止(不上电、不释放刹车,这就是它与 ur_reset_error 的区别)、关闭控制器 |
| 力控 | ur_force_mode / ur_end_force_mode / ur_force_mode_settings / ur_move_force |
力控(Force Mode):沿/绕选定轴柔性贴合环境并持续施加指定力/力矩。参数与手册逐条对齐:task_frame / selection_vector(1=柔性)/ wrench / type(1–3)/ limits(柔性轴=最大 TCP 速度,刚性轴=最大位置偏差)/ damping / gain_scaling。脚本按手册建议先 sleep(0.02) 再进力控;退出用 ur_end_force_mode。⚠️ damping/gain_scaling 控制器侧无法回读,工具只回报本次设置值,不假装读到当前值。ur_move_force 是架在它上面的装配动作:一段 URScript 把 force_mode(...) → movel(...) → stopl(...) → end_force_mode() 一次跑完,于是机械臂朝目标推进、到工件处停住。⚠️ 它不是 vendored 的 move_force_2stop —— 那个靠 RTDE 输入寄存器每 8 ms 推 wrench,而本插件刻意没有 send 配方,那些字段在这里 claim 不了(claim 会让控制器抛异常并崩掉 RTDE 线程)。本工具的做法是:整段运动交给控制器跑,用首尾哨兵证明它真的跑了,再用只读 RTDE 读运动前后的实际位姿 —— 于是"停在目标之前"表示控制器无法再沿轨迹前进(正是力控装配要的效果),但不证明力真的压到了工件上;回报里同时给出 stopped_due_to_safety,把「被力限止住」与「被安全停止」的区分留给你 |
| 速度控制 | ur_speedj / ur_speedl / ur_stopj / ur_stopl / ur_wait_steady |
关节/TCP 速度指令(speedj / speedl)与对应的减速停止(stopj / stopl)。⚠️ 速度指令是开放式的:t=0(默认)时"达速即返回"而机械臂仍在运动,必须用 stop* 或 ur_wait_steady 收尾。ur_wait_steady 轮询 RTDE 已有的速度量判断是否真的停住(没有用 URScript 的 is_steady() —— 那个在力控/示教模式下恒为 false) |
| 实时遥测 | ur_get_runtime_telemetry / ur_get_robot_voltage / ur_get_robot_current / ur_get_joint_temperatures / ur_get_speed_scaling / ur_get_tcp_force / ur_get_tool_telemetry |
关节电流/电压/角速度、TCP 线速度与受力/力矩、工具加速度计、速度倍率、整机电压电流、关节温度、工具电流/电压、I/O 电流。这些字段本来就在 500 Hz 的 RTDE 数据流里,读取零代价、无需改配置。单点读取(整机电压/电流、关节温度)与一次性汇总(ur_get_runtime_telemetry)并存:轮询某一项时用单点,诊断时用汇总 |
| 传送带 | ur_get_conveyor / ur_set_conveyor_tick / ur_set_conveyor_tracking |
传送带 tick 读取 / 设置,以及线性或圆盘跟踪的开启与停止。ur_get_conveyor 对它依赖的那段脚本自证:只有观察到控制器把脚本执行完毕才给出 tick。⚠️ ur_set_conveyor_tracking 未经真机验证 —— 签名与量纲逐条对照官方手册,但跟踪行为需要真带编码器的传送带才能确认,请先在 URSim 上试 |
| 运动 | ur_movej / ur_movel / ur_movep / ur_movec / ur_servoj / ur_move_optimized / ur_move_x / ur_move_y / ur_move_z / ur_move_tool_x / ur_move_tool_y / ur_move_tool_z |
关节空间 / 直线 / 路径 / 圆弧(mode 0 插补姿态、1 固定姿态)/ 连续流 / OptiMove 平滑运动(optimovej/optimovel,jerk 受限、振动更小;⚠️ 它的 a/v 是能力比例 (0,1] 而非 rad/s 或 m/s)/ 沿轴直线运动:_move_* 沿基座轴、_move_tool_* 沿当前工具轴(worker 侧用旋转矩阵换算,不依赖 URScript 的 pose_trans)。默认值取手册值(movej 1.4 / 1.05,movel 1.2 / 0.25);给了 t > 0 时时长优先于 a/v |
| 运动规划 | ur_motion_version / ur_set_speed_scaling / ur_get_freedrive_status |
设置 Motion Version(手册第 14 章)与 jerk 增益(0.01–1.0,只作用于 jerk 受限的剖面:版本 2 的 movej/movel 与 optimovej/optimovel)。版本 2 规划时把速度/加速度钳到硬件上限、交融半径重叠时动态收缩,而不是像版本 1 那样跳过整段运动并给 "Overlapping Blends" 警告。⚠️ 新机型与 PolyScope X 只支持版本 2;CB3 没有这个设置。⚠️ 这两个设置没有回读通道。ur_set_speed_scaling 设置速度滑块(set_speed_slider:0 = 停住 / 1 = 满速)—— 交出精细动作前把整体速度压下来,最直接可靠。与其它设置类 op 不同,它有回读通道(target_speed_fraction):只有回读一致才报 verified: true;又因为该映射从未在真机上确认过,会同时回报 hardware_verified: false。ur_get_freedrive_status 读取当前姿态在 freedrive 下离奇异点的距离(0 正常 / 1 接近 / 2 太接近 —— 不是 freedrive 的开关状态),据此提示操作员换路径 |
| 绘图 | ur_draw_circle / ur_draw_square / ur_draw_rectangle / ur_draw_star |
画圆 / 正方形 / 长方形 / 五角星。只有观察到脚本真的在运行之后才回报「执行完成」 |
| 脚本 / 紧急 | ur_send_script / ur_reset_error |
发送 URScript(校验执行:哨兵注入到函数体内部/顶层语句之间并回读,「已发送」不再冒充「已执行」;含多个函数且看不出调用哪个时拒绝校验并给 verified: null)/ 复位错误 |
除「连接」外,所有工具都接受 ip 参数,且都要求先连接该 IP。
只读 3D 数字孪生
插件还内置机器人的只读 3D 数字孪生(three.js 渲染):入口是右侧栏「开始」面板里的一张卡片,位置在「工作区文件 / 新建终端 / 浏览器」三张卡片下面;点击后 3D 视图占满右侧栏的内容区。视图订阅宿主的 SSE 实时流(/twin/stream,100 ms 一帧),始终与真机同步显示关节姿态、TCP 坐标系与近期运动轨迹。数字孪生严格只读,绝不下发任何机器人指令 —— 它只订阅 host 的只读路由并渲染收到的数据。工具栏提供「重置视角」与等轴测/前视/侧视/俯视四个预设,且相机与基座网格按模型包围盒取景 —— UR3 与 UR20 都能正确入画。数值面板还会显示 dashboard 侧状态(安全模式、机器人模式、程序状态、速度倍率、关节温度、母线电压电流),同样走这条路由、按较慢的节拍取。
运动期间也不会停更:遥测由 worker 主动推送(20 Hz),孪生的每一帧优先读这份推送缓存、不经过那条单线程命令队列 —— 机器人运动时,运动确认循环占住的是命令队列,而 RTDE 缓存由 URBasic 自己的接收线程持续更新,推送因此照常到达(协议上是一条不带 id 的 JSON 行,旧宿主只会把它当成匹配不上的响应,向后兼容)。缓存过期(>300 ms)或缺失时才回退到显式读;孪生的读仍只带 2.5 s 的专用预算且超时不杀 worker:一条只读可视化读绝不能把整条机器人会话(RTDE/Dashboard)带走。万一真的停滞,流里仍会推 stale 事件、面板写出「数据已停止更新 Ns · 正在重试」,绝不静静冻住。断线后无限重连(指数退避封顶 30 s),窗口重新可见或停滞过久时立即重试。诊断时可用 DSH_UR_TWIN_PUSH=0 关掉推送做对照。
跟随桌面程序的颜色主题:面板的 DOM 部分用宿主设计 token(--dsw-alias-*)着色 —— 工具栏、HUD 与状态行会随「设置 → 外观」里选的主题(以及第三方主题)自动翻转,不需要 JS。3D 画布里的基座网格线与回退臂材质色由 src/client/theme.js 监听 body[data-ds-dark-theme]、在主题切换时重刷(CSS 够不到 WebGL 里的数值)。主题判定只认该属性,不退回系统偏好。
host 侧路由(均限定 loopback 调用方):
| 路由 | 说明 |
|---|---|
GET /dsh-nonead-ur/twin/stream[?ip=<ip>] |
SSE 实时流(客户端默认走这条):每 100 ms 一帧完整状态快照,另有 stale(这一帧读还没回来)与 frame-error(这一帧失败)事件 |
GET /dsh-nonead-ur/twin/state[?ip=<ip>][&detail=1] |
实时位姿(关节角 / TCP / 型号)。detail=1 附带 dashboard 侧状态(安全模式、运行状态、速度倍率、关节温度/电流等),位姿通道仍独立轮询,detail 失败不影响位姿 |
GET /dsh-nonead-ur/twin/asset?model=<urXX> |
GLB 网格,带内容哈希 ETag + immutable,支持 If-None-Match → 304 |
GET /dsh-nonead-ur/twin/models |
本地实际存在的模型清单 |
失败响应带机器可读的 code(no_robot / robot_not_connected / ambiguous_robot / worker_unavailable / robot_error)并回显本次解析到的 ip,界面据此给出不同的提示与处置建议。读暂时拿不到(worker 正忙着执行运动指令是最常见的原因)时不会报 robot_error:宿主回放最后一笔已知读数并标记 stale:true(最多 30 s),孪生保留画面并显示停滞提示。回放的帧从不带审批状态:pending_motion 是现场重读的,因此一个早已结束的审批不会被画成幽灵目标。
安装到 DSH profile
📦 尚未发布到 npm,因此没有可写进
dependencies的版本号。请从插件市场/插件页安装,或从本仓库安装(本地检出目录或它的 GitHub 地址)。
方式一:插件市场 / 插件页(GUI)
插件已在 DSH 插件市场中登记为 dsh-nonead-universal-robots。打开设置 → 插件市场(或侧边栏插件页),找到并安装。
两点须知:
- 市场在有 agent 正在运行时会拒绝安装("安装会修改插件文件,正在工作的 agent 可能中途报错")。请先结束或取消当前回合。
- 市场按目录条目里的 GitHub 地址安装,而 pnpm 对
github:规格是从codeload.github.com下 tarball。网络慢或被过滤时这次下载会超时;下面的 CLI 方式可完全绕开它。
方式二:CLI,从本地检出安装(开发时推荐)
# DSH Desktop 会为每个 profile 生成一个 CLI 包装器:
# %APPDATA%\<app>\host-commands\<profile>\generations\<hash>\bin\dsh.cmd
dsh.cmd plugin add "file:D:/path/to/dsh-nonead-universal-robots"
# (该包装器会自己补上 --profile <profile>;其他环境用 `dsh plugin --profile <profile> add <spec>`)
这与插件页走的是同一条路径:它会写入依赖和 dsh.profile.bundles 条目,并把插件热加载进正在运行的会话(无需重启 —— ur_* 工具会立刻出现在工具表里)。
⚠️ file: 安装进来的是副本而非软链:改检出目录不会影响已装的插件 —— 改了源码要重新执行这条命令。
方式三:作为 bundle 手动安装
在目标 profile 的
package.json中把本插件加入依赖:// <DSH_HOME>/profiles/<profile>/package.json { "dependencies": { "dsh-nonead-universal-robots": "file:D:/path/to/dsh-nonead-universal-robots" // 或(GitHub 可达时):"github:NoneadChina/dsh-nonead-universal-robots" }, "dsh": { "profile": { "bundles": [ // 把本插件加到数组里(顺序靠后即可) "...", "dsh-nonead-universal-robots" ] } } }通过
dsh plugin(等价于在 profile 目录内跑 pnpm)安装依赖:dsh plugin --profile <profile> install # 或:cd "$DSH_HOME/profiles/<profile>" && pnpm install重启 DSH。插件的
cordis.patch.yml会把插件行插入配置树,工具以ur_*形式出现在模型工具列表中。
Python 运行时
插件需要一个可用的 Python 以及 numpy、paramiko 才能启动 worker:
pip install -r requirements.txt
⚠️ 依赖必须装在 worker 真正使用的那个解释器里。
lib/worker.js会把PYTHONPATH指向插件的python/目录,而 CPython 启动时会 import 该目录下的sitecustomize.py(如果存在) —— 于是一个把本机用户级site-packages塞进sys.path的sitecustomize.py会"本机能跑、 换台机器就全挂"。请把pythonBin指向已装好依赖的解释器(或一个 venv)。
cordis.patch.yml 中的 pythonBin(默认 python)、commandTimeoutMs(运动/脚本超时,默认 60000)、connectTimeoutMs(首次连接超时,默认 30000)都可按部署覆盖。若 python 不在 PATH,改成绝对路径,例如 "C:\\Python312\\python.exe" 或 "/usr/bin/python3"。ur_ping / npm run test:python 就是这条链路的自检入口。
健康检查 / 测试
无需连接真实机器人即可验证插件与 Python 运行时是否就绪:
npm run test:python # python ur_worker.py --selfcheck:校验 Python/numpy/paramiko/URBasic/RTDE 配置
npm test # 56 项:42 个测试文件 + 9 条门禁 + 5 个离线探针,最后汇总
npm run test:node # 只跑 Node 侧:跳过需要 Python 的测试文件**与门禁**
npm run check # 只跑静态 + 跨语言门禁,不跑测试文件
npm run verify:host # 用宿主真实的 schema DSL 校验全部工具注册与 peer 版本
npm run verify:models # 校验 14 个 GLB 的结构契约
npm test 跑两类东西,两类都必须过:
- 42 个测试文件(端到端协议自检、审批门禁、孪生路由、客户端状态机、FK、vendored 库回归)。名单来自
test/test-manifest.json,并由check:manifest保证"会调用 Python 的用例"不会被漏登记。 - 9 条门禁 加上 5 个离线探针:
check-test-manifest/check-package-metadata/check-client-bundle/check-doc-tools(Node),check-worker-ops/check-tool-params/check-rtde-recipe/check-approval-gate/check-new-ops(Python),随后是scripts/probe-*.mjs那几个孪生/SSE 探针。 - 宿主兼容检查也会被尝试(
check-host-compat --allow-missing-host)。它是环境项,因此在汇总里单独占一行、不计入上面的 56 项:没装 DSH Desktop 的机器得到一条带原因的响亮SKIP(绝不静默放行);提供了宿主的机器/CI 会变成PASS。
被跳过不等于通过。 只要有项目被跳过,整轮就以非零退出 —— 想接受跳过必须显式要求(--allow-skip,或 Node-only 的 --skip-python),而全部被跳过时无论如何都算失败。两个探针需要全局 EventSource,用 --experimental-eventsource 运行(Node ≥22.3);再加上 module.registerHooks(Node ≥22.15),真实的开发下限记录在 devEngines 里。
npm test 会打印逐项汇总。若 python 不在 PATH,用环境变量指定:
UR_PYTHON=C:\\Python312\\python.exe npm test
发布前、以及每次升级 DSH 内核后,都应跑一遍宿主兼容自检 —— 它拿已安装的宿主校验本插件,而不是校验一份抄下来的关键字清单:
npm run verify:host # 严格模式:找不到已装宿主时 exit 2
npm run check:host # 同一检查,但缺宿主时给一条响亮的 SKIP
node scripts/check-host-compat.mjs <node_modules> # 也可显式指定宿主
node scripts/check-host-compat.mjs --host-asar "<...>/resources/app.asar" # DSH Desktop 安装形态
npm run check:host:strict # prepublishOnly 用的严格模式:缺宿主即 exit 2
它会用宿主实装版本核对 peer 范围、用宿主真实的 value-schema DSL 编译每一个工具参数 schema(被拒的 schema 会让该工具静默消失)、驱动宿主路由注册,并校验 dsh.client 声明与客户端 bundle 的 __ModuleLoader__ 注册 id。退出码 0 表示与该宿主兼容;报告里会写清"声明了多少个、注册成功多少个"(当前 104/104)。
--host-asar 直接读 Electron 的 app.asar(DSH Desktop 的标准安装形态:node_modules 打包在 asar 内、磁盘上没有目录):按需把用到的包懒解包到临时目录,宿主的真实 DSL 仍然会执行。prepublishOnly 现在是 npm run check && npm run check:host:strict && npm test,因此发布不可能再绕过真实宿主检查与测试套件。
CI(.github/workflows/ci.yml)在 Node 22.15/24 × Python 3.9/3.12 上跑同样的两条命令,并做完整检出(14 个 GLB 是普通 git 对象,模型测试真的会读它们)。
首次连接到某机器人时,
ur_connect有约 20 s 的 RTDE 就绪等待,超时会返回清晰错误而非卡死。
工作原理
DSH 模型 @deepseek-ai/dsh-tools python/ur_worker.py UR 机器人
│ ur_movej(...) │ │ │
├────────────────► ctx.tools.register(defineTool) │
│ │ lib/worker.js │ │
│ ├── spawn(ur_worker.py) ──────►│ import URBasic │
│ │ {id,op,params} ───────────►│ RTDE+Dashboard+RTC │
│ │ ◄── {message,data} ─────────┤ 各 op 执行 │
│ ◄── text ───────┤ │ │
python/ur_worker.py:常驻 stdio JSON worker。维护ROBOTS/ROBOT_MODELS字典,按 IP 连接,每次调用一条指令。运动指令用「到位确认」轮询(有界),并在出错时返回结构化错误。lib/worker.js:按需拉起 worker 一次并保持存活,行分隔 JSON 协议配对请求/响应,带超时与取消(转发exec.signal)。lib/index.js:Cordis 插件,导入defineTool把上述控制逻辑注册成工具;inject: ['tools']。
工具命名与示例
工具名稳定为 ur_*,示例提示词:
连接 192.168.1.199,读取当前 TCP 位姿。
ur_connect(ip="192.168.1.199")
ur_get_tcp_pose(ip="192.168.1.199")
让 192.168.1.199 的 TCP 沿 Z 轴下降 20 mm。
ur_get_tcp_pose(ip="192.168.1.199")
ur_move_z(ip="192.168.1.199", distance=-0.02)
绘制一个半径 50 mm 的圆(竖直平面)。
ur_draw_circle(ip="192.168.1.199", center=[0.3,-0.2,0.4,0,3.14,0], r=0.05)
注意事项 / 限制
- 连接持久性:worker 进程在插件生命周期内常驻,机器人连接按 IP 保留;worker 崩溃或 DSH 重启后需重新
connect。 - 远程控制模式:部分 UR 机器人需处于「远程控制」才能执行运动/程序指令,
ur_connect会报告该状态。URSoftware 3.1–3.20 之间的 CB3 机器人默认就允许远程控制;但remote_control: false仍说明控制器当前不在远程模式 —— 本地/示教模式下 URScript 与运动指令会被静默丢弃,需在示教器上切到远程控制。其他固件(e-Series,或不在该区间的 CB3)则需先在 PolyScope 中开启 Remote Control。状态读不到时按"未知"处理,而不是假装是false。 - 读不到就不假装读到:没有回读通道的设置型指令只回报
verified: false;控制器拒绝 RTDE 会话时报"未运行"而不是"已连接";孪生某一帧刷新不出来会标stale。 - 本地文件访问:
ur_upload_program会读本地文件、ur_download_program会写本地文件,因此两者都被锁在programDir(默认<DSH_HOME>/nonead-ur/programs,可用programDir配置改)内。宿主负责在realpath之后判围栏(软链/junction 都带不出去)、检查.urp与maxProgramBytes(1 MiB),通过后才把绝对路径以_local_path/_remote_path注入 worker;模型自己无法设置这些下划线协议字段(sanitizeArgs只保留 schema 声明过的键),worker 侧也会拒绝非绝对的_local_path。空programDir落到默认目录;目录缺失或不可用时 fail closed:不读也不写任何本地文件。这与"把 shell 交给模型"是同一类暴露面,所以这里是设围栏,而不是靠信任。 - SSH 口令在审批与审计中脱敏:
password/passphrase/token/secret/known_hosts的值在审批提示与 NDJSON 审计账本里一律替换为***,因此确认上传/下载时口令不会出现在界面或audit.ndjson里;worker 仍会收到 SFTP 所需的真实值。 - 程序传输不是"发出去就算完":上传与下载都比对另一侧的字节数,删除会在删完之后再
stat一次。不一致就报失败(VERIFY_FAILED),绝不报成功 —— 因为调用方的下一步很可能就是去运行那个程序。 - 命名位姿是本机数据,不是机器人状态:
ur_save_pose/ur_get_pose/ur_list_poses/ur_delete_pose只在poseDir下按"一位姿一文件"读写 JSON,不碰控制器(除非让ur_save_pose捕获当前姿态,那需要ip)。空poseDir使用默认目录(<DSH_HOME>/nonead-ur/poses),目录在首次保存时自动创建;目录建不出来或读不了时 fail closed。真要走到某个保存过的位姿,请分两步(先ur_get_pose再ur_movel),让门禁看到真实数字。 - 长时间运动绝不能让孪生冻结:孪生唯一不受命令队列影响的数据源是 worker 的遥测推送线程,而那个线程与 RTDE、Dashboard 装在同一个 Python 子进程里。因此单次工具调用绝不能因为"运动合法地超出了调用预算"就杀掉这个子进程:运动指令的预算现在按参数推算(
t=/timeout_s,力控引导运动另给宽兜底),并且一律killOnTimeout: false—— 超时仍会如实报超时,只是不再毁掉会话(以及孪生)。 - 遥测走独立通道:推送帧走 stderr + 魔数前缀并用自己的锁,不再排在响应写锁后面的 stdout 上。共用 stdout 意味着:一条大响应写(或 stdout 背压)能把那把锁占住超过 300 ms,推送一停,路由就回退到显式读 —— 而那次读正好排在运行中的运动指令后面。普通 stderr(traceback 等)仍原样进诊断尾巴;宿主两路都收,旧版 Python 侧也能工作。
- 工具端数字 I/O:
ur_get_digital_in/ur_set_digital_out的which="tool"用于工具端(法兰侧)数字 I/O。工具端数字信号不经 RTDE,因此读取工具输入会发送一段短的 URScript 程序,可能打断正在运行的程序。工具端数字输出的读回不支持(没有可靠的read_tool_digital_out表达式)。 - 关节电流:接收配方里本来就有
actual_current,ur_get_runtime_telemetry会以joint_currents返回它;缺的只是一个单独的ur_get_joint_current工具,而不是这个值本身。 - 取消 / 超时 ≠ 让机器人停下:取消一次工具调用(或让它超时)只是不再等待回复,不会发送
stopj/stopl,机械臂可能仍在运动。调用会以WORKER_ABORTED/WORKER_TIMEOUT错误码失败;只要还有运动在飞(或结果未知),worker 会被刻意保留而不是杀掉。收尾请用ur_stopj/ur_stopl(力控引导运动之后用ur_stopl),再假定机械臂已静止。 - 非安全边界:此插件与 bash 工具同级,可驱动物理设备,务必在真实机器人上充分测试后再用于生产。
- 未知型号回退:若某机器人没有内置网格(未知或定制型号字符串),会回退到近似几何体渲染而不报错,数字孪生不会因未知型号失败。
- 资产:14 个
assets/models/*.glb网格(约 21 MB,几乎就是整个发布包)由python scripts/convert-meshes.py生成,其结构契约由python scripts/verify-models.py校验(即npm run verify:models)。它们的体积构成与许可来源见THIRD_PARTY_NOTICES.md;npm run analyze:models可看字节都花在哪里。
参考实现
本插件与拓德科技的 Nonead-Universal-Robots-MCP(GitHub: https://github.com/nonead/Nonead-Universal-Robots-MCP)同源,复用其中的 URBasic 库与机器人控制逻辑,由同一团队面向 DeepSeek Harness 重新封装为本插件的 ur_* 工具与 stdio worker 协议。URBasic 为 MIT(© Tony Ke & Anthony Zhuang / Universal Robots,2009-2025)。
第三方资产与许可
除插件自身代码外,本包还随附 14 个机器人网格 assets/models/*.glb(约 21 MB)与 assets/kinematics.json,全部派生自 Universal Robots 的 Universal_Robots_ROS2_Description(humble 分支,获取日期 2026-09-21)。
许可按型号分为两类,同一型号内不会混用:
- BSD-3-Clause(9 款):
ur3、ur5、ur10、ur3e、ur5e、ur7e、ur10e、ur12e、ur16e—— 同时涵盖assets/kinematics.json所派生的运动学与关节限位配置。 - UR「Graphical Documentation」条款(5 款):
ur8long、ur15、ur18、ur20、ur30。这 5 款的网格不是 BSD-3-Clause,而属于 UR 的「Graphical Documentation」,其使用受 UR 的 Terms and Conditions for use of Graphical Documentation 约束 —— 该条款不符合 OSI 开源定义,但允许在若干限制下使用、修改与分享。相关疑问请联系 legal@universal-robots.com。
客户端 3D 渲染基于 three.js(MIT);esbuild(MIT)仅用于构建期。发布产物 lib/client.js 内嵌了 three.js 代码(esbuild 会把客户端半打进同一个 bundle),因此 three.js 的 MIT 声明也必须随包分发。
逐型号派生清单、转换参数与许可全文见 THIRD_PARTY_NOTICES.md。该文件现在会随 npm 包发布(package.json 的 files 白名单),并包含 vendored URBasic 与内嵌于 lib/client.js 的 three.js 的完整 MIT 全文。
License
本项目采用区分用户的双重许可 (User-Segmented Dual Licensing) 模式:
- AGPLv3:适用于个人用户及3 人及以下企业/组织(开源;详见 https://www.gnu.org/licenses/agpl-3.0.html)。
- 商业许可证(必须):适用于超过 3 人的组织,或任何需要规避 AGPLv3 源代码公开义务(如 SaaS / 闭源分发)的用户。
完整条款见 LICENSE。商业授权请联系 service@nonead.com。
python/URBasic(vendored)仍沿用其自身 MIT 许可(© Anthony Zhuang / Universal Robots,2009-2025)。
链接
同类插件
Tencent/WeKnora#dsh-weknora★ 35688
把 WeKnora 知识库接入 dsh 的四个只读工具:列出知识库、混合检索原文片段、按顺序还原单篇文档,以及直接取用 WeKnora 自己带引用的 RAG 或 ReAct agent 回答(含可续聊的 session id)。
superdesigndev/treg★ 6403
给 Agent 的工具目录:按「要做的事」检索约 2,600 个外部接口(SEO 与 SERP、外链、社交、人物与公司信息补全、广告库、抓取),查看参数与单次调用价格后直接调用,凭据由服务端注入。附带技能,MCP 行在未设置 TREG_TOKEN 前保持禁用。
T-Auto/dsh-ops★ 4593
为 Windows 的 DSH 提供 bash、PowerShell 7 及 Rust 高性能工具,减少 token 消耗。
sixtysevenlf/dsh-blender-plugin★ 1388
让 DSH agent 通过直连 TCP 通道驱动本地 Blender,提供 15 个工具:视口帧、自定义视角渲染、Python 执行、无头与作业卸载、事务回滚,以及 34 个 family / 228 个 op 的规划层。
TencentCloudBase/CloudBase-AI-Toolkit#dsh-plugin★ 1152
把腾讯云 CloudBase 后端接入 DeepSeek Harness——在对话里搭好并部署全栈应用,查询结果渲染为表格卡片(分页、排序、导出 CSV),部署后可预览真实域名,并提供 CloudBase MCP 工具集(`mcp__cloudbase__*`),登录走 device-code 流程。
anysearch-team/anysearch-dsh★ 665
基于 AnySearch 的实时网页与垂直搜索插件,为 DeepSeek Harness 提供搜索工具。
社区评论
评论公开保存在 GitHub Discussions。加载评论会连接 GitHub 和 Giscus;发表内容需要 GitHub 账号。