@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
|
@@ -7,6 +7,7 @@ const local_core_acp_transport_js_1 = require("./local-core-acp-transport.js");
|
|
|
7
7
|
const local_core_acp_turn_coordinator_js_1 = require("./local-core-acp-turn-coordinator.js");
|
|
8
8
|
const local_core_acp_session_coordinator_js_1 = require("./local-core-acp-session-coordinator.js");
|
|
9
9
|
const local_core_acp_response_processor_js_1 = require("./local-core-acp-response-processor.js");
|
|
10
|
+
const local_core_acp_content_js_1 = require("./local-core-acp-content.js");
|
|
10
11
|
const command_risk_js_1 = require("../security/command-risk.js");
|
|
11
12
|
const ACP_PROMPT_TIMEOUT_MS = 15 * 60 * 1000;
|
|
12
13
|
class LocalCoreAcpBackend {
|
|
@@ -126,7 +127,7 @@ class LocalCoreAcpBackend {
|
|
|
126
127
|
this.options.store.deleteThread(threadId);
|
|
127
128
|
return { deleted: true };
|
|
128
129
|
}
|
|
129
|
-
async sendThreadMessage(threadId,
|
|
130
|
+
async sendThreadMessage(threadId, input, config) {
|
|
130
131
|
if (!config) {
|
|
131
132
|
throw new Error('localcore-acp message send requires a workspace config.');
|
|
132
133
|
}
|
|
@@ -134,6 +135,8 @@ class LocalCoreAcpBackend {
|
|
|
134
135
|
if (!row) {
|
|
135
136
|
throw new Error(`Thread not found: ${threadId}`);
|
|
136
137
|
}
|
|
138
|
+
const message = (0, local_core_acp_content_js_1.normalizeThreadMessageInput)(input);
|
|
139
|
+
const content = message.displayText;
|
|
137
140
|
this.options.store.appendMessage(threadId, 'user', content, 'final');
|
|
138
141
|
this.options.eventBus.emit({
|
|
139
142
|
type: 'thread.message.accepted',
|
|
@@ -169,7 +172,7 @@ class LocalCoreAcpBackend {
|
|
|
169
172
|
sessionKey: row.bridge_session_key,
|
|
170
173
|
},
|
|
171
174
|
});
|
|
172
|
-
void this.runPrompt(threadId, runId, row.bridge_session_key, config,
|
|
175
|
+
void this.runPrompt(threadId, runId, row.bridge_session_key, config, message).catch((error) => {
|
|
173
176
|
this.options.log?.(`localcore-acp prompt failed for ${threadId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
174
177
|
});
|
|
175
178
|
return { runId };
|
|
@@ -219,7 +222,7 @@ class LocalCoreAcpBackend {
|
|
|
219
222
|
async interruptRun(runId) {
|
|
220
223
|
return this.sessionCoordinator.interruptRun(runId);
|
|
221
224
|
}
|
|
222
|
-
async runPrompt(threadId, runId, bridgeSessionKey, config,
|
|
225
|
+
async runPrompt(threadId, runId, bridgeSessionKey, config, input) {
|
|
223
226
|
const row = this.options.store.getThreadRow(threadId);
|
|
224
227
|
if (!row) {
|
|
225
228
|
throw new Error(`Thread not found: ${threadId}`);
|
|
@@ -229,6 +232,8 @@ class LocalCoreAcpBackend {
|
|
|
229
232
|
sessionKey: bridgeSessionKey,
|
|
230
233
|
replyCtx: runId,
|
|
231
234
|
});
|
|
235
|
+
const message = (0, local_core_acp_content_js_1.normalizeThreadMessageInput)(input);
|
|
236
|
+
const content = message.displayText;
|
|
232
237
|
let session = null;
|
|
233
238
|
try {
|
|
234
239
|
session = await this.sessionCoordinator.ensureSession(threadId, bridgeSessionKey, config);
|
|
@@ -256,12 +261,7 @@ class LocalCoreAcpBackend {
|
|
|
256
261
|
const promptPromise = this.transport.request(session, 'session/prompt', {
|
|
257
262
|
sessionId: session.sessionId,
|
|
258
263
|
messageId: (0, node_crypto_1.randomUUID)(),
|
|
259
|
-
prompt:
|
|
260
|
-
{
|
|
261
|
-
type: 'text',
|
|
262
|
-
text: content,
|
|
263
|
-
},
|
|
264
|
-
],
|
|
264
|
+
prompt: message.contentParts,
|
|
265
265
|
}, ACP_PROMPT_TIMEOUT_MS);
|
|
266
266
|
session.promptPromise = promptPromise;
|
|
267
267
|
const result = await promptPromise;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizeThreadMessageInput = normalizeThreadMessageInput;
|
|
4
|
+
exports.createTextMessageInput = createTextMessageInput;
|
|
5
|
+
function normalizeThreadMessageInput(input) {
|
|
6
|
+
if (typeof input === 'string') {
|
|
7
|
+
return {
|
|
8
|
+
displayText: input,
|
|
9
|
+
contentParts: [{ type: 'text', text: input }],
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
const displayText = String(input.displayText || '').trim();
|
|
13
|
+
const contentParts = Array.isArray(input.contentParts)
|
|
14
|
+
? input.contentParts.filter(isSupportedContentPart)
|
|
15
|
+
: [];
|
|
16
|
+
if (contentParts.length > 0) {
|
|
17
|
+
return {
|
|
18
|
+
displayText: displayText || summarizeContentParts(contentParts),
|
|
19
|
+
contentParts,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
const fallbackText = displayText || '';
|
|
23
|
+
return {
|
|
24
|
+
displayText: fallbackText,
|
|
25
|
+
contentParts: [{ type: 'text', text: fallbackText }],
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function createTextMessageInput(text) {
|
|
29
|
+
return normalizeThreadMessageInput(text);
|
|
30
|
+
}
|
|
31
|
+
function isSupportedContentPart(part) {
|
|
32
|
+
if (!part || typeof part !== 'object') {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
const type = String(part.type || '');
|
|
36
|
+
if (type === 'text') {
|
|
37
|
+
return Boolean(String(part.text || ''));
|
|
38
|
+
}
|
|
39
|
+
if (type === 'image') {
|
|
40
|
+
const image = part;
|
|
41
|
+
return Boolean(String(image.data || image.uri || ''));
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
function summarizeContentParts(parts) {
|
|
46
|
+
const text = parts
|
|
47
|
+
.filter((part) => part.type === 'text')
|
|
48
|
+
.map((part) => part.text.trim())
|
|
49
|
+
.filter(Boolean)
|
|
50
|
+
.join('\n\n')
|
|
51
|
+
.trim();
|
|
52
|
+
if (text) {
|
|
53
|
+
return text;
|
|
54
|
+
}
|
|
55
|
+
const imageCount = parts.filter((part) => part.type === 'image').length;
|
|
56
|
+
if (imageCount > 0) {
|
|
57
|
+
return imageCount === 1 ? '[Image]' : `[${imageCount} images]`;
|
|
58
|
+
}
|
|
59
|
+
return '';
|
|
60
|
+
}
|
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);
|
|
@@ -453,7 +571,8 @@ class LocalCoreLarkGateway extends node_events_1.EventEmitter {
|
|
|
453
571
|
return { paired: true, threadId };
|
|
454
572
|
}
|
|
455
573
|
this.options.store.clearPlatformThreadMessageId(input.workspaceId, input.chatId, input.platformUserId);
|
|
456
|
-
|
|
574
|
+
const wrappedText = (0, desktop_js_1.wrapUserMessageWithSchedulerProtocol)(input.text);
|
|
575
|
+
await router.sendThreadMessage(threadId, this.createThreadMessageInput(wrappedText, input.contentParts));
|
|
457
576
|
return { paired: true, threadId };
|
|
458
577
|
}
|
|
459
578
|
close() {
|
|
@@ -610,18 +729,41 @@ class LocalCoreLarkGateway extends node_events_1.EventEmitter {
|
|
|
610
729
|
catch {
|
|
611
730
|
parsedContent = {};
|
|
612
731
|
}
|
|
732
|
+
const messageType = String(message.message_type || '').trim().toLowerCase();
|
|
613
733
|
const text = String(parsedContent.text || '').trim();
|
|
614
|
-
|
|
615
|
-
|
|
734
|
+
const contentParts = [];
|
|
735
|
+
if (text) {
|
|
736
|
+
contentParts.push({ type: 'text', text });
|
|
737
|
+
}
|
|
738
|
+
if (messageType === 'image') {
|
|
739
|
+
const imageKey = String(parsedContent.image_key || parsedContent.file_key || '').trim();
|
|
740
|
+
if (imageKey) {
|
|
741
|
+
try {
|
|
742
|
+
contentParts.push(await this.downloadMessageImage(workspaceId, String(message.message_id || ''), imageKey));
|
|
743
|
+
}
|
|
744
|
+
catch (error) {
|
|
745
|
+
const errorText = `[Image download failed: ${error instanceof Error ? error.message : String(error)}]`;
|
|
746
|
+
contentParts.push({ type: 'text', text: errorText });
|
|
747
|
+
this.options.log?.(`localcore-lark image download failed for ${workspaceId}: message=${String(message.message_id || '')} imageKey=${imageKey} error=${error instanceof Error ? error.message : String(error)}`);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
if (contentParts.length === 0) {
|
|
752
|
+
this.options.log?.(`localcore-lark ignored unsupported message for ${workspaceId}: type=${String(message.message_type || 'unknown')} contentKeys=${JSON.stringify(Object.keys(parsedContent))}`);
|
|
616
753
|
return;
|
|
617
754
|
}
|
|
755
|
+
const displayText = text || (contentParts.some((part) => part.type === 'image') ? '[Image]' : contentParts
|
|
756
|
+
.filter((part) => part.type === 'text')
|
|
757
|
+
.map((part) => part.text)
|
|
758
|
+
.join('\n\n')
|
|
759
|
+
.trim());
|
|
618
760
|
const platformUserId = String(sender.sender_id?.user_id || sender.sender_id?.open_id || '').trim();
|
|
619
761
|
const chatId = String(message.chat_id || platformUserId).trim();
|
|
620
762
|
if (!platformUserId || !chatId) {
|
|
621
763
|
this.options.log?.(`localcore-lark ignored message without sender/chat for ${workspaceId}: senderKeys=${JSON.stringify(Object.keys(sender?.sender_id || {}))} chat=${String(message.chat_id || '')}`);
|
|
622
764
|
return;
|
|
623
765
|
}
|
|
624
|
-
this.options.log?.(`localcore-lark inbound message for ${workspaceId}: chat=${chatId} user=${platformUserId} text=${JSON.stringify(
|
|
766
|
+
this.options.log?.(`localcore-lark inbound message for ${workspaceId}: chat=${chatId} user=${platformUserId} type=${messageType || 'unknown'} text=${JSON.stringify(displayText.slice(0, 120))}`);
|
|
625
767
|
await this.handleInboundMessage({
|
|
626
768
|
workspaceId,
|
|
627
769
|
platformUserId,
|
|
@@ -629,10 +771,103 @@ class LocalCoreLarkGateway extends node_events_1.EventEmitter {
|
|
|
629
771
|
displayName: String(payload?.sender?.sender_id?.user_id ||
|
|
630
772
|
payload?.sender?.sender_id?.open_id ||
|
|
631
773
|
`Lark ${platformUserId.slice(-6)}`),
|
|
632
|
-
text,
|
|
774
|
+
text: displayText,
|
|
633
775
|
messageId: String(message.message_id || (0, node_crypto_1.randomUUID)()),
|
|
776
|
+
contentParts,
|
|
634
777
|
});
|
|
635
778
|
}
|
|
779
|
+
createWrappedContentParts(wrappedText, parts) {
|
|
780
|
+
const nonTextParts = Array.isArray(parts)
|
|
781
|
+
? parts.filter((part) => part.type !== 'text')
|
|
782
|
+
: [];
|
|
783
|
+
return [
|
|
784
|
+
{ type: 'text', text: wrappedText },
|
|
785
|
+
...nonTextParts,
|
|
786
|
+
];
|
|
787
|
+
}
|
|
788
|
+
createThreadMessageInput(wrappedText, parts) {
|
|
789
|
+
const hasNonTextPart = Array.isArray(parts) && parts.some((part) => part.type !== 'text');
|
|
790
|
+
if (!hasNonTextPart) {
|
|
791
|
+
return wrappedText;
|
|
792
|
+
}
|
|
793
|
+
return {
|
|
794
|
+
displayText: wrappedText,
|
|
795
|
+
contentParts: this.createWrappedContentParts(wrappedText, parts),
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
async downloadMessageImage(workspaceId, messageId, imageKey) {
|
|
799
|
+
const state = this.runtime.get(workspaceId);
|
|
800
|
+
if (!state?.client) {
|
|
801
|
+
throw new Error('Lark client is not connected');
|
|
802
|
+
}
|
|
803
|
+
if (!messageId) {
|
|
804
|
+
throw new Error('Lark image message is missing message_id');
|
|
805
|
+
}
|
|
806
|
+
const resource = await state.client.im.messageResource.get({
|
|
807
|
+
path: {
|
|
808
|
+
message_id: messageId,
|
|
809
|
+
file_key: imageKey,
|
|
810
|
+
},
|
|
811
|
+
params: {
|
|
812
|
+
type: 'image',
|
|
813
|
+
},
|
|
814
|
+
});
|
|
815
|
+
const buffer = await this.readLarkResourceBuffer(resource);
|
|
816
|
+
if (buffer.length === 0) {
|
|
817
|
+
throw new Error('Lark image resource is empty');
|
|
818
|
+
}
|
|
819
|
+
const headerMime = this.extractHeaderMimeType(resource?.headers);
|
|
820
|
+
return {
|
|
821
|
+
type: 'image',
|
|
822
|
+
data: buffer.toString('base64'),
|
|
823
|
+
mimeType: headerMime || this.sniffImageMimeType(buffer),
|
|
824
|
+
fileName: `${imageKey}.${this.sniffImageExtension(buffer)}`,
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
async readLarkResourceBuffer(resource) {
|
|
828
|
+
const readable = resource?.getReadableStream?.();
|
|
829
|
+
if (!readable || typeof readable.on !== 'function') {
|
|
830
|
+
throw new Error('Lark resource did not provide a readable stream');
|
|
831
|
+
}
|
|
832
|
+
const chunks = [];
|
|
833
|
+
await new Promise((resolve, reject) => {
|
|
834
|
+
readable.on('data', (chunk) => {
|
|
835
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
836
|
+
});
|
|
837
|
+
readable.on('error', reject);
|
|
838
|
+
readable.on('end', resolve);
|
|
839
|
+
});
|
|
840
|
+
return Buffer.concat(chunks);
|
|
841
|
+
}
|
|
842
|
+
extractHeaderMimeType(headers) {
|
|
843
|
+
const value = headers && typeof headers === 'object'
|
|
844
|
+
? headers['content-type'] || headers['Content-Type']
|
|
845
|
+
: '';
|
|
846
|
+
return String(value || '').split(';')[0]?.trim() || '';
|
|
847
|
+
}
|
|
848
|
+
sniffImageMimeType(buffer) {
|
|
849
|
+
if (buffer[0] === 0xff && buffer[1] === 0xd8)
|
|
850
|
+
return 'image/jpeg';
|
|
851
|
+
if (buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4e && buffer[3] === 0x47)
|
|
852
|
+
return 'image/png';
|
|
853
|
+
if (buffer[0] === 0x47 && buffer[1] === 0x49 && buffer[2] === 0x46)
|
|
854
|
+
return 'image/gif';
|
|
855
|
+
if (buffer[0] === 0x52 && buffer[1] === 0x49 && buffer[2] === 0x46 && buffer[3] === 0x46)
|
|
856
|
+
return 'image/webp';
|
|
857
|
+
return 'application/octet-stream';
|
|
858
|
+
}
|
|
859
|
+
sniffImageExtension(buffer) {
|
|
860
|
+
const mimeType = this.sniffImageMimeType(buffer);
|
|
861
|
+
if (mimeType === 'image/jpeg')
|
|
862
|
+
return 'jpg';
|
|
863
|
+
if (mimeType === 'image/png')
|
|
864
|
+
return 'png';
|
|
865
|
+
if (mimeType === 'image/gif')
|
|
866
|
+
return 'gif';
|
|
867
|
+
if (mimeType === 'image/webp')
|
|
868
|
+
return 'webp';
|
|
869
|
+
return 'bin';
|
|
870
|
+
}
|
|
636
871
|
attachWsDiagnostics(workspaceId, wsClient) {
|
|
637
872
|
const on = typeof wsClient?.on === 'function' ? wsClient.on.bind(wsClient) : null;
|
|
638
873
|
if (!on) {
|
|
@@ -764,10 +999,9 @@ class LocalCoreLarkGateway extends node_events_1.EventEmitter {
|
|
|
764
999
|
};
|
|
765
1000
|
}
|
|
766
1001
|
async sendTextAsCard(state, chatId, text, buttonRows = [], sessionKey, threadId) {
|
|
767
|
-
const receiveIdType = chatId.startsWith('oc_') ? 'chat_id' : chatId.startsWith('ou_') ? 'open_id' : 'user_id';
|
|
768
1002
|
const response = await state.client.im.message.create({
|
|
769
1003
|
params: {
|
|
770
|
-
receive_id_type:
|
|
1004
|
+
receive_id_type: this.resolveReceiveIdType(chatId),
|
|
771
1005
|
},
|
|
772
1006
|
data: {
|
|
773
1007
|
receive_id: chatId,
|
|
@@ -777,6 +1011,33 @@ class LocalCoreLarkGateway extends node_events_1.EventEmitter {
|
|
|
777
1011
|
});
|
|
778
1012
|
return String(response?.data?.message_id || '').trim();
|
|
779
1013
|
}
|
|
1014
|
+
resolveReceiveIdType(receiveId) {
|
|
1015
|
+
return receiveId.startsWith('oc_') ? 'chat_id' : receiveId.startsWith('ou_') ? 'open_id' : 'user_id';
|
|
1016
|
+
}
|
|
1017
|
+
resolveLarkUploadFileType(fileName) {
|
|
1018
|
+
switch ((0, node_path_1.extname)(fileName).toLowerCase()) {
|
|
1019
|
+
case '.opus':
|
|
1020
|
+
return 'opus';
|
|
1021
|
+
case '.mp4':
|
|
1022
|
+
case '.mov':
|
|
1023
|
+
case '.m4v':
|
|
1024
|
+
return 'mp4';
|
|
1025
|
+
case '.pdf':
|
|
1026
|
+
return 'pdf';
|
|
1027
|
+
case '.doc':
|
|
1028
|
+
case '.docx':
|
|
1029
|
+
return 'doc';
|
|
1030
|
+
case '.xls':
|
|
1031
|
+
case '.xlsx':
|
|
1032
|
+
case '.csv':
|
|
1033
|
+
return 'xls';
|
|
1034
|
+
case '.ppt':
|
|
1035
|
+
case '.pptx':
|
|
1036
|
+
return 'ppt';
|
|
1037
|
+
default:
|
|
1038
|
+
return 'stream';
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
780
1041
|
async patchTextCard(state, messageId, text, buttonRows = [], sessionKey, threadId) {
|
|
781
1042
|
await state.client.im.message.patch({
|
|
782
1043
|
path: {
|
|
@@ -854,7 +1115,7 @@ class LocalCoreLarkGateway extends node_events_1.EventEmitter {
|
|
|
854
1115
|
return turn;
|
|
855
1116
|
}
|
|
856
1117
|
consumeBridgeEvent(turn, event) {
|
|
857
|
-
const content = String(event.content || '').trim();
|
|
1118
|
+
const content = foldToolResultForLark(String(event.content || '').trim());
|
|
858
1119
|
if (event.type === 'typing_start') {
|
|
859
1120
|
// Always start a fresh card for each generation phase.
|
|
860
1121
|
// This keeps Feishu message order aligned with user-visible timeline.
|
|
@@ -913,7 +1174,7 @@ class LocalCoreLarkGateway extends node_events_1.EventEmitter {
|
|
|
913
1174
|
turn.previewText = content;
|
|
914
1175
|
}
|
|
915
1176
|
renderBridgeEventMessage(turn, event) {
|
|
916
|
-
const content = String(event.content || '').trim();
|
|
1177
|
+
const content = foldToolResultForLark(String(event.content || '').trim());
|
|
917
1178
|
if (event.type === 'preview_start' || event.type === 'update_message') {
|
|
918
1179
|
if (content.startsWith('💭 ')) {
|
|
919
1180
|
return this.renderProgressMessage(event.previewHandle || 'thinking-preview', content);
|
|
@@ -1042,12 +1303,107 @@ class LocalCoreLarkGateway extends node_events_1.EventEmitter {
|
|
|
1042
1303
|
}
|
|
1043
1304
|
const router = this.options.getWorkspaceRouter();
|
|
1044
1305
|
await router.sendThreadAction(threadId, response);
|
|
1306
|
+
await this.markPermissionCardActionHandled(workspaceId, data, payload, value, response, threadId);
|
|
1045
1307
|
this.options.log?.(`localcore-lark processed card action for ${workspaceId}: ${response}`);
|
|
1046
1308
|
}
|
|
1047
1309
|
catch (error) {
|
|
1048
1310
|
this.options.log?.(`localcore-lark card action failed for ${workspaceId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1049
1311
|
}
|
|
1050
1312
|
}
|
|
1313
|
+
async markPermissionCardActionHandled(workspaceId, rootPayload, payload, value, response, threadId) {
|
|
1314
|
+
const state = this.runtime.get(workspaceId);
|
|
1315
|
+
if (!state?.client) {
|
|
1316
|
+
return;
|
|
1317
|
+
}
|
|
1318
|
+
const sessionKey = String(value.session_key || '').trim();
|
|
1319
|
+
const turn = sessionKey ? this.outboundTurns.get(sessionKey) : undefined;
|
|
1320
|
+
const messageId = this.extractCardActionMessageId(rootPayload, payload, value) || turn?.permissionMessageId;
|
|
1321
|
+
if (!messageId) {
|
|
1322
|
+
this.options.log?.(`localcore-lark could not find permission card message id for workspace=${workspaceId} sessionKey=${sessionKey || 'missing'}`);
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
try {
|
|
1326
|
+
await this.patchTextCard(state, messageId, [
|
|
1327
|
+
'**工具确认已处理**',
|
|
1328
|
+
`已选择:${this.formatPermissionResponseLabel(response)}`,
|
|
1329
|
+
'等待代理继续执行...',
|
|
1330
|
+
].join('\n\n'), [], sessionKey || undefined, threadId);
|
|
1331
|
+
if (turn) {
|
|
1332
|
+
turn.awaitingPermission = false;
|
|
1333
|
+
turn.buttonRows = [];
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
catch (error) {
|
|
1337
|
+
this.options.log?.(`localcore-lark failed to patch handled permission card ${messageId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
extractCardActionMessageId(...payloads) {
|
|
1341
|
+
for (const payload of payloads) {
|
|
1342
|
+
const messageId = this.extractKnownCardActionMessageId(payload);
|
|
1343
|
+
if (messageId) {
|
|
1344
|
+
return messageId;
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
for (const payload of payloads) {
|
|
1348
|
+
const messageId = this.findNestedCardActionMessageId(payload);
|
|
1349
|
+
if (messageId) {
|
|
1350
|
+
return messageId;
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
return '';
|
|
1354
|
+
}
|
|
1355
|
+
extractKnownCardActionMessageId(payload) {
|
|
1356
|
+
const source = payload;
|
|
1357
|
+
if (!source || typeof source !== 'object') {
|
|
1358
|
+
return '';
|
|
1359
|
+
}
|
|
1360
|
+
const candidates = [
|
|
1361
|
+
source?.permission_message_id,
|
|
1362
|
+
source?.action_message_id,
|
|
1363
|
+
source?.context?.open_message_id,
|
|
1364
|
+
source?.context?.message_id,
|
|
1365
|
+
source?.event?.context?.open_message_id,
|
|
1366
|
+
source?.event?.context?.message_id,
|
|
1367
|
+
source?.event_context?.open_message_id,
|
|
1368
|
+
source?.event_context?.message_id,
|
|
1369
|
+
source?.message?.message_id,
|
|
1370
|
+
source?.message?.open_message_id,
|
|
1371
|
+
source?.event?.message?.message_id,
|
|
1372
|
+
source?.event?.message?.open_message_id,
|
|
1373
|
+
source?.message_id,
|
|
1374
|
+
source?.open_message_id,
|
|
1375
|
+
];
|
|
1376
|
+
return candidates
|
|
1377
|
+
.map((candidate) => String(candidate || '').trim())
|
|
1378
|
+
.find(Boolean) || '';
|
|
1379
|
+
}
|
|
1380
|
+
findNestedCardActionMessageId(payload, seen = new Set()) {
|
|
1381
|
+
if (!payload || typeof payload !== 'object' || seen.has(payload)) {
|
|
1382
|
+
return '';
|
|
1383
|
+
}
|
|
1384
|
+
seen.add(payload);
|
|
1385
|
+
if (Array.isArray(payload)) {
|
|
1386
|
+
for (const item of payload) {
|
|
1387
|
+
const nested = this.findNestedCardActionMessageId(item, seen);
|
|
1388
|
+
if (nested)
|
|
1389
|
+
return nested;
|
|
1390
|
+
}
|
|
1391
|
+
return '';
|
|
1392
|
+
}
|
|
1393
|
+
const source = payload;
|
|
1394
|
+
for (const key of ['open_message_id', 'message_id', 'permission_message_id', 'action_message_id']) {
|
|
1395
|
+
const value = source[key];
|
|
1396
|
+
if (typeof value === 'string' && value.trim()) {
|
|
1397
|
+
return value.trim();
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
for (const value of Object.values(source)) {
|
|
1401
|
+
const nested = this.findNestedCardActionMessageId(value, seen);
|
|
1402
|
+
if (nested)
|
|
1403
|
+
return nested;
|
|
1404
|
+
}
|
|
1405
|
+
return '';
|
|
1406
|
+
}
|
|
1051
1407
|
pushUnique(target, value) {
|
|
1052
1408
|
const normalized = value.trim();
|
|
1053
1409
|
if (!normalized) {
|
|
@@ -1062,8 +1418,10 @@ class LocalCoreLarkGateway extends node_events_1.EventEmitter {
|
|
|
1062
1418
|
}
|
|
1063
1419
|
}
|
|
1064
1420
|
formatPermissionButtonLabel(button) {
|
|
1065
|
-
|
|
1066
|
-
|
|
1421
|
+
return this.formatPermissionResponseLabel(button.data || button.text);
|
|
1422
|
+
}
|
|
1423
|
+
formatPermissionResponseLabel(response) {
|
|
1424
|
+
switch ((0, desktop_js_1.normalizePermissionResponse)(response) || response) {
|
|
1067
1425
|
case 'allow':
|
|
1068
1426
|
return '允许一次';
|
|
1069
1427
|
case 'allow all':
|
|
@@ -1071,7 +1429,7 @@ class LocalCoreLarkGateway extends node_events_1.EventEmitter {
|
|
|
1071
1429
|
case 'deny':
|
|
1072
1430
|
return '拒绝';
|
|
1073
1431
|
default:
|
|
1074
|
-
return
|
|
1432
|
+
return response;
|
|
1075
1433
|
}
|
|
1076
1434
|
}
|
|
1077
1435
|
resolveLarkButtonType(button, index) {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.prepareChannelFile = prepareChannelFile;
|
|
4
|
+
exports.sanitizeChannelFileName = sanitizeChannelFileName;
|
|
5
|
+
const promises_1 = require("node:fs/promises");
|
|
6
|
+
const node_path_1 = require("node:path");
|
|
7
|
+
async function prepareChannelFile(input) {
|
|
8
|
+
const filePath = String(input.path || '').trim();
|
|
9
|
+
if (!filePath) {
|
|
10
|
+
throw new Error('Missing file path');
|
|
11
|
+
}
|
|
12
|
+
const fileStat = await (0, promises_1.stat)(filePath).catch((error) => {
|
|
13
|
+
throw new Error(`Cannot read file "${filePath}": ${error instanceof Error ? error.message : String(error)}`);
|
|
14
|
+
});
|
|
15
|
+
if (!fileStat.isFile()) {
|
|
16
|
+
throw new Error(`Path is not a file: ${filePath}`);
|
|
17
|
+
}
|
|
18
|
+
if (fileStat.size <= 0) {
|
|
19
|
+
throw new Error(`File is empty: ${filePath}`);
|
|
20
|
+
}
|
|
21
|
+
if (input.maxBytes && fileStat.size > input.maxBytes) {
|
|
22
|
+
throw new Error(`File is too large for ${input.platformLabel} upload: ${fileStat.size} bytes`);
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
path: filePath,
|
|
26
|
+
fileName: sanitizeChannelFileName(input.fileName || (0, node_path_1.basename)(filePath)),
|
|
27
|
+
fileSize: fileStat.size,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function sanitizeChannelFileName(fileName) {
|
|
31
|
+
const normalized = String(fileName || '').trim().replace(/[\/\\]+/g, '_');
|
|
32
|
+
return normalized || 'file';
|
|
33
|
+
}
|