@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.
Files changed (62) hide show
  1. package/README.md +7 -0
  2. package/dist/renderer/assets/{Badge-DcFVSGwl.js → Badge-BRmQIqbd.js} +1 -1
  3. package/dist/renderer/assets/{Chat-CTy10fKR.js → Chat-D94ODABB.js} +1 -1
  4. package/dist/renderer/assets/{ChatMarkdown-C-ci0dQL.js → ChatMarkdown-DqW7OUvV.js} +1 -1
  5. package/dist/renderer/assets/{Config-Bl5vuqBS.js → Config-9lgzpTCp.js} +1 -1
  6. package/dist/renderer/assets/{CronList-BQOEXhHD.js → CronList-BYZ-x_bp.js} +1 -1
  7. package/dist/renderer/assets/{Dashboard-CauX4_SR.js → Dashboard-COISXbQy.js} +2 -2
  8. package/dist/renderer/assets/{EmptyState-DVYGGFl9.js → EmptyState-w5-I9zg7.js} +1 -1
  9. package/dist/renderer/assets/{KnowledgeDetail-BlV8Twrx.js → KnowledgeDetail-DVxwrTLw.js} +1 -1
  10. package/dist/renderer/assets/{KnowledgeHome-vd6AO-Fn.js → KnowledgeHome-BKkubcwi.js} +1 -1
  11. package/dist/renderer/assets/{Logs-BrtF26F7.js → Logs-D7HxZ8Nb.js} +1 -1
  12. package/dist/renderer/assets/{Modal-D6ucs8W2.js → Modal-Ceu_MP2e.js} +1 -1
  13. package/dist/renderer/assets/{Page-BdF9afpu.js → Page-CpSczAz8.js} +1 -1
  14. package/dist/renderer/assets/{ProjectDetail-RuoRHs1Q.js → ProjectDetail-FJ_bNXLF.js} +1 -1
  15. package/dist/renderer/assets/{ProjectList-r9MXhUu7.js → ProjectList-DZKL4K9C.js} +1 -1
  16. package/dist/renderer/assets/{Select-CEsAkZ5j.js → Select-BHQk6qCU.js} +1 -1
  17. package/dist/renderer/assets/{SessionChat-DwnZpTVu.js → SessionChat-DpgSXF3m.js} +1 -1
  18. package/dist/renderer/assets/{SessionList-C89Z6STB.js → SessionList-CU6drQ4N.js} +1 -1
  19. package/dist/renderer/assets/{ThreadChat-JB9GY3La.js → ThreadChat-D3s4GXGh.js} +1 -1
  20. package/dist/renderer/assets/Workspace-B7looTVo.js +14 -0
  21. package/dist/renderer/assets/{arrow-left-DuiS0IaQ.js → arrow-left-Dtal-eOi.js} +1 -1
  22. package/dist/renderer/assets/{book-open-B6l3y70b.js → book-open-q814R_Ov.js} +1 -1
  23. package/dist/renderer/assets/{check-CfewkzaJ.js → check-DjfCdLvw.js} +1 -1
  24. package/dist/renderer/assets/{circle-qzty4gzl.js → circle-vfhmyE8v.js} +1 -1
  25. package/dist/renderer/assets/{desktop-DTAjgvNs.js → desktop-BPIxWbkM.js} +1 -1
  26. package/dist/renderer/assets/desktop-DZHC4PDB.js +3 -0
  27. package/dist/renderer/assets/{funnel-CwjI0g4c.js → funnel-CUQibo1I.js} +1 -1
  28. package/dist/renderer/assets/{heart-CVHUi-lA.js → heart-DG9_b8P1.js} +1 -1
  29. package/dist/renderer/assets/{index-tQ6A4Zff.js → index-Bn8ane6_.js} +2 -2
  30. package/dist/renderer/assets/{index-BbDTSo1y.js → index-Ds0IN5rP.js} +1 -1
  31. package/dist/renderer/assets/{pencil-BvQPZxH6.js → pencil-Dij0LlRY.js} +1 -1
  32. package/dist/renderer/assets/{play-hJkkCcS6.js → play-CPFGjIyJ.js} +1 -1
  33. package/dist/renderer/assets/{plug-BGtbkjYJ.js → plug-DH3RKQyn.js} +1 -1
  34. package/dist/renderer/assets/{plus-DrYsrZyx.js → plus-afkK9_Y8.js} +1 -1
  35. package/dist/renderer/assets/projects-DTDS9SnT.js +1 -0
  36. package/dist/renderer/assets/{save-C8W3Wq2Z.js → save-B-cKgWcy.js} +1 -1
  37. package/dist/renderer/assets/{search-BuSByDes.js → search-DR6PeYkt.js} +1 -1
  38. package/dist/renderer/assets/{server-B9wEMLdo.js → server-BM6KOB5a.js} +1 -1
  39. package/dist/renderer/assets/{session-utils-K8E4S3Qx.js → session-utils-BDuThM0a.js} +1 -1
  40. package/dist/renderer/assets/{sessions-B-QTvESY.js → sessions-C1yKps4T.js} +1 -1
  41. package/dist/renderer/assets/{shield-check-C-oxstVp.js → shield-check-BFjMG-r2.js} +1 -1
  42. package/dist/renderer/assets/{trash-2-BQpE15-6.js → trash-2-CW1heMCf.js} +1 -1
  43. package/dist/renderer/assets/{user-CM4R9QVx.js → user-r_2sHG7m.js} +1 -1
  44. package/dist/renderer/assets/{wifi-off-CoaA7hVn.js → wifi-off-VMUokMpE.js} +1 -1
  45. package/dist/renderer/index.html +1 -1
  46. package/dist-electron/electron/agent-runtime-detector.test.js +11 -0
  47. package/dist-electron/electron/local-core-refactor.test.js +522 -0
  48. package/dist-electron/electron/plugin-kernel.test.js +77 -9
  49. package/dist-electron/services/local-ai-core/src/acp/local-core-acp-backend.js +9 -9
  50. package/dist-electron/services/local-ai-core/src/acp/local-core-acp-content.js +60 -0
  51. package/dist-electron/services/local-ai-core/src/gateway/local-core-lark-gateway.js +337 -54
  52. package/dist-electron/services/local-ai-core/src/gateway/local-core-weixin-gateway.js +50 -2
  53. package/dist-electron/services/local-ai-core/src/kernel/bootstrap.js +2 -1
  54. package/dist-electron/services/local-ai-core/src/plugins/builtin/agent-localcore-acp-plugin.js +9 -0
  55. package/dist-electron/services/local-ai-core/src/router/workspace-route-config.js +19 -8
  56. package/dist-electron/services/local-ai-core/src/runtime/agent-runtime-detector.js +23 -12
  57. package/dist-electron/services/local-ai-core/src/runtime/local-core-controller.js +34 -0
  58. package/dist-electron/shared/desktop.js +4 -2
  59. package/package.json +2 -1
  60. package/dist/renderer/assets/Workspace-Cftl8zps.js +0 -14
  61. package/dist/renderer/assets/desktop-D4Z7M9RM.js +0 -3
  62. 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 callbacks are unreliable in current WS-only setup (code 200340).
