M·Desk /开发者文档
↓下载 mpkpack

快速开始

一个 MPK = 你的文件 + 一个 manifest.json。manifest 只回答三个问题。

问题写哪个字段
这是什么应用id、version、name、icon
怎么启动runtime:native(跑二进制)或 docker(跑容器)
浏览器怎么访问web.listen:port(占一个端口)或 socket(同源路径)

其余字段都是这三个问题的细节,完整清单见 manifest 字段。

第 1 步:摆目录

my-app/
  manifest.json      ← 唯一手写的文件
  assets/icon.png    ← 图标(PNG / JPEG / WebP / SVG)
  bin/app            ← 你的程序
  config/            ← 其他资源,可有可无

第 2 步:写 manifest

先问自己:在命令行里我会怎么启动它?然后把这一行逐段翻译成 JSON —— 会随环境变的值换成 ${MDESK_*},其余原样抄。

./app --listen=0.0.0.0:8080 --data=/var/lib/app
命令行里manifest 里
./app"executable": "bin/app"
--listen=0.0.0.0:8080"--listen=0.0.0.0:${MDESK_PORT}"
--data=/var/lib/app"--data=${MDESK_DATA_DIR}"
在哪个目录跑"workingDir": "."
{
  "schema": 1,
  "id": "mdesk.example.myapp",
  "version": "1.0.0",
  "name": "示例应用",
  "description": "我的第一个 MPK 应用。",
  "architectures": ["amd64"],
  "icon": "assets/icon.png",
  "runtime": "native",
  "web": { "listen": "port", "openPath": "/", "healthPath": "/healthz" },
  "native": {
    "executable": "bin/app",
    "args": ["--listen=0.0.0.0:${MDESK_PORT}", "--data=${MDESK_DATA_DIR}"],
    "workingDir": "."
  }
}

第 3 步:打包

下载 mpkpack(6 个平台,不用装 Go),在源目录的上一级执行:

mpkpack validate ../my-app
mpkpack build ../my-app --output ../dist/mdesk.example.myapp-1.0.0.mpk
mpkpack validate ../dist/mdesk.example.myapp-1.0.0.mpk

三条都过就得到合法的 .mpk。任何一条报错,命令会指出具体哪个字段不对。

第 4 步:装进去

M·Desk 后台 → 应用中心 → 安装应用 → 选刚生成的 .mpk。装完点启动:状态「运行中」说明程序起来了,能打开页面说明路径没问题。打不开看 排错。

!

安装时报 package contains too many json metadata files,是 M·Desk 客户端太旧(2.7.114 及更早的验证器缺陷),不是你的包有问题。

manifest 字段

唯一需要手写的文件。字段分四组:顶层、web、native / docker、store。

完整示例

{
  "schema": 1,
  "id": "mdesk.example.myapp",
  "version": "1.0.0",
  "name": "示例应用",
  "description": "一句话说明这个应用。",
  "architectures": ["amd64", "arm64"],
  "icon": "assets/icon.png",
  "runtime": "native",
  "web": { "listen": "port", "openPath": "/", "healthPath": "/healthz" },
  "native": {
    "executable": "bin/app",
    "args": ["--listen=0.0.0.0:${MDESK_PORT}"],
    "workingDir": "."
  }
}

顶层字段

字段必填填什么
schema是固定写数字 1
id是应用唯一标识,匹配 ^[a-z0-9][a-z0-9._-]{2,63}$,建议反向域名
version是SemVer 三段式如 1.0.0。改了版本才能升级
name是显示名称,1–80 字符,可用中文
runtime是native 或 docker,见 四条路线
web是访问契约,见下
description否最多 500 字符
architectures否空数组表示不限制。写错会在别的机器上提示架构不匹配
icon否包内相对路径,仅 PNG / JPEG / WebP / SVG
native / docker条件与 runtime 对应,二者只能有其一
wizard否安装表单,见 安装向导
store—商店展示信息。不填的字段打包时会自动补上。见 发布信息

web 段

字段说明
listenport 或 socket,决定注入哪个变量
preferredPort仅 port 模式。端口被占时 M·Desk 会改分配,这不是强制端口
openPath点图标时打开的页面,通常是 /
healthPath健康检查路径。2xx/3xx 视为正常,401/403 也算服务活着
openMode可选。默认用哪种方式打开:new-tab(浏览器新标签页)或 in-app(桌面窗口内)。不写则按 listen 推断:port → new-tab,socket → in-app

