dsh-code-server-app 0.3.7 → 0.3.9
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.en.md +32 -18
- package/README.md +36 -20
- package/assets/extensions/dshcs-editor-bridge/extension.js +3 -1
- package/assets/extensions/dshcs-editor-bridge/lib/bridge-client.js +8 -4
- package/lib/bridge.mjs +19 -7
- package/lib/index.js +186 -37
- package/package.json +2 -1
- package/vendor/VENDOR.json +2 -2
package/README.en.md
CHANGED
|
@@ -207,32 +207,42 @@ only the editor knows, and lets editor gestures drive the current session.
|
|
|
207
207
|
still go through its own `fs` tools; the bridge only *knows about* them.
|
|
208
208
|
- Status bar shows `$(plug) DSH` while connected (click it for the log in the "DSH Editor Bridge" output channel).
|
|
209
209
|
|
|
210
|
-
### The
|
|
210
|
+
### The channels (since 0.3.9 they live on DSH's webServer under `/code-server-bridge`)
|
|
211
211
|
|
|
212
212
|
```
|
|
213
|
-
extension → host POST /
|
|
214
|
-
extension → host POST /
|
|
215
|
-
extension → host GET /
|
|
216
|
-
|
|
217
|
-
host → extension <extensionsDir>/.dshcs-bridge/bridge.json
|
|
213
|
+
extension → host POST /code-server-bridge/sync one round trip: push editor state + take pending events
|
|
214
|
+
extension → host POST /code-server-bridge/ask push an editor question into the current session
|
|
215
|
+
extension → host GET /code-server-bridge/health unauthenticated liveness probe
|
|
216
|
+
extension → host POST /code-server-bridge/event extension reports open/close etc. (host log tail)
|
|
217
|
+
host → extension <extensionsDir>/.dshcs-bridge/bridge.json base URL + token, re-read by the extension every 5s
|
|
218
218
|
```
|
|
219
219
|
|
|
220
|
+
> **Why not under `/api` (fixed in 0.3.9)**: Connection puts a Host/Origin/cookie fence on `/api`
|
|
221
|
+
> (`requestRejection` in `packages/client/connection/src/index.ts` → 401 without a cookie), while the bridge's
|
|
222
|
+
> client is a **Node process inside the extension host** — it can never hold a browser cookie, so its requests were
|
|
223
|
+
> rejected before ever reaching the plugin's route. Measured on 0.3.7: the extension polled
|
|
224
|
+
> `/api/code-server/bridge/sync` and got either 405 (it reached the launcher/VS Code instead) or 401 (the /api
|
|
225
|
+
> fence) — the bridge had never actually synced. It now mounts on DSH's own webServer with its own token as the
|
|
226
|
+
> only gate. The cost: the bridge needs DSH to provide `webServer` — **the web profile has it, desktop does not**.
|
|
227
|
+
> On desktop the host writes no `bridge.json` (dormant beats pointing at a dead address) and says so in the log;
|
|
228
|
+
> **file opening is unaffected** (it uses the signal file and works in every mode).
|
|
229
|
+
|
|
220
230
|
**Why state is pushed, not pulled**: the extension host is a child process of the VS Code server and **listens on
|
|
221
231
|
no port** — the host cannot call into it. Editor state therefore rides the extension's own polling request, and
|
|
222
232
|
the host caches it for the tools (at most one 600 ms cycle behind; older than 10 s and the tool says so instead
|
|
223
233
|
of passing stale data off as fresh).
|
|
224
234
|
|
|
225
|
-
**Why no SSE/WebSocket**:
|
|
226
|
-
the
|
|
227
|
-
|
|
228
|
-
inherently fresh.
|
|
235
|
+
**Why no SSE/WebSocket**: the extension host has no HTTP server of its own, and everything DSH can offer is
|
|
236
|
+
request/response (the Connection fetch channel allows only `GET | HEAD | POST`; streaming would need the WS mux
|
|
237
|
+
already owned by `dsh-api-gateway`). Polling also buys two useful properties: it is idempotent (a dropped event
|
|
238
|
+
only costs one notification — the data always lives in the editor) and the cached state is inherently fresh.
|
|
229
239
|
|
|
230
240
|
### Security model (four invariants; read before touching `lib/bridge.mjs`)
|
|
231
241
|
|
|
232
242
|
The token lives in `<extensionsDir>/.dshcs-bridge/bridge.json`, **readable by any process of the same local
|
|
233
243
|
user**, so:
|
|
234
244
|
|
|
235
|
-
1. **`/
|
|
245
|
+
1. **`/code-server-bridge/*` is permanently read-only.** No route writes files, edits documents, or runs
|
|
236
246
|
commands. A leaked token is therefore bounded to "sees information that is in the editor" and **can never**
|
|
237
247
|
become arbitrary file writes or command execution. A whitelist assertion in `scripts/test-bridge-routes.mjs`
|
|
238
248
|
guards this.
|
|
@@ -574,10 +584,10 @@ Host/Origin fence and browser auth); in the desktop profile `apps/desktop-host`
|
|
|
574
584
|
| POST | `/api/code-server/stop` | Stop and recycle the process tree |
|
|
575
585
|
| POST | `/api/code-server/setup` | **Compatibility no-op**: since 0.1.36 dependencies are installed by the package manager, so this only re-runs the env self-check and returns |
|
|
576
586
|
| POST | `/api/code-server/open-file` | body `{ file }` — writes the signal consumed by the built-in `dshcs-open-file` extension to open the file in code-server |
|
|
577
|
-
| GET | `/
|
|
578
|
-
| POST | `/
|
|
579
|
-
| POST | `/
|
|
580
|
-
| POST | `/
|
|
587
|
+
| GET | `/code-server-bridge/health` | editor-bridge liveness (**unauthenticated**; no editor data). Mounted on DSH's webServer, not under `/api` |
|
|
588
|
+
| POST | `/code-server-bridge/sync` | editor bridge: the extension pushes state (`{context, diagnostics, workspace, at}`) and takes back events; `?since=<seq>` is the event cursor. Requires `x-dshcs-bridge-token`, and **any Origin header is 403** |
|
|
589
|
+
| POST | `/code-server-bridge/ask` | editor bridge: push an editor question into the current session (`{text, file?, lineStart?, lineEnd?, selection?, languageId?}`); **409** when no session can receive it |
|
|
590
|
+
| POST | `/code-server-bridge/event` | editor bridge: extension reports open/close and similar (host log tail). Requires the token |
|
|
581
591
|
|
|
582
592
|
> All four bridge routes carry their own token check — they **cannot** rely on DSH's cookie fence, because the
|
|
583
593
|
> extension host has no browser cookie — and they are read-only by construction. See "Working with DSH" above.
|
|
@@ -653,9 +663,13 @@ What remains on the plugin side:
|
|
|
653
663
|
|
|
654
664
|
## Known limitations
|
|
655
665
|
|
|
656
|
-
- **The editor bridge
|
|
657
|
-
|
|
658
|
-
|
|
666
|
+
- **The editor bridge needs DSH to provide `webServer`** (corrected in 0.3.9): its client is a Node process inside
|
|
667
|
+
the extension host, which can only reach DSH over HTTP at DSH's own origin (the bridge mounts under
|
|
668
|
+
`BRIDGE_BASE` with its own token). **The web profile has webServer (both `serve: dsh` and loopback) → the bridge
|
|
669
|
+
works; desktop has none → it stays disabled** (`bridge.supported=false`, no `bridge.json` is written, one log
|
|
670
|
+
line explains it). **File opening is unaffected**: it uses the signal file and works regardless of mode.
|
|
671
|
+
Up to 0.3.7 the bridge was registered under `/api/code-server/bridge/*` and was killed by Connection's cookie
|
|
672
|
+
fence (401) — that was a bug.
|
|
659
673
|
- **Bridged state can lag by up to 600 ms**, and the tools say "stale" rather than serving data older than 10 s.
|
|
660
674
|
- **Unsaved buffers are reported, not taken over.** The agent still edits via its own `fs` tools, i.e. against
|
|
661
675
|
disk. What the bridge adds is a notice *before* writing a dirty file, a diff *after*, and a warning instead of
|
package/README.md
CHANGED
|
@@ -203,34 +203,47 @@ DSH 用**资源地址**命名文件,`openFile` 只负责把地址交给右侧栏
|
|
|
203
203
|
- 编辑器侧的入口还有状态栏的 `$(plug) DSH`(连通时显示,点击打开日志),日志在输出面板
|
|
204
204
|
「DSH Editor Bridge」里 —— 出问题时先看它。
|
|
205
205
|
|
|
206
|
-
### 三条通道
|
|
206
|
+
### 三条通道(0.3.9 起走 DSH webServer 的 `/code-server-bridge`)
|
|
207
207
|
|
|
208
208
|
```
|
|
209
|
-
扩展 → host POST /
|
|
210
|
-
扩展 → host POST /
|
|
211
|
-
扩展 → host GET /
|
|
212
|
-
|
|
213
|
-
host → 扩展 <extensionsDir>/.dshcs-bridge/bridge.json
|
|
209
|
+
扩展 → host POST /code-server-bridge/sync 一趟来回:上报编辑器状态 + 取回待处理事件
|
|
210
|
+
扩展 → host POST /code-server-bridge/ask 把编辑器里的提问投进当前会话
|
|
211
|
+
扩展 → host GET /code-server-bridge/health 无鉴权探活(便于重启后一眼确认)
|
|
212
|
+
扩展 → host POST /code-server-bridge/event 扩展上报打开/关闭文件等(进 host 日志尾)
|
|
213
|
+
host → 扩展 <extensionsDir>/.dshcs-bridge/bridge.json base URL + 令牌(扩展每 5s 重读)
|
|
214
214
|
```
|
|
215
215
|
|
|
216
|
+
> **为什么不在 `/api` 下(0.3.9 修正)**:Connection 给 `/api` 装了 Host/Origin/cookie fence
|
|
217
|
+
> (`packages/client/connection/src/index.ts` 里 `requestRejection` → 无 cookie 即 401),而桥的客户端
|
|
218
|
+
> 是扩展宿主里的 **Node 进程** —— 它永远拿不到浏览器 cookie,请求在到达插件路由之前就被挡掉了。
|
|
219
|
+
> 实测(0.3.7):扩展按 `/api/code-server/bridge/sync` 轮询,要么 405(打到 launcher/VS Code)、
|
|
220
|
+
> 要么 401(打到 DSH 的 /api fence),**桥从来没有真正同步过**。
|
|
221
|
+
> 现在桥挂在 DSH 自己的 webServer 前缀下,鉴权完全由桥自己的令牌承担(见下)。
|
|
222
|
+
> 代价:桥需要 DSH 提供 `webServer` —— **web profile 有,desktop 没有**。
|
|
223
|
+
> desktop 下 host 不写 `bridge.json`(宁可休眠,不可指向死地址),并在日志里说明;
|
|
224
|
+
> **文件打开不受影响**(它走信号文件,与 serve 模式无关)。
|
|
225
|
+
|
|
216
226
|
**为什么状态是"推"而不是"拉"**:扩展宿主是 VS Code server 的一个子进程,**不监听任何端口** ——
|
|
217
227
|
host 反向请求不到它。所以编辑器状态只能在扩展主动发起的那趟轮询里带上来,host 缓存后给工具读
|
|
218
228
|
(缓存滞后最多一个轮询周期 600ms,超过 10s 没更新就判为过期,工具会明说"状态已过期");
|
|
219
229
|
|
|
220
|
-
**为什么不用 SSE/WebSocket
|
|
221
|
-
(
|
|
222
|
-
|
|
230
|
+
**为什么不用 SSE/WebSocket**:扩展宿主里没有 HTTP 服务器,而 DSH 侧能给的无非是请求/响应
|
|
231
|
+
(Connection 的 fetch 通道只允许 `GET | HEAD | POST`,流式要另走已被 `dsh-api-gateway` 占用的 WS mux)。
|
|
232
|
+
轮询反而给了两条好性质:幂等(丢一次事件只是少一次提示,数据本身永远在编辑器里),以及状态天然最新(每趟都刷新)。
|
|
223
233
|
|
|
224
234
|
### 安全模型(四条不变量,改 `lib/bridge.mjs` 之前先读)
|
|
225
235
|
|
|
226
236
|
桥的令牌写在 `<extensionsDir>/.dshcs-bridge/bridge.json`(**对本机同用户进程可读**),所以:
|
|
227
237
|
|
|
228
|
-
1. **`/
|
|
238
|
+
1. **`/code-server-bridge/*` 永久只读。** 没有写文件、改文档、执行命令的路由。
|
|
229
239
|
令牌泄露的爆炸半径被封在"看到编辑器里的信息",**不会**变成任意文件写/任意命令执行。
|
|
230
|
-
`scripts/test-bridge-routes.mjs`
|
|
240
|
+
`scripts/test-bridge-routes.mjs` 里有一条白名单断言盯着这件事(未知后缀一律 404)。
|
|
231
241
|
2. **带 `Origin` 的请求一律 403。** 浏览器发起必带 Origin(含沙箱 iframe 的 `Origin: null`),
|
|
232
242
|
扩展宿主是 Node 进程、不带。判定顺序上 Origin **先于令牌** —— 否则等于给浏览器一个
|
|
233
243
|
"令牌猜对没有"的 oracle。
|
|
244
|
+
实现细节:Node 路由 → Fetch 适配器把**原始 headers** 挂在 request 上(`dshcsRawHeaders`),
|
|
245
|
+
因为 undici 的 `Request` 构造器会把 `origin` 当 forbidden header 归一化掉 —— 读 `request.headers`
|
|
246
|
+
会让这道 403 静默失效(测试里有这条实测记录)。
|
|
234
247
|
3. **路径收敛在编辑器当前工作区**(`workspaceFolder` 之外的诊断直接丢弃)。
|
|
235
248
|
4. **有界**:诊断默认 200 条 / 单条截断 500 字符 / 上报体上限 256KB / 事件环形缓冲 64 条。
|
|
236
249
|
|
|
@@ -574,15 +587,15 @@ desktop profile 由 `apps/desktop-host` 把 `/api/*` 交给同一个 `createShar
|
|
|
574
587
|
| POST | `/api/code-server/stop` | 停止并回收进程树 |
|
|
575
588
|
| POST | `/api/code-server/setup` | **兼容空操作**:0.1.36 起依赖由包管理器安装,调用只重新自检 `env` 并返回 |
|
|
576
589
|
| POST | `/api/code-server/open-file` | body `{ file }` — 写信号文件,由内置扩展 `dshcs-open-file` 在 code-server 中打开 |
|
|
577
|
-
| GET | `/
|
|
578
|
-
| POST | `/
|
|
579
|
-
| POST | `/
|
|
580
|
-
| POST | `/
|
|
590
|
+
| GET | `/code-server-bridge/health` | 编辑器桥探活(**无鉴权**;只回答"桥活着吗",不含任何编辑器数据)。挂 DSH 的 webServer,不在 `/api` 下 |
|
|
591
|
+
| POST | `/code-server-bridge/sync` | 编辑器桥:扩展上报状态(`{context, diagnostics, workspace, at}`)并取回事件;`?since=<seq>` 是事件游标。需 `x-dshcs-bridge-token`,**带 Origin 一律 403** |
|
|
592
|
+
| POST | `/code-server-bridge/ask` | 编辑器桥:把编辑器里的提问投进当前会话(`{text, file?, lineStart?, lineEnd?, selection?, languageId?}`);没有可投递的会话时回 **409** |
|
|
593
|
+
| POST | `/code-server-bridge/event` | 编辑器桥:扩展上报打开/关闭文件等(进 host 日志尾)。需令牌 |
|
|
581
594
|
|
|
582
595
|
> 桥的四条路由都自带令牌鉴权(它们**不依赖** DSH 的 cookie fence —— 扩展宿主拿不到浏览器 cookie),
|
|
583
|
-
>
|
|
596
|
+
> 且永远只读。这也是它们**不能**挂在 `/api` 下的原因(见「与 DSH 的协同」)。
|
|
584
597
|
|
|
585
|
-
>
|
|
598
|
+
> 除桥之外,插件不再注册任何插件自有 HTTP 路由;code-server 图标已内联为 data URI(client bundle 内),
|
|
586
599
|
> 因此客户端不请求任何插件自有 HTTP 资源。
|
|
587
600
|
|
|
588
601
|
## DSH Desktop(无 webServer)
|
|
@@ -632,9 +645,12 @@ desktop profile 由 `apps/desktop-host` 把 `/api/*` 交给同一个 `createShar
|
|
|
632
645
|
|
|
633
646
|
## 已知限制
|
|
634
647
|
|
|
635
|
-
-
|
|
636
|
-
|
|
637
|
-
|
|
648
|
+
- **编辑器桥需要 DSH 提供 `webServer`**(0.3.9 修正):桥的客户端是扩展宿主里的 Node 进程,
|
|
649
|
+
它只能通过 HTTP 打到 DSH 自己的 origin(桥挂在 `BRIDGE_BASE` 前缀下,自带令牌鉴权)。
|
|
650
|
+
**web profile(`serve: dsh` 或 loopback 都行)有 webServer → 桥可用;desktop 没有 → 不启用**
|
|
651
|
+
(status 的 `bridge.supported=false`,host 不写 `bridge.json`,日志里说明一次)。
|
|
652
|
+
**文件打开不受影响**:它走信号文件,与 serve 模式和 webServer 都无关。
|
|
653
|
+
0.3.7 及以前把桥挂在 `/api/code-server/bridge/*`,被 Connection 的 cookie fence 401 挡死 —— 那是个 bug。
|
|
638
654
|
- **桥的状态有最多 600ms 滞后**:扩展每 600ms 推一次;超过 10s 没更新时工具会明说"状态已过期"
|
|
639
655
|
而不是拿旧数据当新数据(例如用户在 IDE 里关掉面板之后)。
|
|
640
656
|
- **未保存缓冲区是"上报"而不是"接管"**:agent 仍然通过它自己的 `fs` 工具按磁盘内容编辑。
|
|
@@ -496,9 +496,11 @@ function activate(context) {
|
|
|
496
496
|
|
|
497
497
|
// **方向说明(改之前先读)**:扩展宿主里**没有** HTTP 服务器 —— 它是 VS Code server 的
|
|
498
498
|
// 一个子进程,不监听任何端口。所以 host **不能**反向请求本扩展拿编辑器状态。
|
|
499
|
-
// 实际方向是:本扩展在每次轮询里 `POST
|
|
499
|
+
// 实际方向是:本扩展在每次轮询里 `POST <BRIDGE_BASE>/sync`,把状态推上去、
|
|
500
500
|
// 同时取回 host 的待处理事件(agent 改了哪个文件)。host 侧缓存状态供 agent 工具读。
|
|
501
501
|
// 见 lib/bridge-client.js 的 sync() 与 lib/bridge.mjs 顶部的通道说明。
|
|
502
|
+
// **路径是 /code-server-bridge,不是 /api/...**(0.3.9 修正):/api 那层要求浏览器 cookie,
|
|
503
|
+
// 扩展宿主拿不到 ⇒ 请求永远到不了插件路由。
|
|
502
504
|
|
|
503
505
|
// 命令
|
|
504
506
|
context.subscriptions.push(vscode.commands.registerCommand('dsh-code-server.askAboutSelection', () => {
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// 三条通道里属于扩展的两条:
|
|
8
8
|
// 1. 读 `<extensionsDir>/.dshcs-bridge/bridge.json` —— host 写,扩展**每次请求前重读**
|
|
9
9
|
// (host 重启会让端口与令牌轮换,而 IDE 进程可能被 adopt 继续活着,env 方案跟不上);
|
|
10
|
-
// 2. `POST
|
|
10
|
+
// 2. `POST <BRIDGE_BASE>/sync?since=N` —— **一趟来回同时做两件事**:
|
|
11
11
|
// 把编辑器状态(活动文件/脏缓冲区/诊断)推给 host,并取回 host 推来的 agent 改动提示。
|
|
12
12
|
// 为什么合并:扩展宿主里没有 HTTP 服务器,host 反向请求不到它,状态只能由扩展推上来;
|
|
13
13
|
// 而轮询本来就在跑,合并成一个请求就省掉了第二个定时器与一次往返。
|
|
@@ -22,6 +22,9 @@ const path = require('path');
|
|
|
22
22
|
* 里有一条一致性断言,会把两边的字面量放在一起比)。 */
|
|
23
23
|
const BRIDGE_DIRNAME = '.dshcs-bridge';
|
|
24
24
|
const BRIDGE_FILENAME = 'bridge.json';
|
|
25
|
+
/** 桥的挂载前缀。**故意不在 `/api` 下**:那层有 Connection 的 cookie fence,扩展宿主(Node 进程)
|
|
26
|
+
* 拿不到浏览器 cookie,请求会在到达插件路由之前被 401(0.3.9 修正)。 */
|
|
27
|
+
const BRIDGE_BASE = '/code-server-bridge';
|
|
25
28
|
const TOKEN_HEADER = 'x-dshcs-bridge-token';
|
|
26
29
|
const STATE_FILENAME = 'extension-state.json';
|
|
27
30
|
const REQUEST_TIMEOUT_MS = 3000;
|
|
@@ -173,7 +176,7 @@ function createClient(options) {
|
|
|
173
176
|
},
|
|
174
177
|
/** 轻量探活:host 端点是否可达(不碰编辑器)。 */
|
|
175
178
|
async health() {
|
|
176
|
-
return request(
|
|
179
|
+
return request(`${BRIDGE_BASE}/health`);
|
|
177
180
|
},
|
|
178
181
|
/**
|
|
179
182
|
* **一趟来回:上报编辑器状态 + 取回待处理事件。**
|
|
@@ -187,7 +190,7 @@ function createClient(options) {
|
|
|
187
190
|
async sync(payload) {
|
|
188
191
|
if (refreshConfig(false) === null) return { ok: false, error: 'dormant', status: 0 };
|
|
189
192
|
try {
|
|
190
|
-
const body = await request(
|
|
193
|
+
const body = await request(`${BRIDGE_BASE}/sync?since=${since}`, {
|
|
191
194
|
method: 'POST',
|
|
192
195
|
headers: { 'content-type': 'application/json' },
|
|
193
196
|
body: JSON.stringify(payload),
|
|
@@ -203,7 +206,7 @@ function createClient(options) {
|
|
|
203
206
|
},
|
|
204
207
|
/** 把"选中内容 + 问题"投给 DSH 的当前会话。 */
|
|
205
208
|
async ask(payload) {
|
|
206
|
-
return request(
|
|
209
|
+
return request(`${BRIDGE_BASE}/ask`, {
|
|
207
210
|
method: 'POST',
|
|
208
211
|
headers: { 'content-type': 'application/json' },
|
|
209
212
|
body: JSON.stringify(payload),
|
|
@@ -226,6 +229,7 @@ function createClient(options) {
|
|
|
226
229
|
module.exports = {
|
|
227
230
|
BRIDGE_DIRNAME,
|
|
228
231
|
BRIDGE_FILENAME,
|
|
232
|
+
BRIDGE_BASE,
|
|
229
233
|
TOKEN_HEADER,
|
|
230
234
|
STATE_FILENAME,
|
|
231
235
|
POLL_INTERVAL_MS,
|
package/lib/bridge.mjs
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
*
|
|
31
31
|
* ## 安全不变量(改这个文件之前先读这四条)
|
|
32
32
|
*
|
|
33
|
-
* 1.
|
|
33
|
+
* 1. **`BRIDGE_BASE`(`/code-server-bridge`)命名空间永久只读。** 不允许出现任何写文件、改文档、执行命令、
|
|
34
34
|
* 拉起进程的路由 —— `bridge.json` 里那个令牌对本机同用户进程可读,爆炸半径必须封在
|
|
35
35
|
* "泄露编辑器里的信息",绝不能变成任意文件写 / 任意命令执行。
|
|
36
36
|
* 2. **带 `Origin` 的请求一律 403。** 浏览器发起的请求必带 Origin,扩展宿主进程不带。
|
|
@@ -49,8 +49,15 @@ export const BRIDGE_DIRNAME = '.dshcs-bridge';
|
|
|
49
49
|
/** 桥配置文件名。 */
|
|
50
50
|
export const BRIDGE_FILENAME = 'bridge.json';
|
|
51
51
|
|
|
52
|
-
/**
|
|
53
|
-
|
|
52
|
+
/** 桥路由前缀。
|
|
53
|
+
*
|
|
54
|
+
* **故意不放在 `/api` 下(0.3.9 修正)**:Connection 给 `/api` 装了 Host/Origin/cookie fence
|
|
55
|
+
* (`packages/client/connection/src/index.ts`:`requestRejection` → 无 cookie 即 401),而桥的客户端是
|
|
56
|
+
* VS Code 扩展宿主里的一个 Node 进程 —— 它**永远拿不到浏览器 cookie**,请求在到达插件路由之前就被挡掉了。
|
|
57
|
+
* 实测(0.3.7):扩展按 `/api/code-server/bridge/sync` 轮询,请求要么 405(打到 launcher/VS Code)、
|
|
58
|
+
* 要么 401(打到 DSH 的 /api fence),桥从来没有真正同步过。
|
|
59
|
+
* 现在挂到 DSH 自己的 webServer 前缀下,鉴权完全由桥自己的令牌承担(见下方安全不变量)。 */
|
|
60
|
+
export const BRIDGE_BASE = '/code-server-bridge';
|
|
54
61
|
|
|
55
62
|
/** 事件环形缓冲上限:超出即丢最旧的(事件是提示,不是数据)。 */
|
|
56
63
|
export const EVENT_RING_MAX = 64;
|
|
@@ -162,12 +169,17 @@ function originIsBrowser(origin) {
|
|
|
162
169
|
* 顺序有意义:**先看 Origin**(浏览器一律拒绝,且不因为令牌碰巧对就放行 ——
|
|
163
170
|
* 否则等于给浏览器一个"令牌对不对"的 oracle),再看令牌。两者都不消耗资源,故放在最前面。
|
|
164
171
|
*
|
|
165
|
-
*
|
|
172
|
+
* header 读取面:`request.dshcsRawHeaders`(DSH webServer 路由给的 Node 原始 headers)优先于
|
|
173
|
+
* `request.headers` —— 因为 **undici 的 Request 构造器会把 `origin` 当 forbidden header 归一化掉**,
|
|
174
|
+
* 由它包一层的请求读不到 Origin,那道 403 会静默失效(scripts/test-bridge-routes.mjs 里有实测记录)。
|
|
175
|
+
*
|
|
176
|
+
* @param {Request & {dshcsRawHeaders?: {get(name: string): string|null}}} request 桥路由收到的请求
|
|
166
177
|
* @param {string|null} expectedToken 当前桥令牌(未启用时 null)
|
|
167
178
|
*/
|
|
168
179
|
export function bridgeGuard(request, expectedToken) {
|
|
180
|
+
const headers = request?.dshcsRawHeaders ?? request.headers;
|
|
169
181
|
// 不变量 2:浏览器发起必带 Origin(`null` 也算)。扩展宿主(Node)不带。
|
|
170
|
-
const origin =
|
|
182
|
+
const origin = headers.get('origin');
|
|
171
183
|
if (originIsBrowser(origin)) {
|
|
172
184
|
return new Response(JSON.stringify({ ok: false, error: 'bridge 不接受带 Origin 的请求(浏览器一律拒绝)' }), {
|
|
173
185
|
status: 403,
|
|
@@ -180,7 +192,7 @@ export function bridgeGuard(request, expectedToken) {
|
|
|
180
192
|
headers: { 'content-type': 'application/json; charset=utf-8' },
|
|
181
193
|
});
|
|
182
194
|
}
|
|
183
|
-
const provided =
|
|
195
|
+
const provided = headers.get(BRIDGE_TOKEN_HEADER);
|
|
184
196
|
if (!tokenEquals(provided ?? '', expectedToken)) {
|
|
185
197
|
return new Response(JSON.stringify({ ok: false, error: 'unauthorized' }), {
|
|
186
198
|
status: 401,
|
|
@@ -197,7 +209,7 @@ export function bridgeGuard(request, expectedToken) {
|
|
|
197
209
|
* `signal` 来自宿主工具调用的 `exec.signal`,取消即中断。
|
|
198
210
|
*
|
|
199
211
|
* @param {{base: string, token: string}} target 桥目标(启用时由 host 维护)
|
|
200
|
-
* @param {string} route 形如
|
|
212
|
+
* @param {string} route 形如 `${BRIDGE_BASE}/context`
|
|
201
213
|
* @param {{method?: 'GET'|'POST', body?: unknown, query?: Record<string, string>, signal?: AbortSignal}} [options]
|
|
202
214
|
*/
|
|
203
215
|
export async function callBridge(target, route, options = {}) {
|
package/lib/index.js
CHANGED
|
@@ -39,6 +39,7 @@ import { aliasNodePathDirs, ensureRuntimeLayout, resolveRuntime, runtimePackageN
|
|
|
39
39
|
import { MOUNT_PATH, mountOnWebServer } from './serve-dsh.mjs';
|
|
40
40
|
import { DEFAULT_CLAIM_EXTENSIONS, describeClaimPolicy, normalizeClaimExtensions } from './claim-types.js';
|
|
41
41
|
import {
|
|
42
|
+
BRIDGE_BASE,
|
|
42
43
|
BRIDGE_DIRNAME,
|
|
43
44
|
bodyWithinLimit,
|
|
44
45
|
bridgeGuard,
|
|
@@ -210,30 +211,74 @@ const BUNDLED_EXTENSIONS = [
|
|
|
210
211
|
{ name: 'dshcs-editor-bridge', placement: 'user' },
|
|
211
212
|
];
|
|
212
213
|
|
|
214
|
+
/** 递归列出扩展源目录里的文件(相对路径,posix 分隔)。
|
|
215
|
+
* 为什么必须递归:编辑器桥(0.3.7 起)把纯逻辑放在 `lib/*.js` 里(便于单测,不 require('vscode')),
|
|
216
|
+
* 只拷 `extension.js` 会让扩展加载即 `Cannot find module './lib/bridge-client.js'` —— 而 VS Code
|
|
217
|
+
* 只会把这种失败记成一条 `Marked extension as removed`,界面上什么都没发生。
|
|
218
|
+
* 跳过开发期杂物;`.dshcs-bridge` 是运行期元数据目录,不属于扩展本体。
|
|
219
|
+
* 导出供 scripts/test-bridge-routes.mjs 直接验(它不启动 IDE,而安装发生在 start 里)。 */
|
|
220
|
+
export function listExtensionFiles(srcDir) {
|
|
221
|
+
const skip = new Set(['node_modules', '.git', '.dshcs-bridge', 'test', 'tests']);
|
|
222
|
+
const out = [];
|
|
223
|
+
const walk = (rel) => {
|
|
224
|
+
const abs = rel === '' ? srcDir : path.join(srcDir, rel);
|
|
225
|
+
for (const ent of fs.readdirSync(abs, { withFileTypes: true })) {
|
|
226
|
+
if (skip.has(ent.name)) continue;
|
|
227
|
+
const next = rel === '' ? ent.name : `${rel}/${ent.name}`;
|
|
228
|
+
if (ent.isDirectory()) walk(next);
|
|
229
|
+
else if (ent.isFile()) out.push(next);
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
walk('');
|
|
233
|
+
return out;
|
|
234
|
+
}
|
|
235
|
+
|
|
213
236
|
/** 内置扩展安装:每次启动调用 —— 缺失**或内容有变化**即同步(自愈,且插件升级后能更新已装的旧副本)。
|
|
214
|
-
*
|
|
215
|
-
|
|
237
|
+
* 源目录里已不存在的文件会从目标里删掉(否则旧版本的 `lib/` 会一直留着)。
|
|
238
|
+
* 同时清理"放错位置"的旧副本(用户级/内置两份同时存在会让 VS Code 打架)。
|
|
239
|
+
* 导出供测试直接调(apply 期不装扩展,只有 start 才装)。 */
|
|
240
|
+
export function installBundledExtensions(extensionsDir, userDataDir) {
|
|
216
241
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
217
242
|
for (const ext of BUNDLED_EXTENSIONS) {
|
|
218
243
|
try {
|
|
219
244
|
const src = path.join(here, '..', 'assets', 'extensions', ext.name);
|
|
220
245
|
const manifest = path.join(src, 'package.json');
|
|
221
246
|
if (!fs.existsSync(manifest)) continue;
|
|
222
|
-
const files =
|
|
247
|
+
const files = listExtensionFiles(src);
|
|
223
248
|
const target = extensionTarget(extensionsDir, ext.name, ext.placement);
|
|
224
249
|
const dst = target.dst;
|
|
225
250
|
const stale = files.filter((name) => {
|
|
226
251
|
const to = path.join(dst, name);
|
|
227
252
|
if (!fs.existsSync(to)) return true;
|
|
228
253
|
try {
|
|
229
|
-
return fs.readFileSync(path.join(src, name)
|
|
254
|
+
return !fs.readFileSync(path.join(src, name)).equals(fs.readFileSync(to));
|
|
230
255
|
} catch {
|
|
231
256
|
return true;
|
|
232
257
|
}
|
|
233
258
|
});
|
|
234
259
|
if (stale.length > 0) {
|
|
235
|
-
|
|
236
|
-
|
|
260
|
+
for (const name of stale) {
|
|
261
|
+
const to = path.join(dst, name);
|
|
262
|
+
fs.mkdirSync(path.dirname(to), { recursive: true });
|
|
263
|
+
fs.copyFileSync(path.join(src, name), to);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
// 删除目标里多出来的文件(相对源目录):升级后旧文件不该留在扩展目录里
|
|
267
|
+
const removed = [];
|
|
268
|
+
if (fs.existsSync(dst)) {
|
|
269
|
+
const wanted = new Set(files);
|
|
270
|
+
const walkDst = (rel) => {
|
|
271
|
+
const abs = rel === '' ? dst : path.join(dst, rel);
|
|
272
|
+
for (const ent of fs.readdirSync(abs, { withFileTypes: true })) {
|
|
273
|
+
const next = rel === '' ? ent.name : `${rel}/${ent.name}`;
|
|
274
|
+
if (ent.isDirectory()) walkDst(next);
|
|
275
|
+
else if (ent.isFile() && !wanted.has(next)) {
|
|
276
|
+
fs.rmSync(path.join(dst, next), { force: true });
|
|
277
|
+
removed.push(next);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
walkDst('');
|
|
237
282
|
}
|
|
238
283
|
// 清理放错位置的副本:dshcs-open-file 的旧用户级副本 / 编辑器桥的错误内置副本。
|
|
239
284
|
const wrong = extensionTarget(extensionsDir, ext.name, ext.placement === 'builtin' ? 'user' : 'builtin');
|
|
@@ -241,7 +286,8 @@ function installBundledExtensions(extensionsDir, userDataDir) {
|
|
|
241
286
|
fs.rmSync(wrong.dst, { recursive: true, force: true });
|
|
242
287
|
console.log(`[code-server] 清理放错位置的扩展副本 ${wrong.dst}(${ext.name} 应装在${target.builtin ? '内置' : '用户级'}目录)`);
|
|
243
288
|
}
|
|
244
|
-
console.log(`[code-server] bundled extension ${ext.name} -> ${dst}${target.builtin ? ' (内置,不可卸载)' : ' (用户级,可禁用)'}
|
|
289
|
+
console.log(`[code-server] bundled extension ${ext.name} -> ${dst}${target.builtin ? ' (内置,不可卸载)' : ' (用户级,可禁用)'}`
|
|
290
|
+
+ `${stale.length > 0 ? ` [更新 ${stale.join(',')}]` : ''}${removed.length > 0 ? ` [清理 ${removed.join(',')}]` : ''}`);
|
|
245
291
|
} catch (err) {
|
|
246
292
|
console.warn(`[code-server] bundled extension ${ext.name} install failed:`, err && err.message ? err.message : String(err));
|
|
247
293
|
}
|
|
@@ -497,9 +543,9 @@ export async function apply(ctx, config) {
|
|
|
497
543
|
// 关掉 → 立刻下线(删配置 + 注销工具);开启 → 若 IDE 在跑就补一份配置。
|
|
498
544
|
if (bridgeSetting === false) {
|
|
499
545
|
clearBridgeRuntime();
|
|
500
|
-
} else if (state.status === 'running' && state.
|
|
546
|
+
} else if (state.status === 'running' && state.pid !== null) {
|
|
501
547
|
bridgeToken ??= mintBridgeToken();
|
|
502
|
-
syncBridgeRuntime({
|
|
548
|
+
syncBridgeRuntime({ pid: state.pid, startedAt: state.startedAt });
|
|
503
549
|
}
|
|
504
550
|
}
|
|
505
551
|
}
|
|
@@ -594,6 +640,79 @@ export async function apply(ctx, config) {
|
|
|
594
640
|
|
|
595
641
|
/** 本次启动生成的新桥令牌;null = 本实例没有令牌(adopt 旧实例时会回读 bridge.json)。 */
|
|
596
642
|
let bridgeToken = null;
|
|
643
|
+
/** 桥的 webServer 挂载点(0.3.9)。null = 本部署没有可挂载的 HTTP 面(desktop)。 */
|
|
644
|
+
let bridgeMountDispose = null;
|
|
645
|
+
/** "桥不可用"只提示一次,避免每次起停都刷屏。 */
|
|
646
|
+
let bridgeUnavailableLogged = false;
|
|
647
|
+
|
|
648
|
+
/** 扩展宿主(Node 进程)能到达的 DSH origin;null = 本部署没有这样的 HTTP 面。
|
|
649
|
+
*
|
|
650
|
+
* 为什么不能用 launcher 的 host:port(0.3.7 的错误做法):launcher 只服务 workbench,
|
|
651
|
+
* **没有任何 `/api` 路由** —— 实测 `POST http://127.0.0.1:8090/<path-token>/api/code-server/bridge/sync`
|
|
652
|
+
* 会穿透到 VS Code server 并得到 405。
|
|
653
|
+
* 为什么也不能挂在 `/api` 下(0.3.7 的第二个错误):Connection 给 `/api` 装了 cookie fence
|
|
654
|
+
* (`packages/client/connection/src/index.ts`:`requestRejection` → 401),而扩展宿主是 Node 进程、
|
|
655
|
+
* 永远拿不到浏览器 cookie ⇒ 请求在到达插件路由之前就被 401 掉了。
|
|
656
|
+
* 所以桥改挂 DSH 自己的 webServer 前缀(`BRIDGE_BASE`),自带令牌校验(见 bridge.mjs 的安全不变量)。 */
|
|
657
|
+
function bridgeOrigin() {
|
|
658
|
+
if (webServerSvc === undefined || bridgeMountDispose === null) return null;
|
|
659
|
+
const port = typeof webServerSvc.port === 'number' ? webServerSvc.port : null;
|
|
660
|
+
if (port === null || !Number.isSafeInteger(port) || port <= 0) return null;
|
|
661
|
+
const raw = typeof webServerSvc.config?.host === 'string' ? webServerSvc.config.host : '';
|
|
662
|
+
const host = raw === '' || raw === '0.0.0.0' || raw === '::' ? '127.0.0.1' : raw;
|
|
663
|
+
return bridgeUrl(host, port);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/** 桥的四条路由(后缀 → Fetch 风格 handler)。做成函数而非常量:handler 声明在文件后段,
|
|
667
|
+
* 函数声明会提升,而常量在挂载时可能还在 TDZ。 */
|
|
668
|
+
function bridgeRouteTable() {
|
|
669
|
+
return [
|
|
670
|
+
{ suffix: '/health', methods: ['GET'], fetch: handleBridgeHealth },
|
|
671
|
+
{ suffix: '/sync', methods: ['POST'], fetch: handleBridgeSync },
|
|
672
|
+
{ suffix: '/ask', methods: ['POST'], fetch: handleBridgeAsk },
|
|
673
|
+
{ suffix: '/event', methods: ['POST'], fetch: handleBridgeEvent },
|
|
674
|
+
];
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/** 把 Fetch 风格 handler 适配成 DSH webServer 的 Node 路由(req/res ⇄ Request/Response)。 */
|
|
678
|
+
function nodeRouteFromFetch(dispatch) {
|
|
679
|
+
return async (req, res) => {
|
|
680
|
+
try {
|
|
681
|
+
const chunks = [];
|
|
682
|
+
for await (const chunk of req) chunks.push(chunk);
|
|
683
|
+
const body = Buffer.concat(chunks);
|
|
684
|
+
const url = new URL(req.url ?? '/', `http://${req.headers.host ?? '127.0.0.1'}`);
|
|
685
|
+
const method = (req.method ?? 'GET').toUpperCase();
|
|
686
|
+
const response = await dispatch(url, method, req.headers, body);
|
|
687
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
688
|
+
const headers = {};
|
|
689
|
+
response.headers.forEach((value, key) => { headers[key] = value; });
|
|
690
|
+
res.writeHead(response.status, headers);
|
|
691
|
+
res.end(buffer);
|
|
692
|
+
} catch (error) {
|
|
693
|
+
if (!res.headersSent) res.writeHead(500, { 'content-type': 'text/plain; charset=utf-8' });
|
|
694
|
+
if (!res.writableEnded) res.end('bridge route error');
|
|
695
|
+
console.warn(`[code-server] 编辑器桥路由异常:${error && error.message ? error.message : error}`);
|
|
696
|
+
}
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/** 桥挂载点的分发:按后缀查表,方法不符 405,未知路径 404(绝不落到 VS Code 那边)。 */
|
|
701
|
+
async function dispatchBridge(url, method, headers, body) {
|
|
702
|
+
const suffix = url.pathname.slice(BRIDGE_BASE.length) || '/';
|
|
703
|
+
const route = bridgeRouteTable().find((item) => item.suffix === suffix);
|
|
704
|
+
if (route === undefined) return jsonResponse({ ok: false, error: 'unknown bridge route' }, 404);
|
|
705
|
+
if (!route.methods.includes(method)) return jsonResponse({ ok: false, error: 'method not allowed' }, 405);
|
|
706
|
+
const request = new Request(url, {
|
|
707
|
+
method,
|
|
708
|
+
headers,
|
|
709
|
+
...(method === 'GET' || method === 'HEAD' ? {} : { body }),
|
|
710
|
+
});
|
|
711
|
+
// Origin 必须从 Node 的原始 headers 读:undici 的 Request 构造器把 `origin` 当 forbidden header
|
|
712
|
+
// 归一化掉了,再读 request.headers 会得到 null,bridgeGuard 那道 403 就静默失效。
|
|
713
|
+
request.dshcsRawHeaders = { get: (name) => headers[String(name).toLowerCase()] ?? null };
|
|
714
|
+
return route.fetch(request);
|
|
715
|
+
}
|
|
597
716
|
|
|
598
717
|
/** 桥是否启用(行配置为种子,设置文档里可实时改)。 */
|
|
599
718
|
function bridgeEnabled() {
|
|
@@ -617,14 +736,26 @@ export async function apply(ctx, config) {
|
|
|
617
736
|
});
|
|
618
737
|
|
|
619
738
|
/** 同步桥运行时:写入/更新 bridge.json,并(就绪时)注册编辑器工具。
|
|
620
|
-
* 端口、令牌、pid 三者任一变化都重写 —— 扩展每 5s 重读,故不需要任何推送。
|
|
621
|
-
|
|
739
|
+
* 端口、令牌、pid 三者任一变化都重写 —— 扩展每 5s 重读,故不需要任何推送。
|
|
740
|
+
* **没有扩展可达的 origin 时(desktop)不写配置**,并清掉可能遗留的旧配置(宁可休眠,不可指向死地址)。 */
|
|
741
|
+
function syncBridgeRuntime({ pid, startedAt }) {
|
|
622
742
|
if (!bridgeEnabled()) return;
|
|
623
|
-
|
|
743
|
+
const base = bridgeOrigin();
|
|
744
|
+
if (base === null) {
|
|
745
|
+
if (bridgeMeta !== null) {
|
|
746
|
+
bridgeMeta = null;
|
|
747
|
+
try { removeBridgeConfig(bridgeExtensionsDir); } catch { /* 删不掉也不影响:扩展会因不可达而休眠 */ }
|
|
748
|
+
}
|
|
749
|
+
if (!bridgeUnavailableLogged) {
|
|
750
|
+
bridgeUnavailableLogged = true;
|
|
751
|
+
console.warn('[code-server] 编辑器桥:本部署没有扩展可达的 HTTP 面(desktop 无 webServer)→ 桥不启用'
|
|
752
|
+
+ '(文件打开走信号文件,不受影响)');
|
|
753
|
+
}
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
624
756
|
bridgeToken ??= mintBridgeToken();
|
|
625
|
-
const base = bridgeUrl(host, port);
|
|
626
757
|
const changed = bridgeMeta === null || bridgeMeta.url !== base || bridgeMeta.token !== bridgeToken || bridgeMeta.pid !== pid;
|
|
627
|
-
bridgeMeta = { url: base, token: bridgeToken, pid, startedAt: startedAt ?? null
|
|
758
|
+
bridgeMeta = { url: base, token: bridgeToken, pid, startedAt: startedAt ?? null };
|
|
628
759
|
try {
|
|
629
760
|
writeBridgeConfig(bridgeExtensionsDir, { url: base, token: bridgeToken, pid, startedAt: startedAt ?? null });
|
|
630
761
|
} catch (err) {
|
|
@@ -633,7 +764,7 @@ export async function apply(ctx, config) {
|
|
|
633
764
|
}
|
|
634
765
|
if (changed) {
|
|
635
766
|
// 只打印"已启用",不打印令牌本身(与 path-token 同一决策)。
|
|
636
|
-
console.log(`[code-server] 编辑器桥:已启用(${base},令牌文件 ${path.join(bridgeMetaDir, 'bridge.json')})`);
|
|
767
|
+
console.log(`[code-server] 编辑器桥:已启用(${base}${BRIDGE_BASE},令牌文件 ${path.join(bridgeMetaDir, 'bridge.json')})`);
|
|
637
768
|
}
|
|
638
769
|
ensureBridgeTools();
|
|
639
770
|
}
|
|
@@ -642,14 +773,18 @@ export async function apply(ctx, config) {
|
|
|
642
773
|
* 接管实例时对齐桥令牌:磁盘上已有配置且基址一致 → 沿用(避免无谓轮换打乱正在运行的扩展);
|
|
643
774
|
* 否则 mint 新的(扩展下次轮询就会读到新的 bridge.json,一次请求的失败无所谓)。
|
|
644
775
|
*/
|
|
645
|
-
function adoptBridgeRuntime(
|
|
646
|
-
if (!bridgeEnabled()
|
|
776
|
+
function adoptBridgeRuntime(pid, startedAt) {
|
|
777
|
+
if (!bridgeEnabled()) return;
|
|
778
|
+
const base = bridgeOrigin();
|
|
779
|
+
if (base === null) {
|
|
780
|
+
syncBridgeRuntime({ pid, startedAt });
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
647
783
|
const existing = readBridgeConfig(bridgeExtensionsDir);
|
|
648
|
-
const base = bridgeUrl(host, port);
|
|
649
784
|
bridgeToken = existing !== null && existing.url === base && TOKEN_RE.test(String(existing.token))
|
|
650
785
|
? String(existing.token)
|
|
651
786
|
: mintBridgeToken();
|
|
652
|
-
syncBridgeRuntime({
|
|
787
|
+
syncBridgeRuntime({ pid, startedAt });
|
|
653
788
|
}
|
|
654
789
|
|
|
655
790
|
/** 注销桥运行时(停止 IDE / 插件卸载):删配置 + 注销工具 + 清缓存,扩展随即休眠。 */
|
|
@@ -719,6 +854,19 @@ export async function apply(ctx, config) {
|
|
|
719
854
|
dshMount = null;
|
|
720
855
|
console.error(`[code-server] serve=dsh 挂载失败(将回退 loopback):${error && error.message ? error.message : error}`);
|
|
721
856
|
}
|
|
857
|
+
// 编辑器桥(0.3.9):挂在自己的前缀上,自带令牌 —— 走 /api 会被 Connection 的 cookie fence 401
|
|
858
|
+
// (扩展宿主是 Node 进程,没有浏览器 cookie),详见 bridgeOrigin() 的注释。
|
|
859
|
+
try {
|
|
860
|
+
bridgeMountDispose = wsCtx.webServer.register({
|
|
861
|
+
kind: 'prefix',
|
|
862
|
+
path: BRIDGE_BASE,
|
|
863
|
+
handler: nodeRouteFromFetch((url, method, headers, body) => dispatchBridge(url, method, headers, body)),
|
|
864
|
+
});
|
|
865
|
+
console.log(`[code-server] 编辑器桥挂载就绪:${BRIDGE_BASE}/* (DSH webServer,桥令牌鉴权)`);
|
|
866
|
+
} catch (error) {
|
|
867
|
+
bridgeMountDispose = null;
|
|
868
|
+
console.warn(`[code-server] 编辑器桥挂载失败(桥不可用):${error && error.message ? error.message : error}`);
|
|
869
|
+
}
|
|
722
870
|
});
|
|
723
871
|
|
|
724
872
|
ctx.effect(() => () => {
|
|
@@ -728,6 +876,12 @@ export async function apply(ctx, config) {
|
|
|
728
876
|
}
|
|
729
877
|
}, 'code-server: dsh mount');
|
|
730
878
|
|
|
879
|
+
ctx.effect(() => () => {
|
|
880
|
+
if (bridgeMountDispose === null) return;
|
|
881
|
+
try { bridgeMountDispose(); } catch { /* ignore */ }
|
|
882
|
+
bridgeMountDispose = null;
|
|
883
|
+
}, 'code-server: bridge mount');
|
|
884
|
+
|
|
731
885
|
/** 回环模式的客户端 URL:随机端口 + 路径令牌(令牌是 URL 路径的一段,浏览器会把子请求与 WS
|
|
732
886
|
* 一并带过去 —— 这正是不走 VS Code 自带 cookie 令牌的原因,见 launcher 文件头"安全模型")。 */
|
|
733
887
|
function loopbackUrl() {
|
|
@@ -989,7 +1143,7 @@ export async function apply(ctx, config) {
|
|
|
989
1143
|
state.launchCwd = record.launchCwd ?? record.cwd ?? null;
|
|
990
1144
|
state.startedAt = record.startedAt ?? null;
|
|
991
1145
|
state.adopted = true;
|
|
992
|
-
adoptBridgeRuntime(
|
|
1146
|
+
adoptBridgeRuntime(record.pid, state.startedAt);
|
|
993
1147
|
return snapshot();
|
|
994
1148
|
}
|
|
995
1149
|
state.status = 'error';
|
|
@@ -1131,15 +1285,13 @@ export async function apply(ctx, config) {
|
|
|
1131
1285
|
// 编辑器桥:端口已定(loopback + 已知端口)时写入配置,扩展随即能连上来。
|
|
1132
1286
|
// 新启动会轮换桥令牌 —— 与路径令牌同一时机(每次新启动都换)。
|
|
1133
1287
|
// dsh 模式(无独立端口)不启用:桥的 Host 白名单假设是 127.0.0.1:<实际端口>,管道模式没有端口。
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
});
|
|
1142
|
-
}
|
|
1288
|
+
// 桥与 serve 模式无关:只要求"扩展宿主能到达 DSH 的 HTTP 面"(web 有 webServer;
|
|
1289
|
+
// desktop 没有 → syncBridgeRuntime 会自己判定并保持休眠)。
|
|
1290
|
+
bridgeToken = mintBridgeToken();
|
|
1291
|
+
syncBridgeRuntime({
|
|
1292
|
+
pid: proc.pid ?? null,
|
|
1293
|
+
startedAt: state.startedAt,
|
|
1294
|
+
});
|
|
1143
1295
|
|
|
1144
1296
|
proc.on('error', (err) => {
|
|
1145
1297
|
if (child !== proc) return;
|
|
@@ -1414,13 +1566,10 @@ export async function apply(ctx, config) {
|
|
|
1414
1566
|
{ path: `${API_BASE}/setup`, methods: ['POST'], fetch: handleSetup },
|
|
1415
1567
|
{ path: `${API_BASE}/open-file`, methods: ['POST'], fetch: handleOpenFile },
|
|
1416
1568
|
{ path: `${API_BASE}/ui-mode`, methods: ['POST'], fetch: handleUiMode },
|
|
1417
|
-
// ----
|
|
1418
|
-
//
|
|
1419
|
-
//
|
|
1420
|
-
|
|
1421
|
-
{ path: `${API_BASE}/bridge/sync`, methods: ['POST'], fetch: handleBridgeSync },
|
|
1422
|
-
{ path: `${API_BASE}/bridge/ask`, methods: ['POST'], fetch: handleBridgeAsk },
|
|
1423
|
-
{ path: `${API_BASE}/bridge/event`, methods: ['POST'], fetch: handleBridgeEvent },
|
|
1569
|
+
// ---- 编辑器桥的 4 条路由**不在 /api 下**(0.3.9 修正)----
|
|
1570
|
+
// 原因:Connection 给 `/api` 装了 cookie fence(无 cookie → 401),而扩展宿主是 Node 进程、
|
|
1571
|
+
// 永远拿不到浏览器 cookie ⇒ 挂在这里的路由根本到不了(实测:带令牌也被 401/405 挡回)。
|
|
1572
|
+
// 现在挂到 DSH webServer 的 `${BRIDGE_BASE}/*`,自带桥令牌鉴权(见该处注释与 lib/bridge.mjs 的安全不变量)。
|
|
1424
1573
|
].map(route => connection.fetch.register({
|
|
1425
1574
|
path: route.path,
|
|
1426
1575
|
methods: route.methods,
|
|
@@ -1514,7 +1663,7 @@ export async function apply(ctx, config) {
|
|
|
1514
1663
|
state.startedAt = record.startedAt ?? null;
|
|
1515
1664
|
state.adopted = true;
|
|
1516
1665
|
console.log(`[code-server] adopted running instance pid=${record.pid} port=${adoptPort}(令牌已启用)`);
|
|
1517
|
-
adoptBridgeRuntime(
|
|
1666
|
+
adoptBridgeRuntime(record.pid, state.startedAt);
|
|
1518
1667
|
} else {
|
|
1519
1668
|
removePidFile(cfg);
|
|
1520
1669
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-code-server-app",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.9",
|
|
4
4
|
"description": "VS Code (from a code-server release) inside DSH: a right-sidebar tab driven by the plugin's own launcher over the in-process VS Code server (lib/launcher.mjs). Since 0.3.0 the bundle also ships an editor bridge (assets/extensions/dshcs-editor-bridge): a read-only channel between the in-tree VS Code extension host and DSH, giving the agent what only the editor knows (unsaved buffers, language-server diagnostics, the active selection) and letting editor gestures drive the session. The tab claims DSH file addresses (dsh-resource://file/**) by file type (setting claimExtensions), so the product's own produced-file chips, delivered-file previews and inline prose mentions open in the workbench. The IDE is a resident surface moved with Element.moveBefore instead of being remounted, so switching sidebar tabs no longer reloads it. Opening the tab switches the right sidebar to fullscreen by default (setting fullscreenOnOpen). Following a workspace switch is lightweight: the workbench re-navigates with the new ?folder= and the IDE process is not restarted (since 0.2.12). Requires a DSH with the right-sidebar services (sidebarRightTabs/sidebarRight, >= 0.1.5-alpha.1); older DSH versions get a single upgrade notice on the settings page and no other UI. Two serving modes: loopback port (default) or same-origin mount on DSH's own webServer (/code-server, protected by ctx.connection.requestRejection). No code-server Node layer, no argon2, no C++ toolchain.",
|
|
5
5
|
"homepage": "https://github.com/jinsiyu/dsh-code-server-app",
|
|
6
6
|
"repository": {
|
|
@@ -96,6 +96,7 @@
|
|
|
96
96
|
"node-addon-api": "6.1.0",
|
|
97
97
|
"tar": "7.5.22",
|
|
98
98
|
"tas-client": "0.4.3",
|
|
99
|
+
"tslib": "2.8.1",
|
|
99
100
|
"typescript": "6.0.3",
|
|
100
101
|
"vscode-oniguruma": "1.7.0",
|
|
101
102
|
"vscode-regexpp": "3.1.0",
|
package/vendor/VENDOR.json
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
"vscodeVersion": "1.137.0",
|
|
4
4
|
"productPath": "stable-b11dabdaca0d3369986975be285db92c8795cea5",
|
|
5
5
|
"layout": "vscode-only",
|
|
6
|
-
"preparedAt": "2026-09-
|
|
6
|
+
"preparedAt": "2026-09-12T16:34:30.093Z",
|
|
7
7
|
"source": "registry",
|
|
8
|
-
"node": "v24.
|
|
8
|
+
"node": "v24.21.0",
|
|
9
9
|
"platform": "win32",
|
|
10
10
|
"arch": "arm64",
|
|
11
11
|
"sizeMB": 197.9
|