适用范围:MXOS 1.5 当前源码与运行行为
版本注意:本文档以当前 MXOS 1.5 源码行为为准。源码中个别内部字符串可能仍显示1.6、1.1或 demo 文案v2,不影响本文面向 MXOS 1.5 的开发说明。
MXOS 1.5 是运行在浏览器中的静态 Web 伪系统。第三方应用通过 .mx 包安装,安装器读取包内 manifest.json 和 app.bin,将应用元数据与内容保存到浏览器本地状态,并把应用加入开始菜单。
第三方应用有两种入口:
app.bin 是完整 HTML,运行在 iframe srcdoc 中。app.bin 是 JavaScript,需定义 window.MXOS_APP = { init(windowEl) { ... } }。当前安装器的硬性要求:
.mx 必须是 zip 文件。manifest.json。app.bin。manifest.icon 必须是包内相对路径。manifest.icon 指向的文件名必须严格为 svg.png,推荐 assets/svg.png。建议使用现代 Chromium 系浏览器:Edge、Chrome 或 Chromium。需要支持:
import / exportsrcdoclocalStorageindex.html # 页面入口
js\main.js # 主启动脚本
js\api.js # window.MXOS 公共 API
js\core.js # 窗口、应用启动、安装核心逻辑
js\apps\installer.js # .mx 应用安装器
js\apps\thirdparty.js # 第三方应用运行器
js\sandbox\*.js # 权限、沙箱、postMessage RPC
sw.js # Service Worker
python serve.py
默认地址:
http://localhost:8080
serve.py 会在项目根目录中启动静态服务器,并设置 no-cache 头:
Cache-Control: no-cache, no-store, must-revalidate
Pragma: no-cache
Expires: 0
安装器/商店会动态加载 JSZip:
https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js
如果网络不可用,安装 .mx 时可能出现 JSZip 加载失败。
项目根目录
├─ index.html
├─ serve.py
├─ sw.js
├─ _redirects
├─ MiSans-Normal.ttf
└─ js
├─ main.js
├─ config.js
├─ state.js
├─ core.js
├─ api.js
├─ vfs.js
├─ apps
├─ features
├─ sandbox
├─ utils
├─ system
├─ real
├─ media
├─ ime
└─ ai
启动流程摘要:
main.js 加载配置、状态、核心窗口系统和桌面模块。api.js 挂载 window.MXOS 公共 API。localStorage.mxos_installed_apps 恢复已安装第三方应用。第三方应用安装与运行链路:
.mx 文件
↓ 拖入/选择
installer.js
↓ JSZip 解包
core.handleInstallerFileSimple(...)
↓ 校验 manifest.json / app.bin / assets/svg.png
state.installedApps + localStorage(mxos_installed_apps)
↓ 开始菜单入口
core.launchThirdPartyApp(app)
↓ appConfigs['thirdparty_' + app.id]
apps/thirdparty.js 渲染 app.bin
.mx.mx 是 zip 压缩包。最小结构:
my-app.mx
├─ manifest.json
├─ app.bin
└─ assets
└─ svg.png
推荐工程结构:
my-app
├─ manifest.json
├─ app.bin
└─ assets
├─ svg.png
└─ screenshots
└─ home.png
必需文件:
| 文件 | 必需 | 说明 |
|---|---|---|
manifest.json | 是 | 应用元数据、权限与窗口配置 |
app.bin | 是 | 应用入口,可为 HTML 或 JavaScript |
assets/svg.png | 是 | 当前安装器要求图标文件名严格为 svg.png |
图标规则:
{ "icon": "assets/svg.png" }
以下写法会失败或不推荐:
{ "icon": "assets/icon.png" }
{ "icon": "assets/icon.svg" }
{ "icon": "https://example.com/icon.png" }
{ "icon": "data:image/png;base64,..." }
manifest.json 参考完整示例:
{
"id": "com.example.hello",
"name": "Hello MXOS",
"version": "1.0.0",
"manifestVersion": 2,
"description": "一个 MXOS 1.5 示例应用",
"author": "Your Name",
"category": "demo",
"icon": "assets/svg.png",
"permissions": ["storage", "notifications"],
"window": {
"width": 800,
"height": 600,
"minWidth": 400,
"minHeight": 300,
"resizable": true
},
"screenshots": [
{
"src": "assets/screenshots/home.png",
"width": 800,
"height": 600,
"type": "image/png"
}
]
}
字段说明:
| 字段 | 类型 | 必需 | 说明 |
|---|---|---|---|
id | string | 建议 | 应用唯一 ID;缺省时安装器会自动生成临时 ID |
name | string | 建议 | 应用显示名称;缺省为 未知应用 |
version | string | 否 | 应用版本;缺省 1.0.0 |
manifestVersion | number | 否 | 建议 2 |
description | string | 否 | 应用描述 |
author | string | 否 | 作者 |
category | string | 否 | 分类,如 demo、tools、media |
icon | string | 是 | 包内 svg.png 路径,如 assets/svg.png |
permissions | string[] | 否 | 权限声明 |
window | object | 否 | 初始窗口配置 |
screenshots | object[] | 否 | 截图元数据 |
window 字段:
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
width | number | 800 | 初始宽度 |
height | number | 600 | 初始高度 |
minWidth | number | 400 | 最小宽度 |
minHeight | number | 300 | 最小高度 |
resizable | boolean | true | 是否允许调整窗口 |
app.bin 应用入口若 app.bin 以 <!DOCTYPE、<!doctype、<html 开头,或前 500 字符内包含 <html>,会被识别为 HTML 应用。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Hello MXOS</title>
</head>
<body>
<h1>Hello MXOS</h1>
<button id="notify">发送通知</button>
<script>
const MXOS = window.MXOS || window.parent.MXOS;
document.getElementById('notify').onclick = function () {
if (MXOS && typeof MXOS.notify === 'function') {
MXOS.notify({ title: 'Hello', body: '来自 HTML 应用', type: 'success' });
} else if (MXOS && MXOS.Notification) {
MXOS.Notification.show({ title: 'Hello', message: '来自沙箱桥接' });
} else {
alert('当前环境没有可用通知 API');
}
};
</script>
</body>
</html>
建议优先使用 window.MXOS。为了兼容旧 demo,可回退到 window.parent.MXOS。
脚本应用的 app.bin 必须定义:
window.MXOS_APP = {
init: function (windowEl) {
// windowEl 是当前应用窗口 DOM 元素
}
};
示例:
window.MXOS_APP = {
init: function (windowEl) {
const api = window.__MXOS_APP_API__;
const content = windowEl.querySelector('.window-content');
api.setWindowTitle('脚本应用示例');
api.setWindowSize(720, 480);
content.innerHTML = `
<div style="padding:24px;color:var(--color-foreground,#0F172A);background:var(--color-background,#F8FAFC)">
<h2>Script App</h2>
<button id="saveBtn">保存数据</button>
<button id="notifyBtn">发送通知</button>
</div>
`;
content.querySelector('#saveBtn').onclick = function () {
api.Storage.set('lastClick', Date.now());
alert('已保存');
};
content.querySelector('#notifyBtn').onclick = function () {
api.Notification.show({ title: '脚本应用', body: '通知内容', type: 'info' });
};
}
};
可在宿主环境通过 window.MXOS 访问。
const id = MXOS.notify({
title: '保存成功',
body: '文件已保存到本地',
type: 'success',
duration: 5000,
actions: [{ label: '查看', onClick: function () {} }],
bypassDnd: false
});
type 支持 info、success、warning、error。body 是标准正文参数,兼容层也接受 message。宿主调用直接返回通知 ID;沙箱/RPC 调用返回 Promise<number>,可统一用 await Promise.resolve(...) 获取 ID。MXOS.Notification.show(options) 是兼容别名,与 MXOS.notify(options) 使用同一套参数。
await MXOS.dialog.alert('提示', '操作完成');
const ok = await MXOS.dialog.confirm('确认', '是否继续?');
const name = await MXOS.dialog.prompt('输入名称', '默认值');
MXOS.dialog.toast('已保存', 'success');
MXOS.openApp('calculator');
MXOS.openApp('notepad', { title: '临时笔记', initialContent: 'Hello', fileId: 'note-1' });
MXOS.closeApp('calculator');
const apps = MXOS.listApps();
const cfg = MXOS.getAppConfig('settings');
| API | 返回 | 说明 |
|---|---|---|
MXOS.openApp(appId, args) | boolean | 打开应用;args 支持 fileId、initialContent、title |
MXOS.closeApp(appId) | boolean | 关闭指定 appId 的所有窗口 |
MXOS.listApps() | array | 列出内置应用和已安装应用 |
MXOS.getAppConfig(appId) | object/null | 获取应用配置 |
MXOS.window.focus('settings');
MXOS.window.minimize('settings');
MXOS.window.maximize('settings');
MXOS.window.snap('settings', 'left');
const bounds = MXOS.window.getBounds('settings');
MXOS.window.setBounds('settings', { x: 40, y: 20, width: 900, height: 600 });
MXOS.window.close('settings');
支持:minimize、maximize、close、focus、snap、getBounds、setBounds。
MXOS.fsawait MXOS.fs.createFolder('/Demo');
await MXOS.fs.writeFile('/Demo/hello.txt', 'Hello MXOS');
const text = await MXOS.fs.readFile('/Demo/hello.txt');
const list = await MXOS.fs.listFiles('/Demo');
const exists = await MXOS.fs.exists('/Demo/hello.txt');
await MXOS.fs.copy('/Demo/hello.txt', '/Demo/copy.txt');
await MXOS.fs.move('/Demo/copy.txt', '/Demo/moved.txt');
await MXOS.fs.delete('/Demo/moved.txt');
支持:readFile、writeFile、listFiles、createFolder、delete、move、copy、exists。该文件树保存在 localStorage 的 mxos_api_fs 中。
MXOS.FSconst results = await MXOS.FS.search('report');
const all = await MXOS.FS.listAll();
const children = await MXOS.FS.getChildren(parentId);
await MXOS.clipboard.set('Hello');
const text = await MXOS.clipboard.get();
const history = MXOS.clipboard.history();
history() 最多返回 20 条历史。
MXOS.storage.set('theme', { mode: 'dark' });
const value = MXOS.storage.get('theme');
MXOS.storage.remove('theme');
实际键名由 mxos_storage_ 前缀加业务键名组成。
const theme = MXOS.theme.get();
MXOS.theme.set('dark', '#60a5fa');
const off = MXOS.theme.onChange(function (next) {
console.log(next);
});
const off = MXOS.events.on('demo:event', data => console.log(data));
MXOS.events.emit('demo:event', { message: 'hello' });
MXOS.events.once('demo:once', data => console.log(data));
MXOS.events.off('demo:event');
off();
MXOS.system.lock();
const url = await MXOS.system.screenshot();
const info = MXOS.system.getOSInfo();
screenshot() 依赖浏览器 getDisplayMedia,可能需要用户授权。
MXOS.shortcut.register('ctrl+shift+k', function (event) {
MXOS.dialog.toast('快捷键触发', 'info');
});
MXOS.shortcut.unregister('ctrl+shift+k');
const el = document.querySelector('.scrollable');
MXOS.scroll.apply(el, { autoHide: true, width: 8, radius: 8, color: '#60a5fa' });
MXOS.scroll.scrollToBottom(el);
console.log(MXOS.scroll.getInfo(el));
支持:apply、remove、scrollTo、scrollBy、scrollToTop、scrollToBottom、setTheme、getTheme、isScrollable、getInfo。
HTML 应用 iframe 中可用:
const MXOS = window.MXOS || window.parent.MXOS;
常用能力:
MXOS.storage.set('key', value);
MXOS.storage.get('key');
MXOS.storage.remove('key');
MXOS.storage.clear();
MXOS.storage.keys();
MXOS.notify({ title: '通知', body: '内容' });
MXOS.dialog.confirm('确认?', '说明');
MXOS.dialog.prompt('请输入', '默认值');
MXOS.getAppInfo();
MXOS.getSystemInfo();
沙箱桥接脚本提供:
MXOS.ready(function () {
console.log('bridge ready');
});
MXOS.call('storage.set', ['key', 'value'], { timeout: 30000 });
MXOS.on('permission-granted', data => console.log(data));
MXOS.Storage.set('key', 'value');
MXOS.Storage.get('key');
MXOS.Storage.remove('key');
MXOS.Storage.clear();
MXOS.Storage.keys();
await MXOS.Notification.show({ title: '标题', body: '内容', type: 'info' });
MXOS.Network.fetch('https://example.com');
MXOS.Window.setTitle('新标题');
MXOS.Window.setSize(800, 600);
以下命名空间只有对应模块加载后才可用;未加载时会抛出对应模块尚未加载的错误:
MXOS.user.*MXOS.cloud.*MXOS.shell.runCommand(cmd)MXOS.store.*MXOS.widget.*MXOS.pet.*MXOS.weather.*MXOS.ambient.*MXOS.radio.*MXOS.stamps.*MXOS.evolution.*MXOS.timeCapsule.*MXOS.doodle.*MXOS.graveyard.*MXOS.translate.*MXOS.achievements.*MXOS.launcher.*MXOS.focus.*MXOS.sound.*查看当前 API 可用性:
window.MXOS.apiHelp()
权限定义在 js/sandbox/permissions.js。
| 权限 | 风险 | 说明 |
|---|---|---|
storage | low | 允许应用在本地保存数据 |
notifications | low | 允许应用发送系统通知 |
network | medium | 允许应用访问互联网 |
clipboard | medium | 允许应用读写剪贴板 |
geolocation | high | 允许应用获取地理位置 |
camera | high | 允许应用使用摄像头 |
microphone | high | 允许应用使用麦克风 |
fullscreen | low | 允许应用进入全屏模式 |
window | low | 允许应用修改窗口大小和标题 |
system-info | low | 允许应用读取系统基本信息 |
file-read | medium | 允许应用读取 VFS 文件 |
file-write | high | 允许应用写入 VFS 文件 |
theme | low | 允许应用读取和修改主题 |
声明示例:
{
"permissions": ["storage", "notifications", "network"]
}
授权行为:
mxos_sandbox_permissions。建议遵守最小权限原则:只声明实际需要的权限。
MXOS 应用不需要传统编译。准备 manifest.json、app.bin、assets/svg.png 后压缩为 .mx 即可。
在应用工程目录执行:
Compress-Archive -Path manifest.json,app.bin,assets -DestinationPath my-app.mx -Force
如果先生成了 zip,可重命名:
Rename-Item my-app.zip my-app.mx
const fs = require('fs');
const path = require('path');
const JSZip = require('jszip');
const root = process.cwd();
const zip = new JSZip();
function addFile(rel) {
zip.file(rel.replace(/\\/g, '/'), fs.readFileSync(path.join(root, rel)));
}
function addDir(dir) {
for (const name of fs.readdirSync(path.join(root, dir))) {
const rel = path.join(dir, name);
const abs = path.join(root, rel);
if (fs.statSync(abs).isDirectory()) addDir(rel);
else addFile(rel);
}
}
addFile('manifest.json');
addFile('app.bin');
addDir('assets');
zip.generateAsync({ type: 'nodebuffer', compression: 'DEFLATE' }).then(buffer => {
fs.writeFileSync(path.join(root, 'my-app.mx'), buffer);
console.log('created my-app.mx');
});
运行:
npm install jszip --save-dev
node build-mx.js
.mx 是 zip 格式,文件头为 PK。manifest.json。app.bin。manifest.json 是合法 JSON。manifest.icon 是包内相对路径,且文件名为 svg.png。assets/svg.png。permissions 只声明必要权限。window 尺寸合理。app.bin 是 HTML 或定义了 window.MXOS_APP.init(...)。python serve.py.mx 文件拖入安装区域,或点击选择文件。可通过应用安装器或开始菜单右键卸载。卸载会更新:
state.installedAppsstate.thirdPartyAppDatalocalStorage.mxos_installed_apps部署时保留:
index.html
js/
sw.js
_redirects
MiSans-Normal.ttf
_redirects 内容:
/* /index.html 200
sw.js 使用 network-first + cache fallback:
index.html。开发更新后若仍看到旧页面:
window.__MXOS_BOOT_ERRORS
模块导入失败会记录到该数组。
window.MXOS.apiHelp()
JSON.parse(localStorage.getItem('mxos_installed_apps') || '[]')
window.MXOS.state.installedApps
window.MXOS.state.thirdPartyAppData
JSON.parse(localStorage.getItem('mxos_sandbox_permissions') || '{}')
重置单个应用权限:
const all = JSON.parse(localStorage.getItem('mxos_sandbox_permissions') || '{}');
delete all['com.example.hello'];
localStorage.setItem('mxos_sandbox_permissions', JSON.stringify(all));
HTML 应用:
srcdoc。window.MXOS 和 window.parent.MXOS。脚本应用:
window.MXOS_APP.init 存在。window.__MXOS_APP_API__。app.bin 中未找到 window.MXOS_APP,说明入口对象未定义或脚本执行失败。.mx 包调试Rename-Item my-app.mx my-app.zip
Expand-Archive my-app.zip -DestinationPath .\unpacked -Force
Get-ChildItem .\unpacked -Recurse
正确解包结构应为:
manifest.json
app.bin
assets/svg.png
目录:
hello-mxos
├─ manifest.json
├─ app.bin
└─ assets
└─ svg.png
manifest.json:
{
"id": "com.example.hello",
"name": "Hello MXOS",
"version": "1.0.0",
"manifestVersion": 2,
"description": "MXOS 1.5 最小 HTML 应用示例",
"author": "Example Developer",
"category": "demo",
"icon": "assets/svg.png",
"permissions": ["storage", "notifications"],
"window": {
"width": 760,
"height": 520,
"minWidth": 400,
"minHeight": 300,
"resizable": true
},
"screenshots": []
}
app.bin:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Hello MXOS</title>
<style>
html, body { margin: 0; width: 100%; height: 100%; font-family: Microsoft YaHei, sans-serif; color: #fff; background: #0f172a; }
body { display: flex; align-items: center; justify-content: center; }
.card { padding: 28px; border-radius: 18px; background: rgba(30,41,59,.8); }
button { margin-right: 10px; padding: 10px 16px; border: 0; border-radius: 10px; color: #fff; background: #3b82f6; }
pre { padding: 12px; background: rgba(0,0,0,.35); border-radius: 10px; }
</style>
</head>
<body>
<main class="card">
<h1>Hello MXOS 1.5</h1>
<button id="saveBtn">写入存储</button>
<button id="readBtn">读取存储</button>
<button id="notifyBtn">发送通知</button>
<pre id="log">Ready.</pre>
</main>
<script>
const MXOS = window.MXOS || window.parent.MXOS;
const log = document.getElementById('log');
const print = value => log.textContent = typeof value === 'string' ? value : JSON.stringify(value, null, 2);
document.getElementById('saveBtn').onclick = function () {
const value = { time: new Date().toISOString(), message: 'Hello from MXOS app' };
if (MXOS.storage && MXOS.storage.set) {
MXOS.storage.set('hello_state', value);
print({ saved: value });
} else if (MXOS.Storage && MXOS.Storage.set) {
MXOS.Storage.set('hello_state', value).then(() => print({ saved: value }));
}
};
document.getElementById('readBtn').onclick = function () {
if (MXOS.storage && MXOS.storage.get) print(MXOS.storage.get('hello_state'));
else if (MXOS.Storage && MXOS.Storage.get) MXOS.Storage.get('hello_state').then(print);
};
document.getElementById('notifyBtn').onclick = function () {
if (typeof MXOS.notify === 'function') MXOS.notify({ title: 'Hello MXOS', body: '通知发送成功', type: 'success' });
else if (MXOS.Notification) MXOS.Notification.show({ title: 'Hello MXOS', message: '通知发送成功' });
};
</script>
</body>
</html>
准备 PNG 图标:
assets/svg.png
建议尺寸:256x256。
打包:
Compress-Archive -Path manifest.json,app.bin,assets -DestinationPath hello-mxos.mx -Force
manifest.json:
{
"id": "com.example.scriptapp",
"name": "Script App",
"version": "1.0.0",
"manifestVersion": 2,
"description": "MXOS 1.5 脚本入口示例",
"author": "Example Developer",
"category": "demo",
"icon": "assets/svg.png",
"permissions": ["storage", "notifications", "window", "network"],
"window": { "width": 720, "height": 480, "resizable": true }
}
app.bin:
window.MXOS_APP = {
init: function (windowEl) {
const api = window.__MXOS_APP_API__;
const content = windowEl.querySelector('.window-content');
api.setWindowTitle('Script App 示例');
api.setWindowSize(720, 480);
content.innerHTML = `
<div style="padding:24px;color:var(--color-foreground,#0F172A);background:var(--color-background,#F8FAFC);font-family:MiSans,Microsoft YaHei,sans-serif">
<h2 style="margin-top:0">Script App 示例</h2>
<button id="save">保存</button>
<button id="load">读取</button>
<button id="notify">通知</button>
<button id="fetch">网络请求</button>
<pre id="out" style="margin-top:16px;padding:12px;border-radius:10px;background:rgba(0,0,0,.35);white-space:pre-wrap"></pre>
</div>
`;
const out = content.querySelector('#out');
const print = value => out.textContent = typeof value === 'string' ? value : JSON.stringify(value, null, 2);
content.querySelector('#save').onclick = function () {
api.Storage.set('counter', Date.now());
print('已保存 counter');
};
content.querySelector('#load').onclick = function () {
print(api.Storage.get('counter'));
};
content.querySelector('#notify').onclick = function () {
api.Notification.show({ title: 'Script App', message: '通知测试' });
};
content.querySelector('#fetch').onclick = async function () {
try {
const res = await api.Network.fetch('https://example.com');
print({ ok: res.ok, status: res.status, statusText: res.statusText });
} catch (err) {
print('请求失败:' + err.message);
}
};
}
};
.mx zip 文件原因可能是文件不是 zip、下载到错误 HTML 页面、或包损坏。将 .mx 改名为 .zip 后尝试解压验证。
manifest.json 不存在通常是打包时多包了一层目录。manifest.json 必须在 zip 根目录,而不是 my-app/manifest.json。
app.bin 不存在确保 app.bin 位于 zip 根目录,文件名大小写正确。
manifest.icon 必须指向 svg.png当前安装器要求文件名严格为 svg.png。推荐:
{ "icon": "assets/svg.png" }
检查 manifest.permissions,并查看 localStorage:
JSON.parse(localStorage.getItem('mxos_sandbox_permissions') || '{}')
清理 Service Worker 与 Cache Storage,然后强制刷新。
window.parent.MXOS 与 window.MXOS 差异新应用优先使用 window.MXOS;兼容旧包时再回退 window.parent.MXOS。
window.MXOS_APP确保 app.bin 中定义了:
window.MXOS_APP = {
init: function (windowEl) {}
};
manifest.json 在 zip 根目录。app.bin 在 zip 根目录。assets/svg.png 存在。manifest.icon 指向包内 svg.png。manifest.id 唯一且稳定。manifest.name 可读。manifest.version 已更新。permissions 只包含必要权限。window.width、window.height、minWidth、minHeight 合理。window.MXOS_APP.init(windowEl)。# 启动 MXOS
python serve.py
# 打包应用
Compress-Archive -Path manifest.json,app.bin,assets -DestinationPath my-app.mx -Force
// 检查已安装应用
JSON.parse(localStorage.getItem('mxos_installed_apps') || '[]')
// 检查权限
JSON.parse(localStorage.getItem('mxos_sandbox_permissions') || '{}')
// 查看 API
window.MXOS.apiHelp()
// 查看启动错误
window.__MXOS_BOOT_ERRORS