@kafca/agentdock 0.1.22 → 0.1.24

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 (89) hide show
  1. package/README.md +4 -0
  2. package/dist/renderer/assets/{Badge-B_rgzxYO.js → Badge-CXcUMqb-.js} +1 -1
  3. package/dist/renderer/assets/{Chat-CYtp6m7T.js → Chat-BRxdPzi-.js} +1 -1
  4. package/dist/renderer/assets/{ChatMarkdown-DOBMwa9d.js → ChatMarkdown-sWGBIfIb.js} +1 -1
  5. package/dist/renderer/assets/{Config-DppMSPHF.js → Config-DqxuxIGg.js} +1 -1
  6. package/dist/renderer/assets/{CronList-BDtB1SuA.js → CronList-OgHxbti5.js} +1 -1
  7. package/dist/renderer/assets/{Dashboard-iFlRnd7Y.js → Dashboard-DmklXiH1.js} +2 -2
  8. package/dist/renderer/assets/{EmptyState-bKDt_T88.js → EmptyState-BvDmi8dq.js} +1 -1
  9. package/dist/renderer/assets/{KnowledgeDetail-CKPhKPIg.js → KnowledgeDetail-Bj59Hdpo.js} +2 -2
  10. package/dist/renderer/assets/{KnowledgeHome-CFeJre6i.js → KnowledgeHome-CigQ-nSo.js} +3 -3
  11. package/dist/renderer/assets/{Logs-CyxSVOFK.js → Logs-Bfc7gBpY.js} +1 -1
  12. package/dist/renderer/assets/{Modal-CxPnk1Ey.js → Modal-CfmiFknz.js} +1 -1
  13. package/dist/renderer/assets/{Page-kU9vubzj.js → Page-BxtLEw_b.js} +1 -1
  14. package/dist/renderer/assets/{ProjectDetail-D9FY9NWZ.js → ProjectDetail-CcUphxGm.js} +1 -1
  15. package/dist/renderer/assets/{ProjectList-DIwr2TXg.js → ProjectList-CVkvDIfy.js} +1 -1
  16. package/dist/renderer/assets/{Select-BaWoz5Dn.js → Select-DgQMZ0MD.js} +1 -1
  17. package/dist/renderer/assets/{SessionChat-Cm9CW8Zx.js → SessionChat-CRmGKfBx.js} +1 -1
  18. package/dist/renderer/assets/{SessionList-UPnWlXXO.js → SessionList-D0E8XbYw.js} +1 -1
  19. package/dist/renderer/assets/ThreadChat-DUgqZSBB.js +23 -0
  20. package/dist/renderer/assets/Workspace-Q4g4rlRT.js +14 -0
  21. package/dist/renderer/assets/{arrow-left-BmZAVGV7.js → arrow-left-BySN_zhF.js} +1 -1
  22. package/dist/renderer/assets/{book-open-DZ3GXFgI.js → book-open-BGlrnrTc.js} +1 -1
  23. package/dist/renderer/assets/{check-CeVSvDCv.js → check-BiJogEa5.js} +1 -1
  24. package/dist/renderer/assets/{chevron-down-yryuMoQ-.js → chevron-down-D2fHRJv6.js} +1 -1
  25. package/dist/renderer/assets/{circle-eASsFISH.js → circle-CIcjixmI.js} +1 -1
  26. package/dist/renderer/assets/desktop-DAXY9Bxv.js +4 -0
  27. package/dist/renderer/assets/desktop-DyA_NYew.js +1 -0
  28. package/dist/renderer/assets/{funnel-pwAe2Ixp.js → funnel-BUzwb-id.js} +1 -1
  29. package/dist/renderer/assets/{heart-B2Sp-bCm.js → heart-DZEQ1z_V.js} +1 -1
  30. package/dist/renderer/assets/{index-BC3MWbS1.js → index-CB_KyLOr.js} +39 -39
  31. package/dist/renderer/assets/{index-Cse2Br64.js → index-DgsIo4vJ.js} +3 -3
  32. package/dist/renderer/assets/{pencil-BfEunUF3.js → pencil-DGL4agcl.js} +1 -1
  33. package/dist/renderer/assets/{play-Dcs9Lj_Z.js → play-DzasCLM4.js} +1 -1
  34. package/dist/renderer/assets/{plug-B_YschQe.js → plug-dubNMe_m.js} +1 -1
  35. package/dist/renderer/assets/{plus-Pqo-cXHQ.js → plus-DN4kML4k.js} +1 -1
  36. package/dist/renderer/assets/projects-migPtfRe.js +1 -0
  37. package/dist/renderer/assets/{save-BKM5yi1A.js → save-CNIc9tjl.js} +1 -1
  38. package/dist/renderer/assets/{search-BU42h10M.js → search-C0EGbF3I.js} +1 -1
  39. package/dist/renderer/assets/{server-DA5t3Rtr.js → server-BMxQ4zmc.js} +1 -1
  40. package/dist/renderer/assets/{session-utils-Cr9VhNSq.js → session-utils-Bf5O-iMi.js} +1 -1
  41. package/dist/renderer/assets/{sessions-bvV8ZMfX.js → sessions-BMSs6rEI.js} +1 -1
  42. package/dist/renderer/assets/{shield-check-BeOJKur_.js → shield-check-i3Tf1TUz.js} +1 -1
  43. package/dist/renderer/assets/{trash-2-CQ7HKO3q.js → trash-2-SKrcp18f.js} +1 -1
  44. package/dist/renderer/assets/{user-CtFjY9SV.js → user-cNUZddjZ.js} +1 -1
  45. package/dist/renderer/assets/{wifi-off-Ci5m0ysf.js → wifi-off-Dyb84ANR.js} +1 -1
  46. package/dist/renderer/index.html +1 -1
  47. package/dist-electron/electron/channel-content.test.js +29 -0
  48. package/dist-electron/electron/lac-cli.test.js +49 -4
  49. package/dist-electron/electron/local-core-acp-content.test.js +39 -0
  50. package/dist-electron/electron/local-core-contracts.test.js +67 -0
  51. package/dist-electron/electron/local-core-refactor.test.js +628 -0
  52. package/dist-electron/electron/plugin-kernel.test.js +74 -14
  53. package/dist-electron/electron/workspace-task-store.test.js +124 -0
  54. package/dist-electron/packages/contracts/src/local-core.js +110 -0
  55. package/dist-electron/packages/core-sdk/src/index.js +40 -20
  56. package/dist-electron/services/local-ai-core/src/acp/local-core-acp-content.js +25 -4
  57. package/dist-electron/services/local-ai-core/src/acp/local-core-acp-response-processor.js +3 -2
  58. package/dist-electron/services/local-ai-core/src/acp/local-core-acp-store.js +44 -28
  59. package/dist-electron/services/local-ai-core/src/acp/local-core-acp-turn-coordinator.js +5 -1
  60. package/dist-electron/services/local-ai-core/src/acp/workspace-acp-permissions.js +15 -6
  61. package/dist-electron/services/local-ai-core/src/channel/lark/cards.js +210 -0
  62. package/dist-electron/services/local-ai-core/src/channel/lark/config.js +54 -0
  63. package/dist-electron/services/local-ai-core/src/channel/lark/local-core-lark-gateway.js +280 -496
  64. package/dist-electron/services/local-ai-core/src/channel/lark/registration.js +50 -0
  65. package/dist-electron/services/local-ai-core/src/channel/lark/runtime-state.js +191 -0
  66. package/dist-electron/services/local-ai-core/src/channel/lark/types.js +2 -0
  67. package/dist-electron/services/local-ai-core/src/channel/shared/content.js +23 -0
  68. package/dist-electron/services/local-ai-core/src/channel/shared/plugin.js +31 -0
  69. package/dist-electron/services/local-ai-core/src/channel/weixin/config.js +125 -0
  70. package/dist-electron/services/local-ai-core/src/channel/weixin/local-core-weixin-gateway.js +164 -352
  71. package/dist-electron/services/local-ai-core/src/channel/weixin/transport.js +169 -0
  72. package/dist-electron/services/local-ai-core/src/channel/weixin/types.js +2 -0
  73. package/dist-electron/services/local-ai-core/src/cli/lac.js +19 -16
  74. package/dist-electron/services/local-ai-core/src/kernel/bootstrap.js +5 -0
  75. package/dist-electron/services/local-ai-core/src/plugins/builtin/channel-lark-plugin.js +18 -39
  76. package/dist-electron/services/local-ai-core/src/plugins/builtin/channel-weixin-plugin.js +18 -39
  77. package/dist-electron/services/local-ai-core/src/runtime/local-core-controller.js +55 -44
  78. package/dist-electron/services/local-ai-core/src/runtime/server.js +10 -8
  79. package/dist-electron/services/local-ai-core/src/scheduler/job-id.js +15 -0
  80. package/dist-electron/services/local-ai-core/src/scheduler/scheduler-service.js +30 -5
  81. package/dist-electron/shared/desktop.js +37 -5
  82. package/dist-electron/src/pages/Threads/thread-chat-model.js +57 -0
  83. package/dist-electron/src/pages/Threads/thread-chat-permission.test.js +70 -0
  84. package/package.json +1 -1
  85. package/dist/renderer/assets/ThreadChat-YvVsACW3.js +0 -23
  86. package/dist/renderer/assets/Workspace-ST7V8u6T.js +0 -14
  87. package/dist/renderer/assets/desktop-Che1cbpE.js +0 -4
  88. package/dist/renderer/assets/desktop-DkrP_s-b.js +0 -1
  89. package/dist/renderer/assets/projects-BCj0k_Gy.js +0 -1
