本仓库是 ACLiveFrame 插件开发工具的 monorepo 项目。它提供了完整的插件开发 CLI 工具和模板,支持使用 pnpm workspace 管理多个插件,提供统一的构建系统、热重载开发环境,以及完整的工具箱 API 类型定义。
- 架构概览
- 快速开始
- 插件管理
- 开发工作流
- 项目结构
- 配置指南 (
pluginConfig) - Window / UI / Overlay 形态说明
- 前端开发示例
- 后端开发
- 插件调试
- API 参考
- 故障排除
- 开发提示
- 贡献
ACLiveFrame 插件系统采用 双进程模型 (Dual-Process Model) 以确保稳定性和灵活性:
-
前端 (渲染进程 Renderer):
- 基于 Vue 3 + Vite 构建。
- 运行在安全的沙箱环境中 ( Wujie 微前端)。
- 负责处理 独立窗口 (Window)、侧边栏面板 (UI) 和 直播画板 (Overlay) 的界面渲染。
- 通过标准的 HTTP/WebSocket 或工具箱提供的 IPC 桥接器与后端通信。
前端承载机制: 使用 Wujie 微前端框架通过 iframe 沙箱承载插件,确保插件间的隔离。每个形态对应不同的页面组件 (Window/UI/Overlay),通过统一的
usePluginFramecomposable 处理插件加载和 API 注入。 -
后端 (主进程 Main Process):
- 作为工具箱管理的 Node.js 子进程运行。
- 入口点定义在
src/main/index.ts。 - 拥有访问 工具箱 API (系统、文件系统、AcFun API 等) 的完整权限。
- 负责业务逻辑、数据持久化和繁重的计算任务。
本项目采用 pnpm workspace 管理多个插件:
packages/- 存放所有插件包,每个插件都是独立的npm包template/- 插件开发模板,用于快速创建新插件types/- 共享的TypeScript类型定义,提供完整的API类型支持cli.js- 根目录的CLI工具,提供插件创建、构建、开发等功能
插件通过 manifest.json 文件定义。在此 CLI 工具中,该清单文件由 package.json 中的 pluginConfig 字段自动生成。
- Node.js (推荐 v18+)
- pnpm (必须 v7.0+,推荐最新版)
克隆本仓库并安装依赖:
git clone <repository-url>
cd plugin-cli
pnpm install# 检查 pnpm 版本
pnpm --version
# 检查 workspace 配置
pnpm -r ls# 基本创建(会交互式选择插件类型)
pnpm run create <pluginName>
# 带描述的创建
pnpm run create <pluginName> --description "插件描述"创建过程中会:
- 验证插件名称(必须以英文字母开头,只能包含字母、数字、连字符、下划线)
- 选择需要的插件类型(window、ui、overlay、main)
- 自动复制模板并配置
package.json - 注入 TypeScript 路径别名指向根目录
types/
# 列出所有插件
ls packages/
# 查看插件状态和版本
pnpm -r ls
# 查看插件信息
pnpm info <pluginName>- obs-assistant: OBS直播助手插件,提供自动启动OBS、同步推流配置等功能,支持UI面板形态。
# 在根目录启动指定插件的开发服务器
pnpm run dev <pluginName>
# 例如:开发名为 "obs-assistant" 的插件
pnpm run dev obs-assistant开发模式会:
- 启动 Vite 开发服务器(支持热重载)
- 启动 TypeScript 监听器监控后端代码
- 自动生成 manifest.json
- 输出构建产物到插件目录的
release/文件夹
# 在根目录构建指定插件
pnpm run build <pluginName>
# 构建产物会输出到插件目录的 release/ 文件夹
# 生成的文件名格式: <pluginName>@<version>.zip# 1. 创建新插件(在根目录执行)
pnpm run create my-awesome-plugin
# 2. 进入插件目录
cd packages/my-awesome-plugin
# 3. 安装插件依赖(如果需要额外依赖)
pnpm install
# 4. 返回根目录,启动开发模式
cd ../../
pnpm run dev my-awesome-plugin
# 5. 在另一个终端构建发布版本
pnpm run build my-awesome-plugin在插件目录 (packages/<pluginName>/) 中,你可以使用以下命令:
| 命令 | 说明 |
|---|---|
pnpm run dev |
启动开发服务器,支持热重载 |
pnpm run build |
编译前端和后端代码并打包 |
pnpm run build:main |
仅编译后端 TypeScript 代码 |
pnpm run watch:main |
监听后端代码变化并自动编译 |
pnpm run package |
生成发布包 (zip 文件) |
pnpm run clean |
清理构建产物目录 |
注意: 推荐使用根目录的 pnpm run dev/build <pluginName> 命令进行开发和构建。
plugin-cli/
├── cli.js # 主 CLI 工具 (Node.js)
├── packages/ # 插件包目录
│ └── obs-assistant/ # 示例插件: OBS直播助手
│ ├── src/
│ │ ├── app/ # Vue 3 前端应用
│ │ │ ├── main.ts # 前端入口点
│ │ │ ├── shell/ # 应用外壳组件
│ │ │ └── views/ # 页面视图 (UI, Overlay, Window)
│ │ └── main/ # Node.js 后端逻辑
│ │ └── index.ts # 后端入口点
│ ├── public/ # 静态资源 (图标等)
│ ├── release/ # 插件构建产物
│ ├── package.json # 插件配置 (包含 pluginConfig)
│ ├── vite.config.ts # Vite 配置
│ └── tsconfig*.json # TypeScript 配置
├── template/ # 插件开发模板
├── types/ # 共享类型定义
│ ├── toolbox-api.d.ts # 前端 API 类型
│ ├── toolbox-api-main.d.ts # 后端 API 类型
│ ├── danmu.d.ts # 弹幕相关类型
│ └── global.d.ts # 全局类型定义
├── package.json # Monorepo 配置
├── pnpm-workspace.yaml # Workspace 定义
└── README.md # 项目文档
在 package.json 的 pluginConfig 字段下配置您的插件。
"pluginConfig": {
"spa": true, // 是否为单页应用 (SPA),当为 true 时 html 字段无效
"main": { // 后端入口配置
"dir": ".", // 入口文件所在目录
"file": "index.js", // 入口文件名 (由 src/main/index.ts 编译而来)
"libs": [] // 依赖库 (可选)
},
"icon": "icon.svg", // 插件图标路径 (相对于 public 目录)
// 独立窗口配置
"window": {
"route": "/window", // Vue 应用中的路由路径
"width": 1024, // 窗口宽度
"height": 768, // 窗口高度
"minWidth": 400, // 最小宽度
"minHeight": 200, // 最小高度
"resizable": true, // 是否可调整大小
"html": "index.html" // HTML 模板文件 (当 spa 为 true 时无效)
},
// OBS 直播画板 / 挂件配置
"overlay": {
"route": "/overlay", // Vue 应用中的路由路径
"html": "index.html" // HTML 模板文件 (当 spa 为 true 时无效)
},
// 用户配置项 (显示在工具箱设置页面中)
"config": {
"config": {
"type": "input", // 配置类型: input, select, boolean, textarea, text, file, directory
"label": "配置项", // 显示标签
"description": "这里可以输入插件的配置项,并在插件管理-查看详情-设置中统一管理,支持boolean、number、select、textarea、text、file、directory",
"default": "" // 默认值
},
"select_demo": {
"type": "select", // 下拉选择框
"label": "选择示例",
"description": "这是一个下拉选择框示例",
"default": "option1",
"options": [ // 选项列表
{
"label": "选项1",
"value": "option1"
},
{
"label": "选项2",
"value": "option2"
}
]
}
}
}ACLiveFrame 支持三种不同的插件前端形态,每种形态适合不同的使用场景:
| 形态 | Window | UI | Overlay |
|---|---|---|---|
| 用途 | 独立工具窗口 | 嵌入式侧边栏/面板 | 直播覆盖层/挂件 |
| 特点 | 完整独立页面,可调整大小 | 轻量嵌入,共享宿主上下文 | 高性能渲染,透明定位 |
| 适用场景 | 复杂工具、多步骤流程 | 设置面板、状态监控 | 实时信息展示、交互挂件 |
| 路由路径 | /window |
/ui |
/overlay |
| 生命周期 | 独立窗口生命周期 | 随宿主面板显示 | 随直播场景激活 |
| 性能考虑 | 内存独立,资源充足 | 受宿主约束,轻量化 | 高帧率,DOM 操作最小化 |
| 交互方式 | 窗口控制 API | 面板内交互 | 覆盖层事件处理 |
{
"window": {
"route": "/window",
"width": 1024,
"height": 768,
"minWidth": 400,
"minHeight": 200,
"resizable": true
}
}{
"ui": {
"route": "/ui"
}
}{
"overlay": {
"route": "/overlay"
}
}- 选择 Window:如果插件需要复杂的用户界面、多步骤操作,或需要较大的显示空间
- 选择 UI:如果插件主要是设置界面、信息展示,或需要与宿主工具箱紧密集成
- 选择 Overlay:如果插件需要在直播画面上显示实时信息、进行视觉增强,或需要高性能渲染
在 src/app/main.ts 中配置不同形态的路由:
import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import WindowView from './views/WindowView.vue'
import UiView from './views/UiView.vue'
import OverlayView from './views/OverlayView.vue'
const routes = [
{
path: '/window',
component: WindowView
},
{
path: '/ui',
component: UiView
},
{
path: '/overlay',
component: OverlayView
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
const app = createApp(App)
app.use(router)
app.mount('#app')<template>
<div>
<button @click="sendToBackend">发送消息</button>
</div>
</template>
<script setup lang="ts">
// 假设在 Window 形态中
import type { ToolboxWindowApi } from '@types/toolbox-api'
// --- API Resolution ---
const getApi = (): ToolboxWindowApi => {
if ((window as any).toolboxApi) return (window as any).toolboxApi
console.warn('Toolbox API not found')
return {} as any
}
const api = getApi()
const sendToBackend = async () => {
if (api?.sendMain) {
// 发送消息到后端
await api.sendMain({
action: 'test',
method: 'logger.info',
params: { msg: '来自前端的消息' }
})
}
}
</script><template>
<div>
<p>收到的消息: {{ receivedMessage }}</p>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import type { ToolboxWindowApi } from '@types/toolbox-api'
// --- API Resolution ---
const getApi = (): ToolboxWindowApi => {
if ((window as any).toolboxApi) return (window as any).toolboxApi
console.warn('Toolbox API not found')
return {} as any
}
const api = getApi()
const receivedMessage = ref('')
let unsubscribe: (() => void) | undefined
onMounted(() => {
// 监听后端发送的消息
if (api?.onMainMessage) {
unsubscribe = api.onMainMessage((payload: any) => {
if (payload.type === 'update') {
receivedMessage.value = payload.data
}
})
}
})
onUnmounted(() => {
if (unsubscribe) {
unsubscribe()
}
})
</script><!-- WindowView.vue -->
<template>
<div class="window-container">
<header>
<h1>我的工具窗口</h1>
<button @click="minimizeWindow">最小化</button>
</header>
<main>
<!-- 窗口内容 -->
</main>
</div>
</template>
<script setup lang="ts">
import { onMounted } from 'vue'
import type { ToolboxWindowApi } from '@types/toolbox-api'
// --- API Resolution ---
const getApi = (): ToolboxWindowApi => {
if ((window as any).toolboxApi) return (window as any).toolboxApi
console.warn('Toolbox API not found in WindowView')
return {} as any
}
const api = getApi()
const minimizeWindow = async () => {
if (api?.window) {
await api.window.minimize()
}
}
</script><!-- UiView.vue -->
<template>
<div class="ui-panel">
<div class="panel-header">
<h3>设置面板</h3>
</div>
<div class="panel-content">
<!-- 轻量级设置界面 -->
<form @submit.prevent="saveSettings">
<input v-model="setting" type="text" placeholder="设置项">
<button type="submit">保存</button>
</form>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { ToolboxUiApi } from '@types/toolbox-api'
// --- API Resolution ---
const getApi = (): ToolboxUiApi => {
if ((window as any).toolboxApi) return (window as any).toolboxApi
console.warn('Toolbox API not found in UiView')
return {} as any
}
const api = getApi()
const setting = ref('')
const saveSettings = async () => {
if (api?.settings?.set) {
await api.settings.set('userSetting', setting.value)
}
console.log('保存设置:', setting.value)
}
</script>
<style scoped>
.ui-panel {
max-width: 300px; /* 受宿主面板尺寸限制 */
padding: 16px;
}
</style><!-- OverlayView.vue -->
<template>
<div class="overlay-container" :style="{ left: position.x + 'px', top: position.y + 'px' }">
<div class="overlay-content">
<!-- 实时信息展示 -->
<div class="info-badge">{{ liveInfo.viewerCount }}</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import type { ToolboxOverlayApi } from '@types/toolbox-api'
// --- API Resolution ---
const getApi = (): ToolboxOverlayApi => {
if ((window as any).toolboxApi) return (window as any).toolboxApi
console.warn('Toolbox API not found in OverlayView')
return {} as any
}
const api = getApi()
const position = ref({ x: 100, y: 100 })
const liveInfo = ref({ viewerCount: 0 })
onMounted(async () => {
// 订阅直播数据更新
if (api?.subscribeEvents) {
api.subscribeEvents(['message'], (event) => {
// 高性能更新循环
const updateLoop = () => {
// 处理直播数据更新
liveInfo.value.viewerCount = Math.floor(Math.random() * 1000)
requestAnimationFrame(updateLoop)
}
updateLoop()
})
}
})
</script>
<style scoped>
.overlay-container {
position: absolute;
pointer-events: none; /* 避免阻挡直播画面交互 */
z-index: 1000;
}
.overlay-content {
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 8px 12px;
border-radius: 4px;
font-size: 14px;
backdrop-filter: blur(4px);
}
</style># 在根目录启动开发服务器
pnpm run dev <pluginName>
# 例如:开发 obs-assistant 插件
pnpm run dev obs-assistant
# 访问不同形态:
# - Window: http://localhost:5173/window
# - UI: http://localhost:5173/ui
# - Overlay: http://localhost:5173/overlay# 在根目录构建插件
pnpm run build <pluginName>
# 或者在插件目录内:
# 构建前端资源
pnpm run build
# 构建后端代码
pnpm run build:main
# 打包为发布包
pnpm run package💡 深入了解: 有关后端工作原理、Worker 架构和何时需要后端的详细信息,请参考 插件开发指南 - 后端原理详解。
后端逻辑位于 src/main/index.ts。您必须导出特定的生命周期函数来与工具箱进行交互。
import type { ToolboxMainApi } from '@types/toolbox-api-main'
let apiRef: ToolboxMainApi | undefined
let startedAt: number | undefined
// 本地保存订阅的关闭函数,用于清理
const subscriptions: Map<string, () => void> = new Map()
export function afterLoaded(api: ToolboxMainApi) {
apiRef = api
startedAt = Date.now()
api.logger.info('[插件] 已启动')
// 监听来自前端的消息
api.onUiMessage((payload: any) => {
handleUiMessage(payload)
})
// 示例:监听配置变更
api.settings.onChange((newConfig) => {
api.logger.info('配置已更新:', newConfig)
})
}
export function cleanup() {
apiRef?.logger?.info('[插件] 正在清理...')
// 清理所有订阅
for (const closer of subscriptions.values()) {
try { closer() } catch (e) {}
}
subscriptions.clear()
apiRef = undefined
}
export function getStatus() {
return {
startedAt,
running: !!apiRef
}
}
export function onConfigUpdated(newConfig: unknown) {
apiRef?.logger?.info(`[插件] 配置更新: ${JSON.stringify(newConfig)}`)
}
/**
* 处理来自前端的消息
*/
async function handleUiMessage(payload: any) {
if (!apiRef) return
try {
// 解析消息
if (payload.action === 'test' && payload.method) {
// 处理测试调用
const result = await handleTestMethod(payload.method, payload.params)
// 可以发送结果回前端
apiRef.sendUI({ type: 'response', result })
}
} catch (error) {
apiRef.logger.error('[插件] 处理消息失败:', error)
}
}
/**
* 处理测试方法调用
*/
async function handleTestMethod(method: string, params: any) {
if (!apiRef) throw new Error('API not initialized')
const methodParts = method.split('.')
let target: any = apiRef
// 遍历方法路径
for (const part of methodParts) {
if (target && typeof target === 'object' && part in target) {
target = target[part]
} else {
throw new Error(`Method not found: ${method}`)
}
}
if (typeof target !== 'function') {
throw new Error(`${method} is not a function`)
}
// 调用方法
if (Array.isArray(params)) {
return await target(...params)
} else if (typeof params === 'object' && params !== null) {
// 根据方法签名决定参数传递方式
return await target(params)
} else {
return await target()
}
}ToolboxMainApi 提供了后端访问宿主系统和 AcFun 服务的接口。完整的后端类型定义请参阅 types/toolbox-api-main.d.ts。
ToolboxWindowApi、ToolboxUiApi 等前端 API 提供了前端组件访问工具箱服务的接口。完整的前端类型定义请参阅 types/toolbox-api.d.ts。
- 所有插件侧 SSE 订阅均通过统一通道
GET /sse/plugins/:pluginId/overlay,由注入的toolboxApi封装。 - 订阅/取消订阅由主进程通过
/api/plugins/:pluginId/subscribe管理,无需插件手动拼接kinds或 query。 - 示例:
- 监听消息:
api.subscribeEvents(['message'], (env) => ...) - 监听配置变更:
api.settings.onChange(cb)(底层使用configkind) - 监听只读 store:
api.store.onChange(['account','ui'], cb)(底层使用storekind + storeKeys)
- 监听消息:
访问用户信息、直播间信息、发送弹幕和礼物数据。
// 获取用户信息
const user = await api.acfun.user.getUserInfo('123456');
// 发送弹幕 (需要登录态)
await api.acfun.danmu.sendComment('liveId', '你好,世界');
// 获取直播间信息
const room = await api.acfun.danmu.getLiveRoomInfo('liverId');沙箱化的文件访问和持久化存储。
// 键值对存储 (基于 SQLite)
await api.fs.pluginStorage.write({ key: 'count', value: 1 });
const data = await api.fs.pluginStorage.read('count');
// 简单的文件 IO
await api.fs.writeFile('data.txt', 'content');
const content = await api.fs.readFile('data.txt');控制插件的独立窗口 (如果已配置)。
await api.window.setSize(1024, 768);
await api.window.setAlwaysOnTop(true);
await api.window.minimize();监听应用程序事件。
api.lifecycle.on('beforeUnload', () => {
// 在应用关闭前保存状态
});将日志输出到主工具箱的日志文件中。
api.logger.info('普通信息');
api.logger.error('发生错误');通过主进程发起 HTTP 请求 (绕过 CORS 限制)。
const response = await api.http.get('https://api.example.com/data');访问全局应用程序状态 (只读)。
const state = await api.store.get(['account']);
console.log(state.account.userInfo);订阅直播弹幕、礼物、点赞等实时事件。
// 订阅弹幕事件
api.subscribeDanmaku([], (event: any) => {
switch (event.type) {
case 'comment':
// 处理评论: event.content, event.userInfo.nickname
console.log(`${event.userInfo.nickname}: ${event.content}`);
break;
case 'gift':
// 处理礼物: event.giftDetail.giftName, event.count, event.value
console.log(`${event.userInfo.nickname} 送了 ${event.count} 个 ${event.giftDetail.giftName}`);
break;
case 'like':
// 处理点赞
console.log(`${event.userInfo.nickname} 点了赞`);
break;
}
});
// 按房间订阅特定事件
api.subscribeDanmaku([{
roomId: '123456',
eventTypes: ['comment', 'gift']
}], (event) => {
// 只处理指定房间和事件类型的弹幕
console.log('房间弹幕:', event);
});在插件开发过程中,可以使用工具箱的调试功能来连接外部开发服务器:
# 1. 在根目录启动开发服务器
pnpm run dev <pluginName>
# 例如:启动 obs-assistant 的开发服务器
pnpm run dev obs-assistant
# 2. 在工具箱中添加调试插件
# 打开插件管理页面 -> 添加插件 -> 添加调试插件
# 配置外部开发服务器地址和端口在工具箱插件管理页面中:
- 点击 "添加插件" → "添加调试插件"
- 在弹出的调试工具对话框中配置:
- 插件ID: 唯一标识符
- 开发服务器地址:
http://localhost:5173(或其他端口) - 插件配置: manifest.json 中的配置项
- 点击 "测试加载" 验证连接
- 点击 "确认" 添加调试插件
调试插件支持热重载,当你修改代码时:
- 前端代码会自动重新编译和刷新
- 后端代码修改后需要手动重新加载插件
- 配置文件修改后需要重新添加调试插件
- 控制台日志: 在工具箱开发者工具中查看插件日志
- 网络请求: 监控插件的网络活动
- 状态检查: 使用插件提供的状态检查功能
- 错误追踪: 查看工具箱日志文件中的详细错误信息
调试插件会在插件列表中显示特殊的 "调试插件" 标签,并且:
- 图标会显示随机图标而不是固定图标
- 支持实时重载和配置更新
- 开发服务器断开时会显示连接错误状态
前端调试:
// 在插件代码中使用 console.log
console.log('插件初始化完成', { pluginId, version });
// 使用浏览器开发者工具
// F12 打开开发者工具,查看 Console 和 Network 标签后端调试:
// 在后端代码中使用 logger
api.logger.info('插件收到消息', { message, timestamp: Date.now() });
// 检查插件状态
const status = api.getStatus();
console.log('插件状态:', status);配置调试:
// 检查配置是否正确加载
api.settings.get().then(config => {
console.log('当前配置:', config);
});Q: 插件无法加载或运行?
- 检查
package.json中的pluginConfig配置是否正确 - 确保
src/main/index.ts导出了必需的生命周期函数 (afterLoaded,cleanup) - 查看工具箱的日志输出以获取详细错误信息
Q: 前端页面无法正常显示?
- 确认路由配置正确 (在
pluginConfig中设置正确的route) - 检查 Vue 组件是否正确导入和注册
- 确保构建过程成功完成 (
pnpm build)
Q: API 调用失败?
- 验证 API 方法名称和参数格式
- 检查工具箱版本兼容性
- 查看
src/types/toolbox-api.d.ts获取正确的类型定义
Q: 热重载不工作?
- 确保使用
pnpm dev命令启动开发服务器 - 检查控制台是否有 TypeScript 编译错误
- 确认文件修改已保存
- 使用
api.logger记录调试信息,这些日志会输出到工具箱的主日志文件中 - 在前端代码中使用
console.log,日志会显示在插件的开发者工具中 - 利用
getStatus()函数返回插件的运行状态
- 类型安全: 充分利用
types/目录下的 TypeScript 类型定义,避免运行时错误 - 错误处理: 在异步操作中添加适当的 try-catch 块,后端代码遵循"只在必要的地方用try-catch"的原则
- 资源清理: 在
cleanup()函数中释放所有订阅和定时器 - 代码风格: 函数声明只允许使用箭头函数风格,保持代码简洁干练
- 性能优化: 避免频繁的 API 调用,合理使用缓存,UI形态插件注意轻量化
欢迎提交 Issue 和 Pull Request 来改进这个插件模板!
请确保:
- 代码符合现有的代码风格
- 添加必要的类型定义
- 更新相关文档
- 测试功能在不同环境下正常工作