00概述与交付物
像素播放器插件是「声明 + 可执行体 + 可选界面」的扩展包。第三方开发者交付一个 zip,用户在 App 内导入并授权后启用;插件通过宿主开放的 Host API 访问播放、曲库、投屏、外设、存储、分享与 AI 能力,不需要也不允许修改宿主源码。
交付物
| 文件 | 必需 | 说明 |
manifest.json | 是 | 声明:id、版本、能力、权限、指令坞入口、界面入口 |
main.js | 条件 | runtime 缺省(script)时必需;wasm 插件可省略 |
plugin.wasm | 条件 | runtime: "wasm" 时必需,≤ 8MB,禁止 import |
ui/index.html | 否 | 自带界面入口(WebView 承载),必须在 ui/ 下 |
agreements/terms.md | 条件 | 涉及敏感能力时必须提供,启用前全文展示(Markdown) |
五分钟闭环
终端
cd 插件中心
node tooling/xs-pack.js validate plugins/my-plugin # 校验 manifest / 权限 / 界面入口
node tooling/xs-pack.js test plugins/my-plugin # 本地跑通注册与 onEnable
node tooling/xs-pack.js pack plugins/my-plugin -o out/my-plugin-1.0.0.zip
# 上传到自有商店(可选)
cp out/my-plugin-1.0.0.zip server/zips/
cp plugins/my-plugin/manifest.json server/zips/my-plugin-1.0.0.json
node server/update-index.js
术语统一:指令坞(播放器下方可编排入口)、能力(capability)、权限(permission)、面板(插件自带界面)、宿主(像素播放器 App)。
01运行时与生命周期
两套运行时
| 项 | script(缺省) | wasm |
| 入口 | main.js(≤ 2MB) | entry: "plugin.wasm"(≤ 8MB) |
| 调用 | xs.host.module.method(args) → Promise | xs_action(ptr,len) → 结果 JSON |
| 隔离 | JS 沙箱 + 桥权限 | 禁止 import;内存 64MB;指令 5000 万上限 |
| 界面 | ui/ + WebView | 同左 |
| 适用 | 编排、表单、通知、AI | 哈希、装箱、统计等一次性重计算 |
平台差异:iOS 脚本引擎为 JavaScriptCore,支持脚本内 WebAssembly;安卓 QuickJS 不支持,若你在 main.js 里直接加载 wasm,必须提供等价 JS 兜底。宿主内建解释器(runtime: "wasm")不受脚本引擎限制。
生命周期与状态机
状态机
installed ──同意协议──▶ agreed ──启用──▶ enabled
│ │
│ ├─ onEnable() 一次
│ ├─ onAction(id) 每次点击
│ └─ onDisable() 禁用时一次
└─ 卸载:删除安装包快照、界面资产、WASM 模块、私有 KV
- 安装即持久化:导入成功后,宿主把原始 zip 保存为安装快照;下次冷启动按同一路径重建,不依赖临时目录。
- 重启提示:导入/下载完成后,App 内会提示「插件已部署 · 请重启 App 以启用」。新插件的运行时与指令坞在冷启动时完整挂载。
- 幂等:
onEnable 可能因恢复、重新启用而被多次调用,必须可重入(不要假设只执行一次)。
- 失败可见:恢复失败不会静默删除,会在插件中心控制台记录;用户可卸载后重新导入。
执行环境约束
- 脚本运行在隔离沙箱:无文件系统、无网络直连、无进程派生;唯一 I/O 是
xs.host.*。
- 单次调用有超时预算;长任务要拆分,避免阻塞主线程。
- 禁止 eval / Function 动态构造(宿主在加载阶段拦截)。
02manifest 规范(schema v3)
完整示例
manifest.json
{
"schemaVersion": 3,
"id": "com.example.awesome",
"name": "AWESOME 调度器",
"version": "1.0.0",
"type": "capability",
"apiLevel": 3,
"runtime": "script",
"author": { "name": "Your Name", "email": "dev@example.com" },
"description": "一句话说明插件解决什么问题",
"icon": "bolt.horizontal.circle",
"capabilities": [
{ "id": "media", "version": 1, "permissions": ["media.info", "media.control", "queue.manage"] },
{ "id": "library", "version": 1, "permissions": ["library.read"] },
{ "id": "storage", "version": 1, "permissions": ["storage.read", "storage.write"] },
{ "id": "notification", "version": 1, "permissions": ["notification.send"] }
],
"playerActions": [
{ "id": "open", "icon": "rectangle.3.group", "title": "面板" },
{ "id": "run", "icon": "play.circle", "title": "执行" }
],
"ui": { "entry": "ui/index.html", "title": "AWESOME 面板" },
"agreement": "agreements/terms.md",
"networkDomains": ["api.example.com"]
}
字段与校验
| 字段 | 必需 | 规则 |
schemaVersion | 是 | 固定 3;其它值拒绝安装 |
id | 是 | 反向域名,全局唯一,冲突直接拒绝 |
name | 是 | 1–24 字符,中文优先 |
version | 是 | 语义化 x.y.z,更新必须递增 |
type | 是 | 当前只接受 capability |
apiLevel | 是 | 固定 3;高于宿主能力则拒绝并提示升级 |
runtime | 否 | script(缺省)/ wasm;未知值拒绝安装 |
entry | 条件 | runtime=wasm 时必需,包内 .wasm |
capabilities | 是 | 非空,id 与 permissions 均在白名单内 |
playerActions | 否 | 指令坞入口;id="open" + ui 时点击直接开面板 |
ui | 否 | { entry, title };entry 必须在 ui/ 下且真实存在 |
agreement | 条件 | 敏感能力必填,文件必须在包内 |
networkDomains | 否 | 域名白名单;声明时必须具备 network 能力 |
- zip ≤ 30MB;
main.js ≤ 2MB;ui/ 单文件 ≤ 2MB;.wasm ≤ 8MB;
- 禁止
.so / .dylib / .dll 等原生库;允许 .wasm;
- 条目名禁止
../、绝对路径、符号链接;
- 宿主必须忽略未知字段(向前兼容)。
03能力与权限
14 项能力
| id | 含义 | 常用权限 |
| cast | DLNA / 投屏:设备发现、会话控制、状态 | cast.discover / cast.control |
| music-source | 远程音源聚合搜索与取流 | music.search / music.play |
| push | 推送到第三方设备(墨水屏等) | push.send |
| device-bridge | 穿戴 / 外设桥接 | device.connect |
| theme | 主题与外观(模块名 ui) | theme.apply |
| media | 播放状态、控制、队列、歌词 | media.info / media.control / queue.manage / lyrics.readwrite |
| library | 本地曲库元数据 | library.read |
| storage | 插件私有 KV(按 id 隔离) | storage.read / storage.write |
| network | 受限网络请求(域名白名单) | network.request |
| notification | 本地通知 | notification.send |
| share | 分享文本 / 歌曲 / 声明式海报 | share.invoke |
| ai | 宿主统一 AI(插件拿不到 Key) | ai.invoke |
| clipboard | 剪贴板读写(文本) | clipboard.read / clipboard.write |
| navigation | 深链 / 外部打开(仅 http / https / xiangsuplayer) | navigation.open |
22 项权限与授权流
权限必须挂在某个 capability 的 permissions 下(无独立 lyrics 能力,歌词权限挂 media)。授权三级门槛:协议 → 配置 → 启用,任一失败不进入下一步。未授权调用一律返回 { "error": "permission_denied" }。
| 权限 | 触发时机 |
| cast.discover / cast.control | 设备发现 / 会话控制与状态 |
| music.search / music.play | 聚合搜索 / 获取播放地址 |
| push.send | 向第三方设备推送 |
| device.connect | 桥接状态与指令 |
| theme.apply | 实时改写强调色 / 圆角 / 歌词字号 |
| media.info / media.control / queue.manage | 读播放信息 / 控制播放 / 队列增删 |
| library.read | 曲库搜索与专辑、歌单 |
| storage.read / storage.write | 私有 KV 读写 |
| network.request | 白名单内 HTTP(S) |
| notification.send | 本地通知(需系统授权) |
| share.invoke | 系统分享 / 海报 |
| lyrics.readwrite | 当前曲目歌词读取与跳句 |
| ai.invoke | 宿主 AI 代理调用 |
| clipboard.read / clipboard.write | 读取 / 写入剪贴板文本 |
| library.write | 写入本地曲库(收藏 / 保存在线曲目) |
| navigation.open | 打开白名单协议链接(http / https / xiangsuplayer) |
最小化原则:审核会逐项核对「权限 ↔ 功能」。声明但未使用的权限是常见驳回原因;敏感权限(music.play / device.connect / network.request / ai.invoke)必须附 agreement。
04Host API 契约
调用约定
- 全部异步,返回 Promise;参数与返回均为 JSON 兼容类型;
- 失败返回
{ error: "code" },不抛异常;统一先判 res && !res.error;
- 未实现 →
not_implemented;未授权 → permission_denied;写错方法名 → no_such_method;
- 单位约定:播放进度/时长/歌词时间为 毫秒;投屏
seek 为 秒。
模块总览
| 模块 | 主要方法 | 说明 |
media | getCurrentSong / getPlaybackState / play / pause / next / prev / seekTo / setVolume / getQueue / addToQueue({position}) / addToQueueNext / removeFromQueue | position: "next" 插到当前曲目之后(推荐);缺省追加队尾 |
library | search / getAlbums / getAlbum / getSongs(keyword, limit?, offset?) / count / addSong(song) / getPlaylists / getPlaylist | addSong 需 library.write;count() 返回总数 |
lyrics | getLines / seekToLine / setOverride | 只读;setOverride 未实现 |
storage | get / set / remove / getKeys / clear | 按 xs.plugin.<id>.<key> 隔离 |
ui | getTheme / applySkin / applyCoverStyle / applyLyricStyle / reset | 实时套用 |
notify | send / cancel | 本地通知 |
share | shareText / shareSong / sharePoster | 海报见 §06 |
system | toast / haptics / share / setTitle / info | 无需权限;info 返回版本、系统、设备 |
cast | discover / play({songId,title,artist}) / pause / resume / stop / seek(秒) / status | songId 必须是 NAS 曲库 id |
device | getBridges / sendCommand({target}) / onStatus | target: band / dot |
musicSource | search / getPlayUrl / getLyrics / getArtwork | registerProvider 未开放 |
ai | status / chat({prompt,system,maxTokens}) | prompt ≤ 4000 字,maxTokens 128–2000 |
network | request / fetch(url) | 仅白名单域名 |
clipboard | readText / writeText / clear | 仅文本;需 clipboard.read / clipboard.write |
deepLink | openUrl / canOpen | 仅 http / https / xiangsuplayer 协议 |
错误码
| code | 含义 | 处理建议 |
| permission_denied | 权限未授予 / 域名不在白名单 | 引导到插件中心授权 |
| not_implemented | 宿主版本不支持该方法 | 降级或提示升级 |
| no_such_module / no_such_method | 拼写错误 | 开发期修正 |
| invalid_args / invalid_song / invalid_index | 参数不合法 | 校正单位与字段 |
| song_not_found / no_stream / no_artwork | 音源查不到 | 换源或跳过 |
| notification_denied | 系统通知被拒 | 降级为 toast,不反复弹授权 |
| not_configured | 桥接未启用 / NAS 未配置 | 引导到设置页 |
| ai_failed | AI 未启用 / 额度 / 网络 | 展示 message,禁止自动重试风暴 |
| invalid_scheme | 链接协议不在白名单 | 改用 http / https / xiangsuplayer |
05插件界面与设计规范
WebView 容器
- manifest 声明
ui 后,宿主以 WebView 加载 ui/index.html,资产解包到应用私有目录;
- 注入与脚本插件完全相同的
__xsHost__ 桥,可直接用 xs.host.*;
- CSP:
script-src 'self' file: 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval',connect-src 'self' file:;远程网络必须走 xs.host.network;
- 导航只允许插件目录内
file://;
- 首次进入宿主会显示一次「插件独立界面」引导条,插件内不必重复大段教学。
双页面:原生控制台 + 插件界面
声明 ui 的插件,详情页会同时保留两套页面,顶部用分段控件切换,两套页面共享同一份权限与运行时:
| 页面 | 实现 | 内容 | 可用性 |
| 原生 | 宿主 SwiftUI(Android:Compose) | 运行状态、指令坞动作直执行、最近回执、清除私有数据 | 所有插件都有(无 ui 时只有这一页) |
| 插件 | WebView 加载 ui/index.html | 插件自绘面板、可视化、交互流程 | 仅声明 ui 时出现 |
manifest.json · 指定默认页
"ui": {
"entry": "ui/index.html",
"title": "MY 面板",
"defaultPage": "plugin" // plugin(缺省)| native
}
- 原生页用于兜底与排障:插件界面白屏、CSP 配置错误、运行时异常时,用户仍可执行指令、查看回执;
- 插件页必须可独立完成核心流程,不得只提供「去原生页点按钮」的说明;
- 双端一致:Android 必须实现同样的两页与分段切换,默认页读取同一字段。
视觉与交互规范(摘要)
完整规范见仓库 插件中心/spec/ui-guidelines.md。硬性要求:
- 深色底
#161B1A,强调色 var(--xs-accent, #2DB95A) —— 由宿主注入、跟随 App 主题,插件禁止硬编码主色;
- 左右内边距 16pt,底部
76 + env(safe-area-inset-bottom);不覆盖宿主导航与迷你播放条;
- 卡片圆角 18、细描边 + 顶部主题色高光;中文正文 15pt / 行高 1.65,不用等宽;时间统一
mm:ss,数据用等宽;
- 一屏只有一个主按钮;危险操作必须二次确认;按钮 ≥ 46pt 高、触控区 ≥ 44pt;
- 四态齐全(加载 / 空 / 错误 / 成功),错误文案三段式:发生了什么 + 错误码 + 下一步;
- 禁止 emoji、彩色阴影、远程字体与外部脚本;禁止自绘返回按钮。
现成样式:把 spec/xs-ui-kit.css(v3)复制到插件 ui/ 目录,<link rel="stylesheet" href="xs-ui.css"> 即可获得字体、间距、卡片、按钮与进度条。宿主会在 documentStart 注入 --xs-accent,主题切换时热更新。参考实现:plugins/px07-terminal/ui/index.html、plugins/radio-seed/ui/index.html。
ui/index.html · 实时数据
// 宿主每 500ms 推送:{ song, state:{state,position,duration}, queue }
window.xsOn('playback', function (p) {
bar.style.width = (p.state.duration ? p.state.position / p.state.duration : 0) * 100 + '%';
title.textContent = (p.song && p.song.title) || '未播放';
queue.textContent = p.queue + ' 首';
});
页面骨架
ui/index.html(最小骨架)
<div class="hdr"><span class="sq"></span><span class="t">MY // SUBSYSTEM</span><span class="n">01</span></div>
<div class="kicker">PANEL // 中文小标</div>
<div class="title" id="title">--</div>
<div class="sub">▸ 一句话状态</div>
<div class="rule"></div>
<div class="rows">
<div class="row"><span class="i">01</span><span class="k">FIELD</span><span class="v" id="v1">--</span></div>
</div>
<div class="grid">
<button class="btn" id="run"><b>执行</b><span class="tag">RUN</span></button>
</div>
<div class="out" id="out">待命</div>
<div class="foot"><span>XIANGSU PLAYER</span><span>MY · V1.0.0</span></div>
06声明式海报(sharePoster)
任何插件都能用 share.sharePoster 生成竖版 1080×1920 海报。版式由插件声明 blocks,宿主统一渲染:插件不写坐标、不做 UI 特权。配文只写入剪贴板,分享面板只提交图片。
| block | 关键字段 | 用途 |
| header | left, right | 顶栏标签与编号 |
| kicker | accentText, dimText | 双色引导行 |
| title | text, size, maxLines | 主标题(自动测量) |
| sub / rule | text / — | 副标题 / 强调短线 |
| params | rows: [{index,label,value}] | 编号参数行 |
| segments | filled, total | 分段进度 |
| metrics | left, right | 两端指标 |
| spacer / barcode / ticker / text | 见字段 | 弹性留白 / 装饰条码 / 底部文字 / 自由文本 |
main.js
await xs.host.share.sharePoster({
title: song.title, artist: song.artist, album: song.album,
position: st.position, duration: st.duration, queue: queue.length,
state: String(st.state), source: song.source, accent: '#FF5A1F', sig: 'A1B2C3D4',
blocks: [
{ type: 'header', left: 'MY // POSTER', right: '01' },
{ type: 'kicker', accentText: 'NOW PLAYING', dimText: '// 简报' },
{ type: 'title', text: song.title, size: 104, maxLines: 2 },
{ type: 'sub', text: '▸ ' + song.artist },
{ type: 'spacer' }, { type: 'barcode' },
{ type: 'ticker', accentText: 'XIANGSU PLAYER', dimText: '// POSTER' }
]
});
07WASM 插件
两条路径
| 路径 | iOS | Android | 用途 |
| 脚本内联 wasm(main.js 里 base64) | 支持 | QuickJS 不支持,需 JS 兜底 | 指令坞动作内的重计算 |
runtime: "wasm"(宿主解释器) | 支持 | 待移植 | 纯计算插件,入口即 .wasm |
| 界面内 wasm(ui/ 页面加载) | 支持 | WebView 支持 | 面板内的交互重计算 |
runtime=wasm 的 ABI
模块不得声明 import,导出(参数/返回均为 i32):xs_action(ptr,len)(必需)、xs_alloc 或 buf_ptr/buf_cap、xs_result_ptr/xs_result_len、可选 xs_init / xs_enable / xs_disable。
Rust(no_std)
#[no_mangle] pub extern "C" fn xs_action(ptr: usize, len: usize) -> i32 {
let data = unsafe { core::slice::from_raw_parts(ptr as *const u8, len) };
// data = {"action":"signal","context":{"song":{...},"state":{...},"queue":[...]}}
emit(b"{\"toast\":\"SIG 5FF20A64\"}");
0
}
- 结果 JSON 可含
toast / notify / shareText / addQueue / ui,由宿主按权限执行;
- 内存上限 64MB,指令预算 5000 万;解释执行,约为原生 1/10–1/30,适合一次性计算;
- 构建:
rustc --target wasm32-unknown-unknown --crate-type cdylib -O -C panic=abort -C strip=debuginfo(不 strip 会膨胀到数百 KB);
- 参考实现:
wasm-src/plugin_probe.rs 与官方样例 com.xiangsu.wasmcore。
Rust SDK(xs-plugin v0.1)
官方提供 xs-plugin crate:不手写 ABI、不拼 JSON、不管缓冲区,只写业务逻辑。样例 com.xiangsu.rustcore(4.6KB wasm)可直接拆包对照。
rust/src/lib.rs
#![no_std]
use core::panic::PanicInfo;
use xs_plugin::{xs_main, Out, Request};
#[panic_handler]
fn panic(_: &PanicInfo) -> ! { loop {} }
fn handle(req: Request, out: &mut Out) {
match req.action() {
"signal" => out.toast_hex("SIG ", fnv(req.raw().as_bytes())),
"song" => out.toast(req.str("title").unwrap_or("UNKNOWN")),
_ => out.error("unknown_action"),
}
}
xs_main!(handle);
终端 · 一键构建打包
node tooling/xs-rust.cjs plugins/my-plugin --pack
# cargo 编译 → 校验 ABI → 写入 plugin.wasm → xs-pack 打包
Request:action() / str(key) / num(key) / count(key) / len();Out:toast / toast_hex / notify / share_text / add_queue / skin / error;
- 宿主注入
context.library(声明 library.read 时,最多 2000 首)与 libraryTotal;addQueue 支持数组批量入队;
- 必须
strip = "debuginfo",否则体积从 4–6KB 膨胀到 600KB+;
- 完整规范:仓库 `插件中心/spec/rust-sdk.md`。
08打包、商店与发布
zip 结构
目录
my-plugin/
├── manifest.json
├── main.js # script 插件必需
├── plugin.wasm # wasm 插件必需
├── ui/index.html # 可选,自带界面
├── agreements/terms.md
└── assets/ # 可选,图标等
根目录直接是 manifest.json,不要多包一层文件夹。打包后 zip ≤ 30MB。
商店协议
| 路由 | 返回 |
GET /health | {"ok":true,"service":"xiangsu-plugin-store","plugins":N} |
GET /api/plugins | {"code":0,"data":[StorePlugin...]} |
GET /zips/:file | zip 二进制,支持 Range / HEAD |
默认商店:https://xiangsudev.cn/plugin-store。客户端下载后必须校验 sha256;索引字段变更需向后兼容。
审核与更新
提交入口:/publish.html(开发者入驻)。必填项包含:开发者信息、插件介绍、权限理由、隐私政策、用户协议 / 单独协议、数据流向说明。隐私政策可粘贴 Markdown 全文,也可填公开可访问的 https 链接;同时建议在插件包内提供 agreements/terms.md,并让 manifest 的 agreement 指向它。
- 提交:作者信息、功能说明、权限用途、隐私政策、用户协议、数据流向说明、两张界面截图;
- 更新:
version 递增 → 重新打包 → 替换 zips/ 同名版本文件 → 重建索引;
- 已安装用户下次打开商店可看到「更新」,覆盖安装保留启用与授权状态;
- 下架:索引移除后不可再下载,已安装插件不自动删除。
09安全、隐私与合规
- 数据最小化:只读取功能必需的数据;曲库、歌词、播放信息不得外发;
- 凭据:不得上传用户 Cookie / 账号凭据;第三方凭据只存本地,且必须有单独协议;
- 网络:只能通过
xs.host.network,域名必须写进 networkDomains;
- 可执行体:允许
.wasm,禁止原生动态库;WASM 不得用于绕过 DRM、挖矿、代理;
- 文案红线:不得宣称破解、非授权音源;仅可表述为「本地 / 自建 NAS 曲库」;
- 通知:不得伪造系统样式或冒充其他应用;
- 协议:敏感能力必须提供
agreements/terms.md,启用前全文展示,禁止默认勾选;
- 上架必填:开发者入驻表单中的《隐私政策》《用户协议》《数据流向说明》三项;审核后台会与 manifest 权限逐项对照,缺失或与权限不符直接驳回。
- 卸载:必须能彻底清除私有 KV、回执、安装快照与资产。
隐私政策与用户协议:涉及账号、设备、网络或第三方服务的插件,必须同时提供《插件单独协议》与数据流向说明;宿主会在启用前展示,用户可拒绝。
10版本与兼容
| 维度 | 规则 |
schemaVersion | 固定 3;宿主拒绝其它值 |
apiLevel | 固定 3;宿主能力不足时拒绝安装并提示升级 |
capabilities[].version | 从 1 开始,只增不改;破坏性变更需新版本号 |
| 未知字段 | 宿主必须忽略(向前兼容) |
| 废弃 | 先标注 deprecated,至少保留 2 个宿主版本,之后返回 not_implemented |
| 双端差异 | 仅两处:视图翻译层(未实现)与图标体系(SF Symbols → Material Symbols) |
11提交前验收清单
- manifest 通过
xs-pack validate;能力与权限一一对应,无多余声明;
- 指令坞入口 ≤ 4 个,含
open(有面板时);标题 ≤ 6 字;
- 每个指令都有 toast / 通知反馈;错误走三段式文案与稳定错误码;
- 面板符合《UI 设计规范》:安全区、四态、主按钮唯一、无 emoji、无远程资源;
- 断网 / 空曲库 / 未授权 / 无设备 四种异常路径均不崩溃、不静默失败;
- WASM:模块无 import、导出齐全、无 JS 兜底缺失(如需跨端);
- 协议文本与权限清单一致;隐私说明覆盖所有外发数据;
- 卸载后私有 KV、资产、安装包全部清除;
- 升级路径:旧版本安装后可被新版本覆盖,启用状态与授权保留。