(请填写完整的网址,例如:https://www.example.com)
(贵站展示本站链接的页面地址,一般是友链页面,填写后将自动验证友链关系有效性)
(用于抓取文章)
(用于接收通知)

LRS 插件开发文档

·暂时没有./ext,只可以通过拖入安装插件,删除及禁用在“设置”

·适用版本:LRS-HungerCat 0.0.3-beta 及以上

·适用文件类型:.hlds(HungerCat LRS Declaration for eXtension)

LRS 插件系统允许你 不改主程序源码、不重新编译,通过一个 JSON 文件(.hlds)给 LRS 添加:

  • 右键菜单项
  • 顶栏按钮
  • 文件表新列
  • 设置项

只需把 .hlds 放进 ./ext/ 目录,或直接拖到 LRS 窗口即可安装。改完文件 250ms 内热重载,保存即生效。


目录

  1. 快速开始
  2. 扩展目录与加载机制
  3. .hlds 字段表
  4. 四个扩展点详解
  5. 占位符
  6. 命令执行机制
  7. JSON 转义与中文路径
  8. 安装、启用、卸载
  9. 热重载
  10. 调试与排错
  11. 最佳实践与常见坑
  12. 完整示例
  13. 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 runAppContext.BaseDirectory/ext/*.hlds(由 .csproj 把仓库根 ./ext/ 拷过来)
部署(单文件 EXE)LRS.exe 同目录/ext/*.hlds

启动时若 ext/ 不存在,程序会自动创建。

2.2 加载顺序

  1. 启动时全量扫描 ext/ 目录
  2. *.hlds 顺序读文件,逐个反序列化为 ExtensionManifest
  3. 任意一个文件失败 → 写入 Debug 输出,跳过该文件,继续加载其他(失败隔离
  4. 全部加载完后 Changed 事件触发,UI 刷新
  5. 启动 FileSystemWatcher 监听目录变化

2.3 拖入安装流程

当用户把一个 .hlds 拖到 LRS 主窗口时,会经过:

  1. 校验:先 LoadFromFile 看清单是否合法,坏文件直接拒绝(不复制)
  2. 复制:把源文件复制到 ext/ 目录(覆盖同名)
  3. 重载:调用 ReloadAsync 立即生效
  4. 事件:触发 Changed,设置页的扩展列表自动更新

详见 ExtensionManager.InstallAsync


3. .hlds 字段表

3.1 顶层字段

字段类型必填说明
idstring扩展唯一 ID。建议反向域名(如 com.you.app),目录内不可重复,不能含 :
namestring显示名(中英文都可)
versionstring语义化版本号,仅展示,不参与依赖解析
typestring固定填 "lrs-extension",未来可能加 "lrs-theme" 等其他类型
entrystring默认命令模板(多数场景可与具体命令相同,UI 可能用到)
pointsobject四个扩展点(见 §4),至少要有一个非空数组
authorstring作者名(展示用)
descriptionstring描述(展示用)

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 字典序
  • 内置按钮占用 position 10(搜索)、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.0width: 2.0,后者宽度是前者的 2 倍。


4.4 settings — 设置项

在设置页的「扩展」分区里追加配置项,用户改的值会自动持久化到 configs.jsonExtensions.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显示标签
typetoggle / number / text / combo
default默认值(字符串,渲染时按 type 强转)
optionscombo 必填候选项数组

type 渲染对照

typeUI 控件存储格式
toggleToggleSwitch"true" / "false"
numberNumberBox数字字符串("42"
textTextBox原文
comboComboBox选中的 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,何时直接调

想做的事推荐写法
调一个 EXEnotepad.exe "%F" 直接调
需要链式命令cmd.exe /c echo a & echo b
调 PowerShellpowershell.exe -NoProfile -Command "..."
调 WScript/JScriptcscript.exe //NoLogo "script.js"
调 wtwt.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 → OutputDebug

在命令行:

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
  • 避免用通用动词如 opencopy —— 多个扩展都贡献 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 唯一 | | commandstart 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 子项目或测试工具时):

类型命名空间关键成员
ExtensionManifestLRS.ModelsId, Name, Version, Type, Entry, Points, Author, Description
ExtensionContextLRS.Modelsrecord (File?, Directory?, List?)
ContextMenuApplyToLRS.ModelsAny, File, Folder, Background
ContextMenuContributionLRS.ModelsId, Label, ApplyTo, Command
TopbarButtonContributionLRS.ModelsId, Label, Command, Position
FileColumnContributionLRS.ModelsId, Header, Value, Width
SettingContributionLRS.ModelsKey, Label, Type, Default, Options
ExtensionLoaderLRS.ServicesLoadFromFile(path), SubstitutePlaceholders(template, ctx)
ExtensionManagerLRS.ServicesInitializeAsync, 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-beta1.0当前
0.0.21.0缺少 settings 分区持久化提示
0.0.10.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.