본문으로 건너뛰기
P

dsh-knit

polinnizhong/dsh-knit

Lists the Markdown documents, images and video that already exist anywhere in the session workspace in the DSH sidebar, ranked by relevance to the current conversation: recent messages are matched locally against document title, summary and body with IDF weighting, with no model calls and no network. Because the list is scanned from the workspace instead of remembered, restarting DSH or starting a new session does not empty it. Images and video preview in place, with relative-path images resolved and video streamed over HTTP Range. The same ranking is exposed to the agent as a knit_docs tool, which returns the most relevant documents along with the passage that matched in each, where one is found.

설치

dsh plugin --profile web add github:polinnizhong/dsh-knit

README

Knit

Agent 一天产出 20 篇文档,你找不到刚才那篇。 Knit 把它们放到对话旁边 —— 你正在聊什么,相关的那篇就在最上面。

你的 agent 也一样。 同一份排序也给它当工具用 —— 它问「哪几篇相关」,拿回排名加每篇里命中的那段原文

Knit 面板真机截图:右侧栏按相关性列出工作区文档,就地展开 Markdown 预览,预览头下方是引用条

真机截图(不是原型):一个含 34 篇文档的工作区 —— 列表 + 就地预览 + 引用条。 引用条是 v0.12 加的:展开就能看到「这篇被谁引用」,点一项直接跳过去。 顶行会跟着你正在聊什么变;对话内容还不足时它退回按时间排,并如实说明依据。

扫整个项目文件夹的 Markdown / 图片 / 视频 · 排序跟着对话走 · 不调模型、不联网

dsh plugin --profile web add dsh-knit

这不就是个「最近文件列表」吗?

是的,但有两个关键区别:

  1. 范围:最近打开列表只记你点开过的文件;Knit 扫整个项目文件夹。 重启 DSH、新开会话、跨天回来,它都还在。
  2. 排序:它按时间排;Knit 按你正在聊什么排。

三句话说完它是什么:

  1. 整个项目文件夹的 Markdown,不只是这一轮生成的那几篇 —— 重启、换会话、跨天都还在
  2. 排序跟着你正在聊什么走:聊架构,架构文档浮上来;聊竞品,竞品分析浮上来
  3. 不调模型、不联网:全是本地字符串运算,零延迟、零成本、文档不出本机

「相关」是怎么算出来的

没有玄学,就是字符串运算。三步:

1. 读当前会话。 取最近 6 条用户 / 助手消息,只认真人输入的用户消息 (agent.inject() 塞进来的合成上下文不算,那会把话题带偏)。越新的消息权重越高:3 / 2 / 1 / 1 …

2. 抽关键词。

  • 英文词:取值很高,出现 1 次就要(chokidarmtime 这种精确词)
  • 中文 2/3-gram:出现 2 次,或出现在最新那条消息里
  • 丢掉跨词边界的碎片:中文没有词边界,n-gram 会把相邻两个词的字粘起来 (「图片和」「个插」「的排」)。这类碎片有个共同特征 —— 首字或尾字是纯虚词, 一律丢掉。不丢的话它们会占满候选位,把「图片」「排序」这些真词全挤出去
  • 虚词表过滤 + 贪心去重叠(选了「相关性排序」就不再算「相关性」和「排序」)

3. 给文档打分 —— BM25。

每个词先算 IDF:在语料里越罕见越值钱   ln(1 + (N - df + 0.5) / (df + 0.5))
再按字段加权求和:标题 ×4  +  摘要 ×2  +  正文前 2500 字 ×1
每个字段都按 BM25 饱和 + 长度归一化(k1 = 1.2,b = 0.3 / 0.5 / 0.75)
再叠 10% 的时间新鲜度微调(主排序仍是相关性)

为什么是 BM25 而不是「命中次数 × 权重」(那是最初的做法,已换掉):

  • 没有 IDF 时,语料里到处都是的词(比如项目名)和罕见词一样值钱, 于是高频词不产生任何区分度,还稀释掉罕见词的分辨力
  • 没有长度归一化时,长文档靠堆词就能赢
  • 命中次数封顶 6 次是个手写硬拐点;k1 / b 才是为这件事设计的

实测(test/eval/fixture.mjs,21 个用例,两版引擎跑同一套语料):

top-1 命中MRR
旧做法(加权命中)76.2%0.830
BM2595.2%0.976

这套评测在 npm test 里跑,基线由 test/eval/legacy.mjs 冻结的旧引擎现算, 所以「新引擎必须显著更好」是自动验证的,而不是引用一个写死的数字。

跟「自己数关键词」比knit/tools/scale-benchmark.mjs,N = 20/60/180/540): 语料刻意做成有真实陷阱的 —— 12 篇短而聚焦的主题文档,加上一堆「每条主题各提 5 次、 但哪一件都没讲」的长干扰文档(真实项目里的 CHANGELOG 就长这样)。 主题文档一半用描述性文件名,一半看不出内容。

