跳到主要内容

扩展插件指南

扩展插件是后台服务,可以桥接外部协议、注册虚拟设备、锁定 LED 进行直接控制,并提供自定义 HTML UI 页面。

目录结构

plugins/extension.my_bridge/
├── manifest.json # 元数据 + 权限
├── init.lua # 入口脚本
├── lib/ # 可选 Lua 模块
│ └── protocol.lua
├── data/ # 持久化数据目录
└── page/ # 可选内嵌 HTML UI
└── dist/
└── index.html

生命周期

Core 启动 → 加载已启用的扩展
→ 初始化 Lua 环境
→ plugin.on_start() ← 扩展启动
→ [事件循环]
plugin.on_scan_devices() ← 手动扫描触发
plugin.on_devices_changed() ← 设备列表变化
plugin.on_led_locks_changed()← LED 锁状态变化
plugin.on_system_media_*() ← 媒体播放改变
plugin.on_system_state_changed() ← 系统状态变化
plugin.on_device_frame() ← 实时 LED 数据
plugin.on_page_message() ← 来自 HTML 页面的消息
→ plugin.on_stop() ← 扩展停止

生命周期钩子

on_start()

扩展加载时调用。在此初始化连接、启动外部进程、注册设备。

function plugin.on_start()
ext.log("扩展正在启动")
-- 连接外部服务,注册设备等
end

on_scan_devices()

用户手动触发设备扫描时调用。

function plugin.on_scan_devices()
ext.log("正在扫描设备...")
-- 发现并注册设备
end

on_devices_changed(devices)

全局设备列表变化时调用。

function plugin.on_devices_changed(devices)
-- 响应新增/移除的设备
end

on_led_locks_changed(locks)

任何 LED 锁状态变化时调用。

function plugin.on_led_locks_changed(locks)
-- locks: 当前锁定状态
end

on_system_media_changed(session)

版本

3.0.0-dev.3 起支持。需要 "media:session" 权限。

当系统媒体会话属性(如标题、艺术家、专辑、封面等)发生变化时调用。

function plugin.on_system_media_changed(session)
if session then
ext.log("当前播放曲目已变化: " .. session.title)
end
end

on_system_media_playback_changed(session)

版本

3.0.0-dev.3 起支持。需要 "media:session" 权限。

当播放状态(播放、暂停、停止)发生变化时调用。

function plugin.on_system_media_playback_changed(session)
if session then
ext.log("播放状态: " .. session.playback_status)
end
end

on_system_media_timeline_changed(session)

版本

3.0.0-dev.3 起支持。需要 "media:session" 权限。

当播放进度或时长更新时调用。

function plugin.on_system_media_timeline_changed(session)
-- session.timeline 包含进度和时长信息
end

on_system_state_changed(topic, data)

版本

3.0.0-dev.3 起支持。需要对应主题的权限("system:process""system:window-focus")。

当已订阅的系统状态主题发生变化时调用。topic 字符串标识变化的主题,data 包含变化数据。

