@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.
- package/.eslintrc.js +2 -1
- package/CHANGELOG.md +88 -6
- package/package.json +2 -5
- package/src/config/server.ts +22 -0
- package/src/const/fetch.ts +2 -2
- package/src/locales/create.ts +3 -14
- package/src/locales/default/common.ts +1 -0
- package/src/locales/default/error.ts +27 -0
- package/src/locales/resources/zh_CN.ts +2 -0
- package/src/pages/api/auth.ts +25 -0
- package/src/pages/api/error.ts +21 -0
- package/src/pages/api/openai.api.ts +11 -5
- package/src/pages/api/openai.ts +17 -5
- package/src/pages/api/plugins.api.ts +1 -4
- package/src/pages/chat/[id]/Conversation/ChatList/Error/ApiKeyForm.tsx +40 -0
- package/src/pages/chat/[id]/Conversation/ChatList/Error/ErrorActionContainer.tsx +23 -0
- package/src/pages/chat/[id]/Conversation/ChatList/Error/InvalidAccess.tsx +76 -0
- package/src/pages/chat/[id]/Conversation/ChatList/Error/OpenAiBizError.tsx +52 -0
- package/src/pages/chat/[id]/Conversation/ChatList/Error/style.tsx +44 -0
- package/src/pages/chat/[id]/Conversation/ChatList/OTPInput.tsx +142 -0
- package/src/pages/chat/[id]/Conversation/{ChatList.tsx → ChatList/index.tsx} +36 -18
- package/src/pages/chat/[id]/Conversation/Input/Token.tsx +1 -1
- package/src/pages/setting/Header.tsx +2 -2
- package/src/services/chatModel.ts +2 -1
- package/src/store/session/slices/chat/actions/message.ts +2 -2
- package/src/store/session/slices/chat/initialState.ts +2 -4
- package/src/types/chatMessage.ts +4 -5
- package/src/types/fetch.ts +26 -0
- package/src/utils/fetch.ts +21 -25
- /package/src/pages/chat/[id]/Conversation/{FunctionMessage.tsx → ChatList/FunctionMessage.tsx} +0 -0
- /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
|
-
|
|
5
|
+
## [Version 0.26.0](https://github.com/lobehub/lobe-chat/compare/v0.25.0...v0.26.0)
|
|
6
6
|
|
|
7
|
-
|
|
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
|
+
[](#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
|
-
|
|
47
|
+
</details>
|
|
48
|
+
|
|
49
|
+
<div align="right">
|
|
50
|
+
|
|
51
|
+
[](#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
|
-
|
|
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
|
-
|
|
72
|
+
</details>
|
|
73
|
+
|
|
74
|
+
<div align="right">
|
|
75
|
+
|
|
76
|
+
[](#readme-top)
|
|
18
77
|
|
|
19
|
-
|
|
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
|
+
[](#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.
|
|
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
|
+
};
|
package/src/const/fetch.ts
CHANGED
package/src/locales/create.ts
CHANGED
|
@@ -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
|
|
@@ -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
|
+
};
|
|
@@ -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 {
|
|
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
|
-
|
|
18
|
+
if (!result.auth) {
|
|
19
|
+
return createErrorResponse(result.error as ErrorType);
|
|
20
|
+
}
|
|
15
21
|
|
|
16
|
-
return
|
|
22
|
+
return createChatCompletion({ OPENAI_API_KEY: apiKey, payload });
|
|
17
23
|
}
|
package/src/pages/api/openai.ts
CHANGED
|
@@ -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 = (
|
|
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: !
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
1
|
+
import { ChatList, RenderErrorMessage, RenderMessage } from '@lobehub/ui';
|
|
2
2
|
import isEqual from 'fast-deep-equal';
|
|
3
|
-
import {
|
|
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] =
|
|
17
|
-
(
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
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({
|
|
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({
|
|
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
|
-
|
|
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 = {};
|
package/src/types/chatMessage.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|
package/src/utils/fetch.ts
CHANGED
|
@@ -1,24 +1,8 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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;
|
/package/src/pages/chat/[id]/Conversation/{FunctionMessage.tsx → ChatList/FunctionMessage.tsx}
RENAMED
|
File without changes
|
|
File without changes
|