@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
@@ -9,7 +9,8 @@ const node_fs_1 = __importDefault(require("node:fs"));
9
9
  const node_path_1 = __importDefault(require("node:path"));
10
10
  const node_events_1 = require("node:events");
11
11
  const node_crypto_2 = require("node:crypto");
12
- const desktop_js_1 = require("../../../../shared/desktop.js");
12
+ const desktop_js_1 = require("../../../../../shared/desktop.js");
13
+ const file_utils_js_1 = require("../shared/file-utils.js");
13
14
  // ==================== Constants ====================
14
15
  const PAIRING_EXPIRY_MS = 10 * 60 * 1000;
15
16
  const DEFAULT_BASE_URL = 'https://ilinkai.weixin.qq.com';
@@ -33,6 +34,8 @@ const TEXT_ITEM_TYPE = 1;
33
34
  const IMAGE_ITEM_TYPE = 2;
34
35
  const VOICE_ITEM_TYPE = 3;
35
36
  const FILE_ITEM_TYPE = 4;
37
+ const UPLOAD_MEDIA_TYPE_FILE = 3;
38
+ const WEIXIN_MAX_UPLOAD_FILE_SIZE = 30 * 1024 * 1024;
36
39
  // ==================== Utilities ====================
37
40
  function stripHtml(html) {
38
41
  let result = html;
@@ -453,6 +456,76 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
453
456
  return '';
454
457
  }
455
458
  }
459
+ async sendOutboundMessage(workspaceId, input) {
460
+ const state = this.runtime.get(workspaceId);
461
+ if (!state?.connected) {
462
+ throw new Error(`WeChat workspace is not connected: ${workspaceId}`);
463
+ }
464
+ const channelId = String(input.route?.channelId || '').trim();
465
+ if (!channelId) {
466
+ throw new Error('Missing WeChat target channel id');
467
+ }
468
+ const binding = await this.getBinding(workspaceId);
469
+ const contextToken = this.resolveContextTokenForFileSend(workspaceId, channelId, input.route.participantId);
470
+ const messageIds = [];
471
+ const attachments = [];
472
+ for (const part of input.parts || []) {
473
+ if (part.type === 'text') {
474
+ const text = String(part.text || '').trim();
475
+ if (text) {
476
+ await this.sendTextMessage(state, channelId, text, contextToken);
477
+ messageIds.push(`wx_text_${(0, node_crypto_2.randomUUID)()}`);
478
+ }
479
+ continue;
480
+ }
481
+ if (part.type === 'file') {
482
+ const sent = await this.sendFilePart(binding, channelId, part, contextToken);
483
+ messageIds.push(sent.messageId);
484
+ attachments.push({
485
+ kind: 'file',
486
+ attachmentId: sent.uploaded.fileKey,
487
+ fileName: sent.file.fileName,
488
+ fileSize: sent.file.fileSize,
489
+ metadata: {
490
+ fileKey: sent.uploaded.fileKey,
491
+ encryptedQueryParam: sent.uploaded.encryptedQueryParam,
492
+ },
493
+ });
494
+ }
495
+ }
496
+ return {
497
+ platform: 'weixin',
498
+ workspaceId,
499
+ channelId,
500
+ participantId: input.route.participantId,
501
+ messageIds,
502
+ attachments,
503
+ };
504
+ }
505
+ async sendFile(workspaceId, input) {
506
+ const result = await this.sendOutboundMessage(workspaceId, {
507
+ route: {
508
+ type: 'channel.chat',
509
+ channelId: input.channelId,
510
+ participantId: input.participantId,
511
+ },
512
+ parts: [{
513
+ type: 'file',
514
+ path: input.path,
515
+ fileName: input.fileName,
516
+ }],
517
+ });
518
+ const attachment = result.attachments?.[0];
519
+ return {
520
+ platform: 'weixin',
521
+ workspaceId,
522
+ channelId: result.channelId,
523
+ messageId: result.messageIds[0] || '',
524
+ fileKey: String(attachment?.metadata?.fileKey || attachment?.attachmentId || ''),
525
+ fileName: attachment?.fileName || input.fileName || '',
526
+ fileSize: attachment?.fileSize || 0,
527
+ };
528
+ }
456
529
  // ==================== Inbound Message Handling ====================
