fix(mcp_frame): 修复 c++_dll 工具大量注册失败与写库被文档锁拦截

- 版本校验:基座侧改用编译期常量 (ARX<<16)|SUB_ARX,不再调用运行时的
  acrxGetApiVersion()。该函数由 rxapi.lib 的 acrxGetApiVersionImpl 提供,
  是有状态实现(读写进程内全局),返回值随调用次数变化;AutoCAD 加载 ARX
  时已调用过一次,导致基座值与 DLL 值不等,绝大多数 DLL 工具被拒绝注册。
- 新增 DLL 模块缓存:同一 DLL 全进程只 LoadLibrary 一次,后续工具复用句柄
  并跳过版本校验。
- 文档锁:mcp_Tool_DllProxy::Execute 统一 lockDocument/unlockDocument,
  修复 DLL 工具在 AutoCAD 应用上下文写数据库返回 eLockViolation
  (表现为 openWriteSpace 拿不到模型空间)的问题。
- 代理类不再 FreeLibrary 模块(改由模块缓存统一卸载),避免同一 DLL 的多个
  工具重复释放、引用计数被打穿。
- 诊断日志精简为仅记录 arx_version/dll_version,且每次加载清空重写。
- 新增 verify_annotate.py:批注工具集运行时验证脚本(绘制/校验/改属性/清理)。
This commit is contained in:
gjm committed 2026-10-09 22:31:49 +08:00
1 parent 0bd290c96e
commit ec5f62670d
5 files changed
+327 -58

No files matched your search

