dsh-plugin-tether 0.1.20 → 0.1.22
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 +31 -0
- package/README.md +4 -4
- package/README.zh.md +4 -4
- package/index.js +152 -25
- package/package.json +10 -9
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,37 @@
|
|
|
2
2
|
|
|
3
3
|
本文件记录面向用户的变化。每个版本的这一节会原样作为该版本 Release 的说明。
|
|
4
4
|
|
|
5
|
+
## 0.1.22
|
|
6
|
+
|
|
7
|
+
### 修复
|
|
8
|
+
|
|
9
|
+
- **dsh 0.2.1-rc 上侧栏里的「连接手机」按钮不见了;dsh desktop 上它从来没出现过**([#16](https://github.com/zexadev/dsh-tether/pull/16))。这个按钮是照抄侧栏「设置」入口的样式挂上去的,抄不到就干脆不挂。dsh 0.2.1-rc 把设置入口从侧栏挪走了,浏览器界面上的按钮于是跟着消失;dsh desktop 的设置本来就在应用自己的菜单里,侧栏从来没有可抄的对象,所以那边一次都没出现过。首次配对不受影响(白名单为空时会自动弹出配对码),但换手机、加第二台设备只能靠这个按钮,没有它就只能手工删配对记录。现在抄不到「设置」就改抄侧栏底部那一行(登录后是账号行,没登录是「更多」),两处都抄不到也照样把按钮挂出来。顺带一提,注入界面走的通道也换了:dsh desktop 的界面来自应用自带的前端产物,老通道根本到不了它。
|
|
10
|
+
- **(手机本地模式)目录 fsync 的跳过条件收严**。0.1.20 为了绕开沙箱限制,改成「打不开的目录就跳过 fsync」;但「打不开」和「写不进去」是两回事,一个可写不可读的目录里真可能有本进程刚创建的条目,跳过就等于悄悄少落一次盘。现在要「打不开、而且也确实写不进去」才跳过。手机上那条路径(`/data/user/0` 及其以上对 App 只有执行权限)行为不变。
|
|
11
|
+
|
|
12
|
+
### 变更
|
|
13
|
+
|
|
14
|
+
- **手机本地模式内置的 dsh 从 `0.2.0-rc.2` 升到 `0.2.1-rc.2`**。装上这一版后,第一次启动本地模式会重新解压运行时。真机逐条验过:解压、选工作区、发一条消息、发一张图。arm64 包因内置运行时变大,从约 78 MB 涨到约 83 MB;其余 ABI 不含运行时,体积不变。
|
|
15
|
+
- **已验证的 dsh 版本补上 `0.2.1-rc.2`**。声明的兼容范围本来就覆盖 0.2.1 这一族,不用改,装不装得上没有变化。
|
|
16
|
+
|
|
17
|
+
### English
|
|
18
|
+
|
|
19
|
+
The Connect a phone button disappeared from the sidebar on dsh 0.2.1-rc, and never appeared at all in dsh desktop ([#16](https://github.com/zexadev/dsh-tether/pull/16)). The button is mounted by copying the style of the sidebar Settings entry, and it simply did not mount when there was nothing to copy. dsh 0.2.1-rc moved that entry out of the sidebar, so the button vanished in the browser interface; dsh desktop keeps its settings in the application menu, so the sidebar there never had anything to copy and the button never showed up at all. First pairing was unaffected — a pairing code appears by itself while no device is paired — but switching phones or adding a second device needs this button, and without it the only way out was deleting the pairing record by hand. The button now falls back to copying the row at the bottom of the sidebar (the account row when signed in, the More row when not), and it mounts with its own styling when neither reference exists. The channel the interface is injected through changed as well: the dsh desktop interface comes from the frontend bundled into the application, which the old channel could never reach.
|
|
20
|
+
|
|
21
|
+
In local mode on the phone, the condition for skipping a directory fsync is now stricter. To work around the sandbox, 0.1.20 skipped fsync for any directory the app could not open; but "cannot open" and "cannot write" are different things, and a directory that is writable but not readable may genuinely hold entries this process just created, so skipping it quietly loses a durability step. The fallback now skips only when the directory can neither be opened nor written. Behaviour on the phone is unchanged, since `/data/user/0` and everything above it grants an ordinary app execute only.
|
|
22
|
+
|
|
23
|
+
The dsh bundled into Android local mode moves from `0.2.0-rc.2` to `0.2.1-rc.2`. The first local-mode start after this update unpacks the runtime again. Verified on a real device end to end — unpacking, choosing a workspace, sending a message, and sending an image. The `arm64` APK grows from about 78 MB to about 83 MB because of the larger bundled runtime; the other ABIs carry no runtime and are unchanged.
|
|
24
|
+
|
|
25
|
+
The list of verified dsh versions gains `0.2.1-rc.2`. The declared compatibility range already covered the 0.2.1 family, so nothing changes about whether the plugin installs.
|
|
26
|
+
## 0.1.21
|
|
27
|
+
|
|
28
|
+
### 修复
|
|
29
|
+
|
|
30
|
+
- **在 dsh desktop 里启动时会弹出一个控制台窗口**([#15](https://github.com/zexadev/dsh-tether/issues/15))。插件拉起 sidecar 时没让 Windows 隐藏新窗口。dsh desktop 的插件宿主是以 Node 模式运行的 Electron,属 GUI 程序、自己没有控制台,Windows 于是给 sidecar(控制台程序)新分配一个控制台,连窗口一起显示出来。在终端里跑 dsh 不会这样:宿主已有控制台,子进程直接继承,所以这个窗口只在 desktop 下出现。现在新分配的窗口不再显示;终端里跑 dsh 时 sidecar 的日志照旧打在同一个终端。
|
|
31
|
+
|
|
32
|
+
### English
|
|
33
|
+
|
|
34
|
+
Starting the plugin inside dsh desktop popped up a console window ([#15](https://github.com/zexadev/dsh-tether/issues/15)). The plugin spawned its sidecar without asking Windows to hide the new window. The plugin host of dsh desktop is Electron running in Node mode — a GUI program that owns no console — so Windows allocated a fresh console for the sidecar, which is a console program, and showed its window along with it. Running dsh in a terminal never did this, because the host already owns a console and the child simply inherits it, which is why the window only ever appeared under desktop. The newly allocated window is now hidden, and sidecar logs still go to the same terminal when you run dsh there.
|
|
35
|
+
|
|
5
36
|
## 0.1.20
|
|
6
37
|
|
|
7
38
|
### 修复
|
package/README.md
CHANGED
|
@@ -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.2.
|
|
90
|
+
- **Install and go**: no Termux, no terminal, no commands, no root. Node 24 and the full dependency tree of dsh `0.2.1-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.2.
|
|
93
|
+
- **Versioned with the app**: the bundled dsh upgrades with the app; currently `0.2.1-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
|
|
|
@@ -136,8 +136,8 @@ The Termux route still works if you prefer it: install dsh and this plugin insid
|
|
|
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
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.
|
|
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
|
|
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`**, **`0.1.5-rc.2`**, **`0.1.6-alpha.2`**, **`0.1.7-rc.2`**, **`0.2.0-rc.2`** and **`0.2.1-
|
|
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 83 MB because of the bundled runtime; the others are about 32 MB.
|
|
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`**, **`0.1.5-rc.2`**, **`0.1.6-alpha.2`**, **`0.1.7-rc.2`**, **`0.2.0-rc.2`**, **`0.2.1-alpha.1`**, **`0.2.1-alpha.2`** and **`0.2.1-rc.2`**. dsh is in developer preview — check this line before assuming a newer dsh works. From 0.1.7 dsh refuses to install a plugin whose declared range does not cover it, so a version outside this line is a refusal at install time rather than a silent risk. 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.
|
|
141
141
|
|
|
142
142
|
## What has been verified
|
|
143
143
|
|
package/README.zh.md
CHANGED
|
@@ -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.2.
|
|
90
|
+
- **装完即用**:不装 Termux、不开终端、不敲命令、不要 root。Node 24 与 dsh `0.2.1-rc.2` 的完整依赖树打在 APK 里,首次启动离线解压;之后只有调用模型 API 才需要网络。
|
|
91
91
|
- **没有配对**:dsh 就在这台手机上,没有远端。会话、设置、工作区只存在手机里,和连电脑时看到的完全是两套,来回切换互不影响。
|
|
92
92
|
- **常驻**:运行期间有一条常驻通知(前台服务),退后台、锁屏不会被系统收掉;进程真被杀了,下次打开 App 自动重起,会话已落盘不丢。
|
|
93
|
-
- **版本随 App 走**:内置的 dsh 随 App 升级一起升,当前内置 `0.2.
|
|
93
|
+
- **版本随 App 走**:内置的 dsh 随 App 升级一起升,当前内置 `0.2.1-rc.2`。
|
|
94
94
|
- 只有 `arm64` 的 APK 带本地模式;iOS 的沙箱不允许子进程,iOS 版没有这个入口。
|
|
95
95
|
- 手机上没有 bash 和完整的 coreutils,依赖 shell 的工具用不全;对话、文件读写不受影响。
|
|
96
96
|
|
|
@@ -136,8 +136,8 @@ dsh plugin --profile web add .
|
|
|
136
136
|
- App 打开时才连接,不在后台常驻——Android 的 doze 也留不住它。
|
|
137
137
|
- 手机上的界面是 DSH 自己的,窄屏适配靠注入的最小样式完成;dsh 改版式时可能需要跟进。
|
|
138
138
|
- 中英双语:App 界面跟随手机的系统语言;电脑终端上的输出跟随电脑的系统语言;注入 dsh 界面的那部分跟随 dsh 自己的语言设置。没有单独的语言开关。
|
|
139
|
-
- 本地模式只有 `arm64` 包有,iOS 没有;手机上没有 bash 与完整 coreutils,依赖 shell 的工具用不全。arm64 包因内置运行时约
|
|
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`**、**`0.1.6-alpha.2`**、**`0.1.7-rc.2`**、**`0.2.0-rc.2`** 与 **`0.2.1-
|
|
139
|
+
- 本地模式只有 `arm64` 包有,iOS 没有;手机上没有 bash 与完整 coreutils,依赖 shell 的工具用不全。arm64 包因内置运行时约 83 MB,其余包约 32 MB。
|
|
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`**、**`0.1.6-alpha.2`**、**`0.1.7-rc.2`**、**`0.2.0-rc.2`**、**`0.2.1-alpha.1`**、**`0.2.1-alpha.2`** 与 **`0.2.1-rc.2`** 验证。dsh 处于 developer preview,换更新的 dsh 之前先看这一行。0.1.7 起 dsh 会拒绝安装声明范围没覆盖自己的插件,所以这一行之外的版本是装不上,而不是装上了埋雷。dsh 0.1.2-alpha 引入的浏览器认证完全在电脑侧处理,手机 App 无需更新。
|
|
141
141
|
|
|
142
142
|
## 验证过什么
|
|
143
143
|
|
package/index.js
CHANGED
|
@@ -113,7 +113,7 @@ function refuse(res) {
|
|
|
113
113
|
|
|
114
114
|
/** 与 sidecar 无关的部分:窄屏适配注入,以及手机上看配置文件的路由 */
|
|
115
115
|
function applyUi(ctx) {
|
|
116
|
-
|
|
116
|
+
mountTetherUi(ctx)
|
|
117
117
|
|
|
118
118
|
// 「打开配置文件」在宿主机桌面开编辑器,手机上按了毫无反应。这条路由把同一份
|
|
119
119
|
// 文件的内容原样交给手机自己渲染。路径由 Host 侧的 settings.documentPath 决定,
|
|
@@ -169,13 +169,31 @@ function apply(ctx, config = {}) {
|
|
|
169
169
|
)}`,
|
|
170
170
|
)
|
|
171
171
|
}
|
|
172
|
-
//
|
|
173
|
-
|
|
172
|
+
// dsh 0.2.1-alpha.2 起 webServer 能开 TLS。隧道里转的是明文 HTTP(代理流还要改写请求头),
|
|
173
|
+
// 对着 HTTPS 监听器只会让手机白屏,所以这里直接拒,不留给用户去猜。protocol 是那一版才有的,
|
|
174
|
+
// 旧版本读不到就按 http 算
|
|
175
|
+
if ((ctx.webServer.protocol ?? 'http:') !== 'http:') {
|
|
176
|
+
throw new Error(
|
|
177
|
+
`[tether] ${tt(
|
|
178
|
+
'dsh web 开了 TLS(https),而手机走的代理流只能转明文 HTTP,连上只会白屏。去掉 webServer 的 tls 配置再用手机连接。',
|
|
179
|
+
'dsh web is serving HTTPS, but the proxy stream the phone uses can only carry plain HTTP — the phone would get a blank page. Remove the tls config from webServer before connecting the phone.',
|
|
180
|
+
)}`,
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
// webServer.port 是真实监听端口(config 写 0 时为 OS 分配值),手机由此拿到完整界面。
|
|
184
|
+
// host 从 0.2.1-alpha.2 起可以是任意字面地址:IPv6 的要带方括号,否则 sidecar 那边按
|
|
185
|
+
// SocketAddr 解析 --proxy-target 直接失败(`::1:3000` 不是合法写法)
|
|
186
|
+
const host = ctx.webServer.host.includes(':') ? `[${ctx.webServer.host}]` : ctx.webServer.host
|
|
187
|
+
const proxyTarget = `${host}:${ctx.webServer.port}`
|
|
174
188
|
|
|
175
189
|
// sidecar 的人读日志与这里打在同一个终端上,语言跟着走,免得一半中文一半英文
|
|
176
190
|
const args = ['host', '--proxy-target', proxyTarget, '--lang', terminalZh ? 'zh' : 'en']
|
|
177
191
|
if (config.pair) args.push('--pair')
|
|
178
|
-
|
|
192
|
+
// windowsHide:dsh desktop 的插件宿主是 ELECTRON_RUN_AS_NODE 跑的 Electron,GUI 子系统、自己没有
|
|
193
|
+
// console,于是 Windows 给这个 console 子系统的 sidecar 分配一个新 console 并把窗口显示出来。该
|
|
194
|
+
// 选项让 libuv 传 SW_HIDE,新分配的窗口不显示;stderr 的 inherit 不受影响,在终端里跑 dsh 时
|
|
195
|
+
// sidecar 的日志照旧打在同一个终端
|
|
196
|
+
const child = spawn(binary, args, { stdio: ['pipe', 'pipe', 'inherit'], windowsHide: true })
|
|
179
197
|
ctx.effect(() => () => { child.kill() })
|
|
180
198
|
/** sidecar 不在了的原因;undefined = 活着。等它回话的路由用来立即报错 */
|
|
181
199
|
let sidecarGone
|
|
@@ -380,13 +398,38 @@ function apply(ctx, config = {}) {
|
|
|
380
398
|
})
|
|
381
399
|
}
|
|
382
400
|
|
|
401
|
+
/**
|
|
402
|
+
* 注入有两条通道,按宿主 webserver 的能力二选一:
|
|
403
|
+
*
|
|
404
|
+
* - 结构化的 `webserver/index-inject` 表(dsh 0.2.x 起):行是纯数据,一张表喂两个
|
|
405
|
+
* 渲染器。浏览器那边由宿主渲进 index.html 文本;**dsh desktop** 的 index.html 是
|
|
406
|
+
* 应用自带的前端产物,走 file:// 加 IPC,根本不经过宿主 renderIndex,只会把这张表
|
|
407
|
+
* 经 boot 通道(ready.injections)交给渲染器逐行建元素。桌面端要看到这部分界面,
|
|
408
|
+
* 只能走这条。
|
|
409
|
+
* - `tapIndex` 字符串变换(老宿主):只作用于宿主渲染的 index.html,桌面端永远看不到。
|
|
410
|
+
*
|
|
411
|
+
* 判据是能力(collectIndexInjections 在不在)而不是版本号。两条只走一条:同一份文本
|
|
412
|
+
* 注两遍没有意义,样式和监听器都会多一份。
|
|
413
|
+
*/
|
|
414
|
+
function mountTetherUi(ctx) {
|
|
415
|
+
if (typeof ctx.webServer.collectIndexInjections === 'function') {
|
|
416
|
+
ctx.effect(() =>
|
|
417
|
+
ctx.on('webserver/index-inject', (table) => {
|
|
418
|
+
table.push({ kind: 'style', text: tetherCss })
|
|
419
|
+
table.push({ kind: 'script', placement: 'head', text: tetherScript })
|
|
420
|
+
}),
|
|
421
|
+
)
|
|
422
|
+
return
|
|
423
|
+
}
|
|
424
|
+
ctx.effect(() => ctx.webServer.tapIndex(injectNarrowScreenCss))
|
|
425
|
+
}
|
|
426
|
+
|
|
383
427
|
/**
|
|
384
428
|
* 窄屏修正:只在 ≤640px 生效,桌面浏览器拿到同一份 index.html 也不受影响。
|
|
385
429
|
* 只用语义选择器(role/aria),不碰 dsh 的 CSS Module 类名——那些名字带内容
|
|
386
430
|
* 哈希,dsh 一改样式就会变,针对它们写规则必碎。
|
|
387
431
|
*/
|
|
388
|
-
|
|
389
|
-
const css = `
|
|
432
|
+
const tetherCss = `
|
|
390
433
|
/* 侧栏底部的 sidebar.footer.action 是横排 flex 行,官方设想里放并排的小动作。
|
|
391
434
|
「连接手机」照抄「设置」的类,是占满一行的触发行;别的插件(dsh-cost-meter)
|
|
392
435
|
又往同一行放 width:100% 的面板,横排下两者互挤:本按钮被压到只剩图标,
|
|
@@ -512,9 +555,10 @@ function injectNarrowScreenCss(html) {
|
|
|
512
555
|
z-index: 2;
|
|
513
556
|
}
|
|
514
557
|
}`
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
558
|
+
|
|
559
|
+
// 抽屉遮罩只是画上去的,点它不会收起——那需要脚本。这段只做这一件事:
|
|
560
|
+
// 窄屏下点到抽屉外面就替用户按一次侧栏开关。
|
|
561
|
+
const tetherScript = `
|
|
518
562
|
(function () {
|
|
519
563
|
var narrow = window.matchMedia('(max-width: 640px)')
|
|
520
564
|
// 这一部分长在 dsh 界面里,语言就该跟 dsh 的设置走;它把当前语言写在 <html lang> 上
|
|
@@ -704,37 +748,90 @@ function injectNarrowScreenCss(html) {
|
|
|
704
748
|
.catch(function (e) { viewer(tx('配对', 'Pairing'), '', String(e && e.message ? e.message : e)) })
|
|
705
749
|
}
|
|
706
750
|
|
|
751
|
+
// 可抄样式的参照行。浏览器侧是「设置」:triggerRow(定高的横排容器)套 trigger
|
|
752
|
+
// (flex:1 1 0%)两层,只抄里层放进纵向堆叠的插槽,basis 0% 会变成高度、按钮塌成
|
|
753
|
+
// 一行字,所以两层都抄。dsh desktop 的设置入口在原生壳里(⌘,),侧栏里没有
|
|
754
|
+
// settings.trigger,但 footer 底下那一行(登录后是账号行,没登录是「更多」)是同一
|
|
755
|
+
// 族的 triggerRow,改抄它。
|
|
756
|
+
function hostsReference() {
|
|
757
|
+
var settings = document.querySelector('[data-slot="settings.trigger"]')
|
|
758
|
+
var trigger = settings && settings.closest ? settings.closest('button') : null
|
|
759
|
+
if (trigger && trigger.parentElement) return { row: trigger.parentElement, trigger: trigger, browser: true }
|
|
760
|
+
var row = document.querySelector('[class*="settingsArea"] [class*="triggerRow"]')
|
|
761
|
+
var button = row && row.querySelector('button')
|
|
762
|
+
if (row && button) return { row: row, trigger: button, browser: false }
|
|
763
|
+
return null
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// 同一行里 padding + 图标 + gap 决定文字从哪开始,所以图标与参照行一样宽,两行的
|
|
767
|
+
// 图标列和文字列才对齐。桌面端那一行会随登录状态换形态(没登录是 14px 字形,登录
|
|
768
|
+
// 后是 24px 头像),宽度因此是量参照行量出来的,不写死;量不到就退回 16。
|
|
769
|
+
function referenceIconWidth(trigger) {
|
|
770
|
+
var art = trigger.querySelector('svg, img')
|
|
771
|
+
var width = art ? Math.round(art.getBoundingClientRect().width) : 0
|
|
772
|
+
return width >= 12 && width <= 24 ? width : 16
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// dsh 自己那套小图标的画法:16 视图框、1.3px 描边,字形落在 1.5~14.5 的方框里
|
|
776
|
+
function phoneIcon(size) {
|
|
777
|
+
return '<svg width="' + size + '" height="' + size + '" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.3" aria-hidden="true">'
|
|
778
|
+
+ '<rect x="5.15" y="2.15" width="5.7" height="11.7" rx="1.6"/><path d="M7.1 11.6h1.8" stroke-linecap="round"/></svg>'
|
|
779
|
+
}
|
|
780
|
+
|
|
707
781
|
function mountHostsTrigger() {
|
|
708
782
|
var slot = document.querySelector('[data-slot="sidebar.footer.action"]')
|
|
709
783
|
if (!slot || slot.querySelector('[data-dsh-tether="hosts-trigger"]')) return
|
|
710
|
-
var
|
|
711
|
-
var reference = settings && settings.closest ? settings.closest('button') : null
|
|
712
|
-
if (!reference || !reference.parentElement) return
|
|
713
|
-
// 「设置」是 triggerRow(定高的横排容器)套 trigger(flex:1 1 0%)两层。只抄
|
|
714
|
-
// 里层放进纵向堆叠的插槽,basis 0% 会变成高度,按钮塌成一行字;两层都抄,
|
|
715
|
-
// 几何才在宽态 / rail 态都与「设置」一致。
|
|
784
|
+
var reference = hostsReference()
|
|
716
785
|
var row = document.createElement('div')
|
|
717
786
|
row.setAttribute('data-dsh-tether', 'hosts-row')
|
|
718
|
-
row.className = reference.parentElement.className
|
|
719
787
|
var button = document.createElement('button')
|
|
720
788
|
button.type = 'button'
|
|
721
789
|
button.setAttribute('data-dsh-tether', 'hosts-trigger')
|
|
722
790
|
var label = framed ? tx('我的电脑', 'My computers') : tx('连接手机', 'Connect a phone')
|
|
723
791
|
button.setAttribute('aria-label', label)
|
|
724
|
-
//
|
|
725
|
-
|
|
726
|
-
|
|
792
|
+
// 浏览器侧沿用原来那个 18px 图标,一字不改;抄不到「设置」时按参照行的尺寸画
|
|
793
|
+
var icon = reference === null || reference.browser
|
|
794
|
+
? '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="7" y="2" width="10" height="20" rx="2"/><path d="M11 18h2"/></svg>'
|
|
795
|
+
: phoneIcon(referenceIconWidth(reference.trigger))
|
|
796
|
+
button.innerHTML = icon + '<span data-dsh-tether="hosts-label">' + label + '</span>'
|
|
727
797
|
button.addEventListener('click', function () {
|
|
728
798
|
if (framed) window.parent.postMessage({ type: 'dsh-tether:open-hosts' }, '*')
|
|
729
799
|
else showPairing()
|
|
730
800
|
})
|
|
731
801
|
row.append(button)
|
|
732
802
|
slot.append(row)
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
803
|
+
if (reference === null) {
|
|
804
|
+
ensureFallbackStyle()
|
|
805
|
+
} else {
|
|
806
|
+
var labelSpan = button.querySelector('[data-dsh-tether="hosts-label"]')
|
|
807
|
+
var referenceLabel = reference.trigger.querySelector('span')
|
|
808
|
+
// 类名带内容哈希,照抄参照行当前的,外观自然跟随 dsh 自己的构建。侧栏收起/
|
|
809
|
+
// 展开、登录状态变化时它会换类名,也会换图标尺寸,跟着同步
|
|
810
|
+
var sync = function () {
|
|
811
|
+
row.className = reference.row.className
|
|
812
|
+
button.className = reference.trigger.className
|
|
813
|
+
if (reference.browser) return
|
|
814
|
+
if (referenceLabel) labelSpan.className = referenceLabel.className
|
|
815
|
+
// 账号行的形态写在自己的 data-* 上(没登录 32px 矮一截,收起时 36×36 居中),
|
|
816
|
+
// 光抄类名只拿到组件的默认形态,会比隔壁高一截。属性名按前缀抄,不写死是哪几个
|
|
817
|
+
for (var i = button.attributes.length - 1; i >= 0; i--) {
|
|
818
|
+
var own = button.attributes[i].name
|
|
819
|
+
if (own.indexOf('data-') === 0 && own !== 'data-dsh-tether') button.removeAttribute(own)
|
|
820
|
+
}
|
|
821
|
+
for (var j = 0; j < reference.trigger.attributes.length; j++) {
|
|
822
|
+
var attr = reference.trigger.attributes[j]
|
|
823
|
+
if (attr.name.indexOf('data-') === 0 && attr.name !== 'data-dsh-tether') button.setAttribute(attr.name, attr.value)
|
|
824
|
+
}
|
|
825
|
+
var svg = button.querySelector('svg')
|
|
826
|
+
var width = referenceIconWidth(reference.trigger)
|
|
827
|
+
if (svg) { svg.setAttribute('width', width); svg.setAttribute('height', width) }
|
|
828
|
+
}
|
|
829
|
+
sync()
|
|
830
|
+
// 浏览器侧只有类名会变,照旧只盯 class;桌面端那一行换形态是改 data-*,得全盯
|
|
831
|
+
new MutationObserver(sync).observe(reference.row, reference.browser
|
|
832
|
+
? { attributes: true, attributeFilter: ['class'], subtree: true }
|
|
833
|
+
: { attributes: true, subtree: true })
|
|
834
|
+
}
|
|
738
835
|
// dsh 的语言能在设置里当场切,这个标签跟着换
|
|
739
836
|
new MutationObserver(function () {
|
|
740
837
|
var next = framed ? tx('我的电脑', 'My computers') : tx('连接手机', 'Connect a phone')
|
|
@@ -743,6 +840,33 @@ function injectNarrowScreenCss(html) {
|
|
|
743
840
|
if (span) span.textContent = next
|
|
744
841
|
}).observe(document.documentElement, { attributes: true, attributeFilter: ['lang'] })
|
|
745
842
|
}
|
|
843
|
+
// 两处参照都抄不到时的兜底。只保证这一行能看、能点:宽度铺满插槽、行高与 dsh 的
|
|
844
|
+
// panelRow 一致,颜色圆角焦点环全取 --dsw-* 变量,不碰任何带内容哈希的类名,也不
|
|
845
|
+
// 去猜隔壁此刻长什么样。抄不到就不挂按钮的话,桌面端等于没有配对入口。
|
|
846
|
+
var fallbackStyleInstalled = false
|
|
847
|
+
function ensureFallbackStyle() {
|
|
848
|
+
if (fallbackStyleInstalled) return
|
|
849
|
+
fallbackStyleInstalled = true
|
|
850
|
+
var style = document.createElement('style')
|
|
851
|
+
style.setAttribute('data-dsh-tether', 'fallback-style')
|
|
852
|
+
style.textContent = [
|
|
853
|
+
'[data-dsh-tether="hosts-row"] { box-sizing: border-box; flex: none; display: flex; align-items: center; width: 100%; }',
|
|
854
|
+
'[data-dsh-tether="hosts-row"] > [data-dsh-tether="hosts-trigger"] {',
|
|
855
|
+
' -webkit-appearance: none; appearance: none; box-sizing: border-box;',
|
|
856
|
+
' flex: 1 1 auto; min-width: 0; display: flex; align-items: center; gap: 8px;',
|
|
857
|
+
' height: 36px; margin: 0; padding: 6px; border: none;',
|
|
858
|
+
' border-radius: var(--dsw-radius-md); background: transparent;',
|
|
859
|
+
' color: var(--dsw-alias-label-primary); font: inherit; font-size: 14px; line-height: 22px;',
|
|
860
|
+
' text-align: left; cursor: pointer; overflow: hidden;',
|
|
861
|
+
'}',
|
|
862
|
+
'[data-dsh-tether="hosts-row"] > [data-dsh-tether="hosts-trigger"]:hover { background: var(--dsw-alias-interactive-bg-hover); }',
|
|
863
|
+
'[data-dsh-tether="hosts-row"] [data-dsh-tether="hosts-label"] { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }',
|
|
864
|
+
'[data-sidebar-collapsed] [data-dsh-tether="hosts-row"] > [data-dsh-tether="hosts-trigger"] { justify-content: center; gap: 0; padding: 0; }',
|
|
865
|
+
'[data-sidebar-collapsed] [data-dsh-tether="hosts-label"] { display: none; }',
|
|
866
|
+
].join(' ')
|
|
867
|
+
document.head.append(style)
|
|
868
|
+
}
|
|
869
|
+
|
|
746
870
|
mountHostsTrigger()
|
|
747
871
|
new MutationObserver(mountHostsTrigger).observe(document.documentElement, { childList: true, subtree: true })
|
|
748
872
|
|
|
@@ -759,9 +883,12 @@ function injectNarrowScreenCss(html) {
|
|
|
759
883
|
.catch(function (e) { viewer(tx('配置文件', 'Config file'), '', String(e && e.message ? e.message : e)) })
|
|
760
884
|
}, true)
|
|
761
885
|
})()`
|
|
886
|
+
|
|
887
|
+
/** 把那两份注入以字符串变换塞进 index.html */
|
|
888
|
+
function injectNarrowScreenCss(html) {
|
|
762
889
|
return html.replace(
|
|
763
890
|
'</head>',
|
|
764
|
-
`<style data-dsh-tether="narrow-screen">${
|
|
891
|
+
`<style data-dsh-tether="narrow-screen">${tetherCss}\n</style><script data-dsh-tether="narrow-screen">${tetherScript}\n</script></head>`,
|
|
765
892
|
)
|
|
766
893
|
}
|
|
767
894
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-tether",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.22",
|
|
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,8 +42,9 @@
|
|
|
42
42
|
},
|
|
43
43
|
"androidRuntime": {
|
|
44
44
|
"node": "24.18.0",
|
|
45
|
-
"dsh": "0.2.
|
|
46
|
-
"koffi": "3.3.0"
|
|
45
|
+
"dsh": "0.2.1-rc.2",
|
|
46
|
+
"koffi": "3.3.0",
|
|
47
|
+
"zod": "4.4.3"
|
|
47
48
|
}
|
|
48
49
|
},
|
|
49
50
|
"peerDependencies": {
|
|
@@ -55,11 +56,11 @@
|
|
|
55
56
|
"@deepseek-ai/dsh-user-approval": { "optional": true }
|
|
56
57
|
},
|
|
57
58
|
"optionalDependencies": {
|
|
58
|
-
"dsh-tether-host-android-arm64": "0.1.
|
|
59
|
-
"dsh-tether-host-darwin-arm64": "0.1.
|
|
60
|
-
"dsh-tether-host-darwin-x64": "0.1.
|
|
61
|
-
"dsh-tether-host-linux-arm64": "0.1.
|
|
62
|
-
"dsh-tether-host-linux-x64": "0.1.
|
|
63
|
-
"dsh-tether-host-win32-x64": "0.1.
|
|
59
|
+
"dsh-tether-host-android-arm64": "0.1.22",
|
|
60
|
+
"dsh-tether-host-darwin-arm64": "0.1.22",
|
|
61
|
+
"dsh-tether-host-darwin-x64": "0.1.22",
|
|
62
|
+
"dsh-tether-host-linux-arm64": "0.1.22",
|
|
63
|
+
"dsh-tether-host-linux-x64": "0.1.22",
|
|
64
|
+
"dsh-tether-host-win32-x64": "0.1.22"
|
|
64
65
|
}
|
|
65
66
|
}
|