#!/usr/bin/env python3 """ API 文档提取脚本 支持 FastAPI、TypeScript、Python 模块 """ import subprocess import sys import json from pathlib import Path from typing import Dict, Any, Optional # 项目配置 PROJECTS = { 'backend': { 'path': Path('D:/dongdiankaifa9/backend'), 'type': 'fastapi', 'entry': 'app.main:app', 'output': 'api-docs/backend' }, 'frontend': { 'path': Path('D:/dongdiankaifa9/frontend'), 'type': 'typescript', 'entries': ['lib', 'hooks', 'components'], 'output': 'api-docs/frontend' }, 'hydro_generator_module': { 'path': Path('D:/dongdiankaifa9/hydro_generator_module'), 'type': 'python', 'output': 'api-docs/hydro_generator' }, 'multiphysics_network': { 'path': Path('D:/dongdiankaifa9/multiphysics_network'), 'type': 'python', 'output': 'api-docs/multiphysics' } } def extract_fastapi(name: str, config: Dict[str, Any], output_base: Path) -> bool: """提取 FastAPI OpenAPI 文档""" path = config['path'] output_dir = output_base / config['output'] output_dir.mkdir(parents=True, exist_ok=True) # 添加路径到 sys.path if str(path) not in sys.path: sys.path.insert(0, str(path)) try: # 动态导入 app from app.main import app # 获取 OpenAPI schema openapi_schema = app.openapi() # 保存 JSON json_path = output_dir / 'openapi.json' with open(json_path, 'w', encoding='utf-8') as f: json.dump(openapi_schema, f, indent=2, ensure_ascii=False) # 生成 Markdown 摘要 md_path = output_dir / 'API_SUMMARY.md' generate_api_markdown(openapi_schema, md_path) endpoints = len(openapi_schema.get('paths', {})) print(f" ✓ Extracted {endpoints} endpoints → {output_dir}") return True except ImportError as e: print(f" ✗ Import error: {e}") return False except Exception as e: print(f" ✗ Error: {e}") return False def generate_api_markdown(schema: Dict, output_path: Path): """从 OpenAPI schema 生成 Markdown""" lines = [ f"# {schema.get('info', {}).get('title', 'API Reference')}", "", f"Version: {schema.get('info', {}).get('version', '1.0.0')}", "", "## Endpoints", "", "| Method | Path | Summary |", "|--------|------|---------|" ] for path, methods in schema.get('paths', {}).items(): for method, details in methods.items(): if method in ('get', 'post', 'put', 'delete', 'patch'): summary = details.get('summary', details.get('operationId', '-')) lines.append(f"| `{method.upper()}` | `{path}` | {summary} |") lines.extend([ "", "## Schemas", "" ]) for name, schema_def in schema.get('components', {}).get('schemas', {}).items(): lines.append(f"### {name}") lines.append("") if 'properties' in schema_def: lines.append("| Property | Type | Required |") lines.append("|----------|------|----------|") required = schema_def.get('required', []) for prop, prop_def in schema_def['properties'].items(): prop_type = prop_def.get('type', prop_def.get('$ref', 'any')) is_required = '✓' if prop in required else '' lines.append(f"| `{prop}` | {prop_type} | {is_required} |") lines.append("") with open(output_path, 'w', encoding='utf-8') as f: f.write('\n'.join(lines)) def extract_typescript(name: str, config: Dict[str, Any], output_base: Path) -> bool: """提取 TypeScript 文档 (TypeDoc)""" path = config['path'] output_dir = output_base / config['output'] # 检查 TypeDoc 是否已安装 try: result = subprocess.run( ['npx', 'typedoc', '--version'], cwd=path, capture_output=True, text=True ) if result.returncode != 0: print(f" ⚠ TypeDoc not installed, installing...") subprocess.run( ['npm', 'install', '--save-dev', 'typedoc', 'typedoc-plugin-markdown'], cwd=path, check=True ) except FileNotFoundError: print(f" ✗ npm/npx not found") return False # 运行 TypeDoc try: entries = config.get('entries', ['lib']) cmd = [ 'npx', 'typedoc', '--plugin', 'typedoc-plugin-markdown', '--out', str(output_dir), '--entryPointStrategy', 'expand', '--exclude', '**/node_modules/**', '--exclude', '**/*.test.*', '--readme', 'none' ] for entry in entries: entry_path = path / entry if entry_path.exists(): cmd.extend(['--entryPoints', str(entry_path)]) result = subprocess.run(cmd, cwd=path, capture_output=True, text=True) if result.returncode == 0: print(f" ✓ TypeDoc generated → {output_dir}") return True else: print(f" ✗ TypeDoc error: {result.stderr[:200]}") return False except Exception as e: print(f" ✗ Error: {e}") return False def extract_python_module(name: str, config: Dict[str, Any], output_base: Path) -> bool: """提取 Python 模块文档 (pdoc)""" path = config['path'] output_dir = output_base / config['output'] module_name = path.name # 检查 pdoc try: subprocess.run(['pdoc', '--version'], capture_output=True, check=True) except (FileNotFoundError, subprocess.CalledProcessError): print(f" ⚠ pdoc not installed, installing...") subprocess.run([sys.executable, '-m', 'pip', 'install', 'pdoc'], check=True) # 运行 pdoc try: result = subprocess.run( [ 'pdoc', module_name, '--output-dir', str(output_dir), '--format', 'markdown' ], cwd=path.parent, capture_output=True, text=True ) if result.returncode == 0: # 统计生成的文件 md_files = list(output_dir.glob('**/*.md')) print(f" ✓ pdoc generated {len(md_files)} files → {output_dir}") return True else: print(f" ✗ pdoc error: {result.stderr[:200]}") return False except Exception as e: print(f" ✗ Error: {e}") return False EXTRACTORS = { 'fastapi': extract_fastapi, 'typescript': extract_typescript, 'python': extract_python_module } def main(output_base: Optional[str] = None, projects: Optional[list] = None): """主入口""" base = Path(output_base) if output_base else Path.cwd() print("=" * 50) print("API Documentation Extraction") print("=" * 50) results = {} for name, config in PROJECTS.items(): if projects and name not in projects: continue print(f"\n[{name}] ({config['type']})") if not config['path'].exists(): print(f" ✗ Path not found: {config['path']}") results[name] = False continue extractor = EXTRACTORS.get(config['type']) if extractor: results[name] = extractor(name, config, base) else: print(f" ✗ Unknown type: {config['type']}") results[name] = False # 汇总 print("\n" + "=" * 50) print("Summary") print("=" * 50) success = sum(1 for v in results.values() if v) print(f"Success: {success}/{len(results)}") return all(results.values()) if __name__ == '__main__': import argparse parser = argparse.ArgumentParser(description='Extract API documentation') parser.add_argument('--output', '-o', default='.', help='Output base directory') parser.add_argument('--projects', '-p', nargs='+', help='Specific projects to extract') args = parser.parse_args() success = main(args.output, args.projects) sys.exit(0 if success else 1)