dsh-crossterm 0.3.31 → 0.3.33
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/client/client.js +296 -0
- package/cordis.patch.yml +4 -2
- package/lib/bus.js +1 -1
- package/lib/index.js +6 -1
- package/lib/web.js +292 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +9 -1
- package/parts/account.manifest.json +1 -1
- package/parts/device.manifest.json +1 -1
package/client/client.js
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "dsh-crossterm",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
|
|
8
|
+
const React = require("react");
|
|
9
|
+
const h = React.createElement;
|
|
10
|
+
|
|
11
|
+
const S = {
|
|
12
|
+
card: {
|
|
13
|
+
border: "1px solid rgba(128,128,128,.28)",
|
|
14
|
+
borderRadius: 12,
|
|
15
|
+
background: "rgba(128,128,128,.06)",
|
|
16
|
+
color: "inherit",
|
|
17
|
+
fontSize: 13,
|
|
18
|
+
overflow: "hidden",
|
|
19
|
+
},
|
|
20
|
+
head: {
|
|
21
|
+
display: "flex",
|
|
22
|
+
alignItems: "center",
|
|
23
|
+
gap: 10,
|
|
24
|
+
padding: "12px 14px",
|
|
25
|
+
cursor: "pointer",
|
|
26
|
+
userSelect: "none",
|
|
27
|
+
},
|
|
28
|
+
title: { fontWeight: 600 },
|
|
29
|
+
sub: { opacity: 0.62, fontSize: 12, marginTop: 2 },
|
|
30
|
+
chevron: { marginLeft: "auto", opacity: 0.65, fontSize: 11, lineHeight: 1 },
|
|
31
|
+
body: {
|
|
32
|
+
borderTop: "1px solid rgba(128,128,128,.22)",
|
|
33
|
+
padding: "12px 14px",
|
|
34
|
+
display: "flex",
|
|
35
|
+
flexDirection: "column",
|
|
36
|
+
gap: 10,
|
|
37
|
+
},
|
|
38
|
+
grid: {
|
|
39
|
+
display: "grid",
|
|
40
|
+
gridTemplateColumns: "max-content 1fr",
|
|
41
|
+
gap: "6px 16px",
|
|
42
|
+
alignItems: "baseline",
|
|
43
|
+
},
|
|
44
|
+
key: { opacity: 0.6 },
|
|
45
|
+
val: { wordBreak: "break-all" },
|
|
46
|
+
actions: { display: "flex", gap: 8, flexWrap: "wrap" },
|
|
47
|
+
btn: {
|
|
48
|
+
padding: "5px 12px",
|
|
49
|
+
borderRadius: 8,
|
|
50
|
+
border: "1px solid rgba(128,128,128,.35)",
|
|
51
|
+
background: "rgba(128,128,128,.1)",
|
|
52
|
+
color: "inherit",
|
|
53
|
+
cursor: "pointer",
|
|
54
|
+
fontSize: 12,
|
|
55
|
+
},
|
|
56
|
+
btnPrimary: {
|
|
57
|
+
padding: "5px 12px",
|
|
58
|
+
borderRadius: 8,
|
|
59
|
+
border: "1px solid rgba(110,160,255,.5)",
|
|
60
|
+
background: "rgba(110,160,255,.16)",
|
|
61
|
+
color: "inherit",
|
|
62
|
+
cursor: "pointer",
|
|
63
|
+
fontSize: 12,
|
|
64
|
+
},
|
|
65
|
+
msg: { fontSize: 12, opacity: 0.8, whiteSpace: "pre-wrap", wordBreak: "break-word" },
|
|
66
|
+
switchOn: {
|
|
67
|
+
width: 34,
|
|
68
|
+
height: 18,
|
|
69
|
+
marginLeft: 8,
|
|
70
|
+
padding: 0,
|
|
71
|
+
position: "relative",
|
|
72
|
+
borderRadius: 9,
|
|
73
|
+
border: "1px solid rgba(110,160,255,.6)",
|
|
74
|
+
background: "rgba(110,160,255,.55)",
|
|
75
|
+
cursor: "pointer",
|
|
76
|
+
verticalAlign: "middle",
|
|
77
|
+
},
|
|
78
|
+
switchOff: {
|
|
79
|
+
width: 34,
|
|
80
|
+
height: 18,
|
|
81
|
+
marginLeft: 8,
|
|
82
|
+
padding: 0,
|
|
83
|
+
position: "relative",
|
|
84
|
+
borderRadius: 9,
|
|
85
|
+
border: "1px solid rgba(128,128,128,.45)",
|
|
86
|
+
background: "rgba(128,128,128,.22)",
|
|
87
|
+
cursor: "pointer",
|
|
88
|
+
verticalAlign: "middle",
|
|
89
|
+
},
|
|
90
|
+
knobOn: {
|
|
91
|
+
position: "absolute",
|
|
92
|
+
top: 2,
|
|
93
|
+
left: 17,
|
|
94
|
+
width: 12,
|
|
95
|
+
height: 12,
|
|
96
|
+
borderRadius: "50%",
|
|
97
|
+
background: "#fff",
|
|
98
|
+
transition: "left .15s",
|
|
99
|
+
},
|
|
100
|
+
knobOff: {
|
|
101
|
+
position: "absolute",
|
|
102
|
+
top: 2,
|
|
103
|
+
left: 3,
|
|
104
|
+
width: 12,
|
|
105
|
+
height: 12,
|
|
106
|
+
borderRadius: "50%",
|
|
107
|
+
background: "#fff",
|
|
108
|
+
transition: "left .15s",
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
async function call(pathname, method) {
|
|
113
|
+
const res = await fetch(pathname, { method: method || "GET" });
|
|
114
|
+
const text = await res.text();
|
|
115
|
+
let data;
|
|
116
|
+
try {
|
|
117
|
+
data = JSON.parse(text);
|
|
118
|
+
} catch {
|
|
119
|
+
data = { ok: false, message: text.slice(0, 200) };
|
|
120
|
+
}
|
|
121
|
+
if (!res.ok && data && !data.message) data.message = "HTTP " + res.status;
|
|
122
|
+
return data;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function FriendlyRow(props) {
|
|
126
|
+
return h(
|
|
127
|
+
"div",
|
|
128
|
+
{ style: S.grid },
|
|
129
|
+
h("div", { style: S.key }, props.label),
|
|
130
|
+
h("div", { style: S.val }, props.value || "—"),
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function Card() {
|
|
135
|
+
const [open, setOpen] = React.useState(false);
|
|
136
|
+
const [status, setStatus] = React.useState(null);
|
|
137
|
+
const [busy, setBusy] = React.useState("");
|
|
138
|
+
const [msg, setMsg] = React.useState("");
|
|
139
|
+
|
|
140
|
+
const load = React.useCallback(() => {
|
|
141
|
+
call("/crossterm/api/status")
|
|
142
|
+
.then((data) => {
|
|
143
|
+
if (data && data.ok) setStatus(data);
|
|
144
|
+
else setMsg((data && data.message) || "状态不可用");
|
|
145
|
+
})
|
|
146
|
+
.catch((error) => setMsg("状态不可用:" + String((error && error.message) || error)));
|
|
147
|
+
}, []);
|
|
148
|
+
|
|
149
|
+
React.useEffect(() => {
|
|
150
|
+
load();
|
|
151
|
+
}, [load]);
|
|
152
|
+
|
|
153
|
+
const onCheck = async () => {
|
|
154
|
+
setBusy("check");
|
|
155
|
+
setMsg("正在检查更新…");
|
|
156
|
+
try {
|
|
157
|
+
const data = await call("/crossterm/api/check", "POST");
|
|
158
|
+
if (!data || !data.ok) setMsg("检查失败:" + ((data && data.message) || "未知错误"));
|
|
159
|
+
else if (data.updateAvailable) setMsg(`发现新版本 ${data.latest}(当前 ${data.current})`);
|
|
160
|
+
else if (data.ahead) setMsg(`本地 ${data.current} 领先已发布版 ${data.latest}`);
|
|
161
|
+
else setMsg(`已是最新版本 ${data.current}`);
|
|
162
|
+
} catch (error) {
|
|
163
|
+
setMsg("检查失败:" + String((error && error.message) || error));
|
|
164
|
+
} finally {
|
|
165
|
+
setBusy("");
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const onUpdate = async () => {
|
|
170
|
+
setBusy("update");
|
|
171
|
+
setMsg("正在更新…");
|
|
172
|
+
try {
|
|
173
|
+
const data = await call("/crossterm/api/update", "POST");
|
|
174
|
+
setMsg((data && data.message) || (data && data.ok ? "已更新" : "未能更新"));
|
|
175
|
+
} catch (error) {
|
|
176
|
+
setMsg("更新失败:" + String((error && error.message) || error));
|
|
177
|
+
} finally {
|
|
178
|
+
setBusy("");
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const onToggleCloud = async () => {
|
|
183
|
+
const running = Boolean((status && status.cloud && status.cloud.running) || false);
|
|
184
|
+
setBusy("cloud");
|
|
185
|
+
setMsg(running ? "正在关闭云同步…" : "正在开启云同步…");
|
|
186
|
+
try {
|
|
187
|
+
const data = await call(running ? "/crossterm/api/cloud/stop" : "/crossterm/api/cloud/start", "POST");
|
|
188
|
+
setMsg((data && data.message) || (data && data.ok ? "已切换" : "未能切换"));
|
|
189
|
+
if (data && data.cloud && status) setStatus({ ...status, cloud: data.cloud });
|
|
190
|
+
} catch (error) {
|
|
191
|
+
setMsg("切换失败:" + String((error && error.message) || error));
|
|
192
|
+
} finally {
|
|
193
|
+
setBusy("");
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const version = status ? "v" + status.version : "";
|
|
198
|
+
const mode = status && status.install ? status.install.mode : "";
|
|
199
|
+
const cloud = (status && status.cloud) || {};
|
|
200
|
+
const subtitle = status
|
|
201
|
+
? `${status.node || "本机"} · ${status.chains && status.chains.length ? status.chains.join(" / ") : "未入链"}`
|
|
202
|
+
: "加载中…";
|
|
203
|
+
|
|
204
|
+
return h(
|
|
205
|
+
"div",
|
|
206
|
+
{ style: S.card },
|
|
207
|
+
h(
|
|
208
|
+
"div",
|
|
209
|
+
{
|
|
210
|
+
style: S.head,
|
|
211
|
+
onClick: () => setOpen((v) => !v),
|
|
212
|
+
role: "button",
|
|
213
|
+
tabIndex: 0,
|
|
214
|
+
},
|
|
215
|
+
h(
|
|
216
|
+
"div",
|
|
217
|
+
null,
|
|
218
|
+
h("div", { style: S.title }, `dsh-crossterm ${version}`),
|
|
219
|
+
h("div", { style: S.sub }, subtitle),
|
|
220
|
+
),
|
|
221
|
+
// h("span", { style: S.chevron }, open ? "\u2303" : "\u2304"),
|
|
222
|
+
h("span", { style: S.chevron }, open ? "▲" : "▼"),
|
|
223
|
+
),
|
|
224
|
+
open &&
|
|
225
|
+
h(
|
|
226
|
+
"div",
|
|
227
|
+
{ style: S.body },
|
|
228
|
+
h(FriendlyRow, { label: "当前版本", value: status ? `v${status.version}${mode === "link" ? "(源码 link:)" : ""}` : "" }),
|
|
229
|
+
h(FriendlyRow, {
|
|
230
|
+
label: "当前终端",
|
|
231
|
+
value: status ? `${status.node}(${status.nodeId})· ${status.hostname}` : "",
|
|
232
|
+
}),
|
|
233
|
+
h(
|
|
234
|
+
"div",
|
|
235
|
+
{ style: S.grid },
|
|
236
|
+
h("div", { style: S.key }, "云端位置"),
|
|
237
|
+
h(
|
|
238
|
+
"div",
|
|
239
|
+
{ style: S.val },
|
|
240
|
+
h(
|
|
241
|
+
"span",
|
|
242
|
+
null,
|
|
243
|
+
status
|
|
244
|
+
? `${(cloud.provider && cloud.provider.name) || "云端"} · ${cloud.root || "—"}${cloud.running ? "(同步中)" : "(未同步)"}`
|
|
245
|
+
: "—",
|
|
246
|
+
),
|
|
247
|
+
status
|
|
248
|
+
? h(
|
|
249
|
+
"button",
|
|
250
|
+
{
|
|
251
|
+
style: cloud.running ? S.switchOn : S.switchOff,
|
|
252
|
+
onClick: (event) => {
|
|
253
|
+
event.stopPropagation();
|
|
254
|
+
void onToggleCloud();
|
|
255
|
+
},
|
|
256
|
+
disabled: busy !== "",
|
|
257
|
+
title: cloud.running ? "点击关闭云同步" : "点击开启云同步",
|
|
258
|
+
"aria-label": cloud.running ? "云同步开启中" : "云同步已关闭",
|
|
259
|
+
},
|
|
260
|
+
h("span", { style: cloud.running ? S.knobOn : S.knobOff }),
|
|
261
|
+
)
|
|
262
|
+
: null,
|
|
263
|
+
),
|
|
264
|
+
),
|
|
265
|
+
h(
|
|
266
|
+
"div",
|
|
267
|
+
{ style: S.actions },
|
|
268
|
+
h("button", { style: S.btn, onClick: onCheck, disabled: busy !== "" }, busy === "check" ? "检查中…" : "检查更新"),
|
|
269
|
+
h("button", { style: S.btnPrimary, onClick: onUpdate, disabled: busy !== "" }, busy === "update" ? "更新中…" : "更新"),
|
|
270
|
+
),
|
|
271
|
+
msg ? h("div", { style: S.msg }, msg) : null,
|
|
272
|
+
),
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const name = "dsh-crossterm";
|
|
277
|
+
const inject = ["slots"];
|
|
278
|
+
|
|
279
|
+
function apply(ctx) {
|
|
280
|
+
ctx.slots.inject("plugins.bundle.config", () =>
|
|
281
|
+
ctx.slots.register(
|
|
282
|
+
{
|
|
283
|
+
name: "plugins.bundle.config",
|
|
284
|
+
key: "dsh-crossterm",
|
|
285
|
+
},
|
|
286
|
+
Card,
|
|
287
|
+
),
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
exports.name = name;
|
|
292
|
+
exports.inject = inject;
|
|
293
|
+
exports.apply = apply;
|
|
294
|
+
return module.exports;
|
|
295
|
+
},
|
|
296
|
+
});
|
package/cordis.patch.yml
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
#
|
|
13
13
|
# dsh-crossterm/parts/session 跨会话:term_link / term_unlink / term_broadcast / term_inbox / term_config
|
|
14
14
|
# dsh-crossterm/parts/terminal 跨终端:term_whoami / term_list / term_send / term_send_files
|
|
15
|
-
# dsh-crossterm/parts/device 跨设备:term_cloud
|
|
15
|
+
# dsh-crossterm/parts/device 跨设备:term_cloud(云端总线同步)
|
|
16
16
|
# dsh-crossterm/parts/account 跨账号:预留占位,暂不挂工具
|
|
17
17
|
#
|
|
18
18
|
# 组件名来自 package.json 的 exports 映射:
|
|
@@ -24,7 +24,9 @@
|
|
|
24
24
|
|
|
25
25
|
- insert:
|
|
26
26
|
- id: crossterm-session
|
|
27
|
-
|
|
27
|
+
# 第一行用裸包名:浏览器的 client 半端只对「裸包名」行生效,
|
|
28
|
+
# 插件页里的配置卡片就挂在这一行上。名字仍由 locale/zh.json 的 meta.title 提供。
|
|
29
|
+
name: dsh-crossterm
|
|
28
30
|
config:
|
|
29
31
|
part: session
|
|
30
32
|
- id: crossterm-terminal
|
package/lib/bus.js
CHANGED
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 { registerWeb } from './web.js'
|
|
39
40
|
|
|
40
41
|
const PLUGIN = 'dsh-crossterm'
|
|
41
42
|
|
|
@@ -1615,7 +1616,7 @@ function buildTools(term, agent, part = 'all') {
|
|
|
1615
1616
|
|
|
1616
1617
|
const cloud = defineTool({
|
|
1617
1618
|
name: 'term_cloud',
|
|
1618
|
-
description: '
|
|
1619
|
+
description: '云总线同步:查看/开启/关闭本机总线与云端目录的镜像同步,或立刻同步一次。跨设备互通靠它把邮件、收件箱、链、链接、在线状态搬到云端。',
|
|
1619
1620
|
parameters: {
|
|
1620
1621
|
action: { type: 'string', enum: ['status', 'start', 'stop', 'once'], description: 'status=查看状态;start=开启(本进程内生效);stop=关闭;once=立刻同步一次' },
|
|
1621
1622
|
},
|
|
@@ -1703,6 +1704,10 @@ export default {
|
|
|
1703
1704
|
// 心跳/云同步只在第一个组件加载时启动一次,最后一个组件卸载时才停。
|
|
1704
1705
|
const term = acquireTerm(ctx)
|
|
1705
1706
|
term.parts.add(part)
|
|
1707
|
+
// 插件页卡片的宿主半端:三条路由只注册一次,读版本/终端/云端位置,并支持检查更新与更新
|
|
1708
|
+
try {
|
|
1709
|
+
registerWeb(ctx, term, (line) => void term.warn(line))
|
|
1710
|
+
} catch { /* ignore */ }
|
|
1706
1711
|
const mounted = new Map()
|
|
1707
1712
|
|
|
1708
1713
|
const mount = (agent) => {
|
package/lib/web.js
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
// 插件页卡片的宿主半端:注册三条本地 HTTP 路由,供浏览器半端(client/client.js)读取
|
|
2
|
+
// 状态、检查 npm 上的新版本、一键更新(仅 registry 安装模式可用)。
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import os from 'node:os'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import { spawn } from 'node:child_process'
|
|
7
|
+
import { VERSION } from './bus.js'
|
|
8
|
+
|
|
9
|
+
const PKG = 'dsh-crossterm'
|
|
10
|
+
const NPM_REGISTRIES = ['https://registry.npmmirror.com', 'https://registry.npmjs.org']
|
|
11
|
+
|
|
12
|
+
let installed = false
|
|
13
|
+
|
|
14
|
+
function json(res, code, data) {
|
|
15
|
+
const body = JSON.stringify(data)
|
|
16
|
+
res.writeHead(code, {
|
|
17
|
+
'content-type': 'application/json; charset=utf-8',
|
|
18
|
+
'cache-control': 'no-store',
|
|
19
|
+
})
|
|
20
|
+
res.end(body)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** 找到装着本插件的 profile(<dsh-home>/profiles/<name>/package.json) */
|
|
24
|
+
function profileInfo(busDir) {
|
|
25
|
+
// 先找「正在运行的那个 profile」:桌面宿主把 profile 目录作为 argv 传给自己,
|
|
26
|
+
// 形如 "...DeepSeek Harness.exe <app.asar/dsh> <dsh-home>/profiles/desktop <runtime>..."。
|
|
27
|
+
// 这台机器上 desktop / crossterm-test / crossterm-web 三个 profile 都装了本插件,
|
|
28
|
+
// 只按目录字母序扫会一直报 crossterm-test(DSH_PROFILE 只注入给工具子进程,宿主进程没有)。
|
|
29
|
+
for (const arg of process.argv.slice(0, 10)) {
|
|
30
|
+
if (typeof arg !== 'string') continue
|
|
31
|
+
const hit = /[\\/]profiles[\\/]([^\\/]+)$/.exec(arg)
|
|
32
|
+
if (!hit) continue
|
|
33
|
+
try {
|
|
34
|
+
const doc = JSON.parse(fs.readFileSync(path.join(arg, 'package.json'), 'utf8'))
|
|
35
|
+
const spec = doc && doc.dependencies && doc.dependencies[PKG]
|
|
36
|
+
if (typeof spec === 'string' && spec) return { name: hit[1], dir: arg, spec }
|
|
37
|
+
} catch {
|
|
38
|
+
/* ignore */
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const envName = process.env.DSH_PROFILE
|
|
42
|
+
const envDir = process.env.DSH_PROFILE_DIR
|
|
43
|
+
if (envDir) {
|
|
44
|
+
try {
|
|
45
|
+
const doc = JSON.parse(fs.readFileSync(path.join(envDir, 'package.json'), 'utf8'))
|
|
46
|
+
const spec = doc && doc.dependencies && doc.dependencies[PKG]
|
|
47
|
+
if (typeof spec === 'string' && spec) {
|
|
48
|
+
return { name: envName || path.basename(envDir), dir: envDir, spec }
|
|
49
|
+
}
|
|
50
|
+
} catch {
|
|
51
|
+
/* ignore */
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const home = path.dirname(busDir || '')
|
|
55
|
+
const dir = path.join(home, 'profiles')
|
|
56
|
+
try {
|
|
57
|
+
for (const name of fs.readdirSync(dir)) {
|
|
58
|
+
let doc
|
|
59
|
+
try {
|
|
60
|
+
doc = JSON.parse(fs.readFileSync(path.join(dir, name, 'package.json'), 'utf8'))
|
|
61
|
+
} catch {
|
|
62
|
+
continue
|
|
63
|
+
}
|
|
64
|
+
const spec = doc && doc.dependencies && doc.dependencies[PKG]
|
|
65
|
+
if (typeof spec === 'string' && spec) {
|
|
66
|
+
return { name, dir: path.join(dir, name), spec }
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
} catch {
|
|
70
|
+
/* ignore */
|
|
71
|
+
}
|
|
72
|
+
return null
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function installMode(spec) {
|
|
76
|
+
if (!spec) return 'unknown'
|
|
77
|
+
return spec.startsWith('link:') || spec.startsWith('file:') ? 'link' : 'registry'
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** 云端用哪家网盘:现在只接了百度,靠凭证文件认;以后加别家在这里扩展 */
|
|
81
|
+
function cloudProvider(cfg) {
|
|
82
|
+
if (!cfg) return { id: '', name: '' }
|
|
83
|
+
if (cfg.baiduAppFile) return { id: 'baidu', name: '百度网盘' }
|
|
84
|
+
if (cfg.cloudRoot) return { id: 'custom', name: '自定义云端' }
|
|
85
|
+
return { id: '', name: '' }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** 语义化版本比较:只比 主.次.补(忽略预发布尾巴),a>b 返回正数 */
|
|
89
|
+
function compareVersion(a, b) {
|
|
90
|
+
const num = (v) => String(v || '').split('-')[0].split('.').map((x) => parseInt(x, 10) || 0)
|
|
91
|
+
const x = num(a)
|
|
92
|
+
const y = num(b)
|
|
93
|
+
for (let i = 0; i < 3; i++) {
|
|
94
|
+
const d = (x[i] || 0) - (y[i] || 0)
|
|
95
|
+
if (d !== 0) return d
|
|
96
|
+
}
|
|
97
|
+
return 0
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function latestVersion() {
|
|
101
|
+
let last = ''
|
|
102
|
+
for (const registry of NPM_REGISTRIES) {
|
|
103
|
+
const ctrl = new AbortController()
|
|
104
|
+
const timer = setTimeout(() => ctrl.abort(), 8000)
|
|
105
|
+
try {
|
|
106
|
+
const res = await fetch(`${registry}/${PKG}/latest`, {
|
|
107
|
+
signal: ctrl.signal,
|
|
108
|
+
headers: { accept: 'application/json' },
|
|
109
|
+
})
|
|
110
|
+
if (!res.ok) {
|
|
111
|
+
last = `HTTP ${res.status}`
|
|
112
|
+
continue
|
|
113
|
+
}
|
|
114
|
+
const doc = await res.json()
|
|
115
|
+
if (doc && typeof doc.version === 'string') return doc.version
|
|
116
|
+
last = 'no version field'
|
|
117
|
+
} catch (error) {
|
|
118
|
+
last = String((error && error.message) || error)
|
|
119
|
+
} finally {
|
|
120
|
+
clearTimeout(timer)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
throw new Error(last || 'unknown')
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function run(cmd, args, cwd) {
|
|
127
|
+
return new Promise((resolve) => {
|
|
128
|
+
let out = ''
|
|
129
|
+
let child
|
|
130
|
+
try {
|
|
131
|
+
child = spawn(cmd, args, {
|
|
132
|
+
cwd,
|
|
133
|
+
shell: process.platform === 'win32',
|
|
134
|
+
windowsHide: true,
|
|
135
|
+
})
|
|
136
|
+
} catch (error) {
|
|
137
|
+
resolve({ code: -1, out: String((error && error.message) || error) })
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
const push = (chunk) => {
|
|
141
|
+
out += String(chunk)
|
|
142
|
+
if (out.length > 20000) out = out.slice(-20000)
|
|
143
|
+
}
|
|
144
|
+
child.stdout && child.stdout.on('data', push)
|
|
145
|
+
child.stderr && child.stderr.on('data', push)
|
|
146
|
+
child.on('error', (error) => resolve({ code: -1, out: out + String((error && error.message) || error) }))
|
|
147
|
+
child.on('close', (code) => resolve({ code, out }))
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function statusOf(term) {
|
|
152
|
+
const cfg = term.cfg || {}
|
|
153
|
+
const info = profileInfo(cfg.busDir)
|
|
154
|
+
const mode = installMode(info && info.spec)
|
|
155
|
+
const chains = []
|
|
156
|
+
try {
|
|
157
|
+
for (const link of term.links.values()) if (!chains.includes(link.code)) chains.push(link.code)
|
|
158
|
+
} catch {
|
|
159
|
+
/* ignore */
|
|
160
|
+
}
|
|
161
|
+
const cloud = term.cloud
|
|
162
|
+
return {
|
|
163
|
+
ok: true,
|
|
164
|
+
version: VERSION,
|
|
165
|
+
node: cfg.name || '',
|
|
166
|
+
nodeId: term.nodeId || '',
|
|
167
|
+
hostname: os.hostname(),
|
|
168
|
+
platform: process.platform,
|
|
169
|
+
workspace: cfg.workspace || '',
|
|
170
|
+
install: { profile: (info && info.name) || '', spec: (info && info.spec) || '', mode },
|
|
171
|
+
cloud: {
|
|
172
|
+
enabled: cfg.cloudEnabled === true,
|
|
173
|
+
running: Boolean(cloud && cloud.running),
|
|
174
|
+
root: cfg.cloudRoot || '',
|
|
175
|
+
intervalMs: cfg.cloudIntervalMs || 0,
|
|
176
|
+
provider: cloudProvider(cfg),
|
|
177
|
+
},
|
|
178
|
+
chains,
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* 只注册一次;行被关闭时随 fiber 一起注销,重新启用会再注册。
|
|
184
|
+
*/
|
|
185
|
+
export function registerWeb(ctx, term, log) {
|
|
186
|
+
if (installed) return
|
|
187
|
+
installed = true
|
|
188
|
+
try {
|
|
189
|
+
ctx.inject(['webServer'], (webCtx) => {
|
|
190
|
+
webCtx.effect(() => {
|
|
191
|
+
const disposers = []
|
|
192
|
+
disposers.push(
|
|
193
|
+
webCtx.webServer.register({
|
|
194
|
+
kind: 'exact',
|
|
195
|
+
path: '/crossterm/api/status',
|
|
196
|
+
handler: (req, res) => json(res, 200, statusOf(term)),
|
|
197
|
+
}),
|
|
198
|
+
)
|
|
199
|
+
disposers.push(
|
|
200
|
+
webCtx.webServer.register({
|
|
201
|
+
kind: 'exact',
|
|
202
|
+
path: '/crossterm/api/check',
|
|
203
|
+
handler: async (req, res) => {
|
|
204
|
+
try {
|
|
205
|
+
const latest = await latestVersion()
|
|
206
|
+
// 只有 npm 上的版本「更新」才算有更新:本地源码版常领先于已发布版,
|
|
207
|
+
// 用 !== 会把 0.3.32 说成「发现新版本 0.3.31」。
|
|
208
|
+
const updateAvailable = compareVersion(latest, VERSION) > 0
|
|
209
|
+
const ahead = compareVersion(VERSION, latest) > 0
|
|
210
|
+
json(res, 200, { ok: true, current: VERSION, latest, updateAvailable, ahead })
|
|
211
|
+
} catch (error) {
|
|
212
|
+
json(res, 200, { ok: false, message: String((error && error.message) || error) })
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
}),
|
|
216
|
+
)
|
|
217
|
+
disposers.push(
|
|
218
|
+
webCtx.webServer.register({
|
|
219
|
+
kind: 'exact',
|
|
220
|
+
path: '/crossterm/api/cloud/start',
|
|
221
|
+
handler: (req, res) => {
|
|
222
|
+
try {
|
|
223
|
+
term.enableCloud()
|
|
224
|
+
json(res, 200, { ok: true, message: '云同步已开启', cloud: statusOf(term).cloud })
|
|
225
|
+
} catch (error) {
|
|
226
|
+
json(res, 200, { ok: false, message: `开启失败:${String((error && error.message) || error)}` })
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
}),
|
|
230
|
+
)
|
|
231
|
+
disposers.push(
|
|
232
|
+
webCtx.webServer.register({
|
|
233
|
+
kind: 'exact',
|
|
234
|
+
path: '/crossterm/api/cloud/stop',
|
|
235
|
+
handler: (req, res) => {
|
|
236
|
+
try {
|
|
237
|
+
term.disableCloud()
|
|
238
|
+
json(res, 200, { ok: true, message: '云同步已关闭(本进程内生效,重启后按配置恢复)', cloud: statusOf(term).cloud })
|
|
239
|
+
} catch (error) {
|
|
240
|
+
json(res, 200, { ok: false, message: `关闭失败:${String((error && error.message) || error)}` })
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
}),
|
|
244
|
+
)
|
|
245
|
+
disposers.push(
|
|
246
|
+
webCtx.webServer.register({
|
|
247
|
+
kind: 'exact',
|
|
248
|
+
path: '/crossterm/api/update',
|
|
249
|
+
handler: async (req, res) => {
|
|
250
|
+
const info = profileInfo(term.cfg && term.cfg.busDir)
|
|
251
|
+
if (!info) {
|
|
252
|
+
json(res, 200, { ok: false, message: '找不到安装本插件的 profile' })
|
|
253
|
+
return
|
|
254
|
+
}
|
|
255
|
+
if (installMode(info.spec) === 'link') {
|
|
256
|
+
json(res, 200, {
|
|
257
|
+
ok: false,
|
|
258
|
+
mode: 'link',
|
|
259
|
+
message: '本机是源码(link:)安装,代码即工作区里的源码;更新请直接改源码后重启 DSH',
|
|
260
|
+
})
|
|
261
|
+
return
|
|
262
|
+
}
|
|
263
|
+
const result = await run('dsh', ['plugin', '--profile', info.name, 'add', `${PKG}@latest`], info.dir)
|
|
264
|
+
json(res, 200, {
|
|
265
|
+
ok: result.code === 0,
|
|
266
|
+
mode: 'registry',
|
|
267
|
+
needRestart: result.code === 0,
|
|
268
|
+
message:
|
|
269
|
+
result.code === 0
|
|
270
|
+
? '已更新,重启 DSH 后生效'
|
|
271
|
+
: (result.out || `dsh 退出码 ${result.code}`).slice(-1200),
|
|
272
|
+
})
|
|
273
|
+
},
|
|
274
|
+
}),
|
|
275
|
+
)
|
|
276
|
+
return () => {
|
|
277
|
+
installed = false
|
|
278
|
+
for (const dispose of disposers) {
|
|
279
|
+
try {
|
|
280
|
+
dispose()
|
|
281
|
+
} catch {
|
|
282
|
+
/* ignore */
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}, 'dsh-crossterm: 插件页卡片路由')
|
|
287
|
+
})
|
|
288
|
+
} catch (error) {
|
|
289
|
+
installed = false
|
|
290
|
+
if (typeof log === 'function') log(`web 路由注册失败:${(error && error.message) || error}`)
|
|
291
|
+
}
|
|
292
|
+
}
|
package/locale/en.json
ADDED
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-crossterm",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.33",
|
|
4
4
|
"description": "DSH 跨终端互联:同一账号下的多个 DSH 终端/进程互发邮件与文件(主题内联、正文落盘,收件方自己读)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": "./lib/index.js",
|
|
9
|
+
"./client": "./client/client.js",
|
|
9
10
|
"./package.json": "./package.json",
|
|
11
|
+
"./locale/en.json": "./locale/en.json",
|
|
12
|
+
"./locale/zh.json": "./locale/zh.json",
|
|
10
13
|
"./parts/session": "./lib/index.js",
|
|
11
14
|
"./parts/terminal": "./lib/index.js",
|
|
12
15
|
"./parts/device": "./lib/index.js",
|
|
@@ -18,6 +21,8 @@
|
|
|
18
21
|
},
|
|
19
22
|
"files": [
|
|
20
23
|
"lib",
|
|
24
|
+
"client",
|
|
25
|
+
"locale",
|
|
21
26
|
"parts",
|
|
22
27
|
"cordis.patch.yml",
|
|
23
28
|
"README.md"
|
|
@@ -42,6 +47,9 @@
|
|
|
42
47
|
"dsh": {
|
|
43
48
|
"bundle": {
|
|
44
49
|
"patch": "./cordis.patch.yml"
|
|
50
|
+
},
|
|
51
|
+
"client": {
|
|
52
|
+
"platform": "web"
|
|
45
53
|
}
|
|
46
54
|
},
|
|
47
55
|
"peerDependencies": {
|