diff --git a/packages/services/agent-service/src/infrastructure/dingtalk/dingtalk-router.service.ts b/packages/services/agent-service/src/infrastructure/dingtalk/dingtalk-router.service.ts index 7ce8a93..d23b680 100644 --- a/packages/services/agent-service/src/infrastructure/dingtalk/dingtalk-router.service.ts +++ b/packages/services/agent-service/src/infrastructure/dingtalk/dingtalk-router.service.ts @@ -27,6 +27,8 @@ * - Per-user queue: promise chain guaranteed to always resolve (no dead-tail bug) * - sessionWebhookExpiredTime unit auto-detected (seconds or ms) * - DingTalk API response capped at 256 KB (prevents memory spike on bad response) + * - Bridge (OpenClaw) response also capped at 256 KB + * - Dual routing: senderStaffId (OAuth binding) + senderId (code binding) both handled * - Periodic cleanup for all in-memory maps (5 min interval) */ @@ -572,15 +574,26 @@ export class DingTalkRouterService implements OnModuleInit, OnModuleDestroy { // ── Message routing ──────────────────────────────────────────────────────── private async routeToAgent(userId: string, text: string, msg: BotMsg): Promise { - // Primary lookup by senderId (openId). If not found, also try senderStaffId - // as a fallback in case DingTalk delivers the message with staffId in senderId. - let instance = await this.instanceRepo.findByDingTalkUserId(userId); - if (!instance && msg.senderStaffId && msg.senderStaffId !== userId) { - this.logger.log(`Primary lookup (senderId=${userId}) not found, trying senderStaffId=${msg.senderStaffId}`); - instance = await this.instanceRepo.findByDingTalkUserId(msg.senderStaffId); + // Two binding paths store different DingTalk ID types: + // OAuth binding → stores staffId (resolved via unionId→userId at auth time) + // Code binding → stores senderId ($:LWCP_v1:$... format from bot message) + // + // DingTalk's Stream API senderId ($:LWCP_v1:$...) is NOT the same as the OAuth + // openId returned by /v1.0/contact/users/me. They are different ID encodings. + // Therefore we try senderStaffId first (hits OAuth-bound instances immediately), + // then fall back to senderId (hits code-bound instances). + const staffId = msg.senderStaffId?.trim(); + let instance = staffId ? await this.instanceRepo.findByDingTalkUserId(staffId) : null; + if (!instance) { + // Code-bound instances store senderId directly; also catches any future cases + // where DingTalk aligns senderId with the stored identifier. + instance = await this.instanceRepo.findByDingTalkUserId(userId); + if (instance) { + this.logger.log(`Routed via senderId=${userId} (senderStaffId=${staffId ?? 'none'} not matched)`); + } } if (!instance) { - this.logger.warn(`No binding found for senderId=${userId} or senderStaffId=${msg.senderStaffId ?? 'none'}`); + this.logger.warn(`No binding found for senderStaffId=${staffId ?? 'none'} or senderId=${userId}`); this.reply( msg, '你还没有绑定小龙虾。\n\n请在 IT0 App 中创建一只小龙虾,然后点击「绑定钉钉」获取验证码。', @@ -841,8 +854,16 @@ export class DingTalkRouterService implements OnModuleInit, OnModuleDestroy { }, (res) => { let data = ''; - res.on('data', (c: Buffer) => (data += c.toString())); + let totalBytes = 0; + res.on('data', (c: Buffer) => { + totalBytes += c.length; + if (totalBytes <= MAX_RESPONSE_BYTES) data += c.toString(); + }); res.on('end', () => { + if (totalBytes > MAX_RESPONSE_BYTES) { + reject(new Error(`Bridge response too large (${totalBytes} bytes)`)); + return; + } try { const json = JSON.parse(data); if (res.statusCode && res.statusCode >= 400) {