From c99cd31b6b7fbc00d3b5f386c5b7b9b7af31df90 Mon Sep 17 00:00:00 2001
From: lloydzhou <lloydzhou@qq.com>
Date: Sat, 24 Aug 2024 15:34:33 +0800
Subject: [PATCH 01/38] add openapi-client-axios

---
 package.json |  2 ++
 yarn.lock    | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 72 insertions(+), 1 deletion(-)

diff --git a/package.json b/package.json
index eb0a5ef6735..6907bbf455f 100644
--- a/package.json
+++ b/package.json
@@ -24,6 +24,7 @@
     "@svgr/webpack": "^6.5.1",
     "@vercel/analytics": "^0.1.11",
     "@vercel/speed-insights": "^1.0.2",
+    "axios": "^1.7.5",
     "emoji-picker-react": "^4.9.2",
     "fuse.js": "^7.0.0",
     "heic2any": "^0.0.4",
@@ -33,6 +34,7 @@
     "nanoid": "^5.0.3",
     "next": "^14.1.1",
     "node-fetch": "^3.3.1",
+    "openapi-client-axios": "^7.5.5",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
     "react-markdown": "^8.0.7",
diff --git a/yarn.lock b/yarn.lock
index 793c845d722..6d8c07cc1d3 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2138,6 +2138,11 @@ astral-regex@^2.0.0:
   resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31"
   integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==
 
+asynckit@^0.4.0:
+  version "0.4.0"
+  resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
+  integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
+
 available-typed-arrays@^1.0.5:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7"
@@ -2148,6 +2153,15 @@ axe-core@^4.6.2:
   resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.6.3.tgz#fc0db6fdb65cc7a80ccf85286d91d64ababa3ece"
   integrity sha512-/BQzOX780JhsxDnPpH4ZiyrJAzcd8AfzFPkv+89veFSr1rcMjuq2JDCwypKaPeB6ljHp9KjXhPpjgCvQlWYuqg==
 
+axios@^1.7.5:
+  version "1.7.5"
+  resolved "https://registry.npmjs.org/axios/-/axios-1.7.5.tgz#21eed340eb5daf47d29b6e002424b3e88c8c54b1"
+  integrity sha512-fZu86yCo+svH3uqJ/yTdQ0QHpQu5oL+/QE+QPSv6BZSkDAoky9vytxp7u5qk83OJFS3kEBcesWni9WTZAv3tSw==
+  dependencies:
+    follow-redirects "^1.15.6"
+    form-data "^4.0.0"
+    proxy-from-env "^1.1.0"
+
 axobject-query@^3.1.1:
   version "3.1.1"
   resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.1.1.tgz#3b6e5c6d4e43ca7ba51c5babf99d22a9c68485e1"
@@ -2189,6 +2203,11 @@ balanced-match@^1.0.0:
   resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
   integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
 
+bath-es5@^3.0.3:
+  version "3.0.3"
+  resolved "https://registry.npmjs.org/bath-es5/-/bath-es5-3.0.3.tgz#4e2808e8b33b4a5e3328ec1e9032f370f042193d"
+  integrity sha512-PdCioDToH3t84lP40kUFCKWCOCH389Dl1kbC8FGoqOwamxsmqxxnJSXdkTOsPoNHXjem4+sJ+bbNoQm5zeCqxg==
+
 binary-extensions@^2.0.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
@@ -2392,6 +2411,13 @@ colorette@^2.0.19:
   resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798"
   integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==
 
+combined-stream@^1.0.8:
+  version "1.0.8"
+  resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
+  integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
+  dependencies:
+    delayed-stream "~1.0.0"
+
 comma-separated-tokens@^2.0.0:
   version "2.0.3"
   resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee"
@@ -2925,11 +2951,21 @@ delaunator@5:
   dependencies:
     robust-predicates "^3.0.0"
 
+delayed-stream@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
+  integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
+
 dequal@^2.0.0:
   version "2.0.3"
   resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be"
   integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==
 
+dereference-json-schema@^0.2.1:
+  version "0.2.1"
+  resolved "https://registry.npmjs.org/dereference-json-schema/-/dereference-json-schema-0.2.1.tgz#fcad3c98e0116f7124b0989d39d947fa318cae09"
+  integrity sha512-uzJsrg225owJyRQ8FNTPHIuBOdSzIZlHhss9u6W8mp7jJldHqGuLv9cULagP/E26QVJDnjtG8U7Dw139mM1ydA==
+
 diff@^5.0.0:
   version "5.1.0"
   resolved "https://registry.yarnpkg.com/diff/-/diff-5.1.0.tgz#bc52d298c5ea8df9194800224445ed43ffc87e40"
@@ -3548,6 +3584,11 @@ flatted@^3.1.0:
   resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787"
   integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==
 
+follow-redirects@^1.15.6:
+  version "1.15.6"
+  resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b"
+  integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==
+
 for-each@^0.3.3:
   version "0.3.3"
   resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e"
@@ -3555,6 +3596,15 @@ for-each@^0.3.3:
   dependencies:
     is-callable "^1.1.3"
 
+form-data@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
+  integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==
+  dependencies:
+    asynckit "^0.4.0"
+    combined-stream "^1.0.8"
+    mime-types "^2.1.12"
+
 format@^0.2.0:
   version "0.2.2"
   resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b"
@@ -4937,7 +4987,7 @@ mime-db@1.52.0:
   resolved "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
   integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
 
-mime-types@^2.1.27:
+mime-types@^2.1.12, mime-types@^2.1.27:
   version "2.1.35"
   resolved "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
   integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
@@ -5161,6 +5211,20 @@ onetime@^6.0.0:
   dependencies:
     mimic-fn "^4.0.0"
 
+openapi-client-axios@^7.5.5:
+  version "7.5.5"
+  resolved "https://registry.npmjs.org/openapi-client-axios/-/openapi-client-axios-7.5.5.tgz#4cb2bb7484ff9d1c92d9ff509db235cc35d64f38"
+  integrity sha512-pgCo1z+rxtYmGQXzB+N5DiXvRurTP6JqV+Ao/wtaGUMIIIM+znh3nTztps+FZS8mZgWnDHpdEzL9bWtZuWuvoA==
+  dependencies:
+    bath-es5 "^3.0.3"
+    dereference-json-schema "^0.2.1"
+    openapi-types "^12.1.3"
+
+openapi-types@^12.1.3:
+  version "12.1.3"
+  resolved "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz#471995eb26c4b97b7bd356aacf7b91b73e777dd3"
+  integrity sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==
+
 optionator@^0.9.3:
   version "0.9.3"
   resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64"
@@ -5303,6 +5367,11 @@ property-information@^6.0.0:
   resolved "https://registry.yarnpkg.com/property-information/-/property-information-6.2.0.tgz#b74f522c31c097b5149e3c3cb8d7f3defd986a1d"
   integrity sha512-kma4U7AFCTwpqq5twzC1YVIDXSqg6qQK6JN0smOw8fgRy1OkMi0CYSzFmsy6dnqSenamAtj0CyXMUJ1Mf6oROg==
 
+proxy-from-env@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
+  integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
+
 punycode@^2.1.0:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f"

From f5209fc344b281cf148c6ce4eddea248c106140e Mon Sep 17 00:00:00 2001
From: lloydzhou <lloydzhou@qq.com>
Date: Wed, 28 Aug 2024 23:58:46 +0800
Subject: [PATCH 02/38] stash code

---
 app/api/common.ts               |   5 +-
 app/client/api.ts               |  10 +-
 app/client/platforms/openai.ts  | 276 +++++++++++++++++++++++---------
 app/components/chat.module.scss |  17 +-
 app/components/chat.tsx         |  22 ++-
 app/store/chat.ts               |  29 ++++
 6 files changed, 275 insertions(+), 84 deletions(-)

diff --git a/app/api/common.ts b/app/api/common.ts
index 24453dd9635..25decbf620e 100644
--- a/app/api/common.ts
+++ b/app/api/common.ts
@@ -32,10 +32,7 @@ export async function requestOpenai(req: NextRequest) {
     authHeaderName = "Authorization";
   }
 
-  let path = `${req.nextUrl.pathname}${req.nextUrl.search}`.replaceAll(
-    "/api/openai/",
-    "",
-  );
+  let path = `${req.nextUrl.pathname}`.replaceAll("/api/openai/", "");
 
   let baseUrl =
     (isAzure ? serverConfig.azureUrl : serverConfig.baseUrl) || OPENAI_BASE_URL;
diff --git a/app/client/api.ts b/app/client/api.ts
index d7fb023a226..cecc453baa2 100644
--- a/app/client/api.ts
+++ b/app/client/api.ts
@@ -5,7 +5,13 @@ import {
   ModelProvider,
   ServiceProvider,
 } from "../constant";
-import { ChatMessage, ModelType, useAccessStore, useChatStore } from "../store";
+import {
+  ChatMessageTool,
+  ChatMessage,
+  ModelType,
+  useAccessStore,
+  useChatStore,
+} from "../store";
 import { ChatGPTApi, DalleRequestPayload } from "./platforms/openai";
 import { GeminiProApi } from "./platforms/google";
 import { ClaudeApi } from "./platforms/anthropic";
