|
| 1 | +#!/usr/bin/env python3 |
| 2 | +""" |
| 3 | +从 Issue 中提取项目信息,用 AI 格式化,创建项目文件夹并更新 README。 |
| 4 | +""" |
| 5 | + |
| 6 | +import json |
| 7 | +import os |
| 8 | +import re |
| 9 | +import subprocess |
| 10 | +import sys |
| 11 | +from pathlib import Path |
| 12 | + |
| 13 | +from openai import OpenAI |
| 14 | + |
| 15 | +# ── 读取环境变量 ────────────────────────────────────────────── |
| 16 | +issue_title = os.environ["ISSUE_TITLE"] |
| 17 | +issue_body = os.environ["ISSUE_BODY"] |
| 18 | +issue_number = os.environ["ISSUE_NUMBER"] |
| 19 | +issue_author = os.environ["ISSUE_AUTHOR"] |
| 20 | +api_key = os.environ["AI_API_KEY"] |
| 21 | +base_url = os.environ["AI_BASE_URL"] or "https://api.openai.com/v1" |
| 22 | +model = os.environ["AI_MODEL"] or "gpt-4o" |
| 23 | + |
| 24 | +repo_root = Path(os.getcwd()) |
| 25 | +projects_dir = repo_root / "projects" |
| 26 | +readme_path = repo_root / "README.md" |
| 27 | + |
| 28 | +# ── Step 1: AI 解析 Issue 内容 ──────────────────────────────── |
| 29 | +client = OpenAI(api_key=api_key, base_url=base_url) |
| 30 | + |
| 31 | +system_prompt = """你是一个项目信息提取助手。用户会提交一段自由格式的项目介绍, |
| 32 | +你需要从中提取结构化信息并返回 JSON。 |
| 33 | +
|
| 34 | +请提取以下字段(如果用户没写某项,用合理的默认值): |
| 35 | +- name: 项目名称(简短,用于文件夹名,只允许英文/数字/连字符/下划线) |
| 36 | +- display_name: 项目展示名称(中文也可以) |
| 37 | +- description: 一句话项目描述(50字以内) |
| 38 | +- long_description: 详细项目介绍(保留原始内容的精华) |
| 39 | +- link: 项目链接(GitHub 仓库地址或网页地址) |
| 40 | +- category: 项目分类,必须是以下之一: |
| 41 | + - "配置示例" (配置文件、部署模板) |
| 42 | + - "技能扩展" (Skills/插件/扩展) |
| 43 | + - "实战案例" (使用场景、效率工具、自动化工作流) |
| 44 | + - "教程资源" (教程、指南、学习资料) |
| 45 | + - "工具集成" (API集成、第三方工具对接) |
| 46 | +- author: 提交者 |
| 47 | +- install_command: 安装命令(如果能从内容推断出来) |
| 48 | +- usage_tips: 使用技巧/示例对话 |
| 49 | +
|
| 50 | +只返回 JSON,不要返回其他任何内容。""" |
| 51 | + |
| 52 | +user_prompt = f"""Issue 标题: {issue_title} |
| 53 | +
|
| 54 | +Issue 内容: |
| 55 | +{issue_body} |
| 56 | +
|
| 57 | +提交者: {issue_author}""" |
| 58 | + |
| 59 | +print("🔍 正在用 AI 解析 Issue 内容...") |
| 60 | +response = client.chat.completions.create( |
| 61 | + model=model, |
| 62 | + messages=[ |
| 63 | + {"role": "system", "content": system_prompt}, |
| 64 | + {"role": "user", "content": user_prompt}, |
| 65 | + ], |
| 66 | + temperature=0.1, |
| 67 | +) |
| 68 | + |
| 69 | +content = response.choices[0].message.content.strip() |
| 70 | +# 去掉可能的 markdown 代码块包裹 |
| 71 | +if content.startswith("```"): |
| 72 | + content = re.sub(r"^```\w*\n?", "", content) |
| 73 | + content = re.sub(r"\n?```$", "", content) |
| 74 | + |
| 75 | +data = json.loads(content) |
| 76 | +print(f"✅ 解析完成: {data['name']} ({data['category']})") |
| 77 | + |
| 78 | +# 保存供后续步骤使用 |
| 79 | +with open("/tmp/project_data.json", "w") as f: |
| 80 | + json.dump(data, f, ensure_ascii=False, indent=2) |
| 81 | + |
| 82 | +# ── Step 2: 创建项目文件夹 ──────────────────────────────────── |
| 83 | +projects_dir.mkdir(exist_ok=True) |
| 84 | +project_dir = projects_dir / data["name"] |
| 85 | +project_dir.mkdir(exist_ok=True) |
| 86 | + |
| 87 | +# 生成 README.md |
| 88 | +readme_content = f"""# {data['display_name']} |
| 89 | +
|
| 90 | +> {data['description']} |
| 91 | +
|
| 92 | +**分类**: {data['category']} |
| 93 | +**提交者**: @{data['author']} |
| 94 | +**来源**: [Issue #{issue_number}](https://github.com/xianyu110/awesome-openclaw-tutorial/issues/{issue_number}) |
| 95 | +
|
| 96 | +--- |
| 97 | +
|
| 98 | +## 项目介绍 |
| 99 | +
|
| 100 | +{data['long_description']} |
| 101 | +
|
| 102 | +""" |
| 103 | + |
| 104 | +if data.get("link"): |
| 105 | + readme_content += f"""## 项目链接 |
| 106 | +
|
| 107 | +- **主页**: {data['link']} |
| 108 | +
|
| 109 | +""" |
| 110 | + |
| 111 | +if data.get("install_command"): |
| 112 | + readme_content += f"""## 快速安装 |
| 113 | +
|
| 114 | +```bash |
| 115 | +{data['install_command']} |
| 116 | +``` |
| 117 | +
|
| 118 | +""" |
| 119 | + |
| 120 | +if data.get("usage_tips"): |
| 121 | + readme_content += f"""## 使用示例 |
| 122 | +
|
| 123 | +{data['usage_tips']} |
| 124 | +
|
| 125 | +""" |
| 126 | + |
| 127 | +readme_content += """--- |
| 128 | +
|
| 129 | +*此项目由 AI 自动从 Issue 提取生成,如需修改请提交 PR。* |
| 130 | +""" |
| 131 | + |
| 132 | +(project_dir / "README.md").write_text(readme_content, encoding="utf-8") |
| 133 | +print(f"📁 已创建项目文件夹: {project_dir}") |
| 134 | + |
| 135 | +# ── Step 3: 更新 README.md ──────────────────────────────────── |
| 136 | +readme_text = readme_path.read_text(encoding="utf-8") |
| 137 | + |
| 138 | +# 构造新增条目 |
| 139 | +new_entry = f"- [{data['display_name']}]({project_dir.relative_to(repo_root)}/README.md) - {data['description']}" |
| 140 | + |
| 141 | +# 根据分类决定插入位置 |
| 142 | +category_sections = { |
| 143 | + "配置示例": ("### 📦 配置示例(开箱即用)", "### 🎬 实战场景"), |
| 144 | + "技能扩展": ("### 🔌 Skills 与插件", "### 🎬 实战场景"), |
| 145 | + "实战案例": ("### 🎬 实战场景", "---"), |
| 146 | + "教程资源": ("### 📚 社区教程与资源", None), |
| 147 | + "工具集成": ("### 🔗 工具与集成", "### 🎬 实战场景"), |
| 148 | +} |
| 149 | + |
| 150 | +category, (section_start, section_end) = data["category"], category_sections.get( |
| 151 | + data["category"], ("### 🎬 实战场景", "---") |
| 152 | +) |
| 153 | + |
| 154 | +# 检查分类区块是否存在 |
| 155 | +if section_start not in readme_text: |
| 156 | + # 需要创建新的分类区块 |
| 157 | + insert_pos = readme_text.find("---\n\n## 🤝 贡献指南") |
| 158 | + if insert_pos == -1: |
| 159 | + insert_pos = readme_text.find("---\n\n## 📮 联系方式") |
| 160 | + if insert_pos == -1: |
| 161 | + print("❌ 无法找到合适的插入位置") |
| 162 | + sys.exit(1) |
| 163 | + |
| 164 | + new_section = f"""{section_start} |
| 165 | +
|
| 166 | +{new_entry} |
| 167 | +
|
| 168 | +""" |
| 169 | + readme_text = readme_text[:insert_pos] + new_section + "---\n\n" + readme_text[insert_pos:] |
| 170 | + print(f"📝 已在 README 中创建新分类区块: {category}") |
| 171 | +else: |
| 172 | + # 在已有区块中追加 |
| 173 | + start_idx = readme_text.find(section_start) |
| 174 | + if section_end: |
| 175 | + end_idx = readme_text.find(section_end, start_idx) |
| 176 | + else: |
| 177 | + end_idx = readme_text.find("---\n\n## 🤝 贡献指南", start_idx) |
| 178 | + if end_idx == -1: |
| 179 | + end_idx = readme_text.find("---\n\n## 📮 联系方式", start_idx) |
| 180 | + |
| 181 | + if end_idx == -1 or start_idx == -1: |
| 182 | + print(f"❌ 无法定位分类区块: {category}") |
| 183 | + sys.exit(1) |
| 184 | + |
| 185 | + # 在该区块的最后一条目后插入 |
| 186 | + section_text = readme_text[start_idx:end_idx] |
| 187 | + last_entry_match = re.search(r"(^- .+$)", section_text, re.MULTILINE) |
| 188 | + if last_entry_match: |
| 189 | + insert_idx = start_idx + last_entry_match.end() |
| 190 | + else: |
| 191 | + insert_idx = start_idx + len(section_start) + 1 |
| 192 | + |
| 193 | + readme_text = readme_text[:insert_idx] + "\n" + new_entry + readme_text[insert_idx:] |
| 194 | + print(f"📝 已在 README 区块「{category}」中添加条目") |
| 195 | + |
| 196 | +readme_path.write_text(readme_text, encoding="utf-8") |
| 197 | + |
| 198 | +# ── Step 4: 创建分支并提交 ──────────────────────────────────── |
| 199 | +branch_name = f"add-project-{issue_number}" |
| 200 | +subprocess.run(["git", "checkout", "-b", branch_name], check=True) |
| 201 | +subprocess.run(["git", "add", str(project_dir), str(readme_path)], check=True) |
| 202 | +subprocess.run( |
| 203 | + ["git", "commit", "-m", f"feat: 添加项目 {data['display_name']} (Issue #{issue_number})"], |
| 204 | + check=True, |
| 205 | +) |
| 206 | +subprocess.run(["git", "push", "origin", branch_name], check=True) |
| 207 | + |
| 208 | +print(f"🚀 已推送分支: {branch_name}") |
| 209 | +print("✅ 完成!等待 PR 创建步骤...") |
0 commit comments