@yaoyuanchao/dingtalk 1.7.8 → 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 +1 -1
- package/src/channel.ts +46 -3
- package/src/monitor.ts +56 -6
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
|
|
|
@@ -359,7 +359,17 @@ export const dingtalkPlugin = {
|
|
|
359
359
|
actions: {
|
|
360
360
|
// New SDK interface (2026.3.22+): replaces listActions
|
|
361
361
|
describeMessageTool({ cfg }: { cfg: any }) {
|
|
362
|
-
return {
|
|
362
|
+
return {
|
|
363
|
+
actions: ['send', 'sendAttachment'],
|
|
364
|
+
instructions: [
|
|
365
|
+
'DingTalk @mention syntax: wrap staffId in <at:STAFFID> markers.',
|
|
366
|
+
'Example: "<at:0164546066>你好" sends a real @mention to that user.',
|
|
367
|
+
'The <at:...> marker is stripped from displayed text automatically.',
|
|
368
|
+
'Look up staffId: use "dws contact user search --query NAME --format json".',
|
|
369
|
+
'The message sender is auto-@mentioned in group replies (no marker needed for them).',
|
|
370
|
+
'IMPORTANT: Do NOT write @Name or at:id — only <at:STAFFID> with angle brackets works.',
|
|
371
|
+
].join(' '),
|
|
372
|
+
};
|
|
363
373
|
},
|
|
364
374
|
|
|
365
375
|
// Legacy - kept for compatibility
|
|
@@ -374,7 +384,40 @@ export const dingtalkPlugin = {
|
|
|
374
384
|
async handleAction(ctx: any) {
|
|
375
385
|
const { action, params, cfg, accountId, conversationTarget } = ctx;
|
|
376
386
|
|
|
377
|
-
//
|
|
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
|
|
378
421
|
if (action !== 'sendAttachment') {
|
|
379
422
|
return null; // Let SDK handle other actions
|
|
380
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;
|
|
@@ -1673,7 +1701,9 @@ async function dispatchMessageInternal(params: {
|
|
|
1673
1701
|
// Fallback: existing buffered block dispatcher
|
|
1674
1702
|
// Per-group system prompt for fallback path
|
|
1675
1703
|
const _fallbackGroupsConfig = account?.config?.groups ?? {};
|
|
1676
|
-
const
|
|
1704
|
+
const _fallbackCustomPrompt = isGroup ? (_fallbackGroupsConfig?.[conversationId]?.systemPrompt?.trim() || undefined) : undefined;
|
|
1705
|
+
const _AT_HINT = 'To @mention someone in this group, use <at:STAFFID> in your reply (e.g. "<at:0164546066>请查看"). Look up staffId with "dws contact user search --query NAME --format json". Do NOT write @Name — only <at:STAFFID> with angle brackets triggers a real DingTalk @mention. The sender is auto-@mentioned.';
|
|
1706
|
+
const _fallbackGroupSystemPrompt = isGroup ? (_fallbackCustomPrompt ? `${_fallbackCustomPrompt}\n\n${_AT_HINT}` : _AT_HINT) : undefined;
|
|
1677
1707
|
|
|
1678
1708
|
const ctxPayload = {
|
|
1679
1709
|
Body: rawBody,
|
|
@@ -1831,7 +1861,9 @@ async function dispatchWithFullPipeline(params: {
|
|
|
1831
1861
|
// 6a. Per-group system prompt (read from account.config.groups)
|
|
1832
1862
|
const groupsConfig = account?.config?.groups ?? {};
|
|
1833
1863
|
const groupOverride = !isDm ? (groupsConfig?.[conversationId] ?? {}) : {};
|
|
1834
|
-
const
|
|
1864
|
+
const AT_MENTION_HINT = 'To @mention someone in this group, use <at:STAFFID> in your reply (e.g. "<at:0164546066>请查看"). Look up staffId with "dws contact user search --query NAME --format json". Do NOT write @Name — only <at:STAFFID> with angle brackets triggers a real DingTalk @mention. The sender is auto-@mentioned.';
|
|
1865
|
+
const customGroupPrompt = !isDm ? (groupOverride?.systemPrompt?.trim() || undefined) : undefined;
|
|
1866
|
+
const groupSystemPrompt = !isDm ? (customGroupPrompt ? `${customGroupPrompt}\n\n${AT_MENTION_HINT}` : AT_MENTION_HINT) : undefined;
|
|
1835
1867
|
|
|
1836
1868
|
const ctx = rt.channel.reply.finalizeInboundContext({
|
|
1837
1869
|
Body: body, RawBody: rawBody, CommandBody: rawBody, From: to, To: to,
|
|
@@ -1973,7 +2005,7 @@ function buildMarkdownPreviewTitle(text: string, fallback = "Jax"): string {
|
|
|
1973
2005
|
return fallback;
|
|
1974
2006
|
}
|
|
1975
2007
|
|
|
1976
|
-
async function deliverReply(target: any, text: string, log?: any): Promise<void> {
|
|
2008
|
+
export async function deliverReply(target: any, text: string, log?: any): Promise<void> {
|
|
1977
2009
|
const now = Date.now();
|
|
1978
2010
|
const chunkLimit = target.account.config.textChunkLimit ?? 2000;
|
|
1979
2011
|
const messageFormat = target.account.config.messageFormat ?? "text";
|
|
@@ -2026,8 +2058,26 @@ async function deliverReply(target: any, text: string, log?: any): Promise<void>
|
|
|
2026
2058
|
}
|
|
2027
2059
|
}
|
|
2028
2060
|
|
|
2029
|
-
//
|
|
2030
|
-
const
|
|
2061
|
+
// Parse <at:staffId> markers from agent response and collect explicit @mentions
|
|
2062
|
+
const explicitAtIds: string[] = [];
|
|
2063
|
+
const atPattern = /<at:([a-zA-Z0-9_]+)>/g;
|
|
2064
|
+
// Strip markers from all chunks and collect staffIds
|
|
2065
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
2066
|
+
let match;
|
|
2067
|
+
while ((match = atPattern.exec(chunks[i])) !== null) {
|
|
2068
|
+
if (!explicitAtIds.includes(match[1])) explicitAtIds.push(match[1]);
|
|
2069
|
+
}
|
|
2070
|
+
chunks[i] = chunks[i].replace(atPattern, '').replace(/ +/g, ' ').trim();
|
|
2071
|
+
}
|
|
2072
|
+
if (explicitAtIds.length > 0) {
|
|
2073
|
+
log?.info?.("[dingtalk] Explicit @mentions parsed: " + JSON.stringify(explicitAtIds));
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
// Build atUserIds: explicit markers + auto-@ sender in group chats
|
|
2077
|
+
const atUserIds: string[] = [...explicitAtIds];
|
|
2078
|
+
if (!target.isDm && target.senderId && !atUserIds.includes(target.senderId)) {
|
|
2079
|
+
atUserIds.push(target.senderId);
|
|
2080
|
+
}
|
|
2031
2081
|
let atApplied = false; // Only @ on the first chunk
|
|
2032
2082
|
|
|
2033
2083
|
for (const chunk of chunks) {
|
|
@@ -2041,7 +2091,7 @@ async function deliverReply(target: any, text: string, log?: any): Promise<void>
|
|
|
2041
2091
|
await throttleSend();
|
|
2042
2092
|
log?.info?.("[dingtalk] Using sessionWebhook (attempt " + attempt + "/" + maxRetries + "), format=" + messageFormat);
|
|
2043
2093
|
log?.info?.("[dingtalk] Sending text (" + chunk.length + " chars): " + chunk.substring(0, 200));
|
|
2044
|
-
const currentAt = (!atApplied && atUserIds) ? atUserIds : undefined;
|
|
2094
|
+
const currentAt = (!atApplied && atUserIds.length > 0) ? atUserIds : undefined;
|
|
2045
2095
|
let sendResult: { ok: boolean; errcode?: number; errmsg?: string; processQueryKey?: string };
|
|
2046
2096
|
if (isMarkdown) {
|
|
2047
2097
|
const markdownTitle = buildMarkdownPreviewTitle(chunk, "Jax");
|