Browse Source

Merge branch 'main' of https://github.com/Yidadaa/ChatGPT-Next-Web

GH Action - Upstream Sync 1 year ago
parent
commit
62ad7e5ad3

+ 1 - 3
.gitignore

@@ -36,10 +36,8 @@ yarn-error.log*
 next-env.d.ts
 dev
 
-public/prompts.json
-
 .vscode
 .idea
 
 # docker-compose env files
-.env
+.env

+ 1 - 1
README.md

@@ -31,7 +31,7 @@ One-Click to deploy well-designed ChatGPT web UI on Vercel.
 - New in v2: create, share and debug your chat tools with prompt templates (mask)
 - Awesome prompts powered by [awesome-chatgpt-prompts-zh](https://github.com/PlexPt/awesome-chatgpt-prompts-zh) and [awesome-chatgpt-prompts](https://github.com/f/awesome-chatgpt-prompts)
 - Automatically compresses chat history to support long conversations while also saving your tokens
-- I18n: English, 简体中文, 繁体中文, 日本語, Español, Italiano, Türkçe, Deutsch, Tiếng Việt, Русский, Čeština
+- I18n: English, 简体中文, 繁体中文, 日本語, Français, Español, Italiano, Türkçe, Deutsch, Tiếng Việt, Русский, Čeština
 
 ## Roadmap
 

+ 1 - 2
app/api/auth.ts

@@ -3,8 +3,6 @@ import { getServerSideConfig } from "../config/server";
 import md5 from "spark-md5";
 import { ACCESS_CODE_PREFIX } from "../constant";
 
-const serverConfig = getServerSideConfig();
-
 function getIP(req: NextRequest) {
   let ip = req.ip ?? req.headers.get("x-real-ip");
   const forwardedFor = req.headers.get("x-forwarded-for");
@@ -34,6 +32,7 @@ export function auth(req: NextRequest) {
 
   const hashedCode = md5.hash(accessCode ?? "").trim();
 
+  const serverConfig = getServerSideConfig();
   console.log("[Auth] allowed hashed codes: ", [...serverConfig.codes]);
   console.log("[Auth] got access code:", accessCode);
   console.log("[Auth] hashed access code:", hashedCode);

+ 0 - 4
app/api/common.ts

@@ -25,10 +25,6 @@ export async function requestOpenai(req: NextRequest) {
     console.log("[Org ID]", process.env.OPENAI_ORG_ID);
   }
 
-  if (!authValue || !authValue.startsWith("Bearer sk-")) {
-    console.error("[OpenAI Request] invalid api key provided", authValue);
-  }
-
   return fetch(`${baseUrl}/${openaiPath}`, {
     headers: {
       "Content-Type": "application/json",

+ 38 - 9
app/client/platforms/openai.ts

@@ -3,7 +3,10 @@ import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
 
 import { ChatOptions, getHeaders, LLMApi, LLMUsage } from "../api";
 import Locale from "../../locales";
-import { fetchEventSource } from "@microsoft/fetch-event-source";
+import {
+  EventStreamContentType,
+  fetchEventSource,
+} from "@microsoft/fetch-event-source";
 import { prettyObject } from "@/app/utils/format";
 
 export class ChatGPTApi implements LLMApi {
@@ -68,9 +71,13 @@ export class ChatGPTApi implements LLMApi {
 
       if (shouldStream) {
         let responseText = "";
+        let finished = false;
 
         const finish = () => {
-          options.onFinish(responseText);
+          if (!finished) {
+            options.onFinish(responseText);
+            finished = true;
+          }
         };
 
         controller.signal.onabort = finish;
@@ -79,23 +86,44 @@ export class ChatGPTApi implements LLMApi {
           ...chatPayload,
           async onopen(res) {
             clearTimeout(requestTimeoutId);
-            if (res.status === 401) {
-              let extraInfo = { error: undefined };
+            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") !== EventStreamContentType ||
+              res.status !== 200
+            ) {
+              const responseTexts = [responseText];
+              let extraInfo = await res.clone().text();
               try {
-                extraInfo = await res.clone().json();
+                const resJson = await res.clone().json();
+                extraInfo = prettyObject(resJson);
               } catch {}
 
-              responseText += "\n\n" + Locale.Error.Unauthorized;
+              if (res.status === 401) {
+                responseTexts.push(Locale.Error.Unauthorized);
+              }
 
-              if (extraInfo.error) {
-                responseText += "\n\n" + prettyObject(extraInfo);
+              if (extraInfo) {
+                responseTexts.push(extraInfo);
               }
 
+              responseText = responseTexts.join("\n\n");
+
               return finish();
             }
           },
           onmessage(msg) {
-            if (msg.data === "[DONE]") {
+            if (msg.data === "[DONE]" || finished) {
               return finish();
             }
             const text = msg.data;
@@ -116,6 +144,7 @@ export class ChatGPTApi implements LLMApi {
           onerror(e) {
             options.onError?.(e);
           },
+          openWhenHidden: true,
         });
       } else {
         const res = await fetch(chatPath, chatPayload);

+ 11 - 1
app/components/chat.tsx

@@ -53,7 +53,7 @@ import chatStyle from "./chat.module.scss";
 
 import { ListItem, Modal, showModal } from "./ui-lib";
 import { useLocation, useNavigate } from "react-router-dom";
-import { LAST_INPUT_KEY, Path } from "../constant";
+import { LAST_INPUT_KEY, Path, REQUEST_TIMEOUT_MS } from "../constant";
 import { Avatar } from "./emoji";
 import { MaskAvatar, MaskConfig } from "./mask";
 import { useMaskStore } from "../store/mask";
@@ -487,6 +487,16 @@ export function Chat() {
 
   // stop response
   const onUserStop = (messageId: number) => {
+    chatStore.updateCurrentSession((session) => {
+      const stopTiming = Date.now() - REQUEST_TIMEOUT_MS;
+      session.messages.forEach((m) => {
+        // check if should stop all stale messages
+        if (m.streaming && new Date(m.date).getTime() < stopTiming) {
+          m.isError = false;
+          m.streaming = false;
+        }
+      });
+    });
     ChatControllerPool.stop(sessionIndex, messageId);
   };
 

+ 1 - 0
app/locales/cn.ts

@@ -73,6 +73,7 @@ const cn = {
         cn: "简体中文",
         en: "English",
         tw: "繁體中文",
+        fr: "Français",
         es: "Español",
         it: "Italiano",
         tr: "Türkçe",

+ 1 - 0
app/locales/cs.ts

@@ -75,6 +75,7 @@ const cs: LocaleType = {
         cn: "简体中文",
         en: "English",
         tw: "繁體中文",
+        fr: "Français",
         es: "Español",
         it: "Italiano",
         tr: "Türkçe",

+ 1 - 0
app/locales/de.ts

@@ -76,6 +76,7 @@ const de: LocaleType = {
         cn: "简体中文",
         en: "English",
         tw: "繁體中文",
+        fr: "Français",
         es: "Español",
         it: "Italiano",
         tr: "Türkçe",

+ 1 - 0
app/locales/en.ts

@@ -75,6 +75,7 @@ const en: LocaleType = {
         cn: "简体中文",
         en: "English",
         tw: "繁體中文",
+        fr: "Français",
         es: "Español",
         it: "Italiano",
         tr: "Türkçe",

+ 1 - 0
app/locales/es.ts

@@ -75,6 +75,7 @@ const es: LocaleType = {
         cn: "简体中文",
         en: "English",
         tw: "繁體中文",
+        fr: "Français",
         es: "Español",
         it: "Italiano",
         tr: "Türkçe",

+ 251 - 0
app/locales/fr.ts

@@ -0,0 +1,251 @@
+import { SubmitKey } from "../store/config";
+import type { LocaleType } from "./index";
+
+const fr: LocaleType = {
+  WIP: "Prochainement...",
+  Error: {
+    Unauthorized:
+      "Accès non autorisé, veuillez saisir le code d'accès dans la page des paramètres.",
+  },
+  ChatItem: {
+    ChatItemCount: (count: number) => `${count} messages en total`,
+  },
+  Chat: {
+    SubTitle: (count: number) => `${count} messages échangés avec ChatGPT`,
+    Actions: {
+      ChatList: "Aller à la liste de discussion",
+      CompressedHistory: "Mémoire d'historique compressée Prompt",
+      Export: "Exporter tous les messages en tant que Markdown",
+      Copy: "Copier",
+      Stop: "Arrêter",
+      Retry: "Réessayer",
+      Delete: "Supprimer",
+    },
+    Rename: "Renommer la conversation",
+    Typing: "En train d'écrire…",
+    Input: (submitKey: string) => {
+      var inputHints = `Appuyez sur ${submitKey} pour envoyer`;
+      if (submitKey === String(SubmitKey.Enter)) {
+        inputHints += ", Shift + Enter pour insérer un saut de ligne";
+      }
+      return inputHints + ", / pour rechercher des prompts";
+    },
+    Send: "Envoyer",
+    Config: {
+      Reset: "Restaurer les paramètres par défaut",
+      SaveAs: "Enregistrer en tant que masque",
+    },
+  },
+  Export: {
+    Title: "Tous les messages",
+    Copy: "Tout sélectionner",
+    Download: "Télécharger",
+    MessageFromYou: "Message de votre part",
+    MessageFromChatGPT: "Message de ChatGPT",
+  },
+  Memory: {
+    Title: "Prompt mémoire",
+    EmptyContent: "Rien encore.",
+    Send: "Envoyer la mémoire",
+    Copy: "Copier la mémoire",
+    Reset: "Réinitialiser la session",
+    ResetConfirm:
+      "La réinitialisation supprimera l'historique de la conversation actuelle ainsi que la mémoire de l'historique. Êtes-vous sûr de vouloir procéder à la réinitialisation?",
+  },
+  Home: {
+    NewChat: "Nouvelle discussion",
+    DeleteChat: "Confirmer la suppression de la conversation sélectionnée ?",
+    DeleteToast: "Conversation supprimée",
+    Revert: "Revenir en arrière",
+  },
+  Settings: {
+    Title: "Paramètres",
+    SubTitle: "Toutes les configurations",
+    Actions: {
+      ClearAll: "Effacer toutes les données",
+      ResetAll: "Réinitialiser les configurations",
+      Close: "Fermer",
+      ConfirmResetAll:
+        "Êtes-vous sûr de vouloir réinitialiser toutes les configurations?",
+      ConfirmClearAll: "Êtes-vous sûr de vouloir supprimer toutes les données?",
+    },
+    Lang: {
+      Name: "Language", // ATTENTION : si vous souhaitez ajouter une nouvelle traduction, ne traduisez pas cette valeur, laissez-la sous forme de `Language`
+      All: "Toutes les langues",
+      Options: {
+        cn: "简体中文",
+        en: "English",
+        tw: "繁體中文",
+        fr: "Français",
+        es: "Español",
+        it: "Italiano",
+        tr: "Türkçe",
+        jp: "日本語",
+        de: "Deutsch",
+        vi: "Vietnamese",
+        ru: "Русский",
+        cs: "Čeština",
+      },
+    },
+
+    Avatar: "Avatar",
+    FontSize: {
+      Title: "Taille des polices",
+      SubTitle: "Ajuste la taille de police du contenu de la conversation",
+    },
+    Update: {
+      Version: (x: string) => `Version : ${x}`,
+      IsLatest: "Dernière version",
+      CheckUpdate: "Vérifier la mise à jour",
+      IsChecking: "Vérification de la mise à jour...",
+      FoundUpdate: (x: string) => `Nouvelle version disponible : ${x}`,
+      GoToUpdate: "Mise à jour",
+    },
+    SendKey: "Clé d'envoi",
+    Theme: "Thème",
+    TightBorder: "Bordure serrée",
+    SendPreviewBubble: {
+      Title: "Aperçu de l'envoi dans une bulle",
+      SubTitle: "Aperçu du Markdown dans une bulle",
+    },
+    Mask: {
+      Title: "Écran de masque",
+      SubTitle:
+        "Afficher un écran de masque avant de démarrer une nouvelle discussion",
+    },
+    Prompt: {
+      Disable: {
+        Title: "Désactiver la saisie semi-automatique",
+        SubTitle: "Appuyez sur / pour activer la saisie semi-automatique",
+      },
+      List: "Liste de prompts",
+      ListCount: (builtin: number, custom: number) =>
+        `${builtin} intégré, ${custom} personnalisé`,
+      Edit: "Modifier",
+      Modal: {
+        Title: "Liste de prompts",
+        Add: "Ajouter un élément",
+        Search: "Rechercher des prompts",
+      },
+      EditModal: {
+        Title: "Modifier le prompt",
+      },
+    },
+    HistoryCount: {
+      Title: "Nombre de messages joints",
+      SubTitle: "Nombre de messages envoyés attachés par demande",
+    },
+    CompressThreshold: {
+      Title: "Seuil de compression de l'historique",
+      SubTitle:
+        "Comprimera si la longueur des messages non compressés dépasse cette valeur",
+    },
+    Token: {
+      Title: "Clé API",
+      SubTitle: "Utilisez votre clé pour ignorer la limite du code d'accès",
+      Placeholder: "Clé OpenAI API",
+    },
+    Usage: {
+      Title: "Solde du compte",
+      SubTitle(used: any, total: any) {
+        return `Épuisé ce mois-ci $${used}, abonnement $${total}`;
+      },
+      IsChecking: "Vérification...",
+      Check: "Vérifier",
+      NoAccess: "Entrez la clé API pour vérifier le solde",
+    },
+    AccessCode: {
+      Title: "Code d'accès",
+      SubTitle: "Contrôle d'accès activé",
+      Placeholder: "Code d'accès requis",
+    },
+    Model: "Modèle",
+    Temperature: {
+      Title: "Température",
+      SubTitle: "Une valeur plus élevée rendra les réponses plus aléatoires",
+    },
+    MaxTokens: {
+      Title: "Max Tokens",
+      SubTitle: "Longueur maximale des tokens d'entrée et des tokens générés",
+    },
+    PresencePenalty: {
+      Title: "Pénalité de présence",
+      SubTitle:
+        "Une valeur plus élevée augmentera la probabilité d'introduire de nouveaux sujets",
+    },
+  },
+  Store: {
+    DefaultTopic: "Nouvelle conversation",
+    BotHello: "Bonjour ! Comment puis-je vous aider aujourd'hui ?",
+    Error: "Quelque chose s'est mal passé, veuillez réessayer plus tard.",
+    Prompt: {
+      History: (content: string) =>
+        "Ceci est un résumé de l'historique des discussions entre l'IA et l'utilisateur : " +
+        content,
+      Topic:
+        "Veuillez générer un titre de quatre à cinq mots résumant notre conversation sans introduction, ponctuation, guillemets, points, symboles ou texte supplémentaire. Supprimez les guillemets inclus.",
+      Summarize:
+        "Résumez brièvement nos discussions en 200 mots ou moins pour les utiliser comme prompt de contexte futur.",
+    },
+  },
+  Copy: {
+    Success: "Copié dans le presse-papiers",
+    Failed:
+      "La copie a échoué, veuillez accorder l'autorisation d'accès au presse-papiers",
+  },
+  Context: {
+    Toast: (x: any) => `Avec ${x} contextes de prompts`,
+    Edit: "Contextes et mémoires de prompts",
+    Add: "Ajouter un prompt",
+  },
+  Plugin: {
+    Name: "Extension",
+  },
+  Mask: {
+    Name: "Masque",
+    Page: {
+      Title: "Modèle de prompt",
+      SubTitle: (count: number) => `${count} modèles de prompts`,
+      Search: "Rechercher des modèles",
+      Create: "Créer",
+    },
+    Item: {
+      Info: (count: number) => `${count} prompts`,
+      Chat: "Discussion",
+      View: "Vue",
+      Edit: "Modifier",
+      Delete: "Supprimer",
+      DeleteConfirm: "Confirmer la suppression?",
+    },
+    EditModal: {
+      Title: (readonly: boolean) =>
+        `Modifier le modèle de prompt ${readonly ? "(en lecture seule)" : ""}`,
+      Download: "Télécharger",
+      Clone: "Dupliquer",
+    },
+    Config: {
+      Avatar: "Avatar du bot",
+      Name: "Nom du bot",
+    },
+  },
+  NewChat: {
+    Return: "Retour",
+    Skip: "Passer",
+    Title: "Choisir un masque",
+    SubTitle: "Discutez avec l'âme derrière le masque",
+    More: "En savoir plus",
+    NotShow: "Ne pas afficher à nouveau",
+    ConfirmNoShow:
+      "Confirmez-vous vouloir désactiver cela? Vous pouvez le réactiver plus tard dans les paramètres.",
+  },
+
+  UI: {
+    Confirm: "Confirmer",
+    Cancel: "Annuler",
+    Close: "Fermer",
+    Create: "Créer",
+    Edit: "Éditer",
+  },
+};
+
+export default fr;

+ 3 - 0
app/locales/index.ts

@@ -1,6 +1,7 @@
 import CN from "./cn";
 import EN from "./en";
 import TW from "./tw";
+import FR from "./fr";
 import ES from "./es";
 import IT from "./it";
 import TR from "./tr";
@@ -16,6 +17,7 @@ export const AllLangs = [
   "en",
   "cn",
   "tw",
+  "fr",
   "es",
   "it",
   "tr",
@@ -80,6 +82,7 @@ export default {
   en: EN,
   cn: CN,
   tw: TW,
+  fr: FR,
   es: ES,
   it: IT,
   tr: TR,

+ 1 - 0
app/locales/it.ts

@@ -75,6 +75,7 @@ const it: LocaleType = {
         cn: "简体中文",
         en: "English",
         tw: "繁體中文",
+        fr: "Français",
         es: "Español",
         it: "Italiano",
         tr: "Türkçe",

+ 1 - 0
app/locales/jp.ts

@@ -75,6 +75,7 @@ const jp: LocaleType = {
         cn: "简体中文",
         en: "English",
         tw: "繁體中文",
+        fr: "Français",
         es: "Español",
         it: "Italiano",
         tr: "Türkçe",

+ 1 - 0
app/locales/ru.ts

@@ -75,6 +75,7 @@ const ru: LocaleType = {
         cn: "简体中文",
         en: "English",
         tw: "繁體中文",
+        fr: "Français",
         es: "Español",
         it: "Italiano",
         tr: "Türkçe",

+ 1 - 0
app/locales/tr.ts

@@ -75,6 +75,7 @@ const tr: LocaleType = {
         cn: "简体中文",
         en: "English",
         tw: "繁體中文",
+        fr: "Français",
         es: "Español",
         it: "Italiano",
         tr: "Türkçe",

+ 1 - 0
app/locales/tw.ts

@@ -73,6 +73,7 @@ const tw: LocaleType = {
         cn: "简体中文",
         en: "English",
         tw: "繁體中文",
+        fr: "Français",
         es: "Español",
         it: "Italiano",
         tr: "Türkçe",

+ 1 - 0
app/locales/vi.ts

@@ -75,6 +75,7 @@ const vi: LocaleType = {
         cn: "简体中文",
         en: "English",
         tw: "繁體中文",
+        fr: "Français",
         es: "Español",
         it: "Italiano",
         tr: "Türkçe",

+ 1 - 1
app/store/chat.ts

@@ -7,7 +7,7 @@ import Locale from "../locales";
 import { showToast } from "../components/ui-lib";
 import { ModelType } from "./config";
 import { createEmptyMask, Mask } from "./mask";
-import { StoreKey } from "../constant";
+import { REQUEST_TIMEOUT_MS, StoreKey } from "../constant";
 import { api, RequestMessage } from "../client/api";
 import { ChatControllerPool } from "../client/controller";
 import { prettyObject } from "../utils/format";

+ 95 - 53
docs/faq-cn.md

@@ -1,38 +1,46 @@
 # 常见问题
 
 ## 如何快速获得帮助?
-1. 询问ChatGPT / Bing / 百度 / Google等。
+
+1. 询问 ChatGPT / Bing / 百度 / Google 等。
 2. 询问网友。请提供问题的背景信息和碰到问题的详细描述。高质量的提问容易获得有用的答案。
 
 # 部署相关问题
 
-各种部署方式详细教程参考:https://rptzik3toh.feishu.cn/docx/XtrdduHwXoSCGIxeFLlcEPsdn8b  
+各种部署方式详细教程参考:https://rptzik3toh.feishu.cn/docx/XtrdduHwXoSCGIxeFLlcEPsdn8b
 
 ## 为什么 Docker 部署版本一直提示更新
+
 Docker 版本相当于稳定版,latest Docker 总是与 latest release version 一致,目前我们的发版频率是一到两天发一次,所以 Docker 版本会总是落后最新的提交一到两天,这在预期内。
 
-## 如何部署在Vercel上
-1. 注册Github账号,fork该项目
-2. 注册Vercel(需手机验证,可以用中国号码),连接你的Github账户
-3. Vercel上新建项目,选择你在Github fork的项目,按需填写环境变量,开始部署。部署之后,你可以在有梯子的条件下,通过vercel提供的域名访问你的项目。
-4. 如果需要在国内无墙访问:在你的域名管理网站,添加一条域名的CNAME记录,指向cname.vercel-dns.com。之后在Vercel上设置你的域名访问。
+## 如何部署在 Vercel 上
+
+1. 注册 Github 账号,fork 该项目
+2. 注册 Vercel(需手机验证,可以用中国号码),连接你的 Github 账户
+3. Vercel 上新建项目,选择你在 Github fork 的项目,按需填写环境变量,开始部署。部署之后,你可以在有梯子的条件下,通过 vercel 提供的域名访问你的项目。
+4. 如果需要在国内无墙访问:在你的域名管理网站,添加一条域名的 CNAME 记录,指向 cname.vercel-dns.com。之后在 Vercel 上设置你的域名访问。
 
 ## 如何修改 Vercel 环境变量
+
 - 进入 vercel 的控制台页面;
 - 选中你的 chatgpt next web 项目;
 - 点击页面头部的 Settings 选项;
 - 找到侧边栏的 Environment Variables 选项;
 - 修改对应的值即可。
-  
-## 环境变量CODE是什么?必须设置吗?
+
+## 环境变量 CODE 是什么?必须设置吗?
+
 这是你自定义的访问密码,你可以选择:
+
 1. 不设置,删除该环境变量即可。谨慎:此时任何人可以访问你的项目。
-2. 部署项目时,设置环境变量CODE(支持多个密码逗号分隔)。设置访问密码后,用户需要在设置界面输入访问密码才可以使用。参见[相关说明](https://github.com/Yidadaa/ChatGPT-Next-Web/blob/main/README_CN.md#%E9%85%8D%E7%BD%AE%E9%A1%B5%E9%9D%A2%E8%AE%BF%E9%97%AE%E5%AF%86%E7%A0%81)
+2. 部署项目时,设置环境变量 CODE(支持多个密码逗号分隔)。设置访问密码后,用户需要在设置界面输入访问密码才可以使用。参见[相关说明](https://github.com/Yidadaa/ChatGPT-Next-Web/blob/main/README_CN.md#%E9%85%8D%E7%BD%AE%E9%A1%B5%E9%9D%A2%E8%AE%BF%E9%97%AE%E5%AF%86%E7%A0%81)
 
 ## 为什么我部署的版本没有流式响应
+
 > 相关讨论:[#386](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/386)
 
 如果你使用 ngnix 反向代理,需要在配置文件中增加下列代码:
+
 ```
 # 不缓存,支持流式输出
 proxy_cache off;  # 关闭缓存
@@ -46,7 +54,9 @@ keepalive_timeout 300;  # 设定keep-alive超时时间为65秒
 如果你是在 netlify 部署,此问题依然等待解决,请耐心等待。
 
 ## 我部署好了,但是无法访问
+
 请检查排除以下问题:
+
 - 服务启动了吗?
 - 端口正确映射了吗?
 - 防火墙开放端口了吗?
@@ -54,54 +64,72 @@ keepalive_timeout 300;  # 设定keep-alive超时时间为65秒
 - 域名正确解析了吗?
 
 ## 什么是代理,如何使用?
-由于OpenAI的IP限制,中国和其他一些国家/地区无法直接连接OpenAI API,需要通过代理。你可以使用代理服务器(正向代理),或者已经设置好的OpenAI API反向代理。
-- 正向代理例子:科学上网梯子。docker部署的情况下,设置环境变量HTTP_PROXY为你的代理地址(例如:10.10.10.10:8002)。
-- 反向代理例子:可以用别人搭建的代理地址,或者通过Cloudflare免费设置。设置项目环境变量BASE_URL为你的代理地址。
+
+由于 OpenAI 的 IP 限制,中国和其他一些国家/地区无法直接连接 OpenAI API,需要通过代理。你可以使用代理服务器(正向代理),或者已经设置好的 OpenAI API 反向代理。
+
+- 正向代理例子:科学上网梯子。docker 部署的情况下,设置环境变量 HTTP_PROXY 为你的代理地址(例如:10.10.10.10:8002)。
+- 反向代理例子:可以用别人搭建的代理地址,或者通过 Cloudflare 免费设置。设置项目环境变量 BASE_URL 为你的代理地址。
 
 ## 国内服务器可以部署吗?
+
 可以但需要解决的问题:
-- 需要代理才能连接github和openAI等网站;
+
+- 需要代理才能连接 github 和 openAI 等网站;
 - 国内服务器要设置域名解析的话需要备案;
-- 国内政策限制代理访问外网/ChatGPT相关应用,可能被封。
+- 国内政策限制代理访问外网/ChatGPT 相关应用,可能被封。
+
+## 为什么 docker 部署后出现网络错误?
 
+详见讨论:https://github.com/Yidadaa/ChatGPT-Next-Web/issues/1569
 
 # 使用相关问题
 
 ## 为什么会一直提示“出错了,稍后重试吧”
+
 原因可能有很多,请依次排查:
+
 - 请先检查你的代码版本是否为最新版本,更新到最新版本后重试;
 - 请检查 api key 是否设置正确,环境变量名称必须为全大写加下划线;
 - 请检查 api key 是否可用;
 - 如果经历了上述步骤依旧无法确定问题,请在 issue 区提交一个新 issue,并附上 vercel 的 runtime log 或者 docker 运行时的 log。
 
 ## 为什么 ChatGPT 的回复会乱码
+
 设置界面 - 模型设置项中,有一项为 `temperature`,如果此值大于 1,那么就有可能造成回复乱码,将其调回 1 以内即可。
 
 ## 使用时提示“现在是未授权状态,请在设置页输入访问密码”?
-项目通过环境变量CODE设置了访问密码。第一次使用时,需要到设置中,输入访问码才可以使用。
+
+项目通过环境变量 CODE 设置了访问密码。第一次使用时,需要到设置中,输入访问码才可以使用。
 
 ## 使用时提示"You exceeded your current quota, ..."
-API KEY有问题。余额不足。
+
+API KEY 有问题。余额不足。
 
 # 网络服务相关问题
-## Cloudflare是什么?
-Cloudflare(CF)是一个提供CDN,域名管理,静态页面托管,边缘计算函数部署等的网络服务供应商。常见的用途:购买和/或托管你的域名(解析、动态域名等),给你的服务器套上CDN(可以隐藏ip免被墙),部署网站(CF Pages)。CF免费提供大多数服务。
 
-## Vercel是什么?
-Vercel 是一个全球化的云平台,旨在帮助开发人员更快地构建和部署现代 Web 应用程序。本项目以及许多Web应用可以一键免费部署在Vercel上。无需懂代码,无需懂linux,无需服务器,无需付费,无需设置OpenAI API代理。缺点是需要绑定域名才可以在国内无墙访问。
+## Cloudflare 是什么?
+
+Cloudflare(CF)是一个提供 CDN,域名管理,静态页面托管,边缘计算函数部署等的网络服务供应商。常见的用途:购买和/或托管你的域名(解析、动态域名等),给你的服务器套上 CDN(可以隐藏 ip 免被墙),部署网站(CF Pages)。CF 免费提供大多数服务。
+
+## Vercel 是什么?
+
+Vercel 是一个全球化的云平台,旨在帮助开发人员更快地构建和部署现代 Web 应用程序。本项目以及许多 Web 应用可以一键免费部署在 Vercel 上。无需懂代码,无需懂 linux,无需服务器,无需付费,无需设置 OpenAI API 代理。缺点是需要绑定域名才可以在国内无墙访问。
 
 ## 如何获得一个域名?
-1. 自己去域名供应商处注册,国外有Namesilo(支持支付宝), Cloudflare等等,国内有万网等等;
+
+1. 自己去域名供应商处注册,国外有 Namesilo(支持支付宝), Cloudflare 等等,国内有万网等等;
 2. 免费的域名供应商:eu.org(二级域名)等;
 3. 问朋友要一个免费的二级域名。
 
 ## 如何获得一台服务器
+
 - 国外服务器供应商举例:亚马逊云,谷歌云,Vultr,Bandwagon,Hostdare,等等;
-国外服务器事项:服务器线路影响国内访问速度,推荐CN2 GIA和CN2线路的服务器。若服务器在国内访问困难(丢包严重等),可以尝试套CDN(Cloudflare等供应商)。
+  国外服务器事项:服务器线路影响国内访问速度,推荐 CN2 GIA  CN2 线路的服务器。若服务器在国内访问困难(丢包严重等),可以尝试套 CDN(Cloudflare 等供应商)。
 - 国内服务器供应商:阿里云,腾讯等;
-国内服务器事项:解析域名需要备案;国内服务器带宽较贵;访问国外网站(Github, openAI等)需要代理。
+  国内服务器事项:解析域名需要备案;国内服务器带宽较贵;访问国外网站(Github, openAI 等)需要代理。
 
 ## 什么情况下服务器要备案?
+
 在中国大陆经营的网站按监管要求需要备案。实际操作中,服务器位于国内且有域名解析的情况下,服务器供应商会执行监管的备案要求,否则会关停服务。通常的规则如下:
 |服务器位置|域名供应商|是否需要备案|
 |---|---|---|
@@ -112,36 +140,47 @@ Vercel 是一个全球化的云平台,旨在帮助开发人员更快地构建
 
 换服务器供应商后需要转备案。
 
-# OpenAI相关问题
-## 如何注册OpenAI账号?
-去chat.openai.com注册。你需要:
-- 一个良好的梯子(OpenAI支持地区原生IP地址)
-- 一个支持的邮箱(例如Gmail或者公司/学校邮箱,非Outlook或qq邮箱)
-- 接收短信认证的方式(例如SMS-activate网站)
+# OpenAI 相关问题
+
+## 如何注册 OpenAI 账号?
+
+去 chat.openai.com 注册。你需要:
+
+- 一个良好的梯子(OpenAI 支持地区原生 IP 地址)
+- 一个支持的邮箱(例如 Gmail 或者公司/学校邮箱,非 Outlook 或 qq 邮箱)
+- 接收短信认证的方式(例如 SMS-activate 网站)
+
+## 怎么开通 OpenAI API? 怎么查询 API 余额?
 
-## 怎么开通OpenAI API? 怎么查询API余额?
 官网地址(需梯子):https://platform.openai.com/account/usage
-有网友搭建了无需梯子的余额查询代理,请询问网友获取。请鉴别来源是否可靠,以免API Key泄露。
+有网友搭建了无需梯子的余额查询代理,请询问网友获取。请鉴别来源是否可靠,以免 API Key 泄露。
+
+## 我新注册的 OpenAI 账号怎么没有 API 余额?
+
+(4 月 6 日更新)新注册账号通常会在 24 小时后显示 API 余额。当前新注册账号赠送 5 美元余额。
 
-## 我新注册的OpenAI账号怎么没有API余额?
-(4月6日更新)新注册账号通常会在24小时后显示API余额。当前新注册账号赠送5美元余额。
+## 如何给 OpenAI API 充值?
 
-## 如何给OpenAI API充值?
-OpenAI只接受指定地区的信用卡(中国信用卡无法使用)。一些途径举例:
-1. Depay虚拟信用卡
+OpenAI 只接受指定地区的信用卡(中国信用卡无法使用)。一些途径举例:
+
+1. Depay 虚拟信用卡
 2. 申请国外信用卡
 3. 网上找人代充
 
-## 如何使用GPT-4的API访问?
-- GPT-4的API访问需要单独申请。到以下地址填写你的信息进入申请队列waitlist(准备好你的OpenAI组织ID):https://openai.com/waitlist/gpt-4-api
-之后等待邮件消息。
+## 如何使用 GPT-4 的 API 访问?
+
+- GPT-4 的 API 访问需要单独申请。到以下地址填写你的信息进入申请队列 waitlist(准备好你的 OpenAI 组织 ID):https://openai.com/waitlist/gpt-4-api
+  之后等待邮件消息。
 - 开通 ChatGPT Plus 不代表有 GPT-4 权限,两者毫无关系。
 
 ## 如何使用 Azure OpenAI 接口
+
 请参考:[#371](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/371)
 
 ## 为什么我的 Token 消耗得这么快?
+
 > 相关讨论:[#518](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/518)
+
 - 如果你有 GPT 4 的权限,并且日常在使用 GPT 4 api,那么由于 GPT 4 价格是 GPT 3.5 的 15 倍左右,你的账单金额会急速膨胀;
 - 如果你在使用 GPT 3.5,并且使用频率并不高,仍然发现自己的账单金额在飞快增加,那么请马上按照以下步骤排查:
   - 去 openai 官网查看你的 api key 消费记录,如果你的 token 每小时都有消费,并且每次都消耗了上万 token,那你的 key 一定是泄露了,请立即删除重新生成。**不要在乱七八糟的网站上查余额。**
@@ -149,17 +188,20 @@ OpenAI只接受指定地区的信用卡(中国信用卡无法使用)。一
 - 通过上述两个方法就可以定位到你的 token 被快速消耗的原因:
   - 如果 openai 消费记录异常,但是 docker 日志没有问题,那么说明是 api key 泄露;
   - 如果 docker 日志发现大量 got access code 爆破记录,那么就是密码被爆破了。
- 
-## API是怎么计费的?
-OpenAI网站计费说明:https://openai.com/pricing#language-models  
-OpenAI根据token数收费,1000个token通常可代表750个英文单词,或500个汉字。输入(Prompt)和输出(Completion)分别统计费用。  
-|模型|用户输入(Prompt)计费|模型输出(Completion)计费|每次交互最大token数|
+
+## API 是怎么计费的?
+
+OpenAI 网站计费说明:https://openai.com/pricing#language-models  
+OpenAI 根据 token 数收费,1000 个 token 通常可代表 750 个英文单词,或 500 个汉字。输入(Prompt)和输出(Completion)分别统计费用。  
+|模型|用户输入(Prompt)计费|模型输出(Completion)计费|每次交互最大 token 数|
 |----|----|----|----|
-|gpt-3.5|$0.002 / 1千tokens|$0.002 / 1千tokens|4096|
-|gpt-4|$0.03 / 1千tokens|$0.06 / 1千tokens|8192|
-|gpt-4-32K|$0.06 / 1千tokens|$0.12 / 1千tokens|32768|
-
-## gpt-3.5-turbo和gpt3.5-turbo-0301(或者gpt3.5-turbo-mmdd)模型有什么区别?
-官方文档说明:https://platform.openai.com/docs/models/gpt-3-5  
-- gpt-3.5-turbo是最新的模型,会不断得到更新。
-- gpt-3.5-turbo-0301是3月1日定格的模型快照,不会变化,预期3个月后被新快照替代。
+|gpt-3.5|$0.002 / 1 千 tokens|$0.002 / 1 千 tokens|4096|
+|gpt-4|$0.03 / 1 千 tokens|$0.06 / 1 千 tokens|8192|
+|gpt-4-32K|$0.06 / 1 千 tokens|$0.12 / 1 千 tokens|32768|
+
+## gpt-3.5-turbo 和 gpt3.5-turbo-0301(或者 gpt3.5-turbo-mmdd)模型有什么区别?
+
+官方文档说明:https://platform.openai.com/docs/models/gpt-3-5
+
+- gpt-3.5-turbo 是最新的模型,会不断得到更新。
+- gpt-3.5-turbo-0301 是 3 月 1 日定格的模型快照,不会变化,预期 3 个月后被新快照替代。

+ 3 - 3
package.json

@@ -4,11 +4,11 @@
   "private": false,
   "license": "Anti 996",
   "scripts": {
-    "dev": "yarn fetch && next dev",
-    "build": "yarn fetch && next build",
+    "dev": "next dev",
+    "build": "next build",
     "start": "next start",
     "lint": "next lint",
-    "fetch": "node ./scripts/fetch-prompts.mjs",
+    "prompts": "node ./scripts/fetch-prompts.mjs",
     "prepare": "husky install",
     "proxy-dev": "sh ./scripts/init-proxy.sh && proxychains -f ./scripts/proxychains.conf yarn dev"
   },

File diff suppressed because it is too large
+ 4 - 0
public/prompts.json


+ 18 - 5
scripts/fetch-prompts.mjs

@@ -2,7 +2,7 @@ import fetch from "node-fetch";
 import fs from "fs/promises";
 
 const RAW_FILE_URL = "https://raw.githubusercontent.com/";
-const MIRRORF_FILE_URL = "https://raw.fgit.ml/";
+const MIRRORF_FILE_URL = "http://raw.fgit.ml/";
 
 const RAW_CN_URL = "PlexPt/awesome-chatgpt-prompts-zh/main/prompts-zh.json";
 const CN_URL = MIRRORF_FILE_URL + RAW_CN_URL;
@@ -10,10 +10,12 @@ const RAW_EN_URL = "f/awesome-chatgpt-prompts/main/prompts.csv";
 const EN_URL = MIRRORF_FILE_URL + RAW_EN_URL;
 const FILE = "./public/prompts.json";
 
+const ignoreWords = ["涩涩", "魅魔"];
+
 const timeoutPromise = (timeout) => {
   return new Promise((resolve, reject) => {
     setTimeout(() => {
-      reject(new Error('Request timeout'));
+      reject(new Error("Request timeout"));
     }, timeout);
   });
 };
@@ -21,10 +23,16 @@ const timeoutPromise = (timeout) => {
 async function fetchCN() {
   console.log("[Fetch] fetching cn prompts...");
   try {
-    // const raw = await (await fetch(CN_URL)).json();
     const response = await Promise.race([fetch(CN_URL), timeoutPromise(5000)]);
     const raw = await response.json();
-    return raw.map((v) => [v.act, v.prompt]);
+    return raw
+      .map((v) => [v.act, v.prompt])
+      .filter(
+        (v) =>
+          v[0] &&
+          v[1] &&
+          ignoreWords.every((w) => !v[0].includes(w) && !v[1].includes(w)),
+      );
   } catch (error) {
     console.error("[Fetch] failed to fetch cn prompts", error);
     return [];
@@ -40,7 +48,12 @@ async function fetchEN() {
     return raw
       .split("\n")
       .slice(1)
-      .map((v) => v.split('","').map((v) => v.replace(/^"|"$/g, '').replaceAll('""','"')));
+      .map((v) =>
+        v
+          .split('","')
+          .map((v) => v.replace(/^"|"$/g, "").replaceAll('""', '"'))
+          .filter((v) => v[0] && v[1]),
+      );
   } catch (error) {
     console.error("[Fetch] failed to fetch en prompts", error);
     return [];

Some files were not shown because too many files changed in this diff