dsh-crossterm 0.3.37 → 0.3.39
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 +42 -0
- package/client/client.js +118 -0
- package/lib/bus.js +67 -6
- package/lib/cloud/devices.js +95 -0
- package/lib/cloud/home.js +152 -0
- package/lib/cloud/layout.js +190 -0
- package/lib/cloud/pan.js +62 -6
- package/lib/cloud/relay.js +196 -0
- package/lib/cloud/session-backup.js +188 -0
- package/lib/cloud/sync.js +5 -1
- package/lib/index.js +136 -2
- package/lib/web.js +105 -0
- package/package.json +1 -1
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/index.js
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
import { deriveNameFromFile, findTranscript } from './names.js'
|
|
37
37
|
import { createPan } from './cloud/pan.js'
|
|
38
38
|
import { createCloud } from './cloud/sync.js'
|
|
39
|
+
import { createCloudHome } from './cloud/home.js'
|
|
39
40
|
import { runSetup } from './cloud/setup.js'
|
|
40
41
|
import { registerWeb } from './web.js'
|
|
41
42
|
|
|
@@ -730,6 +731,69 @@ class CrossTerm {
|
|
|
730
731
|
return { ok: true, sessionId, files: landed }
|
|
731
732
|
}
|
|
732
733
|
|
|
734
|
+
// ── 云端中转站(0.3.39)────────────────────────────────────────────────
|
|
735
|
+
|
|
736
|
+
/** 按会话名(也接受短号/完整 id)找本终端里的会话 */
|
|
737
|
+
resolveByName(raw) {
|
|
738
|
+
const s = String(raw || '').trim()
|
|
739
|
+
if (!s) return ''
|
|
740
|
+
const lower = s.toLowerCase()
|
|
741
|
+
const list = this.sessions()
|
|
742
|
+
const exact = list.find((x) => [x.name, x.title, x.short, x.id].some((v) => String(v || '').trim().toLowerCase() === lower))
|
|
743
|
+
if (exact) return exact.id
|
|
744
|
+
const part = list.find((x) => [x.name, x.title].some((v) => String(v || '').trim().toLowerCase().includes(lower)))
|
|
745
|
+
if (part) return part.id
|
|
746
|
+
return this.resolveSessionId(s) || ''
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/** 中转站投递:把云端收件箱里的一封信投进本机对应会话(按收件人会话名) */
|
|
750
|
+
async relayDeliver({ to, envelope = {}, files = [] }) {
|
|
751
|
+
const targets = (Array.isArray(to) ? to : String(to || '').split(/[,,;;\s]+/))
|
|
752
|
+
.map((s) => String(s || '').trim())
|
|
753
|
+
.filter(Boolean)
|
|
754
|
+
if (!targets.length) return { ok: false, reason: '信封里没有收件人(要填会话名)' }
|
|
755
|
+
const controller = this.sessionController()
|
|
756
|
+
if (!controller || typeof controller.prompt !== 'function') {
|
|
757
|
+
return { ok: false, reason: '本 Host 没有 sessionController.prompt,中转站只能留件' }
|
|
758
|
+
}
|
|
759
|
+
const from = envelope.from || {}
|
|
760
|
+
const lines = ['[云端中转站] 收到 1 封']
|
|
761
|
+
lines.push(`来自:${from.name || from.nodeId || '未知设备'}${from.nodeId ? ` (${from.nodeId})` : ''}`)
|
|
762
|
+
if (envelope.subject) lines.push(`主题:${envelope.subject}`)
|
|
763
|
+
if (envelope.text) lines.push('', String(envelope.text).trim())
|
|
764
|
+
const landed = files.filter((f) => f && f.local)
|
|
765
|
+
if (landed.length) {
|
|
766
|
+
lines.push('', '附件已保存到本机:')
|
|
767
|
+
for (const f of landed) lines.push(`- ${f.local}(${formatBytes(f.bytes || 0)})`)
|
|
768
|
+
}
|
|
769
|
+
const bad = files.filter((f) => !f || !f.local)
|
|
770
|
+
if (bad.length) lines.push('', `(有 ${bad.length} 个附件没下载下来:${bad.map((f) => (f && f.name) || '未知').join('、')})`)
|
|
771
|
+
lines.push('', `(这封信由云端中转站投递;收件人:${targets.join('、')}。回复用 term_send。)`)
|
|
772
|
+
const text = lines.join('\n')
|
|
773
|
+
const delivered = []
|
|
774
|
+
const failed = []
|
|
775
|
+
for (const target of targets) {
|
|
776
|
+
const sessionId = this.resolveByName(target)
|
|
777
|
+
if (!sessionId) { failed.push({ target, reason: '本机没有这个会话' }); continue }
|
|
778
|
+
try {
|
|
779
|
+
await this.promptSession(controller, { id: `relay-${Date.now()}-${shortId(sessionId)}` }, sessionId, text)
|
|
780
|
+
delivered.push(sessionId)
|
|
781
|
+
await this.note(`relay delivered -> ${this.labelOf(sessionId)}`)
|
|
782
|
+
} catch (e) {
|
|
783
|
+
failed.push({ target, reason: (e && e.message) || String(e) })
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
if (!delivered.length) return { ok: false, reason: `投递失败:${failed.map((f) => `${f.target}(${f.reason})`).join('、')}` }
|
|
787
|
+
return { ok: true, sessionId: delivered[0], delivered, failed }
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/** 给云端备份用的「会话短号 → 显示名」表 */
|
|
791
|
+
sessionNames() {
|
|
792
|
+
const out = {}
|
|
793
|
+
for (const s of this.sessions()) out[s.short] = { name: s.name || '', cwd: s.cwd || '' }
|
|
794
|
+
return out
|
|
795
|
+
}
|
|
796
|
+
|
|
733
797
|
// ── 会话链(一串特征码链接多个会话)────────────────────────────────────
|
|
734
798
|
|
|
735
799
|
async loadLinks() {
|
|
@@ -1161,10 +1225,42 @@ class CrossTerm {
|
|
|
1161
1225
|
})
|
|
1162
1226
|
this.cloud.start()
|
|
1163
1227
|
void this.note(`cloud sync 启动 → ${this.cfg.cloudRoot}(${this.cfg.cloudIntervalMs}ms)`)
|
|
1228
|
+
this.enableCloudHome(pan)
|
|
1164
1229
|
return this.cloud
|
|
1165
1230
|
}
|
|
1166
1231
|
|
|
1232
|
+
/** 云端目录(账号/设备/共享收件箱)+ 中转站 + 会话备份,见 lib/cloud/home.js */
|
|
1233
|
+
enableCloudHome(pan) {
|
|
1234
|
+
if (this.home) return this.home
|
|
1235
|
+
try {
|
|
1236
|
+
this.home = createCloudHome({
|
|
1237
|
+
cfg: this.cfg,
|
|
1238
|
+
device: {
|
|
1239
|
+
nodeId: this.nodeId,
|
|
1240
|
+
name: this.cfg.name,
|
|
1241
|
+
version: VERSION,
|
|
1242
|
+
platform: process.platform,
|
|
1243
|
+
sessions: this.sessions().length,
|
|
1244
|
+
},
|
|
1245
|
+
pan,
|
|
1246
|
+
busDir: this.bus.root,
|
|
1247
|
+
deliver: (opts) => this.relayDeliver(opts),
|
|
1248
|
+
names: () => this.sessionNames(),
|
|
1249
|
+
log: (m) => void this.note('cloud: ' + m),
|
|
1250
|
+
})
|
|
1251
|
+
this.home.start()
|
|
1252
|
+
} catch (e) {
|
|
1253
|
+
this.home = null
|
|
1254
|
+
void this.warn('云端目录启动失败:' + ((e && e.message) || String(e)))
|
|
1255
|
+
}
|
|
1256
|
+
return this.home
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1167
1259
|
disableCloud() {
|
|
1260
|
+
if (this.home) {
|
|
1261
|
+
this.home.stop()
|
|
1262
|
+
this.home = null
|
|
1263
|
+
}
|
|
1168
1264
|
if (!this.cloud) return false
|
|
1169
1265
|
this.cloud.stop()
|
|
1170
1266
|
this.cloud = null
|
|
@@ -1196,6 +1292,7 @@ class CrossTerm {
|
|
|
1196
1292
|
if (this.timer) clearInterval(this.timer)
|
|
1197
1293
|
this.timer = undefined
|
|
1198
1294
|
if (this.cloud) { this.cloud.stop(); this.cloud = null }
|
|
1295
|
+
if (this.home) { this.home.stop(); this.home = null }
|
|
1199
1296
|
await this.bus.leave(this.nodeId).catch(() => {})
|
|
1200
1297
|
await this.note('stopped')
|
|
1201
1298
|
}
|
|
@@ -1617,14 +1714,33 @@ function buildTools(term, agent, part = 'all') {
|
|
|
1617
1714
|
|
|
1618
1715
|
const cloud = defineTool({
|
|
1619
1716
|
name: 'term_cloud',
|
|
1620
|
-
description: '云总线同步:查看/开启/关闭本机总线与云端目录的镜像同步,或立刻同步一次;setup 可一键配置自己的云盘(弹出记事本填 AppKey/AppSecret → 浏览器授权 →
|
|
1717
|
+
description: '云总线同步:查看/开启/关闭本机总线与云端目录的镜像同步,或立刻同步一次;setup 可一键配置自己的云盘(弹出记事本填 AppKey/AppSecret → 浏览器授权 → 自动开启同步);home/layout/relay/backup 管理云端目录(账号/设备/共享收件箱/中转站/会话备份)。',
|
|
1621
1718
|
parameters: {
|
|
1622
|
-
action: { type: 'string', enum: ['status', 'start', 'stop', 'once', 'setup'], description: 'status=查看状态;start=开启(本进程内生效);stop=关闭;once=立刻同步一次;setup
|
|
1719
|
+
action: { type: 'string', enum: ['status', 'start', 'stop', 'once', 'setup', 'home', 'layout', 'relay', 'backup'], description: 'status=查看状态;start=开启(本进程内生效);stop=关闭;once=立刻同步一次;setup=一键配置自己的云盘;home=云端目录/中转站/备份概况;layout=重建云端目录;relay=立刻收一轮云端收件箱;backup=立刻备份一轮会话' },
|
|
1623
1720
|
code: { type: 'string', description: '仅 setup 用:授权回调 URL 里的 code(浏览器没自动回调时手动粘贴);不填则插件自己起本地回调等待' },
|
|
1624
1721
|
},
|
|
1625
1722
|
output: {
|
|
1626
1723
|
schema: { type: 'json' },
|
|
1627
1724
|
render: textRender((v) => {
|
|
1725
|
+
if (v && v.home) {
|
|
1726
|
+
const h = v.home
|
|
1727
|
+
const lines = []
|
|
1728
|
+
if (h.layout) {
|
|
1729
|
+
lines.push(`云端账号:${h.layout.account}(角色 ${h.layout.role === 'admin' ? '管理员' : '成员'},设备 ${h.layout.deviceId})`)
|
|
1730
|
+
const p = h.layout.paths || {}
|
|
1731
|
+
lines.push(`设备目录:${p.deviceDir || '(未建)'}`)
|
|
1732
|
+
lines.push(`我的收件箱:${p.inboxDir || '(未建)'}`)
|
|
1733
|
+
lines.push(`会话备份:${p.sessionDir || '(未建)'}(${h.backup?.files || 0} 个会话,已备份 ${formatBytes(h.backup?.bytes || 0)})`)
|
|
1734
|
+
} else {
|
|
1735
|
+
lines.push('云端目录:还没建好(可能没连云盘,或建目录失败)')
|
|
1736
|
+
}
|
|
1737
|
+
if (h.relay) lines.push(`中转站:已收 ${h.relay.processed || 0} 封,失败 ${h.relay.failed || 0} 封,每 ${Math.round((h.relay.intervalMs || 0) / 1000)} s 查一次`)
|
|
1738
|
+
if (h.backup?.lastAt) lines.push(`上次备份:${new Date(h.backup.lastAt).toISOString()}(${h.backup.lastCount || 0} 个会话)`)
|
|
1739
|
+
if (v.layout) lines.push(`目录动作:${v.layout.created?.length ? '新建 ' + v.layout.created.join('、') : '已存在'}`)
|
|
1740
|
+
if (v.relay) lines.push(`本轮收信:拉取 ${v.relay.fetched || 0} 封,投递 ${v.relay.delivered || 0},失败 ${v.relay.failed || 0}`)
|
|
1741
|
+
if (v.backup) lines.push(`本轮备份:会话 ${v.backup.sessions?.uploaded || 0},附件 ${v.backup.attachments?.uploaded || 0},耗时 ${v.backup.ms || 0} ms`)
|
|
1742
|
+
return lines.join('\n')
|
|
1743
|
+
}
|
|
1628
1744
|
if (v && v.setup) {
|
|
1629
1745
|
const s = v.setup
|
|
1630
1746
|
const lines = []
|
|
@@ -1666,6 +1782,24 @@ function buildTools(term, agent, part = 'all') {
|
|
|
1666
1782
|
term.enableCloud()
|
|
1667
1783
|
return { ok: true, status: term.cloud ? term.cloud.status() : undefined, setup: ret }
|
|
1668
1784
|
}
|
|
1785
|
+
else if (action === 'home' || action === 'layout' || action === 'relay' || action === 'backup') {
|
|
1786
|
+
if (!term.cloud) term.enableCloud()
|
|
1787
|
+
const h = term.home
|
|
1788
|
+
if (!h) return { ok: false, detail: '云同步没开(先 action="start",或在设置里开云同步)' }
|
|
1789
|
+
if (action === 'layout') {
|
|
1790
|
+
const layout = await h.getLayout(true)
|
|
1791
|
+
return { ok: !!(layout && layout.paths), home: h.status(), layout }
|
|
1792
|
+
}
|
|
1793
|
+
if (action === 'relay') {
|
|
1794
|
+
const relay = await h.relayNow()
|
|
1795
|
+
return { ok: relay.ok !== false, relay, home: h.status() }
|
|
1796
|
+
}
|
|
1797
|
+
if (action === 'backup') {
|
|
1798
|
+
const backup = await h.backupNow()
|
|
1799
|
+
return { ok: backup.ok !== false, backup, home: h.status() }
|
|
1800
|
+
}
|
|
1801
|
+
return { ok: true, home: h.status() }
|
|
1802
|
+
}
|
|
1669
1803
|
else if (action === 'once') {
|
|
1670
1804
|
const c = term.cloud || term.enableCloud()
|
|
1671
1805
|
const result = await c.once()
|
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'
|
|
@@ -256,6 +258,8 @@ function statusOf(term) {
|
|
|
256
258
|
provider,
|
|
257
259
|
},
|
|
258
260
|
chains,
|
|
261
|
+
// 云端目录(0.3.39):账号/角色/设备目录/中转站/会话备份
|
|
262
|
+
home: term.home ? term.home.status() : null,
|
|
259
263
|
}
|
|
260
264
|
}
|
|
261
265
|
|
|
@@ -277,6 +281,107 @@ export function registerWeb(ctx, term, log) {
|
|
|
277
281
|
handler: (req, res) => json(res, 200, statusOf(term)),
|
|
278
282
|
}),
|
|
279
283
|
)
|
|
284
|
+
disposers.push(
|
|
285
|
+
webCtx.webServer.register({
|
|
286
|
+
kind: 'exact',
|
|
287
|
+
path: '/crossterm/api/devices',
|
|
288
|
+
handler: async (req, res) => {
|
|
289
|
+
const cfg = term.cfg || {}
|
|
290
|
+
const info = statusOf(term)
|
|
291
|
+
if (!info.cloud.configured) {
|
|
292
|
+
json(res, 200, {
|
|
293
|
+
ok: false,
|
|
294
|
+
configured: false,
|
|
295
|
+
devices: [],
|
|
296
|
+
message: '这台机器还没连接云盘,看不到其他设备',
|
|
297
|
+
})
|
|
298
|
+
return
|
|
299
|
+
}
|
|
300
|
+
try {
|
|
301
|
+
const pan = createPan({
|
|
302
|
+
appFile: cfg.baiduAppFile,
|
|
303
|
+
authFile: cfg.baiduAuthFile,
|
|
304
|
+
log: (m) => log('devices: ' + m),
|
|
305
|
+
})
|
|
306
|
+
const result = await listCloudDevices(pan, cfg.cloudRoot)
|
|
307
|
+
const me = await localDevice(path.join(cfg.busDir || '', 'presence'), info.nodeId || '')
|
|
308
|
+
if (me && !result.devices.some((d) => d.nodeId === me.nodeId)) result.devices.unshift(me)
|
|
309
|
+
json(res, 200, { ...result, configured: true, me: info.nodeId || '' })
|
|
310
|
+
} catch (error) {
|
|
311
|
+
json(res, 200, {
|
|
312
|
+
ok: false,
|
|
313
|
+
configured: true,
|
|
314
|
+
devices: [],
|
|
315
|
+
message: String((error && error.message) || error),
|
|
316
|
+
})
|
|
317
|
+
}
|
|
318
|
+
},
|
|
319
|
+
}),
|
|
320
|
+
)
|
|
321
|
+
disposers.push(
|
|
322
|
+
webCtx.webServer.register({
|
|
323
|
+
kind: 'exact',
|
|
324
|
+
path: '/crossterm/api/home',
|
|
325
|
+
handler: async (req, res) => {
|
|
326
|
+
const cfg = term.cfg || {}
|
|
327
|
+
if (!statusOf(term).cloud.configured) {
|
|
328
|
+
json(res, 200, { ok: false, configured: false, message: '这台机器还没连接云盘:先「配置云盘」' })
|
|
329
|
+
return
|
|
330
|
+
}
|
|
331
|
+
try {
|
|
332
|
+
if (!term.home) term.enableCloud()
|
|
333
|
+
const home = term.home
|
|
334
|
+
if (!home) {
|
|
335
|
+
json(res, 200, { ok: false, configured: true, message: '云端目录还没启动(云同步可能被关掉了)' })
|
|
336
|
+
return
|
|
337
|
+
}
|
|
338
|
+
const url = String((req && req.url) || '')
|
|
339
|
+
const action = (() => {
|
|
340
|
+
try { return String(new URL(url, 'http://x').searchParams.get('action') || '') } catch { return '' }
|
|
341
|
+
})()
|
|
342
|
+
if (action === 'layout') {
|
|
343
|
+
const layout = await home.getLayout(true)
|
|
344
|
+
log(`home: 云端目录 ${layout && layout.paths ? layout.paths.deviceDir : '未建好'}`)
|
|
345
|
+
json(res, 200, { ok: !!(layout && layout.paths), home: home.status(), layout })
|
|
346
|
+
return
|
|
347
|
+
}
|
|
348
|
+
if (action === 'relay') {
|
|
349
|
+
const relay = await home.relayNow()
|
|
350
|
+
json(res, 200, { ok: relay.ok !== false, relay, home: home.status() })
|
|
351
|
+
return
|
|
352
|
+
}
|
|
353
|
+
if (action === 'backup') {
|
|
354
|
+
const backup = await home.backupNow()
|
|
355
|
+
json(res, 200, { ok: backup.ok !== false, backup, home: home.status() })
|
|
356
|
+
return
|
|
357
|
+
}
|
|
358
|
+
json(res, 200, { ok: true, configured: true, home: home.status(), cloudRoot: cfg.cloudRoot || '' })
|
|
359
|
+
} catch (error) {
|
|
360
|
+
json(res, 200, { ok: false, configured: true, message: String((error && error.message) || error) })
|
|
361
|
+
}
|
|
362
|
+
},
|
|
363
|
+
}),
|
|
364
|
+
)
|
|
365
|
+
disposers.push(
|
|
366
|
+
webCtx.webServer.register({
|
|
367
|
+
kind: 'exact',
|
|
368
|
+
path: '/crossterm/api/backup',
|
|
369
|
+
handler: async (req, res) => {
|
|
370
|
+
try {
|
|
371
|
+
if (!term.home) term.enableCloud()
|
|
372
|
+
if (!term.home) {
|
|
373
|
+
json(res, 200, { ok: false, message: '云端目录还没启动(先在卡片上打开云同步)' })
|
|
374
|
+
return
|
|
375
|
+
}
|
|
376
|
+
const result = await term.home.backupNow()
|
|
377
|
+
log(`backup: 会话 ${result.sessions ? result.sessions.uploaded : 0},附件 ${result.attachments ? result.attachments.uploaded : 0}`)
|
|
378
|
+
json(res, 200, { ok: result.ok !== false, backup: result, home: term.home.status() })
|
|
379
|
+
} catch (error) {
|
|
380
|
+
json(res, 200, { ok: false, message: String((error && error.message) || error) })
|
|
381
|
+
}
|
|
382
|
+
},
|
|
383
|
+
}),
|
|
384
|
+
)
|
|
280
385
|
disposers.push(
|
|
281
386
|
webCtx.webServer.register({
|
|
282
387
|
kind: 'exact',
|