🎁 福利专区全网大模型免费应用 + 新用户福利 + 注册活动入口,低成本玩转 AI

输入预测幽灵文本 dsh-plugin-collection/dsh-suggest-ghost

DeepSeek Harnessv0.8.0
dsh-plugin-collection1 天前有提交已提供中文文档扫描:含敏感能力

DSH Web 输入预测插件:回合结束后 LLM 生成下一条建议,输入框空草稿时渲染幽灵文本,Tab 采纳。

36.4/100
综合分
5
GitHub 星标
590
npm 周下载
—/5
用户评分

数据截至 2026/11/3 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。

安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add dsh-suggest-ghost
npm 源 · dsh-suggest-ghost 的发布者账号已校验与本仓库一致

状态速览

判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态
信任档位:已验证
是什么
dsh 原生插件 · other
装得上吗
本站已在 dsh 0.2.1-rc.5 下真实安装成功
非静态推断
L4 · 真实安装等级说明 →
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 1 天前
档位由哪些信号合成

本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。

验证判据与等级说明 →
社区实装还没有人打卡 · 装过的第一个就是你
安装兼容性检查✓ 自动检查通过
展开查看检查明细 ↓

以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。

✓npm 包dsh-suggest-ghost @ 0.8.0
✓Node 引擎要求 ^22.19 || >=24 · 基线 Node 22.19 满足
✓dsh CLI 依赖未声明 dsh 版本约束
✓入口文件main/exports/bin 已声明

验证方式:npm registry 存在性 + package.json 静态校验 · 最后检查 2026/10/30 21:11:44

安全扫描自动 · 静态分析源码
v2.7.0
🔵 扫描:含敏感能力尚无人工评估
展开查看扫描说明与证据 ↓

静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。

读写/删除本地文件发起外部网络请求
查看扫描证据(共 4 处,展示前 4 条)
  • 读取本地文件
    lib/index.js:335
    return statSync(fileURLToPath(import.meta.url)).mtime;
  • 发起网络请求
    lib/client.js:1661
    const res = await fetch(CATALOG_PATH, { method: "GET" });
  • 发起网络请求
    lib/client.js:1690
    const res = await fetch(`${CATALOG_PATH}?models=${encodeURIComponent(provider)}`, { method: "GET" });
  • 发起网络请求
    lib/client.js:1727
    const res = await fetch(`${CATALOG_PATH}?efforts=${encodeURIComponent(key)}`, { method: "GET" });
扫描于 2026/11/2(1 天前) · 范围 入口闭包 + 全包扫描 · 来源 npm 发布包

静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。

依赖的 DSH / Cordis 模块
@deepseek-ai/cordis@deepseek-ai/cosmokit@deepseek-ai/dsh-client-ui-primitives@deepseek-ai/dsh-llm@deepseek-ai/dsh-session@deepseek-ai/dsh-session-projection@deepseek-ai/dsh-settings@deepseek-ai/dsh-storage@deepseek-ai/dsh-storage-domain@deepseek-ai/dsh-storage-json@deepseek-ai/dsh-timeout@deepseek-ai/schemastery
给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个

界面预览

4 张 · 点击查看大图

功能亮点

  • 回合结束生成下条建议
  • 空草稿渲染幽灵文本
  • Tab一键采纳补全
