@yaoyuanchao/dingtalk 1.7.9 → 1.7.11
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/package.json +1 -1
- package/src/channel.ts +35 -2
- package/src/monitor.ts +53 -4
package/package.json
CHANGED
package/src/channel.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getDingTalkRuntime } from './runtime.js';
|
|
2
2
|
import { resolveDingTalkAccount, listDingTalkAccountIds, resolveDefaultDingTalkAccountId } from './accounts.js';
|
|
3
|
-
import { startDingTalkMonitor } from './monitor.js';
|
|
3
|
+
import { startDingTalkMonitor, getCachedReplyTarget, deliverReply } from './monitor.js';
|
|
4
4
|
import { sendDingTalkRestMessage, uploadMediaFile, sendFileMessage, textToMarkdownFile } from './api.js';
|
|
5
5
|
import { probeDingTalk } from './probe.js';
|
|
6
6
|
|
|
@@ -384,7 +384,40 @@ export const dingtalkPlugin = {
|
|
|
384
384
|
async handleAction(ctx: any) {
|
|
385
385
|
const { action, params, cfg, accountId, conversationTarget } = ctx;
|
|
386
386
|
|
|
387
|
-
//
|
|
387
|
+
// Handle 'send' action: route through deliverReply for sessionWebhook persona
|
|
388
|
+
if (action === 'send') {
|
|
389
|
+
const text = params?.text || params?.message;
|
|
390
|
+
if (!text) return null;
|
|
391
|
+
|
|
392
|
+
let target = params?.target || params?.to || conversationTarget;
|
|
393
|
+
if (!target) return null;
|
|
394
|
+
|
|
395
|
+
const { type, id } = parseOutboundTo(target);
|
|
396
|
+
const cacheKey = (type === 'dm' ? 'dm:' : 'group:') + id;
|
|
397
|
+
const cached = getCachedReplyTarget(cacheKey);
|
|
398
|
+
|
|
399
|
+
// Build a replyTarget — use cached sessionWebhook if available
|
|
400
|
+
const account = resolveDingTalkAccount({ cfg, accountId });
|
|
401
|
+
const replyTarget = cached
|
|
402
|
+
? { ...cached, account } // refresh account credentials
|
|
403
|
+
: {
|
|
404
|
+
sessionWebhook: undefined,
|
|
405
|
+
sessionWebhookExpiry: 0,
|
|
406
|
+
conversationId: type === 'group' ? id : '',
|
|
407
|
+
senderId: type === 'dm' ? id : '',
|
|
408
|
+
isDm: type === 'dm',
|
|
409
|
+
account,
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
await deliverReply(replyTarget, text);
|
|
413
|
+
return {
|
|
414
|
+
ok: true,
|
|
415
|
+
channel: 'dingtalk',
|
|
416
|
+
content: [{ type: 'text', text: JSON.stringify({ ok: true, sent: true }) }],
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Only handle sendAttachment action below
|
|
388
421
|
if (action !== 'sendAttachment') {
|
|
389
422
|
return null; // Let SDK handle other actions
|
|
390
423
|
}
|
package/src/monitor.ts
CHANGED
|
@@ -249,6 +249,30 @@ interface BufferedMessage {
|
|
|
249
249
|
const messageBuffer = new Map<string, BufferedMessage>();
|
|
250
250
|
const AGGREGATION_DELAY_MS = 2000; // 2 seconds - balance between UX and catching split messages
|
|
251
251
|
|
|
252
|
+
// ============================================================================
|
|
253
|
+
// Reply Target Cache — remember the latest replyTarget (with sessionWebhook)
|
|
254
|
+
// per conversation so that proactive/cron sends can use the persona-preserving
|
|
255
|
+
// sessionWebhook path instead of falling back to robot-identity REST API.
|
|
256
|
+
// ============================================================================
|
|
257
|
+
|
|
258
|
+
const replyTargetCache = new Map<string, any>();
|
|
259
|
+
const REPLY_TARGET_CACHE_TTL_MS = 3 * 60 * 60 * 1000; // 3 hours
|
|
260
|
+
|
|
261
|
+
function buildReplyTargetCacheKey(isDm: boolean, conversationId: string, senderId: string): string {
|
|
262
|
+
return isDm ? `dm:${senderId}` : `group:${conversationId}`;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Look up the most recent replyTarget for a conversation (for outbound/cron sends). */
|
|
266
|
+
export function getCachedReplyTarget(cacheKey: string): any | undefined {
|
|
267
|
+
const entry = replyTargetCache.get(cacheKey);
|
|
268
|
+
if (!entry) return undefined;
|
|
269
|
+
if (Date.now() - entry._cachedAt > REPLY_TARGET_CACHE_TTL_MS) {
|
|
270
|
+
replyTargetCache.delete(cacheKey);
|
|
271
|
+
return undefined;
|
|
272
|
+
}
|
|
273
|
+
return entry;
|
|
274
|
+
}
|
|
275
|
+
|
|
252
276
|
// ============================================================================
|
|
253
277
|
// Per-Session Message Queue - serializes dispatch to prevent concurrent
|
|
254
278
|
// processing of messages in the same conversation. Uses Promise chaining:
|
|
@@ -1358,6 +1382,10 @@ async function processInboundMessage(
|
|
|
1358
1382
|
account,
|
|
1359
1383
|
};
|
|
1360
1384
|
|
|
1385
|
+
// Cache replyTarget so proactive/cron sends can use sessionWebhook
|
|
1386
|
+
const rtCacheKey = buildReplyTargetCacheKey(isDm, conversationId, senderId);
|
|
1387
|
+
replyTargetCache.set(rtCacheKey, { ...replyTarget, _cachedAt: Date.now() });
|
|
1388
|
+
|
|
1361
1389
|
// Check if message aggregation is enabled
|
|
1362
1390
|
const aggregationEnabled = account.config.messageAggregation !== false;
|
|
1363
1391
|
const aggregationDelayMs = account.config.messageAggregationDelayMs ?? AGGREGATION_DELAY_MS;
|
|
@@ -1465,6 +1493,22 @@ async function flushMessageBuffer(bufferKey: string): Promise<void> {
|
|
|
1465
1493
|
});
|
|
1466
1494
|
}
|
|
1467
1495
|
|
|
1496
|
+
/**
|
|
1497
|
+
* Resolve queue lane suffix based on message content.
|
|
1498
|
+
* /btw and abort/status commands get their own lanes so they can run
|
|
1499
|
+
* in parallel with the main message queue (matching Feishu/Telegram behaviour).
|
|
1500
|
+
*/
|
|
1501
|
+
function resolveQueueLane(text: string): string {
|
|
1502
|
+
const t = text.trim();
|
|
1503
|
+
// /btw side question — separate lane
|
|
1504
|
+
if (/^\/btw(\s|:|$)/i.test(t)) return ':btw';
|
|
1505
|
+
// abort / stop — control lane
|
|
1506
|
+
if (/^\/stop(\s|$)/i.test(t) || /^(stop|esc|abort|cancel|exit|halt|interrupt)$/i.test(t)) return ':control';
|
|
1507
|
+
// status commands — control lane
|
|
1508
|
+
if (/^\/(help|commands|tools|status|tasks|context)(\s|$)/i.test(t)) return ':control';
|
|
1509
|
+
return '';
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1468
1512
|
/**
|
|
1469
1513
|
* Dispatch a message to the agent (after aggregation or immediately).
|
|
1470
1514
|
* Enqueues into per-session queue to prevent concurrent processing.
|
|
@@ -1485,16 +1529,21 @@ async function dispatchMessage(params: {
|
|
|
1485
1529
|
const { ctx, conversationId } = params;
|
|
1486
1530
|
const { account, log } = ctx;
|
|
1487
1531
|
|
|
1488
|
-
const
|
|
1532
|
+
const lane = resolveQueueLane(params.rawBody);
|
|
1533
|
+
const mainQueueKey = `${account.accountId}:${conversationId}`;
|
|
1534
|
+
const queueKey = `${mainQueueKey}${lane}`;
|
|
1489
1535
|
// Check both the explicit queue AND recent delivery activity.
|
|
1490
1536
|
// The SDK's dispatchReplyFromConfig may resolve before the agent's full turn
|
|
1491
1537
|
// completes (followup turns run in background), clearing the queue entry
|
|
1492
1538
|
// while deliveries are still happening.
|
|
1493
|
-
|
|
1539
|
+
// btw/control lanes check against their OWN key (not the main queue) —
|
|
1540
|
+
// they are allowed to run in parallel with the main queue.
|
|
1541
|
+
const isQueueBusy = sessionQueues.has(queueKey) || (lane === '' && hasActiveDelivery(mainQueueKey));
|
|
1494
1542
|
|
|
1495
1543
|
// If queue is busy, add emotion reaction on user's message to indicate queued
|
|
1544
|
+
// (skip for btw/control lanes — they bypass the main queue by design)
|
|
1496
1545
|
let queueAckCleanup: (() => Promise<void>) | null = null;
|
|
1497
|
-
if (isQueueBusy) {
|
|
1546
|
+
if (isQueueBusy && lane === '') {
|
|
1498
1547
|
log?.info?.("[dingtalk] Queue busy for " + queueKey + ", adding queue reaction");
|
|
1499
1548
|
try {
|
|
1500
1549
|
if (account.clientId && account.clientSecret && params.msg.msgId && conversationId) {
|
|
@@ -1977,7 +2026,7 @@ function buildMarkdownPreviewTitle(text: string, fallback = "Jax"): string {
|
|
|
1977
2026
|
return fallback;
|
|
1978
2027
|
}
|
|
1979
2028
|
|
|
1980
|
-
async function deliverReply(target: any, text: string, log?: any): Promise<void> {
|
|
2029
|
+
export async function deliverReply(target: any, text: string, log?: any): Promise<void> {
|
|
1981
2030
|
const now = Date.now();
|
|
1982
2031
|
const chunkLimit = target.account.config.textChunkLimit ?? 2000;
|
|
1983
2032
|
const messageFormat = target.account.config.messageFormat ?? "text";
|