dsh-code-server-app 0.3.6 → 0.3.8
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/assets/extensions/dshcs-editor-bridge/extension.js +3 -1
- package/assets/extensions/dshcs-editor-bridge/lib/bridge-client.js +8 -4
- package/lib/bridge-session.mjs +19 -1
- package/lib/bridge-tools.mjs +53 -9
- package/lib/bridge.mjs +19 -7
- package/lib/index.js +186 -37
- package/package.json +2 -1
- package/vendor/VENDOR.json +2 -2
|
@@ -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.8 修正):/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.8 修正)。 */
|
|
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-session.mjs
CHANGED
|
@@ -18,6 +18,24 @@ import { loadDshExport } from './dsh-resolve.mjs';
|
|
|
18
18
|
/** 消息来源标记:在会话日志里能一眼看出这条来自编辑器桥。 */
|
|
19
19
|
export const SOURCE_PLUGIN = 'dsh-code-server-app:editor-bridge';
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* 取一个 DSH 服务。
|
|
23
|
+
*
|
|
24
|
+
* **只能走 `ctx.get()`,不能走属性访问**:`ctx.agents` 这类属性访问在服务"存在但对该 fiber
|
|
25
|
+
* 不可达"时会抛 `cannot get property "agents" without inject`;这条路径跑在路由回调里,
|
|
26
|
+
* 抛了就变成 500,连"没有可用会话"的 409 提示都给不出来。
|
|
27
|
+
* (0.3.6 的线上事故同源:`bridge-tools.mjs` 里 `ctx.systemPrompt` 的属性访问让整棵插件树
|
|
28
|
+
* 加载失败、dsh web 起不来。)
|
|
29
|
+
*/
|
|
30
|
+
function getService(ctx, name) {
|
|
31
|
+
if (ctx === undefined || ctx === null || typeof ctx.get !== 'function') return undefined;
|
|
32
|
+
try {
|
|
33
|
+
return ctx.get(name);
|
|
34
|
+
} catch {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
21
39
|
/** 拼进消息的选区文本上限(超长时截断并注明)。 */
|
|
22
40
|
export const MAX_SELECTION_CHARS = 8000;
|
|
23
41
|
|
|
@@ -58,7 +76,7 @@ export function composeEditorPrompt(input) {
|
|
|
58
76
|
* @param {object} ctx cordis 上下文
|
|
59
77
|
*/
|
|
60
78
|
export function pickAgent(ctx) {
|
|
61
|
-
const agents =
|
|
79
|
+
const agents = getService(ctx, 'agents');
|
|
62
80
|
if (agents === undefined || agents === null) return null;
|
|
63
81
|
try {
|
|
64
82
|
if (typeof agents.currentInitiator === 'function') {
|
package/lib/bridge-tools.mjs
CHANGED
|
@@ -69,6 +69,38 @@ async function loadDefineTool() {
|
|
|
69
69
|
return loadDshExport('@deepseek-ai/dsh-tools', 'defineTool');
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
/**
|
|
73
|
+
* 取一个 DSH 服务。
|
|
74
|
+
*
|
|
75
|
+
* **必须走 `ctx.get()`,不能走属性访问**(0.3.6 的真实线上事故:用 `ctx.systemPrompt`
|
|
76
|
+
* 让整棵插件树加载失败、dsh web 直接起不来)。
|
|
77
|
+
*
|
|
78
|
+
* 区别是确定的(cordis `src/reflect.ts`):
|
|
79
|
+
* - `ctx.get(name)` → `ReflectService.get()` → `_getImpl()`:服务没提供就返回 `undefined`,
|
|
80
|
+
* **永不抛**;
|
|
81
|
+
* - `ctx.tools` 这类属性访问 → 走代理的 get trap,服务"存在但对该 fiber 不可达"时
|
|
82
|
+
* 依次尝试 `internal/get` 瀑布 / `props[prop].get` / `reflect.get(prop,false)`,
|
|
83
|
+
* 任一失败都会抛 `cannot get property "x" without inject`
|
|
84
|
+
* —— 而那是在 `apply()` 里,loader 会因此判定 `failed to apply loader entry` 并终止整个 profile。
|
|
85
|
+
*
|
|
86
|
+
* 所以:`ctx.get()` + 判空 = 可选服务的正确姿势;属性访问只对**已声明 inject** 的服务安全。
|
|
87
|
+
*/
|
|
88
|
+
function getService(ctx, name) {
|
|
89
|
+
if (ctx === undefined || ctx === null || typeof ctx.get !== 'function') return undefined;
|
|
90
|
+
try {
|
|
91
|
+
return ctx.get(name);
|
|
92
|
+
} catch {
|
|
93
|
+
return undefined; // 连 get 都抛(上下文形态异常)时,退化为"没有这个服务"
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** 取一个服务上的方法,绑定好 this(避免调用时丢上下文)。 */
|
|
98
|
+
function getServiceMethod(ctx, serviceName, methodName) {
|
|
99
|
+
const service = getService(ctx, serviceName);
|
|
100
|
+
if (service === undefined || service === null || typeof service[methodName] !== 'function') return null;
|
|
101
|
+
return service[methodName].bind(service);
|
|
102
|
+
}
|
|
103
|
+
|
|
72
104
|
// ---------------------------------------------------------------- 工具值投影
|
|
73
105
|
|
|
74
106
|
/**
|
|
@@ -201,10 +233,11 @@ function bridgeLive(deps) {
|
|
|
201
233
|
* (见 lib/bridge.mjs 的 createContextCache —— 扩展在每次 /sync 里刷新它)。
|
|
202
234
|
*/
|
|
203
235
|
export async function registerEditorTools(ctx, deps) {
|
|
204
|
-
const tools =
|
|
236
|
+
const tools = getService(ctx, 'tools');
|
|
205
237
|
if (tools === undefined || tools === null || typeof tools.register !== 'function') return null;
|
|
206
238
|
const defineTool = await loadDefineTool();
|
|
207
239
|
if (defineTool === null) return null;
|
|
240
|
+
const register = tools.register.bind(tools);
|
|
208
241
|
|
|
209
242
|
const contextTool = defineTool({
|
|
210
243
|
name: EDITOR_CONTEXT_TOOL,
|
|
@@ -281,7 +314,7 @@ export async function registerEditorTools(ctx, deps) {
|
|
|
281
314
|
}),
|
|
282
315
|
});
|
|
283
316
|
|
|
284
|
-
const disposers = [
|
|
317
|
+
const disposers = [register(contextTool), register(diagnosticsTool)];
|
|
285
318
|
return () => {
|
|
286
319
|
for (const dispose of disposers) {
|
|
287
320
|
try {
|
|
@@ -317,14 +350,25 @@ function bridgeIsLive() {
|
|
|
317
350
|
|
|
318
351
|
/**
|
|
319
352
|
* 注册系统提示词段落,返回 disposer(或 null = 该 DSH 没有 systemPrompt 服务)。
|
|
353
|
+
*
|
|
354
|
+
* **这里就是 0.3.6 线上事故的位置**:原实现写的是 `ctx?.systemPrompt ?? ctx.get(...)`,
|
|
355
|
+
* 而属性访问会抛(见 `getService` 的说明)—— 可选链只挡 null/undefined,挡不住抛错,
|
|
356
|
+
* 于是 `??` 右边的 `ctx.get()` 永远没机会执行,整个 profile 加载失败。
|
|
357
|
+
* 现在只走 `ctx.get()`,并且对 `section` 调用本身也加保护。
|
|
358
|
+
*
|
|
320
359
|
* @param {object} ctx cordis 上下文
|
|
321
360
|
*/
|
|
322
361
|
export function registerEditorPrompt(ctx) {
|
|
323
|
-
const
|
|
324
|
-
if (
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
362
|
+
const section = getServiceMethod(ctx, 'systemPrompt', 'section');
|
|
363
|
+
if (section === null) return null;
|
|
364
|
+
try {
|
|
365
|
+
return section({
|
|
366
|
+
name: PROMPT_SECTION,
|
|
367
|
+
order: PROMPT_ORDER,
|
|
368
|
+
text: () => (bridgeIsLive() ? PROMPT_TEXT : ''),
|
|
369
|
+
});
|
|
370
|
+
} catch (error) {
|
|
371
|
+
console.warn(`[code-server] 编辑器桥:提示词段落注册失败(不影响其余能力):${error && error.message ? error.message : error}`);
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
330
374
|
}
|
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.8 修正)**: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.8)。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.8):挂在自己的前缀上,自带令牌 —— 走 /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.8 修正)----
|
|
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.8",
|
|
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:19:04.694Z",
|
|
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
|