dsh-crossterm 0.8.4 → 0.8.6
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 +34 -0
- package/MAINTAINERS.md +73 -0
- package/README.md +41 -12
- package/client/client.js +85 -9
- package/lib/bus.js +16 -1
- package/lib/cloud/match.js +1 -0
- package/lib/index.js +8 -2
- package/lib/logs.js +192 -0
- package/lib/web.js +72 -6
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
# dsh-crossterm 变更记录
|
|
2
2
|
|
|
3
|
+
## 0.8.6 — 2026-10-19
|
|
4
|
+
|
|
5
|
+
### 云端设备列表:只列别人,带权限和状态
|
|
6
|
+
|
|
7
|
+
- **不显示本机**:`/crossterm/api/devices` 与卡片「云端设备」行都过滤掉本机 ——
|
|
8
|
+
本机已经在「当前终端」一栏,列在设备里等于把自己当远端。
|
|
9
|
+
- **每台标角色**:`annotateDevices(list, { selfId, ownerDevice })`(`lib/web.js` 导出,纯函数可测);
|
|
10
|
+
角色取云端 `_account.json` 的 `ownerDevice`:是它 → **管理员**,其余 → **成员**。
|
|
11
|
+
- **离线时间口径修正**:以前只读 `updatedAt`,新布局心跳写的是 `at`,离线设备全显示「未知」;
|
|
12
|
+
现在按 `updatedAt → at → lastSeen` 依次取,显示「x 分钟前」。
|
|
13
|
+
- **`term_list` 云端行带角色**:`lib/cloud/match.js` 的 `deviceRow()` 增 `role`;
|
|
14
|
+
`Term.cloudDevices()` 过滤本机,跨设备匹配更干净。
|
|
15
|
+
- 探针 `probe-devices-086.mjs` **11 PASS**(排除本机、剔除无 nodeId、管理员/成员、字段保留、
|
|
16
|
+
at 三种来源、已有 role 不覆盖、空列表)。
|
|
17
|
+
- **要不要跟着动手:不用**(纯展示改动,装上即可)。
|
|
18
|
+
|
|
19
|
+
## 0.8.5 — 2026-10-19
|
|
20
|
+
|
|
21
|
+
### 负责人留痕 + 运行日志可查(日志轮转)
|
|
22
|
+
|
|
23
|
+
- **负责人写清楚**:新增 `MAINTAINERS.md` —— 任期表(起止时间精确到分钟)、换人时怎么改、
|
|
24
|
+
机器可读注释(`<!-- maintainers {...} -->`)。当前负责人 `session-b6aa099b…`(TS-01),
|
|
25
|
+
助手 `session-ccf34a5a…`(同端多会话2),上一任 `session-6ef6da30…`(2026-10-18 01:09 因会话故障交卸)。
|
|
26
|
+
- **换人及时更正**:插件启动时对比 `MAINTAINERS.md` 里的当前负责人,一变就往
|
|
27
|
+
`<busDir>\logs\ownership.log` 追加一行(`logs/ownership-seen.json` 记住已记录的人,不重复写);
|
|
28
|
+
`GET /crossterm/api/logs` 一并返回最近 10 条负责人变更,卡片刻「维护者」一栏。
|
|
29
|
+
- **日志不再无限涨**:`<busDir>\logs\<nodeId>.jsonl` 超过 `logMaxBytes`(默认 5 MB)就压成
|
|
30
|
+
`logs/archive/<nodeId>-<时间戳>.jsonl.gz`,只留最近 `logKeep` 份(默认 20);写日志时每 200 行检查一次。
|
|
31
|
+
- **方便调阅**:新增 `GET /crossterm/api/logs?lines=120`(要接口令牌),返回当前文件、大小、归档清单、
|
|
32
|
+
最近 N 行(当前不够时自动接上一个归档的尾巴);卡片底部新增「运行日志」按钮,点开即看。
|
|
33
|
+
- 探针 `probe-logs-maintainers.mjs` **22 PASS**(轮转/清空/归档/清单/行数上限/keep、负责人解析、
|
|
34
|
+
首次记录、重复不记、about 带 maintainers)。
|
|
35
|
+
- **要不要跟着动手:不用**(装上即可;想让日志更早归档可改 `config.json` 的 `logMaxBytes`)。
|
|
36
|
+
|
|
3
37
|
## 0.8.4 — 2026-10-19
|
|
4
38
|
|
|
5
39
|
### 修备份统计口径:卡片上「已归档 0/24 · 活跃中 38/10」
|
package/MAINTAINERS.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# 维护者:这个插件由谁在改
|
|
2
|
+
|
|
3
|
+
插件目录 `F:\dsh\001X\dsh-crossterm`(npm 包 `dsh-crossterm`)只有「当前负责人」能直接改代码,
|
|
4
|
+
其他会话只读(助手会话按主负责人的派活改 `lib/*.js`)。换人时要**及时**更正这里
|
|
5
|
+
(发现换人就当天改,别拖到下次发版),并留下可核对的时间,事后能查清「哪一版是谁做的」。
|
|
6
|
+
|
|
7
|
+
<!-- maintainers
|
|
8
|
+
{
|
|
9
|
+
"current": {
|
|
10
|
+
"session": "session-b6aa099b-cfa6-42b7-961c-10f720236219",
|
|
11
|
+
"short": "b6aa099b",
|
|
12
|
+
"name": "互通插件主维护",
|
|
13
|
+
"role": "负责人",
|
|
14
|
+
"terminal": "TS-01",
|
|
15
|
+
"nodeId": "ts-01-a52535",
|
|
16
|
+
"since": "2026-10-18T00:59:27+08:00",
|
|
17
|
+
"basis": "用户交接:原负责人会话故障,指定本会话接手"
|
|
18
|
+
},
|
|
19
|
+
"assistant": {
|
|
20
|
+
"session": "session-ccf34a5a-ad6d-4423-9fa8-fd6ffc44052b",
|
|
21
|
+
"short": "ccf34a5a",
|
|
22
|
+
"name": "同端多会话2",
|
|
23
|
+
"role": "助手",
|
|
24
|
+
"terminal": "TS-01",
|
|
25
|
+
"nodeId": "ts-01-a52535",
|
|
26
|
+
"since": "2026-10-18T00:00:00+08:00",
|
|
27
|
+
"scope": "按负责人派活改 lib/*.js;不得改版本号、总线状态、重启、C 盘写入、AGENTS.md"
|
|
28
|
+
},
|
|
29
|
+
"previous": [
|
|
30
|
+
{
|
|
31
|
+
"session": "session-6ef6da30-a7f1-4a4e-...",
|
|
32
|
+
"short": "6ef6da30",
|
|
33
|
+
"name": "互通插件主维护(初版)",
|
|
34
|
+
"role": "负责人",
|
|
35
|
+
"terminal": "TS-01",
|
|
36
|
+
"nodeId": "ts-01-a52535",
|
|
37
|
+
"from": "2026-10-17T20:30:15+08:00",
|
|
38
|
+
"to": "2026-10-18T01:09:28+08:00",
|
|
39
|
+
"reason": "会话故障(投递不消费,转录停在 5.2 MB),用户改派"
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
-->
|
|
44
|
+
|
|
45
|
+
## 任期表
|
|
46
|
+
|
|
47
|
+
| 起(本地时间) | 止(本地时间) | 会话 | 终端 / nodeId | 角色 | 交接依据 |
|
|
48
|
+
| --- | --- | --- | --- | --- | --- |
|
|
49
|
+
| 2026-10-17 20:30 | 2026-10-18 01:09 | `session-6ef6da30…`(初版负责人) | TS-01 / `ts-01-a52535` | 负责人 | 会话故障,转录停止增长、投递不消费;用户改派 |
|
|
50
|
+
| 2026-10-18 00:59 | 至今 | `session-b6aa099b…`(互通插件主维护) | TS-01 / `ts-01-a52535` | 负责人 | 用户首条指令:「你现在是互通插件的新负责人」 |
|
|
51
|
+
| 2026-10-17 21:44 | 至今(按派活) | `session-ccf34a5a…`(同端多会话2) | TS-01 / `ts-01-a52535` | 助手 | 用户:「指定同端多会话2为你的助手,部分修改可让他进行」 |
|
|
52
|
+
|
|
53
|
+
时间口径:接手取该会话转录 `session.v4.jsonl.zstd` 的**创建时间**,交卸取其**最后写入时间**(本机 `C:\Users\Administrator\.dsh\sessions\--F-dsh-001X--\<会话>\`)。
|
|
54
|
+
|
|
55
|
+
## 换人时怎么做(缺一步都算没交接)
|
|
56
|
+
|
|
57
|
+
1. **改这张表**:给旧负责人补「止」时间,给新负责人补「起」时间(精确到分钟),写清交接依据(用户原话或故障证据)。
|
|
58
|
+
2. **改上面的 JSON 注释**:`current` 换成新负责人,旧的推进 `previous[]`;助手变动同步 `assistant`。卡片与 `GET /crossterm/api/about` 的「维护者」一栏读的就是这段。
|
|
59
|
+
3. **记一条日志**:往总线日志目录追加 `logs/ownership.log`,一行一个 JSON(见下)。
|
|
60
|
+
4. **CHANGELOG 提一句**:当版写「负责人变更:… → …(时间,依据)」。
|
|
61
|
+
5. **通知相关会话**:在链上用 `term_send` / `term_broadcast` 说明移交,并提醒对方不要再按旧负责人办事。
|
|
62
|
+
|
|
63
|
+
`logs/ownership.log` 每行一条,格式:
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{"at":"2026-10-18T00:59:27+08:00","from":"6ef6da30","to":"b6aa099b","role":"负责人","basis":"用户交接:原负责人会话故障"}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## 规则(与 AGENTS.md §7 一致)
|
|
70
|
+
|
|
71
|
+
- 只有当前负责人能改 `F:\dsh\001X\dsh-crossterm\`;其他会话可以测、可以报 bug、不能改。
|
|
72
|
+
- 助手只能按负责人的派活改,且不得动版本号、总线状态文件、重启、C 盘写入、`AGENTS.md`。
|
|
73
|
+
- 换人不是「口头说说」:没更新本文件、没写 `logs/ownership.log`,就还是旧负责人在负责。
|
package/README.md
CHANGED
|
@@ -28,19 +28,30 @@ A 终端看不到 B 终端里有哪个会话、在干什么,也没法把一条
|
|
|
28
28
|
> `locatePkgJson()` 对子路径行返回 undefined),而插件页顶部标题与这一行共用同一份 `locale/zh.json` 的 meta,
|
|
29
29
|
> 所以它单独做总开关,标题带「(总开关)」;卡片头用 `shortTitle` 显示短名「跨端互通」。
|
|
30
30
|
|
|
31
|
-
**本版更新(0.8.
|
|
31
|
+
**本版更新(0.8.6)**
|
|
32
32
|
|
|
33
|
-
-
|
|
34
|
-
|
|
35
|
-
-
|
|
33
|
+
- **云端设备只列「别人」**:卡片里那行不再显示本机(自己已经在「当前终端」一栏),
|
|
34
|
+
只列同一账号下的其他设备,避免把「本机」也当远端看。
|
|
35
|
+
- **每台带权限和状态**:逐台显示 `名称 · 管理员/成员 · 在线/离线(x 分钟前)· v版本号`。
|
|
36
|
+
管理员就是云端 `_account.json` 里 `ownerDevice` 那台;其余都是成员。
|
|
37
|
+
- **离线时间不再显示「未知」**:以前只认 `updatedAt`,新布局心跳写的是 `at`,
|
|
38
|
+
于是离线设备一律「未知」;现在按 `updatedAt → at → lastSeen` 依次取,显示「x 分钟前」。
|
|
39
|
+
- **`term_list` 的云端行也带角色**:agent 侧看到的云端设备行同样标管理员/成员。
|
|
40
|
+
- 纯展示改动,装上即可,无需改动配置。
|
|
36
41
|
|
|
37
|
-
**上一版(0.8.
|
|
42
|
+
**上一版(0.8.5)**
|
|
38
43
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
-
|
|
42
|
-
|
|
43
|
-
-
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
- **负责人写清楚、换人及时更正**:新增 `MAINTAINERS.md`(任期表 + 换人步骤 + 机器可读注释)。
|
|
47
|
+
当前负责人 `session-b6aa099b…`(TS-01),助手 `session-ccf34a5a…`(同端多会话2),
|
|
48
|
+
上一任是 `session-6ef6da30…`(2026-10-18 01:09 因会话故障交卸)。
|
|
49
|
+
负责人一变,插件启动时自动往 `<busDir>\logs\ownership.log` 记一行。
|
|
50
|
+
- **运行日志存档、可轮转**:`logs\<nodeId>.jsonl` 超过 5 MB 自动压成
|
|
51
|
+
`logs\archive\<nodeId>-<时间戳>.jsonl.gz`,只留最近 20 份;不再担心日志把盘写满。
|
|
52
|
+
- **方便调阅**:卡片底部新增「运行日志」按钮(看最近 120 行,当前文件不够时接上前一个归档);
|
|
53
|
+
接口是 `GET /crossterm/api/logs?lines=120`(需接口令牌),同时返回归档清单与最近 10 条负责人变更。
|
|
54
|
+
- 卡片新增「维护者」一栏,直接显示当前负责人(含上一任与交卸时间)。
|
|
44
55
|
|
|
45
56
|
**上一版(0.8.2)**
|
|
46
57
|
|
|
@@ -492,8 +503,26 @@ dsh/
|
|
|
492
503
|
- 收到消息后**会作为用户消息进入会话**,agent 会据此行动——所以总线目录只应放在你信任的位置;
|
|
493
504
|
共享盘方式下,谁能写这个目录,谁就能往你的会话里塞话。
|
|
494
505
|
- 文件落地目录可被 agent 读写,`inboxDir` 建议指向专门目录而非系统目录。
|
|
495
|
-
-
|
|
496
|
-
|
|
506
|
+
- **谁能改这个插件**:看仓库根目录的 [`MAINTAINERS.md`](./MAINTAINERS.md)。当前负责人是
|
|
507
|
+
`session-b6aa099b…`(TS-01 / `ts-01-a52535`,自 2026-10-18 00:59),助手是 `session-ccf34a5a…`
|
|
508
|
+
(同端多会话2,只能按负责人派活改 `lib/*.js`)。其他会话(QQ 会话、别的终端/窗口)默认只读,
|
|
509
|
+
可协助测试与报 bug;**要改代码必须经用户明确点名允许**(见工作区 `AGENTS.md` 第 7 条)。
|
|
510
|
+
|
|
511
|
+
### 八.1 负责人怎么交接、日志在哪
|
|
512
|
+
|
|
513
|
+
- **换人**:改 `MAINTAINERS.md` 的任期表(起止时间精确到分钟),同步改文件顶部那段
|
|
514
|
+
`<!-- maintainers {...} -->` 注释(卡片与 `/crossterm/api/about` 的「维护者」读的就是它),
|
|
515
|
+
再在当版 `CHANGELOG.md` 写一句「负责人变更:… → …」。插件下次启动会把变更追加到
|
|
516
|
+
`<busDir>\logs\ownership.log`,两次启动之间不会重复写。
|
|
517
|
+
- **时间口径**:接手取会话转录 `session.v4.jsonl.zstd` 的创建时间,交卸取它的最后写入时间
|
|
518
|
+
(本机在 `C:\Users\<你>\.dsh\sessions\--F-dsh-001X--\<会话>\`)。
|
|
519
|
+
- **运行日志**:`<busDir>\logs\<nodeId>.jsonl`,一行一个 JSON(`{at, line}`)。
|
|
520
|
+
超过 `logMaxBytes`(默认 5 MB)自动压成 `logs\archive\<nodeId>-<时间戳>.jsonl.gz`,
|
|
521
|
+
只留最近 `logKeep` 份(默认 20)。
|
|
522
|
+
- **怎么调阅**:卡片底部「运行日志」按钮看最近 120 行;或
|
|
523
|
+
`curl -H "x-crossterm-token: <令牌>" "http://127.0.0.1:19387/crossterm/api/logs?lines=200"`
|
|
524
|
+
(令牌先 `GET /crossterm/api/token` 取);也可以直接读
|
|
525
|
+
`logs\ownership.log`(负责人变更)和 `logs\update.log`(更新过程)。
|
|
497
526
|
|
|
498
527
|
## 九、开发自测
|
|
499
528
|
|
package/client/client.js
CHANGED
|
@@ -175,13 +175,15 @@ window.__ModuleLoader__.load({
|
|
|
175
175
|
if (!(status && status.cloud && status.cloud.configured)) return "未连接云盘";
|
|
176
176
|
if (!devices || devices.loading) return "读取中…";
|
|
177
177
|
if (!devices.ok) return devices.message || "读取失败";
|
|
178
|
-
const list = devices.devices || [];
|
|
178
|
+
const list = (devices.devices || []).filter((d) => d && (!devices.me || d.nodeId !== devices.me));
|
|
179
179
|
if (!list.length) return "云端还没有其他设备";
|
|
180
180
|
return list
|
|
181
181
|
.map((d) => {
|
|
182
|
-
const label =
|
|
183
|
-
const
|
|
184
|
-
|
|
182
|
+
const label = d.name || d.nodeId || "未知设备";
|
|
183
|
+
const role = d.role || "成员";
|
|
184
|
+
const at = d.updatedAt || d.at || (d.lastSeen ? Date.parse(d.lastSeen) : 0);
|
|
185
|
+
const state = d.online ? "在线" : agoText(at);
|
|
186
|
+
return label + " · " + role + " · " + state + (d.version ? " · v" + d.version : "");
|
|
185
187
|
})
|
|
186
188
|
.join(";");
|
|
187
189
|
}
|
|
@@ -194,6 +196,32 @@ window.__ModuleLoader__.load({
|
|
|
194
196
|
return (n / 1024 / 1024 / 1024).toFixed(2) + " GB";
|
|
195
197
|
}
|
|
196
198
|
|
|
199
|
+
function maintainersText(about) {
|
|
200
|
+
const m = about && about.maintainers;
|
|
201
|
+
const cur = m && m.current;
|
|
202
|
+
if (!cur) return "";
|
|
203
|
+
const since = String(cur.since || "").slice(0, 10);
|
|
204
|
+
const bits = [];
|
|
205
|
+
bits.push(`${cur.name || cur.short || "负责人"}(${cur.short || ""})`);
|
|
206
|
+
if (since) bits.push(`自 ${since}`);
|
|
207
|
+
if (cur.terminal) bits.push(cur.terminal);
|
|
208
|
+
const prev = m && m.previous;
|
|
209
|
+
const head = bits.join(" · ");
|
|
210
|
+
return prev ? `${head}(上一任:${prev.short || prev.name || "—"},止 ${String(prev.to || "").slice(0, 10)})` : head;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function runLogText(log) {
|
|
214
|
+
if (!log || !log.lines) return "";
|
|
215
|
+
return log.lines
|
|
216
|
+
.map((row) => {
|
|
217
|
+
const at = String(row.at || "");
|
|
218
|
+
const stamp = at ? at.slice(11, 19) : "--:--:--";
|
|
219
|
+
return `${stamp} ${row.line || ""}`;
|
|
220
|
+
})
|
|
221
|
+
.join("\n");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
|
|
197
225
|
function backupText(backup) {
|
|
198
226
|
if (!backup || !backup.files) return "还没有备份(会在开云同步后自动开始)";
|
|
199
227
|
const when = backup.lastAt ? agoText(backup.lastAt) : "未知";
|
|
@@ -212,6 +240,8 @@ window.__ModuleLoader__.load({
|
|
|
212
240
|
const [devices, setDevices] = React.useState(null);
|
|
213
241
|
const [about, setAbout] = React.useState(null);
|
|
214
242
|
const [showLog, setShowLog] = React.useState(false);
|
|
243
|
+
const [runLog, setRunLog] = React.useState(null);
|
|
244
|
+
const [showRunLog, setShowRunLog] = React.useState(false);
|
|
215
245
|
|
|
216
246
|
const load = React.useCallback(() => {
|
|
217
247
|
call("/crossterm/api/status")
|
|
@@ -251,6 +281,25 @@ window.__ModuleLoader__.load({
|
|
|
251
281
|
.catch(() => {});
|
|
252
282
|
}, [open, about]);
|
|
253
283
|
|
|
284
|
+
// 运行日志(0.8.5):点开才读,读最近 120 行,含上一个归档的尾巴
|
|
285
|
+
const onRunLog = async () => {
|
|
286
|
+
if (showRunLog) {
|
|
287
|
+
setShowRunLog(false);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
setBusy("logs");
|
|
291
|
+
try {
|
|
292
|
+
const data = await call("/crossterm/api/logs?lines=120");
|
|
293
|
+
setRunLog(data);
|
|
294
|
+
setShowRunLog(Boolean(data && data.ok));
|
|
295
|
+
if (!data || !data.ok) setMsg("日志读取失败:" + ((data && data.message) || "未知错误"));
|
|
296
|
+
} catch (error) {
|
|
297
|
+
setMsg("日志读取失败:" + String((error && error.message) || error));
|
|
298
|
+
} finally {
|
|
299
|
+
setBusy("");
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
|
|
254
303
|
const onCheck = async () => {
|
|
255
304
|
setBusy("check");
|
|
256
305
|
setMsg("正在检查更新…");
|
|
@@ -433,6 +482,12 @@ window.__ModuleLoader__.load({
|
|
|
433
482
|
label: "当前终端",
|
|
434
483
|
value: status ? `${status.node}(${status.nodeId})· ${status.hostname}` : "",
|
|
435
484
|
}),
|
|
485
|
+
about && about.maintainers
|
|
486
|
+
? h(FriendlyRow, {
|
|
487
|
+
label: "维护者",
|
|
488
|
+
value: maintainersText(about),
|
|
489
|
+
})
|
|
490
|
+
: null,
|
|
436
491
|
h(
|
|
437
492
|
"div",
|
|
438
493
|
{ style: S.grid },
|
|
@@ -516,12 +571,33 @@ window.__ModuleLoader__.load({
|
|
|
516
571
|
? h(
|
|
517
572
|
"div",
|
|
518
573
|
{ style: { display: "flex", flexDirection: "column", gap: 8 } },
|
|
519
|
-
h(
|
|
520
|
-
"
|
|
521
|
-
{ style:
|
|
522
|
-
|
|
523
|
-
|
|
574
|
+
h(
|
|
575
|
+
"div",
|
|
576
|
+
{ style: { display: "flex", gap: 8, flexWrap: "wrap" } },
|
|
577
|
+
h(
|
|
578
|
+
"button",
|
|
579
|
+
{ style: S.btn, onClick: () => setShowLog((v) => !v) },
|
|
580
|
+
showLog ? "收起更新日志" : "更新日志",
|
|
581
|
+
),
|
|
582
|
+
h(
|
|
583
|
+
"button",
|
|
584
|
+
{ style: S.btn, onClick: () => void onRunLog(), disabled: busy !== "" },
|
|
585
|
+
busy === "logs" ? "读取中…" : showRunLog ? "收起运行日志" : "运行日志",
|
|
586
|
+
),
|
|
587
|
+
),
|
|
524
588
|
showLog ? h("div", { style: S.log }, about.changelog) : null,
|
|
589
|
+
showRunLog && runLog
|
|
590
|
+
? h(
|
|
591
|
+
"div",
|
|
592
|
+
null,
|
|
593
|
+
h(
|
|
594
|
+
"div",
|
|
595
|
+
{ style: { fontSize: 11, opacity: 0.7, marginBottom: 4 } },
|
|
596
|
+
`${runLog.file || "日志"} · 当前 ${sizeText(runLog.bytes)} · 归档 ${(runLog.archives || []).length} 份${runLog.fromArchive ? ` · 含归档 ${runLog.fromArchive}` : ""}`,
|
|
597
|
+
),
|
|
598
|
+
h("div", { style: S.log }, runLogText(runLog) || "(暂无日志)"),
|
|
599
|
+
)
|
|
600
|
+
: null,
|
|
525
601
|
)
|
|
526
602
|
: null,
|
|
527
603
|
),
|
package/lib/bus.js
CHANGED
|
@@ -21,8 +21,9 @@ import fsp from 'node:fs/promises'
|
|
|
21
21
|
import os from 'node:os'
|
|
22
22
|
import path from 'node:path'
|
|
23
23
|
import crypto from 'node:crypto'
|
|
24
|
+
import { rotateIfNeeded } from './logs.js'
|
|
24
25
|
|
|
25
|
-
export const VERSION = '0.8.
|
|
26
|
+
export const VERSION = '0.8.6'
|
|
26
27
|
|
|
27
28
|
/** DSH 的家目录:与宿主其它状态(sessions / profiles)同一个根,多终端天然共享 */
|
|
28
29
|
export function dshHome() {
|
|
@@ -72,6 +73,9 @@ const DEFAULTS = {
|
|
|
72
73
|
relayIntervalMs: 20000,
|
|
73
74
|
backupIntervalMs: 300000,
|
|
74
75
|
presenceIntervalMs: 60000,
|
|
76
|
+
// 运行日志(logs/<nodeId>.jsonl):超过 logMaxBytes 就压成 archive/*.jsonl.gz,只留最近 logKeep 份
|
|
77
|
+
logMaxBytes: 5 * 1024 * 1024,
|
|
78
|
+
logKeep: 20,
|
|
75
79
|
backupSessionsPerRun: 20,
|
|
76
80
|
backupAttachmentsPerRun: 20,
|
|
77
81
|
attachmentBackupEnabled: true,
|
|
@@ -680,6 +684,17 @@ export class Bus {
|
|
|
680
684
|
`${JSON.stringify({ at: new Date().toISOString(), line: String(line) })}\n`,
|
|
681
685
|
'utf8',
|
|
682
686
|
)
|
|
687
|
+
// 每写若干行看一次大小,超上限压成 archive/<node>-<时间>.jsonl.gz,只留最近若干份
|
|
688
|
+
this._logWrites = (this._logWrites || 0) + 1
|
|
689
|
+
if (this._logWrites % 200 === 1) {
|
|
690
|
+
const cfg = this.cfg || {}
|
|
691
|
+
await rotateIfNeeded({
|
|
692
|
+
dir: this.logsDir,
|
|
693
|
+
nodeId,
|
|
694
|
+
maxBytes: cfg.logMaxBytes,
|
|
695
|
+
keep: cfg.logKeep,
|
|
696
|
+
})
|
|
697
|
+
}
|
|
683
698
|
} catch {
|
|
684
699
|
/* 日志失败不影响主流程 */
|
|
685
700
|
}
|
package/lib/cloud/match.js
CHANGED
package/lib/index.js
CHANGED
|
@@ -29,6 +29,7 @@ import fs from 'node:fs'
|
|
|
29
29
|
import os from 'node:os'
|
|
30
30
|
import path from 'node:path'
|
|
31
31
|
import crypto from 'node:crypto'
|
|
32
|
+
import { fileURLToPath } from 'node:url'
|
|
32
33
|
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
33
34
|
import {
|
|
34
35
|
Bus, VERSION, formatBytes, makeUniqueCode, normalizeCode, readConfig, readJson, slugify, stamp, writeConfig,
|
|
@@ -40,8 +41,10 @@ import { createCloudHome } from './cloud/home.js'
|
|
|
40
41
|
import { runSetup } from './cloud/setup.js'
|
|
41
42
|
import { deviceRow, matchDevice } from './cloud/match.js'
|
|
42
43
|
import { registerWeb } from './web.js'
|
|
44
|
+
import { recordOwnership } from './logs.js'
|
|
43
45
|
|
|
44
46
|
const PLUGIN = 'dsh-crossterm'
|
|
47
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
45
48
|
|
|
46
49
|
function textRender(fn) {
|
|
47
50
|
return (_args, value) => [{ type: 'text', text: fn(value) }]
|
|
@@ -345,12 +348,13 @@ class CrossTerm {
|
|
|
345
348
|
|
|
346
349
|
// ── 云端设备(跨设备投递,0.7.3 起)─────────────────────────────────
|
|
347
350
|
|
|
348
|
-
/**
|
|
351
|
+
/** 云端设备列表:没连云端、没布局、或还读不到时统一回空数组(0.8.6 起不含本机) */
|
|
349
352
|
async cloudDevices() {
|
|
350
353
|
if (!this.home) return []
|
|
351
354
|
try {
|
|
352
355
|
const r = await this.home.devices()
|
|
353
|
-
|
|
356
|
+
const list = r && r.ok && Array.isArray(r.devices) ? r.devices : []
|
|
357
|
+
return list.filter((d) => d && d.nodeId && d.nodeId !== this.nodeId)
|
|
354
358
|
} catch (e) {
|
|
355
359
|
this.warn(`cloudDevices: ${(e && e.message) || String(e)}`)
|
|
356
360
|
return []
|
|
@@ -1396,6 +1400,8 @@ class CrossTerm {
|
|
|
1396
1400
|
if (this.started) return
|
|
1397
1401
|
this.started = true
|
|
1398
1402
|
await this.bus.ensure()
|
|
1403
|
+
// 负责人变更留痕:MAINTAINERS.md 里的当前负责人一变,就往 logs/ownership.log 记一行
|
|
1404
|
+
await recordOwnership({ dir: this.bus.logsDir, root: ROOT }).catch(() => {})
|
|
1399
1405
|
await this.loadLinks()
|
|
1400
1406
|
await this.note(`links loaded: ${this.links.size} 条`)
|
|
1401
1407
|
await this.loadNames()
|
package/lib/logs.js
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import fsp from 'node:fs/promises'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import zlib from 'node:zlib'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 运行日志的落盘与轮转。
|
|
8
|
+
*
|
|
9
|
+
* 位置:`<busDir>\logs\<nodeId>.jsonl`(一行一个 JSON,{at, line})
|
|
10
|
+
* 归档:`<busDir>\logs\archive\<nodeId>-<时间戳>.jsonl.gz`,只留最近 keep 份
|
|
11
|
+
* 目的:长期跑下来日志不会无限涨,出问题时又能翻到上一轮、上上一轮的记录。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export const DEFAULT_MAX_BYTES = 5 * 1024 * 1024
|
|
15
|
+
export const DEFAULT_KEEP = 20
|
|
16
|
+
|
|
17
|
+
export function logFile(dir, nodeId) {
|
|
18
|
+
return path.join(dir, `${nodeId}.jsonl`)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function archiveDir(dir) {
|
|
22
|
+
return path.join(dir, 'archive')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function pruneArchives(dir, keep) {
|
|
26
|
+
const removed = []
|
|
27
|
+
try {
|
|
28
|
+
const names = (await fsp.readdir(archiveDir(dir))).filter((n) => n.endsWith('.jsonl.gz')).sort()
|
|
29
|
+
const over = names.length - Math.max(1, Number(keep) || DEFAULT_KEEP)
|
|
30
|
+
for (let i = 0; i < over; i += 1) {
|
|
31
|
+
const name = names[i]
|
|
32
|
+
await fsp.rm(path.join(archiveDir(dir), name), { force: true })
|
|
33
|
+
removed.push(name)
|
|
34
|
+
}
|
|
35
|
+
} catch {
|
|
36
|
+
/* 归档目录不存在或读失败:不算错 */
|
|
37
|
+
}
|
|
38
|
+
return removed
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 超过上限就把当前日志压成归档并清空。
|
|
43
|
+
* @returns {Promise<null|{name:string,bytes:number,gz:number,removed:string[]}>}
|
|
44
|
+
*/
|
|
45
|
+
export async function rotateIfNeeded({ dir, nodeId, maxBytes = DEFAULT_MAX_BYTES, keep = DEFAULT_KEEP } = {}) {
|
|
46
|
+
if (!dir || !nodeId) return null
|
|
47
|
+
const file = logFile(dir, nodeId)
|
|
48
|
+
let st
|
|
49
|
+
try {
|
|
50
|
+
st = await fsp.stat(file)
|
|
51
|
+
} catch {
|
|
52
|
+
return null
|
|
53
|
+
}
|
|
54
|
+
if (st.size < (Number(maxBytes) || DEFAULT_MAX_BYTES)) return null
|
|
55
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-')
|
|
56
|
+
const name = `${nodeId}-${stamp}.jsonl.gz`
|
|
57
|
+
const body = await fsp.readFile(file)
|
|
58
|
+
const gz = zlib.gzipSync(body)
|
|
59
|
+
await fsp.mkdir(archiveDir(dir), { recursive: true })
|
|
60
|
+
await fsp.writeFile(path.join(archiveDir(dir), name), gz)
|
|
61
|
+
// 清空而不是删除:保留 inode/权限,别的进程正在写也不会断
|
|
62
|
+
await fsp.writeFile(file, '', 'utf8')
|
|
63
|
+
const removed = await pruneArchives(dir, keep)
|
|
64
|
+
return { name, bytes: st.size, gz: gz.length, removed }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** 从 MAINTAINERS.md 的机器可读注释里取负责人(当前 / 上一任 / 助手) */
|
|
68
|
+
export function readMaintainersDoc(root) {
|
|
69
|
+
try {
|
|
70
|
+
const text = fs.readFileSync(path.join(root, 'MAINTAINERS.md'), 'utf8')
|
|
71
|
+
const m = text.match(/<!--\s*maintainers\s*([\s\S]*?)-->/)
|
|
72
|
+
if (!m) return null
|
|
73
|
+
const doc = JSON.parse(m[1])
|
|
74
|
+
const list = Array.isArray(doc.previous) ? doc.previous : []
|
|
75
|
+
return {
|
|
76
|
+
current: doc.current || null,
|
|
77
|
+
previous: list.length ? list[list.length - 1] : null,
|
|
78
|
+
assistant: doc.assistant || null,
|
|
79
|
+
}
|
|
80
|
+
} catch {
|
|
81
|
+
return null
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 负责人变了就在 `logs/ownership.log` 追加一行(一行一个 JSON),
|
|
87
|
+
* 并在 `logs/ownership-seen.json` 记住已记录的那一位,避免每次启动重复写。
|
|
88
|
+
*/
|
|
89
|
+
export async function recordOwnership({ dir, root } = {}) {
|
|
90
|
+
if (!dir || !root) return null
|
|
91
|
+
const doc = readMaintainersDoc(root)
|
|
92
|
+
const cur = doc && doc.current
|
|
93
|
+
if (!cur || !cur.short) return null
|
|
94
|
+
const seenFile = path.join(dir, 'ownership-seen.json')
|
|
95
|
+
let seen = null
|
|
96
|
+
try {
|
|
97
|
+
seen = JSON.parse(await fsp.readFile(seenFile, 'utf8'))
|
|
98
|
+
} catch {
|
|
99
|
+
seen = null
|
|
100
|
+
}
|
|
101
|
+
if (seen && seen.current === cur.short && seen.session === cur.session) return null
|
|
102
|
+
const line = {
|
|
103
|
+
at: new Date().toISOString(),
|
|
104
|
+
role: cur.role || '负责人',
|
|
105
|
+
to: cur.short,
|
|
106
|
+
session: cur.session || '',
|
|
107
|
+
terminal: cur.terminal || '',
|
|
108
|
+
nodeId: cur.nodeId || '',
|
|
109
|
+
since: cur.since || '',
|
|
110
|
+
basis: cur.basis || '',
|
|
111
|
+
previous: doc.previous ? { short: doc.previous.short || '', to: doc.previous.to || '' } : null,
|
|
112
|
+
}
|
|
113
|
+
await fsp.mkdir(dir, { recursive: true })
|
|
114
|
+
await fsp.appendFile(path.join(dir, 'ownership.log'), `${JSON.stringify(line)}\n`, 'utf8')
|
|
115
|
+
await fsp.writeFile(seenFile, JSON.stringify({ current: cur.short, session: cur.session || '', at: line.at }), 'utf8')
|
|
116
|
+
return line
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** 读负责人变更记录(最近 limit 条,新的在前) */
|
|
120
|
+
export async function listOwnership({ dir, limit = 20 } = {}) {
|
|
121
|
+
const out = []
|
|
122
|
+
if (!dir) return out
|
|
123
|
+
try {
|
|
124
|
+
const text = await fsp.readFile(path.join(dir, 'ownership.log'), 'utf8')
|
|
125
|
+
for (const raw of text.split('\n').filter(Boolean)) {
|
|
126
|
+
try {
|
|
127
|
+
out.push(JSON.parse(raw))
|
|
128
|
+
} catch {
|
|
129
|
+
/* 坏行跳过 */
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
} catch {
|
|
133
|
+
return out
|
|
134
|
+
}
|
|
135
|
+
return out.slice(-Math.max(1, Number(limit) || 20)).reverse()
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export async function listLogs({ dir, nodeId } = {}) {
|
|
139
|
+
const out = { file: '', bytes: 0, archives: [] }
|
|
140
|
+
if (!dir || !nodeId) return out
|
|
141
|
+
out.file = logFile(dir, nodeId)
|
|
142
|
+
try {
|
|
143
|
+
const st = await fsp.stat(out.file)
|
|
144
|
+
out.bytes = st.size
|
|
145
|
+
out.at = st.mtime.toISOString()
|
|
146
|
+
} catch {
|
|
147
|
+
out.bytes = 0
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
const names = (await fsp.readdir(archiveDir(dir))).filter((n) => n.endsWith('.jsonl.gz')).sort().reverse()
|
|
151
|
+
for (const name of names) {
|
|
152
|
+
const full = path.join(archiveDir(dir), name)
|
|
153
|
+
const st = await fsp.stat(full).catch(() => null)
|
|
154
|
+
out.archives.push({ name, bytes: st ? st.size : 0, at: st ? st.mtime.toISOString() : '' })
|
|
155
|
+
}
|
|
156
|
+
} catch {
|
|
157
|
+
/* 没归档 */
|
|
158
|
+
}
|
|
159
|
+
return out
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** 读最近 lines 行(当前文件;不够时可带上前一个归档的尾巴) */
|
|
163
|
+
export async function tailLogs({ dir, nodeId, lines = 100, withArchive = false } = {}) {
|
|
164
|
+
const want = Math.max(1, Math.min(2000, Number(lines) || 100))
|
|
165
|
+
const out = { file: '', lines: [], truncated: false, fromArchive: '' }
|
|
166
|
+
if (!dir || !nodeId) return out
|
|
167
|
+
out.file = logFile(dir, nodeId)
|
|
168
|
+
let text = ''
|
|
169
|
+
try {
|
|
170
|
+
text = await fsp.readFile(out.file, 'utf8')
|
|
171
|
+
} catch {
|
|
172
|
+
text = ''
|
|
173
|
+
}
|
|
174
|
+
let rows = text.split('\n').filter(Boolean)
|
|
175
|
+
out.truncated = rows.length > want
|
|
176
|
+
if (rows.length < want && withArchive) {
|
|
177
|
+
try {
|
|
178
|
+
const names = (await fsp.readdir(archiveDir(dir))).filter((n) => n.endsWith('.jsonl.gz')).sort()
|
|
179
|
+
const last = names[names.length - 1]
|
|
180
|
+
if (last) {
|
|
181
|
+
const gz = await fsp.readFile(path.join(archiveDir(dir), last))
|
|
182
|
+
const prev = zlib.gunzipSync(gz).toString('utf8').split('\n').filter(Boolean)
|
|
183
|
+
out.fromArchive = last
|
|
184
|
+
rows = prev.slice(-(want - rows.length)).concat(rows)
|
|
185
|
+
}
|
|
186
|
+
} catch {
|
|
187
|
+
/* 归档读不到就只给当前的 */
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
out.lines = rows.slice(-want)
|
|
191
|
+
return out
|
|
192
|
+
}
|
package/lib/web.js
CHANGED
|
@@ -8,9 +8,10 @@ import { fileURLToPath } from 'node:url'
|
|
|
8
8
|
import { spawn } from 'node:child_process'
|
|
9
9
|
import { VERSION, readConfig, writeConfig } from './bus.js'
|
|
10
10
|
import { createPan } from './cloud/pan.js'
|
|
11
|
-
import { listCloudDevices
|
|
11
|
+
import { listCloudDevices } from './cloud/devices.js'
|
|
12
12
|
import { listPresences } from './cloud/presence.js'
|
|
13
13
|
import { runSetup } from './cloud/setup.js'
|
|
14
|
+
import { listLogs, tailLogs, readMaintainersDoc, listOwnership } from './logs.js'
|
|
14
15
|
|
|
15
16
|
const PKG = 'dsh-crossterm'
|
|
16
17
|
const NPM_REGISTRIES = ['https://registry.npmmirror.com', 'https://registry.npmjs.org']
|
|
@@ -157,6 +158,22 @@ function cloudProvider(cfg) {
|
|
|
157
158
|
return { id: 'none', name: '未连接', configured: false }
|
|
158
159
|
}
|
|
159
160
|
|
|
161
|
+
/**
|
|
162
|
+
* 云端设备列表的展示口径(0.8.6 起):
|
|
163
|
+
* - 不显示本机(自己已经在「当前终端」那一行);
|
|
164
|
+
* - 每台标上账号里的角色:`_account.json` 的 `ownerDevice` 是管理员,其余是成员;
|
|
165
|
+
* - 保底字段:`role` / `online` / `at`(离线时卡片用它算「x 分钟前」)。
|
|
166
|
+
*/
|
|
167
|
+
export function annotateDevices(list, { selfId = '', ownerDevice = '' } = {}) {
|
|
168
|
+
return (Array.isArray(list) ? list : [])
|
|
169
|
+
.filter((d) => d && d.nodeId && d.nodeId !== selfId)
|
|
170
|
+
.map((d) => ({
|
|
171
|
+
...d,
|
|
172
|
+
role: d.role || (d.nodeId === ownerDevice ? '管理员' : '成员'),
|
|
173
|
+
at: Number(d.at || 0) || (d.lastSeen ? Date.parse(d.lastSeen) || 0 : 0) || (d.updatedAt ? Number(d.updatedAt) || 0 : 0),
|
|
174
|
+
}))
|
|
175
|
+
}
|
|
176
|
+
|
|
160
177
|
/** 语义化版本比较:只比 主.次.补(忽略预发布尾巴),a>b 返回正数 */
|
|
161
178
|
function compareVersion(a, b) {
|
|
162
179
|
const num = (v) => String(v || '').split('-')[0].split('.').map((x) => parseInt(x, 10) || 0)
|
|
@@ -466,6 +483,11 @@ function readChangelog(maxSections = 3, maxChars = 6000) {
|
|
|
466
483
|
}
|
|
467
484
|
}
|
|
468
485
|
|
|
486
|
+
/** 从 MAINTAINERS.md 的机器可读注释里取当前/上一任负责人 */
|
|
487
|
+
function readMaintainers() {
|
|
488
|
+
return readMaintainersDoc(ROOT)
|
|
489
|
+
}
|
|
490
|
+
|
|
469
491
|
function aboutOf() {
|
|
470
492
|
if (aboutCache && aboutCache.version === VERSION) return aboutCache
|
|
471
493
|
let meta = {}
|
|
@@ -492,6 +514,7 @@ function aboutOf() {
|
|
|
492
514
|
shortTitle: meta.shortTitle || meta.title || PKG,
|
|
493
515
|
description: meta.description || '',
|
|
494
516
|
components,
|
|
517
|
+
maintainers: readMaintainers(),
|
|
495
518
|
changelog: readChangelog(),
|
|
496
519
|
}
|
|
497
520
|
return aboutCache
|
|
@@ -544,6 +567,45 @@ export function registerWeb(ctx, term, log) {
|
|
|
544
567
|
},
|
|
545
568
|
}),
|
|
546
569
|
)
|
|
570
|
+
disposers.push(
|
|
571
|
+
webCtx.webServer.register({
|
|
572
|
+
kind: 'exact',
|
|
573
|
+
path: '/crossterm/api/logs',
|
|
574
|
+
handler: async (req, res) => {
|
|
575
|
+
if (!checkHttp(req, res, term.cfg)) return
|
|
576
|
+
try {
|
|
577
|
+
const cfg = term.cfg || {}
|
|
578
|
+
const dir = path.join(cfg.busDir || path.join(os.homedir(), '.dsh', 'crossterm'), 'logs')
|
|
579
|
+
const nodeId = String(term.nodeId || 'node')
|
|
580
|
+
const url = new URL(String(req.url || '/'), 'http://127.0.0.1')
|
|
581
|
+
const lines = Number(url.searchParams.get('lines') || 120)
|
|
582
|
+
const info = await listLogs({ dir, nodeId })
|
|
583
|
+
const tail = await tailLogs({ dir, nodeId, lines, withArchive: true })
|
|
584
|
+
const ownership = await listOwnership({ dir, limit: 10 })
|
|
585
|
+
json(res, 200, {
|
|
586
|
+
ok: true,
|
|
587
|
+
nodeId,
|
|
588
|
+
file: tail.file,
|
|
589
|
+
bytes: info.bytes,
|
|
590
|
+
archives: info.archives.slice(0, 10),
|
|
591
|
+
truncated: tail.truncated,
|
|
592
|
+
fromArchive: tail.fromArchive,
|
|
593
|
+
ownership,
|
|
594
|
+
lines: tail.lines.map((raw) => {
|
|
595
|
+
try {
|
|
596
|
+
const row = JSON.parse(raw)
|
|
597
|
+
return { at: row.at || '', line: String(row.line || '') }
|
|
598
|
+
} catch {
|
|
599
|
+
return { at: '', line: String(raw) }
|
|
600
|
+
}
|
|
601
|
+
}),
|
|
602
|
+
})
|
|
603
|
+
} catch (error) {
|
|
604
|
+
json(res, 200, { ok: false, message: String((error && error.message) || error) })
|
|
605
|
+
}
|
|
606
|
+
},
|
|
607
|
+
}),
|
|
608
|
+
)
|
|
547
609
|
disposers.push(
|
|
548
610
|
webCtx.webServer.register({
|
|
549
611
|
kind: 'exact',
|
|
@@ -577,12 +639,16 @@ export function registerWeb(ctx, term, log) {
|
|
|
577
639
|
const byId = new Map()
|
|
578
640
|
for (const d of result.devices || []) byId.set(d.nodeId, d)
|
|
579
641
|
for (const d of fresh.devices || []) byId.set(d.nodeId, { ...byId.get(d.nodeId), ...d, source: 'layout' })
|
|
580
|
-
|
|
642
|
+
// 0.8.6:列表里不显示本机,只给「其他设备」;每台标上账号里的角色(管理员 = _account.json 的 ownerDevice)
|
|
643
|
+
const ownerDevice = (layout && layout.accountInfo && layout.accountInfo.ownerDevice) || (layout && layout.deviceId) || ''
|
|
644
|
+
const selfId = info.nodeId || ''
|
|
645
|
+
result.devices = annotateDevices([...byId.values()], { selfId, ownerDevice }).sort(
|
|
646
|
+
(a, b) => Number(b.lastSeen ? Date.parse(b.lastSeen) : b.at || 0) - Number(a.lastSeen ? Date.parse(a.lastSeen) : a.at || 0),
|
|
647
|
+
)
|
|
581
648
|
delete result.tmpFiles
|
|
582
649
|
result.files = (result.files || 0) + (fresh.files || 0)
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
json(res, 200, { ...result, configured: true, me: info.nodeId || '' })
|
|
650
|
+
result.ownerDevice = ownerDevice
|
|
651
|
+
json(res, 200, { ...result, configured: true, me: selfId })
|
|
586
652
|
} catch (error) {
|
|
587
653
|
json(res, 200, {
|
|
588
654
|
ok: false,
|
|
@@ -815,4 +881,4 @@ export function registerWeb(ctx, term, log) {
|
|
|
815
881
|
}
|
|
816
882
|
|
|
817
883
|
// 只给探针用:把内部小工具导出来,宿主运行时不依赖这些导出
|
|
818
|
-
export { decodeOutput, runtimeTools, looksMissingCommand, updateErrorText, aboutOf }
|
|
884
|
+
export { decodeOutput, runtimeTools, looksMissingCommand, updateErrorText, aboutOf, readMaintainers }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-crossterm",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.6",
|
|
4
4
|
"description": "跨端互通:同一 DSH 账号下的会话、终端、设备之间互通 —— 特征码链、内联邮件、云端中转站与自动备份",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
"parts",
|
|
27
27
|
"cordis.patch.yml",
|
|
28
28
|
"README.md",
|
|
29
|
-
"CHANGELOG.md"
|
|
29
|
+
"CHANGELOG.md",
|
|
30
|
+
"MAINTAINERS.md"
|
|
30
31
|
],
|
|
31
32
|
"license": "MIT",
|
|
32
33
|
"author": "crossterm",
|