569 lines
23 KiB
Python
569 lines
23 KiB
Python
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"]
|