基座(cad_mcp_frame): - 新增 RecvOneFrame: 以 '\n' 为消息边界分帧,去掉单次 recv(2048) 限制(上限 8MB); 用 select 短探测兼容不发 '\n' 的旧客户端;clientSocket 设 SO_RCVTIMEO 防卸载卡死 - JSON-RPC id 支持 string/number;无 id 的通知不回包 - isError 映射: 协议级错误保留顶层 error,工具执行错误转 result.isError=true - 响应补 '\n' 作为消息边界 Agent(新增 dwg_review 包): - mcp_client: 唯一 TCP/JSON-RPC 封装(单飞锁/分帧/重试/兼容 error 与 isError) - cad_tools: 从 tools/list 动态建表,含 15 个只读白名单 - phases: planning -> evidence -> review(受控只读取证子循环)-> report - annotator: 批注回写 AI_REVIEW 层,支持 dry-run 与撤销 - config: 配置全部走环境变量;Windows 下回退读注册表以读到新设的系统变量 其它: 新增 verify_mcp_frame.py 验证脚本;.gitignore 补 Python/AutoCAD 锁文件规则; ai.memory 记录架构决策与实测结论
117 lines
4.1 KiB
Python
117 lines
4.1 KiB
Python
"""批注回写:把审查问题标到 CAD 图上(统一落 AI_REVIEW 层)。
|
||
|
||
只在问题带有 handle 时才回写。产出两类可撤销的副作用:
|
||
- created :新建的批注实体(引线等)→ 用 delete_entities 删除
|
||
- highlighted :被临时改色的原实体 → 用 highlight_entities(restore=true) 还原
|
||
"""
|
||
|
||
from . import config
|
||
from .mcp_client import McpError, McpToolError, result_json
|
||
|
||
# 引线落点相对实体定位点的偏移(图纸单位)
|
||
_LEADER_OFFSET = (30.0, 20.0)
|
||
|
||
|
||
def _pick_point(detail):
|
||
"""从实体详情中挑一个用于放引线的点(WCS)。"""
|
||
if not isinstance(detail, dict):
|
||
return None
|
||
|
||
bbox = detail.get("bbox")
|
||
if isinstance(bbox, (list, tuple)) and len(bbox) == 4:
|
||
return [
|
||
(float(bbox[0]) + float(bbox[2])) / 2.0,
|
||
(float(bbox[1]) + float(bbox[3])) / 2.0,
|
||
0.0,
|
||
]
|
||
|
||
for key in ("position", "insert", "center", "start"):
|
||
pt = detail.get(key)
|
||
if isinstance(pt, (list, tuple)) and len(pt) >= 2:
|
||
return [float(pt[0]), float(pt[1]), float(pt[2]) if len(pt) > 2 else 0.0]
|
||
return None
|
||
|
||
|
||
def annotate(ctx, issues, *, dry_run=False, log=print):
|
||
"""按问题列表回写批注,返回 (created, highlighted) 两个 handle 列表。"""
|
||
created = []
|
||
highlighted = []
|
||
|
||
for issue in issues:
|
||
handle = issue.get("handle")
|
||
if not handle:
|
||
continue
|
||
|
||
text = f"[{issue.get('severity', '?')}] {issue.get('message', '')}"
|
||
|
||
if dry_run:
|
||
log(f"[dry-run] 批注 handle={handle} 层={config.ANNOTATE_LAYER}: {text}")
|
||
continue
|
||
|
||
# 先定位,拿不到坐标就跳过(避免产生无意义的批注)
|
||
detail = result_json(ctx.tools.call("get_entity_detail", {"handle": handle}), {})
|
||
point = _pick_point(detail)
|
||
if not point:
|
||
log(f"[跳过] handle={handle} 无法定位坐标")
|
||
continue
|
||
|
||
landing = [
|
||
point[0] + _LEADER_OFFSET[0],
|
||
point[1] + _LEADER_OFFSET[1],
|
||
0.0,
|
||
]
|
||
try:
|
||
res = result_json(
|
||
ctx.tools.call(
|
||
"add_leader_annotation",
|
||
{
|
||
"text": text,
|
||
"points": [point, landing], # 至少 2 个点,首个为箭头
|
||
"text_position": landing,
|
||
"layer": config.ANNOTATE_LAYER,
|
||
},
|
||
),
|
||
{},
|
||
)
|
||
for item in (res or {}).get("created", []):
|
||
if item.get("handle"):
|
||
created.append(item["handle"])
|
||
except (McpError, McpToolError) as exc:
|
||
log(f"[批注失败] handle={handle}: {exc}")
|
||
continue
|
||
|
||
# 引线建好后再高亮原实体(失败不影响已创建的批注)
|
||
if handle in highlighted:
|
||
continue
|
||
try:
|
||
ctx.tools.call("highlight_entities", {"handle": handle, "color": 1})
|
||
highlighted.append(handle)
|
||
except (McpError, McpToolError) as exc:
|
||
log(f"[高亮失败] handle={handle}: {exc}")
|
||
|
||
if not dry_run:
|
||
log(
|
||
f"[批注] 新建 {len(created)} 个批注实体、高亮 {len(highlighted)} 个原实体"
|
||
f"(层 {config.ANNOTATE_LAYER})"
|
||
)
|
||
return created, highlighted
|
||
|
||
|
||
def clear(ctx, handles=None, highlighted=None, *, log=print):
|
||
"""撤销批注:删除批注实体 + 还原被高亮的实体。"""
|
||
if handles:
|
||
try:
|
||
ctx.tools.call("delete_entities", {"handles": list(handles)})
|
||
log(f"[批注] 已删除 {len(handles)} 个批注实体")
|
||
except (McpError, McpToolError) as exc:
|
||
log(f"[撤销失败] {exc}")
|
||
|
||
if highlighted:
|
||
try:
|
||
ctx.tools.call(
|
||
"highlight_entities", {"handles": list(highlighted), "restore": True}
|
||
)
|
||
log(f"[批注] 已还原 {len(highlighted)} 个实体的颜色")
|
||
except (McpError, McpToolError) as exc:
|
||
log(f"[还原失败] {exc}")
|