dsh-code-server-app 0.3.59 → 0.3.63

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/lib/index.js CHANGED
@@ -51,6 +51,22 @@ import {
51
51
  writeBridgeConfig,
52
52
  } from './bridge.mjs';
53
53
  import { bridgeEndpointPath, startBridgeListener } from './bridge-ipc.mjs';
54
+ // FIM(幽灵)补全(实验性):**注册一个自己的 LLM 适配器**而不是直连 —— 见 lib/fim-adapter.mjs 顶部。
55
+ import {
56
+ clampDebounce,
57
+ compileGlobList,
58
+ createApiKeyResolver,
59
+ createFimAdapterClass,
60
+ createFimBudget,
61
+ createFimStats,
62
+ encodeFimRequest,
63
+ FIM_MAX_TOKENS,
64
+ FIM_MODEL,
65
+ FIM_PROVIDER,
66
+ loadFimBase,
67
+ matchDisabledGlob,
68
+ toSingleLine,
69
+ } from './fim-adapter.mjs';
54
70
  import { registerEditorPrompt, registerEditorTools, setPromptLiveProbe } from './bridge-tools.mjs';
55
71
  import { deliverEditorPrompt } from './bridge-session.mjs';
56
72
  import { dshEntry, dshRequire } from './dsh-resolve.mjs';
@@ -120,6 +136,25 @@ export const Config = z.object({
120
136
  * 诊断、活动选区),并让编辑器里的动作能驱动 DSH。关闭后不写 bridge.json、不注册编辑器工具,
121
137
  * 扩展会休眠(它读不到配置就不做任何事)。安全模型见 lib/bridge.mjs 顶部。 */
122
138
  editorBridge: z.boolean().default(true),
139
+ /** fim=false(默认):**实验性**的 FIM(幽灵)补全。开启后宿主的编辑器桥多一条**有界的模型调用**
140
+ * 路由(`POST /complete`),编辑器里的内联补全(灰字)由它供给;宿主为此注册一个自己的 LLM
141
+ * 适配器路由 `dshcs-fim`(走 Completions API 的 `/beta/completions`)。
142
+ *
143
+ * 为什么默认关、为什么叫实验性:①它会把**光标附近的代码**发给模型(桥的其余部分是只读状态,
144
+ * 不外发内容);②模型在"不该补的位置"会硬凑(实测 2/2 复现),需要编辑器侧判据兜;
145
+ * ③这条调用**不进 DSH 的 token 计量**(一次性调用不是 loop 请求,不写会话日志),
146
+ * 用量只在扩展状态栏与设置卡里可见。安全模型见 lib/fim-adapter.mjs 顶部。 */
147
+ fim: z.boolean().default(false),
148
+ /** 停顿多久才发一次补全请求(毫秒)。太小 ⇒ 打字过程中反复触发;太大 ⇒ 失去"补全"的意义。
149
+ * 取值在消费处夹到 [100, 3000](clampDebounce),设置里填什么都不至于打到链路上。 */
150
+ fimDebounceMs: z.number().default(250),
151
+ /** 是否允许多行补全(true 默认)。false ⇒ 宿主只回第一行(空首行 = 这次不补)。
152
+ * 关掉它是最省心的安全阀:实测模型在"不该补的位置"会硬凑,多行会把这种噪声放大。 */
153
+ fimMultiline: z.boolean().default(true),
154
+ /** 按 glob 禁用补全的文件(分号 / 逗号 / 空白 / 换行分隔;空 = 不禁用)。
155
+ * 语义:`*` 不跨目录、`**` 跨目录、不含 `/` 的模式只匹配文件名、`/` 结尾视作 `/**`。
156
+ * 两侧都会判:扩展侧先判(不发请求),宿主侧再判(防线在服务端)。 */
157
+ fimDisableGlobs: z.string().default(''),
123
158
  });
124
159
 
