dsh-plugin-tether 0.1.2 → 0.1.4

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 ADDED
@@ -0,0 +1,53 @@
1
+ # 更新日志
2
+
3
+ 本文件记录面向用户的变化。每个版本的这一节会原样作为该版本 Release 的说明。
4
+
5
+ ## 0.1.4
6
+
7
+ ### 兼容性
8
+
9
+ - **确认支持 dsh `0.1.0-rc.8`**。已在 rc.8 环境下逐项核对本插件依赖的上游接触面:patch 引用的三个包名仍存在且 patch 实际生效(`--dump-config` 可见 `directory-picker` 已停用、两个 browse 条目已挂上);窄屏样式依赖的布局 class 未变;`sidebar.footer.action` 与 `settings.trigger` 两个插槽仍在;插件的三条 HTTP 路由均正常且跨站请求仍被拒。
10
+ - `peerDependencies` 由精确的 `0.1.0-rc.7` 放宽为 `>=0.1.0-rc.7`。此前的精确版本号会让 rc.8 用户在安装时收到 unmet peer 警告,而两个版本实测都可用。
11
+
12
+ ## 0.1.3
13
+
14
+ ### 新增
15
+
16
+ - **电脑端可以查看并移除已配对的手机**。侧栏「连接手机」弹出的配对串下方新增列表,显示每台手机的名字、ID 前 8 位与在线状态,可逐台移除。
17
+ 此前白名单只增不减,而手机每重装一次 App 就会生成一把新的身份密钥,旧的那把仍留在白名单里持有访问权,电脑端却没有任何入口能看见或撤销它们——只能手工编辑 `paired.json`。
18
+ 移除会同时断开该手机当前的连接:白名单只在握手时读取一次,若不主动断开,正连着的那台要等它自己下线才真正失效。
19
+ - 手机端「我的电脑」页脚的项目主页链接现在可用,由系统浏览器打开。此前该链接为空,不显示。
20
+
21
+ ### 文档
22
+
23
+ - 补上 LINUX DO 社区链接与徽章。
24
+ - 32 位 APK 的实际文件名是 `arm` 而非 `armv7`,文档按实际产出更正。
25
+ - 移除配对章节中包含真实配对串的截图。
26
+
27
+ ## 0.1.2
28
+
29
+ ### 新增
30
+
31
+ - **APK 改为四个 CPU 架构全量构建**:`arm64`、`arm`、`x86`、`x86_64`。此前只构建 `aarch64`,而产物却被命名为 `universal`——那是「不按 ABI 拆分」这个构建变体的名字,不代表包含所有架构;解包可见其中只有 `arm64-v8a`。
32
+ - APK 文件名改为 `dsh-tether-<版本>-<架构>.apk`,此前是不含版本号的 `app-universal-release.apk`。
33
+
34
+ ### 移除
35
+
36
+ - 删除 `dsh-tunnel`——插件化之前的独立隧道原型,其能力已由 `tether-host --proxy-target` 覆盖。
37
+
38
+ ## 0.1.1
39
+
40
+ ### 新增
41
+
42
+ - **新增 `linux-arm64` 平台子包**。主包的 `optionalDependencies` 本已声明五个平台,构建矩阵却只产出四个;缺失的那个按 npm 的可选依赖语义被静默跳过,安装期毫无提示,直到运行时才会发现找不到二进制。
43
+ - 打 tag 时自动发布到 npm,先发平台子包再发主包。
44
+
45
+ ## 0.1.0
46
+
47
+ 首个发布版本。
48
+
49
+ - DeepSeek Harness 插件 + Android App,通过 iroh P2P 直连,中间不经过任何服务器。
50
+ - 手机上显示的是 DSH 自身的完整 Web 界面,窄屏下注入最小样式适配(侧栏改抽屉、设置页导航横排)。
51
+ - 6 位配对码,10 分钟有效,每窗口 3 次尝试上限;配对后凭手机的 iroh 公钥授权。
52
+ - 手机端可保存多台电脑并切换;agent 等待审批时推送系统通知。
53
+ - 平台子包覆盖 win32-x64、darwin-x64、darwin-arm64、linux-x64。
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-4D6BFE?style=flat" alt="dsh 0.1.0-rc.7">
20
+ <img src="https://img.shields.io/badge/dsh-0.1.0--rc.7%20%7C%20rc.8-4D6BFE?style=flat" alt="dsh 0.1.0-rc.7 | rc.8">
21
21
  <img src="https://img.shields.io/badge/Android-4493F8?style=flat" alt="Android">
