dsh-crossterm 0.7.1 → 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,5 +1,29 @@
1
1
  # dsh-crossterm 变更记录
2
2
 
3
+ ## 0.7.2 — 2026-10-19
4
+
5
+ ### 停用云端老文件夹:设备心跳搬进账号共享目录
6
+
7
+ - 背景(User said, paraphrased):查清云盘老目录 `/dsh云盘/crossterm/bus` 里没有必须迁移的数据后,停用老文件夹。
8
+ - 老目录盘点:17 个目录 / 412 个文件 / 679,619 B,其中 200 个 `.md` 是历史邮件、200 个 `.tmp` 是原子写残留、
9
+ 12 个 `json` 是状态镜像;本机 `F:\dsh\001X\crossterm-data\mail` 有 253 个(342,402 B)、`inbox` 306 个(537,410 B),
10
+ 云端那份只是子集。唯一可能陌生的是发往本机的 2 封 `pending`,内容是 10-19 00:52 的跨终端验收测试信。
11
+ - 新文件 `lib/cloud/presence.js`:`DEVICE_FILE='_device.json'`、`makePresence()`、`writePresence({pan,layout,info})`
12
+ (写到 `shared/<deviceId>/_device.json`)、`listPresences({pan,layout,onlineMs=3分钟})`(遍历共享目录、按 `at` 倒序、标 `online`)。
13
+ - `lib/cloud/home.js`:新增 `deviceInfo` 参数(实时快照:版本/会话数/链/pid)、`presenceNow()`、`devices()`;
14
+ `status().presence = { at, error, intervalMs }`;`start()` 起 `presenceIntervalMs`(默认 60 s)定时器,启动 5 s 先报一次。
15
+ - `lib/index.js`:`enableCloud()` 里只有 `cfg.busSyncEnabled === true` 才创建老 `createCloud` 镜像;云端目录(home)
16
+ 一律启用。`term_cloud` 的 `once` 在镜像停用后明确回「总线镜像已停用」,status 兜底对象带 `design:'cloud-home'` 与 `home`。
17
+ - `lib/bus.js` DEFAULTS:新增 `busSyncEnabled: false`(老镜像默认关闭,需要时在 config.json 里开回来)与 `presenceIntervalMs: 60000`。
18
+ - `lib/web.js`:`/crossterm/api/devices` 合并两份来源 —— 老目录 `listCloudDevices(cloudRoot)`(给 `<0.7.2` 的设备)
19
+ + 新布局 `listPresences()`(`source: 'layout'`,同 nodeId 时新布局优先);`status.cloud` 增 `design`,
20
+ 非镜像模式下 `root` 显示账号目录、`intervalMs` 显示心跳间隔。
21
+ - 探针 `probe-presence.mjs`:16 项全 PASS(心跳字段/落点/两台设备在线判定/排序/跳过无心跳目录/无布局返回 no-layout/
22
+ 上传失败抛错/home 集成:`presenceNow`、`status.presence`、`devices`、`start` 后间隔)。
23
+ - 兼容:还跑 `<0.7.2` 的设备仍然读老目录里的邮件与 presence,所以老目录**只停写、不删除**;
24
+ 等所有设备升到 0.7.2 之后再整体删掉。
25
+ - 要不要跟着动手:**要** —— 别的机器升级到 0.7.2 后云端设备列表才来自新布局;老目录可在全部升级后删除。
26
+
3
27
  ## 0.7.1 — 2026-10-19
4
28
 
5
29
  ### 「更新」按钮在别的机器上装不上:三级兜底 + 全步骤回执
package/README.md CHANGED
@@ -28,14 +28,15 @@ A 终端看不到 B 终端里有哪个会话、在干什么,也没法把一条
28
28
  > `locatePkgJson()` 对子路径行返回 undefined),而插件页顶部标题与这一行共用同一份 `locale/zh.json` 的 meta,
