Zum Hauptinhalt springen
9

dsh-computers

9ashwin/dsh-computers

Connect your own machines to a DSH Web deployment and run commands on them from the agent.

Installation

dsh plugin --profile web add github:9ashwin/dsh-computers

README

dsh-computers

把你自己电脑(Mac / Windows / Linux)接到一个 DSH Web 部署上,让部署里的智能体在这些电脑上执行 shell 命令。

形态是「一台服务器作为唯一主控,其余电脑作为被控端接进来」:被控端只向外发起一条长连接,主控端负责注册表、在线状态和命令派发。

  • 每台电脑都装同一个插件,所以不需要在终端里粘命令:桌面端 DSH 打开 设置 → 电脑 → 本机运行时,填服务器地址 + 加入密钥,点「连接」即可。
  • 服务器那一侧是 电脑 列表:在线状态、重命名、删除,以及 + 连接电脑 生成的兜底命令(给不装 DSH 的机器,比如跑 launchd/systemd 的 Mac mini、Linux 服务器)。
  • 对话里智能体用 computer_exec 把命令派到指定电脑上跑,用 computer_list 看有哪些机器。

没有内网穿透、没有开放端口:被控端只向外发起一条 SSE 长连接,所以两台 Mac 在 NAT/家用网络后面也能连上。拓扑明确是 hub-and-spoke,不支持电脑之间互相控制,也不做 P2P。

┌──────────── 服务器(dsh web,role: hub)────────────┐
│ dsh-computers 插件                                  │
│  · 电脑注册表 + SSE hub + 命令派发                  │ ◀──SSE──── ┌─ 这台 Mac(桌面端 DSH + 插件)─┐
│  · computer_list / computer_exec 工具               │ ──命令──▶ │  设置 → 电脑 → 本机运行时      │
│  · 设置 → 电脑 列表 + 连接电脑                      │ ◀─结果─── │  连接后即成为被控端            │
└─────────────────────────────────────────────────────┘           └───────────────────────────────┘
        ▲ 浏览器(手机/笔记本都能控制全部电脑)          ◀──SSE──── ┌─ 另一台 Mac / Linux ──────────┐
        │                                                          │  同样装插件,或跑 agent 兜底   │
        └─────────────────────────────────────────────────────────┴───────────────────────────────┘

1. 服务器:装插件(主控)

插件是纯 JavaScript,无构建、无第三方依赖,直接从 GitHub 装:

推荐从 npm 安装(和官方插件包一样):

# 方式 A:CLI
dsh plugin install dsh-computers

# 方式 B:DSH 设置 → 插件市场 → 安装,搜 dsh-computers

也可以直接从 GitHub 装(跟随仓库最新提交):

dsh plugin install github:9Ashwin/dsh-computers

更新

npm 安装:插件市场 → 已安装 → 「检查更新」即可,或者在 CLI 里装具体版本:

dsh plugin install dsh-computers@1.7.1

