@jayyuen66/dsh-zvec-grep
dsh 工作区语义检索插件(zg):zg_search / zg_index / zg_status + 本地 embedding 设置卡片 + systemPrompt 路由规则 + guard 安全兜底。 适合需要在大型工作区中进行混合检索、强制先检索后读取的开发任务。
Install
$
npx -p @deepseek-ai/dsh dsh plugin --profile web add github:JayYuen666/dsh-zvec-grepREADME
Read the full README ↗@jayyuen66/dsh-zvec-grep
中文
它做什么
- 给 dsh 会话接上工作区语义检索:注册 zg_search / zg_index / zg_status 三个模型工具,底层经宿主 shell 以
--mode direct调 zg CLI(BM25 + 向量 + RRF 混合检索)。 - 注入一段 systemPrompt 硬规则(
zvec-grep-routing,order 1550)+ 两道 tools.guard:zg_* 安全兜底与 search-first 门禁(已建索引的工作区里,grep/rg 前必须先有一次成功的 zg_search)。 - 附带 web 端设置卡:默认 embedding、limit、search-first 配额/时效,以及「工作区重建」按钮。
安装
dsh plugin --profile web add @jayyuen66/dsh-zvec-grep
- 包在公共 npm 上,安装不需要凭据;发布态产物只有 host.js、client.js、cordis.patch.yml(package.json 的 files)。
- 运行期值 import
@jayyuen66/dsh-plugin-shared(lib/http、lib/locale)与@deepseek-ai/schemastery(宿主 fork:0.1.7 的.volatile()解析只有它有实现,公共 schemastery 既没这个方法、解析出的也仍是普通值),两者都在 dependencies 里,装不上就是 ERR_MODULE_NOT_FOUND。
在 dsh 里启用
- add/remove 按包内 cordis.patch.yml 维护注册行
- id: zvec-grep(name 指向本包);该文件由package.json的dsh.bundle.patch指向。 - 宿主版本要求
>=0.2.0-rc.2:写在peerDependencies(0.1.7-rc 起宿主装插件时校验它;alpha.1 还没有这道门)与engines.dsh(同值、无人读)。 dsh.client.platform为 web 且 immediately,设置卡的 client 半随宿主下发;bundle 安装走 pnpm(>= 12.3.0)。- apply 需要 settings/shell/tools/systemPrompt/timer 五个服务面:它们都在
inject里,缺任一 cordis 就不激活本条目(宿主启动诊断点名pending (waiting for service: …));apply 里的守卫再兜一层,ctx 形状不对时抛required services … missing。- 三条
/_dsh/zvec-grep/*端点挂在inject(["webServer"])的子 fiber 上:宿主没有 webServer(如 TUI)时子 fiber 不激活,端点不存在而工具照常。真实宿主上 webServer 比本条目晚到位约 1 秒,所以这里必须是依赖、不能只在 apply 里ctx.get读一次。
- 三条
提供给模型的工具
- zg_search:query / queries / fts / vector 至少其一(合计 --capability embedding --scope workspace` 才肯发请求,那道按 root 的授权本插件不代劳。
requireApprovalForExplicitRoot=false:打开后,三个 zg 工具里显式给了 root、且那个 root 既不是本会话工作区、也不在本会话已登记范围内时,会经官方tools/pre-execute → {kind:'ask'} → ctx.approval弹一次用户确认。默认关,且有三个已知边界:danger-full-access预设把approval/policy写成never(ask 会被确定性拒绝)、委派子代理被钉死approvalPolicy: 'never'(该开关对子代理无效)、宿主没装 approval 服务时 fail-closed 成拒绝。这道确认只加不减:rootOf的授权判据一字未改,越界的 root 即便「用户已批准」照旧被拒。
- 优先级:设置里的运行时值 > 组合包层/用户层行的 config > schema 的
.default();行 config 中显式缺省的字段不覆盖默认。读侧是config..get()(volatile 引用,卡上改完下一个事件即生效);非 volatile 部署值是普通值形态,不经.get()。 - root 校验:必须是以
/开头的绝对路径,词典归一折叠空段与.。- 拒绝空串、非字符串、含 NUL、相对路径、越出文件系统根的
..,归一后为/也拒。 - 目录不存在时单报「root 目录不存在」,不冒成 spawn ENOENT。
- 拒绝空串、非字符串、含 NUL、相对路径、越出文件系统根的
- guard 与 execute 用同一套 root 判据,避免「guard 放行、execute 抛错」说两遍。
- 解锁按「会话 × 索引根」计:每次成功 zg_search 重置为 grepBudgetPerSearch 次、unlockWindowMin 分钟有效;索引根是 root 自身或含索引的最近祖先(最多上溯 8 层)。
- 分片用的会话是根会话:沿
header.parentSession一路上溯到最顶层那一跳。故同一棵委派树上的主代理与子代理共享一份额度——主代理搜过一次,子代理随即就能用那几次 grep/rg,不必各自再搜一次(它们搜的往往是同一棵树)。 - 上溯的降级是三级的,都不抛错:自身即顶层就用自己;某一跳查不到它的父(会话已落定,或宿主没装会话存储)就停在已知的最后一跳;链成环就停在首次重复前。跳数有上限。
- 分片用的会话是根会话:沿
- 「含索引」的判据是
/.zvec-grep/manifest.json在盘上,只判.zvec-grep目录名不够:这个名字同时是 zg 自己的全局 home(ZVEC_GREP_HOME ?? ~/.zvec-grep,装的是 config.json / locks / models),只判目录名会让一个从未建索引、只是祖先目录撞名的普通工作区也被门禁拦下。 - enforceSearchFirst=false 只关掉门禁,zg_* 的安全兜底(confirm、root 判据)不受影响。
外部依赖(rg)
- 真正的二进制依赖是 zg(由
npm install -g @zvec/zvec-grep提供):不在 PATH 时 shell 回 exit 127,工具据此回「zg 未安装或不在 PATH」,与「沙箱 runner 没起来」分开归因。 - 本地 embedding 由 zg 侧后端加载:qwen3/embeddinggemma 走 llama-cpp(device=auto 时试 Metal、失败回退 CPU),potion-* 走 model2vec,其余走 transformers-js 纯 JS。
- 权重按 hfEndpoint 注入 HF_ENDPOINT 后下载。
- 本包不依赖也不调用 ripgrep:rg 只出现在门禁判据里。
- bash/pwsh 命令串经引号/注释感知切词后,命令位上的 grep/egrep/fgrep/rg 与原生 grep 工具才算检索。
--help/--version/-V、echo "grep x"、git commit -m "use rg"不算。
- 有沙箱执行器时会区分 runner 失败、策略拒绝与 zg 自身失败,并在结果里显式打「可能不完整」标记。
对外接口
- 三条路由的 handler 第一句都是
guardTrust(req, res, { servingNonLoopback })(shared/lib/trust),任一判据不过即403+{ ok:false, error: ... }。- 判据次序:Host 权威 →
sec-fetch-site白名单 →origin与 Host 逐字比对;两句错误文本是"untrusted host authority"与"cross-origin request rejected"。 servingNonLoopback只取webServer.host === "0.0.0.0";非本方法的405带Allow与{ ok:false, error:"GET only" | "POST only" },不再是空回显。
- 判据次序:Host 权威 →
GET /_dsh/zvec-grep/rebuild-roots:只回本包自己的数据(每次 apply 重新生成的 CSRF token + 已观测工作区列表),允许 GET/HEAD,过不了上面那道闸门即 403。POST /_dsh/zvec-grep/rebuild?root=...:闸门 + 头x-zvec-grep-csrf+ body 0,看不见了);缓冲区溢出= 执行器的读者报 lossy 或干脆抛了(真少了字节,可能有 spill 文件)。超时说明与沙箱事实**不进环**:环的写权归注册表的泵,而它在落定之后丢弃一切写入(实测),所以这两条由端点在投影时合成,永远不会因为"写晚了"而消失。注册表被重载或记录被别人收走时轮询答 **404**(官方get/readAt对未知 id 抛错,而宿主对 handler 抛出的兜法是记一条日志再写一枚没有 body、没有 content-type 的 **400**,只有头已发时才断掉 socket —— installed dsh-host-webserverlib/index.js:246-256` ⇒ 卡片会进终止态并停轮询,那条还在跑的重建就再也看不见)。
数据与隐私
- 索引的唯一落盘形状是被检索 root 下的
.zvec-grep/目录,由 zg 写入(read-only 沙箱下写不了它,会明确报索引可能不完整)。 - 重建有两条路:模型侧
zg_index(rebuild=true、confirm=true),或设置卡「工作区重建」走上面的 rebuild 路由;删除是zg_indexdrop=true(拼成zg index --drop --yes)。 - 本包自身不写任何文件(唯一的 fs 调用是索引探测:existsSync + 读一次清单),也不向宿主数据目录
$DSH_HOME(``)落数据。- 索引探测不只判「
.zvec-grep/manifest.json在不在」:还要解析清单并确认它的rootPaths真的覆盖这个目录。两种实况会被「只判存在」误判成有索引,而实际检索必然失败——文件在但不是一份清单(半截写入/同名占用),以及清单在但根路径指向别处。此时门禁若还按「已建索引」拦着 grep/rg,就等于把检索与门禁双向堵死。 - 认的结构只有
manifestVersion(严格等于 1)与rootPaths(非空数组、每项带非空absolutePath)两处——版本不等时上游读它会直接抛MANIFEST.INVALID,此时这个目录一次检索都做不了,宽松放行只会把用户卡在「门禁拦着、检索又必失败」的死角里。刻意不校验 embedding / policy 等字段——那些变了索引照样能用,判成「无索引」反而是错的。 - 覆盖判据是归一后相等;macOS 上清单里写的是未解析路径(
/tmp/x)而path字段才是解析过的(/private/tmp/x),故两边都再剥一次/private前缀再比,两种写法都能对上。 - 白名单/解锁表都在内存、有界,且淘汰策略与语义相符:命中即触摸(挪到队尾),淘汰时先按有效期丢过期项、再按最近使用丢最冷的一条到上限之内——不是按插入序。纯插入序对一张「每次成功检索都重置同一批键」的表等于不淘汰:真正冷掉的项永远留着,而很久没被碰过的键一直占位。白名单条目另有一条有效期(一天),一个月前顺手开过的一个目录不该今天还占名额。容量上界(白名单 64 条 / 会话分片 256 个)保持不变,故常驻内存依旧有界。
- 重建作业本体住在宿主注册表里(见上一节),插件卸载时本包侧表随 effect 回收、在跑的作业经注册表 kill 掉。
- 索引探测不只判「
- 默认不进索引的:
*.pem/*.key/*.p12/*.pfx/*.keystore/id_rsa*/id_ed25519*(excludeSecrets=false 可关)。这些规则按大小写不敏感匹配,Server.PEM同样被排除;且排在用户自己的路径规则之后生效,用户写一条宽泛的正向规则也覆盖不掉它们。- 残留缺口:无扩展名的密钥文件(如
myprivatekey、rsa_private)扩展名规则覆盖不到,id_rsa*/id_ed25519*只覆盖 OpenSSH 默认命名;这类文件请走 ignoreFiles(--ignore-file)显式声明。
- 残留缺口:无扩展名的密钥文件(如
- 隐藏文件默认不收,ignore 文件默认生效(noIgnore 可关)。
- 本包只在本机拼 zg 命令并透传输出,不上传代码。
- 唯一的外连是 embedding 权重按 hfEndpoint 下载。
- 结果回显有界:stdout 超限截尾(有 spill 路径会给出)、stderr 尾部 400 字符一并回显,免得「退化成纯词法」被当成完整语义结果。
常见问题
- grep/rg 被拒且提示「尚未执行过 zg_search」:这是 search-first 门禁,先 zg_search;配额用尽或过期同理,换个更准的 query/fts 重搜即可续上,不必关插件。
- 检索超时:默认
--refresh wait会同步刷新既有索引,大工作区请用 globs/fileTypes 缩小范围或调小 limit,也可先手写字面检索。 - 重建按钮回 403「root 不在本 daemon 观测到的工作区白名单内」:该目录从未在本 daemon 开过会话;先在该目录起会话,或让模型带 confirm=true 调 zg_index。
- 卡片显示重建失败而 zg 看着像跑完了:成功判据是 completed 且 exit 0——exit 127(zg 没装)也落在 completed,看输出尾部原因。
- 从未建索引的 root:zg_search 明确报错而不是静默建索引;建索引只在用户明确要求时由 zg_index 做。
- 换了 embedding 却搜不到语义结果:切换 embedding 必须 rebuild=true 重建后才生效。
English
What it does
- Adds semantic workspace search to dsh sessions: registers three model tools, zg_search / zg_index / zg_status, which drive the zg CLI (BM25 + vector + RRF hybrid) through the host shell with
--mode direct. - Injects one systemPrompt rule section (
zvec-grep-routing, order 1550) plus two tools.guard layers: safety backstop for zg_* and the search-first gate (in an indexed workspace, grep/rg requires a prior successful zg_search). - Ships a web settings card: default embedding, limit, search-first quota/window, and a "rebuild workspace" button.
Installation
dsh plugin --profile web add @jayyuen66/dsh-zvec-grep
- The packages are on the public npm registry, so no credentials are needed; the published artifact only carries host.js, client.js and cordis.patch.yml (the files field in package.json).
- Runtime value imports are
@jayyuen66/dsh-plugin-shared(lib/http, lib/locale) and@deepseek-ai/schemastery(the host's fork — the only one whose resolve wraps.volatile()fields intoVolatilereferences); both sit in dependencies.- Missing either one is
ERR_MODULE_NOT_FOUNDat loa
- Missing either one is