DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

peng456 /

peng456/dsh-plugin-mesh

Verified

DSH 局域网 Mesh 插件:自动发现同网段的其他 DSH 实例,提供对端操作页面,支持跨机双向派发任务。

★ 0 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: main@bccf68b3

dsh-plugin-mesh

DSH 的局域网 Mesh 插件:启动后自动发现同一网段的其他 DSH 实例,在侧栏提供一个 操作页面显示各机器的在线状态,并支持把任务派给对端的 agent 执行。

用起来就是你要的那个效果:在 A 机器上开发,把「部署到 B」这件事丢给 agent, 你只下大命令。

零配置:固定端口 + 自动扫描 + ping,装上就能发现彼此,不需要配对码。 首次打开 Mesh 页面会让你给这台机器起个名字(默认填主机名),别的机器就靠这个名字找你。

┌──────── A 机器 (DSH) ────────┐          ┌──────── B 机器 (DSH) ────────┐
│  mesh 插件  :45917            │          │  mesh 插件  :45917            │
│   ├ UDP 组播心跳  ◀───────────┼──────────┼──▶  被动发现                  │
│   ├ 主动扫本网段  ────────────┼──────────┼──▶  兜底发现                  │
│   ├ ping 保活     ────────────┼──────────┼──▶  在线状态                  │
│   └ 侧栏 Mesh 页面            │          │                              │
│                               │───任务──▶│  唤醒本机 agent 执行          │
│                               │◀──回派───┤                              │
└───────────────────────────────┘          └──────────────────────────────┘

发现机制:两条互补的路

只靠组播是不够的 —— 很多路由器/AP 会隔离客户端组播,Windows 防火墙默认也拦入站组播。 所以这里同时用两条路,任一条通就能发现:

路径 做法 优点 缺点
组播心跳 每 5s 向 239.255.42.99:45892 广播,同时监听 零配置、即时、不产生额外连接 可能被路由器/防火墙挡掉
主动扫描 并发 GET 本网段每台机器的 :45917/mesh/v1/health 只要 TCP 通就能成,最可靠 扫一次约 1~2 秒;254 个地址

再加一条ping 保活:每 10s GET 已知对端的 /mesh/v1/health,刷新在线状态。 这条让"发现"一旦建立就不再依赖组播 —— 即使组播完全不通,扫描见过一次之后也能一直维持。

扫描时机:启动时(带退避重试,因为常抢在对方启动之前)+ 每 60s 一次 + UI 上手动触发。

默认端口 45917。固定端口是"扫一下就能连上"的前提。同机跑多个实例时端口会被占用, 此时自动退回随机端口(组播仍能找到,但主动扫描扫不到它)。

组播的作用范围:比"局域网"更窄

239.255.42.99:45892 是我在私有组播段里挑的(239.0.0.0/8 是 RFC 2365 定义的 管理范围,相当于组播里的"私有地址"),TTL 设为 1,所以:

  • ✅ 同一网段(同一个二层广播域)内有效
  • ❌ 路由器不转发 —— 跨子网/VLAN 到不了,那种情况要配 scanRanges
  • ❌ AP isolation 会连它一起挡(那是把客户端之间的所有流量都隔离,不只是组播)
  • ⚠️ 部分交换机的 IGMP snooping 在没有 querier 时会丢掉组播
  • ⚠️ Windows 防火墙默认拦入站组播,要放行 UDP 45892

正因为组播有这么多坑,才加了扫描那条路:只要 TCP 通,扫描就能发现。


身份与命名

每台机器有两个标识,分工不同:

标识 是什么 稳定性 用途
instanceId 启动时随机生成的 8 字节 hex 每次启动都变 内部用:去重、识别"这条是我自己发的"
machineName 人可读的名字,如 客厅的Mac 持久保存 你和 agent 用它指代这台机器

名字解析优先级:插件配置 machineName > 用户首次启动时填的 > 系统主机名。

首次启动时(还没命名过、落在主机名上)Mesh 页面会主动问你一次:输入框预填主机名, 点「就这个」直接用,或改成你想要的名字。名字存在 $DSH_HOME/mesh/state.json (和配对码同一个文件,权限 0600),改完立刻重新广播,对端不用重启就能看到。

