dsh-code-server-app 0.3.12 → 0.3.13
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 -23
- package/README.md +32 -23
- package/assets/extensions/dshcs-editor-bridge/extension.js +5 -5
- package/assets/extensions/dshcs-editor-bridge/lib/bridge-client.js +91 -28
- package/lib/bridge-ipc.mjs +113 -0
- package/lib/bridge.mjs +20 -24
- package/lib/index.js +83 -70
- package/package.json +2 -1
- package/vendor/VENDOR.json +1 -1
package/README.en.md
CHANGED
|
@@ -213,36 +213,43 @@ only the editor knows, and lets editor gestures drive the current session.
|
|
|
213
213
|
every start, so **the bridge never reported any state**. To turn the bridge off use the plugin setting
|
|
214
214
|
`editorBridge=false` (no mount, no tools) rather than uninstalling the extension from the Extensions view.
|
|
215
215
|
|
|
216
|
-
### The channels (since 0.3.
|
|
216
|
+
### The channels (since 0.3.13 over **local IPC**: a Windows named pipe / unix socket)
|
|
217
217
|
|
|
218
218
|
```
|
|
219
219
|
extension → host POST /code-server-bridge/sync one round trip: push editor state + take pending events
|
|
220
220
|
extension → host POST /code-server-bridge/ask push an editor question into the current session
|
|
221
221
|
extension → host GET /code-server-bridge/health unauthenticated liveness probe
|
|
222
222
|
extension → host POST /code-server-bridge/event extension reports open/close etc. (host log tail)
|
|
223
|
-
host → extension <extensionsDir>/.dshcs-bridge/bridge.json
|
|
223
|
+
host → extension <extensionsDir>/.dshcs-bridge/bridge.json endpoint + token, re-read every 5s
|
|
224
224
|
(the directory is announced via the host-injected `DSHCS_EXTENSIONS_DIR` — the extension lives in
|
|
225
225
|
the built-in tree now, so it cannot derive it from its own path)
|
|
226
226
|
```
|
|
227
227
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
>
|
|
231
|
-
>
|
|
232
|
-
>
|
|
233
|
-
>
|
|
234
|
-
>
|
|
235
|
-
>
|
|
236
|
-
> **
|
|
228
|
+
Requests use `http.request({ socketPath })` (`fetch` has no socket support) and **no port is ever opened**.
|
|
229
|
+
|
|
230
|
+
> **Why not HTTP (settled in 0.3.13, all three measured)**
|
|
231
|
+
> 1. **Desktop has no HTTP surface at all**: the renderer calls `host.fetch()` through Electron IPC
|
|
232
|
+
> (`createSharedFetchHandler('/api')` in `apps/desktop-host/src/index.ts:308`) — an in-process call, unreachable
|
|
233
|
+
> from another process; the only HTTP a plugin can mount is the web profile's `webServer`.
|
|
234
|
+
> 2. **`/api` cannot carry it either**: Connection puts a Host/Origin/cookie fence on `/api`
|
|
235
|
+
> (`requestRejection` in `packages/client/connection/src/index.ts` → 401 without a cookie), while the bridge's
|
|
236
|
+
> client is a **Node process inside the extension host** — it can never hold a browser cookie. Measured on 0.3.7:
|
|
237
|
+
> polling `/api/code-server/bridge/sync` returned either 405 (it reached the launcher/VS Code) or 401 (the fence)
|
|
238
|
+
> — the bridge had never actually synced.
|
|
239
|
+
> 3. The two ends are **processes on the same machine** anyway (extension host ← the IDE the plugin spawned ← the
|
|
240
|
+
> plugin). Local IPC is strictly smaller than a port: no network surface, no Host/Origin confused-deputy path, and
|
|
241
|
+
> **web and desktop share one path**. Token auth stays (see below); the Windows pipe name carries a random suffix
|
|
242
|
+
> and the POSIX socket file is `chmod 0600`.
|
|
243
|
+
>
|
|
244
|
+
> History: 0.3.9–0.3.12 mounted it on DSH's `webServer` prefix — which left desktop permanently dormant.
|
|
237
245
|
|
|
238
246
|
**Why state is pushed, not pulled**: the extension host is a child process of the VS Code server and **listens on
|
|
239
247
|
no port** — the host cannot call into it. Editor state therefore rides the extension's own polling request, and
|
|
240
248
|
the host caches it for the tools (at most one 600 ms cycle behind; older than 10 s and the tool says so instead
|
|
241
249
|
of passing stale data off as fresh).
|
|
242
250
|
|
|
243
|
-
**Why no SSE/WebSocket**: the extension host has no HTTP server of its own
|
|
244
|
-
request/response
|
|
245
|
-
already owned by `dsh-api-gateway`). Polling also buys two useful properties: it is idempotent (a dropped event
|
|
251
|
+
**Why no SSE/WebSocket**: the extension host has no HTTP server of its own; the bridge's shape is one
|
|
252
|
+
request/response round trip every 600 ms. Polling also buys two useful properties: it is idempotent (a dropped event
|
|
246
253
|
only costs one notification — the data always lives in the editor) and the cached state is inherently fresh.
|
|
247
254
|
|
|
248
255
|
### Security model (four invariants; read before touching `lib/bridge.mjs`)
|
|
@@ -592,7 +599,7 @@ Host/Origin fence and browser auth); in the desktop profile `apps/desktop-host`
|
|
|
592
599
|
| POST | `/api/code-server/stop` | Stop and recycle the process tree |
|
|
593
600
|
| 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 |
|
|
594
601
|
| 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 |
|
|
595
|
-
| GET | `/code-server-bridge/health` | editor-bridge liveness (**unauthenticated**; no editor data).
|
|
602
|
+
| GET | `/code-server-bridge/health` | editor-bridge liveness (**unauthenticated**; no editor data). Runs over **local IPC** (named pipe / unix socket), not under `/api`, and needs no `webServer` |
|
|
596
603
|
| 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** |
|
|
597
604
|
| 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 |
|
|
598
605
|
| POST | `/code-server-bridge/event` | editor bridge: extension reports open/close and similar (host log tail). Requires the token |
|
|
@@ -610,7 +617,11 @@ Host/Origin fence and browser auth); in the desktop profile `apps/desktop-host`
|
|
|
610
617
|
- The right-sidebar tab, guide entry box, file-address claim, and settings card behave the same as in web (code-server remains an
|
|
611
618
|
iframe to the local `http://127.0.0.1:<port>`; the desktop renderer uses `webSecurity: true` with no CSP, so the cross-origin iframe loads).
|
|
612
619
|
The desktop build ships `dsh-client-ui-sidebar-right` in its seed package set as well, so the 0.2.3 "right-sidebar DSH only" rule is
|
|
613
|
-
not a regression for desktop; the only difference is the missing `webServer`, where `serve: dsh` falls back to loopback
|
|
620
|
+
not a regression for desktop; the only difference is the missing `webServer`, where `serve: dsh` falls back to loopback (that path
|
|
621
|
+
genuinely needs a webServer).
|
|
622
|
+
- **The editor bridge works on desktop since 0.3.13**: it runs over local IPC (named pipe) and does not involve `webServer` at all —
|
|
623
|
+
the extension host is a child of the IDE the plugin itself spawned, so both ends are on the same machine. The host injects
|
|
624
|
+
`DSHCS_EXTENSIONS_DIR`, the extension finds `bridge.json`, and `/status` reports `bridge.supported=true` with the pipe name.
|
|
614
625
|
- Install into the desktop profile through the **desktop plugin manager** (not the CLI, see below).
|
|
615
626
|
- **Desktop installs face a 24-hour supply-chain policy (measured 2026-09-10; this is how 0.2.4 got installed)**:
|
|
616
627
|
- the CLI path is unavailable: `dsh plugin --profile desktop …` is rejected (*"profile "desktop" is managed exclusively by the
|
|
@@ -671,13 +682,11 @@ What remains on the plugin side:
|
|
|
671
682
|
|
|
672
683
|
## Known limitations
|
|
673
684
|
|
|
674
|
-
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
Up to 0.3.7 the bridge was registered under `/api/code-server/bridge/*` and was killed by Connection's cookie
|
|
680
|
-
fence (401) — that was a bug.
|
|
685
|
+
- **~~The editor bridge needs DSH to provide `webServer`~~ no longer true (fixed in 0.3.13)**: the bridge now runs
|
|
686
|
+
over **local IPC** (Windows named pipe / unix socket via `http.request({ socketPath })`), so **web and desktop share
|
|
687
|
+
one path**, with no `webServer` and no open port. History: 0.3.9–0.3.12 mounted it under DSH's webServer prefix
|
|
688
|
+
(⇒ desktop stayed dormant); up to 0.3.7 it was registered under `/api/code-server/bridge/*` and was killed by
|
|
689
|
+
Connection's cookie fence (401). **File opening** was never affected (it uses the signal file).
|
|
681
690
|
- **`/code-server-bridge/health`'s `bridge` field does not mean the extension is running** (clarified in 0.3.12):
|
|
682
691
|
it only says the bridge *target* is configured. Whether the extension actually runs shows up in the exthost log
|
|
683
692
|
or by simply calling `editor_context` — 0.3.0–0.3.11 sat in the state "health says bridge:true, extension never
|
package/README.md
CHANGED
|
@@ -208,34 +208,40 @@ DSH 用**资源地址**命名文件,`openFile` 只负责把地址交给右侧栏
|
|
|
208
208
|
并永远跳过它 —— 每一轮启动都再标一次,**桥因此从来没有上报过状态**。
|
|
209
209
|
要关掉桥请用插件设置 `editorBridge=false`(不挂桥、不注册工具),不要再指望在扩展视图里卸载它。
|
|
210
210
|
|
|
211
|
-
### 三条通道(0.3.
|
|
211
|
+
### 三条通道(0.3.13 起走**本机 IPC**:Windows 命名管道 / unix socket)
|
|
212
212
|
|
|
213
213
|
```
|
|
214
214
|
扩展 → host POST /code-server-bridge/sync 一趟来回:上报编辑器状态 + 取回待处理事件
|
|
215
215
|
扩展 → host POST /code-server-bridge/ask 把编辑器里的提问投进当前会话
|
|
216
216
|
扩展 → host GET /code-server-bridge/health 无鉴权探活(便于重启后一眼确认)
|
|
217
217
|
扩展 → host POST /code-server-bridge/event 扩展上报打开/关闭文件等(进 host 日志尾)
|
|
218
|
-
host → 扩展 <extensionsDir>/.dshcs-bridge/bridge.json
|
|
218
|
+
host → 扩展 <extensionsDir>/.dshcs-bridge/bridge.json 端点 + 令牌(扩展每 5s 重读)
|
|
219
219
|
(目录由 host 注入的 `DSHCS_EXTENSIONS_DIR` 告知 —— 扩展装在内置目录里,自己推不出来)
|
|
220
220
|
```
|
|
221
221
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
>
|
|
225
|
-
>
|
|
226
|
-
>
|
|
227
|
-
>
|
|
228
|
-
>
|
|
229
|
-
>
|
|
230
|
-
>
|
|
222
|
+
请求走 `http.request({ socketPath })`(`fetch` 不支持 socket),**不开任何端口**。
|
|
223
|
+
|
|
224
|
+
> **为什么不是 HTTP(0.3.13 定论,三条都实测过)**
|
|
225
|
+
> 1. **desktop 根本没有 HTTP 面**:渲染进程经 Electron IPC 调 `host.fetch()`
|
|
226
|
+
> (`apps/desktop-host/src/index.ts:308` 的 `createSharedFetchHandler('/api')`)—— 那是进程内函数调用,
|
|
227
|
+
> 进程外不可达;插件能挂 HTTP 的只有 web profile 的 `webServer`。
|
|
228
|
+
> 2. **`/api` 也不行**:Connection 给 `/api` 装了 Host/Origin/cookie fence
|
|
229
|
+
> (`packages/client/connection/src/index.ts` 里 `requestRejection` → 无 cookie 即 401),而桥的客户端
|
|
230
|
+
> 是扩展宿主里的 **Node 进程** —— 它永远拿不到浏览器 cookie。实测(0.3.7):扩展按
|
|
231
|
+
> `/api/code-server/bridge/sync` 轮询,要么 405(打到 launcher/VS Code)、要么 401(打到 /api fence),
|
|
232
|
+
> **桥从来没有真正同步过**。
|
|
233
|
+
> 3. 桥的两端本来就是**同一台机器上的两个进程**(扩展宿主 ← 插件 spawn 的 IDE ← 插件)。
|
|
234
|
+
> 本机 IPC 比开端口更小:没有网络面、没有 Host/Origin 混淆代理问题,**web 与 desktop 走同一条路**。
|
|
235
|
+
> 令牌校验照旧保留(见下),Windows 管道名带随机后缀、POSIX socket 文件 `chmod 0600`。
|
|
236
|
+
>
|
|
237
|
+
> 历史:0.3.9–0.3.12 挂在 DSH 的 `webServer` 前缀下 —— 于是 desktop 永远休眠(没有 webServer)。
|
|
231
238
|
|
|
232
239
|
**为什么状态是"推"而不是"拉"**:扩展宿主是 VS Code server 的一个子进程,**不监听任何端口** ——
|
|
233
240
|
host 反向请求不到它。所以编辑器状态只能在扩展主动发起的那趟轮询里带上来,host 缓存后给工具读
|
|
234
241
|
(缓存滞后最多一个轮询周期 600ms,超过 10s 没更新就判为过期,工具会明说"状态已过期");
|
|
235
242
|
|
|
236
|
-
**为什么不用 SSE/WebSocket**:扩展宿主里没有 HTTP
|
|
237
|
-
(
|
|
238
|
-
轮询反而给了两条好性质:幂等(丢一次事件只是少一次提示,数据本身永远在编辑器里),以及状态天然最新(每趟都刷新)。
|
|
243
|
+
**为什么不用 SSE/WebSocket**:扩展宿主里没有 HTTP 服务器,而桥的形态是"每 600ms 一趟请求/响应"。
|
|
244
|
+
轮询给了两条好性质:幂等(丢一次事件只是少一次提示,数据本身永远在编辑器里),以及状态天然最新(每趟都刷新)。
|
|
239
245
|
|
|
240
246
|
### 安全模型(四条不变量,改 `lib/bridge.mjs` 之前先读)
|
|
241
247
|
|
|
@@ -593,13 +599,14 @@ desktop profile 由 `apps/desktop-host` 把 `/api/*` 交给同一个 `createShar
|
|
|
593
599
|
| POST | `/api/code-server/stop` | 停止并回收进程树 |
|
|
594
600
|
| POST | `/api/code-server/setup` | **兼容空操作**:0.1.36 起依赖由包管理器安装,调用只重新自检 `env` 并返回 |
|
|
595
601
|
| POST | `/api/code-server/open-file` | body `{ file }` — 写信号文件,由内置扩展 `dshcs-open-file` 在 code-server 中打开 |
|
|
596
|
-
| GET | `/code-server-bridge/health` | 编辑器桥探活(**无鉴权**;只回答"桥活着吗",不含任何编辑器数据)
|
|
602
|
+
| GET | `/code-server-bridge/health` | 编辑器桥探活(**无鉴权**;只回答"桥活着吗",不含任何编辑器数据)。走**本机 IPC**(命名管道 / unix socket),不在 `/api` 下、也不需要 `webServer` |
|
|
597
603
|
| POST | `/code-server-bridge/sync` | 编辑器桥:扩展上报状态(`{context, diagnostics, workspace, at}`)并取回事件;`?since=<seq>` 是事件游标。需 `x-dshcs-bridge-token`,**带 Origin 一律 403** |
|
|
598
604
|
| POST | `/code-server-bridge/ask` | 编辑器桥:把编辑器里的提问投进当前会话(`{text, file?, lineStart?, lineEnd?, selection?, languageId?}`);没有可投递的会话时回 **409** |
|
|
599
605
|
| POST | `/code-server-bridge/event` | 编辑器桥:扩展上报打开/关闭文件等(进 host 日志尾)。需令牌 |
|
|
600
606
|
|
|
601
607
|
> 桥的四条路由都自带令牌鉴权(它们**不依赖** DSH 的 cookie fence —— 扩展宿主拿不到浏览器 cookie),
|
|
602
|
-
>
|
|
608
|
+
> 且永远只读。传输是本机 IPC(0.3.13 起),所以 **web 与 desktop 同一套**:
|
|
609
|
+
> 端点由 host 写在 `bridge.json` 的 `pipe` 字段里,扩展用 `http.request({ socketPath })` 访问。
|
|
603
610
|
|
|
604
611
|
> 除桥之外,插件不再注册任何插件自有 HTTP 路由;code-server 图标已内联为 data URI(client bundle 内),
|
|
605
612
|
> 因此客户端不请求任何插件自有 HTTP 资源。
|
|
@@ -611,7 +618,10 @@ desktop profile 由 `apps/desktop-host` 把 `/api/*` 交给同一个 `createShar
|
|
|
611
618
|
- 右侧栏标签、guide 入口框、文件地址认领、设置卡片在 desktop 下与 web 相同(code-server 仍是本机 `http://127.0.0.1:<port>` 的 iframe;
|
|
612
619
|
桌面端 `webSecurity: true` 且页面无 CSP 限制,跨源 iframe 正常加载)。
|
|
613
620
|
桌面端同样自带 `dsh-client-ui-sidebar-right`(见 desktop 构建 seed 包列表),因此 0.2.3 的
|
|
614
|
-
"只支持带右侧栏的 DSH" 对 desktop
|
|
621
|
+
"只支持带右侧栏的 DSH" 对 desktop 不构成降级;`serve: dsh` 会自动回退 loopback(那条路确实需要 webServer)。
|
|
622
|
+
- **编辑器桥在 desktop 下可用(0.3.13 起)**:桥走本机 IPC(命名管道),与 `webServer` 无关 ——
|
|
623
|
+
扩展宿主是插件自己 spawn 的 IDE 的子进程,两端都在同一台机器上。host 注入 `DSHCS_EXTENSIONS_DIR` 后
|
|
624
|
+
扩展即可找到 `bridge.json`;`/status` 的 `bridge.supported/endpoint` 在 desktop 下同样是 `true`/管道名。
|
|
615
625
|
- 安装到 desktop profile:桌面端插件管理窗(**不是** CLI,见下)。
|
|
616
626
|
- **桌面端安装的 24 小时供应链策略(实测,2026-09-10,已用它装上 0.2.4)**:
|
|
617
627
|
- CLI 路径不可用:`dsh plugin --profile desktop …` 会被拒绝(*"profile "desktop" is managed exclusively by the Electron application"*),
|
|
@@ -651,12 +661,11 @@ desktop profile 由 `apps/desktop-host` 把 `/api/*` 交给同一个 `createShar
|
|
|
651
661
|
|
|
652
662
|
## 已知限制
|
|
653
663
|
|
|
654
|
-
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
0.3.7 及以前把桥挂在 `/api/code-server/bridge/*`,被 Connection 的 cookie fence 401 挡死 —— 那是个 bug。
|
|
664
|
+
- **~~编辑器桥需要 DSH 提供 `webServer`~~ 已不成立(0.3.13 修正)**:桥改走**本机 IPC**
|
|
665
|
+
(Windows 命名管道 / unix socket,`http.request({ socketPath })`),**web 与 desktop 同一套**,
|
|
666
|
+
不需要 `webServer`、也不开端口。历史:0.3.9–0.3.12 挂在 DSH 的 webServer 前缀下 ⇒ desktop 永远休眠;
|
|
667
|
+
0.3.7 及以前挂在 `/api/code-server/bridge/*` ⇒ 被 Connection 的 cookie fence 401 挡死。
|
|
668
|
+
**文件打开**从来不受影响(它走信号文件)。
|
|
660
669
|
- **`/code-server-bridge/health` 的 `bridge` 字段不代表扩展在跑**(0.3.12 澄清):它只表示"桥的目标已就绪"。
|
|
661
670
|
扩展是否真的在跑,看 exthost 日志里有没有它的激活记录,或直接用 `editor_context` 试一次 ——
|
|
662
671
|
0.3.0–0.3.11 就是"health 说 bridge:true、扩展却从没被加载"的状态(原因见上:用户级安装被标 `.obsolete`)。
|
|
@@ -353,7 +353,7 @@ async function pollOnce() {
|
|
|
353
353
|
}
|
|
354
354
|
if (!connected) {
|
|
355
355
|
connected = true;
|
|
356
|
-
log(`已连接宿主
|
|
356
|
+
log(`已连接宿主(${client.config.pipe})`);
|
|
357
357
|
updateStatusBar();
|
|
358
358
|
}
|
|
359
359
|
lastPollAt = Date.now();
|
|
@@ -378,7 +378,7 @@ function updateStatusBar() {
|
|
|
378
378
|
if (statusBar === null) return;
|
|
379
379
|
if (connected && client !== null && client.config !== null) {
|
|
380
380
|
statusBar.text = '$(plug) DSH';
|
|
381
|
-
statusBar.tooltip = `编辑器桥已连接:${client.config.
|
|
381
|
+
statusBar.tooltip = `编辑器桥已连接:${client.config.pipe}\n上次轮询:${lastPollAt === 0 ? '—' : new Date(lastPollAt).toLocaleTimeString()}\n点击查看日志`;
|
|
382
382
|
statusBar.command = 'dsh-code-server.showBridgeLog';
|
|
383
383
|
statusBar.show();
|
|
384
384
|
} else {
|
|
@@ -464,7 +464,7 @@ function activate(context) {
|
|
|
464
464
|
log('未找到桥配置(休眠)。DSH 插件启用编辑器桥并启动 IDE 后,这里会自动连上。');
|
|
465
465
|
} else {
|
|
466
466
|
client.restore();
|
|
467
|
-
log(`发现桥配置:${client.config.
|
|
467
|
+
log(`发现桥配置:${client.config.pipe}`);
|
|
468
468
|
}
|
|
469
469
|
|
|
470
470
|
// host 请求时才现算,这里只维护"上次计数",用于日志与将来的变化上报。
|
|
@@ -511,9 +511,9 @@ function activate(context) {
|
|
|
511
511
|
// 定期重读配置(令牌/端口轮换后最多 CONFIG_REREAD_MS 恢复)。
|
|
512
512
|
const refreshTimer = setInterval(() => {
|
|
513
513
|
if (client === null) return;
|
|
514
|
-
const before = client.config === null ? null : client.config.
|
|
514
|
+
const before = client.config === null ? null : client.config.pipe;
|
|
515
515
|
const next = client.refresh();
|
|
516
|
-
const after = next === null ? null : next.
|
|
516
|
+
const after = next === null ? null : next.pipe;
|
|
517
517
|
if (before !== after) {
|
|
518
518
|
log(`桥目标变化:${before ?? '(休眠)'} → ${after ?? '(休眠)'}`);
|
|
519
519
|
if (after !== null) client.restore();
|
|
@@ -6,36 +6,54 @@
|
|
|
6
6
|
//
|
|
7
7
|
// 三条通道里属于扩展的两条:
|
|
8
8
|
// 1. 读 `<extensionsDir>/.dshcs-bridge/bridge.json` —— host 写,扩展**每次请求前重读**
|
|
9
|
-
// (host
|
|
9
|
+
// (host 重启会让端点与令牌轮换,而 IDE 进程可能被 adopt 继续活着,env 方案跟不上);
|
|
10
10
|
// 2. `POST <BRIDGE_BASE>/sync?since=N` —— **一趟来回同时做两件事**:
|
|
11
11
|
// 把编辑器状态(活动文件/脏缓冲区/诊断)推给 host,并取回 host 推来的 agent 改动提示。
|
|
12
12
|
// 为什么合并:扩展宿主里没有 HTTP 服务器,host 反向请求不到它,状态只能由扩展推上来;
|
|
13
13
|
// 而轮询本来就在跑,合并成一个请求就省掉了第二个定时器与一次往返。
|
|
14
14
|
// 带着 x-dshcs-bridge-token 头。
|
|
15
|
+
//
|
|
16
|
+
// **传输是本机 IPC(0.3.13 起)**:配置里的 `pipe` 是 Windows 命名管道名或 unix socket 路径,
|
|
17
|
+
// 请求走 `http.request({ socketPath })`。不走 HTTP 的三个原因(都实测过):desktop 没有 HTTP 面
|
|
18
|
+
// (渲染进程经 Electron IPC 调 host.fetch)、`/api` 有浏览器 cookie fence(扩展宿主拿不到 cookie)、
|
|
19
|
+
// DSH 的 webServer 前缀只有 web profile 有。详见 lib/bridge-ipc.mjs 与 docs 第 19 节。
|
|
15
20
|
|
|
16
21
|
'use strict';
|
|
17
22
|
|
|
18
23
|
const fs = require('fs');
|
|
19
24
|
const path = require('path');
|
|
25
|
+
const http = require('http');
|
|
20
26
|
|
|
21
27
|
/** 与 host 侧 lib/bridge.mjs 的常量保持一致(改动必须两边同步;scripts/test-bridge-extension.mjs
|
|
22
28
|
* 里有一条一致性断言,会把两边的字面量放在一起比)。 */
|
|
23
29
|
const BRIDGE_DIRNAME = '.dshcs-bridge';
|
|
24
30
|
const BRIDGE_FILENAME = 'bridge.json';
|
|
25
|
-
/**
|
|
26
|
-
* 拿不到浏览器 cookie,请求会在到达插件路由之前被 401(0.3.9 修正)。 */
|
|
31
|
+
/** 本机 IPC 上的路由前缀(不是 URL 前缀;语义见 lib/bridge.mjs)。 */
|
|
27
32
|
const BRIDGE_BASE = '/code-server-bridge';
|
|
28
33
|
const TOKEN_HEADER = 'x-dshcs-bridge-token';
|
|
29
34
|
const STATE_FILENAME = 'extension-state.json';
|
|
30
35
|
const REQUEST_TIMEOUT_MS = 3000;
|
|
31
36
|
/** 轮询间隔:host 侧事件只是"去看一眼这个文件"的提示,600ms 足够且几乎无开销。 */
|
|
32
37
|
const POLL_INTERVAL_MS = 600;
|
|
33
|
-
/** 宿主配置重读间隔(
|
|
38
|
+
/** 宿主配置重读间隔(端点/令牌轮换后最多这么久恢复)。 */
|
|
34
39
|
const CONFIG_REREAD_MS = 5000;
|
|
35
40
|
|
|
41
|
+
/** Windows 命名管道的统一前缀(`\\.\pipe\` 是本机命名空间)。 */
|
|
42
|
+
const WIN_PIPE_PREFIX = '\\\\.\\pipe\\';
|
|
36
43
|
|
|
37
44
|
const TOKEN_RE = /^[0-9A-Za-z_-]{16,128}$/;
|
|
38
45
|
|
|
46
|
+
/** 端点形状校验:Windows 必须是命名管道名,其它平台必须像绝对路径。
|
|
47
|
+
* (与 host 侧 lib/bridge-ipc.mjs 的 `isBridgeEndpoint` 同一条判定,两边各有实现、
|
|
48
|
+
* 一致性由 scripts/test-bridge-extension.mjs 的断言钉住 —— 扩展是随包分发的静态文件,
|
|
49
|
+
* 不能 import host 的代码。) */
|
|
50
|
+
function isBridgeEndpoint(value) {
|
|
51
|
+
if (typeof value !== 'string' || value === '') return false;
|
|
52
|
+
if (process.platform === 'win32') return value.startsWith(WIN_PIPE_PREFIX);
|
|
53
|
+
return value.startsWith('/') || /^[A-Za-z]:[\\/]/.test(value);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
39
57
|
/** 桥配置所在目录(=`<extensionsDir>/.dshcs-bridge`)。
|
|
40
58
|
*
|
|
41
59
|
* 取值顺序(0.3.12 修正 —— 这里以前写错了,是"桥永远休眠"的另一半原因):
|
|
@@ -67,15 +85,15 @@ function stateFile(extensionsDir) {
|
|
|
67
85
|
|
|
68
86
|
/**
|
|
69
87
|
* 读桥配置。
|
|
70
|
-
* @returns {{
|
|
88
|
+
* @returns {{pipe: string, token: string, pid: number|null}|null} null = 未配置/格式不对 → 休眠
|
|
71
89
|
*/
|
|
72
90
|
function readBridgeConfig(extensionsDir) {
|
|
73
91
|
try {
|
|
74
92
|
const raw = JSON.parse(fs.readFileSync(bridgeFile(extensionsDir), 'utf8'));
|
|
75
93
|
if (raw === null || typeof raw !== 'object') return null;
|
|
76
|
-
if (
|
|
94
|
+
if (!isBridgeEndpoint(raw.pipe)) return null;
|
|
77
95
|
if (!TOKEN_RE.test(String(raw.token))) return null;
|
|
78
|
-
return {
|
|
96
|
+
return { pipe: String(raw.pipe), token: String(raw.token), pid: Number.isSafeInteger(raw.pid) ? raw.pid : null };
|
|
79
97
|
} catch {
|
|
80
98
|
return null;
|
|
81
99
|
}
|
|
@@ -108,22 +126,68 @@ function writeState(extensionsDir, state) {
|
|
|
108
126
|
|
|
109
127
|
/** host 侧拒绝时的统一错误(带 status,便于区分 401/503/403)。 */
|
|
110
128
|
class BridgeError extends Error {
|
|
111
|
-
constructor(message, status) {
|
|
129
|
+
constructor(message, status, code) {
|
|
112
130
|
super(message);
|
|
113
131
|
this.name = 'BridgeError';
|
|
114
132
|
this.status = status;
|
|
133
|
+
/** 底层 IO 错误码(EPERM/ENOENT/ECONNREFUSED…);状态栏与日志据此区分"权限/不存在/没在跑"。 */
|
|
134
|
+
this.code = code;
|
|
115
135
|
}
|
|
116
136
|
}
|
|
117
137
|
|
|
138
|
+
/**
|
|
139
|
+
* 默认传输:本机 IPC(Windows 命名管道 / unix socket)上的一个 HTTP 请求。
|
|
140
|
+
*
|
|
141
|
+
* 不用 `fetch`:WHATWG fetch 不支持 `socketPath`,而桥**必须**绕开网络栈
|
|
142
|
+
* (desktop 没有 HTTP 面;web 上走 HTTP 则要么撞 cookie fence、要么依赖 webServer)。
|
|
143
|
+
* 用 `http.request({socketPath})` 是 Node 里唯一干净的写法 —— 与 host 侧
|
|
144
|
+
* `lib/launcher.mjs` 的 `--pipe`、`lib/index.js` 的 `healthCheckPipe` 同一套。
|
|
145
|
+
*
|
|
146
|
+
* @param {{socketPath: string, path: string, method: string, headers: object,
|
|
147
|
+
* body: string|null, timeoutMs: number}} options
|
|
148
|
+
* @returns {Promise<{status: number, json: unknown}>}
|
|
149
|
+
*/
|
|
150
|
+
function defaultRequest({ socketPath, path: routePath, method, headers, body, timeoutMs }) {
|
|
151
|
+
return new Promise((resolve, reject) => {
|
|
152
|
+
const payload = typeof body === 'string' && body !== '' ? Buffer.from(body, 'utf8') : null;
|
|
153
|
+
const req = http.request({
|
|
154
|
+
socketPath,
|
|
155
|
+
path: routePath,
|
|
156
|
+
method,
|
|
157
|
+
headers: payload === null ? headers : Object.assign({ 'content-length': String(payload.length) }, headers),
|
|
158
|
+
timeout: timeoutMs,
|
|
159
|
+
}, (res) => {
|
|
160
|
+
const chunks = [];
|
|
161
|
+
res.on('data', (chunk) => { chunks.push(chunk); });
|
|
162
|
+
res.on('error', reject);
|
|
163
|
+
res.on('end', () => {
|
|
164
|
+
const text = Buffer.concat(chunks).toString('utf8');
|
|
165
|
+
let json = null;
|
|
166
|
+
try {
|
|
167
|
+
json = text === '' ? null : JSON.parse(text);
|
|
168
|
+
} catch {
|
|
169
|
+
json = null;
|
|
170
|
+
}
|
|
171
|
+
resolve({ status: res.statusCode === undefined ? 0 : res.statusCode, json });
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
req.on('timeout', () => { req.destroy(new Error(`请求超时(${timeoutMs}ms)`)); });
|
|
175
|
+
req.on('error', reject);
|
|
176
|
+
if (payload !== null) req.write(payload);
|
|
177
|
+
req.end();
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
118
181
|
/**
|
|
119
182
|
* 造一个桥客户端。
|
|
120
183
|
*
|
|
121
|
-
* @param {{extensionsDir?: string,
|
|
122
|
-
* `
|
|
184
|
+
* @param {{extensionsDir?: string, requestImpl?: Function, now?: Function}} [options]
|
|
185
|
+
* `requestImpl` / `now` 可注入,便于单测(默认走 `http.request({socketPath})`)。
|
|
123
186
|
*/
|
|
124
187
|
function createClient(options) {
|
|
125
188
|
const extensionsDir = options && options.extensionsDir !== undefined ? options.extensionsDir : undefined;
|
|
126
|
-
|
|
189
|
+
/** 传输:本机 IPC 上的一个 HTTP 请求。注入点只在测试里用。 */
|
|
190
|
+
const requestImpl = (options && options.requestImpl) || defaultRequest;
|
|
127
191
|
/** 当前配置(null = 休眠)。 */
|
|
128
192
|
let config = null;
|
|
129
193
|
/** 上次读配置的时间(避免每 600ms 都碰磁盘)。 */
|
|
@@ -136,8 +200,8 @@ function createClient(options) {
|
|
|
136
200
|
if (!force && config !== null && now - configReadAt < CONFIG_REREAD_MS) return config;
|
|
137
201
|
configReadAt = now;
|
|
138
202
|
const next = readBridgeConfig(extensionsDir);
|
|
139
|
-
if (next !== null && config !== null && next.
|
|
140
|
-
//
|
|
203
|
+
if (next !== null && config !== null && next.pipe !== config.pipe) {
|
|
204
|
+
// 端点/实例变了:游标失去意义(旧实例的事件不该在新实例上重放)。
|
|
141
205
|
since = 0;
|
|
142
206
|
}
|
|
143
207
|
if (next !== null && config !== null && next.pid !== config.pid) since = 0;
|
|
@@ -150,24 +214,21 @@ function createClient(options) {
|
|
|
150
214
|
const current = refreshConfig(true);
|
|
151
215
|
if (current === null) throw new BridgeError('编辑器桥未配置(休眠中)', 0);
|
|
152
216
|
const headers = Object.assign({ [TOKEN_HEADER]: current.token }, (init && init.headers) || {});
|
|
153
|
-
const controller = new AbortController();
|
|
154
|
-
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
155
217
|
let response;
|
|
156
218
|
try {
|
|
157
|
-
response = await
|
|
219
|
+
response = await requestImpl({
|
|
220
|
+
socketPath: current.pipe,
|
|
221
|
+
path: route,
|
|
222
|
+
method: (init && init.method) || 'GET',
|
|
223
|
+
headers,
|
|
224
|
+
body: init && init.body !== undefined ? init.body : null,
|
|
225
|
+
timeoutMs: REQUEST_TIMEOUT_MS,
|
|
226
|
+
});
|
|
158
227
|
} catch (error) {
|
|
159
|
-
throw new BridgeError(`无法连接宿主:${error && error.message ? error.message : String(error)}`, 0);
|
|
160
|
-
} finally {
|
|
161
|
-
clearTimeout(timer);
|
|
162
|
-
}
|
|
163
|
-
const text = await response.text();
|
|
164
|
-
let body = null;
|
|
165
|
-
try {
|
|
166
|
-
body = text === '' ? null : JSON.parse(text);
|
|
167
|
-
} catch {
|
|
168
|
-
body = null;
|
|
228
|
+
throw new BridgeError(`无法连接宿主:${error && error.message ? error.message : String(error)}`, 0, error && error.code ? error.code : undefined);
|
|
169
229
|
}
|
|
170
|
-
|
|
230
|
+
const body = response !== null && typeof response.json === 'object' ? response.json : null;
|
|
231
|
+
if (response.status < 200 || response.status >= 300) {
|
|
171
232
|
const reason = body !== null && typeof body.error === 'string' ? body.error : `HTTP ${response.status}`;
|
|
172
233
|
throw new BridgeError(reason, response.status);
|
|
173
234
|
}
|
|
@@ -218,7 +279,7 @@ function createClient(options) {
|
|
|
218
279
|
}
|
|
219
280
|
return { ok: true, events };
|
|
220
281
|
} catch (error) {
|
|
221
|
-
return { ok: false, error: error.message, status: error.status };
|
|
282
|
+
return { ok: false, error: error.message, status: error.status, code: error.code };
|
|
222
283
|
}
|
|
223
284
|
},
|
|
224
285
|
/** 把"选中内容 + 问题"投给 DSH 的当前会话。 */
|
|
@@ -253,7 +314,9 @@ module.exports = {
|
|
|
253
314
|
CONFIG_REREAD_MS,
|
|
254
315
|
REQUEST_TIMEOUT_MS,
|
|
255
316
|
BridgeError,
|
|
317
|
+
isBridgeEndpoint,
|
|
256
318
|
defaultExtensionsDir,
|
|
319
|
+
defaultRequest,
|
|
257
320
|
bridgeFile,
|
|
258
321
|
stateFile,
|
|
259
322
|
readBridgeConfig,
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/bridge-ipc.mjs — 编辑器桥的传输层:本机 IPC(Windows 命名管道 / 其它平台 unix socket)。
|
|
3
|
+
*
|
|
4
|
+
* 为什么不走 HTTP(0.3.13 定论,三条都实测过):
|
|
5
|
+
* 1. **desktop 根本没有 HTTP 面**:渲染进程经 Electron IPC 调 `host.fetch()`
|
|
6
|
+
* (`apps/desktop-host/src/index.ts:308` 的 `createSharedFetchHandler('/api')`),
|
|
7
|
+
* 那是**进程内函数调用**,进程外不可达;插件能挂 HTTP 的只有 web profile 的 `webServer`。
|
|
8
|
+
* 2. **`/api` 那条路即使有 webServer 也不行**:Connection 给 `/api` 装了浏览器 cookie fence
|
|
9
|
+
* (`packages/client/connection/src/index.ts:128-134` 的 `requestRejection` → 401),
|
|
10
|
+
* 而桥的客户端是扩展宿主里的 **Node 进程**,永远拿不到浏览器 cookie。
|
|
11
|
+
* 3. 桥的双方本来就是**同一台机器上的两个进程**(扩展宿主 ← 插件 spawn 的 IDE ← 插件),
|
|
12
|
+
* 本机 IPC 比开端口更小:没有网络面、没有 Host/Origin 混淆代理问题。
|
|
13
|
+
*
|
|
14
|
+
* 与 `serve: dsh` 的 IDE 挂载同源:`net`/`http` 的 `listen(path)` + `http.request({socketPath})`
|
|
15
|
+
* 在本机已被验证可用(见 lib/launcher.mjs 的 `--pipe` 与 lib/index.js 的 `healthCheckPipe`)。
|
|
16
|
+
*
|
|
17
|
+
* 安全:令牌校验仍然保留(见 lib/bridge.mjs 的 `bridgeGuard`)。Windows 命名管道用随机后缀
|
|
18
|
+
* (不可猜),POSIX 上 socket 文件 `chmod 0600` 并在关闭时删除。
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { chmodSync, readdirSync, rmSync, statSync } from 'node:fs';
|
|
22
|
+
import { randomBytes } from 'node:crypto';
|
|
23
|
+
import { createServer } from 'node:http';
|
|
24
|
+
import { join, dirname } from 'node:path';
|
|
25
|
+
|
|
26
|
+
/** 命名管道的统一前缀(Windows;`\\.\pipe\` 是本机命名空间,不经过网络栈)。 */
|
|
27
|
+
const WIN_PIPE_PREFIX = '\\\\.\\pipe\\';
|
|
28
|
+
|
|
29
|
+
/** 本机 IPC 端点路径。Windows = 命名管道名;其它平台 = <dataRoot>/bridge-<pid>-<rand>.sock。 */
|
|
30
|
+
export function bridgeEndpointPath(root, pid = process.pid, platform = process.platform) {
|
|
31
|
+
const rand = randomBytes(6).toString('hex');
|
|
32
|
+
if (platform === 'win32') return `${WIN_PIPE_PREFIX}dshcs-bridge-${pid}-${rand}`;
|
|
33
|
+
return join(root, `bridge-${pid}-${rand}.sock`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** 端点是不是本机 IPC(扩展侧与测试共用同一条判定)。 */
|
|
37
|
+
export function isBridgeEndpoint(value, platform = process.platform) {
|
|
38
|
+
if (typeof value !== 'string' || value === '') return false;
|
|
39
|
+
if (platform === 'win32') return value.startsWith(WIN_PIPE_PREFIX);
|
|
40
|
+
return value.startsWith('/') || /^[A-Za-z]:[\\/]/.test(value);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** POSIX 上清掉上一次崩溃留下的 socket 文件(24h 以前的;不碰别人正在用的)。
|
|
44
|
+
* Windows 命名管道由内核回收,不需要清理。 */
|
|
45
|
+
function sweepStaleSockets(root, log) {
|
|
46
|
+
let names;
|
|
47
|
+
try {
|
|
48
|
+
names = readdirSync(root);
|
|
49
|
+
} catch {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const cutoff = Date.now() - 24 * 60 * 60 * 1000;
|
|
53
|
+
for (const name of names) {
|
|
54
|
+
if (!/^bridge-\d+-[0-9a-f]{12}\.sock$/.test(name)) continue;
|
|
55
|
+
const file = join(root, name);
|
|
56
|
+
try {
|
|
57
|
+
if (statSync(file).mtimeMs < cutoff) rmSync(file, { force: true });
|
|
58
|
+
} catch {
|
|
59
|
+
// 清理失败无关正确性(端点带随机后缀,不会撞名)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 起一个只服务桥路由的本机 IPC 监听口。
|
|
66
|
+
*
|
|
67
|
+
* @param {{socketPath: string, handler: (req: import('node:http').IncomingMessage,
|
|
68
|
+
* res: import('node:http').ServerResponse) => unknown, log?: (message: string) => void}} options
|
|
69
|
+
* @returns {Promise<{path: string, close: () => Promise<void>}>} 监听成功后的句柄(失败则 reject)
|
|
70
|
+
*/
|
|
71
|
+
export function startBridgeListener({ socketPath, handler, log = () => {} }) {
|
|
72
|
+
const isWinPipe = socketPath.startsWith(WIN_PIPE_PREFIX);
|
|
73
|
+
if (!isBridgeEndpoint(socketPath)) {
|
|
74
|
+
return Promise.reject(new Error(`非法的桥端点路径:${socketPath}`));
|
|
75
|
+
}
|
|
76
|
+
if (!isWinPipe) {
|
|
77
|
+
sweepStaleSockets(dirname(socketPath), log);
|
|
78
|
+
// unix socket 文件必须先不存在,否则 listen 直接 EADDRINUSE(端点带随机后缀,撞名不可能)
|
|
79
|
+
try { rmSync(socketPath, { force: true }); } catch { /* ignore */ }
|
|
80
|
+
}
|
|
81
|
+
const server = createServer(handler);
|
|
82
|
+
return new Promise((resolve, reject) => {
|
|
83
|
+
const onError = (error) => {
|
|
84
|
+
server.removeListener('listening', onListening);
|
|
85
|
+
reject(error);
|
|
86
|
+
};
|
|
87
|
+
const onListening = () => {
|
|
88
|
+
server.removeListener('error', onError);
|
|
89
|
+
server.on('error', (error) => log(`桥监听异常:${error && error.code ? error.code : error && error.message ? error.message : error}`));
|
|
90
|
+
if (!isWinPipe) {
|
|
91
|
+
try { chmodSync(socketPath, 0o600); } catch { /* 权限收紧失败不影响可用性 */ }
|
|
92
|
+
}
|
|
93
|
+
resolve({
|
|
94
|
+
path: socketPath,
|
|
95
|
+
close: () => new Promise((done) => {
|
|
96
|
+
server.close(() => {
|
|
97
|
+
if (!isWinPipe) {
|
|
98
|
+
try { rmSync(socketPath, { force: true }); } catch { /* ignore */ }
|
|
99
|
+
}
|
|
100
|
+
done();
|
|
101
|
+
});
|
|
102
|
+
}),
|
|
103
|
+
});
|
|
104
|
+
};
|
|
105
|
+
server.once('error', onError);
|
|
106
|
+
server.once('listening', onListening);
|
|
107
|
+
try {
|
|
108
|
+
server.listen(socketPath);
|
|
109
|
+
} catch (error) {
|
|
110
|
+
onError(error);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
}
|
package/lib/bridge.mjs
CHANGED
|
@@ -20,13 +20,14 @@
|
|
|
20
20
|
* - **host → 扩展的"事件"**:同一个轮询的响应体(环形缓冲 + `since` 游标)。
|
|
21
21
|
* 事件只是"去看一眼这个文件"的提示,不是数据。
|
|
22
22
|
*
|
|
23
|
-
* ## 认证(
|
|
23
|
+
* ## 认证(关键:桥绕开 DSH 的浏览器 fence,所以自带令牌)
|
|
24
24
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
25
|
+
* **传输:本机 IPC**(0.3.13 起)—— Windows 命名管道 / 其它平台 unix socket,见 lib/bridge-ipc.mjs。
|
|
26
|
+
* 为什么不走 DSH 的 HTTP 面:`/api` 的 Host/Origin/cookie 校验由 Connection 在分发前做
|
|
27
|
+
* (`packages/client/connection/src/index.ts`:`requestRejection` → 403 不可信 / 401 无 cookie),
|
|
28
|
+
* 而扩展宿主是 Node 进程:`fetch` **不带 Origin**,但它**拿不到浏览器 cookie** → 必然 401;
|
|
29
|
+
* 挂 `webServer` 前缀(0.3.9–0.3.12)则只有 web profile 有 webServer,desktop 根本没有 HTTP 面。
|
|
30
|
+
* 桥两端本来就是同一台机器上的两个进程,所以传输用本机 IPC,鉴权仍走独立令牌:
|
|
30
31
|
*
|
|
31
32
|
* ## 安全不变量(改这个文件之前先读这四条)
|
|
32
33
|
*
|
|
@@ -42,6 +43,7 @@
|
|
|
42
43
|
import { randomBytes, timingSafeEqual } from 'node:crypto';
|
|
43
44
|
import * as fs from 'node:fs';
|
|
44
45
|
import * as path from 'node:path';
|
|
46
|
+
import { isBridgeEndpoint } from './bridge-ipc.mjs';
|
|
45
47
|
|
|
46
48
|
/** 桥端点目录名(位于 extensionsDir 下 —— 扩展必须能读到它)。 */
|
|
47
49
|
export const BRIDGE_DIRNAME = '.dshcs-bridge';
|
|
@@ -49,14 +51,15 @@ export const BRIDGE_DIRNAME = '.dshcs-bridge';
|
|
|
49
51
|
/** 桥配置文件名。 */
|
|
50
52
|
export const BRIDGE_FILENAME = 'bridge.json';
|
|
51
53
|
|
|
52
|
-
/**
|
|
54
|
+
/** 桥路由路径前缀(本机 IPC 上的路径;**不是** URL 前缀)。
|
|
53
55
|
*
|
|
54
56
|
* **故意不放在 `/api` 下(0.3.9 修正)**:Connection 给 `/api` 装了 Host/Origin/cookie fence
|
|
55
57
|
* (`packages/client/connection/src/index.ts`:`requestRejection` → 无 cookie 即 401),而桥的客户端是
|
|
56
58
|
* VS Code 扩展宿主里的一个 Node 进程 —— 它**永远拿不到浏览器 cookie**,请求在到达插件路由之前就被挡掉了。
|
|
57
59
|
* 实测(0.3.7):扩展按 `/api/code-server/bridge/sync` 轮询,请求要么 405(打到 launcher/VS Code)、
|
|
58
60
|
* 要么 401(打到 DSH 的 /api fence),桥从来没有真正同步过。
|
|
59
|
-
*
|
|
61
|
+
* 0.3.9–0.3.12 挂在 DSH 的 webServer 前缀上(只有 web profile 有 webServer);
|
|
62
|
+
* **0.3.13 起走本机 IPC**(Windows 命名管道 / unix socket),web 与 desktop 同一条路 —— 见 lib/bridge-ipc.mjs。 */
|
|
60
63
|
export const BRIDGE_BASE = '/code-server-bridge';
|
|
61
64
|
|
|
62
65
|
/** 事件环形缓冲上限:超出即丢最旧的(事件是提示,不是数据)。 */
|
|
@@ -81,32 +84,23 @@ export function mintBridgeToken() {
|
|
|
81
84
|
return randomBytes(24).toString('base64url');
|
|
82
85
|
}
|
|
83
86
|
|
|
84
|
-
/**
|
|
85
|
-
* 由实际监听地址算桥的基址。
|
|
86
|
-
* @param {string} host 绑定地址(仅回环)
|
|
87
|
-
* @param {number} port 实际端口
|
|
88
|
-
*/
|
|
89
|
-
export function bridgeUrl(host, port) {
|
|
90
|
-
const h = host === '::1' ? '[::1]' : host;
|
|
91
|
-
return `http://${h}:${port}`;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
87
|
/** 桥配置文件路径(`<extensionsDir>/.dshcs-bridge/bridge.json`)。 */
|
|
95
88
|
export function bridgeFile(extensionsDir) {
|
|
96
89
|
return path.join(extensionsDir, BRIDGE_DIRNAME, BRIDGE_FILENAME);
|
|
97
90
|
}
|
|
98
91
|
|
|
99
92
|
/**
|
|
100
|
-
* 原子写桥配置。扩展每 5s 重读一次;需要时(
|
|
93
|
+
* 原子写桥配置。扩展每 5s 重读一次;需要时(端点/令牌变化)由 host 重写。
|
|
101
94
|
* @param {string} extensionsDir 扩展目录
|
|
102
|
-
* @param {{
|
|
95
|
+
* @param {{pipe: string, token: string, pid: number|null, startedAt: number|null}} value
|
|
103
96
|
*/
|
|
104
97
|
export function writeBridgeConfig(extensionsDir, value) {
|
|
105
98
|
const file = bridgeFile(extensionsDir);
|
|
106
99
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
107
100
|
const payload = JSON.stringify({
|
|
108
|
-
version:
|
|
109
|
-
|
|
101
|
+
version: 2,
|
|
102
|
+
/** 本机 IPC 端点(Windows 命名管道名 / 其它平台 unix socket 绝对路径)。0.3.13 起取代 `url`。 */
|
|
103
|
+
pipe: value.pipe,
|
|
110
104
|
token: value.token,
|
|
111
105
|
pid: value.pid,
|
|
112
106
|
startedAt: value.startedAt,
|
|
@@ -134,12 +128,14 @@ export function removeBridgeConfig(extensionsDir) {
|
|
|
134
128
|
}
|
|
135
129
|
}
|
|
136
130
|
|
|
137
|
-
/** 读回桥配置(诊断用;缺失或格式不对返回 null)。
|
|
131
|
+
/** 读回桥配置(诊断用;缺失或格式不对返回 null)。
|
|
132
|
+
* 端点形状与扩展侧同判定(见 lib/bridge-ipc.mjs 的 `isBridgeEndpoint`):
|
|
133
|
+
* Windows 必须是命名管道名,其它平台必须是绝对路径 —— 免得把一个写坏的端点当成有效配置。 */
|
|
138
134
|
export function readBridgeConfig(extensionsDir) {
|
|
139
135
|
try {
|
|
140
136
|
const raw = JSON.parse(fs.readFileSync(bridgeFile(extensionsDir), 'utf8'));
|
|
141
137
|
if (raw === null || typeof raw !== 'object') return null;
|
|
142
|
-
if (
|
|
138
|
+
if (!isBridgeEndpoint(raw.pipe) || !TOKEN_RE.test(String(raw.token))) return null;
|
|
143
139
|
return raw;
|
|
144
140
|
} catch {
|
|
145
141
|
return null;
|
package/lib/index.js
CHANGED
|
@@ -43,7 +43,6 @@ import {
|
|
|
43
43
|
BRIDGE_DIRNAME,
|
|
44
44
|
bodyWithinLimit,
|
|
45
45
|
bridgeGuard,
|
|
46
|
-
bridgeUrl,
|
|
47
46
|
createContextCache,
|
|
48
47
|
createEventRing,
|
|
49
48
|
mintBridgeToken,
|
|
@@ -51,6 +50,7 @@ import {
|
|
|
51
50
|
removeBridgeConfig,
|
|
52
51
|
writeBridgeConfig,
|
|
53
52
|
} from './bridge.mjs';
|
|
53
|
+
import { bridgeEndpointPath, startBridgeListener } from './bridge-ipc.mjs';
|
|
54
54
|
import { registerEditorPrompt, registerEditorTools, setPromptLiveProbe } from './bridge-tools.mjs';
|
|
55
55
|
import { deliverEditorPrompt } from './bridge-session.mjs';
|
|
56
56
|
import { registerBridgeObserver } from './bridge-observe.mjs';
|
|
@@ -589,12 +589,17 @@ export async function apply(ctx, config) {
|
|
|
589
589
|
if (next.editorBridge !== bridgeSetting) {
|
|
590
590
|
bridgeSetting = next.editorBridge;
|
|
591
591
|
console.log(`[code-server] editorBridge updated: ${bridgeSetting}`);
|
|
592
|
-
// 关掉 → 立刻下线(删配置 + 注销工具);开启 →
|
|
592
|
+
// 关掉 → 立刻下线(关监听口 + 删配置 + 注销工具);开启 → 起监听口,IDE 在跑就补一份配置。
|
|
593
593
|
if (bridgeSetting === false) {
|
|
594
594
|
clearBridgeRuntime();
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
595
|
+
void stopBridgeListener();
|
|
596
|
+
} else {
|
|
597
|
+
void ensureBridgeListener().then(() => {
|
|
598
|
+
if (state.status === 'running' && state.pid !== null) {
|
|
599
|
+
bridgeToken ??= mintBridgeToken();
|
|
600
|
+
syncBridgeRuntime({ pid: state.pid, startedAt: state.startedAt });
|
|
601
|
+
}
|
|
602
|
+
});
|
|
598
603
|
}
|
|
599
604
|
}
|
|
600
605
|
}
|
|
@@ -689,27 +694,53 @@ export async function apply(ctx, config) {
|
|
|
689
694
|
|
|
690
695
|
/** 本次启动生成的新桥令牌;null = 本实例没有令牌(adopt 旧实例时会回读 bridge.json)。 */
|
|
691
696
|
let bridgeToken = null;
|
|
692
|
-
/**
|
|
693
|
-
let
|
|
694
|
-
/**
|
|
697
|
+
/** 桥的本机 IPC 监听口(0.3.13)。null = 未起(未启用/起失败)。*/
|
|
698
|
+
let bridgeListener = null;
|
|
699
|
+
/** 监听口启动中的 promise(避免并发起两次)。 */
|
|
700
|
+
let bridgeListenerStarting = null;
|
|
701
|
+
/** "桥监听起不来"只提示一次,避免每次起停都刷屏。 */
|
|
695
702
|
let bridgeUnavailableLogged = false;
|
|
696
703
|
|
|
697
|
-
/**
|
|
704
|
+
/** 桥的传输是**本机 IPC**(Windows 命名管道 / 其它平台 unix socket),不是 HTTP。
|
|
698
705
|
*
|
|
699
|
-
*
|
|
700
|
-
*
|
|
701
|
-
*
|
|
702
|
-
*
|
|
703
|
-
*
|
|
704
|
-
*
|
|
705
|
-
*
|
|
706
|
-
function
|
|
707
|
-
if (
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
706
|
+
* 为什么不走 HTTP(0.3.13 定论,三条都实测过):
|
|
707
|
+
* ① launcher 的 host:port 只服务 workbench,没有任何桥路由(0.3.7 的实测:405);
|
|
708
|
+
* ② `/api` 有 Connection 的浏览器 cookie fence,扩展宿主是 Node 进程、拿不到 cookie(0.3.7–0.3.9);
|
|
709
|
+
* ③ 挂 DSH 的 `webServer` 前缀(0.3.9–0.3.12)只有 **web profile** 有 —— desktop 的渲染进程
|
|
710
|
+
* 经 Electron IPC 调 `host.fetch()`(`apps/desktop-host/src/index.ts:308`),**根本没有 HTTP 面**。
|
|
711
|
+
* 桥的双方本来就是同一台机器上的两个进程,本机 IPC 让 web 与 desktop 走**同一条**路。
|
|
712
|
+
* 详见 lib/bridge-ipc.mjs 顶部。 */
|
|
713
|
+
function ensureBridgeListener() {
|
|
714
|
+
if (bridgeListener !== null) return Promise.resolve(bridgeListener);
|
|
715
|
+
if (bridgeListenerStarting !== null) return bridgeListenerStarting;
|
|
716
|
+
const socketPath = bridgeEndpointPath(bridgeRoot, process.pid);
|
|
717
|
+
bridgeListenerStarting = startBridgeListener({
|
|
718
|
+
socketPath,
|
|
719
|
+
handler: nodeRouteFromFetch((url, method, headers, body) => dispatchBridge(url, method, headers, body)),
|
|
720
|
+
log: (message) => console.warn(`[code-server] ${message}`),
|
|
721
|
+
}).then((handle) => {
|
|
722
|
+
bridgeListener = handle;
|
|
723
|
+
bridgeListenerStarting = null;
|
|
724
|
+
console.log(`[code-server] 编辑器桥监听就绪:${handle.path}(本机 IPC,桥令牌鉴权)`);
|
|
725
|
+
return handle;
|
|
726
|
+
}).catch((error) => {
|
|
727
|
+
bridgeListenerStarting = null;
|
|
728
|
+
if (!bridgeUnavailableLogged) {
|
|
729
|
+
bridgeUnavailableLogged = true;
|
|
730
|
+
console.warn(`[code-server] 编辑器桥监听失败,桥不启用:${error && error.code ? error.code : error && error.message ? error.message : error}`
|
|
731
|
+
+ '(文件打开走信号文件,不受影响)');
|
|
732
|
+
}
|
|
733
|
+
return null;
|
|
734
|
+
});
|
|
735
|
+
return bridgeListenerStarting;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/** 关掉本机 IPC 监听口(禁用桥 / 插件卸载)。 */
|
|
739
|
+
function stopBridgeListener() {
|
|
740
|
+
const handle = bridgeListener;
|
|
741
|
+
bridgeListener = null;
|
|
742
|
+
if (handle === null) return Promise.resolve();
|
|
743
|
+
return handle.close().catch(() => {});
|
|
713
744
|
}
|
|
714
745
|
|
|
715
746
|
/** 桥的四条路由(后缀 → Fetch 风格 handler)。做成函数而非常量:handler 声明在文件后段,
|
|
@@ -723,7 +754,7 @@ export async function apply(ctx, config) {
|
|
|
723
754
|
];
|
|
724
755
|
}
|
|
725
756
|
|
|
726
|
-
/** 把 Fetch 风格 handler
|
|
757
|
+
/** 把 Fetch 风格 handler 适配成本机 IPC 监听口的 Node 路由(req/res ⇄ Request/Response)。 */
|
|
727
758
|
function nodeRouteFromFetch(dispatch) {
|
|
728
759
|
return async (req, res) => {
|
|
729
760
|
try {
|
|
@@ -784,53 +815,44 @@ export async function apply(ctx, config) {
|
|
|
784
815
|
isLive: () => bridgeMeta !== null && !bridgeContext.isStale(),
|
|
785
816
|
});
|
|
786
817
|
|
|
787
|
-
/** 同步桥运行时:写入/更新 bridge.json
|
|
788
|
-
*
|
|
789
|
-
*
|
|
818
|
+
/** 同步桥运行时:写入/更新 bridge.json(端点 = 本机 IPC 路径 + 令牌 + pid)。
|
|
819
|
+
* 端点、令牌、pid 三者任一变化都重写 —— 扩展每 5s 重读,故不需要任何推送。
|
|
820
|
+
* 监听口起不来时**不写配置**(宁可休眠,不可指向死端点),并说明一次。 */
|
|
790
821
|
function syncBridgeRuntime({ pid, startedAt }) {
|
|
791
822
|
if (!bridgeEnabled()) return;
|
|
792
|
-
const
|
|
793
|
-
if (
|
|
823
|
+
const handle = bridgeListener;
|
|
824
|
+
if (handle === null) {
|
|
794
825
|
if (bridgeMeta !== null) {
|
|
795
826
|
bridgeMeta = null;
|
|
796
|
-
try { removeBridgeConfig(bridgeExtensionsDir); } catch { /*
|
|
797
|
-
}
|
|
798
|
-
if (!bridgeUnavailableLogged) {
|
|
799
|
-
bridgeUnavailableLogged = true;
|
|
800
|
-
console.warn('[code-server] 编辑器桥:本部署没有扩展可达的 HTTP 面(desktop 无 webServer)→ 桥不启用'
|
|
801
|
-
+ '(文件打开走信号文件,不受影响)');
|
|
827
|
+
try { removeBridgeConfig(bridgeExtensionsDir); } catch { /* 删不掉也不影响:扩展会因端点在而连不上 */ }
|
|
802
828
|
}
|
|
803
829
|
return;
|
|
804
830
|
}
|
|
805
831
|
bridgeToken ??= mintBridgeToken();
|
|
806
|
-
const changed = bridgeMeta === null || bridgeMeta.
|
|
807
|
-
bridgeMeta = {
|
|
832
|
+
const changed = bridgeMeta === null || bridgeMeta.pipe !== handle.path || bridgeMeta.token !== bridgeToken || bridgeMeta.pid !== pid;
|
|
833
|
+
bridgeMeta = { pipe: handle.path, token: bridgeToken, pid, startedAt: startedAt ?? null };
|
|
808
834
|
try {
|
|
809
|
-
writeBridgeConfig(bridgeExtensionsDir, {
|
|
835
|
+
writeBridgeConfig(bridgeExtensionsDir, { pipe: handle.path, token: bridgeToken, pid, startedAt: startedAt ?? null });
|
|
810
836
|
} catch (err) {
|
|
811
837
|
console.warn(`[code-server] 编辑器桥配置写入失败(${bridgeExtensionsDir}):${err && err.message ? err.message : err}`);
|
|
812
838
|
return;
|
|
813
839
|
}
|
|
814
840
|
if (changed) {
|
|
815
|
-
//
|
|
816
|
-
console.log(`[code-server] 编辑器桥:已启用(${
|
|
841
|
+
// 只打印端点与配置文件位置,不打印令牌本身(与 path-token 同一决策)。
|
|
842
|
+
console.log(`[code-server] 编辑器桥:已启用(${handle.path}${BRIDGE_BASE}/*,令牌文件 ${path.join(bridgeMetaDir, 'bridge.json')})`);
|
|
817
843
|
}
|
|
818
844
|
ensureBridgeTools();
|
|
819
845
|
}
|
|
820
846
|
|
|
821
847
|
/**
|
|
822
|
-
*
|
|
848
|
+
* 接管实例时对齐桥令牌:磁盘上已有配置、端点一致且是本机 IPC → 沿用(避免无谓轮换打乱正在运行的扩展);
|
|
823
849
|
* 否则 mint 新的(扩展下次轮询就会读到新的 bridge.json,一次请求的失败无所谓)。
|
|
824
850
|
*/
|
|
825
851
|
function adoptBridgeRuntime(pid, startedAt) {
|
|
826
852
|
if (!bridgeEnabled()) return;
|
|
827
|
-
const base = bridgeOrigin();
|
|
828
|
-
if (base === null) {
|
|
829
|
-
syncBridgeRuntime({ pid, startedAt });
|
|
830
|
-
return;
|
|
831
|
-
}
|
|
832
853
|
const existing = readBridgeConfig(bridgeExtensionsDir);
|
|
833
|
-
|
|
854
|
+
const endpoint = bridgeListener === null ? null : bridgeListener.path;
|
|
855
|
+
bridgeToken = existing !== null && endpoint !== null && existing.pipe === endpoint && TOKEN_RE.test(String(existing.token))
|
|
834
856
|
? String(existing.token)
|
|
835
857
|
: mintBridgeToken();
|
|
836
858
|
syncBridgeRuntime({ pid, startedAt });
|
|
@@ -860,7 +882,7 @@ export async function apply(ctx, config) {
|
|
|
860
882
|
return null;
|
|
861
883
|
}
|
|
862
884
|
|
|
863
|
-
/** 桥就绪时注册编辑器工具;`tools` / `defineTool` 缺失时静默退化为"
|
|
885
|
+
/** 桥就绪时注册编辑器工具;`tools` / `defineTool` 缺失时静默退化为"只有本机 IPC 面"。 */
|
|
864
886
|
function ensureBridgeTools() {
|
|
865
887
|
if (bridgeMeta === null || bridgeToolDispose !== null) return;
|
|
866
888
|
Promise.resolve(registerEditorTools(ctx, {
|
|
@@ -869,7 +891,7 @@ export async function apply(ctx, config) {
|
|
|
869
891
|
}))
|
|
870
892
|
.then((dispose) => {
|
|
871
893
|
if (dispose === null) {
|
|
872
|
-
console.log('[code-server]
|
|
894
|
+
console.log('[code-server] 编辑器桥:工具服务不可用,仅提供本机 IPC 面(editor_context/editor_diagnostics 未注册)');
|
|
873
895
|
return;
|
|
874
896
|
}
|
|
875
897
|
// 期间桥可能已经被停掉(IDE 退出):立刻回滚,避免留下永远不可用的工具。
|
|
@@ -903,19 +925,6 @@ export async function apply(ctx, config) {
|
|
|
903
925
|
dshMount = null;
|
|
904
926
|
console.error(`[code-server] serve=dsh 挂载失败(将回退 loopback):${error && error.message ? error.message : error}`);
|
|
905
927
|
}
|
|
906
|
-
// 编辑器桥(0.3.9):挂在自己的前缀上,自带令牌 —— 走 /api 会被 Connection 的 cookie fence 401
|
|
907
|
-
// (扩展宿主是 Node 进程,没有浏览器 cookie),详见 bridgeOrigin() 的注释。
|
|
908
|
-
try {
|
|
909
|
-
bridgeMountDispose = wsCtx.webServer.register({
|
|
910
|
-
kind: 'prefix',
|
|
911
|
-
path: BRIDGE_BASE,
|
|
912
|
-
handler: nodeRouteFromFetch((url, method, headers, body) => dispatchBridge(url, method, headers, body)),
|
|
913
|
-
});
|
|
914
|
-
console.log(`[code-server] 编辑器桥挂载就绪:${BRIDGE_BASE}/* (DSH webServer,桥令牌鉴权)`);
|
|
915
|
-
} catch (error) {
|
|
916
|
-
bridgeMountDispose = null;
|
|
917
|
-
console.warn(`[code-server] 编辑器桥挂载失败(桥不可用):${error && error.message ? error.message : error}`);
|
|
918
|
-
}
|
|
919
928
|
});
|
|
920
929
|
|
|
921
930
|
ctx.effect(() => () => {
|
|
@@ -925,11 +934,11 @@ export async function apply(ctx, config) {
|
|
|
925
934
|
}
|
|
926
935
|
}, 'code-server: dsh mount');
|
|
927
936
|
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
}, 'code-server: bridge
|
|
937
|
+
// 编辑器桥的本机 IPC 监听口(web 与 desktop 同一套;不依赖 webServer,见 ensureBridgeListener)。
|
|
938
|
+
ctx.effect(() => {
|
|
939
|
+
if (bridgeEnabled()) void ensureBridgeListener();
|
|
940
|
+
return () => { void stopBridgeListener(); };
|
|
941
|
+
}, 'code-server: bridge ipc listener');
|
|
933
942
|
|
|
934
943
|
/** 回环模式的客户端 URL:随机端口 + 路径令牌(令牌是 URL 路径的一段,浏览器会把子请求与 WS
|
|
935
944
|
* 一并带过去 —— 这正是不走 VS Code 自带 cookie 令牌的原因,见 launcher 文件头"安全模型")。 */
|
|
@@ -968,8 +977,10 @@ export async function apply(ctx, config) {
|
|
|
968
977
|
enabled: bridgeEnabled(),
|
|
969
978
|
live: bridgeMeta !== null,
|
|
970
979
|
toolsRegistered: bridgeToolDispose !== null,
|
|
971
|
-
|
|
972
|
-
|
|
980
|
+
/** 端点已就绪(本机 IPC 监听口在)。0.3.13 起 web 与 desktop 都是 true(不再依赖 webServer)。 */
|
|
981
|
+
supported: bridgeListener !== null,
|
|
982
|
+
/** 本机 IPC 端点(命名管道 / unix socket);null = 监听口没起来。 */
|
|
983
|
+
endpoint: bridgeListener === null ? null : bridgeListener.path,
|
|
973
984
|
file: path.join(bridgeMetaDir, 'bridge.json'),
|
|
974
985
|
},
|
|
975
986
|
env: state.env,
|
|
@@ -1539,11 +1550,13 @@ export async function apply(ctx, config) {
|
|
|
1539
1550
|
|
|
1540
1551
|
async function handleBridgeHealth() {
|
|
1541
1552
|
// 无鉴权:只回一句"桥活着吗",不含任何编辑器数据(便于重启后一眼确认)。
|
|
1553
|
+
// 注意:bridge=true 只表示"配置已就绪",**不代表扩展在跑**(见 README 已知限制)。
|
|
1542
1554
|
return jsonResponse({
|
|
1543
1555
|
ok: true,
|
|
1544
1556
|
bridge: bridgeMeta !== null,
|
|
1545
1557
|
pid: state.pid,
|
|
1546
|
-
|
|
1558
|
+
transport: 'ipc',
|
|
1559
|
+
endpoint: bridgeMeta === null ? null : bridgeMeta.pipe,
|
|
1547
1560
|
});
|
|
1548
1561
|
}
|
|
1549
1562
|
|
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.13",
|
|
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": {
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
"lib/client.js",
|
|
40
40
|
"lib/claim-types.js",
|
|
41
41
|
"lib/bridge.mjs",
|
|
42
|
+
"lib/bridge-ipc.mjs",
|
|
42
43
|
"lib/bridge-tools.mjs",
|
|
43
44
|
"lib/bridge-session.mjs",
|
|
44
45
|
"lib/bridge-observe.mjs",
|
package/vendor/VENDOR.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"vscodeVersion": "1.137.0",
|
|
4
4
|
"productPath": "stable-b11dabdaca0d3369986975be285db92c8795cea5",
|
|
5
5
|
"layout": "vscode-only",
|
|
6
|
-
"preparedAt": "2026-09-
|
|
6
|
+
"preparedAt": "2026-09-13T05:03:27.361Z",
|
|
7
7
|
"source": "registry",
|
|
8
8
|
"node": "v24.21.0",
|
|
9
9
|
"platform": "win32",
|