v0.8.02026-10-30
DSH 输入框的输入预测:回合结束后自动建议你的下一句话(Tab 一键采纳),输入时按会话历史做前缀补全。 · 安装 dsh plugin --profile web add dsh-suggest-ghost · 本次更新 · 流式投递:幽灵文字改为逐帧增长,新增设置项 streamingDelivery(默认关闭) · 设置卡的 Provider / Model 改成下拉框:选项直接来自宿主目录,不再手打 · 辅助调用不再跟着主会话的模型路由漂:这才是补全「完全不触发」的真因 · 修回「插件在三个代次上整个不装载」:上一版流式改动引入的回归 · 逐帧痕迹可从 DOM 读到:overlay 上留 data-stream-frames,回合结束后仍能核对 · 说明 · License: MIT · 仓库:
v0.7.02026-10-27
DSH 输入框的输入预测:回合结束后自动建议你的下一句话(Tab 一键采纳),输入时按会话历史做前缀补全。 · 安装 dsh plugin --profile web add dsh-suggest-ghost · 本次更新 · 建议一次性呈现:幽灵文本改为等「终态帧」再画,不再先出半截再改 · 中文会话不再蹦出英文建议:语言判据从 UI 先验改为按会话实际用字相对比较 · 热重载后不再「幽灵永不出」:root 监听器随旧 fiber 回收,重载前就开着的会话也继续出建议 · HMR 下补写泵不再被毒化:配置事务里重跑 apply() 导致 pull 应答 / 续写 / 清空全丢,已修 · 修掉一次真实事故:HMR 事务内写失败退化成微任务紧循环,曾让 tee 日志涨到 23 GB、事件循环卡死 · 连打期续写不再空转:单飞抢占改成前缀豁免,completion generated 不再整段为 0 · 设置卡两处修复:两个「最少输入字符」不再同名;保存不再静默失败(「= 3」真的写进 profile) · 终端不再刷屏:常态留痕默认静默(健康进程 9294 行 → 0 行),只有告警/错误到达用户侧 · 重启后残留的建议恢复回来:别处会话的 pull / 停顿续写 / 回合结束不再盖掉盘上未判定的残留 · 辅助调用的推理档位可配;--cleanup 现在也清得掉沙箱目录之外的残留 · 说明 · License: MIT · 仓库:
v0.6.02026-10-20
DSH 输入框的输入预测:回合结束后自动建议你的下一句话(Tab 一键采纳),输入时按会话历史做前缀补全。 · 安装 dsh plugin --profile web add dsh-suggest-ghost · 本次更新 · 采纳建议不再吞字:Tab 整条采纳与 → 逐词采纳,都不再吃掉你刚打的字 · 「幽灵永不出」的主要成因已排除:停顿续写的投递被内核事务拒绝、推送到客户端丢失,两条都修了 · 同一个草稿再停顿也出续写:不再只有第一次能出 · 一次输入不再刷出上千次 armed:停顿计时器不再被自己的事件反复推倒重来 · 设置页改开关立刻生效:运行中改配置不再需要重启 · 「边打边等」不再被判失效:判定与显示改为共用同一次前缀对齐结果 · 停顿默认缩短:inlineDelayMs 800 → 100 毫秒,建议来得更快 · 故障不再被自家节流吞掉:续写的五个静默出口现在都留痕 · 诊断与开发体验:新增 scripts/dev-sandbox.mjs(一条命令拉起隔离的第二实例)、check:loop 体检、产物陈旧与 gitignore 守卫 · 说明 · License: MIT · 仓库:
数据来自仓库的 GitHub Release,可能与插件实际版本不一致。

README

由 DeepSeek 最新模型翻译生成
dsh-suggest-ghost DeepSeek Harness Web 输入预测插件:回合结束后调一次辅助 LLM,预测你的下一条提示词,草稿为空时以幽灵文本显示在输入框里;输入过程中则从当前会话历史找前缀补全。Tab 整条采纳,→ 逐词采纳。 这是什么 纯插件挂载(host 监听回合事件 + client 端 DOM overlay),不改 DSH 任何核心代码。三个模式自动切换: - LLM 下一条建议(草稿为空):每个回合完成后,把最近几轮对话脱敏后发给建议模型(默认继承主请求路由,不额外配置),产出一条「你大概率会输入的下一句」——Claude Code 同款体验。长会话开启摘要后,早期目标与偏好也会保留,建议质量不随会话长度下降。 - 历史前缀补全(草稿非空):zsh autosuggestions 式——从会话历史找前缀命中项,按新近度为主、会话内频次与跨会话热度为辅打分取最优,灰色显示剩余部分。全/半角标点、空白、大小写差异不影响匹配。即时、无网络。 - 停顿续写(草稿非空):输入停 inlineDelayMs(默认 100ms)后发一次续写请求,把光标处的下半句以幽灵补出,Tab 整条采纳、→ 逐词采纳。
展开完整文档(README 剩余 1 节)
dsh-suggest-ghost

DeepSeek Harness Web 输入预测插件:回合结束后调一次辅助 LLM,预测你的下一条提示词,草稿为空时以幽灵文本显示在输入框里;输入过程中则从当前会话历史找前缀补全。Tab 整条采纳,→ 逐词采纳。

这是什么

纯插件挂载(host 监听回合事件 + client 端 DOM overlay),不改 DSH 任何核心代码。三个模式自动切换:

- LLM 下一条建议(草稿为空):每个回合完成后,把最近几轮对话脱敏后发给建议模型(默认继承主请求路由,不额外配置),产出一条「你大概率会输入的下一句」——Claude Code 同款体验。长会话开启摘要后,早期目标与偏好也会保留,建议质量不随会话长度下降。
- 历史前缀补全(草稿非空):zsh autosuggestions 式——从会话历史找前缀命中项,按新近度为主、会话内频次与跨会话热度为辅打分取最优,灰色显示剩余部分。全/半角标点、空白、大小写差异不影响匹配。即时、无网络。
- 停顿续写(草稿非空):输入停 inlineDelayMs(默认 100ms)后发一次续写请求,把光标处的下半句以幽灵补出,Tab 整条采纳、→ 逐词采纳。耗时与体感预期:inlineDelayMs 只决定「何时发请求」,模型返回本身实测 1.1~17 秒(2026-10-20 真实环境 42 次生成:最快 1100ms、中位 4900ms、最慢 16986ms),所以停顿后马上等通常看不到。期间再敲字会中止在途请求并重新开始 —— 但已经到达的那份续写仍会按对齐判据显示,不再被丢弃,这类中止在宿主日志里逐次可见(completion-preempted / completion-aborted / completion-timeout)。

