Files
wechat-access-unqclawed/common/message-context.ts

175 lines
8.2 KiB
TypeScript
Raw Permalink 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 { 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-unqclawed", // 频道标识
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-unqclawed", // 频道标识
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-unqclawed", // 提供商标识
Surface: "wechat-access-unqclawed", // 界面标识
MessageSid: messageId, // 消息唯一标识
Timestamp: timestamp, // 时间戳
OriginatingChannel: "wechat-access-unqclawed" as const, // 原始频道
OriginatingTo: `wechat-access:${userId}`, // 原始接收者
});
// ctx 包含了 Agent 处理消息所需的所有信息
return { ctx, route, storePath };
};