mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-19 22:00:34 +00:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 69dac2fc6e | |||
| 75c88425e0 | |||
| 30b2639c4b | |||
| 81867c413c | |||
| f1ffc2c60c | |||
| a9eadafd9f | |||
| 0e1146aa8d | |||
| 997ba159d2 | |||
| ab8aa0a485 | |||
| 00cba71e27 | |||
| 86544a8455 | |||
| f3c49cfd6a | |||
| b6eeba5850 | |||
| 963f7889de | |||
| 541f27591a | |||
| 3fb966cf37 | |||
| b30f55705f | |||
| fbe9ec0e48 | |||
| e5b4b60c3d |
@@ -2,6 +2,56 @@
|
||||
|
||||
# Changelog
|
||||
|
||||
### [Version 1.69.4](https://github.com/lobehub/lobe-chat/compare/v1.69.3...v1.69.4)
|
||||
|
||||
<sup>Released on **2025-03-09**</sup>
|
||||
|
||||
#### 🐛 Bug Fixes
|
||||
|
||||
- **misc**: Fix mistral can not chat.
|
||||
|
||||
<br/>
|
||||
|
||||
<details>
|
||||
<summary><kbd>Improvements and Fixes</kbd></summary>
|
||||
|
||||
#### What's fixed
|
||||
|
||||
- **misc**: Fix mistral can not chat, closes [#6828](https://github.com/lobehub/lobe-chat/issues/6828) ([00cba71](https://github.com/lobehub/lobe-chat/commit/00cba71))
|
||||
|
||||
</details>
|
||||
|
||||
<div align="right">
|
||||
|
||||
[](#readme-top)
|
||||
|
||||
</div>
|
||||
|
||||
### [Version 1.69.3](https://github.com/lobehub/lobe-chat/compare/v1.69.2...v1.69.3)
|
||||
|
||||
<sup>Released on **2025-03-08**</sup>
|
||||
|
||||
#### 💄 Styles
|
||||
|
||||
- **misc**: Add login ui for next-auth.
|
||||
|
||||
<br/>
|
||||
|
||||
<details>
|
||||
<summary><kbd>Improvements and Fixes</kbd></summary>
|
||||
|
||||
#### Styles
|
||||
|
||||
- **misc**: Add login ui for next-auth, closes [#6434](https://github.com/lobehub/lobe-chat/issues/6434) ([541f275](https://github.com/lobehub/lobe-chat/commit/541f275))
|
||||
|
||||
</details>
|
||||
|
||||
<div align="right">
|
||||
|
||||
[](#readme-top)
|
||||
|
||||
</div>
|
||||
|
||||
### [Version 1.69.2](https://github.com/lobehub/lobe-chat/compare/v1.69.1...v1.69.2)
|
||||
|
||||
<sup>Released on **2025-03-07**</sup>
|
||||
|
||||
@@ -1,4 +1,25 @@
|
||||
[
|
||||
{
|
||||
"children": {
|
||||
"fixes": ["Fix mistral can not chat."]
|
||||
},
|
||||
"date": "2025-03-09",
|
||||
"version": "1.69.4"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"improvements": ["Add login ui for next-auth."]
|
||||
},
|
||||
"date": "2025-03-08",
|
||||
"version": "1.69.3"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"improvements": ["Refactor the agent runtime implement."]
|
||||
},
|
||||
"date": "2025-03-07",
|
||||
"version": "1.69.2"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"improvements": ["Add Qwen QwQ model."]
|
||||
|
||||
+92
-1
@@ -170,6 +170,16 @@ show_message() {
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
tips_download_failed)
|
||||
case $LANGUAGE in
|
||||
zh_CN)
|
||||
echo "$2 下载失败,请检查网络连接。"
|
||||
;;
|
||||
*)
|
||||
echo "$2 Download failed, please check the network connection."
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
tips_already_installed)
|
||||
case $LANGUAGE in
|
||||
zh_CN)
|
||||
@@ -260,6 +270,30 @@ show_message() {
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
tips_no_docker_permission)
|
||||
case $LANGUAGE in
|
||||
zh_CN)
|
||||
echo "WARN: 看起来当前用户没有 Docker 权限。"
|
||||
echo "使用 'sudo usermod -aG docker $USER' 为用户分配 Docker 权限(可能需要重新启动 shell)。"
|
||||
;;
|
||||
*)
|
||||
echo "WARN: It look like the current user does not have Docker permissions."
|
||||
echo "Use 'sudo usermod -aG docker $USER' to assign Docker permissions to the user (may require restarting shell)."
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
tips_init_database_failed)
|
||||
case $LANGUAGE in
|
||||
zh_CN)
|
||||
echo "无法初始化数据库,为了避免你的数据重复初始化,请在首次成功启动时运行以下指令清空 Casdoor 初始配置文件:"
|
||||
echo "echo '{}' > init_data.json"
|
||||
;;
|
||||
*)
|
||||
echo "Failed to initialize the database. To avoid your data being initialized repeatedly, run the following command to unmount the initial configuration file of Casdoor when you first start successfully:"
|
||||
echo "echo '{}' > init_data.json"
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
ask_regenerate_secrets)
|
||||
case $LANGUAGE in
|
||||
zh_CN)
|
||||
@@ -320,12 +354,27 @@ show_message() {
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
ask_init_database)
|
||||
case $LANGUAGE in
|
||||
zh_CN)
|
||||
echo "是否初始化数据库?"
|
||||
;;
|
||||
*)
|
||||
echo "Do you want to initialize the database?"
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Function to download files
|
||||
download_file() {
|
||||
wget -q --show-progress "$1" -O "$2"
|
||||
wget --show-progress "$1" -O "$2"
|
||||
# If run failed, exit
|
||||
if [ $? -ne 0 ]; then
|
||||
show_message "tips_download_failed" "$2"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
print_centered() {
|
||||
@@ -629,12 +678,54 @@ section_regenerate_secrets() {
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
show_message "ask_regenerate_secrets"
|
||||
ask "(y/n)" "y"
|
||||
if [[ "$ask_result" == "y" ]]; then
|
||||
section_regenerate_secrets
|
||||
fi
|
||||
|
||||
section_init_database() {
|
||||
if ! command -v docker &> /dev/null ; then
|
||||
echo "docker" $(show_message "tips_no_executable")
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! docker compose &> /dev/null ; then
|
||||
echo "docker compose" $(show_message "tips_no_executable")
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check if user has permissions to run Docker by trying to get the status of Docker (docker status).
|
||||
# If this fails, the user probably does not have permissions for Docker.
|
||||
# ref: https://github.com/paperless-ngx/paperless-ngx/blob/89e5c08a1fe4ca0b7641ae8fbd5554502199ae40/install-paperless-ngx.sh#L64-L72
|
||||
if ! docker stats --no-stream &> /dev/null ; then
|
||||
echo $(show_message "tips_no_docker_permission")
|
||||
return 1
|
||||
fi
|
||||
|
||||
docker compose pull
|
||||
docker compose up --detach postgresql casdoor
|
||||
# hopefully enough time for even the slower systems
|
||||
sleep 15
|
||||
docker compose stop
|
||||
|
||||
# Init finished, remove init mount
|
||||
echo '{}' > init_data.json
|
||||
}
|
||||
|
||||
show_message "ask_init_database"
|
||||
ask "(y/n)" "y"
|
||||
if [[ "$ask_result" == "y" ]]; then
|
||||
# If return 1 means failed
|
||||
section_init_database
|
||||
if [ $? -ne 0 ]; then
|
||||
echo $(show_message "tips_init_database_failed")
|
||||
fi
|
||||
else
|
||||
show_message "tips_init_database_failed"
|
||||
fi
|
||||
|
||||
section_display_configurated_report() {
|
||||
# Display configuration reports
|
||||
echo $(show_message "security_secrect_regenerate_report")
|
||||
|
||||
@@ -27,6 +27,7 @@ Go to [Clerk](https://clerk.com?utm_source=lobehub\&utm_medium=docs) to register
|
||||
```shell
|
||||
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_live_xxxxxxxxxxx
|
||||
CLERK_SECRET_KEY=sk_live_xxxxxxxxxxxxxxxxxxxxxx
|
||||
NEXT_PUBLIC_ENABLE_NEXT_AUTH=0
|
||||
```
|
||||
|
||||
### Create and Configure Webhook in Clerk
|
||||
|
||||
@@ -25,6 +25,7 @@ const nextConfig: NextConfig = {
|
||||
'@lobehub/ui',
|
||||
'gpt-tokenizer',
|
||||
],
|
||||
reactCompiler: true,
|
||||
webVitalsAttribution: ['CLS', 'LCP'],
|
||||
webpackMemoryOptimizations: true,
|
||||
},
|
||||
|
||||
+5
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@lobehub/chat",
|
||||
"version": "1.69.2",
|
||||
"version": "1.69.4",
|
||||
"description": "Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
|
||||
"keywords": [
|
||||
"framework",
|
||||
@@ -122,7 +122,7 @@
|
||||
"@cyntler/react-doc-viewer": "^1.17.0",
|
||||
"@electric-sql/pglite": "0.2.13",
|
||||
"@google-cloud/vertexai": "^1.9.2",
|
||||
"@google/generative-ai": "^0.22.0",
|
||||
"@google/generative-ai": "^0.24.0",
|
||||
"@huggingface/inference": "^2.8.1",
|
||||
"@icons-pack/react-simple-icons": "9.6.0",
|
||||
"@khmyznikov/pwa-install": "0.3.9",
|
||||
@@ -165,7 +165,7 @@
|
||||
"epub2": "^3.0.2",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"file-type": "^20.0.0",
|
||||
"framer-motion": "^11.16.0",
|
||||
"framer-motion": "^12.0.0",
|
||||
"gpt-tokenizer": "^2.8.1",
|
||||
"html-to-text": "^9.0.5",
|
||||
"i18next": "^24.2.1",
|
||||
@@ -180,7 +180,7 @@
|
||||
"langfuse": "3.29.1",
|
||||
"langfuse-core": "3.29.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lucide-react": "^0.477.0",
|
||||
"lucide-react": "^0.479.0",
|
||||
"mammoth": "^1.9.0",
|
||||
"mdast-util-to-markdown": "^2.1.2",
|
||||
"modern-screenshot": "^4.5.5",
|
||||
@@ -286,6 +286,7 @@
|
||||
"@types/ws": "^8.5.13",
|
||||
"@vitest/coverage-v8": "~1.2.2",
|
||||
"ajv-keywords": "^5.1.0",
|
||||
"babel-plugin-react-compiler": "19.0.0-beta-21e868a-20250216",
|
||||
"commitlint": "^19.6.1",
|
||||
"consola": "^3.3.3",
|
||||
"crypto-js": "^4.2.0",
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"@mozilla/readability": "^0.5.0",
|
||||
"happy-dom": "^17.0.0",
|
||||
"node-html-markdown": "^1.3.0",
|
||||
"query-string": "^9.1.1"
|
||||
"query-string": "^9.1.1",
|
||||
"url-join": "^5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { browserless } from '../browserless';
|
||||
|
||||
describe('browserless', () => {
|
||||
it('should throw BrowserlessInitError when env vars not set', async () => {
|
||||
const originalEnv = { ...process.env };
|
||||
process.env = { ...originalEnv };
|
||||
delete process.env.BROWSERLESS_URL;
|
||||
delete process.env.BROWSERLESS_TOKEN;
|
||||
|
||||
await expect(browserless('https://example.com', { filterOptions: {} })).rejects.toThrow(
|
||||
'`BROWSERLESS_URL` or `BROWSERLESS_TOKEN` are required',
|
||||
);
|
||||
|
||||
process.env = originalEnv;
|
||||
});
|
||||
|
||||
it('should return undefined on fetch error', async () => {
|
||||
process.env.BROWSERLESS_TOKEN = 'test-token';
|
||||
global.fetch = vi.fn().mockRejectedValue(new Error('Fetch error'));
|
||||
|
||||
const result = await browserless('https://example.com', { filterOptions: {} });
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined when content is empty', async () => {
|
||||
process.env.BROWSERLESS_TOKEN = 'test-token';
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
text: vi.fn().mockResolvedValue('<html></html>'),
|
||||
} as any);
|
||||
|
||||
const result = await browserless('https://example.com', { filterOptions: {} });
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined when title is "Just a moment..."', async () => {
|
||||
process.env.BROWSERLESS_TOKEN = 'test-token';
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
text: vi.fn().mockResolvedValue('<html><title>Just a moment...</title></html>'),
|
||||
} as any);
|
||||
|
||||
const result = await browserless('https://example.com', { filterOptions: {} });
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return crawl result on successful fetch', async () => {
|
||||
process.env.BROWSERLESS_TOKEN = 'test-token';
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
text: vi.fn().mockResolvedValue(`
|
||||
<html>
|
||||
<head>
|
||||
<title>Test Title</title>
|
||||
<meta name="description" content="Test Description">
|
||||
</head>
|
||||
<body>
|
||||
<h1>Test Content</h1>
|
||||
</body>
|
||||
</html>
|
||||
`),
|
||||
} as any);
|
||||
|
||||
const result = await browserless('https://example.com', { filterOptions: {} });
|
||||
|
||||
expect(result).toEqual({
|
||||
content: expect.any(String),
|
||||
contentType: 'text',
|
||||
description: expect.any(String),
|
||||
length: expect.any(Number),
|
||||
siteName: null,
|
||||
title: 'Test Title',
|
||||
url: 'https://example.com',
|
||||
});
|
||||
});
|
||||
|
||||
it('should use correct URL when BROWSERLESS_URL is provided', async () => {
|
||||
const customUrl = 'https://custom.browserless.io';
|
||||
const originalEnv = { ...process.env };
|
||||
process.env.BROWSERLESS_TOKEN = 'test-token';
|
||||
process.env.BROWSERLESS_URL = customUrl;
|
||||
global.fetch = vi.fn().mockImplementation((url) => {
|
||||
expect(url).toContain(customUrl);
|
||||
return Promise.resolve({
|
||||
text: () => Promise.resolve('<html><title>Test</title></html>'),
|
||||
});
|
||||
});
|
||||
|
||||
await browserless('https://example.com', { filterOptions: {} });
|
||||
|
||||
expect(global.fetch).toHaveBeenCalled();
|
||||
|
||||
process.env = originalEnv;
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import qs from 'query-string';
|
||||
import urlJoin from 'url-join';
|
||||
|
||||
import { CrawlImpl, CrawlSuccessResult } from '../type';
|
||||
import { htmlToMarkdown } from '../utils/htmlToMarkdown';
|
||||
@@ -25,7 +26,7 @@ export const browserless: CrawlImpl = async (url, { filterOptions }) => {
|
||||
|
||||
try {
|
||||
const res = await fetch(
|
||||
qs.stringifyUrl({ query: { token: BROWSERLESS_TOKEN }, url: `${BASE_URL}/content` }),
|
||||
qs.stringifyUrl({ query: { token: BROWSERLESS_TOKEN }, url: urlJoin(BASE_URL, '/content') }),
|
||||
{
|
||||
body: JSON.stringify(input),
|
||||
headers: {
|
||||
|
||||
@@ -61,4 +61,10 @@ export const crawUrlRules: CrawlUrlRule[] = [
|
||||
},
|
||||
urlPattern: 'https://www.qiumiwu.com/standings/(.*)',
|
||||
},
|
||||
|
||||
// mozilla use jina
|
||||
{
|
||||
impls: ['jina'],
|
||||
urlPattern: 'https://developer.mozilla.org(.*)',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
'use client';
|
||||
|
||||
import { LobeChat } from '@lobehub/ui/brand';
|
||||
import { Button, Col, Flex, Row, Skeleton, Typography } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { AuthError } from 'next-auth';
|
||||
import { signIn } from 'next-auth/react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import BrandWatermark from '@/components/BrandWatermark';
|
||||
import AuthIcons from '@/components/NextAuth/AuthIcons';
|
||||
import { DOCUMENTS_REFER_URL, PRIVACY_URL, TERMS_URL } from '@/const/url';
|
||||
import { useUserStore } from '@/store/user';
|
||||
|
||||
const { Title, Paragraph } = Typography;
|
||||
|
||||
const useStyles = createStyles(({ css, token }) => ({
|
||||
button: css`
|
||||
text-transform: capitalize;
|
||||
`,
|
||||
container: css`
|
||||
min-width: 360px;
|
||||
border: 1px solid ${token.colorBorder};
|
||||
border-radius: ${token.borderRadiusLG}px;
|
||||
background: ${token.colorBgContainer};
|
||||
`,
|
||||
contentCard: css`
|
||||
padding-block: 2.5rem;
|
||||
padding-inline: 2rem;
|
||||
`,
|
||||
description: css`
|
||||
margin: 0;
|
||||
color: ${token.colorTextSecondary};
|
||||
`,
|
||||
footer: css`
|
||||
padding: 1rem;
|
||||
border-block-start: 1px solid ${token.colorBorder};
|
||||
border-radius: 0 0 8px 8px;
|
||||
|
||||
color: ${token.colorTextDescription};
|
||||
|
||||
background: ${token.colorBgElevated};
|
||||
`,
|
||||
text: css`
|
||||
text-align: center;
|
||||
`,
|
||||
title: css`
|
||||
margin: 0;
|
||||
color: ${token.colorTextHeading};
|
||||
`,
|
||||
}));
|
||||
|
||||
const BtnListLoading = memo(() => {
|
||||
return (
|
||||
<Flex gap={'small'} vertical>
|
||||
<Skeleton.Button active style={{ minWidth: 300 }} />
|
||||
<Skeleton.Button active style={{ minWidth: 300 }} />
|
||||
<Skeleton.Button active style={{ minWidth: 300 }} />
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Follow the implementation from AuthJS official documentation,
|
||||
* but using client components.
|
||||
* ref: https://authjs.dev/guides/pages/signin
|
||||
*/
|
||||
export default memo(() => {
|
||||
const { styles } = useStyles();
|
||||
const { t } = useTranslation('clerk');
|
||||
const router = useRouter();
|
||||
|
||||
const oAuthSSOProviders = useUserStore((s) => s.oAuthSSOProviders);
|
||||
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
// Redirect back to the page url
|
||||
const callbackUrl = searchParams.get('callbackUrl') ?? '';
|
||||
|
||||
const handleSignIn = async (provider: string) => {
|
||||
try {
|
||||
await signIn(provider, { redirectTo: callbackUrl });
|
||||
} catch (error) {
|
||||
// Signin can fail for a number of reasons, such as the user
|
||||
// not existing, or the user not having the correct role.
|
||||
// In some cases, you may want to redirect to a custom error
|
||||
if (error instanceof AuthError) {
|
||||
return router.push(`/next-auth/?error=${error.type}`);
|
||||
}
|
||||
|
||||
// Otherwise if a redirects happens Next.js can handle it
|
||||
// so you can just re-thrown the error and let Next.js handle it.
|
||||
// Docs: https://nextjs.org/docs/app/api-reference/functions/redirect#server-component
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const footerBtns = [
|
||||
{ href: DOCUMENTS_REFER_URL, id: 0, label: t('footerPageLink__help') },
|
||||
{ href: PRIVACY_URL, id: 1, label: t('footerPageLink__privacy') },
|
||||
{ href: TERMS_URL, id: 2, label: t('footerPageLink__terms') },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.contentCard}>
|
||||
{/* Card Body */}
|
||||
<Flex gap="large" vertical>
|
||||
{/* Header */}
|
||||
<div className={styles.text}>
|
||||
<Title className={styles.title} level={4}>
|
||||
<div>
|
||||
<LobeChat size={48} />
|
||||
</div>
|
||||
{t('signIn.start.title', { applicationName: 'LobeChat' })}
|
||||
</Title>
|
||||
<Paragraph className={styles.description}>{t('signIn.start.subtitle')}</Paragraph>
|
||||
</div>
|
||||
{/* Content */}
|
||||
<Flex gap="small" vertical>
|
||||
{oAuthSSOProviders ? (
|
||||
oAuthSSOProviders.map((provider) => (
|
||||
<Button
|
||||
className={styles.button}
|
||||
icon={AuthIcons(provider, 16)}
|
||||
key={provider}
|
||||
onClick={() => handleSignIn(provider)}
|
||||
>
|
||||
{provider}
|
||||
</Button>
|
||||
))
|
||||
) : (
|
||||
<BtnListLoading />
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</div>
|
||||
<div className={styles.footer}>
|
||||
{/* Footer */}
|
||||
<Row>
|
||||
<Col span={12}>
|
||||
<Flex justify="left" style={{ height: '100%' }}>
|
||||
<BrandWatermark />
|
||||
</Flex>
|
||||
</Col>
|
||||
<Col offset={4} span={8}>
|
||||
<Flex justify="right">
|
||||
{footerBtns.map((btn) => (
|
||||
<Button key={btn.id} onClick={() => router.push(btn.href)} size="small" type="text">
|
||||
{btn.label}
|
||||
</Button>
|
||||
))}
|
||||
</Flex>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Suspense } from 'react';
|
||||
|
||||
import Loading from '@/components/Loading/BrandTextLoading';
|
||||
|
||||
import AuthSignInBox from './AuthSignInBox';
|
||||
|
||||
export default () => (
|
||||
<Suspense fallback={<Loading />}>
|
||||
<AuthSignInBox />
|
||||
</Suspense>
|
||||
);
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
'use client';
|
||||
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { useSendMessage } from '@/features/ChatInput/useSend';
|
||||
import { useChatStore } from '@/store/chat';
|
||||
|
||||
const MessageFromUrl = () => {
|
||||
const updateInputMessage = useChatStore((s) => s.updateInputMessage);
|
||||
const { send: sendMessage } = useSendMessage();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
useEffect(() => {
|
||||
const message = searchParams.get('message');
|
||||
if (message) {
|
||||
// Remove message from URL
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
params.delete('message');
|
||||
const newUrl = `${window.location.pathname}?${params.toString()}`;
|
||||
window.history.replaceState({}, '', newUrl);
|
||||
|
||||
updateInputMessage(message);
|
||||
sendMessage();
|
||||
}
|
||||
}, [searchParams, updateInputMessage, sendMessage]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default MessageFromUrl;
|
||||
+45
-39
@@ -1,7 +1,7 @@
|
||||
import { Button, Space } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { rgba } from 'polished';
|
||||
import { memo, useEffect, useState } from 'react';
|
||||
import { Suspense, memo, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
@@ -13,6 +13,7 @@ import { useChatStore } from '@/store/chat';
|
||||
import { chatSelectors } from '@/store/chat/selectors';
|
||||
import { isMacOS } from '@/utils/platform';
|
||||
|
||||
import MessageFromUrl from './MessageFromUrl';
|
||||
import SendMore from './SendMore';
|
||||
import ShortcutHint from './ShortcutHint';
|
||||
|
||||
@@ -67,49 +68,54 @@ const Footer = memo<FooterProps>(({ onExpandChange, expand }) => {
|
||||
}, [setIsMac]);
|
||||
|
||||
return (
|
||||
<Flexbox
|
||||
align={'end'}
|
||||
className={styles.overrideAntdIcon}
|
||||
distribution={'space-between'}
|
||||
flex={'none'}
|
||||
gap={8}
|
||||
horizontal
|
||||
padding={'0 24px'}
|
||||
>
|
||||
<Flexbox align={'center'} gap={8} horizontal style={{ overflow: 'hidden' }}>
|
||||
{expand && <LocalFiles />}
|
||||
</Flexbox>
|
||||
<Flexbox align={'center'} flex={'none'} gap={8} horizontal>
|
||||
<ShortcutHint />
|
||||
<SaveTopic />
|
||||
<Flexbox style={{ minWidth: 92 }}>
|
||||
{isAIGenerating ? (
|
||||
<Button
|
||||
className={styles.loadingButton}
|
||||
icon={<StopLoadingIcon />}
|
||||
onClick={stopGenerateMessage}
|
||||
>
|
||||
{t('input.stop')}
|
||||
</Button>
|
||||
) : (
|
||||
<Space.Compact>
|
||||
<>
|
||||
<Suspense fallback={null}>
|
||||
<MessageFromUrl />
|
||||
</Suspense>
|
||||
<Flexbox
|
||||
align={'end'}
|
||||
className={styles.overrideAntdIcon}
|
||||
distribution={'space-between'}
|
||||
flex={'none'}
|
||||
gap={8}
|
||||
horizontal
|
||||
padding={'0 24px'}
|
||||
>
|
||||
<Flexbox align={'center'} gap={8} horizontal style={{ overflow: 'hidden' }}>
|
||||
{expand && <LocalFiles />}
|
||||
</Flexbox>
|
||||
<Flexbox align={'center'} flex={'none'} gap={8} horizontal>
|
||||
<ShortcutHint />
|
||||
<SaveTopic />
|
||||
<Flexbox style={{ minWidth: 92 }}>
|
||||
{isAIGenerating ? (
|
||||
<Button
|
||||
disabled={!canSend}
|
||||
loading={!canSend}
|
||||
onClick={() => {
|
||||
sendMessage();
|
||||
onExpandChange?.(false);
|
||||
}}
|
||||
type={'primary'}
|
||||
className={styles.loadingButton}
|
||||
icon={<StopLoadingIcon />}
|
||||
onClick={stopGenerateMessage}
|
||||
>
|
||||
{t('input.send')}
|
||||
{t('input.stop')}
|
||||
</Button>
|
||||
<SendMore disabled={!canSend} isMac={isMac} />
|
||||
</Space.Compact>
|
||||
)}
|
||||
) : (
|
||||
<Space.Compact>
|
||||
<Button
|
||||
disabled={!canSend}
|
||||
loading={!canSend}
|
||||
onClick={() => {
|
||||
sendMessage();
|
||||
onExpandChange?.(false);
|
||||
}}
|
||||
type={'primary'}
|
||||
>
|
||||
{t('input.send')}
|
||||
</Button>
|
||||
<SendMore disabled={!canSend} isMac={isMac} />
|
||||
</Space.Compact>
|
||||
)}
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import { userService } from '@/services/user';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { userProfileSelectors } from '@/store/user/selectors';
|
||||
|
||||
import AuthIcons from './AuthIcons';
|
||||
import AuthIcons from '@/components/NextAuth/AuthIcons';
|
||||
|
||||
const { Item } = List;
|
||||
|
||||
|
||||
+8
-6
@@ -12,10 +12,6 @@ import {
|
||||
} from '@lobehub/ui/icons';
|
||||
import React from 'react';
|
||||
|
||||
const iconProps = {
|
||||
size: 32,
|
||||
};
|
||||
|
||||
const iconComponents: { [key: string]: React.ElementType } = {
|
||||
'auth0': Auth0,
|
||||
'authelia': Authelia.Color,
|
||||
@@ -29,9 +25,15 @@ const iconComponents: { [key: string]: React.ElementType } = {
|
||||
'zitadel': Zitadel.Color,
|
||||
};
|
||||
|
||||
const AuthIcons = (id: string) => {
|
||||
/**
|
||||
* Get the auth icons component for the given id
|
||||
* @param id
|
||||
* @param size default is 36
|
||||
* @returns
|
||||
*/
|
||||
const AuthIcons = (id: string, size = 36) => {
|
||||
const IconComponent = iconComponents[id] || iconComponents.default;
|
||||
return <IconComponent {...iconProps} />;
|
||||
return <IconComponent size={size}/>;
|
||||
};
|
||||
|
||||
export default AuthIcons;
|
||||
@@ -137,6 +137,31 @@ const openrouterChatModels: AIChatModelCard[] = [
|
||||
releasedAt: '2024-06-20',
|
||||
type: 'chat',
|
||||
},
|
||||
{
|
||||
abilities: {
|
||||
functionCall: true,
|
||||
reasoning: true,
|
||||
vision: true,
|
||||
},
|
||||
contextWindowTokens: 200_000,
|
||||
description:
|
||||
'Claude 3.7 Sonnet 是 Anthropic 迄今为止最智能的模型,也是市场上首个混合推理模型。Claude 3.7 Sonnet 可以产生近乎即时的响应或延长的逐步思考,用户可以清晰地看到这些过程。Sonnet 特别擅长编程、数据科学、视觉处理、代理任务。',
|
||||
displayName: 'Claude 3.7 Sonnet',
|
||||
enabled: true,
|
||||
id: 'anthropic/claude-3.7-sonnet',
|
||||
maxOutput: 8192,
|
||||
pricing: {
|
||||
cachedInput: 0.3,
|
||||
input: 3,
|
||||
output: 15,
|
||||
writeCacheInput: 3.75,
|
||||
},
|
||||
releasedAt: '2025-02-24',
|
||||
settings: {
|
||||
extendParams: ['enableReasoning', 'reasoningBudgetToken'],
|
||||
},
|
||||
type: 'chat',
|
||||
},
|
||||
{
|
||||
abilities: {
|
||||
functionCall: true,
|
||||
@@ -258,7 +283,7 @@ const openrouterChatModels: AIChatModelCard[] = [
|
||||
id: 'deepseek/deepseek-r1:free',
|
||||
releasedAt: '2025-01-20',
|
||||
type: 'chat',
|
||||
},
|
||||
},
|
||||
{
|
||||
abilities: {
|
||||
vision: true,
|
||||
|
||||
@@ -112,12 +112,6 @@ const ModelCard = memo<ModelCardProps>(({ pricing, id, provider, displayName })
|
||||
</Flexbox>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip title={t('messages.modelCard.pricing.inputTokens', { amount: inputPrice })}>
|
||||
<Flexbox gap={2} horizontal>
|
||||
<Icon icon={ArrowUpFromDot} />
|
||||
{inputPrice}
|
||||
</Flexbox>
|
||||
</Tooltip>
|
||||
{pricing?.writeCacheInput && (
|
||||
<Tooltip
|
||||
title={t('messages.modelCard.pricing.writeCacheInputTokens', {
|
||||
@@ -130,6 +124,12 @@ const ModelCard = memo<ModelCardProps>(({ pricing, id, provider, displayName })
|
||||
</Flexbox>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip title={t('messages.modelCard.pricing.inputTokens', { amount: inputPrice })}>
|
||||
<Flexbox gap={2} horizontal>
|
||||
<Icon icon={ArrowUpFromDot} />
|
||||
{inputPrice}
|
||||
</Flexbox>
|
||||
</Tooltip>
|
||||
<Tooltip title={t('messages.modelCard.pricing.outputTokens', { amount: outputPrice })}>
|
||||
<Flexbox gap={2} horizontal>
|
||||
<Icon icon={ArrowDownToDot} />
|
||||
|
||||
@@ -138,7 +138,7 @@ const TokenDetail = memo<TokenDetailProps>(({ usage, model, provider }) => {
|
||||
</Flexbox>
|
||||
)}
|
||||
{outputDetails.length > 1 && (
|
||||
<>
|
||||
<Flexbox gap={4}>
|
||||
<Flexbox
|
||||
align={'center'}
|
||||
gap={4}
|
||||
@@ -146,12 +146,12 @@ const TokenDetail = memo<TokenDetailProps>(({ usage, model, provider }) => {
|
||||
justify={'space-between'}
|
||||
width={'100%'}
|
||||
>
|
||||
<div style={{ color: theme.colorTextDescription }}>
|
||||
<div style={{ color: theme.colorTextDescription, fontSize: 12 }}>
|
||||
{t('messages.tokenDetails.outputTitle')}
|
||||
</div>
|
||||
</Flexbox>
|
||||
<TokenProgress data={outputDetails} showIcon />
|
||||
</>
|
||||
</Flexbox>
|
||||
)}
|
||||
<Flexbox>
|
||||
<TokenProgress data={totalDetail} showIcon />
|
||||
|
||||
@@ -57,9 +57,6 @@ describe('specific LobeMistralAI tests', () => {
|
||||
messages: [{ content: 'Hello', role: 'user' }],
|
||||
model: 'open-mistral-7b',
|
||||
stream: true,
|
||||
stream_options: {
|
||||
include_usage: true,
|
||||
},
|
||||
temperature: 0.35,
|
||||
top_p: 1,
|
||||
},
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { ChatModelCard } from '@/types/llm';
|
||||
|
||||
import { ModelProvider } from '../types';
|
||||
import { LobeOpenAICompatibleFactory } from '../utils/openaiCompatibleFactory';
|
||||
|
||||
import type { ChatModelCard } from '@/types/llm';
|
||||
|
||||
export interface MistralModelCard {
|
||||
capabilities: {
|
||||
function_calling: boolean;
|
||||
@@ -16,6 +16,9 @@ export interface MistralModelCard {
|
||||
export const LobeMistralAI = LobeOpenAICompatibleFactory({
|
||||
baseURL: 'https://api.mistral.ai/v1',
|
||||
chatCompletion: {
|
||||
// Mistral API does not support stream_options: { include_usage: true }
|
||||
// refs: https://github.com/lobehub/lobe-chat/issues/6825
|
||||
excludeUsage: true,
|
||||
handlePayload: (payload) => ({
|
||||
...(payload.max_tokens !== undefined && { max_tokens: payload.max_tokens }),
|
||||
messages: payload.messages as any,
|
||||
@@ -33,12 +36,14 @@ export const LobeMistralAI = LobeOpenAICompatibleFactory({
|
||||
models: async ({ client }) => {
|
||||
const { LOBE_DEFAULT_MODEL_LIST } = await import('@/config/aiModels');
|
||||
|
||||
const modelsPage = await client.models.list() as any;
|
||||
const modelsPage = (await client.models.list()) as any;
|
||||
const modelList: MistralModelCard[] = modelsPage.data;
|
||||
|
||||
return modelList
|
||||
.map((model) => {
|
||||
const knownModel = LOBE_DEFAULT_MODEL_LIST.find((m) => model.id.toLowerCase() === m.id.toLowerCase());
|
||||
const knownModel = LOBE_DEFAULT_MODEL_LIST.find(
|
||||
(m) => model.id.toLowerCase() === m.id.toLowerCase(),
|
||||
);
|
||||
|
||||
return {
|
||||
contextWindowTokens: model.max_context_length,
|
||||
@@ -47,9 +52,7 @@ export const LobeMistralAI = LobeOpenAICompatibleFactory({
|
||||
enabled: knownModel?.enabled || false,
|
||||
functionCall: model.capabilities.function_calling,
|
||||
id: model.id,
|
||||
reasoning:
|
||||
knownModel?.abilities?.reasoning
|
||||
|| false,
|
||||
reasoning: knownModel?.abilities?.reasoning || false,
|
||||
vision: model.capabilities.vision,
|
||||
};
|
||||
})
|
||||
|
||||
@@ -92,6 +92,39 @@ describe('LobeOpenRouterAI', () => {
|
||||
expect(result).toBeInstanceOf(Response);
|
||||
});
|
||||
|
||||
it('should add reasoning field when thinking is enabled', async () => {
|
||||
// Arrange
|
||||
const mockStream = new ReadableStream();
|
||||
const mockResponse = Promise.resolve(mockStream);
|
||||
|
||||
(instance['client'].chat.completions.create as Mock).mockResolvedValue(mockResponse);
|
||||
|
||||
// Act
|
||||
const result = await instance.chat({
|
||||
messages: [{ content: 'Hello', role: 'user' }],
|
||||
model: 'mistralai/mistral-7b-instruct:free',
|
||||
temperature: 0.7,
|
||||
thinking: {
|
||||
type: 'enabled',
|
||||
budget_tokens: 1500,
|
||||
},
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
messages: [{ content: 'Hello', role: 'user' }],
|
||||
model: 'mistralai/mistral-7b-instruct:free',
|
||||
reasoning: {
|
||||
max_tokens: 1500,
|
||||
},
|
||||
temperature: 0.7,
|
||||
}),
|
||||
{ headers: { Accept: '*/*' } },
|
||||
);
|
||||
expect(result).toBeInstanceOf(Response);
|
||||
});
|
||||
|
||||
describe('Error', () => {
|
||||
it('should return OpenRouterBizError with an openai error response when OpenAI.APIError is thrown', async () => {
|
||||
// Arrange
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { ChatModelCard } from '@/types/llm';
|
||||
|
||||
import { ModelProvider } from '../types';
|
||||
import { LobeOpenAICompatibleFactory } from '../utils/openaiCompatibleFactory';
|
||||
import { OpenRouterModelCard, OpenRouterModelExtraInfo } from './type';
|
||||
import { OpenRouterModelCard, OpenRouterModelExtraInfo, OpenRouterReasoning } from './type';
|
||||
|
||||
const formatPrice = (price: string) => {
|
||||
if (price === '-1') return undefined;
|
||||
@@ -13,10 +13,19 @@ export const LobeOpenRouterAI = LobeOpenAICompatibleFactory({
|
||||
baseURL: 'https://openrouter.ai/api/v1',
|
||||
chatCompletion: {
|
||||
handlePayload: (payload) => {
|
||||
const { thinking } = payload;
|
||||
|
||||
let reasoning: OpenRouterReasoning = {};
|
||||
if (thinking?.type === 'enabled') {
|
||||
reasoning = {
|
||||
max_tokens: thinking.budget_tokens,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...payload,
|
||||
include_reasoning: true,
|
||||
model: payload.enabledSearch ? `${payload.model}:online` : payload.model,
|
||||
reasoning,
|
||||
stream: payload.stream ?? true,
|
||||
} as any;
|
||||
},
|
||||
|
||||
@@ -37,3 +37,22 @@ export interface OpenRouterModelExtraInfo {
|
||||
endpoint?: OpenRouterModelEndpoint;
|
||||
slug: string;
|
||||
}
|
||||
|
||||
interface OpenRouterOpenAIReasoning {
|
||||
effort: 'high' | 'medium' | 'low';
|
||||
exclude?: boolean;
|
||||
}
|
||||
|
||||
interface OpenRouterAnthropicReasoning {
|
||||
exclude?: boolean;
|
||||
max_tokens: number;
|
||||
}
|
||||
|
||||
interface OpenRouterCommonReasoning {
|
||||
exclude?: boolean;
|
||||
}
|
||||
|
||||
export type OpenRouterReasoning =
|
||||
| OpenRouterOpenAIReasoning
|
||||
| OpenRouterAnthropicReasoning
|
||||
| OpenRouterCommonReasoning;
|
||||
|
||||
@@ -35,7 +35,7 @@ export const debugStream = async (stream: ReadableStream) => {
|
||||
|
||||
console.log(`[chunk ${chunk}] ${getTime()}`);
|
||||
console.log(chunkValue);
|
||||
console.log(`\n`);
|
||||
console.log('');
|
||||
|
||||
finished = done;
|
||||
chunk++;
|
||||
|
||||
@@ -57,6 +57,7 @@ interface OpenAICompatibleFactoryOptions<T extends Record<string, any> = any> {
|
||||
apiKey?: string;
|
||||
baseURL?: string;
|
||||
chatCompletion?: {
|
||||
excludeUsage?: boolean;
|
||||
handleError?: (
|
||||
error: any,
|
||||
options: ConstructorOptions<T>,
|
||||
@@ -224,12 +225,17 @@ export const LobeOpenAICompatibleFactory = <T extends Record<string, any> = any>
|
||||
...postPayload,
|
||||
messages,
|
||||
...(chatCompletion?.noUserId ? {} : { user: options?.user }),
|
||||
stream_options: postPayload.stream ? { include_usage: true } : undefined,
|
||||
stream_options:
|
||||
postPayload.stream && !chatCompletion?.excludeUsage
|
||||
? { include_usage: true }
|
||||
: undefined,
|
||||
};
|
||||
|
||||
if (debug?.chatCompletion?.()) {
|
||||
console.log('[requestPayload]:', JSON.stringify(finalPayload, null, 2));
|
||||
console.log('[requestPayload]');
|
||||
console.log(JSON.stringify(finalPayload), '\n');
|
||||
}
|
||||
|
||||
response = await this.client.chat.completions.create(finalPayload, {
|
||||
// https://github.com/lobehub/lobe-chat/pull/318
|
||||
headers: { Accept: '*/*', ...options?.requestHeaders },
|
||||
|
||||
@@ -42,6 +42,7 @@ export default {
|
||||
debug: authEnv.NEXT_AUTH_DEBUG,
|
||||
pages: {
|
||||
error: '/next-auth/error',
|
||||
signIn: '/next-auth/signin',
|
||||
},
|
||||
providers: initSSOProviders(),
|
||||
secret: authEnv.NEXT_AUTH_SECRET,
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ export const config = {
|
||||
|
||||
'/login(.*)',
|
||||
'/signup(.*)',
|
||||
'/next-auth/error',
|
||||
'/next-auth/(.*)',
|
||||
// ↓ cloud ↓
|
||||
],
|
||||
};
|
||||
|
||||
+19
-13
@@ -17,13 +17,19 @@ import {
|
||||
} from '@/libs/agent-runtime';
|
||||
import { filesPrompts } from '@/prompts/files';
|
||||
import { BuiltinSystemRolePrompts } from '@/prompts/systemRole';
|
||||
import { aiModelSelectors, aiProviderSelectors, useAiInfraStore } from '@/store/aiInfra';
|
||||
import { getAgentChatConfig } from '@/store/chat/slices/aiChat/actions/helpers';
|
||||
import { useSessionStore } from '@/store/session';
|
||||
import { getAgentStoreState } from '@/store/agent';
|
||||
import { agentChatConfigSelectors } from '@/store/agent/selectors';
|
||||
import {
|
||||
aiModelSelectors,
|
||||
aiProviderSelectors,
|
||||
getAiInfraStoreState,
|
||||
useAiInfraStore,
|
||||
} from '@/store/aiInfra';
|
||||
import { getSessionStoreState } from '@/store/session';
|
||||
import { sessionMetaSelectors } from '@/store/session/selectors';
|
||||
import { useToolStore } from '@/store/tool';
|
||||
import { getToolStoreState } from '@/store/tool';
|
||||
import { pluginSelectors, toolSelectors } from '@/store/tool/selectors';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { getUserStoreState, useUserStore } from '@/store/user';
|
||||
import {
|
||||
modelConfigSelectors,
|
||||
modelProviderSelectors,
|
||||
@@ -46,10 +52,10 @@ import { API_ENDPOINTS } from './_url';
|
||||
const isCanUseFC = (model: string, provider: string) => {
|
||||
// TODO: remove isDeprecatedEdition condition in V2.0
|
||||
if (isDeprecatedEdition) {
|
||||
return modelProviderSelectors.isModelEnabledFunctionCall(model)(useUserStore.getState());
|
||||
return modelProviderSelectors.isModelEnabledFunctionCall(model)(getUserStoreState());
|
||||
}
|
||||
|
||||
return aiModelSelectors.isModelSupportToolUse(model, provider)(useAiInfraStore.getState());
|
||||
return aiModelSelectors.isModelSupportToolUse(model, provider)(getAiInfraStoreState());
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -169,7 +175,7 @@ class ChatService {
|
||||
);
|
||||
|
||||
// =================== 0. process search =================== //
|
||||
const chatConfig = getAgentChatConfig();
|
||||
const chatConfig = agentChatConfigSelectors.currentChatConfig(getAgentStoreState());
|
||||
|
||||
const enabledSearch = chatConfig.searchMode !== 'off';
|
||||
const isModelHasBuiltinSearch = aiModelSelectors.isModelHasBuiltinSearch(
|
||||
@@ -200,7 +206,7 @@ class ChatService {
|
||||
|
||||
// ============ 2. preprocess tools ============ //
|
||||
|
||||
let filterTools = toolSelectors.enabledSchema(pluginIds)(useToolStore.getState());
|
||||
let filterTools = toolSelectors.enabledSchema(pluginIds)(getToolStoreState());
|
||||
|
||||
// check this model can use function call
|
||||
const canUseFC = isCanUseFC(payload.model, payload.provider!);
|
||||
@@ -378,7 +384,7 @@ class ChatService {
|
||||
* @param options
|
||||
*/
|
||||
runPluginApi = async (params: PluginRequestPayload, options?: FetchOptions) => {
|
||||
const s = useToolStore.getState();
|
||||
const s = getToolStoreState();
|
||||
|
||||
const settings = pluginSelectors.getPluginSettingsById(params.identifier)(s);
|
||||
const manifest = pluginSelectors.getToolManifestById(params.identifier)(s);
|
||||
@@ -537,7 +543,7 @@ class ChatService {
|
||||
const hasTools = tools && tools?.length > 0;
|
||||
const hasFC = hasTools && isCanUseFC(model, provider);
|
||||
const toolsSystemRoles =
|
||||
hasFC && toolSelectors.enabledSystemRoles(tools)(useToolStore.getState());
|
||||
hasFC && toolSelectors.enabledSystemRoles(tools)(getToolStoreState());
|
||||
|
||||
const injectSystemRoles = BuiltinSystemRolePrompts({
|
||||
historySummary: options?.historySummary,
|
||||
@@ -565,9 +571,9 @@ class ChatService {
|
||||
};
|
||||
|
||||
private mapTrace = (trace?: TracePayload, tag?: TraceTagMap): TracePayload => {
|
||||
const tags = sessionMetaSelectors.currentAgentMeta(useSessionStore.getState()).tags || [];
|
||||
const tags = sessionMetaSelectors.currentAgentMeta(getSessionStoreState()).tags || [];
|
||||
|
||||
const enabled = preferenceSelectors.userAllowTrace(useUserStore.getState());
|
||||
const enabled = preferenceSelectors.userAllowTrace(getUserStoreState());
|
||||
|
||||
if (!enabled) return { ...trace, enabled: false };
|
||||
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export type { AgentStore } from './store';
|
||||
export { useAgentStore } from './store';
|
||||
export { getAgentStoreState, useAgentStore } from './store';
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { contextCachingModels, thinkingWithToolClaudeModels } from '@/const/models';
|
||||
import { DEFAULT_AGENT_CHAT_CONFIG } from '@/const/settings';
|
||||
import { AgentStoreState } from '@/store/agent/initialState';
|
||||
import { LobeAgentChatConfig } from '@/types/agent';
|
||||
|
||||
@@ -30,10 +31,10 @@ const enableHistoryCount = (s: AgentStoreState) => {
|
||||
return chatConfig.enableHistoryCount;
|
||||
};
|
||||
|
||||
const historyCount = (s: AgentStoreState) => {
|
||||
const historyCount = (s: AgentStoreState): number => {
|
||||
const chatConfig = currentAgentChatConfig(s);
|
||||
|
||||
return chatConfig.historyCount;
|
||||
return chatConfig.historyCount || (DEFAULT_AGENT_CHAT_CONFIG.historyCount as number);
|
||||
};
|
||||
|
||||
const displayMode = (s: AgentStoreState) => {
|
||||
|
||||
@@ -20,3 +20,5 @@ const createStore: StateCreator<AgentStore, [['zustand/devtools', never]]> = (..
|
||||
const devtools = createDevtools('agent');
|
||||
|
||||
export const useAgentStore = createWithEqualityFn<AgentStore>()(devtools(createStore), shallow);
|
||||
|
||||
export const getAgentStoreState = () => useAgentStore.getState();
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export * from './selectors';
|
||||
export { useAiInfraStore } from './store';
|
||||
export { getAiInfraStoreState,useAiInfraStore } from './store';
|
||||
|
||||
@@ -23,3 +23,5 @@ const createStore: StateCreator<AiInfraStore, [['zustand/devtools', never]]> = (
|
||||
const devtools = createDevtools('aiInfra');
|
||||
|
||||
export const useAiInfraStore = createWithEqualityFn<AiInfraStore>()(devtools(createStore), shallow);
|
||||
|
||||
export const getAiInfraStoreState = () => useAiInfraStore.getState();
|
||||
|
||||
@@ -5,13 +5,14 @@ import { template } from 'lodash-es';
|
||||
import { StateCreator } from 'zustand/vanilla';
|
||||
|
||||
import { LOADING_FLAT, MESSAGE_CANCEL_FLAT } from '@/const/message';
|
||||
import { DEFAULT_AGENT_CHAT_CONFIG } from '@/const/settings';
|
||||
import { TraceEventType, TraceNameMap } from '@/const/trace';
|
||||
import { isServerMode } from '@/const/version';
|
||||
import { knowledgeBaseQAPrompts } from '@/prompts/knowledgeBaseQA';
|
||||
import { chatService } from '@/services/chat';
|
||||
import { messageService } from '@/services/message';
|
||||
import { useAgentStore } from '@/store/agent';
|
||||
import { agentChatConfigSelectors, agentSelectors } from '@/store/agent/selectors';
|
||||
import { getAgentStoreState } from '@/store/agent/store';
|
||||
import { chatHelpers } from '@/store/chat/helpers';
|
||||
import { ChatStore } from '@/store/chat/store';
|
||||
import { messageMapKey } from '@/store/chat/utils/messageMapKey';
|
||||
@@ -21,12 +22,6 @@ import { MessageSemanticSearchChunk } from '@/types/rag';
|
||||
import { setNamespace } from '@/utils/storeDebug';
|
||||
|
||||
import { chatSelectors, topicSelectors } from '../../../selectors';
|
||||
import {
|
||||
getAgentChatConfig,
|
||||
getAgentConfig,
|
||||
getAgentEnableHistoryCount,
|
||||
getAgentKnowledge,
|
||||
} from './helpers';
|
||||
|
||||
const n = setNamespace('ai');
|
||||
|
||||
@@ -163,7 +158,7 @@ export const generateAIChat: StateCreator<
|
||||
threadId: activeThreadId,
|
||||
};
|
||||
|
||||
const agentConfig = getAgentChatConfig();
|
||||
const agentConfig = agentChatConfigSelectors.currentChatConfig(getAgentStoreState());
|
||||
|
||||
let tempMessageId: string | undefined = undefined;
|
||||
let newTopicId: string | undefined = undefined;
|
||||
@@ -288,7 +283,7 @@ export const generateAIChat: StateCreator<
|
||||
// create a new array to avoid the original messages array change
|
||||
const messages = [...originalMessages];
|
||||
|
||||
const { model, provider, chatConfig } = getAgentConfig();
|
||||
const { model, provider, chatConfig } = agentSelectors.currentAgentConfig(getAgentStoreState());
|
||||
|
||||
let fileChunks: MessageSemanticSearchChunk[] | undefined;
|
||||
let ragQueryId;
|
||||
@@ -312,7 +307,7 @@ export const generateAIChat: StateCreator<
|
||||
chunks,
|
||||
userQuery: lastMsg.content,
|
||||
rewriteQuery,
|
||||
knowledge: getAgentKnowledge(),
|
||||
knowledge: agentSelectors.currentEnabledKnowledge(getAgentStoreState()),
|
||||
});
|
||||
|
||||
// 3. add the retrieve context messages to the messages history
|
||||
@@ -354,11 +349,10 @@ export const generateAIChat: StateCreator<
|
||||
}
|
||||
|
||||
// 5. summary history if context messages is larger than historyCount
|
||||
const historyCount =
|
||||
chatConfig.historyCount || (DEFAULT_AGENT_CHAT_CONFIG.historyCount as number);
|
||||
const historyCount = agentChatConfigSelectors.historyCount(getAgentStoreState());
|
||||
|
||||
if (
|
||||
chatConfig.enableHistoryCount &&
|
||||
agentChatConfigSelectors.enableHistoryCount(getAgentStoreState()) &&
|
||||
chatConfig.enableCompressHistory &&
|
||||
originalMessages.length > historyCount
|
||||
) {
|
||||
@@ -387,7 +381,7 @@ export const generateAIChat: StateCreator<
|
||||
n('generateMessage(start)', { messageId, messages }) as string,
|
||||
);
|
||||
|
||||
const agentConfig = getAgentConfig();
|
||||
const agentConfig = agentSelectors.currentAgentConfig(getAgentStoreState());
|
||||
const chatConfig = agentConfig.chatConfig;
|
||||
|
||||
const compiler = template(chatConfig.inputTemplate, { interpolate: /{{([\S\s]+?)}}/g });
|
||||
@@ -397,10 +391,13 @@ export const generateAIChat: StateCreator<
|
||||
// ================================== //
|
||||
|
||||
// 1. slice messages with config
|
||||
const historyCount = agentChatConfigSelectors.historyCount(getAgentStoreState());
|
||||
const enableHistoryCount = agentChatConfigSelectors.enableHistoryCount(getAgentStoreState());
|
||||
|
||||
let preprocessMsgs = chatHelpers.getSlicedMessages(messages, {
|
||||
includeNewUserMessage: true,
|
||||
enableHistoryCount: getAgentEnableHistoryCount(),
|
||||
historyCount: chatConfig.historyCount,
|
||||
enableHistoryCount,
|
||||
historyCount,
|
||||
});
|
||||
|
||||
// 2. replace inputMessage template
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import { useAgentStore } from '@/store/agent';
|
||||
import { agentChatConfigSelectors, agentSelectors } from '@/store/agent/selectors';
|
||||
|
||||
export const getAgentConfig = () => agentSelectors.currentAgentConfig(useAgentStore.getState());
|
||||
|
||||
export const getAgentChatConfig = () =>
|
||||
agentChatConfigSelectors.currentChatConfig(useAgentStore.getState());
|
||||
|
||||
export const getAgentEnableHistoryCount = () =>
|
||||
agentChatConfigSelectors.enableHistoryCount(useAgentStore.getState());
|
||||
|
||||
export const getAgentKnowledge = () =>
|
||||
agentSelectors.currentEnabledKnowledge(useAgentStore.getState());
|
||||
@@ -1,2 +1,2 @@
|
||||
export type { SessionStore } from './store';
|
||||
export { useSessionStore } from './store';
|
||||
export { getSessionStoreState,useSessionStore } from './store';
|
||||
|
||||
@@ -31,3 +31,5 @@ export const useSessionStore = createWithEqualityFn<SessionStore>()(
|
||||
),
|
||||
shallow,
|
||||
);
|
||||
|
||||
export const getSessionStoreState = () => useSessionStore.getState();
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export * from './helpers';
|
||||
export { useToolStore } from './store';
|
||||
export { getToolStoreState, useToolStore } from './store';
|
||||
|
||||
@@ -30,3 +30,5 @@ const createStore: StateCreator<ToolStore, [['zustand/devtools', never]]> = (...
|
||||
const devtools = createDevtools('tools');
|
||||
|
||||
export const useToolStore = createWithEqualityFn<ToolStore>()(devtools(createStore), shallow);
|
||||
|
||||
export const getToolStoreState = () => useToolStore.getState();
|
||||
|
||||
@@ -40,3 +40,5 @@ export const useUserStore = createWithEqualityFn<UserStore>()(
|
||||
subscribeWithSelector(devtools(createStore)),
|
||||
shallow,
|
||||
);
|
||||
|
||||
export const getUserStoreState = () => useUserStore.getState();
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
/* eslint-disable sort-keys-fix/sort-keys-fix, typescript-sort-keys/interface */
|
||||
import { z } from 'zod';
|
||||
|
||||
import { SearchMode } from '@/types/search';
|
||||
|
||||
export interface WorkingModel {
|
||||
model: string;
|
||||
provider: string;
|
||||
}
|
||||
|
||||
export interface LobeAgentChatConfig {
|
||||
displayMode?: 'chat' | 'docs';
|
||||
|
||||
enableAutoCreateTopic?: boolean;
|
||||
autoCreateTopicThreshold: number;
|
||||
|
||||
enableMaxTokens?: boolean;
|
||||
|
||||
/**
|
||||
* 是否开启推理
|
||||
*/
|
||||
enableReasoning?: boolean;
|
||||
/**
|
||||
* 自定义推理强度
|
||||
*/
|
||||
enableReasoningEffort?: boolean;
|
||||
reasoningBudgetToken?: number;
|
||||
|
||||
/**
|
||||
* 禁用上下文缓存
|
||||
*/
|
||||
disableContextCaching?: boolean;
|
||||
/**
|
||||
* 历史消息条数
|
||||
*/
|
||||
historyCount?: number;
|
||||
/**
|
||||
* 开启历史记录条数
|
||||
*/
|
||||
enableHistoryCount?: boolean;
|
||||
/**
|
||||
* 历史消息长度压缩阈值
|
||||
*/
|
||||
enableCompressHistory?: boolean;
|
||||
|
||||
inputTemplate?: string;
|
||||
|
||||
searchMode?: SearchMode;
|
||||
searchFCModel?: WorkingModel;
|
||||
useModelBuiltinSearch?: boolean;
|
||||
}
|
||||
/* eslint-enable */
|
||||
|
||||
export const AgentChatConfigSchema = z.object({
|
||||
autoCreateTopicThreshold: z.number().default(2),
|
||||
displayMode: z.enum(['chat', 'docs']).optional(),
|
||||
enableAutoCreateTopic: z.boolean().optional(),
|
||||
enableCompressHistory: z.boolean().optional(),
|
||||
enableHistoryCount: z.boolean().optional(),
|
||||
enableMaxTokens: z.boolean().optional(),
|
||||
enableReasoning: z.boolean().optional(),
|
||||
enableReasoningEffort: z.boolean().optional(),
|
||||
historyCount: z.number().optional(),
|
||||
reasoningBudgetToken: z.number().optional(),
|
||||
searchMode: z.enum(['off', 'on', 'auto']).optional(),
|
||||
});
|
||||
@@ -1,9 +1,8 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { FileItem } from '@/types/files';
|
||||
import { KnowledgeBaseItem } from '@/types/knowledgeBase';
|
||||
import { FewShots, LLMParams } from '@/types/llm';
|
||||
import { SearchMode } from '@/types/search';
|
||||
|
||||
import { LobeAgentChatConfig } from './chatConfig';
|
||||
|
||||
export type TTSServer = 'openai' | 'edge' | 'microsoft';
|
||||
|
||||
@@ -55,64 +54,8 @@ export interface LobeAgentConfig {
|
||||
tts: LobeAgentTTSConfig;
|
||||
}
|
||||
|
||||
/* eslint-disable sort-keys-fix/sort-keys-fix, typescript-sort-keys/interface */
|
||||
|
||||
export interface LobeAgentChatConfig {
|
||||
displayMode?: 'chat' | 'docs';
|
||||
|
||||
enableAutoCreateTopic?: boolean;
|
||||
autoCreateTopicThreshold: number;
|
||||
|
||||
enableMaxTokens?: boolean;
|
||||
|
||||
/**
|
||||
* 是否开启推理
|
||||
*/
|
||||
enableReasoning?: boolean;
|
||||
/**
|
||||
* 自定义推理强度
|
||||
*/
|
||||
enableReasoningEffort?: boolean;
|
||||
reasoningBudgetToken?: number;
|
||||
|
||||
/**
|
||||
* 禁用上下文缓存
|
||||
*/
|
||||
disableContextCaching?: boolean;
|
||||
/**
|
||||
* 历史消息条数
|
||||
*/
|
||||
historyCount?: number;
|
||||
/**
|
||||
* 开启历史记录条数
|
||||
*/
|
||||
enableHistoryCount?: boolean;
|
||||
/**
|
||||
* 历史消息长度压缩阈值
|
||||
*/
|
||||
enableCompressHistory?: boolean;
|
||||
|
||||
inputTemplate?: string;
|
||||
|
||||
searchMode?: SearchMode;
|
||||
useModelBuiltinSearch?: boolean;
|
||||
}
|
||||
/* eslint-enable */
|
||||
|
||||
export const AgentChatConfigSchema = z.object({
|
||||
autoCreateTopicThreshold: z.number().default(2),
|
||||
displayMode: z.enum(['chat', 'docs']).optional(),
|
||||
enableAutoCreateTopic: z.boolean().optional(),
|
||||
enableCompressHistory: z.boolean().optional(),
|
||||
enableHistoryCount: z.boolean().optional(),
|
||||
enableMaxTokens: z.boolean().optional(),
|
||||
enableReasoning: z.boolean().optional(),
|
||||
enableReasoningEffort: z.boolean().optional(),
|
||||
historyCount: z.number().optional(),
|
||||
reasoningBudgetToken: z.number().optional(),
|
||||
searchMode: z.enum(['off', 'on', 'auto']).optional(),
|
||||
});
|
||||
|
||||
export type LobeAgentConfigKeys =
|
||||
| keyof LobeAgentConfig
|
||||
| ['params', keyof LobeAgentConfig['params']];
|
||||
|
||||
export * from './chatConfig';
|
||||
|
||||
Reference in New Issue
Block a user