#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ dev_workflow.py - Agent Engineering 开发流程辅助工具 用法: python3 dev_workflow.py create-spec --title "标题" --description "描述" python3 dev_workflow.py create-branch --spec .planning/spec.md python3 dev_workflow.py create-pr --branch feature/xxx --title "PR标题" python3 dev_workflow.py record-debate --file .planning/debate.md python3 dev_workflow.py record-review --round 1 --issues '[...]' python3 dev_workflow.py status """ import os import sys import json import argparse import subprocess import re from datetime import datetime from pathlib import Path # 配置 GITEA_URL = os.environ.get('GITEA_URL', 'http://192.168.1.22:8418') GITEA_TOKEN = os.environ.get('GITEA_TOKEN', '') GITEA_REPO = os.environ.get('GITEA_REPO', 'starmen1265/kb-skills') REPO_PATH = Path.home() / 'work' / 'kb-skills' PLANNING_DIR = REPO_PATH / '.planning' # Cross 模式配置 CROSS_AGENTS = ['claude-code', 'codex', 'gemini'] DEBATE_FILE = PLANNING_DIR / 'debate.md' REVIEW_FILE = PLANNING_DIR / 'review.json' def run_cmd(cmd, cwd=None, capture=True): """执行 shell 命令""" result = subprocess.run( cmd, shell=True, cwd=cwd or REPO_PATH, capture_output=capture, text=True ) return result def create_spec(title: str, description: str, from_debate: bool = False) -> dict: """创建 spec 文件""" PLANNING_DIR.mkdir(parents=True, exist_ok=True) # 生成分支名 branch_name = re.sub(r'[^a-z0-9]+', '-', title.lower()).strip('-') # 读取模板 template_path = Path(__file__).parent / 'templates' / 'spec-template.md' if template_path.exists(): content = template_path.read_text(encoding='utf-8') else: content = """# Spec: {{TITLE}} **创建时间:** {{DATE}} **状态:** draft **分支:** feature/{{BRANCH_NAME}} **来源:** {{SOURCE}} --- ## 概述 {{DESCRIPTION}} ## 需求 ### 功能需求 - [ ] **REQ-01**: 待补充 ## 验收标准 1. 待补充 --- ## Cross-Debate 汇总 {{DEBATE_SUMMARY}} --- ## 变更日志 | 日期 | 变更 | |------|------| | {{DATE}} | 初始创建 | """ # 替换变量 now = datetime.now().strftime('%Y-%m-%d %H:%M') content = content.replace('{{TITLE}}', title) content = content.replace('{{DATE}}', now) content = content.replace('{{BRANCH_NAME}}', branch_name) content = content.replace('{{DESCRIPTION}}', description) content = content.replace('{{SOURCE}}', 'Cross-Debate' if from_debate else '用户需求') # 如果有 debate 文件,提取汇总 if from_debate and DEBATE_FILE.exists(): debate_content = DEBATE_FILE.read_text(encoding='utf-8') # 提取共识点部分 match = re.search(r'## 共识点\n(.*?)(?=\n## |$)', debate_content, re.DOTALL) if match: content = content.replace('{{DEBATE_SUMMARY}}', match.group(1).strip()) else: content = content.replace('{{DEBATE_SUMMARY}}', '见 .planning/debate.md') else: content = content.replace('{{DEBATE_SUMMARY}}', '无') # 写入文件 spec_file = PLANNING_DIR / 'spec.md' spec_file.write_text(content, encoding='utf-8') return { "success": True, "spec_file": str(spec_file), "branch_name": branch_name, "title": title, "from_debate": from_debate } def create_branch(spec_file: str) -> dict: """从 spec 创建功能分支""" spec_path = Path(spec_file) if not spec_path.exists(): return {"success": False, "error": f"Spec 文件不存在: {spec_file}"} # 从 spec 提取分支名 content = spec_path.read_text(encoding='utf-8') match = re.search(r'\*\*分支:\*\*\s*feature/([^\n]+)', content) if match: branch_suffix = match.group(1).strip() else: branch_suffix = f"feature-{datetime.now().strftime('%Y%m%d%H%M%S')}" branch_name = f"feature/{branch_suffix}" # 确保在 main 分支 run_cmd('git checkout main') run_cmd('git pull origin main') # 创建功能分支 result = run_cmd(f'git checkout -b {branch_name}') if result.returncode != 0: # 分支可能已存在,尝试切换 result = run_cmd(f'git checkout {branch_name}') if result.returncode != 0: return {"success": False, "error": f"无法创建分支: {result.stderr}"} return { "success": True, "branch": branch_name, "spec_file": spec_file } def create_pr(branch: str, title: str, body: str = "") -> dict: """调用 Gitea API 创建 PR(禁止 auto-merge)""" if not GITEA_TOKEN: return {"success": False, "error": "GITEA_TOKEN 未配置"} # 获取最新提交信息作为 body(如果未提供) if not body: result = run_cmd('git log -1 --pretty=%B') body = result.stdout.strip() if result.returncode == 0 else "" # 添加 Cross 模式说明 review_info = "" if REVIEW_FILE.exists(): try: review_data = json.loads(REVIEW_FILE.read_text()) total_issues = sum(len(v) for v in review_data.get('rounds', {}).values()) rounds = len(review_data.get('rounds', {})) review_info = f"\n\n**Cross-Review:** {rounds} 轮审核,修复 {total_issues} 个问题" except: pass body += review_info body += "\n\n⚠️ **禁止 auto-merge** - 等待人工最终审核" # 调用 Gitea API import urllib.request import urllib.parse import json as json_module url = f"{GITEA_URL}/api/v1/repos/{GITEA_REPO}/pulls" data = { "title": title, "head": branch, "base": "main", "body": body } req = urllib.request.Request( url, data=json_module.dumps(data).encode('utf-8'), headers={ 'Authorization': f'token {GITEA_TOKEN}', 'Content-Type': 'application/json' }, method='POST' ) try: with urllib.request.urlopen(req, timeout=30) as response: result = json_module.loads(response.read().decode('utf-8')) return { "success": True, "pr_number": result.get("number"), "pr_url": result.get("html_url"), "branch": branch, "warning": "禁止设置 auto-merge" } except urllib.error.HTTPError as e: error_body = e.read().decode('utf-8') if e.fp else str(e) return {"success": False, "error": f"API 错误: {e.code} - {error_body}"} except Exception as e: return {"success": False, "error": f"请求失败: {str(e)}"} def record_debate(file: str = None) -> dict: """记录 Cross-Debate 结果""" PLANNING_DIR.mkdir(parents=True, exist_ok=True) debate_content = None if file: file_path = Path(file) if file_path.exists(): debate_content = file_path.read_text(encoding='utf-8') if debate_content: DEBATE_FILE.write_text(debate_content, encoding='utf-8') return { "success": True, "file": str(DEBATE_FILE), "message": "Cross-Debate 结果已记录" } else: # 创建模板 template = f"""# Cross-Debate 结果 **创建时间:** {datetime.now().strftime('%Y-%m-%d %H:%M')} **需求:** --- ## Agent 方案 ### Claude Code(保守派) **方案:** **理由:** --- ### Codex(全面派) **方案:** **理由:** --- ### Gemini(快速派) **方案:** **理由:** --- ## 共识点 - ✅ - ✅ ## 分歧点 | Agent | 方案 | 理由 | |-------|------|------| | | | | ## 用户决策 **选择方案:** --- """ DEBATE_FILE.write_text(template, encoding='utf-8') return { "success": True, "file": str(DEBATE_FILE), "message": "Cross-Debate 模板已创建,请填写" } def record_review(round_num: int, issues: str = None) -> dict: """记录 Cross-Review 结果""" PLANNING_DIR.mkdir(parents=True, exist_ok=True) # 读取现有数据 review_data = {"rounds": {}} if REVIEW_FILE.exists(): try: review_data = json.loads(REVIEW_FILE.read_text()) except: pass # 解析 issues issues_list = [] if issues: try: issues_list = json.loads(issues) except: # 简单格式:每行一个问题 issues_list = [{"level": "medium", "desc": line} for line in issues.split('\n') if line.strip()] # 添加本轮结果 round_key = f"round_{round_num}" review_data["rounds"][round_key] = { "timestamp": datetime.now().isoformat(), "issues": issues_list } # 统计 review_data["stats"] = { "total_rounds": len(review_data["rounds"]), "total_issues": sum(len(v.get("issues", [])) for v in review_data["rounds"].values()) } REVIEW_FILE.write_text(json.dumps(review_data, ensure_ascii=False, indent=2), encoding='utf-8') return { "success": True, "file": str(REVIEW_FILE), "round": round_num, "issues_count": len(issues_list), "stats": review_data["stats"] } def get_status() -> dict: """获取当前开发状态""" result = { "current_branch": None, "spec_exists": False, "debate_exists": False, "review_exists": False, "planning_exists": False, "uncommitted_changes": False, "cross_mode": False } # 当前分支 branch_result = run_cmd('git branch --show-current') if branch_result.returncode == 0: result["current_branch"] = branch_result.stdout.strip() # 各种文件状态 result["spec_exists"] = (PLANNING_DIR / 'spec.md').exists() result["debate_exists"] = DEBATE_FILE.exists() result["review_exists"] = REVIEW_FILE.exists() result["planning_exists"] = PLANNING_DIR.exists() result["cross_mode"] = result["debate_exists"] or result["review_exists"] # 未提交的更改 status_result = run_cmd('git status --porcelain') result["uncommitted_changes"] = bool(status_result.stdout.strip()) # 如果有 review 数据,显示统计 if REVIEW_FILE.exists(): try: review_data = json.loads(REVIEW_FILE.read_text()) result["review_stats"] = review_data.get("stats", {}) except: pass return result def main(): parser = argparse.ArgumentParser(description='dev-workflow 辅助工具(Cross 模式)') subparsers = parser.add_subparsers(dest='command', help='命令') # create-spec spec_parser = subparsers.add_parser('create-spec', help='创建 spec 文件') spec_parser.add_argument('--title', required=True, help='功能标题') spec_parser.add_argument('--description', default='', help='功能描述') spec_parser.add_argument('--from-debate', action='store_true', help='从 Cross-Debate 结果生成') # create-branch branch_parser = subparsers.add_parser('create-branch', help='创建功能分支') branch_parser.add_argument('--spec', required=True, help='spec 文件路径') # create-pr pr_parser = subparsers.add_parser('create-pr', help='创建 PR(禁止 auto-merge)') pr_parser.add_argument('--branch', required=True, help='分支名') pr_parser.add_argument('--title', required=True, help='PR 标题') pr_parser.add_argument('--body', default='', help='PR 描述') # record-debate debate_parser = subparsers.add_parser('record-debate', help='记录 Cross-Debate 结果') debate_parser.add_argument('--file', help='输入文件路径(可选)') # record-review review_parser = subparsers.add_parser('record-review', help='记录 Cross-Review 结果') review_parser.add_argument('--round', type=int, required=True, help='审核轮次') review_parser.add_argument('--issues', help='问题列表(JSON 或简单文本)') # status subparsers.add_parser('status', help='查看状态') # stats subparsers.add_parser('stats', help='查看 Cross 模式统计报告') # record-event event_parser = subparsers.add_parser('record-event', help='记录 Cross 模式事件') event_parser.add_argument('--type', required=True, choices=['debate', 'review', 'develop', 'fix', 'other'], help='事件类型') event_parser.add_argument('--mode', default='cross', choices=['cross', 'normal'], help='模式') event_parser.add_argument('--metadata', default='{}', help='元数据(JSON)') event_parser.add_argument('--status', default='success', help='状态') args = parser.parse_args() if args.command == 'create-spec': result = create_spec(args.title, args.description, args.from_debate) elif args.command == 'create-branch': result = create_branch(args.spec) elif args.command == 'create-pr': result = create_pr(args.branch, args.title, args.body) elif args.command == 'record-debate': result = record_debate(args.file) elif args.command == 'record-review': result = record_review(args.round, args.issues) elif args.command == 'status': result = get_status() elif args.command == 'stats': # 导入 stats 模块 sys.path.insert(0, str(Path(__file__).parent)) from stats import get_stats, format_stats_report stats = get_stats() print(format_stats_report(stats)) result = stats elif args.command == 'record-event': # 导入 stats 模块 sys.path.insert(0, str(Path(__file__).parent)) from stats import record_event try: metadata = json.loads(args.metadata) except: metadata = {} result = {"success": record_event(args.type, args.mode, metadata, args.status)} else: parser.print_help() return print(json.dumps(result, ensure_ascii=False, indent=2)) if __name__ == '__main__': main()