@adongguo/dingtalk 0.1.3
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/LICENSE +21 -0
- package/README.md +247 -0
- package/clawdbot.plugin.json +9 -0
- package/index.ts +86 -0
- package/openclaw.plugin.json +9 -0
- package/package.json +60 -0
- package/src/accounts.ts +49 -0
- package/src/ai-card.ts +341 -0
- package/src/bot.ts +403 -0
- package/src/channel.ts +220 -0
- package/src/client.ts +49 -0
- package/src/config-schema.ts +119 -0
- package/src/directory.ts +90 -0
- package/src/gateway-stream.ts +159 -0
- package/src/media.ts +608 -0
- package/src/monitor.ts +127 -0
- package/src/onboarding.ts +355 -0
- package/src/outbound.ts +46 -0
- package/src/policy.ts +92 -0
- package/src/probe.ts +41 -0
- package/src/reactions.ts +64 -0
- package/src/reply-dispatcher.ts +167 -0
- package/src/runtime.ts +14 -0
- package/src/send.ts +314 -0
- package/src/session.ts +144 -0
- package/src/streaming-handler.ts +298 -0
- package/src/targets.ts +56 -0
- package/src/types.ts +198 -0
- package/src/typing.ts +36 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Streaming Message Handler for DingTalk
|
|
3
|
+
*
|
|
4
|
+
* Integrates AI Card streaming, session management, Gateway SSE,
|
|
5
|
+
* and image post-processing for enhanced message handling.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { DWClient } from "dingtalk-stream";
|
|
9
|
+
import type { DingTalkConfig, DingTalkIncomingMessage } from "./types.js";
|
|
10
|
+
import { createAICard, streamAICard, finishAICard, failAICard } from "./ai-card.js";
|
|
11
|
+
import { isNewSessionCommand, getSessionKey, DEFAULT_SESSION_TIMEOUT } from "./session.js";
|
|
12
|
+
import { streamFromGateway } from "./gateway-stream.js";
|
|
13
|
+
import { buildMediaSystemPrompt, processLocalImages, getOapiAccessToken } from "./media.js";
|
|
14
|
+
import { sendDingTalkMessage, sendDingTalkTextMessage } from "./send.js";
|
|
15
|
+
|
|
16
|
+
// ============ Types ============
|
|
17
|
+
|
|
18
|
+
interface Logger {
|
|
19
|
+
info?: (msg: string) => void;
|
|
20
|
+
warn?: (msg: string) => void;
|
|
21
|
+
error?: (msg: string) => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface StreamingHandlerParams {
|
|
25
|
+
config: DingTalkConfig;
|
|
26
|
+
data: DingTalkIncomingMessage;
|
|
27
|
+
sessionWebhook: string;
|
|
28
|
+
client?: DWClient;
|
|
29
|
+
log?: Logger;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface ExtractedContent {
|
|
33
|
+
text: string;
|
|
34
|
+
messageType: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ============ Message Content Extraction ============
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Extract text content from incoming message.
|
|
41
|
+
*/
|
|
42
|
+
function extractMessageContent(data: DingTalkIncomingMessage): ExtractedContent {
|
|
43
|
+
const msgtype = data.msgtype || "text";
|
|
44
|
+
|
|
45
|
+
switch (msgtype) {
|
|
46
|
+
case "text":
|
|
47
|
+
return { text: data.text?.content?.trim() || "", messageType: "text" };
|
|
48
|
+
case "richText": {
|
|
49
|
+
// Parse richText if available
|
|
50
|
+
if (data.content) {
|
|
51
|
+
try {
|
|
52
|
+
const parsed = JSON.parse(data.content);
|
|
53
|
+
const parts = extractRichTextParts(parsed);
|
|
54
|
+
return { text: parts || "[富文本消息]", messageType: "richText" };
|
|
55
|
+
} catch {
|
|
56
|
+
return { text: data.content || "[富文本消息]", messageType: "richText" };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return { text: "[富文本消息]", messageType: "richText" };
|
|
60
|
+
}
|
|
61
|
+
case "picture":
|
|
62
|
+
case "image":
|
|
63
|
+
return { text: "[图片]", messageType: "picture" };
|
|
64
|
+
case "voice":
|
|
65
|
+
return { text: "[语音消息]", messageType: "voice" };
|
|
66
|
+
case "file":
|
|
67
|
+
return { text: "[文件]", messageType: "file" };
|
|
68
|
+
default:
|
|
69
|
+
return { text: data.text?.content?.trim() || `[${msgtype}消息]`, messageType: msgtype };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Extract text from richText structure.
|
|
75
|
+
*/
|
|
76
|
+
function extractRichTextParts(richText: unknown): string {
|
|
77
|
+
if (!richText || typeof richText !== "object") return "";
|
|
78
|
+
const parts: string[] = [];
|
|
79
|
+
|
|
80
|
+
function traverse(node: unknown): void {
|
|
81
|
+
if (!node || typeof node !== "object") return;
|
|
82
|
+
if (Array.isArray(node)) {
|
|
83
|
+
for (const item of node) traverse(item);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const obj = node as Record<string, unknown>;
|
|
87
|
+
if (obj.text && typeof obj.text === "string") parts.push(obj.text);
|
|
88
|
+
if (obj.content) traverse(obj.content);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
traverse(richText);
|
|
92
|
+
return parts.join("").trim();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ============ Main Streaming Handler ============
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Handle DingTalk message with AI Card streaming.
|
|
99
|
+
*
|
|
100
|
+
* Flow:
|
|
101
|
+
* 1. Parse incoming message
|
|
102
|
+
* 2. Check for new session commands
|
|
103
|
+
* 3. Get/create session key
|
|
104
|
+
* 4. Build system prompts (including media prompt)
|
|
105
|
+
* 5. Create AI Card for streaming
|
|
106
|
+
* 6. Stream from Gateway and update AI Card
|
|
107
|
+
* 7. Post-process images and finalize
|
|
108
|
+
* 8. Fall back to regular message if AI Card fails
|
|
109
|
+
*/
|
|
110
|
+
export async function handleDingTalkStreamingMessage(params: StreamingHandlerParams): Promise<void> {
|
|
111
|
+
const { config, data, sessionWebhook, client, log } = params;
|
|
112
|
+
|
|
113
|
+
// Extract message content
|
|
114
|
+
const content = extractMessageContent(data);
|
|
115
|
+
if (!content.text) {
|
|
116
|
+
log?.info?.(`[DingTalk][Streaming] Empty message, skipping`);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const isDirect = data.conversationType === "1";
|
|
121
|
+
const senderId = data.senderStaffId || data.conversationId;
|
|
122
|
+
const senderName = data.senderNick || "Unknown";
|
|
123
|
+
|
|
124
|
+
log?.info?.(`[DingTalk][Streaming] Message from ${senderName}: "${content.text.slice(0, 50)}..."`);
|
|
125
|
+
|
|
126
|
+
// ===== Session Management =====
|
|
127
|
+
const sessionTimeout = config.sessionTimeout ?? DEFAULT_SESSION_TIMEOUT;
|
|
128
|
+
const forceNewSession = isNewSessionCommand(content.text);
|
|
129
|
+
|
|
130
|
+
// Handle new session command
|
|
131
|
+
if (forceNewSession) {
|
|
132
|
+
const { sessionKey } = getSessionKey(senderId, true, sessionTimeout, log);
|
|
133
|
+
await sendDingTalkMessage({
|
|
134
|
+
sessionWebhook,
|
|
135
|
+
text: "✨ 已开启新会话,之前的对话已清空。",
|
|
136
|
+
useMarkdown: false,
|
|
137
|
+
atUserId: !isDirect ? senderId : undefined,
|
|
138
|
+
client,
|
|
139
|
+
});
|
|
140
|
+
log?.info?.(`[DingTalk][Streaming] New session requested: ${senderId}, key=${sessionKey}`);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Get or create session
|
|
145
|
+
const { sessionKey, isNew } = getSessionKey(senderId, false, sessionTimeout, log);
|
|
146
|
+
log?.info?.(`[DingTalk][Session] key=${sessionKey}, isNew=${isNew}`);
|
|
147
|
+
|
|
148
|
+
// ===== Build System Prompts =====
|
|
149
|
+
const systemPrompts: string[] = [];
|
|
150
|
+
let oapiToken: string | null = null;
|
|
151
|
+
|
|
152
|
+
// Media upload prompt
|
|
153
|
+
if (config.enableMediaUpload !== false) {
|
|
154
|
+
systemPrompts.push(buildMediaSystemPrompt());
|
|
155
|
+
oapiToken = await getOapiAccessToken(config, client);
|
|
156
|
+
log?.info?.(`[DingTalk][Media] oapiToken: ${oapiToken ? "obtained" : "failed"}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Custom system prompt
|
|
160
|
+
if (config.systemPrompt) {
|
|
161
|
+
systemPrompts.push(config.systemPrompt);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ===== Gateway Auth =====
|
|
165
|
+
const gatewayAuth = config.gatewayToken || config.gatewayPassword || "";
|
|
166
|
+
|
|
167
|
+
// ===== AI Card Mode =====
|
|
168
|
+
const aiCardEnabled = config.aiCardMode !== "disabled";
|
|
169
|
+
|
|
170
|
+
if (aiCardEnabled) {
|
|
171
|
+
// Try to create AI Card
|
|
172
|
+
const card = await createAICard(
|
|
173
|
+
config,
|
|
174
|
+
{
|
|
175
|
+
conversationType: data.conversationType,
|
|
176
|
+
conversationId: data.conversationId,
|
|
177
|
+
senderStaffId: data.senderStaffId,
|
|
178
|
+
senderId: senderId,
|
|
179
|
+
},
|
|
180
|
+
log,
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
if (card) {
|
|
184
|
+
// ===== AI Card Streaming Mode =====
|
|
185
|
+
log?.info?.(`[DingTalk][Streaming] AI Card created: ${card.cardInstanceId}`);
|
|
186
|
+
|
|
187
|
+
let accumulated = "";
|
|
188
|
+
let lastUpdateTime = 0;
|
|
189
|
+
const updateInterval = 300; // Min update interval ms
|
|
190
|
+
let chunkCount = 0;
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
log?.info?.(`[DingTalk][Streaming] Starting Gateway stream...`);
|
|
194
|
+
|
|
195
|
+
for await (const chunk of streamFromGateway({
|
|
196
|
+
userContent: content.text,
|
|
197
|
+
systemPrompts,
|
|
198
|
+
sessionKey,
|
|
199
|
+
gatewayAuth,
|
|
200
|
+
gatewayPort: config.gatewayPort,
|
|
201
|
+
log,
|
|
202
|
+
})) {
|
|
203
|
+
accumulated += chunk;
|
|
204
|
+
chunkCount++;
|
|
205
|
+
|
|
206
|
+
if (chunkCount <= 3) {
|
|
207
|
+
log?.info?.(
|
|
208
|
+
`[DingTalk][Streaming] Chunk #${chunkCount}: "${chunk.slice(0, 50)}..." (total=${accumulated.length})`,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Throttle updates
|
|
213
|
+
const now = Date.now();
|
|
214
|
+
if (now - lastUpdateTime >= updateInterval) {
|
|
215
|
+
await streamAICard(card, accumulated, false, log);
|
|
216
|
+
lastUpdateTime = now;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
log?.info?.(`[DingTalk][Streaming] Stream complete: ${chunkCount} chunks, ${accumulated.length} chars`);
|
|
221
|
+
|
|
222
|
+
// Post-process: upload local images
|
|
223
|
+
log?.info?.(
|
|
224
|
+
`[DingTalk][Media] Post-processing, oapiToken=${oapiToken ? "yes" : "no"}, preview="${accumulated.slice(0, 200)}..."`,
|
|
225
|
+
);
|
|
226
|
+
accumulated = await processLocalImages(accumulated, oapiToken, log);
|
|
227
|
+
|
|
228
|
+
// Finalize AI Card
|
|
229
|
+
await finishAICard(card, accumulated, log);
|
|
230
|
+
log?.info?.(`[DingTalk][Streaming] AI Card finished, ${accumulated.length} chars`);
|
|
231
|
+
} catch (err: unknown) {
|
|
232
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
233
|
+
log?.error?.(`[DingTalk][Streaming] Gateway error: ${errMsg}`);
|
|
234
|
+
|
|
235
|
+
// Try to show error in card
|
|
236
|
+
accumulated += `\n\n⚠️ 响应中断: ${errMsg}`;
|
|
237
|
+
try {
|
|
238
|
+
await finishAICard(card, accumulated, log);
|
|
239
|
+
} catch (finishErr: unknown) {
|
|
240
|
+
const finishErrMsg = finishErr instanceof Error ? finishErr.message : String(finishErr);
|
|
241
|
+
log?.error?.(`[DingTalk][Streaming] Failed to finish card with error: ${finishErrMsg}`);
|
|
242
|
+
await failAICard(card, errMsg, log);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
log?.warn?.(`[DingTalk][Streaming] AI Card creation failed, falling back to regular message`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ===== Fallback: Regular Message Mode =====
|
|
253
|
+
let fullResponse = "";
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
for await (const chunk of streamFromGateway({
|
|
257
|
+
userContent: content.text,
|
|
258
|
+
systemPrompts,
|
|
259
|
+
sessionKey,
|
|
260
|
+
gatewayAuth,
|
|
261
|
+
gatewayPort: config.gatewayPort,
|
|
262
|
+
log,
|
|
263
|
+
})) {
|
|
264
|
+
fullResponse += chunk;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Post-process images
|
|
268
|
+
fullResponse = await processLocalImages(fullResponse, oapiToken, log);
|
|
269
|
+
|
|
270
|
+
await sendDingTalkMessage({
|
|
271
|
+
sessionWebhook,
|
|
272
|
+
text: fullResponse || "(无响应)",
|
|
273
|
+
useMarkdown: true,
|
|
274
|
+
atUserId: !isDirect ? senderId : undefined,
|
|
275
|
+
client,
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
log?.info?.(`[DingTalk][Streaming] Regular message sent, ${fullResponse.length} chars`);
|
|
279
|
+
} catch (err: unknown) {
|
|
280
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
281
|
+
log?.error?.(`[DingTalk][Streaming] Gateway error: ${errMsg}`);
|
|
282
|
+
|
|
283
|
+
await sendDingTalkTextMessage({
|
|
284
|
+
sessionWebhook,
|
|
285
|
+
text: `抱歉,处理请求时出错: ${errMsg}`,
|
|
286
|
+
atUserId: !isDirect ? senderId : undefined,
|
|
287
|
+
client,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Check if streaming mode should be used based on config.
|
|
294
|
+
*/
|
|
295
|
+
export function shouldUseStreamingMode(config: DingTalkConfig): boolean {
|
|
296
|
+
// Streaming mode requires Gateway integration
|
|
297
|
+
return config.aiCardMode !== "disabled" && (!!config.gatewayToken || !!config.gatewayPassword);
|
|
298
|
+
}
|
package/src/targets.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { DingTalkIdType } from "./types.js";
|
|
2
|
+
|
|
3
|
+
// DingTalk uses different ID formats
|
|
4
|
+
const CONVERSATION_ID_PREFIX = "cid";
|
|
5
|
+
|
|
6
|
+
export function detectIdType(id: string): DingTalkIdType | null {
|
|
7
|
+
const trimmed = id.trim();
|
|
8
|
+
if (trimmed.startsWith(CONVERSATION_ID_PREFIX)) return "chatId";
|
|
9
|
+
// DingTalk staffId is typically a numeric string
|
|
10
|
+
if (/^\d+$/.test(trimmed)) return "staffId";
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function normalizeDingTalkTarget(raw: string): string | null {
|
|
15
|
+
const trimmed = raw.trim();
|
|
16
|
+
if (!trimmed) return null;
|
|
17
|
+
|
|
18
|
+
const lowered = trimmed.toLowerCase();
|
|
19
|
+
if (lowered.startsWith("chat:")) {
|
|
20
|
+
return trimmed.slice("chat:".length).trim() || null;
|
|
21
|
+
}
|
|
22
|
+
if (lowered.startsWith("user:")) {
|
|
23
|
+
return trimmed.slice("user:".length).trim() || null;
|
|
24
|
+
}
|
|
25
|
+
if (lowered.startsWith("staff:")) {
|
|
26
|
+
return trimmed.slice("staff:".length).trim() || null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return trimmed;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function formatDingTalkTarget(id: string, type?: DingTalkIdType): string {
|
|
33
|
+
const trimmed = id.trim();
|
|
34
|
+
if (type === "chatId" || trimmed.startsWith(CONVERSATION_ID_PREFIX)) {
|
|
35
|
+
return `chat:${trimmed}`;
|
|
36
|
+
}
|
|
37
|
+
if (type === "staffId") {
|
|
38
|
+
return `user:${trimmed}`;
|
|
39
|
+
}
|
|
40
|
+
return trimmed;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function looksLikeDingTalkId(raw: string): boolean {
|
|
44
|
+
const trimmed = raw.trim();
|
|
45
|
+
if (!trimmed) return false;
|
|
46
|
+
if (/^(chat|user|staff):/i.test(trimmed)) return true;
|
|
47
|
+
if (trimmed.startsWith(CONVERSATION_ID_PREFIX)) return true;
|
|
48
|
+
// DingTalk IDs are typically alphanumeric
|
|
49
|
+
if (/^[a-zA-Z0-9_-]+$/.test(trimmed)) return true;
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Not used in DingTalk (sessionWebhook-based), but kept for compatibility
|
|
54
|
+
export function resolveReceiveIdType(_id: string): "chatId" | "staffId" {
|
|
55
|
+
return "chatId";
|
|
56
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import type { DingTalkConfigSchema, DingTalkGroupSchema, z } from "./config-schema.js";
|
|
2
|
+
|
|
3
|
+
export type DingTalkConfig = z.infer<typeof DingTalkConfigSchema>;
|
|
4
|
+
export type DingTalkGroupConfig = z.infer<typeof DingTalkGroupSchema>;
|
|
5
|
+
|
|
6
|
+
export type DingTalkConnectionMode = "stream" | "webhook";
|
|
7
|
+
|
|
8
|
+
export type ResolvedDingTalkAccount = {
|
|
9
|
+
accountId: string;
|
|
10
|
+
enabled: boolean;
|
|
11
|
+
configured: boolean;
|
|
12
|
+
appKey?: string;
|
|
13
|
+
robotCode?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type DingTalkIdType = "staffId" | "odpsUserId" | "chatId";
|
|
17
|
+
|
|
18
|
+
export type DingTalkMessageContext = {
|
|
19
|
+
conversationId: string;
|
|
20
|
+
messageId: string;
|
|
21
|
+
senderId: string; // senderStaffId
|
|
22
|
+
senderNick?: string;
|
|
23
|
+
chatType: "p2p" | "group"; // 1 = single chat, 2 = group chat
|
|
24
|
+
mentionedBot: boolean;
|
|
25
|
+
sessionWebhook: string;
|
|
26
|
+
sessionWebhookExpiredTime?: number;
|
|
27
|
+
content: string;
|
|
28
|
+
contentType: string; // text, image, voice, file, link, markdown, action_card
|
|
29
|
+
robotCode?: string;
|
|
30
|
+
chatbotCorpId?: string;
|
|
31
|
+
isAdmin?: boolean;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type DingTalkSendResult = {
|
|
35
|
+
messageId?: string;
|
|
36
|
+
conversationId: string;
|
|
37
|
+
processQueryKey?: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type DingTalkProbeResult = {
|
|
41
|
+
ok: boolean;
|
|
42
|
+
error?: string;
|
|
43
|
+
appKey?: string;
|
|
44
|
+
robotCode?: string;
|
|
45
|
+
connected?: boolean;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type DingTalkMediaInfo = {
|
|
49
|
+
path: string;
|
|
50
|
+
contentType?: string;
|
|
51
|
+
placeholder: string;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// DingTalk incoming message structure (from Stream callback)
|
|
55
|
+
export type DingTalkIncomingMessage = {
|
|
56
|
+
msgId: string;
|
|
57
|
+
msgtype: "text" | "image" | "voice" | "file" | "link" | "markdown" | "richText" | "picture";
|
|
58
|
+
text?: { content: string };
|
|
59
|
+
content?: string; // For richText
|
|
60
|
+
conversationId: string;
|
|
61
|
+
conversationType: "1" | "2"; // 1 = single chat, 2 = group chat
|
|
62
|
+
chatbotCorpId: string;
|
|
63
|
+
chatbotUserId?: string;
|
|
64
|
+
senderNick: string;
|
|
65
|
+
senderStaffId?: string;
|
|
66
|
+
senderCorpId?: string;
|
|
67
|
+
sessionWebhook: string;
|
|
68
|
+
sessionWebhookExpiredTime: number;
|
|
69
|
+
createAt: number;
|
|
70
|
+
robotCode?: string;
|
|
71
|
+
atUsers?: Array<{
|
|
72
|
+
dingtalkId: string;
|
|
73
|
+
staffId?: string;
|
|
74
|
+
}>;
|
|
75
|
+
isAdmin?: boolean;
|
|
76
|
+
isInAtList?: boolean;
|
|
77
|
+
// For media messages
|
|
78
|
+
downloadCode?: string;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// DingTalk outbound message types
|
|
82
|
+
export type DingTalkTextMessage = {
|
|
83
|
+
msgtype: "text";
|
|
84
|
+
text: {
|
|
85
|
+
content: string;
|
|
86
|
+
};
|
|
87
|
+
at?: {
|
|
88
|
+
atUserIds?: string[];
|
|
89
|
+
atMobiles?: string[];
|
|
90
|
+
isAtAll?: boolean;
|
|
91
|
+
};
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export type DingTalkMarkdownMessage = {
|
|
95
|
+
msgtype: "markdown";
|
|
96
|
+
markdown: {
|
|
97
|
+
title: string;
|
|
98
|
+
text: string;
|
|
99
|
+
};
|
|
100
|
+
at?: {
|
|
101
|
+
atUserIds?: string[];
|
|
102
|
+
atMobiles?: string[];
|
|
103
|
+
isAtAll?: boolean;
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export type DingTalkActionCardMessage = {
|
|
108
|
+
msgtype: "actionCard";
|
|
109
|
+
actionCard: {
|
|
110
|
+
title: string;
|
|
111
|
+
text: string;
|
|
112
|
+
singleTitle?: string;
|
|
113
|
+
singleURL?: string;
|
|
114
|
+
btnOrientation?: "0" | "1";
|
|
115
|
+
btns?: Array<{
|
|
116
|
+
title: string;
|
|
117
|
+
actionURL: string;
|
|
118
|
+
}>;
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export type DingTalkLinkMessage = {
|
|
123
|
+
msgtype: "link";
|
|
124
|
+
link: {
|
|
125
|
+
title: string;
|
|
126
|
+
text: string;
|
|
127
|
+
messageUrl: string;
|
|
128
|
+
picUrl?: string;
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export type DingTalkOutboundMessage =
|
|
133
|
+
| DingTalkTextMessage
|
|
134
|
+
| DingTalkMarkdownMessage
|
|
135
|
+
| DingTalkActionCardMessage
|
|
136
|
+
| DingTalkLinkMessage;
|
|
137
|
+
|
|
138
|
+
// ============ AI Card Types ============
|
|
139
|
+
|
|
140
|
+
/** AI Card instance for streaming responses */
|
|
141
|
+
export type AICardInstance = {
|
|
142
|
+
cardInstanceId: string;
|
|
143
|
+
accessToken: string;
|
|
144
|
+
inputingStarted: boolean;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
/** AI Card status values */
|
|
148
|
+
export type AICardStatusType = "1" | "2" | "3" | "4" | "5";
|
|
149
|
+
|
|
150
|
+
/** AI Card message data for creating cards */
|
|
151
|
+
export type AICardMessageData = {
|
|
152
|
+
conversationType: "1" | "2";
|
|
153
|
+
conversationId: string;
|
|
154
|
+
senderStaffId?: string;
|
|
155
|
+
senderId?: string;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// ============ Session Types ============
|
|
159
|
+
|
|
160
|
+
/** User session for conversation persistence */
|
|
161
|
+
export type UserSession = {
|
|
162
|
+
lastActivity: number;
|
|
163
|
+
sessionId: string;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// ============ Gateway Types ============
|
|
167
|
+
|
|
168
|
+
/** Gateway streaming options */
|
|
169
|
+
export type GatewayOptions = {
|
|
170
|
+
userContent: string;
|
|
171
|
+
systemPrompts: string[];
|
|
172
|
+
sessionKey: string;
|
|
173
|
+
gatewayUrl?: string;
|
|
174
|
+
gatewayPort?: number;
|
|
175
|
+
gatewayAuth?: string;
|
|
176
|
+
log?: {
|
|
177
|
+
info?: (msg: string) => void;
|
|
178
|
+
warn?: (msg: string) => void;
|
|
179
|
+
error?: (msg: string) => void;
|
|
180
|
+
};
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
// ============ Streaming Handler Types ============
|
|
184
|
+
|
|
185
|
+
/** Options for streaming message handler */
|
|
186
|
+
export type StreamingHandlerOptions = {
|
|
187
|
+
cfg: unknown;
|
|
188
|
+
accountId: string;
|
|
189
|
+
data: DingTalkIncomingMessage;
|
|
190
|
+
sessionWebhook: string;
|
|
191
|
+
config: DingTalkConfig;
|
|
192
|
+
client?: unknown;
|
|
193
|
+
log?: {
|
|
194
|
+
info?: (msg: string) => void;
|
|
195
|
+
warn?: (msg: string) => void;
|
|
196
|
+
error?: (msg: string) => void;
|
|
197
|
+
};
|
|
198
|
+
};
|
package/src/typing.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ClawdbotConfig } from "openclaw/plugin-sdk";
|
|
2
|
+
|
|
3
|
+
// DingTalk doesn't have a native typing indicator API
|
|
4
|
+
// This is a stub implementation that does nothing but maintains interface compatibility
|
|
5
|
+
|
|
6
|
+
export type TypingIndicatorState = {
|
|
7
|
+
sessionWebhook?: string;
|
|
8
|
+
active: boolean;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Add a typing indicator (stub - DingTalk doesn't support this)
|
|
13
|
+
*/
|
|
14
|
+
export async function addTypingIndicator(params: {
|
|
15
|
+
cfg: ClawdbotConfig;
|
|
16
|
+
sessionWebhook?: string;
|
|
17
|
+
}): Promise<TypingIndicatorState> {
|
|
18
|
+
// DingTalk doesn't support typing indicators
|
|
19
|
+
// Just return a state object for tracking
|
|
20
|
+
return {
|
|
21
|
+
sessionWebhook: params.sessionWebhook,
|
|
22
|
+
active: true,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Remove a typing indicator (stub - DingTalk doesn't support this)
|
|
28
|
+
*/
|
|
29
|
+
export async function removeTypingIndicator(params: {
|
|
30
|
+
cfg: ClawdbotConfig;
|
|
31
|
+
state: TypingIndicatorState;
|
|
32
|
+
}): Promise<void> {
|
|
33
|
+
// DingTalk doesn't support typing indicators
|
|
34
|
+
// Nothing to do here
|
|
35
|
+
params.state.active = false;
|
|
36
|
+
}
|