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

174
common/message-context.ts Normal file
View File

@@ -0,0 +1,174 @@
import type { FuwuhaoMessage } from "../http/types.js";
import { getWecomRuntime } from "./runtime.js";
// ============================================
// 渠道来源标签
// ============================================
// 用于 ChannelSource标识消息来自哪个微信渠道
// UI 侧可通过此字段区分不同来源,做差异化展示或交互限制
export const WECHAT_CHANNEL_LABELS = {
/** 微信服务号 */
serviceAccount: "serviceAccount",
/** 微信小程序 */
miniProgram: "miniProgram",
} as const;
// ============================================
// 消息上下文构建
// ============================================
// 将微信服务号的原始消息转换为 OpenClaw 标准的消息上下文
// 包括路由解析、会话管理、消息格式化等核心功能
/**
* 消息上下文返回类型
* @property ctx - OpenClaw 标准的消息上下文对象,包含所有必要的消息元数据
* @property route - 路由信息,用于确定消息应该发送到哪个 Agent
* @property storePath - 会话存储路径,用于持久化会话数据
*/
export interface MessageContext {
ctx: Record<string, unknown>;
route: {
sessionKey: string; // 会话唯一标识,用于关联同一用户的多轮对话
agentId: string; // Agent ID标识处理此消息的 Agent
accountId: string; // 账号 ID用于多账号场景
};
storePath: string;
}
/**
* 构建消息上下文
* @param message - 微信服务号的原始消息对象
* @returns MessageContext 包含上下文、路由和存储路径的完整消息上下文
* @description
* 此函数是消息处理的核心,负责:
* 1. 提取和标准化消息字段(兼容多种格式)
* 2. 解析路由,确定消息应该发送到哪个 Agent
* 3. 获取会话存储路径,用于持久化对话历史
* 4. 格式化消息为 OpenClaw 标准格式
* 5. 构建完整的消息上下文对象
*
* 内部流程:
* - 从 runtime 获取配置
* - 提取用户 ID、消息 ID、内容等关键信息
* - 调用 routing.resolveAgentRoute 解析路由
* - 调用 session.resolveStorePath 获取存储路径
* - 调用 reply.formatInboundEnvelope 格式化消息
* - 调用 reply.finalizeInboundContext 构建最终上下文
*/
export const buildMessageContext = (message: FuwuhaoMessage): MessageContext => {
// 获取 OpenClaw 运行时实例
const runtime = getWecomRuntime();
// 加载全局配置(包含 Agent 配置、路由规则等)
const cfg = runtime.config.loadConfig();
// ============================================
// 1. 提取和标准化消息字段
// ============================================
// 兼容多种字段命名FromUserName/userid
const userId = message.FromUserName || message.userid || "unknown";
const toUser = message.ToUserName || "unknown";
// 确保消息 ID 唯一(用于去重和追踪)
const messageId = message.MsgId || message.msgid || `${Date.now()}`;
// TODO: 微信的 CreateTime 是秒级时间戳,需要转换为毫秒
// const timestamp = message.CreateTime ? message.CreateTime * 1000 : Date.now();
const timestamp = Date.now();
// 提取消息内容(兼容 Content 和 text.content 两种格式)
const content = message.Content || message.text?.content || "";
// ============================================
// 2. 解析路由 - 确定消息应该发送到哪个 Agent
// ============================================
// runtime.channel.routing.resolveAgentRoute 是 OpenClaw 的核心路由方法
// 根据频道、账号、对话类型等信息,决定使用哪个 Agent 处理消息
const frameworkRoute = runtime.channel.routing.resolveAgentRoute({
cfg, // 全局配置
channel: "wechat-access", // 频道标识
accountId: "default", // 账号 ID支持多账号场景
peer: {
kind: "dm", // 对话类型dm=私聊group=群聊
id: userId, // 对话对象 ID用户 ID
},
});
// 框架返回的 sessionKey 通常是 agent:main:main与 PC 端默认 session 相同。
// 为了让 UI 能区分外部渠道消息,使用独立的 sessionKey 格式:
// agent:{agentId}:wechat-access:direct:{userId}
const channelSessionKey = `agent:${frameworkRoute.agentId}:wechat-access:direct:${userId}`;
const route = {
...frameworkRoute,
sessionKey: channelSessionKey,
};
// ============================================
// 3. 获取消息格式化选项
// ============================================
// runtime.channel.reply.resolveEnvelopeFormatOptions 获取消息格式化配置
// 包括时间格式、前缀、后缀等显示选项
const envelopeOptions = runtime.channel.reply.resolveEnvelopeFormatOptions(cfg);
// ============================================
// 4. 获取会话存储路径
// ============================================
// runtime.channel.session.resolveStorePath 计算会话数据的存储路径
// 用于持久化对话历史、上下文等信息
const storePath = runtime.channel.session.resolveStorePath(cfg.session?.store, {
agentId: route.agentId,
});
// 存储路径通常类似:/data/sessions/{agentId}/{sessionKey}.json
// ============================================
// 5. 读取上次会话时间
// ============================================
// runtime.channel.session.readSessionUpdatedAt 读取上次会话的更新时间
// 用于判断会话是否过期,是否需要重置上下文
const previousTimestamp = runtime.channel.session.readSessionUpdatedAt({
storePath,
sessionKey: route.sessionKey,
});
// 如果距离上次会话时间过长,可能会清空历史上下文
// ============================================
// 6. 格式化入站消息
// ============================================
// runtime.channel.reply.formatInboundEnvelope 将原始消息格式化为标准格式
// 添加时间戳、发送者信息、格式化选项等
const body = runtime.channel.reply.formatInboundEnvelope({
channel: "wechat-access", // 频道标识
from: userId, // 发送者 ID
timestamp, // 消息时间戳
body: content, // 消息内容
chatType: "direct", // 对话类型direct=私聊)
sender: {
id: userId, // 发送者 ID
},
previousTimestamp, // 上次会话时间(用于判断是否需要添加时间分隔符)
envelope: envelopeOptions, // 格式化选项
});
// 返回格式化后的消息体,可能包含时间前缀、发送者名称等
// ============================================
// 7. 构建完整的消息上下文
// ============================================
// runtime.channel.reply.finalizeInboundContext 构建 OpenClaw 标准的消息上下文
// 这是 Agent 处理消息时使用的核心数据结构
const ctx = runtime.channel.reply.finalizeInboundContext({
Body: body, // 格式化后的消息体
RawBody: content, // 原始消息内容
CommandBody: content, // 命令体(用于解析命令)
From: `wechat-access:${userId}`, // 发送者标识(带频道前缀)
To: `wechat-access:${toUser}`, // 接收者标识
SessionKey: route.sessionKey, // 会话键
AccountId: route.accountId, // 账号 ID
ChatType: "direct" as const, // 对话类型
ChannelSource: WECHAT_CHANNEL_LABELS.serviceAccount, // 渠道来源标识(用于 UI 侧区分消息来源)
SenderId: userId, // 发送者 ID
Provider: "wechat-access", // 提供商标识
Surface: "wechat-access", // 界面标识
MessageSid: messageId, // 消息唯一标识
Timestamp: timestamp, // 时间戳
OriginatingChannel: "wechat-access" as const, // 原始频道
OriginatingTo: `wechat-access:${userId}`, // 原始接收者
});
// ctx 包含了 Agent 处理消息所需的所有信息
return { ctx, route, storePath };
};