RESTful API、类型系统、同步缓冲区
API 协议是插件与微信服务器通信的基础,而数据流设计决定了消息如何在整个系统中流转。本文将深入剖析 OpenClaw WeChat 插件的 API 协议设计、数据类型系统、同步缓冲区机制以及日志与监控体系,帮助开发者理解其底层通信原理。
一、API 协议架构概览
OpenClaw WeChat 插件采用 RESTful API 与微信服务器通信,所有请求使用 JSON 格式,通过 HTTP/HTTPS 传输:
┌─────────────────────────────────────────────────────────────────────────┐
│ API Protocol Stack │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Application Layer │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────┐ │ │
│ │ │ getUpdates │ │ sendMessage │ │ getUploadUrl│ │getConfig│ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Transport Layer (HTTP) │ │
│ │ POST /ilink/bot/getupdates JSON Request/Response │ │
│ │ POST /ilink/bot/sendmessage │ │
│ │ POST /ilink/bot/getuploadurl │ │
│ │ POST /ilink/bot/getconfig │ │
│ │ POST /ilink/bot/sendtyping │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Security Layer │ │
│ │ Authorization: Bearer <token> │ │
│ │ AuthorizationType: ilink_bot_token │ │
│ │ X-WECHAT-UIN: <random> │ │
│ │ SKRouteTag: <optional> │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
二、核心 API 接口详解
2.1 API 选项配置
所有 API 调用共享统一的选项配置:
export type WeixinApiOptions = {
baseUrl: string;
token?: string;
timeoutMs?: number;
/** Long-poll timeout for getUpdates (server may hold the request up to this). */
longPollTimeoutMs?: number;
};
默认超时配置根据 API 类型区分:
const DEFAULT_LONG_POLL_TIMEOUT_MS = 35_000;
const DEFAULT_API_TIMEOUT_MS = 15_000;
const DEFAULT_CONFIG_TIMEOUT_MS = 10_000;
2.2 通用请求构建
所有 API 请求共享统一的请求构建逻辑:
async function apiFetch(params: {
baseUrl: string;
endpoint: string;
body: string;
token?: string;
timeoutMs: number;
label: string;
}): Promise<string> {
const base = ensureTrailingSlash(params.baseUrl);
const url = new URL(params.endpoint, base);
const hdrs = buildHeaders({ token: params.token, body: params.body });
logger.debug(`POST ${redactUrl(url.toString())} body=${redactBody(params.body)}`);
const controller = new AbortController();
const t = setTimeout(() => controller.abort(), params.timeoutMs);
try {
const res = await fetch(url.toString(), {
method: "POST",
headers: hdrs,
body: params.body,
signal: controller.signal,
});
clearTimeout(t);
const rawText = await res.text();
logger.debug(`${params.label} status=${res.status} raw=${redactBody(rawText)}`);
if (!res.ok) {
throw new Error(`${params.label} ${res.status}: ${rawText}`);
}
return rawText;
} catch (err) {
clearTimeout(t);
throw err;
}
}
2.3 请求头构建
请求头包含身份验证和路由信息:
function buildHeaders(opts: { token?: string; body: string }): Record<string, string> {
const headers: Record<string, string> = {
"Content-Type": "application/json",
AuthorizationType: "ilink_bot_token",
"Content-Length": String(Buffer.byteLength(opts.body, "utf-8")),
"X-WECHAT-UIN": randomWechatUin(),
};
if (opts.token?.trim()) {
headers.Authorization = `Bearer ${opts.token.trim()}`;
}
const routeTag = loadConfigRouteTag();
if (routeTag) {
headers.SKRouteTag = routeTag;
}
return headers;
}
/** X-WECHAT-UIN header: random uint32 -> decimal string -> base64. */
function randomWechatUin(): string {
const uint32 = crypto.randomBytes(4).readUInt32BE(0);
return Buffer.from(String(uint32), "utf-8").toString("base64");
}
2.4 GetUpdates 长轮询
GetUpdates 是消息接收的核心接口,采用长轮询机制:
export async function getUpdates(
params: GetUpdatesReq & {
baseUrl: string;
token?: string;
timeoutMs?: number;
},
): Promise<GetUpdatesResp> {
const timeout = params.timeoutMs ?? DEFAULT_LONG_POLL_TIMEOUT_MS;
try {
const rawText = await apiFetch({
baseUrl: params.baseUrl,
endpoint: "ilink/bot/getupdates",
body: JSON.stringify({
get_updates_buf: params.get_updates_buf ?? "",
base_info: buildBaseInfo(),
}),
token: params.token,
timeoutMs: timeout,
label: "getUpdates",
});
const resp: GetUpdatesResp = JSON.parse(rawText);
return resp;
} catch (err) {
// Long-poll timeout is normal; return empty response so caller can retry
if (err instanceof Error && err.name === "AbortError") {
logger.debug(`getUpdates: client-side timeout after ${timeout}ms, returning empty response`);
return { ret: 0, msgs: [], get_updates_buf: params.get_updates_buf };
}
throw err;
}
}
长轮询的特点:
- 客户端设置 35 秒超时
- 服务器保持连接直到有新消息或超时
- 客户端超时视为正常情况,自动重试
- 返回
get_updates_buf用于下次请求
2.5 发送消息
SendMessage 用于向用户发送消息:
export async function sendMessage(
params: WeixinApiOptions & { body: SendMessageReq },
): Promise<void> {
await apiFetch({
baseUrl: params.baseUrl,
endpoint: "ilink/bot/sendmessage",
body: JSON.stringify({ ...params.body, base_info: buildBaseInfo() }),
token: params.token,
timeoutMs: params.timeoutMs ?? DEFAULT_API_TIMEOUT_MS,
label: "sendMessage",
});
}
2.6 获取上传 URL
GetUploadUrl 用于获取 CDN 上传的预签名 URL:
export async function getUploadUrl(
params: GetUploadUrlReq & WeixinApiOptions,
): Promise<GetUploadUrlResp> {
const rawText = await apiFetch({
baseUrl: params.baseUrl,
endpoint: "ilink/bot/getuploadurl",
body: JSON.stringify({
filekey: params.filekey,
media_type: params.media_type,
to_user_id: params.to_user_id,
rawsize: params.rawsize,
rawfilemd5: params.rawfilemd5,
filesize: params.filesize,
no_need_thumb: params.no_need_thumb,
aeskey: params.aeskey,
base_info: buildBaseInfo(),
}),
token: params.token,
timeoutMs: params.timeoutMs ?? DEFAULT_API_TIMEOUT_MS,
label: "getUploadUrl",
});
const resp: GetUploadUrlResp = JSON.parse(rawText);
return resp;
}
2.7 获取配置
GetConfig 用于获取用户的配置信息,包括 typing_ticket:
export async function getConfig(
params: WeixinApiOptions & { ilinkUserId: string; contextToken?: string },
): Promise<GetConfigResp> {
const rawText = await apiFetch({
baseUrl: params.baseUrl,
endpoint: "ilink/bot/getconfig",
body: JSON.stringify({
ilink_user_id: params.ilinkUserId,
context_token: params.contextToken,
base_info: buildBaseInfo(),
}),
token: params.token,
timeoutMs: params.timeoutMs ?? DEFAULT_CONFIG_TIMEOUT_MS,
label: "getConfig",
});
const resp: GetConfigResp = JSON.parse(rawText);
return resp;
}
2.8 发送打字指示器
SendTyping 用于向用户显示"正在输入"状态:
export async function sendTyping(
params: WeixinApiOptions & { body: SendTypingReq },
): Promise<void> {
await apiFetch({
baseUrl: params.baseUrl,
endpoint: "ilink/bot/sendtyping",
body: JSON.stringify({ ...params.body, base_info: buildBaseInfo() }),
token: params.token,
timeoutMs: params.timeoutMs ?? DEFAULT_CONFIG_TIMEOUT_MS,
label: "sendTyping",
});
}
三、数据类型系统
3.1 基础信息类型
每个 API 请求都包含基础信息:
export interface BaseInfo {
channel_version?: string;
}
function readChannelVersion(): string {
try {
const dir = path.dirname(fileURLToPath(import.meta.url));
const pkgPath = path.resolve(dir, "..", "..", "package.json");
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8")) as { version?: string };
return pkg.version ?? "unknown";
} catch {
return "unknown";
}
}
const CHANNEL_VERSION = readChannelVersion();
export function buildBaseInfo(): BaseInfo {
return { channel_version: CHANNEL_VERSION };
}
3.2 消息类型定义
消息系统支持多种类型的消息项:
export const MessageItemType = {
NONE: 0,
TEXT: 1,
IMAGE: 2,
VOICE: 3,
FILE: 4,
VIDEO: 5,
} as const;
export const MessageType = {
NONE: 0,
USER: 1,
BOT: 2,
} as const;
export const MessageState = {
NEW: 0,
GENERATING: 1,
FINISH: 2,
} as const;
3.3 消息项结构
消息项采用联合类型设计,通过 type 字段区分:
export interface MessageItem {
type?: number;
create_time_ms?: number;
update_time_ms?: number;
is_completed?: boolean;
msg_id?: string;
ref_msg?: RefMessage;
text_item?: TextItem;
image_item?: ImageItem;
voice_item?: VoiceItem;
file_item?: FileItem;
video_item?: VideoItem;
}
export interface TextItem {
text?: string;
}
export interface ImageItem {
media?: CDNMedia;
thumb_media?: CDNMedia;
aeskey?: string;
url?: string;
mid_size?: number;
thumb_size?: number;
hd_size?: number;
}
export interface VoiceItem {
media?: CDNMedia;
encode_type?: number;
sample_rate?: number;
playtime?: number;
text?: string;
}
export interface FileItem {
media?: CDNMedia;
file_name?: string;
md5?: string;
len?: string;
}
export interface VideoItem {
media?: CDNMedia;
video_size?: number;
play_length?: number;
thumb_media?: CDNMedia;
}
3.4 CDN 媒体引用
媒体文件通过 CDN 引用访问:
export interface CDNMedia {
encrypt_query_param?: string;
aes_key?: string;
encrypt_type?: number;
}
3.5 统一消息结构
WeixinMessage 是统一的消息结构:
export interface WeixinMessage {
seq?: number;
message_id?: number;
from_user_id?: string;
to_user_id?: string;
client_id?: string;
create_time_ms?: number;
update_time_ms?: number;
delete_time_ms?: number;
session_id?: string;
group_id?: string;
message_type?: number;
message_state?: number;
item_list?: MessageItem[];
context_token?: string;
}
关键字段说明:
seq:消息序列号,用于排序message_id:唯一消息标识from_user_id/to_user_id:发送者和接收者client_id:客户端生成的消息 IDcreate_time_ms:消息创建时间(毫秒时间戳)session_id:会话标识item_list:消息内容项列表context_token:上下文令牌,回复时必须携带
3.6 GetUpdates 请求/响应
export interface GetUpdatesReq {
/** @deprecated compat only, will be removed */
sync_buf?: string;
/** Full context buf cached locally; send "" when none (first request or after reset). */
get_updates_buf?: string;
}
export interface GetUpdatesResp {
ret?: number;
errcode?: number;
errmsg?: string;
msgs?: WeixinMessage[];
get_updates_buf?: string;
longpolling_timeout_ms?: number;
}
3.7 打字状态
export const TypingStatus = {
TYPING: 1,
CANCEL: 2,
} as const;
export interface SendTypingReq {
ilink_user_id?: string;
typing_ticket?: string;
status?: number;
}
四、同步缓冲区机制
4.1 同步缓冲区的作用
同步缓冲区(sync buffer)是实现消息不丢失的关键机制:
┌─────────────────────────────────────────────────────────────────────────┐
│ Sync Buffer Flow │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Client Weixin Server │
│ │ │ │
│ │ 1. getUpdates(buf="") │ │
│ │ -------------------------> │ │
│ │ │ │
│ │ 2. msgs: [A, B, C] │ │
│ │ new_buf: "XYZ123" │ │
│ │ <------------------------- │ │
│ │ │ │
│ │ [Save "XYZ123" to file] │ │
│ │ │ │
│ │ 3. getUpdates(buf="XYZ123") │
│ │ -------------------------> │ │
│ │ │ │
│ │ [Server knows client has A, B, C] │
│ │ │ │
│ │ 4. msgs: [D, E] │ │
│ │ new_buf: "ABC789" │ │
│ │ <------------------------- │ │
│ │ │ │
│ │ [Save "ABC789" to file] │ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
4.2 同步缓冲区存储
export type SyncBufData = {
get_updates_buf: string;
};
export function getSyncBufFilePath(accountId: string): string {
return path.join(resolveAccountsDir(), `${accountId}.sync.json`);
}
export function saveGetUpdatesBuf(filePath: string, getUpdatesBuf: string): void {
const dir = path.dirname(filePath);
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(filePath, JSON.stringify({ get_updates_buf: getUpdatesBuf }, null, 0), "utf-8");
}
4.3 多层兼容性回退
同步缓冲区加载支持多层回退:
export function loadGetUpdatesBuf(filePath: string): string | undefined {
const value = readSyncBufFile(filePath);
if (value !== undefined) return value;
// Compat: if given path uses a normalized accountId (e.g. "b0f5860fdecb-im-bot.sync.json"),
// also try the old raw-ID filename (e.g. "b0f5860fdecb@im.bot.sync.json").
const accountId = path.basename(filePath, ".sync.json");
const rawId = deriveRawAccountId(accountId);
if (rawId) {
const compatPath = path.join(resolveAccountsDir(), `${rawId}.sync.json`);
const compatValue = readSyncBufFile(compatPath);
if (compatValue !== undefined) return compatValue;
}
// Legacy fallback: old single-account installs stored syncbuf without accountId.
return readSyncBufFile(getLegacySyncBufDefaultJsonPath());
}
回退层级:
- 主路径:规范化账号 ID 的同步文件
- 兼容路径:原始格式账号 ID 的同步文件
- 遗留路径:单账号时代的默认同步文件
五、状态目录管理
5.1 状态目录解析
插件使用统一的状态目录存储所有持久化数据:
export function resolveStateDir(): string {
return (
process.env.OPENCLAW_STATE_DIR?.trim() ||
process.env.CLAWDBOT_STATE_DIR?.trim() ||
path.join(os.homedir(), ".openclaw")
);
}
环境变量优先级:
OPENCLAW_STATE_DIR:首选环境变量CLAWDBOT_STATE_DIR:向后兼容的旧变量名- 默认路径:
~/.openclaw
5.2 目录结构
~/.openclaw/
├── openclaw-weixin/
│ ├── accounts.json # 账号索引
│ ├── accounts/
│ │ ├── {accountId}.json # 账号凭证
│ │ └── {accountId}.sync.json # 同步缓冲区
│ └── debug-mode.json # 调试模式状态
└── credentials/
└── openclaw-weixin-{accountId}-allowFrom.json # 授权列表
六、日志系统
6.1 日志架构
插件使用与 OpenClaw 核心统一的日志格式:
const MAIN_LOG_DIR = path.join("/tmp", "openclaw");
const SUBSYSTEM = "gateway/channels/openclaw-weixin";
const RUNTIME = "node";
const RUNTIME_VERSION = process.versions.node;
const HOSTNAME = os.hostname() || "unknown";
6.2 日志级别
const LEVEL_IDS: Record<string, number> = {
TRACE: 1,
DEBUG: 2,
INFO: 3,
WARN: 4,
ERROR: 5,
FATAL: 6,
};
const DEFAULT_LOG_LEVEL = "INFO";
6.3 日志记录实现
function writeLog(level: string, message: string, accountId?: string): void {
const levelId = LEVEL_IDS[level] ?? LEVEL_IDS.INFO;
if (levelId < minLevelId) return;
const now = new Date();
const loggerName = buildLoggerName(accountId);
const prefixedMessage = accountId ? `[${accountId}] ${message}` : message;
const entry = JSON.stringify({
"0": loggerName,
"1": prefixedMessage,
_meta: {
runtime: RUNTIME,
runtimeVersion: RUNTIME_VERSION,
hostname: HOSTNAME,
name: loggerName,
parentNames: PARENT_NAMES,
date: now.toISOString(),
logLevelId: LEVEL_IDS[level] ?? LEVEL_IDS.INFO,
logLevelName: level,
},
time: toLocalISO(now),
});
try {
if (!logDirEnsured) {
fs.mkdirSync(MAIN_LOG_DIR, { recursive: true });
logDirEnsured = true;
}
fs.appendFileSync(resolveMainLogPath(), `${entry}\n`, "utf-8");
} catch {
// Best-effort; never block on logging failures.
}
}
6.4 日志格式
日志采用 JSON Lines 格式,便于结构化处理:
{
"0": "gateway/channels/openclaw-weixin/b0f5860fdecb-im-bot",
"1": "[b0f5860fdecb-im-bot] inbound message: from=xxx@im.wechat types=1",
"_meta": {
"runtime": "node",
"runtimeVersion": "22.0.0",
"hostname": "myhost",
"name": "gateway/channels/openclaw-weixin/b0f5860fdecb-im-bot",
"parentNames": ["openclaw"],
"date": "2026-03-22T10:30:00.000Z",
"logLevelId": 3,
"logLevelName": "INFO"
},
"time": "2026-03-22T18:30:00.000+08:00"
}
6.5 子日志器
支持按账号创建子日志器:
export type Logger = {
info(message: string): void;
debug(message: string): void;
warn(message: string): void;
error(message: string): void;
withAccount(accountId: string): Logger;
getLogFilePath(): string;
close(): void;
};
function createLogger(accountId?: string): Logger {
return {
info(message: string): void {
writeLog("INFO", message, accountId);
},
// ... 其他级别
withAccount(id: string): Logger {
return createLogger(id);
},
};
}
七、敏感信息脱敏
7.1 脱敏工具函数
export function truncate(s: string | undefined, max: number): string {
if (!s) return "";
if (s.length <= max) return s;
return `${s.slice(0, max)}…(len=${s.length})`;
}
export function redactToken(token: string | undefined, prefixLen = 6): string {
if (!token) return "(none)";
if (token.length <= prefixLen) return `****(len=${token.length})`;
return `${token.slice(0, prefixLen)}…(len=${token.length})`;
}
export function redactBody(body: string | undefined, maxLen = 200): string {
if (!body) return "(empty)";
if (body.length <= maxLen) return body;
return `${body.slice(0, maxLen)}…(truncated, totalLen=${body.length})`;
}
export function redactUrl(rawUrl: string): string {
try {
const u = new URL(rawUrl);
const base = `${u.origin}${u.pathname}`;
return u.search ? `${base}?<redacted>` : base;
} catch {
return truncate(rawUrl, 80);
}
}
7.2 脱敏策略
- Token:显示前 6 个字符,隐藏其余部分
- 请求体:截断至 200 字符
- URL:隐藏查询字符串(可能包含签名)
- 空值:明确标记为 "(none)" 或 "(empty)"
八、ID 生成与随机数
8.1 消息 ID 生成
export function generateId(prefix: string): string {
return `${prefix}:${Date.now()}-${crypto.randomBytes(4).toString("hex")}`;
}
格式:{prefix}:{timestamp}-{8-char hex}
示例:openclaw-weixin:1711090800000-a1b2c3d4
8.2 临时文件名生成
export function tempFileName(prefix: string, ext: string): string {
return `${prefix}-${Date.now()}-${crypto.randomBytes(4).toString("hex")}${ext}`;
}
格式:{prefix}-{timestamp}-{8-char hex}{ext}
示例:weixin-remote-1711090800000-a1b2c3d4.jpg
8.3 设计考量
- 时间戳:确保基本的有序性
- 随机数:防止冲突,增强不可预测性
- 前缀:便于识别和分类
- crypto 模块:使用加密安全的随机数生成
九、总结
OpenClaw WeChat 插件的 API 协议与数据流设计展现了以下特点:
- RESTful API:统一的 HTTP JSON 接口,易于理解和调试
- 长轮询机制:实现低延迟消息接收,同时保持简单性
- 类型安全:完整的 TypeScript 类型定义,编译时检查
- 同步缓冲:确保消息不丢失,支持断点续传
- 统一日志:与 OpenClaw 核心一致的日志格式,便于集中分析
- 安全脱敏:敏感信息自动脱敏,防止日志泄露
- ID 生成:时间戳+随机数的混合策略,兼顾有序性和唯一性
这些设计不仅保证了系统的稳定性和可靠性,也为开发者提供了清晰的接口契约和调试手段。在下一篇文章中,我们将探讨进阶开发与实践,包括调试技巧、性能优化和故障排查。