29
29
  > 所以它单独做总开关,标题带「(总开关)」;卡片头用 `shortTitle` 显示短名「跨端互通」。
30
30
 
31
- **本版更新(0.7.1)**
32
-
33
- - **修「别的机器点更新装不上」**:更新不再只依赖 PATH 里的 `dsh`,改成三级兜底 ——
34
- ① PATH 里的 `dsh`;② DSH 自带 CLI(`Electron --expose-internals <安装目录>\resources\app.asar\dsh\node_modules\@deepseek-ai\dsh-desktop-host\lib\cli.js`);
35
- ③ 直接调自带 pnpm(`<DSH_HOME>\dsh-runtimes\...\dependencies\pnpm` 或 `<安装目录>\resources\runtime\pnpm`)。
36
- - 装完会读 `<profile>\node_modules\dsh-crossterm\package.json` **核对真实版本**,分别提示「已更新到 X」「已经是最新」「更新没成功」。
37
- - 失败时卡片直接把每步的退出码、耗时、输出尾部显示出来;全过程写 `<总线目录>\logs\update.log`。
38
- - 顺带补上 `status.home.layout` 里缺的 `rulesVersion` / `rulesFailed` / `accountSource`。
31
+ **本版更新(0.7.2)**
32
+
33
+ - **云端老文件夹停用**:不再把本机总线镜像到 `/dsh云盘/crossterm/bus`(老目录里 12 个 json 是状态镜像、
34
+ 200 个 md 是本机邮件的子集、200 个 tmp 是原子写残留,没有必须迁移的数据)。
35
+ - **设备心跳搬进账号共享目录**:`<云端根>/<账号>/shared/<设备>/_device.json`,每 60 s 写一次;
36
+ 「云端设备」列表改为新布局优先,老目录只作 `<0.7.2` 设备的兼容来源。
37
+ - 老镜像需要时可在 `config.json` 里把 `busSyncEnabled` 设 true 开回来;`term_cloud` 的 `once` 在镜像停用时给出说明。
38
+ - 老目录**只停写、不删除**:还跑 `<0.7.2` 的设备仍从那里读邮件与 presence,等全部升级后再整体删掉。
39
+ - 要不要跟着动手:**要** —— 别的机器升到 0.7.2 后,云端设备列表才会来自新布局。
39
40
 
40
41
  ## 一、安装与卸载
41
42
 
package/lib/bus.js CHANGED
@@ -22,7 +22,7 @@ import os from 'node:os'
22
22
  import path from 'node:path'
23
23
  import crypto from 'node:crypto'
24
24
 
25
- export const VERSION = '0.7.1'
25
+ export const VERSION = '0.7.2'
26
26
 
27
27
  /** DSH 的家目录:与宿主其它状态(sessions / profiles)同一个根,多终端天然共享 */