125
160
  const DEFAULT_CONFIG = {
@@ -136,6 +171,12 @@ const DEFAULT_CONFIG = {
136
171
  readyTimeoutMs: 60000,
137
172
  /** 编辑器桥(0.3.0):见 Config.editorBridge。行配置与设置文档都可关;设置卡片暂不提供行。 */
138
173
  editorBridge: true,
174
+ /** FIM 补全(实验性,0.3.61):见 Config.fim。默认关。 */
175
+ fim: false,
176
+ /** FIM 的三个子项(0.3.62):停顿毫秒数 / 是否多行 / 按 glob 禁用。见 Config 里的说明。 */
177
+ fimDebounceMs: 250,
178
+ fimMultiline: true,
179
+ fimDisableGlobs: '',
139
180
  };
140
181
 
141
182
  const LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '::1']);
@@ -615,6 +656,10 @@ function readEndpoint(config) {
615
656
  }
616
657
  }
617
658
 
659
+ /** 仅测试用:最近一次 apply() 装好的桥分发器(消费方是文件末尾的 bridgeRequestForTests)。
660
+ * 运行时代码**不读它** —— 它存在的唯一目的是让回归不必经过命名管道,原因见那个函数的注释。 */
661
+ let activeBridgeDispatch = null;
662
+
618
663
  export async function apply(ctx, config) {
619
664
  const cfg = { ...DEFAULT_CONFIG, ...(config ?? {}) };
620
665
 
@@ -632,6 +677,12 @@ export async function apply(ctx, config) {
632
677
  let fullscreenOnOpen = true; // 客户端"打开标签即全屏右侧栏"(0.2.9)
633
678
  /** 编辑器桥开关(0.3.0):行配置 cfg.editorBridge 是种子,设置文档里可改并实时生效。 */
634
679
  let bridgeSetting = cfg.editorBridge !== false;
680
+ /** FIM 补全开关(实验性,0.3.61):行配置 cfg.fim 是种子,设置文档里可改并实时生效。默认关。 */
681
+ let fimSetting = cfg.fim === true;
682
+ /** FIM 的三个子项(0.3.62):停顿毫秒数 / 是否多行 / 按 glob 禁用(原始文本,消费时解析)。 */
683
+ let fimDebounceMs = clampDebounce(cfg.fimDebounceMs);
684
+ let fimMultiline = cfg.fimMultiline !== false;
685
+ let fimDisableGlobs = typeof cfg.fimDisableGlobs === 'string' ? cfg.fimDisableGlobs : '';
635
686
  if (settingsSvc !== undefined && typeof settingsSvc.register === 'function') {
636
687
  try {
637
688
  const scope = settingsSvc.register(SETTINGS_NS, Config);
@@ -645,6 +696,10 @@ export async function apply(ctx, config) {
645
696
  : DEFAULT_CLAIM_EXTENSIONS;
646
697
  fullscreenOnOpen = resolved && typeof resolved.fullscreenOnOpen === 'boolean' ? resolved.fullscreenOnOpen : true;
647
698
  bridgeSetting = resolved && typeof resolved.editorBridge === 'boolean' ? resolved.editorBridge : bridgeSetting;
699
+ fimSetting = resolved && typeof resolved.fim === 'boolean' ? resolved.fim : fimSetting;
700
+ fimDebounceMs = clampDebounce(resolved && resolved.fimDebounceMs !== undefined ? resolved.fimDebounceMs : fimDebounceMs);
701
+ fimMultiline = resolved && typeof resolved.fimMultiline === 'boolean' ? resolved.fimMultiline : fimMultiline;
702
+ fimDisableGlobs = resolved && typeof resolved.fimDisableGlobs === 'string' ? resolved.fimDisableGlobs : fimDisableGlobs;
648
703
  }
649
704
  scope.watch((next) => {
650
705
  if (next != null && (next.serve === 'dsh' || next.serve === 'loopback')) {
@@ -671,6 +726,36 @@ export async function apply(ctx, config) {
671
726
  }
672
727
  }
673
728
  }
729
+ // FIM(实验性):开 → 注册我们自己的 LLM 适配器路由(编辑器补全要靠它);
730
+ // 关 → 注销。翻转立刻生效,不需要重启宿主(扩展侧下一趟 /sync 就会看到 enabled 变了)。
731
+ if (next != null && typeof next.fim === 'boolean') {
732
+ if (next.fim !== fimSetting) {
733
+ fimSetting = next.fim;
734
+ console.log(`[code-server] fim updated: ${fimSetting}(实验性 FIM 补全)`);
735
+ void ensureFimAdapter();
736
+ }
737
+ }
738
+ // FIM 的三个子项(0.3.62):都**即时生效**,不需要重启宿主 —— 扩展每趟 /sync 都会拿到新值
739
+ // (停顿与 glob 是扩展侧行为,多行由宿主在返回前裁)。改动只打一行日志,便于事后对齐现场。
740
+ if (next != null && next.fimDebounceMs !== undefined) {
741
+ const clamped = clampDebounce(next.fimDebounceMs);
742
+ if (clamped !== fimDebounceMs) {
743
+ fimDebounceMs = clamped;
744
+ console.log(`[code-server] fimDebounceMs updated: ${fimDebounceMs}ms`);
745
+ }
746
+ }
747
+ if (next != null && typeof next.fimMultiline === 'boolean') {
748
+ if (next.fimMultiline !== fimMultiline) {
749
+ fimMultiline = next.fimMultiline;
750
+ console.log(`[code-server] fimMultiline updated: ${fimMultiline}`);
751
+ }
752
+ }
753
+ if (next != null && typeof next.fimDisableGlobs === 'string') {
754
+ if (next.fimDisableGlobs !== fimDisableGlobs) {
755
+ fimDisableGlobs = next.fimDisableGlobs;
756
+ console.log(`[code-server] fimDisableGlobs updated: ${compileGlobList(fimDisableGlobs).length} 条`);
757
+ }
758
+ }
674
759
  if (next != null && typeof next.keepResident === 'boolean') {
675
760
  keepResident = next.keepResident;
676
761
  console.log(`[code-server] keepResident updated: ${keepResident}`);
@@ -1055,7 +1140,9 @@ export async function apply(ctx, config) {
1055
1140
  /** 桥的路由(后缀 → Fetch 风格 handler)。做成函数而非常量:handler 声明在文件后段,
1056
1141
  * 函数声明会提升,而常量在挂载时可能还在 TDZ。
1057
1142
  *
1058
- * **四条路由全部只读**:/health 探活、/sync 上报状态+取事件、/old 取写前原文、/event 上报意图
1143
+ * **四条只读路由 + 一条有界的模型调用**(0.3.61 起):/health 探活、/sync 上报状态+取事件、
1144
+ * /old 取写前原文、/event 上报意图;/complete 是**唯一的例外** —— 它把光标前后的两段文本
1145
+ * 交给模型换回一段补全文本(实验性 FIM,设置里默认关;安全不变量见 handleBridgeComplete)。
1059
1146
  * (请宿主打开对话框等)。提问与授权**不在这里** —— 它们走 DSH 同源的
1060
1147
  * `/api/code-server/ask/{send,approve}`,调用方是 DSH 页面里的插件客户端(client 半部),
1061
1148
  * 吃 DSH 自己的 cookie/Origin 校验。 */
@@ -1065,6 +1152,7 @@ export async function apply(ctx, config) {
1065
1152
  { suffix: '/sync', methods: ['POST'], fetch: handleBridgeSync },
1066
1153
  { suffix: '/old', methods: ['GET'], fetch: handleBridgeOld },
1067
1154
  { suffix: '/event', methods: ['POST'], fetch: handleBridgeEvent },
1155
+ { suffix: '/complete', methods: ['POST'], fetch: handleBridgeComplete },
1068
1156
  ];
1069
1157
  }
1070
1158
 
@@ -1108,6 +1196,11 @@ export async function apply(ctx, config) {
1108
1196
  return route.fetch(request);
1109
1197
  }
1110
1198
 
1199
+ // 仅测试用:把分发器交给模块级钩子(见 bridgeRequestForTests)。沙箱里**连接**命名管道 EPERM
1200
+ // ⇒ 走 IPC 的用例会全体 SKIP,"本地全绿"里就没有桥的状态码断言了(0.3.63 的 /complete 403
1201
+ // 就是这么假绿到 runner 才红的)。有了它,断言在任何机器上都能真跑。
1202
+ activeBridgeDispatch = dispatchBridge;
1203
+
1111
1204
  /** 桥是否启用(行配置为种子,设置文档里可实时改)。 */
1112
1205
  function bridgeEnabled() {
1113
1206
  return bridgeSetting !== false;
@@ -1117,6 +1210,10 @@ export async function apply(ctx, config) {
1117
1210
  * 所以在 IDE 从未启动的部署里它也只是一段空注册,不产生任何提示词开销。 */
1118
1211
  setPromptLiveProbe(() => bridgeMeta !== null);
1119
1212
  bridgePromptDispose = registerEditorPrompt(ctx);
1213
+ // FIM(实验性)的适配器注册**不在这里**:它的状态声明(fimStats/fimAdapterDispose…)在文件后段,
1214
+ // 这里调用会撞 `Cannot access 'fimAdapterDispose' before initialization`(TDZ)——
1215
+ // async 函数声明会提升,但它读的 `let` 还没初始化,异常会直接把整个插件加载打挂
1216
+ // (与 0.3.6 的 `ctx.systemPrompt` 事故同一类)。真正的调用点在 FIM 运行时声明之后。
1120
1217
  if (bridgePromptDispose === null && bridgeEnabled()) {
1121
1218
  console.warn('[code-server] 编辑器桥:systemPrompt 服务不可用,提示词段落未注册(工具仍可用)');
1122
1219
  }
@@ -1930,6 +2027,172 @@ export async function apply(ctx, config) {
1930
2027
  return jsonResponse({ ok: false, error: reason }, 503);
1931
2028
  }
1932
2029
 
2030
+ // ---------------------------------------------------------------- FIM(幽灵)补全(实验性,0.3.61)
2031
+
2032
+ /** 用量与调用计数:这是**唯一**的可见处(状态栏 + 设置卡)。一次性插件调用不是 loop 请求、
2033
+ * 不写会话日志 ⇒ DSH 的 token-meter 看不到它(见 lib/fim-adapter.mjs 顶部"计量会落在哪")。 */
2034
+ const fimStats = createFimStats();
2035
+ /** 速率/并发闸:这条链路由击键触发,任何一处无界都会变成"打字时把宿主/账号打满"。 */
2036
+ const fimBudget = createFimBudget();
2037
+ /** 已注册的适配器句柄(null = 未注册)。 */
2038
+ let fimAdapterDispose = null;
2039
+ /** 消息构造器(从 DSH 解析;拿不到就不启用 —— 手搓 message 对象容易缺字段,不值得赌)。 */
2040
+ let fimCreateUserMessage = null;
2041
+ /** 不可用的原因(状态栏与设置卡要能说清"为什么没反应")。 */
2042
+ let fimUnavailable = '未启用';
2043
+
2044
+ /**
2045
+ * 按设置注册/注销适配器路由。
2046
+ *
2047
+ * 为什么是**适配器**而不是直连 fetch:这样这条调用仍然走 DSH 的 LLM 服务 —— 取消、超时、
2048
+ * 终态 chunk、稳定错误码都按服务契约走(见 lib/fim-adapter.mjs 顶部)。解析不到 llm 服务或
2049
+ * LlmAdapter 时**不报错、只是不可用**:FIM 是可选能力,不该让插件在缺服务的部署里出问题。
2050
+ */
2051
+ async function ensureFimAdapter() {
2052
+ if (fimAdapterDispose !== null) {
2053
+ try { fimAdapterDispose(); } catch { /* 已被 fiber 回收 */ }
2054
+ fimAdapterDispose = null;
2055
+ }
2056
+ if (!fimSetting) {
2057
+ fimUnavailable = '设置里未开启';
2058
+ return false;
2059
+ }
2060
+ const llm = (() => { try { return ctx.get('llm'); } catch { return undefined; } })();
2061
+ if (llm === undefined || llm === null || typeof llm.registerAdapter !== 'function') {
2062
+ fimUnavailable = '这个 DSH 没有 llm 服务';
2063
+ console.warn('[code-server] FIM 补全:ctx.llm 不可用,补全保持关闭');
2064
+ return false;
2065
+ }
2066
+ const base = await loadFimBase();
2067
+ if (base === null || base.createUserMessage === null) {
2068
+ fimUnavailable = '解析不到 @deepseek-ai/dsh-llm 的 LlmAdapter/createUserMessage';
2069
+ console.warn(`[code-server] FIM 补全:${fimUnavailable};补全保持关闭`);
2070
+ return false;
2071
+ }
2072
+ try {
2073
+ const Adapter = createFimAdapterClass(base.Base, {
2074
+ resolveApiKey: createApiKeyResolver(ctx),
2075
+ attributionHeaders: base.attributionHeaders,
2076
+ assertUsableApiKey: base.assertUsableApiKey,
2077
+ log: (message) => console.log(`[code-server] FIM:${message}`),
2078
+ });
2079
+ const handle = llm.registerAdapter([FIM_PROVIDER], new Adapter());
2080
+ fimAdapterDispose = typeof handle === 'function' ? handle : () => {};
2081
+ fimCreateUserMessage = base.createUserMessage;
2082
+ fimUnavailable = null;
2083
+ console.log(`[code-server] FIM 补全(实验性)已注册适配器路由:${FIM_PROVIDER}/${FIM_MODEL}`);
2084
+ return true;
2085
+ } catch (error) {
2086
+ fimUnavailable = error && error.message ? error.message : String(error);
2087
+ console.warn(`[code-server] FIM 补全:注册适配器失败(${fimUnavailable})`);
2088
+ return false;
2089
+ }
2090
+ }
2091
+
2092
+ /** 给客户端(/sync)看的快照:开关状态 + 可用性 + 用量。 */
2093
+ function fimSnapshot() {
2094
+ const stats = fimStats.snapshot();
2095
+ const budget = fimBudget.snapshot();
2096
+ return {
2097
+ enabled: fimSetting,
2098
+ available: fimSetting && fimAdapterDispose !== null,
2099
+ reason: fimUnavailable,
2100
+ provider: FIM_PROVIDER,
2101
+ model: FIM_MODEL,
2102
+ inflight: budget.inflight,
2103
+ callsLastMinute: budget.callsLastMinute,
2104
+ // 三个子项(0.3.62):扩展侧靠这些值决定"等多久、要不要接受多行、这个文件要不要跳过"。
2105
+ debounceMs: fimDebounceMs,
2106
+ multiline: fimMultiline,
2107
+ disableGlobs: compileGlobList(fimDisableGlobs),
2108
+ ...stats,
2109
+ };
2110
+ }
2111
+
2112
+ /**
2113
+ * `POST /complete` —— 桥的**第五条路由**,也是唯一一条**会发起模型调用**的路由。
2114
+ *
2115
+ * 安全不变量(比前四条多三条,别放宽):
2116
+ * (a) **只有设置里开了 FIM 才存在** —— 没开一律 403,且不会发出任何网络请求;
2117
+ * (b) **不写文件、不执行命令** —— 输入是两个字符串,输出是一段文本 + 用量;
2118
+ * (c) **有界** —— body 上限、前后缀窗口(适配器侧再裁一遍)、速率/并发闸、单次 4s 超时。
2119
+ * 即:桥令牌泄露时,攻击面是"替用户花一点补全的 token 并读到一段补全文本",
2120
+ * 而不是任意文件写 / 任意命令执行 —— 与前四条保持同一量级。
2121
+ */
2122
+ async function handleBridgeComplete(request) {
2123
+ const denied = bridgeRejection(request);
2124
+ if (denied !== null) return denied;
2125
+ if (!fimSetting) return jsonResponse({ ok: false, error: 'fim-disabled' }, 403);
2126
+ if (fimAdapterDispose === null || fimCreateUserMessage === null) {
2127
+ return jsonResponse({ ok: false, error: 'fim-unavailable', reason: fimUnavailable }, 503);
2128
+ }
2129
+ let body;
2130
+ try {
2131
+ body = await readJsonBody(request);
2132
+ } catch {
2133
+ return jsonResponse({ ok: false, error: '请求体不是合法 JSON' }, 400);
2134
+ }
2135
+ if (!bodyWithinLimit(body)) return jsonResponse({ ok: false, error: '请求体过大' }, 413);
2136
+ const prompt = typeof body.prompt === 'string' ? body.prompt : null;
2137
+ const suffix = typeof body.suffix === 'string' ? body.suffix : null;
2138
+ if (prompt === null || suffix === null) return jsonResponse({ ok: false, error: '缺少 prompt/suffix' }, 400);
2139
+ // 按 glob 禁用(0.3.62):扩展侧先判过一遍,这里是**服务端那道**——令牌泄露也只能换来"被拒",
2140
+ // 不会替用户在已禁用的大文件/无关目录上烧 token。
2141
+ const blocked = matchDisabledGlob(typeof body.path === 'string' ? body.path : '', compileGlobList(fimDisableGlobs));
2142
+ if (blocked !== null) return jsonResponse({ ok: false, error: 'disabled-by-glob', glob: blocked }, 403);
2143
+ const gate = fimBudget.acquire();
2144
+ if (gate !== 'ok') return jsonResponse({ ok: false, error: gate }, gate === 'busy' ? 409 : 429);
2145
+ const started = Date.now();
2146
+ try {
2147
+ const llm = ctx.get('llm');
2148
+ const message = fimCreateUserMessage({
2149
+ content: [{ type: 'text', text: encodeFimRequest({ prompt, suffix, language: body.language, path: body.path }) }],
2150
+ });
2151
+ let text = '';
2152
+ let usage = null;
2153
+ let failure = null;
2154
+ for await (const chunk of llm.stream({
2155
+ provider: FIM_PROVIDER,
2156
+ model: FIM_MODEL,
2157
+ messages: [message],
2158
+ maxTokens: FIM_MAX_TOKENS,
2159
+ temperature: 0,
2160
+ })) {
2161
+ if (chunk === null || typeof chunk !== 'object') continue;
2162
+ if (chunk.type === 'text-delta' && typeof chunk.text === 'string') text += chunk.text;
2163
+ else if (chunk.type === 'usage') usage = chunk.usage ?? usage;
2164
+ else if (chunk.type === 'finish') {
2165
+ const kind = chunk.reason?.kind;
2166
+ if (kind !== 'stop' && kind !== 'max-tokens') {
2167
+ failure = chunk.reason?.failure ?? { code: String(kind ?? 'error'), message: '补全未正常结束' };
2168
+ }
2169
+ }
2170
+ }
2171
+ const ms = Date.now() - started;
2172
+ if (failure !== null) {
2173
+ fimStats.recordFailure(failure.code, ms);
2174
+ return jsonResponse({ ok: false, error: failure.code, reason: failure.message, ms }, 502);
2175
+ }
2176
+ // "允许多行"关掉时**在返回前裁**(0.3.62):扩展侧拿到的就是最终形态,缓存里也不会存多行;
2177
+ // 首行是空白 ⇒ 返回空串,扩展据此当作"这次不补"(与"模型没给出东西"同一处理)。
2178
+ const finalText = fimMultiline ? text : toSingleLine(text);
2179
+ fimStats.recordOk(usage, ms);
2180
+ return jsonResponse({ ok: true, text: finalText, usage, ms });
2181
+ } catch (error) {
2182
+ const ms = Date.now() - started;
2183
+ const message = error && error.message ? error.message : String(error);
2184
+ fimStats.recordFailure(error && error.code ? error.code : 'ERROR', ms);
2185
+ console.warn(`[code-server] FIM 补全失败:${message}`);
2186
+ return jsonResponse({ ok: false, error: error && error.code ? error.code : 'ERROR', reason: message, ms }, 502);
2187
+ } finally {
2188
+ fimBudget.release();
2189
+ }
2190
+ }
2191
+
2192
+ // FIM 运行时的声明都已执行到这里 ⇒ 现在才能安全调用(见前面那段 TDZ 说明)。
2193
+ // **不阻塞启动**(void):它是可选能力,解析不到就只是"补全不可用",不该拖慢插件激活。
2194
+ void ensureFimAdapter();
2195
+
1933
2196
  async function handleBridgeHealth() {
1934
2197
  // 无鉴权:只回一句"桥活着吗",不含任何编辑器数据(便于重启后一眼确认)。
1935
2198
  // 注意:bridge=true 只表示"配置已就绪",**不代表扩展在跑**(见 README 已知限制)。
@@ -2022,6 +2285,9 @@ export async function apply(ctx, config) {
2022
2285
  askDialog: askDialogLive(),
2023
2286
  // 诊断用:请求过对话框但客户端没响应(浏览器缓存旧 bundle?)。
2024
2287
  askDialogRequested: askDialog.open,
2288
+ // FIM(实验性)状态与用量:扩展据此决定要不要注册内联补全 provider,并把用量显示在状态栏。
2289
+ // **必须每趟都带**(它是唯一能让用户看到"补全花了多少"的地方 —— 这条调用不进 DSH 的计量)。
2290
+ fim: fimSnapshot(),
2025
2291
  });
2026
2292
  }
2027
2293
 
@@ -2262,3 +2528,30 @@ export async function apply(ctx, config) {
2262
2528
 
2263
2529
  console.log(`[code-server] static plugin loaded (serve=${requestedServe()} host=${cfg.host} port=${cfg.port === 0 ? '0=随机' : cfg.port})`);
2264
2530
  }
2531
+
2532
+ /**
2533
+ * 仅测试用:把一次桥请求交给**当前实例**的桥分发器,不经过命名管道 / unix socket。
2534
+ *
2535
+ * 为什么需要这条钩子(2026-09-22,0.3.63 CI 红 #2 的根治):本机沙箱里**连接**命名管道会 EPERM
2536
+ * (监听允许、连接不允许),于是 scripts/test-bridge-routes.mjs 里所有走 IPC 的用例在本机一律
2537
+ * SKIP —— "本地 pass=30" 里**不含**桥的状态码断言,`/complete` 的 403 就是这么在本地假绿、
2538
+ * 推到 runner 才红的。传输可以是本机的,断言不能是本机的:有了它,那些断言在任何机器上都真跑。
2539
+ *
2540
+ * 与真实传输**同构**(逐字段对照 nodeRouteFromFetch):同一个 dispatchBridge、body 同样收成
2541
+ * Buffer、Response 同样摊平成 {status, headers, text}。缺的只有 socket 本身 —— 所以它**不能**
2542
+ * 替代"监听口能起来"这条验证,那一条仍由真实运行的 IPC 覆盖(test-bridge-routes 里那条
2543
+ * 「桥走本机 IPC」的用例走的还是真端点)。
2544
+ */
2545
+ export async function bridgeRequestForTests({ path, method = 'GET', headers = {}, body = '' } = {}) {
2546
+ if (activeBridgeDispatch === null) throw new Error('bridgeRequestForTests: 还没有 apply() 过,拿不到桥分发器');
2547
+ const response = await activeBridgeDispatch(
2548
+ new URL(path, 'http://127.0.0.1'),
2549
+ String(method).toUpperCase(),
2550
+ // 与 Node 的 IncomingMessage.headers 同形(键全小写):guard 读 origin 与令牌头就靠它。
2551
+ Object.fromEntries(Object.entries(headers).map(([k, v]) => [String(k).toLowerCase(), v])),
2552
+ Buffer.from(body ?? '', 'utf8'),
2553
+ );
2554
+ const flat = {};
2555
+ response.headers.forEach((value, key) => { flat[key] = value; });
2556
+ return { status: response.status, headers: flat, text: await response.text() };
2557
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-code-server-app",
3
- "version": "0.3.59",
3
+ "version": "0.3.63",
4
4
  "description": "VS Code (from a code-server release) inside DSH: a right-sidebar tab driven by the plugin's own launcher over the in-process VS Code server (lib/launcher.mjs), 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": {
@@ -48,6 +48,7 @@
48
48
  "lib/dsh-resolve.mjs",
49
49
  "lib/child-node.mjs",
50
50
  "lib/edit-snapshot.mjs",
51
+ "lib/fim-adapter.mjs",
51
52
  "lib/launcher.mjs",
52
53
  "lib/serve-dsh.mjs",
53
54
  "lib/vendor.js",
@@ -63,6 +64,8 @@
63
64
  "vendor/VENDOR.json",
64
65
  "cordis.patch.yml",
65
66
  "LICENSE",
67
+ "LICENSE-Apache-2.0.txt",
68
+ "THIRD_PARTY_NOTICES.md",
66
69
  "README.md",
67
70
  "README.en.md"
68
71
  ],
@@ -79,7 +82,7 @@
79
82
  "@jinsiyu/dshcs-ssh2": "1.17.0",
80
83
  "@jinsiyu/dshcs-vscode-fs-copyfile": "2.0.0",
81
84
  "@jinsiyu/dshcs-vscode-proxy-agent": "0.44.0",
82
- "@jinsiyu/dshcs-vscode-server": "4.137.0",
85
+ "@jinsiyu/dshcs-vscode-server": "4.138.0",
83
86
  "@microsoft/1ds-core-js": "3.2.13",
84
87
  "@microsoft/1ds-post-js": "3.2.13",
85
88
  "@vscode/iconv-lite-umd": "0.7.1",
@@ -87,16 +90,16 @@
87
90
  "@vscode/sandbox-runtime": "0.0.1",
88
91
  "@vscode/tree-sitter-wasm": "0.3.1",
89
92
  "@vscode/vscode-languagedetection": "1.0.23",
90
- "@xterm/addon-clipboard": "0.3.0-beta.302",
91
- "@xterm/addon-image": "0.10.0-beta.300",
92
- "@xterm/addon-ligatures": "0.11.0-beta.300",
93
- "@xterm/addon-progress": "0.3.0-beta.300",
94
- "@xterm/addon-search": "0.17.0-beta.300",
95
- "@xterm/addon-serialize": "0.15.0-beta.300",
96
- "@xterm/addon-unicode11": "0.10.0-beta.300",
97
- "@xterm/addon-webgl": "0.20.0-beta.299",
98
- "@xterm/headless": "6.1.0-beta.302",
99
- "@xterm/xterm": "6.1.0-beta.303",
93
+ "@xterm/addon-clipboard": "0.3.0-beta.303",
94
+ "@xterm/addon-image": "0.10.0-beta.301",
95
+ "@xterm/addon-ligatures": "0.11.0-beta.301",
96
+ "@xterm/addon-progress": "0.3.0-beta.301",
97
+ "@xterm/addon-search": "0.17.0-beta.301",
98
+ "@xterm/addon-serialize": "0.15.0-beta.301",
99
+ "@xterm/addon-unicode11": "0.10.0-beta.301",
100
+ "@xterm/addon-webgl": "0.20.0-beta.300",
101
+ "@xterm/headless": "6.1.0-beta.303",
102
+ "@xterm/xterm": "6.1.0-beta.304",
100
103
  "bindings": "1.5.0",
101
104
  "cookie": "0.7.2",
102
105
  "detect-libc": "2.1.2",
@@ -104,12 +107,12 @@
104
107
  "http-proxy-agent": "7.0.0",
105
108
  "https-proxy-agent": "7.0.2",
106
109
  "jschardet": "3.1.4",
110
+ "katex": "0.16.22",
107
111
  "minimist": "1.2.8",
108
112
  "mkdirp": "1.0.4",
109
113
  "node-addon-api": "6.1.0",
110
114
  "tar": "7.5.22",
111
- "tas-client": "0.4.3",
112
- "tslib": "2.8.1",
115
+ "tas-client": "0.4.6",
113
116
  "typescript": "6.0.3",
114
117
  "uuid": "14.0.2",
115
118
  "vscode-oniguruma": "1.7.0",
@@ -1,12 +1,12 @@
1
1
  {
2
- "codeServerVersion": "4.137.0",
3
- "vscodeVersion": "1.137.0",
4
- "productPath": "stable-b11dabdaca0d3369986975be285db92c8795cea5",
2
+ "codeServerVersion": "4.138.0",
3
+ "vscodeVersion": "1.138.0",
4
+ "productPath": "stable-59c988c744a240b05b039f57b856a5312f19d5b1",
5
5
  "layout": "vscode-only",
6
- "preparedAt": "2026-09-20T15:36:43.387Z",
6
+ "preparedAt": "2026-09-22T02:29:14.118Z",
7
7
  "source": "registry",
8
8
  "node": "v24.20.0",
9
9
  "platform": "linux",
10
10
  "arch": "x64",
11
- "sizeMB": 197.9
11
+ "sizeMB": 198.3
12
12
  }