Skip to content

插件开发 ​

插件(Plugin)是 ClassIntra 与外部系统联动的标准化扩展机制。与普通应用不同,插件不提供桌面图标和独立页面,而是通过通信契约将外部系统(如视频站、第三方平台)接入 ClassIntra。

源码位置:插件源码维护在 market 仓库 的 plugins/ 目录;运行时部署到班级服务器的 plugins/ 目录(该目录不属于主仓库,仅作为运行时加载点)。相关集成代码位于 client/src/integrations/、server/src/integrations/。

安装与卸载(插件市场) ​

插件通过内置插件市场分发。管理员在市场页一键安装 / 卸载,无需重启服务器——后端路由由热挂载调度器即时接管。

端点权限说明
GET /api/market/plugins-installed登录列出已安装插件
POST /api/market/install-plugin管理员按市场名安装,body { name, source? }(source 默认 gitee,失败时按源列表回退)
POST /api/market/uninstall-plugin管理员按名卸载,body { name }

安装链路:拉取市场源 index.json 的 plugins 段 → 校验插件名(kebab-case)→ 拒绝降级(市场版本低于已装版本直接报错)→ 逐文件下载(限制扩展名、单文件与总量上限, 且路径必须位于 plugins/<name>/ 下)→ 原子写入运行时 plugins/ 目录 → 后端路由热挂载 → 在 app_control 表注册为默认启用。若热挂载失败,插件仍已落盘,重启后由 route-aggregator 自动重试。

市场源 index.json 通过 plugins 段声明可安装插件,每个条目至少包含 name / label / version / description 与 files(相对仓库根的文件清单):

json
{
  "plugins": [
    {
      "name": "astrbot-relay",
      "label": "AstrBot 机器人接入",
      "version": "1.0.0",
      "description": "将 AstrBot 机器人接入 ClassIntra 私聊与公共聊天室……",
      "author": "ClassIntra",
      "files": [
        "plugins/astrbot-relay/manifest.json",
        "plugins/astrbot-relay/README.md",
        "plugins/astrbot-relay/backend/routes.js"
      ]
    }
  ]
}

插件市场的完整案例见 AstrBot 机器人接入 与 网易云音乐插件。

插件与应用的区别 ​

维度应用(app)插件(plugin)
位置apps/<name>/plugins/<name>/
manifest typeappplugin
桌面图标有(category: desktop)无(category: hidden)
前端页面必须有 frontend.route无独立路由,提供桥接模块
后端路由可选 backend通常有(身份验证等)
核心职责完整功能系统间联动

目录结构 ​

plugins/my-bridge/
├── manifest.json              # 必填,插件清单
├── README.md                  # 插件说明
├── backend/
│   └── routes.js              # 后端路由(身份验证等)
├── frontend/
│   └── bridge.js              # 前端桥接模块(postMessage 通信)
└── shared/
    └── contract.js            # 通信契约(双方共享的消息协议定义)

manifest 编写 ​

以内置的 CampusBili 桥接插件为例:

json
{
  "name": "campusbili-bridge",
  "type": "plugin",
  "version": "1.2.0",
  "label": "CampusBili 桥接",
  "category": "hidden",
  "order": 99,
  "defaultEnabled": true,
  "canDisable": false,
  "description": "ClassIntra 与 CampusBili 的统一联动插件(v1.1 协议)",
  "backend": {
    "mountPath": "/api/campusbili-bridge",
    "entry": "./backend/routes.js"
  },
  "integration": {
    "contract": "./shared/contract.js",
    "frontendBridge": "./frontend/bridge.js",
    "clientEntry": "client/src/integrations/campusbili-bridge-client.js",
    "protocolVersion": "1.1",
    "channels": [
      "handshake",
      "identity-injection",
      "request-mute",
      "video-control",
      "playback-status",
      "share-request",
      "back-button",
      "page-info",
      "ping"
    ]
  }
}

字段说明 ​

字段类型必填说明
namestring是插件唯一标识,kebab-case
typestring是固定为 plugin
categorystring否建议为 hidden(不在桌面显示图标)
canDisableboolean否联动基础插件建议 false(不允许禁用)
backendobject否后端路由挂载,同应用
integrationobject是联动集成配置,插件的核心字段

integration 字段 ​

字段类型说明
contractstring通信契约文件路径,前后端共享消息协议定义
frontendBridgestring前端桥接模块路径(供嵌入的 iframe 页面使用)
clientEntrystringclient 侧集成入口(由 client 构建引用)
protocolVersionstring协议版本号,握手时用于版本协商
channelsstring[]允许的消息通道白名单,未列出的通道消息会被拒绝

通信契约(contract.js) ​

契约文件定义双方共同遵守的消息协议,是联动的「宪法」:

javascript
// plugins/my-bridge/shared/contract.js
// 协议版本与通道白名单的唯一事实来源(Single Source of Truth)

var PROTOCOL_VERSION = '1.1';

// 允许的消息通道(未在白名单中的消息一律拒绝)
var CHANNELS = [
  'handshake',        // 握手
  'identity-injection', // 身份注入
  'ping'              // 心跳
];

