@kafca/agentdock 0.1.20 → 0.1.22

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 (73) hide show
  1. package/README.md +13 -0
  2. package/dist/renderer/assets/{Badge-C1f8JpRb.js → Badge-B_rgzxYO.js} +1 -1
  3. package/dist/renderer/assets/{Chat-S50iFg3I.js → Chat-CYtp6m7T.js} +1 -1
  4. package/dist/renderer/assets/{ChatMarkdown-VWL9Vnpz.js → ChatMarkdown-DOBMwa9d.js} +1 -1
  5. package/dist/renderer/assets/{Config-Yc9ExUI6.js → Config-DppMSPHF.js} +2 -2
  6. package/dist/renderer/assets/CronList-BDtB1SuA.js +1 -0
  7. package/dist/renderer/assets/{Dashboard-CzXCvZVR.js → Dashboard-iFlRnd7Y.js} +2 -2
  8. package/dist/renderer/assets/{EmptyState-9FbY2o6z.js → EmptyState-bKDt_T88.js} +1 -1
  9. package/dist/renderer/assets/{KnowledgeDetail-D_BDc4Zt.js → KnowledgeDetail-CKPhKPIg.js} +2 -2
  10. package/dist/renderer/assets/{KnowledgeHome-DsUA_BCR.js → KnowledgeHome-CFeJre6i.js} +3 -3
  11. package/dist/renderer/assets/Logs-CyxSVOFK.js +1 -0
  12. package/dist/renderer/assets/{Modal-Bt5VBC6S.js → Modal-CxPnk1Ey.js} +1 -1
  13. package/dist/renderer/assets/{Page-Da_Kc6Qm.js → Page-kU9vubzj.js} +1 -1
  14. package/dist/renderer/assets/{ProjectDetail-B4Ps80yv.js → ProjectDetail-D9FY9NWZ.js} +1 -1
  15. package/dist/renderer/assets/{ProjectList-BWTP9npz.js → ProjectList-DIwr2TXg.js} +1 -1
  16. package/dist/renderer/assets/{Select-DWs3hVUh.js → Select-BaWoz5Dn.js} +2 -7
  17. package/dist/renderer/assets/SessionChat-Cm9CW8Zx.js +1 -0
  18. package/dist/renderer/assets/SessionList-UPnWlXXO.js +1 -0
  19. package/dist/renderer/assets/{ThreadChat-nhnikHnk.js → ThreadChat-YvVsACW3.js} +7 -7
  20. package/dist/renderer/assets/{Workspace-pq_JYdTN.js → Workspace-ST7V8u6T.js} +2 -2
  21. package/dist/renderer/assets/{arrow-left-CR39hNvq.js → arrow-left-BmZAVGV7.js} +1 -1
  22. package/dist/renderer/assets/{book-open-Bot2WKl8.js → book-open-DZ3GXFgI.js} +1 -1
  23. package/dist/renderer/assets/{check-Drbl2nU3.js → check-CeVSvDCv.js} +1 -1
  24. package/dist/renderer/assets/chevron-down-yryuMoQ-.js +6 -0
  25. package/dist/renderer/assets/{circle-BXFfAwnA.js → circle-eASsFISH.js} +1 -1
  26. package/dist/renderer/assets/desktop-Che1cbpE.js +4 -0
  27. package/dist/renderer/assets/desktop-DkrP_s-b.js +1 -0
  28. package/dist/renderer/assets/{funnel-BNKYan_N.js → funnel-pwAe2Ixp.js} +1 -1
  29. package/dist/renderer/assets/{heart-wUOSwQE-.js → heart-B2Sp-bCm.js} +1 -1
  30. package/dist/renderer/assets/{index-DT5la73R.js → index-BC3MWbS1.js} +3 -3
  31. package/dist/renderer/assets/{index-CmOphqrf.js → index-Cse2Br64.js} +1 -1
  32. package/dist/renderer/assets/{index-3CzgyQYB.css → index-DFiJR0vv.css} +1 -1
  33. package/dist/renderer/assets/{pencil-D7-8ggue.js → pencil-BfEunUF3.js} +1 -1
  34. package/dist/renderer/assets/{play-BCYOAfet.js → play-Dcs9Lj_Z.js} +1 -1
  35. package/dist/renderer/assets/{plug-Dsik74Yl.js → plug-B_YschQe.js} +1 -1
  36. package/dist/renderer/assets/{plus-CKrU26_l.js → plus-Pqo-cXHQ.js} +1 -1
  37. package/dist/renderer/assets/projects-BCj0k_Gy.js +1 -0
  38. package/dist/renderer/assets/{save-q7M2_eAl.js → save-BKM5yi1A.js} +1 -1
  39. package/dist/renderer/assets/{search-BptY4_MT.js → search-BU42h10M.js} +1 -1
  40. package/dist/renderer/assets/{server-DSSEF0D8.js → server-DA5t3Rtr.js} +1 -1
  41. package/dist/renderer/assets/{session-utils-BFuqPZ5x.js → session-utils-Cr9VhNSq.js} +1 -1
  42. package/dist/renderer/assets/{sessions-CrWo3g3u.js → sessions-bvV8ZMfX.js} +1 -1
  43. package/dist/renderer/assets/{shield-check-CCU722RQ.js → shield-check-BeOJKur_.js} +1 -1
  44. package/dist/renderer/assets/{trash-2-DaeeiV8x.js → trash-2-CQ7HKO3q.js} +1 -1
  45. package/dist/renderer/assets/{user-Bkz8J7Yw.js → user-CtFjY9SV.js} +1 -1
  46. package/dist/renderer/assets/{wifi-off-P8xdIOHU.js → wifi-off-Ci5m0ysf.js} +1 -1
  47. package/dist/renderer/index.html +2 -2
  48. package/dist-electron/electron/agent-runtime-detector.test.js +11 -0
  49. package/dist-electron/electron/local-core-refactor.test.js +403 -3
  50. package/dist-electron/electron/plugin-kernel.test.js +43 -9
  51. package/dist-electron/services/local-ai-core/src/acp/local-core-acp-backend.js +9 -9
  52. package/dist-electron/services/local-ai-core/src/acp/local-core-acp-content.js +60 -0
  53. package/dist-electron/services/local-ai-core/src/{gateway → channel/lark}/local-core-lark-gateway.js +371 -13
  54. package/dist-electron/services/local-ai-core/src/channel/shared/file-utils.js +33 -0
  55. package/dist-electron/services/local-ai-core/src/{gateway → channel/weixin}/local-core-weixin-gateway.js +238 -3
  56. package/dist-electron/services/local-ai-core/src/cli/lac.js +41 -1
  57. package/dist-electron/services/local-ai-core/src/kernel/bootstrap.js +2 -1
  58. package/dist-electron/services/local-ai-core/src/plugins/builtin/agent-localcore-acp-plugin.js +9 -0
  59. package/dist-electron/services/local-ai-core/src/plugins/builtin/channel-lark-plugin.js +1 -1
  60. package/dist-electron/services/local-ai-core/src/plugins/builtin/channel-weixin-plugin.js +1 -1
  61. package/dist-electron/services/local-ai-core/src/router/workspace-route-config.js +19 -8
  62. package/dist-electron/services/local-ai-core/src/runtime/agent-runtime-detector.js +23 -12
  63. package/dist-electron/services/local-ai-core/src/runtime/local-core-controller.js +41 -0
  64. package/dist-electron/services/local-ai-core/src/runtime/server.js +10 -0
  65. package/dist-electron/shared/desktop.js +17 -2
  66. package/package.json +2 -1
  67. package/dist/renderer/assets/CronList-DTboWazi.js +0 -1
  68. package/dist/renderer/assets/Logs-DDAmR67f.js +0 -1
  69. package/dist/renderer/assets/SessionChat-DOhUnOfH.js +0 -1
  70. package/dist/renderer/assets/SessionList-ChZYFXuo.js +0 -1
  71. package/dist/renderer/assets/desktop-5k3W66fH.js +0 -1
  72. package/dist/renderer/assets/desktop-D4Z7M9RM.js +0 -3
  73. package/dist/renderer/assets/projects-CGDtNb0M.js +0 -1
