Files
wehub-resource-sync 41b710f9c7
CI / Frontend checks (push) Failing after 0s
CI / Backend tests (push) Failing after 1s
I18n Documentation Sync / sync-docs (push) Failing after 0s
chore: import upstream snapshot with attribution
2026-07-13 12:28:40 +08:00

22 KiB

👨‍💻 AutoClip Desktop 开发者指南

📋 目录

🏗️ 项目架构

整体架构

AutoClip Desktop
├── Frontend (React + TypeScript + Ant Design)
├── Backend (Python + FastAPI + Celery)
├── Tauri (Rust + WebView)
└── Resources (FFmpeg + 模型文件)

技术栈

  • 前端: React 18, TypeScript, Ant Design, Vite
  • 后端: Python 3.9+, FastAPI, Celery, SQLAlchemy
  • 桌面: Tauri 2.0, Rust
  • 数据库: SQLite (桌面模式)
  • 任务队列: Celery with SQLite transport
  • AI服务: OpenAI, DashScope, Google Gemini
  • 语音识别: Whisper, 云端API

🛠️ 开发环境设置

系统要求

  • Node.js: 18.0+
  • Python: 3.9+
  • Rust: 1.70+
  • Git: 2.0+

安装步骤

  1. 克隆项目
git clone https://github.com/your-org/autoclip-desktop.git
cd autoclip-desktop
  1. 安装前端依赖
cd frontend
npm install
  1. 安装后端依赖
cd ../backend
pip install -r requirements.txt
  1. 安装Tauri依赖
cd ../src-tauri
cargo install tauri-cli
  1. 设置环境变量
# 复制环境变量模板
cp .env.example .env

# 编辑环境变量
nano .env

开发环境配置

# 启动开发环境
npm run dev

# 启动后端服务
python backend/desktop_main.py

# 构建Tauri应用
npm run tauri build

📁 代码结构

前端结构

frontend/
├── src/
│   ├── components/          # 通用组件
│   │   ├── ErrorBoundary.tsx
│   │   ├── OfflineIndicator.tsx
│   │   └── ...
│   ├── pages/              # 页面组件
│   │   ├── HomePage.tsx
│   │   ├── SettingsPage.tsx
│   │   ├── OnboardingPage.tsx
│   │   └── ...
│   ├── hooks/              # 自定义Hooks
│   │   ├── useFirstRun.ts
│   │   ├── useDesktopConfig.ts
│   │   └── ...
│   ├── services/           # API服务
│   │   ├── api.ts
│   │   ├── projectApi.ts
│   │   └── ...
│   ├── store/              # 状态管理
│   │   ├── useProjectStore.ts
│   │   ├── useConfigStore.ts
│   │   └── ...
│   ├── utils/              # 工具函数
│   │   ├── apiUtils.ts
│   │   ├── errorHandler.ts
│   │   └── ...
│   └── types/              # 类型定义
│       ├── api.ts
│       ├── project.ts
│       └── ...
├── public/                 # 静态资源
└── package.json

后端结构

backend/
├── api/                    # API路由
│   └── v1/
│       ├── projects.py
│       ├── settings.py
│       ├── health.py
│       └── ...
├── core/                   # 核心模块
│   ├── desktop_config.py
│   ├── llm_providers.py
│   ├── speech_recognition.py
│   └── ...
├── services/               # 业务服务
│   ├── project_service.py
│   ├── video_service.py
│   ├── ai_service.py
│   └── ...
├── models/                 # 数据模型
│   ├── project.py
│   ├── clip.py
│   └── ...
├── utils/                  # 工具模块
│   ├── error_handler.py
│   ├── performance_config.py
│   ├── chunked_upload.py
│   └── ...
├── tasks/                  # Celery任务
│   ├── __init__.py
│   ├── video_tasks.py
│   └── ...
├── desktop_main.py         # 桌面应用入口
├── desktop_celery.py       # 桌面Celery配置
└── requirements.txt

Tauri结构

src-tauri/
├── src/
│   ├── lib.rs              # 主入口
│   ├── commands.rs         # Tauri命令
│   ├── backend_manager.rs  # 后端管理
│   └── tray.rs             # 系统托盘
├── Cargo.toml              # Rust依赖
├── tauri.conf.json         # Tauri配置
└── resources/              # 资源文件
    └── ffmpeg/             # FFmpeg二进制