@@ -18,6 +18,7 @@ const local_core_weixin_gateway_js_1 = require("../services/local-ai-core/src/ch
18
18
  const local_core_lark_gateway_js_1 = require("../services/local-ai-core/src/channel/lark/local-core-lark-gateway.js");
19
19
  const local_core_acp_turn_coordinator_js_1 = require("../services/local-ai-core/src/acp/local-core-acp-turn-coordinator.js");
20
20
  const local_core_acp_store_js_1 = require("../services/local-ai-core/src/acp/local-core-acp-store.js");
21
+ const workspace_acp_permissions_js_1 = require("../services/local-ai-core/src/acp/workspace-acp-permissions.js");
21
22
  (0, node_test_1.default)('ACP tool call update is emitted with its pending tool name', () => {
22
23
  const appended = [];
23
24
  const emitted = [];
@@ -71,6 +72,29 @@ const local_core_acp_store_js_1 = require("../services/local-ai-core/src/acp/loc
71
72
  strict_1.default.equal(emitted.length, 1);
72
73
  strict_1.default.equal(emitted[0]?.content, appended[0]?.content);
73
74
  });
75
+ (0, node_test_1.default)('ACP permission normalization treats allow_all as allow all', () => {
76
+ strict_1.default.equal((0, workspace_acp_permissions_js_1.normalizePermissionAction)('allow_all'), 'allow all');
77
+ strict_1.default.equal((0, workspace_acp_permissions_js_1.normalizePermissionAction)('allow_always'), 'allow all');
78
+ strict_1.default.equal((0, workspace_acp_permissions_js_1.normalizePermissionAction)('always'), 'allow all');
79
+ strict_1.default.equal((0, workspace_acp_permissions_js_1.normalizePermissionAction)('allow_once'), 'allow');
80
+ });
81
+ (0, node_test_1.default)('ACP permission normalization preserves always-allow option semantics', () => {
82
+ strict_1.default.equal((0, workspace_acp_permissions_js_1.normalizePermissionOptionAction)({
83
+ optionId: 'approve',
84
+ name: 'Always allow',
85
+ kind: 'allow',
86
+ }), 'allow all');
87
+ strict_1.default.equal((0, workspace_acp_permissions_js_1.normalizePermissionOptionAction)({
88
+ optionId: 'allow-all-tools',
89
+ name: '始终允许',
90
+ kind: 'allow',
91
+ }), 'allow all');
92
+ strict_1.default.equal((0, workspace_acp_permissions_js_1.normalizePermissionOptionAction)({
93
+ optionId: 'allow_once',
94
+ name: 'Allow once',
95
+ kind: 'allow',
96
+ }), 'allow');
97
+ });
74
98
  (0, node_test_1.default)('ACP tool call running and completed updates share one message id', () => {
75
99
  const upserted = [];
76
100
  const emitted = [];
@@ -305,6 +329,55 @@ const local_core_acp_store_js_1 = require("../services/local-ai-core/src/acp/loc
305
329
  strict_1.default.match(upserted[0]?.content || '', /ls -la ~\/Desktop/);
306
330
  strict_1.default.match(upserted[0]?.content || '', /completed - Desktop file list/);
307
331
  });
332
+ (0, node_test_1.default)('ACP permission button rows preserve always allow actions', () => {
333
+ const emitted = [];
334
+ const coordinator = new local_core_acp_turn_coordinator_js_1.LocalCoreAcpTurnCoordinator({
335
+ appendMessage: () => { },
336
+ emitBridge: (event) => emitted.push(event),
337
+ updateRunStatus: () => { },
338
+ sendRaw: () => true,
339
+ });
340
+ const session = {
341
+ threadId: 'thread-1',
342
+ bridgeSessionKey: 'session:thread-1',
343
+ currentRunId: 'run-1',
344
+ currentTurn: {
345
+ runId: 'run-1',
346
+ replyCtx: 'run-1',
347
+ previewHandle: 'preview-1',
348
+ assistantText: '',
349
+ typingStarted: true,
350
+ previewStarted: false,
351
+ permission: null,
352
+ },
353
+ pendingPermissionByRun: new Map(),
354
+ loadReplayMode: false,
355
+ schedulerJobCreatedByRun: new Map(),
356
+ };
357
+ coordinator.handleAgentRequest(session, {
358
+ method: 'session/request_permission',
359
+ id: 42,
360
+ params: {
361
+ toolCall: {
362
+ title: 'Terminal',
363
+ parameters: {
364
+ command: 'system_profiler SPHardwareDataType',
365
+ },
366
+ },
367
+ options: [
368
+ { optionId: 'approve-once', name: 'Allow once', kind: 'allow' },
369
+ { optionId: 'approve-always', name: 'Always allow', kind: 'allow' },
370
+ { optionId: 'reject', name: 'Reject', kind: 'reject' },
371
+ ],
372
+ },
373
+ });
374
+ strict_1.default.deepEqual(emitted[0]?.buttonRows, [[
375
+ { text: 'allow', data: 'allow' },
376
+ { text: 'allow all', data: 'allow all' },
377
+ { text: 'deny', data: 'deny' },
378
+ ]]);
379
+ strict_1.default.equal(session.pendingPermissionByRun.get('run-1')?.options[1]?.optionId, 'approve-always');
380
+ });
308
381
  (0, node_test_1.default)('ACP bare tool call is flushed before assistant text', () => {
309
382
  const appended = [];
310
383
  const emitted = [];
@@ -573,6 +646,81 @@ const local_core_acp_store_js_1 = require("../services/local-ai-core/src/acp/loc
573
646
  strict_1.default.equal(patchedCards[0]?.text, '💭 先理解问题,再检查代码');
574
647
  strict_1.default.deepEqual(storedMessageIds, ['lark-msg-2']);
575
648
  });
649
+ (0, node_test_1.default)('lark bridge sends final reply as a separate card after streamed final updates', async () => {
650
+ const createdCards = [];
651
+ const patchedCards = [];
652
+ const storedMessageIds = [];
653
+ const client = {
654
+ im: {
655
+ message: {
656
+ create: async (request) => {
657
+ const messageId = `lark-msg-${createdCards.length + 1}`;
658
+ const card = JSON.parse(String(request.data.content || '{}'));
659
+ createdCards.push({
660
+ messageId,
661
+ text: String(card.elements?.[0]?.content || ''),
662
+ });
663
+ return { data: { message_id: messageId } };
664
+ },
665
+ patch: async (request) => {
666
+ const card = JSON.parse(String(request.data.content || '{}'));
667
+ patchedCards.push({
668
+ messageId: String(request.path.message_id || ''),
669
+ text: String(card.elements?.[0]?.content || ''),
670
+ });
671
+ },
672
+ },
673
+ },
674
+ };
675
+ const gateway = new local_core_lark_gateway_js_1.LocalCoreLarkGateway({
676
+ store: {
677
+ getPlatformThreadBinding: () => ({
678
+ workspace_id: 'default',
679
+ platform: 'lark',
680
+ chat_id: 'chat-1',
681
+ platform_user_id: 'user-1',
682
+ thread_id: 'thread-1',
683
+ last_platform_message_id: null,
684
+ }),
685
+ updatePlatformThreadMessageId: (_workspaceId, _chatId, _platformUserId, messageId) => {
686
+ storedMessageIds.push(messageId);
687
+ },
688
+ },
689
+ readConfig: async () => null,
690
+ getWorkspaceRouter: () => ({}),
691
+ eventBus: { emit: () => { }, on: () => () => { } },
692
+ });
693
+ const internals = gateway;
694
+ internals.runtime.set('default', {
695
+ workspaceId: 'default',
696
+ enabled: true,
697
+ status: 'running',
698
+ connected: true,
699
+ appId: 'app-1',
700
+ client,
701
+ });
702
+ internals.threadRouting.set('session:thread-1', {
703
+ workspaceId: 'default',
704
+ platformUserId: 'user-1',
705
+ chatId: 'chat-1',
706
+ threadId: 'thread-1',
707
+ });
708
+ await gateway.onBridgeEvent({
709
+ type: 'update_message',
710
+ sessionKey: 'session:thread-1',
711
+ replyCtx: 'run-1',
712
+ content: '流式中的最终回答草稿',
713
+ });
714
+ await gateway.onBridgeEvent({
715
+ type: 'reply',
716
+ sessionKey: 'session:thread-1',
717
+ replyCtx: 'run-1',
718
+ content: '真正最终回答',
719
+ });
720
+ strict_1.default.deepEqual(createdCards.map((card) => card.text), ['流式中的最终回答草稿', '真正最终回答']);
721
+ strict_1.default.equal(patchedCards.length, 0);
722
+ strict_1.default.deepEqual(storedMessageIds, ['lark-msg-1', 'lark-msg-2']);
723
+ });
576
724
  (0, node_test_1.default)('lark bridge does not leave typing placeholders or throttle thought updates', async () => {
577
725
  const createdCards = [];
578
726
  const patchedCards = [];
@@ -779,6 +927,116 @@ const local_core_acp_store_js_1 = require("../services/local-ai-core/src/acp/loc
779
927
  strict_1.default.match(patchedCards[0]?.card?.elements?.[0]?.content || '', /工具确认已处理/);
780
928
  strict_1.default.equal(patchedCards[0]?.card?.elements?.some((element) => element.tag === 'action'), false);
781
929
  });
930
+ (0, node_test_1.default)('lark allow all card action preserves the final reply after tool execution', async () => {
931
+ const createdCards = [];
932
+ const patchedCards = [];
933
+ let nextMessageId = 1;
934
+ let gateway;
935
+ const client = {
936
+ im: {
937
+ message: {
938
+ create: async (request) => {
939
+ const messageId = `card-${nextMessageId++}`;
940
+ createdCards.push({
941
+ messageId,
942
+ card: JSON.parse(String(request.data.content || '{}')),
943
+ });
944
+ return { data: { message_id: messageId } };
945
+ },
946
+ patch: async (request) => {
947
+ patchedCards.push({
948
+ messageId: request.path?.message_id,
949
+ card: JSON.parse(String(request.data.content || '{}')),
950
+ });
951
+ },
952
+ },
953
+ },
954
+ };
955
+ gateway = new local_core_lark_gateway_js_1.LocalCoreLarkGateway({
956
+ store: {
957
+ getPlatformThreadBinding: () => ({
958
+ workspace_id: 'default',
959
+ platform: 'lark',
960
+ chat_id: 'chat-1',
961
+ platform_user_id: 'user-1',
962
+ thread_id: 'thread-1',
963
+ last_platform_message_id: null,
964
+ }),
965
+ clearPlatformThreadMessageId: () => { },
966
+ updatePlatformThreadMessageId: () => { },
967
+ },
968
+ readConfig: async () => null,
969
+ getWorkspaceRouter: () => ({
970
+ sendThreadAction: async (threadId, action) => {
971
+ strict_1.default.equal(threadId, 'thread-1');
972
+ strict_1.default.equal(action, 'allow all');
973
+ await gateway.onBridgeEvent({
974
+ type: 'typing_start',
975
+ sessionKey: 'session:thread-1',
976
+ replyCtx: 'run-1',
977
+ });
978
+ await gateway.onBridgeEvent({
979
+ type: 'reply',
980
+ sessionKey: 'session:thread-1',
981
+ replyCtx: 'run-1',
982
+ content: '桌面文件列表:AI进展报告_2026年4月.md',
983
+ });
984
+ return { runId: 'run-1' };
985
+ },
986
+ }),
987
+ eventBus: { emit: () => { }, on: () => () => { } },
988
+ });
989
+ const internals = gateway;
990
+ internals.runtime.set('default', {
991
+ workspaceId: 'default',
992
+ enabled: true,
993
+ status: 'running',
994
+ connected: true,
995
+ appId: 'app-1',
996
+ cardActionsEnabled: true,
997
+ client,
998
+ });
999
+ internals.threadRouting.set('session:thread-1', {
1000
+ workspaceId: 'default',
1001
+ platformUserId: 'user-1',
1002
+ chatId: 'chat-1',
1003
+ threadId: 'thread-1',
1004
+ });
1005
+ internals.outboundTurns.set('session:thread-1', {
1006
+ sessionKey: 'session:thread-1',
1007
+ progressMessageIds: {},
1008
+ permissionMessageId: 'permission-msg-1',
1009
+ awaitingPermission: true,
1010
+ processing: true,
1011
+ previewText: '',
1012
+ finalText: '',
1013
+ thinkingSteps: [],
1014
+ toolCalls: [],
1015
+ statusLines: [],
1016
+ buttonRows: [[{ text: '始终允许', data: 'allow all' }]],
1017
+ lastPatchedAt: 0,
1018
+ lastPatchedAtByMessageId: {},
1019
+ });
1020
+ await internals.handleCardActionEvent('default', {
1021
+ event: {
1022
+ action: {
1023
+ value: {
1024
+ action: 'permission_response',
1025
+ response: 'allow all',
1026
+ thread_id: 'thread-1',
1027
+ session_key: 'session:thread-1',
1028
+ },
1029
+ },
1030
+ context: {
1031
+ open_message_id: 'permission-msg-1',
1032
+ },
1033
+ },
1034
+ });
1035
+ strict_1.default.equal(patchedCards[0]?.messageId, 'permission-msg-1');
1036
+ strict_1.default.match(patchedCards[0]?.card?.elements?.[0]?.content || '', /工具确认已处理/);
1037
+ strict_1.default.equal(createdCards.length, 1);
1038
+ strict_1.default.match(createdCards[0]?.card?.elements?.[0]?.content || '', /桌面文件列表/);
1039
+ });
782
1040
  (0, node_test_1.default)('lark channel folds tool result output in progress cards', async () => {
783
1041
  const createdCards = [];
784
1042
  const patchedCards = [];
@@ -1044,6 +1302,102 @@ const local_core_acp_store_js_1 = require("../services/local-ai-core/src/acp/loc
1044
1302
  strict_1.default.equal(sentMessages[0]?.content?.contentParts?.[1]?.mimeType, 'image/png');
1045
1303
  strict_1.default.equal(sentMessages[0]?.content?.contentParts?.[1]?.data, pngBytes.toString('base64'));
1046
1304
  });
1305
+ (0, node_test_1.default)('lark file messages are downloaded and forwarded as generic channel file parts', async () => {
1306
+ const sentMessages = [];
1307
+ const fileBytes = Buffer.from('file content');
1308
+ const client = {
1309
+ im: {
1310
+ messageResource: {
1311
+ get: async (request) => {
1312
+ strict_1.default.equal(request.path.message_id, 'msg-file-in-1');
1313
+ strict_1.default.equal(request.path.file_key, 'file-key-in-1');
1314
+ strict_1.default.equal(request.params.type, 'file');
1315
+ return {
1316
+ headers: { 'content-type': 'application/pdf' },
1317
+ getReadableStream: () => node_stream_1.Readable.from([fileBytes]),
1318
+ };
1319
+ },
1320
+ },
1321
+ messageReaction: {
1322
+ create: async () => ({ data: { reaction_id: 'reaction-1' } }),
1323
+ },
1324
+ },
1325
+ };
1326
+ const gateway = new local_core_lark_gateway_js_1.LocalCoreLarkGateway({
1327
+ store: {
1328
+ expirePendingPairings: () => { },
1329
+ getAuthorizedUser: () => ({
1330
+ id: 'auth-1',
1331
+ workspace_id: 'default',
1332
+ platform: 'lark',
1333
+ platform_user_id: 'user-1',
1334
+ chat_id: 'chat-1',
1335
+ display_name: 'User',
1336
+ thread_id: 'thread-1',
1337
+ }),
1338
+ getPlatformThreadBinding: () => ({
1339
+ workspace_id: 'default',
1340
+ platform: 'lark',
1341
+ chat_id: 'chat-1',
1342
+ platform_user_id: 'user-1',
1343
+ thread_id: 'thread-1',
1344
+ last_platform_message_id: null,
1345
+ }),
1346
+ getLatestRunForThread: () => null,
1347
+ clearPlatformThreadMessageId: () => { },
1348
+ },
1349
+ readConfig: async () => ({
1350
+ projects: [{
1351
+ name: 'default',
1352
+ root: '/tmp/project',
1353
+ platforms: [{
1354
+ type: 'lark',
1355
+ options: {
1356
+ app_id: 'app-1',
1357
+ app_secret: 'secret-1',
1358
+ },
1359
+ }],
1360
+ }],
1361
+ }),
1362
+ getWorkspaceRouter: () => ({
1363
+ getThreadSessionKey: (threadId) => `session:${threadId}`,
1364
+ sendThreadMessage: async (threadId, content) => {
1365
+ sentMessages.push({ threadId, content });
1366
+ return { runId: 'run-1' };
1367
+ },
1368
+ }),
1369
+ eventBus: { emit: () => { }, on: () => () => { } },
1370
+ });
1371
+ const internals = gateway;
1372
+ internals.runtime.set('default', {
1373
+ workspaceId: 'default',
1374
+ enabled: true,
1375
+ status: 'running',
1376
+ connected: true,
1377
+ appId: 'app-1',
1378
+ client,
1379
+ });
1380
+ await internals.handleMessageEvent('default', {
1381
+ event: {
1382
+ sender: {
1383
+ sender_id: { user_id: 'user-1' },
1384
+ },
1385
+ message: {
1386
+ message_id: 'msg-file-in-1',
1387
+ message_type: 'file',
1388
+ chat_id: 'chat-1',
1389
+ content: JSON.stringify({ file_key: 'file-key-in-1', file_name: 'report.pdf' }),
1390
+ },
1391
+ },
1392
+ });
1393
+ strict_1.default.equal(sentMessages.length, 1);
1394
+ strict_1.default.equal(sentMessages[0]?.threadId, 'thread-1');
1395
+ strict_1.default.match(sentMessages[0]?.content?.displayText, /\[User Message\]\n\[File: report\.pdf\]\n\[\/User Message\]/);
1396
+ strict_1.default.deepEqual(sentMessages[0]?.content?.contentParts?.map((part) => part.type), ['text', 'file']);
1397
+ strict_1.default.equal(sentMessages[0]?.content?.contentParts?.[1]?.mimeType, 'application/pdf');
1398
+ strict_1.default.equal(sentMessages[0]?.content?.contentParts?.[1]?.data, fileBytes.toString('base64'));
1399
+ strict_1.default.equal(sentMessages[0]?.content?.contentParts?.[1]?.fileName, 'report.pdf');
1400
+ });
1047
1401
  (0, node_test_1.default)('lark channel can upload and send a local file', async () => {
1048
1402
  const tempDir = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'lark-file-send-'));
1049
1403
  try {
@@ -1310,6 +1664,80 @@ const local_core_acp_store_js_1 = require("../services/local-ai-core/src/acp/loc
1310
1664
  line.includes('sender=user-1')));
1311
1665
  strict_1.default.ok(logs.some((line) => line.includes('handling message event') && line.includes('contentBytes=')));
1312
1666
  });
1667
+ (0, node_test_1.default)('lark inbound messages use active runtime binding before config refresh catches up', async () => {
1668
+ const users = new Map();
1669
+ const threadBindings = new Map();
1670
+ const createdThreads = [];
1671
+ const sentCards = [];
1672
+ const platformKey = 'lark:lark-hot';
1673
+ const bindingKey = `${platformKey}:chat-1:user-1`;
1674
+ const gateway = new local_core_lark_gateway_js_1.LocalCoreLarkGateway({
1675
+ store: {
1676
+ expirePendingPairings: () => { },
1677
+ listPendingPairings: () => [],
1678
+ listAuthorizedUsers: () => [...users.values()],
1679
+ getAuthorizedUser: (_workspaceId, platformUserId, requestedPlatform) => users.get(`${requestedPlatform}:${platformUserId}`),
1680
+ createAuthorizedUser: (user) => users.set(`${user.platform}:${user.platform_user_id}`, user),
1681
+ updateAuthorizedUserThread: (_workspaceId, platformUserId, threadId, requestedPlatform) => {
1682
+ users.set(`${requestedPlatform}:${platformUserId}`, { ...users.get(`${requestedPlatform}:${platformUserId}`), thread_id: threadId });
1683
+ },
1684
+ getPlatformThreadBinding: () => threadBindings.get(bindingKey),
1685
+ upsertPlatformThreadBinding: (binding) => threadBindings.set(bindingKey, binding),
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.match(sentCards[0]?.elements?.[0]?.content || '', /已开始新会话/);
1740
+ });
1313
1741
  (0, node_test_1.default)('ACP skips empty generic running tool updates', () => {
1314
1742
  const appended = [];
1315
1743
  const emitted = [];
@@ -1696,6 +2124,8 @@ const local_core_acp_store_js_1 = require("../services/local-ai-core/src/acp/loc
1696
2124
  ticket: 'ticket-1',
1697
2125
  expiresIn: 180,
1698
2126
  qrCodeUrl: 'https://liteapp.weixin.qq.com/q/test?qrcode=ticket-1&bot_type=3',
2127
+ instanceId: 'default',
2128
+ displayName: 'WeChat 1',
1699
2129
  });
1700
2130
  strict_1.default.equal(requests[0]?.url, 'https://ilinkai.weixin.qq.com/ilink/bot/get_bot_qrcode?bot_type=3');
1701
2131
  strict_1.default.equal(requests[0]?.headers.has('Authorization'), false);
@@ -1764,6 +2194,177 @@ const local_core_acp_store_js_1 = require("../services/local-ai-core/src/acp/loc
1764
2194
  (0, node_fs_1.rmSync)(stateDir, { recursive: true, force: true });
1765
2195
  }
1766
2196
  });
2197
+ (0, node_test_1.default)('lark channel can request an official app registration QR code without extra setup', async () => {
2198
+ const originalFetch = globalThis.fetch;
2199
+ const requests = [];
2200
+ globalThis.fetch = (async (input, init) => {
2201
+ requests.push({
2202
+ url: String(input),
2203
+ method: init?.method,
2204
+ body: String(init?.body || ''),
2205
+ headers: new Headers(init?.headers),
2206
+ });
2207
+ return new Response(JSON.stringify({
2208
+ device_code: 'device-code-1',
2209
+ user_code: 'ABCD-EFGH',
2210
+ expires_in: 300,
2211
+ interval: 5,
2212
+ }), {
2213
+ status: 200,
2214
+ headers: { 'Content-Type': 'application/json' },
2215
+ });
2216
+ });
2217
+ try {
2218
+ const gateway = new local_core_lark_gateway_js_1.LocalCoreLarkGateway({
2219
+ store: {},
2220
+ readConfig: async () => ({
2221
+ projects: [
2222
+ {
2223
+ name: 'default',
2224
+ agent: { type: 'localcore-acp', providers: [] },
2225
+ platforms: [{ type: 'lark', options: {} }],
2226
+ },
2227
+ ],
2228
+ }),
2229
+ getWorkspaceRouter: () => ({}),
2230
+ eventBus: { emit: () => { }, on: () => () => { } },
2231
+ });
2232
+ const result = await gateway.getQrCode('default');
2233
+ strict_1.default.deepEqual(result, {
2234
+ ticket: 'device-code-1',
2235
+ expiresIn: 300,
2236
+ interval: 5,
2237
+ qrCodeUrl: 'https://open.feishu.cn/page/openclaw?user_code=ABCD-EFGH&from=openclaw',
2238
+ instanceId: 'default',
2239
+ displayName: 'Lark 1',
2240
+ });
2241
+ strict_1.default.equal(requests[0]?.url, 'https://accounts.feishu.cn/oauth/v1/app/registration');
2242
+ strict_1.default.equal(requests[0]?.method, 'POST');
2243
+ strict_1.default.equal(requests[0]?.headers.get('Content-Type'), 'application/x-www-form-urlencoded');
2244
+ strict_1.default.equal(new URLSearchParams(requests[0]?.body).get('action'), 'begin');
2245
+ strict_1.default.equal(new URLSearchParams(requests[0]?.body).get('archetype'), 'PersonalAgent');
2246
+ strict_1.default.equal(new URLSearchParams(requests[0]?.body).get('request_callbacks'), null);
2247
+ }
2248
+ finally {
2249
+ globalThis.fetch = originalFetch;
2250
+ }
2251
+ });
2252
+ (0, node_test_1.default)('lark app registration QR confirmation returns app credentials', async () => {
2253
+ const originalFetch = globalThis.fetch;
2254
+ const requests = [];
2255
+ globalThis.fetch = (async (input, init) => {
2256
+ const url = String(input);
2257
+ requests.push({ url, body: String(init?.body || '') });
2258
+ return new Response(JSON.stringify({
2259
+ client_id: 'cli_lark_1',
2260
+ client_secret: 'secret-1',
2261
+ }), {
2262
+ status: 200,
2263
+ headers: { 'Content-Type': 'application/json' },
2264
+ });
2265
+ });
2266
+ try {
2267
+ const gateway = new local_core_lark_gateway_js_1.LocalCoreLarkGateway({
2268
+ store: {},
2269
+ readConfig: async () => ({
2270
+ projects: [
2271
+ {
2272
+ name: 'default',
2273
+ agent: { type: 'localcore-acp', providers: [] },
2274
+ platforms: [{ type: 'lark', options: {} }],
2275
+ },
2276
+ ],
2277
+ }),
2278
+ getWorkspaceRouter: () => ({}),
2279
+ eventBus: { emit: () => { }, on: () => () => { } },
2280
+ });
2281
+ const result = await gateway.checkQrCodeStatus('default', 'lark-ticket-1');
2282
+ strict_1.default.equal(requests[0]?.url, 'https://accounts.feishu.cn/oauth/v1/app/registration');
2283
+ strict_1.default.equal(new URLSearchParams(requests[0]?.body).get('action'), 'poll');
2284
+ strict_1.default.equal(new URLSearchParams(requests[0]?.body).get('device_code'), 'lark-ticket-1');
2285
+ strict_1.default.deepEqual(result, {
2286
+ status: 'confirmed',
2287
+ credentials: {
2288
+ appId: 'cli_lark_1',
2289
+ appSecret: 'secret-1',
2290
+ },
2291
+ });
2292
+ }
2293
+ finally {
2294
+ globalThis.fetch = originalFetch;
2295
+ }
2296
+ });
2297
+ (0, node_test_1.default)('lark channel keeps multiple bot instances in one workspace isolated', async () => {
2298
+ const gateway = new local_core_lark_gateway_js_1.LocalCoreLarkGateway({
2299
+ store: {
2300
+ expirePendingPairings: () => { },
2301
+ listPendingPairings: () => [],
2302
+ listAuthorizedUsers: () => [],
2303
+ },
2304
+ readConfig: async () => ({
2305
+ projects: [
2306
+ {
2307
+ name: 'default',
2308
+ agent: { type: 'localcore-acp', providers: [] },
2309
+ platforms: [
2310
+ { type: 'lark', options: { instance_id: 'bot-a', app_id: 'cli_a', app_secret: 'secret-a' } },
2311
+ { type: 'lark', options: { instance_id: 'bot-b', app_id: 'cli_b', app_secret: 'secret-b' } },
2312
+ ],
2313
+ },
2314
+ ],
2315
+ }),
2316
+ getWorkspaceRouter: () => ({}),
2317
+ eventBus: { emit: () => { }, on: () => () => { } },
2318
+ });
2319
+ gateway.larkModulePromise = Promise.resolve({
2320
+ AppType: { SelfBuild: 'self-build' },
2321
+ Domain: { Feishu: 'feishu' },
2322
+ LoggerLevel: { info: 'info' },
2323
+ Client: class {
2324
+ },
2325
+ EventDispatcher: class {
2326
+ register() { }
2327
+ },
2328
+ WSClient: class {
2329
+ async start() { }
2330
+ },
2331
+ });
2332
+ await gateway.refreshBindings();
2333
+ const statuses = gateway.listStatuses();
2334
+ strict_1.default.deepEqual(statuses.map((status) => [status.workspaceId, status.instanceId, status.appId, status.status]), [
2335
+ ['default', 'bot-a', 'cli_a', 'running'],
2336
+ ['default', 'bot-b', 'cli_b', 'running'],
2337
+ ]);
2338
+ });
2339
+ (0, node_test_1.default)('weixin channel keeps multiple bot instances in one workspace isolated', async () => {
2340
+ const gateway = new local_core_weixin_gateway_js_1.LocalCoreWeixinGateway({
2341
+ store: {
2342
+ expirePendingPairings: () => { },
2343
+ listPendingPairings: () => [],
2344
+ listAuthorizedUsers: () => [],
2345
+ },
2346
+ readConfig: async () => ({
2347
+ projects: [
2348
+ {
2349
+ name: 'default',
2350
+ agent: { type: 'localcore-acp', providers: [] },
2351
+ platforms: [
2352
+ { type: 'weixin', options: { instance_id: 'wx-a', account_id: 'account-a' } },
2353
+ { type: 'weixin', options: { instance_id: 'wx-b', account_id: 'account-b' } },
2354
+ ],
2355
+ },
2356
+ ],
2357
+ }),
2358
+ getWorkspaceRouter: () => ({}),
2359
+ eventBus: { emit: () => { }, on: () => () => { } },
2360
+ });
2361
+ await gateway.refreshBindings();
2362
+ const statuses = gateway.listStatuses();
2363
+ strict_1.default.deepEqual(statuses.map((status) => [status.workspaceId, status.instanceId, status.appId, status.status]), [
2364
+ ['default', 'wx-a', 'account-a', 'stopped'],
2365
+ ['default', 'wx-b', 'account-b', 'stopped'],
2366
+ ]);
2367
+ });
1767
2368
  (0, node_test_1.default)('weixin inbound message handling is idempotent by message identity', async () => {
1768
2369
  const sentThreadMessages = [];
1769
2370
  const users = new Map();
@@ -1820,6 +2421,33 @@ const local_core_acp_store_js_1 = require("../services/local-ai-core/src/acp/loc
1820
2421
  strict_1.default.equal(sentThreadMessages.length, 1);
1821
2422
  strict_1.default.match(sentThreadMessages[0] || '', /hello/);
1822
2423
  });
2424
+ (0, node_test_1.default)('weixin downloaded file attachment becomes a structured file content part', () => {
2425
+ const part = (0, local_core_weixin_gateway_js_1.createWeixinAttachmentContentPart)({
2426
+ path: '/tmp/report.pdf',
2427
+ kind: 'file',
2428
+ name: 'report.pdf',
2429
+ });
2430
+ strict_1.default.deepEqual(part, {
2431
+ type: 'file',
2432
+ path: '/tmp/report.pdf',
2433
+ fileName: 'report.pdf',
2434
+ });
2435
+ });
2436
+ (0, node_test_1.default)('weixin downloaded image attachment keeps image data content part', () => {
2437
+ const part = (0, local_core_weixin_gateway_js_1.createWeixinAttachmentContentPart)({
2438
+ path: '/tmp/image.png',
2439
+ kind: 'image',
2440
+ name: 'image.png',
2441
+ data: 'aW1n',
2442
+ mimeType: 'image/png',
2443
+ });
2444
+ strict_1.default.deepEqual(part, {
2445
+ type: 'image',
2446
+ data: 'aW1n',
2447
+ mimeType: 'image/png',
2448
+ fileName: 'image.png',
2449
+ });
2450
+ });
1823
2451
  (0, node_test_1.default)('weixin bridge skips duplicate rendered replies', async () => {
1824
2452
  const originalFetch = globalThis.fetch;
1825
2453
  const sentBodies = [];