dsh-crossterm 0.3.37 → 0.3.38
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/README.md +13 -0
- package/client/client.js +48 -0
- package/lib/bus.js +59 -6
- package/lib/cloud/devices.js +95 -0
- package/lib/cloud/pan.js +22 -4
- package/lib/cloud/sync.js +5 -1
- package/lib/web.js +39 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -244,6 +244,19 @@ dsh plugin --profile desktop remove dsh-crossterm
|
|
|
244
244
|
|
|
245
245
|
> 插件、npm 包、B 机安装包**都不携带任何凭证**;换机器要各自配自己的。
|
|
246
246
|
|
|
247
|
+
### 看有哪些机器连着(0.3.38)
|
|
248
|
+
|
|
249
|
+
插件页卡片展开后有「云端设备」一行,也可以直接 `GET /crossterm/api/devices`:
|
|
250
|
+
|
|
251
|
+
- 数据来自云端总线的 `presence/*.json`(每台机器 10 秒一次心跳),字段含名字、nodeId、版本、平台、会话数、入链;
|
|
252
|
+
- 最后心跳 3 分钟内的算「在线」,其余显示「N 分钟前」;
|
|
253
|
+
- 本机永远在列表里(标「(本机)」),取自本地 presence,不依赖云端往返;
|
|
254
|
+
- 没配云盘的机器只显示「未连接云盘」,不会假装有别的设备。
|
|
255
|
+
|
|
256
|
+
> 0.3.38 之前这里常常显示「云端还没有其他设备」:云端 `presence/` 里堆了 220 多个 `.tmp`
|
|
257
|
+
> 残骸,而百度网盘列目录一页只给 200 条 —— 真实 json 被挤到了第二页。现在列目录会翻页,
|
|
258
|
+
> 云端也不再上传 `.tmp`。
|
|
259
|
+
|
|
247
260
|
### 手动 / 自动注入开关(三层)
|
|
248
261
|
|
|
249
262
|
| 层 | 开关 | 粒度 |
|
package/client/client.js
CHANGED
|
@@ -131,11 +131,37 @@ window.__ModuleLoader__.load({
|
|
|
131
131
|
);
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
function agoText(ts) {
|
|
135
|
+
if (!ts) return "未知";
|
|
136
|
+
const mins = Math.round((Date.now() - ts) / 60000);
|
|
137
|
+
if (mins <= 0) return "刚刚";
|
|
138
|
+
if (mins < 60) return mins + " 分钟前";
|
|
139
|
+
const hours = Math.round(mins / 60);
|
|
140
|
+
if (hours < 24) return hours + " 小时前";
|
|
141
|
+
return Math.round(hours / 24) + " 天前";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function devicesText(status, devices) {
|
|
145
|
+
if (!(status && status.cloud && status.cloud.configured)) return "未连接云盘";
|
|
146
|
+
if (!devices || devices.loading) return "读取中…";
|
|
147
|
+
if (!devices.ok) return devices.message || "读取失败";
|
|
148
|
+
const list = devices.devices || [];
|
|
149
|
+
if (!list.length) return "云端还没有其他设备";
|
|
150
|
+
return list
|
|
151
|
+
.map((d) => {
|
|
152
|
+
const label = (d.name || d.nodeId || "未知设备") + (d.nodeId === devices.me ? "(本机)" : "");
|
|
153
|
+
const state = d.online ? "在线" : agoText(d.updatedAt);
|
|
154
|
+
return label + " · " + state + (d.version ? " · v" + d.version : "");
|
|
155
|
+
})
|
|
156
|
+
.join(";");
|
|
157
|
+
}
|
|
158
|
+
|
|
134
159
|
function Card() {
|
|
135
160
|
const [open, setOpen] = React.useState(false);
|
|
136
161
|
const [status, setStatus] = React.useState(null);
|
|
137
162
|
const [busy, setBusy] = React.useState("");
|
|
138
163
|
const [msg, setMsg] = React.useState("");
|
|
164
|
+
const [devices, setDevices] = React.useState(null);
|
|
139
165
|
|
|
140
166
|
const load = React.useCallback(() => {
|
|
141
167
|
call("/crossterm/api/status")
|
|
@@ -150,6 +176,21 @@ window.__ModuleLoader__.load({
|
|
|
150
176
|
load();
|
|
151
177
|
}, [load]);
|
|
152
178
|
|
|
179
|
+
const loadDevices = React.useCallback(async () => {
|
|
180
|
+
setDevices({ loading: true, devices: [] });
|
|
181
|
+
try {
|
|
182
|
+
const data = await call("/crossterm/api/devices");
|
|
183
|
+
setDevices({ loading: false, ...(data || {}) });
|
|
184
|
+
} catch (error) {
|
|
185
|
+
setDevices({ loading: false, ok: false, devices: [], message: String((error && error.message) || error) });
|
|
186
|
+
}
|
|
187
|
+
}, []);
|
|
188
|
+
|
|
189
|
+
// 展开时自动读一次云端设备(收起不读,避免每次进页面都等网盘)
|
|
190
|
+
React.useEffect(() => {
|
|
191
|
+
if (open && !devices) void loadDevices();
|
|
192
|
+
}, [open, devices, loadDevices]);
|
|
193
|
+
|
|
153
194
|
const onCheck = async () => {
|
|
154
195
|
setBusy("check");
|
|
155
196
|
setMsg("正在检查更新…");
|
|
@@ -265,6 +306,12 @@ window.__ModuleLoader__.load({
|
|
|
265
306
|
label: "当前终端",
|
|
266
307
|
value: status ? `${status.node}(${status.nodeId})· ${status.hostname}` : "",
|
|
267
308
|
}),
|
|
309
|
+
h(
|
|
310
|
+
"div",
|
|
311
|
+
{ style: S.grid },
|
|
312
|
+
h("div", { style: S.key }, "云端设备"),
|
|
313
|
+
h("div", { style: S.val }, devicesText(status, devices)),
|
|
314
|
+
),
|
|
268
315
|
h(
|
|
269
316
|
"div",
|
|
270
317
|
{ style: S.grid },
|
|
@@ -318,6 +365,7 @@ window.__ModuleLoader__.load({
|
|
|
318
365
|
{ style: S.actions },
|
|
319
366
|
h("button", { style: S.btn, onClick: onCheck, disabled: busy !== "" }, busy === "check" ? "检查中…" : "检查更新"),
|
|
320
367
|
h("button", { style: S.btnPrimary, onClick: onUpdate, disabled: busy !== "" }, busy === "update" ? "更新中…" : "更新"),
|
|
368
|
+
h("button", { style: S.btn, onClick: () => void loadDevices(), disabled: Boolean(devices && devices.loading) }, devices && devices.loading ? "读取中…" : "刷新设备"),
|
|
321
369
|
),
|
|
322
370
|
msg ? h("div", { style: S.msg }, msg) : null,
|
|
323
371
|
),
|
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.3.
|
|
25
|
+
export const VERSION = '0.3.38'
|
|
26
26
|
|
|
27
27
|
/** DSH 的家目录:与宿主其它状态(sessions / profiles)同一个根,多终端天然共享 */
|
|
28
28
|
export function dshHome() {
|
|
@@ -203,8 +203,9 @@ export function formatBytes(n) {
|
|
|
203
203
|
export async function writeJsonAtomic(file, value) {
|
|
204
204
|
await fsp.mkdir(path.dirname(file), { recursive: true })
|
|
205
205
|
const tmp = `${file}.${process.pid}.${crypto.randomBytes(4).toString('hex')}.tmp`
|
|
206
|
-
|
|
207
|
-
await fsp.
|
|
206
|
+
const data = JSON.stringify(value, null, 2)
|
|
207
|
+
await fsp.writeFile(tmp, data, 'utf8')
|
|
208
|
+
await renameWithRetry(tmp, file, data)
|
|
208
209
|
}
|
|
209
210
|
|
|
210
211
|
/** 原子写文本:同上,用于邮件正文 */
|
|
@@ -212,7 +213,53 @@ export async function writeFileAtomic(file, text) {
|
|
|
212
213
|
await fsp.mkdir(path.dirname(file), { recursive: true })
|
|
213
214
|
const tmp = `${file}.${process.pid}.${crypto.randomBytes(4).toString('hex')}.tmp`
|
|
214
215
|
await fsp.writeFile(tmp, text, 'utf8')
|
|
215
|
-
await
|
|
216
|
+
await renameWithRetry(tmp, file, text)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Windows 上目标文件被别的进程(读方、杀毒、同步盘)短暂占用时 rename 会 EPERM/EBUSY,
|
|
220
|
+
// 以前就这么把 .tmp 漏在原地:presence/ 攒了两百多个。这里重试几次,最后退化成直接覆盖,
|
|
221
|
+
// 无论成败都把 tmp 删掉,绝不留垃圾。
|
|
222
|
+
async function renameWithRetry(tmp, file, data) {
|
|
223
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
224
|
+
try {
|
|
225
|
+
await fsp.rename(tmp, file)
|
|
226
|
+
return
|
|
227
|
+
} catch (error) {
|
|
228
|
+
const code = error && error.code
|
|
229
|
+
if (!['EPERM', 'EBUSY', 'EACCES', 'ENOENT'].includes(code)) throw error
|
|
230
|
+
await new Promise((resolve) => setTimeout(resolve, 15 * (attempt + 1)))
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
await fsp.writeFile(file, data, 'utf8')
|
|
234
|
+
await fsp.rm(tmp, { force: true }).catch(() => {})
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** 清掉总线目录里超过 maxAgeMs 的 .tmp 残骸(原子写的漏网之鱼) */
|
|
238
|
+
export async function sweepTmp(root, maxAgeMs = 10 * 60 * 1000, dirs = ['presence', 'chains', 'links', 'inbox', 'mail']) {
|
|
239
|
+
const now = Date.now()
|
|
240
|
+
let removed = 0
|
|
241
|
+
for (const dir of dirs) {
|
|
242
|
+
const abs = path.join(root, dir)
|
|
243
|
+
let entries
|
|
244
|
+
try {
|
|
245
|
+
entries = await fsp.readdir(abs, { withFileTypes: true })
|
|
246
|
+
} catch {
|
|
247
|
+
continue
|
|
248
|
+
}
|
|
249
|
+
for (const entry of entries) {
|
|
250
|
+
if (!entry.isFile() || !entry.name.endsWith('.tmp')) continue
|
|
251
|
+
const target = path.join(abs, entry.name)
|
|
252
|
+
try {
|
|
253
|
+
const st = await fsp.stat(target)
|
|
254
|
+
if (now - st.mtimeMs < maxAgeMs) continue
|
|
255
|
+
await fsp.rm(target, { force: true })
|
|
256
|
+
removed += 1
|
|
257
|
+
} catch {
|
|
258
|
+
/* ignore */
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return removed
|
|
216
263
|
}
|
|
217
264
|
|
|
218
265
|
export async function readJson(file) {
|
|
@@ -300,8 +347,14 @@ export class Bus {
|
|
|
300
347
|
const file = this.mailFile(msg.id)
|
|
301
348
|
fs.mkdirSync(path.dirname(file), { recursive: true })
|
|
302
349
|
const tmp = `${file}.${process.pid}.${crypto.randomBytes(4).toString('hex')}.tmp`
|
|
303
|
-
|
|
304
|
-
fs.
|
|
350
|
+
const text = `${head.join('\n')}\n${body}\n`
|
|
351
|
+
fs.writeFileSync(tmp, text, 'utf8')
|
|
352
|
+
try {
|
|
353
|
+
fs.renameSync(tmp, file)
|
|
354
|
+
} catch {
|
|
355
|
+
fs.writeFileSync(file, text, 'utf8')
|
|
356
|
+
try { fs.rmSync(tmp, { force: true }) } catch { /* ignore */ }
|
|
357
|
+
}
|
|
305
358
|
return file
|
|
306
359
|
}
|
|
307
360
|
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import fsp from 'node:fs/promises'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
|
|
5
|
+
const ONLINE_MS = 3 * 60 * 1000
|
|
6
|
+
|
|
7
|
+
function trimRoot(root) {
|
|
8
|
+
return String(root || '').replace(/\/+$/, '')
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function summarize(data, now) {
|
|
12
|
+
const updatedAt = Number(data.updatedAt) || 0
|
|
13
|
+
return {
|
|
14
|
+
nodeId: String(data.nodeId || ''),
|
|
15
|
+
name: String(data.name || data.hostname || data.nodeId || ''),
|
|
16
|
+
hostname: String(data.hostname || ''),
|
|
17
|
+
version: String(data.version || ''),
|
|
18
|
+
platform: String(data.platform || ''),
|
|
19
|
+
pid: Number(data.pid) || 0,
|
|
20
|
+
sessions: Array.isArray(data.sessions) ? data.sessions.length : 0,
|
|
21
|
+
chains: Array.isArray(data.chains)
|
|
22
|
+
? [...new Set(data.chains.map((c) => (typeof c === 'string' ? c : c && c.code)).filter(Boolean))]
|
|
23
|
+
: [],
|
|
24
|
+
updatedAt,
|
|
25
|
+
lastSeen: updatedAt ? new Date(updatedAt).toISOString() : '',
|
|
26
|
+
online: updatedAt > 0 && now - updatedAt < ONLINE_MS,
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 云端设备列表:读 <cloudRoot>/presence/*.json(翻页列目录,跳过 .tmp 残骸),
|
|
32
|
+
* 逐个下载解析出 nodeId/名字/版本/最后心跳。
|
|
33
|
+
* 注意 cfg.cloudRoot 本身就是总线根(形如 /dsh云盘/crossterm/bus),不再拼一层 bus。
|
|
34
|
+
*/
|
|
35
|
+
export async function listCloudDevices(pan, cloudRoot, opts = {}) {
|
|
36
|
+
const now = opts.now || Date.now()
|
|
37
|
+
const root = trimRoot(cloudRoot)
|
|
38
|
+
const dir = `${root}/presence`
|
|
39
|
+
const result = { ok: false, dir, devices: [], files: 0, tmpFiles: 0, error: '' }
|
|
40
|
+
if (!root) {
|
|
41
|
+
result.error = '还没配置云盘'
|
|
42
|
+
return result
|
|
43
|
+
}
|
|
44
|
+
let listing
|
|
45
|
+
try {
|
|
46
|
+
listing = await pan.lsAll(dir)
|
|
47
|
+
} catch (error) {
|
|
48
|
+
result.error = String((error && error.message) || error)
|
|
49
|
+
return result
|
|
50
|
+
}
|
|
51
|
+
if (!listing || listing.errno !== 0) {
|
|
52
|
+
// 4 = 目录不存在(还没同步过)
|
|
53
|
+
result.ok = true
|
|
54
|
+
result.error = listing && listing.errno === -9 ? '云端目录不存在' : ''
|
|
55
|
+
return result
|
|
56
|
+
}
|
|
57
|
+
const entries = listing.list || []
|
|
58
|
+
const jsonFiles = entries.filter((f) => f.isdir === 0 && f.server_filename.endsWith('.json'))
|
|
59
|
+
result.files = jsonFiles.length
|
|
60
|
+
result.tmpFiles = entries.filter((f) => f.isdir === 0 && f.server_filename.endsWith('.tmp')).length
|
|
61
|
+
|
|
62
|
+
const tmpDir = await fsp.mkdtemp(path.join(os.tmpdir(), 'ct-devices-'))
|
|
63
|
+
try {
|
|
64
|
+
const byId = new Map()
|
|
65
|
+
for (const entry of jsonFiles) {
|
|
66
|
+
const name = entry.server_filename
|
|
67
|
+
const nodeId = name.slice(0, -'.json'.length)
|
|
68
|
+
const local = path.join(tmpDir, name)
|
|
69
|
+
try {
|
|
70
|
+
await pan.get(`${dir}/${name}`, local)
|
|
71
|
+
const data = JSON.parse(await fsp.readFile(local, 'utf8'))
|
|
72
|
+
if (!data || !data.nodeId) continue
|
|
73
|
+
byId.set(data.nodeId, summarize(data, now))
|
|
74
|
+
} catch {
|
|
75
|
+
/* 单个坏文件不影响整体 */
|
|
76
|
+
}
|
|
77
|
+
if (byId.size >= 50) break
|
|
78
|
+
}
|
|
79
|
+
result.devices = [...byId.values()].sort((a, b) => b.updatedAt - a.updatedAt)
|
|
80
|
+
result.ok = true
|
|
81
|
+
return result
|
|
82
|
+
} finally {
|
|
83
|
+
await fsp.rm(tmpDir, { recursive: true, force: true }).catch(() => {})
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** 本地自己的 presence(不依赖云端,用来兜底显示"我") */
|
|
88
|
+
export async function localDevice(presenceDir, nodeId, now = Date.now()) {
|
|
89
|
+
try {
|
|
90
|
+
const raw = await fsp.readFile(path.join(presenceDir, `${nodeId}.json`), 'utf8')
|
|
91
|
+
return summarize(JSON.parse(raw), now)
|
|
92
|
+
} catch {
|
|
93
|
+
return undefined
|
|
94
|
+
}
|
|
95
|
+
}
|
package/lib/cloud/pan.js
CHANGED
|
@@ -73,9 +73,27 @@ export function createPan({ appFile, authFile, log = () => {} }) {
|
|
|
73
73
|
return api(`https://pan.baidu.com/rest/2.0/xpan/nas?method=uinfo&access_token=${encodeURIComponent(tok)}`)
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
async function
|
|
76
|
+
async function lsPage(dir, start, limit) {
|
|
77
77
|
await ensureToken()
|
|
78
|
-
return api(`https://pan.baidu.com/rest/2.0/xpan/file?method=list&access_token=${encodeURIComponent(tok)}&dir=${encodeURIComponent(dir)}&order=time&start
|
|
78
|
+
return api(`https://pan.baidu.com/rest/2.0/xpan/file?method=list&access_token=${encodeURIComponent(tok)}&dir=${encodeURIComponent(dir)}&order=time&start=${start}&limit=${limit}`)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function ls(dir = '/') {
|
|
82
|
+
return lsPage(dir, 0, 200)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 一个目录超过 200 个文件时必须翻页,否则第 201 个之后永远看不见
|
|
86
|
+
// (presence/ 里被 .tmp 残骸占满时,真实 node json 就躲在第二页)。
|
|
87
|
+
async function lsAll(dir = '/', pageSize = 200) {
|
|
88
|
+
const all = []
|
|
89
|
+
for (let page = 0; page < 50; page += 1) {
|
|
90
|
+
const j = await lsPage(dir, page * pageSize, pageSize)
|
|
91
|
+
if (j.errno !== 0) return j
|
|
92
|
+
const list = j.list || []
|
|
93
|
+
all.push(...list)
|
|
94
|
+
if (list.length < pageSize) return { ...j, list: all, errno: 0 }
|
|
95
|
+
}
|
|
96
|
+
return { errno: 0, list: all }
|
|
79
97
|
}
|
|
80
98
|
|
|
81
99
|
async function put(localPath, remotePath) {
|
|
@@ -123,7 +141,7 @@ export function createPan({ appFile, authFile, log = () => {} }) {
|
|
|
123
141
|
await ensureToken()
|
|
124
142
|
const dir = path.posix.dirname(remotePath)
|
|
125
143
|
const name = path.posix.basename(remotePath)
|
|
126
|
-
const list = await
|
|
144
|
+
const list = await lsAll(dir)
|
|
127
145
|
if (list.errno !== 0) throw new Error('列目录失败:' + JSON.stringify(list))
|
|
128
146
|
const hit = (list.list || []).find((f) => f.server_filename === name && f.isdir === 0)
|
|
129
147
|
if (!hit) throw new Error('远端没有该文件:' + remotePath)
|
|
@@ -148,5 +166,5 @@ export function createPan({ appFile, authFile, log = () => {} }) {
|
|
|
148
166
|
)
|
|
149
167
|
}
|
|
150
168
|
|
|
151
|
-
return { uinfo, ls, put, get, rm, ensureToken }
|
|
169
|
+
return { uinfo, ls, lsAll, put, get, rm, ensureToken }
|
|
152
170
|
}
|
package/lib/cloud/sync.js
CHANGED
|
@@ -44,6 +44,9 @@ export function createCloud(opts) {
|
|
|
44
44
|
if (!r.includes('/')) continue
|
|
45
45
|
if (EXCLUDE_TOP.has(r)) continue
|
|
46
46
|
}
|
|
47
|
+
// 原子写的临时文件(xxx.json.<pid>.<hex>.tmp)绝不能当数据同步,
|
|
48
|
+
// 以前漏过滤,云端 presence/ 里攒了两百多个残骸。
|
|
49
|
+
if (e.name.endsWith('.tmp')) continue
|
|
47
50
|
let st
|
|
48
51
|
try { st = fs.statSync(path.join(root, r)) } catch { continue }
|
|
49
52
|
out.push({
|
|
@@ -62,10 +65,11 @@ export function createCloud(opts) {
|
|
|
62
65
|
async function walkCloud() {
|
|
63
66
|
const out = new Map()
|
|
64
67
|
async function rec(dir, rel) {
|
|
65
|
-
const j = await pan.
|
|
68
|
+
const j = await pan.lsAll(dir)
|
|
66
69
|
if (j.errno !== 0) return
|
|
67
70
|
for (const f of j.list || []) {
|
|
68
71
|
if (EXCLUDE_DIRS.has(f.server_filename)) continue
|
|
72
|
+
if (f.server_filename.endsWith('.tmp')) continue
|
|
69
73
|
const r = rel ? path.posix.join(rel, f.server_filename) : f.server_filename
|
|
70
74
|
if (f.isdir === 1) { await rec(f.path, r); continue }
|
|
71
75
|
out.set(r, { size: f.size, mtimeMs: (f.server_mtime || 0) * 1000, fs_id: f.fs_id })
|
package/lib/web.js
CHANGED
|
@@ -5,6 +5,8 @@ import os from 'node:os'
|
|
|
5
5
|
import path from 'node:path'
|
|
6
6
|
import { spawn } from 'node:child_process'
|
|
7
7
|
import { VERSION, readConfig, writeConfig } from './bus.js'
|
|
8
|
+
import { createPan } from './cloud/pan.js'
|
|
9
|
+
import { listCloudDevices, localDevice } from './cloud/devices.js'
|
|
8
10
|
import { runSetup } from './cloud/setup.js'
|
|
9
11
|
|
|
10
12
|
const PKG = 'dsh-crossterm'
|
|
@@ -277,6 +279,43 @@ export function registerWeb(ctx, term, log) {
|
|
|
277
279
|
handler: (req, res) => json(res, 200, statusOf(term)),
|
|
278
280
|
}),
|
|
279
281
|
)
|
|
282
|
+
disposers.push(
|
|
283
|
+
webCtx.webServer.register({
|
|
284
|
+
kind: 'exact',
|
|
285
|
+
path: '/crossterm/api/devices',
|
|
286
|
+
handler: async (req, res) => {
|
|
287
|
+
const cfg = term.cfg || {}
|
|
288
|
+
const info = statusOf(term)
|
|
289
|
+
if (!info.cloud.configured) {
|
|
290
|
+
json(res, 200, {
|
|
291
|
+
ok: false,
|
|
292
|
+
configured: false,
|
|
293
|
+
devices: [],
|
|
294
|
+
message: '这台机器还没连接云盘,看不到其他设备',
|
|
295
|
+
})
|
|
296
|
+
return
|
|
297
|
+
}
|
|
298
|
+
try {
|
|
299
|
+
const pan = createPan({
|
|
300
|
+
appFile: cfg.baiduAppFile,
|
|
301
|
+
authFile: cfg.baiduAuthFile,
|
|
302
|
+
log: (m) => log('devices: ' + m),
|
|
303
|
+
})
|
|
304
|
+
const result = await listCloudDevices(pan, cfg.cloudRoot)
|
|
305
|
+
const me = await localDevice(path.join(cfg.busDir || '', 'presence'), info.nodeId || '')
|
|
306
|
+
if (me && !result.devices.some((d) => d.nodeId === me.nodeId)) result.devices.unshift(me)
|
|
307
|
+
json(res, 200, { ...result, configured: true, me: info.nodeId || '' })
|
|
308
|
+
} catch (error) {
|
|
309
|
+
json(res, 200, {
|
|
310
|
+
ok: false,
|
|
311
|
+
configured: true,
|
|
312
|
+
devices: [],
|
|
313
|
+
message: String((error && error.message) || error),
|
|
314
|
+
})
|
|
315
|
+
}
|
|
316
|
+
},
|
|
317
|
+
}),
|
|
318
|
+
)
|
|
280
319
|
disposers.push(
|
|
281
320
|
webCtx.webServer.register({
|
|
282
321
|
kind: 'exact',
|