建立 Pydantic → TypeScript 自動生成工具鏈: 1. scripts/generate-schemas.py - 從 Pydantic 模型生成 JSON Schema - 正確處理 Pydantic 2.x 的 $defs 格式 - 支援 Approval/Incident/Terminal/Playbook/CSRF 模型 2. packages/shared-types/ - @awoooi/shared-types 套件 - 44 個型別定義,40 個介面 - json-schema-to-typescript 自動生成 3. 前端整合 - apps/web 加入 @awoooi/shared-types 依賴 - typecheck 通過 使用方式: cd packages/shared-types pnpm generate # 重新生成型別 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
258 lines
7.3 KiB
Python
258 lines
7.3 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Pydantic → JSON Schema 生成器
|
|
=============================
|
|
Phase 14.3: 共用型別系統
|
|
|
|
功能:
|
|
- 從 Pydantic 模型生成 JSON Schema
|
|
- 正確處理 Pydantic 2.x 的 $defs 格式
|
|
- 輸出到 packages/shared-types/schemas/
|
|
|
|
使用方式:
|
|
cd apps/api
|
|
python ../../scripts/generate-schemas.py
|
|
|
|
建立: 2026-03-31 (台北時區)
|
|
建立者: Claude Code (Phase 14.3)
|
|
"""
|
|
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
# 加入 apps/api/src 到 Python path
|
|
api_src = Path(__file__).parent.parent / "apps" / "api" / "src"
|
|
sys.path.insert(0, str(api_src))
|
|
|
|
# 輸出目錄
|
|
OUTPUT_DIR = Path(__file__).parent.parent / "packages" / "shared-types" / "schemas"
|
|
|
|
|
|
def extract_and_merge_defs(schema: dict[str, Any], all_defs: dict[str, Any]) -> dict[str, Any]:
|
|
"""
|
|
從單一模型 schema 中提取 $defs 並合併到全局 $defs
|
|
同時修正 $ref 路徑
|
|
"""
|
|
# 提取並合併 $defs
|
|
if "$defs" in schema:
|
|
for def_name, def_schema in schema["$defs"].items():
|
|
if def_name not in all_defs:
|
|
# 遞迴處理巢狀 $defs
|
|
cleaned_def = extract_and_merge_defs(def_schema.copy(), all_defs)
|
|
all_defs[def_name] = cleaned_def
|
|
|
|
# 移除 schema 中的 $defs (已合併到全局)
|
|
cleaned_schema = {k: v for k, v in schema.items() if k != "$defs"}
|
|
|
|
return cleaned_schema
|
|
|
|
|
|
def fix_refs(obj: Any) -> Any:
|
|
"""遞迴修正所有 $ref 路徑從 #/$defs/X 到 #/definitions/X"""
|
|
if isinstance(obj, dict):
|
|
result = {}
|
|
for key, value in obj.items():
|
|
if key == "$ref" and isinstance(value, str):
|
|
# 修正 ref 路徑
|
|
result[key] = value.replace("#/$defs/", "#/definitions/")
|
|
else:
|
|
result[key] = fix_refs(value)
|
|
return result
|
|
elif isinstance(obj, list):
|
|
return [fix_refs(item) for item in obj]
|
|
else:
|
|
return obj
|
|
|
|
|
|
def generate_schemas():
|
|
"""生成所有模型的 JSON Schema"""
|
|
|
|
# 確保輸出目錄存在
|
|
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
|
|
|
# 收集所有 definitions
|
|
all_definitions: dict[str, Any] = {}
|
|
model_count = 0
|
|
|
|
print("📦 Approval Models:")
|
|
# === Approval Models ===
|
|
try:
|
|
from models.approval import (
|
|
ApprovalRequest,
|
|
ApprovalRequestCreate,
|
|
ApprovalRequestResponse,
|
|
BlastRadius,
|
|
DryRunCheck,
|
|
PendingApprovalsResponse,
|
|
RejectRequest,
|
|
Signature,
|
|
SignRequest,
|
|
SignResponse,
|
|
)
|
|
|
|
approval_models = [
|
|
ApprovalRequest,
|
|
ApprovalRequestCreate,
|
|
ApprovalRequestResponse,
|
|
BlastRadius,
|
|
DryRunCheck,
|
|
PendingApprovalsResponse,
|
|
RejectRequest,
|
|
Signature,
|
|
SignRequest,
|
|
SignResponse,
|
|
]
|
|
|
|
for model in approval_models:
|
|
schema = model.model_json_schema()
|
|
cleaned = extract_and_merge_defs(schema, all_definitions)
|
|
all_definitions[model.__name__] = cleaned
|
|
model_count += 1
|
|
print(f" ✓ {model.__name__}")
|
|
|
|
except ImportError as e:
|
|
print(f" ⚠ Import failed: {e}")
|
|
|
|
print("\n📦 Incident Models:")
|
|
# === Incident Models ===
|
|
try:
|
|
from models.incident import (
|
|
AIDecisionChain,
|
|
Incident,
|
|
IncidentCreate,
|
|
IncidentOutcome,
|
|
IncidentResponse,
|
|
IncidentUpdate,
|
|
Signal,
|
|
)
|
|
|
|
incident_models = [
|
|
AIDecisionChain,
|
|
Incident,
|
|
IncidentCreate,
|
|
IncidentOutcome,
|
|
IncidentResponse,
|
|
IncidentUpdate,
|
|
Signal,
|
|
]
|
|
|
|
for model in incident_models:
|
|
schema = model.model_json_schema()
|
|
cleaned = extract_and_merge_defs(schema, all_definitions)
|
|
all_definitions[model.__name__] = cleaned
|
|
model_count += 1
|
|
print(f" ✓ {model.__name__}")
|
|
|
|
except ImportError as e:
|
|
print(f" ⚠ Import failed: {e}")
|
|
|
|
print("\n📦 Terminal Models:")
|
|
# === Terminal Models ===
|
|
try:
|
|
from models.terminal import (
|
|
SpatialContext,
|
|
TerminalAbortRequest,
|
|
TerminalAbortResponse,
|
|
TerminalIntentRequest,
|
|
TerminalIntentResponse,
|
|
TerminalStatusResponse,
|
|
TerminalThoughtEvent,
|
|
)
|
|
|
|
terminal_models = [
|
|
TerminalIntentRequest,
|
|
TerminalIntentResponse,
|
|
TerminalStatusResponse,
|
|
TerminalAbortRequest,
|
|
TerminalAbortResponse,
|
|
TerminalThoughtEvent,
|
|
SpatialContext,
|
|
]
|
|
|
|
for model in terminal_models:
|
|
schema = model.model_json_schema()
|
|
cleaned = extract_and_merge_defs(schema, all_definitions)
|
|
all_definitions[model.__name__] = cleaned
|
|
model_count += 1
|
|
print(f" ✓ {model.__name__}")
|
|
|
|
except ImportError as e:
|
|
print(f" ⚠ Import failed: {e}")
|
|
|
|
print("\n📦 Playbook Models:")
|
|
# === Playbook Models ===
|
|
try:
|
|
from models.playbook import (
|
|
Playbook,
|
|
PlaybookCreateRequest,
|
|
PlaybookListResponse,
|
|
PlaybookRecommendation,
|
|
PlaybookResponse,
|
|
PlaybookUpdateRequest,
|
|
RepairStep,
|
|
SymptomPattern,
|
|
)
|
|
|
|
playbook_models = [
|
|
Playbook,
|
|
PlaybookResponse,
|
|
PlaybookListResponse,
|
|
PlaybookCreateRequest,
|
|
PlaybookUpdateRequest,
|
|
PlaybookRecommendation,
|
|
SymptomPattern,
|
|
RepairStep,
|
|
]
|
|
|
|
for model in playbook_models:
|
|
schema = model.model_json_schema()
|
|
cleaned = extract_and_merge_defs(schema, all_definitions)
|
|
all_definitions[model.__name__] = cleaned
|
|
model_count += 1
|
|
print(f" ✓ {model.__name__}")
|
|
|
|
except ImportError as e:
|
|
print(f" ⚠ Import failed: {e}")
|
|
|
|
print("\n📦 Other Models:")
|
|
# === CSRF Models ===
|
|
try:
|
|
from models.csrf import CSRFTokenResponse
|
|
|
|
schema = CSRFTokenResponse.model_json_schema()
|
|
cleaned = extract_and_merge_defs(schema, all_definitions)
|
|
all_definitions["CSRFTokenResponse"] = cleaned
|
|
model_count += 1
|
|
print(" ✓ CSRFTokenResponse")
|
|
|
|
except ImportError as e:
|
|
print(f" ⚠ CSRF import failed: {e}")
|
|
|
|
# 修正所有 $ref 路徑
|
|
all_definitions = fix_refs(all_definitions)
|
|
|
|
# 組裝最終 schema
|
|
final_schema = {
|
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
"title": "AWOOOI API Types",
|
|
"description": "Auto-generated from Pydantic models - DO NOT EDIT",
|
|
"definitions": all_definitions,
|
|
}
|
|
|
|
# 寫入檔案
|
|
output_file = OUTPUT_DIR / "api-types.json"
|
|
with open(output_file, "w", encoding="utf-8") as f:
|
|
json.dump(final_schema, f, indent=2, ensure_ascii=False)
|
|
|
|
print(f"\n✅ Schema 生成完成: {output_file}")
|
|
print(f" 共 {len(all_definitions)} 個型別定義")
|
|
|
|
return final_schema
|
|
|
|
|
|
if __name__ == "__main__":
|
|
print("🔄 開始生成 JSON Schema...\n")
|
|
generate_schemas()
|