feat: add WeChat QR code login and AGP WebSocket channel plugin

- Auth module: WeChat OAuth2 scan-to-login flow with terminal QR code
- Token persistence to ~/.openclaw/wechat-access-auth.json (chmod 600)
- Token resolution: config > saved state > interactive login
- Invite code verification (configurable bypass)
- Production/test environment support
- AGP WebSocket client with heartbeat, reconnect, wake detection
- Message handler: Agent dispatch with streaming text and tool calls
- Random device GUID generation (persisted, no real machine ID)
This commit is contained in:
HenryXiaoYang
2026-03-10 02:29:06 +08:00
commit ba754ccc31
33 changed files with 14992 additions and 0 deletions

278
http/webhook.ts Normal file
View File

@@ -0,0 +1,278 @@
import type { IncomingMessage, ServerResponse } from "node:http";
import type { FuwuhaoMessage, SimpleAccount } from "./types.js";
import { verifySignature, decryptMessage } from "./crypto-utils.js";
import { parseQuery, readBody, isFuwuhaoWebhookPath } from "./http-utils.js";
import { handleMessage, handleMessageStream } from "./message-handler.js";
// ============================================
// 账号配置
// ============================================
// 微信服务号的账号配置信息
// 生产环境应从环境变量或配置文件中读取
/**
* 模拟账号存储
* @description
* 生产环境建议:
* 1. 从环境变量读取process.env.FUWUHAO_TOKEN 等
* 2. 从配置文件读取config.json
* 3. 从数据库读取:支持多账号场景
* 4. 使用密钥管理服务:如 AWS Secrets Manager
*/
const mockAccount: SimpleAccount = {
token: "your_token_here", // 微信服务号配置的 Token
encodingAESKey: "your_encoding_aes_key_here", // 消息加密密钥43位字符
receiveId: "your_receive_id_here" // 服务号的原始 ID
};
// ============================================
// Webhook 处理器(主入口)
// ============================================
/**
* 处理微信服务号的 Webhook 请求
* @param req - Node.js HTTP 请求对象
* @param res - Node.js HTTP 响应对象
* @returns Promise<boolean> 是否处理了此请求true=已处理false=交给其他处理器)
* @description
* 此函数是微信服务号集成的主入口,负责:
* 1. 路径匹配:检查是否是服务号 webhook 路径
* 2. GET 请求:处理 URL 验证(微信服务器验证)
* 3. POST 请求:处理用户消息
* - 支持加密消息(验证签名 + 解密)
* - 支持明文消息(测试用)
* - 支持同步返回和流式返回SSE
*
* 请求流程:
* - GET /wechat-access?signature=xxx&timestamp=xxx&nonce=xxx&echostr=xxx
* → 验证签名 → 解密 echostr → 返回明文
* - POST /wechat-access (同步)
* → 验证签名 → 解密消息 → 调用 Agent → 返回 JSON
* - POST /wechat-access?stream=true (流式)
* → 验证签名 → 解密消息 → 调用 Agent → 返回 SSE 流
*/
export const handleSimpleWecomWebhook = async (
req: IncomingMessage,
res: ServerResponse
): Promise<boolean> => {
// ============================================
// 1. 路径匹配检查
// ============================================
// 检查请求路径是否匹配服务号 webhook 路径
// 支持:/wechat-access、/wechat-access/webhook、/wechat-access/*
if (!isFuwuhaoWebhookPath(req.url || "")) {
return false; // 不是我们的路径,交给其他处理器
}
console.log(`[wechat-access] 收到请求: ${req.method} ${req.url}`);
try {
// ============================================
// 2. 解析查询参数
// ============================================
// 微信服务器会在 URL 中附加验证参数
const query = parseQuery(req);
const timestamp = query.get("timestamp") || ""; // 时间戳
const nonce = query.get("nonce") || ""; // 随机数
const signature = query.get("msg_signature") || query.get("signature") || ""; // 签名
// ============================================
// 3. 处理 GET 请求 - URL 验证
// ============================================
// 微信服务器在配置 webhook 时会发送 GET 请求验证 URL
// 请求格式GET /wechat-access?signature=xxx&timestamp=xxx&nonce=xxx&echostr=xxx
if (req.method === "GET") {
const echostr = query.get("echostr") || "";
// 验证签名(确保请求来自微信服务器)
const isValid = verifySignature({
token: mockAccount.token,
timestamp,
nonce,
encrypt: echostr,
signature
});
if (!isValid) {
res.statusCode = 401;
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.end("签名验证失败");
return true;
}
// 解密 echostr 并返回(微信服务器会验证返回值)
try {
const decrypted = decryptMessage({
encodingAESKey: mockAccount.encodingAESKey,
receiveId: mockAccount.receiveId,
encrypt: echostr
});
res.statusCode = 200;
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.end(decrypted);
return true;
} catch {
res.statusCode = 400;
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.end("解密失败");
return true;
}
}
// ============================================
// 4. 处理 POST 请求 - 用户消息
// ============================================
// 微信服务器会将用户发送的消息通过 POST 请求转发过来
// 请求格式POST /wechat-access?signature=xxx&timestamp=xxx&nonce=xxx
// 请求体:加密的 JSON 或 XML 格式消息
if (req.method === "POST") {
// 读取请求体
const body = await readBody(req);
let message: FuwuhaoMessage;
// ============================================
// 4.1 解析和解密消息
// ============================================
// 尝试解析 JSON 格式
try {
const data = JSON.parse(body);
const encrypt = data.encrypt || data.Encrypt || "";
if (encrypt) {
// ============================================
// 加密消息处理流程
// ============================================
// 1. 验证签名(确保消息来自微信服务器)
const isValid = verifySignature({
token: mockAccount.token,
timestamp,
nonce,
encrypt,
signature
});
if (!isValid) {
res.statusCode = 401;
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.end("签名验证失败");
return true;
}
// 2. 解密消息
const decrypted = decryptMessage({
encodingAESKey: mockAccount.encodingAESKey,
receiveId: mockAccount.receiveId,
encrypt
});
message = JSON.parse(decrypted);
} else {
// ============================================
// 明文消息(用于测试)
// ============================================
// 直接使用 JSON 数据,无需解密
message = data;
}
} catch {
// ============================================
// XML 格式处理(简化版)
// ============================================
// 可能是 XML 格式,简单处理
console.log("[wechat-access] 收到非JSON格式数据尝试简单解析");
message = {
msgtype: "text",
Content: body,
FromUserName: "unknown",
MsgId: `${Date.now()}`
};
}
// ============================================
// 4.2 检查是否请求流式返回SSE
// ============================================
// 客户端可以通过以下方式请求流式返回:
// 1. Accept: text/event-stream header
// 2. ?stream=true 查询参数
// 3. ?stream=1 查询参数
const acceptHeader = req.headers.accept || "";
const wantsStream = acceptHeader.includes("text/event-stream") ||
query.get("stream") === "true" ||
query.get("stream") === "1";
console.log('adam-sssss-markoint===wantsStreamwantsStreamwantsStream', wantsStream)
if (wantsStream) {
// ============================================
// 流式返回模式Server-Sent Events
// ============================================
// SSE 是一种服务器向客户端推送实时数据的技术
// 适用于:实时显示 AI 生成过程、工具调用状态等
console.log("[wechat-access] 使用流式返回模式 (SSE)");
// 设置 SSE 响应头
res.statusCode = 200;
res.setHeader("Content-Type", "text/event-stream; charset=utf-8"); // SSE 标准格式
res.setHeader("Cache-Control", "no-cache, no-transform"); // 禁用缓存
res.setHeader("Connection", "keep-alive"); // 保持连接
res.setHeader("X-Accel-Buffering", "no"); // 禁用 nginx 缓冲
res.setHeader("Access-Control-Allow-Origin", "*"); // 允许跨域
res.flushHeaders(); // 立即发送 headers建立 SSE 连接
// 发送初始连接确认事件
const connectedEvent = `data: ${JSON.stringify({ type: "connected", timestamp: Date.now() })}\n\n`;
console.log("[wechat-access] SSE 发送连接确认:", connectedEvent.trim());
res.write(connectedEvent);
try {
// 调用流式消息处理器
// handleMessageStream 会通过回调函数实时推送数据
await handleMessageStream(message, (chunk) => {
// SSE 数据格式data: {JSON}\n\n
const sseData = `data: ${JSON.stringify(chunk)}\n\n`;
console.log("[wechat-access] SSE 发送数据:", chunk.type, chunk.text?.slice(0, 50));
res.write(sseData);
// 如果是完成或错误,关闭连接
if (chunk.type === "done" || chunk.type === "error") {
console.log("[wechat-access] SSE 连接关闭:", chunk.type);
res.end();
}
});
} catch (streamErr) {
// 流式处理异常,发送错误事件
console.error("[wechat-access] SSE 流式处理异常:", streamErr);
const errorData = `data: ${JSON.stringify({ type: "error", text: String(streamErr), timestamp: Date.now() })}\n\n`;
res.write(errorData);
res.end();
}
return true;
}
// ============================================
// 4.3 普通同步返回模式
// ============================================
// 等待 Agent 处理完成后一次性返回结果
// 适用于:简单问答、不需要实时反馈的场景
const reply = await handleMessage(message);
res.statusCode = 200;
res.setHeader("Content-Type", "application/json; charset=utf-8");
res.end(JSON.stringify({
success: true,
reply: reply || "消息已接收,正在处理中..."
}));
return true;
}
return false;
} catch (error) {
// ============================================
// 5. 异常处理
// ============================================
// 捕获所有未处理的异常,返回 500 错误
console.error("[wechat-access] Webhook 处理异常:", error);
res.statusCode = 500;
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.end("服务器内部错误");
return true;
}
};