dsh-code-server-app 0.2.14 → 0.3.7
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 +85 -0
- package/README.md +96 -0
- 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 +142 -0
- package/lib/bridge-tools.mjs +374 -0
- package/lib/bridge.mjs +325 -0
- package/lib/dsh-resolve.mjs +91 -0
- package/lib/index.js +354 -46
- package/package.json +11 -3
- package/vendor/VENDOR.json +1 -1
package/lib/index.js
CHANGED
|
@@ -38,6 +38,21 @@ import {
|
|
|
38
38
|
import { aliasNodePathDirs, ensureRuntimeLayout, resolveRuntime, runtimePackageName, verifyNatives } from './native.js';
|
|
39
39
|
import { MOUNT_PATH, mountOnWebServer } from './serve-dsh.mjs';
|
|
40
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';
|
|
41
56
|
|
|
42
57
|
// schemastery 由 DSH 部署自带(官方核心依赖),仿 auto-open-web 的解析策略:
|
|
43
58
|
// 常规 import 优先,不可用时回退到全局 npm 布局的 DSH 部署副本。
|
|
@@ -88,6 +103,11 @@ export const Config = z.object({
|
|
|
88
103
|
* 客户端把右侧栏切到全屏(铺满窗口);false 则保持 DSH 默认的 push(与对话并排)。
|
|
89
104
|
* 只影响"打开那一刻":之后用户点「退出全屏」不会被抢回去。 */
|
|
90
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),
|
|
91
111
|
});
|
|
92
112
|
|
|
93
113
|
const DEFAULT_CONFIG = {
|
|
@@ -102,6 +122,8 @@ const DEFAULT_CONFIG = {
|
|
|
102
122
|
extensionsDir: '',
|
|
103
123
|
locale: '',
|
|
104
124
|
readyTimeoutMs: 60000,
|
|
125
|
+
/** 编辑器桥(0.3.0):见 Config.editorBridge。行配置与设置文档都可关;设置卡片暂不提供行。 */
|
|
126
|
+
editorBridge: true,
|
|
105
127
|
};
|
|
106
128
|
|
|
107
129
|
const LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '::1']);
|
|
@@ -159,56 +181,72 @@ function launcherPath() {
|
|
|
159
181
|
return path.join(path.dirname(fileURLToPath(import.meta.url)), 'launcher.mjs');
|
|
160
182
|
}
|
|
161
183
|
|
|
162
|
-
/**
|
|
163
|
-
* (位于程序内置目录的扩展被 VS Code 视为内置——用户视图显示为"内置"
|
|
164
|
-
*
|
|
165
|
-
* 返回 { dst, builtin }——builtin=true
|
|
166
|
-
function extensionTarget(extensionsDir) {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
+
}
|
|
173
197
|
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
return { dst: path.join(extensionsDir,
|
|
198
|
+
} catch { /* fall through */ }
|
|
199
|
+
}
|
|
200
|
+
return { dst: path.join(extensionsDir, name), builtin: false };
|
|
177
201
|
}
|
|
178
202
|
|
|
179
|
-
/**
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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));
|
|
197
237
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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));
|
|
207
247
|
}
|
|
208
|
-
console.log(`[code-server] bundled extension dshcs-open-file -> ${dst}${target.builtin ? ' (内置,不可卸载)' : ' (用户级回退)'}${stale.length > 0 ? ` [更新 ${stale.join(',')}]` : ''}`);
|
|
209
|
-
} catch (err) {
|
|
210
|
-
console.warn('[code-server] bundled extension install failed:', err && err.message ? err.message : String(err));
|
|
211
248
|
}
|
|
249
|
+
void userDataDir;
|
|
212
250
|
}
|
|
213
251
|
|
|
214
252
|
/** 打开文件信号文件:<user-data>/User/dshcs-open.json(扩展轮询此文件)。 */
|
|
@@ -429,6 +467,8 @@ export async function apply(ctx, config) {
|
|
|
429
467
|
let keepResident = true; // 客户端常驻预热(0.2.2)
|
|
430
468
|
let claimExtensions = DEFAULT_CLAIM_EXTENSIONS; // 认领类型清单(0.2.11,取代 fileOpenScope)
|
|
431
469
|
let fullscreenOnOpen = true; // 客户端"打开标签即全屏右侧栏"(0.2.9)
|
|
470
|
+
/** 编辑器桥开关(0.3.0):行配置 cfg.editorBridge 是种子,设置文档里可改并实时生效。 */
|
|
471
|
+
let bridgeSetting = cfg.editorBridge !== false;
|
|
432
472
|
if (settingsSvc !== undefined && typeof settingsSvc.register === 'function') {
|
|
433
473
|
try {
|
|
434
474
|
const scope = settingsSvc.register(SETTINGS_NS, Config);
|
|
@@ -441,6 +481,7 @@ export async function apply(ctx, config) {
|
|
|
441
481
|
? normalizeClaimExtensions(resolved.claimExtensions)
|
|
442
482
|
: DEFAULT_CLAIM_EXTENSIONS;
|
|
443
483
|
fullscreenOnOpen = resolved && typeof resolved.fullscreenOnOpen === 'boolean' ? resolved.fullscreenOnOpen : true;
|
|
484
|
+
bridgeSetting = resolved && typeof resolved.editorBridge === 'boolean' ? resolved.editorBridge : bridgeSetting;
|
|
444
485
|
}
|
|
445
486
|
scope.watch((next) => {
|
|
446
487
|
if (next != null && (next.serve === 'dsh' || next.serve === 'loopback')) {
|
|
@@ -449,6 +490,19 @@ export async function apply(ctx, config) {
|
|
|
449
490
|
console.log(`[code-server] serve updated: ${serveSetting}(下次启动生效)`);
|
|
450
491
|
}
|
|
451
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
|
+
}
|
|
452
506
|
if (next != null && typeof next.keepResident === 'boolean') {
|
|
453
507
|
keepResident = next.keepResident;
|
|
454
508
|
console.log(`[code-server] keepResident updated: ${keepResident}`);
|
|
@@ -519,6 +573,134 @@ export async function apply(ctx, config) {
|
|
|
519
573
|
let pollTimer = null;
|
|
520
574
|
let disposeKilled = false;
|
|
521
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
|
+
|
|
522
704
|
// ---- DSH 同源挂载(serve=dsh):把 /code-server 注册到 DSH 自己的 webServer ----
|
|
523
705
|
// webServer 只在 web profile 存在(desktop 显式禁用该行)→ 用 ctx.inject 特性检测,
|
|
524
706
|
// 缺失时 requestedServe() 自动回退 loopback。
|
|
@@ -578,6 +760,15 @@ export async function apply(ctx, config) {
|
|
|
578
760
|
claimExtensions, // 客户端据此按扩展名决定认领哪些文件(0.2.11,取代 fileOpenScope)
|
|
579
761
|
fullscreenOnOpen, // 客户端据此决定"打开标签即全屏右侧栏"(0.2.9)
|
|
580
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
|
+
},
|
|
581
772
|
env: state.env,
|
|
582
773
|
setup: {
|
|
583
774
|
running: state.setup.running,
|
|
@@ -674,6 +865,7 @@ export async function apply(ctx, config) {
|
|
|
674
865
|
await killTree(pid);
|
|
675
866
|
}
|
|
676
867
|
removePidFile(cfg);
|
|
868
|
+
clearBridgeRuntime(); // 桥在 IDE 停止时同步下线:配置删除,扩展随即休眠
|
|
677
869
|
state.status = 'stopped';
|
|
678
870
|
state.pid = null;
|
|
679
871
|
state.cwd = null;
|
|
@@ -797,6 +989,7 @@ export async function apply(ctx, config) {
|
|
|
797
989
|
state.launchCwd = record.launchCwd ?? record.cwd ?? null;
|
|
798
990
|
state.startedAt = record.startedAt ?? null;
|
|
799
991
|
state.adopted = true;
|
|
992
|
+
adoptBridgeRuntime(cfg.host, cfg.port, record.pid, state.startedAt);
|
|
800
993
|
return snapshot();
|
|
801
994
|
}
|
|
802
995
|
state.status = 'error';
|
|
@@ -812,8 +1005,9 @@ export async function apply(ctx, config) {
|
|
|
812
1005
|
fs.mkdirSync(userDataDir, { recursive: true });
|
|
813
1006
|
fs.mkdirSync(extensionsDir, { recursive: true });
|
|
814
1007
|
|
|
815
|
-
//
|
|
816
|
-
|
|
1008
|
+
// 安装树内自带扩展(dshcs-open-file:host 信号文件 → VS Code 打开文件;
|
|
1009
|
+
// dshcs-editor-bridge:编辑器桥的扩展侧,0.3.0)
|
|
1010
|
+
installBundledExtensions(extensionsDir, userDataDir);
|
|
817
1011
|
|
|
818
1012
|
const args = launch.kind === 'launcher'
|
|
819
1013
|
? [
|
|
@@ -934,11 +1128,24 @@ export async function apply(ctx, config) {
|
|
|
934
1128
|
writeInstanceRecord();
|
|
935
1129
|
console.log(`[code-server] 随机端口: ${state.port}(host=${endpoint.host}, pid=${endpoint.pid})`);
|
|
936
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
|
+
}
|
|
937
1143
|
|
|
938
1144
|
proc.on('error', (err) => {
|
|
939
1145
|
if (child !== proc) return;
|
|
940
1146
|
child = null;
|
|
941
1147
|
removePidFile(cfg);
|
|
1148
|
+
clearBridgeRuntime();
|
|
942
1149
|
state.status = 'error';
|
|
943
1150
|
state.error = `code-server 启动失败: ${err && err.message ? err.message : String(err)}\n${state.logTail.slice(-1000)}`;
|
|
944
1151
|
});
|
|
@@ -947,6 +1154,7 @@ export async function apply(ctx, config) {
|
|
|
947
1154
|
if (child !== proc) return; // 已被 stop/dispose 接管
|
|
948
1155
|
child = null;
|
|
949
1156
|
removePidFile(cfg);
|
|
1157
|
+
clearBridgeRuntime();
|
|
950
1158
|
if (disposeKilled) return;
|
|
951
1159
|
state.status = 'error';
|
|
952
1160
|
state.error = `code-server 意外退出${code !== null ? `(exit ${code})` : signal ? `(signal ${signal})` : ''}:\n${state.logTail.slice(-1500)}`;
|
|
@@ -1109,6 +1317,95 @@ export async function apply(ctx, config) {
|
|
|
1109
1317
|
}
|
|
1110
1318
|
}
|
|
1111
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
|
+
|
|
1112
1409
|
// 每条操作一条 exact Fetch 路由。desktop 的 assetHandler 只把 /api/* 交给
|
|
1113
1410
|
// createSharedFetchHandler('/api'),所以路径必须落在 /api 下。
|
|
1114
1411
|
const disposers = [ { path: `${API_BASE}/status`, methods: ['GET'], fetch: handleStatus },
|
|
@@ -1117,6 +1414,13 @@ export async function apply(ctx, config) {
|
|
|
1117
1414
|
{ path: `${API_BASE}/setup`, methods: ['POST'], fetch: handleSetup },
|
|
1118
1415
|
{ path: `${API_BASE}/open-file`, methods: ['POST'], fetch: handleOpenFile },
|
|
1119
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 },
|
|
1120
1424
|
].map(route => connection.fetch.register({
|
|
1121
1425
|
path: route.path,
|
|
1122
1426
|
methods: route.methods,
|
|
@@ -1128,6 +1432,9 @@ export async function apply(ctx, config) {
|
|
|
1128
1432
|
return () => {
|
|
1129
1433
|
disposeKilled = true;
|
|
1130
1434
|
stopPolling();
|
|
1435
|
+
bridgeObserveDispose = disposeSafely(bridgeObserveDispose);
|
|
1436
|
+
bridgePromptDispose = disposeSafely(bridgePromptDispose);
|
|
1437
|
+
clearBridgeRuntime();
|
|
1131
1438
|
for (const d of disposers) {
|
|
1132
1439
|
try {
|
|
1133
1440
|
// connection.fetch.register 的 disposer 是异步的(返回 Promise);
|
|
@@ -1207,6 +1514,7 @@ export async function apply(ctx, config) {
|
|
|
1207
1514
|
state.startedAt = record.startedAt ?? null;
|
|
1208
1515
|
state.adopted = true;
|
|
1209
1516
|
console.log(`[code-server] adopted running instance pid=${record.pid} port=${adoptPort}(令牌已启用)`);
|
|
1517
|
+
adoptBridgeRuntime(cfg.host, adoptPort, record.pid, state.startedAt);
|
|
1210
1518
|
} else {
|
|
1211
1519
|
removePidFile(cfg);
|
|
1212
1520
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-code-server-app",
|
|
3
|
-
"version": "0.
|
|
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). 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.",
|
|
3
|
+
"version": "0.3.7",
|
|
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": {
|
|
7
7
|
"type": "git",
|
|
@@ -41,6 +41,11 @@
|
|
|
41
41
|
"lib/index.js",
|
|
42
42
|
"lib/client.js",
|
|
43
43
|
"lib/claim-types.js",
|
|
44
|
+
"lib/bridge.mjs",
|
|
45
|
+
"lib/bridge-tools.mjs",
|
|
46
|
+
"lib/bridge-session.mjs",
|
|
47
|
+
"lib/bridge-observe.mjs",
|
|
48
|
+
"lib/dsh-resolve.mjs",
|
|
44
49
|
"lib/launcher.mjs",
|
|
45
50
|
"lib/serve-dsh.mjs",
|
|
46
51
|
"lib/vendor.js",
|
|
@@ -48,6 +53,7 @@
|
|
|
48
53
|
"assets/favicon.ico",
|
|
49
54
|
"assets/favicon.svg",
|
|
50
55
|
"assets/extensions/dshcs-open-file",
|
|
56
|
+
"assets/extensions/dshcs-editor-bridge",
|
|
51
57
|
"scripts/vendor-vscode-server.mjs",
|
|
52
58
|
"scripts/vendor-code-server.mjs",
|
|
53
59
|
"scripts/vendor-repacks.mjs",
|
|
@@ -117,6 +123,8 @@
|
|
|
117
123
|
"test:launcher-routes": "node scripts/test-launcher-routes.mjs",
|
|
118
124
|
"test:fullscreen": "node scripts/test-sidebar-fullscreen.mjs",
|
|
119
125
|
"test:claim-types": "node scripts/test-claim-types.mjs",
|
|
120
|
-
"test:workspace-switch": "node scripts/test-workspace-switch.mjs"
|
|
126
|
+
"test:workspace-switch": "node scripts/test-workspace-switch.mjs",
|
|
127
|
+
"test:bridge-routes": "node scripts/test-bridge-routes.mjs",
|
|
128
|
+
"test:bridge-extension": "node scripts/test-bridge-extension.mjs"
|
|
121
129
|
}
|
|
122
130
|
}
|
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-12T02:24:49.146Z",
|
|
7
7
|
"source": "registry",
|
|
8
8
|
"node": "v24.13.1",
|
|
9
9
|
"platform": "win32",
|