dsh-code-server-app 0.3.59 → 0.3.65

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.md CHANGED
@@ -27,11 +27,11 @@
27
27
  > 公式都一致,而且**不可能**版本错配),授权也在同一个对话框里就地处理 —— 详见
28
28
  > 「与 DSH 的协同:编辑器桥」。整条链上**没有任何构建步骤**。
29
29
 
30
- ## UI 载体与 DSH 版本要求(0.2.3 起只支持带右侧栏的 DSH)
30
+ ## UI 载体与 DSH 支持范围(0.2.3 起只支持带右侧栏的 DSH;2026-09-22 收敛为下面两条线)
31
31
 
32
32
  | DSH 版本 | 载体 | 入口 |
33
33
  |---|---|---|
34
- | **≥ 0.1.5-alpha.1**(有 `sidebarRight` / `sidebarRightTabs` 服务) | **右侧栏标签**(kind=`code-server`,标签名 `Code Server`),并**认领文件地址**(见下) | ① **DSH 官方的产物 chip / 「交付」卡片预览 / 正文里的文件名**(0.2.5 起,走官方 `openFile` → 文件地址 → 本 tab);② 右侧栏「开始」页的 **Code Server 入口框**;③ 设置 → 插件 → Code Server → **「在右侧栏打开」** |
34
+ | **rc 线** `0.1.5-rc.x`(最新 `0.1.5-rc.3` = npm `latest`/`next`)**与 alpha 线** `≥ 0.1.6-alpha.2`(最新 `0.1.7-alpha.1`)—— 判据是**有没有** `sidebarRight` / `sidebarRightTabs` 服务,不按版本号硬判 | **右侧栏标签**(kind=`code-server`,标签名 `Code Server`),并**认领文件地址**(见下) | ① **DSH 官方的产物 chip / 「交付」卡片预览 / 正文里的文件名**(0.2.5 起,走官方 `openFile` → 文件地址 → 本 tab);② 右侧栏「开始」页的 **Code Server 入口框**;③ 设置 → 插件 → Code Server → **「在右侧栏打开」** |
35
35
  | 更早(无右侧栏服务) | **不受支持**:除设置页的一条提示外**不提供任何入口** | 无(设置 → 插件 → Code Server 显示升级提示) |
36
36
 
37
37
  - 检测方式:先 `ctx.get('sidebarRightTabs') / ctx.get('sidebarRight')` 同步探测;
@@ -62,7 +62,7 @@
62
62
  含窄视口下的连带处理)。按钮找不到时保持原模式并 `console.warn` 一条,绝不影响面板渲染。
63
63
  - **IDE 常驻(0.2.2 起,默认开)**:切到别的标签/收起侧栏再回来**不再重载** code-server——
64
64
  未保存的编辑缓冲区、终端、调试会话都留在原处(见下方「为什么切标签不再重载」)。
65
- - 设置卡片只有**三个设置**:「**认领类型**」「**打开即全屏**」与「**后台常驻(切标签不重载)**」——没有其它行(0.2.7 起移除「入口」「依赖安装」「环境检测」)。
65
+ - 设置卡片只有**四组设置**:「**认领类型**」「**打开即全屏**」「**FIM 补全(实验性,默认关;含停顿毫秒数 / 允许多行 / 按 glob 禁用三个子项)**」与「**后台常驻(切标签不重载)**」——没有其它行(0.2.7 起移除「入口」「依赖安装」「环境检测」)。
66
66
  打开 IDE 用右侧栏「开始」页的 **Code Server 入口框**,或直接点官方的产物 chip / 交付卡片 / 正文文件名;
67
67
  诊断看 DSH host 日志里的 `[code-server]` 输出(`/api/code-server/status` 仍返回 `env` 供脚本排查)。
68
68
  旧版的 `windowedOpen`(窗口化打开)、`reserveComposer` 已在 0.2.6 移除:旧设置文档里残留的这两个键不会报错,只是被忽略(不再出现在 schema 里)。
@@ -200,7 +200,7 @@ DSH 用**资源地址**命名文件,`openFile` 只负责把地址交给右侧栏
200
200
  边界(说清楚,不夸大):这一层挡的是"本机其它应用/端口扫描器/浏览器页面"顺手访问你的 IDE;
201
201
  **同用户的本地恶意程序**本来就能直接读你的文件、也能读那个令牌文件 —— 那不在本插件的威胁模型内。
202
202
 
203
- - 在 `cordis.patch.yml` 的 `config.serve`(或设置文档里的 `code-server.serve`)切换,下次启动生效 —— **设置卡片不提供这一行**(卡片只有认领类型/打开即全屏/后台常驻三个设置)。
203
+ - 在 `cordis.patch.yml` 的 `config.serve`(或设置文档里的 `code-server.serve`)切换,下次启动生效 —— **设置卡片不提供这一行**(卡片只有认领类型/打开即全屏/后台常驻/FIM 补全四个设置)。
204
204
  - `dsh` 模式的实际收益:单一 URL/单一端口(远程访问 DSH 即可用 IDE)、不再暴露额外回环端口、认证与 DSH 同级。
205
205
  - `dsh` 模式的两点**已知取舍**:
206
206
  1. iframe 与 DSH **同源** → 该模式下不再挂 `sandbox`(同源 + `allow-same-origin` 可被 frame 自行摘除,属"看起来有防护");
@@ -365,6 +365,51 @@ seq,并且 `/sync` 回应里带 `lastSeq`(高水位),扩展据此自查游标是
365
365
  诊断:`GET /api/code-server/status` 的 `bridge` 字段返回
366
366
  `{ enabled, live, toolsRegistered, supported, url, file }` —— **不含令牌**(令牌只在那个文件里)。
367
367
 
