dsh-crossterm 0.3.40 → 0.4.1

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 ADDED
@@ -0,0 +1,368 @@
1
+ # dsh-crossterm 变更记录
2
+
3
+ ## 0.4.1 — 2026-10-19
4
+
5
+ ### 发版纪律:简介与本版更新随包一起走
6
+ - `README.md` 顶部新增「零、简介与本版更新」一节:插件名称/简介 + 四个组件(跨会话/跨终端/跨设备/跨账号)
7
+ 的名称与简介表 + **本版更新**,每次发版重写这一节。
8
+ - `package.json` 的 `files` 补 `CHANGELOG.md`,npm 包里从此带完整变更记录。
9
+ - 每个版本额外产出 `更新说明-<版本>.md`(插件与组件简介 + 本版更新),与 zip、README 一起上传云盘
10
+ `setup/`,装到 B 机也能看到这一版改了什么。
11
+ - 功能与 0.4.0 完全一致,无行为变化。
12
+
13
+ ## 0.4.0 — 2026-10-19
14
+
15
+ ### 版本号重标(内容同 0.3.40,只换号)
16
+ - 0.3.39 把云端从「一个扁平总线目录」改成「账号 / 设备 / 共享」三层多机结构,
17
+ 还加了云端中转站与会话增量备份 —— 这是**结构性大更新**,按语义化版本本应是 0.4.0。
18
+ 当时沿用了「每次 +1 末位」的习惯,标成 0.3.39/0.3.40,从号上看不出分量。
19
+ - 本版把同一份代码重标为 **0.4.0**,作为「云端多机结构」的里程碑;功能与 0.3.40 完全一致。
20
+ - 此后按三分位升号:修 bug → 第三位;加功能 / 改结构 → 第二位并归零;破坏兼容 → 第一位。
21
+
22
+ ## 0.3.40 — 2026-10-19
23
+
24
+ ### 修复:附件备份一个都没上传
25
+ - DSH 的附件仓库是两层结构 `attachments/v1/objects/<前两位>/<sha256>`,
26
+ 0.3.39 的 `backupAttachments()` 只读顶层文件 → `total = 0`,云端 `attachments/` 一直是空的。
27
+ - 现在遍历两层目录(顶层散文件也认),文件名仍是 sha256,云端路径不变,去重逻辑不变。
28
+
29
+ ## 0.3.39 — 2026-10-19
30
+
31
+ ### 新增:云端按「账号 / 设备 / 共享」分目录(`lib/cloud/layout.js`)
32
+ - 云盘里建一个大文件夹(默认 `dsh`),下面自动长出:
33
+ `dsh/<账号>/` → `_account.json`(谁是这个账号的第一个设备)、
34
+ `dsh/<账号>/<设备>/` → `_device.json` + `sessions/` + `attachments/`、
35
+ `dsh/<账号>/shared/<设备>/` → `_inbox.json` + `inbox/` + `done/`(+ 发件用 `files/`)。
36
+ - 账号名解析顺序:配置 `cloudAccount` → 环境变量 `DSH_ACCOUNT` → 云盘账号名 → `default`。
37
+ - 角色:**该账号第一个进云的设备自动是管理员(admin)**,之后进来的都是成员(member),
38
+ 记在 `_account.json.ownerDevice` 与 `_device.json.role`。
39
+ - 目录结果缓存在 `<总线目录>/cloud/layout.json`(10 分钟),断网时用缓存继续工作。
40
+ - `pan.js` 补 `mkdir` / `exists` / `putBuffer` / `getBuffer`;目录已存在(errno 31061 / -8)按成功处理。
41
+
42
+ ### 新增:云端中转站(`lib/cloud/relay.js`)
43
+ - 每台设备在 `shared/<设备>/inbox/` 有自己的收件箱;发件写成
44
+ `<目标设备>/inbox/<mailId>.json` + 附件放 `<目标设备>/files/<mailId>/`。
45
+ - 本机按 `relayIntervalMs`(默认 20 秒)查自己的收件箱:取信 → 存本地
46
+ `<总线目录>/cloud/relay/incoming/` → 读收件人(会话名)→ 投递给对应会话 →
47
+ 原信归档到 `done/` 后从 `inbox/` 删掉;失败留件并记 `failed[tries]`。
48
+
49
+ ### 新增:会话数据增量备份(`lib/cloud/session-backup.js`)
50
+ - transcript(`sessions/<工区>/<会话>/session.v4.jsonl.zstd`)是多帧 zstd,**按字节备份、不解压**:
51
+ 首次传 `full-<时间戳>.bin`,之后只传 `[上次字节数, 现在大小)` 这一段,命名 `part-0001.bin`…
52
+ - 每个会话目录里写 `_session.json` 当**水位标注**:`bytes` = 已备份到第几字节、
53
+ `parts`、`chunks[{name, from, to, sha256}]`、会话显示名与工作目录。
54
+ 下次备份从这个标注往后继续(文件变短则整份重传)。
55
+ - 附件按 sha256 内容寻址上传到 `<设备>/attachments/<sha256>`,天然去重;
56
+ 单轮有上限(`backupSessionsPerRun` / `backupAttachmentsPerRun`,默认各 20),余量下一轮继续。
57
+ - 插件页卡片新增三行:**云端账号**(角色 + 设备号)、**云端中转站**(已收/失败/间隔)、
58
+ **会话备份**(会话数 · 体积 · 上次时间),按钮 **收信** / **备份会话**。
59
+ - 新接口:`GET /crossterm/api/home`(`?action=layout|relay|backup`)、`POST /crossterm/api/backup`;
60
+ `GET /crossterm/api/status` 里新增 `home` 段。
61
+ - `term_cloud` 新增动作:`home`、`layout`、`relay`、`backup`。
62
+ - 探针 `F:\dsh\001X\crossterm-work\probe-cloud-home.mjs`:内存版云盘打桩,40 项全 PASS。
63
+
64
+ ## 0.3.38 — 2026-10-18
65
+
66
+ ### 新增:插件页卡片能看「云端设备」
67
+ - `GET /crossterm/api/devices`:读云端总线的 `presence/*.json`,聚合出每台机器的
68
+ 名字、nodeId、版本、平台、会话数、入链、最后心跳(3 分钟内有心跳算「在线」)。
69
+ 没配云盘时回 `ok:false` 并提示,不会假装有设备。
70
+ - 卡片新增「云端设备」一行:展开自动读一次,另有 **刷新设备** 按钮;
71
+ 本机永远在列表里(取自本地 presence,标「(本机)」)。
72
+
73
+ ### 修复:云端 presence 里的 `.tmp` 残骸把真实设备挤到第二页
74
+ - 根因有三层:
75
+ 1. `lib/bus.js` 的原子写(写 `*.tmp` 再 rename)在 Windows 上偶发 `EPERM/EBUSY`,
76
+ rename 失败后 `.tmp` 留在目录里 —— 本地 `presence` 220 个、`chains` 1659 个;
77
+ 2. `lib/cloud/sync.js` 不看后缀,把这些 `.tmp` 全同步上云(云端 presence 221 个);
78
+ 3. `lib/cloud/pan.js` 的 `ls()` 写死 `start=0&limit=200`,按时间倒序时前 200 条全是
79
+ `.tmp`,真实 node json 排在后面 → `get()` 找不到文件,设备列表看起来是空的。
80
+ - 修法:`renameWithRetry()`(5 次退避重试,最终退化为直接覆盖写,任何路径都不留 tmp);
81
+ `sync.js` 的 `walkLocal`/`walkCloud` 都跳过 `.tmp`;`pan.js` 新增 `lsAll()` 翻页
82
+ (上限 50 页),`get()` 与 `walkCloud` 都改用它。另导出 `sweepTmp()` 供后续清本地残骸。
83
+
84
+ ### 探针
85
+ - `F:\dsh\001X\crossterm-work\probe-devices.mjs`:真实凭证列云端 presence,
86
+ 修复前 0 台(全被 `.tmp` 挤掉)→ 修复后 2 台(`YOCE_C`、`TS-01`,均 0.3.37、在线)。
87
+
88
+ ## 0.3.37 — 2026-10-18
89
+
90
+ ### 增强:没有 `dsh` 命令也能点「更新」
91
+ - 桌面版安装通常没把 `dsh` 放进 PATH,0.3.36 之前点「更新」直接失败。现在按顺序尝试:
92
+ 1. `dsh plugin --profile <p> add dsh-crossterm@latest`;
93
+ 2. 识别到「不是内部或外部命令 / command not found」时,退回 **DSH 自带运行时**的
94
+ `dsh-runtimes\dsh-primary-runtime\dependencies\node\bin\node.exe` +
95
+ `dependencies\pnpm\bin\pnpm.cjs`,在 profile 目录跑 `pnpm add dsh-crossterm@latest`(`shell:false`,路径带空格也不怕)。
96
+ 3. 两条都不通才提示手动执行命令。
97
+ - 成功文案会注明走的哪条路(`已更新(用 DSH 自带 pnpm),重启 DSH 后生效`)。
98
+ - 探针 `F:\dsh\001X\crossterm-work\probe-web-decode.mjs`:GBK/UTF-8 解码、ANSI 清理、空输出、
99
+ 缺命令识别、运行时路径探测、失败文案(10 项全 PASS)。
100
+
101
+ ## 0.3.36 — 2026-10-18
102
+
103
+ ### 修复:点「更新」后卡片上是一串乱码(����…)
104
+ - 根因:`lib/web.js` 的 `run()` 里 `out += String(chunk)` —— Buffer 默认按 UTF-8 解码,
105
+ 而中文 Windows 控制台(`dsh` 命令)输出的是 **CP936 / GBK**,中文全部变成替换字符,
106
+ 于是「'dsh' 不是内部或外部命令…」在卡片上成了一条乱码。
107
+ - 改为先收集原始 Buffer(只留最后 64 KB),整段先按 UTF-8 **严格**解码,失败再退回 `TextDecoder('gbk')`,
108
+ 并清掉 ANSI 控制符与 `\r` 进度残留。
109
+ - 命令本身跑不起来时给可读提示:路径里没有 `dsh` 命令 → 明说「请在命令行手动执行
110
+ `dsh plugin add dsh-crossterm@latest`,然后重启 DSH」,不再把整段输出直接甩到卡片上。
111
+
112
+ ## 0.3.35 — 2026-10-18
113
+
114
+ ### 新增:一键配置云盘(挂记事本)
115
+ - `term_cloud` 增加 `action: "setup"`:自动生成 `baidu-app.json` 模板并用**记事本**打开,
116
+ 用户把 AppKey / AppSecret 填进引号、保存并关闭记事本后,插件自动继续 ——
117
+ 校验字段 → 起本地回调(默认 `127.0.0.1:19388/oauth/callback`)→ 打开浏览器授权页 →
118
+ 用 code 换 token → 写 `baidu-auth.json` → 打开 `cloudEnabled` 并启动同步。
119
+ 密钥全程只在记事本里输入,不经过聊天、不进安装包、不上传任何地方。
120
+ - 支持 `code` 参数:浏览器没能自动回调时,把授权 URL 里的 `code` 贴回来即可完成(失败时输出里带手动授权链接)。
121
+ - 插件页卡片在「未连接」状态把灰开关换成 **配置云盘** 按钮:点一下即走上面整套流程(宿主半端新路由
122
+ `POST /crossterm/api/cloud/setup`),卡片每 3 秒轮询状态,授权完成后自动显示服务商并打开同步。
123
+ - 新增 `lib/cloud/setup.js`(`runSetup(cfg, opts)`):已授权且 appKey 未变时直接复用(`step: "already"`),
124
+ 不会反复弹记事本;记事本一直开着返回 `step: "notepad-open"` 提示重跑;端口被占用 / 非本机回调 / 占位符
125
+ 都有明确报错文案。
126
+ - 探针 `F:\dsh\001X\crossterm-work\probe-setup.mjs` 覆盖:模板生成、占位符拦截、非本机回调拦截、
127
+ 已授权复用、端口占用、手动授权链接回传(全部 PASS)。
128
+
129
+ ## 0.3.34 — 2026-10-18
130
+
131
+ ### 修复:没配云盘的新机器也显示「云端」
132
+ - 根因:`readConfig()` 会把 `baiduAppFile` / `baiduAuthFile` 补成默认路径(非空字符串),
133
+ `cloudProvider()` 只看「路径非空」就返回 `百度网盘`,`status` 的 `cloud.root` 又直接吐默认
134
+ `/dsh云盘/crossterm/bus` —— 于是刚装插件、一次都没授权过的机器,卡片也写着
135
+ 「百度网盘 · /dsh云盘/crossterm/bus(同步中)」。
136
+ - `cloudProvider()` 改为按**文件是否真的存在**判定(两个凭证都在才返回 `configured: true`),
137
+ 否则返回 `{ id: 'none', name: '未连接' }`。
138
+ - `status.cloud` 新增 `configured` 字段:未配置时 `enabled` / `running` 一律为 false、`root` 返回空串。
139
+ - 卡片在未配置时显示「未连接(还没配置云盘)」,开关保持灰底,点它只弹配置提示。
140
+ - `POST /crossterm/api/cloud/start` 在未配置时直接返回 `ok:false` 和配置指引,不再假装已开启。
141
+
142
+ ## 0.3.33 — 2026-10-18
143
+
144
+ ### 新增:云端位置的一行开关
145
+ - 插件页折叠卡里「云端位置」右侧加了同步开关:蓝底滑块靠右=同步中,灰底靠左=已关闭,点一下即切。
146
+ - 宿主半端新增 `POST /crossterm/api/cloud/stop` 与 `POST /crossterm/api/cloud/start`,
147
+ 分别调用 `disableCloud()` / `enableCloud()`;与 `term_cloud stop|start` 同一口径,**本进程内生效**,
148
+ 重启后按 `config.json` 的 `cloudEnabled` 恢复。
149
+
150
+ ### 改进:云端状态带上服务商、文案去品牌
151
+ - status 的 cloud 增加 `provider: {id, name}`(按凭证文件判定,当前 `baidu → 百度网盘`),
152
+ 卡片显示为「百度网盘 · /dsh云盘/crossterm/bus(同步中)」。
153
+ - 组件描述与 `term_cloud` 工具描述里的「百度网盘」改为「云端总线 / 云端目录」,
154
+ 只有运行时状态与代码注释仍写具体服务商。
155
+
156
+ ### 修复:版本比较方向
157
+ - 「检查更新」此前用 `!==` 判断,本地源码版领先已发布版时会误报「发现新版本 0.3.31」。
158
+ 改成 `compareVersion(latest, VERSION) > 0` 才算有更新,并新增 `ahead` 标记(本地领先)。
159
+
160
+ ## 0.3.32 — 2026-10-18
161
+
162
+ ### 新增:插件页里的配置卡片
163
+ - 浏览器半端 `client/client.js`:在 `plugins.bundle.config` 插槽挂卡片,
164
+ 折叠显「dsh-crossterm vX / 终端 · 特征码」,展开显 当前版本 / 当前终端 / 云端位置 与
165
+ 「检查更新」「更新」两个动作。
166
+ - 宿主半端 `lib/web.js`:注册 `/crossterm/api/status`、`/crossterm/api/check`、`/crossterm/api/update`,
167
+ `install` 段按 argv 里的 `--profile <名>` 识别安装 profile(`PID` 里的 profileInfo 会按字母序误选 `crossterm-test`)。
168
+ - `package.json` 增加 `exports` 的 `./client`、`./locale/*` 与 `dsh.client { platform: "web" }`;
169
+ `files` 白名单补 `client`、`locale`。
170
+ - 浏览器半端只由**裸包名**那一行承载:`cordis.patch.yml` 首行 name 改为 `dsh-crossterm`(part 仍是 session),
171
+ 配 `locale/zh.json` 的 `meta.title=跨会话`,组件数仍是 4、名字不变。
172
+
173
+ ## 0.3.31 — 2026-10-18
174
+
175
+ ### 安全:安装包不再携带任何网盘凭证
176
+ - 起因:B 机安装 zip 里曾打包开发者的 `cloud-auth/baidu-app.json`(AppKey/SecretKey)与
177
+ `baidu-auth.json`(access/refresh token),拿到 zip 就等于能读写该账号的网盘。
178
+ - 现在:安装包只含插件代码;`install-crossterm.ps1` 仅当目标机数据目录里**已存在使用者自己的**
179
+ `baidu-app.json` + `baidu-auth.json` 时才把 `cloudEnabled` 写成 true,否则默认关闭云同步
180
+ (跨会话、跨终端不受影响)并打印配置指引。
181
+ - npm 包本来就不含凭证:`files` 白名单只放行 `lib`、`parts`、`cordis.patch.yml`、`README.md`。
182
+ - 本版插件代码与 0.3.30 一致,仅版本号与文档更新;真正的改动在安装包与安装文档。
183
+
184
+ ## 0.3.30 — 2026-10-18
185
+
186
+ ### 新增:四个组件各自显示自己的名字
187
+ - 0.3.29 拆出四个组件后,插件详情页四行全叫 `dsh-crossterm`(DSH 的行标题取的是**包级元数据**)。
188
+ - 解法:`package.json` 增加 `exports` 子路径映射——`./parts/<part>` → `./lib/index.js`、
189
+ `./parts/<part>/package.json` → `./parts/<part>.manifest.json`;`cordis.patch.yml` 里每行的
190
+ `name` 写成 `dsh-crossterm/parts/<part>`,于是每行读到自己那份清单里的名字/描述。
191
+ - 实测:插件页显示「跨会话」「跨终端」「跨设备」「跨账号(预留)」四个带名字的组件。
192
+
193
+ ## 0.3.29 — 2026-10-18
194
+
195
+ ### 新增:按功能拆成四个组件,各自带开关
196
+ - `cordis.patch.yml` 由 1 行变 4 行:`crossterm-session`(跨会话)/ `crossterm-terminal`(跨终端)/
197
+ `crossterm-device`(跨设备)/ `crossterm-account`(跨账号,预留),每行带 `config.part`。
198
+ - `index.js` 增加 `TOOL_PART` 映射与 `normalizePart()`,`buildTools(term, agent, part)` 只挂该组件的工具;
199
+ 四个组件通过 `acquireTerm()/releaseTerm()` 共享**同一个** CrossTerm 实例(引用计数,最后一个卸载才 `stop()`),
200
+ 所以心跳、链、云同步都只有一份。
201
+ - 关掉「跨设备」组件 → 云同步不启动;其余组件各自独立开关,切换即时生效(DSH 组件开关)。
202
+
203
+ ## 0.3.28 — 2026-10-18
204
+
205
+ ### 新增:云总线同步(百度网盘),跨设备互通
206
+ - 新增 `lib/cloud/pan.mjs`(百度网盘开放 API 封装:uinfo/ls/put/get/rm,token 自动刷新)
207
+ 与 `lib/cloud/sync.mjs`(本机总线 ⇄ 网盘目录双向镜像 + chains 成员并集合并)。
208
+ - 配置项(`~/.dsh/crossterm/config.json`):`cloudEnabled` / `cloudRoot`(默认
209
+ `/dsh云盘/crossterm/bus`)/ `cloudIntervalMs`(默认 4000,钳制 1000–60000)/
210
+ `baiduAppFile` / `baiduAuthFile` / `cloudStateFile`。默认**关闭**,`cloudEnabled=false`
211
+ 时插件行为与 0.3.27 完全一致。
212
+ - 新工具 `term_cloud`:`status` 看运行状态 / `start` 开启 / `stop` 关闭 / `once` 立刻同步一轮。
213
+ - 同步语义:本地新文件 → 上传;云端新文件 → 落到本地并走原有收件箱投递;
214
+ `chains/*.json` 不覆盖而是**成员并集**(否则两机会互相覆盖成「只剩自己」);
215
+ `inbox/**/pending/*` 在本地已 `delivered/`、`dropped/` 后自动从云端删除,避免重复投递。
216
+
217
+ ### 修:状态文件不区分云根、首轮不全量
218
+ - 现象:云根从 `/apps/...` 换成 `/dsh云盘/...` 后,`cloud-sync-state.json` 里按相对路径
219
+ 记的 `uploaded` 仍命中,旧文件被当成「已传过」永久跳过 —— 实测云端只剩 4 个文件
220
+ (147 封邮件里只有 1 封),本地 245 个却全部 `skipped`。
221
+ - 修法:`loadState()` 里 `s.root !== cloudRoot` 就整体作废;状态增加 `bootstrapped` 标记,
222
+ 首轮(或换根后首轮)跳过 `RECENT_MS` 的「旧文件不上传」剪枝,做一次全量补齐。
223
+ - 验证:全量后 `uploaded=244`、云端递归文件数 206(`logs/`、`delivered/` 按设计不入云);
224
+ 全量首轮约 5.5 分钟,之后每轮只传增量。
225
+
226
+ ## 0.3.27 — 2026-10-18
227
+
228
+ ### 修复:`term_link` 回执里成员还是短码,跟 `term_list` 显示不一致
229
+ - 现象:0.3.21 让 `term_list` / 邮件头 / 各工具回执都改用会话名后,**只有 `term_link` 的成员回执漏了**。
230
+ 实测(2026-10-18T04:56 把 `6884dbad` 接进 `YZ566D`)回执是
231
+ `- 「TS-01」b6aa099b(同终端)`,而同一时刻 `term_list` 显示的是 `会话 互通插件负责人切换`。
232
+ - 根因:`lib/index.js` 里存在两个成员视图构造点——
233
+ `term_list` 用的那处(约 `:1265`)早就写了 `label` 字段,而 `term_link` 走的是 `chainView()`
234
+ (`:891`),它只给了 `short: shortId(...)`,没有 `label`;回执渲染(`:1379`)读的正是 `m.short`。
235
+ - 修法:
236
+ 1. `chainView()` 增加 `label`:本终端会话用 `this.labelOf(sessionId)`,远端会话用它 presence
237
+ 带过来的 `sessions[].name`,都没有才退回短码(保证永不出现空名)。
238
+ 2. `term_link` 回执渲染改成 `${m.label || m.short}`,与 `term_list` 的写法对齐。
239
+ - 影响面:只动显示,不改链数据、不改成员判定。
240
+ - 验证:三个 lib 文件 `node --check` 通过;`VERSION = 0.3.27`。
241
+ - 需重启宿主生效。
242
+
243
+ ## 0.3.26 — 2026-10-18
244
+
245
+ ### 优化:特征码更好念、也不再像验证码
246
+ - 现象:旧规则从 `23456789ABCDEFGHJKMNPQRSTUVWXYZ` 逐位随机,会生成 `748156` 这种**纯数字码**
247
+ (2026-10-18 新会话实验里就出现过,用户直接把它当短信验证码/编号看,还容易跟工单号、端口混淆)。
248
+ - 改法(`lib/bus.js`):
249
+ 1. `makeCode()` 首位**强制字母**(念起来像一个词),其余位仍混字母/数字,并保证整串至少含 1 位数字(方便念成 `K7-F2-QD`);
250
+ 2. 字母表继续排除易混的 `0/O/1/I/L`;
251
+ 3. 新增 `makeUniqueCode(bus)`:生成后先 `bus.readChain(code)` 查盘,撞上已有链就重抽(最多 32 次,仍撞则加长到 8 位)。
252
+ `lib/index.js:921` 的 `term_link` 未显式给码时改用它——此前理论上可能悄悄 join 到别人的链上。
253
+ - 兼容:`normalizeCode()` 未变,旧码(含 `748156` 这类纯数字)照常可读写。
254
+ - 验证:`crossterm-work\code-test.mjs`,20000 个样本 → 无 `0/O/1/I/L`、首位全字母、全部含数字、无纯数字、长度 6;
255
+ 撞码重抽(第 4 次返回码)、撞满加长到 8 位、旧码规范化,全部 PASS;两文件 `node --check` 通过。
256
+ - 需重启宿主生效。
257
+
258
+ ## 0.3.25 — 2026-10-18
259
+
260
+ ### 修复:重启后「没点开的会话」会被误判为死会话,链上成员被退掉
261
+ - 现象:宿主重启后 120 秒宽限一过,凡是 tab 还没点开的会话就被 `warm 已过 + known=false ×5` 退链。
262
+ 2026-10-18T04:11:12.690Z 实测:`WARN links: 退掉 4RPP8Y ccf34a5a(known=false ×5,warm 已过)`、
263
+ `WARN links: 退掉 4RPP8Y 6884dbad(…)`,同一毫秒两条——而这两个会话 7 分钟后正常挂载、会话完好。
264
+ 03:53:06Z 那次「links 全被清空」也是同一机制(重启后 tab 都没打开)。
265
+ - 根因:`sessionPresence()` 只问 `ctx.agents.get()` / `ctx.sessions.get()`,两者都只认**已挂载**的会话;
266
+ 重启后未打开的会话对它们等于不存在,于是被判死。
267
+ - 修法(`lib/index.js`):`sessionPresence()` 在两者都说不认识后,追加一道 `sessionOnDisk(sessionId)`
268
+ ——列举 `~/.dsh/sessions/<项目>/<sessionId>`,目录还在就认定 `known: true`。只有会话目录真被删了才会退链。
269
+ 只读、每个未挂载链接每拍一次目录列举(当前 3 条链接,开销可忽略)。
270
+ - 验证:`node --check` 通过;磁盘探测自测 `ccf34a5a` / `6884dbad` 均 FOUND,伪造 id 为 MISSING。
271
+ - 需重启宿主生效。
272
+
273
+ ## 0.3.24 — 2026-10-18
274
+
275
+ ### 取证:退链路径与 links 载入不再静默
276
+ - 背景:2026-10-18T03:53:06Z 重启后出现过一次 `links\ts-01-a52535.json` 被清成 `[]`、`chains\4RPP8Y.json` 被删,
277
+ 而插件日志里**一行都没有**(退链分支不打日志),导致无法复盘到底是「4 条各自 miss≥5」还是一次异常批量退链。
278
+ - 改动(`lib/index.js`):
279
+ 1. 退链分支补 `this.warn(\`links: 退掉 <code> <short>(known=false ×<miss>,warm 已过)\`)`;
280
+ 2. `start()` 在 `loadLinks()` 之后补 `note(\`links loaded: <n> 条\`)`,开盘即知载入几条。
281
+ - 版本:`bus.js` 的 `VERSION` 与 `package.json` 一起升到 0.3.24(**需重启宿主才生效**;本机 0.3.23 仍在跑)。
282
+ - 未解:那次「4 条一起消失」仍未确诊。已知 a3e9f80e 确实该被退(重启后没加载,`known=false` 属设计),
283
+ 但另外 3 条活会话同时消失解释不通;0.3.23 的修复只解决「退掉后又被回填」,不解决「为什么退」。
284
+
285
+ ## 0.3.23 — 2026-10-18
286
+
287
+ ### 修复:退链后磁盘 links 里的记录会被「幽灵回填」回来
288
+ - 现象:`links\<nodeId>.json` 里的条目在会话没加载时被 `refreshChains()` 从内存删掉、链上也确实退了,
289
+ 但磁盘上那条**永远删不掉**,每次写盘又冒出来;曾误判成「`loadLinks` 没把 links 补进链」。
290
+ - 根因:`refreshChains()` 的退链分支只 `this.links.delete(key)`,没有记删除意图;
291
+ 而 `persistLinks()` 是「磁盘条目 + 内存条目」的合并写,磁盘那条被原样读回 → 复活。
292
+ 全文件原本只有 `term_unlink` 路径会 `linksDropped.add(key)`。
293
+ - 修法:退链分支补 `this.linksDropped.add(key)`(`lib/index.js` 约 `:797`),
294
+ 并在本拍 `if (dropped) continue` 跳过重 join(`:809`);`persistLinks()` 的 `linksDropped.clear()` 未动,
295
+ 仍是一次性语义——只删一次,不影响之后的重新 link。
296
+ - 验证:`crossterm-work/ghost-test.mjs` 用**真实 CrossTerm 原型**(复制 lib 加一行 export)+ 临时 Bus root,
297
+ 造 3 条幽灵跑两拍:第 1 拍退链并同步删盘、第 2 拍不复活、重新 link 后仍能写回,3 项断言全 PASS。
298
+ - 说明:本机当前 4 条 links 都是活会话,所以这个修复**不会**主动清掉现在磁盘上的任何记录,只防止以后再攒。
299
+
300
+ ## 0.3.22 — 2026-10-18
301
+
302
+ ### 新增:邮件正文、附件、收件箱队列可以放到系统盘之外
303
+ - 用户要求「邮件不要存 C 盘」。原来 `mail/`(正文)、`files/`(附件暂存)、`inbox/`(队列 + delivered/dropped 历史)
304
+ 都硬编码在总线目录下,而总线目录默认是 `C:\Users\Administrator\.dsh\crossterm`。
305
+ - `config.json` 新增三个键,绝对路径生效,留空则沿用旧默认(跟着总线目录走):
306
+ `mailDir`、`filesDir`、`inboxRoot`。本机配置:
307
+ ```json
308
+ { "mailDir": "F:\\dsh\\001X\\crossterm-data\\mail",
309
+ "filesDir": "F:\\dsh\\001X\\crossterm-data\\files",
310
+ "inboxRoot": "F:\\dsh\\001X\\crossterm-data\\inbox" }
311
+ ```
312
+ - 本机已迁移:147 封邮件(201,308 B,逐字节核对一致)、205 个队列文件(354,467 B,一致)。
313
+ C 盘侧 `mail\`、`inbox\` 已删除。**链/链接/心跳/日志/names.json 仍在总线目录**(合计约 50 KB,跨终端共享)。
314
+ - 注意:`config.json` 同机所有终端共享,改一处全场生效;换机器要各自配。
315
+ `inbox` 里可能还留着发给**离线终端**的 pending 项,迁移时原样带过去,不影响投递。
316
+
317
+ ## 0.3.21 — 2026-10-18
318
+
319
+ ### 新增:对外只显示会话名,不显示会话码
320
+ - 名字来源是 DSH 自己写在会话转录里的 `session/title` 事件(就是 GUI 里显示的标题):
321
+ `{"type":"session/title","seq":18,"data":{"title":"互通插件负责人切换","source":{"kind":"provider"}}}`
322
+ 同一会话可能有多条(fallback 截首句 → LLM 命名 → 手动重命名),取 `seq` 最大的一条。
323
+ - **不要**在进程里读 projection(`ctx.sessionProjections.stateOf(session,'title')`):本机实测 9 个会话全返回空,
324
+ 而转录里标题明明存在。也不要在转录头部 `{"type":"session",…}` 里找 title,那里根本没有这个字段。
325
+ - 覆盖范围:`term_list` / `term_whoami` / 邮件头 / `term_send`、`term_broadcast`、`term_link`、`term_unlink`、
326
+ `term_inbox` 回执与错误文案。原始 sessionId 仍留在 sidecar json 与 links 文件里,供定位投递用。
327
+ - 名字缓存在总线根目录 `names.json`(`{ "短码": "名字" }`),启动后与每分钟清理节拍里同步一次:
328
+ 已有名字的会话 5 分钟复检(GUI 改名能跟上),还没名字的 1 分钟试一次。
329
+
330
+ ### 修复:loadLinks 读盘失败被静默吞掉
331
+ - 现象:0.3.20 在 2026-10-18 00:50 那次重启后,磁盘 links 有 4 条、内存只剩 1 条,
332
+ 链文件里另外 3 个活会话看起来像被剪掉;`refreshChains` 的补 join 其实一直是好的,只是内存里没数据可补。
333
+ - 处理:`loadLinks()` 改为三次重试(150ms 起翻倍),每次失败打 `loadLinks 第 N 次读取 links 失败:<message>`,
334
+ 三次都失败打 `loadLinks 放弃:links 读不出来,本轮不恢复任何链接关系(链文件里的记录不会被删)`,然后标记 `linksLoaded=false`。
335
+ - 0.3.21 重启实测:日志零失败行、内存 4 条 / 链文件 4 人,干净读入(重试没被触发)。
336
+
337
+ ### 修复:term_list 里特征码重复
338
+ - 同一终端多个会话接同一串码时,`特征码 4RPP8Y/4RPP8Y/4RPP8Y/4RPP8Y` → 去重后 `特征码 4RPP8Y`。
339
+
340
+ ### 已知未修
341
+ - `presence.sessions[].active` 用的是「最后一次 mount tools 的会话」(`index.js:265` `activeId || this.activeSessionId`),
342
+ 不是「正在跑 turn 的会话」,所以正在干活的会话可能显示 `active:false`。只影响 presence 展示和
343
+ `term_send` 省略 sessionId 时的默认投递目标,不影响显式投递。下一版改。
344
+
345
+ ## 0.3.20 及更早
346
+ - 见 `README.md` 各节。
347
+
348
+ ## 0.3.29 — 拆成四个组件(跨会话 / 跨终端 / 跨设备 / 跨账号)
349
+
350
+ - `cordis.patch.yml`:从单行 `insert` 改成 4 行(`crossterm-session` / `crossterm-terminal` / `crossterm-device` / `crossterm-account`),每行带 `config.part`。DSH 插件详情页「包含的组件」会列出这 4 行,每行可单独开关。
351
+ - `lib/index.js`:新增 `TOOL_PART` 归属表与 `normalizePart()`;`buildTools(term, agent, part)` 按组件过滤工具:
352
+ - 跨会话 `session`:term_link / term_unlink / term_broadcast / term_inbox / term_config
353
+ - 跨终端 `terminal`:term_whoami / term_list / term_send / term_send_files
354
+ - 跨设备 `device`:term_cloud(云同步只在 device 组件启用且 cloudEnabled=true 时启动)
355
+ - 跨账号 `account`:本次只是预留行,不挂工具
356
+ - 四个组件共用进程内一份 `CrossTerm`(`SHARED` + `acquireTerm`/`releaseTerm` 引用计数):心跳只起一次,最后一个组件卸载时才 `stop()`;`start()` 加幂等保护。
357
+ - 启动日志带组件名:`plugin loaded v0.3.29 (跨终端)`。
358
+ - 注意:组件加载顺序不保证,任一组件卸载不影响其余组件。
359
+ ## 0.3.30 — 四个组件各自有名字
360
+
361
+ - 现象:0.3.29 的四行组件在插件详情页都叫 `dsh-crossterm`、描述也一样,只有底部 `crossterm·session` 这类行 ID 能区分。
362
+ - 根因:DSH 的组件行显示文本取自「包」的元数据(`readPluginMeta(specifier)`,即 `${specifier}/package.json` 与 `${specifier}/locale/en.json`),四行指向同一个模块名,自然同名。
363
+ - 解法:给 `package.json` 加 `exports`,把每个组件的 `package.json` 子路径映射到自己的清单:
364
+ - `"./parts/session": "./lib/index.js"`,`"./parts/session/package.json": "./parts/session.manifest.json"`
365
+ - terminal / device / account 同理
366
+ - 清单里的 `name` 与 `description` 就是该组件的显示名与说明(`parts/*.manifest.json`)
367
+ - `cordis.patch.yml` 的四行 `name` 改成 `dsh-crossterm/parts/<part>`,仍靠 `config.part` 决定挂哪组工具,加载的仍是 `lib/index.js`。
368
+ - 已验证 Node 解析:`dsh-crossterm/parts/session/package.json` → `parts/session.manifest.json`。
package/README.md CHANGED
@@ -10,6 +10,26 @@ A 终端看不到 B 终端里有哪个会话、在干什么,也没法把一条
10
10
  - **递得到** —— 把文字/文件投给另一个终端,由**它自己**把消息注入到目标会话的对话里(所以不会撞写锁);
11
11
  - **收得下** —— 文件落到目标会话的工作目录,并把绝对路径交给对方 agent。
12
12
 
13
+ ## 零、简介与本版更新(每次发版重写这一节)
14
+
15
+ **插件名称**:`dsh-crossterm` | **插件简介**:DSH 跨终端互联 —— 同一账号下的多个 DSH 终端/进程互发邮件与文件(主题内联、正文落盘,收件方自己读)。
16
+
17
+ **四个组件(插件页里就是这四个开关,名称与简介来自各自的 manifest)**
18
+
19
+ | 组件名称 | 组件简介 | 工具 |
20
+ |---|---|---|
21
+ | 跨会话 | 同一终端里的多个会话之间互通:特征码链、内联邮件、收件箱 | `term_link` `term_unlink` `term_broadcast` `term_inbox` `term_config` |
22
+ | 跨终端 | 同一台机器上的多个 DSH 终端/进程之间互发邮件与文件 | `term_whoami` `term_list` `term_send` `term_send_files` |
23
+ | 跨设备 | 经云端总线,在多台机器的 DSH 之间互发邮件与文件 | `term_cloud` |
24
+ | 跨账号 | 预留位:后续用于跨账号互通,当前不挂工具 | — |
25
+
26
+ **本版更新(0.4.1)**
27
+
28
+ - 发版纪律落地:**每个版本随包附带简介与本版更新**(`README.md` 这一节 + `CHANGELOG.md` + `更新说明-<版本>.md`),
29
+ npm 包、B 机 zip、云盘 `setup/` 三处一起上传,装到哪台机器都能看到这一版改了什么。
30
+ - `package.json` 的 `files` 补 `CHANGELOG.md`,npm 包里从此能看到完整变更记录。
31
+ - 功能与 0.4.0 完全一致,无行为变化。
32
+
13
33
  ## 一、安装与卸载
14
34
 
15
35
  插件目录:`F:\dsh\001X\dsh-crossterm`
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.40'
25
+ export const VERSION = '0.4.1'
26
26
 
27
27
  /** DSH 的家目录:与宿主其它状态(sessions / profiles)同一个根,多终端天然共享 */
28
28
  export function dshHome() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-crossterm",
3
- "version": "0.3.40",
3
+ "version": "0.4.1",
4
4
  "description": "DSH 跨终端互联:同一账号下的多个 DSH 终端/进程互发邮件与文件(主题内联、正文落盘,收件方自己读)",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -25,7 +25,8 @@
25
25
  "locale",
26
26
  "parts",
27
27
  "cordis.patch.yml",
28
- "README.md"
28
+ "README.md",
29
+ "CHANGELOG.md"
29
30
  ],
30
31
  "license": "MIT",
31
32
  "author": "crossterm",