@lobehub/chat 0.25.0 → 0.26.0

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 (31) hide show
  1. package/.eslintrc.js +2 -1
  2. package/CHANGELOG.md +88 -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/locales/create.ts +3 -14
  7. package/src/locales/default/common.ts +1 -0
  8. package/src/locales/default/error.ts +27 -0
  9. package/src/locales/resources/zh_CN.ts +2 -0
  10. package/src/pages/api/auth.ts +25 -0
  11. package/src/pages/api/error.ts +21 -0
  12. package/src/pages/api/openai.api.ts +11 -5
  13. package/src/pages/api/openai.ts +17 -5
  14. package/src/pages/api/plugins.api.ts +1 -4
  15. package/src/pages/chat/[id]/Conversation/ChatList/Error/ApiKeyForm.tsx +40 -0
  16. package/src/pages/chat/[id]/Conversation/ChatList/Error/ErrorActionContainer.tsx +23 -0
  17. package/src/pages/chat/[id]/Conversation/ChatList/Error/InvalidAccess.tsx +76 -0
  18. package/src/pages/chat/[id]/Conversation/ChatList/Error/OpenAiBizError.tsx +52 -0
  19. package/src/pages/chat/[id]/Conversation/ChatList/Error/style.tsx +44 -0
  20. package/src/pages/chat/[id]/Conversation/ChatList/OTPInput.tsx +142 -0
  21. package/src/pages/chat/[id]/Conversation/{ChatList.tsx → ChatList/index.tsx} +36 -18
  22. package/src/pages/chat/[id]/Conversation/Input/Token.tsx +1 -1
  23. package/src/pages/setting/Header.tsx +2 -2
  24. package/src/services/chatModel.ts +2 -1
  25. package/src/store/session/slices/chat/actions/message.ts +2 -2
  26. package/src/store/session/slices/chat/initialState.ts +2 -4
  27. package/src/types/chatMessage.ts +4 -5
  28. package/src/types/fetch.ts +26 -0
  29. package/src/utils/fetch.ts +21 -25
  30. /package/src/pages/chat/[id]/Conversation/{FunctionMessage.tsx → ChatList/FunctionMessage.tsx} +0 -0
  31. /package/src/pages/chat/[id]/Conversation/{MessageExtra.tsx → ChatList/MessageExtra.tsx} +0 -0
package/.eslintrc.js CHANGED
@@ -1,7 +1,8 @@
1
1
  const config = require('@lobehub/lint').eslint;
2
2
 
3
3
  config.extends.push('plugin:@next/next/recommended');
4
- //config.extends.push('plugin:@next/next/core-web-vitals');
5
4
 
6
5
  config.rules['unicorn/no-negated-condition'] = 0;
6
+ config.rules['unicorn/prefer-type-error'] = 0;
7
+
7
8
  module.exports = config;
package/CHANGELOG.md CHANGED
@@ -2,24 +2,106 @@
2
2
 
3
3
  # Changelog
4
4
 
5
- # [0.25.0](https://github.com/lobehub/lobe-chat/compare/v0.24.0...v0.25.0) (2023-07-26)
5
+ ## [Version 0.26.0](https://github.com/lobehub/lobe-chat/compare/v0.25.0...v0.26.0)
6
6
 
