概览
宝盒的分工很简单:本体管系统,模块管功能。宝盒本体只碰系统层的事:托盘、面板、窗口、快捷键、系统权限;业务逻辑跑在各自独立的模块进程里;界面是随模块分发、由宝盒动态装载的自包含 UI bundle。
三者被打成一个 .pboxmod 包(zip + ed25519 签名),经模块市场分发。安装、更新即时生效,壳无需重启;卸载一个模块,它的进程与界面就彻底消失。
一个可以直接开工的模块项目模板:内含写好的 CLAUDE.md / AGENTS.md 系统提示词与最小可编译骨架。解压后丢给 Claude Code 或 Codex,一句话描述你要的模块即可。
快速开始
目录骨架
一个模块就是一个目录:一份清单,一个 Rust 进程,一套 Svelte 界面。
<id>/
├── module.json # 模块清单(manifest)
├── core/ # Rust 进程:业务逻辑
│ ├── Cargo.toml # [[bin]] name = "pbox-mod-<id>"
│ └── src/main.rs
└── ui/ # Svelte 5 界面
├── package.json # 依赖 @pbox/ui-sdk
├── vite.config.ts # moduleUi({ card: "src/card.ts", ... })
└── src/
module.json 字段速查
| 字段 | 示例 | 说明 |
|---|---|---|
schema | 1 | 清单格式版本,固定为 1 |
id | "worldclock" | 唯一 id;二进制名固定为 pbox-mod-{id} |
name / hint | "世界时钟" | 显示名与一句话副标题 |
icon / tint | "gauge" / "#af52de" | 图标名与主色,用于面板与市场展示 |
iconSvg | 内联 SVG 内容 | 可选;模块自带图标(24×24 viewBox),换图标零壳改;缺省回退到 icon 名 |
version / minShell | "0.1.0" / "0.3.0" | 模块版本与要求的最低壳版本 |
process | { "bin": "bin/{triple}/pbox-mod-<id>", "keepAlive": true } | 模块进程入口,按目标三元组选择二进制 |
capabilities | ["notification"] | host.* 能力白名单,未声明的调用会被总线拒绝 |
panel | { "card": { "entry": "ui/card.mjs" } } | 面板卡片与顶栏小图标(widget)的 UI 入口;card.actions 可声明卡片头部按钮(见下文) |
settings | { "entry": "ui/settings.mjs" } | 设置界面入口,由壳注入统一设置窗 |
windows | [{ "name": "main", "entry": "ui/main.mjs", … }] | 模块独立窗口声明(可选),复杂交互放这里(见下文「独立窗口」) |
hotkeys | [{ "action": "toggle", "default": "Cmd+Shift+K", "handler": "win:main", … }] | 快捷键动作声明,壳注册全局热键并在设置里让用户改键/单独关闭(见下文「快捷键」) |
traySource | { "mode": "battery", "label": "电量" } | 菜单栏托盘数据源声明,配合 tray.push 能力推送内容(见下文「托盘数据源」) |
services | { "provides": [], "uses": [] } | 模块间联动声明,未声明 uses 的跨模块调用被拒(见下文「模块间联动」) |
requiresVip | false | 整模块是否会员专享 |
features | ["ocr"] | 模块内会员门控功能 key,上架时登记进权益表 |
defaultDisplay | "card" | 默认形态:card / widget / hotkey |
permissions | ["notifications"] | 用到的系统权限,向用户展示:screen 屏幕录制、accessibility 辅助功能、notifications 通知、bluetooth 蓝牙、localNetwork 本地网络 |
category | "开发常用" | 市场分组:系统显示 / 办公效率 / 开发常用 |
description | 长文 | 市场详情页的完整介绍 |
core:最小 Rust 实现
模块进程通过 NDJSON JSON-RPC 与壳通信,SDK 已把协议细节全部接管,你只注册方法:
fn main() {
pbox_module::run("<id>", env!("CARGO_PKG_VERSION"), |init| {
// init.hello: data_dir(模块私有数据目录)/ settings / license / appearance
let mut router = pbox_module::Router::new();
router.on("<id>.snapshot", move |_params, _host| {
Ok(serde_json::json!({ /* 首帧数据,来自内存缓存 */ }))
});
router
});
}
两条原则:请求路径不做慢事——耗时扫描与网络请求放后台线程,完成后 host.emit("changed", …) 通知 UI 重拉;日志走 host.log()/stderr,stdout 只属于协议。
调用 AI 模型(ai.config)
用户在宝盒里配好的大模型服务,模块可以直接拿来用。用户在宝盒「设置 → AI 模型」里管理 Provider——可以配多个、动态拉取模型列表、指定默认模型;模块不索要也不保存任何 key,只声明能力、按需取配置。先在清单的 capabilities 里声明 "ai.config"(安装前对用户可见),然后在模块进程里取配置,自己向 AI 服务发请求(支持流式):
let cfg = host.call("host.ai.config", serde_json::json!({}))?;
// cfg = {
// "providers": [{
// "id": "…", "name": "…", "baseUrl": "…", "apiKey": "…",
// "flavor": "anthropic", // "anthropic" | "openai" | ""(自动)
// "models": ["…"], "defaultModel": "…"
// }],
// "defaultProvider": "…"
// }
用户改动 AI 配置后,模块进程会收到 mod.aiConfigChanged 通知,重新调一次 host.ai.config 即可。
重要口径:hello / settingsChanged 里下发的 settings 是脱敏副本——apiKey 一律是 "__masked__" 哨兵值,只能用来判断「用户是否已配置 AI」;真实 key 必须走 ai.config 能力获取。
ui:mount 契约
每个 UI 入口(card / widget / settings / 窗口)导出一个 mount 函数:
import { mount as svelteMount, unmount as svelteUnmount } from "svelte";
import type { ModuleCtx, ModuleUiHandle } from "@pbox/ui-sdk";
export function mount(el: HTMLElement, ctx: ModuleCtx): ModuleUiHandle {
const instance = svelteMount(Card, { target: el, props: { ctx } });
return { destroy() { void svelteUnmount(instance); } };
}
ctx 是模块 UI 能用到的全部接口:ctx.call("<id>.method", params) 调本模块进程;ctx.on("changed", fn) 收事件;ctx.service(target, method, params) 跨模块联动;ctx.license.featureEnabled(key) 判定会员功能;ctx.win.show/hide/toggle/setSize(name) 控制模块声明的窗口;ctx.settings.getOwn()/patchOwn() 读写模块自有配置(见下文「模块自有设置」);ctx.host 提供 toast / openUrl / revealFile / pickFolder() 等宿主能力。主题一律使用壳注入的 CSS 变量(--text / --text-2 / --sep / --accent 等),模块界面即可自动跟随明暗外观。
卡片头部按钮
模块可以在卡片右上角放最多 3 个小按钮(比如「立即刷新」)。按钮不用自绘——在清单的 panel.card 里声明,由宝盒统一渲染:
"panel": {
"card": {
"entry": "ui/card.mjs",
"actions": [
{ "id": "refresh", "icon": "rotate", "label": "立即刷新" }
]
}
}
点击后,壳会向本模块广播 cardAction 事件,卡片 UI 里接住即可:
const un = ctx.on<string>("cardAction", (id) => {
if (id === "refresh") load();
});
icon 用宝盒图标集里的名称(rotate / plus / check / bolt / gauge 等),label 是悬停提示。按钮的外观、位置与提示气泡全部由壳负责,第三方卡片因此和内置卡片长得一样。
独立窗口
复杂交互不塞进卡片,放独立窗口——比如系统监控卡片上点一个小按钮,弹出完整的端口管理窗。窗口在清单的 windows 里声明:
"windows": [
{
"name": "main", // 窗口名,ctx.win / host.window 用它引用
"entry": "ui/main.mjs", // 该窗口的 UI 入口(同 mount 契约)
"width": 640, "height": 460,
"style": {
"decorations": false, // 无系统边框
"transparent": true, // 默认 true
"popIn": true, // 常驻假隐藏,零延时弹出(高频窗口用)
"vibrancy": "hudWindow", // 毛玻璃材质:hudWindow | popover | menu | sidebar
"resizable": false,
"alwaysOnTop": true, // 默认 true
"focus": true // 显示时是否夺焦,默认 true
}
}
]
窗口由壳在模块就绪时自动创建并装载 entry;声明了 popIn 的窗口会预热常驻,show/hide 只翻转透明度,弹出零延时。记得在 vite.config.ts 的 moduleUi entries 里加上对应入口(如 main: "src/main.ts")。
控制显隐有两条路:卡片/小图标 UI 里用 ctx.win.show("main") / hide / toggle;模块进程里用 host.call("host.window.show", json!({"name":"main"}))(hide / toggle 同理)。未在清单声明的窗口名会被拒绝。
典型组合——卡片头部按钮开关窗口,这正是「系统监控 → 端口管理」式交互的标准写法:
// 清单 panel.card.actions 里声明按钮 { "id": "ports", "icon": "bolt", "label": "端口管理" }
// 卡片 UI 里接住 cardAction,开关窗口:
ctx.on<string>("cardAction", (id) => {
if (id === "ports") ctx.win.toggle("main");
});
快捷键
模块在清单的 hotkeys 里声明快捷键动作,壳负责注册全局热键、在「设置 → 快捷键」里展示,并允许用户改键或单独关闭——模块不碰任何全局热键 API:
"hotkeys": [
{
"action": "toggle", // 动作 id,与模块 id 拼成 "<id>.toggle"
"default": "Cmd+Shift+K", // 默认键位(用户可在设置里改)
"label": "打开面板",
"handler": "win:main", // 按下时壳做什么(见下)
"feature": "floating_note", // 可选:会员门控此键的功能 id;无则省略
"primary": true // 是否为顶栏小图标点击触发的主动作
}
]
handler 决定按下后壳执行什么,数据驱动,壳内不含任何模块 id 硬编码:
"win:<name>"——切换该模块声明的窗口(最常用);"module:<method>"——fire-and-forget 调本模块 core 的方法;"shell:capture"/"shell:captureOcr"/"shell:clipboard"/"shell:shelf"/"shell:note"——壳托管的少数原语(截图 / 快速 OCR / 剪贴板 / 暂存 / 笔记),供内置模块使用。
用户改键、停用某个键都在壳的设置页完成,模块零改;停用整个模块时它的热键自动失效。
托盘数据源
模块可以往菜单栏托盘推自己的内容——电量、CPU、内存、日历都是这么做的。先在 capabilities 里声明 "tray.push",再用 traySource 声明托盘模式:
"capabilities": ["tray.push"],
"traySource": { "mode": "battery", "label": "电量" }
// 多模式(如系统监控):
// "traySource": { "modes": [
// { "mode": "cpu", "label": "CPU" },
// { "mode": "mem", "label": "内存" }
// ]}
模块进程里按模式推送——文本,或一张自绘的 RGBA 图标(template: true 让图标随明暗菜单栏自动黑白适配):
host.call("host.tray.push", serde_json::json!({
"mode": "battery",
"text": "82%"
// 或推图标: "icon": { "rgba": "<base64>", "w": 26, "h": 22, "key": "b82", "template": true }
}))?;
只能推自己在 traySource 里声明过的模式,冒充别的源会被总线拒绝。用户在托盘设置里选择显示哪个模式。
模块自有设置
模块的持久配置存在壳里、随设置同步,但只有属主模块能读到真值。设置界面(settings.entry)里用 ctx.settings 读写自己那片:
const cfg = await ctx.settings.getOwn<MyCfg>(); // 读自己的配置(含密钥真值)
ctx.settings.patchOwn({ interval: 30 }); // 浅合并写入,即时持久化并广播
两类字段可在清单的 settings 里进一步声明:
"settings": {
"entry": "ui/settings.mjs",
"secretKeys": ["youdaoSecret"], // 广播脱敏,仅属主 getOwn 拿真值
"gatedKeys": { "tools": "screenshot_toolbar_customization" } // 会员门控写入:{ 设置键: 功能id }
}
secretKeys(数组)——密钥类字段,下发给其他模块/UI 的广播里会被换成哨兵值,真实值只有属主getOwn能取到;gatedKeys({ 设置键: 功能id }映射)——受会员门控的字段,非会员patchOwn写入会被壳直接丢弃。
历史上模块也可以把私有配置写进自己的 {data_dir}/config.json(进程侧),但涉及设置界面、脱敏、会员门控时,一律走 getOwn / patchOwn。
模块间联动(services)
模块之间可以互相调用,但必须在清单里双向声明,总线双向鉴权。提供方在 services.provides 里登记方法(真实例子:悬浮笔记),core 里它就是一个普通的 Router 方法(router.on("note.createFromText", …)),不需要任何额外代码:
"services": {
"provides": [{ "method": "note.createFromText", "label": "追加文本到今日笔记" }],
"uses": []
}
调用方在 services.uses 里声明要用的方法(真实例子:万年历的「记到笔记」):
"services": { "provides": [], "uses": ["note.createFromText"] }
调用有两条路。UI 里:
await ctx.service("note", "note.createFromText", { text, source: "万年历" });
模块进程里:
host.call("host.service.call", serde_json::json!({
"target": "note", "method": "note.createFromText", "payload": { /* … */ }
}))?;
鉴权规则由总线执行,四条都是硬性的:
- 调用方清单的
services.uses必须声明该方法,否则总线拒绝; - 目标模块清单的
services.provides必须确实提供该方法,否则拒绝; - 不能联动调用自身;
- 目标模块被用户停用/卸载时调用被拒(报「模块已停用」)。
配套铁律:联动入口的 UI(按钮/菜单项)必须跟随目标模块的可用性显隐——目标不可用就隐藏入口,而不是让用户点了报错。用 module_available 判断,并监听两个事件刷新:
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
let noteAvail = $state(false);
const refresh = async () => {
noteAvail = await invoke<boolean>("module_available", { id: "note" });
};
refresh();
listen("modules:changed", refresh);
listen("settings:changed", refresh);
构建与提交
vibe coding 启动包是自包含的:三个 SDK 已 vendor 进包内 _sdk/,解压后在目录里直接构建,不需要宝盒源码仓库。
cd core && cargo build --release # 编译 core → target/release/pbox-mod-<id>
cd ../ui && pnpm install && pnpm build # 构建 UI bundle → ui/dist/
本机真实联调:启动包根目录跑 bash pack.sh,一键构建并打出 dist/<id>-<version>.pboxmod(dev 包,未签名)。打开宝盒 → 设置 → 模块市场,右上开启「开发者模式」,点「载入本地模块…」选中它——模块立即装进当前宝盒,卡片、窗口、设置、通知全部走真实链路。改完代码重新 pack.sh + 重新载入即可。
满意后把整个模块目录提交模块市场审核:分发用的 .pboxmod 由官方统一构建、签名、上架。
安全边界:宝盒对市场分发的每个包强制 ed25519 验签;「开发者模式」只放行清单带 "dev": true 的本机包,且 dev 包不可上架(服务端上传即拒)。不开开发者模式,未签名包一律装不进来。
UI 设计规范
面板里的卡片来自不同作者,但用户看到的是同一块面板。照下面这几条写,第三方模块和内置模块摆在一起就不违和。这些同时是上架审核的检查项,提交前自己先过一遍。
主题变量是唯一取色来源
颜色一律用壳注入的 CSS 变量,不写死 hex。变量在明暗两套外观下各有取值,模块界面因此自动适配:
| 变量 | 用途 |
|---|---|
--text / --text-2 / --text-3 | 正文 / 次要 / 辅助文字,层级递弱 |
--sep | 分隔线(卡片内区块之间) |
--card-bg / --chip-bg | 卡片底色 / 小色块底色(进度条槽、标签底) |
--hover | 可点元素的悬停底色 |
--good / --warn / --bad | 状态色:正常 / 警告 / 危险 |
--accent | 强调色(链接、选中态) |
组件优先用 @pbox/ui-sdk
Icon——统一图标集,与壳同一套线条风格;不内嵌自己的 SVG。poll——轮询封装,替代裸setInterval。format——bytes/duration等格式化函数,数字口径全站一致。
排版基调
- 卡片内容 12–13px 正文;辅助文字 10.5–11px,用
--text-3。 - 区块间距 8px;进度条 4px 高、圆角,槽用
--chip-bg。 - 状态色语义固定:绿 = 正常、黄 = 警告、红 = 危险。绿色不用于开关等非状态元素。
必备状态
这三种情况每张卡片都得处理,不能白屏,也不能把报错原样抛给用户:
- 空态——没数据时给一句灰字说明(
--text-3),如「未检测到使用记录」。 - 加载失败——catch 住并安排重试(自动重试或给出重试入口)。
- 数据积累期——估算类数据还不够准时明说,如「峰值数据积累中」。
克制原则
- 卡片是纯展示面,不放表单——设置一律走清单的
settings.entry,由壳注入统一设置窗。 - 不用 emoji、不造魔法图标;图标只从宝盒图标集取。
- 文案用短句,辅助信息一行放得下。
demo 走查:「vibe coding 用量」模块
市场里的「vibe coding 用量」模块监控 Claude Code 与 Codex CLI 的 token 用量,是个不错的参考:面板卡片、顶栏小图标、设置注入、系统通知,这些能力它全都用到了。
清单:声明形态与能力
{
"id": "vibeusage",
"name": "vibe coding 用量",
"icon": "gauge", "tint": "#af52de",
"capabilities": ["notification"],
"panel": {
"card": { "entry": "ui/card.mjs", "defaultVisible": true },
"widget": { "entry": "ui/widget.mjs" }
},
"settings": { "entry": "ui/settings.mjs" },
"defaultDisplay": "card",
"permissions": ["notifications"],
"category": "开发常用"
}
同一个模块声明了两种形态:card 是面板里的完整卡片,widget 是常驻顶栏的小图标;用户在摆放时自由选择。settings.entry 声明后,壳会把设置界面注入统一的设置窗——模块不用自己造窗口。
core:后台扫描 + 事件通知
router.on("vibeusage.snapshot", move |_params, _host| {
Ok(serde_json::to_value(cache.lock().clone())?) // 首帧永远来自缓存
});
// 后台线程:定期解析本机会话记录,算完再广播
std::thread::spawn(move || loop {
let usage = scan_usage(&data_dir);
*cache.lock() = usage.clone();
host.emit("changed", serde_json::json!({})); // UI 收到后重拉 snapshot
if usage.ratio >= threshold {
let _ = host.call("host.notify", serde_json::json!({
"title": "用量提醒", "body": "本窗口已用 80%"
}));
}
std::thread::sleep(POLL);
});
注意分工:snapshot 方法只读内存缓存,永远瞬时返回;真正的扫描在后台线程里做。阈值提醒通过 host.notify 发系统通知,对应清单里的 capabilities: ["notification"] 与 permissions: ["notifications"]。
ui:卡片只做展示
let snap = $state<Snapshot | null>(null);
async function refresh() {
try {
snap = await ctx.call("vibeusage.snapshot");
} catch {
setTimeout(refresh, 1500); // 安装/重启的握手窗口期,稍后重试
}
}
ctx.on("changed", refresh); // core 广播 → 重拉
ctx.on("up", refresh); // 模块进程恢复 → 自愈
refresh();
卡片是纯展示层:不算数据、不做 IO,只把 core 给的快照画出来。顶栏 widget 也复用同一个 snapshot 方法,只是画得更小。
vibe coding 教程:让 AI 替你写模块
模块的结构是固定的:清单、core、ui 三件套,外加一组铁律。结构越固定,AI 写起来越稳。把下面的提示词整段丢给你的 AI 编程工具,只改第一段的想法描述。更省事的做法:直接下载页首的 vibe coding 启动包,这份提示词已经写成 CLAUDE.md / AGENTS.md 放在模板里,还带着可编译的项目骨架。
提示词模板
你是宝盒(PBox)模块开发助手。请为 macOS 菜单栏应用「宝盒」开发一个模块,严格遵守以下契约。
## 我想要的模块
<用一两句话描述想法,例如:一个世界时钟卡片,可添加多个城市,顶栏小图标显示第一个城市的时间>
## 目录骨架(必须遵守)
<id>/
├── module.json # 模块清单
├── core/ # Rust 子进程,业务逻辑
│ ├── Cargo.toml # [[bin]] name = "pbox-mod-<id>"
│ └── src/main.rs
└── ui/ # Svelte 5 界面,构建为自包含 bundle
└── src/
## module.json 契约
{
"schema": 1,
"id": "<小写唯一 id>",
"name": "<显示名>", "hint": "<一句话副标题>",
"icon": "<图标名>", "tint": "<主色 #hex>",
"version": "0.1.0", "minShell": "0.3.0",
"process": { "bin": "bin/{triple}/pbox-mod-<id>", "keepAlive": true },
"capabilities": [], // 需要的 host.* 能力,如 "notification"
"panel": { "card": { "entry": "ui/card.mjs", "defaultVisible": true } },
"settings": { "entry": "ui/settings.mjs" }, // 有设置项时声明,由壳注入统一设置窗
"defaultDisplay": "card",
"permissions": [], // screen/accessibility/notifications/bluetooth/localNetwork
"category": "<系统显示|办公效率|开发常用>",
"description": "<市场详情介绍>"
}
## core(Rust)契约
fn main() {
pbox_module::run("<id>", env!("CARGO_PKG_VERSION"), |init| {
let mut router = pbox_module::Router::new();
router.on("<id>.snapshot", |_params, _host| Ok(serde_json::json!({})));
router
});
}
## ui(Svelte 5)入口契约(每个 entry 一个)
import { mount as svelteMount, unmount as svelteUnmount } from "svelte";
export function mount(el, ctx) {
const instance = svelteMount(Card, { target: el, props: { ctx } });
return { destroy() { void svelteUnmount(instance); } };
}
## 铁律(违反即返工)
1. 请求路径不做慢事:snapshot 只读内存缓存;耗时扫描/网络放后台线程,完成后 host.emit("changed", …) 通知 UI 重拉。
2. 卡片是纯展示层:数据全部来自 core,复杂交互放独立窗口。
3. 模块私有配置存 {data_dir}/config.json,不碰壳的全局设置。
4. 日志走 host.log()/stderr;stdout 只属于协议,禁止用 println! 调试。
5. UI 首次 ctx.call 失败要 catch 并在 1.5 秒后重试(安装/重启存在握手窗口期);监听 up 事件,进程恢复后重拉数据。
6. 会员功能判定只用 ctx.license.featureEnabled(key),core 侧读 license.features;不要自行推断会员等级。
7. 卡片在「进程未就绪/无数据」时显示说明文字,不留空白。
8. 样式只用壳注入的 CSS 变量(--text/--text-2/--text-3/--sep/--chip-bg/--hover/--good/--warn/--bad/--accent),不硬编码颜色。
人只需要做的三件事
- 起意图把想要的模块用一两句话写清楚,连同上面的模板一起交给 AI。
- 本机联调
bash pack.sh打出 dev 包,宝盒开启「开发者模式」后经「载入本地模块」装上,真实用一遍。 - 提交上架把整个模块目录提交至模块市场审核,由官方构建 + 签名 + 分发,通过后所有宝盒用户都能一键安装。
验收清单
无论谁写的代码,上架前对着这份清单过一遍:
- 卡片是纯展示层:数据来自 core 的快照方法,重逻辑不写在 UI 里
- 设置通过清单的
settings.entry声明,由壳注入统一设置窗,而不是自绘窗口 - 首次
ctx.call失败时安排一次短暂重试,覆盖安装/重启的握手窗口期 - 监听
up事件,模块进程恢复后自动重拉数据,界面自愈 - 跨模块入口跟随目标模块的可用性显示或隐藏——模块随时可能被停用或卸载
- 会员功能一律用
ctx.license.featureEnabled(key)判定,不自行推断会员等级 - 进程未就绪、暂无数据时,卡片显示说明文字而不是一片空白
发布与分发
签名验证
每个 .pboxmod 包在打包时用 ed25519 私钥签名,壳内置对应公钥,安装前逐包验签:签名对不上或内容被改动,包就装不进来。再配上逐版本的 SHA-256 校验,从市场到用户机器,包在路上没被动过手脚,来源也查得到。
分类
模块按用途分入三个分组,决定它在安装向导与市场里的位置:系统显示 办公效率 开发常用
定价:两层,都可运营
- 整模块会员专享——清单里
requiresVip: true。未激活会员的用户在市场里会看到金色「会员」徽标,安装入口变为「会员专享 · 去激活」。 - 模块内功能门控——清单里
features: ["<key>", …]声明付费功能点,上架时自动登记进权益表;之后可以按 key 随时放开做限免。代码里的判定点只有一个:UI 用ctx.license.featureEnabled(key),core 读 license 里的features[key]。
原则:只做「放开」和「新功能定价」,不做「回收已付费功能」。
上架流程
模块打包完成后提交至模块市场审核;通过后即对所有用户可见,用户侧安装、更新即时生效。后续迭代递增 version 再次提交即可,版本历史与更新说明会在市场详情页展示。