@kafca/agentdock 0.1.43 → 0.1.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +6 -0
  2. package/dist/renderer/assets/{Badge-CbG6yRSm.js → Badge-Qk9qHovH.js} +1 -1
  3. package/dist/renderer/assets/{Chat-BvNNWGGX.js → Chat-BK-fm0Xr.js} +1 -1
  4. package/dist/renderer/assets/{ChatMarkdown-Ct-PvJB8.js → ChatMarkdown-BKdW8OON.js} +1 -1
  5. package/dist/renderer/assets/{Config-lQX2qVVV.js → Config-B93CQGvY.js} +1 -1
  6. package/dist/renderer/assets/{CronList-Dj_3muVG.js → CronList-Z3TvSRKP.js} +1 -1
  7. package/dist/renderer/assets/{Dashboard-eh_RhGYb.js → Dashboard-Ch3QkWwf.js} +2 -2
  8. package/dist/renderer/assets/{EmptyState-Bn2Qh14U.js → EmptyState-kD9yAwtw.js} +1 -1
  9. package/dist/renderer/assets/{KnowledgeDetail-Bsz6Ou41.js → KnowledgeDetail-BAMCLrjz.js} +1 -1
  10. package/dist/renderer/assets/{KnowledgeHome-Cgj4XFXn.js → KnowledgeHome-ChvjOfrc.js} +1 -1
  11. package/dist/renderer/assets/{Logs-Dp-NF6GN.js → Logs-Cv0nth3j.js} +1 -1
  12. package/dist/renderer/assets/{Modal-B6YPPBN6.js → Modal-DwY4lecS.js} +1 -1
  13. package/dist/renderer/assets/{MonitorList-xEZWg460.js → MonitorList-JEvMoWUm.js} +1 -1
  14. package/dist/renderer/assets/{Page-Bn7RjGOS.js → Page-CnB9ePsb.js} +1 -1
  15. package/dist/renderer/assets/{ProjectDetail-Cv_jIE61.js → ProjectDetail-DJa3sMmU.js} +1 -1
  16. package/dist/renderer/assets/{ProjectList-BDk5_gqY.js → ProjectList-D6CXumR_.js} +1 -1
  17. package/dist/renderer/assets/{Select-xshfMIEQ.js → Select-zfoksLGB.js} +1 -1
  18. package/dist/renderer/assets/{SessionChat-BkHy90z3.js → SessionChat-DUTh4WoM.js} +1 -1
  19. package/dist/renderer/assets/{SessionList-BZyXT92L.js → SessionList-D9o-lUst.js} +1 -1
  20. package/dist/renderer/assets/{ThreadChat-DUOIyXrS.js → ThreadChat-BYXlT-Kc.js} +1 -1
  21. package/dist/renderer/assets/{Workspace-Dl80fNEC.js → Workspace-BrhSwRQ-.js} +1 -1
  22. package/dist/renderer/assets/{arrow-left-C54ZI-AS.js → arrow-left-CNEAuElg.js} +1 -1
  23. package/dist/renderer/assets/{book-open-Dl7GO0zn.js → book-open-BNJMhNI-.js} +1 -1
  24. package/dist/renderer/assets/{check-CiqgtrOL.js → check-BnclClRU.js} +1 -1
  25. package/dist/renderer/assets/{chevron-down-DTL2OuVh.js → chevron-down-Bh53e0O-.js} +1 -1
  26. package/dist/renderer/assets/{circle-DaVKfa3P.js → circle-ClZTWCWO.js} +1 -1
  27. package/dist/renderer/assets/{desktop-jaAiayNB.js → desktop-C5vBT0yM.js} +1 -1
  28. package/dist/renderer/assets/{funnel-CrmnLMJI.js → funnel-_5hsz0xI.js} +1 -1
  29. package/dist/renderer/assets/{heart-BMfQI7y_.js → heart-u4Q1aOqj.js} +1 -1
  30. package/dist/renderer/assets/{index-HPNb1zfx.js → index-B36eowj-.js} +2 -2
  31. package/dist/renderer/assets/{index-nreiKY1f.js → index-BNFx4uYv.js} +1 -1
  32. package/dist/renderer/assets/{pencil-wTQ4l465.js → pencil-DLh9QNmT.js} +1 -1
  33. package/dist/renderer/assets/{play-BK--MXmw.js → play-BDTk9Scm.js} +1 -1
  34. package/dist/renderer/assets/{plug-BIx9fjip.js → plug-BG0JHW6B.js} +1 -1
  35. package/dist/renderer/assets/{plus-Dn7JFWQ8.js → plus-BSc8POsz.js} +1 -1
  36. package/dist/renderer/assets/projects-DwfZwxDW.js +1 -0
  37. package/dist/renderer/assets/{save-CC3zOSWb.js → save-D5AO_G6a.js} +1 -1
  38. package/dist/renderer/assets/{search-CDTy9Czu.js → search-CniPk7Ey.js} +1 -1
  39. package/dist/renderer/assets/{server-DtQk3t2a.js → server-GsXyQAZm.js} +1 -1
  40. package/dist/renderer/assets/{session-utils-4ep5D66T.js → session-utils-BDxzgTBC.js} +1 -1
  41. package/dist/renderer/assets/{sessions-Dor1LteH.js → sessions-CLcKA1Tt.js} +1 -1
  42. package/dist/renderer/assets/{shield-check-Cz2LpU6r.js → shield-check-4i0E3hmf.js} +1 -1
  43. package/dist/renderer/assets/{trash-2-C2EmWFZV.js → trash-2-DhtOCsO5.js} +1 -1
  44. package/dist/renderer/assets/{user-I8B-cs5A.js → user-BtC_pVXV.js} +1 -1
  45. package/dist/renderer/assets/{wifi-off-CPevTQJn.js → wifi-off-BHPzPY_B.js} +1 -1
  46. package/dist/renderer/index.html +1 -1
  47. package/dist-electron/services/local-ai-core/src/channel/lark/config.js +2 -0
  48. package/dist-electron/services/local-ai-core/src/channel/lark/inbound.js +114 -0
  49. package/dist-electron/services/local-ai-core/src/channel/lark/local-core-lark-gateway.js +52 -65
  50. package/dist-electron/services/local-ai-core/src/channel/shared/thread-routing.js +34 -0
  51. package/dist-electron/services/local-ai-core/src/channel/weixin/local-core-weixin-gateway.js +12 -18
  52. package/dist-electron/tests/integration/channel-gateways.test.js +32 -0
  53. package/dist-electron/tests/integration/lark-gateway.test.js +1918 -0
  54. package/dist-electron/tests/integration/local-core-acp-progress.test.js +1772 -0
  55. package/dist-electron/tests/integration/local-core-refactor.test.js +0 -4544
  56. package/dist-electron/tests/integration/local-core-routes.test.js +204 -0
  57. package/dist-electron/tests/integration/weixin-gateway.test.js +927 -0
  58. package/package.json +1 -1
  59. package/dist/renderer/assets/projects-brJZgwEJ.js +0 -1
@@ -0,0 +1,1918 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const node_test_1 = __importDefault(require("node:test"));
7
+ const strict_1 = __importDefault(require("node:assert/strict"));
8
+ const node_fs_1 = require("node:fs");
9
+ const node_os_1 = require("node:os");
10
+ const node_path_1 = require("node:path");
11
+ const node_stream_1 = require("node:stream");
12
+ const local_core_lark_gateway_js_1 = require("../../services/local-ai-core/src/channel/lark/local-core-lark-gateway.js");
13
+ const runtime_state_js_1 = require("../../services/local-ai-core/src/channel/lark/runtime-state.js");
14
+ function extractLarkCreatedMessage(request) {
15
+ const msgType = String(request.data?.msg_type || '');
16
+ const content = JSON.parse(String(request.data?.content || '{}'));
17
+ return {
18
+ msgType,
19
+ content,
20
+ text: extractLarkCreatedMessageText(msgType, content),
21
+ };
22
+ }
23
+ function extractLarkCreatedMessageText(msgType, content) {
24
+ if (msgType === 'interactive') {
25
+ return String(content.elements?.[0]?.content || content.body?.elements?.[0]?.content || '');
26
+ }
27
+ if (msgType === 'post') {
28
+ return (content.zh_cn?.content || [])
29
+ .map((line) => (Array.isArray(line) ? line : [])
30
+ .map((item) => {
31
+ return String(item?.text || item?.href || '');
32
+ })
33
+ .join(''))
34
+ .join('\n');
35
+ }
36
+ return String(content.text || '');
37
+ }
38
+ function findLarkPostMdText(content) {
39
+ for (const line of content.zh_cn?.content || []) {
40
+ for (const item of Array.isArray(line) ? line : []) {
41
+ if (item?.tag === 'md') {
42
+ return String(item.text || '');
43
+ }
44
+ }
45
+ }
46
+ return '';
47
+ }
48
+ (0, node_test_1.default)('lark bridge sends completed thought once before final answer', async () => {
49
+ const createdMessages = [];
50
+ const patchedCards = [];
51
+ const storedMessageIds = [];
52
+ const client = {
53
+ im: {
54
+ message: {
55
+ create: async (request) => {
56
+ const messageId = `lark-msg-${createdMessages.length + 1}`;
57
+ const message = extractLarkCreatedMessage(request);
58
+ createdMessages.push({
59
+ messageId,
60
+ msgType: message.msgType,
61
+ text: message.text,
62
+ content: message.content,
63
+ });
64
+ return { data: { message_id: messageId } };
65
+ },
66
+ patch: async (request) => {
67
+ const card = JSON.parse(String(request.data.content || '{}'));
68
+ patchedCards.push({
69
+ messageId: String(request.path.message_id || ''),
70
+ text: String(card.elements?.[0]?.content || ''),
71
+ });
72
+ },
73
+ },
74
+ },
75
+ };
76
+ const gateway = new local_core_lark_gateway_js_1.LocalCoreLarkGateway({
77
+ store: {
78
+ getPlatformThreadBinding: () => ({
79
+ workspace_id: 'default',
80
+ platform: 'lark',
81
+ chat_id: 'chat-1',
82
+ platform_user_id: 'user-1',
83
+ thread_id: 'thread-1',
84
+ last_platform_message_id: null,
85
+ }),
86
+ updatePlatformThreadMessageId: (_workspaceId, _chatId, _platformUserId, messageId) => {
87
+ storedMessageIds.push(messageId);
88
+ },
89
+ },
90
+ readConfig: async () => null,
91
+ getWorkspaceRouter: () => ({}),
92
+ eventBus: { emit: () => { }, on: () => () => { } },
93
+ });
94
+ const internals = gateway;
95
+ internals.runtime.set('default', {
96
+ workspaceId: 'default',
97
+ enabled: true,
98
+ status: 'running',
99
+ connected: true,
100
+ appId: 'app-1',
101
+ client,
102
+ });
103
+ internals.threadRouting.set('session:thread-1', {
104
+ workspaceId: 'default',
105
+ platformUserId: 'user-1',
106
+ chatId: 'chat-1',
107
+ threadId: 'thread-1',
108
+ });
109
+ await gateway.onBridgeEvent({
110
+ type: 'preview_start',
111
+ sessionKey: 'session:thread-1',
112
+ replyCtx: 'run-1',
113
+ previewHandle: 'thought-preview-1',
114
+ bridgeKind: 'thought',
115
+ content: '先理解问题',
116
+ });
117
+ await gateway.onBridgeEvent({
118
+ type: 'update_message',
119
+ sessionKey: 'session:thread-1',
120
+ replyCtx: 'run-1',
121
+ previewHandle: 'thought-preview-1',
122
+ bridgeKind: 'thought',
123
+ content: '先理解问题,再检查代码',
124
+ });
125
+ await gateway.onBridgeEvent({
126
+ type: 'reply',
127
+ sessionKey: 'session:thread-1',
128
+ replyCtx: 'run-1',
129
+ content: '最终回答',
130
+ });
131
+ strict_1.default.deepEqual(createdMessages.map((message) => message.msgType), ['interactive', 'post']);
132
+ strict_1.default.equal(createdMessages[0]?.text, '先理解问题,再检查代码');
133
+ strict_1.default.equal(createdMessages[1]?.text, '最终回答');
134
+ strict_1.default.equal(patchedCards.length, 0);
135
+ strict_1.default.deepEqual(storedMessageIds, ['lark-msg-2']);
136
+ });
137
+ (0, node_test_1.default)('lark bridge sends final reply as its own post message instead of streaming draft', async () => {
138
+ const createdMessages = [];
139
+ const patchedCards = [];
140
+ const storedMessageIds = [];
141
+ const client = {
142
+ im: {
143
+ message: {
144
+ create: async (request) => {
145
+ const messageId = `lark-msg-${createdMessages.length + 1}`;
146
+ const message = extractLarkCreatedMessage(request);
147
+ createdMessages.push({
148
+ messageId,
149
+ msgType: message.msgType,
150
+ text: message.text,
151
+ content: message.content,
152
+ });
153
+ return { data: { message_id: messageId } };
154
+ },
155
+ patch: async (request) => {
156
+ const card = JSON.parse(String(request.data.content || '{}'));
157
+ patchedCards.push({
158
+ messageId: String(request.path.message_id || ''),
159
+ text: String(card.elements?.[0]?.content || ''),
160
+ });
161
+ },
162
+ },
163
+ },
164
+ };
165
+ const gateway = new local_core_lark_gateway_js_1.LocalCoreLarkGateway({
166
+ store: {
167
+ getPlatformThreadBinding: () => ({
168
+ workspace_id: 'default',
169
+ platform: 'lark',
170
+ chat_id: 'chat-1',
171
+ platform_user_id: 'user-1',
172
+ thread_id: 'thread-1',
173
+ last_platform_message_id: null,
174
+ }),
175
+ updatePlatformThreadMessageId: (_workspaceId, _chatId, _platformUserId, messageId) => {
176
+ storedMessageIds.push(messageId);
177
+ },
178
+ },
179
+ readConfig: async () => null,
180
+ getWorkspaceRouter: () => ({}),
181
+ eventBus: { emit: () => { }, on: () => () => { } },
182
+ });
183
+ const internals = gateway;
184
+ internals.runtime.set('default', {
185
+ workspaceId: 'default',
186
+ enabled: true,
187
+ status: 'running',
188
+ connected: true,
189
+ appId: 'app-1',
190
+ client,
191
+ });
192
+ internals.threadRouting.set('session:thread-1', {
193
+ workspaceId: 'default',
194
+ platformUserId: 'user-1',
195
+ chatId: 'chat-1',
196
+ threadId: 'thread-1',
197
+ });
198
+ await gateway.onBridgeEvent({
199
+ type: 'update_message',
200
+ sessionKey: 'session:thread-1',
201
+ replyCtx: 'run-1',
202
+ content: '流式中的最终回答草稿',
203
+ });
204
+ await gateway.onBridgeEvent({
205
+ type: 'reply',
206
+ sessionKey: 'session:thread-1',
207
+ replyCtx: 'run-1',
208
+ content: '真正最终回答',
209
+ });
210
+ strict_1.default.deepEqual(createdMessages.map((message) => message.msgType), ['post']);
211
+ strict_1.default.deepEqual(createdMessages.map((message) => message.text), ['真正最终回答']);
212
+ strict_1.default.deepEqual(patchedCards, []);
213
+ strict_1.default.deepEqual(storedMessageIds, ['lark-msg-1']);
214
+ });
215
+ (0, node_test_1.default)('lark bridge sends completed assistant segments as separate non-final messages', async () => {
216
+ const createdMessages = [];
217
+ const patchedCards = [];
218
+ const storedMessageIds = [];
219
+ const client = {
220
+ im: {
221
+ message: {
222
+ create: async (request) => {
223
+ const messageId = `lark-msg-${createdMessages.length + 1}`;
224
+ const message = extractLarkCreatedMessage(request);
225
+ createdMessages.push({
226
+ messageId,
227
+ msgType: message.msgType,
228
+ text: message.text,
229
+ content: message.content,
230
+ });
231
+ return { data: { message_id: messageId } };
232
+ },
233
+ patch: async (request) => {
234
+ const card = JSON.parse(String(request.data.content || '{}'));
235
+ patchedCards.push({
236
+ messageId: String(request.path.message_id || ''),
237
+ text: String(card.elements?.[0]?.content || ''),
238
+ });
239
+ },
240
+ },
241
+ },
242
+ };
243
+ const gateway = new local_core_lark_gateway_js_1.LocalCoreLarkGateway({
244
+ store: {
245
+ getPlatformThreadBinding: () => ({
246
+ workspace_id: 'default',
247
+ platform: 'lark',
248
+ chat_id: 'chat-1',
249
+ platform_user_id: 'user-1',
250
+ thread_id: 'thread-1',
251
+ last_platform_message_id: null,
252
+ }),
253
+ updatePlatformThreadMessageId: (_workspaceId, _chatId, _platformUserId, messageId) => {
254
+ storedMessageIds.push(messageId);
255
+ },
256
+ },
257
+ readConfig: async () => null,
258
+ getWorkspaceRouter: () => ({}),
259
+ eventBus: { emit: () => { }, on: () => () => { } },
260
+ });
261
+ const internals = gateway;
262
+ internals.runtime.set('default', {
263
+ workspaceId: 'default',
264
+ enabled: true,
265
+ status: 'running',
266
+ connected: true,
267
+ appId: 'app-1',
268
+ client,
269
+ });
270
+ internals.threadRouting.set('session:thread-1', {
271
+ workspaceId: 'default',
272
+ platformUserId: 'user-1',
273
+ chatId: 'chat-1',
274
+ threadId: 'thread-1',
275
+ });
276
+ await gateway.onBridgeEvent({
277
+ type: 'reply',
278
+ sessionKey: 'session:thread-1',
279
+ replyCtx: 'run-1',
280
+ messageId: 'run-1-assistant-1',
281
+ bridgeKind: 'assistant',
282
+ content: '我先检查一下',
283
+ });
284
+ await gateway.onBridgeEvent({
285
+ type: 'reply',
286
+ sessionKey: 'session:thread-1',
287
+ replyCtx: 'run-1',
288
+ content: '最终回答',
289
+ });
290
+ strict_1.default.deepEqual(createdMessages.map((message) => message.msgType), ['post', 'post']);
291
+ strict_1.default.deepEqual(createdMessages.map((message) => message.text), ['我先检查一下', '最终回答']);
292
+ strict_1.default.deepEqual(patchedCards, []);
293
+ strict_1.default.deepEqual(storedMessageIds, ['lark-msg-2']);
294
+ });
295
+ (0, node_test_1.default)('lark bridge creates final replies without patching prior final messages', async () => {
296
+ const createdMessages = [];
297
+ const patchedCards = [];
298
+ const client = {
299
+ im: {
300
+ message: {
301
+ create: async (request) => {
302
+ const messageId = `lark-msg-${createdMessages.length + 1}`;
303
+ const message = extractLarkCreatedMessage(request);
304
+ createdMessages.push({
305
+ messageId,
306
+ msgType: message.msgType,
307
+ text: message.text,
308
+ content: message.content,
309
+ });
310
+ return { data: { message_id: messageId } };
311
+ },
312
+ patch: async (request) => {
313
+ const card = JSON.parse(String(request.data.content || '{}'));
314
+ patchedCards.push({
315
+ messageId: String(request.path.message_id || ''),
316
+ text: String(card.elements?.[0]?.content || ''),
317
+ });
318
+ },
319
+ },
320
+ },
321
+ };
322
+ const gateway = new local_core_lark_gateway_js_1.LocalCoreLarkGateway({
323
+ store: {
324
+ getPlatformThreadBinding: () => ({
325
+ workspace_id: 'default',
326
+ platform: 'lark',
327
+ chat_id: 'chat-1',
328
+ platform_user_id: 'user-1',
329
+ thread_id: 'thread-1',
330
+ last_platform_message_id: null,
331
+ }),
332
+ updatePlatformThreadMessageId: () => { },
333
+ },
334
+ readConfig: async () => null,
335
+ getWorkspaceRouter: () => ({}),
336
+ eventBus: { emit: () => { }, on: () => () => { } },
337
+ });
338
+ const internals = gateway;
339
+ internals.runtime.set('default', {
340
+ workspaceId: 'default',
341
+ enabled: true,
342
+ status: 'running',
343
+ connected: true,
344
+ appId: 'app-1',
345
+ client,
346
+ });
347
+ internals.threadRouting.set('session:thread-1', {
348
+ workspaceId: 'default',
349
+ platformUserId: 'user-1',
350
+ chatId: 'chat-1',
351
+ threadId: 'thread-1',
352
+ });
353
+ await gateway.onBridgeEvent({
354
+ type: 'reply',
355
+ sessionKey: 'session:thread-1',
356
+ replyCtx: 'run-1',
357
+ content: 'first final',
358
+ });
359
+ await gateway.onBridgeEvent({
360
+ type: 'reply',
361
+ sessionKey: 'session:thread-1',
362
+ replyCtx: 'run-2',
363
+ content: 'second final',
364
+ });
365
+ strict_1.default.deepEqual(createdMessages.map((message) => message.msgType), ['post', 'post']);
366
+ strict_1.default.deepEqual(createdMessages.map((message) => message.text), ['first final', 'second final']);
367
+ strict_1.default.deepEqual(patchedCards, []);
368
+ });
369
+ (0, node_test_1.default)('lark bridge sends tool and final as separate post messages without patching tool progress', async () => {
370
+ const createdMessages = [];
371
+ const patchedCards = [];
372
+ const storedMessageIds = [];
373
+ const client = {
374
+ im: {
375
+ message: {
376
+ create: async (request) => {
377
+ const messageId = `lark-msg-${createdMessages.length + 1}`;
378
+ const message = extractLarkCreatedMessage(request);
379
+ createdMessages.push({
380
+ messageId,
381
+ msgType: message.msgType,
382
+ text: message.text,
383
+ content: message.content,
384
+ });
385
+ return { data: { message_id: messageId } };
386
+ },
387
+ patch: async (request) => {
388
+ const card = JSON.parse(String(request.data.content || '{}'));
389
+ patchedCards.push({
390
+ messageId: String(request.path.message_id || ''),
391
+ text: String(card.elements?.[0]?.content || ''),
392
+ });
393
+ },
394
+ },
395
+ },
396
+ };
397
+ const gateway = new local_core_lark_gateway_js_1.LocalCoreLarkGateway({
398
+ store: {
399
+ getPlatformThreadBinding: () => ({
400
+ workspace_id: 'default',
401
+ platform: 'lark',
402
+ chat_id: 'chat-1',
403
+ platform_user_id: 'user-1',
404
+ thread_id: 'thread-1',
405
+ last_platform_message_id: null,
406
+ }),
407
+ updatePlatformThreadMessageId: (_workspaceId, _chatId, _platformUserId, messageId) => {
408
+ storedMessageIds.push(messageId);
409
+ },
410
+ },
411
+ readConfig: async () => null,
412
+ getWorkspaceRouter: () => ({}),
413
+ eventBus: { emit: () => { }, on: () => () => { } },
414
+ });
415
+ const internals = gateway;
416
+ internals.runtime.set('default', {
417
+ workspaceId: 'default',
418
+ enabled: true,
419
+ status: 'running',
420
+ connected: true,
421
+ appId: 'app-1',
422
+ client,
423
+ });
424
+ internals.threadRouting.set('session:thread-1', {
425
+ workspaceId: 'default',
426
+ platformUserId: 'user-1',
427
+ chatId: 'chat-1',
428
+ threadId: 'thread-1',
429
+ });
430
+ await gateway.onBridgeEvent({
431
+ type: 'update_message',
432
+ sessionKey: 'session:thread-1',
433
+ replyCtx: 'run-1',
434
+ content: '好的,文件存在,现在发送给你:已',
435
+ });
436
+ await gateway.onBridgeEvent({
437
+ type: 'reply',
438
+ sessionKey: 'session:thread-1',
439
+ replyCtx: 'run-1',
440
+ messageId: 'tool-1',
441
+ bridgeKind: 'tool',
442
+ content: 'bash completed',
443
+ toolCall: {
444
+ id: 'tool-1',
445
+ name: 'bash',
446
+ status: 'completed',
447
+ output: 'Sent file CLAUDE.md to chat-1: msg-file-1',
448
+ },
449
+ });
450
+ await gateway.onBridgeEvent({
451
+ type: 'reply',
452
+ sessionKey: 'session:thread-1',
453
+ replyCtx: 'run-1',
454
+ content: '好的,文件存在,现在发送给你:已发送!`CLAUDE.md` 文件已经发出去了,请查收',
455
+ });
456
+ strict_1.default.deepEqual(createdMessages.map((message) => message.msgType), ['post', 'post']);
457
+ strict_1.default.deepEqual(createdMessages.map((message) => message.text), [
458
+ '🔧 bash',
459
+ '好的,文件存在,现在发送给你:已发送!`CLAUDE.md` 文件已经发出去了,请查收',
460
+ ]);
461
+ strict_1.default.deepEqual(patchedCards, []);
462
+ strict_1.default.deepEqual(storedMessageIds, ['lark-msg-2']);
463
+ });
464
+ (0, node_test_1.default)('lark bridge flushes interleaved thought segments before tools and final', async () => {
465
+ const createdMessages = [];
466
+ const patchedCards = [];
467
+ const storedMessageIds = [];
468
+ const client = {
469
+ im: {
470
+ message: {
471
+ create: async (request) => {
472
+ const messageId = `lark-msg-${createdMessages.length + 1}`;
473
+ const message = extractLarkCreatedMessage(request);
474
+ createdMessages.push({
475
+ messageId,
476
+ msgType: message.msgType,
477
+ text: message.text,
478
+ content: message.content,
479
+ });
480
+ return { data: { message_id: messageId } };
481
+ },
482
+ patch: async (request) => {
483
+ const card = JSON.parse(String(request.data.content || '{}'));
484
+ patchedCards.push({
485
+ messageId: String(request.path.message_id || ''),
486
+ text: String(card.elements?.[0]?.content || ''),
487
+ });
488
+ },
489
+ },
490
+ },
491
+ };
492
+ const gateway = new local_core_lark_gateway_js_1.LocalCoreLarkGateway({
493
+ store: {
494
+ getPlatformThreadBinding: () => ({
495
+ workspace_id: 'default',
496
+ platform: 'lark',
497
+ chat_id: 'chat-1',
498
+ platform_user_id: 'user-1',
499
+ thread_id: 'thread-1',
500
+ last_platform_message_id: 'old-message',
501
+ }),
502
+ updatePlatformThreadMessageId: (_workspaceId, _chatId, _platformUserId, messageId) => {
503
+ storedMessageIds.push(messageId);
504
+ },
505
+ },
506
+ readConfig: async () => null,
507
+ getWorkspaceRouter: () => ({}),
508
+ eventBus: { emit: () => { }, on: () => () => { } },
509
+ });
510
+ const internals = gateway;
511
+ internals.runtime.set('default', {
512
+ workspaceId: 'default',
513
+ enabled: true,
514
+ status: 'running',
515
+ connected: true,
516
+ appId: 'app-1',
517
+ client,
518
+ });
519
+ internals.threadRouting.set('session:thread-1', {
520
+ workspaceId: 'default',
521
+ platformUserId: 'user-1',
522
+ chatId: 'chat-1',
523
+ threadId: 'thread-1',
524
+ });
525
+ await gateway.onBridgeEvent({
526
+ type: 'preview_start',
527
+ sessionKey: 'session:thread-1',
528
+ replyCtx: 'run-1',
529
+ previewHandle: 'thought-preview-1',
530
+ bridgeKind: 'thought',
531
+ content: '先理解',
532
+ });
533
+ await gateway.onBridgeEvent({
534
+ type: 'update_message',
535
+ sessionKey: 'session:thread-1',
536
+ replyCtx: 'run-1',
537
+ previewHandle: 'thought-preview-1',
538
+ bridgeKind: 'thought',
539
+ content: '先理解用户需求',
540
+ });
541
+ await gateway.onBridgeEvent({
542
+ type: 'reply',
543
+ sessionKey: 'session:thread-1',
544
+ replyCtx: 'run-1',
545
+ messageId: 'run-1-tool-1',
546
+ bridgeKind: 'tool',
547
+ content: 'Terminal running',
548
+ toolCall: {
549
+ id: 'call-1',
550
+ name: 'Terminal',
551
+ status: 'running',
552
+ input: { command: 'uname -a', description: 'Get system info' },
553
+ output: '',
554
+ },
555
+ });
556
+ await gateway.onBridgeEvent({
557
+ type: 'reply',
558
+ sessionKey: 'session:thread-1',
559
+ replyCtx: 'run-1',
560
+ messageId: 'run-1-tool-1',
561
+ bridgeKind: 'tool',
562
+ content: 'Terminal completed',
563
+ toolCall: {
564
+ id: 'call-1',
565
+ name: 'Terminal',
566
+ status: 'completed',
567
+ input: { command: 'uname -a', description: 'Get system info' },
568
+ output: 'Linux',
569
+ },
570
+ });
571
+ await gateway.onBridgeEvent({
572
+ type: 'update_message',
573
+ sessionKey: 'session:thread-1',
574
+ replyCtx: 'run-1',
575
+ previewHandle: 'thought-preview-1',
576
+ bridgeKind: 'thought',
577
+ content: '看到了 Linux',
578
+ });
579
+ await gateway.onBridgeEvent({
580
+ type: 'reply',
581
+ sessionKey: 'session:thread-1',
582
+ replyCtx: 'run-1',
583
+ content: '最终回答',
584
+ });
585
+ strict_1.default.deepEqual(createdMessages.map((message) => message.msgType), ['interactive', 'post', 'interactive', 'post']);
586
+ strict_1.default.deepEqual(createdMessages.map((message) => message.text), [
587
+ '先理解用户需求',
588
+ '🔧 Terminal\n\n```json\n{\n "command": "uname -a",\n "description": "Get system info"\n}\n```',
589
+ '看到了 Linux',
590
+ '最终回答',
591
+ ]);
592
+ strict_1.default.equal(findLarkPostMdText(createdMessages[1]?.content), '🔧 Terminal\n\n```json\n{\n "command": "uname -a",\n "description": "Get system info"\n}\n```');
593
+ strict_1.default.deepEqual(patchedCards, []);
594
+ strict_1.default.deepEqual(storedMessageIds, ['lark-msg-4']);
595
+ });
596
+ (0, node_test_1.default)('lark bridge does not stream thought updates before completion', async () => {
597
+ const createdCards = [];
598
+ const patchedCards = [];
599
+ const client = {
600
+ im: {
601
+ message: {
602
+ create: async (request) => {
603
+ const messageId = `lark-msg-${createdCards.length + 1}`;
604
+ const card = JSON.parse(String(request.data.content || '{}'));
605
+ createdCards.push({
606
+ messageId,
607
+ text: String(card.elements?.[0]?.content || ''),
608
+ });
609
+ return { data: { message_id: messageId } };
610
+ },
611
+ patch: async (request) => {
612
+ const card = JSON.parse(String(request.data.content || '{}'));
613
+ patchedCards.push({
614
+ messageId: String(request.path.message_id || ''),
615
+ text: String(card.elements?.[0]?.content || ''),
616
+ });
617
+ },
618
+ },
619
+ },
620
+ };
621
+ const gateway = new local_core_lark_gateway_js_1.LocalCoreLarkGateway({
622
+ store: {
623
+ getPlatformThreadBinding: () => ({
624
+ workspace_id: 'default',
625
+ platform: 'lark',
626
+ chat_id: 'chat-1',
627
+ platform_user_id: 'user-1',
628
+ thread_id: 'thread-1',
629
+ last_platform_message_id: null,
630
+ }),
631
+ updatePlatformThreadMessageId: () => { },
632
+ },
633
+ readConfig: async () => null,
634
+ getWorkspaceRouter: () => ({}),
635
+ eventBus: { emit: () => { }, on: () => () => { } },
636
+ });
637
+ const internals = gateway;
638
+ internals.runtime.set('default', {
639
+ workspaceId: 'default',
640
+ enabled: true,
641
+ status: 'running',
642
+ connected: true,
643
+ appId: 'app-1',
644
+ client,
645
+ });
646
+ internals.threadRouting.set('session:thread-1', {
647
+ workspaceId: 'default',
648
+ platformUserId: 'user-1',
649
+ chatId: 'chat-1',
650
+ threadId: 'thread-1',
651
+ });
652
+ await gateway.onBridgeEvent({
653
+ type: 'typing_start',
654
+ sessionKey: 'session:thread-1',
655
+ replyCtx: 'run-1',
656
+ });
657
+ await gateway.onBridgeEvent({
658
+ type: 'preview_start',
659
+ sessionKey: 'session:thread-1',
660
+ replyCtx: 'run-1',
661
+ previewHandle: 'thought-preview-1',
662
+ bridgeKind: 'thought',
663
+ content: 'The user',
664
+ });
665
+ await gateway.onBridgeEvent({
666
+ type: 'update_message',
667
+ sessionKey: 'session:thread-1',
668
+ replyCtx: 'run-1',
669
+ previewHandle: 'thought-preview-1',
670
+ bridgeKind: 'thought',
671
+ content: 'The user sent a short casual message.',
672
+ });
673
+ await gateway.onBridgeEvent({
674
+ type: 'update_message',
675
+ sessionKey: 'session:thread-1',
676
+ replyCtx: 'run-1',
677
+ previewHandle: 'thought-preview-1',
678
+ bridgeKind: 'thought',
679
+ content: 'The user sent a short casual message. I should reply briefly.',
680
+ });
681
+ await gateway.onBridgeEvent({
682
+ type: 'typing_stop',
683
+ sessionKey: 'session:thread-1',
684
+ replyCtx: 'run-1',
685
+ });
686
+ strict_1.default.deepEqual(createdCards.map((card) => card.text), ['The user sent a short casual message. I should reply briefly.']);
687
+ strict_1.default.deepEqual(patchedCards.map((card) => card.text), []);
688
+ strict_1.default.ok(!createdCards.some((card) => /处理中|正在思考/.test(card.text)));
689
+ });
690
+ (0, node_test_1.default)('lark permission requests render as clickable card buttons', async () => {
691
+ const createdCards = [];
692
+ const patchedCards = [];
693
+ const threadActions = [];
694
+ const client = {
695
+ im: {
696
+ message: {
697
+ create: async (request) => {
698
+ createdCards.push(JSON.parse(String(request.data.content || '{}')));
699
+ return { data: { message_id: 'permission-msg-1' } };
700
+ },
701
+ patch: async (request) => {
702
+ patchedCards.push({
703
+ messageId: request.path?.message_id,
704
+ card: JSON.parse(String(request.data.content || '{}')),
705
+ });
706
+ },
707
+ },
708
+ },
709
+ };
710
+ const gateway = new local_core_lark_gateway_js_1.LocalCoreLarkGateway({
711
+ store: {
712
+ getPlatformThreadBinding: () => ({
713
+ workspace_id: 'default',
714
+ platform: 'lark',
715
+ chat_id: 'chat-1',
716
+ platform_user_id: 'user-1',
717
+ thread_id: 'thread-1',
718
+ last_platform_message_id: null,
719
+ }),
720
+ },
721
+ readConfig: async () => null,
722
+ getWorkspaceRouter: () => ({
723
+ sendThreadAction: async (threadId, action) => {
724
+ threadActions.push({ threadId, action });
725
+ return { runId: 'run-1' };
726
+ },
727
+ }),
728
+ eventBus: { emit: () => { }, on: () => () => { } },
729
+ });
730
+ const internals = gateway;
731
+ internals.runtime.set('default', {
732
+ workspaceId: 'default',
733
+ enabled: true,
734
+ status: 'running',
735
+ connected: true,
736
+ appId: 'app-1',
737
+ cardActionsEnabled: true,
738
+ client,
739
+ });
740
+ internals.threadRouting.set('session:thread-1', {
741
+ workspaceId: 'default',
742
+ platformUserId: 'user-1',
743
+ chatId: 'chat-1',
744
+ threadId: 'thread-1',
745
+ });
746
+ await gateway.onBridgeEvent({
747
+ type: 'buttons',
748
+ sessionKey: 'session:thread-1',
749
+ replyCtx: 'run-1',
750
+ content: [
751
+ '等待工具确认',
752
+ '',
753
+ 'Terminal',
754
+ '',
755
+ 'parameters:',
756
+ '{"command":"ls"}',
757
+ '',
758
+ '请选择一个选项继续执行。',
759
+ ].join('\n'),
760
+ buttonRows: [[
761
+ { text: 'allow', data: 'allow' },
762
+ { text: 'allow all', data: 'allow all' },
763
+ { text: 'deny', data: 'deny' },
764
+ ]],
765
+ });
766
+ const actionElement = createdCards[0]?.elements?.find((element) => element.tag === 'action');
767
+ strict_1.default.equal(createdCards.length, 1);
768
+ strict_1.default.match(createdCards[0]?.elements?.[0]?.content || '', /需要工具确认/);
769
+ strict_1.default.deepEqual(actionElement?.actions?.map((action) => ({
770
+ label: action.text?.content,
771
+ type: action.type,
772
+ response: action.value?.response,
773
+ threadId: action.value?.thread_id,
774
+ })), [
775
+ { label: '允许一次', type: 'primary', response: 'allow', threadId: 'thread-1' },
776
+ { label: '始终允许', type: 'default', response: 'allow all', threadId: 'thread-1' },
777
+ { label: '拒绝', type: 'danger', response: 'deny', threadId: 'thread-1' },
778
+ ]);
779
+ await internals.handleCardActionEvent('default', {
780
+ event: {
781
+ action: {
782
+ value: {
783
+ action: 'permission_response',
784
+ response: 'allow all',
785
+ thread_id: 'thread-1',
786
+ session_key: 'session:thread-1',
787
+ },
788
+ },
789
+ context: {
790
+ open_message_id: 'permission-msg-1',
791
+ },
792
+ },
793
+ });
794
+ strict_1.default.deepEqual(threadActions, [
795
+ { threadId: 'thread-1', action: 'allow all' },
796
+ ]);
797
+ strict_1.default.equal(patchedCards.length, 1);
798
+ strict_1.default.equal(patchedCards[0]?.messageId, 'permission-msg-1');
799
+ strict_1.default.match(patchedCards[0]?.card?.elements?.[0]?.content || '', /工具确认已处理/);
800
+ strict_1.default.equal(patchedCards[0]?.card?.elements?.some((element) => element.tag === 'action'), false);
801
+ });
802
+ (0, node_test_1.default)('lark allow all card action preserves the final reply after tool execution', async () => {
803
+ const createdMessages = [];
804
+ const patchedCards = [];
805
+ let nextMessageId = 1;
806
+ let gateway;
807
+ const client = {
808
+ im: {
809
+ message: {
810
+ create: async (request) => {
811
+ const messageId = `card-${nextMessageId++}`;
812
+ const message = extractLarkCreatedMessage(request);
813
+ createdMessages.push({
814
+ messageId,
815
+ msgType: message.msgType,
816
+ text: message.text,
817
+ content: message.content,
818
+ });
819
+ return { data: { message_id: messageId } };
820
+ },
821
+ patch: async (request) => {
822
+ patchedCards.push({
823
+ messageId: request.path?.message_id,
824
+ card: JSON.parse(String(request.data.content || '{}')),
825
+ });
826
+ },
827
+ },
828
+ },
829
+ };
830
+ gateway = new local_core_lark_gateway_js_1.LocalCoreLarkGateway({
831
+ store: {
832
+ getPlatformThreadBinding: () => ({
833
+ workspace_id: 'default',
834
+ platform: 'lark',
835
+ chat_id: 'chat-1',
836
+ platform_user_id: 'user-1',
837
+ thread_id: 'thread-1',
838
+ last_platform_message_id: null,
839
+ }),
840
+ clearPlatformThreadMessageId: () => { },
841
+ updatePlatformThreadMessageId: () => { },
842
+ },
843
+ readConfig: async () => null,
844
+ getWorkspaceRouter: () => ({
845
+ sendThreadAction: async (threadId, action) => {
846
+ strict_1.default.equal(threadId, 'thread-1');
847
+ strict_1.default.equal(action, 'allow all');
848
+ await gateway.onBridgeEvent({
849
+ type: 'typing_start',
850
+ sessionKey: 'session:thread-1',
851
+ replyCtx: 'run-1',
852
+ });
853
+ await gateway.onBridgeEvent({
854
+ type: 'reply',
855
+ sessionKey: 'session:thread-1',
856
+ replyCtx: 'run-1',
857
+ content: '桌面文件列表:AI进展报告_2026年4月.md',
858
+ });
859
+ return { runId: 'run-1' };
860
+ },
861
+ }),
862
+ eventBus: { emit: () => { }, on: () => () => { } },
863
+ });
864
+ const internals = gateway;
865
+ internals.runtime.set('default', {
866
+ workspaceId: 'default',
867
+ enabled: true,
868
+ status: 'running',
869
+ connected: true,
870
+ appId: 'app-1',
871
+ cardActionsEnabled: true,
872
+ client,
873
+ });
874
+ internals.threadRouting.set('session:thread-1', {
875
+ workspaceId: 'default',
876
+ platformUserId: 'user-1',
877
+ chatId: 'chat-1',
878
+ threadId: 'thread-1',
879
+ });
880
+ internals.outboundTurns.set('session:thread-1', {
881
+ sessionKey: 'session:thread-1',
882
+ progressMessageIds: {},
883
+ permissionMessageId: 'permission-msg-1',
884
+ awaitingPermission: true,
885
+ processing: true,
886
+ previewText: '',
887
+ finalText: '',
888
+ thinkingSteps: [],
889
+ thoughtSegmentSequence: 0,
890
+ toolCalls: [],
891
+ statusLines: [],
892
+ buttonRows: [[{ text: '始终允许', data: 'allow all' }]],
893
+ lastPatchedAt: 0,
894
+ lastPatchedAtByMessageId: {},
895
+ });
896
+ await internals.handleCardActionEvent('default', {
897
+ event: {
898
+ action: {
899
+ value: {
900
+ action: 'permission_response',
901
+ response: 'allow all',
902
+ thread_id: 'thread-1',
903
+ session_key: 'session:thread-1',
904
+ },
905
+ },
906
+ context: {
907
+ open_message_id: 'permission-msg-1',
908
+ },
909
+ },
910
+ });
911
+ strict_1.default.equal(patchedCards[0]?.messageId, 'permission-msg-1');
912
+ strict_1.default.match(patchedCards[0]?.card?.elements?.[0]?.content || '', /工具确认已处理/);
913
+ strict_1.default.equal(createdMessages.length, 1);
914
+ strict_1.default.equal(createdMessages[0]?.msgType, 'post');
915
+ strict_1.default.match(createdMessages[0]?.text || '', /桌面文件列表/);
916
+ });
917
+ (0, node_test_1.default)('lark channel sends tool name and parameters once without streaming output', async () => {
918
+ const createdMessages = [];
919
+ const patchedCards = [];
920
+ const client = {
921
+ im: {
922
+ message: {
923
+ create: async (request) => {
924
+ createdMessages.push(extractLarkCreatedMessage(request));
925
+ return { data: { message_id: 'tool-msg-1' } };
926
+ },
927
+ patch: async (request) => {
928
+ patchedCards.push(JSON.parse(String(request.data.content || '{}')));
929
+ },
930
+ },
931
+ },
932
+ };
933
+ const gateway = new local_core_lark_gateway_js_1.LocalCoreLarkGateway({
934
+ store: {
935
+ getPlatformThreadBinding: () => ({
936
+ workspace_id: 'default',
937
+ platform: 'lark',
938
+ chat_id: 'chat-1',
939
+ platform_user_id: 'user-1',
940
+ thread_id: 'thread-1',
941
+ last_platform_message_id: null,
942
+ }),
943
+ },
944
+ readConfig: async () => null,
945
+ getWorkspaceRouter: () => ({}),
946
+ eventBus: { emit: () => { }, on: () => () => { } },
947
+ });
948
+ const internals = gateway;
949
+ internals.runtime.set('default', {
950
+ workspaceId: 'default',
951
+ enabled: true,
952
+ status: 'running',
953
+ connected: true,
954
+ appId: 'app-1',
955
+ client,
956
+ });
957
+ internals.threadRouting.set('session:thread-1', {
958
+ workspaceId: 'default',
959
+ platformUserId: 'user-1',
960
+ chatId: 'chat-1',
961
+ threadId: 'thread-1',
962
+ });
963
+ await gateway.onBridgeEvent({
964
+ type: 'reply',
965
+ sessionKey: 'session:thread-1',
966
+ replyCtx: 'run-1',
967
+ messageId: 'tool-1',
968
+ bridgeKind: 'tool',
969
+ content: 'Terminal running',
970
+ toolCall: {
971
+ id: 'tool-1',
972
+ name: 'Terminal',
973
+ status: 'running',
974
+ input: { command: 'ls ~/Desktop' },
975
+ output: '',
976
+ },
977
+ });
978
+ await gateway.onBridgeEvent({
979
+ type: 'reply',
980
+ sessionKey: 'session:thread-1',
981
+ replyCtx: 'run-1',
982
+ messageId: 'tool-1',
983
+ bridgeKind: 'tool',
984
+ content: 'Terminal completed',
985
+ toolCall: {
986
+ id: 'tool-1',
987
+ name: 'Terminal',
988
+ status: 'completed',
989
+ input: { command: 'ls ~/Desktop' },
990
+ output: 'secret terminal output',
991
+ },
992
+ });
993
+ const text = createdMessages[0]?.text || '';
994
+ strict_1.default.equal(createdMessages[0]?.msgType, 'post');
995
+ strict_1.default.match(text, /🔧 Terminal/);
996
+ strict_1.default.match(text, /```json\n{\n "command": "ls ~\/Desktop"\n}\n```/);
997
+ strict_1.default.doesNotMatch(text, /参数/);
998
+ strict_1.default.equal(findLarkPostMdText(createdMessages[0]?.content), '🔧 Terminal\n\n```json\n{\n "command": "ls ~/Desktop"\n}\n```');
999
+ strict_1.default.equal(patchedCards.length, 0);
1000
+ strict_1.default.doesNotMatch(text, /completed/);
1001
+ strict_1.default.doesNotMatch(text, /secret terminal output/);
1002
+ });
1003
+ (0, node_test_1.default)('lark card action message id can be extracted from full callback payload', async () => {
1004
+ const patchedCards = [];
1005
+ const threadActions = [];
1006
+ const client = {
1007
+ im: {
1008
+ message: {
1009
+ patch: async (request) => {
1010
+ patchedCards.push({
1011
+ messageId: request.path?.message_id,
1012
+ card: JSON.parse(String(request.data.content || '{}')),
1013
+ });
1014
+ },
1015
+ },
1016
+ },
1017
+ };
1018
+ const gateway = new local_core_lark_gateway_js_1.LocalCoreLarkGateway({
1019
+ store: {},
1020
+ readConfig: async () => null,
1021
+ getWorkspaceRouter: () => ({
1022
+ sendThreadAction: async (threadId, action) => {
1023
+ threadActions.push({ threadId, action });
1024
+ return { runId: 'run-1' };
1025
+ },
1026
+ }),
1027
+ eventBus: { emit: () => { }, on: () => () => { } },
1028
+ });
1029
+ const internals = gateway;
1030
+ internals.runtime.set('default', {
1031
+ workspaceId: 'default',
1032
+ enabled: true,
1033
+ status: 'running',
1034
+ connected: true,
1035
+ appId: 'app-1',
1036
+ client,
1037
+ });
1038
+ await internals.handleCardActionEvent('default', {
1039
+ schema: '2.0',
1040
+ event: {
1041
+ action: {
1042
+ value: {
1043
+ action: 'permission_response',
1044
+ response: 'allow',
1045
+ thread_id: 'thread-1',
1046
+ },
1047
+ },
1048
+ },
1049
+ event_context: {
1050
+ open_message_id: 'permission-msg-nested',
1051
+ },
1052
+ });
1053
+ strict_1.default.deepEqual(threadActions, [
1054
+ { threadId: 'thread-1', action: 'allow' },
1055
+ ]);
1056
+ strict_1.default.equal(patchedCards[0]?.messageId, 'permission-msg-nested');
1057
+ strict_1.default.equal(patchedCards[0]?.card?.elements?.some((element) => element.tag === 'action'), false);
1058
+ });
1059
+ (0, node_test_1.default)('lark permission requests fall back to text commands when card actions are disabled', async () => {
1060
+ const createdCards = [];
1061
+ const gateway = new local_core_lark_gateway_js_1.LocalCoreLarkGateway({
1062
+ store: {
1063
+ getPlatformThreadBinding: () => ({
1064
+ workspace_id: 'default',
1065
+ platform: 'lark',
1066
+ chat_id: 'chat-1',
1067
+ platform_user_id: 'user-1',
1068
+ thread_id: 'thread-1',
1069
+ last_platform_message_id: null,
1070
+ }),
1071
+ },
1072
+ readConfig: async () => null,
1073
+ getWorkspaceRouter: () => ({}),
1074
+ eventBus: { emit: () => { }, on: () => () => { } },
1075
+ });
1076
+ const internals = gateway;
1077
+ internals.runtime.set('default', {
1078
+ workspaceId: 'default',
1079
+ enabled: true,
1080
+ status: 'running',
1081
+ connected: true,
1082
+ appId: 'app-1',
1083
+ cardActionsEnabled: false,
1084
+ client: {
1085
+ im: {
1086
+ message: {
1087
+ create: async (request) => {
1088
+ createdCards.push(JSON.parse(String(request.data.content || '{}')));
1089
+ return { data: { message_id: 'permission-msg-1' } };
1090
+ },
1091
+ patch: async () => { },
1092
+ },
1093
+ },
1094
+ },
1095
+ });
1096
+ internals.threadRouting.set('session:thread-1', {
1097
+ workspaceId: 'default',
1098
+ platformUserId: 'user-1',
1099
+ chatId: 'chat-1',
1100
+ threadId: 'thread-1',
1101
+ });
1102
+ await gateway.onBridgeEvent({
1103
+ type: 'buttons',
1104
+ sessionKey: 'session:thread-1',
1105
+ replyCtx: 'run-1',
1106
+ content: '等待工具确认\n\nTerminal\n\n请选择一个选项继续执行。',
1107
+ buttonRows: [[
1108
+ { text: 'allow', data: 'allow' },
1109
+ { text: 'deny', data: 'deny' },
1110
+ ]],
1111
+ });
1112
+ strict_1.default.equal(createdCards.length, 1);
1113
+ strict_1.default.match(createdCards[0]?.elements?.[0]?.content || '', /请直接回复/);
1114
+ strict_1.default.equal(createdCards[0]?.elements?.some((element) => element.tag === 'action'), false);
1115
+ });
1116
+ (0, node_test_1.default)('lark image messages are downloaded and forwarded as generic channel image parts', async () => {
1117
+ const sentMessages = [];
1118
+ const pngBytes = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
1119
+ const client = {
1120
+ im: {
1121
+ messageResource: {
1122
+ get: async (request) => {
1123
+ strict_1.default.equal(request.path.message_id, 'msg-image-1');
1124
+ strict_1.default.equal(request.path.file_key, 'img-key-1');
1125
+ strict_1.default.equal(request.params.type, 'image');
1126
+ return {
1127
+ headers: { 'content-type': 'image/png' },
1128
+ getReadableStream: () => node_stream_1.Readable.from([pngBytes]),
1129
+ };
1130
+ },
1131
+ },
1132
+ messageReaction: {
1133
+ create: async () => ({ data: { reaction_id: 'reaction-1' } }),
1134
+ },
1135
+ },
1136
+ };
1137
+ const gateway = new local_core_lark_gateway_js_1.LocalCoreLarkGateway({
1138
+ store: {
1139
+ expirePendingPairings: () => { },
1140
+ getAuthorizedUser: () => ({
1141
+ id: 'auth-1',
1142
+ workspace_id: 'default',
1143
+ platform: 'lark',
1144
+ platform_user_id: 'user-1',
1145
+ chat_id: 'chat-1',
1146
+ display_name: 'User',
1147
+ thread_id: 'thread-1',
1148
+ }),
1149
+ getPlatformThreadBinding: () => ({
1150
+ workspace_id: 'default',
1151
+ platform: 'lark',
1152
+ chat_id: 'chat-1',
1153
+ platform_user_id: 'user-1',
1154
+ thread_id: 'thread-1',
1155
+ last_platform_message_id: null,
1156
+ }),
1157
+ getLatestRunForThread: () => null,
1158
+ clearPlatformThreadMessageId: () => { },
1159
+ },
1160
+ readConfig: async () => ({
1161
+ projects: [{
1162
+ name: 'default',
1163
+ root: '/tmp/project',
1164
+ platforms: [{
1165
+ type: 'lark',
1166
+ options: {
1167
+ app_id: 'app-1',
1168
+ app_secret: 'secret-1',
1169
+ },
1170
+ }],
1171
+ }],
1172
+ }),
1173
+ getWorkspaceRouter: () => ({
1174
+ getThreadSessionKey: (threadId) => `session:${threadId}`,
1175
+ sendThreadMessage: async (threadId, content) => {
1176
+ sentMessages.push({ threadId, content });
1177
+ return { runId: 'run-1' };
1178
+ },
1179
+ }),
1180
+ eventBus: { emit: () => { }, on: () => () => { } },
1181
+ });
1182
+ const internals = gateway;
1183
+ internals.runtime.set('default', {
1184
+ workspaceId: 'default',
1185
+ enabled: true,
1186
+ status: 'running',
1187
+ connected: true,
1188
+ appId: 'app-1',
1189
+ client,
1190
+ });
1191
+ await internals.handleMessageEvent('default', {
1192
+ event: {
1193
+ sender: {
1194
+ sender_id: { user_id: 'user-1' },
1195
+ },
1196
+ message: {
1197
+ message_id: 'msg-image-1',
1198
+ message_type: 'image',
1199
+ chat_id: 'chat-1',
1200
+ content: JSON.stringify({ image_key: 'img-key-1' }),
1201
+ },
1202
+ },
1203
+ });
1204
+ strict_1.default.equal(sentMessages.length, 1);
1205
+ strict_1.default.equal(sentMessages[0]?.threadId, 'thread-1');
1206
+ strict_1.default.match(sentMessages[0]?.content?.displayText, /\[User Message\]\n\[Image\]\n\[\/User Message\]/);
1207
+ strict_1.default.deepEqual(sentMessages[0]?.content?.contentParts?.map((part) => part.type), ['text', 'image']);
1208
+ strict_1.default.equal(sentMessages[0]?.content?.contentParts?.[1]?.mimeType, 'image/png');
1209
+ strict_1.default.equal(sentMessages[0]?.content?.contentParts?.[1]?.data, pngBytes.toString('base64'));
1210
+ });
1211
+ (0, node_test_1.default)('lark file messages are downloaded and forwarded as generic channel file parts', async () => {
1212
+ const sentMessages = [];
1213
+ const fileBytes = Buffer.from('file content');
1214
+ const client = {
1215
+ im: {
1216
+ messageResource: {
1217
+ get: async (request) => {
1218
+ strict_1.default.equal(request.path.message_id, 'msg-file-in-1');
1219
+ strict_1.default.equal(request.path.file_key, 'file-key-in-1');
1220
+ strict_1.default.equal(request.params.type, 'file');
1221
+ return {
1222
+ headers: { 'content-type': 'application/pdf' },
1223
+ getReadableStream: () => node_stream_1.Readable.from([fileBytes]),
1224
+ };
1225
+ },
1226
+ },
1227
+ messageReaction: {
1228
+ create: async () => ({ data: { reaction_id: 'reaction-1' } }),
1229
+ },
1230
+ },
1231
+ };
1232
+ const gateway = new local_core_lark_gateway_js_1.LocalCoreLarkGateway({
1233
+ store: {
1234
+ expirePendingPairings: () => { },
1235
+ getAuthorizedUser: () => ({
1236
+ id: 'auth-1',
1237
+ workspace_id: 'default',
1238
+ platform: 'lark',
1239
+ platform_user_id: 'user-1',
1240
+ chat_id: 'chat-1',
1241
+ display_name: 'User',
1242
+ thread_id: 'thread-1',
1243
+ }),
1244
+ getPlatformThreadBinding: () => ({
1245
+ workspace_id: 'default',
1246
+ platform: 'lark',
1247
+ chat_id: 'chat-1',
1248
+ platform_user_id: 'user-1',
1249
+ thread_id: 'thread-1',
1250
+ last_platform_message_id: null,
1251
+ }),
1252
+ getLatestRunForThread: () => null,
1253
+ clearPlatformThreadMessageId: () => { },
1254
+ },
1255
+ readConfig: async () => ({
1256
+ projects: [{
1257
+ name: 'default',
1258
+ root: '/tmp/project',
1259
+ platforms: [{
1260
+ type: 'lark',
1261
+ options: {
1262
+ app_id: 'app-1',
1263
+ app_secret: 'secret-1',
1264
+ },
1265
+ }],
1266
+ }],
1267
+ }),
1268
+ getWorkspaceRouter: () => ({
1269
+ getThreadSessionKey: (threadId) => `session:${threadId}`,
1270
+ sendThreadMessage: async (threadId, content) => {
1271
+ sentMessages.push({ threadId, content });
1272
+ return { runId: 'run-1' };
1273
+ },
1274
+ }),
1275
+ eventBus: { emit: () => { }, on: () => () => { } },
1276
+ });
1277
+ const internals = gateway;
1278
+ internals.runtime.set('default', {
1279
+ workspaceId: 'default',
1280
+ enabled: true,
1281
+ status: 'running',
1282
+ connected: true,
1283
+ appId: 'app-1',
1284
+ client,
1285
+ });
1286
+ await internals.handleMessageEvent('default', {
1287
+ event: {
1288
+ sender: {
1289
+ sender_id: { user_id: 'user-1' },
1290
+ },
1291
+ message: {
1292
+ message_id: 'msg-file-in-1',
1293
+ message_type: 'file',
1294
+ chat_id: 'chat-1',
1295
+ content: JSON.stringify({ file_key: 'file-key-in-1', file_name: 'report.pdf' }),
1296
+ },
1297
+ },
1298
+ });
1299
+ strict_1.default.equal(sentMessages.length, 1);
1300
+ strict_1.default.equal(sentMessages[0]?.threadId, 'thread-1');
1301
+ strict_1.default.match(sentMessages[0]?.content?.displayText, /\[User Message\]\n\[File: report\.pdf\]\n\[\/User Message\]/);
1302
+ strict_1.default.deepEqual(sentMessages[0]?.content?.contentParts?.map((part) => part.type), ['text', 'file']);
1303
+ strict_1.default.equal(sentMessages[0]?.content?.contentParts?.[1]?.mimeType, 'application/pdf');
1304
+ strict_1.default.equal(sentMessages[0]?.content?.contentParts?.[1]?.data, fileBytes.toString('base64'));
1305
+ strict_1.default.equal(sentMessages[0]?.content?.contentParts?.[1]?.fileName, 'report.pdf');
1306
+ });
1307
+ (0, node_test_1.default)('lark group text messages strip the bot mention before dispatching', async () => {
1308
+ const sentMessages = [];
1309
+ const gateway = new local_core_lark_gateway_js_1.LocalCoreLarkGateway({
1310
+ store: {
1311
+ expirePendingPairings: () => { },
1312
+ getAuthorizedUser: () => ({
1313
+ id: 'auth-1',
1314
+ workspace_id: 'default',
1315
+ platform: 'lark',
1316
+ platform_user_id: 'user-1',
1317
+ chat_id: 'oc_group_1',
1318
+ display_name: 'User',
1319
+ thread_id: 'thread-1',
1320
+ }),
1321
+ getPlatformThreadBinding: () => ({
1322
+ workspace_id: 'default',
1323
+ platform: 'lark',
1324
+ chat_id: 'oc_group_1',
1325
+ platform_user_id: 'user-1',
1326
+ thread_id: 'thread-1',
1327
+ last_platform_message_id: null,
1328
+ }),
1329
+ getLatestRunForThread: () => null,
1330
+ clearPlatformThreadMessageId: () => { },
1331
+ },
1332
+ readConfig: async () => ({ projects: [] }),
1333
+ getWorkspaceRouter: () => ({
1334
+ getThreadSessionKey: (threadId) => `session:${threadId}`,
1335
+ sendThreadMessage: async (threadId, content) => {
1336
+ sentMessages.push({ threadId, content });
1337
+ return { runId: 'run-1' };
1338
+ },
1339
+ }),
1340
+ eventBus: { emit: () => { }, on: () => () => { } },
1341
+ });
1342
+ const internals = gateway;
1343
+ internals.runtime.set('default', {
1344
+ workspaceId: 'default',
1345
+ enabled: true,
1346
+ status: 'running',
1347
+ connected: true,
1348
+ appId: 'app-1',
1349
+ botOpenId: 'ou_bot',
1350
+ groupReplyAll: false,
1351
+ client: {
1352
+ im: {
1353
+ messageReaction: {
1354
+ create: async () => ({ data: { reaction_id: 'reaction-1' } }),
1355
+ },
1356
+ },
1357
+ },
1358
+ });
1359
+ await internals.handleMessageEvent('default', {
1360
+ event: {
1361
+ sender: {
1362
+ sender_id: { user_id: 'user-1' },
1363
+ },
1364
+ message: {
1365
+ message_id: 'msg-group-1',
1366
+ message_type: 'text',
1367
+ chat_id: 'oc_group_1',
1368
+ chat_type: 'group',
1369
+ mentions: [
1370
+ { key: '@_user_1', id: { open_id: 'ou_bot' }, name: 'AgentDock' },
1371
+ { key: '@_user_2', id: { open_id: 'ou_other' }, name: '张三' },
1372
+ ],
1373
+ content: JSON.stringify({ text: '@_user_1 ask @_user_2 to review' }),
1374
+ },
1375
+ },
1376
+ });
1377
+ strict_1.default.equal(sentMessages.length, 1);
1378
+ strict_1.default.equal(sentMessages[0]?.threadId, 'thread-1');
1379
+ strict_1.default.match(sentMessages[0]?.content, /\[User Message\]\nask @张三 to review\n\[\/User Message\]/);
1380
+ });
1381
+ (0, node_test_1.default)('lark group text messages ignore non-mentioned bot messages by default', async () => {
1382
+ const sentMessages = [];
1383
+ const gateway = new local_core_lark_gateway_js_1.LocalCoreLarkGateway({
1384
+ store: {
1385
+ expirePendingPairings: () => { },
1386
+ getAuthorizedUser: () => {
1387
+ throw new Error('group message without bot mention should not look up authorization');
1388
+ },
1389
+ },
1390
+ readConfig: async () => ({ projects: [] }),
1391
+ getWorkspaceRouter: () => ({
1392
+ sendThreadMessage: async (threadId, content) => {
1393
+ sentMessages.push({ threadId, content });
1394
+ return { runId: 'run-1' };
1395
+ },
1396
+ }),
1397
+ eventBus: { emit: () => { }, on: () => () => { } },
1398
+ });
1399
+ const internals = gateway;
1400
+ internals.runtime.set('default', {
1401
+ workspaceId: 'default',
1402
+ enabled: true,
1403
+ status: 'running',
1404
+ connected: true,
1405
+ appId: 'app-1',
1406
+ botOpenId: 'ou_bot',
1407
+ groupReplyAll: false,
1408
+ client: {},
1409
+ });
1410
+ await internals.handleMessageEvent('default', {
1411
+ event: {
1412
+ sender: {
1413
+ sender_id: { user_id: 'user-1' },
1414
+ },
1415
+ message: {
1416
+ message_id: 'msg-group-2',
1417
+ message_type: 'text',
1418
+ chat_id: 'oc_group_1',
1419
+ chat_type: 'group',
1420
+ mentions: [{ key: '@_user_2', id: { open_id: 'ou_other' }, name: '张三' }],
1421
+ content: JSON.stringify({ text: 'ask @_user_2 to review' }),
1422
+ },
1423
+ },
1424
+ });
1425
+ strict_1.default.equal(sentMessages.length, 0);
1426
+ });
1427
+ (0, node_test_1.default)('lark inbound messages create a chat binding when an authorized user has an old direct thread', async () => {
1428
+ const bindings = [];
1429
+ const createdThreads = [];
1430
+ const sentMessages = [];
1431
+ const gateway = new local_core_lark_gateway_js_1.LocalCoreLarkGateway({
1432
+ store: {
1433
+ expirePendingPairings: () => { },
1434
+ getAuthorizedUser: () => ({
1435
+ id: 'auth-1',
1436
+ workspace_id: 'project-1',
1437
+ platform: 'lark:lark-1',
1438
+ platform_user_id: 'user-1',
1439
+ chat_id: 'old-direct-chat',
1440
+ display_name: 'User',
1441
+ thread_id: 'old-thread-1',
1442
+ }),
1443
+ getPlatformThreadBinding: () => undefined,
1444
+ upsertPlatformThreadBinding: (binding) => bindings.push(binding),
1445
+ updateAuthorizedUserThread: () => { },
1446
+ getLatestRunForThread: () => null,
1447
+ clearPlatformThreadMessageId: () => { },
1448
+ },
1449
+ readConfig: async () => ({ projects: [] }),
1450
+ getWorkspaceRouter: () => ({
1451
+ createThread: async (workspaceId, title) => {
1452
+ const thread = { id: 'group-thread-1', workspaceId, title };
1453
+ createdThreads.push(thread);
1454
+ return thread;
1455
+ },
1456
+ getThreadSessionKey: (threadId) => `session:${threadId}`,
1457
+ sendThreadMessage: async (threadId, content) => {
1458
+ sentMessages.push({ threadId, content });
1459
+ return { runId: 'run-1' };
1460
+ },
1461
+ }),
1462
+ eventBus: { emit: () => { }, on: () => () => { } },
1463
+ });
1464
+ const internals = gateway;
1465
+ internals.runtime.set('project-1::lark-1', {
1466
+ workspaceId: 'project-1',
1467
+ instanceId: 'lark-1',
1468
+ platformKey: 'lark:lark-1',
1469
+ enabled: true,
1470
+ status: 'running',
1471
+ connected: true,
1472
+ appId: 'app-1',
1473
+ botOpenId: 'ou_bot',
1474
+ groupReplyAll: false,
1475
+ client: {
1476
+ im: {
1477
+ messageReaction: {
1478
+ create: async () => ({ data: { reaction_id: 'reaction-1' } }),
1479
+ },
1480
+ },
1481
+ },
1482
+ });
1483
+ await internals.handleMessageEvent('project-1', 'lark-1', 'lark:lark-1', {
1484
+ event: {
1485
+ sender: {
1486
+ sender_id: { user_id: 'user-1' },
1487
+ },
1488
+ message: {
1489
+ message_id: 'msg-group-old-user',
1490
+ message_type: 'text',
1491
+ chat_id: 'new-group-chat',
1492
+ chat_type: 'group',
1493
+ mentions: [{ key: '@_user_1', id: { open_id: 'ou_bot' }, name: 'AgentDock' }],
1494
+ content: JSON.stringify({ text: '@_user_1 hi' }),
1495
+ },
1496
+ },
1497
+ });
1498
+ strict_1.default.deepEqual(createdThreads, [{ id: 'group-thread-1', workspaceId: 'project-1', title: 'user-1' }]);
1499
+ strict_1.default.equal(bindings.length, 1);
1500
+ strict_1.default.equal(bindings[0]?.platform, 'lark:lark-1');
1501
+ strict_1.default.equal(bindings[0]?.chat_id, 'new-group-chat');
1502
+ strict_1.default.equal(bindings[0]?.platform_user_id, 'user-1');
1503
+ strict_1.default.equal(bindings[0]?.thread_id, 'group-thread-1');
1504
+ strict_1.default.equal(sentMessages[0]?.threadId, 'group-thread-1');
1505
+ });
1506
+ (0, node_test_1.default)('lark channel can upload and send a local file', async () => {
1507
+ const tempDir = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'lark-file-send-'));
1508
+ try {
1509
+ const filePath = (0, node_path_1.join)(tempDir, 'report.pdf');
1510
+ (0, node_fs_1.writeFileSync)(filePath, 'pdf content');
1511
+ const uploads = [];
1512
+ const messages = [];
1513
+ const client = {
1514
+ im: {
1515
+ file: {
1516
+ create: async (request) => {
1517
+ uploads.push(request);
1518
+ await new Promise((resolve, reject) => {
1519
+ request.data.file.on('data', () => { });
1520
+ request.data.file.on('error', reject);
1521
+ request.data.file.on('end', resolve);
1522
+ });
1523
+ return { file_key: 'file-key-1' };
1524
+ },
1525
+ },
1526
+ message: {
1527
+ create: async (request) => {
1528
+ messages.push(request);
1529
+ return { data: { message_id: 'msg-file-1' } };
1530
+ },
1531
+ },
1532
+ },
1533
+ };
1534
+ const gateway = new local_core_lark_gateway_js_1.LocalCoreLarkGateway({
1535
+ store: {},
1536
+ readConfig: async () => null,
1537
+ getWorkspaceRouter: () => ({}),
1538
+ eventBus: { emit: () => { }, on: () => () => { } },
1539
+ });
1540
+ const internals = gateway;
1541
+ internals.runtime.set('default', {
1542
+ workspaceId: 'default',
1543
+ enabled: true,
1544
+ status: 'running',
1545
+ connected: true,
1546
+ appId: 'app-1',
1547
+ client,
1548
+ });
1549
+ const result = await gateway.sendFile('default', {
1550
+ path: filePath,
1551
+ channelId: 'oc_chat_1',
1552
+ });
1553
+ strict_1.default.equal(uploads.length, 1);
1554
+ strict_1.default.equal(uploads[0]?.data?.file_type, 'pdf');
1555
+ strict_1.default.equal(uploads[0]?.data?.file_name, 'report.pdf');
1556
+ strict_1.default.equal(messages.length, 1);
1557
+ strict_1.default.equal(messages[0]?.params?.receive_id_type, 'chat_id');
1558
+ strict_1.default.equal(messages[0]?.data?.receive_id, 'oc_chat_1');
1559
+ strict_1.default.equal(messages[0]?.data?.msg_type, 'file');
1560
+ strict_1.default.deepEqual(JSON.parse(messages[0]?.data?.content), { file_key: 'file-key-1' });
1561
+ strict_1.default.deepEqual(result, {
1562
+ platform: 'lark',
1563
+ workspaceId: 'default',
1564
+ channelId: 'oc_chat_1',
1565
+ messageId: 'msg-file-1',
1566
+ fileKey: 'file-key-1',
1567
+ fileName: 'report.pdf',
1568
+ fileSize: Buffer.byteLength('pdf content'),
1569
+ });
1570
+ }
1571
+ finally {
1572
+ (0, node_fs_1.rmSync)(tempDir, { recursive: true, force: true });
1573
+ }
1574
+ });
1575
+ (0, node_test_1.default)('lark message callbacks acknowledge before long thread runs finish', async () => {
1576
+ let registeredHandlers = {};
1577
+ let sentMessages = 0;
1578
+ const logs = [];
1579
+ const gateway = new local_core_lark_gateway_js_1.LocalCoreLarkGateway({
1580
+ store: {
1581
+ expirePendingPairings: () => { },
1582
+ getAuthorizedUser: () => ({
1583
+ id: 'auth-1',
1584
+ workspace_id: 'default',
1585
+ platform_user_id: 'user-1',
1586
+ chat_id: 'chat-1',
1587
+ display_name: 'User',
1588
+ thread_id: 'thread-1',
1589
+ }),
1590
+ getPlatformThreadBinding: () => ({
1591
+ workspace_id: 'default',
1592
+ platform: 'lark',
1593
+ chat_id: 'chat-1',
1594
+ platform_user_id: 'user-1',
1595
+ thread_id: 'thread-1',
1596
+ last_platform_message_id: null,
1597
+ }),
1598
+ getLatestRunForThread: () => null,
1599
+ clearPlatformThreadMessageId: () => { },
1600
+ listPendingPairings: () => [],
1601
+ listAuthorizedUsers: () => [],
1602
+ },
1603
+ readConfig: async () => ({
1604
+ projects: [
1605
+ {
1606
+ name: 'default',
1607
+ agent: { type: 'claudecode', providers: [] },
1608
+ platforms: [{ type: 'lark', options: { app_id: 'app-1', app_secret: 'secret-1', auto_approve: true } }],
1609
+ },
1610
+ ],
1611
+ }),
1612
+ getWorkspaceRouter: () => ({
1613
+ getThreadSessionKey: (threadId) => `session:${threadId}`,
1614
+ sendThreadMessage: async () => {
1615
+ sentMessages++;
1616
+ return new Promise(() => { });
1617
+ },
1618
+ }),
1619
+ eventBus: { emit: () => { }, on: () => () => { } },
1620
+ log: (message) => logs.push(message),
1621
+ });
1622
+ gateway.larkModulePromise = Promise.resolve({
1623
+ AppType: { SelfBuild: 'self-build' },
1624
+ Domain: { Feishu: 'feishu' },
1625
+ LoggerLevel: { info: 'info' },
1626
+ Client: class {
1627
+ },
1628
+ EventDispatcher: class {
1629
+ register(handlers) {
1630
+ registeredHandlers = handlers;
1631
+ }
1632
+ },
1633
+ WSClient: class {
1634
+ async start() { }
1635
+ },
1636
+ });
1637
+ await gateway.enable('default');
1638
+ const handler = registeredHandlers['im.message.receive_v1'];
1639
+ strict_1.default.equal(typeof handler, 'function');
1640
+ const result = await Promise.race([
1641
+ handler({
1642
+ event: {
1643
+ sender: { sender_id: { user_id: 'user-1' } },
1644
+ message: {
1645
+ message_id: 'msg-1',
1646
+ chat_id: 'chat-1',
1647
+ message_type: 'text',
1648
+ content: JSON.stringify({ text: 'hi' }),
1649
+ },
1650
+ },
1651
+ }),
1652
+ new Promise((resolve) => setTimeout(() => resolve('timed-out'), 25)),
1653
+ ]);
1654
+ strict_1.default.notEqual(result, 'timed-out');
1655
+ await new Promise((resolve) => setTimeout(resolve, 0));
1656
+ strict_1.default.equal(sentMessages, 1);
1657
+ strict_1.default.ok(logs.some((line) => line.includes('received im.message.receive_v1') &&
1658
+ line.includes('message=msg-1') &&
1659
+ line.includes('type=text') &&
1660
+ line.includes('chat=chat-1') &&
1661
+ line.includes('sender=user-1')));
1662
+ strict_1.default.ok(logs.some((line) => line.includes('handling message event') && line.includes('contentBytes=')));
1663
+ });
1664
+ (0, node_test_1.default)('lark inbound messages use active runtime binding before config refresh catches up', async () => {
1665
+ const users = new Map();
1666
+ const threadBindings = new Map();
1667
+ const createdThreads = [];
1668
+ const updatedModes = [];
1669
+ const sentCards = [];
1670
+ const platformKey = 'lark:lark-hot';
1671
+ const bindingKey = `${platformKey}:chat-1:user-1`;
1672
+ const gateway = new local_core_lark_gateway_js_1.LocalCoreLarkGateway({
1673
+ store: {
1674
+ expirePendingPairings: () => { },
1675
+ listPendingPairings: () => [],
1676
+ listAuthorizedUsers: () => [...users.values()],
1677
+ getAuthorizedUser: (_workspaceId, platformUserId, requestedPlatform) => users.get(`${requestedPlatform}:${platformUserId}`),
1678
+ createAuthorizedUser: (user) => users.set(`${user.platform}:${user.platform_user_id}`, user),
1679
+ updateAuthorizedUserThread: (_workspaceId, platformUserId, threadId, requestedPlatform) => {
1680
+ users.set(`${requestedPlatform}:${platformUserId}`, { ...users.get(`${requestedPlatform}:${platformUserId}`), thread_id: threadId });
1681
+ },
1682
+ getPlatformThreadBinding: () => threadBindings.get(bindingKey),
1683
+ upsertPlatformThreadBinding: (binding) => threadBindings.set(bindingKey, binding),
1684
+ getThreadRow: (threadId) => ({ id: threadId, agent_mode: threadId === 'thread-1' ? 'bypassPermissions' : 'default' }),
1685
+ updateThreadAgentMode: (threadId, mode) => updatedModes.push({ threadId, mode }),
1686
+ getLatestRunForThread: () => null,
1687
+ clearPlatformThreadMessageId: () => { },
1688
+ },
1689
+ readConfig: async () => ({ projects: [] }),
1690
+ getWorkspaceRouter: () => ({
1691
+ createThread: async (_workspaceId, title) => {
1692
+ const id = `thread-${createdThreads.length + 1}`;
1693
+ createdThreads.push(`${id}:${title}`);
1694
+ return { id };
1695
+ },
1696
+ getThreadSessionKey: (threadId) => `session:${threadId}`,
1697
+ sendThreadMessage: async () => ({ runId: 'run-1' }),
1698
+ }),
1699
+ eventBus: { emit: () => { }, on: () => () => { } },
1700
+ });
1701
+ const internals = gateway;
1702
+ internals.runtime.set('project-1::lark-hot', {
1703
+ workspaceId: 'project-1',
1704
+ instanceId: 'lark-hot',
1705
+ displayName: 'Lark Hot',
1706
+ platformKey,
1707
+ enabled: true,
1708
+ status: 'running',
1709
+ connected: true,
1710
+ appId: 'app-1',
1711
+ autoApprove: true,
1712
+ cardActionsEnabled: true,
1713
+ client: {
1714
+ im: {
1715
+ message: {
1716
+ create: async (request) => {
1717
+ sentCards.push(JSON.parse(String(request.data.content || '{}')));
1718
+ return { data: { message_id: 'card-1' } };
1719
+ },
1720
+ },
1721
+ messageReaction: {
1722
+ create: async () => ({ data: { reaction_id: 'reaction-1' } }),
1723
+ },
1724
+ },
1725
+ },
1726
+ });
1727
+ await gateway.handleInboundMessage({
1728
+ workspaceId: 'project-1',
1729
+ instanceId: 'lark-hot',
1730
+ platformKey,
1731
+ platformUserId: 'user-1',
1732
+ chatId: 'chat-1',
1733
+ displayName: 'User',
1734
+ text: '/new',
1735
+ messageId: 'msg-1',
1736
+ });
1737
+ strict_1.default.equal(users.get(`${platformKey}:user-1`)?.thread_id, 'thread-2');
1738
+ strict_1.default.deepEqual(createdThreads.map((item) => item.split(':')[0]), ['thread-1', 'thread-2']);
1739
+ strict_1.default.deepEqual(updatedModes, [{ threadId: 'thread-2', mode: 'bypassPermissions' }]);
1740
+ strict_1.default.match(sentCards[0]?.elements?.[0]?.content || '', /已开始新会话/);
1741
+ });
1742
+ (0, node_test_1.default)('lark rendering suppresses noisy pending tool progress cards', () => {
1743
+ const turn = (0, runtime_state_js_1.createLarkTurnState)('session-1');
1744
+ const rendered = (0, runtime_state_js_1.renderLarkBridgeEventMessage)(turn, {
1745
+ type: 'update_message',
1746
+ sessionKey: 'session-1',
1747
+ replyCtx: 'run-1',
1748
+ previewHandle: 'tool-1',
1749
+ bridgeKind: 'tool',
1750
+ content: 'bash pending',
1751
+ toolCall: {
1752
+ id: 'tool-1',
1753
+ name: 'bash',
1754
+ status: 'pending',
1755
+ output: '',
1756
+ },
1757
+ });
1758
+ strict_1.default.equal(rendered.text, '');
1759
+ strict_1.default.equal(rendered.key, 'noop');
1760
+ const completed = (0, runtime_state_js_1.renderLarkBridgeEventMessage)(turn, {
1761
+ type: 'update_message',
1762
+ sessionKey: 'session-1',
1763
+ replyCtx: 'run-1',
1764
+ previewHandle: 'tool-1',
1765
+ bridgeKind: 'tool',
1766
+ content: 'bash completed',
1767
+ toolCall: {
1768
+ id: 'tool-1',
1769
+ name: 'bash: Tool update',
1770
+ status: 'completed',
1771
+ output: 'verbose output',
1772
+ },
1773
+ });
1774
+ strict_1.default.equal(completed.text, '🔧 bash: Tool update');
1775
+ strict_1.default.equal(completed.delivery, 'message');
1776
+ });
1777
+ (0, node_test_1.default)('lark channel can request an official app registration QR code without extra setup', async () => {
1778
+ const originalFetch = globalThis.fetch;
1779
+ const requests = [];
1780
+ globalThis.fetch = (async (input, init) => {
1781
+ requests.push({
1782
+ url: String(input),
1783
+ method: init?.method,
1784
+ body: String(init?.body || ''),
1785
+ headers: new Headers(init?.headers),
1786
+ });
1787
+ return new Response(JSON.stringify({
1788
+ device_code: 'device-code-1',
1789
+ user_code: 'ABCD-EFGH',
1790
+ expires_in: 300,
1791
+ interval: 5,
1792
+ }), {
1793
+ status: 200,
1794
+ headers: { 'Content-Type': 'application/json' },
1795
+ });
1796
+ });
1797
+ try {
1798
+ const gateway = new local_core_lark_gateway_js_1.LocalCoreLarkGateway({
1799
+ store: {},
1800
+ readConfig: async () => ({
1801
+ projects: [
1802
+ {
1803
+ name: 'default',
1804
+ agent: { type: 'localcore-acp', providers: [] },
1805
+ platforms: [{ type: 'lark', options: {} }],
1806
+ },
1807
+ ],
1808
+ }),
1809
+ getWorkspaceRouter: () => ({}),
1810
+ eventBus: { emit: () => { }, on: () => () => { } },
1811
+ });
1812
+ const result = await gateway.getQrCode('default');
1813
+ strict_1.default.deepEqual(result, {
1814
+ ticket: 'device-code-1',
1815
+ expiresIn: 300,
1816
+ interval: 5,
1817
+ qrCodeUrl: 'https://open.feishu.cn/page/openclaw?user_code=ABCD-EFGH&from=openclaw',
1818
+ instanceId: 'default',
1819
+ displayName: 'Lark 1',
1820
+ });
1821
+ strict_1.default.equal(requests[0]?.url, 'https://accounts.feishu.cn/oauth/v1/app/registration');
1822
+ strict_1.default.equal(requests[0]?.method, 'POST');
1823
+ strict_1.default.equal(requests[0]?.headers.get('Content-Type'), 'application/x-www-form-urlencoded');
1824
+ strict_1.default.equal(new URLSearchParams(requests[0]?.body).get('action'), 'begin');
1825
+ strict_1.default.equal(new URLSearchParams(requests[0]?.body).get('archetype'), 'PersonalAgent');
1826
+ strict_1.default.equal(new URLSearchParams(requests[0]?.body).get('request_callbacks'), null);
1827
+ }
1828
+ finally {
1829
+ globalThis.fetch = originalFetch;
1830
+ }
1831
+ });
1832
+ (0, node_test_1.default)('lark app registration QR confirmation returns app credentials', async () => {
1833
+ const originalFetch = globalThis.fetch;
1834
+ const requests = [];
1835
+ globalThis.fetch = (async (input, init) => {
1836
+ const url = String(input);
1837
+ requests.push({ url, body: String(init?.body || '') });
1838
+ return new Response(JSON.stringify({
1839
+ client_id: 'cli_lark_1',
1840
+ client_secret: 'secret-1',
1841
+ }), {
1842
+ status: 200,
1843
+ headers: { 'Content-Type': 'application/json' },
1844
+ });
1845
+ });
1846
+ try {
1847
+ const gateway = new local_core_lark_gateway_js_1.LocalCoreLarkGateway({
1848
+ store: {},
1849
+ readConfig: async () => ({
1850
+ projects: [
1851
+ {
1852
+ name: 'default',
1853
+ agent: { type: 'localcore-acp', providers: [] },
1854
+ platforms: [{ type: 'lark', options: {} }],
1855
+ },
1856
+ ],
1857
+ }),
1858
+ getWorkspaceRouter: () => ({}),
1859
+ eventBus: { emit: () => { }, on: () => () => { } },
1860
+ });
1861
+ const result = await gateway.checkQrCodeStatus('default', 'lark-ticket-1');
1862
+ strict_1.default.equal(requests[0]?.url, 'https://accounts.feishu.cn/oauth/v1/app/registration');
1863
+ strict_1.default.equal(new URLSearchParams(requests[0]?.body).get('action'), 'poll');
1864
+ strict_1.default.equal(new URLSearchParams(requests[0]?.body).get('device_code'), 'lark-ticket-1');
1865
+ strict_1.default.deepEqual(result, {
1866
+ status: 'confirmed',
1867
+ credentials: {
1868
+ appId: 'cli_lark_1',
1869
+ appSecret: 'secret-1',
1870
+ },
1871
+ });
1872
+ }
1873
+ finally {
1874
+ globalThis.fetch = originalFetch;
1875
+ }
1876
+ });
1877
+ (0, node_test_1.default)('lark channel keeps multiple bot instances in one workspace isolated', async () => {
1878
+ const gateway = new local_core_lark_gateway_js_1.LocalCoreLarkGateway({
1879
+ store: {
1880
+ expirePendingPairings: () => { },
1881
+ listPendingPairings: () => [],
1882
+ listAuthorizedUsers: () => [],
1883
+ },
1884
+ readConfig: async () => ({
1885
+ projects: [
1886
+ {
1887
+ name: 'default',
1888
+ agent: { type: 'localcore-acp', providers: [] },
1889
+ platforms: [
1890
+ { type: 'lark', options: { instance_id: 'bot-a', app_id: 'cli_a', app_secret: 'secret-a' } },
1891
+ { type: 'lark', options: { instance_id: 'bot-b', app_id: 'cli_b', app_secret: 'secret-b' } },
1892
+ ],
1893
+ },
1894
+ ],
1895
+ }),
1896
+ getWorkspaceRouter: () => ({}),
1897
+ eventBus: { emit: () => { }, on: () => () => { } },
1898
+ });
1899
+ gateway.larkModulePromise = Promise.resolve({
1900
+ AppType: { SelfBuild: 'self-build' },
1901
+ Domain: { Feishu: 'feishu' },
1902
+ LoggerLevel: { info: 'info' },
1903
+ Client: class {
1904
+ },
1905
+ EventDispatcher: class {
1906
+ register() { }
1907
+ },
1908
+ WSClient: class {
1909
+ async start() { }
1910
+ },
1911
+ });
1912
+ await gateway.refreshBindings();
1913
+ const statuses = gateway.listStatuses();
1914
+ strict_1.default.deepEqual(statuses.map((status) => [status.workspaceId, status.instanceId, status.appId, status.status]), [
1915
+ ['default', 'bot-a', 'cli_a', 'running'],
1916
+ ['default', 'bot-b', 'cli_b', 'running'],
1917
+ ]);
1918
+ });