MXOS 1.5 应用开发文档

适用范围:MXOS 1.5 当前源码与运行行为
版本注意:本文档以当前 MXOS 1.5 源码行为为准。源码中个别内部字符串可能仍显示 1.61.1 或 demo 文案 v2,不影响本文面向 MXOS 1.5 的开发说明。


1. 概览

MXOS 1.5 是运行在浏览器中的静态 Web 伪系统。第三方应用通过 .mx 包安装,安装器读取包内 manifest.jsonapp.bin,将应用元数据与内容保存到浏览器本地状态,并把应用加入开始菜单。

第三方应用有两种入口:

  1. HTML 应用app.bin 是完整 HTML,运行在 iframe srcdoc 中。
  2. 脚本应用app.bin 是 JavaScript,需定义 window.MXOS_APP = { init(windowEl) { ... } }

当前安装器的硬性要求:


2. 环境与运行

2.1 浏览器要求

建议使用现代 Chromium 系浏览器:Edge、Chrome 或 Chromium。需要支持:

2.2 关键入口

index.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

2.3 本地启动

python serve.py

默认地址:

http://localhost:8080

serve.py 会在项目根目录中启动静态服务器,并设置 no-cache 头:

Cache-Control: no-cache, no-store, must-revalidate
Pragma: no-cache
Expires: 0

2.4 外部依赖

安装器/商店会动态加载 JSZip:

https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js

如果网络不可用,安装 .mx 时可能出现 JSZip 加载失败


3. 目录与架构

项目根目录
├─ 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

启动流程摘要:

  1. main.js 加载配置、状态、核心窗口系统和桌面模块。
  2. api.js 挂载 window.MXOS 公共 API。
  3. 加载锁屏、主题、通知、动画、日志等基础功能。
  4. 初始化桌面。
  5. 延迟加载内置应用、功能模块、系统工具、媒体模块和沙箱模块。
  6. 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

4. 应用包格式 .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,..." }

5. 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"
    }
  ]
}

字段说明:

字段类型必需说明
idstring建议应用唯一 ID;缺省时安装器会自动生成临时 ID
namestring建议应用显示名称;缺省为 未知应用
versionstring应用版本;缺省 1.0.0
manifestVersionnumber建议 2
descriptionstring应用描述
authorstring作者
categorystring分类,如 demotoolsmedia
iconstring包内 svg.png 路径,如 assets/svg.png
permissionsstring[]权限声明
windowobject初始窗口配置
screenshotsobject[]截图元数据

window 字段:

字段类型默认值说明
widthnumber800初始宽度
heightnumber600初始高度
minWidthnumber400最小宽度
minHeightnumber300最小高度
resizablebooleantrue是否允许调整窗口

6. app.bin 应用入口

6.1 HTML 应用

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

6.2 脚本应用

脚本应用的 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' });
    };
  }
};

7. API 参考

7.1 宿主全局 API

可在宿主环境通过 window.MXOS 访问。

通知

const id = MXOS.notify({
  title: '保存成功',
  body: '文件已保存到本地',
  type: 'success',
  duration: 5000,
  actions: [{ label: '查看', onClick: function () {} }],
  bypassDnd: false
});

type 支持 infosuccesswarningerrorbody 是标准正文参数,兼容层也接受 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 支持 fileIdinitialContenttitle
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');

支持:minimizemaximizeclosefocussnapgetBoundssetBounds

文件系统:MXOS.fs

await 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');

支持:readFilewriteFilelistFilescreateFolderdeletemovecopyexists。该文件树保存在 localStorage 的 mxos_api_fs 中。

高级 VFS:MXOS.FS

const 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));

支持:applyremovescrollToscrollByscrollToTopscrollToBottomsetThemegetThemeisScrollablegetInfo

7.2 第三方 HTML 应用桥接 API

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();

7.3 沙箱 RPC API

沙箱桥接脚本提供:

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);

7.4 代理 API

以下命名空间只有对应模块加载后才可用;未加载时会抛出对应模块尚未加载的错误:

查看当前 API 可用性:

window.MXOS.apiHelp()

8. 权限模型

权限定义在 js/sandbox/permissions.js

权限风险说明
storagelow允许应用在本地保存数据
notificationslow允许应用发送系统通知
networkmedium允许应用访问互联网
clipboardmedium允许应用读写剪贴板
geolocationhigh允许应用获取地理位置
camerahigh允许应用使用摄像头
microphonehigh允许应用使用麦克风
fullscreenlow允许应用进入全屏模式
windowlow允许应用修改窗口大小和标题
system-infolow允许应用读取系统基本信息
file-readmedium允许应用读取 VFS 文件
file-writehigh允许应用写入 VFS 文件
themelow允许应用读取和修改主题

声明示例:

{
  "permissions": ["storage", "notifications", "network"]
}

授权行为:

建议遵守最小权限原则:只声明实际需要的权限。


9. 编译与打包

MXOS 应用不需要传统编译。准备 manifest.jsonapp.binassets/svg.png 后压缩为 .mx 即可。

9.1 PowerShell 打包

在应用工程目录执行:

Compress-Archive -Path manifest.json,app.bin,assets -DestinationPath my-app.mx -Force

如果先生成了 zip,可重命名:

Rename-Item my-app.zip my-app.mx

9.2 Node / JSZip 打包脚本

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

9.3 打包前检查清单


10. 安装、部署与更新

10.1 本地安装

  1. 启动 MXOS:
    python serve.py
  2. 打开 MXOS。
  3. 打开"应用安装器"。
  4. .mx 文件拖入安装区域,或点击选择文件。
  5. 如出现权限确认,按需允许。
  6. 安装成功后,应用出现在开始菜单。

10.2 卸载

可通过应用安装器或开始菜单右键卸载。卸载会更新:

10.3 静态部署

部署时保留:

index.html
js/
sw.js
_redirects
MiSans-Normal.ttf

_redirects 内容:

/* /index.html 200

10.4 Service Worker 缓存

sw.js 使用 network-first + cache fallback:

开发更新后若仍看到旧页面:

  1. DevTools → Application → Service Workers → unregister。
  2. 清空 Cache Storage。
  3. 清空相关 localStorage。
  4. 强制刷新页面。

11. 调试指南

11.1 启动错误

window.__MXOS_BOOT_ERRORS

模块导入失败会记录到该数组。

11.2 API 可用性

window.MXOS.apiHelp()

11.3 已安装应用

JSON.parse(localStorage.getItem('mxos_installed_apps') || '[]')
window.MXOS.state.installedApps
window.MXOS.state.thirdPartyAppData

11.4 权限状态

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));

11.5 iframe / app.bin 调试

HTML 应用:

脚本应用:

11.6 .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

12. 完整示例

12.1 最小 HTML 应用

目录:

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

12.2 脚本应用示例

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);
      }
    };
  }
};

13. 常见问题

安装包不是有效的 .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" }

权限未授权导致 API 抛错

检查 manifest.permissions,并查看 localStorage:

JSON.parse(localStorage.getItem('mxos_sandbox_permissions') || '{}')

修改后仍运行旧版本

清理 Service Worker 与 Cache Storage,然后强制刷新。

HTML 应用中 window.parent.MXOSwindow.MXOS 差异

新应用优先使用 window.MXOS;兼容旧包时再回退 window.parent.MXOS

脚本应用提示未找到 window.MXOS_APP

确保 app.bin 中定义了:

window.MXOS_APP = {
  init: function (windowEl) {}
};

14. 发布检查清单


附录:快速命令

# 启动 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