Skip to content

插件开发最佳实践

本文档总结了 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 已更新
  • [ ] 安全漏洞扫描通过
  • [ ] 性能测试通过
  • [ ] 向后兼容性检查

相关链接

Released under the MIT License.