368
+ ## FIM 补全(实验性,0.3.61 起,**默认关**)
369
+
370
+ 在编辑器里打字停顿时,光标后出现灰色续写(Tab 接受、Esc 丢弃)。**默认关闭**,在设置卡片里那一行开启,开启后即时生效(不用重启宿主、也不用重装插件)。
371
+
372
+ | 项 | 值 |
373
+ |---|---|
374
+ | 端点 | DeepSeek **FIM(Beta)**:`POST https://api.deepseek.com/beta/completions`,参数 `prompt`(前缀)+ `suffix`(后缀) |
375
+ | 模型 | `deepseek-flash`(官方 FIM 文档的示例模型,也就是本部署的默认模型) |
376
+ | 凭据 | 复用 DSH 里已有的 `DEEPSEEK_API_KEY`:`ctx.get('credentials').resolve(...)`,与官方适配器同一条路(取不到再退回启动环境变量) |
377
+ | 实测延迟 | **112–416 ms**(非流式;流式反而更慢 ⇒ 故意用非流式) |
378
+ | 触发条件 | 停顿 ≥250ms 且通过门控:有选区 / 非 `file` 文档 / 空上下文 / 文档 >2 万行 —— 这几种**根本不发请求** |
379
+
380
+ **它怎么接进来的(方案 A)**:FIM 走的是 Completions API,而 `ctx.llm.stream(GenerateOptions)` 的词汇表里
381
+ 只有 `messages`(没有 `prompt`/`suffix`,`purpose` 还是封闭联合 `'compaction' | 'session-title'`)⇒ 插件
382
+ **自己注册一个 LLM 适配器路由** `dshcs-fim`:把前缀/后缀装进一条带固定前缀(`dshcs-fim/1 `)的 messages 信封,
383
+ 适配器解出信封再打那个端点。这样这条调用**仍然走 DSH 的 LLM 服务** —— 取消、超时、终态 chunk、稳定错误码
384
+ 都按服务契约走(而不是插件自己直连绕过服务层)。依据与实测数据见 `docs/analysis-continuedev-reuse.md` 的 B6/B7。
385
+
386
+ **三个可调项(0.3.62,都在设置卡里,即时生效)**:
387
+
388
+ | 设置 | 默认 | 作用 |
389
+ |---|---|---|
390
+ | 停顿毫秒数 | `250` | 打字停下多久才请求一次。范围 100–3000ms(保存时夹取);**实测端点往返 112–416ms**,所以停顿基本就是"感知到的延迟" |
391
+ | 允许多行补全 | 开 | 关掉后宿主只回第一行;首行是空白 ⇒ 这次不补。想更克制(少被打扰)就关掉它 |
392
+ | 按 glob 禁用 | 空 | 这些文件里**根本不发请求**。语义:`*` 不跨目录、`**` 跨目录、不含 `/` 的模式只匹配文件名、含 `/` 的模式按**路径尾段**匹配(所以 `vendor/**`、`src/*.ts` 在任何层级都生效)、`/` 结尾视作 `/**`。例:`*.md;vendor/**;**/dist/**` |
393
+
394
+ > 这三项的语义在**宿主与扩展各有一份实现**(扩展是随包分发的静态文件,不能 import 宿主代码),
395
+ > 一致性由 `scripts/test-fim.mjs` 用同一组(模式,路径)语料对两侧做等价断言钉住。
396
+
397
+ **安全与边界**(这是本插件唯一会把内容发出去的能力):
398
+
399
+ - **只读不变**:扩展仍然不写文件、不执行命令 —— 它只"提议"一段文本,插入由你按 Tab 完成;
400
+ - 只有开启后桥才有第五条路由 `POST /complete`(**唯一一条会发起模型调用**的);没开一律 403,不发任何请求;
401
+ - 有界:前后缀各 ≤6000/2000 字符(120/40 行,字符与行数取先到者)、输出 ≤2000 字符、单次 4s 超时、
402
+ 最小间隔 120ms、并发 1、每分钟 60 次;超限的请求被拒(409/429),扩展这一轮就当"这次没有补全";
403
+ - 扩展侧还有一层:关掉开关后立刻注销 provider(连请求都不发),相同前后文 2 分钟内直接命中本地缓存;
404
+
405
+ **用量只在两处可见**:这条调用**不是会话请求**,不写会话日志 ⇒ DSH 自己的 token 计量(逐轮用量、
406
+ 上下文压力、遥测)**都不含它**。所以用量显示在:① IDE 状态栏的 DSH 项 —— 开启后多一格 `$(zap) 1.2k`,
407
+ 悬停看输入 / 输出 / 缓存命中 / 最近耗时 / 最近错误;② `GET /api/code-server/status` 的 `fim` 字段(同一份快照)。
408
+
409
+ **已知限制**:模型偶尔会在"并不需要补"的位置硬凑一段(实测在不该补的位置 2/2 复现);过滤管线会剥掉代码围栏与
410
+ 控制标记,但"该不该补"最终由你判断 —— Esc 或继续打字都会让它消失。更稳的形态要等更快的完成路由,
411
+ 或者 DSH 把 completion 做成一等请求(那时把适配器里的取数换成新 API 即可,调用方一行不用改)。
412
+
368
413
  ## 旧版 DSH(0.2.3 起不再支持)
369
414
 
370
415
  **行为**:探测不到 `sidebarRightTabs` / `sidebarRight` 时,插件只注册一张设置卡片,内容是:
@@ -372,7 +417,7 @@ seq,并且 `/sync` 回应里带 `lastSeq`(高水位),扩展据此自查游标是
372
417
  > **Code Server** — 当前 DSH 版本不受支持(缺少右侧栏服务)
373
418
  > 本插件自 0.2.3 起不再兼容旧版 DSH。
374
419
  > 未检测到右侧栏插件服务 sidebarRightTabs / sidebarRight,因此插件不提供任何入口(旧版的悬浮球与浮动窗口已移除),
375
- > 也不会后台启动 IDE。升级 DSH 到带右侧栏的版本(≥ 0.1.5-alpha.1)后,Code Server 会出现在右侧栏标签里,
420
+ > 也不会后台启动 IDE。升级 DSH 到 rc 线(0.1.5-rc.x)或 0.1.6-alpha.2 起的 alpha 线后,Code Server 会出现在右侧栏标签里,
376
421
  > 本页同时显示完整设置项;升级后无需重装本插件,刷新页面即可。
377
422
 
378
423
  - **没有任何其他 UI**:不注册 `shell.overlay`(悬浮球)、不认领文件地址、不做常驻预热。
@@ -388,8 +433,8 @@ seq,并且 `/sync` 回应里带 `lastSeq`(高水位),扩展据此自查游标是
388
433
 
389
434
  - code-server 服务目录**跟随活动工作区/会话**:打开期间切换 DSH 会话/工作区,code-server 自动切到新目录