22
22
  </p>
23
23
 
@@ -46,7 +46,7 @@ If you only use your phone on the same Wi-Fi as your machine, you don't need any
46
46
 
47
47
  The plugin carries a small Rust sidecar that owns the iroh connection, shipped as one package per platform; installing pulls only the one matching your system, with nothing to choose.
48
48
 
49
- The phone build is split by CPU architecture. **Any Android phone from the last decade takes `arm64`**; `armv7` is for 32-bit legacy devices, and `x86` / `x86_64` are for emulators and ChromeOS. Picking the wrong one simply gets refused at install time.
49
+ The phone build is split by CPU architecture. **Any Android phone from the last decade takes `arm64`**; `arm` is for 32-bit legacy devices, and `x86` / `x86_64` are for emulators and ChromeOS. Picking the wrong one simply gets refused at install time.
50
50
 
51
51
  You can also build the sidecar from source (needs Rust):
52
52
 
@@ -60,8 +60,6 @@ dsh plugin --profile web add .
60
60
 
61
61
  Start `dsh web` as you normally would, then click **Connect phone** at the bottom of the sidebar. You get one line to copy:
62
62
 
63
- <p align="center"><img src="assets/pc-pairing.png" width="620" alt="The pairing string, shown on the computer"></p>
64
-
65
63
  On the phone, open DSH Tether → **Add computer** → paste that whole line → name the computer → connect.
66
64
 
67
65
  From then on the app connects by itself when you open it.
@@ -104,7 +102,7 @@ From then on the app connects by itself when you open it.
104
102
  - Android only. iOS is not planned for now.
105
103
  - The app connects when you open it and holds no background connection — Android's doze would not let it anyway.
106
104
  - 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.
107
- - Verified against dsh **`0.1.0-rc.7`**. dsh is in developer preview — check this line before assuming a newer dsh works.
105
+ - Verified against dsh **`0.1.0-rc.7`** and **`0.1.0-rc.8`**. dsh is in developer preview — check this line before assuming a newer dsh works.
108
106
 
109
107
  ## What has been verified
110
108
 
@@ -139,3 +137,11 @@ pnpm exec tauri android build --apk --target aarch64
139
137
  ## License
140
138
 
141
139
  MIT
