Initial commit: skills library
- 70 skills with code and documentation - Add .gitignore (ignore __pycache__, output/, temp/, venv/) - Clean up test intermediates and caches
This commit is contained in:
@@ -0,0 +1,458 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user