插件与主题开发指南
应用中心 文档首页

MtcldForum 插件与主题开发指南

版本 1.0 · 适用于 MtcldForum 插件中心 API v1



一、快速开始

1. 注册开发者  →  获取 API Key
2. 开发插件/主题  →  本地编译测试
3. 调用发布 API  →  提交到应用中心
4. 等待审核  →  上架后用户可安装

应用中心地址: https://plug.mtcld.cn API 基础路径: https://plug.mtcld.cn/api/v1


二、注册开发者账号

curl -X POST https://plug.mtcld.cn/api/v1/developers/register \
  -H "Content-Type: application/json" \
  -d '{
    "name": "你的名字",
    "email": "dev@example.com",
    "website": "https://example.com"
  }'

响应示例

{
  "success": true,
  "data": {
    "id": "a1b2c3d4-...",
    "api_key": "rfp_a1b2c3d4e5f6...",
    "message": "Developer registered. Save your API key - it won't be shown again"
  }
}

重要api_key 只会返回一次,请务必安全保存。后续所有发布操作都需要此密钥。


三、插件开发

3.1 插件结构

my-plugin/
├── Cargo.toml          # Rust 项目配置
├── src/
│   └── lib.rs          # WASM 插件主代码
├── manifest.json       # 插件元信息
└── README.md           # 插件说明

最终发布时只需要两个文件: - manifest.json — 插件描述 - plugin.wasm — 编译后的 WASM 二进制

3.2 manifest.json 说明

{
  "id": "my-plugin",
  "name": "我的插件",
  "version": "1.0.0",
  "description": "一句话描述插件功能",
  "author": "开发者名称",
  "hooks": ["before_post_create", "after_post_create"],
  "permissions": ["read_posts"]
}
字段 类型 必填 说明
id string 唯一标识,小写字母+连字符,如 spam-filter
name string 显示名称
version string 语义化版本号 x.y.z
description string 简短描述
author string 作者名称
hooks string[] 需要监听的钩子列表,可为空数组
permissions string[] 需要的权限列表,可为空数组

ID 命名规范: - 全小写,单词用 - 连接 - 不超过 100 个字符 - 例:spam-filterdark-modesocial-login

3.3 WASM 插件开发(Rust)

插件使用 WebAssembly (WASM) 格式,运行在 Wasmtime 沙箱中,安全隔离。

Cargo.toml:

[package]
name = "my-plugin"
version = "1.0.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"

src/lib.rs:

use serde::{Deserialize, Serialize};

#[derive(Deserialize)]
struct HookContext {
    hook: String,
    data: serde_json::Value,
}

#[derive(Serialize)]
struct HookResult {
    modified_data: Option<serde_json::Value>,
    cancel: bool,
    messages: Vec<String>,
}

/// WASM 内存分配函数(必须导出)
#[no_mangle]
pub extern "C" fn alloc(len: usize) -> *mut u8 {
    let mut buf = Vec::with_capacity(len);
    let ptr = buf.as_mut_ptr();
    std::mem::forget(buf);
    ptr
}

/// 钩子处理入口(必须导出)
#[no_mangle]
pub extern "C" fn handle_hook(ptr: i32, len: i32) -> i32 {
    let input = unsafe {
        let slice = std::slice::from_raw_parts(ptr as *const u8, len as usize);
        String::from_utf8_lossy(slice).to_string()
    };

    let ctx: HookContext = match serde_json::from_str(&input) {
        Ok(c) => c,
        Err(_) => return 0,
    };

    let result = match ctx.hook.as_str() {
        "before_post_create" => handle_before_post_create(&ctx.data),
        "after_post_create" => handle_after_post_create(&ctx.data),
        _ => HookResult {
            modified_data: None,
            cancel: false,
            messages: vec![],
        },
    };

    let json = serde_json::to_string(&result).unwrap_or_default();
    let bytes = json.as_bytes();
    let out_ptr = alloc(bytes.len() + 4);

    unsafe {
        // 前 4 字节存长度(小端序)
        let len_bytes = (bytes.len() as u32).to_le_bytes();
        std::ptr::copy_nonoverlapping(len_bytes.as_ptr(), out_ptr, 4);
        std::ptr::copy_nonoverlapping(bytes.as_ptr(), out_ptr.add(4), bytes.len());
    }

    out_ptr as i32
}

