@insta-dev01/intclaw 1.0.11 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.en.md +424 -0
- package/README.md +365 -164
- package/index.ts +28 -0
- package/openclaw.plugin.json +10 -39
- package/package.json +69 -40
- package/src/channel.ts +557 -0
- package/src/config/accounts.ts +230 -0
- package/src/config/schema.ts +144 -0
- package/src/core/connection.ts +733 -0
- package/src/core/message-handler.ts +1268 -0
- package/src/core/provider.ts +106 -0
- package/src/core/state.ts +54 -0
- package/src/directory.ts +95 -0
- package/src/gateway-methods.ts +237 -0
- package/src/onboarding.ts +387 -0
- package/src/policy.ts +19 -0
- package/src/probe.ts +213 -0
- package/src/reply-dispatcher.ts +674 -0
- package/src/runtime.ts +7 -0
- package/src/sdk/helpers.ts +317 -0
- package/src/sdk/types.ts +515 -0
- package/src/secret-input.ts +19 -0
- package/src/services/media/audio.ts +54 -0
- package/src/services/media/chunk-upload.ts +293 -0
- package/src/services/media/common.ts +154 -0
- package/src/services/media/file.ts +70 -0
- package/src/services/media/image.ts +67 -0
- package/src/services/media/index.ts +10 -0
- package/src/services/media/video.ts +162 -0
- package/src/services/media.ts +1134 -0
- package/src/services/messaging/index.ts +16 -0
- package/src/services/messaging/send.ts +137 -0
- package/src/services/messaging.ts +800 -0
- package/src/targets.ts +45 -0
- package/src/types/index.ts +52 -0
- package/src/utils/agent.ts +63 -0
- package/src/utils/async.ts +51 -0
- package/src/utils/constants.ts +9 -0
- package/src/utils/http-client.ts +84 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/logger.ts +78 -0
- package/src/utils/session.ts +118 -0
- package/src/utils/token.ts +94 -0
- package/src/utils/utils-legacy.ts +506 -0
- package/.env.example +0 -11
- package/skills/intclaw_matrix/SKILL.md +0 -20
- package/src/channel/intclaw_channel.js +0 -155
- package/src/index.js +0 -23
|
@@ -0,0 +1,733 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IntClaw WebSocket 连接层
|
|
3
|
+
*
|
|
4
|
+
* 职责:
|
|
5
|
+
* - 管理单个IntClaw账号的 WebSocket 连接
|
|
6
|
+
* - 实现应用层心跳检测(10 秒间隔,90 秒超时)
|
|
7
|
+
* - 处理连接重连逻辑,带指数退避
|
|
8
|
+
* - 消息去重(内置 Map,5 分钟 TTL)
|
|
9
|
+
*
|
|
10
|
+
* 核心特性:
|
|
11
|
+
* - 关闭 SDK 内置 keepAlive,使用自定义心跳
|
|
12
|
+
* - 详细的消息接收日志(三阶段:接收、解析、处理)
|
|
13
|
+
* - 连接统计和监控(每分钟输出)
|
|
14
|
+
*/
|
|
15
|
+
import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk";
|
|
16
|
+
import type { ResolvedIntclawAccount } from "../types/index.ts";
|
|
17
|
+
import {
|
|
18
|
+
isMessageProcessed,
|
|
19
|
+
markMessageProcessed,
|
|
20
|
+
} from "../utils/utils-legacy.ts";
|
|
21
|
+
import { createLoggerFromConfig } from "../utils/logger.ts";
|
|
22
|
+
import { INTCLAW_CONFIG } from "../../config.ts";
|
|
23
|
+
|
|
24
|
+
// ============ 类型定义 ============
|
|
25
|
+
|
|
26
|
+
export type IntclawReactionCreatedEvent = {
|
|
27
|
+
type: "reaction_created";
|
|
28
|
+
channelId: string;
|
|
29
|
+
messageId: string;
|
|
30
|
+
userId: string;
|
|
31
|
+
emoji: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type MonitorIntclawAccountOpts = {
|
|
35
|
+
cfg: ClawdbotConfig;
|
|
36
|
+
account: ResolvedIntclawAccount;
|
|
37
|
+
runtime?: RuntimeEnv;
|
|
38
|
+
abortSignal?: AbortSignal;
|
|
39
|
+
messageHandler: MessageHandler; // 直接传入消息处理器
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// 消息处理器函数类型
|
|
43
|
+
export type MessageHandler = (params: {
|
|
44
|
+
accountId: string;
|
|
45
|
+
config: any;
|
|
46
|
+
data: any;
|
|
47
|
+
sessionWebhook: string;
|
|
48
|
+
runtime?: RuntimeEnv;
|
|
49
|
+
log?: any;
|
|
50
|
+
cfg: ClawdbotConfig;
|
|
51
|
+
}) => Promise<void>;
|
|
52
|
+
|
|
53
|
+
// ============ 连接配置 ============
|
|
54
|
+
|
|
55
|
+
/** 心跳间隔(毫秒) */
|
|
56
|
+
const HEARTBEAT_INTERVAL = 10 * 1000; // 10 秒
|
|
57
|
+
/** 超时阈值(毫秒) */
|
|
58
|
+
const TIMEOUT_THRESHOLD = 20 * 1000; // 20 秒(2 次心跳未响应)
|
|
59
|
+
/** 基础退避时间(毫秒) */
|
|
60
|
+
const BASE_BACKOFF_DELAY = 1000; // 1 秒
|
|
61
|
+
/** 最大退避时间(毫秒) */
|
|
62
|
+
const MAX_BACKOFF_DELAY = 30 * 1000; // 30 秒
|
|
63
|
+
|
|
64
|
+
// ============ 监控账号 ============
|
|
65
|
+
|
|
66
|
+
export async function monitorSingleAccount(
|
|
67
|
+
opts: MonitorIntclawAccountOpts,
|
|
68
|
+
): Promise<void> {
|
|
69
|
+
const { cfg, account, runtime, abortSignal, messageHandler } = opts;
|
|
70
|
+
const { accountId } = account;
|
|
71
|
+
|
|
72
|
+
// 保存 cfg 以便传递给 messageHandler
|
|
73
|
+
const clawdbotConfig = cfg;
|
|
74
|
+
const log = runtime?.log;
|
|
75
|
+
|
|
76
|
+
// 创建 debug logger(仅在 debug 模式下输出 info/debug 日志)
|
|
77
|
+
const logger = createLoggerFromConfig(account.config, `IntClaw:${accountId}`);
|
|
78
|
+
|
|
79
|
+
// 验证凭据是否存在
|
|
80
|
+
if (!account.clientId || !account.clientSecret) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`[IntClaw][${accountId}] Missing credentials: ` +
|
|
83
|
+
`clientId=${account.clientId ? "present" : "MISSING"}, ` +
|
|
84
|
+
`clientSecret=${account.clientSecret ? "present" : "MISSING"}. ` +
|
|
85
|
+
`Please check your configuration in channels.intclaw-connector.`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 验证凭据格式
|
|
90
|
+
const clientIdStr = String(account.clientId);
|
|
91
|
+
const clientSecretStr = String(account.clientSecret);
|
|
92
|
+
|
|
93
|
+
if (clientIdStr.length < 10 || clientSecretStr.length < 10) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`[IntClaw][${accountId}] Invalid credentials format: ` +
|
|
96
|
+
`clientId length=${clientIdStr.length}, clientSecret length=${clientSecretStr.length}. ` +
|
|
97
|
+
`Credentials appear to be too short or invalid.`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
logger.info(`Starting IntClaw Stream client...`);
|
|
102
|
+
logger.info(`Initializing with clientId: ${clientIdStr.substring(0, 8)}...`);
|
|
103
|
+
logger.info(`WebSocket keepAlive: false (using application-layer heartbeat)`);
|
|
104
|
+
|
|
105
|
+
// 动态导入 ws 模块
|
|
106
|
+
const wsModule = await import("ws");
|
|
107
|
+
const WebSocket = wsModule.default;
|
|
108
|
+
|
|
109
|
+
// 包装器,兼容原有的 client 接口
|
|
110
|
+
const client = {
|
|
111
|
+
socket: null as import("ws").WebSocket | null,
|
|
112
|
+
messageHandler: null as ((res: any) => void) | null,
|
|
113
|
+
|
|
114
|
+
// 连接
|
|
115
|
+
connect: async () => {
|
|
116
|
+
return new Promise<void>((resolve, reject) => {
|
|
117
|
+
const endpoint = account.config.endpoint || INTCLAW_CONFIG.WS_ENDPOINT;
|
|
118
|
+
const headers = {
|
|
119
|
+
"x-app-key": String(account.clientId),
|
|
120
|
+
"x-app-secret": String(account.clientSecret),
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
logger.info(`开始连接 WebSocket: ${endpoint}`);
|
|
124
|
+
const ws = new WebSocket(endpoint, { headers });
|
|
125
|
+
|
|
126
|
+
const onOpen = () => {
|
|
127
|
+
ws.removeListener('error', onError);
|
|
128
|
+
client.socket = ws;
|
|
129
|
+
rebindListeners();
|
|
130
|
+
resolve();
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const onError = (err: any) => {
|
|
134
|
+
ws.removeListener('open', onOpen);
|
|
135
|
+
reject(err);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
ws.once('open', onOpen);
|
|
139
|
+
ws.once('error', onError);
|
|
140
|
+
});
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
// 断开
|
|
144
|
+
disconnect: async () => {
|
|
145
|
+
if (client.socket) {
|
|
146
|
+
client.socket.removeAllListeners();
|
|
147
|
+
client.socket.terminate();
|
|
148
|
+
client.socket = null;
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
// 回复响应
|
|
153
|
+
socketCallBackResponse: (messageId: string, payload: any) => {
|
|
154
|
+
if (client.socket && client.socket.readyState === WebSocket.OPEN) {
|
|
155
|
+
client.socket.send(JSON.stringify({
|
|
156
|
+
headers: { messageId },
|
|
157
|
+
data: payload
|
|
158
|
+
}));
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
// 注册消息处理
|
|
163
|
+
registerCallbackListener: (topic: string, handler: (res: any) => void) => {
|
|
164
|
+
client.messageHandler = handler;
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
// 处理旧的 client.on
|
|
168
|
+
on: (evt: string, cb: any) => {}
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// ============ 连接状态管理 ============
|
|
172
|
+
|
|
173
|
+
let lastSocketAvailableTime = Date.now();
|
|
174
|
+
let connectionEstablishedTime = Date.now(); // 记录连接建立时间
|
|
175
|
+
let isReconnecting = false;
|
|
176
|
+
let reconnectAttempts = 0;
|
|
177
|
+
let keepAliveTimer: NodeJS.Timeout | null = null;
|
|
178
|
+
let isStopped = false;
|
|
179
|
+
|
|
180
|
+
// ============ 消息处理活跃标记 ============
|
|
181
|
+
// 用于在消息处理期间防止心跳超时触发重连
|
|
182
|
+
let activeMessageProcessing = false;
|
|
183
|
+
let messageProcessingKeepAliveTimer: NodeJS.Timeout | null = null;
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* 标记消息处理开始,启动定期更新机制
|
|
187
|
+
* 在消息处理期间,每 30 秒更新一次 lastSocketAvailableTime
|
|
188
|
+
* 防止长时间处理(如复杂的 AI 任务)触发心跳超时
|
|
189
|
+
*/
|
|
190
|
+
function markMessageProcessingStart() {
|
|
191
|
+
activeMessageProcessing = true;
|
|
192
|
+
lastSocketAvailableTime = Date.now();
|
|
193
|
+
|
|
194
|
+
// 清理旧的定时器(如果存在)
|
|
195
|
+
if (messageProcessingKeepAliveTimer) {
|
|
196
|
+
clearInterval(messageProcessingKeepAliveTimer);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// 每 30 秒更新一次,确保不会触发 90 秒超时
|
|
200
|
+
messageProcessingKeepAliveTimer = setInterval(() => {
|
|
201
|
+
if (activeMessageProcessing) {
|
|
202
|
+
lastSocketAvailableTime = Date.now();
|
|
203
|
+
logger.debug(`📝 消息处理中,更新 socket 可用时间`);
|
|
204
|
+
}
|
|
205
|
+
}, 30 * 1000); // 30 秒间隔
|
|
206
|
+
|
|
207
|
+
logger.debug(`📝 消息处理开始,启动活跃标记定时器`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* 标记消息处理结束,停止定期更新机制
|
|
212
|
+
*/
|
|
213
|
+
function markMessageProcessingEnd() {
|
|
214
|
+
activeMessageProcessing = false;
|
|
215
|
+
|
|
216
|
+
if (messageProcessingKeepAliveTimer) {
|
|
217
|
+
clearInterval(messageProcessingKeepAliveTimer);
|
|
218
|
+
messageProcessingKeepAliveTimer = null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// 最后更新一次时间
|
|
222
|
+
lastSocketAvailableTime = Date.now();
|
|
223
|
+
logger.debug(`✅ 消息处理结束,清理活跃标记定时器`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ============ 辅助函数 ============
|
|
227
|
+
|
|
228
|
+
/** 计算指数退避延迟(带抖动) */
|
|
229
|
+
function calculateBackoffDelay(attempt: number): number {
|
|
230
|
+
const exponentialDelay = BASE_BACKOFF_DELAY * Math.pow(2, attempt);
|
|
231
|
+
const jitter = Math.random() * 1000; // 0-1 秒随机抖动
|
|
232
|
+
return Math.min(exponentialDelay + jitter, MAX_BACKOFF_DELAY);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** 统一重连函数,带指数退避(无限重连) */
|
|
236
|
+
async function doReconnect(immediate = false) {
|
|
237
|
+
if (isReconnecting || isStopped) {
|
|
238
|
+
logger.debug(`正在重连中或已停止,跳过`);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
isReconnecting = true;
|
|
243
|
+
|
|
244
|
+
// 应用指数退避(非立即重连时)
|
|
245
|
+
if (!immediate && reconnectAttempts > 0) {
|
|
246
|
+
const delay = calculateBackoffDelay(reconnectAttempts);
|
|
247
|
+
logger.info(
|
|
248
|
+
`⏳ 等待 ${Math.round(delay / 1000)} 秒后重连 (尝试 ${reconnectAttempts + 1})`,
|
|
249
|
+
);
|
|
250
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
// 1. 先断开旧连接(检查 WebSocket 状态)
|
|
255
|
+
if (client.socket?.readyState === 1 || client.socket?.readyState === 3) {
|
|
256
|
+
await client.disconnect();
|
|
257
|
+
logger.info(`已断开旧连接`);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// 2. 重新建立连接
|
|
261
|
+
await client.connect();
|
|
262
|
+
|
|
263
|
+
// 3. 等待连接真正建立(监听 open 事件,最多等待 10 秒)
|
|
264
|
+
const connectionEstablished = await new Promise<boolean>((resolve) => {
|
|
265
|
+
const timeout = setTimeout(() => {
|
|
266
|
+
resolve(false);
|
|
267
|
+
}, 10_000); // 10 秒超时
|
|
268
|
+
|
|
269
|
+
// 如果已经是 OPEN 状态,直接返回
|
|
270
|
+
if (client.socket?.readyState === 1) {
|
|
271
|
+
clearTimeout(timeout);
|
|
272
|
+
resolve(true);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// 否则监听 open 事件
|
|
277
|
+
const onOpen = () => {
|
|
278
|
+
clearTimeout(timeout);
|
|
279
|
+
client.socket?.removeListener('open', onOpen);
|
|
280
|
+
client.socket?.removeListener('error', onError);
|
|
281
|
+
resolve(true);
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
const onError = (err: any) => {
|
|
285
|
+
clearTimeout(timeout);
|
|
286
|
+
client.socket?.removeListener('open', onOpen);
|
|
287
|
+
client.socket?.removeListener('error', onError);
|
|
288
|
+
logger.warn(`连接建立失败: ${err.message}`);
|
|
289
|
+
resolve(false);
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
client.socket?.once('open', onOpen);
|
|
293
|
+
client.socket?.once('error', onError);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
if (!connectionEstablished) {
|
|
297
|
+
throw new Error(`连接建立超时或失败`);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// 4. 重置 socket 可用时间、连接建立时间和重连计数
|
|
301
|
+
lastSocketAvailableTime = Date.now();
|
|
302
|
+
connectionEstablishedTime = Date.now(); // 重置连接建立时间
|
|
303
|
+
reconnectAttempts = 0; // 重连成功,重置计数
|
|
304
|
+
|
|
305
|
+
logger.info(`✅ 重连成功 (socket 状态=${client.socket?.readyState})`);
|
|
306
|
+
} catch (err: any) {
|
|
307
|
+
reconnectAttempts++;
|
|
308
|
+
log?.error?.(
|
|
309
|
+
`重连失败:${err.message} (尝试 ${reconnectAttempts})`,
|
|
310
|
+
);
|
|
311
|
+
throw err;
|
|
312
|
+
} finally {
|
|
313
|
+
isReconnecting = false;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** 重新绑定所有 WebSocket 事件监听器 */
|
|
318
|
+
function rebindListeners() {
|
|
319
|
+
if (!client.socket) return;
|
|
320
|
+
|
|
321
|
+
client.socket.on("pong", () => {
|
|
322
|
+
lastSocketAvailableTime = Date.now();
|
|
323
|
+
logger.debug(`收到 PONG 响应`);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
client.socket.on("message", (data: any) => {
|
|
327
|
+
try {
|
|
328
|
+
const payload = Object.prototype.toString.call(data) === '[object Buffer]' ? data.toString() : data as string;
|
|
329
|
+
const msg = JSON.parse(payload);
|
|
330
|
+
|
|
331
|
+
// 检查 disconnect 类型
|
|
332
|
+
if (msg.type === "SYSTEM" && msg.headers?.topic === "disconnect") {
|
|
333
|
+
if (!isStopped && !isReconnecting) {
|
|
334
|
+
doReconnect(true).catch((err) => {
|
|
335
|
+
log?.error?.(`[${accountId}] 重连失败:${err.message}`);
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// 派发给外部 handler
|
|
342
|
+
// 如果是从普通 WS 收到,保证有 headers.messageId
|
|
343
|
+
if (client.messageHandler) {
|
|
344
|
+
const res = msg.headers ? msg : { headers: { messageId: msg.msgId || msg.messageId }, data: payload };
|
|
345
|
+
client.messageHandler(res);
|
|
346
|
+
}
|
|
347
|
+
} catch (e) {
|
|
348
|
+
// 忽略解析错误
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
client.socket.on("close", (code, reason) => {
|
|
353
|
+
logger.info(
|
|
354
|
+
`WebSocket close: code=${code}, reason=${reason || "未知"}, isStopped=${isStopped}`
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
if (isStopped) {
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
setTimeout(() => {
|
|
362
|
+
doReconnect(true).catch((err) => {
|
|
363
|
+
log?.error?.(`重连失败:${err.message}`);
|
|
364
|
+
});
|
|
365
|
+
}, 0);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
client.socket.on("error", (err) => {
|
|
369
|
+
log?.error?.(`WebSocket Error: ${err.message}`);
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* 启动 keepAlive 机制(单定时器 + 指数退避)
|
|
375
|
+
*
|
|
376
|
+
* 业界最佳实践:
|
|
377
|
+
* - 单定时器:每 10 秒检查一次,同时完成心跳和超时检测
|
|
378
|
+
* - 使用 WebSocket 原生 Ping
|
|
379
|
+
* - 指数退避重连:避免雪崩效应
|
|
380
|
+
*/
|
|
381
|
+
function startKeepAlive(): () => void {
|
|
382
|
+
logger.debug(
|
|
383
|
+
`🚀 启动 keepAlive 定时器,间隔=${HEARTBEAT_INTERVAL / 1000}秒`,
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
keepAliveTimer = setInterval(async () => {
|
|
387
|
+
if (isStopped) {
|
|
388
|
+
if (keepAliveTimer) clearInterval(keepAliveTimer);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
try {
|
|
393
|
+
const elapsed = Date.now() - lastSocketAvailableTime;
|
|
394
|
+
|
|
395
|
+
// 【超时检测】超过 90 秒未确认 socket 可用,触发重连
|
|
396
|
+
if (elapsed > TIMEOUT_THRESHOLD) {
|
|
397
|
+
logger.info(
|
|
398
|
+
`⚠️ 超时检测:已 ${Math.round(elapsed / 1000)} 秒未确认 socket 可用,触发重连...`,
|
|
399
|
+
);
|
|
400
|
+
await doReconnect();
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// 【心跳检测】检查 socket 状态
|
|
405
|
+
const socketState = client.socket?.readyState;
|
|
406
|
+
const timeSinceConnection = Date.now() - connectionEstablishedTime;
|
|
407
|
+
logger.debug(
|
|
408
|
+
`🔍 心跳检测:socket 状态=${socketState}, elapsed=${Math.round(elapsed / 1000)}s, 连接已建立=${Math.round(timeSinceConnection / 1000)}s`,
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
// 给新建立的连接 15 秒宽限期,避免在连接建立初期就触发重连
|
|
412
|
+
if (socketState !== 1) {
|
|
413
|
+
if (timeSinceConnection < 15_000) {
|
|
414
|
+
logger.debug(
|
|
415
|
+
`⏳ 连接建立中(已 ${Math.round(timeSinceConnection / 1000)}s),跳过状态检查`,
|
|
416
|
+
);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
logger.info(
|
|
421
|
+
`⚠️ 心跳检测:socket 状态=${socketState},触发重连...`,
|
|
422
|
+
);
|
|
423
|
+
await doReconnect(true); // 立即重连,不退避
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// 【发送原生 Ping】更新可用时间
|
|
428
|
+
try {
|
|
429
|
+
client.socket?.ping();
|
|
430
|
+
lastSocketAvailableTime = Date.now();
|
|
431
|
+
logger.debug(`💓 发送 PING 心跳成功`);
|
|
432
|
+
} catch (err: any) {
|
|
433
|
+
log?.warn?.(`发送 PING 失败:${err.message}`);
|
|
434
|
+
// 发送失败也计入超时
|
|
435
|
+
}
|
|
436
|
+
} catch (err: any) {
|
|
437
|
+
log?.error?.(`keepAlive 检测失败:${err.message}`);
|
|
438
|
+
}
|
|
439
|
+
}, HEARTBEAT_INTERVAL); // 每 10 秒检测一次
|
|
440
|
+
|
|
441
|
+
logger.debug(`✅ keepAlive 定时器已启动`);
|
|
442
|
+
|
|
443
|
+
// 返回清理函数
|
|
444
|
+
return () => {
|
|
445
|
+
if (keepAliveTimer) clearInterval(keepAliveTimer);
|
|
446
|
+
keepAliveTimer = null;
|
|
447
|
+
logger.debug(`keepAlive 定时器已清理`);
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/** 停止并清理所有资源 */
|
|
452
|
+
function stop() {
|
|
453
|
+
isStopped = true;
|
|
454
|
+
|
|
455
|
+
// 清理心跳定时器
|
|
456
|
+
if (keepAliveTimer) clearInterval(keepAliveTimer);
|
|
457
|
+
keepAliveTimer = null;
|
|
458
|
+
|
|
459
|
+
// 清理消息处理活跃标记定时器
|
|
460
|
+
if (messageProcessingKeepAliveTimer) {
|
|
461
|
+
clearInterval(messageProcessingKeepAliveTimer);
|
|
462
|
+
messageProcessingKeepAliveTimer = null;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// 清理事件监听器
|
|
466
|
+
if (client.socket) {
|
|
467
|
+
client.socket.removeAllListeners();
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
logger.debug(`Connection 已停止`);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// 事件监听将在 connect() 中的 onOpen 中绑定
|
|
474
|
+
|
|
475
|
+
return new Promise<void>(async (resolve, reject) => {
|
|
476
|
+
// Handle abort signal
|
|
477
|
+
if (abortSignal) {
|
|
478
|
+
const onAbort = async () => {
|
|
479
|
+
logger.info(`Abort signal received, stopping...`);
|
|
480
|
+
stop();
|
|
481
|
+
try {
|
|
482
|
+
// 只在连接已建立时才断开
|
|
483
|
+
if (client.socket && client.socket.readyState === 1) {
|
|
484
|
+
await client.disconnect();
|
|
485
|
+
}
|
|
486
|
+
} catch (err: any) {
|
|
487
|
+
log?.warn?.(`断开连接时出错:${err.message}`);
|
|
488
|
+
}
|
|
489
|
+
resolve();
|
|
490
|
+
};
|
|
491
|
+
abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// 消息接收统计(用于检测消息丢失)
|
|
495
|
+
let receivedCount = 0;
|
|
496
|
+
let processedCount = 0;
|
|
497
|
+
let lastMessageTime = Date.now();
|
|
498
|
+
|
|
499
|
+
// 定期输出统计信息
|
|
500
|
+
const statsInterval = setInterval(() => {
|
|
501
|
+
const now = Date.now();
|
|
502
|
+
const timeSinceLastMessage = Math.round((now - lastMessageTime) / 1000);
|
|
503
|
+
logger.info(
|
|
504
|
+
`统计:收到=${receivedCount}, 处理=${processedCount}, ` +
|
|
505
|
+
`丢失=${receivedCount - processedCount}, 距上次消息=${timeSinceLastMessage}s`,
|
|
506
|
+
);
|
|
507
|
+
}, 60000); // 每分钟输出一次
|
|
508
|
+
|
|
509
|
+
// Register message handler
|
|
510
|
+
client.registerCallbackListener("robot", async (res: any) => {
|
|
511
|
+
receivedCount++;
|
|
512
|
+
lastMessageTime = Date.now();
|
|
513
|
+
const messageId = res.headers?.messageId;
|
|
514
|
+
const timestamp = new Date().toISOString();
|
|
515
|
+
|
|
516
|
+
// ===== 第一步:记录原始消息接收 =====
|
|
517
|
+
logger.info(`\n========== 收到新消息 ==========`);
|
|
518
|
+
logger.info(`时间:${timestamp}`);
|
|
519
|
+
logger.info(`MessageId: ${messageId || "N/A"}`);
|
|
520
|
+
logger.info(`Headers: ${JSON.stringify(res.headers || {})}`);
|
|
521
|
+
logger.info(`Data 长度:${res.data?.length || 0} 字符`);
|
|
522
|
+
|
|
523
|
+
// 立即确认回调
|
|
524
|
+
if (messageId) {
|
|
525
|
+
client.socketCallBackResponse(messageId, { success: true });
|
|
526
|
+
logger.info(`✅ 已立即确认回调:messageId=${messageId}`);
|
|
527
|
+
} else {
|
|
528
|
+
log?.warn?.(`⚠️ 警告:消息没有 messageId`);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// 消息去重
|
|
532
|
+
if (messageId && isMessageProcessed(messageId)) {
|
|
533
|
+
log?.warn?.(`⚠️ 检测到重复消息,跳过处理:messageId=${messageId}`);
|
|
534
|
+
logger.info(`========== 消息处理结束(重复) ==========\n`);
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (messageId) {
|
|
539
|
+
markMessageProcessed(messageId);
|
|
540
|
+
logger.info(`标记消息为已处理:messageId=${messageId}`);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// 异步处理消息
|
|
544
|
+
// ✅ 标记消息处理开始,防止长时间处理触发心跳超时
|
|
545
|
+
markMessageProcessingStart();
|
|
546
|
+
|
|
547
|
+
try {
|
|
548
|
+
// 解析消息数据
|
|
549
|
+
let data;
|
|
550
|
+
try {
|
|
551
|
+
data = JSON.parse(res.data);
|
|
552
|
+
} catch (parseError: any) {
|
|
553
|
+
logger.error('Failed to parse response data as JSON:', {
|
|
554
|
+
error: parseError instanceof Error ? parseError.message : String(parseError),
|
|
555
|
+
rawData: typeof res.data === 'string'
|
|
556
|
+
? res.data.substring(0, 500) // 只记录前 500 字符
|
|
557
|
+
: res.data,
|
|
558
|
+
dataType: typeof res.data,
|
|
559
|
+
});
|
|
560
|
+
throw new Error(
|
|
561
|
+
`Invalid JSON response from IntClaw API. ` +
|
|
562
|
+
`Error: ${parseError instanceof Error ? parseError.message : String(parseError)}. ` +
|
|
563
|
+
`Raw data (first 100 chars): ${String(res.data).substring(0, 100)}`
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// ===== 第二步:记录解析后的消息详情 =====
|
|
568
|
+
logger.info(`\n----- 消息详情 -----`);
|
|
569
|
+
logger.info(`消息类型:${data.msgtype || "unknown"}`);
|
|
570
|
+
logger.info(
|
|
571
|
+
`会话类型:${data.conversationType === "1" ? "DM (单聊)" : data.conversationType === "2" ? "Group (群聊)" : data.conversationType}`,
|
|
572
|
+
);
|
|
573
|
+
logger.info(
|
|
574
|
+
`发送者:${data.senderNick || "unknown"} (${data.senderStaffId || data.senderId || "unknown"})`,
|
|
575
|
+
);
|
|
576
|
+
logger.info(`会话 ID: ${data.conversationId || "N/A"}`);
|
|
577
|
+
logger.info(`消息 ID: ${data.msgId || "N/A"}`);
|
|
578
|
+
logger.info(
|
|
579
|
+
`SessionWebhook: ${data.sessionWebhook ? "已提供" : "未提供"}`,
|
|
580
|
+
);
|
|
581
|
+
logger.info(
|
|
582
|
+
`RobotCode: ${data.robotCode || account.config?.clientId || "N/A"}`,
|
|
583
|
+
);
|
|
584
|
+
|
|
585
|
+
// 记录消息内容(简化版,避免过长)
|
|
586
|
+
let contentPreview = "N/A";
|
|
587
|
+
if (data.text?.content) {
|
|
588
|
+
contentPreview =
|
|
589
|
+
data.text.content.length > 100
|
|
590
|
+
? data.text.content.substring(0, 100) + "..."
|
|
591
|
+
: data.text.content;
|
|
592
|
+
} else if (data.content) {
|
|
593
|
+
contentPreview =
|
|
594
|
+
JSON.stringify(data.content).substring(0, 100) + "...";
|
|
595
|
+
}
|
|
596
|
+
logger.info(`消息内容预览:${contentPreview}`);
|
|
597
|
+
logger.info(`完整数据字段:${Object.keys(data).join(", ")}`);
|
|
598
|
+
logger.info(`----- 消息详情结束 -----\n`);
|
|
599
|
+
|
|
600
|
+
// ===== 第三步:开始处理消息 =====
|
|
601
|
+
logger.info(`🚀 开始处理消息...`);
|
|
602
|
+
|
|
603
|
+
await messageHandler({
|
|
604
|
+
accountId,
|
|
605
|
+
config: account.config,
|
|
606
|
+
data,
|
|
607
|
+
sessionWebhook: data.sessionWebhook,
|
|
608
|
+
runtime,
|
|
609
|
+
log,
|
|
610
|
+
cfg: clawdbotConfig,
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
processedCount++;
|
|
614
|
+
logger.info(`✅ 消息处理完成 (${processedCount}/${receivedCount})`);
|
|
615
|
+
logger.info(`========== 消息处理结束(成功) ==========\n`);
|
|
616
|
+
} catch (error: any) {
|
|
617
|
+
processedCount++;
|
|
618
|
+
const errorMsg = `❌ 处理消息异常 (${processedCount}/${receivedCount}): ${error?.message || "未知错误"}`;
|
|
619
|
+
const errorStack = error?.stack || "无堆栈信息";
|
|
620
|
+
|
|
621
|
+
// 使用 logger 确保错误信息一定会被打印
|
|
622
|
+
logger.info(errorMsg);
|
|
623
|
+
logger.info(`错误堆栈:\n${errorStack}`);
|
|
624
|
+
|
|
625
|
+
// 同时使用 log?.error 记录(如果可用)
|
|
626
|
+
log?.error?.(errorMsg);
|
|
627
|
+
log?.error?.(`错误堆栈:\n${errorStack}`);
|
|
628
|
+
|
|
629
|
+
logger.info(`========== 消息处理结束(失败) ==========\n`);
|
|
630
|
+
} finally {
|
|
631
|
+
// ✅ 无论成功或失败,都要标记消息处理结束
|
|
632
|
+
markMessageProcessingEnd();
|
|
633
|
+
}
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
// 清理定时器
|
|
637
|
+
const cleanup = () => {
|
|
638
|
+
clearInterval(statsInterval);
|
|
639
|
+
stop();
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
// Connect to IntClaw Stream
|
|
643
|
+
try {
|
|
644
|
+
await client.connect();
|
|
645
|
+
logger.info(`Connected to IntClaw Stream successfully`);
|
|
646
|
+
logger.info(`PID: ${process.pid}`);
|
|
647
|
+
logger.info(
|
|
648
|
+
`✅ 自定义 keepAlive: true (10 秒心跳,90 秒超时), 指数退避重连`,
|
|
649
|
+
);
|
|
650
|
+
|
|
651
|
+
// 启动自定义心跳检测
|
|
652
|
+
const cleanupKeepAlive = startKeepAlive();
|
|
653
|
+
|
|
654
|
+
// 重写 cleanup 函数,包含 keepAlive 清理
|
|
655
|
+
const enhancedCleanup = () => {
|
|
656
|
+
cleanupKeepAlive();
|
|
657
|
+
clearInterval(statsInterval);
|
|
658
|
+
stop();
|
|
659
|
+
};
|
|
660
|
+
|
|
661
|
+
// 进程退出时清理(使用 once 确保只执行一次)
|
|
662
|
+
process.once("exit", enhancedCleanup);
|
|
663
|
+
process.once("SIGINT", enhancedCleanup);
|
|
664
|
+
process.once("SIGTERM", enhancedCleanup);
|
|
665
|
+
} catch (error: any) {
|
|
666
|
+
cleanup(); // 连接失败时清理资源
|
|
667
|
+
|
|
668
|
+
// 记录完整错误信息用于调试
|
|
669
|
+
logger.info(`连接失败,错误详情:`);
|
|
670
|
+
logger.info(` - error.message: ${error.message}`);
|
|
671
|
+
logger.info(` - error.response?.status: ${error.response?.status}`);
|
|
672
|
+
logger.info(` - error.response?.data: ${JSON.stringify(error.response?.data || {})}`);
|
|
673
|
+
logger.info(` - error.code: ${error.code}`);
|
|
674
|
+
logger.info(` - error.stack: ${error.stack?.split('\n').slice(0, 3).join('\n')}`);
|
|
675
|
+
|
|
676
|
+
// 处理 400 错误(请求参数错误)
|
|
677
|
+
if (error.response?.status === 400 || error.message?.includes("status code 400") || error.message?.includes("400")) {
|
|
678
|
+
throw new Error(
|
|
679
|
+
`[IntClaw][${accountId}] Bad Request (400):\n` +
|
|
680
|
+
` - clientId or clientSecret format is invalid\n` +
|
|
681
|
+
` - clientId: ${clientIdStr} (type: ${typeof account.clientId}, length: ${clientIdStr.length})\n` +
|
|
682
|
+
` - clientSecret: ****** (type: ${typeof account.clientSecret}, length: ${clientSecretStr.length})\n` +
|
|
683
|
+
` - Common issues:\n` +
|
|
684
|
+
` 1. clientId/clientSecret must be strings, not numbers\n` +
|
|
685
|
+
` 2. Remove any quotes or special characters\n` +
|
|
686
|
+
` 3. Ensure credentials are from the correct IntClaw app\n` +
|
|
687
|
+
` 4. Check if clientId starts with 'ding' prefix\n` +
|
|
688
|
+
` - Error details: ${error.message}\n` +
|
|
689
|
+
` - Response data: ${JSON.stringify(error.response?.data || {})}`,
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// 处理 401 认证错误
|
|
694
|
+
if (error.response?.status === 401 || error.message?.includes("401")) {
|
|
695
|
+
throw new Error(
|
|
696
|
+
`[IntClaw][${accountId}] Authentication failed (401 Unauthorized):\n` +
|
|
697
|
+
` - Your clientId or clientSecret is invalid, expired, or revoked\n` +
|
|
698
|
+
` - clientId: ${clientIdStr.substring(0, 8)}...\n` +
|
|
699
|
+
` - Please verify your credentials at IntClaw Developer Console\n` +
|
|
700
|
+
` - Error details: ${error.message}`,
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// 处理其他连接错误
|
|
705
|
+
throw new Error(
|
|
706
|
+
`[IntClaw][${accountId}] Failed to connect to IntClaw Stream: ${error.message}`,
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// Handle disconnection(已被自定义 close 监听器替代)
|
|
711
|
+
// client.on('close', ...) - 已移除,使用 setupCloseListener
|
|
712
|
+
|
|
713
|
+
client.on("error", (err: Error) => {
|
|
714
|
+
log?.error?.(`Connection error: ${err.message}`);
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
// 监听重连事件(仅用于日志,实际重连由自定义逻辑处理)
|
|
718
|
+
client.on("reconnect", () => {
|
|
719
|
+
logger.info(`SDK reconnecting...`);
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
client.on("reconnected", () => {
|
|
723
|
+
logger.info(`✅ SDK reconnected successfully`);
|
|
724
|
+
});
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
export function resolveReactionSyntheticEvent(
|
|
729
|
+
event: any,
|
|
730
|
+
): IntclawReactionCreatedEvent | null {
|
|
731
|
+
void event;
|
|
732
|
+
return null;
|
|
733
|
+
}
|