落地空运单聊询价字段与线路选择。

必填核对后再建单;多条线路走 H5 点选;费用只展示该线路 TMS 回包,不套用整单空运费。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-22 15:02:32 +08:00
co-authored by Cursor
parent 8afe909230
commit cfbce2fd69
53 changed files with 3470 additions and 372 deletions
+45
View File
@@ -0,0 +1,45 @@
/**
* 空运群出站报文:禁止 stream(企微会加「内容由AI生成」和赞踩)。
* 只发 markdown,且不带 feedback。
* 有这次 @ 的 req_id 才走 aibot_respond_msg,群里才会引用原消息。
*/
function pickReplyReqId({ fromCaller, fromChat, lastAny }) {
const caller = String(fromCaller || "").trim();
if (caller) {
return caller;
}
const chat = String(fromChat || "").trim();
if (chat) {
return chat;
}
return String(lastAny || "").trim();
}
function buildGroupMessageFrame({ chatId, content, callbackReqId, reqId }) {
const text = String(content || "");
const markdown = { content: text };
if (callbackReqId) {
return {
cmd: "aibot_respond_msg",
headers: { req_id: callbackReqId },
body: {
chatid: chatId,
chat_type: 2,
msgtype: "markdown",
markdown,
},
};
}
return {
cmd: "aibot_send_msg",
headers: { req_id: reqId || "aibot_send_msg" },
body: {
chatid: chatId,
chat_type: 2,
msgtype: "markdown",
markdown,
},
};
}
module.exports = { buildGroupMessageFrame, pickReplyReqId };
+37 -35
View File
@@ -12,6 +12,7 @@
"use strict";
const crypto = require("crypto");
const { buildGroupMessageFrame, pickReplyReqId } = require("./group_message_frame.cjs");
function generateReqId(prefix) {
const timestamp = Date.now();
@@ -49,6 +50,7 @@ function startLongConnectionLoop(opts) {
lastError: "",
pending: new Map(),
lastCallback: new Map(),
lastAnyReq: "",
heartbeatTimer: null,
reconnectTimer: null,
stop: () => {},
@@ -83,38 +85,34 @@ function startLongConnectionLoop(opts) {
});
}
function sendGroupMessage({ chatId, content }) {
function sendGroupMessage({ chatId, content, callbackReqId }) {
const text = String(content || "");
const callbackReqId = state.lastCallback.get(String(chatId || "").trim()) || "";
if (callbackReqId) {
return sendWs(
{
cmd: "aibot_respond_msg",
headers: { req_id: callbackReqId },
body: {
msgtype: "stream",
stream: { id: crypto.randomUUID(), finish: true, content: text },
},
},
callbackReqId
).finally(() => {
state.lastCallback.delete(String(chatId || "").trim());
});
}
const reqId = generateReqId("aibot_send_msg");
return sendWs(
{
cmd: "aibot_send_msg",
headers: { req_id: reqId },
body: {
chatid: chatId,
chat_type: 2,
msgtype: "markdown",
markdown: { content: text },
},
},
reqId
const fromCaller = String(callbackReqId || "").trim();
const chatKey = String(chatId || "").trim();
const fromChat = state.lastCallback.get(chatKey) || "";
const replyReqId = pickReplyReqId({
fromCaller,
fromChat,
lastAny: state.lastAnyReq || "",
});
// 用这次 @ 的 req_id 回 aibot_respond_msg,群里才会引用原消息。
const reqId = replyReqId || generateReqId("aibot_send_msg");
const frame = buildGroupMessageFrame({
chatId,
content: text,
callbackReqId: replyReqId,
reqId,
});
// eslint-disable-next-line no-console
console.log(
"[aibot-long-connection] 出站 cmd=",
frame.cmd,
"chat=",
chatKey || "-",
"hasReq=",
Boolean(replyReqId)
);
return sendWs(frame, reqId);
}
/**
@@ -303,8 +301,10 @@ function startLongConnectionLoop(opts) {
return;
}
const reqId = frame.headers && frame.headers.req_id;
// 出站 ACK 带 chatid,不能当成入站,否则会盖掉这次 @ 的 req_id,群里就没有引用。
if (reqId && state.pending.has(reqId)) {
settlePending(reqId, frame.errcode, frame.errmsg, frame);
return;
}
if (frame.cmd === "aibot_event_callback") {
const ev = (frame.body && frame.body.event && frame.body.event.eventtype) || "";
@@ -318,9 +318,7 @@ function startLongConnectionLoop(opts) {
const bodyHint = frame.body || {};
const looksLikeInbound = Boolean(
frame.cmd === "aibot_msg_callback" ||
bodyHint.msgid ||
bodyHint.chatid ||
(bodyHint.text && bodyHint.text.content)
(bodyHint.msgid && (bodyHint.from || (bodyHint.text && bodyHint.text.content)))
);
if (looksLikeInbound) {
// eslint-disable-next-line no-console
@@ -333,8 +331,12 @@ function startLongConnectionLoop(opts) {
bodyHint.chatid || "-"
);
const inbound = normalizeInbound(frame);
if (reqId && inbound.chat_id) {
state.lastCallback.set(inbound.chat_id, reqId);
const chatKey = inbound.chat_id || String(bodyHint.chatid || "").trim();
if (reqId && chatKey) {
state.lastCallback.set(chatKey, reqId);
}
if (reqId) {
state.lastAnyReq = reqId;
}
const officialBody = Object.assign({}, frame.body || {}, {
aibot_direct: true,
+11 -3
View File
@@ -4,7 +4,7 @@
* 本文件职责:
* - /health 存活
* - POST /forward 把已解析消息转到智能体 WECOM_AIBOT_INGRESS_URL(默认 8910/internal/aibot/inbound)
* - POST /send-group 走长连接 aibot_send_msg(发文件后马上在群里说话)
* - POST /send、/send-group:有 aibot_req_id 走 aibot_respond_msg(引用这次 @),没有则主动发言
* - 默认不连企微(并存期禁止双连);WECOM_AIBOT_ENABLED=true 时订阅长连接
*
* 禁止:在本进程写工单/六态/Graph。
@@ -165,7 +165,11 @@ const server = http.createServer(async (req, res) => {
sendJson(res, 400, { error: "missing_chat_or_content" });
return;
}
await longConn.sendGroupMessage({ chatId, content });
await longConn.sendGroupMessage({
chatId,
content,
callbackReqId: String(body.aibot_req_id || body.callback_req_id || "").trim(),
});
sendJson(res, 200, { status: "sent", chatId });
} catch (e) {
const code = e && e.code ? e.code : "send_failed";
@@ -232,7 +236,11 @@ const server = http.createServer(async (req, res) => {
sendJson(res, 400, { error: "missing_chat_or_content" });
return;
}
await longConn.sendGroupMessage({ chatId, content });
await longConn.sendGroupMessage({
chatId,
content,
callbackReqId: String(body.aibot_req_id || body.callback_req_id || "").trim(),
});
sendJson(res, 200, { status: "sent", chatId });
} catch (e) {
const code = e && e.code ? e.code : "send_failed";
@@ -0,0 +1,33 @@
const test = require("node:test");
const assert = require("node:assert/strict");
const { buildGroupMessageFrame, pickReplyReqId } = require("../group_message_frame.cjs");
test("group reply never uses stream", () => {
const withCb = buildGroupMessageFrame({
chatId: "wr_1",
content: "工单号:WO202609210001",
callbackReqId: "cb_1",
});
assert.notEqual(withCb.body.msgtype, "stream");
assert.equal(withCb.body.msgtype, "markdown");
assert.equal(withCb.cmd, "aibot_respond_msg");
assert.equal(withCb.body.chatid, "wr_1");
assert.equal(withCb.body.markdown.feedback, undefined);
const proactive = buildGroupMessageFrame({
chatId: "wr_1",
content: "附件已保存",
callbackReqId: "",
});
assert.equal(proactive.cmd, "aibot_send_msg");
assert.equal(proactive.body.msgtype, "markdown");
assert.equal(proactive.body.markdown.feedback, undefined);
});
test("reply req id falls back to last inbound when chat map misses", () => {
assert.equal(pickReplyReqId({ fromCaller: "cb_1", fromChat: "old", lastAny: "any" }), "cb_1");
assert.equal(pickReplyReqId({ fromCaller: "", fromChat: "chat_cb", lastAny: "any" }), "chat_cb");
assert.equal(pickReplyReqId({ fromCaller: "", fromChat: "", lastAny: "jeD_msF" }), "jeD_msF");
assert.equal(pickReplyReqId({ fromCaller: "", fromChat: "", lastAny: "" }), "");
});