@@ -7,6 +7,7 @@ const local_core_acp_transport_js_1 = require("./local-core-acp-transport.js");
7
7
  const local_core_acp_turn_coordinator_js_1 = require("./local-core-acp-turn-coordinator.js");
8
8
  const local_core_acp_session_coordinator_js_1 = require("./local-core-acp-session-coordinator.js");
9
9
  const local_core_acp_response_processor_js_1 = require("./local-core-acp-response-processor.js");
10
+ const local_core_acp_content_js_1 = require("./local-core-acp-content.js");
10
11
  const command_risk_js_1 = require("../security/command-risk.js");
11
12
  const ACP_PROMPT_TIMEOUT_MS = 15 * 60 * 1000;
12
13
  class LocalCoreAcpBackend {
@@ -126,7 +127,7 @@ class LocalCoreAcpBackend {
126
127
  this.options.store.deleteThread(threadId);
127
128
  return { deleted: true };
128
129
  }
129
- async sendThreadMessage(threadId, content, config) {
130
+ async sendThreadMessage(threadId, input, config) {
130
131
  if (!config) {
131
132
  throw new Error('localcore-acp message send requires a workspace config.');
132
133
  }
@@ -134,6 +135,8 @@ class LocalCoreAcpBackend {
134
135
  if (!row) {
135
136
  throw new Error(`Thread not found: ${threadId}`);
136
137
  }
138
+ const message = (0, local_core_acp_content_js_1.normalizeThreadMessageInput)(input);
139
+ const content = message.displayText;
137
140
  this.options.store.appendMessage(threadId, 'user', content, 'final');
138
141
  this.options.eventBus.emit({
139
142
  type: 'thread.message.accepted',
@@ -169,7 +172,7 @@ class LocalCoreAcpBackend {
169
172
  sessionKey: row.bridge_session_key,
170
173
  },
171
174
  });
172
- void this.runPrompt(threadId, runId, row.bridge_session_key, config, content).catch((error) => {
175
+ void this.runPrompt(threadId, runId, row.bridge_session_key, config, message).catch((error) => {
173
176
  this.options.log?.(`localcore-acp prompt failed for ${threadId}: ${error instanceof Error ? error.message : String(error)}`);
174
177
  });
175
178
  return { runId };
@@ -219,7 +222,7 @@ class LocalCoreAcpBackend {
219
222
  async interruptRun(runId) {
220
223
  return this.sessionCoordinator.interruptRun(runId);
221
224
  }
222
- async runPrompt(threadId, runId, bridgeSessionKey, config, content) {
225
+ async runPrompt(threadId, runId, bridgeSessionKey, config, input) {
223
226
  const row = this.options.store.getThreadRow(threadId);
224
227
  if (!row) {
225
228
  throw new Error(`Thread not found: ${threadId}`);
@@ -229,6 +232,8 @@ class LocalCoreAcpBackend {
229
232
  sessionKey: bridgeSessionKey,
230
233
  replyCtx: runId,
231
234
  });
235
+ const message = (0, local_core_acp_content_js_1.normalizeThreadMessageInput)(input);
236
+ const content = message.displayText;
232
237
  let session = null;
233
238
  try {
234
239
  session = await this.sessionCoordinator.ensureSession(threadId, bridgeSessionKey, config);
@@ -256,12 +261,7 @@ class LocalCoreAcpBackend {
256
261
  const promptPromise = this.transport.request(session, 'session/prompt', {
257
262
  sessionId: session.sessionId,
258
263
  messageId: (0, node_crypto_1.randomUUID)(),
259
- prompt: [
260
- {
261
- type: 'text',
262
- text: content,
263
- },
264
- ],
264
+ prompt: message.contentParts,
265
265
  }, ACP_PROMPT_TIMEOUT_MS);
266
266
  session.promptPromise = promptPromise;
267
267
  const result = await promptPromise;
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.normalizeThreadMessageInput = normalizeThreadMessageInput;
4
+ exports.createTextMessageInput = createTextMessageInput;
5
+ function normalizeThreadMessageInput(input) {
6
+ if (typeof input === 'string') {
7
+ return {
8
+ displayText: input,
9
+ contentParts: [{ type: 'text', text: input }],
10
+ };
11
+ }
12
+ const displayText = String(input.displayText || '').trim();
13
+ const contentParts = Array.isArray(input.contentParts)
14
+ ? input.contentParts.filter(isSupportedContentPart)
15
+ : [];
16
+ if (contentParts.length > 0) {
17
+ return {
18
+ displayText: displayText || summarizeContentParts(contentParts),
19
+ contentParts,
20
+ };
21
+ }
22
+ const fallbackText = displayText || '';
23
+ return {
24
+ displayText: fallbackText,
25
+ contentParts: [{ type: 'text', text: fallbackText }],
26
+ };
27
+ }
28
+ function createTextMessageInput(text) {
29
+ return normalizeThreadMessageInput(text);
30
+ }
31
+ function isSupportedContentPart(part) {
32
+ if (!part || typeof part !== 'object') {
33
+ return false;
34
+ }
35
+ const type = String(part.type || '');
36
+ if (type === 'text') {
37
+ return Boolean(String(part.text || ''));
38
+ }
39
+ if (type === 'image') {
40
+ const image = part;
41
+ return Boolean(String(image.data || image.uri || ''));
42
+ }
43
+ return false;
44
+ }
45
+ function summarizeContentParts(parts) {
46
+ const text = parts
47
+ .filter((part) => part.type === 'text')
48
+ .map((part) => part.text.trim())
49
+ .filter(Boolean)
50
+ .join('\n\n')
51
+ .trim();
52
+ if (text) {
53
+ return text;
54
+ }
55
+ const imageCount = parts.filter((part) => part.type === 'image').length;
56
+ if (imageCount > 0) {
57
+ return imageCount === 1 ? '[Image]' : `[${imageCount} images]`;
58
+ }
59
+ return '';
60
+ }
@@ -36,8 +36,21 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.LocalCoreLarkGateway = void 0;
37
37
  const node_events_1 = require("node:events");
38
38
  const node_crypto_1 = require("node:crypto");
39
- const desktop_js_1 = require("../../../../shared/desktop.js");
39
+ const node_fs_1 = require("node:fs");
40
+ const node_path_1 = require("node:path");
41
+ const desktop_js_1 = require("../../../../../shared/desktop.js");
42
+ const file_utils_js_1 = require("../shared/file-utils.js");
43
+ function foldToolResultForLark(content) {
44
+ const normalized = String(content || '').trim();
45
+ if (!normalized.startsWith('🔧 '))
46
+ return normalized;
47
+ const parts = normalized.split(' - ');
48
+ if (parts.length <= 2)
49
+ return normalized;
50
+ return parts.slice(0, 2).join(' - ');
51
+ }
40
52
  const PAIRING_EXPIRY_MS = 10 * 60 * 1000;
53
+ const LARK_MAX_UPLOAD_FILE_SIZE = 30 * 1024 * 1024;
41
54
  class LocalCoreLarkGateway extends node_events_1.EventEmitter {
42
55
  options;
43
56
  // Lark returns 200340 when card action events are not enabled in the app's
@@ -115,6 +128,111 @@ class LocalCoreLarkGateway extends node_events_1.EventEmitter {
115
128
  async sendScheduledMessage(workspaceId, route, text) {
116
129
  return this.sendScheduledCard(workspaceId, route.channelId, text);
117
130
  }
131
+ async sendOutboundMessage(workspaceId, input) {
132
+ const state = this.runtime.get(workspaceId);
133
+ if (!state?.client || !state.connected) {
134
+ throw new Error(`Lark workspace is not connected: ${workspaceId}`);
135
+ }
136
+ const channelId = String(input.route?.channelId || '').trim();
137
+ if (!channelId) {
138
+ throw new Error('Missing Lark target channel id');
139
+ }
140
+ const messageIds = [];
141
+ const attachments = [];
142
+ for (const part of input.parts || []) {
143
+ if (part.type === 'text') {
144
+ const text = String(part.text || '').trim();
145
+ if (text) {
146
+ messageIds.push(await this.sendTextAsCard(state, channelId, text));
147
+ }
148
+ continue;
149
+ }
150
+ if (part.type === 'file') {
151
+ const sent = await this.sendFilePart(state, channelId, part);
152
+ messageIds.push(sent.messageId);
153
+ attachments.push({
154
+ kind: 'file',
155
+ attachmentId: sent.fileKey,
156
+ fileName: sent.file.fileName,
157
+ fileSize: sent.file.fileSize,
158
+ metadata: {
159
+ fileKey: sent.fileKey,
160
+ },
161
+ });
162
+ }
163
+ }
164
+ return {
165
+ platform: 'lark',
166
+ workspaceId,
167
+ channelId,
168
+ participantId: input.route.participantId,
169
+ messageIds: messageIds.filter(Boolean),
170
+ attachments,
171
+ };
172
+ }
173
+ async sendFile(workspaceId, input) {
174
+ const result = await this.sendOutboundMessage(workspaceId, {
175
+ route: {
176
+ type: 'channel.chat',
177
+ channelId: input.channelId,
178
+ participantId: input.participantId,
179
+ },
180
+ parts: [{
181
+ type: 'file',
182
+ path: input.path,
183
+ fileName: input.fileName,
184
+ }],
185
+ });
186
+ const attachment = result.attachments?.[0];
187
+ return {
188
+ platform: 'lark',
189
+ workspaceId,
190
+ channelId: result.channelId,
191
+ messageId: result.messageIds[0] || '',
192
+ fileKey: String(attachment?.metadata?.fileKey || attachment?.attachmentId || ''),
193
+ fileName: attachment?.fileName || input.fileName || '',
194
+ fileSize: attachment?.fileSize || 0,
195
+ };
196
+ }
197
+ async sendFilePart(state, channelId, part) {
198
+ const file = await (0, file_utils_js_1.prepareChannelFile)({
199
+ path: part.path,
200
+ fileName: part.fileName,
201
+ maxBytes: LARK_MAX_UPLOAD_FILE_SIZE,
202
+ platformLabel: 'Lark',
203
+ });
204
+ const upload = await state.client.im.file.create({
205
+ data: {
206
+ file_type: this.resolveLarkUploadFileType(file.fileName),
207
+ file_name: file.fileName,
208
+ file: (0, node_fs_1.createReadStream)(file.path),
209
+ },
210
+ });
211
+ const fileKey = String(upload?.file_key || upload?.data?.file_key || '').trim();
212
+ if (!fileKey) {
213
+ throw new Error('Lark file upload did not return a file key');
214
+ }
215
+ const response = await state.client.im.message.create({
216
+ params: {
217
+ receive_id_type: this.resolveReceiveIdType(channelId),
218
+ },
219
+ data: {
220
+ receive_id: channelId,
221
+ msg_type: 'file',
222
+ content: JSON.stringify({ file_key: fileKey }),
223
+ },
224
+ });
225
+ const messageId = String(response?.data?.message_id || '').trim();
226
+ if (!messageId) {
227
+ throw new Error('Lark file message did not return a message id');
228
+ }
229
+ this.options.log?.(`localcore-lark sent file ${file.fileName} (${file.fileSize} bytes) to ${channelId}`);
230
+ return {
231
+ messageId,
232
+ fileKey,
233
+ file,
234
+ };
235
+ }
118
236
  muteThreadBridge(threadId) {
119
237
  const current = this.mutedThreadBridgeCounts.get(threadId) || 0;
120
238
  this.mutedThreadBridgeCounts.set(threadId, current + 1);
@@ -453,7 +571,8 @@ class LocalCoreLarkGateway extends node_events_1.EventEmitter {
453
571
  return { paired: true, threadId };
454
572
  }
455
573
  this.options.store.clearPlatformThreadMessageId(input.workspaceId, input.chatId, input.platformUserId);
456
- await router.sendThreadMessage(threadId, (0, desktop_js_1.wrapUserMessageWithSchedulerProtocol)(input.text));
574
+ const wrappedText = (0, desktop_js_1.wrapUserMessageWithSchedulerProtocol)(input.text);
575
+ await router.sendThreadMessage(threadId, this.createThreadMessageInput(wrappedText, input.contentParts));
457
576
  return { paired: true, threadId };
458
577
  }
459
578
  close() {
@@ -610,18 +729,41 @@ class LocalCoreLarkGateway extends node_events_1.EventEmitter {
610
729
  catch {
611
730
  parsedContent = {};
612
731
  }
732
+ const messageType = String(message.message_type || '').trim().toLowerCase();
613
733
  const text = String(parsedContent.text || '').trim();
614
- if (!text) {
615
- this.options.log?.(`localcore-lark ignored non-text message for ${workspaceId}: type=${String(message.message_type || 'unknown')} contentKeys=${JSON.stringify(Object.keys(parsedContent))}`);
734
+ const contentParts = [];
735
+ if (text) {
736
+ contentParts.push({ type: 'text', text });
737
+ }
738
+ if (messageType === 'image') {
739
+ const imageKey = String(parsedContent.image_key || parsedContent.file_key || '').trim();
740
+ if (imageKey) {
741
+ try {
742
+ contentParts.push(await this.downloadMessageImage(workspaceId, String(message.message_id || ''), imageKey));
743
+ }
744
+ catch (error) {
745
+ const errorText = `[Image download failed: ${error instanceof Error ? error.message : String(error)}]`;
746
+ contentParts.push({ type: 'text', text: errorText });
747
+ 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)}`);
748
+ }
749
+ }
750
+ }
751
+ if (contentParts.length === 0) {
752
+ this.options.log?.(`localcore-lark ignored unsupported message for ${workspaceId}: type=${String(message.message_type || 'unknown')} contentKeys=${JSON.stringify(Object.keys(parsedContent))}`);
616
753
  return;
617
754
  }
755
+ const displayText = text || (contentParts.some((part) => part.type === 'image') ? '[Image]' : contentParts
756
+ .filter((part) => part.type === 'text')
757
+ .map((part) => part.text)
758
+ .join('\n\n')
759
+ .trim());
618
760
  const platformUserId = String(sender.sender_id?.user_id || sender.sender_id?.open_id || '').trim();
619
761
  const chatId = String(message.chat_id || platformUserId).trim();
620
762
  if (!platformUserId || !chatId) {
621
763
  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 || '')}`);
622
764
  return;
623
765
  }
624
- this.options.log?.(`localcore-lark inbound message for ${workspaceId}: chat=${chatId} user=${platformUserId} text=${JSON.stringify(text.slice(0, 120))}`);
766
+ this.options.log?.(`localcore-lark inbound message for ${workspaceId}: chat=${chatId} user=${platformUserId} type=${messageType || 'unknown'} text=${JSON.stringify(displayText.slice(0, 120))}`);
625
767
  await this.handleInboundMessage({
626
768
  workspaceId,
627
769
  platformUserId,
@@ -629,10 +771,103 @@ class LocalCoreLarkGateway extends node_events_1.EventEmitter {
629
771
  displayName: String(payload?.sender?.sender_id?.user_id ||
630
772
  payload?.sender?.sender_id?.open_id ||
631
773
  `Lark ${platformUserId.slice(-6)}`),
632
- text,
774
+ text: displayText,
633
775
  messageId: String(message.message_id || (0, node_crypto_1.randomUUID)()),
776
+ contentParts,
634
777
  });
635
778
  }
779
+ createWrappedContentParts(wrappedText, parts) {
780
+ const nonTextParts = Array.isArray(parts)
781
+ ? parts.filter((part) => part.type !== 'text')
782
+ : [];
783
+ return [
784
+ { type: 'text', text: wrappedText },
785
+ ...nonTextParts,
786
+ ];
787
+ }
788
+ createThreadMessageInput(wrappedText, parts) {
789
+ const hasNonTextPart = Array.isArray(parts) && parts.some((part) => part.type !== 'text');
790
+ if (!hasNonTextPart) {
791
+ return wrappedText;
792
+ }
793
+ return {
794
+ displayText: wrappedText,
795
+ contentParts: this.createWrappedContentParts(wrappedText, parts),
796
+ };
797
+ }
798
+ async downloadMessageImage(workspaceId, messageId, imageKey) {
799
+ const state = this.runtime.get(workspaceId);
800
+ if (!state?.client) {
801
+ throw new Error('Lark client is not connected');
802
+ }
803
+ if (!messageId) {
804
+ throw new Error('Lark image message is missing message_id');
805
+ }
806
+ const resource = await state.client.im.messageResource.get({
807
+ path: {
808
+ message_id: messageId,
809
+ file_key: imageKey,
810
+ },
811
+ params: {
812
+ type: 'image',
813
+ },
814
+ });
815
+ const buffer = await this.readLarkResourceBuffer(resource);
816
+ if (buffer.length === 0) {
817
+ throw new Error('Lark image resource is empty');
818
+ }
819
+ const headerMime = this.extractHeaderMimeType(resource?.headers);
820
+ return {
821
+ type: 'image',
822
+ data: buffer.toString('base64'),
823
+ mimeType: headerMime || this.sniffImageMimeType(buffer),
824
+ fileName: `${imageKey}.${this.sniffImageExtension(buffer)}`,
825
+ };
826
+ }
827
+ async readLarkResourceBuffer(resource) {
828
+ const readable = resource?.getReadableStream?.();
829
+ if (!readable || typeof readable.on !== 'function') {
830
+ throw new Error('Lark resource did not provide a readable stream');
831
+ }
832
+ const chunks = [];
833
+ await new Promise((resolve, reject) => {
834
+ readable.on('data', (chunk) => {
835
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
836
+ });
837
+ readable.on('error', reject);
838
+ readable.on('end', resolve);
839
+ });
840
+ return Buffer.concat(chunks);
841
+ }
842
+ extractHeaderMimeType(headers) {
843
+ const value = headers && typeof headers === 'object'
844
+ ? headers['content-type'] || headers['Content-Type']
845
+ : '';
846
+ return String(value || '').split(';')[0]?.trim() || '';
847
+ }
848
+ sniffImageMimeType(buffer) {
849
+ if (buffer[0] === 0xff && buffer[1] === 0xd8)
850
+ return 'image/jpeg';
851
+ if (buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4e && buffer[3] === 0x47)
852
+ return 'image/png';
853
+ if (buffer[0] === 0x47 && buffer[1] === 0x49 && buffer[2] === 0x46)
854
+ return 'image/gif';
855
+ if (buffer[0] === 0x52 && buffer[1] === 0x49 && buffer[2] === 0x46 && buffer[3] === 0x46)
856
+ return 'image/webp';
857
+ return 'application/octet-stream';
858
+ }
859
+ sniffImageExtension(buffer) {
860
+ const mimeType = this.sniffImageMimeType(buffer);
861
+ if (mimeType === 'image/jpeg')
862
+ return 'jpg';
863
+ if (mimeType === 'image/png')
864
+ return 'png';
865
+ if (mimeType === 'image/gif')
866
+ return 'gif';
867
+ if (mimeType === 'image/webp')
868
+ return 'webp';
869
+ return 'bin';
870
+ }
636
871
  attachWsDiagnostics(workspaceId, wsClient) {
637
872
  const on = typeof wsClient?.on === 'function' ? wsClient.on.bind(wsClient) : null;
638
873
  if (!on) {
@@ -764,10 +999,9 @@ class LocalCoreLarkGateway extends node_events_1.EventEmitter {
764
999
  };
765
1000
  }
766
1001
  async sendTextAsCard(state, chatId, text, buttonRows = [], sessionKey, threadId) {
767
- const receiveIdType = chatId.startsWith('oc_') ? 'chat_id' : chatId.startsWith('ou_') ? 'open_id' : 'user_id';
768
1002
  const response = await state.client.im.message.create({
769
1003
  params: {
770
- receive_id_type: receiveIdType,
1004
+ receive_id_type: this.resolveReceiveIdType(chatId),
771
1005
  },
772
1006
  data: {
773
1007
  receive_id: chatId,
@@ -777,6 +1011,33 @@ class LocalCoreLarkGateway extends node_events_1.EventEmitter {
777
1011
  });
778
1012
  return String(response?.data?.message_id || '').trim();
779
1013
  }
1014
+ resolveReceiveIdType(receiveId) {
1015
+ return receiveId.startsWith('oc_') ? 'chat_id' : receiveId.startsWith('ou_') ? 'open_id' : 'user_id';
1016
+ }
1017
+ resolveLarkUploadFileType(fileName) {
1018
+ switch ((0, node_path_1.extname)(fileName).toLowerCase()) {
1019
+ case '.opus':
1020
+ return 'opus';
1021
+ case '.mp4':
1022
+ case '.mov':
1023
+ case '.m4v':
1024
+ return 'mp4';
1025
+ case '.pdf':
1026
+ return 'pdf';
1027
+ case '.doc':
1028
+ case '.docx':
1029
+ return 'doc';
1030
+ case '.xls':
1031
+ case '.xlsx':
1032
+ case '.csv':
1033
+ return 'xls';
1034
+ case '.ppt':
1035
+ case '.pptx':
1036
+ return 'ppt';
1037
+ default:
1038
+ return 'stream';
1039
+ }
1040
+ }
780
1041
  async patchTextCard(state, messageId, text, buttonRows = [], sessionKey, threadId) {
781
1042
  await state.client.im.message.patch({
782
1043
  path: {
@@ -854,7 +1115,7 @@ class LocalCoreLarkGateway extends node_events_1.EventEmitter {
854
1115
  return turn;
855
1116
  }
856
1117
  consumeBridgeEvent(turn, event) {
857
- const content = String(event.content || '').trim();
1118
+ const content = foldToolResultForLark(String(event.content || '').trim());
858
1119
  if (event.type === 'typing_start') {
859
1120
  // Always start a fresh card for each generation phase.
860
1121
  // This keeps Feishu message order aligned with user-visible timeline.
@@ -913,7 +1174,7 @@ class LocalCoreLarkGateway extends node_events_1.EventEmitter {
913
1174
  turn.previewText = content;
914
1175
  }
915
1176
  renderBridgeEventMessage(turn, event) {
916
- const content = String(event.content || '').trim();
1177
+ const content = foldToolResultForLark(String(event.content || '').trim());
917
1178
  if (event.type === 'preview_start' || event.type === 'update_message') {
918
1179
  if (content.startsWith('💭 ')) {
919
1180
  return this.renderProgressMessage(event.previewHandle || 'thinking-preview', content);
@@ -1042,12 +1303,107 @@ class LocalCoreLarkGateway extends node_events_1.EventEmitter {
1042
1303
  }
1043
1304
  const router = this.options.getWorkspaceRouter();
1044
1305
  await router.sendThreadAction(threadId, response);
1306
+ await this.markPermissionCardActionHandled(workspaceId, data, payload, value, response, threadId);
1045
1307
  this.options.log?.(`localcore-lark processed card action for ${workspaceId}: ${response}`);
1046
1308
  }
1047
1309
  catch (error) {
1048
1310
  this.options.log?.(`localcore-lark card action failed for ${workspaceId}: ${error instanceof Error ? error.message : String(error)}`);
1049
1311
  }
1050
1312
  }
1313
+ async markPermissionCardActionHandled(workspaceId, rootPayload, payload, value, response, threadId) {
1314
+ const state = this.runtime.get(workspaceId);
1315
+ if (!state?.client) {
1316
+ return;
1317
+ }
1318
+ const sessionKey = String(value.session_key || '').trim();
1319
+ const turn = sessionKey ? this.outboundTurns.get(sessionKey) : undefined;
1320
+ const messageId = this.extractCardActionMessageId(rootPayload, payload, value) || turn?.permissionMessageId;
1321
+ if (!messageId) {
1322
+ this.options.log?.(`localcore-lark could not find permission card message id for workspace=${workspaceId} sessionKey=${sessionKey || 'missing'}`);
1323
+ return;
1324
+ }
1325
+ try {
1326
+ await this.patchTextCard(state, messageId, [
1327
+ '**工具确认已处理**',
1328
+ `已选择:${this.formatPermissionResponseLabel(response)}`,
1329
+ '等待代理继续执行...',
1330
+ ].join('\n\n'), [], sessionKey || undefined, threadId);
1331
+ if (turn) {
1332
+ turn.awaitingPermission = false;
1333
+ turn.buttonRows = [];
1334
+ }
1335
+ }
1336
+ catch (error) {
1337
+ this.options.log?.(`localcore-lark failed to patch handled permission card ${messageId}: ${error instanceof Error ? error.message : String(error)}`);
1338
+ }
1339
+ }
1340
+ extractCardActionMessageId(...payloads) {
1341
+ for (const payload of payloads) {
1342
+ const messageId = this.extractKnownCardActionMessageId(payload);
1343
+ if (messageId) {
1344
+ return messageId;
1345
+ }
1346
+ }
1347
+ for (const payload of payloads) {
1348
+ const messageId = this.findNestedCardActionMessageId(payload);
1349
+ if (messageId) {
1350
+ return messageId;
1351
+ }
1352
+ }
1353
+ return '';
1354
+ }
1355
+ extractKnownCardActionMessageId(payload) {
1356
+ const source = payload;
1357
+ if (!source || typeof source !== 'object') {
1358
+ return '';
1359
+ }
1360
+ const candidates = [
1361
+ source?.permission_message_id,
1362
+ source?.action_message_id,
1363
+ source?.context?.open_message_id,
1364
+ source?.context?.message_id,
1365
+ source?.event?.context?.open_message_id,
1366
+ source?.event?.context?.message_id,
1367
+ source?.event_context?.open_message_id,
1368
+ source?.event_context?.message_id,
1369
+ source?.message?.message_id,
1370
+ source?.message?.open_message_id,
1371
+ source?.event?.message?.message_id,
1372
+ source?.event?.message?.open_message_id,
1373
+ source?.message_id,
1374
+ source?.open_message_id,
1375
+ ];
1376
+ return candidates
1377
+ .map((candidate) => String(candidate || '').trim())
1378
+ .find(Boolean) || '';
1379
+ }
1380
+ findNestedCardActionMessageId(payload, seen = new Set()) {
1381
+ if (!payload || typeof payload !== 'object' || seen.has(payload)) {
1382
+ return '';
1383
+ }
1384
+ seen.add(payload);
1385
+ if (Array.isArray(payload)) {
1386
+ for (const item of payload) {
1387
+ const nested = this.findNestedCardActionMessageId(item, seen);
1388
+ if (nested)
1389
+ return nested;
1390
+ }
1391
+ return '';
1392
+ }
1393
+ const source = payload;
1394
+ for (const key of ['open_message_id', 'message_id', 'permission_message_id', 'action_message_id']) {
1395
+ const value = source[key];
1396
+ if (typeof value === 'string' && value.trim()) {
1397
+ return value.trim();
1398
+ }
1399
+ }
1400
+ for (const value of Object.values(source)) {
1401
+ const nested = this.findNestedCardActionMessageId(value, seen);
1402
+ if (nested)
1403
+ return nested;
1404
+ }
1405
+ return '';
1406
+ }
1051
1407
  pushUnique(target, value) {
1052
1408
  const normalized = value.trim();
1053
1409
  if (!normalized) {
@@ -1062,8 +1418,10 @@ class LocalCoreLarkGateway extends node_events_1.EventEmitter {
1062
1418
  }
1063
1419
  }
1064
1420
  formatPermissionButtonLabel(button) {
1065
- const response = (0, desktop_js_1.normalizePermissionResponse)(button.data) || (0, desktop_js_1.normalizePermissionResponse)(button.text);
1066
- switch (response) {
1421
+ return this.formatPermissionResponseLabel(button.data || button.text);
1422
+ }
1423
+ formatPermissionResponseLabel(response) {
1424
+ switch ((0, desktop_js_1.normalizePermissionResponse)(response) || response) {
1067
1425
  case 'allow':
1068
1426
  return '允许一次';
1069
1427
  case 'allow all':
@@ -1071,7 +1429,7 @@ class LocalCoreLarkGateway extends node_events_1.EventEmitter {
1071
1429
  case 'deny':
1072
1430
  return '拒绝';
1073
1431
  default:
1074
- return button.text;
1432
+ return response;
1075
1433
  }
1076
1434
  }
1077
1435
  resolveLarkButtonType(button, index) {
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.prepareChannelFile = prepareChannelFile;
4
+ exports.sanitizeChannelFileName = sanitizeChannelFileName;
5
+ const promises_1 = require("node:fs/promises");
6
+ const node_path_1 = require("node:path");
7
+ async function prepareChannelFile(input) {
8
+ const filePath = String(input.path || '').trim();
9
+ if (!filePath) {
10
+ throw new Error('Missing file path');
11
+ }
12
+ const fileStat = await (0, promises_1.stat)(filePath).catch((error) => {
13
+ throw new Error(`Cannot read file "${filePath}": ${error instanceof Error ? error.message : String(error)}`);
14
+ });
15
+ if (!fileStat.isFile()) {
16
+ throw new Error(`Path is not a file: ${filePath}`);
17
+ }
18
+ if (fileStat.size <= 0) {
19
+ throw new Error(`File is empty: ${filePath}`);
20
+ }
21
+ if (input.maxBytes && fileStat.size > input.maxBytes) {
22
+ throw new Error(`File is too large for ${input.platformLabel} upload: ${fileStat.size} bytes`);
23
+ }
24
+ return {
25
+ path: filePath,
26
+ fileName: sanitizeChannelFileName(input.fileName || (0, node_path_1.basename)(filePath)),
27
+ fileSize: fileStat.size,
28
+ };
29
+ }
30
+ function sanitizeChannelFileName(fileName) {
31
+ const normalized = String(fileName || '').trim().replace(/[\/\\]+/g, '_');
32
+ return normalized || 'file';
33
+ }