+200
View File
@@ -0,0 +1,200 @@
# -*- coding: utf-8 -*-
"""
verify_annotate.py
批注工具集运行时验证脚本。
前提:CAD 已打开并加载 cad_mcp_frame.arx(插件 DLL),127.0.0.1:8080 在监听。
用法:
python verify_annotate.py # 绘制 -> 校验 -> 清理(默认清理)
python verify_annotate.py --keep # 保留批注,便于肉眼观察
覆盖工具:
draw_revision_cloud / add_text / add_mtext / add_polyline / add_line /
add_leader_annotation / set_entity_properties / delete_entities
"""
import socket
import json
import sys
HOST, PORT = "127.0.0.1", 8080
ANNOT_LAYER = "AI_REVIEW"
# ---------------------------------------------------------------- MCP 通信
def rpc(method, params=None, timeout=20):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(timeout)
s.connect((HOST, PORT))
req = {"jsonrpc": "2.0", "id": 1, "method": method}
if params is not None:
req["params"] = params
s.sendall(json.dumps(req).encode("utf-8"))
data = b""
while True:
chunk = s.recv(8192)
if not chunk:
break
data += chunk
if len(chunk) < 8192:
break
s.close()
return json.loads(data.decode("utf-8"))
def call_tool(name, arguments):
return rpc("tools/call", {"name": name, "arguments": arguments})
def payload(resp):
"""从 tools/call 响应里取出数据类工具返回的内层 JSON。"""
if resp is None:
return False, "no response"
if "error" in resp:
return False, resp["error"]
try:
for item in resp["result"]["content"]:
if item.get("type") == "text":
return True, json.loads(item["text"])
except Exception as e: # noqa: BLE001
return False, "parse error: %s" % e
return False, "no text content"
# ---------------------------------------------------------------- 测试驱动
RESULTS = []
def record(name, ok, detail):
RESULTS.append((name, ok, detail))
print(" [%s] %-24s %s" % ("PASS" if ok else "FAIL", name, detail))
def expect_created(name, resp, want_type):
"""校验工具返回 ok 且 created[0].type == want_type,返回 handle 列表。"""
ok, obj = payload(resp)
if not ok:
record(name, False, "调用失败: %s" % obj)
return []
if not obj.get("ok"):
record(name, False, "返回非 ok: %s" % obj)
return []
created = obj.get("created", [])
handles = [c.get("handle", "") for c in created]
types = [c.get("type", "") for c in created]
layers = [c.get("layer", "") for c in created]
if want_type not in types:
record(name, False, "类型不符: 期望 %s 实际 %s" % (want_type, types))
return handles
bad_layer = [l for l in layers if l != ANNOT_LAYER]
if bad_layer:
record(name, False, "图层不符: %s" % layers)
return handles
record(name, True, "created=%s layer=%s" % (types, layers))
return handles
def main():
keep = "--keep" in sys.argv
print("=== 批注工具集运行时验证 ===")
print("目标: %s:%d\n" % (HOST, PORT))
# 0) 连接 & 工具清单
try:
tools_resp = rpc("tools/list")
except Exception as e: # noqa: BLE001
print("[错误] 无法连接 MCP 服务: %s" % e)
print("请确认 CAD 已打开、插件已加载、8080 端口在监听。")
return 2
names = [t["name"] for t in tools_resp.get("result", {}).get("tools", [])]
want = ["draw_revision_cloud", "add_text", "add_mtext", "add_polyline",
"add_line", "add_leader_annotation", "delete_entities",
"set_entity_properties"]
missing = [w for w in want if w not in names]
print("工具总数: %d" % len(names))
if missing:
print("[FAIL] 未注册的工具: %s" % missing)
return 1
print("[PASS] 8 个批注工具均已注册\n")
created_handles = []
# 1) 云线(矩形)
print("1) 绘制工具")
h = expect_created("draw_revision_cloud", call_tool("draw_revision_cloud", {
"shape": "rect", "min_x": 0, "min_y": 0, "max_x": 100, "max_y": 60,
}), "LWPOLYLINE")
created_handles += h
# 2) 单行文字
h = expect_created("add_text", call_tool("add_text", {
"text": "AI批注-单行", "position": [0, 80], "height": 5,
}), "TEXT")
created_handles += h
# 3) 多行文字
h = expect_created("add_mtext", call_tool("add_mtext", {
"text": "AI批注-多行\n第二行", "position": [0, 100], "height": 5,
}), "MTEXT")
created_handles += h
# 4) 多段线
h = expect_created("add_polyline", call_tool("add_polyline", {
"points": [[0, 130], [60, 130], [60, 170]], "closed": False,
}), "LWPOLYLINE")
created_handles += h
# 5) 直线
h = expect_created("add_line", call_tool("add_line", {
"start": [0, 190], "end": [100, 190],
}), "LINE")
created_handles += h
# 6) 引线批注(返回 MTEXT + LEADER 两个)
h = expect_created("add_leader_annotation", call_tool("add_leader_annotation", {
"text": "引线批注", "points": [[300, 200], [200, 150]], "height": 5,
}), "LEADER")
created_handles += h
# 7) 下钻校验:取第一个实体详情,确认图层
print("\n2) 校验")
if created_handles:
ok, obj = payload(call_tool("get_entity_detail", {"handle": created_handles[0]}))
if ok and obj.get("layer") == ANNOT_LAYER:
record("get_entity_detail", True, "handle=%s layer=%s" % (created_handles[0], obj.get("layer")))
else:
record("get_entity_detail", False, "%s" % obj)
else:
record("get_entity_detail", False, "无可用 handle")
# 8) 修改实体属性(把第一个实体改红)
if created_handles:
ok, obj = payload(call_tool("set_entity_properties", {
"handle": created_handles[0], "color": 1,
}))
record("set_entity_properties", bool(ok and obj.get("count") == 1), "%s" % obj)
# 9) 清理
print("\n3) 清理")
if keep:
record("delete_entities", True, "已跳过(--keep);保留 handle: %s" % created_handles)
elif created_handles:
ok, obj = payload(call_tool("delete_entities", {"handles": created_handles}))
good = ok and obj.get("count") == len(created_handles)
record("delete_entities", good, "删除 %s / 共 %s" % (obj.get("count"), len(created_handles)))
else:
record("delete_entities", False, "无 handle 可删")
# 汇总
print("\n=== 结果汇总 ===")
passed = sum(1 for _, ok, _ in RESULTS if ok)
for name, ok, detail in RESULTS:
print(" %-4s %-24s %s" % ("OK" if ok else "NG", name, detail))
print("\n通过 %d / %d" % (passed, len(RESULTS)))
return 0 if passed == len(RESULTS) else 1
if __name__ == "__main__":
sys.exit(main())