140
+
141
+ ## Links
142
+
143
+ - [LINUX DO](https://linux.do) — where this project is shared
144
+
145
+ <p align="center">
146
+ <a href="https://linux.do/"><img src="https://img.shields.io/badge/%E7%A4%BE%E5%8C%BA-LINUX%20DO-4D6BFE?style=for-the-badge" alt="LINUX DO"></a>
147
+ </p>
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-4D6BFE?style=flat" alt="dsh 0.1.0-rc.7">
20
+ <img src="https://img.shields.io/badge/dsh-0.1.0--rc.7%20%7C%20rc.8-4D6BFE?style=flat" alt="dsh 0.1.0-rc.7 | rc.8">
21
21
  <img src="https://img.shields.io/badge/Android-4493F8?style=flat" alt="Android">
22
22
  </p>
23
23
 
@@ -46,7 +46,7 @@ DSH Tether 把 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harnes
46
46
 
47
47
  插件带一个 Rust 写的 sidecar 负责 iroh 连接,按平台拆成独立子包;安装时只会下载与你系统匹配的那一个,不用手动选。
48
48
 
49
- 手机端按 CPU 架构分了四个包,**近十年的安卓手机都选 `arm64`**;`armv7` 给 32 位老设备,`x86` / `x86_64` 给模拟器和 ChromeOS。装错了系统会直接拒绝安装,不会装出问题。
49
+ 手机端按 CPU 架构分了四个包,**近十年的安卓手机都选 `arm64`**;`arm` 给 32 位老设备,`x86` / `x86_64` 给模拟器和 ChromeOS。装错了系统会直接拒绝安装,不会装出问题。
50
50
 
51
51
  也可以从源码构建 sidecar(需要 Rust):
52
52
 
@@ -60,8 +60,6 @@ dsh plugin --profile web add .
60
60
 
61
61
  电脑上照常启动 `dsh web`,在侧栏底部点**「连接手机」**,会给出一行配对串:
62
62
 
63
- <p align="center"><img src="assets/pc-pairing.png" width="620" alt="电脑上出示配对串"></p>
64
-
65
63
  手机上打开 DSH Tether →「添加电脑」→ 把那一整行粘进去 → 给这台电脑起个名字 → 连接。
66
64
 
67
65
  之后每次打开 App 自动连上,不用再配对。
@@ -104,7 +102,7 @@ dsh plugin --profile web add .
104
102
  - 只有 Android。iOS 暂不做。
105
103
  - App 打开时才连接,不在后台常驻——Android 的 doze 也留不住它。
106
104
  - 手机上的界面是 DSH 自己的,窄屏适配靠注入的最小样式完成;dsh 改版式时可能需要跟进。
107
- - 已针对 dsh **`0.1.0-rc.7`** 验证。dsh 处于 developer preview,换更新的 dsh 之前先看这一行。
105
+ - 已针对 dsh **`0.1.0-rc.7`** 与 **`0.1.0-rc.8`** 验证。dsh 处于 developer preview,换更新的 dsh 之前先看这一行。
108
106
 
109
107
  ## 验证过什么
110
108
 
@@ -146,3 +144,11 @@ pnpm exec tauri android build --apk --target aarch64
146
144
  ## 许可证
147
145
 
148
146
  MIT
147
+
148
+ ## 友情链接
149
+
150
+ - [LINUX DO](https://linux.do) —— 本项目在该社区分享
151
+
152
+ <p align="center">
153
+ <a href="https://linux.do/"><img src="https://img.shields.io/badge/%E7%A4%BE%E5%8C%BA-LINUX%20DO-4D6BFE?style=for-the-badge" alt="LINUX DO"></a>
154
+ </p>
package/index.js CHANGED
@@ -21,6 +21,7 @@ const inject = ['webServer', 'settings']
21
21
  const CONFIG_DOCUMENT_PATH = '/dsh-tether/config-document'
22
22
  /** 按需开一个配对窗口,拿回配对串 */
23
23
  const PAIRING_PATH = '/dsh-tether/pairing'
24
+ const DEVICES_PATH = '/dsh-tether/devices'
24
25
 
25
26
  /** dsh 的回环判定:localhost、IPv6 回环、以及整个 127/8 */
26
27
  function isLoopbackHostname(hostname) {
@@ -62,6 +63,24 @@ function isTrustedRequest(req) {
62
63
  }
63
64
 
64
65
  /** 未通过栅栏时统一的拒绝应答,和 dsh 的 /api 一样是裸 403,不泄露路由细节 */
66
+ /** 读 POST body 里的 {"id": "..."};格式不对一律当作没给 */
67
+ async function readForgetId(req) {
68
+ const chunks = []
69
+ let size = 0
70
+ for await (const chunk of req) {
71
+ size += chunk.length
72
+ // 请求体只可能是一个 64 位十六进制 id,超过这个量级就是异常输入
73
+ if (size > 4096) return undefined
74
+ chunks.push(chunk)
75
+ }
76
+ try {
77
+ const id = JSON.parse(Buffer.concat(chunks).toString('utf8')).id
78
+ return typeof id === 'string' && id !== '' ? id : undefined
79
+ } catch {
80
+ return undefined
81
+ }
82
+ }
83
+
65
84
  function refuse(res) {
66
85
  res.writeHead(403, { 'content-type': 'text/plain; charset=utf-8' })
67
86
  res.end('forbidden')
@@ -97,6 +116,8 @@ function apply(ctx, config = {}) {
97
116
  let endpointId = ''
98
117
  /** 等着拿配对串的那个请求;sidecar 回 pairing 时兑现 */
99
118
  let pendingPairing
119
+ /** 等着拿设备列表的那些请求;sidecar 回 devices 时一并兑现 */
120
+ let pendingDevices = []
100
121
 
101
122
  createInterface({ input: child.stdout }).on('line', (line) => {
102
123
  let msg
@@ -138,6 +159,12 @@ function apply(ctx, config = {}) {
138
159
  case 'peer-disconnected':
139
160
  console.log('[tether] 手机已断开')
140
161
  break
162
+ case 'devices': {
163
+ const waiters = pendingDevices
164
+ pendingDevices = []
165
+ for (const w of waiters) w(msg.devices)
166
+ break
167
+ }
141
168
  default:
142
169
  console.error(`[tether] 未知 sidecar 消息: ${line}`)
143
170
  }
@@ -201,6 +228,43 @@ function apply(ctx, config = {}) {
201
228
  },
202
229
  }))
203
230
 
231
+ // 白名单只增不减是个安全问题:手机每重装一次 App 就换一把身份密钥,旧的那把
232
+ // 仍留在白名单里持有访问权,而电脑端此前没有任何入口能看见或撤销它们。
233
+ ctx.effect(() => ctx.webServer.register({
234
+ kind: 'exact',
235
+ path: DEVICES_PATH,
236
+ handler: async (req, res) => {
237
+ if (!isTrustedRequest(req)) return refuse(res)
238
+ if (req.method === 'POST') {
239
+ const id = await readForgetId(req)
240
+ if (id === undefined) {
241
+ res.writeHead(400, { 'content-type': 'text/plain; charset=utf-8' })
242
+ res.end('缺少要移除的设备 id')
243
+ return
244
+ }
245
+ send({ type: 'device-forget', id })
246
+ } else {
247
+ send({ type: 'device-list' })
248
+ }
249
+ const devices = await new Promise((resolve) => {
250
+ pendingDevices.push(resolve)
251
+ setTimeout(() => {
252
+ const at = pendingDevices.indexOf(resolve)
253
+ if (at === -1) return
254
+ pendingDevices.splice(at, 1)
255
+ resolve(undefined)
256
+ }, 5000)
257
+ })
258
+ if (devices === undefined) {
259
+ res.writeHead(504, { 'content-type': 'text/plain; charset=utf-8' })
260
+ res.end('sidecar 没有在 5 秒内返回设备列表')
261
+ return
262
+ }
263
+ res.writeHead(200, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' })
264
+ res.end(JSON.stringify({ devices }))
265
+ },
266
+ }))
267
+
204
268
  ctx.on('approval/request', async (req, next) => {
205
269
  send({ type: 'approval', id: req.callId, tool_name: req.toolName ?? '', reason: req.reason ?? '' })
206
270
  try {
@@ -355,7 +419,7 @@ function injectNarrowScreenCss(html) {
355
419
  // 就地渲染文件内容——只读,想改仍然去电脑上改。
356
420
  // 色值/圆角/字重全部取自 dsh 自身的实测计算值,别在人家界面里插一块不一样的东西。
357
421
  // 窄屏铺满一屏,宽屏收成居中卡片——和 dsh 的弹窗一个做法。
358
- function viewer(title, body, error) {
422
+ function viewer(title, body, error, extraFactory) {
359
423
  var dark = matchMedia('(prefers-color-scheme: dark)').matches
360
424
  var t = dark
361
425
  ? { fg: '#f9fafb', surface: '#2c2c2e', line: 'rgba(255,255,255,.12)', muted: '#adb2b8', danger: '#f5766b' }
@@ -408,6 +472,7 @@ function injectNarrowScreenCss(html) {
408
472
  pre.textContent = error ? error : body
409
473
 
410
474
  card.append(bar, pre)
475
+ if (extraFactory) card.append(extraFactory(t, slim))
411
476
  mask.append(card)
412
477
  // 宽屏点遮罩关闭,窄屏没有遮罩可点
413
478
  if (!narrow.matches) mask.addEventListener('click', function (e) { if (e.target === mask) mask.remove() })
@@ -420,6 +485,90 @@ function injectNarrowScreenCss(html) {
420
485
  // 好让用户换手机、加设备时有地方拿新配对码(否则只能去删 paired.json)。
421
486
  var framed = window.parent !== window
422
487
 
488
+ // 已配对手机列表。白名单每条都持有访问权,而手机重装 App 会换身份密钥,
489
+ // 旧条目就此变成没人认识的常驻凭证——这里是撤销它们的地方。
490
+ function devicesBlock(t, slim) {
491
+ var box = document.createElement('div')
492
+ box.style.cssText = 'flex:none;border-top:1px solid ' + t.line + ';padding:12px 16px 14px'
493
+ var head = document.createElement('div')
494
+ head.style.cssText = 'font-size:12px;color:' + t.muted + ';padding-bottom:8px'
495
+ head.textContent = '已配对的手机'
496
+ var list = document.createElement('div')
497
+ box.append(head, list)
498
+
499
+ function render(devices) {
500
+ list.textContent = ''
501
+ if (!devices.length) {
502
+ var none = document.createElement('div')
503
+ none.style.cssText = 'font-size:12px;color:' + t.muted
504
+ none.textContent = '还没有手机配对过'
505
+ list.append(none)
506
+ return
507
+ }
508
+ devices.forEach(function (d) {
509
+ var row = document.createElement('div')
510
+ row.style.cssText = 'display:flex;align-items:center;gap:10px;padding:7px 0'
511
+ var dot = document.createElement('span')
512
+ dot.style.cssText = 'flex:none;width:7px;height:7px;border-radius:50%;background:'
513
+ + (d.online ? '#12b76a' : t.line)
514
+ dot.title = d.online ? '在线' : '离线'
515
+ var label = document.createElement('div')
516
+ label.style.cssText = 'flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap'
517
+ label.textContent = d.name
518
+ var idTag = document.createElement('span')
519
+ idTag.style.cssText = 'font-size:12px;color:' + t.muted
520
+ + ';font-family:ui-monospace,"SF Mono",Consolas,monospace;padding-left:8px'
521
+ idTag.textContent = d.id.slice(0, 8)
522
+ label.append(idTag)
523
+ // 不用原生 confirm:第一下改文案,第二下才真删
524
+ var drop = document.createElement('button')
525
+ drop.textContent = '移除'
526
+ drop.style.cssText = slim
527
+ var armed = false
528
+ drop.addEventListener('click', function () {
529
+ if (!armed) {
530
+ armed = true
531
+ drop.textContent = '确认移除?'
532
+ drop.style.color = t.danger
533
+ setTimeout(function () {
534
+ if (!armed) return
535
+ armed = false
536
+ drop.textContent = '移除'
537
+ drop.style.color = 'inherit'
538
+ }, 4000)
539
+ return
540
+ }
541
+ drop.disabled = true
542
+ drop.textContent = '移除中'
543
+ fetch('${DEVICES_PATH}', {
544
+ method: 'POST',
545
+ headers: { 'content-type': 'application/json' },
546
+ body: JSON.stringify({ id: d.id }),
547
+ })
548
+ .then(function (r) { return r.ok ? r.json() : r.text().then(function (x) { throw new Error(x) }) })
549
+ .then(function (x) { render(x.devices) })
550
+ .catch(function (e) {
551
+ drop.disabled = false
552
+ drop.textContent = '移除失败'
553
+ console.error('[dsh-tether] 移除设备失败', e)
554
+ })
555
+ })
556
+ row.append(dot, label, drop)
557
+ list.append(row)
558
+ })
559
+ }
560
+
561
+ var loading = document.createElement('div')
562
+ loading.style.cssText = 'font-size:12px;color:' + t.muted
563
+ loading.textContent = '读取中…'
564
+ list.append(loading)
565
+ fetch('${DEVICES_PATH}')
566
+ .then(function (r) { return r.ok ? r.json() : r.text().then(function (x) { throw new Error(x) }) })
567
+ .then(function (x) { render(x.devices) })
568
+ .catch(function (e) { loading.textContent = '读取失败: ' + (e && e.message ? e.message : e) })
569
+ return box
570
+ }
571
+
423
572
  function showPairing() {
424
573
  fetch('${PAIRING_PATH}')
425
574
  .then(function (r) { return r.ok ? r.json() : r.text().then(function (t) { throw new Error(t) }) })
@@ -427,6 +576,8 @@ function injectNarrowScreenCss(html) {
427
576
  viewer(
428
577
  '在手机上「添加电脑」里粘贴整行,' + Math.round(d.expiresInSec / 60) + ' 分钟内有效',
429
578
  d.pairingString,
579
+ undefined,
580
+ devicesBlock,
430
581
  )
431
582
  })
432
583
  .catch(function (e) { viewer('配对', '', String(e && e.message ? e.message : e)) })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-tether",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Reach the DeepSeek Harness on your dev machine from your phone — across networks, through no server at all (iroh P2P)",
5
5
  "keywords": [
6
6
  "dsh",
@@ -21,7 +21,8 @@
21
21
  "index.js",
22
22
  "cordis.patch.yml",
23
23
  "README.md",
24
- "README.zh.md"
24
+ "README.zh.md",
25
+ "CHANGELOG.md"
25
26
  ],
26
27
  "dsh": {
27
28
  "bundle": {
@@ -29,18 +30,18 @@
29
30
  }
30
31
  },
31
32
  "peerDependencies": {
32
- "@deepseek-ai/dsh-host-webserver": "0.1.0-rc.7",
33
- "@deepseek-ai/dsh-user-approval": "0.1.0-rc.7"
33
+ "@deepseek-ai/dsh-host-webserver": ">=0.1.0-rc.7",
34
+ "@deepseek-ai/dsh-user-approval": ">=0.1.0-rc.7"
34
35
  },
35
36
  "peerDependenciesMeta": {
36
37
  "@deepseek-ai/dsh-host-webserver": { "optional": true },
37
38
  "@deepseek-ai/dsh-user-approval": { "optional": true }
38
39
  },
39
40
  "optionalDependencies": {
40
- "dsh-tether-host-darwin-arm64": "0.1.2",
41
- "dsh-tether-host-darwin-x64": "0.1.2",
42
- "dsh-tether-host-linux-arm64": "0.1.2",
43
- "dsh-tether-host-linux-x64": "0.1.2",
44
- "dsh-tether-host-win32-x64": "0.1.2"
41
+ "dsh-tether-host-darwin-arm64": "0.1.4",
42
+ "dsh-tether-host-darwin-x64": "0.1.4",
43
+ "dsh-tether-host-linux-arm64": "0.1.4",
44
+ "dsh-tether-host-linux-x64": "0.1.4",
45
+ "dsh-tether-host-win32-x64": "0.1.4"
45
46
  }
46
47
  }