GitHub 安装:profile 里是按 spec + commit 固定的(github:user/repo 或 #v1.0.2),所以

  • 用同一个 spec 再装一次只会得到 already-installed(这就是「再安装没法更新」的原因);
  • 装 tag 会被冻住,市场永远检测不到更新;装分支(不带 #)才能被市场「检查更新」发现新提交;
  • 手动升级到某个版本要换 spec:dsh plugin install github:9Ashwin/dsh-computers#v1.9.2。

更新完记得重启 Host 并刷新页面,具体见 docs/deploy.md。

装好后重启 dsh web,设置里会出现 电脑 页。

服务端建议把 role 固定成 hub(只当主控)。对外地址会自动识别:你从哪个域名打开设置页(含反向代理的 X-Forwarded-Proto/Host、Forwarded 头),生成的命令就用哪个地址,通常不用配。需要强制指定时才写 publicUrl:

# ~/.dsh/profiles/<profile>/cordis.patch.yml
- id: dsh-computers
  name: dsh-computers
  config:
    role: hub
    # 留空 = 按浏览器请求自动推导(含反代的 X-Forwarded-* / Forwarded);
    # 只有不想信任代理时才显式写死,例如 https://dsh.example.com
    publicUrl: ''
    execEnabled: true          # false 时拒绝所有远程执行
    execTimeoutMs: 120000      # 单条命令默认超时
    keyTtlMinutes: 15          # 加入密钥有效期

安全前提:请开启 DSH 的浏览器登录/可信来源(dsh web 默认就有),不要把裸端口暴露给公网。插件给"能使用这个 DSH 的智能体"的权限,等于能在这台电脑上执行任意命令。

2. 每台电脑:装同一个插件(被控)

  1. 按上一节的方式在这台电脑的 DSH 里装上 dsh-computers(桌面端 / 命令行装法相同)。
  2. 打开 DSH → 设置 → 电脑,在 本机运行时 卡片里填:
    • 服务器地址:你服务器的对外地址,如 https://dsh.example.com
    • 加入密钥:在服务器的 设置 → 电脑 → + 连接电脑 里复制的 dshc_…
  3. 点 连接,卡片变成「运行中」,服务器列表里随即出现这台电脑(在线)。

被控端默认 role: both,所以这台电脑自己也能当主控;只想当被控端就设 role: runtime。

运行时的开关是持久的:DSH 一启动就自动连回服务器,不用每次手动点。关掉 DSH 就离线。

兜底:不装 DSH 的机器

Mac mini 常驻、Linux 服务器这类不跑桌面的机器,用服务器生成的命令(launchd / systemd 常驻):

curl -fsSL 'https://<你的服务器>/api/dsh-computers/join.sh?key=dshc_xxx' | sh

它会下载 agent 到 ~/.dsh-computers/、注册系统服务并启动。手动管理:

node ~/.dsh-computers/agent.mjs status
node ~/.dsh-computers/agent.mjs service restart
node ~/.dsh-computers/agent.mjs service uninstall
tail -f ~/.dsh-computers/agent.log

3. 对话里怎么用

智能体会看到这些工具(computer 参数都可省略,省略时用输入框左侧选中的目标电脑):

工具作用
computer_list列出可用电脑(名字、平台、在线、能力、当前固定目标)
computer_exec执行命令,支持 run_in_background;超时默认转后台而不是杀掉
computer_read读远端文件(offset/limit 行)
computer_write写远端文件(会自动建目录)
computer_edit字面替换(old 必须唯一,除非 replaceAll)
computer_glob远端 glob(*、?、**、{a,b})
computer_grep远端正则搜索(支持 include、命中上限)

文件类工具需要那台电脑的 agent 上报 fs 能力(见下)。

系统提示里会自动附上当前电脑清单,所以直接说「看看两台 mac 的内存」它就会分别调用。

3.1 能力、流式与后台任务

  • 能力协商:agent 在握手时上报 capabilities(exec / fs,以后还有 pty)和 workspaceBase(默认工作目录)。服务器据此决定是否提供文件类工具——旧 agent 不会因为版本落后而被拒绝,只是没有新能力;设置页会用 可升级 徽标提示(版本落后不影响 computer_exec)。只有在配置里显式写 minAgentVersion 才会强制拦截旧 agent。
  • 流式:长命令的分块输出会随帧回传(含心跳),后台任务的输出可在 DSH 的任务里查看。
  • 超时转后台:前台命令超时默认不杀(promoteOnTimeout: true),而是转入后台任务并返回 jobId,之后用内置的 job_output / job_kill 接管。
  • 激活时机:新电脑要等 agent 首次建流后才出现在列表(从生成密钥到真正连上之间,弹窗会一直显示"等待电脑连接…")。

2.6 一键中继 + 单列表界面(v1.9.2)

中继一键启动:设置 → 电脑 → 高级 → 中继 → 「启动中继」。插件自己拉起来(代码就在插件包里), 所以升级插件就是升级中继,不用 git / systemd / root。启动后地址自动填好,点「启用」即可。

界面只剩一个列表:账号。「接入地址 / 复制接入地址 / 已连接的电脑」全部从界面移除, 加机器只有一条路:复制账号密钥 → 在新机器粘上登录。

本机不再默认连:自运行时第一次用到「本地电脑」时才启动;self: true 的本机记录不会出现在远端列表里。

斜杠菜单只显示当前选中机器的技能:选中哪台就看哪台的技能。

2.7 一把钥匙 + 任意机器执行(v1.9.1)

登录只要一把钥匙(随时可在 高级 → 账号中心 复制):

账号密钥  dshk_6d56dbd4988a3fe3_887d4f25dd9318bce58b4a47f46c664c

本地电脑真能执行了:插件在本机起一个独立的自运行时,「本地电脑」不再是占位。

跨机执行:输入框选择器里选「账号里的电脑」= 在那台机器上执行, 走 <entryUrl>/api/dsh-computers/fleet/exec,用账号密钥鉴权,不需要逐台配对。

被控开关:设置 → 接入 → 账号里的电脑,本机那行有开关。 关掉后别人能看到你、但不能在你机器上执行(403 not-controllable)。

2.8 远端技能进斜杠菜单(v1.9.0)

每台电脑把自己的技能报给主控,斜杠菜单里按机器区分:

/                      ← 输入 / 打开
  /image-compress      ← 服务器本机
  /air-image-compress  ← Air 上的技能
  /mini-video-cut      ← mini 上的技能
  • 用的是 DSH 官方扩展点 ctx.skills.registerProvider,和本机技能并列
  • 选中后主控去那台机器读 SKILL.md,执行落在那台机器上
  • 机器离线时它的技能从菜单消失
  • 另有 computer_skills 工具显式列出某台机器有什么技能

2.9 账号中心(v1.8.0)

同一个账号下的机器自动互相可见,不需要逐台配对:

中继机器:设置 → 电脑 → 高级 → 账号中心 → 创建账号中心
         → 得到 账号 ID + 密码(只显示这一次)

其他机器:设置 → 电脑 → 接入 → 登录账号 → 填 账号 ID + 密码
         → 自动注册,出现在同一账号的设备列表里
  • 账号中心跑在中继上(relay/main.mjs,同一进程,无需额外服务)
  • 密钥在服务端只存 sha256;插件的 /account 与 /state 永不返回它
  • 中继地址不用手写:/account/discover 会探测本机的中继并预填
  • 旧的一次性密钥 / 接入地址方式全部保留

3.0 接入:填一个地址(v1.7.0)

主控那台 设置 → 电脑 → 复制接入地址,得到一串:

https://dsh.example.com#join=dshf_…

在另一台机器 设置 → 电脑 → 接入地址 里粘贴 → 连接,完成。不需要生成密钥、不需要记 15 分钟有效期。

  • # 之后是 fragment,代理和浏览器都不会发给服务器,token 不进日志
  • 地址是长期凭据,可随时「重新生成」(旧地址立刻失效,已接入的机器不受影响)
  • 一次性密钥仍保留(「用加入密钥」里),适合"用完即废"
  • 详细说明见 docs/onboarding.md

3.1 能力位与执行终态

  • 能力位(caps):agent 在握手里上报 caps: { exec, fs, jobs, pty, tunnel },服务器据此决定提供哪些工具,不再用版本号推断功能。minAgentVersion 只剩"运维强制升级"这一个作用;版本落后只在 UI 显示 可升级。
  • 能力标签:设置页按 caps 显示 执行 / 文件 / 后台任务 / 终端,每项带一句人话说明;缺哪项能力时,工具报错会直接点名缺什么、怎么补。
  • 执行终态:每次执行都以 { status, disposition, confirmed, exitCode, signal, error } 收尾—— status: completed | failed | cancelled | timeout,disposition: reusable | broken。 取消不再一律报"已取消":agent 确认停止 → cancelled / confirmed:true;只发出取消但没等到确认 → cancelled / confirmed:false / broken,并明确提示"命令可能还在那台机器上运行";流中断 → failed / broken。
  • 协议与常量:所有 wire 形状、错误码、能力与状态词表集中在 lib/contracts.js(零依赖、零状态),协议文档见 PROTOCOL.md §5.2 / §11。

3.1.1 权限、在线判定与路由健康(v1.5.0)

  • 一张权限表:所有操作(exec / 后台 exec / cancel / 文件五件套 / fs 请求 / list)都在 lib/policy.js 的冻结表里,加载时自检"没有操作漏登记";准入由唯一的 authorize() 决定,拒绝时给出码 + 被哪条规则挡住 + 怎么改(capability-missing / session-denied / preset-denied / offline / version-below-minimum …)。
  • 在线 = 有 lease:只有已校验的入站帧(hello / 心跳 / chunk / result / fs result)才刷新在场,45 秒租约 + 10 秒扫描。OS 睡眠、代理假死、半开连接都会正确掉线,而不是一直显示在线。
  • 路由健康:每个地址候选带 health { state, checkedAt, failures, cooldownUntil }。后台探活 GET /api/dsh-computers/health(1.5 秒预算,不阻塞页面),失败即冷却 60 秒并让位给下一个可用地址——坏掉的隧道不会被复制进别人的连接命令。
  • relay 传输层(预览,未接线):relay/main.mjs 独立零依赖中继 + lib/relay/{wire,mux,hub-link,bridge}.js 已落地并有 47 项测试(S0/S1)。尚未接入插件:hub 目前还不能经 relay 出站,S2(WebSocket 升级)与接线在后续版本。

3.1.2 中继(v1.6.0)

让主控也只需要出站:任意一台机器(mini / air / 服务器上的 dsh web)都能当主控,不需要公网 IP、不需要开放端口。

relay:
  enabled: true
  url: wss://relay.example.com
  • 自动生成并持久化 hubId 到 <stateDir>/relay.json(0600)——重置电脑列表不会让别人的收藏地址失效
  • 设置 → 电脑 → 本机地址 出现一行 中继:https://relay.example.com/r/hub_xxx,连接成功时是推荐地址
  • 别的电脑把「服务器地址」填成这个 URL + 加入密钥即可接入;手机浏览器直接打开它就是完整 UI(含 WebSocket)
  • 全部在界面里配(v1.7.1):高级 → 中继 → 填地址 → 启用,现场拨号/换址;cordis.patch.yml 可以完全留空
  • 中继断开时该行标「未监听」并回落推荐,不会给你连不上的地址
  • 安全边界:hub 侧 bridge 只打本机 127.0.0.1:<自己的端口>(拒绝绝对 URL、..、强制 Host),中继看不到浏览器登录态
  • 部署(systemd + nginx + 证书)见 docs/relay-deploy.md,协议见 PROTOCOL.md §12

3.2 地址、可达性与 Tailscale

  • 地址候选:设置 → 电脑 顶部「本机地址」会列出本机所有候选地址并标注是否可达—— 公网(publicUrl)→ 转发地址 → Tailscale → 局域网 → 本机名 → 本机。 只有真正在监听的地址才会被推荐(例如桌面端只监听 127.0.0.1 时,Tailscale 那行会标「未监听」,推荐回落到 loopback),避免把连不上的地址复制给别人。

  • + 连接电脑 里的服务器地址是可下拉选择的,默认选中推荐地址,切换后命令/地址随之变化。

  • 自动 Tailscale Serve(可选):

    - id: dsh-computers
      config:
        tailscaleServe:
          enabled: true
          httpsPort: 8443
    

    插件加载后自动执行 tailscale serve --bg --https=8443 http://127.0.0.1:<当前端口>,把本机 DSH 暴露到 tailnet,并读取 tailscale status 的真实 DNSName 作为推荐地址。幂等(已指向本端口就跳过),端口每次随机变也会重新对接,失败只记录 tunnel.error 不推荐假地址。

  • 强制偏好:preferAddress: auto | public | tailscale | lan | loopback(只在可达的候选里选,选不到就回落 auto)。

  • 管理电脑:访问权限弹窗里的 ⚙ 管理电脑 会直接跳到独立的「电脑管理」主面板(侧栏也有入口),不再只是文字提示。

桌面端注意:不要把 webserver.host 改成非 loopback 地址(窗口写死连 127.0.0.1:<port>,会连不上)。桌面端请用上面的 tailscaleServe,独立 dsh web 才用 --host。详见 docs/deploy.md。

4. 端点一览

方法路径说明
POST/api/dsh-computers/agent/enroll加入密钥换取电脑 token(runtime 卡片也走这里)
GET/api/dsh-computers/agent/events被控端的 SSE 长连接(Bearer token)
POST/api/dsh-computers/agent/results命令执行结果回传(最终结果,权威)
POST/api/dsh-computers/agent/results/chunk流式输出分块(stdout/stderr/heartbeat)
POST/api/dsh-computers/agent/fs/result远端文件操作结果
POST/api/dsh-computers/agent/heartbeat心跳
GET/api/dsh-computers/state设置页数据(角色、电脑列表,需要浏览器会话)
POST/api/dsh-computers/keys生成一次性加入密钥
GET/api/dsh-computers/key-status弹窗轮询用
POST/api/dsh-computers/delete / rename删除 / 重命名电脑
GET/api/dsh-computers/runtime本机运行时状态
POST/api/dsh-computers/runtime/connect / enable / disconnect连接 / 启用开关 / 断开本机运行时
GET/api/dsh-computers/join.sh兜底安装脚本
GET/api/dsh-computers/agent.mjsagent 本体(兜底脚本下载)
GET/api/dsh-computers/health健康检查

浏览器端点都先过 DSH 的请求闸门(connection.requestRejection),未登录/不可信来源直接拒绝;被控端端点用 Bearer token 认证(服务端只存 sha256(token))。

5. 开发与验证

node scripts/host-smoke.mjs     # Host 半:真实 HTTP 监听 + 假 agent/hub,覆盖注册/SSE/exec/超时/runtime
node --test agent/test/         # agent 半:enroll / SSE / exec / 超时 / 截断 / 内嵌运行时
node --check client.js          # 浏览器半

协议细节(所有路由、SSE 事件、状态文件、工具 schema、runtime 契约)见 PROTOCOL.md。

6. 已知限制

  • 一次执行返回一次性结果(不流式),长命令靠 timeoutMs;输出单流上限 1 MiB。
  • 一台电脑同一时刻只保留一条连接(新的会顶掉旧的)。
  • 在线判定只看 SSE 是否存活:杀掉进程立刻离线;网络静默(无 FIN)最多差一个心跳周期。
  • Windows 被控端目前可用 node agent.mjs run 手动运行,服务安装只覆盖 macOS/Linux。
  • 电脑与 Bot 的绑定还没有(列表里的 1/1 Bot 是占位徽标);v1 里所有电脑对智能体都可选。
  • 不做 P2P / 电脑互控;要隔离环境请用 VPN,或把某台桌面端当 hub、另一台连过去(仍是单向)。

License

MIT

开发与测试

npm test           # 全部:host smoke + agent + relay
npm run test:host  # host smoke(186 组断言)
npm run test:agent # agent(43 项)
npm run test:relay # relay S0/S1(47 项)

Node 24 把裸目录参数当作模块说明符,所以 relay 测试用显式 glob(脚本里已带引号)。

relay.example.com 是占位示例。中继跑在你自己机器上,域名和端口由你决定(配置文件或界面里填),项目不绑定任何域名。

Ähnliche Plugins