Skip to content

Eagle 插件架构设计

概述

Eagle2Ae-Eagle 是基于 Eagle 插件 API 开发的服务型插件,采用模块化架构设计,实现了文件选择、数据传输、WebSocket 通信和后台服务等核心功能。插件以服务模式运行,提供持续的后台服务和用户交互界面。

整体架构

架构图

┌─────────────────────────────────────────────────────────────┐
│                        Eagle 应用                          │
│  ┌─────────────────────────────────────────────────────┐    │
│  │              Eagle2Ae 插件                          │    │
│  │                                                     │    │
│  │  ┌─────────────┐    ┌─────────────┐    ┌─────────┐  │    │
│  │  │   管理界面   │    │   服务核心   │    │ 配置管理 │  │    │
│  │  │ (HTML/JS)   │◄──►│ (plugin.js) │◄──►│ (JSON)  │  │    │
│  │  └─────────────┘    └─────────────┘    └─────────┘  │    │
│  │         │                   │                       │    │
│  │         ▼                   ▼                       │    │
│  │  ┌─────────────────────────────────────────────────┐  │    │
│  │  │              Eagle API 接口层                  │  │    │
│  │  └─────────────────────────────────────────────────┘  │    │
│  │                           │                          │    │
│  │                           ▼                          │    │
│  │  ┌─────────────────────────────────────────────────┐  │    │
│  │  │            WebSocket 服务器模块                 │  │    │
│  │  │  ┌─────────────┐  ┌─────────────┐  ┌─────────┐  │  │    │
│  │  │  │ 连接管理器   │  │ 消息路由器   │  │ 端口分配 │  │  │    │
│  │  │  └─────────────┘  └─────────────┘  └─────────┘  │  │    │
│  │  └─────────────────────────────────────────────────┘  │    │
│  │                           │                          │    │
│  │                           ▼                          │    │
│  │  ┌─────────────────────────────────────────────────┐  │    │
│  │  │              数据处理模块                       │  │    │
│  │  │  ┌─────────────┐  ┌─────────────┐  ┌─────────┐  │  │    │
│  │  │  │ 文件读取器   │  │ 数据库访问   │  │ 剪贴板   │  │  │    │
│  │  │  └─────────────┘  └─────────────┘  └─────────┘  │  │    │
│  │  └─────────────────────────────────────────────────┘  │    │
│  └─────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────┘

                              ▼ (WebSocket)
┌─────────────────────────────────────────────────────────────┐
│                   After Effects CEP 扩展                   │
└─────────────────────────────────────────────────────────────┘

核心组件

1. 插件主体 (Plugin Core)

  • 技术栈: JavaScript ES6+, Eagle Plugin API
  • 职责: 插件生命周期管理、用户交互处理
  • 特点: 服务模式运行、事件驱动架构
  • 文件位置: js/plugin.js

2. WebSocket 服务器 (WebSocket Server)

  • 技术栈: Node.js ws 库
  • 职责: 与 AE 扩展建立通信、消息传输
  • 特点: 动态端口分配、自动重启、连接管理
  • 文件位置: js/websocket-server.js

3. 数据处理模块 (Data Processing)

  • 技术栈: fs-extra, SQLite 读取
  • 职责: 文件信息收集、数据库查询、数据格式化
  • 特点: 异步处理、错误恢复、数据验证
  • 文件位置: js/database/, js/utils/

4. 剪贴板集成 (Clipboard Integration)

  • 技术栈: @crosscopy/clipboard
  • 职责: 文件路径读写、数据交换
  • 特点: 跨平台兼容、格式转换、错误处理
  • 文件位置: js/clipboard/

5. 通信协议模块 (Communication Protocol)

  • 技术栈: WebSocket, JSON
  • 职责: 消息格式化、协议兼容性处理
  • 特点: 标准化消息格式、版本兼容
  • 文件位置: js/websocket-protocol.js, js/websocket-eagle-compatible.js

6. 端口管理模块 (Port Management)

  • 技术栈: Node.js net 模块
  • 职责: 动态端口分配、端口冲突检测
  • 特点: 自动端口发现、冲突避免
  • 文件位置: js/dynamic-port-allocator.js

模块详细设计

插件主体模块

生命周期管理

javascript
/**
 * 插件主类
 */
class Eagle2AePlugin {
    constructor() {
        this.isServiceMode = true;
        this.server = null;
        this.config = null;
        this.status = 'stopped';
    }