7
- ### ✨ Features
7
+ <sup>Released on **2023-07-28**</sup>
8
+
9
+ #### ✨ Features
10
+
11
+ - **misc**: support password auth and error.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### What's improved
19
+
20
+ - **misc**: support password auth and error, closes [#22](https://github.com/lobehub/lobe-chat/issues/22) ([67f1f4d](https://github.com/lobehub/lobe-chat/commit/67f1f4d))
21
+
22
+ </details>
23
+
24
+ <div align="right">
25
+
26
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
27
+
28
+ </div>
29
+
30
+ ## [Version 0.25.0](https://github.com/lobehub/lobe-chat/compare/v0.24.0...v0.25.0)
31
+
32
+ <sup>Released on **2023-07-26**</sup>
33
+
34
+ #### ✨ Features
35
+
36
+ - **sidebar**: Add import functionality and set labels and onClick functions
37
+
38
+ <br/>
39
+
40
+ <details>
41
+ <summary><kbd>Improvements and Fixes</kbd></summary>
42
+
43
+ #### ✨ Features
8
44
 
9
45
  - **sidebar**: Add import functionality and set labels and onClick functions ([03ea9bd](https://github.com/lobehub/lobe-chat/commit/03ea9bd))
10
46
 
11
- # [0.24.0](https://github.com/lobehub/lobe-chat/compare/v0.23.0...v0.24.0) (2023-07-26)
47
+ </details>
48
+
49
+ <div align="right">
50
+
51
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
52
+
53
+ </div>
54
+
55
+ ## [Version 0.24.0](https://github.com/lobehub/lobe-chat/compare/v0.23.0...v0.24.0)
56
+
57
+ <sup>Released on **2023-07-26**</sup>
58
+
59
+ #### ✨ Features
60
+
61
+ - **misc**: Add new translations, update existing translations, add functionality to components, modify styling, and adjust placeholder text
62
+
63
+ <br/>
64
+
65
+ <details>
66
+ <summary><kbd>Improvements and Fixes</kbd></summary>
12
67
 
13
- ### ✨ Features
68
+ #### ✨ Features
14
69
 
15
70
  - Add new translations, update existing translations, add functionality to components, modify styling, and adjust placeholder text ([da4ae72](https://github.com/lobehub/lobe-chat/commit/da4ae72))
16
71
 
17
- # [0.23.0](https://github.com/lobehub/lobe-chat/compare/v0.22.2...v0.23.0) (2023-07-26)
72
+ </details>
73
+
74
+ <div align="right">
75
+
76
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
18
77
 
19
- ### ✨ Features
78
+ </div>
79
+
80
+ ## [Version 0.23.0](https://github.com/lobehub/lobe-chat/compare/v0.22.2...v0.23.0)
81
+
82
+ <sup>Released on **2023-07-26**</sup>
83
+
84
+ #### ✨ Features
85
+
86
+ - **misc**: Add new features, update URLs, customize appearance, and implement components
87
+
88
+ <br/>
89
+
90
+ <details>
91
+ <summary><kbd>Improvements and Fixes</kbd></summary>
92
+
93
+ #### ✨ Features
20
94
 
21
95
  - Add new features, update URLs, customize appearance, and implement components ([4b61bf4](https://github.com/lobehub/lobe-chat/commit/4b61bf4))
22
96
 
97
+ </details>
98
+
99
+ <div align="right">
100
+
101
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
102
+
103
+ </div>
104
+
23
105
  ### [Version 0.22.2](https://github.com/lobehub/lobe-chat/compare/v0.22.1...v0.22.2)
24
106
 
25
107
  <sup>Released on **2023-07-26**</sup>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "0.25.0",
3
+ "version": "0.26.0",
4
4
  "description": "Lobe Chat is an open-source chatbot client using LangChain, Typescript and Next.js",
5
5
  "keywords": [
6
6
  "chatbot",
@@ -122,12 +122,10 @@
122
122
  "lint-staged": "^13",
123
123
  "next-pwa": "^5",
124
124
  "node-fetch": "^3",
125
- "postcss-styled-syntax": "^0.4",
126
125
  "prettier": "^2",
127
126
  "remark": "^14",
128
127
  "remark-cli": "^11",
129
128
  "semantic-release": "^21",
130
- "semantic-release-config-gitmoji": "^1",
131
129
  "stylelint": "^15",
132
130
  "ts-node": "^10",
133
131
  "typescript": "^5",
@@ -142,6 +140,5 @@
142
140
  "publishConfig": {
143
141
  "access": "public",
144
142
  "registry": "https://registry.npmjs.org"
145
- },
146
- "changelog": "https://github.com/lobehub/lobe-chat"
143
+ }
147
144
  }
@@ -0,0 +1,22 @@
1
+ declare global {
2
+ // eslint-disable-next-line @typescript-eslint/no-namespace
3
+ namespace NodeJS {
4
+ interface ProcessEnv {
5
+ ACCESS_CODE?: string;
6
+ OPENAI_API_KEY?: string;
7
+ OPENAI_PROXY_URL?: string;
8
+ }
9
+ }
10
+ }
11
+
12
+ export const getServerConfig = () => {
13
+ if (typeof process === 'undefined') {
14
+ throw new Error('[Server Config] you are importing a nodejs-only module outside of nodejs');
15
+ }
16
+
17
+ return {
18
+ ACCESS_CODE: process.env.ACCESS_CODE,
19
+ OPENAI_API_KEY: process.env.OPENAI_API_KEY,
20
+ OPENAI_PROXY_URL: process.env.OPENAI_PROXY_URL,
21
+ };
22
+ };
@@ -1,3 +1,3 @@
1
- export const OPENAI_SERVICE_ERROR_CODE = 555;
2
-
3
1
  export const OPENAI_API_KEY_HEADER_KEY = 'X-OPENAI-API-KEY';
2
+
3
+ export const LOBE_CHAT_ACCESS_CODE = 'X-LOBE_CHAT_ACCESS_CODE';
@@ -7,23 +7,12 @@ import type { Namespaces } from '@/types/locale';
7
7
 
8
8
  import resources from './resources';
9
9
 
10
- // const getRes = (res: Resources, namespace: Namespaces[]) => {
11
- // const newRes: any = {};
12
- // for (const [locale, value] of Object.entries(res)) {
13
- // newRes[locale] = {};
14
- // for (const ns of namespace) {
15
- // newRes[locale][ns] = value[ns];
16
- // }
17
- // }
18
- // return newRes;
19
- // };
20
-
21
10
  export const createI18nNext = (namespace?: Namespaces[] | Namespaces) => {
22
11
  const ns: Namespaces[] = namespace
23
12
  ? isArray(namespace)
24
- ? ['common', ...namespace]
25
- : ['common', namespace]
26
- : ['common'];
13
+ ? ['error', 'common', ...namespace]
14
+ : ['error', 'common', namespace]
15
+ : ['error', 'common'];
27
16
  return (
28
17
  i18n
29
18
  // detect user language
@@ -30,6 +30,7 @@ export default {
30
30
  },
31
31
  feedback: '反馈与建议',
32
32
  import: '导入配置',
33
+ moreSetting: '更多设置...',
33
34
  newAgent: '新建助手',
34
35
  noDescription: '暂无描述',
35
36
  ok: '确定',
@@ -0,0 +1,27 @@
1
+ export default {
2
+ response: {
3
+ 400: '很抱歉,服务器不明白您的请求,请确认您的请求参数是否正确',
4
+ 401: '很抱歉,服务器拒绝了您的请求,可能是因为您的权限不足或未提供有效的身份验证',
5
+ 403: '很抱歉,服务器拒绝了您的请求,您没有访问此内容的权限 ',
6
+ 404: '很抱歉,服务器找不到您请求的页面或资源,请确认您的 URL 是否正确',
7
+ 429: '很抱歉,您的请求太多,服务器有点累了,请稍后再试',
8
+ 500: '很抱歉,服务器似乎遇到了一些困难,暂时无法完成您的请求,请稍后再试',
9
+ 502: '很抱歉,服务器似乎迷失了方向,暂时无法提供服务,请稍后再试',
10
+ 503: '很抱歉,服务器当前无法处理您的请求,可能是由于过载或正在进行维护,请稍后再试',
11
+ 504: '很抱歉,服务器没有等到上游服务器的回应,请稍后再试',
12
+
13
+ InvalidAccessCode: '密码不正确或为空,请输入正确的访问密码,或者添加自定义 OpenAI API Key',
14
+ OpenAIBizError: '请求 OpenAI 服务出错,请根据以下信息排查或重试',
15
+ },
16
+ unlock: {
17
+ apikey: {
18
+ description: '输入你的 OpenAI API Key 即可绕过密码验证。应用不会记录你的 API Key',
19
+ title: '使用自定义 API Key',
20
+ },
21
+ confirm: '确认并重试',
22
+ password: {
23
+ description: '管理员已开启应用加密,输入应用密码后即可解锁应用。密码只需填写一次',
24
+ title: '输入密码解锁应用',
25
+ },
26
+ },
27
+ };
@@ -1,9 +1,11 @@
1
1
  import common from '../default/common';
2
+ import error from '../default/error';
2
3
  import plugin from '../default/plugin';
3
4
  import setting from '../default/setting';
4
5
 
5
6
  const resources = {
6
7
  common,
8
+ error,
7
9
  plugin,
8
10
  setting,
9
11
  } as const;
@@ -0,0 +1,25 @@
1
+ import { getServerConfig } from '@/config/server';
2
+ import { ErrorType } from '@/types/fetch';
3
+
4
+ interface AuthConfig {
5
+ accessCode?: string | null;
6
+ apiKey?: string | null;
7
+ }
8
+
9
+ export const checkAuth = ({ apiKey, accessCode }: AuthConfig) => {
10
+ const { ACCESS_CODE } = getServerConfig();
11
+
12
+ // 如果存在 apiKey
13
+ if (apiKey) {
14
+ return { auth: true };
15
+ }
16
+
17
+ // 如果不存在,则检查 accessCode
18
+ if (!ACCESS_CODE) return { auth: true };
19
+
20
+ if (accessCode !== ACCESS_CODE) {
21
+ return { auth: false, error: ErrorType.InvalidAccessCode };
22
+ }
23
+
24
+ return { auth: true };
25
+ };
@@ -0,0 +1,21 @@
1
+ import { ErrorResponse, ErrorType } from '@/types/fetch';
2
+
3
+ const getStatus = (errorType: ErrorType) => {
4
+ switch (errorType) {
5
+ case ErrorType.InvalidAccessCode: {
6
+ return 401;
7
+ }
8
+
9
+ case ErrorType.OpenAIBizError: {
10
+ return 577;
11
+ }
12
+ }
13
+ };
14
+
15
+ export const createErrorResponse = (errorType: ErrorType, body?: any) => {
16
+ const statusCode = getStatus(errorType);
17
+
18
+ const data: ErrorResponse = { body, errorType };
19
+
20
+ return new Response(JSON.stringify(data), { status: statusCode });
21
+ };
@@ -1,8 +1,9 @@
1
- import { StreamingTextResponse } from 'ai';
2
-
3
- import { OPENAI_API_KEY_HEADER_KEY } from '@/const/fetch';
1
+ import { LOBE_CHAT_ACCESS_CODE, OPENAI_API_KEY_HEADER_KEY } from '@/const/fetch';
2
+ import { ErrorType } from '@/types/fetch';
4
3
  import { OpenAIStreamPayload } from '@/types/openai';
5
4
 
5
+ import { checkAuth } from './auth';
6
+ import { createErrorResponse } from './error';
6
7
  import { createChatCompletion } from './openai';
7
8
 
8
9
  export const runtime = 'edge';
@@ -10,8 +11,13 @@ export const runtime = 'edge';
10
11
  export default async function handler(req: Request) {
11
12
  const payload = (await req.json()) as OpenAIStreamPayload;
12
13
  const apiKey = req.headers.get(OPENAI_API_KEY_HEADER_KEY);
14
+ const accessCode = req.headers.get(LOBE_CHAT_ACCESS_CODE);
15
+
16
+ const result = checkAuth({ accessCode, apiKey });
13
17
 
14
- const stream = await createChatCompletion({ OPENAI_API_KEY: apiKey, payload });
18
+ if (!result.auth) {
19
+ return createErrorResponse(result.error as ErrorType);
20
+ }
15
21
 
16
- return new StreamingTextResponse(stream);
22
+ return createChatCompletion({ OPENAI_API_KEY: apiKey, payload });
17
23
  }
@@ -1,18 +1,22 @@
1
- import { OpenAIStream, OpenAIStreamCallbacks } from 'ai';
1
+ import { OpenAIStream, OpenAIStreamCallbacks, StreamingTextResponse } from 'ai';
2
2
  import { Configuration, OpenAIApi } from 'openai-edge';
3
3
  import { ChatCompletionFunctions } from 'openai-edge/types/api';
4
4
 
5
+ import { getServerConfig } from '@/config/server';
6
+ import { createErrorResponse } from '@/pages/api/error';
7
+ import { ErrorType } from '@/types/fetch';
5
8
  import { OpenAIStreamPayload } from '@/types/openai';
6
9
 
7
10
  import pluginList from '../../plugins';
8
11
 
9
12
  const isDev = process.env.NODE_ENV === 'development';
10
- const OPENAI_PROXY_URL = process.env.OPENAI_PROXY_URL;
11
13
 
12
14
  // 创建 OpenAI 实例
13
- export const createOpenAI = (OPENAI_API_KEY: string | null) => {
15
+ export const createOpenAI = (userApiKey: string | null) => {
16
+ const { OPENAI_API_KEY, OPENAI_PROXY_URL } = getServerConfig();
17
+
14
18
  const config = new Configuration({
15
- apiKey: !OPENAI_API_KEY ? process.env.OPENAI_API_KEY : OPENAI_API_KEY,
19
+ apiKey: !userApiKey ? OPENAI_API_KEY : userApiKey,
16
20
  });
17
21
 
18
22
  return new OpenAIApi(config, isDev && OPENAI_PROXY_URL ? OPENAI_PROXY_URL : undefined);
@@ -58,5 +62,13 @@ export const createChatCompletion = async ({
58
62
 
59
63
  const response = await openai.createChatCompletion(requestParams);
60
64
 
61
- return OpenAIStream(response, callbacks?.(requestParams));
65
+ if (!response.ok) {
66
+ const error = await response.json();
67
+
68
+ return createErrorResponse(ErrorType.OpenAIBizError, error);
69
+ }
70
+
71
+ const stream = OpenAIStream(response, callbacks?.(requestParams));
72
+
73
+ return new StreamingTextResponse(stream);
62
74
  };
@@ -1,4 +1,3 @@
1
- import { StreamingTextResponse } from 'ai';
2
1
  import { ChatCompletionRequestMessage } from 'openai-edge';
3
2
 
4
3
  import { OPENAI_API_KEY_HEADER_KEY } from '@/const/fetch';
@@ -15,7 +14,7 @@ export default async function handler(req: Request) {
15
14
 
16
15
  const openai = createOpenAI(apiKey);
17
16
 
18
- const stream = await createChatCompletion({
17
+ return await createChatCompletion({
19
18
  OPENAI_API_KEY: apiKey,
20
19
  callbacks: (payload) => ({
21
20
  experimental_onFunctionCall: async (
@@ -42,6 +41,4 @@ export default async function handler(req: Request) {
42
41
  }),
43
42
  payload,
44
43
  });
45
-
46
- return new StreamingTextResponse(stream);
47
44
  }
@@ -0,0 +1,40 @@
1
+ import { Avatar } from '@lobehub/ui';
2
+ import { Button, Input } from 'antd';
3
+ import { memo } from 'react';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { Center, Flexbox } from 'react-layout-kit';
6
+ import { shallow } from 'zustand/shallow';
7
+
8
+ import { useSettings } from '@/store/settings';
9
+
10
+ import { useStyles } from './style';
11
+
12
+ const APIKeyForm = memo<{ onConfirm?: () => void }>(({ onConfirm }) => {
13
+ const { t } = useTranslation('error');
14
+ const { styles, theme } = useStyles();
15
+ const [apiKey, setSettings] = useSettings(
16
+ (s) => [s.settings.OPENAI_API_KEY, s.setSettings],
17
+ shallow,
18
+ );
19
+
20
+ return (
21
+ <Center gap={16} style={{ maxWidth: 300 }}>
22
+ <Avatar avatar={'🔑'} background={theme.colorText} gap={12} size={80} />
23
+ <Flexbox style={{ fontSize: 20 }}>{t('unlock.apikey.title')}</Flexbox>
24
+ <Flexbox className={styles.desc}>{t('unlock.apikey.description')}</Flexbox>
25
+ <Input
26
+ onChange={(e) => {
27
+ setSettings({ OPENAI_API_KEY: e.target.value });
28
+ }}
29
+ placeholder={'sk-*****************************************'}
30
+ type={'block'}
31
+ value={apiKey}
32
+ />
33
+ <Button block onClick={onConfirm} style={{ marginTop: 8 }} type={'primary'}>
34
+ {t('unlock.confirm')}
35
+ </Button>
36
+ </Center>
37
+ );
38
+ });
39
+
40
+ export default APIKeyForm;
@@ -0,0 +1,23 @@
1
+ import { createStyles } from 'antd-style';
2
+ import { ReactNode, memo } from 'react';
3
+ import { Center } from 'react-layout-kit';
4
+
5
+ const useStyles = createStyles(({ css, token }) => ({
6
+ container: css`
7
+ background: ${token.colorBgContainer};
8
+ border: 1px solid ${token.colorSplit};
9
+ border-radius: 8px;
10
+ `,
11
+ }));
12
+
13
+ const ErrorActionContainer = memo<{ children: ReactNode }>(({ children }) => {
14
+ const { styles } = useStyles();
15
+
16
+ return (
17
+ <Center className={styles.container} gap={24} padding={24}>
18
+ {children}
19
+ </Center>
20
+ );
21
+ });
22
+
23
+ export default ErrorActionContainer;
@@ -0,0 +1,76 @@
1
+ import { Icon } from '@lobehub/ui';
2
+ import { Button, Segmented } from 'antd';
3
+ import { KeySquare, SquareAsterisk } from 'lucide-react';
4
+ import { memo, useState } from 'react';
5
+ import { useTranslation } from 'react-i18next';
6
+ import { Flexbox } from 'react-layout-kit';
7
+ import { shallow } from 'zustand/shallow';
8
+
9
+ import { useSessionStore } from '@/store/session';
10
+ import { useSettings } from '@/store/settings';
11
+
12
+ import OtpInput from '../OTPInput';
13
+ import APIKeyForm from './ApiKeyForm';
14
+ import { ErrorActionContainer, FormAction } from './style';
15
+
16
+ const InvalidAccess = memo<{ id: string }>(({ id }) => {
17
+ const { t } = useTranslation('error');
18
+ const [mode, setMode] = useState('password');
19
+ const [password, setSettings] = useSettings((s) => [s.settings.password, s.setSettings], shallow);
20
+ const [resend, deleteMessage] = useSessionStore(
21
+ (s) => [s.resendMessage, s.deleteMessage],
22
+ shallow,
23
+ );
24
+
25
+ return (
26
+ <ErrorActionContainer>
27
+ <Segmented
28
+ block
29
+ onChange={(value) => setMode(value as string)}
30
+ options={[
31
+ { icon: <Icon icon={SquareAsterisk} />, label: '密码', value: 'password' },
32
+ { icon: <Icon icon={KeySquare} />, label: 'OpenAI API Key', value: 'api' },
33
+ ]}
34
+ style={{ width: '100%' }}
35
+ value={mode}
36
+ />
37
+ <Flexbox gap={24}>
38
+ {mode === 'password' ? (
39
+ <>
40
+ <FormAction
41
+ avatar={'🗳'}
42
+ description={t('unlock.password.description')}
43
+ title={t('unlock.password.title')}
44
+ >
45
+ <OtpInput
46
+ onChange={(e) => {
47
+ setSettings({ password: e });
48
+ }}
49
+ validationPattern={/[\dA-Za-z]/}
50
+ value={password}
51
+ />
52
+ </FormAction>
53
+ <Button
54
+ onClick={() => {
55
+ resend(id);
56
+ deleteMessage(id);
57
+ }}
58
+ type={'primary'}
59
+ >
60
+ {t('unlock.confirm')}
61
+ </Button>
62
+ </>
63
+ ) : (
64
+ <APIKeyForm
65
+ onConfirm={() => {
66
+ resend(id);
67
+ deleteMessage(id);
68
+ }}
69
+ />
70
+ )}
71
+ </Flexbox>
72
+ </ErrorActionContainer>
73
+ );
74
+ });
75
+
76
+ export default InvalidAccess;
@@ -0,0 +1,52 @@
1
+ import { Highlighter } from '@lobehub/ui';
2
+ import { memo } from 'react';
3
+ import { Flexbox } from 'react-layout-kit';
4
+ import { shallow } from 'zustand/shallow';
5
+
6
+ import { useSessionStore } from '@/store/session';
7
+
8
+ import APIKeyForm from './ApiKeyForm';
9
+ import { ErrorActionContainer, useStyles } from './style';
10
+
11
+ interface OpenAIError {
12
+ code: 'invalid_api_key' | string;
13
+ message: string;
14
+ param?: any;
15
+ type: string;
16
+ }
17
+
18
+ interface OpenAIErrorResponse {
19
+ error: OpenAIError;
20
+ }
21
+
22
+ const OpenAiBizError = memo<{ content: OpenAIErrorResponse; id: string }>(({ content, id }) => {
23
+ const { styles } = useStyles();
24
+
25
+ const [resend, deleteMessage] = useSessionStore(
26
+ (s) => [s.resendMessage, s.deleteMessage],
27
+ shallow,
28
+ );
29
+
30
+ const errorCode = content.error?.code;
31
+
32
+ if (errorCode === 'invalid_api_key') {
33
+ return (
34
+ <ErrorActionContainer>
35
+ <APIKeyForm
36
+ onConfirm={() => {
37
+ console.log(id);
38
+ resend(id);
39
+ deleteMessage(id);
40
+ }}
41
+ />
42
+ </ErrorActionContainer>
43
+ );
44
+ }
45
+ return (
46
+ <Flexbox className={styles.container} style={{ maxWidth: 600 }}>
47
+ <Highlighter language={'json'}>{JSON.stringify(content, null, 2)}</Highlighter>
48
+ </Flexbox>
49
+ );
50
+ });
51
+
52
+ export default OpenAiBizError;
@@ -0,0 +1,44 @@
1
+ import { Avatar } from '@lobehub/ui';
2
+ import { createStyles } from 'antd-style';
3
+ import { ReactNode, memo } from 'react';
4
+ import { Center, Flexbox } from 'react-layout-kit';
5
+
6
+ export const useStyles = createStyles(({ css, token }) => ({
7
+ container: css`
8
+ background: ${token.colorBgContainer};
9
+ border: 1px solid ${token.colorSplit};
10
+ border-radius: 8px;
11
+ `,
12
+ desc: css`
13
+ color: ${token.colorTextTertiary};
14
+ text-align: center;
15
+ `,
16
+ }));
17
+
18
+ export const ErrorActionContainer = memo<{ children: ReactNode }>(({ children }) => {
19
+ const { styles } = useStyles();
20
+
21
+ return (
22
+ <Center className={styles.container} gap={24} padding={24}>
23
+ {children}
24
+ </Center>
25
+ );
26
+ });
27
+
28
+ export const FormAction = memo<{
29
+ avatar: string;
30
+ children: ReactNode;
31
+ description: string;
32
+ title: string;
33
+ }>(({ children, title, description, avatar }) => {
34
+ const { styles, theme } = useStyles();
35
+
36
+ return (
37
+ <Center gap={16} style={{ maxWidth: 300 }}>
38
+ <Avatar avatar={avatar} background={theme.colorText} gap={12} size={80} />
39
+ <Flexbox style={{ fontSize: 20 }}>{title}</Flexbox>
40
+ <Flexbox className={styles.desc}>{description}</Flexbox>
41
+ {children}
42
+ </Center>
43
+ );
44
+ });
@@ -0,0 +1,142 @@
1
+ import { useControllableValue } from 'ahooks';
2
+ import { createStyles } from 'antd-style';
3
+ import React, { memo } from 'react';
4
+ import { Flexbox } from 'react-layout-kit';
5
+
6
+ const useStyles = createStyles(
7
+ ({ css, token }) => css`
8
+ width: ${token.controlHeight}px;
9
+ height: ${token.controlHeight}px;
10
+
11
+ font-size: 16px;
12
+ color: ${token.colorText};
13
+ text-align: center;
14
+
15
+ background: ${token.colorBgContainer};
16
+ border: 1px solid ${token.colorBorder};
17
+ border-radius: 8px;
18
+
19
+ &:focus,
20
+ &:focus-visible {
21
+ border-color: ${token.colorPrimary};
22
+ outline: none;
23
+ }
24
+ `,
25
+ );
26
+
27
+ /**
28
+ * Let's borrow some props from HTML "input". More info below:
29
+ *
30
+ * [Pick Documentation](https://www.typescriptlang.org/docs/handbook/utility-types.html#picktype-keys)
31
+ *
32
+ * [How to extend HTML Elements](https://reacthustle.com/blog/how-to-extend-html-elements-in-react-typescript)
33
+ */
34
+ type PartialInputProps = Pick<React.ComponentPropsWithoutRef<'input'>, 'className' | 'style'>;
35
+
36
+ interface OtpInputProps extends PartialInputProps {
37
+ onChange?: (value: string) => void;
38
+ /**
39
+ * Number of characters/input for this component
40
+ */
41
+ size?: number;
42
+ /**
43
+ * Validation pattern for each input.
44
+ * e.g: /[0-9]{1}/ for digits only or /[0-9a-zA-Z]{1}/ for alphanumeric
45
+ */
46
+ validationPattern?: RegExp;
47
+ /**
48
+ * full value of the otp input, up to {size} characters
49
+ */
50
+ value?: string;
51
+ }
52
+
53
+ const handleKeyUp = (e: React.KeyboardEvent<HTMLInputElement>) => {
54
+ const current = e.currentTarget;
55
+ if (e.key === 'ArrowLeft' || e.key === 'Backspace') {
56
+ const prev = current.previousElementSibling as HTMLInputElement | null;
57
+ prev?.focus();
58
+ prev?.setSelectionRange(0, 1);
59
+ return;
60
+ }
61
+
62
+ if (e.key === 'ArrowRight') {
63
+ const prev = current.nextSibling as HTMLInputElement | null;
64
+ prev?.focus();
65
+ prev?.setSelectionRange(0, 1);
66
+ return;
67
+ }
68
+ };
69
+
70
+ const OtpInput = memo<OtpInputProps>((props) => {
71
+ const {
72
+ //Set the default size to 6 characters
73
+ size = 6,
74
+ //Default validation is digits
75
+ validationPattern = /\d/,
76
+ value: outerValue,
77
+ onChange,
78
+ className,
79
+ ...restProps
80
+ } = props;
81
+ const [value, setValue] = useControllableValue({ onChange, value: outerValue });
82
+
83
+ const { styles, cx } = useStyles();
84
+ // Create an array based on the size.
85
+ const arr = Array.from({ length: size }).fill('-');
86
+
87
+ const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>, index: number) => {
88
+ const elem = e.target;
89
+ const val = e.target.value;
90
+
91
+ // check if the value is valid
92
+ if (!validationPattern.test(val) && val !== '') return;
93
+
94
+ // change the value using onChange props
95
+ const valueArr = value?.split('') || [];
96
+ valueArr[index] = val;
97
+ const newVal = valueArr.join('').slice(0, 6);
98
+ setValue(newVal);
99
+
100
+ //focus the next element if there's a value
101
+ if (val) {
102
+ const next = elem.nextElementSibling as HTMLInputElement | null;
103
+ next?.focus();
104
+ }
105
+ };
106
+
107
+ const handlePaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
108
+ e.preventDefault();
109
+ const val = e.clipboardData.getData('text').slice(0, Math.max(0, size));
110
+
111
+ setValue(val);
112
+ };
113
+
114
+ return (
115
+ <Flexbox gap={12} horizontal>
116
+ {arr.map((_, index) => {
117
+ return (
118
+ <input
119
+ key={index}
120
+ {...restProps}
121
+ /**
122
+ * Add some styling to the input using daisyUI + tailwind.
123
+ * Allows the user to override the classname for a different styling
124
+ */
125
+ autoComplete="one-time-code"
126
+ className={cx(styles, className)}
127
+ inputMode="numeric"
128
+ maxLength={6}
129
+ onChange={(e) => handleInputChange(e, index)}
130
+ onKeyUp={handleKeyUp}
131
+ onPaste={handlePaste}
132
+ pattern={validationPattern.source}
133
+ type="text"
134
+ value={value?.at(index) ?? ''}
135
+ />
136
+ );
137
+ })}
138
+ </Flexbox>
139
+ );
140
+ });
141
+
142
+ export default OtpInput;
@@ -1,38 +1,55 @@
1
- import { ChatList, ChatMessage } from '@lobehub/ui';
1
+ import { ChatList, RenderErrorMessage, RenderMessage } from '@lobehub/ui';
2
2
  import isEqual from 'fast-deep-equal';
3
- import { ReactNode, memo } from 'react';
3
+ import { memo } from 'react';
4
4
  import { useTranslation } from 'react-i18next';
5
5
  import { shallow } from 'zustand/shallow';
6
6
 
7
7
  import { agentSelectors, chatSelectors, useSessionStore } from '@/store/session';
8
+ import { ErrorType } from '@/types/fetch';
8
9
  import { isFunctionMessage } from '@/utils/message';
9
10
 
11
+ import InvalidAccess from './Error/InvalidAccess';
12
+ import OpenAiBizError from './Error/OpenAiBizError';
10
13
  import FunctionMessage from './FunctionMessage';
11
14
  import MessageExtra from './MessageExtra';
12
15
 
16
+ const renderMessage: RenderMessage = (content, message) => {
17
+ if (message.role === 'function')
18
+ return isFunctionMessage(message.content) ? <FunctionMessage /> : content;
19
+
20
+ return content;
21
+ };
22
+
23
+ const renderErrorMessage: RenderErrorMessage = (error, message) => {
24
+ switch (error.type as ErrorType) {
25
+ case 'InvalidAccessCode': {
26
+ return <InvalidAccess id={message.id} />;
27
+ }
28
+ case 'OpenAIBizError': {
29
+ return <OpenAiBizError content={(error as any).body} id={message.id} />;
30
+ }
31
+ }
32
+ };
33
+
13
34
  const List = () => {
14
35
  const { t } = useTranslation('common');
15
36
  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
- };
37
+ const [displayMode, chatLoadingId, deleteMessage, resendMessage, dispatchMessage] =
38
+ useSessionStore(
39
+ (s) => [
40
+ agentSelectors.currentAgentConfigSafe(s).displayMode,
41
+ s.chatLoadingId,
42
+ s.deleteMessage,
43
+ s.resendMessage,
44
+ s.dispatchMessage,
45
+ ],
46
+ shallow,
47
+ );
32
48
 
33
49
  return (
34
50
  <ChatList
35
51
  data={data}
52
+ loadingId={chatLoadingId}
36
53
  onActionClick={(key, id) => {
37
54
  switch (key) {
38
55
  case 'delete': {
@@ -49,6 +66,7 @@ const List = () => {
49
66
  onMessageChange={(id, content) => {
50
67
  dispatchMessage({ id, key: 'content', type: 'updateMessage', value: content });
51
68
  }}
69
+ renderErrorMessage={renderErrorMessage}
52
70
  renderMessage={renderMessage}
53
71
  renderMessageExtra={MessageExtra}
54
72
  style={{ marginTop: 24 }}
@@ -8,7 +8,7 @@ import { ModelTokens } from '@/const/modelTokens';
8
8
  import { agentSelectors, chatSelectors, useSessionStore } from '@/store/session';
9
9
 
10
10
  const Token = memo<{ input: string }>(({ input }) => {
11
- const { t } = useTranslation();
11
+ const { t } = useTranslation('common');
12
12
 
13
13
  const inputTokenCount = useMemo(() => encode(input).length, [input]);
14
14
 
@@ -4,7 +4,7 @@ import Router from 'next/router';
4
4
  import { memo } from 'react';
5
5
  import { useTranslation } from 'react-i18next';
6
6
 
7
- import { version } from '@/../package.json';
7
+ import pkg from '@/../package.json';
8
8
  import HeaderTitle from '@/components/HeaderTitle';
9
9
 
10
10
  const Header = memo(() => {
@@ -12,7 +12,7 @@ const Header = memo(() => {
12
12
 
13
13
  return (
14
14
  <ChatHeader
15
- left={<HeaderTitle tag={<Tag>{`v${version}`}</Tag>} title={t('header.global')} />}
15
+ left={<HeaderTitle tag={<Tag>{`v${pkg.version}`}</Tag>} title={t('header.global')} />}
16
16
  onBackClick={() => Router.back()}
17
17
  showBackButton
18
18
  />
@@ -1,6 +1,6 @@
1
1
  import { merge } from 'lodash-es';
2
2
 
3
- import { OPENAI_API_KEY_HEADER_KEY } from '@/const/fetch';
3
+ import { LOBE_CHAT_ACCESS_CODE, OPENAI_API_KEY_HEADER_KEY } from '@/const/fetch';
4
4
  import { initialLobeAgentConfig } from '@/store/session';
5
5
  import { useSettings } from '@/store/settings';
6
6
  import type { OpenAIStreamPayload } from '@/types/openai';
@@ -32,6 +32,7 @@ export const fetchChatModel = (
32
32
  body: JSON.stringify(payload),
33
33
  headers: {
34
34
  'Content-Type': 'application/json',
35
+ [LOBE_CHAT_ACCESS_CODE]: useSettings.getState().settings.password || '',
35
36
  [OPENAI_API_KEY_HEADER_KEY]: useSettings.getState().settings.OPENAI_API_KEY || '',
36
37
  },
37
38
  method: 'POST',
@@ -110,7 +110,7 @@ export const chatMessage: StateCreator<
110
110
 
111
111
  generateMessage: async (messages, assistantId, withPlugin) => {
112
112
  const { dispatchMessage } = get();
113
- set({ chatLoading: true });
113
+ set({ chatLoadingId: assistantId });
114
114
  const config = agentSelectors.currentAgentConfigSafe(get());
115
115
 
116
116
  const fetcher = () =>
@@ -156,7 +156,7 @@ export const chatMessage: StateCreator<
156
156
  },
157
157
  });
158
158
 
159
- set({ chatLoading: false });
159
+ set({ chatLoadingId: undefined });
160
160
 
161
161
  return { isFunctionCall, output };
162
162
  },
@@ -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 = {};
@@ -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,9 @@ 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;
14
13
  }
15
14
 
16
15
  export interface ChatMessage extends BaseDataModel {
@@ -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,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;