diff --git a/.env b/.env index 53fd4d07ce..9a7a52f27f 100644 --- a/.env +++ b/.env @@ -1,7 +1,10 @@ # Glob API URL VITE_GLOB_API_URL=/api -VITE_APP_API_BASE_URL=http://localhost:3002/ +VITE_APP_API_BASE_URL=http://127.0.0.1:3002/ # Whether long replies are supported, which may result in higher API fees VITE_GLOB_OPEN_LONG_REPLY=false + +# When you want to use PWA +VITE_GLOB_APP_PWA=false diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000000..cbd43f677d --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,25 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "chrome", + "request": "launch", + "name": "Launch Web App", + "url": "http://localhost:1002", + "webRoot": "${workspaceFolder}" + }, + { + "type": "node", + "request": "launch", + "name": "Launch Service Server", + "runtimeExecutable": "${workspaceFolder}/service/node_modules/.bin/esno", + "skipFiles": ["/**"], + "program": "${workspaceFolder}/service/src/index.ts", + "outFiles": ["${workspaceFolder}/service/**/*.js"], + "envFile": "${workspaceFolder}/service/.env" + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 084cd4564a..005a2a6811 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -37,6 +37,7 @@ "linkify", "logprobs", "mdhljs", + "mila", "nodata", "OPENAI", "pinia", diff --git a/CHANGELOG.md b/CHANGELOG.md index aa301bb4c8..1879753c88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,114 @@ +## v2.10.8 + +`2023-03-23` + +如遇问题,请删除 `node_modules` 重新安装依赖。 + +## Feature +- 显示回复消息原文的选项 [[yilozt](https://github.com/Chanzhaoyu/chatgpt-web/pull/672)] +- 添加单 `IP` 每小时请求限制。环境变量: `MAX_REQUEST_PER_HOUR` [[zhuxindong ](https://github.com/Chanzhaoyu/chatgpt-web/pull/718)] +- 前端添加角色设定,仅 `API` 方式可见 [[quzard](https://github.com/Chanzhaoyu/chatgpt-web/pull/768)] +- `OPENAI_API_MODEL` 变量现在对 `ChatGPTUnofficialProxyAPI` 也生效,注意:`Token` 和 `API` 的模型命名不一致,不能直接填入 `gpt-3.5` 或者 `gpt-4` [[hncboy](https://github.com/Chanzhaoyu/chatgpt-web/pull/632)] +- 添加繁体中文 `Prompts` [[PeterDaveHello](https://github.com/Chanzhaoyu/chatgpt-web/pull/796)] + +## Enhancement +- 重置回答时滚动定位至该回答 [[shunyue1320](https://github.com/Chanzhaoyu/chatgpt-web/pull/781)] +- 当 `API` 是 `gpt-4` 时增加可用的 `Max Tokens` [[simonwu53](https://github.com/Chanzhaoyu/chatgpt-web/pull/729)] +- 判断和忽略回复字符 [[liut](https://github.com/Chanzhaoyu/chatgpt-web/pull/474)] +- 切换会话时,自动聚焦输入框 [[JS-an](https://github.com/Chanzhaoyu/chatgpt-web/pull/735)] +- 渲染的链接新窗口打开 +- 查询余额可选 `API_BASE_URL` 代理地址 +- `config` 接口添加验证防止被无限制调用 +- `PWA` 默认不开启,现在需手动修改 `.env` 文件 `VITE_GLOB_APP_PWA` 变量 +- 当网络连接时,刷新页面,`500` 错误页自动跳转到主页 + +## BugFix +- `scrollToBottom` 调回 `scrollToBottomIfAtBottom` [[shunyue1320](https://github.com/Chanzhaoyu/chatgpt-web/pull/771)] +- 重置异常的 `loading` 会话 + +## Common +- 创建 `start.cmd` 在 `windows` 下也可以运行 [vulgatecnn](https://github.com/Chanzhaoyu/chatgpt-web/pull/656)] +- 添加 `visual-studio-code` 中调试配置 [[ChandlerVer5](https://github.com/Chanzhaoyu/chatgpt-web/pull/296)] +- 修复文档中 `docker` 端口为本地 [[kilvn](https://github.com/Chanzhaoyu/chatgpt-web/pull/802)] +## Other +- 依赖更新 + + +## v2.10.7 + +`2023-03-17` + +## BugFix +- 回退 `chatgpt` 版本,原因:导致 `OPENAI_API_BASE_URL` 代理失效 +- 修复缺省状态的 `usingContext` 默认值 + +## v2.10.6 + +`2023-03-17` + +## Feature +- 显示 `API` 余额 [[pzcn](https://github.com/Chanzhaoyu/chatgpt-web/pull/582)] + +## Enhancement +- 美化滚动条样式和 `UI` 保持一致 [[haydenull](https://github.com/Chanzhaoyu/chatgpt-web/pull/617)] +- 优化移动端 `Prompt` 样式 [[CornerSkyless](https://github.com/Chanzhaoyu/chatgpt-web/pull/608)] +- 上下文开关改为全局开关,现在记录在本地缓存中 +- 配置信息按接口类型显示 + +## Perf +- 优化函数方法 [[kirklin](https://github.com/Chanzhaoyu/chatgpt-web/pull/583)] +- 字符错误 [[pdsuwwz](https://github.com/Chanzhaoyu/chatgpt-web/pull/585)] +- 文档描述错误 [[lizhongyuan3](https://github.com/Chanzhaoyu/chatgpt-web/pull/636)] + +## BugFix +- 修复 `Prompt` 导入、导出兼容性错误 +- 修复 `highlight.js` 控制台兼容性警告 + +## Other +- 依赖更新 + +## v2.10.5 + +`2023-03-13` + +更新依赖,`access_token` 默认代理为 [acheong08](https://github.com/acheong08) 的 `https://bypass.duti.tech/api/conversation` + +## Feature +- `Prompt` 商店在线导入可以导入两种 `recommend.json`里提到的模板 [simonwu53](https://github.com/Chanzhaoyu/chatgpt-web/pull/521) +- 支持 `HTTPS_PROXY` [whatwewant](https://github.com/Chanzhaoyu/chatgpt-web/pull/308) +- `Prompt` 添加查询筛选 + +## Enhancement +- 调整输入框最大行数 [yi-ge](https://github.com/Chanzhaoyu/chatgpt-web/pull/502) +- 优化 `docker` 打包 [whatwewant](https://github.com/Chanzhaoyu/chatgpt-web/pull/520) +- `Prompt` 添加翻译和优化布局 +- 「繁体中文」补全和审阅 [PeterDaveHello](https://github.com/Chanzhaoyu/chatgpt-web/pull/542) +- 语言选择调整为下路框形式 +- 权限输入框类型调整为密码形式 + +## BugFix +- `JSON` 导入检查 [Nothing1024](https://github.com/Chanzhaoyu/chatgpt-web/pull/523) +- 修复 `AUTH_SECRET_KEY` 模式下跨域异常并添加对 `node.js 19` 版本的支持 [yi-ge](https://github.com/Chanzhaoyu/chatgpt-web/pull/499) +- 确定清空上下文时不应该重置会话标题 + +## Other +- 调整文档 +- 更新依赖 + +## v2.10.4 + +`2023-03-11` + +## Feature +- 感谢 [Nothing1024](https://github.com/Chanzhaoyu/chatgpt-web/pull/268) 添加 `Prompt` 模板和 `Prompt` 商店支持 + +## Enhancement +- 设置添加关闭按钮[#495] + +## Demo + +![Prompt](https://camo.githubusercontent.com/6a51af751eb29238cb7ef4f8fbd89f63db837562f97f33273095424e62dc9194/68747470733a2f2f73312e6c6f63696d672e636f6d2f323032332f30332f30342f333036326665633163613562632e676966) + ## v2.10.3 `2023-03-10` diff --git a/Dockerfile b/Dockerfile index 90a0f09344..1c73771565 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,28 +1,56 @@ # build front-end -FROM node:lts-alpine AS builder +FROM node:lts-alpine AS frontend + +RUN npm install pnpm -g -COPY ./ /app WORKDIR /app -RUN apk add --no-cache git \ - && npm install pnpm -g \ - && pnpm install \ - && pnpm run build \ - && rm -rf /root/.npm /root/.pnpm-store /usr/local/share/.cache /tmp/* +COPY ./package.json /app + +COPY ./pnpm-lock.yaml /app + +RUN pnpm install + +COPY . /app + +RUN pnpm run build + +# build backend +FROM node:lts-alpine as backend + +RUN npm install pnpm -g + +WORKDIR /app + +COPY /service/package.json /app + +COPY /service/pnpm-lock.yaml /app + +RUN pnpm install + +COPY /service /app + +RUN pnpm build # service FROM node:lts-alpine -COPY /service /app -COPY --from=builder /app/dist /app/public +RUN npm install pnpm -g WORKDIR /app -RUN apk add --no-cache git \ - && npm install pnpm -g \ - && pnpm install --only=production \ - && rm -rf /root/.npm /root/.pnpm-store /usr/local/share/.cache /tmp/* +COPY /service/package.json /app + +COPY /service/pnpm-lock.yaml /app + +RUN pnpm install --production && rm -rf /root/.npm /root/.pnpm-store /usr/local/share/.cache /tmp/* + +COPY /service /app + +COPY --from=frontend /app/dist /app/public + +COPY --from=backend /app/build /app/build EXPOSE 3002 -CMD ["pnpm", "run", "start"] +CMD ["pnpm", "run", "prod"] diff --git a/README.en.md b/README.en.md index db9044935d..555ec7d113 100644 --- a/README.en.md +++ b/README.en.md @@ -174,6 +174,8 @@ pnpm dev - `TIMEOUT_MS` timeout, in milliseconds, optional - `SOCKS_PROXY_HOST` optional, effective with SOCKS_PROXY_PORT - `SOCKS_PROXY_PORT` optional, effective with SOCKS_PROXY_HOST +- `HTTPS_PROXY` optional, support http,https, socks5 +- `ALL_PROXY` optional, support http,https, socks5 ![docker](./docs/docker.png) @@ -183,10 +185,10 @@ pnpm dev docker build -t chatgpt-web . # foreground operation -docker run --name chatgpt-web --rm -it -p 3002:3002 --env OPENAI_API_KEY=your_api_key chatgpt-web +docker run --name chatgpt-web --rm -it -p 127.0.0.1:3002:3002 --env OPENAI_API_KEY=your_api_key chatgpt-web # background operation -docker run --name chatgpt-web -d -p 3002:3002 --env OPENAI_API_KEY=your_api_key chatgpt-web +docker run --name chatgpt-web -d -p 127.0.0.1:3002:3002 --env OPENAI_API_KEY=your_api_key chatgpt-web # running address http://localhost:3002/ @@ -203,7 +205,7 @@ services: app: image: chenzhaoyu94/chatgpt-web # always use latest, pull the tag image again when updating ports: - - 3002:3002 + - 127.0.0.1:3002:3002 environment: # one of two OPENAI_API_KEY: xxxxxx @@ -223,6 +225,8 @@ services: SOCKS_PROXY_HOST: xxxx # socks proxy port, optional, effective with SOCKS_PROXY_HOST SOCKS_PROXY_PORT: xxxx + # HTTPS Proxy,optional, support http, https, socks5 + HTTPS_PROXY: http://xxx:7890 ``` The `OPENAI_API_BASE_URL` is optional and only used when setting the `OPENAI_API_KEY`. The `OPENAI_API_MODEL` is optional and only used when setting the `OPENAI_API_KEY`. @@ -245,6 +249,8 @@ The `OPENAI_API_MODEL` is optional and only used when setting the `OPENAI_API_KE | `API_REVERSE_PROXY` | Optional, only for `Web API` | Reverse proxy address for `Web API`. [Details](https://github.com/transitive-bullshit/chatgpt-api#reverse-proxy) | | `SOCKS_PROXY_HOST` | Optional, effective with `SOCKS_PROXY_PORT` | Socks proxy. | | `SOCKS_PROXY_PORT` | Optional, effective with `SOCKS_PROXY_HOST` | Socks proxy port. | +| `HTTPS_PROXY` | Optional | HTTPS Proxy. | +| `ALL_PROXY` | Optional | ALL Proxy. | > Note: Changing environment variables in Railway will cause re-deployment. @@ -271,7 +277,7 @@ PS: You can also run `pnpm start` directly on the server without packaging. #### Frontend webpage -1. Refer to the root directory `.env.example` file content to create `.env` file, modify `VITE_APP_API_BASE_URL` in `.env` at the root directory to your actual backend interface address. +1. Refer to the root directory `.env.example` file content to create `.env` file, modify `VITE_GLOB_API_URL` in `.env` at the root directory to your actual backend interface address. 2. Run the following command in the root directory and then copy the files in the `dist` folder to the root directory of your website service. [Reference information](https://cn.vitejs.dev/guide/static-deploy.html#building-the-app) diff --git a/README.md b/README.md index 73569b9e84..f0cdc6b7fd 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ - [测试环境运行](#测试环境运行) - [后端服务](#后端服务) - [前端网页](#前端网页) + - [环境变量](#环境变量) - [打包](#打包) - [使用 Docker](#使用-docker) - [Docker 参数示例](#docker-参数示例) @@ -51,7 +52,12 @@ 1. `ChatGPTAPI` 使用 `gpt-3.5-turbo-0301` 通过官方`OpenAI`补全`API`模拟`ChatGPT`(最稳健的方法,但它不是免费的,并且没有使用针对聊天进行微调的模型) 2. `ChatGPTUnofficialProxyAPI` 使用非官方代理服务器访问 `ChatGPT` 的后端`API`,绕过`Cloudflare`(使用真实的的`ChatGPT`,非常轻量级,但依赖于第三方服务器,并且有速率限制) -[查看详情](https://github.com/Chanzhaoyu/chatgpt-web/issues/138) +警告: +1. 你应该首先使用 `API` 方式 +2. 使用 `API` 时,如果网络不通,那是国内被墙了,你需要自建代理,绝对不要使用别人的公开代理,那是危险的。 +3. 使用 `accessToken` 方式时反向代理将向第三方暴露您的访问令牌,这样做应该不会产生任何不良影响,但在使用这种方法之前请考虑风险。 +4. 使用 `accessToken` 时,不管你是国内还是国外的机器,都会使用代理。默认代理为 [acheong08](https://github.com/acheong08) 大佬的 `https://bypass.duti.tech/api/conversation`,这不是后门也不是监听,除非你有能力自己翻过 `CF` 验证,用前请知悉。[社区代理](https://github.com/transitive-bullshit/chatgpt-api#reverse-proxy)(注意:只有这两个是推荐,其他第三方来源,请自行甄别) +5. 把项目发布到公共网络时,你应该设置 `AUTH_SECRET_KEY` 变量添加你的密码访问权限,你也应该修改 `index.html` 中的 `title`,防止被关键词搜索到。 切换方式: 1. 进入 `service/.env.example` 文件,复制内容到 `service/.env` 文件 @@ -59,21 +65,12 @@ 3. 使用 `Web API` 请填写 `OPENAI_ACCESS_TOKEN` 字段 [(获取 accessToken)](https://chat.openai.com/api/auth/session) 4. 同时存在时以 `OpenAI API Key` 优先 -反向代理: - -`ChatGPTUnofficialProxyAPI`时可用,[详情](https://github.com/transitive-bullshit/chatgpt-api#reverse-proxy) - -```shell -# service/.env -API_REVERSE_PROXY= -``` - 环境变量: -全部参数变量请查看或[这里](#docker-参数示例) +全部参数变量请查看或[这里](#环境变量) ``` -/service/.env +/service/.env.example ``` ## 待实现路线 @@ -99,7 +96,7 @@ API_REVERSE_PROXY= ### Node -`node` 需要 `^16 || ^18` 版本(`node >= 14` 需要安装 [fetch polyfill](https://github.com/developit/unfetch#usage-as-a-polyfill)),使用 [nvm](https://github.com/nvm-sh/nvm) 可管理本地多个 `node` 版本 +`node` 需要 `^16 || ^18 || ^19` 版本(`node >= 14` 需要安装 [fetch polyfill](https://github.com/developit/unfetch#usage-as-a-polyfill)),使用 [nvm](https://github.com/nvm-sh/nvm) 可管理本地多个 `node` 版本 ```shell node -v @@ -157,21 +154,34 @@ pnpm start pnpm dev ``` -## 打包 +## 环境变量 -### 使用 Docker +`API` 可用: -#### Docker 参数示例 +- `OPENAI_API_KEY` 和 `OPENAI_ACCESS_TOKEN` 二选一 +- `OPENAI_API_MODEL` 设置模型,可选,默认:`gpt-3.5-turbo` +- `OPENAI_API_BASE_URL` 设置接口地址,可选,默认:`https://api.openai.com` + +`ACCESS_TOKEN` 可用: + +- `OPENAI_ACCESS_TOKEN` 和 `OPENAI_API_KEY` 二选一,同时存在时,`OPENAI_API_KEY` 优先 +- `API_REVERSE_PROXY` 设置反向代理,可选,默认:`https://bypass.duti.tech/api/conversation`,[社区](https://github.com/transitive-bullshit/chatgpt-api#reverse-proxy)(注意:只有这两个是推荐,其他第三方来源,请自行甄别) + +通用: -- `OPENAI_API_KEY` 二选一 -- `OPENAI_ACCESS_TOKEN` 二选一,同时存在时,`OPENAI_API_KEY` 优先 -- `OPENAI_API_BASE_URL` 可选,设置 `OPENAI_API_KEY` 时可用 -- `OPENAI_API_MODEL` 可选,设置 `OPENAI_API_KEY` 时可用 -- `API_REVERSE_PROXY` 可选,设置 `OPENAI_ACCESS_TOKEN` 时可用 [参考](#介绍) - `AUTH_SECRET_KEY` 访问权限密钥,可选 +- `MAX_REQUEST_PER_HOUR` 每小时最大请求次数,可选,默认无限 - `TIMEOUT_MS` 超时,单位毫秒,可选 -- `SOCKS_PROXY_HOST` 可选,和 SOCKS_PROXY_PORT 一起时生效 -- `SOCKS_PROXY_PORT` 可选,和 SOCKS_PROXY_HOST 一起时生效 +- `SOCKS_PROXY_HOST` 和 `SOCKS_PROXY_PORT` 一起时生效,可选 +- `SOCKS_PROXY_PORT` 和 `SOCKS_PROXY_HOST` 一起时生效,可选 +- `HTTPS_PROXY` 支持 `http`,`https`, `socks5`,可选 +- `ALL_PROXY` 支持 `http`,`https`, `socks5`,可选 + +## 打包 + +### 使用 Docker + +#### Docker 参数示例 ![docker](./docs/docker.png) @@ -181,10 +191,10 @@ pnpm dev docker build -t chatgpt-web . # 前台运行 -docker run --name chatgpt-web --rm -it -p 3002:3002 --env OPENAI_API_KEY=your_api_key chatgpt-web +docker run --name chatgpt-web --rm -it -p 127.0.0.1:3002:3002 --env OPENAI_API_KEY=your_api_key chatgpt-web # 后台运行 -docker run --name chatgpt-web -d -p 3002:3002 --env OPENAI_API_KEY=your_api_key chatgpt-web +docker run --name chatgpt-web -d -p 127.0.0.1:3002:3002 --env OPENAI_API_KEY=your_api_key chatgpt-web # 运行地址 http://localhost:3002/ @@ -201,26 +211,30 @@ services: app: image: chenzhaoyu94/chatgpt-web # 总是使用 latest ,更新时重新 pull 该 tag 镜像即可 ports: - - 3002:3002 + - 127.0.0.1:3002:3002 environment: # 二选一 - OPENAI_API_KEY: xxxxxx + OPENAI_API_KEY: sk-xxx # 二选一 - OPENAI_ACCESS_TOKEN: xxxxxx + OPENAI_ACCESS_TOKEN: xxx # API接口地址,可选,设置 OPENAI_API_KEY 时可用 - OPENAI_API_BASE_URL: xxxx + OPENAI_API_BASE_URL: xxx # API模型,可选,设置 OPENAI_API_KEY 时可用 - OPENAI_API_MODEL: xxxx + OPENAI_API_MODEL: xxx # 反向代理,可选 API_REVERSE_PROXY: xxx # 访问权限密钥,可选 AUTH_SECRET_KEY: xxx + # 每小时最大请求次数,可选,默认无限 + MAX_REQUEST_PER_HOUR: 0 # 超时,单位毫秒,可选 TIMEOUT_MS: 60000 # Socks代理,可选,和 SOCKS_PROXY_PORT 一起时生效 - SOCKS_PROXY_HOST: xxxx + SOCKS_PROXY_HOST: xxx # Socks代理端口,可选,和 SOCKS_PROXY_HOST 一起时生效 - SOCKS_PROXY_PORT: xxxx + SOCKS_PROXY_PORT: xxx + # HTTPS 代理,可选,支持 http,https,socks5 + HTTPS_PROXY: http://xxx:7890 ``` - `OPENAI_API_BASE_URL` 可选,设置 `OPENAI_API_KEY` 时可用 - `OPENAI_API_MODEL` 可选,设置 `OPENAI_API_KEY` 时可用 @@ -234,6 +248,7 @@ services: | --------------------- | ---------------------- | -------------------------------------------------------------------------------------------------- | | `PORT` | 必填 | 默认 `3002` | `AUTH_SECRET_KEY` | 可选 | 访问权限密钥 | +| `MAX_REQUEST_PER_HOUR` | 可选 | 每小时最大请求次数,可选,默认无限 | | `TIMEOUT_MS` | 可选 | 超时时间,单位毫秒 | | `OPENAI_API_KEY` | `OpenAI API` 二选一 | 使用 `OpenAI API` 所需的 `apiKey` [(获取 apiKey)](https://platform.openai.com/overview) | | `OPENAI_ACCESS_TOKEN` | `Web API` 二选一 | 使用 `Web API` 所需的 `accessToken` [(获取 accessToken)](https://chat.openai.com/api/auth/session) | @@ -242,6 +257,8 @@ services: | `API_REVERSE_PROXY` | 可选,`Web API` 时可用 | `Web API` 反向代理地址 [详情](https://github.com/transitive-bullshit/chatgpt-api#reverse-proxy) | | `SOCKS_PROXY_HOST` | 可选,和 `SOCKS_PROXY_PORT` 一起时生效 | Socks代理 | | `SOCKS_PROXY_PORT` | 可选,和 `SOCKS_PROXY_HOST` 一起时生效 | Socks代理端口 | +| `HTTPS_PROXY` | 可选 | HTTPS 代理,支持 http,https, socks5 | +| `ALL_PROXY` | 可选 | 所有代理 代理,支持 http,https, socks5 | > 注意: `Railway` 修改环境变量会重新 `Deploy` @@ -266,7 +283,7 @@ PS: 不进行打包,直接在服务器上运行 `pnpm start` 也可 #### 前端网页 -1、修改根目录下 `.env` 文件中的 `VITE_APP_API_BASE_URL` 为你的实际后端接口地址 +1、修改根目录下 `.env` 文件中的 `VITE_GLOB_API_URL` 为你的实际后端接口地址 2、根目录下运行以下命令,然后将 `dist` 文件夹内的文件复制到你网站服务的根目录下 diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml index f6082eddb4..dcf99b63cb 100644 --- a/docker-compose/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -7,23 +7,27 @@ services: - 3002:3002 environment: # 二选一 - OPENAI_API_KEY: xxxx + OPENAI_API_KEY: sk-xxx # 二选一 - OPENAI_ACCESS_TOKEN: xxxxxx + OPENAI_ACCESS_TOKEN: xxx # API接口地址,可选,设置 OPENAI_API_KEY 时可用 - OPENAI_API_BASE_URL: xxxx + OPENAI_API_BASE_URL: xxx # API模型,可选,设置 OPENAI_API_KEY 时可用 - OPENAI_API_MODEL: xxxx + OPENAI_API_MODEL: xxx # 反向代理,可选 API_REVERSE_PROXY: xxx # 访问权限密钥,可选 AUTH_SECRET_KEY: xxx + # 每小时最大请求次数,可选,默认无限 + MAX_REQUEST_PER_HOUR: 0 # 超时,单位毫秒,可选 TIMEOUT_MS: 60000 # Socks代理,可选,和 SOCKS_PROXY_PORT 一起时生效 - SOCKS_PROXY_HOST: xxxx + SOCKS_PROXY_HOST: xxx # Socks代理端口,可选,和 SOCKS_PROXY_HOST 一起时生效 - SOCKS_PROXY_PORT: xxxx + SOCKS_PROXY_PORT: xxx + # HTTPS_PROXY 代理,可选 + HTTPS_PROXY: http://xxx:7890 nginx: image: nginx:alpine ports: diff --git a/docs/c1-2.9.0.png b/docs/c1-2.9.0.png new file mode 100644 index 0000000000..48dddfa9f8 Binary files /dev/null and b/docs/c1-2.9.0.png differ diff --git a/docs/c1.png b/docs/c1.png index 48dddfa9f8..0aae034779 100644 Binary files a/docs/c1.png and b/docs/c1.png differ diff --git a/docs/c2-2.9.0.png b/docs/c2-2.9.0.png new file mode 100644 index 0000000000..10482c8bf4 Binary files /dev/null and b/docs/c2-2.9.0.png differ diff --git a/docs/c2.png b/docs/c2.png index 10482c8bf4..5bee3bfad5 100644 Binary files a/docs/c2.png and b/docs/c2.png differ diff --git a/package.json b/package.json index b1aee9fd6c..ae6f4213b0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "chatgpt-web", - "version": "2.10.3", + "version": "2.10.8", "private": false, "description": "ChatGPT Web", "author": "ChenZhaoYu ", @@ -30,7 +30,7 @@ "katex": "^0.16.4", "markdown-it": "^13.0.1", "naive-ui": "^2.34.3", - "pinia": "^2.0.32", + "pinia": "^2.0.33", "vue": "^3.2.47", "vue-i18n": "^9.2.2", "vue-router": "^4.1.6" @@ -43,6 +43,7 @@ "@types/crypto-js": "^4.1.1", "@types/katex": "^0.16.0", "@types/markdown-it": "^12.2.3", + "@types/markdown-it-link-attributes": "^3.0.1", "@types/node": "^18.14.6", "@vitejs/plugin-vue": "^4.0.0", "autoprefixer": "^10.4.13", @@ -52,12 +53,13 @@ "husky": "^8.0.3", "less": "^4.1.3", "lint-staged": "^13.1.2", + "markdown-it-link-attributes": "^4.0.1", "npm-run-all": "^4.1.5", "postcss": "^8.4.21", "rimraf": "^4.2.0", "tailwindcss": "^3.2.7", "typescript": "~4.9.5", - "vite": "^4.1.4", + "vite": "^4.2.0", "vite-plugin-pwa": "^0.14.4", "vue-tsc": "^1.2.0" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c2f4362465..d0f97566f2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,6 +9,7 @@ specifiers: '@types/crypto-js': ^4.1.1 '@types/katex': ^0.16.0 '@types/markdown-it': ^12.2.3 + '@types/markdown-it-link-attributes': ^3.0.1 '@types/node': ^18.14.6 '@vitejs/plugin-vue': ^4.0.0 '@vueuse/core': ^9.13.0 @@ -23,14 +24,15 @@ specifiers: less: ^4.1.3 lint-staged: ^13.1.2 markdown-it: ^13.0.1 + markdown-it-link-attributes: ^4.0.1 naive-ui: ^2.34.3 npm-run-all: ^4.1.5 - pinia: ^2.0.32 + pinia: ^2.0.33 postcss: ^8.4.21 rimraf: ^4.2.0 tailwindcss: ^3.2.7 typescript: ~4.9.5 - vite: ^4.1.4 + vite: ^4.2.0 vite-plugin-pwa: ^0.14.4 vue: ^3.2.47 vue-i18n: ^9.2.2 @@ -58,8 +60,9 @@ devDependencies: '@types/crypto-js': 4.1.1 '@types/katex': 0.16.0 '@types/markdown-it': 12.2.3 + '@types/markdown-it-link-attributes': 3.0.1 '@types/node': 18.14.6 - '@vitejs/plugin-vue': 4.0.0_vite@4.1.4+vue@3.2.47 + '@vitejs/plugin-vue': 4.0.0_vite@4.2.0+vue@3.2.47 autoprefixer: 10.4.13_postcss@8.4.21 axios: 1.3.4 crypto-js: 4.1.1 @@ -67,13 +70,14 @@ devDependencies: husky: 8.0.3 less: 4.1.3 lint-staged: 13.1.2 + markdown-it-link-attributes: 4.0.1 npm-run-all: 4.1.5 postcss: 8.4.21 rimraf: 4.3.0 tailwindcss: 3.2.7_postcss@8.4.21 typescript: 4.9.5 - vite: 4.1.4_4l5pdn5ozbjpiwj3fcgseihr44 - vite-plugin-pwa: 0.14.4_vite@4.1.4 + vite: 4.2.0_4l5pdn5ozbjpiwj3fcgseihr44 + vite-plugin-pwa: 0.14.4_vite@4.2.0 vue-tsc: 1.2.0_typescript@4.9.5 packages: @@ -1531,8 +1535,8 @@ packages: resolution: {integrity: sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==} dev: false - /@esbuild/android-arm/0.16.17: - resolution: {integrity: sha512-N9x1CMXVhtWEAMS7pNNONyA14f71VPQN9Cnavj1XQh6T7bskqiLLrSca4O0Vr8Wdcga943eThxnVp3JLnBMYtw==} + /@esbuild/android-arm/0.17.11: + resolution: {integrity: sha512-CdyX6sRVh1NzFCsf5vw3kULwlAhfy9wVt8SZlrhQ7eL2qBjGbFhRBWkkAzuZm9IIEOCKJw4DXA6R85g+qc8RDw==} engines: {node: '>=12'} cpu: [arm] os: [android] @@ -1540,8 +1544,8 @@ packages: dev: true optional: true - /@esbuild/android-arm64/0.16.17: - resolution: {integrity: sha512-MIGl6p5sc3RDTLLkYL1MyL8BMRN4tLMRCn+yRJJmEDvYZ2M7tmAf80hx1kbNEUX2KJ50RRtxZ4JHLvCfuB6kBg==} + /@esbuild/android-arm64/0.17.11: + resolution: {integrity: sha512-QnK4d/zhVTuV4/pRM4HUjcsbl43POALU2zvBynmrrqZt9LPcLA3x1fTZPBg2RRguBQnJcnU059yKr+bydkntjg==} engines: {node: '>=12'} cpu: [arm64] os: [android] @@ -1549,8 +1553,8 @@ packages: dev: true optional: true - /@esbuild/android-x64/0.16.17: - resolution: {integrity: sha512-a3kTv3m0Ghh4z1DaFEuEDfz3OLONKuFvI4Xqczqx4BqLyuFaFkuaG4j2MtA6fuWEFeC5x9IvqnX7drmRq/fyAQ==} + /@esbuild/android-x64/0.17.11: + resolution: {integrity: sha512-3PL3HKtsDIXGQcSCKtWD/dy+mgc4p2Tvo2qKgKHj9Yf+eniwFnuoQ0OUhlSfAEpKAFzF9N21Nwgnap6zy3L3MQ==} engines: {node: '>=12'} cpu: [x64] os: [android] @@ -1558,8 +1562,8 @@ packages: dev: true optional: true - /@esbuild/darwin-arm64/0.16.17: - resolution: {integrity: sha512-/2agbUEfmxWHi9ARTX6OQ/KgXnOWfsNlTeLcoV7HSuSTv63E4DqtAc+2XqGw1KHxKMHGZgbVCZge7HXWX9Vn+w==} + /@esbuild/darwin-arm64/0.17.11: + resolution: {integrity: sha512-pJ950bNKgzhkGNO3Z9TeHzIFtEyC2GDQL3wxkMApDEghYx5Qers84UTNc1bAxWbRkuJOgmOha5V0WUeh8G+YGw==} engines: {node: '>=12'} cpu: [arm64] os: [darwin] @@ -1567,8 +1571,8 @@ packages: dev: true optional: true - /@esbuild/darwin-x64/0.16.17: - resolution: {integrity: sha512-2By45OBHulkd9Svy5IOCZt376Aa2oOkiE9QWUK9fe6Tb+WDr8hXL3dpqi+DeLiMed8tVXspzsTAvd0jUl96wmg==} + /@esbuild/darwin-x64/0.17.11: + resolution: {integrity: sha512-iB0dQkIHXyczK3BZtzw1tqegf0F0Ab5texX2TvMQjiJIWXAfM4FQl7D909YfXWnB92OQz4ivBYQ2RlxBJrMJOw==} engines: {node: '>=12'} cpu: [x64] os: [darwin] @@ -1576,8 +1580,8 @@ packages: dev: true optional: true - /@esbuild/freebsd-arm64/0.16.17: - resolution: {integrity: sha512-mt+cxZe1tVx489VTb4mBAOo2aKSnJ33L9fr25JXpqQqzbUIw/yzIzi+NHwAXK2qYV1lEFp4OoVeThGjUbmWmdw==} + /@esbuild/freebsd-arm64/0.17.11: + resolution: {integrity: sha512-7EFzUADmI1jCHeDRGKgbnF5sDIceZsQGapoO6dmw7r/ZBEKX7CCDnIz8m9yEclzr7mFsd+DyasHzpjfJnmBB1Q==} engines: {node: '>=12'} cpu: [arm64] os: [freebsd] @@ -1585,8 +1589,8 @@ packages: dev: true optional: true - /@esbuild/freebsd-x64/0.16.17: - resolution: {integrity: sha512-8ScTdNJl5idAKjH8zGAsN7RuWcyHG3BAvMNpKOBaqqR7EbUhhVHOqXRdL7oZvz8WNHL2pr5+eIT5c65kA6NHug==} + /@esbuild/freebsd-x64/0.17.11: + resolution: {integrity: sha512-iPgenptC8i8pdvkHQvXJFzc1eVMR7W2lBPrTE6GbhR54sLcF42mk3zBOjKPOodezzuAz/KSu8CPyFSjcBMkE9g==} engines: {node: '>=12'} cpu: [x64] os: [freebsd] @@ -1594,8 +1598,8 @@ packages: dev: true optional: true - /@esbuild/linux-arm/0.16.17: - resolution: {integrity: sha512-iihzrWbD4gIT7j3caMzKb/RsFFHCwqqbrbH9SqUSRrdXkXaygSZCZg1FybsZz57Ju7N/SHEgPyaR0LZ8Zbe9gQ==} + /@esbuild/linux-arm/0.17.11: + resolution: {integrity: sha512-M9iK/d4lgZH0U5M1R2p2gqhPV/7JPJcRz+8O8GBKVgqndTzydQ7B2XGDbxtbvFkvIs53uXTobOhv+RyaqhUiMg==} engines: {node: '>=12'} cpu: [arm] os: [linux] @@ -1603,8 +1607,8 @@ packages: dev: true optional: true - /@esbuild/linux-arm64/0.16.17: - resolution: {integrity: sha512-7S8gJnSlqKGVJunnMCrXHU9Q8Q/tQIxk/xL8BqAP64wchPCTzuM6W3Ra8cIa1HIflAvDnNOt2jaL17vaW+1V0g==} + /@esbuild/linux-arm64/0.17.11: + resolution: {integrity: sha512-Qxth3gsWWGKz2/qG2d5DsW/57SeA2AmpSMhdg9TSB5Svn2KDob3qxfQSkdnWjSd42kqoxIPy3EJFs+6w1+6Qjg==} engines: {node: '>=12'} cpu: [arm64] os: [linux] @@ -1612,8 +1616,8 @@ packages: dev: true optional: true - /@esbuild/linux-ia32/0.16.17: - resolution: {integrity: sha512-kiX69+wcPAdgl3Lonh1VI7MBr16nktEvOfViszBSxygRQqSpzv7BffMKRPMFwzeJGPxcio0pdD3kYQGpqQ2SSg==} + /@esbuild/linux-ia32/0.17.11: + resolution: {integrity: sha512-dB1nGaVWtUlb/rRDHmuDQhfqazWE0LMro/AIbT2lWM3CDMHJNpLckH+gCddQyhhcLac2OYw69ikUMO34JLt3wA==} engines: {node: '>=12'} cpu: [ia32] os: [linux] @@ -1621,8 +1625,8 @@ packages: dev: true optional: true - /@esbuild/linux-loong64/0.16.17: - resolution: {integrity: sha512-dTzNnQwembNDhd654cA4QhbS9uDdXC3TKqMJjgOWsC0yNCbpzfWoXdZvp0mY7HU6nzk5E0zpRGGx3qoQg8T2DQ==} + /@esbuild/linux-loong64/0.17.11: + resolution: {integrity: sha512-aCWlq70Q7Nc9WDnormntGS1ar6ZFvUpqr8gXtO+HRejRYPweAFQN615PcgaSJkZjhHp61+MNLhzyVALSF2/Q0g==} engines: {node: '>=12'} cpu: [loong64] os: [linux] @@ -1630,8 +1634,8 @@ packages: dev: true optional: true - /@esbuild/linux-mips64el/0.16.17: - resolution: {integrity: sha512-ezbDkp2nDl0PfIUn0CsQ30kxfcLTlcx4Foz2kYv8qdC6ia2oX5Q3E/8m6lq84Dj/6b0FrkgD582fJMIfHhJfSw==} + /@esbuild/linux-mips64el/0.17.11: + resolution: {integrity: sha512-cGeGNdQxqY8qJwlYH1BP6rjIIiEcrM05H7k3tR7WxOLmD1ZxRMd6/QIOWMb8mD2s2YJFNRuNQ+wjMhgEL2oCEw==} engines: {node: '>=12'} cpu: [mips64el] os: [linux] @@ -1639,8 +1643,8 @@ packages: dev: true optional: true - /@esbuild/linux-ppc64/0.16.17: - resolution: {integrity: sha512-dzS678gYD1lJsW73zrFhDApLVdM3cUF2MvAa1D8K8KtcSKdLBPP4zZSLy6LFZ0jYqQdQ29bjAHJDgz0rVbLB3g==} + /@esbuild/linux-ppc64/0.17.11: + resolution: {integrity: sha512-BdlziJQPW/bNe0E8eYsHB40mYOluS+jULPCjlWiHzDgr+ZBRXPtgMV1nkLEGdpjrwgmtkZHEGEPaKdS/8faLDA==} engines: {node: '>=12'} cpu: [ppc64] os: [linux] @@ -1648,8 +1652,8 @@ packages: dev: true optional: true - /@esbuild/linux-riscv64/0.16.17: - resolution: {integrity: sha512-ylNlVsxuFjZK8DQtNUwiMskh6nT0vI7kYl/4fZgV1llP5d6+HIeL/vmmm3jpuoo8+NuXjQVZxmKuhDApK0/cKw==} + /@esbuild/linux-riscv64/0.17.11: + resolution: {integrity: sha512-MDLwQbtF+83oJCI1Cixn68Et/ME6gelmhssPebC40RdJaect+IM+l7o/CuG0ZlDs6tZTEIoxUe53H3GmMn8oMA==} engines: {node: '>=12'} cpu: [riscv64] os: [linux] @@ -1657,8 +1661,8 @@ packages: dev: true optional: true - /@esbuild/linux-s390x/0.16.17: - resolution: {integrity: sha512-gzy7nUTO4UA4oZ2wAMXPNBGTzZFP7mss3aKR2hH+/4UUkCOyqmjXiKpzGrY2TlEUhbbejzXVKKGazYcQTZWA/w==} + /@esbuild/linux-s390x/0.17.11: + resolution: {integrity: sha512-4N5EMESvws0Ozr2J94VoUD8HIRi7X0uvUv4c0wpTHZyZY9qpaaN7THjosdiW56irQ4qnJ6Lsc+i+5zGWnyqWqQ==} engines: {node: '>=12'} cpu: [s390x] os: [linux] @@ -1666,8 +1670,8 @@ packages: dev: true optional: true - /@esbuild/linux-x64/0.16.17: - resolution: {integrity: sha512-mdPjPxfnmoqhgpiEArqi4egmBAMYvaObgn4poorpUaqmvzzbvqbowRllQ+ZgzGVMGKaPkqUmPDOOFQRUFDmeUw==} + /@esbuild/linux-x64/0.17.11: + resolution: {integrity: sha512-rM/v8UlluxpytFSmVdbCe1yyKQd/e+FmIJE2oPJvbBo+D0XVWi1y/NQ4iTNx+436WmDHQBjVLrbnAQLQ6U7wlw==} engines: {node: '>=12'} cpu: [x64] os: [linux] @@ -1675,8 +1679,8 @@ packages: dev: true optional: true - /@esbuild/netbsd-x64/0.16.17: - resolution: {integrity: sha512-/PzmzD/zyAeTUsduZa32bn0ORug+Jd1EGGAUJvqfeixoEISYpGnAezN6lnJoskauoai0Jrs+XSyvDhppCPoKOA==} + /@esbuild/netbsd-x64/0.17.11: + resolution: {integrity: sha512-4WaAhuz5f91h3/g43VBGdto1Q+X7VEZfpcWGtOFXnggEuLvjV+cP6DyLRU15IjiU9fKLLk41OoJfBFN5DhPvag==} engines: {node: '>=12'} cpu: [x64] os: [netbsd] @@ -1684,8 +1688,8 @@ packages: dev: true optional: true - /@esbuild/openbsd-x64/0.16.17: - resolution: {integrity: sha512-2yaWJhvxGEz2RiftSk0UObqJa/b+rIAjnODJgv2GbGGpRwAfpgzyrg1WLK8rqA24mfZa9GvpjLcBBg8JHkoodg==} + /@esbuild/openbsd-x64/0.17.11: + resolution: {integrity: sha512-UBj135Nx4FpnvtE+C8TWGp98oUgBcmNmdYgl5ToKc0mBHxVVqVE7FUS5/ELMImOp205qDAittL6Ezhasc2Ev/w==} engines: {node: '>=12'} cpu: [x64] os: [openbsd] @@ -1693,8 +1697,8 @@ packages: dev: true optional: true - /@esbuild/sunos-x64/0.16.17: - resolution: {integrity: sha512-xtVUiev38tN0R3g8VhRfN7Zl42YCJvyBhRKw1RJjwE1d2emWTVToPLNEQj/5Qxc6lVFATDiy6LjVHYhIPrLxzw==} + /@esbuild/sunos-x64/0.17.11: + resolution: {integrity: sha512-1/gxTifDC9aXbV2xOfCbOceh5AlIidUrPsMpivgzo8P8zUtczlq1ncFpeN1ZyQJ9lVs2hILy1PG5KPp+w8QPPg==} engines: {node: '>=12'} cpu: [x64] os: [sunos] @@ -1702,8 +1706,8 @@ packages: dev: true optional: true - /@esbuild/win32-arm64/0.16.17: - resolution: {integrity: sha512-ga8+JqBDHY4b6fQAmOgtJJue36scANy4l/rL97W+0wYmijhxKetzZdKOJI7olaBaMhWt8Pac2McJdZLxXWUEQw==} + /@esbuild/win32-arm64/0.17.11: + resolution: {integrity: sha512-vtSfyx5yRdpiOW9yp6Ax0zyNOv9HjOAw8WaZg3dF5djEHKKm3UnoohftVvIJtRh0Ec7Hso0RIdTqZvPXJ7FdvQ==} engines: {node: '>=12'} cpu: [arm64] os: [win32] @@ -1711,8 +1715,8 @@ packages: dev: true optional: true - /@esbuild/win32-ia32/0.16.17: - resolution: {integrity: sha512-WnsKaf46uSSF/sZhwnqE4L/F89AYNMiD4YtEcYekBt9Q7nj0DiId2XH2Ng2PHM54qi5oPrQ8luuzGszqi/veig==} + /@esbuild/win32-ia32/0.17.11: + resolution: {integrity: sha512-GFPSLEGQr4wHFTiIUJQrnJKZhZjjq4Sphf+mM76nQR6WkQn73vm7IsacmBRPkALfpOCHsopSvLgqdd4iUW2mYw==} engines: {node: '>=12'} cpu: [ia32] os: [win32] @@ -1720,8 +1724,8 @@ packages: dev: true optional: true - /@esbuild/win32-x64/0.16.17: - resolution: {integrity: sha512-y+EHuSchhL7FjHgvQL/0fnnFmO4T1bhvWANX6gcnqTjtnKWbTvUMCpGnv2+t+31d7RzyEAYAd4u2fnIhHL6N/Q==} + /@esbuild/win32-x64/0.17.11: + resolution: {integrity: sha512-N9vXqLP3eRL8BqSy8yn4Y98cZI2pZ8fyuHx6lKjiG2WABpT2l01TXdzq5Ma2ZUBzfB7tx5dXVhge8X9u0S70ZQ==} engines: {node: '>=12'} cpu: [x64] os: [win32] @@ -2065,6 +2069,12 @@ packages: resolution: {integrity: sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==} dev: false + /@types/markdown-it-link-attributes/3.0.1: + resolution: {integrity: sha512-K8RnNb1q8j7rDOJbMF7AnlhCC/45BjrQ8z3WZWOrvkBIl8u9RXvmBdG/hfpnmK1JhhEZcmFEKWt+ilW1Mly+2Q==} + dependencies: + '@types/markdown-it': 12.2.3 + dev: true + /@types/markdown-it/12.2.3: resolution: {integrity: sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==} dependencies: @@ -2246,14 +2256,14 @@ packages: eslint-visitor-keys: 3.3.0 dev: true - /@vitejs/plugin-vue/4.0.0_vite@4.1.4+vue@3.2.47: + /@vitejs/plugin-vue/4.0.0_vite@4.2.0+vue@3.2.47: resolution: {integrity: sha512-e0X4jErIxAB5oLtDqbHvHpJe/uWNkdpYV83AOG2xo2tEVSzCzewgJMtREZM30wXnM5ls90hxiOtAuVU6H5JgbA==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: vite: ^4.0.0 vue: ^3.2.25 dependencies: - vite: 4.1.4_4l5pdn5ozbjpiwj3fcgseihr44 + vite: 4.2.0_4l5pdn5ozbjpiwj3fcgseihr44 vue: 3.2.47 dev: true @@ -3317,34 +3327,34 @@ packages: is-symbol: 1.0.4 dev: true - /esbuild/0.16.17: - resolution: {integrity: sha512-G8LEkV0XzDMNwXKgM0Jwu3nY3lSTwSGY6XbxM9cr9+s0T/qSV1q1JVPBGzm3dcjhCic9+emZDmMffkwgPeOeLg==} + /esbuild/0.17.11: + resolution: {integrity: sha512-pAMImyokbWDtnA/ufPxjQg0fYo2DDuzAlqwnDvbXqHLphe+m80eF++perYKVm8LeTuj2zUuFXC+xgSVxyoHUdg==} engines: {node: '>=12'} hasBin: true requiresBuild: true optionalDependencies: - '@esbuild/android-arm': 0.16.17 - '@esbuild/android-arm64': 0.16.17 - '@esbuild/android-x64': 0.16.17 - '@esbuild/darwin-arm64': 0.16.17 - '@esbuild/darwin-x64': 0.16.17 - '@esbuild/freebsd-arm64': 0.16.17 - '@esbuild/freebsd-x64': 0.16.17 - '@esbuild/linux-arm': 0.16.17 - '@esbuild/linux-arm64': 0.16.17 - '@esbuild/linux-ia32': 0.16.17 - '@esbuild/linux-loong64': 0.16.17 - '@esbuild/linux-mips64el': 0.16.17 - '@esbuild/linux-ppc64': 0.16.17 - '@esbuild/linux-riscv64': 0.16.17 - '@esbuild/linux-s390x': 0.16.17 - '@esbuild/linux-x64': 0.16.17 - '@esbuild/netbsd-x64': 0.16.17 - '@esbuild/openbsd-x64': 0.16.17 - '@esbuild/sunos-x64': 0.16.17 - '@esbuild/win32-arm64': 0.16.17 - '@esbuild/win32-ia32': 0.16.17 - '@esbuild/win32-x64': 0.16.17 + '@esbuild/android-arm': 0.17.11 + '@esbuild/android-arm64': 0.17.11 + '@esbuild/android-x64': 0.17.11 + '@esbuild/darwin-arm64': 0.17.11 + '@esbuild/darwin-x64': 0.17.11 + '@esbuild/freebsd-arm64': 0.17.11 + '@esbuild/freebsd-x64': 0.17.11 + '@esbuild/linux-arm': 0.17.11 + '@esbuild/linux-arm64': 0.17.11 + '@esbuild/linux-ia32': 0.17.11 + '@esbuild/linux-loong64': 0.17.11 + '@esbuild/linux-mips64el': 0.17.11 + '@esbuild/linux-ppc64': 0.17.11 + '@esbuild/linux-riscv64': 0.17.11 + '@esbuild/linux-s390x': 0.17.11 + '@esbuild/linux-x64': 0.17.11 + '@esbuild/netbsd-x64': 0.17.11 + '@esbuild/openbsd-x64': 0.17.11 + '@esbuild/sunos-x64': 0.17.11 + '@esbuild/win32-arm64': 0.17.11 + '@esbuild/win32-ia32': 0.17.11 + '@esbuild/win32-x64': 0.17.11 dev: true /escalade/3.1.1: @@ -3912,7 +3922,7 @@ packages: resolution: {integrity: sha512-0rcTq621PD5jM/e0a3EJoGC/1TC5ZBCERW82LQuwfGnCa1V8w7dpYH1yNu+SLb6E5dkeCBzKEyLGlFrnr+dUyw==} engines: {node: '>=14.14'} dependencies: - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 jsonfile: 6.1.0 universalify: 2.0.0 dev: true @@ -3922,7 +3932,7 @@ packages: engines: {node: '>=10'} dependencies: at-least-node: 1.0.0 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 jsonfile: 6.1.0 universalify: 2.0.0 dev: true @@ -4083,8 +4093,8 @@ packages: get-intrinsic: 1.2.0 dev: true - /graceful-fs/4.2.10: - resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} + /graceful-fs/4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} dev: true /grapheme-splitter/1.0.4: @@ -4580,7 +4590,7 @@ packages: dependencies: universalify: 2.0.0 optionalDependencies: - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 dev: true /jsonparse/1.3.1: @@ -4615,7 +4625,7 @@ packages: tslib: 2.5.0 optionalDependencies: errno: 0.1.8 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 image-size: 0.5.5 make-dir: 2.1.0 mime: 1.6.0 @@ -4704,7 +4714,7 @@ packages: resolution: {integrity: sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==} engines: {node: '>=4'} dependencies: - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 parse-json: 4.0.0 pify: 3.0.0 strip-bom: 3.0.0 @@ -4848,6 +4858,10 @@ packages: engines: {node: '>=8'} dev: true + /markdown-it-link-attributes/4.0.1: + resolution: {integrity: sha512-pg5OK0jPLg62H4k7M9mRJLT61gUp9nvG0XveKYHMOOluASo9OEF13WlXrpAp2aj35LbedAy3QOCgQCw0tkLKAQ==} + dev: true + /markdown-it/13.0.1: resolution: {integrity: sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q==} hasBin: true @@ -6395,7 +6409,7 @@ packages: vue: 3.2.47 dev: false - /vite-plugin-pwa/0.14.4_vite@4.1.4: + /vite-plugin-pwa/0.14.4_vite@4.2.0: resolution: {integrity: sha512-M7Ct0so8OlouMkTWgXnl8W1xU95glITSKIe7qswZf1tniAstO2idElGCnsrTJ5NPNSx1XqfTCOUj8j94S6FD7Q==} peerDependencies: vite: ^3.1.0 || ^4.0.0 @@ -6405,7 +6419,7 @@ packages: fast-glob: 3.2.12 pretty-bytes: 6.1.0 rollup: 3.18.0 - vite: 4.1.4_4l5pdn5ozbjpiwj3fcgseihr44 + vite: 4.2.0_4l5pdn5ozbjpiwj3fcgseihr44 workbox-build: 6.5.4 workbox-window: 6.5.4 transitivePeerDependencies: @@ -6413,8 +6427,8 @@ packages: - supports-color dev: true - /vite/4.1.4_4l5pdn5ozbjpiwj3fcgseihr44: - resolution: {integrity: sha512-3knk/HsbSTKEin43zHu7jTwYWv81f8kgAL99G5NWBcA1LKvtvcVAC4JjBH1arBunO9kQka+1oGbrMKOjk4ZrBg==} + /vite/4.2.0_4l5pdn5ozbjpiwj3fcgseihr44: + resolution: {integrity: sha512-AbDTyzzwuKoRtMIRLGNxhLRuv1FpRgdIw+1y6AQG73Q5+vtecmvzKo/yk8X/vrHDpETRTx01ABijqUHIzBXi0g==} engines: {node: ^14.18.0 || >=16.0.0} hasBin: true peerDependencies: @@ -6439,7 +6453,7 @@ packages: optional: true dependencies: '@types/node': 18.14.6 - esbuild: 0.16.17 + esbuild: 0.17.11 less: 4.1.3 postcss: 8.4.21 resolve: 1.22.1 diff --git a/service/.env.example b/service/.env.example index 43fe3a616c..ac8e9d6467 100644 --- a/service/.env.example +++ b/service/.env.example @@ -16,6 +16,9 @@ API_REVERSE_PROXY= # timeout TIMEOUT_MS=100000 +# Rate Limit +MAX_REQUEST_PER_HOUR= + # Secret key AUTH_SECRET_KEY= @@ -24,3 +27,7 @@ SOCKS_PROXY_HOST= # Socks Proxy Port SOCKS_PROXY_PORT= + +# HTTPS PROXY +HTTPS_PROXY= + diff --git a/service/package.json b/service/package.json index aa75b7c6a9..d457c7b57f 100644 --- a/service/package.json +++ b/service/package.json @@ -11,12 +11,12 @@ "express" ], "engines": { - "node": "^16 || ^18" + "node": "^16 || ^18 || ^19" }, "scripts": { "start": "esno ./src/index.ts", "dev": "esno watch ./src/index.ts", - "prod": "esno ./build/index.js", + "prod": "node ./build/index.mjs", "build": "pnpm clean && tsup", "clean": "rimraf build", "lint": "eslint .", @@ -24,10 +24,13 @@ "common:cleanup": "rimraf node_modules && rimraf pnpm-lock.yaml" }, "dependencies": { - "chatgpt": "^5.0.9", + "axios": "^1.3.4", + "chatgpt": "^5.1.2", "dotenv": "^16.0.3", "esno": "^0.16.3", "express": "^4.18.2", + "express-rate-limit": "^6.7.0", + "https-proxy-agent": "^5.0.1", "isomorphic-fetch": "^3.0.0", "node-fetch": "^3.3.0", "socks-proxy-agent": "^7.0.0" diff --git a/service/pnpm-lock.yaml b/service/pnpm-lock.yaml index a27fef4e58..04e84d3cd7 100644 --- a/service/pnpm-lock.yaml +++ b/service/pnpm-lock.yaml @@ -4,11 +4,14 @@ specifiers: '@antfu/eslint-config': ^0.35.3 '@types/express': ^4.17.17 '@types/node': ^18.14.6 - chatgpt: ^5.0.9 + axios: ^1.3.4 + chatgpt: ^5.1.2 dotenv: ^16.0.3 eslint: ^8.35.0 esno: ^0.16.3 express: ^4.18.2 + express-rate-limit: ^6.7.0 + https-proxy-agent: ^5.0.1 isomorphic-fetch: ^3.0.0 node-fetch: ^3.3.0 rimraf: ^4.3.0 @@ -17,10 +20,13 @@ specifiers: typescript: ^4.9.5 dependencies: - chatgpt: 5.0.9 + axios: 1.3.4 + chatgpt: 5.1.2 dotenv: 16.0.3 esno: 0.16.3 express: 4.18.2 + express-rate-limit: 6.7.0_express@4.18.2 + https-proxy-agent: 5.0.1 isomorphic-fetch: 3.0.0 node-fetch: 3.3.0 socks-proxy-agent: 7.0.0 @@ -150,8 +156,8 @@ packages: chalk: 2.4.2 js-tokens: 4.0.0 - /@dqbd/tiktoken/0.4.0: - resolution: {integrity: sha512-iaHgmwKAOqowBFZKxelyszoeGLoNw62eOULcmyme1aA1Ymr3JgYl0V7jwpuUm7fksalycZajx3loFn9TRUaviw==} + /@dqbd/tiktoken/1.0.2: + resolution: {integrity: sha512-AjGTBRWsMoVmVeN55NLyupyM8TNamOUBl6tj5t/leLDVup3CFGO9tVagNL1jf3GyZLkWZSTmYVbPQ/M2LEcNzw==} dev: false /@esbuild-kit/cjs-loader/2.4.2: @@ -764,6 +770,10 @@ packages: es-shim-unscopables: 1.0.0 dev: true + /asynckit/0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + dev: false + /atomically/2.0.1: resolution: {integrity: sha512-sxBhVZUFBFhqSAsYMM3X2oaUi2NVDJ8U026FsIusM8gYXls9AYs/eXzgGrufs1Qjpkxi9zunds+75QUFz+m7UQ==} dependencies: @@ -776,6 +786,16 @@ packages: engines: {node: '>= 0.4'} dev: true + /axios/1.3.4: + resolution: {integrity: sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==} + dependencies: + follow-redirects: 1.15.2 + form-data: 4.0.0 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + dev: false + /balanced-match/1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} dev: true @@ -902,12 +922,12 @@ packages: resolution: {integrity: sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==} dev: true - /chatgpt/5.0.9: - resolution: {integrity: sha512-H0MMegLKcYyYh3LeFO4ubIdJSiSAl4rRjTeXf3KjHfGXDM7QZ1EkiTH9RuIoaNzOm8rJTn4QEhrwBbOIpbalxw==} + /chatgpt/5.1.2: + resolution: {integrity: sha512-b/NnDQHDOpouK+gmhUCKcKuvnKEh+DotwXlP6Qgw1mqmkcfVl9Wt3/3noT8jbGJpMEpFOfzexXn5Rehpea1N0w==} engines: {node: '>=14'} hasBin: true dependencies: - '@dqbd/tiktoken': 0.4.0 + '@dqbd/tiktoken': 1.0.2 cac: 6.7.14 conf: 11.0.1 eventsource-parser: 0.0.5 @@ -964,6 +984,13 @@ packages: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} dev: true + /combined-stream/1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + dependencies: + delayed-stream: 1.0.0 + dev: false + /commander/4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} @@ -1080,6 +1107,11 @@ packages: object-keys: 1.1.1 dev: true + /delayed-stream/1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + dev: false + /depd/2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -1710,6 +1742,15 @@ packages: strip-final-newline: 2.0.0 dev: true + /express-rate-limit/6.7.0_express@4.18.2: + resolution: {integrity: sha512-vhwIdRoqcYB/72TK3tRZI+0ttS8Ytrk24GfmsxDXK9o9IhHNO5bXRiXQSExPQ4GbaE5tvIS7j1SGrxsuWs+sGA==} + engines: {node: '>= 12.9.0'} + peerDependencies: + express: ^4 || ^5 + dependencies: + express: 4.18.2 + dev: false + /express/4.18.2: resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==} engines: {node: '>= 0.10.0'} @@ -1850,12 +1891,31 @@ packages: resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==} dev: true + /follow-redirects/1.15.2: + resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + dev: false + /for-each/0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} dependencies: is-callable: 1.2.7 dev: true + /form-data/4.0.0: + resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} + engines: {node: '>= 6'} + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + dev: false + /formdata-polyfill/4.0.10: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} engines: {node: '>=12.20.0'} @@ -2079,6 +2139,16 @@ packages: toidentifier: 1.0.1 dev: false + /https-proxy-agent/5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + dependencies: + agent-base: 6.0.2 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: false + /human-signals/2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} @@ -2906,6 +2976,10 @@ packages: ipaddr.js: 1.9.1 dev: false + /proxy-from-env/1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + dev: false + /punycode/2.3.0: resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} engines: {node: '>=6'} diff --git a/service/src/chatgpt/index.ts b/service/src/chatgpt/index.ts index 9ddebafd63..c702e6ac81 100644 --- a/service/src/chatgpt/index.ts +++ b/service/src/chatgpt/index.ts @@ -3,9 +3,17 @@ import 'isomorphic-fetch' import type { ChatGPTAPIOptions, ChatMessage, SendMessageOptions } from 'chatgpt' import { ChatGPTAPI, ChatGPTUnofficialProxyAPI } from 'chatgpt' import { SocksProxyAgent } from 'socks-proxy-agent' +import httpsProxyAgent from 'https-proxy-agent' import fetch from 'node-fetch' +import axios from 'axios' import { sendResponse } from '../utils' +import { isNotEmptyString } from '../utils/is' import type { ApiModel, ChatContext, ChatGPTUnofficialProxyAPIOptions, ModelConfig } from '../types' +import type { RequestOptions } from './types' + +const { HttpsProxyAgent } = httpsProxyAgent + +dotenv.config() const ErrorCodeMessage: Record = { 401: '[OpenAI] 提供错误的API密钥 | Incorrect API key provided', @@ -16,13 +24,11 @@ const ErrorCodeMessage: Record = { 500: '[OpenAI] 服务器繁忙,请稍后再试 | Internal Server Error', } -dotenv.config() - const timeoutMs: number = !isNaN(+process.env.TIMEOUT_MS) ? +process.env.TIMEOUT_MS : 30 * 1000 let apiModel: ApiModel -if (!process.env.OPENAI_API_KEY && !process.env.OPENAI_ACCESS_TOKEN) +if (!isNotEmptyString(process.env.OPENAI_API_KEY) && !isNotEmptyString(process.env.OPENAI_ACCESS_TOKEN)) throw new Error('Missing OPENAI_API_KEY or OPENAI_ACCESS_TOKEN environment variable') let api: ChatGPTAPI | ChatGPTUnofficialProxyAPI @@ -30,72 +36,70 @@ let api: ChatGPTAPI | ChatGPTUnofficialProxyAPI (async () => { // More Info: https://github.com/transitive-bullshit/chatgpt-api - if (process.env.OPENAI_API_KEY) { + if (isNotEmptyString(process.env.OPENAI_API_KEY)) { + const OPENAI_API_BASE_URL = process.env.OPENAI_API_BASE_URL const OPENAI_API_MODEL = process.env.OPENAI_API_MODEL - const model = (typeof OPENAI_API_MODEL === 'string' && OPENAI_API_MODEL.length > 0) - ? OPENAI_API_MODEL - : 'gpt-3.5-turbo' + const model = isNotEmptyString(OPENAI_API_MODEL) ? OPENAI_API_MODEL : 'gpt-3.5-turbo' const options: ChatGPTAPIOptions = { apiKey: process.env.OPENAI_API_KEY, completionParams: { model }, - debug: false, + debug: true, } - if (process.env.OPENAI_API_BASE_URL && process.env.OPENAI_API_BASE_URL.trim().length > 0) - options.apiBaseUrl = process.env.OPENAI_API_BASE_URL - - if (process.env.SOCKS_PROXY_HOST && process.env.SOCKS_PROXY_PORT) { - const agent = new SocksProxyAgent({ - hostname: process.env.SOCKS_PROXY_HOST, - port: process.env.SOCKS_PROXY_PORT, - }) - options.fetch = (url, options) => { - return fetch(url, { agent, ...options }) + // increase max token limit if use gpt-4 + if (model.toLowerCase().includes('gpt-4')) { + // if use 32k model + if (model.toLowerCase().includes('32k')) { + options.maxModelTokens = 32768 + options.maxResponseTokens = 8192 + } + else { + options.maxModelTokens = 8192 + options.maxResponseTokens = 2048 } } + if (isNotEmptyString(OPENAI_API_BASE_URL)) + options.apiBaseUrl = `${OPENAI_API_BASE_URL}/v1` + + setupProxy(options) + api = new ChatGPTAPI({ ...options }) apiModel = 'ChatGPTAPI' } else { + const OPENAI_API_MODEL = process.env.OPENAI_API_MODEL const options: ChatGPTUnofficialProxyAPIOptions = { accessToken: process.env.OPENAI_ACCESS_TOKEN, - debug: false, + debug: true, } + if (isNotEmptyString(OPENAI_API_MODEL)) + options.model = OPENAI_API_MODEL - if (process.env.SOCKS_PROXY_HOST && process.env.SOCKS_PROXY_PORT) { - const agent = new SocksProxyAgent({ - hostname: process.env.SOCKS_PROXY_HOST, - port: process.env.SOCKS_PROXY_PORT, - }) - options.fetch = (url, options) => { - return fetch(url, { agent, ...options }) - } - } - - if (process.env.API_REVERSE_PROXY) + if (isNotEmptyString(process.env.API_REVERSE_PROXY)) options.apiReverseProxyUrl = process.env.API_REVERSE_PROXY + setupProxy(options) + api = new ChatGPTUnofficialProxyAPI({ ...options }) apiModel = 'ChatGPTUnofficialProxyAPI' } })() -async function chatReplyProcess( - message: string, - lastContext?: { conversationId?: string; parentMessageId?: string }, - process?: (chat: ChatMessage) => void, -) { - // if (!message) - // return sendResponse({ type: 'Fail', message: 'Message is empty' }) - +async function chatReplyProcess(options: RequestOptions) { + const { message, lastContext, process, systemMessage } = options try { let options: SendMessageOptions = { timeoutMs } - if (lastContext) { + if (apiModel === 'ChatGPTAPI') { + if (isNotEmptyString(systemMessage)) + options.systemMessage = systemMessage + } + + if (lastContext != null) { if (apiModel === 'ChatGPTAPI') - options = { parentMessageId: lastContext.parentMessageId } + options.parentMessageId = lastContext.parentMessageId else options = { ...lastContext } } @@ -118,18 +122,68 @@ async function chatReplyProcess( } } +async function fetchBalance() { + const OPENAI_API_KEY = process.env.OPENAI_API_KEY + const OPENAI_API_BASE_URL = process.env.OPENAI_API_BASE_URL + + if (!isNotEmptyString(OPENAI_API_KEY)) + return Promise.resolve('-') + + const API_BASE_URL = isNotEmptyString(OPENAI_API_BASE_URL) + ? OPENAI_API_BASE_URL + : 'https://api.openai.com' + + try { + const headers = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${OPENAI_API_KEY}` } + const response = await axios.get(`${API_BASE_URL}/dashboard/billing/credit_grants`, { headers }) + const balance = response.data.total_available ?? 0 + return Promise.resolve(balance.toFixed(3)) + } + catch { + return Promise.resolve('-') + } +} + async function chatConfig() { - return sendResponse({ + const balance = await fetchBalance() + const reverseProxy = process.env.API_REVERSE_PROXY ?? '-' + const httpsProxy = (process.env.HTTPS_PROXY || process.env.ALL_PROXY) ?? '-' + const socksProxy = (process.env.SOCKS_PROXY_HOST && process.env.SOCKS_PROXY_PORT) + ? (`${process.env.SOCKS_PROXY_HOST}:${process.env.SOCKS_PROXY_PORT}`) + : '-' + return sendResponse({ type: 'Success', - data: { - apiModel, - reverseProxy: process.env.API_REVERSE_PROXY, - timeoutMs, - socksProxy: (process.env.SOCKS_PROXY_HOST && process.env.SOCKS_PROXY_PORT) ? (`${process.env.SOCKS_PROXY_HOST}:${process.env.SOCKS_PROXY_PORT}`) : '-', - } as ModelConfig, + data: { apiModel, reverseProxy, timeoutMs, socksProxy, httpsProxy, balance }, }) } +function setupProxy(options: ChatGPTAPIOptions | ChatGPTUnofficialProxyAPIOptions) { + if (process.env.SOCKS_PROXY_HOST && process.env.SOCKS_PROXY_PORT) { + const agent = new SocksProxyAgent({ + hostname: process.env.SOCKS_PROXY_HOST, + port: process.env.SOCKS_PROXY_PORT, + }) + options.fetch = (url, options) => { + return fetch(url, { agent, ...options }) + } + } + else { + if (process.env.HTTPS_PROXY || process.env.ALL_PROXY) { + const httpsProxy = process.env.HTTPS_PROXY || process.env.ALL_PROXY + if (httpsProxy) { + const agent = new HttpsProxyAgent(httpsProxy) + options.fetch = (url, options) => { + return fetch(url, { agent, ...options }) + } + } + } + } +} + +function currentModel(): ApiModel { + return apiModel +} + export type { ChatContext, ChatMessage } -export { chatReplyProcess, chatConfig } +export { chatReplyProcess, chatConfig, currentModel } diff --git a/service/src/chatgpt/types.ts b/service/src/chatgpt/types.ts new file mode 100644 index 0000000000..1e65f4ca39 --- /dev/null +++ b/service/src/chatgpt/types.ts @@ -0,0 +1,8 @@ +import type { ChatMessage } from 'chatgpt' + +export interface RequestOptions { + message: string + lastContext?: { conversationId?: string; parentMessageId?: string } + process?: (chat: ChatMessage) => void + systemMessage?: string +} diff --git a/service/src/index.ts b/service/src/index.ts index 04b5134930..28041d1c70 100644 --- a/service/src/index.ts +++ b/service/src/index.ts @@ -1,7 +1,10 @@ import express from 'express' -import type { ChatContext, ChatMessage } from './chatgpt' -import { chatConfig, chatReplyProcess } from './chatgpt' +import type { RequestProps } from './types' +import type { ChatMessage } from './chatgpt' +import { chatConfig, chatReplyProcess, currentModel } from './chatgpt' import { auth } from './middleware/auth' +import { limiter } from './middleware/limiter' +import { isNotEmptyString } from './utils/is' const app = express() const router = express.Router() @@ -11,20 +14,25 @@ app.use(express.json()) app.all('*', (_, res, next) => { res.header('Access-Control-Allow-Origin', '*') - res.header('Access-Control-Allow-Headers', 'Content-Type') + res.header('Access-Control-Allow-Headers', 'authorization, Content-Type') res.header('Access-Control-Allow-Methods', '*') next() }) -router.post('/chat-process', auth, async (req, res) => { +router.post('/chat-process', [auth, limiter], async (req, res) => { res.setHeader('Content-type', 'application/octet-stream') try { - const { prompt, options = {} } = req.body as { prompt: string; options?: ChatContext } + const { prompt, options = {}, systemMessage } = req.body as RequestProps let firstChunk = true - await chatReplyProcess(prompt, options, (chat: ChatMessage) => { - res.write(firstChunk ? JSON.stringify(chat) : `\n${JSON.stringify(chat)}`) - firstChunk = false + await chatReplyProcess({ + message: prompt, + lastContext: options, + process: (chat: ChatMessage) => { + res.write(firstChunk ? JSON.stringify(chat) : `\n${JSON.stringify(chat)}`) + firstChunk = false + }, + systemMessage, }) } catch (error) { @@ -35,7 +43,7 @@ router.post('/chat-process', auth, async (req, res) => { } }) -router.post('/config', async (req, res) => { +router.post('/config', auth, async (req, res) => { try { const response = await chatConfig() res.send(response) @@ -48,8 +56,8 @@ router.post('/config', async (req, res) => { router.post('/session', async (req, res) => { try { const AUTH_SECRET_KEY = process.env.AUTH_SECRET_KEY - const hasAuth = typeof AUTH_SECRET_KEY === 'string' && AUTH_SECRET_KEY.length > 0 - res.send({ status: 'Success', message: '', data: { auth: hasAuth } }) + const hasAuth = isNotEmptyString(AUTH_SECRET_KEY) + res.send({ status: 'Success', message: '', data: { auth: hasAuth, model: currentModel() } }) } catch (error) { res.send({ status: 'Fail', message: error.message, data: null }) @@ -74,5 +82,6 @@ router.post('/verify', async (req, res) => { app.use('', router) app.use('/api', router) +app.set('trust proxy', 1) app.listen(3002, () => globalThis.console.log('Server is running on port 3002')) diff --git a/service/src/middleware/auth.ts b/service/src/middleware/auth.ts index 50bf02a257..8b04ced1e7 100644 --- a/service/src/middleware/auth.ts +++ b/service/src/middleware/auth.ts @@ -1,6 +1,8 @@ +import { isNotEmptyString } from '../utils/is' + const auth = async (req, res, next) => { const AUTH_SECRET_KEY = process.env.AUTH_SECRET_KEY - if (typeof AUTH_SECRET_KEY === 'string' && AUTH_SECRET_KEY.length > 0) { + if (isNotEmptyString(AUTH_SECRET_KEY)) { try { const Authorization = req.header('Authorization') if (!Authorization || Authorization.replace('Bearer ', '').trim() !== AUTH_SECRET_KEY.trim()) diff --git a/service/src/middleware/limiter.ts b/service/src/middleware/limiter.ts new file mode 100644 index 0000000000..d4df149395 --- /dev/null +++ b/service/src/middleware/limiter.ts @@ -0,0 +1,19 @@ +import { rateLimit } from 'express-rate-limit' +import { isNotEmptyString } from '../utils/is' + +const MAX_REQUEST_PER_HOUR = process.env.MAX_REQUEST_PER_HOUR + +const maxCount = (isNotEmptyString(MAX_REQUEST_PER_HOUR) && !isNaN(Number(MAX_REQUEST_PER_HOUR))) + ? parseInt(MAX_REQUEST_PER_HOUR) + : 0 // 0 means unlimited + +const limiter = rateLimit({ + windowMs: 60 * 60 * 1000, // Maximum number of accesses within an hour + max: maxCount, + statusCode: 200, // 200 means success,but the message is 'Too many request from this IP in 1 hour' + message: async (req, res) => { + res.send({ status: 'Fail', message: 'Too many request from this IP in 1 hour', data: null }) + }, +}) + +export { limiter } diff --git a/service/src/types.ts b/service/src/types.ts index dcade0c047..fdbf68c797 100644 --- a/service/src/types.ts +++ b/service/src/types.ts @@ -1,5 +1,11 @@ import type { FetchFn } from 'chatgpt' +export interface RequestProps { + prompt: string + options?: ChatContext + systemMessage: string +} + export interface ChatContext { conversationId?: string parentMessageId?: string @@ -19,6 +25,8 @@ export interface ModelConfig { reverseProxy?: string timeoutMs?: number socksProxy?: string + httpsProxy?: string + balance?: string } export type ApiModel = 'ChatGPTAPI' | 'ChatGPTUnofficialProxyAPI' | undefined diff --git a/service/src/utils/index.ts b/service/src/utils/index.ts index 1d83253055..726f807eae 100644 --- a/service/src/utils/index.ts +++ b/service/src/utils/index.ts @@ -1,10 +1,10 @@ -interface SendResponseOptions { +interface SendResponseOptions { type: 'Success' | 'Fail' message?: string - data?: any + data?: T } -export function sendResponse(options: SendResponseOptions) { +export function sendResponse(options: SendResponseOptions) { if (options.type === 'Success') { return Promise.resolve({ message: options.message ?? null, diff --git a/service/src/utils/is.ts b/service/src/utils/is.ts new file mode 100644 index 0000000000..c1253f21dd --- /dev/null +++ b/service/src/utils/is.ts @@ -0,0 +1,19 @@ +export function isNumber(value: T | unknown): value is number { + return Object.prototype.toString.call(value) === '[object Number]' +} + +export function isString(value: T | unknown): value is string { + return Object.prototype.toString.call(value) === '[object String]' +} + +export function isNotEmptyString(value: any): boolean { + return typeof value === 'string' && value.length > 0 +} + +export function isBoolean(value: T | unknown): value is boolean { + return Object.prototype.toString.call(value) === '[object Boolean]' +} + +export function isFunction any | void | never>(value: T | unknown): value is T { + return Object.prototype.toString.call(value) === '[object Function]' +} diff --git a/service/tsup.config.ts b/service/tsup.config.ts index 4f24c6a19f..534f06df67 100644 --- a/service/tsup.config.ts +++ b/service/tsup.config.ts @@ -4,7 +4,7 @@ export default defineConfig({ entry: ['src/index.ts'], outDir: 'build', target: 'es2020', - format: ['cjs'], + format: ['esm'], splitting: false, sourcemap: true, minify: false, diff --git a/src/api/index.ts b/src/api/index.ts index e576ab1319..9f61fc531b 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,5 +1,6 @@ import type { AxiosProgressEvent, GenericAbortSignal } from 'axios' import { post } from '@/utils/request' +import { useSettingStore } from '@/store' export function fetchChatAPI( prompt: string, @@ -26,9 +27,11 @@ export function fetchChatAPIProcess( signal?: GenericAbortSignal onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void }, ) { + const settingStore = useSettingStore() + return post({ url: '/chat-process', - data: { prompt: params.prompt, options: params.options }, + data: { prompt: params.prompt, options: params.options, systemMessage: settingStore.systemMessage }, signal: params.signal, onDownloadProgress: params.onDownloadProgress, }) diff --git a/src/assets/recommend.json b/src/assets/recommend.json new file mode 100644 index 0000000000..384a940659 --- /dev/null +++ b/src/assets/recommend.json @@ -0,0 +1,14 @@ +[ + { + "key": "awesome-chatgpt-prompts-zh", + "desc": "ChatGPT 中文调教指南", + "downloadUrl": "https://raw.githubusercontent.com/PlexPt/awesome-chatgpt-prompts-zh/main/prompts-zh.json", + "url": "https://github.com/PlexPt/awesome-chatgpt-prompts-zh" + }, + { + "key": "awesome-chatgpt-prompts-zh-TW", + "desc": "ChatGPT 中文調教指南 (透過 OpenAI / OpenCC 協助,從簡體中文轉換為繁體中文的版本)", + "downloadUrl": "https://raw.githubusercontent.com/PlexPt/awesome-chatgpt-prompts-zh/main/prompts-zh-TW.json", + "url": "https://github.com/PlexPt/awesome-chatgpt-prompts-zh" + } +] diff --git a/src/components/common/PromptStore/index.vue b/src/components/common/PromptStore/index.vue new file mode 100644 index 0000000000..cbee4b051d --- /dev/null +++ b/src/components/common/PromptStore/index.vue @@ -0,0 +1,478 @@ + + + diff --git a/src/components/common/Setting/About.vue b/src/components/common/Setting/About.vue index d585e5ea23..3edb317bed 100644 --- a/src/components/common/Setting/About.vue +++ b/src/components/common/Setting/About.vue @@ -1,20 +1,27 @@ + + diff --git a/src/components/common/Setting/General.vue b/src/components/common/Setting/General.vue index e75b0a99dd..d6383b9cce 100644 --- a/src/components/common/Setting/General.vue +++ b/src/components/common/Setting/General.vue @@ -1,16 +1,19 @@