from __future__ import annotations from collections.abc import Mapping from pathlib import Path from .scdm_backend import ScdmBackendInfo, is_scdm_disabled, load_scdm_backend_cache from .scdm_capabilities import CAPABILITY_DEFINITIONS, ScdmCapabilityDefinition def cached_scdm_backend_payload(project_root: str | Path | None = None) -> dict[str, object] | None: if is_scdm_disabled(): return {"disabled": True, "reason": "disabled", "message": "SCDM backend is disabled by environment."} backend = load_scdm_backend_cache(project_root_override=project_root) return backend.to_cache() if backend is not None else None def summarize_scdm_runtime( *, backend: ScdmBackendInfo | Mapping[str, object] | None = None, cache_state: str = "", cache_message: str = "", feature_cache: Mapping[str, object] | None = None, ) -> dict[str, object]: backend_payload = _backend_payload(backend) disabled = _backend_disabled(backend) state = str(cache_state or "empty").strip().lower() message = _compact(str(cache_message or "").strip(), 120) count = _feature_cache_counts(feature_cache) if disabled: headline = "SCDM:已关闭,当前使用 OCCT/Analysis Situs 兜底" path = "" elif backend_payload: version = str(backend_payload.get("version") or "").strip() source = _source_label(str(backend_payload.get("source") or "").strip()) version_text = f" {version}" if version else "" headline = f"SCDM:已配置{version_text}({source})" path = str(backend_payload.get("path") or "").strip() else: headline = "SCDM:未配置,当前可用 OCCT/Analysis Situs 兜底" path = "" if disabled: detail = "检测到 SCDM 禁用开关;本次不会启动 SpaceClaim.exe。" elif state == "running": detail = "正在后台识别可修改参数;界面可继续旋转查看模型。" elif state == "ready": object_text = f"{count['objects']} 个对象" if count["objects"] else "0 个对象" capability_text = f"{count['capabilities']} 项能力" if count["capabilities"] else "0 项能力" detail = f"识别缓存已就绪:{object_text},{capability_text}。" elif state == "failed": reason = message or "未拿到 SCDM 识别结果" detail = f"识别未启用:{reason};当前使用本软件已有能力。" elif state == "deferred": detail = message or "大模型已延后 SCDM 全量识别,优先保证查看、旋转和点选流畅。" elif state == "stale": detail = message or "缓存已失效,导入、编辑、撤销或重做后会后台重新识别。" else: detail = "导入 STEP 后会尝试启动 SCDM 识别;找不到时使用 OCCT/Analysis Situs 兜底。" tooltip_lines = [headline, detail] if path: tooltip_lines.append(f"路径:{path}") if backend_payload: run_script_ok = backend_payload.get("runScriptOk") license_ok = backend_payload.get("licenseOk") tooltip_lines.append(f"/RunScript:{_ok_text(run_script_ok)}") tooltip_lines.append(f"许可证:{_ok_text(license_ok)}") return { "headline": headline, "detail": detail, "tooltip": "\n".join(line for line in tooltip_lines if line), "backendReady": bool(backend_payload) and not disabled, "cacheState": state, "objectCount": count["objects"], "capabilityCount": count["capabilities"], } def summarize_scdm_capability_progress( *, feature_cache: Mapping[str, object] | None = None, execution_ready: bool | set[str] | list[str] | tuple[str, ...] = False, ) -> dict[str, object]: ready_keys = _execution_ready_keys(execution_ready) detection_counts = _cache_capability_counts(feature_cache) blocked_counts = _cache_blocked_capability_counts(feature_cache) planned_counts = _planned_capability_counts(feature_cache) hint_counts = _geometry_candidate_hint_counts(feature_cache) discovered_summary = _discovered_not_productized_summary(feature_cache) probe_evidence = _probe_evidence_summary(feature_cache) rows: list[dict[str, object]] = [] for key, definition in sorted(CAPABILITY_DEFINITIONS.items(), key=lambda item: (_stage_sort_key(item[1].roadmap_stage), item[0])): detected = int(detection_counts.get(key, 0)) blocked = int(blocked_counts.get(key, 0)) planned_detected = int(planned_counts.get(key, 0)) hint_detected = int(hint_counts.get(key, 0)) runner_ready = _capability_runner_ready(key, execution_ready, ready_keys) status, reason = _capability_progress_status( key, definition, detected=detected, blocked=blocked, planned_detected=planned_detected, hint_detected=hint_detected, runner_ready=runner_ready, ) executable = detected if definition.productized and runner_ready else 0 if blocked: executable = max(0, executable - blocked) rows.append( { "key": key, "displayName": definition.display_name, "roadmapStage": definition.roadmap_stage, "productized": definition.productized, "runnerReady": runner_ready, "detectedCount": detected, "plannedDetectedCount": planned_detected, "hintDetectedCount": hint_detected, "blockedCount": blocked, "executableCount": executable, "status": status, "reason": reason, } ) executable_count = sum(int(row["executableCount"]) for row in rows) productized_count = sum(1 for row in rows if bool(row["productized"])) runner_ready_count = sum(1 for row in rows if bool(row["productized"]) and bool(row["runnerReady"])) planned_detected_total = sum(int(row["plannedDetectedCount"]) for row in rows) hint_detected_total = sum(int(row["hintDetectedCount"]) for row in rows) blocked_total = sum(int(row["blockedCount"]) for row in rows) return { "rows": rows, "summary": { "defined": len(rows), "productized": productized_count, "runnerReady": runner_ready_count, "detectedCapabilities": sum(int(row["detectedCount"]) for row in rows), "executableCapabilities": executable_count, "plannedDetected": planned_detected_total, "geometryHints": hint_detected_total, "backendBlocked": blocked_total, "discoveredNotProductized": discovered_summary["count"], "faceAdjacency": probe_evidence["faceAdjacency"], "circularEdges": probe_evidence["circularEdges"], "inventoryObjectTypes": probe_evidence["inventoryObjectTypes"], "inventoryOperationCandidates": probe_evidence["inventoryOperationCandidates"], "derivedFeatureCandidates": probe_evidence["derivedFeatureCandidates"], }, "productizedLines": _capability_progress_lines( row for row in rows if bool(row["productized"]) ), "plannedLines": _capability_progress_lines( row for row in rows if not bool(row["productized"]) and ( int(row["plannedDetectedCount"]) > 0 or int(row["detectedCount"]) > 0 or int(row["hintDetectedCount"]) > 0 ) ), "roadmapLines": _capability_progress_lines( row for row in rows if not bool(row["productized"]) and int(row["plannedDetectedCount"]) <= 0 and int(row["detectedCount"]) <= 0 and int(row["hintDetectedCount"]) <= 0 ), "discoveredNotProductized": discovered_summary, "probeEvidence": probe_evidence, } def _backend_payload(backend: ScdmBackendInfo | Mapping[str, object] | None) -> dict[str, object]: if isinstance(backend, ScdmBackendInfo): return backend.to_cache() if not isinstance(backend, Mapping): return {} nested = backend.get("backend") if isinstance(nested, ScdmBackendInfo): return nested.to_cache() if isinstance(nested, Mapping): return _backend_payload(nested) path = str(backend.get("path") or "").strip() if not path: return {} return { "path": path, "source": str(backend.get("source") or ""), "version": str(backend.get("version") or ""), "verifiedAt": str(backend.get("verifiedAt") or ""), "runScriptOk": backend.get("runScriptOk"), "licenseOk": backend.get("licenseOk"), "message": str(backend.get("message") or ""), } def _backend_disabled(backend: ScdmBackendInfo | Mapping[str, object] | None) -> bool: return isinstance(backend, Mapping) and bool(backend.get("disabled")) def _feature_cache_counts(feature_cache: Mapping[str, object] | None) -> dict[str, int]: if not isinstance(feature_cache, Mapping): return {"objects": 0, "capabilities": 0} objects = feature_cache.get("objects") if not isinstance(objects, list): return {"objects": 0, "capabilities": 0} capability_count = 0 object_count = 0 for item in objects: if not isinstance(item, Mapping): continue object_count += 1 capabilities = item.get("capabilities") if isinstance(capabilities, list): capability_count += sum(1 for capability in capabilities if isinstance(capability, Mapping)) return {"objects": object_count, "capabilities": capability_count} def _cache_capability_counts(feature_cache: Mapping[str, object] | None) -> dict[str, int]: result: dict[str, int] = {} if not isinstance(feature_cache, Mapping): return result objects = feature_cache.get("objects") if not isinstance(objects, list): return result for item in objects: if not isinstance(item, Mapping): continue capabilities = item.get("capabilities") if not isinstance(capabilities, list): continue for capability in capabilities: if not isinstance(capability, Mapping): continue key = str(capability.get("key") or "").strip() if key: result[key] = result.get(key, 0) + 1 return result def _cache_blocked_capability_counts(feature_cache: Mapping[str, object] | None) -> dict[str, int]: result: dict[str, int] = {} if not isinstance(feature_cache, Mapping): return result objects = feature_cache.get("objects") if not isinstance(objects, list): return result for item in objects: if not isinstance(item, Mapping): continue object_block = str(item.get("blockReason") or "").strip() capabilities = item.get("capabilities") if not isinstance(capabilities, list): continue for capability in capabilities: if not isinstance(capability, Mapping): continue key = str(capability.get("key") or "").strip() if not key: continue capability_block = str(capability.get("blockReason") or "").strip() if object_block or capability_block or capability.get("editable") is False: result[key] = result.get(key, 0) + 1 return result def _planned_capability_counts(feature_cache: Mapping[str, object] | None) -> dict[str, int]: diagnostics = _cache_diagnostics(feature_cache) planned = diagnostics.get("planned_not_productized") result: dict[str, int] = {} if not isinstance(planned, list): return result for item in planned: if not isinstance(item, Mapping): continue key = str(item.get("capabilityKey") or "").strip() if key: result[key] = result.get(key, 0) + 1 return result def _geometry_candidate_hint_counts(feature_cache: Mapping[str, object] | None) -> dict[str, int]: diagnostics = _cache_diagnostics(feature_cache) hints = diagnostics.get("geometry_candidate_hints") result: dict[str, int] = {} if not isinstance(hints, list): return result for item in hints: if not isinstance(item, Mapping): continue key = str(item.get("capabilityKey") or "").strip() if not key: continue count = _int_value(item.get("evidenceCount")) result[key] = result.get(key, 0) + max(count, 1) return result def _discovered_not_productized_summary(feature_cache: Mapping[str, object] | None) -> dict[str, object]: diagnostics = _cache_diagnostics(feature_cache) discovered = diagnostics.get("discovered_not_productized") by_type: dict[str, int] = {} if not isinstance(discovered, list): return {"count": 0, "byObjectType": {}, "lines": []} for item in discovered: if not isinstance(item, Mapping): continue object_type = str(item.get("objectType") or "object").strip() or "object" by_type[object_type] = by_type.get(object_type, 0) + 1 lines = [f"{name}:{count} 个" for name, count in sorted(by_type.items(), key=lambda item: (-item[1], item[0]))[:6]] return {"count": sum(by_type.values()), "byObjectType": by_type, "lines": lines} def _probe_evidence_summary(feature_cache: Mapping[str, object] | None) -> dict[str, object]: diagnostics = _cache_diagnostics(feature_cache) face_adjacency = diagnostics.get("face_adjacency") edge_summary = diagnostics.get("edge_geometry_summary") feature_inventory = diagnostics.get("feature_inventory") adjacency_count = len(face_adjacency) if isinstance(face_adjacency, list) else 0 edge_summary = edge_summary if isinstance(edge_summary, Mapping) else {} feature_inventory = feature_inventory if isinstance(feature_inventory, Mapping) else {} edge_kind_counts = edge_summary.get("edgeKindCounts") edge_kind_counts = edge_kind_counts if isinstance(edge_kind_counts, Mapping) else {} object_type_counts = _mapping_count_dict(feature_inventory.get("objectTypeCounts")) surface_type_counts = _mapping_count_dict(feature_inventory.get("surfaceTypeCounts")) operation_counts = _mapping_count_dict(feature_inventory.get("operationCounts")) geometry_hints = diagnostics.get("geometry_candidate_hints") geometry_hint_lines = _geometry_candidate_hint_lines(geometry_hints) derived_candidates = diagnostics.get("derived_feature_candidates") derived_candidate_lines = _derived_feature_candidate_lines(derived_candidates) derived_candidate_count = len(derived_candidates) if isinstance(derived_candidates, list) else 0 circular_edges = _int_value(edge_summary.get("circularEdgeCount")) if circular_edges <= 0: circular_edges = _int_value(edge_kind_counts.get("circular")) linear_edges = _int_value(edge_kind_counts.get("linear")) total_edges = _int_value(edge_summary.get("totalEdgeCount")) radius_buckets = edge_summary.get("circularRadiusBuckets") radius_bucket_count = len(radius_buckets) if isinstance(radius_buckets, list) else 0 lines = [] if adjacency_count: lines.append(f"Face 邻接 {adjacency_count} 组") if total_edges: lines.append(f"Edge {total_edges} 条") if circular_edges: lines.append(f"圆边 {circular_edges} 条") if linear_edges: lines.append(f"直边 {linear_edges} 条") if radius_bucket_count: lines.append(f"圆边半径分组 {radius_bucket_count} 类") object_lines = _count_summary_lines(object_type_counts, label="对象") surface_lines = _count_summary_lines(surface_type_counts, label="曲面") operation_lines = _count_summary_lines(operation_counts, label="命令候选") lines.extend(object_lines[:2]) lines.extend(surface_lines[:2]) lines.extend(operation_lines[:2]) lines.extend(derived_candidate_lines[:3]) lines.extend(geometry_hint_lines[:4]) return { "faceAdjacency": adjacency_count, "totalEdges": total_edges, "circularEdges": circular_edges, "linearEdges": linear_edges, "radiusBucketCount": radius_bucket_count, "inventoryObjectTypes": sum(object_type_counts.values()), "inventorySurfaceTypes": sum(surface_type_counts.values()), "inventoryOperationCandidates": sum(operation_counts.values()), "derivedFeatureCandidates": derived_candidate_count, "derivedFeatureCandidateLines": derived_candidate_lines, "objectTypeCounts": object_type_counts, "surfaceTypeCounts": surface_type_counts, "operationCounts": operation_counts, "geometryHintLines": geometry_hint_lines, "lines": lines, } def _derived_feature_candidate_lines(value: object, *, limit: int = 4) -> list[str]: if not isinstance(value, list): return [] counts: dict[str, int] = {} for item in value: if not isinstance(item, Mapping): continue object_type = str(item.get("objectType") or "object").strip() or "object" counts[object_type] = counts.get(object_type, 0) + 1 rows = sorted(counts.items(), key=lambda item: (-int(item[1]), item[0]))[: max(1, int(limit))] return [f"派生候选 {name}:{count}" for name, count in rows] def _geometry_candidate_hint_lines(value: object, *, limit: int = 4) -> list[str]: if not isinstance(value, list): return [] best: dict[str, dict[str, object]] = {} for item in value: if not isinstance(item, Mapping): continue key = str(item.get("capabilityKey") or "").strip() if not key: continue count = max(_int_value(item.get("evidenceCount")), 1) existing = best.get(key) if existing is None or count > int(existing.get("evidenceCount") or 0): best[key] = { "displayName": str(item.get("displayName") or key), "evidenceCount": count, "confidence": str(item.get("confidence") or ""), } rows = sorted(best.items(), key=lambda item: (-int(item[1].get("evidenceCount") or 0), item[0]))[: max(1, int(limit))] return [ f"几何候选 {payload['displayName']}:{payload['evidenceCount']}({payload['confidence'] or 'unknown'})" for _key, payload in rows ] def _mapping_count_dict(value: object) -> dict[str, int]: if not isinstance(value, Mapping): return {} result: dict[str, int] = {} for key, count in value.items(): text = str(key or "").strip() or "unknown" number = _int_value(count) if number > 0: result[text] = number return result def _count_summary_lines(counts: Mapping[str, int], *, label: str, limit: int = 4) -> list[str]: if not counts: return [] rows = sorted(counts.items(), key=lambda item: (-int(item[1]), item[0]))[: max(1, int(limit))] summary = ",".join(f"{name}:{count}" for name, count in rows) return [f"{label}分布 {summary}"] def _int_value(value: object) -> int: try: return int(value) except (TypeError, ValueError): return 0 def _cache_diagnostics(feature_cache: Mapping[str, object] | None) -> Mapping[str, object]: if not isinstance(feature_cache, Mapping): return {} diagnostics = feature_cache.get("diagnostics") return diagnostics if isinstance(diagnostics, Mapping) else {} def _execution_ready_keys(execution_ready: bool | set[str] | list[str] | tuple[str, ...]) -> set[str]: if isinstance(execution_ready, bool): return set() try: return {str(item) for item in execution_ready} except TypeError: return set() def _capability_runner_ready( key: str, execution_ready: bool | set[str] | list[str] | tuple[str, ...], ready_keys: set[str], ) -> bool: return bool(execution_ready) if isinstance(execution_ready, bool) else key in ready_keys def _capability_progress_status( key: str, definition: ScdmCapabilityDefinition, *, detected: int, blocked: int, planned_detected: int, hint_detected: int, runner_ready: bool, ) -> tuple[str, str]: if definition.productized and runner_ready and detected > blocked: return "已开放", "已识别到对象时会显示在特征参数表。" if definition.productized and runner_ready and blocked: return "已开放但被后端阻止", "当前模型里识别到该能力,但 SCDM 命令、对象状态或安全守门暂时阻止执行。" if definition.productized and runner_ready: return "已开放待识别", "执行链路已接入,当前 cache 还没有识别到可执行对象。" if definition.productized and detected: return "已识别待执行器", "能力已进入产品字典,但当前 UI 执行器还未开放。" if definition.productized: return "已产品化待对象", "能力已定义,等待 SCDM 在当前模型中识别到对象。" if planned_detected or detected: return "已识别待验证", definition.block_reason or "已识别到候选,但还没有完成真实 STEP 回测。" if hint_detected: return "几何证据待分类", "SCDM probe 已看到相关曲面/边/命令线索,但还没有确认成可执行特征对象。" return "路线中待接入", definition.block_reason or f"{key} 还没有接入可执行闭环。" def _capability_progress_lines(rows: object) -> list[str]: result: list[str] = [] for row in rows: # type: ignore[assignment] if not isinstance(row, Mapping): continue display = str(row.get("displayName") or row.get("key") or "").strip() status = str(row.get("status") or "").strip() detected = int(row.get("detectedCount") or 0) planned = int(row.get("plannedDetectedCount") or 0) hinted = int(row.get("hintDetectedCount") or 0) blocked = int(row.get("blockedCount") or 0) suffix_parts = [] if detected: suffix_parts.append(f"cache {detected}") if planned: suffix_parts.append(f"候选 {planned}") if hinted: suffix_parts.append(f"证据 {hinted}") if blocked: suffix_parts.append(f"阻止 {blocked}") suffix = f"({','.join(suffix_parts)})" if suffix_parts else "" result.append(f"- {display}:{status}{suffix}") return result def _stage_sort_key(stage: str) -> tuple[int, int, str]: text = str(stage or "") numbers: list[int] = [] for part in text.replace("S", "").split("."): try: numbers.append(int(part)) except ValueError: pass while len(numbers) < 2: numbers.append(0) return numbers[0], numbers[1], text def _source_label(source: str) -> str: if source.startswith("registry:"): return "注册表" if source.startswith("env:"): return "环境变量" if source.startswith("common:"): return "常见安装目录" if source.lower() == "path": return "PATH" if source == "manual": return "手动配置" if source == "cache": return "缓存" return source or "未知来源" def _ok_text(value: object) -> str: if value is True: return "可用" if value is False: return "不可用" return "未验证" def _compact(text: str, limit: int) -> str: text = " ".join(text.split()) if len(text) <= limit: return text return text[: max(limit - 1, 0)].rstrip() + "…" __all__ = ["cached_scdm_backend_payload", "summarize_scdm_capability_progress", "summarize_scdm_runtime"]