dsh-code-server-app 0.3.57 → 0.3.59
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 +158 -92
- package/README.md +139 -80
- package/assets/extensions/dshcs-editor-bridge/extension.js +43 -220
- package/assets/extensions/dshcs-editor-bridge/lib/bridge-client.js +5 -37
- package/assets/extensions/dshcs-editor-bridge/package.json +2 -2
- package/lib/bridge-session.mjs +55 -5
- package/lib/bridge.mjs +11 -7
- package/lib/claim-types.js +4 -2
- package/lib/client.js +2884 -4
- package/lib/index.js +74 -206
- package/package.json +6 -29
- package/vendor/VENDOR.json +1 -1
- package/assets/extensions/dshcs-editor-bridge/lib/ask-panel.js +0 -416
- package/assets/extensions/dshcs-editor-bridge/webview/THIRD-PARTY.md +0 -22
- package/assets/extensions/dshcs-editor-bridge/webview/src/app.jsx +0 -242
- package/assets/extensions/dshcs-editor-bridge/webview/src/approval.jsx +0 -61
- package/assets/extensions/dshcs-editor-bridge/webview/src/official-tokens.css +0 -576
- package/assets/extensions/dshcs-editor-bridge/webview/src/panel.css +0 -380
- package/assets/extensions/dshcs-editor-bridge/webview/src/thread.jsx +0 -160
- package/assets/extensions/dshcs-editor-bridge/webview/thread.css +0 -1
- package/assets/extensions/dshcs-editor-bridge/webview/thread.js +0 -465
|
@@ -13,13 +13,16 @@
|
|
|
13
13
|
// 就什么都不做。状态栏不显示任何东西,更不弹通知。
|
|
14
14
|
// 3. **绝不覆盖未保存改动**:agent 改了文件而该文档是脏的就只告警 + 给 diff,由用户决定。
|
|
15
15
|
//
|
|
16
|
-
// 「问 DSH
|
|
17
|
-
// -
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
16
|
+
// 「问 DSH」对话框(0.2.0 起;0.3.59 起是**唯一**的提问 UI):
|
|
17
|
+
// - 提问:右键命令只向宿主上报意图(`POST /event {kind:'ask-open', mode}`),对话框开在 **DSH 页面**里;
|
|
18
|
+
// 提问正文由插件的客户端半部经 `/api/code-server/ask/send` 投递(host 侧以**用户输入**进对话);
|
|
19
|
+
// - 回答:host 用 `sessionController.follow` 把该会话的**新内容**投影成条目,对话框轮询
|
|
20
|
+
// `/api/code-server/ask/state` 取回,面板用 DSH 官方 markdown 渲染器显示 —— 面板本体在客户端
|
|
21
|
+
// 半部(lib/client.js):手写、不打包,直接用 DSH 页面模块表里那一份渲染器;
|
|
22
|
+
// - 授权(写工作区外文件 / 执行命令):host 把待决请求放进同一份状态,用户在对话框上
|
|
23
|
+
// 「允许一次 / 拒绝」,客户端半部调 `/api/code-server/ask/approve` 提交;
|
|
24
|
+
// - 编辑器里**不再有**自己的提问面板:0.3.59 删掉了那份 webview 产物(thread.js/css),
|
|
25
|
+
// 探测不到对话框心跳时只弹一条提示,不开一扇用户看不见的窗。
|
|
23
26
|
//
|
|
24
27
|
// 只依赖 `vscode` 与 Node 内置模块;纯逻辑在 lib/ 下且不 require('vscode'),便于单测。
|
|
25
28
|
|
|
@@ -27,7 +30,6 @@
|
|
|
27
30
|
|
|
28
31
|
const vscode = require('vscode');
|
|
29
32
|
const crypto = require('crypto');
|
|
30
|
-
const fs = require('fs');
|
|
31
33
|
const path = require('path');
|
|
32
34
|
|
|
33
35
|
const {
|
|
@@ -37,14 +39,6 @@ const {
|
|
|
37
39
|
} = require('./lib/bridge-client.js');
|
|
38
40
|
const { createProjector } = require('./lib/context-model.js');
|
|
39
41
|
const { chooseOldSide, createDiffCache, describeChange } = require('./lib/diff-model.js');
|
|
40
|
-
const {
|
|
41
|
-
applySync,
|
|
42
|
-
createPanelState,
|
|
43
|
-
failPanel,
|
|
44
|
-
panelPayload,
|
|
45
|
-
pendingQuestion,
|
|
46
|
-
renderPanelHtml,
|
|
47
|
-
} = require('./lib/ask-panel.js');
|
|
48
42
|
|
|
49
43
|
/** 状态栏项(仅在桥连通时显示)。 */
|
|
50
44
|
let statusBar = null;
|
|
@@ -56,16 +50,8 @@ let client = null;
|
|
|
56
50
|
let pollTimer = null;
|
|
57
51
|
/** 上次成功轮询的时间(状态栏 tooltip 用)。 */
|
|
58
52
|
let lastPollAt = 0;
|
|
59
|
-
/**
|
|
60
|
-
let askPanel = null;
|
|
61
|
-
/** 宿主是否支持 DSH 页面里的悬浮对话框(0.2.5 探测;0.3.26 起不再自动启用)。 */
|
|
53
|
+
/** 宿主是否已经把"对话框活着"证明给我们看(host 侧 askDialogLive:5 秒内收到过 /ask/state 轮询)。 */
|
|
62
54
|
let askDialogSupported = false;
|
|
63
|
-
/** 上一份对话流快照(host 只在变化时回传,这里兜住"没变化"的那些轮询)。 */
|
|
64
|
-
let lastThreadSnapshot;
|
|
65
|
-
/** 本扩展**当前持有**的对话流修订号(随请求上报;-1 = 一份都还没有 ⇒ host 必须发). */
|
|
66
|
-
let lastThreadRev = -1;
|
|
67
|
-
/** 扩展根目录(activate 时记下;面板要按它取 webview 产物 URI)。 */
|
|
68
|
-
let extensionRoot = null;
|
|
69
55
|
/** 是否已经确认过 host 端点可达(避免把"IDE 刚起、扩展先加载"误判为断线)。 */
|
|
70
56
|
let connected = false;
|
|
71
57
|
/** 诊断集合缓存:host 请求时现算,这里只做"有没有变化"的计数上报。 */
|
|
@@ -397,17 +383,6 @@ async function pollOnce() {
|
|
|
397
383
|
}),
|
|
398
384
|
diagnostics,
|
|
399
385
|
at: Date.now(),
|
|
400
|
-
// 面板声明它要看哪个会话:host 据此对齐 `sessionController.follow` 订阅,
|
|
401
|
-
// 并用"有没有人在看"决定授权请求是先问面板还是直接交给 DSH 界面
|
|
402
|
-
// (见 host 侧 lib/bridge-thread.mjs / lib/bridge-approval.mjs)。
|
|
403
|
-
watch: askPanel !== null && askPanel.state.sessionId !== null ? [askPanel.state.sessionId] : [],
|
|
404
|
-
// 声明"本扩展懂对话流修订号"(0.2.9):host 才会在没变化时省略 thread 省流量。
|
|
405
|
-
// 不声明(旧扩展)时 host 照旧发整份快照 —— 否则旧扩展会误报"宿主没有对话流能力"。
|
|
406
|
-
threadRev: lastThreadRev,
|
|
407
|
-
// 声明"我这边能把授权卡片画出来"(0.3.30):面板开着 **且** 对话流正常(不是那条"宿主没有对话流能力"
|
|
408
|
-
// 的坏状态)。宿主只在它为 true 时才敢抢答授权 —— 否则"抢过来却没人看得见"就是用户遇到的
|
|
409
|
-
// "没弹出授权"(官方卡片被我们吞了,面板又不画)。
|
|
410
|
-
approvalsUi: askPanel !== null && askPanel.state.available === true,
|
|
411
386
|
};
|
|
412
387
|
const result = await client.sync(payload);
|
|
413
388
|
if (result.ok !== true) {
|
|
@@ -429,28 +404,15 @@ async function pollOnce() {
|
|
|
429
404
|
await handleEvent(event);
|
|
430
405
|
}
|
|
431
406
|
if ((result.events ?? []).length > 0) client.persist();
|
|
432
|
-
//
|
|
407
|
+
// 能力探测(0.3.24):宿主证明"DSH 页面里的对话框活着"⇒ 右键提问走 ask-open 事件。
|
|
408
|
+
// 0.3.59 起这是**唯一**的提问 UI(编辑器里的 webview 面板已退役):探测不到就只弹一条提示,
|
|
409
|
+
// 绝不退回到一扇用户看不见的窗。
|
|
433
410
|
if (result.askDialog === true && !askDialogSupported) {
|
|
434
411
|
askDialogSupported = true;
|
|
435
|
-
log('
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
// 0.3.27 起 host **没变化就不带 thread**(省掉每 600ms 一份 ~1MB 的快照,那会把 /sync 拖成超时,
|
|
440
|
-
// 于是 approvals 也一起丢掉、卡片永远不出现)⇒ 这里沿用上一份快照。
|
|
441
|
-
if (askPanel !== null) {
|
|
442
|
-
const thread = result.thread !== undefined
|
|
443
|
-
? result.thread
|
|
444
|
-
: (result.threadRev !== undefined ? lastThreadSnapshot : undefined);
|
|
445
|
-
if (result.thread !== undefined) lastThreadSnapshot = result.thread;
|
|
446
|
-
if (Number.isSafeInteger(result.threadRev)) lastThreadRev = result.threadRev;
|
|
447
|
-
// 刚开面板、手里还没有任何快照,而 host 说"没变化"(它只看到我们持有 -1)→ 等下一份,别误报
|
|
448
|
-
// "宿主没有对话流能力"(0.3.29)。
|
|
449
|
-
if (result.thread === undefined && lastThreadSnapshot === undefined) {
|
|
450
|
-
refreshAskPanel();
|
|
451
|
-
return;
|
|
452
|
-
}
|
|
453
|
-
if (applySync(askPanel.state, { ...result, thread })) refreshAskPanel();
|
|
412
|
+
log('宿主已证明「问 DSH」对话框活着:右键提问将打开 DSH 页面里的对话框');
|
|
413
|
+
} else if (result.askDialog !== true && askDialogSupported) {
|
|
414
|
+
askDialogSupported = false;
|
|
415
|
+
log('宿主不再报告对话框心跳(页面关了/刷新过):右键提问会给出提示');
|
|
454
416
|
}
|
|
455
417
|
}
|
|
456
418
|
|
|
@@ -504,152 +466,14 @@ function captureAskContext(mode) {
|
|
|
504
466
|
};
|
|
505
467
|
}
|
|
506
468
|
|
|
507
|
-
/**
|
|
508
|
-
function
|
|
509
|
-
|
|
510
|
-
void askPanel.panel.webview.postMessage(panelPayload(askPanel.state));
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
/** 面板进入错误态:状态行 + 最新一条本地提问都标上原因(绝不静默)。 */
|
|
514
|
-
function failAskPanel(message) {
|
|
515
|
-
if (askPanel === null) return;
|
|
516
|
-
failPanel(askPanel.state, message);
|
|
469
|
+
/** 提问对话框不可用时的提示(0.3.59 起不再退回编辑器面板 —— 那一半已退役)。 */
|
|
470
|
+
function notifyAskDialogUnavailable() {
|
|
471
|
+
const message = '「问 DSH」对话框不可用:请在 DSH 页面里打开(或刷新)Code Server 标签后重试。';
|
|
517
472
|
log(message);
|
|
518
|
-
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
/** 面板收到一条提问:投递到 DSH,**本地先乐观显示**,回答由 host 的对话流回填。 */
|
|
522
|
-
async function sendAskFromPanel(text) {
|
|
523
|
-
if (askPanel === null || client === null) return;
|
|
524
|
-
// 用面板自己的意图(selection / file)取上下文:文件级提问永远不带行号。
|
|
525
|
-
const context = captureAskContext(askPanel.mode) ?? askPanel.state.context;
|
|
526
|
-
pendingQuestion(askPanel.state, text, context);
|
|
527
|
-
refreshAskPanel();
|
|
528
|
-
try {
|
|
529
|
-
const result = await client.ask({
|
|
530
|
-
text,
|
|
531
|
-
file: context === null ? null : context.file,
|
|
532
|
-
lineStart: context === null ? null : context.lineStart,
|
|
533
|
-
lineEnd: context === null ? null : context.lineEnd,
|
|
534
|
-
selection: context === null ? null : context.selection,
|
|
535
|
-
languageId: context === null ? null : context.languageId,
|
|
536
|
-
});
|
|
537
|
-
if (result.ok === true) {
|
|
538
|
-
askPanel.state.sessionId = typeof result.sessionId === 'string' ? result.sessionId : null;
|
|
539
|
-
askPanel.state.status = 'thinking';
|
|
540
|
-
log(`已投递编辑器消息(session=${result.sessionId ?? '?'})`);
|
|
541
|
-
} else {
|
|
542
|
-
failAskPanel(`未投递:${result.error ?? '未知原因'}`);
|
|
543
|
-
return;
|
|
544
|
-
}
|
|
545
|
-
} catch (error) {
|
|
546
|
-
const status = error && error.status;
|
|
547
|
-
failAskPanel(status === 409
|
|
548
|
-
? 'DSH 里没有可投递的会话:请先在 DSH 里打开或新建一个会话。'
|
|
549
|
-
: (status === 401 || status === 503
|
|
550
|
-
? '编辑器桥尚未就绪,请稍后重试。'
|
|
551
|
-
: `投递失败:${error && error.message ? error.message : error}`));
|
|
552
|
-
return;
|
|
553
|
-
}
|
|
554
|
-
refreshAskPanel();
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
/**
|
|
558
|
-
* 面板对一条授权请求的决策 → `POST /approve`。
|
|
559
|
-
*
|
|
560
|
-
* 只接受白名单的两个值(host 侧同样只认这两个);失败必须说出来:面板里点不动的时候,
|
|
561
|
-
* 用户唯一的线索就是这行状态/日志。
|
|
562
|
-
*/
|
|
563
|
-
async function decideApproval(id, outcome) {
|
|
564
|
-
if (askPanel === null || client === null) return;
|
|
565
|
-
if (outcome !== 'allowed-once' && outcome !== 'rejected') return;
|
|
566
|
-
try {
|
|
567
|
-
await client.approve(id, outcome);
|
|
568
|
-
log(`面板授权决策已提交:${id} → ${outcome}`);
|
|
569
|
-
} catch (error) {
|
|
570
|
-
const status = error && error.status;
|
|
571
|
-
const message = status === 409
|
|
572
|
-
? '这条授权请求已经过期或已被处理(DSH 界面里会弹同一张卡片)。'
|
|
573
|
-
: (status === 401 || status === 503
|
|
574
|
-
? '编辑器桥尚未就绪,授权没有提交。'
|
|
575
|
-
: `授权提交失败:${error && error.message ? error.message : error}`);
|
|
576
|
-
// 卡片已经没意义了:本地撤掉,避免用户反复点。
|
|
577
|
-
askPanel.state.approvals = askPanel.state.approvals.filter((item) => item.id !== id);
|
|
578
|
-
failAskPanel(message);
|
|
579
|
-
return;
|
|
580
|
-
}
|
|
581
|
-
refreshAskPanel();
|
|
473
|
+
void vscode.window.showInformationMessage(message);
|
|
582
474
|
}
|
|
583
475
|
|
|
584
|
-
/**
|
|
585
|
-
* 打开(或聚焦)「问 DSH」面板。
|
|
586
|
-
* @param {'selection'|'file'} mode 提问意图(决定发送时带不带行号/选区),见 captureAskContext。
|
|
587
|
-
*/
|
|
588
|
-
function openAskPanel(mode = 'selection') {
|
|
589
|
-
const contextInfo = captureAskContext(mode);
|
|
590
|
-
if (askPanel === null) {
|
|
591
|
-
// webview 产物是构建出来的(scripts/build-webview.mjs)。缺了就别开一个空白面板。
|
|
592
|
-
const webviewDir = vscode.Uri.joinPath(extensionRoot, 'webview');
|
|
593
|
-
const bundle = vscode.Uri.joinPath(webviewDir, 'thread.js');
|
|
594
|
-
const style = vscode.Uri.joinPath(webviewDir, 'thread.css');
|
|
595
|
-
if (!fs.existsSync(bundle.fsPath) || !fs.existsSync(style.fsPath)) {
|
|
596
|
-
const message = `面板产物缺失(${bundle.fsPath}):请重新安装插件,或在源码里跑 pnpm run build:webview`;
|
|
597
|
-
log(message);
|
|
598
|
-
void vscode.window.showErrorMessage(message);
|
|
599
|
-
return;
|
|
600
|
-
}
|
|
601
|
-
const panel = vscode.window.createWebviewPanel(
|
|
602
|
-
'dshAsk',
|
|
603
|
-
'DSH 对话',
|
|
604
|
-
// 对话框形状(0.2.4):开在**当前编辑器组**,占满工作台宽度 —— 不再用 `Beside` 挤成一条侧栏。
|
|
605
|
-
// 面板内容自己居中限宽,看起来就是编辑器上的一扇对话窗;右上角的 ✕ 关掉它。
|
|
606
|
-
{ viewColumn: vscode.ViewColumn.Active, preserveFocus: false },
|
|
607
|
-
{
|
|
608
|
-
enableScripts: true,
|
|
609
|
-
retainContextWhenHidden: true,
|
|
610
|
-
// 只允许加载扩展自己的 webview 目录(产物 + 字体),不给别的本地文件开口子。
|
|
611
|
-
localResourceRoots: [webviewDir],
|
|
612
|
-
},
|
|
613
|
-
);
|
|
614
|
-
askPanel = { panel, state: createPanelState(), mode };
|
|
615
|
-
panel.webview.html = renderPanelHtml({
|
|
616
|
-
cspSource: panel.webview.cspSource,
|
|
617
|
-
nonce: crypto.randomBytes(16).toString('base64url'),
|
|
618
|
-
scriptUri: panel.webview.asWebviewUri(bundle).toString(),
|
|
619
|
-
styleUri: panel.webview.asWebviewUri(style).toString(),
|
|
620
|
-
});
|
|
621
|
-
panel.webview.onDidReceiveMessage((message) => {
|
|
622
|
-
if (message === null || typeof message !== 'object') return;
|
|
623
|
-
if (message.type === 'ready') {
|
|
624
|
-
refreshAskPanel();
|
|
625
|
-
return;
|
|
626
|
-
}
|
|
627
|
-
if (message.type === 'ask' && typeof message.text === 'string') {
|
|
628
|
-
const text = message.text.trim();
|
|
629
|
-
if (text !== '') void sendAskFromPanel(text);
|
|
630
|
-
return;
|
|
631
|
-
}
|
|
632
|
-
if (message.type === 'approve' && typeof message.id === 'string') {
|
|
633
|
-
void decideApproval(message.id, message.outcome);
|
|
634
|
-
return;
|
|
635
|
-
}
|
|
636
|
-
if (message.type === 'close') {
|
|
637
|
-
// 面板上的 ✕:关掉它(= 不再看这个会话 ⇒ 待决授权立刻交回 DSH 界面,不用干等窗口)。
|
|
638
|
-
panel.dispose();
|
|
639
|
-
}
|
|
640
|
-
});
|
|
641
|
-
panel.onDidDispose(() => {
|
|
642
|
-
askPanel = null;
|
|
643
|
-
});
|
|
644
|
-
}
|
|
645
|
-
askPanel.state.context = contextInfo;
|
|
646
|
-
askPanel.mode = mode;
|
|
647
|
-
// 已经开着就提到前面(对话框不重复开)。
|
|
648
|
-
askPanel.panel.reveal(vscode.ViewColumn.Active, false);
|
|
649
|
-
refreshAskPanel();
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
/** 选中内容 → DSH(编辑器→DSH 的主入口):打开面板并带上当前上下文。 */
|
|
476
|
+
/** 选中内容 → DSH(编辑器→DSH 的主入口):请宿主打开对话框并带上当前上下文。 */
|
|
653
477
|
async function askAboutSelection() {
|
|
654
478
|
await openAskPanelFor('selection');
|
|
655
479
|
}
|
|
@@ -659,7 +483,7 @@ async function askAboutFile() {
|
|
|
659
483
|
await openAskPanelFor('file');
|
|
660
484
|
}
|
|
661
485
|
|
|
662
|
-
/** 两个命令的公共前置检查(桥可用 + 有活动编辑器)。 */
|
|
486
|
+
/** 两个命令的公共前置检查(桥可用 + 有活动编辑器 + 对话框活着)。 */
|
|
663
487
|
async function openAskPanelFor(mode) {
|
|
664
488
|
if (client === null || client.isDormant()) {
|
|
665
489
|
vscode.window.showInformationMessage('编辑器桥未启用:请在 DSH 里打开 Code Server 标签后重试。');
|
|
@@ -669,25 +493,27 @@ async function openAskPanelFor(mode) {
|
|
|
669
493
|
vscode.window.showInformationMessage('没有活动的编辑器:请先打开一个文件。');
|
|
670
494
|
return;
|
|
671
495
|
}
|
|
672
|
-
// 0.3.
|
|
673
|
-
//
|
|
674
|
-
//
|
|
675
|
-
//
|
|
676
|
-
//
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
} catch (error) {
|
|
687
|
-
log('ask-open 失败(' + (error && error.message ? error.message : error) + ') → 退回编辑器面板');
|
|
496
|
+
// 0.3.59:提问 UI 只有一处 —— **DSH 页面里的悬浮对话框**(面板本体在插件客户端半部:手写、不打包,
|
|
497
|
+
// 直接用页面模块表里的官方渲染器)。编辑器里的 webview 面板已退役:那份 1.4MB 产物 + esbuild +
|
|
498
|
+
// 十几个打包依赖,只为了兜底一条"宿主证明不了对话框活着"的路 —— 而那种情况下用户在编辑器里
|
|
499
|
+
// 看到的其实也不是他想要的对话(而且我们在 0.3.30 之后就从没走到过那条路)。
|
|
500
|
+
// 所以现在:拿不到心跳就**明确说清怎么办**,不再开一扇看不见的窗。
|
|
501
|
+
if (!askDialogSupported) {
|
|
502
|
+
notifyAskDialogUnavailable();
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
try {
|
|
506
|
+
const result = await client.askOpen(mode);
|
|
507
|
+
if (result !== null && result.ok === true) {
|
|
508
|
+
log(`已请宿主打开「问 DSH」对话框(mode=${mode})`);
|
|
509
|
+
return;
|
|
688
510
|
}
|
|
511
|
+
log(`宿主没有打开对话框(${result !== null && result.error ? result.error : '未知原因'})`);
|
|
512
|
+
notifyAskDialogUnavailable();
|
|
513
|
+
} catch (error) {
|
|
514
|
+
log(`ask-open 失败(${error && error.message ? error.message : error})`);
|
|
515
|
+
notifyAskDialogUnavailable();
|
|
689
516
|
}
|
|
690
|
-
openAskPanel(mode);
|
|
691
517
|
}
|
|
692
518
|
|
|
693
519
|
function showBridgeLog() {
|
|
@@ -700,9 +526,6 @@ function activate(context) {
|
|
|
700
526
|
output = vscode.window.createOutputChannel('DSH Editor Bridge');
|
|
701
527
|
context.subscriptions.push(output);
|
|
702
528
|
|
|
703
|
-
// 面板 webview 的产物按扩展根目录取 URI(0.2.3 起正文走官方渲染器,产物是打包出来的)。
|
|
704
|
-
extensionRoot = context.extensionUri;
|
|
705
|
-
|
|
706
529
|
statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 90);
|
|
707
530
|
context.subscriptions.push(statusBar);
|
|
708
531
|
|
|
@@ -285,20 +285,12 @@ function createClient(options) {
|
|
|
285
285
|
// 症状就是"每个 IDE 会话只收到第一条 diff")。退回 0 重新对齐即可自愈。
|
|
286
286
|
const lastSeq = body !== null && Number.isSafeInteger(body.lastSeq) ? body.lastSeq : null;
|
|
287
287
|
if (lastSeq !== null && lastSeq < since) since = 0;
|
|
288
|
-
// 0.
|
|
289
|
-
//
|
|
290
|
-
//
|
|
291
|
-
// - approvals:待决授权请求(工作区外写入 / 命令执行),面板就地作答;
|
|
292
|
-
// - uiVersion:DSH 界面的版本(面板据此提示"渲染器与界面版本不一致")。
|
|
293
|
-
// 全部原样透传,解析与合并交给 lib/ask-panel.js 的纯模型。
|
|
288
|
+
// 0.3.59:对话流 / 待决授权**不再经这条通道**(那是编辑器 webview 面板时代的分工)。
|
|
289
|
+
// 现在唯一的面板在 DSH 页面里、由插件的客户端半部直接读 `/api/code-server/ask/state`,
|
|
290
|
+
// 扩展这边只留下能力位:宿主有没有证明"对话框活着"。
|
|
294
291
|
return {
|
|
295
292
|
ok: true,
|
|
296
293
|
events,
|
|
297
|
-
thread: body !== null && body.thread !== undefined ? body.thread : undefined,
|
|
298
|
-
approvals: body !== null && Array.isArray(body.approvals) ? body.approvals : [],
|
|
299
|
-
approvalHoldMs: body !== null && Number.isSafeInteger(body.approvalHoldMs) ? body.approvalHoldMs : undefined,
|
|
300
|
-
uiVersion: body !== null && typeof body.uiVersion === 'string' ? body.uiVersion : undefined,
|
|
301
|
-
// 能力探测(0.3.24):宿主支持 DSH 页面里的悬浮对话框 ⇒ 提问改走 ask-open 事件(不开 webview 面板)。
|
|
302
294
|
askDialog: body !== null && body.askDialog === true,
|
|
303
295
|
};
|
|
304
296
|
} catch (error) {
|
|
@@ -316,35 +308,11 @@ function createClient(options) {
|
|
|
316
308
|
body: JSON.stringify({ kind: 'ask-open', mode: mode === 'file' ? 'file' : 'selection' }),
|
|
317
309
|
});
|
|
318
310
|
},
|
|
319
|
-
/** 把"选中内容 + 问题"投给 DSH 的当前会话。 */
|
|
320
|
-
async ask(payload) {
|
|
321
|
-
return request(`${BRIDGE_BASE}/ask`, {
|
|
322
|
-
method: 'POST',
|
|
323
|
-
headers: { 'content-type': 'application/json' },
|
|
324
|
-
body: JSON.stringify(payload),
|
|
325
|
-
});
|
|
326
|
-
},
|
|
327
|
-
/**
|
|
328
|
-
* 回答一条待决的授权请求(桥里**唯一**的非只读路由)。
|
|
329
|
-
*
|
|
330
|
-
* 约束在 host 侧写死:只认本进程发起、仍未决的 id(单次使用),outcome 只接受
|
|
331
|
-
* `allowed-once` / `rejected`,不接受任何自由文本 —— 这条路由只能"回答问题",不能"发起动作"。
|
|
332
|
-
*
|
|
333
|
-
* @param {string} id 授权请求 id(来自 /sync 的 approvals)
|
|
334
|
-
* @param {'allowed-once'|'rejected'} outcome
|
|
335
|
-
*/
|
|
336
|
-
async approve(id, outcome) {
|
|
337
|
-
return request(`${BRIDGE_BASE}/approve`, {
|
|
338
|
-
method: 'POST',
|
|
339
|
-
headers: { 'content-type': 'application/json' },
|
|
340
|
-
body: JSON.stringify({ id, outcome }),
|
|
341
|
-
});
|
|
342
|
-
},
|
|
343
311
|
/**
|
|
344
312
|
* 取一份"写前原文"快照(0.3.55):事件里带的是不透明 key,文本单独取。
|
|
345
313
|
*
|
|
346
|
-
*
|
|
347
|
-
*
|
|
314
|
+
* 为什么不把文本塞进事件:一趟 /sync 要驮的东西越少越好 —— 塞进 ~1MB 文本会把那一趟拖成超时,
|
|
315
|
+
* 而超时意味着事件也一起丢(用户看到的症状就是"diff 不弹")。
|
|
348
316
|
*
|
|
349
317
|
* 取不到(404 = 过期/被淘汰/宿主重启)时**返回 null 而不是抛**:调用方据此回退到缓冲区或缓存。
|
|
350
318
|
*
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dshcs-editor-bridge",
|
|
3
3
|
"displayName": "DSH Editor Bridge",
|
|
4
|
-
"description": "只读地把编辑器状态(未保存缓冲区、诊断、活动选区)提供给 DSH;右键提问打开 DSH 页面里的悬浮对话对话框(0.
|
|
5
|
-
"version": "0.2.
|
|
4
|
+
"description": "只读地把编辑器状态(未保存缓冲区、诊断、活动选区)提供给 DSH;右键提问打开 DSH 页面里的悬浮对话对话框(0.3.59 起是唯一路径:面板在插件客户端半部,正文/思考/上下文都用页面模块表里的官方渲染器,并可就地回答授权请求;拿不到对话框心跳时只给一条提示)。由 dsh-code-server-app 插件安装,可禁用。",
|
|
5
|
+
"version": "0.2.18",
|
|
6
6
|
"publisher": "dsh-code-server-app",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"private": true,
|
package/lib/bridge-session.mjs
CHANGED
|
@@ -140,6 +140,37 @@ export function pickAgent(ctx) {
|
|
|
140
140
|
return null;
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
/**
|
|
144
|
+
* 忙碌时按 Enter 的投递方式 —— **跟着 DSH 自己的设置走**。
|
|
145
|
+
*
|
|
146
|
+
* DSH 的对话插件把这个偏好放在 `ui-conversation` 命名空间的 `busyEnter` 字段
|
|
147
|
+
* (`dsh-client-ui-conversation` 契约:`BUSY_ENTER_BEHAVIORS = ['queue','steer']`,默认 `'queue'`),
|
|
148
|
+
* 用户在 设置 → 对话 里改的就是它。面板里按 Enter 与主界面按 Enter 是同一个手势,所以这里读同一个值:
|
|
149
|
+
* - `'steer'` ⇒ `agent.steer(msg)`(DSH 契约:"an idle driver starts a turn; a running driver
|
|
150
|
+
* consumes it at its **next step boundary**")—— 正在跑的那一轮就能看到你的追问;
|
|
151
|
+
* - `'queue'` ⇒ `agent.followup(msg)`(契约:"becomes the sole ordinary message of **its own turn**")
|
|
152
|
+
* —— 排到下一轮,不打扰当前轮。
|
|
153
|
+
*
|
|
154
|
+
* 读法走官方 API `ctx.settings.get(ns)`(宿主侧 `@deepseek-ai/dsh-settings` 的契约:
|
|
155
|
+
* "Read one registered namespace's resolved value")。读不到(命名空间没注册 / 极简组合 / 老版本 DSH)
|
|
156
|
+
* 一律回默认 `'queue'`,并且**逐项特性探测** `agent.steer` 是否存在 —— 探测失败绝不抛。
|
|
157
|
+
*
|
|
158
|
+
* @param {object} ctx cordis 上下文
|
|
159
|
+
* @returns {'queue'|'steer'}
|
|
160
|
+
*/
|
|
161
|
+
export function pickBusyEnter(ctx) {
|
|
162
|
+
const settings = getService(ctx, 'settings');
|
|
163
|
+
if (settings === undefined || settings === null || typeof settings.get !== 'function') return 'queue';
|
|
164
|
+
try {
|
|
165
|
+
const section = settings.get('ui-conversation');
|
|
166
|
+
const value = section !== null && typeof section === 'object' ? section.busyEnter : undefined;
|
|
167
|
+
return value === 'steer' ? 'steer' : 'queue';
|
|
168
|
+
} catch {
|
|
169
|
+
// 未注册的命名空间在多数实现里是 undefined,但契约允许实现抛 —— 一律当默认值
|
|
170
|
+
return 'queue';
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
143
174
|
/**
|
|
144
175
|
* 投递一条编辑器消息。
|
|
145
176
|
*
|
|
@@ -148,9 +179,10 @@ export function pickAgent(ctx) {
|
|
|
148
179
|
*
|
|
149
180
|
* @param {object} ctx cordis 上下文
|
|
150
181
|
* @param {object} input `composeEditorPrompt` 的输入
|
|
151
|
-
* @
|
|
182
|
+
* @param {{delivery?: 'queue'|'steer'}} [options] 投递方式(缺省按 DSH 设置推导,见 pickBusyEnter)
|
|
183
|
+
* @returns {Promise<{ok: boolean, code?: string, error?: string, sessionId?: string, text?: string, delivery?: string}>}
|
|
152
184
|
*/
|
|
153
|
-
export async function deliverEditorPrompt(ctx, input) {
|
|
185
|
+
export async function deliverEditorPrompt(ctx, input, options = {}) {
|
|
154
186
|
const createUserMessage = await loadDshExport('@deepseek-ai/dsh-llm', 'createUserMessage');
|
|
155
187
|
if (typeof createUserMessage !== 'function') {
|
|
156
188
|
return { ok: false, code: 'NO_LLM', error: '解析不到 @deepseek-ai/dsh-llm 的 createUserMessage(DSH 部署不完整?)' };
|
|
@@ -174,9 +206,24 @@ export async function deliverEditorPrompt(ctx, input) {
|
|
|
174
206
|
} catch (err) {
|
|
175
207
|
return { ok: false, code: 'BAD_MESSAGE', error: `构造消息失败:${err && err.message ? err.message : String(err)}` };
|
|
176
208
|
}
|
|
209
|
+
// 请求方可以显式指定(queue/steer);没指定就跟着 DSH 的 `ui-conversation.busyEnter` 走。
|
|
210
|
+
const requested = options.delivery === 'steer' || options.delivery === 'queue' ? options.delivery : null;
|
|
211
|
+
const busy = agent.status === 'running';
|
|
212
|
+
let delivery = requested ?? pickBusyEnter(ctx);
|
|
213
|
+
// 空闲时两者等价(DSH 契约:steer 在空闲驱动上同样"starts a turn"),但**空闲一律走 followup** ——
|
|
214
|
+
// 语义最直白,也不依赖 steer 存在。
|
|
215
|
+
if (!busy) delivery = 'queue';
|
|
216
|
+
// steer 是 0.3.59 才用到的 API:老宿主没有它时静默退回 queue,绝不因为"设置里写了 steer"就投不出去。
|
|
217
|
+
if (delivery === 'steer' && typeof agent.steer !== 'function') delivery = 'queue';
|
|
177
218
|
try {
|
|
178
|
-
|
|
179
|
-
|
|
219
|
+
if (delivery === 'steer') {
|
|
220
|
+
// steer = "Submit steering for the nearest step":正在跑的那一轮会在下一个 step 边界领取它,
|
|
221
|
+
// 所以追问能当轮被看到(而不是等下一轮)。
|
|
222
|
+
agent.steer(message);
|
|
223
|
+
} else {
|
|
224
|
+
// followup = 下一轮 + 唤醒:agent 空闲就立刻开一轮,在跑就排队,两种情况用户都能看到。
|
|
225
|
+
agent.followup(message);
|
|
226
|
+
}
|
|
180
227
|
} catch (err) {
|
|
181
228
|
return { ok: false, code: 'DELIVER_FAILED', error: `投递失败:${err && err.message ? err.message : String(err)}` };
|
|
182
229
|
}
|
|
@@ -185,5 +232,8 @@ export async function deliverEditorPrompt(ctx, input) {
|
|
|
185
232
|
: (agent.id ?? null);
|
|
186
233
|
// `agent` 是给调用方用的**内部句柄**(授权拦截要拿 `agent.ctx` 注册 agent 作用域监听),
|
|
187
234
|
// 路由返回前必须剥掉:它不可 JSON 序列化,也不该泄漏进编辑器。
|
|
188
|
-
|
|
235
|
+
// `delivery` + `busy` 一并回去:面板据此说清这条追问会怎么被处理 ——
|
|
236
|
+
// 只看 delivery 会把**空闲**时的 followup 误报成"排到下一轮"(实测踩到:空闲时发一条,
|
|
237
|
+
// 面板却写"当前轮结束后…",而当时根本没有当前轮,消息是立刻开的新一轮)。
|
|
238
|
+
return { ok: true, sessionId, text, delivery, busy, agent };
|
|
189
239
|
}
|
package/lib/bridge.mjs
CHANGED
|
@@ -29,18 +29,22 @@
|
|
|
29
29
|
* 挂 `webServer` 前缀(0.3.9–0.3.12)则只有 web profile 有 webServer,desktop 根本没有 HTTP 面。
|
|
30
30
|
* 桥两端本来就是同一台机器上的两个进程,所以传输用本机 IPC,鉴权仍走独立令牌:
|
|
31
31
|
*
|
|
32
|
-
* ## 安全不变量(
|
|
32
|
+
* ## 安全不变量(改这个文件之前先读这几条)
|
|
33
33
|
*
|
|
34
|
-
* 1. **`BRIDGE_BASE`(`/code-server-bridge`)
|
|
35
|
-
* (
|
|
36
|
-
* `
|
|
37
|
-
*
|
|
38
|
-
*
|
|
34
|
+
* 1. **`BRIDGE_BASE`(`/code-server-bridge`)命名空间**只有四条路由,而且**全部只读**:
|
|
35
|
+
* `/health`(无鉴权探活)、`/sync`(上报编辑器状态 + 取事件)、`/old`(取写前原文快照)、
|
|
36
|
+
* `/event`(上报意图,如"请宿主打开对话框")。这里**不允许**出现写文件、改文档、执行命令、
|
|
37
|
+
* 拉起进程的路由 —— `bridge.json` 里那个令牌对本机同用户进程可读,爆炸半径必须封在
|
|
38
|
+
* "泄露编辑器里的信息",绝不能变成任意文件写 / 任意命令执行。
|
|
39
|
+
* 提问与授权答复不走这里:它们在 DSH 同源的 `/api/code-server/ask/*` 上,调用方是 DSH 页面里的
|
|
40
|
+
* 插件客户端(吃 DSH 自己的 cookie/Origin 校验)。
|
|
41
|
+
* 2. **授权答复的四条约束**(在 `/api/code-server/ask/approve` 上,缺一条就等于开了任意命令执行的后门,
|
|
42
|
+
* 不许放宽):
|
|
39
43
|
* (a) 它只能**回答**已经存在的授权请求,不能发起任何动作 —— 请求体只有 `{id, outcome}`,
|
|
40
44
|
* 不接受自由文本 / 路径 / 命令参数;
|
|
41
45
|
* (b) `id` 必须是本进程自己发起、且**仍未决**的请求(用后即废,单次使用);
|
|
42
46
|
* (c) `outcome` 只接受 `allowed-once` / `rejected` 两个白名单值,没有"永久允许";
|
|
43
|
-
* (d)
|
|
47
|
+
* (d) 对话框没在看 / 窗口超时(默认 5 分钟)→ **交回官方链路**,绝不自动放行(DSH 的
|
|
44
48
|
* `approval/request` 本身 fail closed,这里只能把"没人答"保持成"没人答")。
|
|
45
49
|
* 3. **带 `Origin` 的请求一律 403。** 浏览器发起的请求必带 Origin,扩展宿主进程不带。
|
|
46
50
|
* 这条只排除浏览器,不误伤扩展(也顺带挡住 CSRF / DNS rebinding 这类"用你的浏览器打本机端口")。
|
package/lib/claim-types.js
CHANGED
|
@@ -12,8 +12,10 @@
|
|
|
12
12
|
* !md 不认领 .md(排除优先于认领)
|
|
13
13
|
* 空文本 = 不认领任何文件类型(只保留页面 tab,文件点击全部落回 DSH 预览)。
|
|
14
14
|
*
|
|
15
|
-
* 本模块是 host(`lib/index.js` 的 Config
|
|
16
|
-
*
|
|
15
|
+
* 本模块是 host(`lib/index.js` 的 Config 默认值与工具实现)的**事实来源**,随 npm 包一起发布
|
|
16
|
+
* (见 package.json 的 files),避免两边默认值漂移。
|
|
17
|
+
* 客户端半部 `lib/client.js` 里有一份**内联副本**(经典脚本拿不到 host 模块,见那个文件的
|
|
18
|
+
* "认领类型"段),两份逐字等价由 scripts/test-client-entry.mjs 的 E2 守卫钉住 —— 改这里必须同步改那边。
|
|
17
19
|
* 纯字符串逻辑,不碰文件系统 → 可离线单测(scripts/test-claim-types.mjs)。
|
|
18
20
|
*/
|
|
19
21
|
|