🔧 核心模块

1. 桌面配置管理

文件: backend/core/desktop_config.py

class DesktopConfig:
    """桌面应用配置管理"""
    
    def __init__(self):
        self.data_dir = self._get_desktop_data_dir()
        self.config_file = self.data_dir / "config.json"
        self.settings_file = self.data_dir / "settings.json"
    
    def save_desktop_config(self, config: 'DesktopConfig') -> bool:
        """保存桌面配置"""
        try:
            config_dict = config.dict()
            # 递归转换Path对象为字符串
            config_dict = self._convert_paths_to_strings(config_dict)
            
            with open(self.config_file, 'w', encoding='utf-8') as f:
                json.dump(config_dict, f, ensure_ascii=False, indent=2)
            
            return True
        except Exception as e:
            logger.error(f"保存桌面配置失败: {e}")
            return False

2. 语音识别服务

文件: backend/core/speech_recognition.py

class SpeechRecognizer:
    """语音识别服务"""
    
    def __init__(self, config: SpeechRecognitionConfig):
        self.config = config
        self.recognizer = self._create_recognizer()
    
    def transcribe_audio(self, audio_path: str) -> str:
        """转写音频文件"""
        if self.config.provider == "whisper_local":
            return self._transcribe_with_whisper(audio_path)
        elif self.config.provider == "openai":
            return self._transcribe_with_openai(audio_path)
        # ... 其他服务

3. 性能优化

文件: backend/utils/performance_config.py

class PerformanceConfig:
    """性能配置管理"""
    
    def __init__(self, level: str = "medium"):
        self.level = level
        self.settings = self._get_settings_for_level(level)
    
    def _get_settings_for_level(self, level: str) -> PerformanceSettings:
        """根据级别获取性能设置"""
        levels = {
            "low": PerformanceSettings(
                max_concurrent_tasks=1,
                chunk_size_mb=5,
                upload_timeout_seconds=300
            ),
            "medium": PerformanceSettings(
                max_concurrent_tasks=2,
                chunk_size_mb=10,
                upload_timeout_seconds=600
            ),
            "high": PerformanceSettings(
                max_concurrent_tasks=4,
                chunk_size_mb=20,
                upload_timeout_seconds=1200
            )
        }
        return levels.get(level, levels["medium"])

🔌 API接口

项目管理API

# 创建项目
@router.post("/projects", response_model=ProjectResponse)
async def create_project(project: ProjectCreate):
    """创建新项目"""
    return await project_service.create_project(project)

# 获取项目列表
@router.get("/projects", response_model=List[ProjectResponse])
async def get_projects():
    """获取项目列表"""
    return await project_service.get_projects()

# 获取项目详情
@router.get("/projects/{project_id}", response_model=ProjectDetailResponse)
async def get_project(project_id: int):
    """获取项目详情"""
    return await project_service.get_project(project_id)

设置管理API

# 更新设置
@router.put("/settings", response_model=SettingsResponse)
async def update_settings(settings: DesktopSettings):
    """更新应用设置"""
    config = get_desktop_config()
    config.settings = settings
    save_desktop_config(config)
    return SettingsResponse(success=True)

# 测试API连接
@router.post("/settings/test-api", response_model=ApiTestResponse)
async def test_api_connection(request: TestApiRequest):
    """测试API连接"""
    provider = create_llm_provider(request.provider, request.api_key)
    success = provider.test_connection()
    return ApiTestResponse(success=success)

🎨 前端开发

状态管理

文件: frontend/src/store/useProjectStore.ts

interface ProjectStore {
  projects: Project[]
  currentProject: Project | null
  loading: boolean
  error: string | null
  
  // Actions
  fetchProjects: () => Promise<void>
  createProject: (project: ProjectCreate) => Promise<void>
  updateProject: (id: number, updates: Partial<Project>) => Promise<void>
  deleteProject: (id: number) => Promise<void>
}