路线文件名说得清文件名看不出MRR 随规模
Knit(BM25)100%100%1.000(不随规模变)
自己 grep -c 数关键词17%0%0.313 → 0.089
只看文件名100%0%0.602

三件事:排序强于自己数关键词(所以让 agent 重算是不理性的); 文件名匹配只在名字描述内容时好使,Knit 是唯一两种都 100% 的; 自己数的可靠性随规模单调下降

关于那行「按「xxx」排序」:显示的是命中词在原文里覆盖的那一段,不是词表里的碎片。 中文没有词边界,候选里必然有跨词的碎片(「项目文档」会切出 项目文 / 目文档), 直接显示就成了乱码 —— 把它们的区间合并再切原文,正好还原出 项目文档。 标签是你自己打的字,所以大小写原样保留(打 BM25 就显示 BM25)。

全是字符串运算 —— 没有 embedding,没有模型调用

并且老实说边界

  • 对话只有一两句时关键词太少,它会退回按修改时间排,并在面板上说明这一点 —— 不假装排了个序
  • 语料只有三五篇时 IDF 几乎不起作用df 的取值范围太窄,动态范围被压扁。 文档越多这个排序越准 —— 这正是它该有的样子
  • 它只能排「和对话有共同词汇」的文档:如果一个词都没命中,所有文档同分, 名次就退化成按时间排

安装

dsh plugin --profile web add dsh-knit

装完重启 DSH,然后硬刷新浏览器(Cmd + Shift + R)。

怎么打开:

  • 会话头部右侧的 Knit 图标按钮(就在右侧栏展开按钮旁边)—— 一键开面板
  • 或右侧栏 tab 条上的「+」→「Knit 最近文档」

可选:装了 dsh-better-sidebar 的话,面板也会注册成它的一个 tab; 没装不受影响,两边是各自独立的可选依赖。


功能