幽灵文本是 DOM overlay 近似渲染(跟随输入框字体、跟随滚动),不依赖官方 setGhost 输入机能力,rc.6 即可运行;中文输入法组合期间不拦截按键。

效果预览

实操演示:逐字输入,幽灵浮现,Tab 一键采纳

① 历史前缀补全(草稿非空)——输入「列出 do」,幽灵自动补全这条历史消息的剩余部分:

历史前缀补全实拍

② LLM 下一条建议(草稿为空,回合结束自动预测下一步):

LLM 下一条建议实拍

深色文字 = 已输入;灰色 = 幽灵建议。Tab 整条采纳,→ 逐词采纳。幽灵文本跟随输入框字体与滚动渲染,不遮挡、不抢焦点;不合意就无视它,继续打字即消失——零成本。

安装

npm 安装(推荐,预构建产物,无需授权):

dsh plugin --profile web add dsh-suggest-ghost

从 GitHub 一行安装(构建产物已随仓库提交,无需本地构建):

dsh plugin --profile web add "github:WuJiaoJue/dsh-suggest-ghost"

或者源码方式:

git clone https://github.com/dsh-plugin-collection/dsh-suggest-ghost.git
cd dsh-suggest-ghost && pnpm install && pnpm run build
dsh plugin --profile web add .

装完重启 dsh web,页面 Ctrl+Shift+R 硬刷新。开箱即用,无需任何配置。

配置

全部设置在 Settings → Plugins 面板的 Suggest ghost 卡片,保存后即时生效:卡片文案(字段 label/hint、按钮、徽章)跟随宿主 DSH 界面语言(中文 / English),设置页切换语言后实时跟随,无需刷新;旧宿主无 locale 服务时回退中文。

| 分组 | 字段 |
| LLM 下一条建议 | 启用开关、输出令牌上限、建议字符上限、参考回合数、转录字符预算、超时(毫秒)、采纳快捷键、provider / model 路由(留空继承主请求)、建议推理档位 suggestionReasoningEffort(默认为空 = 不传) |
| 停顿续写 | 启用开关、停顿时长(默认 100ms)、最少输入字符(停顿续写)、续写长度上限、续写推理档位 completionReasoningEffort(默认为空 = 不传) |
| 历史前缀补全 | 启用开关、跨会话搜索、最大历史条目、最少输入字符(历史补全)、逐词采纳 |
| 热度管理 | 跨会话高频短语列表:固定(不参与淘汰、候选恒置顶)、删除、手工新增(默认固定)、清空全部——即时生效,无需保存 |

推理档位为什么默认留空:reasoningEffort 是按 provider、按 model 声明的,不是全局开关。
本机 profile 的 18 个模型里有 9 个根本没声明 reasoningEfforts(efficient / fast / lite 等),给它们带任何档位都会在请求发出之前被适配器拒掉(UNSUPPORTED_REASONING_EFFORT,实测 0-20ms 失败)。所以默认空串 = 插件不写这个键 = 沿用该路由自己的默认档位。
填了模型没声明的档位也不会让这一轮没有建议:宿主会退一档(不带档位)重发一次,宿主日志里记 effort_rejected=1。也别拿它当「关思考」用——只有显式声明了 off / none 的路由才接受这类值(qfmodel 连 off 都拒,profile 只声明 low/medium/xhigh)。

档位能省多少时间取决于模型:同一沙箱内测得 qfmodel 缺省 1468ms、带 low 828ms (约 1.8x),而 efficient 缺省就要 6238ms。换模型比调档位是更大的杠杆(约 4x)。
辅助调用的首块延迟、增量数与请求档位现在都写在留痕行里(suggestion generated … / completion generated …,含 first_text_delta_ms / first_reasoning_delta_ms / outcome=empty|filtered)。

也可以在 cordis.patch.yml 按 id 覆盖(作为上述项的初始值):

- id: suggest-ghost
config:
maxInputBytes: 4096 # 框架化用户提示字节上限 maxOutputTokens: 512 # 建议输出令牌上限 timeoutMs: 60000 # 辅助请求截止时间(毫秒) maxRecentTurns: 3 # 送入建议模型的最近完成回合数(默认 3) maxTranscriptChars: 12000 # 转录字符预算 contextSummaryEnabled: true # 长会话摘要开关(旧消息被裁剪时提取摘要) contextSummaryChars: 1500 # 摘要字符预算 maxSuggestionChars: 240 # 建议可见字符上限 acceptKey: Tab # 采纳快捷键 llmEnabled: true # LLM 建议开关 suggestionReasoningEffort: '' # 建议调用的推理档位(默认为空 = 不传 = 沿用路由默认) completionReasoningEffort: '' # 续写调用的推理档位(同上)

