@lobehub/chat 0.25.0 → 0.26.1

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 (60) hide show
  1. package/.eslintrc.js +2 -1
  2. package/CHANGELOG.md +113 -6
  3. package/package.json +2 -5
  4. package/src/config/server.ts +22 -0
  5. package/src/const/fetch.ts +2 -2
  6. package/src/const/message.ts +2 -0
  7. package/src/locales/create.ts +3 -14
  8. package/src/locales/default/common.ts +4 -0
  9. package/src/locales/default/error.ts +27 -0
  10. package/src/locales/default/plugin.ts +19 -1
  11. package/src/locales/resources/zh_CN.ts +2 -0
  12. package/src/pages/api/auth.ts +25 -0
  13. package/src/pages/api/error.ts +21 -0
  14. package/src/pages/api/openai.api.ts +11 -5
  15. package/src/pages/api/openai.ts +24 -9
  16. package/src/pages/api/plugins.api.ts +11 -36
  17. package/src/pages/chat/[id]/Conversation/ChatList/Error/ApiKeyForm.tsx +40 -0
  18. package/src/pages/chat/[id]/Conversation/ChatList/Error/ErrorActionContainer.tsx +23 -0
  19. package/src/pages/chat/[id]/Conversation/ChatList/Error/InvalidAccess.tsx +76 -0
  20. package/src/pages/chat/[id]/Conversation/ChatList/Error/OpenAiBizError.tsx +52 -0
  21. package/src/pages/chat/[id]/Conversation/ChatList/Error/style.tsx +44 -0
  22. package/src/pages/chat/[id]/Conversation/{MessageExtra.tsx → ChatList/MessageExtra.tsx} +28 -26
  23. package/src/pages/chat/[id]/Conversation/ChatList/OTPInput.tsx +142 -0
  24. package/src/pages/chat/[id]/Conversation/ChatList/Plugins/FunctionCall.tsx +60 -0
  25. package/src/pages/chat/[id]/Conversation/ChatList/Plugins/PluginMessage.tsx +45 -0
  26. package/src/pages/chat/[id]/Conversation/ChatList/Plugins/PluginResultRender.tsx +51 -0
  27. package/src/pages/chat/[id]/Conversation/ChatList/Plugins/style.ts +25 -0
  28. package/src/pages/chat/[id]/Conversation/ChatList/index.tsx +112 -0
  29. package/src/pages/chat/[id]/Conversation/Input/Token.tsx +1 -1
  30. package/src/pages/chat/[id]/edit/AgentPlugin/index.tsx +2 -2
  31. package/src/pages/setting/Header.tsx +2 -2
  32. package/src/pages/setting/index.page.tsx +11 -28
  33. package/src/pages/setting/layout.tsx +35 -0
  34. package/src/plugins/Render.ts +8 -0
  35. package/src/plugins/index.ts +7 -3
  36. package/src/plugins/searchEngine/index.ts +4 -1
  37. package/src/plugins/searchEngine/runner.ts +5 -73
  38. package/src/plugins/searchEngine/type.ts +80 -0
  39. package/src/plugins/type.ts +17 -0
  40. package/src/plugins/weather/Render.tsx +53 -0
  41. package/src/plugins/weather/index.ts +9 -1
  42. package/src/plugins/weather/runner.ts +12 -7
  43. package/src/plugins/weather/type.ts +36 -0
  44. package/src/plugins/webCrawler/index.ts +4 -1
  45. package/src/plugins/webCrawler/runner.ts +3 -27
  46. package/src/plugins/webCrawler/type.ts +32 -0
  47. package/src/services/chatModel.ts +3 -3
  48. package/src/services/plugin.ts +30 -0
  49. package/src/store/session/slices/chat/actions/message.ts +61 -42
  50. package/src/store/session/slices/chat/initialState.ts +2 -4
  51. package/src/store/session/slices/chat/selectors/utils.test.ts +3 -2
  52. package/src/store/session/slices/chat/selectors/utils.ts +4 -3
  53. package/src/types/chatMessage.ts +9 -6
  54. package/src/types/fetch.ts +26 -0
  55. package/src/types/openai.ts +3 -0
  56. package/src/types/plugin.ts +4 -0
  57. package/src/utils/fetch.ts +21 -25
  58. package/src/utils/message.ts +16 -0
  59. package/src/pages/chat/[id]/Conversation/ChatList.tsx +0 -69
  60. package/src/pages/chat/[id]/Conversation/FunctionMessage.tsx +0 -31
