import os
import shutil
import json
import platform
import subprocess
import zipfile
import argparse
import re
from pathlib import Path
import sys
# 工具配置
TOOL_VERSION = "2.0"
SUPPORTED_YINGDAO_VERSIONS = ["5.0", "5.1", "5.2", "6.0"]
IGNORE_DIRS = [".git", "__pycache__", "temp", "logs", "venv"]
IGNORE_FILES = [".DS_Store", "thumbs.db", "desktop.ini"]
# 主文件检测模式
MAIN_FILE_PATTERNS = [
r"main\.ya?ml", # main.yaml 或 main.yml
r"main\.json",
r"main\.py",
r"main_\d+\.ya?ml", # main_001.yaml 等
r"main_\d+\.json",
r"main_\d+\.py",
r"project_main\.ya?ml",
r"project_main\.json",
r"project_main\.py"
]
class YingDaoProjectValidator:
def __init__(self, project_path):
self.project_path = Path(project_path)
self.valid = False
self.main_file = None
self.project_json = None
self.validation_errors = []
def validate(self):
"""验证影刀项目结构"""
self._check_project_json()
self._find_main_file()
self._check_required_dirs()
self.valid = not bool(self.validation_errors)
return self.valid
def _check_project_json(self):
"""检查project.json文件"""
project_json_path = self.project_path / "project.json"
if project_json_path.exists():
try:
with open(project_json_path, "r") as f:
self.project_json = json.load(f)
# 检查基本结构
if "projectName" not in self.project_json:
self.validation_errors.append("project.json缺少projectName字段")
except json.JSONDecodeError:
self.validation_errors.append("project.json解析失败")
else:
# 尝试在子目录中查找
for root, _, files in os.walk(self.project_path):
if "project.json" in files:
self.project_json = Path(root) / "project.json"
return
self.validation_errors.append("缺失project.json文件")
def _find_main_file(self):
"""查找主流程文件"""
# 首先检查项目配置中是否指定了主文件
if self.project_json and "mainFile" in self.project_json:
main_file_path = self.project_path / self.project_json["mainFile"]
if main_file_path.exists():
self.main_file = main_file_path
return
# 使用模式匹配查找主文件
for pattern in MAIN_FILE_PATTERNS:
regex = re.compile(pattern, re.IGNORECASE)
for file in self.project_path.glob("**/*"):
if file.is_file() and regex.match(file.name):
self.main_file = file
return
# 如果仍未找到,尝试创建默认主文件
self._create_default_main_file()
def _create_default_main_file(self):
"""创建默认主文件"""
possible_names = [
"main.yaml",
"main.yml",
"main.json",
"main.py"
]
for name in possible_names:
candidate = self.project_path / name
if not candidate.exists():
try:
# 创建简单的YAML主文件
if name.endswith(('.yaml', '.yml')):
candidate.write_text("# 自动生成的影刀主流程\nsteps:\n - name: 开始\n action: print\n args: 'Hello YingDao!'")
# 创建JSON主文件
elif name.endswith('.json'):
candidate.write_text('{"steps": [{"name": "开始", "action": "print", "args": "Hello YingDao!"}]}')
# 创建Python主文件
elif name.endswith('.py'):
candidate.write_text('print("Hello YingDao!")')
self.main_file = candidate
self.validation_errors.append(f"已创建默认主文件: {name}")
return
except Exception as e:
self.validation_errors.append(f"创建默认主文件失败: {str(e)}")
self.validation_errors.append("无法找到或创建主流程文件")
def _check_required_dirs(self):
"""检查必要的目录结构"""
required_dirs = ["modules", "resources", "data"]
for dir_name in required_dirs:
dir_path = self.project_path / dir_name
if not dir_path.exists():
try:
dir_path.mkdir()
self.validation_errors.append(f"已创建缺失目录: {dir_name}")
except Exception as e:
self.validation_errors.append(f"无法创建目录 {dir_name}: {str(e)}")
class YingDaoMigrator:
def __init__(self, project_path):
self.project_path = Path(project_path)
self.migration_package = Path.cwd() / f"{self.project_path.name}_migration_package"
self.dependencies = set()
self.validator = YingDaoProjectValidator(project_path)
def validate_project(self):
"""验证并修复影刀项目"""
print(f"[*] 验证项目: {self.project_path}")
if not self.validator.validate():
print("[!] 项目验证存在问题:")
for error in self.validator.validation_errors:
print(f" - {error}")
# 用户确认是否继续
response = input("项目存在问题,是否继续迁移? (y/n): ").lower()
if response != 'y':
print("迁移已取消")
sys.exit(1)
print(f"[+] 项目验证通过, 主文件: {self.validator.main_file.name}")
def collect_project_files(self):
"""收集项目文件到迁移包"""
print(f"[*] 收集项目文件: {self.project_path.name}")
# 创建目录结构
project_dir = self.migration_package / "project"
project_dir.mkdir(parents=True, exist_ok=True)
# 复制项目文件(排除忽略项)
for item in self.project_path.glob("**/*"):
if any(ignore in item.parts for ignore in IGNORE_DIRS):
continue
if item.name in IGNORE_FILES:
continue
rel_path = item.relative_to(self.project_path)
dest = project_dir / rel_path
if item.is_dir():
dest.mkdir(exist_ok=True)
else:
shutil.copy2(item, dest)
def detect_environment(self):
"""检测Python环境和依赖项"""
print("[*] 检测系统环境...")
# 获取系统信息
env_info = {
"platform": platform.system(),
"platform_version": platform.version(),
"python_version": platform.python_version(),
"yingdao_version": self.detect_yingdao_version(),
"environment_vars": dict(os.environ)
}
# 保存环境信息
with open(self.migration_package / "environment.json", "w") as f:
json.dump(env_info, f, indent=2)
return env_info
def detect_yingdao_version(self):
"""尝试检测影刀版本"""
try:
# 方法1: 检查注册表 (Windows)
if platform.system() == "Windows":
import winreg
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Software\YingDaoRPA") as key:
return winreg.QueryValueEx(key, "Version")[0]
# 方法2: 检查安装目录
common_paths = [
Path("/Applications/YingDaoRPA.app/Contents/Info.plist"),
Path.home() / "AppData/Local/YingDaoRPA"
]
for path in common_paths:
if path.exists():
if path.suffix == ".plist":
# macOS 解析 plist
import plistlib
with open(path, "rb") as f:
return plistlib.load(f).get("CFBundleShortVersionString")
else:
# Windows 版本文件
version_file = path / "version.txt"
if version_file.exists():
return version_file.read_text().strip()
except Exception as e:
print(f"警告: 无法检测影刀版本 - {str(e)}")
return "unknown"
def collect_dependencies(self):
"""收集Python依赖项"""
print("[*] 收集依赖项...")
# 查找所有Python文件
for py_file in self.project_path.glob("**/*.py"):
with open(py_file, "r", encoding="utf-8") as f:
try:
content = f.read()
# 简单解析导入语句 (实际项目应使用AST)
for line in content.splitlines():
line = line.strip()
if line.startswith("import ") or line.startswith("from "):
parts = line.split()
if len(parts) > 1:
module = parts[1].split(".")[0]
if module and module not in ["os", "sys", "json"]:
self.dependencies.add(module)
except UnicodeDecodeError:
continue
# 保存依赖列表
with open(self.migration_package / "requirements.txt", "w") as f:
f.write("\n".join(sorted(self.dependencies)))
print(f"找到 {len(self.dependencies)} 个依赖: {', '.join(sorted(self.dependencies))}")
def create_installer_script(self):
"""创建安装脚本"""
print("[*] 创建安装脚本...")
# 根据平台生成不同的安装脚本
installer_path = self.migration_package / "install.bat" if platform.system() == "Windows" else self.migration_package / "install.sh"
with open(installer_path, "w") as f:
if platform.system() == "Windows":
f.write("""@echo off
echo 正在安装影刀迁移包...
echo.
echo [1/3] 创建项目目录...
set /p PROJECT_DIR="请输入项目安装路径 (默认: %USERPROFILE%\YingDaoProjects): "
if "%PROJECT_DIR%"=="" set PROJECT_DIR=%USERPROFILE%\YingDaoProjects
mkdir "%PROJECT_DIR%" >nul 2>&1
echo [2/3] 复制项目文件...
xcopy /E /I /Y "project" "%PROJECT_DIR%"
echo [3/3] 安装Python依赖...
pip install -r requirements.txt
echo 迁移完成! 项目已安装到: %PROJECT_DIR%
pause
""")
else: # macOS/Linux
f.write("""#!/bin/bash
echo "正在安装影刀迁移包..."
echo
echo "[1/3] 创建项目目录..."
read -p "请输入项目安装路径 (默认: ~/YingDaoProjects): " PROJECT_DIR
PROJECT_DIR=${PROJECT_DIR:-~/YingDaoProjects}
mkdir -p "$ PROJECT_DIR"
echo "[2/3] 复制项目文件..."
cp -R project/* "$ PROJECT_DIR/"
echo "[3/3] 安装Python依赖..."
pip3 install -r requirements.txt
echo "迁移完成! 项目已安装到: $ PROJECT_DIR"
""")
# 设置执行权限 (Unix)
if platform.system() != "Windows":
os.chmod(installer_path, 0o755)
def create_migration_package(self):
"""创建完整的迁移包"""
print(f"[*] 创建迁移包: {self.migration_package}")
# 清理旧包
if self.migration_package.exists():
shutil.rmtree(self.migration_package)
# 创建目录
self.migration_package.mkdir()
# 执行迁移步骤
self.validate_project() # 修改后的验证方法
self.collect_project_files()
self.detect_environment()
self.collect_dependencies()
self.create_installer_script()
# 添加说明文件
with open(self.migration_package / "README.txt", "w") as f:
f.write(f"""影刀应用程序迁移包
版本: {TOOL_VERSION}
生成时间: {subprocess.getoutput('date /t' if platform.system()=='Windows' else 'date')}
项目状态报告:
{'✓ 项目验证通过' if self.validator.valid else '⚠ 项目存在问题'}
主文件: {self.validator.main_file.name if self.validator.main_file else '未找到'}
修复操作:
{chr(10).join(self.validator.validation_errors) if self.validator.validation_errors else '无'}
使用说明:
1. 将此文件夹复制到目标电脑
2. 运行{'install.bat' if platform.system()=='Windows' else 'install.sh'}
3. 按照提示完成安装
注意:
- 目标电脑需要安装相同版本的影刀RPA ({self.detect_yingdao_version()})
- 确保目标电脑已安装Python {platform.python_version()}
""")
# 压缩为ZIP文件
zip_path = self.migration_package.with_suffix(".zip")
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zipf:
for root, _, files in os.walk(self.migration_package):
for file in files:
file_path = Path(root) / file
zipf.write(file_path, arcname=file_path.relative_to(self.migration_package.parent))
print(f"[+] 迁移包创建完成: {zip_path}")
return zip_path
def main():
parser = argparse.ArgumentParser(description="影刀RPA应用程序迁移工具")
parser.add_argument("project_path", help="影刀项目目录路径")
args = parser.parse_args()
try:
migrator = YingDaoMigrator(args.project_path)
migrator.create_migration_package()
except Exception as e:
print(f"[!] 迁移失败: {str(e)}")
if isinstance(e, FileNotFoundError):
print("请确保指定的路径是有效的影刀项目目录")
if __name__ == "__main__":
main()
最新发布