# 行内代码补全 本页对应 `代码补全系统实现文档.md` 的实现落地与现状。四期已全部实现,实机验证情况见「验证状态」。 ## 怎么用 **改过宿主侧代码之后要重启应用才会生效。** 宿主插件只在进程启动时加载,客户端部分刷新页面即可。 若不重启,接口会返回 `unknown workbench API method` —— 那不是缺陷,是旧进程里还没有这份代码。 插件加载后,在编辑器里输入即可触发;补全以幻影文本呈现。第一次运行时索引在后台构建, 期间补全照常可用(只是不注入检索片段)。 | 操作 | 按键 | 由谁提供 | | --- | --- | --- | | 采纳整条 | `Tab` | Monaco 自带 | | 采纳下一段(按词) | `Ctrl+方向键右` | Monaco 自带 | | 采纳下一行 | `Ctrl+Alt+方向键右` | 本插件(Monaco 没有这一粒度) | | 上一条 / 下一条候选 | `Alt+[` / `Alt+]` | Monaco 自带 | | 关闭建议 | `Esc` | Monaco 自带 | 触发时机由本插件控制(文档 4.1):换行立即触发,其余按语言防抖,连续输入会作废在途请求。 防抖时长按「快语言 / 慢语言」两档,由服务端按实验分组下发(参数默认快档 90 ms、慢档 125 ms, treatment 组下发 70/110 ms),客户端不写死。优先级:**用户显式改过设置页的触发延迟 > 服务端/实验 下发值 > 参数默认值** —— 设置页里的数字等于默认值 90 时视为没改过、不覆盖两档;改成别的数字后两档 统一用用户值。 ## 服务端接口 | 方法 | 用途 | | --- | --- | | `POST /dsh-vscode/completion` | 补全请求,SSE 返回(`meta` / `output` / `error` / `content_security` / `done`) | | `completion.config` | 下发实验分组、该组参数、租户安全规则 | | `completion.telemetry` | 写入埋点(展示 / 采纳 / 取消 / 失败) | | `completion.stats` | 读取采纳率、取消率、首字节延迟、失败次数与最近失败原因 | | `completion.index.status` / `completion.index.refresh` | 检索索引状态与重建 | 审计日志写在 `<工作区>/.dsh-workbench/audit.jsonl`:内容安全命中的两种动作(拦截、脱敏)都落一条, 字段含时间、动作、规则标识与名称、会话、文件、语言。该目录被索引自带的 `.gitignore` 排除。 ## 配置 ### 选路 选路顺序:`DSH_COMPLETION_PROVIDER=local` → 本地兜底;配了 `DSH_COMPLETION_ENDPOINT` → 该端点; 其余情况用 DSH 的模型服务(默认跟随应用的默认模型,可用下面的 `route` 单独指定)。 **模型不可用时不会自动回落**:故障应当显形,而不是被掩盖成「补全质量忽然变差」。要回落就显式开启。 ### 补全单独的模型路由 补全可以与对话用**不同**的模型(例如对话用付费模型、补全用免费模型): ``` DSH_COMPLETION_ROUTE=provider/model ``` 不设则沿用应用的默认模型。写错的值(缺斜杠、任一侧为空)当作没设,回落到默认模型。 ### 模型失败时回落本地兜底 ``` DSH_COMPLETION_FALLBACK=local ``` 开启后,模型调用抛错时改用本地兜底出候选,**同时把完整原因写进宿主日志**(回落不等于藏故障)。 两条边界:上游正常返回空候选不回落(那是有效结果);已经产出增量后再失败也不回落(半条候选接上 另一家的文本只会拼出垃圾)。 ### 环境变量一览 | 变量 | 作用 | | --- | --- | | `DSH_COMPLETION_PROVIDER=local` | 强制走本地兜底提供方(离线或单独验证客户端链路时用) | | `DSH_COMPLETION_ENDPOINT` | OpenAI 兼容的补全端点基址,例如 `http://127.0.0.1:8080/v1` | | `DSH_COMPLETION_MODEL` | 上面那个端点使用的模型标识,默认 `completion` | | `DSH_COMPLETION_API_KEY` | 上面那个端点的密钥,作为 `Authorization: Bearer` 发送 | | `DSH_COMPLETION_ROUTE` | 补全单独的模型路由,`provider/model` 形式 | | `DSH_COMPLETION_FALLBACK=local` | 模型失败时回落本地兜底 | ### 设置文件 桌面端应用不方便设环境变量,因此还支持一个设置文件(**不动配置档**): ``` %DSH_HOME%/completion.settings.json { "completion": { "route": "provider/model", "fallback": true } } ``` **不需要手改。** DSH 设置里的「AIDE」一页就写这个文件:模型路由是一个输入框(留空=跟随 应用默认模型),宿主会把 **DSH 已配置的模型目录**作为候选列出来(`GET /dsh-vscode/model-routes`, 数据来自 `llm.listProviders()/listModels()` 与 `agentDefaultModel.currentSelection()`,任一提供方失败只丢它自己); 兜底是一个开关,保存后立刻生效(宿主写完就地重算,不等两秒的重读周期)。设置页还会 显示「当前生效的模型」与「最近一次补全失败原因」,排查「没有补全」时先看这两行。 触发延迟也在这里改(客户侧偏好,默认 90ms;只有你改过它时才生效 —— 改过之后两档语言统一成该值, 没改过则用服务端按实验分组下发的快/慢档),不必动设置文件。 优先级:**环境变量 > 设置文件 > 插件配置 > 默认值**。文件不存在、不是 JSON、字段不合法,一律当作 没设 —— 不会因为一个写坏的文件让补全整体失效。路由格式不合法(没有 `/`)会在保存时被拒绝,不会写进文件。 平铺的旧文件(`{ "route": ... }`)会在下一次保存时自动收进 `completion` 下。 > 关于插件配置:当前 DSH 版本里,bundle patch 的 `config` 送不到插件的 `apply`(同一机制下,其它 > 插件的配置同样送不到,已实测)。所以设置要走上面两条入口,别写进配置档的 `config`。 ## 验收脚本 对着一个正在运行的 DSH 实例逐项取证据(只读,不写工作区、不改配置): ```sh node scripts/completion-acceptance.mjs http://127.0.0.1:3999 ``` 它会检查:配置下发(选路、模型、实验分组、参数、安全规则)、索引规模与上限、一次真实补全请求 (结构化请求进、meta 与候选出、首字节延迟)、统计与失败留痕。本地兜底下 0 项未通过; 模型不可用时补全一项会打印失败原因(含路由与机器码),脚本仍以 0 退出 —— 「模型不可用」是环境事实, 不是验收失败。 ## 文档对照表 每一行是文档的一处要求、它落在哪个文件、以及凭什么说它成立。证据四类:单测(`pnpm test`)、 验收脚本(`scripts/completion-acceptance.mjs`)、浏览器实测(探针脚本,见仓库外的测试目录)、实测数值。 | 文档 | 要求 | 实现位置 | 证据 | | --- | --- | --- | --- | | 4.1 | 触发与防抖、终止符与撤销补全 | `client/inlineCompletion.ts` | 单测(触发计划、防抖);浏览器实测 | | 4.1 | 请求可被频繁取消 | `completion/service.ts` | 单测;统计里的取消率 | | 4.2 | 上下文采集(当前文件、光标、编辑历史) | `completion/params.ts`、`client/inlineCompletion.ts` | 单测(截断、偏移映射) | | 4.3 | 结构化请求,服务端不接收拼装好的 prompt | `completion/protocol.ts`、`prompt.ts` | 单测;验收脚本的「配置」与「补全」两项 | | 4.4 | SSE 流式返回与增量渲染 | `completion/protocol.ts`、`client/completion.ts` | 单测(编码、分块解析);浏览器实测 | | 4.5 | 六条丢弃判定 + 五种开关 | `completion/filter.ts` | 单测(逐条 + 真实输出形态);接线集成用例(`lastEdit` → `rollback-edit`) | | 4.5 | 后处理五件事 | `completion/filter.ts` | 单测(含顺序:闭合括号先于去重复尾符号) | | 4.5 | 请求前预过滤、租户规则 | `completion/security.ts`、`client/inlineCompletion.ts` | 单测;浏览器实测;预过滤耗时实测 0.58 ms / 50 万字符 | | 4.6 | 幻影文本、三级采纳、采纳记账、候选切换 | `client/inlineCompletion.ts` | 浏览器实测(含切换不重新请求);采纳率实测 0.5 | | 5.1 | 上下文服务进程与接口 | `completion/register.ts` | 验收脚本 | | 5.2 | 结构解析、实体去重、单文件上限 | `completion/chunks.ts`、`retrieval.ts` | 单测(分块、跳过大文件、内容去重) | | 5.3 | 检索链路、嵌入、向量存储、召回 | `completion/retrieval.ts` | 单测;实例测算:冷启动 4.4 s、增量 1.25 s | | 5.3 | 索引维护(全量、周期、增量、上限) | `completion/workspaceIndex.ts` | 单测(刷新复用同一在途任务);上限可见性实测 | | 5.4 | TopK、打分、片段数量与长度、预算分配 | `completion/retrieval.ts`、`prompt.ts` | 单测(预算丢弃顺序、符号额度) | | 6.1 | 接口契约 | `completion/register.ts` | 验收脚本 | | 6.2 | FIM 标记与全角变体、截断参数、后缀起始模式 | `completion/prompt.ts` | 单测(各排布、截断、块边界) | | 6.3 | 模型可选、不固化在客户端 | `completion/register.ts`、`dshProvider.ts` | 验收脚本(选路与模型来自配置) | | 7 | 参数由服务端下发、按实验分组 | `completion/params.ts`、`experiment.ts` | 验收脚本(分组与参数一致) | | 7 | 指标:采纳率、取消率、首字节延迟、失败留痕 | `completion/telemetry.ts` | 单测;验收脚本;实例实测 | | 9 | 边界与开放问题 | 本页「已知局限」 | 逐条写明取舍与代价 | ## 验证状态 **已在真实宿主上验证**(本地兜底提供方,浏览器端到端): 一次完整扫尾(同日): | 检查 | 结果 | | --- | --- | | `scripts/completion-acceptance.mjs` | 0 项未通过(配置、索引、补全、统计四项全绿;补全一项含「光标在文档末尾」「光标在文件中段」「1200 行文档中段」三个用例) | | 安全拦截(接口层) | 命中拦截规则时下发 `content_security`(`action: block`)且无 `output` | | 幻影呈现与采纳闭环(浏览器) | 幻影出现、真实按键采纳后模型与候选一致、采纳被记账 | | 候选切换(浏览器,三向) | 在两条候选间往返,且不重新发起请求 | | 冷启动路径 | 索引尚未建好时补全照常可用(只是不注入片段);建好后 2910 个文件 / 12000 个分块,注入 4 条片段 | | 单独指定补全模型 | `DSH_COMPLETION_ROUTE` 生效:配置接口报出指定的模型,请求也带该模型 | | 模型失败回落本地兜底 | 上游不可用时仍产出候选,宿主日志留完整原因;另有两处边界(空结果不回落、已产出不回落)由单测钉住 | | 设置文件 | 只写 `%DSH_HOME%/completion.settings.json`、不设任何环境变量,重启后补全即按文件里的路由走 | - 幻影文本呈现、整块采纳(真实按键)、候选切换(在两条候选间往返,且切换不重新发起请求) - 结果过滤与后处理、三级采纳粒度、采纳记账、就近取消 - 检索:结构分块、本地嵌入、按项目索引、片段注入(`meta.snippets` 可观测) - 内容安全:跨数据块的模式能被抓到(逐块匹配会漏),拦截中止本次补全、脱敏保留内容并标记, 两种动作都进审计日志 - 实验分组、采纳率 / 取消率 / 首字节延迟统计、失败留痕 - 采纳记账由内容变更识别:Monaco 不执行条目上的 `command`(命令注册本身正常),采纳会在光标处插入 文本并产生内容变更事件(实测 `{"text":"te","rangeLength":0}`);插入长度 ≥ 2 且是最近一次候选的 前缀时按采纳记账,单字符不记。实测采纳率 0.5。 - 状态量:195 项单测通过(另有 1 项为既有的 Windows symlink 权限用例,与本功能无关) **未验证**: - **DSH 模型提供方的实机调用**。这是环境边界,不是代码缺陷,依据如下:端点本身可达(不带凭据请求 `https://api.deepseek.com/anthropic/v1/messages`,154 ms 返回 401);两条路由都会失败,且都在约 150 ms 内瞬时返回,属于凭据层面的立即拒绝;桌面端配置用的是 `deepseek-account`(账号会话由应用进程持有), 它那条传输可用。因此独立实例里只有本地兜底那条路能端到端跑通,实测请以桌面端为准。 失败信息会带出机器码与状态码(例如 `… transport failed · TRANSPORT · status 401`),便于区分鉴权、 路由与网络。 - **真实模型的补全质量**。本地兜底只做标识符补全,质量结论必须来自真实模型。 - **OpenAI 兼容通道的响应解析**。请求能到达上游(本地桩实测),但 DSH 宿主进程内的响应体没有解析出 文本;同一个提供方在普通 Node 进程里对同一个桩解析正常,所以问题在宿主这一侧,暂未定位。 这条通道不在生产路径上(生产用 DSH 模型服务)。 失败一旦发生会留下痕迹:`completion.stats` 的 `failed` 与 `lastFailure` 会写明路由与原因 (例如 `completion upstream failed (deepseek-official/deepseek-flash): …`)。 ## 模型调用失败怎么排查 先看错误里的机器码,**不要只看消息文本**。DSH 的 DeepSeek 适配器把内部任何错误(含取凭据失败) 统一包成 `DeepSeek Messages transport failed · TRANSPORT`,所以「TRANSPORT」既可能是网络不通, 也可能是凭据没取到 —— 它本身不具诊断力。 按这三步缩小范围: 1. **端点是否可达**:直接请求提供方实际使用的地址。DeepSeek 路由用的是 `https://api.deepseek.com/anthropic/v1/messages`(不是 `/v1`)。返回 401 说明网络与 TLS 都没问题, 问题在凭据;返回连接错误才是网络层。 2. **凭据能否取到**:密钥来自 DSH 的凭据存储(`%DSH_HOME%/.credentials.yaml`)或环境变量。 前者有、后者没有时,要看该提供方声明的 `apiKeyEnv` 与实际存放的名字是否一致。 3. **是否需要转发**:本机若对某些域名直连会被拒 TLS(例如 anyrouter.top),而 DSH 跑在 Node 上、 **不遵循 Windows 系统代理**,就必须经本机转发器(配置档里 `llm-pi-ai` 的对应提供方指向它)。 这类失败同样是 TRANSPORT。 模型调不通时,若已开启回落,编辑器里仍会有候选,宿主日志同时写明是哪条路由、什么原因。 ## 已知局限(文档第九节) 1. 补全质量主要取决于模型与 prompt,不取决于客户端代码。客户端做的是把上下文准备好并按契约收发。 2. prompt 设计没有通用最优解:序列结构、字段顺序、截断策略都需要按目标模型实测。 3. 检索质量取决于嵌入模型与分块策略。本实现用哈希嵌入(256 维)做兜底,相似度阈值因此按实测调到 0.15,而不是文档建议的 0.7(后者对应真实嵌入模型)。 4. 多模型融合是可选路径,本实现未做。 5. 长上下文不等于高相关:预算分配按优先级丢弃实现,符号上下文上限为总预算的 10%。 6. 内容安全与隐私需要显式设计。当前实现不上传代码到第三方,检索索引只落在工作区本地目录; 请求前预过滤会按拦截规则阻止内容离开本机。 7. 截断完整代码块只认花括号:圆括号不计。若把圆括号也算作块,形如 `items.map(item => (…))` 的合法 多行补全会被从中间截断。代价是模型若在补全之后又附一整个无关函数,那部分会保留下来。 8. 纯解释(例如「无法补全。」)不会被过滤掉:丢弃规则只看候选里有没有语言字符,而中文也是语言字符, 按「含中文就丢」处理会误伤字符串字面量与注释。模型表达「补不出」的正当出口是输出空行, 空候选会被丢弃;这一点写在指令里。呈现层的杂质(围栏、行内反引号、加粗、复述回来的上下文头) 会自动剥掉。 7. 检索索引默认落在 `<工作区>/.dsh-workbench/index.json`(自带 `.gitignore`)。若不想在工作区留文件, 需要改到宿主存储域 —— 这一点尚未决定。 8. `output` 事件的候选序号由提供方给出:逐字流式的模型把同一条候选拆成多块,这些块共用同一个序号。 9. 文档 4.5 的「八条丢弃判定」现在只实现六条:`deletion-after-growth`(判定 3)需要「光标所在行的 上一版本」,客户端没有可靠来源、插进请求还要先扩协议,而插入式补全也表达不了「删除」;`repeats- cancelled-suggestion`(判定 4)的状态生产者 `recordCancel` 已不存在。两条连同对应状态字段一起删掉, 没有留下永远不触发的死判定。剩下的六条里 `multi-line-deletion` 目前被更靠前的 `empty-change` 完全遮蔽(纯空白候选先被它拦下),实际不触发,待后续统一处理。 ## 与文档建议的几处偏离 以下都是有意选择,不是遗漏;列出来是为了让人能按同一份文档核对: | 文档建议 | 本实现 | 原因 | | --- | --- | --- | | 增量解析器(tree-sitter 一类)抽取模块/类/函数三级实体 | 按大括号与缩进抽窗口分块,正则是实体边界 | 零依赖是硬约束(不允许为此新增依赖);实体名与签名靠分块正文自带 | | 本地嵌入模型 + SQLite 向量扩展 | 哈希嵌入(256 维)+ JSON 文件 | 同上;索引体积与延迟已在性能分析目录实测 | | 每项目分库、周期重建以小时或天计 | 每工作区一份索引,周期 24 小时 | 与文档一致,仅存储形态不同 | | 服务端维护模型清单,按语言/分组/等级下发 | 配置接口下发单个模型与分组参数 | 清单是能力不是必需;当前只有一条选路 | | 进程间 IPC + JSON-RPC | HTTP(API 路由 + SSE) | 分层与边界一致:编辑器在浏览器渲染进程,组装与检索在宿主进程 | 分块去重(文档 5.2 的实体去重)已实现:同一段内容只入库一次,因此同样的分块预算下能容纳更多不同文件 (实测入库文件数由 2587 升到 2776)。