openMode:应用该在哪儿打开

默认行为是跟着 listen 走的,因为这两者本来就绑在一起: socket 的应用和 M·Desk 同源,可以放进窗口里的 iframe; port 的应用是另一个源(不同端口),浏览器不允许这样嵌,只能开新标签页。

如果你的应用情况特殊,可以显式声明 openMode 覆盖这个默认。常见两种情况:

  • 端口应用但能在窗口里跑:程序自己允许被嵌入(不发 X-Frame-Options: DENY,也不限制 frame-ancestors),那可以写 in-app,用户体验更连贯
  • 套接字应用但需要完整浏览器能力:比如要用剪贴板、文件下载、OAuth 跳转,写 new-tab 更稳妥
"web": {
  "listen": "port",
  "openPath": "/",
  "healthPath": "/healthz",
  "openMode": "in-app"
}

这只是默认值,不是强制。用户建好桌面图标后,可以在图标的编辑对话框里改成另一种打开方式,用户的选择优先于这里声明的值。 用户没改过时,包升级后改了 openMode,已有图标会自动跟着更新。

i

只接受两个值。写错(比如 window、iframe、tab)会在 mpkpack validate 和安装时直接报错,报错信息里会点名 web.openMode。大小写和首尾空格会自动归一化,NEW-TAB 等同于 new-tab。

native 段

字段说明
executable包内相对路径。构建时只有这个文件被设为 0755,其余一律 0644
args参数数组,一个参数一个字符串,见 参数与变量
workingDir工作目录,. 表示版本目录

docker 段

字段说明
compose包内 Compose 文件路径,如 docker-compose.yml

多架构怎么打包

商店按 id 建索引:一个 id 只对应一个条目、一个包文件。所以不能按架构拆成多个包 —— 同 id 再导入会报「已存在」,加 --force 会直接覆盖,先导入的架构就没了。

情况怎么做
native,多架构各架构二进制都放进同一个包,executable 指向启动脚本,按 uname -m 分发
docker,多架构用多架构镜像,architectures 写全,Docker 会自动拉对应架构
只有一种架构architectures 只写那一个。其他架构的机器上这个应用直接不显示(不是灰掉,是整条看不到)
"architectures": ["amd64", "arm64"],
"native": { "executable": "bin/run", "args": ["--listen=0.0.0.0:${MDESK_PORT}"] }
#!/bin/sh
# bin/run —— 构建时只有它被设为 0755,其余文件是 0644
case "$(uname -m)" in
  x86_64|amd64)  BIN=app-amd64 ;;
  aarch64|arm64) BIN=app-arm64 ;;
  *) echo "unsupported arch: $(uname -m)" >&2; exit 1 ;;
esac
DIR="$(dirname "$0")"
chmod +x "$DIR/$BIN" 2>/dev/null || true   # 否则 permission denied
exec "$DIR/$BIN" "$@"
!

别把 architectures 写空来「兼容所有机器」。空数组等于不限制,会在跑不了的架构上装成功、启动才失败。

写 JSON 的三个坑

✕
  • 不能写注释。JSON 不支持 // 和 /* */。
  • 最后一个字段后不能有逗号。
  • 文件必须是 UTF-8。中文直接写,不用转 \uXXXX。

不确定就 mpkpack validate,它会指出哪一行有问题。

◆

id 同时是安装目录名、/apps/<id>/ 路径段和 Compose 项目名的来源。定下来后不要改 —— 改了等于换一个应用,用户数据和快捷方式都对不上。

四条路线

两个独立选择组合成四条路:怎么跑(native / docker)× 怎么访问(port / socket)。

选择一:怎么跑

nativedocker
跑什么包内的可执行文件Compose 定义的容器
适合单文件二进制、自带运行时已有镜像、多服务、依赖复杂

选择二:怎么访问

portsocket
访问地址http://主机:端口//apps/<id>/...
占用端口占一个宿主端口不占端口
Cookie与其他端口应用共享主机域天然同源隔离
程序改动无要支持 Socket + 路径前缀
注入变量${MDESK_PORT}${MDESK_SOCKET}、${MDESK_BASE_PATH}
◆