@@ -1,8 +1,9 @@
1
1
  import { StateCreator } from 'zustand/vanilla';
2
2
 
3
3
  import { fetchChatModel } from '@/services/chatModel';
4
+ import { fetchPlugin } from '@/services/plugin';
4
5
  import { SessionStore, agentSelectors, chatSelectors, sessionSelectors } from '@/store/session';
5
- import { ChatMessage } from '@/types/chatMessage';
6
+ import { ChatMessage, OpenAIFunctionCall } from '@/types/chatMessage';
6
7
  import { fetchSSE } from '@/utils/fetch';
7
8
  import { isFunctionMessage } from '@/utils/message';
8
9
  import { nanoid } from '@/utils/uuid';
@@ -42,8 +43,7 @@ export interface ChatMessageAction {
42
43
  generateMessage: (
43
44
  messages: ChatMessage[],
44
45
  assistantMessageId: string,
45
- withPlugin?: boolean,
46
- ) => Promise<{ isFunctionCall: boolean; output: string }>;
46
+ ) => Promise<{ isFunctionCall: boolean }>;
47
47
 
48
48
  /**
49
49
  * 实际获取 AI 响应
@@ -63,6 +63,7 @@ export interface ChatMessageAction {
63
63
  * @param text - 消息文本
64
64
  */
65
65
  sendMessage: (text: string) => Promise<void>;
66
+ triggerFunctionCall: (id: string) => Promise<void>;
66
67
  }
67
68
 
68
69
  export const chatMessage: StateCreator<
@@ -108,16 +109,13 @@ export const chatMessage: StateCreator<
108
109
  get().dispatchSession({ chats, id: activeId, type: 'updateSessionChat' });
109
110
  },
110
111
 
111
- generateMessage: async (messages, assistantId, withPlugin) => {
112
+ generateMessage: async (messages, assistantId) => {
112
113
  const { dispatchMessage } = get();
113
- set({ chatLoading: true });
114
+ set({ chatLoadingId: assistantId });
114
115
  const config = agentSelectors.currentAgentConfigSafe(get());
115
116
 
116
117
  const fetcher = () =>
117
- fetchChatModel(
118
- { messages, model: config.model, ...config.params, plugins: config.plugins },
119
- { withPlugin },
120
- );
118
+ fetchChatModel({ messages, model: config.model, ...config.params, plugins: config.plugins });
121
119
 
122
120
  let output = '';
123
121
  let isFunctionCall = false;
@@ -139,13 +137,6 @@ export const chatMessage: StateCreator<
139
137
  // 如果是 function call
140
138
  if (isFunctionMessage(output)) {
141
139
  isFunctionCall = true;
142
- // 设为 function
143
- dispatchMessage({
144
- id: assistantId,
145
- key: 'role',
146
- type: 'updateMessage',
147
- value: 'function',
148
- });
149
140
  }
150
141
 
151
142
  // 滚动到最后一条消息
@@ -156,13 +147,13 @@ export const chatMessage: StateCreator<
156
147
  },
157
148
  });
158
149
 
159
- set({ chatLoading: false });
150
+ set({ chatLoadingId: undefined });
160
151
 
161
- return { isFunctionCall, output };
152
+ return { isFunctionCall };
162
153
  },
163
154
 
