fix(dingtalk): senderStaffId-first routing + bridge response size cap
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 Stream API senderId != OAuth openId (different encodings), so primary lookup by senderId always missed OAuth-bound instances, requiring a fallback every time. Reverse the lookup order: try senderStaffId first (direct hit for OAuth binding), fall back to senderId (code binding). Also add MAX_RESPONSE_BYTES cap to httpPostJson — previously uncapped unlike the DingTalk API helpers which already had the 256KB guard. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
e905559c46
commit
5a66f85235
|
|
@ -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<void> {
|
||||
// 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) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue