Files
geMoldInsight/src/moldinsight/api/advanced_router.py
T
2026-07-27 15:54:25 +08:00

553 lines
21 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from pathlib import Path
import os
from datetime import datetime
from urllib.parse import quote
from fastapi import APIRouter, Depends, HTTPException, Request
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from shared.services.auth_service import get_current_active_user
from shared.services.redis_task_manager import redis_task_manager
from moldinsight.services.processing_service import processing_service
from moldinsight.services.storage_integration_rustfs import StorageIntegrationService
from moldinsight.services.task_query_service import TaskQueryService
from shared.database.database import get_db_session
from shared.models.database import User
from shared.models.database import ProcessingTask, STPFile
from moldinsight.core.cad_exporter import CADExporter
from shared.utils.logger import get_logger
logger = get_logger(__name__)
router = APIRouter()
cad_exporter = CADExporter()
storage_service = StorageIntegrationService()
_cached_instances = {}
def _get_cached_import(key: str):
"""惰性导入核心模块,避免路由器模块级加载时的循环依赖。"""
if key in _cached_instances:
return _cached_instances[key]
try:
if key == "side_action_designer":
from moldinsight.core.side_action_designer import SideActionDesigner
instance = SideActionDesigner()
elif key == "cavity_layout_optimizer":
from moldinsight.core.cavity_layout_optimizer import CavityLayoutOptimizer
instance = CavityLayoutOptimizer()
elif key == "mold_system_designer":
from moldinsight.core.mold_system_designer import MoldSystemDesigner
instance = MoldSystemDesigner()
elif key == "mold_cam_designer":
from moldinsight.core.mold_cam import MoldCAMDesigner
instance = MoldCAMDesigner()
elif key == "collision_detector":
from moldinsight.core.mold_machining import CollisionDetector
instance = CollisionDetector()
elif key == "toolpath_optimizer":
from moldinsight.core.mold_machining import ToolpathOptimizer
instance = ToolpathOptimizer()
elif key == "edm_designer":
from moldinsight.core.mold_machining import EDMElectrodeDesigner
instance = EDMElectrodeDesigner()
elif key == "machining_simulator":
from moldinsight.core.mold_machining import MachiningSimulator
instance = MachiningSimulator()
else:
return None
_cached_instances[key] = instance
return instance
except Exception as e:
logger.warning(f"核心模块 {key} 加载失败: {e}")
return None
async def _get_task_data(task_id: str) -> dict:
return await redis_task_manager.get_task(task_id)
async def _ensure_task_access(
db_session: AsyncSession,
task_id: str,
user_id: int,
):
row = await db_session.execute(
select(ProcessingTask, STPFile)
.join(STPFile, ProcessingTask.stp_file_id == STPFile.id)
.where(ProcessingTask.task_id == task_id)
)
row = row.first()
if not row:
raise HTTPException(404, "任务不存在")
_, stp_file = row
owner_id = getattr(stp_file, "user_id", None)
if owner_id is not None and owner_id != user_id:
raise HTTPException(403, "无权访问该任务的导出文件")
return row
def _get_export_artifacts(task_data: dict) -> dict:
if not isinstance(task_data, dict):
return {}
direct = task_data.get("export_artifacts")
if isinstance(direct, dict):
return direct
parameters = task_data.get("parameters")
if isinstance(parameters, dict) and isinstance(parameters.get("export_artifacts"), dict):
return parameters.get("export_artifacts")
return {}
def _expand_components(components):
requested = components or ["cavity", "core"]
if "all" in requested:
return ["cavity", "core", "parting_surface"]
return list(dict.fromkeys(requested))
def _augment_export_files(task_id: str, files):
items = []
for file in files or []:
item = dict(file)
relative_path = item.get("relative_path")
if not relative_path and item.get("filepath"):
relative_path = cad_exporter.get_relative_path(item["filepath"])
if relative_path:
relative_path = str(relative_path).replace("\\", "/").strip("/")
item["relative_path"] = relative_path
item["download_path"] = f"/api/export-download/{quote(relative_path, safe='/')}?task_id={task_id}"
items.append(item)
return items
def _merge_export_artifacts(existing: dict, export_result: dict) -> dict:
merged = dict(existing or {})
schemes = dict(merged.get("schemes") or {})
scheme_id = export_result.get("scheme_id") or "default"
previous = dict(schemes.get(scheme_id) or {})
file_map = {}
for file in previous.get("files", []):
file_map[(file.get("component"), file.get("format"))] = file
for file in export_result.get("files", []):
file_map[(file.get("component"), file.get("format"))] = file
schemes[scheme_id] = {
"base_filename": export_result.get("base_filename") or previous.get("base_filename"),
"generated_at": datetime.now().isoformat(),
"files": sorted(
file_map.values(),
key=lambda item: (item.get("component", ""), item.get("format", "")),
),
"errors": export_result.get("errors", []),
"total_files": len(file_map),
"total_errors": len(export_result.get("errors", [])),
}
merged["version"] = 1
merged["task_id"] = export_result.get("task_id") or merged.get("task_id")
merged["generated_at"] = merged.get("generated_at") or datetime.now().isoformat()
merged["schemes"] = schemes
return merged
def _select_persisted_files(task_id: str, task_data: dict, scheme_id: str, formats, components):
artifacts = _get_export_artifacts(task_data)
scheme_data = (artifacts.get("schemes") or {}).get(scheme_id)
if not scheme_data:
return None
component_list = _expand_components(components)
format_list = list(dict.fromkeys(formats or ["step", "stl"]))
expected = {(component, fmt) for component in component_list for fmt in format_list}
available = []
available_keys = set()
for file in scheme_data.get("files", []):
component = file.get("component")
fmt = file.get("format")
if component not in component_list or fmt not in format_list:
continue
relative_path = str(file.get("relative_path") or "").replace("\\", "/").strip("/")
if not relative_path:
continue
full_path = os.path.join(cad_exporter.output_dir, relative_path.replace("/", os.sep))
if not os.path.exists(full_path):
continue
available.append(file)
available_keys.add((component, fmt))
if expected and not expected.issubset(available_keys):
return None
return _augment_export_files(task_id, available)
@router.post("/optimize-layout")
async def optimize_cavity_layout(
request: Request,
current_user: User = Depends(get_current_active_user),
):
body = await request.json()
product_bbox = body.get("product_bbox", {"dimensions": [100, 100, 50]})
cavity_count = body.get("cavity_count", 1)
mold_base_size = body.get("mold_base_size")
layout_type = body.get("layout_type", "auto")
if cavity_count < 1 or cavity_count > 64:
raise HTTPException(400, "型腔数量必须在 1-64 之间")
optimizer = _get_cached_import("cavity_layout_optimizer")
if not optimizer:
raise HTTPException(503, "服务不可用:核心模块未加载")
result = optimizer.optimize_layout(
product_bbox=product_bbox,
cavity_count=cavity_count,
mold_base_size=mold_base_size,
layout_type=layout_type,
)
return {"status": "success", "data": result}
@router.post("/design-cooling")
async def design_cooling_system(
request: Request,
current_user: User = Depends(get_current_active_user),
):
body = await request.json()
mold_size = body.get("mold_size", {"length": 300, "width": 300, "height": 200})
product_bbox = body.get("product_bbox", {"dimensions": [100, 100, 50]})
material = body.get("material", "ABS")
cavity_count = body.get("cavity_count", 1)
cycle_time_target = body.get("cycle_time_target")
from moldinsight.core.mold_system_designer import CoolingSystemDesigner
designer = CoolingSystemDesigner()
result = designer.design_cooling_system(
mold_size=mold_size, product_bbox=product_bbox,
material=material, cavity_count=cavity_count,
cycle_time_target=cycle_time_target,
)
return {"status": "success", "data": result}
@router.post("/design-gating")
async def design_gating_system(
request: Request,
current_user: User = Depends(get_current_active_user),
):
body = await request.json()
product_bbox = body.get("product_bbox", {"dimensions": [100, 100, 50]})
material = body.get("material", "ABS")
cavity_count = body.get("cavity_count", 1)
gate_type = body.get("gate_type", "auto")
layout_positions = body.get("layout_positions")
from moldinsight.core.mold_system_designer import GatingSystemDesigner
designer = GatingSystemDesigner()
result = designer.design_gating_system(
product_bbox=product_bbox, material=material,
cavity_count=cavity_count, gate_type=gate_type,
layout_positions=layout_positions,
)
return {"status": "success", "data": result}
@router.post("/design-mold-system")
async def design_complete_mold_system(
request: Request,
current_user: User = Depends(get_current_active_user),
):
body = await request.json()
mold_size = body.get("mold_size", {"length": 300, "width": 300, "height": 200})
product_bbox = body.get("product_bbox", {"dimensions": [100, 100, 50]})
material = body.get("material", "ABS")
cavity_count = body.get("cavity_count", 1)
gate_type = body.get("gate_type", "auto")
cycle_time_target = body.get("cycle_time_target")
layout_positions = body.get("layout_positions")
ds = _get_cached_import("mold_system_designer")
if not ds:
raise HTTPException(503, "服务不可用:核心模块未加载")
result = ds.design_complete_system(
mold_size=mold_size, product_bbox=product_bbox,
material=material, cavity_count=cavity_count,
gate_type=gate_type, cycle_time_target=cycle_time_target,
layout_positions=layout_positions,
)
return {"status": "success", "data": result}
@router.post("/detect-undercuts")
async def detect_undercuts(
request: Request,
current_user: User = Depends(get_current_active_user),
):
body = await request.json()
task_id = body.get("task_id")
parting_direction = body.get("parting_direction", [0, 0, 1])
mold_size = body.get("mold_size", {"length": 300, "width": 300, "height": 200})
if not task_id:
raise HTTPException(404, "缺少 task_id")
task_data = await _get_task_data(task_id)
if not task_data:
raise HTTPException(404, "任务不存在")
sd = _get_cached_import("side_action_designer")
if not sd:
raise HTTPException(503, "服务不可用:核心模块未加载")
result = sd.analyze_and_design(
shape=None, parting_direction=parting_direction, mold_size=mold_size,
)
return {"status": "success", "data": result}
@router.post("/cost-estimate")
async def estimate_cost(
request: Request,
current_user: User = Depends(get_current_active_user),
):
"""LLM 模具成本估算(P2-2:真 AI 落地,需启用 LLM)"""
body = await request.json()
task_id = body.get("task_id")
if not task_id:
raise HTTPException(404, "缺少 task_id")
task_data = await _get_task_data(task_id)
if not task_data:
raise HTTPException(404, "任务不存在")
analysis_result = task_data.get("analysis_result")
if not analysis_result:
raise HTTPException(400, "该任务尚未完成分析")
detailed_context = {
"candidate_schemes": task_data.get("candidate_schemes", []),
"geometry_data": task_data.get("geometry_data", {}),
"metadata": {"selected_material": task_data.get("material")},
}
from moldinsight.services.llm_service import llm_service
result = await llm_service.estimate_cost(analysis_result, detailed_context)
if result is None:
raise HTTPException(503, "成本估算不可用(LLM 未启用或生成失败)")
return {"status": "success", "data": result}
@router.post("/design-cam")
async def design_mold_cam(
request: Request,
current_user: User = Depends(get_current_active_user),
):
body = await request.json()
cavity_bbox = body.get("cavity_bbox", {"dimensions": [100, 100, 50], "min": [-50, -50, -25], "max": [50, 50, 25]})
stock_bbox = body.get("stock_bbox", {"dimensions": [150, 150, 100], "min": [-75, -75, -50], "max": [75, 75, 50]})
mold_steel = body.get("mold_steel", "P20")
surface_quality = body.get("surface_quality", "standard")
controller = body.get("controller", "fanuc")
cam = _get_cached_import("mold_cam_designer")
if not cam:
raise HTTPException(503, "服务不可用:核心模块未加载")
result = cam.design_mold_cam(
cavity_bbox=cavity_bbox, stock_bbox=stock_bbox,
mold_steel=mold_steel, surface_quality=surface_quality,
controller=controller,
)
return {"status": "success", "data": result}
@router.post("/check-collision")
async def check_toolpath_collision(
request: Request,
current_user: User = Depends(get_current_active_user),
):
body = await request.json()
toolpath_points = body.get("toolpath_points", [[0, 0, 50], [10, 10, -5], [20, 20, -10]])
tool = body.get("tool", {"diameter": 10, "flute_length": 30, "shank_diameter": 10})
stock_bbox = body.get("stock_bbox", {"min": [-50, -50, -25], "max": [50, 50, 25]})
clamp_positions = body.get("clamp_positions")
cd = _get_cached_import("collision_detector")
if not cd:
raise HTTPException(503, "服务不可用:核心模块未加载")
result = cd.check_toolpath_safety(toolpath_points, tool, stock_bbox, clamp_positions)
return {"status": "success", "data": result}
@router.post("/optimize-toolpath")
async def optimize_toolpath(
request: Request,
current_user: User = Depends(get_current_active_user),
):
body = await request.json()
toolpath_points = body.get("toolpath_points", [[0, 0, 50], [10, 10, -5], [20, 20, -10]])
cutting_params = body.get("cutting_params", {"feed_rate_mm_min": 500})
stock_bbox = body.get("stock_bbox")
to = _get_cached_import("toolpath_optimizer")
if not to:
raise HTTPException(503, "服务不可用:核心模块未加载")
result = to.optimize_toolpath(toolpath_points, cutting_params, stock_bbox)
return {"status": "success", "data": result}
@router.post("/design-electrodes")
async def design_edm_electrodes(
request: Request,
current_user: User = Depends(get_current_active_user),
):
body = await request.json()
undercut_regions = body.get("undercut_regions", [{"center": [0, 0, 0], "area": 100, "type": "undercut"}])
cavity_bbox = body.get("cavity_bbox", {"dimensions": [100, 100, 50]})
material = body.get("material", "copper")
spark_gap = body.get("spark_gap", 0.05)
overburn = body.get("overburn", 0.1)
ed = _get_cached_import("edm_designer")
if not ed:
raise HTTPException(503, "服务不可用:核心模块未加载")
result = ed.design_electrodes(undercut_regions, cavity_bbox, material, spark_gap, overburn)
return {"status": "success", "data": result}
@router.post("/simulate-machining")
async def simulate_machining(
request: Request,
current_user: User = Depends(get_current_active_user),
):
body = await request.json()
operations = body.get("operations", [{"strategy": "z_level_roughing", "levels": [{"z": -5}]}])
stock_bbox = body.get("stock_bbox", {"dimensions": [100, 100, 50], "min": [-50, -50, -25], "max": [50, 50, 25]})
resolution = body.get("resolution", 2.0)
ms = _get_cached_import("machining_simulator")
if not ms:
raise HTTPException(503, "服务不可用:核心模块未加载")
result = ms.simulate_machining(operations, stock_bbox, resolution)
return {"status": "success", "data": result}
@router.post("/export-mold")
async def export_mold_results(
request: Request,
current_user: User = Depends(get_current_active_user),
db_session: AsyncSession = Depends(get_db_session),
):
body = await request.json()
task_id = body.get("task_id")
scheme_id = body.get("scheme_id")
formats = body.get("formats", ["step", "stl"])
components = body.get("components", ["cavity", "core"])
if not task_id:
raise HTTPException(404, "缺少 task_id")
await _ensure_task_access(db_session, task_id, current_user.id)
task_data = await TaskQueryService.get_task_view(db_session, task_id)
if not task_data:
raise HTTPException(404, "任务不存在")
resolved_scheme_id = scheme_id or task_data.get("best_scheme_id") or "default"
persisted_files = _select_persisted_files(
task_id=task_id,
task_data=task_data,
scheme_id=resolved_scheme_id,
formats=formats,
components=components,
)
if persisted_files:
return {
"status": "success",
"data": {
"base_filename": Path(task_data.get("filename", f"mold_{task_id}")).stem,
"task_id": task_id,
"scheme_id": resolved_scheme_id,
"files": persisted_files,
"errors": [],
"total_files": len(persisted_files),
"total_errors": 0,
"source": "persisted",
},
}
cavity_shapes = processing_service.get_export_shapes(
task_id,
resolved_scheme_id,
)
filename = task_data.get("filename", f"mold_{task_id}")
if not cavity_shapes:
raise HTTPException(
409,
"导出缓存已失效或任务尚未完成,请重新分析后再导出以保证方案一致性",
)
base_filename = Path(filename).stem
result = cad_exporter.export_mold_results(
cavity_data=cavity_shapes,
base_filename=base_filename,
formats=formats,
components=components,
task_id=task_id,
scheme_id=resolved_scheme_id,
)
result["files"] = _augment_export_files(task_id, result.get("files", []))
result["source"] = "generated"
merged_artifacts = _merge_export_artifacts(_get_export_artifacts(task_data), result)
await storage_service.update_task_parameters(
db_session,
task_id,
{"export_artifacts": merged_artifacts},
)
await redis_task_manager.update_task(task_id, {"export_artifacts": merged_artifacts})
return {"status": "success", "data": result}
@router.get("/export-download/{filepath:path}")
async def download_export_file(
filepath: str,
task_id: str,
current_user: User = Depends(get_current_active_user),
db_session: AsyncSession = Depends(get_db_session),
):
from fastapi.responses import FileResponse
if not task_id:
raise HTTPException(400, "缺少 task_id")
await _ensure_task_access(db_session, task_id, current_user.id)
task_data = await TaskQueryService.get_task_view(db_session, task_id)
if not task_data:
raise HTTPException(404, "任务不存在")
allowed_paths = set()
artifacts = _get_export_artifacts(task_data)
for scheme in (artifacts.get("schemes") or {}).values():
for file in scheme.get("files", []):
relative_path = str(file.get("relative_path") or "").replace("\\", "/").strip("/")
if relative_path:
allowed_paths.add(relative_path)
normalized_path = str(filepath or "").replace("\\", "/").strip("/")
if normalized_path not in allowed_paths:
raise HTTPException(403, "该文件不在任务允许下载清单中")
full_path = os.path.join(cad_exporter.output_dir, normalized_path.replace("/", os.sep))
if not os.path.exists(full_path):
raise HTTPException(404, "文件不存在")
if not os.path.abspath(full_path).startswith(os.path.abspath(cad_exporter.output_dir)):
raise HTTPException(403, "禁止访问")
media_types = {
".step": "application/step", ".stp": "application/step",
".iges": "application/iges", ".igs": "application/iges",
".stl": "model/stl", ".brep": "application/octet-stream",
}
ext = Path(full_path).suffix.lower()
media_type = media_types.get(ext, "application/octet-stream")
return FileResponse(full_path, media_type=media_type, filename=os.path.basename(full_path))
@router.get("/export-recommendations")
async def get_export_recommendations(
target: str = "ug",
current_user: User = Depends(get_current_active_user),
):
result = cad_exporter.get_export_recommendations(target)
return {"status": "success", "data": result}