fn handle_before_post_create(data: &serde_json::Value) -> HookResult {
    // 示例:检查帖子标题长度
    if let Some(title) = data.get("title").and_then(|v| v.as_str()) {
        if title.len() < 5 {
            return HookResult {
                modified_data: None,
                cancel: true,
                messages: vec!["标题太短,至少需要 5 个字符".into()],
            };
        }
    }

    HookResult {
        modified_data: None,
        cancel: false,
        messages: vec![],
    }
}

fn handle_after_post_create(data: &serde_json::Value) -> HookResult {
    // 示例:帖子创建后的处理逻辑
    let post_id = data.get("id").and_then(|v| v.as_str()).unwrap_or("unknown");
    HookResult {
        modified_data: None,
        cancel: false,
        messages: vec![format!("帖子 {} 创建成功", post_id)],
    }
}

编译命令

# 安装 WASM 编译目标(首次)
rustup target add wasm32-wasip1

# 编译
cargo build --target wasm32-wasip1 --release

# 产出文件在:
# target/wasm32-wasip1/release/my_plugin.wasm

3.4 钩子系统(Hooks)

插件通过钩子介入论坛的各个环节。钩子分为 before_*(可取消操作)和 after_*(仅通知)两类。

钩子名称 触发时机 可取消 data 字段
before_post_create 创建帖子前 {title, content, category_id, author_id}
after_post_create 创建帖子后 {id, title, content, author_id}
before_post_update 更新帖子前 {id, title, content}
after_post_update 更新帖子后 {id, title, content}
before_post_delete 删除帖子前 {id, author_id}
after_post_delete 删除帖子后 {id}
before_comment_create 创建评论前 {post_id, content, author_id}
after_comment_create 创建评论后 {id, post_id, content}
before_user_register 用户注册前 {username, email}
after_user_register 用户注册后 {id, username, email}
before_user_login 用户登录前 {username}
after_user_login 用户登录后 {id, username}
on_search 搜索时 {query, user_id}
on_render 页面渲染时 {page, user_id}
on_notification 通知发送时 {type, user_id, message}

HookResult 说明

{
  "modified_data": { "title": "修改后的标题" },  // 返回修改后的数据,null 表示不修改
  "cancel": false,                               // true = 取消此操作(仅 before_* 有效)
  "messages": ["日志消息"]                         // 调试/日志消息
}

3.5 发布插件

# 将 WASM 文件转为 base64
WASM_B64=$(base64 -w 0 target/wasm32-wasip1/release/my_plugin.wasm)

# 发布
curl -X POST https://plug.mtcld.cn/api/v1/plugins \
  -H "Content-Type: application/json" \
  -H "X-API-Key: rfp_你的密钥" \
  -d "{
    \"id\": \"my-plugin\",
    \"name\": \"我的插件\",
    \"description\": \"插件功能描述\",
    \"long_description\": \"支持 Markdown 的详细说明...\",
    \"version\": \"1.0.0\",
    \"category\": \"utility\",
    \"homepage\": \"https://github.com/you/my-plugin\",
    \"repository\": \"https://github.com/you/my-plugin\",
    \"license\": \"MIT\",
    \"hooks\": [\"before_post_create\"],
    \"permissions\": [\"read_posts\"],
    \"changelog\": \"首次发布\",
    \"wasm_base64\": \"$WASM_B64\"
  }"

发布请求全字段

