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-filter、dark-mode、social-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.css 和 manifest.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
排序选项:downloads、rating、name、updated、featured
插件详情
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(无记录)
用户会看到的提示: - ✓ 官方校验通过 — 绿色徽章 - ⚠ 已篡改 — 红色警告:"此文件与应用中心记录不一致,可能为破解版或已被篡改。请注意甄别,避免财产损失!" - ? 未校验 — 黄色提示:"非应用中心应用,请注意甄别"
七、最佳实践
插件开发
- 最小权限原则:只声明实际需要的 hooks 和 permissions
- 错误处理:
handle_hook中捕获所有 panic,返回安全默认值 - 性能:钩子函数应在毫秒级完成,避免阻塞请求
- 幂等性:
before_*钩子可能被重试,确保逻辑幂等 - 版本号:严格遵循语义化版本,Breaking Change 必须升大版本
主题开发
- 同时支持亮/暗色:使用
:root和.dark分别定义 - 只覆盖 CSS 变量:尽量通过变量修改,避免覆盖具体选择器
- 测试各种内容:长标题、空帖子、多级评论等边界情况
- 无障碍:确保文字与背景的对比度符合 WCAG AA 标准
- 图片资源:使用外部 CDN 托管,不要嵌入 base64
通用
- ID 全局唯一:发布前搜索确认 ID 未被占用
- 详细的 changelog:每次更新说明具体改动
- 提供 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