能力
按当前对话相关性排序(BM25 + IDF,纯本地,零模型)
给 agent 用的 knit_docs 工具:让模型自己查「这个项目里跟当前话题最相关的文档」
相关性 / 修改时间双模式一键切换(偏好记在 localStorage)
扫描会话工作区里的 .md(递归,深度 ≤ 6,跳过 node_modules / .git / dist
每项显示:H1 标题(无则文件名)+ 相对时间 + 首段摘要
单击就地展开预览,再点收起
相对路径图片真正渲染(./img/a.png../assets/b.png
文档 / 图片与视频 / 全部 三类一键切换(偏好记住,默认仍是文档,老体验不变);选中态是中性灰填充,不带品牌色描边
图片与视频:方形缩略图网格,最少 3 列、宽了才加列;格子 64px 起步,一屏基准 8 个,超过 8 个不隐藏而是整块等比缩小;视频自动取首帧、叠播放三角与时长角标(零依赖、不转码)
点图片 / 视频在面板内就地预览:图片大图、视频可播放可拖动(HTTP Range 流式,不全量下载)
「全部」视图分上下两区:文档最多 4 条(超出给「查看全部 →」);图片视频不截断,只给计数
预览面板可拖高度(20%–80%,位置记住)、可全屏,Esc 退出
「在本地打开」:用系统默认应用打开当前预览的这篇文档(预览头的路径面包屑同样可点)
双击在新标签页打开(官方文档预览,带 PDF 渲染器与渲染方式切换)
过滤框:按标题 / 摘要 / 路径实时过滤
点工作区路径:用系统文件管理器打开项目文件夹
悬停入口按钮偷看:弹只读浮层列最近 5 篇,点击才进右边栏(不推挤布局)
键盘导航: 移动即预览 / Enter 切换 / Esc 收起
每 5 秒自动刷新 + 手动刷新;2 分钟内改动过的文档打 🆕
中英双语,跟随 DSH 语言实时切换(不用重载插件)
零模型调用、零网络出口

相关度不做可视化(不显示百分比、不画长条)—— 排序本身就是答案,名次即相关度。


也给 agent 用:knit_docs 工具

同一份排序,除了给你看,也开了一个口子给模型。

装好之后,agent 的工具列表里会多一个 knit_docs:它可以问 「这个项目里跟当前话题最相关的文档是哪几篇」,拿到按相关性排好序的 路径 + 标题 + 摘要,再用它自己的 read 打开其中一篇。

为什么有用:agent 想引用项目里已有的文档时,只能靠猜路径、或者把 glob 出来的路径一个个 read 试过去 —— 费 token 又慢。而这份排序 Knit 每一轮已经算好了, 这个工具只是把它交出去。

只读,且不存储任何东西:它读的是项目里现成的文件,不是「记忆」。 和记忆类插件的区别是:它们起点是空的(agent 得先记过才有东西可召回), Knit 一装上就有整个项目的历史文档可用。

三个细节:

  • 不返回相关度分数。它是相对分数(永远有一篇 100%,每次刷新可能换人当), 给模型看会被当成绝对置信度去推理。顺序即相关度 —— 与面板同一条规矩。
  • 不返回正文。agent 有自己的 read 工具;Knit 负责发现,不负责搬运
  • 拿不到会话就报错,不兜底。HTTP 路由在会话查不到时会兜底到进程 cwd (兼容不带 sessionId 的老客户端),工具没有这个包袱 —— 兜底只会扫到一个不相干的项目并返回它的文档。宁可报错,也不返回错的东西。

⚠️ 代价要说清楚:工具描述会进每一次请求的系统提示词。 装 Knit 的用户每个会话都会多占一点 token。这是「让 agent 有能力」的必要成本。


它读什么,不读什么

  • 扫描当前会话工作区内.md、图片与视频(路径越出工作区一律拒绝);媒体只取元信息,不读画面内容
  • 只读当前会话的对话事件(用来排序)
  • knit_docs 工具只读:不写任何文件、不落盘任何索引
  • 不发起任何对外网络请求:客户端的 fetch 都指向插件自己的同源路由
  • 没有安装期脚本(无 install / postinstall
  • 零依赖 —— 装完不需要构建授权,也没有构建步骤
  • 按路径读文件的接口只放行图片 / 视频扩展名白名单(图片 ≤ 12MB、视频 ≤ 256MB), 视频走 HTTP Range 按需取字节,响应带 nosniffdefault-src 'none'; sandbox

面板里那份「相关度」只影响排序,不显示也不外传

上面每一条都有自动化检查守着,逐条列在 SECURITY.md 里 —— 每条属性都指向一个真实存在的测试。npm test 会校验那张表本身没腐烂。


已知限制

  • 对话太短时排序会退化:只有一两句时关键词不足,退回按修改时间,并在面板上说明
  • 中文分词是 n-gram 近似:没有引入分词库(那会带来依赖)。跨词边界的碎片已经按 「首尾是虚词」丢掉、并按原文区间合并还原成真词,但仍可能有孤立碎片 (比如「视频上」这种没有重叠伙伴的)出现在「按「xxx」排序」那行里 —— 匹配不上任何文档的碎片不参与打分
  • 语料太少时 IDF 作用有限:只有三五篇文档时,df 的取值范围被压扁, 排序更接近按命中次数排;文档越多越准
  • 右侧栏默认页会变成 guide:DSH 的规则是「guide 入口只有一个才直接开那一页」, 内置 Files 占了一个,所以展开右侧栏先看到 guide,需要点一下胶囊
  • 媒体只认常见格式与大小:图片 png/jpg/jpeg/gif/webp/avif/bmp/ico/svg、 视频 mp4/m4v/webm/mov/ogv;图片 ≤ 12MB、视频 ≤ 256MB,超出不列出
  • 媒体只按文件名参与相关性匹配:不解析画面 / 语音内容,截图与录屏建议用可检索的文件名
  • 右侧栏状态是 memory-only:刷新或新会话会回到收起状态

开发

git clone https://github.com/PolinniZhong/dsh-knit.git
cd dsh-knit

npm test          # 215 项,零依赖,不需要先 npm install

改动生效方式:宿主半边(src/host/)改了必须重启 DSH(实测不热加载); 客户端半边(src/client/)改了硬刷新浏览器即可。

没有构建步骤:客户端半边是手写的 window.__ModuleLoader__.load({...}), 用 React.createElement 而不是 JSX,所以不需要 tsdown / tsc。 静态资源也是内联的 —— 改图标要同时改 assets/ 源文件和 src/client/client.js 里的 KNIT_ICON_PATHtest/icon.test.mjs 会核对两者逐字一致。

knit/
├── package.json          # dsh.bundle.patch + dsh.client
├── cordis.patch.yml      # 挂进 plugin tree 的 insert 行
├── assets/               # 图标源文件(path 已内联进 client.js)
├── src/
│   ├── host/index.js     # /knit/api/recent · /doc · /raw
│   ├── host/relevance.js # 相关性引擎:BM25 + 关键词抽取(纯函数)
│   ├── host/tool.js      # agent 文档工具 knit_docs(手写 ToolDefinition)
│   └── client/client.js  # 双宿主注册 + 面板 UI
└── test/                 # 215 项测试
    └── eval/             # 离线质量评测:语料 + 用例 + 冻结的 v0.5.2 基线

细节和取舍写在源码注释里;贡献流程见 CONTRIBUTING.md, 版本变更见 CHANGELOG.md

版本策略:0.x 表示功能还在动,可能有破坏性变更。 1.0.0 留给「真实留存被验证之后」,不因为功能做完就发。

协议

MIT © Polinni

관련 플러그인