export const useProjectStore = create<ProjectStore>((set, get) => ({
  projects: [],
  currentProject: null,
  loading: false,
  error: null,
  
  fetchProjects: async () => {
    set({ loading: true, error: null })
    try {
      const projects = await projectApi.getProjects()
      set({ projects, loading: false })
    } catch (error) {
      set({ error: error.message, loading: false })
    }
  },
  
  // ... 其他actions
}))

自定义Hooks

文件: frontend/src/hooks/useFirstRun.ts

export const useFirstRun = () => {
  const [isFirstRun, setIsFirstRun] = useState<boolean | null>(null)
  const [loading, setLoading] = useState(true)
  
  const checkFirstRun = async () => {
    try {
      const config = await configApi.getConfig()
      const hasApiKey = !!(config.settings?.llm?.api_key)
      setIsFirstRun(!hasApiKey)
    } catch (error) {
      console.error('检查首次运行状态失败:', error)
      setIsFirstRun(true)
    } finally {
      setLoading(false)
    }
  }
  
  useEffect(() => {
    checkFirstRun()
  }, [])
  
  return { isFirstRun, loading, refresh: checkFirstRun }
}

错误处理

文件: frontend/src/utils/errorHandler.ts

class ErrorHandler {
  private static instance: ErrorHandler
  
  public static getInstance(): ErrorHandler {
    if (!ErrorHandler.instance) {
      ErrorHandler.instance = new ErrorHandler()
    }
    return ErrorHandler.instance
  }
  
  public handleError(error: any, category: ErrorCategory = 'SYSTEM', context?: string) {
    let errorMessage = '发生未知错误,请重试'
    let errorLevel: 'error' | 'warning' | 'info' = 'error'
    
    if (error.response) {
      // 处理HTTP错误
      errorMessage = error.response.data?.message || `服务器错误: ${error.response.status}`
      if (error.response.status === 429) {
        errorMessage = '系统正在处理其他项目,请稍后再试'
        errorLevel = 'warning'
      }
    } else if (error.request) {
      // 处理网络错误
      errorMessage = '网络连接失败,请检查网络或后端服务是否运行'
    } else if (error.message) {
      // 处理其他错误
      errorMessage = error.message
    }
    
    const finalMessage = context ? `[${context}] ${errorMessage}` : errorMessage
    console.error(`[${category} Error] ${finalMessage}`, error)
    
    if (errorLevel === 'warning') {
      message.warning(finalMessage)
    } else {
      message.error(finalMessage)
    }
  }
}

🐍 后端开发

服务管理

文件: backend/desktop_main.py

class DesktopServiceManager:
    """桌面服务管理器"""
    
    def __init__(self, config: DesktopConfig):
        self.config = config
        self.is_running = False
        self.server_thread = None
        self.celery_worker_process = None
    
    def start(self):
        """启动所有服务"""
        if self.is_running:
            return
        
        try:
            # 启动FastAPI服务器
            self._start_fastapi_server()
            
            # 启动Celery Worker
            self._start_celery_worker()
            
            self.is_running = True
            self.start_time = time.time()
            logger.info("✅ 所有服务启动成功")
            
        except Exception as e:
            logger.error(f"❌ 服务启动失败: {e}")
            self.stop()
            raise
    
    def stop(self):
        """停止所有服务"""
        if not self.is_running:
            return
        
        try:
            # 停止Celery Worker进程
            if hasattr(self, 'celery_worker_process') and self.celery_worker_process:
                self.celery_worker_process.terminate()
                self.celery_worker_process.wait(timeout=5)
            
            # 停止FastAPI服务器
            if self.server_thread and self.server_thread.is_alive():
                self.server_thread.join(timeout=5)
            
            self.is_running = False
            logger.info("✅ 服务已停止")
            
        except Exception as e:
            logger.error(f"❌ 停止服务失败: {e}")

任务处理

文件: backend/tasks/video_tasks.py