390
435
  (解析优先级:当前会话 cwd → 会话所属 `workspace.path` → 最近活跃会话所属 workspace → 首个 workspace.path;
391
- **"当前会话"的信源随 DSH 版本而变**:≥ 0.1.6-alpha.2 读会话作用域标准 prop `sessionId`,
392
- ≤ 0.1.6-alpha.1 退回会话列表快照上的 `current` —— 详见下面 0.3.48 那条;纯逻辑内联在 `lib/client.js`
436
+ **"当前会话"的信源随支持线而变**:alpha 线(≥ 0.1.6-alpha.2)读会话作用域标准 prop `sessionId`,
437
+ rc 线(≤ 0.1.5-rc.3)退回会话列表快照上的 `current` —— 详见下面 0.3.48 那条;纯逻辑内联在 `lib/client.js`
393
438
  的"工作区解析"段,两版形状的契约由 `scripts/test-client-bundle-cwd.mjs` 直接对**入口**钉住);
394
439
  打开目录显示在 code-server 页面内(`?folder=<cwd>`,跟随切换时页面自动重新加载);
395
440
  实现要点:iframe src 必须带 `?folder=<cwd>`——code-server 前端会记住“最近工作区”并自行恢复,
@@ -422,7 +467,7 @@ seq,并且 `/sync` 回应里带 `lastSeq`(高水位),扩展据此自查游标是
422
467
  (客户端半部与提问面板都是入库的手写源码 —— 整条链上**没有任何构建步骤**)。
423
468
 
424
469
  > 本机(BM: Windows 11 ARM64)实测:树/依赖全链路是"平台子包直挂插件依赖"供给 ——
425
- > 树包 `@jinsiyu/dshcs-vscode-server`(当前 4.137.0,50.8 MB tgz)、纯 JS 内部依赖与 8 个平台无关
470
+ > 树包 `@jinsiyu/dshcs-vscode-server`(当前 4.138.0,50.9 MB tgz)、纯 JS 内部依赖与 8 个平台无关
426
471
  > 重打包包直接进插件 `dependencies`、8 个平台专属重打包包按 win32-arm64/x64 进
427
472
  > `optionalDependencies`(包自带 os/cpu 自动选),原始名字由 `lib/native.js` 补 junction 还原
428
473
  > → healthz 200 → 停止 → 回收全链路验证。
@@ -478,7 +523,7 @@ pnpm run promote -- <version>
478
523
  | 目标 | 命令 |
479
524
  |---|---|
480
525
  | **打最新版(上游 code-server 发行版)** | `pnpm run vendor:latest`(= `--force`):从 registry 取 `code-server@latest` 的树到 `vendor/vscode`;之后**必须**重跑 `repack:build` 并重发全部子包 |
481
- | **指定版本** | `pnpm run vendor:vscode -- --version 4.137.0` |
526
+ | **指定版本** | `pnpm run vendor:vscode -- --version 4.138.0` |
482
527
  | **从已装好的树快照** | `pnpm run vendor:vscode -- --from <code-server 目录>`(秒级) |
483
528
  | **开发期让树可直接跑** | `pnpm run vendor:vscode -- --dev-links`(额外把 `lib/vscode/node_modules` 用 junction 补上) |
484
529
  | **完整重打子包** | `pnpm run repack:build -- --target win32-arm64,win32-x64 --pack`(不给 `--from` 会自动 npm install 解包 + 编译,耗时)。`--target` 只决定**本次在这台机器上构建哪些目标**(原生包要现编,所以 Linux 目标在 Linux 机器或 `repacks.yml` 的 Linux 腿上构建);产品覆盖哪些目标、每模块在哪些目标上有子包,由 `scripts/repack-platforms.json` 决定 |
@@ -505,12 +550,12 @@ pnpm test:bridge-extension # 编辑器桥扩展侧纯逻辑:未保存缓冲区
505
550
  pnpm test:ask-dialog # 「问 DSH」对话框的接线:没有产物/构建链了、宿主 4 条 ask 路由、扩展只上报编辑器状态、授权四条、桥的安全不变式
506
551
  pnpm test:launcher-routes # launcher 的 HTTP 面(起真进程,较慢)
507
552
  pnpm test:workspace-switch # 切工作区不重启进程
508
- pnpm test:workspace-cwd # "当前工作区目录"解析:DSH 0.1.6-alpha.2(sessionId)与旧版(current)两套形状
553
+ pnpm test:workspace-cwd # "当前工作区目录"解析:alpha 线(≥0.1.6-alpha.2,sessionId)与 rc 线(≤0.1.5-rc.3,current)两套形状
509
554
  pnpm test:client-cwd # 同一件事但直接对**客户端入口** lib/client.js 验(注册出来的 body 真发不发 cwd、URL 带不带 folder)
510
555
  pnpm test:client-tabs # "DSH 侧只留一个 code-server 标签页":新标签挂载时收掉同窗格旧标签(跨窗格/不可见时不动)
511
556
  pnpm test:client-entry # 客户端入口守卫:经典脚本+工厂包装、require 白名单(= DSH 模块表种子词)、src/ 已消失、与 lib/claim-types.js 逐字一致
512
557
  pnpm test:ask-panel # 「问 DSH」对话框面板(0.3.59 起手写):注入机制已下线、视图白名单、注入 CSS 的选择器/var() 安全、六种条目与授权卡片、三条消息落点、拿不到官方部件时的降级
513
- pnpm test:client-seat # 设置卡住哪个座位:插件页 plugins.bundle.config(DSH ≥ 0.1.6-alpha.2)vs settings.plugin.item(≤ alpha.1;新版已退役)
558
+ pnpm test:client-seat # 设置卡住哪个座位:alpha 线用插件页 plugins.bundle.config(≥ 0.1.6-alpha.2)vs rc 线用 settings.plugin.item(≤ 0.1.5-rc.3;alpha 线已退役)
514
559
  pnpm test:fullscreen # 打开标签即全屏
515
560
  pnpm test:vendored # 重打包表 ↔ 插件依赖表一致(无 npm: 别名 / 无聚合包 / vendored.json 进了 files)
516
561
  pnpm test:dsh-resolve # 部署位置表:各平台全局装布局(npm --prefix / nvm / pnpm global / %APPDATA%)都能找到 DSH 部署
@@ -940,7 +985,7 @@ dsh plugin --profile web add C:\Users\User\Desktop\dsh-code-server-app
940
985
  ### 升级 VS Code 树(上游 = code-server 发行版)
