@jeik/dingtalk-connector 0.8.28 → 0.8.30
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 +40 -30
- package/README.en.md +114 -130
- package/README.md +112 -131
- package/dist/{background-work-Cxb4b-91.mjs → background-work-DkaSDqWk.mjs} +12 -1
- package/dist/card-content-cache-C7-XHhHw.mjs +2 -0
- package/dist/{card-content-cache-bDQFz2u8.mjs → card-content-cache-DT_OYThh.mjs} +15 -1
- package/dist/chunk-upload-BKM1y4Ba.mjs +2 -0
- package/dist/{chunk-upload-BE71drPg.mjs → chunk-upload-CIU5G_ay.mjs} +1 -1
- package/dist/{common-BZiVgd3A.mjs → common-BfIYs1yC.mjs} +3 -3
- package/dist/{common-DgL28gCZ.mjs → common-C0Oaf0z-.mjs} +1 -1
- package/dist/{connection-CZ7ZoN5_.mjs → connection-obtrPkj4.mjs} +56 -65
- package/dist/entry-bundled.mjs +1 -1
- package/dist/{gateway-methods-kNX3qvqw.mjs → gateway-methods-BdcpfxEZ.mjs} +1 -1
- package/dist/gateway-methods-DQ6TwOAr.mjs +2 -0
- package/dist/index.mjs +2 -2
- package/dist/{logger-mZ9OSbmD.mjs → logger-CnBTcwyq.mjs} +16 -2
- package/dist/logger-D8H2IcYz.mjs +2 -0
- package/dist/{media-BqpUGCni.mjs → media-DDtX37T2.mjs} +1 -1
- package/dist/{media-DnQB2BrL.mjs → media-g8tJtpfU.mjs} +29 -20
- package/dist/{message-handler-B1usz1wb.mjs → message-handler-BeTQQCUe.mjs} +33 -30
- package/dist/{messaging-DVS3UKOp.mjs → messaging-CK0Wz3Ze.mjs} +18 -20
- package/dist/{runtime-A6G7WAWn.mjs → runtime-CjE4Z9G5.mjs} +6 -5
- package/dist/{utils-Bjh4r_qS.mjs → utils-Bwz2Fls_.mjs} +1 -1
- package/docs/RELEASE_NOTES_V0.8.28.md +4 -4
- package/docs/RELEASE_NOTES_V0.8.29.md +72 -0
- package/openclaw.plugin.json +9 -5
- package/package.json +1 -1
- package/src/config/schema.ts +2 -1
- package/src/core/connection.ts +90 -113
- package/src/core/message-handler.ts +24 -16
- package/src/reply-dispatcher.ts +33 -20
- package/src/services/media.ts +60 -27
- package/src/services/messaging/card-content-cache.ts +11 -6
- package/src/services/messaging.ts +52 -38
- package/src/utils/background-work.ts +3 -1
- package/src/utils/logger.ts +23 -2
- package/dist/card-content-cache-c4Ls96eJ.mjs +0 -2
- package/dist/chunk-upload-PwhogwfE.mjs +0 -2
- package/dist/gateway-methods-BkI1gKaC.mjs +0 -2
- package/dist/logger-BmJkQkm1.mjs +0 -2
package/openclaw.plugin.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "dingtalk-connector",
|
|
3
3
|
"name": "DingTalk Channel",
|
|
4
|
-
"version": "0.8.
|
|
5
|
-
"description": "
|
|
6
|
-
"author": "
|
|
4
|
+
"version": "0.8.29",
|
|
5
|
+
"description": "OpenClaw DingTalk channel plugin (community) | 钉钉 OpenClaw 社区增强版",
|
|
6
|
+
"author": "jeik",
|
|
7
7
|
"main": "index.ts",
|
|
8
8
|
"channels": [
|
|
9
9
|
"dingtalk-connector"
|
|
@@ -240,7 +240,9 @@
|
|
|
240
240
|
"type": "string"
|
|
241
241
|
},
|
|
242
242
|
"debug": {
|
|
243
|
-
"type": "boolean"
|
|
243
|
+
"type": "boolean",
|
|
244
|
+
"default": false,
|
|
245
|
+
"description": "调试日志:连接/入站、图片 mediaId、引用回填、队列计数等。日常关闭;排障时设 true 后重启 gateway。"
|
|
244
246
|
},
|
|
245
247
|
"messageImageMd": {
|
|
246
248
|
"type": "boolean",
|
|
@@ -491,7 +493,9 @@
|
|
|
491
493
|
"type": "string"
|
|
492
494
|
},
|
|
493
495
|
"debug": {
|
|
494
|
-
"type": "boolean"
|
|
496
|
+
"type": "boolean",
|
|
497
|
+
"default": false,
|
|
498
|
+
"description": "调试日志:连接/入站、图片 mediaId、引用回填、队列计数等。日常关闭;排障时设 true 后重启 gateway。"
|
|
495
499
|
},
|
|
496
500
|
"messageImageMd": {
|
|
497
501
|
"type": "boolean",
|
package/package.json
CHANGED
package/src/config/schema.ts
CHANGED
|
@@ -81,7 +81,8 @@ const DingtalkSharedConfigShape = {
|
|
|
81
81
|
asyncMode: z.boolean().optional(),
|
|
82
82
|
ackText: z.string().optional(),
|
|
83
83
|
endpoint: z.string().optional(), // DWClient gateway endpoint
|
|
84
|
-
|
|
84
|
+
/** 调试日志:连接/入站摘要、图片 mediaId 追踪、引用回填、队列计数等(默认关,生产勿开) */
|
|
85
|
+
debug: z.boolean().optional(),
|
|
85
86
|
enableMediaUpload: z.boolean().optional(),
|
|
86
87
|
systemPrompt: z.string().optional(),
|
|
87
88
|
/**
|
package/src/core/connection.ts
CHANGED
|
@@ -114,7 +114,7 @@ function printConnectionNoticeOnce(): void {
|
|
|
114
114
|
_connectionNoticePrinted = true;
|
|
115
115
|
console.log(
|
|
116
116
|
"[dingtalk-connector] ℹ️ 上游 dingtalk-stream 噪音已过滤;" +
|
|
117
|
-
"
|
|
117
|
+
"OPEN 成功即可上线,REGISTERED 尽量等待;若环境不推 REGISTERED,首条消息会确认订阅。" +
|
|
118
118
|
"正常运行不应出现 ≤30s 周期性硬重连。",
|
|
119
119
|
);
|
|
120
120
|
}
|
|
@@ -135,7 +135,9 @@ export async function monitorSingleAccount(
|
|
|
135
135
|
const log = runtime?.log;
|
|
136
136
|
|
|
137
137
|
// 创建 debug logger(仅在 debug 模式下输出 info/debug 日志)
|
|
138
|
-
|
|
138
|
+
// 配置 debug: true 时同步打开图片/引用/队列详细诊断(无需环境变量)
|
|
139
|
+
const { createLoggerFromConfig, setDingtalkDebug } = await import('../utils/logger');
|
|
140
|
+
if (account.config?.debug) setDingtalkDebug(true);
|
|
139
141
|
const logger = createLoggerFromConfig(account.config, `DingTalk:${accountId}`);
|
|
140
142
|
|
|
141
143
|
// 验证凭据是否存在
|
|
@@ -288,15 +290,18 @@ export async function monitorSingleAccount(
|
|
|
288
290
|
}
|
|
289
291
|
|
|
290
292
|
/**
|
|
291
|
-
* 等待钉钉 Stream
|
|
293
|
+
* 等待钉钉 Stream 可服务。
|
|
292
294
|
*
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
+
* - OPEN:必须成功,否则 throw
|
|
296
|
+
* - REGISTERED:尽量等待;超时则 **降级 OPEN 上线**(不 throw)
|
|
295
297
|
*
|
|
296
|
-
*
|
|
297
|
-
*
|
|
298
|
+
* 原因(生产日志):部分环境 socket 已 OPEN 且 CALLBACK 能收消息,
|
|
299
|
+
* 但长期收不到 SYSTEM/REGISTERED 或 client.registered 不被置 true。
|
|
300
|
+
* 若硬性失败会触发网关 auto-restart 风暴,而旧连接/半连接其实仍在处理消息。
|
|
298
301
|
*/
|
|
299
|
-
async function waitForStreamReady(timeoutMs = STREAM_READY_TIMEOUT_MS): Promise<
|
|
302
|
+
async function waitForStreamReady(timeoutMs = STREAM_READY_TIMEOUT_MS): Promise<{
|
|
303
|
+
registered: boolean;
|
|
304
|
+
}> {
|
|
300
305
|
const started = Date.now();
|
|
301
306
|
|
|
302
307
|
// 1) 等 WebSocket OPEN
|
|
@@ -332,12 +337,12 @@ export async function monitorSingleAccount(
|
|
|
332
337
|
}
|
|
333
338
|
}
|
|
334
339
|
|
|
335
|
-
// 2) 等服务端 REGISTERED
|
|
340
|
+
// 2) 等服务端 REGISTERED(可选;超时降级)
|
|
336
341
|
if ((client as any).registered === true) {
|
|
337
342
|
logger.info(
|
|
338
343
|
`✅ Stream 已就绪(OPEN + REGISTERED),耗时 ${Date.now() - started}ms`,
|
|
339
344
|
);
|
|
340
|
-
return;
|
|
345
|
+
return { registered: true };
|
|
341
346
|
}
|
|
342
347
|
|
|
343
348
|
const registered = await new Promise<boolean>((resolve) => {
|
|
@@ -367,7 +372,7 @@ export async function monitorSingleAccount(
|
|
|
367
372
|
return;
|
|
368
373
|
}
|
|
369
374
|
} catch {
|
|
370
|
-
// ignore
|
|
375
|
+
// ignore
|
|
371
376
|
}
|
|
372
377
|
if ((client as any).registered === true) {
|
|
373
378
|
finish(true);
|
|
@@ -387,13 +392,15 @@ export async function monitorSingleAccount(
|
|
|
387
392
|
logger.info(
|
|
388
393
|
`✅ Stream 已就绪(OPEN + REGISTERED),耗时 ${Date.now() - started}ms`,
|
|
389
394
|
);
|
|
390
|
-
return;
|
|
395
|
+
return { registered: true };
|
|
391
396
|
}
|
|
392
397
|
|
|
393
|
-
//
|
|
394
|
-
|
|
395
|
-
|
|
398
|
+
// 降级:OPEN 成功即可上线。许多环境不推 REGISTERED 或检测不到,但仍可收 CALLBACK。
|
|
399
|
+
logger.warn(
|
|
400
|
+
`⚠️ 未观察到 SYSTEM/REGISTERED(${timeoutMs}ms),socket 已 OPEN → 降级上线。` +
|
|
401
|
+
` 收到首条机器人消息后会记为「订阅 empirically 生效」。`,
|
|
396
402
|
);
|
|
403
|
+
return { registered: false };
|
|
397
404
|
}
|
|
398
405
|
|
|
399
406
|
/**
|
|
@@ -409,22 +416,20 @@ export async function monitorSingleAccount(
|
|
|
409
416
|
}
|
|
410
417
|
|
|
411
418
|
/**
|
|
412
|
-
* 单次:connect + 挂 listener + 等 OPEN
|
|
413
|
-
* 失败 throw
|
|
419
|
+
* 单次:connect + 挂 listener + 等 OPEN(REGISTERED 可选)。
|
|
420
|
+
* 仅 OPEN 失败 throw;REGISTERED 超时则降级成功。
|
|
414
421
|
*/
|
|
415
|
-
async function
|
|
422
|
+
async function connectAndWaitReady(): Promise<{ registered: boolean }> {
|
|
416
423
|
await client.connect();
|
|
417
424
|
attachSocketLifecycleListeners();
|
|
418
|
-
await waitForStreamReady(STREAM_READY_TIMEOUT_MS);
|
|
419
|
-
if ((client as any).registered !== true) {
|
|
420
|
-
// 双保险:wait 已要求 registered,此处再断言
|
|
421
|
-
throw new Error("connect 后 client.registered 仍为 false");
|
|
422
|
-
}
|
|
425
|
+
return await waitForStreamReady(STREAM_READY_TIMEOUT_MS);
|
|
423
426
|
}
|
|
424
427
|
|
|
425
428
|
/**
|
|
426
|
-
*
|
|
427
|
-
*
|
|
429
|
+
* 建立可服务连接。
|
|
430
|
+
* - 优先拿到 REGISTERED
|
|
431
|
+
* - OPEN 成功即可上线(避免环境不推 REGISTERED 时 restart 风暴)
|
|
432
|
+
* - 仅在 OPEN 都失败时多轮重试
|
|
428
433
|
*/
|
|
429
434
|
async function ensureRegisteredConnection(
|
|
430
435
|
maxAttempts = REGISTERED_CONNECT_MAX_ATTEMPTS,
|
|
@@ -432,12 +437,12 @@ export async function monitorSingleAccount(
|
|
|
432
437
|
let lastError: Error | undefined;
|
|
433
438
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
434
439
|
if (isStopped) {
|
|
435
|
-
throw new Error("连接已停止,中止
|
|
440
|
+
throw new Error("连接已停止,中止 Stream 建立");
|
|
436
441
|
}
|
|
437
442
|
try {
|
|
438
443
|
if (attempt > 1) {
|
|
439
444
|
logger.warn(
|
|
440
|
-
`🔄
|
|
445
|
+
`🔄 Stream 建立重试 ${attempt}/${maxAttempts}…`,
|
|
441
446
|
);
|
|
442
447
|
try {
|
|
443
448
|
if ((client as any).socket) {
|
|
@@ -450,27 +455,28 @@ export async function monitorSingleAccount(
|
|
|
450
455
|
await new Promise((r) => setTimeout(r, delay));
|
|
451
456
|
} else {
|
|
452
457
|
logger.info(
|
|
453
|
-
`⏳ 建立 Stream
|
|
458
|
+
`⏳ 建立 Stream(OPEN 必须;REGISTERED 尽量等待 ${STREAM_READY_TIMEOUT_MS}ms)…`,
|
|
454
459
|
);
|
|
455
460
|
}
|
|
456
461
|
|
|
457
|
-
await
|
|
458
|
-
noteSocketAlive("registered-ok");
|
|
462
|
+
const ready = await connectAndWaitReady();
|
|
463
|
+
noteSocketAlive(ready.registered ? "registered-ok" : "open-degraded");
|
|
459
464
|
connectionEstablishedTime = Date.now();
|
|
460
465
|
logger.info(
|
|
461
|
-
`✅
|
|
466
|
+
`✅ Stream 可服务(attempt ${attempt}/${maxAttempts}, ` +
|
|
467
|
+
`registered=${ready.registered || Boolean((client as any).registered)}, ` +
|
|
462
468
|
`socket=${(client as any).socket?.readyState})`,
|
|
463
469
|
);
|
|
464
470
|
return;
|
|
465
471
|
} catch (err: any) {
|
|
466
472
|
lastError = err instanceof Error ? err : new Error(String(err));
|
|
467
473
|
logger.warn(
|
|
468
|
-
`⚠️ Stream
|
|
474
|
+
`⚠️ Stream 建立失败 attempt=${attempt}/${maxAttempts}: ${lastError.message}`,
|
|
469
475
|
);
|
|
470
476
|
}
|
|
471
477
|
}
|
|
472
478
|
throw new Error(
|
|
473
|
-
`钉钉 Stream 在 ${maxAttempts}
|
|
479
|
+
`钉钉 Stream 在 ${maxAttempts} 次尝试后仍无法 OPEN。` +
|
|
474
480
|
` 最后错误: ${lastError?.message || "unknown"}`,
|
|
475
481
|
);
|
|
476
482
|
}
|
|
@@ -784,31 +790,27 @@ export async function monitorSingleAccount(
|
|
|
784
790
|
lastMessageTime = Date.now();
|
|
785
791
|
// 能收到 CALLBACK 说明链路活着——立刻刷新,打断「软超时→硬重连」误判
|
|
786
792
|
noteSocketAlive("inbound-callback");
|
|
793
|
+
// 部分环境收不到 SYSTEM/REGISTERED,但 CALLBACK 已通:记为 empirically 订阅成功
|
|
794
|
+
if ((client as any).registered !== true) {
|
|
795
|
+
(client as any).registered = true;
|
|
796
|
+
logger.info(
|
|
797
|
+
`✅ 收到机器人 CALLBACK,视为订阅已生效(此前可能未观察到 SYSTEM/REGISTERED)`,
|
|
798
|
+
);
|
|
799
|
+
}
|
|
787
800
|
|
|
788
801
|
// 收到消息时,向框架报告 lastInboundAt(用于 UI 显示 "Last inbound")
|
|
789
802
|
onStatusChange?.({ lastInboundAt: Date.now() });
|
|
790
803
|
|
|
791
804
|
const messageId = res.headers?.messageId;
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
// ===== 第一步:记录原始消息接收 =====
|
|
795
|
-
logger.info(`\n========== 收到新消息 ==========`);
|
|
796
|
-
logger.info(`时间:${timestamp}`);
|
|
797
|
-
logger.info(`MessageId: ${messageId || "N/A"}`);
|
|
798
|
-
logger.info(`Headers: ${JSON.stringify(res.headers || {})}`);
|
|
799
|
-
logger.info(`Data 长度:${res.data?.length || 0} 字符`);
|
|
800
|
-
|
|
801
|
-
// ⚠️ 不要在解析/入队前 ACK。
|
|
802
|
-
// 旧逻辑「立刻 socketCallBackResponse」:若随后去重误杀、解析失败、或入队前进程重连,
|
|
803
|
-
// 钉钉认为已送达,不再重推 → 网关 UI 永远收不到。
|
|
804
|
-
// 正确:入队成功后再 ACK;失败则不 ACK,让钉钉 ~60s 内重投。
|
|
805
|
+
|
|
806
|
+
// 入队成功后再 ACK;失败不 ACK 以便钉钉重投
|
|
805
807
|
let acked = false;
|
|
806
808
|
const ackMessage = (why: string) => {
|
|
807
809
|
if (acked || !messageId) return;
|
|
808
810
|
try {
|
|
809
811
|
(client as any).socketCallBackResponse(messageId, { success: true });
|
|
810
812
|
acked = true;
|
|
811
|
-
logger.
|
|
813
|
+
logger.debug(`ACK messageId=${messageId} (${why})`);
|
|
812
814
|
} catch (ackErr: any) {
|
|
813
815
|
logger.warn(`确认回调失败: ${ackErr?.message || ackErr}`);
|
|
814
816
|
}
|
|
@@ -817,80 +819,62 @@ export async function monitorSingleAccount(
|
|
|
817
819
|
markMessageProcessingStart();
|
|
818
820
|
|
|
819
821
|
try {
|
|
820
|
-
// 解析消息数据
|
|
821
822
|
let data;
|
|
822
823
|
try {
|
|
823
824
|
data = JSON.parse(res.data);
|
|
824
825
|
} catch (parseError: any) {
|
|
825
|
-
logger.error(
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
? res.data.substring(0, 500)
|
|
829
|
-
: res.data,
|
|
830
|
-
dataType: typeof res.data,
|
|
831
|
-
});
|
|
832
|
-
// 解析失败:不 ACK,允许钉钉重投
|
|
826
|
+
logger.error(
|
|
827
|
+
`JSON 解析失败: ${parseError instanceof Error ? parseError.message : String(parseError)}`,
|
|
828
|
+
);
|
|
833
829
|
throw new Error(
|
|
834
|
-
`Invalid JSON
|
|
835
|
-
`Error: ${parseError instanceof Error ? parseError.message : String(parseError)}. ` +
|
|
836
|
-
`Raw data (first 100 chars): ${String(res.data).substring(0, 100)}`
|
|
830
|
+
`Invalid JSON from DingTalk: ${parseError instanceof Error ? parseError.message : String(parseError)}`,
|
|
837
831
|
);
|
|
838
832
|
}
|
|
839
833
|
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
);
|
|
834
|
+
const businessMsgId = data.msgId;
|
|
835
|
+
const convType =
|
|
836
|
+
data.conversationType === "1"
|
|
837
|
+
? "DM"
|
|
838
|
+
: data.conversationType === "2"
|
|
839
|
+
? "Group"
|
|
840
|
+
: String(data.conversationType ?? "?");
|
|
841
|
+
const preview =
|
|
842
|
+
(data.text?.content || "").trim().slice(0, 80) ||
|
|
843
|
+
data.msgtype ||
|
|
844
|
+
"(no text)";
|
|
845
|
+
|
|
846
|
+
// 一行摘要(debug 模式才打 Headers/全字段)
|
|
854
847
|
logger.info(
|
|
855
|
-
|
|
848
|
+
`📩 入站 ${convType} from=${data.senderNick || data.senderStaffId || data.senderId || "?"} ` +
|
|
849
|
+
`msg=${businessMsgId || messageId || "-"} preview=${JSON.stringify(preview)}`,
|
|
856
850
|
);
|
|
851
|
+
logger.debug(`Headers: ${JSON.stringify(res.headers || {})}`);
|
|
852
|
+
logger.debug(`字段: ${Object.keys(data).join(",")}`);
|
|
853
|
+
|
|
854
|
+
// BotIdentity 每个进程每个账号只打一次(配置多机器人时用)
|
|
857
855
|
if (data.chatbotUserId || data.chatbotCorpId) {
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
856
|
+
const key = `${accountId}:${data.chatbotUserId || ""}:${data.chatbotCorpId || ""}`;
|
|
857
|
+
if (!(globalThis as any).__dingtalkBotIdentityLogged) {
|
|
858
|
+
(globalThis as any).__dingtalkBotIdentityLogged = new Set<string>();
|
|
859
|
+
}
|
|
860
|
+
const seen = (globalThis as any).__dingtalkBotIdentityLogged as Set<string>;
|
|
861
|
+
if (!seen.has(key)) {
|
|
862
|
+
seen.add(key);
|
|
863
|
+
console.log(
|
|
864
|
+
`[DingTalk:${accountId}] [BotIdentity] chatbotUserId=${data.chatbotUserId || "N/A"} chatbotCorpId=${data.chatbotCorpId || "N/A"}`,
|
|
865
|
+
);
|
|
866
|
+
}
|
|
861
867
|
}
|
|
862
868
|
|
|
863
|
-
const businessMsgId = data.msgId;
|
|
864
|
-
|
|
865
|
-
// 双层去重:协议 messageId + 业务 msgId(须在解析后一次完成)
|
|
866
869
|
if (checkAndMarkDingtalkMessage(accountId, messageId, businessMsgId)) {
|
|
867
870
|
processedCount++;
|
|
868
|
-
// 重复投递:ACK 掉避免钉钉无限重推
|
|
869
871
|
ackMessage("duplicate");
|
|
870
872
|
logger.warn(
|
|
871
|
-
`⚠️
|
|
873
|
+
`⚠️ 重复消息已跳过 protocol=${messageId || "-"} business=${businessMsgId || "-"}`,
|
|
872
874
|
);
|
|
873
|
-
logger.info(`========== 消息处理结束(重复) ==========\n`);
|
|
874
875
|
return;
|
|
875
876
|
}
|
|
876
877
|
|
|
877
|
-
let contentPreview = "N/A";
|
|
878
|
-
if (data.text?.content) {
|
|
879
|
-
contentPreview =
|
|
880
|
-
data.text.content.length > 100
|
|
881
|
-
? data.text.content.substring(0, 100) + "..."
|
|
882
|
-
: data.text.content;
|
|
883
|
-
} else if (data.content) {
|
|
884
|
-
contentPreview =
|
|
885
|
-
JSON.stringify(data.content).substring(0, 100) + "...";
|
|
886
|
-
}
|
|
887
|
-
logger.info(`消息内容预览:${contentPreview}`);
|
|
888
|
-
logger.info(`完整数据字段:${Object.keys(data).join(", ")}`);
|
|
889
|
-
logger.info(`----- 消息详情结束 -----\n`);
|
|
890
|
-
|
|
891
|
-
// ===== 第三步:入队处理 =====
|
|
892
|
-
logger.info(`🚀 开始处理消息...`);
|
|
893
|
-
|
|
894
878
|
await messageHandler({
|
|
895
879
|
accountId,
|
|
896
880
|
config: account.config,
|
|
@@ -901,29 +885,22 @@ export async function monitorSingleAccount(
|
|
|
901
885
|
cfg: clawdbotConfig,
|
|
902
886
|
});
|
|
903
887
|
|
|
904
|
-
// 入队/受理成功后再 ACK(handleDingTalkMessage 返回表示已进 session 队列)
|
|
905
888
|
ackMessage("enqueued");
|
|
906
889
|
noteSocketAlive("after-enqueue");
|
|
907
890
|
|
|
908
891
|
processedCount++;
|
|
909
|
-
logger.info(`✅
|
|
910
|
-
logger.info(`========== 消息处理结束(成功) ==========\n`);
|
|
892
|
+
logger.info(`✅ 入队完成 (${processedCount}/${receivedCount})`);
|
|
911
893
|
} catch (error: any) {
|
|
912
894
|
processedCount++;
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
logger.error
|
|
917
|
-
logger.error(`错误堆栈:\n${errorStack}`);
|
|
918
|
-
|
|
919
|
-
// 未成功入队:故意不 ACK,让钉钉重投(最多约 60s)
|
|
895
|
+
logger.error(
|
|
896
|
+
`❌ 入站处理失败 (${processedCount}/${receivedCount}): ${error?.message || "未知错误"}`,
|
|
897
|
+
);
|
|
898
|
+
if (error?.stack) logger.debug(error.stack);
|
|
920
899
|
if (!acked) {
|
|
921
900
|
logger.warn(
|
|
922
|
-
`⚠️ 未 ACK messageId=${messageId || "N/A"}
|
|
901
|
+
`⚠️ 未 ACK messageId=${messageId || "N/A"},等待钉钉重投`,
|
|
923
902
|
);
|
|
924
903
|
}
|
|
925
|
-
|
|
926
|
-
logger.info(`========== 消息处理结束(失败) ==========\n`);
|
|
927
904
|
} finally {
|
|
928
905
|
markMessageProcessingEnd();
|
|
929
906
|
}
|
|
@@ -66,7 +66,7 @@ import { createDingtalkReplyDispatcher, matchModelErrorText } from "../reply-dis
|
|
|
66
66
|
import { normalizeSlashCommand } from "../utils/session.ts";
|
|
67
67
|
import { getDingtalkRuntime } from "../runtime.ts";
|
|
68
68
|
import { dingtalkHttp } from '../utils/http-client.ts';
|
|
69
|
-
import { createLoggerFromConfig } from '../utils/index.ts';
|
|
69
|
+
import { createLoggerFromConfig, isDingtalkDebug } from '../utils/index.ts';
|
|
70
70
|
import { trackBackgroundWork } from "../utils/background-work.ts";
|
|
71
71
|
import * as fs from 'fs';
|
|
72
72
|
import * as path from 'path';
|
|
@@ -358,11 +358,13 @@ function extractQuotedMsgText(
|
|
|
358
358
|
allowConversationRecent: false,
|
|
359
359
|
}) || '';
|
|
360
360
|
if (cardText) {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
361
|
+
if (isDingtalkDebug()) {
|
|
362
|
+
console.log(
|
|
363
|
+
`[DingTalk][Quote] interactiveCard 已从 CardCache 精确回填 | ids=${ids.join(',') || '-'} len=${cardText.length}`,
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
} else if (isDingtalkDebug()) {
|
|
367
|
+
// 调试:dump 载荷确认钉钉给了哪些字段
|
|
366
368
|
const dump = JSON.stringify(repliedMsg ?? {}).slice(0, 800);
|
|
367
369
|
console.log(
|
|
368
370
|
`[DingTalk][Quote] interactiveCard 无正文且缓存未命中(未使用会话最近兜底) | conv=${conversationId || '-'} keys=${Object.keys(contentObj || {}).join(',')} repliedMsg=${dump}`,
|
|
@@ -398,26 +400,32 @@ function extractQuotedMsgText(
|
|
|
398
400
|
});
|
|
399
401
|
if (cached) {
|
|
400
402
|
bodyText = cached;
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
403
|
+
if (isDingtalkDebug()) {
|
|
404
|
+
console.log(
|
|
405
|
+
`[DingTalk][Quote] msgType=${msgType} 已从 CardCache 精确回填 | ids=${ids.join(',') || '-'} len=${cached.length}`,
|
|
406
|
+
);
|
|
407
|
+
}
|
|
404
408
|
} else if (
|
|
405
409
|
/card|interactive/i.test(String(msgType || "")) ||
|
|
406
410
|
ids.length > 0
|
|
407
411
|
) {
|
|
408
412
|
bodyText = "钉钉卡片消息。";
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
+
if (isDingtalkDebug()) {
|
|
414
|
+
console.log(
|
|
415
|
+
`[DingTalk][Quote] 未知卡片类引用 msgType=${msgType} 已简化占位 | keys=${Object.keys(contentObj || {}).join(',')}`,
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
} else if (isDingtalkDebug()) {
|
|
413
419
|
console.log(
|
|
414
420
|
`[DingTalk][Quote] 未知引用类型 msgType=${msgType} keys=${Object.keys(contentObj || {}).join(',')} dump=${JSON.stringify(repliedMsg ?? {}).slice(0, 600)}`,
|
|
415
421
|
);
|
|
416
422
|
}
|
|
417
423
|
} catch {
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
424
|
+
if (isDingtalkDebug()) {
|
|
425
|
+
console.log(
|
|
426
|
+
`[DingTalk][Quote] 未知引用类型 msgType=${msgType} keys=${Object.keys(contentObj || {}).join(',')}`,
|
|
427
|
+
);
|
|
428
|
+
}
|
|
421
429
|
}
|
|
422
430
|
}
|
|
423
431
|
break;
|
package/src/reply-dispatcher.ts
CHANGED
|
@@ -26,7 +26,7 @@ const {
|
|
|
26
26
|
logTypingFailure,
|
|
27
27
|
} = channelRuntimeModule;
|
|
28
28
|
|
|
29
|
-
import { createLoggerFromConfig } from "./utils/logger.ts";
|
|
29
|
+
import { createLoggerFromConfig, isDingtalkDebug } from "./utils/logger.ts";
|
|
30
30
|
import { CHANNEL_ID } from "./channel.ts";
|
|
31
31
|
import { resolveDingtalkAccount } from "./config/accounts.ts";
|
|
32
32
|
import { getDingtalkRuntime } from "./runtime.ts";
|
|
@@ -877,30 +877,26 @@ export function createDingtalkReplyDispatcher(params: CreateDingtalkReplyDispatc
|
|
|
877
877
|
? { type: 'user', userId: senderId }
|
|
878
878
|
: { type: 'group', openConversationId: conversationId };
|
|
879
879
|
|
|
880
|
-
//
|
|
881
|
-
console.log(
|
|
882
|
-
`[DingTalk][LocalImage] closeStreaming 开始媒体处理 | target=${JSON.stringify(target)} hasToken=${!!oapiToken} textLen=${finalText.length}`,
|
|
883
|
-
);
|
|
884
|
-
// 预览正文中是否含 
|
|
885
881
|
const mdImgCount = (finalText.match(/!\[[^\]]*\]\([^)]+\)/g) || []).length;
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
882
|
+
if (mdImgCount > 0 || isDingtalkDebug()) {
|
|
883
|
+
log.info(
|
|
884
|
+
`[DingTalk][LocalImage] closeStreaming 媒体处理 | target=${JSON.stringify(target)} hasToken=${!!oapiToken} textLen=${finalText.length} mdImgs=${mdImgCount}`,
|
|
885
|
+
);
|
|
886
|
+
}
|
|
887
|
+
|
|
890
888
|
if (oapiToken) {
|
|
891
|
-
// 处理本地图片(含 /mnt 共享盘;失败会 /tmp 重试,避免留下本地路径灰图)
|
|
892
889
|
const beforeImg = finalText;
|
|
893
890
|
finalText = await processLocalImages(finalText, oapiToken, log);
|
|
894
891
|
if (beforeImg !== finalText) {
|
|
895
|
-
|
|
896
|
-
`[DingTalk][LocalImage] closeStreaming
|
|
892
|
+
log.info(
|
|
893
|
+
`[DingTalk][LocalImage] closeStreaming 图片已处理 | ${beforeImg.length}→${finalText.length}`,
|
|
897
894
|
);
|
|
898
895
|
}
|
|
899
|
-
// 仅统计代码块外的本地 ![]——示例 JSON/参数说明里的路径不算灰图风险
|
|
900
896
|
try {
|
|
901
897
|
const { hasResidualLocalMdImagesOutsideCode } = await import("./services/media.ts");
|
|
902
898
|
if (hasResidualLocalMdImagesOutsideCode(finalText)) {
|
|
903
|
-
|
|
899
|
+
log.warn(
|
|
904
900
|
`[DingTalk][LocalImage] closeStreaming 定稿后仍有代码块外的本地 MD 图未上传,钉钉可能灰图`,
|
|
905
901
|
);
|
|
906
902
|
}
|
|
@@ -938,7 +934,6 @@ export function createDingtalkReplyDispatcher(params: CreateDingtalkReplyDispatc
|
|
|
938
934
|
);
|
|
939
935
|
|
|
940
936
|
// ✅ 处理裸露的本地文件路径(绕过 OpenClaw SDK 的 bug)
|
|
941
|
-
log.info(`[DingTalk][closeStreaming] 准备调用 processRawMediaPaths`);
|
|
942
937
|
const { processRawMediaPaths } = await import('./services/media');
|
|
943
938
|
finalText = await processRawMediaPaths(
|
|
944
939
|
finalText,
|
|
@@ -1246,13 +1241,12 @@ export function createDingtalkReplyDispatcher(params: CreateDingtalkReplyDispatc
|
|
|
1246
1241
|
// 模型主动 NO_REPLY(无错误)保持静默——不在此伪造错误文案,避免群聊刷屏。
|
|
1247
1242
|
// 此时 earlyCard 仍会由 onIdle→closeStreaming 收尾(思考完成 / 无文本),不会永久转圈。
|
|
1248
1243
|
if (!raw && !isErr) {
|
|
1249
|
-
|
|
1250
|
-
`[DingTalk][onSkip] kind=${info?.kind} reason=${info?.reason} 非错误静默(NO_REPLY/empty),跳过错误捕获`,
|
|
1251
|
-
);
|
|
1244
|
+
// 模型主动 NO_REPLY:静默,不打日志
|
|
1252
1245
|
return;
|
|
1253
1246
|
}
|
|
1247
|
+
// 失败类静默:保留一行关键 warn(避免与 closeStreaming 重复刷堆栈)
|
|
1254
1248
|
log.warn(
|
|
1255
|
-
`[DingTalk][onSkip] kind=${info?.kind} reason=${info?.reason}
|
|
1249
|
+
`[DingTalk][onSkip] 群聊静默失败 kind=${info?.kind} reason=${info?.reason} rawLen=${raw.length}`,
|
|
1256
1250
|
);
|
|
1257
1251
|
captureErrorFromPayload(payload, `onSkip:${info?.kind ?? "?"}:${info?.reason ?? "?"}`);
|
|
1258
1252
|
},
|
|
@@ -1313,6 +1307,25 @@ export function createDingtalkReplyDispatcher(params: CreateDingtalkReplyDispatc
|
|
|
1313
1307
|
const isModelErrorFinal =
|
|
1314
1308
|
Boolean((payload as any).isError) && info?.kind === "final" && Boolean(text.trim());
|
|
1315
1309
|
|
|
1310
|
+
// Gateway incomplete_turn 误报:会话/流式已有真实答案,却仍发
|
|
1311
|
+
// "Agent couldn't generate a response" isError final。若覆盖 accumulatedText
|
|
1312
|
+
// 并 closeStreaming,钉钉卡会定格错误、后续过程不再流式。
|
|
1313
|
+
// 此时忽略误报,用已有 lastAnswerText/accumulatedText 正常收尾。
|
|
1314
|
+
const existingAnswerText = (lastAnswerText.trim() || accumulatedText.trim()).trim();
|
|
1315
|
+
const isIncompleteTurnFalsePositive =
|
|
1316
|
+
isModelErrorFinal &&
|
|
1317
|
+
/Agent couldn't generate a response/i.test(text) &&
|
|
1318
|
+
existingAnswerText.length >= 20 &&
|
|
1319
|
+
!/Agent couldn't generate a response/i.test(existingAnswerText);
|
|
1320
|
+
if (isIncompleteTurnFalsePositive) {
|
|
1321
|
+
pendingErrorText = null;
|
|
1322
|
+
log.warn(
|
|
1323
|
+
`[DingTalk][deliver] 忽略 incomplete_turn 误报 isError final(卡片已有真实答案 len=${existingAnswerText.length}),不定格错误卡`,
|
|
1324
|
+
);
|
|
1325
|
+
await closeStreaming();
|
|
1326
|
+
return;
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1316
1329
|
// ✅ 上游模型异常 final:必须写入 accumulatedText,供 closeStreaming 定稿。
|
|
1317
1330
|
// 工具失败 (kind=tool) 仍不计入最终答案。
|
|
1318
1331
|
if (isModelErrorFinal) {
|