28
28
  export function dshHome() {
@@ -61,6 +61,9 @@ const DEFAULTS = {
61
61
  inboxRoot: '',
62
62
  // 云总线(百度网盘)同步:默认关闭,开启后插件自己按 cloudIntervalMs 往返镜像。
63
63
  cloudEnabled: false,
64
+ // 0.7.2 起老的总线镜像(cloudRoot 那一套)默认不开,云端只用「账号/设备/共享收件箱」目录。
65
+ // 还跑 <0.7.2 的设备仍会把状态写进 cloudRoot,本机用只读方式兼容它。
66
+ busSyncEnabled: false,
64
67
  cloudRoot: '/dsh云盘/crossterm/bus',
65
68
  cloudIntervalMs: 4000,
66
69
  // 云端目录(0.3.39 起):<cloudHome>/<账号>/<设备>/ + <账号>/shared/<设备>/inbox
@@ -68,6 +71,7 @@ const DEFAULTS = {
68
71
  cloudAccount: '',
69
72
  relayIntervalMs: 20000,
70
73
  backupIntervalMs: 300000,
74
+ presenceIntervalMs: 60000,
71
75
  backupSessionsPerRun: 20,
72
76
  backupAttachmentsPerRun: 20,
73
77
  attachmentBackupEnabled: true,
package/lib/cloud/home.js CHANGED
@@ -4,19 +4,21 @@ import fs from 'node:fs'
4
4
  import path from 'node:path'
5
5
  import { ensureLayout, readLayoutCache, DEFAULT_HOME } from './layout.js'
6
6
  import { createRelay } from './relay.js'
7
+ import { writePresence, listPresences } from './presence.js'
7
8
  import { backupSessions, backupAttachments, backupSummary } from './session-backup.js'
8
9
 
9
10
  /**
10
11
  * @param {object} opts
11
12
  * @param {object} opts.cfg 插件配置(readConfig 结果)
12
13
  * @param {object} opts.device { nodeId, name, version, platform, sessions }
14
+ * @param {Function} [opts.deviceInfo] 返回当前设备快照(版本/会话数/链),心跳用;不传就用 device
13
15
  * @param {object} opts.pan 云盘客户端
14
16
  * @param {string} opts.busDir 总线根(<DSH_HOME>/crossterm)
15
17
  * @param {Function} opts.deliver async ({ to, envelope, files }) => { ok, sessionId, reason }
16
18
  * @param {Function} [opts.names] 返回 { <会话短号>: { name } },给 _session.json 补显示名
17
19
  * @param {Function} [opts.log]
18
20
  */
19
- export function createCloudHome({ cfg, device, pan, busDir, deliver, names, log = () => {} }) {
21
+ export function createCloudHome({ cfg, device, deviceInfo, pan, busDir, deliver, names, log = () => {} }) {
20
22
  const home = path.dirname(busDir)
21
23
  const root = path.join(busDir, 'cloud')
22
24
  fs.mkdirSync(root, { recursive: true })
@@ -33,6 +35,9 @@ export function createCloudHome({ cfg, device, pan, busDir, deliver, names, log
33
35
  let backupBusy = null
34
36
  let relayTimer = 0
35
37
  let backupTimer = 0
38
+ let presenceTimer = 0
39
+ let presenceAt = 0
40
+ let presenceError = ''
36
41
 
37
42
  const relay = createRelay({
38
43
  pan,
@@ -114,6 +119,44 @@ export function createCloudHome({ cfg, device, pan, busDir, deliver, names, log
114
119
  return relay.sendEnvelope(opts)
115
120
  }
116
121
 
122
+ /** 把本机状态写进共享目录(0.7.2:取代云端老目录的 presence 镜像) */
123
+ async function presenceNow() {
124
+ try {
125
+ const layout = await getLayout(false)
126
+ if (!layout || !layout.paths) return { ok: false, reason: 'no-layout' }
127
+ const extra = typeof deviceInfo === 'function' ? deviceInfo() || {} : {}
128
+ const ret = await writePresence({
129
+ pan,
130
+ layout,
131
+ info: {
132
+ nodeId: device.nodeId,
133
+ name: extra.name || device.name,
134
+ hostname: extra.hostname || device.hostname || '',
135
+ version: extra.version || device.version,
136
+ platform: extra.platform || device.platform,
137
+ pid: extra.pid || device.pid || 0,
138
+ sessions: extra.sessions ?? device.sessions ?? 0,
139
+ chains: extra.chains || [],
140
+ startedAt: extra.startedAt || 0,
141
+ },
142
+ log: (line) => log(line),
143
+ })
144
+ presenceAt = Date.now()
145
+ presenceError = ret.ok ? '' : ret.reason || 'unknown'
146
+ return ret
147
+ } catch (e) {
148
+ presenceError = (e && e.message) || String(e)
149
+ return { ok: false, reason: presenceError }
150
+ }
151
+ }
152
+
153
+ /** 读云端所有设备心跳(新布局;不含老目录) */
154
+ async function devices() {
155
+ const layout = await getLayout(false)
156
+ if (!layout || !layout.paths) return { ok: false, devices: [] }
157
+ return listPresences({ pan, layout, log: (line) => log(line) })
158
+ }
159
+
117
160
  function status() {
118
161
  const layout = readLayoutCache(cacheFile)
119
162
  return {
@@ -132,6 +175,7 @@ export function createCloudHome({ cfg, device, pan, busDir, deliver, names, log
132
175
  : null,
133
176
  relay: { ...relay.stats(), intervalMs: relayTimer },
134
177
  backup: { ...backupSummary(backupState), intervalMs: backupTimer },
178
+ presence: { at: presenceAt, error: presenceError, intervalMs: presenceTimer },
135
179
  roots: { sessions: sessionsRoot, attachments: attachmentsRoot },
136
180
  }
137
181
  }
@@ -141,14 +185,17 @@ export function createCloudHome({ cfg, device, pan, busDir, deliver, names, log
141
185
  started = true
142
186
  relayTimer = Number(cfg.relayIntervalMs || 20000)
143
187
  backupTimer = Number(cfg.backupIntervalMs || 300000)
188
+ presenceTimer = Number(cfg.presenceIntervalMs || 60000)
144
189
  void getLayout(false).catch(() => {})
145
190
  timers.push(setInterval(() => { void relayNow().catch(() => {}) }, relayTimer))
146
191
  timers.push(setInterval(() => { void backupNow().catch(() => {}) }, backupTimer))
192
+ timers.push(setInterval(() => { void presenceNow().catch(() => {}) }, presenceTimer))
147
193
  for (const t of timers) if (t && typeof t.unref === 'function') t.unref()
148
194
  // 启动 8 秒后先跑一轮收信、30 秒后跑第一轮备份(别和宿主启动抢资源)
149
195
  setTimeout(() => { void relayNow().catch(() => {}) }, 8000)
150
196
  setTimeout(() => { void backupNow().catch(() => {}) }, 30000)
151
- log(`云端目录就绪:relay ${relayTimer}ms / backup ${backupTimer}ms(根 ${cfg.cloudHome || DEFAULT_HOME})`)
197
+ setTimeout(() => { void presenceNow().catch(() => {}) }, 5000)
198
+ log(`云端目录就绪:relay ${relayTimer}ms / backup ${backupTimer}ms / presence ${presenceTimer}ms(根 ${cfg.cloudHome || DEFAULT_HOME})`)
152
199
  }
153
200
 
154
201
  function stop() {
@@ -157,5 +204,5 @@ export function createCloudHome({ cfg, device, pan, busDir, deliver, names, log
157
204
  timers = []
158
205
  }
159
206
 
160
- return { start, stop, status, getLayout, backupNow, relayNow, send, cacheFile, relayDir }
207
+ return { start, stop, status, getLayout, backupNow, relayNow, send, presenceNow, devices, cacheFile, relayDir }
161
208
  }
@@ -0,0 +1,71 @@
1
+ // 新布局下的设备心跳:<accountDir>/shared/<deviceId>/_device.json
2
+ //
3
+ // 为什么要它:0.7.2 起不再把本地总线镜像到云端老目录(/dsh云盘/crossterm/bus),
4
+ // 但「云端设备」列表得继续有数据 —— 于是每台设备把自己的状态写进账号共享目录。
5
+ // 老目录仍在读(给还跑旧版本的设备留兼容),只是不再写。
6
+
7
+ export const DEVICE_FILE = '_device.json'
8
+
9
+ /** 组装一份心跳内容(全字段可 JSON 序列化) */
10
+ export function makePresence({
11
+ nodeId = '',
12
+ name = '',
13
+ hostname = '',
14
+ version = '',
15
+ platform = '',
16
+ pid = 0,
17
+ sessions = 0,
18
+ chains = [],
19
+ startedAt = 0,
20
+ at = Date.now(),
21
+ } = {}) {
22
+ return {
23
+ nodeId,
24
+ name,
25
+ hostname,
26
+ version,
27
+ platform,
28
+ pid,
29
+ sessions,
30
+ chains: Array.isArray(chains) ? chains : [],
31
+ startedAt,
32
+ at,
33
+ }
34
+ }
35
+
36
+ /** 把心跳写进共享目录(覆盖自己那份) */
37
+ export async function writePresence({ pan, layout, info, log = () => {} }) {
38
+ const paths = layout && layout.paths
39
+ if (!paths || !paths.deviceRoot) return { ok: false, reason: 'no-layout' }
40
+ const remote = `${paths.deviceRoot}/${DEVICE_FILE}`
41
+ const payload = makePresence(info)
42
+ const buf = Buffer.from(JSON.stringify(payload, null, 2) + '\n', 'utf8')
43
+ await pan.putBuffer(buf, remote)
44
+ log(`presence: 已上报 ${payload.name || payload.nodeId} v${payload.version} → ${remote}`)
45
+ return { ok: true, remote, payload }
46
+ }
47
+
48
+ /** 列出所有设备的共享目录,逐个取 _device.json */
49
+ export async function listPresences({ pan, layout, onlineMs = 3 * 60 * 1000, now = Date.now(), log = () => {} }) {
50
+ const paths = layout && layout.paths
51
+ if (!paths || !paths.sharedDir) return { ok: false, devices: [], files: 0, reason: 'no-layout' }
52
+ const list = await pan.lsAll(paths.sharedDir)
53
+ const entries = (list && list.list) || []
54
+ const devices = []
55
+ let files = 0
56
+ for (const entry of entries) {
57
+ if (entry.isdir !== 1) continue
58
+ const remote = `${paths.sharedDir}/${entry.server_filename}/${DEVICE_FILE}`
59
+ try {
60
+ const got = await pan.getBuffer(remote)
61
+ const info = JSON.parse(got.buffer.toString('utf8'))
62
+ devices.push({ ...info, online: now - Number(info.at || 0) < onlineMs })
63
+ files += 1
64
+ } catch {
65
+ // 目录里还没心跳(或刚建好),跳过
66
+ }
67
+ }
68
+ devices.sort((a, b) => Number(b.at || 0) - Number(a.at || 0))
69
+ log(`presence: 共享目录里读到 ${devices.length} 台设备(文件 ${files})`)
70
+ return { ok: true, devices, files, tmpFiles: 0 }
71
+ }
package/lib/index.js CHANGED
@@ -1205,27 +1205,30 @@ class CrossTerm {
1205
1205
  }
1206
1206
  }
1207
1207
 
1208
- /** 启动云总线同步(百度网盘):只有配置里 cloudEnabled 才会跑 */
1208
+ /** 启动云端:0.7.2 起默认只跑「云端目录」(账号/设备/共享收件箱/中转站/备份),
1209
+ * 老的总线镜像(cloudRoot=/dsh云盘/crossterm/bus)需要 busSyncEnabled=true 才跑 */
1209
1210
  enableCloud() {
1210
- if (this.cloud) return this.cloud
1211
1211
  const pan = createPan({
1212
1212
  appFile: this.cfg.baiduAppFile,
1213
1213
  authFile: this.cfg.baiduAuthFile,
1214
1214
  log: (m) => void this.note('cloud: ' + m),
1215
1215
  })
1216
- this.cloud = createCloud({
1217
- busDir: this.bus.root,
1218
- mailDir: this.cfg.mailDir,
1219
- inboxDir: this.cfg.inboxRoot,
1220
- cloudRoot: this.cfg.cloudRoot,
1221
- stateFile: this.cfg.cloudStateFile,
1222
- intervalMs: this.cfg.cloudIntervalMs,
1223
- pan,
1224
- log: (m) => void this.note('cloud: ' + m),
1225
- })
1226
- this.cloud.start()
1227
- void this.note(`cloud sync 启动 → ${this.cfg.cloudRoot}(${this.cfg.cloudIntervalMs}ms)`)
1216
+ if (this.cfg.busSyncEnabled && !this.cloud) {
1217
+ this.cloud = createCloud({
1218
+ busDir: this.bus.root,
1219
+ mailDir: this.cfg.mailDir,
1220
+ inboxDir: this.cfg.inboxRoot,
1221
+ cloudRoot: this.cfg.cloudRoot,
1222
+ stateFile: this.cfg.cloudStateFile,
1223
+ intervalMs: this.cfg.cloudIntervalMs,
1224
+ pan,
1225
+ log: (m) => void this.note('cloud: ' + m),
1226
+ })
1227
+ this.cloud.start()
1228
+ void this.note(`总线镜像启动 → ${this.cfg.cloudRoot}(${this.cfg.cloudIntervalMs}ms)`)
1229
+ }
1228
1230
  this.enableCloudHome(pan)
1231
+ this.pan = pan
1229
1232
  return this.cloud
1230
1233
  }
1231
1234
 
@@ -1246,6 +1249,17 @@ class CrossTerm {
1246
1249
  busDir: this.bus.root,
1247
1250
  deliver: (opts) => this.relayDeliver(opts),
1248
1251
  names: () => this.sessionNames(),
1252
+ deviceInfo: () => ({
1253
+ nodeId: this.nodeId,
1254
+ name: this.cfg.name,
1255
+ hostname: os.hostname(),
1256
+ version: VERSION,
1257
+ platform: process.platform,
1258
+ pid: process.pid,
1259
+ sessions: this.sessions().length,
1260
+ chains: [...new Set([...this.links.values()].map((l) => l.code))],
1261
+ startedAt: this.startedAt || 0,
1262
+ }),
1249
1263
  log: (m) => void this.note('cloud: ' + m),
1250
1264
  })
1251
1265
  this.home.start()
@@ -1803,10 +1817,23 @@ function buildTools(term, agent, part = 'all') {
1803
1817
  }
1804
1818
  else if (action === 'once') {
1805
1819
  const c = term.cloud || term.enableCloud()
1820
+ if (!c) return { ok: false, detail: '总线镜像已停用(0.7.2 起默认只跑云端目录:账号/设备/共享收件箱),改用 action="home" 或 "relay"/"backup"' }
1806
1821
  const result = await c.once()
1807
1822
  return { ok: true, status: c.status(), result }
1808
1823
  }
1809
- const status = term.cloud ? term.cloud.status() : { running: false, intervalMs: term.cfg.cloudIntervalMs, cloudRoot: term.cfg.cloudRoot, cycles: 0, up: 0, down: 0, merged: 0 }
1824
+ const status = term.cloud
1825
+ ? term.cloud.status()
1826
+ : {
1827
+ running: Boolean(term.home),
1828
+ design: term.home ? 'cloud-home' : 'off',
1829
+ intervalMs: term.cfg.cloudIntervalMs,
1830
+ cloudRoot: term.home ? term.cfg.cloudHome || 'dsh' : '',
1831
+ cycles: 0,
1832
+ up: 0,
1833
+ down: 0,
1834
+ merged: 0,
1835
+ home: term.home ? term.home.status() : null,
1836
+ }
1810
1837
  return { ok: true, status }
1811
1838
  } catch (e) {
1812
1839
  return { ok: false, detail: (e && e.message) || String(e) }
package/lib/web.js CHANGED
@@ -8,6 +8,7 @@ import { spawn } from 'node:child_process'
8
8
  import { VERSION, readConfig, writeConfig } from './bus.js'
9
9
  import { createPan } from './cloud/pan.js'
10
10
  import { listCloudDevices, localDevice } from './cloud/devices.js'
11
+ import { listPresences } from './cloud/presence.js'
11
12
  import { runSetup } from './cloud/setup.js'
12
13
 
13
14
  const PKG = 'dsh-crossterm'
@@ -344,7 +345,10 @@ function statusOf(term) {
344
345
  /* ignore */
345
346
  }
346
347
  const cloud = term.cloud
348
+ const home = term.home
349
+ const homeStatus = home ? home.status() : null
347
350
  const provider = cloudProvider(cfg)
351
+ const busMirror = cfg.busSyncEnabled === true
348
352
  return {
349
353
  ok: true,
350
354
  version: VERSION,
@@ -358,14 +362,19 @@ function statusOf(term) {
358
362
  // 没配云盘时不要把默认路径当成「已连接的云端」显示出来
359
363
  configured: provider.configured,
360
364
  enabled: cfg.cloudEnabled === true && provider.configured,
361
- running: provider.configured && Boolean(cloud && cloud.running),
362
- root: provider.configured ? cfg.cloudRoot || '' : '',
363
- intervalMs: cfg.cloudIntervalMs || 0,
365
+ running: provider.configured && Boolean((cloud && cloud.running) || home),
366
+ root: provider.configured
367
+ ? busMirror
368
+ ? cfg.cloudRoot || ''
369
+ : (homeStatus && homeStatus.layout && homeStatus.layout.paths && homeStatus.layout.paths.accountDir) || cfg.cloudRoot || ''
370
+ : '',
371
+ intervalMs: busMirror ? cfg.cloudIntervalMs || 0 : (cfg.presenceIntervalMs || 60000),
372
+ design: busMirror ? 'bus+home' : 'home',
364
373
  provider,
365
374
  },
366
375
  chains,
367
- // 云端目录(0.3.39):账号/角色/设备目录/中转站/会话备份
368
- home: term.home ? term.home.status() : null,
376
+ // 云端目录(0.3.39):账号/角色/设备目录/中转站/会话备份/设备心跳
377
+ home: homeStatus,
369
378
  }
370
379
  }
371
380
 
@@ -482,6 +491,19 @@ export function registerWeb(ctx, term, log) {
482
491
  log: (m) => log('devices: ' + m),
483
492
  })
484
493
  const result = await listCloudDevices(pan, cfg.cloudRoot)
494
+ let fresh = { devices: [], files: 0 }
495
+ try {
496
+ const layout = term.home ? await term.home.getLayout(false) : null
497
+ if (layout) fresh = await listPresences({ pan, layout, log: (m) => log('devices: ' + m) })
498
+ } catch (error) {
499
+ log('devices: 新布局读取失败 ' + String((error && error.message) || error))
500
+ }
501
+ const byId = new Map()
502
+ for (const d of result.devices || []) byId.set(d.nodeId, d)
503
+ for (const d of fresh.devices || []) byId.set(d.nodeId, { ...byId.get(d.nodeId), ...d, source: 'layout' })
504
+ result.devices = [...byId.values()].sort((a, b) => Number(b.lastSeen ? Date.parse(b.lastSeen) : b.at || 0) - Number(a.lastSeen ? Date.parse(a.lastSeen) : a.at || 0))
505
+ delete result.tmpFiles
506
+ result.files = (result.files || 0) + (fresh.files || 0)
485
507
  const me = await localDevice(path.join(cfg.busDir || '', 'presence'), info.nodeId || '')
486
508
  if (me && !result.devices.some((d) => d.nodeId === me.nodeId)) result.devices.unshift(me)
487
509
  json(res, 200, { ...result, configured: true, me: info.nodeId || '' })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-crossterm",
3
- "version": "0.7.1",
3
+ "version": "0.7.2",
4
4
  "description": "跨端互通:同一 DSH 账号下的会话、终端、设备之间互通 —— 特征码链、内联邮件、云端中转站与自动备份",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",