941
986
 
942
987
  - **打包期决定版本**:`pnpm run vendor:latest`(= `--force`)取 npm **最新版 code-server** 的树并重建 `vendor/vscode`;
943
- 也可 `pnpm run vendor:vscode -- --version 4.137.0` 或设 `DSHCS_CODE_SERVER_VERSION`。
988
+ 也可 `pnpm run vendor:vscode -- --version 4.138.0` 或设 `DSHCS_CODE_SERVER_VERSION`。
944
989
  已有 `vendor/vscode` 时,不带 `--force`/`--version` 不会升级(日常 `pnpm pack` 是 no-op)。
945
990
  **源树优先级(0.2.13 修正)**:显式 `--from` 用给定树;显式 `--force`/`--version` **一定走 registry**
946
991
  —— 修正前它们会被"本机已有源树"抢走(`defaultSourceTree()` 先命中 `vendor/code-server` 或 profile 里的旧树),
@@ -959,7 +1004,7 @@ dsh plugin --profile web add C:\Users\User\Desktop\dsh-code-server-app
959
1004
  - `productPath`(`<quality>-<commit>`,客户端 WS 路径的组成)**从 `lib/vscode/product.json` 现算**,
960
1005
  升级树后无需改代码 —— 但也意味着切版本后必须重启 dsh web(路由在激活期注册)。
961
1006
  - **不再有运行期自动升级**:不会在启动时联网取 latest;版本完全由内置产物决定。
962
- - 本机当前内置:`code-server@4.137.0` 的树(VS Code 1.137.0,`productPath=stable-b11dabda…`)。
1007
+ - 本机当前内置:`code-server@4.138.0` 的树(VS Code 1.138.0,`productPath=stable-59c988c7…`)。
963
1008
 
964
1009
  ### 兼容旧安装位
965
1010
 
@@ -974,8 +1019,8 @@ host 探测顺序:`@jinsiyu/dshcs-vscode-server/vscode`(**0.2.0+ 正式布局**)
974
1019
 
975
1020
  | DSH | 座位 | 长什么样 |
976
1021
  |---|---|---|
977
- | **≥ 0.1.6-alpha.2** | `plugins.bundle.config`,**键 = 包名** `dsh-code-server-app` | 插件页 → 找到 `dsh-code-server-app` → 打开该插件页面,设置区在**描述与各行之间**(页面自己画标题/图标/面包屑,我们只出表单 + 保存控件) |
978
- | ≤ 0.1.6-alpha.1 | `settings.plugin.item`(key `code-server`) | 设置 → 插件 → Code Server 的自绘可折叠卡片(该插槽在新版**已退役**) |
1022
+ | **alpha 线 ≥ 0.1.6-alpha.2**(最新 0.1.7-alpha.1) | `plugins.bundle.config`,**键 = 包名** `dsh-code-server-app` | 插件页 → 找到 `dsh-code-server-app` → 打开该插件页面,设置区在**描述与各行之间**(页面自己画标题/图标/面包屑,我们只出表单 + 保存控件) |
1023
+ | **rc 线 ≤ 0.1.5-rc.3**(最新 rc 仍是这一支) | `settings.plugin.item`(key `code-server`) | 设置 → 插件 → Code Server 的自绘可折叠卡片(该插槽在新版**已退役**) |
979
1024
 
980
1025
  > 为什么必须跟着搬:上游 agent note《插件页上的插件配置》把配置从设置页搬到插件页,并**删掉了
981
1026
  > `settings.plugin.item`**;插件页只在 `ledger.bundles.has(包名)` 时才渲染那块区域 —— 键写错或还用旧座位,
@@ -988,8 +1033,12 @@ host 探测顺序:`@jinsiyu/dshcs-vscode-server/vscode`(**0.2.0+ 正式布局**)
988
1033
  | `claimExtensions` | `*` + 三组排除(预览友好 / 可执行文件 / Office 文档;完整清单见「认领类型」一节) | **认领类型**(0.2.11,取代 0.2.5 的 `fileOpenScope`):按扩展名决定哪些文件交给 VS Code,分号分隔;`*` = 其余类型也认领,`!ext` = 不认领(排除优先)。默认把 DSH 预览渲染得好的四类(markdown/html/图片/PDF)**以及可执行文件(0.3.51)与 Office 文档(0.3.51)**留给 DSH,其余全进 IDE;清空 = 不认领任何文件。**不再区分 session/absolute 作用域** |
989
1034
  | `fullscreenOnOpen` | `true` | **打开即全屏**(0.2.9):打开 Code Server 标签(含点开文件)时自动把右侧栏切到全屏(铺满窗口);关闭则保持 DSH 默认的 push(与对话并排)。只影响打开那一刻,用户点「退出全屏」不会被抢回去 |
990
1035
  | `keepResident` | `true` | **后台常驻**:开启后宿主启动即把 IDE 预加载到"停放区",切标签/收起侧栏不重载、首次打开免等待;关闭则只在打开面板时加载(省内存) |
1036
+ | `fim` | `false` | **FIM 补全(实验性,0.3.61)**:开启后编辑器里打字停顿会出现灰字续写(Tab 接受 / Esc 丢弃)。**默认关闭** —— 它是本插件唯一会把内容发出去的能力(每次补全把光标附近有上限的一小段代码交给模型),而且这条调用**不进 DSH 的 token 计量**,用量只在 IDE 状态栏可见。详见「FIM 补全」一节 |
1037
+ | `fimDebounceMs` | `250` | **FIM · 停顿毫秒数**(0.3.62):打字停下多久才发一次补全请求。有效范围 **100–3000ms**,保存时按它夹取(与宿主同一套规则);空/非法值回落 250 |
1038
+ | `fimMultiline` | `true` | **FIM · 允许多行补全**(0.3.62):关掉后宿主只回第一行(首行为空 = 这次不补)。实测模型在不该补的位置会硬凑,多行会放大这种噪声 |
1039
+ | `fimDisableGlobs` | 空 | **FIM · 按 glob 禁用**(0.3.62):分号/换行分隔。`*` 不跨目录、`**` 跨目录、不含 `/` 的模式只匹配文件名、含 `/` 的模式按路径尾段匹配、`/` 结尾视作 `/**`。例:`*.md`、`vendor/**`、`**/dist/**`。**两边都判**:扩展侧先判(根本不发请求),宿主侧再判一遍 |
991
1040
 
