dsh-plugin-library
v0.1.3
Published
本地文件资料库:内容寻址的不可变修订、派生的可检索正文、文件夹与模型工具。
Readme
dsh-plugin-library
M5:把 WorkDSH 的本地资料库复刻成 DSH 插件。
原生 DSH 没有"带修订的本地资料"这一层:文件要么在工作区里被就地改写,要么只是附件。
本包把导入的每个文件变成一个 资产(asset),每次导入产生一个 不可变修订(revision):
原始字节内容寻址存放、永不修改;检索读的是派生的纯文本;任务与项目只引用
assetId + revisionId,不引用可变路径。
安装
前置条件:DSH 0.2.0-rc.2+。
- npm 包:在 DSH 的插件市场/插件管理里安装
dsh-plugin-library,或在 profile 的package.json的dependencies里加"dsh-plugin-library": "^0.1.0" - tarball:
dependencies里写"dsh-plugin-library": "file:./dsh-plugin-library-0.1.0.tgz",然后重启 DSH 触发安装
安装后完全退出并重启 DSH(Host 半边只在启动时加载)。验证:重启后插件列表里本包显示「已启用」,且对应面板/工具可用。
做什么
- 从绝对路径导入(Host 读文件,走
ctx.fs)。同一目录下同名导入 = 给该资产追加新修订。 - 不可变修订:修订 id = 原始字节的 SHA-256。改动后重新导入产生新修订,旧修订仍可读。
- 内容寻址去重:字节完全相同 ⇒ 复用已有修订(同一 id、同一编号、不再落一份盘)。
- 目录树:新建文件夹、重命名、移动、移除。移除是软移除 —— 只标记、不删字节。
- 检索:跨派生正文搜索,返回资产、目录、类型、修订号与转换状态。
- 读取:读派生正文(
read-text)与读原始字节(read-original,base64)。 - 启停:停用后读取与检索立刻拒绝(历史引用也不再解析),重新启用即恢复。
- 限额:单文件 50 MiB、原件总量 5 GiB,越界给出明确拒绝码。
- 模型工具:
library_search、library_read,与面板走同一套服务方法。 - 面板:
main(keydsh-plugin-library)+sidebar.panellist(order 46,资料库)。 布局照 WorkDSH 上游(LibraryPanel.tsx111–122 行)复刻为双栏应用壳: 左侧wd-library-sidebar(标题、搜索/最近/本地产物三行导航、我的资料 +、目录树、 页脚),右侧wd-library-content按view切换——选中即文档视图(面包屑 + 编辑/下载/•••),搜索视图带表单与「资料类型/资料来源/更新日期」筛选行, 最近/本地产物是裸列表,未选中时是欢迎页。导入按绝对路径进行(本 Host 没有 浏览器文件选择框),上游的<input type="file">槽位保留在样式表中但不再渲染。
明确不做(M5 范围外)
- PDF / DOCX / PPTX / XLSX 的二进制预览渲染器及其重依赖
(
pdfjs-dist、jszip、docx-preview、pptx-viewer)——不装。 - 右侧栏预览页签(
sidebar.right.pane.tab)。 - 输入框
@引用(inputTriggers/conversation.input.left)。
因此只对文本类格式做正文转换:.md / .markdown / .txt / .html / .htm /
.json / .csv / .log。HTML 用去标签得到正文。其余扩展名一律按原始字节保存,
转换状态为 unavailable("无文本转换"),仍可下载、仍可按文件名搜到。
磁盘布局
根目录:$DSH_HOME/dsh-plugin-library(DSH_HOME 未设置时退回 ~/.dsh)。
objects/<assetId>/<revisionId>/original.<ext> 原始字节,精确副本,mode 0600
objects/<assetId>/<revisionId>/content.md 派生检索文本(无转换时为空)
objects/<assetId>/<revisionId>/revision.json 小型元数据记录
.tmp/<uuid>/ 写入暂存区,成功后 rename 就位revisionId就是sha256(原始字节)的小写十六进制。- 每个修订目录先写进
.tmp/<uuid>,再rename到最终位置:崩溃不会留下"看起来权威"的半成品。 apply()不建目录、不写文件(docs/PORTING-NOTES.md§10);目录在第一次导入时出现。- 读导入来源走
ctx.fs;写自有对象存储用node:fs(ctx.fs没有二进制写), 每一条路径都过safePath(),越出根目录即library/path-escape。
领域数据
| 域 | 表 | 键 | 内容 |
| --- | --- | --- | --- |
| dsh_skills_library | states | local | nodes(文件夹 / 资产节点)、assets(资产元数据)、revisions(修订记录,键 ${assetId}:${sha256}) |
三个集合都是宽松 record:DTO 校验放在 manager 里,这样拒绝能指名到字段,而严格 schema
会让每次加字段都变成迁移。WorkDSH 的键是 organizationId_principalId;当前是个人 profile,
键用常量 local,多租户这一维不存在,不是假的。
注意:revisionId 是内容哈希,所以不同资产导入相同字节时 id 相同、记录各自独立
(对象目录含 assetId,因此各有自己的原件目录)。引用永远是 assetId + revisionId 的组合,
不会歧义。
路由端点
POST /api/dsh-skills/library,信封 { endpoint, payload } → { ok, value | error }:
overview、tree、asset、create-folder、import、rename、move、remove、
set-enabled、search、read-text、read-original。
模型工具
| 工具 | 参数 | 行为 |
| --- | --- | --- |
| library_search | query(必填)、kind | 搜索派生正文,返回 asset_id / revision_id / 目录 / 类型 / 修订号 / 转换状态 / 摘要 |
| library_read | asset_id(必填)、revision_id、offset、limit(1–20000) | 读某个修订的派生正文,可分页 |
两者都调用与面板相同的 LibraryManager 方法,所以停用、移除、限额对两条路径同时生效。
defineTool 来自安装内的 @deepseek-ai/dsh-tools(经 importDshPackage();
见 docs/PORTING-NOTES.md §9)。离线跑不了这个 specifier,src/tools.js 里有
一个只覆盖本包所用 schema 子集的本地编译器作兜底(见下"没验到的")。
错误码
| 码 | 含义 |
| --- | --- |
| library/invalid-request | 缺字段或字段类型不对 |
| library/invalid-name | 名称非法(空 / 超长 / 含路径分隔符) |
| library/not-found | 资产、修订或文件夹不存在(含来源文件不存在、已移除) |
| library/not-a-file | 导入来源不是普通文件 |
| library/not-an-asset / library/not-folder | 节点类型不符 |
| library/name-conflict | 同目录已有同名项目(大小写不敏感) |
| library/cycle | 文件夹移到自己的子目录 |
| library/file-size | 单文件超过 50 MiB(或读来源超过上限) |
| library/quota-exceeded | 原件总量将超过 5 GiB |
| library/disabled | 资产已停用,读取 / 检索被阻断 |
| library/conversion-unavailable | 该修订没有可读的派生正文(unavailable / failed) |
| library/path-escape | 记录里的对象路径越出资料库根目录 |
| library/fs-unavailable | ctx.fs 不可用 |
| library/read-denied / library/read-failed / library/aborted | 来源读取被沙箱拒绝 / 失败 / 取消 |
| library/not-ready | 存储域尚未就绪 |
| library/invalid-page | library_read 的 offset / limit 非法 |
验证
npm run gate
node --test "tests/library.test.mjs"15 条测试走真实 Host 入口(apply + 注册出来的路由 + 注册出来的工具):
导入与派生正文、不可变修订(改字节出新修订且旧修订仍可读)、同字节去重、
目录新建 / 重命名 / 移动 / 环检测、检索命中、停用即刻阻断读取与检索、软移除保留字节、
单文件限额、总量配额、路径逃逸拒绝、来源缺失 / 目录拒绝、工具注册与执行、
未知端点信封、apply 不落盘。
没验到的
- 面板在真实页面上的渲染与浅深色:
cordis_inspect_query的 ClientSlots查询本次 超时(页面未响应),所以main/sidebar.panellist的注册只按docs/PORTING-NOTES.md§5 的实测契约与同仓 probe/connectors/projects 的既有写法对齐, 没有在运行中的页面上核对。 - 官方
defineTool的编译路径:离线无法解析@deepseek-ai/dsh-tools,测试跑的是src/tools.js的本地兜底编译器。真实安装里走官方defineTool,但这条路径没有被离线测试覆盖。 ctx.fs的真实实现:离线用的是一个node:fs替身;真实后端对路径解析、沙箱可见性 与FsError码的处理没有实测(本包只映射了FS_NOT_FOUND/FS_NOT_REGULAR_FILE/FS_TOO_LARGE/FS_SANDBOX_DENIED/FS_ABORTED/FS_IO_ERROR)。- 默认限额本身:测试用
maxBytes: 64、maxTotalBytes: 24验证拒绝逻辑, 50 MiB / 5 GiB 这两个默认值没有真的导入过一个越界文件。 ctx.storageDomain的真实后端:离线是内存替身。领域 schema 是宽松 record, 真实 zod 校验是否接受本包的记录形状没有实测。- 软移除的恢复:只做了"移除后隐藏且读不到",没有做撤销 / 回收站界面。
revision.json的下游消费者:文件写了,但没有第二个读者。- 跨资产同字节去重:同一字节导入到两个不同资产时按设计各存一份原件
(对象路径含
assetId),磁盘上不共享;测试没有覆盖这个取舍。 sidebar.panellist图标点击是否真的切到本面板:面板 key 与入口 id 一致, 但没有真机点击验证。
