@lobehub/chat 0.11.0 → 0.12.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.
- package/CHANGELOG.md +50 -0
- package/package.json +3 -1
- package/src/locales/default/common.ts +3 -0
- package/src/pages/api/openai.api.ts +25 -6
- package/src/pages/chat/[id]/Conversation/Input/Action.tsx +24 -0
- package/src/pages/chat/[id]/Conversation/Input/Token.tsx +32 -0
- package/src/pages/chat/[id]/Conversation/Input/index.tsx +62 -0
- package/src/pages/chat/[id]/Conversation/index.tsx +1 -1
- package/src/pages/chat/[id]/edit/AgentConfig/Plugin.tsx +46 -0
- package/src/pages/chat/[id]/edit/AgentConfig/index.tsx +14 -3
- package/src/pages/chat/[id]/edit/style.ts +0 -8
- package/src/plugins/index.ts +6 -0
- package/src/plugins/searchEngine/index.ts +27 -0
- package/src/plugins/searchEngine/runner.ts +106 -0
- package/src/{pages/api/plugins → plugins}/weather/index.ts +2 -2
- package/src/store/session/index.ts +3 -1
- package/src/store/session/slices/agentConfig/action.ts +25 -1
- package/src/store/session/slices/chat/action.ts +6 -6
- package/src/store/session/slices/chat/selectors/chat.ts +5 -45
- package/src/store/session/slices/chat/selectors/utils.test.ts +264 -0
- package/src/store/session/slices/chat/selectors/utils.ts +53 -0
- package/src/store/settings/index.ts +3 -1
- package/src/types/openai.ts +5 -0
- package/src/types/session.ts +4 -0
- package/src/utils/env.ts +1 -0
- package/src/pages/api/plugins/index.ts +0 -3
- package/src/pages/chat/[id]/Conversation/Input.tsx +0 -73
- /package/src/{pages/api/plugins → plugins}/weather/runner.ts +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,56 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
+
### [Version 0.12.1](https://github.com/lobehub/lobe-chat/compare/v0.12.0...v0.12.1)
|
|
6
|
+
|
|
7
|
+
<sup>Released on **2023-07-23**</sup>
|
|
8
|
+
|
|
9
|
+
#### 🐛 Bug Fixes
|
|
10
|
+
|
|
11
|
+
- **misc**: 修正 message parentId 不正确的问题.
|
|
12
|
+
|
|
13
|
+
<br/>
|
|
14
|
+
|
|
15
|
+
<details>
|
|
16
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
17
|
+
|
|
18
|
+
#### What's fixed
|
|
19
|
+
|
|
20
|
+
- **misc**: 修正 message parentId 不正确的问题 ([f86852a](https://github.com/lobehub/lobe-chat/commit/f86852a))
|
|
21
|
+
|
|
22
|
+
</details>
|
|
23
|
+
|
|
24
|
+
<div align="right">
|
|
25
|
+
|
|
26
|
+
[](#readme-top)
|
|
27
|
+
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
## [Version 0.12.0](https://github.com/lobehub/lobe-chat/compare/v0.11.0...v0.12.0)
|
|
31
|
+
|
|
32
|
+
<sup>Released on **2023-07-23**</sup>
|
|
33
|
+
|
|
34
|
+
#### ✨ Features
|
|
35
|
+
|
|
36
|
+
- **misc**: 支持插件列表 与 基于 Serpapi 的搜索引擎插件.
|
|
37
|
+
|
|
38
|
+
<br/>
|
|
39
|
+
|
|
40
|
+
<details>
|
|
41
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
42
|
+
|
|
43
|
+
#### What's improved
|
|
44
|
+
|
|
45
|
+
- **misc**: 支持插件列表 与 基于 Serpapi 的搜索引擎插件, closes [#12](https://github.com/lobehub/lobe-chat/issues/12) ([d89e06f](https://github.com/lobehub/lobe-chat/commit/d89e06f))
|
|
46
|
+
|
|
47
|
+
</details>
|
|
48
|
+
|
|
49
|
+
<div align="right">
|
|
50
|
+
|
|
51
|
+
[](#readme-top)
|
|
52
|
+
|
|
53
|
+
</div>
|
|
54
|
+
|
|
5
55
|
## [Version 0.11.0](https://github.com/lobehub/lobe-chat/compare/v0.10.2...v0.11.0)
|
|
6
56
|
|
|
7
57
|
<sup>Released on **2023-07-23**</sup>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobehub/chat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.1",
|
|
4
4
|
"description": "Lobe Chat is an open-source chatbot client using LangChain, Typescript and Next.js",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"chatbot",
|
|
@@ -82,11 +82,13 @@
|
|
|
82
82
|
"next": "13.4.7",
|
|
83
83
|
"openai-edge": "^1",
|
|
84
84
|
"polished": "^4",
|
|
85
|
+
"query-string": "^8",
|
|
85
86
|
"react": "^18",
|
|
86
87
|
"react-dom": "^18",
|
|
87
88
|
"react-hotkeys-hook": "^4",
|
|
88
89
|
"react-i18next": "^13",
|
|
89
90
|
"react-layout-kit": "^1",
|
|
91
|
+
"serpapi": "^1",
|
|
90
92
|
"swr": "^2",
|
|
91
93
|
"ts-md5": "^1",
|
|
92
94
|
"uuid": "^9",
|
|
@@ -35,6 +35,9 @@ export default {
|
|
|
35
35
|
'newAgent': '新建助手',
|
|
36
36
|
'noDescription': '暂无描述',
|
|
37
37
|
'ok': '确定',
|
|
38
|
+
'plugin-realtimeWeather': '实时天气预报',
|
|
39
|
+
'plugin-searchEngine': '搜索引擎',
|
|
40
|
+
'pluginList': '插件列表',
|
|
38
41
|
'profile': '助手身份',
|
|
39
42
|
'reset': '重置',
|
|
40
43
|
'searchAgentPlaceholder': '搜索助手和对话...',
|
|
@@ -4,7 +4,7 @@ import { ChatCompletionFunctions, ChatCompletionRequestMessage } from 'openai-ed
|
|
|
4
4
|
|
|
5
5
|
import { OpenAIStreamPayload } from '@/types/openai';
|
|
6
6
|
|
|
7
|
-
import
|
|
7
|
+
import pluginList from '../../plugins';
|
|
8
8
|
|
|
9
9
|
export const runtime = 'edge';
|
|
10
10
|
|
|
@@ -18,12 +18,28 @@ const config = new Configuration({
|
|
|
18
18
|
|
|
19
19
|
const openai = new OpenAIApi(config, isDev && OPENAI_PROXY_URL ? OPENAI_PROXY_URL : undefined);
|
|
20
20
|
|
|
21
|
-
const functions: ChatCompletionFunctions[] = plugins.map((f) => f.schema);
|
|
22
|
-
|
|
23
21
|
export default async function handler(req: Request) {
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
const {
|
|
23
|
+
messages,
|
|
24
|
+
plugins: enabledPlugins,
|
|
25
|
+
...params
|
|
26
|
+
} = (await req.json()) as OpenAIStreamPayload;
|
|
27
|
+
|
|
28
|
+
// ============ 1. 前置处理 functions ============ //
|
|
29
|
+
|
|
30
|
+
const filterFunctions: ChatCompletionFunctions[] = pluginList
|
|
31
|
+
.filter((p) => {
|
|
32
|
+
// 如果不存在 enabledPlugins,那么全部不启用
|
|
33
|
+
if (!enabledPlugins) return false;
|
|
34
|
+
|
|
35
|
+
// 如果存在 enabledPlugins,那么只启用 enabledPlugins 中的插件
|
|
36
|
+
return enabledPlugins.includes(p.name);
|
|
37
|
+
})
|
|
38
|
+
.map((f) => f.schema);
|
|
26
39
|
|
|
40
|
+
const functions = filterFunctions.length === 0 ? undefined : filterFunctions;
|
|
41
|
+
|
|
42
|
+
// ============ 2. 前置处理 messages ============ //
|
|
27
43
|
const formatMessages = messages.map((m) => ({ content: m.content, role: m.role }));
|
|
28
44
|
|
|
29
45
|
const response = await openai.createChatCompletion({
|
|
@@ -35,7 +51,9 @@ export default async function handler(req: Request) {
|
|
|
35
51
|
|
|
36
52
|
const stream = OpenAIStream(response, {
|
|
37
53
|
experimental_onFunctionCall: async ({ name, arguments: args }, createFunctionCallMessages) => {
|
|
38
|
-
|
|
54
|
+
console.log(`执行 functionCall [${name}]`, 'args:', args);
|
|
55
|
+
|
|
56
|
+
const func = pluginList.find((f) => f.name === name);
|
|
39
57
|
|
|
40
58
|
if (func) {
|
|
41
59
|
const result = await func.runner(args as any);
|
|
@@ -51,5 +69,6 @@ export default async function handler(req: Request) {
|
|
|
51
69
|
}
|
|
52
70
|
},
|
|
53
71
|
});
|
|
72
|
+
|
|
54
73
|
return new StreamingTextResponse(stream);
|
|
55
74
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ActionIcon } from '@lobehub/ui';
|
|
2
|
+
import { Popconfirm } from 'antd';
|
|
3
|
+
import { Eraser, Languages } from 'lucide-react';
|
|
4
|
+
import { memo } from 'react';
|
|
5
|
+
import { useTranslation } from 'react-i18next';
|
|
6
|
+
import { shallow } from 'zustand/shallow';
|
|
7
|
+
|
|
8
|
+
import { useSessionStore } from '@/store/session';
|
|
9
|
+
|
|
10
|
+
const InputActions = memo(() => {
|
|
11
|
+
const { t } = useTranslation();
|
|
12
|
+
const [clearMessage] = useSessionStore((s) => [s.clearMessage], shallow);
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<>
|
|
16
|
+
<ActionIcon icon={Languages} />
|
|
17
|
+
<Popconfirm onConfirm={() => clearMessage()} title={t('confirmClearCurrentMessages')}>
|
|
18
|
+
<ActionIcon icon={Eraser} title={t('clearCurrentMessages')} />
|
|
19
|
+
</Popconfirm>
|
|
20
|
+
</>
|
|
21
|
+
);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export default InputActions;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { TokenTag, Tooltip } from '@lobehub/ui';
|
|
2
|
+
import { encode } from 'gpt-tokenizer';
|
|
3
|
+
import { memo, useMemo } from 'react';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
import { shallow } from 'zustand/shallow';
|
|
6
|
+
|
|
7
|
+
import { ModelTokens } from '@/const/modelTokens';
|
|
8
|
+
import { agentSelectors, chatSelectors, useSessionStore } from '@/store/session';
|
|
9
|
+
|
|
10
|
+
const Token = memo<{ input: string }>(({ input }) => {
|
|
11
|
+
const { t } = useTranslation();
|
|
12
|
+
|
|
13
|
+
const inputTokenCount = useMemo(() => encode(input).length, [input]);
|
|
14
|
+
|
|
15
|
+
const [totalToken, systemRoleToken, chatsToken, model] = useSessionStore(
|
|
16
|
+
(s) => [
|
|
17
|
+
chatSelectors.totalTokenCount(s),
|
|
18
|
+
chatSelectors.systemRoleTokenCount(s),
|
|
19
|
+
chatSelectors.chatsTokenCount(s),
|
|
20
|
+
agentSelectors.currentAgentModel(s),
|
|
21
|
+
],
|
|
22
|
+
shallow,
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<Tooltip title={t('tokenDetail', { chatsToken, systemRoleToken })}>
|
|
27
|
+
<TokenTag maxValue={ModelTokens[model]} value={totalToken + inputTokenCount} />
|
|
28
|
+
</Tooltip>
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export default Token;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { ChatInputArea, DraggablePanel, Icon } from '@lobehub/ui';
|
|
2
|
+
import { Button } from 'antd';
|
|
3
|
+
import { Archive } from 'lucide-react';
|
|
4
|
+
import { memo, useMemo, useState } from 'react';
|
|
5
|
+
import { useTranslation } from 'react-i18next';
|
|
6
|
+
import { shallow } from 'zustand/shallow';
|
|
7
|
+
|
|
8
|
+
import { CHAT_TEXTAREA_HEIGHT, HEADER_HEIGHT } from '@/const/layoutTokens';
|
|
9
|
+
import { useSessionStore } from '@/store/session';
|
|
10
|
+
import { useSettings } from '@/store/settings';
|
|
11
|
+
|
|
12
|
+
import InputActions from './Action';
|
|
13
|
+
import Token from './Token';
|
|
14
|
+
|
|
15
|
+
const ChatInput = () => {
|
|
16
|
+
const { t } = useTranslation();
|
|
17
|
+
const [expand, setExpand] = useState<boolean>(false);
|
|
18
|
+
const [text, setText] = useState('');
|
|
19
|
+
|
|
20
|
+
const [inputHeight] = useSettings((s) => [s.inputHeight], shallow);
|
|
21
|
+
const [sendMessage] = useSessionStore((s) => [s.createOrSendMsg], shallow);
|
|
22
|
+
|
|
23
|
+
const footer = useMemo(
|
|
24
|
+
() => <Button icon={<Icon icon={Archive} title={t('archiveCurrentMessages')} />} />,
|
|
25
|
+
[],
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<DraggablePanel
|
|
30
|
+
expandable={false}
|
|
31
|
+
fullscreen={expand}
|
|
32
|
+
headerHeight={HEADER_HEIGHT}
|
|
33
|
+
minHeight={CHAT_TEXTAREA_HEIGHT}
|
|
34
|
+
onSizeChange={(_, size) => {
|
|
35
|
+
if (!size) return;
|
|
36
|
+
useSettings.setState({
|
|
37
|
+
inputHeight: typeof size.height === 'string' ? Number.parseInt(size.height) : size.height,
|
|
38
|
+
});
|
|
39
|
+
}}
|
|
40
|
+
placement="bottom"
|
|
41
|
+
size={{ height: inputHeight, width: '100%' }}
|
|
42
|
+
style={{ zIndex: 10 }}
|
|
43
|
+
>
|
|
44
|
+
<ChatInputArea
|
|
45
|
+
actions={
|
|
46
|
+
<>
|
|
47
|
+
<InputActions />
|
|
48
|
+
<Token input={text} />
|
|
49
|
+
</>
|
|
50
|
+
}
|
|
51
|
+
expand={expand}
|
|
52
|
+
footer={footer}
|
|
53
|
+
minHeight={CHAT_TEXTAREA_HEIGHT}
|
|
54
|
+
onExpandChange={setExpand}
|
|
55
|
+
onInputChange={setText}
|
|
56
|
+
onSend={sendMessage}
|
|
57
|
+
/>
|
|
58
|
+
</DraggablePanel>
|
|
59
|
+
);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export default memo(ChatInput);
|
|
@@ -2,9 +2,9 @@ import { memo } from 'react';
|
|
|
2
2
|
import { Flexbox } from 'react-layout-kit';
|
|
3
3
|
|
|
4
4
|
import HeaderSpacing from '@/components/HeaderSpacing';
|
|
5
|
-
import ChatInput from '@/pages/chat/[id]/Conversation/Input';
|
|
6
5
|
|
|
7
6
|
import ChatList from './ChatList';
|
|
7
|
+
import ChatInput from './Input';
|
|
8
8
|
|
|
9
9
|
const Conversation = () => {
|
|
10
10
|
return (
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Avatar } from '@lobehub/ui';
|
|
2
|
+
import { List, Switch, Tag } from 'antd';
|
|
3
|
+
import isEqual from 'fast-deep-equal';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
import { Flexbox } from 'react-layout-kit';
|
|
6
|
+
import { shallow } from 'zustand/shallow';
|
|
7
|
+
|
|
8
|
+
import pluginList from '@/plugins';
|
|
9
|
+
import { agentSelectors, useSessionStore } from '@/store/session';
|
|
10
|
+
|
|
11
|
+
const PluginList = () => {
|
|
12
|
+
const { t } = useTranslation('common');
|
|
13
|
+
|
|
14
|
+
const config = useSessionStore(agentSelectors.currentAgentConfigSafe, isEqual);
|
|
15
|
+
|
|
16
|
+
const [toggleAgentPlugin] = useSessionStore((s) => [s.toggleAgentPlugin], shallow);
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<List
|
|
20
|
+
bordered
|
|
21
|
+
dataSource={pluginList}
|
|
22
|
+
renderItem={(item) => (
|
|
23
|
+
<List.Item>
|
|
24
|
+
<List.Item.Meta
|
|
25
|
+
avatar={<Avatar avatar={item.avatar} />}
|
|
26
|
+
description={item.schema.description}
|
|
27
|
+
title={
|
|
28
|
+
<Flexbox align={'center'} gap={8} horizontal>
|
|
29
|
+
{t(`plugin-${item.name}` as any)} <Tag>{item.name}</Tag>
|
|
30
|
+
</Flexbox>
|
|
31
|
+
}
|
|
32
|
+
/>
|
|
33
|
+
<Switch
|
|
34
|
+
checked={!config.plugins ? false : config.plugins.includes(item.name)}
|
|
35
|
+
onChange={() => {
|
|
36
|
+
toggleAgentPlugin(item.name);
|
|
37
|
+
}}
|
|
38
|
+
/>
|
|
39
|
+
</List.Item>
|
|
40
|
+
)}
|
|
41
|
+
size={'large'}
|
|
42
|
+
/>
|
|
43
|
+
);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export default PluginList;
|
|
@@ -9,6 +9,7 @@ import { LanguageModel } from '@/types/llm';
|
|
|
9
9
|
|
|
10
10
|
import { FormItem } from '../FormItem';
|
|
11
11
|
import { useStyles } from '../style';
|
|
12
|
+
import Plugin from './Plugin';
|
|
12
13
|
import Prompt from './Prompt';
|
|
13
14
|
|
|
14
15
|
const AgentConfig = () => {
|
|
@@ -58,8 +59,6 @@ const AgentConfig = () => {
|
|
|
58
59
|
</FormItem>
|
|
59
60
|
<Prompt />
|
|
60
61
|
<Collapse
|
|
61
|
-
activeKey={['advanceSettings']}
|
|
62
|
-
bordered={false}
|
|
63
62
|
className={styles.title}
|
|
64
63
|
expandIconPosition={'end'}
|
|
65
64
|
items={[
|
|
@@ -95,7 +94,19 @@ const AgentConfig = () => {
|
|
|
95
94
|
},
|
|
96
95
|
]}
|
|
97
96
|
/>
|
|
98
|
-
|
|
97
|
+
<Flexbox
|
|
98
|
+
align={'center'}
|
|
99
|
+
distribution={'space-between'}
|
|
100
|
+
horizontal
|
|
101
|
+
paddingBlock={12}
|
|
102
|
+
style={{
|
|
103
|
+
borderBottom: `1px solid ${theme.colorBorder}`,
|
|
104
|
+
}}
|
|
105
|
+
>
|
|
106
|
+
<Flexbox className={styles.profile}> {t('pluginList')}</Flexbox>
|
|
107
|
+
</Flexbox>
|
|
108
|
+
<Plugin />
|
|
109
|
+
</Flexbox>
|
|
99
110
|
</ConfigProvider>
|
|
100
111
|
);
|
|
101
112
|
};
|
|
@@ -30,13 +30,5 @@ export const useStyles = createStyles(({ css, token }) => ({
|
|
|
30
30
|
title: css`
|
|
31
31
|
font-size: 16px;
|
|
32
32
|
font-weight: 500;
|
|
33
|
-
|
|
34
|
-
.ant-collapse-header {
|
|
35
|
-
padding: 0 !important;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
.ant-collapse-content-box {
|
|
39
|
-
padding: 0 !important;
|
|
40
|
-
}
|
|
41
33
|
`,
|
|
42
34
|
}));
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { ChatCompletionFunctions } from 'openai-edge/types/api';
|
|
2
|
+
|
|
3
|
+
import runner from './runner';
|
|
4
|
+
|
|
5
|
+
const schema: ChatCompletionFunctions = {
|
|
6
|
+
description: '查询搜索引擎获取信息',
|
|
7
|
+
name: 'searchEngine',
|
|
8
|
+
parameters: {
|
|
9
|
+
properties: {
|
|
10
|
+
keywords: {
|
|
11
|
+
description: '关键词',
|
|
12
|
+
type: 'string',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
required: ['keywords'],
|
|
16
|
+
type: 'object',
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const searchEngine = {
|
|
21
|
+
avatar: '🔍',
|
|
22
|
+
name: 'searchEngine',
|
|
23
|
+
runner,
|
|
24
|
+
schema,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export default searchEngine;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import querystring from 'query-string';
|
|
2
|
+
|
|
3
|
+
const BASE_URL = 'https://serpapi.com/search';
|
|
4
|
+
|
|
5
|
+
const API_KEY = process.env.SERPAI_API_KEY;
|
|
6
|
+
|
|
7
|
+
export type OrganicResults = OrganicResult[];
|
|
8
|
+
|
|
9
|
+
export interface OrganicResult {
|
|
10
|
+
about_page_link: string;
|
|
11
|
+
about_page_serpapi_link: string;
|
|
12
|
+
about_this_result: AboutThisResult;
|
|
13
|
+
cached_page_link?: string;
|
|
14
|
+
date?: string;
|
|
15
|
+
displayed_link: string;
|
|
16
|
+
favicon?: string;
|
|
17
|
+
link: string;
|
|
18
|
+
position: number;
|
|
19
|
+
related_results?: RelatedResult[];
|
|
20
|
+
rich_snippet?: RichSnippet;
|
|
21
|
+
snippet: string;
|
|
22
|
+
snippet_highlighted_words?: string[];
|
|
23
|
+
source: string;
|
|
24
|
+
thumbnail?: string;
|
|
25
|
+
title: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface AboutThisResult {
|
|
29
|
+
languages: string[];
|
|
30
|
+
regions: string[];
|
|
31
|
+
source: Source;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface Source {
|
|
35
|
+
description: string;
|
|
36
|
+
icon: string;
|
|
37
|
+
security?: string;
|
|
38
|
+
source_info_link?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface RelatedResult {
|
|
42
|
+
about_page_link: string;
|
|
43
|
+
about_page_serpapi_link: string;
|
|
44
|
+
about_this_result: AboutThisResult2;
|
|
45
|
+
cached_page_link: string;
|
|
46
|
+
date: string;
|
|
47
|
+
displayed_link: string;
|
|
48
|
+
link: string;
|
|
49
|
+
position: number;
|
|
50
|
+
snippet: string;
|
|
51
|
+
snippet_highlighted_words: string[];
|
|
52
|
+
title: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface AboutThisResult2 {
|
|
56
|
+
languages: string[];
|
|
57
|
+
regions: string[];
|
|
58
|
+
source: Source2;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface Source2 {
|
|
62
|
+
description: string;
|
|
63
|
+
icon: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface RichSnippet {
|
|
67
|
+
top: Top;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface Top {
|
|
71
|
+
detected_extensions: DetectedExtensions;
|
|
72
|
+
extensions: string[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface DetectedExtensions {
|
|
76
|
+
month_ago: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const fetchResult = async (keywords: string) => {
|
|
80
|
+
const params = {
|
|
81
|
+
api_key: API_KEY,
|
|
82
|
+
gl: 'cn',
|
|
83
|
+
google_domain: 'google.com',
|
|
84
|
+
hl: 'zh-cn',
|
|
85
|
+
location: 'China',
|
|
86
|
+
q: keywords,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const query = querystring.stringify(params);
|
|
90
|
+
|
|
91
|
+
const res = await fetch(`${BASE_URL}?${query}`);
|
|
92
|
+
|
|
93
|
+
const data = await res.json();
|
|
94
|
+
|
|
95
|
+
const results = data.organic_results as OrganicResults;
|
|
96
|
+
|
|
97
|
+
return results.map((r) => ({
|
|
98
|
+
content: r.snippet,
|
|
99
|
+
date: r.date,
|
|
100
|
+
link: r.link,
|
|
101
|
+
source: r.source,
|
|
102
|
+
title: r.title,
|
|
103
|
+
}));
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export default fetchResult;
|
|
@@ -2,7 +2,7 @@ import runner from './runner';
|
|
|
2
2
|
|
|
3
3
|
const schema = {
|
|
4
4
|
description: '获取当前天气情况',
|
|
5
|
-
name: '
|
|
5
|
+
name: 'realtimeWeather',
|
|
6
6
|
parameters: {
|
|
7
7
|
properties: {
|
|
8
8
|
city: {
|
|
@@ -15,6 +15,6 @@ const schema = {
|
|
|
15
15
|
},
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
-
const getWeather = { name: '
|
|
18
|
+
const getWeather = { avatar: '☂️', name: 'realtimeWeather', runner, schema };
|
|
19
19
|
|
|
20
20
|
export default getWeather;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { create } from 'zustand';
|
|
2
2
|
import { PersistOptions, devtools, persist } from 'zustand/middleware';
|
|
3
3
|
|
|
4
|
+
import { isDev } from '@/utils/env';
|
|
5
|
+
|
|
4
6
|
import { SessionStore, createStore } from './store';
|
|
5
7
|
|
|
6
8
|
type SessionPersist = Pick<SessionStore, 'sessions'>;
|
|
@@ -23,7 +25,7 @@ const persistOptions: PersistOptions<SessionStore, SessionPersist> = {
|
|
|
23
25
|
export const useSessionStore = create<SessionStore>()(
|
|
24
26
|
persist(
|
|
25
27
|
devtools(createStore, {
|
|
26
|
-
name: LOBE_CHAT,
|
|
28
|
+
name: LOBE_CHAT + (isDev ? '_DEV' : ''),
|
|
27
29
|
}),
|
|
28
30
|
persistOptions,
|
|
29
31
|
),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { produce } from 'immer';
|
|
1
2
|
import { StateCreator } from 'zustand/vanilla';
|
|
2
3
|
|
|
3
4
|
import { promptPickEmoji, promptSummaryAgentName, promptSummaryDescription } from '@/prompts/agent';
|
|
@@ -43,12 +44,13 @@ export interface AgentAction {
|
|
|
43
44
|
* @returns 任意类型的返回值
|
|
44
45
|
*/
|
|
45
46
|
internalUpdateAgentMeta: (id: string) => any;
|
|
47
|
+
toggleAgentPlugin: (pluginId: string) => void;
|
|
48
|
+
|
|
46
49
|
/**
|
|
47
50
|
* 切换配置
|
|
48
51
|
* @param showPanel - 是否显示面板,默认为 true
|
|
49
52
|
*/
|
|
50
53
|
toggleConfig: (showPanel?: boolean) => void;
|
|
51
|
-
|
|
52
54
|
/**
|
|
53
55
|
* 更新代理配置
|
|
54
56
|
* @param config - 部分 LobeAgentConfig 的配置
|
|
@@ -191,6 +193,27 @@ export const createAgentSlice: StateCreator<
|
|
|
191
193
|
};
|
|
192
194
|
},
|
|
193
195
|
|
|
196
|
+
toggleAgentPlugin: (id: string) => {
|
|
197
|
+
const { activeId } = get();
|
|
198
|
+
const session = sessionSelectors.currentSession(get());
|
|
199
|
+
if (!activeId || !session) return;
|
|
200
|
+
|
|
201
|
+
const config = produce(session.config, (draft) => {
|
|
202
|
+
if (draft.plugins === undefined) {
|
|
203
|
+
draft.plugins = [id];
|
|
204
|
+
} else {
|
|
205
|
+
const plugins = draft.plugins;
|
|
206
|
+
if (plugins.includes(id)) {
|
|
207
|
+
plugins.splice(plugins.indexOf(id), 1);
|
|
208
|
+
} else {
|
|
209
|
+
plugins.push(id);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
get().dispatchSession({ config, id: activeId, type: 'updateSessionConfig' });
|
|
215
|
+
},
|
|
216
|
+
|
|
194
217
|
toggleConfig: (newValue) => {
|
|
195
218
|
const showAgentSettings = typeof newValue === 'boolean' ? newValue : !get().showAgentSettings;
|
|
196
219
|
|
|
@@ -204,6 +227,7 @@ export const createAgentSlice: StateCreator<
|
|
|
204
227
|
|
|
205
228
|
get().dispatchSession({ config, id: activeId, type: 'updateSessionConfig' });
|
|
206
229
|
},
|
|
230
|
+
|
|
207
231
|
updateAgentMeta: (meta) => {
|
|
208
232
|
const { activeId } = get();
|
|
209
233
|
const session = sessionSelectors.currentSession(get());
|
|
@@ -50,7 +50,7 @@ export interface ChatAction {
|
|
|
50
50
|
* @param messages - 聊天消息数组
|
|
51
51
|
* @param parentId - 父消息 ID,可选
|
|
52
52
|
*/
|
|
53
|
-
realFetchAIResponse: (messages: ChatMessage[], parentId
|
|
53
|
+
realFetchAIResponse: (messages: ChatMessage[], parentId: string) => Promise<void>;
|
|
54
54
|
/**
|
|
55
55
|
* 重新发送消息
|
|
56
56
|
* @param id - 消息 ID
|
|
@@ -104,7 +104,8 @@ export const createChatSlice: StateCreator<
|
|
|
104
104
|
set({ chatLoading: true });
|
|
105
105
|
const config = agentSelectors.currentAgentConfigSafe(get());
|
|
106
106
|
|
|
107
|
-
const fetcher = () =>
|
|
107
|
+
const fetcher = () =>
|
|
108
|
+
fetchChatModel({ messages, model: config.model, ...config.params, plugins: config.plugins });
|
|
108
109
|
|
|
109
110
|
await fetchSSE(fetcher, options);
|
|
110
111
|
|
|
@@ -115,7 +116,7 @@ export const createChatSlice: StateCreator<
|
|
|
115
116
|
set({ editingMessageId: messageId });
|
|
116
117
|
},
|
|
117
118
|
|
|
118
|
-
realFetchAIResponse: async (messages
|
|
119
|
+
realFetchAIResponse: async (messages, userMessageId) => {
|
|
119
120
|
const { dispatchMessage, generateMessage } = get();
|
|
120
121
|
|
|
121
122
|
// 添加 systemRole
|
|
@@ -127,12 +128,11 @@ export const createChatSlice: StateCreator<
|
|
|
127
128
|
// 再添加一个空的信息用于放置 ai 响应,注意顺序不能反
|
|
128
129
|
// 因为如果顺序反了,messages 中将包含新增的 ai message
|
|
129
130
|
const assistantId = nanoid();
|
|
130
|
-
const userId = parentId ?? nanoid();
|
|
131
131
|
|
|
132
132
|
dispatchMessage({
|
|
133
133
|
id: assistantId,
|
|
134
134
|
message: LOADING_FLAT,
|
|
135
|
-
parentId:
|
|
135
|
+
parentId: userMessageId,
|
|
136
136
|
role: 'assistant',
|
|
137
137
|
type: 'addMessage',
|
|
138
138
|
});
|
|
@@ -208,7 +208,7 @@ export const createChatSlice: StateCreator<
|
|
|
208
208
|
// 先拿到当前的 messages
|
|
209
209
|
const messages = chatSelectors.currentChats(get());
|
|
210
210
|
|
|
211
|
-
await realFetchAIResponse(messages);
|
|
211
|
+
await realFetchAIResponse(messages, userId);
|
|
212
212
|
|
|
213
213
|
const chats = chatSelectors.currentChats(get());
|
|
214
214
|
if (chats.length >= 4) {
|
|
@@ -5,57 +5,17 @@ import { ChatMessage } from '@/types/chatMessage';
|
|
|
5
5
|
import type { SessionStore } from '../../../store';
|
|
6
6
|
import { DEFAULT_AVATAR } from '../../agentConfig';
|
|
7
7
|
import { sessionSelectors } from '../../session';
|
|
8
|
+
import { organizeChats } from './utils';
|
|
8
9
|
|
|
9
10
|
// 展示在聊天框中的消息
|
|
10
11
|
export const currentChats = (s: SessionStore): ChatMessage[] => {
|
|
11
12
|
const session = sessionSelectors.currentSession(s);
|
|
12
13
|
if (!session) return [];
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
.
|
|
17
|
-
|
|
18
|
-
.filter((m) => !m.archive)
|
|
19
|
-
// 映射头像关系
|
|
20
|
-
.map((m) => {
|
|
21
|
-
return {
|
|
22
|
-
...m,
|
|
23
|
-
meta:
|
|
24
|
-
m.role === 'assistant'
|
|
25
|
-
? {
|
|
26
|
-
avatar: agentSelectors.currentAgentAvatar(s),
|
|
27
|
-
title: session.meta.title,
|
|
28
|
-
}
|
|
29
|
-
: m.role === 'user'
|
|
30
|
-
? {
|
|
31
|
-
avatar: useSettings.getState().settings.avatar || DEFAULT_AVATAR,
|
|
32
|
-
}
|
|
33
|
-
: m.meta,
|
|
34
|
-
};
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
const finalList: ChatMessage[] = [];
|
|
38
|
-
|
|
39
|
-
const addItem = (item: ChatMessage) => {
|
|
40
|
-
const isExist = finalList.findIndex((i) => item.id === i.id) > -1;
|
|
41
|
-
if (!isExist) {
|
|
42
|
-
finalList.push(item);
|
|
43
|
-
}
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
// 基于添加逻辑进行重排序
|
|
47
|
-
for (const item of basic) {
|
|
48
|
-
// 先判存在与否,不存在就加入
|
|
49
|
-
addItem(item);
|
|
50
|
-
|
|
51
|
-
for (const another of basic) {
|
|
52
|
-
if (another.parentId === item.id) {
|
|
53
|
-
addItem(another);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return finalList;
|
|
15
|
+
return organizeChats(session, {
|
|
16
|
+
assistant: agentSelectors.currentAgentAvatar(s),
|
|
17
|
+
user: useSettings.getState().settings.avatar || DEFAULT_AVATAR,
|
|
18
|
+
});
|
|
59
19
|
};
|
|
60
20
|
|
|
61
21
|
export const systemRoleSel = (s: SessionStore): string => {
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { LobeAgentSession } from '@/types/session';
|
|
4
|
+
|
|
5
|
+
import { organizeChats } from './utils';
|
|
6
|
+
|
|
7
|
+
let session: LobeAgentSession;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
session = {
|
|
11
|
+
chats: {
|
|
12
|
+
'1': {
|
|
13
|
+
id: '1',
|
|
14
|
+
createAt: 1639440000000,
|
|
15
|
+
updateAt: 1639440000000,
|
|
16
|
+
meta: {},
|
|
17
|
+
content: 'Message 1',
|
|
18
|
+
role: 'assistant',
|
|
19
|
+
},
|
|
20
|
+
'2': {
|
|
21
|
+
id: '2',
|
|
22
|
+
createAt: 1639450000000,
|
|
23
|
+
updateAt: 1639450000000,
|
|
24
|
+
meta: {},
|
|
25
|
+
content: 'Message 2',
|
|
26
|
+
role: 'user',
|
|
27
|
+
},
|
|
28
|
+
'3': {
|
|
29
|
+
id: '3',
|
|
30
|
+
createAt: 1639460000000,
|
|
31
|
+
updateAt: 1639460000000,
|
|
32
|
+
meta: {},
|
|
33
|
+
content: 'Message 3',
|
|
34
|
+
role: 'assistant',
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
config: {
|
|
38
|
+
model: 'gpt-4',
|
|
39
|
+
params: {
|
|
40
|
+
temperature: 0.6,
|
|
41
|
+
},
|
|
42
|
+
systemRole: '',
|
|
43
|
+
},
|
|
44
|
+
type: 'agent',
|
|
45
|
+
createAt: 1690110700808,
|
|
46
|
+
id: 'abc',
|
|
47
|
+
meta: {},
|
|
48
|
+
updateAt: 1690110700808,
|
|
49
|
+
} as LobeAgentSession;
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('organizeChats', () => {
|
|
53
|
+
it('should return an array of chat messages', () => {
|
|
54
|
+
const result = organizeChats(session, { assistant: '', user: '' });
|
|
55
|
+
expect(Array.isArray(result)).toBe(true);
|
|
56
|
+
expect(result.length).toBe(3);
|
|
57
|
+
expect(result[0].id).toBe('1');
|
|
58
|
+
expect(result[1].id).toBe('2');
|
|
59
|
+
expect(result[2].id).toBe('3');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should sort chat messages in ascending order of createAt', () => {
|
|
63
|
+
const result = organizeChats(session, { assistant: '', user: '' });
|
|
64
|
+
expect(result[0].id).toBe('1');
|
|
65
|
+
expect(result[1].id).toBe('2');
|
|
66
|
+
expect(result[2].id).toBe('3');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should filter out archived messages', () => {
|
|
70
|
+
session.chats['2'].archive = true;
|
|
71
|
+
const result = organizeChats(session, { assistant: '', user: '' });
|
|
72
|
+
expect(result.length).toBe(2);
|
|
73
|
+
expect(result[0].id).toBe('1');
|
|
74
|
+
expect(result[1].id).toBe('3');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should map avatars correctly', () => {
|
|
78
|
+
const avatar = 'https://example.com/avatar.png';
|
|
79
|
+
const settings = {
|
|
80
|
+
avatar: 'https://example.com/user-avatar.png',
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const result = organizeChats(session, {
|
|
84
|
+
assistant: avatar,
|
|
85
|
+
user: settings.avatar,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
expect(result[0].meta.avatar).toBe(avatar);
|
|
89
|
+
expect(result[1].meta.avatar).toBe(settings.avatar);
|
|
90
|
+
expect(result[2].meta.avatar).toBe(avatar);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should reorder messages based on parent-child relationship', () => {
|
|
94
|
+
session.chats['2'].parentId = '1';
|
|
95
|
+
session.chats['3'].parentId = '2';
|
|
96
|
+
const result = organizeChats(session, { assistant: '', user: '' });
|
|
97
|
+
expect(result.length).toBe(3);
|
|
98
|
+
expect(result[0].id).toBe('1');
|
|
99
|
+
expect(result[1].id).toBe('2');
|
|
100
|
+
expect(result[2].id).toBe('3');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should remove duplicate messages', () => {
|
|
104
|
+
session.chats['2'].parentId = '1';
|
|
105
|
+
session.chats['3'].parentId = '2';
|
|
106
|
+
session.chats['3'].id = '2';
|
|
107
|
+
const result = organizeChats(session, { assistant: '', user: '' });
|
|
108
|
+
expect(result.length).toBe(2);
|
|
109
|
+
expect(result[0].id).toBe('1');
|
|
110
|
+
expect(result[1].id).toBe('2');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should return an empty array for empty session', () => {
|
|
114
|
+
const emptySession = {
|
|
115
|
+
chats: {},
|
|
116
|
+
config: {},
|
|
117
|
+
type: 'agent',
|
|
118
|
+
} as LobeAgentSession;
|
|
119
|
+
const result = organizeChats(emptySession, { assistant: '', user: '' });
|
|
120
|
+
expect(Array.isArray(result)).toBe(true);
|
|
121
|
+
expect(result.length).toBe(0);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should handle large number of chat messages', () => {
|
|
125
|
+
const largeSession = {
|
|
126
|
+
chats: {},
|
|
127
|
+
config: {},
|
|
128
|
+
type: 'agent',
|
|
129
|
+
meta: {},
|
|
130
|
+
} as LobeAgentSession;
|
|
131
|
+
|
|
132
|
+
const numMessages = 1000;
|
|
133
|
+
|
|
134
|
+
for (let i = 1; i <= numMessages; i++) {
|
|
135
|
+
largeSession.chats[i.toString()] = {
|
|
136
|
+
id: i.toString(),
|
|
137
|
+
createAt: i,
|
|
138
|
+
updateAt: i,
|
|
139
|
+
meta: {},
|
|
140
|
+
content: `Message ${i}`,
|
|
141
|
+
role: 'assistant',
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const result = organizeChats(largeSession, { assistant: '', user: '' });
|
|
146
|
+
expect(result.length).toBe(numMessages);
|
|
147
|
+
expect(result[0].id).toBe('1');
|
|
148
|
+
expect(result[numMessages - 1].id).toBe(numMessages.toString());
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('按照预期进行排序', () => {
|
|
152
|
+
const realSession = {
|
|
153
|
+
chats: {
|
|
154
|
+
'Ftei28dF': {
|
|
155
|
+
content: '鲁迅为何暴打周树人',
|
|
156
|
+
createAt: 1690111354731,
|
|
157
|
+
id: 'Ftei28dF',
|
|
158
|
+
meta: {},
|
|
159
|
+
role: 'user',
|
|
160
|
+
updateAt: 1690111354731,
|
|
161
|
+
},
|
|
162
|
+
'9bQW9hTs': {
|
|
163
|
+
content:
|
|
164
|
+
'这是一种误解。鲁迅和周树人是同一人,指的都是现代文学家周樟寿。"鲁迅"是他的笔名,"周树人"则是他的原名。所以,鲁迅并没有暴打周树人,这只是一种说法上的误解。',
|
|
165
|
+
createAt: 1690111354734,
|
|
166
|
+
id: '9bQW9hTs',
|
|
167
|
+
meta: {},
|
|
168
|
+
parentId: 'Ftei28dF',
|
|
169
|
+
role: 'assistant',
|
|
170
|
+
updateAt: 1690111361514,
|
|
171
|
+
extra: {
|
|
172
|
+
fromModel: 'gpt-4',
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
'HRQGSszU': {
|
|
176
|
+
content:
|
|
177
|
+
'这个问题基于一个误解。实际上,鲁迅和周树人是同一个人,这是中国现代文学的开山鼻祖周树人的笔名。他的本名是周树人,鲁迅则是他在30岁时开始使用的笔名。因此,鲁迅不能暴打周树人,因为他们是同一个人。',
|
|
178
|
+
createAt: 1690111364344,
|
|
179
|
+
id: 'HRQGSszU',
|
|
180
|
+
meta: {},
|
|
181
|
+
parentId: 'Ftei28dF',
|
|
182
|
+
role: 'assistant',
|
|
183
|
+
updateAt: 1690111369519,
|
|
184
|
+
extra: {
|
|
185
|
+
fromModel: 'gpt-4',
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
'981qr9n0': {
|
|
189
|
+
content:
|
|
190
|
+
'这是一个误解。鲁迅和周树人是同一个人,是中国现代文学的奠基人。鲁迅是他的笔名,周树人是他的本名。所以,鲁迅不可能暴打周树人。这种说法可能是源于一些误解或误传。',
|
|
191
|
+
createAt: 1690111375456,
|
|
192
|
+
id: '981qr9n0',
|
|
193
|
+
meta: {},
|
|
194
|
+
parentId: 'Ftei28dF',
|
|
195
|
+
role: 'assistant',
|
|
196
|
+
updateAt: 1690111381458,
|
|
197
|
+
extra: {
|
|
198
|
+
fromModel: 'gpt-4',
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
'ddd': {
|
|
202
|
+
content: '鲁迅是谁',
|
|
203
|
+
createAt: 1690211354731,
|
|
204
|
+
id: 'ddd',
|
|
205
|
+
meta: {},
|
|
206
|
+
role: 'user',
|
|
207
|
+
updateAt: 1690211354731,
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
config: {
|
|
211
|
+
model: 'gpt-4',
|
|
212
|
+
params: {
|
|
213
|
+
temperature: 0.6,
|
|
214
|
+
},
|
|
215
|
+
systemRole: '',
|
|
216
|
+
},
|
|
217
|
+
createAt: 1690110700808,
|
|
218
|
+
id: '1515e861-0c64-49a3-bb85-2b24d65a19d6',
|
|
219
|
+
meta: {},
|
|
220
|
+
type: 'agent',
|
|
221
|
+
updateAt: 1690110700808,
|
|
222
|
+
} as LobeAgentSession;
|
|
223
|
+
|
|
224
|
+
const result = organizeChats(realSession, { assistant: '', user: '' });
|
|
225
|
+
|
|
226
|
+
expect(
|
|
227
|
+
result.map((i) => ({ id: i.id, content: i.content, role: i.role, createAt: i.createAt })),
|
|
228
|
+
).toEqual([
|
|
229
|
+
{
|
|
230
|
+
content: '鲁迅为何暴打周树人',
|
|
231
|
+
createAt: 1690111354731,
|
|
232
|
+
id: 'Ftei28dF',
|
|
233
|
+
role: 'user',
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
content:
|
|
237
|
+
'这是一种误解。鲁迅和周树人是同一人,指的都是现代文学家周樟寿。"鲁迅"是他的笔名,"周树人"则是他的原名。所以,鲁迅并没有暴打周树人,这只是一种说法上的误解。',
|
|
238
|
+
createAt: 1690111354734,
|
|
239
|
+
id: '9bQW9hTs',
|
|
240
|
+
role: 'assistant',
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
content:
|
|
244
|
+
'这个问题基于一个误解。实际上,鲁迅和周树人是同一个人,这是中国现代文学的开山鼻祖周树人的笔名。他的本名是周树人,鲁迅则是他在30岁时开始使用的笔名。因此,鲁迅不能暴打周树人,因为他们是同一个人。',
|
|
245
|
+
createAt: 1690111364344,
|
|
246
|
+
id: 'HRQGSszU',
|
|
247
|
+
role: 'assistant',
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
content:
|
|
251
|
+
'这是一个误解。鲁迅和周树人是同一个人,是中国现代文学的奠基人。鲁迅是他的笔名,周树人是他的本名。所以,鲁迅不可能暴打周树人。这种说法可能是源于一些误解或误传。',
|
|
252
|
+
createAt: 1690111375456,
|
|
253
|
+
id: '981qr9n0',
|
|
254
|
+
role: 'assistant',
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
content: '鲁迅是谁',
|
|
258
|
+
createAt: 1690211354731,
|
|
259
|
+
id: 'ddd',
|
|
260
|
+
role: 'user',
|
|
261
|
+
},
|
|
262
|
+
]);
|
|
263
|
+
});
|
|
264
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { ChatMessage } from '@/types/chatMessage';
|
|
2
|
+
import { LobeAgentSession } from '@/types/session';
|
|
3
|
+
|
|
4
|
+
export const organizeChats = (
|
|
5
|
+
session: LobeAgentSession,
|
|
6
|
+
avatar: { assistant: string; user: string },
|
|
7
|
+
) => {
|
|
8
|
+
const basic = Object.values<ChatMessage>(session.chats)
|
|
9
|
+
// 首先按照时间顺序排序,越早的在越前面
|
|
10
|
+
.sort((pre, next) => pre.createAt - next.createAt)
|
|
11
|
+
// 过滤掉已归档的消息,归档消息不应该出现在聊天框中
|
|
12
|
+
.filter((m) => !m.archive)
|
|
13
|
+
// 映射头像关系
|
|
14
|
+
.map((m) => {
|
|
15
|
+
return {
|
|
16
|
+
...m,
|
|
17
|
+
meta:
|
|
18
|
+
m.role === 'assistant'
|
|
19
|
+
? {
|
|
20
|
+
avatar: avatar.assistant,
|
|
21
|
+
title: session.meta.title,
|
|
22
|
+
}
|
|
23
|
+
: m.role === 'user'
|
|
24
|
+
? {
|
|
25
|
+
avatar: avatar.user,
|
|
26
|
+
}
|
|
27
|
+
: m.meta,
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const finalList: ChatMessage[] = [];
|
|
32
|
+
|
|
33
|
+
const addItem = (item: ChatMessage) => {
|
|
34
|
+
const isExist = finalList.findIndex((i) => item.id === i.id) > -1;
|
|
35
|
+
if (!isExist) {
|
|
36
|
+
finalList.push(item);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// 基于添加逻辑进行重排序
|
|
41
|
+
for (const item of basic) {
|
|
42
|
+
// 先判存在与否,不存在就加入
|
|
43
|
+
addItem(item);
|
|
44
|
+
|
|
45
|
+
for (const another of basic) {
|
|
46
|
+
if (another.parentId === item.id) {
|
|
47
|
+
addItem(another);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return finalList;
|
|
53
|
+
};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { create } from 'zustand';
|
|
2
2
|
import { type PersistOptions, devtools, persist } from 'zustand/middleware';
|
|
3
3
|
|
|
4
|
+
import { isDev } from '@/utils/env';
|
|
5
|
+
|
|
4
6
|
import { type SettingsStore, createStore } from './store';
|
|
5
7
|
|
|
6
8
|
export const LOBE_SETTINGS = 'LOBE_SETTINGS';
|
|
@@ -13,7 +15,7 @@ const persistOptions: PersistOptions<SettingsStore> = {
|
|
|
13
15
|
export const useSettings = create<SettingsStore>()(
|
|
14
16
|
persist(
|
|
15
17
|
devtools(createStore, {
|
|
16
|
-
name: LOBE_SETTINGS,
|
|
18
|
+
name: LOBE_SETTINGS + (isDev ? '_DEV' : ''),
|
|
17
19
|
}),
|
|
18
20
|
persistOptions,
|
|
19
21
|
),
|
package/src/types/openai.ts
CHANGED
|
@@ -39,6 +39,10 @@ export interface OpenAIStreamPayload {
|
|
|
39
39
|
* @title 返回的文本数量
|
|
40
40
|
*/
|
|
41
41
|
n?: number;
|
|
42
|
+
/**
|
|
43
|
+
* 开启的插件列表
|
|
44
|
+
*/
|
|
45
|
+
plugins?: string[];
|
|
42
46
|
/**
|
|
43
47
|
* @title 控制生成文本中的惩罚系数,用于减少主题的变化
|
|
44
48
|
* @default 0
|
|
@@ -54,6 +58,7 @@ export interface OpenAIStreamPayload {
|
|
|
54
58
|
* @default 0.5
|
|
55
59
|
*/
|
|
56
60
|
temperature: number;
|
|
61
|
+
|
|
57
62
|
/**
|
|
58
63
|
* @title 控制生成文本中最高概率的单个令牌
|
|
59
64
|
* @default 1
|
package/src/types/session.ts
CHANGED
package/src/utils/env.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const isDev = process.env.NODE_ENV === 'development';
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import { ActionIcon, ChatInputArea, DraggablePanel, Icon, TokenTag, Tooltip } from '@lobehub/ui';
|
|
2
|
-
import { Button, Popconfirm } from 'antd';
|
|
3
|
-
import { encode } from 'gpt-tokenizer';
|
|
4
|
-
import { Archive, Eraser, Languages } from 'lucide-react';
|
|
5
|
-
import { memo, useMemo, useState } from 'react';
|
|
6
|
-
import { useTranslation } from 'react-i18next';
|
|
7
|
-
import { shallow } from 'zustand/shallow';
|
|
8
|
-
|
|
9
|
-
import { CHAT_TEXTAREA_HEIGHT, HEADER_HEIGHT } from '@/const/layoutTokens';
|
|
10
|
-
import { ModelTokens } from '@/const/modelTokens';
|
|
11
|
-
import { agentSelectors, chatSelectors, useSessionStore } from '@/store/session';
|
|
12
|
-
import { useSettings } from '@/store/settings';
|
|
13
|
-
|
|
14
|
-
const ChatInput = () => {
|
|
15
|
-
const { t } = useTranslation();
|
|
16
|
-
const [expand, setExpand] = useState<boolean>(false);
|
|
17
|
-
const [text, setText] = useState('');
|
|
18
|
-
const inputTokenCount = useMemo(() => encode(text).length, [text]);
|
|
19
|
-
|
|
20
|
-
const [inputHeight] = useSettings((s) => [s.inputHeight], shallow);
|
|
21
|
-
const [totalToken, systemRoleToken, chatsToken, model, sendMessage, clearMessage] =
|
|
22
|
-
useSessionStore(
|
|
23
|
-
(s) => [
|
|
24
|
-
chatSelectors.totalTokenCount(s),
|
|
25
|
-
chatSelectors.systemRoleTokenCount(s),
|
|
26
|
-
chatSelectors.chatsTokenCount(s),
|
|
27
|
-
agentSelectors.currentAgentModel(s),
|
|
28
|
-
s.createOrSendMsg,
|
|
29
|
-
s.clearMessage,
|
|
30
|
-
],
|
|
31
|
-
shallow,
|
|
32
|
-
);
|
|
33
|
-
|
|
34
|
-
return (
|
|
35
|
-
<DraggablePanel
|
|
36
|
-
expandable={false}
|
|
37
|
-
fullscreen={expand}
|
|
38
|
-
headerHeight={HEADER_HEIGHT}
|
|
39
|
-
minHeight={CHAT_TEXTAREA_HEIGHT}
|
|
40
|
-
onSizeChange={(_, size) => {
|
|
41
|
-
if (!size) return;
|
|
42
|
-
useSettings.setState({
|
|
43
|
-
inputHeight: typeof size.height === 'string' ? Number.parseInt(size.height) : size.height,
|
|
44
|
-
});
|
|
45
|
-
}}
|
|
46
|
-
placement="bottom"
|
|
47
|
-
size={{ height: inputHeight, width: '100%' }}
|
|
48
|
-
style={{ zIndex: 10 }}
|
|
49
|
-
>
|
|
50
|
-
<ChatInputArea
|
|
51
|
-
actions={
|
|
52
|
-
<>
|
|
53
|
-
<ActionIcon icon={Languages} />
|
|
54
|
-
<Popconfirm onConfirm={() => clearMessage()} title={t('confirmClearCurrentMessages')}>
|
|
55
|
-
<ActionIcon icon={Eraser} title={t('clearCurrentMessages')} />
|
|
56
|
-
</Popconfirm>
|
|
57
|
-
<Tooltip title={t('tokenDetail', { chatsToken, systemRoleToken })}>
|
|
58
|
-
<TokenTag maxValue={ModelTokens[model]} value={totalToken + inputTokenCount} />
|
|
59
|
-
</Tooltip>
|
|
60
|
-
</>
|
|
61
|
-
}
|
|
62
|
-
expand={expand}
|
|
63
|
-
footer={<Button icon={<Icon icon={Archive} title={t('archiveCurrentMessages')} />} />}
|
|
64
|
-
minHeight={CHAT_TEXTAREA_HEIGHT}
|
|
65
|
-
onExpandChange={setExpand}
|
|
66
|
-
onInputChange={setText}
|
|
67
|
-
onSend={sendMessage}
|
|
68
|
-
/>
|
|
69
|
-
</DraggablePanel>
|
|
70
|
-
);
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
export default memo(ChatInput);
|
|
File without changes
|