- 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)
291 lines
10 KiB
TypeScript
291 lines
10 KiB
TypeScript
/**
|
||
* @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> {
|
||
/** 全局唯一消息 ID(UUID),用于幂等去重 */
|
||
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;
|
||
/**
|
||
* 重连间隔基准值(毫秒),默认 3000(3秒)
|
||
* 实际重连间隔使用指数退避策略,此值是第一次重连的等待时间
|
||
*/
|
||
reconnectInterval?: number;
|
||
/**
|
||
* 最大重连次数,默认 0(无限重连)
|
||
* 设为正整数时,超过此次数后停止重连并将状态设为 disconnected
|
||
*/
|
||
maxReconnectAttempts?: number;
|
||
/**
|
||
* 心跳间隔(毫秒),默认 240000(4分钟)
|
||
* 应小于服务端的空闲超时时间(通常为 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;
|
||
}
|