// 每个通道的消息结构定义
var MESSAGE_SCHEMAS = {
  'handshake': {
    // iframe → ClassIntra
    request: { source: 'string', event: 'string', protocolVersion: 'string' },
    // ClassIntra → iframe
    response: { source: 'string', event: 'string', protocolVersion: 'string' }
  }
};

module.exports = {
  PROTOCOL_VERSION: PROTOCOL_VERSION,
  CHANNELS: CHANNELS,
  MESSAGE_SCHEMAS: MESSAGE_SCHEMAS
};

契约优先

所有联动消息必须通过契约规范:通道名进白名单、消息结构符合 schema。私自新增通道会造成版本兼容性问题——升级协议时必须同步递增 protocolVersion。

握手流程 ​

ClassIntra 与嵌入页面通过 postMessage 通信,遵循完整握手流程:

iframe(外部页面)                          ClassIntra(宿主)
      │                                          │
      │ 1. HELLO { protocolVersion: '1.1' }        │
      ├─────────────────────────────────────────→ │
      │                                          │ 2. 版本协商(兼容则继续)
      │ 3. WELCOME { protocolVersion: '1.1' }      │
      ├←───────────────────────────────────────── │
      │ 4. READY                                    │
      ├─────────────────────────────────────────→ │
      │                                          │ 5. identity-injection
      │    { token, user } (身份注入)              │
      ├←───────────────────────────────────────── │
      │                                          │
      │ 6. 业务消息(白名单通道内双向通信)            │
      ├←────────────────────────────────────────→ │
  1. HELLO:iframe 加载后向宿主声明协议版本
  2. 版本协商:宿主比对 protocolVersion,不兼容则拒绝通信
  3. WELCOME:宿主确认握手成功
  4. READY:iframe 声明就绪
  5. 身份注入:宿主将登录态(token / 用户信息)注入 iframe
  6. 业务消息:在白名单通道内双向通信(如 video-control、request-mute)

前端桥接模块(bridge.js) ​

桥接模块运行在被嵌入的外部页面中,封装 postMessage 细节:

javascript
// plugins/my-bridge/frontend/bridge.js
// 外部页面引入此模块即可与 ClassIntra 通信

(function(global) {
  'use strict';

  var PROTOCOL_VERSION = '1.1';
  var handlers = {};
  var ready = false;

  // 监听宿主消息
  global.addEventListener('message', function(event) {
    var msg = event.data;
    if (!msg || msg.source !== 'classintra') return;
    if (msg.event === 'welcome') {
      ready = true;
      post('ready', {});
    }
    if (handlers[msg.event]) handlers[msg.event](msg.data);
  });

  function post(event, data) {
    global.parent.postMessage({
      source: 'external-app',
      event: event,
      protocolVersion: PROTOCOL_VERSION,
      data: data || {}
    }, '*');
  }

  var Bridge = {
    // 发起握手
    hello: function() { post('hello', { protocolVersion: PROTOCOL_VERSION }); },
    // 注册通道处理器
    on: function(channel, fn) { handlers[channel] = fn; },
    // 发送消息(仅白名单通道)
    send: function(channel, data) {
      if (!ready) { console.warn('[bridge] 尚未完成握手'); return; }
      post(channel, data);
    },
    isReady: function() { return ready; }
  };

  global.MyBridge = Bridge;
})(window);

client 集成入口 ​

宿主侧(ClassIntra)的集成逻辑位于 client/src/integrations/,负责:

  • 监听并校验 iframe 消息(来源、协议版本、通道白名单)
  • 握手响应与身份注入
  • 将业务事件转发给对应模块(如超能岛视频岛、静音控制)
client/src/integrations/campusbili-bridge-client.js
    ├─ 校验 event.source / protocolVersion / channels
    ├─ 响应 HELLO → WELCOME
    ├─ READY 后注入身份
    └─ video-control → Vuex island 模块(视频岛)
       request-mute → 音乐应用暂停

后端路由 ​

插件后端路由与应用一致,挂载到 backend.mountPath:

javascript
// plugins/my-bridge/backend/routes.js
var express = require('express');
var router = express.Router();
var auth = require('../../server/src/middleware/auth');

// 验证联动会话有效性(iframe 内 fetch 携带注入的 token)
router.get('/session', auth.requireAuth, function(req, res) {
  res.json({
    code: 200,
    data: { user_id: req.user.user_id, nickname: req.user.nickname }
  });
});

module.exports = router;

相对路径

插件位于仓库根 plugins/,引用 server 模块的相对路径为 ../../server/src/...(应用是 ../../../server/src/...)。

验证清单 ​

  • ✅ manifest.json 通过校验:type: plugin、integration 完整
  • ✅ 后端启动日志显示路由挂载:挂载应用路由: my-bridge -> /api/my-bridge
  • ✅ iframe 引入 bridge.js 后能完成 HELLO → WELCOME → READY 握手
  • ✅ 非白名单通道消息被拒绝并告警
  • ✅ 协议版本不匹配时握手失败且不抛异常
  • ✅ 身份注入后 iframe 内 API 调用携带有效 token

下一步 ​