Files
geMoldInsight/src/moldinsight/api/advanced_router.py
T

553 lines
21 KiB
Python
Raw Normal View History

2026-04-23 23:37:39 +08:00
from pathlib import Path
import os
2026-05-25 10:16:05 +08:00
from datetime import datetime
from urllib.parse import quote
2026-04-23 23:37:39 +08:00
from fastapi import APIRouter, Depends, HTTPException, Request
2026-05-25 10:16:05 +08:00
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
2026-04-23 23:37:39 +08:00
2026-05-29 18:10:08 +08:00
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
2026-04-23 23:37:39 +08:00
logger = get_logger(__name__)
router = APIRouter()
2026-05-25 10:16:05 +08:00
cad_exporter = CADExporter()
storage_service = StorageIntegrationService()
2026-04-23 23:37:39 +08:00
2026-06-09 16:19:15 +08:00
_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:
2026-04-30 17:41:47 +08:00
return None
2026-06-09 16:19:15 +08:00
_cached_instances[key] = instance
return instance
except Exception as e:
logger.warning(f"核心模块 {key} 加载失败: {e}")
return None
2026-04-30 17:41:47 +08:00
2026-04-23 23:37:39 +08:00
2026-04-30 17:01:43 +08:00
async def _get_task_data(task_id: str) -> dict:
2026-06-09 16:19:15 +08:00
return await redis_task_manager.get_task(task_id)
2026-04-30 17:01:43 +08:00
2026-05-25 10:16:05 +08:00
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)
2026-04-23 23:37:39 +08:00
@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 之间")
2026-06-09 16:19:15 +08:00
optimizer = _get_cached_import("cavity_layout_optimizer")
2026-04-30 17:41:47 +08:00
if not optimizer:
raise HTTPException(503, "服务不可用:核心模块未加载")
result = optimizer.optimize_layout(
2026-04-23 23:37:39 +08:00
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")
2026-05-29 18:10:08 +08:00
from moldinsight.core.mold_system_designer import CoolingSystemDesigner
2026-04-23 23:37:39 +08:00
designer = CoolingSystemDesigner()
result = designer.design_cooling_system(
2026-04-30 17:41:47 +08:00
mold_size=mold_size, product_bbox=product_bbox,
material=material, cavity_count=cavity_count,
2026-04-23 23:37:39 +08:00
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")
2026-05-29 18:10:08 +08:00
from moldinsight.core.mold_system_designer import GatingSystemDesigner
2026-04-23 23:37:39 +08:00
designer = GatingSystemDesigner()
result = designer.design_gating_system(
2026-04-30 17:41:47 +08:00
product_bbox=product_bbox, material=material,
cavity_count=cavity_count, gate_type=gate_type,
2026-04-23 23:37:39 +08:00
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")
2026-06-09 16:19:15 +08:00
ds = _get_cached_import("mold_system_designer")
2026-04-30 17:41:47 +08:00
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,
2026-04-23 23:37:39 +08:00
layout_positions=layout_positions,
)
return {"status": "success", "data": result}
2026-07-27 15:54:25 +08:00
@router.post("/detect-undercuts")
async def detect_undercuts(
2026-04-23 23:37:39 +08:00
request: Request,
current_user: User = Depends(get_current_active_user),
):
body = await request.json()
task_id = body.get("task_id")
2026-07-27 15:54:25 +08:00
parting_direction = body.get("parting_direction", [0, 0, 1])
mold_size = body.get("mold_size", {"length": 300, "width": 300, "height": 200})
2026-04-30 17:01:43 +08:00
if not task_id:
raise HTTPException(404, "缺少 task_id")
task_data = await _get_task_data(task_id)
if not task_data:
2026-04-23 23:37:39 +08:00
raise HTTPException(404, "任务不存在")
2026-07-27 15:54:25 +08:00
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,
)
2026-04-23 23:37:39 +08:00
return {"status": "success", "data": result}
2026-07-27 15:54:25 +08:00
@router.post("/cost-estimate")
async def estimate_cost(
2026-04-23 23:37:39 +08:00
request: Request,
current_user: User = Depends(get_current_active_user),
):
2026-07-27 15:54:25 +08:00
"""LLM 模具成本估算(P2-2:真 AI 落地,需启用 LLM)"""
2026-04-23 23:37:39 +08:00
body = await request.json()
task_id = body.get("task_id")
2026-04-30 17:01:43 +08:00
if not task_id:
raise HTTPException(404, "缺少 task_id")
task_data = await _get_task_data(task_id)
if not task_data:
2026-04-23 23:37:39 +08:00
raise HTTPException(404, "任务不存在")
2026-07-27 15:54:25 +08:00
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 未启用或生成失败)")
2026-04-23 23:37:39 +08:00
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")
2026-06-09 16:19:15 +08:00
cam = _get_cached_import("mold_cam_designer")
2026-04-30 17:41:47 +08:00
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,
2026-04-23 23:37:39 +08:00
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")
2026-06-09 16:19:15 +08:00
cd = _get_cached_import("collision_detector")
2026-04-30 17:41:47 +08:00
if not cd:
raise HTTPException(503, "服务不可用:核心模块未加载")
result = cd.check_toolpath_safety(toolpath_points, tool, stock_bbox, clamp_positions)
2026-04-23 23:37:39 +08:00
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")
2026-06-09 16:19:15 +08:00
to = _get_cached_import("toolpath_optimizer")
2026-04-30 17:41:47 +08:00
if not to:
raise HTTPException(503, "服务不可用:核心模块未加载")
result = to.optimize_toolpath(toolpath_points, cutting_params, stock_bbox)
2026-04-23 23:37:39 +08:00
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)
2026-06-09 16:19:15 +08:00
ed = _get_cached_import("edm_designer")
2026-04-30 17:41:47 +08:00
if not ed:
raise HTTPException(503, "服务不可用:核心模块未加载")
result = ed.design_electrodes(undercut_regions, cavity_bbox, material, spark_gap, overburn)
2026-04-23 23:37:39 +08:00
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)
2026-06-09 16:19:15 +08:00
ms = _get_cached_import("machining_simulator")
2026-04-30 17:41:47 +08:00
if not ms:
raise HTTPException(503, "服务不可用:核心模块未加载")
result = ms.simulate_machining(operations, stock_bbox, resolution)
2026-04-23 23:37:39 +08:00
return {"status": "success", "data": result}
@router.post("/export-mold")
async def export_mold_results(
request: Request,
current_user: User = Depends(get_current_active_user),
2026-05-25 10:16:05 +08:00
db_session: AsyncSession = Depends(get_db_session),
2026-04-23 23:37:39 +08:00
):
body = await request.json()
task_id = body.get("task_id")
2026-05-14 16:56:18 +08:00
scheme_id = body.get("scheme_id")
2026-04-23 23:37:39 +08:00
formats = body.get("formats", ["step", "stl"])
components = body.get("components", ["cavity", "core"])
2026-04-30 17:01:43 +08:00
if not task_id:
raise HTTPException(404, "缺少 task_id")
2026-05-25 10:16:05 +08:00
await _ensure_task_access(db_session, task_id, current_user.id)
task_data = await TaskQueryService.get_task_view(db_session, task_id)
2026-04-30 17:01:43 +08:00
if not task_data:
2026-04-23 23:37:39 +08:00
raise HTTPException(404, "任务不存在")
2026-05-25 10:16:05 +08:00
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",
},
}
2026-05-14 16:56:18 +08:00
cavity_shapes = processing_service.get_export_shapes(
task_id,
2026-05-25 10:16:05 +08:00
resolved_scheme_id,
2026-05-14 16:56:18 +08:00
)
2026-04-30 17:01:43 +08:00
filename = task_data.get("filename", f"mold_{task_id}")
if not cavity_shapes:
2026-05-14 16:56:18 +08:00
raise HTTPException(
409,
"导出缓存已失效或任务尚未完成,请重新分析后再导出以保证方案一致性",
)
2026-04-30 17:41:47 +08:00
2026-04-30 17:01:43 +08:00
base_filename = Path(filename).stem
2026-05-25 10:16:05 +08:00
result = cad_exporter.export_mold_results(
2026-04-23 23:37:39 +08:00
cavity_data=cavity_shapes,
base_filename=base_filename,
formats=formats,
components=components,
2026-05-25 10:16:05 +08:00
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},
2026-04-23 23:37:39 +08:00
)
2026-05-25 10:16:05 +08:00
await redis_task_manager.update_task(task_id, {"export_artifacts": merged_artifacts})
2026-04-23 23:37:39 +08:00
return {"status": "success", "data": result}
@router.get("/export-download/{filepath:path}")
async def download_export_file(
filepath: str,
2026-05-25 10:16:05 +08:00
task_id: str,
2026-04-23 23:37:39 +08:00
current_user: User = Depends(get_current_active_user),
2026-05-25 10:16:05 +08:00
db_session: AsyncSession = Depends(get_db_session),
2026-04-23 23:37:39 +08:00
):
from fastapi.responses import FileResponse
2026-05-25 10:16:05 +08:00
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))
2026-04-23 23:37:39 +08:00
if not os.path.exists(full_path):
raise HTTPException(404, "文件不存在")
2026-05-25 10:16:05 +08:00
if not os.path.abspath(full_path).startswith(os.path.abspath(cad_exporter.output_dir)):
2026-04-23 23:37:39 +08:00
raise HTTPException(403, "禁止访问")
media_types = {
2026-04-30 17:41:47 +08:00
".step": "application/step", ".stp": "application/step",
".iges": "application/iges", ".igs": "application/iges",
".stl": "model/stl", ".brep": "application/octet-stream",
2026-04-23 23:37:39 +08:00
}
ext = Path(full_path).suffix.lower()
media_type = media_types.get(ext, "application/octet-stream")
2026-04-30 17:41:47 +08:00
return FileResponse(full_path, media_type=media_type, filename=os.path.basename(full_path))
2026-04-23 23:37:39 +08:00
@router.get("/export-recommendations")
async def get_export_recommendations(
target: str = "ug",
current_user: User = Depends(get_current_active_user),
):
2026-05-25 10:16:05 +08:00
result = cad_exporter.get_export_recommendations(target)
2026-04-23 23:37:39 +08:00
return {"status": "success", "data": result}
2026-04-30 17:01:43 +08:00