Ckarefulon/dsh-tailscale-remote ↗★ 1
dsh-tailscale-remote
通过Tailscale将Web界面发布至专属虚拟网 适合需要从手机等远程设备安全连接并控制DSH Web界面的用户。
安裝
npx -p @deepseek-ai/dsh dsh plugin --profile web add github:Ckarefulon/dsh-tailscale-remote說明文件
閱讀完整 README ↗配置
| 字段 | 默认 | 说明 |
|---|---|---|
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)。桌面端不受影响。侧边栏底部的图标点一下可以随时改,改了就以那个持久选择为准。 |
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 上做手术:
- 用一份白名单在活的插件图上求闭包,得到精简图(实测 30/86 个包);
- 把
globalThis["__DSH_BOOT__"]换成精简图 —— 客户端于是只认这些包; - 把整图那些首屏 preload 换成精简版,由插件自己的路由服务,并用 brotli 压 (比 webserver 默认的 gzip level 1 再省约 32%);
- 在 head 里补一段 ``,把右侧栏、轨迹页签藏掉,顺手把 「工作区」标题行里搜索按钮前面那段硬编码空白归零。
桌面端一个字节都不会变。 Electron 不走这条路:它直接读磁盘上的 index.html,
再经 IPC 把注入表交给客户端;而 tapIndex 只参与 webServer.renderIndex(),
也就是 HTTP 那条回退路径。所以「不动桌面 DSH」是结构保证,不是小心翼翼。
另外客户端半边在桌面窗口里一个插槽都不注册(判据是 preload 注入的
dshDesktopBoot,只有桌面窗口才有),所以桌面上的 DSH 与没装本插件时完全一致。
实测(真浏览器 + 真宿主):精简页面的插件请求只有两个 —— 一个 bootstrap 小包加一个
合并请求,首屏不再有那条约 5 MB 的 /plugins/??...;页面照常启动、连接正常、
工作区与会话列表、输入框全部可用,控制台零报错。
怎么切换:精简页面上,侧边栏底部「设置」图标旁边多了一个图标按钮 (16×16 的双向箭头,与 DSH 自己的图标同规格)。点一下在「精简 / 完整」之间切换:
- 关掉后刷新手机上的页面,就会发完整版(所有插件)。
- 站点本身立刻记录这个选择,但页面是在下一次渲染 index 时变的 —— 所以按钮的
title/aria-label会写清「刷新后生效」,否则看起来像按钮坏了。 - 完整模式页面里不显示这个图标(那时完整模式页面跟手机上没区别,用不着它)。
- 桌面窗口里根本没有这个图标(连插槽都不注册)。
- 设置图标本身始终保留:
sidebar.settings是 single 插槽、由ui-settings注册在 侧边栏底部,是手机上进设置页的唯一入口,藏了就没法改模型/账号/插件了。
为什么不能简单地「把不要的包从图里删掉就算完」:
exports.inject是服务级 门禁。像ui-sidebar-right提供的sidebarRight被ui-chat硬依赖,删了它ui-chat的 fiber 会停在pending,而 shell 的 boot 审计把pending算失败, 整页白屏。所以这类包留着、只藏界面;真正能删的是那些没人依赖其服务的。
安全阀:如果白名单在活图上失配(DSH 升级拆包/改名),插件原样发完整页面并记一条日志 —— 宁可多发几 MB,也绝不发一个进不去设置页的残缺页面给手机(那里用户没有任何自救手段)。
手机中途连接为什么快了很多
「刷新页面」只是第一段。手机真正难熬的是中途操作:载入历史会话、发命令、收流式回复。 这两件事走的是完全不同的两条路,插件各修了一处:
① 拉会话历史 / 收事件 = 一条 WebSocket,而 DSH 默认完全不压它。
会话历史不走 HTTP,走 /api/remote.mux;DSH 用 new WebSocketServer({ noServer: true })
建它,而 ws 的 perMessageDeflate 默认是 false。实测握手响应里没有
sec-websocket-extensions —— 那些高度重复的 JSON 事件是明文过的(一个界面 12 KB 的会话,
背后收进了 22.5 MB 下行文本)。
插件在 ws.WebSocketServer.prototype.handleUpgrade 上补一层:把 this.options.perMessageDeflate
打开。位置很关键 —— ws 在 handleUpgrade 内部同步用 this.options 构造 socket,
所以在这里改一定生效,没有竞态。打的又是 DSH 自己那一个 ws 模块实例(从 process.resourcesPath
下的 app.asar 解析),所以对已经建好的 mux 也生效,不用重启、不碰 DSH 一行代码。
② 启动开销为零。 require('ws') 要从 asar 里读并编译,实测 159 ms —— 花在启动路径上
就是「插件拖慢了启动」。所以这个 patch 是惰性的:启动期只挂一个一次性钩子
(server.prependListener('upgrade'),只观察不消费事件),等第一次 WS 握手路过时再 patch。
那 159 ms 于是落在手机那条连接上,而不是你的启动路径上(实测装配耗时 0.04 ms)。
③ 首屏合并请求自己压。 webserver 的 gzip 是 level 1(约 1.30 MB),同一份字节用
brotli q5 约 0.89 MB、gzip level 6 约 1.06 MB。插件自己压并缓存结果(只算一次),
按 Accept-Encoding 协商 br → gzip → 原文,并带 Vary: Accept-Encoding。
关于「永久不过期」
DSH 把 cookie 有效期上限写死在 connection 的 cookieMaxAgeDays 里,并且校验时要求
expiresAt - issuedAt <= maxAgeMilliseconds —— 所以自己造一个超长 cookie 是无效的,
必须改这个上限。本插件启动时会直接改 connection.browserAuth.maxAgeMilliseconds,
默认给 10 年(3650 天),够用且不会溢出 JS 安全整数范围。
想更长就把它调大(比如 36500 = 100 年);0 表示完全不动 DSH 的默认 30 天。