Ckarefulon/dsh-tailscale-remote

dsh-tailscale-remote

DSH plugin: expose the DeepSeek Harness Web GUI on your tailnet so a phone anywhere can connect, and tear the route down on exit.

Uncategorizeddsh-plugins

dsh-tailscale-remote

打开 DSH 就把本机 Web GUI 通过 Tailscale Serve 暴露到你的 tailnet 上,手机在任何网络下 打开一个网址就能用;DSH 退出时把这条路由撤掉。手机链接直接显示在 「设置 → 通用 → 手机连接」里,点一下就能复制。

给 DeepSeek Harness(DSH)用的插件。
只需要「随时随地用手机连上自己电脑上的 DSH」这一件事,不引入任何第三方中转。

先看这个:手机怎么进

最省事的办法:DSH 里打开「设置 → 通用」,往下找到「手机连接」那一行,里面就有链接, 点一下「复制」,发到手机上打开一次即可。 那一行还能随时「更换密钥」——万一链接泄露, 换了之后旧链接和所有旧登录一起失效。

不想开 DSH 界面时,读状态文件也一样(字段是 loginUrl):

# 装好并重启 DSH 后,执行(Windows 用 PowerShell 见下)
cat ~/.dsh/tailscale-remote.json

读里面 `loginUrl` 那个值,形如:

https://<你的机器>.<tailnet>.ts.net:7259/remote/<持久密钥>

在手机浏览器打开它一次(会自动跳转并登录),然后存成书签/主屏幕图标 —— 这条链接跨重启不变,以后点一下就进。

Windows PowerShell 读法:

Get-Content "$env:USERPROFILE\.dsh\tailscale-remote.json"

就这么两步:拿到 `loginUrl` → 手机打开一次并存书签。下面都是细节。

DSH 的 Web GUI 要求一次登录令牌,没令牌只显示一句
dsh web authentication required; ...。DSH Desktop 又从不打印那行链接,令牌还每次重启都换 ——
所以插件给你一条存一次就永久用的入口。

安装

dsh plugin --profile <你的 profile> add dsh-tailscale-remote

装完重启 DSH(新包需要重启才会加载)。之后每次开 DSH 它都会自己跑起来。

也可以从本地目录装:

dsh plugin --profile <profile> add -w /path/to/dsh-tailscale-remote

它做什么

启动时:

  1. 确认 tailscale 真的在跑(**用 tailscale status --json 验证 BackendState,

不是相信 tailscale up 的退出码** —— 见下文)。

  1. 把 https://..ts.net:7259 代理到 DSH 自己的 Web 端口

(端口从 webServer 服务上读,不写死)。

  1. 把 ..ts.net:7259 加进 DSH /api 的 Host 信任列表。
  2. 打印手机可用的网址,并写一份到 $DSH_HOME/tailscale-remote.json。
  3. 精简手机那个页面(默认开,见下文):只发手机真正需要的客户端插件,首屏插件

从约 5.3 MB gzip 降到约 1.3 MB。远程端每次连上来都是精简页,只有按了侧边栏底部 那个切换图标的那一次才发完整页(选择不保留)。桌面端不受影响。

退出时:只回滚本次运行自己做过的事——撤掉 7259 的路由、移除信任项;只有 tailscale 本来就是关着、由 DSH 拉起来的情况下才 tailscale down。

例外:如果 7259 上已经有一条精确指向本实例 DSH 端口的路由(几乎只可能是上次
DSH 崩溃/被强杀留下的),插件会把它认领下来,退出时一并撤掉 —— 否则崩溃恢复
之后「关 DSH 就关掉」会永远失效,路由一直留在那儿。

为什么必须动信任列表

tailscale serve 保留客户端的 Host 头(实测:从 tailnet 地址访问时,后端收到的 host 就是 xxx.ts.net:7259)。而 DSH 的 /api fence 要求 Host 是回环地址或命中 trustedHosts,否则一律 403 —— 表现为手机上页面能打开、但一发消息就报错。 插件在运行时把该 authority 推进 connection.trustedHosts(这是个可变数组,fence 每次 请求都读它),所以不需要改 profile 的 cordis.yml。

手机上怎么用(重点:怎么拿到登录链接)

DSH 的 Web GUI 默认要求一次登录令牌才让你进去,没有令牌就只显示一句话:

dsh web authentication required; reopen the URL printed by dsh web.

麻烦的地方在于:DSH Desktop 从不打印这行链接(它走 IPC,不走 dsh web),而令牌又是 进程级的、每次重启都换。所以本插件给你三个拿链接的地方,任选其一。

① 设置页里的「手机连接」行(推荐)

设置 → 通用,滚到下面有「手机连接」这一行:

  • 里面是手机登录链接,一个只读输入框 + 「复制」按钮,点一下就能发到手机上。
  • 旁边有「更换密钥」:点一下会先让你确认,确认后旧密钥与所有旧登录一起失效。
  • 换的到底是什么:登录链接(/remote/)只负责换取一张会话 cookie。

密钥泄露后,如果对方已经用它登录过,他手里那张 cookie 是另一把签名密钥签的、 有效期 10 年 —— 只换登录密钥完全不影响它。所以这里的「更换密钥」会同时换掉 登录密钥和会话签名密钥,让泄露的密钥真正失效。

  • 代价:所有设备(包括你自己的手机)都会被登出,需要用新链接重新登录一次。

点按钮的这台设备会被自动补发一张新 cookie、不掉线,所以在电脑上操作最顺手。

  • 想只换链接、不踢设备?设 revokeSessionsOnRotate: false(此时泄露的旧 cookie

仍然有效,只适合「我确定没人用过那条链接」的情况)。

  • 还没就绪时会直接告诉你卡在哪一步(比如「Tailscale 没在运行」),而不是给你一个空框。
