@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.
- package/README.md +13 -0
- package/dist/renderer/assets/{Badge-C1f8JpRb.js → Badge-B_rgzxYO.js} +1 -1
- package/dist/renderer/assets/{Chat-S50iFg3I.js → Chat-CYtp6m7T.js} +1 -1
- package/dist/renderer/assets/{ChatMarkdown-VWL9Vnpz.js → ChatMarkdown-DOBMwa9d.js} +1 -1
- package/dist/renderer/assets/{Config-Yc9ExUI6.js → Config-DppMSPHF.js} +2 -2
- package/dist/renderer/assets/CronList-BDtB1SuA.js +1 -0
- package/dist/renderer/assets/{Dashboard-CzXCvZVR.js → Dashboard-iFlRnd7Y.js} +2 -2
- package/dist/renderer/assets/{EmptyState-9FbY2o6z.js → EmptyState-bKDt_T88.js} +1 -1
- package/dist/renderer/assets/{KnowledgeDetail-D_BDc4Zt.js → KnowledgeDetail-CKPhKPIg.js} +2 -2
- package/dist/renderer/assets/{KnowledgeHome-DsUA_BCR.js → KnowledgeHome-CFeJre6i.js} +3 -3
- package/dist/renderer/assets/Logs-CyxSVOFK.js +1 -0
- package/dist/renderer/assets/{Modal-Bt5VBC6S.js → Modal-CxPnk1Ey.js} +1 -1
- package/dist/renderer/assets/{Page-Da_Kc6Qm.js → Page-kU9vubzj.js} +1 -1
- package/dist/renderer/assets/{ProjectDetail-B4Ps80yv.js → ProjectDetail-D9FY9NWZ.js} +1 -1
- package/dist/renderer/assets/{ProjectList-BWTP9npz.js → ProjectList-DIwr2TXg.js} +1 -1
- package/dist/renderer/assets/{Select-DWs3hVUh.js → Select-BaWoz5Dn.js} +2 -7
- package/dist/renderer/assets/SessionChat-Cm9CW8Zx.js +1 -0
- package/dist/renderer/assets/SessionList-UPnWlXXO.js +1 -0
- package/dist/renderer/assets/{ThreadChat-nhnikHnk.js → ThreadChat-YvVsACW3.js} +7 -7
- package/dist/renderer/assets/{Workspace-pq_JYdTN.js → Workspace-ST7V8u6T.js} +2 -2
- package/dist/renderer/assets/{arrow-left-CR39hNvq.js → arrow-left-BmZAVGV7.js} +1 -1
- package/dist/renderer/assets/{book-open-Bot2WKl8.js → book-open-DZ3GXFgI.js} +1 -1
- package/dist/renderer/assets/{check-Drbl2nU3.js → check-CeVSvDCv.js} +1 -1
- package/dist/renderer/assets/chevron-down-yryuMoQ-.js +6 -0
- package/dist/renderer/assets/{circle-BXFfAwnA.js → circle-eASsFISH.js} +1 -1
- package/dist/renderer/assets/desktop-Che1cbpE.js +4 -0
- package/dist/renderer/assets/desktop-DkrP_s-b.js +1 -0
- package/dist/renderer/assets/{funnel-BNKYan_N.js → funnel-pwAe2Ixp.js} +1 -1
- package/dist/renderer/assets/{heart-wUOSwQE-.js → heart-B2Sp-bCm.js} +1 -1
- package/dist/renderer/assets/{index-DT5la73R.js → index-BC3MWbS1.js} +3 -3
- package/dist/renderer/assets/{index-CmOphqrf.js → index-Cse2Br64.js} +1 -1
- package/dist/renderer/assets/{index-3CzgyQYB.css → index-DFiJR0vv.css} +1 -1
- package/dist/renderer/assets/{pencil-D7-8ggue.js → pencil-BfEunUF3.js} +1 -1
- package/dist/renderer/assets/{play-BCYOAfet.js → play-Dcs9Lj_Z.js} +1 -1
- package/dist/renderer/assets/{plug-Dsik74Yl.js → plug-B_YschQe.js} +1 -1
- package/dist/renderer/assets/{plus-CKrU26_l.js → plus-Pqo-cXHQ.js} +1 -1
- package/dist/renderer/assets/projects-BCj0k_Gy.js +1 -0
- package/dist/renderer/assets/{save-q7M2_eAl.js → save-BKM5yi1A.js} +1 -1
- package/dist/renderer/assets/{search-BptY4_MT.js → search-BU42h10M.js} +1 -1
- package/dist/renderer/assets/{server-DSSEF0D8.js → server-DA5t3Rtr.js} +1 -1
- package/dist/renderer/assets/{session-utils-BFuqPZ5x.js → session-utils-Cr9VhNSq.js} +1 -1
- package/dist/renderer/assets/{sessions-CrWo3g3u.js → sessions-bvV8ZMfX.js} +1 -1
- package/dist/renderer/assets/{shield-check-CCU722RQ.js → shield-check-BeOJKur_.js} +1 -1
- package/dist/renderer/assets/{trash-2-DaeeiV8x.js → trash-2-CQ7HKO3q.js} +1 -1
- package/dist/renderer/assets/{user-Bkz8J7Yw.js → user-CtFjY9SV.js} +1 -1
- package/dist/renderer/assets/{wifi-off-P8xdIOHU.js → wifi-off-Ci5m0ysf.js} +1 -1
- package/dist/renderer/index.html +2 -2
- package/dist-electron/electron/agent-runtime-detector.test.js +11 -0
- package/dist-electron/electron/local-core-refactor.test.js +403 -3
- package/dist-electron/electron/plugin-kernel.test.js +43 -9
- package/dist-electron/services/local-ai-core/src/acp/local-core-acp-backend.js +9 -9
- package/dist-electron/services/local-ai-core/src/acp/local-core-acp-content.js +60 -0
- package/dist-electron/services/local-ai-core/src/{gateway → channel/lark}/local-core-lark-gateway.js +371 -13
- package/dist-electron/services/local-ai-core/src/channel/shared/file-utils.js +33 -0
- package/dist-electron/services/local-ai-core/src/{gateway → channel/weixin}/local-core-weixin-gateway.js +238 -3
- package/dist-electron/services/local-ai-core/src/cli/lac.js +41 -1
- package/dist-electron/services/local-ai-core/src/kernel/bootstrap.js +2 -1
- package/dist-electron/services/local-ai-core/src/plugins/builtin/agent-localcore-acp-plugin.js +9 -0
- package/dist-electron/services/local-ai-core/src/plugins/builtin/channel-lark-plugin.js +1 -1
- package/dist-electron/services/local-ai-core/src/plugins/builtin/channel-weixin-plugin.js +1 -1
- package/dist-electron/services/local-ai-core/src/router/workspace-route-config.js +19 -8
- package/dist-electron/services/local-ai-core/src/runtime/agent-runtime-detector.js +23 -12
- package/dist-electron/services/local-ai-core/src/runtime/local-core-controller.js +41 -0
- package/dist-electron/services/local-ai-core/src/runtime/server.js +10 -0
- package/dist-electron/shared/desktop.js +17 -2
- package/package.json +2 -1
- package/dist/renderer/assets/CronList-DTboWazi.js +0 -1
- package/dist/renderer/assets/Logs-DDAmR67f.js +0 -1
- package/dist/renderer/assets/SessionChat-DOhUnOfH.js +0 -1
- package/dist/renderer/assets/SessionList-ChZYFXuo.js +0 -1
- package/dist/renderer/assets/desktop-5k3W66fH.js +0 -1
- package/dist/renderer/assets/desktop-D4Z7M9RM.js +0 -3
- 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("
|
|
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
|
-
|
|
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 {
|
|
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
|
-
...['
|
|
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;
|
package/dist-electron/services/local-ai-core/src/plugins/builtin/agent-localcore-acp-plugin.js
CHANGED
|
@@ -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("../../
|
|
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("../../
|
|
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
|
|
153
|
+
function resolveBundledAcpCommand(packageName, binName) {
|
|
154
154
|
const require = (0, node_module_1.createRequire)(__filename);
|
|
155
|
-
const packageJsonPath = require.resolve(`${
|
|
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?.[
|
|
160
|
+
: binField?.[binName];
|
|
161
161
|
if (!relativeBinPath) {
|
|
162
|
-
throw new Error(`Bundled package "${
|
|
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 === '
|
|
214
|
-
? [...(
|
|
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 =
|
|
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
|
|
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(`${
|
|
227
|
+
const packageJsonPath = require.resolve(`${packageName}/package.json`);
|
|
222
228
|
const packageDir = (0, node_path_1.dirname)(packageJsonPath);
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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;
|