@lobehub/chat 1.1.16 → 1.1.18

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 (121) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/locales/ar/plugin.json +1 -0
  3. package/locales/ar/portal.json +4 -0
  4. package/locales/bg-BG/plugin.json +1 -0
  5. package/locales/bg-BG/portal.json +4 -0
  6. package/locales/de-DE/plugin.json +1 -0
  7. package/locales/de-DE/portal.json +4 -0
  8. package/locales/en-US/plugin.json +1 -0
  9. package/locales/en-US/portal.json +4 -0
  10. package/locales/es-ES/plugin.json +1 -0
  11. package/locales/es-ES/portal.json +4 -0
  12. package/locales/fr-FR/plugin.json +1 -0
  13. package/locales/fr-FR/portal.json +4 -0
  14. package/locales/it-IT/plugin.json +1 -0
  15. package/locales/it-IT/portal.json +4 -0
  16. package/locales/ja-JP/plugin.json +1 -0
  17. package/locales/ja-JP/portal.json +4 -0
  18. package/locales/ko-KR/plugin.json +1 -0
  19. package/locales/ko-KR/portal.json +4 -0
  20. package/locales/nl-NL/plugin.json +1 -0
  21. package/locales/nl-NL/portal.json +4 -0
  22. package/locales/pl-PL/plugin.json +1 -0
  23. package/locales/pl-PL/portal.json +4 -0
  24. package/locales/pt-BR/plugin.json +1 -0
  25. package/locales/pt-BR/portal.json +4 -0
  26. package/locales/ru-RU/plugin.json +1 -0
  27. package/locales/ru-RU/portal.json +4 -0
  28. package/locales/tr-TR/plugin.json +1 -0
  29. package/locales/tr-TR/portal.json +4 -0
  30. package/locales/vi-VN/plugin.json +1 -0
  31. package/locales/vi-VN/portal.json +4 -0
  32. package/locales/zh-CN/plugin.json +1 -0
  33. package/locales/zh-CN/portal.json +4 -0
  34. package/locales/zh-TW/plugin.json +1 -0
  35. package/locales/zh-TW/portal.json +4 -0
  36. package/package.json +1 -1
  37. package/src/app/(main)/chat/(workspace)/@portal/_layout/Desktop.tsx +17 -0
  38. package/src/app/(main)/chat/(workspace)/@portal/_layout/Mobile.tsx +18 -0
  39. package/src/app/(main)/chat/(workspace)/@portal/default.tsx +27 -0
  40. package/src/app/(main)/chat/(workspace)/@portal/features/Header.tsx +52 -0
  41. package/src/app/(main)/chat/(workspace)/@portal/features/Tools/ToolList/Item/index.tsx +74 -0
  42. package/src/app/(main)/chat/(workspace)/@portal/features/Tools/ToolList/Item/style.ts +46 -0
  43. package/src/app/(main)/chat/(workspace)/@portal/features/Tools/ToolList/index.tsx +39 -0
  44. package/src/app/(main)/chat/(workspace)/@portal/features/Tools/ToolUI/Footer.tsx +33 -0
  45. package/src/app/(main)/chat/(workspace)/@portal/features/Tools/ToolUI/ToolRender.tsx +50 -0
  46. package/src/app/(main)/chat/(workspace)/@portal/features/Tools/ToolUI/index.tsx +37 -0
  47. package/src/app/(main)/chat/(workspace)/@portal/index.tsx +18 -0
  48. package/src/app/(main)/chat/(workspace)/_layout/Desktop/Portal.tsx +79 -0
  49. package/src/app/(main)/chat/(workspace)/_layout/Desktop/TopicPanel.tsx +27 -22
  50. package/src/app/(main)/chat/(workspace)/_layout/Desktop/index.tsx +3 -1
  51. package/src/app/(main)/chat/(workspace)/_layout/type.ts +1 -0
  52. package/src/app/(main)/chat/loading.tsx +2 -20
  53. package/src/components/CircleLoading/index.tsx +21 -0
  54. package/src/config/modelProviders/google.ts +44 -61
  55. package/src/const/layoutTokens.test.ts +11 -0
  56. package/src/const/layoutTokens.ts +4 -0
  57. package/src/const/message.ts +0 -14
  58. package/src/database/client/models/__tests__/message.test.ts +9 -12
  59. package/src/database/client/models/message.ts +6 -0
  60. package/src/database/server/models/__tests__/message.test.ts +70 -0
  61. package/src/database/server/models/message.ts +10 -0
  62. package/src/database/server/schemas/lobechat.ts +3 -1
  63. package/src/features/Conversation/Messages/Assistant/ToolCalls/index.tsx +3 -17
  64. package/src/features/Conversation/Messages/Tool/Inspector/index.tsx +26 -2
  65. package/src/features/Conversation/Messages/Tool/index.tsx +33 -6
  66. package/src/features/Conversation/Messages/components/Arguments.tsx +1 -1
  67. package/src/features/PluginAvatar/index.tsx +28 -0
  68. package/src/features/PluginsUI/Render/BuiltinType/index.tsx +44 -0
  69. package/src/features/{Conversation/Plugins → PluginsUI}/Render/StandaloneType/index.tsx +8 -1
  70. package/src/features/{Conversation/Plugins → PluginsUI}/Render/index.tsx +12 -1
  71. package/src/{features/Conversation/Messages/hooks → hooks}/useYamlArguments.ts +3 -1
  72. package/src/locales/default/index.ts +2 -0
  73. package/src/locales/default/plugin.ts +1 -0
  74. package/src/locales/default/portal.ts +4 -0
  75. package/src/server/routers/lambda/message.ts +12 -0
  76. package/src/services/message/client.test.ts +35 -0
  77. package/src/services/message/client.ts +6 -0
  78. package/src/services/message/server.ts +9 -0
  79. package/src/store/chat/initialState.ts +10 -1
  80. package/src/store/chat/selectors.ts +1 -0
  81. package/src/store/chat/slices/message/action.test.ts +1 -1
  82. package/src/store/chat/slices/message/action.ts +5 -5
  83. package/src/store/chat/slices/message/reducer.test.ts +230 -7
  84. package/src/store/chat/slices/message/reducer.ts +45 -22
  85. package/src/store/chat/slices/message/selectors.test.ts +133 -2
  86. package/src/store/chat/slices/message/selectors.ts +7 -0
  87. package/src/store/chat/slices/plugin/action.test.ts +309 -2
  88. package/src/store/chat/slices/plugin/action.ts +51 -1
  89. package/src/store/chat/slices/portal/action.test.ts +109 -0
  90. package/src/store/chat/slices/portal/action.ts +31 -0
  91. package/src/store/chat/slices/portal/initialState.ts +8 -0
  92. package/src/store/chat/slices/portal/selectors.test.ts +73 -0
  93. package/src/store/chat/slices/portal/selectors.ts +15 -0
  94. package/src/store/chat/store.ts +7 -1
  95. package/src/store/tool/selectors/tool.test.ts +14 -0
  96. package/src/store/tool/selectors/tool.ts +13 -0
  97. package/src/tools/docks.ts +3 -0
  98. package/src/types/tool/builtin.ts +11 -1
  99. package/src/utils/safeParseJSON.test.ts +71 -0
  100. package/src/utils/safeParseJSON.ts +12 -0
  101. package/src/const/message.test.ts +0 -55
  102. package/src/features/Conversation/Plugins/Render/BuiltinType/index.tsx +0 -30
  103. /package/src/features/{Conversation/Plugins → PluginsUI}/Render/BuiltinType/index.test.tsx +0 -0
  104. /package/src/features/{Conversation/Plugins → PluginsUI}/Render/DefaultType/IFrameRender/index.tsx +0 -0
  105. /package/src/features/{Conversation/Plugins → PluginsUI}/Render/DefaultType/SystemJsRender/index.tsx +0 -0
  106. /package/src/features/{Conversation/Plugins → PluginsUI}/Render/DefaultType/SystemJsRender/utils.ts +0 -0
  107. /package/src/features/{Conversation/Plugins → PluginsUI}/Render/DefaultType/index.tsx +0 -0
  108. /package/src/features/{Conversation/Plugins → PluginsUI}/Render/Loading.tsx +0 -0
  109. /package/src/features/{Conversation/Plugins → PluginsUI}/Render/MarkdownType/index.tsx +0 -0
  110. /package/src/features/{Conversation/Plugins → PluginsUI}/Render/StandaloneType/Iframe.tsx +0 -0
  111. /package/src/features/{Conversation/Plugins → PluginsUI}/Render/useParseContent.ts +0 -0
  112. /package/src/features/{Conversation/Plugins → PluginsUI}/Render/utils/iframeOnReady.test.ts +0 -0
  113. /package/src/features/{Conversation/Plugins → PluginsUI}/Render/utils/iframeOnReady.ts +0 -0
  114. /package/src/features/{Conversation/Plugins → PluginsUI}/Render/utils/listenToPlugin.test.ts +0 -0
  115. /package/src/features/{Conversation/Plugins → PluginsUI}/Render/utils/listenToPlugin.ts +0 -0
  116. /package/src/features/{Conversation/Plugins → PluginsUI}/Render/utils/pluginSettings.test.ts +0 -0
  117. /package/src/features/{Conversation/Plugins → PluginsUI}/Render/utils/pluginSettings.ts +0 -0
  118. /package/src/features/{Conversation/Plugins → PluginsUI}/Render/utils/pluginState.test.ts +0 -0
  119. /package/src/features/{Conversation/Plugins → PluginsUI}/Render/utils/pluginState.ts +0 -0
  120. /package/src/features/{Conversation/Plugins → PluginsUI}/Render/utils/postMessage.test.ts +0 -0
  121. /package/src/features/{Conversation/Plugins → PluginsUI}/Render/utils/postMessage.ts +0 -0