重名怎么办

名字是人手填的,可能撞。页面会检测并告警,派发时也不会随便挑一台:

  • 从 A 看:--peer winbox 只要在 A 的上游对端里唯一就能用(自己不算)
  • 从第三台机器看:如果它发现有两台都叫 winbox,mesh_run 会拒绝并列出候选, 让你改用 <ip>:<port> 精确定位

地址也接受实例 id 或 ip:port,所以撞名时总有办法精确指定。


实测验证状态

以下都在真实 DSH host 上跑出来(临时 DSH_HOME,未动你正在用的应用):

项目 结果
配置装配进 profile ✅ --dump-config 含本项目两行
插件加载、apply 执行 ✅ 挂载 5 个 UI 接口、注册 4 个工具
两个真实 DSH 互相发现(仅扫描,组播全关) ✅ 双向,24 秒内(含启动竞态重试)
两个真实 DSH 互相发现(组播开) ✅ 双向,1 秒内
零配对派发任务 ✅ ok: true,全程没有任何配对操作
对端 agent 被唤醒 ✅ 建了 mesh-* 会话,17 个事件,含任务提示词 + 线程 + 回派指令,turn/start → assistant/attempt
离线判定 ✅ 杀掉对端后约 20 秒标记离线(ttl 20s)
固定端口被占用 → 回退 ✅ 配置 45919、实际 63570
requirePairing: true 时 ✅ 错误 token → 403;正确 token → 200
首次启动询问名字 ✅ 未命名时 needsName=true,输入框预填主机名
改名立刻传播 ✅ 改完 2 秒内对端就看到新名字(立即重新广播)
名字持久化 ✅ 存 state.json,重启后仍是 客厅的Mac / source=saved
名字校验 ✅ 空、含 /、超 32 字符均被拒
配置优先 ✅ 配置里写了 machineName 时改名返回 409,不静默覆盖
重名检测 ✅ 两端 duplicateNames 均正确列出
客户端 bundle 被提供 ✅ 出现在 __DSH_BOOT__,路由 200,内容与源码逐字节一致
UI 页面渲染 ✅ 机器列表 / 任务记录两个 tab
不花你的 token ✅ 测试环境无模型密钥,agent 止步于 no API key

没验证的(如实说明):

  • 没在你正在运行的 DSH Desktop 里验证 —— 需要重启应用,会掐断本次会话。 用 dev-boot.mjs 复现了真实主进程的模块解析行为来代替。
  • Windows 侧没跑(手上没那台机器)。代码跨平台,但 Windows 防火墙要放行 TCP 45917 和 UDP 45892。
  • 扫多端口(scanPorts)是我为了在同机测试才加的,真实两机场景用不到。

安装

两台机器都装。装完必须重启 DSH Desktop 才会加载。

方式 A:一键脚本(推荐)

git clone https://github.com/peng456/dsh-plugin-mesh.git
cd dsh-plugin-mesh
./install.sh

脚本做两件事,且可重复执行(已装就跳过):

  1. 把插件复制到 ~/.dsh/profiles/desktop/node_modules/dsh-plugin-mesh/
  2. 把它加进该 profile package.json 的 dsh.profile.bundles (改前自动备份为 package.json.bak)

常用选项:

./install.sh --dry-run              # 只打印会做什么,不落盘
./install.sh --profile web          # 装到别的 profile
./install.sh --home /path/to/.dsh   # 指定 DSH_HOME
./install.sh --uninstall            # 卸载(删目录 + 从 bundles 移除)

方式 B:手动装

# macOS
cp -R dsh-plugin-mesh ~/.dsh/profiles/desktop/node_modules/dsh-plugin-mesh

# Windows (PowerShell)
Copy-Item -Recurse dsh-plugin-mesh "$env:USERPROFILE\.dsh\profiles\desktop\node_modules\dsh-plugin-mesh"

然后在 ~/.dsh/profiles/desktop/package.json 的 bundles 里加上 "dsh-plugin-mesh":

{
  "dsh": {
    "profile": {
      "bundles": ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-web-app", "dsh-plugin-mesh"],
      "patchReload": "live"
    }
  }
}

