Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions packages/channels/dingtalk/src/DingtalkAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2373,6 +2373,106 @@ describe('DingtalkChannel parsed-message logging', () => {
});
});

describe('DingtalkChannel chat records', () => {
it('includes a replied chat-record title and summary as referenced context', () => {
const channel = createChannel();
const downstream = {
data: JSON.stringify({
msgId: 'chat-record-reply-m1',
conversationType: '2',
conversationId: 'cid-chat-record',
conversationTitle: 'Channel test group',
sessionWebhook:
'https://oapi.dingtalk.com/robot/send?access_token=token',
senderNick: 'Alice',
senderStaffId: 'staff-1',
senderId: 'sender-1',
chatbotUserId: 'bot-1',
isInAtList: true,
text: {
content: '@DingTalkTest can you see this?',
isReplyMsg: true,
repliedMsg: {
msgId: 'forwarded-record-m1',
msgType: 'chatRecord',
senderId: 'sender-1',
content: {
title: 'Group chat history',
summary: 'Alice: first message\nBob: [message]',
},
},
},
}),
headers: { messageId: 'chat-record-reply-m1' },
} as unknown as DWClientDownStream;

(
channel as unknown as { onMessage(d: DWClientDownStream): void }
).onMessage(downstream);

expect(channel.handleInbound).toHaveBeenCalledWith(
expect.objectContaining({
text: 'can you see this?',
referencedText:
'[Group chat history] Alice: first message\nBob: [message]',
}),
);
expect(
vi.mocked(channel.handleInbound).mock.calls[0]![0].referencedText,
).not.toContain('[Chat record messages]');
});

it.each([
['chatRecord', false],
['records', true],
['messages', false],
])(
'expands top-level %s entries in their original order',
(entryField, stringifyEntries) => {
const channel = createChannel();
const entries = [
{ senderName: 'Alice', content: 'first message' },
{ senderNick: 'Bob', msgType: 'picture' },
{
sender: 'Carol',
msgType: 'file',
content: { fileName: 'report.pdf' },
},
{ senderId: 'dan-id', content: { text: 'last message' } },
];
const downstream = {
data: JSON.stringify({
msgId: `chat-record-${entryField}`,
conversationType: '1',
conversationId: 'cid-chat-record-dm',
sessionWebhook:
'https://oapi.dingtalk.com/robot/send?access_token=token',
senderNick: 'Alice',
senderStaffId: 'staff-1',
senderId: 'sender-1',
msgtype: 'chatRecord',
content: {
title: 'Group chat history',
summary: 'Alice: first message\nBob: [image]',
[entryField]: stringifyEntries ? JSON.stringify(entries) : entries,
},
}),
headers: { messageId: `chat-record-${entryField}` },
} as unknown as DWClientDownStream;

(
channel as unknown as { onMessage(d: DWClientDownStream): void }
).onMessage(downstream);

expect(channel.handleInbound).toHaveBeenCalledWith(
expect.objectContaining({
text: '[Group chat history] Alice: first message\nBob: [image]\n\n[Chat record messages]\nAlice: first message\nBob: [image]\nCarol: [file: report.pdf]\ndan-id: last message',
}),
);
},
);
});