164
155
  realFetchAIResponse: async (messages, userMessageId) => {
165
- const { dispatchMessage, generateMessage, activeTopicId } = get();
156
+ const { dispatchMessage, generateMessage, triggerFunctionCall, activeTopicId } = get();
166
157
 
167
158
  // 添加 systemRole
168
159
  const { systemRole, model } = agentSelectors.currentAgentConfigSafe(get());
@@ -191,31 +182,11 @@ export const chatMessage: StateCreator<
191
182
  dispatchMessage({ id: mid, key: 'fromModel', type: 'updateMessageExtra', value: model });
192
183
 
193
184
  // 生成 ai message
194
- const { output, isFunctionCall } = await generateMessage(messages, mid);
185
+ const { isFunctionCall } = await generateMessage(messages, mid);
195
186
 
196
187
  // 如果是 function,则发送函数调用方法
197
188
  if (isFunctionCall) {
198
- const { function_call } = JSON.parse(output);
199
-
200
- dispatchMessage({
201
- id: mid,
202
- key: 'function_call',
203
- type: 'updateMessage',
204
- value: function_call,
205
- });
206
-
207
- await generateMessage(
208
- [...messages, { content: '', function_call, id: mid, role: 'assistant' } as ChatMessage],
209
- mid,
210
- true,
211
- );
212
-
213
- dispatchMessage({
214
- id: mid,
215
- key: 'role',
216
- type: 'updateMessage',
217
- value: 'assistant',
218
- });
189
+ triggerFunctionCall(mid);
219
190
  }
220
191
  },
221
192
 
@@ -245,7 +216,6 @@ export const chatMessage: StateCreator<
245
216
 
246
217
  await realFetchAIResponse(histories, latestMsg.id);
247
218
  },
248
-
249
219
  sendMessage: async (message) => {
250
220
  const { dispatchMessage, realFetchAIResponse, autocompleteSessionAgentMeta, activeTopicId } =
251
221
  get();
@@ -270,4 +240,53 @@ export const chatMessage: StateCreator<
270
240
  autocompleteSessionAgentMeta(session.id);
271
241
  }
272
242
  },
243
+
244
+ triggerFunctionCall: async (id) => {
245
+ const { dispatchMessage, generateMessage } = get();
246
+ const session = sessionSelectors.currentSession(get());
247
+
248
+ if (!session) return;
249
+
250
+ const message = session.chats[id];
251
+ if (!message) return;
252
+
253
+ let payload: OpenAIFunctionCall = { name: '' };
254
+ if (message.content) {
255
+ const { function_call } = JSON.parse(message.content);
256
+ dispatchMessage({ id, key: 'function_call', type: 'updateMessage', value: function_call });
257
+ dispatchMessage({ id, key: 'content', type: 'updateMessage', value: '' });
258
+ payload = function_call;
259
+ } else {
260
+ if (message.function_call) {
261
+ payload = message.function_call;
262
+ }
263
+ }
264
+
265
+ if (!payload.name) return;
266
+
267
+ // const fid = nanoid();
268
+ dispatchMessage({ id, key: 'role', type: 'updateMessage', value: 'function' });
269
+ dispatchMessage({ id, key: 'name', type: 'updateMessage', value: payload.name });
270
+ dispatchMessage({ id, key: 'function_call', type: 'updateMessage', value: payload });
271
+
272
+ // dispatchMessage({
273
+ // id: id,
274
+ // message: FUNCTION_LOADING,
275
+ // parentId: message.,
276
+ // role: 'function',
277
+ // type: 'addMessage',
278
+ // });
279
+
280
+ const data = await fetchPlugin(payload);
281
+
282
+ dispatchMessage({ id, key: 'content', type: 'updateMessage', value: data });
283
+
284
+ const mid = nanoid();
285
+
286
+ dispatchMessage({ id: mid, message: LOADING_FLAT, role: 'assistant', type: 'addMessage' });
287
+
288
+ const chats = chatSelectors.currentChats(get());
289
+
290
+ await generateMessage(chats, mid);
291
+ },
273
292
  });
@@ -1,9 +1,7 @@
1
1
  export interface ChatState {
2
2
  activeTopicId?: string;
3
- chatLoading: boolean;
3
+ chatLoadingId?: string;
4
4
  topicLoadingId?: string;
5
5
  }
6
6
 
7
- export const initialChatState: ChatState = {
8
- chatLoading: false,
9
- };
7
+ export const initialChatState: ChatState = {};
@@ -295,7 +295,7 @@ describe('organizeChats', () => {
295
295
  const result = organizeChats(session, avatar);
296
296
  const meta = result[0].meta;
297
297
 
298
- expect(meta.avatar).toBe(avatar.assistant);
298
+ expect(meta.avatar).toBe('assistant-avatar');
299
299
  expect(meta.title).toBeUndefined();
300
300
  });
301
301
 