安全

- 默认取最近 3 轮对话发给建议模型,开启长会话摘要后旧消息的关键信息也会保留;发送前自动脱敏常见凭据;输出经净化,不合格回复静默丢弃
- 全程有界:输入字节 / 输出 token / 超时封顶;同回合防重入,新回合作废旧生成,卸载即中止在途请求

兼容性

现状(2026-10-30):支持 6 个代次 —— 0.1.2-rc.1 / 0.1.5-rc.3 / 0.1.7-rc.1 / 0.1.7-rc.2 / 0.2.0-rc.2 / 0.2.1-rc.2;0.2.2-alpha.2 是预发布线,已登记进代次表并跑过装配 / 门禁 / 类型 / 投影四面(npm run verify:kernels 与 scripts/cross-kernel/type-surface.mjs 全 GREEN),但它不是对 0.2.2 正式版的承诺。0.1.1-rc.2 已从支持列表删除:它的前端没有 remote.settings 这个服务,而 cordis 对 inject 是静态 park(声明了拿不到就整条 fiber 不激活),运行时降级做不到 —— 硬约束证据与对照实验在 scripts/cross-kernel/FINDINGS.md 顶部与 §20。
下文任何提到 0.1.1-rc.2 的段落都是历史记录。

| DSH 内核 | 准入门禁 | 装配 + 投影注册 | 类型面 | UI 面(真 GUI 有截图) |
| 0.1.2-rc.1 | ⚪ 该代不导出 evaluator ⇒ 未判定 | ✅ | ✅ | ✅ FINDINGS §13 |
| 0.1.5-rc.3 | ⚪ 同上 ⇒ 未判定 | ✅ | ✅ | ✅ FINDINGS §13 |
| 0.1.7-rc.1 | ✅ 仅 peer | ✅ | ✅ | ✅ FINDINGS §13 |
| 0.1.7-rc.2 | ✅ 仅 peer | ✅ | ✅ | ✅ FINDINGS §8 / §12 |
| 0.2.0-rc.2 | ✅ 仅 peer | ✅ | ✅ | ✅ FINDINGS §8 |
| 0.2.1-rc.2 | ✅ engines.dsh 单独决定 | ✅ | ✅ | ✅ FINDINGS §8 / §12 |
| 0.2.2-alpha.2 | ✅ engines.dsh 单独决定 | ✅ | ✅ | ⏳ 未判定(FINDINGS §20.6,别当绿) |

三条读数纪律:未判定 ≠ 绿(前两代根本没有闸,"装配 GREEN"不证明准入验证过); peerDependencies 的逐代枚举 ≠ 内核的匹配口径(见下一节两套口径); UI 面是四面里唯一能回答"用户到底看见没看见"的那一面,也是最容易被前三面全绿掩盖的一支。

peer 枚举,以及 0.2.0 才开始有的硬门禁

peer 依赖写成 ^0.1.2-rc.1 || ^0.1.5-rc.1 || ^0.1.7-rc.2 || ^0.2.0-rc.2 这种逐代枚举
(今天 package.json 里那 6 个 @deepseek-ai/dsh- peer,每一条都是这一串),而不是 >=0.1.2-rc.1。
原因分两套口径,别把它们当同一套:

- 安装面(npm/pnpm 解 peer,不带 includePrerelease):node-semver 默认不把预发布版算进任何范围,只有比较符带同一 [major.minor.patch] 元组时才放行 ⇒ ^0.1.2-rc.1 对 0.1.5-rc.1 为假、 同样为假, 跨代没有区间写法,每发布一根新 rc 就得补一个枚举项。
- 准入口径(内核自己判,semver.satisfies(v, range, { includePrerelease: true })):实测 ^0.1.2-rc.1 覆盖 0.1.x 全部(含 0.1.7-rc.1),^0.2.0-rc.2 覆盖 >=0.2.0-rc.2 <0.3.0-0 的全部(含 0.2.1-rc.5 与 0.2.2-alpha.2);上面那四条在 npm 口径下全是 false。

⇒ 「补枚举项」是安装面的事,不是准入要求。本仓因此没有为一根 alpha 放宽 peer (那等于把未验证的代次写进支持声明);0.2.2 正式版出来时两种口径都自动覆盖,届时只需在代次表新增一条并重跑(2026-10-30 实测,FINDINGS §20.4)。

0.2.0 起这条从「安装期提示」升级为「加载期硬拒绝」:内核在组装 profile 时逐条用
semver.satisfies(runtimeVersion, range, { includePrerelease: true }) 校验 peer
(dsh-app-boot 的插件兼容性检查),不满足就整个 bundle 跳过:

dsh: skipping profile bundle "dsh-suggest-ghost": Error: Plugin … is incompatible with dsh 0.2.0-rc.2: peerDependencies {…}

