落地并存测服全端:智能体 Graph/路由、后台切 8180、主账与部署脚本齐备,便于线上重发与验收。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+10
-1
@@ -7,4 +7,13 @@ node server.cjs
|
||||
curl http://127.0.0.1:8913/health
|
||||
```
|
||||
|
||||
并存期请保持 `WECOM_AIBOT_ENABLED=false`,不要与现网 8813 同时连同一 Bot。
|
||||
转发联调(智能体 HTTP 需已起):
|
||||
|
||||
```text
|
||||
curl -X POST http://127.0.0.1:8913/forward -H "Content-Type: application/json" -d "{\"sender_id\":\"userid\",\"message_id\":\"m1\",\"content\":\"hi\",\"chat_type\":\"group\"}"
|
||||
```
|
||||
|
||||
环境变量:`WECOM_AIBOT_INGRESS_URL`(默认 `http://127.0.0.1:8910/internal/aibot/inbound`)、`WECOM_AIBOT_BRIDGE_TOKEN`。
|
||||
|
||||
并存期请保持 `WECOM_AIBOT_ENABLED=false`,不要与现网 8813 同时连同一 Bot。
|
||||
`long_connection.cjs`:启用时只跑帧循环/心跳壳(默认不连 Bot SDK);正式 SDK 只替换传输段。
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* AIBOT 长连接传输壳(无业务)。
|
||||
*
|
||||
* 本文件职责:
|
||||
* - 当 WECOM_AIBOT_ENABLED=true 时启动「帧循环」占位(默认只心跳日志,不连真 Bot SDK)
|
||||
* - 收到已规范化的入站帧时转发到同进程 forwardToAgent(由 server.cjs 注入)
|
||||
*
|
||||
* 禁止:写工单/六态/Graph;并存期勿与现网 8813 双连同一 Bot。
|
||||
* 正式 SDK 接入:替换 startLongConnectionLoop 内的 connect 段,保持只做传输。
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* @param {object} opts
|
||||
* @param {boolean} opts.enabled
|
||||
* @param {(payloadText: string) => Promise<object>} opts.forwardToAgent
|
||||
* @param {() => boolean} [opts.shouldStop]
|
||||
*/
|
||||
function startLongConnectionLoop(opts) {
|
||||
const enabled = !!opts.enabled;
|
||||
const forwardToAgent = opts.forwardToAgent;
|
||||
const shouldStop = opts.shouldStop || (() => false);
|
||||
|
||||
if (!enabled) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log("[aibot-long-connection] 未启用(WECOM_AIBOT_ENABLED=false),跳过帧循环壳");
|
||||
return { stop: () => {} };
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
"[aibot-long-connection] 帧循环壳已启动:当前不连企微 Bot SDK,仅心跳;" +
|
||||
"请确认现网桥已停,避免双连"
|
||||
);
|
||||
|
||||
let timer = null;
|
||||
const tick = () => {
|
||||
if (shouldStop()) {
|
||||
return;
|
||||
}
|
||||
// 心跳:证明传输壳在跑;无帧可转发
|
||||
// eslint-disable-next-line no-console
|
||||
console.log("[aibot-long-connection] heartbeat shell (no SDK frame)");
|
||||
};
|
||||
timer = setInterval(tick, 60000);
|
||||
tick();
|
||||
|
||||
return {
|
||||
stop: () => {
|
||||
if (timer) {
|
||||
clearInterval(timer);
|
||||
timer = null;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 测试注入:模拟 SDK 收到一帧已解析 JSON,只做转发。
|
||||
* @param {object|string} frame
|
||||
*/
|
||||
injectFrameForTest: async (frame) => {
|
||||
const text = typeof frame === "string" ? frame : JSON.stringify(frame);
|
||||
return forwardToAgent(text);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { startLongConnectionLoop };
|
||||
+117
-16
@@ -1,46 +1,142 @@
|
||||
/**
|
||||
* AIBOT 长连接桥(骨架)
|
||||
* AIBOT 长连接桥(骨架 + HTTP 转发)
|
||||
*
|
||||
* 本文件职责:仅做本机回环 HTTP 健康检查与转发壳;不拥有业务状态。
|
||||
* 端口:环境变量 AIBOT_BRIDGE_PORT(测试默认 8913),绑定 AIBOT_BRIDGE_BIND(默认 127.0.0.1)。
|
||||
* 禁止:并存期连接现网 Bot 长连接(现网 8813 已占用);禁止在本进程写工单/六态/Graph。
|
||||
* 启动:在 aibot-bridge 目录执行 node server.cjs
|
||||
* 本文件职责:
|
||||
* - /health 存活
|
||||
* - POST /forward 把已解析消息转到智能体 WECOM_AIBOT_INGRESS_URL(默认 8910/internal/aibot/inbound)
|
||||
* - 默认不连企微 Bot 长连接(并存期禁止双连);WECOM_AIBOT_ENABLED=true 时仅打警告
|
||||
*
|
||||
* 禁止:在本进程写工单/六态/Graph。
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const http = require("http");
|
||||
const { URL } = require("url");
|
||||
const { startLongConnectionLoop } = require("./long_connection.cjs");
|
||||
|
||||
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";
|
||||
const ingressUrl =
|
||||
process.env.WECOM_AIBOT_INGRESS_URL ||
|
||||
"http://127.0.0.1:8910/internal/aibot/inbound";
|
||||
const bridgeToken = process.env.WECOM_AIBOT_BRIDGE_TOKEN || "";
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
// 只处理短请求;真实 AIBOT 帧转发后续迭代。请求线程不做业务。
|
||||
if (req.url === "/health" || req.url === "/health/") {
|
||||
const body = JSON.stringify({
|
||||
function readBody(req) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunks = [];
|
||||
req.on("data", (c) => chunks.push(c));
|
||||
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
||||
req.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
function sendJson(res, code, obj) {
|
||||
const body = JSON.stringify(obj);
|
||||
res.writeHead(code, { "Content-Type": "application/json; charset=utf-8" });
|
||||
res.end(body);
|
||||
}
|
||||
|
||||
function forwardToAgent(payloadText) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let target;
|
||||
try {
|
||||
target = new URL(ingressUrl);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
return;
|
||||
}
|
||||
const data = Buffer.from(payloadText, "utf8");
|
||||
const req = http.request(
|
||||
{
|
||||
protocol: target.protocol,
|
||||
hostname: target.hostname,
|
||||
port: target.port || 80,
|
||||
path: target.pathname + (target.search || ""),
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
"Content-Length": data.length,
|
||||
"X-Aibot-Bridge-Token": bridgeToken,
|
||||
},
|
||||
timeout: 5000,
|
||||
},
|
||||
(res) => {
|
||||
const chunks = [];
|
||||
res.on("data", (c) => chunks.push(c));
|
||||
res.on("end", () => {
|
||||
resolve({
|
||||
status: res.statusCode,
|
||||
body: Buffer.concat(chunks).toString("utf8"),
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
req.on("error", reject);
|
||||
req.on("timeout", () => {
|
||||
req.destroy();
|
||||
reject(new Error("ingress_timeout"));
|
||||
});
|
||||
req.write(data);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
const server = http.createServer(async (req, res) => {
|
||||
const url = (req.url || "").split("?")[0];
|
||||
|
||||
if (req.method === "GET" && (url === "/health" || url === "/health/")) {
|
||||
sendJson(res, 200, {
|
||||
status: "UP",
|
||||
service: "aibot-bridge",
|
||||
bind,
|
||||
port,
|
||||
wecomAibotEnabled: enabled,
|
||||
ingressUrl,
|
||||
longConnectionShell: true,
|
||||
note: enabled
|
||||
? "enabled flag on — 仍须确认未与现网双连"
|
||||
: "scaffold: Bot 长连接默认关闭(并存期禁止再连)",
|
||||
? "enabled:长连接帧循环壳已起(默认无 Bot SDK);勿与现网双连"
|
||||
: "Bot 长连接默认关闭;可用 POST /forward 测转发",
|
||||
});
|
||||
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
|
||||
res.end(body);
|
||||
return;
|
||||
}
|
||||
|
||||
res.writeHead(404, { "Content-Type": "application/json; charset=utf-8" });
|
||||
res.end(JSON.stringify({ error: "not_found", path: req.url }));
|
||||
// 测试/后续 Bot 回调:把 JSON 原样转到智能体(不做业务)
|
||||
if (req.method === "POST" && (url === "/forward" || url === "/forward/")) {
|
||||
try {
|
||||
const raw = await readBody(req);
|
||||
if (!raw) {
|
||||
sendJson(res, 400, { error: "empty_body" });
|
||||
return;
|
||||
}
|
||||
// 轻量校验 JSON
|
||||
JSON.parse(raw);
|
||||
const out = await forwardToAgent(raw);
|
||||
sendJson(res, 200, {
|
||||
status: "forwarded",
|
||||
ingressStatus: out.status,
|
||||
ingressBody: (() => {
|
||||
try {
|
||||
return JSON.parse(out.body);
|
||||
} catch (_) {
|
||||
return out.body;
|
||||
}
|
||||
})(),
|
||||
});
|
||||
} catch (e) {
|
||||
sendJson(res, 502, { error: "forward_failed", message: String(e && e.message ? e.message : e) });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
sendJson(res, 404, { error: "not_found", path: req.url });
|
||||
});
|
||||
|
||||
server.listen(port, bind, () => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
`[aibot-bridge] listening http://${bind}:${port}/health wecomAibotEnabled=${enabled}`
|
||||
`[aibot-bridge] http://${bind}:${port}/health forward→${ingressUrl} enabled=${enabled}`
|
||||
);
|
||||
if (enabled) {
|
||||
// eslint-disable-next-line no-console
|
||||
@@ -48,4 +144,9 @@ server.listen(port, bind, () => {
|
||||
"[aibot-bridge] WECOM_AIBOT_ENABLED=true:请确认已停现网桥且不会双连同一 Bot"
|
||||
);
|
||||
}
|
||||
// 传输壳:enabled 时跑心跳循环;未接 SDK 前不会产生入站帧
|
||||
startLongConnectionLoop({
|
||||
enabled,
|
||||
forwardToAgent,
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user