1. 增加了请求框架
2. 增加了删除虚拟环境的脚本
This commit is contained in:
345
request/src/utils/doc_generator.py
Normal file
345
request/src/utils/doc_generator.py
Normal file
@@ -0,0 +1,345 @@
|
||||
"""
|
||||
API文档生成器 - 离线生成Swagger文档
|
||||
"""
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from ..utils import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
# 文档缓存目录
|
||||
STATIC_DIR = Path("static")
|
||||
CACHE_FILE = STATIC_DIR / ".openapi_cache"
|
||||
DOC_FILE = STATIC_DIR / "doc.html"
|
||||
|
||||
|
||||
def get_openapi_hash(openapi_spec: dict) -> str:
|
||||
"""计算OpenAPI规范的哈希值"""
|
||||
spec_str = json.dumps(openapi_spec, sort_keys=True, ensure_ascii=False)
|
||||
return hashlib.md5(spec_str.encode()).hexdigest()
|
||||
|
||||
|
||||
def get_cached_hash() -> Optional[str]:
|
||||
"""获取缓存的哈希值"""
|
||||
try:
|
||||
if CACHE_FILE.exists():
|
||||
return CACHE_FILE.read_text().strip()
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def save_cache_hash(hash_value: str):
|
||||
"""保存哈希值到缓存"""
|
||||
try:
|
||||
CACHE_FILE.write_text(hash_value)
|
||||
except Exception as e:
|
||||
logger.warning(f"无法保存文档缓存: {e}")
|
||||
|
||||
|
||||
def generate_doc_html(openapi_spec: dict) -> str:
|
||||
"""生成文档HTML内容"""
|
||||
# 将OpenAPI规范内嵌到HTML中
|
||||
spec_json = json.dumps(openapi_spec, ensure_ascii=False, indent=2)
|
||||
api_version = openapi_spec.get("info", {}).get("version", "1.0.0")
|
||||
api_title = openapi_spec.get("info", {}).get("title", "API文档")
|
||||
|
||||
html_content = f'''<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{api_title} - 接口文档</title>
|
||||
<!-- 本地Swagger UI资源 -->
|
||||
<link rel="stylesheet" href="/vendor/swagger-ui.css">
|
||||
<script src="/vendor/swagger-ui-bundle.js"></script>
|
||||
<style>
|
||||
* {{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}}
|
||||
|
||||
body {{
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
background: #f8fafc;
|
||||
}}
|
||||
|
||||
.nav {{
|
||||
background: white;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
height: 64px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 24px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}}
|
||||
|
||||
.nav-brand {{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
}}
|
||||
|
||||
.nav-brand i {{
|
||||
color: #3b82f6;
|
||||
margin-right: 8px;
|
||||
}}
|
||||
|
||||
.nav-links {{
|
||||
display: flex;
|
||||
margin-left: 24px;
|
||||
gap: 8px;
|
||||
}}
|
||||
|
||||
.nav-link {{
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
transition: all 0.2s;
|
||||
}}
|
||||
|
||||
.nav-link:hover {{
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
color: #3b82f6;
|
||||
}}
|
||||
|
||||
.nav-link.active {{
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
color: #3b82f6;
|
||||
border-bottom: 2px solid #3b82f6;
|
||||
}}
|
||||
|
||||
.nav-right {{
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}}
|
||||
|
||||
.api-version {{
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
}}
|
||||
|
||||
.api-version span {{
|
||||
font-weight: 500;
|
||||
color: #1f2937;
|
||||
}}
|
||||
|
||||
.main {{
|
||||
padding: 16px 24px;
|
||||
}}
|
||||
|
||||
.doc-container {{
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
min-height: calc(100vh - 120px);
|
||||
}}
|
||||
|
||||
/* Swagger UI 样式覆盖 */
|
||||
.swagger-ui .topbar {{
|
||||
display: none;
|
||||
}}
|
||||
|
||||
.swagger-ui .info {{
|
||||
margin: 20px 0;
|
||||
}}
|
||||
|
||||
.swagger-ui .scheme-container {{
|
||||
background: #f8fafc;
|
||||
padding: 15px;
|
||||
box-shadow: none;
|
||||
}}
|
||||
|
||||
/* Font Awesome 图标 (内联) */
|
||||
.fa {{
|
||||
display: inline-block;
|
||||
font-style: normal;
|
||||
}}
|
||||
.fa-file-text-o:before {{ content: "📄"; }}
|
||||
.fa-file-text:before {{ content: "📝"; }}
|
||||
.fa-book:before {{ content: "📖"; }}
|
||||
.fa-home:before {{ content: "🏠"; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="nav">
|
||||
<div class="nav-brand">
|
||||
<i class="fa fa-file-text-o"></i>
|
||||
X-Request 管理系统
|
||||
</div>
|
||||
<div class="nav-links">
|
||||
<a href="/log.html" class="nav-link">
|
||||
<i class="fa fa-file-text"></i> 日志管理
|
||||
</a>
|
||||
<a href="/doc.html" class="nav-link active">
|
||||
<i class="fa fa-book"></i> 接口文档
|
||||
</a>
|
||||
<a href="/" class="nav-link">
|
||||
<i class="fa fa-home"></i> 首页
|
||||
</a>
|
||||
</div>
|
||||
<div class="nav-right">
|
||||
<div class="api-version">
|
||||
API版本: <span>{api_version}</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="main">
|
||||
<div class="doc-container">
|
||||
<div id="swagger-ui"></div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
// 内嵌的OpenAPI规范
|
||||
const spec = {spec_json};
|
||||
|
||||
window.onload = function() {{
|
||||
SwaggerUIBundle({{
|
||||
spec: spec,
|
||||
dom_id: '#swagger-ui',
|
||||
deepLinking: true,
|
||||
presets: [
|
||||
SwaggerUIBundle.presets.apis,
|
||||
SwaggerUIBundle.SwaggerUIStandalonePreset
|
||||
],
|
||||
layout: "BaseLayout",
|
||||
defaultModelsExpandDepth: 1,
|
||||
defaultModelExpandDepth: 1,
|
||||
docExpansion: "list",
|
||||
filter: true,
|
||||
showExtensions: true,
|
||||
showCommonExtensions: true
|
||||
}});
|
||||
}};
|
||||
</script>
|
||||
</body>
|
||||
</html>'''
|
||||
|
||||
return html_content
|
||||
|
||||
|
||||
def generate_loading_html() -> str:
|
||||
"""生成加载中的HTML页面"""
|
||||
return '''<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>生成文档中...</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
background: #f8fafc;
|
||||
}
|
||||
.loading {
|
||||
text-align: center;
|
||||
}
|
||||
.spinner {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 4px solid #e5e7eb;
|
||||
border-top-color: #3b82f6;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 20px;
|
||||
}
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
h2 {
|
||||
color: #1f2937;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
p {
|
||||
color: #6b7280;
|
||||
}
|
||||
</style>
|
||||
<meta http-equiv="refresh" content="2">
|
||||
</head>
|
||||
<body>
|
||||
<div class="loading">
|
||||
<div class="spinner"></div>
|
||||
<h2>正在生成API文档</h2>
|
||||
<p>请稍候,页面将自动刷新...</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>'''
|
||||
|
||||
|
||||
def should_regenerate(openapi_spec: dict) -> bool:
|
||||
"""检查是否需要重新生成文档"""
|
||||
# 检查文档文件是否存在
|
||||
if not DOC_FILE.exists():
|
||||
return True
|
||||
|
||||
# 检查本地资源是否存在
|
||||
vendor_dir = STATIC_DIR / "vendor"
|
||||
if not (vendor_dir / "swagger-ui.css").exists() or not (vendor_dir / "swagger-ui-bundle.js").exists():
|
||||
return True
|
||||
|
||||
# 比较哈希值
|
||||
current_hash = get_openapi_hash(openapi_spec)
|
||||
cached_hash = get_cached_hash()
|
||||
|
||||
return current_hash != cached_hash
|
||||
|
||||
|
||||
def generate_docs(app) -> bool:
|
||||
"""
|
||||
生成API文档
|
||||
|
||||
Args:
|
||||
app: FastAPI应用实例
|
||||
|
||||
Returns:
|
||||
bool: 是否重新生成了文档
|
||||
"""
|
||||
try:
|
||||
# 获取OpenAPI规范
|
||||
openapi_spec = app.openapi()
|
||||
|
||||
# 检查是否需要重新生成
|
||||
if not should_regenerate(openapi_spec):
|
||||
logger.info("API文档未变化,使用缓存")
|
||||
return False
|
||||
|
||||
logger.info("检测到API变化,正在生成文档...")
|
||||
|
||||
# 先写入加载页面
|
||||
DOC_FILE.write_text(generate_loading_html(), encoding='utf-8')
|
||||
|
||||
# 生成文档HTML
|
||||
doc_html = generate_doc_html(openapi_spec)
|
||||
|
||||
# 写入文档文件
|
||||
DOC_FILE.write_text(doc_html, encoding='utf-8')
|
||||
|
||||
# 保存哈希值
|
||||
current_hash = get_openapi_hash(openapi_spec)
|
||||
save_cache_hash(current_hash)
|
||||
|
||||
logger.info("API文档生成完成")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"生成API文档失败: {e}")
|
||||
return False
|
||||
Reference in New Issue
Block a user