@@ -10,8 +10,7 @@ import { chatSelectors } from '@/store/chat/selectors';
10
10
  import { messageMapKey } from '@/store/chat/slices/message/utils';
11
11
  import { useChatStore } from '@/store/chat/store';
12
12
  import { useToolStore } from '@/store/tool';
13
- import { ChatMessage, ChatToolPayload } from '@/types/message';
14
- import { LobeTool } from '@/types/tool';
13
+ import { ChatMessage, ChatToolPayload, MessageToolCall } from '@/types/message';
15
14
 
16
15
  const invokeStandaloneTypePlugin = useChatStore.getState().invokeStandaloneTypePlugin;
17
16
 
@@ -23,6 +22,7 @@ vi.mock('@/services/message', () => ({
23
22
  updateMessage: vi.fn(),
24
23
  updateMessageError: vi.fn(),
25
24
  updateMessagePluginState: vi.fn(),
25
+ updateMessagePluginArguments: vi.fn(),
26
26
  createMessage: vi.fn(),
27
27
  },
28
28
  }));
@@ -657,4 +657,311 @@ describe('ChatPluginAction', () => {
657
657
  expect(result.current.refreshMessages).toHaveBeenCalled();
658
658
  });
659
659
  });
660
+
661
+ describe('reInvokeToolMessage', () => {
662
+ it('should re-invoke a tool message', async () => {
663
+ const messageId = 'message-id';
664
+ const message = {
665
+ id: messageId,
666
+ role: 'tool',
667
+ content: 'Original content',
668
+ plugin: {
669
+ type: 'default',
670
+ identifier: 'plugin-id',
671
+ apiName: 'api-name',
672
+ arguments: '{}',
673
+ },
674
+ tool_call_id: 'tool-id',
675
+ } as ChatMessage;
676
+
677
+ const internal_invokeDifferentTypePluginMock = vi.fn();
678
+ act(() => {
679
+ useChatStore.setState({
680
+ activeId: 'session-id',
681
+ messagesMap: { [messageMapKey('session-id')]: [message] },
682
+ internal_invokeDifferentTypePlugin: internal_invokeDifferentTypePluginMock,
683
+ internal_updateMessageError: vi.fn(),
684
+ });
685
+ });
686
+
687
+ const { result } = renderHook(() => useChatStore());
688
+
689
+ await act(async () => {
690
+ await result.current.reInvokeToolMessage(messageId);
691
+ });
692
+
693
+ expect(internal_invokeDifferentTypePluginMock).toHaveBeenCalledWith(
694
+ messageId,
695
+ expect.objectContaining(message.plugin),
696
+ );
697
+ });
698
+
699
+ it('should clear error content when re-invoking', async () => {
700
+ const messageId = 'message-id';
701
+ const message = {
702
+ id: messageId,
703
+ role: 'tool',
704
+ content: 'Original content',
705
+ plugin: {
706
+ type: 'default',
707
+ identifier: 'plugin-id',
708
+ apiName: 'api-name',
709
+ arguments: '{}',
710
+ },
711
+ tool_call_id: 'tool-id',
712
+ error: { message: 'Previous error', type: 'ProviderBizError' },
713
+ } as ChatMessage;
714
+
715
+ const internal_updateMessageErrorMock = vi.fn();
716
+
717
+ act(() => {
718
+ useChatStore.setState({
719
+ activeId: 'session-id',
720
+ messagesMap: { [messageMapKey('session-id')]: [message] },
721
+ internal_invokeDifferentTypePlugin: vi.fn(),
722
+ internal_updateMessageError: internal_updateMessageErrorMock,
723
+ });
724
+ });
725
+
726
+ const { result } = renderHook(() => useChatStore());
727
+
728
+ await act(async () => {
729
+ await result.current.reInvokeToolMessage(messageId);
730
+ });
731
+
732
+ expect(internal_updateMessageErrorMock).toHaveBeenCalledWith(messageId, null);
733
+ });
734
+ });
735
+
736
+ describe('updatePluginArguments', () => {
737
+ it('should update plugin arguments and refresh messages', async () => {
738
+ const messageId = 'message-id';
739
+ const toolCallId = 'tool-call-id';
740
+ const parentId = 'parent-id';
741
+ const identifier = 'plugin';
742
+ const newArguments = { newKey: 'newValue' };
743
+
744
+ const toolMessage = {
745
+ id: messageId,
746
+ role: 'tool',
747
+ content: 'Tool content',
748
+ plugin: { identifier: identifier, arguments: '{"oldKey":"oldValue"}' },
749
+ tool_call_id: toolCallId,
750
+ parentId,
751
+ } as ChatMessage;
752
+
753
+ const assistantMessage = {
754
+ id: parentId,
755
+ role: 'assistant',
756
+ content: 'Assistant content',
757
+ tools: [{ identifier: identifier, arguments: '{"oldKey":"oldValue"}', id: toolCallId }],
758
+ } as ChatMessage;
759
+
760
+ act(() => {
761
+ useChatStore.setState({
762
+ activeId: 'anbccfdd',
763
+ messagesMap: { [messageMapKey('anbccfdd')]: [assistantMessage, toolMessage] },
764
+ refreshMessages: vi.fn(),
765
+ });
766
+ });
767
+
768
+ const { result } = renderHook(() => useChatStore());
769
+
770
+ await act(async () => {
771
+ await result.current.updatePluginArguments(messageId, newArguments);
772
+ });
773
+
774
+ expect(messageService.updateMessagePluginArguments).toHaveBeenCalledWith(
775
+ messageId,
776
+ expect.objectContaining(newArguments),
777
+ );
778
+ // TODO: 需要验证 updateMessage 是否被调用
779
+ // expect(messageService.updateMessage).toHaveBeenCalledWith(
780
+ // parentId,
781
+ // expect.objectContaining({ tools: expect.any(Array) }),
782
+ // );
783
+ expect(result.current.refreshMessages).toHaveBeenCalled();
784
+ });
785
+ });
786
+
787
+ describe('internal_callPluginApi', () => {
788
+ it('should call plugin API and update message content', async () => {
789
+ const messageId = 'message-id';
790
+ const payload: ChatToolPayload = {
791
+ id: 'tool-id',
792
+ type: 'default',
793
+ identifier: 'plugin-id',
794
+ apiName: 'api-name',
795
+ arguments: '{}',
796
+ };
797
+ const apiResponse = 'API response';
798
+
799
+ vi.spyOn(chatService, 'runPluginApi').mockResolvedValue({
800
+ text: apiResponse,
801
+ traceId: 'trace-id',
802
+ });
803
+
804
+ act(() => {
805
+ useChatStore.setState({
806
+ internal_togglePluginApiCalling: vi.fn(),
807
+ internal_updateMessageContent: vi.fn(),
808
+ refreshMessages: vi.fn(),
809
+ });
810
+ });
811
+
812
+ const { result } = renderHook(() => useChatStore());
813
+
814
+ await act(async () => {
815
+ await result.current.internal_callPluginApi(messageId, payload);
816
+ });
817
+
818
+ expect(chatService.runPluginApi).toHaveBeenCalledWith(payload, expect.any(Object));
819
+ expect(result.current.internal_updateMessageContent).toHaveBeenCalledWith(
820
+ messageId,
821
+ apiResponse,
822
+ );
823
+ expect(messageService.updateMessage).toHaveBeenCalledWith(messageId, { traceId: 'trace-id' });
824
+ });
825
+
826
+ it('should handle API call errors', async () => {
827
+ const messageId = 'message-id';
828
+ const payload: ChatToolPayload = {
829
+ id: 'tool-id',
830
+ type: 'default',
831
+ identifier: 'plugin-id',
832
+ apiName: 'api-name',
833
+ arguments: '{}',
834
+ };
835
+ const error = new Error('API call failed');
836
+
837
+ vi.spyOn(chatService, 'runPluginApi').mockRejectedValue(error);
838
+
839
+ act(() => {
840
+ useChatStore.setState({
841
+ internal_togglePluginApiCalling: vi.fn(),
842
+ refreshMessages: vi.fn(),
843
+ });
844
+ });
845
+
846
+ const { result } = renderHook(() => useChatStore());
847
+
848
+ await act(async () => {
849
+ await result.current.internal_callPluginApi(messageId, payload);
850
+ });
851
+
852
+ expect(messageService.updateMessageError).toHaveBeenCalledWith(messageId, error);
853
+ expect(result.current.refreshMessages).toHaveBeenCalled();
854
+ });
855
+ });
856
+
857
+ describe('internal_transformToolCalls', () => {
858
+ it('should transform tool calls correctly', () => {
859
+ const toolCalls: MessageToolCall[] = [
860
+ {
861
+ id: 'tool1',
862
+ function: {
863
+ name: ['plugin1', 'api1', 'default'].join(PLUGIN_SCHEMA_SEPARATOR),
864
+ arguments: '{}',
865
+ },
866
+ type: 'function',
867
+ },
868
+ {
869
+ id: 'tool2',
870
+ function: {
871
+ name: ['plugin2', 'api2', 'markdown'].join(PLUGIN_SCHEMA_SEPARATOR),
872
+ arguments: '{}',
873
+ },
874
+ type: 'function',
875
+ },
876
+ ];
877
+
878
+ const { result } = renderHook(() => useChatStore());
879
+
880
+ const transformed = result.current.internal_transformToolCalls(toolCalls);
881
+
882
+ expect(transformed).toEqual([
883
+ {
884
+ id: 'tool1',
885
+ identifier: 'plugin1',
886
+ apiName: 'api1',
887
+ type: 'default',
888
+ arguments: '{}',
889
+ },
890
+ {
891
+ id: 'tool2',
892
+ identifier: 'plugin2',
893
+ apiName: 'api2',
894
+ type: 'markdown',
895
+ arguments: '{}',
896
+ },
897
+ ]);
898
+ });
899
+
900
+ it('should handle MD5 hashed API names', () => {
901
+ const apiName = 'testApi';
902
+ const md5Hash = Md5.hashStr(apiName);
903
+ const toolCalls: MessageToolCall[] = [
904
+ {
905
+ id: 'tool1',
906
+ function: {
907
+ name: ['plugin1', PLUGIN_SCHEMA_API_MD5_PREFIX + md5Hash, 'default'].join(
908
+ PLUGIN_SCHEMA_SEPARATOR,
909
+ ),
910
+ arguments: '{}',
911
+ },
912
+ type: 'function',
913
+ },
914
+ ];
915
+
916
+ act(() => {
917
+ useToolStore.setState({
918
+ installedPlugins: [
919
+ {
920
+ type: 'plugin',
921
+ identifier: 'plugin1',
922
+ manifest: {
923
+ identifier: 'plugin1',
924
+ api: [
925
+ {
926
+ name: apiName,
927
+ parameters: { type: 'object', properties: {} },
928
+ description: 'abc',
929
+ },
930
+ ],
931
+ type: 'default',
932
+ } as any,
933
+ },
934
+ ],
935
+ });
936
+ });
937
+
938
+ const { result } = renderHook(() => useChatStore());
939
+
940
+ const transformed = result.current.internal_transformToolCalls(toolCalls);
941
+
942
+ expect(transformed[0].apiName).toBe(apiName);
943
+ });
944
+ });
945
+
946
+ describe('internal_updatePluginError', () => {
947
+ it('should update plugin error and refresh messages', async () => {
948
+ const messageId = 'message-id';
949
+ const error = { message: 'Plugin error' };
950
+
951
+ act(() => {
952
+ useChatStore.setState({
953
+ refreshMessages: vi.fn(),
954
+ });
955
+ });
956
+
957
+ const { result } = renderHook(() => useChatStore());
958
+
959
+ await act(async () => {
960
+ await result.current.internal_updatePluginError(messageId, error);
961
+ });
962
+
963
+ expect(messageService.updateMessage).toHaveBeenCalledWith(messageId, { pluginError: error });
964
+ expect(result.current.refreshMessages).toHaveBeenCalled();
965
+ });
966
+ });
660
967
  });
