@yaoyuanchao/dingtalk 1.7.9 → 1.7.10

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yaoyuanchao/dingtalk",
3
- "version": "1.7.9",
3
+ "version": "1.7.10",
4
4
  "type": "module",
5
5
  "description": "DingTalk channel plugin for ClawdBot/OpenClaw with Stream Mode support",
6
6
  "license": "MIT",
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
- // Only handle sendAttachment action
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;
@@ -1977,7 +2005,7 @@ function buildMarkdownPreviewTitle(text: string, fallback = "Jax"): string {
1977
2005
  return fallback;
1978
2006
  }
1979
2007
 
1980
- async function deliverReply(target: any, text: string, log?: any): Promise<void> {
2008
+ export async function deliverReply(target: any, text: string, log?: any): Promise<void> {
1981
2009
  const now = Date.now();
1982
2010
  const chunkLimit = target.account.config.textChunkLimit ?? 2000;
1983
2011
  const messageFormat = target.account.config.messageFormat ?? "text";