Files
skills/gen_requirements.py
hmo 04db423416 Initial commit: skills library
- 70 skills with code and documentation
- Add .gitignore (ignore __pycache__, output/, temp/, venv/)
- Clean up test intermediates and caches
2026-04-26 19:27:40 +08:00

459 lines
11 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
自动生成技能 requirements.txt 脚本
分析技能的Python代码,提取第三方依赖并生成requirements.txt
用法:
python .opencode/skills/gen_requirements.py # 分析所有无requirements.txt的技能
python .opencode/skills/gen_requirements.py image-service # 只分析指定技能
python .opencode/skills/gen_requirements.py --dry-run # 只显示不写入
"""
import os
import re
import sys
from pathlib import Path
from typing import Dict, Set, List
from collections import defaultdict
# 内置模块(排除)
BUILTIN_MODULES = {
"sys",
"os",
"re",
"json",
"pathlib",
"typing",
"asyncio",
"argparse",
"shutil",
"subprocess",
"tempfile",
"io",
"hashlib",
"uuid",
"datetime",
"zipfile",
"base64",
"collections",
"logging",
"unittest",
"dataclasses",
"contextmanager",
"copy",
"traceback",
"warnings",
"abc",
"functools",
"itertools",
"operator",
"random",
"time",
"gc",
"inspect",
"textwrap",
"types",
"ast",
"linecache",
"locale",
"fnmatch",
"glob",
"signal",
"atexit",
"errno",
"stat",
"pathlib",
"enum",
"csv",
"configparser",
"textwrap",
"string",
"sqlite3",
"urllib",
"urllib.parse",
"http",
"ftplib",
"smtplib",
"email",
"html",
"xml",
"webbrowser",
"math",
"statistics",
"random",
"decimal",
"fractions",
"cmath",
"pprint",
"doctest",
"unittest",
"collections",
"itertools",
"functools",
"operator",
"multiprocessing",
"threading",
"concurrent",
"asyncio",
"socket",
"ssl",
"select",
"signal",
"platform",
"errno",
"ctypes",
"struct",
"array",
"weakref",
"types",
"copy",
"pprint",
"reprlib",
"abc",
"contextvars",
"dataclasses",
"typing",
"collections.abc",
"itertools",
"functools",
"operator",
"pathlib",
"fileinput",
"getopt",
"getpass",
"getopt",
"optparse",
"argparse",
"cmd",
"shlex",
"subprocess",
"pipes",
"node",
"symbol",
"keyword",
"lexer",
"parser",
"ast",
"dis",
"inspect",
"traceback",
"gc",
"weakref",
"gc",
"warnings",
"contextlib",
"abc",
"dataclasses",
"typing",
"collections.abc",
"contextvars",
"numbers",
"abc",
"functools",
"operator",
"pathlib",
}
# 常见第三方包映射(模块名 -> 包名)
MODULE_TO_PACKAGE = {
"PIL": "Pillow",
"cv2": "opencv-python",
"numpy": "numpy",
"pandas": "pandas",
"matplotlib": "matplotlib",
"seaborn": "seaborn",
"sklearn": "scikit-learn",
"torch": "torch",
"tensorflow": "tensorflow",
"transformers": "transformers",
"requests": "requests",
"httpx": "httpx",
"aiohttp": "aiohttp",
"flask": "flask",
"django": "django",
"fastapi": "fastapi",
"pydantic": "pydantic",
"sqlalchemy": "sqlalchemy",
"psycopg2": "psycopg2-binary",
"pymysql": "pymysql",
"redis": "redis",
"pymongo": "pymongo",
"yaml": "pyyaml",
"docx": "python-docx",
"pptx": "python-pptx",
"openpyxl": "openpyxl",
"xlsxwriter": "xlsxwriter",
"pdfplumber": "pdfplumber",
"pypdf": "pypdf",
"pypdf2": "pypdf2",
"reportlab": "reportlab",
"markdown": "markdown",
"jinja2": "jinja2",
"celery": "celery",
"rq": "rq",
"dotenv": "python-dotenv",
"tqdm": "tqdm",
"click": "click",
"typer": "typer",
"rich": "rich",
"colorama": "colorama",
"pillow": "Pillow",
"yaml": "pyyaml",
"edge_tts": "edge-tts",
"manim": "manim",
"whisper": "openai-whisper",
"diffusers": "diffusers",
"accelerate": "accelerate",
"torchvision": "torchvision",
"cv2": "opencv-python",
"albumentations": "albumentations",
"soundfile": "soundfile",
"librosa": "librosa",
"scipy": "scipy",
"skimage": "scikit-image",
"plotly": "plotly",
"altair": "altair",
"bokeh": "bokeh",
"streamlit": "streamlit",
"dash": "dash",
"psycopg2": "psycopg2-binary",
"pymysql": "pymysql",
"sqlite3": None, # builtin
" MySQLdb": None,
"mysql.connector": "mysql-connector-python",
"psycopg2": "psycopg2-binary",
"jinja2": "jinja2",
"mako": "mako",
"email": None, # builtin
"html": None, # builtin
"http": None, # builtin
"urllib": None, # builtin
"xml": None, # builtin
"dbus": "dbus-python",
"gi": "pygobject",
"gi.repository": "pygobject",
"gtk": "pygtk",
"wx": "wxpython",
"tkinter": None, # builtin
"PyQt5": "PyQt5",
"PyQt6": "PyQt6",
"PySide2": "PySide2",
"PySide6": "PySide6",
"kivy": "kivy",
"flask": "flask",
"bottle": "bottle",
"cherrypy": "cherrypy",
"tornado": "tornado",
"webapp2": "webapp2",
"falcon": "falcon",
"sanic": "sanic",
"starlette": "starlette",
"fastapi": "fastapi",
"black": "black",
"ruff": "ruff",
"flake8": "flake8",
"pylint": "pylint",
"mypy": "mypy",
"pytest": "pytest",
"coverage": "coverage",
"tox": "tox",
"poetry": "poetry",
"pip": "pip",
"setuptools": "setuptools",
"wheel": "wheel",
"twine": "twine",
"build": "build",
"jupytext": "jupytext",
"nbformat": "nbformat",
"nbconvert": "nbconvert",
"jupyter": "jupyter",
"ipykernel": "ipykernel",
"ipywidgets": "ipywidgets",
"widgetsnbextension": "widgetsnbextension",
"ipyleaflet": "ipyleaflet",
"plotly": "plotly",
"altair": "altair",
"bqplot": "bqplot",
"ipyvolume": "ipyvolume",
"pythreejs": "pythreejs",
}
def extract_imports(skill_dir: Path) -> Set[str]:
"""从技能目录提取所有第三方import"""
imports = set()
for py_file in skill_dir.rglob("*.py"):
try:
content = py_file.read_text(encoding="utf-8", errors="ignore")
# 匹配 "from xxx import" 和 "import xxx"
for line in content.splitlines():
line = line.strip()
# from module import xxx
match = re.match(r"^from\s+([a-zA-Z_][a-zA-Z0-9_.]*)", line)
if match:
module = match.group(1).split(".")[0]
imports.add(module)
# import xxx
match = re.match(r"^import\s+([a-zA-Z_][a-zA-Z0-9_.]*)", line)
if match:
module = match.group(1).split(" as ")[0].strip()
imports.add(module)
except Exception as e:
print(f" Warning: Could not read {py_file}: {e}", file=sys.stderr)
return imports
def filter_third_party(imports: Set[str]) -> Set[str]:
"""过滤出第三方包"""
third_party = set()
for imp in imports:
# 排除内置模块
if imp.lower() in BUILTIN_MODULES:
continue
# 排除以_开头的私有模块
if imp.startswith("_"):
continue
# 排除本地模块(.开头的目录或本地文件)
if imp.startswith("."):
continue
third_party.add(imp)
return third_party
def resolve_package_name(module: str) -> str:
"""将模块名转换为包名"""
# 直接映射
if module in MODULE_TO_PACKAGE:
return MODULE_TO_PACKAGE[module]
# 尝试将下划线转为横线(common -> common-mark
package = module.replace("_", "-")
return package
def generate_requirements(skill_dir: Path, dry_run: bool = False) -> bool:
"""为单个技能生成requirements.txt"""
skill_name = skill_dir.name
req_file = skill_dir / "requirements.txt"
# 检查是否已有
if req_file.exists():
print(f"[=] {skill_name}: already has requirements.txt, skipping")
return True
# 提取依赖
imports = extract_imports(skill_dir)
third_party = filter_third_party(imports)
if not third_party:
print(f"[-] {skill_name}: no third-party dependencies found")
return True
# 转换为包名
packages = set()
for module in third_party:
pkg = resolve_package_name(module)
if pkg:
packages.add(pkg)
if not packages:
print(f"[-] {skill_name}: no third-party packages to write")
return True
# 生成内容
content_lines = [f"# {skill_name} - dependencies"]
for pkg in sorted(packages):
content_lines.append(f"{pkg}>=0.0.1")
content = "\n".join(content_lines) + "\n"
if dry_run:
print(f"[dry-run] {skill_name}: would create:")
for line in content_lines[1:]:
print(f" {line}")
return True
# 写入文件
req_file.write_text(content, encoding="utf-8")
print(f"[+] {skill_name}: created requirements.txt with {len(packages)} packages")
return True
def main():
import argparse
parser = argparse.ArgumentParser(description="自动生成技能requirements.txt")
parser.add_argument("skill", nargs="*", help="技能名称(可选,多个)")
parser.add_argument("--dry-run", action="store_true", help="只显示不写入")
parser.add_argument("--path", default=".", help="工作目录")
args = parser.parse_args()
base_path = Path(args.path).resolve()
# 尝试多种可能的技能目录位置
possible_paths = [
base_path / ".opencode" / "skills",
base_path / "skills",
base_path,
]
skills_dir = None
for path in possible_paths:
if path.exists() and path.is_dir():
skills_dir = path
break
if not skills_dir:
print(
f"Error: skills directory not found in any of: {possible_paths}",
file=sys.stderr,
)
sys.exit(1)
print(f"Using skills directory: {skills_dir}")
# 确定要处理的技能
if args.skill:
skill_dirs = []
for s in args.skill:
skill_dir = skills_dir / s
if not skill_dir.exists():
print(f"Error: skill not found: {s}", file=sys.stderr)
sys.exit(1)
skill_dirs.append(skill_dir)
else:
# 所有没有requirements.txt的技能
skill_dirs = []
for d in sorted(skills_dir.iterdir()):
if d.is_dir() and not (d / "requirements.txt").exists():
skill_dirs.append(d)
print(f"Found {len(skill_dirs)} skills without requirements.txt")
print()
# 生成
success = 0
for skill_dir in skill_dirs:
if generate_requirements(skill_dir, args.dry_run):
success += 1
print()
print(f"Processed {success}/{len(skill_dirs)} skills")
if args.dry_run:
print("\n(dry-run mode - no files were written)")
if __name__ == "__main__":
main()