开发者文档

为宝盒写一个模块。

从建目录到上架市场,看完这一页就够了——自己写,或者交给 AI 写,流程是同一套。

概览

宝盒的分工很简单:本体管系统,模块管功能。宝盒本体只碰系统层的事:托盘、面板、窗口、快捷键、系统权限;业务逻辑跑在各自独立的模块进程里;界面是随模块分发、由宝盒动态装载的自包含 UI bundle。

宝盒本体 托盘 / 面板 / 窗口 / 快捷键 / 系统权限
模块进程 独立 Rust 子进程,业务逻辑与数据
模块界面 自包含 Svelte bundle,渲染在壳的面板与窗口里

三者被打成一个 .pboxmod 包(zip + ed25519 签名),经模块市场分发。安装、更新即时生效,壳无需重启;卸载一个模块,它的进程与界面就彻底消失。

vibe coding 启动包

一个可以直接开工的模块项目模板:内含写好的 CLAUDE.md / AGENTS.md 系统提示词与最小可编译骨架。解压后丢给 Claude Code 或 Codex,一句话描述你要的模块即可。

下载 demo.zip

快速开始

目录骨架

一个模块就是一个目录:一份清单,一个 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 字段速查

字段示例说明
schema1清单格式版本,固定为 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 的跨模块调用被拒(见下文「模块间联动」)
requiresVipfalse整模块是否会员专享
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.tsmoduleUi 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),不硬编码颜色。

人只需要做的三件事

  1. 起意图把想要的模块用一两句话写清楚,连同上面的模板一起交给 AI。
  2. 本机联调bash pack.sh 打出 dev 包,宝盒开启「开发者模式」后经「载入本地模块」装上,真实用一遍。
  3. 提交上架把整个模块目录提交至模块市场审核,由官方构建 + 签名 + 分发,通过后所有宝盒用户都能一键安装。

验收清单

无论谁写的代码,上架前对着这份清单过一遍:

  • 卡片是纯展示层:数据来自 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 再次提交即可,版本历史与更新说明会在市场详情页展示。