⚠️ 必须是真实目录,不能是软链。 DSH 的 profile 包解析器会拒绝 realpath 落在 <profile>/node_modules 之外的包(module-resolution-*.js 里那条判断, 目的是防止陈旧的外部状态覆盖已封装安装)。把插件软链到你的工作目录会在启动时 报 ERR_MODULE_NOT_FOUND。install.sh 已经处理好了这一点。

方式 C:官方命令(用 DSH Desktop 自带的终端)

托盘/菜单 → 终端:

dsh plugin add /path/to/dsh-plugin-mesh

注意 dsh plugin --profile desktop 在普通终端里会被拒绝 (bin.js:29:profile "desktop" is managed exclusively by the Electron application), 必须用应用自带的那个终端。

Windows 防火墙

New-NetFirewallRule -DisplayName "DSH Mesh" -Direction Inbound -Protocol TCP -LocalPort 45917 -Action Allow
New-NetFirewallRule -DisplayName "DSH Mesh (multicast)" -Direction Inbound -Protocol UDP -LocalPort 45892 -Action Allow

只放行 TCP 也行 —— 那时靠主动扫描发现,不依赖组播。

使用

侧栏会出现一个 Mesh 图标(三个节点连成的网状),点开是主页面:

  • 本机信息:机器名、地址、端口、当前发现方式与配对要求
  • 发现的机器:每台一行,在线绿点/离线灰点,显示地址、系统、用户、最后心跳; 每行「探测」和「派发任务」
  • 扫描本网段 / 重新广播 按钮
  • 派发任务:选目标机器 → 填任务 → 派发
  • 任务记录:收到 / 派出的任务
  • 配对码(折叠,默认不需要)

agent 多出四个工具:

工具 作用
mesh_peers 列出发现的机器
mesh_scan 主动扫本网段,找还没发现的机器
mesh_run 把任务派给某台机器的 agent(异步)
mesh_tasks 查看收到/派出的任务记录

所以你可以直接说:

「看看局域网里有哪些机器,然后让 B 把 web 服务重新部署一下,跑完告诉我。」


工作原理

唤醒:自包含实现,不依赖额外插件

agent 没有事件循环,只在有回合时才思考。所以收到对端任务后,必须建一个会话并让它跑起来。

DSH 提供了官方插件 @deepseek-ai/dsh-webhook 做这件事,但本插件没有用它,理由:

  1. 它不在任何默认 bundle 里,靠补丁替用户插一行,会让"装一个插件"变成配置里两行,困惑
  2. 更实际的问题:万一它已经被启用(比如你以后装了 GitHub webhook 插件), cordis 的 provide 遇到重名会直接抛错(service "webhookRuntime" has been registered at <...>), 而且那是在它的构造函数里,本插件的安全网拦不住

所以这里按官方 createWebhookSession 的做法自己走一遍,用到的都是 DSH 基础服务 (桌面 profile 本来就全都有):

ctx.permissionPresets.resolve / set      校验并设置权限 preset
ctx.agentPresets.resolve / mount         解析并挂载 agent preset
ctx.workspaceRegistry.create             建/取工作区
ctx.agents.create                        建 Agent
ctx.sessionTitle.rename                  给会话起名
ctx.agentDefaultModel.currentSelection   取默认 provider/model
agent.followup(createUserMessage(...))   ← 这一步才是"唤醒"

服务齐备时才启用(用 ctx.inject([...]) 探测);缺任何一个就降级为"只收件不唤醒", 并在日志里说明缺了什么。插件是自包含的,profile 里只多一行。

为什么发现要两条路

我先只做了组播,后来加扫描,是因为组播在真实家庭网络里失败率比想象中高: AP isolation、Windows 防火墙默认策略、跨 VLAN,任何一个都能让它静默失效。 而"TCP 直连固定端口"只要网络通就能成。


配置项

默认已经可用。要改就在 profile 的 cordis.patch.yml 里按 id 覆盖 (注意:patch 是整体替换 config,所以要把想保留的项一起写上):