先用 port。只有程序本身支持 Socket 且能处理路径前缀时,才用 socket 换同源体验。

A · Native + Port

最简单的一条路,程序不用改。要求:能监听动态分配的端口,且监听 0.0.0.0。

"runtime": "native",
"web": { "listen": "port", "preferredPort": 18080, "openPath": "/", "healthPath": "/healthz" },
"native": {
  "executable": "bin/app",
  "args": ["--listen=0.0.0.0:${MDESK_PORT}", "--data=${MDESK_DATA_DIR}"],
  "workingDir": "."
}
!

只支持固定端口的程序走不通这条路 —— preferredPort 只是「首选」,端口冲突时会被改分配。

B · Native + Socket

要求:程序能监听 Unix Socket、承载 HTTP,并正确处理 /apps/<id>/ 前缀。

"runtime": "native",
"web": { "listen": "socket", "openPath": "/", "healthPath": "/healthz" },
"native": {
  "executable": "bin/app",
  "args": [
    "--socket=${MDESK_SOCKET}",
    "--base-path=${MDESK_BASE_PATH}",
    "--data=${MDESK_DATA_DIR}"
  ],
  "workingDir": "."
}

Socket 路径不要自己写(如 /tmp/app.sock),一律用 ${MDESK_SOCKET}。代理只按 HTTP 使用,非 HTTP 的 Socket 跑不起来。

C · Docker + Port

已有 Compose 的项目首选。要求:用已发布镜像(不现场 build),主服务只有一个 HTTP 端口。

"runtime": "docker",
"web": { "listen": "port", "preferredPort": 18081, "openPath": "/", "healthPath": "/" },
"docker": { "compose": "docker-compose.yml" }
services:
  web:
    image: example/webapp:1.0.0
    ports:
      - "${MDESK_PORT}:8080"      # 宿主端口用变量,容器端口写死
    volumes:
      - "${MDESK_DATA_DIR}:/data"  # 不挂这里,升级后数据会丢
    restart: unless-stopped

不要写 container_name(多实例会冲突)。Compose 里的 ${MDESK_*} 由 M·Desk 传给 docker compose 命令本身,写的是变量名。

D · Docker + Socket

要求:容器内服务能监听 Unix Socket,且能处理路径前缀。

"runtime": "docker",
"web": { "listen": "socket", "openPath": "/", "healthPath": "/healthz" },
"docker": { "compose": "docker-compose.yml" }
services:
  web:
    image: example/webapp:1.0.0
    command: ["--socket=/run/mdesk/app.sock", "--base-path=${MDESK_BASE_PATH}"]
    volumes:
      - "${MDESK_RUN_DIR}:/run/mdesk"   # Socket 目录挂进容器
      - "${MDESK_DATA_DIR}:/data"
✕

Compose 里不能写 ${field.x},只认环境变量名。写成字段引用会报 invalid interpolation format,表现为「包装不上」。

socket 必读:子路径适配

socket 模式下 M·Desk 把应用挂在 /apps/<id>/ 并且不剥离前缀。程序收到的请求自带前缀,任何写死 / 的地方都要改。

位置常见错误正确做法
HTML 资源<script src="/app.js">相对路径 ./app.js,或拼 base
前端 RoutercreateWebHistory() 无 basecreateWebHistory(window.__BASE__)
API 请求fetch('/api/x')fetch(base + '/api/x')
重定向Location: /loginLocation: /apps/<id>/login
CookiePath=/Path=/apps/<id>
WebSocketws://host/ws由当前 location 推导
Service Workerscope 落在根scope 与注册路径都带前缀
iframeX-Frame-Options: DENY允许同源嵌入,否则窗口里空白

最省事的做法是从当前页面反推前缀,不要写死应用 ID:

const base = new URL(".", window.location.href);
function appUrl(path) {
  return new URL(path.replace(/^\/+/, ""), base).toString();
}

服务端要生成重定向或 HTML 时,把 ${MDESK_BASE_PATH} 传进去:"args": ["--base-path=${MDESK_BASE_PATH}"]。

◆

改不动就别硬改。退回 port 模式能立刻可用,代价只是多占一个端口。

参数与变量

最容易写错的地方。一句话:把命令行搬进数组,会变的值换成占位符。

args 是数组,不是一整行

