feat: Enhance PluginHttpClient and SoftwareConfig for improved task handling and configuration management

This commit is contained in:
sladro 2026-03-03 18:17:32 +08:00
parent 08623bf4d6
commit 9008201d51
5 changed files with 165 additions and 4 deletions

View File

@ -65,11 +65,16 @@ class SoftwareConfig:
def __init__(self, config_path: str):
self.config_path = config_path
self._config = None
self._config_mtime: Optional[float] = None
def load_config(self) -> dict:
try:
with open(self.config_path, "r", encoding="utf-8") as f:
self._config = yaml.safe_load(f) or {}
try:
self._config_mtime = Path(self.config_path).stat().st_mtime
except OSError:
self._config_mtime = None
return self._config
except FileNotFoundError as exc:
raise FileNotFoundError(f"Software config not found: {self.config_path}") from exc
@ -86,6 +91,18 @@ class SoftwareConfig:
def _ensure_loaded(self):
if self._config is None:
self.load_config()
return
try:
current_mtime = Path(self.config_path).stat().st_mtime
except OSError:
current_mtime = None
if self._config_mtime is None or current_mtime is None:
return
if current_mtime > self._config_mtime:
self.load_config()
def get_software_list(self) -> List[str]:
self._ensure_loaded()

View File

@ -2,6 +2,7 @@
import asyncio
import json
import os
from typing import Any, Dict, Optional
from urllib import error, parse, request
@ -35,6 +36,14 @@ class PluginHttpClient:
url = parse.urljoin(base_url.rstrip("/") + "/", submit_path.lstrip("/"))
request_payload = self._build_payload(payload=payload, body_mode=body_mode)
if software_id == "creo" and task_type == "open_model":
request_payload.setdefault("software_type", "creo")
# Creo plugin expects snake_case file path keys.
raw_path = request_payload.get("file_path") or request_payload.get("filePath")
if isinstance(raw_path, str) and raw_path.strip():
request_payload["file_path"] = raw_path
request_payload.setdefault("dirname", os.path.dirname(raw_path))
request_payload.setdefault("filename", os.path.basename(raw_path))
try:
return await asyncio.to_thread(self._post_json, url, request_payload, timeout)
@ -105,7 +114,7 @@ class PluginHttpClient:
@staticmethod
def _post_json(url: str, payload: Dict[str, Any], timeout: int) -> Dict[str, Any]:
raw = json.dumps(payload).encode("utf-8")
raw = json.dumps(payload, ensure_ascii=False).encode("utf-8")
req = request.Request(
url=url,
data=raw,

View File

@ -14,6 +14,20 @@ file_storage:
- .rte
plugins:
creo:
auto_close_exclude_task_types:
- open_model
- close_model
auto_close_include_task_types:
- shrinkwrap_shell
- creo_shrinkwrap
auto_close_model_after_tasks: true
auto_open_exclude_task_types:
- open_model
- close_model
auto_open_include_task_types:
- shrinkwrap_shell
- creo_shrinkwrap
auto_open_model_before_tasks: true
base_url: http://localhost:12345
callback_timeout_sec: 60
callback_token: creo-callback-token
@ -23,8 +37,10 @@ plugins:
pre_batch_cleanup_task_params:
force_close: true
pre_batch_cleanup_task_type: close_model
pre_batch_cleanup_ignore_error_markers: []
close_model_ignore_error_markers: []
pre_batch_cleanup_ignore_error_markers:
- No current model loaded
close_model_ignore_error_markers:
- No current model loaded
request_timeout_sec: 10
retry_backoff_sec:
- 1
@ -37,6 +53,7 @@ plugins:
path: /api/model/close
open_model:
body_mode: file_path
completion_mode: sync
path: /api/model/open
shell_analysis:
body_mode: task_params_only
@ -46,6 +63,10 @@ plugins:
body_mode: task_params_only
completion_mode: sync
path: /api/creo/shrinkwrap/shell
creo_shrinkwrap:
body_mode: task_params_only
completion_mode: sync
path: /api/creo/shrinkwrap/shell
pdms:
base_url: http://localhost:9001
callback_timeout_sec: 60

View File

@ -32,7 +32,10 @@ async def test_plugin_http_client_uses_creo_open_model_endpoint_and_payload(monk
assert response["status"] == "success"
assert captured["url"].endswith("/api/model/open")
assert captured["payload"]["filePath"].endswith("part.prt")
assert captured["payload"]["file_path"].endswith("part.prt")
assert captured["payload"]["dirname"].endswith("models")
assert captured["payload"]["filename"] == "part.prt"
assert captured["payload"]["software_type"] == "creo"
assert captured["payload"]["execution_id"] == "exec-1"
@ -143,3 +146,36 @@ async def test_plugin_http_client_uses_revit_close_model_empty_payload(monkeypat
assert response["success"] is True
assert captured["url"].endswith("/api/close")
assert captured["payload"] == {}
@pytest.mark.asyncio
async def test_plugin_http_client_maps_creo_shrinkwrap_alias_to_shell_endpoint(monkeypatch):
client = PluginHttpClient()
captured = {}
def fake_post_json(url, payload, timeout):
captured["url"] = url
captured["payload"] = payload
captured["timeout"] = timeout
return {"success": True}
monkeypatch.setattr(PluginHttpClient, "_post_json", staticmethod(fake_post_json))
response = await client.submit_task(
"creo",
"creo_shrinkwrap",
{
"model_path": r"C:\\models\\overall_top_design.asm",
"task_params": {"quality": "normal"},
"execution_id": "exec-creo-shrinkwrap",
"batch_id": "batch-creo-shrinkwrap",
"item_id": "item-creo-shrinkwrap",
"callback_url": "http://localhost/callback",
"attempt": 0,
},
)
assert response["success"] is True
assert captured["url"].endswith("/api/creo/shrinkwrap/shell")
assert captured["payload"]["quality"] == "normal"
assert captured["payload"]["execution_id"] == "exec-creo-shrinkwrap"

View File

@ -27,6 +27,12 @@ class FakePluginHttpClient:
behavior = payload.get("task_params", {}).get("behavior", "success")
attempt = payload.get("attempt", 0)
if task_type == "open_model":
return {"success": True, "code": 200}
if task_type == "close_model" and behavior == "no_current_model_loaded":
return {"success": False, "error": "No current model loaded", "code": 500}
if task_type == "close_model" and behavior == "no_document_open":
return {"success": False, "error": "NO_DOCUMENT_OPEN", "code": 409, "message": "没有打开的文档"}
@ -200,6 +206,78 @@ async def test_pre_batch_cleanup_ignores_no_document_open():
await manager.stop()
@pytest.mark.asyncio
async def test_creo_pre_batch_cleanup_ignores_no_current_model_loaded():
callback_registry = PluginCallbackRegistry()
fake_client = FakePluginHttpClient(callback_registry)
router = CadTaskRouter({".asm": "creo"})
creo_plugin = software_config._config.setdefault("plugins", {}).setdefault("creo", {})
creo_plugin["pre_batch_cleanup_enabled"] = True
creo_plugin["pre_batch_cleanup_task_type"] = "close_model"
creo_plugin["pre_batch_cleanup_task_params"] = {"behavior": "no_current_model_loaded"}
creo_plugin["pre_batch_cleanup_ignore_error_markers"] = ["No current model loaded"]
creo_plugin["auto_open_model_before_tasks"] = False
creo_plugin["auto_close_model_after_tasks"] = False
creo_plugin.setdefault("tasks", {}).setdefault("close_model", {})["completion_mode"] = "sync"
creo_plugin.setdefault("tasks", {}).setdefault("shrinkwrap_shell", {})["completion_mode"] = "submit_only"
manager = CadBatchManager(task_router=router, plugin_client=fake_client, callback_registry=callback_registry)
await manager.start()
request = BatchSubmitRequest(
items=[BatchSubmitItem(model_path="a.asm", task_type="shrinkwrap_shell", task_params={})],
)
try:
batch = await manager.create_batch(request, submitter_id="tester")
final_batch = await _wait_batch_terminal(manager, batch.id)
items = await manager.get_batch_items(batch.id)
assert final_batch.status == BatchStatus.COMPLETED
assert items[0].status.value == "succeeded"
assert len(fake_client.calls) == 2
assert fake_client.calls[0]["task_type"] == "close_model"
assert fake_client.calls[1]["task_type"] == "shrinkwrap_shell"
finally:
await manager.stop()
@pytest.mark.asyncio
async def test_creo_auto_open_runs_before_creo_shrinkwrap():
callback_registry = PluginCallbackRegistry()
fake_client = FakePluginHttpClient(callback_registry)
router = CadTaskRouter({".asm": "creo"})
creo_plugin = software_config._config.setdefault("plugins", {}).setdefault("creo", {})
creo_plugin["pre_batch_cleanup_enabled"] = False
creo_plugin["auto_open_model_before_tasks"] = True
creo_plugin["auto_open_include_task_types"] = ["creo_shrinkwrap"]
creo_plugin["auto_close_model_after_tasks"] = False
creo_plugin.setdefault("tasks", {}).setdefault("open_model", {})["completion_mode"] = "sync"
creo_plugin.setdefault("tasks", {}).setdefault("creo_shrinkwrap", {})["completion_mode"] = "submit_only"
manager = CadBatchManager(task_router=router, plugin_client=fake_client, callback_registry=callback_registry)
await manager.start()
request = BatchSubmitRequest(
items=[BatchSubmitItem(model_path="a.asm", task_type="creo_shrinkwrap", task_params={})],
)
try:
batch = await manager.create_batch(request, submitter_id="tester")
final_batch = await _wait_batch_terminal(manager, batch.id)
items = await manager.get_batch_items(batch.id)
assert final_batch.status == BatchStatus.COMPLETED
assert items[0].status.value == "succeeded"
assert len(fake_client.calls) == 2
assert fake_client.calls[0]["task_type"] == "open_model"
assert fake_client.calls[1]["task_type"] == "creo_shrinkwrap"
finally:
await manager.stop()
@pytest.mark.asyncio
async def test_auto_close_ignores_no_document_open():
callback_registry = PluginCallbackRegistry()