两点值得记住:

- 门禁带 includePrerelease: true,但它不是万能放行:^0.2.0(不带 rc)对 0.2.0-rc.2 仍然判假(实测两种口径都是 false),要放行这一代就得把下界写成带 rc 的 ^0.2.0-rc.2;
- 需要临时放行时可以用 dsh plugin allow-version 给精确版本豁免,但那是绕过检查,不是修复 ——准入面的正解是代次表登记 + 重跑(scripts/cross-kernel/generations.mjs),安装面的正解才是补枚举项。两者别混,见上面那两套口径。

0.1.7 的 settings 体系变更(源码里唯一的大改)

0.1.7 移除了整个 settings.yaml 体系,本插件的数据通道随之搬家:

| | 0.1.5 | 0.1.7+ |
| 用户可调项 | ctx.settings.register(ns, schema, …) 注册命名空间 | Config 里逐字段 .volatile(),内核把插件自己的 Config schema 投影成表单,ns = Loader 条目 id |
| host → client | SettingsScope 的 _push 字段 | ctx.settings.update(ns, {_push, _hot})(两格都标 volatile;_push = 建议格,_hot = 热度/历史格) |
| client 读取 | settingsScope.bind(ns).get() | remote.settings.describe()(首读 + 事件驱动重读)|
| client → host | scope.set('_ops', …) | remote.settings.mutate(ns, [{op:'set',path:['_ops'],…}]) |
| 变更通知(host) | scope.watch | settings/document-updated(ns, revision) 事件 |
| 变更通知(client) | scope.subscribe | remote.$on('settings/document-updated') |

载荷结构的归属守卫(suggestionSessionId / historySessionId / completionSessionId)逐字未变,变的只是载体。0.6.1 起 host → client 分两格:_push 只装
{suggestion(+frame/final), suggestionSessionId, completion(+final), error},_hot 只装
{hot, total, history, historySessionId}——两格各自判重,一次纯热度变化不再连带重写建议格。
两格都脏时合并成一次 settings.update(省的是客户端一次重解析,不是「一次整档重写」:补写泵不排队,各写各的最坏只多等一个 500ms tick)。缺省语义按格成立:整格缺席 = 保留现值, 不是清空——旧宿主没有 _hot 时客户端的热度与历史必须原样留着。

热度格写合并窗口(1000ms):只合并纯热度的变化(连续输入、一批多 op);建议格永远立即写,不被窗口拖慢;pull 与 complete(客户端正在合成时发的那条)都会让路 —— 窗口未到期时立刻把当前热度放出去,因为那正是「有人马上要用热表」的时刻。热表恢复后的那次补推与插件卸载前的那一次同样绕开窗口。用户可见代价:热表排序可能滞后 ≤1.5s,不影响建议本身。实测(真实会话日志 + 纯函数判定,120 会话 / 8134 次判定):「这次没补出来」的无条件比例 ≈ 0.033%(3 例);而「落后一整个回合」的代价是 12.6% ——窗口的可见代价取决于「用户输入节奏 ÷ 陈旧上界」这个比值(本机实测相邻输入间隔
p50 = 341 秒、≤1.5s 的仅 0.29%),不是窗口本身多大。复算:
node --experimental-strip-types scripts/hot-window-staleness.mjs 与
scripts/hot-window-timeline.mjs(不接 test:smoke 链,一次性量度)。
程序化连发(相邻输入 ≤1.5s)下这 12.6% 会变成真实代价 ⇒ 见 issue #63。

宿主升级后请刷新页面(不需要重启宿主)。客户端 bundle 由同一个宿主提供(lib/client.js),所以版本偏斜只影响升级前就已打开的旧标签页,而那里的现象两个格子不一样(都按旧代码实读确认):热度列表会被清成空(旧代码对缺省的 hot 判成 null,见 1c205d6 的 src/client/index.ts:1840-1842),历史环则只是停更(旧代码对 history 是「省略 = 保留现值」)。建议格不受影响,它还在 _push 里。
两者都不等于丢数据:热表的权威存储是 suggest_ghost_hotness 域(src/hotness-store.ts:44-45), _hot 只是给客户端的快照,刷新后即恢复。

三处细节是踩过才知道的:

- Config 不能整对象标 .volatile(),也不能把 z.object({…}).volatile().dict 摊平再 spread —— 前者会把策略参数一并暴露成表单,后者摊平时丢掉每个字段自己的 meta,实测直接导致整张表单不可见。正确写法是逐字段 .volatile()。
- volatile 字段解析后是引用(cosmokit 的 Volatile<T>),不是裸值,读值必须走
.get()。因此加了一层 flattenConfig() 把引用拍平成裸值,上层代码对代次无感;
它同时认得裸值形态,所以 0.1.5 及更早的代次照样工作。
- provider / model 从 z.string().min(1) 改成 .default(''):标进表单后必填会让插件「不填就不能启动」,而留空=继承主请求路由恰恰要求它是可选的。

