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

290
websocket/types.ts Normal file
View File

@@ -0,0 +1,290 @@
/**
* @file types.ts
* @description AGP (Agent Gateway Protocol) 协议类型定义
*
* AGP 是 OpenClaw 与外部服务(如微信服务号后端)之间的 WebSocket 通信协议。
* 所有消息都使用统一的「信封Envelope」格式通过 method 字段区分消息类型。
*
* 消息方向:
* 下行(服务端 → 客户端session.prompt、session.cancel
* 上行(客户端 → 服务端session.update、session.promptResponse
*
* 基于 websocket.md 协议文档定义
*/
// ============================================
// AGP 消息信封
// ============================================
/**
* AGP 统一消息信封
* 所有 WebSocket 消息(上行和下行)均使用此格式
*/
export interface AGPEnvelope<T = unknown> {
/** 全局唯一消息 IDUUID用于幂等去重 */
msg_id: string;
/** 设备唯一标识(下行消息携带,上行消息需原样回传) */
guid?: string;
/** 用户 ID下行消息携带上行消息需原样回传 */
user_id?: string;
/** 消息类型 */
method: AGPMethod;
/** 消息载荷 */
payload: T;
}
// ============================================
// Method 枚举
// ============================================
/**
* AGP 消息方法枚举
* - session.prompt: 下发用户指令(服务端 → 客户端)
* - session.cancel: 取消 Prompt Turn服务端 → 客户端)
* - session.update: 流式中间更新(客户端 → 服务端)
* - session.promptResponse: 最终结果(客户端 → 服务端)
*/
export type AGPMethod =
| "session.prompt"
| "session.cancel"
| "session.update"
| "session.promptResponse"
| "ping";
// ============================================
// 通用数据结构
// ============================================
/**
* 内容块
* 当前仅支持 text 类型
*/
export interface ContentBlock {
type: "text";
text: string;
}
/**
* 工具调用状态枚举
*/
export type ToolCallStatus = "pending" | "in_progress" | "completed" | "failed";
/**
* 工具调用类型枚举
*/
export type ToolCallKind = "read" | "edit" | "delete" | "execute" | "search" | "fetch" | "think" | "other";
/**
* 工具操作路径
* @description 记录工具调用涉及的文件或目录路径,用于在 UI 中展示操作位置
*/
export interface ToolLocation {
/** 文件或目录的绝对路径 */
path: string;
}
/**
* 工具调用
* @description
* 描述一次工具调用的完整信息,用于在 session.update 消息中实时推送工具执行状态。
* 一次工具调用会产生多个 session.update 消息:
* 1. update_type=tool_call工具开始执行status=in_progress
* 2. update_type=tool_call_update执行中间状态status=in_progress可选
* 3. update_type=tool_call_update执行完成status=completed/failed
*/
export interface ToolCall {
/** 工具调用唯一 ID用于关联同一次工具调用的多个 update 消息 */
tool_call_id: string;
/** 工具调用标题(展示用,通常是工具名称,如 "read_file" */
title?: string;
/** 工具类型,用于 UI 展示不同的图标 */
kind?: ToolCallKind;
/** 工具调用当前状态 */
status: ToolCallStatus;
/** 工具调用结果内容phase=result 时附带,用于展示工具输出) */
content?: ContentBlock[];
/** 工具操作涉及的文件路径(可选,用于 UI 展示操作位置) */
locations?: ToolLocation[];
}
// ============================================
// 下行消息(服务端 → 客户端)
// ============================================
/**
* session.prompt 载荷 — 下发用户指令
* @description
* 服务端收到用户消息后通过此消息将用户指令下发给客户端OpenClaw Agent处理。
* 客户端处理完毕后,需要发送 session.promptResponse 作为响应。
*/
export interface PromptPayload {
/** 所属 Session ID标识一个完整的对话会话 */
session_id: string;
/** 本次 Turn 唯一 ID标识一次「用户提问 + AI 回答」的完整交互) */
prompt_id: string;
/** 目标 AI 应用标识(指定由哪个 Agent 处理此消息) */
agent_app: string;
/** 用户指令内容(结构化内容块数组,目前只支持 text 类型) */
content: ContentBlock[];
}
/**
* session.cancel 载荷 — 取消 Prompt Turn
* @description
* 用户主动取消正在处理的请求时,服务端发送此消息。
* 客户端收到后应停止 Agent 处理,并发送 stop_reason=cancelled 的 promptResponse。
*/
export interface CancelPayload {
/** 所属 Session ID */
session_id: string;
/** 要取消的 Turn ID与对应 session.prompt 的 prompt_id 一致) */
prompt_id: string;
/** 目标 AI 应用标识 */
agent_app: string;
}
// ============================================
// 上行消息(客户端 → 服务端)
// ============================================
/**
* session.update 的更新类型
* @description
* 定义 session.update 消息中 update_type 字段的可选值:
* - message_chunk: Agent 生成的增量文本片段(流式输出,每次只包含新增的部分)
* - tool_call: Agent 开始调用一个工具(通知服务端展示工具调用状态)
* - tool_call_update: 工具调用状态变更(执行中的中间结果,或执行完成/失败)
*/
export type UpdateType = "message_chunk" | "tool_call" | "tool_call_update";
/**
* session.update 载荷 — 流式中间更新
* @description
* 在 Agent 处理 session.prompt 的过程中,通过此消息实时推送中间状态。
* 服务端收到后转发给用户端,实现流式输出效果。
*
* 根据 update_type 的不同,使用不同的字段:
* - message_chunk: 使用 content 字段(单个 ContentBlock非数组
* - tool_call / tool_call_update: 使用 tool_call 字段
*/
export interface UpdatePayload {
/** 所属 Session ID */
session_id: string;
/** 所属 Turn ID与对应 session.prompt 的 prompt_id 一致) */
prompt_id: string;
/** 更新类型,决定使用 content 还是 tool_call 字段 */
update_type: UpdateType;
/**
* 文本内容块update_type=message_chunk 时使用)
* 注意:这里是单个 ContentBlock 对象,而非数组
*/
content?: ContentBlock;
/** 工具调用信息update_type=tool_call 或 tool_call_update 时使用) */
tool_call?: ToolCall;
}
/**
* 停止原因枚举
* - end_turn: 正常完成
* - cancelled: 被取消
* - refusal: AI 应用拒绝执行
* - error: 技术错误
*/
export type StopReason = "end_turn" | "cancelled" | "refusal" | "error";
/**
* session.promptResponse 载荷 — 最终结果
* @description
* Agent 处理完 session.prompt 后,必须发送此消息告知服务端本次 Turn 已结束。
* 无论正常完成、被取消还是出错,都需要发送此消息。
* 服务端收到后才会认为本次 Turn 已关闭,可以接受下一个 prompt。
*/
export interface PromptResponsePayload {
/** 所属 Session ID */
session_id: string;
/** 所属 Turn ID与对应 session.prompt 的 prompt_id 一致) */
prompt_id: string;
/** 停止原因,告知服务端 Turn 是如何结束的 */
stop_reason: StopReason;
/**
* 最终结果内容ContentBlock 数组)
* stop_reason=end_turn 时附带,包含 Agent 的完整回复文本
* stop_reason=cancelled/error 时通常为空
*/
content?: ContentBlock[];
/** 错误描述stop_reason 为 error 或 refusal 时附带,说明失败原因) */
error?: string;
}
// ============================================
// 类型别名(方便使用)
// ============================================
/** 下行session.prompt 消息 */
export type PromptMessage = AGPEnvelope<PromptPayload>;
/** 下行session.cancel 消息 */
export type CancelMessage = AGPEnvelope<CancelPayload>;
/** 上行session.update 消息 */
export type UpdateMessage = AGPEnvelope<UpdatePayload>;
/** 上行session.promptResponse 消息 */
export type PromptResponseMessage = AGPEnvelope<PromptResponsePayload>;
// ============================================
// WebSocket 客户端配置
// ============================================
/**
* WebSocket 客户端配置
* @description
* 在插件入口index.ts的 WS_CONFIG 常量中配置,传入 WechatAccessWebSocketClient 构造函数。
*/
export interface WebSocketClientConfig {
/** WebSocket 服务端地址(如 ws://21.0.62.97:8080/ */
url: string;
/** 设备唯一标识,用于服务端识别连接来源(作为 URL 查询参数传递) */
guid: string;
/** 用户账户 ID作为 URL 查询参数传递,也用于上行消息的 user_id 字段) */
userId: string;
/** 鉴权 token可选作为 URL 查询参数传递,当前服务端未校验) */
token?: string;
/**
* 重连间隔基准值(毫秒),默认 30003秒
* 实际重连间隔使用指数退避策略,此值是第一次重连的等待时间
*/
reconnectInterval?: number;
/**
* 最大重连次数,默认 0无限重连
* 设为正整数时,超过此次数后停止重连并将状态设为 disconnected
*/
maxReconnectAttempts?: number;
/**
* 心跳间隔(毫秒),默认 2400004分钟
* 应小于服务端的空闲超时时间(通常为 5 分钟),确保连接不会因空闲被断开
* 心跳使用 WebSocket 原生 ping 控制帧ws 库的 ws.ping() 方法)
*/
heartbeatInterval?: number;
/**
* 当前 openclaw gateway 监听的端口号(来自 cfg.gateway.port
* 用于日志前缀,方便区分多实例
*/
gatewayPort?: string;
}
/**
* WebSocket 连接状态
*/
export type ConnectionState = "disconnected" | "connecting" | "connected" | "reconnecting";
/**
* WebSocket 客户端事件回调
*/
export interface WebSocketClientCallbacks {
/** 连接成功 */
onConnected?: () => void;
/** 连接断开 */
onDisconnected?: (reason?: string) => void;
/** 收到 session.prompt 消息 */
onPrompt?: (message: PromptMessage) => void;
/** 收到 session.cancel 消息 */
onCancel?: (message: CancelMessage) => void;
/** 发生错误 */
onError?: (error: Error) => void;
}