"args": ["--listen=0.0.0.0:${MDESK_PORT}", "--data=${MDESK_DATA_DIR}"]

等价于 ./app "--listen=0.0.0.0:8080" "--data=/data/mpk/apps/xxx"。一个数组元素 = 一个参数,不要塞多个。

这里不经过 shell

你写的结果
|、>、&&、$HOME不解释,原样传给程序,程序看不懂
$( 或反引号直接报错,应用起不来:native argument N contains a shell expression

需要 shell 逻辑(管道、多进程、条件判断)就写个启动脚本,把 executable 指向它。脚本里可以直接读 $MDESK_PORT —— M·Desk 把这些值作为环境变量传给子进程。

#!/bin/sh
exec "$(dirname "$0")/bin/app" --listen="0.0.0.0:${MDESK_PORT}" --data="${MDESK_DATA_DIR}"

10 个占位符

占位符值典型用途
${MDESK_PORT}分配给你的 TCP 端口--listen=0.0.0.0:${MDESK_PORT}
${MDESK_SOCKET}Unix Socket 完整路径--socket=${MDESK_SOCKET}
${MDESK_BASE_PATH}访问前缀,如 /apps/<id>--base-path=${MDESK_BASE_PATH}
${MDESK_DATA_DIR}持久数据目录--data=${MDESK_DATA_DIR}
${MDESK_APP_ROOT}当前版本安装目录(升级会换)读包内只读资源
${MDESK_VAR_DIR}可重建的临时目录缓存
${MDESK_RUN_DIR}运行时文件目录放 pid、Socket
${MDESK_LOG_DIR}日志目录--log=${MDESK_LOG_DIR}/app.log
${MDESK_APP_ID}应用 ID日志前缀、上报
${MDESK_APP_VERSION}应用版本号上报、缓存标记

两个变量互斥

web.listen${MDESK_PORT}${MDESK_BASE_PATH}
port实际端口空字符串
socket空字符串/apps/<id>

不要同时依赖这两个。${MDESK_SOCKET} 则始终有值。

数据写哪里

存什么写到为什么
用户数据、数据库、配置${MDESK_DATA_DIR}升级、停止、卸载保留数据时都不动
缓存、可重建的文件${MDESK_VAR_DIR}丢了能重建
程序自带的只读资源${MDESK_APP_ROOT}跟着版本走,升级后会变
✕

不要写死端口(8080),端口冲突时直接起不来。不要写死路径(/tmp/app.sock),换环境就失效。

构建打包

下载 mpkpack,三条命令得到成品。不用装 Go,也不用源码。

下载 mpkpack

系统下载大小SHA256 前 16 位
Windows(x64)mpkpack-windows-amd64.exe4.9 MBe8d256c5ef376b13
Linux(x64)mpkpack-linux-amd644.8 MB48bfb4ee204066e6
Linux(ARM64,多数 NAS)mpkpack-linux-arm644.5 MBa6f79b7ea283b35f
Linux(ARMv7)mpkpack-linux-arm4.8 MBf1eab79716b50a71
macOS(Apple 芯片)mpkpack-darwin-arm644.5 MB25fb5e23489868c5
macOS(Intel)mpkpack-darwin-amd644.8 MB61269b15928f68ad

完整校验和:checksums.txt,下载后可核对:

sha256sum -c checksums.txt

Linux / macOS 加执行权限即可:

chmod +x mpkpack-linux-amd64
./mpkpack-linux-amd64 validate ../my-app

# 想直接用 mpkpack 这个名字,放进 PATH
sudo mv mpkpack-linux-amd64 /usr/local/bin/mpkpack

Windows 直接运行 mpkpack-windows-amd64.exe。

◆

整份文档是单文件 HTML,可以存一份离线看:下载本文档 —— 断网也能查,样式和表格全保留。

生成骨架(可选)

mpkpack init ../my-app \
  --id mdesk.example.myapp \
  --name "示例应用" \
  --runtime native \
  --listen socket

生成最小骨架,之后把资源、启动器和图标放进去,再按 字段表 补 manifest.json。

三条命令

# 1. 校验源目录
mpkpack validate ../my-app

# 2. 构建
mpkpack build ../my-app --output ../dist/mdesk.example.myapp-1.0.0.mpk

# 3. 校验成品
mpkpack validate ../dist/mdesk.example.myapp-1.0.0.mpk

files= 是你的文件数,会不一样。checksums.json 由构建器生成,不要手改。覆盖已有输出要加 --force。

文件权限

构建时只有 native.executable 被设为 0755,其余一律 0644。脚本内部调用的其他程序要自己 chmod。

失败对照

提示原因处理
manifest 解析失败注释、多余逗号、非 UTF-8按「写 JSON 的三个坑」检查
unknown field字段名拼错,或用了旧版不认识的段对照字段表
icon not found图标路径与包内实际不符改 icon 或放对文件
executable not foundnative.executable 指向不存在的文件改路径
contains a shell expressionargs 里有 $( 或反引号去掉,改用启动脚本
unsupported file包内有符号链接、设备文件、FIFO换成普通文件

发布信息

在 manifest 里加一个 store 段,打包,交付。其余信息商店从包里自动读。

不写也行 —— 打包时会把没填的字段自动补上。写了就按你写的来。

第 1 步:写 store 段

三个字段决定商店里怎么展示你:

"store": {
  "category": "工具",
  "author": "M·Desk",
  "developer": "示例团队"
}
字段不填时说明上限
category未分类商店分组,如 工具、AI、媒体160 字
authorM·Desk维护者,显示在商店页面160 字
developer应用名称开发者署名160 字
introduction同 description长介绍16000 字
summary同 description列表摘要4000 字

介绍不用写两遍:introduction 与 summary 留空时自动用 description。

第 2 步:打包并交付

mpkpack build ../my-app --output ../dist/mdesk.example.myapp-1.0.0.mpk

把 .mpk 交给商店即可。名称、版本、ID、架构、图标、包大小、哈希商店都从包里取,不用另外说明。

发新版本:改 version(不改版本号商店认不出是更新)→ 重新打包 → 重新交付。

搭建自己的源进阶

三步:打包 → 生成目录 → 扔到网上。用户填一个网址,就能在应用中心看到并安装你的应用。

应用源就是一个普通静态目录:没有后端、没有数据库、不用登录、不用写一行服务端代码。Nginx、对象存储、GitHub Pages、内网文件服务器,随便哪个都行。

三分钟跑通

# 1. 打包
mpkpack build ./my-app -output ./dist/example.app-1.0.0.mpk

# 2. 生成源目录
mpkpack store ./dist/example.app-1.0.0.mpk --store-dir ./my-store

# 3. 把这个目录整个传到网上,比如放到 https://apps.example.com/mystore/

然后在 M·Desk 里打开 应用中心 → 三方订阅源 → 管理订阅,填上那个网址,保存。应用中心侧栏第二项就是你的源。

i

mpkpack store 会自动建好 index.json、每个应用的详情文件、icons/ 和 packages/。这些文件不要手写、不要手改,改了哈希就对不上。格式细节见 源目录格式。

用户在哪儿添加

在应用中心切到「三方订阅源」,点右上角 管理订阅,表单里填两样:

填什么规则例子
名称最多 60 字,显示在管理列表里我的应用源
地址http(s),指向放 index.json 的那个目录https://apps.example.com/mystore/

保存后立即拉取一次。拉取失败不影响管理列表,状态会显示「未加载」,应用中心点一次刷新即可重试。同一个地址重复添加会被拒绝。

应用 ID 会被自动加前缀

这是整套机制里唯一需要记住的一件事。mpkpack store 入库时统一加 mdesk.,M·Desk 再把它换成该源自己的前缀:

manifest.json 里你写的        id = "example.app"
mpkpack store 入库后         id = "mdesk.example.app"     ← index.json 里长这样
用户在应用中心看到的          id = "myco.example.app"      ← 前缀由系统分配

你不用改 manifest,也不用管前缀。 好处是两个不同的源可以发同名应用,互不冲突:一个叫 myco.example.app,另一个叫 other.example.app,用户装哪个就是哪个。

系统给每个源分配前缀时会避开内置源占用的名字:

mdesk   casaos   1panel   official   installed   thirdparty   online

所以你不用担心自己的应用跟官方源撞名 —— 撞名也是两个独立条目。

发新版本

# 先改 manifest 里的 version,重新打包
mpkpack build ./my-app -output ./dist/example.app-1.1.0.mpk
# 再入库,覆盖旧条目
mpkpack store ./dist/example.app-1.1.0.mpk --store-dir ./my-store --force

然后让用户在应用中心点一次「刷新」。不点刷新就继续看缓存里的旧版本。

源之间互不干扰

每个源有自己的目录缓存、自己的图标缓存、自己的刷新时间:

<数据目录>/mdeskapps/
  catalog-cache.json                 ← M·Desk 官方源
  sources/
    myco/catalog-cache.json          ← 你的源
    other/catalog-cache.json         ← 别人的源

一个源挂了、停了、返回乱数据,其他源照常显示。停用某个源只让它从列表消失,已安装的应用不会被卸载。

刷新与失效

情况表现
刚添加立即拉一次;网络慢也不卡住管理列表
平时走磁盘缓存,只有用户点「刷新」才重新拉
源暂时打不开继续用缓存,列表照常显示,带刷新警告
停用源列表里消失,直接访问它的应用 ID 返回 404
删除源同上;已安装的应用继续能跑

上线前自检

检查怎么验
index.json 打得开浏览器访问「你的地址 + index.json」,返回 JSON 而不是 404
图标能加载访问某条应用的 icon 相对路径
包能下载访问 package.path,大小与 package.size 一致
哈希对得上sha256sum 结果与 package.sha256 相同
详情文件都在每个 detail 指向的 .json 存在,且 id 与清单一致
子目录可用放在 /mystore/ 这类子路径下也没问题

常见卡住的地方

现象原因处理
状态一直「未加载」地址少了结尾 /,或路径下没有 index.json用浏览器直接打开「地址 + index.json」确认能返回
列表是空的只放了 index.json,没放 packages/ 和 icons/整个目录一起传,别挑文件
图标裂开icons/ 没传,或打包时 manifest 没写 iconmanifest 写 icon 后重新 build + store
安装报「包校验失败」手改过 index.json 或 packages/ 里的文件重新 mpkpack store --force,不要手改
提示「已存在」别的源已经发过同一个 packageId改 manifest 的 id,同名就是同一个应用

上限

项目上限
一台设备能加的源20 个
单个源的应用数1000 个
index.json 大小2 MB
单个图标大小8 MB

超出上限时那个源会直接报错、不显示,其他源不受影响。应用特别多就拆成多个源。

源目录格式

了解结构就够排查问题了。正常情况下这些文件全由 mpkpack store 生成,你不该手写。

目录结构

my-store/
  index.json                          ← 清单:有哪些应用
  thirdparty.demo.json                ← 详情,文件名 = packageId + .json
  icons/
    thirdparty.demo.svg               ← 图标,独立文件
  packages/
    thirdparty.demo-1.0.0.mpk         ← 安装包本体

四部分缺一不可。detail、icon、package.path 都是相对这个目录的路径。

!

文件名用的是 packageId(你在 manifest 里写的那个),不是带 mdesk. 前缀的 id。两者只差一个前缀,手写时最容易搞错。

index.json

只有清单,不含安装包信息。真实产物:

{
  "schema": 1,
  "name": "M·Desk 应用",
  "updatedAt": "2026-10-22T19:01:54+08:00",
  "apps": [
    {
      "schema": 1,
      "id": "mdesk.thirdparty.demo",
      "packageId": "thirdparty.demo",
      "name": "三方示例",
      "summary": "来自三方订阅源的示例应用",
      "category": "工具",
      "version": "1.0.0",
      "author": "Third Party",
      "developer": "第三方开发者",
      "icon": "icons/thirdparty.demo.svg",
      "detail": "thirdparty.demo.json",
      "introduction": "来自三方订阅源的示例应用"
    }
  ]
}
字段说明
schema固定 1
name源名称,仅供展示
updatedAt生成时间,RFC 3339
apps[].id对外 ID,必须以 mdesk. 开头,否则整个源被判非法。正常由工具自动加,手写要自己带
apps[].packageId你 manifest 里写的原始 ID,全局唯一
apps[].detail详情文件名,不是 id + .json,而是 packageId + .json
apps[].icon图标相对路径;manifest 没写 icon 时这个字段不存在
✕

apps[].id 缺了 mdesk. 前缀,程序会直接报 invalid application ID,整个 index.json 作废。这是手写源最容易踩的坑 —— 前缀不是你随便起的名字,它是硬性格式的一部分。

详情文件

比清单条目多一个 package 段,其余字段相同:

{
  "schema": 1,
  "id": "mdesk.thirdparty.demo",
  "packageId": "thirdparty.demo",
  "name": "三方示例",
  "summary": "来自三方订阅源的示例应用",
  "category": "工具",
  "version": "1.0.0",
  "author": "Third Party",
  "developer": "第三方开发者",
  "icon": "icons/thirdparty.demo.svg",
  "detail": "thirdparty.demo.json",
  "introduction": "来自三方订阅源的示例应用",
  "package": {
    "path": "packages/thirdparty.demo-1.0.0.mpk",
    "sha256": "51b39a0b8e4ffc57443ffa80eb778a1d147f91c9a2759b5532bfb67028f2f478",
    "size": 763
  }
}
字段说明
package.path相对源目录的包路径
package.sha256包的 SHA-256,安装前逐字节校验
package.size包的字节数

字段从哪来

清单和详情里的展示信息,全部来自你 manifest 的 store 段和顶层字段:

源目录字段来自 manifest
namename
versionversion
summarystore.summary,留空回退 description
introductionstore.introduction,留空回退 description
categorystore.category,不填自动补 未分类
authorstore.author,不填自动补 M·Desk
developerstore.developer,不填自动用应用名称
iconicon,从包里抽出来单独存

正常走 mpkpack build 就不会缺这些字段:打包时会把整段 store 连同三个必填项一起补齐(命令输出里会提示 filled store defaults)。只有绕过 build、手工改包导致 store 段整个不存在时,mpkpack store 才会拒绝并报「manifest.json has no store section」。

客户端怎么读

M·Desk 只做这几件事,你不需要提供任何接口:

GET  <地址>index.json              读清单
GET  <地址><detail>               读某个应用的详情
GET  <地址><icon>                 取图标
GET  <地址><package.path>         下载安装包
     ↓
     逐字节校验 sha256 与 size,通过才装
✕

下载回来的包会先算 SHA-256 与 package.sha256 比对。手改过任何文件都会在这里失败,报「包校验失败」。改内容请走 mpkpack store --force 重新生成。

格式约束

约束说明
不支持源内分页一次读完整个 index.json,最多 1000 条
不支持跨目录引用所有路径必须落在源目录内,不能用 ../ 逃出去
不支持重定向服务端不跟随 3xx,地址要直接可访问
图标类型PNG / JPEG / WebP / SVG
包类型只有 .mpk(tar + gzip),扩展名不参与判断

安装向导可选

让用户在安装时填端口、密码、主页。向导只是传参层,不执行任何代码。

什么时候需要

用户要自定义端口、用户名、密码、主页,或应用首次启动必须有某个值才能跑。不需要就别加 —— 没有 wizard 段的包,安装行为完全不变。

目录与 manifest

my-app/
  wizard/
    install.json   ← 安装时展示(可选)
    config.json    ← 已安装后在「应用设置」里展示(可选)
"wizard": {
  "install": "wizard/install.json",
  "config":  "wizard/config.json",
  "bindings": {
    "preferredPort": "${field.port}",
    "native": { "env": { "APP_HOME": "${field.home}" } }
  }
}

两个文件至少声明一个才叫「有向导」,都省略就是无向导包。

表单格式

顶层是一个对象,里面只有 steps 一个键;每一步用 stepTitle 起标题,用 items 装控件;每个控件用 field 声明变量名。就这三个键名,没有别的写法。

{
  "steps": [
    {
      "stepTitle": "基本设置",
      "items": [
        { "field": "port", "type": "number", "label": "端口",
          "initValue": 7863, "min": 1024, "max": 65535 },
        { "field": "home", "type": "text", "label": "主页",
          "initValue": "https://example.com/", "required": true }
      ]
    }
  ]
}
✕

键名只有 steps / stepTitle / items / field 四个。写成 title、fields、name 会直接报错(json: unknown field "title"),解析器不认任何未知键 —— 它宁可报错也不猜你想写什么。

顶层也可以直接写数组,省略 {"steps": ...} 这层,两种写法等价。

键位置说明
steps顶层步骤数组,1–16 步
stepTitle步骤这一步的标题,最多 60 字,可省略
items步骤控件数组,每步 1–32 个,整张表单最多 64 个
field控件变量名,匹配 ^[a-z][a-z0-9_]{0,63}$;除 tips 外必填,同一张表单内不可重名

field 不能用 manifest 自己的键名(id、version、web、wizard 等),免得绑定写错时和清单属性混淆。

type说明可用属性
text单行文本initValue、min、max、len、pattern
password敏感文本,API 恒返回 __MASKED__同上,另加 required
number整数initValue、min、max
radio / select单选options、initValue
checkbox多选options、initValue(数组)
switch布尔开关initValue: true / false
tips纯提示文本,不参与提交text
✕

len 是精确长度,不是上限。写 "len": 128 会让「下一步」永远点不动。要上限用 max,要区间用 min + max。

值怎么传给程序

写法含义能出现在
${MDESK_*}M·Desk 注入的运行时上下文args、env
${field.<name>}用户填的值args、bindings.*.env、bindings.preferredPort

两个命名空间严格分离。用户值只展开一次,填 ${field.b} 也不会变成另一个变量的值。

字段为空时对应环境变量完全不注入(不是注入空串),所以程序里写 ${START_URL:-默认值} 能天然回落。

Docker 包怎么接住值

✕

Compose 里插值的是环境变量名,不是 ${field.x}。写成字段引用会报 invalid interpolation format。

environment:
  APP_HOST: "${APP_HOST}"        # 对
  # APP_HOST: "${field.host}"    # 错,Compose 报 invalid interpolation format

排错与验收

按阶段定位:装不上、起不来、打得开但白屏。交付前再过一遍验收清单。

安装阶段

现象原因处理
提示包非法产物被手工改过重新 mpkpack build,不要手改 checksums.json
too many json metadata filesM·Desk ≤ 2.7.114 的验证器缺陷升级 M·Desk,与包无关
提示架构不匹配architectures 写错或本机不支持改 architectures,或换目标机器

启动阶段

现象原因处理
一直「启动中」后失败程序没监听 healthPath确认程序真在监听、路径写对
立刻退出参数写错、端口没读到看应用日志,确认 ${MDESK_PORT} 被替换
contains a shell expressionargs 里有 $( 或反引号去掉,改用启动脚本
permission denied可执行位没设只有 executable 是 0755,脚本内部调用的文件自己 chmod
端口冲突程序写死端口改成读 ${MDESK_PORT}

页面阶段

现象原因处理
白屏、控制台 404资源用了绝对路径 /改相对路径或拼 base,见 子路径适配
页面能开但接口失败API 前缀没带同上
登录后立刻掉线Cookie Path 不对设为 /apps/<id>
WebSocket 连不上URL 写死 ws://host/ws由当前 location 推导
窗口里空白应用禁止 iframe允许同源嵌入

升级与卸载

  • 升级后数据没了:数据写到了 ${MDESK_APP_ROOT},应改到 ${MDESK_DATA_DIR}。
  • 升级后起不来:M·Desk 会按字节回滚到旧版本,检查新版本启动参数是否兼容。
  • 升级成功但停车:状态是 waitingConfig,补填必填项即可启动 —— 这不是失败。
◆

先看日志再改包:应用目录下 logs/application.log 与 var/logs/。

验收清单

打包成功只是第一步。交付前逐项过一遍:

环节检查
包结构mpkpack validate 通过;图标路径存在;启动器或 Compose 文件实际存在;无符号链接/特殊文件/路径穿越
运行实际监听正确模式(socket 用 ${MDESK_SOCKET},TCP 用 ${MDESK_PORT} 并监听 0.0.0.0);healthPath 约 35 秒内返回可接受状态;停止重启后仍健康
Web首页、CSS、JS、图片、字体都加载;API 与登录可用;Cookie 范围正确;重定向不跳错主机;WebSocket 能收发
生命周期首次安装、停止启动、运行中升级、停止状态升级都成功;启动失败能回滚;普通卸载留数据、彻底卸载删数据;桌面图标创建/刷新/移除正常
向导仅当声明了 wizard:两个 json 路径与格式合法;绑定字段真实存在;bindings 与 runtime 匹配;留空仍能启动;password 只出现在 secrets.json
上架有 store 段且三个必填都填;版本号已递增;architectures 与实际发布一致