    /**
     * 插件初始化
     */
    async initialize() {
        try {
            // 加载配置
            this.config = await this.loadConfig();
            
            // 初始化 WebSocket 服务器
            this.server = new WebSocketServer(this.config.port);
            
            // 注册事件监听器
            this.registerEventListeners();
            
            // 启动服务
            await this.startService();
            
            this.status = 'running';
            this.log('插件初始化完成');
        } catch (error) {
            this.handleError('初始化失败', error);
        }
    }

    /**
     * 插件销毁
     */
    async destroy() {
        try {
            // 停止服务
            await this.stopService();
            
            // 清理资源
            this.cleanup();
            
            this.status = 'stopped';
            this.log('插件已停止');
        } catch (error) {
            this.handleError('停止失败', error);
        }
    }
}

事件系统

javascript
/**
 * 事件管理器
 */
class EventManager {
    constructor() {
        this.listeners = new Map();
    }

    /**
     * 注册事件监听器
     */
    on(event, callback) {
        if (!this.listeners.has(event)) {
            this.listeners.set(event, []);
        }
        this.listeners.get(event).push(callback);
    }

    /**
     * 触发事件
     */
    emit(event, data) {
        const callbacks = this.listeners.get(event);
        if (callbacks) {
            callbacks.forEach(callback => {
                try {
                    callback(data);
                } catch (error) {
                    console.error(`事件处理错误 [${event}]:`, error);
                }
            });
        }
    }
}

WebSocket 服务器模块

服务器架构

javascript
/**
 * WebSocket 服务器
 */
class WebSocketServer {
    constructor(options = {}) {
        this.options = {
            port: 8080,
            host: '127.0.0.1',
            maxConnections: 5,
            heartbeatInterval: 30000,
            ...options
        };
        
        this.server = null;
        this.clients = new Map();
        this.isRunning = false;
    }

    /**
     * 启动服务器
     */
    async start() {
        return new Promise((resolve, reject) => {
            try {
                const WebSocket = require('ws');
                
                // 创建 WebSocket 服务器
                this.server = new WebSocket.Server({
                    port: this.options.port,
                    host: this.options.host
                });
                
                // 监听连接事件
                this.server.on('connection', (ws, req) => {
                    this.handleConnection(ws, req);
                });
                
                // 监听服务器事件
                this.server.on('listening', () => {
                    this.isRunning = true;
                    this.log(`WebSocket 服务器启动: ${this.options.host}:${this.options.port}`);
                    resolve();
                });
                
                this.server.on('error', (error) => {
                    this.handleServerError(error);
                    reject(error);
                });
                
            } catch (error) {
                reject(error);
            }
        });
    }

    /**
     * 处理客户端连接
     */
    handleConnection(ws, req) {
        const clientId = this.generateClientId();
        const clientInfo = {
            id: clientId,
            ws: ws,
            ip: req.socket.remoteAddress,
            connected: Date.now(),
            lastActivity: Date.now()
        };
        
        // 存储客户端信息
        this.clients.set(clientId, clientInfo);
        
        // 设置事件监听器
        ws.on('message', (data) => {
            this.handleMessage(clientId, data);
        });
        
        ws.on('close', (code, reason) => {
            this.handleDisconnection(clientId, code, reason);
        });
        
        ws.on('error', (error) => {
            this.handleClientError(clientId, error);
        });
        
        // 发送欢迎消息
        this.sendWelcomeMessage(clientId);
        
        this.log(`客户端连接: ${clientId} (${clientInfo.ip})`);
    }
}

动态端口分配

javascript
/**
 * 动态端口分配器
 */
class PortAllocator {
    constructor(startPort = 8080, endPort = 8090) {
        this.startPort = startPort;
        this.endPort = endPort;
        this.usedPorts = new Set();
    }

    /**
     * 查找可用端口
     */
    async findAvailablePort() {
        for (let port = this.startPort; port <= this.endPort; port++) {
            if (!this.usedPorts.has(port) && await this.isPortAvailable(port)) {
                this.usedPorts.add(port);
                return port;
            }
        }
        throw new Error('没有可用端口');
    }

    /**
     * 检查端口是否可用
     */
    async isPortAvailable(port) {
        return new Promise((resolve) => {
            const net = require('net');
            const server = net.createServer();
            
            server.listen(port, '127.0.0.1', () => {
                server.close(() => resolve(true));
            });
            
            server.on('error', () => resolve(false));
        });
    }

    /**
     * 释放端口
     */
    releasePort(port) {
        this.usedPorts.delete(port);
    }
}

数据处理模块

文件信息收集器

javascript
/**
 * 文件信息收集器
 */
class FileInfoCollector {
    constructor() {
        this.fs = require('fs-extra');
        this.path = require('path');
    }

