feat(types): Phase 14.3 共用型別系統 (#97-#100)
建立 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>
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@awoooi/lewooogo-core": "workspace:*",
|
||||
"@awoooi/shared-types": "workspace:^",
|
||||
"@sentry/nextjs": "^10.45.0",
|
||||
"@tanstack/react-query": "^5.17.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
|
||||
24
packages/shared-types/package.json
Normal file
24
packages/shared-types/package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "@awoooi/shared-types",
|
||||
"version": "1.0.0",
|
||||
"description": "AWOOOI 前後端共用型別定義 (自動生成自 Pydantic)",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"scripts": {
|
||||
"generate:schema": "cd ../../apps/api && python ../../scripts/generate-schemas.py",
|
||||
"generate:types": "json2ts -i ./schemas/api-types.json -o ./src/api-types.ts --bannerComment \"/* Auto-generated from Pydantic models - DO NOT EDIT */\" --unreachableDefinitions",
|
||||
"generate": "pnpm generate:schema && pnpm generate:types",
|
||||
"clean": "rm -rf ./schemas/*.json ./src/api-types.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"json-schema-to-typescript": "^15.0.0",
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"files": [
|
||||
"src",
|
||||
"schemas"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
2212
packages/shared-types/schemas/api-types.json
Normal file
2212
packages/shared-types/schemas/api-types.json
Normal file
File diff suppressed because it is too large
Load Diff
1352
packages/shared-types/src/api-types.ts
Normal file
1352
packages/shared-types/src/api-types.ts
Normal file
File diff suppressed because it is too large
Load Diff
16
packages/shared-types/src/index.ts
Normal file
16
packages/shared-types/src/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* @awoooi/shared-types
|
||||
* ====================
|
||||
* 前後端共用型別定義
|
||||
*
|
||||
* 自動生成自 Pydantic 模型,請勿手動編輯 api-types.ts
|
||||
*
|
||||
* 使用方式:
|
||||
* import type { ApprovalRequest, IncidentResponse } from '@awoooi/shared-types'
|
||||
*
|
||||
* 重新生成:
|
||||
* pnpm --filter @awoooi/shared-types generate
|
||||
*/
|
||||
|
||||
// 自動生成的 API 型別 (執行 pnpm generate 後產生)
|
||||
export * from './api-types'
|
||||
14
packages/shared-types/tsconfig.json
Normal file
14
packages/shared-types/tsconfig.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
49
pnpm-lock.yaml
generated
49
pnpm-lock.yaml
generated
@@ -29,6 +29,9 @@ importers:
|
||||
'@awoooi/lewooogo-core':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/lewooogo-core
|
||||
'@awoooi/shared-types':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/shared-types
|
||||
'@sentry/nextjs':
|
||||
specifier: ^10.45.0
|
||||
version: 10.45.0(@opentelemetry/context-async-hooks@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(next@14.1.0(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(webpack@5.105.4)
|
||||
@@ -149,6 +152,15 @@ importers:
|
||||
specifier: ^1.2.0
|
||||
version: 1.6.1(@types/node@20.19.37)(terser@5.46.1)
|
||||
|
||||
packages/shared-types:
|
||||
devDependencies:
|
||||
json-schema-to-typescript:
|
||||
specifier: ^15.0.0
|
||||
version: 15.0.4
|
||||
typescript:
|
||||
specifier: ^5.0.0
|
||||
version: 5.9.3
|
||||
|
||||
packages/tsconfig: {}
|
||||
|
||||
packages:
|
||||
@@ -157,6 +169,10 @@ packages:
|
||||
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
'@apidevtools/json-schema-ref-parser@11.9.3':
|
||||
resolution: {integrity: sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ==}
|
||||
engines: {node: '>= 16'}
|
||||
|
||||
'@babel/code-frame@7.29.0':
|
||||
resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -608,6 +624,9 @@ packages:
|
||||
'@jridgewell/trace-mapping@0.3.31':
|
||||
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
||||
|
||||
'@jsdevtools/ono@7.1.3':
|
||||
resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==}
|
||||
|
||||
'@napi-rs/wasm-runtime@0.2.12':
|
||||
resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
|
||||
|
||||
@@ -1470,6 +1489,9 @@ packages:
|
||||
'@types/json5@0.0.29':
|
||||
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
|
||||
|
||||
'@types/lodash@4.17.24':
|
||||
resolution: {integrity: sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==}
|
||||
|
||||
'@types/mysql@2.15.27':
|
||||
resolution: {integrity: sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA==}
|
||||
|
||||
@@ -2825,6 +2847,11 @@ packages:
|
||||
json-parse-even-better-errors@2.3.1:
|
||||
resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
|
||||
|
||||
json-schema-to-typescript@15.0.4:
|
||||
resolution: {integrity: sha512-Su9oK8DR4xCmDsLlyvadkXzX6+GGXJpbhwoLtOGArAG61dvbW4YQmSEno2y66ahpIdmLMg6YUf/QHLgiwvkrHQ==}
|
||||
engines: {node: '>=16.0.0'}
|
||||
hasBin: true
|
||||
|
||||
json-schema-traverse@0.4.1:
|
||||
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
|
||||
|
||||
@@ -4002,6 +4029,12 @@ snapshots:
|
||||
|
||||
'@alloc/quick-lru@5.2.0': {}
|
||||
|
||||
'@apidevtools/json-schema-ref-parser@11.9.3':
|
||||
dependencies:
|
||||
'@jsdevtools/ono': 7.1.3
|
||||
'@types/json-schema': 7.0.15
|
||||
js-yaml: 4.1.1
|
||||
|
||||
'@babel/code-frame@7.29.0':
|
||||
dependencies:
|
||||
'@babel/helper-validator-identifier': 7.28.5
|
||||
@@ -4370,6 +4403,8 @@ snapshots:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
'@jsdevtools/ono@7.1.3': {}
|
||||
|
||||
'@napi-rs/wasm-runtime@0.2.12':
|
||||
dependencies:
|
||||
'@emnapi/core': 1.9.1
|
||||
@@ -5207,6 +5242,8 @@ snapshots:
|
||||
|
||||
'@types/json5@0.0.29': {}
|
||||
|
||||
'@types/lodash@4.17.24': {}
|
||||
|
||||
'@types/mysql@2.15.27':
|
||||
dependencies:
|
||||
'@types/node': 20.19.37
|
||||
@@ -6810,6 +6847,18 @@ snapshots:
|
||||
|
||||
json-parse-even-better-errors@2.3.1: {}
|
||||
|
||||
json-schema-to-typescript@15.0.4:
|
||||
dependencies:
|
||||
'@apidevtools/json-schema-ref-parser': 11.9.3
|
||||
'@types/json-schema': 7.0.15
|
||||
'@types/lodash': 4.17.24
|
||||
is-glob: 4.0.3
|
||||
js-yaml: 4.1.1
|
||||
lodash: 4.17.23
|
||||
minimist: 1.2.8
|
||||
prettier: 3.8.1
|
||||
tinyglobby: 0.2.15
|
||||
|
||||
json-schema-traverse@0.4.1: {}
|
||||
|
||||
json-schema-traverse@1.0.0: {}
|
||||
|
||||
257
scripts/generate-schemas.py
Normal file
257
scripts/generate-schemas.py
Normal file
@@ -0,0 +1,257 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user