Преглед изворни кода

Merge branch 'ChatGPTNextWeb:main' into main

endless-learner пре 1 година
родитељ
комит
47fb40d572

+ 1 - 1
.env.template

@@ -66,4 +66,4 @@ ANTHROPIC_API_VERSION=
 ANTHROPIC_URL=
 
 ### (optional)
-WHITE_WEBDEV_ENDPOINTS=
+WHITE_WEBDAV_ENDPOINTS=

+ 1 - 1
README.md

@@ -340,7 +340,7 @@ For ByteDance: use `modelName@bytedance=deploymentName` to customize model name
 
 Change default model
 
-### `WHITE_WEBDEV_ENDPOINTS` (optional)
+### `WHITE_WEBDAV_ENDPOINTS` (optional)
 
 You can use this option if you want to increase the number of webdav service addresses you are allowed to access, as required by the format:
 - Each address must be a complete endpoint 

+ 1 - 1
README_CN.md

@@ -202,7 +202,7 @@ ByteDance Api Url.
 
 如果你想禁用从链接解析预制设置,将此环境变量设置为 1 即可。
 
-### `WHITE_WEBDEV_ENDPOINTS` (可选)
+### `WHITE_WEBDAV_ENDPOINTS` (可选)
 
 如果你想增加允许访问的webdav服务地址,可以使用该选项,格式要求:
 - 每一个地址必须是一个完整的 endpoint

+ 1 - 1
README_JA.md

@@ -193,7 +193,7 @@ ByteDance API の URL。
 
 リンクからのプリセット設定解析を無効にしたい場合は、この環境変数を 1 に設定します。
 
-### `WHITE_WEBDEV_ENDPOINTS` (オプション)
+### `WHITE_WEBDAV_ENDPOINTS` (オプション)
 
 アクセス許可を与える WebDAV サービスのアドレスを追加したい場合、このオプションを使用します。フォーマット要件:
 - 各アドレスは完全なエンドポイントでなければなりません。

+ 1 - 1
app/api/webdav/[...path]/route.ts

