dsh-plugin-tether 0.1.15 → 0.1.17
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 +18 -0
- package/README.md +1 -0
- package/README.zh.md +1 -0
- package/index.js +92 -52
- package/package.json +7 -7
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
本文件记录面向用户的变化。每个版本的这一节会原样作为该版本 Release 的说明。
|
|
4
4
|
|
|
5
|
+
## 0.1.17
|
|
6
|
+
|
|
7
|
+
### 修复
|
|
8
|
+
|
|
9
|
+
- **sidecar 自己的 `--help` 还是中文**([#8](https://github.com/zexadev/dsh-tether/issues/8))。0.1.16 把界面和终端输出都做了英文,唯独 `tether-host --help` 和每个参数的说明漏了,手跑二进制的人只能对着中文猜参数。现在帮助文本也按语言出中英。
|
|
10
|
+
- **Windows 和 macOS 上手跑 sidecar 恒出中文**。判定语言只认 `LC_ALL`/`LANG`,而这两个系统本来就不设这两个变量,英文用户于是必得中文。现在环境变量没有就问系统语言。
|
|
11
|
+
- **配对失败的原因在手机上显示成中文**。原因是电脑侧直接把中文句子发给了手机——这句该由看到它的那一端来措辞。线上改成只传原因码,手机 App 和 dsh 界面各按自己那侧的语言显示。另有几处写死的中文一并补齐(phone-sim 的代理就绪、本地代理监听失败、前台服务日志、移除设备失败)。
|
|
12
|
+
|
|
13
|
+
### English
|
|
14
|
+
|
|
15
|
+
This release finishes what 0.1.16 started: `tether-host --help` and all option descriptions are now English on an English machine, the language is detected from the system when `LC_ALL`/`LANG` are unset (which is the normal case on Windows and macOS), and a failed pairing is now worded by whichever side displays it, so the phone no longer shows a Chinese reason. Nothing a user can see is Chinese-only any more; if you find something, please open an issue.
|
|
16
|
+
|
|
17
|
+
## 0.1.16
|
|
18
|
+
|
|
19
|
+
### 新增
|
|
20
|
+
|
|
21
|
+
- **英文界面**([#8](https://github.com/zexadev/dsh-tether/issues/8))。手机 App 的界面、连接与本地模式的报错、常驻通知,以及电脑上插件与 sidecar 打在终端里的输出,现在都有英文。没有单独的语言开关,各处跟随各自那一侧的语言:App 跟手机的系统语言,终端输出跟电脑的系统语言,插件注入在 dsh 界面里的那部分(侧栏按钮、配对串弹窗、已配对手机列表)跟 dsh 自己的语言设置,在 dsh 设置里切换语言即时生效。dsh 界面本身的语言仍由 dsh 的设置决定。
|
|
22
|
+
|
|
5
23
|
## 0.1.15
|
|
6
24
|
|
|
7
25
|
### 新增
|
package/README.md
CHANGED
|
@@ -135,6 +135,7 @@ 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
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
|
|
package/README.zh.md
CHANGED
|
@@ -135,6 +135,7 @@ 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
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
|
|
package/index.js
CHANGED
|
@@ -17,6 +17,31 @@ 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
|
+
* sidecar 发的是配对失败原因码(tether-core 定义),不是人话——同一条消息手机上也要显示,
|
|
31
|
+
* 只能各端各译。认不出的原样带出:0.1.16 及更早的 sidecar 直接发中文句子。
|
|
32
|
+
*/
|
|
33
|
+
const pairFailText = (code) => ({
|
|
34
|
+
'no-window': tt('主机上没有开着的配对窗口', 'no pairing window is open'),
|
|
35
|
+
expired: tt('配对窗口已过期', 'the pairing window expired'),
|
|
36
|
+
'too-many-attempts': tt('配对码试错次数超限', 'too many wrong codes'),
|
|
37
|
+
'bad-code': tt('配对码不正确', 'wrong pairing code'),
|
|
38
|
+
}[code] ?? code)
|
|
39
|
+
/** 路由文案 */
|
|
40
|
+
const tr = (req, zh, en) => {
|
|
41
|
+
const declared = req.headers['x-dsh-tether-lang'] || req.headers['accept-language'] || ''
|
|
42
|
+
return /(^|[\s,])zh/i.test(declared) ? zh : en
|
|
43
|
+
}
|
|
44
|
+
|
|
20
45
|
/** 手机侧只读查看配置文件的路由 */
|
|
21
46
|
const CONFIG_DOCUMENT_PATH = '/dsh-tether/config-document'
|
|
22
47
|
/** 按需开一个配对窗口,拿回配对串 */
|
|
@@ -101,7 +126,7 @@ function applyUi(ctx) {
|
|
|
101
126
|
const path = await ctx.settings.prepareDocument()
|
|
102
127
|
if (path === undefined) {
|
|
103
128
|
res.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' })
|
|
104
|
-
res.end('当前的设置存储不是本地文件,没有可查看的配置文件')
|
|
129
|
+
res.end(tr(req, '当前的设置存储不是本地文件,没有可查看的配置文件', 'The settings store is not a local file, so there is no config file to show'))
|
|
105
130
|
return
|
|
106
131
|
}
|
|
107
132
|
let text
|
|
@@ -109,7 +134,7 @@ function applyUi(ctx) {
|
|
|
109
134
|
text = await readFile(path, 'utf8')
|
|
110
135
|
} catch (error) {
|
|
111
136
|
res.writeHead(500, { 'content-type': 'text/plain; charset=utf-8' })
|
|
112
|
-
res.end(
|
|
137
|
+
res.end(`${tr(req, '读不到配置文件 ', 'cannot read the config file ')}${path}: ${String(error)}`)
|
|
113
138
|
return
|
|
114
139
|
}
|
|
115
140
|
res.writeHead(200, {
|
|
@@ -136,16 +161,19 @@ function apply(ctx, config = {}) {
|
|
|
136
161
|
const binary = config.hostBinary ?? resolveHostBinary()
|
|
137
162
|
if (binary === undefined || !existsSync(binary)) {
|
|
138
163
|
throw new Error(
|
|
139
|
-
`[tether]
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
164
|
+
`[tether] ${tt(
|
|
165
|
+
`找不到 sidecar 二进制(平台 ${process.platform}-${process.arch})。从源码用:先跑 cargo build --release -p tether-host;`
|
|
166
|
+
+ '或在插件 config.hostBinary 里指定路径。若你是从 npm/GitHub 安装的,说明该平台的 dsh-tether-host-* 子包还没发布,请提 issue。',
|
|
167
|
+
`sidecar binary not found for ${process.platform}-${process.arch}. From source: run cargo build --release -p tether-host, `
|
|
168
|
+
+ '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.',
|
|
169
|
+
)}`,
|
|
143
170
|
)
|
|
144
171
|
}
|
|
145
172
|
// webServer.port 是真实监听端口(config 写 0 时为 OS 分配值),手机由此拿到完整界面
|
|
146
173
|
const proxyTarget = `${ctx.webServer.host}:${ctx.webServer.port}`
|
|
147
174
|
|
|
148
|
-
|
|
175
|
+
// sidecar 的人读日志与这里打在同一个终端上,语言跟着走,免得一半中文一半英文
|
|
176
|
+
const args = ['host', '--proxy-target', proxyTarget, '--lang', terminalZh ? 'zh' : 'en']
|
|
149
177
|
if (config.pair) args.push('--pair')
|
|
150
178
|
const child = spawn(binary, args, { stdio: ['pipe', 'pipe', 'inherit'] })
|
|
151
179
|
ctx.effect(() => () => { child.kill() })
|
|
@@ -153,16 +181,16 @@ function apply(ctx, config = {}) {
|
|
|
153
181
|
let sidecarGone
|
|
154
182
|
child.on('exit', (code) => {
|
|
155
183
|
// sidecar 死了必须可见:手机端会静默失联,而电脑侧一切照常
|
|
156
|
-
sidecarGone = `sidecar 已退出(code=${code})`
|
|
157
|
-
console.error(`[tether] sidecar 退出 code=${code}
|
|
184
|
+
sidecarGone = tt(`sidecar 已退出(code=${code})`, `the sidecar exited (code=${code})`)
|
|
185
|
+
console.error(`[tether] ${tt(`sidecar 退出 code=${code};手机端已失联`, `the sidecar exited with code=${code}; the phone is now cut off`)}`)
|
|
158
186
|
})
|
|
159
187
|
// spawn 失败只走 error 不走 exit;不接住就是 uncaught。EACCES 曾实发过:
|
|
160
188
|
// 打包丢了二进制执行位,现场只有一句干瘪的 Permission denied,毫无指向
|
|
161
189
|
child.on('error', (error) => {
|
|
162
190
|
const hint = error.code === 'EACCES'
|
|
163
|
-
? ';EACCES 通常是二进制缺可执行位,试 chmod 0755 该文件并升级插件'
|
|
191
|
+
? tt(';EACCES 通常是二进制缺可执行位,试 chmod 0755 该文件并升级插件', '; EACCES usually means the binary lost its execute bit — try chmod 0755 on it and update the plugin')
|
|
164
192
|
: ''
|
|
165
|
-
sidecarGone =
|
|
193
|
+
sidecarGone = `${tt('sidecar 启动失败: ', 'the sidecar failed to start: ')}${error.message}${hint}`
|
|
166
194
|
console.error(`[tether] ${sidecarGone}(${binary})`)
|
|
167
195
|
})
|
|
168
196
|
|
|
@@ -178,43 +206,43 @@ function apply(ctx, config = {}) {
|
|
|
178
206
|
try {
|
|
179
207
|
msg = JSON.parse(line)
|
|
180
208
|
} catch {
|
|
181
|
-
console.error(`[tether] 无法解析 sidecar 消息: ${line}`)
|
|
209
|
+
console.error(`[tether] ${tt('无法解析 sidecar 消息: ', 'cannot parse the sidecar message: ')}${line}`)
|
|
182
210
|
return
|
|
183
211
|
}
|
|
184
212
|
switch (msg.type) {
|
|
185
213
|
case 'ready':
|
|
186
214
|
endpointId = msg['endpoint_id']
|
|
187
|
-
console.log(`[tether] 就绪,本机设备 ID: ${endpointId}`)
|
|
188
|
-
console.log(`[tether]
|
|
215
|
+
console.log(`[tether] ${tt('就绪,本机设备 ID: ', 'ready, device ID of this machine: ')}${endpointId}`)
|
|
216
|
+
console.log(`[tether] ${tt(`手机将看到 http://${proxyTarget} 的完整界面`, `the phone will see the full interface at http://${proxyTarget}`)}`)
|
|
189
217
|
break
|
|
190
218
|
case 'pairing': {
|
|
191
219
|
// 一行可整体粘贴到手机,省去分别输 ID 和码
|
|
192
220
|
const pairingString = `${endpointId}#${msg.code}`
|
|
193
|
-
console.log(`[tether]
|
|
221
|
+
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
222
|
const waiter = pendingPairing
|
|
195
223
|
pendingPairing = undefined
|
|
196
224
|
waiter?.({ pairingString, expiresInSec: msg['expires_in_sec'] })
|
|
197
225
|
break
|
|
198
226
|
}
|
|
199
227
|
case 'pairing-closed':
|
|
200
|
-
console.log(`[tether] 配对窗口已关闭: ${msg.reason}`)
|
|
228
|
+
console.log(`[tether] ${tt('配对窗口已关闭: ', 'the pairing window closed: ')}${pairFailText(msg.reason)}`)
|
|
201
229
|
break
|
|
202
230
|
case 'pairing-done':
|
|
203
|
-
console.log(`[tether] 配对成功: ${msg.name}(${msg.peer.slice(0, 16)}…)`)
|
|
231
|
+
console.log(`[tether] ${tt('配对成功: ', 'paired: ')}${msg.name}(${msg.peer.slice(0, 16)}…)`)
|
|
204
232
|
break
|
|
205
233
|
case 'peer-connected':
|
|
206
|
-
console.log(`[tether] 手机已连接: ${msg.name}`)
|
|
234
|
+
console.log(`[tether] ${tt('手机已连接: ', 'phone connected: ')}${msg.name}`)
|
|
207
235
|
break
|
|
208
236
|
case 'peer-path':
|
|
209
|
-
console.log(msg.kind === 'direct'
|
|
210
|
-
?
|
|
211
|
-
:
|
|
237
|
+
console.log(`[tether] ${msg.kind === 'direct'
|
|
238
|
+
? tt('连接路径: P2P 直连(NAT 打洞成功) ', 'path: direct P2P (NAT hole punch succeeded) ')
|
|
239
|
+
: tt('连接路径: relay 中转(打洞未成,可用但延迟略高) ', 'path: via relay (hole punch failed; works, slightly higher latency) ')}${msg.remote}`)
|
|
212
240
|
break
|
|
213
241
|
case 'peer-disconnected':
|
|
214
|
-
console.log(
|
|
242
|
+
console.log(`[tether] ${tt('手机已断开', 'phone disconnected')}`)
|
|
215
243
|
break
|
|
216
244
|
case 'proxy-opened':
|
|
217
|
-
console.log(
|
|
245
|
+
console.log(`[tether] ${tt('手机已开始加载界面', 'the phone started loading the interface')}`)
|
|
218
246
|
break
|
|
219
247
|
case 'devices': {
|
|
220
248
|
const waiters = pendingDevices
|
|
@@ -223,7 +251,7 @@ function apply(ctx, config = {}) {
|
|
|
223
251
|
break
|
|
224
252
|
}
|
|
225
253
|
default:
|
|
226
|
-
console.error(`[tether] 未知 sidecar 消息: ${line}`)
|
|
254
|
+
console.error(`[tether] ${tt('未知 sidecar 消息: ', 'unknown sidecar message: ')}${line}`)
|
|
227
255
|
}
|
|
228
256
|
})
|
|
229
257
|
|
|
@@ -244,7 +272,7 @@ function apply(ctx, config = {}) {
|
|
|
244
272
|
const res = await fetch(url, { redirect: 'manual' })
|
|
245
273
|
const setCookie = res.headers.getSetCookie()
|
|
246
274
|
if (res.status !== 303 || setCookie.length !== 1) {
|
|
247
|
-
throw new Error(
|
|
275
|
+
throw new Error(`${tt('token 兑换应答异常: ', 'unexpected token-exchange reply: ')}HTTP ${res.status}, set-cookie ×${setCookie.length}`)
|
|
248
276
|
}
|
|
249
277
|
send({ type: 'proxy-auth', cookie: setCookie[0].split(';', 1)[0], authority })
|
|
250
278
|
}
|
|
@@ -254,7 +282,7 @@ function apply(ctx, config = {}) {
|
|
|
254
282
|
return await exchange()
|
|
255
283
|
} catch (error) {
|
|
256
284
|
if (attempt >= 3) {
|
|
257
|
-
console.error(
|
|
285
|
+
console.error(`[tether] ${tt('浏览器认证 cookie 获取失败,手机端将收到 401:', 'could not obtain the browser auth cookie; the phone will get 401:')}`, error)
|
|
258
286
|
return
|
|
259
287
|
}
|
|
260
288
|
await new Promise((resolve) => setTimeout(resolve, 1000 * attempt))
|
|
@@ -291,7 +319,7 @@ function apply(ctx, config = {}) {
|
|
|
291
319
|
})
|
|
292
320
|
if (result === undefined) {
|
|
293
321
|
res.writeHead(504, { 'content-type': 'text/plain; charset=utf-8' })
|
|
294
|
-
res.end('sidecar 没有在 5 秒内给出配对码')
|
|
322
|
+
res.end(tr(req, 'sidecar 没有在 5 秒内给出配对码', 'the sidecar did not return a pairing code within 5 seconds'))
|
|
295
323
|
return
|
|
296
324
|
}
|
|
297
325
|
res.writeHead(200, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' })
|
|
@@ -315,7 +343,7 @@ function apply(ctx, config = {}) {
|
|
|
315
343
|
const id = await readForgetId(req)
|
|
316
344
|
if (id === undefined) {
|
|
317
345
|
res.writeHead(400, { 'content-type': 'text/plain; charset=utf-8' })
|
|
318
|
-
res.end('缺少要移除的设备 id')
|
|
346
|
+
res.end(tr(req, '缺少要移除的设备 id', 'the device id to remove is missing'))
|
|
319
347
|
return
|
|
320
348
|
}
|
|
321
349
|
send({ type: 'device-forget', id })
|
|
@@ -333,7 +361,7 @@ function apply(ctx, config = {}) {
|
|
|
333
361
|
})
|
|
334
362
|
if (devices === undefined) {
|
|
335
363
|
res.writeHead(504, { 'content-type': 'text/plain; charset=utf-8' })
|
|
336
|
-
res.end('sidecar 没有在 5 秒内返回设备列表')
|
|
364
|
+
res.end(tr(req, 'sidecar 没有在 5 秒内返回设备列表', 'the sidecar did not return the device list within 5 seconds'))
|
|
337
365
|
return
|
|
338
366
|
}
|
|
339
367
|
res.writeHead(200, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' })
|
|
@@ -489,6 +517,10 @@ function injectNarrowScreenCss(html) {
|
|
|
489
517
|
const script = `
|
|
490
518
|
(function () {
|
|
491
519
|
var narrow = window.matchMedia('(max-width: 640px)')
|
|
520
|
+
// 这一部分长在 dsh 界面里,语言就该跟 dsh 的设置走;它把当前语言写在 <html lang> 上
|
|
521
|
+
function zhUi() { return /^zh/i.test(document.documentElement.lang || '') }
|
|
522
|
+
function tx(zh, en) { return zhUi() ? zh : en }
|
|
523
|
+
function langHeaders() { return { 'x-dsh-tether-lang': zhUi() ? 'zh' : 'en' } }
|
|
492
524
|
document.addEventListener('pointerdown', function (event) {
|
|
493
525
|
if (!narrow.matches) return
|
|
494
526
|
var frame = document.querySelector('[class*="_frame"]')
|
|
@@ -538,16 +570,16 @@ function injectNarrowScreenCss(html) {
|
|
|
538
570
|
var slim = 'flex:none;font:inherit;font-size:12px;font-weight:400;padding:5px 12px;border-radius:14px;'
|
|
539
571
|
+ 'border:1px solid ' + t.line + ';background:transparent;color:inherit;cursor:pointer'
|
|
540
572
|
var copy = document.createElement('button')
|
|
541
|
-
copy.textContent = '复制'
|
|
573
|
+
copy.textContent = tx('复制', 'Copy')
|
|
542
574
|
copy.style.cssText = slim
|
|
543
575
|
copy.addEventListener('click', function () {
|
|
544
576
|
navigator.clipboard.writeText(body).then(
|
|
545
|
-
function () { copy.textContent = '已复制' },
|
|
546
|
-
function () { copy.textContent = '复制失败' },
|
|
577
|
+
function () { copy.textContent = tx('已复制', 'Copied') },
|
|
578
|
+
function () { copy.textContent = tx('复制失败', 'Copy failed') },
|
|
547
579
|
)
|
|
548
580
|
})
|
|
549
581
|
var close = document.createElement('button')
|
|
550
|
-
close.textContent = '关闭'
|
|
582
|
+
close.textContent = tx('关闭', 'Close')
|
|
551
583
|
close.style.cssText = slim
|
|
552
584
|
close.addEventListener('click', function () { mask.remove() })
|
|
553
585
|
bar.append(name)
|
|
@@ -580,7 +612,7 @@ function injectNarrowScreenCss(html) {
|
|
|
580
612
|
box.style.cssText = 'flex:none;border-top:1px solid ' + t.line + ';padding:12px 16px 14px'
|
|
581
613
|
var head = document.createElement('div')
|
|
582
614
|
head.style.cssText = 'font-size:12px;color:' + t.muted + ';padding-bottom:8px'
|
|
583
|
-
head.textContent = '已配对的手机'
|
|
615
|
+
head.textContent = tx('已配对的手机', 'Paired phones')
|
|
584
616
|
var list = document.createElement('div')
|
|
585
617
|
box.append(head, list)
|
|
586
618
|
|
|
@@ -589,7 +621,7 @@ function injectNarrowScreenCss(html) {
|
|
|
589
621
|
if (!devices.length) {
|
|
590
622
|
var none = document.createElement('div')
|
|
591
623
|
none.style.cssText = 'font-size:12px;color:' + t.muted
|
|
592
|
-
none.textContent = '还没有手机配对过'
|
|
624
|
+
none.textContent = tx('还没有手机配对过', 'No phone paired yet')
|
|
593
625
|
list.append(none)
|
|
594
626
|
return
|
|
595
627
|
}
|
|
@@ -599,7 +631,7 @@ function injectNarrowScreenCss(html) {
|
|
|
599
631
|
var dot = document.createElement('span')
|
|
600
632
|
dot.style.cssText = 'flex:none;width:7px;height:7px;border-radius:50%;background:'
|
|
601
633
|
+ (d.online ? '#12b76a' : t.line)
|
|
602
|
-
dot.title = d.online ? '在线' : '离线'
|
|
634
|
+
dot.title = d.online ? tx('在线', 'online') : tx('离线', 'offline')
|
|
603
635
|
var label = document.createElement('div')
|
|
604
636
|
label.style.cssText = 'flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap'
|
|
605
637
|
label.textContent = d.name
|
|
@@ -610,35 +642,35 @@ function injectNarrowScreenCss(html) {
|
|
|
610
642
|
label.append(idTag)
|
|
611
643
|
// 不用原生 confirm:第一下改文案,第二下才真删
|
|
612
644
|
var drop = document.createElement('button')
|
|
613
|
-
drop.textContent = '移除'
|
|
645
|
+
drop.textContent = tx('移除', 'Remove')
|
|
614
646
|
drop.style.cssText = slim
|
|
615
647
|
var armed = false
|
|
616
648
|
drop.addEventListener('click', function () {
|
|
617
649
|
if (!armed) {
|
|
618
650
|
armed = true
|
|
619
|
-
drop.textContent = '确认移除?'
|
|
651
|
+
drop.textContent = tx('确认移除?', 'Remove it?')
|
|
620
652
|
drop.style.color = t.danger
|
|
621
653
|
setTimeout(function () {
|
|
622
654
|
if (!armed) return
|
|
623
655
|
armed = false
|
|
624
|
-
drop.textContent = '移除'
|
|
656
|
+
drop.textContent = tx('移除', 'Remove')
|
|
625
657
|
drop.style.color = 'inherit'
|
|
626
658
|
}, 4000)
|
|
627
659
|
return
|
|
628
660
|
}
|
|
629
661
|
drop.disabled = true
|
|
630
|
-
drop.textContent = '移除中'
|
|
662
|
+
drop.textContent = tx('移除中', 'Removing')
|
|
631
663
|
fetch('${DEVICES_PATH}', {
|
|
632
664
|
method: 'POST',
|
|
633
|
-
headers: { 'content-type': 'application/json' },
|
|
665
|
+
headers: Object.assign({ 'content-type': 'application/json' }, langHeaders()),
|
|
634
666
|
body: JSON.stringify({ id: d.id }),
|
|
635
667
|
})
|
|
636
668
|
.then(function (r) { return r.ok ? r.json() : r.text().then(function (x) { throw new Error(x) }) })
|
|
637
669
|
.then(function (x) { render(x.devices) })
|
|
638
670
|
.catch(function (e) {
|
|
639
671
|
drop.disabled = false
|
|
640
|
-
drop.textContent = '移除失败'
|
|
641
|
-
console.error('[dsh-tether] 移除设备失败', e)
|
|
672
|
+
drop.textContent = tx('移除失败', 'Removal failed')
|
|
673
|
+
console.error('[dsh-tether] ' + tx('移除设备失败', 'could not remove the device'), e)
|
|
642
674
|
})
|
|
643
675
|
})
|
|
644
676
|
row.append(dot, label, drop)
|
|
@@ -648,27 +680,28 @@ function injectNarrowScreenCss(html) {
|
|
|
648
680
|
|
|
649
681
|
var loading = document.createElement('div')
|
|
650
682
|
loading.style.cssText = 'font-size:12px;color:' + t.muted
|
|
651
|
-
loading.textContent = '读取中…'
|
|
683
|
+
loading.textContent = tx('读取中…', 'Loading…')
|
|
652
684
|
list.append(loading)
|
|
653
|
-
fetch('${DEVICES_PATH}')
|
|
685
|
+
fetch('${DEVICES_PATH}', { headers: langHeaders() })
|
|
654
686
|
.then(function (r) { return r.ok ? r.json() : r.text().then(function (x) { throw new Error(x) }) })
|
|
655
687
|
.then(function (x) { render(x.devices) })
|
|
656
|
-
.catch(function (e) { loading.textContent = '读取失败: ' + (e && e.message ? e.message : e) })
|
|
688
|
+
.catch(function (e) { loading.textContent = tx('读取失败: ', 'Loading failed: ') + (e && e.message ? e.message : e) })
|
|
657
689
|
return box
|
|
658
690
|
}
|
|
659
691
|
|
|
660
692
|
function showPairing() {
|
|
661
|
-
fetch('${PAIRING_PATH}')
|
|
693
|
+
fetch('${PAIRING_PATH}', { headers: langHeaders() })
|
|
662
694
|
.then(function (r) { return r.ok ? r.json() : r.text().then(function (t) { throw new Error(t) }) })
|
|
663
695
|
.then(function (d) {
|
|
664
696
|
viewer(
|
|
665
|
-
'在手机上「添加电脑」里粘贴整行,'
|
|
697
|
+
tx('在手机上「添加电脑」里粘贴整行,', 'Paste the whole line into “Add a computer” on the phone; valid for ')
|
|
698
|
+
+ Math.round(d.expiresInSec / 60) + tx(' 分钟内有效', ' min'),
|
|
666
699
|
d.pairingString,
|
|
667
700
|
undefined,
|
|
668
701
|
devicesBlock,
|
|
669
702
|
)
|
|
670
703
|
})
|
|
671
|
-
.catch(function (e) { viewer('配对', '', String(e && e.message ? e.message : e)) })
|
|
704
|
+
.catch(function (e) { viewer(tx('配对', 'Pairing'), '', String(e && e.message ? e.message : e)) })
|
|
672
705
|
}
|
|
673
706
|
|
|
674
707
|
function mountHostsTrigger() {
|
|
@@ -686,7 +719,7 @@ function injectNarrowScreenCss(html) {
|
|
|
686
719
|
var button = document.createElement('button')
|
|
687
720
|
button.type = 'button'
|
|
688
721
|
button.setAttribute('data-dsh-tether', 'hosts-trigger')
|
|
689
|
-
var label = framed ? '我的电脑' : '连接手机'
|
|
722
|
+
var label = framed ? tx('我的电脑', 'My computers') : tx('连接手机', 'Connect a phone')
|
|
690
723
|
button.setAttribute('aria-label', label)
|
|
691
724
|
// 类名带内容哈希,照抄隔壁「设置」按钮当前的,外观自然一致
|
|
692
725
|
button.className = reference.className
|
|
@@ -702,6 +735,13 @@ function injectNarrowScreenCss(html) {
|
|
|
702
735
|
button.className = reference.className
|
|
703
736
|
row.className = reference.parentElement.className
|
|
704
737
|
}).observe(reference.parentElement, { attributes: true, attributeFilter: ['class'], subtree: true })
|
|
738
|
+
// dsh 的语言能在设置里当场切,这个标签跟着换
|
|
739
|
+
new MutationObserver(function () {
|
|
740
|
+
var next = framed ? tx('我的电脑', 'My computers') : tx('连接手机', 'Connect a phone')
|
|
741
|
+
button.setAttribute('aria-label', next)
|
|
742
|
+
var span = button.querySelector('[data-dsh-tether="hosts-label"]')
|
|
743
|
+
if (span) span.textContent = next
|
|
744
|
+
}).observe(document.documentElement, { attributes: true, attributeFilter: ['lang'] })
|
|
705
745
|
}
|
|
706
746
|
mountHostsTrigger()
|
|
707
747
|
new MutationObserver(mountHostsTrigger).observe(document.documentElement, { childList: true, subtree: true })
|
|
@@ -713,10 +753,10 @@ function injectNarrowScreenCss(html) {
|
|
|
713
753
|
if (!button || !/打开配置文件|Open config|open the config/i.test(button.textContent || '')) return
|
|
714
754
|
event.preventDefault()
|
|
715
755
|
event.stopPropagation()
|
|
716
|
-
fetch('${CONFIG_DOCUMENT_PATH}')
|
|
756
|
+
fetch('${CONFIG_DOCUMENT_PATH}', { headers: langHeaders() })
|
|
717
757
|
.then(function (r) { return r.ok ? r.json() : r.text().then(function (t) { throw new Error(t) }) })
|
|
718
758
|
.then(function (d) { viewer(d.path, d.text) })
|
|
719
|
-
.catch(function (e) { viewer('配置文件', '', String(e && e.message ? e.message : e)) })
|
|
759
|
+
.catch(function (e) { viewer(tx('配置文件', 'Config file'), '', String(e && e.message ? e.message : e)) })
|
|
720
760
|
}, true)
|
|
721
761
|
})()`
|
|
722
762
|
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.17",
|
|
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",
|
|
@@ -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.17",
|
|
58
|
+
"dsh-tether-host-darwin-arm64": "0.1.17",
|
|
59
|
+
"dsh-tether-host-darwin-x64": "0.1.17",
|
|
60
|
+
"dsh-tether-host-linux-arm64": "0.1.17",
|
|
61
|
+
"dsh-tether-host-linux-x64": "0.1.17",
|
|
62
|
+
"dsh-tether-host-win32-x64": "0.1.17"
|
|
63
63
|
}
|
|
64
64
|
}
|