@celery_app.task(bind=True)
def process_video_task(self, project_id: int, video_path: str):
    """处理视频任务"""
    try:
        # 更新任务状态
        self.update_state(state='PROGRESS', meta={'status': '开始处理视频'})
        
        # 提取音频
        audio_path = extract_audio(video_path)
        self.update_state(state='PROGRESS', meta={'status': '音频提取完成'})
        
        # 语音识别
        transcript = transcribe_audio(audio_path)
        self.update_state(state='PROGRESS', meta={'status': '语音识别完成'})
        
        # 生成切片
        clips = generate_clips(transcript, video_path)
        self.update_state(state='PROGRESS', meta={'status': '切片生成完成'})
        
        # 保存结果
        save_project_results(project_id, clips)
        
        return {'status': '处理完成', 'clips_count': len(clips)}
        
    except Exception as e:
        logger.error(f"视频处理失败: {e}")
        raise self.retry(exc=e, countdown=60, max_retries=3)

错误处理

文件: backend/utils/error_handler.py

class AutoClipsException(Exception):
    """AutoClip自定义异常"""
    
    def __init__(self, 
                 message: str, 
                 category: ErrorCategory = ErrorCategory.SYSTEM,
                 error_code: ErrorCode = ErrorCode.UNKNOWN_ERROR,
                 details: dict = None):
        self.message = message
        self.category = category
        self.error_code = error_code
        self.details = details or {}
        super().__init__(self.message)

def handle_autoclips_exception(exc: AutoClipsException, request_id: str = None) -> JSONResponse:
    """处理AutoClip异常"""
    status_code = 500
    if exc.category == ErrorCategory.VALIDATION:
        status_code = 422
    elif exc.category == ErrorCategory.CONFIGURATION:
        status_code = 400
    elif exc.category == ErrorCategory.NETWORK:
        status_code = 503
    
    return JSONResponse(
        status_code=status_code,
        content={
            "success": False,
            "error_code": exc.error_code.value,
            "message": exc.message,
            "details": exc.details,
            "request_id": request_id,
            "timestamp": time.time()
        }
    )

🦀 Tauri集成

命令定义

文件: src-tauri/src/commands.rs

#[tauri::command]
pub async fn start_backend_service(
    manager: State<'_, BackendManager>
) -> Result<BackendStatus, String> {
    match manager.start().await {
        Ok(_) => Ok(BackendStatus::Running),
        Err(e) => Err(format!("启动后端服务失败: {}", e)),
    }
}

#[tauri::command]
pub async fn stop_backend_service(
    manager: State<'_, BackendManager>
) -> Result<BackendStatus, String> {
    match manager.stop().await {
        Ok(_) => Ok(BackendStatus::Stopped),
        Err(e) => Err(format!("停止后端服务失败: {}", e)),
    }
}

#[tauri::command]
pub async fn get_service_status(
    manager: State<'_, BackendManager>
) -> Result<BackendStatus, String> {
    Ok(manager.get_status().await)
}

后端管理

文件: src-tauri/src/backend_manager.rs

pub struct BackendManager {
    status: BackendStatus,
    process: Option<Child>,
}

impl BackendManager {
    pub fn new() -> Self {
        Self {
            status: BackendStatus::Stopped,
            process: None,
        }
    }
    
    pub async fn start(&mut self) -> Result<(), String> {
        if self.status == BackendStatus::Running {
            return Ok(());
        }
        
        // 启动后端进程
        let mut cmd = Command::new("python")
            .arg("backend/desktop_main.py")
            .current_dir(std::env::current_dir().unwrap())
            .spawn()
            .map_err(|e| format!("启动后端进程失败: {}", e))?;
        
        self.process = Some(cmd);
        self.status = BackendStatus::Running;
        
        Ok(())
    }
    
    pub async fn stop(&mut self) -> Result<(), String> {
        if let Some(mut process) = self.process.take() {
            process.kill().map_err(|e| format!("停止后端进程失败: {}", e))?;
        }
        
        self.status = BackendStatus::Stopped;
        Ok(())
    }
}

系统托盘

文件: src-tauri/src/tray.rs