@@ -329,6 +329,7 @@ describe('organizeChats', () => {
329
329
  function_call: {
330
330
  name: 'realtimeWeather',
331
331
  },
332
+ name: 'realtimeWeather',
332
333
  } as ChatMessage;
333
334
 
334
335
  session.chats[message.id] = message;
@@ -336,7 +337,7 @@ describe('organizeChats', () => {
336
337
  const result = organizeChats(session, avatar);
337
338
  const meta = result[3].meta;
338
339
 
339
- expect(meta.avatar).toBe('☂️');
340
+ expect(meta.avatar).toBe('🧩');
340
341
  expect(meta.title).toBe('realtimeWeather');
341
342
  });
342
343
  });
@@ -1,4 +1,4 @@
1
- import pluginList from '@/plugins';
1
+ import { PluginsMap } from '@/plugins';
2
2
  import { ChatMessage } from '@/types/chatMessage';
3
3
  import { LobeAgentSession } from '@/types/session';
4
4
 
@@ -27,9 +27,10 @@ export const organizeChats = (
27
27
  }
28
28
 
29
29
  case 'function': {
30
- const plugin = pluginList.find((p) => p.name === message.function_call?.name);
30
+ const plugin = PluginsMap[message.name || ''];
31
+
31
32
  return {
32
- avatar: plugin?.avatar || '🧩',
33
+ avatar: '🧩',
33
34
  title: plugin?.name || 'plugin-unknown',
34
35
  };
35
36
  }
@@ -1,3 +1,5 @@
1
+ import { ErrorType } from '@/types/fetch';
2
+
1
3
  import { LLMRoleType } from './llm';
2
4
  import { BaseDataModel } from './meta';
3
5
 
@@ -5,12 +7,13 @@ import { BaseDataModel } from './meta';
5
7
  * 聊天消息错误对象
6
8
  */
7
9
  export interface ChatMessageError {
8
- /**
9
- * 错误信息
10
- */
10
+ body?: any;
11
11
  message: string;
12
- status: number;
13
- type: 'general' | 'llm';
12
+ type: ErrorType;
13
+ }
14
+ export interface OpenAIFunctionCall {
15
+ arguments?: string;
16
+ name: string;
14
17
  }
15
18
 
16
19
  export interface ChatMessage extends BaseDataModel {
@@ -30,7 +33,7 @@ export interface ChatMessage extends BaseDataModel {
30
33
  };
31
34
  } & Record<string, any>;
32
35
 
33
- function_call?: { arguments?: string; name: string };
36
+ function_call?: OpenAIFunctionCall;
34
37
  name?: string;
35
38
 
36
39
  parentId?: string;
@@ -0,0 +1,26 @@
1
+ export enum ErrorType {
2
+ // ******* 业务错误语义 ******* //
3
+
4
+ // 密码无效
5
+ InvalidAccessCode = 'InvalidAccessCode',
6
+ // OpenAI 返回的业务错误
7
+ OpenAIBizError = 'OpenAIBizError',
8
+
9
+ // ******* 客户端错误 ******* //
10
+ BadRequest = 400,
11
+ Unauthorized = 401,
12
+ Forbidden = 403,
13
+ ContentNotFound = 404, // 没找到接口
14
+ TooManyRequests = 429,
15
+
16
+ // ******* 服务端错误 ******* //
17
+ InternalServerError = 500,
18
+ BadGateway = 502,
19
+ ServiceUnavailable = 503,
20
+ GatewayTimeout = 504,
21
+ }
22
+
23
+ export interface ErrorResponse {
24
+ body: any;
25
+ errorType: ErrorType;
26
+ }
@@ -1,3 +1,4 @@
1
+ import { OpenAIFunctionCall } from '@/types/chatMessage';
1
2
  import { LLMRoleType } from '@/types/llm';
2
3
 
