插件开发最佳实践
本文档总结了 XAI-SDK 插件开发的最佳实践,帮助您创建高质量、可维护的插件。
设计原则
1. 单一职责原则
每个插件应该专注于一个特定的功能领域:
typescript
// ✅ 好的做法 - 专注于天气功能
class WeatherPlugin extends BasePlugin {
name = 'weather-plugin'
// 只处理天气相关功能
async getWeather(city: string): Promise<WeatherData> {}
async getForecast(city: string): Promise<ForecastData> {}
}
// ❌ 不好的做法 - 功能过于复杂
class SuperPlugin extends BasePlugin {
name = 'super-plugin'
// 混合了多种不相关的功能
async getWeather(): Promise<any> {}
async sendEmail(): Promise<any> {}
async processImage(): Promise<any> {}
}
2. 开放封闭原则
插件应该对扩展开放,对修改封闭:
typescript
// ✅ 好的做法 - 使用接口和抽象
abstract class BaseDataProvider {
abstract fetchData(query: string): Promise<any>
}
class APIDataProvider extends BaseDataProvider {
async fetchData(query: string): Promise<any> {
// API 实现
}
}
class DatabaseDataProvider extends BaseDataProvider {
async fetchData(query: string): Promise<any> {
// 数据库实现
}
}
3. 依赖倒置原则
依赖抽象而不是具体实现:
typescript
// ✅ 好的做法 - 依赖接口
interface Logger {
log(message: string): void
}
class MyPlugin extends BasePlugin {
constructor(private logger: Logger) {
super()
}
}
// ❌ 不好的做法 - 依赖具体实现
class MyPlugin extends BasePlugin {
constructor() {
super()
this.logger = new ConsoleLogger() // 硬编码依赖
}
}
代码质量
1. 类型安全
使用 TypeScript 提供完整的类型定义:
typescript
// ✅ 好的做法 - 完整的类型定义
interface WeatherConfig {
apiKey: string
baseUrl: string
timeout?: number
retries?: number
}
interface WeatherData {
temperature: number
humidity: number
description: string
timestamp: Date
}
class WeatherPlugin extends BasePlugin<WeatherConfig> {
async getWeather(city: string): Promise<WeatherData> {
// 类型安全的实现
}
}
// ❌ 不好的做法 - 使用 any 类型
class WeatherPlugin extends BasePlugin {
async getWeather(city: any): Promise<any> {
// 缺乏类型安全
}
}
2. 错误处理
实现完善的错误处理机制:
typescript
// ✅ 好的做法 - 完善的错误处理
class WeatherPlugin extends BasePlugin {
async getWeather(city: string): Promise<WeatherData> {
try {
this.validateInput(city)
const data = await this.fetchWeatherData(city)
return this.transformData(data)
} catch (error) {
this.logger.error('Failed to get weather data', {
city,
error: error.message,
stack: error.stack
})
if (error instanceof ValidationError) {
throw new PluginError(
`Invalid city name: ${city}`,
'INVALID_INPUT',
this.name,
error
)
}
throw new PluginError(
'Weather service unavailable',
'SERVICE_ERROR',
this.name,
error
)
}
}
private validateInput(city: string): void {
if (!city || typeof city !== 'string') {
throw new ValidationError('City name is required')
}
}
}
// ❌ 不好的做法 - 忽略错误处理
class WeatherPlugin extends BasePlugin {
async getWeather(city: string): Promise<any> {
const data = await fetch(`/api/weather/${city}`)
return data.json() // 没有错误处理
}
}
3. 配置验证
验证插件配置的有效性:
typescript
// ✅ 好的做法 - 配置验证
import Joi from 'joi'
const configSchema = Joi.object({
apiKey: Joi.string().required(),
baseUrl: Joi.string().uri().required(),
timeout: Joi.number().min(1000).max(60000).default(30000),
retries: Joi.number().min(0).max(5).default(3)
})
class WeatherPlugin extends BasePlugin<WeatherConfig> {
protected async onInitialize(config: WeatherConfig): Promise<void> {
const { error, value } = configSchema.validate(config)
if (error) {
throw new ConfigurationError(
`Invalid configuration: ${error.message}`,
'INVALID_CONFIG',
this.name
)
}
this.config = value
await this.testConnection()
}
private async testConnection(): Promise<void> {
try {
await this.fetchWeatherData('test')
} catch (error) {
throw new ConfigurationError(
'Failed to connect to weather service',
'CONNECTION_ERROR',
this.name,
error
)
}
}
}
性能优化
1. 缓存策略
实现合理的缓存机制:
typescript
// ✅ 好的做法 - 智能缓存
class WeatherPlugin extends BasePlugin {
private cache = new Map<string, CacheEntry>()
private readonly CACHE_TTL = 5 * 60 * 1000 // 5分钟
async getWeather(city: string): Promise<WeatherData> {
const cacheKey = `weather:${city.toLowerCase()}`
const cached = this.cache.get(cacheKey)
if (cached && Date.now() - cached.timestamp < this.CACHE_TTL) {
this.logger.debug('Cache hit', { city })
return cached.data
}
const data = await this.fetchWeatherData(city)
this.cache.set(cacheKey, {
data,
timestamp: Date.now()
})
// 清理过期缓存
this.cleanupCache()
return data
}
private cleanupCache(): void {
const now = Date.now()
for (const [key, entry] of this.cache.entries()) {
if (now - entry.timestamp >= this.CACHE_TTL) {
this.cache.delete(key)
}
}
}
}
interface CacheEntry {
data: WeatherData
timestamp: number
}
2. 资源管理
正确管理资源和连接:
typescript
// ✅ 好的做法 - 资源管理
class DatabasePlugin extends BasePlugin {
private connection: DatabaseConnection | null = null
private connectionPool: ConnectionPool | null = null
protected async onActivate(): Promise<void> {
this.connectionPool = new ConnectionPool({
host: this.config.host,
port: this.config.port,
maxConnections: 10,
idleTimeout: 30000
})
await this.connectionPool.initialize()
this.logger.info('Database connection pool initialized')
}
protected async onDeactivate(): Promise<void> {
if (this.connectionPool) {
await this.connectionPool.close()
this.connectionPool = null
this.logger.info('Database connection pool closed')
}
}
async query(sql: string, params?: any[]): Promise<any[]> {
if (!this.connectionPool) {
throw new Error('Plugin not activated')
}
const connection = await this.connectionPool.acquire()
try {
return await connection.query(sql, params)
} finally {
this.connectionPool.release(connection)
}
}
}
3. 异步操作优化
合理处理异步操作:
typescript
// ✅ 好的做法 - 并发控制
class BatchProcessingPlugin extends BasePlugin {
private readonly MAX_CONCURRENT = 5
async processBatch(items: string[]): Promise<ProcessResult[]> {
const results: ProcessResult[] = []
// 分批处理,控制并发数
for (let i = 0; i < items.length; i += this.MAX_CONCURRENT) {
const batch = items.slice(i, i + this.MAX_CONCURRENT)
const batchPromises = batch.map(item => this.processItem(item))
const batchResults = await Promise.allSettled(batchPromises)
for (const result of batchResults) {
if (result.status === 'fulfilled') {
results.push(result.value)
} else {
this.logger.error('Item processing failed', result.reason)
results.push({
success: false,
error: result.reason.message
})
}
}
}
return results
}
private async processItem(item: string): Promise<ProcessResult> {
// 添加超时控制
return Promise.race([
this.doProcessItem(item),
this.timeout(30000) // 30秒超时
])
}
private timeout(ms: number): Promise<never> {
return new Promise((_, reject) => {
setTimeout(() => reject(new Error('Operation timeout')), ms)
})
}
}
安全性
1. 输入验证
严格验证所有输入:
typescript
// ✅ 好的做法 - 输入验证
class FileProcessorPlugin extends BasePlugin {
private readonly ALLOWED_EXTENSIONS = ['.txt', '.json', '.csv']
private readonly MAX_FILE_SIZE = 10 * 1024 * 1024 // 10MB
async processFile(filePath: string): Promise<ProcessResult> {
// 验证文件路径
this.validateFilePath(filePath)
// 验证文件扩展名
this.validateFileExtension(filePath)
// 验证文件大小
await this.validateFileSize(filePath)
// 处理文件
return this.doProcessFile(filePath)
}
private validateFilePath(filePath: string): void {
if (!filePath || typeof filePath !== 'string') {
throw new ValidationError('File path is required')
}
// 防止路径遍历攻击
if (filePath.includes('..') || filePath.includes('~')) {
throw new ValidationError('Invalid file path')
}
// 确保路径在允许的目录内
const allowedDir = path.resolve(this.config.workingDirectory)
const resolvedPath = path.resolve(filePath)
if (!resolvedPath.startsWith(allowedDir)) {
throw new ValidationError('File path outside allowed directory')
}
}
private validateFileExtension(filePath: string): void {
const ext = path.extname(filePath).toLowerCase()
if (!this.ALLOWED_EXTENSIONS.includes(ext)) {
throw new ValidationError(`Unsupported file type: ${ext}`)
}
}
private async validateFileSize(filePath: string): Promise<void> {
const stats = await fs.stat(filePath)
if (stats.size > this.MAX_FILE_SIZE) {
throw new ValidationError('File too large')
}
}
}
2. 权限控制
实现细粒度的权限控制:
typescript
// ✅ 好的做法 - 权限控制
class AdminPlugin extends BasePlugin {
async executeCommand(command: string, user: User): Promise<CommandResult> {
// 检查用户权限
await this.checkPermission(user, 'admin:execute')
// 验证命令安全性
this.validateCommand(command)
// 记录操作日志
this.auditLog(user, command)
return this.doExecuteCommand(command)
}
private async checkPermission(user: User, permission: string): Promise<void> {
if (!user.permissions.includes(permission)) {
throw new PermissionError(
`User ${user.id} lacks permission: ${permission}`
)
}
}
private validateCommand(command: string): void {
const dangerousCommands = ['rm', 'del', 'format', 'shutdown']
const lowerCommand = command.toLowerCase()
for (const dangerous of dangerousCommands) {
if (lowerCommand.includes(dangerous)) {
throw new SecurityError(`Dangerous command detected: ${dangerous}`)
}
}
}
private auditLog(user: User, command: string): void {
this.logger.info('Admin command executed', {
userId: user.id,
command,
timestamp: new Date(),
ip: user.ip
})
}
}
测试策略
1. 单元测试
编写全面的单元测试:
typescript
// ✅ 好的做法 - 完整的单元测试
describe('WeatherPlugin', () => {
let plugin: WeatherPlugin
let mockApiClient: jest.Mocked<ApiClient>
beforeEach(() => {
mockApiClient = createMockApiClient()
plugin = new WeatherPlugin(mockApiClient)
})
afterEach(async () => {
await plugin.destroy()
})
describe('getWeather', () => {
it('should return weather data for valid city', async () => {
// Arrange
const city = 'Beijing'
const expectedData = {
temperature: 25,
humidity: 60,
description: 'Sunny'
}
mockApiClient.get.mockResolvedValue(expectedData)
// Act
const result = await plugin.getWeather(city)
// Assert
expect(result).toEqual(expectedData)
expect(mockApiClient.get).toHaveBeenCalledWith(`/weather/${city}`)
})
it('should throw error for invalid city', async () => {
// Arrange
const invalidCity = ''
// Act & Assert
await expect(plugin.getWeather(invalidCity))
.rejects
.toThrow('City name is required')
})
it('should handle API errors gracefully', async () => {
// Arrange
const city = 'Beijing'
mockApiClient.get.mockRejectedValue(new Error('API Error'))
// Act & Assert
await expect(plugin.getWeather(city))
.rejects
.toThrow('Weather service unavailable')
})
})
describe('caching', () => {
it('should cache weather data', async () => {
// Arrange
const city = 'Beijing'
const weatherData = { temperature: 25 }
mockApiClient.get.mockResolvedValue(weatherData)
// Act
await plugin.getWeather(city)
await plugin.getWeather(city) // Second call should use cache
// Assert
expect(mockApiClient.get).toHaveBeenCalledTimes(1)
})
})
})
2. 集成测试
测试插件与 SDK 的集成:
typescript
// ✅ 好的做法 - 集成测试
describe('WeatherPlugin Integration', () => {
let sdk: XAI
let plugin: WeatherPlugin
beforeEach(async () => {
sdk = new XAI()
plugin = new WeatherPlugin()
await sdk.use(plugin, {
apiKey: 'test-key',
baseUrl: 'https://api.weather.com'
})
})
afterEach(async () => {
await sdk.destroy()
})
it('should integrate with SDK chat system', async () => {
const response = await sdk.chat({
messages: [{
role: 'user',
content: 'What is the weather in Beijing?'
}],
tools: [weatherTool]
})
expect(response.message.toolCalls).toBeDefined()
expect(response.message.toolCalls[0].function.name).toBe('get-weather')
})
})
文档和维护
1. 代码文档
编写清晰的代码文档:
typescript
/**
* WeatherPlugin 提供天气数据查询功能
*
* @example
* ```typescript
* const plugin = new WeatherPlugin()
* await sdk.use(plugin, {
* apiKey: 'your-api-key',
* baseUrl: 'https://api.weather.com'
* })
*
* const weather = await plugin.getWeather('Beijing')
* console.log(weather.temperature)
* ```
*/
export class WeatherPlugin extends BasePlugin<WeatherConfig> {
/**
* 获取指定城市的天气信息
*
* @param city - 城市名称
* @returns Promise<WeatherData> 天气数据
*
* @throws {ValidationError} 当城市名称无效时
* @throws {PluginError} 当天气服务不可用时
*
* @example
* ```typescript
* const weather = await plugin.getWeather('Beijing')
* console.log(`Temperature: ${weather.temperature}°C`)
* ```
*/
async getWeather(city: string): Promise<WeatherData> {
// 实现代码
}
}
2. 版本管理
遵循语义化版本控制:
json
{
"name": "xai-sdk-plugin-weather",
"version": "1.2.3",
"description": "Weather data plugin for XAI-SDK",
"changelog": {
"1.2.3": {
"date": "2024-01-15",
"changes": [
"Fixed: Cache cleanup memory leak",
"Improved: Error handling for network timeouts"
]
},
"1.2.0": {
"date": "2024-01-01",
"changes": [
"Added: Forecast data support",
"Added: Multiple city batch queries"
]
}
}
}
3. 监控和日志
实现完善的监控和日志:
typescript
class WeatherPlugin extends BasePlugin {
private metrics = {
requestCount: 0,
errorCount: 0,
cacheHitRate: 0,
averageResponseTime: 0
}
async getWeather(city: string): Promise<WeatherData> {
const startTime = Date.now()
this.metrics.requestCount++
try {
const result = await this.doGetWeather(city)
// 记录成功指标
const responseTime = Date.now() - startTime
this.updateMetrics(responseTime, true)
this.logger.info('Weather data retrieved', {
city,
responseTime,
cacheHit: result.fromCache
})
return result.data
} catch (error) {
this.metrics.errorCount++
this.logger.error('Failed to get weather data', {
city,
error: error.message,
responseTime: Date.now() - startTime
})
throw error
}
}
private updateMetrics(responseTime: number, success: boolean): void {
// 更新平均响应时间
this.metrics.averageResponseTime =
(this.metrics.averageResponseTime + responseTime) / 2
// 定期报告指标
if (this.metrics.requestCount % 100 === 0) {
this.reportMetrics()
}
}
private reportMetrics(): void {
this.logger.info('Plugin metrics', this.metrics)
}
}
部署和分发
1. 构建配置
配置合适的构建流程:
json
{
"scripts": {
"build": "tsc && npm run bundle",
"bundle": "rollup -c rollup.config.js",
"test": "jest",
"test:coverage": "jest --coverage",
"lint": "eslint src/**/*.ts",
"prepublishOnly": "npm run test && npm run build"
},
"files": [
"dist/",
"README.md",
"LICENSE"
]
}
2. 发布检查清单
发布前的检查清单:
- [ ] 所有测试通过
- [ ] 代码覆盖率 > 80%
- [ ] 文档完整且最新
- [ ] 版本号正确更新
- [ ] CHANGELOG 已更新
- [ ] 安全漏洞扫描通过
- [ ] 性能测试通过
- [ ] 向后兼容性检查