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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-peers-syh",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "description": "给每台 DSH 设备起名(可拨 / 已连接 / 仅连入三源合并),名字跨设备互报,并让一个 agent 直接给另一台设备上的 agent 派活(基于 ds-harness-remote);面板长在 Remote 的「远程工作台」弹窗里。",
5
5
  "keywords": [
6
6
  "dsh",
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 auto;display:flex;flex-direction:column;gap:3px}",
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:0 0 auto;width:160px;height:32px;padding:0 10px;border-radius:8px;",
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
- try {
35
- return await run()
36
- } catch (error) {
37
- const message = String((error && error.message) || error)
38
- if (!message.includes('REMOTE_PREFLIGHT_FAILED')) throw error
39
- await log('preflight failed, retrying: ' + message.slice(0, 160))
40
- await sleep(1500)
41
- return run()
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
- deviceId = (status && status.deviceId) || null
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
- let selfId = null
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
- selfId = (status && status.deviceId) || null
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 (selfId && entry.hostId === selfId) continue
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 (selfId && hostId === selfId) continue
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({ name: info.name, deviceId: info.deviceId || '', platform: info.platform, line })
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