本文基于 DSH
0.2.1-rc.5,插件接口后续可能变化。
用 DSH 桌面版看文档的人,大概率遇到过这种事:
右键一个 .md,想用 Typora 打开,系统弹记事本;去 Windows 设置里把默认应用改好,过几天又被别的软件抢回去。
Windows 的文件关联是全局的,而我只想改 DSH 里的右键行为。于是我写了一个 DSH 插件 dsh-open-with:它在插件里维护一套「扩展名 → 程序」的映射,不修改系统注册表,也不抢 Windows 默认关联。
这篇文章记录一下插件的结构、Host 端实现、Windows 关联查询的坑,以及设置页两次返工的过程。
一、先看 DSH 插件的最小结构
DSH 插件本质上是一个带 dsh 字段的 npm 包,通过 bundle patch 挂进 profile:
json
{
"name": "dsh-open-with",
"version": "1.0.0",
"type": "module",
"main": "lib/index.js",
"dsh": {
"client": { "platform": "web" },
"bundle": { "patch": "./cordis.patch.yml" }
}
}
cordis.patch.yml 负责把插件插入 DSH 的 bundle 列表:
yaml
- insert:
- id: open-with
name: dsh-open-with
装进 DSH 桌面版 profile 后,需要完全退出再启动,刷新页面不会重新加载 bundle。
插件分两半:
lib/index.js:Host 端,负责配置读写、打开文件、查询 Windows 关联、弹文件选择框;lib/client.js:客户端,负责右键菜单和设置页。
客户端通过两个 slot 挂载:
js
ctx.slots.inject("shell.overlay", () =>
ctx.slots.register({
name: "shell.overlay",
id: "open-with",
order: 70,
locale: NS
}, Overlay)
);
ctx.slots.inject("plugins.bundle.config", () =>
ctx.slots.register({
name: "plugins.bundle.config",
key: "dsh-open-with",
order: 0,
locale: NS
}, ConfigPage)
);
shell.overlay 用来画右键菜单,plugins.bundle.config 是插件详情页里的设置区域。
二、Host 端:配置、路由和打开文件
配置存在 $DSH_HOME/open-with.json,结构很简单:
json
{
"version": 1,
"apps": {
"md": {
"name": "Typora",
"path": "C:\\Users\\you\\AppData\\Local\\Programs\\Typora\\Typora.exe"
}
}
}
写入用临时文件 + rename,避免写到一半断电留下半截 JSON:
js
const tmp = `${configFile}.tmp`;
await writeFile(tmp, JSON.stringify(state, null, 2), "utf8");
await rename(tmp, configFile);
路由通过 ctx.webServer.register 注册,统一先过 DSH 的连接鉴权:
js
export const inject = ["webServer", "connection"];
export function apply(ctx) {
ctx.webServer.register({
kind: "exact",
path: "/open-with/handlers",
handler: async (req, res) => {
if (ctx.connection.requestRejection(req)) return;
const ext = new URL(req.url, "http://localhost")
.searchParams.get("ext") ?? "";
const apps = await handlersFor(ext);
res.writeHead(200, { "content-type": "application/json" });
res.end(JSON.stringify({ apps }));
}
});
}
打开文件时,Host 直接 spawn 配置里的 exe,把文件路径作为第一个参数,不经过 Windows 默认关联解析。启动前做两件事:
- 校验文件路径在 DSH 文件系统映射范围内;
- 清理子进程环境里的敏感变量。
/open-with/open 只接受配置里存在的 exe,不接受客户端传任意命令。
三、关键坑:Windows 的"通用回退列表"
应用推荐来自 Windows Shell 的 SHAssocEnumHandlers,通过 @deepseek-ai/dsh-native-command 的 nativeFileApplications 调用:
js
import { nativeFileApplications } from "@deepseek-ai/dsh-native-command";
const apps = await nativeFileApplications("md");
问题在于:如果扩展名没有登记任何处理器,Windows 不会返回空数组,而是返回一个约 18 个应用的通用列表,里面混着网盘、压缩软件、播放器。用户看到"用坚果云打开 .zzz"会觉得插件在乱猜。
我先查了注册表,确认这个扩展名到底有没有关联类:
js
// 伪代码
async function hasRegisteredClass(ext) {
const { code } = await run("reg.exe", ["query", `HKCR\\.${ext}`]);
return code === 0;
}
async function handlersFor(ext) {
if (!(await hasRegisteredClass(ext))) return [];
return nativeFileApplications(ext);
}
效果对比:
| 扩展名 | 修复前 | 修复后 |
|---|---|---|
md |
20 个应用 | 20 个应用 |
py |
21 个应用 | 21 个应用 |
zzz |
18 个无关应用 | 0 个 |
odp |
18 个无关应用 | 0 个 |
没有登记时,插件显示"没有找到可打开 .xxx 的应用",让用户手动填 exe 路径,而不是推荐一堆无关程序。
查询结果按扩展名缓存 5 分钟,避免每次打开下拉都调一次 Shell API。
四、客户端:右键菜单和设置页
右键菜单挂在文件卡片上,主要提供这些动作:
- 用 X 打开:读插件配置,没有配置时回退系统默认;
- 打开方式...:插件应用 + 系统关联应用;
- 在文件夹中显示;
- 复制路径;
- 在侧栏预览;
- 打开方式设置...。
设置页是这次返工最多的地方。
第一版:表格塞进插件详情页
第一版把「扩展名、应用、路径」做成表格,直接放在插件详情页里。功能没问题,但在 31.5 寸 4K 屏上很难看:
- 插件详情页是阅读宽度,正文列约 650px;
- 表格四列挤在 650px 里,路径被截断;
- 屏幕两侧大量空白,表格却不够用;
- 扩展名下拉只有 300px,类型筛选按钮换行。
第二版:摘要 + 宽抽屉
后来改成:
- 插件页只显示摘要:已配置数量、前几个映射、打开设置;
- 点「打开设置」或右键「打开方式设置...」打开一个宽抽屉;
- 抽屉宽度
min(1320px, 100vw - 280px),不再受插件详情页 max-width 限制; - 抽屉里是完整表格,Esc、✕、点遮罩都能关。
这样不需要覆盖 DSH 父容器的宽度,也不会因为 DSH 升级改布局而错位。
配图 1:右键菜单。 配图 2:宽抽屉设置页。
字号放大与遮挡
抽屉里加了 Aa 缩放:100% / 110% / 120% / 130%,默认 120%。
一开始只放大字体,结果列宽、按钮、下拉面板没跟着变,反而更挤。后来用整体 zoom,让文字、列宽、行高、按钮、浮层同比缩放,才保持一致。
另一个问题是批量新增:原来是一个浮在表格右上的弹层,打开后会遮住路径列。改成标题下方全宽内联面板,表格整体下移,面板内部滚动,底部"取消/添加"固定可见。
配图 3:批量新增 3D 格式的内联面板。
自动保存
设置页没有「保存」按钮:
- 下拉选择、删除立即保存;
- 路径输入停止 600ms 后保存;
- 只保存"扩展名 + 绝对路径"完整的行;
- 缺应用/路径的行是草稿,不写入配置,标题行显示「N 行待完成」。
这样既不会频繁写盘,也不会把半成品写进配置。
五、两个 Windows 坑
1. 中文文件名和 PowerShell 5.1
安装脚本里要复制 安装与使用说明.pdf 这样的中文文件名。脚本如果保存成 UTF-8 无 BOM,Windows PowerShell 5.1 会按 ANSI 读取,中文文件名直接乱码,Test-Path 找不到文件。
解决:install.ps1 保存为 UTF-8 with BOM。PowerShell 7 也兼容。
2. 子进程与测试沙箱
测试时遇到 spawn EPERM,排查后发现不是插件逻辑问题,而是测试用的 DSH 服务跑在受限沙箱里,子进程管道被拦住。换成正常的桌面环境运行就正常了。
如果以后做类似插件,测试环境要跟真实桌面环境保持一致,否则这类错误很容易误判。
六、安装与分发
插件只支持 Windows 10/11 + DSH 桌面版,不需要管理员权限。
安装:
powershell
powershell -ExecutionPolicy Bypass -File .\install.ps1
完全退出并重新打开 DSH 后,插件列表里会出现「文件打开方式 v1.0.0」。
当前 DSH 没有官方插件市场,所以主要通过本地 bundle + 安装脚本分发;DSH 也有实验性的自定义插件市场索引,后面可以再接一个 market-index.json。
卸载:
powershell
powershell -ExecutionPolicy Bypass -File .\install.ps1 -Uninstall
配置文件 open-with.json 不会被删除,需要的话手动清理。
七、项目地址
- Gitee:
https://gitee.com/leecmd786567/dsh-open-with - 许可证:MIT
- 版本:V1.0.0
代码不多,欢迎提 Issue 或者直接看实现。如果也在用 DSH,遇到过 Windows 默认关联被改的问题,这个插件应该能省点事。