字段 类型 必填 说明
id string 插件唯一 ID
name string 显示名称
description string 简短描述
long_description string Markdown 格式详细说明
version string 语义化版本号
category string 分类 slug(见下方分类列表)
homepage string 主页链接
repository string 源码仓库链接
license string 许可证,默认 MIT
icon_url string 图标 URL
hooks string[] 钩子列表
permissions string[] 权限列表
min_forum_version string 最低论坛版本要求
changelog string 版本更新日志
wasm_base64 string Base64 编码的 WASM 文件(最大 50MB)

可用分类 slug

slug 名称 说明
content 内容增强 增强帖子和内容功能
moderation 审核管理 内容审核和社区管理工具
integration 集成对接 第三方服务集成
theme 主题外观 界面主题和样式定制
notification 通知推送 通知和消息推送扩展
analytics 数据分析 统计和分析工具
seo SEO优化 搜索引擎优化插件
utility 实用工具 通用工具类插件

3.6 更新插件

curl -X PUT https://plug.mtcld.cn/api/v1/plugins/my-plugin \
  -H "Content-Type: application/json" \
  -H "X-API-Key: rfp_你的密钥" \
  -d "{
    \"version\": \"1.1.0\",
    \"description\": \"更新后的描述\",
    \"changelog\": \"修复了 xxx 问题\",
    \"wasm_base64\": \"$(base64 -w 0 target/wasm32-wasip1/release/my_plugin.wasm)\"
  }"

只需传要更新的字段,wasm_base64 仅在更新代码时传。


四、主题开发

4.1 主题结构

my-theme/
├── theme.css           # 主题样式文件(必需)
├── manifest.json       # 主题元信息
└── README.md           # 主题说明

发布时需要将 theme.cssmanifest.json 打包成 ZIP 文件

manifest.json

{
  "id": "my-theme",
  "name": "我的主题",
  "version": "1.0.0",
  "description": "一个自定义主题",
  "author": "开发者名称"
}

4.2 CSS 变量参考

主题通过覆盖 CSS 自定义属性来改变论坛外观。以下是所有可用变量:

变量名 说明 默认值示例
--color-bg 页面主背景色 #ffffff
--color-bg-secondary 卡片/区块背景色 #f8f9fa
--color-bg-tertiary 输入框/深层背景色 #e9ecef
--color-text 主文字颜色 #212529
--color-text-secondary 次要文字颜色 #495057
--color-text-muted 辅助/灰色文字 #6c757d
--color-primary 品牌主色/强调色 #6366f1
--color-primary-hover 主色悬停状态 #4f46e5
--color-border 边框颜色 #dee2e6
--color-success 成功/确认色 #10b981
--color-danger 危险/错误色 #ef4444
--color-warning 警告色 #f59e0b
--radius 全局圆角值 8px
--shadow 基础阴影 0 2px 8px rgba(0,0,0,0.08)
--shadow-lg 大号阴影(弹窗等) 0 8px 24px rgba(0,0,0,0.12)

4.3 组件样式覆盖

除了 CSS 变量,还可以直接覆盖组件样式:

