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
package/lib/index.js
CHANGED
|
@@ -337,6 +337,31 @@ export function installBundledExtensions(extensionsDir, userDataDir, options = {
|
|
|
337
337
|
}
|
|
338
338
|
};
|
|
339
339
|
walkDst('');
|
|
340
|
+
// 文件删完还要**收掉空目录**:0.3.59 删掉了整个 webview/ 子目录,只删文件的话会在用户的
|
|
341
|
+
// VS Code 树里留下一串空目录(实测:webview/、webview/src、webview/fonts 各 0 字节)。
|
|
342
|
+
// 自底向上收,只收"确实空了"的目录(非空的目录说明里面有别的东西,不动)。
|
|
343
|
+
const pruneEmptyDirs = (rel) => {
|
|
344
|
+
const abs = rel === '' ? dst : path.join(dst, rel);
|
|
345
|
+
let entries;
|
|
346
|
+
try {
|
|
347
|
+
entries = fs.readdirSync(abs, { withFileTypes: true });
|
|
348
|
+
} catch {
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
for (const ent of entries) {
|
|
352
|
+
if (ent.isDirectory()) pruneEmptyDirs(rel === '' ? ent.name : `${rel}/${ent.name}`);
|
|
353
|
+
}
|
|
354
|
+
if (rel === '') return;
|
|
355
|
+
try {
|
|
356
|
+
if (fs.readdirSync(abs).length === 0) {
|
|
357
|
+
fs.rmdirSync(abs);
|
|
358
|
+
removed.push(`${rel}/`);
|
|
359
|
+
}
|
|
360
|
+
} catch {
|
|
361
|
+
// 竞态(别的东西刚写进来)或权限问题:保持原样,不影响主流程
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
pruneEmptyDirs('');
|
|
340
365
|
}
|
|
341
366
|
// 清理放错位置的副本:编辑器桥的旧用户级副本(0.3.11 及更早)/ dshcs-open-file 的旧用户级副本。
|
|
342
367
|
const wrong = extensionTarget(extensionsDir, ext.name, ext.placement === 'builtin' ? 'user' : 'builtin', treeRoot);
|
|
@@ -755,22 +780,19 @@ export async function apply(ctx, config) {
|
|
|
755
780
|
const bridgeApprovalBoard = createApprovalBoard();
|
|
756
781
|
/**
|
|
757
782
|
* 谁在"看着这个会话" —— 授权拦截据此决定要不要抢答(没人看就直接交给官方链路):
|
|
758
|
-
* - `ids`:编辑器扩展面板声明要看的会话(每趟 /sync 上报);
|
|
759
783
|
* - `dialogIds`:DSH 页面里的对话框(0.3.24)声明要看的会话;
|
|
760
|
-
* - `at`:最近一次声明的时间(
|
|
784
|
+
* - `at`:最近一次声明的时间(诊断用)。
|
|
785
|
+
*
|
|
786
|
+
* 0.3.59:编辑器扩展的 webview 面板退役后只剩对话框一路 —— 以前还有 `ids`(扩展每趟 /sync 上报的
|
|
787
|
+
* 观看列表),两个来源求并集、还要按心跳新鲜度区分,现在都不需要了。
|
|
761
788
|
*/
|
|
762
|
-
const bridgeWatch = {
|
|
763
|
-
/** 上次回给扩展的对话流修订号(0.3.27:没变化就不重传整份快照)。 */
|
|
764
|
-
let bridgeThreadRevSent = -1;
|
|
789
|
+
const bridgeWatch = { dialogIds: [], at: 0 };
|
|
765
790
|
/** 上次记录过的"有没有人在看"(0.3.30:状态翻转才写诊断日志)。 */
|
|
766
791
|
let bridgeWatchLogged = false;
|
|
767
792
|
/** 首帧是否已写过诊断(0.3.31:"一直没人看"也要留下证据)。 */
|
|
768
793
|
let bridgeWatchWroteFirst = false;
|
|
769
794
|
/** 上次写过的 hasPanel 判据原文(变了才写,避免每 600ms 刷屏)。 */
|
|
770
795
|
let bridgeHasPanelLine = '';
|
|
771
|
-
/** 客户端(self-declared)是否能把授权卡片画出来(0.3.30)。 */
|
|
772
|
-
let bridgeApprovalsUi = false;
|
|
773
|
-
let bridgeApprovalsUiLogged = false;
|
|
774
796
|
|
|
775
797
|
/**
|
|
776
798
|
* 授权/看护状态的**诊断日志**(0.3.30):写到 <home>/.dsh/code-server/bridge-approval.log。
|
|
@@ -795,36 +817,30 @@ export async function apply(ctx, config) {
|
|
|
795
817
|
|
|
796
818
|
/** 当前所有"在看"的会话(去重)。 */
|
|
797
819
|
function watchedSessions() {
|
|
798
|
-
|
|
799
|
-
return [...new Set(all.filter((id) => typeof id === 'string' && id !== ''))].slice(0, 4);
|
|
820
|
+
return [...new Set(bridgeWatch.dialogIds.filter((id) => typeof id === 'string' && id !== ''))].slice(0, 4);
|
|
800
821
|
}
|
|
801
822
|
/** 现在有没有人在看(授权拦截的唯一判据)。 */
|
|
802
823
|
function hasWatcher() {
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
// **对话框是宿主侧状态**(只在 /ask/close 时消失):不拿轮询新鲜度去判它 ——
|
|
806
|
-
// 浏览器把后台标签的定时器节流到分钟级时,900ms 的心跳一断、5 秒窗口就过期,
|
|
824
|
+
// 0.3.59 起只有对话框一路。**对话框是宿主侧状态**(只在 /ask/close 时消失),所以不拿轮询新鲜度
|
|
825
|
+
// 去判它 —— 浏览器把后台标签的定时器节流到分钟级时,900ms 的心跳一断、5 秒窗口就过期,
|
|
807
826
|
// 我们会误判"没人看"而放弃拦截(用户实测:授权卡片因此跑到 DSH 界面)。
|
|
808
|
-
|
|
809
|
-
// 编辑器面板靠 /sync 的 600ms 心跳;窗口放宽到 60 秒(正常心跳远小于它)。
|
|
810
|
-
return Date.now() - bridgeWatch.at < 60000;
|
|
827
|
+
return watchedSessions().length > 0;
|
|
811
828
|
}
|
|
812
829
|
|
|
813
830
|
const bridgeApproval = createApprovalInterceptor({
|
|
814
831
|
board: bridgeApprovalBoard,
|
|
815
832
|
holdMs: DEFAULT_HOLD_MS,
|
|
816
|
-
//
|
|
833
|
+
// **只在对话框真的能显示卡片时才抢答**(0.3.30 / 0.3.59 收紧)。
|
|
817
834
|
// 反例(用户实测):面板处于"宿主没有对话流能力"的坏状态时我们仍然抢答 ⇒ 官方卡片不出现、
|
|
818
835
|
// 面板也不画 ⇒ 用户看到的是"没弹出授权",请求在宿主里干等 5 分钟。
|
|
819
|
-
//
|
|
820
|
-
//
|
|
821
|
-
// 编辑器面板是 null ⇒ 旧判据误判成"画不出卡片",授权被让给了 DSH 界面。
|
|
836
|
+
// 0.3.59 起编辑器面板退役,"能显示卡片"只剩**对话框开着 + 有人看 + 客户端半部在轮询**一条路
|
|
837
|
+
// (客户端自己声明 approvalsUi 那条也随面板一起删了 —— 声明者已经不存在)。
|
|
822
838
|
hasPanel: () => {
|
|
823
839
|
const watcher = hasWatcher();
|
|
824
840
|
const dialogCapable = askDialogLive();
|
|
825
|
-
const verdict = watcher &&
|
|
841
|
+
const verdict = watcher && dialogCapable;
|
|
826
842
|
// 判据的**原始值**一起落日志(0.3.35):只记结论的话,出问题还是只能猜。
|
|
827
|
-
const line = 'hasPanel -> ' + verdict + ' (hasWatcher=' + watcher
|
|
843
|
+
const line = 'hasPanel -> ' + verdict + ' (hasWatcher=' + watcher
|
|
828
844
|
+ ' dialogLive=' + dialogCapable + ' polls=' + askDialog.polls + ' open=' + askDialog.open
|
|
829
845
|
+ ' session=' + String(askDialog.sessionId) + ')';
|
|
830
846
|
if (line !== bridgeHasPanelLine) { bridgeHasPanelLine = line; bridgeDiag(line); }
|
|
@@ -839,11 +855,15 @@ export async function apply(ctx, config) {
|
|
|
839
855
|
/**
|
|
840
856
|
* 「问 DSH」对话框的宿主侧状态(0.3.24)。
|
|
841
857
|
*
|
|
842
|
-
*
|
|
843
|
-
* 面板是编辑器里的一个 tab/一列,而用户要的是"像 DSH 那样的对话框"。host 这边只负责:
|
|
858
|
+
* 对话只有一个宿主:**DSH 页面里的浮动对话框**(client 半部,见 lib/client.js)。host 这边只负责:
|
|
844
859
|
* - 打开/关闭状态(编辑器扩展右键 → 桥 `/event` 上报 `ask-open`);
|
|
845
860
|
* - 状态与动作的 HTTP 面(`/api/code-server/ask/*`,与 DSH 同源、吃它的 cookie/Origin 校验);
|
|
846
|
-
* -
|
|
861
|
+
* - 把"被看的会话"投影成有界条目流(见 lib/bridge-thread.mjs)与待决授权快照。
|
|
862
|
+
*
|
|
863
|
+
* **0.3.59 起不再往页面里送产物,也不再服务编辑器面板**:面板就在 `lib/client.js` 里,渲染器走
|
|
864
|
+
* DSH 页面的模块表(`react-dom/client` + `@deepseek-ai/dsh-client-ui-primitives`);
|
|
865
|
+
* `/ask/bundle` 路由、`askBundle()` 与编辑器里的 webview 面板在同一版里一起删掉
|
|
866
|
+
* (thread.js / thread.css / build-webview.mjs / esbuild 那一整套)。
|
|
847
867
|
*/
|
|
848
868
|
const askDialog = { open: false, armed: false, mode: 'selection', at: 0, sessionId: null, polls: 0, lastPollAt: 0 };
|
|
849
869
|
|
|
@@ -852,8 +872,8 @@ export async function apply(ctx, config) {
|
|
|
852
872
|
*
|
|
853
873
|
* 0.3.24 的教训:宿主无条件报 `askDialog: true`,扩展于是不再开编辑器面板;而客户端半部只要没跑起来
|
|
854
874
|
* (浏览器还缓存着旧 bundle / 没硬刷新),就**没有任何一方在看这个会话** ⇒ 授权悄悄落到 DSH 界面的
|
|
855
|
-
* 官方卡片,用户在编辑器里什么都看不到。现在只有**真的收到过 `/ask/state` 轮询**(
|
|
856
|
-
*
|
|
875
|
+
* 官方卡片,用户在编辑器里什么都看不到。现在只有**真的收到过 `/ask/state` 轮询**(10 分钟内)才算有
|
|
876
|
+
* 这个能力;0.3.59 起不满足时扩展也不再退编辑器面板(那一半已退役),而是明确提示用户刷新/打开页面。
|
|
857
877
|
*/
|
|
858
878
|
function askDialogLive() {
|
|
859
879
|
// 对话框每 900ms 问一次 /ask/state —— 那就是"活跃心跳"。心跳一旦建立,在 TTL 内
|
|
@@ -922,7 +942,8 @@ export async function apply(ctx, config) {
|
|
|
922
942
|
statusText: '',
|
|
923
943
|
error: null,
|
|
924
944
|
sessionId: snapshot.sessionId ?? askDialog.sessionId ?? null,
|
|
925
|
-
uiVersion
|
|
945
|
+
// 0.3.59:`uiVersion` 只服务过"面板内置渲染器与界面版本比对"那行告警 —— 面板退役后删掉。
|
|
946
|
+
// `dshVersion` 留着:诊断时一眼看出这套宿主跑在哪个 DSH 上。
|
|
926
947
|
dshVersion: dshVersion(),
|
|
927
948
|
};
|
|
928
949
|
}
|
|
@@ -932,10 +953,12 @@ export async function apply(ctx, config) {
|
|
|
932
953
|
return bridgeThread.rev(watchedSessions()) * 8 + bridgeApprovalBoard.rev() * 8 + (askDialog.open ? 1 : 0) + (askDialog.mode === 'file' ? 2 : 0);
|
|
933
954
|
}
|
|
934
955
|
|
|
935
|
-
/** 对话框刷新它的"我在看"时间戳(每趟 /state 都算一次)。
|
|
956
|
+
/** 对话框刷新它的"我在看"时间戳(每趟 /state 都算一次)。
|
|
957
|
+
* 0.3.59 起这里也负责对齐会话流订阅:以前是扩展每 600ms 的 `/sync` 顺带做的。 */
|
|
936
958
|
function refreshDialogWatch() {
|
|
937
959
|
bridgeWatch.dialogIds = askDialog.open && askDialog.sessionId !== null ? [askDialog.sessionId] : [];
|
|
938
960
|
bridgeWatch.at = Date.now();
|
|
961
|
+
bridgeThread.sync(watchedSessions());
|
|
939
962
|
}
|
|
940
963
|
|
|
941
964
|
/**
|
|
@@ -952,29 +975,6 @@ export async function apply(ctx, config) {
|
|
|
952
975
|
return { status: ok ? 200 : 409, body: { ok, outcome: ok ? outcome : null } };
|
|
953
976
|
}
|
|
954
977
|
|
|
955
|
-
/** 面板产物(thread.js / thread.css):对话框把它们注入 DSH 页面,所以由 host 读盘给文本。 */
|
|
956
|
-
let askBundleCache = null;
|
|
957
|
-
function askBundle() {
|
|
958
|
-
if (askBundleCache !== null) return askBundleCache;
|
|
959
|
-
const dir = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'assets', 'extensions', 'dshcs-editor-bridge', 'webview');
|
|
960
|
-
const read = (name) => {
|
|
961
|
-
try {
|
|
962
|
-
return fs.readFileSync(path.join(dir, name), 'utf8');
|
|
963
|
-
} catch {
|
|
964
|
-
return null;
|
|
965
|
-
}
|
|
966
|
-
};
|
|
967
|
-
const js = read('thread.js');
|
|
968
|
-
const css = read('thread.css');
|
|
969
|
-
// 产物缺失(打包漏了 build:webview)→ 明确报出来,不返回半个面板。
|
|
970
|
-
if (js === null || css === null) {
|
|
971
|
-
console.warn(`[code-server] 「问 DSH」对话框的产物缺失:${dir}(请在源码里跑 pnpm run build:webview)`);
|
|
972
|
-
return null;
|
|
973
|
-
}
|
|
974
|
-
askBundleCache = { js, css };
|
|
975
|
-
return askBundleCache;
|
|
976
|
-
}
|
|
977
|
-
|
|
978
978
|
|
|
979
979
|
/** 本部署的 DSH 版本(诊断用;面板要的是下面那个"界面版本")。 */
|
|
980
980
|
function dshVersion() {
|
|
@@ -988,36 +988,6 @@ export async function apply(ctx, config) {
|
|
|
988
988
|
}
|
|
989
989
|
}
|
|
990
990
|
|
|
991
|
-
/**
|
|
992
|
-
* 本部署**界面**的版本(0.3.22)。
|
|
993
|
-
*
|
|
994
|
-
* 面板里的 markdown 渲染器是从 `@deepseek-ai/dsh-client-ui-primitives` 打包进去的,而那份
|
|
995
|
-
* 渲染器就装在 `@deepseek-ai/dsh-web-frontend` 里(DSH Web 的前端包)。面板拿这个版本和
|
|
996
|
-
* 自己打包时的版本比一比,不一致就在面板顶部显示一行提示 —— 版本错配的排版差异不许静默。
|
|
997
|
-
*/
|
|
998
|
-
function uiVersion() {
|
|
999
|
-
try {
|
|
1000
|
-
const entry = dshEntry();
|
|
1001
|
-
if (entry === undefined || entry === null) return null;
|
|
1002
|
-
const dir = path.dirname(entry);
|
|
1003
|
-
const candidates = [
|
|
1004
|
-
path.join(dir, 'node_modules', '@deepseek-ai', 'dsh-web-frontend', 'package.json'),
|
|
1005
|
-
path.join(dir, '..', 'dsh-web-frontend', 'package.json'),
|
|
1006
|
-
];
|
|
1007
|
-
for (const candidate of candidates) {
|
|
1008
|
-
try {
|
|
1009
|
-
const manifest = JSON.parse(fs.readFileSync(candidate, 'utf8'));
|
|
1010
|
-
if (typeof manifest.version === 'string') return manifest.version;
|
|
1011
|
-
} catch {
|
|
1012
|
-
// 下一个候选
|
|
1013
|
-
}
|
|
1014
|
-
}
|
|
1015
|
-
return null;
|
|
1016
|
-
} catch {
|
|
1017
|
-
return null;
|
|
1018
|
-
}
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
991
|
/** 本次启动生成的新桥令牌;null = 本实例没有令牌(adopt 旧实例时会回读 bridge.json)。 */
|
|
1022
992
|
let bridgeToken = null;
|
|
1023
993
|
/** 桥的本机 IPC 监听口(0.3.13)。null = 未起(未启用/起失败)。*/
|
|
@@ -1084,15 +1054,17 @@ export async function apply(ctx, config) {
|
|
|
1084
1054
|
|
|
1085
1055
|
/** 桥的路由(后缀 → Fetch 风格 handler)。做成函数而非常量:handler 声明在文件后段,
|
|
1086
1056
|
* 函数声明会提升,而常量在挂载时可能还在 TDZ。
|
|
1087
|
-
*
|
|
1057
|
+
*
|
|
1058
|
+
* **四条路由全部只读**:/health 探活、/sync 上报状态+取事件、/old 取写前原文、/event 上报意图
|
|
1059
|
+
* (请宿主打开对话框等)。提问与授权**不在这里** —— 它们走 DSH 同源的
|
|
1060
|
+
* `/api/code-server/ask/{send,approve}`,调用方是 DSH 页面里的插件客户端(client 半部),
|
|
1061
|
+
* 吃 DSH 自己的 cookie/Origin 校验。 */
|
|
1088
1062
|
function bridgeRouteTable() {
|
|
1089
1063
|
return [
|
|
1090
1064
|
{ suffix: '/health', methods: ['GET'], fetch: handleBridgeHealth },
|
|
1091
1065
|
{ suffix: '/sync', methods: ['POST'], fetch: handleBridgeSync },
|
|
1092
1066
|
{ suffix: '/old', methods: ['GET'], fetch: handleBridgeOld },
|
|
1093
|
-
{ suffix: '/ask', methods: ['POST'], fetch: handleBridgeAsk },
|
|
1094
1067
|
{ suffix: '/event', methods: ['POST'], fetch: handleBridgeEvent },
|
|
1095
|
-
{ suffix: '/approve', methods: ['POST'], fetch: handleBridgeApprove },
|
|
1096
1068
|
];
|
|
1097
1069
|
}
|
|
1098
1070
|
|
|
@@ -1159,12 +1131,12 @@ export async function apply(ctx, config) {
|
|
|
1159
1131
|
});
|
|
1160
1132
|
|
|
1161
1133
|
// 会话流与授权拦截都是**按会话**建立的(在 /ask 成功时 watch + intercept),
|
|
1162
|
-
//
|
|
1134
|
+
// 这里只登记卸载钩子:对话框关掉 / IDE 停掉时把订阅与 pending 授权一起收干净。
|
|
1163
1135
|
bridgeAnswerDispose = () => {
|
|
1164
1136
|
bridgeThread.dispose();
|
|
1165
1137
|
bridgeApproval.dispose();
|
|
1166
1138
|
bridgeApprovalBoard.clear();
|
|
1167
|
-
bridgeWatch.
|
|
1139
|
+
bridgeWatch.dialogIds = [];
|
|
1168
1140
|
bridgeWatch.at = 0;
|
|
1169
1141
|
};
|
|
1170
1142
|
|
|
@@ -1518,7 +1490,7 @@ export async function apply(ctx, config) {
|
|
|
1518
1490
|
|
|
1519
1491
|
/** 已运行实例切换工作区:**只改"当前 workbench 目录",不重启进程**。
|
|
1520
1492
|
*
|
|
1521
|
-
* 为什么够用:工作区目录由客户端 URL 的 `&folder=<绝对路径>` 决定(见
|
|
1493
|
+
* 为什么够用:工作区目录由客户端 URL 的 `&folder=<绝对路径>` 决定(见 lib/client.js 的
|
|
1522
1494
|
* buildPageUrl),服务进程自己的 cwd 只影响它 spawn 时对相对路径的解析;真正"换目录"是
|
|
1523
1495
|
* workbench 拿新 folder 重新导航(客户端导航 iframe)。
|
|
1524
1496
|
* 旧行为(0.2.11 及以前)在这里 stop('restart') + 重新 launch:切一次工作区就整进程重启,
|
|
@@ -2021,17 +1993,7 @@ export async function apply(ctx, config) {
|
|
|
2021
1993
|
// 把当前最大 seq 一并回去:客户端据此自查"我的游标是不是跑到宿主前面去了"(跨版本升级、
|
|
2022
1994
|
// 接管了别的宿主留下的游标文件等),发现后退回 since=0 重新对齐,免得永久失聪。
|
|
2023
1995
|
const lastSeq = bridgeEvents.lastSeq();
|
|
2024
|
-
// 面板声明它要看哪些会话(数组;空 = 关掉了)。据此对齐订阅 + 判断"面板在不在看"
|
|
2025
|
-
// (授权拦截只在面板看着的时候才抢答,否则一律交给官方链路)。
|
|
2026
|
-
const watch = Array.isArray(body.watch) ? body.watch.filter((id) => typeof id === 'string' && id !== '') : [];
|
|
2027
|
-
bridgeWatch.ids = watch.slice(0, 4);
|
|
2028
|
-
bridgeApprovalsUi = body.approvalsUi === true;
|
|
2029
|
-
if (bridgeApprovalsUi !== bridgeApprovalsUiLogged) {
|
|
2030
|
-
bridgeApprovalsUiLogged = bridgeApprovalsUi;
|
|
2031
|
-
bridgeDiag('approvalsUi → ' + bridgeApprovalsUi + '(客户端可否显示授权卡片)');
|
|
2032
|
-
}
|
|
2033
1996
|
bridgeWatch.at = Date.now();
|
|
2034
|
-
bridgeThread.sync(watchedSessions());
|
|
2035
1997
|
// 看护状态变化就写一行;并且**首帧必写**(0.3.31:只记"翻转"会让"一直没人看"这种情况在日志里
|
|
2036
1998
|
// 完全空白,事后无从判断 —— 上一轮就是这样被拖了一整轮)。
|
|
2037
1999
|
const watchingNow = hasWatcher();
|
|
@@ -2039,118 +2001,30 @@ export async function apply(ctx, config) {
|
|
|
2039
2001
|
bridgeWatchLogged = watchingNow;
|
|
2040
2002
|
bridgeWatchWroteFirst = true;
|
|
2041
2003
|
bridgeDiag('hasWatcher → ' + watchingNow
|
|
2042
|
-
+ ' (
|
|
2043
|
-
+ ' approvalsUi=' + bridgeApprovalsUi + ')');
|
|
2004
|
+
+ ' (对话框=[' + bridgeWatch.dialogIds.join(',') + '])');
|
|
2044
2005
|
}
|
|
2045
2006
|
// 请求过对话框、客户端半部却从没来问过 → 说清楚原因(多半是浏览器还缓存着旧的客户端 bundle)。
|
|
2046
2007
|
if (askDialog.open && !askDialogLive() && askDialog.warned !== true) {
|
|
2047
2008
|
askDialog.warned = true;
|
|
2048
2009
|
console.warn('[code-server] 请求了「问 DSH」对话框,但客户端半部没有轮询 /ask/state:'
|
|
2049
|
-
+ '浏览器可能还在用旧的客户端 bundle(硬刷新 / 清 Code Cache)。'
|
|
2050
|
-
+ '此期间提问会退回编辑器面板,授权也在面板里批。');
|
|
2010
|
+
+ '浏览器可能还在用旧的客户端 bundle(硬刷新 / 清 Code Cache)。');
|
|
2051
2011
|
}
|
|
2052
|
-
//
|
|
2053
|
-
//
|
|
2054
|
-
//
|
|
2055
|
-
const threadRev = bridgeThread.rev(watchedSessions());
|
|
2056
|
-
const snapshot = bridgeThread.supported()
|
|
2057
|
-
? bridgeThread.snapshot(watchedSessions())
|
|
2058
|
-
: { available: false, sessionId: null, cursor: null, rev: 0, entries: [], error: '宿主没有 sessionController.follow(DSH 版本过旧)' };
|
|
2059
|
-
// **按"客户端已经拿到哪一版"决定发不发**(0.3.29 修):0.3.28 比的是宿主自己上次发过的版本,
|
|
2060
|
-
// 于是"面板是后开的"这种情形下宿主认为没变化、什么都不发,面板手里一份快照都没有 ⇒ 旧模型把它
|
|
2061
|
-
// 当成"宿主没有对话流能力"(用户实测到的那条误报)。现在由客户端用 threadRev 声明自己持有的版本;
|
|
2062
|
-
// 不带这个字段的旧扩展一律收整份(不省流量,但绝不误报)。
|
|
2063
|
-
const peerRev = Number.isSafeInteger(body.threadRev) ? body.threadRev : null;
|
|
2064
|
-
const threadChanged = peerRev === null || peerRev !== threadRev || snapshot.available === false;
|
|
2065
|
-
bridgeThreadRevSent = threadRev;
|
|
2066
|
-
// 对话流走独立字段:每个会话给有界条目(见 lib/bridge-thread.mjs),
|
|
2067
|
-
// 授权待决列表也一并回去(面板显示卡片)。
|
|
2012
|
+
// **对话流不再经这里**(0.3.59):那是编辑器 webview 面板时代的分工 —— 面板退役后这条
|
|
2013
|
+
// 每 600ms 一趟的通道上驮着 ≤1MB 的快照(0.3.27 还专门为它做过"没变化就不重传"的省流量),
|
|
2014
|
+
// 现在全归 `/api/code-server/ask/state`:对话框每 900ms 只问修订号,没变化只回一个数字。
|
|
2068
2015
|
return jsonResponse({
|
|
2069
2016
|
ok: true,
|
|
2070
2017
|
events,
|
|
2071
2018
|
// 事件序号的高水位:扩展用它自查游标是否超前(超前 ⇒ 退回 since=0 重新对齐,避免永久失聪)。
|
|
2072
2019
|
lastSeq,
|
|
2073
|
-
//
|
|
2074
|
-
|
|
2075
|
-
thread: threadChanged ? snapshot : undefined,
|
|
2076
|
-
approvals: bridgeApprovalBoard.snapshot(),
|
|
2077
|
-
// 授权卡片要在面板上倒计时,窗口长度由 host 给(面板不猜)。
|
|
2078
|
-
approvalHoldMs: DEFAULT_HOLD_MS,
|
|
2079
|
-
dshVersion: dshVersion(),
|
|
2080
|
-
// 界面版本:面板据此提示"内置渲染器与当前 DSH 界面版本不一致"。
|
|
2081
|
-
uiVersion: uiVersion(),
|
|
2082
|
-
// 能力探测(0.3.24;0.3.25 起以"客户端半部真的在轮询"为准):
|
|
2083
|
-
// 只有对话框活着的时候才让扩展走 ask-open;否则扩展开编辑器面板(那里也能批授权)。
|
|
2020
|
+
// 能力探测(0.3.24;0.3.25 起以"客户端半部真的在轮询 /ask/state"为准):
|
|
2021
|
+
// 只有对话框活着的时候才让扩展走 ask-open;0.3.59 起不满足时扩展给一条提示(不再退编辑器面板)。
|
|
2084
2022
|
askDialog: askDialogLive(),
|
|
2085
2023
|
// 诊断用:请求过对话框但客户端没响应(浏览器缓存旧 bundle?)。
|
|
2086
2024
|
askDialogRequested: askDialog.open,
|
|
2087
2025
|
});
|
|
2088
2026
|
}
|
|
2089
2027
|
|
|
2090
|
-
async function handleBridgeAsk(request) {
|
|
2091
|
-
const denied = bridgeRejection(request);
|
|
2092
|
-
if (denied !== null) return denied;
|
|
2093
|
-
let body;
|
|
2094
|
-
try {
|
|
2095
|
-
body = await readJsonBody(request);
|
|
2096
|
-
} catch {
|
|
2097
|
-
return jsonResponse({ ok: false, error: '请求体不是合法 JSON' }, 400);
|
|
2098
|
-
}
|
|
2099
|
-
const text = typeof body.text === 'string' ? body.text : '';
|
|
2100
|
-
if (text === '') return jsonResponse({ ok: false, error: '需要 text 字段(要问 DSH 的话)' }, 400);
|
|
2101
|
-
const file = typeof body.file === 'string' && body.file !== '' ? body.file : null;
|
|
2102
|
-
const lineStart = Number.isSafeInteger(body.lineStart) ? body.lineStart : null;
|
|
2103
|
-
const lineEnd = Number.isSafeInteger(body.lineEnd) ? body.lineEnd : null;
|
|
2104
|
-
const selection = typeof body.selection === 'string' && body.selection !== '' ? body.selection : null;
|
|
2105
|
-
// 交给 bridge-session.mjs(agent 投递);它内部特性探测 agents,缺失时返回
|
|
2106
|
-
// {ok:false, code:'NO_AGENT'} —— 这里原样透传,并把 NO_AGENT 映射成 409。
|
|
2107
|
-
const result = await deliverEditorPrompt(ctx, {
|
|
2108
|
-
text,
|
|
2109
|
-
file,
|
|
2110
|
-
lineStart,
|
|
2111
|
-
lineEnd,
|
|
2112
|
-
selection,
|
|
2113
|
-
languageId: typeof body.languageId === 'string' ? body.languageId : null,
|
|
2114
|
-
});
|
|
2115
|
-
// 会话流 + 授权拦截都在这里建立(只针对**被问过的**那个会话,agent 作用域):
|
|
2116
|
-
// - thread:面板像 DSH 对话那样显示"新内容"(见 lib/bridge-thread.mjs);
|
|
2117
|
-
// - approval:该会话的敏感动作(工作区外写入 / 命令执行)先在面板里问一句,
|
|
2118
|
-
// 8 秒没人答就交给官方链路(见 lib/bridge-approval.mjs)。
|
|
2119
|
-
if (result.ok === true && typeof result.sessionId === 'string' && result.sessionId !== '') {
|
|
2120
|
-
const ok = bridgeThread.watch(result.sessionId);
|
|
2121
|
-
if (!ok) console.warn('[code-server] 编辑器桥:会话订阅未建立(宿主没有 sessionController.follow)');
|
|
2122
|
-
bridgeApproval.intercept(result.agent, result.sessionId);
|
|
2123
|
-
}
|
|
2124
|
-
// `agent` 是内部句柄,不能进 JSON。
|
|
2125
|
-
const { agent: _agent, ...payload } = result;
|
|
2126
|
-
void _agent;
|
|
2127
|
-
return jsonResponse(payload, result.ok === true ? 200 : (result.code === 'NO_AGENT' ? 409 : 200));
|
|
2128
|
-
}
|
|
2129
|
-
|
|
2130
|
-
/**
|
|
2131
|
-
* 面板对授权请求的决策(0.3.22)。
|
|
2132
|
-
*
|
|
2133
|
-
* **这是命名空间里唯一的非只读路由**,约束写死在实现里:
|
|
2134
|
-
* - 只能回答**本 host 发起、仍未决**的请求(单次使用,用后即废);
|
|
2135
|
-
* - outcome 只接受 `allowed-once` / `rejected`;
|
|
2136
|
-
* - 不接受任何自由文本、路径或命令参数 —— 它只能"回答问题",不能"发起动作"。
|
|
2137
|
-
*/
|
|
2138
|
-
async function handleBridgeApprove(request) {
|
|
2139
|
-
const denied = bridgeRejection(request);
|
|
2140
|
-
if (denied !== null) return denied;
|
|
2141
|
-
let body;
|
|
2142
|
-
try {
|
|
2143
|
-
body = await readJsonBody(request);
|
|
2144
|
-
} catch {
|
|
2145
|
-
return jsonResponse({ ok: false, error: '请求体不是合法 JSON' }, 400);
|
|
2146
|
-
}
|
|
2147
|
-
const verdict = answerApproval(
|
|
2148
|
-
typeof body.id === 'string' ? body.id : '',
|
|
2149
|
-
typeof body.outcome === 'string' ? body.outcome : '',
|
|
2150
|
-
);
|
|
2151
|
-
return jsonResponse(verdict.body, verdict.status);
|
|
2152
|
-
}
|
|
2153
|
-
|
|
2154
2028
|
// ---------------------------------------------------------------- 「问 DSH」对话框(client 半部)
|
|
2155
2029
|
|
|
2156
2030
|
/** 对话框每 900ms 问一次:给修订号,没变就只回一个数字(不重传几十 KB 的对话流)。 */
|
|
@@ -2193,6 +2067,9 @@ export async function apply(ctx, config) {
|
|
|
2193
2067
|
if (text === '') return jsonResponse({ ok: false, error: '需要 text 字段(要问 DSH 的话)' }, 400);
|
|
2194
2068
|
const mode = body.mode === 'file' ? 'file' : askDialog.mode;
|
|
2195
2069
|
const context = askContextFromCache(mode);
|
|
2070
|
+
// 投递方式(0.3.59):默认跟着 DSH 自己的设置 `ui-conversation.busyEnter` 走(见
|
|
2071
|
+
// lib/bridge-session.mjs 的 pickBusyEnter)—— 面板按 Enter 与主界面按 Enter 是同一个手势,
|
|
2072
|
+
// 行为必须一致。显式传 `delivery: 'queue'|'steer'` 可覆盖(测试用,或将来在面板上做"插队"按钮)。
|
|
2196
2073
|
const result = await deliverEditorPrompt(ctx, {
|
|
2197
2074
|
text,
|
|
2198
2075
|
file: context === null ? null : context.file,
|
|
@@ -2200,7 +2077,7 @@ export async function apply(ctx, config) {
|
|
|
2200
2077
|
lineEnd: context === null ? null : context.lineEnd,
|
|
2201
2078
|
selection: context === null ? null : context.selection,
|
|
2202
2079
|
languageId: context === null ? null : context.languageId,
|
|
2203
|
-
});
|
|
2080
|
+
}, { delivery: typeof body.delivery === 'string' ? body.delivery : undefined });
|
|
2204
2081
|
if (result.ok === true && typeof result.sessionId === 'string' && result.sessionId !== '') {
|
|
2205
2082
|
askDialog.sessionId = result.sessionId;
|
|
2206
2083
|
askDialog.mode = mode;
|
|
@@ -2213,7 +2090,7 @@ export async function apply(ctx, config) {
|
|
|
2213
2090
|
return jsonResponse(payload, result.ok === true ? 200 : (result.code === 'NO_AGENT' ? 409 : 200));
|
|
2214
2091
|
}
|
|
2215
2092
|
|
|
2216
|
-
/** 对话框回答授权(
|
|
2093
|
+
/** 对话框回答授权(与宿主内部 answerApproval 同一套校验:白名单 outcome + 只认仍未决的 id)。 */
|
|
2217
2094
|
async function handleAskApprove(request) {
|
|
2218
2095
|
let body;
|
|
2219
2096
|
try {
|
|
@@ -2239,15 +2116,6 @@ export async function apply(ctx, config) {
|
|
|
2239
2116
|
return jsonResponse({ ok: true });
|
|
2240
2117
|
}
|
|
2241
2118
|
|
|
2242
|
-
/** 面板产物(thread.js / thread.css)给对话框注入用。 */
|
|
2243
|
-
async function handleAskBundle() {
|
|
2244
|
-
const bundle = askBundle();
|
|
2245
|
-
if (bundle === null) {
|
|
2246
|
-
return jsonResponse({ ok: false, error: '面板产物缺失:请在插件源码里跑 pnpm run build:webview,或重装插件' }, 503);
|
|
2247
|
-
}
|
|
2248
|
-
return jsonResponse({ ok: true, rev: askRevision(), ...bundle });
|
|
2249
|
-
}
|
|
2250
|
-
|
|
2251
2119
|
async function handleBridgeEvent(request) {
|
|
2252
2120
|
const denied = bridgeRejection(request);
|
|
2253
2121
|
if (denied !== null) return denied;
|
|
@@ -2283,7 +2151,7 @@ export async function apply(ctx, config) {
|
|
|
2283
2151
|
{ path: `${API_BASE}/ask/send`, methods: ['POST'], fetch: handleAskSend },
|
|
2284
2152
|
{ path: `${API_BASE}/ask/approve`, methods: ['POST'], fetch: handleAskApprove },
|
|
2285
2153
|
{ path: `${API_BASE}/ask/close`, methods: ['POST'], fetch: handleAskClose },
|
|
2286
|
-
|
|
2154
|
+
// 0.3.59 删掉了 `/ask/bundle`(面板产物不再存在:面板本体在 lib/client.js 里,渲染器走页面模块表)。
|
|
2287
2155
|
// ---- 编辑器桥的 4 条路由**不在 /api 下**(0.3.9 修正)----
|
|
2288
2156
|
// 原因:Connection 给 `/api` 装了 cookie fence(无 cookie → 401),而扩展宿主是 Node 进程、
|
|
2289
2157
|
// 永远拿不到浏览器 cookie ⇒ 挂在这里的路由根本到不了(实测:带令牌也被 401/405 挡回)。
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-code-server-app",
|
|
3
|
-
"version": "0.3.
|
|
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)
|
|
3
|
+
"version": "0.3.59",
|
|
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), plus an editor bridge (assets/extensions/dshcs-editor-bridge) that gives the agent what only the editor knows (unsaved buffers, language-server diagnostics, the active selection) and lets editor gestures drive the session. The bridge is completely read-only: all four of its routes (/health, /sync, /old, /event) are reads. Asking DSH from the editor opens a floating dialog inside the DSH page (draggable, resizable, bottom-right): the plugin's client half sends the question as user input (POST /api/code-server/ask/send) and renders that session's new content with DSH's own Markdown renderer - the same @deepseek-ai/dsh-client-ui-primitives instance the UI uses, required from the page's module table, so typography, highlighting and math match the interface and a renderer version mismatch is impossible. Thinking appears as collapsed rows and approval requests (writing outside the workspace / running commands) can be answered in place: whole-plugin write route POST /api/code-server/ask/approve, constrained to pending in-process requests and the outcomes allowed-once/rejected; dialog-first for 5 minutes, handed back to the official card the moment the dialog closes, never auto-approved. There is no build step anywhere: lib/client.js (client half including the dialog panel) and the extension are committed hand-written source, devDependencies holds a single test-only entry, and the published package carries no frontend bundle. Also: the tab claims DSH file addresses (dsh-resource://file/**) by file type (setting claimExtensions), so 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 does not reload it; opening the tab switches the right sidebar to fullscreen by default (setting fullscreenOnOpen); a workspace switch re-navigates the workbench with the new ?folder= without restarting the IDE process. Requires a DSH with the right-sidebar services (sidebarRightTabs/sidebarRight, >= 0.1.5-alpha.1); older DSH versions only get an upgrade notice on the settings page. Two serving modes: loopback port (default) or same-origin mount on DSH's own webServer (/code-server, protected by ctx.connection.requestRejection). The diff the bridge opens always has the real pre-write text on the left, taken from result.value.before in tools/post-execute into a bounded snapshot cache and fetched by opaque key over the read-only GET /old, so files that are not open in the editor work too. 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",
|
|
@@ -67,30 +67,7 @@
|
|
|
67
67
|
"README.en.md"
|
|
68
68
|
],
|
|
69
69
|
"devDependencies": {
|
|
70
|
-
"@deepseek-ai/
|
|
71
|
-
"@deepseek-ai/dsh-client-ui-theme": "0.1.6-alpha.1",
|
|
72
|
-
"@deepseek-ai/schemastery": "3.18.2",
|
|
73
|
-
"@shikijs/langs": "4.3.1",
|
|
74
|
-
"anser": "2.3.5",
|
|
75
|
-
"clsx": "2.0.0",
|
|
76
|
-
"diff": "9.0.0",
|
|
77
|
-
"esbuild": "^0.25.0",
|
|
78
|
-
"katex": "0.16.47",
|
|
79
|
-
"mdast-util-from-markdown": "2.0.3",
|
|
80
|
-
"mdast-util-gfm": "3.1.0",
|
|
81
|
-
"mdast-util-math": "3.0.0",
|
|
82
|
-
"micromark-core-commonmark": "2.0.3",
|
|
83
|
-
"micromark-extension-gfm": "3.0.0",
|
|
84
|
-
"micromark-extension-math": "3.1.0",
|
|
85
|
-
"micromark-factory-space": "2.0.1",
|
|
86
|
-
"micromark-util-character": "2.1.1",
|
|
87
|
-
"micromark-util-classify-character": "2.0.1",
|
|
88
|
-
"micromark-util-sanitize-uri": "2.0.1",
|
|
89
|
-
"micromark-util-symbol": "2.0.1",
|
|
90
|
-
"micromark-util-types": "2.0.2",
|
|
91
|
-
"react": "18.3.1",
|
|
92
|
-
"react-dom": "18.3.1",
|
|
93
|
-
"shiki": "4.3.1"
|
|
70
|
+
"@deepseek-ai/schemastery": "3.18.2"
|
|
94
71
|
},
|
|
95
72
|
"license": "MIT",
|
|
96
73
|
"dependencies": {
|
|
@@ -183,8 +160,6 @@
|
|
|
183
160
|
"publish:repacks": "node scripts/publish-repacks.mjs",
|
|
184
161
|
"publish:plugin": "node scripts/publish-plugin.mjs",
|
|
185
162
|
"promote": "node scripts/promote.mjs",
|
|
186
|
-
"build:client": "node scripts/build-client.mjs",
|
|
187
|
-
"build:webview": "node scripts/build-webview.mjs",
|
|
188
163
|
"test:apply": "node scripts/test-plugin-apply.mjs",
|
|
189
164
|
"test:launcher-routes": "node scripts/test-launcher-routes.mjs",
|
|
190
165
|
"test:fullscreen": "node scripts/test-sidebar-fullscreen.mjs",
|
|
@@ -193,6 +168,8 @@
|
|
|
193
168
|
"test:workspace-cwd": "node scripts/test-workspace-cwd.mjs",
|
|
194
169
|
"test:client-cwd": "node scripts/test-client-bundle-cwd.mjs",
|
|
195
170
|
"test:client-tabs": "node scripts/test-client-bundle-tabs.mjs",
|
|
171
|
+
"test:client-entry": "node scripts/test-client-entry.mjs",
|
|
172
|
+
"test:ask-panel": "node scripts/test-ask-panel-inline.mjs",
|
|
196
173
|
"test:client-seat": "node scripts/test-client-settings-seat.mjs",
|
|
197
174
|
"test:dsh-resolve": "node scripts/test-dsh-resolve.mjs",
|
|
198
175
|
"test:child-node": "node scripts/test-child-node.mjs",
|
|
@@ -200,7 +177,7 @@
|
|
|
200
177
|
"test:bridge-routes": "node scripts/test-bridge-routes.mjs",
|
|
201
178
|
"test:edit-snapshot": "node scripts/test-edit-snapshot.mjs",
|
|
202
179
|
"test:bridge-extension": "node scripts/test-bridge-extension.mjs",
|
|
203
|
-
"test:
|
|
180
|
+
"test:ask-dialog": "node scripts/test-ask-dialog.mjs",
|
|
204
181
|
"test:vendored": "node scripts/test-vendored-table.mjs"
|
|
205
182
|
}
|
|
206
183
|
}
|
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-20T15:36:43.387Z",
|
|
7
7
|
"source": "registry",
|
|
8
8
|
"node": "v24.20.0",
|
|
9
9
|
"platform": "linux",
|