Appearance
插件开发
AnythingLLM 提供了强大的插件系统,允许开发者扩展平台功能、集成第三方服务,以及创建自定义的工作流程。本指南将详细介绍如何开发、测试和部署 AnythingLLM 插件。
插件系统概述
插件架构
AnythingLLM 的插件系统基于事件驱动架构,支持以下类型的插件:
- LLM 提供商插件: 集成新的大语言模型
- 向量数据库插件: 支持新的向量存储后端
- 文档处理插件: 扩展文档解析和处理能力
- 认证插件: 集成第三方认证系统
- 工作流插件: 创建自定义的自动化流程
- UI 组件插件: 扩展前端界面功能
插件生命周期
- 初始化: 插件加载和配置
- 注册: 注册事件监听器和路由
- 执行: 响应系统事件和用户操作
- 清理: 插件卸载和资源释放
快速开始
创建插件项目
使用官方脚手架工具快速创建插件项目:
bash
# 安装插件开发工具
npm install -g @anythingllm/plugin-cli
# 创建新插件
anythingllm-plugin create my-awesome-plugin
# 进入插件目录
cd my-awesome-plugin
# 安装依赖
npm install
插件目录结构
my-awesome-plugin/
├── package.json # 插件配置和依赖
├── plugin.json # 插件元数据
├── src/
│ ├── index.js # 插件入口文件
│ ├── handlers/ # 事件处理器
│ ├── routes/ # API 路由
│ ├── components/ # 前端组件
│ └── utils/ # 工具函数
├── tests/ # 测试文件
├── docs/ # 插件文档
└── README.md
插件配置文件
plugin.json
定义了插件的基本信息:
json
{
"name": "my-awesome-plugin",
"version": "1.0.0",
"description": "一个很棒的 AnythingLLM 插件",
"author": "Your Name",
"license": "MIT",
"main": "src/index.js",
"type": "workflow",
"permissions": [
"workspace.read",
"workspace.write",
"document.process"
],
"dependencies": {
"axios": "^1.0.0"
},
"config": {
"apiKey": {
"type": "string",
"required": true,
"description": "第三方服务 API 密钥"
},
"endpoint": {
"type": "string",
"default": "https://api.example.com",
"description": "API 端点 URL"
}
}
}
插件开发
基础插件结构
每个插件都需要实现基础的插件类:
javascript
// src/index.js
const { BasePlugin } = require('@anythingllm/plugin-sdk');
class MyAwesomePlugin extends BasePlugin {
constructor(config) {
super(config);
this.name = 'my-awesome-plugin';
this.version = '1.0.0';
}
// 插件初始化
async initialize() {
console.log('插件初始化中...');
// 注册事件监听器
this.registerEventHandlers();
// 注册 API 路由
this.registerRoutes();
// 注册前端组件
this.registerComponents();
console.log('插件初始化完成');
}
// 注册事件处理器
registerEventHandlers() {
// 文档上传事件
this.on('document.uploaded', this.handleDocumentUploaded.bind(this));
// 聊天消息事件
this.on('chat.message', this.handleChatMessage.bind(this));
// 工作空间创建事件
this.on('workspace.created', this.handleWorkspaceCreated.bind(this));
}
// 注册 API 路由
registerRoutes() {
this.router.get('/status', this.getStatus.bind(this));
this.router.post('/process', this.processData.bind(this));
}
// 注册前端组件
registerComponents() {
this.addComponent('sidebar', {
name: 'MyPluginSidebar',
component: require('./components/Sidebar')
});
}
// 事件处理器
async handleDocumentUploaded(event) {
const { document, workspace } = event.data;
console.log(`文档 ${document.filename} 已上传到工作空间 ${workspace.name}`);
// 自定义处理逻辑
await this.processDocument(document);
}
async handleChatMessage(event) {
const { message, workspace, user } = event.data;
// 检查是否需要插件处理
if (message.includes('@my-plugin')) {
const response = await this.generateResponse(message);
return { response, handled: true };
}
return { handled: false };
}
// API 路由处理器
async getStatus(req, res) {
res.json({
status: 'active',
version: this.version,
uptime: process.uptime()
});
}
async processData(req, res) {
try {
const result = await this.processUserData(req.body);
res.json({ success: true, data: result });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
}
// 插件清理
async cleanup() {
console.log('插件清理中...');
// 清理资源、关闭连接等
}
}
module.exports = MyAwesomePlugin;
LLM 提供商插件
创建自定义 LLM 提供商插件:
javascript
// src/providers/CustomLLMProvider.js
const { BaseLLMProvider } = require('@anythingllm/plugin-sdk');
class CustomLLMProvider extends BaseLLMProvider {
constructor(config) {
super(config);
this.name = 'custom-llm';
this.apiKey = config.apiKey;
this.endpoint = config.endpoint;
}
// 实现聊天接口
async chat(messages, options = {}) {
try {
const response = await this.makeRequest('/chat/completions', {
messages: this.formatMessages(messages),
model: options.model || 'default',
temperature: options.temperature || 0.7,
max_tokens: options.maxTokens || 2048
});
return {
content: response.choices[0].message.content,
usage: response.usage,
model: response.model
};
} catch (error) {
throw new Error(`CustomLLM Error: ${error.message}`);
}
}
// 实现流式聊天接口
async streamChat(messages, options = {}) {
const stream = await this.makeStreamRequest('/chat/completions', {
messages: this.formatMessages(messages),
model: options.model || 'default',
temperature: options.temperature || 0.7,
max_tokens: options.maxTokens || 2048,
stream: true
});
return stream;
}
// 格式化消息
formatMessages(messages) {
return messages.map(msg => ({
role: msg.role,
content: msg.content
}));
}
// 发送 HTTP 请求
async makeRequest(endpoint, data) {
const response = await fetch(`${this.endpoint}${endpoint}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`
},
body: JSON.stringify(data)
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
}
// 获取可用模型
async getModels() {
const response = await this.makeRequest('/models', {});
return response.data.map(model => ({
id: model.id,
name: model.name,
maxTokens: model.max_tokens
}));
}
}
module.exports = CustomLLMProvider;
文档处理插件
创建自定义文档处理器:
javascript
// src/processors/CustomDocumentProcessor.js
const { BaseDocumentProcessor } = require('@anythingllm/plugin-sdk');
class CustomDocumentProcessor extends BaseDocumentProcessor {
constructor(config) {
super(config);
this.supportedTypes = ['.custom', '.special'];
}
// 检查是否支持该文件类型
canProcess(filename) {
const ext = path.extname(filename).toLowerCase();
return this.supportedTypes.includes(ext);
}
// 处理文档
async process(filePath, options = {}) {
try {
const content = await this.extractContent(filePath);
const chunks = await this.chunkContent(content, options);
return {
content,
chunks,
metadata: {
processor: this.constructor.name,
processedAt: new Date().toISOString(),
chunkCount: chunks.length
}
};
} catch (error) {
throw new Error(`文档处理失败: ${error.message}`);
}
}
// 提取文档内容
async extractContent(filePath) {
// 实现自定义文档解析逻辑
const buffer = await fs.readFile(filePath);
// 根据文件类型进行解析
if (filePath.endsWith('.custom')) {
return this.parseCustomFormat(buffer);
} else if (filePath.endsWith('.special')) {
return this.parseSpecialFormat(buffer);
}
throw new Error('不支持的文件格式');
}
// 分块处理
async chunkContent(content, options) {
const chunkSize = options.chunkSize || 1000;
const overlap = options.overlap || 200;
const chunks = [];
let start = 0;
while (start < content.length) {
const end = Math.min(start + chunkSize, content.length);
const chunk = content.slice(start, end);
chunks.push({
content: chunk,
start,
end,
length: chunk.length
});
start = end - overlap;
}
return chunks;
}
// 解析自定义格式
parseCustomFormat(buffer) {
// 实现自定义解析逻辑
return buffer.toString('utf-8');
}
// 解析特殊格式
parseSpecialFormat(buffer) {
// 实现特殊格式解析逻辑
return buffer.toString('utf-8');
}
}
module.exports = CustomDocumentProcessor;
前端组件插件
创建自定义前端组件:
jsx
// src/components/CustomSidebar.jsx
import React, { useState, useEffect } from 'react';
import { usePlugin } from '@anythingllm/plugin-sdk/react';
const CustomSidebar = () => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const plugin = usePlugin();
useEffect(() => {
loadData();
}, []);
const loadData = async () => {
try {
setLoading(true);
const response = await plugin.api.get('/status');
setData(response.data);
} catch (error) {
console.error('加载数据失败:', error);
} finally {
setLoading(false);
}
};
const handleAction = async () => {
try {
await plugin.api.post('/process', { action: 'custom' });
plugin.notify('操作执行成功', 'success');
await loadData();
} catch (error) {
plugin.notify('操作执行失败', 'error');
}
};
if (loading) {
return <div className="loading">加载中...</div>;
}
return (
<div className="custom-sidebar">
<h3>自定义插件</h3>
<div className="status-section">
<h4>状态信息</h4>
<p>版本: {data?.version}</p>
<p>状态: {data?.status}</p>
<p>运行时间: {Math.floor(data?.uptime / 60)} 分钟</p>
</div>
<div className="actions-section">
<h4>操作</h4>
<button onClick={handleAction} className="btn-primary">
执行自定义操作
</button>
</div>
<div className="settings-section">
<h4>设置</h4>
<PluginSettings />
</div>
</div>
);
};
// 插件设置组件
const PluginSettings = () => {
const plugin = usePlugin();
const [settings, setSettings] = useState({});
useEffect(() => {
loadSettings();
}, []);
const loadSettings = async () => {
const config = await plugin.getConfig();
setSettings(config);
};
const saveSettings = async () => {
try {
await plugin.updateConfig(settings);
plugin.notify('设置保存成功', 'success');
} catch (error) {
plugin.notify('设置保存失败', 'error');
}
};
return (
<div className="plugin-settings">
<div className="form-group">
<label>API 密钥:</label>
<input
type="password"
value={settings.apiKey || ''}
onChange={(e) => setSettings({...settings, apiKey: e.target.value})}
/>
</div>
<div className="form-group">
<label>端点 URL:</label>
<input
type="url"
value={settings.endpoint || ''}
onChange={(e) => setSettings({...settings, endpoint: e.target.value})}
/>
</div>
<button onClick={saveSettings} className="btn-secondary">
保存设置
</button>
</div>
);
};
export default CustomSidebar;
插件测试
单元测试
javascript
// tests/unit/plugin.test.js
const MyAwesomePlugin = require('../src/index');
describe('MyAwesomePlugin', () => {
let plugin;
beforeEach(() => {
plugin = new MyAwesomePlugin({
apiKey: 'test-key',
endpoint: 'https://test.example.com'
});
});
test('应该正确初始化插件', () => {
expect(plugin.name).toBe('my-awesome-plugin');
expect(plugin.version).toBe('1.0.0');
});
test('应该处理文档上传事件', async () => {
const event = {
data: {
document: { filename: 'test.pdf' },
workspace: { name: 'test-workspace' }
}
};
const spy = jest.spyOn(plugin, 'processDocument');
await plugin.handleDocumentUploaded(event);
expect(spy).toHaveBeenCalledWith(event.data.document);
});
test('应该正确处理聊天消息', async () => {
const event = {
data: {
message: '@my-plugin 帮我处理这个',
workspace: { id: 1 },
user: { id: 1 }
}
};
const result = await plugin.handleChatMessage(event);
expect(result.handled).toBe(true);
expect(result.response).toBeDefined();
});
});
集成测试
javascript
// tests/integration/api.test.js
const request = require('supertest');
const { createTestApp } = require('@anythingllm/test-utils');
describe('插件 API 集成测试', () => {
let app;
let plugin;
beforeAll(async () => {
app = await createTestApp();
plugin = new MyAwesomePlugin({ apiKey: 'test-key' });
await app.loadPlugin(plugin);
});
test('GET /plugins/my-awesome-plugin/status', async () => {
const response = await request(app)
.get('/plugins/my-awesome-plugin/status')
.expect(200);
expect(response.body.status).toBe('active');
expect(response.body.version).toBe('1.0.0');
});
test('POST /plugins/my-awesome-plugin/process', async () => {
const response = await request(app)
.post('/plugins/my-awesome-plugin/process')
.send({ data: 'test data' })
.expect(200);
expect(response.body.success).toBe(true);
expect(response.body.data).toBeDefined();
});
});
插件部署
本地开发
bash
# 启动开发模式
npm run dev
# 运行测试
npm test
# 构建插件
npm run build
插件打包
bash
# 打包插件
anythingllm-plugin build
# 生成插件包
anythingllm-plugin package
插件安装
bash
# 从本地文件安装
anythingllm-plugin install ./my-awesome-plugin.zip
# 从 npm 安装
anythingllm-plugin install my-awesome-plugin
# 从 GitHub 安装
anythingllm-plugin install github:username/my-awesome-plugin
插件管理
bash
# 列出已安装的插件
anythingllm-plugin list
# 启用插件
anythingllm-plugin enable my-awesome-plugin
# 禁用插件
anythingllm-plugin disable my-awesome-plugin
# 卸载插件
anythingllm-plugin uninstall my-awesome-plugin
最佳实践
性能优化
- 异步处理: 使用异步操作避免阻塞主线程
- 缓存机制: 实现适当的缓存策略
- 资源管理: 及时释放不需要的资源
- 批量处理: 对大量数据进行批量处理
安全考虑
- 输入验证: 验证所有用户输入
- 权限检查: 实施适当的权限控制
- 数据加密: 加密敏感数据
- 错误处理: 避免泄露敏感信息
代码质量
- 代码规范: 遵循 ESLint 和 Prettier 规则
- 测试覆盖: 保持高测试覆盖率
- 文档完整: 编写完整的 API 文档
- 版本管理: 使用语义化版本控制
插件示例
天气查询插件
javascript
class WeatherPlugin extends BasePlugin {
constructor(config) {
super(config);
this.name = 'weather-plugin';
this.apiKey = config.weatherApiKey;
}
registerEventHandlers() {
this.on('chat.message', this.handleWeatherQuery.bind(this));
}
async handleWeatherQuery(event) {
const { message } = event.data;
if (message.includes('天气')) {
const location = this.extractLocation(message);
const weather = await this.getWeather(location);
return {
response: `${location}的天气: ${weather.description},温度 ${weather.temperature}°C`,
handled: true
};
}
return { handled: false };
}
async getWeather(location) {
const response = await fetch(
`https://api.openweathermap.org/data/2.5/weather?q=${location}&appid=${this.apiKey}&units=metric&lang=zh`
);
const data = await response.json();
return {
description: data.weather[0].description,
temperature: Math.round(data.main.temp)
};
}
extractLocation(message) {
// 简单的位置提取逻辑
const match = message.match(/(.+?)的天气/);
return match ? match[1] : '北京';
}
}
文档摘要插件
javascript
class DocumentSummaryPlugin extends BasePlugin {
constructor(config) {
super(config);
this.name = 'document-summary-plugin';
}
registerEventHandlers() {
this.on('document.processed', this.generateSummary.bind(this));
}
async generateSummary(event) {
const { document } = event.data;
try {
const content = await this.getDocumentContent(document.id);
const summary = await this.createSummary(content);
await this.saveDocumentSummary(document.id, summary);
this.emit('summary.generated', {
documentId: document.id,
summary
});
} catch (error) {
console.error('生成摘要失败:', error);
}
}
async createSummary(content) {
const llm = this.getLLMProvider();
const response = await llm.chat([
{
role: 'system',
content: '请为以下文档生成一个简洁的摘要,突出主要内容和关键点。'
},
{
role: 'user',
content: content.slice(0, 4000) // 限制输入长度
}
]);
return response.content;
}
}
社区和支持
插件市场
- 浏览官方插件市场
- 分享您的插件
- 发现社区贡献的插件
开发者资源
获取帮助
开始创建您的第一个 AnythingLLM 插件,为平台添加独特的功能!