dsh-plugin-tether 0.1.14 → 0.1.16
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/CHANGELOG.md +16 -0
- package/README.md +5 -4
- package/README.zh.md +5 -4
- package/index.js +84 -52
- package/package.json +8 -8
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
本文件记录面向用户的变化。每个版本的这一节会原样作为该版本 Release 的说明。
|
|
4
4
|
|
|
5
|
+
## 0.1.16
|
|
6
|
+
|
|
7
|
+
### 新增
|
|
8
|
+
|
|
9
|
+
- **英文界面**([#8](https://github.com/zexadev/dsh-tether/issues/8))。手机 App 的界面、连接与本地模式的报错、常驻通知,以及电脑上插件与 sidecar 打在终端里的输出,现在都有英文。没有单独的语言开关,各处跟随各自那一侧的语言:App 跟手机的系统语言,终端输出跟电脑的系统语言,插件注入在 dsh 界面里的那部分(侧栏按钮、配对串弹窗、已配对手机列表)跟 dsh 自己的语言设置,在 dsh 设置里切换语言即时生效。dsh 界面本身的语言仍由 dsh 的设置决定。
|
|
10
|
+
|
|
11
|
+
## 0.1.15
|
|
12
|
+
|
|
13
|
+
### 新增
|
|
14
|
+
|
|
15
|
+
- **手机本地模式内置的 dsh 升到 `0.1.5-rc.2`**(此前是 `0.1.2-rc.1`)。升级后第一次进入本地模式会重新解压运行时(约 9 秒);手机上已有的会话会在打开时自动迁移到新版日志格式,内容不丢。dsh 本身的变化见它自己的更新说明。
|
|
16
|
+
|
|
17
|
+
### 修复
|
|
18
|
+
|
|
19
|
+
- **电脑上的 dsh 升到 0.1.5 后,手机上的版式右侧一列错位、对话区被挤窄**。上游把布局类名从 `detailsCol` 改成了 `rightbarCol`,插件注入的窄屏样式匹配不到就静默失效。两个名字现在都兼容,新旧 dsh 都正常。
|
|
20
|
+
|
|
5
21
|
## 0.1.14
|
|
6
22
|
|
|
7
23
|
### 修复
|
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
<a href="../../releases"><img src="https://img.shields.io/github/downloads/zexadev/dsh-tether/total?style=flat&label=downloads&color=4D6BFE" alt="Downloads"></a>
|
|
18
18
|
<a href="../../stargazers"><img src="https://img.shields.io/github/stars/zexadev/dsh-tether?style=flat&label=%E2%98%85&color=08C" alt="Stars"></a>
|
|
19
19
|
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-2EA44F?style=flat" alt="MIT"></a>
|
|
20
|
-
<img src="https://img.shields.io/badge/dsh-0.1.0--rc.7%20%7C%20rc.8%20%7C%200.1.2%20%7C%200.1.5
|
|
20
|
+
<img src="https://img.shields.io/badge/dsh-0.1.0--rc.7%20%7C%20rc.8%20%7C%200.1.2%20%7C%200.1.5-4D6BFE?style=flat" alt="dsh 0.1.0-rc.7 | rc.8 | 0.1.2 | 0.1.5">
|
|
21
21
|
<img src="https://img.shields.io/badge/Android-4493F8?style=flat" alt="Android">
|
|
22
22
|
<img src="https://img.shields.io/badge/iOS-beta-8E8E93?style=flat" alt="iOS beta">
|
|
23
23
|
<a href="https://www.dsh.so/artifact/dsh-tether"><img src="https://www.dsh.so/badge/dsh-tether.svg" alt="dsh.so security scan"></a>
|
|
@@ -87,10 +87,10 @@ From then on the app connects by itself when you open it.
|
|
|
87
87
|
|
|
88
88
|
Open the app → **Hosts** in the top bar → **Run on this phone**. The first start unpacks the runtime first (about 9 seconds measured); after that the DSH interface appears in about 2 seconds. Enter an API key in Settings and start talking.
|
|
89
89
|
|
|
90
|
-
- **Install and go**: no Termux, no terminal, no commands, no root. Node 24 and the full dependency tree of dsh `0.1.
|
|
90
|
+
- **Install and go**: no Termux, no terminal, no commands, no root. Node 24 and the full dependency tree of dsh `0.1.5-rc.2` ship inside the APK and unpack offline on first start; after that only model API calls need the network.
|
|
91
91
|
- **No pairing**: dsh is on this phone, there is no remote end. Sessions, settings and workspaces live only on the phone and are entirely separate from what you see when connected to a machine; switching back and forth touches neither.
|
|
92
92
|
- **Stays up**: while running there is a persistent notification (a foreground service), so backgrounding or locking the screen does not get the process reclaimed. If the process is killed anyway, the next launch restarts it; sessions are already on disk.
|
|
93
|
-
- **Versioned with the app**: the bundled dsh upgrades with the app; currently `0.1.
|
|
93
|
+
- **Versioned with the app**: the bundled dsh upgrades with the app; currently `0.1.5-rc.2`.
|
|
94
94
|
- Only the `arm64` APK carries local mode; iOS forbids child processes, so the iOS build has no such entry.
|
|
95
95
|
- The phone has no bash and no full coreutils, so shell-dependent tools are incomplete; chat and file read/write are unaffected.
|
|
96
96
|
|
|
@@ -135,8 +135,9 @@ The Termux route still works if you prefer it: install dsh and this plugin insid
|
|
|
135
135
|
- The iOS build is beta: built only in CI, never run on a real device, and you sign it yourself. Android is the one verified on real hardware.
|
|
136
136
|
- The app connects when you open it and holds no background connection — Android's doze would not let it anyway.
|
|
137
137
|
- The interface on the phone is DSH's own; the narrow-screen fit comes from minimal injected styles, so a dsh layout change may need a follow-up here.
|
|
138
|
+
- English and Chinese: the app follows your phone's system language, terminal output follows the computer's, and the part injected into the dsh interface follows dsh's own language setting. There is no separate language switch.
|
|
138
139
|
- Local mode exists only in the `arm64` build, never on iOS; the phone lacks bash and a full coreutils, so shell-dependent tools are incomplete. The `arm64` APK is about 71 MB because of the bundled runtime; the others are about 32 MB.
|
|
139
|
-
- Verified against dsh **`0.1.0-rc.7`**, **`0.1.0-rc.8`**, **`0.1.2`** (alpha and rc.1) and **`0.1.5-
|
|
140
|
+
- Verified against dsh **`0.1.0-rc.7`**, **`0.1.0-rc.8`**, **`0.1.2`** (alpha and rc.1), **`0.1.5-alpha.1`** and **`0.1.5-rc.2`**. dsh is in developer preview — check this line before assuming a newer dsh works. The browser authentication dsh introduced in 0.1.2-alpha is handled entirely on the machine side; the phone app needs no update for it.
|
|
140
141
|
|
|
141
142
|
## What has been verified
|
|
142
143
|
|
package/README.zh.md
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
<a href="../../releases"><img src="https://img.shields.io/github/downloads/zexadev/dsh-tether/total?style=flat&label=downloads&color=4D6BFE" alt="下载量"></a>
|
|
18
18
|
<a href="../../stargazers"><img src="https://img.shields.io/github/stars/zexadev/dsh-tether?style=flat&label=%E2%98%85&color=08C" alt="Stars"></a>
|
|
19
19
|
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-2EA44F?style=flat" alt="MIT"></a>
|
|
20
|
-
<img src="https://img.shields.io/badge/dsh-0.1.0--rc.7%20%7C%20rc.8%20%7C%200.1.2%20%7C%200.1.5
|
|
20
|
+
<img src="https://img.shields.io/badge/dsh-0.1.0--rc.7%20%7C%20rc.8%20%7C%200.1.2%20%7C%200.1.5-4D6BFE?style=flat" alt="dsh 0.1.0-rc.7 | rc.8 | 0.1.2 | 0.1.5">
|
|
21
21
|
<img src="https://img.shields.io/badge/Android-4493F8?style=flat" alt="Android">
|
|
22
22
|
<img src="https://img.shields.io/badge/iOS-%E6%B5%8B%E8%AF%95%E7%89%88-8E8E93?style=flat" alt="iOS 测试版">
|
|
23
23
|
<a href="https://www.dsh.so/artifact/dsh-tether"><img src="https://www.dsh.so/badge/dsh-tether.svg" alt="dsh.so 安全扫描"></a>
|
|
@@ -87,10 +87,10 @@ dsh plugin --profile web add .
|
|
|
87
87
|
|
|
88
88
|
打开 App → 顶栏「主机」→ **在本机运行**。第一次要先解压运行时(实测约 9 秒),之后每次约 2 秒出现 dsh 的界面;在设置里填上 API Key 就能对话。
|
|
89
89
|
|
|
90
|
-
- **装完即用**:不装 Termux、不开终端、不敲命令、不要 root。Node 24 与 dsh `0.1.
|
|
90
|
+
- **装完即用**:不装 Termux、不开终端、不敲命令、不要 root。Node 24 与 dsh `0.1.5-rc.2` 的完整依赖树打在 APK 里,首次启动离线解压;之后只有调用模型 API 才需要网络。
|
|
91
91
|
- **没有配对**:dsh 就在这台手机上,没有远端。会话、设置、工作区只存在手机里,和连电脑时看到的完全是两套,来回切换互不影响。
|
|
92
92
|
- **常驻**:运行期间有一条常驻通知(前台服务),退后台、锁屏不会被系统收掉;进程真被杀了,下次打开 App 自动重起,会话已落盘不丢。
|
|
93
|
-
- **版本随 App 走**:内置的 dsh 随 App 升级一起升,当前内置 `0.1.
|
|
93
|
+
- **版本随 App 走**:内置的 dsh 随 App 升级一起升,当前内置 `0.1.5-rc.2`。
|
|
94
94
|
- 只有 `arm64` 的 APK 带本地模式;iOS 的沙箱不允许子进程,iOS 版没有这个入口。
|
|
95
95
|
- 手机上没有 bash 和完整的 coreutils,依赖 shell 的工具用不全;对话、文件读写不受影响。
|
|
96
96
|
|
|
@@ -135,8 +135,9 @@ dsh plugin --profile web add .
|
|
|
135
135
|
- iOS 版是测试版:只在 CI 上构建,从未在真机运行过,且需要自行签名。安卓版是经过真机验证的那一个。
|
|
136
136
|
- App 打开时才连接,不在后台常驻——Android 的 doze 也留不住它。
|
|
137
137
|
- 手机上的界面是 DSH 自己的,窄屏适配靠注入的最小样式完成;dsh 改版式时可能需要跟进。
|
|
138
|
+
- 中英双语:App 界面跟随手机的系统语言;电脑终端上的输出跟随电脑的系统语言;注入 dsh 界面的那部分跟随 dsh 自己的语言设置。没有单独的语言开关。
|
|
138
139
|
- 本地模式只有 `arm64` 包有,iOS 没有;手机上没有 bash 与完整 coreutils,依赖 shell 的工具用不全。arm64 包因内置运行时约 71 MB,其余包约 32 MB。
|
|
139
|
-
- 已针对 dsh **`0.1.0-rc.7`**、**`0.1.0-rc.8`**、**`0.1.2`**(alpha 与 rc.1)与 **`0.1.5-
|
|
140
|
+
- 已针对 dsh **`0.1.0-rc.7`**、**`0.1.0-rc.8`**、**`0.1.2`**(alpha 与 rc.1)、**`0.1.5-alpha.1`** 与 **`0.1.5-rc.2`** 验证。dsh 处于 developer preview,换更新的 dsh 之前先看这一行。dsh 0.1.2-alpha 引入的浏览器认证完全在电脑侧处理,手机 App 无需更新。
|
|
140
141
|
|
|
141
142
|
## 验证过什么
|
|
142
143
|
|
package/index.js
CHANGED
|
@@ -17,6 +17,21 @@ import { dirname, join } from 'node:path'
|
|
|
17
17
|
const name = 'dsh-plugin-tether'
|
|
18
18
|
const inject = ['webServer', 'settings']
|
|
19
19
|
|
|
20
|
+
// 文案三处出处各不相同,各自跟随自己那一侧的语言:
|
|
21
|
+
// 终端输出 → 电脑的系统语言
|
|
22
|
+
// 路由的报错 → 请求方声明的语言(手机侧界面会带 x-dsh-tether-lang,其余按 Accept-Language)
|
|
23
|
+
// 注入 dsh 界面的那部分 → dsh 自己的语言设置,它写在 <html lang> 上
|
|
24
|
+
const terminalZh = /^zh/i.test(
|
|
25
|
+
process.env.LC_ALL || process.env.LANG || Intl.DateTimeFormat().resolvedOptions().locale || '',
|
|
26
|
+
)
|
|
27
|
+
/** 终端文案 */
|
|
28
|
+
const tt = (zh, en) => (terminalZh ? zh : en)
|
|
29
|
+
/** 路由文案 */
|
|
30
|
+
const tr = (req, zh, en) => {
|
|
31
|
+
const declared = req.headers['x-dsh-tether-lang'] || req.headers['accept-language'] || ''
|
|
32
|
+
return /(^|[\s,])zh/i.test(declared) ? zh : en
|
|
33
|
+
}
|
|
34
|
+
|
|
20
35
|
/** 手机侧只读查看配置文件的路由 */
|
|
21
36
|
const CONFIG_DOCUMENT_PATH = '/dsh-tether/config-document'
|
|
22
37
|
/** 按需开一个配对窗口,拿回配对串 */
|
|
@@ -101,7 +116,7 @@ function applyUi(ctx) {
|
|
|
101
116
|
const path = await ctx.settings.prepareDocument()
|
|
102
117
|
if (path === undefined) {
|
|
103
118
|
res.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' })
|
|
104
|
-
res.end('当前的设置存储不是本地文件,没有可查看的配置文件')
|
|
119
|
+
res.end(tr(req, '当前的设置存储不是本地文件,没有可查看的配置文件', 'The settings store is not a local file, so there is no config file to show'))
|
|
105
120
|
return
|
|
106
121
|
}
|
|
107
122
|
let text
|
|
@@ -109,7 +124,7 @@ function applyUi(ctx) {
|
|
|
109
124
|
text = await readFile(path, 'utf8')
|
|
110
125
|
} catch (error) {
|
|
111
126
|
res.writeHead(500, { 'content-type': 'text/plain; charset=utf-8' })
|
|
112
|
-
res.end(
|
|
127
|
+
res.end(`${tr(req, '读不到配置文件 ', 'cannot read the config file ')}${path}: ${String(error)}`)
|
|
113
128
|
return
|
|
114
129
|
}
|
|
115
130
|
res.writeHead(200, {
|
|
@@ -136,16 +151,19 @@ function apply(ctx, config = {}) {
|
|
|
136
151
|
const binary = config.hostBinary ?? resolveHostBinary()
|
|
137
152
|
if (binary === undefined || !existsSync(binary)) {
|
|
138
153
|
throw new Error(
|
|
139
|
-
`[tether]
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
154
|
+
`[tether] ${tt(
|
|
155
|
+
`找不到 sidecar 二进制(平台 ${process.platform}-${process.arch})。从源码用:先跑 cargo build --release -p tether-host;`
|
|
156
|
+
+ '或在插件 config.hostBinary 里指定路径。若你是从 npm/GitHub 安装的,说明该平台的 dsh-tether-host-* 子包还没发布,请提 issue。',
|
|
157
|
+
`sidecar binary not found for ${process.platform}-${process.arch}. From source: run cargo build --release -p tether-host, `
|
|
158
|
+
+ 'or point config.hostBinary at it. If you installed from npm/GitHub, the dsh-tether-host-* package for this platform is not published yet — please open an issue.',
|
|
159
|
+
)}`,
|
|
143
160
|
)
|
|
144
161
|
}
|
|
145
162
|
// webServer.port 是真实监听端口(config 写 0 时为 OS 分配值),手机由此拿到完整界面
|
|
146
163
|
const proxyTarget = `${ctx.webServer.host}:${ctx.webServer.port}`
|
|
147
164
|
|
|
148
|
-
|
|
165
|
+
// sidecar 的人读日志与这里打在同一个终端上,语言跟着走,免得一半中文一半英文
|
|
166
|
+
const args = ['host', '--proxy-target', proxyTarget, '--lang', terminalZh ? 'zh' : 'en']
|
|
149
167
|
if (config.pair) args.push('--pair')
|
|
150
168
|
const child = spawn(binary, args, { stdio: ['pipe', 'pipe', 'inherit'] })
|
|
151
169
|
ctx.effect(() => () => { child.kill() })
|
|
@@ -153,16 +171,16 @@ function apply(ctx, config = {}) {
|
|
|
153
171
|
let sidecarGone
|
|
154
172
|
child.on('exit', (code) => {
|
|
155
173
|
// sidecar 死了必须可见:手机端会静默失联,而电脑侧一切照常
|
|
156
|
-
sidecarGone = `sidecar 已退出(code=${code})`
|
|
157
|
-
console.error(`[tether] sidecar 退出 code=${code}
|
|
174
|
+
sidecarGone = tt(`sidecar 已退出(code=${code})`, `the sidecar exited (code=${code})`)
|
|
175
|
+
console.error(`[tether] ${tt(`sidecar 退出 code=${code};手机端已失联`, `the sidecar exited with code=${code}; the phone is now cut off`)}`)
|
|
158
176
|
})
|
|
159
177
|
// spawn 失败只走 error 不走 exit;不接住就是 uncaught。EACCES 曾实发过:
|
|
160
178
|
// 打包丢了二进制执行位,现场只有一句干瘪的 Permission denied,毫无指向
|
|
161
179
|
child.on('error', (error) => {
|
|
162
180
|
const hint = error.code === 'EACCES'
|
|
163
|
-
? ';EACCES 通常是二进制缺可执行位,试 chmod 0755 该文件并升级插件'
|
|
181
|
+
? tt(';EACCES 通常是二进制缺可执行位,试 chmod 0755 该文件并升级插件', '; EACCES usually means the binary lost its execute bit — try chmod 0755 on it and update the plugin')
|
|
164
182
|
: ''
|
|
165
|
-
sidecarGone =
|
|
183
|
+
sidecarGone = `${tt('sidecar 启动失败: ', 'the sidecar failed to start: ')}${error.message}${hint}`
|
|
166
184
|
console.error(`[tether] ${sidecarGone}(${binary})`)
|
|
167
185
|
})
|
|
168
186
|
|
|
@@ -178,43 +196,43 @@ function apply(ctx, config = {}) {
|
|
|
178
196
|
try {
|
|
179
197
|
msg = JSON.parse(line)
|
|
180
198
|
} catch {
|
|
181
|
-
console.error(`[tether] 无法解析 sidecar 消息: ${line}`)
|
|
199
|
+
console.error(`[tether] ${tt('无法解析 sidecar 消息: ', 'cannot parse the sidecar message: ')}${line}`)
|
|
182
200
|
return
|
|
183
201
|
}
|
|
184
202
|
switch (msg.type) {
|
|
185
203
|
case 'ready':
|
|
186
204
|
endpointId = msg['endpoint_id']
|
|
187
|
-
console.log(`[tether] 就绪,本机设备 ID: ${endpointId}`)
|
|
188
|
-
console.log(`[tether]
|
|
205
|
+
console.log(`[tether] ${tt('就绪,本机设备 ID: ', 'ready, device ID of this machine: ')}${endpointId}`)
|
|
206
|
+
console.log(`[tether] ${tt(`手机将看到 http://${proxyTarget} 的完整界面`, `the phone will see the full interface at http://${proxyTarget}`)}`)
|
|
189
207
|
break
|
|
190
208
|
case 'pairing': {
|
|
191
209
|
// 一行可整体粘贴到手机,省去分别输 ID 和码
|
|
192
210
|
const pairingString = `${endpointId}#${msg.code}`
|
|
193
|
-
console.log(`[tether]
|
|
211
|
+
console.log(`[tether] ${tt(`配对串(${Math.round(msg['expires_in_sec'] / 60)} 分钟内有效): `, `pairing string (valid for ${Math.round(msg['expires_in_sec'] / 60)} min): `)}${pairingString}`)
|
|
194
212
|
const waiter = pendingPairing
|
|
195
213
|
pendingPairing = undefined
|
|
196
214
|
waiter?.({ pairingString, expiresInSec: msg['expires_in_sec'] })
|
|
197
215
|
break
|
|
198
216
|
}
|
|
199
217
|
case 'pairing-closed':
|
|
200
|
-
console.log(`[tether] 配对窗口已关闭: ${msg.reason}`)
|
|
218
|
+
console.log(`[tether] ${tt('配对窗口已关闭: ', 'the pairing window closed: ')}${msg.reason}`)
|
|
201
219
|
break
|
|
202
220
|
case 'pairing-done':
|
|
203
|
-
console.log(`[tether] 配对成功: ${msg.name}(${msg.peer.slice(0, 16)}…)`)
|
|
221
|
+
console.log(`[tether] ${tt('配对成功: ', 'paired: ')}${msg.name}(${msg.peer.slice(0, 16)}…)`)
|
|
204
222
|
break
|
|
205
223
|
case 'peer-connected':
|
|
206
|
-
console.log(`[tether] 手机已连接: ${msg.name}`)
|
|
224
|
+
console.log(`[tether] ${tt('手机已连接: ', 'phone connected: ')}${msg.name}`)
|
|
207
225
|
break
|
|
208
226
|
case 'peer-path':
|
|
209
|
-
console.log(msg.kind === 'direct'
|
|
210
|
-
?
|
|
211
|
-
:
|
|
227
|
+
console.log(`[tether] ${msg.kind === 'direct'
|
|
228
|
+
? tt('连接路径: P2P 直连(NAT 打洞成功) ', 'path: direct P2P (NAT hole punch succeeded) ')
|
|
229
|
+
: tt('连接路径: relay 中转(打洞未成,可用但延迟略高) ', 'path: via relay (hole punch failed; works, slightly higher latency) ')}${msg.remote}`)
|
|
212
230
|
break
|
|
213
231
|
case 'peer-disconnected':
|
|
214
|
-
console.log(
|
|
232
|
+
console.log(`[tether] ${tt('手机已断开', 'phone disconnected')}`)
|
|
215
233
|
break
|
|
216
234
|
case 'proxy-opened':
|
|
217
|
-
console.log(
|
|
235
|
+
console.log(`[tether] ${tt('手机已开始加载界面', 'the phone started loading the interface')}`)
|
|
218
236
|
break
|
|
219
237
|
case 'devices': {
|
|
220
238
|
const waiters = pendingDevices
|
|
@@ -223,7 +241,7 @@ function apply(ctx, config = {}) {
|
|
|
223
241
|
break
|
|
224
242
|
}
|
|
225
243
|
default:
|
|
226
|
-
console.error(`[tether] 未知 sidecar 消息: ${line}`)
|
|
244
|
+
console.error(`[tether] ${tt('未知 sidecar 消息: ', 'unknown sidecar message: ')}${line}`)
|
|
227
245
|
}
|
|
228
246
|
})
|
|
229
247
|
|
|
@@ -244,7 +262,7 @@ function apply(ctx, config = {}) {
|
|
|
244
262
|
const res = await fetch(url, { redirect: 'manual' })
|
|
245
263
|
const setCookie = res.headers.getSetCookie()
|
|
246
264
|
if (res.status !== 303 || setCookie.length !== 1) {
|
|
247
|
-
throw new Error(
|
|
265
|
+
throw new Error(`${tt('token 兑换应答异常: ', 'unexpected token-exchange reply: ')}HTTP ${res.status}, set-cookie ×${setCookie.length}`)
|
|
248
266
|
}
|
|
249
267
|
send({ type: 'proxy-auth', cookie: setCookie[0].split(';', 1)[0], authority })
|
|
250
268
|
}
|
|
@@ -254,7 +272,7 @@ function apply(ctx, config = {}) {
|
|
|
254
272
|
return await exchange()
|
|
255
273
|
} catch (error) {
|
|
256
274
|
if (attempt >= 3) {
|
|
257
|
-
console.error(
|
|
275
|
+
console.error(`[tether] ${tt('浏览器认证 cookie 获取失败,手机端将收到 401:', 'could not obtain the browser auth cookie; the phone will get 401:')}`, error)
|
|
258
276
|
return
|
|
259
277
|
}
|
|
260
278
|
await new Promise((resolve) => setTimeout(resolve, 1000 * attempt))
|
|
@@ -291,7 +309,7 @@ function apply(ctx, config = {}) {
|
|
|
291
309
|
})
|
|
292
310
|
if (result === undefined) {
|
|
293
311
|
res.writeHead(504, { 'content-type': 'text/plain; charset=utf-8' })
|
|
294
|
-
res.end('sidecar 没有在 5 秒内给出配对码')
|
|
312
|
+
res.end(tr(req, 'sidecar 没有在 5 秒内给出配对码', 'the sidecar did not return a pairing code within 5 seconds'))
|
|
295
313
|
return
|
|
296
314
|
}
|
|
297
315
|
res.writeHead(200, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' })
|
|
@@ -315,7 +333,7 @@ function apply(ctx, config = {}) {
|
|
|
315
333
|
const id = await readForgetId(req)
|
|
316
334
|
if (id === undefined) {
|
|
317
335
|
res.writeHead(400, { 'content-type': 'text/plain; charset=utf-8' })
|
|
318
|
-
res.end('缺少要移除的设备 id')
|
|
336
|
+
res.end(tr(req, '缺少要移除的设备 id', 'the device id to remove is missing'))
|
|
319
337
|
return
|
|
320
338
|
}
|
|
321
339
|
send({ type: 'device-forget', id })
|
|
@@ -333,7 +351,7 @@ function apply(ctx, config = {}) {
|
|
|
333
351
|
})
|
|
334
352
|
if (devices === undefined) {
|
|
335
353
|
res.writeHead(504, { 'content-type': 'text/plain; charset=utf-8' })
|
|
336
|
-
res.end('sidecar 没有在 5 秒内返回设备列表')
|
|
354
|
+
res.end(tr(req, 'sidecar 没有在 5 秒内返回设备列表', 'the sidecar did not return the device list within 5 seconds'))
|
|
337
355
|
return
|
|
338
356
|
}
|
|
339
357
|
res.writeHead(200, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' })
|
|
@@ -427,7 +445,9 @@ function injectNarrowScreenCss(html) {
|
|
|
427
445
|
grid-template-columns: 56px minmax(0, 1fr) 0px !important;
|
|
428
446
|
}
|
|
429
447
|
[class*="_frame"] > [class*="centerCol"] { grid-column: 2 !important; }
|
|
430
|
-
|
|
448
|
+
/* 右列 dsh 0.1.5 起从 detailsCol 改名 rightbarCol,两个名字都钉住 */
|
|
449
|
+
[class*="_frame"] > [class*="detailsCol"],
|
|
450
|
+
[class*="_frame"] > [class*="rightbarCol"] { grid-column: 3 !important; }
|
|
431
451
|
[class*="_frame"] > [class*="sidebarCol"] {
|
|
432
452
|
position: absolute !important;
|
|
433
453
|
top: 0;
|
|
@@ -487,6 +507,10 @@ function injectNarrowScreenCss(html) {
|
|
|
487
507
|
const script = `
|
|
488
508
|
(function () {
|
|
489
509
|
var narrow = window.matchMedia('(max-width: 640px)')
|
|
510
|
+
// 这一部分长在 dsh 界面里,语言就该跟 dsh 的设置走;它把当前语言写在 <html lang> 上
|
|
511
|
+
function zhUi() { return /^zh/i.test(document.documentElement.lang || '') }
|
|
512
|
+
function tx(zh, en) { return zhUi() ? zh : en }
|
|
513
|
+
function langHeaders() { return { 'x-dsh-tether-lang': zhUi() ? 'zh' : 'en' } }
|
|
490
514
|
document.addEventListener('pointerdown', function (event) {
|
|
491
515
|
if (!narrow.matches) return
|
|
492
516
|
var frame = document.querySelector('[class*="_frame"]')
|
|
@@ -536,16 +560,16 @@ function injectNarrowScreenCss(html) {
|
|
|
536
560
|
var slim = 'flex:none;font:inherit;font-size:12px;font-weight:400;padding:5px 12px;border-radius:14px;'
|
|
537
561
|
+ 'border:1px solid ' + t.line + ';background:transparent;color:inherit;cursor:pointer'
|
|
538
562
|
var copy = document.createElement('button')
|
|
539
|
-
copy.textContent = '复制'
|
|
563
|
+
copy.textContent = tx('复制', 'Copy')
|
|
540
564
|
copy.style.cssText = slim
|
|
541
565
|
copy.addEventListener('click', function () {
|
|
542
566
|
navigator.clipboard.writeText(body).then(
|
|
543
|
-
function () { copy.textContent = '已复制' },
|
|
544
|
-
function () { copy.textContent = '复制失败' },
|
|
567
|
+
function () { copy.textContent = tx('已复制', 'Copied') },
|
|
568
|
+
function () { copy.textContent = tx('复制失败', 'Copy failed') },
|
|
545
569
|
)
|
|
546
570
|
})
|
|
547
571
|
var close = document.createElement('button')
|
|
548
|
-
close.textContent = '关闭'
|
|
572
|
+
close.textContent = tx('关闭', 'Close')
|
|
549
573
|
close.style.cssText = slim
|
|
550
574
|
close.addEventListener('click', function () { mask.remove() })
|
|
551
575
|
bar.append(name)
|
|
@@ -578,7 +602,7 @@ function injectNarrowScreenCss(html) {
|
|
|
578
602
|
box.style.cssText = 'flex:none;border-top:1px solid ' + t.line + ';padding:12px 16px 14px'
|
|
579
603
|
var head = document.createElement('div')
|
|
580
604
|
head.style.cssText = 'font-size:12px;color:' + t.muted + ';padding-bottom:8px'
|
|
581
|
-
head.textContent = '已配对的手机'
|
|
605
|
+
head.textContent = tx('已配对的手机', 'Paired phones')
|
|
582
606
|
var list = document.createElement('div')
|
|
583
607
|
box.append(head, list)
|
|
584
608
|
|
|
@@ -587,7 +611,7 @@ function injectNarrowScreenCss(html) {
|
|
|
587
611
|
if (!devices.length) {
|
|
588
612
|
var none = document.createElement('div')
|
|
589
613
|
none.style.cssText = 'font-size:12px;color:' + t.muted
|
|
590
|
-
none.textContent = '还没有手机配对过'
|
|
614
|
+
none.textContent = tx('还没有手机配对过', 'No phone paired yet')
|
|
591
615
|
list.append(none)
|
|
592
616
|
return
|
|
593
617
|
}
|
|
@@ -597,7 +621,7 @@ function injectNarrowScreenCss(html) {
|
|
|
597
621
|
var dot = document.createElement('span')
|
|
598
622
|
dot.style.cssText = 'flex:none;width:7px;height:7px;border-radius:50%;background:'
|
|
599
623
|
+ (d.online ? '#12b76a' : t.line)
|
|
600
|
-
dot.title = d.online ? '在线' : '离线'
|
|
624
|
+
dot.title = d.online ? tx('在线', 'online') : tx('离线', 'offline')
|
|
601
625
|
var label = document.createElement('div')
|
|
602
626
|
label.style.cssText = 'flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap'
|
|
603
627
|
label.textContent = d.name
|
|
@@ -608,34 +632,34 @@ function injectNarrowScreenCss(html) {
|
|
|
608
632
|
label.append(idTag)
|
|
609
633
|
// 不用原生 confirm:第一下改文案,第二下才真删
|
|
610
634
|
var drop = document.createElement('button')
|
|
611
|
-
drop.textContent = '移除'
|
|
635
|
+
drop.textContent = tx('移除', 'Remove')
|
|
612
636
|
drop.style.cssText = slim
|
|
613
637
|
var armed = false
|
|
614
638
|
drop.addEventListener('click', function () {
|
|
615
639
|
if (!armed) {
|
|
616
640
|
armed = true
|
|
617
|
-
drop.textContent = '确认移除?'
|
|
641
|
+
drop.textContent = tx('确认移除?', 'Remove it?')
|
|
618
642
|
drop.style.color = t.danger
|
|
619
643
|
setTimeout(function () {
|
|
620
644
|
if (!armed) return
|
|
621
645
|
armed = false
|
|
622
|
-
drop.textContent = '移除'
|
|
646
|
+
drop.textContent = tx('移除', 'Remove')
|
|
623
647
|
drop.style.color = 'inherit'
|
|
624
648
|
}, 4000)
|
|
625
649
|
return
|
|
626
650
|
}
|
|
627
651
|
drop.disabled = true
|
|
628
|
-
drop.textContent = '移除中'
|
|
652
|
+
drop.textContent = tx('移除中', 'Removing')
|
|
629
653
|
fetch('${DEVICES_PATH}', {
|
|
630
654
|
method: 'POST',
|
|
631
|
-
headers: { 'content-type': 'application/json' },
|
|
655
|
+
headers: Object.assign({ 'content-type': 'application/json' }, langHeaders()),
|
|
632
656
|
body: JSON.stringify({ id: d.id }),
|
|
633
657
|
})
|
|
634
658
|
.then(function (r) { return r.ok ? r.json() : r.text().then(function (x) { throw new Error(x) }) })
|
|
635
659
|
.then(function (x) { render(x.devices) })
|
|
636
660
|
.catch(function (e) {
|
|
637
661
|
drop.disabled = false
|
|
638
|
-
drop.textContent = '移除失败'
|
|
662
|
+
drop.textContent = tx('移除失败', 'Removal failed')
|
|
639
663
|
console.error('[dsh-tether] 移除设备失败', e)
|
|
640
664
|
})
|
|
641
665
|
})
|
|
@@ -646,27 +670,28 @@ function injectNarrowScreenCss(html) {
|
|
|
646
670
|
|
|
647
671
|
var loading = document.createElement('div')
|
|
648
672
|
loading.style.cssText = 'font-size:12px;color:' + t.muted
|
|
649
|
-
loading.textContent = '读取中…'
|
|
673
|
+
loading.textContent = tx('读取中…', 'Loading…')
|
|
650
674
|
list.append(loading)
|
|
651
|
-
fetch('${DEVICES_PATH}')
|
|
675
|
+
fetch('${DEVICES_PATH}', { headers: langHeaders() })
|
|
652
676
|
.then(function (r) { return r.ok ? r.json() : r.text().then(function (x) { throw new Error(x) }) })
|
|
653
677
|
.then(function (x) { render(x.devices) })
|
|
654
|
-
.catch(function (e) { loading.textContent = '读取失败: ' + (e && e.message ? e.message : e) })
|
|
678
|
+
.catch(function (e) { loading.textContent = tx('读取失败: ', 'Loading failed: ') + (e && e.message ? e.message : e) })
|
|
655
679
|
return box
|
|
656
680
|
}
|
|
657
681
|
|
|
658
682
|
function showPairing() {
|
|
659
|
-
fetch('${PAIRING_PATH}')
|
|
683
|
+
fetch('${PAIRING_PATH}', { headers: langHeaders() })
|
|
660
684
|
.then(function (r) { return r.ok ? r.json() : r.text().then(function (t) { throw new Error(t) }) })
|
|
661
685
|
.then(function (d) {
|
|
662
686
|
viewer(
|
|
663
|
-
'在手机上「添加电脑」里粘贴整行,'
|
|
687
|
+
tx('在手机上「添加电脑」里粘贴整行,', 'Paste the whole line into “Add a computer” on the phone; valid for ')
|
|
688
|
+
+ Math.round(d.expiresInSec / 60) + tx(' 分钟内有效', ' min'),
|
|
664
689
|
d.pairingString,
|
|
665
690
|
undefined,
|
|
666
691
|
devicesBlock,
|
|
667
692
|
)
|
|
668
693
|
})
|
|
669
|
-
.catch(function (e) { viewer('配对', '', String(e && e.message ? e.message : e)) })
|
|
694
|
+
.catch(function (e) { viewer(tx('配对', 'Pairing'), '', String(e && e.message ? e.message : e)) })
|
|
670
695
|
}
|
|
671
696
|
|
|
672
697
|
function mountHostsTrigger() {
|
|
@@ -684,7 +709,7 @@ function injectNarrowScreenCss(html) {
|
|
|
684
709
|
var button = document.createElement('button')
|
|
685
710
|
button.type = 'button'
|
|
686
711
|
button.setAttribute('data-dsh-tether', 'hosts-trigger')
|
|
687
|
-
var label = framed ? '我的电脑' : '连接手机'
|
|
712
|
+
var label = framed ? tx('我的电脑', 'My computers') : tx('连接手机', 'Connect a phone')
|
|
688
713
|
button.setAttribute('aria-label', label)
|
|
689
714
|
// 类名带内容哈希,照抄隔壁「设置」按钮当前的,外观自然一致
|
|
690
715
|
button.className = reference.className
|
|
@@ -700,6 +725,13 @@ function injectNarrowScreenCss(html) {
|
|
|
700
725
|
button.className = reference.className
|
|
701
726
|
row.className = reference.parentElement.className
|
|
702
727
|
}).observe(reference.parentElement, { attributes: true, attributeFilter: ['class'], subtree: true })
|
|
728
|
+
// dsh 的语言能在设置里当场切,这个标签跟着换
|
|
729
|
+
new MutationObserver(function () {
|
|
730
|
+
var next = framed ? tx('我的电脑', 'My computers') : tx('连接手机', 'Connect a phone')
|
|
731
|
+
button.setAttribute('aria-label', next)
|
|
732
|
+
var span = button.querySelector('[data-dsh-tether="hosts-label"]')
|
|
733
|
+
if (span) span.textContent = next
|
|
734
|
+
}).observe(document.documentElement, { attributes: true, attributeFilter: ['lang'] })
|
|
703
735
|
}
|
|
704
736
|
mountHostsTrigger()
|
|
705
737
|
new MutationObserver(mountHostsTrigger).observe(document.documentElement, { childList: true, subtree: true })
|
|
@@ -711,10 +743,10 @@ function injectNarrowScreenCss(html) {
|
|
|
711
743
|
if (!button || !/打开配置文件|Open config|open the config/i.test(button.textContent || '')) return
|
|
712
744
|
event.preventDefault()
|
|
713
745
|
event.stopPropagation()
|
|
714
|
-
fetch('${CONFIG_DOCUMENT_PATH}')
|
|
746
|
+
fetch('${CONFIG_DOCUMENT_PATH}', { headers: langHeaders() })
|
|
715
747
|
.then(function (r) { return r.ok ? r.json() : r.text().then(function (t) { throw new Error(t) }) })
|
|
716
748
|
.then(function (d) { viewer(d.path, d.text) })
|
|
717
|
-
.catch(function (e) { viewer('配置文件', '', String(e && e.message ? e.message : e)) })
|
|
749
|
+
.catch(function (e) { viewer(tx('配置文件', 'Config file'), '', String(e && e.message ? e.message : e)) })
|
|
718
750
|
}, true)
|
|
719
751
|
})()`
|
|
720
752
|
return html.replace(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-tether",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.16",
|
|
4
4
|
"description": "Mobile client for the DeepSeek Harness — use the dsh on your dev machine from your Android or iOS phone, remotely across networks, peer-to-peer over iroh with no server to set up (falls back to a relay that only sees ciphertext)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
},
|
|
43
43
|
"androidRuntime": {
|
|
44
44
|
"node": "24.18.0",
|
|
45
|
-
"dsh": "0.1.
|
|
45
|
+
"dsh": "0.1.5-rc.2"
|
|
46
46
|
}
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
@@ -54,11 +54,11 @@
|
|
|
54
54
|
"@deepseek-ai/dsh-user-approval": { "optional": true }
|
|
55
55
|
},
|
|
56
56
|
"optionalDependencies": {
|
|
57
|
-
"dsh-tether-host-android-arm64": "0.1.
|
|
58
|
-
"dsh-tether-host-darwin-arm64": "0.1.
|
|
59
|
-
"dsh-tether-host-darwin-x64": "0.1.
|
|
60
|
-
"dsh-tether-host-linux-arm64": "0.1.
|
|
61
|
-
"dsh-tether-host-linux-x64": "0.1.
|
|
62
|
-
"dsh-tether-host-win32-x64": "0.1.
|
|
57
|
+
"dsh-tether-host-android-arm64": "0.1.16",
|
|
58
|
+
"dsh-tether-host-darwin-arm64": "0.1.16",
|
|
59
|
+
"dsh-tether-host-darwin-x64": "0.1.16",
|
|
60
|
+
"dsh-tether-host-linux-arm64": "0.1.16",
|
|
61
|
+
"dsh-tether-host-linux-x64": "0.1.16",
|
|
62
|
+
"dsh-tether-host-win32-x64": "0.1.16"
|
|
63
63
|
}
|
|
64
64
|
}
|