457
530
  async handleInboundMessage(input) {
458
531
  if (this.isDuplicateInboundMessage(input)) {
@@ -585,7 +658,8 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
585
658
  return { paired: true, threadId };
586
659
  }
587
660
  this.options.store.updatePlatformThreadMessageId(input.workspaceId, input.chatId, input.platformUserId, input.contextToken || input.messageId, 'weixin');
588
- await router.sendThreadMessage(threadId, (0, desktop_js_1.wrapUserMessageWithSchedulerProtocol)(input.text));
661
+ const wrappedText = (0, desktop_js_1.wrapUserMessageWithSchedulerProtocol)(input.text);
662
+ await router.sendThreadMessage(threadId, this.createThreadMessageInput(wrappedText, input.contentParts));
589
663
  return { paired: true, threadId };
590
664
  }
591
665
  // ==================== Private: Bindings ====================
@@ -799,6 +873,7 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
799
873
  const msgId = msg.msg_id ?? String(Date.now());
800
874
  // Handle attachments
801
875
  let attachmentText = '';
876
+ const attachmentParts = [];
802
877
  if (mediaItems.length > 0) {
803
878
  const uploadsDir = node_path_1.default.join(binding.stateDir, 'weixin-uploads');
804
879
  for (const [idx, item] of mediaItems.entries()) {
@@ -807,6 +882,14 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
807
882
  if (att) {
808
883
  attachmentText += attachmentText ? '\n' : '';
809
884
  attachmentText += att.kind === 'image' ? `[Image: ${att.path}]` : `[File "${att.name}": ${att.path}]`;
885
+ if (att.kind === 'image' && att.data) {
886
+ attachmentParts.push({
887
+ type: 'image',
888
+ data: att.data,
889
+ mimeType: att.mimeType,
890
+ fileName: att.name,
891
+ });
892
+ }
810
893
  }
811
894
  }
812
895
  catch (dlErr) {
@@ -825,6 +908,10 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
825
908
  text: fullText,
826
909
  messageId: msgId,
827
910
  contextToken: msg.context_token,
911
+ contentParts: [
912
+ ...(text ? [{ type: 'text', text }] : []),
913
+ ...attachmentParts,
914
+ ],
828
915
  });
829
916
  }
830
917
  }
@@ -935,9 +1022,123 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
935
1022
  base_info: { channel_version: WEIXIN_CHANNEL_VERSION },
936
1023
  }, API_TIMEOUT_MS);
937
1024
  }
1025
+ async sendFileMessage(binding, toUserId, fileName, uploaded, contextToken) {
1026
+ const clientId = `openclaw-weixin-${node_crypto_1.default.randomUUID()}`;
1027
+ const resp = await this.apiPost(binding, 'ilink/bot/sendmessage', {
1028
+ msg: {
1029
+ from_user_id: '',
1030
+ to_user_id: toUserId,
1031
+ client_id: clientId,
1032
+ message_type: 2,
1033
+ message_state: 2,
1034
+ item_list: [{
1035
+ type: FILE_ITEM_TYPE,
1036
+ file_item: {
1037
+ media: {
1038
+ encrypt_query_param: uploaded.encryptedQueryParam,
1039
+ aes_key: Buffer.from(uploaded.aesKeyHex).toString('base64'),
1040
+ encrypt_type: 1,
1041
+ },
1042
+ file_name: fileName,
1043
+ len: String(uploaded.fileSize),
1044
+ },
1045
+ }],
1046
+ ...(contextToken ? { context_token: contextToken } : {}),
1047
+ },
1048
+ base_info: { channel_version: WEIXIN_CHANNEL_VERSION },
1049
+ }, API_TIMEOUT_MS);
1050
+ if (this.isSendMessageError(resp)) {
1051
+ throw new Error(`WeChat send file failed: ret=${resp.ret} errcode=${resp.errcode}${resp.errmsg ? ` errmsg=${resp.errmsg}` : ''}`);
1052
+ }
1053
+ return clientId;
1054
+ }
1055
+ async sendFilePart(binding, channelId, part, contextToken) {
1056
+ const file = await (0, file_utils_js_1.prepareChannelFile)({
1057
+ path: part.path,
1058
+ fileName: part.fileName,
1059
+ maxBytes: WEIXIN_MAX_UPLOAD_FILE_SIZE,
1060
+ platformLabel: 'WeChat',
1061
+ });
1062
+ const uploaded = await this.uploadFileToCdn(binding, channelId, file.path);
1063
+ const messageId = await this.sendFileMessage(binding, channelId, file.fileName, uploaded, contextToken);
1064
+ this.options.log?.(`localcore-weixin sent file ${file.fileName} (${file.fileSize} bytes) to ${channelId}`);
1065
+ return {
1066
+ messageId,
1067
+ uploaded,
1068
+ file,
1069
+ };
1070
+ }
938
1071
  isSendMessageError(resp) {
939
1072
  return (resp.ret !== undefined && resp.ret !== 0) || (resp.errcode !== undefined && resp.errcode !== 0);
940
1073
  }
