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:
hmo
2026-04-26 19:27:40 +08:00
commit 04db423416
861 changed files with 210414 additions and 0 deletions
+458
View File
@@ -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()