落地空运群内协同与锁舱出站。

群里按 BOT 通道激活当前工单、航线报价和成交跟进;没有 TMS 报价编号时用工单字段补齐仍调锁舱,避免被本地规则拦住。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-16 17:25:42 +08:00
co-authored by Cursor
parent 12219cacb1
commit d9029dec89
62 changed files with 6772 additions and 159 deletions
+130 -6
View File
@@ -4,7 +4,8 @@
* 本文件职责:
* - /health 存活
* - POST /forward 把已解析消息转到智能体 WECOM_AIBOT_INGRESS_URL(默认 8910/internal/aibot/inbound)
* - 默认不连企微 Bot 长连接(并存期禁止双连);WECOM_AIBOT_ENABLED=true 时仅打警告
* - POST /send-group 走长连接 aibot_send_msg(发文件后马上在群里说话)
* - 默认不连企微(并存期禁止双连);WECOM_AIBOT_ENABLED=true 时订阅长连接
*
* 禁止:在本进程写工单/六态/Graph。
*/
@@ -15,6 +16,20 @@ const http = require("http");
const { URL } = require("url");
const { startLongConnectionLoop } = require("./long_connection.cjs");
let longConn = {
sendGroupMessage: async () => {
const err = new Error("ws_offline");
err.code = "ws_offline";
throw err;
},
sendGroupFile: async () => {
const err = new Error("ws_offline");
err.code = "ws_offline";
throw err;
},
status: () => ({ enabled, connected: false, subscribed: false }),
};
const port = Number(process.env.AIBOT_BRIDGE_PORT || 8913);
const bind = process.env.AIBOT_BRIDGE_BIND || "127.0.0.1";
const enabled = String(process.env.WECOM_AIBOT_ENABLED || "false").toLowerCase() === "true";
@@ -94,10 +109,10 @@ const server = http.createServer(async (req, res) => {
port,
wecomAibotEnabled: enabled,
ingressUrl,
longConnectionShell: true,
longConnection: longConn.status(),
note: enabled
? "enabled:长连接帧循环壳已起(默认无 Bot SDK);勿与现网双连"
: "Bot 长连接默认关闭;可用 POST /forward 测转发",
? "enabled:长连接已尝试订阅;POST /send-group 可主动往群里说话;勿与现网双连"
: "Bot 长连接默认关闭;可用 POST /forward 测转发。主动发言需 WECOM_AIBOT_ENABLED=true",
});
return;
}
@@ -130,9 +145,118 @@ const server = http.createServer(async (req, res) => {
return;
}
// 官方桥口径:POST /send {chatid, operation:"text", content}
if (req.method === "POST" && (url === "/send" || url === "/send/")) {
if (bridgeToken) {
const got = String(
req.headers["x-aibot-bridge-token"] || req.headers["x-ytd-aibot-bridge-token"] || ""
);
if (got !== bridgeToken) {
sendJson(res, 401, { error: "unauthorized" });
return;
}
}
try {
const raw = await readBody(req);
const body = raw ? JSON.parse(raw) : {};
const chatId = String(body.chatid || body.chat_id || "").trim();
const content = String(body.content || "").trim();
if (!chatId || !content) {
sendJson(res, 400, { error: "missing_chat_or_content" });
return;
}
await longConn.sendGroupMessage({ chatId, content });
sendJson(res, 200, { status: "sent", chatId });
} catch (e) {
const code = e && e.code ? e.code : "send_failed";
const httpCode = code === "ws_offline" ? 503 : 502;
sendJson(res, httpCode, {
error: code,
message: String(e && e.message ? e.message : e),
});
}
return;
}
// 空运群发填好的报价模板:上传素材后 aibot_send_msg file
if (req.method === "POST" && (url === "/send-file" || url === "/send-file/")) {
if (bridgeToken) {
const got = String(
req.headers["x-aibot-bridge-token"] || req.headers["x-ytd-aibot-bridge-token"] || ""
);
if (got !== bridgeToken) {
sendJson(res, 401, { error: "unauthorized" });
return;
}
}
try {
const raw = await readBody(req);
const body = raw ? JSON.parse(raw) : {};
const chatId = String(body.chat_id || body.chatid || "").trim();
const filename = String(body.filename || "quote.xlsx").trim() || "quote.xlsx";
const fileB64 = String(body.file_b64 || body.fileBase64 || "").trim();
if (!chatId || !fileB64) {
sendJson(res, 400, { error: "missing_chat_or_file" });
return;
}
await longConn.sendGroupFile({ chatId, filename, fileB64 });
sendJson(res, 200, { status: "sent", chatId, filename });
} catch (e) {
const code = e && e.code ? e.code : "send_failed";
const httpCode = code === "ws_offline" ? 503 : 502;
sendJson(res, httpCode, {
error: code,
message: String(e && e.message ? e.message : e),
});
}
return;
}
// 智能体 Worker:存档听到附件后马上在群里说话(aibot_send_msg)
if (req.method === "POST" && (url === "/send-group" || url === "/send-group/")) {
if (bridgeToken) {
const got = String(
req.headers["x-aibot-bridge-token"] || req.headers["x-ytd-aibot-bridge-token"] || ""
);
if (got !== bridgeToken) {
sendJson(res, 401, { error: "unauthorized" });
return;
}
}
try {
const raw = await readBody(req);
const body = raw ? JSON.parse(raw) : {};
const chatId = String(body.chat_id || body.chatid || "").trim();
const content = String(body.content || "").trim();
if (!chatId || !content) {
sendJson(res, 400, { error: "missing_chat_or_content" });
return;
}
await longConn.sendGroupMessage({ chatId, content });
sendJson(res, 200, { status: "sent", chatId });
} catch (e) {
const code = e && e.code ? e.code : "send_failed";
const httpCode = code === "ws_offline" ? 503 : 502;
sendJson(res, httpCode, {
error: code,
message: String(e && e.message ? e.message : e),
});
}
return;
}
sendJson(res, 404, { error: "not_found", path: req.url });
});
process.on("uncaughtException", (e) => {
// eslint-disable-next-line no-console
console.warn("[aibot-bridge] uncaught", String(e && e.stack ? e.stack : e).slice(0, 400));
});
process.on("unhandledRejection", (e) => {
// eslint-disable-next-line no-console
console.warn("[aibot-bridge] rejection", String(e && e.stack ? e.stack : e).slice(0, 400));
});
server.listen(port, bind, () => {
// eslint-disable-next-line no-console
console.log(
@@ -144,8 +268,8 @@ server.listen(port, bind, () => {
"[aibot-bridge] WECOM_AIBOT_ENABLED=true:请确认已停现网桥且不会双连同一 Bot"
);
}
// 传输壳:enabled 时跑心跳循环;未接 SDK 前不会产生入站帧
startLongConnectionLoop({
// 启用时订阅企微长连接;关闭时 /send-group 会回 ws_offline
longConn = startLongConnectionLoop({
enabled,
forwardToAgent,
});