dsh-code-server-app 0.2.13 → 0.3.6
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 +122 -4
- package/README.md +130 -8
- package/assets/extensions/dshcs-editor-bridge/extension.js +539 -0
- package/assets/extensions/dshcs-editor-bridge/lib/bridge-client.js +241 -0
- package/assets/extensions/dshcs-editor-bridge/lib/context-model.js +204 -0
- package/assets/extensions/dshcs-editor-bridge/lib/diff-model.js +123 -0
- package/assets/extensions/dshcs-editor-bridge/package.json +58 -0
- package/cordis.patch.yml +16 -2
- package/lib/bridge-observe.mjs +184 -0
- package/lib/bridge-session.mjs +124 -0
- package/lib/bridge-tools.mjs +330 -0
- package/lib/bridge.mjs +325 -0
- package/lib/client.js +1 -1
- package/lib/dsh-resolve.mjs +91 -0
- package/lib/index.js +472 -64
- package/lib/launcher.mjs +120 -4
- package/package.json +11 -3
- package/vendor/VENDOR.json +1 -1
package/lib/index.js
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import { spawn, execFile, execFileSync } from 'node:child_process';
|
|
20
|
+
import { randomBytes } from 'node:crypto';
|
|
20
21
|
import * as fs from 'node:fs';
|
|
21
22
|
import * as path from 'node:path';
|
|
22
23
|
import * as os from 'node:os';
|
|
@@ -37,6 +38,21 @@ import {
|
|
|
37
38
|
import { aliasNodePathDirs, ensureRuntimeLayout, resolveRuntime, runtimePackageName, verifyNatives } from './native.js';
|
|
38
39
|
import { MOUNT_PATH, mountOnWebServer } from './serve-dsh.mjs';
|
|
39
40
|
import { DEFAULT_CLAIM_EXTENSIONS, describeClaimPolicy, normalizeClaimExtensions } from './claim-types.js';
|
|
41
|
+
import {
|
|
42
|
+
BRIDGE_DIRNAME,
|
|
43
|
+
bodyWithinLimit,
|
|
44
|
+
bridgeGuard,
|
|
45
|
+
bridgeUrl,
|
|
46
|
+
createContextCache,
|
|
47
|
+
createEventRing,
|
|
48
|
+
mintBridgeToken,
|
|
49
|
+
readBridgeConfig,
|
|
50
|
+
removeBridgeConfig,
|
|
51
|
+
writeBridgeConfig,
|
|
52
|
+
} from './bridge.mjs';
|
|
53
|
+
import { registerEditorPrompt, registerEditorTools, setPromptLiveProbe } from './bridge-tools.mjs';
|
|
54
|
+
import { deliverEditorPrompt } from './bridge-session.mjs';
|
|
55
|
+
import { registerBridgeObserver } from './bridge-observe.mjs';
|
|
40
56
|
|
|
41
57
|
// schemastery 由 DSH 部署自带(官方核心依赖),仿 auto-open-web 的解析策略:
|
|
42
58
|
// 常规 import 优先,不可用时回退到全局 npm 布局的 DSH 部署副本。
|
|
@@ -87,22 +103,33 @@ export const Config = z.object({
|
|
|
87
103
|
* 客户端把右侧栏切到全屏(铺满窗口);false 则保持 DSH 默认的 push(与对话并排)。
|
|
88
104
|
* 只影响"打开那一刻":之后用户点「退出全屏」不会被抢回去。 */
|
|
89
105
|
fullscreenOnOpen: z.boolean().default(true),
|
|
106
|
+
/** editorBridge=true(默认):启用「编辑器桥」(0.3.0)—— 树内扩展 dshcs-editor-bridge 与
|
|
107
|
+
* host 之间建立**只读**通道,给 agent 提供 only-the-editor-knows 的上下文(未保存缓冲区、
|
|
108
|
+
* 诊断、活动选区),并让编辑器里的动作能驱动 DSH。关闭后不写 bridge.json、不注册编辑器工具,
|
|
109
|
+
* 扩展会休眠(它读不到配置就不做任何事)。安全模型见 lib/bridge.mjs 顶部。 */
|
|
110
|
+
editorBridge: z.boolean().default(true),
|
|
90
111
|
});
|
|
91
112
|
|
|
92
113
|
const DEFAULT_CONFIG = {
|
|
93
114
|
bin: '',
|
|
94
115
|
host: '127.0.0.1',
|
|
95
|
-
|
|
116
|
+
/** 0(默认)= 每次启动由系统分配空闲端口(launcher 把实际端口写进 endpoint 文件,host 读回);
|
|
117
|
+
* 显式给端口则照用(用于需要固定地址的场景)。 */
|
|
118
|
+
port: 0,
|
|
96
119
|
auth: 'none',
|
|
97
120
|
serve: 'loopback',
|
|
98
121
|
userDataDir: '',
|
|
99
122
|
extensionsDir: '',
|
|
100
123
|
locale: '',
|
|
101
124
|
readyTimeoutMs: 60000,
|
|
125
|
+
/** 编辑器桥(0.3.0):见 Config.editorBridge。行配置与设置文档都可关;设置卡片暂不提供行。 */
|
|
126
|
+
editorBridge: true,
|
|
102
127
|
};
|
|
103
128
|
|
|
104
129
|
const LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '::1']);
|
|
105
130
|
const LOG_TAIL_MAX = 6000;
|
|
131
|
+
/** 路径令牌格式(与 launcher 的一致;字符集同时满足 VS Code 自己的 token 校验)。 */
|
|
132
|
+
const TOKEN_RE = /^[0-9A-Za-z_-]{16,128}$/;
|
|
106
133
|
|
|
107
134
|
function dshHome() {
|
|
108
135
|
return process.env.DSH_HOME || path.join(os.homedir(), '.dsh');
|
|
@@ -116,6 +143,16 @@ function pidFile(config) {
|
|
|
116
143
|
return path.join(dataRoot(config), 'pid.json');
|
|
117
144
|
}
|
|
118
145
|
|
|
146
|
+
/** 路径令牌文件(0600 语义:位于用户 profile 下,默认 ACL 仅本人可读)。 */
|
|
147
|
+
function tokenFile(config) {
|
|
148
|
+
return path.join(dataRoot(config), 'path-token');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** launcher 回写实际监听地址的文件(端口 0 时 host 只能从这里知道端口)。 */
|
|
152
|
+
function endpointFile(config) {
|
|
153
|
+
return path.join(dataRoot(config), 'endpoint.json');
|
|
154
|
+
}
|
|
155
|
+
|
|
119
156
|
function isAlive(pid) {
|
|
120
157
|
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
121
158
|
try {
|
|
@@ -144,56 +181,72 @@ function launcherPath() {
|
|
|
144
181
|
return path.join(path.dirname(fileURLToPath(import.meta.url)), 'launcher.mjs');
|
|
145
182
|
}
|
|
146
183
|
|
|
147
|
-
/**
|
|
148
|
-
* (位于程序内置目录的扩展被 VS Code 视为内置——用户视图显示为"内置"
|
|
149
|
-
*
|
|
150
|
-
* 返回 { dst, builtin }——builtin=true
|
|
151
|
-
function extensionTarget(extensionsDir) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
184
|
+
/** 扩展安装目标:`placement: 'builtin'` 时优先 VS Code「内置扩展」目录 = <树>/lib/vscode/extensions
|
|
185
|
+
* (位于程序内置目录的扩展被 VS Code 视为内置——用户视图显示为"内置",**不能卸载**);
|
|
186
|
+
* `placement: 'user'` 时只用 --extensions-dir(用户级扩展,可被用户禁用/卸载)。
|
|
187
|
+
* 返回 { dst, builtin }——builtin=true 时是核心路径;找不到树时回退用户级。 */
|
|
188
|
+
function extensionTarget(extensionsDir, name, placement = 'builtin') {
|
|
189
|
+
if (placement === 'builtin') {
|
|
190
|
+
try {
|
|
191
|
+
const root = vsRoot();
|
|
192
|
+
if (root !== null) {
|
|
193
|
+
const vscodeExt = path.join(root, 'lib', 'vscode', 'extensions');
|
|
194
|
+
if (fs.existsSync(vscodeExt)) {
|
|
195
|
+
return { dst: path.join(vscodeExt, name), builtin: true };
|
|
196
|
+
}
|
|
158
197
|
}
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
return { dst: path.join(extensionsDir,
|
|
198
|
+
} catch { /* fall through */ }
|
|
199
|
+
}
|
|
200
|
+
return { dst: path.join(extensionsDir, name), builtin: false };
|
|
162
201
|
}
|
|
163
202
|
|
|
164
|
-
/**
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
203
|
+
/** 树内自带扩展清单。
|
|
204
|
+
* - dshcs-open-file:host 信号文件 → VS Code 打开文件(可带行号)。**内置**(用户不需要看到它)。
|
|
205
|
+
* - dshcs-editor-bridge(0.3.0):编辑器桥的扩展侧。**必须用户级** —— 它是可选能力,
|
|
206
|
+
* 用户得有办法一键禁用;内置扩展按 VS Code 语义不能禁用。代价是用户可能卸载它,
|
|
207
|
+
* 这没关系:host 侧本来就按"桥不可用"降级。 */
|
|
208
|
+
const BUNDLED_EXTENSIONS = [
|
|
209
|
+
{ name: 'dshcs-open-file', placement: 'builtin' },
|
|
210
|
+
{ name: 'dshcs-editor-bridge', placement: 'user' },
|
|
211
|
+
];
|
|
212
|
+
|
|
213
|
+
/** 内置扩展安装:每次启动调用 —— 缺失**或内容有变化**即同步(自愈,且插件升级后能更新已装的旧副本)。
|
|
214
|
+
* 同时清理"放错位置"的旧副本(用户级/内置两份同时存在会让 VS Code 打架)。 */
|
|
215
|
+
function installBundledExtensions(extensionsDir, userDataDir) {
|
|
216
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
217
|
+
for (const ext of BUNDLED_EXTENSIONS) {
|
|
218
|
+
try {
|
|
219
|
+
const src = path.join(here, '..', 'assets', 'extensions', ext.name);
|
|
220
|
+
const manifest = path.join(src, 'package.json');
|
|
221
|
+
if (!fs.existsSync(manifest)) continue;
|
|
222
|
+
const files = ['package.json', 'extension.js'];
|
|
223
|
+
const target = extensionTarget(extensionsDir, ext.name, ext.placement);
|
|
224
|
+
const dst = target.dst;
|
|
225
|
+
const stale = files.filter((name) => {
|
|
226
|
+
const to = path.join(dst, name);
|
|
227
|
+
if (!fs.existsSync(to)) return true;
|
|
228
|
+
try {
|
|
229
|
+
return fs.readFileSync(path.join(src, name), 'utf8') !== fs.readFileSync(to, 'utf8');
|
|
230
|
+
} catch {
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
if (stale.length > 0) {
|
|
235
|
+
fs.mkdirSync(dst, { recursive: true });
|
|
236
|
+
for (const name of stale) fs.copyFileSync(path.join(src, name), path.join(dst, name));
|
|
182
237
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
238
|
+
// 清理放错位置的副本:dshcs-open-file 的旧用户级副本 / 编辑器桥的错误内置副本。
|
|
239
|
+
const wrong = extensionTarget(extensionsDir, ext.name, ext.placement === 'builtin' ? 'user' : 'builtin');
|
|
240
|
+
if (wrong.dst !== dst && fs.existsSync(wrong.dst)) {
|
|
241
|
+
fs.rmSync(wrong.dst, { recursive: true, force: true });
|
|
242
|
+
console.log(`[code-server] 清理放错位置的扩展副本 ${wrong.dst}(${ext.name} 应装在${target.builtin ? '内置' : '用户级'}目录)`);
|
|
243
|
+
}
|
|
244
|
+
console.log(`[code-server] bundled extension ${ext.name} -> ${dst}${target.builtin ? ' (内置,不可卸载)' : ' (用户级,可禁用)'}${stale.length > 0 ? ` [更新 ${stale.join(',')}]` : ''}`);
|
|
245
|
+
} catch (err) {
|
|
246
|
+
console.warn(`[code-server] bundled extension ${ext.name} install failed:`, err && err.message ? err.message : String(err));
|
|
192
247
|
}
|
|
193
|
-
console.log(`[code-server] bundled extension dshcs-open-file -> ${dst}${target.builtin ? ' (内置,不可卸载)' : ' (用户级回退)'}${stale.length > 0 ? ` [更新 ${stale.join(',')}]` : ''}`);
|
|
194
|
-
} catch (err) {
|
|
195
|
-
console.warn('[code-server] bundled extension install failed:', err && err.message ? err.message : String(err));
|
|
196
248
|
}
|
|
249
|
+
void userDataDir;
|
|
197
250
|
}
|
|
198
251
|
|
|
199
252
|
/** 打开文件信号文件:<user-data>/User/dshcs-open.json(扩展轮询此文件)。 */
|
|
@@ -315,9 +368,10 @@ function pipeName() {
|
|
|
315
368
|
return win32() ? `\\\\.\\pipe\\dshcs-vscode-${key}` : path.join(os.tmpdir(), `dshcs-vscode-${key}.sock`);
|
|
316
369
|
}
|
|
317
370
|
|
|
318
|
-
function healthCheck(host, port, timeoutMs = 1500) {
|
|
371
|
+
function healthCheck(host, port, token, timeoutMs = 1500) {
|
|
372
|
+
const requestPath = token === null || token === undefined ? '/healthz' : `/${token}/healthz`;
|
|
319
373
|
return new Promise((resolve) => {
|
|
320
|
-
const req = http.get({ host, port, path:
|
|
374
|
+
const req = http.get({ host, port, path: requestPath, timeout: timeoutMs, method: 'GET' }, (res) => {
|
|
321
375
|
res.resume();
|
|
322
376
|
resolve({ ok: res.statusCode >= 200 && res.statusCode < 500, statusCode: res.statusCode });
|
|
323
377
|
});
|
|
@@ -367,6 +421,37 @@ function removePidFile(config) {
|
|
|
367
421
|
}
|
|
368
422
|
}
|
|
369
423
|
|
|
424
|
+
/** 读路径令牌;文件缺失或格式不对返回 null(不抛:调用方据此判定"没有可接管的实例")。 */
|
|
425
|
+
function readToken(config) {
|
|
426
|
+
try {
|
|
427
|
+
const raw = fs.readFileSync(tokenFile(config), 'utf8').trim();
|
|
428
|
+
return TOKEN_RE.test(raw) ? raw : null;
|
|
429
|
+
} catch {
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** 生成并写入新的路径令牌(每次**新启动**都换;原子写,0600)。 */
|
|
435
|
+
function rotateToken(config) {
|
|
436
|
+
const token = randomBytes(24).toString('base64url'); // [A-Za-z0-9_-] 共 32 位
|
|
437
|
+
const file = tokenFile(config);
|
|
438
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
439
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
440
|
+
fs.writeFileSync(tmp, token, { encoding: 'utf8', mode: 0o600 });
|
|
441
|
+
fs.renameSync(tmp, file);
|
|
442
|
+
return token;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/** 读 launcher 回写的监听端点(端口 0 时唯一可信来源);缺失/不可用返回 null。 */
|
|
446
|
+
function readEndpoint(config) {
|
|
447
|
+
try {
|
|
448
|
+
const raw = JSON.parse(fs.readFileSync(endpointFile(config), 'utf8'));
|
|
449
|
+
return raw && Number.isInteger(raw.pid) && Number.isInteger(raw.port) && raw.port > 0 ? raw : null;
|
|
450
|
+
} catch {
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
370
455
|
export async function apply(ctx, config) {
|
|
371
456
|
const cfg = { ...DEFAULT_CONFIG, ...(config ?? {}) };
|
|
372
457
|
|
|
@@ -382,6 +467,8 @@ export async function apply(ctx, config) {
|
|
|
382
467
|
let keepResident = true; // 客户端常驻预热(0.2.2)
|
|
383
468
|
let claimExtensions = DEFAULT_CLAIM_EXTENSIONS; // 认领类型清单(0.2.11,取代 fileOpenScope)
|
|
384
469
|
let fullscreenOnOpen = true; // 客户端"打开标签即全屏右侧栏"(0.2.9)
|
|
470
|
+
/** 编辑器桥开关(0.3.0):行配置 cfg.editorBridge 是种子,设置文档里可改并实时生效。 */
|
|
471
|
+
let bridgeSetting = cfg.editorBridge !== false;
|
|
385
472
|
if (settingsSvc !== undefined && typeof settingsSvc.register === 'function') {
|
|
386
473
|
try {
|
|
387
474
|
const scope = settingsSvc.register(SETTINGS_NS, Config);
|
|
@@ -394,6 +481,7 @@ export async function apply(ctx, config) {
|
|
|
394
481
|
? normalizeClaimExtensions(resolved.claimExtensions)
|
|
395
482
|
: DEFAULT_CLAIM_EXTENSIONS;
|
|
396
483
|
fullscreenOnOpen = resolved && typeof resolved.fullscreenOnOpen === 'boolean' ? resolved.fullscreenOnOpen : true;
|
|
484
|
+
bridgeSetting = resolved && typeof resolved.editorBridge === 'boolean' ? resolved.editorBridge : bridgeSetting;
|
|
397
485
|
}
|
|
398
486
|
scope.watch((next) => {
|
|
399
487
|
if (next != null && (next.serve === 'dsh' || next.serve === 'loopback')) {
|
|
@@ -402,6 +490,19 @@ export async function apply(ctx, config) {
|
|
|
402
490
|
console.log(`[code-server] serve updated: ${serveSetting}(下次启动生效)`);
|
|
403
491
|
}
|
|
404
492
|
}
|
|
493
|
+
if (next != null && typeof next.editorBridge === 'boolean') {
|
|
494
|
+
if (next.editorBridge !== bridgeSetting) {
|
|
495
|
+
bridgeSetting = next.editorBridge;
|
|
496
|
+
console.log(`[code-server] editorBridge updated: ${bridgeSetting}`);
|
|
497
|
+
// 关掉 → 立刻下线(删配置 + 注销工具);开启 → 若 IDE 在跑就补一份配置。
|
|
498
|
+
if (bridgeSetting === false) {
|
|
499
|
+
clearBridgeRuntime();
|
|
500
|
+
} else if (state.status === 'running' && state.serve === 'loopback' && Number.isSafeInteger(state.port) && state.port > 0) {
|
|
501
|
+
bridgeToken ??= mintBridgeToken();
|
|
502
|
+
syncBridgeRuntime({ host: cfg.host, port: state.port, pid: state.pid, startedAt: state.startedAt });
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
405
506
|
if (next != null && typeof next.keepResident === 'boolean') {
|
|
406
507
|
keepResident = next.keepResident;
|
|
407
508
|
console.log(`[code-server] keepResident updated: ${keepResident}`);
|
|
@@ -448,6 +549,8 @@ export async function apply(ctx, config) {
|
|
|
448
549
|
status: 'stopped', // stopped | starting | running | stopping | error
|
|
449
550
|
pid: null,
|
|
450
551
|
port: cfg.port,
|
|
552
|
+
/** 本实例的路径令牌(回环模式;不写日志、不进 argv)。 */
|
|
553
|
+
token: null,
|
|
451
554
|
cwd: null,
|
|
452
555
|
/** 进程**启动时**的工作目录(诊断用):运行时切工作区只改 cwd,进程 cwd 保持这里不变。 */
|
|
453
556
|
launchCwd: null,
|
|
@@ -470,6 +573,134 @@ export async function apply(ctx, config) {
|
|
|
470
573
|
let pollTimer = null;
|
|
471
574
|
let disposeKilled = false;
|
|
472
575
|
|
|
576
|
+
// ---- 编辑器桥(0.3.0):配置/凭据写在 <extensionsDir>/.dshcs-bridge/bridge.json ----
|
|
577
|
+
// 这里只算路径;是否启用还要看 cfg.editorBridge 与 setting 值。详见 lib/bridge.mjs 顶部。
|
|
578
|
+
const bridgeRoot = dataRoot(cfg);
|
|
579
|
+
const bridgeUserDataDir = cfg.userDataDir || path.join(bridgeRoot, 'user-data');
|
|
580
|
+
const bridgeExtensionsDir = cfg.extensionsDir || path.join(bridgeRoot, 'extensions');
|
|
581
|
+
const bridgeMetaDir = path.join(bridgeExtensionsDir, BRIDGE_DIRNAME);
|
|
582
|
+
/** 已写入的桥配置(供桥路由与 /status 读回);null = 未启用/未就绪。 */
|
|
583
|
+
let bridgeMeta = null;
|
|
584
|
+
/** 桥工具(editor_context / editor_diagnostics)的 disposer;null = 未注册。 */
|
|
585
|
+
let bridgeToolDispose = null;
|
|
586
|
+
/** 提示词段落的 disposer(只注册一次;文本按桥是否存活渲染)。 */
|
|
587
|
+
let bridgePromptDispose = null;
|
|
588
|
+
/** agent 写操作观察器的 disposer。 */
|
|
589
|
+
let bridgeObserveDispose = null;
|
|
590
|
+
/** 推给编辑器的事件环形缓冲(tools/result → 扩展轮询)。 */
|
|
591
|
+
const bridgeEvents = createEventRing();
|
|
592
|
+
/** 编辑器状态缓存:扩展在每次 /sync 里推上来,agent 的工具调用来读它。 */
|
|
593
|
+
const bridgeContext = createContextCache();
|
|
594
|
+
|
|
595
|
+
/** 本次启动生成的新桥令牌;null = 本实例没有令牌(adopt 旧实例时会回读 bridge.json)。 */
|
|
596
|
+
let bridgeToken = null;
|
|
597
|
+
|
|
598
|
+
/** 桥是否启用(行配置为种子,设置文档里可实时改)。 */
|
|
599
|
+
function bridgeEnabled() {
|
|
600
|
+
return bridgeSetting !== false;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/** 提示词段落在插件激活时注册一次:文本按"桥是否存活"渲染(桥停时为空串 → DSH 丢弃该段),
|
|
604
|
+
* 所以在 IDE 从未启动的部署里它也只是一段空注册,不产生任何提示词开销。 */
|
|
605
|
+
setPromptLiveProbe(() => bridgeMeta !== null);
|
|
606
|
+
bridgePromptDispose = registerEditorPrompt(ctx);
|
|
607
|
+
if (bridgePromptDispose === null && bridgeEnabled()) {
|
|
608
|
+
console.warn('[code-server] 编辑器桥:systemPrompt 服务不可用,提示词段落未注册(工具仍可用)');
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// 观察 agent 的写操作(→ 编辑器 diff 提示)并在写脏文件前附一条提醒。
|
|
612
|
+
// 与 IDE 是否在跑无关:桥没起来时 isLive() 为 false,dirty 检查直接跳过。
|
|
613
|
+
bridgeObserveDispose = registerBridgeObserver(ctx, {
|
|
614
|
+
emit: (kind, fields) => bridgeEvents.push(kind, fields),
|
|
615
|
+
context: () => bridgeContext.get(),
|
|
616
|
+
isLive: () => bridgeMeta !== null && !bridgeContext.isStale(),
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
/** 同步桥运行时:写入/更新 bridge.json,并(就绪时)注册编辑器工具。
|
|
620
|
+
* 端口、令牌、pid 三者任一变化都重写 —— 扩展每 5s 重读,故不需要任何推送。 */
|
|
621
|
+
function syncBridgeRuntime({ host, port, pid, startedAt }) {
|
|
622
|
+
if (!bridgeEnabled()) return;
|
|
623
|
+
if (typeof host !== 'string' || !Number.isSafeInteger(port) || port <= 0) return;
|
|
624
|
+
bridgeToken ??= mintBridgeToken();
|
|
625
|
+
const base = bridgeUrl(host, port);
|
|
626
|
+
const changed = bridgeMeta === null || bridgeMeta.url !== base || bridgeMeta.token !== bridgeToken || bridgeMeta.pid !== pid;
|
|
627
|
+
bridgeMeta = { url: base, token: bridgeToken, pid, startedAt: startedAt ?? null, host, port };
|
|
628
|
+
try {
|
|
629
|
+
writeBridgeConfig(bridgeExtensionsDir, { url: base, token: bridgeToken, pid, startedAt: startedAt ?? null });
|
|
630
|
+
} catch (err) {
|
|
631
|
+
console.warn(`[code-server] 编辑器桥配置写入失败(${bridgeExtensionsDir}):${err && err.message ? err.message : err}`);
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
if (changed) {
|
|
635
|
+
// 只打印"已启用",不打印令牌本身(与 path-token 同一决策)。
|
|
636
|
+
console.log(`[code-server] 编辑器桥:已启用(${base},令牌文件 ${path.join(bridgeMetaDir, 'bridge.json')})`);
|
|
637
|
+
}
|
|
638
|
+
ensureBridgeTools();
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* 接管实例时对齐桥令牌:磁盘上已有配置且基址一致 → 沿用(避免无谓轮换打乱正在运行的扩展);
|
|
643
|
+
* 否则 mint 新的(扩展下次轮询就会读到新的 bridge.json,一次请求的失败无所谓)。
|
|
644
|
+
*/
|
|
645
|
+
function adoptBridgeRuntime(host, port, pid, startedAt) {
|
|
646
|
+
if (!bridgeEnabled() || typeof host !== 'string' || !Number.isSafeInteger(port) || port <= 0) return;
|
|
647
|
+
const existing = readBridgeConfig(bridgeExtensionsDir);
|
|
648
|
+
const base = bridgeUrl(host, port);
|
|
649
|
+
bridgeToken = existing !== null && existing.url === base && TOKEN_RE.test(String(existing.token))
|
|
650
|
+
? String(existing.token)
|
|
651
|
+
: mintBridgeToken();
|
|
652
|
+
syncBridgeRuntime({ host, port, pid, startedAt });
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/** 注销桥运行时(停止 IDE / 插件卸载):删配置 + 注销工具 + 清缓存,扩展随即休眠。 */
|
|
656
|
+
function clearBridgeRuntime() {
|
|
657
|
+
bridgeToolDispose = disposeSafely(bridgeToolDispose);
|
|
658
|
+
bridgeContext.clear();
|
|
659
|
+
bridgeEvents.reset();
|
|
660
|
+
if (bridgeMeta === null) return;
|
|
661
|
+
bridgeMeta = null;
|
|
662
|
+
try {
|
|
663
|
+
removeBridgeConfig(bridgeExtensionsDir);
|
|
664
|
+
} catch {
|
|
665
|
+
// 配置删不掉不影响正确性(扩展会因 IDE 不可达而休眠)
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function disposeSafely(dispose) {
|
|
670
|
+
if (typeof dispose !== 'function') return null;
|
|
671
|
+
try {
|
|
672
|
+
dispose();
|
|
673
|
+
} catch (err) {
|
|
674
|
+
console.warn(`[code-server] 桥工具注销失败:${err && err.message ? err.message : err}`);
|
|
675
|
+
}
|
|
676
|
+
return null;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/** 桥就绪时注册编辑器工具;`tools` / `defineTool` 缺失时静默退化为"只有 HTTP 面"。 */
|
|
680
|
+
function ensureBridgeTools() {
|
|
681
|
+
if (bridgeMeta === null || bridgeToolDispose !== null) return;
|
|
682
|
+
Promise.resolve(registerEditorTools(ctx, {
|
|
683
|
+
target: () => bridgeMeta,
|
|
684
|
+
cache: () => bridgeContext,
|
|
685
|
+
}))
|
|
686
|
+
.then((dispose) => {
|
|
687
|
+
if (dispose === null) {
|
|
688
|
+
console.log('[code-server] 编辑器桥:工具服务不可用,仅提供 HTTP 面(editor_context/editor_diagnostics 未注册)');
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
// 期间桥可能已经被停掉(IDE 退出):立刻回滚,避免留下永远不可用的工具。
|
|
692
|
+
if (bridgeMeta === null) {
|
|
693
|
+
disposeSafely(dispose);
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
bridgeToolDispose = dispose;
|
|
697
|
+
console.log('[code-server] 编辑器桥:已注册 editor_context / editor_diagnostics');
|
|
698
|
+
})
|
|
699
|
+
.catch((err) => {
|
|
700
|
+
console.warn(`[code-server] 编辑器桥工具注册失败:${err && err.message ? err.message : err}`);
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
|
|
473
704
|
// ---- DSH 同源挂载(serve=dsh):把 /code-server 注册到 DSH 自己的 webServer ----
|
|
474
705
|
// webServer 只在 web profile 存在(desktop 显式禁用该行)→ 用 ctx.inject 特性检测,
|
|
475
706
|
// 缺失时 requestedServe() 自动回退 loopback。
|
|
@@ -497,6 +728,13 @@ export async function apply(ctx, config) {
|
|
|
497
728
|
}
|
|
498
729
|
}, 'code-server: dsh mount');
|
|
499
730
|
|
|
731
|
+
/** 回环模式的客户端 URL:随机端口 + 路径令牌(令牌是 URL 路径的一段,浏览器会把子请求与 WS
|
|
732
|
+
* 一并带过去 —— 这正是不走 VS Code 自带 cookie 令牌的原因,见 launcher 文件头"安全模型")。 */
|
|
733
|
+
function loopbackUrl() {
|
|
734
|
+
const token = typeof state.token === 'string' && state.token !== '' ? `${state.token}/` : '';
|
|
735
|
+
return `http://${cfg.host}:${state.port}/${token}`;
|
|
736
|
+
}
|
|
737
|
+
|
|
500
738
|
function snapshot() {
|
|
501
739
|
const running = state.status === 'running' && state.pid !== null;
|
|
502
740
|
const dshMode = state.serve === 'dsh';
|
|
@@ -510,8 +748,9 @@ export async function apply(ctx, config) {
|
|
|
510
748
|
pid: state.pid,
|
|
511
749
|
cwd: state.cwd,
|
|
512
750
|
launchCwd: state.launchCwd,
|
|
513
|
-
// dsh 模式:同源相对地址(由 DSH webServer 的 prefix 路由提供服务)
|
|
514
|
-
|
|
751
|
+
// dsh 模式:同源相对地址(由 DSH webServer 的 prefix 路由提供服务);
|
|
752
|
+
// loopback:随机端口 + 路径令牌(令牌只出现在这个 URL 里,客户端据此加载 iframe)
|
|
753
|
+
url: running ? (dshMode ? '/code-server/' : loopbackUrl()) : null,
|
|
515
754
|
productPath: state.env?.productPath ?? null,
|
|
516
755
|
version: state.version,
|
|
517
756
|
error: state.error,
|
|
@@ -521,6 +760,15 @@ export async function apply(ctx, config) {
|
|
|
521
760
|
claimExtensions, // 客户端据此按扩展名决定认领哪些文件(0.2.11,取代 fileOpenScope)
|
|
522
761
|
fullscreenOnOpen, // 客户端据此决定"打开标签即全屏右侧栏"(0.2.9)
|
|
523
762
|
sidebarUi: state.sidebarUi,
|
|
763
|
+
/** 编辑器桥状态(0.3.0,只读诊断面;**不含令牌** —— 令牌只在 bridge.json 里)。 */
|
|
764
|
+
bridge: {
|
|
765
|
+
enabled: bridgeEnabled(),
|
|
766
|
+
live: bridgeMeta !== null,
|
|
767
|
+
toolsRegistered: bridgeToolDispose !== null,
|
|
768
|
+
supported: state.serve === 'loopback',
|
|
769
|
+
url: bridgeMeta === null ? null : bridgeMeta.url,
|
|
770
|
+
file: path.join(bridgeMetaDir, 'bridge.json'),
|
|
771
|
+
},
|
|
524
772
|
env: state.env,
|
|
525
773
|
setup: {
|
|
526
774
|
running: state.setup.running,
|
|
@@ -617,19 +865,22 @@ export async function apply(ctx, config) {
|
|
|
617
865
|
await killTree(pid);
|
|
618
866
|
}
|
|
619
867
|
removePidFile(cfg);
|
|
868
|
+
clearBridgeRuntime(); // 桥在 IDE 停止时同步下线:配置删除,扩展随即休眠
|
|
620
869
|
state.status = 'stopped';
|
|
621
870
|
state.pid = null;
|
|
622
871
|
state.cwd = null;
|
|
623
872
|
state.launchCwd = null;
|
|
873
|
+
state.token = null;
|
|
874
|
+
state.port = cfg.port;
|
|
624
875
|
state.startedAt = null;
|
|
625
876
|
state.adopted = false;
|
|
626
877
|
if (reason) state.error = null;
|
|
627
878
|
}
|
|
628
879
|
|
|
629
|
-
/** 就绪探针:loopback 走 TCP,dsh 模式走命名管道。 */
|
|
880
|
+
/** 就绪探针:loopback 走 TCP(带路径令牌),dsh 模式走命名管道。 */
|
|
630
881
|
function probeReady(timeoutMs = 1500) {
|
|
631
882
|
if (state.pipe !== null) return healthCheckPipe(state.pipe, timeoutMs);
|
|
632
|
-
return healthCheck(cfg.host, state.port, timeoutMs);
|
|
883
|
+
return healthCheck(cfg.host, state.port, state.token, timeoutMs);
|
|
633
884
|
}
|
|
634
885
|
|
|
635
886
|
function beginPollingReady() {
|
|
@@ -720,23 +971,29 @@ export async function apply(ctx, config) {
|
|
|
720
971
|
console.warn('[code-server] 0.2.0 起不再支持口令认证(argon2 已移除),按 auth=none 运行');
|
|
721
972
|
}
|
|
722
973
|
|
|
723
|
-
// 端口占用则尝试 adopt(仅
|
|
724
|
-
|
|
725
|
-
|
|
974
|
+
// 端口占用则尝试 adopt(仅"显式指定端口"时;默认 port=0 由系统分配,不存在占用问题)。
|
|
975
|
+
// 探针必须带路径令牌(被接管的实例是我们自己拉的,令牌在令牌文件里)——
|
|
976
|
+
// 没有令牌文件说明那个实例不是本插件当前这代拉起来的,不接管。
|
|
977
|
+
if (serve === 'loopback' && cfg.port !== 0) {
|
|
978
|
+
const existingToken = readToken(cfg);
|
|
979
|
+
const probe = await healthCheck(cfg.host, cfg.port, existingToken, 800);
|
|
726
980
|
if (probe.ok) {
|
|
727
981
|
const record = readPidFile(cfg);
|
|
728
982
|
if (record && isAlive(record.pid)) {
|
|
729
983
|
state.serve = 'loopback';
|
|
730
984
|
state.status = 'running';
|
|
731
985
|
state.pid = record.pid;
|
|
986
|
+
state.port = cfg.port;
|
|
987
|
+
state.token = existingToken;
|
|
732
988
|
state.cwd = cwd ?? record.cwd ?? null;
|
|
733
989
|
state.launchCwd = record.launchCwd ?? record.cwd ?? null;
|
|
734
990
|
state.startedAt = record.startedAt ?? null;
|
|
735
991
|
state.adopted = true;
|
|
992
|
+
adoptBridgeRuntime(cfg.host, cfg.port, record.pid, state.startedAt);
|
|
736
993
|
return snapshot();
|
|
737
994
|
}
|
|
738
995
|
state.status = 'error';
|
|
739
|
-
state.error = `端口 ${cfg.port} 已被占用且没有有效的 pid.json 记录(拒绝误杀)
|
|
996
|
+
state.error = `端口 ${cfg.port} 已被占用且没有有效的 pid.json 记录(拒绝误杀);请释放端口或把 port 改回 0(随机端口)`;
|
|
740
997
|
return snapshot();
|
|
741
998
|
}
|
|
742
999
|
}
|
|
@@ -748,8 +1005,9 @@ export async function apply(ctx, config) {
|
|
|
748
1005
|
fs.mkdirSync(userDataDir, { recursive: true });
|
|
749
1006
|
fs.mkdirSync(extensionsDir, { recursive: true });
|
|
750
1007
|
|
|
751
|
-
//
|
|
752
|
-
|
|
1008
|
+
// 安装树内自带扩展(dshcs-open-file:host 信号文件 → VS Code 打开文件;
|
|
1009
|
+
// dshcs-editor-bridge:编辑器桥的扩展侧,0.3.0)
|
|
1010
|
+
installBundledExtensions(extensionsDir, userDataDir);
|
|
753
1011
|
|
|
754
1012
|
const args = launch.kind === 'launcher'
|
|
755
1013
|
? [
|
|
@@ -773,7 +1031,12 @@ export async function apply(ctx, config) {
|
|
|
773
1031
|
args.push('--pipe', state.pipe);
|
|
774
1032
|
} else {
|
|
775
1033
|
state.pipe = null;
|
|
776
|
-
|
|
1034
|
+
// 随机端口(port=0)+ 路径令牌:令牌**不进程 argv**(本机任意进程都能读命令行),
|
|
1035
|
+
// 走文件传递;实际端口由 launcher 写 endpoint 文件回报。
|
|
1036
|
+
fs.rmSync(endpointFile(cfg), { force: true }); // 清掉上一实例的记录,避免读到旧端口
|
|
1037
|
+
state.token = rotateToken(cfg);
|
|
1038
|
+
args.push('--port', String(cfg.port), '--host', cfg.host,
|
|
1039
|
+
'--token-file', tokenFile(cfg), '--endpoint-file', endpointFile(cfg));
|
|
777
1040
|
}
|
|
778
1041
|
if (typeof cfg.locale === 'string' && cfg.locale !== '') args.push('--locale', cfg.locale);
|
|
779
1042
|
}
|
|
@@ -783,6 +1046,7 @@ export async function apply(ctx, config) {
|
|
|
783
1046
|
state.error = null;
|
|
784
1047
|
state.logTail = '';
|
|
785
1048
|
state.status = 'starting';
|
|
1049
|
+
state.port = serve === 'dsh' ? null : cfg.port;
|
|
786
1050
|
state.cwd = cwd ?? null;
|
|
787
1051
|
state.launchCwd = cwd ?? null;
|
|
788
1052
|
state.startedAt = Date.now();
|
|
@@ -827,26 +1091,61 @@ export async function apply(ctx, config) {
|
|
|
827
1091
|
}
|
|
828
1092
|
child = proc;
|
|
829
1093
|
state.pid = proc.pid ?? null;
|
|
830
|
-
|
|
1094
|
+
/** 实例记录:pid 一到手就先写(崩溃/回收要靠它);端口 0 时在拿到实际端口后再补写一次。 */
|
|
1095
|
+
const writeInstanceRecord = () => writePidFile(cfg, {
|
|
831
1096
|
pid: proc.pid,
|
|
832
1097
|
startedAt: state.startedAt,
|
|
833
1098
|
cwd: cwd ?? null,
|
|
834
1099
|
launchCwd: cwd ?? null,
|
|
835
1100
|
host: serve === 'dsh' ? null : cfg.host,
|
|
836
|
-
port: serve === 'dsh' ? null :
|
|
1101
|
+
port: serve === 'dsh' ? null : state.port,
|
|
837
1102
|
serve,
|
|
838
1103
|
pipe: state.pipe,
|
|
839
1104
|
launchKind: launch.kind,
|
|
840
1105
|
launchCommand: launch.kind === 'bin' ? launch.command : launch.script,
|
|
1106
|
+
tokenized: state.token !== null,
|
|
841
1107
|
});
|
|
1108
|
+
writeInstanceRecord();
|
|
842
1109
|
|
|
843
1110
|
proc.stdout?.on?.('data', appendLog);
|
|
844
1111
|
proc.stderr?.on?.('data', appendLog);
|
|
845
1112
|
|
|
1113
|
+
// 端口 0(默认)时 host 只能从 endpoint 文件知道实际端口:等 launcher 写完再开始就绪轮询。
|
|
1114
|
+
if (launch.kind === 'launcher' && serve === 'loopback' && cfg.port === 0) {
|
|
1115
|
+
const deadline = Date.now() + 10000;
|
|
1116
|
+
let endpoint = null;
|
|
1117
|
+
while (endpoint === null && Date.now() < deadline) {
|
|
1118
|
+
endpoint = readEndpoint(cfg);
|
|
1119
|
+
if (endpoint !== null && endpoint.pid !== (proc.pid ?? -1)) endpoint = null; // 旧进程的残留记录
|
|
1120
|
+
if (endpoint === null) await new Promise((r) => setTimeout(r, 100));
|
|
1121
|
+
}
|
|
1122
|
+
if (endpoint === null) {
|
|
1123
|
+
state.status = 'error';
|
|
1124
|
+
state.error = 'launcher 未回报监听端口(endpoint 文件缺失);请查看启动日志';
|
|
1125
|
+
return snapshot();
|
|
1126
|
+
}
|
|
1127
|
+
state.port = endpoint.port;
|
|
1128
|
+
writeInstanceRecord();
|
|
1129
|
+
console.log(`[code-server] 随机端口: ${state.port}(host=${endpoint.host}, pid=${endpoint.pid})`);
|
|
1130
|
+
}
|
|
1131
|
+
// 编辑器桥:端口已定(loopback + 已知端口)时写入配置,扩展随即能连上来。
|
|
1132
|
+
// 新启动会轮换桥令牌 —— 与路径令牌同一时机(每次新启动都换)。
|
|
1133
|
+
// dsh 模式(无独立端口)不启用:桥的 Host 白名单假设是 127.0.0.1:<实际端口>,管道模式没有端口。
|
|
1134
|
+
if (serve === 'loopback' && (cfg.port !== 0 || state.port !== null)) {
|
|
1135
|
+
bridgeToken = mintBridgeToken();
|
|
1136
|
+
syncBridgeRuntime({
|
|
1137
|
+
host: cfg.host,
|
|
1138
|
+
port: state.port,
|
|
1139
|
+
pid: proc.pid ?? null,
|
|
1140
|
+
startedAt: state.startedAt,
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
|
|
846
1144
|
proc.on('error', (err) => {
|
|
847
1145
|
if (child !== proc) return;
|
|
848
1146
|
child = null;
|
|
849
1147
|
removePidFile(cfg);
|
|
1148
|
+
clearBridgeRuntime();
|
|
850
1149
|
state.status = 'error';
|
|
851
1150
|
state.error = `code-server 启动失败: ${err && err.message ? err.message : String(err)}\n${state.logTail.slice(-1000)}`;
|
|
852
1151
|
});
|
|
@@ -855,6 +1154,7 @@ export async function apply(ctx, config) {
|
|
|
855
1154
|
if (child !== proc) return; // 已被 stop/dispose 接管
|
|
856
1155
|
child = null;
|
|
857
1156
|
removePidFile(cfg);
|
|
1157
|
+
clearBridgeRuntime();
|
|
858
1158
|
if (disposeKilled) return;
|
|
859
1159
|
state.status = 'error';
|
|
860
1160
|
state.error = `code-server 意外退出${code !== null ? `(exit ${code})` : signal ? `(signal ${signal})` : ''}:\n${state.logTail.slice(-1500)}`;
|
|
@@ -1017,6 +1317,95 @@ export async function apply(ctx, config) {
|
|
|
1017
1317
|
}
|
|
1018
1318
|
}
|
|
1019
1319
|
|
|
1320
|
+
// ---- 编辑器桥路由(0.3.0)----
|
|
1321
|
+
// 这些路由**不依赖** DSH 的 cookie 认证:扩展宿主是 Node 进程,拿不到浏览器 cookie,
|
|
1322
|
+
// 所以自带独立令牌(见 lib/bridge.mjs 顶部的安全不变量)。命名空间永久只读。
|
|
1323
|
+
|
|
1324
|
+
/** 每个桥路由都先过 guard;返回 Response 表示拒绝,调用方直接返回它。 */
|
|
1325
|
+
function bridgeRejection(request) {
|
|
1326
|
+
return bridgeGuard(request, bridgeMeta === null ? null : bridgeMeta.token);
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
/** 桥不可达时的统一 503(扩展侧没跑 / IDE 刚起还没加载扩展)。 */
|
|
1330
|
+
function bridgeDown(reason) {
|
|
1331
|
+
return jsonResponse({ ok: false, error: reason }, 503);
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
async function handleBridgeHealth() {
|
|
1335
|
+
// 无鉴权:只回一句"桥活着吗",不含任何编辑器数据(便于重启后一眼确认)。
|
|
1336
|
+
return jsonResponse({
|
|
1337
|
+
ok: true,
|
|
1338
|
+
bridge: bridgeMeta !== null,
|
|
1339
|
+
pid: state.pid,
|
|
1340
|
+
url: bridgeMeta === null ? null : bridgeMeta.url,
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
async function handleBridgeSync(request) {
|
|
1345
|
+
const denied = bridgeRejection(request);
|
|
1346
|
+
if (denied !== null) return denied;
|
|
1347
|
+
let body;
|
|
1348
|
+
try {
|
|
1349
|
+
body = await readJsonBody(request);
|
|
1350
|
+
} catch {
|
|
1351
|
+
return jsonResponse({ ok: false, error: '请求体不是合法 JSON' }, 400);
|
|
1352
|
+
}
|
|
1353
|
+
if (!bodyWithinLimit(body)) return jsonResponse({ ok: false, error: '上报内容过大' }, 413);
|
|
1354
|
+
bridgeContext.update(body);
|
|
1355
|
+
// 取事件 + 推进游标:与上报同一趟来回,扩展不需要第二个定时器。
|
|
1356
|
+
let since = 0;
|
|
1357
|
+
try {
|
|
1358
|
+
const raw = new URL(request.url).searchParams.get('since');
|
|
1359
|
+
const parsed = raw === null ? 0 : Number.parseInt(raw, 10);
|
|
1360
|
+
if (Number.isSafeInteger(parsed) && parsed > 0) since = parsed;
|
|
1361
|
+
} catch {
|
|
1362
|
+
since = 0;
|
|
1363
|
+
}
|
|
1364
|
+
const events = bridgeEvents.since(since);
|
|
1365
|
+
bridgeEvents.reset();
|
|
1366
|
+
return jsonResponse({ ok: true, events });
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
async function handleBridgeAsk(request) {
|
|
1370
|
+
const denied = bridgeRejection(request);
|
|
1371
|
+
if (denied !== null) return denied;
|
|
1372
|
+
let body;
|
|
1373
|
+
try {
|
|
1374
|
+
body = await readJsonBody(request);
|
|
1375
|
+
} catch {
|
|
1376
|
+
return jsonResponse({ ok: false, error: '请求体不是合法 JSON' }, 400);
|
|
1377
|
+
}
|
|
1378
|
+
const text = typeof body.text === 'string' ? body.text : '';
|
|
1379
|
+
if (text === '') return jsonResponse({ ok: false, error: '需要 text 字段(要问 DSH 的话)' }, 400);
|
|
1380
|
+
const file = typeof body.file === 'string' && body.file !== '' ? body.file : null;
|
|
1381
|
+
const lineStart = Number.isSafeInteger(body.lineStart) ? body.lineStart : null;
|
|
1382
|
+
const lineEnd = Number.isSafeInteger(body.lineEnd) ? body.lineEnd : null;
|
|
1383
|
+
const selection = typeof body.selection === 'string' && body.selection !== '' ? body.selection : null;
|
|
1384
|
+
// 交给 bridge-session.mjs(agent 投递);它内部特性探测 agents,缺失时返回
|
|
1385
|
+
// {ok:false, code:'NO_AGENT'} —— 这里原样透传,并把 NO_AGENT 映射成 409。
|
|
1386
|
+
const result = await deliverEditorPrompt(ctx, {
|
|
1387
|
+
text,
|
|
1388
|
+
file,
|
|
1389
|
+
lineStart,
|
|
1390
|
+
lineEnd,
|
|
1391
|
+
selection,
|
|
1392
|
+
languageId: typeof body.languageId === 'string' ? body.languageId : null,
|
|
1393
|
+
});
|
|
1394
|
+
return jsonResponse(result, result.ok === true ? 200 : (result.code === 'NO_AGENT' ? 409 : 200));
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
async function handleBridgeEvent(request) {
|
|
1398
|
+
const denied = bridgeRejection(request);
|
|
1399
|
+
if (denied !== null) return denied;
|
|
1400
|
+
const body = await readJsonBody(request);
|
|
1401
|
+
const kind = typeof body.kind === 'string' ? body.kind : '';
|
|
1402
|
+
if (kind === '') return jsonResponse({ ok: false, error: '需要 kind 字段' }, 400);
|
|
1403
|
+
// 扩展上报的编辑器侧事件(打开/关闭文件等):目前只进日志尾,供诊断时回看。
|
|
1404
|
+
appendLog(`[bridge] ${kind}${typeof body.path === 'string' ? ` ${body.path}` : ''}\n`);
|
|
1405
|
+
return jsonResponse({ ok: true });
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
|
|
1020
1409
|
// 每条操作一条 exact Fetch 路由。desktop 的 assetHandler 只把 /api/* 交给
|
|
1021
1410
|
// createSharedFetchHandler('/api'),所以路径必须落在 /api 下。
|
|
1022
1411
|
const disposers = [ { path: `${API_BASE}/status`, methods: ['GET'], fetch: handleStatus },
|
|
@@ -1025,6 +1414,13 @@ export async function apply(ctx, config) {
|
|
|
1025
1414
|
{ path: `${API_BASE}/setup`, methods: ['POST'], fetch: handleSetup },
|
|
1026
1415
|
{ path: `${API_BASE}/open-file`, methods: ['POST'], fetch: handleOpenFile },
|
|
1027
1416
|
{ path: `${API_BASE}/ui-mode`, methods: ['POST'], fetch: handleUiMode },
|
|
1417
|
+
// ---- 编辑器桥(0.3.0):扩展侧的 HTTP 面。全部自带令牌鉴权,**命名空间永久只读**。----
|
|
1418
|
+
// 只有 4 条:扩展宿主没有 HTTP 服务器,所以"拿编辑器状态"由扩展在 /sync 里推上来,
|
|
1419
|
+
// host 缓存后供 agent 工具读取(见 lib/bridge.mjs 顶部的通道说明)。
|
|
1420
|
+
{ path: `${API_BASE}/bridge/health`, methods: ['GET'], fetch: handleBridgeHealth },
|
|
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 },
|
|
1028
1424
|
].map(route => connection.fetch.register({
|
|
1029
1425
|
path: route.path,
|
|
1030
1426
|
methods: route.methods,
|
|
@@ -1036,6 +1432,9 @@ export async function apply(ctx, config) {
|
|
|
1036
1432
|
return () => {
|
|
1037
1433
|
disposeKilled = true;
|
|
1038
1434
|
stopPolling();
|
|
1435
|
+
bridgeObserveDispose = disposeSafely(bridgeObserveDispose);
|
|
1436
|
+
bridgePromptDispose = disposeSafely(bridgePromptDispose);
|
|
1437
|
+
clearBridgeRuntime();
|
|
1039
1438
|
for (const d of disposers) {
|
|
1040
1439
|
try {
|
|
1041
1440
|
// connection.fetch.register 的 disposer 是异步的(返回 Promise);
|
|
@@ -1094,19 +1493,28 @@ export async function apply(ctx, config) {
|
|
|
1094
1493
|
console.log(`[code-server] 检测到旧版安装根 ${legacyRoot}(0.1.35 及更早遗留,已不再使用,可安全删除)`);
|
|
1095
1494
|
}
|
|
1096
1495
|
|
|
1097
|
-
// DSH host 重启后 adopt(仅 loopback 模式):pid.json
|
|
1496
|
+
// DSH host 重启后 adopt(仅 loopback 模式):pid.json 有效、进程存活、令牌文件在、endpoint 记录的端口
|
|
1497
|
+
// 上 /healthz(带令牌)响应 → 接管。端口 0 时不能拿 cfg.port 去探(那是个占位值),必须用 endpoint 文件。
|
|
1098
1498
|
// dsh 模式的 launcher 带 --parent-pid 看门狗:host 一退出它就自行退出,不存在可接管的孤儿。
|
|
1099
1499
|
if (liveInstance && (record.serve !== 'dsh')) {
|
|
1100
|
-
const
|
|
1500
|
+
const adoptedToken = readToken(cfg);
|
|
1501
|
+
const endpoint = readEndpoint(cfg);
|
|
1502
|
+
const adoptPort = endpoint !== null && endpoint.pid === record.pid ? endpoint.port : (cfg.port !== 0 ? cfg.port : null);
|
|
1503
|
+
const probe = adoptPort === null || adoptedToken === null
|
|
1504
|
+
? { ok: false }
|
|
1505
|
+
: await healthCheck(cfg.host, adoptPort, adoptedToken, 800);
|
|
1101
1506
|
if (probe.ok) {
|
|
1102
1507
|
state.serve = 'loopback';
|
|
1103
1508
|
state.status = 'running';
|
|
1104
1509
|
state.pid = record.pid;
|
|
1510
|
+
state.port = adoptPort;
|
|
1511
|
+
state.token = adoptedToken;
|
|
1105
1512
|
state.cwd = record.cwd ?? null;
|
|
1106
1513
|
state.launchCwd = record.launchCwd ?? record.cwd ?? null;
|
|
1107
1514
|
state.startedAt = record.startedAt ?? null;
|
|
1108
1515
|
state.adopted = true;
|
|
1109
|
-
console.log(`[code-server] adopted running instance pid=${record.pid} port=${
|
|
1516
|
+
console.log(`[code-server] adopted running instance pid=${record.pid} port=${adoptPort}(令牌已启用)`);
|
|
1517
|
+
adoptBridgeRuntime(cfg.host, adoptPort, record.pid, state.startedAt);
|
|
1110
1518
|
} else {
|
|
1111
1519
|
removePidFile(cfg);
|
|
1112
1520
|
}
|
|
@@ -1116,5 +1524,5 @@ export async function apply(ctx, config) {
|
|
|
1116
1524
|
|
|
1117
1525
|
maybePrestart();
|
|
1118
1526
|
|
|
1119
|
-
console.log(`[code-server] static plugin loaded (serve=${requestedServe()} host=${cfg.host} port=${cfg.port})`);
|
|
1527
|
+
console.log(`[code-server] static plugin loaded (serve=${requestedServe()} host=${cfg.host} port=${cfg.port === 0 ? '0=随机' : cfg.port})`);
|
|
1120
1528
|
}
|