992
- (0.2.9 起卡片只留上面三个设置;`windowedOpen` 与 `reserveComposer` 已移除 —— 旧设置文档里残留的键既不报错也不生效。
1041
+ (0.2.9 起卡片只留上面这些设置(0.3.61 加 FIM 补全,0.3.62 加它的三个子项);`windowedOpen` 与 `reserveComposer` 已移除 —— 旧设置文档里残留的键既不报错也不生效。
993
1042
  `serve` 仍是设置命名空间里的键(便于用设置文档切换),但**没有卡片行**,见「服务方式」。)
994
1043
 
995
1044
  0.2.7 起卡片**没有**「入口」「依赖安装」「环境检测」三行:入口在右侧栏「开始」页的 Code Server 入口框(或官方的文件点击),
@@ -1113,6 +1162,19 @@ desktop profile 由 `apps/desktop-host` 把 `/api/*` 交给同一个 `createShar
1113
1162
  ```
1114
1163
  - 影响面:不只本插件——**任何**客户端插件升级后都可能继续跑旧代码;发布后请按上面的标记法确认渲染器真的换了 bundle。
1115
1164
 
1165
+ ## 第三方与许可
1166
+
1167
+ 本项目自身以 **MIT** 发布(见 [`LICENSE`](LICENSE))。实验性 **FIM 补全**的设计**参考**了
1168
+ [continuedev/continue](https://github.com/continuedev/continue)(Apache License 2.0,Copyright 2023 Continue)——
1169
+ 具体是它 `tabAutocompleteOptions` 的设置项划分(`debounceDelay` / `useAutocompleteMultilineCompletions` /
1170
+ `disableInFiles`)与停顿去抖、光标附近窗口、过滤、有界缓存的思路。
1171
+
1172
+ **本项目不含 continuedev/continue 的任何源码片段、模板字符串或整份文件**(补全请求走 DeepSeek 官方
1173
+ FIM(Beta)端点,提示词形态由其官方文档与本机实测确定)。标准 Apache-2.0 全文随包分发
1174
+ ([`LICENSE-Apache-2.0.txt`](LICENSE-Apache-2.0.txt)),逐处参考登记见
1175
+ [`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md) —— 将来若真移植代码片段,Apache-2.0 的义务
1176
+ (声明、标注被改动文件、保留上游 NOTICE)已经就位。Apache-2.0 不含商标授权,本项目不使用 "Continue" 作名称或宣传。
1177
+
1116
1178
  ## 已知限制
1117
1179
 
1118
1180
  - **~~编辑器桥需要 DSH 提供 `webServer`~~ 已不成立(0.3.13 修正)**:桥改走**本机 IPC**
@@ -1153,6 +1215,7 @@ desktop profile 由 `apps/desktop-host` 把 `/api/*` 交给同一个 `createShar
1153
1215
  `loopback` 模式跨源,`sandbox` 作为真防护保留。
1154
1216
  - **跨会话单实例**:host 级共享一份 IDE;切换 cwd 只换 workbench 目录(0.2.12 起不重启进程,旧目录的后台终端不会被收走)。
1155
1217
  - **旧版 DSH 不受支持(0.2.3 起)**:没有 `sidebarRightTabs`/`sidebarRight` 的 DSH 上,除设置页一条升级提示外无任何入口;
1218
+ 支持范围**只有两条线**(2026-09-22 收敛):rc 线 `0.1.5-rc.x`(旧座位 + 快照上的 `current`)与 alpha 线 `≥ 0.1.6-alpha.2`(新座位 + `sessionId` 标准 prop);更早的 alpha(`0.1.5-alpha.x`、`0.1.6-alpha.1`)**不单独支持** —— 形状与 rc 线相同,所以代码走得通,但不作为验证目标。
1156
1219
  旧版用户请留在 `0.2.2`(`dsh plugin --profile web add dsh-code-server-app@0.2.2`)。
1157
1220
  - **侧栏标签切换**(0.2.2 起不再重载):DSH 右侧栏只渲染当前激活标签的 body,React 卸载会移走 iframe;
1158
1221
  插件把 iframe 收成单例常驻面,用 `Element.moveBefore()`(状态保持型原子移动)在停靠位与文档级停放区之间搬,
@@ -0,0 +1,60 @@
1
+ # 第三方声明(Third-Party Notices)
2
+
3
+ 本项目 `dsh-code-server-app` 自身以 **MIT** 许可发布(见 [`LICENSE`](LICENSE))。
4
+ 在开发过程中**参考**过下列第三方开源项目,本文件登记参考范围、许可与本项目的处理方式。
5
+
6
+ ---
7
+
8
+ ## continuedev/continue —— Apache License 2.0
9
+
10
+ | 项 | 值 |
11
+ |---|---|
12
+ | 上游 | https://github.com/continuedev/continue |
13
+ | 参考时修订 | `5522c6f44ca0ac3528b37244818fbfa39b5af470`(`refs/heads/main`,2026-09-21 取) |
14
+ | 许可 | **Apache License 2.0**,Copyright 2023 Continue |
15
+ | 上游 `NOTICE` | **没有**(已核对根目录清单;将来若出现,按 Apache-2.0 一并保留) |
16
+ | 许可全文 | [`LICENSE-Apache-2.0.txt`](LICENSE-Apache-2.0.txt)(标准 Apache-2.0 文本,11,357 字节,含 §9 与 APPENDIX) |
17
+
18
+ ### 参考了什么(逐处登记)
19
+
20
+ | 本项目位置 | 参考内容 | 是否含上游代码 |
21
+ |---|---|---|
22
+ | `lib/fim-adapter.mjs`、`assets/extensions/dshcs-editor-bridge/lib/fim-completion.js` | 实验性 **FIM(幽灵)补全**的**设计**:停顿去抖、只送光标附近的窗口、结果过滤与单行化、有界缓存、按文件禁用;以及**设置项划分**(对应上游 `tabAutocompleteOptions` 的 `debounceDelay` / `useAutocompleteMultilineCompletions` / `disableInFiles`) | **否**,独立实现 |
23
+ | `lib/index.js`、`lib/client.js`、`assets/extensions/dshcs-editor-bridge/extension.js` | 上述设计在本插件内的接线(桥的第五条路由、设置卡三行、内联补全 provider) | 否 |
24
+ | `docs/analysis-continuedev-reuse.md` | 对上游模块的**事实性描述与判定**(路径、字节数、blob sha、用途) | 否(事实与评述) |
25
+
26
+ **本项目的全部源码均为独立编写,不含 continuedev/continue 的任何源码片段、模板字符串或整份文件。**
27
+ 补全请求走的是 DeepSeek 官方的 FIM(Beta)端点(`POST https://api.deepseek.com/beta/completions`,
28
+ 参数 `prompt` + `suffix`),提示词形态由该官方文档与本机实测确定,与上游的 FIM 模板表无关
29
+ (那张表在本机路由上实测不可用,见 `docs/analysis-continuedev-reuse.md` 的 B6)。
30
+
31
+ ### 将来若要移植上游代码片段,按 Apache-2.0 必须做到
32
+
33
+ 1. **标注被改动的文件**:在每个移植文件头部写明 `来源: continuedev/continue@<sha> <path> · Apache-2.0 · 已改动`;
34
+ 2. **保留版权与许可声明**:本文件 + `LICENSE-Apache-2.0.txt` 已经随包分发,移植时无需新增文件;
35
+ 3. **上游 `NOTICE` 若出现则一并保留**;
36
+ 4. **不含商标授权**:Apache-2.0 不授予商标权 —— 不得用 "Continue" 作为本项目的名称、包名或宣传语。
37
+
38
+ ---
39
+
40
+ ## 随包分发的第三方依赖
41
+
42
+ 本包依赖的第三方包(`@jinsiyu/dshcs-*`、`@vscode/*`、`@xterm/*`、`zod` 等)各自带有许可与声明文件,
43
+ 随 `node_modules` 一并分发,不在此重复登记;内嵌的 VS Code 树自带 `ThirdPartyNotices.txt`。
44
+
45
+ ---
46
+
47
+ ## English summary
48
+
49
+ `dsh-code-server-app` is MIT-licensed (see `LICENSE`). Its experimental **FIM completion** feature was
50
+ **designed with reference to** [continuedev/continue](https://github.com/continuedev/continue)
51
+ (Apache License 2.0, Copyright 2023 Continue) — specifically the settings taxonomy of upstream
52
+ `tabAutocompleteOptions` (`debounceDelay`, `useAutocompleteMultilineCompletions`, `disableInFiles`) and its
53
+ debounce / cursor-window / filter / bounded-cache approach.
54
+
55
+ **No source code, template strings, or files from continuedev/continue are included in this project**;
56
+ every implementation here is independently written, and the completion requests target DeepSeek's official
57
+ FIM (Beta) endpoint as documented by DeepSeek. The standard Apache License 2.0 text ships as
58
+ `LICENSE-Apache-2.0.txt` so that Apache-2.0's obligations (notices, marking modified files, preserving an
59
+ upstream `NOTICE` if one appears) are already satisfied should a code fragment ever be ported in.
60
+ Apache-2.0 grants no trademark rights: "Continue" must not be used as this project's name or in its promotion.
@@ -38,6 +38,17 @@ const {
38
38
  createClient,
39
39
  } = require('./lib/bridge-client.js');
40
40
  const { createProjector } = require('./lib/context-model.js');
41
+ const {
42
+ FIM_DEBOUNCE_DEFAULT_MS,
43
+ shouldRequest,
44
+ buildPayload,
45
+ fingerprint,
46
+ createCompletionCache,
47
+ formatTokens,
48
+ describeFimUsage,
49
+ matchDisabledGlob,
50
+ toSingleLine,
51
+ } = require('./lib/fim-completion.js');
41
52
  const { chooseOldSide, createDiffCache, describeChange } = require('./lib/diff-model.js');
42
53
 
43
54
  /** 状态栏项(仅在桥连通时显示)。 */
@@ -65,6 +76,119 @@ const diffTextStore = new Map();
65
76
  /** 同一个文件的 diff tab 不重复开。 */
66
77
  const openDiffTabs = new Map();
67
78
 
79
+ // ---------------------------------------------------------------- FIM(幽灵)补全:实验性
80
+ //
81
+ // 这条能力**只在宿主说 enabled=true 时才注册 provider**(宿主的 /sync 每趟都带上来),
82
+ // 所以"设置里没开"不会让编辑器每敲一个字就发一次注定被拒的请求。
83
+ //
84
+ // 三条与宿主一致的分工:
85
+ // - **该不该问**在这里判(纯逻辑 shouldRequest):不值得问的位置根本不发请求 ——
86
+ // 实测模型在被问到"不该补的地方"时会硬凑(2/2),所以判据必须前置;
87
+ // - **问什么**在这里裁(窗口内的前后文),宿主侧再兜一层同样的上限;
88
+ // - **问过没有**用本地缓存答(相同前后文 + 语言)。
89
+ /** 宿主报来的 FIM 状态与用量({enabled:false} 表示设置里没开)。 */
90
+ let fimSnapshot = { enabled: false };
91
+ /** 已注册的 provider disposer(null = 未注册)。 */
92
+ let fimProvider = null;
93
+ /** 扩展上下文(activate 里赋值):provider 需要在轮询里注册/注销,而轮询拿不到 context 参数。 */
94
+ let extensionContext = null;
95
+ /** 前后文 → 补全文本 的有界缓存。 */
96
+ const fimCache = createCompletionCache();
97
+
98
+ /** 取消感知的停顿:打字过程中的请求全丢掉(宿主侧也有速率闸,但那是最后一道)。
99
+ * 停顿毫秒数由设置在宿主侧提供(0.3.62),每趟 /sync 带上来;拿不到就用默认值。 */
100
+ async function fimWait(token) {
101
+ const configured = Number.isFinite(fimSnapshot.debounceMs) ? fimSnapshot.debounceMs : FIM_DEBOUNCE_DEFAULT_MS;
102
+ const deadline = Date.now() + configured;
103
+ while (Date.now() < deadline) {
104
+ if (token.isCancellationRequested === true) return false;
105
+ // 40ms 一跳:足够细,且不会为一次补全拉起一串定时器。
106
+ await new Promise((resolve) => setTimeout(resolve, 40));
107
+ }
108
+ return token.isCancellationRequested !== true;
109
+ }
110
+
111
+ /** 取光标前后的窗口(只按行取,不把整份文档摸一遍 —— 大文件上每次击键都 getText() 是灾难)。 */
112
+ function fimWindows(document, position) {
113
+ const startLine = Math.max(0, position.line - 120);
114
+ const endLine = Math.min(document.lineCount - 1, position.line + 40);
115
+ const prefixText = document.getText(new vscode.Range(new vscode.Position(startLine, 0), position));
116
+ const afterEnd = document.lineAt(endLine).range.end;
117
+ const suffixText = document.getText(new vscode.Range(position, afterEnd));
118
+ const lineTextBefore = document.lineAt(position.line).text.slice(0, position.character);
119
+ return { prefixText, suffixText, lineTextBefore };
120
+ }
121
+
122
+ /** 内联补全 provider。返回 null = 这次不补(与"补一个空串"是两回事)。 */
123
+ function createFimProvider() {
124
+ return {
125
+ async provideInlineCompletionItems(document, position, _context, token) {
126
+ if (fimSnapshot.enabled !== true) return null;
127
+ const editor = vscode.window.activeTextEditor;
128
+ const windows = fimWindows(document, position);
129
+ const decision = shouldRequest({
130
+ selectionEmpty: editor === undefined || editor === null ? true : editor.selection.isEmpty,
131
+ scheme: document.uri.scheme,
132
+ lineCount: document.lineCount,
133
+ prefixText: windows.prefixText,
134
+ suffixText: windows.suffixText,
135
+ lineTextBefore: windows.lineTextBefore,
136
+ });
137
+ if (decision.ask !== true) return null;
138
+ const payload = buildPayload({
139
+ prefixText: windows.prefixText,
140
+ suffixText: windows.suffixText,
141
+ language: document.languageId,
142
+ path: document.uri.scheme === 'file' ? document.uri.fsPath : document.uri.toString(),
143
+ });
144
+ // 按 glob 禁用(0.3.62):**在发请求之前**判 —— 这是这一层存在的主要理由(不花冤枉钱)。
145
+ // 宿主侧还会再判一次(服务端那道),所以即使这里漏了也不会真的发出去。
146
+ const blocked = matchDisabledGlob(payload.path, fimSnapshot.disableGlobs);
147
+ if (blocked !== null) return null;
148
+ const key = fingerprint(payload.prompt, payload.suffix, payload.language);
149
+ const cached = fimCache.get(key);
150
+ if (cached !== null) {
151
+ return cached === '' ? null : [new vscode.InlineCompletionItem(cached, new vscode.Range(position, position))];
152
+ }
153
+ if ((await fimWait(token)) !== true) return null;
154
+ if (client === null || client.isDormant()) return null;
155
+ if (typeof client.complete !== 'function') return null;
156
+ const result = await client.complete(payload);
157
+ if (token.isCancellationRequested === true) return null;
158
+ if (result.ok !== true) {
159
+ if (result.status !== 409 && result.status !== 429) log(`补全失败:${result.error ?? '未知'}(status=${result.status ?? 0})`);
160
+ return null;
161
+ }
162
+ // 多行关掉时宿主已经裁过一道;这里再裁一次是防"宿主版本比扩展旧"(随包分发的静态文件可能不同步)。
163
+ const raw = typeof result.text === 'string' ? result.text : '';
164
+ const text = fimSnapshot.multiline === false ? toSingleLine(raw) : raw;
165
+ fimCache.set(key, text);
166
+ if (text.trim() === '') return null; // 模型说"这里不用补"是合法回答
167
+ log(`补全 ${text.length} 字符(${result.ms ?? '?'}ms,缓存${result.usage?.cacheReadTokens ?? 0} tok)`);
168
+ return [new vscode.InlineCompletionItem(text, new vscode.Range(position, position))];
169
+ },
170
+ };
171
+ }
172
+
173
+ /** 按宿主状态注册/注销 provider(状态翻转时才动)。 */
174
+ function syncFimProvider() {
175
+ const want = fimSnapshot.enabled === true;
176
+ if (want && fimProvider === null) {
177
+ fimProvider = vscode.languages.registerInlineCompletionItemProvider({ pattern: '**' }, createFimProvider());
178
+ if (extensionContext !== null) extensionContext.subscriptions.push(fimProvider);
179
+ log('FIM 补全(实验性)已启用:开始提供内联补全');
180
+ } else if (!want && fimProvider !== null) {
181
+ try {
182
+ fimProvider.dispose();
183
+ } catch {
184
+ // 已被 VS Code 回收
185
+ }
186
+ fimProvider = null;
187
+ fimCache.clear();
188
+ log('FIM 补全已关闭:不再提供内联补全');
189
+ }
190
+ }
191
+
68
192
  /** 把"改动前的文本"登记进 store,返回它的 docId。 */
69
193
  function registerDiffText(text) {
70
194
  const docId = crypto.createHash('sha256').update(text ?? '').digest('hex');
@@ -404,6 +528,13 @@ async function pollOnce() {
404
528
  await handleEvent(event);
405
529
  }
406
530
  if ((result.events ?? []).length > 0) client.persist();
531
+ // FIM(实验性):宿主每趟带状态与用量上来。只在**翻转**时注册/注销 provider,
532
+ // 但状态栏每趟都刷 —— 用户要看到的是"这轮补全花了多少 token",它一直在变。
533
+ const nextFim = result.fim !== null && typeof result.fim === 'object' ? result.fim : { enabled: false };
534
+ const wasEnabled = fimSnapshot.enabled === true;
535
+ fimSnapshot = nextFim;
536
+ if ((nextFim.enabled === true) !== wasEnabled) syncFimProvider();
537
+ updateStatusBar();
407
538
  // 能力探测(0.3.24):宿主证明"DSH 页面里的对话框活着"⇒ 右键提问走 ask-open 事件。
408
539
  // 0.3.59 起这是**唯一**的提问 UI(编辑器里的 webview 面板已退役):探测不到就只弹一条提示,
409
540
  // 绝不退回到一扇用户看不见的窗。
@@ -430,8 +561,23 @@ function startPolling(context) {
430
561
  function updateStatusBar() {
431
562
  if (statusBar === null) return;
432
563
  if (connected && client !== null && client.config !== null) {
433
- statusBar.text = '$(plug) DSH';
434
- statusBar.tooltip = `编辑器桥已连接:${client.config.pipe}\n上次轮询:${lastPollAt === 0 ? '—' : new Date(lastPollAt).toLocaleTimeString()}\n点击查看日志`;
564
+ // FIM(实验性)开着时,状态栏多一格 token 计数 —— 这条链路的调用**不进 DSH 的计量**
565
+ // (一次性调用不是 loop 请求,不进会话日志 ⇒ token-meter 看不到它),所以这里是用户唯一能看到
566
+ // "补全花了多少"的地方(设置卡里也有同样一份,见 README)。
567
+ const fimOn = fimSnapshot !== null && fimSnapshot.enabled === true;
568
+ const used = fimOn ? Number(fimSnapshot.totalTokens ?? 0) : 0;
569
+ statusBar.text = fimOn
570
+ ? (used > 0 ? `$(plug) DSH $(zap) ${formatTokens(used)}` : '$(plug) DSH $(zap)')
571
+ : '$(plug) DSH';
572
+ const lines = [
573
+ `编辑器桥已连接:${client.config.pipe}`,
574
+ `上次轮询:${lastPollAt === 0 ? '—' : new Date(lastPollAt).toLocaleTimeString()}`,
575
+ '',
576
+ ...describeFimUsage(fimSnapshot),
577
+ '',
578
+ '点击查看日志',
579
+ ];
580
+ statusBar.tooltip = lines.join('\n');
435
581
  statusBar.command = 'dsh-code-server.showBridgeLog';
436
582
  statusBar.show();
437
583
  } else {
@@ -523,6 +669,7 @@ function showBridgeLog() {
523
669
  // ---------------------------------------------------------------- 激活
524
670
 
525
671
  function activate(context) {
672
+ extensionContext = context;
526
673
  output = vscode.window.createOutputChannel('DSH Editor Bridge');
527
674
  context.subscriptions.push(output);
528
675
 
@@ -35,6 +35,9 @@ const STATE_FILENAME = 'extension-state.json';
35
35
  // 请求超时(0.2.8 起 8s):3s 在实测里太紧 —— 对话流快照一大,宿主序列化就要几秒,
36
36
  // 超时会让扩展**丢整份响应**(包括 approvals),表现就是"授权卡片一直不出现"。
37
37
  const REQUEST_TIMEOUT_MS = 8000;
38
+ /** 补全请求的超时:**独立且更短**。宿主侧自己在 4s 放弃,这里给 6s 只是别先于宿主超时 ——
39
+ * 补全的价值在"比你打字快",8s 那个值对它毫无意义,而挂住的请求会挡住下一次补全。 */
40
+ const COMPLETE_TIMEOUT_MS = 6000;
38
41
  /** 轮询间隔:host 侧事件只是"去看一眼这个文件"的提示,600ms 足够且几乎无开销。 */
39
42
  const POLL_INTERVAL_MS = 600;
40
43
  /** 宿主配置重读间隔(端点/令牌轮换后最多这么久恢复)。 */
@@ -292,6 +295,9 @@ function createClient(options) {
292
295
  ok: true,
293
296
  events,
294
297
  askDialog: body !== null && body.askDialog === true,
298
+ // FIM(实验性)状态与用量:宿主每趟都带上来,扩展据此决定"要不要注册补全 provider"
299
+ // 以及状态栏上那串数字怎么显示。没开时是 {enabled:false}(不是 null)。
300
+ fim: body !== null && typeof body.fim === 'object' && body.fim !== null ? body.fim : { enabled: false },
295
301
  };
296
302
  } catch (error) {
297
303
  return { ok: false, error: error.message, status: error.status, code: error.code };
@@ -328,11 +334,40 @@ function createClient(options) {
328
334
  return null;
329
335
  }
330
336
  },
337
+ /**
338
+ * 求一次 FIM(幽灵)补全(实验性)。
339
+ *
340
+ * **不抛**:补全失败是常态(没开、太快、模型抽风),抛异常会让 provider 每次都写错误日志。
341
+ * 返回值统一是 `{ok, text?, usage?, ms?, error?, status?}` —— 调用方只看 `ok`。
342
+ * 超时是独立的短超时(宿主侧 4s 就放弃了,这里给到 6s 只是别先于宿主超时)。
343
+ */
344
+ async complete(payload) {
345
+ try {
346
+ const body = await requestImpl({
347
+ socketPath: (refreshConfig(true) ?? {}).pipe,
348
+ path: `${BRIDGE_BASE}/complete`,
349
+ method: 'POST',
350
+ headers: { [TOKEN_HEADER]: config === null ? '' : config.token, 'content-type': 'application/json' },
351
+ body: JSON.stringify(payload),
352
+ timeoutMs: COMPLETE_TIMEOUT_MS,
353
+ });
354
+ if (body === null || typeof body !== 'object') return { ok: false, error: '空响应' };
355
+ const status = body.status === undefined ? 0 : body.status;
356
+ if (status < 200 || status >= 300) {
357
+ const reason = body.json !== null && typeof body.json.error === 'string' ? body.json.error : `HTTP ${status}`;
358
+ return { ok: false, error: reason, status };
359
+ }
360
+ const json = body.json === null || typeof body.json !== 'object' ? {} : body.json;
361
+ if (json.ok !== true) return { ok: false, error: typeof json.error === 'string' ? json.error : '宿主未返回补全', status };
362
+ return { ok: true, text: typeof json.text === 'string' ? json.text : '', usage: json.usage ?? null, ms: json.ms ?? null };
363
+ } catch (error) {
364
+ return { ok: false, error: error && error.message ? error.message : String(error), status: error && error.status };
365
+ }
366
+ },
331
367
  /** 把游标落盘(实例重启后不重复播报旧事件)。 */
332
368
  persist() {
333
369
  return writeState(extensionsDir, { since, pid: config === null ? null : config.pid });
334
- },
335
- /** 从磁盘恢复游标(实例没换才生效)。 */
370
+ }, /** 从磁盘恢复游标(实例没换才生效)。 */
336
371
  restore() {
337
372
  const saved = readState(extensionsDir);
338
373
  if (config !== null && saved.pid === config.pid) since = saved.since;
@@ -351,6 +386,7 @@ module.exports = {
351
386
  POLL_INTERVAL_MS,
352
387
  CONFIG_REREAD_MS,
353
388
  REQUEST_TIMEOUT_MS,
389
+ COMPLETE_TIMEOUT_MS,
354
390
  BridgeError,
355
391
  isBridgeEndpoint,
356
392
  defaultExtensionsDir,