完整 API 参考和 Capability 映射表请查看 对外 API 文档(含插件 API 与能力表,自动生成)。
rustup target add wasm32-unknown-unknown
cargo install wasm-toolscargo new my-plugin --lib
cd my-plugin[package]
name = "my-plugin"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
phira-plugin-sdk = { path = "../phira-plugin-sdk" }
serde_json = "1.0"
wit-bindgen = "0.58"
[features]
default = ["wit-bindgen"]wget https://github.com/HyperSynapseNetwork/Phira-mp-plus/releases/latest/download/phira-plugin-sdk.tar.gz
tar xzf phira-plugin-sdk.tar.gz
# 解压后得到 phira-plugin-sdk/ 和 wit/// src/lib.rs
phira_plugin_sdk::wit_bindgen!("phira-plugin-v3");
export!(MyPlugin);
use serde_json::{json, Value};
use crate::phira::plugin::phira_host;
struct MyPlugin;
fn host_api(method: &str, args: &[Value]) -> Result<Value, String> {
let wit_args: Vec<JsonValue> = args.iter().map(json_value_to_wit).collect();
match phira_host::api_call(method, &wit_args) {
ApiResult::Ok(value) => Ok(wit_json_to_serde(&value)),
ApiResult::Error(e) => Err(e),
}
}
impl Guest for MyPlugin {
fn init() -> Result<(), String> {
Ok(())
}
fn get_info() -> PluginInfo {
PluginInfo {
name: "my-plugin".to_string(),
version: "0.1.0".to_string(),
author: "your-name".to_string(),
description: "My plugin".to_string(),
}
}
fn cleanup() {}
fn on_event(_event: PluginEvent) -> Result<bool, String> { Ok(false) }
fn on_api(method: String, args: Vec<JsonValue>) -> ApiResult {
ApiResult::Ok(JsonValue::Null)
}
}cargo build --target wasm32-unknown-unknown --release
wasm-tools component new \
target/wasm32-unknown-unknown/release/my_plugin.wasm \
-o target/wasm32-unknown-unknown/release/my_plugin.component.wasmcp target/wasm32-unknown-unknown/release/my_plugin.component.wasm \
/path/to/phira-mp-plus/plugins/my-plugin.wasm启动服务器后插件自动加载。控制台执行 plugin list 确认。
宿主使用插件文件的稳定 ID 读取同目录 sidecar。例如 plugins/my-plugin.wasm 对应:
// plugins/my-plugin.capabilities.json
{
"capabilities": ["state.read", "send", "http"]
}允许值为 state.read、send、ext、config、file.read、file.write、plugin.call、plugin.register、http、room.manage、admin。未知值会拒绝加载。room.manage、admin 和 http 等能力必须显式授予;不要用可变的插件显示名称作为授权身份。
缺少 sidecar 时仅获得兼容性默认能力,不包含 http、room.manage 或 admin。插件应显式提交最小 capability 清单,不要依赖默认集合。
TODON:支持第一次安装任意插件时通过CLI审批其申请的权限
- 每次 guest 调用有 fuel 预算,线性内存、实例、内存与表数量受 Store limiter 限制。
- 同一插件同时只执行一个 init/event/API 调用;并发请求会快速失败。
- 调用超过
call_timeout_ms后插件会被 quarantine,后续调用被拒绝。 - quarantine 是故障隔离,不是线程强杀。若插件进入任意阻塞宿主函数,进程内运行时无法像独立进程一样强制终止它。
- 完全不可信插件应部署到独立进程/容器,不应只依赖 PMP 进程内沙箱。
插件通过 api-call 注册 HTTP 路由:
fn init() -> Result<(), String> {
host_api("http.register_route", &[json!({
"path": "/api/hello",
"plugin": "my-plugin"
})]);
Ok(())
}请求到来时,宿主调用 on_api(method, args):
fn on_api(method: String, args: Vec<JsonValue>) -> ApiResult {
match method.as_str() {
"/api/hello" => ApiResult::Ok(json_value_to_wit(&json!({"msg": "hello"}))),
"/api/greet/:name" => {
let name = args.get(0).and_then(|v| match v { JsonValue::Text(s) => Some(s.clone()), _ => None })
.unwrap_or("world".to_string());
ApiResult::Ok(json_value_to_wit(&json!({"greeting": format!("hello {name}")})))
}
_ => ApiResult::Error("unknown route".to_string()),
}
}插件可以通过 sse.register_stream 注册 SSE(Server-Sent Events)端点。宿主自动为每个注册的流创建 HTTP 路由,客户端连接后事件会通过插件的 on_api("sse:translate", …) 翻译后推送:
fn init() -> Result<(), String> {
host_api("sse.register_stream", &[json!({
"path": "/api/rooms/listen",
"plugin": "my-plugin",
"event_types": ["create_room", "join_room", "leave_room", "new_round"],
})]);
Ok(())
}注册后,客户端可连接 GET /api/rooms/listen 接收 SSE 事件。宿主收到每个 MpEvent 后调用插件 on_api("sse:translate", &[json!({"event_type": ..., "data": ...})]),插件返回翻译后的事件对象(或 null 跳过该事件)。
event_types 会在调用插件前由宿主执行过滤;空数组表示接收全部事件。内置房间事件名称为 create_room、update_room、join_room、leave_room、new_round。为兼容旧配置,CreateRoom/RoomCreate 等历史写法仍可识别。插件启用或重载后新增的 SSE 路由立即生效,不需要重启 HTTP 服务。
路由路径中支持 :param、<param>、{param} 参数占位符。路径缺少开头 / 时宿主会自动补全;重复注册同一路径会替换原处理器。普通 HTTP 路由与 SSE 路由均可在插件重载后即时生效。
宿主维护长连接,并通过 on_api 回调插件翻译事件。
fn init() -> Result<(), String> {
host_api("sse.register_stream", &[json!({
"path": "/api/events/rooms",
"plugin": "my-plugin",
"event_types": ["create_room", "join_room", "leave_room"],
})]);
Ok(())
}事件发生时,宿主调用 on_api("sse:translate", &[event_json]):
fn on_api(method: String, args: Vec<JsonValue>) -> ApiResult {
match method.as_str() {
"sse:translate" => {
let obj = wit_json_to_serde(&args[0]).as_object().cloned().unwrap_or_default();
let raw_type = obj.get("event_type").and_then(|v| v.as_str()).unwrap_or("");
let raw_data: Value = obj.get("data")
.and_then(|v| v.as_str())
.and_then(|s| serde_json::from_str(s).ok())
.unwrap_or(json!({}));
let translated = match raw_type {
"join_room" => json!({"type": "join_room", "room": raw_data.get("room"), "user": raw_data.get("user")}),
_ => json!(null), // null = 跳过此事件
};
ApiResult::Ok(json_value_to_wit(&translated))
}
_ => ApiResult::Error("unknown route".to_string()),
}
}注意:SSE 是长连接,插件本身不处理 HTTP 流式响应。宿主负责连接管理,插件只负责事件翻译。on_api 返回 null 时宿主跳过该事件不发送。
插件使用 WIT 接口与宿主通信。定义文件:wit/phira-plugin.wit,World:phira-plugin-v3。
| 函数 | 签名 | 说明 |
|---|---|---|
init |
func() -> result<_, string> |
初始化,返回 ok/error |
get-info |
func() -> plugin-info |
元数据(名称/版本/作者/描述) |
cleanup |
func() |
卸载时清理 |
on-event |
func(event: plugin-event) -> result<bool, string> |
事件处理 |
on-api |
func(method: string, args: list<json-value>) -> api-result |
API 调用入口 |
| 函数 | 说明 |
|---|---|
log(level, message) |
日志 |
generate-uuid() |
UUID v4 |
current-time-ms() |
Unix 毫秒 |
api-call(method, args) |
通用查询 |
send-chat(user-id, message) |
聊天消息 |
http-request(url, method, headers, body) |
沙箱 HTTP 请求 |
get-user / get-room / list-rooms / list-online-users / is-user-online / get-user-extra / set-user-extra / get-room-extra
create-empty-room / kick-from-room / transfer-host / set-host / set-room-lock / set-room-hidden / close-room / set-room-phira-api-endpoint
全部 53 个函数的详细签名见 WIT 定义文件。
完整插件示例:HSNPhira-v2-PMP-plugin ——HSNPhira v2 前端的 Web API 插件,展示了路由注册、API 处理、JSON 转换等全部模式。
PMP 插件状态机:加载 → 验证 → 启用 → 运行 → 禁用 → 移除
加载:启动时扫描 plugins_dir/*.wasm,CLI 命令 plugin reload 手动重载。
资源限制:
| 限制 | 默认值 | 说明 |
|---|---|---|
| 燃料 | 10,000,000 | 每次调用消耗,耗尽后 trap |
| 内存 | 64 MB | 线性内存上限 |
| 超时 | 2000 ms | 同步调用超时 |
| 并发 | 8 | 同事件内最大并行插件数 |
故障隔离 (Quarantine):连续超时或 trap 后进入隔离状态,不再接收新事件。
插件需在 .wasm 同级目录放置 {name}.capabilities.json:
{
"http": true,
"crypto": false,
"storage": true,
"send": false,
"max_concurrent_calls": 1
}| 权限 | 说明 |
|---|---|
http |
注册 HTTP 路由 |
crypto |
调用 sign/verify/sha256 |
storage |
读写扩展数据 |
send |
发送聊天消息 |
tcp |
发起 TCP 连接 |
max_concurrent_calls |
并发 API 调用数(默认 1) |
缺省授予 default_capabilities 集合(state.read、send、ext、config、http、room.manage、admin、crypto、timer、tcp、room-state 等)——manifest 声明的能力用于显式覆盖/收紧。动态能力请求(request-capability)默认拒绝。
cargo build --target wasm32-unknown-unknown --release
wasm-tools component new \
target/wasm32-unknown-unknown/release/my_plugin.wasm \
-o my_plugin.component.wasm
cp my_plugin.component.wasm plugins/
cp my_plugin.capabilities.json plugins/热加载:plugin reload my_plugin
服务器可通过 server_config.yml 为插件提供全局配置:
wasm_runtime:
max_memory_mb: 64
fuel_per_call: 10000000
http_timeout_secs: 10
max_http_response_bytes: 2097152
event_queue_capacity: 2048插件内通过 API 读取配置:
let config = self.api_call("config.get".into(), vec![json!("my_key")]);plugin list # 列出所有插件
plugin info <name> # 查看插件详情
plugin enable <name> # 启用
plugin disable <name> # 禁用
plugin reload <name> # 热重载
plugin remove <name> # 移除(保留文件)
plugin purge <name> # 彻底清理remove 只禁用和卸载,purge 才删除文件和数据。建议先 remove 确认无影响后再 purge。
SDK 宏路径:phira_plugin_sdk::wit_bindgen!("phira-plugin-v3")
// 原生 SDK 用法(在 PMP 仓库外开发时)
wit_bindgen::generate!({
path: "path/to/wit/phira-plugin.wit",
world: "phira-plugin-v3",
});完整 host API 列表见 WIT 定义文件。
本文档由
wit_abi_contracts::generate_wit_docs()自动生成规范,请勿手动编辑。 更新方式: 修改 WIT 文件后运行cargo test --test wit_abi_contracts验证一致性。
| 属性 | 值 |
|---|---|
| 运行时 ABI | abi-wit-v3 (WIT / Component Model) |
| 目标 ABI | abi-wit-v3 |
| 规范 WIT | wit/phira-plugin.wit |
| MIGRATION_PHASE | 3 (Stable: JSON bridge removed, WIT-only component ABI) |
| 接口数量 | 15 |
WIT 文件定义了以下接口与 world phira-plugin-v3:
Core data types shared between host and guest.
导出:
touch-event-pointjudge-event-itemplugin-infohttp-responsegame-end-recordjson-valueapi-result
Host functions available to WASM plugins.
导出:
loggenerate-uuidcurrent-time-msapi-callsend-chathttp-request
Events the host sends to plugins.
导出:
user-connect-infouser-disconnect-inforoom-user-eventroom-modify-infogame-end-infoplayer-touches-infoplayer-judges-inforound-complete-inforoom-join-infoplugin-event
User and room data query APIs available to plugins.
导出:
get-userget-user-extraset-user-extraget-roomget-room-extralist-roomslist-online-usersis-user-online
Room management operations.
导出:
create-empty-roomkick-from-roomtransfer-hostset-hostset-room-lockset-room-hiddenclose-roomset-room-phira-api-endpoint
User management and moderation.
导出:
kick-userban-userunban-userget-ban-listis-banned
Messaging — send messages and broadcast.
导出:
send-to-usersend-to-roomsend-to-all
Persistence read API — incremental event/snapshot queries.
导出:
query-eventsquery-room-snapshotsquery-touchesquery-judgesget-playtimetop-playtime
Admin Phira ID configuration.
导出:
list-admin-idsis-adminadd-admin-idremove-admin-idset-admin-ids
Plugin configuration (key-value, JSON, per-plugin config.json on disk).
导出:
get-configset-configlist-configreload-configpoll-config-changes
Cryptographic operations (host-side key management).
导出:
signverifysha256get-node-public-key
Federated networking — plugin-controlled TLS connections.
导出:
connectlistensendset-read-timeoutclose
Non-realtime timer for plugin-internal scheduling.
导出:
set-timerclear-timer
Runtime diagnostics.
导出:
statuseventscommands
phira-plugin-v3 — 导入上述所有接口,导出 init、get-info、cleanup、on-event、on-api。
宿主按插件稳定 ID 绑定 capability。关键映射:
| 能力 | 典型接口 |
|---|---|
state.read |
用户、房间、runtime、持久化查询 |
send |
消息发送 |
ext |
extension 数据 |
config |
插件配置读写/重载 |
file.read / file.write |
插件私有文件 |
plugin.call / plugin.register |
插件 API 调用与内部路由/SSE 注册 |
http |
出站 HTTP |
room.manage |
房间管理写操作 |
admin |
踢人、封禁、管理员写操作 |
未知方法映射为拒绝,未知 capability 也拒绝加载。管理员和房间管理能力不会因缺少 sidecar 自动授予。
- Wasmtime 启用 fuel;每次 guest 调用重新设置预算。
- Store limiter 限制线性内存、实例、内存和表数量。
- 每插件只允许一个执行中的调用;并发调用快速失败。
- API/event 超时后插件进入 quarantine,后续调用被拒绝,直到显式重新启用或重载。
- 进程内
spawn_blocking不能强杀已经进入任意阻塞宿主函数的线程。完全不可信插件必须迁移到独立进程或容器边界。
- 仅支持
abi-wit-v3 - WIT 的破坏性修改必须提升 package/ABI 版本,而不是只修改 Rust 实现。
- 新增字段优先使用可选类型或新增接口,避免改变已有 record/variant 的二进制契约。
- 插件元数据中的显示名称不是安全身份;授权与清理均使用稳定插件 ID。
至少执行:
cargo check --locked --workspace --all-targets
cargo check --locked -p phira-mp-plus-server --no-default-features --features wit-bindgen
cargo test --locked --workspace
cargo test --locked -p phira-mp-plus-server --test wit_abi_contracts
cargo clippy --locked --workspace --all-targets另需覆盖无限循环、内存增长、越权、初始化超时、事件超时、API 超时、trap 后重载和卸载清理。
当玩家加入房间时发送系统消息:
fn on_event(&mut self, event: PluginEvent) -> Result<bool, String> {
match event {
PluginEvent::RoomJoin(info) => {
let msg = format!("欢迎 {} 加入房间!", info.user_id);
self.api_call("send".into(), vec![
json!(0), // user_id = 0 (system)
json!(msg),
]);
Ok(true)
}
_ => Ok(false),
}
}通过 ext(扩展 KV 存储)跨会话累加游玩时间:
fn on_api(&mut self, method: String, args: Vec<JsonValue>) -> ApiResult {
match method.as_str() {
"playtime.get" => {
let uid = args[0].as_i64().unwrap_or(0);
let key = format!("playtime:{}", uid);
let data = self.api_call("ext.get".into(), vec![json!(key)]);
ApiResult::Ok(data)
}
_ => ApiResult::Error("unknown method".into()),
}
}游戏结束后将结果写入文件:
fn on_event(&mut self, event: PluginEvent) -> Result<bool, String> {
if let PluginEvent::RoundComplete(info) = event {
let filename = format!("round_{}.json", info.round_id);
let content = serde_json::to_string(&info).unwrap_or_default();
self.api_call("file.write".into(), vec![
json!(filename),
json!(content),
]);
}
Ok(false)
}注册 /api/rooms/info 端点返回房间状态:
fn init(&mut self) -> Result<(), String> {
self.register_route("GET", "/api/rooms/info")?;
Ok(())
}
fn on_api(&mut self, method: String, _args: Vec<JsonValue>) -> ApiResult {
if method == "room_info" {
let info = self.api_call("get_room_info".into(), vec![]);
return ApiResult::Ok(info);
}
ApiResult::Error("not found".into())
}通过 player_touches 事件追踪实时触控数据:
fn on_event(&mut self, event: PluginEvent) -> Result<bool, String> {
if let PluginEvent::PlayerTouches(info) = event {
let total = info.data.len();
println!("玩家 {} 发送了 {} 个触控点", info.user_id, total);
}
Ok(false)
}以下功能直接在 WASM 侧用库解决,不进宿主 API:
| 功能 | 原因 |
|---|---|
| CBOR 确定性编码 | tiny crate ~2KB WASM |
| ULEB128 帧编码 | 10 行代码 |
| 会话管理状态机 | 业务逻辑,属于插件自身 |
| 事件链哈希验证 | 已暴露 sha256,链由插件维护 |
| 心跳/Ping | set-timer + TCP send |
| Peer 路由表 | 插件内存数据 |