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:
1 parent
3b1cc88262
commit
ec5292debe
20 files changed
+1502
-20
No files matched your search
@@ -42,8 +42,8 @@ namespace
|
||||
std::string rawJson;
|
||||
};
|
||||
|
||||
// 处理工具查询请求
|
||||
std::string HandleToolsList(int msgId)
|
||||
// 处理工具查询请求(msgId 允许 string 或 number)
|
||||
std::string HandleToolsList(const nlohmann::json &msgId)
|
||||
{
|
||||
nlohmann::json resp;
|
||||
resp["jsonrpc"] = "2.0";
|
||||
@@ -55,8 +55,8 @@ namespace
|
||||
return resp.dump();
|
||||
}
|
||||
|
||||
// 处理工具调用请求
|
||||
std::string HandleToolsCall(int msgId, const nlohmann::json ¶ms)
|
||||
// 处理工具调用请求(msgId 允许 string 或 number)
|
||||
std::string HandleToolsCall(const nlohmann::json &msgId, const nlohmann::json ¶ms)
|
||||
{
|
||||
nlohmann::json resp;
|
||||
resp["jsonrpc"] = "2.0";
|
||||
@@ -68,8 +68,34 @@ namespace
|
||||
// 让工具集去执行,返回 payload 可能是 {"result": ...} 也可能是 {"error": ...}
|
||||
nlohmann::json payload = g_mcpTools.Call(toolName, args);
|
||||
|
||||
// 将 payload 注入到外层信封中
|
||||
resp.update(payload);
|
||||
if (payload.contains("error") && payload["error"].is_object())
|
||||
{
|
||||
int code = payload["error"].value("code", -32603);
|
||||
std::string message = payload["error"].value("message", "");
|
||||
|
||||
// 协议级错误(JSON-RPC 保留码:解析/请求/方法/参数)保持为顶层 error;
|
||||
// 工具执行错误按 MCP 规范改为 result.isError = true,错误信息进 content[0].text。
|
||||
if (code == -32700 || code == -32600 || code == -32601 || code == -32602)
|
||||
{
|
||||
resp.update(payload);
|
||||
}
|
||||
else
|
||||
{
|
||||
resp["result"] = {
|
||||
{ "isError", true },
|
||||
{ "content", { { { "type", "text" }, { "text", message } } } }
|
||||
};
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 正常结果:将 payload({"result": ...})注入到外层信封中
|
||||
resp.update(payload);
|
||||
|
||||
// 显式标注 isError=false(MCP 规范字段)
|
||||
if (resp.contains("result") && resp["result"].is_object())
|
||||
resp["result"]["isError"] = false;
|
||||
}
|
||||
|
||||
return resp.dump();
|
||||
}
|
||||
@@ -92,31 +118,40 @@ namespace
|
||||
else
|
||||
{
|
||||
std::string method = req.value("method", "");
|
||||
int msgId = req.value("id", 0);
|
||||
|
||||
// id 允许 string 或 number;缺失 id 表示这是一条"通知",按规范不应产生任何响应
|
||||
bool isNotification = !req.contains("id");
|
||||
nlohmann::json idVal = isNotification ? nlohmann::json(nullptr) : req["id"];
|
||||
|
||||
if (method == "tools/list")
|
||||
{
|
||||
responseStr = HandleToolsList(msgId);
|
||||
responseStr = HandleToolsList(idVal);
|
||||
}
|
||||
else if (method == "tools/call")
|
||||
{
|
||||
responseStr = HandleToolsCall(msgId, req.value("params", nlohmann::json::object()));
|
||||
responseStr = HandleToolsCall(idVal, req.value("params", nlohmann::json::object()));
|
||||
}
|
||||
else
|
||||
{
|
||||
// 找不到方法
|
||||
nlohmann::json err;
|
||||
err["jsonrpc"] = "2.0";
|
||||
err["id"] = msgId;
|
||||
err["id"] = idVal;
|
||||
err["error"] = { {"code", -32601}, {"message", "Method not found"} };
|
||||
responseStr = err.dump();
|
||||
}
|
||||
|
||||
// 通知不回包(send 处对空串已有保护)
|
||||
if (isNotification)
|
||||
responseStr.clear();
|
||||
}
|
||||
|
||||
// 通过传递过来的 Socket 将结果发送回大模型端
|
||||
if (pTask->clientSocket != INVALID_SOCKET)
|
||||
// 响应同样以 '\n' 结尾作为消息边界(json.loads 容忍尾部换行,旧客户端不受影响)
|
||||
if (pTask->clientSocket != INVALID_SOCKET && !responseStr.empty())
|
||||
{
|
||||
send(pTask->clientSocket, responseStr.c_str(), responseStr.length(), 0);
|
||||
responseStr.push_back('\n');
|
||||
send(pTask->clientSocket, responseStr.c_str(), (int)responseStr.length(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,6 +171,63 @@ namespace
|
||||
return DefWindowProc(hWnd, message, wParam, lParam);
|
||||
}
|
||||
|
||||
// 从 socket 读取一整帧 JSON-RPC 报文。
|
||||
// 消息边界规则:优先以 '\n' 作为帧结束标记(新客户端);
|
||||
// 若客户端不发 '\n'(旧脚本),则在收到数据后用 select 短暂探测是否还有后续数据,
|
||||
// 无后续数据即视为整帧,从而保持向后兼容。
|
||||
// 返回 true 表示成功取到一帧(内容写入 out);false 表示无数据或出错。
|
||||
// 注意:本函数运行在后台 TCP 线程,只做字节累积,不解析 JSON、不触碰任何 ARX API。
|
||||
bool RecvOneFrame(SOCKET sock, std::string &out)
|
||||
{
|
||||
const size_t kMaxFrameBytes = 8 * 1024 * 1024; // 8MB 上限,防御异常/恶意大报文
|
||||
char buf[4096];
|
||||
out.clear();
|
||||
|
||||
for (;;)
|
||||
{
|
||||
int n = recv(sock, buf, sizeof(buf), 0);
|
||||
if (n > 0)
|
||||
{
|
||||
out.append(buf, n);
|
||||
|
||||
// 命中换行符即成帧(去掉 '\n' 及之后内容,并兼容 CRLF)
|
||||
size_t pos = out.find('\n');
|
||||
if (pos != std::string::npos)
|
||||
{
|
||||
out.resize(pos);
|
||||
if (!out.empty() && out.back() == '\r')
|
||||
out.pop_back();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (out.size() > kMaxFrameBytes)
|
||||
return false; // 超过上限,判为非法报文
|
||||
|
||||
// 尚未见到 '\n':短暂探测是否还有后续分片。
|
||||
// 无后续数据 => 判定为"整帧已到"(兼容不发 '\n' 的旧客户端)。
|
||||
fd_set probe;
|
||||
FD_ZERO(&probe);
|
||||
FD_SET(sock, &probe);
|
||||
timeval probeTimeout = { 0, 150000 }; // 150ms
|
||||
int pr = select(0, &probe, nullptr, nullptr, &probeTimeout);
|
||||
if (pr <= 0)
|
||||
return true;
|
||||
}
|
||||
else if (n == 0)
|
||||
{
|
||||
// 对端正常关闭:若已收到数据则视为整帧
|
||||
return !out.empty();
|
||||
}
|
||||
else
|
||||
{
|
||||
int err = WSAGetLastError();
|
||||
if ((err == WSAETIMEDOUT || err == WSAEWOULDBLOCK) && !out.empty())
|
||||
return true; // 超时但已有数据:视为整帧
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TCP 监听后台线程
|
||||
void TcpServerLoop()
|
||||
{
|
||||
@@ -166,14 +258,19 @@ namespace
|
||||
SOCKET clientSocket = accept(g_listenSocket, nullptr, nullptr);
|
||||
if (clientSocket != INVALID_SOCKET)
|
||||
{
|
||||
char recvBuf[2048] = { 0 };
|
||||
int bytesReceived = recv(clientSocket, recvBuf, sizeof(recvBuf) - 1, 0);
|
||||
if (bytesReceived > 0)
|
||||
// 设置接收超时:保证插件卸载时后台线程能及时退出,不会永久阻塞在 recv 上
|
||||
DWORD recvTimeoutMs = 5000;
|
||||
setsockopt(clientSocket, SOL_SOCKET, SO_RCVTIMEO,
|
||||
(const char *)&recvTimeoutMs, sizeof(recvTimeoutMs));
|
||||
|
||||
// 按消息边界读取一整帧(不再受 2048 字节单次 recv 限制)
|
||||
std::string frame;
|
||||
if (RecvOneFrame(clientSocket, frame) && !frame.empty())
|
||||
{
|
||||
// 将数据拷贝到堆内存,准备派发给主线程
|
||||
TaskData *pTask = new TaskData;
|
||||
pTask->clientSocket = clientSocket; // 记录 Socket
|
||||
pTask->rawJson = recvBuf;
|
||||
pTask->rawJson = frame;
|
||||
|
||||
// 关键转变:通过 PostMessage 发送给主线程拥有的隐式窗口
|
||||
if (g_hMsgWnd != NULL)
|
||||
@@ -185,6 +282,11 @@ namespace
|
||||
delete pTask;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 空帧或读取失败:直接关闭连接
|
||||
closesocket(clientSocket);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user