@@ -1,5 +1,6 @@
1
1
  /* eslint-disable sort-keys-fix/sort-keys-fix, typescript-sort-keys/interface */
2
2
  import { PluginErrorType } from '@lobehub/chat-plugin-sdk';
3
+ import isEqual from 'fast-deep-equal';
3
4
  import { t } from 'i18next';
4
5
  import { Md5 } from 'ts-md5';
5
6
  import { StateCreator } from 'zustand/vanilla';
@@ -12,6 +13,8 @@ import { ChatStore } from '@/store/chat/store';
12
13
  import { useToolStore } from '@/store/tool';
13
14
  import { pluginSelectors } from '@/store/tool/selectors';
14
15
  import { ChatToolPayload, MessageToolCall } from '@/types/message';
16
+ import { merge } from '@/utils/merge';
17
+ import { safeParseJSON } from '@/utils/safeParseJSON';
15
18
  import { setNamespace } from '@/utils/storeDebug';
16
19
 
17
20
  import { chatSelectors } from '../../slices/message/selectors';
@@ -37,6 +40,7 @@ export interface ChatPluginAction {
37
40
 
38
41
  triggerToolCalls: (id: string) => Promise<void>;
39
42
  updatePluginState: (id: string, value: any) => Promise<void>;
43
+ updatePluginArguments: <T = any>(id: string, value: T) => Promise<void>;
40
44
 
41
45
  internal_callPluginApi: (id: string, payload: ChatToolPayload) => Promise<string | undefined>;
42
46
  internal_invokeDifferentTypePlugin: (id: string, payload: ChatToolPayload) => Promise<any>;
@@ -203,10 +207,56 @@ export const chatPlugin: StateCreator<
203
207
  updatePluginState: async (id, value) => {
204
208
  const { refreshMessages } = get();
205
209
 
210
+ // optimistic update
211
+ get().internal_dispatchMessage({ id, type: 'updateMessage', value: { pluginState: value } });
212
+
206
213
  await messageService.updateMessagePluginState(id, value);
207
214
  await refreshMessages();
208
215
  },
209
216
 
217
+ updatePluginArguments: async (id, value) => {
218
+ const { refreshMessages } = get();
219
+ const toolMessage = chatSelectors.getMessageById(id)(get());
220
+ if (!toolMessage || !toolMessage?.tool_call_id) return;
221
+
222
+ let assistantMessage = chatSelectors.getMessageById(toolMessage?.parentId || '')(get());
223
+
224
+ const prevArguments = toolMessage?.plugin?.arguments;
225
+ const prevJson = safeParseJSON(prevArguments || '');
226
+ const nextValue = merge(prevJson || {}, value);
227
+ if (isEqual(prevJson, nextValue)) return;
228
+
229
+ // optimistic update
230
+ get().internal_dispatchMessage({
231
+ id,
232
+ type: 'updateMessagePlugin',
233
+ value: { arguments: JSON.stringify(nextValue) },
234
+ });
235
+
236
+ // 同样需要更新 assistantMessage 的 pluginArguments
237
+ if (assistantMessage) {
238
+ get().internal_dispatchMessage({
239
+ id: assistantMessage.id,
240
+ type: 'updateMessageTools',
241
+ tool_call_id: toolMessage?.tool_call_id,
242
+ value: { arguments: JSON.stringify(nextValue) },
243
+ });
244
+ assistantMessage = chatSelectors.getMessageById(assistantMessage?.id)(get());
245
+ }
246
+
247
+ const updateAssistantMessage = async () => {
248
+ if (!assistantMessage) return;
249
+ await messageService.updateMessage(assistantMessage!.id, { tools: assistantMessage?.tools });
250
+ };
251
+
252
+ await Promise.all([
253
+ messageService.updateMessagePluginArguments(id, nextValue),
254
+ updateAssistantMessage(),
255
+ ]);
256
+
257
+ await refreshMessages();
258
+ },
259
+
210
260
  internal_callPluginApi: async (id, payload) => {
211
261
  const { internal_updateMessageContent, refreshMessages, internal_togglePluginApiCalling } =
212
262
  get();
@@ -314,7 +364,7 @@ export const chatPlugin: StateCreator<
314
364
  internal_updatePluginError: async (id, error) => {
315
365
  const { refreshMessages } = get();
316
366
 
317
- get().internal_dispatchMessage({ id, type: 'updateMessages', value: { error } });
367
+ get().internal_dispatchMessage({ id, type: 'updateMessage', value: { error } });
318
368
  await messageService.updateMessage(id, { pluginError: error });
319
369
  await refreshMessages();
320
370
  },
@@ -0,0 +1,109 @@
1
+ import { act, renderHook } from '@testing-library/react';
2
+ import { describe, expect, it, vi } from 'vitest';
3
+
4
+ import { useChatStore } from '@/store/chat';
5
+
6
+ vi.mock('zustand/traditional');
7
+
8
+ describe('chatDockSlice', () => {
9
+ describe('closeToolUI', () => {
10
+ it('should set dockToolMessage to undefined', () => {
11
+ const { result } = renderHook(() => useChatStore());
12
+
13
+ act(() => {
14
+ result.current.openToolUI('test-id', 'test-identifier');
15
+ });
16
+
17
+ expect(result.current.dockToolMessage).toEqual({
18
+ id: 'test-id',
19
+ identifier: 'test-identifier',
20
+ });
21
+
22
+ act(() => {
23
+ result.current.closeToolUI();
24
+ });
25
+
26
+ expect(result.current.dockToolMessage).toBeUndefined();
27
+ });
28
+ });
29
+
30
+ describe('openToolUI', () => {
31
+ it('should set dockToolMessage and open dock if it is closed', () => {
32
+ const { result } = renderHook(() => useChatStore());
33
+
34
+ expect(result.current.showDock).toBe(false);
35
+
36
+ act(() => {
37
+ result.current.openToolUI('test-id', 'test-identifier');
38
+ });
39
+
40
+ expect(result.current.dockToolMessage).toEqual({
41
+ id: 'test-id',
42
+ identifier: 'test-identifier',
43
+ });
44
+ expect(result.current.showDock).toBe(true);
45
+ });
46
+
47
+ it('should not change dock state if it is already open', () => {
48
+ const { result } = renderHook(() => useChatStore());
49
+
50
+ act(() => {
51
+ result.current.toggleDock(true);
52
+ });
53
+
54
+ expect(result.current.showDock).toBe(true);
55
+
56
+ act(() => {
57
+ result.current.openToolUI('test-id', 'test-identifier');
58
+ });
59
+
60
+ expect(result.current.dockToolMessage).toEqual({
61
+ id: 'test-id',
62
+ identifier: 'test-identifier',
63
+ });
64
+ expect(result.current.showDock).toBe(true);
65
+ });
66
+ });
67
+
68
+ describe('toggleDock', () => {
69
+ it('should toggle dock state when no argument is provided', () => {
70
+ const { result } = renderHook(() => useChatStore());
71
+
72
+ expect(result.current.showDock).toBe(false);
73
+
74
+ act(() => {
75
+ result.current.toggleDock();
76
+ });
77
+
78
+ expect(result.current.showDock).toBe(true);
79
+
80
+ act(() => {
81
+ result.current.toggleDock();
82
+ });
83
+
84
+ expect(result.current.showDock).toBe(false);
85
+ });
86
+
87
+ it('should set dock state to the provided value', () => {
88
+ const { result } = renderHook(() => useChatStore());
89
+
90
+ act(() => {
91
+ result.current.toggleDock(true);
92
+ });
93
+
94
+ expect(result.current.showDock).toBe(true);
95
+
96
+ act(() => {
97
+ result.current.toggleDock(false);
98
+ });
99
+
100
+ expect(result.current.showDock).toBe(false);
101
+
102
+ act(() => {
103
+ result.current.toggleDock(true);
104
+ });
105
+
106
+ expect(result.current.showDock).toBe(true);
107
+ });
108
+ });
109
+ });
@@ -0,0 +1,31 @@
1
+ import { StateCreator } from 'zustand/vanilla';
2
+
3
+ import { ChatStore } from '@/store/chat/store';
4
+
5
+ export interface ChatPortalAction {
6
+ closeToolUI: () => void;
7
+ openToolUI: (messageId: string, identifier: string) => void;
8
+ toggleDock: (open?: boolean) => void;
9
+ }
10
+
11
+ export const chatPortalSlice: StateCreator<
12
+ ChatStore,
13
+ [['zustand/devtools', never]],
14
+ [],
15
+ ChatPortalAction
16
+ > = (set, get) => ({
17
+ closeToolUI: () => {
18
+ set({ dockToolMessage: undefined }, false, 'openToolUI');
19
+ },
20
+ openToolUI: (id, identifier) => {
21
+ if (!get().showDock) {
22
+ get().toggleDock(true);
23
+ }
24
+
25
+ set({ dockToolMessage: { id, identifier } }, false, 'openToolUI');
26
+ },
27
+ toggleDock: (open) => {
28
+ const showInspector = open === undefined ? !get().showDock : open;
29
+ set({ showDock: showInspector }, false, 'toggleInspector');
30
+ },
31
+ });
@@ -0,0 +1,8 @@
1
+ export interface ChatPortalState {
2
+ dockToolMessage?: { id: string; identifier: string };
3
+ showDock: boolean;
4
+ }
5
+
6
+ export const initialChatPortalState: ChatPortalState = {
7
+ showDock: false,
8
+ };
@@ -0,0 +1,73 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import type { ChatStoreState } from '@/store/chat';
4
+
5
+ import { chatPortalSelectors } from './selectors';
6
+
7
+ describe('chatDockSelectors', () => {
8
+ const createState = (overrides?: Partial<ChatStoreState>) =>
9
+ ({
10
+ showDock: false,
11
+ dockToolMessage: undefined,
12
+ ...overrides,
13
+ }) as ChatStoreState;
14
+
15
+ describe('showDock', () => {
16
+ it('should return the showDock state', () => {
17
+ expect(chatPortalSelectors.showDock(createState({ showDock: true }))).toBe(true);
18
+ expect(chatPortalSelectors.showDock(createState({ showDock: false }))).toBe(false);
19
+ });
20
+ });
21
+
22
+ describe('toolUIMessageId', () => {
23
+ it('should return undefined when dockToolMessage is not set', () => {
24
+ expect(chatPortalSelectors.toolUIMessageId(createState())).toBeUndefined();
25
+ });
26
+
27
+ it('should return the id when dockToolMessage is set', () => {
28
+ const state = createState({ dockToolMessage: { id: 'test-id', identifier: 'test' } });
29
+ expect(chatPortalSelectors.toolUIMessageId(state)).toBe('test-id');
30
+ });
31
+ });
32
+
33
+ describe('isMessageToolUIOpen', () => {
34
+ it('should return false when id does not match or showDock is false', () => {
35
+ const state = createState({
36
+ dockToolMessage: { id: 'test-id', identifier: 'test' },
37
+ showDock: false,
38
+ });
39
+ expect(chatPortalSelectors.isMessageToolUIOpen('test-id')(state)).toBe(false);
40
+ expect(chatPortalSelectors.isMessageToolUIOpen('other-id')(state)).toBe(false);
41
+ });
42
+
43
+ it('should return true when id matches and showDock is true', () => {
44
+ const state = createState({
45
+ dockToolMessage: { id: 'test-id', identifier: 'test' },
46
+ showDock: true,
47
+ });
48
+ expect(chatPortalSelectors.isMessageToolUIOpen('test-id')(state)).toBe(true);
49
+ });
50
+ });
51
+
52
+ describe('showToolUI', () => {
53
+ it('should return false when dockToolMessage is not set', () => {
54
+ expect(chatPortalSelectors.showToolUI(createState())).toBe(false);
55
+ });
56
+
57
+ it('should return true when dockToolMessage is set', () => {
58
+ const state = createState({ dockToolMessage: { id: 'test-id', identifier: 'test' } });
59
+ expect(chatPortalSelectors.showToolUI(state)).toBe(true);
60
+ });
61
+ });
62
+
63
+ describe('toolUIIdentifier', () => {
64
+ it('should return undefined when dockToolMessage is not set', () => {
65
+ expect(chatPortalSelectors.toolUIIdentifier(createState())).toBeUndefined();
66
+ });
67
+
68
+ it('should return the identifier when dockToolMessage is set', () => {
69
+ const state = createState({ dockToolMessage: { id: 'test-id', identifier: 'test' } });
70
+ expect(chatPortalSelectors.toolUIIdentifier(state)).toBe('test');
71
+ });
72
+ });
73
+ });
@@ -0,0 +1,15 @@
1
+ import type { ChatStoreState } from '@/store/chat';
2
+
3
+ const toolUIMessageId = (s: ChatStoreState) => s.dockToolMessage?.id;
4
+ const showDock = (s: ChatStoreState) => s.showDock;
5
+
6
+ const isMessageToolUIOpen = (id: string) => (s: ChatStoreState) =>
7
+ toolUIMessageId(s) === id && showDock(s);
8
+
9
+ export const chatPortalSelectors = {
10
+ isMessageToolUIOpen,
11
+ showDock,
12
+ showToolUI: (state: ChatStoreState) => !!state.dockToolMessage,
13
+ toolUIIdentifier: (state: ChatStoreState) => state.dockToolMessage?.identifier,
14
+ toolUIMessageId,
15
+ };
@@ -1,3 +1,4 @@
1
+ // sort-imports-ignore
1
2
  import { subscribeWithSelector } from 'zustand/middleware';
2
3
  import { shallow } from 'zustand/shallow';
3
4
  import { createWithEqualityFn } from 'zustand/traditional';
@@ -6,6 +7,7 @@ import { StateCreator } from 'zustand/vanilla';
6
7
  import { createDevtools } from '../middleware/createDevtools';
7
8
  import { ChatStoreState, initialState } from './initialState';
8
9
  import { ChatBuiltinToolAction, chatToolSlice } from './slices/builtinTool/action';
10
+ import { ChatPortalAction, chatPortalSlice } from './slices/portal/action';
9
11
  import { ChatEnhanceAction, chatEnhance } from './slices/enchance/action';
10
12
  import { ChatMessageAction, chatMessage } from './slices/message/action';
11
13
  import { ChatPluginAction, chatPlugin } from './slices/plugin/action';
@@ -18,7 +20,8 @@ export interface ChatStoreAction
18
20
  ShareAction,
19
21
  ChatEnhanceAction,
20
22
  ChatPluginAction,
21
- ChatBuiltinToolAction {}
23
+ ChatBuiltinToolAction,
24
+ ChatPortalAction {}
22
25
 
23
26
  export type ChatStore = ChatStoreAction & ChatStoreState;
24
27
 
@@ -33,6 +36,9 @@ const createStore: StateCreator<ChatStore, [['zustand/devtools', never]]> = (...
33
36
  ...chatEnhance(...params),
34
37
  ...chatToolSlice(...params),
35
38
  ...chatPlugin(...params),
39
+ ...chatPortalSlice(...params),
40
+
41
+ // cloud
36
42
  });
37
43
 
38
44
  // =============== 实装 useStore ============ //