Ver código fonte

Merge pull request #1584 from Yidadaa/bugfix-0518

fix: #1571 #1578 handle more error code
Yifei Zhang 1 ano atrás
pai
commit
9ef6713aa1

+ 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

+ 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",

+ 14 - 8
app/client/platforms/openai.ts

@@ -71,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;
@@ -83,19 +87,21 @@ export class ChatGPTApi implements LLMApi {
           async onopen(res) {
             clearTimeout(requestTimeoutId);
             if (
-              res.ok &&
-              res.headers.get("content-type") !== EventStreamContentType
+              !res.ok ||
+              res.headers.get("content-type") !== EventStreamContentType ||
+              res.status !== 200
             ) {
-              responseText += await res.clone().json();
-              return finish();
-            }
-            if (res.status === 401) {
               let extraInfo = { error: undefined };
               try {
                 extraInfo = await res.clone().json();
               } catch {}
 
-              responseText += "\n\n" + Locale.Error.Unauthorized;
+              if (res.status === 401) {
+                if (responseText.length > 0) {
+                  responseText += "\n\n";
+                }
+                responseText += Locale.Error.Unauthorized;
+              }
 
               if (extraInfo.error) {
                 responseText += "\n\n" + prettyObject(extraInfo);

+ 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 - 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"
   },

Diferenças do arquivo suprimidas por serem muito extensas
+ 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 [];

Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff