@axhub/genie 0.1.6 → 0.1.8

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.
Files changed (37) hide show
  1. package/dist/api-docs.html +351 -909
  2. package/dist/assets/index-CVjMty4a.js +902 -0
  3. package/dist/assets/index-eo5scY_Z.css +32 -0
  4. package/dist/index.html +5 -5
  5. package/dist/manifest.json +2 -2
  6. package/package.json +8 -2
  7. package/server/channels/core/ChannelManager.js +399 -0
  8. package/server/channels/core/PluginManager.js +59 -0
  9. package/server/channels/index.js +3 -0
  10. package/server/channels/plugins/BasePlugin.js +46 -0
  11. package/server/channels/plugins/dingtalk/DingTalkAdapter.js +156 -0
  12. package/server/channels/plugins/dingtalk/DingTalkPlugin.js +592 -0
  13. package/server/channels/plugins/dingtalk/index.js +2 -0
  14. package/server/channels/plugins/lark/LarkAdapter.js +100 -0
  15. package/server/channels/plugins/lark/LarkCards.js +43 -0
  16. package/server/channels/plugins/lark/LarkPlugin.js +260 -0
  17. package/server/channels/runtime/AgentRuntimeAdapter.js +179 -0
  18. package/server/channels/runtime/DingTalkStreamWriter.js +105 -0
  19. package/server/channels/runtime/LarkStreamWriter.js +99 -0
  20. package/server/channels/store/ChannelStore.js +236 -0
  21. package/server/database/db.js +109 -1
  22. package/server/database/init.sql +47 -1
  23. package/server/gemini-cli.js +280 -0
  24. package/server/index.js +230 -11
  25. package/server/openai-codex.js +104 -8
  26. package/server/opencode-cli.js +673 -0
  27. package/server/projects.js +645 -5
  28. package/server/routes/agent.js +40 -12
  29. package/server/routes/channels.js +221 -0
  30. package/server/routes/cli-auth.js +317 -0
  31. package/server/routes/commands.js +29 -3
  32. package/server/routes/git.js +15 -5
  33. package/server/routes/opencode.js +72 -0
  34. package/shared/modelConstants.js +62 -17
  35. package/dist/assets/index-CtRxrKDm.css +0 -32
  36. package/dist/assets/index-OENtErNy.js +0 -1249
  37. package/server/database/auth.db +0 -0