其它跨代取舍

- 不用 settingsNamespace():该 helper 在 0.1.2 已被删除,运行时导入会让整个模块链接失败。它只是校验后原样返回 brand 字符串,故改用 'suggest-ghost' as SettingsNamespace 字面量断言。
- 不导入 deepFreeze:0.1.1 由 dsh-llm 导出,0.1.2 把它迁到新增的 dsh-util-values 并停止转发,而该包在 0.1.1 内核中不存在 —— 换导入源只会反向破坏另一代,因此 src/generate.ts 自带一份等价实现(迭代遍历、循环引用安全、跳过 AbortSignal)。
- client 端不从 @deepseek-ai/dsh-client-runtime 取类型:该包是 0.1.1 内核特有,0.1.2 已拆走。lib/client.js 实测零内核导入(只需 react),跨代无关;ctx.remote 的最小面在 client 侧本地声明(见 src/client/index.ts 的 RemoteLike)。
- source.kind 用 'user' 而非 'plugin':0.1.7 删了 plugin kind、新增的 schedule 在 0.1.5 又不存在,两代无交集,故取共有的 user。该字段只用于构造一次性 LLM 请求载荷,不参与持久化与投影。

已知取舍与限制

- 旧设置会迁移:实测 0.1.5 时代改过的值(例如 historyMaxEntries)在 0.2.0 上原样恢复:profile 文档里就存着非默认值(实测有 historyMaxEntries: 77、historyCrossSession: true),所以重启后读回的是这份持久值,而不是上一次运行的内存值。
- client 侧无轮询:0.1.7 随 settingsScope 一起移除的只是 scope.subscribe,内核另有转发给 client 的事件通道 —— ctx.remote.$on('settings/document-updated') (在 dsh-api-remotes 的转发白名单内,mode emit)。内核自己的设置页即用它驱动 SettingsDescribeMirror。本插件同款订阅,只在 host 真推 _push 时才发
describe(),另在首读与 connection/reset 时各读一次。

复现方式

为待测内核建隔离 profile($DSH_HOME 指向临时目录,node_modules 用 cp -al 硬链接对应内核树,避免向上解析撞到另一代的树),先 dsh --profile --dump-config 校验 manifest 组装 —— 这一步在 0.2.0 上就会暴露 peer 门禁的拒绝,再 dsh --profile "say hi" 与 dsh --profile -- --no-open --port 观察插件加载。headless 启动停在 MISSING_CREDENTIAL(临时 home 无凭据),该点在插件加载之后。

开发

src/index.ts host 入口:turn/end(completed) → 有界生成建议;Config(策略 + volatile 可编辑项) src/coldstart.ts 冷启动语义(纯函数):日志取环种子、回合语义校验、建议追踪 src/generate.ts 转录提取 → 脱敏 → ctx.llm.stream → 净化;flattenConfig 拍平 volatile 引用 src/transcript.ts 转录纯逻辑:字符 + UTF-8 字节双预算裁剪(纯函数,可单测) src/sanitize.ts 脱敏 / 净化 / 语义过滤 / 截断(纯函数) src/settings.ts _push(建议格)/ _hot(热度格)/ _ops 载荷形状 + effectiveConfig src/hotness.ts 跨会话热度表(增量去重、最小堆淘汰、内存上界、固定/管理 API) src/hotness-store.ts 热度持久化(storageDomain 域、回合边界合并落盘、重启恢复合并、管理 ops 应用) src/projection.ts suggestGhost 投影 last-wins fold src/client/ 幽灵渲染、历史匹配、逐词切分、快捷键、设置卡片; settings-scope.ts 是卡片 scope 最小面,index.ts 的 remoteSettingsScope() 适配 scripts/ 冒烟测试与会话日志回放

改双向 settings 通道前必读

