dsh-peers-syh 0.7.0 → 0.7.2
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 +12 -0
- package/README.md +37 -0
- package/package.json +1 -1
- package/src/client.js +32 -3
- package/src/index.js +33 -15
package/CHANGELOG.md
CHANGED
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.7.2
|
|
4
|
+
|
|
5
|
+
- README 新增「让 AI 自己装(一句话)」:给出可直接粘给 DSH 的一句话指令 + agent 执行清单(profile 定位、npm 源、dependencies 与 bundles 必须双写、手机端完全访问与同一 Remote 账号)。
|
|
6
|
+
- 无代码改动。
|
|
7
|
+
|
|
8
|
+
## 0.7.1
|
|
9
|
+
|
|
10
|
+
- 修:手机窄屏(约 390px)下 Remote 弹窗排版塌陷——标题被左上角图标压住、主机行状态竖排、设备名被输入框盖住。新增 `@media (max-width:560px)` 补丁:面板行允许换行且输入框窄屏占满一行;Remote 头部标题换行并留出 54px 图标位;分组标题与主机行允许换行、状态列 `flex:0 0 auto`。
|
|
11
|
+
- 修:`peer_hello` 报的是 Client 身份 deviceId,可拨的却是 Host 身份,两者不同。改为优先报 `status().host.deviceId`,并新增 `clientDeviceId` 字段;设备表剔除本机时同时排除两个身份。
|
|
12
|
+
- 修:重启后第一次连对端常报 `REMOTE_PREFLIGHT_FAILED`(remote 侧握手尚未完成),原来只重试 1 次、间隔 1.5s,实测常连败两次。改为 1.5s / 4s / 8s 退避、最多 4 次尝试;该错误保证「没有变更被派发」,重试安全。
|
|
13
|
+
|
|
3
14
|
## 0.7.0
|
|
4
15
|
|
|
16
|
+
- 已发布到 npm:`npm i dsh-peers-syh@0.7.0`(dist-tags `latest`、`beta` 均指向 0.7.0)。
|
|
5
17
|
- 设备表三源合并:服务端设备目录(可拨)+ `status().host.connectedClients`(此刻连入)+ `~/.dsh/remote/servers/*/host/trusted-peers.json`(历史信任),每行标注 `dialable` / `connected` / `trustedAt`。
|
|
6
18
|
- 新增「仅连入」状态;对这类设备调用 `peer_delegate` 报 `PEER_NOT_DIALABLE` 并提示去那台设备打开「允许控制本机」。
|
|
7
19
|
- 名字互报(PEER-HELLO):派活时附带发起方自报名;对端调用 `peer_hello` 把身份行放在回复最后一行,发起端解析后写入 `peers.json` 的 `remoteName`,并从正文里剥掉该行。
|
package/README.md
CHANGED
|
@@ -21,6 +21,26 @@
|
|
|
21
21
|
- 不共享剪贴板、不投屏、不做双端同时编辑同一个会话;
|
|
22
22
|
- 不替代 Remote 的审批:对端需要人点确认时,只有人能点。
|
|
23
23
|
|
|
24
|
+
## 让 AI 自己装(一句话)
|
|
25
|
+
|
|
26
|
+
不用记命令:把下面这段整句发给你的 DSH(桌面端或 dsh-Go 手机端),它自己会装。
|
|
27
|
+
|
|
28
|
+
> 按 https://github.com/xuanzhihang-s/dsh-peers-syh 的 README「让 AI 自己装」一节,帮我把 dsh-peers-syh 装到本机并配好;需要登录、扫码、输密码或点授权的时候停下来等我。
|
|
29
|
+
|
|
30
|
+
给 agent 的执行清单:
|
|
31
|
+
|
|
32
|
+
1. 确认已装 `ds-harness-remote` 并登录同一账号(Remote 弹窗里能看到对方设备)。
|
|
33
|
+
2. 定位当前 profile 目录:`~/.dsh/profiles/<profile>`(桌面端是 `desktop`,dsh-Go 手机端是 `web`)。
|
|
34
|
+
3. 在该目录执行:`npm i dsh-peers-syh@0.7.2 --registry=https://registry.npmjs.org`。
|
|
35
|
+
4. 把 `"dsh-peers-syh"` **同时**写进该 profile 的 `package.json` 的 `dependencies` 和 `dsh.profile.bundles`。只写 bundles 会被 npm/pnpm 当多余包清掉,插件页会显示「异常 + 未安装」。
|
|
36
|
+
5. 重启 DSH(手机端重启 App)。重启后 Remote「远程工作台」弹窗里会出现「设备名称」区块。
|
|
37
|
+
6. 给两台设备起名,然后试一句:让它报告对方的工作目录。
|
|
38
|
+
|
|
39
|
+
手机端两点注意:
|
|
40
|
+
|
|
41
|
+
- 在 dsh-Go 里把权限预设设成**完全访问**,否则 npm 安装与写 profile 会被审批卡住、装不完整。
|
|
42
|
+
- 跨网需要两端登录同一个 Remote 账号(默认服务端 `dsh.r2049.cn`,用知乎账号登录)。
|
|
43
|
+
|
|
24
44
|
## 前置要求
|
|
25
45
|
|
|
26
46
|
| 项目 | 要求 |
|
|
@@ -40,6 +60,12 @@ dsh plugin add dsh-peers-syh
|
|
|
40
60
|
|
|
41
61
|
从源码 / git 索引源安装的六步说明在仓库根目录的 `FRIENDS.md`。装完需要**重启 DSH** 才会加载。
|
|
42
62
|
|
|
63
|
+
> 国内镜像(`registry.npmmirror.com`)同步新版本有延迟,刚发布时可能报 `ETARGET: no matching version`。先查公共源确认版本存在,再指定源安装:
|
|
64
|
+
>
|
|
65
|
+
> ```bash
|
|
66
|
+
> npm i dsh-peers-syh --registry=https://registry.npmjs.org
|
|
67
|
+
> ```
|
|
68
|
+
|
|
43
69
|
**两侧都装**才有双向能力:只装一侧时,只有装的那一侧能发起派活,对端只能在被派活时执行;反方向要等它自己装了插件、并且开了 Remote 的「允许控制本机」。
|
|
44
70
|
|
|
45
71
|
## 用法
|
|
@@ -107,6 +133,17 @@ agent → peer_delegate { peer: "小蜜", prompt: "列出 /sdcard/DCIM/Camera
|
|
|
107
133
|
|
|
108
134
|
宿主侧不往 `index.html` 注入任何脚本(0.3.x 的实现已移除),UI 全部走 `dsh.client` 声明 + `exports["./client"]` 的浏览器模块;本插件不注册任何 `shell.overlay` 槽,也不改 Remote 的源码。
|
|
109
135
|
|
|
136
|
+
### 窄屏(手机)适配(0.7.1 起)
|
|
137
|
+
|
|
138
|
+
手机上(约 390px CSS 宽)原来的排版会塌:标题被左上角图标压住、主机行的「在线」被挤成竖排两字、设备行的输入框把名字盖住。0.7.1 加了一段 `@media (max-width:560px)` 的补丁,全部选择器都锚在 `.dshRemotePage` 与 `.dshPeers*` 上:
|
|
139
|
+
|
|
140
|
+
- 面板自己的行允许换行,输入框在窄屏占满一行(`flex:1 1 100%`)。
|
|
141
|
+
- Remote 弹窗头部让标题/副标题换行而不是省略号截断,并给左上角图标留出 54px 内边距;窄屏隐藏「Star 项目」装饰链接。
|
|
142
|
+
- 分组标题(「主机」)可换行、「允许控制本机」开关按内容宽固定,不再把「主机」压成竖排。
|
|
143
|
+
- 主机行允许换行,状态列 `flex:0 0 auto; white-space:nowrap`,元信息 `overflow-wrap:anywhere`。
|
|
144
|
+
|
|
145
|
+
这些是**覆盖上游样式**,不改 `ds-harness-remote` 的文件;上游大改类名时会失效(失效表现为回到原排版,不影响功能)。
|
|
146
|
+
|
|
110
147
|
## 工作方式
|
|
111
148
|
|
|
112
149
|
```text
|
package/package.json
CHANGED
package/src/client.js
CHANGED
|
@@ -39,8 +39,8 @@ window.__ModuleLoader__.load({
|
|
|
39
39
|
".dshPeersRefresh:disabled{opacity:.5;cursor:default}",
|
|
40
40
|
".dshPeersHint{margin:0 0 10px;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.4}",
|
|
41
41
|
".dshPeersList{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;border-top:1px solid var(--dsw-alias-border-l2)}",
|
|
42
|
-
".dshPeersRow{display:flex;align-items:center;gap:12px;min-height:52px;padding:8px 4px;border-bottom:1px solid var(--dsw-alias-border-l2);text-align:left}",
|
|
43
|
-
".dshPeersRowMain{min-width:0;flex:1 1
|
|
42
|
+
".dshPeersRow{display:flex;align-items:center;flex-wrap:wrap;gap:8px 12px;min-height:52px;padding:8px 4px;border-bottom:1px solid var(--dsw-alias-border-l2);text-align:left}",
|
|
43
|
+
".dshPeersRowMain{min-width:0;flex:1 1 160px;display:flex;flex-direction:column;gap:3px}",
|
|
44
44
|
".dshPeersRowTop{display:flex;align-items:center;gap:7px;min-width:0}",
|
|
45
45
|
".dshPeersRowName{font-size:13px;font-weight:600;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
|
46
46
|
".dshPeersRowMeta{color:var(--dsw-alias-label-secondary);font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}",
|
|
@@ -48,7 +48,7 @@ window.__ModuleLoader__.load({
|
|
|
48
48
|
".dshPeersBadge{flex:none;padding:1px 6px;border-radius:999px;font-size:10px;line-height:16px;",
|
|
49
49
|
"border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary)}",
|
|
50
50
|
".dshPeersDot[data-off=true]{background:var(--dsw-alias-label-tertiary,#8b949e)}",
|
|
51
|
-
".dshPeersInput{flex:
|
|
51
|
+
".dshPeersInput{flex:1 1 160px;width:auto;min-width:0;max-width:100%;height:32px;padding:0 10px;border-radius:8px;",
|
|
52
52
|
"border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:inherit;font:inherit;text-align:left}",
|
|
53
53
|
".dshPeersInput::placeholder{color:var(--dsw-alias-label-tertiary)}",
|
|
54
54
|
".dshPeersInput:focus{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-1px;border-color:transparent}",
|
|
@@ -66,6 +66,35 @@ window.__ModuleLoader__.load({
|
|
|
66
66
|
".dshPeersNote{margin:8px 0 0;color:var(--dsw-alias-label-secondary);font-size:11px;line-height:1.4}",
|
|
67
67
|
".dshPeersError{margin:8px 0 0;color:var(--dsw-alias-state-danger-primary,#f85149);font-size:11px;line-height:1.4;word-break:break-word}",
|
|
68
68
|
".dshPeersEmpty{margin:0;color:var(--dsw-alias-label-secondary)}",
|
|
69
|
+
// 窄屏(手机)补丁:面板自己的行让输入框换行;Remote 弹窗的样式来自上游
|
|
70
|
+
// ds-harness-remote,本插件不改它的文件,只用同名选择器在窄屏下覆盖几何,
|
|
71
|
+
// 让标题/主机行/状态列不再被压成竖排。选择器都锚在 .dshRemotePage 里,
|
|
72
|
+
// 因此只在 Remote 弹窗打开时生效,不污染宿主其它页面。
|
|
73
|
+
"@media (max-width:560px){",
|
|
74
|
+
".dshPeersRow{align-items:flex-start;gap:6px 10px}",
|
|
75
|
+
".dshPeersInput{flex:1 1 100%;width:100%}",
|
|
76
|
+
".dshPeersRowName{white-space:normal;overflow-wrap:anywhere}",
|
|
77
|
+
".dshPeersRowMeta{white-space:normal;overflow-wrap:anywhere}",
|
|
78
|
+
".dshPeersPanelHead{flex-wrap:wrap;gap:6px 12px}",
|
|
79
|
+
".dshPeersFooterActions{gap:8px}",
|
|
80
|
+
".dshRemotePage .dshRemotePageHeader{flex-wrap:wrap;align-items:flex-start;gap:8px 10px;padding:12px 12px 12px 54px}",
|
|
81
|
+
".dshRemotePage .dshRemotePageIntro{flex:1 1 140px}",
|
|
82
|
+
".dshRemotePage .dshRemotePageTitleRow{flex-wrap:wrap;gap:6px 8px}",
|
|
83
|
+
".dshRemotePage .dshRemotePageHeader strong{white-space:normal;overflow:visible;font-size:16px;line-height:1.35}",
|
|
84
|
+
".dshRemotePage .dshRemotePageHeader p{white-space:normal;overflow:visible;max-width:none;font-size:12px;line-height:1.4}",
|
|
85
|
+
".dshRemotePage .dshRemoteStarLink{display:none}",
|
|
86
|
+
".dshRemotePage .dshRemotePageBody{padding:16px 12px;gap:16px}",
|
|
87
|
+
".dshRemotePage .dshRemoteSectionHeading{flex-wrap:wrap;gap:6px 10px}",
|
|
88
|
+
".dshRemotePage .dshRemoteSectionTitle{flex:1 1 auto;min-width:0;flex-wrap:wrap;gap:6px 10px}",
|
|
89
|
+
".dshRemotePage .dshRemoteHostControlToggle{flex:0 0 auto}",
|
|
90
|
+
".dshRemotePage .dshRemoteConnectedMenu{flex:0 0 auto;margin-right:0}",
|
|
91
|
+
".dshRemotePage .dshRemoteSectionTitle>strong{white-space:nowrap}",
|
|
92
|
+
".dshRemotePage .dshRemoteSectionActions{flex:1 1 100%;justify-content:space-between;gap:8px}",
|
|
93
|
+
".dshRemotePage .dshRemoteHostList>button{flex-wrap:wrap;gap:2px 10px;min-height:52px;padding:10px 2px}",
|
|
94
|
+
".dshRemotePage .dshRemoteHostList>button>span{flex:1 1 auto;min-width:0}",
|
|
95
|
+
".dshRemotePage .dshRemoteHostList>button>span small{white-space:normal;overflow-wrap:anywhere;font-size:11px}",
|
|
96
|
+
".dshRemotePage .dshRemoteHostList>button>small{flex:0 0 auto;white-space:nowrap;font-size:12px}",
|
|
97
|
+
"}",
|
|
69
98
|
].join("");
|
|
70
99
|
|
|
71
100
|
function installStyle() {
|
package/src/index.js
CHANGED
|
@@ -29,16 +29,22 @@ async function log(line) {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
// 重启后第一次连对端时,remote 端可能还没走完握手,会让 create/send 报 REMOTE_PREFLIGHT_FAILED;
|
|
32
|
-
// 该错误保证「没有任何变更被派发」,因此换新的 operationId
|
|
32
|
+
// 该错误保证「没有任何变更被派发」,因此换新的 operationId 重试是安全的。
|
|
33
|
+
// 握手有时要 10 秒以上,单次重试不够:按 1.5s / 4s / 8s 退避,最多 4 次尝试。
|
|
34
|
+
const PREFLIGHT_BACKOFF = [1500, 4000, 8000]
|
|
33
35
|
async function withPreflightRetry(run) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
36
|
+
let attempt = 0
|
|
37
|
+
for (;;) {
|
|
38
|
+
try {
|
|
39
|
+
return await run()
|
|
40
|
+
} catch (error) {
|
|
41
|
+
const message = String((error && error.message) || error)
|
|
42
|
+
if (!message.includes('REMOTE_PREFLIGHT_FAILED') || attempt >= PREFLIGHT_BACKOFF.length) throw error
|
|
43
|
+
const wait = PREFLIGHT_BACKOFF[attempt]
|
|
44
|
+
attempt += 1
|
|
45
|
+
await log('preflight failed, retry ' + attempt + ' in ' + wait + 'ms: ' + message.slice(0, 160))
|
|
46
|
+
await sleep(wait)
|
|
47
|
+
}
|
|
42
48
|
}
|
|
43
49
|
}
|
|
44
50
|
|
|
@@ -279,17 +285,22 @@ function jsonOutput(schema) {
|
|
|
279
285
|
|
|
280
286
|
// 面板里的「本机」行:显示 Remote 当前生效的身份名,以及配置里待生效的名字。
|
|
281
287
|
async function selfInfo(ctx, store) {
|
|
288
|
+
// status().deviceId 是「Client 身份」,status().host.deviceId 才是「Host 身份」;
|
|
289
|
+
// 对端拨过来用的是 Host 身份,所以对外报名字时优先用 Host id。
|
|
282
290
|
let deviceId = null
|
|
291
|
+
let clientDeviceId = null
|
|
283
292
|
let effective = null
|
|
284
293
|
try {
|
|
285
294
|
const client = ctx.get('dshRemoteClient')
|
|
286
295
|
const status = client && typeof client.status === 'function' ? client.status() : null
|
|
287
|
-
|
|
296
|
+
clientDeviceId = (status && status.deviceId) || null
|
|
297
|
+
deviceId = (status && status.host && status.host.deviceId) || clientDeviceId
|
|
288
298
|
effective = (status && status.deviceName) || null
|
|
289
299
|
} catch {}
|
|
290
300
|
const configured = await readConfiguredDeviceName()
|
|
291
301
|
return {
|
|
292
302
|
deviceId,
|
|
303
|
+
clientDeviceId,
|
|
293
304
|
platform: process.platform,
|
|
294
305
|
name: effective || store.selfName || configured || hostname(),
|
|
295
306
|
configuredName: configured || null,
|
|
@@ -403,11 +414,12 @@ async function rosterOf(ctx, store, signal) {
|
|
|
403
414
|
if (!entry.name) entry.name = peer.name || null
|
|
404
415
|
if (!entry.platform) entry.platform = peer.platform || null
|
|
405
416
|
}
|
|
406
|
-
|
|
417
|
+
const selfIds = new Set()
|
|
407
418
|
try {
|
|
408
419
|
const client = ctx.get('dshRemoteClient')
|
|
409
420
|
const status = client && typeof client.status === 'function' ? client.status() : null
|
|
410
|
-
|
|
421
|
+
if (status && status.deviceId) selfIds.add(status.deviceId)
|
|
422
|
+
if (status && status.host && status.host.deviceId) selfIds.add(status.host.deviceId)
|
|
411
423
|
} catch {}
|
|
412
424
|
const rows = []
|
|
413
425
|
const add = (hostId, name, platform, flags, trustedAt, mode) => {
|
|
@@ -430,12 +442,12 @@ async function rosterOf(ctx, store, signal) {
|
|
|
430
442
|
})
|
|
431
443
|
}
|
|
432
444
|
for (const entry of table.values()) {
|
|
433
|
-
if (
|
|
445
|
+
if (selfIds.has(entry.hostId)) continue
|
|
434
446
|
add(entry.hostId, entry.name, entry.platform, entry, entry.trustedAt, entry.mode)
|
|
435
447
|
}
|
|
436
448
|
// 只剩本地别名的历史条目(换了账号、设备下线)也保留,否则用户起的名字会凭空消失。
|
|
437
449
|
for (const hostId of Object.keys(store.peers)) {
|
|
438
|
-
if (
|
|
450
|
+
if (selfIds.has(hostId)) continue
|
|
439
451
|
if (rows.some((row) => row.hostId === hostId)) continue
|
|
440
452
|
const record = store.peers[hostId] || {}
|
|
441
453
|
if (!record.alias && !record.remoteName) continue
|
|
@@ -1070,7 +1082,13 @@ export function apply(ctx, config = {}) {
|
|
|
1070
1082
|
const store = await loadStore()
|
|
1071
1083
|
const info = await selfInfo(ctx, store)
|
|
1072
1084
|
const line = helloLine({ name: info.name, deviceId: info.deviceId, platform: info.platform })
|
|
1073
|
-
return sanitize({
|
|
1085
|
+
return sanitize({
|
|
1086
|
+
name: info.name,
|
|
1087
|
+
deviceId: info.deviceId || '',
|
|
1088
|
+
clientDeviceId: info.clientDeviceId || '',
|
|
1089
|
+
platform: info.platform,
|
|
1090
|
+
line,
|
|
1091
|
+
})
|
|
1074
1092
|
},
|
|
1075
1093
|
})
|
|
1076
1094
|
|