1074
+ async uploadFileToCdn(binding, toUserId, filePath) {
1075
+ const plaintext = await node_fs_1.default.promises.readFile(filePath);
1076
+ const fileKey = node_crypto_1.default.randomBytes(16).toString('hex');
1077
+ const aesKey = node_crypto_1.default.randomBytes(16);
1078
+ const aesKeyHex = aesKey.toString('hex');
1079
+ const rawMd5 = node_crypto_1.default.createHash('md5').update(plaintext).digest('hex');
1080
+ const cipherSize = this.getAesEcbPaddedSize(plaintext.length);
1081
+ const uploadUrlResp = await this.apiPost(binding, 'ilink/bot/getuploadurl', {
1082
+ filekey: fileKey,
1083
+ media_type: UPLOAD_MEDIA_TYPE_FILE,
1084
+ to_user_id: toUserId,
1085
+ rawsize: plaintext.length,
1086
+ rawfilemd5: rawMd5,
1087
+ filesize: cipherSize,
1088
+ no_need_thumb: true,
1089
+ aeskey: aesKeyHex,
1090
+ base_info: { channel_version: WEIXIN_CHANNEL_VERSION },
1091
+ }, API_TIMEOUT_MS);
1092
+ if (this.isSendMessageError(uploadUrlResp) || !uploadUrlResp.upload_param) {
1093
+ throw new Error(`WeChat getuploadurl failed: ret=${uploadUrlResp.ret} errcode=${uploadUrlResp.errcode}${uploadUrlResp.errmsg ? ` errmsg=${uploadUrlResp.errmsg}` : ''}`);
1094
+ }
1095
+ const encryptedQueryParam = await this.uploadEncryptedBufferToCdn(binding, plaintext, uploadUrlResp.upload_param, fileKey, aesKey);
1096
+ return {
1097
+ fileKey,
1098
+ encryptedQueryParam,
1099
+ aesKeyHex,
1100
+ fileSize: plaintext.length,
1101
+ cipherSize,
1102
+ };
1103
+ }
1104
+ async uploadEncryptedBufferToCdn(binding, plaintext, uploadParam, fileKey, aesKey) {
1105
+ const cipher = node_crypto_1.default.createCipheriv('aes-128-ecb', aesKey, null);
1106
+ const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
1107
+ const url = `${binding.cdnBaseUrl.replace(/\/$/, '')}/upload?encrypted_query_param=${encodeURIComponent(uploadParam)}&filekey=${encodeURIComponent(fileKey)}`;
1108
+ const res = await fetch(url, {
1109
+ method: 'POST',
1110
+ headers: {
1111
+ 'Content-Type': 'application/octet-stream',
1112
+ },
1113
+ body: new Uint8Array(ciphertext),
1114
+ });
1115
+ if (!res.ok) {
1116
+ throw new Error(`WeChat CDN upload failed: HTTP ${res.status}`);
1117
+ }
1118
+ const encryptedQueryParam = res.headers.get('x-encrypted-param') || '';
1119
+ if (!encryptedQueryParam) {
1120
+ throw new Error('WeChat CDN upload response missing x-encrypted-param');
1121
+ }
1122
+ return encryptedQueryParam;
1123
+ }
1124
+ getAesEcbPaddedSize(size) {
1125
+ return Math.ceil((size + 1) / 16) * 16;
1126
+ }
1127
+ resolveContextTokenForFileSend(workspaceId, channelId, participantId) {
1128
+ const preferredParticipantId = String(participantId || '').trim();
1129
+ const direct = preferredParticipantId
1130
+ ? this.options.store.getPlatformThreadBinding(workspaceId, channelId, preferredParticipantId, 'weixin')
1131
+ : undefined;
1132
+ if (direct?.last_platform_message_id) {
1133
+ return direct.last_platform_message_id;
1134
+ }
1135
+ const users = this.options.store.listAuthorizedUsers(workspaceId, 'weixin');
1136
+ const user = users.find((entry) => entry.chatId === channelId || entry.platformUserId === channelId);
1137
+ if (!user) {
1138
+ return '';
1139
+ }
1140
+ return this.options.store.getPlatformThreadBinding(workspaceId, user.chatId, user.platformUserId, 'weixin')?.last_platform_message_id || '';
1141
+ }
941
1142
  // ==================== Private: Attachment Download ====================