@@ -56,6 +62,8 @@ export interface ChatOptions {
   onFinish: (message: string) => void;
   onError?: (err: Error) => void;
   onController?: (controller: AbortController) => void;
+  onBeforeTool?: (tool: ChatMessageTool) => void;
+  onAfterTool?: (tool: ChatMessageTool) => void;
 }
 
 export interface LLMUsage {
diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts
index d4e262c16b4..03bc3e09f8b 100644
--- a/app/client/platforms/openai.ts
+++ b/app/client/platforms/openai.ts
@@ -250,6 +250,8 @@ export class ChatGPTApi implements LLMApi {
         let responseText = "";
         let remainText = "";
         let finished = false;
+        let running = false;
+        let runTools = [];
 
         // animate response to make it looks smooth
         function animateResponseText() {
@@ -276,8 +278,70 @@ export class ChatGPTApi implements LLMApi {
         // start animaion
         animateResponseText();
 
+        // TODO 后面这里是从选择的plugins中获取function列表
+        const funcs = {
+          get_current_weather: (args) => {
+            console.log("call get_current_weather", args);
+            return "30";
+          },
+        };
         const finish = () => {
           if (!finished) {
+            console.log("try run tools", runTools.length, finished, running);
+            if (!running && runTools.length > 0) {
+              const toolCallMessage = {
+                role: "assistant",
+                tool_calls: [...runTools],
+              };
+              running = true;
+              runTools.splice(0, runTools.length); // empty runTools
+              return Promise.all(
+                toolCallMessage.tool_calls.map((tool) => {
+                  options?.onBeforeTool(tool);
+                  return Promise.resolve(
+                    funcs[tool.function.name](
+                      JSON.parse(tool.function.arguments),
+                    ),
+                  )
+                    .then((content) => {
+                      options?.onAfterTool({
+                        ...tool,
+                        content,
+                        isError: false,
+                      });
+                      return content;
+                    })
+                    .catch((e) => {
+                      options?.onAfterTool({ ...tool, isError: true });
+                      return e.toString();
+                    })
+                    .then((content) => ({
+                      role: "tool",
+                      content,
+                      tool_call_id: tool.id,
+                    }));
+                }),
+              ).then((toolCallResult) => {
+                console.log("end runTools", toolCallMessage, toolCallResult);
+                requestPayload["messages"].splice(
+                  requestPayload["messages"].length,
+                  0,
+                  toolCallMessage,
+                  ...toolCallResult,
+                );
+                setTimeout(() => {
+                  // call again
+                  console.log("start again");
+                  running = false;
+                  chatApi(chatPath, requestPayload); // call fetchEventSource
+                }, 0);
+              });
+              console.log("try run tools", runTools.length, finished);
+              return;
+            }
+            if (running) {
+              return;
+            }
             finished = true;
             options.onFinish(responseText + remainText);
           }
@@ -285,90 +349,148 @@ export class ChatGPTApi implements LLMApi {
 
         controller.signal.onabort = finish;
 
-        fetchEventSource(chatPath, {
-          ...chatPayload,
-          async onopen(res) {
-            clearTimeout(requestTimeoutId);
-            const contentType = res.headers.get("content-type");
-            console.log(
-              "[OpenAI] request response content type: ",
-              contentType,
-            );
-
-            if (contentType?.startsWith("text/plain")) {
-              responseText = await res.clone().text();
-              return finish();
-            }
+        function chatApi(chatPath, requestPayload) {
+          const chatPayload = {
+            method: "POST",
+            body: JSON.stringify({
+              ...requestPayload,
+              // TODO 这里暂时写死的,后面从store.tools中按照当前session中选择的获取
+              tools: [
+                {
+                  type: "function",
+                  function: {
+                    name: "get_current_weather",
+                    description: "Get the current weather",
+                    parameters: {
+                      type: "object",
+                      properties: {
+                        location: {
+                          type: "string",
+                          description:
+                            "The city and country, eg. San Francisco, USA",
+                        },
+                        format: {
+                          type: "string",
+                          enum: ["celsius", "fahrenheit"],
+                        },
+                      },
+                      required: ["location", "format"],
+                    },
+                  },
+                },
+              ],
+            }),
+            signal: controller.signal,
+            headers: getHeaders(),
+          };
+          console.log("chatApi", chatPath, requestPayload, chatPayload);
+          fetchEventSource(chatPath, {
+            ...chatPayload,
+            async onopen(res) {
+              clearTimeout(requestTimeoutId);
+              const contentType = res.headers.get("content-type");
+              console.log(
+                "[OpenAI] request response content type: ",
+                contentType,
+              );
+
+              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.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 (res.status === 401) {
+                  responseTexts.push(Locale.Error.Unauthorized);
+                }
 
-              if (extraInfo) {
-                responseTexts.push(extraInfo);
-              }
+                if (extraInfo) {
+                  responseTexts.push(extraInfo);
+                }
 
-              responseText = responseTexts.join("\n\n");
+                responseText = responseTexts.join("\n\n");
 
-              return finish();
-            }
-          },
-          onmessage(msg) {
-            if (msg.data === "[DONE]" || finished) {
-              return finish();
-            }
-            const text = msg.data;
-            try {
-              const json = JSON.parse(text);
-              const choices = json.choices as Array<{
-                delta: { content: string };
-              }>;
-              const delta = choices[0]?.delta?.content;
-              const textmoderation = json?.prompt_filter_results;
-
-              if (delta) {
-                remainText += delta;
+                return finish();
               }
-
-              if (
-                textmoderation &&
-                textmoderation.length > 0 &&
-                ServiceProvider.Azure
-              ) {
-                const contentFilterResults =
-                  textmoderation[0]?.content_filter_results;
-                console.log(
-                  `[${ServiceProvider.Azure}] [Text Moderation] flagged categories result:`,
-                  contentFilterResults,
-                );
+            },
+            onmessage(msg) {
+              if (msg.data === "[DONE]" || finished) {
+                return finish();
               }
-            } catch (e) {
-              console.error("[Request] parse error", text, msg);
-            }
-          },
-          onclose() {
-            finish();
-          },
-          onerror(e) {
-            options.onError?.(e);
-            throw e;
-          },
-          openWhenHidden: true,
-        });
+              const text = msg.data;
+              try {
+                const json = JSON.parse(text);
+                const choices = json.choices as Array<{
+                  delta: { content: string };
+                }>;
+                console.log("choices", choices);
+                const delta = choices[0]?.delta?.content;
+                const tool_calls = choices[0]?.delta?.tool_calls;
+                const textmoderation = json?.prompt_filter_results;
+
+                if (delta) {
+                  remainText += delta;
+                }
+                if (tool_calls?.length > 0) {
+                  const index = tool_calls[0]?.index;
+                  const id = tool_calls[0]?.id;
+                  const args = tool_calls[0]?.function?.arguments;
+                  if (id) {
+                    runTools.push({
+                      id,
+                      type: tool_calls[0]?.type,
+                      function: {
+                        name: tool_calls[0]?.function?.name,
+                        arguments: args,
+                      },
+                    });
+                  } else {
+                    runTools[index]["function"]["arguments"] += args;
+                  }
+                }
+
+                console.log("runTools", runTools);
+
+                if (
+                  textmoderation &&
+                  textmoderation.length > 0 &&
+                  ServiceProvider.Azure
+                ) {
+                  const contentFilterResults =
+                    textmoderation[0]?.content_filter_results;
+                  console.log(
+                    `[${ServiceProvider.Azure}] [Text Moderation] flagged categories result:`,
+                    contentFilterResults,
+                  );
+                }
+              } catch (e) {
+                console.error("[Request] parse error", text, msg);
+              }
+            },
+            onclose() {
+              finish();
+            },
+            onerror(e) {
+              options.onError?.(e);
+              throw e;
+            },
+            openWhenHidden: true,
+          });
+        }
+        chatApi(chatPath, requestPayload); // call fetchEventSource
       } else {
         const res = await fetch(chatPath, chatPayload);
         clearTimeout(requestTimeoutId);
diff --git a/app/components/chat.module.scss b/app/components/chat.module.scss
index 3b5c143b9d9..33ccaf5235d 100644
--- a/app/components/chat.module.scss
+++ b/app/components/chat.module.scss
@@ -413,6 +413,21 @@
   margin-top: 5px;
 }
 
+.chat-message-tools {
+  font-size: 12px;
+  color: #aaa;
+  line-height: 1.5;
+  margin-top: 5px;
+  .chat-message-tool {
+    display: inline-flex;
+    align-items: end;
+    svg {
+      margin-left: 5px;
+      margin-right: 5px;
+    }
+  }
+}
+
 .chat-message-item {
   box-sizing: border-box;
   max-width: 100%;
@@ -630,4 +645,4 @@
   .chat-input-send {
     bottom: 30px;
   }
-}
\ No newline at end of file
+}
diff --git a/app/components/chat.tsx b/app/components/chat.tsx
index ed5b06799c3..3ad8cd5c9a0 100644
--- a/app/components/chat.tsx
+++ b/app/components/chat.tsx
@@ -28,6 +28,7 @@ import DeleteIcon from "../icons/clear.svg";
 import PinIcon from "../icons/pin.svg";
 import EditIcon from "../icons/rename.svg";
 import ConfirmIcon from "../icons/confirm.svg";
+import CloseIcon from "../icons/close.svg";
 import CancelIcon from "../icons/cancel.svg";
 import ImageIcon from "../icons/image.svg";
 
@@ -1573,11 +1574,30 @@ function _Chat() {
                       </div>
                     )}
                   </div>
-                  {showTyping && (
+                  {message?.tools?.length == 0 && showTyping && (
                     <div className={styles["chat-message-status"]}>
                       {Locale.Chat.Typing}
                     </div>
                   )}
+                  {message?.tools?.length > 0 && (
+                    <div className={styles["chat-message-tools"]}>
+                      {message?.tools?.map((tool) => (
+                        <div
+                          key={tool.id}
+                          className={styles["chat-message-tool"]}
+                        >
+                          {tool.isError === false ? (
+                            <ConfirmIcon />
+                          ) : tool.isError === true ? (
+                            <CloseIcon />
+                          ) : (
+                            <LoadingButtonIcon />
+                          )}
+                          <span>{tool.function.name}</span>
+                        </div>
+                      ))}
+                    </div>
+                  )}
                   <div className={styles["chat-message-item"]}>
                     <Markdown
                       key={message.streaming ? "loading" : "done"}
diff --git a/app/store/chat.ts b/app/store/chat.ts
index 653926d1b02..b035e51af19 100644
--- a/app/store/chat.ts
+++ b/app/store/chat.ts
@@ -28,12 +28,24 @@ import { collectModelsWithDefaultModel } from "../utils/model";
 import { useAccessStore } from "./access";
 import { isDalle3 } from "../utils";
 
+export type ChatMessageTool = {
+  id: string;
+  type?: string;
+  function?: {
+    name: string;
+    arguments?: string;
+  };
+  content?: string;
+  isError?: boolean;
+};
+
 export type ChatMessage = RequestMessage & {
   date: string;
   streaming?: boolean;
   isError?: boolean;
   id: string;
   model?: ModelType;
+  tools?: ChatMessageTool[];
 };
 
 export function createMessage(override: Partial<ChatMessage>): ChatMessage {
@@ -389,6 +401,23 @@ export const useChatStore = createPersistStore(
             }
             ChatControllerPool.remove(session.id, botMessage.id);
           },
+          onBeforeTool(tool: ChatMessageTool) {
+            (botMessage.tools = botMessage?.tools || []).push(tool);
+            get().updateCurrentSession((session) => {
+              session.messages = session.messages.concat();
+            });
+          },
+          onAfterTool(tool: ChatMessageTool) {
+            console.log("onAfterTool", botMessage);
+            botMessage?.tools?.forEach((t, i, tools) => {
+              if (tool.id == t.id) {
+                tools[i] = { ...tool };
+              }
+            });
+            get().updateCurrentSession((session) => {
+              session.messages = session.messages.concat();
+            });
+          },
           onError(error) {
             const isAborted = error.message.includes("aborted");
             botMessage.content +=

From 29b5cd9436d0aa0bbd0dcc79fd06440ca5a73469 Mon Sep 17 00:00:00 2001
From: lloydzhou <lloydzhou@qq.com>
Date: Thu, 29 Aug 2024 00:21:26 +0800
Subject: [PATCH 03/38] ts error

---
 app/client/platforms/openai.ts | 39 ++++++++++++++++++++++------------
 app/components/chat.tsx        |  3 ++-
 app/store/chat.ts              |  1 +
 3 files changed, 29 insertions(+), 14 deletions(-)

diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts
index 03bc3e09f8b..5e98845e05b 100644
--- a/app/client/platforms/openai.ts
+++ b/app/client/platforms/openai.ts
@@ -9,7 +9,12 @@ import {
   REQUEST_TIMEOUT_MS,
   ServiceProvider,
 } from "@/app/constant";
-import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
+import {
+  ChatMessageTool,
+  useAccessStore,
+  useAppConfig,
+  useChatStore,
+} from "@/app/store";
 import { collectModelsWithDefaultModel } from "@/app/utils/model";
 import {
   preProcessImageContent,
@@ -251,7 +256,7 @@ export class ChatGPTApi implements LLMApi {
         let remainText = "";
         let finished = false;
         let running = false;
-        let runTools = [];
+        let runTools: ChatMessageTool[] = [];
 
         // animate response to make it looks smooth
         function animateResponseText() {
@@ -280,7 +285,7 @@ export class ChatGPTApi implements LLMApi {
 
         // TODO 后面这里是从选择的plugins中获取function列表
         const funcs = {
-          get_current_weather: (args) => {
+          get_current_weather: (args: any) => {
             console.log("call get_current_weather", args);
             return "30";
           },
@@ -297,14 +302,16 @@ export class ChatGPTApi implements LLMApi {
               runTools.splice(0, runTools.length); // empty runTools
               return Promise.all(
                 toolCallMessage.tool_calls.map((tool) => {
-                  options?.onBeforeTool(tool);
+                  options?.onBeforeTool?.(tool);
                   return Promise.resolve(
+                    // @ts-ignore
                     funcs[tool.function.name](
+                      // @ts-ignore
                       JSON.parse(tool.function.arguments),
                     ),
                   )
                     .then((content) => {
-                      options?.onAfterTool({
+                      options?.onAfterTool?.({
                         ...tool,
                         content,
                         isError: false,
@@ -312,7 +319,7 @@ export class ChatGPTApi implements LLMApi {
                       return content;
                     })
                     .catch((e) => {
-                      options?.onAfterTool({ ...tool, isError: true });
+                      options?.onAfterTool?.({ ...tool, isError: true });
                       return e.toString();
                     })
                     .then((content) => ({
@@ -323,8 +330,10 @@ export class ChatGPTApi implements LLMApi {
                 }),
               ).then((toolCallResult) => {
                 console.log("end runTools", toolCallMessage, toolCallResult);
-                requestPayload["messages"].splice(
-                  requestPayload["messages"].length,
+                // @ts-ignore
+                requestPayload?.messages?.splice(
+                  // @ts-ignore
+                  requestPayload?.messages?.length,
                   0,
                   toolCallMessage,
                   ...toolCallResult,
@@ -333,7 +342,7 @@ export class ChatGPTApi implements LLMApi {
                   // call again
                   console.log("start again");
                   running = false;
-                  chatApi(chatPath, requestPayload); // call fetchEventSource
+                  chatApi(chatPath, requestPayload as RequestPayload); // call fetchEventSource
                 }, 0);
               });
               console.log("try run tools", runTools.length, finished);
@@ -349,7 +358,7 @@ export class ChatGPTApi implements LLMApi {
 
         controller.signal.onabort = finish;
 
-        function chatApi(chatPath, requestPayload) {
+        function chatApi(chatPath: string, requestPayload: RequestPayload) {
           const chatPayload = {
             method: "POST",
             body: JSON.stringify({
@@ -434,7 +443,10 @@ export class ChatGPTApi implements LLMApi {
               try {
                 const json = JSON.parse(text);
                 const choices = json.choices as Array<{
-                  delta: { content: string };
+                  delta: {
+                    content: string;
+                    tool_calls: ChatMessageTool[];
+                  };
                 }>;
                 console.log("choices", choices);
                 const delta = choices[0]?.delta?.content;
@@ -453,11 +465,12 @@ export class ChatGPTApi implements LLMApi {
                       id,
                       type: tool_calls[0]?.type,
                       function: {
-                        name: tool_calls[0]?.function?.name,
+                        name: tool_calls[0]?.function?.name as string,
                         arguments: args,
                       },
                     });
                   } else {
+                    // @ts-ignore
                     runTools[index]["function"]["arguments"] += args;
                   }
                 }
@@ -490,7 +503,7 @@ export class ChatGPTApi implements LLMApi {
             openWhenHidden: true,
           });
         }
-        chatApi(chatPath, requestPayload); // call fetchEventSource
+        chatApi(chatPath, requestPayload as RequestPayload); // call fetchEventSource
       } else {
         const res = await fetch(chatPath, chatPayload);
         clearTimeout(requestTimeoutId);
diff --git a/app/components/chat.tsx b/app/components/chat.tsx
index 3ad8cd5c9a0..2ad579aa560 100644
--- a/app/components/chat.tsx
+++ b/app/components/chat.tsx
@@ -1579,6 +1579,7 @@ function _Chat() {
                       {Locale.Chat.Typing}
                     </div>
                   )}
+                  {/*@ts-ignore*/}
                   {message?.tools?.length > 0 && (
                     <div className={styles["chat-message-tools"]}>
                       {message?.tools?.map((tool) => (
@@ -1593,7 +1594,7 @@ function _Chat() {
                           ) : (
                             <LoadingButtonIcon />
                           )}
-                          <span>{tool.function.name}</span>
+                          <span>{tool?.function?.name}</span>
                         </div>
                       ))}
                     </div>
diff --git a/app/store/chat.ts b/app/store/chat.ts
index b035e51af19..c2d1995194b 100644
--- a/app/store/chat.ts
+++ b/app/store/chat.ts
@@ -30,6 +30,7 @@ import { isDalle3 } from "../utils";
 
 export type ChatMessageTool = {
   id: string;
+  index?: number;
   type?: string;
   function?: {
     name: string;

From f3f6dc57c32173d37ad50a38b9d15166247bebe7 Mon Sep 17 00:00:00 2001
From: lloydzhou <lloydzhou@qq.com>
Date: Thu, 29 Aug 2024 00:32:35 +0800
Subject: [PATCH 04/38] stash code

---
 app/client/platforms/openai.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts
index 5e98845e05b..f3b48816352 100644
--- a/app/client/platforms/openai.ts
+++ b/app/client/platforms/openai.ts
@@ -343,7 +343,7 @@ export class ChatGPTApi implements LLMApi {
                   console.log("start again");
                   running = false;
                   chatApi(chatPath, requestPayload as RequestPayload); // call fetchEventSource
-                }, 0);
+                }, 5);
               });
               console.log("try run tools", runTools.length, finished);
               return;

From d212df8b952636937814a22f40b1b8d46f38d3d1 Mon Sep 17 00:00:00 2001
From: lloydzhou <lloydzhou@qq.com>
Date: Thu, 29 Aug 2024 00:39:51 +0800
Subject: [PATCH 05/38] stash code

---
 app/client/platforms/openai.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts
index f3b48816352..0eea4331cc9 100644
--- a/app/client/platforms/openai.ts
+++ b/app/client/platforms/openai.ts
@@ -338,12 +338,12 @@ export class ChatGPTApi implements LLMApi {
                   toolCallMessage,
                   ...toolCallResult,
                 );
-                setTimeout(() => {
+                requestAnimationFrame(() => {
                   // call again
                   console.log("start again");
                   running = false;
                   chatApi(chatPath, requestPayload as RequestPayload); // call fetchEventSource
-                }, 5);
+                });
               });
               console.log("try run tools", runTools.length, finished);
               return;

From f7a5f836db61c81cee25f862230c6b8952751dfe Mon Sep 17 00:00:00 2001
From: lloydzhou <lloydzhou@qq.com>
Date: Thu, 29 Aug 2024 00:56:20 +0800
Subject: [PATCH 06/38] stash code

---
 app/client/platforms/openai.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts
index 0eea4331cc9..4146ec9c123 100644
--- a/app/client/platforms/openai.ts
+++ b/app/client/platforms/openai.ts
@@ -338,12 +338,12 @@ export class ChatGPTApi implements LLMApi {
                   toolCallMessage,
                   ...toolCallResult,
                 );
-                requestAnimationFrame(() => {
+                setTimeout(() => {
                   // call again
                   console.log("start again");
                   running = false;
                   chatApi(chatPath, requestPayload as RequestPayload); // call fetchEventSource
-                });
+                }, 60);
               });
               console.log("try run tools", runTools.length, finished);
               return;

From d58b99d6023ad4681e24f3fabcf34fd525cc8672 Mon Sep 17 00:00:00 2001
From: lloydzhou <lloydzhou@qq.com>
Date: Thu, 29 Aug 2024 01:00:16 +0800
Subject: [PATCH 07/38] stash code

---
 app/client/platforms/openai.ts | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts
index 4146ec9c123..fe1ef38d7d3 100644
--- a/app/client/platforms/openai.ts
+++ b/app/client/platforms/openai.ts
@@ -287,7 +287,9 @@ export class ChatGPTApi implements LLMApi {
         const funcs = {
           get_current_weather: (args: any) => {
             console.log("call get_current_weather", args);
-            return "30";
+            return new Promise((resolve) => {
+              setTimeout(() => resolve("30"), 3000);
+            });
           },
         };
         const finish = () => {

From 341a52a61532c51489fe5b66109737ec9fccf2ad Mon Sep 17 00:00:00 2001
From: lloydzhou <lloydzhou@qq.com>
Date: Thu, 29 Aug 2024 01:35:41 +0800
Subject: [PATCH 08/38] stash code

---
 app/components/chat.module.scss | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/app/components/chat.module.scss b/app/components/chat.module.scss
index 33ccaf5235d..7176399cc36 100644
--- a/app/components/chat.module.scss
+++ b/app/components/chat.module.scss
@@ -419,7 +419,7 @@
   line-height: 1.5;
   margin-top: 5px;
   .chat-message-tool {
-    display: inline-flex;
+    display: flex;
     align-items: end;
     svg {
       margin-left: 5px;

From 7fc0d11931e6a5330850a5f903f5337f09ca3eb2 Mon Sep 17 00:00:00 2001
From: lloydzhou <lloydzhou@qq.com>
Date: Thu, 29 Aug 2024 17:14:23 +0800
Subject: [PATCH 09/38] create common function stream for fetchEventSource

---
 app/client/platforms/openai.ts | 327 ++++++++-------------------------
 app/utils/chat.ts              | 207 ++++++++++++++++++++-
 2 files changed, 279 insertions(+), 255 deletions(-)

diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts
index fe1ef38d7d3..edac751b0aa 100644
--- a/app/client/platforms/openai.ts
+++ b/app/client/platforms/openai.ts
@@ -20,6 +20,7 @@ import {
   preProcessImageContent,
   uploadImage,
   base64Image2Blob,
+  stream,
 } from "@/app/utils/chat";
 import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
 import { DalleSize, DalleQuality, DalleStyle } from "@/app/typing";
@@ -238,52 +239,30 @@ export class ChatGPTApi implements LLMApi {
           isDalle3 ? OpenaiPath.ImagePath : OpenaiPath.ChatPath,
         );
       }
-      const chatPayload = {
-        method: "POST",
-        body: JSON.stringify(requestPayload),
-        signal: controller.signal,
-        headers: getHeaders(),
-      };
-
-      // make a fetch request
-      const requestTimeoutId = setTimeout(
-        () => controller.abort(),
-        isDalle3 ? REQUEST_TIMEOUT_MS * 2 : REQUEST_TIMEOUT_MS, // dalle3 using b64_json is slow.
-      );
-
       if (shouldStream) {
-        let responseText = "";
-        let remainText = "";
-        let finished = false;
-        let running = false;
-        let runTools: ChatMessageTool[] = [];
-
-        // animate response to make it looks smooth
-        function animateResponseText() {
-          if (finished || controller.signal.aborted) {
-            responseText += remainText;
-            console.log("[Response Animation] finished");
-            if (responseText?.length === 0) {
-              options.onError?.(new Error("empty response from server"));
-            }
-            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);
-        }
-
-        // start animaion
-        animateResponseText();
-
-        // TODO 后面这里是从选择的plugins中获取function列表
+        const tools = [
+          {
+            type: "function",
+            function: {
+              name: "get_current_weather",
+              description: "Get the current weather",
+              parameters: {
+                type: "object",
+                properties: {
+                  location: {
+                    type: "string",
+                    description: "The city and country, eg. San Francisco, USA",
+                  },
+                  format: {
+                    type: "string",
+                    enum: ["celsius", "fahrenheit"],
+                  },
+                },
+                required: ["location", "format"],
+              },
+            },
+          },
+        ];
         const funcs = {
           get_current_weather: (args: any) => {
             console.log("call get_current_weather", args);
@@ -292,221 +271,61 @@ export class ChatGPTApi implements LLMApi {
             });
           },
         };
-        const finish = () => {
-          if (!finished) {
-            console.log("try run tools", runTools.length, finished, running);
-            if (!running && runTools.length > 0) {
-              const toolCallMessage = {
-                role: "assistant",
-                tool_calls: [...runTools],
+        stream(
+          chatPath,
+          requestPayload,
+          getHeaders(),
+          tools,
+          funcs,
+          controller,
+          (text: string, runTools: ChatMessageTool[]) => {
+            console.log("parseSSE", text, runTools);
+            const json = JSON.parse(text);
+            const choices = json.choices as Array<{
+              delta: {
+                content: string;
+                tool_calls: ChatMessageTool[];
               };
-              running = true;
-              runTools.splice(0, runTools.length); // empty runTools
-              return Promise.all(
-                toolCallMessage.tool_calls.map((tool) => {
-                  options?.onBeforeTool?.(tool);
-                  return Promise.resolve(
-                    // @ts-ignore
-                    funcs[tool.function.name](
-                      // @ts-ignore
-                      JSON.parse(tool.function.arguments),
-                    ),
-                  )
-                    .then((content) => {
-                      options?.onAfterTool?.({
-                        ...tool,
-                        content,
-                        isError: false,
-                      });
-                      return content;
-                    })
-                    .catch((e) => {
-                      options?.onAfterTool?.({ ...tool, isError: true });
-                      return e.toString();
-                    })
-                    .then((content) => ({
-                      role: "tool",
-                      content,
-                      tool_call_id: tool.id,
-                    }));
-                }),
-              ).then((toolCallResult) => {
-                console.log("end runTools", toolCallMessage, toolCallResult);
-                // @ts-ignore
-                requestPayload?.messages?.splice(
-                  // @ts-ignore
-                  requestPayload?.messages?.length,
-                  0,
-                  toolCallMessage,
-                  ...toolCallResult,
-                );
-                setTimeout(() => {
-                  // call again
-                  console.log("start again");
-                  running = false;
-                  chatApi(chatPath, requestPayload as RequestPayload); // call fetchEventSource
-                }, 60);
-              });
-              console.log("try run tools", runTools.length, finished);
-              return;
-            }
-            if (running) {
-              return;
-            }
-            finished = true;
-            options.onFinish(responseText + remainText);
-          }
-        };
-
-        controller.signal.onabort = finish;
-
-        function chatApi(chatPath: string, requestPayload: RequestPayload) {
-          const chatPayload = {
-            method: "POST",
-            body: JSON.stringify({
-              ...requestPayload,
-              // TODO 这里暂时写死的,后面从store.tools中按照当前session中选择的获取
-              tools: [
-                {
-                  type: "function",
+            }>;
+            const tool_calls = choices[0]?.delta?.tool_calls;
+            if (tool_calls?.length > 0) {
+              const index = tool_calls[0]?.index;
+              const id = tool_calls[0]?.id;
+              const args = tool_calls[0]?.function?.arguments;
+              if (id) {
+                runTools.push({
+                  id,
+                  type: tool_calls[0]?.type,
                   function: {
-                    name: "get_current_weather",
-                    description: "Get the current weather",
-                    parameters: {
-                      type: "object",
-                      properties: {
-                        location: {
-                          type: "string",
-                          description:
-                            "The city and country, eg. San Francisco, USA",
-                        },
-                        format: {
-                          type: "string",
-                          enum: ["celsius", "fahrenheit"],
-                        },
-                      },
-                      required: ["location", "format"],
-                    },
+                    name: tool_calls[0]?.function?.name as string,
+                    arguments: args,
                   },
-                },
-              ],
-            }),
-            signal: controller.signal,
-            headers: getHeaders(),
-          };
-          console.log("chatApi", chatPath, requestPayload, chatPayload);
-          fetchEventSource(chatPath, {
-            ...chatPayload,
-            async onopen(res) {
-              clearTimeout(requestTimeoutId);
-              const contentType = res.headers.get("content-type");
-              console.log(
-                "[OpenAI] request response content type: ",
-                contentType,
-              );
-
-              if (contentType?.startsWith("text/plain")) {
-                responseText = await res.clone().text();
-                return finish();
+                });
+              } else {
+                // @ts-ignore
+                runTools[index]["function"]["arguments"] += args;
               }
+            }
 
-              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) {
-              if (msg.data === "[DONE]" || finished) {
-                return finish();
-              }
-              const text = msg.data;
-              try {
-                const json = JSON.parse(text);
-                const choices = json.choices as Array<{
-                  delta: {
-                    content: string;
-                    tool_calls: ChatMessageTool[];
-                  };
-                }>;
-                console.log("choices", choices);
-                const delta = choices[0]?.delta?.content;
-                const tool_calls = choices[0]?.delta?.tool_calls;
-                const textmoderation = json?.prompt_filter_results;
-
-                if (delta) {
-                  remainText += delta;
-                }
-                if (tool_calls?.length > 0) {
-                  const index = tool_calls[0]?.index;
-                  const id = tool_calls[0]?.id;
-                  const args = tool_calls[0]?.function?.arguments;
-                  if (id) {
-                    runTools.push({
-                      id,
-                      type: tool_calls[0]?.type,
-                      function: {
-                        name: tool_calls[0]?.function?.name as string,
-                        arguments: args,
-                      },
-                    });
-                  } else {
-                    // @ts-ignore
-                    runTools[index]["function"]["arguments"] += args;
-                  }
-                }
-
-                console.log("runTools", runTools);
-
-                if (
-                  textmoderation &&
-                  textmoderation.length > 0 &&
-                  ServiceProvider.Azure
-                ) {
-                  const contentFilterResults =
-                    textmoderation[0]?.content_filter_results;
-                  console.log(
-                    `[${ServiceProvider.Azure}] [Text Moderation] flagged categories result:`,
-                    contentFilterResults,
-                  );
-                }
-              } catch (e) {
-                console.error("[Request] parse error", text, msg);
-              }
-            },
-            onclose() {
-              finish();
-            },
-            onerror(e) {
-              options.onError?.(e);
-              throw e;
-            },
-            openWhenHidden: true,
-          });
-        }
-        chatApi(chatPath, requestPayload as RequestPayload); // call fetchEventSource
+            console.log("runTools", runTools);
+            return choices[0]?.delta?.content;
+          },
+          options,
+        );
       } else {
+        const chatPayload = {
+          method: "POST",
+          body: JSON.stringify(requestPayload),
+          signal: controller.signal,
+          headers: getHeaders(),
+        };
+
+        // make a fetch request
+        const requestTimeoutId = setTimeout(
+          () => controller.abort(),
+          isDalle3 ? REQUEST_TIMEOUT_MS * 2 : REQUEST_TIMEOUT_MS, // dalle3 using b64_json is slow.
+        );
+
         const res = await fetch(chatPath, chatPayload);
         clearTimeout(requestTimeoutId);
 
diff --git a/app/utils/chat.ts b/app/utils/chat.ts
index 6a296e5765d..1289695b991 100644
--- a/app/utils/chat.ts
+++ b/app/utils/chat.ts
@@ -1,5 +1,15 @@
-import { CACHE_URL_PREFIX, UPLOAD_URL } from "@/app/constant";
+import {
+  CACHE_URL_PREFIX,
+  UPLOAD_URL,
+  REQUEST_TIMEOUT_MS,
+} from "@/app/constant";
 import { RequestMessage } from "@/app/client/api";
+import Locale from "@/app/locales";
+import {
+  EventStreamContentType,
+  fetchEventSource,
+} from "@fortaine/fetch-event-source";
+import { prettyObject } from "./format";
 
 export function compressImage(file: Blob, maxSize: number): Promise<string> {
   return new Promise((resolve, reject) => {
@@ -142,3 +152,198 @@ export function removeImage(imageUrl: string) {
     credentials: "include",
   });
 }
+
+export function stream(
+  chatPath: string,
+  requestPayload: any,
+  headers: any,
+  tools: any[],
+  funcs: any,
+  controller: AbortController,
+  parseSSE: (text: string, runTools: any[]) => string | undefined,
+  options: any,
+) {
+  let responseText = "";
+  let remainText = "";
+  let finished = false;
+  let running = false;
+  let runTools: any[] = [];
+
+  // animate response to make it looks smooth
+  function animateResponseText() {
+    if (finished || controller.signal.aborted) {
+      responseText += remainText;
+      console.log("[Response Animation] finished");
+      if (responseText?.length === 0) {
+        options.onError?.(new Error("empty response from server"));
+      }
+      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);
+  }
+
+  // start animaion
+  animateResponseText();
+
+  const finish = () => {
+    if (!finished) {
+      console.log("try run tools", runTools.length, finished, running);
+      if (!running && runTools.length > 0) {
+        const toolCallMessage = {
+          role: "assistant",
+          tool_calls: [...runTools],
+        };
+        running = true;
+        runTools.splice(0, runTools.length); // empty runTools
+        return Promise.all(
+          toolCallMessage.tool_calls.map((tool) => {
+            options?.onBeforeTool?.(tool);
+            return Promise.resolve(
+              // @ts-ignore
+              funcs[tool.function.name](
+                // @ts-ignore
+                JSON.parse(tool.function.arguments),
+              ),
+            )
+              .then((content) => {
+                options?.onAfterTool?.({
+                  ...tool,
+                  content,
+                  isError: false,
+                });
+                return content;
+              })
+              .catch((e) => {
+                options?.onAfterTool?.({ ...tool, isError: true });
+                return e.toString();
+              })
+              .then((content) => ({
+                role: "tool",
+                content,
+                tool_call_id: tool.id,
+              }));
+          }),
+        ).then((toolCallResult) => {
+          console.log("end runTools", toolCallMessage, toolCallResult);
+          // @ts-ignore
+          requestPayload?.messages?.splice(
+            // @ts-ignore
+            requestPayload?.messages?.length,
+            0,
+            toolCallMessage,
+            ...toolCallResult,
+          );
+          setTimeout(() => {
+            // call again
+            console.log("start again");
+            running = false;
+            chatApi(chatPath, headers, requestPayload, tools); // call fetchEventSource
+          }, 60);
+        });
+        console.log("try run tools", runTools.length, finished);
+        return;
+      }
+      if (running) {
+        return;
+      }
+      finished = true;
+      options.onFinish(responseText + remainText);
+    }
+  };
+
+  controller.signal.onabort = finish;
+
+  function chatApi(
+    chatPath: string,
+    headers: any,
+    requestPayload: any,
+    tools: any,
+  ) {
+    const chatPayload = {
+      method: "POST",
+      body: JSON.stringify({
+        ...requestPayload,
+        tools,
+      }),
+      signal: controller.signal,
+      headers,
+    };
+    const requestTimeoutId = setTimeout(
+      () => controller.abort(),
+      REQUEST_TIMEOUT_MS,
+    );
+    fetchEventSource(chatPath, {
+      ...chatPayload,
+      async onopen(res) {
+        clearTimeout(requestTimeoutId);
+        const contentType = res.headers.get("content-type");
+        console.log("[Request] response content type: ", contentType);
+
+        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) {
+        if (msg.data === "[DONE]" || finished) {
+          return finish();
+        }
+        const text = msg.data;
+        try {
+          const chunk = parseSSE(msg.data, runTools);
+          if (chunk) {
+            remainText += chunk;
+          }
+        } catch (e) {
+          console.error("[Request] parse error", text, msg);
+        }
+      },
+      onclose() {
+        finish();
+      },
+      onerror(e) {
+        options?.onError?.(e);
+        throw e;
+      },
+      openWhenHidden: true,
+    });
+    console.log("chatApi", chatPath, requestPayload, tools);
+  }
+  chatApi(chatPath, headers, requestPayload, tools); // call fetchEventSource
+}

From d2cb984cedf688e44d36a483871bd384009affb8 Mon Sep 17 00:00:00 2001
From: lloydzhou <lloydzhou@qq.com>
Date: Thu, 29 Aug 2024 17:28:15 +0800
Subject: [PATCH 10/38] add processToolMessage callback

---
 app/client/platforms/openai.ts | 21 ++++++++++++++++++---
 app/utils/chat.ts              | 22 +++++++++-------------
 2 files changed, 27 insertions(+), 16 deletions(-)

diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts
index edac751b0aa..69dbad00505 100644
--- a/app/client/platforms/openai.ts
+++ b/app/client/platforms/openai.ts
@@ -240,6 +240,7 @@ export class ChatGPTApi implements LLMApi {
         );
       }
       if (shouldStream) {
+        // TODO mock tools and funcs
         const tools = [
           {
             type: "function",
@@ -278,8 +279,9 @@ export class ChatGPTApi implements LLMApi {
           tools,
           funcs,
           controller,
+          // parseSSE
           (text: string, runTools: ChatMessageTool[]) => {
-            console.log("parseSSE", text, runTools);
+            // console.log("parseSSE", text, runTools);
             const json = JSON.parse(text);
             const choices = json.choices as Array<{
               delta: {
@@ -306,10 +308,23 @@ export class ChatGPTApi implements LLMApi {
                 runTools[index]["function"]["arguments"] += args;
               }
             }
-
-            console.log("runTools", runTools);
             return choices[0]?.delta?.content;
           },
+          // processToolMessage, include tool_calls message and tool call results
+          (
+            requestPayload: RequestPayload,
+            toolCallMessage: any,
+            toolCallResult: any[],
+          ) => {
+            // @ts-ignore
+            requestPayload?.messages?.splice(
+              // @ts-ignore
+              requestPayload?.messages?.length,
+              0,
+              toolCallMessage,
+              ...toolCallResult,
+            );
+          },
           options,
         );
       } else {
diff --git a/app/utils/chat.ts b/app/utils/chat.ts
index 1289695b991..aa268a9fe41 100644
--- a/app/utils/chat.ts
+++ b/app/utils/chat.ts
@@ -161,6 +161,11 @@ export function stream(
   funcs: any,
   controller: AbortController,
   parseSSE: (text: string, runTools: any[]) => string | undefined,
+  processToolMessage: (
+    requestPayload: any,
+    toolCallMessage: any,
+    toolCallResult: any[],
+  ) => void,
   options: any,
 ) {
   let responseText = "";
@@ -196,7 +201,6 @@ export function stream(
 
   const finish = () => {
     if (!finished) {
-      console.log("try run tools", runTools.length, finished, running);
       if (!running && runTools.length > 0) {
         const toolCallMessage = {
           role: "assistant",
@@ -233,28 +237,20 @@ export function stream(
               }));
           }),
         ).then((toolCallResult) => {
-          console.log("end runTools", toolCallMessage, toolCallResult);
-          // @ts-ignore
-          requestPayload?.messages?.splice(
-            // @ts-ignore
-            requestPayload?.messages?.length,
-            0,
-            toolCallMessage,
-            ...toolCallResult,
-          );
+          processToolMessage(requestPayload, toolCallMessage, toolCallResult);
           setTimeout(() => {
             // call again
-            console.log("start again");
+            console.debug("[ChatAPI] restart");
             running = false;
             chatApi(chatPath, headers, requestPayload, tools); // call fetchEventSource
           }, 60);
         });
-        console.log("try run tools", runTools.length, finished);
         return;
       }
       if (running) {
         return;
       }
+      console.debug("[ChatAPI] end");
       finished = true;
       options.onFinish(responseText + remainText);
     }
@@ -343,7 +339,7 @@ export function stream(
       },
       openWhenHidden: true,
     });
-    console.log("chatApi", chatPath, requestPayload, tools);
   }
+  console.debug("[ChatAPI] start");
   chatApi(chatPath, headers, requestPayload, tools); // call fetchEventSource
 }

From 571ce11e5322dbad657b561e93b457c6531c3d35 Mon Sep 17 00:00:00 2001
From: lloydzhou <lloydzhou@qq.com>
Date: Thu, 29 Aug 2024 19:55:09 +0800
Subject: [PATCH 11/38] stash code

---
 app/client/platforms/openai.ts |  10 ++-
 app/components/chat.tsx        |  79 +++++++++++++++++++-
 app/constant.ts                |   1 +
 app/store/index.ts             |   1 +
 app/store/plugin.ts            | 127 +++++++++++++++++++++++++++++++++
 5 files changed, 213 insertions(+), 5 deletions(-)
 create mode 100644 app/store/plugin.ts

diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts
index 69dbad00505..b96e128f18d 100644
--- a/app/client/platforms/openai.ts
+++ b/app/client/platforms/openai.ts
@@ -14,6 +14,7 @@ import {
   useAccessStore,
   useAppConfig,
   useChatStore,
+  usePluginStore,
 } from "@/app/store";
 import { collectModelsWithDefaultModel } from "@/app/utils/model";
 import {
@@ -240,6 +241,11 @@ export class ChatGPTApi implements LLMApi {
         );
       }
       if (shouldStream) {
+        const [tools1, funcs2] = usePluginStore
+          .getState()
+          .getAsTools(useChatStore.getState().currentSession().mask?.plugin);
+        console.log("getAsTools", tools1, funcs2);
+        // return
         // TODO mock tools and funcs
         const tools = [
           {
@@ -276,8 +282,8 @@ export class ChatGPTApi implements LLMApi {
           chatPath,
           requestPayload,
           getHeaders(),
-          tools,
-          funcs,
+          tools1,
+          funcs2,
           controller,
           // parseSSE
           (text: string, runTools: ChatMessageTool[]) => {
diff --git a/app/components/chat.tsx b/app/components/chat.tsx
index 2ad579aa560..9d3b86f4ecc 100644
--- a/app/components/chat.tsx
+++ b/app/components/chat.tsx
@@ -54,6 +54,7 @@ import {
   useAppConfig,
   DEFAULT_TOPIC,
   ModelType,
+  usePluginStore,
 } from "../store";
 
 import {
@@ -440,6 +441,71 @@ export function ChatActions(props: {
   const config = useAppConfig();
   const navigate = useNavigate();
   const chatStore = useChatStore();
+  const pluginStore = usePluginStore();
+  console.log("pluginStore", pluginStore.getAll());
+  // test
+  if (pluginStore.getAll().length == 0) {
+    pluginStore.create({
+      title: "Pet API",
+      version: "1.0.0",
+      content: `{
+  "openapi": "3.0.2",
+  "info": {
+    "title": "Pet API",
+    "version": "1.0.0"
+  },
+  "paths": {
+    "/api/pets": {
+      "get": {
+        "operationId": "getPets",
+        "description": "Returns all pets from the system that the user has access to",
+        "responses": {
+          "200": {
+            "description": "List of Pets",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "type": "array",
+                  "items": {
+                    "$ref": "#/components/schemas/Pet"
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  },
+  "components": {
+    "schemas": {
+      "Pet": {
+        "type": "object",
+        "properties": {
+          "id": {
+            "type": "string"
+          },
+          "type": {
+            "type": "string",
+            "enum": [
+              "cat",
+              "dog"
+            ]
+          },
+          "name": {
+            "type": "string"
+          }
+        },
+        "required": [
+          "id",
+          "type"
+        ]
+      }
+    }
+  }
+}`,
+    });
+  }
 
   // switch themes
   const theme = config.theme;
@@ -738,15 +804,22 @@ export function ChatActions(props: {
               title: Locale.Plugin.Artifacts,
               value: Plugin.Artifacts,
             },
-          ]}
+          ].concat(
+            pluginStore
+              .getAll()
+              .map((item) => ({
+                title: `${item.title}@${item.version}`,
+                value: item.id,
+              })),
+          )}
           onClose={() => setShowPluginSelector(false)}
           onSelection={(s) => {
             const plugin = s[0];
             chatStore.updateCurrentSession((session) => {
               session.mask.plugin = s;
             });
-            if (plugin) {
-              showToast(plugin);
+            if (s.includes(Plugin.Artifacts)) {
+              showToast(Plugin.Artifacts);
             }
           }}
         />
diff --git a/app/constant.ts b/app/constant.ts
index e88d497ca94..db365f0fc5a 100644
--- a/app/constant.ts
+++ b/app/constant.ts
@@ -78,6 +78,7 @@ export enum Plugin {
 
 export enum StoreKey {
   Chat = "chat-next-web-store",
+  Plugin = "chat-next-web-plugin",
   Access = "access-control",
   Config = "app-config",
   Mask = "mask-store",
diff --git a/app/store/index.ts b/app/store/index.ts
index 0760f48ca26..122afd5d3cb 100644
--- a/app/store/index.ts
+++ b/app/store/index.ts
@@ -2,3 +2,4 @@ export * from "./chat";
 export * from "./update";
 export * from "./access";
 export * from "./config";
+export * from "./plugin";
diff --git a/app/store/plugin.ts b/app/store/plugin.ts
new file mode 100644
index 00000000000..d93044c4dc1
--- /dev/null
+++ b/app/store/plugin.ts
@@ -0,0 +1,127 @@
+import OpenAPIClientAxios from "openapi-client-axios";
+import { getLang, Lang } from "../locales";
+import { StoreKey, Plugin } from "../constant";
+import { nanoid } from "nanoid";
+import { createPersistStore } from "../utils/store";
+import yaml from "js-yaml";
+
+export type Plugin = {
+  id: string;
+  createdAt: number;
+  title: string;
+  version: string;
+  context: string;
+  builtin: boolean;
+};
+
+export const createEmptyPlugin = () =>
+  ({
+    id: nanoid(),
+    title: "",
+    version: "",
+    context: "",
+    builtin: false,
+    createdAt: Date.now(),
+  }) as Plugin;
+
+export const DEFAULT_PLUGIN_STATE = {
+  plugins: {} as Record<string, Plugin>,
+};
+
+export const usePluginStore = createPersistStore(
+  { ...DEFAULT_PLUGIN_STATE },
+
+  (set, get) => ({
+    create(plugin?: Partial<Plugin>) {
+      const plugins = get().plugins;
+      const id = nanoid();
+      plugins[id] = {
+        ...createEmptyPlugin(),
+        ...plugin,
+        id,
+        builtin: false,
+      };
+
+      set(() => ({ plugins }));
+      get().markUpdate();
+
+      return plugins[id];
+    },
+    updatePlugin(id: string, updater: (plugin: Plugin) => void) {
+      const plugins = get().plugins;
+      const plugin = plugins[id];
+      if (!plugin) return;
+      const updatePlugin = { ...plugin };
+      updater(updatePlugin);
+      plugins[id] = updatePlugin;
+      set(() => ({ plugins }));
+      get().markUpdate();
+    },
+    delete(id: string) {
+      const plugins = get().plugins;
+      delete plugins[id];
+      set(() => ({ plugins }));
+      get().markUpdate();
+    },
+
+    getAsTools(ids: string[]) {
+      const plugins = get().plugins;
+      const selected = ids
+        .map((id) => plugins[id])
+        .filter((i) => i)
+        .map((i) => [
+          i,
+          new OpenAPIClientAxios({ definition: yaml.load(i.content) }),
+        ])
+        .map(([item, api]) => {
+          api.initSync();
+          const operations = api.getOperations().map((o) => {
+            const parameters = o.parameters;
+            return [
+              {
+                type: "function",
+                function: {
+                  name: o.operationId,
+                  description: o.description,
+                  parameters: o.parameters,
+                },
+              },
+              api.client[o.operationId],
+            ];
+            // return [{
+            // }, function(arg) {
+            //   const args = []
+            //   for (const p in parameters) {
+            //     if (p.type === "object") {
+            //       const a = {}
+            //       for (const n of p.)
+            //     }
+            //   }
+            // }]
+          });
+          return [item, api, operations];
+        });
+      console.log("selected", selected);
+      const result = selected.reduce((s, i) => s.concat(i[2]), []);
+      return [
+        result.map(([t, _]) => t),
+        result.reduce((s, i) => {
+          s[i[0].function.name] = i[1];
+          return s;
+        }, {}),
+      ];
+    },
+    get(id?: string) {
+      return get().plugins[id ?? 1145141919810];
+    },
+    getAll() {
+      return Object.values(get().plugins).sort(
+        (a, b) => b.createdAt - a.createdAt,
+      );
+    },
+  }),
+  {
+    name: StoreKey.Plugin,
+    version: 1,
+  },
+);

From cac99e390855482c782d2f11af81c2c595164190 Mon Sep 17 00:00:00 2001
From: lloydzhou <lloydzhou@qq.com>
Date: Fri, 30 Aug 2024 13:02:03 +0800
Subject: [PATCH 12/38] add Plugin page

---
 app/components/home.tsx   |   5 ++
 app/components/plugin.tsx | 149 ++++++++++++++++++++++++++++++++++++++
 app/constant.ts           |   2 +
 app/locales/en.ts         |  41 +++++++++++
 4 files changed, 197 insertions(+)
 create mode 100644 app/components/plugin.tsx

diff --git a/app/components/home.tsx b/app/components/home.tsx
index 24e71b9e569..465ad0f1ed1 100644
--- a/app/components/home.tsx
+++ b/app/components/home.tsx
@@ -59,6 +59,10 @@ const MaskPage = dynamic(async () => (await import("./mask")).MaskPage, {
   loading: () => <Loading noLogo />,
 });
 
+const PluginPage = dynamic(async () => (await import("./plugin")).PluginPage, {
+  loading: () => <Loading noLogo />,
+});
+
 const SearchChat = dynamic(
   async () => (await import("./search-chat")).SearchChatPage,
   {
@@ -181,6 +185,7 @@ function Screen() {
             <Route path={Path.Home} element={<Chat />} />
             <Route path={Path.NewChat} element={<NewChat />} />
             <Route path={Path.Masks} element={<MaskPage />} />
+            <Route path={Path.Plugins} element={<PluginPage />} />
             <Route path={Path.SearchChat} element={<SearchChat />} />
             <Route path={Path.Chat} element={<Chat />} />
             <Route path={Path.Settings} element={<Settings />} />
diff --git a/app/components/plugin.tsx b/app/components/plugin.tsx
new file mode 100644
index 00000000000..769e02f2afb
--- /dev/null
+++ b/app/components/plugin.tsx
@@ -0,0 +1,149 @@
+import { IconButton } from "./button";
+import { ErrorBoundary } from "./error";
+
+import styles from "./mask.module.scss";
+
+import DownloadIcon from "../icons/download.svg";
+import EditIcon from "../icons/edit.svg";
+import AddIcon from "../icons/add.svg";
+import CloseIcon from "../icons/close.svg";
+import DeleteIcon from "../icons/delete.svg";
+import EyeIcon from "../icons/eye.svg";
+import CopyIcon from "../icons/copy.svg";
+
+import { Plugin, usePluginStore } from "../store/plugin";
+import {
+  Input,
+  List,
+  ListItem,
+  Modal,
+  Popover,
+  Select,
+  showConfirm,
+} from "./ui-lib";
+import Locale from "../locales";
+import { useNavigate } from "react-router-dom";
+import { useEffect, useState } from "react";
+import { Path } from "../constant";
+import { nanoid } from "nanoid";
+
+export function PluginPage() {
+  const navigate = useNavigate();
+  const pluginStore = usePluginStore();
+  const plugins = pluginStore.getAll();
+
+  const [editingPluginId, setEditingPluginId] = useState<string | undefined>();
+  const editingPlugin = pluginStore.get(editingPluginId);
+  const closePluginModal = () => setEditingPluginId(undefined);
+
+  return (
+    <ErrorBoundary>
+      <div className={styles["mask-page"]}>
+        <div className="window-header">
+          <div className="window-header-title">
+            <div className="window-header-main-title">
+              {Locale.Plugin.Page.Title}
+            </div>
+            <div className="window-header-submai-title">
+              {Locale.Plugin.Page.SubTitle(plugins.length)}
+            </div>
+          </div>
+
+          <div className="window-actions">
+            <div className="window-action-button">
+              <IconButton
+                icon={<CloseIcon />}
+                bordered
+                onClick={() => navigate(-1)}
+              />
+            </div>
+          </div>
+        </div>
+
+        <div className={styles["mask-page-body"]}>
+          <div>
+            {plugins.map((m) => (
+              <div className={styles["mask-item"]} key={m.id}>
+                <div className={styles["mask-header"]}>
+                  <div className={styles["mask-icon"]}></div>
+                  <div className={styles["mask-title"]}>
+                    <div className={styles["mask-name"]}>
+                      {m.title}@<small>{m.version}</small>
+                    </div>
+                    <div className={styles["mask-info"] + " one-line"}>
+                      {`${Locale.Plugin.Item.Info(m.content.length)} / / `}
+                    </div>
+                  </div>
+                </div>
+                <div className={styles["mask-actions"]}>
+                  {m.builtin ? (
+                    <IconButton
+                      icon={<EyeIcon />}
+                      text={Locale.Plugin.Item.View}
+                      onClick={() => setEditingPluginId(m.id)}
+                    />
+                  ) : (
+                    <IconButton
+                      icon={<EditIcon />}
+                      text={Locale.Plugin.Item.Edit}
+                      onClick={() => setEditingPluginId(m.id)}
+                    />
+                  )}
+                  {!m.builtin && (
+                    <IconButton
+                      icon={<DeleteIcon />}
+                      text={Locale.Plugin.Item.Delete}
+                      onClick={async () => {
+                        if (
+                          await showConfirm(Locale.Plugin.Item.DeleteConfirm)
+                        ) {
+                          pluginStore.delete(m.id);
+                        }
+                      }}
+                    />
+                  )}
+                </div>
+              </div>
+            ))}
+          </div>
+        </div>
+      </div>
+
+      {editingPlugin && (
+        <div className="modal-mask">
+          <Modal
+            title={Locale.Plugin.EditModal.Title(editingPlugin?.builtin)}
+            onClose={closePluginModal}
+            actions={[
+              <IconButton
+                icon={<DownloadIcon />}
+                text={Locale.Plugin.EditModal.Download}
+                key="export"
+                bordered
+                onClick={() =>
+                  downloadAs(
+                    JSON.stringify(editingPlugin),
+                    `${editingPlugin.name}.json`,
+                  )
+                }
+              />,
+              <IconButton
+                key="copy"
+                icon={<CopyIcon />}
+                bordered
+                text={Locale.Plugin.EditModal.Clone}
+                onClick={() => {
+                  navigate(Path.Plugins);
+                  pluginStore.create(editingPlugin);
+                  setEditingPluginId(undefined);
+                }}
+              />,
+            ]}
+          >
+            PluginConfig
+          </Modal>
+        </div>
+      )}
+    </ErrorBoundary>
+  );
+}
diff --git a/app/constant.ts b/app/constant.ts
index db365f0fc5a..0fa14dca573 100644
--- a/app/constant.ts
+++ b/app/constant.ts
@@ -39,6 +39,7 @@ export enum Path {
   Settings = "/settings",
   NewChat = "/new-chat",
   Masks = "/masks",
+  Plugins = "/plugins",
   Auth = "/auth",
   Sd = "/sd",
   SdNew = "/sd-new",
@@ -480,6 +481,7 @@ export const internalAllowedWebDavEndpoints = [
 
 export const DEFAULT_GA_ID = "G-89WN60ZK2E";
 export const PLUGINS = [
+  { name: "Plugins", path: Path.Plugins },
   { name: "Stable Diffusion", path: Path.Sd },
   { name: "Search Chat", path: Path.SearchChat },
 ];
diff --git a/app/locales/en.ts b/app/locales/en.ts
index 77f3a700ae1..ea098c0f3a4 100644
--- a/app/locales/en.ts
+++ b/app/locales/en.ts
@@ -542,6 +542,47 @@ const en: LocaleType = {
       View: "View",
     },
   },
+  Plugin: {
+    Name: "Plugin",
+    Page: {
+      Title: "Plugins",
+      SubTitle: (count: number) => `${count} plugins`,
+      Search: "Search Plugin",
+      Create: "Create",
+    },
+    Item: {
+      Info: (count: number) => `${count} plugins`,
+      Chat: "Chat",
+      View: "View",
+      Edit: "Edit",
+      Delete: "Delete",
+      DeleteConfirm: "Confirm to delete?",
+    },
+    EditModal: {
+      Title: (readonly: boolean) =>
+        `Edit Plugin ${readonly ? "(readonly)" : ""}`,
+      Download: "Download",
+      Clone: "Clone",
+    },
+    Config: {
+      Avatar: "Bot Avatar",
+      Name: "Bot Name",
+      Sync: {
+        Title: "Use Global Config",
+        SubTitle: "Use global config in this chat",
+        Confirm: "Confirm to override custom config with global config?",
+      },
+      HideContext: {
+        Title: "Hide Context Prompts",
+        SubTitle: "Do not show in-context prompts in chat",
+      },
+      Share: {
+        Title: "Share This Plugin",
+        SubTitle: "Generate a link to this mask",
+        Action: "Copy Link",
+      },
+    },
+  },
   Mask: {
     Name: "Mask",
     Page: {

From 271f58d9cf1219cfe74b041ad90b162f2c519a53 Mon Sep 17 00:00:00 2001
From: lloydzhou <lloydzhou@qq.com>
Date: Fri, 30 Aug 2024 17:31:20 +0800
Subject: [PATCH 13/38] stash code

---
 app/client/platforms/openai.ts    |  41 +---------
 app/components/chat.tsx           |  74 +----------------
 app/components/plugin.module.scss |  15 ++++
 app/components/plugin.tsx         | 129 ++++++++++++++++++++++++++----
 app/locales/cn.ts                 |  28 ++++++-
 app/locales/en.ts                 |  30 ++-----
 app/store/plugin.ts               | 123 ++++++++++++++++++----------
 7 files changed, 246 insertions(+), 194 deletions(-)
 create mode 100644 app/components/plugin.module.scss

diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts
index b96e128f18d..f0c577c7516 100644
--- a/app/client/platforms/openai.ts
+++ b/app/client/platforms/openai.ts
@@ -241,49 +241,16 @@ export class ChatGPTApi implements LLMApi {
         );
       }
       if (shouldStream) {
-        const [tools1, funcs2] = usePluginStore
+        const [tools, funcs] = usePluginStore
           .getState()
           .getAsTools(useChatStore.getState().currentSession().mask?.plugin);
-        console.log("getAsTools", tools1, funcs2);
-        // return
-        // TODO mock tools and funcs
-        const tools = [
-          {
-            type: "function",
-            function: {
-              name: "get_current_weather",
-              description: "Get the current weather",
-              parameters: {
-                type: "object",
-                properties: {
-                  location: {
-                    type: "string",
-                    description: "The city and country, eg. San Francisco, USA",
-                  },
-                  format: {
-                    type: "string",
-                    enum: ["celsius", "fahrenheit"],
-                  },
-                },
-                required: ["location", "format"],
-              },
-            },
-          },
-        ];
-        const funcs = {
-          get_current_weather: (args: any) => {
-            console.log("call get_current_weather", args);
-            return new Promise((resolve) => {
-              setTimeout(() => resolve("30"), 3000);
-            });
-          },
-        };
+        console.log("getAsTools", tools, funcs);
         stream(
           chatPath,
           requestPayload,
           getHeaders(),
-          tools1,
-          funcs2,
+          tools,
+          funcs,
           controller,
           // parseSSE
           (text: string, runTools: ChatMessageTool[]) => {
diff --git a/app/components/chat.tsx b/app/components/chat.tsx
index 9d3b86f4ecc..0e2aa3e2fe6 100644
--- a/app/components/chat.tsx
+++ b/app/components/chat.tsx
@@ -442,70 +442,6 @@ export function ChatActions(props: {
   const navigate = useNavigate();
   const chatStore = useChatStore();
   const pluginStore = usePluginStore();
-  console.log("pluginStore", pluginStore.getAll());
-  // test
-  if (pluginStore.getAll().length == 0) {
-    pluginStore.create({
-      title: "Pet API",
-      version: "1.0.0",
-      content: `{
-  "openapi": "3.0.2",
-  "info": {
-    "title": "Pet API",
-    "version": "1.0.0"
-  },
-  "paths": {
-    "/api/pets": {
-      "get": {
-        "operationId": "getPets",
-        "description": "Returns all pets from the system that the user has access to",
-        "responses": {
-          "200": {
-            "description": "List of Pets",
-            "content": {
-              "application/json": {
-                "schema": {
-                  "type": "array",
-                  "items": {
-                    "$ref": "#/components/schemas/Pet"
-                  }
-                }
-              }
-            }
-          }
-        }
-      }
-    }
-  },
-  "components": {
-    "schemas": {
-      "Pet": {
-        "type": "object",
-        "properties": {
-          "id": {
-            "type": "string"
-          },
-          "type": {
-            "type": "string",
-            "enum": [
-              "cat",
-              "dog"
-            ]
-          },
-          "name": {
-            "type": "string"
-          }
-        },
-        "required": [
-          "id",
-          "type"
-        ]
-      }
-    }
-  }
-}`,
-    });
-  }
 
   // switch themes
   const theme = config.theme;
@@ -805,12 +741,10 @@ export function ChatActions(props: {
               value: Plugin.Artifacts,
             },
           ].concat(
-            pluginStore
-              .getAll()
-              .map((item) => ({
-                title: `${item.title}@${item.version}`,
-                value: item.id,
-              })),
+            pluginStore.getAll().map((item) => ({
+              title: `${item.title}@${item.version}`,
+              value: item.id,
+            })),
           )}
           onClose={() => setShowPluginSelector(false)}
           onSelection={(s) => {
diff --git a/app/components/plugin.module.scss b/app/components/plugin.module.scss
new file mode 100644
index 00000000000..53c63246836
--- /dev/null
+++ b/app/components/plugin.module.scss
@@ -0,0 +1,15 @@
+.plugin-title {
+  font-weight: bolder;
+  font-size: 16px;
+  margin: 10px 0;
+}
+.plugin-content {
+  font-size: 14px;
+  font-family: inherit;
+  pre code {
+    max-height: 240px;
+    overflow-y: auto;
+    white-space: pre-wrap;
+  }
+}
+
diff --git a/app/components/plugin.tsx b/app/components/plugin.tsx
index 769e02f2afb..247cba257cd 100644
--- a/app/components/plugin.tsx
+++ b/app/components/plugin.tsx
@@ -1,7 +1,11 @@
+import { useDebouncedCallback } from "use-debounce";
+import OpenAPIClientAxios from "openapi-client-axios";
+import yaml from "js-yaml";
 import { IconButton } from "./button";
 import { ErrorBoundary } from "./error";
 
 import styles from "./mask.module.scss";
+import pluginStyles from "./plugin.module.scss";
 
 import DownloadIcon from "../icons/download.svg";
 import EditIcon from "../icons/edit.svg";
@@ -11,7 +15,7 @@ import DeleteIcon from "../icons/delete.svg";
 import EyeIcon from "../icons/eye.svg";
 import CopyIcon from "../icons/copy.svg";
 
-import { Plugin, usePluginStore } from "../store/plugin";
+import { Plugin, usePluginStore, FunctionToolService } from "../store/plugin";
 import {
   Input,
   List,
@@ -20,7 +24,9 @@ import {
   Popover,
   Select,
   showConfirm,
+  showToast,
 } from "./ui-lib";
+import { downloadAs } from "../utils";
 import Locale from "../locales";
 import { useNavigate } from "react-router-dom";
 import { useEffect, useState } from "react";
@@ -30,12 +36,56 @@ import { nanoid } from "nanoid";
 export function PluginPage() {
   const navigate = useNavigate();
   const pluginStore = usePluginStore();
-  const plugins = pluginStore.getAll();
+
+  const allPlugins = pluginStore.getAll();
+  const [searchPlugins, setSearchPlugins] = useState<Plugin[]>([]);
+  const [searchText, setSearchText] = useState("");
+  const plugins = searchText.length > 0 ? searchPlugins : allPlugins;
+
+  // refactored already, now it accurate
+  const onSearch = (text: string) => {
+    setSearchText(text);
+    if (text.length > 0) {
+      const result = allPlugins.filter((m) =>
+        m.title.toLowerCase().includes(text.toLowerCase()),
+      );
+      setSearchPlugins(result);
+    } else {
+      setSearchPlugins(allPlugins);
+    }
+  };
 
   const [editingPluginId, setEditingPluginId] = useState<string | undefined>();
   const editingPlugin = pluginStore.get(editingPluginId);
+  const editingPluginTool = FunctionToolService.get(editingPlugin?.id);
   const closePluginModal = () => setEditingPluginId(undefined);
 
+  const onChangePlugin = useDebouncedCallback((editingPlugin, e) => {
+    const content = e.target.innerText;
+    try {
+      const api = new OpenAPIClientAxios({ definition: yaml.load(content) });
+      api
+        .init()
+        .then(() => {
+          if (content != editingPlugin.content) {
+            pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
+              plugin.content = content;
+              const tool = FunctionToolService.add(plugin, true);
+              plugin.title = tool.api.definition.info.title;
+              plugin.version = tool.api.definition.info.version;
+            });
+          }
+        })
+        .catch((e) => {
+          console.error(e);
+          showToast(Locale.Plugin.EditModal.Error);
+        });
+    } catch (e) {
+      console.error(e);
+      showToast(Locale.Plugin.EditModal.Error);
+    }
+  }, 100).bind(null, editingPlugin);
+
   return (
     <ErrorBoundary>
       <div className={styles["mask-page"]}>
@@ -61,6 +111,27 @@ export function PluginPage() {
         </div>
 
         <div className={styles["mask-page-body"]}>
+          <div className={styles["mask-filter"]}>
+            <input
+              type="text"
+              className={styles["search-bar"]}
+              placeholder={Locale.Plugin.Page.Search}
+              autoFocus
+              onInput={(e) => onSearch(e.currentTarget.value)}
+            />
+
+            <IconButton
+              className={styles["mask-create"]}
+              icon={<AddIcon />}
+              text={Locale.Plugin.Page.Create}
+              bordered
+              onClick={() => {
+                const createdPlugin = pluginStore.create();
+                setEditingPluginId(createdPlugin.id);
+              }}
+            />
+          </div>
+
           <div>
             {plugins.map((m) => (
               <div className={styles["mask-item"]} key={m.id}>
@@ -71,7 +142,9 @@ export function PluginPage() {
                       {m.title}@<small>{m.version}</small>
                     </div>
                     <div className={styles["mask-info"] + " one-line"}>
-                      {`${Locale.Plugin.Item.Info(m.content.length)} / / `}
+                      {Locale.Plugin.Item.Info(
+                        FunctionToolService.add(m).length,
+                      )}
                     </div>
                   </div>
                 </div>
@@ -123,24 +196,48 @@ export function PluginPage() {
                 onClick={() =>
                   downloadAs(
                     JSON.stringify(editingPlugin),
-                    `${editingPlugin.name}.json`,
+                    `${editingPlugin.title}@${editingPlugin.version}.json`,
                   )
                 }
               />,
-              <IconButton
-                key="copy"
-                icon={<CopyIcon />}
-                bordered
-                text={Locale.Plugin.EditModal.Clone}
-                onClick={() => {
-                  navigate(Path.Plugins);
-                  pluginStore.create(editingPlugin);
-                  setEditingPluginId(undefined);
-                }}
-              />,
             ]}
           >
-            PluginConfig
+            <div className={styles["mask-page"]}>
+              <div className={pluginStyles["plugin-title"]}>
+                {Locale.Plugin.EditModal.Content}
+              </div>
+              <div
+                className={`markdown-body ${pluginStyles["plugin-content"]}`}
+                dir="auto"
+              >
+                <pre>
+                  <code
+                    contentEditable={true}
+                    dangerouslySetInnerHTML={{ __html: editingPlugin.content }}
+                    onBlur={onChangePlugin}
+                  ></code>
+                </pre>
+              </div>
+              <div className={pluginStyles["plugin-title"]}>
+                {Locale.Plugin.EditModal.Method}
+              </div>
+              <div className={styles["mask-page-body"]} style={{ padding: 0 }}>
+                {editingPluginTool?.tools.map((tool, index) => (
+                  <div className={styles["mask-item"]} key={index}>
+                    <div className={styles["mask-header"]}>
+                      <div className={styles["mask-title"]}>
+                        <div className={styles["mask-name"]}>
+                          {tool?.function?.name}
+                        </div>
+                        <div className={styles["mask-info"] + " one-line"}>
+                          {tool?.function?.description}
+                        </div>
+                      </div>
+                    </div>
+                  </div>
+                ))}
+              </div>
+            </div>
           </Modal>
         </div>
       )}
diff --git a/app/locales/cn.ts b/app/locales/cn.ts
index 9a3227d68a5..f0ff705c180 100644
--- a/app/locales/cn.ts
+++ b/app/locales/cn.ts
@@ -509,10 +509,6 @@ const cn = {
     Clear: "上下文已清除",
     Revert: "恢复上下文",
   },
-  Plugin: {
-    Name: "插件",
-    Artifacts: "Artifacts",
-  },
   Discovery: {
     Name: "发现",
   },
@@ -534,6 +530,30 @@ const cn = {
       View: "查看",
     },
   },
+  Plugin: {
+    Name: "插件",
+    Artifacts: "Artifacts",
+    Page: {
+      Title: "插件",
+      SubTitle: (count: number) => `${count} 个插件`,
+      Search: "搜索插件",
+      Create: "新建",
+    },
+    Item: {
+      Info: (count: number) => `${count} 方法`,
+      View: "查看",
+      Edit: "编辑",
+      Delete: "删除",
+      DeleteConfirm: "确认删除?",
+    },
+    EditModal: {
+      Title: (readonly: boolean) => `编辑插件 ${readonly ? "(只读)" : ""}`,
+      Download: "下载",
+      Content: "OpenAPI Schema",
+      Method: "方法",
+      Error: "格式错误",
+    },
+  },
   Mask: {
     Name: "面具",
     Page: {
diff --git a/app/locales/en.ts b/app/locales/en.ts
index ea098c0f3a4..15db8190a81 100644
--- a/app/locales/en.ts
+++ b/app/locales/en.ts
@@ -517,10 +517,6 @@ const en: LocaleType = {
     Clear: "Context Cleared",
     Revert: "Revert",
   },
-  Plugin: {
-    Name: "Plugin",
-    Artifacts: "Artifacts",
-  },
   Discovery: {
     Name: "Discovery",
   },
@@ -544,6 +540,7 @@ const en: LocaleType = {
   },
   Plugin: {
     Name: "Plugin",
+    Artifacts: "Artifacts",
     Page: {
       Title: "Plugins",
       SubTitle: (count: number) => `${count} plugins`,
@@ -551,8 +548,7 @@ const en: LocaleType = {
       Create: "Create",
     },
     Item: {
-      Info: (count: number) => `${count} plugins`,
-      Chat: "Chat",
+      Info: (count: number) => `${count} method`,
       View: "View",
       Edit: "Edit",
       Delete: "Delete",
@@ -562,25 +558,9 @@ const en: LocaleType = {
       Title: (readonly: boolean) =>
         `Edit Plugin ${readonly ? "(readonly)" : ""}`,
       Download: "Download",
-      Clone: "Clone",
-    },
-    Config: {
-      Avatar: "Bot Avatar",
-      Name: "Bot Name",
-      Sync: {
-        Title: "Use Global Config",
-        SubTitle: "Use global config in this chat",
-        Confirm: "Confirm to override custom config with global config?",
-      },
-      HideContext: {
-        Title: "Hide Context Prompts",
-        SubTitle: "Do not show in-context prompts in chat",
-      },
-      Share: {
-        Title: "Share This Plugin",
-        SubTitle: "Generate a link to this mask",
-        Action: "Copy Link",
-      },
+      Content: "OpenAPI Schema",
+      Method: "Method",
+      Error: "OpenAPI Schema Error",
     },
   },
   Mask: {
diff --git a/app/store/plugin.ts b/app/store/plugin.ts
index d93044c4dc1..b25b162a34b 100644
--- a/app/store/plugin.ts
+++ b/app/store/plugin.ts
@@ -10,16 +10,91 @@ export type Plugin = {
   createdAt: number;
   title: string;
   version: string;
-  context: string;
+  content: string;
   builtin: boolean;
 };
 
+export type FunctionToolItem = {
+  type: string;
+  function: {
+    name: string;
+    description?: string;
+    parameters: Object;
+  };
+};
+
+type FunctionToolServiceItem = {
+  api: OpenAPIClientAxios;
+  tools: FunctionToolItem[];
+  funcs: Function[];
+};
+
+export const FunctionToolService = {
+  tools: {} as Record<string, FunctionToolServiceItem>,
+  add(plugin: Plugin, replace = false) {
+    if (!replace && this.tools[plugin.id]) return this.tools[plugin.id];
+    const api = new OpenAPIClientAxios({
+      definition: yaml.load(plugin.content),
+    });
+    console.log("add", plugin, api);
+    try {
+      api.initSync();
+    } catch (e) {}
+    const operations = api.getOperations();
+    return (this.tools[plugin.id] = {
+      api,
+      length: operations.length,
+      tools: operations.map((o) => {
+        const parameters = o?.requestBody?.content["application/json"]
+          ?.schema || {
+          type: "object",
+          properties: {},
+        };
+        if (!parameters["required"]) {
+          parameters["required"] = [];
+        }
+        if (o.parameters instanceof Array) {
+          o.parameters.forEach((p) => {
+            if (p.in == "query" || p.in == "path") {
+              // const name = `${p.in}__${p.name}`
+              const name = p.name;
+              console.log("p", p, p.schema);
+              parameters["properties"][name] = {
+                type: p.schema.type,
+                description: p.description,
+              };
+              if (p.required) {
+                parameters["required"].push(name);
+              }
+            }
+          });
+        }
+        return {
+          type: "function",
+          function: {
+            name: o.operationId,
+            description: o.description,
+            parameters: parameters,
+          },
+        };
+      }),
+      funcs: operations.reduce((s, o) => {
+        s[o.operationId] = api.client[o.operationId];
+        return s;
+      }, {}),
+    });
+  },
+  get(id) {
+    return this.tools[id];
+  },
+};
+
 export const createEmptyPlugin = () =>
   ({
     id: nanoid(),
     title: "",
-    version: "",
-    context: "",
+    version: "1.0.0",
+    content: "",
     builtin: false,
     createdAt: Date.now(),
   }) as Plugin;
@@ -69,46 +144,10 @@ export const usePluginStore = createPersistStore(
       const selected = ids
         .map((id) => plugins[id])
         .filter((i) => i)
-        .map((i) => [
-          i,
-          new OpenAPIClientAxios({ definition: yaml.load(i.content) }),
-        ])
-        .map(([item, api]) => {
-          api.initSync();
-          const operations = api.getOperations().map((o) => {
-            const parameters = o.parameters;
-            return [
-              {
-                type: "function",
-                function: {
-                  name: o.operationId,
-                  description: o.description,
-                  parameters: o.parameters,
-                },
-              },
-              api.client[o.operationId],
-            ];
-            // return [{
-            // }, function(arg) {
-            //   const args = []
-            //   for (const p in parameters) {
-            //     if (p.type === "object") {
-            //       const a = {}
-            //       for (const n of p.)
-            //     }
-            //   }
-            // }]
-          });
-          return [item, api, operations];
-        });
-      console.log("selected", selected);
-      const result = selected.reduce((s, i) => s.concat(i[2]), []);
+        .map((p) => FunctionToolService.add(p));
       return [
-        result.map(([t, _]) => t),
-        result.reduce((s, i) => {
-          s[i[0].function.name] = i[1];
-          return s;
-        }, {}),
+        selected.reduce((s, i) => s.concat(i.tools), []),
+        selected.reduce((s, i) => Object.assign(s, i.funcs), {}),
       ];
     },
     get(id?: string) {

From 9326ff9d0873e2fe684dbd5df0eccdc3e1ae3403 Mon Sep 17 00:00:00 2001
From: lloydzhou <lloydzhou@qq.com>
Date: Fri, 30 Aug 2024 23:39:08 +0800
Subject: [PATCH 14/38] ts error

---
 app/client/platforms/openai.ts |  6 ++++--
 app/components/chat.tsx        | 17 +++++++++--------
 app/components/markdown.tsx    |  4 ++--
 app/components/plugin.tsx      |  8 +++++---
 app/constant.ts                |  2 +-
 app/store/mask.ts              |  6 +++---
 app/store/plugin.ts            | 24 ++++++++++++++++--------
 app/utils/chat.ts              |  2 +-
 package.json                   |  1 +
 yarn.lock                      |  5 +++++
 10 files changed, 47 insertions(+), 28 deletions(-)

diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts
index f0c577c7516..4c5831fe3e9 100644
--- a/app/client/platforms/openai.ts
+++ b/app/client/platforms/openai.ts
@@ -243,13 +243,15 @@ export class ChatGPTApi implements LLMApi {
       if (shouldStream) {
         const [tools, funcs] = usePluginStore
           .getState()
-          .getAsTools(useChatStore.getState().currentSession().mask?.plugin);
+          .getAsTools(
+            useChatStore.getState().currentSession().mask?.plugin as string[],
+          );
         console.log("getAsTools", tools, funcs);
         stream(
           chatPath,
           requestPayload,
           getHeaders(),
-          tools,
+          tools as any,
           funcs,
           controller,
           // parseSSE
diff --git a/app/components/chat.tsx b/app/components/chat.tsx
index 0e2aa3e2fe6..7bac62bc4c3 100644
--- a/app/components/chat.tsx
+++ b/app/components/chat.tsx
@@ -97,7 +97,7 @@ import {
   REQUEST_TIMEOUT_MS,
   UNFINISHED_INPUT,
   ServiceProvider,
-  Plugin,
+  ArtifactsPlugin,
 } from "../constant";
 import { Avatar } from "./emoji";
 import { ContextPrompts, MaskAvatar, MaskConfig } from "./mask";
@@ -738,22 +738,23 @@ export function ChatActions(props: {
           items={[
             {
               title: Locale.Plugin.Artifacts,
-              value: Plugin.Artifacts,
+              value: ArtifactsPlugin.Artifacts as string,
             },
           ].concat(
             pluginStore.getAll().map((item) => ({
-              title: `${item.title}@${item.version}`,
-              value: item.id,
+              // @ts-ignore
+              title: `${item?.title}@${item?.version}`,
+              // @ts-ignore
+              value: item?.id,
             })),
           )}
           onClose={() => setShowPluginSelector(false)}
           onSelection={(s) => {
-            const plugin = s[0];
             chatStore.updateCurrentSession((session) => {
-              session.mask.plugin = s;
+              session.mask.plugin = s as string[];
             });
-            if (s.includes(Plugin.Artifacts)) {
-              showToast(Plugin.Artifacts);
+            if (s.includes(ArtifactsPlugin.Artifacts)) {
+              showToast(ArtifactsPlugin.Artifacts);
             }
           }}
         />
diff --git a/app/components/markdown.tsx b/app/components/markdown.tsx
index 500af71752f..58579ab47a4 100644
--- a/app/components/markdown.tsx
+++ b/app/components/markdown.tsx
@@ -19,7 +19,7 @@ import {
   HTMLPreview,
   HTMLPreviewHander,
 } from "./artifacts";
-import { Plugin } from "../constant";
+import { ArtifactsPlugin } from "../constant";
 import { useChatStore } from "../store";
 import { IconButton } from "./button";
 
@@ -95,7 +95,7 @@ export function PreCode(props: { children: any }) {
   }, 600);
 
   const enableArtifacts = useMemo(
-    () => plugins?.includes(Plugin.Artifacts),
+    () => plugins?.includes(ArtifactsPlugin.Artifacts),
     [plugins],
   );
 
diff --git a/app/components/plugin.tsx b/app/components/plugin.tsx
index 247cba257cd..35cda90894b 100644
--- a/app/components/plugin.tsx
+++ b/app/components/plugin.tsx
@@ -46,8 +46,8 @@ export function PluginPage() {
   const onSearch = (text: string) => {
     setSearchText(text);
     if (text.length > 0) {
-      const result = allPlugins.filter((m) =>
-        m.title.toLowerCase().includes(text.toLowerCase()),
+      const result = allPlugins.filter(
+        (m) => m?.title.toLowerCase().includes(text.toLowerCase()),
       );
       setSearchPlugins(result);
     } else {
@@ -63,7 +63,9 @@ export function PluginPage() {
   const onChangePlugin = useDebouncedCallback((editingPlugin, e) => {
     const content = e.target.innerText;
     try {
-      const api = new OpenAPIClientAxios({ definition: yaml.load(content) });
+      const api = new OpenAPIClientAxios({
+        definition: yaml.load(content) as any,
+      });
       api
         .init()
         .then(() => {
diff --git a/app/constant.ts b/app/constant.ts
index 0fa14dca573..f6026d671c8 100644
--- a/app/constant.ts
+++ b/app/constant.ts
@@ -73,7 +73,7 @@ export enum FileName {
   Prompts = "prompts.json",
 }
 
-export enum Plugin {
+export enum ArtifactsPlugin {
   Artifacts = "artifacts",
 }
 
diff --git a/app/store/mask.ts b/app/store/mask.ts
index a790f89f833..05f511b0fbb 100644
--- a/app/store/mask.ts
+++ b/app/store/mask.ts
@@ -2,7 +2,7 @@ import { BUILTIN_MASKS } from "../masks";
 import { getLang, Lang } from "../locales";
 import { DEFAULT_TOPIC, ChatMessage } from "./chat";
 import { ModelConfig, useAppConfig } from "./config";
-import { StoreKey, Plugin } from "../constant";
+import { StoreKey, ArtifactsPlugin } from "../constant";
 import { nanoid } from "nanoid";
 import { createPersistStore } from "../utils/store";
 
@@ -17,7 +17,7 @@ export type Mask = {
   modelConfig: ModelConfig;
   lang: Lang;
   builtin: boolean;
-  plugin?: Plugin[];
+  plugin?: string[];
 };
 
 export const DEFAULT_MASK_STATE = {
@@ -38,7 +38,7 @@ export const createEmptyMask = () =>
     lang: getLang(),
     builtin: false,
     createdAt: Date.now(),
-    plugin: [Plugin.Artifacts],
+    plugin: [ArtifactsPlugin.Artifacts as string],
   }) as Mask;
 
 export const useMaskStore = createPersistStore(
diff --git a/app/store/plugin.ts b/app/store/plugin.ts
index b25b162a34b..031a2aaf55b 100644
--- a/app/store/plugin.ts
+++ b/app/store/plugin.ts
@@ -1,6 +1,6 @@
 import OpenAPIClientAxios from "openapi-client-axios";
 import { getLang, Lang } from "../locales";
-import { StoreKey, Plugin } from "../constant";
+import { StoreKey } from "../constant";
 import { nanoid } from "nanoid";
 import { createPersistStore } from "../utils/store";
 import yaml from "js-yaml";
@@ -25,8 +25,9 @@ export type FunctionToolItem = {
 
 type FunctionToolServiceItem = {
   api: OpenAPIClientAxios;
+  length: number;
   tools: FunctionToolItem[];
-  funcs: Function[];
+  funcs: Record<string, Function>;
 };
 
 export const FunctionToolService = {
@@ -34,7 +35,7 @@ export const FunctionToolService = {
   add(plugin: Plugin, replace = false) {
     if (!replace && this.tools[plugin.id]) return this.tools[plugin.id];
     const api = new OpenAPIClientAxios({
-      definition: yaml.load(plugin.content),
+      definition: yaml.load(plugin.content) as any,
     });
     console.log("add", plugin, api);
     try {
@@ -45,6 +46,7 @@ export const FunctionToolService = {
       api,
       length: operations.length,
       tools: operations.map((o) => {
+        // @ts-ignore
         const parameters = o?.requestBody?.content["application/json"]
           ?.schema || {
           type: "object",
@@ -55,14 +57,18 @@ export const FunctionToolService = {
         }
         if (o.parameters instanceof Array) {
           o.parameters.forEach((p) => {
-            if (p.in == "query" || p.in == "path") {
+            // @ts-ignore
+            if (p?.in == "query" || p?.in == "path") {
               // const name = `${p.in}__${p.name}`
-              const name = p.name;
-              console.log("p", p, p.schema);
+              // @ts-ignore
+              const name = p?.name;
               parameters["properties"][name] = {
+                // @ts-ignore
                 type: p.schema.type,
+                // @ts-ignore
                 description: p.description,
               };
+              // @ts-ignore
               if (p.required) {
                 parameters["required"].push(name);
               }
@@ -76,15 +82,16 @@ export const FunctionToolService = {
             description: o.description,
             parameters: parameters,
           },
-        };
+        } as FunctionToolItem;
       }),
       funcs: operations.reduce((s, o) => {
+        // @ts-ignore
         s[o.operationId] = api.client[o.operationId];
         return s;
       }, {}),
     });
   },
-  get(id) {
+  get(id: string) {
     return this.tools[id];
   },
 };
@@ -146,6 +153,7 @@ export const usePluginStore = createPersistStore(
         .filter((i) => i)
         .map((p) => FunctionToolService.add(p));
       return [
+        // @ts-ignore
         selected.reduce((s, i) => s.concat(i.tools), []),
         selected.reduce((s, i) => Object.assign(s, i.funcs), {}),
       ];
diff --git a/app/utils/chat.ts b/app/utils/chat.ts
index aa268a9fe41..454a24771ca 100644
--- a/app/utils/chat.ts
+++ b/app/utils/chat.ts
@@ -158,7 +158,7 @@ export function stream(
   requestPayload: any,
   headers: any,
   tools: any[],
-  funcs: any,
+  funcs: Record<string, Function>,
   controller: AbortController,
   parseSSE: (text: string, runTools: any[]) => string | undefined,
   processToolMessage: (
diff --git a/package.json b/package.json
index 6907bbf455f..82f23a4a0f8 100644
--- a/package.json
+++ b/package.json
@@ -57,6 +57,7 @@
     "@types/react-dom": "^18.2.7",
     "@types/react-katex": "^3.0.0",
     "@types/spark-md5": "^3.0.4",
+    "@types/js-yaml": "4.0.9",
     "concurrently": "^8.2.2",
     "cross-env": "^7.0.3",
     "eslint": "^8.49.0",
diff --git a/yarn.lock b/yarn.lock
index 6d8c07cc1d3..f138eb95738 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1684,6 +1684,11 @@
     "@types/react" "*"
     hoist-non-react-statics "^3.3.0"
 
+"@types/js-yaml@4.0.9":
+  version "4.0.9"
+  resolved "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz#cd82382c4f902fed9691a2ed79ec68c5898af4c2"
+  integrity sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==
+
 "@types/json-schema@*", "@types/json-schema@^7.0.8":
   version "7.0.12"
   resolved "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb"

From 2214689920f1531a6701171d77cc33c342d35f83 Mon Sep 17 00:00:00 2001
From: lloydzhou <lloydzhou@qq.com>
Date: Fri, 30 Aug 2024 23:51:03 +0800
Subject: [PATCH 15/38] add gapier proxy

---
 next.config.mjs | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/next.config.mjs b/next.config.mjs
index 27c60dd2997..6b1dd0c35c8 100644
--- a/next.config.mjs
+++ b/next.config.mjs
@@ -86,6 +86,10 @@ if (mode !== "export") {
         source: "/api/proxy/anthropic/:path*",
         destination: "https://api.anthropic.com/:path*",
       },
+      {
+        source: "/api/proxy/gapier/:path*",
+        destination: "https://a.gapier.com/:path*",
+      },
       {
         source: "/google-fonts/:path*",
         destination: "https://fonts.googleapis.com/:path*",

From b2965e1deb48f9e63aabecb1d477185d22f19e1f Mon Sep 17 00:00:00 2001
From: lloydzhou <lloydzhou@qq.com>
Date: Sat, 31 Aug 2024 00:16:47 +0800
Subject: [PATCH 16/38] update

---
 app/utils/chat.ts | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/app/utils/chat.ts b/app/utils/chat.ts
index 454a24771ca..d8ab5770cc2 100644
--- a/app/utils/chat.ts
+++ b/app/utils/chat.ts
@@ -218,6 +218,13 @@ export function stream(
                 JSON.parse(tool.function.arguments),
               ),
             )
+              .then((res) => {
+                const content = JSON.stringify(res.data);
+                if (res.status >= 300) {
+                  return Promise.reject(content);
+                }
+                return content;
+              })
               .then((content) => {
                 options?.onAfterTool?.({
                   ...tool,

From f652f7326070dfbffcc5f90a999e95df21f4c650 Mon Sep 17 00:00:00 2001
From: lloydzhou <lloydzhou@qq.com>
Date: Mon, 2 Sep 2024 18:11:19 +0800
Subject: [PATCH 17/38] plugin add auth config

---
 app/api/[provider]/[...path]/route.ts |   6 +-
 app/api/proxy.ts                      |  75 +++++++++++++++
 app/components/plugin.tsx             | 133 ++++++++++++++++++--------
 app/components/ui-lib.tsx             |   2 +-
 app/locales/cn.ts                     |  11 +++
 app/locales/en.ts                     |  11 +++
 app/store/plugin.ts                   |  46 ++++++++-
 next.config.mjs                       |   4 -
 8 files changed, 237 insertions(+), 51 deletions(-)
 create mode 100644 app/api/proxy.ts

diff --git a/app/api/[provider]/[...path]/route.ts b/app/api/[provider]/[...path]/route.ts
index 06e3e51603c..24aa5ec040f 100644
--- a/app/api/[provider]/[...path]/route.ts
+++ b/app/api/[provider]/[...path]/route.ts
@@ -10,6 +10,8 @@ import { handle as alibabaHandler } from "../../alibaba";
 import { handle as moonshotHandler } from "../../moonshot";
 import { handle as stabilityHandler } from "../../stability";
 import { handle as iflytekHandler } from "../../iflytek";
+import { handle as proxyHandler } from "../../proxy";
+
 async function handle(
   req: NextRequest,
   { params }: { params: { provider: string; path: string[] } },
@@ -36,8 +38,10 @@ async function handle(
       return stabilityHandler(req, { params });
     case ApiPath.Iflytek:
       return iflytekHandler(req, { params });
-    default:
+    case ApiPath.OpenAI:
       return openaiHandler(req, { params });
+    default:
+      return proxyHandler(req, { params });
   }
 }
 
diff --git a/app/api/proxy.ts b/app/api/proxy.ts
new file mode 100644
index 00000000000..731003aa1ea
--- /dev/null
+++ b/app/api/proxy.ts
@@ -0,0 +1,75 @@
+import { NextRequest, NextResponse } from "next/server";
+
+export async function handle(
+  req: NextRequest,
+  { params }: { params: { path: string[] } },
+) {
+  console.log("[Proxy Route] params ", params);
+
+  if (req.method === "OPTIONS") {
+    return NextResponse.json({ body: "OK" }, { status: 200 });
+  }
+
+  // remove path params from searchParams
+  req.nextUrl.searchParams.delete("path");
+  req.nextUrl.searchParams.delete("provider");
+
+  const subpath = params.path.join("/");
+  const fetchUrl = `${req.headers.get(
+    "x-base-url",
+  )}/${subpath}?${req.nextUrl.searchParams.toString()}`;
+  const skipHeaders = ["connection", "host", "origin", "referer", "cookie"];
+  const headers = new Headers(
+    Array.from(req.headers.entries()).filter((item) => {
+      if (
+        item[0].indexOf("x-") > -1 ||
+        item[0].indexOf("sec-") > -1 ||
+        skipHeaders.includes(item[0])
+      ) {
+        return false;
+      }
+      return true;
+    }),
+  );
+  const controller = new AbortController();
+  const fetchOptions: RequestInit = {
+    headers,
+    method: req.method,
+    body: req.body,
+    // to fix #2485: https://stackoverflow.com/questions/55920957/cloudflare-worker-typeerror-one-time-use-body
+    redirect: "manual",
+    // @ts-ignore
+    duplex: "half",
+    signal: controller.signal,
+  };
+
+  const timeoutId = setTimeout(
+    () => {
+      controller.abort();
+    },
+    10 * 60 * 1000,
+  );
+
+  try {
+    const res = await fetch(fetchUrl, fetchOptions);
+    // to prevent browser prompt for credentials
+    const newHeaders = new Headers(res.headers);
+    newHeaders.delete("www-authenticate");
+    // to disable nginx buffering
+    newHeaders.set("X-Accel-Buffering", "no");
+
+    // The latest version of the OpenAI API forced the content-encoding to be "br" in json response
+    // So if the streaming is disabled, we need to remove the content-encoding header
+    // Because Vercel uses gzip to compress the response, if we don't remove the content-encoding header
+    // The browser will try to decode the response with brotli and fail
+    newHeaders.delete("content-encoding");
+
+    return new Response(res.body, {
+      status: res.status,
+      statusText: res.statusText,
+      headers: newHeaders,
+    });
+  } finally {
+    clearTimeout(timeoutId);
+  }
+}
diff --git a/app/components/plugin.tsx b/app/components/plugin.tsx
index 35cda90894b..5aa66ce9467 100644
--- a/app/components/plugin.tsx
+++ b/app/components/plugin.tsx
@@ -14,10 +14,12 @@ import CloseIcon from "../icons/close.svg";
 import DeleteIcon from "../icons/delete.svg";
 import EyeIcon from "../icons/eye.svg";
 import CopyIcon from "../icons/copy.svg";
+import ConfirmIcon from "../icons/confirm.svg";
 
 import { Plugin, usePluginStore, FunctionToolService } from "../store/plugin";
 import {
   Input,
+  PasswordInput,
   List,
   ListItem,
   Modal,
@@ -191,55 +193,102 @@ export function PluginPage() {
             onClose={closePluginModal}
             actions={[
               <IconButton
-                icon={<DownloadIcon />}
-                text={Locale.Plugin.EditModal.Download}
+                icon={<ConfirmIcon />}
+                text={Locale.UI.Confirm}
                 key="export"
                 bordered
-                onClick={() =>
-                  downloadAs(
-                    JSON.stringify(editingPlugin),
-                    `${editingPlugin.title}@${editingPlugin.version}.json`,
-                  )
-                }
+                onClick={() => setEditingPluginId("")}
               />,
             ]}
           >
-            <div className={styles["mask-page"]}>
-              <div className={pluginStyles["plugin-title"]}>
-                {Locale.Plugin.EditModal.Content}
-              </div>
-              <div
-                className={`markdown-body ${pluginStyles["plugin-content"]}`}
-                dir="auto"
+            <List>
+              <ListItem title={Locale.Plugin.EditModal.Auth}>
+                <select
+                  value={editingPlugin?.authType}
+                  onChange={(e) => {
+                    pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
+                      plugin.authType = e.target.value;
+                    });
+                  }}
+                >
+                  <option value="">{Locale.Plugin.Auth.None}</option>
+                  <option value="bearer">{Locale.Plugin.Auth.Bearer}</option>
+                  <option value="basic">{Locale.Plugin.Auth.Basic}</option>
+                  <option value="custom">{Locale.Plugin.Auth.Custom}</option>
+                </select>
+              </ListItem>
+              {editingPlugin.authType == "custom" && (
+                <ListItem title={Locale.Plugin.Auth.CustomHeader}>
+                  <input
+                    type="text"
+                    value={editingPlugin?.authHeader}
+                    onChange={(e) => {
+                      pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
+                        plugin.authHeader = e.target.value;
+                      });
+                    }}
+                  ></input>
+                </ListItem>
+              )}
+              {["bearer", "basic", "custom"].includes(
+                editingPlugin.authType as string,
+              ) && (
+                <ListItem title={Locale.Plugin.Auth.Token}>
+                  <PasswordInput
+                    type="text"
+                    value={editingPlugin?.authToken}
+                    onChange={(e) => {
+                      pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
+                        plugin.authToken = e.currentTarget.value;
+                      });
+                    }}
+                  ></PasswordInput>
+                </ListItem>
+              )}
+              <ListItem
+                title={Locale.Plugin.Auth.Proxy}
+                subTitle={Locale.Plugin.Auth.ProxyDescription}
               >
-                <pre>
-                  <code
-                    contentEditable={true}
-                    dangerouslySetInnerHTML={{ __html: editingPlugin.content }}
-                    onBlur={onChangePlugin}
-                  ></code>
-                </pre>
-              </div>
-              <div className={pluginStyles["plugin-title"]}>
-                {Locale.Plugin.EditModal.Method}
-              </div>
-              <div className={styles["mask-page-body"]} style={{ padding: 0 }}>
-                {editingPluginTool?.tools.map((tool, index) => (
-                  <div className={styles["mask-item"]} key={index}>
-                    <div className={styles["mask-header"]}>
-                      <div className={styles["mask-title"]}>
-                        <div className={styles["mask-name"]}>
-                          {tool?.function?.name}
-                        </div>
-                        <div className={styles["mask-info"] + " one-line"}>
-                          {tool?.function?.description}
-                        </div>
-                      </div>
-                    </div>
+                <input
+                  type="checkbox"
+                  checked={editingPlugin?.usingProxy}
+                  style={{ minWidth: 16 }}
+                  onChange={(e) => {
+                    pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
+                      plugin.usingProxy = e.currentTarget.checked;
+                    });
+                  }}
+                ></input>
+              </ListItem>
+            </List>
+            <List>
+              <ListItem
+                title={Locale.Plugin.EditModal.Content}
+                subTitle={
+                  <div
+                    className={`markdown-body ${pluginStyles["plugin-content"]}`}
+                    dir="auto"
+                  >
+                    <pre>
+                      <code
+                        contentEditable={true}
+                        dangerouslySetInnerHTML={{
+                          __html: editingPlugin.content,
+                        }}
+                        onBlur={onChangePlugin}
+                      ></code>
+                    </pre>
                   </div>
-                ))}
-              </div>
-            </div>
+                }
+              ></ListItem>
+              {editingPluginTool?.tools.map((tool, index) => (
+                <ListItem
+                  key={index}
+                  title={tool?.function?.name}
+                  subTitle={tool?.function?.description}
+                />
+              ))}
+            </List>
           </Modal>
         </div>
       )}
diff --git a/app/components/ui-lib.tsx b/app/components/ui-lib.tsx
index fd78f9c4765..828c9a27d96 100644
--- a/app/components/ui-lib.tsx
+++ b/app/components/ui-lib.tsx
@@ -51,7 +51,7 @@ export function Card(props: { children: JSX.Element[]; className?: string }) {
 
 export function ListItem(props: {
   title: string;
-  subTitle?: string;
+  subTitle?: string | JSX.Element;
   children?: JSX.Element | JSX.Element[];
   icon?: JSX.Element;
   className?: string;
diff --git a/app/locales/cn.ts b/app/locales/cn.ts
index f0ff705c180..af600564e17 100644
--- a/app/locales/cn.ts
+++ b/app/locales/cn.ts
@@ -546,9 +546,20 @@ const cn = {
       Delete: "删除",
       DeleteConfirm: "确认删除?",
     },
+    Auth: {
+      None: "不需要授权",
+      Basic: "Basic",
+      Bearer: "Bearer",
+      Custom: "自定义",
+      CustomHeader: "自定义头",
+      Token: "Token",
+      Proxy: "使用代理",
+      ProxyDescription: "使用代理解决 CORS 错误",
+    },
     EditModal: {
       Title: (readonly: boolean) => `编辑插件 ${readonly ? "(只读)" : ""}`,
       Download: "下载",
+      Auth: "授权方式",
       Content: "OpenAPI Schema",
       Method: "方法",
       Error: "格式错误",
diff --git a/app/locales/en.ts b/app/locales/en.ts
index 15db8190a81..59a2840066f 100644
--- a/app/locales/en.ts
+++ b/app/locales/en.ts
@@ -554,10 +554,21 @@ const en: LocaleType = {
       Delete: "Delete",
       DeleteConfirm: "Confirm to delete?",
     },
+    Auth: {
+      None: "None",
+      Basic: "Basic",
+      Bearer: "Bearer",
+      Custom: "Custom",
+      CustomHeader: "Custom Header",
+      Token: "Token",
+      Proxy: "Using Proxy",
+      ProxyDescription: "Using proxies to solve CORS error",
+    },
     EditModal: {
       Title: (readonly: boolean) =>
         `Edit Plugin ${readonly ? "(readonly)" : ""}`,
       Download: "Download",
+      Auth: "Authentication Type",
       Content: "OpenAPI Schema",
       Method: "Method",
       Error: "OpenAPI Schema Error",
diff --git a/app/store/plugin.ts b/app/store/plugin.ts
index 031a2aaf55b..d7b0de2551d 100644
--- a/app/store/plugin.ts
+++ b/app/store/plugin.ts
@@ -12,6 +12,10 @@ export type Plugin = {
   version: string;
   content: string;
   builtin: boolean;
+  authType?: string;
+  authHeader?: string;
+  authToken?: string;
+  usingProxy?: boolean;
 };
 
 export type FunctionToolItem = {
@@ -34,10 +38,30 @@ export const FunctionToolService = {
   tools: {} as Record<string, FunctionToolServiceItem>,
   add(plugin: Plugin, replace = false) {
     if (!replace && this.tools[plugin.id]) return this.tools[plugin.id];
+    const headerName = (
+      plugin?.authType == "custom" ? plugin?.authHeader : "Authorization"
+    ) as string;
+    const tokenValue =
+      plugin?.authType == "basic"
+        ? `Basic ${plugin?.authToken}`
+        : plugin?.authType == "bearer"
+        ? ` Bearer ${plugin?.authToken}`
+        : plugin?.authToken;
+    const definition = yaml.load(plugin.content) as any;
+    const serverURL = definition.servers?.[0]?.url;
+    const baseURL = !!plugin?.usingProxy ? "/api/proxy" : serverURL;
     const api = new OpenAPIClientAxios({
       definition: yaml.load(plugin.content) as any,
+      axiosConfigDefaults: {
+        baseURL,
+        headers: {
+          // 'Cache-Control': 'no-cache',
+          // 'Content-Type': 'application/json',  // TODO
+          [headerName]: tokenValue,
+          "X-Base-URL": !!plugin?.usingProxy ? serverURL : undefined,
+        },
+      },
     });
-    console.log("add", plugin, api);
     try {
       api.initSync();
     } catch (e) {}
@@ -79,14 +103,29 @@ export const FunctionToolService = {
           type: "function",
           function: {
             name: o.operationId,
-            description: o.description,
+            description: o.description || o.summary,
             parameters: parameters,
           },
         } as FunctionToolItem;
       }),
       funcs: operations.reduce((s, o) => {
         // @ts-ignore
-        s[o.operationId] = api.client[o.operationId];
+        s[o.operationId] = function (args) {
+          const argument = [];
+          if (o.parameters instanceof Array) {
+            o.parameters.forEach((p) => {
+              // @ts-ignore
+              argument.push(args[p?.name]);
+              // @ts-ignore
+              delete args[p?.name];
+            });
+          } else {
+            argument.push(null);
+          }
+          argument.push(args);
+          // @ts-ignore
+          return api.client[o.operationId].apply(null, argument);
+        };
         return s;
       }, {}),
     });
@@ -136,6 +175,7 @@ export const usePluginStore = createPersistStore(
       const updatePlugin = { ...plugin };
       updater(updatePlugin);
       plugins[id] = updatePlugin;
+      FunctionToolService.add(updatePlugin, true);
       set(() => ({ plugins }));
       get().markUpdate();
     },
diff --git a/next.config.mjs b/next.config.mjs
index 6b1dd0c35c8..27c60dd2997 100644
--- a/next.config.mjs
+++ b/next.config.mjs
@@ -86,10 +86,6 @@ if (mode !== "export") {
         source: "/api/proxy/anthropic/:path*",
         destination: "https://api.anthropic.com/:path*",
       },
-      {
-        source: "/api/proxy/gapier/:path*",
-        destination: "https://a.gapier.com/:path*",
-      },
       {
         source: "/google-fonts/:path*",
         destination: "https://fonts.googleapis.com/:path*",

From 877668b6293a4f5a50b5da9d1c0677cf55fce7c0 Mon Sep 17 00:00:00 2001
From: lloydzhou <lloydzhou@qq.com>
Date: Mon, 2 Sep 2024 18:29:00 +0800
Subject: [PATCH 18/38] hotfix

---
 app/store/plugin.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/app/store/plugin.ts b/app/store/plugin.ts
index d7b0de2551d..cad733a7e9c 100644
--- a/app/store/plugin.ts
+++ b/app/store/plugin.ts
@@ -48,7 +48,7 @@ export const FunctionToolService = {
         ? ` Bearer ${plugin?.authToken}`
         : plugin?.authToken;
     const definition = yaml.load(plugin.content) as any;
-    const serverURL = definition.servers?.[0]?.url;
+    const serverURL = definition?.servers?.[0]?.url;
     const baseURL = !!plugin?.usingProxy ? "/api/proxy" : serverURL;
     const api = new OpenAPIClientAxios({
       definition: yaml.load(plugin.content) as any,

From 801b62543a909e2cf6c71d635e49f839012723fa Mon Sep 17 00:00:00 2001
From: lloydzhou <lloydzhou@qq.com>
Date: Mon, 2 Sep 2024 21:45:47 +0800
Subject: [PATCH 19/38] claude support function call

---
 app/api/auth.ts                   |   1 +
 app/client/platforms/anthropic.ts | 228 ++++++++++++++++--------------
 app/client/platforms/openai.ts    |   2 +-
 app/components/chat.tsx           |  15 +-
 app/utils.ts                      |  11 ++
 app/utils/chat.ts                 |   2 +-
 6 files changed, 146 insertions(+), 113 deletions(-)

diff --git a/app/api/auth.ts b/app/api/auth.ts
index 95965ceec2d..c8fa6787d9a 100644
--- a/app/api/auth.ts
+++ b/app/api/auth.ts
@@ -38,6 +38,7 @@ export function auth(req: NextRequest, modelProvider: ModelProvider) {
   console.log("[Auth] hashed access code:", hashedCode);
   console.log("[User IP] ", getIP(req));
   console.log("[Time] ", new Date().toLocaleString());
+  console.log("[ModelProvider] ", modelProvider);
 
   if (serverConfig.needCode && !serverConfig.codes.has(hashedCode) && !apiKey) {
     return {
diff --git a/app/client/platforms/anthropic.ts b/app/client/platforms/anthropic.ts
index b079ba1ada2..91434ffcc06 100644
--- a/app/client/platforms/anthropic.ts
+++ b/app/client/platforms/anthropic.ts
@@ -1,6 +1,12 @@
 import { ACCESS_CODE_PREFIX, Anthropic, ApiPath } from "@/app/constant";
 import { ChatOptions, getHeaders, LLMApi, MultimodalContent } from "../api";
-import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
+import {
+  useAccessStore,
+  useAppConfig,
+  useChatStore,
+  usePluginStore,
+  ChatMessageTool,
+} from "@/app/store";
 import { getClientConfig } from "@/app/config/client";
 import { DEFAULT_API_HOST } from "@/app/constant";
 import {
@@ -11,8 +17,9 @@ import {
 import Locale from "../../locales";
 import { prettyObject } from "@/app/utils/format";
 import { getMessageTextContent, isVisionModel } from "@/app/utils";
-import { preProcessImageContent } from "@/app/utils/chat";
+import { preProcessImageContent, stream } from "@/app/utils/chat";
 import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
+import { RequestPayload } from "./openai";
 
 export type MultiBlockContent = {
   type: "image" | "text";
@@ -191,112 +198,123 @@ export class ClaudeApi implements LLMApi {
     const controller = new AbortController();
     options.onController?.(controller);
 
-    const payload = {
-      method: "POST",
-      body: JSON.stringify(requestBody),
-      signal: controller.signal,
-      headers: {
-        ...getHeaders(), // get common headers
-        "anthropic-version": accessStore.anthropicApiVersion,
-        // do not send `anthropicApiKey` in browser!!!
-        // Authorization: getAuthKey(accessStore.anthropicApiKey),
-      },
-    };
-
     if (shouldStream) {
-      try {
-        const context = {
-          text: "",
-          finished: false,
-        };
-
-        const finish = () => {
-          if (!context.finished) {
-            options.onFinish(context.text);
-            context.finished = true;
-          }
-        };
-
-        controller.signal.onabort = finish;
-        fetchEventSource(path, {
-          ...payload,
-          async onopen(res) {
-            const contentType = res.headers.get("content-type");
-            console.log("response content type: ", contentType);
-
-            if (contentType?.startsWith("text/plain")) {
-              context.text = await res.clone().text();
-              return finish();
-            }
-
-            if (
-              !res.ok ||
-              !res.headers
-                .get("content-type")
-                ?.startsWith(EventStreamContentType) ||
-              res.status !== 200
-            ) {
-              const responseTexts = [context.text];
-              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);
-              }
-
-              context.text = responseTexts.join("\n\n");
-
-              return finish();
-            }
-          },
-          onmessage(msg) {
-            let chunkJson:
-              | undefined
-              | {
-                  type: "content_block_delta" | "content_block_stop";
-                  delta?: {
-                    type: "text_delta";
-                    text: string;
-                  };
-                  index: number;
+      let index = -1;
+      const [tools, funcs] = usePluginStore
+        .getState()
+        .getAsTools(
+          useChatStore.getState().currentSession().mask?.plugin as string[],
+        );
+      console.log("getAsTools", tools, funcs);
+      return stream(
+        path,
+        requestBody,
+        {
+          ...getHeaders(),
+          "anthropic-version": accessStore.anthropicApiVersion,
+        },
+        // @ts-ignore
+        tools.map((tool) => ({
+          name: tool?.function?.name,
+          description: tool?.function?.description,
+          input_schema: tool?.function?.parameters,
+        })),
+        funcs,
+        controller,
+        // parseSSE
+        (text: string, runTools: ChatMessageTool[]) => {
+          // console.log("parseSSE", text, runTools);
+          let chunkJson:
+            | undefined
+            | {
+                type: "content_block_delta" | "content_block_stop";
+                content_block?: {
+                  type: "tool_use";
+                  id: string;
+                  name: string;
                 };
-            try {
-              chunkJson = JSON.parse(msg.data);
-            } catch (e) {
-              console.error("[Response] parse error", msg.data);
-            }
-
-            if (!chunkJson || chunkJson.type === "content_block_stop") {
-              return finish();
-            }
-
-            const { delta } = chunkJson;
-            if (delta?.text) {
-              context.text += delta.text;
-              options.onUpdate?.(context.text, delta.text);
-            }
-          },
-          onclose() {
-            finish();
-          },
-          onerror(e) {
-            options.onError?.(e);
-            throw e;
-          },
-          openWhenHidden: true,
-        });
-      } catch (e) {
-        console.error("failed to chat", e);
-        options.onError?.(e as Error);
-      }
+                delta?: {
+                  type: "text_delta" | "input_json_delta";
+                  text?: string;
+                  partial_json?: string;
+                };
+                index: number;
+              };
+          chunkJson = JSON.parse(text);
+
+          if (chunkJson?.content_block?.type == "tool_use") {
+            index += 1;
+            const id = chunkJson?.content_block.id;
+            const name = chunkJson?.content_block.name;
+            runTools.push({
+              id,
+              type: "function",
+              function: {
+                name,
+                arguments: "",
+              },
+            });
+          }
+          if (
+            chunkJson?.delta?.type == "input_json_delta" &&
+            chunkJson?.delta?.partial_json
+          ) {
+            // @ts-ignore
+            runTools[index]["function"]["arguments"] +=
+              chunkJson?.delta?.partial_json;
+          }
+          return chunkJson?.delta?.text;
+        },
+        // processToolMessage, include tool_calls message and tool call results
+        (
+          requestPayload: RequestPayload,
+          toolCallMessage: any,
+          toolCallResult: any[],
+        ) => {
+          // @ts-ignore
+          requestPayload?.messages?.splice(
+            // @ts-ignore
+            requestPayload?.messages?.length,
+            0,
+            {
+              role: "assistant",
+              content: toolCallMessage.tool_calls.map(
+                (tool: ChatMessageTool) => ({
+                  type: "tool_use",
+                  id: tool.id,
+                  name: tool?.function?.name,
+                  input: JSON.parse(tool?.function?.arguments as string),
+                }),
+              ),
+            },
+            // @ts-ignore
+            ...toolCallResult.map((result) => ({
+              role: "user",
+              content: [
+                {
+                  type: "tool_result",
+                  tool_use_id: result.tool_call_id,
+                  content: result.content,
+                },
+              ],
+            })),
+          );
+        },
+        options,
+      );
     } else {
+      const payload = {
+        method: "POST",
+        body: JSON.stringify(requestBody),
+        signal: controller.signal,
+        headers: {
+          ...getHeaders(), // get common headers
+          "anthropic-version": accessStore.anthropicApiVersion,
+          // do not send `anthropicApiKey` in browser!!!
+          // Authorization: getAuthKey(accessStore.anthropicApiKey),
+        },
+      };
+
       try {
         controller.signal.onabort = () => options.onFinish("");
 
diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts
index 4c5831fe3e9..b3b306d1d11 100644
--- a/app/client/platforms/openai.ts
+++ b/app/client/platforms/openai.ts
@@ -246,7 +246,7 @@ export class ChatGPTApi implements LLMApi {
           .getAsTools(
             useChatStore.getState().currentSession().mask?.plugin as string[],
           );
-        console.log("getAsTools", tools, funcs);
+        // console.log("getAsTools", tools, funcs);
         stream(
           chatPath,
           requestPayload,
diff --git a/app/components/chat.tsx b/app/components/chat.tsx
index 7bac62bc4c3..7d180f0b739 100644
--- a/app/components/chat.tsx
+++ b/app/components/chat.tsx
@@ -66,6 +66,7 @@ import {
   getMessageImages,
   isVisionModel,
   isDalle3,
+  showPlugins,
 } from "../utils";
 
 import { uploadImage as uploadImageRemote } from "@/app/utils/chat";
@@ -741,12 +742,14 @@ export function ChatActions(props: {
               value: ArtifactsPlugin.Artifacts as string,
             },
           ].concat(
-            pluginStore.getAll().map((item) => ({
-              // @ts-ignore
-              title: `${item?.title}@${item?.version}`,
-              // @ts-ignore
-              value: item?.id,
-            })),
+            showPlugins(currentProviderName, currentModel)
+              ? pluginStore.getAll().map((item) => ({
+                  // @ts-ignore
+                  title: `${item?.title}@${item?.version}`,
+                  // @ts-ignore
+                  value: item?.id,
+                }))
+              : [],
           )}
           onClose={() => setShowPluginSelector(false)}
           onSelection={(s) => {
diff --git a/app/utils.ts b/app/utils.ts
index 2a292290755..b9884c70644 100644
--- a/app/utils.ts
+++ b/app/utils.ts
@@ -2,6 +2,7 @@ import { useEffect, useState } from "react";
 import { showToast } from "./components/ui-lib";
 import Locale from "./locales";
 import { RequestMessage } from "./client/api";
+import { ServiceProvider } from "./constant";
 
 export function trimTopic(topic: string) {
   // Fix an issue where double quotes still show in the Indonesian language
@@ -270,3 +271,13 @@ export function isVisionModel(model: string) {
 export function isDalle3(model: string) {
   return "dall-e-3" === model;
 }
+
+export function showPlugins(provider: ServiceProvider, model: string) {
+  if (provider == ServiceProvider.OpenAI || provider == ServiceProvider.Azure) {
+    return true;
+  }
+  if (provider == ServiceProvider.Anthropic && !model.includes("claude-2")) {
+    return true;
+  }
+  return false;
+}
diff --git a/app/utils/chat.ts b/app/utils/chat.ts
index d8ab5770cc2..49e5060d432 100644
--- a/app/utils/chat.ts
+++ b/app/utils/chat.ts
@@ -334,7 +334,7 @@ export function stream(
             remainText += chunk;
           }
         } catch (e) {
-          console.error("[Request] parse error", text, msg);
+          console.error("[Request] parse error", text, msg, e);
         }
       },
       onclose() {

From 078305f5ac15f8af33ed9f8dd4ed0843543793a0 Mon Sep 17 00:00:00 2001
From: lloydzhou <lloydzhou@qq.com>
Date: Mon, 2 Sep 2024 21:55:17 +0800
Subject: [PATCH 20/38] kimi support function call

---
 app/client/platforms/moonshot.ts | 172 ++++++++++++-------------------
 app/utils.ts                     |   6 +-
 2 files changed, 70 insertions(+), 108 deletions(-)

diff --git a/app/client/platforms/moonshot.ts b/app/client/platforms/moonshot.ts
index 7d257ccb2e6..311f2726a11 100644
--- a/app/client/platforms/moonshot.ts
+++ b/app/client/platforms/moonshot.ts
@@ -8,9 +8,15 @@ import {
   REQUEST_TIMEOUT_MS,
   ServiceProvider,
 } from "@/app/constant";
-import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
+import {
+  useAccessStore,
+  useAppConfig,
+  useChatStore,
+  ChatMessageTool,
+  usePluginStore,
+} from "@/app/store";
 import { collectModelsWithDefaultModel } from "@/app/utils/model";
-import { preProcessImageContent } from "@/app/utils/chat";
+import { preProcessImageContent, stream } from "@/app/utils/chat";
 import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
 
 import {
@@ -116,115 +122,67 @@ export class MoonshotApi implements LLMApi {
       );
 
       if (shouldStream) {
-        let responseText = "";
-        let remainText = "";
-        let finished = false;
-
-        // animate response to make it looks smooth
-        function animateResponseText() {
-          if (finished || controller.signal.aborted) {
-            responseText += remainText;
-            console.log("[Response Animation] finished");
-            if (responseText?.length === 0) {
-              options.onError?.(new Error("empty response from server"));
-            }
-            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);
-        }
-
-        // start animaion
-        animateResponseText();
-
-        const finish = () => {
-          if (!finished) {
-            finished = true;
-            options.onFinish(responseText + remainText);
-          }
-        };
-
-        controller.signal.onabort = finish;
-
-        fetchEventSource(chatPath, {
-          ...chatPayload,
-          async onopen(res) {
-            clearTimeout(requestTimeoutId);
-            const contentType = res.headers.get("content-type");
-            console.log(
-              "[OpenAI] request response content type: ",
-              contentType,
-            );
-
-            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);
+        const [tools, funcs] = usePluginStore
+          .getState()
+          .getAsTools(
+            useChatStore.getState().currentSession().mask?.plugin as string[],
+          );
+        console.log("getAsTools", tools, funcs);
+        return stream(
+          chatPath,
+          requestPayload,
+          getHeaders(),
+          tools as any,
+          funcs,
+          controller,
+          // parseSSE
+          (text: string, runTools: ChatMessageTool[]) => {
+            // console.log("parseSSE", text, runTools);
+            const json = JSON.parse(text);
+            const choices = json.choices as Array<{
+              delta: {
+                content: string;
+                tool_calls: ChatMessageTool[];
+              };
+            }>;
+            const tool_calls = choices[0]?.delta?.tool_calls;
+            if (tool_calls?.length > 0) {
+              const index = tool_calls[0]?.index;
+              const id = tool_calls[0]?.id;
+              const args = tool_calls[0]?.function?.arguments;
+              if (id) {
+                runTools.push({
+                  id,
+                  type: tool_calls[0]?.type,
+                  function: {
+                    name: tool_calls[0]?.function?.name as string,
+                    arguments: args,
+                  },
+                });
+              } else {
+                // @ts-ignore
+                runTools[index]["function"]["arguments"] += args;
               }
-
-              responseText = responseTexts.join("\n\n");
-
-              return finish();
-            }
-          },
-          onmessage(msg) {
-            if (msg.data === "[DONE]" || finished) {
-              return finish();
-            }
-            const text = msg.data;
-            try {
-              const json = JSON.parse(text);
-              const choices = json.choices as Array<{
-                delta: { content: string };
-              }>;
-              const delta = choices[0]?.delta?.content;
-              const textmoderation = json?.prompt_filter_results;
-
-              if (delta) {
-                remainText += delta;
-              }
-            } catch (e) {
-              console.error("[Request] parse error", text, msg);
             }
+            return choices[0]?.delta?.content;
           },
-          onclose() {
-            finish();
-          },
-          onerror(e) {
-            options.onError?.(e);
-            throw e;
+          // processToolMessage, include tool_calls message and tool call results
+          (
+            requestPayload: RequestPayload,
+            toolCallMessage: any,
+            toolCallResult: any[],
+          ) => {
+            // @ts-ignore
+            requestPayload?.messages?.splice(
+              // @ts-ignore
+              requestPayload?.messages?.length,
+              0,
+              toolCallMessage,
+              ...toolCallResult,
+            );
           },
-          openWhenHidden: true,
-        });
+          options,
+        );
       } else {
         const res = await fetch(chatPath, chatPayload);
         clearTimeout(requestTimeoutId);
diff --git a/app/utils.ts b/app/utils.ts
index b9884c70644..0ac5867fab0 100644
--- a/app/utils.ts
+++ b/app/utils.ts
@@ -273,7 +273,11 @@ export function isDalle3(model: string) {
 }
 
 export function showPlugins(provider: ServiceProvider, model: string) {
-  if (provider == ServiceProvider.OpenAI || provider == ServiceProvider.Azure) {
+  if (
+    provider == ServiceProvider.OpenAI ||
+    provider == ServiceProvider.Azure ||
+    provider == ServiceProvider.Moonshot
+  ) {
     return true;
   }
   if (provider == ServiceProvider.Anthropic && !model.includes("claude-2")) {

From 6435e7a30e5b0d7703ba6ccec0df9c6e767f3e8b Mon Sep 17 00:00:00 2001
From: lloydzhou <lloydzhou@qq.com>
Date: Mon, 2 Sep 2024 23:42:56 +0800
Subject: [PATCH 21/38] update readme

---
 README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/README.md b/README.md
index da1adb6a5f0..24a05667387 100644
--- a/README.md
+++ b/README.md
@@ -89,7 +89,7 @@ For enterprise inquiries, please contact: **business@nextchat.dev**
 - [x] Desktop App with tauri
 - [x] Self-host Model: Fully compatible with [RWKV-Runner](https://github.com/josStorer/RWKV-Runner), as well as server deployment of [LocalAI](https://github.com/go-skynet/LocalAI): llama/gpt4all/rwkv/vicuna/koala/gpt4all-j/cerebras/falcon/dolly etc.
 - [x] Artifacts: Easily preview, copy and share generated content/webpages through a separate window [#5092](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/pull/5092)
-- [x] Plugins: support artifacts, network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165)
+- [x] Plugins: support artifacts, network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) [#5353](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5353)
   - [x] artifacts
   - [ ] network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165)
 - [ ] local knowledge base

From 3ec67f9f477976c880dba238d22e7482405af4b4 Mon Sep 17 00:00:00 2001
From: lloydzhou <lloydzhou@qq.com>
Date: Tue, 3 Sep 2024 00:45:11 +0800
Subject: [PATCH 22/38] add load from url

---
 app/components/plugin.tsx | 50 +++++++++++++++++++++++++++++++++++++--
 app/components/ui-lib.tsx |  2 +-
 app/locales/cn.ts         |  1 +
 app/locales/en.ts         |  1 +
 4 files changed, 51 insertions(+), 3 deletions(-)

diff --git a/app/components/plugin.tsx b/app/components/plugin.tsx
index 5aa66ce9467..fcf671bff69 100644
--- a/app/components/plugin.tsx
+++ b/app/components/plugin.tsx
@@ -15,6 +15,7 @@ import DeleteIcon from "../icons/delete.svg";
 import EyeIcon from "../icons/eye.svg";
 import CopyIcon from "../icons/copy.svg";
 import ConfirmIcon from "../icons/confirm.svg";
+import ReloadIcon from "../icons/reload.svg";
 
 import { Plugin, usePluginStore, FunctionToolService } from "../store/plugin";
 import {
@@ -31,7 +32,7 @@ import {
 import { downloadAs } from "../utils";
 import Locale from "../locales";
 import { useNavigate } from "react-router-dom";
-import { useEffect, useState } from "react";
+import { useEffect, useState, useCallback } from "react";
 import { Path } from "../constant";
 import { nanoid } from "nanoid";
 
@@ -90,6 +91,37 @@ export function PluginPage() {
     }
   }, 100).bind(null, editingPlugin);
 
+  const [loadUrl, setLoadUrl] = useState<string>("");
+  const loadFromUrl = (loadUrl: string) =>
+    fetch(loadUrl)
+      .catch((e) => {
+        const p = new URL(loadUrl);
+        return fetch(`/api/proxy/${p.pathname}?${p.search}`, {
+          headers: {
+            "X-Base-URL": p.origin,
+          },
+        });
+      })
+      .then((res) => res.text())
+      .then((content) => {
+        try {
+          return JSON.stringify(JSON.parse(content), null, "  ");
+        } catch (e) {
+          return content;
+        }
+      })
+      .then((content) => {
+        pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
+          plugin.content = content;
+          const tool = FunctionToolService.add(plugin, true);
+          plugin.title = tool.api.definition.info.title;
+          plugin.version = tool.api.definition.info.version;
+        });
+      })
+      .catch((e) => {
+        showToast(Locale.Plugin.EditModal.Error);
+      });
+
   return (
     <ErrorBoundary>
       <div className={styles["mask-page"]}>
@@ -262,8 +294,22 @@ export function PluginPage() {
               </ListItem>
             </List>
             <List>
+              <ListItem title={Locale.Plugin.EditModal.Content}>
+                <div style={{ display: "flex", justifyContent: "flex-end" }}>
+                  <input
+                    type="text"
+                    style={{ minWidth: 200, marginRight: 20 }}
+                    onInput={(e) => setLoadUrl(e.currentTarget.value)}
+                  ></input>
+                  <IconButton
+                    icon={<ReloadIcon />}
+                    text={Locale.Plugin.EditModal.Load}
+                    bordered
+                    onClick={() => loadFromUrl(loadUrl)}
+                  />
+                </div>
+              </ListItem>
               <ListItem
-                title={Locale.Plugin.EditModal.Content}
                 subTitle={
                   <div
                     className={`markdown-body ${pluginStyles["plugin-content"]}`}
diff --git a/app/components/ui-lib.tsx b/app/components/ui-lib.tsx
index 828c9a27d96..85693b98616 100644
--- a/app/components/ui-lib.tsx
+++ b/app/components/ui-lib.tsx
@@ -50,7 +50,7 @@ export function Card(props: { children: JSX.Element[]; className?: string }) {
 }
 
 export function ListItem(props: {
-  title: string;
+  title?: string;
   subTitle?: string | JSX.Element;
   children?: JSX.Element | JSX.Element[];
   icon?: JSX.Element;
diff --git a/app/locales/cn.ts b/app/locales/cn.ts
index af600564e17..4e405eed717 100644
--- a/app/locales/cn.ts
+++ b/app/locales/cn.ts
@@ -561,6 +561,7 @@ const cn = {
       Download: "下载",
       Auth: "授权方式",
       Content: "OpenAPI Schema",
+      Load: "从网页加载",
       Method: "方法",
       Error: "格式错误",
     },
diff --git a/app/locales/en.ts b/app/locales/en.ts
index 59a2840066f..80666f3b21d 100644
--- a/app/locales/en.ts
+++ b/app/locales/en.ts
@@ -570,6 +570,7 @@ const en: LocaleType = {
       Download: "Download",
       Auth: "Authentication Type",
       Content: "OpenAPI Schema",
+      Load: "Load From URL",
       Method: "Method",
       Error: "OpenAPI Schema Error",
     },

From 2b317f60c87aca80b1a2c7b5b36c13817618d818 Mon Sep 17 00:00:00 2001
From: lloydzhou <lloydzhou@qq.com>
Date: Tue, 3 Sep 2024 12:00:55 +0800
Subject: [PATCH 23/38] add config auth location

---
 app/components/plugin.module.scss |  1 +
 app/components/plugin.tsx         | 24 +++++++++++++++++++++++
 app/locales/cn.ts                 |  6 +++++-
 app/locales/en.ts                 |  6 +++++-
 app/store/plugin.ts               | 32 +++++++++++++++++++------------
 5 files changed, 55 insertions(+), 14 deletions(-)

diff --git a/app/components/plugin.module.scss b/app/components/plugin.module.scss
index 53c63246836..a179e0a07a8 100644
--- a/app/components/plugin.module.scss
+++ b/app/components/plugin.module.scss
@@ -10,6 +10,7 @@
     max-height: 240px;
     overflow-y: auto;
     white-space: pre-wrap;
+    min-width: 300px;
   }
 }
 
diff --git a/app/components/plugin.tsx b/app/components/plugin.tsx
index fcf671bff69..35fb2abc624 100644
--- a/app/components/plugin.tsx
+++ b/app/components/plugin.tsx
@@ -249,6 +249,30 @@ export function PluginPage() {
                   <option value="custom">{Locale.Plugin.Auth.Custom}</option>
                 </select>
               </ListItem>
+              {["bearer", "basic", "custom"].includes(
+                editingPlugin.authType as string,
+              ) && (
+                <ListItem title={Locale.Plugin.Auth.Location}>
+                  <select
+                    value={editingPlugin?.authLocation}
+                    onChange={(e) => {
+                      pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
+                        plugin.authLocation = e.target.value;
+                      });
+                    }}
+                  >
+                    <option value="header">
+                      {Locale.Plugin.Auth.LocationHeader}
+                    </option>
+                    <option value="query">
+                      {Locale.Plugin.Auth.LocationQuery}
+                    </option>
+                    <option value="body">
+                      {Locale.Plugin.Auth.LocationBody}
+                    </option>
+                  </select>
+                </ListItem>
+              )}
               {editingPlugin.authType == "custom" && (
                 <ListItem title={Locale.Plugin.Auth.CustomHeader}>
                   <input
diff --git a/app/locales/cn.ts b/app/locales/cn.ts
index 4e405eed717..201f7dc5d49 100644
--- a/app/locales/cn.ts
+++ b/app/locales/cn.ts
@@ -551,10 +551,14 @@ const cn = {
       Basic: "Basic",
       Bearer: "Bearer",
       Custom: "自定义",
-      CustomHeader: "自定义头",
+      CustomHeader: "自定义参数名称",
       Token: "Token",
       Proxy: "使用代理",
       ProxyDescription: "使用代理解决 CORS 错误",
+      Location: "位置",
+      LocationHeader: "Header",
+      LocationQuery: "Query",
+      LocationBody: "Body",
     },
     EditModal: {
       Title: (readonly: boolean) => `编辑插件 ${readonly ? "(只读)" : ""}`,
diff --git a/app/locales/en.ts b/app/locales/en.ts
index 80666f3b21d..d2b27fdcdb6 100644
--- a/app/locales/en.ts
+++ b/app/locales/en.ts
@@ -559,10 +559,14 @@ const en: LocaleType = {
       Basic: "Basic",
       Bearer: "Bearer",
       Custom: "Custom",
-      CustomHeader: "Custom Header",
+      CustomHeader: "Parameter Name",
       Token: "Token",
       Proxy: "Using Proxy",
       ProxyDescription: "Using proxies to solve CORS error",
+      Location: "Location",
+      LocationHeader: "Header",
+      LocationQuery: "Query",
+      LocationBody: "Body",
     },
     EditModal: {
       Title: (readonly: boolean) =>
diff --git a/app/store/plugin.ts b/app/store/plugin.ts
index cad733a7e9c..260c33c3293 100644
--- a/app/store/plugin.ts
+++ b/app/store/plugin.ts
@@ -13,6 +13,7 @@ export type Plugin = {
   content: string;
   builtin: boolean;
   authType?: string;
+  authLocation?: string;
   authHeader?: string;
   authToken?: string;
   usingProxy?: boolean;
@@ -50,16 +51,17 @@ export const FunctionToolService = {
     const definition = yaml.load(plugin.content) as any;
     const serverURL = definition?.servers?.[0]?.url;
     const baseURL = !!plugin?.usingProxy ? "/api/proxy" : serverURL;
+    const headers: Record<string, string | undefined> = {
+      "X-Base-URL": !!plugin?.usingProxy ? serverURL : undefined,
+    };
+    if (plugin?.authLocation == "header") {
+      headers[headerName] = tokenValue;
+    }
     const api = new OpenAPIClientAxios({
       definition: yaml.load(plugin.content) as any,
       axiosConfigDefaults: {
         baseURL,
-        headers: {
-          // 'Cache-Control': 'no-cache',
-          // 'Content-Type': 'application/json',  // TODO
-          [headerName]: tokenValue,
-          "X-Base-URL": !!plugin?.usingProxy ? serverURL : undefined,
-        },
+        headers,
       },
     });
     try {
@@ -111,20 +113,26 @@ export const FunctionToolService = {
       funcs: operations.reduce((s, o) => {
         // @ts-ignore
         s[o.operationId] = function (args) {
-          const argument = [];
+          const parameters: Record<string, any> = {};
           if (o.parameters instanceof Array) {
             o.parameters.forEach((p) => {
               // @ts-ignore
-              argument.push(args[p?.name]);
+              parameters[p?.name] = args[p?.name];
               // @ts-ignore
               delete args[p?.name];
             });
-          } else {
-            argument.push(null);
           }
-          argument.push(args);
+          if (plugin?.authLocation == "query") {
+            parameters[headerName] = tokenValue;
+          } else if (plugin?.authLocation == "body") {
+            args[headerName] = tokenValue;
+          }
           // @ts-ignore
-          return api.client[o.operationId].apply(null, argument);
+          return api.client[o.operationId](
+            parameters,
+            args,
+            api.axiosConfigDefaults,
+          );
         };
         return s;
       }, {}),

From 236736deeaee40fb0241f294c9a50eef03fead90 Mon Sep 17 00:00:00 2001
From: lloydzhou <lloydzhou@qq.com>
Date: Tue, 3 Sep 2024 15:37:23 +0800
Subject: [PATCH 24/38] remove no need code

---
 app/api/auth.ts           |  1 -
 app/components/plugin.tsx | 10 +---------
 2 files changed, 1 insertion(+), 10 deletions(-)

diff --git a/app/api/auth.ts b/app/api/auth.ts
index c8fa6787d9a..95965ceec2d 100644
--- a/app/api/auth.ts
+++ b/app/api/auth.ts
@@ -38,7 +38,6 @@ export function auth(req: NextRequest, modelProvider: ModelProvider) {
   console.log("[Auth] hashed access code:", hashedCode);
   console.log("[User IP] ", getIP(req));
   console.log("[Time] ", new Date().toLocaleString());
-  console.log("[ModelProvider] ", modelProvider);
 
   if (serverConfig.needCode && !serverConfig.codes.has(hashedCode) && !apiKey) {
     return {
diff --git a/app/components/plugin.tsx b/app/components/plugin.tsx
index 35fb2abc624..5aad2c70aaa 100644
--- a/app/components/plugin.tsx
+++ b/app/components/plugin.tsx
@@ -7,34 +7,26 @@ import { ErrorBoundary } from "./error";
 import styles from "./mask.module.scss";
 import pluginStyles from "./plugin.module.scss";
 
-import DownloadIcon from "../icons/download.svg";
 import EditIcon from "../icons/edit.svg";
 import AddIcon from "../icons/add.svg";
 import CloseIcon from "../icons/close.svg";
 import DeleteIcon from "../icons/delete.svg";
 import EyeIcon from "../icons/eye.svg";
-import CopyIcon from "../icons/copy.svg";
 import ConfirmIcon from "../icons/confirm.svg";
 import ReloadIcon from "../icons/reload.svg";
 
 import { Plugin, usePluginStore, FunctionToolService } from "../store/plugin";
 import {
-  Input,
   PasswordInput,
   List,
   ListItem,
   Modal,
-  Popover,
-  Select,
   showConfirm,
   showToast,
 } from "./ui-lib";
-import { downloadAs } from "../utils";
 import Locale from "../locales";
 import { useNavigate } from "react-router-dom";
-import { useEffect, useState, useCallback } from "react";
-import { Path } from "../constant";
-import { nanoid } from "nanoid";
+import { useEffect, useState } from "react";
 
 export function PluginPage() {
   const navigate = useNavigate();

From 4fdd997108a9b754f6c5c368e2b77b4e7fb2f435 Mon Sep 17 00:00:00 2001
From: lloydzhou <lloydzhou@qq.com>
Date: Tue, 3 Sep 2024 16:23:54 +0800
Subject: [PATCH 25/38] hotfix

---
 app/store/plugin.ts | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/app/store/plugin.ts b/app/store/plugin.ts
index 260c33c3293..934a989dc5e 100644
--- a/app/store/plugin.ts
+++ b/app/store/plugin.ts
@@ -48,13 +48,14 @@ export const FunctionToolService = {
         : plugin?.authType == "bearer"
         ? ` Bearer ${plugin?.authToken}`
         : plugin?.authToken;
+    const authLocation = plugin?.authLocation || "header";
     const definition = yaml.load(plugin.content) as any;
     const serverURL = definition?.servers?.[0]?.url;
     const baseURL = !!plugin?.usingProxy ? "/api/proxy" : serverURL;
     const headers: Record<string, string | undefined> = {
       "X-Base-URL": !!plugin?.usingProxy ? serverURL : undefined,
     };
-    if (plugin?.authLocation == "header") {
+    if (authLocation == "header") {
       headers[headerName] = tokenValue;
     }
     const api = new OpenAPIClientAxios({
@@ -122,9 +123,9 @@ export const FunctionToolService = {
               delete args[p?.name];
             });
           }
-          if (plugin?.authLocation == "query") {
+          if (authLocation == "query") {
             parameters[headerName] = tokenValue;
-          } else if (plugin?.authLocation == "body") {
+          } else if (authLocation == "body") {
             args[headerName] = tokenValue;
           }
           // @ts-ignore

From d30351e7b07c3975fb3fe21cf751c244abc02269 Mon Sep 17 00:00:00 2001
From: lloydzhou <lloydzhou@qq.com>
Date: Tue, 3 Sep 2024 17:18:43 +0800
Subject: [PATCH 26/38] update readme

---
 README.md | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/README.md b/README.md
index 24a05667387..b6f974bd457 100644
--- a/README.md
+++ b/README.md
@@ -91,7 +91,7 @@ For enterprise inquiries, please contact: **business@nextchat.dev**
 - [x] Artifacts: Easily preview, copy and share generated content/webpages through a separate window [#5092](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/pull/5092)
 - [x] Plugins: support artifacts, network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) [#5353](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5353)
   - [x] artifacts
-  - [ ] network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165)
+  - [x] network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) [#5353](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5353)
 - [ ] local knowledge base
 
 ## What's New
@@ -126,9 +126,9 @@ For enterprise inquiries, please contact: **business@nextchat.dev**
 - [x] 使用 tauri 打包桌面应用
 - [x] 支持自部署的大语言模型:开箱即用 [RWKV-Runner](https://github.com/josStorer/RWKV-Runner) ,服务端部署 [LocalAI 项目](https://github.com/go-skynet/LocalAI) llama / gpt4all / rwkv / vicuna / koala / gpt4all-j / cerebras / falcon / dolly 等等,或者使用 [api-for-open-llm](https://github.com/xusenlinzy/api-for-open-llm)
 - [x] Artifacts: 通过独立窗口,轻松预览、复制和分享生成的内容/可交互网页 [#5092](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/pull/5092)
-- [x] 插件机制,支持 artifacts,联网搜索、计算器、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165)
+- [x] 插件机制,支持 artifacts,联网搜索、计算器、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) [#5353](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5353)
    - [x] artifacts
-   - [ ] 支持联网搜索、计算器、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165)
+   - [x] 支持联网搜索、计算器、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) [#5353](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5353)
  - [ ] 本地知识库
 
 ## 最新动态

From 0a5522d28cc594636957ada475b35a41a3d6eda2 Mon Sep 17 00:00:00 2001
From: lloydzhou <lloydzhou@qq.com>
Date: Tue, 3 Sep 2024 19:35:36 +0800
Subject: [PATCH 27/38] update

---
 app/client/platforms/anthropic.ts | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/app/client/platforms/anthropic.ts b/app/client/platforms/anthropic.ts
index 91434ffcc06..d6958dfc53c 100644
--- a/app/client/platforms/anthropic.ts
+++ b/app/client/platforms/anthropic.ts
@@ -271,6 +271,8 @@ export class ClaudeApi implements LLMApi {
           toolCallMessage: any,
           toolCallResult: any[],
         ) => {
+          // reset index value
+          index = -1;
           // @ts-ignore
           requestPayload?.messages?.splice(
             // @ts-ignore

From 7180ed9a60e312d8500f3e264cffb0961377ed32 Mon Sep 17 00:00:00 2001
From: lloydzhou <lloydzhou@qq.com>
Date: Tue, 3 Sep 2024 19:56:22 +0800
Subject: [PATCH 28/38] hotfix

---
 app/client/platforms/anthropic.ts | 4 +++-
 app/utils/chat.ts                 | 4 +++-
 2 files changed, 6 insertions(+), 2 deletions(-)

diff --git a/app/client/platforms/anthropic.ts b/app/client/platforms/anthropic.ts
index d6958dfc53c..e30b53ed212 100644
--- a/app/client/platforms/anthropic.ts
+++ b/app/client/platforms/anthropic.ts
@@ -285,7 +285,9 @@ export class ClaudeApi implements LLMApi {
                   type: "tool_use",
                   id: tool.id,
                   name: tool?.function?.name,
-                  input: JSON.parse(tool?.function?.arguments as string),
+                  input: tool?.function?.arguments
+                    ? JSON.parse(tool?.function?.arguments)
+                    : {},
                 }),
               ),
             },
diff --git a/app/utils/chat.ts b/app/utils/chat.ts
index 49e5060d432..df266e59ddb 100644
--- a/app/utils/chat.ts
+++ b/app/utils/chat.ts
@@ -215,7 +215,9 @@ export function stream(
               // @ts-ignore
               funcs[tool.function.name](
                 // @ts-ignore
-                JSON.parse(tool.function.arguments),
+                tool?.function?.arguments
+                  ? JSON.parse(tool?.function?.arguments)
+                  : {},
               ),
             )
               .then((res) => {

From 6ab6b3dbca566ffbe908fd918438087527b09c62 Mon Sep 17 00:00:00 2001
From: lloydzhou <lloydzhou@qq.com>
Date: Tue, 3 Sep 2024 20:21:37 +0800
Subject: [PATCH 29/38] remove no need code

---
 app/client/platforms/anthropic.ts | 1 -
 app/client/platforms/moonshot.ts  | 1 -
 app/store/chat.ts                 | 1 -
 3 files changed, 3 deletions(-)

diff --git a/app/client/platforms/anthropic.ts b/app/client/platforms/anthropic.ts
index e30b53ed212..fce675a1671 100644
--- a/app/client/platforms/anthropic.ts
+++ b/app/client/platforms/anthropic.ts
@@ -205,7 +205,6 @@ export class ClaudeApi implements LLMApi {
         .getAsTools(
           useChatStore.getState().currentSession().mask?.plugin as string[],
         );
-      console.log("getAsTools", tools, funcs);
       return stream(
         path,
         requestBody,
diff --git a/app/client/platforms/moonshot.ts b/app/client/platforms/moonshot.ts
index 311f2726a11..c38d3317bd0 100644
--- a/app/client/platforms/moonshot.ts
+++ b/app/client/platforms/moonshot.ts
@@ -127,7 +127,6 @@ export class MoonshotApi implements LLMApi {
           .getAsTools(
             useChatStore.getState().currentSession().mask?.plugin as string[],
           );
-        console.log("getAsTools", tools, funcs);
         return stream(
           chatPath,
           requestPayload,
diff --git a/app/store/chat.ts b/app/store/chat.ts
index 9e7aa043ff1..db7fe35f0fc 100644
--- a/app/store/chat.ts
+++ b/app/store/chat.ts
@@ -410,7 +410,6 @@ export const useChatStore = createPersistStore(
             });
           },
           onAfterTool(tool: ChatMessageTool) {
-            console.log("onAfterTool", botMessage);
             botMessage?.tools?.forEach((t, i, tools) => {
               if (tool.id == t.id) {
                 tools[i] = { ...tool };

From 04e1ab63bbcb863721dcd50a8d1a2c862a096b96 Mon Sep 17 00:00:00 2001
From: lloydzhou <lloydzhou@qq.com>
Date: Wed, 4 Sep 2024 11:47:42 +0800
Subject: [PATCH 30/38] update readme

---
 README.md | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/README.md b/README.md
index 9811f1d353c..8b6a22818b7 100644
--- a/README.md
+++ b/README.md
@@ -98,6 +98,7 @@ For enterprise inquiries, please contact: **business@nextchat.dev**
 
 ## What's New
 
+- 🚀 v2.15.0 Now supports Plugins! Read this: [NextChat-Awesome-Plugins](https://github.com/ChatGPTNextWeb/NextChat-Awesome-Plugins)
 - 🚀 v2.14.0 Now supports  Artifacts & SD 
 - 🚀 v2.10.1 support Google Gemini Pro model.
 - 🚀 v2.9.11 you can use azure endpoint now.
@@ -135,6 +136,7 @@ For enterprise inquiries, please contact: **business@nextchat.dev**
 
 ## 最新动态
 
+- 🚀 v2.15.0 现在支持插件功能了!了解更多:[NextChat-Awesome-Plugins](https://github.com/ChatGPTNextWeb/NextChat-Awesome-Plugins)
 - 🚀 v2.14.0 现在支持 Artifacts & SD 了。
 - 🚀 v2.10.1 现在支持 Gemini Pro 模型。
 - 🚀 v2.9.11 现在可以使用自定义 Azure 服务了。

From f9a047aad48c6b2884e7b3d1077d27a7335884ef Mon Sep 17 00:00:00 2001
From: lloydzhou <lloydzhou@qq.com>
Date: Wed, 4 Sep 2024 21:04:13 +0800
Subject: [PATCH 31/38] using tauri http api run plugin to fixed cors in App

---
 app/components/plugin.tsx | 33 ++++++++++++++++++---------------
 app/global.d.ts           |  8 +++++++-
 app/store/chat.ts         |  2 +-
 app/store/plugin.ts       |  2 ++
 app/utils.ts              | 35 ++++++++++++++++++++++++++++++++++-
 package.json              |  3 ++-
 src-tauri/Cargo.toml      |  2 +-
 src-tauri/tauri.conf.json |  5 +++++
 yarn.lock                 |  5 +++++
 9 files changed, 75 insertions(+), 20 deletions(-)

diff --git a/app/components/plugin.tsx b/app/components/plugin.tsx
index 5aad2c70aaa..b847ad78e1b 100644
--- a/app/components/plugin.tsx
+++ b/app/components/plugin.tsx
@@ -27,6 +27,7 @@ import {
 import Locale from "../locales";
 import { useNavigate } from "react-router-dom";
 import { useEffect, useState } from "react";
+import { getClientConfig } from "../config/client";
 
 export function PluginPage() {
   const navigate = useNavigate();
@@ -293,21 +294,23 @@ export function PluginPage() {
                   ></PasswordInput>
                 </ListItem>
               )}
-              <ListItem
-                title={Locale.Plugin.Auth.Proxy}
-                subTitle={Locale.Plugin.Auth.ProxyDescription}
-              >
-                <input
-                  type="checkbox"
-                  checked={editingPlugin?.usingProxy}
-                  style={{ minWidth: 16 }}
-                  onChange={(e) => {
-                    pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
-                      plugin.usingProxy = e.currentTarget.checked;
-                    });
-                  }}
-                ></input>
-              </ListItem>
+              {!getClientConfig()?.isApp && (
+                <ListItem
+                  title={Locale.Plugin.Auth.Proxy}
+                  subTitle={Locale.Plugin.Auth.ProxyDescription}
+                >
+                  <input
+                    type="checkbox"
+                    checked={editingPlugin?.usingProxy}
+                    style={{ minWidth: 16 }}
+                    onChange={(e) => {
+                      pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
+                        plugin.usingProxy = e.currentTarget.checked;
+                      });
+                    }}
+                  ></input>
+                </ListItem>
+              )}
             </List>
             <List>
               <ListItem title={Locale.Plugin.EditModal.Content}>
diff --git a/app/global.d.ts b/app/global.d.ts
index 31e2b6e8a84..8ee636bcd3c 100644
--- a/app/global.d.ts
+++ b/app/global.d.ts
@@ -21,10 +21,16 @@ declare interface Window {
       writeBinaryFile(path: string, data: Uint8Array): Promise<void>;
       writeTextFile(path: string, data: string): Promise<void>;
     };
-    notification:{
+    notification: {
       requestPermission(): Promise<Permission>;
       isPermissionGranted(): Promise<boolean>;
       sendNotification(options: string | Options): void;
     };
+    http: {
+      fetch<T>(
+        url: string,
+        options?: Record<string, unknown>,
+      ): Promise<Response<T>>;
+    };
   };
 }
diff --git a/app/store/chat.ts b/app/store/chat.ts
index db7fe35f0fc..8b0cc39eb62 100644
--- a/app/store/chat.ts
+++ b/app/store/chat.ts
@@ -420,7 +420,7 @@ export const useChatStore = createPersistStore(
             });
           },
           onError(error) {
-            const isAborted = error.message.includes("aborted");
+            const isAborted = error.message?.includes?.("aborted");
             botMessage.content +=
               "\n\n" +
               prettyObject({
diff --git a/app/store/plugin.ts b/app/store/plugin.ts
index 934a989dc5e..3bf08e68d4d 100644
--- a/app/store/plugin.ts
+++ b/app/store/plugin.ts
@@ -4,6 +4,7 @@ import { StoreKey } from "../constant";
 import { nanoid } from "nanoid";
 import { createPersistStore } from "../utils/store";
 import yaml from "js-yaml";
+import { adapter } from "../utils";
 
 export type Plugin = {
   id: string;
@@ -61,6 +62,7 @@ export const FunctionToolService = {
     const api = new OpenAPIClientAxios({
       definition: yaml.load(plugin.content) as any,
       axiosConfigDefaults: {
+        adapter: adapter as any,
         baseURL,
         headers,
       },
diff --git a/app/utils.ts b/app/utils.ts
index 0ac5867fab0..60041ba060f 100644
--- a/app/utils.ts
+++ b/app/utils.ts
@@ -2,7 +2,9 @@ import { useEffect, useState } from "react";
 import { showToast } from "./components/ui-lib";
 import Locale from "./locales";
 import { RequestMessage } from "./client/api";
-import { ServiceProvider } from "./constant";
+import { ServiceProvider, REQUEST_TIMEOUT_MS } from "./constant";
+import isObject from "lodash-es/isObject";
+import { fetch as tauriFetch, Body, ResponseType } from "@tauri-apps/api/http";
 
 export function trimTopic(topic: string) {
   // Fix an issue where double quotes still show in the Indonesian language
@@ -285,3 +287,34 @@ export function showPlugins(provider: ServiceProvider, model: string) {
   }
   return false;
 }
+
+export function fetch(
+  url: string,
+  options?: Record<string, unknown>,
+): Promise<any> {
+  if (window.__TAURI__) {
+    const payload = options?.body || options?.data;
+    return tauriFetch(url, {
+      ...options,
+      body:
+        payload &&
+        ({
+          type: "Text",
+          payload,
+        } as any),
+      timeout: ((options?.timeout as number) || REQUEST_TIMEOUT_MS) / 1000,
+      responseType:
+        options?.responseType == "text" ? ResponseType.Text : ResponseType.JSON,
+    } as any);
+  }
+  return window.fetch(url, options);
+}
+
+export function adapter(config: Record<string, unknown>) {
+  const { baseURL, url, params, ...rest } = config;
+  const path = baseURL ? `${baseURL}${url}` : url;
+  const fetchUrl = params
+    ? `${path}?${new URLSearchParams(params as any).toString()}`
+    : path;
+  return fetch(fetchUrl as string, { ...rest, responseType: "text" });
+}
diff --git a/package.json b/package.json
index d3f0621851c..ca5fcc0f5df 100644
--- a/package.json
+++ b/package.json
@@ -51,14 +51,15 @@
     "zustand": "^4.3.8"
   },
   "devDependencies": {
+    "@tauri-apps/api": "^1.6.0",
     "@tauri-apps/cli": "1.5.11",
+    "@types/js-yaml": "4.0.9",
     "@types/lodash-es": "^4.17.12",
     "@types/node": "^20.11.30",
     "@types/react": "^18.2.70",
     "@types/react-dom": "^18.2.7",
     "@types/react-katex": "^3.0.0",
     "@types/spark-md5": "^3.0.4",
-    "@types/js-yaml": "4.0.9",
     "concurrently": "^8.2.2",
     "cross-env": "^7.0.3",
     "eslint": "^8.49.0",
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index e0892590223..387584491ba 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -17,7 +17,7 @@ tauri-build = { version = "1.5.1", features = [] }
 [dependencies]
 serde_json = "1.0"
 serde = { version = "1.0", features = ["derive"] }
-tauri = { version = "1.5.4", features = [
+tauri = { version = "1.5.4", features = [ "http-all",
     "notification-all",
     "fs-all",
     "clipboard-all",
diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json
index 120ab9b5aee..78807a2c5fe 100644
--- a/src-tauri/tauri.conf.json
+++ b/src-tauri/tauri.conf.json
@@ -50,6 +50,11 @@
       },
       "notification": {
         "all": true
+      },
+      "http": {
+        "all": true,
+        "request": true,
+        "scope": ["https://*", "http://*"]
       }
     },
     "bundle": {
diff --git a/yarn.lock b/yarn.lock
index 5ca929baf56..4979e4d995e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1553,6 +1553,11 @@
   dependencies:
     tslib "^2.4.0"
 
+"@tauri-apps/api@^1.6.0":
+  version "1.6.0"
+  resolved "https://registry.npmjs.org/@tauri-apps/api/-/api-1.6.0.tgz#745b7e4e26782c3b2ad9510d558fa5bb2cf29186"
+  integrity sha512-rqI++FWClU5I2UBp4HXFvl+sBWkdigBkxnpJDQUWttNyG7IZP4FwQGhTNL5EOw0vI8i6eSAJ5frLqO7n7jbJdg==
+
 "@tauri-apps/cli-darwin-arm64@1.5.11":
   version "1.5.11"
   resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-1.5.11.tgz#a831f98f685148e46e8050dbdddbf4bcdda9ddc6"

From 09aec7b22e90c311e632fc64bf7aadaa6e79989d Mon Sep 17 00:00:00 2001
From: lloydzhou <lloydzhou@qq.com>
Date: Wed, 4 Sep 2024 21:32:22 +0800
Subject: [PATCH 32/38] using tauri http api run plugin to fixed cors in App

---
 app/store/plugin.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/app/store/plugin.ts b/app/store/plugin.ts
index 3bf08e68d4d..74f0fbe17a4 100644
--- a/app/store/plugin.ts
+++ b/app/store/plugin.ts
@@ -62,7 +62,7 @@ export const FunctionToolService = {
     const api = new OpenAPIClientAxios({
       definition: yaml.load(plugin.content) as any,
       axiosConfigDefaults: {
-        adapter: adapter as any,
+        adapter: (window.__TAURI__ ? adapter : ["xhr"]) as any,
         baseURL,
         headers,
       },

From b590d0857c289259a5e3b5be330cffbce944879c Mon Sep 17 00:00:00 2001
From: lloydzhou <lloydzhou@qq.com>
Date: Thu, 5 Sep 2024 13:51:22 +0800
Subject: [PATCH 33/38] disable nextjs proxy, then can using dalle as plugin

---
 next.config.mjs | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/next.config.mjs b/next.config.mjs
index 27c60dd2997..26dadca4c9e 100644
--- a/next.config.mjs
+++ b/next.config.mjs
@@ -65,10 +65,10 @@ if (mode !== "export") {
   nextConfig.rewrites = async () => {
     const ret = [
       // adjust for previous version directly using "/api/proxy/" as proxy base route
-      {
-        source: "/api/proxy/v1/:path*",
-        destination: "https://api.openai.com/v1/:path*",
-      },
+      // {
+      //   source: "/api/proxy/v1/:path*",
+      //   destination: "https://api.openai.com/v1/:path*",
+      // },
       {
         // https://{resource_name}.openai.azure.com/openai/deployments/{deploy_name}/chat/completions
         source: "/api/proxy/azure/:resource_name/deployments/:deploy_name/:path*",

From caf50b6e6c7887587c7daa8fd83ad7b9d30ef98a Mon Sep 17 00:00:00 2001
From: lloydzhou <lloydzhou@qq.com>
Date: Thu, 5 Sep 2024 14:46:16 +0800
Subject: [PATCH 34/38] move artifacts into mask settings

---
 app/components/chat.tsx     | 37 ++++++++++++-------------------------
 app/components/markdown.tsx |  7 +------
 app/components/mask.tsx     | 16 ++++++++++++++++
 app/constant.ts             |  4 ----
 app/locales/cn.ts           |  5 ++++-
 app/locales/en.ts           |  5 ++++-
 app/store/mask.ts           |  5 +++--
 7 files changed, 40 insertions(+), 39 deletions(-)

diff --git a/app/components/chat.tsx b/app/components/chat.tsx
index 4011858f67d..e39bc74a9b8 100644
--- a/app/components/chat.tsx
+++ b/app/components/chat.tsx
@@ -98,7 +98,6 @@ import {
   REQUEST_TIMEOUT_MS,
   UNFINISHED_INPUT,
   ServiceProvider,
-  ArtifactsPlugin,
 } from "../constant";
 import { Avatar } from "./emoji";
 import { ContextPrompts, MaskAvatar, MaskConfig } from "./mask";
@@ -727,38 +726,26 @@ export function ChatActions(props: {
         />
       )}
 
-      <ChatAction
-        onClick={() => setShowPluginSelector(true)}
-        text={Locale.Plugin.Name}
-        icon={<PluginIcon />}
-      />
-      {showPluginSelector && (
+      {showPlugins(currentProviderName, currentModel) && (
+        <ChatAction
+          onClick={() => setShowPluginSelector(true)}
+          text={Locale.Plugin.Name}
+          icon={<PluginIcon />}
+        />
+      )}
+      {showPluginSelector && showPlugins(currentProviderName, currentModel) && (
         <Selector
           multiple
           defaultSelectedValue={chatStore.currentSession().mask?.plugin}
-          items={[
-            {
-              title: Locale.Plugin.Artifacts,
-              value: ArtifactsPlugin.Artifacts as string,
-            },
-          ].concat(
-            showPlugins(currentProviderName, currentModel)
-              ? pluginStore.getAll().map((item) => ({
-                  // @ts-ignore
-                  title: `${item?.title}@${item?.version}`,
-                  // @ts-ignore
-                  value: item?.id,
-                }))
-              : [],
-          )}
+          items={pluginStore.getAll().map((item) => ({
+            title: `${item?.title}@${item?.version}`,
+            value: item?.id,
+          }))}
           onClose={() => setShowPluginSelector(false)}
           onSelection={(s) => {
             chatStore.updateCurrentSession((session) => {
               session.mask.plugin = s as string[];
             });
-            if (s.includes(ArtifactsPlugin.Artifacts)) {
-              showToast(ArtifactsPlugin.Artifacts);
-            }
           }}
         />
       )}
diff --git a/app/components/markdown.tsx b/app/components/markdown.tsx
index 58579ab47a4..4b9e608c9a9 100644
--- a/app/components/markdown.tsx
+++ b/app/components/markdown.tsx
@@ -19,7 +19,6 @@ import {
   HTMLPreview,
   HTMLPreviewHander,
 } from "./artifacts";
-import { ArtifactsPlugin } from "../constant";
 import { useChatStore } from "../store";
 import { IconButton } from "./button";
 
@@ -77,7 +76,6 @@ export function PreCode(props: { children: any }) {
   const { height } = useWindowSize();
   const chatStore = useChatStore();
   const session = chatStore.currentSession();
-  const plugins = session.mask?.plugin;
 
   const renderArtifacts = useDebouncedCallback(() => {
     if (!ref.current) return;
@@ -94,10 +92,7 @@ export function PreCode(props: { children: any }) {
     }
   }, 600);
 
-  const enableArtifacts = useMemo(
-    () => plugins?.includes(ArtifactsPlugin.Artifacts),
-    [plugins],
-  );
+  const enableArtifacts = session.mask?.enableArtifacts !== false;
 
   //Wrap the paragraph for plain-text
   useEffect(() => {
diff --git a/app/components/mask.tsx b/app/components/mask.tsx
index 8c17a544a26..78b89f26067 100644
--- a/app/components/mask.tsx
+++ b/app/components/mask.tsx
@@ -167,6 +167,22 @@ export function MaskConfig(props: {
           ></input>
         </ListItem>
 
+        <ListItem
+          title={Locale.Mask.Config.Artifacts.Title}
+          subTitle={Locale.Mask.Config.Artifacts.SubTitle}
+        >
+          <input
+            aria-label={Locale.Mask.Config.Artifacts.Title}
+            type="checkbox"
+            checked={props.mask.enableArtifacts}
+            onChange={(e) => {
+              props.updateMask((mask) => {
+                mask.enableArtifacts = e.currentTarget.checked;
+              });
+            }}
+          ></input>
+        </ListItem>
+
         {!props.shouldSyncFromGlobal ? (
           <ListItem
             title={Locale.Mask.Config.Share.Title}
diff --git a/app/constant.ts b/app/constant.ts
index f6026d671c8..cd5c79b256f 100644
--- a/app/constant.ts
+++ b/app/constant.ts
@@ -73,10 +73,6 @@ export enum FileName {
   Prompts = "prompts.json",
 }
 
-export enum ArtifactsPlugin {
-  Artifacts = "artifacts",
-}
-
 export enum StoreKey {
   Chat = "chat-next-web-store",
   Plugin = "chat-next-web-plugin",
diff --git a/app/locales/cn.ts b/app/locales/cn.ts
index 201f7dc5d49..742f85952c5 100644
--- a/app/locales/cn.ts
+++ b/app/locales/cn.ts
@@ -532,7 +532,6 @@ const cn = {
   },
   Plugin: {
     Name: "插件",
-    Artifacts: "Artifacts",
     Page: {
       Title: "插件",
       SubTitle: (count: number) => `${count} 个插件`,
@@ -604,6 +603,10 @@ const cn = {
         Title: "隐藏预设对话",
         SubTitle: "隐藏后预设对话不会出现在聊天界面",
       },
+      Artifacts: {
+        Title: "启用Artifacts",
+        SubTitle: "启用之后可以直接渲染HTML页面",
+      },
       Share: {
         Title: "分享此面具",
         SubTitle: "生成此面具的直达链接",
diff --git a/app/locales/en.ts b/app/locales/en.ts
index d2b27fdcdb6..13b3fe5e6a9 100644
--- a/app/locales/en.ts
+++ b/app/locales/en.ts
@@ -540,7 +540,6 @@ const en: LocaleType = {
   },
   Plugin: {
     Name: "Plugin",
-    Artifacts: "Artifacts",
     Page: {
       Title: "Plugins",
       SubTitle: (count: number) => `${count} plugins`,
@@ -613,6 +612,10 @@ const en: LocaleType = {
         Title: "Hide Context Prompts",
         SubTitle: "Do not show in-context prompts in chat",
       },
+      Artifacts: {
+        Title: "Enable Artifacts",
+        SubTitle: "Can render HTML page when enable artifacts.",
+      },
       Share: {
         Title: "Share This Mask",
         SubTitle: "Generate a link to this mask",
diff --git a/app/store/mask.ts b/app/store/mask.ts
index 05f511b0fbb..083121b65cc 100644
--- a/app/store/mask.ts
+++ b/app/store/mask.ts
@@ -2,7 +2,7 @@ import { BUILTIN_MASKS } from "../masks";
 import { getLang, Lang } from "../locales";
 import { DEFAULT_TOPIC, ChatMessage } from "./chat";
 import { ModelConfig, useAppConfig } from "./config";
-import { StoreKey, ArtifactsPlugin } from "../constant";
+import { StoreKey } from "../constant";
 import { nanoid } from "nanoid";
 import { createPersistStore } from "../utils/store";
 
@@ -18,6 +18,7 @@ export type Mask = {
   lang: Lang;
   builtin: boolean;
   plugin?: string[];
+  enableArtifacts?: boolean;
 };
 
 export const DEFAULT_MASK_STATE = {
@@ -38,7 +39,7 @@ export const createEmptyMask = () =>
     lang: getLang(),
     builtin: false,
     createdAt: Date.now(),
-    plugin: [ArtifactsPlugin.Artifacts as string],
+    plugin: [],
   }) as Mask;
 
 export const useMaskStore = createPersistStore(

From 80b8f956a9aca708a2d4e1690f40c1ab11a9e781 Mon Sep 17 00:00:00 2001
From: lloydzhou <lloydzhou@qq.com>
Date: Thu, 5 Sep 2024 14:49:11 +0800
Subject: [PATCH 35/38] move artifacts into mask settings

---
 README.md | 6 ++----
 1 file changed, 2 insertions(+), 4 deletions(-)

diff --git a/README.md b/README.md
index 8b6a22818b7..c8b158956b3 100644
--- a/README.md
+++ b/README.md
@@ -91,8 +91,7 @@ For enterprise inquiries, please contact: **business@nextchat.dev**
 - [x] Desktop App with tauri
 - [x] Self-host Model: Fully compatible with [RWKV-Runner](https://github.com/josStorer/RWKV-Runner), as well as server deployment of [LocalAI](https://github.com/go-skynet/LocalAI): llama/gpt4all/rwkv/vicuna/koala/gpt4all-j/cerebras/falcon/dolly etc.
 - [x] Artifacts: Easily preview, copy and share generated content/webpages through a separate window [#5092](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/pull/5092)
-- [x] Plugins: support artifacts, network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) [#5353](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5353)
-  - [x] artifacts
+- [x] Plugins: support network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) [#5353](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5353)
   - [x] network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) [#5353](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5353)
 - [ ] local knowledge base
 
@@ -129,8 +128,7 @@ For enterprise inquiries, please contact: **business@nextchat.dev**
 - [x] 使用 tauri 打包桌面应用
 - [x] 支持自部署的大语言模型:开箱即用 [RWKV-Runner](https://github.com/josStorer/RWKV-Runner) ,服务端部署 [LocalAI 项目](https://github.com/go-skynet/LocalAI) llama / gpt4all / rwkv / vicuna / koala / gpt4all-j / cerebras / falcon / dolly 等等,或者使用 [api-for-open-llm](https://github.com/xusenlinzy/api-for-open-llm)
 - [x] Artifacts: 通过独立窗口,轻松预览、复制和分享生成的内容/可交互网页 [#5092](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/pull/5092)
-- [x] 插件机制,支持 artifacts,联网搜索、计算器、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) [#5353](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5353)
-   - [x] artifacts
+- [x] 插件机制,支持`联网搜索`、`计算器`、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) [#5353](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5353)
    - [x] 支持联网搜索、计算器、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) [#5353](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5353)
  - [ ] 本地知识库
 

From 7c0acc7b77cb868e23ad517bc17b37da15b8a6c7 Mon Sep 17 00:00:00 2001
From: lloydzhou <lloydzhou@qq.com>
Date: Thu, 5 Sep 2024 22:02:06 +0800
Subject: [PATCH 36/38] hotfix tools empty array

---
 app/utils/chat.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/app/utils/chat.ts b/app/utils/chat.ts
index df266e59ddb..7f3bb23c58e 100644
--- a/app/utils/chat.ts
+++ b/app/utils/chat.ts
@@ -277,7 +277,7 @@ export function stream(
       method: "POST",
       body: JSON.stringify({
         ...requestPayload,
-        tools,
+        tools: tools && tools.length ? tools : undefined,
       }),
       signal: controller.signal,
       headers,

From 7455978ee57b3ab7a0cb9b375ec0548fb6b19b5f Mon Sep 17 00:00:00 2001
From: lloydzhou <lloydzhou@qq.com>
Date: Fri, 6 Sep 2024 09:26:06 +0800
Subject: [PATCH 37/38] default enable artifact

---
 app/components/mask.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/app/components/mask.tsx b/app/components/mask.tsx
index 78b89f26067..62503c37a85 100644
--- a/app/components/mask.tsx
+++ b/app/components/mask.tsx
@@ -174,7 +174,7 @@ export function MaskConfig(props: {
           <input
             aria-label={Locale.Mask.Config.Artifacts.Title}
             type="checkbox"
-            checked={props.mask.enableArtifacts}
+            checked={props.mask.enableArtifacts !== false}
             onChange={(e) => {
               props.updateMask((mask) => {
                 mask.enableArtifacts = e.currentTarget.checked;

From 9275f2d7531a9301fe4c430c9b01ee0200191854 Mon Sep 17 00:00:00 2001
From: lloydzhou <lloydzhou@qq.com>
Date: Fri, 6 Sep 2024 19:37:24 +0800
Subject: [PATCH 38/38] add awesome plugin repo url

---
 app/components/chat.tsx   | 10 ++++++++--
 app/components/plugin.tsx | 31 +++++++++++++++++++++++++++++++
 app/constant.ts           |  1 +
 app/locales/cn.ts         |  1 +
 app/locales/en.ts         |  1 +
 5 files changed, 42 insertions(+), 2 deletions(-)

diff --git a/app/components/chat.tsx b/app/components/chat.tsx
index e39bc74a9b8..dad1933ace9 100644
--- a/app/components/chat.tsx
+++ b/app/components/chat.tsx
@@ -728,12 +728,18 @@ export function ChatActions(props: {
 
       {showPlugins(currentProviderName, currentModel) && (
         <ChatAction
-          onClick={() => setShowPluginSelector(true)}
+          onClick={() => {
+            if (pluginStore.getAll().length == 0) {
+              navigate(Path.Plugins);
+            } else {
+              setShowPluginSelector(true);
+            }
+          }}
           text={Locale.Plugin.Name}
           icon={<PluginIcon />}
         />
       )}
-      {showPluginSelector && showPlugins(currentProviderName, currentModel) && (
+      {showPluginSelector && (
         <Selector
           multiple
           defaultSelectedValue={chatStore.currentSession().mask?.plugin}
diff --git a/app/components/plugin.tsx b/app/components/plugin.tsx
index b847ad78e1b..6f0b371075e 100644
--- a/app/components/plugin.tsx
+++ b/app/components/plugin.tsx
@@ -1,6 +1,7 @@
 import { useDebouncedCallback } from "use-debounce";
 import OpenAPIClientAxios from "openapi-client-axios";
 import yaml from "js-yaml";
+import { PLUGINS_REPO_URL } from "../constant";
 import { IconButton } from "./button";
 import { ErrorBoundary } from "./error";
 
@@ -14,6 +15,7 @@ import DeleteIcon from "../icons/delete.svg";
 import EyeIcon from "../icons/eye.svg";
 import ConfirmIcon from "../icons/confirm.svg";
 import ReloadIcon from "../icons/reload.svg";
+import GithubIcon from "../icons/github.svg";
 
 import { Plugin, usePluginStore, FunctionToolService } from "../store/plugin";
 import {
@@ -129,6 +131,15 @@ export function PluginPage() {
           </div>
 
           <div className="window-actions">
+            <div className="window-action-button">
+              <a
+                href={PLUGINS_REPO_URL}
+                target="_blank"
+                rel="noopener noreferrer"
+              >
+                <IconButton icon={<GithubIcon />} bordered />
+              </a>
+            </div>
             <div className="window-action-button">
               <IconButton
                 icon={<CloseIcon />}
@@ -162,6 +173,26 @@ export function PluginPage() {
           </div>
 
           <div>
+            {plugins.length == 0 && (
+              <div
+                style={{
+                  display: "flex",
+                  margin: "60px auto",
+                  alignItems: "center",
+                  justifyContent: "center",
+                }}
+              >
+                {Locale.Plugin.Page.Find}
+                <a
+                  href={PLUGINS_REPO_URL}
+                  target="_blank"
+                  rel="noopener noreferrer"
+                  style={{ marginLeft: 16 }}
+                >
+                  <IconButton icon={<GithubIcon />} bordered />
+                </a>
+              </div>
+            )}
             {plugins.map((m) => (
               <div className={styles["mask-item"]} key={m.id}>
                 <div className={styles["mask-header"]}>
diff --git a/app/constant.ts b/app/constant.ts
index cd5c79b256f..90557c16c72 100644
--- a/app/constant.ts
+++ b/app/constant.ts
@@ -3,6 +3,7 @@ import path from "path";
 export const OWNER = "ChatGPTNextWeb";
 export const REPO = "ChatGPT-Next-Web";
 export const REPO_URL = `https://github.com/${OWNER}/${REPO}`;
+export const PLUGINS_REPO_URL = `https://github.com/${OWNER}/NextChat-Awesome-Plugins`;
 export const ISSUE_URL = `https://github.com/${OWNER}/${REPO}/issues`;
 export const UPDATE_URL = `${REPO_URL}#keep-updated`;
 export const RELEASE_URL = `${REPO_URL}/releases`;
diff --git a/app/locales/cn.ts b/app/locales/cn.ts
index 742f85952c5..33e368f69f4 100644
--- a/app/locales/cn.ts
+++ b/app/locales/cn.ts
@@ -537,6 +537,7 @@ const cn = {
       SubTitle: (count: number) => `${count} 个插件`,
       Search: "搜索插件",
       Create: "新建",
+      Find: "您可以在Github上找到优秀的插件:",
     },
     Item: {
       Info: (count: number) => `${count} 方法`,
diff --git a/app/locales/en.ts b/app/locales/en.ts
index 13b3fe5e6a9..403b9b687e7 100644
--- a/app/locales/en.ts
+++ b/app/locales/en.ts
@@ -545,6 +545,7 @@ const en: LocaleType = {
       SubTitle: (count: number) => `${count} plugins`,
       Search: "Search Plugin",
       Create: "Create",
+      Find: "You can find awesome plugins on github: ",
     },
     Item: {
       Info: (count: number) => `${count} method`,