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 记录架构决策与实测结论
This commit is contained in:
gjm committed 2026-10-13 23:44:11 +08:00
1 parent 3b1cc88262
commit ec5292debe
20 files changed
+1502 -20

No files matched your search

+84
View File
@@ -0,0 +1,84 @@
"""阶段 4:汇总报告 + 结构化问题抽取。"""
import json
from datetime import datetime
from pathlib import Path
from .. import config
def summarize(ctx, findings, log=print):
"""用 LLM 把各图框结论汇总为总报告。"""
if not findings:
return "未识别到可审查的图框,或所有图框均无有效包围盒。"
report_input = "\n\n".join(
f"【图框 {f['frame']}(图幅 {f.get('sheet')})】\n{f['finding']}" for f in findings
)
system = (
"你是图纸审查负责人。请把下面各图框的审查意见汇总为一份结构化的审查报告:"
"先给整体结论,再按图框列出问题,最后给出整改建议。语言简洁、条目化。"
)
try:
return ctx.llm.chat_text(system, report_input)
except Exception as exc: # noqa: BLE001
log(f"[汇总失败] {exc}")
return report_input
_EXTRACT_SYSTEM = (
"你是结构化信息抽取器。请把下面的图纸审查意见抽取为 JSON 数组,"
"每个元素形如:"
'{"frame": <图框序号>, "handle": "<实体句柄,没有则省略>", '
'"category": "<图层/文字/标注/几何/图面整体>", '
'"severity": "<high|medium|low>", "message": "<问题描述>"}。'
"只输出 JSON 数组,不要输出任何解释、不要用 markdown 代码块包裹。"
"若某条意见无法定位到具体实体,省略 handle 字段。"
)
def extract_issues(ctx, findings, log=print):
"""把自由文本的审查意见抽取为结构化问题列表(用于批注回写)。"""
if not findings:
return []
source = "\n\n".join(
f"图框 {f['frame']}:\n{f['finding']}" for f in findings
)
try:
text = ctx.llm.chat_text(_EXTRACT_SYSTEM, source)
text = text.strip()
if text.startswith("```"):
text = text.strip("`")
text = text.split("\n", 1)[1] if "\n" in text else text
data = json.loads(text)
if isinstance(data, list):
return data
except Exception as exc: # noqa: BLE001
log(f"[结构化抽取失败] {exc}")
return []
def save_report(markdown, findings, plan, issues=None, log=print):
"""落盘 Markdown + JSON 报告,返回两个路径。"""
out_dir = Path(config.REPORT_DIR)
out_dir.mkdir(parents=True, exist_ok=True)
stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
md_path = out_dir / f"review_{stamp}.md"
json_path = out_dir / f"review_{stamp}.json"
md_path.write_text(markdown, encoding="utf-8")
json_path.write_text(
json.dumps(
{
"generated_at": stamp,
"findings": findings,
"issues": issues or [],
"frames": plan.get("frames"),
},
ensure_ascii=False,
indent=2,
),
encoding="utf-8",
)
log(f"[报告] 已写入 {md_path} 与 {json_path}")
return md_path, json_path