942
1143
  sniffExtAndKind(buf) {
943
1144
  if (buf[0] === 0xff && buf[1] === 0xd8)
@@ -991,7 +1192,41 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
991
1192
  const filePath = node_path_1.default.join(uploadsDir, fileName);
992
1193
  node_fs_1.default.mkdirSync(uploadsDir, { recursive: true });
993
1194
  node_fs_1.default.writeFileSync(filePath, resultBuf);
994
- return { path: filePath, kind, name: declaredName };
1195
+ return {
1196
+ path: filePath,
1197
+ kind,
1198
+ name: declaredName,
1199
+ data: kind === 'image' ? resultBuf.toString('base64') : undefined,
1200
+ mimeType: kind === 'image' ? this.mimeTypeForImageExt(ext) : undefined,
1201
+ };
1202
+ }
1203
+ createWrappedContentParts(wrappedText, parts) {
1204
+ const nonTextParts = Array.isArray(parts)
1205
+ ? parts.filter((part) => part.type !== 'text')
1206
+ : [];
1207
+ return [
1208
+ { type: 'text', text: wrappedText },
1209
+ ...nonTextParts,
1210
+ ];
1211
+ }
1212
+ createThreadMessageInput(wrappedText, parts) {
1213
+ const hasNonTextPart = Array.isArray(parts) && parts.some((part) => part.type !== 'text');
1214
+ if (!hasNonTextPart) {
1215
+ return wrappedText;
1216
+ }
1217
+ return {
1218
+ displayText: wrappedText,
1219
+ contentParts: this.createWrappedContentParts(wrappedText, parts),
1220
+ };
1221
+ }
1222
+ mimeTypeForImageExt(ext) {
1223
+ if (ext === '.jpg')
1224
+ return 'image/jpeg';
1225
+ if (ext === '.png')
1226
+ return 'image/png';
1227
+ if (ext === '.gif')
1228
+ return 'image/gif';
1229
+ return 'application/octet-stream';
995
1230
  }
996
1231
  // ==================== Private: Turn State ====================