describe('DingtalkChannel downstream logging', () => {
it('replaces raw SDK Buffer logging with a structured downstream summary', () => {
createChannel();
Expand Down
117 changes: 105 additions & 12 deletions packages/channels/dingtalk/src/DingtalkAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,16 +66,24 @@ interface DingTalkRichTextPart {
atName?: string;
}

interface DingTalkMessageContent {
text?: string;
richText?: DingTalkRichTextPart[];
downloadCode?: string;
fileName?: string;
recognition?: string;
title?: string;
summary?: string;
chatRecord?: unknown;
records?: unknown;
messages?: unknown;
}

interface DingTalkRepliedMsg {
msgId?: string;
msgType?: string;
senderId?: string;
content?: {
text?: string;
richText?: DingTalkRichTextPart[];
downloadCode?: string;
fileName?: string;
};
content?: DingTalkMessageContent;
}

interface DingTalkAtUser {
Expand Down Expand Up @@ -107,12 +115,86 @@ interface DingTalkMessageData {
text?: { content?: string };
msgtype?: string;
};
content?: {
richText?: DingTalkRichTextPart[];
downloadCode?: string;
fileName?: string;
recognition?: string;
};
content?: DingTalkMessageContent;
}

function nonEmptyString(value: unknown): string | undefined {
if (typeof value !== 'string') return undefined;
const trimmed = value.trim();
return trimmed || undefined;
}

function formatChatRecordEntryBody(record: Record<string, unknown>): string {
const rawContent = record['content'];
const content =
rawContent && typeof rawContent === 'object'
? (rawContent as Record<string, unknown>)
: undefined;
const body =
nonEmptyString(record['text']) ||
nonEmptyString(rawContent) ||
nonEmptyString(content?.['text']) ||
nonEmptyString(record['message']) ||
Comment thread
qqqys marked this conversation as resolved.
nonEmptyString(record['body']);
if (body) return body;

const msgType =
nonEmptyString(record['msgType']) || nonEmptyString(record['msgtype']);
switch (msgType) {
case 'picture':
return '[image]';
Comment thread
qqqys marked this conversation as resolved.
Outdated
case 'file':
return `[file: ${nonEmptyString(content?.['fileName']) || 'file'}]`;
Comment thread
qqqys marked this conversation as resolved.
Outdated
case 'audio':
return '[audio]';
case 'video':
return '[video]';
default:
return msgType ? `[${msgType}]` : '[message]';
}
}

function formatChatRecord(content?: DingTalkMessageContent): string {
const title = nonEmptyString(content?.title);
const summary = nonEmptyString(content?.summary);
let entries = content?.chatRecord ?? content?.records ?? content?.messages;

if (typeof entries === 'string') {
try {
entries = JSON.parse(entries);
} catch {
entries = undefined;
}
}

const recordLines = Array.isArray(entries)
? entries.flatMap((entry) => {
if (typeof entry === 'string') {
const body = nonEmptyString(entry);
return body ? [`Unknown: ${body}`] : [];
}
Comment thread
qqqys marked this conversation as resolved.
if (!entry || typeof entry !== 'object') return [];
const record = entry as Record<string, unknown>;
const sender =
nonEmptyString(record['senderName']) ||
nonEmptyString(record['senderNick']) ||
nonEmptyString(record['sender']) ||
nonEmptyString(record['senderId']) ||
'Unknown';
return [`${sender}: ${formatChatRecordEntryBody(record)}`];
})
: [];

const parts: string[] = [];
if (summary && summary !== '[]') {
parts.push(`[${title || 'Chat record'}] ${summary}`);
} else if (title) {
parts.push(`[${title}]`);
}
Comment thread
qqqys marked this conversation as resolved.
if (recordLines.length > 0) {
parts.push(`[Chat record messages]\n${recordLines.join('\n')}`);
}
return parts.join('\n\n');
}

/** Track seen msgIds to deduplicate retried callbacks. */
Expand Down Expand Up @@ -1431,6 +1513,10 @@ export class DingtalkChannel extends ChannelBase {
if (summary) return summary;
}

if (msgType === 'chatRecord') {
Comment thread
qqqys marked this conversation as resolved.
return formatChatRecord(content);
}
Comment thread
qqqys marked this conversation as resolved.
Outdated

// Media type placeholders
switch (msgType) {
case 'picture':
Expand Down Expand Up @@ -1521,6 +1607,13 @@ export class DingtalkChannel extends ChannelBase {
};
}

if (msgtype === 'chatRecord') {
Comment thread
qqqys marked this conversation as resolved.
return {
text: formatChatRecord(data.content) || '(chat record)',
Comment thread
qqqys marked this conversation as resolved.
Outdated
Comment thread
qqqys marked this conversation as resolved.
Outdated
downloadCodes: [],
Comment thread
qqqys marked this conversation as resolved.
};
}

// Default: text message
return { text: data.text?.content?.trim() || '', downloadCodes: [] };
}
Expand Down
Loading