169 lines
8.4 KiB
Markdown
169 lines
8.4 KiB
Markdown
# 插件开发与排错
|
||
|
||
插件安装、授权、运行状态和日志都在 **插件面板** 中操作。
|
||
|
||
## 创建项目
|
||
|
||
在 NyaTerm 仓库根目录执行:
|
||
|
||
```sh
|
||
# 纯界面插件:查看选中终端的会话信息
|
||
pnpm plugin:create example.session temp/plugins/my-session --template ui
|
||
pnpm plugin:pack temp/plugins/my-session temp/plugins/my-session.nyap
|
||
|
||
# Rust 插件:界面调用后台,也提供原生命令
|
||
pnpm plugin:create example.hello temp/plugins/my-hello --template rust
|
||
node temp/plugins/my-hello/build.mjs
|
||
pnpm plugin:pack temp/plugins/my-hello temp/plugins/my-hello.nyap
|
||
```
|
||
|
||
创建器拒绝覆盖已有目录。项目包含 manifest、可用入口、SDK 和 README;
|
||
Rust 模板还包含编译脚本,编译后自动填入当前平台的可执行文件路径。
|
||
需要 Node 和 Rust 1.94 或更新版本。Rust SDK 使用本地路径依赖,移动项目时
|
||
应一起提供 SDK 或调整 Cargo.toml 中的路径。
|
||
|
||
将生成的 `.nyap` 安装到插件面板,检查权限并启用。UI 模板需先选中已连接终端;
|
||
Rust 模板可以打开界面点击运行,也可以直接执行插件卡片上的原生命令。
|
||
|
||
## 页面字体
|
||
|
||
宿主会将应用的界面字体栈通过 `NyaTerm.context.theme` 中的 `--font-sans`、
|
||
`--font-display` 同步到插件页面;修改设置时,已打开的页面也会收到更新,无需重载。
|
||
`--font-mono` 提供宿主的默认等宽字体栈。SDK 默认使用界面字体,表单控件继承字体;
|
||
插件自定义 CSS 应使用 `font-family: var(--font-sans, system-ui, sans-serif)`,
|
||
代码区域可使用 `var(--font-mono, monospace)`,避免写死 `system-ui`。
|
||
|
||
iframe 可以使用系统安装的字体,但不会继承主界面的 `@font-face`。
|
||
若使用未安装到系统的内置字体或其他 Web 字体,需要在插件包内提供字体文件并声明
|
||
`@font-face`。已有插件若写死字体,需要修改 CSS 并重新打包安装。
|
||
|
||
## 编写 Rust 后台
|
||
|
||
入口实现 `Plugin`,由 `Server::from_env()` 启动服务。SDK 负责握手、传输、
|
||
并发、作用域、超时和取消,无需手写 JSON-RPC:
|
||
|
||
```rust
|
||
#[async_trait]
|
||
impl Plugin for MyPlugin {
|
||
async fn call(&self, context: Context, method: &str, input: Value) -> Result<Value> {
|
||
match method {
|
||
"ui/session" => {
|
||
let session = context.host().session().await?;
|
||
context.log("info", "Session inspected").await?;
|
||
Ok(session)
|
||
}
|
||
_ => Err(RpcError::method_not_found(method)),
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
以上会话读取需在 manifest 中声明并获授 `session.read`;原生执行需 `native`。
|
||
UI 调用 `NyaTerm.backend("ui/session")`,contributions.commands 中的原生命令
|
||
则使用 manifest 声明的方法。不要向 stdout 打印日志,它专用于协议;使用
|
||
`Context::log` 或 stderr。不要记录口令、令牌、终端输入输出等敏感内容。
|
||
|
||
默认采用 JSONL。将 `backend.transport` 改为 `stdio-framed` 后,SDK 根据宿主
|
||
环境自动切换协议,支持二进制帧;界面流式订阅尚未提供。完整 API 和取消约定
|
||
见 [Rust SDK](../plugins/sdk/rust/nyaterm-plugin-sdk/README.md)。
|
||
|
||
## 固定远端 Probe 和共享 Monitor
|
||
|
||
Manifest v1 可选声明 `contributions.probes` 和 `contributions.monitors`,旧包不必增加字段。
|
||
以 GPU 为例:
|
||
|
||
```json
|
||
{
|
||
"contributions": {
|
||
"panels": [{ "id": "overview", "title": "GPU", "entry": "ui/index.html" }],
|
||
"probes": [
|
||
{
|
||
"id": "gpu-overview",
|
||
"title": "GPU probe",
|
||
"entry": "assets/probes/gpu.sh",
|
||
"timeoutMs": 15000
|
||
}
|
||
],
|
||
"monitors": [
|
||
{
|
||
"id": "gpu",
|
||
"title": "GPU",
|
||
"schema": "gpu.v1",
|
||
"method": "monitor/collect",
|
||
"panel": "overview"
|
||
}
|
||
]
|
||
},
|
||
"permissions": ["native", "remote.probe"]
|
||
}
|
||
```
|
||
|
||
每类最多 16 项,ID 不可与其他贡献重复。脚本必须位于包内 `assets/probes/*.sh`,为非空 UTF-8、无 NUL、最多 64 KiB;
|
||
超时范围 1–30 秒。Monitor 必须引用已声明面板,声明 `monitor/*` 方法、原生后台和 Probe;当前只支持 `gpu.v1`。
|
||
自动授权绑定审核版本;启用对话框展示脚本全文,更新/切换版本移除 `remote.probe` 授权。
|
||
宿主不保证第三方脚本只读,不允许两个有效插件同时提供同一种 Monitor schema。
|
||
|
||
后台每次 `monitor/collect` 调用 `context.host().remote_probe("gpu-overview").await?`,解析结果并返回 GPU JSON。
|
||
对应 RPC `host/remote/probe` **只接受** `{ "probeId": "gpu-overview" }`,返回 `{ stdout, stderr, exitStatus }`。
|
||
不能提供命令、参数、stdin 或其他会话 ID;宿主从当前包解析脚本,在所属 SSH 连接的新 exec 通道执行 `sh -s`。
|
||
stdout/stderr 合计限 1 MiB,超时或取消关闭该通道,不向交互终端输入命令、不关闭 SSH 连接。
|
||
执行前后检查版本、授权、窗口、会话及锁屏;网络设备和禁止远程探测的会话不可执行。
|
||
|
||
UI 只订阅结果,不直接启动后台采集循环:
|
||
|
||
```js
|
||
await NyaTerm.ready;
|
||
const monitor = await NyaTerm.monitoring.subscribe("gpu", (snapshot) => {
|
||
// revision, sessionId, overview, error, refreshing, paused
|
||
render(snapshot);
|
||
});
|
||
await monitor.refresh();
|
||
await monitor.unsubscribe();
|
||
```
|
||
|
||
宿主以插件/版本/窗口/会话/Monitor 为键共享任务,订阅返回初始快照,再推送递增 revision。
|
||
Bridge 仅向对应 iframe 发送所属订阅;未开放通用原生事件透传或 UI 调用 `monitor/*`。
|
||
宿主消费者使用 typed `pluginApi.subscribeMonitor/refreshMonitor/unsubscribeMonitor`;
|
||
`subscribePluginMonitor` 负责先监听再订阅、初始快照竞态、revision 去重及释放。
|
||
每个 iframe 最多 16 个订阅;宿主最多 256 订阅和 128 共享任务。
|
||
|
||
SDK Context 提供 `language`、主题变量和 `monitorIntervalSeconds`。使用 `onContextChange` 同步语言/主题,
|
||
仅间隔变化时重新订阅。主窗口订阅 25 分钟续期;iframe 作用域按现有机制重建。
|
||
闲置任务不持有请求租约;后台每次只处理一次采集,不保存已经结束请求的 Context。
|
||
|
||
`gpu.v1` 结构与 `RemoteGpuOverview` 相同,宿主拒绝未知字段并限制设备/进程数量和文本长度。
|
||
资产回填只能来自当前会话的 GPU 数据,没有任意连接修改接口。
|
||
完整示例与构建命令见 [GPU 插件](../plugins/examples/gpu-monitor/README.md)。
|
||
|
||
## 查看状态和日志
|
||
|
||
原生后台按需启动并复用进程。卡片显示未启动、启动中、运行中、已停止或运行异常;
|
||
禁用插件和仅界面插件有独立标识。RPC 方法返回错误时,只记录调用失败,后台仍可
|
||
继续运行;进程退出、握手或协议失败则显示运行异常。
|
||
|
||
点击 **查看日志** 可刷新、复制和清空。日志包括时间、版本、级别、来源和消息。
|
||
日志弹窗每秒刷新,卡片状态每 2.5 秒刷新;关闭插件管理面板后停止轮询。
|
||
每个插件最多保留 100 条、64 KiB 消息与来源/版本文本,单条消息最多 2 KiB。
|
||
stderr 每次启动最多采集约 64 KiB,超过后继续排空而不保留。
|
||
日志只在内存中保留,退出应用即清除,不进入备份或云同步。
|
||
宿主会遮蔽常见凭据和作用域令牌,但插件作者仍需自行避免写入敏感内容。
|
||
|
||
**停止后台** 保留启用状态和权限。普通调用可启动新进程,进程内计数等状态会重置;
|
||
监控任务会同时暂停,定时器不会自行重新启动后台,手动刷新或重新启用可恢复。
|
||
插件有进行中的请求时会提示忙碌;先结束或关闭该操作,再停止后台。
|
||
禁用、更新、切换版本、锁屏和退出应用也会停止后台。旧进程的延迟消息不会覆盖
|
||
新启动进程的状态。更新插件必须提升版本号,同版本不同内容不能重新安装。
|
||
|
||
## 本地验证
|
||
|
||
```sh
|
||
node --test scripts/create-plugin.node-test.mjs
|
||
cargo test --manifest-path plugins/sdk/rust/nyaterm-plugin-sdk/Cargo.toml
|
||
cargo test --manifest-path src-tauri/crates/nyaterm-plugin-runtime/Cargo.toml --features sdk-fixture
|
||
pnpm exec vitest run src/components/plugins/PluginDiagnostics.test.tsx --maxWorkers=4
|
||
```
|
||
|
||
`native-counter` 示例已使用 SDK,并升到 1.0.1:执行两次可看到计数递增和日志;
|
||
停止后台后再执行,计数从 1 开始。新版安装包为 `temp/plugins/native-counter.nyap`。
|
||
模板编译和打包已在 Windows 验证;其他平台需构建对应二进制并填写 executables。
|