Repository: chatchat-space/Langchain-Chatchat (language: python)

Issue:
知识库接口对接完毕



Discussion / hints:

Fix patch:
diff --git a/frontend/.env.example b/frontend/.env.example
index 149cc8e863..d956ff1975 100644
--- a/frontend/.env.example
+++ b/frontend/.env.example
@@ -1,5 +1,5 @@
 # add a access code to lock your lobe-chat application, you can set a long password to avoid leaking. If this value contains a comma, it is a password array.
-#ACCESS_CODE=lobe66
+# ACCESS_CODE=lobe66
 
 # add your custom model name, multi model separate by comma. for example gpt-3.5-1106,gpt-4-1106
 # CUSTOM_MODELS=model1,model2,model3
@@ -14,7 +14,7 @@
 ########################################
 
 # you openai api key
-OPENAI_API_KEY=sk-xxxxxxxxx
+OPENAI_API_KEY = sk-xxxxxxxxx
 
 # use a proxy to connect to the OpenAI API
 # OPENAI_PROXY_URL=https://api.openai.com/v1
@@ -40,27 +40,27 @@ OPENAI_API_KEY=sk-xxxxxxxxx
 ############ ZhiPu AI Service ##########
 ########################################
 
-#ZHIPU_API_KEY=xxxxxxxxxxxxxxxxxxx.xxxxxxxxxxxxx
+# ZHIPU_API_KEY=xxxxxxxxxxxxxxxxxxx.xxxxxxxxxxxxx
 
 ########################################
 ########## Moonshot AI Service #########
 ########################################
 
-#MOONSHOT_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+# MOONSHOT_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
 
 ########################################
 ########### Google AI Service ##########
 ########################################
 
-#GOOGLE_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+# GOOGLE_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
 
 ########################################
 ######### AWS Bedrock Service ##########
 ########################################
 
-#AWS_REGION=us-east-1
-#AWS_ACCESS_KEY_ID=xxxxxxxxxxxxxxxxxxx
-#AWS_SECRET_ACCESS_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+# AWS_REGION=us-east-1
+# AWS_ACCESS_KEY_ID=xxxxxxxxxxxxxxxxxxx
+# AWS_SECRET_ACCESS_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
 
 ########################################
 ######### Ollama AI Service ##########
@@ -73,19 +73,19 @@ OPENAI_API_KEY=sk-xxxxxxxxx
 ########### Mistral AI Service ##########
 ########################################
 
-#MISTRAL_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+# MISTRAL_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
 
 ########################################
 ######### Perplexity Service ##########
 ########################################
 
-#PERPLEXITY_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+# PERPLEXITY_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
 
 ########################################
 ######### Anthropic Service ##########
 ########################################
 
-#ANTHROPIC_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+# ANTHROPIC_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
 
 ########################################
 ############ Market Service ############
@@ -98,6 +98,12 @@ OPENAI_API_KEY=sk-xxxxxxxxx
 ############ Plugin Service ############
 ########################################
 
+# you can use ChatChat.The local/remote ChatChat service url
+CHATCHAT_PROXY_URL = 'http://localhost:7861/v1'
+
+# Knowledge Base Service. Default as follows
+KNOWLEDGE_PROXY_URL = 'http://localhost:7861/knowledge_base'
+
 # The LobeChat plugins store index url
 # PLUGINS_INDEX_URL=https://chat-plugins.lobehub.com
 
diff --git a/frontend/next.config.mjs b/frontend/next.config.mjs
index 2de05fb415..f2d2d632eb 100644
--- a/frontend/next.config.mjs
+++ b/frontend/next.config.mjs
@@ -58,7 +58,7 @@ const nextConfig = {
     { source: '/docs', destination: `${docsBasePath}/docs` },
     { source: '/docs/zh', destination: `${docsBasePath}/docs/zh` },
     { source: '/docs/en', destination: `${docsBasePath}/docs/en` },
-    { source: '/docs/:path*', destination: `${docsBasePath}/docs/:path*` },
+    { source: '/docs/:path*', destination: `${docsBasePath}/docs/:path*` }
   ],
   reactStrictMode: true,
 
diff --git a/frontend/src/app/api/chat/[provider]/agentRuntime.ts b/frontend/src/app/api/chat/[provider]/agentRuntime.ts
index d681036a9e..8bef591831 100644
--- a/frontend/src/app/api/chat/[provider]/agentRuntime.ts
+++ b/frontend/src/app/api/chat/[provider]/agentRuntime.ts
@@ -20,6 +20,7 @@ import {
   LobePerplexityAI,
   LobeRuntimeAI,
   LobeZhipuAI,
+  LobeChatChatAI,
   ModelProvider,
 } from '@/libs/agent-runtime';
 import { TraceClient } from '@/libs/traces';
@@ -167,6 +168,11 @@ class AgentRuntime {
         runtimeModel = this.initMistral(payload);
         break;
       }
+
+      case ModelProvider.ChatChat: {
+        runtimeModel = this.initChatChat(payload);
+        break;
+      }
     }
 
     return new AgentRuntime(runtimeModel);
@@ -268,6 +274,13 @@ class AgentRuntime {
 
     return new LobeMistralAI({ apiKey });
   }
+
+  private static initChatChat(payload: JWTPayload) {
+    const { CHATCHAT_PROXY_URL } = getServerConfig();
+    const baseURL = payload?.endpoint || CHATCHAT_PROXY_URL;
+
+    return new LobeChatChatAI({ baseURL });
+  }
 }
 
 export default AgentRuntime;
diff --git a/frontend/src/app/api/knowledge/add/route.ts b/frontend/src/app/api/knowledge/add/route.ts
new file mode 100644
index 0000000000..76c52c33b1
--- /dev/null
+++ b/frontend/src/app/api/knowledge/add/route.ts
@@ -0,0 +1,15 @@
+
+import { getServerConfig } from '@/config/server'; 
+const { KNOWLEDGE_PROXY_URL } = getServerConfig();
+
+export const POST = async (request: Request) => {
+    const params = await request.json(); 
+    const fetchRes = await fetch(`${KNOWLEDGE_PROXY_URL}/create_knowledge_base`, {
+        body: JSON.stringify(params),
+        headers: {
+            'Content-Type': 'application/json',
+        },
+        method: 'POST',
+    });  
+    return fetchRes
+};
diff --git a/frontend/src/app/api/knowledge/del/route.ts b/frontend/src/app/api/knowledge/del/route.ts
new file mode 100644
index 0000000000..716079b5eb
--- /dev/null
+++ b/frontend/src/app/api/knowledge/del/route.ts
@@ -0,0 +1,15 @@
+
+import { getServerConfig } from '@/config/server'; 
+const { KNOWLEDGE_PROXY_URL } = getServerConfig();
+
+export const POST = async (request: Request) => {
+    const params = await request.text();  
+    const fetchRes = await fetch(`${KNOWLEDGE_PROXY_URL}/delete_knowledge_base`, {
+        body: params,
+        headers: {
+            'Content-Type': 'application/json',
+        },
+        method: 'POST',
+    });  
+    return fetchRes
+};
diff --git a/frontend/src/app/api/knowledge/delVectorDocs/route.ts b/frontend/src/app/api/knowledge/delVectorDocs/route.ts
new file mode 100644
index 0000000000..2dad6f62da
--- /dev/null
+++ b/frontend/src/app/api/knowledge/delVectorDocs/route.ts
@@ -0,0 +1,13 @@
+import { getServerConfig } from '@/config/server'; 
+const { KNOWLEDGE_PROXY_URL } = getServerConfig(); 
+export const POST = async (request: Request) => {  
+    const params = await request.json(); 
+    const fetchRes = await fetch(`${KNOWLEDGE_PROXY_URL}/delete_docs`, {
+        body: JSON.stringify(params),
+        headers: {
+            'Content-Type': 'application/json',
+        },
+        method: 'POST',
+    });  
+    return fetchRes
+};
diff --git a/frontend/src/app/api/knowledge/deleteDocs/route.ts b/frontend/src/app/api/knowledge/deleteDocs/route.ts
new file mode 100644
index 0000000000..2dad6f62da
--- /dev/null
+++ b/frontend/src/app/api/knowledge/deleteDocs/route.ts
@@ -0,0 +1,13 @@
+import { getServerConfig } from '@/config/server'; 
+const { KNOWLEDGE_PROXY_URL } = getServerConfig(); 
+export const POST = async (request: Request) => {  
+    const params = await request.json(); 
+    const fetchRes = await fetch(`${KNOWLEDGE_PROXY_URL}/delete_docs`, {
+        body: JSON.stringify(params),
+        headers: {
+            'Content-Type': 'application/json',
+        },
+        method: 'POST',
+    });  
+    return fetchRes
+};
diff --git a/frontend/src/app/api/knowledge/downloadDocs/route.ts b/frontend/src/app/api/knowledge/downloadDocs/route.ts
new file mode 100644
index 0000000000..0877da141b
--- /dev/null
+++ b/frontend/src/app/api/knowledge/downloadDocs/route.ts
@@ -0,0 +1,13 @@
+import { getServerConfig } from '@/config/server';
+const { KNOWLEDGE_PROXY_URL } = getServerConfig();
+
+export const GET = async (request: Request) => {
+    const searchParams = new URL(request.url).searchParams;
+    const knowledge_base_name = searchParams.get('knowledge_base_name') as string;
+    const file_name = searchParams.get('file_name') as string;
+    const preview = searchParams.get('preview') as string;
+ 
+    const queryString = new URLSearchParams({ knowledge_base_name, file_name, preview }).toString();
+    const fetchRes = await fetch(`${KNOWLEDGE_PROXY_URL}/download_doc?${queryString}`);
+    return fetchRes;
+};
diff --git a/frontend/src/app/api/knowledge/list/route.ts b/frontend/src/app/api/knowledge/list/route.ts
new file mode 100644
index 0000000000..6b5ff80757
--- /dev/null
+++ b/frontend/src/app/api/knowledge/list/route.ts
@@ -0,0 +1,6 @@
+import { getServerConfig } from '@/config/server'; 
+const { KNOWLEDGE_PROXY_URL } = getServerConfig();
+export const GET = async (request: Request) => {   
+    const fetchRes = await fetch(`${KNOWLEDGE_PROXY_URL}/list_knowledge_bases`);  
+    return fetchRes;
+};
diff --git a/frontend/src/app/api/knowledge/listFiles/route.ts b/frontend/src/app/api/knowledge/listFiles/route.ts
new file mode 100644
index 0000000000..b625da8561
--- /dev/null
+++ b/frontend/src/app/api/knowledge/listFiles/route.ts
@@ -0,0 +1,9 @@
+import { getServerConfig } from '@/config/server'; 
+
+const { KNOWLEDGE_PROXY_URL } = getServerConfig(); 
+export const GET = async (request: Request) => {
+    const knowledge_base_name: string = new URL(request.url).searchParams.get('knowledge_base_name') as string; 
+    const queryString = new URLSearchParams({ knowledge_base_name }).toString();
+    const fetchRes = await fetch(`${KNOWLEDGE_PROXY_URL}/list_files?${queryString}`);
+    return fetchRes;
+};
diff --git a/frontend/src/app/api/knowledge/reAddVectorDB/route.ts b/frontend/src/app/api/knowledge/reAddVectorDB/route.ts
new file mode 100644
index 0000000000..da805580a5
--- /dev/null
+++ b/frontend/src/app/api/knowledge/reAddVectorDB/route.ts
@@ -0,0 +1,14 @@
+ 
+import { getServerConfig } from '@/config/server'; 
+const { KNOWLEDGE_PROXY_URL } = getServerConfig(); 
+export const POST = async (request: Request) => {  
+    const params = await request.json(); 
+    const fetchRes = await fetch(`${KNOWLEDGE_PROXY_URL}/update_docs`, {
+        body: JSON.stringify(params),
+        headers: {
+            'Content-Type': 'application/json',
+        },
+        method: 'POST',
+    });  
+    return fetchRes
+};
diff --git a/frontend/src/app/api/knowledge/rebuildVectorDB/route.ts b/frontend/src/app/api/knowledge/rebuildVectorDB/route.ts
new file mode 100644
index 0000000000..cc59e1d595
--- /dev/null
+++ b/frontend/src/app/api/knowledge/rebuildVectorDB/route.ts
@@ -0,0 +1,14 @@
+ 
+import { getServerConfig } from '@/config/server'; 
+const { KNOWLEDGE_PROXY_URL } = getServerConfig(); 
+export const POST = async (request: Request) => {  
+    const params = await request.json(); 
+    const fetchRes = await fetch(`${KNOWLEDGE_PROXY_URL}/recreate_vector_store`, {
+        body: JSON.stringify(params),
+        headers: {
+            'Content-Type': 'application/json',
+        },
+        method: 'POST',
+    });  
+    return fetchRes
+};
diff --git a/frontend/src/app/api/knowledge/searchDocs/route.ts b/frontend/src/app/api/knowledge/searchDocs/route.ts
new file mode 100644
index 0000000000..9ccce5434c
--- /dev/null
+++ b/frontend/src/app/api/knowledge/searchDocs/route.ts
@@ -0,0 +1,15 @@
+
+import { getServerConfig } from '@/config/server'; 
+const { KNOWLEDGE_PROXY_URL } = getServerConfig();
+
+export const POST = async (request: Request) => {
+    const params = await request.json(); 
+    const fetchRes = await fetch(`${KNOWLEDGE_PROXY_URL}/search_docs`, {
+        body: JSON.stringify(params),
+        headers: {
+            'Content-Type': 'application/json',
+        },
+        method: 'POST',
+    });  
+    return fetchRes
+};
diff --git a/frontend/src/app/api/knowledge/update/route.ts b/frontend/src/app/api/knowledge/update/route.ts
new file mode 100644
index 0000000000..b9c04cf76f
--- /dev/null
+++ b/frontend/src/app/api/knowledge/update/route.ts
@@ -0,0 +1,15 @@
+
+import { getServerConfig } from '@/config/server'; 
+const { KNOWLEDGE_PROXY_URL } = getServerConfig();
+
+export const POST = async (request: Request) => {
+    const params = await request.json(); 
+    const fetchRes = await fetch(`${KNOWLEDGE_PROXY_URL}/update_info`, {
+        body: JSON.stringify(params),
+        headers: {
+            'Content-Type': 'application/json',
+        },
+        method: 'POST',
+    });  
+    return fetchRes
+};
diff --git a/frontend/src/app/api/knowledge/updateDocs/route.ts b/frontend/src/app/api/knowledge/updateDocs/route.ts
new file mode 100644
index 0000000000..af7ed65b02
--- /dev/null
+++ b/frontend/src/app/api/knowledge/updateDocs/route.ts
@@ -0,0 +1,16 @@
+
+import { getServerConfig } from '@/config/server';  
+const { KNOWLEDGE_PROXY_URL } = getServerConfig();
+
+export const POST = async (request: Request) => {
+    const params = await request.json(); 
+    // console.log('请求参数：', params)
+    const fetchRes = await fetch(`${KNOWLEDGE_PROXY_URL}/update_docs`, {
+        body: JSON.stringify(params),
+        headers: {
+            'Content-Type': 'application/json',
+        },
+        method: 'POST',
+    });  
+    return fetchRes 
+};
diff --git a/frontend/src/app/api/knowledge/uploadDocs/route.ts b/frontend/src/app/api/knowledge/uploadDocs/route.ts
new file mode 100644
index 0000000000..a5902c7209
--- /dev/null
+++ b/frontend/src/app/api/knowledge/uploadDocs/route.ts
@@ -0,0 +1,12 @@
+
+import { getServerConfig } from '@/config/server'; 
+const { KNOWLEDGE_PROXY_URL } = getServerConfig();
+
+export const POST = async (request: Request) => {
+    const formData = await request.formData(); 
+    const fetchRes = await fetch(`${KNOWLEDGE_PROXY_URL}/upload_docs`, {
+        body: formData, 
+        method: 'POST',
+    });  
+    return fetchRes
+};
diff --git a/frontend/src/app/api/models/chatchat/route.ts b/frontend/src/app/api/models/chatchat/route.ts
new file mode 100644
index 0000000000..93e41d7631
--- /dev/null
+++ b/frontend/src/app/api/models/chatchat/route.ts
@@ -0,0 +1,40 @@
+import { getServerConfig } from '@/config/server';
+import { createErrorResponse } from '@/app/api/errorResponse';
+import { LOBE_CHAT_AUTH_HEADER, OAUTH_AUTHORIZED } from '@/const/auth';
+import { getJWTPayload } from '../../chat/auth';
+
+export const GET = async (req: Request) => {
+
+  // get Authorization from header
+  const authorization = req.headers.get(LOBE_CHAT_AUTH_HEADER);
+  
+  const { CHATCHAT_PROXY_URL } = getServerConfig();
+  
+  let baseURL = CHATCHAT_PROXY_URL;
+
+  // 为了方便拿到 endpoint，这里直接解析 JWT
+  if (authorization) {
+    const jwtPayload = await getJWTPayload(authorization);
+    if (jwtPayload.endpoint) {
+      baseURL = jwtPayload.endpoint;
+    }
+  }
+
+  let res: Response;
+
+  try {
+    console.log('get models from:', baseURL)
+
+    res = await fetch(`${baseURL}/models`);
+
+    if (!res.ok) {
+      // throw new Error(`Failed to fetch models: ${res.status}`);
+      return createErrorResponse(500, { error: `Failed to fetch models: ${res.status}` });
+    }
+
+    return res;
+
+  } catch (e) {
+    return createErrorResponse(500, { error: e });
+  }
+}
\ No newline at end of file
diff --git a/frontend/src/app/chat/features/SessionListContent/ListItem/index.tsx b/frontend/src/app/chat/features/SessionListContent/ListItem/index.tsx
index e1b5cda624..a39bf1d01c 100644
--- a/frontend/src/app/chat/features/SessionListContent/ListItem/index.tsx
+++ b/frontend/src/app/chat/features/SessionListContent/ListItem/index.tsx
@@ -3,6 +3,7 @@ import { useHover } from 'ahooks';
 import { createStyles, useResponsive } from 'antd-style';
 import { memo, useMemo, useRef } from 'react';
 import Avatar from '@/components/Avatar';
