@aiot-nexus/plugin-sdk
v3.2.0
Published
Types, PluginFrame client, and CLI for AIoT Nexus independent plugins.
Readme
@aiot-nexus/plugin-sdk
当前 SDK 包版本为
3.2.0,Framework API 为3.2.0,manifest schema 保持1。Framework 3 将可信插件的mainEntry直接加载到 Nexus Electron 主进程;它不是安全沙箱。PluginFrame iframe 仍保持 sandbox、无 Node/Electron 访问。宿主运行模型见 Framework v3 架构。
本包提供公开 TypeScript 契约、PluginFrame 浏览器客户端和 nexus-plugin CLI。v2 项目升级前请先阅读 v2 到 v3 迁移说明。完整开发流程见 独立插件开发指南。
兼容基线
| 项目 | 固定值 |
| --- | --- |
| SDK | 3.2.0 |
| Framework API | 3.2.0 |
| Manifest schema | 1 |
| Node 安装和构建 | >=16.20.0 <17 |
| Electron | 22.3.27 |
| TypeScript | 4.9.4 |
| 目标系统 | Windows 7 x64 |
不要为了新 Electron API、类型或依赖升级这些兼容锁。插件原生模块必须为 Electron 22 ABI 重建。
CLI
nexus-plugin info [--json]
nexus-plugin validate <plugin-package-directory>
nexus-plugin create <plugin-id> --template minimal|sqlite --output <parent-directory>
nexus-plugin upgrade [plugin-project-directory] [--check] [--sdk-source <version-or-local-tgz>]
nexus-plugin dev [plugin-project-directory]
nexus-plugin dev:web [plugin-project-directory]
nexus-plugin dev:nexus [plugin-project-directory] --host <nexus.exe-or-directory>仓库内 SDK 命令:
npm run build
npm test
npm run pack:sdk
npm run publish:sdkpack:sdk 把离线 tgz 写入 plugin-sdk/package/。publish:sdk 的 prepack 会运行测试、构建和公开文件清单验证。安装阶段和 CLI 启动阶段会非阻塞检查 SDK 更新;设置 NEXUS_PLUGIN_SDK_UPDATE_CHECK=0 或 NO_UPDATE_NOTIFIER=1 可关闭检查。检查失败不会阻断安装、构建或调试,版本通知本身不会修改依赖;只有显式执行 upgrade 才会进入升级事务。
升级插件工程 SDK
标准插件工程的 nexus-plugin 来自本地 @aiot-nexus/plugin-sdk,因此 CLI 和工程 SDK 是同一个依赖。旧工程应由目标版本的 CLI 执行升级,不能只手工修改 package.json,因为 lockfile 和 node_modules 仍可能保留旧版本。
registry 升级与只读预检:
npx --package=@aiot-nexus/[email protected] nexus-plugin upgrade . --check
npx --package=@aiot-nexus/[email protected] nexus-plugin upgrade .发布前或本地 tgz:
npx --package=./aiot-nexus-plugin-sdk-3.2.0.tgz nexus-plugin upgrade . \
--sdk-source ./aiot-nexus-plugin-sdk-3.2.0.tgzCI 可以追加 --json 获得 up-to-date、upgrade-available、upgraded、rolled-back 或 blocked。--allow-major 只解除 major 保护,不会改写业务源码;跨 major 必须先阅读迁移指南。--skip-project-checks 只跳过项目 typecheck/build,安装后的 SDK、package 和 lockfile 一致性仍会验证。
升级器直接修改的范围只有 SDK 依赖、package-lock.json、node_modules 和命令管理的 SDK tgz。本地包会保存为 vendor/aiot-nexus-plugin-sdk-<version>.tgz,下一次本地升级会安全清理上一份由命令管理的版本化归档。Electron、TypeScript、@types/node 和 Node engine 只检查不修改;manifest、模板、README、构建脚本和业务依赖不会被覆盖。
修改前,命令会在项目根目录创建 .nexus-plugin-upgrade-recovery/,持久备份 package、lock、受管 tgz 和标准 dist/<pluginId>,并写入 recovery.json 阶段记录。成功或确定性回滚后该目录自动删除;硬中断、回滚重装失败或清理失败时保留,后续 upgrade 会停止并要求先按 journal 恢复。不要在项目恢复并完成验证前删除它。
安装或项目验证失败时恢复备份,并在原 lockfile 存在时运行 npm ci。没有原 lockfile 时会运行 npm install 并恢复原 SDK,但无法证明完整传递依赖树与升级前一致,因此返回 blocked 并保留恢复数据。标准脚手架 build 的 dist/<pluginId> 在回滚范围内;自定义 typecheck/build 若修改其他路径,这些副作用不属于升级事务,项目必须自行管理。
SDK 3.0.x / 3.1.x 插件可以由 3.2.x 宿主继续加载。升级到 SDK 3.2.0 只提供主题能力和新的契约;插件必须引用 --nexus-color-* 或读取 context.theme 才会使用宿主主题色。没有引用新变量的插件保持原有颜色;需要自定义主题时传入 autoApplyHostTheme: false。原有 hostApiVersion: 3.0.0 可以保持不变,只有实际使用 showHostStatusBar 时才需要声明 hostApiVersion: 3.1.0 或更高。SDK 3.1.1 修复 registry 升级命令丢失 npm 包名的问题,作为历史兼容说明保留。
创建插件
nexus-plugin create my-frame-plugin \
--template minimal \
--output . \
--display-name "My Frame Plugin" \
--installminimal提供最小 PluginFrame、同步生命周期和原生 Electron 文件对话框示例。sqlite提供[email protected]通用数据库浏览器。--sdk-source可指定严格版本、@aiot-nexus/[email protected]或本地.tgz。- 默认不安装依赖;
--install执行npm install。 - 非空目标目录会被拒绝,不会覆盖现有项目。
- 两套模板都支持中英文、亮暗主题和同一 iframe 内的 context 更新。
离线示例:
npm run pack:sdk
nexus-plugin create my-frame-plugin --sdk-source ./package/aiot-nexus-plugin-sdk-3.2.0.tgz --install生成插件后,可以在 src/manifest.json 的业务入口中静态控制 Nexus 宿主状态栏。相关 manifest 片段如下,其余必填字段沿用脚手架生成值:
{
"hostApiVersion": "3.2.0",
"businessEntries": [
{
"businessId": "my-frame-plugin",
"titleKey": "plugin.displayName",
"iconKey": "extension",
"section": "home.main",
"order": 50,
"component": "PluginFrame",
"pageRoute": "my-frame-plugin",
"showHostStatusBar": false
}
]
}仅显式设置为 false 时隐藏。字段省略或设置为 true 时显示;离开该业务入口后,宿主状态栏自动恢复。该字段要求 hostApiVersion >= 3.1.0。
Framework 3.2 mainEntry
mainEntry 是可信 Electron 主进程代码,可直接导入 Electron、Node 内置模块和插件本地依赖:
import { dialog } from "electron";
import { promises as fs } from "fs";
import type { PluginMainActivationContext } from "@aiot-nexus/plugin-sdk";
const maxTextBytes = 1024 * 1024;
export function activate(context: PluginMainActivationContext): void {
const registered = context.host.registerRendererMethod("editor.chooseText", async () => {
const selection = await dialog.showOpenDialog({
properties: ["openFile"],
filters: [{ name: "Text", extensions: ["txt", "md"] }]
});
if (selection.canceled || selection.filePaths.length === 0) {
return { canceled: true };
}
const contentResult = await readTextFile(selection.filePaths[0]);
return contentResult.ok
? { canceled: false, content: contentResult.value }
: contentResult;
});
if (!registered.ok) {
throw new Error(registered.error.message);
}
}
export function deactivate(): void {
// 同步注销 ipcMain/protocol/shortcut/timer,并关闭窗口、数据库和子进程。
}
async function readTextFile(filePath: string) {
const file = await fs.open(filePath, "r");
try {
const stat = await file.stat();
if (!stat.isFile() || stat.size > maxTextBytes) {
return failure("FileTooLarge", "Select a regular file no larger than 1 MiB.");
}
const buffer = Buffer.alloc(maxTextBytes + 1);
let bytesRead = 0;
while (bytesRead < buffer.length) {
const chunk = await file.read(buffer, bytesRead, buffer.length - bytesRead, bytesRead);
if (chunk.bytesRead === 0) break;
bytesRead += chunk.bytesRead;
}
return bytesRead > maxTextBytes
? failure("FileTooLarge", "File grew beyond 1 MiB while reading.")
: { ok: true as const, value: buffer.subarray(0, bytesRead).toString("utf8") };
} finally {
await file.close();
}
}
function failure(code: string, message: string) {
return { ok: false as const, error: { code, message } };
}固定契约:
activate(context)和deactivate(reason)必须同步完成,不能声明为async,也不能返回 Promise。- 声明为
async的生命周期函数会在调用前被拒绝;普通函数若返回 Promise,函数体已经执行,但 Framework 不等待该 Promise,并将本次生命周期判定为不受支持。 context.host.*直接返回NexusResult<T>,不需要await。- 注册的 operation handler 可以返回 Promise,可等待异步文件对话框、网络或其他用户交互。
- operation 默认没有业务超时。浏览器客户端
requestTimeoutMs默认是0;20 秒仅用于 iframe 初始化握手。 - 同步 Electron API 也能调用,但
showOpenDialogSync()、同步文件 I/O、长 CPU 任务会阻塞 Nexus 主进程、所有窗口和插件,应优先使用异步版本。 - Framework 不跟踪插件直接创建的 Electron/Node 资源。插件必须在
deactivate中自行清理。 - Node 模块图和原生模块不能安全热卸载。已加载
mainEntry、依赖、原生模块或相关 manifest 变化必须重启 Nexus。
Framework 3 不再提供 context.host.electron Facade,也没有 electronHostControl 权限。manifest 权限只描述 Host API 能力,不限制可信 mainEntry 直接调用 Electron/Node。SDK 保留 createPluginElectronIpcChannel() 和 createPluginElectronProtocolScheme() 作为命名约定帮助函数;宿主不会自动隔离或清理对应注册。
原生文件、窗口、IPC 与 Monaco 示例见 Electron/Node 示例,支持矩阵和风险边界分别见 兼容矩阵 与 安全边界。
Renderer 边界
PluginFrame iframe 不能直接获得 Node、Electron、数据库对象、任意文件路径或完整 ipcRenderer。Renderer 应通过 client.invokeMain() 调用经过命名和输入校验的业务 operation。
关键 CSP:
script-src 'self'; style-src 'self' 'unsafe-inline'; worker-src 'self' blob:这允许 Monaco 运行时注入样式和同源/Blob Worker,但仍阻止内联脚本、远程脚本和 Node iframe 访问。脚本、Monaco、语言包、字体和 worker 必须随插件作为同源资源交付。
createPluginFrameClient() 的首次初始化默认等待 20 秒。业务请求默认不设超时;只有插件显式传入正数 requestTimeoutMs 时客户端才会主动超时。进度、取消和恢复仍建议使用 taskId + getStatus + cancel,但这是任务 UX 和资源管理策略,不是 worker 存活要求。通信契约见 插件 IPC 与事件通知示例。
宿主主题色
Framework 3.2 在 PluginRendererContext.theme 中提供 light 和 dark 两套语义 palette。SDK frame client 默认把当前 palette 的固定白名单字段写入 --nexus-color-*,并设置 data-nexus-theme="light|dark";themeMode: "system" 会跟随系统主题变化。SDK 不会修改插件已有的 --bg、--accent、data-theme 或 color-scheme。
SDK 升级本身只提供能力。插件需要在 CSS 中引用 var(--nexus-color-primary, #8fc31f) 等变量,或从 context.theme?.dark.primary 读取值,才会使用宿主主题色。旧宿主或缺少 theme 的上下文仍可正常初始化。自定义主题插件可以显式使用 createPluginFrameClient({ autoApplyHostTheme: false }),此时不注入变量、不设置宿主主题标记,但仍可手动读取 context.theme。
SQLite 模板
sqlite 模板默认打开 pluginDataDirectory/plugin.db 并初始化 plugin_records。工具栏“打开数据库”由 mainEntry 直接调用 dialog.showOpenDialog();iframe 只接收数据库信息和参数化 operation。
模板支持:
- 最近外部数据库路径原子持久化和失效回退。
- 普通用户表发现、动态 schema、分页和 25/50/100 页大小。
- rowid、单/复合主键与
WITHOUT ROWID定位。 - default/null/value 动态插入和 no-change/null/value 更新。
- 精确 INTEGER、有限 REAL、NUMERIC 文本兼容和 BLOB 十六进制。
- TEXT 16 KiB 预览、BLOB 字节数和前 32 字节预览。
- 单字段 1 MiB、单次提交 4 MiB 上限。
模板不暴露任意 SQL、DDL、ATTACH、路径输入或 SQL 控制台。它刻意不依赖 Quasar,因此数据库网格使用原生 <table>;这是独立、依赖最小化 SDK 模板对宿主 QTable 规则的明确例外。
调试模式
| 命令 | mainEntry | Electron/Node | 用途 |
| --- | --- | --- | --- |
| npm run dev:web | 不执行 | 不可用 | 只调试 Renderer;invokeMain 返回 PluginCapabilityUnsupported。 |
| npm run dev | Electron 调试宿主主进程直接加载 | 可用 | 调试真实生命周期、对话框、SQLite 和插件窗口。 |
| npm run dev:debug | 同 dev | 可用 | Electron 主进程以 --inspect-brk=9229 启动。 |
| npm run dev:nexus -- --host ... | 正式 Nexus 主进程直接加载 | 可用 | 最终集成和监听更新。 |
Electron 调试壳仅保留 context 工具栏;插件 iframe 无边距、无边框,填满剩余窗口,不设置最小宽高,也不根据内容上报限制尺寸。Renderer DevTools 默认打开。
VS Code 模板提供 Debug Nexus plugin 和 Attach Nexus plugin main process (9229)。Renderer 断点在 DevTools Sources 中设置。
dev:nexus 更新规则:
| 修改范围 | 处理方式 |
| --- | --- |
| src/renderer/** | 只构建 Renderer 并重载 iframe,Nexus PID 不变。 |
| 无 mainEntry 插件的安全 manifest 展示/贡献字段 | 原子替换 manifest 并重扫,PID 不变。 |
| 有 mainEntry 插件的 manifest 变化 | 完整构建并重启 Nexus。 |
| src/main.*、依赖、原生模块、构建脚本 | 完整构建并重启 Nexus。 |
构建失败保留旧部署;新宿主启动或就绪失败会恢复旧包和旧宿主。--inspect / --inspect-brk 调试的是 Nexus Electron 主进程,因此会同时暂停或暴露宿主代码。按 Ctrl+C 时 CLI 关闭自己启动的宿主。
发布前检查
hostApiVersion使用当前3.2.0(或按实际能力保持旧版本),manifest schema 仍为1。- 业务入口的
showHostStatusBar可静态声明是否保留宿主状态栏;省略时默认保留,显式使用时要求hostApiVersion >= 3.1.0。 activate/deactivate同步,operation handler 才使用异步。- Renderer 只使用窄口径 operation,所有输入在 mainEntry 再校验。
- 窗口保持
contextIsolation: true、sandbox: true、nodeIntegration: false、webSecurity: true。 - IPC、protocol、快捷键、菜单和文件路径使用插件命名空间并检测冲突。
deactivate关闭窗口、Session 监听、IPC、protocol、快捷键、定时器、数据库和子进程。- 没有同步长任务、无限载荷、任意命令/SQL/路径透传或日志敏感信息。
- 主题集成明确引用
--nexus-color-*/context.theme,或显式关闭autoApplyHostTheme。 - 运行
npm test、插件构建、正式 Nexus 集成和 Windows 7 真机验收。