pub fn setup_system_tray(app_handle: &AppHandle) -> Result<(), Box<dyn std::error::Error>> {
    let tray_menu = MenuBuilder::new()
        .item(&MenuItem::new("显示主窗口", "show_main_window"))
        .separator()
        .item(&MenuItem::new("启动服务", "start_service"))
        .item(&MenuItem::new("停止服务", "stop_service"))
        .separator()
        .item(&MenuItem::new("退出", "quit_app"))
        .build()?;
    
    let _tray = TrayIconBuilder::new()
        .icon(app_handle.default_window_icon().unwrap().clone())
        .menu(&tray_menu)
        .on_menu_event(move |app, event| {
            match event.id.as_ref() {
                "show_main_window" => {
                    if let Some(window) = app.get_window("main") {
                        let _ = window.show();
                        let _ = window.set_focus();
                    }
                }
                "start_service" => {
                    // 启动服务逻辑
                }
                "stop_service" => {
                    // 停止服务逻辑
                }
                "quit_app" => {
                    app.exit(0);
                }
                _ => {}
            }
        })
        .build(app_handle)?;
    
    Ok(())
}

🏗️ 构建和部署

前端构建

# 开发环境
npm run dev

# 生产构建
npm run build

# 预览构建结果
npm run preview

后端构建

# 安装依赖
pip install -r requirements.txt

# 运行测试
pytest

# 代码检查
flake8 backend/
black backend/

Tauri构建

# 开发模式
npm run tauri dev

# 生产构建
npm run tauri build

# 构建特定平台
npm run tauri build -- --target x86_64-apple-darwin  # macOS Intel
npm run tauri build -- --target aarch64-apple-darwin # macOS ARM
npm run tauri build -- --target x86_64-pc-windows-msvc # Windows
npm run tauri build -- --target x86_64-unknown-linux-gnu # Linux

跨平台构建

# 构建所有平台
npm run build:all

# 构建特定平台
npm run build:macos
npm run build:windows
npm run build:linux

🧪 测试

前端测试

# 运行测试
npm test

# 运行测试并生成覆盖率报告
npm run test:coverage

# 运行E2E测试
npm run test:e2e

后端测试

# 运行所有测试
pytest

# 运行特定测试
pytest tests/test_project_service.py

# 运行测试并生成覆盖率报告
pytest --cov=backend tests/

# 运行性能测试
pytest tests/performance/

集成测试

# 启动测试环境
docker-compose -f docker-compose.test.yml up -d

# 运行集成测试
pytest tests/integration/

# 清理测试环境
docker-compose -f docker-compose.test.yml down

🤝 贡献指南

开发流程

  1. Fork项目

    git clone https://github.com/your-username/autoclip-desktop.git
    cd autoclip-desktop
    
  2. 创建分支

    git checkout -b feature/your-feature-name
    
  3. 开发功能

    • 编写代码
    • 添加测试
    • 更新文档
  4. 提交代码

    git add .
    git commit -m "feat: add your feature"
    git push origin feature/your-feature-name
    
  5. 创建Pull Request

    • 在GitHub上创建PR
    • 填写详细的描述
    • 等待代码审查

代码规范

前端规范

  • 使用TypeScript
  • 遵循ESLint规则
  • 使用Prettier格式化
  • 组件使用函数式组件
  • 使用Hooks进行状态管理

后端规范

  • 使用Python 3.9+
  • 遵循PEP 8规范
  • 使用类型注解
  • 编写文档字符串
  • 使用Pydantic进行数据验证

Rust规范

  • 使用Rust 1.70+
  • 遵循Clippy建议
  • 使用rustfmt格式化
  • 编写文档注释
  • 使用Result处理错误

提交信息规范

<type>(<scope>): <subject>

<body>

<footer>

类型:

  • feat: 新功能
  • fix: 修复bug
  • docs: 文档更新
  • style: 代码格式
  • refactor: 重构
  • test: 测试
  • chore: 构建/工具

示例:

feat(api): add project creation endpoint

Add new API endpoint for creating projects with validation
and error handling.

Closes #123

测试要求

  • 新功能必须包含测试
  • 测试覆盖率不低于80%
  • 所有测试必须通过
  • 包含单元测试和集成测试

文档要求

  • 更新相关文档
  • 添加API文档
  • 更新用户指南
  • 添加代码注释

📚 相关资源

🆘 获取帮助

  • GitHub Issues: 报告bug和功能请求
  • Discussions: 技术讨论和问题解答
  • Discord: 实时交流和协作
  • 邮件: 联系维护者

🎉 感谢您的贡献!