function plugin.on_system_state_changed(topic, data)
if topic == "process" then
ext.log("运行中的应用: " .. #data.apps)
for _, change in ipairs(data.changes) do
if change.current_instance_count > change.previous_instance_count then
ext.log("已启动: " .. change.name)
else
ext.log("已停止: " .. change.name)
end
end
elseif topic == "window_focus" then
if data.current then
ext.log("当前焦点: " .. (data.current.app_name or "未知"))
end
end
end

详见系统状态监控了解可用主题和数据结构。

on_device_frame(port, outputs)

每个活跃设备帧触发,携带实时 LED 颜色数据,高频(最高 30+ fps)。

function plugin.on_device_frame(port, outputs)
-- outputs: {output_id = {r,g,b,r,g,b,...}, ...}
local colors = outputs["out1"]
if colors then
-- 将颜色转发至外部系统
end
end

on_page_message(data)

内嵌 HTML 页面发送消息时调用。

function plugin.on_page_message(data)
if data.action == "refresh" then
-- 处理页面请求
ext.page_emit({status = "ok", devices = ext.get_devices()})
end
end

on_stop()

插件即将卸载时调用,清理所有资源。

function plugin.on_stop()
ext.log("扩展正在停止")
-- 关闭连接,终止进程,注销设备
end

注册虚拟设备

扩展可以注册虚拟设备,这些设备在 Skydimo 中像实体硬件一样显示:

local port = ext.register_device({
controller_port = "openrgb://device_0",
manufacturer = "Corsair",
model = "Vengeance RGB",
serial_id = "ABC123",
description = "RAM 模块",
controller_id = "extension.openrgb",
device_type = "dram",
outputs = {
{
id = "zone0",
name = "Zone 0",
leds_count = 8,
output_type = "linear",
default_effect = "rainbow_wave", -- 可选(自 3.0.0-dev.4 起支持)
}
}
})

这些设备从 Skydimo 接收灯效,你可以通过 on_device_frame 将渲染后的颜色转发至真实硬件。

更新设备

-- 更改设备昵称
ext.set_device_nickname(port, "我的内存条")

-- 更新输出配置
ext.update_output(port, "zone0", {
leds_count = 16,
matrix = nil -- 或 {width=4, height=4, map={...}}
})

-- 移除设备
ext.remove_extension_device(port)

LED 锁定

扩展可以锁定特定 LED 进行直接颜色控制,覆盖当前活跃的灯效:

-- 锁定 LED 0-9 进行直接控制
ext.lock_leds("COM3", "out1", {0, 1, 2, 3, 4, 5, 6, 7, 8, 9})

-- 对锁定的 LED 设置颜色
ext.set_leds("COM3", "out1", {
{0, 255, 0, 0}, -- LED 0: 红
{1, 0, 255, 0}, -- LED 1: 绿
{2, 0, 0, 255}, -- LED 2: 蓝
})

-- 释放锁定
ext.unlock_leds("COM3", "out1", {0, 1, 2, 3, 4, 5, 6, 7, 8, 9})

网络(TCP)

扩展可以建立 TCP 连接。需要 "network:tcp" 权限。

-- 连接
local handle = ext.tcp_connect("127.0.0.1", 6742)
-- 带超时:
local handle = ext.tcp_connect("127.0.0.1", 6742, 5000)

-- 发送数据
local bytes_sent = ext.tcp_send(handle, data_string)

-- 接收数据
local data = ext.tcp_recv(handle, 1024) -- 最多 1024 字节
local data = ext.tcp_recv(handle, 1024, 5000) -- 带 5s 超时
local data = ext.tcp_recv_exact(handle, 256) -- 恰好 256 字节
local data = ext.tcp_recv_exact(handle, 256, 5000) -- 带超时

-- 关闭
ext.tcp_close(handle)

进程管理

扩展可以启动和管理外部进程。需要 "process" 权限。

-- 启动进程
local handle = ext.spawn_process("openrgb.exe", {"--server"}, {
hidden = true,
working_dir = ext.data_dir
})

-- 检查是否运行中
if ext.is_process_alive(handle) then
ext.log("进程正在运行")
end

-- 终止进程
ext.kill_process(handle)

HID 设备访问

3.0.0-dev.2 起支持

扩展可以直接打开并与 HID 设备通信。需要 "hardware:hid" 权限。

-- 枚举所有已连接的 HID 设备(可按 VID/PID 过滤)
local devices = ext.hid_enumerate() -- 全部设备
local devices = ext.hid_enumerate(0x1B1C) -- 按 VID 过滤
local devices = ext.hid_enumerate(0x1B1C, 0x1B2D) -- 按 VID + PID 过滤

-- 每项包含:
-- device.path、device.vid、device.pid、device.serial、
-- device.manufacturer、device.product、
-- device.interface_number、device.usage、device.usage_page

-- 按 VID/PID 打开设备(可选序列号)
local handle = ext.hid_open(0x1B1C, 0x1B2D)
local handle = ext.hid_open(0x1B1C, 0x1B2D, "SN123456")

-- 或按操作系统设备路径打开
local handle = ext.hid_open_path(devices[1].path)

-- 写入数据(返回写入字节数)
local n = ext.hid_write(handle, "\x00\x01\x02\x03")

-- 读取数据
local data = ext.hid_read(handle, 64) -- 非阻塞
local data = ext.hid_read(handle, 64, 1000) -- 超时 1000ms
local data = ext.hid_read(handle, 64, -1) -- 阻塞等待

-- Feature Report
local n = ext.hid_send_feature_report(handle, "\x00\xff\x01")
local data = ext.hid_get_feature_report(handle, 64)
local data = ext.hid_get_feature_report(handle, 64, 0x01) -- 指定 report ID

-- 关闭
ext.hid_close(handle)

扩展停止时,所有打开的 HID 句柄会被自动关闭。

原生 C 模块

3.0.0-dev.2 起支持

扩展可以加载放置在插件目录中的原生 C 模块(Windows 上为 .dll,Linux/macOS 上为 .so)。需要 "native" 权限。

声明 native 权限后:

  • Lua VM 将使用 unsafe_new() 创建,以允许加载原生库。
  • 插件目录及其 lib/ 子目录会被自动添加到 package.cpath
manifest.json
{
"permissions": ["native"]
}
-- 从插件目录加载原生模块
local mylib = require("mylib") -- 加载 mylib.dll / mylib.so
local helper = require("lib/helper") -- 加载 lib/helper.dll / lib/helper.so

mylib.do_something()
警告

native 权限会绕过 Lua 的沙箱机制。请仅对经过充分审查的可信原生模块使用此权限。分发需要 native 权限的插件前,必须进行严格的安全审计。

扩展可以查询和控制设备上的灯效:

-- 获取所有可用灯效
local effects = ext.get_effects()

-- 获取灯效参数 schema
local params = ext.get_effect_params("rainbow")

-- 在设备输出端口上设置灯效
ext.set_effect("COM3", "out1", "rainbow")
ext.set_effect("COM3", "out1", "rainbow", {speed = 3.0})

页面通信

扩展可以包含一个内嵌 HTML 页面,显示在 Skydimo UI 中。有两种模式:

路径模式(仅桌面应用)

指向插件随附的本地 HTML 文件。该模式仅在 Skydimo 桌面应用中可用。

manifest.json
{
"page": "page/dist/index.html"
}

URL 模式

版本

3.0.0-dev.4 起支持。

指向一个外部 URL。该模式在 桌面应用和浏览器 环境中均可使用。

manifest.json
{
"page_url": "http://localhost:5173"
}

使用 URL 模式时,Skydimo 会将页面加载为 iframe,并自动在 URL 后附加以下 查询参数

参数说明
extId扩展的插件 ID
locale当前 UI 语言(如 en-USzh-CN
wsUrlSkydimo Core 的 WebSocket 地址(如 ws://127.0.0.1:42070

例如,如果 page_urlhttp://localhost:5173,实际的 iframe URL 将为:

http://localhost:5173?extId=my_extension&locale=zh-CN&wsUrl=ws://127.0.0.1:42070

适配 URL 模式

你的页面需要能从宿主注入的全局变量(window.__SKYDIMO_EXT_PAGE__ URL 查询参数中解析连接信息。以下是推荐的模式(来自 LED Canvas 扩展):

bridge.ts
// 来源(按优先级排序):
// 1. window.__SKYDIMO_EXT_PAGE__ — 由宿主启动脚本注入(路径模式)
// 2. URL 查询参数 — 由 UI iframe 加载器设置(URL 模式)或手动开发
// 3. 硬编码回退值

interface SkydimoExtPage {
extId: string
wsUrl: string
locale?: string
}

declare global {
interface Window {
__SKYDIMO_EXT_PAGE__?: Partial<SkydimoExtPage>
}
}

const _params = new URLSearchParams(window.location.search)

const PAGE: SkydimoExtPage = {
extId: window.__SKYDIMO_EXT_PAGE__?.extId ?? _params.get('extId') ?? 'my_extension',
wsUrl: window.__SKYDIMO_EXT_PAGE__?.wsUrl ?? _params.get('wsUrl') ?? 'ws://127.0.0.1:42070',
}

对于语言解析,同样需要回退到查询参数:

i18n.ts
function resolveLocale(): string {
// 1. 从宿主注入或 URL 查询参数获取
const injected = window.__SKYDIMO_EXT_PAGE__?.locale
?? new URLSearchParams(window.location.search).get('locale')
if (injected && supportedLocales.includes(injected)) return injected

// 2. 从 navigator 进行基础语言匹配
// ...
}

浏览器调试

使用 URL 模式时,你可以直接在浏览器中调试扩展页面,无需桌面应用:

  1. 启动页面的开发服务器(例如 npm run devhttp://localhost:5173
  2. 确保 Skydimo Core 正在运行
  3. 在浏览器中直接打开页面,附带查询参数:
    http://localhost:5173?extId=my_extension&wsUrl=ws://127.0.0.1:<core_port>
  4. 你可以完整使用浏览器开发者工具 —— 检查元素、调试 JS、监控 WebSocket 帧等。
提示

这在开发过程中非常有用。你可以在开发时使用 page_url 指向开发服务器以获得热重载,然后在发布时切换为 page 指向生产构建结果。

Lua ↔ 页面通信

在 Lua 与页面之间双向通信:

-- 向 HTML 页面发送数据
ext.page_emit({type = "update", devices = ext.get_devices()})

-- 接收来自页面的消息(通过 on_page_message 钩子)
function plugin.on_page_message(data)
if data.type == "set_color" then
ext.set_leds(data.port, data.output, data.colors)
end
end

通知

向用户发送 Toast 通知:

-- 简单通知
ext.notify("连接成功", "已连接到 OpenRGB 服务器")

-- 带级别
ext.notify("警告", "设备无响应", "warn") -- "info", "warn", "error"

-- 持久通知(保持显示直到关闭,相同 ID 会就地更新文本)
ext.notify_persistent("conn_status", "正在连接...", "尝试连接到服务器")

-- 技巧:实现实时进度更新
-- 如果在之后多次发送具有**完全相同 ID**的持久通知,Chakra UI 会直接就地更新已存在 Toast 的
-- title 和 description,而不是弹出新的 Toast。这对实现「进度条」文字更新非常实用。
ext.notify_persistent("conn_status", "正在同步...", "步骤 1/3:认证中")
ext.notify_persistent("conn_status", "正在同步...", "步骤 2/3:同步设备")
ext.notify_persistent("conn_status", "正在同步...", "步骤 3/3:完成")

-- 稍后关闭
ext.dismiss_persistent("conn_status")

系统状态监控

3.0.0-dev.3 起支持

扩展可以订阅和查询系统状态主题,例如运行中的进程和当前聚焦窗口。每个主题需要对应的权限。

平台支持

系统状态监控目前仅支持 Windows。在不支持的平台上,ext.list_system_state_topics() 返回的主题将显示 supported = falseext.get_system_state() 返回的快照中 supported = false 且数据为空。

可用主题

主题权限说明
processsystem:process运行中的应用程序进程(名称和实例数)
window_focussystem:window-focus当前聚焦的前台窗口(应用名称和窗口标题)

查询系统状态

-- 列出本插件可用的主题(根据已声明权限过滤)
local topics = ext.list_system_state_topics()
for _, topic in ipairs(topics) do
ext.log("主题: " .. topic.id .. " supported=" .. tostring(topic.supported))
end

-- 获取当前状态快照
local state = ext.get_system_state("process")
if state and state.supported then
for _, app in ipairs(state.apps) do
ext.log(app.name .. " (" .. app.instance_count .. " 个实例)")
end
end

local focus = ext.get_system_state("window_focus")
if focus and focus.supported and focus.current then
ext.log("当前焦点: " .. (focus.current.app_name or "?") .. " - " .. (focus.current.window_title or ""))
end

接收变化事件

声明对应权限并实现 on_system_state_changed

manifest.json
{
"permissions": ["system:process", "system:window-focus"]
}
function plugin.on_system_state_changed(topic, data)
if topic == "process" then
-- data.apps: 完整的 {name, instance_count} 列表
-- data.changes: {name, previous_instance_count, current_instance_count} 列表
for _, change in ipairs(data.changes) do
if change.current_instance_count > change.previous_instance_count then
ext.log(change.name .. " 已启动")
elseif change.current_instance_count == 0 then
ext.log(change.name .. " 已关闭")
end
end
elseif topic == "window_focus" then
-- data.reason: "snapshot"、"foreground_changed" 或 "title_changed"
-- data.current: {app_name?, window_title?} 或 nil
-- data.previous: {app_name?, window_title?} 或 nil
if data.current then
ext.log("窗口焦点 → " .. (data.current.app_name or ""))
end
end
end

完整示例:协议网桥

local plugin = {}

local conn = nil
local devices = {}

function plugin.on_start()
ext.log("网桥扩展正在启动")

-- 启动外部服务
local handle = ext.spawn_process("bridge-server.exe", {"--port", "9000"}, {
hidden = true
})

-- 等待服务器启动
ext.sleep(2000)

-- 连接
conn = ext.tcp_connect("127.0.0.1", 9000, 5000)
if not conn then
ext.error("连接网桥服务器失败")
return
end

-- 发现并注册设备
discover_devices()
end

function discover_devices()
ext.tcp_send(conn, "LIST_DEVICES\n")
local response = ext.tcp_recv(conn, 4096, 3000)
if not response then return end

-- 解析响应并注册每个设备
for name, leds in response:gmatch("(%S+):(%d+)") do
local port = ext.register_device({
controller_port = "bridge://" .. name,
manufacturer = "Bridge",
model = name,
controller_id = "extension.my_bridge",
device_type = "light",
outputs = {{
id = "main",
name = "Main",
leds_count = tonumber(leds),
output_type = "linear"
}}
})
devices[port] = {name = name, conn = conn}
end
end

function plugin.on_device_frame(port, outputs)
local dev = devices[port]
if not dev then return end

local colors = outputs["main"]
if colors then
local packet = "SET_LEDS:" .. dev.name .. ":" .. table.concat(colors, ",")
ext.tcp_send(conn, packet .. "\n")
end
end

function plugin.on_scan_devices()
discover_devices()
end

function plugin.on_stop()
-- 注销所有设备
for port in pairs(devices) do
ext.remove_extension_device(port)
end
-- 关闭连接
if conn then ext.tcp_close(conn) end
end

return plugin

完整 API 请参阅扩展 API 参考