#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 项目初始化与部署工作流管理器 提供状态管理、步骤执行和错误恢复功能 """ import os import sys import json import shutil import subprocess from datetime import datetime from pathlib import Path from typing import Dict, List, Optional, Tuple from dataclasses import dataclass, asdict from enum import Enum class StepStatus(Enum): PENDING = "⏭️ 未开始" IN_PROGRESS = "⏸️ 进行中" COMPLETED = "✅ 已完成" FAILED = "❌ 失败" SKIPPED = "⏭️ 跳过" class ProjectType(Enum): DOCKER_WEB = "docker_web" DOCKER_API = "docker_api" DOCKER_MICROSERVICES = "docker_microservices" DESKTOP = "desktop" MOBILE = "mobile" CLI = "cli" UNKNOWN = "unknown" class TechStack(Enum): JAVA_SPRING = "java_spring" NODE_EXPRESS = "node_express" NODE_NEST = "node_nest" PYTHON_DJANGO = "python_django" PYTHON_FASTAPI = "python_fastapi" GO_GIN = "go_gin" GO_FIBER = "go_fiber" REACT = "react" VUE = "vue" ANGULAR = "angular" UNKNOWN = "unknown" @dataclass class ProjectInfo: """项目信息""" name: str directory: str git_url: str port_range: Optional[str] = None project_type: ProjectType = ProjectType.UNKNOWN tech_stack: List[TechStack] = None def __post_init__(self): if self.tech_stack is None: self.tech_stack = [] @dataclass class Step: """工作流步骤""" id: int name: str description: str applicable_types: List[ProjectType] status: StepStatus = StepStatus.PENDING start_time: Optional[str] = None end_time: Optional[str] = None notes: str = "" error_info: Optional[str] = None class WorkflowManager: """工作流管理器""" # 完整的20个核心步骤 ALL_TYPES = [ProjectType.DOCKER_WEB, ProjectType.DOCKER_API, ProjectType.DOCKER_MICROSERVICES, ProjectType.DESKTOP, ProjectType.MOBILE, ProjectType.CLI] DOCKER_TYPES = [ProjectType.DOCKER_WEB, ProjectType.DOCKER_API, ProjectType.DOCKER_MICROSERVICES] # 关键时序:初始基线启动(3,内部) → 初始界面截图(4,内部) → 面板改造(6-11) → 产品构建启动(15) # → 健康验证(16) → 产品截图与录屏(17) → README/归档(18) → 交付核查(19) CORE_STEPS = [ Step(0, "克隆项目", "从Git仓库克隆项目到本地,不修改任何业务代码", ALL_TYPES), Step(1, "项目完整度评估", "评估项目完整度、依赖情况与可运行性", ALL_TYPES), Step(2, "项目整体分析", "分析项目结构、技术栈、启动方式和部署策略", ALL_TYPES), Step(3, "初始版本基线启动(内部)", "在改动任何代码前,用项目现有启动方式跑通初始版本并确认可访问;仅用于内部质量核对", ALL_TYPES), Step(4, "初始界面基线截图(内部)", "代码零改动状态下采集初始界面作为内部对比基线;严禁进入交付包或使用 before 等对外文件名", ALL_TYPES), Step(5, "Docker配置生成", "生成/完善 Docker Compose 配置文件", DOCKER_TYPES), Step(6, "页面信息与品牌名称修改", "替换页面品牌信息并建立科技感面板基础样式", ALL_TYPES), Step(7, "Logo生成与替换", "生成并替换项目 Logo、Favicon,适配深色面板", ALL_TYPES), Step(8, "版权与第三方标识规范", "清理依法可移除的公司名称、LOGO、商标、水印、联系方式等专属标识;MIT 等许可证要求保留的版权与许可文本必须随源码保留", ALL_TYPES), Step(9, "页脚、导航与社区链接", "导航、页脚改为半透明悬浮面板风格,并放置开源社区与 GCC AI Coding 链接", ALL_TYPES), Step(10, "登录页面", "改成居中玻璃面板、发光输入框、动态背景", ALL_TYPES), Step(11, "AI对话功能", "实现前端AI对话界面与指定服务商API对接及使用教程", ALL_TYPES), Step(12, "示例数据", "生成和导入示例数据,数据卡片/列表面板承载", DOCKER_TYPES), Step(13, "CORS配置检查", "检查并配置 CORS 设置", DOCKER_TYPES), Step(14, "端口检查与分配", "检查端口占用并分配端口", DOCKER_TYPES), Step(15, "改造后构建与启动", "Docker项目构建镜像启动容器;非Docker项目手动构建并启动改造版", ALL_TYPES), Step(16, "健康验证", "验证服务健康、面板UI审计0违规、全功能走查无报错", ALL_TYPES), Step(17, "产品截图与全功能录屏", "采集当前产品全套高清界面截图(中性文件名)和全功能演示录屏;内部基线素材不进入交付物", ALL_TYPES), Step(18, "标准化README与版本归档", "编写标准化 README(引用最终素材、含架构图与部署教程);不主动写来源/致谢宣传,并按授权决定是否提交", ALL_TYPES), Step(19, "交付物核查", "按7项交付物清单与7条验收标准做交付前阻断式核查", ALL_TYPES), ] # 旧版本步骤名 → 新版本步骤名(用于历史状态迁移) STEP_NAME_ALIASES = { "构建与启动": "改造后构建与启动", "素材录制归档": "产品截图与全功能录屏", "原始项目基线启动": "初始版本基线启动(内部)", "改造前基线截图": "初始界面基线截图(内部)", "原作者信息与版权痕迹合规处理": "版权与第三方标识规范", "页脚与导航增强": "页脚、导航与社区链接", "改造后截图与全功能录屏": "产品截图与全功能录屏", "标准化README与提交": "标准化README与版本归档", } def __init__(self, project_dir: str): self.project_dir = Path(project_dir) self.claude_dir = self.project_dir / ".Codex" self.state_file = self.claude_dir / "workflow_state.json" self.error_file = self.claude_dir / "error_log.md" self.project_info: Optional[ProjectInfo] = None self.steps: List[Step] = [] def init_project(self, name: str, git_url: str, port_range: Optional[str] = None): """初始化项目信息""" self.project_info = ProjectInfo( name=name, directory=str(self.project_dir), git_url=git_url, port_range=port_range ) self.steps = [Step(s.id, s.name, s.description, s.applicable_types) for s in self.CORE_STEPS] self._ensure_directories() self._save_state() def _ensure_directories(self): """确保必要的目录存在""" self.claude_dir.mkdir(exist_ok=True) (self.claude_dir / 'reports').mkdir(exist_ok=True) def _save_state(self): """保存状态到JSON文件""" def enum_to_value(obj): if isinstance(obj, Enum): return obj.value if isinstance(obj, list): return [enum_to_value(item) for item in obj] if isinstance(obj, dict): return {k: enum_to_value(v) for k, v in obj.items()} return obj project_info_dict = None if self.project_info: project_info_dict = asdict(self.project_info) project_info_dict["project_type"] = project_info_dict["project_type"].value project_info_dict["tech_stack"] = [ts.value for ts in project_info_dict["tech_stack"]] state = { "project_info": project_info_dict, "steps": [ { "id": s.id, "name": s.name, "status": s.status.value, "start_time": s.start_time, "end_time": s.end_time, "notes": s.notes, "error_info": s.error_info } for s in self.steps ], "last_updated": datetime.now().isoformat() } with open(self.state_file, 'w', encoding='utf-8') as f: json.dump(state, f, ensure_ascii=False, indent=2) def load_state(self) -> bool: """从JSON文件加载状态""" if not self.state_file.exists(): return False try: with open(self.state_file, 'r', encoding='utf-8') as f: state = json.load(f) if state.get("project_info"): info = state["project_info"] self.project_info = ProjectInfo( name=info["name"], directory=info["directory"], git_url=info["git_url"], port_range=info.get("port_range"), project_type=ProjectType(info.get("project_type", "unknown")), tech_stack=[TechStack(ts) for ts in info.get("tech_stack", [])] ) # 以新 CORE_STEPS 为权威骨架;旧状态按“步骤名(含别名)”映射进度, # 避免编号错位导致已完成的步骤被重置。 old_steps = state.get("steps", []) name_to_state: Dict[str, dict] = {} for s in old_steps: canon = self.STEP_NAME_ALIASES.get(s.get("name", ""), s.get("name", "")) name_to_state[canon] = s self.steps = [] for cs in self.CORE_STEPS: prev = name_to_state.get(cs.name) if prev is not None: self.steps.append(Step( id=cs.id, name=cs.name, description=cs.description, applicable_types=cs.applicable_types, status=StepStatus(prev.get("status", StepStatus.PENDING.value)), start_time=prev.get("start_time"), end_time=prev.get("end_time"), notes=prev.get("notes", ""), error_info=prev.get("error_info"), )) else: # 新增步骤默认未开始 self.steps.append(Step(cs.id, cs.name, cs.description, cs.applicable_types)) return True except Exception as e: self._log_error(f"加载状态失败: {e}") return False def get_current_step(self) -> Optional[Step]: """获取当前步骤""" for step in self.steps: if step.status == StepStatus.IN_PROGRESS: return step if step.status == StepStatus.PENDING: return step return None def start_step(self, step_id: int) -> bool: """开始执行步骤""" step = next((s for s in self.steps if s.id == step_id), None) if not step: return False # 检查步骤是否适用于当前项目类型 if self.project_info and self.project_info.project_type != ProjectType.UNKNOWN: if self.project_info.project_type not in step.applicable_types: step.status = StepStatus.SKIPPED step.notes = f"步骤不适用于项目类型: {self.project_info.project_type.value}" self._save_state() return True step.status = StepStatus.IN_PROGRESS step.start_time = datetime.now().isoformat() self._save_state() return True def complete_step(self, step_id: int, notes: str = ""): """完成步骤""" step = next((s for s in self.steps if s.id == step_id), None) if step: step.status = StepStatus.COMPLETED step.end_time = datetime.now().isoformat() step.notes = notes self._save_state() def fail_step(self, step_id: int, error_info: str): """标记步骤失败""" step = next((s for s in self.steps if s.id == step_id), None) if step: step.status = StepStatus.FAILED step.end_time = datetime.now().isoformat() step.error_info = error_info self._save_state() self._log_error(f"步骤 {step_id} ({step.name}) 失败: {error_info}") def skip_step(self, step_id: int, reason: str = ""): """跳过步骤""" step = next((s for s in self.steps if s.id == step_id), None) if step: step.status = StepStatus.SKIPPED step.notes = reason self._save_state() def _log_error(self, message: str): """记录错误到错误日志""" timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") error_entry = f"\n## [{timestamp}]\n{message}\n\n" with open(self.error_file, 'a', encoding='utf-8') as f: f.write(error_entry) def get_progress(self) -> Dict: """获取进度信息""" total = len(self.steps) completed = sum(1 for s in self.steps if s.status == StepStatus.COMPLETED) failed = sum(1 for s in self.steps if s.status == StepStatus.FAILED) skipped = sum(1 for s in self.steps if s.status == StepStatus.SKIPPED) pending = sum(1 for s in self.steps if s.status == StepStatus.PENDING) return { "total": total, "completed": completed, "failed": failed, "skipped": skipped, "pending": pending, "percentage": (completed / total * 100) if total > 0 else 0 } def can_resume(self) -> Tuple[bool, Optional[int]]: """检查是否可以恢复执行""" if not self.state_file.exists(): return False, None self.load_state() # 检查是否有失败的步骤 failed_step = next((s for s in self.steps if s.status == StepStatus.FAILED), None) if failed_step: return True, failed_step.id # 检查是否有进行中的步骤 in_progress = next((s for s in self.steps if s.status == StepStatus.IN_PROGRESS), None) if in_progress: return True, in_progress.id # 检查是否有未开始的步骤 pending = next((s for s in self.steps if s.status == StepStatus.PENDING), None) if pending: return True, pending.id return False, None def generate_report(self) -> str: """生成执行报告""" progress = self.get_progress() report = f"""# 工作流执行报告 ## 项目信息 - **项目名称**: {self.project_info.name if self.project_info else 'N/A'} - **项目目录**: {self.project_info.directory if self.project_info else 'N/A'} - **Git地址**: {self.project_info.git_url if self.project_info else 'N/A'} - **项目类型**: {self.project_info.project_type.value if self.project_info else 'N/A'} ## 执行进度 - **总步骤**: {progress['total']} - **已完成**: {progress['completed']} - **失败**: {progress['failed']} - **跳过**: {progress['skipped']} - **待执行**: {progress['pending']} - **完成度**: {progress['percentage']:.1f}% ## 步骤详情 """ for step in self.steps: report += f"\n### 步骤 {step.id}: {step.name}\n" report += f"- **状态**: {step.status.value}\n" if step.start_time: report += f"- **开始时间**: {step.start_time}\n" if step.end_time: report += f"- **完成时间**: {step.end_time}\n" if step.notes: report += f"- **说明**: {step.notes}\n" if step.error_info: report += f"- **错误**: {step.error_info}\n" return report # 技术栈检测工具 class TechStackDetector: """技术栈检测器""" DETECTION_RULES = { TechStack.JAVA_SPRING: ["pom.xml", "build.gradle", "src/main/java"], TechStack.NODE_EXPRESS: ["package.json", "app.js", "server.js"], TechStack.NODE_NEST: ["nest-cli.json", "@nestjs"], TechStack.PYTHON_DJANGO: ["manage.py", "django", "requirements.txt"], TechStack.PYTHON_FASTAPI: ["fastapi", "main.py"], TechStack.GO_GIN: ["go.mod", "gin"], TechStack.GO_FIBER: ["go.mod", "fiber"], TechStack.REACT: ["package.json", "react", "src/App.js", "src/App.tsx", "vite.config.ts", "vite.config.js"], TechStack.VUE: ["package.json", "vue", "vue.config.js", "vite.config.ts", "vite.config.js"], TechStack.ANGULAR: ["angular.json", "@angular"], } @classmethod def detect(cls, project_dir: str) -> List[TechStack]: """检测项目技术栈""" detected = [] project_path = Path(project_dir) for tech, indicators in cls.DETECTION_RULES.items(): found = False for indicator in indicators: # 递归检查文件存在 for _ in project_path.rglob(indicator): found = True break if found: break # 递归检查文件内容 for file in project_path.rglob("*.json"): try: with open(file, 'r', encoding='utf-8') as f: content = f.read() if indicator in content: found = True break except: pass if found: break if found and tech not in detected: detected.append(tech) return detected if detected else [TechStack.UNKNOWN] # 项目类型判断器 class ProjectTypeDetector: """项目类型检测器""" @classmethod def detect(cls, project_dir: str, tech_stacks: List[TechStack]) -> ProjectType: """检测项目类型""" project_path = Path(project_dir) # 检查桌面应用特征 desktop_indicators = ["electron", "qt", "swing", "wpf", ".desktop"] for indicator in desktop_indicators: if any(indicator in str(tech.value).lower() for tech in tech_stacks): return ProjectType.DESKTOP # 检查移动应用特征 mobile_indicators = ["react-native", "flutter", "android", "ios"] for indicator in mobile_indicators: if any(indicator in str(tech.value).lower() for tech in tech_stacks): return ProjectType.MOBILE # 检查Docker配置 if (project_path / "Dockerfile").exists() or (project_path / "docker-compose.yml").exists(): # 检查微服务特征 if (project_path / "services").exists() or len(list(project_path.glob("**/Dockerfile"))) > 1: return ProjectType.DOCKER_MICROSERVICES return ProjectType.DOCKER_WEB # 检查API服务特征 if any(tech in [TechStack.NODE_EXPRESS, TechStack.NODE_NEST, TechStack.PYTHON_FASTAPI, TechStack.GO_GIN] for tech in tech_stacks): return ProjectType.DOCKER_API return ProjectType.UNKNOWN def main(): """命令行入口""" if len(sys.argv) < 2: print("Usage: workflow_manager.py [args...]") print("Commands: init, status, resume, report") sys.exit(1) command = sys.argv[1] if command == "init": if len(sys.argv) < 5: print("Usage: workflow_manager.py init [port_range]") sys.exit(1) project_dir = sys.argv[2] name = sys.argv[3] git_url = sys.argv[4] port_range = sys.argv[5] if len(sys.argv) > 5 else None manager = WorkflowManager(project_dir) manager.init_project(name, git_url, port_range) print(f"项目已初始化: {name}") elif command == "start_step": if len(sys.argv) < 4: print("Usage: workflow_manager.py start_step ") sys.exit(1) project_dir = sys.argv[2] step_id = int(sys.argv[3]) manager = WorkflowManager(project_dir) if manager.load_state(): if manager.start_step(step_id): print(f"步骤 {step_id} 已开始") else: print(f"步骤 {step_id} 开始失败") else: print("未找到状态文件") elif command == "complete_step": if len(sys.argv) < 4: print("Usage: workflow_manager.py complete_step [notes]") sys.exit(1) project_dir = sys.argv[2] step_id = int(sys.argv[3]) notes = sys.argv[4] if len(sys.argv) > 4 else "" manager = WorkflowManager(project_dir) if manager.load_state(): manager.complete_step(step_id, notes) print(f"步骤 {step_id} 已完成") else: print("未找到状态文件") elif command == "detect": if len(sys.argv) < 3: print("Usage: workflow_manager.py detect ") sys.exit(1) project_dir = sys.argv[2] manager = WorkflowManager(project_dir) if manager.load_state(): tech_stack = TechStackDetector.detect(project_dir) project_type = ProjectTypeDetector.detect(project_dir, tech_stack) manager.project_info.tech_stack = tech_stack manager.project_info.project_type = project_type manager.start_step(2) manager.complete_step(2, f"Detected tech stack: {[ts.value for ts in tech_stack]}, project type: {project_type.value}") print(f"Tech stack: {[ts.value for ts in tech_stack]}, Project type: {project_type.value}") else: print("未找到状态文件") elif command == "status": if len(sys.argv) < 3: print("Usage: workflow_manager.py status ") sys.exit(1) project_dir = sys.argv[2] manager = WorkflowManager(project_dir) if manager.load_state(): progress = manager.get_progress() print(f"进度: {progress['completed']}/{progress['total']} ({progress['percentage']:.1f}%)") current = manager.get_current_step() if current: print(f"当前步骤: 步骤 {current.id} - {current.name}") else: print("未找到状态文件") elif command == "resume": if len(sys.argv) < 3: print("Usage: workflow_manager.py resume ") sys.exit(1) project_dir = sys.argv[2] manager = WorkflowManager(project_dir) can_resume, step_id = manager.can_resume() if can_resume: print(f"可以从步骤 {step_id} 恢复执行") else: print("无法恢复,请重新初始化") elif command == "report": if len(sys.argv) < 3: print("Usage: workflow_manager.py report ") sys.exit(1) project_dir = sys.argv[2] manager = WorkflowManager(project_dir) if manager.load_state(): print(manager.generate_report()) else: print("未找到状态文件") if __name__ == "__main__": main()