dsh-crossterm 0.0.0-stage → 0.3.31
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 +517 -2
- package/cordis.patch.yml +41 -0
- package/lib/bus.js +622 -0
- package/lib/cloud/pan.js +152 -0
- package/lib/cloud/sync.js +254 -0
- package/lib/index.js +1790 -0
- package/lib/names.js +255 -0
- package/package.json +47 -3
- package/parts/account.manifest.json +4 -0
- package/parts/device.manifest.json +4 -0
- package/parts/session.manifest.json +4 -0
- package/parts/terminal.manifest.json +4 -0
package/README.md
CHANGED
|
@@ -1,3 +1,518 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-crossterm — 让同一账号的不同终端互相互通
|
|
2
|
+
|
|
3
|
+
同一个 DSH 账号(同一个 `DSH_HOME`,默认 `C:\Users\<你>\.dsh`)可以在多个终端里同时开 DSH:
|
|
4
|
+
桌面 GUI、`dsh web`、`dsh headless`、SSH 终端……它们**本来就共享会话文件**,但进程之间互不相识:
|
|
5
|
+
A 终端看不到 B 终端里有哪个会话、在干什么,也没法把一条消息或一个文件递过去。
|
|
6
|
+
|
|
7
|
+
这个插件补的就是这一段:
|
|
8
|
+
|
|
9
|
+
- **看得见** —— 每个终端进程把「我是谁、我开着哪些会话」写进共享总线目录;
|
|
10
|
+
- **递得到** —— 把文字/文件投给另一个终端,由**它自己**把消息注入到目标会话的对话里(所以不会撞写锁);
|
|
11
|
+
- **收得下** —— 文件落到目标会话的工作目录,并把绝对路径交给对方 agent。
|
|
12
|
+
|
|
13
|
+
## 一、安装与卸载
|
|
14
|
+
|
|
15
|
+
插件目录:`F:\dsh\001X\dsh-crossterm`
|
|
16
|
+
|
|
17
|
+
```powershell
|
|
18
|
+
# 装进桌面 profile(装完需要重启 DSH 才加载)
|
|
19
|
+
dsh plugin --profile desktop add "link:F:\dsh\001X\dsh-crossterm"
|
|
20
|
+
|
|
21
|
+
# 卸载
|
|
22
|
+
dsh plugin --profile desktop remove dsh-crossterm
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
任何 profile 同理(`--profile web` 等)。装/卸都会改 profile 的 `package.json`,
|
|
26
|
+
`link:` 安装意味着**改完源码后重启 DSH 即生效**,不用重新安装。
|
|
27
|
+
|
|
28
|
+
## 二、5 分钟上手
|
|
29
|
+
|
|
30
|
+
1. **两个终端都装好并重启**,各开一个会话。
|
|
31
|
+
2. 在终端 A 问它:`你是谁?`(它会调用 `term_whoami`),能拿到自己的节点名,例如 `TS-01/a3f9c1`。
|
|
32
|
+
3. 在终端 A 说:`看看现在有哪些终端`(`term_list`)——
|
|
33
|
+
会列出 B 终端、B 上打开的会话 id、标题、工作目录、是否最近活跃。
|
|
34
|
+
4. 在终端 A 说:`给 B 发条消息:晚上八点一起跑构建`(`term_send`)。
|
|
35
|
+
5. 终端 B 的会话里会**立刻出现一条用户消息**:
|
|
36
|
+
`[跨终端消息] 来自 「默认终端」(TS-01/a3f9c1) 的会话 …` + 正文。
|
|
37
|
+
B 上正在跑别的任务时会排队(`mode: queue`),不会打断。
|
|
38
|
+
|
|
39
|
+
传文件:
|
|
40
|
+
|
|
41
|
+
6. 终端 A:`把 F:\dsh\001X\fish-images\a.png 发给 B`(`term_send_files`)。
|
|
42
|
+
7. 同一台机器:B 直接拿到**源文件的绝对路径**(不复制、不占双份空间);
|
|
43
|
+
跨机器(总线在共享盘上):文件会**拷贝进总线再落到 B 的会话目录** `crossterm-inbox\<时间戳>\`。
|
|
44
|
+
|
|
45
|
+
## 三、九个工具
|
|
46
|
+
|
|
47
|
+
| 工具 | 作用 | 主要参数 |
|
|
48
|
+
| --- | --- | --- |
|
|
49
|
+
| `term_whoami` | 我是哪个节点、这个进程开着哪些会话、总线路径、配置 | — |
|
|
50
|
+
| `term_list` | 有哪些终端在线、各开着什么会话、哪些特征码 | `includeSelf` |
|
|
51
|
+
| `term_send` | 给某个终端/会话发一封邮件(主题内联、正文落盘,收件方自己 `read`);`to` 写成特征码时等价于广播 | `to`(节点名/id/特征码)、`text`、`subject`、`sessionId`、`silent` |
|
|
52
|
+
| `term_send_files` | 发文件(可同时带一封说明邮件) | `to`、`paths`(绝对路径数组)、`note`、`subject`、`sessionId`、`silent` |
|
|
53
|
+
| `term_link` | 把会话接进一串特征码(默认=调用方自己;只能接宿主认识的会话);不传 `code` 会自动生成 | `code`、`sessionId` |
|
|
54
|
+
| `term_unlink` | 退出某串特征码(默认只退调用方自己;`all=true` 才退本终端全部) | `code`、`sessionId`、`all` |
|
|
55
|
+
| `term_broadcast` | 用一串特征码把同一封邮件/同一批文件同时投给链上所有其它会话 | `code`、`text`、`subject`、`paths`、`silent` |
|
|
56
|
+
| `term_inbox` | 查看/手动投递/丢弃发给本终端的消息 | `action`(list/deliver/drop)、`ids` |
|
|
57
|
+
| `term_config` | 改本终端显示名、是否自动投递、落地目录 | `name`、`autoDeliver`、`inboxDir` |
|
|
58
|
+
|
|
59
|
+
关于 `to`:可以写完整 `nodeId`(`ts-01-a3f9c1`)、显示名(`办公室PC`)、主机名、它们的前缀/片段,
|
|
60
|
+
**或者一串特征码**;前三种是点对点,特征码是一发多投。
|
|
61
|
+
关于 `sessionId`:不填就投给**目标终端当前最活跃的那个会话**;填了就投给指定会话(该会话不在对方进程里时会一直排队,等它被打开)。
|
|
62
|
+
|
|
63
|
+
`silent: true` 表示「只入对方的收件箱,不自动注入对话」,适合通知类消息——对方需要时用 `term_inbox deliver` 拉。
|
|
64
|
+
|
|
65
|
+
### 邮件化:主题内联,正文落盘
|
|
66
|
+
|
|
67
|
+
跨终端投递走的不是「把一整段话塞进对方对话」,而是**邮件**:正文先打包成文件,落到总线目录的 `mail/<消息id>.md`,
|
|
68
|
+
对方会话里只收到**主题 + 正文文件路径 + 附件清单**,正文由那边用 `read` 自己打开。好处很直接:
|
|
69
|
+
|
|
70
|
+
- 长正文不再挤占对方上下文,一次多封也不会把对话刷爆;
|
|
71
|
+
- 邮件在总线上留档(`mail/` 目录),事后能翻;
|
|
72
|
+
- 主题可以单独指定(`subject` 参数),不指定就取正文首行前 60 字。
|
|
73
|
+
|
|
74
|
+
一封邮件的文件长这样(`<总线>/mail/<消息id>.md`):
|
|
75
|
+
|
|
76
|
+
```markdown
|
|
77
|
+
# 主题:热更新方案 v1
|
|
78
|
+
|
|
79
|
+
- 邮件 id:3f9a1b2c-…
|
|
80
|
+
- 发件终端:「办公室PC」(ts-01-a3f9c1)
|
|
81
|
+
- 发件会话:7d1c0e2a
|
|
82
|
+
- 特征码:TEAM42
|
|
83
|
+
- 发出时间:2026-10-17 22:41:03
|
|
84
|
+
- 附件:方案.pdf(41.2 KB)
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
正文全文在这里……
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### 多条未读会合并成一轮对话
|
|
92
|
+
|
|
93
|
+
同一个会话里攒了多条未读时,**不会一条消息开一轮对话**,而是先合并再投递:轮询到一批(同一目标会话的全部未读)
|
|
94
|
+
就拼成**一条** user turn 交进去,里面按 `1/N` 编号逐条列出邮件;每封只给主题和正文文件路径,正文不内联。
|
|
95
|
+
会话一次就能看完整批主题,需要细节再 `read` 对应文件,然后统一回一次。
|
|
96
|
+
|
|
97
|
+
多条未读一起到的时候,投递消息里**每封都还是独立的一封**:各自一条 `── i/N ──` 分节、各自一个 `mail\<id>.md` 正文文件,
|
|
98
|
+
不会把几封的正文拼成同一个文件。清单末尾会明确要求「先逐个 `read` 全部读完,再统一回一封」——这样既不会一封一封回,
|
|
99
|
+
也不会漏读其中某一封。
|
|
100
|
+
|
|
101
|
+
```text
|
|
102
|
+
[跨终端邮件] 本批 3 封(每封都是独立的一封;先把这 3 封全部 read 完,再统一回一封)
|
|
103
|
+
|
|
104
|
+
── 1/3 ──
|
|
105
|
+
主题:第一件事
|
|
106
|
+
来自:「办公室PC」(ts-01-a3f9c1) · 会话 7d1c0e2a · 特征码「TEAM42」
|
|
107
|
+
正文:C:\Users\…\.dsh\crossterm\mail\3f9a1b2c-….md(用 read 工具打开,432 B)
|
|
108
|
+
|
|
109
|
+
── 2/3 ──
|
|
110
|
+
主题:第二件事
|
|
111
|
+
来自:「办公室PC」(ts-01-a3f9c1) · 会话 7d1c0e2a · 特征码「TEAM42」
|
|
112
|
+
正文:C:\Users\…\.dsh\crossterm\mail\7c1e55a0-….md(用 read 工具打开,288 B)
|
|
113
|
+
附件:报表.csv(2.1 KB)
|
|
114
|
+
|
|
115
|
+
── 3/3 ──
|
|
116
|
+
主题:第三件事
|
|
117
|
+
来自:「办公室PC」(ts-01-a3f9c1) · 会话 7d1c0e2a · 特征码「TEAM42」
|
|
118
|
+
正文:C:\Users\…\.dsh\crossterm\mail\9d2b4f10-….md(用 read 工具打开,96 B)
|
|
119
|
+
|
|
120
|
+
(读法:上面 3 封各有各的正文文件,先逐个 read 全部读完,再统一回一封;不要读一封回一封。)
|
|
121
|
+
|
|
122
|
+
(回信:调用 term_broadcast,参数 code="TEAM42",一次回复会发给这串特征码上的所有会话。)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
规则:
|
|
126
|
+
- 只有同一次轮询里、**同一目标会话**的未读会被合批;不同会话各算一批(不会串味);
|
|
127
|
+
- **同一个终端里给别的会话发信,必须显式带 `sessionId`**:不带的话会落进该终端的「当前会话」,
|
|
128
|
+
很可能就发回给自己(插件会直接拒绝并提示,不替你猜);跨终端发到 `to` 即可;
|
|
129
|
+
- 目标会话即使当前没打开,只要**宿主认识它**(在会话列表里),投递会用 `sessionController.prompt` 把它 resume 起来再投;
|
|
130
|
+
`to.sessionId` 可以写完整 id、8 位短名或唯一前缀,收件端会自动归一;
|
|
131
|
+
- 多封**不合并成单个文件**:每封各一个 `mail\<id>.md`,清单里逐封给路径,并要求「先全部读完再统一回一封」;
|
|
132
|
+
- `silent` 消息不参与合批(它们本来就不自动注入,留在收件箱等 `term_inbox deliver`);
|
|
133
|
+
- 每批的 prompt `requestId` 取批次里第一条消息的 id,每条消息各自 settle 归档(归档里能看到 `delivered`,审计不丢);
|
|
134
|
+
- `term_inbox action="deliver"` 手动投递时同样按会话归堆,返回里的 `batches` 就是合并成了几轮;
|
|
135
|
+
- 主题缺省取正文首个非空行(剥掉 `#`/`>`/`-` 前缀,截 60 字);纯附件邮件显示 `(N 个附件,无正文)`;
|
|
136
|
+
- 收件箱列表(`term_inbox`)也只显示主题 + 正文路径,不打印正文。
|
|
137
|
+
|
|
138
|
+
### 会话显示名:对外只显示名字,不显示会话码(0.3.21)
|
|
139
|
+
|
|
140
|
+
会话 id / 8 位短码只在**定位与投递**时用(`to.sessionId`、links.json、日志),所有给人看的文案改显示「会话名」:
|
|
141
|
+
|
|
142
|
+
term_whoami 活会话:4 个(互通插件负责人切换、自动重启、…)
|
|
143
|
+
term_list 会话 互通插件负责人切换 ★当前
|
|
144
|
+
邮件头 - 发件会话:互通插件负责人切换
|
|
145
|
+
term_send/broadcast/link/unlink 回执、错误提示也都走名字
|
|
146
|
+
|
|
147
|
+
**名字就是 DSH 界面上显示的会话标题**,直接读会话转录里的 `session/title` 事件(同一会话可能有多条,
|
|
148
|
+
取 `seq` 最大的那条,手动重命名也走这个事件):
|
|
149
|
+
|
|
150
|
+
{"type":"session/title","seq":14,"data":{"title":"你现在是互通插件的新负责人","source":{"kind":"fallback"}}}
|
|
151
|
+
{"type":"session/title","seq":18,"data":{"title":"互通插件负责人切换","source":{"kind":"provider"}}}
|
|
152
|
+
|
|
153
|
+
为什么不在进程里读 projection(`ctx.sessionProjections.stateOf(session,'title')`):本机实测 9 个会话
|
|
154
|
+
全返回 `undefined`,而转录里标题明明存在,所以直接读转录最可靠。转录头部的 `{"type":"session",...}`
|
|
155
|
+
记录里确实没有 title 字段,别在那里找。
|
|
156
|
+
|
|
157
|
+
兜底顺序:标题事件 → (还没有标题时)首条用户消息截一行 → 8 位短码。派生结果缓存在总线根目录的
|
|
158
|
+
`names.json`(`{ "短码": "名字" }`);有名字的会话 5 分钟复检一次(改标题能跟上),没名字的 1 分钟试一次。
|
|
159
|
+
|
|
160
|
+
## 四、一串特征码:把多个会话串起来
|
|
161
|
+
|
|
162
|
+
这一节是「同账号不同终端」最常用的姿势:**不用记 nodeId、不用管对方是桌面端还是 dsh web,一串码就是一个房间。**
|
|
163
|
+
|
|
164
|
+
```text
|
|
165
|
+
终端 A(桌面端) term_link → 生成 TEAM42
|
|
166
|
+
终端 B(dsh web) term_link code="TEAM42"
|
|
167
|
+
房间里的任意第三个 term_link code="TEAM42"
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
之后:
|
|
171
|
+
|
|
172
|
+
- `term_broadcast code="TEAM42" text="..."` —— 一句话同时投给房间里**其它所有**会话(自己不会收到自己的);
|
|
173
|
+
- `term_send to="TEAM42" text="..."` —— 同上,`to` 直接写码即可;
|
|
174
|
+
- `term_send to="TEAM42" paths=["F:\\报表.xlsx"]` —— 带文件也一样,一次广播所有成员各自落地到本地 `crossterm-inbox`;
|
|
175
|
+
- `term_list` 会显示「在线终端 + 它们各自接在哪串码上 + 每串码几个成员」。
|
|
176
|
+
|
|
177
|
+
特征码规则:`A-Z0-9_-`,自动转大写,至少 4 位;不传就自动生成 6 位(字母表去掉了易混的 `0/O/1/I/L`)。
|
|
178
|
+
|
|
179
|
+
**接链位置规则(0.3.14 起,避免接错会话)**:
|
|
180
|
+
|
|
181
|
+
- `term_link` 不给 `sessionId` 时,接进去的是**调用这个工具的那个会话**(以前会随便挑本终端的一个会话,容易接到别处);
|
|
182
|
+
- 只有**宿主认识**的会话才能接进链:传了不存在的会话 id 会直接报「宿主里找不到会话」,不会留一个投不进去的幽灵成员;
|
|
183
|
+
- 代别的会话接链时可以传 `sessionId`,成员里会记下 `linkedBy`(发起方),便于审计;
|
|
184
|
+
- 退出用 `term_unlink code="TEAM42"`:**默认只退调用方自己**;要退掉本终端接在这串码上的全部会话,显式传 `all=true`;
|
|
185
|
+
- 链上成员全退了码就自动作废。
|
|
186
|
+
|
|
187
|
+
**投递方式分两种(按对端宿主能力自动选)**:
|
|
188
|
+
|
|
189
|
+
| 对端宿主 | 收消息的方式 |
|
|
190
|
+
| --- | --- |
|
|
191
|
+
| 桌面端 / `dsh web`(有 `sessionController`) | **推送**:自动作为一条 user 消息注入对方当前会话,对方的 agent 立刻开始处理 |
|
|
192
|
+
| `dsh headless` 等没有 web-api 层的宿主 | **拉取**:消息留在收件箱,对方的 `term_inbox`(或它自己 `drain` 失败后重试)来取,不消耗重试次数、不会被判失败 |
|
|
193
|
+
|
|
194
|
+
**生命周期**:链关系落盘在 `<总线>/chains/<码>.json` 与 `<总线>/links/<nodeId>.json`。
|
|
195
|
+
`nodeId` 是**跨重启稳定**的:`slug(显示名) + '-' + sha1(主机名|显示名)` 前 6 位(本机为 `ts-01-a52535`)。
|
|
196
|
+
稳定带来两个好处:① 进程重启后仍在同一条链上,不用重新 `term_link`;② 重启前投进本终端信箱的消息不会变成孤儿。
|
|
197
|
+
代价是**同一台机器开多个终端必须区分显示名**(`DSH_CROSSTERM_NAME` 或 `term_config` 的 `name`),否则它们共用一个信箱。
|
|
198
|
+
|
|
199
|
+
> 0.1.x 时 nodeId 是每进程随机(`ts-01-acb266` 这类)。从随机版升级上来的那一次重启,
|
|
200
|
+
> 链上还挂着旧身份成员,广播会投进死信箱 —— 处理办法:删掉 `presence/<旧id>.json`,
|
|
201
|
+
> 各会话重新 `term_link` 一次即可(旧 `inbox/<旧id>/pending` 可移到 `dropped/` 留证)。
|
|
202
|
+
|
|
203
|
+
成员心跳超过 `offlineAfterMs × 3`(默认 60 秒)没刷新,会被自动从链上摘掉,避免对着早就关掉的终端一直投。
|
|
204
|
+
|
|
205
|
+
## 五、配置
|
|
206
|
+
|
|
207
|
+
配置文件:`<DSH_HOME>\crossterm\config.json`(首次运行自动生成),也可以用 `term_config` 工具改。
|
|
208
|
+
|
|
209
|
+
| 键 | 默认 | 说明 |
|
|
210
|
+
| --- | --- | --- |
|
|
211
|
+
| `busDir` | 空 = `<DSH_HOME>\crossterm` | 总线目录。改成共享盘路径即可跨机器(如 `\\nas\dsh-bus`) |
|
|
212
|
+
| `name` | `主机名` | 本终端显示名,多开时用 `term_config` 或环境变量区分 |
|
|
213
|
+
| `autoDeliver` | `true` | 收到消息是否自动注入当前会话;`false` = 只进收件箱,等 `term_inbox` 手动取 |
|
|
214
|
+
| `heartbeatMs` | `5000` | 心跳间隔 |
|
|
215
|
+
| `offlineAfterMs` | `20000` | 多久没心跳算离线 |
|
|
216
|
+
| `pollMs` | `1000` | 收件箱轮询间隔 |
|
|
217
|
+
| `maxFileBytes` | 128 MiB | 单个文件上限(跨机需要真正拷贝时才有意义) |
|
|
218
|
+
| `maxFiles` | 10 | 一次最多几个文件 |
|
|
219
|
+
| `deliverAttempts` | 5 | 注入失败重试次数,超过进 `failed` 目录 |
|
|
220
|
+
| `inboxDir` | 空 = `<会话工作目录>\crossterm-inbox` | 文件落地根目录 |
|
|
221
|
+
|
|
222
|
+
环境变量优先级高于 `config.json`,适合「同一份配置起多个终端」:
|
|
223
|
+
|
|
224
|
+
| 环境变量 | 作用 |
|
|
225
|
+
| --- | --- |
|
|
226
|
+
| `DSH_CROSSTERM_NAME` | 本终端的显示名(多开时区分) |
|
|
227
|
+
| `DSH_CROSSTERM_AUTODELIVER` | `0/false/off/no` = 只进收件箱不自动注入;`1/true/on/yes` = 强制自动注入 |
|
|
228
|
+
|
|
229
|
+
### 手动 / 自动注入开关(三层)
|
|
230
|
+
|
|
231
|
+
| 层 | 开关 | 粒度 |
|
|
232
|
+
| --- | --- | --- |
|
|
233
|
+
| 单条消息 | 发送方带 `silent: true` | 这一条只排队,对方 `term_inbox` 才看得到 |
|
|
234
|
+
| 本终端 | `term_config autoDeliver=false`(写 `config.json`,**立即生效**) | 本进程收到的一切都只进收件箱 |
|
|
235
|
+
| 本进程 | `$env:DSH_CROSSTERM_AUTODELIVER='0'`(启动前设) | 只影响这个终端,且不受 `config.json` 影响 |
|
|
236
|
+
|
|
237
|
+
关掉自动注入后,收消息的流程是:`term_inbox action="list"` 看有什么 → `term_inbox action="deliver" ids=["..."]` 挑要的注入(`ids` 留空 = 全部),
|
|
238
|
+
不想要的 `term_inbox action="drop"`。`config.json` 是同一个 DSH_HOME 共享的,所以 `term_config autoDeliver=false` 会写进共享配置;
|
|
239
|
+
只有调用它的那个进程立即生效,别的终端要等下次重启或自己再调一次 `term_config`。
|
|
240
|
+
|
|
241
|
+
## 六、工作原理(排错时看这里)
|
|
242
|
+
|
|
243
|
+
```
|
|
244
|
+
<busDir>\
|
|
245
|
+
config.json 公共配置(显示名等)
|
|
246
|
+
presence\<nodeId>.json 每个终端的心跳 + 会话清单(20s 无更新算离线)
|
|
247
|
+
inbox\<nodeId>\
|
|
248
|
+
pending\ 发给我的、还没投递成功的消息
|
|
249
|
+
delivered\ 已投递
|
|
250
|
+
failed\ 重试用尽
|
|
251
|
+
files\<msgId>\<name> 需要跨机时暂存的真实字节
|
|
252
|
+
mail\<msgId>.md 邮件正文(主题/发件人/时间 + 全文),收件方按路径自己读
|
|
253
|
+
chains\<码>.json 特征码 → 成员会话(一串码链接多个会话)
|
|
254
|
+
links\<nodeId>.json 本终端把哪些会话接到了哪串码上
|
|
255
|
+
logs\<nodeId>.jsonl 本终端的事件日志
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
- 发送 = 往对方 `inbox\<nodeId>\pending\` 写一个 JSON(**临时文件 + rename 原子落盘**,对方读到的一定是完整消息)。
|
|
259
|
+
- 投递 = 收件方进程 `ctx.sessionController.prompt({ sessionId, mode:'queue', content })`,
|
|
260
|
+
相当于「以用户身份在该会话里说了一句话」。**只有持有该会话的那个进程能投**,
|
|
261
|
+
所以跨进程靠总线传消息,不会出现两个进程同时写同一个会话(避免 `session/writer-held`)。
|
|
262
|
+
- 失败语义:会话暂时不在对方进程里 → 不消耗重试次数,留在 `pending/`;
|
|
263
|
+
真实错误(agent 忙、会话已销毁)→ 每次尝试 +1,到 `deliverAttempts` 进 `failed/` 并记日志。
|
|
264
|
+
|
|
265
|
+
常见问题:
|
|
266
|
+
|
|
267
|
+
- **`term_list` 里看不到另一个终端**:检查两边 `term_whoami` 的 `busDir` 是否一致;对方是不是还在跑;
|
|
268
|
+
总线若在共享盘上,确认两边都能写。
|
|
269
|
+
- **消息进了 pending 但对话里没出现**:目标会话不在对方进程里(比如没打开那个会话),或对方 agent 正忙。
|
|
270
|
+
用 `term_inbox action="list"` 看排队情况。
|
|
271
|
+
- **看不到 title/cwd**:会话刚建、还没有投影时是空的,属正常。
|
|
272
|
+
- **在 PowerShell 里看总线里的中文是乱码**(形如 `鍚屼即缁堢`):这是 PS 5.1 的
|
|
273
|
+
`Get-Content` 默认按 GBK 解码造成的显示问题,文件本身是正确的 UTF-8。
|
|
274
|
+
核对请用 `[System.IO.File]::ReadAllText($p,[System.Text.Encoding]::UTF8)`。
|
|
275
|
+
- 子代理(subagent)会话不接受直接投递,需要投给它所属的 root 会话。
|
|
276
|
+
|
|
277
|
+
## 七、安全边界
|
|
278
|
+
|
|
279
|
+
- 只在本机/共享目录内工作,不联网、不连云端,不依赖任何外部服务。
|
|
280
|
+
- 收到消息后**会作为用户消息进入会话**,agent 会据此行动——所以总线目录只应放在你信任的位置;
|
|
281
|
+
共享盘方式下,谁能写这个目录,谁就能往你的会话里塞话。
|
|
282
|
+
- 文件落地目录可被 agent 读写,`inboxDir` 建议指向专门目录而非系统目录。
|
|
283
|
+
- **谁能改这个插件**:主维护会话 = 桌面端那条长会话(`session-6ef6da30-…`)。其他会话(QQ 会话、别的终端/窗口)
|
|
284
|
+
默认只读,可协助测试与报 bug;**要改代码必须经用户明确点名允许**(见工作区 `AGENTS.md` 第 7 条)。
|
|
285
|
+
|
|
286
|
+
## 八、开发自测
|
|
287
|
+
|
|
288
|
+
```powershell
|
|
289
|
+
$node = 'C:\Users\Administrator\.dsh\dsh-runtimes\dsh-primary-runtime\dependencies\node\bin\node.exe'
|
|
290
|
+
& $node test\bus.test.mjs # 总线:原子写 / 目录布局 / 落地 / 清理
|
|
291
|
+
& $node --import ./test/register.mjs test\plugin.test.mjs # 插件:工具注册 / presence / 自动注入 / 文件语义
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
`@deepseek-ai/dsh-tools` 只存在于宿主的 app.asar 里,普通 node 进程解析不到,所以集成测试用
|
|
295
|
+
`test/register.mjs` + `test/resolve-hooks.mjs` 把该包解析到本地替身 `test/dsh-tools-stub.mjs`。
|
|
296
|
+
**不要在插件目录里放假的 `node_modules/@deepseek-ai/dsh-tools`**——链接安装后那会盖掉宿主的真实实现。
|
|
297
|
+
|
|
298
|
+
## 九、两终端实测
|
|
299
|
+
|
|
300
|
+
### 1. 起第二个终端
|
|
301
|
+
|
|
302
|
+
DSH 同一个 `DSH_HOME`(账号、模型、会话都在里面)可同时跑多个实例,只要端口不同:
|
|
303
|
+
|
|
304
|
+
```powershell
|
|
305
|
+
dsh --profile crossterm-web --port 19999 --no-open
|
|
306
|
+
# 浏览器打开 http://127.0.0.1:19999/ ,用启动日志里的 token 链接进入
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
`crossterm-web` 是个极简 profile(base + web-app + dsh-crossterm),
|
|
310
|
+
`C:\Users\Administrator\.dsh\profiles\crossterm-web\package.json` 里已装好本插件。
|
|
311
|
+
想给这个终端起个便于识别的名字:
|
|
312
|
+
|
|
313
|
+
```powershell
|
|
314
|
+
$env:DSH_CROSSTERM_NAME = 'WEB-2'; dsh --profile crossterm-web --port 19999
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
### 2. 两边互相看得见
|
|
318
|
+
|
|
319
|
+
两个终端各自会话里 `term_list`,应能看到对方的 `nodeId` / 名字 / 工作区 / 活会话。
|
|
320
|
+
投递与文件互传在真宿主里实测通过(`F:\dsh\001X\crossterm\e2e-send.mjs`、`e2e-files.mjs` 是对端模拟脚本):
|
|
321
|
+
|
|
322
|
+
```
|
|
323
|
+
delivered e2e-mvceynw5 -> session-6ef6da30-… # 文本
|
|
324
|
+
delivered e2e-files-mvceyze6 -> session-6ef6da30-… (+1 files) # 文件
|
|
325
|
+
落地:F:\dsh\001X\crossterm-inbox\20261017-211003\互传样例.csv (41B)
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### 3. 断线留存实测(2026-10-17 22:28)
|
|
329
|
+
|
|
330
|
+
这一条专门验证「DSH 关着的时候发来的消息,重开之后还在不在」。
|
|
331
|
+
|
|
332
|
+
做法(脚本 `crossterm\restart-with-offline-test.bat` + `crossterm\offline-send.mjs`):
|
|
333
|
+
|
|
334
|
+
```
|
|
335
|
+
timeout 20s → taskkill DSH → 停机窗口里 offline-send.mjs 投 2 条 → timeout 3s → 重新拉起 DSH
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
结果:
|
|
339
|
+
|
|
340
|
+
```
|
|
341
|
+
offline-send.log target nodeId = ts-01-a52535
|
|
342
|
+
enqueued 2 messages (1 条文本 + 1 个文件 离线留存样例-222811.txt)
|
|
343
|
+
|
|
344
|
+
重启后 ts-01-a52535.jsonl 22:28:16 plugin loaded v0.2.0
|
|
345
|
+
22:28:16 started node=ts-01-a52535 name=TS-01
|
|
346
|
+
22:28:18 delivered 2 msg(s) -> session-ccf34a5a-… (+1 files)
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
- **两条都在**,而且是在启动后第 2 秒投出去的;
|
|
350
|
+
- 两条不同来源的消息**合并成一轮**(0.2.0 的合批);
|
|
351
|
+
- 文件落到 `F:\dsh\001X\crossterm-inbox\20261017-222818\`;
|
|
352
|
+
- 运行日志里两条都是 `outcome=delivered`,`attempts=0`。
|
|
353
|
+
|
|
354
|
+
前提是身份稳定(见第四节):nodeId 跨重启不变,信箱路径才不变,离线期投入的消息才不会变成孤儿。
|
|
355
|
+
对端模拟脚本(`e2e-send.mjs`)与停机窗口投递脚本的区别只有一点:后者在 DSH 进程**不存在**的时候写文件。
|
|
356
|
+
|
|
357
|
+
### 4. 跨机器实测(模拟远端,2026-10-18 00:32)
|
|
358
|
+
|
|
359
|
+
跨机器和同机走的是**两条不同的代码路径**:同 `hostname` 时消息里只带「源路径」,接收方自己复制;
|
|
360
|
+
不同 `hostname` 时发送方必须把字节**暂存进总线** `bus\files\<msgId>\`,接收方再从总线读出来落盘。
|
|
361
|
+
|
|
362
|
+
本机没有第二台物理机,所以用 `F:\dsh\001X\crossterm\xhost-sim.mjs` 模拟远端发送方:
|
|
363
|
+
把 112 B 的二进制载荷写进 `bus\files\<msgId>\`,消息里 `from.hostname='FAKE-REMOTE-PC'`、
|
|
364
|
+
`files:[{name, bytes}]` 且**故意不带 `source`** —— 接收端只剩「从总线取字节」这一条路。
|
|
365
|
+
|
|
366
|
+
结果:
|
|
367
|
+
|
|
368
|
+
```
|
|
369
|
+
日志 delivered 1 msg(s) -> session-6ef6da30-… (+1 files) (16:32:12)
|
|
370
|
+
落地 F:\dsh\001X\crossterm-inbox\20261018-003212\跨机器样例-20261017163211.bin
|
|
371
|
+
校验 112 B,MD5 3D3782C59C94306CF174313FFF21483C(与发送侧一致)
|
|
372
|
+
清理 bus\files\xhost-1792254731534\ 投递后自动清空
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
真正跨机器部署时只需要两步:
|
|
376
|
+
|
|
377
|
+
1. `term_config`(或 `<DSH_HOME>\crossterm\config.json`)把 `busDir` 指到**两边都能读写的共享目录**,
|
|
378
|
+
两台机器的 `name` 取不同值(`identityBase` 用 name,同名会共用信箱);
|
|
379
|
+
2. 两台机器上各自能看到对方的 `presence`(`hostname` 不同即自动走暂存路径),其余用法与同机完全一样。
|
|
380
|
+
|
|
381
|
+
### 5. 注意
|
|
382
|
+
|
|
383
|
+
- 同一会话同一时刻只能由一个进程持有(`session/writer-held`),所以「在 A 终端对 B 终端的某个会话说话」
|
|
384
|
+
是把消息**交给 B 去投**,而不是 A 直接写 B 的会话。
|
|
385
|
+
- 两个实例同时开着同一会话时,以最后持有写锁的那个为准;跨终端沟通建议发到对方**当前活跃会话**。
|
|
386
|
+
|
|
387
|
+
## 十、故障排查:`[exit code: 3221225794]`
|
|
388
|
+
|
|
389
|
+
**现象**:某个会话里所有 `pwsh` 命令都返回空输出 + `[exit code: 3221225794]`(`0xC0000142`),
|
|
390
|
+
连 `Write-Output "alive"`、`$PSVersionTable` 都起不来;但同一个进程里的 `read`/`glob`/`term_*` 全都正常。
|
|
391
|
+
|
|
392
|
+
**不是插件的问题**,也不必然是某一个会话坏了。判定顺序:
|
|
393
|
+
|
|
394
|
+
1. 在**另一个会话**里跑同一条命令。它成功 → 宿主健康,问题在被测会话的沙箱包装层。
|
|
395
|
+
2. 用**全新进程 + 默认 `workspace-write`** 的会话复现(`dsh --profile <任意> "跑一条 pwsh 命令"`)。
|
|
396
|
+
它也失败 → 这台机器上 `workspace-write` 的限制性令牌沙箱失效,全局性问题。
|
|
397
|
+
|
|
398
|
+
**已排除的假设**(都实证过,别再重复走):
|
|
399
|
+
|
|
400
|
+
| 假设 | 结论 | 依据 |
|
|
401
|
+
| --- | --- | --- |
|
|
402
|
+
| 缺 keep-alive 组(logon SID + Everyone) | ❌ 不成立 | `lib/types-Cl_DXjhk.js:1152-1154` 在两种模式下必带;真缺了会在 `CreateRestrictedToken` 前 throw,拿到的是 exit 127,不是 3221225794 |
|
|
403
|
+
| ACL / 工作区授权配坏 | ❌ 不成立 | 换 `danger-full-access` 立刻正常;沙箱目录树未变 |
|
|
404
|
+
| 受限令牌 + Low IL + 沙箱 runner 本身 | ❌ 不成立 | 用 `ELECTRON_RUN_AS_NODE=1` 直跑生产 runner `dsh-sandbox-windows-acl/lib/runner.js --mode read-only -- <目标>`,8 组全通(`powershell.exe -Command exit 42`→42、`$PSVersionTable`→5.1.26100.8875、`cmd /c echo`→0、`node -e`→0) |
|
|
405
|
+
| `windowsHide: true` → `CREATE_NO_WINDOW` → 无控制台 | ❌ 不成立 | 上面那组 A/B 唯一变量就是 windowsHide,结果无差异;`console-probe.js` 实测该子进程**仍然 HAS_CONSOLE**(`CREATE_NO_WINDOW` 不会把子进程从父控制台剥离) |
|
|
406
|
+
| 「无控制台 → DLL 初始化死」 | ❌ 不成立 | 受限子进程里开 `CONOUT$` 探测得 `CHILD-CONSOLE-ABSENT`(确实没有控制台),进程却照常跑完并回吐 stdout;再用 `wscript.exe`(GUI 子系统)→ Electron → 同一条受限令牌链,3/3 exit 0(`probe-asar\electron-parent\result.json`) |
|
|
407
|
+
|
|
408
|
+
文档里那条确实是**已知边界**(`dsh-sandbox-windows-acl/README.md:127`:「Console isolation is unavailable —
|
|
409
|
+
children created with `CREATE_NO_WINDOW` / `CREATE_NEW_CONSOLE` die during DLL initialization with
|
|
410
|
+
`STATUS_DLL_INIT_FAILED` (0xC0000142)」),但它不等于本机这次故障的根因。
|
|
411
|
+
|
|
412
|
+
**已定位到 `workspace-write` 专属路径**(脱离 DSH 宿主复现,`F:\dsh\001X\crossterm\wt-probe.mjs`):
|
|
413
|
+
|
|
414
|
+
直跑生产 runner,唯一变量是 `--mode`:
|
|
415
|
+
|
|
416
|
+
| 命令 | 结果 |
|
|
417
|
+
| --- | --- |
|
|
418
|
+
| `runner.js --workspace F:\dsh\001X --temp <tmp> --mode read-only -- cmd /c echo HI` | exit **0**,输出 `HI` |
|
|
419
|
+
| 同上但 `--mode workspace-write`(带正确 `--write-sid`/`--temp-write-sid`) | exit **3221225794**(无输出) |
|
|
420
|
+
| 同上,`--mode workspace-write`、目标换成 `powershell.exe -Command $PSVersionTable…` | exit **3221225794** |
|
|
421
|
+
|
|
422
|
+
再往下二分(`wt-probe2.mjs`,直接 `new AclSandbox(...)` + `manageDacls:false`,绕开 ACL 与 temp 改写):
|
|
423
|
+
|
|
424
|
+
| 变体 | 结果 |
|
|
425
|
+
| --- | --- |
|
|
426
|
+
| `mode:'read-only'`,不给任何 SID | exit **0** |
|
|
427
|
+
| `mode:'workspace-write'` + 工作区真 SID | exit **3221225794** |
|
|
428
|
+
| `mode:'workspace-write'` + **伪造的无关 S-1-4 双段 SID** | exit **3221225794** |
|
|
429
|
+
| `mode:'workspace-write'` 但完全不传 `writeSid` | 抛 `AclSandbox workspace-write requires a write SID` |
|
|
430
|
+
|
|
431
|
+
⇒ 触发条件与 ACL 授予、私有 temp、`TMP/TEMP` 改写、控制台、完整性级别**都无关**(上面那组
|
|
432
|
+
`manageDacls:false` 一个 ACE 都没写、temp 为 `null`,照样挂);**唯一变量是传给
|
|
433
|
+
`createRestrictedToken(api, currentToken, logonSid, [writeSidPtr, tempWriteSidPtr], { world }, mode)`
|
|
434
|
+
的那个 capability SID**(`lib/types-Cl_DXjhk.js:1288-1290`,read-only 时该数组为空)。
|
|
435
|
+
伪造 SID 也炸,说明不是「授权没给到」这种语义问题,而是**令牌/restricting SID 数组的装配**本身
|
|
436
|
+
(同一段 `:1293` 的 `setTokenDefaultDaclGrant` 会把 token 默认 DACL 的 owner 换成该 capability SID,
|
|
437
|
+
是第二个可疑点)。
|
|
438
|
+
|
|
439
|
+
**根因(已确证,含 A/B 验证的修法)**:
|
|
440
|
+
|
|
441
|
+
故障在 `dsh-sandbox-windows-acl` 初始化令牌的最后一步:
|
|
442
|
+
|
|
443
|
+
```js
|
|
444
|
+
// lib/types-Cl_DXjhk.js:1293
|
|
445
|
+
setTokenDefaultDaclGrant(api, restrictedToken, this.tempWriteSidPtr ?? this.writeSidPtr ?? worldSid);
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
它把 **write SID 的 `FILE_ALL_ACCESS` 并进「令牌默认 DACL」**,作为唯一一条 ACE。
|
|
449
|
+
|
|
450
|
+
- `read-only`:`writeSidPtr` 为空 → 并进去的是 `world`(Everyone),所有进程本来就在组里 → 子进程按默认 DACL 隐式创建对象(管道 / 事件 / section)时 pass-1 能通过 → 一切正常。
|
|
451
|
+
- `workspace-write`:并进去的是 **capability SID `S-1-4-x-y`**。capability SID 永远不会出现在子进程令牌的**组**里,于是子进程 pass-1 无任何可用组 → 连首次 DLL 初始化要建的对象都拿不到访问权 → **`0xC0000142`(STATUS_DLL_INIT_FAILED)**。
|
|
452
|
+
|
|
453
|
+
这解释了全部观察:为什么四个目标(`cmd.exe` / `node.exe` / `where.exe` / `powershell.exe`)在 workspace-write 下**全部**失败、连不写文件的 `where.exe` 也失败;为什么伪造的无关 `S-1-4-*` 一样失败(结构性问题,与授权语义无关);为什么 `danger-full-access` 完全正常(不走这层)。
|
|
454
|
+
|
|
455
|
+
**已验证的修法**(一行,仍在 `:1293` 之前追加):
|
|
456
|
+
|
|
457
|
+
```js
|
|
458
|
+
setTokenDefaultDaclGrant(api, restrictedToken, worldSid); // 或更窄的 logonSid
|
|
459
|
+
setTokenDefaultDaclGrant(api, restrictedToken, this.tempWriteSidPtr ?? this.writeSidPtr ?? worldSid);
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
验证方式:`node:module` 的 `load` 钩子在**内存里**注入这一行(脚本 `probe-asar\patch-hook.mjs` + `probe-asar\wt-fixtest.mjs`,零文件改动、未碰 `F:\dsh\resources`),复跑 `wt-fixtest.mjs`:
|
|
463
|
+
|
|
464
|
+
| 场景 | 补丁前 | 补丁后 |
|
|
465
|
+
| --- | --- | --- |
|
|
466
|
+
| read-only | 0 | 0 |
|
|
467
|
+
| workspace-write + 真 capability SID | 3221225794 | **0** |
|
|
468
|
+
| workspace-write + 伪造 `S-1-4-*` | 3221225794 | **0** |
|
|
469
|
+
| workspace-write + `powershell -Command "exit 42"` | 3221225794 | **42** |
|
|
470
|
+
|
|
471
|
+
`worldSid` 与 `logonSid` 两个变体都验过,结果一致(`WT_PATCH_SID=logonSid`)。
|
|
472
|
+
|
|
473
|
+
**本机已修复(2026-10-17 22:30)**:补丁已打进生产 `F:\dsh\resources\app.asar`,插入的就是上面那一行
|
|
474
|
+
(`setTokenDefaultDaclGrant(api, restrictedToken, logonSid);`,选 `logonSid` 而不是 `worldSid`,范围更窄)。
|
|
475
|
+
|
|
476
|
+
两条基线(自己复核过,不是转述):
|
|
477
|
+
|
|
478
|
+
| 文件 | 字节数 | SHA-256 |
|
|
479
|
+
| --- | --- | --- |
|
|
480
|
+
| 补丁前备份 `F:\dsh\001X\backups\app.asar.orig-20261017-223003` | 121,348,951 | `983CA71114E6DFD353FC79AF5A1F9481A250EE64C2A3C757673029B811B23BC2` |
|
|
481
|
+
| 补丁后现行 `F:\dsh\resources\app.asar` | 121,413,326 | `301642273AE3E1F03C958BFD249A5719EEF9132AADC1939040B489DE6DEE0AEE` |
|
|
482
|
+
|
|
483
|
+
修复后我用同一份 `crossterm\wt-probe3.mjs` 复测,四个目标在 workspace-write 下全部恢复:
|
|
484
|
+
|
|
485
|
+
| 目标 | 修复前 workspace-write | 修复后 workspace-write |
|
|
486
|
+
| --- | --- | --- |
|
|
487
|
+
| `cmd.exe` | 3221225794 | **0**(HI-CMD) |
|
|
488
|
+
| `node.exe` | 3221225794 | **0**(v24.21.0) |
|
|
489
|
+
| `where.exe` | 3221225794 | **0** |
|
|
490
|
+
| `powershell.exe` | 3221225794 | **7**(脚本自己 `exit 7`,符合预期) |
|
|
491
|
+
|
|
492
|
+
**回滚**:把备份换回去(`app.asar.orig-20261017-223003` → `F:\dsh\resources\app.asar`),再重启 DSH。
|
|
493
|
+
|
|
494
|
+
**重新打补丁(升级后要重做时)**:asar 是明文拼接格式,改完要重写头部四个 `uint32`,否则 Electron 直接报
|
|
495
|
+
`Failed to parse header`:
|
|
496
|
+
|
|
497
|
+
```
|
|
498
|
+
@0 = 4 @4 = jsonSize + 8
|
|
499
|
+
@8 = jsonSize + 4 @12 = jsonSize
|
|
500
|
+
JSON 头部从 @16 开始,正文(含补丁后的 JS)紧接其后
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
坑:沿用原文件 `@8` 的旧值会解析失败——那个字段就得写成 `jsonSize + 4`。
|
|
504
|
+
|
|
505
|
+
**副作用(记一笔)**:令牌默认 DACL 里现在多了一条 logon SID 的 `FILE_ALL_ACCESS`,等于把默认 DACL
|
|
506
|
+
放宽到 read-only 的水平;`workspace-write` 对工作区的写限制仍由 `writableDirs` 上的显式 ACE 生效。
|
|
507
|
+
|
|
508
|
+
复现与验证脚本全部在工作区:`crossterm\wt-probe.mjs`、`crossterm\wt-probe2.mjs`、`crossterm\wt-probe3.mjs`、`probe-asar\wt-verify.mjs`、`probe-asar\wt-fixtest.mjs`、`probe-asar\patch-hook.mjs`。
|
|
509
|
+
对端的实验记录见 `F:\dsh\001X\probe-asar\FINDINGS.md`。
|
|
510
|
+
|
|
511
|
+
**三种处置**:
|
|
512
|
+
|
|
513
|
+
| 方案 | 说明 |
|
|
514
|
+
| --- | --- |
|
|
515
|
+
| 真修宿主(**已做**) | 一行补丁进 `app.asar`,见上;改前已整份备份,回滚只需换回备份并重启 |
|
|
516
|
+
| 该会话切 `danger-full-access` | 万一将来失灵:立刻可用,但没有文件沙箱保护(**实测有效**) |
|
|
517
|
+
| 命令发到别的终端代跑 | `term_send` 把命令丢给健康终端,把输出发回来 |
|
|
2
518
|
|
|
3
|
-
This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# ============================================================================
|
|
2
|
+
# dsh-crossterm —— DSH bundle patch(host-only 插件)
|
|
3
|
+
# ============================================================================
|
|
4
|
+
# 安装(本地目录,对每个要互通的 profile 各装一次):
|
|
5
|
+
# dsh plugin --profile desktop add link:F:\dsh\001X\dsh-crossterm
|
|
6
|
+
# dsh plugin --profile web add link:F:\dsh\001X\dsh-crossterm
|
|
7
|
+
# 卸载:
|
|
8
|
+
# dsh plugin --profile desktop remove dsh-crossterm
|
|
9
|
+
#
|
|
10
|
+
# 每个 insert 行在 DSH「插件 → dsh-crossterm → 包含的组件」里是一行组件,可单独开关。
|
|
11
|
+
# 四行指向同一个包,只是走不同子路径:
|
|
12
|
+
#
|
|
13
|
+
# dsh-crossterm/parts/session 跨会话:term_link / term_unlink / term_broadcast / term_inbox / term_config
|
|
14
|
+
# dsh-crossterm/parts/terminal 跨终端:term_whoami / term_list / term_send / term_send_files
|
|
15
|
+
# dsh-crossterm/parts/device 跨设备:term_cloud(百度网盘云总线)
|
|
16
|
+
# dsh-crossterm/parts/account 跨账号:预留占位,暂不挂工具
|
|
17
|
+
#
|
|
18
|
+
# 组件名来自 package.json 的 exports 映射:
|
|
19
|
+
# "./parts/session/package.json" -> "./parts/session.manifest.json"
|
|
20
|
+
# 每个组件显示自己的中文名,实际加载的仍是 lib/index.js,靠 config.part 决定挂哪组工具。
|
|
21
|
+
# 四个组件在同一 DSH 进程里共用一份总线实例:心跳只起一次,云同步只在 device 组件启用
|
|
22
|
+
# 且 config.json 里 cloudEnabled=true 时才跑。安装/改动后需重启对应的 DSH 进程。
|
|
23
|
+
# ============================================================================
|
|
24
|
+
|
|
25
|
+
- insert:
|
|
26
|
+
- id: crossterm-session
|
|
27
|
+
name: dsh-crossterm/parts/session
|
|
28
|
+
config:
|
|
29
|
+
part: session
|
|
30
|
+
- id: crossterm-terminal
|
|
31
|
+
name: dsh-crossterm/parts/terminal
|
|
32
|
+
config:
|
|
33
|
+
part: terminal
|
|
34
|
+
- id: crossterm-device
|
|
35
|
+
name: dsh-crossterm/parts/device
|
|
36
|
+
config:
|
|
37
|
+
part: device
|
|
38
|
+
- id: crossterm-account
|
|
39
|
+
name: dsh-crossterm/parts/account
|
|
40
|
+
config:
|
|
41
|
+
part: account
|