Repository navigation
Releases
v0.1.14 — 修复系统目录对话框不弹出
为什么系统对话框一直没弹出来
不是我猜的——直接在运行中的实例上打接口,拿到的是 502:
POST /path-lockbox/api/pick {"kind":"folders"}
-> 502 {"native":true,"error":"Cannot read properties of undefined (reading 'aborted')"}
native:true 说明宿主确实提供了原生能力,错在调用方:
// dsh-host-directory-picker-native
async function pickWin32Directory(signal) {
if (signal.aborted) throw new Error("native directory picker aborted"); // ← 第一行就用 signal0.1.9 我调用时写的是 capability.pick(),没传 AbortSignal → 立即抛错 → 前端把 5xx 当成"没有原生能力"→ 回退内置浏览器。所以每次看到的都是那个简陋弹窗。
修复
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 10 * 60 * 1000); // 兜底,abort 会关闭对话框
try {
const picked = await capability.pick(controller.signal); // ← 补上 signal
return json({ native: true, path: picked ? String(picked) : null, cancelled: !picked });
} finally {
clearTimeout(timer);
}同时修内置浏览器的「点了没反应」
- 旧逻辑:
pickOk里if (!pick.selPath) return;—— 双击进入目标文件夹后直接点「确定」,什么都不发生;
现在未选中条目时回退到当前路径。 - 增加一行提示:
双击进入文件夹,单击选中,点「确定」添加;未选中时添加当前路径。
验证(不弹真实对话框)
给 Cordis 测试夹具注入一个 mock directoryPicker,记录 pick 收到的参数:
POST api/pick (folders) -> 200 {"native":true,"path":"D:\\mock-picked","cancelled":false}
pick(signal) 记录 -> [{"isAbortSignal":true,"aborted":false}]
POST api/pick (files) -> 200 {"native":false,"reason":"directory-only"}
RESULT: PASS
node --test 27/27。
升级
重启后:文件夹 / 分区点「+」应弹出 Windows 系统文件夹选择框;文件仍走内置浏览器(原生能力是目录选择器)。
v0.1.13 — 选择器弹窗标题
选择器弹窗标题与类别对齐
| 原 | 现 |
|---|---|
| 选择文件 | 选择要锁定的文件 |
| 选择文件夹 | 选择要锁定的文件夹 |
| 选择硬盘 | 选择要锁定的分区 |
只改 KINDS[kind].pickTitle 这一处,弹窗标题、以及原生选择器不可用时的回退路径都会跟着变($("pickTitle").textContent = KINDS[kind].pickTitle)。
提示:文件夹 / 硬盘在正常环境下走系统对话框,这个弹窗只在宿主没有
directoryPicker能力时才出现;文件类始终走这个弹窗。
升级
"dsh-path-lockbox": "github:thson-xue/dsh-path-lockbox#v0.1.13"安装后重启 dsh。
验证
- 产物含三段标题(esbuild 转义后逐字匹配)
node --test27/27
v0.1.12 — 按钮精简(+ / 选择路径…)
按钮精简
| 原 | 现 |
|---|---|
| 添加… | +(32px 紧凑主按钮,蓝色) |
| 添加自定义 | 选择路径… |
随动同步:
- 弹窗标题
添加自定义→ 选择路径 - 空态提示
暂无条目,点「添加…」或「添加自定义」→暂无条目,点「+」或「选择路径…」
行为不变:
- + 对文件夹 / 硬盘走系统原生目录对话框,文件类走内置浏览器;
- 选择路径… 仍是手动输入绝对路径的弹窗(
customHint提示文件/目录/盘符写法)。
升级
"dsh-path-lockbox": "github:thson-xue/dsh-path-lockbox#v0.1.12"安装后重启 dsh。
验证
- 产物含
data-add title="添加">+<、选择路径…、新空态文案与.btn.plus样式 node --test27/27
v0.1.11 — 类别改名与说明文字
三个类别改名并加说明
| 原 | 现 | 说明(浅灰小字) |
|---|---|---|
| 文件 | 锁定文件 | 支持所有文件类型 |
| 文件夹 | 锁定文件夹 | 打开要锁定的文件夹 |
| 硬盘 | 锁定分区 | 锁定的硬盘分区 |
实现:卡片头部新增 <span class="card-desc">,样式 color: var(--lb-fg3); font-size: 12px;
(跟随主题的第三级前景色,浅灰);标题保持 font-weight: 500。计数仍在最右(margin-left: auto)。
升级
"dsh-path-lockbox": "github:thson-xue/dsh-path-lockbox#v0.1.11"安装后重启 dsh(页面由宿主路由加载,重启后刷新)。
验证
- 构建产物含三段文案(esbuild 以
\uXXXX转义,逐字匹配通过)与.card-desc样式 node --test27/27
v0.1.10 — 锁图标
插件图标改为「锁」
插件列表 / 详情页的图标来自包清单的 icon 字段(宿主 plugin-inventory 会把它转成
base64 data URL)。dsh-better-sidebar、dsh-codex-ui、dsh-ops 都是这么声明的。
本次:
- 新增
assets/lock.svg:36×36 描边挂锁,与内置图标的 36px 网格一致 package.json增加"icon": "./assets/lock.svg"- 版本 0.1.10
一个限制要说明
设置页左侧导航的图标改不了。 dsh-client-ui-settings 里没有任何 icon 逻辑,
导航字形是按栏目由宿主固定的;settings.section 注册项也没有 icon 字段可传。
要改得改 DSH 本身(让设置栏目的导航读取注册项里的图标)。
所以本次改到的是插件自身的图标(插件列表、插件详情页标题旁那个)。
升级
"dsh-path-lockbox": "github:thson-xue/dsh-path-lockbox#v0.1.10"安装后重启 dsh。图标由宿主在启动时从清单读取,不重启不会刷新。
验证
assets/lock.svg可解析(36×36,viewBox="0 0 36 36")npm pack产物包含package/assets/lock.svgnode --test27/27
v0.1.9 — 系统原生目录对话框
文件夹 / 硬盘改用系统原生目录对话框
之前「添加…」只弹插件内置的浏览器。现在目录类直接调用操作系统的原生选择器:
ctx.directoryPicker.capability() -> { kind: "native", pick(signal) }
Windows 上是 IFileOpenDialog + FOS_PICKFOLDERS(宿主自带实现,走独立子进程 + Win32 绑定)。
改动
- 宿主新增
POST /path-lockbox/api/pick- 用
ctx.get("directoryPicker")取服务(不写进inject,宿主没有该服务时插件照常激活) - 要求
capability.kind === "native";用 busy 标记串行化,避免同时弹两个对话框 - 返回
{ native, path }/{ native, cancelled }/{ native: false } - 文件类直接返回
{ native: false, reason: "directory-only" }:native 能力是目录选择器,选不了文件
- 用
- 前端
src/ui.js- 文件夹 / 硬盘的「添加…」先请求该接口,拿到路径直接入库
- 宿主没有该能力或调用失败 → 自动回退到原内置浏览器
- 文件类保持内置浏览器
- 入库逻辑抽成
addPicked(),两条路径的保存/刷新行为一致
验证
node --test # 27/27
Cordis 最小上下文:注册路由由 5 条变为 6 条,POST /api/pick 在无 picker 的宿主上返回
{"native":false} —— 正是前端判断回退的信号。
升级
"dsh-path-lockbox": "github:thson-xue/dsh-path-lockbox#v0.1.9"安装后重启 dsh。之后在「设置 → 文件锁」里点文件夹或硬盘卡片的「添加…」,弹出的就是
系统目录选择框;文件类仍是内置浏览器。
版本脉络
| 版本 | 内容 |
|---|---|
| 0.1.3 | 宿主激活、cfg() 用 apply 实参、mountRoute 适配 webServer.register |
| 0.1.4 | client 半身改用 ctx.slots |
| 0.1.5 | 声明 inject = ["slots"];UI 注册失败可降级 |
| 0.1.6 | Config schema(volatile)→ 自动设置项 |
| 0.1.7 | settings.section → 设置里显示「文件锁」 |
| 0.1.8 | 去掉 .volatile() → 只保留「文件锁」一个入口 |
| 0.1.9 | 文件夹/硬盘改用系统原生目录对话框,失败回退内置浏览器 |
v0.1.8 — 只保留「文件锁」一个设置入口
只保留「文件锁」一个设置入口
0.1.6 把整个 Config 标了 .volatile(),于是 dsh-settings 会自动生成一个以
命名空间 id(path-lockbox)为标题的配置表单。0.1.7 又加了正式的 settings.section
栏目(中文名「文件锁」),两者并存就成了两个入口。
本次去掉 .volatile():
- export const Config = z.object({ ... }).volatile();
+ export const Config = z.object({ ... });volatileForm(schema)只认 volatile 字段 → 自动表单不再生成- schema 仍保留,用于校验与默认值(
apply()拿到的config恢复为普通对象) readConfig()仍兼容 volatile 引用(typeof config.get === "function"),防止以后改回来tests/host.test.js新增断言:Config必须导出、带toJSON、且刻意非 volatile,
防止重复入口悄悄回来
验证
node --test # 27/27 通过
产物:lib/index.js 已不含 .volatile();lib/client.js 仍是
settings.section + label: () => "文件锁" + inject = ["slots"]。
升级
"dsh-path-lockbox": "github:thson-xue/dsh-path-lockbox#v0.1.8"安装后重启 dsh:设置里只有 文件锁 一个入口,点开即插件自带界面。
版本脉络
| 版本 | 内容 |
|---|---|
| 0.1.3 | 宿主激活、cfg() 用 apply 实参、mountRoute 适配 webServer.register |
| 0.1.4 | client 半身改用 ctx.slots |
| 0.1.5 | 声明 inject = ["slots"];UI 注册失败可降级 |
| 0.1.6 | Config schema(volatile)→ 自动设置项 |
| 0.1.7 | settings.section → 设置里显示「文件锁」,栏目内嵌自带界面 |
| 0.1.8 | 去掉 .volatile() → 只保留「文件锁」一个入口 |
v0.1.7 — 设置里显示「文件锁」
「设置」里出现中文名 文件锁
设置页左侧导航由 settings.section 槽构建,每个条目提供 id / order / label。
之前只有自动生成的配置表单(标题是原始命名空间 path-lockbox),现在额外注册了一个
正式栏目:
ctx.slots.inject("settings.section", () =>
ctx.slots.register(
{ name: "settings.section", id: "path-lockbox", order: 50, label: () => "文件锁" },
PathLockboxSection, // 直接嵌入 /path-lockbox/ui
),
);label是函数,宿主每次渲染重新取值,所以显示就是 文件锁- 栏目内容复用插件自带界面(iframe →
/path-lockbox/ui),不是自动生成的表单 plugins.detail.section(插件详情页配置区)保留,两处入口并存dsh.client.inject补上@deepseek-ai/dsh-client-ui-settings(它声明了settings.section)- 两个槽各自独立 try/catch:任一槽缺失只跳过该入口,绝不让 entry 激活失败
(那是启动级故障)
验证
node --test # 26/26 通过(新增 3 项:栏目注册、中文 label、共用界面)
产物检查:含 settings.section、plugins.detail.section、exports.inject=["slots"]、
无顶层 export。
升级
"dsh-path-lockbox": "github:thson-xue/dsh-path-lockbox#v0.1.7"安装后重启 dsh,「设置」里会出现 文件锁 一节。
版本脉络
| 版本 | 内容 |
|---|---|
| 0.1.3 | 宿主激活、cfg() 用 apply 实参、mountRoute 适配 webServer.register |
| 0.1.4 | client 半身改用 ctx.slots |
| 0.1.5 | 声明 inject = ["slots"];UI 注册失败可降级 |
| 0.1.6 | Config schema(volatile)→ 自动设置项 |
| 0.1.7 | settings.section → 设置里显示中文名「文件锁」,栏目内嵌自带界面 |
v0.1.6 — 出现在「设置」页(volatile Config)
现在会出现在「设置」页
宿主的 dsh-settings 会遍历所有已激活的配置项:只要插件的 runtime.Config
带 toJSON(),且 volatileForm() 能从中切出 volatile 字段,就自动生成一个设置项。
dsh-plugin-marketplace 用的正是这套机制(z.object({...}).volatile())。
本次改动
- 新增
Configschema:files/folders/drives(每项path+label+
recursive+watch)、matchCase、guidance、extraPathArgs、extraCommandArgs,
整体.volatile() - 新增依赖
@deepseek-ai/schemastery(构建时打包进lib/index.js;宿主按鸭子类型
'toJSON' in schema识别,不依赖模块实例同一性) - 标了
.volatile()后apply(ctx, config)收到的是可读取引用,故新增
readConfig()解包config.get()(普通对象仍然直读,单测不受影响)
验证
node --test23/23Config.toJSON()经new schemastery(...)往返后meta.volatile保留,7 个字段可切出- 带 schema 激活:5 条路由照常注册,
/path-lockbox/ui200、/api/config200、
DELETE405
规则优先级(重要)
$DSH_HOME\path-lockbox.json ← 插件自带 UI 写入
↓ 文件不存在时才回落到
profile config ← 设置页编辑的就是它
两者同时可用,但自带 UI 优先:一旦用自带 UI 保存过规则,设置页的列表就不生效
(删掉那个 JSON 后会重新以设置为准)。需要反过来(设置优先)说一声,改一行即可。
升级
"dsh-path-lockbox": "github:thson-xue/dsh-path-lockbox#v0.1.6"安装后重启 dsh,在「设置」里应能看到 path-lockbox 一节。
版本脉络
| 版本 | 修复/功能 |
|---|---|
| 0.1.3 | 宿主激活(inject)、cfg() 用 apply 实参、mountRoute 适配 webServer.register |
| 0.1.4 | client 半身改用 ctx.slots;补齐 dsh.client.inject;react external |
| 0.1.5 | 声明 inject = ["slots"];UI 注册失败可降级,不再影响启动 |
| 0.1.6 | Config schema(volatile)→ 出现在「设置」页 |
v0.1.5 — 修复启动崩溃(声明 slots 服务)
修复 0.1.4 引入的启动崩溃
0.1.4 让设置界面能注册了,但直接读 ctx.slots 却没有声明它。Cordis 的注入守卫会拒绝该属性访问:
cannot get property "slots" without inject
这个 client entry 因此激活失败,而宿主把「任一条目未激活」判定为整次启动失败:
web boot: 1 entry did not activate
dsh-path-lockbox: failed
修法
- 声明服务依赖:
export const inject = ["slots"],与dsh-ops的
exports.inject = ['slots', 'locale', 'remote']同形。客户端插件和宿主插件一样,
只有声明过的服务才能用ctx.<name>读取。 - 插件绝不能拖垮外壳:
require("react")、ctx.slots读取、槽注册全部收进
try/catch。UI 注册失败时只退化为「直接访问/path-lockbox/ui」,
不能再让整个应用起不来。 tests/client.test.js增加inject断言 —— 漏掉它是启动级回归。
验证
node --test # 23/23 通过
构建产物:var inject = ["slots"]、plugins.detail.section 注册、无顶层 export、
require("react") 处于 try 内。
升级
"dsh-path-lockbox": "github:thson-xue/dsh-path-lockbox#v0.1.5"安装后重启 dsh。若界面仍未出现,说明 slots 服务在该宿主未就绪,插件会安全退化为
URL 直达(/path-lockbox/ui),不会再影响启动。
版本脉络
| 版本 | 修复 |
|---|---|
| 0.1.3 | 宿主激活(inject 去掉 connection)、cfg() 用 apply(ctx, config)、mountRoute 适配 webServer.register |
| 0.1.4 | client 半身改用 ctx.slots;补齐 dsh.client.inject;react external;客户端测试 |
| 0.1.5 | 声明 inject = ["slots"];全部 UI 注册失败可降级,不再影响启动 |