@dingtalk-real-ai/dingtalk-connector 0.8.0 → 0.8.2
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/CHANGELOG.md +26 -0
- package/README.en.md +14 -8
- package/README.md +14 -8
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/channel.ts +51 -51
- package/src/config/accounts.ts +13 -1
- package/src/core/connection.ts +71 -35
- package/src/core/message-handler.ts +118 -126
- package/src/core/provider.ts +2 -1
- package/src/onboarding.ts +91 -31
- package/src/reply-dispatcher.ts +14 -1
- package/src/services/media.ts +1 -1
- package/src/services/messaging.ts +32 -38
- package/src/utils/logger.ts +3 -3
- package/src/utils/session.ts +7 -2
- package/src/utils/utils-legacy.ts +4 -101
- package/test-dingtalk-connection.mjs +0 -105
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,32 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.8.2] - 2026-03-22
|
|
9
|
+
|
|
10
|
+
### 修复 / Fixes
|
|
11
|
+
- 🐛 **多账号重复启动问题** - 修复 `enabled: false` 的账号仍会建立 WebSocket 连接的问题,禁用账号现在正确保持 pending 状态直到 Gateway 停止
|
|
12
|
+
**Multi-account duplicate startup** - Fixed accounts with `enabled: false` still establishing WebSocket connections; disabled accounts now correctly remain in a pending state until the Gateway stops
|
|
13
|
+
|
|
14
|
+
- 🐛 **相同 clientId 账号去重** - 修复多个账号配置相同 `clientId` 时建立重复连接的问题,通过静态配置分析确保同一 `clientId` 只有列表中第一个启用账号建立连接
|
|
15
|
+
**Duplicate clientId deduplication** - Fixed duplicate connections when multiple accounts share the same `clientId`; static config analysis now ensures only the first enabled account per `clientId` establishes a connection
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### 改进 / Improvements
|
|
19
|
+
- ✅ **Onboarding 配置向导优化** - 改进钉钉连接器配置引导逻辑,调整凭据输入顺序(先 Client ID 后 Client Secret),优化引导文案
|
|
20
|
+
**Onboarding wizard improvement** - Improved DingTalk connector onboarding flow, adjusted credential input order (Client ID first, then Client Secret), and refined guidance text
|
|
21
|
+
|
|
22
|
+
- ✅ **会话 Key 遵循 OpenClaw 规范** - 会话上下文按 OpenClaw 标准规则构建,通过 `channel`、`accountId`、`chatType`、`peerId` 唯一标识会话,支持 `sharedMemoryAcrossConversations` 跨会话记忆共享
|
|
23
|
+
**Session key follows OpenClaw convention** - Session context now built per OpenClaw standard rules, uniquely identified via `channel`, `accountId`, `chatType`, `peerId`; supports `sharedMemoryAcrossConversations` for cross-conversation memory sharing
|
|
24
|
+
|
|
25
|
+
- ✅ **消息处理逻辑优化** - 重构消息处理流程,提升消息响应速度和处理可靠性,确保消息按序正确处理
|
|
26
|
+
**Message processing logic optimization** - Refactored message processing flow to improve response speed and reliability, ensuring messages are processed correctly in order
|
|
27
|
+
|
|
28
|
+
## [0.8.1] - 2026-03-20
|
|
29
|
+
|
|
30
|
+
### 修复 / Fixes
|
|
31
|
+
- 🐛 **文件和图片下载 OSS 签名验证失败** - 修复默认 `Content-Type` 请求头导致 OSS 签名验证失败的问题,确保文件和图片能够正常下载
|
|
32
|
+
**File and image download OSS signature verification failure** - Fixed OSS signature verification failure caused by default `Content-Type` header, ensuring files and images download correctly
|
|
33
|
+
|
|
8
34
|
## [0.8.0] - 2026-03-20
|
|
9
35
|
|
|
10
36
|
### 重构 / Refactoring
|
package/README.en.md
CHANGED
|
@@ -268,20 +268,26 @@ Configure multiple bots connected to different agents:
|
|
|
268
268
|
"agents": {
|
|
269
269
|
"list": [
|
|
270
270
|
{
|
|
271
|
-
"
|
|
271
|
+
"id": "ding-bot1",
|
|
272
|
+
"name": "Customer Service Bot",
|
|
272
273
|
"model": "your-model-config",
|
|
273
|
-
"
|
|
274
|
-
|
|
275
|
-
"
|
|
274
|
+
"workspace": "~/.openclaw/workspace-bot1",
|
|
275
|
+
"identity": {
|
|
276
|
+
"name": "Service Assistant",
|
|
277
|
+
"theme": "customer service",
|
|
278
|
+
"emoji": "🤝"
|
|
276
279
|
}
|
|
277
280
|
// Other agent configurations...
|
|
278
281
|
},
|
|
279
282
|
{
|
|
280
|
-
"
|
|
283
|
+
"id": "ding-bot2",
|
|
284
|
+
"name": "Technical Support Bot",
|
|
281
285
|
"model": "your-model-config",
|
|
282
|
-
"
|
|
283
|
-
|
|
284
|
-
"
|
|
286
|
+
"workspace": "~/.openclaw/workspace-bot2",
|
|
287
|
+
"identity": {
|
|
288
|
+
"name": "Tech Expert",
|
|
289
|
+
"theme": "technical support",
|
|
290
|
+
"emoji": "🔧"
|
|
285
291
|
}
|
|
286
292
|
// Other agent configurations...
|
|
287
293
|
}
|
package/README.md
CHANGED
|
@@ -279,20 +279,26 @@ openclaw logs --follow
|
|
|
279
279
|
"agents": {
|
|
280
280
|
"list": [
|
|
281
281
|
{
|
|
282
|
-
"
|
|
282
|
+
"id": "ding-bot1",
|
|
283
|
+
"name": "钉钉客服机器人",
|
|
283
284
|
"model": "your-model-config",
|
|
284
|
-
"
|
|
285
|
-
|
|
286
|
-
"
|
|
285
|
+
"workspace": "~/.openclaw/workspace-bot1",
|
|
286
|
+
"identity": {
|
|
287
|
+
"name": "客服小助手",
|
|
288
|
+
"theme": "专业客服",
|
|
289
|
+
"emoji": "🤝"
|
|
287
290
|
}
|
|
288
291
|
// 其他 agent 配置...
|
|
289
292
|
},
|
|
290
293
|
{
|
|
291
|
-
"
|
|
294
|
+
"id": "ding-bot2",
|
|
295
|
+
"name": "钉钉技术支持机器人",
|
|
292
296
|
"model": "your-model-config",
|
|
293
|
-
"
|
|
294
|
-
|
|
295
|
-
"
|
|
297
|
+
"workspace": "~/.openclaw/workspace-bot2",
|
|
298
|
+
"identity": {
|
|
299
|
+
"name": "技术专家",
|
|
300
|
+
"theme": "技术支持",
|
|
301
|
+
"emoji": "🔧"
|
|
296
302
|
}
|
|
297
303
|
// 其他 agent 配置...
|
|
298
304
|
}
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
package/src/channel.ts
CHANGED
|
@@ -495,64 +495,64 @@ export const dingtalkPlugin: ChannelPlugin<ResolvedDingtalkAccount> = {
|
|
|
495
495
|
gateway: {
|
|
496
496
|
startAccount: async (ctx) => {
|
|
497
497
|
const account = resolveDingtalkAccount({ cfg: ctx.cfg, accountId: ctx.accountId });
|
|
498
|
-
|
|
498
|
+
|
|
499
|
+
// 检查账号是否启用和配置
|
|
500
|
+
if (!account.enabled) {
|
|
501
|
+
ctx.log?.info?.(`dingtalk-connector[${ctx.accountId}] is disabled, skipping startup`);
|
|
502
|
+
// 返回一个永不 resolve 的 Promise,保持 pending 状态直到 abort
|
|
503
|
+
return new Promise<void>((resolve) => {
|
|
504
|
+
if (ctx.abortSignal?.aborted) {
|
|
505
|
+
resolve();
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
ctx.abortSignal?.addEventListener('abort', () => resolve(), { once: true });
|
|
509
|
+
});
|
|
510
|
+
}
|
|
499
511
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
// 使用直接属性访问而不是解构
|
|
527
|
-
const monitorDingtalkProvider = monitorModule.monitorDingtalkProvider;
|
|
528
|
-
logger.info(`解构 monitorDingtalkProvider 完成: ${typeof monitorDingtalkProvider}`);
|
|
529
|
-
|
|
530
|
-
if (!monitorDingtalkProvider) {
|
|
531
|
-
ctx.log?.error?.(`monitorDingtalkProvider 未找到!可用导出: ${Object.keys(monitorModule).join(', ')}`);
|
|
532
|
-
throw new Error("monitorDingtalkProvider not found in monitor module");
|
|
512
|
+
if (!account.configured) {
|
|
513
|
+
throw new Error(`DingTalk account "${ctx.accountId}" is not properly configured`);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// 去重检查:如果列表中排在当前账号之前的账号已使用相同 clientId,则跳过当前账号
|
|
517
|
+
// 使用静态配置分析(而非运行时状态),避免并发竞态条件
|
|
518
|
+
// 规则:同一 clientId 只有列表中第一个启用且已配置的账号才会建立连接
|
|
519
|
+
if (account.clientId) {
|
|
520
|
+
const clientId = String(account.clientId);
|
|
521
|
+
const allAccountIds = listDingtalkAccountIds(ctx.cfg);
|
|
522
|
+
const currentIndex = allAccountIds.indexOf(ctx.accountId);
|
|
523
|
+
const priorAccountWithSameClientId = allAccountIds.slice(0, currentIndex).find((otherId) => {
|
|
524
|
+
const other = resolveDingtalkAccount({ cfg: ctx.cfg, accountId: otherId });
|
|
525
|
+
return other.enabled && other.configured && other.clientId && String(other.clientId) === clientId;
|
|
526
|
+
});
|
|
527
|
+
if (priorAccountWithSameClientId) {
|
|
528
|
+
ctx.log?.info?.(
|
|
529
|
+
`dingtalk-connector[${ctx.accountId}] skipped: clientId "${clientId.substring(0, 8)}..." is already used by account "${priorAccountWithSameClientId}"`
|
|
530
|
+
);
|
|
531
|
+
return new Promise<void>((resolve) => {
|
|
532
|
+
if (ctx.abortSignal?.aborted) {
|
|
533
|
+
resolve();
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
ctx.abortSignal?.addEventListener('abort', () => resolve(), { once: true });
|
|
537
|
+
});
|
|
533
538
|
}
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
logger.info(`准备调用 monitorDingtalkProvider`);
|
|
543
|
-
|
|
544
|
-
const result = await monitorDingtalkProvider({
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
ctx.setStatus({ accountId: ctx.accountId, port: null });
|
|
542
|
+
ctx.log?.info(
|
|
543
|
+
`starting dingtalk-connector[${ctx.accountId}] (mode: stream)`,
|
|
544
|
+
);
|
|
545
|
+
try {
|
|
546
|
+
return await monitorDingtalkProvider({
|
|
545
547
|
config: ctx.cfg,
|
|
546
548
|
runtime: ctx.runtime,
|
|
547
549
|
abortSignal: ctx.abortSignal,
|
|
548
550
|
accountId: ctx.accountId,
|
|
549
551
|
});
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
ctx.log?.error?.(`错误堆栈: ${error.stack}`);
|
|
555
|
-
throw error;
|
|
552
|
+
} catch (err: any) {
|
|
553
|
+
// 打印真实错误到 stderr,绕过框架 log 系统(框架的 runtime.log 可能未初始化)
|
|
554
|
+
ctx.log?.error(`[dingtalk-connector][${ctx.accountId}] startAccount error:`, err?.message ?? err, err?.stack);
|
|
555
|
+
throw err;
|
|
556
556
|
}
|
|
557
557
|
},
|
|
558
558
|
},
|
package/src/config/accounts.ts
CHANGED
|
@@ -222,9 +222,21 @@ export function resolveDingtalkAccount(params: {
|
|
|
222
222
|
|
|
223
223
|
/**
|
|
224
224
|
* List all enabled and configured accounts.
|
|
225
|
+
* Deduplicates by clientId to avoid creating multiple connections with the same credentials.
|
|
225
226
|
*/
|
|
226
227
|
export function listEnabledDingtalkAccounts(cfg: ClawdbotConfig): ResolvedDingtalkAccount[] {
|
|
227
|
-
|
|
228
|
+
const accounts = listDingtalkAccountIds(cfg)
|
|
228
229
|
.map((accountId) => resolveDingtalkAccount({ cfg, accountId }))
|
|
229
230
|
.filter((account) => account.enabled && account.configured);
|
|
231
|
+
|
|
232
|
+
// Deduplicate by clientId to avoid multiple connections with same credentials
|
|
233
|
+
const seen = new Set<string>();
|
|
234
|
+
return accounts.filter((account) => {
|
|
235
|
+
if (!account.clientId) return true;
|
|
236
|
+
if (seen.has(account.clientId)) {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
seen.add(account.clientId);
|
|
240
|
+
return true;
|
|
241
|
+
});
|
|
230
242
|
}
|
package/src/core/connection.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* - 连接统计和监控(每分钟输出)
|
|
14
14
|
*/
|
|
15
15
|
import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk";
|
|
16
|
-
import type {
|
|
16
|
+
import type { ResolvedDingtalkAccount } from "../types/index.ts";
|
|
17
17
|
import {
|
|
18
18
|
isMessageProcessed,
|
|
19
19
|
markMessageProcessed,
|
|
@@ -30,14 +30,6 @@ export type DingtalkReactionCreatedEvent = {
|
|
|
30
30
|
emoji: string;
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
-
export type MonitorDingtalkAccountOpts = {
|
|
34
|
-
cfg: ClawdbotConfig;
|
|
35
|
-
account: ResolvedDingtalkAccount;
|
|
36
|
-
runtime?: RuntimeEnv;
|
|
37
|
-
abortSignal?: AbortSignal;
|
|
38
|
-
messageHandler: MessageHandler; // 直接传入消息处理器
|
|
39
|
-
};
|
|
40
|
-
|
|
41
33
|
// 消息处理器函数类型
|
|
42
34
|
export type MessageHandler = (params: {
|
|
43
35
|
accountId: string;
|
|
@@ -49,6 +41,14 @@ export type MessageHandler = (params: {
|
|
|
49
41
|
cfg: ClawdbotConfig;
|
|
50
42
|
}) => Promise<void>;
|
|
51
43
|
|
|
44
|
+
export type MonitorDingtalkAccountOpts = {
|
|
45
|
+
cfg: ClawdbotConfig;
|
|
46
|
+
account: ResolvedDingtalkAccount;
|
|
47
|
+
runtime?: RuntimeEnv;
|
|
48
|
+
abortSignal?: AbortSignal;
|
|
49
|
+
messageHandler: MessageHandler;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
52
|
// ============ 连接配置 ============
|
|
53
53
|
|
|
54
54
|
/** 心跳间隔(毫秒) */
|
|
@@ -138,6 +138,7 @@ export async function monitorSingleAccount(
|
|
|
138
138
|
// ============ 连接状态管理 ============
|
|
139
139
|
|
|
140
140
|
let lastSocketAvailableTime = Date.now();
|
|
141
|
+
let connectionEstablishedTime = Date.now(); // 记录连接建立时间
|
|
141
142
|
let isReconnecting = false;
|
|
142
143
|
let reconnectAttempts = 0;
|
|
143
144
|
let keepAliveTimer: NodeJS.Timeout | null = null;
|
|
@@ -226,21 +227,52 @@ export async function monitorSingleAccount(
|
|
|
226
227
|
// 2. 重新建立连接
|
|
227
228
|
await client.connect();
|
|
228
229
|
|
|
229
|
-
// 3.
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
230
|
+
// 3. 等待连接真正建立(监听 open 事件,最多等待 10 秒)
|
|
231
|
+
const connectionEstablished = await new Promise<boolean>((resolve) => {
|
|
232
|
+
const timeout = setTimeout(() => {
|
|
233
|
+
resolve(false);
|
|
234
|
+
}, 10_000); // 10 秒超时
|
|
235
|
+
|
|
236
|
+
// 如果已经是 OPEN 状态,直接返回
|
|
237
|
+
if (client.socket?.readyState === 1) {
|
|
238
|
+
clearTimeout(timeout);
|
|
239
|
+
resolve(true);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// 否则监听 open 事件
|
|
244
|
+
const onOpen = () => {
|
|
245
|
+
clearTimeout(timeout);
|
|
246
|
+
client.socket?.removeListener('open', onOpen);
|
|
247
|
+
client.socket?.removeListener('error', onError);
|
|
248
|
+
resolve(true);
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const onError = (err: any) => {
|
|
252
|
+
clearTimeout(timeout);
|
|
253
|
+
client.socket?.removeListener('open', onOpen);
|
|
254
|
+
client.socket?.removeListener('error', onError);
|
|
255
|
+
logger.warn(`连接建立失败: ${err.message}`);
|
|
256
|
+
resolve(false);
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
client.socket?.once('open', onOpen);
|
|
260
|
+
client.socket?.once('error', onError);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
if (!connectionEstablished) {
|
|
264
|
+
throw new Error(`连接建立超时或失败`);
|
|
234
265
|
}
|
|
235
266
|
|
|
236
|
-
// 4. 重置 socket
|
|
267
|
+
// 4. 重置 socket 可用时间、连接建立时间和重连计数
|
|
237
268
|
lastSocketAvailableTime = Date.now();
|
|
269
|
+
connectionEstablishedTime = Date.now(); // 重置连接建立时间
|
|
238
270
|
reconnectAttempts = 0; // 重连成功,重置计数
|
|
239
271
|
|
|
240
|
-
logger.info(`✅ 重连成功 (socket 状态=${
|
|
272
|
+
logger.info(`✅ 重连成功 (socket 状态=${client.socket?.readyState})`);
|
|
241
273
|
} catch (err: any) {
|
|
242
274
|
reconnectAttempts++;
|
|
243
|
-
|
|
275
|
+
logger.error(
|
|
244
276
|
`重连失败:${err.message} (尝试 ${reconnectAttempts})`,
|
|
245
277
|
);
|
|
246
278
|
throw err;
|
|
@@ -266,7 +298,7 @@ export async function monitorSingleAccount(
|
|
|
266
298
|
if (!isStopped && !isReconnecting) {
|
|
267
299
|
// 立即重连,不退避
|
|
268
300
|
doReconnect(true).catch((err) => {
|
|
269
|
-
|
|
301
|
+
logger.error(`[${accountId}] 重连失败:${err.message}`);
|
|
270
302
|
});
|
|
271
303
|
}
|
|
272
304
|
}
|
|
@@ -290,7 +322,7 @@ export async function monitorSingleAccount(
|
|
|
290
322
|
// 立即重连,不退避
|
|
291
323
|
setTimeout(() => {
|
|
292
324
|
doReconnect(true).catch((err) => {
|
|
293
|
-
|
|
325
|
+
logger.error(`重连失败:${err.message}`);
|
|
294
326
|
});
|
|
295
327
|
}, 0);
|
|
296
328
|
});
|
|
@@ -329,11 +361,20 @@ export async function monitorSingleAccount(
|
|
|
329
361
|
|
|
330
362
|
// 【心跳检测】检查 socket 状态
|
|
331
363
|
const socketState = client.socket?.readyState;
|
|
364
|
+
const timeSinceConnection = Date.now() - connectionEstablishedTime;
|
|
332
365
|
logger.debug(
|
|
333
|
-
`🔍 心跳检测:socket 状态=${socketState}, elapsed=${Math.round(elapsed / 1000)}s`,
|
|
366
|
+
`🔍 心跳检测:socket 状态=${socketState}, elapsed=${Math.round(elapsed / 1000)}s, 连接已建立=${Math.round(timeSinceConnection / 1000)}s`,
|
|
334
367
|
);
|
|
335
368
|
|
|
369
|
+
// 给新建立的连接 15 秒宽限期,避免在连接建立初期就触发重连
|
|
336
370
|
if (socketState !== 1) {
|
|
371
|
+
if (timeSinceConnection < 15_000) {
|
|
372
|
+
logger.debug(
|
|
373
|
+
`⏳ 连接建立中(已 ${Math.round(timeSinceConnection / 1000)}s),跳过状态检查`,
|
|
374
|
+
);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
337
378
|
logger.info(
|
|
338
379
|
`⚠️ 心跳检测:socket 状态=${socketState},触发重连...`,
|
|
339
380
|
);
|
|
@@ -347,11 +388,11 @@ export async function monitorSingleAccount(
|
|
|
347
388
|
lastSocketAvailableTime = Date.now();
|
|
348
389
|
logger.debug(`💓 发送 PING 心跳成功`);
|
|
349
390
|
} catch (err: any) {
|
|
350
|
-
|
|
391
|
+
logger.warn(`发送 PING 失败:${err.message}`);
|
|
351
392
|
// 发送失败也计入超时
|
|
352
393
|
}
|
|
353
394
|
} catch (err: any) {
|
|
354
|
-
|
|
395
|
+
logger.error(`keepAlive 检测失败:${err.message}`);
|
|
355
396
|
}
|
|
356
397
|
}, HEARTBEAT_INTERVAL); // 每 10 秒检测一次
|
|
357
398
|
|
|
@@ -404,7 +445,7 @@ export async function monitorSingleAccount(
|
|
|
404
445
|
await client.disconnect();
|
|
405
446
|
}
|
|
406
447
|
} catch (err: any) {
|
|
407
|
-
|
|
448
|
+
logger.warn(`断开连接时出错:${err.message}`);
|
|
408
449
|
}
|
|
409
450
|
resolve();
|
|
410
451
|
};
|
|
@@ -445,12 +486,13 @@ export async function monitorSingleAccount(
|
|
|
445
486
|
client.socketCallBackResponse(messageId, { success: true });
|
|
446
487
|
logger.info(`✅ 已立即确认回调:messageId=${messageId}`);
|
|
447
488
|
} else {
|
|
448
|
-
|
|
489
|
+
logger.warn(`⚠️ 警告:消息没有 messageId`);
|
|
449
490
|
}
|
|
450
491
|
|
|
451
492
|
// 消息去重
|
|
452
493
|
if (messageId && isMessageProcessed(messageId)) {
|
|
453
|
-
|
|
494
|
+
processedCount++; // ✅ 修复:重复消息也要计入 processedCount
|
|
495
|
+
logger.warn(`⚠️ 检测到重复消息,跳过处理:messageId=${messageId} (${processedCount}/${receivedCount})`);
|
|
454
496
|
logger.info(`========== 消息处理结束(重复) ==========\n`);
|
|
455
497
|
return;
|
|
456
498
|
}
|
|
@@ -519,8 +561,6 @@ export async function monitorSingleAccount(
|
|
|
519
561
|
|
|
520
562
|
// ===== 第三步:开始处理消息 =====
|
|
521
563
|
logger.info(`🚀 开始处理消息...`);
|
|
522
|
-
logger.info(`AccountId: ${accountId}`);
|
|
523
|
-
logger.info(`HasConfig: ${!!account.config}`);
|
|
524
564
|
|
|
525
565
|
await messageHandler({
|
|
526
566
|
accountId,
|
|
@@ -540,13 +580,9 @@ export async function monitorSingleAccount(
|
|
|
540
580
|
const errorMsg = `❌ 处理消息异常 (${processedCount}/${receivedCount}): ${error?.message || "未知错误"}`;
|
|
541
581
|
const errorStack = error?.stack || "无堆栈信息";
|
|
542
582
|
|
|
543
|
-
// 使用 logger
|
|
544
|
-
logger.
|
|
545
|
-
logger.
|
|
546
|
-
|
|
547
|
-
// 同时使用 log?.error 记录(如果可用)
|
|
548
|
-
log?.error?.(errorMsg);
|
|
549
|
-
log?.error?.(`错误堆栈:\n${errorStack}`);
|
|
583
|
+
// 使用 logger 记录错误信息
|
|
584
|
+
logger.error(errorMsg);
|
|
585
|
+
logger.error(`错误堆栈:\n${errorStack}`);
|
|
550
586
|
|
|
551
587
|
logger.info(`========== 消息处理结束(失败) ==========\n`);
|
|
552
588
|
} finally {
|
|
@@ -633,7 +669,7 @@ export async function monitorSingleAccount(
|
|
|
633
669
|
// client.on('close', ...) - 已移除,使用 setupCloseListener
|
|
634
670
|
|
|
635
671
|
client.on("error", (err: Error) => {
|
|
636
|
-
|
|
672
|
+
logger.error(`Connection error: ${err.message}`);
|
|
637
673
|
});
|
|
638
674
|
|
|
639
675
|
// 监听重连事件(仅用于日志,实际重连由自定义逻辑处理)
|