Files
wechat-access-unqclawed/http/webhook.ts
HenryXiaoYang ba754ccc31 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)
2026-03-10 02:29:06 +08:00

278 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
}
};