@@ -0,0 +1,592 @@
1
+ import https from 'https';
2
+
3
+ import { DWClient, TOPIC_ROBOT, EventAck } from 'dingtalk-stream';
4
+
5
+ import { BasePlugin } from '../BasePlugin.js';
6
+ import {
7
+ DINGTALK_MESSAGE_LIMIT,
8
+ encodeChatId,
9
+ parseChatId,
10
+ toDingTalkSendParams,
11
+ toIncomingMessage,
12
+ convertHtmlToDingTalkMarkdown,
13
+ truncateDingTalkText,
14
+ } from './DingTalkAdapter.js';
15
+
16
+ const EVENT_CACHE_TTL = 5 * 60 * 1000;
17
+ const EVENT_CACHE_CLEANUP_INTERVAL = 60 * 1000;
18
+ const DINGTALK_API_BASE = 'https://api.dingtalk.com';
19
+ const AI_CARD_TEMPLATE_ID = '382e4302-551d-4880-bf29-a30acfab2e71.schema';
20
+
21
+ const AI_CARD_STATUS = {
22
+ INPUTING: '2',
23
+ FINISHED: '3',
24
+ };
25
+
26
+ export class DingTalkPlugin extends BasePlugin {
27
+ constructor() {
28
+ super('dingtalk');
29
+
30
+ this.client = null;
31
+ this.isConnected = false;
32
+
33
+ this.clientId = '';
34
+ this.clientSecret = '';
35
+
36
+ this.tokenCache = null;
37
+ this.activeUsers = new Set();
38
+ this.processedEvents = new Map();
39
+ this.eventCleanupTimer = null;
40
+
41
+ this.aiCardSessions = new Map();
42
+ this.webhookCache = new Map();
43
+ }
44
+
45
+ async onInitialize(config) {
46
+ const clientId = config?.clientId;
47
+ const clientSecret = config?.clientSecret;
48
+
49
+ if (!clientId || !clientSecret) {
50
+ throw new Error('DingTalk Client ID and Client Secret are required');
51
+ }
52
+
53
+ this.clientId = String(clientId).trim();
54
+ this.clientSecret = String(clientSecret).trim();
55
+ }
56
+
57
+ async onStart() {
58
+ if (!this.clientId || !this.clientSecret) {
59
+ throw new Error('DingTalk credentials are missing');
60
+ }
61
+
62
+ await this.refreshAccessToken();
63
+
64
+ this.client = new DWClient({
65
+ clientId: this.clientId,
66
+ clientSecret: this.clientSecret,
67
+ keepAlive: true,
68
+ debug: false,
69
+ });
70
+
71
+ this.client.registerCallbackListener(TOPIC_ROBOT, (msg) => {
72
+ this.client?.socketCallBackResponse(msg.headers.messageId, EventAck.SUCCESS);
73
+
74
+ try {
75
+ const data = JSON.parse(msg.data);
76
+ void this.handleRobotMessage(data, msg.headers.messageId).catch((error) => {
77
+ console.error('[DingTalkPlugin] Error handling robot message:', error);
78
+ });
79
+ } catch (error) {
80
+ console.error('[DingTalkPlugin] Failed to parse robot message:', error);
81
+ }
82
+ });
83
+
84
+ await this.client.connect();
85
+ this.isConnected = true;
86
+ this.startEventCleanup();
87
+
88
+ console.log(`[DingTalkPlugin] Started for client ${this.clientId}`);
89
+ }
90
+
91
+ async onStop() {
92
+ this.stopEventCleanup();
93
+
94
+ if (this.client) {
95
+ try {
96
+ this.client.disconnect();
97
+ } catch {
98
+ // ignore
99
+ }
100
+ this.client = null;
101
+ }
102
+
103
+ this.tokenCache = null;
104
+ this.activeUsers.clear();
105
+ this.processedEvents.clear();
106
+ this.aiCardSessions.clear();
107
+ this.webhookCache.clear();
108
+ this.isConnected = false;
109
+
110
+ console.log('[DingTalkPlugin] Stopped and cleaned up');
111
+ }
112
+
113
+ getActiveUserCount() {
114
+ return this.activeUsers.size;
115
+ }
116
+
117
+ getBotInfo() {
118
+ if (!this.clientId) {
119
+ return null;
120
+ }
121
+ return {
122
+ id: this.clientId,
123
+ displayName: 'Genie DingTalk Bot',
124
+ };
125
+ }
126
+
127
+ async sendText(chatId, text) {
128
+ return this.sendMessage(chatId, {
129
+ type: 'text',
130
+ text: truncateDingTalkText(convertHtmlToDingTalkMarkdown(String(text || '')), DINGTALK_MESSAGE_LIMIT),
131
+ });
132
+ }
133
+
134
+ async sendMessage(chatId, message) {
135
+ await this.ensureAccessToken();
136
+
137
+ const { contentType, content, rawText } = toDingTalkSendParams(message);
138
+ const { type: chatType, id } = parseChatId(chatId);
139
+
140
+ if (contentType === 'markdown' && rawText !== undefined) {
141
+ try {
142
+ const cardMessageId = await this.createAndDeliverAICard(chatType, id);
143
+ const initialText = truncateDingTalkText(convertHtmlToDingTalkMarkdown(rawText), DINGTALK_MESSAGE_LIMIT);
144
+ if (initialText) {
145
+ await this.streamAICard(this.aiCardSessions.get(cardMessageId)?.outTrackId, initialText, false);
146
+ }
147
+ return cardMessageId;
148
+ } catch (error) {
149
+ console.warn('[DingTalkPlugin] AI Card failed, falling back to webhook/API:', error?.message || error);
150
+ }
151
+ }
152
+
153
+ const webhook = this.webhookCache.get(chatId);
154
+ if (webhook) {
155
+ const messageId = await this.sendViaWebhook(webhook, contentType, content, rawText);
156
+ return messageId;
157
+ }
158
+
159
+ return this.sendViaAPI(chatType, id, contentType, content, rawText);
160
+ }
161
+
162
+ async editMessage(chatId, messageId, message) {
163
+ const session = this.aiCardSessions.get(messageId);
164
+ const isFinal = !!message?.isFinal;
165
+
166
+ if (!session || session.isFinished) {
167
+ if (isFinal && message?.text) {
168
+ await this.sendPlainMessage(chatId, message);
169
+ }
170
+ return;
171
+ }
172
+
173
+ const text = truncateDingTalkText(
174
+ convertHtmlToDingTalkMarkdown(String(message?.text || '')),
175
+ DINGTALK_MESSAGE_LIMIT
176
+ ) || ' ';
177
+
178
+ try {
179
+ await this.streamAICard(session.outTrackId, text, isFinal);
180
+
181
+ if (isFinal) {
182
+ await this.finishAICard(session.outTrackId, text);
183
+ this.aiCardSessions.set(messageId, { ...session, isFinished: true });
184
+ }
185
+ } catch (error) {
186
+ console.error('[DingTalkPlugin] Failed to update AI Card:', error);
187
+ this.aiCardSessions.set(messageId, { ...session, isFinished: true });
188
+
189
+ if (isFinal && message?.text) {
190
+ await this.sendPlainMessage(chatId, message);
191
+ }
192
+ }
193
+ }
194
+
195
+ async handleRobotMessage(data, streamMessageId) {
196
+ const eventId = data?.msgId || streamMessageId;
197
+
198
+ if (eventId && this.isEventProcessed(eventId)) {
199
+ return;
200
+ }
201
+ if (eventId) {
202
+ this.markEventProcessed(eventId);
203
+ }
204
+
205
+ const userId = String(data?.senderStaffId || '').trim();
206
+ if (!userId) {
207
+ return;
208
+ }
209
+
210
+ this.activeUsers.add(userId);
211
+
212
+ if (data?.sessionWebhook) {
213
+ const chatId = encodeChatId(data);
214
+ this.webhookCache.set(chatId, data.sessionWebhook);
215
+ }
216
+
217
+ const incoming = toIncomingMessage(data);
218
+ if (!incoming || !this.messageHandler) {
219
+ return;
220
+ }
221
+
222
+ try {
223
+ await this.messageHandler(incoming);
224
+ } catch (error) {
225
+ console.error('[DingTalkPlugin] Failed to process incoming message:', error);
226
+ }
227
+ }
228
+
229
+ async createAndDeliverAICard(chatType, id) {
230
+ const token = await this.getAccessToken();
231
+ const outTrackId = `genie_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
232
+
233
+ await this.apiRequest('POST', '/v1.0/card/instances', token, {
234
+ cardTemplateId: AI_CARD_TEMPLATE_ID,
235
+ outTrackId,
236
+ cardData: {
237
+ cardParamMap: {},
238
+ },
239
+ callbackType: 'STREAM',
240
+ imGroupOpenSpaceModel: { supportForward: true },
241
+ imRobotOpenSpaceModel: { supportForward: true },
242
+ });
243
+
244
+ const openSpaceId = chatType === 'group' ? `dtv1.card//IM_GROUP.${id}` : `dtv1.card//IM_ROBOT.${id}`;
245
+
246
+ await this.apiRequest('POST', '/v1.0/card/instances/deliver', token, {
247
+ outTrackId,
248
+ openSpaceId,
249
+ userIdType: 1,
250
+ imGroupOpenDeliverModel: chatType === 'group' ? { robotCode: this.clientId } : undefined,
251
+ imRobotOpenDeliverModel: chatType === 'user' ? { spaceType: 'IM_ROBOT' } : undefined,
252
+ });
253
+
254
+ const messageId = `aicard_${outTrackId}`;
255
+ this.aiCardSessions.set(messageId, {
256
+ outTrackId,
257
+ openSpaceId,
258
+ isFinished: false,
259
+ inputingStarted: false,
260
+ });
261
+
262
+ return messageId;
263
+ }
264
+
265
+ async streamAICard(outTrackId, content, isFinalize = false) {
266
+ if (!outTrackId) {
267
+ throw new Error('Missing outTrackId for AI Card stream');
268
+ }
269
+
270
+ const token = await this.getAccessToken();
271
+ const session = this.findCardSessionByTrackId(outTrackId);
272
+
273
+ if (session && !session.inputingStarted) {
274
+ await this.apiRequest('PUT', '/v1.0/card/instances', token, {
275
+ outTrackId,
276
+ cardData: {
277
+ cardParamMap: {
278
+ flowStatus: AI_CARD_STATUS.INPUTING,
279
+ msgContent: '',
280
+ staticMsgContent: '',
281
+ sys_full_json_obj: JSON.stringify({ order: ['msgContent'] }),
282
+ },
283
+ },
284
+ });
285
+ session.inputingStarted = true;
286
+ }
287
+
288
+ await this.apiRequest('PUT', '/v1.0/card/streaming', token, {
289
+ outTrackId,
290
+ key: 'msgContent',
291
+ content,
292
+ isFull: true,
293
+ isFinalize,
294
+ isError: false,
295
+ guid: `${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
296
+ });
297
+ }
298
+
299
+ async finishAICard(outTrackId, finalContent) {
300
+ if (!outTrackId) {
301
+ return;
302
+ }
303
+
304
+ const token = await this.getAccessToken();
305
+ await this.apiRequest('PUT', '/v1.0/card/instances', token, {
306
+ outTrackId,
307
+ cardData: {
308
+ cardParamMap: {
309
+ flowStatus: AI_CARD_STATUS.FINISHED,
310
+ msgContent: finalContent,
311
+ staticMsgContent: '',
312
+ sys_full_json_obj: JSON.stringify({ order: ['msgContent'] }),
313
+ },
314
+ },
315
+ });
316
+ }
317
+
318
+ findCardSessionByTrackId(outTrackId) {
319
+ for (const session of this.aiCardSessions.values()) {
320
+ if (session.outTrackId === outTrackId) {
321
+ return session;
322
+ }
323
+ }
324
+ return undefined;
325
+ }
326
+
327
+ async sendPlainMessage(chatId, message) {
328
+ try {
329
+ await this.ensureAccessToken();
330
+ const { contentType, content, rawText } = toDingTalkSendParams(message);
331
+ const { type: chatType, id } = parseChatId(chatId);
332
+
333
+ const webhook = this.webhookCache.get(chatId);
334
+ if (webhook) {
335
+ await this.sendViaWebhook(webhook, contentType, content, rawText);
336
+ return;
337
+ }
338
+
339
+ await this.sendViaAPI(chatType, id, contentType, content, rawText);
340
+ } catch (error) {
341
+ console.error('[DingTalkPlugin] Fallback plain message send failed:', error);
342
+ }
343
+ }
344
+
345
+ async sendViaWebhook(webhook, contentType, content, rawText) {
346
+ let body;
347
+
348
+ if (contentType === 'actionCard') {
349
+ body = {
350
+ msgtype: 'actionCard',
351
+ actionCard: content,
352
+ };
353
+ } else {
354
+ body = {
355
+ msgtype: 'markdown',
356
+ markdown: {
357
+ title: 'Message',
358
+ text: rawText || JSON.stringify(content),
359
+ },
360
+ };
361
+ }
362
+
363
+ const response = await this.httpPost(webhook, body);
364
+ return response?.messageId || `webhook_${Date.now()}`;
365
+ }
366
+
367
+ async sendViaAPI(chatType, id, contentType, content, rawText) {
368
+ const token = await this.getAccessToken();
369
+
370
+ if (chatType === 'user') {
371
+ const body = {
372
+ robotCode: this.clientId,
373
+ userIds: [id],
374
+ msgKey: contentType === 'actionCard' ? 'sampleActionCard6' : 'sampleMarkdown',
375
+ msgParam:
376
+ contentType === 'actionCard'
377
+ ? JSON.stringify(content)
378
+ : JSON.stringify({ title: 'Message', text: rawText || '' }),
379
+ };
380
+
381
+ const response = await this.apiRequest('POST', '/v1.0/robot/oToMessages/batchSend', token, body);
382
+ return response?.processQueryKey || `api_${Date.now()}`;
383
+ }
384
+
385
+ const body = {
386
+ robotCode: this.clientId,
387
+ openConversationId: id,
388
+ msgKey: contentType === 'actionCard' ? 'sampleActionCard6' : 'sampleMarkdown',
389
+ msgParam:
390
+ contentType === 'actionCard'
391
+ ? JSON.stringify(content)
392
+ : JSON.stringify({ title: 'Message', text: rawText || '' }),
393
+ };
394
+
395
+ const response = await this.apiRequest('POST', '/v1.0/robot/groupMessages/send', token, body);
396
+ return response?.processQueryKey || `api_${Date.now()}`;
397
+ }
398
+
399
+ async getAccessToken() {
400
+ await this.ensureAccessToken();
401
+ return this.tokenCache?.accessToken || '';
402
+ }
403
+
404
+ async ensureAccessToken() {
405
+ const now = Date.now();
406
+ if (!this.tokenCache || this.tokenCache.expiresAt - now < 60 * 1000) {
407
+ await this.refreshAccessToken();
408
+ }
409
+ }
410
+
411
+ async refreshAccessToken() {
412
+ const response = await this.httpPost(`${DINGTALK_API_BASE}/v1.0/oauth2/accessToken`, {
413
+ appKey: this.clientId,
414
+ appSecret: this.clientSecret,
415
+ });
416
+
417
+ if (!response?.accessToken) {
418
+ throw new Error('Failed to refresh DingTalk access token');
419
+ }
420
+
421
+ this.tokenCache = {
422
+ accessToken: response.accessToken,
423
+ expiresAt: Date.now() + (response.expireIn || 7200) * 1000,
424
+ };
425
+ }
426
+
427
+ async apiRequest(method, path, token, body) {
428
+ const url = `${DINGTALK_API_BASE}${path}`;
429
+ return this.httpRequest(method, url, body, {
430
+ 'x-acs-dingtalk-access-token': token,
431
+ 'Content-Type': 'application/json',
432
+ });
433
+ }
434
+
435
+ async httpPost(url, body) {
436
+ return this.httpRequest('POST', url, body, {
437
+ 'Content-Type': 'application/json',
438
+ });
439
+ }
440
+
441
+ httpRequest(method, url, body, headers = {}) {
442
+ return new Promise((resolve, reject) => {
443
+ const urlObj = new URL(url);
444
+ const payload = body ? JSON.stringify(body) : undefined;
445
+
446
+ const options = {
447
+ hostname: urlObj.hostname,
448
+ port: urlObj.port || 443,
449
+ path: urlObj.pathname + urlObj.search,
450
+ method,
451
+ headers: {
452
+ ...headers,
453
+ ...(payload ? { 'Content-Length': Buffer.byteLength(payload).toString() } : {}),
454
+ },
455
+ };
456
+
457
+ const req = https.request(options, (res) => {
458
+ let responseData = '';
459
+
460
+ res.on('data', (chunk) => {
461
+ responseData += chunk;
462
+ });
463
+
464
+ res.on('end', () => {
465
+ try {
466
+ const parsed = responseData ? JSON.parse(responseData) : {};
467
+ if (res.statusCode && res.statusCode >= 400) {
468
+ reject(new Error(`HTTP ${res.statusCode}: ${JSON.stringify(parsed)}`));
469
+ return;
470
+ }
471
+ resolve(parsed);
472
+ } catch {
473
+ resolve(responseData);
474
+ }
475
+ });
476
+ });
477
+
478
+ req.on('error', reject);
479
+ req.setTimeout(30000, () => {
480
+ req.destroy(new Error('Request timeout'));
481
+ });
482
+
483
+ if (payload) {
484
+ req.write(payload);
485
+ }
486
+
487
+ req.end();
488
+ });
489
+ }
490
+
491
+ isEventProcessed(eventId) {
492
+ return this.processedEvents.has(eventId);
493
+ }
494
+
495
+ markEventProcessed(eventId) {
496
+ this.processedEvents.set(eventId, Date.now());
497
+ }
498
+
499
+ startEventCleanup() {
500
+ if (this.eventCleanupTimer) {
501
+ return;
502
+ }
503
+
504
+ this.eventCleanupTimer = setInterval(() => {
505
+ const now = Date.now();
506
+ for (const [eventId, timestamp] of this.processedEvents.entries()) {
507
+ if (now - timestamp > EVENT_CACHE_TTL) {
508
+ this.processedEvents.delete(eventId);
509
+ }
510
+ }
511
+ }, EVENT_CACHE_CLEANUP_INTERVAL);
512
+ }
513
+
514
+ stopEventCleanup() {
515
+ if (!this.eventCleanupTimer) {
516
+ return;
517
+ }
518
+
519
+ clearInterval(this.eventCleanupTimer);
520
+ this.eventCleanupTimer = null;
521
+ }
522
+
523
+ static async testConnection(clientId, clientSecret = '') {
524
+ const resolvedClientId = String(clientId || '').trim();
525
+ const resolvedClientSecret = String(clientSecret || '').trim();
526
+
527
+ if (!resolvedClientId || !resolvedClientSecret) {
528
+ return {
529
+ success: false,
530
+ error: 'Client ID and Client Secret are required',
531
+ };
532
+ }
533
+
534
+ try {
535
+ const response = await new Promise((resolve, reject) => {
536
+ const data = JSON.stringify({
537
+ appKey: resolvedClientId,
538
+ appSecret: resolvedClientSecret,
539
+ });
540
+
541
+ const options = {
542
+ hostname: 'api.dingtalk.com',
543
+ port: 443,
544
+ path: '/v1.0/oauth2/accessToken',
545
+ method: 'POST',
546
+ headers: {
547
+ 'Content-Type': 'application/json',
548
+ 'Content-Length': Buffer.byteLength(data).toString(),
549
+ },
550
+ };
551
+
552
+ const req = https.request(options, (res) => {
553
+ let responseData = '';
554
+ res.on('data', (chunk) => {
555
+ responseData += chunk;
556
+ });
557
+ res.on('end', () => {
558
+ try {
559
+ resolve(JSON.parse(responseData));
560
+ } catch {
561
+ reject(new Error('Invalid response from DingTalk API'));
562
+ }
563
+ });
564
+ });
565
+
566
+ req.on('error', reject);
567
+ req.setTimeout(10000, () => {
568
+ req.destroy(new Error('Connection timeout'));
569
+ });
570
+ req.write(data);
571
+ req.end();
572
+ });
573
+
574
+ if (response?.accessToken) {
575
+ return {
576
+ success: true,
577
+ botInfo: { name: 'DingTalk Bot' },
578
+ };
579
+ }
580
+
581
+ return {
582
+ success: false,
583
+ error: response?.message || response?.errmsg || 'Failed to connect to DingTalk API',
584
+ };
585
+ } catch (error) {
586
+ return {
587
+ success: false,
588
+ error: error?.message || 'Failed to connect to DingTalk API',
589
+ };
590
+ }
591
+ }
592
+ }
@@ -0,0 +1,2 @@
1
+ export { DingTalkPlugin } from './DingTalkPlugin.js';
2
+ export * from './DingTalkAdapter.js';
@@ -0,0 +1,100 @@
1
+ export const LARK_TEXT_LIMIT = 4000;
2
+
3
+ export function extractEffectiveUserId(sender = {}) {
4
+ const senderId = sender?.sender_id || {};
5
+ return senderId.user_id || senderId.open_id || senderId.union_id || '';
6
+ }
7
+
8
+ export function toIncomingMessage(event = {}) {
9
+ const message = event?.message;
10
+ const sender = event?.sender;
11
+
12
+ if (!message || !sender) {
13
+ return null;
14
+ }
15
+
16
+ const effectiveUserId = extractEffectiveUserId(sender);
17
+ if (!effectiveUserId) {
18
+ return {
19
+ id: message.message_id || `${Date.now()}`,
20
+ chatId: message.chat_id || '',
21
+ effectiveUserId: '',
22
+ text: '',
23
+ messageType: message.message_type || 'unknown',
24
+ displayName: 'Unknown User',
25
+ raw: event,
26
+ };
27
+ }
28
+
29
+ const parsed = parseLarkContent(message.message_type, message.content);
30
+
31
+ return {
32
+ id: message.message_id || `${Date.now()}`,
33
+ chatId: message.chat_id || effectiveUserId,
34
+ effectiveUserId,
35
+ text: parsed.text,
36
+ messageType: message.message_type || 'text',
37
+ displayName: `User ${effectiveUserId.slice(-6)}`,
38
+ raw: event,
39
+ };
40
+ }
41
+
42
+ function parseLarkContent(messageType, content) {
43
+ let parsed = content;
44
+ try {
45
+ parsed = typeof content === 'string' ? JSON.parse(content) : content;
46
+ } catch {
47
+ parsed = content;
48
+ }
49
+
50
+ if (messageType === 'text') {
51
+ return {
52
+ text: typeof parsed === 'object' && parsed ? parsed.text || '' : String(parsed || ''),
53
+ };
54
+ }
55
+
56
+ return {
57
+ text: '',
58
+ };
59
+ }
60
+
61
+ export function convertHtmlToLarkMarkdown(input) {
62
+ if (!input) return '';
63
+
64
+ let text = String(input);
65
+ text = text
66
+ .replace(/&quot;/gi, '"')
67
+ .replace(/&#39;|&apos;/gi, "'")
68
+ .replace(/&#x([0-9a-f]+);/gi, (_, hex) => String.fromCharCode(parseInt(hex, 16)))
69
+ .replace(/&#(\d+);/g, (_, dec) => String.fromCharCode(parseInt(dec, 10)));
70
+
71
+ text = text.replace(/<b>(.+?)<\/b>/gi, '**$1**');
72
+ text = text.replace(/<strong>(.+?)<\/strong>/gi, '**$1**');
73
+ text = text.replace(/<i>(.+?)<\/i>/gi, '*$1*');
74
+ text = text.replace(/<em>(.+?)<\/em>/gi, '*$1*');
75
+ text = text.replace(/<code>(.+?)<\/code>/gi, '`$1`');
76
+ text = text.replace(/<pre><code>([\s\S]+?)<\/code><\/pre>/gi, '```\n$1\n```');
77
+
78
+ text = text.replace(/<a href="([^"]+)">(.+?)<\/a>/gi, (_, url, label) => {
79
+ const normalized = String(url || '').trim().toLowerCase();
80
+ const safe = /^(https?:\/\/|mailto:|\/)|^[^:]*$/.test(normalized);
81
+ if (!safe) return label;
82
+ return `[${label}](${url})`;
83
+ });
84
+
85
+ let prev = '';
86
+ while (prev !== text) {
87
+ prev = text;
88
+ text = text.replace(/<[^>]+>/g, '');
89
+ }
90
+
91
+ return text;
92
+ }
93
+
94
+ export function truncateLarkText(text, maxLength = LARK_TEXT_LIMIT) {
95
+ const normalized = String(text || '');
96
+ if (normalized.length <= maxLength) {
97
+ return normalized;
98
+ }
99
+ return `${normalized.slice(0, maxLength - 3)}...`;
100
+ }