44
- // Keep permission approval on explicit text commands: allow all / allow / deny.
45
- enableCardActions = false;
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.renderTurnCard(turn);
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
- turn.messageId &&
300
- Date.now() - turn.lastPatchedAt < 900;
301
- this.options.log?.(`localcore-lark bridge event type=${event.type} sessionKey=${sessionKey} hasMessageId=${Boolean(turn.messageId)} throttle=${shouldThrottle}`);
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 (!turn.messageId) {
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.messageId = createdId;
309
- this.options.store.updatePlatformThreadMessageId(route.workspaceId, route.chatId, route.platformUserId, createdId);
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, turn.messageId, rendered.text, rendered.buttonRows, sessionKey, binding.thread_id);
319
+ await this.patchTextCard(state, existingMessageId, rendered.text, rendered.buttonRows, sessionKey, binding.thread_id);
315
320
  turn.lastPatchedAt = Date.now();
316
- this.options.log?.(`localcore-lark patched card message ${turn.messageId} for sessionKey=${sessionKey}`);
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
- await router.sendThreadMessage(threadId, (0, desktop_js_1.wrapUserMessageWithSchedulerProtocol)(input.text));
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
- await this.handleMessageEvent(binding.workspaceId, data);
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
- if (!text) {
595
- this.options.log?.(`localcore-lark ignored non-text message for ${workspaceId}: ${String(message.message_type || 'unknown')}`);
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(text.slice(0, 120))}`);
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.text,
861
+ content: this.formatPermissionButtonLabel(button),
660
862
  },
661
- type: index === 0 ? 'primary' : 'default',
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 = this.enableCardActions && Array.isArray(event.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
- renderTurnCard(turn) {
832
- const sections = [];
833
- if (turn.thinkingSteps.length > 0) {
834
- sections.push(`**思考过程**\n${turn.thinkingSteps.map((step) => `• ${step.replace(/\s+/g, ' ').trim()}`).join('\n')}`);
835
- }
836
- if (turn.finalText) {
837
- sections.push(turn.finalText);
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
- else if (turn.previewText) {
840
- sections.push(turn.previewText);
1048
+ if (event.type === 'status') {
1049
+ return this.renderProgressMessage(this.progressKey('status', event), content.startsWith('⏳ ') ? content : `⏳ ${content}`);
841
1050
  }
842
- else if (turn.processing && turn.toolCalls.length > 0) {
843
- sections.push(`**处理中**\n${this.formatCompactToolLines(turn.toolCalls).join('\n')}`);
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
- else if (turn.processing) {
846
- sections.push('**处理中**\n正在思考...');
1068
+ if (event.type === 'typing_start' || event.type === 'typing_stop') {
1069
+ return { key: 'noop', text: '', buttonRows: [], isFinal: false };
847
1070
  }
848
1071
  return {
849
- text: sections.join('\n\n').trim(),
850
- buttonRows: turn.buttonRows,
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
- renderPermissionCard(turn, event) {
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
- '回复:`allow` / `allow all` / `deny`',
1114
+ cardActionsEnabled
1115
+ ? '也可以直接回复:`allow` / `allow all` / `deny`'
1116
+ : '请直接回复:`allow` / `allow all` / `deny`',
860
1117
  ].filter(Boolean);
861
- const buttonRows = this.enableCardActions && Array.isArray(event.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) => ({ text: button.text, data: button.data }))
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);