瀏覽代碼

分离bigmodel与deepSeek

李富豪 9 月之前
父節點
當前提交
d0c5a4c8d7

+ 9 - 3
app/client/api.ts

@@ -5,7 +5,8 @@ import {
   ServiceProvider,
 } from "../constant";
 import { ChatMessage, ModelType, useAccessStore, useChatStore } from "../store";
-import { BigModelApi } from "./platforms/bigmodel";
+import { BigModelApi } from "./platforms/bigModel";
+import { DeepSeekApi } from "./platforms/deepSeek";
 import { ChatGPTApi, DalleRequestPayload } from "./platforms/openai";
 import { GeminiProApi } from "./platforms/google";
 import { ClaudeApi } from "./platforms/anthropic";
@@ -109,9 +110,12 @@ export class ClientApi {
 
   constructor(provider: ModelProvider = ModelProvider.GPT) {
     switch (provider) {
-      case ModelProvider.GLM:
+      case ModelProvider.BigModel:
         this.llm = new BigModelApi();
         break;
+      case ModelProvider.DeepSeek:
+        this.llm = new DeepSeekApi();
+        break;
       case ModelProvider.GeminiPro:
         this.llm = new GeminiProApi();
         break;
@@ -287,7 +291,9 @@ export function getHeaders() {
 export function getClientApi(provider: ServiceProvider): ClientApi {
   switch (provider) {
     case ServiceProvider.BigModel:
-      return new ClientApi(ModelProvider.GLM);
+      return new ClientApi(ModelProvider.BigModel);
+    case ServiceProvider.DeepSeek:
+      return new ClientApi(ModelProvider.DeepSeek);
     case ServiceProvider.Google:
       return new ClientApi(ModelProvider.GeminiPro);
     case ServiceProvider.Anthropic:

+ 11 - 28
app/client/platforms/bigmodel.ts

@@ -18,17 +18,10 @@ import api from "@/app/api/api";
 export class BigModelApi implements LLMApi {
   public baseURL: string;
   public apiPath: string;
-  public deepSeekApiPath: string;
-  public apiType: 'bigModel' | 'deepSeek';
 
   constructor() {
     this.baseURL = '/bigmodel-api';
     this.apiPath = this.baseURL + '/bigmodel/api/model-api/sse-invoke';
-    // 切换deepSeek模型请求地址
-    // this.deepSeekApiPath = 'http://192.168.3.209:8000/chat';
-    this.deepSeekApiPath = 'http://sse.deepseek.ryuiso.com:56780/chat';
-    // 切换api请求类型
-    this.apiType = 'deepSeek';
   }
 
   async chat(options: ChatOptions) {
@@ -48,33 +41,23 @@ export class BigModelApi implements LLMApi {
       });
     }
 
-    // 大模型参数
-    let params: any = {};
-
-    if (this.apiType === 'bigModel') {
-      params = {
-        appId: options.config.appId,// 应用id
-        prompt: userMessages,
-        // 进阶配置
-        request_id: 'jkec2024-knowledge-base',
-        returnType: undefined,
-        knowledge_ids: undefined,
-        document_ids: undefined,
-      };
-    } else {
-      params = {
-        model: 'deepseek-r1:8b',
-        messages: userMessages,
-        stream: true,
-      };
-    }
+    // 参数
+    const params = {
+      appId: options.config.appId,// 应用id
+      prompt: userMessages,
+      // 进阶配置
+      request_id: 'jkec2024-knowledge-base',
+      returnType: undefined,
+      knowledge_ids: undefined,
+      document_ids: undefined,
+    };
 
     const controller = new AbortController();
 
     options.onController?.(controller);
 
     try {
-      const chatPath = this.apiType === 'bigModel' ? this.apiPath : this.deepSeekApiPath;
+      const chatPath = this.apiPath;
       const chatPayload = {
         method: "POST",
         body: JSON.stringify(params),

+ 178 - 0
app/client/platforms/deepSeek.ts

@@ -0,0 +1,178 @@
+"use client";
+import { REQUEST_TIMEOUT_MS } from "@/app/constant";
+import {
+  ChatOptions,
+  LLMApi,
+  LLMModel,
+} from "../api";
+import Locale from "../../locales";
+import {
+  EventStreamContentType,
+  fetchEventSource,
+} from "@fortaine/fetch-event-source";
+import { prettyObject } from "@/app/utils/format";
+import { getMessageTextContent } from "@/app/utils";
+
+export class DeepSeekApi implements LLMApi {
+  public apiPath: string;
+
+  constructor() {
+    this.apiPath = 'http://sse.deepseek.ryuiso.com:56780/chat';
+  }
+
+  async chat(options: ChatOptions) {
+    const messages = options.messages.map((item) => {
+      return {
+        role: item.role,
+        content: getMessageTextContent(item),
+      }
+    });
+
+    const userMessages = messages.filter(item => item.content);
+
+    if (userMessages.length % 2 === 0) {
+      userMessages.unshift({
+        role: "user",
+        content: "⠀",
+      });
+    }
+
+    // 参数
+    const params = {
+      model: 'deepseek-r1:8b',
+      messages: userMessages,
+      stream: true,
+      // 进阶配置
+      max_tokens: undefined,
+      temperature: undefined,
+    };
+
+    const controller = new AbortController();
+
+    options.onController?.(controller);
+
+    try {
+      const chatPath = this.apiPath;
+      const chatPayload = {
+        method: "POST",
+        body: JSON.stringify(params),
+        signal: controller.signal,
+        headers: {
+          'Content-Type': 'application/json',
+        },
+      };
+
+      const requestTimeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
+
+      let responseText = "";
+      let remainText = "";
+      let finished = false;
+
+      function animateResponseText() {
+        if (finished || controller.signal.aborted) {
+          responseText += remainText;
+          if (responseText?.length === 0) {
+            options.onError?.(new Error("请求已中止,请检查网络环境。"));
+          }
+          return;
+        }
+
+        if (remainText.length > 0) {
+          const fetchCount = Math.max(1, Math.round(remainText.length / 60));
+          const fetchText = remainText.slice(0, fetchCount);
+          responseText += fetchText;
+          remainText = remainText.slice(fetchCount);
+          options.onUpdate?.(responseText, fetchText);
+        }
+
+        requestAnimationFrame(animateResponseText);
+      }
+
+      animateResponseText();
+
+      const finish = () => {
+        if (!finished) {
+          finished = true;
+          console.log(remainText, 'remainText');
+
+          let text = responseText + remainText;
+          options.onFinish(text);
+        }
+      };
+
+      controller.signal.onabort = finish;
+
+      fetchEventSource(chatPath, {
+        ...chatPayload,
+        async onopen(res: any) {
+          clearTimeout(requestTimeoutId);
+          const contentType = res.headers.get("content-type");
+
+          if (contentType?.startsWith("text/plain")) {
+            responseText = await res.clone().text();
+            return finish();
+          }
+
+          if (
+            !res.ok ||
+            !res.headers.get("content-type")?.startsWith(EventStreamContentType) ||
+            res.status !== 200
+          ) {
+            const responseTexts = [responseText];
+            let extraInfo = await res.clone().text();
+            try {
+              const resJson = await res.clone().json();
+              extraInfo = prettyObject(resJson);
+            } catch { }
+
+            if (res.status === 401) {
+              responseTexts.push(Locale.Error.Unauthorized);
+            }
+
+            if (extraInfo) {
+              responseTexts.push(extraInfo);
+            }
+
+            responseText = responseTexts.join("\n\n");
+
+            return finish();
+          }
+        },
+        onmessage: (msg) => {
+          const info = JSON.parse(msg.data);
+          if (info.event === 'finish') {
+            return finish();
+          }
+          // 获取当前的数据
+          const currentData = info.data;
+          const format = '```think' + '' + '```';
+          if (responseText.startsWith(format)) {
+            responseText = responseText.replace(format, '');
+          }
+          remainText += currentData;
+        },
+        onclose() {
+          finish();
+        },
+        onerror(e) {
+          options.onError?.(e);
+          throw e;
+        },
+        openWhenHidden: true,
+      });
+    } catch (e) {
+      options.onError?.(e as Error);
+    }
+  }
+
+  async usage() {
+    return {
+      used: 0,
+      total: 0,
+    };
+  }
+
+  async models(): Promise<LLMModel[]> {
+    return [];
+  }
+}

+ 35 - 28
app/components/DeekSeek.tsx

@@ -3,56 +3,63 @@ import { Chat } from './DeepSeekChat';
 import whiteLogo from "../icons/whiteLogo.png";
 import jkxz from "../icons/jkxz.png";
 import { useChatStore } from "../store";
+import { useMobileScreen } from '../utils';
 import './deepSeek.scss';
 
 const DeekSeek: React.FC = () => {
     const chatStore = useChatStore();
+    const isMobileScreen = useMobileScreen();
+    const [list, setList] = React.useState<{ title: string, onClick?: () => void }[]>([]);
 
     React.useEffect(() => {
         chatStore.clearSessions();
+        setList([
+            {
+                title: '知识库问答',
+                onClick: () => {
+                    window.open('http://xia0miduo.gicp.net:3002/', '_blank');
+                }
+            },
+            {
+                title: '员工小百科',
+            },
+            {
+                title: '报批报建助手',
+            },
+            {
+                title: '施工方案审查',
+            },
+            {
+                title: '更多',
+            }
+        ])
     }, []);
 
     return (
         <div className='deekSeek'>
-            <div className='deekSeek-header'>
-                <div style={{ display: 'flex', alignItems: 'center', marginRight: 20 }}>
+            <div className='deekSeek-header' style={{ justifyContent: isMobileScreen ? 'flex-start' : 'center' }}>
+                <div style={{ display: 'flex', alignItems: 'center', margin: '0 20px' }}>
                     <img src={whiteLogo.src} style={{ width: 20, marginRight: 10 }} />
-                    <div>
+                    <div style={{ whiteSpace: 'nowrap' }}>
                         上海建科
                     </div>
                 </div>
-                <div style={{ marginRight: 20, color: '#98b4fa', cursor: 'pointer' }} onClick={() => {
-                    window.open('http://xia0miduo.gicp.net:3002/', '_blank');
-                }}>
-                    知识库问答
-                </div>
-                <div style={{ marginRight: 20, color: '#98b4fa' }}>
-                    员工小百科
-                </div>
-                <div style={{ marginRight: 20, color: '#98b4fa' }}>
-                    报批报建助手
-                </div>
-                <div style={{ marginRight: 20, color: '#98b4fa' }}>
-                    施工方案审查
-                </div>
-                {/* <div style={{ marginRight: 20, color: '#98b4fa' }}> */}
-                {/*     OCR */}
-                {/* </div> */}
-                {/* <div style={{ marginRight: 20, color: '#98b4fa' }}> */}
-                {/*     后期扩展 */}
-                {/* </div> */}
-                <div style={{ color: '#98b4fa' }}>
-                    更多
-                </div>
+                {
+                    list.map((item, index) => {
+                        return <div style={{ whiteSpace: 'nowrap', marginRight: 20, color: '#98b4fa', cursor: 'pointer' }} onClick={item.onClick} key={index}>
+                            {item.title}
+                        </div>
+                    })
+                }
             </div>
             <div className='deekSeek-content'>
                 <div className='deekSeek-content-title'>
                     <img src={jkxz.src} />
                 </div>
-                <div className='deekSeek-content-title2'>
+                <div className='deekSeek-content-title2' style={{ marginBottom: isMobileScreen ? 10 : 35 }}>
                     智能问答助手
                 </div>
-                <div className='deekSeek-content-outer'>
+                <div className={isMobileScreen ? 'deekSeek-content-mobile' : 'deekSeek-content-pc'}>
                     <Chat />
                 </div>
             </div>

+ 1 - 135
app/components/DeepSeekChat.tsx

@@ -589,75 +589,8 @@ export function ChatActions(props: {
     }
   }, [chatStore, currentModel, models]);
 
-  const fetchGuessList = async (record: ChatMessage) => {
-    try {
-      const data = {
-        messages: [
-          {
-            content: record.content,
-            role: record.role,
-          }
-        ]
-      }
-      const res = await api.post('/bigmodel/api/async/completions', data);
-      setGuessList(res.data);
-    } catch (error) {
-      console.error(error);
-    }
-  }
-
-  useEffect(() => {
-    setGuessList([]);
-    const messages = session.messages.slice();
-    const backList = messages.reverse();
-    const item = backList.find(item => item.content && item.role === 'assistant')
-    if (item) {
-      fetchGuessList(item)
-    }
-  }, [session.messages.length]);
-
   return (
     <div className={styles["chat-input-actions"]}>
-      {/* <CallWord */}
-      {/*   setUserInput={props.setUserInput} */}
-      {/*   doSubmit={props.doSubmit} */}
-      {/* /> */}
-
-      {/* <ChatAction
-        onClick={props.showPromptHints}
-        text={Locale.Chat.InputActions.Prompt}
-        icon={<PromptIcon />}
-      /> */}
-
-      {/* <ChatAction
-        onClick={() => {
-          navigate(Path.Masks);
-        }}
-        text={Locale.Chat.InputActions.Masks}
-        icon={<MaskIcon />}
-      /> */}
-
-      {/* <ChatAction
-        text={Locale.Chat.InputActions.Clear}
-        icon={<BreakIcon />}
-        onClick={() => {
-          chatStore.updateCurrentSession((session) => {
-            if (session.clearContextIndex === session.messages.length) {
-              session.clearContextIndex = undefined;
-            } else {
-              session.clearContextIndex = session.messages.length;
-              session.memoryPrompt = ""; // will clear memory
-            }
-          });
-        }}
-      /> */}
-
-      {/* <ChatAction
-        onClick={() => setShowModelSelector(true)}
-        text={currentModelName}
-        icon={<RobotIcon />}
-      /> */}
-
       {showModelSelector && (
         <Selector
           defaultSelectedValue={`${currentModel}@${currentProviderName}`}
@@ -718,12 +651,6 @@ export function ChatActions(props: {
         />
       )}
 
-      {/* <ChatAction
-        onClick={() => setShowPluginSelector(true)}
-        text={Locale.Plugin.Name}
-        icon={<PluginIcon />}
-      /> */}
-
       {showPluginSelector && (
         <Selector
           multiple
@@ -878,67 +805,6 @@ function _Chat() {
   const [questionList, setQuestionList] = useState<QuestionList>([]);
   const location = useLocation();
 
-  // 获取应用列表
-  const fetchApplicationList = async () => {
-    setLoading(true);
-    try {
-      const res = await api.get('/bigmodel/api/application/list');
-      const list = res.data.filter((item: any) => item.appId !== '1234567890123456789').map((item: any) => {
-        return {
-          label: item.name,
-          value: item.appId,
-        }
-      })
-      setAppList(list);
-      let appValue = '';
-      const search = location.search;
-      if (search.startsWith('?appId=')) {
-        const value = search.slice(7);
-        if (list.find((item: any) => item.value === value)) {
-          appValue = value;
-        } else {
-          appValue = list[0]?.value;
-        }
-      } else {
-        appValue = list[0]?.value;
-      }
-      setAppValue(appValue);
-      globalStore.setSelectedAppId(appValue);
-      chatStore.updateCurrentSession((session) => {
-        session.appId = appValue;
-      });
-    } catch (error) {
-      console.error(error);
-    } finally {
-      setLoading(false);
-    }
-  }
-
-  // 获取预设问题列表
-  const fetchDefaultQuestion = async (appId: string) => {
-    try {
-      const res = await api.get(`/bigmodel/api/presets/${appId}`);
-      setQuestionList(res.data);
-    } catch (error) {
-      console.error(error);
-    } finally {
-      setLoading(false);
-    }
-  }
-
-  const init = async () => {
-    await fetchApplicationList();
-  }
-
-  useEffect(() => {
-    init();
-  }, [])
-
-  useEffect(() => {
-    if (appValue) {
-      fetchDefaultQuestion(appValue);
-    }
-  }, [appValue])
 
   const [inputRows, setInputRows] = useState(2);
   const measure = useDebouncedCallback(
@@ -1566,7 +1432,7 @@ function _Chat() {
           <textarea
             id="chat-input"
             ref={inputRef}
-            className={styles["chat-input"]}
+            className={styles["chat-input2"]}
             placeholder={Locale.Chat.Input(submitKey)}
             onInput={(e) => onInput(e.currentTarget.value)}
             value={userInput}

+ 8 - 0
app/components/chat.module.scss

@@ -601,6 +601,10 @@
   border: 1px solid var(--primary);
 }
 
+.chat-input-panel-inner:has(.chat-input2:focus) {
+  border-color: #495fe6;
+}
+
 .chat-input {
   height: 100%;
   width: 100%;
@@ -622,6 +626,10 @@
   color: #FFFFFF;
 }
 
+.chat-input2 {
+  @extend .chat-input;
+}
+
 // placeholder颜色
 .chat-input::placeholder {
   color: #FFFFFF;

+ 11 - 2
app/components/deepSeek.scss

@@ -11,6 +11,8 @@
     color: #FFFFFF;
     justify-content: center;
     align-items: center;
+    overflow-x: auto;
+    overflow-y: hidden;
   }
 
   &-content {
@@ -34,15 +36,22 @@
     &-title2 {
       font-size: 20px;
       color: #FFFFFF;
-      margin-bottom: 35px;
     }
 
-    &-outer {
+    &-pc {
       width: 32%;
       height: 78%;
       background: #FFFFFF;
       border-radius: 12px;
       overflow: hidden;
     }
+
+    &-mobile {
+      width: 90%;
+      height: 82%;
+      background: #FFFFFF;
+      border-radius: 12px;
+      overflow: hidden;
+    }
   }
 }

+ 43 - 26
app/components/home.tsx

@@ -2,7 +2,7 @@
 
 require("../polyfill");
 
-import { useState, useEffect } from "react";
+import { useState, useEffect, FC } from "react";
 
 import styles from "./home.module.scss";
 
@@ -28,7 +28,7 @@ import { useAppConfig } from "../store/config";
 import { AuthPage } from "./auth";
 import { getClientConfig } from "../config/client";
 import { type ClientApi, getClientApi } from "../client/api";
-import { useAccessStore } from "../store";
+import { useAccessStore, useChatStore } from "../store";
 
 export function Loading() {
   /** second版本注释掉进度条 */
@@ -265,36 +265,53 @@ function Screen() {
       </Routes>
     );
   }
+
+  const DeepSeekApp: FC = () => {
+    const chatStore = useChatStore();
+
+    useEffect(() => {
+      chatStore.setModel('DeepSeek');
+    }, [])
+
+    return (
+      <>
+        <WindowContent>
+          <Routes>
+            <Route path='/deepSeek' element={<DeekSeek />} />
+          </Routes>
+        </WindowContent>
+      </>
+    )
+  }
+
+  const BigModelApp: FC = () => {
+    const chatStore = useChatStore();
+
+    useEffect(() => {
+      chatStore.setModel('BigModel');
+    }, [])
+
+    return (
+      <>
+        <SideBar className={isHome ? styles["sidebar-show"] : ""} />
+        <WindowContent>
+          <Routes>
+            <Route path={Path.Home} element={<Chat />} />
+            {/* <Route path={'/record'} element={<Record />} /> */}
+            <Route path={Path.Chat} element={<Chat />} />
+          </Routes>
+        </WindowContent>
+      </>
+    )
+  }
+
   const renderContent = () => {
     if (isAuth) return <AuthPage />;
     if (isSd) return <Sd />;
     if (isSdNew) return <Sd />;
     return (
       <>
-        {
-          location.pathname === '/deepSeek' ?
-            <>
-              <WindowContent>
-                <Routes>
-                  <Route path='/deepSeek' element={<DeekSeek />} />
-                </Routes>
-              </WindowContent>
-            </>
-            :
-            <>
-              <SideBar className={isHome ? styles["sidebar-show"] : ""} />
-              <WindowContent>
-                <Routes>
-                  <Route path={Path.Home} element={<Chat />} />
-                  {/* <Route path={'/record'} element={<Record />} /> */}
-                  {/* <Route path={Path.NewChat} element={<NewChat />} /> */}
-                  {/* <Route path={Path.Masks} element={<MaskPage />} /> */}
-                  <Route path={Path.Chat} element={<Chat />} />
-                  {/* <Route path={Path.Settings} element={<Settings />} /> */}
-                </Routes>
-              </WindowContent>
-            </>
-        }
+        {location.pathname === '/deepSeek' ? <DeepSeekApp /> : <BigModelApp />}
       </>
     );
   };

+ 12 - 0
app/components/markdown.tsx

@@ -105,6 +105,7 @@ export function PreCode(props: { children: any }) {
       ) as NodeListOf<HTMLElement>;
       const wrapLanguages = [
         "",
+        "think",
         "md",
         "markdown",
         "text",
@@ -213,6 +214,17 @@ function _MarkDownContent(props: { content: string }) {
       // 控制不同标签的显示样式
       components={{
         pre: PreCode,
+        code: ({ className, children }) => {
+          if (className && className.includes('language-think')) {
+            return (
+              <code style={{ whiteSpace: 'pre-wrap', background: '#f3f4f6', color: '#525252' }}>
+                {children}
+              </code>
+            );
+          } else {
+            return children;
+          }
+        },
         p: (pProps) => <p {...pProps} dir="auto" />,
         a: (aProps) => {
           const href = aProps.href || "";

+ 3 - 1
app/constant.ts

@@ -107,6 +107,7 @@ export const EXPORT_MESSAGE_CLASS_NAME = "export-markdown";
 
 export enum ServiceProvider {
   BigModel = "BigModel",
+  DeepSeek = "DeepSeek",
   OpenAI = "OpenAI",
   Azure = "Azure",
   Google = "Google",
@@ -128,7 +129,8 @@ export enum GoogleSafetySettingsThreshold {
 }
 
 export enum ModelProvider {
-  GLM = "GLM",
+  BigModel = "bigModel",
+  DeepSeek = "deepSeek",
   Stability = "Stability",
   GPT = "GPT",
   GeminiPro = "GeminiPro",

+ 12 - 8
app/store/chat.ts

@@ -166,6 +166,7 @@ function fillTemplateWith(input: string, modelConfig: ModelConfig) {
 }
 
 const DEFAULT_CHAT_STATE = {
+  model: 'BigModel' as 'BigModel' | 'DeepSeek',
   sessions: [createEmptySession()],
   currentSessionIndex: 0,
 };
@@ -179,8 +180,10 @@ export const useChatStore = createPersistStore(
         ...methods,
       };
     }
-
     const methods = {
+      setModel(model: 'BigModel' | 'DeepSeek') {
+        set({ model: model });
+      },
       clearSessions() {
         set(() => ({
           sessions: [createEmptySession()],
@@ -369,9 +372,9 @@ export const useChatStore = createPersistStore(
             botMessage,
           ]);
         });
-        // 固定使用大模型
-        const providerName = 'BigModel' as ServiceProvider;
-        const api: ClientApi = getClientApi(providerName);
+        // 使用BigModel或DeepSeek
+        const model = get().model as ServiceProvider;
+        const api: ClientApi = getClientApi(model);
         api.llm.chat({
           messages: sendMessages,
           config: {
@@ -555,9 +558,9 @@ export const useChatStore = createPersistStore(
           return;
         }
 
-        // 固定使用大模型
-        const providerName = 'BigModel' as ServiceProvider;
-        const api: ClientApi = getClientApi(providerName);
+        // 使用BigModel或DeepSeek
+        const model = get().model as ServiceProvider;
+        const api: ClientApi = getClientApi(model);
 
         // remove error messages if any
         const messages = session.messages;
@@ -581,7 +584,7 @@ export const useChatStore = createPersistStore(
             config: {
               model: getSummarizeModel(session.mask.modelConfig.model),
               stream: false,
-              providerName,
+              providerName: model,
             },
             onFinish(message) {
               get().updateCurrentSession(
@@ -602,6 +605,7 @@ export const useChatStore = createPersistStore(
 
         const historyMsgLength = countMessages(toBeSummarizedMsgs);
 
+        // @ts-ignore
         if (historyMsgLength > modelConfig?.max_tokens ?? 4000) {
           const n = toBeSummarizedMsgs.length;
           toBeSummarizedMsgs = toBeSummarizedMsgs.slice(