dsh-local-library
fucaplos/dsh-local-library
本地资料库(仅 Windows,需用随包的 PowerShell 脚本安装):文件传一次,之后每个对话都能用四个工具(library_list / library_search / library_read / library_save)检索、读取;上传、下载与打包下载在设置页完成。文件只保存在本机——除非你启用可选的视觉模型,它会把图片发到你配置的接口。
安装
dsh plugin --profile web add github:fucaplos/dsh-local-libraryREADME
dsh-local-library
中文 · English
English — A local document library for DSH Desktop: a file cabinet shared across all conversations. Files stay on your machine (nothing is uploaded); every new chat can search, read and download them through four tools (
library_list,library_search,library_read,library_save), and a Local Library section shows up in Settings. PDF / Word / Excel / PowerPoint / images are extracted into searchable text. MIT licensed. → Full English README
给 DSH Desktop 用的本地资料库插件:一个跨会话共享的文件柜。
- 设置左栏多一个「本地资料库」分区:拖拽上传(含整个文件夹)、检索、按类型过滤、下载、批量打包下载、改标签、删除、重建索引。
- 每个新对话天然知道它存在:插件往系统提示词里加一段说明,并注册 4 个工具(
library_list/library_search/library_read/library_save)。用户不必再说「我有一份资料」,模型自己会先来查。 - 重复检测:同样的内容再传一次会问「覆盖 / 再存一份 / 跳过」,不会悄悄塞进两份。
- 文件只在本机:存在
$DSH_HOME\local-library(默认%APPDATA%\dsh-desktop\harness\local-library),不上传任何云端。 - 可检索正文:
.md/.txt/.csv/.json/...、.docx、.xlsx、.pptx、.pdf会被抽成纯文本;PDF 优先用本机 PyMuPDF(能拿中文),失败回退到零依赖的流级抽取;配了视觉模型的话,图片会被描述成文字后一起参与检索。
装好之后长什么样
设置(左下角齿轮)→ 左栏「本地资料库」:
- 上方是拖拽区(可拖文件夹,按顶层文件夹名记一笔),带上传进度;
- 中间是搜索框 + 类型过滤 chips(全部 / PDF / Word / 表格 / PPT / 图片 / 文本),搜索支持
kind:、tag:、after:、before:这种过滤语法; - 列表每行:勾选框(多选后「打包下载」成 zip)、文件名与元信息、标签(点「标签」行内编辑,逗号分隔)、命中片段、下载、删除(点两次确认);
- 底部:文件数 / 占用 / 已建索引数 / 重建索引 / 存储路径(可复制)。
对话里可以直接说「看看我资料库里关于 XX 的东西」,模型就会调 library_search;也可以什么都不说——只要请求看起来要用到你自己的材料,它会先来查一眼(说明文字是插件写进系统提示词的,每个新会话都会带上)。
安装
前提:Windows(靠 junction 让包名可解析,不需要管理员权限)、DSH Desktop、PowerShell 7。
从仓库拉到本机,跑一条命令:
git clone https://github.com/Fucaplos/dsh-local-library.git
cd dsh-local-library
pwsh -File install.ps1
装完重启 DSH Desktop(宿主半侧在启动时加载),再刷新页面 F5(客户端半侧由启动清单投递)。设置(左下角齿轮)→ 左栏应出现「本地资料库」。
脚本是幂等的:已经装过就只做自检,不会重复写配置。改配置前会先备份成 cordis.patch.yml.bak-dsh-local-library-<时间戳>。
如果 DSH 的 profile 不在默认位置(自定义了 DSH_HOME、或多 profile),显式传路径:
pwsh -File install.ps1 -ProfileRoot 'D:\my-dsh\harness\profiles\web'
卸载(数据默认保留,加 -PurgeData 才连文件一起删):
pwsh -File uninstall.ps1
安装做了什么(也说明了为什么这么装):
- 在
profiles\<name>\node_modules\dsh-local-library建一个 junction 指向本目录。DSH 靠包名解析插件,junction 让包名成立,同时代码留在工作区、改完重启即生效。 - 在
profiles\<name>\cordis.patch.yml已有的- insert:列表末尾加两行:- id: dsh-local-library name: dsh-local-libraryid必须全局唯一且与包名一致;id: 值行尾绝不能写注释(商城预检会判错)。 - 不碰
package.json/pnpm-lock.yaml/ generation 投影。走 pnpm 会被.desktop-bin\pnpm.cmd的 pnpm-runner 干扰,容易把 generation 投影还原成旧状态。 - 最后做一次包名解析自检:
dsh-local-library与dsh-local-library/client都要能解析到,否则宿主半侧或客户端半侧会缺一半。
结构
| 文件 | 作用 |
|---|---|
lib/index.js | 宿主半侧:HTTP 路由、4 个工具、系统提示词段、索引流水线 |
lib/client.js | 客户端半侧:设置分区 UI(React,window.__ModuleLoader__ 载入) |
lib/store.js | 存储与索引:index.json 原子写、中文 bigram 检索打分、正文 LRU 缓存、sha256 去重、分段读 |
lib/extract.js | 零依赖文本抽取:docx/xlsx/pptx 用自研 ZIP 读取器,pdf 走 FlateDecode |
lib/pdftext.js | 可选:调本机 PyMuPDF 抽 PDF(中文更稳),探测不到就跳过 |
lib/vision.js | 可选:图片转文字描述(OpenAI 兼容 chat/completions,默认智谱 glm-4v-flash) |
lib/zip.js | 打包下载用的流式 ZIP 写入器(stored,UTF-8 文件名) |
cordis.patch.yml | bundle 清单(insert 一节) |
install.ps1 / uninstall.ps1 | 装/卸到 DSH profile(建 junction + 登记 patch 行) |
test/*.mjs | 六套自检,见下 |
LICENSE | MIT |
HTTP 接口(同源,浏览器直接调)
前缀 /dsh-local-library,无需鉴权(在鉴权层之外):
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /list?q=&limit= | {items, stats, pending};带 q 时按检索打分返回并附 snippet;limit 默认 200、上限 1000 |
| GET | /stats | {root, count, bytes, bytesText, indexed} |
| GET | /config | {matchMode};严格匹配开关的持久化位置 |
| PUT | /config | body {matchMode:"loose"|"strict"},非法值回 400 |
| POST | /upload?name=&dup=&tags= | body 为原始字节,流式落盘;dup = reject(默认)/keep/replace;同内容重复返回 409 带 duplicate |
| PATCH | /item?id= | body {name?, tags?},改文件名或标签 |
| POST | /bundle | body {ids:[]},返回 zip(最多 300 个) |
| GET | /download?id= | 原文件回流,content-disposition 带 RFC5987 文件名 |
| DELETE | /item?id= | 删除条目与原文件 |
| POST | /reindex?all=1 | 重跑索引;不传 all 只补未建索引的 |
工具(进系统提示词,模型每轮可见)
| 工具 | 参数 | 用途 |
|---|---|---|
library_list | limit? | 列出全部 |
library_search | query、limit?、kind?、tag?、strict? | 检索文件名+正文+标签,返回片段与 matchedBy(命中来源);kind/tag 是快捷过滤;strict 走严格匹配(不传则用设置页的开关) |
library_read | id 或 name、maxChars?、offset? | 读正文,长文可分段续读(返回 nextOffset) |
library_save | name、content、tags? | 把结论/笔记存进库 |
可选配置
在 profile 的 cordis.patch.yml 里给这个条目加 config(不配就是关):
- id: dsh-local-library
name: dsh-local-library
config:
vision:
enabled: true
apiKey: <你的 key>
model: glm-4v-flash # 默认
endpoint: https://open.bigmodel.cn/api/paas/v4/chat/completions # 默认
开了之后上传的图片会被描述成一段文字存进索引,library_search 就能按图片内容找。
严格匹配(设置页开关)
中文是按两字组合(bigram)切词的,所以「不存在的词zzz」这种查询也会因为「不存 / 存在 / 在的」在别的文档里出现过而返回几条擦边结果。设置页检索框右侧的严格匹配开关可以收紧:
- 关(默认,宽松):按打分排序返回,结果多但可能有擦边。
- 开(严格):要求查询词的命中率 ≥ 60%,且查询里的英文/数字必须真的出现;整句原样出现的一律保留。
开关状态存在 本地资料库\settings.json(只有一个字段):
{ "matchMode": "strict" }
对应接口:GET /config 读、PUT /config 写(body {"matchMode":"strict"|"loose"},非法值回 400)。模型侧也可以单次覆盖:library_search({ query, strict: true })。
自检
$env:ELECTRON_NO_ASAR='1'
$node = "$env:APPDATA\dsh-desktop\harness\.desktop-bin\node.cmd"
& $node test\client.smoke.mjs # 客户端:载入、注册分区、SSR 渲染(含列表/多选/标签/重复提示)
& $node test\plugin.smoke.mjs # 宿主:上传→检索→分段读→标签→打包→删除→工具→提示词段
& $node test\bugfix.test.mjs # 回归:审计出的每个 bug 一条断言(28 条:文件名 %、offset 越界、空文件打包、标签精确匹配、非法日期提示、init 重试…)
& $node test\navicon.test.mjs # 左栏图标的假 DOM 测试(弹窗未开不查、防抖、disposer)
& $node test\cordis.integration.mjs # 真 Cordis 容器里加载插件,断言注册物
& $node test\extract.test.mjs # 抽取器 18 条
已知限制
- PDF 抽取两条路:PyMuPDF(准,能出中文,但要本机有 bundled python)→ 回退流级抽取(只适合 ASCII/标准编码,中文子集 CID 字体可能乱码)。扫描件没有文本层。
- 图片搜索依赖视觉模型(
config.vision),没配就只能按文件名/标签找。 - ZIP 读取器不支持 ZIP64、加密 ZIP,不校验 CRC;单个 zip 与单条目上限 128 MiB。
- 文本类文件索引上限 4 MiB(超出截断,检索会标明
truncated)。 - xlsx 不还原日期序列号、公式结果与合并单元格。
- 检索是关键词打分(中文 bigram + 拉丁词,文件名/标签权重高),不是向量检索。
- 拖文件夹上传是平铺的(不建子目录),顶层文件夹名会写成标签,方便按来源筛。
tag:过滤是精确匹配整个标签(0.4.1 起):tag:财务不再连「财务部」一起筛出来;要匹配「财务部」就写tag:财务部。after:/before:只吃能被Date.parse认出来的日期(after:2026-10-01)。写错(after:2026-13-99)时这个过滤条件会被忽略,但GET /list、library_search与设置页都会明确提示「已忽略」,不会静默失效。- ComfyUI 工作流 JSON(
.json,>1.5KB)只留模型名、提示词、采样参数、画布尺寸、节点标题与类型,节点坐标/控件值不进正文——所以按坐标或参数数字搜不到。其他 JSON 原样入正文。 - 这个前缀路由在 DSH 鉴权层之外(设置页要能直接 fetch),所以凡是能访问 127.0.0.1:端口 的本机进程都能读写整个库。库是本机私有数据,不要放密钥。
更新记录
0.4.1(把审计剩下的 8 条小毛病全修了)
- 修:上传没有超时,网络卡住时
XHR永远不 settle、队列项一直显示「上传中」。现在 60 秒没有进度就自动放弃并提示。 - 修:上传进度每来一个
progress事件就刷一次 state,512MB 文件能把页面拖卡;改成按时间节流(150ms)。 - 修:标签编辑按 Esc 取消后,输入框卸载触发的 blur 又把旧值存了回去。现在 Esc 是真正的取消。
- 修:
tag:用包含匹配,tag:财务会把「财务部」也算上(见「已知限制」)。现在是精确匹配。 - 修:
after:/before:日期写错时静默失效;现在会把「已忽略这个过滤条件」回给调用方和页面。 - 修:
init()失败一次就被记忆成 rejected,整个进程生命周期里资料库都不可用;现在会重试。 - 修:
library_read的offset落在 emoji 代理对中间时,正文从半个字符开始;现在自动对齐到下一个完整字符。 - 补:
PATCH /item改名时ext/mime早就会跟着变,这次补上回归断言(BUG10)防止以后退化。
0.4.0(严格匹配开关)
- 新:设置页检索框右侧加「严格匹配」开关。之前搜「不存在的词zzz」也会返回几条——中文按 bigram 切词,「不存 / 存在 / 在的 / 的词」分别在别的文档里出现过,很难靠打分避免。现在可以一键收紧。
- 新:严格规则——查询词命中率 ≥ 60%,且查询里的英文/数字必须真的出现,整句原样出现的一律保留。
- 新:开关状态存在
本地资料库\settings.json({"matchMode":"loose"}),接口GET /config/PUT /config;library_search新增strict参数,可以只对一次检索生效。
0.3.1(结构化抽取 + 命中来源)
- 新:ComfyUI 工作流 JSON 做结构化抽取,只留「模型 / 提示词 / 采样 / 画布 / 节点标题与类型」。实测 214KB 的工作流从 152,663 字压到 2,187 字,模型名和提示词照样能搜到。
- 新:
library_search/GET /list?q=返回matchedBy(body/name/both),只命中文件名或标签时,工具里会写「按文件名/标签命中」,设置页列表也会显示这一行,不再出现「命中但没片段」的空结果。
0.3(bug 审计轮)
- 修:文件名含
%时上传 500(URLSearchParams已解码又decodeURIComponent一次)。 - 修:
library_save传空白文件名会静默存成「未命名」。 - 修:
GET /list忽略limit,且把内部sha256吐给前端。 - 修:
library_read的offset越过末尾时返回越界提示当正文。 - 修:只选 0 字节文件打包时 404。
- 修:
reindex/批量上传时无并发上限(改成 2 路队列)。 - 修:改名后
ext/mime不更新,下载的 Content-Type 是旧的。 - 修:分段读与文件名截断会切断 emoji 代理对。
- 修:客户端下载中断时打包写入可能永远等
drain。 - 修:客户端编辑某行标签时其它行标签全部消失。
- 修:「文件太大已跳过」的提示被「已上传 N 个」覆盖。
- 修:选中超过 300 个时静默少打包,现在会说明。
- 修:
installNavIcon的 MutationObserver 在聊天流式输出时高频跑全局选择器(改 250ms 防抖 + 只在设置弹窗内查)。
许可证
MIT © 2026 Fucaplos
随便用、改、卖,保留版权声明和许可文本即可;软件按「原样」提供,作者不担责。