    /**
     * 收集文件信息
     */
    async collectFileInfo(filePaths) {
        const fileInfos = [];
        
        for (const filePath of filePaths) {
            try {
                const info = await this.getFileInfo(filePath);
                if (info) {
                    fileInfos.push(info);
                }
            } catch (error) {
                console.error(`获取文件信息失败: ${filePath}`, error);
            }
        }
        
        return fileInfos;
    }

    /**
     * 获取单个文件信息
     */
    async getFileInfo(filePath) {
        try {
            const stats = await this.fs.stat(filePath);
            const parsed = this.path.parse(filePath);
            
            return {
                path: filePath,
                name: parsed.name,
                ext: parsed.ext,
                size: stats.size,
                mtime: stats.mtime,
                type: this.getFileType(parsed.ext),
                metadata: await this.getFileMetadata(filePath)
            };
        } catch (error) {
            console.error(`读取文件信息失败: ${filePath}`, error);
            return null;
        }
    }

    /**
     * 获取文件类型
     */
    getFileType(ext) {
        const imageExts = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.webp'];
        const videoExts = ['.mp4', '.avi', '.mov', '.mkv', '.wmv', '.flv'];
        const audioExts = ['.mp3', '.wav', '.aac', '.flac', '.ogg'];
        
        ext = ext.toLowerCase();
        
        if (imageExts.includes(ext)) return 'image';
        if (videoExts.includes(ext)) return 'video';
        if (audioExts.includes(ext)) return 'audio';
        
        return 'other';
    }
}

Eagle 数据库访问器

javascript
/**
 * Eagle 数据库访问器
 */
class EagleDatabase {
    constructor(dbPath) {
        this.dbPath = dbPath;
        this.db = null;
    }

    /**
     * 连接数据库
     */
    async connect() {
        try {
            const sqlite3 = require('sqlite3');
            
            return new Promise((resolve, reject) => {
                this.db = new sqlite3.Database(this.dbPath, sqlite3.OPEN_READONLY, (err) => {
                    if (err) {
                        reject(err);
                    } else {
                        resolve();
                    }
                });
            });
        } catch (error) {
            throw new Error(`数据库连接失败: ${error.message}`);
        }
    }

    /**
     * 查询文件信息
     */
    async queryFileInfo(filePath) {
        return new Promise((resolve, reject) => {
            const sql = `
                SELECT 
                    id, name, size, ext, tags, 
                    width, height, duration,
                    palettes, annotation
                FROM items 
                WHERE filePath = ?
            `;
            
            this.db.get(sql, [filePath], (err, row) => {
                if (err) {
                    reject(err);
                } else {
                    resolve(row);
                }
            });
        });
    }

    /**
     * 查询文件夹信息
     */
    async queryFolderInfo(folderId) {
        return new Promise((resolve, reject) => {
            const sql = `
                SELECT id, name, description, iconColor
                FROM folders 
                WHERE id = ?
            `;
            
            this.db.get(sql, [folderId], (err, row) => {
                if (err) {
                    reject(err);
                } else {
                    resolve(row);
                }
            });
        });
    }

    /**
     * 关闭数据库连接
     */
    async close() {
        if (this.db) {
            return new Promise((resolve) => {
                this.db.close((err) => {
                    if (err) {
                        console.error('关闭数据库失败:', err);
                    }
                    resolve();
                });
            });
        }
    }
}

剪贴板集成模块

剪贴板处理器

javascript
/**
 * 剪贴板处理器
 */
class ClipboardHandler {
    constructor() {
        this.clipboard = require('@crosscopy/clipboard');
    }

    /**
     * 读取剪贴板文件路径
     */
    async readFilePaths() {
        try {
            // 读取剪贴板内容
            const clipboardData = await this.clipboard.read();
            
            // 解析文件路径
            const filePaths = this.parseFilePaths(clipboardData);
            
            return filePaths;
        } catch (error) {
            console.error('读取剪贴板失败:', error);
            return [];
        }
    }

    /**
     * 写入文件路径到剪贴板
     */
    async writeFilePaths(filePaths) {
        try {
            // 格式化文件路径
            const formattedPaths = this.formatFilePaths(filePaths);
            
            // 写入剪贴板
            await this.clipboard.write(formattedPaths);
            
            return true;
        } catch (error) {
            console.error('写入剪贴板失败:', error);
            return false;
        }
    }

