Files
cad-agent/dwg_review/annotator.py
T
gjm ec5292debe feat(mcp): 基座换行分帧与 isError 映射;新增 dwg_review 审查 Agent
基座(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 记录架构决策与实测结论
2026-10-13 23:44:11 +08:00

117 lines
4.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""批注回写:把审查问题标到 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}")