- id: mesh
  config:
    machineName: ''            # 对端列表里的名字,留空用主机名
    port: 45917                # 固定端口
    useMulticast: true         # 组播被动发现
    scanOnStart: true          # 启动扫描(带退避重试)
    scanIntervalMs: 60000      # 周期扫描;0 = 关闭
    scanRanges: []             # 额外网段,如 ["192.168.1.0/24"];空则扫本机 /24
    scanPorts: []              # 除本机端口外额外要扫的端口
    scanConcurrency: 64
    scanTimeoutMs: 800
    pingIntervalMs: 10000      # ping 保活;0 = 关闭
    announceIntervalMs: 5000
    peerTtlMs: 20000           # 多久没消息算离线
    requirePairing: false      # 见下
    agentPreset: standard
    permissionPreset: workspace-write
    workspaceRoot: ''
    # ── 循环限速 ──
    maxWakesPerThread: 30      # 每窗口内最多自动唤醒几次;0 = 不限制
    threadWindowMs: 3600000    # 窗口长度(毫秒),默认 1 小时
    threadCooldownMs: 20000    # 两次自动唤醒的最小间隔;0 = 不限制
    minTaskChars: 24           # 正文短于此长度只入箱不唤醒(挡 ping/ack)

循环限速(重要)

为什么需要:入站任务会自动新建会话并立刻让 agent 开跑,而每条消息都由 远端决定。两个 agent 互相回执时会形成自持的 ping-pong —— 实测一个互回执线程 在 2.5 小时内自动开了 54 个会话、约 800 万 token,全部是重复探索。

四道闸门,任何一道触发都只入箱、不唤醒(任务仍可在 /mesh/v1/tasks 和 Mesh 页面看到,不会被丢弃):

闸门 默认 作用
minTaskChars 24 太短的内容不开会话
寒暄识别 内置 ping / ack / 收到 / 好的 等整条即寒暄的,不开会话
threadCooldownMs 20s 同一 thread 不会连续秒回,管突发
maxWakesPerThread 30 同一 thread 在 threadWindowMs 内的唤醒上限,管持续速率

maxWakesPerThread 是滑动窗口限速,不是终身配额:只统计最近 threadWindowMs(默认 1 小时)内的唤醒。窗口内的唤醒点会随时间自然过期, 该 thread 自动恢复,不需要重启。

⚠ 限速只会让循环变慢,不会让它停。 如果循环的自然节奏低于上限, 窗口永远填不满,限速就永不触发 —— 实测事故节奏约 21 轮/小时,即使把上限 压到 10/小时,24 小时也仍会放行 276 轮。要真正止住请把上限设得低于 循环节奏,或配合会话复用把单价降下来。

撞上限时交给用户决定

限速触发时不静默丢弃,而是在 Mesh 页面出现一张提示卡:

⚠ 有 thread 撞到唤醒上限,已暂停自动唤醒
  diet-reconcile   上限 30 次 / 60 分钟 · 已挡下 12 条
  [提高上限并放行最新一条]  [忽略]
  • 提高上限 → 把该 thread 的上限翻倍(至少 +10),并立刻放行最新那一条 (对话的最新一环)。其余被挡的仍留在收件箱 —— 一次性全放会造成新突发, 反而失去限速的意义。覆盖值落盘,重启后仍有效。
  • 忽略 → 只清提示,上限不变,任务仍留在收件箱。

对应的 API:POST /mesh/api/throttle/raise {thread, cap?}、 POST /mesh/api/throttle/dismiss {thread};被挡的 thread 列表在 GET /mesh/api/state 的 throttled 字段里。

账本与覆盖值落盘在 ~/.dsh/mesh/threads.json,重启不清零 —— 否则对端 只要等到你重启就能重新灌满窗口。

另外两点配套改动:

  • 同 thread 复用会话:同一 thread 的后续消息注入同一个会话,不再每条 从零重新探索。这是省 token 的最大一项,也让 agent 记得前几轮结论。
  • noWake: true:发送方可以显式声明"这条只回执,别开跑"。mesh_run 工具已带这个参数 —— 纯回执/确认请用它,否则会凭空开一个 agent 会话。