    /**
     * 解析剪贴板中的文件路径
     */
    parseFilePaths(clipboardData) {
        const filePaths = [];
        
        if (clipboardData.text) {
            // 处理文本格式的路径
            const lines = clipboardData.text.split('\n');
            for (const line of lines) {
                const trimmed = line.trim();
                if (trimmed && this.isValidFilePath(trimmed)) {
                    filePaths.push(trimmed);
                }
            }
        }
        
        if (clipboardData.files) {
            // 处理文件对象格式
            for (const file of clipboardData.files) {
                if (file.path && this.isValidFilePath(file.path)) {
                    filePaths.push(file.path);
                }
            }
        }
        
        return filePaths;
    }

    /**
     * 验证文件路径有效性
     */
    isValidFilePath(path) {
        const fs = require('fs');
        try {
            return fs.existsSync(path) && fs.statSync(path).isFile();
        } catch {
            return false;
        }
    }
}

数据流设计

文件选择和传输流程

mermaid
sequenceDiagram
    participant U as 用户
    participant E as Eagle界面
    participant P as 插件核心
    participant W as WebSocket服务器
    participant D as 数据处理器
    participant A as AE扩展
    
    U->>E: 选择文件
    E->>P: 触发导出事件
    P->>D: 收集文件信息
    D->>D: 读取文件属性
    D->>D: 查询Eagle数据库
    D->>P: 返回文件信息
    P->>W: 准备传输数据
    W->>A: 发送文件信息
    A->>W: 确认接收
    W->>P: 传输完成
    P->>E: 更新状态
    E->>U: 显示结果

服务启动流程

mermaid
flowchart TD
    A[插件加载] --> B[读取配置]
    B --> C[初始化组件]
    C --> D[分配端口]
    D --> E{端口可用?}
    E -->|否| F[尝试下一个端口]
    F --> E
    E -->|是| G[启动WebSocket服务器]
    G --> H{启动成功?}
    H -->|否| I[记录错误并重试]
    I --> D
    H -->|是| J[注册事件监听器]
    J --> K[显示管理界面]
    K --> L[服务就绪]

配置管理

配置文件结构

javascript
/**
 * 插件配置
 */
const defaultConfig = {
    // 服务器配置
    server: {
        host: '127.0.0.1',
        portRange: {
            start: 8080,
            end: 8090
        },
        maxConnections: 5,
        heartbeatInterval: 30000
    },
    
    // 文件处理配置
    fileProcessing: {
        maxFileSize: 100 * 1024 * 1024, // 100MB
        supportedTypes: ['image', 'video', 'audio'],
        batchSize: 10
    },
    
    // 日志配置
    logging: {
        level: 'info', // debug, info, warn, error
        maxLogFiles: 5,
        maxLogSize: 10 * 1024 * 1024 // 10MB
    },
    
    // UI 配置
    ui: {
        theme: 'auto', // light, dark, auto
        language: 'zh-CN',
        showNotifications: true
    }
};

配置管理器

javascript
/**
 * 配置管理器
 */
class ConfigManager {
    constructor(configPath) {
        this.configPath = configPath;
        this.config = null;
    }

    /**
     * 加载配置
     */
    async load() {
        try {
            const fs = require('fs-extra');
            
            if (await fs.pathExists(this.configPath)) {
                const configData = await fs.readJson(this.configPath);
                this.config = { ...defaultConfig, ...configData };
            } else {
                this.config = { ...defaultConfig };
                await this.save();
            }
            
            return this.config;
        } catch (error) {
            console.error('加载配置失败:', error);
            this.config = { ...defaultConfig };
            return this.config;
        }
    }

    /**
     * 保存配置
     */
    async save() {
        try {
            const fs = require('fs-extra');
            await fs.ensureDir(require('path').dirname(this.configPath));
            await fs.writeJson(this.configPath, this.config, { spaces: 2 });
            return true;
        } catch (error) {
            console.error('保存配置失败:', error);
            return false;
        }
    }

    /**
     * 获取配置值
     */
    get(key, defaultValue = null) {
        const keys = key.split('.');
        let value = this.config;
        
        for (const k of keys) {
            if (value && typeof value === 'object' && k in value) {
                value = value[k];
            } else {
                return defaultValue;
            }
        }
        
        return value;
    }

    /**
     * 设置配置值
     */
    set(key, value) {
        const keys = key.split('.');
        let target = this.config;
        
        for (let i = 0; i < keys.length - 1; i++) {
            const k = keys[i];
            if (!(k in target) || typeof target[k] !== 'object') {
                target[k] = {};
            }
            target = target[k];
        }
        
        target[keys[keys.length - 1]] = value;
    }
}

错误处理和恢复

错误分类

javascript
/**
 * 错误类型定义
 */
