落地空运单聊询价字段与线路选择。
必填核对后再建单;多条线路走 H5 点选;费用只展示该线路 TMS 回包,不套用整单空运费。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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 };
|
||||
@@ -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
@@ -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: "" }), "");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user