/* 链接 */
a { color: #your-link-color; }
a:hover { color: #your-hover-color; }

/* 主按钮 */
.btn-primary { background: #your-primary; color: #fff; }
.btn-primary:hover { background: #your-primary-hover; }

/* 徽章 */
.badge-primary { background: rgba(99,102,241,0.15); color: #6366f1; }
.badge-success { background: rgba(16,185,129,0.15); color: #10b981; }
.badge-danger { background: rgba(239,68,68,0.15); color: #ef4444; }

/* 卡片 */
.card { border-color: #your-border; }
.card:hover { box-shadow: var(--shadow-lg); }

/* 页面整体 */
body { font-family: 'Your Font', sans-serif; }

4.4 配色模式

主题需要支持亮色和暗色两种模式(推荐),系统通过 :root(亮色)和 .dark(暗色)切换:

/* 亮色模式(默认) */
:root {
  --color-bg: #ffffff;
  --color-text: #212529;
  /* ... 其他亮色值 ... */
}

/* 暗色模式 */
.dark {
  --color-bg: #1a1b2e;
  --color-text: #e8e8e8;
  /* ... 其他暗色值 ... */
}

配色模式选项: - light — 仅亮色 - dark — 仅暗色 - both — 同时支持亮色和暗色(推荐)

4.5 发布主题

第一步:打包 ZIP

cd my-theme/
zip my-theme.zip theme.css manifest.json

第二步:发布

FILE_B64=$(base64 -w 0 my-theme.zip)

curl -X POST https://plug.mtcld.cn/api/v1/themes \
  -H "Content-Type: application/json" \
  -H "X-API-Key: rfp_你的密钥" \
  -d "{
    \"id\": \"my-theme\",
    \"name\": \"我的主题\",
    \"description\": \"主题描述\",
    \"version\": \"1.0.0\",
    \"color_scheme\": \"both\",
    \"tags\": [\"简约\", \"暗色\"],
    \"css_variables\": {
      \"primary\": \"#6366f1\",
      \"bg\": \"#ffffff\"
    },
    \"changelog\": \"首次发布\",
    \"file_base64\": \"$FILE_B64\"
  }"

发布请求全字段

字段 类型 必填 说明
id string 主题唯一 ID,建议 theme- 前缀
name string 显示名称
description string 简短描述
long_description string Markdown 格式详细说明
version string 语义化版本号
color_scheme string light / dark / both,默认 both
tags string[] 标签列表
css_variables object 主要颜色变量预览
preview_url string 预览图 URL
thumbnail_url string 缩略图 URL
homepage string 主页链接
repository string 源码仓库
license string 许可证,默认 MIT
min_forum_version string 最低论坛版本
changelog string 更新日志
file_base64 string Base64 编码的 ZIP 文件(最大 50MB)

五、API 参考

5.1 认证方式

开发者接口使用 API Key 认证,通过 HTTP Header 传递:

X-API-Key: rfp_a1b2c3d4e5f6...

无需认证的接口:列表、详情、下载、评价、校验。

5.2 开发者接口

注册开发者

POST /api/v1/developers/register
字段 类型 必填
name string
email string 是(唯一)
website string

查看我的插件

GET /api/v1/developers/me/plugins
Header: X-API-Key: rfp_...

5.3 插件接口

插件列表

GET /api/v1/plugins?page=1&per_page=20&search=关键词&category=utility&sort=downloads&featured=true

排序选项:downloadsratingnameupdatedfeatured

插件详情

GET /api/v1/plugins/{id}

响应包含版本历史、哈希值等完整信息。

发布插件

POST /api/v1/plugins
Header: X-API-Key: rfp_...
Body: PublishRequest (见 3.5)

更新插件

PUT /api/v1/plugins/{id}
Header: X-API-Key: rfp_...(必须是原作者)
Body: 要更新的字段

下载插件

GET /api/v1/plugins/{id}/download

返回 WASM 二进制文件,响应头 X-Plugin-Manifest 包含 base64 编码的 manifest。

5.4 主题接口

主题列表

GET /api/v1/themes?page=1&per_page=20&search=关键词&color_scheme=dark&sort=featured

主题详情

GET /api/v1/themes/{id}

发布主题

POST /api/v1/themes
Header: X-API-Key: rfp_...
Body: PublishThemeRequest (见 4.5)

下载主题

GET /api/v1/themes/{id}/download

返回 ZIP 文件。

获取当前激活主题 CSS

GET /api/v1/theme/active.css

返回纯 CSS 文本,可直接在 <link> 标签引用。

5.5 完整性校验接口

用于验证本地文件是否与应用中心版本一致:

POST /api/v1/verify
Content-Type: application/json

{
  "id": "插件或主题ID",
  "hash": "文件的SHA256哈希值",
  "type": "plugin 或 theme"
}

响应状态

status 含义 说明
official 官方正版 哈希匹配,与应用中心版本一致
modified 已被篡改 哈希不匹配,文件可能被修改过
not_found 不在应用中心 该 ID 在应用中心不存在
no_hash 无哈希记录 应用中心有此项但缺少哈希(历史数据)

示例

# 计算文件哈希
HASH=$(sha256sum plugin.wasm | awk '{print $1}')

# 调用校验
curl -X POST https://plug.mtcld.cn/api/v1/verify \
  -H "Content-Type: application/json" \
  -d "{\"id\":\"my-plugin\",\"hash\":\"$HASH\",\"type\":\"plugin\"}"

5.6 分类与评价

获取分类列表

GET /api/v1/categories

插件评价

POST /api/v1/plugins/{id}/reviews
Content-Type: application/json

{
  "author_name": "用户名",
  "rating": 5,
  "content": "评价内容"
}

rating 为 1-5 的整数。

主题评价

POST /api/v1/themes/{id}/reviews

请求格式同插件评价。


六、哈希校验机制

应用中心为每个插件和主题生成 SHA-256 哈希值,用于完整性校验:

发布时:服务端计算 WASM/CSS 文件的 SHA-256 → 存入数据库
安装时:计算下载文件的 SHA-256 → 与服务端记录比对
结果  :official(正版)/ modified(已篡改)/ unknown(无记录)

用户会看到的提示: - ✓ 官方校验通过 — 绿色徽章 - ⚠ 已篡改 — 红色警告:"此文件与应用中心记录不一致,可能为破解版或已被篡改。请注意甄别,避免财产损失!" - ? 未校验 — 黄色提示:"非应用中心应用,请注意甄别"


七、最佳实践

插件开发

  1. 最小权限原则:只声明实际需要的 hooks 和 permissions
  2. 错误处理handle_hook 中捕获所有 panic,返回安全默认值
  3. 性能:钩子函数应在毫秒级完成,避免阻塞请求
  4. 幂等性before_* 钩子可能被重试,确保逻辑幂等
  5. 版本号:严格遵循语义化版本,Breaking Change 必须升大版本

主题开发

  1. 同时支持亮/暗色:使用 :root.dark 分别定义
  2. 只覆盖 CSS 变量:尽量通过变量修改,避免覆盖具体选择器
  3. 测试各种内容:长标题、空帖子、多级评论等边界情况
  4. 无障碍:确保文字与背景的对比度符合 WCAG AA 标准
  5. 图片资源:使用外部 CDN 托管,不要嵌入 base64

通用

  1. ID 全局唯一:发布前搜索确认 ID 未被占用
  2. 详细的 changelog:每次更新说明具体改动
  3. 提供 long_description:支持 Markdown,方便用户了解功能

八、常见问题

Q: 插件发布后为什么搜不到?

A: 新发布的插件需要管理员审核通过后才会在市场显示。审核通常在 1-3 个工作日内完成。

Q: 如何更新已发布的插件?

A: 使用 PUT /api/v1/plugins/{id} 接口,只传需要更新的字段。更新版本号和 WASM 文件时会自动创建新版本记录。

Q: API Key 丢失怎么办?

A: 目前无法自助找回,请联系管理员重新生成。

Q: WASM 文件大小限制?

A: 单个文件最大 50MB,请求体最大 200MB。建议 Release 模式编译并使用 wasm-opt 优化。

Q: 主题 ZIP 里必须包含什么?

A: 至少包含 theme.css 文件。建议同时包含 manifest.json。ZIP 内不要有 __MACOSX 等系统文件。

Q: 哈希校验失败是什么原因?

A: 可能原因:文件在传输中损坏、被第三方修改、或者不是从官方应用中心下载的。建议从应用中心重新安装。


文档最后更新:2026-02-16