3
4
  export interface OpenAIChatMessage {
@@ -7,6 +8,8 @@ export interface OpenAIChatMessage {
7
8
  */
8
9
  content: string;
9
10
 
11
+ function_call?: OpenAIFunctionCall;
12
+ name?: string;
10
13
  /**
11
14
  * 角色
12
15
  * @description 消息发送者的角色
@@ -0,0 +1,4 @@
1
+ export interface OpenAIPluginPayload {
2
+ arguments: string;
3
+ name: string;
4
+ }
@@ -1,24 +1,8 @@
1
- // import { notification } from '@/layout';
1
+ import { t } from 'i18next';
2
+
2
3
  import { fetchChatModel } from '@/services/chatModel';
3
4
  import { ChatMessageError } from '@/types/chatMessage';
4
-
5
- const codeMessage: Record<number, string> = {
6
- 200: '成功获取数据,服务已响应',
7
- 201: '操作成功,数据已保存',
8
- 202: '您的请求已进入后台排队,请耐心等待异步任务完成',
9
- 204: '数据已成功删除',
10
- 400: '很抱歉,您的请求出错,服务器未执行任何数据的创建或修改操作',
11
- 401: '很抱歉,您的权限不足。请确认用户名或密码是否正确',
12
- 403: '很抱歉,您无权访问此内容',
13
- 404: '很抱歉,您请求的记录不存在,服务器未能执行任何操作',
14
- 406: '很抱歉,服务器不支持该请求格式',
15
- 410: '很抱歉,你所请求的资源已永久删除',
16
- 422: '很抱歉,在创建对象时遇到验证错误,请稍后再试',
17
- 500: '很抱歉,服务器出现了问题,请稍后再试',
18
- 502: '很抱歉,您遇到了网关错误。这可能是由于网络故障或服务器问题导致的。请稍后再试,或联系管理员以获取更多帮助',
19
- 503: '很抱歉,我们的服务器过载或处在维护中,服务暂时不可用',
20
- 504: '很抱歉,网关请求超时,请稍后再试',
21
- };
5
+ import { ErrorResponse } from '@/types/fetch';
22
6
 
23
7
  export interface FetchSSEOptions {
24
8
  onErrorHandle?: (error: ChatMessageError) => void;
@@ -33,13 +17,25 @@ export interface FetchSSEOptions {
33
17
  export const fetchSSE = async (fetchFn: () => Promise<Response>, options: FetchSSEOptions = {}) => {
34
18
  const response = await fetchFn();
35
19
 
36
- // 如果不 ok 说明有连接请求错误
20
+ // 如果不 ok 说明有请求错误
37
21
  if (!response.ok) {
38
- const chatMessageError: ChatMessageError = {
39
- message: codeMessage[response.status],
40
- status: response.status,
41
- type: 'general',
42
- };
22
+ let chatMessageError: ChatMessageError;
23
+
24
+ // 尝试取一波业务错误语义
25
+ try {
26
+ const data = (await response.json()) as ErrorResponse;
27
+ chatMessageError = {
28
+ body: data.body,
29
+ message: t(`response.${data.errorType}`),
30
+ type: data.errorType,
31
+ };
32
+ } catch {
33
+ // 如果无法正常返回,说明是常规报错
34
+ chatMessageError = {
35
+ message: t(`response.${response.status}`),
36
+ type: response.status,
37
+ };
38
+ }
43
39
 
44
40
  options.onErrorHandle?.(chatMessageError);
45
41
  return;
@@ -3,3 +3,19 @@ import { FUNCTION_MESSAGE_FLAG } from '@/const/message';
3
3
  export const isFunctionMessage = (content: string) => {
4
4
  return content.startsWith(FUNCTION_MESSAGE_FLAG);
5
5
  };
6
+
7
+ // export const createFunctionCallMessage = () => {
8
+ // return [
9
+ // {
10
+ // content: '',
11
+ // function_call: { arguments: '{\n "city": "杭州"\n}', name: 'realtimeWeather' },
12
+ // role: 'assistant',
13
+ // },
14
+ // {
15
+ // content:
16
+ // '{"status":"1","count":"1","info":"OK","infocode":"10000","forecasts":[{"city":"杭州市","adcode":"300","province":"浙江","reporttime":"2023-07-28 20:32:48","casts":[{"date":"2023-07-28","week":"5","dayweather"小雨","nightweather":"小雨-中雨","daytemp":"32","nighttemp":"26","daywind":"东","nightwind":"东","daypower":"6power":"6","daytemp_float":"32.0","nighttemp_float":"26.0"},{"date":"2023-07-29","week":"6","dayweather":"小雨雨","nightweather":"小雨","daytemp":"30","nighttemp":"25","daywind":"东南","nightwind":"东南","daypower":"4","wer":"4","daytemp_float":"30.0","nighttemp_float":"25.0"},{"date":"2023-07-30","week":"7","dayweather":"小雨",ightweather":"雷阵雨","daytemp":"31","nighttemp":"25","daywind":"东南","nightwind":"东南","daypower":"4","nigh:"4","daytemp_float":"31.0","nighttemp_float":"25.0"},{"date":"2023-07-31","week":"1","dayweather":"雷阵雨","ntweather":"雷阵雨","daytemp":"33","nighttemp":"25","daywind":"东","nightwind":"东","daypower":"4","nightpower""daytemp_float":"33.0","nighttemp_float":"25.0"}]}]}',
17
+ // name: 'realtimeWeather',
18
+ // role: 'function',
19
+ // },
20
+ // ];
21
+ // };
@@ -1,69 +0,0 @@
1
- import { ChatList, ChatMessage } from '@lobehub/ui';
2
- import isEqual from 'fast-deep-equal';
3
- import { ReactNode, memo } from 'react';
4
- import { useTranslation } from 'react-i18next';
5
- import { shallow } from 'zustand/shallow';
6
-
7
- import { agentSelectors, chatSelectors, useSessionStore } from '@/store/session';
8
- import { isFunctionMessage } from '@/utils/message';
9
-
10
- import FunctionMessage from './FunctionMessage';
11
- import MessageExtra from './MessageExtra';
12
-
13
- const List = () => {
14
- const { t } = useTranslation('common');
15
- const data = useSessionStore(chatSelectors.currentChats, isEqual);
16
- const [displayMode, deleteMessage, resendMessage, dispatchMessage] = useSessionStore(
17
- (s) => [
18
- agentSelectors.currentAgentConfigSafe(s).displayMode,
19
- s.deleteMessage,
20
- s.resendMessage,
21
- s.dispatchMessage,
22
- ],
23
- shallow,
24
- );
25
-
26
- const renderMessage = (content: ReactNode, message: ChatMessage) => {
27
- if (message.role === 'function')
28
- return isFunctionMessage(message.content) ? <FunctionMessage /> : content;
29
-
30
- return content;
31
- };
32
-
33
- return (
34
- <ChatList
35
- data={data}
36
- onActionClick={(key, id) => {
37
- switch (key) {
38
- case 'delete': {
39
- deleteMessage(id);
40
- break;
41
- }
42
-
43
- case 'regenerate': {
44
- resendMessage(id);
45
- break;
46
- }
47
- }
48
- }}
49
- onMessageChange={(id, content) => {
50
- dispatchMessage({ id, key: 'content', type: 'updateMessage', value: content });
51
- }}
52
- renderMessage={renderMessage}
53
- renderMessageExtra={MessageExtra}
54
- style={{ marginTop: 24 }}
55
- text={{
56
- cancel: t('cancel'),
57
- confirm: t('ok'),
58
- copy: t('copy'),
59
- copySuccess: t('copySuccess'),
60
- delete: t('delete'),
61
- edit: t('edit'),
62
- regenerate: t('regenerate'),
63
- }}
64
- type={displayMode}
65
- />
66
- );
67
- };
68
-
69
- export default memo(List);
@@ -1,31 +0,0 @@
1
- import { LoadingOutlined } from '@ant-design/icons';
2
- import { createStyles } from 'antd-style';
3
- import { memo } from 'react';
4
- import { useTranslation } from 'react-i18next';
5
- import { Flexbox } from 'react-layout-kit';
6
-
7
- const useStyles = createStyles(({ css, token }) => ({
8
- container: css`
9
- padding: 4px 8px;
10
-
11
- color: ${token.colorTextSecondary};
12
-
13
- background: ${token.colorFillTertiary};
14
- border: 1px solid ${token.colorBorder};
15
- border-radius: 6px;
16
- `,
17
- }));
18
- const FunctionMessage = memo(() => {
19
- const { t } = useTranslation('plugin');
20
- const { styles } = useStyles();
21
- return (
22
- <Flexbox className={styles.container} gap={8} horizontal>
23
- <div>
24
- <LoadingOutlined />
25
- </div>
26
- {t('pluginLoading')}
27
- </Flexbox>
28
- );
29
- });
30
-
31
- export default FunctionMessage;