LRS 插件开发文档
·暂时没有./ext,只可以通过拖入安装插件,删除及禁用在“设置”
·适用版本:LRS-HungerCat 0.0.3-beta 及以上
·适用文件类型:.hlds(HungerCat LRS Declaration for eXtension)
LRS 插件系统允许你 不改主程序源码、不重新编译,通过一个 JSON 文件(.hlds)给 LRS 添加:
- 右键菜单项
- 顶栏按钮
- 文件表新列
- 设置项
只需把 .hlds 放进 ./ext/ 目录,或直接拖到 LRS 窗口即可安装。改完文件 250ms 内热重载,保存即生效。
目录
- 快速开始
- 扩展目录与加载机制
- .hlds 字段表
- 四个扩展点详解
- 4.1 context_menu — 右键菜单
- 4.2 topbar_buttons — 顶栏按钮
- 4.3 file_columns — 文件表新列
- 4.4 settings — 设置项
- 占位符
- 命令执行机制
- JSON 转义与中文路径
- 安装、启用、卸载
- 热重载
- 调试与排错
- 最佳实践与常见坑
- 完整示例
- API 速查(程序员向)
1. 快速开始
1.1 三步写一个最小插件
第 1 步:新建文本文件 hello.hlds。
第 2 步:写入以下 JSON:
{
"id": "com.example.hello",
"name": "打个招呼",
"version": "1.0.0",
"type": "lrs-extension",
"entry": "cmd.exe /c echo Hello from LRS!",
"points": {
"topbar_buttons": [
{
"id": "hello_btn",
"label": "👋 打招呼",
"command": "cmd.exe /c echo Hi!"
}
]
}
}
第 3 步:保存后把文件拖进 LRS 窗口。或者放到仓库根的 ./ext/ 目录后重新构建一次。
顶栏会出现一个 👋 打招呼 按钮,点击执行 cmd.exe /c echo Hi!(cmd 是隐藏窗口运行,看不到弹出窗,但 Debug 输出里能看到 stdout)。
1.2 最小可用清单字段
必填项缺一不可,否则
ExtensionLoader会拒绝加载并把错误写到 Debug 输出。
| 必填 | 字段 | 说明 |
|---|---|---|
| ✅ | id | 扩展唯一 ID,目录内不可重复,不能含 : |
| ✅ | name | 显示名 |
| ✅ | version | 语义化版本号(仅展示用,不参与兼容性判断) |
| ✅ | type | 固定填 "lrs-extension" |
| ✅ | entry | 默认命令模板(占位符见 §5),多数插件可以写任意合法命令 |
| ✅ | points | 至少包含 4 个 key 中的一个(见 §4) |
2. 扩展目录与加载机制
2.1 扫描路径
| 模式 | 实际扫描路径 |
|---|---|
开发(dotnet run) | AppContext.BaseDirectory/ext/*.hlds(由 .csproj 把仓库根 ./ext/ 拷过来) |
| 部署(单文件 EXE) | LRS.exe 同目录/ext/*.hlds |
启动时若
ext/不存在,程序会自动创建。
2.2 加载顺序
- 启动时全量扫描
ext/目录 - 按
*.hlds顺序读文件,逐个反序列化为ExtensionManifest - 任意一个文件失败 → 写入 Debug 输出,跳过该文件,继续加载其他(失败隔离)
- 全部加载完后
Changed事件触发,UI 刷新 - 启动
FileSystemWatcher监听目录变化
2.3 拖入安装流程
当用户把一个 .hlds 拖到 LRS 主窗口时,会经过:
- 校验:先
LoadFromFile看清单是否合法,坏文件直接拒绝(不复制) - 复制:把源文件复制到
ext/目录(覆盖同名) - 重载:调用
ReloadAsync立即生效 - 事件:触发
Changed,设置页的扩展列表自动更新
详见 ExtensionManager.InstallAsync。
3. .hlds 字段表
3.1 顶层字段
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
id | string | ✅ | 扩展唯一 ID。建议反向域名(如 com.you.app),目录内不可重复,不能含 : |
name | string | ✅ | 显示名(中英文都可) |
version | string | ✅ | 语义化版本号,仅展示,不参与依赖解析 |
type | string | ✅ | 固定填 "lrs-extension",未来可能加 "lrs-theme" 等其他类型 |
entry | string | ✅ | 默认命令模板(多数场景可与具体命令相同,UI 可能用到) |
points | object | ✅ | 四个扩展点(见 §4),至少要有一个非空数组 |
author | string | ❌ | 作者名(展示用) |
description | string | ❌ | 描述(展示用) |
3.2 校验规则
由 ExtensionLoader 强制:
id缺失 → 拒绝id含:→ 拒绝(避免和命名空间混淆)name缺失 → 拒绝version缺失 → 拒绝type缺失 → 拒绝entry缺失 → 拒绝id重复 → 该次加载跳过,记录到ManifestErrors
JSON 解析失败(语法错、字段类型错)也都会被吞掉写到 Debug 输出,不会崩溃主程序。
4. 四个扩展点详解
4.1 context_menu — 右键菜单
在文件列表、目录树、空白区域右键时弹出的菜单项。
"context_menu": [
{
"id": "open_with_notepad",
"label": "用记事本打开",
"applyTo": "File",
"command": "notepad.exe \"%F\""
}
]
| 子字段 | 必填 | 说明 |
|---|---|---|
id | ✅ | 命令唯一 ID,扩展内不重复 |
label | ✅ | 菜单显示文本(可含 emoji) |
applyTo | ❌ | 注入位置过滤,默认 Any |
command | ✅ | 命令模板,支持占位符(§5) |
applyTo 取值
| 值 | 含义 | 占位符行为 |
| -- | ---- | ---------- |
| Any | 任意位置(默认) | 视上下文 |
| File | 仅当右键目标为文件 | %F=文件路径,%D=父目录 |
| Folder | 仅当右键目标为文件夹 | %F=文件夹路径,%D=父目录 |
| Background | 仅在空白区域右键 | %F 空,%D=当前目录 |
多个菜单项
数组里写几项就有几项,按数组顺序插入到原生菜单的扩展区域。原生右键菜单(包括系统 Shell 菜单)会先显示,你的扩展菜单追加在底部。
图标(TODO)
当前版本未开放菜单图标,1.0 后会加 icon 字段。
4.2 topbar_buttons — 顶栏按钮
LRS 顶部导航条上的按钮(新建文件夹、设置旁边那一排)。
"topbar_buttons": [
{
"id": "git_pull",
"label": "Git Pull",
"command": "git.exe -C \"%D\" pull",
"position": 50
}
]
| 子字段 | 必填 | 说明 |
|---|---|---|
id | ✅ | 命令唯一 ID |
label | ✅ | 按钮显示文本(可含 emoji) |
command | ✅ | 命令模板 |
position | ❌ | 插入位置,越小越靠左,默认 100 |
排序规则
- 同一
position内按id字典序 - 内置按钮占用
position10(搜索)、50(新建文件夹)、60(新建文件)、999(设置)附近 - 想要插到「新建文件夹」左边 →
position < 50;插到右边 →position > 60
Tooltip
当前用 label 同时作为 Tooltip,未来会拆出 tooltip 字段。
4.3 file_columns — 文件表新列
在右侧文件列表的「名称 / 修改日期 / 创建日期 / 大小」之外追加新列。
"file_columns": [
{
"id": "col_ext",
"header": "扩展名",
"value": "extension",
"width": 1.0
}
]
| 子字段 | 必填 | 说明 |
|---|---|---|
id | ✅ | 列唯一 ID |
header | ✅ | 表头文本 |
value | ✅ | 取值表达式,必须是下表预定义之一 |
width | ❌ | 列宽(相对权重),默认 1.0 |
value 预定义表达式
| 表达式 | 输出 |
|---|---|
length | 文件大小(格式化字符串,文件夹为空) |
modified | 最后修改时间(本地化) |
created | 创建时间(本地化) |
extension | 文件扩展名(含点,如 .txt) |
name | 名称(不含路径) |
path | 完整路径 |
isfile | "true" / "false" |
type | 节点类型名("File" / "Directory") |
⚠️ 不支持任意运行时表达式(出于安全考虑,不能写 C# / JavaScript / PowerShell)。要加新表达式需要改主程序。
列宽
width 是相对权重。例如同区域有两列 width: 1.0 和 width: 2.0,后者宽度是前者的 2 倍。
4.4 settings — 设置项
在设置页的「扩展」分区里追加配置项,用户改的值会自动持久化到 configs.json 的 Extensions.Settings[<extensionId>.<key>]。
"settings": [
{
"key": "autoRefresh",
"label": "操作后自动刷新",
"type": "toggle",
"default": "true"
},
{
"key": "defaultEditor",
"label": "默认编辑器",
"type": "combo",
"default": "notepad",
"options": ["notepad", "code", "subl", "vim"]
}
]
| 子字段 | 必填 | 说明 |
|---|---|---|
key | ✅ | 设置键(在同一扩展内唯一) |
label | ✅ | 显示标签 |
type | ✅ | toggle / number / text / combo |
default | ❌ | 默认值(字符串,渲染时按 type 强转) |
options | combo 必填 | 候选项数组 |
type 渲染对照
type | UI 控件 | 存储格式 |
|---|---|---|
toggle | ToggleSwitch | "true" / "false" |
number | NumberBox | 数字字符串("42") |
text | TextBox | 原文 |
combo | ComboBox | 选中的 options 元素 |
读取设置值(程序化)
当前 0.0.3-beta 阶段扩展命令内不能直接读 Extensions.Settings(未来会加占位符)。常规用法是:
- 让用户在 UI 里改 → 自动落盘
- 扩展里硬编码常用值
- 或者用
%F/%D等路径占位符 + 读取外部配置文件
设计上故意不暴露太多内部状态,扩展应该尽量"无状态"。
5. 占位符
命令模板(entry 和每个 command)支持以下占位符,在执行瞬间替换:
| 占位符 | 替换为 | 适用场景 |
|---|---|---|
%F | 当前右键/选中的文件路径 | 文件右键、文件夹右键、顶栏(可能为空) |
%D | 当前目录 | 所有位置都至少有值 |
%L | 多选时所有选中项的路径,; 分隔 | 多选文件时(v0.0.4+) |
示例
| 场景 | command 模板 |
|---|---|
| 用记事本打开当前文件 | notepad.exe "%F" |
| 在当前目录打开终端 | wt.exe -d "%D" |
| 复制当前文件完整路径 | powershell.exe -NoProfile -Command "Set-Clipboard -Value '%F'" |
| Git 拉取当前目录 | git.exe -C "%D" pull |
| 7z 压缩当前文件 | "C:\Program Files\7-Zip\7z.exe" a "%F.zip" "%F" |
⚠️ 占位符替换只是字符串替换。如果
%F含&|<>^等 cmd 特殊字符,可能导致命令解析错乱。 解决:路径必须用"..."包裹,并避免把%F直接拼接到不可控参数里。
6. 命令执行机制
执行链:
UI 触发
└─→ ExtensionManager.ExecuteAsync(extensionId, commandId, ctx)
├─→ ExtensionLoader.SubstitutePlaceholders(commandTemplate, ctx)
└─→ ProcessStartInfo { FileName, Arguments, UseShellExecute=false, CreateNoWindow=true }
└─→ Process.Start(...)
├─→ stdout / stderr 重定向到 Debug.WriteLine
└─→ 异步等待退出码
关键点:
UseShellExecute = false:走 CreateProcess,不会触发 Shell 关联(如.txt不会用记事本打开)CreateNoWindow = true:主窗口对 GUI 程序不隐藏窗口本身(窗口仍会弹出),但对 cmd 控制台程序隐藏控制台- 首空格拆分:
commandTemplate按第一个空格切成FileName+Arguments(见RunProcessAsync) - 错误隔离:执行失败不影响 UI 状态;退出码与 stderr 会写到 Debug 输出
- 跨平台:仅 Windows(
Process调用是 OS 无关的,但占位符和路径处理是 Windows 风格)
6.1 何时用 cmd.exe /c,何时直接调
| 想做的事 | 推荐写法 |
|---|---|
| 调一个 EXE | notepad.exe "%F" 直接调 |
| 需要链式命令 | cmd.exe /c echo a & echo b |
| 调 PowerShell | powershell.exe -NoProfile -Command "..." |
| 调 WScript/JScript | cscript.exe //NoLogo "script.js" |
| 调 wt | wt.exe -d "%D" |
6.2 命令找不到
- 如果
FileName不在 PATH 里,会静默失败(exit code != 0,stderr 写到 Debug) - 用绝对路径最稳:
"C:\Program Files\Git\bin\git.exe" -C "%D" pull
7. JSON 转义与中文路径
7.1 Windows 路径的反斜杠
Windows 路径用 \ 分隔,在 JSON 字符串里必须写成 \\:
✅ 正确:
"command": "notepad.exe \"C:\\Users\\me\\file.txt\""
❌ 错误:
"command": "notepad.exe \"C:\Users\me\file.txt\""
7.2 cmd 命令里的引号
如果 command 里要再嵌引号(比如传给 PowerShell),需要 \" 转义:
"command": "powershell.exe -NoProfile -Command \"Set-Clipboard -Value '%F'\""
7.3 中文路径
完全 OK。LRS 是 .NET + WinUI 3,路径是 UTF-16 内部表示,传给子进程时会按系统 OEM/ANSI 编码转换。但:
- 子进程如果是老旧控制台程序(如
cmd.exe),默认 ANSI 编码,中文可能乱码 - 解决:用
chcp 65001切到 UTF-8,或调 PowerShell(默认 UTF-8)
"command": "cmd.exe /c chcp 65001 > nul & type \"%F\""
8. 安装、启用、卸载
8.1 安装
方式 A:拖入窗口
把 .hlds 拖到 LRS 主窗口 → 自动校验 + 复制 + 加载。
方式 B:手动放到 ext/
把 .hlds 放到 LRS.exe 同目录下的 ext/ 子目录里。下次启动自动加载(运行中则 250ms 内热重载)。
方式 C:开发期
把 .hlds 放到仓库根的 ./ext/ 目录,重新 dotnet build(csproj 会把它拷到输出目录)。
8.2 启用 / 禁用
在设置页「扩展」分区,每个扩展右侧有开关:
- 关闭 → 该扩展的所有贡献点(菜单 / 按钮 / 列 / 设置)立即从 UI 消失
- 重新打开 → 重新出现
- 禁用状态持久化到
configs.json
8.3 卸载
直接删除 ext/ 目录里的 .hlds 文件即可。删除后 250ms 内 UI 自动刷新。
没有"软卸载"按钮(防误删)。要禁用就关开关,要彻底移除就删文件。
8.4 更新
替换 ext/ 里的 .hlds 即可。文件变化触发 FileSystemWatcher,250ms 内重载。
⚠️ 重载不会清掉用户已保存的设置项值(存在
configs.json里,与 .hlds 内容解耦)。
9. 热重载
LRS 启动 FileSystemWatcher 监听 ext/:
- 监听
Created/Changed/Deleted/Renamed事件 - 250ms 防抖(
System.Timers.Timer) - 防抖到期 → 全量重新扫描整个目录(不是单文件 patch)
- 扫描完后
Changed事件 → UI 刷新
重载是全量的:单个文件改了也会重新解析所有 .hlds。对于几十个扩展的规模,性能完全 OK。
10. 调试与排错
10.1 看错误输出
核心原则:所有错误都写 Debug 输出,不弹窗、不阻塞。
在 Visual Studio:
- 运行 LRS →
Debug → Windows → Output选Debug
在命令行:
dotnet run --project LRS
命令行窗口本身会显示 stdout,但 Debug.WriteLine 不会显示。要看的话用 DebugView 或 attach 调试器。
10.2 常见错误
| 现象 | 原因 | 排查 |
|---|---|---|
| 扩展没出现在设置页 | JSON 解析失败 | 检查 id/name/version/type/entry 是否都有;JSON 是否合法 |
| 扩展出现但菜单里没有 | applyTo 不匹配 | 右键的位置是不是你设的 applyTo |
| 按钮点了没反应 | 命令本身出错 | 退出码 / stderr 都在 Debug;先在 cmd 里手动跑一遍 |
%F 是空字符串 | 当前 context 没有文件 | 顶栏按钮 / Background 菜单 %F 为空;改用 %D |
| 路径乱码 | cmd 默认编码不是 UTF-8 | 在命令前加 chcp 65001 > nul |
中文显示成 ??? | 子进程是 ANSI 程序且没切编码 | 同上,或改用 PowerShell |
10.3 在命令里写日志
调试时可以让插件写日志到文件:
"command": "cmd.exe /c echo %F > %TEMP%\\my_plugin.log"
或者用 PowerShell:
"command": "powershell.exe -NoProfile -Command \"'%F' | Out-File -Append '%TEMP%\\my_plugin.log'\""
11. 最佳实践与常见坑
11.1 命名与 ID
id用反向域名:com.yourain.lrs.search而不是my-search- 避免用通用动词如
open、copy—— 多个扩展都贡献open时 UI 会混乱 - id 不要含
:(被 loader 拒绝)
11.2 命令设计
- 幂等:点两次按钮应该等价于一次
- 快速返回:长任务应该自己用
start /b后台化,不要阻塞 LRS 进程(cmd 退出后 LRS 才会继续响应) - 路径带空格:始终用
"%F"/"%D"加引号 - 不要假设 PATH:用绝对路径最稳,或显式
set PATH=...
11.3 性能
- 单个 .hlds 文件 < 1KB
- 总扩展数 < 100 时启动无感
- 顶栏按钮 > 30 个时 UI 会拥挤,考虑分组或用下拉菜单(未来支持)
11.4 兼容性
entry/command是黑盒字符串,主程序不解析- 升级 LRS 时可能会改占位符语义或新增占位符(向后兼容:旧占位符保留)
- 升级 LRS 时不会改
value预定义表达式的取值
11.5 安全
- ⚠️ 扩展能执行任意命令。只安装可信来源的 .hlds
- 复制别人发你的 .hlds 前先打开看一遍
- 可以在测试环境先试,再丢到生产
- LRS 不做沙箱,跑插件 = 跑你的 EXE
11.6 常见坑
| 坑 | 表现 | 修复 |
| -- | ---- | ---- |
| 多个扩展 id 相同 | 后加载的覆盖先加载的;Debug 里有 duplicate id 警告 | 改 id 唯一 |
| command 用 start cmd.exe /c ... 启动新窗口 | 用户看到黑色 cmd 窗口闪一下 | 接受现状(这是 Windows 行为);或写 GUI 程序 |
| 想用 %F 拼到 PowerShell 脚本里 | PowerShell 看不到完整路径(被 cmd 提前展开) | 用 start /WAIT /B powershell ... 或用 base64 -EncodedCommand |
| 想在 command 里写多行 | JSON 不支持裸多行(除非用 \n) | 用 & 串行:cmd.exe /c echo a & echo b & echo c |
| file_columns.value 写 C# 表达式 | 直接报错或显示空 | 只能用预定义表达式;自定义需要改主程序 |
12. 完整示例
参考 samples/example.hlds,覆盖全部 4 个扩展点:
{
"id": "com.example.lrs.sample",
"name": "示例扩展 / Sample Extension",
"version": "1.0.0",
"type": "lrs-extension",
"author": "LRS Team",
"description": "覆盖全部 4 个扩展点:右键菜单 / 顶栏按钮 / 文件列 / 设置项。",
"entry": "cmd.exe /c echo %F",
"points": {
"context_menu": [
{
"id": "open_with_notepad",
"label": "用记事本打开",
"applyTo": "File",
"command": "notepad.exe \"%F\""
},
{
"id": "copy_full_path",
"label": "复制完整路径",
"applyTo": "Any",
"command": "powershell.exe -NoProfile -Command \"Set-Clipboard -Value '%F'\""
},
{
"id": "open_terminal_here",
"label": "在此处打开终端",
"applyTo": "Folder",
"command": "wt.exe -d \"%D\""
}
],
"topbar_buttons": [
{
"id": "sample_git_pull",
"label": "Git Pull",
"command": "git.exe -C \"%D\" pull",
"position": 50
}
],
"file_columns": [
{
"id": "sample_extension",
"header": "扩展名",
"value": "extension",
"width": 1.0
}
],
"settings": [
{
"key": "autoRefresh",
"label": "操作后自动刷新",
"type": "toggle",
"default": "true"
}
]
}
}
更多灵感:
- 复制文件名到剪贴板
- 用 VS Code 打开
- 计算文件哈希(SHA256)
- Git add + commit(顶栏按钮)
- 批量重命名(右键菜单 + InputBox)
- 在文件树里高亮某类文件(file_columns)
13. API 速查(程序员向)
如果想直接调 LRS 的 C# 扩展 API(写 LRS 子项目或测试工具时):
| 类型 | 命名空间 | 关键成员 |
|---|---|---|
ExtensionManifest | LRS.Models | Id, Name, Version, Type, Entry, Points, Author, Description |
ExtensionContext | LRS.Models | record (File?, Directory?, List?) |
ContextMenuApplyTo | LRS.Models | Any, File, Folder, Background |
ContextMenuContribution | LRS.Models | Id, Label, ApplyTo, Command |
TopbarButtonContribution | LRS.Models | Id, Label, Command, Position |
FileColumnContribution | LRS.Models | Id, Header, Value, Width |
SettingContribution | LRS.Models | Key, Label, Type, Default, Options |
ExtensionLoader | LRS.Services | LoadFromFile(path), SubstitutePlaceholders(template, ctx) |
ExtensionManager | LRS.Services | InitializeAsync, ReloadAsync, GetContributions<T>, GetTypedContributions<T>, ExecuteAsync(extensionId, commandId, ctx), InstallAsync(sourcePath), SetEnabled(id, bool), LoadedExtensions, Changed event |
13.1 自己写测试
var manifest = ExtensionLoader.LoadFromFile(@"ext\my-plugin.hlds");
if (manifest == null) { /* 失败,看 Debug */ }
var ctx = new ExtensionContext(
File: @"C:\Users\me\file.txt",
Directory: @"C:\Users\me",
List: null);
var mgr = new ExtensionManager();
await mgr.InitializeAsync();
var result = await mgr.ExecuteAsync(
extensionId: manifest.Id,
commandId: "open_with_notepad",
ctx: ctx);
Console.WriteLine($"exit={result.ExitCode}, error={result.Error}");
13.2 贡献类型映射
PointNameByType = {
[typeof(ContextMenuContribution)] = "context_menu",
[typeof(TopbarButtonContribution)] = "topbar_buttons",
[typeof(FileColumnContribution)] = "file_columns",
[typeof(SettingContribution)] = "settings",
};
附录:版本兼容
| LRS 版本 | .hlds 规范版本 | 备注 |
|---|---|---|
| 0.0.3-beta | 1.0 | 当前 |
| 0.0.2 | 1.0 | 缺少 settings 分区持久化提示 |
| 0.0.1 | 0.9 | 早期试验性接口,已弃用 |
未来 1.0 计划加入:
- 菜单图标 (
icon字段) - 设置项读取占位符(
%Settings:myKey%) - 文件列支持自定义格式化
- 主题扩展(
type: "lrs-theme") - 多语言(
i18n字段)
有问题或建议?欢迎在 GitHub 提 Issue / PR。
Have questions or suggestions? Feel free to open an Issue / PR on GitHub.