Bundle
dsh-knit
Find the doc your agent just wrote. Lists the DSH workspace's Markdown, images and video in the sidebar, ranked by relevance to the current conversation, with inline preview — and hands the same ranking to the agent as a tool, so it reads one doc instead of five. Zero model calls, zero network.
- Source
- PolinniZhong
- stars
- 1 stars
- License
- MIT
- Updated
- Updated 15 hours ago
Readme
# Knit
> Agent 一天产出 20 篇文档,你找不到刚才那篇。
> Knit 把它们放到对话旁边 —— 你正在聊什么,相关的那篇就在最上面。
>
> **你的 agent 也一样。** 同一份排序也给它当工具用 —— 它读一篇,而不是翻五篇。

*真机截图(不是原型):一个含 16 篇文档的工作区,列表 + 就地预览。*
*顶行会跟着你正在聊什么变;对话内容还不足时它退回按时间排,并如实说明依据。*
扫整个项目文件夹的 Markdown / 图片 / 视频 · 排序跟着对话走 · 不调模型、不联网
```sh
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 次就要(`chokidar`、`mtime` 这种精确词)
- **中文 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 |
| **BM25** | **95.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` 的取值范围太窄,动态范围被压扁。
文档越多这个排序越准 —— 这正是它该有的样子
- **它只能排「和对话有共同词汇」的文档**:如果一个词都没命中,所有文档同分,
名次就退化成按时间排
---
## 安装
```sh
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 按需取字节,响应带 `nosniff` 与 `default-src 'none'; sandbox`
面板里那份「相关度」只影响排序,**不显示也不外传**。
> 上面每一条都有自动化检查守着,逐条列在 **[SECURITY.md](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**:刷新或新会话会回到收起状态
---
## 开发
```sh
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_PATH`,`test/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](CONTRIBUTING.md),
版本变更见 [CHANGELOG.md](CHANGELOG.md)。
> 版本策略:`0.x` 表示功能还在动,可能有破坏性变更。
> `1.0.0` 留给「真实留存被验证之后」,不因为功能做完就发。
## 协议
[MIT](LICENSE) © Polinni
Install
dsh plugin --profile web add github:PolinniZhong/dsh-knit
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install dsh-knit from the hub
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.