+
 const { Item } = List;
 
 const useStyles = createStyles(({ css, token, responsive }) => {
diff --git a/frontend/src/app/knowledge/(desktop)/features/KnowledgeCard.tsx b/frontend/src/app/knowledge/(desktop)/features/KnowledgeCard.tsx
new file mode 100644
index 0000000000..4a7bba1534
--- /dev/null
+++ b/frontend/src/app/knowledge/(desktop)/features/KnowledgeCard.tsx
@@ -0,0 +1,68 @@
+import { DeleteOutlined, EditOutlined, ExclamationCircleOutlined } from '@ant-design/icons';
+import { Card, Skeleton, message, Modal } from 'antd';
+import { useRouter } from 'next/navigation';
+import React, { useState } from 'react';
+import { useKnowledgeStore } from '@/store/knowledge';
+
+const { Meta } = Card;
+
+interface KnowLedgeCardProps {
+  intro: string;
+  name: string;
+  vector_store_type?: string;
+  embed_model?: string;
+}
+const KnowledgeCard: React.FC<KnowLedgeCardProps> = (props: KnowLedgeCardProps) => {
+
+
+  const [useFetchKnowledgeDel, useFetchKnowledgeList, setEditKnowledge] = useKnowledgeStore((s) => [
+    s.useFetchKnowledgeDel, s.useFetchKnowledgeList, s.setEditKnowledge
+  ]);
+  const { mutate } = useFetchKnowledgeList()
+
+  const [loading, setLoading] = useState(false);
+  const { name, intro } = props;
+  const router = useRouter();
+  const handleCardEditClick = () => {
+    setEditKnowledge({
+      knowledge_base_name: props.name,
+      kb_info:  props.intro,
+      vector_store_type: props.vector_store_type,
+      embed_model:props.embed_model,
+    });
+    router.push(`/knowledge/${encodeURIComponent(name)}/base`);
+  };
+  const delClick = async () => {
+    Modal.confirm({
+      title: `确认 ${name} 删除吗?`,
+      icon: <ExclamationCircleOutlined />,
+      async onOk() {
+        const { code: resCode, msg: resMsg } = await useFetchKnowledgeDel(name)
+        if (resCode !== 200) {
+          message.error(resMsg)
+        } else {
+          message.success(resMsg)
+          mutate()
+        }
+        return Promise.resolve();
+      },
+    });
+
+  };
+  return (
+    <Card
+      actions={[
+        <EditOutlined key="edit" onClick={handleCardEditClick} />,
+        <DeleteOutlined key="ellipsis" onClick={delClick} />,
+      ]}
+      bordered={false}
+      style={{ marginTop: 16, width: 300 }}
+    >
+      <Skeleton active avatar loading={loading}>
+        <Meta description={intro} title={name} />
+      </Skeleton>
+    </Card>
+  );
+};
+
+export default KnowledgeCard;
diff --git a/frontend/src/app/knowledge/(desktop)/features/KnowledgeList.tsx b/frontend/src/app/knowledge/(desktop)/features/KnowledgeList.tsx
new file mode 100644
index 0000000000..d65d1161ac
--- /dev/null
+++ b/frontend/src/app/knowledge/(desktop)/features/KnowledgeList.tsx
@@ -0,0 +1,61 @@
+import React, { memo } from 'react';
+import { Empty, Spin } from 'antd';
+import { createStyles } from 'antd-style';
+import KnowledgeCard from './KnowledgeCard';
+import { useKnowledgeStore } from '@/store/knowledge';
+import { Flexbox } from 'react-layout-kit';
+
+const useStyles = createStyles(({ css, token, stylish }) => ({
+  wrap: css`
+  min-height: 200px;
+  height: 100%;
+  width: 100%; 
+  `,
+  null: css`
+    display: block;
+    position: absolute;
+    top: 0px; bottom: 0px; left: 0px; right: 0px;
+    margin: auto;
+    height: 100px;
+  `,
+}));
+
+const RenderList = memo(() => {
+  const { styles } = useStyles();
+  const [listData, useFetchKnowledgeList] = useKnowledgeStore((s) => [
+    s.listData, s.useFetchKnowledgeList
+  ]);
+  const { isLoading } = useFetchKnowledgeList();
+
+  const list = listData.map(({ kb_info, kb_name }) => ({
+    intro: kb_info, 
+    name: kb_name
+  }))
+  // const list = [
+  //   { intro: '知识库简介', name: '知识库名称' },
+  //   { intro: '知识库简介', name: '知识库名称' }, 
+  // ];
+  
+  if (!isLoading && !listData.length) {
+    return <div className={styles.null}>
+      <Empty />
+    </div>
+  }
+  return <div className={styles.wrap}>
+    <Spin tip="Loading..." spinning={isLoading}>
+      <div className={styles.wrap}>
+        <Flexbox gap={20} horizontal justify="flex-start" wrap="wrap">
+          {list.map((item, index) => {
+            return <KnowledgeCard key={index} {...item} />;
+          })}
+        </Flexbox> 
+      </div>
+    </Spin>
+  </div>
+});
+
+const KnowledgeCardList = memo(() => {
+  return <RenderList />;
+});
+
+export default KnowledgeCardList;
diff --git a/frontend/src/app/knowledge/(desktop)/features/ModalCreateKnowledge.tsx b/frontend/src/app/knowledge/(desktop)/features/ModalCreateKnowledge.tsx
new file mode 100644
index 0000000000..d8617720f2
--- /dev/null
+++ b/frontend/src/app/knowledge/(desktop)/features/ModalCreateKnowledge.tsx
@@ -0,0 +1,92 @@
+import { Modal, type ModalProps } from '@lobehub/ui';
+import { Form, Input, Select, FormInstance, message } from 'antd';
+import { memo, useRef, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { Flexbox } from 'react-layout-kit';
+
+import { useKnowledgeStore } from '@/store/knowledge'; 
+
+const DEFAULT_FIELD_VALUE = {
+  vector_store_type: "faiss",
+  embed_model: "bge-large-zh-v1.5"
+};
+interface ModalCreateKnowledgeProps extends ModalProps {
+  toggleModal: (open: boolean) => void;
+}
+const CreateKnowledgeBase = memo<ModalCreateKnowledgeProps>(({ toggleModal, open }) => {
+  const [confirmLoading, setConfirmLoading] = useState(false);
+  const { t } = useTranslation('chat');
+  const antdFormInstance = useRef<FormInstance>();
+  const [useFetchKnowledgeAdd, useFetchKnowledgeList] = useKnowledgeStore((s) => [
+    s.useFetchKnowledgeAdd, s.useFetchKnowledgeList
+  ]);
+  const { mutate } = useFetchKnowledgeList()
+
+  const onSubmit = async () => {
+    if (!antdFormInstance.current) return;
+    const fieldsError = await antdFormInstance.current.validateFields();
+    if (fieldsError.length) return;
+    const values = antdFormInstance.current.getFieldsValue(true);
+
+    setConfirmLoading(true);
+    const { code: resCode, data: resData, msg: resMsg } = await useFetchKnowledgeAdd({ ...values })
+    setConfirmLoading(true);
+
+    if (resCode !== 200) {
+      message.error(resMsg)
+      return;
+    }
+    mutate();
+    toggleModal(false); 
+  }
+
+  return (
+    <Modal
+      allowFullscreen
+      centered={false}
+      maxHeight={false}
+      onCancel={() => toggleModal(false)}
+      onOk={onSubmit}
+      open={open}
+      title="创建知识库"
+      confirmLoading={confirmLoading}
+    >
+      <Form initialValues={DEFAULT_FIELD_VALUE} layout="vertical" ref={antdFormInstance}>
+        <Form.Item label="知识库名称" name="knowledge_base_name" rules={[{ required: true, message: '请输入知识库名称' }]}>
+          <Input autoFocus />
+        </Form.Item>
+        <Form.Item label="知识库简介" name="kb_info">
+          <Input />
+        </Form.Item>
+        <Flexbox direction="horizontal" gap={10} justify="space-between">
+          <div style={{ flex: '1' }}>
+            <Form.Item label="向量库类型" name="vector_store_type" rules={[{ required: true, message: '请选择向量库类型' }]}>
+              <Select>
+                <Select.Option value="faiss">faiss</Select.Option>
+                <Select.Option value="milvus">milvus</Select.Option>
+                <Select.Option value="zilliz">zilliz</Select.Option>
+                <Select.Option value="pg">pg</Select.Option>
+                <Select.Option value="es">es</Select.Option>
+                <Select.Option value="chromadb">chromadb</Select.Option>
+              </Select>
+            </Form.Item>
+          </div>
+          <div style={{ flex: '1' }}>
+            <Form.Item label="Embedding模型" name="embed_model" rules={[{ required: true, message: '请选择Embedding模型' }]}>
+              <Select>
+                <Select.Option value="bce-embedding-base_v1">bce-embedding-base_v1</Select.Option>
+                <Select.Option value="bge-large-zh-v1.5">bge-large-zh-v1.5</Select.Option>
+                <Select.Option value="text-embedding-v1">text-embedding-v1</Select.Option>
+                <Select.Option value="Bert">Bert</Select.Option>
+                <Select.Option value="Word2Vec">Word2Vec</Select.Option>
+                <Select.Option value="FastText">FastText</Select.Option>
+              </Select>
+            </Form.Item>
+          </div>
+        </Flexbox>
+      </Form>
+    </Modal>
+  );
+});
+
+export default CreateKnowledgeBase;
diff --git a/frontend/src/app/knowledge/(desktop)/index.tsx b/frontend/src/app/knowledge/(desktop)/index.tsx
new file mode 100644
index 0000000000..92fc4208e5
--- /dev/null
+++ b/frontend/src/app/knowledge/(desktop)/index.tsx
@@ -0,0 +1,38 @@
+'use client';
+
+import { FloatButton } from 'antd';
+import { Plus } from 'lucide-react';
+import dynamic from 'next/dynamic';
+import { memo, useState } from 'react';
+import { createStyles } from 'antd-style';
+
+import KnowledgeCardList from './features/KnowledgeList'; 
+import Layout from './layout.desktop';
+
+const useStyle = createStyles(({ css, token }) => ({
+  addButton: css`
+    width: 20px;
+  `,
+}));
+
+const ModalCreateKnowledge = dynamic(() => import('./features/ModalCreateKnowledge'));
+
+const DesktopPage = memo(() => {
+  const { styles } = useStyle();
+
+  const [showModal, setShowModal] = useState(false);
+  return (
+    <>
+      <Layout>
+        <KnowledgeCardList />
+        <FloatButton
+          // className={styles.addButton}
+          icon={<Plus />} onClick={() => setShowModal(true)}>
+          新建知识库
+        </FloatButton>
+      </Layout>
+      <ModalCreateKnowledge open={showModal} toggleModal={setShowModal} />
+    </>
+  );
+});
+export default DesktopPage;
diff --git a/frontend/src/app/knowledge/(desktop)/layout.desktop.tsx b/frontend/src/app/knowledge/(desktop)/layout.desktop.tsx
new file mode 100644
index 0000000000..509fec9021
--- /dev/null
+++ b/frontend/src/app/knowledge/(desktop)/layout.desktop.tsx
@@ -0,0 +1,22 @@
+'use client';
+
+import { PropsWithChildren, memo } from 'react';
+import { Flexbox } from 'react-layout-kit';
+
+import AppLayoutDesktop from '@/layout/AppLayout.desktop';
+import { SidebarTabKey } from '@/store/global/initialState';
+
+export default memo(({ children }: PropsWithChildren) => {
+  return (
+    <AppLayoutDesktop sidebarKey={SidebarTabKey.Knowledge}>
+      <Flexbox
+        flex={1}
+        height={'100%'}
+        id={'lobe-conversion-container'}
+        style={{ paddingLeft: 20, paddingRight: 20, position: 'relative' }}
+      >
+        {children}
+      </Flexbox>
+    </AppLayoutDesktop>
+  );
+});
diff --git a/frontend/src/app/knowledge/[id]/base/[fileId]/features/ModalSegment.tsx b/frontend/src/app/knowledge/[id]/base/[fileId]/features/ModalSegment.tsx
new file mode 100644
index 0000000000..1a77775bc5
--- /dev/null
+++ b/frontend/src/app/knowledge/[id]/base/[fileId]/features/ModalSegment.tsx
@@ -0,0 +1,72 @@
+import { Input, Modal, message } from 'antd';
+import { BaseSyntheticEvent, memo, useState } from 'react';
+import { Center, Flexbox } from 'react-layout-kit';
+import { useKnowledgeStore } from '@/store/knowledge';
+import type { KnowledgeUpdateDocsParams, KnowledgeSearchDocsListItem, KnowledgeSearchDocsList } from '@/types/knowledge';
+
+type ModalSegmentProps = {
+  dataSource: KnowledgeSearchDocsList;
+  fileId: string;
+  kbName: string;
+  toggleOpen: (open: boolean) => void;
+};
+
+const ModalSegment = memo<ModalSegmentProps>(({ kbName, fileId, dataSource = [], toggleOpen }) => {
+
+  const [updateLoading, setUpdateLoading] = useState(false);
+  const [editKnowledgeInfo, editContentInfo, useFetcUpdateDocs] = useKnowledgeStore((s) => [
+    s.editKnowledgeInfo, s.editContentInfo, s.useFetcUpdateDocs
+  ]);
+  const [textValue, setTextValue] = useState<string>(editContentInfo?.page_content || "");
+
+  const onOk = async () => {
+    const newDataSource = dataSource.map(item => ({
+      ...item,
+      page_content: item.id === editContentInfo?.id ? textValue : item.page_content
+    }))
+    const params: KnowledgeUpdateDocsParams = {
+      knowledge_base_name: kbName,
+      override_custom_docs: false,
+      to_vector_store: true,
+      zh_title_enhance: false,
+      not_refresh_vs_cache: false,
+      chunk_size: 250,
+      chunk_overlap: 50,
+      file_names: [decodeURIComponent(fileId)],
+      docs: JSON.stringify({
+        [decodeURIComponent(fileId)]: [...newDataSource]
+      })
+    }
+
+    try {
+      setUpdateLoading(true)
+      const { code: resCode, msg: resMsg } = await useFetcUpdateDocs(params)
+      setUpdateLoading(false)
+      toggleOpen(false)
+
+      if (resCode !== 200) {
+        message.error(resMsg)
+        return;
+      }
+      message.success(resMsg)
+    } catch (err) {
+      message.error(`${err}`)
+      setUpdateLoading(false)
+    }
+  }
+  const onChange = (event: BaseSyntheticEvent) => {
+    setTextValue(event.target.value)
+  }
+
+  return (
+    <Modal okText="确认修改" onOk={onOk} onCancel={() => toggleOpen(false)} open title="知识片段" confirmLoading={updateLoading}>
+      <Flexbox padding={20}>
+        <Center>
+          <Input.TextArea autoSize={{ maxRows: 15, minRows: 10 }} style={{ width: 600 }} value={textValue} onChange={onChange} />
+        </Center>
+      </Flexbox>
+    </Modal>
+  );
+});
+
+export default ModalSegment;
diff --git a/frontend/src/app/knowledge/[id]/base/[fileId]/page.tsx b/frontend/src/app/knowledge/[id]/base/[fileId]/page.tsx
new file mode 100644
index 0000000000..7f4815d036
--- /dev/null
+++ b/frontend/src/app/knowledge/[id]/base/[fileId]/page.tsx
@@ -0,0 +1,104 @@
+'use client';
+
+import { Card, List, Empty, Spin } from 'antd';
+import { createStyles } from 'antd-style';
+import dynamic from 'next/dynamic';
+import React, { memo, useEffect, useState } from 'react';
+import { useKnowledgeStore } from '@/store/knowledge';
+
+const ModalSegment = dynamic(() => import('./features/ModalSegment'));
+
+
+const useStyle = createStyles(({ css, token }) => ({
+  page: css`
+    width: 100%;  
+    padding-top: 12px;
+  `,
+  card: css`
+    cursor: pointer;
+    overflow: hidden;
+    &:hover {
+      box-shadow: 0 0 0 1px ${token.colorText};
+    }
+
+    &:active {
+      scale: 0.95;
+    }
+  `,
+  null: css`
+  display: block;
+  position: absolute;
+  top: 0px; bottom: 0px; left: 0px; right: 0px;
+  margin: auto;
+  height: 100px;
+`,
+}));
+
+const App = memo((props: { params: { id: string; fileId: string } }) => {
+  const { params: { id, fileId } } = props;
+  const { styles } = useStyle();
+  const [isModalOpen, toggleOpen] = useState(false);
+  const [fileSearchData, useFetchSearchDocs, setEditContentInfo] = useKnowledgeStore((s) => [
+    s.fileSearchData, s.useFetchSearchDocs, s.setEditContentInfo
+  ]);
+  // const fileSearchData = [
+  //   {
+  //     id: 1,
+  //     page_content: "This is a test", 
+  //   },
+  //   {
+  //     id: 2,
+  //     page_content: "This is a test22", 
+  //   },  
+  // ] 
+  const { isLoading, mutate } = useFetchSearchDocs({
+    query: "", 
+    top_k: 3,
+    score_threshold: 1,
+    knowledge_base_name: id, 
+    file_name: decodeURIComponent(fileId)
+  });
+
+  useEffect(()=>{
+    !isModalOpen && mutate();
+  }, [isModalOpen])
+
+  const handleSegmentCardClick: typeof setEditContentInfo = (item) => {
+    setEditContentInfo({...item})
+    toggleOpen(true);
+  };
+
+
+  if (!isLoading && !fileSearchData.length) {
+    return <div className={styles.null}>
+      <Empty />
+    </div>
+  }
+
+  return (
+    <div className={styles.page} >
+      <Spin tip="Loading..." spinning={isLoading}>
+        <List
+          dataSource={fileSearchData}
+          grid={{
+            gutter: 16,
+            lg: 2,
+            xl: 2,
+            xxl: 2,
+          }}
+          renderItem={(item) => (
+            <List.Item>
+              <Card className={styles.card} onClick={() => handleSegmentCardClick(item)}>
+                {item.page_content}
+              </Card>
+            </List.Item>
+          )}
+          size="large"
+        />
+        {isModalOpen && <ModalSegment dataSource={[...fileSearchData]} toggleOpen={toggleOpen} kbName={props.params.id} fileId={props.params.fileId} />}
+      </Spin>
+    </div>
+  );
+});
+
+export default App;
diff --git a/frontend/src/app/knowledge/[id]/base/features/ModalAddFile.tsx b/frontend/src/app/knowledge/[id]/base/features/ModalAddFile.tsx
new file mode 100644
index 0000000000..b0257225d7
--- /dev/null
+++ b/frontend/src/app/knowledge/[id]/base/features/ModalAddFile.tsx
@@ -0,0 +1,184 @@
+import { InboxOutlined } from '@ant-design/icons';
+import { Form, Modal, Upload, InputNumber, Radio, message, Input } from 'antd';
+import React, { memo, useState } from 'react';
+import { useKnowledgeStore } from '@/store/knowledge';
+import type { GetProp, UploadFile, UploadProps } from 'antd';
+import type { KnowledgeUplodDocsParams } from '@/types/knowledge';
+
+type ModalAddFileProps = {
+  kbName: string;
+  open: boolean;
+  setModalOpen: (open: boolean) => void;
+  setSelectedRowKeys: React.Dispatch<React.SetStateAction<string[]>>;
+
+  selectedRowKeys: string[];
+  isRebuildVectorDB?: boolean;
+  initialValue?: KnowledgeUplodDocsParams;
+};
+
+type FileType = Parameters<GetProp<UploadProps, 'beforeUpload'>>[0];
+
+const ModalAddFile = memo<ModalAddFileProps>(({ open, setModalOpen, setSelectedRowKeys, selectedRowKeys, kbName, initialValue, isRebuildVectorDB }) => {
+  const [confirmLoading, setConfirmLoading] = useState(false);
+  const [antdFormInstance] = Form.useForm();
+  const [useFetchKnowledgeUploadDocs, useFetchKnowledgeFilesList, useFetcReAddVectorDB] = useKnowledgeStore((s) => [
+    s.useFetchKnowledgeUploadDocs, s.useFetchKnowledgeFilesList,
+    s.useFetcReAddVectorDB
+  ]);
+  const { mutate } = useFetchKnowledgeFilesList(kbName)
+  const [fileList, setFileList] = useState<UploadFile[]>([]);
+
+  const antdUploadProps: UploadProps = {
+    name: "files",
+    // multiple: true,
+    onRemove: (file) => {
+      const index = fileList.indexOf(file);
+      const newFileList = fileList.slice();
+      newFileList.splice(index, 1);
+      setFileList(newFileList);
+    },
+    beforeUpload: (file) => {
+      setFileList([...fileList, file]);
+      return false;
+    },
+    fileList,
+  };
+
+  const onSubmit = async () => {
+    if (!antdFormInstance) return;
+    const fieldsError = await antdFormInstance.validateFields();
+    if (fieldsError.length) return;
+    const values = antdFormInstance.getFieldsValue(true);
+
+    if (isRebuildVectorDB) {
+      // Re-add to vector library
+      setConfirmLoading(true);
+      await useFetcReAddVectorDB({
+        ...values,
+        "knowledge_base_name": kbName,
+        "file_names": selectedRowKeys,
+      }).catch(() => {
+        message.error(`更新知识库失败`);
+      })
+      setConfirmLoading(false);
+      setSelectedRowKeys([]);
+      return;
+    }
+
+    if (!fileList.length) {
+      message.error('请选择文件')
+      return;
+    }
+
+    const formData = new FormData();
+    fileList.forEach((file) => {
+      formData.append('files', file as FileType);
+    });
+    for (const key in values) {
+      formData.append(key, values[key]);
+    }
+    formData.append('knowledge_base_name', kbName);
+
+    try {
+      setConfirmLoading(true);
+      const { code: resCode, msg: resMsg } = await useFetchKnowledgeUploadDocs(formData)
+      setConfirmLoading(false);
+
+      if (resCode !== 200) {
+        message.error(resMsg)
+        return;
+      }
+      message.success(resMsg)
+      mutate();
+      setModalOpen(false);
+    } catch (err) {
+      message.error(`${err}`)
+      setConfirmLoading(false);
+    }
+  }
+
+
+  const layout = {
+    labelCol: { span: 10 },
+    wrapperCol: { span: 14 },
+  }
+
+  return (
+    <Modal
+      onCancel={() => setModalOpen(false)}
+      open={open}
+      title={isRebuildVectorDB ? "重新添加至向量库" : "添加文件"}
+      onOk={onSubmit}
+      confirmLoading={confirmLoading}
+      width={600}
+      destroyOnClose
+      afterOpenChange={(open) => { 
+        !open && setFileList([])
+      }}
+    >
+
+      <Form
+        name="validate_other"
+        initialValues={{
+          override: true,
+          chunk_size: 250,
+          chunk_overlap: 50,
+          to_vector_store: true,
+          ...initialValue
+        }}
+        form={antdFormInstance}
+      >
+
+        {!isRebuildVectorDB && <>
+          <div style={{ padding: `24px 0px` }}>
+            <Upload.Dragger {...antdUploadProps}>
+              <p className="ant-upload-drag-icon">
+                <InboxOutlined />
+              </p>
+              <p className="ant-upload-text">单击或拖动文件到此区域进行上传</p>
+              {/* <p className="ant-upload-hint">支持单个或批量上传。</p> */}
+            </Upload.Dragger>
+          </div>
+          <Form.Item name="override" label="覆盖已有文件" {...layout}>
+            <Radio.Group>
+              <Radio value={true}>是</Radio>
+              <Radio value={false}>否</Radio>
+            </Radio.Group>
+          </Form.Item>
+        </>}
+
+        <Form.Item name="to_vector_store" label="进行向量化" {...layout}>
+          <Radio.Group>
+            <Radio value={true}>是</Radio>
+            <Radio value={false}>否</Radio>
+          </Radio.Group>
+        </Form.Item>
+        <Form.Item name="zh_title_enhance" label="开启中文标题加强" {...layout}>
+          <Radio.Group>
+            <Radio value={true}>是</Radio>
+            <Radio value={false}>否</Radio>
+          </Radio.Group>
+        </Form.Item>
+        <Form.Item name="not_refresh_vs_cache" label="暂不保存向量库（用于FAISS）" {...layout}>
+          <Radio.Group>
+            <Radio value={true}>是</Radio>
+            <Radio value={false}>否</Radio>
+          </Radio.Group>
+        </Form.Item>
+
+        <Form.Item name="chunk_size" label="单段文本最大长度" {...layout} {...layout}>
+          <InputNumber min={0} style={{ width: 200 }} />
+        </Form.Item>
+
+        <Form.Item name="chunk_overlap" label="相邻文本重合长度" {...layout} {...layout}>
+          <InputNumber min={0} style={{ width: 200 }} />
+        </Form.Item>
+        {/* <Form.Item name="docs" label="自定义的docs" {...layout} {...layout}>
+          <Input style={{ width: 200 }} />
+        </Form.Item> */}
+      </Form>
+    </Modal>
+  );
+});
+
+export default ModalAddFile;
diff --git a/frontend/src/app/knowledge/[id]/base/page.tsx b/frontend/src/app/knowledge/[id]/base/page.tsx
new file mode 100644
index 0000000000..e8533a716d
--- /dev/null
+++ b/frontend/src/app/knowledge/[id]/base/page.tsx
@@ -0,0 +1,275 @@
+'use client';
+
+import { Button, Table, message, Spin, Modal } from 'antd';
+import type { TableColumnsType } from 'antd';
+import dynamic from 'next/dynamic';
+import Link from 'next/link';
+import React, { useState } from 'react';
+import { Flexbox } from 'react-layout-kit';
+import { useKnowledgeStore } from '@/store/knowledge';
+import { useRouter } from 'next/navigation';
+import { UndoOutlined, DeleteOutlined, DownloadOutlined, PlusOutlined, ExclamationCircleOutlined } from '@ant-design/icons';
+
+import type { KnowledgeUpdateDocsParams } from '@/types/knowledge';
+
+const ModalAddFile = dynamic(() => import('./features/ModalAddFile'));
+interface DataType {
+  id: React.Key;
+  name: string;
+  loader: string;
+  splitter: string;
+  source: string;
+  vector: string;
+}
+
+const App: React.FC<{ params: { id: string } }> = ({ params }) => {
+  const router = useRouter();
+  const [
+    filesData,
+    useFetchKnowledgeFilesList,
+    useFetchKnowledgeDownloadDocs,
+    useFetcDelInknowledgeDB,
+    useFetcDelInVectorDB,
+    useFetcRebuildVectorDB,
+    useFetchKnowledgeDel,
+    useFetcUpdateDocs
+  ] = useKnowledgeStore((s) => [
+    s.filesData,
+    s.useFetchKnowledgeFilesList,
+    s.useFetchKnowledgeDownloadDocs,
+    s.useFetcDelInknowledgeDB,
+    s.useFetcDelInVectorDB,
+    s.useFetcRebuildVectorDB,
+    s.useFetchKnowledgeDel,
+    s.useFetcUpdateDocs
+  ]);
+  const { isLoading, mutate } = useFetchKnowledgeFilesList(params.id);
+
+  const [downloadLoading, setDownloadLoading] = useState(false);
+  const [delDocsLoading, setDelDocsLoading] = useState(false);
+  const [delVSLoading, setDelVSLoading] = useState(false);
+  const [rebuildVectorDBLoading, setRebuildVectorDBLoading] = useState(false);
+
+  // rebuild progress
+  const [rebuildProgress, setRebuildProgress] = useState("0%");
+  const data: DataType[] = filesData.map(({ No, file_name, text_splitter, in_folder, in_db }, i) => ({
+    index: No,
+    id: file_name,
+    name: file_name,
+    loader: "",
+    splitter: text_splitter,
+    source: in_folder ? "✔️" : "❌",
+    vector: in_db ? "✔️" : "❌",
+  }));
+  // const data = [
+  //   { id: '1', name: 'name1', loader: "loader", splitter: "splitter", source: "source", vector: "vector" },
+  //   { id: '2', name: 'name2', loader: "loader", splitter: "splitter", source: "source", vector: "vector" },
+  // ];
+
+  const [selectedRowKeys, setSelectedRowKeys] = useState<string[]>([]);
+  const [loading, setLoading] = useState(false);
+  const [isShowModal, setModal] = useState(false);
+  const [isRebuildVectorDB, setIsRebuildVectorDB] = useState(false);
+  const onSelectChange = (newSelectedRowKeys: React.Key[]) => {
+    setSelectedRowKeys(newSelectedRowKeys as string[]);
+  };
+  const columns: TableColumnsType<DataType> = [
+    {
+      dataIndex: 'index',
+      title: '序号',
+    },
+    {
+      dataIndex: 'name',
+      render: (text, rowData) => <Link href={`/knowledge/${params.id}/base/${encodeURIComponent(rowData.name)}`}>{text}</Link>,
+      title: '文档名称',
+    },
+    // {
+    //   dataIndex: 'loader',
+    //   title: '文档加载器',
+    // },
+    {
+      dataIndex: 'splitter',
+      title: '分词器',
+    },
+    {
+      dataIndex: 'source',
+      title: '源文件',
+    },
+    {
+      dataIndex: 'vector',
+      title: '向量库',
+    },
+  ];
+  const hasSelected = selectedRowKeys.length > 0;
+
+  const download = async () => {
+    // setDownloadLoading(true);
+    console.log('selectedRowKeys', selectedRowKeys)
+    selectedRowKeys.forEach((docName) => {
+      console.log('docName', docName)
+      useFetchKnowledgeDownloadDocs(params.id, docName).catch(() => {
+        message.error(`下载 ${docName} 失败`);
+      })
+    })
+    // setDownloadLoading(false);
+  };
+  const reAddVectorDB = async () => {
+    Modal.confirm({
+      title: `确认将所选数据重新添加至向量库吗?`,
+      icon: <ExclamationCircleOutlined />,
+      onOk() {
+        return new Promise(async (resolve) => {
+          const _params: KnowledgeUpdateDocsParams = {
+            knowledge_base_name: params.id,
+            override_custom_docs: false,
+            to_vector_store: true,
+            zh_title_enhance: false,
+            not_refresh_vs_cache: false,
+            chunk_size: 250,
+            chunk_overlap: 50,
+            file_names: selectedRowKeys.map(decodeURIComponent),
+            docs: ""
+          }
+          await useFetcUpdateDocs(_params).catch(() => {
+            message.error(`更新失败`);
+          })
+          mutate();
+          resolve(true)
+        })
+      },
+    });
+
+  }
+  const rebuildVectorDB = async () => {
+    Modal.confirm({
+      title: `确认依据源文件重建 ${params.id} 的向量库吗?`,
+      icon: <ExclamationCircleOutlined />,
+      async onOk() {
+        setRebuildVectorDBLoading(true);
+        try {
+          useFetcRebuildVectorDB({
+            "knowledge_base_name": params.id,
+            "allow_empty_kb": true,
+            "vs_type": "faiss",
+            "embed_model": "text-embedding-v1",
+            "chunk_size": 250,
+            "chunk_overlap": 50,
+            "zh_title_enhance": false,
+            "not_refresh_vs_cache": false,
+          }, {
+            onFinish: async () => {
+              message.success(`重建向量库成功`);
+              setRebuildVectorDBLoading(false);
+              mutate();
+            },
+            onMessageHandle: (text) => {
+              // console.log('text', text)
+              setRebuildProgress(text)
+            }
+          })
+        } catch (err) {
+          message.error(`请求错误`);
+          setRebuildVectorDBLoading(false);
+        }
+      },
+    });
+
+  }
+  const delInVectorDB = async () => {
+    setDelVSLoading(true);
+    await useFetcDelInVectorDB({
+      "knowledge_base_name": params.id,
+      "file_names": [...selectedRowKeys],
+      "delete_content": false, // 不删除文件
+      "not_refresh_vs_cache": false
+    }).catch(() => {
+      message.error(`删除失败`);
+    })
+    setDelVSLoading(false);
+    setSelectedRowKeys([]);
+    mutate();
+  }
+  const delInknowledgeDB = async () => {
+    setDelDocsLoading(true);
+    await useFetcDelInknowledgeDB({
+      "knowledge_base_name": params.id,
+      "file_names": [...selectedRowKeys],
+      "delete_content": true,
+      "not_refresh_vs_cache": false
+    }).catch(() => {
+      message.error(`删除失败`);
+    })
+    setDelDocsLoading(false);
+    setSelectedRowKeys([]);
+    mutate();
+  }
+  const delKnowledge = async () => {
+    Modal.confirm({
+      title: `确认删除 ${params.id} 吗?`,
+      icon: <ExclamationCircleOutlined />,
+      async onOk() {
+        const { code: resCode, msg: resMsg } = await useFetchKnowledgeDel(params.id)
+        if (resCode !== 200) {
+          message.error(resMsg)
+        } else {
+          message.success(resMsg);
+          router.push('/knowledge')
+        }
+        return Promise.resolve();
+      },
+    });
+
+  }
+  return (
+    <>
+      <Flexbox width={'100%'}>
+        <Flexbox direction="horizontal" justify="space-between" style={{ marginBottom: 16 }}>
+          <Flexbox direction="horizontal" gap={20}>
+            <Button disabled={!hasSelected} loading={downloadLoading} type="default" onClick={download} icon={<DownloadOutlined />}>
+              下载选中文档
+            </Button>
+            <Button disabled={!hasSelected} loading={loading} type="default" onClick={reAddVectorDB}>
+              重新添加至向量库
+            </Button>
+            <Button danger disabled={!hasSelected} loading={delVSLoading} type="default" onClick={delInVectorDB} icon={<DeleteOutlined />}>
+              向量库删除
+            </Button>
+            <Button danger disabled={!hasSelected} loading={delDocsLoading} type="default" onClick={delInknowledgeDB} icon={<DeleteOutlined />}>
+              从知识库中删除
+            </Button>
+          </Flexbox>
+          <Flexbox direction="horizontal" gap={20}>
+            <Button danger loading={loading} onClick={delKnowledge} type="primary" icon={<DeleteOutlined />}>
+              删除知识库
+            </Button>
+            <Button loading={loading} onClick={() => { setIsRebuildVectorDB(false); setModal(true) }} type="primary" icon={<PlusOutlined />}>
+              添加文件
+            </Button>
+          </Flexbox>
+        </Flexbox>
+        <Spin spinning={rebuildVectorDBLoading} tip={rebuildProgress}>
+          <Table
+            columns={columns}
+            dataSource={data}
+            rowSelection={{
+              onChange: onSelectChange,
+              selectedRowKeys,
+            }}
+            size="middle"
+            style={{ width: '100%' }}
+            rowKey={"name"}
+            loading={isLoading}
+            footer={() => <div>
+              <Button danger loading={rebuildVectorDBLoading} onClick={rebuildVectorDB} icon={<UndoOutlined />}>
+                {rebuildVectorDBLoading ? rebuildProgress : ''} 依据源文件重建向量库
+              </Button>
+            </div>}
+          />
+        </Spin>
+      </Flexbox >
+      <ModalAddFile open={isShowModal} setModalOpen={setModal} kbName={params.id} selectedRowKeys={selectedRowKeys} setSelectedRowKeys={setSelectedRowKeys} isRebuildVectorDB={isRebuildVectorDB} />
+    </>
+  );
+};
+
+export default App;
diff --git a/frontend/src/app/knowledge/[id]/config/page.tsx b/frontend/src/app/knowledge/[id]/config/page.tsx
new file mode 100644
index 0000000000..4e21b7b9f4
--- /dev/null
+++ b/frontend/src/app/knowledge/[id]/config/page.tsx
@@ -0,0 +1,94 @@
+'use client';
+
+import { Form, type ItemGroup } from '@lobehub/ui';
+import { Form as AntForm, Button, Input, InputNumber, Switch, message } from 'antd';
+import { Settings } from 'lucide-react';
+import { memo, useCallback, useState } from 'react';
+import { Flexbox } from 'react-layout-kit';
+import { useKnowledgeStore } from '@/store/knowledge';
+import { FORM_STYLE } from '@/const/layoutTokens';
+
+const KnowledgeBaseConfig = memo(({ params }: { params: { id: string } }) => {
+  const [form] = AntForm.useForm(); 
+  const [submitLoading, setSubmitLoading] = useState(false);
+
+  const [
+    editKnowledgeInfo,
+    useFetchKnowledgeUpdate
+  ] = useKnowledgeStore((s) => [
+    s.editKnowledgeInfo,
+    s.useFetchKnowledgeUpdate
+  ]);
+  // console.log("editKnowledgeInfo===", editKnowledgeInfo);
+  const handleConfigChange = useCallback(async () => {
+    try {
+      const values = await form.validateFields();
+      // console.log('Success:', values);
+      setSubmitLoading(true);
+      const { msg, code } = await useFetchKnowledgeUpdate(values);
+      if (code === 200) {
+        message.success(msg);
+      } else {
+        message.error(msg);
+      }
+      setSubmitLoading(false);
+    } catch (errorInfo) {
+      console.log('Failed:', errorInfo);
+    }
+  }, [form]);
+
+  const system: ItemGroup = {
+    children: [
+      {
+        children: <Input placeholder={'请为知识库命名'} disabled />,
+        label: '知识库名称',
+        name: 'knowledge_base_name',
+        rules: [{ message: '请输入知识库名称', required: true }], 
+      },
+      {
+        children: <Input.TextArea placeholder={'请简单介绍你的知识库'} />,
+        label: '知识库简介',
+        name: 'kb_info',
+        rules: [{ message: '请输入知识库简介', required: true }],
+      },
+      // {
+      //   children: <InputNumber placeholder={'请输入数字'} style={{ width: 200 }} />,
+      //   label: '单段文本最大长度',
+      //   name: 'paragraphMaxLength',
+      //   rules: [{ message: '请输入知识库名称', required: true }],
+      // },
+      // {
+      //   children: <InputNumber placeholder={'请输入数字'} style={{ width: 200 }} />,
+      //   label: '相邻文本重合长度',
+      //   name: 'paragraphOverlapLength',
+      //   rules: [{ message: '请输入知识库名称', required: true }],
+      // },
+      // {
+      //   children: <InputNumber style={{ width: 200 }} />,
+      //   label: '文本匹配条数',
+      //   name: 'paragraphMatchCount',
+      //   rules: [{ message: '请输入知识库名称', required: true }],
+      // },
+      // {
+      //   children: <Switch style={{ width: 50 }} />,
+      //   label: '开启中文标题加强',
+      //   name: 'chineseTitleEnhance',
+      // },
+    ],
+    icon: Settings,
+    title: '知识库设置',
+  };
+
+  return (
+    <>
+      <Form form={form} items={[system]} initialValues={{...editKnowledgeInfo}} {...FORM_STYLE} />
+      <Flexbox padding={50}>
+        <Button loading={submitLoading} size="large" style={{ width: 400 }} onClick={handleConfigChange} >
+          保存
+        </Button>
+      </Flexbox>
+    </>
+  );
+});
+
+export default KnowledgeBaseConfig;
diff --git a/frontend/src/app/knowledge/[id]/layout.tsx b/frontend/src/app/knowledge/[id]/layout.tsx
new file mode 100644
index 0000000000..7754036113
--- /dev/null
+++ b/frontend/src/app/knowledge/[id]/layout.tsx
@@ -0,0 +1,64 @@
+'use client';
+
+import { PropsWithChildren, memo } from 'react';
+import { Center, Flexbox } from 'react-layout-kit';
+import AppLayoutDesktop from '@/layout/AppLayout.desktop';
+import { SidebarTabKey } from '@/store/global/initialState';
+import { LeftOutlined } from "@ant-design/icons"
+import { Button, Breadcrumb } from "antd"
+import KnowledgeTabs from './tabs';
+import { useRouter, useParams } from 'next/navigation';
+interface LayoutProps extends PropsWithChildren {
+  params: Record<string, string>;
+}
+export default memo<LayoutProps>(({ children }) => {
+  const router = useRouter();
+  const params = useParams<Record<string, string>>();
+  function goBack() {
+    router.push('/knowledge')
+  }
+  function goRootBack() {
+    router.push('/knowledge')
+  }
+  function goToFileList() {
+    router.push(`/knowledge/${params.id}/base`)
+  } 
+  return (
+    <AppLayoutDesktop sidebarKey={SidebarTabKey.Knowledge}>
+      <Flexbox direction="horizontal" flex={1} gap={40} height={'100%'}>
+        <Flexbox
+          direction="vertical"
+          gap={12}
+          padding={20}
+          style={{ borderInlineEnd: '1px solid #333333' }}
+        >
+          <Flexbox padding={10}>
+            <Center>
+              <Button onClick={goBack} type="link" icon={<LeftOutlined />}>{params.id}</Button>
+            </Center>
+          </Flexbox>
+
+          <KnowledgeTabs params={params} />
+        </Flexbox>
+        <Flexbox padding={40} width={'100%'} height={"100%"}>
+          <div style={{ paddingBlock: 12 }}>
+            {params.id && <Breadcrumb items={[
+              {
+                title: <a onClick={goRootBack}>知识库</a>,
+              },
+              {
+                title: <a onClick={goToFileList}>{params.id}</a>,
+              },
+              {
+                title: params.fileId ? decodeURIComponent(params.fileId) : null,
+              }
+            ].filter((_) => _.title)} />}
+          </div>
+          <div style={{ height: "calc(100% - 12px)", overflow: "auto" }}>
+            <Center>{children}</Center>
+          </div>
+        </Flexbox>
+      </Flexbox>
+    </AppLayoutDesktop>
+  );
+});
diff --git a/frontend/src/app/knowledge/[id]/tabs/TabItem.tsx b/frontend/src/app/knowledge/[id]/tabs/TabItem.tsx
new file mode 100644
index 0000000000..22fcd99a81
--- /dev/null
+++ b/frontend/src/app/knowledge/[id]/tabs/TabItem.tsx
@@ -0,0 +1,50 @@
+import { Icon, List } from '@lobehub/ui';
+import { createStyles, useResponsive } from 'antd-style';
+import { ChevronRight, type LucideIcon } from 'lucide-react';
+import { CSSProperties, ReactNode, memo } from 'react';
+
+const { Item } = List;
+
+const useStyles = createStyles(({ css, token, responsive }) => ({
+  container: css`
+    position: relative;
+    padding-top: 16px;
+    padding-bottom: 16px;
+    border-radius: ${token.borderRadius}px;
+    ${responsive.mobile} {
+      border-radius: 0;
+    }
+  `,
+  noHover: css`
+    pointer-events: none;
+  `,
+}));
+
+export interface ItemProps {
+  active?: boolean;
+  className?: string;
+  hoverable?: boolean;
+  icon: LucideIcon;
+  label: ReactNode;
+  style?: CSSProperties;
+}
+
+const KnowledgeTabItem = memo<ItemProps>(
+  ({ label, icon, hoverable = true, active = false, style, className }) => {
+    const { cx, styles } = useStyles();
+    const { mobile } = useResponsive();
+    return (
+      <Item
+        active={active}
+        avatar={<Icon icon={icon} size={{ fontSize: 20 }} />}
+        className={cx(styles.container, !hoverable && styles.noHover, className)}
+        style={style}
+        title={label as string}
+      >
+        {mobile && <Icon icon={ChevronRight} size={{ fontSize: 16 }} />}
+      </Item>
+    );
+  },
+);
+
+export default KnowledgeTabItem;
diff --git a/frontend/src/app/knowledge/[id]/tabs/index.tsx b/frontend/src/app/knowledge/[id]/tabs/index.tsx
new file mode 100644
index 0000000000..c055bf59ad
--- /dev/null
+++ b/frontend/src/app/knowledge/[id]/tabs/index.tsx
@@ -0,0 +1,35 @@
+import { Settings2, Webhook } from 'lucide-react';
+import { useRouter } from 'next/navigation';
+import { memo, useState } from 'react';
+
+import Item from './TabItem';
+
+export enum KnowledgeTabs {
+  Base = 'base',
+  Config = 'config',
+}
+
+export interface KnowledgeTabsProps {
+  activeTab?: KnowledgeTabs;
+  params: Record<string, string>;
+}
+
+const KnowledgeTabsBox = memo<KnowledgeTabsProps>(({ params }) => { 
+  const [activeTab, setActiveTab] = useState<KnowledgeTabs>(KnowledgeTabs.Base);
+  const items = [
+    { icon: Webhook, label: '知识库', value: KnowledgeTabs.Base },
+    { icon: Settings2, label: '配置', value: KnowledgeTabs.Config },
+  ];
+  const router = useRouter();
+  const handleTabClick = (value: KnowledgeTabs) => {
+    setActiveTab(value);
+    router.push(`/knowledge/${params.id}/${value}`);
+  };
+  return items.map(({ value, icon, label }) => (
+    <div aria-label={label} key={value} onClick={() => handleTabClick(value)}>
+      <Item active={activeTab === value} hoverable icon={icon} label={label} />
+    </div>
+  ));
+});
+
+export default KnowledgeTabsBox;
diff --git a/frontend/src/app/knowledge/page.tsx b/frontend/src/app/knowledge/page.tsx
new file mode 100644
index 0000000000..fc90ec13e8
--- /dev/null
+++ b/frontend/src/app/knowledge/page.tsx
@@ -0,0 +1,16 @@
+import DesktopPage from './(desktop)';
+
+// import MobilePage from './(mobile)';
+// import SessionHydration from './components/SessionHydration';
+// import Migration from './features/Migration';
+
+const Page = () => {
+  // const mobile = isMobileDevice();
+
+  // const Page = mobile ? MobilePage : DesktopPage;
+  const Page = DesktopPage;
+
+  return <Page />;
+};
+
+export default Page;
diff --git "a/frontend/src/app/knowledge/\346\265\213\350\257\225.txt" "b/frontend/src/app/knowledge/\346\265\213\350\257\225.txt"
new file mode 100644
index 0000000000..364fb93211
--- /dev/null
+++ "b/frontend/src/app/knowledge/\346\265\213\350\257\225.txt"
@@ -0,0 +1,15 @@
+
+
+pass. 知识库增加
+pass. 知识库列表
+pass. 知识库删除
+pass. 知识库修改
+pass. 知识库文档-文档列表
+pass. 知识库文档-添加文件
+pass. 知识库文档-向量库删除
+pass. 知识库文档-从知识库删除
+pass. 知识库文档-重新添加至向量库
+. 知识库文档-下载选中的文档
+pass. 知识库文档-依据源文件重构向量库
+pass. 知识库文档-知识片段列表
+pass. 知识库文档-知识片段修改 
\ No newline at end of file
diff --git a/frontend/src/app/settings/(mobile)/features/Header/Home.tsx b/frontend/src/app/settings/(mobile)/features/Header/Home.tsx
index 9d06c51e9a..a58143588d 100644
--- a/frontend/src/app/settings/(mobile)/features/Header/Home.tsx
+++ b/frontend/src/app/settings/(mobile)/features/Header/Home.tsx
@@ -1,6 +1,7 @@
 import { MobileNavBar } from '@lobehub/ui';
 import { memo } from 'react';
 import Logo from '@/components/Logo';
+
 const Header = memo(() => {
   return <MobileNavBar center={<Logo type={'text'} />} />;
 });
diff --git a/frontend/src/app/settings/llm/ChatChat/index.tsx b/frontend/src/app/settings/llm/ChatChat/index.tsx
new file mode 100644
index 0000000000..b3f90eeae0
--- /dev/null
+++ b/frontend/src/app/settings/llm/ChatChat/index.tsx
@@ -0,0 +1,71 @@
+import { Input, Flex } from 'antd';
+import { useTheme } from 'antd-style';
+import { memo } from 'react';
+import { useTranslation } from 'react-i18next';
+import Avatar from 'next/image';
+
+import { imageUrl } from '@/const/url';
+
+import { ModelProvider } from '@/libs/agent-runtime';
+
+import Checker from '../components/Checker';
+import ProviderConfig from '../components/ProviderConfig';
+import { LLMProviderBaseUrlKey, LLMProviderConfigKey } from '../const';
+import ModelSelector from '../components/ModelSeletor';
+
+const providerKey = 'chatchat';
+
+const ChatChatProvider = memo(() => {
+  const { t } = useTranslation('setting');
+  const theme = useTheme();
+
+  return (
+    <ProviderConfig
+      configItems={[
+        {
+          children: <Input allowClear placeholder={t('llm.ChatChat.endpoint.placeholder')} />,
+          desc: t('llm.ChatChat.endpoint.desc'),
+          label: t('llm.ChatChat.endpoint.title'),
+          name: [LLMProviderConfigKey, providerKey, LLMProviderBaseUrlKey],
+        },
+        {
+          children: (
+            <Input.TextArea
+              allowClear
+              placeholder={t('llm.ChatChat.customModelName.placeholder')}
+              style={{ height: 100 }}
+            />
+          ),
+          desc: t('llm.ChatChat.customModelName.desc'),
+          label: t('llm.ChatChat.customModelName.title'),
+          name: [LLMProviderConfigKey, providerKey, 'customModelName'],
+        },
+        {
+          children: <ModelSelector provider={ModelProvider.ChatChat} />,
+          desc: t('llm.selectorModel.desc'),
+          label: t('llm.selectorModel.title'),
+        },
+        {
+          children: <Checker model={'gml-4'} provider={ModelProvider.ChatChat} />,
+          desc: t('llm.ChatChat.checker.desc'),
+          label: t('llm.checker.title'),
+          minWidth: undefined,
+        },
+      ]}
+      provider={providerKey}
+      title={
+        <Flex>
+          <Avatar
+            alt={'Chatchat'}
+            height={24}
+            src={imageUrl('logo.png')}
+            width={24}
+          />
+          { 'ChatChat' }
+        </Flex>
+      }
+    />
+  );
+});
+
+export default ChatChatProvider;
diff --git a/frontend/src/app/settings/llm/components/ModelSeletor.tsx b/frontend/src/app/settings/llm/components/ModelSeletor.tsx
new file mode 100644
index 0000000000..5966fa584f
--- /dev/null
+++ b/frontend/src/app/settings/llm/components/ModelSeletor.tsx
@@ -0,0 +1,110 @@
+import { CheckCircleFilled } from '@ant-design/icons';
+import { Alert, Highlighter } from '@lobehub/ui';
+import { Button } from 'antd';
+import { useTheme } from 'antd-style';
+import { memo, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { Flexbox } from 'react-layout-kit';
+
+import { useIsMobile } from '@/hooks/useIsMobile';
+import { ModelSelectorError } from '@/types/message';
+import { modelsServer } from '@/services/models';
+import { useGlobalStore } from '@/store/global';
+import { GlobalLLMProviderKey } from '@/types/settings/modelProvider';
+import { currentSettings } from '@/store/global/slices/settings/selectors/settings';
+
+interface FetchModelParams {
+  provider: GlobalLLMProviderKey;
+}
+
+const ModelSelector = memo<FetchModelParams>(({ provider }) => {
+  const { t } = useTranslation('setting');
+
+  const [loading, setLoading] = useState(false);
+  const [pass, setPass] = useState(false);
+
+  const theme = useTheme();
+  const [error, setError] = useState<ModelSelectorError | undefined>();
+
+  const [setConfig, languageModel ] = useGlobalStore((s) => [
+    s.setModelProviderConfig,
+    currentSettings(s).languageModel,
+  ]);
+
+  const enable = languageModel[provider]?.enabled || false;
+
+  // 过滤格式
+  const filterModel = (data: any[] = []) => {
+    return data.map((item) => {
+
+      return {
+        tokens: item?.tokens || 8000,
+        displayName: item.displayName || item.id,
+        functionCall:  false, // false 默认都不能用使用插件，chatchat 的插件还没弄
+        ...item
+      }
+    })
+  }
+
+  const processProviderModels = () => {
+    if(!enable) return
+
+    setLoading(true);
+
+    modelsServer.getModels(provider).then((data) => {
+      if (data.error) {
+        setError({ message: data.error, type: 500});
+      } else {
+        // 更新模型
+        setConfig(provider, { models: filterModel(data.data) });
+
+        setError(undefined);
+        setPass(true);
+      }
+
+    }).finally(() => {
+      setLoading(false);
+    })
+  }
+
+  const isMobile = useIsMobile();
+
+  return (
+    <Flexbox align={isMobile ? 'flex-start' : 'flex-end'} gap={8}>
+      <Flexbox align={'center'} direction={isMobile ? 'horizontal-reverse' : 'horizontal'} gap={12}>
+        {pass && (
+          <Flexbox gap={4} horizontal>
+            <CheckCircleFilled
+              style={{
+                color: theme.colorSuccess,
+              }}
+            />
+            {t('llm.selectorModel.pass')}
+          </Flexbox>
+        )}
+        <Button loading={loading} onClick={processProviderModels}>
+          {t('llm.selectorModel.button')}
+        </Button>
+      </Flexbox>
+      {error && (
+        <Flexbox gap={8} style={{ maxWidth: '600px', width: '100%' }}>
+          <Alert
+            banner
+            extra={
+              <Flexbox>
+                <Highlighter copyButtonSize={'small'} language={'json'} type={'pure'}>
+                  {JSON.stringify(error, null, 2)}
+                </Highlighter>
+              </Flexbox>
+            }
+            message={t(`response.${error.type}` as any, { ns: 'error' })}
+            showIcon
+            type={'error'}
+          />
+        </Flexbox>
+      )}
+    </Flexbox>
+  );
+});
+
+export default ModelSelector;
diff --git a/frontend/src/app/settings/llm/index.tsx b/frontend/src/app/settings/llm/index.tsx
index 72ba486892..c085bc39d9 100644
--- a/frontend/src/app/settings/llm/index.tsx
+++ b/frontend/src/app/settings/llm/index.tsx
@@ -17,6 +17,7 @@ import Ollama from './Ollama';
 import OpenAI from './OpenAI';
 import Perplexity from './Perplexity';
 import Zhipu from './Zhipu';
+import ChatChat from './ChatChat'
 
 export default memo<{ showOllama: boolean }>(({ showOllama }) => {
   const { t } = useTranslation('setting');
@@ -34,6 +35,7 @@ export default memo<{ showOllama: boolean }>(({ showOllama }) => {
       <Anthropic />
       <Mistral />
       {showOllama && <Ollama />}
+      <ChatChat/>
       <Footer>
         <Trans i18nKey="llm.waitingForMore" ns={'setting'}>
           更多模型正在
diff --git a/frontend/src/app/welcome/(mobile)/features/Header.tsx b/frontend/src/app/welcome/(mobile)/features/Header.tsx
index 0da2e65b29..e00e211dbe 100644
--- a/frontend/src/app/welcome/(mobile)/features/Header.tsx
+++ b/frontend/src/app/welcome/(mobile)/features/Header.tsx
@@ -1,6 +1,7 @@
 import { MobileNavBar } from '@lobehub/ui';
 import { memo } from 'react';
 import Logo from '@/components/Logo';
+
 const Header = memo(() => <MobileNavBar center={<Logo type={'text'} />} />);
 
 export default Header;
diff --git a/frontend/src/components/FullscreenLoading/index.tsx b/frontend/src/components/FullscreenLoading/index.tsx
index f6564a179e..ced75cc446 100644
--- a/frontend/src/components/FullscreenLoading/index.tsx
+++ b/frontend/src/components/FullscreenLoading/index.tsx
@@ -3,6 +3,7 @@ import { Loader2 } from 'lucide-react';
 import { memo } from 'react';
 import { Center, Flexbox } from 'react-layout-kit';
 import Logo from '@/components/Logo';
+
 const FullscreenLoading = memo<{ title?: string }>(({ title }) => {
   return (
     <Flexbox height={'100%'} style={{ userSelect: 'none' }} width={'100%'}>
diff --git a/frontend/src/components/ModelIcon/index.tsx b/frontend/src/components/ModelIcon/index.tsx
index cd281f612d..f5e6062f50 100644
--- a/frontend/src/components/ModelIcon/index.tsx
+++ b/frontend/src/components/ModelIcon/index.tsx
@@ -12,6 +12,8 @@ import {
   OpenAI,
   Perplexity,
   Tongyi,
+  Spark,
+  Wenxin,
 } from '@lobehub/icons';
 import { memo } from 'react';
 
@@ -25,7 +27,7 @@ const ModelIcon = memo<ModelProviderIconProps>(({ model, size = 12 }) => {
 
   if (model.startsWith('gpt-3')) return <OpenAI.Avatar size={size} type={'gpt3'} />;
   if (model.startsWith('gpt-4')) return <OpenAI.Avatar size={size} type={'gpt4'} />;
-  if (model.startsWith('glm')) return <ChatGLM.Avatar size={size} />;
+  if (model.includes('glm')) return <ChatGLM.Avatar size={size} />;
   if (model.includes('claude')) return <Claude.Avatar size={size} />;
   if (model.includes('titan')) return <Aws.Avatar size={size} />;
   if (model.includes('llama')) return <Meta.Avatar size={size} />;
@@ -38,6 +40,8 @@ const ModelIcon = memo<ModelProviderIconProps>(({ model, size = 12 }) => {
     return <Baichuan.Avatar background={Baichuan.colorPrimary} size={size} />;
   if (model.includes('mistral') || model.includes('mixtral')) return <Mistral.Avatar size={size} />;
   if (model.includes('pplx')) return <Perplexity.Avatar size={size} />;
+  if (model.includes('Spark')) return <Spark.Avatar size={size} />;
+  if (model.includes('ERNIE')) return <Wenxin.Avatar size={size} />;
 });
 
 export default ModelIcon;
diff --git a/frontend/src/components/ModelProviderIcon/index.tsx b/frontend/src/components/ModelProviderIcon/index.tsx
index 456b9f3d19..834bc300b1 100644
--- a/frontend/src/components/ModelProviderIcon/index.tsx
+++ b/frontend/src/components/ModelProviderIcon/index.tsx
@@ -12,9 +12,12 @@ import {
 } from '@lobehub/icons';
 import { memo } from 'react';
 import { Center } from 'react-layout-kit';
+import Avatar from 'next/image';
 
 import { ModelProvider } from '@/libs/agent-runtime';
 
+import { imageUrl } from '@/const/url';
+
 interface ModelProviderIconProps {
   provider?: string;
 }
@@ -69,6 +72,15 @@ const ModelProviderIcon = memo<ModelProviderIconProps>(({ provider }) => {
       return <Anthropic size={20} />;
     }
 
+    case ModelProvider.ChatChat: {
+      return  <Avatar
+                alt={'Chatchat'}
+                height={24}
+                src={imageUrl('logo.png')}
+                width={24}
+              />
+    }
+
     default: {
       return null;
     }
diff --git a/frontend/src/config/modelProviders/chatchat.ts b/frontend/src/config/modelProviders/chatchat.ts
new file mode 100644
index 0000000000..5c736fb898
--- /dev/null
+++ b/frontend/src/config/modelProviders/chatchat.ts
@@ -0,0 +1,83 @@
+import { ModelProviderCard } from '@/types/llm';
+
+const ChatChat: ModelProviderCard = {
+  id: 'chatchat',
+  chatModels: [
+    {
+      id: 'chatglm3-6b',
+      tokens: 4000,
+      displayName: 'chatglm3-6b',
+      functionCall: true,
+    },
+    {
+      id: 'chatglm_turbo',
+      tokens: 4000,
+      displayName: 'chatglm_turbo',
+    },
+    {
+      id: 'chatglm_std',
+      tokens: 4000,
+      displayName: 'chatglm_std',
+    },
+    {
+      id: 'chatglm_lite',
+      tokens: 4000,
+      displayName: 'chatglm_lite',
+    },
+    {
+      id: 'qwen-turbo',
+      tokens: 4000,
+      displayName: 'qwen-turbo',
+      functionCall: true,
+    },
+    {
+      id: 'qwen-plus',
+      tokens: 4000,
+      displayName: 'qwen-plus',
+    },
+    {
+      id: 'qwen-max',
+      tokens: 4000,
+      displayName: 'qwen-max',
+    },
+    {
+      id: 'qwen:7b',
+      tokens: 4000,
+      displayName: 'qwen:7b',
+      functionCall: true,
+    },
+    {
+      id: 'qwen:14b',
+      tokens: 4000,
+      displayName: 'qwen:14b',
+      functionCall: true,
+    },
+    {
+      id: 'qwen-max-longcontext',
+      tokens: 4000,
+      displayName: 'qwen-max-longcontext',
+    },
+    {
+      id: 'ERNIE-Bot',
+      tokens: 4000,
+      displayName: 'ERNIE-Bot',
+    },
+    {
+      id: 'ERNIE-Bot-turbo',
+      tokens: 4000,
+      displayName: 'ERNIE-Bot-turbo',
+    },
+    {
+      id: 'ERNIE-Bot-4',
+      tokens: 4000,
+      displayName: 'ERNIE-Bot-4',
+    },
+    {
+      id: 'SparkDesk',
+      tokens: 4000,
+      displayName: 'SparkDesk',
+    }
+  ]
+}
+
+export default ChatChat;
\ No newline at end of file
diff --git a/frontend/src/config/modelProviders/index.ts b/frontend/src/config/modelProviders/index.ts
index e30bd72edf..12e6552a40 100644
--- a/frontend/src/config/modelProviders/index.ts
+++ b/frontend/src/config/modelProviders/index.ts
@@ -9,6 +9,7 @@ import OllamaProvider from './ollama';
 import OpenAIProvider from './openai';
 import PerplexityProvider from './perplexity';
 import ZhiPuProvider from './zhipu';
+import ChatChatProvider from './chatchat'
 
 export const LOBE_DEFAULT_MODEL_LIST: ChatModelCard[] = [
   OpenAIProvider.chatModels,
@@ -20,6 +21,7 @@ export const LOBE_DEFAULT_MODEL_LIST: ChatModelCard[] = [
   OllamaProvider.chatModels,
   PerplexityProvider.chatModels,
   AnthropicProvider.chatModels,
+  ChatChatProvider.chatModels,
 ].flat();
 
 export { default as AnthropicProvider } from './anthropic';
@@ -31,3 +33,4 @@ export { default as OllamaProvider } from './ollama';
 export { default as OpenAIProvider } from './openai';
 export { default as PerplexityProvider } from './perplexity';
 export { default as ZhiPuProvider } from './zhipu';
+export { default as ChatChatProvider } from './chatchat' 
diff --git a/frontend/src/config/server/provider.ts b/frontend/src/config/server/provider.ts
index 250714dec0..336c15e790 100644
--- a/frontend/src/config/server/provider.ts
+++ b/frontend/src/config/server/provider.ts
@@ -46,6 +46,12 @@ declare global {
 
       // Ollama Provider;
       OLLAMA_PROXY_URL?: string;
+
+      // ChatChat
+      CHATCHAT_PROXY_URL?: string;
+
+      // knowledeg
+      KNOWLEDGE_PROXY_URL?: string;
     }
   }
 }
@@ -73,7 +79,7 @@ export const getProviderConfig = () => {
   if (process.env.OPENAI_FUNCTION_REGIONS) {
     regions = process.env.OPENAI_FUNCTION_REGIONS.split(',');
   }
-
+  
   return {
     CUSTOM_MODELS: process.env.CUSTOM_MODELS,
 
@@ -114,5 +120,9 @@ export const getProviderConfig = () => {
 
     ENABLE_OLLAMA: !!process.env.OLLAMA_PROXY_URL,
     OLLAMA_PROXY_URL: process.env.OLLAMA_PROXY_URL || '',
+
+    CHATCHAT_PROXY_URL: process.env.CHATCHAT_PROXY_URL || '',
+    KNOWLEDGE_PROXY_URL: process.env.KNOWLEDGE_PROXY_URL || 'http://localhost:7861/knowledge_base',
+    // KNOWLEDGE_PROXY_URL: process.env.KNOWLEDGE_PROXY_URL || 'https://beige-points-count.loca.lt/knowledge_base',
   };
 };
diff --git a/frontend/src/const/settings.ts b/frontend/src/const/settings.ts
index 20d0d4f136..d68d80d7a1 100644
--- a/frontend/src/const/settings.ts
+++ b/frontend/src/const/settings.ts
@@ -91,6 +91,10 @@ export const DEFAULT_LLM_CONFIG: GlobalLLMConfig = {
     apiKey: '',
     enabled: false,
   },
+  chatchat: {
+    enabled: false,
+    endpoint: ''
+  },
 };
 
 export const DEFAULT_AGENT: GlobalDefaultAgent = {
diff --git a/frontend/src/features/Conversation/Error/style.tsx b/frontend/src/features/Conversation/Error/style.tsx
index 036a26585a..64a2e646e6 100644
--- a/frontend/src/features/Conversation/Error/style.tsx
+++ b/frontend/src/features/Conversation/Error/style.tsx
@@ -3,6 +3,7 @@ import { createStyles } from 'antd-style';
 import { ReactNode, memo } from 'react';
 import { Center, Flexbox } from 'react-layout-kit';
 import Avatar from '@/components/Avatar';
+
 export const useStyles = createStyles(({ css, token }) => ({
   container: css`
     color: ${token.colorText};
diff --git a/frontend/src/features/ModelSwitchPanel/index.tsx b/frontend/src/features/ModelSwitchPanel/index.tsx
index da10ec4148..af00cf2273 100644
--- a/frontend/src/features/ModelSwitchPanel/index.tsx
+++ b/frontend/src/features/ModelSwitchPanel/index.tsx
@@ -44,7 +44,7 @@ const ModelSwitchPanel = memo<PropsWithChildren>(({ children }) => {
       provider.chatModels
         .filter((c) => !c.hidden)
         .map((model) => ({
-          key: model.id,
+          key: `${provider.id}-${model.id}`,
           label: <ModelItemRender {...model} />,
           onClick: () => {
             updateAgentConfig({ model: model.id, provider: provider.id });
diff --git a/frontend/src/features/SideBar/TopActions.tsx b/frontend/src/features/SideBar/TopActions.tsx
index 66f5a0abe7..df1a4c0789 100644
--- a/frontend/src/features/SideBar/TopActions.tsx
+++ b/frontend/src/features/SideBar/TopActions.tsx
@@ -1,5 +1,5 @@
 import { ActionIcon } from '@lobehub/ui';
-import { Compass, MessageSquare } from 'lucide-react';
+import { Compass, Library, MessageSquare } from 'lucide-react';
 import Link from 'next/link';
 import { memo } from 'react';
 import { useTranslation } from 'react-i18next';
@@ -43,6 +43,15 @@ const TopActions = memo<TopActionProps>(({ tab }) => {
           title={t('tab.market')}
         />
       </Link>
+      <Link aria-label={'知识库'} href={'/knowledge'}>
+        <ActionIcon
+          active={tab === SidebarTabKey.Knowledge}
+          icon={Library}
+          placement={'right'}
+          size="large"
+          title={'知识库'}
+        />
+      </Link>
     </>
   );
 });
diff --git a/frontend/src/libs/agent-runtime/chatchat/index.ts b/frontend/src/libs/agent-runtime/chatchat/index.ts
new file mode 100644
index 0000000000..4666c101ea
--- /dev/null
+++ b/frontend/src/libs/agent-runtime/chatchat/index.ts
@@ -0,0 +1,112 @@
+import { OpenAIStream, StreamingTextResponse } from 'ai';
+import OpenAI, { ClientOptions } from 'openai';
+
+import { LobeRuntimeAI } from '../BaseAI';
+import { AgentRuntimeErrorType } from '../error';
+import { ChatCompetitionOptions, ChatStreamPayload, ModelProvider } from '../types';
+import { AgentRuntimeError } from '../utils/createError';
+import { debugStream } from '../utils/debugStream';
+import { desensitizeUrl } from '../utils/desensitizeUrl';
+import { handleOpenAIError } from '../utils/handleOpenAIError';
+import { Stream } from 'openai/streaming';
+
+const DEFAULT_BASE_URL = 'http://localhost:7861/v1';
+// const DEFAULT_BASE_URL = 'https://beige-points-count.loca.lt/v1';
+
+
+export class LobeChatChatAI implements LobeRuntimeAI {
+  private client: OpenAI;
+
+  baseURL: string;
+
+  constructor({ apiKey = 'chatChat', baseURL = DEFAULT_BASE_URL, ...res }: ClientOptions) {
+    if (!baseURL) throw AgentRuntimeError.createError(AgentRuntimeErrorType.InvalidChatChatArgs);
+
+    this.client = new OpenAI({ apiKey, baseURL, ...res });
+    this.baseURL = baseURL;
+  }
+
+  async chat(payload: ChatStreamPayload, options?: ChatCompetitionOptions) {
+
+    try {
+      const response = await this.client.chat.completions.create(
+        payload as unknown as (OpenAI.ChatCompletionCreateParamsStreaming | OpenAI.ChatCompletionCreateParamsNonStreaming),
+      );
+
+      if (LobeChatChatAI.isStream(response)) {
+
+        const [prod, debug] = response.tee();
+
+        if (process.env.DEBUG_OLLAMA_CHAT_COMPLETION === '1') {
+          debugStream(debug.toReadableStream()).catch(console.error);
+        }
+
+        return new StreamingTextResponse(OpenAIStream(prod, options?.callback), {
+          headers: options?.headers,
+        });
+      } else {
+
+        if (process.env.DEBUG_OLLAMA_CHAT_COMPLETION === '1') {
+          console.debug(JSON.stringify(response));
+        }
+
+        const stream = LobeChatChatAI.createChatCompletionStream(response?.choices[0].message.content || '');
+
+        return new StreamingTextResponse(stream);
+      }
+    } catch (error) {
+      let desensitizedEndpoint = this.baseURL;
+
+      if (this.baseURL !== DEFAULT_BASE_URL) {
+        desensitizedEndpoint = desensitizeUrl(this.baseURL);
+      }
+
+      if ('status' in (error as any)) {
+        switch ((error as Response).status) {
+          case 401: {
+            throw AgentRuntimeError.chat({
+              endpoint: desensitizedEndpoint,
+              error: error as any,
+              errorType: AgentRuntimeErrorType.InvalidChatChatArgs,
+              provider: ModelProvider.ChatChat,
+            });
+          }
+
+          default: {
+            break;
+          }
+        }
+      }
+
+      const { errorResult, RuntimeError } = handleOpenAIError(error);
+
+      const errorType = RuntimeError || AgentRuntimeErrorType.ChatChatBizError;
+
+      throw AgentRuntimeError.chat({
+        endpoint: desensitizedEndpoint,
+        error: errorResult,
+        errorType,
+        provider: ModelProvider.ChatChat,
+      });
+    }
+  }
+
+  static isStream(obj: unknown): obj is Stream<OpenAI.Chat.Completions.ChatCompletionChunk> {
+    return typeof Stream !== 'undefined' && (obj instanceof Stream || obj instanceof ReadableStream);
+  }
+
+
+  // 创建一个类型为 Stream<string> 的流
+  static createChatCompletionStream(text: string): ReadableStream<string> {
+
+    const stream = new ReadableStream({
+      start(controller) {
+        controller.enqueue(text);
+        controller.close();
+      },
+    });
+
+    return stream;
+  }
+
+}
\ No newline at end of file
diff --git a/frontend/src/libs/agent-runtime/error.ts b/frontend/src/libs/agent-runtime/error.ts
index 37828b0faa..4078f1de33 100644
--- a/frontend/src/libs/agent-runtime/error.ts
+++ b/frontend/src/libs/agent-runtime/error.ts
@@ -34,6 +34,9 @@ export const AgentRuntimeErrorType = {
 
   InvalidAnthropicAPIKey: 'InvalidAnthropicAPIKey',
   AnthropicBizError: 'AnthropicBizError',
+
+  InvalidChatChatArgs: 'InvalidChatChatArgs',
+  ChatChatBizError: 'ChatChatBizError',
 } as const;
 
 export type ILobeAgentRuntimeErrorType =
diff --git a/frontend/src/libs/agent-runtime/index.ts b/frontend/src/libs/agent-runtime/index.ts
index b73072c11e..047b1eab8c 100644
--- a/frontend/src/libs/agent-runtime/index.ts
+++ b/frontend/src/libs/agent-runtime/index.ts
@@ -2,6 +2,7 @@ export { LobeAnthropicAI } from './anthropic';
 export { LobeAzureOpenAI } from './azureOpenai';
 export * from './BaseAI';
 export { LobeBedrockAI } from './bedrock';
+export { LobeChatChatAI } from './chatchat';
 export * from './error';
 export { LobeGoogleAI } from './google';
 export { LobeMistralAI } from './mistral';
diff --git a/frontend/src/libs/agent-runtime/types/type.ts b/frontend/src/libs/agent-runtime/types/type.ts
index b43eaf7020..f7cb2576ea 100644
--- a/frontend/src/libs/agent-runtime/types/type.ts
+++ b/frontend/src/libs/agent-runtime/types/type.ts
@@ -25,6 +25,7 @@ export enum ModelProvider {
   Anthropic = 'anthropic',
   Azure = 'azure',
   Bedrock = 'bedrock',
+  ChatChat = 'chatchat',
   ChatGLM = 'chatglm',
   Google = 'google',
   Mistral = 'mistral',
@@ -33,5 +34,5 @@ export enum ModelProvider {
   OpenAI = 'openai',
   Perplexity = 'perplexity',
   Tongyi = 'tongyi',
-  ZhiPu = 'zhipu',
+  ZhiPu = 'zhipu'
 }
diff --git a/frontend/src/locales/default/common.ts b/frontend/src/locales/default/common.ts
index 49514f7c76..d0b3e6991a 100644
--- a/frontend/src/locales/default/common.ts
+++ b/frontend/src/locales/default/common.ts
@@ -111,6 +111,7 @@ export default {
     openai: 'OpenAI',
     perplexity: 'Perplexity',
     zhipu: '智谱AI',
+    chatchat: 'ChatChat',
   },
   noDescription: '暂无描述',
   oauth: 'SSO 登录',
diff --git a/frontend/src/locales/default/setting.ts b/frontend/src/locales/default/setting.ts
index f58779bccc..66e2a827f3 100644
--- a/frontend/src/locales/default/setting.ts
+++ b/frontend/src/locales/default/setting.ts
@@ -181,6 +181,29 @@ export default {
         title: 'API Key',
       },
     },
+    ChatChat: {
+      title: 'ChatChat',
+      checker: {
+        desc: '测试地址是否正确填写',
+      },
+      customModelName: {
+        desc: '增加自定义模型，多个模型使用逗号（,）隔开',
+        placeholder: 'gml-4',
+        title: '自定义模型名称',
+      },
+      endpoint: {
+        desc: '填入 ChatCaht 接口代理地址，本地未额外指定可留空',
+        placeholder: 'http://127.0.0.1:7861/chat',
+        title: '接口代理地址',
+      },
+    },
+
+    selectorModel: {
+      button: '更新',
+      desc: '选择代理地址所有模型，默认/v1/models获取',
+      pass: '更新成功',
+      title: '更新模型到本地',
+    },
 
     checker: {
       button: '检查',
diff --git a/frontend/src/services/_auth.ts b/frontend/src/services/_auth.ts
index e056a09cb3..b889538cb1 100644
--- a/frontend/src/services/_auth.ts
+++ b/frontend/src/services/_auth.ts
@@ -60,6 +60,10 @@ export const getProviderAuthPayload = (provider: string) => {
       return { apiKey: modelProviderSelectors.mistralAPIKey(useGlobalStore.getState()) };
     }
 
+    case ModelProvider.ChatChat: {
+      return { endpoint: modelProviderSelectors.chatChatProxyUrl(useGlobalStore.getState()) }
+    }
+
     default:
     case ModelProvider.OpenAI: {
       const openai = modelProviderSelectors.openAIConfig(useGlobalStore.getState());
diff --git a/frontend/src/services/_url.ts b/frontend/src/services/_url.ts
index 29ff60c71d..a17f5d4c30 100644
--- a/frontend/src/services/_url.ts
+++ b/frontend/src/services/_url.ts
@@ -36,9 +36,28 @@ export const API_ENDPOINTS = mapWithBasePath({
   // image
   images: '/api/openai/images',
 
+  // models
+  models: (provider: string) => withBasePath(`/api/models/${provider}`),
+
   // TTS & STT
   stt: '/api/openai/stt',
   tts: '/api/openai/tts',
   edge: '/api/tts/edge-speech',
   microsoft: '/api/tts/microsoft-speech',
+
+  // knowledge
+  knowledgeList: '/api/knowledge/list', 
+  knowledgeAdd: '/api/knowledge/add',
+  knowledgeUpdate: '/api/knowledge/update',
+  knowledgeDel: '/api/knowledge/del',
+  // knowledge files
+  knowledgeFilesList: '/api/knowledge/listFiles', 
+  knowledgeUploadDocs:  '/api/knowledge/uploadDocs', 
+  updateDocsContent:  '/api/knowledge/updateDocs', 
+  knowledgeDownloadDocs:  '/api/knowledge/downloadDocs', 
+  knowledgeDelInknowledgeDB:  '/api/knowledge/deleteDocs', 
+  knowledgeDelVectorDB:'/api/knowledge/delVectorDocs', 
+  knowledgeRebuildVectorDB:  '/api/knowledge/rebuildVectorDB', 
+  knowledgeReAddVectorDB:  '/api/knowledge/reAddVectorDB', 
+  knowledgeSearchDocs: '/api/knowledge/searchDocs',  
 });
diff --git a/frontend/src/services/knowledge.ts b/frontend/src/services/knowledge.ts
new file mode 100644
index 0000000000..952783d509
--- /dev/null
+++ b/frontend/src/services/knowledge.ts
@@ -0,0 +1,166 @@
+
+import type {
+    KnowledgeList, KnowledgeFormFields, Reseponse,
+    KnowledgeFilesList, KnowledgeDelDocsParams, KnowledgeDelDocsRes,
+    KnowledgeRebuildVectorParams, 
+    ReAddVectorDBParams, ReAddVectorDBRes,
+    KnowledgeSearchDocsParams, KnowledgeSearchDocsList, KnowledgeUpdateDocsParams
+} from '@/types/knowledge';
+
+import { fetchSSE, FetchSSEOptions } from '@/utils/fetch';
+import { API_ENDPOINTS } from './_url';
+
+class KnowledgeService {
+    getList = async (): Promise<Reseponse<KnowledgeList>> => {
+        const res = await fetch(`${API_ENDPOINTS.knowledgeList}`);
+        const data = await res.json();
+        return data;
+    };
+
+    add = async (formValues: KnowledgeFormFields) => {
+        const res = await fetch(`${API_ENDPOINTS.knowledgeAdd}`, {
+            body: JSON.stringify(formValues),
+            headers: {
+                'Content-Type': 'application/json',
+            },
+            method: 'POST',
+        });
+        return res.json();
+    };
+    update = async (formValues: Partial<KnowledgeFormFields>) => {
+        const res = await fetch(`${API_ENDPOINTS.knowledgeUpdate}`, {
+            body: JSON.stringify(formValues),
+            headers: {
+                'Content-Type': 'application/json',
+            },
+            method: 'POST',
+        });
+        return res.json();
+    };
+
+    del = async (name: string) => {
+        const res = await fetch(`${API_ENDPOINTS.knowledgeDel}`, {
+            body: JSON.stringify(name),
+            headers: {
+                'Content-Type': 'application/json',
+            },
+            method: 'POST',
+        });
+        return res.json();
+    };
+
+    getFilesList = (name: string): () => Promise<Reseponse<KnowledgeFilesList>> => {
+        const queryString = new URLSearchParams({
+            knowledge_base_name: name
+        }).toString();
+        return async () => {
+            const res = await fetch(`${API_ENDPOINTS.knowledgeFilesList}?${queryString}`);
+            const data = await res.json();
+            return data;
+        }
+    };
+
+    uploadDocs = async (formData: FormData): Promise<Reseponse<{}>> => {
+        const res = await fetch(`${API_ENDPOINTS.knowledgeUploadDocs}`, {
+            body: formData,
+            method: 'POST',
+        });
+        return res.json();
+    };
+
+    delInknowledgeDB = async (params: KnowledgeDelDocsParams): Promise<Reseponse<KnowledgeDelDocsRes>> => {
+        const res = await fetch(`${API_ENDPOINTS.knowledgeDelInknowledgeDB}`, {
+            body: JSON.stringify({
+                ...params,
+            }),
+            headers: {
+                'Content-Type': 'application/json',
+            },
+            method: 'POST',
+        });
+        return res.json();
+    };
+
+    rebuildVectorDB = async (params: KnowledgeRebuildVectorParams, opts:
+        { onFinish: FetchSSEOptions["onFinish"]; onMessageHandle: FetchSSEOptions["onMessageHandle"] }
+    ) => {
+        const { onFinish, onMessageHandle } = opts;
+        fetchSSE(async () => await fetch(`${API_ENDPOINTS.knowledgeRebuildVectorDB}`, {
+            body: JSON.stringify({
+                ...params,
+            }),
+            headers: {
+                'Content-Type': 'application/json',
+            },
+            method: 'POST'
+        }), {
+            onErrorHandle: (error) => {
+                throw new Error('请求错误：' + error);
+            },
+            onFinish,
+            onMessageHandle
+        })
+    };
+
+    delVectorDocs = async (params: KnowledgeDelDocsParams): Promise<Reseponse<KnowledgeDelDocsRes>> => {
+        const res = await fetch(`${API_ENDPOINTS.knowledgeDelVectorDB}`, {
+            body: JSON.stringify({
+                ...params,
+            }),
+            headers: {
+                'Content-Type': 'application/json',
+            },
+            method: 'POST',
+        });
+        return res.json();
+    };
+ 
+    downloadDocs = async (kbName: string, docName: string): Promise<void> => {
+        const queryString = new URLSearchParams({
+            knowledge_base_name: kbName,
+            file_name: docName,
+            preview: 'false'
+        }).toString();
+        const url = `${API_ENDPOINTS.knowledgeDownloadDocs}?${queryString}`;
+        window.open(url, docName); 
+    }; 
+    reAddVectorDB = async (params: ReAddVectorDBParams): Promise<Reseponse<ReAddVectorDBRes>> => {
+        const res = await fetch(`${API_ENDPOINTS.knowledgeReAddVectorDB}`, {
+            body: JSON.stringify({
+                ...params,
+            }),
+            headers: {
+                'Content-Type': 'application/json',
+            },
+            method: 'POST',
+        });
+        return res.json();
+    };
+    // searchDocs = async (params: KnowledgeSearchDocsParams): Promise<Reseponse<KnowledgeSearchDocsList>> => {
+    searchDocs = async (params: KnowledgeSearchDocsParams): Promise<KnowledgeSearchDocsList> => {
+        const res = await fetch(`${API_ENDPOINTS.knowledgeSearchDocs}`, {
+            body: JSON.stringify({
+                ...params,
+            }),
+            headers: {
+                'Content-Type': 'application/json',
+            },
+            method: 'POST',
+        });
+        return res.json();
+    }; 
+    updateDocs = async (params: KnowledgeUpdateDocsParams): Promise<Reseponse<{}>> => {
+        const res = await fetch(`${API_ENDPOINTS.updateDocsContent}`, {
+            body: JSON.stringify({
+                ...params,
+            }),
+            headers: {
+                'Content-Type': 'application/json',
+            },
+            method: 'POST',
+        });
+        return res.json();
+    }; 
+}
+
+export const knowledgeService = new KnowledgeService();
diff --git a/frontend/src/services/models.ts b/frontend/src/services/models.ts
new file mode 100644
index 0000000000..2a0fce3e7e
--- /dev/null
+++ b/frontend/src/services/models.ts
@@ -0,0 +1,28 @@
+import { getMessageError } from "@/utils/fetch";
+import { API_ENDPOINTS } from "./_url";
+import { createHeaderWithAuth } from "./_auth";
+import { ModelsResponse } from "@/types/models";
+import { GlobalLLMProviderKey } from "@/types/settings/modelProvider";
+
+
+class ModelsServer{
+  getModels = async (provider: GlobalLLMProviderKey): Promise<ModelsResponse> => {
+    const headers = await createHeaderWithAuth({ provider, headers: { 'Content-Type': 'application/json' } });
+
+    try {
+      const res = await fetch(API_ENDPOINTS.models(provider), {
+        headers,
+      });
+
+      if (!res.ok) {
+        throw await getMessageError(res);
+      }
+
+      return res.json();
+    } catch (error) {
+      return {  error: JSON.stringify(error) };
+    }
+  }
+}
+
+export const modelsServer = new ModelsServer();
\ No newline at end of file
diff --git a/frontend/src/store/global/slices/common/initialState.ts b/frontend/src/store/global/slices/common/initialState.ts
index dcdb2b828e..77a9f0da39 100644
--- a/frontend/src/store/global/slices/common/initialState.ts
+++ b/frontend/src/store/global/slices/common/initialState.ts
@@ -2,6 +2,7 @@ import { AppRouterInstance } from 'next/dist/shared/lib/app-router-context.share
 
 export enum SidebarTabKey {
   Chat = 'chat',
+  Knowledge = 'knowledge',
   Market = 'market',
   Setting = 'settings',
 }
diff --git a/frontend/src/store/global/slices/settings/selectors/modelProvider.ts b/frontend/src/store/global/slices/settings/selectors/modelProvider.ts
index 09ae50f833..67411c8a94 100644
--- a/frontend/src/store/global/slices/settings/selectors/modelProvider.ts
+++ b/frontend/src/store/global/slices/settings/selectors/modelProvider.ts
@@ -11,6 +11,7 @@ import {
   OpenAIProvider,
   PerplexityProvider,
   ZhiPuProvider,
+  ChatChatProvider,
 } from '@/config/modelProviders';
 import { ChatModelCard, ModelProviderCard } from '@/types/llm';
 import { GlobalLLMProviderKey } from '@/types/settings';
@@ -60,6 +61,10 @@ const perplexityAPIKey = (s: GlobalStore) => modelProvider(s).perplexity.apiKey;
 const enableAnthropic = (s: GlobalStore) => modelProvider(s).anthropic.enabled;
 const anthropicAPIKey = (s: GlobalStore) => modelProvider(s).anthropic.apiKey;
 
+const enableChatChat = (s: GlobalStore) => modelProvider(s).chatchat.enabled;
+const chatChatProxyUrl = (s: GlobalStore) => modelProvider(s).chatchat.endpoint;
+const chatChatModels = (s: GlobalStore) => modelProvider(s).chatchat.models || [];
+
 // const azureModelList = (s: GlobalStore): ModelProviderCard => {
 //   const azure = azureConfig(s);
 //   return {
@@ -134,6 +139,12 @@ const modelSelectList = (s: GlobalStore): ModelProviderCard[] => {
 
   const ollamaChatModels = processChatModels(ollamaModelConfig, OllamaProvider.chatModels);
 
+
+  const chatChatModelConfig = parseModelString(
+    currentSettings(s).languageModel.chatchat.customModelName
+  )
+  const chatChatChatModels = processChatModels(chatChatModelConfig, chatChatModels(s))
+
   return [
     {
       ...OpenAIProvider,
@@ -148,6 +159,7 @@ const modelSelectList = (s: GlobalStore): ModelProviderCard[] => {
     { ...PerplexityProvider, enabled: enablePerplexity(s) },
     { ...AnthropicProvider, enabled: enableAnthropic(s) },
     { ...MistralProvider, enabled: enableMistral(s) },
+    { ...ChatChatProvider, chatModels: chatChatChatModels, enabled: enableChatChat(s) },
   ];
 };
 
@@ -230,4 +242,8 @@ export const modelProviderSelectors = {
   // Mistral
   enableMistral,
   mistralAPIKey,
+
+  // ChatChat
+  enableChatChat,
+  chatChatProxyUrl,
 };
diff --git a/frontend/src/store/knowledge/action.ts b/frontend/src/store/knowledge/action.ts
new file mode 100644
index 0000000000..8c5aafca32
--- /dev/null
+++ b/frontend/src/store/knowledge/action.ts
@@ -0,0 +1,140 @@
+
+import useSWR, { SWRResponse } from 'swr';
+import type { StateCreator } from 'zustand/vanilla';
+import { knowledgeService } from '@/services/knowledge';
+import { globalHelpers } from '@/store/global/helpers';
+
+import type {
+  KnowledgeFormFields, KnowledgeList, Reseponse, KnowledgeFilesList,
+  KnowledgeDelDocsParams, KnowledgeDelDocsRes,
+  KnowledgeRebuildVectorParams, KnowledgeUplodDocsParams, KnowledgeUplodDocsRes,
+  ReAddVectorDBParams, ReAddVectorDBRes,
+  KnowledgeSearchDocsParams, KnowledgeSearchDocsList, KnowledgeSearchDocsListItem, KnowledgeUpdateDocsParams
+} from '@/types/knowledge';
+import type { FetchSSEOptions } from '@/utils/fetch';
+
+import type { Store } from './store';
+
+export interface StoreAction {
+
+  // 当前编辑的知识库
+  editKnowledgeInfo: null | KnowledgeFormFields;
+  setEditKnowledge: (data: KnowledgeFormFields) => void;
+
+  // 知识库数据列表
+  listData: KnowledgeList;
+  useFetchKnowledgeList: () => SWRResponse<Reseponse<KnowledgeList>>;
+  useFetchKnowledgeAdd: (arg: KnowledgeFormFields) => Promise<Reseponse<KnowledgeFormFields>>;
+  useFetchKnowledgeUpdate: (arg: Partial<KnowledgeFormFields>) => Promise<Reseponse<KnowledgeFormFields>>;
+  useFetchKnowledgeDel: (name: string) => Promise<Reseponse<{}>>;
+
+  // files
+  filesData: KnowledgeFilesList;
+  useFetchKnowledgeFilesList: (name: string) => SWRResponse<Reseponse<KnowledgeFilesList>>;
+  useFetchKnowledgeUploadDocs: (arg: FormData) => Promise<Reseponse<{}>>;
+  // useFetchKnowledgeDownloadDocs: (kbName: string, docName: string) => Promise<Reseponse<{}>>;
+  useFetchKnowledgeDownloadDocs: (kbName: string, docName: string) => Promise<void>;
+  useFetcDelInknowledgeDB: (arg: KnowledgeDelDocsParams) => Promise<Reseponse<KnowledgeDelDocsRes>>;
+  useFetcDelInVectorDB: (arg: KnowledgeDelDocsParams) => Promise<Reseponse<KnowledgeDelDocsRes>>;
+  useFetcRebuildVectorDB: (arg: KnowledgeRebuildVectorParams, options: {
+    onFinish: FetchSSEOptions["onFinish"];
+    onMessageHandle: FetchSSEOptions["onMessageHandle"]
+  }) => void;
+  useFetcReAddVectorDB: (arg: ReAddVectorDBParams) => Promise<Reseponse<ReAddVectorDBRes>>; 
+
+  fileSearchData: KnowledgeSearchDocsList;
+  // useFetchSearchDocs:  (arg: KnowledgeSearchDocsParams) => SWRResponse<Reseponse<KnowledgeSearchDocsList>>;
+  useFetchSearchDocs:  (arg: KnowledgeSearchDocsParams) => SWRResponse<KnowledgeSearchDocsList>;
+  useFetcUpdateDocs:  (arg: KnowledgeUpdateDocsParams) => Promise<Reseponse<{}>>; 
+  editContentInfo: null | KnowledgeSearchDocsListItem;
+  setEditContentInfo: (data: KnowledgeSearchDocsListItem) => void;
+}
+
+export const createKnowledgeAction: StateCreator<
+  Store,
+  [['zustand/devtools', never]],
+  [],
+  StoreAction
+> = (set, get) => ({
+  listData: [],
+  useFetchKnowledgeList: () => {
+    return useSWR<Reseponse<KnowledgeList>>(
+      globalHelpers.getCurrentLanguage(),
+      knowledgeService.getList,
+      {
+        onSuccess: (res) => {
+          set({ listData: res.data })
+        },
+      },
+    )
+  },
+  useFetchKnowledgeAdd: async (formValues) => {
+    return await knowledgeService.add(formValues)
+  },
+  useFetchKnowledgeUpdate: async (formValues) => {
+    return await knowledgeService.update(formValues)
+  },
+  useFetchKnowledgeDel: async (name) => {
+    return await knowledgeService.del(name)
+  },
+  filesData: [],
+  useFetchKnowledgeFilesList: (knowledge_base_name) => {
+    return useSWR<Reseponse<KnowledgeFilesList>>(
+      [globalHelpers.getCurrentLanguage(), knowledge_base_name],
+      knowledgeService.getFilesList(knowledge_base_name),
+      {
+        onSuccess: (res) => {
+          set({ filesData: res.data })
+        },
+      },
+    )
+  },
+  useFetchKnowledgeUploadDocs: (formData) => {
+    return knowledgeService.uploadDocs(formData);
+  },
+  useFetchKnowledgeDownloadDocs: (kbName: string, docName: string) => {
+    return knowledgeService.downloadDocs(kbName, docName);
+  },
+  useFetcDelInknowledgeDB: (params) => {
+    return knowledgeService.delInknowledgeDB(params);
+  },
+  useFetcDelInVectorDB: async (name) => {
+    return await knowledgeService.delVectorDocs(name)
+  },
+  useFetcRebuildVectorDB: (params, options) => {
+    return knowledgeService.rebuildVectorDB(params, options);
+  },
+  useFetcReAddVectorDB: (params) => {
+    return knowledgeService.reAddVectorDB(params);
+  },
+
+
+  editKnowledgeInfo: null,
+  setEditKnowledge: (data) => {
+    set({ editKnowledgeInfo: data })
+  },
+  
+  
+  fileSearchData: [],
+  useFetchSearchDocs: (params) => {
+    // return useSWR<Reseponse<KnowledgeSearchDocsList>>(
+    return useSWR<KnowledgeSearchDocsList>(
+      globalHelpers.getCurrentLanguage(),
+      ()=> knowledgeService.searchDocs(params),
+      {
+        onSuccess: (res) => {
+          // set({ fileSearchData: res.data })
+          set({ fileSearchData: res })
+        },
+      },
+    )
+  },
+  useFetcUpdateDocs: (params) => {
+    return knowledgeService.updateDocs(params);
+  }, 
+  editContentInfo: null,
+  setEditContentInfo: (data) => {
+    set({ editContentInfo: data })
+  },
+
+});
diff --git a/frontend/src/store/knowledge/index.ts b/frontend/src/store/knowledge/index.ts
new file mode 100644
index 0000000000..c718aee108
--- /dev/null
+++ b/frontend/src/store/knowledge/index.ts
@@ -0,0 +1,3 @@
+export * from './store';
+export { useKnowledgeStore } from './store';
+export { type Store } from './store';
diff --git a/frontend/src/store/knowledge/store.ts b/frontend/src/store/knowledge/store.ts
new file mode 100644
index 0000000000..56d5fd4875
--- /dev/null
+++ b/frontend/src/store/knowledge/store.ts
@@ -0,0 +1,22 @@
+import { subscribeWithSelector, devtools, persist } from 'zustand/middleware';
+import { shallow } from 'zustand/shallow';
+import { createWithEqualityFn } from 'zustand/traditional';
+import type { StateCreator } from 'zustand/vanilla'; 
+ 
+import { isDev } from '@/utils/env';
+
+import { type StoreAction, createKnowledgeAction } from './action'; 
+export type Store = StoreAction;
+ 
+const createStore: StateCreator<Store, [['zustand/devtools', never]]> = (...parameters) => ({ 
+  ...createKnowledgeAction(...parameters),
+});
+ 
+export const useKnowledgeStore = createWithEqualityFn<Store>()(
+  subscribeWithSelector(
+    devtools(createStore, {
+      name: 'ChatChat_Chat' + (isDev ? '_DEV' : ''),
+    }),
+  ),
+  shallow,
+);
diff --git a/frontend/src/types/knowledge.ts b/frontend/src/types/knowledge.ts
new file mode 100644
index 0000000000..1a6818aefb
--- /dev/null
+++ b/frontend/src/types/knowledge.ts
@@ -0,0 +1,122 @@
+export interface Reseponse<T> { code: number; msg: string; data: T }
+
+
+// create Knowledge fields
+export interface KnowledgeFormFields {
+    knowledge_base_name: string;
+    vector_store_type?: string;
+    kb_info?: string;
+    embed_model?: string;
+    metadata?: any;
+    type?: string;
+}
+
+// Knowledge base list
+export interface KnowledgeListFields {
+    "id": number;
+    "kb_name": string;
+    "kb_info": string;
+    "vs_type": string;
+    "embed_model": string;
+    "file_count": number;
+    "create_time": string;
+}
+export type KnowledgeList = KnowledgeListFields[];
+
+// Knowledge base file list
+export type KnowledgeFilesFields = {
+    No: number;
+    docs_count: number;
+    document_loader: string;
+    file_ext: string;
+    file_name: string;
+    file_version: number;
+    in_db: boolean;
+    in_folder: boolean;
+    kb_name: string;
+    text_splitter: string;
+};
+export type KnowledgeFilesList = KnowledgeFilesFields[];
+
+// Example Delete parameters of the knowledge base file
+export interface KnowledgeDelDocsParams {
+    knowledge_base_name: string;
+    file_names: string[];
+    delete_content: boolean;
+    not_refresh_vs_cache: boolean;
+}
+export interface KnowledgeDelDocsRes { }
+
+
+// upload docs
+export interface KnowledgeUplodDocsParams {
+    knowledge_base_name: string;
+    files: File[];
+    override?: boolean;
+    to_vector_store?: string;
+    chunk_size?: string;
+    chunk_overlap?: string;
+    zh_title_enhance?: string;
+    docs?: { file_name: { page_content: string; type?: string; metadata?: string; }[] };
+    docsnot_refresh_vs_cache?: string;
+}
+export interface KnowledgeUplodDocsRes { }
+
+export interface KnowledgeUpdateDocsParams {
+    knowledge_base_name: string;
+    file_names: string[],
+    override_custom_docs?: boolean;
+    chunk_size?: number;
+    to_vector_store?: boolean;
+    chunk_overlap?: number;
+    zh_title_enhance?: boolean;
+    not_refresh_vs_cache?: boolean;
+    docs?: string | { [file_name: string]: { page_content: string; type?: string; metadata?: string; }[] };
+    // docs?: string;
+}
+
+// re add docs
+export interface ReAddVectorDBParams {
+    "knowledge_base_name": string,
+    "file_names": string[];
+    "chunk_size": number;
+    "chunk_overlap": number;
+    "zh_title_enhance": boolean;
+    "override_custom_docs": boolean;
+    "docs": string;
+    "not_refresh_vs_cache": boolean
+}
+export interface ReAddVectorDBRes { }
+
+
+// Rebuild the vector library
+export interface KnowledgeRebuildVectorParams {
+    "knowledge_base_name": string;
+    "allow_empty_kb": boolean;
+    "vs_type": string;
+    "embed_model": string
+    "chunk_size": number;
+    "chunk_overlap": number;
+    "zh_title_enhance": boolean;
+    "not_refresh_vs_cache": boolean;
+}
+export interface KnowledgeRebuildVectorRes { }
+
+
+// Knowledge file content list
+export interface KnowledgeSearchDocsParams {
+    "query"?: string;
+    "knowledge_base_name": string;
+    "top_k"?: number;
+    "score_threshold"?: number;
+    "file_name": string,
+    "metadata"?: Record<string, string>;
+}
+
+export interface KnowledgeSearchDocsListItem {
+    id?: number;
+    page_content: string;
+    type?: string;
+    metadata?: string;
+}
+export type KnowledgeSearchDocsList = KnowledgeSearchDocsListItem[];
\ No newline at end of file
diff --git a/frontend/src/types/message/index.ts b/frontend/src/types/message/index.ts
index 0b8b5a1e7a..0f28496d1d 100644
--- a/frontend/src/types/message/index.ts
+++ b/frontend/src/types/message/index.ts
@@ -17,6 +17,11 @@ export interface ChatMessageError {
   type: ErrorType | IPluginErrorType | ILobeAgentRuntimeErrorType;
 }
 
+export interface ModelSelectorError {
+  message: string;
+  type: ErrorType;
+}
+
 export interface ChatTranslate extends Translate {
   content?: string;
 }
diff --git a/frontend/src/types/models.ts b/frontend/src/types/models.ts
new file mode 100644
index 0000000000..49102f5fad
--- /dev/null
+++ b/frontend/src/types/models.ts
@@ -0,0 +1,15 @@
+interface Model {
+  id: string;
+  created: number; // 时间戳
+  platform_name: string;
+  owned_by: string;
+  object: string;
+  tokens?: number;
+  displayName?: string;
+}
+
+export interface ModelsResponse {
+  object?: 'list';
+  data?: Model[];
+  error?: string;
+}
\ No newline at end of file
diff --git a/frontend/src/types/settings/modelProvider.ts b/frontend/src/types/settings/modelProvider.ts
index 7d78d2b0ce..0ca05fdbae 100644
--- a/frontend/src/types/settings/modelProvider.ts
+++ b/frontend/src/types/settings/modelProvider.ts
@@ -1,3 +1,5 @@
+import { ChatModelCard } from "../llm";
+
 export type CustomModels = { displayName: string; id: string }[];
 
 export interface OpenAIConfig {
@@ -22,23 +24,27 @@ export interface AzureOpenAIConfig {
   deployments: string;
   enabled: boolean;
   endpoint?: string;
+  models?: ChatModelCard[]
 }
 
 export interface ZhiPuConfig {
   apiKey?: string;
   enabled: boolean;
   endpoint?: string;
+  models?: ChatModelCard[]
 }
 
 export interface MoonshotConfig {
   apiKey?: string;
   enabled: boolean;
+  models?: ChatModelCard[]
 }
 
 export interface GoogleConfig {
   apiKey?: string;
   enabled: boolean;
   endpoint?: string;
+  models?: ChatModelCard[]
 }
 
 export interface AWSBedrockConfig {
@@ -46,28 +52,40 @@ export interface AWSBedrockConfig {
   enabled: boolean;
   region?: string;
   secretAccessKey?: string;
+  models?: ChatModelCard[]
 }
 
 export interface OllamaConfig {
   customModelName?: string;
   enabled?: boolean;
   endpoint?: string;
+  models?: ChatModelCard[]
 }
 
 export interface PerplexityConfig {
   apiKey?: string;
   enabled: boolean;
   endpoint?: string;
+  models?: ChatModelCard[]
 }
 
 export interface AnthropicConfig {
   apiKey?: string;
   enabled: boolean;
+  models?: ChatModelCard[]
 }
 
 export interface MistralConfig {
   apiKey?: string;
   enabled: boolean;
+  models?: ChatModelCard[]
+}
+
+export interface ChatChatConfig {
+  customModelName?: string;
+  enabled?: boolean;
+  endpoint?: string;
+  models?: ChatModelCard[]
 }
 
 export interface GlobalLLMConfig {
@@ -81,6 +99,7 @@ export interface GlobalLLMConfig {
   openAI: OpenAIConfig;
   perplexity: PerplexityConfig;
   zhipu: ZhiPuConfig;
+  chatchat: ChatChatConfig;
 }
 
 export type GlobalLLMProviderKey = keyof GlobalLLMConfig;


Test patch:
diff --git a/frontend/src/libs/agent-runtime/chatchat/index.test.ts b/frontend/src/libs/agent-runtime/chatchat/index.test.ts
new file mode 100644
index 0000000000..bfce5c9327
--- /dev/null
+++ b/frontend/src/libs/agent-runtime/chatchat/index.test.ts
@@ -0,0 +1,102 @@
+// @vitest-environment node
+import OpenAI from 'openai';
+import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+
+import { ChatStreamCallbacks } from '@/libs/agent-runtime';
+
+import * as debugStreamModule from '../utils/debugStream';
+import { LobeChatChatAI } from './index';
+
+const provider = 'knowledge';
+const defaultBaseURL = 'http://localhost:7861/v1';
+const bizErrorType = 'knowledgeBizError';
+const invalidErrorType = 'InvalidKnowledgeArgs';
+
+// Mock the console.error to avoid polluting test output
+vi.spyOn(console, 'error').mockImplementation(() => {});
+
+let instance: LobeChatChatAI;
+
+beforeEach(() => {
+  instance = new LobeChatChatAI({ apiKey: 'knowledge', baseURL: defaultBaseURL });
+
+  // 使用 vi.spyOn 来模拟 chat.completions.create 方法
+  vi.spyOn(instance['client'].chat.completions, 'create').mockResolvedValue(
+    new ReadableStream() as any,
+  );
+});
+
+afterEach(() => {
+  vi.clearAllMocks();
+});
+
+describe('LobeChatChatAI', () => {
+
+  describe('init', ()=>{
+    it('should init with default baseURL', () => {
+      expect(instance.baseURL).toBe(defaultBaseURL);
+    });
+  })
+
+  describe('chat', () => {
+    it('should return a StreamingTextResponse on successful API call', 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: 'gpt-3.5-turbo',
+        temperature: 0,
+      });
+
+      // Assert
+      expect(result).toBeInstanceOf(Response);
+    });
+
+    it('should return a StreamingTextResponse on successful API call', async () => {
+      // Arrange
+      const mockResponse = Promise.resolve({
+        "id": "chatcmpl-98QIb3NiYLYlRTB6t0VrJ0wntNW6K",
+        "object": "chat.completion",
+        "created": 1711794745,
+        "model": "gpt-3.5-turbo-0125",
+        "choices": [
+          {
+            "index": 0,
+            "message": {
+              "role": "assistant",
+              "content": "你好！有什么可以帮助你的吗？"
+            },
+            "logprobs": null,
+            "finish_reason": "stop"
+          }
+        ],
+        "usage": {
+          "prompt_tokens": 9,
+          "completion_tokens": 17,
+          "total_tokens": 26
+        },
+        "system_fingerprint": "fp_b28b39ffa8"
+      });
+
+      (instance['client'].chat.completions.create as Mock).mockResolvedValue(mockResponse);
+
+      // Act
+      const result = await instance.chat({
+        messages: [{ content: 'Hello', role: 'user' }],
+        model: 'gpt-3.5-turbo',
+        stream: false,
+        temperature: 0,
+      });
+
+      // Assert
+      expect(result).toBeInstanceOf(Response);
+    });
+  })
+
+
+});
\ No newline at end of file