dsh-simple-search
源码与问题追踪:github.com/Snowy117/dsh-simple-search
给 DeepSeek Harness 用的结构化内容搜索插件:一次调用回答一个问题,返回值是 canonical JSON,
坐标、排序与翻页三组承诺都有明确的语义。引擎是打包的 ripgrep,所以它就是 rg 的速度与
匹配语义,只是把「给人看的输出」换成了「给程序看的输出」。
调用方拿到什么
工具名 search,与内置 grep/glob 共存、不覆盖。三种 shape 共用一个外壳,
root / items / nextOffset / elapsedMs 同名同义:
shape | 一次调用回答的问题 | items 是什么 |
|---|
lines(默认) | 哪些行命中了、长什么样 | 逐行命中:path / line / column / text / matches |
files | 哪些文件提到过它(或名字模式:范围内有哪些文件) | 命中(名字模式下列出)的文件路径,附精确 totalFiles |
count | 每个文件各有多少处 | {path, count},附精确 totalFiles / totalMatches |
翻页协议:无状态 offset。返回值里有 nextOffset 就表示后面还有;把它原样作为下一次的
offset 继续,三种 shape 的翻页习语逐字相同。nextOffset 不存在就是到底了(此时不编「还有 N 条」
——lines 是流式早停的,总数不可知)。
坐标语义:line 是 1-based 行号;column 是 0-based 列号且恒等于 matches[0].start;
matches 里的 start / end 是 text 内的 JS 字符下标(半开区间),恒有
text.slice(start, end) 等于那段命中文本。超长行只交付围绕首个命中开的窗口,此时
textTruncated: true,坐标只对 text 有意义、不再对应磁盘上的整行。
零结果与「结果多」都是正常返回,不是错误:items: []、没有 nextOffset。
安装
本包发布为 npm 包,也是 DSH 的 plugin bundle(cordis.patch.yml 声明 search 工具)。
装进某个 profile(第一方 CLI 的插件管理是转发给 pnpm 的):
# 发布到 registry 之后(包名 dsh-simple-search)
dsh plugin --profile <profile> add dsh-simple-search
# 从本地 tarball 装(当前仓库可直接这样试)
npm pack # 产出 dsh-simple-search-0.1.2.tgz
dsh plugin --profile <profile> add "$PWD/dsh-simple-search-0.1.2.tgz"
dsh plugin 把参数原样转发给 profile 目录里的 pnpm,所以本地路径要写绝对路径。
本机需要有 pnpm;装完重启该 profile 生效。不想动 DSH 配置的话,也可以在任何 ESM 工程里直接
npm install dsh-simple-search,它导出 apply(ctx) / install(host) / inject / name
供 cordis 宿主装配。
依赖约定:@deepseek-ai/dsh-tools 是 peer,必须由宿主提供,版本与 dsh 本体一致
(当前 0.2.0-rc.2)。不要把它并进 profile 依赖——多出来的第二份副本会让 dsh 每次
工具调用都报 Cannot read properties of undefined (reading 'prepare');0.1.1 的发布事故
正是如此,0.1.2 起改为 peer。
参数
| 参数 | 类型 | 默认 | 说明 |
|---|
pattern | string | — | ripgrep 正则;mode: "literal" 时按字面串。空串匹配所有行。省略即名字模式:不筛内容、只按 glob 选文件,此时必须显式 shape: "files",且不能传 mode/case |
path | string | 会话工作目录 | 搜索根目录(不是文件);结果里的 path 相对它 |
glob | string | string[] | 不过滤 | ripgrep glob,! 前缀为排除。"*.ts"、["src/**", "!**/dist/**"] |
mode | "regex" | "literal" | regex | 含正则元字符的字面串用 literal(内容模式) |
case | "smart" | "sensitive" | "insensitive" | smart | pattern 全小写时忽略大小写(内容模式) |
includeHidden | boolean | false | 搜隐藏文件与点目录(.github/、.env.example、.dsh/) |
noIgnore | boolean | false | 无视 .gitignore/.ignore(vendored、生成物);可能很慢。VCS 元数据目录永远排除 |
shape | "lines" | "files" | "count" | lines | 见上表 |
limit | integer | 200 | 本页最多多少条,1..2000;越界报错而不是钳位 |
offset | integer | 0 | 跳过前 N 条;续页时传上一页的 nextOffset |
关于 glob 有一个容易意外的 rg 语义:正向 glob 会覆盖 .gitignore 与隐藏过滤,但只有命中「目录」时
才解除该目录的剪枝。所以 glob: "**/*" 会连 node_modules、被 ignore 的目录、点目录一起列出来,
而 glob: "**/generated.ts" 救不回被 ignore 目录里的同名文件(要写 glob: ["ignored", "**/generated.ts"])。
想按常规范围搜就别给 glob,用 includeHidden / noIgnore 两根旋钮。
只搜单个文件:传它所在的目录 + glob: "文件名"。
名字模式(省略 pattern + 显式 shape: "files"):不读文件内容,只按 glob 列出范围内的文件,
返回值仍是 files 分支(items / totalFiles / nextOffset 语义不变)。它与内容模式 pattern: ""
的差别是三个静默缺口——空文件(0 字节)、二进制文件(NUL 嗅探)、权限不可读的文件,内容搜索
都会漏掉或整通失败,名字模式照列:
{ "shape": "files", "glob": "**/*.png" } // 仓库里有哪些 png
{ "shape": "files", "glob": ["src/**", "!**/*.test.ts"] } // src 下的非测试文件
{ "shape": "files", "includeHidden": true, "glob": "**/.env*" } // 连隐藏的 .env 一起找
名字模式的 items 与内容模式同一套规范序(comparePaths),totalFiles 同样精确、与分页无关。
它治不了的一类:不可读的目录会让遍历本身失败,两种模式都整通报 SEARCH_FAILED——
照 message 里的建议传 glob: ["!<那个目录>"] 绕开。
为什么会误用:shape: "files" 又忘了写 pattern,会列出文件而不是报错(其余忘记 pattern
的写法都是响亮报错)。想按内容找文件,请带上 pattern。
错误码
失败一律 reject 成带 code 的错误,message 是可诊断的中文文本:
code | 何时 | 怎么处理 |
|---|
SEARCH_INVALID_PARAMS | 未知参数键、非法枚举、越界数值、glob 为 ""/"!"/空数组、path 空串或含 NUL;省略 pattern 时 shape 不是显式 "files",或给了 mode/case | 按 message 点名的那一项改参数 |
SEARCH_INVALID_PATTERN | rg 拒绝了这个正则或 glob | 修 pattern/glob;字面串用 mode: "literal" |
SEARCH_PATH_NOT_FOUND | path 不存在或不是目录 | 改 path,或用「所在目录 + glob」 |
SEARCH_TIMEOUT | 超过工具超时或调用方取消(经 exec.signal 协作中止) | 缩小范围(glob、path)或调大超时 |
SEARCH_FAILED | 引擎启动失败、rg 退出码 ≥ 2(不可读路径等环境故障) | 按 message:它点名出问题的路径,并给出可直接重试的排除 glob |
树里只要有一个不可读文件或目录,rg 就会以退出码 2 收场——此时整体失败、已交付的部分丢弃,
与第一方 grep 行为一致。message 会点名路径并给出形如 glob:["!locked"] 的建议,照抄重试即可。
开发
npm run check # 两份 tsconfig 的类型检查 + 行数闸门 + node --test
npm run build # tsc → dist/(发布产物)
node scripts/pack-check.ts # 打包验收:npm pack → 解包断言 → 临时装包跑通
测试直接跑 TypeScript 源码(Node 的类型剥离),不必先编译;tests/ 里的 fixture 都建在临时目录、
用完即删。PTC 声明是 interface 的一部分,改动后要用
node scripts/render-declaration.ts --write 重新生成 golden。
设计出处
参数与返回值的取舍、错误模式的判据、性能特性(lines 流式早停 vs files/count 全遍历)、
以及「为什么这样定」都在 DESIGN.md。本 README 只做使用说明,冲突时以
DESIGN.md 与实现为准。
发布(维护者)
npm login # 首次需要(浏览器或 automation token)
npm publish --dry-run # 先看会发什么:只有 dist/ + cordis.patch.yml + README + LICENSE
npm publish # prepublishOnly 会先跑 check + build;publishConfig 已指定 public / latest
发布后即可 npm install dsh-simple-search,或用上面的 dsh plugin 命令装进 profile。
升级 dsh 本体后:@deepseek-ai/dsh-tools 的 peer 版本要跟本体一起改(prerelease 用精确版本,
由 host 提供的那一份必须是同一个版本)。
许可证
MIT