这一行只在你打开设置页时才请求一次状态(外加窗口重新获得焦点时刷新一次),
不轮询、不挂定时器。它注册在 DSH 自己的 /api 上,所以只有已登录的浏览器能读。

② 稳定登录入口(存一次书签就够)

插件会在 DSH 自己的 webserver 上注册一条路由:

https://<你的机器>.<tailnet>.ts.net:7259/remote/<持久密钥>

打开它就会 302 跳转到带令牌的地址、种下 cookie,然后你就进 GUI 了。

这条链接的密钥存在状态文件里,跨重启不变 —— 所以手机上存一次书签,以后点一下就进。

密钥本身就是访问门槛:不知道它的人拿不到会话(和 DSH 自己的令牌同一级别)。
不需要的话可以设 loginRoute: false 关掉。

③ 状态文件(永远可以查到)

任何一次启动后,下面这个文件里都有当前可用的链接:

# 三个字段:loginUrl(稳定入口)、url(一次性令牌链接)、plainUrl(干净地址)
cat ~/.dsh/tailscale-remote.json

Windows PowerShell:

Get-Content "$env:USERPROFILE\.dsh\tailscale-remote.json"

拿 loginUrl 那个值,在手机上打开即可。它同时也会打到 DSH 的日志里。

登录之后

cookie 种下后,直接访问干净地址 https://..ts.net:7259/ 就能进, 可以「添加到主屏幕」当 App 用。

cookie 有效期默认 10 年(见下面的 cookieMaxAgeDays)—— DSH 原版是 30 天,对 「手机随时连」太短,插件启动时会把 connection 上那个活的 maxAgeMilliseconds 直接改长。所以正常情况下你只需要登录一次。

手机上好不好用:实测可用。DSH 0.2.0-rc.2 的 Web 前端确实没有专门的移动端外壳
(JS 无 matchMedia,CSS 里只有 prefers-reduced-motion、没有宽度断点),但它的三栏是
弹性布局,在手机宽度下会自然收窄,日常查看会话、发消息、看结果都没问题,不会挤成一条。
想要更像 App 的体验(抽屉侧栏、底部导航)需要另外写客户端插件。

前置条件

  • 本机已安装并登录 Tailscale。
  • 该 tailnet 已启用 HTTPS 证书(Tailscale 管理台 → DNS → HTTPS Certificates)。

没启用时 tailscale serve 会失败,日志里会提示。

配置

字段 · 默认 · 说明 enabled · true · 关掉则插件完全不动作。 httpsPort · 7259 · tailnet 上的 HTTPS 端口。 connectOnStart · true · tailscale 没在跑时是否把它拉起来。 serveOnStart · true · 是否建立 Serve 路由。 teardownOnExit · true · 退出时是否回滚。 trustedHosts · true · 是否把该 authority 加进 /api 信任列表。 loginRoute · true · 是否注册 /remote/ 这个稳定登录入口(设置页那一行的「更换密钥」也依赖它)。 revokeSessionsOnRotate · true · 换密钥时是否同时吊销所有已登录的会话。默认开:密钥泄露时对方可能已用旧密钥换到一张 10 年 cookie,只换登录密钥拦不住他。关掉则只换链接、所有设备继续登录。 cookieMaxAgeDays · 3650 · 登录 cookie 有效期(天)。0 = 不插手 DSH 的 30 天默认值。 binaryPath · 自动探测 · tailscale CLI 可执行文件路径。 guiPath · 自动探测 · Tailscale 桌面客户端路径(Windows 默认 tailscale-ipn.exe)。 readyTimeoutMs · 8000 · 等 tailscale up 生效的预算。 guiReadyTimeoutMs · 25000 · 启动桌面客户端后等 backend 就绪的预算。 stateFile · $DSH_HOME/tailscale-remote.json · 状态文件位置。 lite · true · 精简模式(默认开):手机那个页面只带基础插件(首屏插件从约 5.3 MB gzip 降到约 1.1 MB)。桌面端不受影响。这是硬开关:false = 这台实例永远不裁页面,那个切换按钮也不出现。开着的时候,远程端每次连上来都是精简页,只有按了按钮的那一次才发完整页(选中不保留,见下文)。 liteSeeds · 内置白名单 · 精简模式保留哪些插件(闭包种子)。只在 DSH 升级改包名、你想自己补几个包时才需要。 liteHide · 内置列表 · 精简页面上要藏掉的界面锚点(CSS 选择器)。默认藏掉右侧栏与轨迹页签。设置入口永远不藏(手机上进设置页只有那一个地方)。 websocketCompression · true · 给 DSH 那条 WebSocket(/api/remote.mux,会话历史走它)开 permessage-deflate。惰性挂钩:启动期零开销,第一次握手时才 patch。

精简模式(手机流量省 79%)

DSH 的 Web 首屏会把插件图里每一个客户端包 preload 下来,本机实测约 5.3 MB(gzip)。 其中大半手机根本用不到:设置里的账号插画单个包就 2.4 MB,还有插件管理器、模型设置、 各种第三方面板。手机走 Tailscale 时带宽窄,这些字节是纯粹的负担。

插件的做法是不动 DSH 一行代码,只在渲染出来的 index.html 上做手术:

  1. 用一份白名单在活的插件图上求闭包,得到精简图(实测 30/86 个包);
  2. 把 globalThis["__DSH_BOOT__"] 换成精简图 —— 客户端于是只认这些包;
  3. 把整图那些首屏 preload 换成精简版,由插件自己的路由服务,并用 brotli 压