落地并存测服全端:智能体 Graph/路由、后台切 8180、主账与部署脚本齐备,便于线上重发与验收。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
linmianxun
2026-09-13 21:15:58 +08:00
co-authored by Cursor
parent 0eb1adecc8
commit 8103b3b651
143 changed files with 8968 additions and 277 deletions
+10 -1
View File
@@ -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 只替换传输段。
+67
View File
@@ -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
View File
@@ -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,
});
});