chore: remove dead PyInstaller-route scripts; rewrite build docs

After the CI unification, the PyInstaller / prepare_resources route scripts
were fully orphaned (referenced only by each other and stale docs). Remove the
cluster:

  build_backend.py, prepare_resources.py, check_bundle_size.py, smoke_api.py,
  build_desktop.py, dev_desktop.py, test_desktop.py, quick_build_check.sh,
  build_release.py

scripts/ now holds only what's live: build_macos_arm.sh (the one packaging
route), verify_desktop.sh (backend smoke), monitor_whisper.py (runtime).

Rewrite scripts/README.md and BUILD_GUIDE.md to describe the single PBS route
accurately (they previously documented the dead PyInstaller flow), and fix the
stale build command in RELEASE_CHECKLIST.md.

Also removed a 94M stale _autoclip-backend-pyinstaller-bak left in
src-tauri/resources/ (untracked build junk).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
周小舟
2026-05-30 17:30:27 +08:00
parent 4fa9b62fcf
commit ba3df7e60b
12 changed files with 91 additions and 2258 deletions

View File

@@ -1,205 +1,71 @@
# 🚀 AutoClip Desktop 构建指南
## 📋 概述
桌面客户端只有**一条**打包路线python-build-standalonePBS。它把便携 Python 运行时、
后端源码、静态 ffmpeg/ffprobe 全部打进 `.app`,用户机器**无需预装 Python 或 ffmpeg**。
AutoClip Desktop支持跨平台构建包括macOS (Intel/ARM)、Windows和Linux。由于架构限制我们采用以下策略
> 历史上的 PyInstaller / prepare_resources 路线(以及对应的 6+ 个 CI 工作流)从未成功产出可用包,已全部移除。
- **M芯片Mac**: 本地构建M芯片版本
- **其他平台**: 使用GitHub Actions CI/CD构建
## 本地构建macOS Apple Silicon
## 🛠️ 本地构建 (M芯片Mac)
### 快速构建
```bash
# 使用专用脚本
./scripts/build_macos_arm.sh
```
### 手动构建
```bash
# 1. 激活虚拟环境
source venv/bin/activate
# 2. 构建前端
cd frontend
npm ci
npm run build
cd ..
# 3. 构建后端
export AUTOCLIP_DESKTOP_MODE=1
export AUTOCLIP_MODE=desktop
python -m PyInstaller --onefile --name autoclip-backend \
--distpath src-tauri/resources \
--workpath build/pyinstaller \
--specpath build/specs \
--clean --noconfirm \
backend/desktop_main.py
# 4. 准备FFmpeg
mkdir -p src-tauri/resources/ffmpeg-unified
cp $(which ffmpeg) src-tauri/resources/ffmpeg-unified/ffmpeg-macos
chmod +x src-tauri/resources/ffmpeg-unified/ffmpeg-macos
# 5. 构建Tauri应用
cd src-tauri
tauri build
cd ..
产物:
```
src-tauri/target/release/bundle/macos/
├── AutoClip Desktop.app # 应用包(~550M
└── AutoClip Desktop_1.0.0_aarch64.dmg # DMG 安装包(~260M
```
## 🌐 跨平台构建 (GitHub Actions)
脚本各步骤说明见 [`scripts/README.md`](scripts/README.md)。
### 前置依赖
| 工具 | 版本 | 说明 |
|------|------|------|
| Node.js | 18+ | 前端构建 |
| Rust | stable | 带 `aarch64-apple-darwin` target |
| cargo-tauri | 2.x | `cargo install tauri-cli` |
系统 **不需要** 预装 Python / ffmpeg —— 脚本自带便携版(首次构建会下载并缓存到 `build/`)。
## CI / 发布GitHub Actions
`.github/workflows/desktop-build.yml` 跑同一个 `build_macos_arm.sh`
### 触发构建
```bash
# 创建标签触发构建
# 手动触发:仓库页面 → Actions → "Desktop Build (macOS arm64)" → Run workflow
# 或打 tag 触发,并自动把 DMG 挂到 GitHub Release
git tag v1.0.0
git push origin v1.0.0
# 或手动触发
# 在GitHub仓库页面 -> Actions -> 跨平台构建 -> Run workflow
```
### 构建平台
- **macOS Intel**: x86_64-apple-darwin
- **Windows**: x86_64-pc-windows-msvc
- **Linux**: x86_64-unknown-linux-gnu
## 安装与首次运行
## 📦 构建产物
DMG 是 ad-hoc 签名(未做 Apple 公证),所以:
### 本地构建 (M芯片Mac)
```
src-tauri/target/release/bundle/
├── macos/
│ └── AutoClip Desktop.app # macOS应用包
└── dmg/
└── AutoClip Desktop_1.0.0_aarch64.dmg # DMG安装包
```
1. 双击 DMG`AutoClip Desktop` 拖到 Applications
2. **首次打开右键点应用 → 选「打开」** 以绕过 Gatekeeper
3. 后端会自动启动并在 `~/Library/Application Support/AutoClip` 建数据目录
### GitHub Actions构建
```
artifacts/
├── autoclip-macos-intel/
│ └── AutoClip Desktop_1.0.0_x64.dmg
├── autoclip-windows/
│ └── AutoClip Desktop_1.0.0_x64.exe
└── autoclip-linux/
└── AutoClip Desktop_1.0.0_x86_64.AppImage
```
## 故障排除
## 🔧 依赖要求
### 本地构建
- **Node.js**: 18+
- **Python**: 3.11+
- **Rust**: 1.70+
- **Tauri CLI**: 2.0+
- **FFmpeg**: 系统安装
### GitHub Actions
- 自动安装所有依赖
- 支持所有目标平台
- 自动生成发布包
## 🚀 快速开始
### 1. 本地测试 (M芯片Mac)
命令行直接跑 app 看后端输出:
```bash
# 克隆项目
git clone <repository-url>
cd autoclip
# 安装依赖
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
# 构建
./scripts/build_macos_arm.sh
'/Applications/AutoClip Desktop.app/Contents/MacOS/autoclip-desktop'
```
应看到 `Backend started on port: XXXXX``Application startup complete`
### 2. 跨平台发布
```bash
# 提交代码
git add .
git commit -m "Release v1.0.0"
git push
| 现象 | 排查 |
|------|------|
| `ModuleNotFoundError: No module named 'X'` | 把 `X` 加进 `requirements.txt` 重新构建(构建期的依赖检查应该已经拦下,正常不会发生)|
| 窗口黑屏 | 前端没挂载,看 WebView 控制台;通常是打包/资源问题 |
| 视频处理失败 | 确认 `Contents/Resources/resources/ffmpeg/{ffmpeg,ffprobe}` 存在且可执行 |
| 构建失败想重来 | `rm -rf src-tauri/target build/pbs-cache build/ffmpeg-cache` 后重跑(会重新下载)|
# 创建标签
git tag v1.0.0
git push origin v1.0.0
## 已知限制
# GitHub Actions会自动构建所有平台
```
## 📋 构建检查清单
### 构建前检查
- [ ] 代码已提交
- [ ] 依赖已安装
- [ ] 环境变量已设置
- [ ] FFmpeg已安装
### 构建后验证
- [ ] 应用包存在
- [ ] 安装包存在
- [ ] 文件大小合理
- [ ] 可以正常安装
- [ ] 应用可以启动
## 🐛 故障排除
### 常见问题
#### 1. PyInstaller未找到
```bash
# 激活虚拟环境
source venv/bin/activate
pip install pyinstaller
```
#### 2. Tauri CLI未找到
```bash
# 安装Tauri CLI
cargo install tauri-cli
```
#### 3. FFmpeg未找到
```bash
# macOS
brew install ffmpeg
# 或使用conda
conda install ffmpeg
```
#### 4. 构建失败
```bash
# 清理构建缓存
rm -rf build/
rm -rf src-tauri/target/
# 重新构建
./scripts/build_macos_arm.sh
```
### 日志查看
```bash
# 查看构建日志
tail -f logs/backend.log
tail -f logs/frontend.log
```
## 📞 支持
如果遇到构建问题,请:
1. 查看构建日志
2. 检查依赖版本
3. 提交Issue到GitHub
4. 提供详细的错误信息
## 🔄 更新说明
- **v1.0.0**: 初始版本支持M芯片Mac
- **v1.1.0**: 添加跨平台构建支持
- **v1.2.0**: 优化构建流程和错误处理
- 仅 Apple Silicon (arm64),暂无 Intel / Windows / Linux 包
- ad-hoc 签名、未公证,首次需右键打开

View File

@@ -65,11 +65,11 @@
### 2. 构建阶段
```bash
# 运行发布构建脚本
python scripts/build_release.py
# 打桌面客户端macOS arm64详见 BUILD_GUIDE.md
./scripts/build_macos_arm.sh
# 验证构建产物
ls -la releases/
ls -la "src-tauri/target/release/bundle/macos/"
```
### 3. 测试阶段

View File

@@ -1,240 +1,63 @@
# AutoClip 桌面客户端开发脚本
# scripts/
本目录包含了AutoClip桌面客户端的开发、构建和测试脚本。
构建与运维脚本。桌面客户端只有**一条**打包路线python-build-standalone下称 PBS
历史上的 PyInstaller / prepare_resources 路线及其脚本已全部移除。
## 📁 脚本说明
## 脚本清单
### 🔧 构建脚本
| 脚本 | 用途 |
|------|------|
| `build_macos_arm.sh` | **唯一的桌面打包脚本**macOS Apple Silicon。端到端产出 `.app` + `.dmg`。 |
| `verify_desktop.sh` | 后端冒烟测试:`cargo check` + 起后端,校验 `/health``/api/v1/video-categories`。被 `nightly-desktop-smoke.yml` 调用。 |
| `monitor_whisper.py` | 运行期 Whisper 任务监控,被根目录 `start_autoclip.sh` / `check_whisper_status.sh` 调用。 |
#### `build_backend.py`
构建后端二进制文件使用PyInstaller打包Python代码。
## 打包桌面客户端macOS arm64
```bash
python scripts/build_backend.py
./scripts/build_macos_arm.sh
```
**功能:**
- 安装Python依赖
- 安装PyInstaller
- 创建PyInstaller规格文件
- 构建单文件可执行程序
- 将构建结果复制到Tauri资源目录
#### `build_desktop.py`
构建完整的桌面应用包括前端、后端和Tauri应用。
```bash
python scripts/build_desktop.py
产物:
```
src-tauri/target/release/bundle/macos/
├── AutoClip Desktop.app
└── AutoClip Desktop_1.0.0_aarch64.dmg
```
**功能:**
- 检查构建环境Node.js、npm、Rust、Tauri CLI
- 构建后端二进制
- 安装前端依赖
- 构建前端
- 构建Tauri应用
- 显示构建结果
### 这个脚本做了什么
### 🚀 开发脚本
1. 下载便携 Python 运行时python-build-standalone缓存在 `build/pbs-cache/`
2. 用便携 Python 安装 `requirements.txt` 的全部依赖
3. **依赖完整性检查**AST 扫描后端所有第三方 import缺任何一个就让构建失败
(防止"开发机能跑、打包就 500"
4. rsync 拷贝后端源码到 `src-tauri/resources/backend/`
5. 下载**静态** ffmpeg + ffprobearm64自包含零 homebrew 依赖,缓存在 `build/ffmpeg-cache/`
6. 构建前端(`npm ci && npm run build`
7. `cargo tauri build --bundles app`
8.`python/``backend/``ffmpeg/` 注入 `.app/Contents/Resources/resources/`
9. ad-hoc 签名 + `hdiutil` 手动打 DMG
#### `dev_desktop.py`
管理桌面应用的开发环境。
### 前置依赖
```bash
# 启动开发环境
python scripts/dev_desktop.py start
# 停止开发环境
python scripts/dev_desktop.py stop
# 重启开发环境
python scripts/dev_desktop.py restart
# 查看状态
python scripts/dev_desktop.py status
# 安装依赖
python scripts/dev_desktop.py install
```
**功能:**
- 自动安装依赖
- 启动后端服务
- 启动前端开发服务器
- 启动Tauri开发模式
- 进程管理和监控
- 服务健康检查
### 🧪 测试脚本
#### `test_desktop.py`
运行桌面应用的功能测试。
```bash
python scripts/test_desktop.py
```
**测试项目:**
- 文件结构完整性
- Python依赖检查
- Node.js依赖检查
- Rust环境检查
- Tauri配置验证
- Cargo配置验证
- 前端构建测试
- 后端API测试需要服务运行
## 🛠️ 环境要求
### 必需工具
- **Python 3.11+** - 后端开发
- **Node.js 18+** - 前端开发
- **Rust 1.70+** - Tauri应用开发
- **Tauri CLI** - 桌面应用构建
### 安装Tauri CLI
```bash
npm install -g @tauri-apps/cli
```
## 🚀 快速开始
### 1. 开发环境
```bash
# 启动开发环境
python scripts/dev_desktop.py start
# 查看状态
python scripts/dev_desktop.py status
# 停止开发环境
python scripts/dev_desktop.py stop
```
### 2. 功能测试
```bash
# 运行所有测试
python scripts/test_desktop.py
# 查看帮助
python scripts/test_desktop.py --help
```
### 3. 构建发布版本
```bash
# 构建完整桌面应用
python scripts/build_desktop.py
```
## 📊 项目结构
```
autoclip/
├── backend/ # 后端代码
│ ├── desktop_main.py # 桌面模式主启动文件
│ ├── desktop_celery.py # 桌面模式Celery配置
│ ├── core/desktop_config.py # 桌面配置管理
│ └── api/v1/desktop.py # 桌面专用API
├── src-tauri/ # Tauri桌面应用
│ ├── src/
│ │ ├── lib.rs # 主入口
│ │ ├── backend_manager.rs # 后端管理器
│ │ ├── commands.rs # Tauri命令
│ │ └── tray.rs # 系统托盘
│ ├── tauri.conf.json # Tauri配置
│ └── Cargo.toml # Rust依赖
├── frontend/src/components/ # 前端组件
│ ├── DesktopSettings.tsx # 桌面设置界面
│ └── OfflineModeIndicator.tsx # 离线模式指示器
└── scripts/ # 开发脚本
├── build_backend.py # 后端构建脚本
├── build_desktop.py # 完整构建脚本
├── dev_desktop.py # 开发环境管理
├── test_desktop.py # 功能测试脚本
└── README.md # 本文档
```
## 🔧 配置说明
### 桌面模式配置
桌面应用使用专门的配置系统,配置文件位置:
- **macOS**: `~/Library/Application Support/AutoClip/config.json`
- **Windows**: `%LOCALAPPDATA%/AutoClip/config.json`
- Node.js 18+、Rust`aarch64-apple-darwin` target、cargo-tauri (`cargo install tauri-cli`)
- 系统 **不需要** 预装 Python / ffmpeg —— 脚本会自带便携版
### 环境变量
开发模式下会自动设置以下环境变量:
- `AUTOCLIP_DESKTOP_MODE=true`
- `AUTOCLIP_MODE=desktop`
## 🐛 故障排除
- `PIP_INDEX_URL`pip 源默认清华镜像CI 里设为 `https://pypi.org/simple`
### 常见问题
## CI
#### 1. 后端服务启动失败
`.github/workflows/desktop-build.yml``workflow_dispatch``v*` tag 上跑同一个
`build_macos_arm.sh`,打 tag 时把 DMG 挂到 GitHub Release。
> 当前仅 macOS arm64。PBS 与静态 ffmpeg 对 Intel/Windows/Linux 都有对应版本,
> 后续把 runner、下载 URL、tauri target 参数化即可泛化。
## 开发模式(不打包)
直接用 Tauri 开发模式,热重载:
```bash
# 检查端口是否被占用
lsof -i :8000
# 检查Python依赖
python -c "import fastapi, uvicorn, celery"
cd src-tauri && cargo tauri dev
```
#### 2. 前端构建失败
```bash
# 清理node_modules并重新安装
cd frontend
rm -rf node_modules package-lock.json
npm install
```
#### 3. Tauri构建失败
```bash
# 检查Rust环境
rustc --version
cargo --version
# 检查Tauri CLI
tauri --version
```
#### 4. 权限问题
```bash
# 给脚本执行权限
chmod +x scripts/*.py
```
### 日志查看
- **后端日志**: `data/logs/autoclip.log`
- **开发进程**: `dev_processes.json`
## 📝 开发说明
### 代码结构
- **后端**: 使用FastAPI + Celery + SQLite
- **前端**: 使用React + TypeScript + Ant Design
- **桌面**: 使用Tauri + Rust
### 开发流程
1. 使用 `dev_desktop.py start` 启动开发环境
2. 修改代码,自动热重载
3. 使用 `test_desktop.py` 运行测试
4. 使用 `build_desktop.py` 构建发布版本
### 贡献指南
1. Fork项目
2. 创建功能分支
3. 运行测试确保通过
4. 提交Pull Request
## 📞 支持
如果遇到问题,请:
1. 查看本文档的故障排除部分
2. 运行 `test_desktop.py` 检查环境
3. 查看日志文件
4. 提交Issue到项目仓库
---
**最后更新**: 2024年12月
**版本**: 1.0.0
(前端 :3000 + 后端动态端口,由 `backend_manager.rs` 拉起)

View File

@@ -1,233 +0,0 @@
#!/usr/bin/env python3
"""
构建后端二进制文件 - 桌面版
"""
import os
import sys
import subprocess
import shutil
import argparse
from pathlib import Path
def main():
parser = argparse.ArgumentParser(description="构建桌面版后端二进制文件")
parser.add_argument(
"--install-deps",
action="store_true",
help="构建前安装 requirements.txt 和 PyInstaller。默认只使用现有虚拟环境依赖。",
)
args = parser.parse_args()
print("[BUILD] Starting desktop backend build...")
# 设置路径
project_root = Path(__file__).parent.parent
backend_dir = project_root / "backend"
resources_dir = project_root / "src-tauri" / "resources"
# 确保资源目录存在
resources_dir.mkdir(parents=True, exist_ok=True)
# 检查虚拟环境(本地开发)或使用当前 PythonCI
venv_path = project_root / "venv"
if venv_path.exists():
pip_path = venv_path / "bin" / "pip"
python_path = venv_path / "bin" / "python"
else:
# CI 或无 venv 环境:使用当前 Python
pip_path = sys.executable
python_path = sys.executable
# 将 pip install 改为 python -m pip
print("[BUILD] No venv detected, using current Python environment")
if args.install_deps:
# 安装依赖
print("📦 安装Python依赖...")
try:
# 如果 pip_path 就是 sys.executable用 python -m pip
if pip_path == sys.executable:
subprocess.run([str(python_path), "-m", "pip", "install", "-r", "requirements.txt"],
check=True, cwd=project_root)
else:
subprocess.run([str(pip_path), "install", "-r", "requirements.txt"],
check=True, cwd=project_root)
print("✅ Python依赖安装成功")
except subprocess.CalledProcessError as e:
print(f"❌ Python依赖安装失败: {e}")
return 1
# 安装PyInstaller
print("📦 安装PyInstaller...")
try:
if pip_path == sys.executable:
subprocess.run([str(python_path), "-m", "pip", "install", "pyinstaller"], check=True)
else:
subprocess.run([str(pip_path), "install", "pyinstaller"], check=True)
except subprocess.CalledProcessError as e:
print(f"❌ PyInstaller安装失败: {e}")
return 1
else:
try:
subprocess.run(
[str(python_path), "-m", "PyInstaller", "--version"],
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
except subprocess.CalledProcessError:
print("❌ PyInstaller 不可用,请先运行: python scripts/build_backend.py --install-deps")
return 1
# 创建PyInstaller规格文件
spec_content = f"""
# -*- mode: python ; coding: utf-8 -*-
from PyInstaller.utils.hooks import collect_submodules
block_cipher = None
a = Analysis(
['{backend_dir}/desktop_main.py'],
pathex=['{project_root}', '{backend_dir}'],
binaries=[],
datas=[
('{backend_dir}/core', 'backend/core'),
('{backend_dir}/api', 'backend/api'),
('{backend_dir}/models', 'backend/models'),
('{backend_dir}/services', 'backend/services'),
('{backend_dir}/repositories', 'backend/repositories'),
('{backend_dir}/schemas', 'backend/schemas'),
('{backend_dir}/utils', 'backend/utils'),
('{backend_dir}/tasks', 'backend/tasks'),
('{backend_dir}/pipeline', 'backend/pipeline'),
('{backend_dir}/prompt', 'backend/prompt'),
],
hiddenimports=[
'uvicorn',
'fastapi',
'celery',
'sqlalchemy',
'pydantic',
'psutil',
'requests',
'aiofiles',
'multipart',
'kombu.transport.filesystem',
'sqlalchemy.dialects.sqlite',
'backend',
'backend.app_factory',
'backend.desktop_celery',
'backend.tasks.processing',
]
+ collect_submodules('celery')
+ collect_submodules('kombu')
+ collect_submodules('billiard')
+ collect_submodules('vine'),
hookspath=[],
hooksconfig={{}},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
exe = EXE(
pyz,
a.scripts,
[],
name='autoclip-backend',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=True,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
exclude_binaries=True,
)
coll = COLLECT(
exe,
a.binaries,
a.zipfiles,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
name='autoclip-backend',
)
"""
spec_file = backend_dir / "autoclip-backend.spec"
with open(spec_file, 'w', encoding='utf-8') as f:
f.write(spec_content)
# 构建后端二进制
print("🔨 构建后端二进制...")
try:
# 清理旧的构建产物
print("🧹 清理旧的构建产物...")
for stale_dir in [
backend_dir / "dist",
backend_dir / "build",
project_root / "build",
]:
if stale_dir.exists():
shutil.rmtree(stale_dir)
stale_dist = project_root / "dist" / "autoclip-backend"
if stale_dist.exists():
if stale_dist.is_dir():
shutil.rmtree(stale_dist)
else:
stale_dist.unlink()
resource_backend = resources_dir / "autoclip-backend"
if resource_backend.exists():
if resource_backend.is_dir():
shutil.rmtree(resource_backend)
else:
resource_backend.unlink()
# 在项目根目录执行构建,确保路径正确
subprocess.run([
str(python_path), "-m", "PyInstaller",
"--clean",
"--noconfirm",
str(spec_file)
], check=True, cwd=project_root)
# 移动构建结果到资源目录
# PyInstaller 输出到项目根目录的 dist 文件夹
dist_dir = project_root / "dist" / "autoclip-backend"
if dist_dir.exists():
shutil.copytree(dist_dir, resources_dir / "autoclip-backend")
print("✅ 后端二进制构建成功")
else:
print("❌ 构建文件不存在")
return 1
except subprocess.CalledProcessError as e:
print(f"❌ 后端二进制构建失败: {e}")
return 1
# 清理临时文件
if spec_file.exists():
spec_file.unlink()
print("🎉 桌面版后端构建完成!")
print(f"📁 二进制文件位置: {resources_dir / 'autoclip-backend'}")
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -1,116 +0,0 @@
#!/usr/bin/env python3
"""
构建完整的桌面应用 - 桌面版
"""
import os
import sys
import subprocess
import shutil
from pathlib import Path
def main():
print("🚀 开始构建桌面应用...")
# 设置路径
project_root = Path(__file__).parent.parent
frontend_dir = project_root / "frontend"
src_tauri_dir = project_root / "src-tauri"
# 检查必要工具
print("🔍 检查构建环境...")
# 检查Node.js
try:
result = subprocess.run(["node", "--version"], capture_output=True, text=True)
print(f"✅ Node.js版本: {result.stdout.strip()}")
except FileNotFoundError:
print("❌ Node.js未安装")
return 1
# 检查npm
try:
result = subprocess.run(["npm", "--version"], capture_output=True, text=True)
print(f"✅ npm版本: {result.stdout.strip()}")
except FileNotFoundError:
print("❌ npm未安装")
return 1
# 检查Rust
try:
result = subprocess.run(["rustc", "--version"], capture_output=True, text=True)
print(f"✅ Rust版本: {result.stdout.strip()}")
except FileNotFoundError:
print("❌ Rust未安装")
return 1
# 检查Tauri CLI
try:
result = subprocess.run(["tauri", "--version"], capture_output=True, text=True)
print(f"✅ Tauri CLI版本: {result.stdout.strip()}")
except FileNotFoundError:
print("❌ Tauri CLI未安装正在安装...")
try:
subprocess.run(["npm", "install", "-g", "@tauri-apps/cli"], check=True)
print("✅ Tauri CLI安装成功")
except subprocess.CalledProcessError as e:
print(f"❌ Tauri CLI安装失败: {e}")
return 1
# 1. 构建后端
print("\n🔧 构建后端...")
try:
result = subprocess.run([sys.executable, "scripts/build_backend.py"],
check=True, cwd=project_root)
print("✅ 后端构建成功")
except subprocess.CalledProcessError as e:
print(f"❌ 后端构建失败: {e}")
return 1
# 2. 安装前端依赖
print("\n📦 安装前端依赖...")
try:
subprocess.run(["npm", "install"], check=True, cwd=frontend_dir)
print("✅ 前端依赖安装成功")
except subprocess.CalledProcessError as e:
print(f"❌ 前端依赖安装失败: {e}")
return 1
# 3. 构建前端
print("\n📦 构建前端...")
try:
subprocess.run(["npm", "run", "build"], check=True, cwd=frontend_dir)
print("✅ 前端构建成功")
except subprocess.CalledProcessError as e:
print(f"❌ 前端构建失败: {e}")
return 1
# 4. 构建Tauri应用
print("\n🦀 构建Tauri应用...")
try:
subprocess.run(["tauri", "build"], check=True, cwd=src_tauri_dir)
print("✅ Tauri应用构建成功")
except subprocess.CalledProcessError as e:
print(f"❌ Tauri应用构建失败: {e}")
return 1
print("\n🎉 桌面应用构建完成!")
# 显示构建结果
bundle_dir = src_tauri_dir / "target" / "release" / "bundle"
if bundle_dir.exists():
print(f"📁 安装包位置: {bundle_dir}")
# 列出可用的安装包
for item in bundle_dir.iterdir():
if item.is_dir():
print(f" 📦 {item.name}/")
for file in item.iterdir():
if file.is_file():
size = file.stat().st_size / (1024 * 1024) # MB
print(f" 📄 {file.name} ({size:.1f} MB)")
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -1,223 +0,0 @@
#!/usr/bin/env python3
"""
发布版本构建脚本
用于构建所有平台的桌面应用发布版本
"""
import os
import sys
import subprocess
import shutil
import json
from pathlib import Path
from datetime import datetime
def get_version():
"""获取当前版本号"""
try:
with open("src-tauri/Cargo.toml", "r") as f:
for line in f:
if line.startswith("version ="):
return line.split('"')[1]
except:
return "1.0.0"
def clean_build_artifacts():
"""清理构建产物"""
print("🧹 清理构建产物...")
paths_to_clean = [
"dist/",
"build/",
"src-tauri/target/",
"frontend/dist/",
"src-tauri/resources/autoclip-backend",
"src-tauri/resources/autoclip-backend-macos-arm64",
]
for path in paths_to_clean:
if os.path.exists(path):
if os.path.isdir(path):
shutil.rmtree(path)
print(f" ✓ 删除目录: {path}")
else:
os.remove(path)
print(f" ✓ 删除文件: {path}")
def build_frontend():
"""构建前端"""
print("🎨 构建前端...")
try:
subprocess.run(["npm", "run", "build"], cwd="frontend", check=True)
print(" ✓ 前端构建成功")
return True
except subprocess.CalledProcessError as e:
print(f" ❌ 前端构建失败: {e}")
return False
def prepare_backend_resources():
"""准备后端资源"""
print("🔧 准备后端资源...")
try:
subprocess.run([sys.executable, "prepare_resources.py"], cwd="scripts", check=True)
print(" ✓ 后端资源准备完成")
return True
except subprocess.CalledProcessError as e:
print(f" ❌ 后端资源准备失败: {e}")
return False
def build_desktop_app():
"""构建桌面应用"""
print("🖥️ 构建桌面应用...")
try:
# 构建所有平台
subprocess.run(["npm", "run", "tauri", "build"], cwd="src-tauri", check=True)
print(" ✓ 桌面应用构建成功")
return True
except subprocess.CalledProcessError as e:
print(f" ❌ 桌面应用构建失败: {e}")
return False
def create_release_package():
"""创建发布包"""
print("📦 创建发布包...")
version = get_version()
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
# 创建发布目录
release_dir = Path(f"releases/v{version}_{timestamp}")
release_dir.mkdir(parents=True, exist_ok=True)
# 复制构建产物
dist_dir = Path("src-tauri/target/release/bundle")
if dist_dir.exists():
for item in dist_dir.iterdir():
if item.is_dir():
shutil.copytree(item, release_dir / item.name)
print(f" ✓ 复制: {item.name}")
# 复制文档
docs_to_copy = [
"README.md",
"RELEASE_NOTES.md",
"CHANGELOG.md",
"LICENSE",
"env.example"
]
for doc in docs_to_copy:
if os.path.exists(doc):
shutil.copy2(doc, release_dir)
print(f" ✓ 复制文档: {doc}")
# 创建安装说明
install_guide = release_dir / "INSTALL.md"
with open(install_guide, "w", encoding="utf-8") as f:
f.write(f"""# AutoClip 桌面版安装指南
## 版本: v{version}
## 构建时间: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
### 安装步骤
1. 根据您的操作系统选择对应的安装包:
- Windows: 选择 .msi 文件
- macOS: 选择 .dmg 文件
- Linux: 选择 .AppImage 或 .deb 文件
2. 运行安装包并按照提示完成安装
3. 首次启动时,系统会自动初始化
4. 在设置中配置通义千问API密钥参考 env.example
### 系统要求
- Windows 10+ / macOS 10.13+ / Linux
- 4GB+ RAM
- 10GB+ 可用存储空间
- 稳定的网络连接
### 获取帮助
- 查看 README.md 了解详细使用说明
- 查看 RELEASE_NOTES.md 了解新功能
- 提交问题到: https://github.com/zhouxiaoka/autoclip/issues
### 许可证
本项目采用 MIT 许可证,详见 LICENSE 文件。
""")
print(f" ✓ 发布包创建完成: {release_dir}")
return release_dir
def generate_checksums(release_dir):
"""生成校验和"""
print("🔐 生成校验和...")
checksums = []
for file_path in release_dir.rglob("*"):
if file_path.is_file():
try:
import hashlib
with open(file_path, "rb") as f:
sha256 = hashlib.sha256(f.read()).hexdigest()
relative_path = file_path.relative_to(release_dir)
checksums.append(f"{sha256} {relative_path}")
except Exception as e:
print(f" ⚠️ 跳过文件 {file_path}: {e}")
checksum_file = release_dir / "checksums.txt"
with open(checksum_file, "w") as f:
f.write("\n".join(checksums))
print(f" ✓ 校验和文件: {checksum_file}")
def main():
"""主函数"""
print("🚀 开始构建发布版本...")
print(f"版本: {get_version()}")
print(f"时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
# 检查环境
if not os.path.exists("package.json"):
print("❌ 请在项目根目录运行此脚本")
return 1
# 构建流程
steps = [
("清理构建产物", clean_build_artifacts),
("构建前端", build_frontend),
("准备后端资源", prepare_backend_resources),
("构建桌面应用", build_desktop_app),
("创建发布包", create_release_package),
]
for step_name, step_func in steps:
print(f"\n📋 {step_name}...")
if not step_func():
print(f"{step_name}失败,构建终止")
return 1
# 生成校验和
release_dir = create_release_package()
generate_checksums(release_dir)
print("\n🎉 发布版本构建完成!")
print(f"📦 发布包位置: {release_dir}")
print("\n📋 下一步:")
print("1. 测试发布包在不同平台上的安装和运行")
print("2. 创建 GitHub Release")
print("3. 上传发布包到 GitHub Releases")
print("4. 更新项目文档")
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -1,191 +0,0 @@
#!/usr/bin/env python3
"""
安装包体积检查脚本
用于检查 Tauri 构建产物的体积,确保不超过预设阈值
"""
import argparse
import os
import sys
from pathlib import Path
from typing import Dict, List, Tuple
class Colors:
RED = '\033[0;31m'
GREEN = '\033[0;32m'
YELLOW = '\033[1;33m'
BLUE = '\033[0;34m'
NC = '\033[0m' # No Color
class BundleSizeChecker:
def __init__(self, project_root: Path, max_size_mb: int = 500):
self.project_root = project_root
self.max_size_mb = max_size_mb
self.max_size_bytes = max_size_mb * 1024 * 1024
# Tauri 构建产物路径
self.bundle_paths = [
project_root / "src-tauri" / "target" / "release" / "bundle",
project_root / "dist",
project_root / "build"
]
def log(self, message: str, color: str = Colors.NC):
"""打印带颜色的日志"""
print(f"{color}{message}{Colors.NC}")
def get_file_size(self, file_path: Path) -> int:
"""获取文件大小(字节)"""
try:
return file_path.stat().st_size
except (OSError, FileNotFoundError):
return 0
def get_dir_size(self, dir_path: Path) -> int:
"""获取目录总大小(字节)"""
total_size = 0
try:
for item in dir_path.rglob('*'):
if item.is_file():
total_size += self.get_file_size(item)
except (OSError, FileNotFoundError):
pass
return total_size
def format_size(self, size_bytes: int) -> str:
"""格式化文件大小"""
if size_bytes < 1024:
return f"{size_bytes} B"
elif size_bytes < 1024 * 1024:
return f"{size_bytes / 1024:.1f} KB"
elif size_bytes < 1024 * 1024 * 1024:
return f"{size_bytes / (1024 * 1024):.1f} MB"
else:
return f"{size_bytes / (1024 * 1024 * 1024):.1f} GB"
def find_bundle_files(self) -> List[Tuple[Path, int]]:
"""查找所有构建产物文件"""
bundle_files = []
for bundle_path in self.bundle_paths:
if bundle_path.exists():
if bundle_path.is_file():
size = self.get_file_size(bundle_path)
bundle_files.append((bundle_path, size))
elif bundle_path.is_dir():
# 查找常见的安装包文件
for pattern in ["*.dmg", "*.pkg", "*.deb", "*.rpm", "*.msi", "*.exe", "*.app"]:
for file_path in bundle_path.rglob(pattern):
if file_path.is_file():
size = self.get_file_size(file_path)
bundle_files.append((file_path, size))
return bundle_files
def check_resource_size(self) -> Dict[str, int]:
"""检查资源文件大小"""
resources_path = self.project_root / "src-tauri" / "resources"
resource_sizes = {}
if not resources_path.exists():
return resource_sizes
for item in resources_path.rglob('*'):
if item.is_file():
size = self.get_file_size(item)
relative_path = item.relative_to(resources_path)
resource_sizes[str(relative_path)] = size
return resource_sizes
def run_check(self, verbose: bool = False) -> bool:
"""运行体积检查"""
self.log("🔍 开始检查安装包体积", Colors.YELLOW)
self.log(f"最大允许体积: {self.format_size(self.max_size_bytes)}", Colors.YELLOW)
self.log("")
# 检查构建产物
bundle_files = self.find_bundle_files()
if not bundle_files:
self.log("⚠️ 未找到构建产物文件", Colors.YELLOW)
self.log("请确保已经运行过构建命令", Colors.YELLOW)
return True
total_size = 0
oversized_files = []
self.log("📦 构建产物文件:", Colors.BLUE)
for file_path, size in bundle_files:
total_size += size
size_str = self.format_size(size)
if size > self.max_size_bytes:
oversized_files.append((file_path, size))
self.log(f"{file_path.name}: {size_str} (超过限制)", Colors.RED)
else:
self.log(f"{file_path.name}: {size_str}", Colors.GREEN)
self.log("")
self.log(f"📊 总大小: {self.format_size(total_size)}", Colors.YELLOW)
# 检查资源文件
if verbose:
self.log("")
self.log("📁 资源文件详情:", Colors.BLUE)
resource_sizes = self.check_resource_size()
# 按大小排序
sorted_resources = sorted(resource_sizes.items(), key=lambda x: x[1], reverse=True)
for relative_path, size in sorted_resources:
size_str = self.format_size(size)
self.log(f" {relative_path}: {size_str}")
# 检查结果
if oversized_files:
self.log("")
self.log("❌ 体积检查失败!", Colors.RED)
self.log("以下文件超过体积限制:", Colors.RED)
for file_path, size in oversized_files:
size_str = self.format_size(size)
self.log(f" - {file_path.name}: {size_str}", Colors.RED)
return False
elif total_size > self.max_size_bytes:
self.log("")
self.log("❌ 体积检查失败!", Colors.RED)
self.log(f"总大小 {self.format_size(total_size)} 超过限制 {self.format_size(self.max_size_bytes)}", Colors.RED)
return False
else:
self.log("")
self.log("✅ 体积检查通过!", Colors.GREEN)
return True
def main():
parser = argparse.ArgumentParser(description='安装包体积检查脚本')
parser.add_argument('-m', '--max-size', type=int, default=500, help='最大允许体积MB默认: 500')
parser.add_argument('-v', '--verbose', action='store_true', help='详细输出')
parser.add_argument('--project-root', type=str, help='项目根目录路径')
args = parser.parse_args()
# 确定项目根目录
if args.project_root:
project_root = Path(args.project_root)
else:
# 从脚本位置推断项目根目录
script_dir = Path(__file__).parent
project_root = script_dir.parent
if not project_root.exists():
print(f"错误: 项目根目录不存在: {project_root}")
sys.exit(1)
# 创建检查器并运行检查
checker = BundleSizeChecker(project_root, args.max_size)
success = checker.run_check(args.verbose)
sys.exit(0 if success else 1)
if __name__ == '__main__':
main()

View File

@@ -1,274 +0,0 @@
#!/usr/bin/env python3
"""
桌面应用开发环境管理脚本
"""
import os
import sys
import subprocess
import signal
import time
import json
from pathlib import Path
from typing import Dict, Any
class DesktopDevManager:
def __init__(self):
self.project_root = Path(__file__).parent.parent
self.backend_dir = self.project_root / "backend"
self.frontend_dir = self.project_root / "frontend"
self.src_tauri_dir = self.project_root / "src-tauri"
self.pid_file = self.project_root / "dev_processes.json"
self.processes: Dict[str, int] = {}
def load_processes(self):
"""加载进程信息"""
if self.pid_file.exists():
try:
with open(self.pid_file, 'r') as f:
self.processes = json.load(f)
except Exception as e:
print(f"⚠️ 加载进程信息失败: {e}")
self.processes = {}
def save_processes(self):
"""保存进程信息"""
try:
with open(self.pid_file, 'w') as f:
json.dump(self.processes, f, indent=2)
except Exception as e:
print(f"⚠️ 保存进程信息失败: {e}")
def start_backend(self):
"""启动后端服务"""
print("🚀 启动后端服务...")
# 设置环境变量
env = os.environ.copy()
env["AUTOCLIP_DESKTOP_MODE"] = "true"
env["AUTOCLIP_MODE"] = "desktop"
try:
# 启动后端
process = subprocess.Popen(
[sys.executable, "desktop_main.py"],
cwd=self.backend_dir,
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
self.processes["backend"] = process.pid
self.save_processes()
print(f"✅ 后端服务已启动 (PID: {process.pid})")
print(f"🌐 服务地址: http://127.0.0.1:8000")
return True
except Exception as e:
print(f"❌ 启动后端服务失败: {e}")
return False
def start_frontend(self):
"""启动前端开发服务器"""
print("🚀 启动前端开发服务器...")
try:
# 启动前端开发服务器
process = subprocess.Popen(
["npm", "run", "dev"],
cwd=self.frontend_dir,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
self.processes["frontend"] = process.pid
self.save_processes()
print(f"✅ 前端开发服务器已启动 (PID: {process.pid})")
print(f"🌐 前端地址: http://localhost:5173")
return True
except Exception as e:
print(f"❌ 启动前端开发服务器失败: {e}")
return False
def start_tauri(self):
"""启动Tauri开发模式"""
print("🚀 启动Tauri开发模式...")
try:
# 启动Tauri开发模式
process = subprocess.Popen(
["npm", "run", "tauri:dev"],
cwd=self.src_tauri_dir,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
self.processes["tauri"] = process.pid
self.save_processes()
print(f"✅ Tauri开发模式已启动 (PID: {process.pid})")
return True
except Exception as e:
print(f"❌ 启动Tauri开发模式失败: {e}")
return False
def stop_process(self, name: str):
"""停止指定进程"""
if name not in self.processes:
print(f"⚠️ 进程 {name} 未运行")
return
pid = self.processes[name]
try:
os.kill(pid, signal.SIGTERM)
print(f"✅ 进程 {name} 已停止 (PID: {pid})")
del self.processes[name]
self.save_processes()
except ProcessLookupError:
print(f"⚠️ 进程 {name} (PID: {pid}) 不存在")
del self.processes[name]
self.save_processes()
except Exception as e:
print(f"❌ 停止进程 {name} 失败: {e}")
def stop_all(self):
"""停止所有进程"""
print("🛑 停止所有开发进程...")
for name in list(self.processes.keys()):
self.stop_process(name)
# 清理PID文件
if self.pid_file.exists():
self.pid_file.unlink()
print("✅ 所有进程已停止")
def status(self):
"""显示服务状态"""
print("📊 开发环境状态:")
print("-" * 50)
if not self.processes:
print("❌ 没有运行中的进程")
return
for name, pid in self.processes.items():
try:
os.kill(pid, 0) # 检查进程是否存在
print(f"{name}: 运行中 (PID: {pid})")
except ProcessLookupError:
print(f"{name}: 已停止 (PID: {pid})")
del self.processes[name]
self.save_processes()
# 检查服务健康状态
print("\n🔍 服务健康检查:")
try:
import requests
response = requests.get("http://127.0.0.1:8000/health", timeout=5)
if response.status_code == 200:
print("✅ 后端服务健康")
else:
print("⚠️ 后端服务响应异常")
except Exception as e:
print(f"❌ 后端服务不可用: {e}")
def install_dependencies(self):
"""安装依赖"""
print("📦 安装依赖...")
# 安装Python依赖
print("安装Python依赖...")
try:
subprocess.run([sys.executable, "-m", "pip", "install", "-r", "requirements.txt"],
check=True, cwd=self.project_root)
print("✅ Python依赖安装成功")
except subprocess.CalledProcessError as e:
print(f"❌ Python依赖安装失败: {e}")
return False
# 安装前端依赖
print("安装前端依赖...")
try:
subprocess.run(["npm", "install"], check=True, cwd=self.frontend_dir)
print("✅ 前端依赖安装成功")
except subprocess.CalledProcessError as e:
print(f"❌ 前端依赖安装失败: {e}")
return False
return True
def main():
if len(sys.argv) < 2:
print("用法: python dev_desktop.py <command>")
print("命令:")
print(" start - 启动开发环境")
print(" stop - 停止开发环境")
print(" restart - 重启开发环境")
print(" status - 显示状态")
print(" install - 安装依赖")
return 1
manager = DesktopDevManager()
manager.load_processes()
command = sys.argv[1].lower()
if command == "start":
print("🚀 启动桌面应用开发环境...")
# 安装依赖
if not manager.install_dependencies():
return 1
# 启动服务
success = True
success &= manager.start_backend()
time.sleep(2) # 等待后端启动
success &= manager.start_frontend()
time.sleep(2) # 等待前端启动
success &= manager.start_tauri()
if success:
print("\n🎉 开发环境启动成功!")
print("📱 桌面应用将自动打开")
print("🌐 前端开发服务器: http://localhost:5173")
print("🔧 后端API服务: http://127.0.0.1:8000")
else:
print("\n❌ 开发环境启动失败")
return 1
elif command == "stop":
manager.stop_all()
elif command == "restart":
print("🔄 重启开发环境...")
manager.stop_all()
time.sleep(2)
return main() # 递归调用start
elif command == "status":
manager.status()
elif command == "install":
if not manager.install_dependencies():
return 1
print("✅ 依赖安装完成")
else:
print(f"❌ 未知命令: {command}")
return 1
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -1,175 +0,0 @@
#!/usr/bin/env python3
"""
资源准备脚本 - 只打包当前平台所需的文件
避免将三平台 ffmpeg 和大量不必要文件打入安装包
"""
import os
import sys
import shutil
import platform
from pathlib import Path
def get_platform_name():
"""获取当前平台名称"""
system = platform.system().lower()
if system == "darwin":
return "macos"
elif system == "windows":
return "windows"
elif system == "linux":
return "linux"
else:
raise ValueError(f"不支持的平台: {system}")
def prepare_resources():
"""准备打包资源"""
# 获取项目根目录
project_root = Path(__file__).parent.parent
tauri_resources = project_root / "src-tauri" / "resources"
print(f"项目根目录: {project_root}")
print(f"Tauri 资源目录: {tauri_resources}")
# 清理旧的资源文件(保留 ffmpeg-unified
if tauri_resources.exists():
print("清理旧的资源文件...")
# 先备份 ffmpeg-unified 目录
ffmpeg_unified_backup = None
ffmpeg_unified_dir = tauri_resources / "ffmpeg-unified"
if ffmpeg_unified_dir.exists():
ffmpeg_unified_backup = project_root / "temp_ffmpeg_unified"
if ffmpeg_unified_backup.exists():
shutil.rmtree(ffmpeg_unified_backup)
shutil.move(str(ffmpeg_unified_dir), str(ffmpeg_unified_backup))
shutil.rmtree(tauri_resources)
# 恢复 ffmpeg-unified 目录
if ffmpeg_unified_backup and ffmpeg_unified_backup.exists():
tauri_resources.mkdir(parents=True, exist_ok=True)
shutil.move(str(ffmpeg_unified_backup), str(ffmpeg_unified_dir))
# 创建资源目录
tauri_resources.mkdir(parents=True, exist_ok=True)
# 获取当前平台
current_platform = get_platform_name()
print(f"当前平台: {current_platform}")
# 不复制后端二进制文件(太大,改用 Python 源代码)
# 桌面端将在运行时创建 Python 环境
print(" 跳过后端二进制文件(体积过大),将使用 Python 源代码")
# 只复制当前平台的 ffmpeg
ffmpeg_source_dir = project_root / "src-tauri" / "resources" / "ffmpeg-unified"
if ffmpeg_source_dir.exists():
# 创建 ffmpeg 目录
ffmpeg_dest_dir = tauri_resources / "ffmpeg"
ffmpeg_dest_dir.mkdir(exist_ok=True)
# 根据平台选择对应的 ffmpeg 文件
platform_files = {
"macos": "ffmpeg-macos",
"windows": "ffmpeg-windows.exe",
"linux": "ffmpeg-linux"
}
if current_platform in platform_files:
source_file = ffmpeg_source_dir / platform_files[current_platform]
if source_file.exists():
dest_file = ffmpeg_dest_dir / "ffmpeg"
shutil.copy2(source_file, dest_file)
# 设置执行权限
os.chmod(dest_file, 0o755)
print(f"✓ 复制 {current_platform} ffmpeg")
else:
print(f"{current_platform} 平台的 ffmpeg 文件不存在: {source_file}")
else:
print(f"⚠ 不支持的平台: {current_platform}")
else:
print("⚠ ffmpeg 源目录不存在")
# 复制后端 Python 源代码(排除不必要文件)
backend_source = project_root / "backend"
if backend_source.exists():
backend_dest = tauri_resources / "backend"
backend_dest.mkdir(exist_ok=True)
# 要排除的目录和文件
exclude_dirs = {
'__pycache__', '.mypy_cache', '.ruff_cache', '.pytest_cache',
'tests', 'data', 'logs', 'temp', 'uploads', 'whisper_models'
}
exclude_files = {'.pyc', '.pyo', '.pyd', '.log', '.pid', '.rdb'}
def should_exclude(path):
if path.name in exclude_dirs:
return True
if path.suffix in exclude_files:
return True
return False
def copy_backend_files(source, dest):
for item in source.iterdir():
if should_exclude(item):
continue
dest_item = dest / item.name
if item.is_dir():
dest_item.mkdir(exist_ok=True)
copy_backend_files(item, dest_item)
else:
shutil.copy2(item, dest_item)
copy_backend_files(backend_source, backend_dest)
print("✓ 复制后端 Python 源代码")
# 复制 requirements.txt
requirements_file = project_root / "requirements.txt"
if requirements_file.exists():
shutil.copy2(requirements_file, tauri_resources / "requirements.txt")
print("✓ 复制 requirements.txt")
else:
print("⚠ 后端源代码目录不存在")
# 复制 uvPython 包管理器)
uv_source = project_root / "uv"
if uv_source.exists():
shutil.copy2(uv_source, tauri_resources / "uv")
os.chmod(tauri_resources / "uv", 0o755)
print("✓ 复制 uv")
else:
print("⚠ uv 文件不存在,将使用系统 Python")
print(f"\n资源准备完成!")
print(f"资源目录大小: {get_dir_size(tauri_resources):.2f} MB")
# 列出最终的文件结构
print("\n最终资源文件结构:")
for root, dirs, files in os.walk(tauri_resources):
level = root.replace(str(tauri_resources), '').count(os.sep)
indent = ' ' * 2 * level
print(f"{indent}{os.path.basename(root)}/")
subindent = ' ' * 2 * (level + 1)
for file in files:
file_path = Path(root) / file
size = file_path.stat().st_size / (1024 * 1024) # MB
print(f"{subindent}{file} ({size:.2f} MB)")
def get_dir_size(path):
"""获取目录大小MB"""
total_size = 0
for dirpath, dirnames, filenames in os.walk(path):
for filename in filenames:
filepath = os.path.join(dirpath, filename)
if os.path.exists(filepath):
total_size += os.path.getsize(filepath)
return total_size / (1024 * 1024) # 转换为 MB
if __name__ == "__main__":
try:
prepare_resources()
except Exception as e:
print(f"错误: {e}")
sys.exit(1)

View File

@@ -1,107 +0,0 @@
#!/bin/bash
set -e
echo "🚀 AutoClip 快速构建自检脚本"
echo "================================"
# 获取项目根目录
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$PROJECT_ROOT"
echo "📁 项目根目录: $PROJECT_ROOT"
# 1) 清理构建产物
echo "🧹 清理构建产物..."
rm -rf frontend/dist
rm -rf frontend/node_modules/.vite
rm -rf src-tauri/target
rm -rf src-tauri/resources
echo "✓ 清理完成"
# 2) 前端重装 + 生产构建
echo "📦 前端依赖安装..."
cd frontend
npm install
echo "✓ 前端依赖安装完成"
echo "🔨 前端生产构建..."
npm run build
echo "✓ 前端构建完成"
# 检查 dist 目录
if [ -d "dist" ]; then
DIST_SIZE=$(du -sh dist | cut -f1)
echo "📊 前端产物大小: $DIST_SIZE"
else
echo "❌ 前端构建失败dist 目录不存在"
exit 1
fi
cd "$PROJECT_ROOT"
# 3) 准备资源文件
echo "📋 准备资源文件..."
python scripts/prepare_resources.py
echo "✓ 资源准备完成"
# 4) 检查 .taurignore
echo "📝 检查 .taurignore..."
if [ -f ".taurignore" ]; then
echo "✓ .taurignore 文件存在"
echo "排除的文件类型:"
grep -E "^[^#]" .taurignore | head -10
else
echo "⚠ .taurignore 文件不存在"
fi
# 5) 检查资源目录大小
echo "📊 资源目录分析..."
if [ -d "src-tauri/resources" ]; then
RESOURCES_SIZE=$(du -sh src-tauri/resources | cut -f1)
echo "📦 资源目录大小: $RESOURCES_SIZE"
echo "资源文件列表:"
find src-tauri/resources -type f -exec ls -lh {} \; | awk '{print $5, $9}' | head -10
else
echo "❌ 资源目录不存在"
exit 1
fi
# 6) Release 打包
echo "🔨 开始 Release 打包..."
cd src-tauri
# 设置环境变量关闭调试符号
export RUSTFLAGS="-C debuginfo=0"
# 执行 Tauri 构建
echo "执行: cargo tauri build"
cargo tauri build
echo "✅ 构建完成!"
# 7) 查看最终安装包体积
echo "📊 最终安装包分析..."
if [ -d "target/release/bundle" ]; then
echo "安装包文件:"
find target/release/bundle -name "*.dmg" -o -name "*.app" -o -name "*.exe" -o -name "*.deb" -o -name "*.rpm" | while read file; do
if [ -f "$file" ]; then
size=$(du -sh "$file" | cut -f1)
echo " 📦 $file: $size"
fi
done
echo ""
echo "🎉 构建成功!安装包已生成在 target/release/bundle/ 目录"
else
echo "❌ 构建失败bundle 目录不存在"
exit 1
fi
echo ""
echo "✨ 快速自检完成!"
echo "如果安装包体积仍然很大,请检查:"
echo "1. 是否还有大文件被打包进去"
echo "2. .taurignore 是否生效"
echo "3. 资源准备脚本是否正确过滤了文件"

View File

@@ -1,202 +0,0 @@
#!/usr/bin/env python3
"""
API 烟雾测试脚本 (Python 版本)
用于验证后端 API 的关键端点是否正常工作
"""
import argparse
import sys
import time
import requests
from typing import List, Tuple, Optional
from urllib.parse import urljoin
class Colors:
RED = '\033[0;31m'
GREEN = '\033[0;32m'
YELLOW = '\033[1;33m'
BLUE = '\033[0;34m'
NC = '\033[0m' # No Color
class SmokeTest:
def __init__(self, base_url: str, timeout: int = 10, verbose: bool = False):
self.base_url = base_url.rstrip('/')
self.timeout = timeout
self.verbose = verbose
self.session = requests.Session()
self.session.timeout = timeout
# 测试用例定义
self.test_cases = [
{
'endpoint': '/health',
'expected_key': 'status',
'description': '根健康检查',
'required': True
},
{
'endpoint': '/api/health',
'expected_key': 'status',
'description': 'API 健康检查',
'required': True
},
{
'endpoint': '/api/v1/video-categories',
'expected_key': 'categories',
'description': '视频分类配置',
'required': True
},
{
'endpoint': '/api/v1/projects',
'expected_key': 'data',
'description': '项目列表',
'required': False # 可能为空,但端点应该存在
},
{
'endpoint': '/api/v1/settings',
'expected_key': 'current_provider',
'description': '设置信息',
'required': False
},
{
'endpoint': '/api/v1/settings/desktop-mode',
'expected_key': 'is_desktop_mode',
'description': '桌面模式检查',
'required': False
}
]
def log(self, message: str, color: str = Colors.NC):
"""打印带颜色的日志"""
print(f"{color}{message}{Colors.NC}")
def test_endpoint(self, test_case: dict) -> Tuple[bool, str]:
"""测试单个端点"""
endpoint = test_case['endpoint']
expected_key = test_case['expected_key']
description = test_case['description']
required = test_case['required']
url = urljoin(self.base_url, endpoint)
if self.verbose:
self.log(f"测试: {description}", Colors.YELLOW)
self.log(f"URL: {url}", Colors.YELLOW)
try:
response = self.session.get(url)
if response.status_code == 200:
try:
data = response.json()
if expected_key and expected_key not in data:
error_msg = f"响应中缺少期望的键: {expected_key}"
if self.verbose:
self.log(f"响应: {data}", Colors.RED)
return False, error_msg
if self.verbose:
self.log(f"响应: {data}", Colors.GREEN)
return True, "成功"
except ValueError as e:
error_msg = f"响应不是有效的 JSON: {e}"
if self.verbose:
self.log(f"原始响应: {response.text}", Colors.RED)
return False, error_msg
else:
error_msg = f"HTTP {response.status_code}"
if self.verbose:
self.log(f"响应: {response.text}", Colors.RED)
return False, error_msg
except requests.exceptions.Timeout:
return False, "请求超时"
except requests.exceptions.ConnectionError:
return False, "连接失败"
except requests.exceptions.RequestException as e:
return False, f"请求异常: {e}"
def run_tests(self) -> Tuple[int, int]:
"""运行所有测试"""
self.log("🚀 开始 API 烟雾测试", Colors.YELLOW)
self.log(f"目标地址: {self.base_url}", Colors.YELLOW)
self.log(f"超时时间: {self.timeout}", Colors.YELLOW)
self.log("")
total_tests = 0
passed_tests = 0
failed_tests = []
self.log("📋 执行测试用例...", Colors.YELLOW)
self.log("")
for test_case in self.test_cases:
total_tests += 1
success, message = self.test_endpoint(test_case)
if success:
passed_tests += 1
self.log(f"{test_case['description']}", Colors.GREEN)
else:
failed_tests.append((test_case['description'], message))
if test_case['required']:
self.log(f"{test_case['description']} - {message}", Colors.RED)
else:
self.log(f"⚠️ {test_case['description']} - {message}", Colors.YELLOW)
return total_tests, passed_tests, failed_tests
def print_summary(self, total_tests: int, passed_tests: int, failed_tests: List[Tuple[str, str]]):
"""打印测试总结"""
self.log("")
self.log("📊 测试结果", Colors.YELLOW)
self.log(f"总测试数: {total_tests}", Colors.YELLOW)
self.log(f"通过: {passed_tests}", Colors.GREEN)
self.log(f"失败: {len(failed_tests)}", Colors.RED)
if total_tests > 0:
success_rate = (passed_tests * 100) // total_tests
self.log(f"成功率: {success_rate}%", Colors.YELLOW)
if failed_tests:
self.log("")
self.log("失败的测试:", Colors.RED)
for description, message in failed_tests:
self.log(f" - {description}: {message}", Colors.RED)
if passed_tests == total_tests:
self.log("🎉 所有测试通过API 运行正常", Colors.GREEN)
return 0
elif success_rate >= 80:
self.log("⚠️ 大部分测试通过,但有一些问题", Colors.YELLOW)
return 1
else:
self.log("💥 测试失败率过高API 可能存在问题", Colors.RED)
return 2
else:
self.log("❌ 没有执行任何测试", Colors.RED)
return 3
def main():
parser = argparse.ArgumentParser(description='API 烟雾测试脚本')
parser.add_argument('base_url', help='API 基础 URL')
parser.add_argument('-t', '--timeout', type=int, default=10, help='超时时间(秒,默认: 10')
parser.add_argument('-v', '--verbose', action='store_true', help='详细输出')
args = parser.parse_args()
# 创建测试实例
smoke_test = SmokeTest(args.base_url, args.timeout, args.verbose)
# 运行测试
total_tests, passed_tests, failed_tests = smoke_test.run_tests()
# 打印总结
exit_code = smoke_test.print_summary(total_tests, passed_tests, failed_tests)
sys.exit(exit_code)
if __name__ == '__main__':
main()

View File

@@ -1,335 +0,0 @@
#!/usr/bin/env python3
"""
桌面应用功能测试脚本
"""
import os
import sys
import subprocess
import requests
import time
import json
from pathlib import Path
from typing import Dict, Any, List
class DesktopTester:
def __init__(self):
self.project_root = Path(__file__).parent.parent
self.backend_url = "http://127.0.0.1:8000"
self.test_results: List[Dict[str, Any]] = []
def log_test(self, test_name: str, success: bool, message: str = ""):
"""记录测试结果"""
result = {
"test": test_name,
"success": success,
"message": message,
"timestamp": time.time()
}
self.test_results.append(result)
status = "" if success else ""
print(f"{status} {test_name}: {message}")
def test_file_structure(self):
"""测试文件结构完整性"""
print("\n📁 测试文件结构...")
required_files = [
"backend/desktop_main.py",
"backend/desktop_celery.py",
"backend/core/desktop_config.py",
"backend/api/v1/desktop.py",
"src-tauri/src/lib.rs",
"src-tauri/src/backend_manager.rs",
"src-tauri/src/commands.rs",
"src-tauri/src/tray.rs",
"src-tauri/tauri.conf.json",
"src-tauri/Cargo.toml",
"frontend/src/components/DesktopSettings.tsx",
"frontend/src/components/OfflineModeIndicator.tsx",
"scripts/build_backend.py",
"scripts/build_desktop.py",
"scripts/dev_desktop.py",
"scripts/test_desktop.py"
]
missing_files = []
for file_path in required_files:
full_path = self.project_root / file_path
if not full_path.exists():
missing_files.append(file_path)
if missing_files:
self.log_test("文件结构检查", False, f"缺失文件: {', '.join(missing_files)}")
else:
self.log_test("文件结构检查", True, f"所有 {len(required_files)} 个文件都存在")
def test_python_dependencies(self):
"""测试Python依赖"""
print("\n🐍 测试Python依赖...")
required_packages = [
"fastapi",
"uvicorn",
"celery",
"sqlalchemy",
"pydantic",
"psutil",
"requests"
]
missing_packages = []
for package in required_packages:
try:
__import__(package)
except ImportError:
missing_packages.append(package)
if missing_packages:
self.log_test("Python依赖检查", False, f"缺失包: {', '.join(missing_packages)}")
else:
self.log_test("Python依赖检查", True, f"所有 {len(required_packages)} 个包都已安装")
def test_node_dependencies(self):
"""测试Node.js依赖"""
print("\n📦 测试Node.js依赖...")
try:
result = subprocess.run(["node", "--version"], capture_output=True, text=True)
if result.returncode == 0:
self.log_test("Node.js检查", True, f"版本: {result.stdout.strip()}")
else:
self.log_test("Node.js检查", False, "Node.js未安装或不可用")
return
except FileNotFoundError:
self.log_test("Node.js检查", False, "Node.js未安装")
return
try:
result = subprocess.run(["npm", "--version"], capture_output=True, text=True)
if result.returncode == 0:
self.log_test("npm检查", True, f"版本: {result.stdout.strip()}")
else:
self.log_test("npm检查", False, "npm未安装或不可用")
except FileNotFoundError:
self.log_test("npm检查", False, "npm未安装")
def test_rust_environment(self):
"""测试Rust环境"""
print("\n🦀 测试Rust环境...")
try:
result = subprocess.run(["rustc", "--version"], capture_output=True, text=True)
if result.returncode == 0:
self.log_test("Rust检查", True, f"版本: {result.stdout.strip()}")
else:
self.log_test("Rust检查", False, "Rust未安装或不可用")
return
except FileNotFoundError:
self.log_test("Rust检查", False, "Rust未安装")
return
try:
result = subprocess.run(["cargo", "--version"], capture_output=True, text=True)
if result.returncode == 0:
self.log_test("Cargo检查", True, f"版本: {result.stdout.strip()}")
else:
self.log_test("Cargo检查", False, "Cargo未安装或不可用")
except FileNotFoundError:
self.log_test("Cargo检查", False, "Cargo未安装")
def test_tauri_config(self):
"""测试Tauri配置"""
print("\n⚙️ 测试Tauri配置...")
tauri_conf_path = self.project_root / "src-tauri" / "tauri.conf.json"
if not tauri_conf_path.exists():
self.log_test("Tauri配置检查", False, "tauri.conf.json不存在")
return
try:
with open(tauri_conf_path, 'r') as f:
config = json.load(f)
# 检查必要配置项
required_keys = ["package", "build", "tauri"]
missing_keys = [key for key in required_keys if key not in config]
if missing_keys:
self.log_test("Tauri配置检查", False, f"缺失配置项: {', '.join(missing_keys)}")
else:
self.log_test("Tauri配置检查", True, "配置文件格式正确")
except json.JSONDecodeError as e:
self.log_test("Tauri配置检查", False, f"JSON格式错误: {e}")
except Exception as e:
self.log_test("Tauri配置检查", False, f"读取配置失败: {e}")
def test_cargo_config(self):
"""测试Cargo配置"""
print("\n📦 测试Cargo配置...")
cargo_toml_path = self.project_root / "src-tauri" / "Cargo.toml"
if not cargo_toml_path.exists():
self.log_test("Cargo配置检查", False, "Cargo.toml不存在")
return
try:
with open(cargo_toml_path, 'r') as f:
content = f.read()
# 检查必要依赖
required_deps = ["tauri", "serde", "tokio", "reqwest"]
missing_deps = [dep for dep in required_deps if dep not in content]
if missing_deps:
self.log_test("Cargo配置检查", False, f"缺失依赖: {', '.join(missing_deps)}")
else:
self.log_test("Cargo配置检查", True, "依赖配置正确")
except Exception as e:
self.log_test("Cargo配置检查", False, f"读取配置失败: {e}")
def test_backend_api(self):
"""测试后端API需要服务运行"""
print("\n🔧 测试后端API...")
try:
# 测试健康检查端点
response = requests.get(f"{self.backend_url}/health", timeout=5)
if response.status_code == 200:
self.log_test("后端健康检查", True, "服务正常运行")
else:
self.log_test("后端健康检查", False, f"HTTP {response.status_code}")
return
except requests.exceptions.ConnectionError:
self.log_test("后端健康检查", False, "无法连接到后端服务")
return
except Exception as e:
self.log_test("后端健康检查", False, f"请求失败: {e}")
return
# 测试桌面API端点
desktop_endpoints = [
"/api/v1/desktop/system/info",
"/api/v1/desktop/service/status",
"/api/v1/desktop/config"
]
for endpoint in desktop_endpoints:
try:
response = requests.get(f"{self.backend_url}{endpoint}", timeout=5)
if response.status_code == 200:
self.log_test(f"API端点 {endpoint}", True, "响应正常")
else:
self.log_test(f"API端点 {endpoint}", False, f"HTTP {response.status_code}")
except Exception as e:
self.log_test(f"API端点 {endpoint}", False, f"请求失败: {e}")
def test_frontend_build(self):
"""测试前端构建"""
print("\n🌐 测试前端构建...")
frontend_dir = self.project_root / "frontend"
if not frontend_dir.exists():
self.log_test("前端构建测试", False, "frontend目录不存在")
return
try:
# 检查package.json
package_json = frontend_dir / "package.json"
if not package_json.exists():
self.log_test("前端构建测试", False, "package.json不存在")
return
# 尝试构建前端
result = subprocess.run(
["npm", "run", "build"],
cwd=frontend_dir,
capture_output=True,
text=True,
timeout=300 # 5分钟超时
)
if result.returncode == 0:
self.log_test("前端构建测试", True, "构建成功")
else:
self.log_test("前端构建测试", False, f"构建失败: {result.stderr}")
except subprocess.TimeoutExpired:
self.log_test("前端构建测试", False, "构建超时")
except Exception as e:
self.log_test("前端构建测试", False, f"构建异常: {e}")
def run_all_tests(self):
"""运行所有测试"""
print("🧪 开始桌面应用功能测试...")
print("=" * 60)
# 基础环境测试
self.test_file_structure()
self.test_python_dependencies()
self.test_node_dependencies()
self.test_rust_environment()
self.test_tauri_config()
self.test_cargo_config()
# 构建测试
self.test_frontend_build()
# API测试可选需要服务运行
print("\n⚠️ 注意: 以下测试需要后端服务运行")
print("如果后端服务未运行,这些测试将失败")
self.test_backend_api()
# 输出测试结果
self.print_summary()
def print_summary(self):
"""打印测试摘要"""
print("\n" + "=" * 60)
print("📊 测试结果摘要")
print("=" * 60)
total_tests = len(self.test_results)
passed_tests = sum(1 for result in self.test_results if result["success"])
failed_tests = total_tests - passed_tests
print(f"总测试数: {total_tests}")
print(f"通过: {passed_tests}")
print(f"失败: {failed_tests}")
print(f"成功率: {(passed_tests/total_tests*100):.1f}%")
if failed_tests > 0:
print("\n❌ 失败的测试:")
for result in self.test_results:
if not result["success"]:
print(f" - {result['test']}: {result['message']}")
print("\n" + "=" * 60)
def main():
if len(sys.argv) > 1 and sys.argv[1] == "--help":
print("桌面应用功能测试脚本")
print("用法: python test_desktop.py")
print("测试项目:")
print(" - 文件结构完整性")
print(" - Python依赖检查")
print(" - Node.js依赖检查")
print(" - Rust环境检查")
print(" - Tauri配置验证")
print(" - Cargo配置验证")
print(" - 前端构建测试")
print(" - 后端API测试需要服务运行")
return 0
tester = DesktopTester()
tester.run_all_tests()
# 返回失败测试数量作为退出码
failed_count = sum(1 for result in tester.test_results if not result["success"])
return min(failed_count, 255) # 限制退出码范围
if __name__ == "__main__":
sys.exit(main())