已知绕过:限速只认 thread 名字,而名字由发送方自由填写。对端若每次都 换 thread 名或不带 thread,就不受限速约束(此时只有 threadCooldownMs 的 突发冷却在起作用)。它是刹车,不是安全边界。


安全

默认不需要配对 —— 同一内网里任何发现到本机的 DSH 都能派发任务。 对本机 agent 而言,这些任务会被当成一条用户消息(默认权限 workspace-write: 能读写工作区、能执行命令)。

这在自己家的可信内网里是合理取舍,也正是你要的"零配置"。但如果你在 不完全可信的网络里(合租、公司、宿舍、有访客设备、IoT 设备多),请打开:

- id: mesh
  config:
    requirePairing: true

然后在两台机器的 Mesh 页面里互相对一下配对码(首次启动自动生成,页面上可直接复制)。 打开后所有控制接口都要过配对码,用 timingSafeEqual 比较。

其他:

  • 发现是公开的:同一网段任何人都能看见机器名、地址、系统、用户名。这是有意的取舍。
  • 明文 HTTP:局域网内可接受;跨不可信网络请套 SSH 隧道或 TLS 反代。

已知限制

  1. 同 thread 复用会话,但跨 thread 不复用:同一 thread 的后续消息会注入 同一个会话;没带 thread 的任务仍然每次新建。(早于 2026-09 的版本无此复用, 每条消息都新建会话。)
  2. 去重只在进程生命周期内(内存),重启后同 id 会重复入箱。
  3. 没有离线排队:对端离线时 mesh_run 直接失败,不暂存重投。 (要这个能力可以用工作区里的 lanmsg.py。)
  4. 同机多实例:第二个实例端口被占会退回随机端口,主动扫描扫不到它(组播仍能发现)。 真要同机多实例,给它们配不同 port 和对方的 scanPorts。
  5. sidebar.panellist 槽位官方插件没人用过(翻遍 58 个客户端 bundle,只有官方侧栏 自己声明它)。我按源码契约接的,实测注册和渲染都正常,但不如 sidebar.footer.action 那样有第三方先例。

开发与测试

# 搭隔离环境(把插件真实复制进 profile,不碰你的 desktop profile)
MACHINE=macbox PORT=45917 MULTICAST=false SCAN_PORTS="[45918]" \
  ./dev-setup.sh meshtest /tmp/mesh-dsh

# 启动(dev-boot.mjs 复现真实主进程的模块解析行为)
DSH_HOME=/tmp/mesh-dsh ELECTRON_RUN_AS_NODE=1 \
  "/Applications/DSH Desktop 2.app/Contents/MacOS/DSH Desktop" \
  dev-boot.mjs --profile meshtest --port 8877

dev-setup.sh 支持的环境变量:MACHINE PORT MULTICAST SCAN_ON_START SCAN_PORTS PING_INTERVAL_MS REQUIRE_PAIRING。

为什么需要 dev-boot.mjs:desktop-cli.js:78 只在 app.asar 内才安装 profile 包解析器,而这个安装是未打包的 Resources/app。真实 Electron 主进程 (main.js:4478)是无条件安装的,所以真实应用没这个问题,只有 CLI 路径有。

发现模块可以脱离 DSH 单独测:

node lib/_selftest-discovery.mjs alpha 45900 9000   # 两个进程同时跑可验证互相发现

文件结构

dsh-plugin-mesh/
├── package.json           # dsh.bundle.patch + dsh.client 声明
├── cordis.patch.yml       # 只插入本插件(含默认配置)
├── lib/
│   ├── index.js           # 主机侧:发现编排 + 对端 HTTP + agent 工具 + UI 接口 + 入站唤醒
│   ├── discovery.js       # UDP 组播被动发现 + 对端注册表 + 过期清理
│   ├── scan.js            # 主动扫描(固定端口 + CIDR 展开)
│   └── _selftest-discovery.mjs
├── client/
│   └── client.js          # 浏览器侧:手写 bundle,无需打包工具
├── dev-setup.sh           # 隔离测试环境搭建
├── dev-boot.mjs           # 带模块解析器的测试启动器
└── README.md
—/ 5

No ratings yet

Verified DSH bundle

Commit bccf68b3369f

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout