@kafca/agentdock 0.1.21 → 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 +6 -0
- package/dist/renderer/assets/{Badge-BRmQIqbd.js → Badge-B_rgzxYO.js} +1 -1
- package/dist/renderer/assets/{Chat-D94ODABB.js → Chat-CYtp6m7T.js} +1 -1
- package/dist/renderer/assets/{ChatMarkdown-DqW7OUvV.js → ChatMarkdown-DOBMwa9d.js} +1 -1
- package/dist/renderer/assets/{Config-9lgzpTCp.js → Config-DppMSPHF.js} +2 -2
- package/dist/renderer/assets/CronList-BDtB1SuA.js +1 -0
- package/dist/renderer/assets/{Dashboard-COISXbQy.js → Dashboard-iFlRnd7Y.js} +2 -2
- package/dist/renderer/assets/{EmptyState-w5-I9zg7.js → EmptyState-bKDt_T88.js} +1 -1
- package/dist/renderer/assets/{KnowledgeDetail-DVxwrTLw.js → KnowledgeDetail-CKPhKPIg.js} +2 -2
- package/dist/renderer/assets/{KnowledgeHome-BKkubcwi.js → KnowledgeHome-CFeJre6i.js} +3 -3
- package/dist/renderer/assets/Logs-CyxSVOFK.js +1 -0
- package/dist/renderer/assets/{Modal-Ceu_MP2e.js → Modal-CxPnk1Ey.js} +1 -1
- package/dist/renderer/assets/{Page-CpSczAz8.js → Page-kU9vubzj.js} +1 -1
- package/dist/renderer/assets/{ProjectDetail-FJ_bNXLF.js → ProjectDetail-D9FY9NWZ.js} +1 -1
- package/dist/renderer/assets/{ProjectList-DZKL4K9C.js → ProjectList-DIwr2TXg.js} +1 -1
- package/dist/renderer/assets/{Select-BHQk6qCU.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-D3s4GXGh.js → ThreadChat-YvVsACW3.js} +7 -7
- package/dist/renderer/assets/{Workspace-B7looTVo.js → Workspace-ST7V8u6T.js} +2 -2
- package/dist/renderer/assets/{arrow-left-Dtal-eOi.js → arrow-left-BmZAVGV7.js} +1 -1
- package/dist/renderer/assets/{book-open-q814R_Ov.js → book-open-DZ3GXFgI.js} +1 -1
- package/dist/renderer/assets/{check-DjfCdLvw.js → check-CeVSvDCv.js} +1 -1
- package/dist/renderer/assets/chevron-down-yryuMoQ-.js +6 -0
- package/dist/renderer/assets/{circle-vfhmyE8v.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-CUQibo1I.js → funnel-pwAe2Ixp.js} +1 -1
- package/dist/renderer/assets/{heart-DG9_b8P1.js → heart-B2Sp-bCm.js} +1 -1
- package/dist/renderer/assets/{index-Bn8ane6_.js → index-BC3MWbS1.js} +3 -3
- package/dist/renderer/assets/{index-Ds0IN5rP.js → index-Cse2Br64.js} +1 -1
- package/dist/renderer/assets/{index-3CzgyQYB.css → index-DFiJR0vv.css} +1 -1
- package/dist/renderer/assets/{pencil-Dij0LlRY.js → pencil-BfEunUF3.js} +1 -1
- package/dist/renderer/assets/{play-CPFGjIyJ.js → play-Dcs9Lj_Z.js} +1 -1
- package/dist/renderer/assets/{plug-DH3RKQyn.js → plug-B_YschQe.js} +1 -1
- package/dist/renderer/assets/{plus-afkK9_Y8.js → plus-Pqo-cXHQ.js} +1 -1
- package/dist/renderer/assets/projects-BCj0k_Gy.js +1 -0
- package/dist/renderer/assets/{save-B-cKgWcy.js → save-BKM5yi1A.js} +1 -1
- package/dist/renderer/assets/{search-DR6PeYkt.js → search-BU42h10M.js} +1 -1
- package/dist/renderer/assets/{server-BM6KOB5a.js → server-DA5t3Rtr.js} +1 -1
- package/dist/renderer/assets/{session-utils-BDuThM0a.js → session-utils-Cr9VhNSq.js} +1 -1
- package/dist/renderer/assets/{sessions-C1yKps4T.js → sessions-bvV8ZMfX.js} +1 -1
- package/dist/renderer/assets/{shield-check-BFjMG-r2.js → shield-check-BeOJKur_.js} +1 -1
- package/dist/renderer/assets/{trash-2-CW1heMCf.js → trash-2-CQ7HKO3q.js} +1 -1
- package/dist/renderer/assets/{user-r_2sHG7m.js → user-CtFjY9SV.js} +1 -1
- package/dist/renderer/assets/{wifi-off-VMUokMpE.js → wifi-off-Ci5m0ysf.js} +1 -1
- package/dist/renderer/index.html +2 -2
- package/dist-electron/electron/local-core-refactor.test.js +307 -3
- package/dist-electron/services/local-ai-core/src/{gateway → channel/lark}/local-core-lark-gateway.js +249 -8
- 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 +188 -1
- package/dist-electron/services/local-ai-core/src/cli/lac.js +41 -1
- 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/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 +14 -1
- package/package.json +1 -1
- package/dist/renderer/assets/CronList-BYZ-x_bp.js +0 -1
- package/dist/renderer/assets/Logs-D7HxZ8Nb.js +0 -1
- package/dist/renderer/assets/SessionChat-DpgSXF3m.js +0 -1
- package/dist/renderer/assets/SessionList-CU6drQ4N.js +0 -1
- package/dist/renderer/assets/desktop-BPIxWbkM.js +0 -1
- package/dist/renderer/assets/desktop-DZHC4PDB.js +0 -3
- package/dist/renderer/assets/projects-DTDS9SnT.js +0 -1
package/dist-electron/services/local-ai-core/src/{gateway → channel/lark}/local-core-lark-gateway.js
RENAMED
|
@@ -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
|
|
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);
|
|
@@ -881,10 +999,9 @@ class LocalCoreLarkGateway extends node_events_1.EventEmitter {
|
|
|
881
999
|
};
|
|
882
1000
|
}
|
|
883
1001
|
async sendTextAsCard(state, chatId, text, buttonRows = [], sessionKey, threadId) {
|
|
884
|
-
const receiveIdType = chatId.startsWith('oc_') ? 'chat_id' : chatId.startsWith('ou_') ? 'open_id' : 'user_id';
|
|
885
1002
|
const response = await state.client.im.message.create({
|
|
886
1003
|
params: {
|
|
887
|
-
receive_id_type:
|
|
1004
|
+
receive_id_type: this.resolveReceiveIdType(chatId),
|
|
888
1005
|
},
|
|
889
1006
|
data: {
|
|
890
1007
|
receive_id: chatId,
|
|
@@ -894,6 +1011,33 @@ class LocalCoreLarkGateway extends node_events_1.EventEmitter {
|
|
|
894
1011
|
});
|
|
895
1012
|
return String(response?.data?.message_id || '').trim();
|
|
896
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
|
+
}
|
|
897
1041
|
async patchTextCard(state, messageId, text, buttonRows = [], sessionKey, threadId) {
|
|
898
1042
|
await state.client.im.message.patch({
|
|
899
1043
|
path: {
|
|
@@ -971,7 +1115,7 @@ class LocalCoreLarkGateway extends node_events_1.EventEmitter {
|
|
|
971
1115
|
return turn;
|
|
972
1116
|
}
|
|
973
1117
|
consumeBridgeEvent(turn, event) {
|
|
974
|
-
const content = String(event.content || '').trim();
|
|
1118
|
+
const content = foldToolResultForLark(String(event.content || '').trim());
|
|
975
1119
|
if (event.type === 'typing_start') {
|
|
976
1120
|
// Always start a fresh card for each generation phase.
|
|
977
1121
|
// This keeps Feishu message order aligned with user-visible timeline.
|
|
@@ -1030,7 +1174,7 @@ class LocalCoreLarkGateway extends node_events_1.EventEmitter {
|
|
|
1030
1174
|
turn.previewText = content;
|
|
1031
1175
|
}
|
|
1032
1176
|
renderBridgeEventMessage(turn, event) {
|
|
1033
|
-
const content = String(event.content || '').trim();
|
|
1177
|
+
const content = foldToolResultForLark(String(event.content || '').trim());
|
|
1034
1178
|
if (event.type === 'preview_start' || event.type === 'update_message') {
|
|
1035
1179
|
if (content.startsWith('💭 ')) {
|
|
1036
1180
|
return this.renderProgressMessage(event.previewHandle || 'thinking-preview', content);
|
|
@@ -1159,12 +1303,107 @@ class LocalCoreLarkGateway extends node_events_1.EventEmitter {
|
|
|
1159
1303
|
}
|
|
1160
1304
|
const router = this.options.getWorkspaceRouter();
|
|
1161
1305
|
await router.sendThreadAction(threadId, response);
|
|
1306
|
+
await this.markPermissionCardActionHandled(workspaceId, data, payload, value, response, threadId);
|
|
1162
1307
|
this.options.log?.(`localcore-lark processed card action for ${workspaceId}: ${response}`);
|
|
1163
1308
|
}
|
|
1164
1309
|
catch (error) {
|
|
1165
1310
|
this.options.log?.(`localcore-lark card action failed for ${workspaceId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1166
1311
|
}
|
|
1167
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
|
+
}
|
|
1168
1407
|
pushUnique(target, value) {
|
|
1169
1408
|
const normalized = value.trim();
|
|
1170
1409
|
if (!normalized) {
|
|
@@ -1179,8 +1418,10 @@ class LocalCoreLarkGateway extends node_events_1.EventEmitter {
|
|
|
1179
1418
|
}
|
|
1180
1419
|
}
|
|
1181
1420
|
formatPermissionButtonLabel(button) {
|
|
1182
|
-
|
|
1183
|
-
|
|
1421
|
+
return this.formatPermissionResponseLabel(button.data || button.text);
|
|
1422
|
+
}
|
|
1423
|
+
formatPermissionResponseLabel(response) {
|
|
1424
|
+
switch ((0, desktop_js_1.normalizePermissionResponse)(response) || response) {
|
|
1184
1425
|
case 'allow':
|
|
1185
1426
|
return '允许一次';
|
|
1186
1427
|
case 'allow all':
|
|
@@ -1188,7 +1429,7 @@ class LocalCoreLarkGateway extends node_events_1.EventEmitter {
|
|
|
1188
1429
|
case 'deny':
|
|
1189
1430
|
return '拒绝';
|
|
1190
1431
|
default:
|
|
1191
|
-
return
|
|
1432
|
+
return response;
|
|
1192
1433
|
}
|
|
1193
1434
|
}
|
|
1194
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
|
+
}
|
|
@@ -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)) {
|
|
@@ -949,9 +1022,123 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
|
|
|
949
1022
|
base_info: { channel_version: WEIXIN_CHANNEL_VERSION },
|
|
950
1023
|
}, API_TIMEOUT_MS);
|
|
951
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
|
+
}
|
|
952
1071
|
isSendMessageError(resp) {
|
|
953
1072
|
return (resp.ret !== undefined && resp.ret !== 0) || (resp.errcode !== undefined && resp.errcode !== 0);
|
|
954
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
|
+
}
|
|
955
1142
|
// ==================== Private: Attachment Download ====================
|
|
956
1143
|
sniffExtAndKind(buf) {
|
|
957
1144
|
if (buf[0] === 0xff && buf[1] === 0xd8)
|
|
@@ -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) {
|
|
@@ -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;
|