dsh-tailscale-remote 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +264 -2
- package/client/client.js +457 -0
- package/cordis.patch.yml +9 -0
- package/index.js +1003 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +61 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ckarefulon
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,265 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-tailscale-remote
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
打开 DSH 就把本机 Web GUI 通过 **Tailscale Serve** 暴露到你的 tailnet 上,手机在任何网络下
|
|
4
|
+
打开一个网址就能用;DSH 退出时把这条路由撤掉。手机链接直接显示在
|
|
5
|
+
**「设置 → 通用 → 手机连接」**里,点一下就能复制。
|
|
6
|
+
|
|
7
|
+
> 给 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(DSH)用的插件。
|
|
8
|
+
> 只需要「随时随地用手机连上自己电脑上的 DSH」这一件事,不引入任何第三方中转。
|
|
9
|
+
|
|
10
|
+
## 先看这个:手机怎么进
|
|
11
|
+
|
|
12
|
+
**最省事的办法:DSH 里打开「设置 → 通用」,往下找到「手机连接」那一行,里面就有链接,
|
|
13
|
+
点一下「复制」,发到手机上打开一次即可。** 那一行还能随时「更换密钥」。
|
|
14
|
+
|
|
15
|
+
不想开 DSH 界面时,读状态文件也一样(字段是 `loginUrl`):
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
# 装好并重启 DSH 后,执行(Windows 用 PowerShell 见下)
|
|
19
|
+
cat ~/.dsh/tailscale-remote.json
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
读里面 **`loginUrl`** 那个值,形如:
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
https://<你的机器>.<tailnet>.ts.net:7259/remote/<持久密钥>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**在手机浏览器打开它一次**(会自动跳转并登录),然后**存成书签/主屏幕图标** ——
|
|
29
|
+
这条链接跨重启不变,以后点一下就进。
|
|
30
|
+
|
|
31
|
+
Windows PowerShell 读法:
|
|
32
|
+
|
|
33
|
+
```powershell
|
|
34
|
+
Get-Content "$env:USERPROFILE\.dsh\tailscale-remote.json"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
就这么两步:**拿到 `loginUrl` → 手机打开一次并存书签**。下面都是细节。
|
|
38
|
+
|
|
39
|
+
> DSH 的 Web GUI 要求一次**登录令牌**,没令牌只显示一句
|
|
40
|
+
> `dsh web authentication required; ...`。DSH Desktop 又**从不打印**那行链接,令牌还每次重启都换 ——
|
|
41
|
+
> 所以插件给你一条**存一次就永久用**的入口。
|
|
42
|
+
|
|
43
|
+
## 安装
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
dsh plugin --profile <你的 profile> add dsh-tailscale-remote
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
装完**重启 DSH**(新包需要重启才会加载)。之后每次开 DSH 它都会自己跑起来。
|
|
50
|
+
|
|
51
|
+
也可以从本地目录装:
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
dsh plugin --profile <profile> add -w /path/to/dsh-tailscale-remote
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## 它做什么
|
|
58
|
+
|
|
59
|
+
启动时:
|
|
60
|
+
|
|
61
|
+
1. 确认 `tailscale` 真的在跑(**用 `tailscale status --json` 验证 `BackendState`,
|
|
62
|
+
不是相信 `tailscale up` 的退出码** —— 见下文)。
|
|
63
|
+
2. 把 `https://<你的机器>.<tailnet>.ts.net:7259` 代理到 DSH 自己的 Web 端口
|
|
64
|
+
(端口从 `webServer` 服务上读,不写死)。
|
|
65
|
+
3. 把 `<你的机器>.<tailnet>.ts.net:7259` 加进 DSH `/api` 的 Host 信任列表。
|
|
66
|
+
4. 打印手机可用的网址,并写一份到 `$DSH_HOME/tailscale-remote.json`。
|
|
67
|
+
|
|
68
|
+
退出时:只回滚**本次运行自己做过的事**——撤掉 7259 的路由、移除信任项;只有 tailscale
|
|
69
|
+
本来就是关着、由 DSH 拉起来的情况下才 `tailscale down`。
|
|
70
|
+
|
|
71
|
+
> 例外:如果 7259 上已经有一条**精确指向本实例 DSH 端口**的路由(几乎只可能是上次
|
|
72
|
+
> DSH 崩溃/被强杀留下的),插件会把它**认领**下来,退出时一并撤掉 —— 否则崩溃恢复
|
|
73
|
+
> 之后「关 DSH 就关掉」会永远失效,路由一直留在那儿。
|
|
74
|
+
|
|
75
|
+
## 为什么必须动信任列表
|
|
76
|
+
|
|
77
|
+
`tailscale serve` **保留客户端的 Host 头**(实测:从 tailnet 地址访问时,后端收到的
|
|
78
|
+
`host` 就是 `xxx.ts.net:7259`)。而 DSH 的 `/api` fence 要求 Host 是回环地址或命中
|
|
79
|
+
`trustedHosts`,否则一律 403 —— 表现为**手机上页面能打开、但一发消息就报错**。
|
|
80
|
+
插件在运行时把该 authority 推进 `connection.trustedHosts`(这是个可变数组,fence 每次
|
|
81
|
+
请求都读它),所以不需要改 profile 的 `cordis.yml`。
|
|
82
|
+
|
|
83
|
+
## 手机上怎么用(重点:怎么拿到登录链接)
|
|
84
|
+
|
|
85
|
+
DSH 的 Web GUI 默认要求一次**登录令牌**才让你进去,没有令牌就只显示一句话:
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
dsh web authentication required; reopen the URL printed by dsh web.
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
麻烦的地方在于:**DSH Desktop 从不打印这行链接**(它走 IPC,不走 `dsh web`),而令牌又是
|
|
92
|
+
**进程级**的、每次重启都换。所以本插件给你三个拿链接的地方,任选其一。
|
|
93
|
+
|
|
94
|
+
### ① 设置页里的「手机连接」行(推荐)
|
|
95
|
+
|
|
96
|
+
**设置 → 通用**,滚到下面有「手机连接」这一行:
|
|
97
|
+
|
|
98
|
+
- 里面是**手机登录链接**,一个只读输入框 + 「复制」按钮,点一下就能发到手机上。
|
|
99
|
+
- 旁边有「更换密钥」:点一下会先让你确认,确认后立刻换一把新密钥并**让旧链接失效**。
|
|
100
|
+
- 已经登录过的手机**不受影响**(DSH 的会话 cookie 由另一个签名密钥签发,和登录密钥无关)。
|
|
101
|
+
- 换完之后把新链接重新存一次书签即可。
|
|
102
|
+
- 还没就绪时会直接告诉你卡在哪一步(比如「Tailscale 没在运行」),而不是给你一个空框。
|
|
103
|
+
|
|
104
|
+
> 这一行只在你**打开设置页**时才请求一次状态(外加窗口重新获得焦点时刷新一次),
|
|
105
|
+
> 不轮询、不挂定时器。它注册在 DSH 自己的 `/api` 上,所以**只有已登录的浏览器**能读。
|
|
106
|
+
|
|
107
|
+
### ② 稳定登录入口(存一次书签就够)
|
|
108
|
+
|
|
109
|
+
插件会在 DSH 自己的 webserver 上注册一条路由:
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
https://<你的机器>.<tailnet>.ts.net:7259/remote/<持久密钥>
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
打开它就会 **302 跳转**到带令牌的地址、种下 cookie,然后你就进 GUI 了。
|
|
116
|
+
|
|
117
|
+
这条链接的密钥存在状态文件里,**跨重启不变** —— 所以手机上存一次书签,以后点一下就进。
|
|
118
|
+
|
|
119
|
+
> 密钥本身就是访问门槛:不知道它的人拿不到会话(和 DSH 自己的令牌同一级别)。
|
|
120
|
+
> 不需要的话可以设 `loginRoute: false` 关掉。
|
|
121
|
+
|
|
122
|
+
### ③ 状态文件(永远可以查到)
|
|
123
|
+
|
|
124
|
+
任何一次启动后,下面这个文件里都有**当前**可用的链接:
|
|
125
|
+
|
|
126
|
+
```sh
|
|
127
|
+
# 三个字段:loginUrl(稳定入口)、url(一次性令牌链接)、plainUrl(干净地址)
|
|
128
|
+
cat ~/.dsh/tailscale-remote.json
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Windows PowerShell:
|
|
132
|
+
|
|
133
|
+
```powershell
|
|
134
|
+
Get-Content "$env:USERPROFILE\.dsh\tailscale-remote.json"
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
拿 `loginUrl` 那个值,在手机上打开即可。它同时也会打到 DSH 的日志里。
|
|
138
|
+
|
|
139
|
+
### 登录之后
|
|
140
|
+
|
|
141
|
+
cookie 种下后,直接访问干净地址 `https://<机器>.<tailnet>.ts.net:7259/` 就能进,
|
|
142
|
+
可以「添加到主屏幕」当 App 用。
|
|
143
|
+
|
|
144
|
+
**cookie 有效期默认 10 年**(见下面的 `cookieMaxAgeDays`)—— DSH 原版是 30 天,对
|
|
145
|
+
「手机随时连」太短,插件启动时会把 `connection` 上那个活的 `maxAgeMilliseconds`
|
|
146
|
+
直接改长。所以正常情况下你**只需要登录一次**。
|
|
147
|
+
|
|
148
|
+
> 手机上好不好用:**实测可用**。DSH 0.2.0-rc.2 的 Web 前端确实**没有专门的移动端外壳**
|
|
149
|
+
> (JS 无 `matchMedia`,CSS 里只有 `prefers-reduced-motion`、没有宽度断点),但它的三栏是
|
|
150
|
+
> 弹性布局,在手机宽度下会自然收窄,日常查看会话、发消息、看结果都没问题,不会挤成一条。
|
|
151
|
+
> 想要更像 App 的体验(抽屉侧栏、底部导航)需要另外写客户端插件。
|
|
152
|
+
|
|
153
|
+
## 前置条件
|
|
154
|
+
|
|
155
|
+
- 本机已安装并登录 Tailscale。
|
|
156
|
+
- 该 tailnet 已启用 HTTPS 证书(Tailscale 管理台 → DNS → HTTPS Certificates)。
|
|
157
|
+
没启用时 `tailscale serve` 会失败,日志里会提示。
|
|
158
|
+
|
|
159
|
+
## 配置
|
|
160
|
+
|
|
161
|
+
| 字段 | 默认 | 说明 |
|
|
162
|
+
| --- | --- | --- |
|
|
163
|
+
| `enabled` | `true` | 关掉则插件完全不动作。 |
|
|
164
|
+
| `httpsPort` | `7259` | tailnet 上的 HTTPS 端口。 |
|
|
165
|
+
| `connectOnStart` | `true` | tailscale 没在跑时是否把它拉起来。 |
|
|
166
|
+
| `serveOnStart` | `true` | 是否建立 Serve 路由。 |
|
|
167
|
+
| `teardownOnExit` | `true` | 退出时是否回滚。 |
|
|
168
|
+
| `trustedHosts` | `true` | 是否把该 authority 加进 `/api` 信任列表。 |
|
|
169
|
+
| `loginRoute` | `true` | 是否注册 `/remote/<持久密钥>` 这个稳定登录入口(设置页那一行的「更换密钥」也依赖它)。 |
|
|
170
|
+
| `cookieMaxAgeDays` | `3650` | 登录 cookie 有效期(天)。`0` = 不插手 DSH 的 30 天默认值。 |
|
|
171
|
+
| `binaryPath` | 自动探测 | `tailscale` CLI 可执行文件路径。 |
|
|
172
|
+
| `guiPath` | 自动探测 | Tailscale **桌面客户端**路径(Windows 默认 `tailscale-ipn.exe`)。 |
|
|
173
|
+
| `readyTimeoutMs` | `8000` | 等 `tailscale up` 生效的预算。 |
|
|
174
|
+
| `guiReadyTimeoutMs` | `25000` | 启动桌面客户端后等 backend 就绪的预算。 |
|
|
175
|
+
| `stateFile` | `$DSH_HOME/tailscale-remote.json` | 状态文件位置。 |
|
|
176
|
+
|
|
177
|
+
### 关于「永久不过期」
|
|
178
|
+
|
|
179
|
+
DSH 把 cookie 有效期上限写死在 `connection` 的 `cookieMaxAgeDays` 里,并且校验时要求
|
|
180
|
+
`expiresAt - issuedAt <= maxAgeMilliseconds` —— 所以**自己造一个超长 cookie 是无效的**,
|
|
181
|
+
必须改这个上限。本插件启动时会直接改 `connection.browserAuth.maxAgeMilliseconds`,
|
|
182
|
+
默认给 10 年(`3650` 天),够用且不会溢出 JS 安全整数范围。
|
|
183
|
+
|
|
184
|
+
想更长就把它调大(比如 `36500` = 100 年);`0` 表示完全不动 DSH 的默认 30 天。
|
|
185
|
+
|
|
186
|
+
## 为什么不能只调 `tailscale up`
|
|
187
|
+
|
|
188
|
+
在 Windows 上,如果你从托盘把 Tailscale **完全退出**,tailscaled 服务还在跑,但 backend
|
|
189
|
+
处于 `NoState`(`tailscale status` 报 `Tailscale is starting. Please wait.`)。此时
|
|
190
|
+
**`tailscale up` 会「立刻 exit 0、不输出任何东西、也什么都不做」**(实测),backend 永远
|
|
191
|
+
不会变成 `Running` —— 只看退出码就会误判成功,然后因为读不到 DNS 名而静默放弃。
|
|
192
|
+
|
|
193
|
+
所以插件**每一步都用 `tailscale status --json` 验证 `BackendState`,而不是相信退出码**:
|
|
194
|
+
`tailscale up` 之后 backend 还不是 `Running`,就改为启动**桌面客户端**
|
|
195
|
+
(`tailscale-ipn.exe`),它才会真正初始化 backend(实测启动后立刻 `Running` + `Online`)。
|
|
196
|
+
桌面客户端以 detached 方式启动,不会随 DSH 退出而被带走。
|
|
197
|
+
|
|
198
|
+
如果连客户端都拉不起来(例如需要交互式登录),插件会留下明确提示让你手动打开 Tailscale,
|
|
199
|
+
并且**不会**谎报成功去配一条连不上的路由。
|
|
200
|
+
|
|
201
|
+
## 发布(维护者)
|
|
202
|
+
|
|
203
|
+
包名 `dsh-tailscale-remote` 已发布到 npm。`package.json` 里写了 `publishConfig`
|
|
204
|
+
(`access: public` + `registry: https://registry.npmjs.org/`),所以**直接 `npm publish`
|
|
205
|
+
不带任何参数**就会发到正确的地方——即使本机 `~/.npmrc` 指向的是镜像站。
|
|
206
|
+
|
|
207
|
+
后续版本**自动发布**:推送到 `main` 时 `.github/workflows/publish.yml` 会先跑测试再
|
|
208
|
+
`npm publish`——但**只有 `package.json` 里的 `version` 是 registry 上还没有的版本时才发**。
|
|
209
|
+
所以发版流程就是两步:
|
|
210
|
+
|
|
211
|
+
1. 改 `package.json` 的 `version`。
|
|
212
|
+
2. 推送到 `main`。
|
|
213
|
+
|
|
214
|
+
这次运行要么发出这个版本,要么报告「该版本已在线上」并以成功结束。
|
|
215
|
+
**没改版本号的推送是 no-op,不是失败**——所以每个要发出去的改动都必须自己带一次版本号
|
|
216
|
+
变更;只改文档的提交同样会触发工作流,同样 no-op。
|
|
217
|
+
|
|
218
|
+
认证用 npm **Trusted Publishing(OIDC)**:GitHub 给这个 job 签一个短时 token,npm 拿它换
|
|
219
|
+
一个只对这一个包有效的发布 token。**仓库里没有任何 `NPM_TOKEN` secret。** 工作流刻意
|
|
220
|
+
**不用** `actions/setup-node` 的 `registry-url` 输入——它会往 `~/.npmrc` 写
|
|
221
|
+
`//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}`,而该变量没定义时这个占位符会
|
|
222
|
+
原样留下、被 npm 当作 token 发出去。registry 交给 `publishConfig` 就够了。
|
|
223
|
+
|
|
224
|
+
<details>
|
|
225
|
+
<summary>一次性配置:绑定 Trusted Publisher</summary>
|
|
226
|
+
|
|
227
|
+
Trusted Publisher **只能绑到 registry 上已存在的包**,所以**首次发布是手工做的**
|
|
228
|
+
(在检出目录里跑了一次 `npm publish`),从第二个版本起才轮到工作流接管。在 npmjs.com 上:
|
|
229
|
+
包 → Settings → Trusted Publisher → GitHub Actions,填
|
|
230
|
+
|
|
231
|
+
| 字段 | 值 |
|
|
232
|
+
|---|---|
|
|
233
|
+
| Repository | `Ckarefulon/dsh-tailscale-remote` |
|
|
234
|
+
| Workflow filename | `publish.yml` |
|
|
235
|
+
| Environment | *留空* |
|
|
236
|
+
|
|
237
|
+
文件名必须**逐字一致**——npm 把授权绑在它上面,写错会表现为发布报 `ENEEDAUTH`。
|
|
238
|
+
|
|
239
|
+
</details>
|
|
240
|
+
|
|
241
|
+
## 安全边界
|
|
242
|
+
|
|
243
|
+
这条路由**只对你的 tailnet 可见**(Tailscale Serve 的默认范围,不是 Funnel),也就是说
|
|
244
|
+
只有登录到你这个 tailnet 的设备能连上。DSH 自己的浏览器鉴权(30 天签名 cookie)仍然
|
|
245
|
+
生效,两层是叠加的。
|
|
246
|
+
|
|
247
|
+
设置页那一行背后的两个端点注册在 DSH 自己的 `/api` 前缀路由上
|
|
248
|
+
(`connection.fetch`),因此**直接白拿 DSH 的 Host/Origin fence 与浏览器 cookie 鉴权**:
|
|
249
|
+
未登录的请求在到达插件之前就被 401 挡掉了。它们也刻意**不用**
|
|
250
|
+
`webServer.register({ kind: 'exact', path: '/api/...' })` —— `webserver` 的 `match()`
|
|
251
|
+
先查 exact 表,那样会**抢先命中**并遮掉整条 `/api` 路由,把 fence 和鉴权一起绕过去。
|
|
252
|
+
|
|
253
|
+
| 端点 | 作用 |
|
|
254
|
+
| --- | --- |
|
|
255
|
+
| `GET /api/dsh-tailscale-remote/state` | 当前阶段、手机链接、能否换密钥。不缓存。 |
|
|
256
|
+
| `POST /api/dsh-tailscale-remote/rotate` | 换一把新登录密钥,并让旧链接立刻失效。 |
|
|
257
|
+
|
|
258
|
+
「换密钥」只换**登录入口的密钥**,不动 DSH 的会话 cookie 签名密钥,所以已经登录的手机
|
|
259
|
+
不会被踢下线 —— 这也是为什么它足够安全:它收回的是「谁还能用那条链接登录」,不是
|
|
260
|
+
「谁现在是登录状态」。想彻底踢掉已登录设备,去改 DSH 自己的凭据(那会换掉签名密钥,
|
|
261
|
+
所有 cookie 一起失效)。
|
|
262
|
+
|
|
263
|
+
插件**不会覆盖** 7259 上已有的、指向别处的 Serve 配置:检测到冲突就只记日志、不动手。
|
|
264
|
+
唯一会接管的情况是发现一条指向**另一个回环端口**的陈旧路由(通常是上次 DSH 换了端口
|
|
265
|
+
留下的)。
|