@kafca/agentdock 0.1.19 → 0.1.21
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/README.md +7 -0
- package/dist/renderer/assets/{Badge-DcFVSGwl.js → Badge-BRmQIqbd.js} +1 -1
- package/dist/renderer/assets/{Chat-CTy10fKR.js → Chat-D94ODABB.js} +1 -1
- package/dist/renderer/assets/{ChatMarkdown-C-ci0dQL.js → ChatMarkdown-DqW7OUvV.js} +1 -1
- package/dist/renderer/assets/{Config-Bl5vuqBS.js → Config-9lgzpTCp.js} +1 -1
- package/dist/renderer/assets/{CronList-BQOEXhHD.js → CronList-BYZ-x_bp.js} +1 -1
- package/dist/renderer/assets/{Dashboard-CauX4_SR.js → Dashboard-COISXbQy.js} +2 -2
- package/dist/renderer/assets/{EmptyState-DVYGGFl9.js → EmptyState-w5-I9zg7.js} +1 -1
- package/dist/renderer/assets/{KnowledgeDetail-BlV8Twrx.js → KnowledgeDetail-DVxwrTLw.js} +1 -1
- package/dist/renderer/assets/{KnowledgeHome-vd6AO-Fn.js → KnowledgeHome-BKkubcwi.js} +1 -1
- package/dist/renderer/assets/{Logs-BrtF26F7.js → Logs-D7HxZ8Nb.js} +1 -1
- package/dist/renderer/assets/{Modal-D6ucs8W2.js → Modal-Ceu_MP2e.js} +1 -1
- package/dist/renderer/assets/{Page-BdF9afpu.js → Page-CpSczAz8.js} +1 -1
- package/dist/renderer/assets/{ProjectDetail-RuoRHs1Q.js → ProjectDetail-FJ_bNXLF.js} +1 -1
- package/dist/renderer/assets/{ProjectList-r9MXhUu7.js → ProjectList-DZKL4K9C.js} +1 -1
- package/dist/renderer/assets/{Select-CEsAkZ5j.js → Select-BHQk6qCU.js} +1 -1
- package/dist/renderer/assets/{SessionChat-DwnZpTVu.js → SessionChat-DpgSXF3m.js} +1 -1
- package/dist/renderer/assets/{SessionList-C89Z6STB.js → SessionList-CU6drQ4N.js} +1 -1
- package/dist/renderer/assets/{ThreadChat-JB9GY3La.js → ThreadChat-D3s4GXGh.js} +1 -1
- package/dist/renderer/assets/Workspace-B7looTVo.js +14 -0
- package/dist/renderer/assets/{arrow-left-DuiS0IaQ.js → arrow-left-Dtal-eOi.js} +1 -1
- package/dist/renderer/assets/{book-open-B6l3y70b.js → book-open-q814R_Ov.js} +1 -1
- package/dist/renderer/assets/{check-CfewkzaJ.js → check-DjfCdLvw.js} +1 -1
- package/dist/renderer/assets/{circle-qzty4gzl.js → circle-vfhmyE8v.js} +1 -1
- package/dist/renderer/assets/{desktop-DTAjgvNs.js → desktop-BPIxWbkM.js} +1 -1
- package/dist/renderer/assets/desktop-DZHC4PDB.js +3 -0
- package/dist/renderer/assets/{funnel-CwjI0g4c.js → funnel-CUQibo1I.js} +1 -1
- package/dist/renderer/assets/{heart-CVHUi-lA.js → heart-DG9_b8P1.js} +1 -1
- package/dist/renderer/assets/{index-tQ6A4Zff.js → index-Bn8ane6_.js} +2 -2
- package/dist/renderer/assets/{index-BbDTSo1y.js → index-Ds0IN5rP.js} +1 -1
- package/dist/renderer/assets/{pencil-BvQPZxH6.js → pencil-Dij0LlRY.js} +1 -1
- package/dist/renderer/assets/{play-hJkkCcS6.js → play-CPFGjIyJ.js} +1 -1
- package/dist/renderer/assets/{plug-BGtbkjYJ.js → plug-DH3RKQyn.js} +1 -1
- package/dist/renderer/assets/{plus-DrYsrZyx.js → plus-afkK9_Y8.js} +1 -1
- package/dist/renderer/assets/projects-DTDS9SnT.js +1 -0
- package/dist/renderer/assets/{save-C8W3Wq2Z.js → save-B-cKgWcy.js} +1 -1
- package/dist/renderer/assets/{search-BuSByDes.js → search-DR6PeYkt.js} +1 -1
- package/dist/renderer/assets/{server-B9wEMLdo.js → server-BM6KOB5a.js} +1 -1
- package/dist/renderer/assets/{session-utils-K8E4S3Qx.js → session-utils-BDuThM0a.js} +1 -1
- package/dist/renderer/assets/{sessions-B-QTvESY.js → sessions-C1yKps4T.js} +1 -1
- package/dist/renderer/assets/{shield-check-C-oxstVp.js → shield-check-BFjMG-r2.js} +1 -1
- package/dist/renderer/assets/{trash-2-BQpE15-6.js → trash-2-CW1heMCf.js} +1 -1
- package/dist/renderer/assets/{user-CM4R9QVx.js → user-r_2sHG7m.js} +1 -1
- package/dist/renderer/assets/{wifi-off-CoaA7hVn.js → wifi-off-VMUokMpE.js} +1 -1
- package/dist/renderer/index.html +1 -1
- package/dist-electron/electron/agent-runtime-detector.test.js +11 -0
- package/dist-electron/electron/local-core-refactor.test.js +522 -0
- package/dist-electron/electron/plugin-kernel.test.js +77 -9
- package/dist-electron/services/local-ai-core/src/acp/local-core-acp-backend.js +9 -9
- package/dist-electron/services/local-ai-core/src/acp/local-core-acp-content.js +60 -0
- package/dist-electron/services/local-ai-core/src/gateway/local-core-lark-gateway.js +337 -54
- package/dist-electron/services/local-ai-core/src/gateway/local-core-weixin-gateway.js +50 -2
- package/dist-electron/services/local-ai-core/src/kernel/bootstrap.js +2 -1
- package/dist-electron/services/local-ai-core/src/plugins/builtin/agent-localcore-acp-plugin.js +9 -0
- package/dist-electron/services/local-ai-core/src/router/workspace-route-config.js +19 -8
- package/dist-electron/services/local-ai-core/src/runtime/agent-runtime-detector.js +23 -12
- package/dist-electron/services/local-ai-core/src/runtime/local-core-controller.js +34 -0
- package/dist-electron/shared/desktop.js +4 -2
- package/package.json +2 -1
- package/dist/renderer/assets/Workspace-Cftl8zps.js +0 -14
- package/dist/renderer/assets/desktop-D4Z7M9RM.js +0 -3
- package/dist/renderer/assets/projects-DcNm9FDK.js +0 -1
|
@@ -40,9 +40,9 @@ const desktop_js_1 = require("../../../../shared/desktop.js");
|
|
|
40
40
|
const PAIRING_EXPIRY_MS = 10 * 60 * 1000;
|
|
41
41
|
class LocalCoreLarkGateway extends node_events_1.EventEmitter {
|
|
42
42
|
options;
|
|
43
|
-
// Lark card action
|
|
44
|
-
// Keep
|
|
45
|
-
|
|
43
|
+
// Lark returns 200340 when card action events are not enabled in the app's
|
|
44
|
+
// event subscription. Keep card actions opt-in so text approval always works.
|
|
45
|
+
defaultCardActionsEnabled = false;
|
|
46
46
|
// Keep permission state in a dedicated card to avoid mixing order in the main reply card.
|
|
47
47
|
mirrorPermissionStateInMainCard = false;
|
|
48
48
|
runtime = new Map();
|
|
@@ -79,6 +79,7 @@ class LocalCoreLarkGateway extends node_events_1.EventEmitter {
|
|
|
79
79
|
status: 'disabled',
|
|
80
80
|
connected: false,
|
|
81
81
|
appId: binding.appId,
|
|
82
|
+
cardActionsEnabled: binding.cardActionsEnabled,
|
|
82
83
|
});
|
|
83
84
|
}
|
|
84
85
|
continue;
|
|
@@ -272,7 +273,7 @@ class LocalCoreLarkGateway extends node_events_1.EventEmitter {
|
|
|
272
273
|
this.options.store.clearPlatformThreadMessageId(route.workspaceId, route.chatId, route.platformUserId);
|
|
273
274
|
}
|
|
274
275
|
if (event.type === 'buttons') {
|
|
275
|
-
const permissionCard = this.renderPermissionCard(turn, event);
|
|
276
|
+
const permissionCard = this.renderPermissionCard(turn, event, Boolean(state.cardActionsEnabled));
|
|
276
277
|
if (permissionCard.text || permissionCard.buttonRows.length > 0) {
|
|
277
278
|
if (!turn.permissionMessageId) {
|
|
278
279
|
const createdId = await this.sendTextAsCard(state, route.chatId, permissionCard.text, permissionCard.buttonRows, sessionKey, binding.thread_id);
|
|
@@ -290,30 +291,35 @@ class LocalCoreLarkGateway extends node_events_1.EventEmitter {
|
|
|
290
291
|
return;
|
|
291
292
|
}
|
|
292
293
|
this.consumeBridgeEvent(turn, event);
|
|
293
|
-
const rendered = this.
|
|
294
|
+
const rendered = this.renderBridgeEventMessage(turn, event);
|
|
294
295
|
if (!rendered.text && rendered.buttonRows.length === 0) {
|
|
295
296
|
this.options.log?.(`localcore-lark bridge event produced empty render for sessionKey=${sessionKey} type=${event.type}`);
|
|
296
297
|
return;
|
|
297
298
|
}
|
|
299
|
+
const existingMessageId = this.getRenderedMessageId(turn, rendered);
|
|
298
300
|
const shouldThrottle = event.type === 'update_message' &&
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
301
|
+
rendered.isFinal &&
|
|
302
|
+
existingMessageId &&
|
|
303
|
+
Date.now() - (turn.lastPatchedAtByMessageId[existingMessageId] || 0) < 900;
|
|
304
|
+
this.options.log?.(`localcore-lark bridge event type=${event.type} sessionKey=${sessionKey} hasMessageId=${Boolean(existingMessageId)} throttle=${shouldThrottle}`);
|
|
302
305
|
if (shouldThrottle) {
|
|
303
306
|
return;
|
|
304
307
|
}
|
|
305
|
-
if (!
|
|
308
|
+
if (!existingMessageId) {
|
|
306
309
|
const createdId = await this.sendTextAsCard(state, route.chatId, rendered.text, rendered.buttonRows, sessionKey, binding.thread_id);
|
|
307
310
|
if (createdId) {
|
|
308
|
-
turn
|
|
309
|
-
|
|
311
|
+
this.setRenderedMessageId(turn, rendered, createdId);
|
|
312
|
+
if (rendered.isFinal) {
|
|
313
|
+
this.options.store.updatePlatformThreadMessageId(route.workspaceId, route.chatId, route.platformUserId, createdId);
|
|
314
|
+
}
|
|
310
315
|
this.options.log?.(`localcore-lark sent new card message ${createdId} for sessionKey=${sessionKey}`);
|
|
311
316
|
}
|
|
312
317
|
return;
|
|
313
318
|
}
|
|
314
|
-
await this.patchTextCard(state,
|
|
319
|
+
await this.patchTextCard(state, existingMessageId, rendered.text, rendered.buttonRows, sessionKey, binding.thread_id);
|
|
315
320
|
turn.lastPatchedAt = Date.now();
|
|
316
|
-
|
|
321
|
+
turn.lastPatchedAtByMessageId[existingMessageId] = turn.lastPatchedAt;
|
|
322
|
+
this.options.log?.(`localcore-lark patched card message ${existingMessageId} for sessionKey=${sessionKey}`);
|
|
317
323
|
}
|
|
318
324
|
catch (error) {
|
|
319
325
|
this.options.log?.(`localcore-lark bridge send failed for sessionKey=${sessionKey}: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -447,7 +453,8 @@ class LocalCoreLarkGateway extends node_events_1.EventEmitter {
|
|
|
447
453
|
return { paired: true, threadId };
|
|
448
454
|
}
|
|
449
455
|
this.options.store.clearPlatformThreadMessageId(input.workspaceId, input.chatId, input.platformUserId);
|
|
450
|
-
|
|
456
|
+
const wrappedText = (0, desktop_js_1.wrapUserMessageWithSchedulerProtocol)(input.text);
|
|
457
|
+
await router.sendThreadMessage(threadId, this.createThreadMessageInput(wrappedText, input.contentParts));
|
|
451
458
|
return { paired: true, threadId };
|
|
452
459
|
}
|
|
453
460
|
close() {
|
|
@@ -496,10 +503,12 @@ class LocalCoreLarkGateway extends node_events_1.EventEmitter {
|
|
|
496
503
|
status: 'starting',
|
|
497
504
|
connected: false,
|
|
498
505
|
appId: binding.appId,
|
|
506
|
+
cardActionsEnabled: binding.cardActionsEnabled,
|
|
499
507
|
};
|
|
500
508
|
this.runtime.set(binding.workspaceId, status);
|
|
501
509
|
this.notifyRuntimeStateChanged();
|
|
502
510
|
try {
|
|
511
|
+
this.options.log?.(`localcore-lark starting workspace=${binding.workspaceId} app=${this.maskLarkAppId(binding.appId)} cardActions=${binding.cardActionsEnabled}`);
|
|
503
512
|
const mod = await this.getLarkModule();
|
|
504
513
|
status.client = new mod.Client({
|
|
505
514
|
appId: binding.appId,
|
|
@@ -514,8 +523,11 @@ class LocalCoreLarkGateway extends node_events_1.EventEmitter {
|
|
|
514
523
|
});
|
|
515
524
|
status.eventDispatcher.register({
|
|
516
525
|
'im.message.receive_v1': async (data) => {
|
|
517
|
-
this.options.log?.(`localcore-lark received im.message.receive_v1 for ${binding.workspaceId}`);
|
|
518
|
-
|
|
526
|
+
this.options.log?.(`localcore-lark received im.message.receive_v1 for ${binding.workspaceId}: ${this.summarizeLarkEvent(data)}`);
|
|
527
|
+
void this.handleMessageEvent(binding.workspaceId, data).catch((error) => {
|
|
528
|
+
this.options.log?.(`localcore-lark inbound message failed for ${binding.workspaceId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
529
|
+
});
|
|
530
|
+
return {};
|
|
519
531
|
},
|
|
520
532
|
'card.action.trigger': async (data) => {
|
|
521
533
|
this.options.log?.(`localcore-lark received card.action.trigger for ${binding.workspaceId}`);
|
|
@@ -523,12 +535,15 @@ class LocalCoreLarkGateway extends node_events_1.EventEmitter {
|
|
|
523
535
|
return {};
|
|
524
536
|
},
|
|
525
537
|
});
|
|
538
|
+
this.options.log?.(`localcore-lark event dispatcher registered for ${binding.workspaceId}`);
|
|
526
539
|
status.wsClient = new mod.WSClient({
|
|
527
540
|
appId: binding.appId,
|
|
528
541
|
appSecret: binding.appSecret,
|
|
529
542
|
domain: mod.Domain.Feishu,
|
|
530
543
|
loggerLevel: mod.LoggerLevel.info,
|
|
531
544
|
});
|
|
545
|
+
this.attachWsDiagnostics(binding.workspaceId, status.wsClient);
|
|
546
|
+
this.options.log?.(`localcore-lark ws starting for ${binding.workspaceId}`);
|
|
532
547
|
await status.wsClient.start({
|
|
533
548
|
eventDispatcher: status.eventDispatcher,
|
|
534
549
|
});
|
|
@@ -536,6 +551,7 @@ class LocalCoreLarkGateway extends node_events_1.EventEmitter {
|
|
|
536
551
|
status.connected = true;
|
|
537
552
|
status.connectedAt = new Date().toISOString();
|
|
538
553
|
status.lastError = undefined;
|
|
554
|
+
this.options.log?.(`localcore-lark ws ready for ${binding.workspaceId}`);
|
|
539
555
|
}
|
|
540
556
|
catch (error) {
|
|
541
557
|
status.status = 'error';
|
|
@@ -550,10 +566,13 @@ class LocalCoreLarkGateway extends node_events_1.EventEmitter {
|
|
|
550
566
|
if (!state) {
|
|
551
567
|
return;
|
|
552
568
|
}
|
|
569
|
+
this.options.log?.(`localcore-lark stopping workspace=${workspaceId} status=${state.status}`);
|
|
553
570
|
try {
|
|
554
571
|
await state.wsClient?.stop?.();
|
|
572
|
+
this.options.log?.(`localcore-lark stopped workspace=${workspaceId}`);
|
|
555
573
|
}
|
|
556
|
-
catch {
|
|
574
|
+
catch (error) {
|
|
575
|
+
this.options.log?.(`localcore-lark stop failed for ${workspaceId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
557
576
|
// Best effort: current SDK versions may not implement stop().
|
|
558
577
|
}
|
|
559
578
|
this.runtime.set(workspaceId, {
|
|
@@ -562,6 +581,7 @@ class LocalCoreLarkGateway extends node_events_1.EventEmitter {
|
|
|
562
581
|
status: 'stopped',
|
|
563
582
|
connected: false,
|
|
564
583
|
appId: state.appId,
|
|
584
|
+
cardActionsEnabled: state.cardActionsEnabled,
|
|
565
585
|
});
|
|
566
586
|
this.notifyRuntimeStateChanged();
|
|
567
587
|
}
|
|
@@ -579,6 +599,7 @@ class LocalCoreLarkGateway extends node_events_1.EventEmitter {
|
|
|
579
599
|
: data;
|
|
580
600
|
const message = payload?.message;
|
|
581
601
|
const sender = payload?.sender;
|
|
602
|
+
this.options.log?.(`localcore-lark handling message event for ${workspaceId}: ${this.summarizeLarkPayload(payload)}`);
|
|
582
603
|
if (!message || !sender) {
|
|
583
604
|
this.options.log?.(`localcore-lark ignored event without message/sender for ${workspaceId}: ${JSON.stringify(Object.keys(data || {}))}`);
|
|
584
605
|
return;
|
|
@@ -590,18 +611,41 @@ class LocalCoreLarkGateway extends node_events_1.EventEmitter {
|
|
|
590
611
|
catch {
|
|
591
612
|
parsedContent = {};
|
|
592
613
|
}
|
|
614
|
+
const messageType = String(message.message_type || '').trim().toLowerCase();
|
|
593
615
|
const text = String(parsedContent.text || '').trim();
|
|
594
|
-
|
|
595
|
-
|
|
616
|
+
const contentParts = [];
|
|
617
|
+
if (text) {
|
|
618
|
+
contentParts.push({ type: 'text', text });
|
|
619
|
+
}
|
|
620
|
+
if (messageType === 'image') {
|
|
621
|
+
const imageKey = String(parsedContent.image_key || parsedContent.file_key || '').trim();
|
|
622
|
+
if (imageKey) {
|
|
623
|
+
try {
|
|
624
|
+
contentParts.push(await this.downloadMessageImage(workspaceId, String(message.message_id || ''), imageKey));
|
|
625
|
+
}
|
|
626
|
+
catch (error) {
|
|
627
|
+
const errorText = `[Image download failed: ${error instanceof Error ? error.message : String(error)}]`;
|
|
628
|
+
contentParts.push({ type: 'text', text: errorText });
|
|
629
|
+
this.options.log?.(`localcore-lark image download failed for ${workspaceId}: message=${String(message.message_id || '')} imageKey=${imageKey} error=${error instanceof Error ? error.message : String(error)}`);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
if (contentParts.length === 0) {
|
|
634
|
+
this.options.log?.(`localcore-lark ignored unsupported message for ${workspaceId}: type=${String(message.message_type || 'unknown')} contentKeys=${JSON.stringify(Object.keys(parsedContent))}`);
|
|
596
635
|
return;
|
|
597
636
|
}
|
|
637
|
+
const displayText = text || (contentParts.some((part) => part.type === 'image') ? '[Image]' : contentParts
|
|
638
|
+
.filter((part) => part.type === 'text')
|
|
639
|
+
.map((part) => part.text)
|
|
640
|
+
.join('\n\n')
|
|
641
|
+
.trim());
|
|
598
642
|
const platformUserId = String(sender.sender_id?.user_id || sender.sender_id?.open_id || '').trim();
|
|
599
643
|
const chatId = String(message.chat_id || platformUserId).trim();
|
|
600
644
|
if (!platformUserId || !chatId) {
|
|
601
|
-
this.options.log?.(`localcore-lark ignored message without sender/chat for ${workspaceId}`);
|
|
645
|
+
this.options.log?.(`localcore-lark ignored message without sender/chat for ${workspaceId}: senderKeys=${JSON.stringify(Object.keys(sender?.sender_id || {}))} chat=${String(message.chat_id || '')}`);
|
|
602
646
|
return;
|
|
603
647
|
}
|
|
604
|
-
this.options.log?.(`localcore-lark inbound message for ${workspaceId}: chat=${chatId} user=${platformUserId} text=${JSON.stringify(
|
|
648
|
+
this.options.log?.(`localcore-lark inbound message for ${workspaceId}: chat=${chatId} user=${platformUserId} type=${messageType || 'unknown'} text=${JSON.stringify(displayText.slice(0, 120))}`);
|
|
605
649
|
await this.handleInboundMessage({
|
|
606
650
|
workspaceId,
|
|
607
651
|
platformUserId,
|
|
@@ -609,10 +653,164 @@ class LocalCoreLarkGateway extends node_events_1.EventEmitter {
|
|
|
609
653
|
displayName: String(payload?.sender?.sender_id?.user_id ||
|
|
610
654
|
payload?.sender?.sender_id?.open_id ||
|
|
611
655
|
`Lark ${platformUserId.slice(-6)}`),
|
|
612
|
-
text,
|
|
656
|
+
text: displayText,
|
|
613
657
|
messageId: String(message.message_id || (0, node_crypto_1.randomUUID)()),
|
|
658
|
+
contentParts,
|
|
614
659
|
});
|
|
615
660
|
}
|
|
661
|
+
createWrappedContentParts(wrappedText, parts) {
|
|
662
|
+
const nonTextParts = Array.isArray(parts)
|
|
663
|
+
? parts.filter((part) => part.type !== 'text')
|
|
664
|
+
: [];
|
|
665
|
+
return [
|
|
666
|
+
{ type: 'text', text: wrappedText },
|
|
667
|
+
...nonTextParts,
|
|
668
|
+
];
|
|
669
|
+
}
|
|
670
|
+
createThreadMessageInput(wrappedText, parts) {
|
|
671
|
+
const hasNonTextPart = Array.isArray(parts) && parts.some((part) => part.type !== 'text');
|
|
672
|
+
if (!hasNonTextPart) {
|
|
673
|
+
return wrappedText;
|
|
674
|
+
}
|
|
675
|
+
return {
|
|
676
|
+
displayText: wrappedText,
|
|
677
|
+
contentParts: this.createWrappedContentParts(wrappedText, parts),
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
async downloadMessageImage(workspaceId, messageId, imageKey) {
|
|
681
|
+
const state = this.runtime.get(workspaceId);
|
|
682
|
+
if (!state?.client) {
|
|
683
|
+
throw new Error('Lark client is not connected');
|
|
684
|
+
}
|
|
685
|
+
if (!messageId) {
|
|
686
|
+
throw new Error('Lark image message is missing message_id');
|
|
687
|
+
}
|
|
688
|
+
const resource = await state.client.im.messageResource.get({
|
|
689
|
+
path: {
|
|
690
|
+
message_id: messageId,
|
|
691
|
+
file_key: imageKey,
|
|
692
|
+
},
|
|
693
|
+
params: {
|
|
694
|
+
type: 'image',
|
|
695
|
+
},
|
|
696
|
+
});
|
|
697
|
+
const buffer = await this.readLarkResourceBuffer(resource);
|
|
698
|
+
if (buffer.length === 0) {
|
|
699
|
+
throw new Error('Lark image resource is empty');
|
|
700
|
+
}
|
|
701
|
+
const headerMime = this.extractHeaderMimeType(resource?.headers);
|
|
702
|
+
return {
|
|
703
|
+
type: 'image',
|
|
704
|
+
data: buffer.toString('base64'),
|
|
705
|
+
mimeType: headerMime || this.sniffImageMimeType(buffer),
|
|
706
|
+
fileName: `${imageKey}.${this.sniffImageExtension(buffer)}`,
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
async readLarkResourceBuffer(resource) {
|
|
710
|
+
const readable = resource?.getReadableStream?.();
|
|
711
|
+
if (!readable || typeof readable.on !== 'function') {
|
|
712
|
+
throw new Error('Lark resource did not provide a readable stream');
|
|
713
|
+
}
|
|
714
|
+
const chunks = [];
|
|
715
|
+
await new Promise((resolve, reject) => {
|
|
716
|
+
readable.on('data', (chunk) => {
|
|
717
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
718
|
+
});
|
|
719
|
+
readable.on('error', reject);
|
|
720
|
+
readable.on('end', resolve);
|
|
721
|
+
});
|
|
722
|
+
return Buffer.concat(chunks);
|
|
723
|
+
}
|
|
724
|
+
extractHeaderMimeType(headers) {
|
|
725
|
+
const value = headers && typeof headers === 'object'
|
|
726
|
+
? headers['content-type'] || headers['Content-Type']
|
|
727
|
+
: '';
|
|
728
|
+
return String(value || '').split(';')[0]?.trim() || '';
|
|
729
|
+
}
|
|
730
|
+
sniffImageMimeType(buffer) {
|
|
731
|
+
if (buffer[0] === 0xff && buffer[1] === 0xd8)
|
|
732
|
+
return 'image/jpeg';
|
|
733
|
+
if (buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4e && buffer[3] === 0x47)
|
|
734
|
+
return 'image/png';
|
|
735
|
+
if (buffer[0] === 0x47 && buffer[1] === 0x49 && buffer[2] === 0x46)
|
|
736
|
+
return 'image/gif';
|
|
737
|
+
if (buffer[0] === 0x52 && buffer[1] === 0x49 && buffer[2] === 0x46 && buffer[3] === 0x46)
|
|
738
|
+
return 'image/webp';
|
|
739
|
+
return 'application/octet-stream';
|
|
740
|
+
}
|
|
741
|
+
sniffImageExtension(buffer) {
|
|
742
|
+
const mimeType = this.sniffImageMimeType(buffer);
|
|
743
|
+
if (mimeType === 'image/jpeg')
|
|
744
|
+
return 'jpg';
|
|
745
|
+
if (mimeType === 'image/png')
|
|
746
|
+
return 'png';
|
|
747
|
+
if (mimeType === 'image/gif')
|
|
748
|
+
return 'gif';
|
|
749
|
+
if (mimeType === 'image/webp')
|
|
750
|
+
return 'webp';
|
|
751
|
+
return 'bin';
|
|
752
|
+
}
|
|
753
|
+
attachWsDiagnostics(workspaceId, wsClient) {
|
|
754
|
+
const on = typeof wsClient?.on === 'function' ? wsClient.on.bind(wsClient) : null;
|
|
755
|
+
if (!on) {
|
|
756
|
+
this.options.log?.(`localcore-lark ws diagnostics unavailable for ${workspaceId}: client has no on()`);
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
for (const eventName of ['open', 'connect', 'connected', 'ready', 'close', 'closed', 'disconnect', 'error', 'reconnect']) {
|
|
760
|
+
try {
|
|
761
|
+
on(eventName, (...args) => {
|
|
762
|
+
this.options.log?.(`localcore-lark ws event ${eventName} for ${workspaceId}: ${this.summarizeWsArgs(args)}`);
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
catch (error) {
|
|
766
|
+
this.options.log?.(`localcore-lark ws diagnostic hook failed for ${workspaceId} event=${eventName}: ${error instanceof Error ? error.message : String(error)}`);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
summarizeLarkEvent(data) {
|
|
771
|
+
const payload = (data?.event && typeof data.event === 'object')
|
|
772
|
+
? data.event
|
|
773
|
+
: data;
|
|
774
|
+
return this.summarizeLarkPayload(payload);
|
|
775
|
+
}
|
|
776
|
+
summarizeLarkPayload(payload) {
|
|
777
|
+
const message = payload?.message || {};
|
|
778
|
+
const sender = payload?.sender || {};
|
|
779
|
+
const senderId = sender?.sender_id || {};
|
|
780
|
+
const content = typeof message.content === 'string' ? message.content : '';
|
|
781
|
+
return [
|
|
782
|
+
`message=${String(message.message_id || '') || 'missing'}`,
|
|
783
|
+
`type=${String(message.message_type || '') || 'missing'}`,
|
|
784
|
+
`chat=${String(message.chat_id || '') || 'missing'}`,
|
|
785
|
+
`sender=${String(senderId.user_id || senderId.open_id || '') || 'missing'}`,
|
|
786
|
+
`contentBytes=${Buffer.byteLength(content, 'utf8')}`,
|
|
787
|
+
`keys=${JSON.stringify(Object.keys(payload || {}))}`,
|
|
788
|
+
].join(' ');
|
|
789
|
+
}
|
|
790
|
+
summarizeWsArgs(args) {
|
|
791
|
+
if (args.length === 0) {
|
|
792
|
+
return 'no-args';
|
|
793
|
+
}
|
|
794
|
+
return args.map((arg) => {
|
|
795
|
+
if (arg instanceof Error) {
|
|
796
|
+
return `${arg.name}: ${arg.message}`;
|
|
797
|
+
}
|
|
798
|
+
if (typeof arg === 'string') {
|
|
799
|
+
return arg.slice(0, 200);
|
|
800
|
+
}
|
|
801
|
+
if (arg && typeof arg === 'object') {
|
|
802
|
+
return JSON.stringify(Object.keys(arg));
|
|
803
|
+
}
|
|
804
|
+
return String(arg);
|
|
805
|
+
}).join(' ');
|
|
806
|
+
}
|
|
807
|
+
maskLarkAppId(appId) {
|
|
808
|
+
const value = String(appId || '').trim();
|
|
809
|
+
if (value.length <= 8) {
|
|
810
|
+
return value ? '***' : '';
|
|
811
|
+
}
|
|
812
|
+
return `${value.slice(0, 6)}...${value.slice(-4)}`;
|
|
813
|
+
}
|
|
616
814
|
collectBindings(config) {
|
|
617
815
|
const projects = Array.isArray(config?.projects) ? config.projects : [];
|
|
618
816
|
return projects.flatMap((project) => {
|
|
@@ -633,6 +831,10 @@ class LocalCoreLarkGateway extends node_events_1.EventEmitter {
|
|
|
633
831
|
verificationToken: String(platform.options.verification_token || '').trim(),
|
|
634
832
|
autoApprove: String(platform.options.auto_approve || '').trim().toLowerCase() === 'true'
|
|
635
833
|
|| platform.options.auto_approve === true,
|
|
834
|
+
cardActionsEnabled: String(platform.options.card_actions || platform.options.enable_card_actions || '').trim().toLowerCase() === 'true'
|
|
835
|
+
|| platform.options.card_actions === true
|
|
836
|
+
|| platform.options.enable_card_actions === true
|
|
837
|
+
|| this.defaultCardActionsEnabled,
|
|
636
838
|
enabled: Boolean(String(platform.options.app_id || '').trim() && String(platform.options.app_secret || '').trim()),
|
|
637
839
|
project,
|
|
638
840
|
}));
|
|
@@ -656,12 +858,12 @@ class LocalCoreLarkGateway extends node_events_1.EventEmitter {
|
|
|
656
858
|
tag: 'button',
|
|
657
859
|
text: {
|
|
658
860
|
tag: 'plain_text',
|
|
659
|
-
content: button
|
|
861
|
+
content: this.formatPermissionButtonLabel(button),
|
|
660
862
|
},
|
|
661
|
-
type: index
|
|
863
|
+
type: this.resolveLarkButtonType(button, index),
|
|
662
864
|
value: {
|
|
663
865
|
action: 'permission_response',
|
|
664
|
-
response: button.data,
|
|
866
|
+
response: (0, desktop_js_1.normalizePermissionResponse)(button.data) || button.data,
|
|
665
867
|
session_key: sessionKey || '',
|
|
666
868
|
thread_id: threadId || '',
|
|
667
869
|
},
|
|
@@ -741,11 +943,13 @@ class LocalCoreLarkGateway extends node_events_1.EventEmitter {
|
|
|
741
943
|
processing: false,
|
|
742
944
|
previewText: '',
|
|
743
945
|
finalText: '',
|
|
946
|
+
progressMessageIds: {},
|
|
744
947
|
thinkingSteps: [],
|
|
745
948
|
toolCalls: [],
|
|
746
949
|
statusLines: [],
|
|
747
950
|
buttonRows: [],
|
|
748
951
|
lastPatchedAt: 0,
|
|
952
|
+
lastPatchedAtByMessageId: {},
|
|
749
953
|
};
|
|
750
954
|
this.outboundTurns.set(sessionKey, turn);
|
|
751
955
|
return turn;
|
|
@@ -755,12 +959,14 @@ class LocalCoreLarkGateway extends node_events_1.EventEmitter {
|
|
|
755
959
|
if (existing) {
|
|
756
960
|
if (messageId && !existing.messageId) {
|
|
757
961
|
existing.messageId = messageId;
|
|
962
|
+
existing.finalMessageId = messageId;
|
|
758
963
|
}
|
|
759
964
|
return existing;
|
|
760
965
|
}
|
|
761
966
|
const turn = this.createTurnState(sessionKey);
|
|
762
967
|
if (messageId) {
|
|
763
968
|
turn.messageId = messageId;
|
|
969
|
+
turn.finalMessageId = messageId;
|
|
764
970
|
}
|
|
765
971
|
return turn;
|
|
766
972
|
}
|
|
@@ -770,6 +976,8 @@ class LocalCoreLarkGateway extends node_events_1.EventEmitter {
|
|
|
770
976
|
// Always start a fresh card for each generation phase.
|
|
771
977
|
// This keeps Feishu message order aligned with user-visible timeline.
|
|
772
978
|
turn.messageId = undefined;
|
|
979
|
+
turn.finalMessageId = undefined;
|
|
980
|
+
turn.progressMessageIds = {};
|
|
773
981
|
turn.processing = true;
|
|
774
982
|
turn.permissionMessageId = undefined;
|
|
775
983
|
turn.previewText = '';
|
|
@@ -778,6 +986,7 @@ class LocalCoreLarkGateway extends node_events_1.EventEmitter {
|
|
|
778
986
|
turn.toolCalls = [];
|
|
779
987
|
turn.buttonRows = [];
|
|
780
988
|
turn.statusLines = [];
|
|
989
|
+
turn.lastPatchedAtByMessageId = {};
|
|
781
990
|
return;
|
|
782
991
|
}
|
|
783
992
|
if (event.type === 'typing_stop') {
|
|
@@ -796,15 +1005,7 @@ class LocalCoreLarkGateway extends node_events_1.EventEmitter {
|
|
|
796
1005
|
}
|
|
797
1006
|
if (event.type === 'buttons') {
|
|
798
1007
|
turn.awaitingPermission = true;
|
|
799
|
-
turn.buttonRows =
|
|
800
|
-
? event.buttonRows
|
|
801
|
-
.map((row) => Array.isArray(row)
|
|
802
|
-
? row
|
|
803
|
-
.filter((button) => Boolean(button?.text && button?.data))
|
|
804
|
-
.map((button) => ({ text: button.text, data: button.data }))
|
|
805
|
-
: [])
|
|
806
|
-
.filter((row) => row.length > 0)
|
|
807
|
-
: [];
|
|
1008
|
+
turn.buttonRows = [];
|
|
808
1009
|
if (this.mirrorPermissionStateInMainCard && content) {
|
|
809
1010
|
this.pushUnique(turn.statusLines, `等待确认: ${content}`);
|
|
810
1011
|
}
|
|
@@ -828,42 +1029,101 @@ class LocalCoreLarkGateway extends node_events_1.EventEmitter {
|
|
|
828
1029
|
turn.finalText = content;
|
|
829
1030
|
turn.previewText = content;
|
|
830
1031
|
}
|
|
831
|
-
|
|
832
|
-
const
|
|
833
|
-
if (
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
1032
|
+
renderBridgeEventMessage(turn, event) {
|
|
1033
|
+
const content = String(event.content || '').trim();
|
|
1034
|
+
if (event.type === 'preview_start' || event.type === 'update_message') {
|
|
1035
|
+
if (content.startsWith('💭 ')) {
|
|
1036
|
+
return this.renderProgressMessage(event.previewHandle || 'thinking-preview', content);
|
|
1037
|
+
}
|
|
1038
|
+
if (content.startsWith('🔧 ')) {
|
|
1039
|
+
return this.renderProgressMessage(this.progressKey('tool', event), content);
|
|
1040
|
+
}
|
|
1041
|
+
return {
|
|
1042
|
+
key: 'final',
|
|
1043
|
+
text: content,
|
|
1044
|
+
buttonRows: turn.buttonRows,
|
|
1045
|
+
isFinal: true,
|
|
1046
|
+
};
|
|
838
1047
|
}
|
|
839
|
-
|
|
840
|
-
|
|
1048
|
+
if (event.type === 'status') {
|
|
1049
|
+
return this.renderProgressMessage(this.progressKey('status', event), content.startsWith('⏳ ') ? content : `⏳ ${content}`);
|
|
841
1050
|
}
|
|
842
|
-
|
|
843
|
-
|
|
1051
|
+
if (event.type === 'reply') {
|
|
1052
|
+
if (content.startsWith('💭 ')) {
|
|
1053
|
+
return this.renderProgressMessage(this.progressKey('thinking', event), content);
|
|
1054
|
+
}
|
|
1055
|
+
if (content.startsWith('🔧 ')) {
|
|
1056
|
+
return this.renderProgressMessage(this.progressKey('tool', event), content);
|
|
1057
|
+
}
|
|
1058
|
+
if (content.startsWith('⏳ ') || content.startsWith('📤 ')) {
|
|
1059
|
+
return this.renderProgressMessage(this.progressKey('status', event), content);
|
|
1060
|
+
}
|
|
1061
|
+
return {
|
|
1062
|
+
key: 'final',
|
|
1063
|
+
text: content,
|
|
1064
|
+
buttonRows: turn.buttonRows,
|
|
1065
|
+
isFinal: true,
|
|
1066
|
+
};
|
|
844
1067
|
}
|
|
845
|
-
|
|
846
|
-
|
|
1068
|
+
if (event.type === 'typing_start' || event.type === 'typing_stop') {
|
|
1069
|
+
return { key: 'noop', text: '', buttonRows: [], isFinal: false };
|
|
847
1070
|
}
|
|
848
1071
|
return {
|
|
849
|
-
|
|
850
|
-
|
|
1072
|
+
key: 'noop',
|
|
1073
|
+
text: '',
|
|
1074
|
+
buttonRows: [],
|
|
1075
|
+
isFinal: false,
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
renderProgressMessage(key, text) {
|
|
1079
|
+
return {
|
|
1080
|
+
key,
|
|
1081
|
+
text,
|
|
1082
|
+
buttonRows: [],
|
|
1083
|
+
isFinal: false,
|
|
851
1084
|
};
|
|
852
1085
|
}
|
|
853
|
-
|
|
1086
|
+
progressKey(prefix, event) {
|
|
1087
|
+
const stableId = String(event.messageId || event.previewHandle || '').trim();
|
|
1088
|
+
if (stableId) {
|
|
1089
|
+
return `${prefix}:${stableId}`;
|
|
1090
|
+
}
|
|
1091
|
+
const content = String(event.content || '').trim().replace(/\s+/g, ' ');
|
|
1092
|
+
return `${prefix}:${content.slice(0, 120)}`;
|
|
1093
|
+
}
|
|
1094
|
+
getRenderedMessageId(turn, rendered) {
|
|
1095
|
+
if (rendered.isFinal) {
|
|
1096
|
+
return turn.finalMessageId || turn.messageId;
|
|
1097
|
+
}
|
|
1098
|
+
return turn.progressMessageIds[rendered.key];
|
|
1099
|
+
}
|
|
1100
|
+
setRenderedMessageId(turn, rendered, messageId) {
|
|
1101
|
+
if (rendered.isFinal) {
|
|
1102
|
+
turn.finalMessageId = messageId;
|
|
1103
|
+
turn.messageId = messageId;
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
turn.progressMessageIds[rendered.key] = messageId;
|
|
1107
|
+
}
|
|
1108
|
+
renderPermissionCard(turn, event, cardActionsEnabled) {
|
|
854
1109
|
const summary = this.buildPermissionSummary(turn, String(event.content || '').trim());
|
|
855
1110
|
const sections = [
|
|
856
1111
|
'**需要工具确认**',
|
|
857
1112
|
summary.command ? `\`${summary.command}\`` : '',
|
|
858
1113
|
summary.reason || '',
|
|
859
|
-
|
|
1114
|
+
cardActionsEnabled
|
|
1115
|
+
? '也可以直接回复:`allow` / `allow all` / `deny`'
|
|
1116
|
+
: '请直接回复:`allow` / `allow all` / `deny`',
|
|
860
1117
|
].filter(Boolean);
|
|
861
|
-
const buttonRows =
|
|
1118
|
+
const buttonRows = cardActionsEnabled && Array.isArray(event.buttonRows)
|
|
862
1119
|
? event.buttonRows
|
|
863
1120
|
.map((row) => Array.isArray(row)
|
|
864
1121
|
? row
|
|
865
1122
|
.filter((button) => Boolean(button?.text && button?.data))
|
|
866
|
-
.map((button) => ({
|
|
1123
|
+
.map((button) => ({
|
|
1124
|
+
text: this.formatPermissionButtonLabel(button),
|
|
1125
|
+
data: (0, desktop_js_1.normalizePermissionResponse)(button.data) || button.data,
|
|
1126
|
+
}))
|
|
867
1127
|
: [])
|
|
868
1128
|
.filter((row) => row.length > 0)
|
|
869
1129
|
: [];
|
|
@@ -892,7 +1152,7 @@ class LocalCoreLarkGateway extends node_events_1.EventEmitter {
|
|
|
892
1152
|
if (!value || value.action !== 'permission_response') {
|
|
893
1153
|
return;
|
|
894
1154
|
}
|
|
895
|
-
const response = String(value.response || '').trim();
|
|
1155
|
+
const response = (0, desktop_js_1.normalizePermissionResponse)(String(value.response || '').trim()) || String(value.response || '').trim();
|
|
896
1156
|
const threadId = String(value.thread_id || '').trim();
|
|
897
1157
|
if (!response || !threadId) {
|
|
898
1158
|
return;
|
|
@@ -918,6 +1178,29 @@ class LocalCoreLarkGateway extends node_events_1.EventEmitter {
|
|
|
918
1178
|
target.splice(0, target.length - 6);
|
|
919
1179
|
}
|
|
920
1180
|
}
|
|
1181
|
+
formatPermissionButtonLabel(button) {
|
|
1182
|
+
const response = (0, desktop_js_1.normalizePermissionResponse)(button.data) || (0, desktop_js_1.normalizePermissionResponse)(button.text);
|
|
1183
|
+
switch (response) {
|
|
1184
|
+
case 'allow':
|
|
1185
|
+
return '允许一次';
|
|
1186
|
+
case 'allow all':
|
|
1187
|
+
return '始终允许';
|
|
1188
|
+
case 'deny':
|
|
1189
|
+
return '拒绝';
|
|
1190
|
+
default:
|
|
1191
|
+
return button.text;
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
resolveLarkButtonType(button, index) {
|
|
1195
|
+
const response = (0, desktop_js_1.normalizePermissionResponse)(button.data) || (0, desktop_js_1.normalizePermissionResponse)(button.text);
|
|
1196
|
+
if (response === 'deny') {
|
|
1197
|
+
return 'danger';
|
|
1198
|
+
}
|
|
1199
|
+
if (response === 'allow') {
|
|
1200
|
+
return 'primary';
|
|
1201
|
+
}
|
|
1202
|
+
return index === 0 ? 'primary' : 'default';
|
|
1203
|
+
}
|
|
921
1204
|
buildPermissionSummary(turn, rawContent) {
|
|
922
1205
|
const lastTool = turn.toolCalls[turn.toolCalls.length - 1] || '';
|
|
923
1206
|
const [commandPart = '', reasonPart = ''] = lastTool.split(/\s+-\s+/, 2);
|