@@ -6,7 +6,7 @@ const config = getServerSideConfig();
 
 const mergedAllowedWebDavEndpoints = [
   ...internalAllowedWebDavEndpoints,
-  ...config.allowedWebDevEndpoints,
+  ...config.allowedWebDavEndpoints,
 ].filter((domain) => Boolean(domain.trim()));
 
 const normalizeUrl = (url: string) => {

+ 4 - 1
app/client/platforms/openai.ts

@@ -277,6 +277,7 @@ export class ChatGPTApi implements LLMApi {
         );
       }
       if (shouldStream) {
+        let index = -1;
         const [tools, funcs] = usePluginStore
           .getState()
           .getAsTools(
@@ -302,10 +303,10 @@ export class ChatGPTApi implements LLMApi {
             }>;
             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) {
+                index += 1;
                 runTools.push({
                   id,
                   type: tool_calls[0]?.type,
@@ -327,6 +328,8 @@ export class ChatGPTApi implements LLMApi {
             toolCallMessage: any,
             toolCallResult: any[],
           ) => {
+            // reset index value
+            index = -1;
             // @ts-ignore
             requestPayload?.messages?.splice(
               // @ts-ignore

+ 21 - 3
app/components/markdown.tsx

@@ -21,6 +21,7 @@ import {
 } from "./artifacts";
 import { useChatStore } from "../store";
 import { IconButton } from "./button";
+import { useAppConfig } from "../store/config";
 
 export function Mermaid(props: { code: string }) {
   const ref = useRef<HTMLDivElement>(null);
@@ -92,7 +93,9 @@ export function PreCode(props: { children: any }) {
     }
   }, 600);
 
-  const enableArtifacts = session.mask?.enableArtifacts !== false;
+  const config = useAppConfig();
+  const enableArtifacts =
+    session.mask?.enableArtifacts !== false && config.enableArtifacts;
 
   //Wrap the paragraph for plain-text
   useEffect(() => {
@@ -128,8 +131,9 @@ export function PreCode(props: { children: any }) {
           className="copy-code-button"
           onClick={() => {
             if (ref.current) {
-              const code = ref.current.innerText;
-              copyToClipboard(code);
+              copyToClipboard(
+                ref.current.querySelector("code")?.innerText ?? "",
+              );
             }
           }}
         ></span>
@@ -278,6 +282,20 @@ function _MarkDownContent(props: { content: string }) {
         p: (pProps) => <p {...pProps} dir="auto" />,
         a: (aProps) => {
           const href = aProps.href || "";
+          if (/\.(aac|mp3|opus|wav)$/.test(href)) {
+            return (
+              <figure>
+                <audio controls src={href}></audio>
+              </figure>
+            );
+          }
+          if (/\.(3gp|3g2|webm|ogv|mpeg|mp4|avi)$/.test(href)) {
+            return (
+              <video controls width="99.9%">
+                <source src={href} />
+              </video>
+            );
+          }
           const isInternal = /^\/#/i.test(href);
           const target = isInternal ? "_self" : aProps.target ?? "_blank";
           return <a {...aProps} target={target} />;

+ 17 - 15
app/components/mask.tsx

@@ -166,21 +166,23 @@ 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 !== false}
-            onChange={(e) => {
-              props.updateMask((mask) => {
-                mask.enableArtifacts = e.currentTarget.checked;
-              });
-            }}
-          ></input>
-        </ListItem>
+        {globalConfig.enableArtifacts && (
+          <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 !== false}
+              onChange={(e) => {
+                props.updateMask((mask) => {
+                  mask.enableArtifacts = e.currentTarget.checked;
+                });
+              }}
+            ></input>
+          </ListItem>
+        )}
 
         {!props.shouldSyncFromGlobal ? (
           <ListItem

+ 23 - 1
app/components/plugin.module.scss

@@ -10,7 +10,29 @@
     max-height: 240px;
     overflow-y: auto;
     white-space: pre-wrap;
-    min-width: 300px;
+    min-width: 280px;
   }
 }
 
+.plugin-schema {
+  display: flex;
+  justify-content: flex-end;
+  flex-direction: row;
+
+  input {
+    margin-right: 20px;
+
+    @media screen and (max-width: 600px) {
+        margin-right: 0px;
+      }
+  }
+
+  @media screen and (max-width: 600px) {
+    flex-direction: column;
+    gap: 5px;
+
+    button {
+      padding: 10px;
+    }
+  }
+}

+ 7 - 34
app/components/plugin.tsx

@@ -12,7 +12,6 @@ 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 ConfirmIcon from "../icons/confirm.svg";
 import ReloadIcon from "../icons/reload.svg";
 import GithubIcon from "../icons/github.svg";
@@ -29,7 +28,6 @@ import {
 import Locale from "../locales";
 import { useNavigate } from "react-router-dom";
 import { useState } from "react";
-import { getClientConfig } from "../config/client";
 
 export function PluginPage() {
   const navigate = useNavigate();
@@ -209,19 +207,11 @@ export function PluginPage() {
                   </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)}
-                    />
-                  )}
+                  <IconButton
+                    icon={<EditIcon />}
+                    text={Locale.Plugin.Item.Edit}
+                    onClick={() => setEditingPluginId(m.id)}
+                  />
                   {!m.builtin && (
                     <IconButton
                       icon={<DeleteIcon />}
@@ -325,30 +315,13 @@ export function PluginPage() {
                   ></PasswordInput>
                 </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}>
-                <div style={{ display: "flex", justifyContent: "flex-end" }}>
+                <div className={pluginStyles["plugin-schema"]}>
                   <input
                     type="text"
-                    style={{ minWidth: 200, marginRight: 20 }}
+                    style={{ minWidth: 200 }}
                     onInput={(e) => setLoadUrl(e.currentTarget.value)}
                   ></input>
                   <IconButton

+ 17 - 0
app/components/settings.tsx

@@ -1465,6 +1465,23 @@ export function Settings() {
               }
             ></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={config.enableArtifacts}
+              onChange={(e) =>
+                updateConfig(
+                  (config) =>
+                    (config.enableArtifacts = e.currentTarget.checked),
+                )
+              }
+            ></input>
+          </ListItem>
         </List>
 
         <SyncItems />

+ 3 - 3
app/config/server.ts

@@ -154,8 +154,8 @@ export const getServerSideConfig = () => {
   //   `[Server Config] using ${randomIndex + 1} of ${apiKeys.length} api key`,
   // );
 
-  const allowedWebDevEndpoints = (
-    process.env.WHITE_WEBDEV_ENDPOINTS ?? ""
+  const allowedWebDavEndpoints = (
+    process.env.WHITE_WEBDAV_ENDPOINTS ?? ""
   ).split(",");
 
   return {
@@ -229,6 +229,6 @@ export const getServerSideConfig = () => {
     disableFastLink: !!process.env.DISABLE_FAST_LINK,
     customModels,
     defaultModel,
-    allowedWebDevEndpoints,
+    allowedWebDavEndpoints,
   };
 };

+ 5 - 0
app/store/chat.ts

@@ -615,6 +615,7 @@ export const useChatStore = createPersistStore(
               providerName,
             },
             onFinish(message) {
+              if (!isValidMessage(message)) return;
               get().updateCurrentSession(
                 (session) =>
                   (session.topic =
@@ -690,6 +691,10 @@ export const useChatStore = createPersistStore(
             },
           });
         }
+
+        function isValidMessage(message: any): boolean {
+          return typeof message === "string" && !message.startsWith("```json");
+        }
       },
 
       updateStat(message: ChatMessage) {

+ 2 - 0
app/store/config.ts

@@ -50,6 +50,8 @@ export const DEFAULT_CONFIG = {
   enableAutoGenerateTitle: true,
   sidebarWidth: DEFAULT_SIDEBAR_WIDTH,
 
+  enableArtifacts: true, // show artifacts config
+
   disablePromptHint: false,
 
   dontShowMaskSplashScreen: false, // dont show splash screen when create chat

+ 52 - 5
app/store/plugin.ts

@@ -2,8 +2,12 @@ import OpenAPIClientAxios from "openapi-client-axios";
 import { StoreKey } from "../constant";
 import { nanoid } from "nanoid";
 import { createPersistStore } from "../utils/store";
+import { getClientConfig } from "../config/client";
 import yaml from "js-yaml";
 import { adapter } from "../utils";
+import { useAccessStore } from "./access";
+
+const isApp = getClientConfig()?.isApp;
 
 export type Plugin = {
   id: string;
@@ -16,7 +20,6 @@ export type Plugin = {
   authLocation?: string;
   authHeader?: string;
   authToken?: string;
-  usingProxy?: boolean;
 };
 
 export type FunctionToolItem = {
@@ -46,18 +49,25 @@ export const FunctionToolService = {
       plugin?.authType == "basic"
         ? `Basic ${plugin?.authToken}`
         : plugin?.authType == "bearer"
-        ? ` Bearer ${plugin?.authToken}`
+        ? `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 baseURL = !isApp ? "/api/proxy" : serverURL;
     const headers: Record<string, string | undefined> = {
-      "X-Base-URL": !!plugin?.usingProxy ? serverURL : undefined,
+      "X-Base-URL": !isApp ? serverURL : undefined,
     };
     if (authLocation == "header") {
       headers[headerName] = tokenValue;
     }
+    // try using openaiApiKey for Dalle3 Plugin.
+    if (!tokenValue && plugin.id === "dalle3") {
+      const openaiApiKey = useAccessStore.getState().openaiApiKey;
+      if (openaiApiKey) {
+        headers[headerName] = `Bearer ${openaiApiKey}`;
+      }
+    }
     const api = new OpenAPIClientAxios({
       definition: yaml.load(plugin.content) as any,
       axiosConfigDefaults: {
@@ -165,7 +175,7 @@ export const usePluginStore = createPersistStore(
   (set, get) => ({
     create(plugin?: Partial<Plugin>) {
       const plugins = get().plugins;
-      const id = nanoid();
+      const id = plugin?.id || nanoid();
       plugins[id] = {
         ...createEmptyPlugin(),
         ...plugin,
@@ -220,5 +230,42 @@ export const usePluginStore = createPersistStore(
   {
     name: StoreKey.Plugin,
     version: 1,
+    onRehydrateStorage(state) {
+      // Skip store rehydration on server side
+      if (typeof window === "undefined") {
+        return;
+      }
+
+      fetch("./plugins.json")
+        .then((res) => res.json())
+        .then((res) => {
+          Promise.all(
+            res.map((item: any) =>
+              // skip get schema
+              state.get(item.id)
+                ? item
+                : fetch(item.schema)
+                    .then((res) => res.text())
+                    .then((content) => ({
+                      ...item,
+                      content,
+                    }))
+                    .catch((e) => item),
+            ),
+          ).then((builtinPlugins: any) => {
+            builtinPlugins
+              .filter((item: any) => item?.content)
+              .forEach((item: any) => {
+                const plugin = state.create(item);
+                state.updatePlugin(plugin.id, (plugin) => {
+                  const tool = FunctionToolService.add(plugin, true);
+                  plugin.title = tool.api.definition.info.title;
+                  plugin.version = tool.api.definition.info.version;
+                  plugin.builtin = true;
+                });
+              });
+          });
+        });
+    },
   },
 );

+ 17 - 0
public/plugins.json

@@ -0,0 +1,17 @@
+[
+  {
+    "id": "dalle3",
+    "name": "Dalle3",
+    "schema": "https://ghp.ci/https://raw.githubusercontent.com/ChatGPTNextWeb/NextChat-Awesome-Plugins/main/plugins/dalle/openapi.json"
+  },
+  {
+    "id": "arxivsearch",
+    "name": "ArxivSearch",
+    "schema": "https://ghp.ci/https://raw.githubusercontent.com/ChatGPTNextWeb/NextChat-Awesome-Plugins/main/plugins/arxivsearch/openapi.json"
+  },
+  {
+    "id": "duckduckgolite",
+    "name": "DuckDuckGoLiteSearch",
+    "schema": "https://ghp.ci/https://raw.githubusercontent.com/ChatGPTNextWeb/NextChat-Awesome-Plugins/main/plugins/duckduckgolite/openapi.json"
+  }
+]