const ErrorTypes = {
    // 网络错误
    NETWORK_ERROR: 'network_error',
    CONNECTION_FAILED: 'connection_failed',
    PORT_IN_USE: 'port_in_use',
    
    // 文件错误
    FILE_NOT_FOUND: 'file_not_found',
    FILE_ACCESS_DENIED: 'file_access_denied',
    INVALID_FILE_TYPE: 'invalid_file_type',
    
    // 数据库错误
    DATABASE_ERROR: 'database_error',
    QUERY_FAILED: 'query_failed',
    
    // 系统错误
    INSUFFICIENT_MEMORY: 'insufficient_memory',
    PERMISSION_DENIED: 'permission_denied',
    UNKNOWN_ERROR: 'unknown_error'
};

错误处理器

javascript
/**
 * 错误处理器
 */
class ErrorHandler {
    constructor() {
        this.errorCounts = new Map();
        this.maxRetries = 3;
    }

    /**
     * 处理错误
     */
    handleError(error, context = {}) {
        const errorInfo = {
            type: this.classifyError(error),
            message: error.message,
            stack: error.stack,
            context: context,
            timestamp: Date.now()
        };
        
        // 记录错误
        this.logError(errorInfo);
        
        // 尝试恢复
        this.attemptRecovery(errorInfo);
        
        // 通知用户
        this.notifyUser(errorInfo);
    }

    /**
     * 错误分类
     */
    classifyError(error) {
        if (error.code === 'EADDRINUSE') {
            return ErrorTypes.PORT_IN_USE;
        }
        if (error.code === 'ENOENT') {
            return ErrorTypes.FILE_NOT_FOUND;
        }
        if (error.code === 'EACCES') {
            return ErrorTypes.PERMISSION_DENIED;
        }
        
        return ErrorTypes.UNKNOWN_ERROR;
    }

    /**
     * 尝试错误恢复
     */
    attemptRecovery(errorInfo) {
        const errorType = errorInfo.type;
        const retryCount = this.errorCounts.get(errorType) || 0;
        
        if (retryCount < this.maxRetries) {
            this.errorCounts.set(errorType, retryCount + 1);
            
            switch (errorType) {
                case ErrorTypes.PORT_IN_USE:
                    this.recoverPortError();
                    break;
                case ErrorTypes.CONNECTION_FAILED:
                    this.recoverConnectionError();
                    break;
                case ErrorTypes.DATABASE_ERROR:
                    this.recoverDatabaseError();
                    break;
            }
        }
    }
}

性能优化

内存管理

javascript
/**
 * 内存管理器
 */
class MemoryManager {
    constructor() {
        this.memoryThreshold = 100 * 1024 * 1024; // 100MB
        this.checkInterval = 30000; // 30秒
        this.timer = null;
    }

    /**
     * 开始内存监控
     */
    startMonitoring() {
        this.timer = setInterval(() => {
            this.checkMemoryUsage();
        }, this.checkInterval);
    }

    /**
     * 检查内存使用情况
     */
    checkMemoryUsage() {
        const memUsage = process.memoryUsage();
        
        if (memUsage.heapUsed > this.memoryThreshold) {
            console.warn('内存使用过高:', {
                heapUsed: Math.round(memUsage.heapUsed / 1024 / 1024) + 'MB',
                heapTotal: Math.round(memUsage.heapTotal / 1024 / 1024) + 'MB'
            });
            
            // 触发垃圾回收
            if (global.gc) {
                global.gc();
            }
        }
    }

    /**
     * 停止监控
     */
    stopMonitoring() {
        if (this.timer) {
            clearInterval(this.timer);
            this.timer = null;
        }
    }
}

连接池管理

javascript
/**
 * 连接池管理器
 */
class ConnectionPool {
    constructor(maxConnections = 5) {
        this.maxConnections = maxConnections;
        this.activeConnections = new Map();
        this.waitingQueue = [];
    }

    /**
     * 获取连接
     */
    async getConnection() {
        if (this.activeConnections.size < this.maxConnections) {
            const connection = await this.createConnection();
            this.activeConnections.set(connection.id, connection);
            return connection;
        } else {
            return new Promise((resolve) => {
                this.waitingQueue.push(resolve);
            });
        }
    }

    /**
     * 释放连接
     */
    releaseConnection(connectionId) {
        this.activeConnections.delete(connectionId);
        
        if (this.waitingQueue.length > 0) {
            const resolve = this.waitingQueue.shift();
            this.getConnection().then(resolve);
        }
    }
}

更新记录

日期版本更新内容作者
2024-01-051.0初始架构设计文档开发团队

相关文档:

Released under the MIT License.