997
1232
  createTurnState(sessionKey) {
@@ -10,11 +10,20 @@ async function runCli(argv, env = node_process_1.default.env, io = node_process_
10
10
  try {
11
11
  const { positionals, flags } = parseArgs(argv);
12
12
  const [domain = '', action = '', maybeId = ''] = positionals;
13
+ const json = getBooleanFlag(flags, 'json', false);
14
+ if (domain === 'channel') {
15
+ switch (action) {
16
+ case 'send-file':
17
+ return await handleChannelSendFile(flags, env, io, json);
18
+ default:
19
+ printUsage(io.stderr);
20
+ return 2;
21
+ }
22
+ }
13
23
  if (domain !== 'scheduler') {
14
24
  printUsage(io.stderr);
15
25
  return 2;
16
26
  }
17
- const json = getBooleanFlag(flags, 'json', false);
18
27
  switch (action) {
19
28
  case 'add':
20
29
  return await handleAdd(flags, env, io, json);
@@ -39,6 +48,36 @@ async function runCli(argv, env = node_process_1.default.env, io = node_process_
39
48
  return 1;
40
49
  }
41
50
  }
51
+ async function handleChannelSendFile(flags, env, io, json) {
52
+ const context = resolveContext(flags, env);
53
+ const filePath = getRequiredFlag(flags, 'path');
54
+ const platform = getFlag(flags, 'platform') || context.platform;
55
+ if (!platform) {
56
+ throw new Error('channel send-file requires a platform. Set LOCAL_AI_PLATFORM or pass --platform.');
57
+ }
58
+ if (!context.workspaceId) {
59
+ throw new Error('channel send-file requires a workspace context. Set LOCAL_AI_WORKSPACE_ID or pass --workspace.');
60
+ }
61
+ const target = getFlag(flags, 'target') || getFlag(flags, 'chat-id') || context.chatId;
62
+ if (!target) {
63
+ throw new Error('channel send-file requires a target. Set LOCAL_AI_CHAT_ID or pass --target.');
64
+ }
65
+ const result = await request(context.baseUrl, 'POST', `/platforms/${encodeURIComponent(platform)}/${encodeURIComponent(context.workspaceId)}/messages`, {
66
+ route: {
67
+ type: 'channel.chat',
68
+ channelId: target,
69
+ participantId: getFlag(flags, 'participant-id') || context.platformUserId || undefined,
70
+ },
71
+ parts: [{
72
+ type: 'file',
73
+ path: filePath,
74
+ fileName: getFlag(flags, 'name') || undefined,
75
+ }],
76
+ });
77
+ const file = result.attachments?.[0];
78
+ print(json, io.stdout, result, `Sent file ${file?.fileName || filePath} to ${result.channelId}: ${result.messageIds[0] || ''}`);
79
+ return 0;
80
+ }
42
81
  async function handleAdd(flags, env, io, json) {
43
82
  const cronExpr = getRequiredFlag(flags, 'cron');
44
83
  const promptTemplate = getRequiredFlag(flags, 'message');
@@ -265,6 +304,7 @@ function printUsage(output) {
265
304
  ' lac scheduler edit <job-id> [--cron "<expr>"] [--message "<text>"] [--desc "<label>"] [--enabled true|false] [--execution-mode same-thread|side-thread] [--json]',
266
305
  ' lac scheduler del <job-id> [--json]',
267
306
  ' lac scheduler run <job-id> [--json]',
307
+ ' lac channel send-file --path "<file>" [--target <chat-or-user-id>] [--workspace <id>] [--platform lark] [--name <filename>] [--json]',
268
308
  ].join('\n') + '\n');
269
309
  }
270
310
  function formatJobLine(job) {
@@ -35,7 +35,7 @@ function bootstrapLocalCoreKernel(options) {
35
35
  const lifecycle = new lifecycle_manager_js_1.LocalCoreLifecycleManager(plugins, context);
36
36
  const diagnostics = new diagnostics_js_1.LocalCoreDiagnostics(plugins, lifecycle);
37
37
  const builtIns = [
38
- ...['codex', 'cursor', 'gemini', 'qoder', 'iflow'].map((agentType) => (0, agent_localcore_acp_plugin_js_1.createBuiltinStaticAgentCapabilityPlugin)(agentType)),
38
+ ...['cursor', 'gemini', 'qoder', 'iflow'].map((agentType) => (0, agent_localcore_acp_plugin_js_1.createBuiltinStaticAgentCapabilityPlugin)(agentType)),
39
39
  (0, agent_localcore_acp_plugin_js_1.createBuiltinLocalCoreAcpAgentPlugin)(),
40
40
  (0, scheduler_cron_plugin_js_1.createBuiltinCronSchedulerPlugin)(),
41
41
  ];
@@ -164,6 +164,7 @@ function bootstrapLocalCoreRuntime(options) {
164
164
  const agentPlugins = [
165
165
  localCoreAgentPlugin,
166
166
  (0, agent_localcore_acp_plugin_js_1.createBuiltinOpencodeAgentPlugin)(),
167
+ (0, agent_localcore_acp_plugin_js_1.createBuiltinCodexAgentPlugin)(),
167
168
  (0, agent_localcore_acp_plugin_js_1.createBuiltinClaudeCodeAgentPlugin)(),
168
169
  ];
169
170
  let workspaceRouter;
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createBuiltinLocalCoreAcpAgentPlugin = createBuiltinLocalCoreAcpAgentPlugin;
4
4
  exports.createBuiltinOpencodeAgentPlugin = createBuiltinOpencodeAgentPlugin;
5
+ exports.createBuiltinCodexAgentPlugin = createBuiltinCodexAgentPlugin;
5
6
  exports.createBuiltinClaudeCodeAgentPlugin = createBuiltinClaudeCodeAgentPlugin;
6
7
  exports.createBuiltinStaticAgentCapabilityPlugin = createBuiltinStaticAgentCapabilityPlugin;
7
8
  const desktop_js_1 = require("../../../../../shared/desktop.js");
@@ -85,6 +86,14 @@ function createBuiltinOpencodeAgentPlugin() {
85
86
  displayName: 'OpenCode',
86
87
  });
87
88
  }
89
+ function createBuiltinCodexAgentPlugin() {
90
+ return createBuiltinAgentPlugin({
91
+ pluginId: 'builtin.agent-codex',
92
+ agentType: 'codex',
93
+ match: (normalizedAgentType) => normalizedAgentType === 'codex',
94
+ displayName: 'Codex',
95
+ });
96
+ }
88
97
  function createBuiltinClaudeCodeAgentPlugin() {
89
98
  return createBuiltinAgentPlugin({
90
99
  pluginId: 'builtin.agent-claudecode',
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createBuiltinLarkChannelPlugin = createBuiltinLarkChannelPlugin;
4
- const local_core_lark_gateway_js_1 = require("../../gateway/local-core-lark-gateway.js");
4
+ const local_core_lark_gateway_js_1 = require("../../channel/lark/local-core-lark-gateway.js");
5
5
  function createBuiltinLarkChannelPlugin(options) {
6
6
  let runtime = null;
7
7
  let unsubscribeBridgeEvents = null;
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createBuiltinWeixinChannelPlugin = createBuiltinWeixinChannelPlugin;
4
- const local_core_weixin_gateway_js_1 = require("../../gateway/local-core-weixin-gateway.js");
4
+ const local_core_weixin_gateway_js_1 = require("../../channel/weixin/local-core-weixin-gateway.js");
5
5
  function createBuiltinWeixinChannelPlugin(options) {
6
6
  let runtime = null;
7
7
  let unsubscribeBridgeEvents = null;
@@ -150,22 +150,28 @@ function shouldUseOpenAiCompatibleProvider(providerId) {
150
150
  ]);
151
151
  return !builtInNonCompatibleProviders.has(providerId);
152
152
  }
153
- function resolveBundledClaudeCodeCommand() {
153
+ function resolveBundledAcpCommand(packageName, binName) {
154
154
  const require = (0, node_module_1.createRequire)(__filename);
155
- const packageJsonPath = require.resolve(`${desktop_js_1.DESKTOP_CLAUDECODE_ACP_PACKAGE}/package.json`);
155
+ const packageJsonPath = require.resolve(`${packageName}/package.json`);
156
156
  const packageJson = require(packageJsonPath);
157
157
  const binField = packageJson.bin;
158
158
  const relativeBinPath = typeof binField === 'string'
159
159
  ? binField
160
- : binField?.['claude-agent-acp'];
160
+ : binField?.[binName];
161
161
  if (!relativeBinPath) {
162
- throw new Error(`Bundled package "${desktop_js_1.DESKTOP_CLAUDECODE_ACP_PACKAGE}" does not declare the claude-agent-acp bin.`);
162
+ throw new Error(`Bundled package "${packageName}" does not declare the ${binName} bin.`);
163
163
  }
164
164
  return {
165
165
  command: process.execPath,
166
166
  args: [(0, node_path_1.resolve)((0, node_path_1.dirname)(packageJsonPath), relativeBinPath)],
167
167
  };
168
168
  }
169
+ function resolveBundledCodexCommand() {
170
+ return resolveBundledAcpCommand(desktop_js_1.DESKTOP_CODEX_ACP_PACKAGE, 'codex-acp');
171
+ }
172
+ function resolveBundledClaudeCodeCommand() {
173
+ return resolveBundledAcpCommand(desktop_js_1.DESKTOP_CLAUDECODE_ACP_PACKAGE, 'claude-agent-acp');
174
+ }
169
175
  function toLocalCoreProjectConfig(configState, project) {
170
176
  const rawWorkDir = String(project.agent?.options?.work_dir || '.').trim() || '.';
171
177
  const configDir = (0, node_path_1.dirname)(configState.path);
@@ -198,21 +204,26 @@ function toLocalCoreProjectConfig(configState, project) {
198
204
  ANTHROPIC_MODEL: model,
199
205
  }
200
206
  : {};
207
+ const bundledCodex = agentType === 'codex'
208
+ ? resolveBundledCodexCommand()
209
+ : null;
201
210
  const bundledClaudeCode = agentType === 'claudecode'
202
211
  ? resolveBundledClaudeCodeCommand()
203
212
  : null;
204
213
  const inferredCommand = agentType === 'opencode'
205
214
  ? 'opencode'
206
- : bundledClaudeCode?.command || '';
215
+ : bundledCodex?.command || bundledClaudeCode?.command || '';
207
216
  const command = String(project.agent?.options?.command || inferredCommand).trim();
208
217
  if (!command) {
209
218
  throw new Error(`Workspace "${project.name}" requires [projects.agent.options].command for Local AI Core ACP execution.`);
210
219
  }
211
220
  const defaultArgs = agentType === 'opencode'
212
221
  ? ['acp']
213
- : agentType === 'claudecode'
214
- ? [...(bundledClaudeCode?.args || [])]
215
- : [];
222
+ : agentType === 'codex'
223
+ ? [...(bundledCodex?.args || [])]
224
+ : agentType === 'claudecode'
225
+ ? [...(bundledClaudeCode?.args || [])]
226
+ : [];
216
227
  return {
217
228
  workspaceId: project.name,
218
229
  agentType,
@@ -18,7 +18,7 @@ const DISPLAY_NAMES = {
18
18
  };
19
19
  const COMMAND_CANDIDATES = {
20
20
  opencode: ['opencode'],
21
- codex: ['codex'],
21
+ codex: ['codex-acp', 'codex'],
22
22
  claudecode: ['claude-agent-acp'],
23
23
  cursor: ['cursor-agent', 'cursor'],
24
24
  gemini: ['gemini'],
@@ -61,8 +61,14 @@ function detectInstalledAgentRuntimes(options = {}) {
61
61
  ? installedRuntime(agentType, resolved, 'config', detectedAt, env, versionTimeoutMs)
62
62
  : missingRuntime(agentType, detectedAt, `Configured command not found: ${configured}`);
63
63
  }
64
- if (agentType === 'claudecode') {
65
- const bundled = resolveBundledClaudeAgentAcp(options.requireFrom);
64
+ if (agentType === 'codex' || agentType === 'claudecode') {
65
+ const bundled = agentType === 'codex'
66
+ ? resolveBundledAcpPackage(options.requireFrom, desktop_js_1.DESKTOP_CODEX_ACP_PACKAGE, ['bin/codex-acp.js'])
67
+ : resolveBundledAcpPackage(options.requireFrom, desktop_js_1.DESKTOP_CLAUDECODE_ACP_PACKAGE, [
68
+ 'dist/cli.js',
69
+ 'bin/claude-agent-acp.js',
70
+ 'cli.js',
71
+ ]);
66
72
  if (bundled) {
67
73
  return installedRuntime(agentType, bundled, 'bundled', detectedAt, env, versionTimeoutMs);
68
74
  }
@@ -215,18 +221,23 @@ function parseVersionOutput(output) {
215
221
  const match = text.match(/\d+(?:\.\d+)+(?:[-+._a-zA-Z0-9]*)?/);
216
222
  return match?.[0] || text || undefined;
217
223
  }
218
- function resolveBundledClaudeAgentAcp(requireFrom) {
224
+ function resolveBundledAcpPackage(requireFrom, packageName, candidates) {
219
225
  try {
220
226
  const require = (0, node_module_1.createRequire)(requireFrom || (0, node_path_1.join)(process.cwd(), 'package.json'));
221
- const packageJsonPath = require.resolve(`${desktop_js_1.DESKTOP_CLAUDECODE_ACP_PACKAGE}/package.json`);
227
+ const packageJsonPath = require.resolve(`${packageName}/package.json`);
222
228
  const packageDir = (0, node_path_1.dirname)(packageJsonPath);
223
- for (const candidate of [
224
- (0, node_path_1.join)(packageDir, 'dist', 'cli.js'),
225
- (0, node_path_1.join)(packageDir, 'bin', 'claude-agent-acp.js'),
226
- (0, node_path_1.join)(packageDir, 'cli.js'),
227
- ]) {
228
- if ((0, node_fs_1.existsSync)(candidate)) {
229
- return candidate;
229
+ const packageJson = require(packageJsonPath);
230
+ const binField = packageJson.bin;
231
+ if (typeof binField === 'string') {
232
+ candidates = [binField, ...candidates];
233
+ }
234
+ else if (binField && typeof binField === 'object') {
235
+ candidates = [...Object.values(binField), ...candidates];
236
+ }
237
+ for (const candidate of candidates) {
238
+ const candidatePath = (0, node_path_1.join)(packageDir, candidate);
239
+ if ((0, node_fs_1.existsSync)(candidatePath)) {
240
+ return candidatePath;
230
241
  }
231
242
  }
232
243
  return packageJsonPath;
@@ -343,6 +343,47 @@ class LocalCoreController extends node_events_1.EventEmitter {
343
343
  async listChannelAuthorizedUsers(platform, workspaceId) {
344
344
  return this.resolveChannelRuntime(platform).listAuthorizedUsers(workspaceId);
345
345
  }
346
+ async sendChannelFile(platform, workspaceId, input) {
347
+ const runtime = this.resolveChannelRuntime(platform);
348
+ if (runtime.sendOutboundMessage) {
349
+ const result = await runtime.sendOutboundMessage(workspaceId, {
350
+ route: {
351
+ type: 'channel.chat',
352
+ channelId: input.channelId,
353
+ participantId: input.participantId,
354
+ },
355
+ parts: [{
356
+ type: 'file',
357
+ path: input.path,
358
+ fileName: input.fileName,
359
+ }],
360
+ });
361
+ const attachment = result.attachments?.[0];
362
+ return {
363
+ platform: result.platform,
364
+ workspaceId: result.workspaceId,
365
+ channelId: result.channelId,
366
+ messageId: result.messageIds[0] || '',
367
+ messageIds: result.messageIds,
368
+ fileKey: String(attachment?.metadata?.fileKey || attachment?.attachmentId || ''),
369
+ attachmentId: attachment?.attachmentId,
370
+ fileName: attachment?.fileName || input.fileName || '',
371
+ fileSize: attachment?.fileSize || 0,
372
+ metadata: result.metadata,
373
+ };
374
+ }
375
+ if (!runtime.sendFile) {
376
+ throw new Error(`Channel platform does not support sending files: ${platform}`);
377
+ }
378
+ return runtime.sendFile(workspaceId, input);
379
+ }
380
+ async sendChannelMessage(platform, workspaceId, input) {
381
+ const runtime = this.resolveChannelRuntime(platform);
382
+ if (!runtime.sendOutboundMessage) {
383
+ throw new Error(`Channel platform does not support outbound messages: ${platform}`);
384
+ }
385
+ return runtime.sendOutboundMessage(workspaceId, input);
386
+ }
346
387
  async getWeixinQrCode(workspaceId) {
347
388
  const runtime = this.weixinChannelRuntime;
348
389
  return runtime.getQrCode(workspaceId);
@@ -249,6 +249,16 @@ class LocalAiCoreServer {
249
249
  json(res, 200, await this.bindings.disableChannelGateway(platform, workspaceOrCollection));
250
250
  return;
251
251
  }
252
+ if (action === 'files') {
253
+ const body = await readJsonBody(req);
254
+ json(res, 200, await this.bindings.sendChannelFile(platform, workspaceOrCollection, body));
255
+ return;
256
+ }
257
+ if (action === 'messages') {
258
+ const body = await readJsonBody(req);
259
+ json(res, 200, await this.bindings.sendChannelMessage(platform, workspaceOrCollection, body));
260
+ return;
261
+ }
252
262
  if (platform === 'weixin' && segments.length === 3 && workspaceOrCollection !== 'pairings' && action === 'qrcode') {
253
263
  json(res, 200, await this.bindings.getWeixinQrCode(workspaceOrCollection));
254
264
  return;