_ops / _push / _hot / _residual 是插件写进 settings 的全部通道(_push 与 _hot host→client 的两格,客户端都会读;_residual 是宿主自用格——装「读到过但所属会话始终没进店所以判不了」的残留,#56,客户端不读它,所以它不进投影面),改过的三个缺陷都长在通道语义上,不在业务逻辑上:mutate() 的第三个参数是「我读到的 revision」,不是「我要覆盖的版本」,被拒时看起来像写成功;zod schema 与 TS 联合类型是两份互不约束的真相,只改一边就静默丢载荷;单槽 last-wins 快照装不下「只存在一瞬间」的载荷。另外三条更容易反复踩到:apply(ctx, config) 捕获的 volatile 引用会在首次配置写入后冻结,settings/document-updated 挂在插件自己的 ctx 上收不到,以及在它的回调里再发起任何 settings 写入都会被内核拒成 HMR transactions cannot be nested(ALS 事务沿 await 传播,推迟一个 tick 不算逃出)。文档里另有一节讲清留痕为什么只能走 stdout:ctx.logger 在健康进程里不落任何地方,运行期唯一可见面是 console.log 经宿主 tee 进日志,所以本插件用语义标记 [dsh-suggest-ghost][ops-trace] 把留痕与调试噪声分开。

Gitea 协作流程(评论命令的正确形式与回读、分支/PR 约定、lib/ 与主仓纪律)见 docs/gitea-workflow.md。

内核 settings 通道的坑见 docs/internals-cordis-settings-pitfalls.md:每条按「症状 → 根因 → 正确用法 → 错误用法」写,附源码行号与 commit,并区分「已修」「待宿主验证」「内核契约」「仅本插件经验」四档确定性,文末是改动前后的复查清单。

改代码前:哪份副本会被宿主加载

结论:宿主加载的是 <profile>/node_modules/dsh-suggest-ghost 这条符号链接解析到的工作副本,不是你正在编辑的那一个。 本机现状:

~/.dsh/profiles/web/node_modules/dsh-suggest-ghost
-> ~/workspace/dsh-plugins/dsh-suggest-ghost

配套事实有两条,都会决定一轮验证算不算白跑:

- 本仓 lib/ 纳入版本管理,宿主 import('dsh-suggest-ghost') 加载的是 lib/index.js (package.json 的 main)与 lib/client.js(exports["./client"])。src/.ts 只是源,改完 src/ 不重建就请求重启,加载的仍是旧字节码。
- 仓库可能同时存在多个 git worktree(并行修多个 issue 时必然如此),链接只指向其中一个。
在别的副本里改、构建、重启,都不会生效。

开工前跑这两条,都是只读的:

node scripts/artifact-freshness.mjs # lib 是否比 src 旧(2s 容差);部署链接指向哪个副本
node scripts/gitignore-check.mjs # 有没有「已跟踪却被 ignore 覆盖」的目录,fatal 退出码非 0

gitignore-check 挡的是一类静默失效:lib/ 必须入库,所以一条盖住它的规则(.gitignore 里的 build/、lib/、src/ 之类,或 ~/.config/git/ignore 里的全局规则)会让该目录下新建的文件从此不进索引。文件在盘上、构建照常、git status 一片干净,而 git checkout 恢复不出它们,因为 git 从来不知道它们存在。这条判据本身有个易踩的坑:git check-ignore 默认跳过已跟踪路径,必须加 --no-index,否则恰好查不到这种状态。

改动落盘与验证的顺序(本仓多次因为跳过后半程返工):

1. 在你的 worktree 里改完,跑 pnpm run test:smoke(全绿再动下一步);
2. node build.mjs 重建 lib/,把 src/ 与 lib/ 一起提交;
3. node scripts/artifact-freshness.mjs 确认产物不陈旧、且指向的副本就是你要宿主加载的那份;
4. 再请求重启。未提交的改动不要拿去验证:并发会话或其他工具对工作区的写入是不可预期的,提交过的内容才追得回来。

宿主层改动怎么不重启就生效

改 src/index.ts → node build.mjs 之后,默认配置下要重启宿主才生效,但这是 profile 配置的结果,不是平台限制。DSH 自带插件热重载(@deepseek-ai/dsh-hmr),本 profile 没开它的源码监听:合成配置里那个条目是 config: { root: [] },而它的 README 写明 root 为 [] 时「只保留 explicit configuration watches」,即只监听 profile 的配置文件。

第二层原因更容易踩:默认 ignored 是 ["/node_modules", "/.", "cache", "data"],而开发期的本插件是以 link: 形式挂在 profiles/web/node_modules/dsh-suggest-ghost 下的。所以即便把 root 配成 ["."],默认 ignore 规则也照样把它排除在监听之外。

要开监听,在 profile 的 cordis.patch.yml 末尾加一段(root 收窄到本插件产物目录,别让 chokidar 去遍历整个 node_modules):

- id: hmr
disabled: false
config:
root:
- node_modules/dsh-suggest-ghost/lib
ignored: []
debounce: 100

这段的合法性可以先不启动就验:dsh --profile web --patch <这个文件> --dump-config,对照 id: hmr 是否带上这三项、其余条目行数不变。
启动之后的判据就在日志里,不用猜:本插件每次挂载都会打一行

[dsh-suggest-ghost][ops-trace] apply called; artifact mtime=<那份 lib/index.js 的 mtime>

touch lib/index.js 后再看这一行:mtime 变了,说明热重载把新构建换进来了;mtime 不变,说明还在旧字节码上,此时无论验证结果如何都不作数(这正是 issue #20 那类白跑一轮)。客户端侧不需要宿主重启:browser 侧有独立的模块加载机制,刷新页面即可。

不想手工做这些就用 pnpm run sandbox(scripts/dev-sandbox.mjs):独立 DSH_HOME、独立插件产物副本、自动避开被占用的端口、剥掉会重放的遗留载荷,并把该 grep 的留痕串打印出来。脚本本体不含任何插件专有知识,本仓专有的值放在 package.json 的 sandbox 字段(entry / rewrites / disable / greps)。

限制:hmr/change 的重载走内核的 runExclusive 事务,因此重载期间插件在事件回调里发起的 settings 写入同样会被拒(docs/internals-cordis-settings-pitfalls.md 陷阱 1.5)。重载后第一次 _ops 清空失败属预期,不代表改动没生效,判据是 apply called 那行的 mtime。

冷启动语义(重启后为何立即可用)

重启后「没有数据」并非真的没数据:历史在会话日志里,建议由 host 按语义校验后重推。所谓冷启动问题,本质是判据选错了——旧实现用「时间」(重启即作废、事件到达才播种)当判据,于是留下两个空窗。现在统一改为语义判据:

- 历史环是会话日志的缓存:pull 到达时未命中就从日志现算(ringFor),不存在「播种时机」,也就没有「pull 抢在首个事件之前 → 回空」的竞态。会话活跃期间由 session/event 增量维护。
- 建议是否可用,取决于它对应的回合是否仍是该会话的最后一个已完成回合(suggestionIsCurrent)。host 每次组装状态都经这道校验:有效则原样恢复(打开页面即见上一轮建议,不等新回合);可判定为陈旧则清场。
- 三条推送路径(pull 应答 / 回合结束 / 热度恢复补推)共用同一份状态组装(statePushOf),不再各写各的;client 侧 pull 统一在会话绑定成功处发起,冷启动、切会话、会话晚就绪三合一。
- 热度表恢复完成后(whenReady)补推一次完整快照,关掉「恢复窗口内推送了不完整频次」的窗口。

与 0.1.5 的差别:那时 _push 落 settings.yaml,重启后可回读盘上残留的上一条建议。0.1.7+ 之后别把"volatile 字段不落盘"当成保证:本插件的 _ops / _push 实测就写在 profile 文档里(cordis.patch.yml 中能看到 _ops: '{"rev":…}'、_push: '{"rev":…}' 两行),而"消费后清空 _ops"在事件回调里会被内核按 nested transaction 拒掉,所以载荷会长期留在盘上。直接后果是新实例一启动就重放上一进程遗留的 op(隔离实例实测:客户端还没连上就消费了一条 complete)。历史前缀补全与热度面板照常可用——它们走会话日志与 storage 域。注意区分:用户的设置值同样在 profile 文档里、照常迁移(见「已知取舍与限制」)。

pnpm run build # tsc 编译 host + esbuild 打包 client → lib/
pnpm run test:smoke # 纯函数冒烟测试(含热度持久化语义、冷启动语义)
pnpm run test:e2e # 真实栈端到端:热度落盘→重启恢复;冷启动重启对账/pull 应答
pnpm run replay # 用真实会话日志回放补全管线
pnpm run sandbox # 起一个隔离的 DSH 第二实例(不重启,且不碰 live 要加载的产物)
pnpm run sandbox:check # 只合成校验;sandbox:deploy <dir> 只装构建

已知限制

- 跨会话热度表已持久化(宿主 storage 域,落 ~/.dsh/storages/suggest_ghost_hotness.json):重启后频次恢复,不再从零累积;宿主无 storage 域(如极简 headless 组合)时自动降级为内存态,重启清零
- 别把「volatile 字段不落盘」当保证:本插件的 _ops / _push 实测就写在 profile 文档里,而"消费后清空 _ops"在事件回调里会被内核按 nested transaction 拒掉,所以载荷长期留在盘上,新实例一启动就会重放它(隔离实例实测:客户端还没连上就消费了一条遗留 complete)。详见上方冷启动一节与 issue #22
- 重启后历史补全与上一轮建议立即可用(判据是语义而非重启时机);但新回合的建议仍需等该回合跑完才能生成——这是「预测下一条」的固有代价,不是启动延迟
- 热度管理面板显示并过滤的是推送的 top-K 快照(默认 50 条,「共 N 条」展示全量);更大范围的主机侧搜索未做
- 删除只清当前统计——再次输入同文本会重新计入;「固定」才是不被淘汰的语义
- LLM 建议只覆盖当前会话;把其他会话文本并作候选需开启「跨会话搜索」
- 每个完成回合都会调一次建议模型(与输入框是否有内容无关),不需要时可在设置里关闭省 token

许可

MIT © wujue。安全与生成管线参考 dsh-suggest-prompt(MIT)实现。
预览分享卡片长什么样 ↓dsh-plugin-collection/dsh-suggest-ghost
分享 · DeepSeek Harness Hub
dsh-plugin-collection/dsh-suggest-ghost

DSH Web 输入预测插件:回合结束后 LLM 生成下一条建议,输入框空草稿时渲染幽灵文本,Tab 采纳。

回到顶部