@aiot-nexus/plugin-sdk 2.0.0 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -3
- package/dev-host/host.html +3 -0
- package/dev-host/main.js +30 -1
- package/dev-host/plugin-host.js +14 -2
- package/dist/browser/frame-client.mjs +1 -1
- package/dist/src/browser/frame-client.js +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +1 -1
- package/package.json +1 -1
- package/templates/minimal/README.md.tmpl +2 -0
- package/templates/sqlite/README.md.tmpl +2 -0
- package/templates/sqlite/src/renderer/index.html +1 -1
- package/templates/sqlite/src/renderer/plugin.css +14 -1
package/README.md
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
# @aiot-nexus/plugin-sdk
|
|
2
2
|
|
|
3
|
+
> 版本适用性:当前包版本为 `2.0.2`,只面向 Framework API `2.0.0` 的 v2 worker/RPC 模型。1.x SDK 和同步 Host API 已停止适用,但其历史语义保留在本文末尾和 [v1 到 v2 迁移说明](../docs/plugin-framework-v1-v2-migration.md);2.x 不提供 1.x 运行模型兼容层。
|
|
4
|
+
|
|
3
5
|
AIoT Nexus 独立插件开发包,包含公开 TypeScript 契约、PluginFrame 浏览器客户端和 `nexus-plugin` 命令行工具。
|
|
4
6
|
|
|
7
|
+
通信边界、`postMessage` 消息格式、`invokeMain` 请求/响应、分层超时和 worker 终止策略、长任务轮询示例见仓库文档:[插件 IPC 与事件通知示例](../docs/plugin-ipc-event-notification-example.md)。当前 SDK 提供 `onContextChanged()` 用于语言、主题和启动 context 更新,但不提供通用业务事件订阅 API。
|
|
8
|
+
|
|
5
9
|
## 环境
|
|
6
10
|
|
|
7
11
|
- Node.js `>=16.20.0 <17`
|
|
@@ -35,7 +39,7 @@ SDK 提供两层非阻塞版本检查:
|
|
|
35
39
|
- `nexus-plugin` 启动时先读取缓存。缓存新鲜期为 24 小时;缓存缺失或过期时在后台检查,不等待网络,不改变命令退出码。检测到普通更新时会显示:
|
|
36
40
|
|
|
37
41
|
```text
|
|
38
|
-
npm notice Update available for @aiot-nexus/plugin-sdk: 2.0.
|
|
42
|
+
npm notice Update available for @aiot-nexus/plugin-sdk: 2.0.2 -> 2.1.0
|
|
39
43
|
npm notice Run npm install --save-dev @aiot-nexus/plugin-sdk@latest
|
|
40
44
|
```
|
|
41
45
|
|
|
@@ -64,7 +68,7 @@ nexus-plugin create my-frame-plugin \
|
|
|
64
68
|
- 目标目录非空时命令以返回码 `2` 拒绝覆盖。生成或安装失败返回 `1`。
|
|
65
69
|
- 两套模板均读取宿主 `PluginRendererContext.language` 和 `themeMode`,通过 `onContextChanged` 在同一 iframe 内响应运行时语言与主题切换,并内置 `zh-CN` / `en-US` renderer 文案;manifest 使用相同文化资源本地化宿主入口。模板页面自身不提供语言或主题选择控件。
|
|
66
70
|
|
|
67
|
-
默认来源从 npm registry 安装。离线开发时先执行 `npm run pack:sdk`,再通过 `--sdk-source /path/to/aiot-nexus-plugin-sdk-2.0.
|
|
71
|
+
默认来源从 npm registry 安装。离线开发时先执行 `npm run pack:sdk`,再通过 `--sdk-source /path/to/aiot-nexus-plugin-sdk-2.0.2.tgz` 指定本地包;CLI 会把本地包一并放入生成工程。
|
|
68
72
|
|
|
69
73
|
## 独立调试
|
|
70
74
|
|
|
@@ -117,6 +121,21 @@ npm run dev:nexus -- --host D:\prj\tmp\aiot-nexus\dist\electron\nexus-win32-x64\
|
|
|
117
121
|
| --- | --- |
|
|
118
122
|
| `src/renderer/**` | 只运行 `build:web` 并替换 renderer;宿主 PID 不变,iframe 重新完成 `ready/init`。 |
|
|
119
123
|
| `src/manifest.json` 的展示、贡献点等安全字段 | 构建 renderer、原子替换 manifest 后重扫插件目录;宿主 PID 不变。 |
|
|
120
|
-
| `src/main.ts
|
|
124
|
+
| `src/main.ts`、依赖、原生模块或 worker 相关 manifest 字段 | 完整构建成功后先停止插件 worker,原子替换完整插件包,再在同一 Nexus 进程中启动新 worker;宿主 PID 不变。 |
|
|
121
125
|
|
|
122
126
|
更新构建或 worker 重载失败时保留上一个可运行部署,并回滚插件包后重新激活旧 worker。`mainEntry` 不通过清理 `require.cache` 热替换;每次包更新都执行 `deactivate`、替换包和新的 worker 激活,但不重启整个 Nexus。默认打开 Renderer DevTools;可传 `--no-devtools`。`--inspect[=<port>]` 和 `--inspect-brk[=<port>]` 调试的是插件 worker,贡献点选择支持 `--surface` 与 `--contribution-id`。按 `Ctrl+C` 停止 CLI 时,CLI 会先请求插件 worker 关闭,再关闭其启动的正式宿主。
|
|
127
|
+
|
|
128
|
+
## v1 SDK 历史说明(不适用于当前包)
|
|
129
|
+
|
|
130
|
+
SDK 1.x 对应 Framework API `1.0.0`:`mainEntry` 在 Electron 主进程中加载,Host API 直接返回同步 `NexusResult<T>`,完整插件包变化需要重启 Nexus。旧离线包名使用 `aiot-nexus-plugin-sdk-1.1.1.tgz`,旧调试目标是 `Attach Nexus plugin main (9229)`。
|
|
131
|
+
|
|
132
|
+
| v1 项目 | 历史行为 | v2 替代 |
|
|
133
|
+
| --- | --- | --- |
|
|
134
|
+
| `npm run dev` | Electron 调试宿主在主进程加载 `mainEntry` | 调试宿主创建独立插件 worker |
|
|
135
|
+
| `npm run dev:debug` | `--inspect-brk=9229` 暂停 Electron 主进程 | inspect 端口连接插件 worker,宿主 UI 可独立启动 |
|
|
136
|
+
| `npm run dev:nexus` 完整包变化 | 退出旧 Nexus、替换包、启动新 Nexus,PID 改变 | 停止目标 worker、替换包、启动新 worker,Nexus PID 不变 |
|
|
137
|
+
| Host API 调用 | 直接获得 `NexusResult<T>` | `await` `Promise<NexusResult<T>>` |
|
|
138
|
+
| Renderer-only 调试 | 不执行 `mainEntry`,`invokeMain` 返回 unsupported | 该限制在 v2 继续成立 |
|
|
139
|
+
| 更新失败 | 恢复旧包并重新启动宿主 | 恢复旧包和旧 worker,不重启 Nexus |
|
|
140
|
+
|
|
141
|
+
这些约定只用于阅读旧插件和历史报告。迁移到当前 SDK 时必须把 manifest 升到 2.x、异步等待 Host API、实现资源关闭,并改用插件 worker 调试和生命周期;不能只修改 npm 包版本而保留同步调用。
|
package/dev-host/host.html
CHANGED
|
@@ -115,6 +115,8 @@
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
.frame-shell {
|
|
118
|
+
display: flex;
|
|
119
|
+
flex-direction: column;
|
|
118
120
|
flex: 1 1 auto;
|
|
119
121
|
min-width: 0;
|
|
120
122
|
padding: 12px;
|
|
@@ -123,6 +125,7 @@
|
|
|
123
125
|
|
|
124
126
|
iframe {
|
|
125
127
|
display: block;
|
|
128
|
+
flex: 1 1 auto;
|
|
126
129
|
width: 100%;
|
|
127
130
|
min-height: 360px;
|
|
128
131
|
border: 1px solid var(--border);
|
package/dev-host/main.js
CHANGED
|
@@ -16,6 +16,7 @@ const pluginPermissions = ["businessLog", "settings", "filesystem", "udp", "seri
|
|
|
16
16
|
const implementedPermissions = new Set(["businessLog", "settings"]);
|
|
17
17
|
const routeGuardPermissions = new Set(["filesystem", "udp", "serial", "dialog"]);
|
|
18
18
|
const workerProtocolVersion = 2;
|
|
19
|
+
const workerOperationTimeoutMs = 15000;
|
|
19
20
|
|
|
20
21
|
protocol.registerSchemesAsPrivileged([
|
|
21
22
|
{
|
|
@@ -263,11 +264,30 @@ class PluginWorkerClient {
|
|
|
263
264
|
}
|
|
264
265
|
const requestId = this.nextRequestId("invoke");
|
|
265
266
|
return new Promise((resolve) => {
|
|
266
|
-
|
|
267
|
+
const pending = {
|
|
268
|
+
kind: "invoke",
|
|
269
|
+
resolve,
|
|
270
|
+
timer: setTimeout(() => {
|
|
271
|
+
if (this.pending.get(requestId) !== pending) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
this.pending.delete(requestId);
|
|
275
|
+
pending.timer = undefined;
|
|
276
|
+
this.ready = false;
|
|
277
|
+
resolve(fail("PluginOperationTimeout", "Plugin worker operation timed out.", {
|
|
278
|
+
pluginId: this.configuration.manifest.pluginId,
|
|
279
|
+
operation: payload.operation,
|
|
280
|
+
timeoutMs: workerOperationTimeoutMs
|
|
281
|
+
}));
|
|
282
|
+
this.kill();
|
|
283
|
+
}, workerOperationTimeoutMs)
|
|
284
|
+
};
|
|
285
|
+
this.pending.set(requestId, pending);
|
|
267
286
|
try {
|
|
268
287
|
this.post({ protocolVersion: workerProtocolVersion, type: "invoke", requestId, payload });
|
|
269
288
|
} catch (error) {
|
|
270
289
|
this.pending.delete(requestId);
|
|
290
|
+
clearPendingTimer(pending);
|
|
271
291
|
resolve(fail("PluginWorkerExited", "Plugin worker request could not be sent.", errorMessage(error)));
|
|
272
292
|
}
|
|
273
293
|
});
|
|
@@ -358,6 +378,7 @@ class PluginWorkerClient {
|
|
|
358
378
|
return;
|
|
359
379
|
}
|
|
360
380
|
this.pending.delete(message.requestId);
|
|
381
|
+
clearPendingTimer(pending);
|
|
361
382
|
if (pending.kind === "shutdown") {
|
|
362
383
|
clearTimeout(this.shutdownTimer);
|
|
363
384
|
this.shutdownTimer = undefined;
|
|
@@ -398,6 +419,7 @@ class PluginWorkerClient {
|
|
|
398
419
|
this.failStart(new Error(result.error.message));
|
|
399
420
|
}
|
|
400
421
|
for (const pending of this.pending.values()) {
|
|
422
|
+
clearPendingTimer(pending);
|
|
401
423
|
pending.resolve(result);
|
|
402
424
|
}
|
|
403
425
|
this.pending.clear();
|
|
@@ -445,6 +467,13 @@ class PluginWorkerClient {
|
|
|
445
467
|
}
|
|
446
468
|
}
|
|
447
469
|
|
|
470
|
+
function clearPendingTimer(pending) {
|
|
471
|
+
if (pending && pending.timer) {
|
|
472
|
+
clearTimeout(pending.timer);
|
|
473
|
+
pending.timer = undefined;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
448
477
|
function readWorkerMessage(message) {
|
|
449
478
|
return isRecord(message) && Object.prototype.hasOwnProperty.call(message, "data") ? message.data : message;
|
|
450
479
|
}
|
package/dev-host/plugin-host.js
CHANGED
|
@@ -16,6 +16,7 @@ if (!port) {
|
|
|
16
16
|
|
|
17
17
|
const operations = new Map();
|
|
18
18
|
const pendingHostRequests = new Map();
|
|
19
|
+
const inFlightOperations = new Set();
|
|
19
20
|
let initialized = false;
|
|
20
21
|
let ready = false;
|
|
21
22
|
let shuttingDown = false;
|
|
@@ -56,7 +57,7 @@ async function handleMessage(message) {
|
|
|
56
57
|
return;
|
|
57
58
|
}
|
|
58
59
|
if (message.type === "invoke") {
|
|
59
|
-
await
|
|
60
|
+
await invokeTracked(message);
|
|
60
61
|
return;
|
|
61
62
|
}
|
|
62
63
|
if (message.type === "shutdown") {
|
|
@@ -162,12 +163,23 @@ async function invoke(message) {
|
|
|
162
163
|
}
|
|
163
164
|
}
|
|
164
165
|
|
|
166
|
+
async function invokeTracked(message) {
|
|
167
|
+
const operation = invoke(message);
|
|
168
|
+
inFlightOperations.add(operation);
|
|
169
|
+
try {
|
|
170
|
+
await operation;
|
|
171
|
+
} finally {
|
|
172
|
+
inFlightOperations.delete(operation);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
165
176
|
async function shutdown(message) {
|
|
166
177
|
if (shuttingDown || !isWorkerRequestId(message.requestId)) {
|
|
167
178
|
return;
|
|
168
179
|
}
|
|
169
180
|
shuttingDown = true;
|
|
170
181
|
try {
|
|
182
|
+
await Promise.allSettled(Array.from(inFlightOperations));
|
|
171
183
|
const deactivate = pluginModule && (typeof pluginModule.deactivate === "function"
|
|
172
184
|
? pluginModule.deactivate
|
|
173
185
|
: pluginModule.default && typeof pluginModule.default.deactivate === "function"
|
|
@@ -262,7 +274,7 @@ function createHostApi() {
|
|
|
262
274
|
}
|
|
263
275
|
|
|
264
276
|
function requestHost(method, params) {
|
|
265
|
-
if (exiting
|
|
277
|
+
if (exiting) {
|
|
266
278
|
return Promise.resolve(fail("PluginWorkerExited", "Plugin worker is stopping."));
|
|
267
279
|
}
|
|
268
280
|
const requestId = pluginId + "-host-" + String(++requestSequence);
|
|
@@ -4,7 +4,7 @@ const readyMessageType = "nexus-plugin:ready";
|
|
|
4
4
|
const requestMessageType = "nexus-plugin:request";
|
|
5
5
|
const responseMessageType = "nexus-plugin:response";
|
|
6
6
|
const resizeMessageType = "nexus-plugin:resize";
|
|
7
|
-
const defaultRequestTimeoutMs =
|
|
7
|
+
const defaultRequestTimeoutMs = 20000;
|
|
8
8
|
export class PluginFrameClientError extends Error {
|
|
9
9
|
constructor(code, message) {
|
|
10
10
|
super(message);
|
|
@@ -7,7 +7,7 @@ const readyMessageType = "nexus-plugin:ready";
|
|
|
7
7
|
const requestMessageType = "nexus-plugin:request";
|
|
8
8
|
const responseMessageType = "nexus-plugin:response";
|
|
9
9
|
const resizeMessageType = "nexus-plugin:resize";
|
|
10
|
-
const defaultRequestTimeoutMs =
|
|
10
|
+
const defaultRequestTimeoutMs = 20000;
|
|
11
11
|
class PluginFrameClientError extends Error {
|
|
12
12
|
constructor(code, message) {
|
|
13
13
|
super(message);
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import pluginManifestSchema from "../.generated/plugin-manifest.schema.json";
|
|
2
|
-
export declare const PLUGIN_SDK_VERSION = "2.0.
|
|
2
|
+
export declare const PLUGIN_SDK_VERSION = "2.0.2";
|
|
3
3
|
export { pluginManifestSchema };
|
|
4
4
|
export * from "../.generated/plugin-framework-version";
|
|
5
5
|
export * from "./types";
|
package/dist/src/index.js
CHANGED
|
@@ -20,7 +20,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
20
20
|
exports.pluginManifestSchema = exports.PLUGIN_SDK_VERSION = void 0;
|
|
21
21
|
const plugin_manifest_schema_json_1 = __importDefault(require("../.generated/plugin-manifest.schema.json"));
|
|
22
22
|
exports.pluginManifestSchema = plugin_manifest_schema_json_1.default;
|
|
23
|
-
exports.PLUGIN_SDK_VERSION = "2.0.
|
|
23
|
+
exports.PLUGIN_SDK_VERSION = "2.0.2";
|
|
24
24
|
__exportStar(require("../.generated/plugin-framework-version"), exports);
|
|
25
25
|
__exportStar(require("./types"), exports);
|
|
26
26
|
__exportStar(require("./browser/frame-client"), exports);
|
package/package.json
CHANGED
|
@@ -21,6 +21,8 @@ The renderer calls the plugin-worker `demo.echo` operation through `@aiot-nexus/
|
|
|
21
21
|
|
|
22
22
|
The renderer follows `context.language` and `context.themeMode` from Nexus, subscribes to in-place context updates without reloading the iframe, and includes `en-US` and `zh-CN` message catalogs. The plugin page has no language or theme selector; the controls shown by `npm run dev` belong to the debug host and simulate Nexus context changes. Add or edit renderer messages in `src/renderer/app.ts`; edit host-shell titles and descriptions in `src/manifest.json` resources.
|
|
23
23
|
|
|
24
|
+
The current SDK has no generic business-event subscription API. For a long-running operation, register a fast `job.start` operation that returns a `taskId`, then expose `job.getStatus` and optional `job.cancel` for renderer polling. See the Nexus repository's plugin IPC and event notification example for the full contract and code.
|
|
25
|
+
|
|
24
26
|
Use `npm run dev:web` for renderer-only UI work in the system browser. It runs `build:web`, writes an isolated package under `.nexus-plugin-dev/web-package/`, builds with source maps, serves only on `127.0.0.1:4173`, and opens the browser automatically. It does not execute `mainEntry` or overwrite the deployable `dist` package. Language and theme controls still simulate Nexus context, while `invokeMain` is intentionally unavailable. Pass `-- --no-open` or `-- --port <port>` when needed; use `npm run dev` for Host API integration.
|
|
25
27
|
|
|
26
28
|
Use `npm run dev:nexus -- --host C:\path\to\nexus.exe` for live development against a compiled, packaged Nexus. The CLI atomically deploys this plugin to `<exeDir>/Plugins/__PLUGIN_ID__/`. Renderer changes rebuild and reload the iframe without changing the Nexus PID; safe manifest changes rescan contributions in the same process; `mainEntry`, dependency, native-module, or worker-sensitive manifest changes rebuild the full package, stop the old plugin worker, replace the package, and start a new worker while keeping the Nexus PID. Failed builds keep the previous deployment; a failed worker reload rolls the package back and restores the previous worker. Add `--no-devtools`, `--inspect[=<port>]`, `--inspect-brk[=<port>]`, `--surface`, or `--contribution-id` after the second `--` as needed.
|
|
@@ -21,6 +21,8 @@ The deployable plugin package is `dist/__PLUGIN_ID__/`. For a manual installatio
|
|
|
21
21
|
|
|
22
22
|
The renderer follows `context.language` and `context.themeMode` from Nexus, subscribes to in-place context updates without reloading the iframe, and includes `en-US` and `zh-CN` message catalogs. The plugin page has no language or theme selector; the controls shown by `npm run dev` belong to the debug host and simulate Nexus context changes. Static labels, CRUD status messages, action buttons, validation text, and date formatting switch together. Edit renderer messages in `src/renderer/app.ts`; edit host-shell titles and descriptions in `src/manifest.json` resources.
|
|
23
23
|
|
|
24
|
+
The current SDK has no generic business-event subscription API. For a long-running operation, register a fast `job.start` operation that returns a `taskId`, then expose `job.getStatus` and optional `job.cancel` for renderer polling. See the Nexus repository's plugin IPC and event notification example for the full contract and code.
|
|
25
|
+
|
|
24
26
|
Use `npm run dev:web` for renderer-only layout, styling, localization, and responsive checks in the system browser. It runs `build:web`, writes an isolated package under `.nexus-plugin-dev/web-package/`, and does not rebuild or load `better-sqlite3`, execute `mainEntry`, copy native runtime files, or overwrite the deployable `dist` package. It builds with source maps, serves only on `127.0.0.1:4173`, and opens the browser automatically. Database operations report unsupported; use `npm run dev` for SQLite and Host API integration. Pass `-- --no-open` or `-- --port <port>` when needed.
|
|
25
27
|
|
|
26
28
|
Use `npm run dev:nexus -- --host C:\path\to\nexus.exe` to run the plugin inside a compiled, packaged Nexus. The initial deployment and every full package replacement are atomic. Renderer-only and safe manifest updates keep the Nexus PID; `mainEntry`, dependency, native-module, or worker-sensitive manifest changes run the complete build (including the Electron 22.3.27 native rebuild), stop the old plugin worker, replace the package, and start a new worker in the same Nexus process. The database remains under the host-provided plugin data directory across worker replacements. Failed builds keep the previous deployment, and a failed worker reload rolls the package back and restores the previous worker. Add `--no-devtools`, `--inspect[=<port>]`, `--inspect-brk[=<port>]`, `--surface`, or `--contribution-id` after the second `--` as needed.
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
<p id="formError" class="error" hidden></p>
|
|
37
37
|
</section>
|
|
38
38
|
|
|
39
|
-
<section class="panel">
|
|
39
|
+
<section class="panel recent-rows-panel">
|
|
40
40
|
<div class="panel-heading">
|
|
41
41
|
<h2 data-i18n="section.recentRows">Recent Rows</h2>
|
|
42
42
|
<span id="statusLog" data-i18n="status.idle">Idle</span>
|
|
@@ -15,13 +15,15 @@
|
|
|
15
15
|
|
|
16
16
|
body {
|
|
17
17
|
margin: 0;
|
|
18
|
-
min-height:
|
|
18
|
+
min-height: 100vh;
|
|
19
19
|
background: #f3f5f7;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
.plugin-shell {
|
|
23
23
|
display: grid;
|
|
24
|
+
grid-template-rows: auto auto auto minmax(0, 1fr);
|
|
24
25
|
gap: 12px;
|
|
26
|
+
min-height: 100vh;
|
|
25
27
|
padding: 16px;
|
|
26
28
|
}
|
|
27
29
|
|
|
@@ -100,6 +102,17 @@ h2 {
|
|
|
100
102
|
padding: 14px 16px;
|
|
101
103
|
}
|
|
102
104
|
|
|
105
|
+
.recent-rows-panel {
|
|
106
|
+
display: flex;
|
|
107
|
+
flex-direction: column;
|
|
108
|
+
min-height: 0;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.recent-rows-panel .table-wrap {
|
|
112
|
+
flex: 1 1 auto;
|
|
113
|
+
min-height: 0;
|
|
114
|
+
}
|
|
115
|
+
|
|
103
116
|
.entry-form {
|
|
104
117
|
display: grid;
|
|
105
118
|
grid-template-columns: minmax(150px, 1fr) minmax(220px, 2fr) auto;
|