Compare commits

...

100 Commits

Author SHA1 Message Date
Peter Dave Hello
7474b2c149 Add GPT-4o mini support (#2102)
Reference:
- https://platform.openai.com/docs/models/gpt-4o-mini
- https://openai.com/index/gpt-4o-mini-advancing-cost-efficient-intelligence/
2024-07-19 11:07:35 +08:00
ChenZhoYu
574aac2ff1 perf: 数学公式 $$ 处理 2024-06-07 10:18:56 +08:00
ChenZhoYu
a546d856d7 feat: markdown mermaid 2024-06-07 10:14:34 +08:00
ChenZhoYu
e3ce91cfa1 fix: model error 2024-05-17 23:56:45 +08:00
Peter Dave Hello
e1a3308355 chore: replace EOL Node.js v19 w/ LTS v20 in engine spec (#2080)
Update the Node.js engine setting in service/package.json to use LTS v20
instead of the end-of-life v19. This aligns with our Dockerfile that
uses node:lts-alpine, which has been a while without issues. The update
also helps suppress the runtime warnings about engine compatibility:

> WARN  Unsupported engine: wanted: {"node":"^16 || ^18 || ^19"} (current: {"node":"v20.12.2","pnpm":"9.0.4"})
2024-05-16 09:40:06 +08:00
Peter Dave Hello
f257a54604 feat: add GPT-4o support (#2082)
Reference:
- https://platform.openai.com/docs/models/gpt-4o
- https://openai.com/index/hello-gpt-4o/
2024-05-16 09:39:49 +08:00
L
fa8874b6dd update esno to fix tsx issue on Node v18.19 (#2083) 2024-05-16 09:39:33 +08:00
ChenZhoYu
ab9dca471d perf: change katex package 2024-05-16 09:38:50 +08:00
ChenZhoYu
a4103769ce perf: html2canvas => html-to-image 2024-04-19 10:30:10 +08:00
Peter Dave Hello
f4f1d351b7 Add GPT-4 Turbo API the latest GA version (#2072)
Reference:
- https://platform.openai.com/docs/models/gpt-4-and-gpt-4-turbo
2024-04-16 14:59:51 +08:00
Peter Dave Hello
8dd447ce69 feat: improve GPT-3.5 Turbo 16k support (#2049)
Reference:
- https://platform.openai.com/docs/models/gpt-3-5-turbo
2024-03-11 13:02:19 +08:00
ChenZhoYu
6432efeb35 chore: doc 2024-03-11 12:00:53 +08:00
Lukin
c520580cda fix(server): compatible with gateway.ai.cloudflare.com (#2029) 2024-03-06 13:17:10 +08:00
Peter Dave Hello
15903fae38 feat: support the latest GPT-4 Turbo preview models (#2024)
This includes the latest:
- gpt-4-0125-preview
- gpt-4-turbo-preview

Reference:
- https://openai.com/blog/new-embedding-models-and-api-updates
2024-02-01 08:25:50 +08:00
zf
05e1df5332 fix: SvgIcon打包报类型错误 (#2012) 2024-01-10 19:38:20 +08:00
Ed Burnette
60f1f71d27 Added es-ES (#1989)
* Additional English translations
Provide an English version of the readme, and add a few translations
that were missing elsewhere

* Use browser language by default

* Support 'en' and 'vi' as languages

* Fixed: Browserslist: caniuse-lite is outdated.
Full message was:
Browserslist: caniuse-lite is outdated. Please run:
  npx update-browserslist-db@latest
  Why you should do it regularly: https://github.com/browserslist/update-db#readme

* Added es-ES
These changes were originally from https://github.com/rasta26/chatgpt-web
although I did tweak the translations a bit.

---------

Co-authored-by: Ed Burnette <ed.burnette@hiddenmind.ai>
2023-12-07 00:23:52 -06:00
Ed Burnette
15a6b19897 localization: Add additional English translations (#1987)
* Additional English translations
Provide an English version of the readme, and add a few translations
that were missing elsewhere

* Use browser language by default

* Support 'en' and 'vi' as languages

---------

Co-authored-by: Ed Burnette <ed.burnette@hiddenmind.ai>
2023-12-03 04:27:38 -06:00
Peter Dave Hello
ed1e41c4f1 feat: support the latest GPT-4 Turbo (gpt-4-1106-preview) model (#1968)
Reference:
- https://platform.openai.com/docs/models/gpt-4-and-gpt-4-turbo
- https://openai.com/blog/new-models-and-developer-products-announced-at-devday#gpt-4-turbo-with-128k-context
2023-11-23 21:58:37 -06:00
ChenZhaoYu
a6605e8a57 chore: v2.11.1 2023-10-11 16:17:19 +08:00
ChenZhaoYu
3482565481 chore: doc 2023-10-07 08:33:01 +08:00
ChenZhaoYu
a6f670101a fix: 不规范的引入 2023-09-26 11:53:18 +08:00
ChenZhaoYu
2683977e21 chore: 2.11.1 2023-09-26 11:52:38 +08:00
ChenZhaoYu
e1d8f5ff56 feat: 清空聊天历史 2023-09-26 11:50:38 +08:00
ChenZhaoYu
4f9232c130 chore: 格式化代码 2023-09-26 11:39:28 +08:00
ChenZhaoYu
c226d07368 fix: 修复打字机动画 2023-09-26 11:38:58 +08:00
Carson Yang
2b2efe2f15 Update README.md (#1864) 2023-08-09 11:45:11 +08:00
march
c2ce7009e6 fix: store循环引用 (#1880) 2023-08-09 11:43:57 +08:00
Peter Dave Hello
b651ef8373 chore: Improve zh-TW locale (#1837) 2023-07-15 11:19:45 +08:00
shansing
b8f2a0e849 feat: 允许temperature调到2 (#1797)
ref: https://platform.openai.com/docs/api-reference/chat/create#chat/create-temperature
2023-06-26 18:17:11 +08:00
Tin Nguyen Trong
c6e1663a39 Create vi-VN.ts (#1798) 2023-06-26 18:16:47 +08:00
怀瑾
847a2d4d8c style: 优化移动端代码展示 (#1752) 2023-06-19 14:28:10 +08:00
BertramRay
6e272bb343 feat: 支持最新的gpt-3.5-turbo-16k模型 (#1789)
* fix: 增加 16k 模型支持

* fix: 修改判断逻辑

* fix: 修改 gpt-3.5-turbo tokens 判断逻辑

---------

Co-authored-by: ziyang <ziyang@dora.design>
2023-06-19 14:26:22 +08:00
舜岳
bc390ef09d feat: "Stop Responding" 国际化,使用 chatgpt 翻译 (#1735) 2023-05-31 14:05:53 +08:00
tranhungonline
1cb5393b91 fix: 移动端新建会话关闭侧边栏 (#1661)
Co-authored-by: ChenZhaoYu <790348264@qq.com>
2023-05-06 10:58:06 +08:00
ChenZhaoYu
0f51e51827 fix: 移动端塌缩 2023-05-04 11:19:25 +08:00
ChenZhaoYu
3dff5bd4c8 chore: version 2.11.0 2023-04-26 11:07:37 +08:00
ChenZhaoYu
dbb57d8894 perf: 优化复制逻辑 2023-04-26 08:27:46 +08:00
ChenZhaoYu
3b033d0ed4 feat: 补充语言列表 2023-04-26 08:26:50 +08:00
ChenZhaoYu
dd20e9aea6 chore: 移除无用文件 2023-04-26 08:18:19 +08:00
aquaratixc
838679f837 chore: Update index.ts (#1570)
* Update index.ts

Added russian translation

* fix: locale name

---------

Co-authored-by: Redon <790348264@qq.com>
2023-04-24 20:15:28 +08:00
夜法之书(appotry)
89f78bd4c7 添加nginx防止爬虫爬取配置 (#1187)
* 添加nginx防止爬虫爬取配置

* Update nginx.conf

* Update README.md
2023-04-24 20:08:30 +08:00
24min
d598dc65ce fix(proxy url): change default proxy url (#1567) 2023-04-24 20:07:46 +08:00
aquaratixc
ac8b69dfd4 Create ru-RU.ts (#1571)
Added russian localization
2023-04-24 20:05:41 +08:00
wanglong001
226ce2bded [fix] markdown 表格内没有换行 (#1492) 2023-04-17 21:10:36 +08:00
Peter Dave Hello
7583985f44 chore: Improve zh-TW locale (#1460) 2023-04-16 15:18:53 +08:00
Peter Dave Hello
537872968c Let's make GitHub happy (#1453) 2023-04-15 16:35:33 +08:00
Kamilake
eed33fbb0e feat: Added Korean translation (#1372)
* add ko-KR

* oops

* type-check & delete unused comments
2023-04-12 09:34:36 +08:00
idawnwon
da04383772 fix: default return in useLanguange() hook (#1352)
if `setLocale('zh-CN')`, `return` should be `zhCN`, not `enUS`.
2023-04-10 21:29:03 +08:00
ChenZhaoYu
527c8613b2 chore: mark days 2023-04-10 15:51:19 +08:00
ChenZhaoYu
05a241408e fix: conversationOptions 2023-04-10 08:24:16 +08:00
舜岳
20aa35f209 chore: optimize code (#1328) 2023-04-09 22:14:33 +08:00
ChenZhaoYu
ddc7066f4e chore: 默认 100 秒 2023-04-08 11:57:41 +08:00
ChenZhaoYu
44f00c95cd fix: 调整光标到底部 2023-04-08 11:49:41 +08:00
ZuoNing
439104f195 fix: 查询使用量支持代理&修正使用量文案 (#1296)
* fix: 查询使用量支持代理&修正使用量文案

* fix: 修复默认错误

* chore: 移除打印

---------

Co-authored-by: ChenZhaoYu <790348264@qq.com>
2023-04-08 11:47:00 +08:00
quzard
86bba7d8f3 feat: 添加自定义 temperature 和 top_p (#1260)
* 在设置的高级面板里自定义temperature和top_p

* change default temperature from 0.8 to 0.5

* pref: 检查代码,增加仅 api 的接口判断

* chore: 锁定 pnpm-lock.yaml

---------

Co-authored-by: ChenZhaoYu <790348264@qq.com>
2023-04-07 14:32:09 +08:00
wangxi
1187d88593 fix: 修复API余额查询 (#1174)
* fix: 使API余额查询可用

* chore: 调整计算方式

* perf: 余额描述变更

---------

Co-authored-by: ChenZhaoYu <790348264@qq.com>
2023-04-04 08:27:16 +08:00
RyanzeX
b07f01b0cf chore: 引导用户触发提示词 (#1183) 2023-04-04 07:54:05 +08:00
LeafSummer
9b66fed26e fix: requestOptions (#1188)
修复页面上重复执行onRegenerate操作,会丢失上下文的问题
2023-04-04 07:53:37 +08:00
ChenZhaoYu
abc4c3ad22 chore: 回退 chatgpt 包版本,原因:token 长回复报错 2023-04-03 16:19:38 +08:00
ChenZhaoYu
a7c97026b7 chore: v2.10.9 2023-04-03 09:46:46 +08:00
ChenZhaoYu
0ff7825387 chore: update README.md 2023-04-03 09:21:08 +08:00
ChenZhaoYu
c2b25a84c7 chore: default API_REVERSE_PROXY 2023-04-03 09:19:12 +08:00
ChenZhaoYu
553e239db3 chore: update .env.examples 2023-04-03 09:01:56 +08:00
idawnwon
e3a3e4dc29 fix: Update @acheong08 Reverse Proxy URL (#1085) 2023-04-03 08:53:52 +08:00
ChenZhaoYu
4e4e41b0d6 chore: update deps 2023-04-03 08:51:04 +08:00
vchenpeng
5594b0baa9 fix: 空引用显示undefined (#1103)
Co-authored-by: peng.chen <peng.chen@freemud.com>
2023-04-03 08:48:59 +08:00
Fog3211
e2ad3fe248 fix: x-scrollbar height (#1153)
Co-authored-by: Fog3211 <23151576+Fog3211@users.noreply.github.com>
2023-04-03 08:25:07 +08:00
ChenZhaoYu
9bd88eac84 fix: 宽度问题 2023-03-31 14:52:54 +08:00
ChenZhaoYu
431de382dc perf: input auto size 2023-03-31 14:43:40 +08:00
ChenZhaoYu
b241240fc6 perf: 默认 systemMessage 2023-03-31 13:42:16 +08:00
ChenZhaoYu
40fa028408 fix: some error 2023-03-31 13:37:07 +08:00
ChenZhaoYu
15f3aac88e perf: 调整光标位置 2023-03-31 13:20:55 +08:00
Hank
90f0c3a80b feat: add socks username/password config (#890)
Co-authored-by: Redon <790348264@qq.com>
2023-03-31 13:09:51 +08:00
ChenZhaoYu
d2a852d5eb chore: .eslintignore 2023-03-31 13:05:30 +08:00
yimiaoxiehou
85543deca3 feat: 添加 socks5 代理认证 (#999)
* support socks5 proxy auth

* Update build_docker.yml

* Update README.md

* perf: 增加判断

* fix: lint

---------

Co-authored-by: ChenZhaoYu <790348264@qq.com>
2023-03-31 12:23:26 +08:00
ChenZhaoYu
142759ac50 chore: lint fix 2023-03-31 11:53:10 +08:00
Miku
da20aed4b0 feat: add kubernetes deploy (#1001) 2023-03-31 11:52:10 +08:00
puppywang
c0f4af05e3 feat: add typing effect (#1017)
* feat: add typing effect

* fix: ts2339 xxx not exist on type 'never'

---------

Co-authored-by: WangYi <wangyi@windimg.com>
2023-03-31 11:50:32 +08:00
cong
468bed7705 feat: allow user disable openai API debug log (#1041)
* feat: allow user disable openai API debug log

* chore: fix pnpm lock
2023-03-31 11:40:01 +08:00
LOVECHEN
0878af0239 perf: 加入容器名字 (#1035)
这样查看服务的时候会更优雅
2023-03-31 11:34:31 +08:00
ChenZhaoYu
aa0487edc4 chore: gitHub action 2023-03-29 16:21:08 +08:00
ChenZhaoYu
76cef650b4 chore: build rollback 2023-03-28 16:06:11 +08:00
assassinliujie
78bcf7f4ce perf: optimized output (#962)
* Update index.ts

* Update index.vue

* Update index.ts
2023-03-28 15:47:40 +08:00
ChenZhaoYu
c0a9fd5208 fix: 快速按下删除会话导致的问题 #917 2023-03-28 13:48:49 +08:00
ChenZhaoYu
07123b70ad chore: rolled back to clear the impact 2023-03-28 13:20:55 +08:00
assassinliujie
b579d24d19 pref: message output optimization (#935)
* Update index.ts

修改后端,让保留打字机效果的同时优化前后端之间传输的内容,节省流量和性能

* Update index.vue

修改前端,和之前修改的后端匹配,保留打字机效果同时优化性能和流量传输

* chore: lint fix

---------

Co-authored-by: ChenZhaoYu <790348264@qq.com>
2023-03-28 09:40:20 +08:00
KingTwinkle
e2eeee455a fix: local import error and NModal not as expected (#938) 2023-03-28 09:34:15 +08:00
weifeng
32ad20416c chore: Update README.md (#880)
* Update README.md

docker-compose 文件中注释掉未使用的配置节点,因为 留xxx 会导代码读取节点内容值为xxx,而不是期望的默认值,导致代码运行异常
增加 对模型可选参数的备注

* style: eslint fix

---------

Co-authored-by: ChenZhaoYu <790348264@qq.com>
2023-03-27 15:58:54 +08:00
吴杉(Shan Wu)
799af86739 perf: add localization for sider (#911) 2023-03-27 15:49:42 +08:00
ChenZhaoYu
c3f431118b chore: update docs 2023-03-26 10:08:01 +08:00
zaimoe
a3944f86b7 fix: missing VITE_GLOB_API_URL when docker build, fixed #690 #717 # (#877) 2023-03-25 17:23:57 +08:00
Yige
365a7df1ff Merge pull request #883 from yi-ge/dev-main
docs: 更新README.md,添加长回复使用方法
2023-03-25 17:06:20 +08:00
yi-ge
639152f987 docs: update README.md, add long reply feature 2023-03-25 17:01:12 +08:00
gitgitgogogo
fb8ad3790d fix: 反向代理限流失效 (#863)
https://docs.colyseus.io/zh_hk/colyseus/how-to/rate-limit/

使用nginx限流会只识别为服务器ip,需启用trust proxy
2023-03-25 08:30:15 +08:00
ChenZhaoYu
902321026b perf: 本地地址 2023-03-24 08:14:51 +08:00
ChenZhaoYu
9b0d7dbee8 fix: 移动端焦点不触发的问题 2023-03-23 21:12:36 +08:00
ChenZhaoYu
634c879108 perf: 自动焦点时移动端上的不便 2023-03-23 20:51:20 +08:00
Kid
57a1d6e3cd fix: ESM error (#826) 2023-03-23 20:43:47 +08:00
ChenZhaoYu
9081b22ce9 perf: 移动端删除 chat 时,侧边栏应该收起 2023-03-23 16:56:59 +08:00
Redon
73e12b1fdd fix: 移动端新建会话关闭侧边栏 (#813) 2023-03-23 16:23:57 +08:00
71 changed files with 19492 additions and 7671 deletions

View File

@@ -0,0 +1,22 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node
{
"name": "Node.js & TypeScript",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"image": "mcr.microsoft.com/devcontainers/typescript-node:1-20-bullseye"
// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],
// Use 'postCreateCommand' to run commands after the container is created.
// "postCreateCommand": "yarn install",
// Configure tool-specific properties.
// "customizations": {},
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root"
}

View File

@@ -4,3 +4,4 @@ node_modules
Dockerfile
.*
*/.*
!.env

2
.env
View File

@@ -1,7 +1,7 @@
# 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

2
.eslintignore Normal file
View File

@@ -0,0 +1,2 @@
docker-compose
kubernetes

22
.github/workflows/issues_close.yml vendored Normal file
View File

@@ -0,0 +1,22 @@
name: Close inactive issues
on:
schedule:
- cron: '30 1 * * *'
jobs:
close-issues:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@v5
with:
days-before-issue-stale: 10
days-before-issue-close: 2
stale-issue-label: stale
stale-issue-message: This issue is stale because it has been open for 10 days with no activity.
close-issue-message: This issue was closed because it has been inactive for 2 days since being marked as stale.
days-before-pr-stale: -1
days-before-pr-close: -1
repo-token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -2,7 +2,7 @@
"prettier.enable": false,
"editor.formatOnSave": false,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
"source.fixAll.eslint": "explicit"
},
"eslint.validate": [
"javascript",

View File

@@ -1,3 +1,70 @@
## v2.11.1
`2023-10-11`
## Enhancement
- 优化打字机光标效果
- 清空聊天历史按钮
- 更新文档
## BugFix
- 修复移动端上的问题
- 修复不规范的引入导致的问题
## v2.11.0
`2023-04-26`
> [chatgpt-web-plus](https://github.com/Chanzhaoyu/chatgpt-web-plus) 新界面、完整用户管理
## Enhancement
- 更新默认 `accessToken` 反代地址为 [[pengzhile](https://github.com/pengzhile)] 的 `https://ai.fakeopen.com/api/conversation` [[24min](https://github.com/Chanzhaoyu/chatgpt-web/pull/1567/files)]
- 添加自定义 `temperature``top_p` [[quzard](https://github.com/Chanzhaoyu/chatgpt-web/pull/1260)]
- 优化代码 [[shunyue1320](https://github.com/Chanzhaoyu/chatgpt-web/pull/1328)]
- 优化复制代码反馈效果
## BugFix
- 修复余额查询和文案 [[luckywangxi](https://github.com/Chanzhaoyu/chatgpt-web/pull/1174)][[zuoning777](https://github.com/Chanzhaoyu/chatgpt-web/pull/1296)]
- 修复默认语言错误 [[idawnwon](https://github.com/Chanzhaoyu/chatgpt-web/pull/1352)]
- 修复 `onRegenerate` 下问题 [[leafsummer](https://github.com/Chanzhaoyu/chatgpt-web/pull/1188)]
## Other
- 引导用户触发提示词 [[RyanXinOne](https://github.com/Chanzhaoyu/chatgpt-web/pull/1183)]
- 添加韩语翻译 [[Kamilake](https://github.com/Chanzhaoyu/chatgpt-web/pull/1372)]
- 添加俄语翻译 [[aquaratixc](https://github.com/Chanzhaoyu/chatgpt-web/pull/1571)]
- 优化翻译和文本检查 [[PeterDaveHello](https://github.com/Chanzhaoyu/chatgpt-web/pull/1460)]
- 移除无用文件
## v2.10.9
`2023-04-03`
> 更新默认 `accessToken` 反代地址为 [[pengzhile](https://github.com/pengzhile)] 的 `https://ai.fakeopen.com/api/conversation`
## Enhancement
- 添加 `socks5` 代理认证 [[yimiaoxiehou](https://github.com/Chanzhaoyu/chatgpt-web/pull/999)]
- 添加 `socks` 代理用户名密码的配置 [[hank-cp](https://github.com/Chanzhaoyu/chatgpt-web/pull/890)]
- 添加可选日志打印 [[zcong1993](https://github.com/Chanzhaoyu/chatgpt-web/pull/1041)]
- 更新侧边栏按钮本地化[[simonwu53](https://github.com/Chanzhaoyu/chatgpt-web/pull/911)]
- 优化代码块滚动条高度 [[Fog3211](https://github.com/Chanzhaoyu/chatgpt-web/pull/1153)]
## BugFix
- 修复 `PWA` 问题 [[bingo235](https://github.com/Chanzhaoyu/chatgpt-web/pull/807)]
- 修复 `ESM` 错误 [[kidonng](https://github.com/Chanzhaoyu/chatgpt-web/pull/826)]
- 修复反向代理开启时限流失效的问题 [[gitgitgogogo](https://github.com/Chanzhaoyu/chatgpt-web/pull/863)]
- 修复 `docker` 构建时 `.env` 可能被忽略的问题 [[zaiMoe](https://github.com/Chanzhaoyu/chatgpt-web/pull/877)]
- 修复导出异常错误 [[KingTwinkle](https://github.com/Chanzhaoyu/chatgpt-web/pull/938)]
- 修复空值异常 [[vchenpeng](https://github.com/Chanzhaoyu/chatgpt-web/pull/1103)]
- 移动端上的体验问题
## Other
- `Docker` 容器名字名义 [[LOVECHEN](https://github.com/Chanzhaoyu/chatgpt-web/pull/1035)]
- `kubernetes` 部署配置 [[CaoYunzhou](https://github.com/Chanzhaoyu/chatgpt-web/pull/1001)]
- 感谢 [[assassinliujie](https://github.com/Chanzhaoyu/chatgpt-web/pull/962)] 和 [[puppywang](https://github.com/Chanzhaoyu/chatgpt-web/pull/1017)] 的某些贡献
- 更新 `kubernetes/deploy.yaml` [[idawnwon](https://github.com/Chanzhaoyu/chatgpt-web/pull/1085)]
- 文档更新 [[#yi-ge](https://github.com/Chanzhaoyu/chatgpt-web/pull/883)]
- 文档更新 [[weifeng12x](https://github.com/Chanzhaoyu/chatgpt-web/pull/880)]
- 依赖更新
## v2.10.8
`2023-03-23`
@@ -71,7 +138,7 @@
`2023-03-13`
更新依赖,`access_token` 默认代理为 [acheong08](https://github.com/acheong08) 的 `https://bypass.duti.tech/api/conversation`
更新依赖,`access_token` 默认代理为 [pengzhile](https://github.com/pengzhile) 的 `https://bypass.duti.tech/api/conversation`
## Feature
- `Prompt` 商店在线导入可以导入两种 `recommend.json`里提到的模板 [simonwu53](https://github.com/Chanzhaoyu/chatgpt-web/pull/521)

View File

@@ -1,333 +0,0 @@
# ChatGPT Web
<div style="font-size: 1.5rem;">
<a href="./README.md">中文</a> |
<a href="./README.en.md">English</a>
</div>
</br>
> Disclaimer: This project is only released on GitHub, under the MIT License, free and for open-source learning purposes. There will be no account selling, paid services, discussion groups, or forums. Beware of fraud.
![cover](./docs/c1.png)
![cover2](./docs/c2.png)
- [ChatGPT Web](#chatgpt-web)
- [Introduction](#introduction)
- [Roadmap](#roadmap)
- [Prerequisites](#prerequisites)
- [Node](#node)
- [PNPM](#pnpm)
- [Fill in the Keys](#fill-in-the-keys)
- [Install Dependencies](#install-dependencies)
- [Backend](#backend)
- [Frontend](#frontend)
- [Run in Test Environment](#run-in-test-environment)
- [Backend Service](#backend-service)
- [Frontend Webpage](#frontend-webpage)
- [Packaging](#packaging)
- [Using Docker](#using-docker)
- [Docker Parameter Example](#docker-parameter-example)
- [Docker Build \& Run](#docker-build--run)
- [Docker Compose](#docker-compose)
- [Deployment with Railway](#deployment-with-railway)
- [Railway Environment Variables](#railway-environment-variables)
- [Manual packaging](#manual-packaging)
- [Backend service](#backend-service-1)
- [Frontend webpage](#frontend-webpage-1)
- [Frequently Asked Questions](#frequently-asked-questions)
- [Contributing](#contributing)
- [Sponsorship](#sponsorship)
- [License](#license)
## Introduction
Supports dual models, provides two unofficial `ChatGPT API` methods:
| Method | Free? | Reliability | Quality |
| --------------------------------------------- | ------ | ----------- | ------- |
| `ChatGPTAPI(gpt-3.5-turbo-0301)` | No | Reliable | Relatively clumsy |
| `ChatGPTUnofficialProxyAPI(Web accessToken)` | Yes | Relatively unreliable | Smart |
Comparison:
1. `ChatGPTAPI` uses `gpt-3.5-turbo-0301` to simulate `ChatGPT` through the official `OpenAI` completion `API` (the most reliable method, but it is not free and does not use models specifically tuned for chat).
2. `ChatGPTUnofficialProxyAPI` accesses `ChatGPT`'s backend `API` via an unofficial proxy server to bypass `Cloudflare` (uses the real `ChatGPT`, is very lightweight, but depends on third-party servers and has rate limits).
[Details](https://github.com/Chanzhaoyu/chatgpt-web/issues/138)
Switching Methods:
1. Go to the `service/.env.example` file and copy the contents to the `service/.env` file.
2. For `OpenAI API Key`, fill in the `OPENAI_API_KEY` field [(Get apiKey)](https://platform.openai.com/overview).
3. For `Web API`, fill in the `OPENAI_ACCESS_TOKEN` field [(Get accessToken)](https://chat.openai.com/api/auth/session).
4. When both are present, `OpenAI API Key` takes precedence.
Reverse Proxy:
Available when using `ChatGPTUnofficialProxyAPI`.[Details](https://github.com/transitive-bullshit/chatgpt-api#reverse-proxy)
```shell
# service/.env
API_REVERSE_PROXY=
```
Environment Variables:
For all parameter variables, check [here](#docker-parameter-example) or see:
```
/service/.env
```
## Roadmap
[✓] Dual models
[✓] Multiple session storage and context logic
[✓] Formatting and beautifying code-like message types
[✓] Access rights control
[✓] Data import and export
[✓] Save message to local image
[✓] Multilingual interface
[✓] Interface themes
[✗] More...
## Prerequisites
### Node
`node` requires version `^16 || ^18` (`node >= 14` requires installation of [fetch polyfill](https://github.com/developit/unfetch#usage-as-a-polyfill)), and multiple local `node` versions can be managed using [nvm](https://github.com/nvm-sh/nvm).
```shell
node -v
```
### PNPM
If you have not installed `pnpm` before:
```shell
npm install pnpm -g
```
### Fill in the Keys
Get `Openai Api Key` or `accessToken` and fill in the local environment variables [jump](#introduction)
```
# service/.env file
# OpenAI API Key - https://platform.openai.com/overview
OPENAI_API_KEY=
# change this to an `accessToken` extracted from the ChatGPT site's `https://chat.openai.com/api/auth/session` response
OPENAI_ACCESS_TOKEN=
```
## Install Dependencies
> To make it easier for `backend developers` to understand, we did not use the front-end `workspace` mode, but stored it in different folders. If you only need to do secondary development of the front-end page, delete the `service` folder.
### Backend
Enter the `/service` folder and run the following command
```shell
pnpm install
```
### Frontend
Run the following command in the root directory
```shell
pnpm bootstrap
```
## Run in Test Environment
### Backend Service
Enter the `/service` folder and run the following command
```shell
pnpm start
```
### Frontend Webpage
Run the following command in the root directory
```shell
pnpm dev
```
## Packaging
### Using Docker
#### Docker Parameter Example
- `OPENAI_API_KEY` one of two
- `OPENAI_ACCESS_TOKEN` one of two, `OPENAI_API_KEY` takes precedence when both are present
- `OPENAI_API_BASE_URL` optional, available when `OPENAI_API_KEY` is set
- `OPENAI_API_MODEL` optional, available when `OPENAI_API_KEY` is set
- `API_REVERSE_PROXY` optional, available when `OPENAI_ACCESS_TOKEN` is set [Reference](#introduction)
- `AUTH_SECRET_KEY` Access Passwordoptional
- `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 httphttps, socks5
- `ALL_PROXY` optional, support httphttps, socks5
![docker](./docs/docker.png)
#### Docker Build & Run
```bash
docker build -t chatgpt-web .
# foreground operation
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 127.0.0.1:3002:3002 --env OPENAI_API_KEY=your_api_key chatgpt-web
# running address
http://localhost:3002/
```
#### Docker Compose
[Hub Address](https://hub.docker.com/repository/docker/chenzhaoyu94/chatgpt-web/general)
```yml
version: '3'
services:
app:
image: chenzhaoyu94/chatgpt-web # always use latest, pull the tag image again when updating
ports:
- 127.0.0.1:3002:3002
environment:
# one of two
OPENAI_API_KEY: xxxxxx
# one of two
OPENAI_ACCESS_TOKEN: xxxxxx
# api interface url, optional, available when OPENAI_API_KEY is set
OPENAI_API_BASE_URL: xxxx
# api model, optional, available when OPENAI_API_KEY is set
OPENAI_API_MODEL: xxxx
# reverse proxy, optional
API_REVERSE_PROXY: xxx
# access passwordoptional
AUTH_SECRET_KEY: xxx
# timeout, in milliseconds, optional
TIMEOUT_MS: 60000
# socks proxy, optional, effective with SOCKS_PROXY_PORT
SOCKS_PROXY_HOST: xxxx
# socks proxy port, optional, effective with SOCKS_PROXY_HOST
SOCKS_PROXY_PORT: xxxx
# HTTPS Proxyoptional, 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`.
### Deployment with Railway
[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/new/template/yytmgc)
#### Railway Environment Variables
| Environment Variable | Required | Description |
| -------------------- | -------- | ------------------------------------------------------------------------------------------------- |
| `PORT` | Required | Default: `3002` |
| `AUTH_SECRET_KEY` | Optional | access password |
| `TIMEOUT_MS` | Optional | Timeout in milliseconds |
| `OPENAI_API_KEY` | Optional | Required for `OpenAI API`. `apiKey` can be obtained from [here](https://platform.openai.com/overview). |
| `OPENAI_ACCESS_TOKEN`| Optional | Required for `Web API`. `accessToken` can be obtained from [here](https://chat.openai.com/api/auth/session).|
| `OPENAI_API_BASE_URL` | Optional, only for `OpenAI API` | API endpoint. |
| `OPENAI_API_MODEL` | Optional, only for `OpenAI API` | API model. |
| `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.
### Manual packaging
#### Backend service
> If you don't need the `node` interface of this project, you can skip the following steps.
Copy the `service` folder to a server that has a `node` service environment.
```shell
# Install
pnpm install
# Build
pnpm build
# Run
pnpm prod
```
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_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)
```shell
pnpm build
```
## Frequently Asked Questions
Q: Why does Git always report an error when committing?
A: Because there is submission information verification, please follow the [Commit Guidelines](./CONTRIBUTING.en.md).
Q: Where to change the request interface if only the frontend page is used?
A: The `VITE_GLOB_API_URL` field in the `.env` file at the root directory.
Q: All red when saving the file?
A: For `vscode`, please install the recommended plug-in of the project or manually install the `Eslint` plug-in.
Q: Why doesn't the frontend have a typewriter effect?
A: One possible reason is that after Nginx reverse proxying, buffering is turned on, and Nginx will try to buffer a certain amount of data from the backend before sending it to the browser. Please try adding `proxy_buffering off;` after the reverse proxy parameter and then reloading Nginx. Other web server configurations are similar.
## Contributing
Please read the [Contributing Guidelines](./CONTRIBUTING.en.md) before contributing.
Thanks to all the contributors!
<a href="https://github.com/Chanzhaoyu/chatgpt-web/graphs/contributors">
<img src="https://contrib.rocks/image?repo=Chanzhaoyu/chatgpt-web" />
</a>
## Sponsorship
If you find this project helpful and circumstances permit, you can give me a little support. Thank you very much for your support~
<div style="display: flex; gap: 20px;">
<div style="text-align: center">
<img style="max-width: 100%" src="./docs/wechat.png" alt="WeChat" />
<p>WeChat Pay</p>
</div>
<div style="text-align: center">
<img style="max-width: 100%" src="./docs/alipay.png" alt="Alipay" />
<p>Alipay</p>
</div>
</div>
## License
MIT © [ChenZhaoYu](./license)

351
README.md
View File

@@ -1,118 +1,113 @@
# ChatGPT Web
<div style="font-size: 1.5rem;">
<a href="./README.md">中文</a> |
<a href="./README.en.md">English</a>
</div>
</br>
> Disclaimer: This project is only published on GitHub, based on the MIT license, free and for open source learning usage. And there will be no any form of account selling, paid service, discussion group, discussion group and other behaviors. Beware of being deceived.
> 声明:此项目只发布于 Github基于 MIT 协议,免费且作为开源学习使用。并且不会有任何形式的卖号、付费服务、讨论群、讨论组等行为。谨防受骗。
[中文](README.zh.md)
![cover](./docs/c1.png)
![cover2](./docs/c2.png)
- [ChatGPT Web](#chatgpt-web)
- [介绍](#介绍)
- [待实现路线](#待实现路线)
- [前置要求](#前置要求)
- [Introduction](#introduction)
- [Roadmap](#roadmap)
- [Prerequisites](#prerequisites)
- [Node](#node)
- [PNPM](#pnpm)
- [填写密钥](#填写密钥)
- [安装依赖](#安装依赖)
- [后端](#后端)
- [前端](#前端)
- [测试环境运行](#测试环境运行)
- [后端服务](#后端服务)
- [前端网页](#前端网页)
- [环境变量](#环境变量)
- [打包](#打包)
- [使用 Docker](#使用-docker)
- [Docker 参数示例](#docker-参数示例)
- [Filling in the Key](#filling-in-the-key)
- [Install Dependencies](#install-dependencies)
- [Backend](#backend)
- [Frontend](#frontend)
- [Run in Test Environment](#run-in-test-environment)
- [Backend Service](#backend-service)
- [Frontend Webpage](#frontend-webpage)
- [Environment Variables](#environment-variables)
- [Packaging](#packaging)
- [Use Docker](#use-docker)
- [Docker Parameter Examples](#docker-parameter-examples)
- [Docker build \& Run](#docker-build--run)
- [Docker compose](#docker-compose)
- [使用 Railway 部署](#使用-railway-部署)
- [Railway 环境变量](#railway-环境变量)
- [手动打包](#手动打包)
- [后端服务](#后端服务-1)
- [前端网页](#前端网页-1)
- [常见问题](#常见问题)
- [参与贡献](#参与贡献)
- [赞助](#赞助)
- [Prevent Crawlers](#prevent-crawlers)
- [Deploy with Railway](#deploy-with-railway)
- [Railway Environment Variables](#railway-environment-variables)
- [Deploy with Sealos](#deploy-with-sealos)
- [Package Manually](#package-manually)
- [Backend Service](#backend-service-1)
- [Frontend Webpage](#frontend-webpage-1)
- [FAQ](#faq)
- [Contributing](#contributing)
- [Acknowledgements](#acknowledgements)
- [Sponsors](#sponsors)
- [License](#license)
## 介绍
## Introduction
支持双模型,提供了两种非官方 `ChatGPT API` 方法
Supports dual models and provides two unofficial `ChatGPT API` methods
| 方式 | 免费? | 可靠性 | 质量 |
| --------------------------------------------- | ------ | ---------- | ---- |
| `ChatGPTAPI(gpt-3.5-turbo-0301)` | 否 | 可靠 | 相对较笨 |
| `ChatGPTUnofficialProxyAPI(网页 accessToken)` | | 相对不可靠 | 聪明 |
| Method | Free? | Reliability | Quality |
| ---------------------------------- | ----- | ----------- | ------- |
| `ChatGPTAPI(gpt-3.5-turbo-0301)` | No | Reliable | Relatively stupid |
| `ChatGPTUnofficialProxyAPI(web accessToken)` | Yes | Relatively unreliable | Smart |
对比:
1. `ChatGPTAPI` 使用 `gpt-3.5-turbo-0301` 通过官方`OpenAI`补全`API`模拟`ChatGPT`(最稳健的方法,但它不是免费的,并且没有使用针对聊天进行微调的模型)
2. `ChatGPTUnofficialProxyAPI` 使用非官方代理服务器访问 `ChatGPT` 的后端`API`,绕过`Cloudflare`(使用真实的的`ChatGPT`,非常轻量级,但依赖于第三方服务器,并且有速率限制)
Comparison:
1. `ChatGPTAPI` uses `gpt-3.5-turbo` through `OpenAI` official `API` to call `ChatGPT`
2. `ChatGPTUnofficialProxyAPI` uses unofficial proxy server to access `ChatGPT`'s backend `API`, bypass `Cloudflare` (dependent on third-party servers, and has rate limits)
警告:
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`,防止被关键词搜索到。
Warnings:
1. You should first use the `API` method
2. When using the `API`, if the network is not working, it is blocked in China, you need to build your own proxy, never use someone else's public proxy, which is dangerous.
3. When using the `accessToken` method, the reverse proxy will expose your access token to third parties. This should not have any adverse effects, but please consider the risks before using this method.
4. When using `accessToken`, whether you are a domestic or foreign machine, proxies will be used. The default proxy is [pengzhile](https://github.com/pengzhile)'s `https://ai.fakeopen.com/api/conversation`. This is not a backdoor or monitoring unless you have the ability to flip over `CF` verification yourself. Use beforehand acknowledge. [Community Proxy](https://github.com/transitive-bullshit/chatgpt-api#reverse-proxy) (Note: Only these two are recommended, other third-party sources, please identify for yourself)
5. When publishing the project to public network, you should set the `AUTH_SECRET_KEY` variable to add your password access, you should also modify the `title` in `index. html` to prevent it from being searched by keywords.
切换方式:
1. 进入 `service/.env.example` 文件,复制内容到 `service/.env` 文件
2. 使用 `OpenAI API Key` 请填写 `OPENAI_API_KEY` 字段 [(获取 apiKey)](https://platform.openai.com/overview)
3. 使用 `Web API` 请填写 `OPENAI_ACCESS_TOKEN` 字段 [(获取 accessToken)](https://chat.openai.com/api/auth/session)
4. 同时存在时以 `OpenAI API Key` 优先
Switching methods:
1. Enter the `service/.env.example` file, copy the contents to the `service/.env` file
2. To use `OpenAI API Key`, fill in the `OPENAI_API_KEY` field [(get apiKey)](https://platform.openai.com/overview)
3. To use `Web API`, fill in the `OPENAI_ACCESS_TOKEN` field [(get accessToken)](https://chat.openai.com/api/auth/session)
4. `OpenAI API Key` takes precedence when both exist
环境变量:
Environment variables:
全部参数变量请查看或[这里](#环境变量)
See all parameter variables [here](#environment-variables)
```
/service/.env.example
```
## Roadmap
[✓] Dual models
## 待实现路线
[✓] 双模型
[✓] Multi-session storage and context logic
[✓] 多会话储存和上下文逻辑
[✓] Formatting and beautification of code and other message types
[✓] 对代码等消息类型的格式化美化处理
[✓] Access control
[✓] 访问权限控制
[✓] Data import/export
[✓] 数据导入、导出
[✓] Save messages as local images
[✓] 保存消息到本地图片
[✓] Multilingual interface
[✓] 界面多语言
[✓] 界面主题
[✓] Interface themes
[✗] More...
## 前置要求
## Prerequisites
### 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` 版本
`node` requires version `^16 || ^18 || ^19` (`node >= 14` needs [fetch polyfill](https://github.com/developit/unfetch#usage-as-a-polyfill) installation), use [nvm](https://github.com/nvm-sh/nvm) to manage multiple local `node` versions
```shell
node -v
```
### PNPM
如果你没有安装过 `pnpm`
If you haven't installed `pnpm`
```shell
npm install pnpm -g
```
### 填写密钥
获取 `Openai Api Key` `accessToken` 并填写本地环境变量 [跳转](#介绍)
### Filling in the Key
Get `Openai Api Key` or `accessToken` and fill in the local environment variables [Go to Introduction](#introduction)
```
# service/.env 文件
# service/.env file
# OpenAI API Key - https://platform.openai.com/overview
OPENAI_API_KEY=
@@ -121,67 +116,68 @@ OPENAI_API_KEY=
OPENAI_ACCESS_TOKEN=
```
## 安装依赖
## Install Dependencies
> 为了简便 `后端开发人员` 的了解负担,所以并没有采用前端 `workspace` 模式,而是分文件夹存放。如果只需要前端页面做二次开发,删除 `service` 文件夹即可。
> For the convenience of "backend developers" to understand the burden, the front-end "workspace" mode is not adopted, but separate folders are used to store them. If you only need to do secondary development of the front-end page, delete the `service` folder.
### 后端
### Backend
进入文件夹 `/service` 运行以下命令
Enter the folder `/service` and run the following commands
```shell
pnpm install
```
### 前端
根目录下运行以下命令
### Frontend
Run the following commands at the root directory
```shell
pnpm bootstrap
```
## 测试环境运行
### 后端服务
## Run in Test Environment
### Backend Service
进入文件夹 `/service` 运行以下命令
Enter the folder `/service` and run the following commands
```shell
pnpm start
```
### 前端网页
根目录下运行以下命令
### Frontend Webpage
Run the following commands at the root directory
```shell
pnpm dev
```
## 环境变量
## Environment Variables
`API` 可用:
`API` available:
- `OPENAI_API_KEY` `OPENAI_ACCESS_TOKEN` 二选一
- `OPENAI_API_MODEL` 设置模型,可选,默认:`gpt-3.5-turbo`
- `OPENAI_API_BASE_URL` 设置接口地址,可选,默认:`https://api.openai.com`
- `OPENAI_API_KEY` and `OPENAI_ACCESS_TOKEN` choose one
- `OPENAI_API_MODEL` Set model, optional, default: `gpt-3.5-turbo`
- `OPENAI_API_BASE_URL` Set interface address, optional, default: `https://api.openai.com`
- `OPENAI_API_DISABLE_DEBUG` Set interface to close debug logs, optional, default: empty does not close
`ACCESS_TOKEN` 可用:
`ACCESS_TOKEN` available:
- `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_ACCESS_TOKEN` and `OPENAI_API_KEY` choose one, `OPENAI_API_KEY` takes precedence when both exist
- `API_REVERSE_PROXY` Set reverse proxy, optional, default: `https://ai.fakeopen.com/api/conversation`, [Community](https://github.com/transitive-bullshit/chatgpt-api#reverse-proxy) (Note: Only these two are recommended, other third party sources, please identify for yourself)
通用:
Common:
- `AUTH_SECRET_KEY` 访问权限密钥,可选
- `MAX_REQUEST_PER_HOUR` 每小时最大请求次数,可选,默认无限
- `TIMEOUT_MS` 超时,单位毫秒,可选
- `SOCKS_PROXY_HOST` `SOCKS_PROXY_PORT` 一起时生效,可选
- `SOCKS_PROXY_PORT` `SOCKS_PROXY_HOST` 一起时生效,可选
- `HTTPS_PROXY` 支持 `http``https`, `socks5`,可选
- `ALL_PROXY` 支持 `http``https`, `socks5`,可选
- `AUTH_SECRET_KEY` Access permission key, optional
- `MAX_REQUEST_PER_HOUR` Maximum number of requests per hour, optional, unlimited by default
- `TIMEOUT_MS` Timeout, unit milliseconds, optional
- `SOCKS_PROXY_HOST` and `SOCKS_PROXY_PORT` take effect together, optional
- `SOCKS_PROXY_PORT` and `SOCKS_PROXY_HOST` take effect together, optional
- `HTTPS_PROXY` Support `http`, `https`, `socks5`, optional
- `ALL_PROXY` Support `http`, `https`, `socks5`, optional
## 打包
## Packaging
### 使用 Docker
### Use Docker
#### Docker 参数示例
#### Docker Parameter Examples
![docker](./docs/docker.png)
@@ -190,150 +186,179 @@ pnpm dev
```bash
docker build -t chatgpt-web .
# 前台运行
# Foreground running
docker run --name chatgpt-web --rm -it -p 127.0.0.1:3002:3002 --env OPENAI_API_KEY=your_api_key chatgpt-web
# 后台运行
# Background running
docker run --name chatgpt-web -d -p 127.0.0.1:3002:3002 --env OPENAI_API_KEY=your_api_key chatgpt-web
# 运行地址
# Run address
http://localhost:3002/
```
#### Docker compose
[Hub 地址](https://hub.docker.com/repository/docker/chenzhaoyu94/chatgpt-web/general)
[Hub address](https://hub.docker.com/repository/docker/chenzhaoyu94/chatgpt-web/general)
```yml
version: '3'
services:
app:
image: chenzhaoyu94/chatgpt-web # 总是使用 latest ,更新时重新 pull tag 镜像即可
image: chenzhaoyu94/chatgpt-web # always use latest, pull the tag image again to update
ports:
- 127.0.0.1:3002:3002
environment:
# 二选一
# choose one
OPENAI_API_KEY: sk-xxx
# 二选一
# choose one
OPENAI_ACCESS_TOKEN: xxx
# API接口地址,可选,设置 OPENAI_API_KEY 时可用
# API interface address, optional, available when OPENAI_API_KEY is set
OPENAI_API_BASE_URL: xxx
# API模型,可选,设置 OPENAI_API_KEY 时可用
# API model, optional, available when OPENAI_API_KEY is set, https://platform.openai.com/docs/models
# gpt-4, gpt-4o, gpt-4o-mini, gpt-4-turbo, gpt-4-turbo-preview, gpt-4-0125-preview, gpt-4-1106-preview, gpt-4-0314, gpt-4-0613, gpt-4-32k, gpt-4-32k-0314, gpt-4-32k-0613, gpt-3.5-turbo-16k, gpt-3.5-turbo-16k-0613, gpt-3.5-turbo, gpt-3.5-turbo-0301, gpt-3.5-turbo-0613, text-davinci-003, text-davinci-002, code-davinci-002
OPENAI_API_MODEL: xxx
# 反向代理,可选
# reverse proxy, optional
API_REVERSE_PROXY: xxx
# 访问权限密钥,可选
# access permission key, optional
AUTH_SECRET_KEY: xxx
# 每小时最大请求次数,可选,默认无限
# maximum number of requests per hour, optional, unlimited by default
MAX_REQUEST_PER_HOUR: 0
# 超时,单位毫秒,可选
# timeout, unit milliseconds, optional
TIMEOUT_MS: 60000
# Socks代理,可选,和 SOCKS_PROXY_PORT 一起时生效
# Socks proxy, optional, take effect with SOCKS_PROXY_PORT
SOCKS_PROXY_HOST: xxx
# Socks代理端口,可选,和 SOCKS_PROXY_HOST 一起时生效
# Socks proxy port, optional, take effect with SOCKS_PROXY_HOST
SOCKS_PROXY_PORT: xxx
# HTTPS 代理,可选,支持 httphttpssocks5
# HTTPS proxy, optional, support http,https,socks5
HTTPS_PROXY: http://xxx:7890
```
- `OPENAI_API_BASE_URL` 可选,设置 `OPENAI_API_KEY` 时可用
- `OPENAI_API_MODEL` 可选,设置 `OPENAI_API_KEY` 时可用
### 使用 Railway 部署
- `OPENAI_API_BASE_URL` Optional, available when `OPENAI_API_KEY` is set
- `OPENAI_API_MODEL` Optional, available when `OPENAI_API_KEY` is set
#### Prevent Crawlers
**nginx**
Fill in the following configuration in the nginx configuration file to prevent crawlers. You can refer to the `docker-compose/nginx/nginx.conf` file to add anti-crawler methods
```
# Prevent crawlers
if ($http_user_agent ~* "360Spider|JikeSpider|Spider|spider|bot|Bot|2345Explorer|curl|wget|webZIP|qihoobot|Baiduspider|Googlebot|Googlebot-Mobile|Googlebot-Image|Mediapartners-Google|Adsbot-Google|Feedfetcher-Google|Yahoo! Slurp|Yahoo! Slurp China|YoudaoBot|Sosospider|Sogou spider|Sogou web spider|MSNBot|ia_archiver|Tomato Bot|NSPlayer|bingbot")
{
return 403;
}
```
### Deploy with Railway
[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/new/template/yytmgc)
#### Railway 环境变量
#### Railway Environment Variables
| 环境变量名称 | 必填 | 备注 |
| Environment variable name | Required | Remarks |
| --------------------- | ---------------------- | -------------------------------------------------------------------------------------------------- |
| `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) |
| `OPENAI_API_BASE_URL` | 可选,`OpenAI API` 时可用 | `API`接口地址 |
| `OPENAI_API_MODEL` | 可选,`OpenAI API` 时可用 | `API`模型 |
| `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 代理,支持 httphttps, socks5 |
| `ALL_PROXY` | 可选 | 所有代理 代理,支持 httphttps, socks5 |
| `PORT` | Required | Default `3002` |
| `AUTH_SECRET_KEY` | Optional | Access permission key |
| `MAX_REQUEST_PER_HOUR` | Optional | Maximum number of requests per hour, optional, unlimited by default |
| `TIMEOUT_MS` | Optional | Timeout, unit milliseconds |
| `OPENAI_API_KEY` | `OpenAI API` choose one | `apiKey` required for `OpenAI API` [(get apiKey)](https://platform.openai.com/overview) |
| `OPENAI_ACCESS_TOKEN` | `Web API` choose one | `accessToken` required for `Web API` [(get accessToken)](https://chat.openai.com/api/auth/session) |
| `OPENAI_API_BASE_URL` | Optional, available when `OpenAI API` | `API` interface address |
| `OPENAI_API_MODEL` | Optional, available when `OpenAI API` | `API` model |
| `API_REVERSE_PROXY` | Optional, available when `Web API` | `Web API` reverse proxy address [Details](https://github.com/transitive-bullshit/chatgpt-api#reverse-proxy) |
| `SOCKS_PROXY_HOST` | Optional, take effect with `SOCKS_PROXY_PORT` | Socks proxy |
| `SOCKS_PROXY_PORT` | Optional, take effect with `SOCKS_PROXY_HOST` | Socks proxy port |
| `SOCKS_PROXY_USERNAME` | Optional, take effect with `SOCKS_PROXY_HOST` | Socks proxy username |
| `SOCKS_PROXY_PASSWORD` | Optional, take effect with `SOCKS_PROXY_HOST` | Socks proxy password |
| `HTTPS_PROXY` | Optional | HTTPS proxy, support http,https, socks5 |
| `ALL_PROXY` | Optional | All proxies, support http,https, socks5 |
> 注意: `Railway` 修改环境变量会重新 `Deploy`
> Note: Modifying environment variables on `Railway` will re-`Deploy`
### 手动打包
#### 后端服务
> 如果你不需要本项目的 `node` 接口,可以省略如下操作
### Deploy with Sealos
复制 `service` 文件夹到你有 `node` 服务环境的服务器上。
[![](https://raw.githubusercontent.com/labring-actions/templates/main/Deploy-on-Sealos.svg)](https://cloud.sealos.io/?openapp=system-fastdeploy%3FtemplateName%3Dchatgpt-web)
> Environment variables are consistent with Docker environment variables
### Package Manually
#### Backend Service
> If you don't need the `node` interface of this project, you can omit the following operations
Copy the `service` folder to the server where you have the `node` service environment.
```shell
# 安装
# Install
pnpm install
# 打包
# Pack
pnpm build
# 运行
# Run
pnpm prod
```
PS: 不进行打包,直接在服务器上运行 `pnpm start` 也可
PS: It is also okay to run `pnpm start` directly on the server without packing
#### 前端网页
#### Frontend Webpage
1、修改根目录下 `.env` 文件中的 `VITE_GLOB_API_URL` 为你的实际后端接口地址
1. Modify the `VITE_GLOB_API_URL` field in the `.env` file at the root directory to your actual backend interface address
2、根目录下运行以下命令然后将 `dist` 文件夹内的文件复制到你网站服务的根目录下
2. Run the following commands at the root directory, then copy the files in the `dist` folder to the root directory of your website service
[参考信息](https://cn.vitejs.dev/guide/static-deploy.html#building-the-app)
[Reference](https://cn.vitejs.dev/guide/static -deploy.html#building-the-app)
```shell
pnpm build
```
## 常见问题
Q: 为什么 `Git` 提交总是报错?
## FAQ
Q: Why does `Git` commit always report errors?
A: 因为有提交信息验证,请遵循 [Commit 指南](./CONTRIBUTING.md)
A: Because there is a commit message verification, please follow the [Commit Guide](./CONTRIBUTING.md)
Q: 如果只使用前端页面,在哪里改请求接口?
Q: Where to change the request interface if only the front-end page is used?
A: 根目录下 `.env` 文件中的 `VITE_GLOB_API_URL` 字段。
A: The `VITE_GLOB_API_URL` field in the `.env` file at the root directory.
Q: 文件保存时全部爆红?
Q: All files explode red when saving?
A: `vscode` 请安装项目推荐插件,或手动安装 `Eslint` 插件。
A: `vscode` please install the recommended plug-ins for the project, or manually install the `Eslint` plug-in.
Q: 前端没有打字机效果?
Q: No typewriter effect on the front end?
A: 一种可能原因是经过 Nginx 反向代理,开启了 buffer则 Nginx 会尝试从后端缓冲一定大小的数据再发送给浏览器。请尝试在反代参数后添加 `proxy_buffering off;`,然后重载 Nginx。其他 web server 配置同理。
A: One possible reason is that after Nginx reverse proxy, buffer is turned on, then Nginx will try to buffer some data from the backend before sending it to the browser. Please try adding `proxy_buffering off; ` after the reverse proxy parameter, then reload Nginx. Other web server configurations are similar.
## 参与贡献
## Contributing
贡献之前请先阅读 [贡献指南](./CONTRIBUTING.md)
Please read the [Contributing Guide](./CONTRIBUTING.md) before contributing
感谢所有做过贡献的人!
Thanks to everyone who has contributed!
<a href="https://github.com/Chanzhaoyu/chatgpt-web/graphs/contributors">
<img src="https://contrib.rocks/image?repo=Chanzhaoyu/chatgpt-web" />
</a>
## 赞助
## Acknowledgements
如果你觉得这个项目对你有帮助,并且情况允许的话,可以给我一点点支持,总之非常感谢支持~
Thanks to [JetBrains](https://www.jetbrains.com/) SoftWare for providing free Open Source license for this project.
## Sponsors
If you find this project helpful and can afford it, you can give me a little support. Anyway, thanks for your support~
<div style="display: flex; gap: 20px;">
<div style="text-align: center">
<img style="max-width: 100%" src="./docs/wechat.png" alt="微信" />
<img style="max-width: 100%" src="./docs/wechat.png" alt="WeChat" />
<p>WeChat Pay</p>
</div>
<div style="text-align: center">
<img style="max-width: 100%" src="./docs/alipay.png" alt="支付宝" />
<img style="max-width: 100%" src="./docs/alipay.png" alt="Alipay" />
<p>Alipay</p>
</div>
</div>
## License
MIT © [ChenZhaoYu](./license)
MIT © [ChenZhaoYu]

367
README.zh.md Normal file
View File

@@ -0,0 +1,367 @@
# ChatGPT Web
> 声明:此项目只发布于 GitHub基于 MIT 协议,免费且作为开源学习使用。并且不会有任何形式的卖号、付费服务、讨论群、讨论组等行为。谨防受骗。
[English](README.md)
![cover](./docs/c1.png)
![cover2](./docs/c2.png)
- [ChatGPT Web](#chatgpt-web)
- [介绍](#介绍)
- [待实现路线](#待实现路线)
- [前置要求](#前置要求)
- [Node](#node)
- [PNPM](#pnpm)
- [填写密钥](#填写密钥)
- [安装依赖](#安装依赖)
- [后端](#后端)
- [前端](#前端)
- [测试环境运行](#测试环境运行)
- [后端服务](#后端服务)
- [前端网页](#前端网页)
- [环境变量](#环境变量)
- [打包](#打包)
- [使用 Docker](#使用-docker)
- [Docker 参数示例](#docker-参数示例)
- [Docker build \& Run](#docker-build--run)
- [Docker compose](#docker-compose)
- [防止爬虫抓取](#防止爬虫抓取)
- [使用 Railway 部署](#使用-railway-部署)
- [Railway 环境变量](#railway-环境变量)
- [使用 Sealos 部署](#使用-sealos-部署)
- [手动打包](#手动打包)
- [后端服务](#后端服务-1)
- [前端网页](#前端网页-1)
- [常见问题](#常见问题)
- [参与贡献](#参与贡献)
- [致谢](#致谢)
- [赞助](#赞助)
- [License](#license)
## 介绍
支持双模型,提供了两种非官方 `ChatGPT API` 方法
| 方式 | 免费? | 可靠性 | 质量 |
| --------------------------------------------- | ------ | ---------- | ---- |
| `ChatGPTAPI(gpt-3.5-turbo-0301)` | 否 | 可靠 | 相对较笨 |
| `ChatGPTUnofficialProxyAPI(网页 accessToken)` | 是 | 相对不可靠 | 聪明 |
对比:
1. `ChatGPTAPI` 使用 `gpt-3.5-turbo` 通过 `OpenAI` 官方 `API` 调用 `ChatGPT`
2. `ChatGPTUnofficialProxyAPI` 使用非官方代理服务器访问 `ChatGPT` 的后端`API`,绕过`Cloudflare`(依赖于第三方服务器,并且有速率限制)
警告:
1. 你应该首先使用 `API` 方式
2. 使用 `API` 时,如果网络不通,那是国内被墙了,你需要自建代理,绝对不要使用别人的公开代理,那是危险的。
3. 使用 `accessToken` 方式时反向代理将向第三方暴露您的访问令牌,这样做应该不会产生任何不良影响,但在使用这种方法之前请考虑风险。
4. 使用 `accessToken` 时,不管你是国内还是国外的机器,都会使用代理。默认代理为 [pengzhile](https://github.com/pengzhile) 大佬的 `https://ai.fakeopen.com/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` 文件
2. 使用 `OpenAI API Key` 请填写 `OPENAI_API_KEY` 字段 [(获取 apiKey)](https://platform.openai.com/overview)
3. 使用 `Web API` 请填写 `OPENAI_ACCESS_TOKEN` 字段 [(获取 accessToken)](https://chat.openai.com/api/auth/session)
4. 同时存在时以 `OpenAI API Key` 优先
环境变量:
全部参数变量请查看或[这里](#环境变量)
```
/service/.env.example
```
## 待实现路线
[✓] 双模型
[✓] 多会话储存和上下文逻辑
[✓] 对代码等消息类型的格式化美化处理
[✓] 访问权限控制
[✓] 数据导入、导出
[✓] 保存消息到本地图片
[✓] 界面多语言
[✓] 界面主题
[✗] More...
## 前置要求
### 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
```
### PNPM
如果你没有安装过 `pnpm`
```shell
npm install pnpm -g
```
### 填写密钥
获取 `Openai Api Key``accessToken` 并填写本地环境变量 [跳转](#介绍)
```
# service/.env 文件
# OpenAI API Key - https://platform.openai.com/overview
OPENAI_API_KEY=
# change this to an `accessToken` extracted from the ChatGPT site's `https://chat.openai.com/api/auth/session` response
OPENAI_ACCESS_TOKEN=
```
## 安装依赖
> 为了简便 `后端开发人员` 的了解负担,所以并没有采用前端 `workspace` 模式,而是分文件夹存放。如果只需要前端页面做二次开发,删除 `service` 文件夹即可。
### 后端
进入文件夹 `/service` 运行以下命令
```shell
pnpm install
```
### 前端
根目录下运行以下命令
```shell
pnpm bootstrap
```
## 测试环境运行
### 后端服务
进入文件夹 `/service` 运行以下命令
```shell
pnpm start
```
### 前端网页
根目录下运行以下命令
```shell
pnpm dev
```
## 环境变量
`API` 可用:
- `OPENAI_API_KEY``OPENAI_ACCESS_TOKEN` 二选一
- `OPENAI_API_MODEL` 设置模型,可选,默认:`gpt-3.5-turbo`
- `OPENAI_API_BASE_URL` 设置接口地址,可选,默认:`https://api.openai.com`
- `OPENAI_API_DISABLE_DEBUG` 设置接口关闭 debug 日志可选默认empty 不关闭
`ACCESS_TOKEN` 可用:
- `OPENAI_ACCESS_TOKEN``OPENAI_API_KEY` 二选一,同时存在时,`OPENAI_API_KEY` 优先
- `API_REVERSE_PROXY` 设置反向代理,可选,默认:`https://ai.fakeopen.com/api/conversation`[社区](https://github.com/transitive-bullshit/chatgpt-api#reverse-proxy)(注意:只有这两个是推荐,其他第三方来源,请自行甄别)
通用:
- `AUTH_SECRET_KEY` 访问权限密钥,可选
- `MAX_REQUEST_PER_HOUR` 每小时最大请求次数,可选,默认无限
- `TIMEOUT_MS` 超时,单位毫秒,可选
- `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)
#### Docker build & Run
```bash
docker build -t 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 127.0.0.1:3002:3002 --env OPENAI_API_KEY=your_api_key chatgpt-web
# 运行地址
http://localhost:3002/
```
#### Docker compose
[Hub 地址](https://hub.docker.com/repository/docker/chenzhaoyu94/chatgpt-web/general)
```yml
version: '3'
services:
app:
image: chenzhaoyu94/chatgpt-web # 总是使用 latest ,更新时重新 pull 该 tag 镜像即可
ports:
- 127.0.0.1:3002:3002
environment:
# 二选一
OPENAI_API_KEY: sk-xxx
# 二选一
OPENAI_ACCESS_TOKEN: xxx
# API接口地址可选设置 OPENAI_API_KEY 时可用
OPENAI_API_BASE_URL: xxx
# API模型可选设置 OPENAI_API_KEY 时可用https://platform.openai.com/docs/models
# gpt-4, gpt-4o, gpt-4o-mini, gpt-4-turbo, gpt-4-turbo-preview, gpt-4-0125-preview, gpt-4-1106-preview, gpt-4-0314, gpt-4-0613, gpt-4-32k, gpt-4-32k-0314, gpt-4-32k-0613, gpt-3.5-turbo-16k, gpt-3.5-turbo-16k-0613, gpt-3.5-turbo, gpt-3.5-turbo-0301, gpt-3.5-turbo-0613, text-davinci-003, text-davinci-002, code-davinci-002
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: xxx
# Socks代理端口可选和 SOCKS_PROXY_HOST 一起时生效
SOCKS_PROXY_PORT: xxx
# HTTPS 代理,可选,支持 httphttpssocks5
HTTPS_PROXY: http://xxx:7890
```
- `OPENAI_API_BASE_URL` 可选,设置 `OPENAI_API_KEY` 时可用
- `OPENAI_API_MODEL` 可选,设置 `OPENAI_API_KEY` 时可用
#### 防止爬虫抓取
**nginx**
将下面配置填入nginx配置文件中可以参考 `docker-compose/nginx/nginx.conf` 文件中添加反爬虫的方法
```
# 防止爬虫抓取
if ($http_user_agent ~* "360Spider|JikeSpider|Spider|spider|bot|Bot|2345Explorer|curl|wget|webZIP|qihoobot|Baiduspider|Googlebot|Googlebot-Mobile|Googlebot-Image|Mediapartners-Google|Adsbot-Google|Feedfetcher-Google|Yahoo! Slurp|Yahoo! Slurp China|YoudaoBot|Sosospider|Sogou spider|Sogou web spider|MSNBot|ia_archiver|Tomato Bot|NSPlayer|bingbot")
{
return 403;
}
```
### 使用 Railway 部署
[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/new/template/yytmgc)
#### Railway 环境变量
| 环境变量名称 | 必填 | 备注 |
| --------------------- | ---------------------- | -------------------------------------------------------------------------------------------------- |
| `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) |
| `OPENAI_API_BASE_URL` | 可选,`OpenAI API` 时可用 | `API`接口地址 |
| `OPENAI_API_MODEL` | 可选,`OpenAI API` 时可用 | `API`模型 |
| `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代理端口 |
| `SOCKS_PROXY_USERNAME` | 可选,和 `SOCKS_PROXY_HOST` 一起时生效 | Socks代理用户名 |
| `SOCKS_PROXY_PASSWORD` | 可选,和 `SOCKS_PROXY_HOST` 一起时生效 | Socks代理密码 |
| `HTTPS_PROXY` | 可选 | HTTPS 代理,支持 httphttps, socks5 |
| `ALL_PROXY` | 可选 | 所有代理 代理,支持 httphttps, socks5 |
> 注意: `Railway` 修改环境变量会重新 `Deploy`
### 使用 Sealos 部署
[![](https://raw.githubusercontent.com/labring-actions/templates/main/Deploy-on-Sealos.svg)](https://cloud.sealos.io/?openapp=system-fastdeploy%3FtemplateName%3Dchatgpt-web)
> 环境变量与 Docker 环境变量一致
### 手动打包
#### 后端服务
> 如果你不需要本项目的 `node` 接口,可以省略如下操作
复制 `service` 文件夹到你有 `node` 服务环境的服务器上。
```shell
# 安装
pnpm install
# 打包
pnpm build
# 运行
pnpm prod
```
PS: 不进行打包,直接在服务器上运行 `pnpm start` 也可
#### 前端网页
1、修改根目录下 `.env` 文件中的 `VITE_GLOB_API_URL` 为你的实际后端接口地址
2、根目录下运行以下命令然后将 `dist` 文件夹内的文件复制到你网站服务的根目录下
[参考信息](https://cn.vitejs.dev/guide/static-deploy.html#building-the-app)
```shell
pnpm build
```
## 常见问题
Q: 为什么 `Git` 提交总是报错?
A: 因为有提交信息验证,请遵循 [Commit 指南](./CONTRIBUTING.md)
Q: 如果只使用前端页面,在哪里改请求接口?
A: 根目录下 `.env` 文件中的 `VITE_GLOB_API_URL` 字段。
Q: 文件保存时全部爆红?
A: `vscode` 请安装项目推荐插件,或手动安装 `Eslint` 插件。
Q: 前端没有打字机效果?
A: 一种可能原因是经过 Nginx 反向代理,开启了 buffer则 Nginx 会尝试从后端缓冲一定大小的数据再发送给浏览器。请尝试在反代参数后添加 `proxy_buffering off;`,然后重载 Nginx。其他 web server 配置同理。
## 参与贡献
贡献之前请先阅读 [贡献指南](./CONTRIBUTING.md)
感谢所有做过贡献的人!
<a href="https://github.com/Chanzhaoyu/chatgpt-web/graphs/contributors">
<img src="https://contrib.rocks/image?repo=Chanzhaoyu/chatgpt-web" />
</a>
## 致谢
感谢 [JetBrains](https://www.jetbrains.com/) 为这个项目提供免费开源许可的软件。
## 赞助
如果你觉得这个项目对你有帮助,并且情况允许的话,可以给我一点点支持,总之非常感谢支持~
<div style="display: flex; gap: 20px;">
<div style="text-align: center">
<img style="max-width: 100%" src="./docs/wechat.png" alt="微信" />
<p>WeChat Pay</p>
</div>
<div style="text-align: center">
<img style="max-width: 100%" src="./docs/alipay.png" alt="支付宝" />
<p>Alipay</p>
</div>
</div>
## License
MIT © [ChenZhaoYu](./license)

View File

@@ -1 +0,0 @@
export * from './proxy'

View File

@@ -1,16 +0,0 @@
import type { ProxyOptions } from 'vite'
export function createViteProxy(isOpenProxy: boolean, viteEnv: ImportMetaEnv) {
if (!isOpenProxy)
return
const proxy: Record<string, string | ProxyOptions> = {
'/api': {
target: viteEnv.VITE_APP_API_BASE_URL,
changeOrigin: true,
rewrite: path => path.replace('/api/', '/'),
},
}
return proxy
}

14
docker-compose/README.md Normal file
View File

@@ -0,0 +1,14 @@
### docker-compose Deployment Tutorial
-Put the packaged front-end files in the `nginx/html` directory
- ```shell
# start up
docker-compose up -d
```
- ```shell
# Check the running status
docker ps
```
- ```shell
# end run
docker-compose down
```

View File

@@ -2,33 +2,39 @@ version: '3'
services:
app:
image: chenzhaoyu94/chatgpt-web # 总是使用latest,更新时重新pull该tag镜像即可
container_name: chatgpt-web
image: chenzhaoyu94/chatgpt-web # Always use latest, just pull the tag image again when updating
ports:
- 3002:3002
environment:
# 二选一
OPENAI_API_KEY: sk-xxx
# 二选一
OPENAI_ACCESS_TOKEN: xxx
# API接口地址,可选,设置 OPENAI_API_KEY 时可用
OPENAI_API_BASE_URL: xxx
# API模型,可选,设置 OPENAI_API_KEY 时可用
OPENAI_API_MODEL: xxx
# 反向代理,可选
API_REVERSE_PROXY: xxx
# 访问权限密钥,可选
AUTH_SECRET_KEY: xxx
# 每小时最大请求次数,可选,默认无限
# pick one of two
OPENAI_API_KEY:
# pick one of two
OPENAI_ACCESS_TOKEN:
# API interface address, optional, available when OPENAI_API_KEY is set
OPENAI_API_BASE_URL:
# API model, optional, available when OPENAI_API_KEY is set
OPENAI_API_MODEL:
# reverse proxy, optional
API_REVERSE_PROXY:
# Access permission key, optional
AUTH_SECRET_KEY:
# The maximum number of requests per hour, optional, default unlimited
MAX_REQUEST_PER_HOUR: 0
# 超时,单位毫秒,可选
# timeout in milliseconds, optional
TIMEOUT_MS: 60000
# Socks代理,可选,和 SOCKS_PROXY_PORT 一起时生效
SOCKS_PROXY_HOST: xxx
# Socks代理端口,可选,和 SOCKS_PROXY_HOST 一起时生效
SOCKS_PROXY_PORT: xxx
# HTTPS_PROXY 代理,可选
HTTPS_PROXY: http://xxx:7890
# Socks proxy, optional, works with SOCKS_PROXY_PORT
SOCKS_PROXY_HOST:
# Socks proxy port, optional, effective when combined with SOCKS_PROXY_HOST
SOCKS_PROXY_PORT:
# Socks proxy username, optional, effective when combined with SOCKS_PROXY_HOST & SOCKS_PROXY_PORT
SOCKS_PROXY_USERNAME:
# Socks proxy password, optional, effective when combined with SOCKS_PROXY_HOST & SOCKS_PROXY_PORT
SOCKS_PROXY_PASSWORD:
# HTTPS_PROXY proxy, optional
HTTPS_PROXY:
nginx:
container_name: nginx
image: nginx:alpine
ports:
- '80:80'

View File

@@ -3,13 +3,20 @@ server {
server_name localhost;
charset utf-8;
error_page 500 502 503 504 /50x.html;
# Prevent crawlers from crawling
if ($http_user_agent ~* "360Spider|JikeSpider|Spider|spider|bot|Bot|2345Explorer|curl|wget|webZIP|qihoobot|Baiduspider|Googlebot|Googlebot-Mobile|Googlebot-Image|Mediapartners-Google|Adsbot-Google|Feedfetcher-Google|Yahoo! Slurp|Yahoo! Slurp China|YoudaoBot|Sosospider|Sogou spider|Sogou web spider|MSNBot|ia_archiver|Tomato Bot|NSPlayer|bingbot")
{
return 403;
}
location / {
root /usr/share/nginx/html;
try_files $uri /index.html;
}
location /api {
proxy_set_header X-Real-IP $remote_addr; #转发用户IP
proxy_set_header X-Real-IP $remote_addr; #Forward user IP
proxy_pass http://app:3002;
}

View File

@@ -1,14 +0,0 @@
### docker-compose 部署教程
- 将打包好的前端文件放到 `nginx/html` 目录下
- ```shell
# 启动
docker-compose up -d
```
- ```shell
# 查看运行状态
docker ps
```
- ```shell
# 结束运行
docker-compose down
```

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="zh-cmn-Hans">
<html>
<head>
<meta charset="UTF-8">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">

9
kubernetes/README.md Normal file
View File

@@ -0,0 +1,9 @@
## 增加一个Kubernetes的部署方式
```
kubectl apply -f deploy.yaml
```
### 如果需要Ingress域名接入
```
kubectl apply -f ingress.yaml
```

66
kubernetes/deploy.yaml Normal file
View File

@@ -0,0 +1,66 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: chatgpt-web
labels:
app: chatgpt-web
spec:
replicas: 1
selector:
matchLabels:
app: chatgpt-web
strategy:
type: RollingUpdate
template:
metadata:
labels:
app: chatgpt-web
spec:
containers:
- image: chenzhaoyu94/chatgpt-web
name: chatgpt-web
imagePullPolicy: Always
ports:
- containerPort: 3002
env:
- name: OPENAI_API_KEY
value: sk-xxx
- name: OPENAI_API_BASE_URL
value: 'https://api.openai.com'
- name: OPENAI_API_MODEL
value: gpt-3.5-turbo
- name: API_REVERSE_PROXY
value: https://ai.fakeopen.com/api/conversation
- name: AUTH_SECRET_KEY
value: '123456'
- name: TIMEOUT_MS
value: '60000'
- name: SOCKS_PROXY_HOST
value: ''
- name: SOCKS_PROXY_PORT
value: ''
- name: HTTPS_PROXY
value: ''
resources:
limits:
cpu: 500m
memory: 500Mi
requests:
cpu: 300m
memory: 300Mi
---
apiVersion: v1
kind: Service
metadata:
labels:
app: chatgpt-web
name: chatgpt-web
spec:
ports:
- name: chatgpt-web
port: 3002
protocol: TCP
targetPort: 3002
selector:
app: chatgpt-web
type: ClusterIP

21
kubernetes/ingress.yaml Normal file
View File

@@ -0,0 +1,21 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
kubernetes.io/ingress.class: nginx
nginx.ingress.kubernetes.io/proxy-connect-timeout: '5'
name: chatgpt-web
spec:
rules:
- host: chatgpt.example.com
http:
paths:
- backend:
service:
name: chatgpt-web
port:
number: 3002
path: /
pathType: ImplementationSpecific
tls:
- secretName: chatgpt-web-tls

View File

@@ -1,6 +1,6 @@
{
"name": "chatgpt-web",
"version": "2.10.8",
"version": "2.11.1",
"private": false,
"description": "ChatGPT Web",
"author": "ChenZhaoYu <chenzhaoyu1994@gmail.com>",
@@ -23,12 +23,13 @@
"common:prepare": "husky install"
},
"dependencies": {
"@traptitech/markdown-it-katex": "^3.6.0",
"@vscode/markdown-it-katex": "^1.0.3",
"@vueuse/core": "^9.13.0",
"highlight.js": "^11.7.0",
"html2canvas": "^1.4.1",
"html-to-image": "^1.11.11",
"katex": "^0.16.4",
"markdown-it": "^13.0.1",
"mermaid-it-markdown": "^1.0.8",
"naive-ui": "^2.34.3",
"pinia": "^2.0.33",
"vue": "^3.2.47",
@@ -56,7 +57,7 @@
"markdown-it-link-attributes": "^4.0.1",
"npm-run-all": "^4.1.5",
"postcss": "^8.4.21",
"rimraf": "^4.2.0",
"rimraf": "^4.3.0",
"tailwindcss": "^3.2.7",
"typescript": "~4.9.5",
"vite": "^4.2.0",

11248
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,7 +10,12 @@ OPENAI_API_BASE_URL=
# OpenAI API Model - https://platform.openai.com/docs/models
OPENAI_API_MODEL=
# Reverse Proxy
# set `true` to disable OpenAI API debug log
OPENAI_API_DISABLE_DEBUG=
# Reverse Proxy - Available on accessToken
# Default: https://ai.fakeopen.com/api/conversation
# More: https://github.com/transitive-bullshit/chatgpt-api#reverse-proxy
API_REVERSE_PROXY=
# timeout
@@ -28,6 +33,12 @@ SOCKS_PROXY_HOST=
# Socks Proxy Port
SOCKS_PROXY_PORT=
# Socks Proxy Username
SOCKS_PROXY_USERNAME=
# Socks Proxy Password
SOCKS_PROXY_PASSWORD=
# HTTPS PROXY
HTTPS_PROXY=

7031
service/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -11,12 +11,12 @@
"express"
],
"engines": {
"node": "^16 || ^18 || ^19"
"node": "^16 || ^18 || ^20"
},
"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 .",
@@ -27,7 +27,7 @@
"axios": "^1.3.4",
"chatgpt": "^5.1.2",
"dotenv": "^16.0.3",
"esno": "^0.16.3",
"esno": "^4.7.0",
"express": "^4.18.2",
"express-rate-limit": "^6.7.0",
"https-proxy-agent": "^5.0.1",

6103
service/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,13 +3,14 @@ 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 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'
import type { RequestOptions, SetProxyOptions, UsageResponse } from './types'
const { HttpsProxyAgent } = httpsProxyAgent
dotenv.config()
@@ -22,9 +23,11 @@ const ErrorCodeMessage: Record<string, string> = {
500: '[OpenAI] 服务器繁忙,请稍后再试 | Internal Server Error',
}
const timeoutMs: number = !isNaN(+process.env.TIMEOUT_MS) ? +process.env.TIMEOUT_MS : 30 * 1000
const timeoutMs: number = !isNaN(+process.env.TIMEOUT_MS) ? +process.env.TIMEOUT_MS : 100 * 1000
const disableDebug: boolean = process.env.OPENAI_API_DISABLE_DEBUG === 'true'
let apiModel: ApiModel
const model = isNotEmptyString(process.env.OPENAI_API_MODEL) ? process.env.OPENAI_API_MODEL : 'gpt-3.5-turbo'
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')
@@ -36,13 +39,11 @@ let api: ChatGPTAPI | ChatGPTUnofficialProxyAPI
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 = isNotEmptyString(OPENAI_API_MODEL) ? OPENAI_API_MODEL : 'gpt-3.5-turbo'
const options: ChatGPTAPIOptions = {
apiKey: process.env.OPENAI_API_KEY,
completionParams: { model },
debug: true,
debug: !disableDebug,
}
// increase max token limit if use gpt-4
@@ -52,14 +53,34 @@ let api: ChatGPTAPI | ChatGPTUnofficialProxyAPI
options.maxModelTokens = 32768
options.maxResponseTokens = 8192
}
else if (/-4o-mini/.test(model.toLowerCase())) {
options.maxModelTokens = 128000
options.maxResponseTokens = 16384
}
// if use GPT-4 Turbo or GPT-4o
else if (/-preview|-turbo|o/.test(model.toLowerCase())) {
options.maxModelTokens = 128000
options.maxResponseTokens = 4096
}
else {
options.maxModelTokens = 8192
options.maxResponseTokens = 2048
}
}
else if (model.toLowerCase().includes('gpt-3.5')) {
if (/16k|1106|0125/.test(model.toLowerCase())) {
options.maxModelTokens = 16384
options.maxResponseTokens = 4096
}
}
if (isNotEmptyString(OPENAI_API_BASE_URL))
options.apiBaseUrl = `${OPENAI_API_BASE_URL}/v1`
if (isNotEmptyString(OPENAI_API_BASE_URL)) {
// if find /v1 in OPENAI_API_BASE_URL then use it
if (OPENAI_API_BASE_URL.includes('/v1'))
options.apiBaseUrl = `${OPENAI_API_BASE_URL}`
else
options.apiBaseUrl = `${OPENAI_API_BASE_URL}/v1`
}
setupProxy(options)
@@ -67,16 +88,12 @@ let api: ChatGPTAPI | ChatGPTUnofficialProxyAPI
apiModel = 'ChatGPTAPI'
}
else {
const OPENAI_API_MODEL = process.env.OPENAI_API_MODEL
const options: ChatGPTUnofficialProxyAPIOptions = {
accessToken: process.env.OPENAI_ACCESS_TOKEN,
debug: true,
apiReverseProxyUrl: isNotEmptyString(process.env.API_REVERSE_PROXY) ? process.env.API_REVERSE_PROXY : 'https://ai.fakeopen.com/api/conversation',
model,
debug: !disableDebug,
}
if (isNotEmptyString(OPENAI_API_MODEL))
options.model = OPENAI_API_MODEL
if (isNotEmptyString(process.env.API_REVERSE_PROXY))
options.apiReverseProxyUrl = process.env.API_REVERSE_PROXY
setupProxy(options)
@@ -86,13 +103,14 @@ let api: ChatGPTAPI | ChatGPTUnofficialProxyAPI
})()
async function chatReplyProcess(options: RequestOptions) {
const { message, lastContext, process, systemMessage } = options
const { message, lastContext, process, systemMessage, temperature, top_p } = options
try {
let options: SendMessageOptions = { timeoutMs }
if (apiModel === 'ChatGPTAPI') {
if (isNotEmptyString(systemMessage))
options.systemMessage = systemMessage
options.completionParams = { model, temperature, top_p }
}
if (lastContext != null) {
@@ -120,7 +138,7 @@ async function chatReplyProcess(options: RequestOptions) {
}
}
async function fetchBalance() {
async function fetchUsage() {
const OPENAI_API_KEY = process.env.OPENAI_API_KEY
const OPENAI_API_BASE_URL = process.env.OPENAI_API_BASE_URL
@@ -131,19 +149,47 @@ async function fetchBalance() {
? 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))
const [startDate, endDate] = formatDate()
// 每月使用量
const urlUsage = `${API_BASE_URL}/v1/dashboard/billing/usage?start_date=${startDate}&end_date=${endDate}`
const headers = {
'Authorization': `Bearer ${OPENAI_API_KEY}`,
'Content-Type': 'application/json',
}
catch {
const options = {} as SetProxyOptions
setupProxy(options)
try {
// 获取已使用量
const useResponse = await options.fetch(urlUsage, { headers })
if (!useResponse.ok)
throw new Error('获取使用量失败')
const usageData = await useResponse.json() as UsageResponse
const usage = Math.round(usageData.total_usage) / 100
return Promise.resolve(usage ? `$${usage}` : '-')
}
catch (error) {
global.console.log(error)
return Promise.resolve('-')
}
}
function formatDate(): string[] {
const today = new Date()
const year = today.getFullYear()
const month = today.getMonth() + 1
const lastDay = new Date(year, month, 0)
const formattedFirstDay = `${year}-${month.toString().padStart(2, '0')}-01`
const formattedLastDay = `${year}-${month.toString().padStart(2, '0')}-${lastDay.getDate().toString().padStart(2, '0')}`
return [formattedFirstDay, formattedLastDay]
}
async function chatConfig() {
const balance = await fetchBalance()
const usage = await fetchUsage()
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)
@@ -151,31 +197,36 @@ async function chatConfig() {
: '-'
return sendResponse<ModelConfig>({
type: 'Success',
data: { apiModel, reverseProxy, timeoutMs, socksProxy, httpsProxy, balance },
data: { apiModel, reverseProxy, timeoutMs, socksProxy, httpsProxy, usage },
})
}
function setupProxy(options: ChatGPTAPIOptions | ChatGPTUnofficialProxyAPIOptions) {
if (process.env.SOCKS_PROXY_HOST && process.env.SOCKS_PROXY_PORT) {
function setupProxy(options: SetProxyOptions) {
if (isNotEmptyString(process.env.SOCKS_PROXY_HOST) && isNotEmptyString(process.env.SOCKS_PROXY_PORT)) {
const agent = new SocksProxyAgent({
hostname: process.env.SOCKS_PROXY_HOST,
port: process.env.SOCKS_PROXY_PORT,
userId: isNotEmptyString(process.env.SOCKS_PROXY_USERNAME) ? process.env.SOCKS_PROXY_USERNAME : undefined,
password: isNotEmptyString(process.env.SOCKS_PROXY_PASSWORD) ? process.env.SOCKS_PROXY_PASSWORD : undefined,
})
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 })
}
else if (isNotEmptyString(process.env.HTTPS_PROXY) || isNotEmptyString(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 })
}
}
}
else {
options.fetch = (url, options) => {
return fetch(url, { ...options })
}
}
}
function currentModel(): ApiModel {

View File

@@ -1,8 +1,19 @@
import type { ChatMessage } from 'chatgpt'
import type fetch from 'node-fetch'
export interface RequestOptions {
message: string
lastContext?: { conversationId?: string; parentMessageId?: string }
process?: (chat: ChatMessage) => void
systemMessage?: string
temperature?: number
top_p?: number
}
export interface SetProxyOptions {
fetch?: typeof fetch
}
export interface UsageResponse {
total_usage: number
}

View File

@@ -23,7 +23,7 @@ router.post('/chat-process', [auth, limiter], async (req, res) => {
res.setHeader('Content-type', 'application/octet-stream')
try {
const { prompt, options = {}, systemMessage } = req.body as RequestProps
const { prompt, options = {}, systemMessage, temperature, top_p } = req.body as RequestProps
let firstChunk = true
await chatReplyProcess({
message: prompt,
@@ -33,6 +33,8 @@ router.post('/chat-process', [auth, limiter], async (req, res) => {
firstChunk = false
},
systemMessage,
temperature,
top_p,
})
}
catch (error) {
@@ -82,5 +84,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'))

View File

@@ -4,6 +4,8 @@ export interface RequestProps {
prompt: string
options?: ChatContext
systemMessage: string
temperature?: number
top_p?: number
}
export interface ChatContext {
@@ -26,7 +28,7 @@ export interface ModelConfig {
timeoutMs?: number
socksProxy?: string
httpsProxy?: string
balance?: string
usage?: string
}
export type ApiModel = 'ChatGPTAPI' | 'ChatGPTUnofficialProxyAPI' | undefined

View File

@@ -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,

View File

@@ -1,6 +1,6 @@
import type { AxiosProgressEvent, GenericAbortSignal } from 'axios'
import { post } from '@/utils/request'
import { useSettingStore } from '@/store'
import { useAuthStore, useSettingStore } from '@/store'
export function fetchChatAPI<T = any>(
prompt: string,
@@ -28,10 +28,25 @@ export function fetchChatAPIProcess<T = any>(
onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void },
) {
const settingStore = useSettingStore()
const authStore = useAuthStore()
let data: Record<string, any> = {
prompt: params.prompt,
options: params.options,
}
if (authStore.isChatGPTAPI) {
data = {
...data,
systemMessage: settingStore.systemMessage,
temperature: settingStore.temperature,
top_p: settingStore.top_p,
}
}
return post<T>({
url: '/chat-process',
data: { prompt: params.prompt, options: params.options, systemMessage: settingStore.systemMessage },
data,
signal: params.signal,
onDownloadProgress: params.onDownloadProgress,
})

View File

@@ -147,7 +147,7 @@ const clearPromptTemplate = () => {
message.success(t('common.clearSuccess'))
}
const importPromptTemplate = () => {
const importPromptTemplate = (from = 'online') => {
try {
const jsonData = JSON.parse(tempPromptValue.value)
let key = ''
@@ -168,7 +168,7 @@ const importPromptTemplate = () => {
}
for (const i of jsonData) {
if (!('key' in i) || !('value' in i))
if (!(key in i) || !(value in i))
throw new Error(t('store.importError'))
let safe = true
for (const j of promptList.value) {
@@ -191,6 +191,8 @@ const importPromptTemplate = () => {
catch {
message.error('JSON 格式错误,请检查 JSON 格式')
}
if (from === 'local')
showModal.value = !showModal.value
}
// 模板导出
@@ -469,7 +471,7 @@ const dataSource = computed(() => {
block
type="primary"
:disabled="inputStatus"
@click="() => { importPromptTemplate() }"
@click="() => { importPromptTemplate('local') }"
>
{{ t('common.import') }}
</NButton>

View File

@@ -1,8 +1,8 @@
<script setup lang='ts'>
import { computed, onMounted, ref } from 'vue'
import { NSpin } from 'naive-ui'
import pkg from '../../../../package.json'
import { fetchChatConfig } from '@/api'
import pkg from '@/../package.json'
import { useAuthStore } from '@/store'
interface ConfigState {
@@ -11,7 +11,7 @@ interface ConfigState {
apiModel?: string
socksProxy?: string
httpsProxy?: string
balance?: string
usage?: string
}
const authStore = useAuthStore()
@@ -46,23 +46,23 @@ onMounted(() => {
</h2>
<div class="p-2 space-y-2 rounded-md bg-neutral-100 dark:bg-neutral-700">
<p>
此项目开源于
{{ $t("setting.openSource") }}
<a
class="text-blue-600 dark:text-blue-500"
href="https://github.com/Chanzhaoyu/chatgpt-web"
target="_blank"
>
Github
GitHub
</a>
免费且基于 MIT 协议没有任何形式的付费行为
{{ $t("setting.freeMIT") }}
</p>
<p>
如果你觉得此项目对你有帮助请在 Github 帮我点个 Star 或者给予一点赞助谢谢
{{ $t("setting.stars") }}
</p>
</div>
<p>{{ $t("setting.api") }}{{ config?.apiModel ?? '-' }}</p>
<p v-if="isChatGPTAPI">
{{ $t("setting.balance") }}{{ config?.balance ?? '-' }}
{{ $t("setting.monthlyUsage") }}{{ config?.usage ?? '-' }}
</p>
<p v-if="!isChatGPTAPI">
{{ $t("setting.reverseProxy") }}{{ config?.reverseProxy ?? '-' }}

View File

@@ -1,6 +1,6 @@
<script lang="ts" setup>
import { ref } from 'vue'
import { NButton, NInput, useMessage } from 'naive-ui'
import { NButton, NInput, NSlider, useMessage } from 'naive-ui'
import { useSettingStore } from '@/store'
import type { SettingsState } from '@/store/modules/settings/helper'
import { t } from '@/locales'
@@ -11,6 +11,10 @@ const ms = useMessage()
const systemMessage = ref(settingStore.systemMessage ?? '')
const temperature = ref(settingStore.temperature ?? 0.5)
const top_p = ref(settingStore.top_p ?? 1)
function updateSettings(options: Partial<SettingsState>) {
settingStore.updateSetting(options)
ms.success(t('common.success'))
@@ -27,16 +31,36 @@ function handleReset() {
<div class="p-4 space-y-5 min-h-[200px]">
<div class="space-y-6">
<div class="flex items-center space-x-4">
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.role') }}</span>
<span class="flex-shrink-0 w-[120px]">{{ $t('setting.role') }}</span>
<div class="flex-1">
<NInput v-model:value="systemMessage" placeholder="" />
<NInput v-model:value="systemMessage" type="textarea" :autosize="{ minRows: 1, maxRows: 4 }" />
</div>
<NButton size="tiny" text type="primary" @click="updateSettings({ systemMessage })">
{{ $t('common.save') }}
</NButton>
</div>
<div class="flex items-center space-x-4">
<span class="flex-shrink-0 w-[100px]">&nbsp;</span>
<span class="flex-shrink-0 w-[120px]">{{ $t('setting.temperature') }} </span>
<div class="flex-1">
<NSlider v-model:value="temperature" :max="2" :min="0" :step="0.1" />
</div>
<span>{{ temperature }}</span>
<NButton size="tiny" text type="primary" @click="updateSettings({ temperature })">
{{ $t('common.save') }}
</NButton>
</div>
<div class="flex items-center space-x-4">
<span class="flex-shrink-0 w-[120px]">{{ $t('setting.top_p') }} </span>
<div class="flex-1">
<NSlider v-model:value="top_p" :max="1" :min="0" :step="0.1" />
</div>
<span>{{ top_p }}</span>
<NButton size="tiny" text type="primary" @click="updateSettings({ top_p })">
{{ $t('common.save') }}
</NButton>
</div>
<div class="flex items-center space-x-4">
<span class="flex-shrink-0 w-[120px]">&nbsp;</span>
<NButton size="small" @click="handleReset">
{{ $t('common.reset') }}
</NButton>

View File

@@ -54,9 +54,13 @@ const themeOptions: { label: string; key: Theme; icon: string }[] = [
]
const languageOptions: { label: string; key: Language; value: Language }[] = [
{ label: 'English', key: 'en-US', value: 'en-US' },
{ label: 'Español', key: 'es-ES', value: 'es-ES' },
{ label: '한국어', key: 'ko-KR', value: 'ko-KR' },
{ label: 'Русский язык', key: 'ru-RU', value: 'ru-RU' },
{ label: 'Tiếng Việt', key: 'vi-VN', value: 'vi-VN' },
{ label: '简体中文', key: 'zh-CN', value: 'zh-CN' },
{ label: '繁體中文', key: 'zh-TW', value: 'zh-TW' },
{ label: 'English', key: 'en-US', value: 'en-US' },
]
function updateUserInfo(options: Partial<UserInfo>) {

View File

@@ -17,5 +17,5 @@ const bindAttrs = computed<{ class: string; style: string }>(() => ({
</script>
<template>
<Icon :icon="icon" v-bind="bindAttrs" />
<Icon :icon="icon || ''" v-bind="bindAttrs" />
</template>

View File

@@ -1,5 +1,5 @@
import { computed } from 'vue'
import { enUS, zhCN, zhTW } from 'naive-ui'
import { enUS, esAR, koKR, ruRU, viVN, zhCN, zhTW } from 'naive-ui'
import { useAppStore } from '@/store'
import { setLocale } from '@/locales'
@@ -7,18 +7,23 @@ export function useLanguage() {
const appStore = useAppStore()
const language = computed(() => {
setLocale(appStore.language)
switch (appStore.language) {
case 'en-US':
setLocale('en-US')
return enUS
case 'es-ES':
return esAR
case 'ko-KR':
return koKR
case 'vi-VN':
return viVN
case 'ru-RU':
return ruRU
case 'zh-CN':
setLocale('zh-CN')
return zhCN
case 'zh-TW':
setLocale('zh-TW')
return zhTW
default:
setLocale('zh-CN')
return enUS
}
})

View File

@@ -26,13 +26,17 @@ export default {
failed: 'Failed',
verify: 'Verify',
unauthorizedTips: 'Unauthorized, please verify first.',
stopResponding: 'Stop Responding',
},
chat: {
placeholder: 'Ask me anything...(Shift + Enter = line break)',
newChatButton: 'New Chat',
newChatTitle: 'New Chat',
placeholder: 'Ask me anything...(Shift + Enter = line break, "/" to trigger prompts)',
placeholderMobile: 'Ask me anything...',
copy: 'Copy',
copied: 'Copied',
copyCode: 'Copy Code',
copyFailed: 'Copy Failed',
clearChat: 'Clear Chat',
clearChatConfirm: 'Are you sure to clear this chat?',
exportImage: 'Export Image',
@@ -48,6 +52,7 @@ export default {
clearHistoryConfirm: 'Are you sure to clear chat history?',
preview: 'Preview',
showRawText: 'Show as raw text',
thinking: 'Thinking...',
},
setting: {
setting: 'Setting',
@@ -58,6 +63,8 @@ export default {
name: 'Name',
description: 'Description',
role: 'Role',
temperature: 'Temperature',
top_p: 'Top_p',
resetUserInfo: 'Reset UserInfo',
chatHistory: 'ChatHistory',
theme: 'Theme',
@@ -68,8 +75,13 @@ export default {
socks: 'Socks',
httpsProxy: 'HTTPS Proxy',
balance: 'API Balance',
monthlyUsage: 'Monthly Usage',
openSource: 'This project is open sourced at',
freeMIT: 'free and based on the MIT license, without any form of paid behavior!',
stars: 'If you find this project helpful, please give me a Star on GitHub or give a little sponsorship, thank you!',
},
store: {
siderButton: 'Prompt Store',
local: 'Local',
online: 'Online',
title: 'Title',

100
src/locales/es-ES.ts Normal file
View File

@@ -0,0 +1,100 @@
export default {
common: {
add: 'Agregar',
addSuccess: 'Agregado con éxito',
edit: 'Editar',
editSuccess: 'Edición exitosa',
delete: 'Borrar',
deleteSuccess: 'Borrado con éxito',
save: 'Guardar',
saveSuccess: 'Guardado con éxito',
reset: 'Reiniciar',
action: 'Acción',
export: 'Exportar',
exportSuccess: 'Exportación exitosa',
import: 'Importar',
importSuccess: 'Importación exitosa',
clear: 'Limpiar',
clearSuccess: 'Limpieza exitosa',
yes: 'Sí',
no: 'No',
confirm: 'Confirmar',
download: 'Descargar',
noData: 'Sin datos',
wrong: 'Algo salió mal, inténtalo de nuevo más tarde.',
success: 'Exitoso',
failed: 'Fallido',
verify: 'Verificar',
unauthorizedTips: 'No autorizado, por favor verifique primero.',
stopResponding: 'No responde',
},
chat: {
newChatButton: 'Nueva conversación',
newChatTitle: 'Nueva conversación',
placeholder: 'Pregúntame lo que sea...(Shift + Enter = salto de línea, "/" para activar avisos)',
placeholderMobile: 'Pregúntame lo que sea...',
copy: 'Copiar',
copied: 'Copiado',
copyCode: 'Copiar código',
copyFailed: 'Copia fallida',
clearChat: 'Limpiar chat',
clearChatConfirm: '¿Estás seguro de borrar este chat?',
exportImage: 'Exportar imagen',
exportImageConfirm: '¿Estás seguro de exportar este chat a png?',
exportSuccess: 'Exportación exitosa',
exportFailed: 'Exportación fallida',
usingContext: 'Modo de contexto',
turnOnContext: 'En el modo actual, el envío de mensajes llevará registros de chat anteriores.',
turnOffContext: 'En el modo actual, el envío de mensajes no incluirá registros de conversaciones anteriores.',
deleteMessage: 'Borrar mensaje',
deleteMessageConfirm: '¿Estás seguro de eliminar este mensaje?',
deleteHistoryConfirm: '¿Estás seguro de borrar esta historia?',
clearHistoryConfirm: '¿Estás seguro de borrar el historial de chat?',
preview: 'Avance',
showRawText: 'Mostrar como texto sin formato',
},
setting: {
setting: 'Configuración',
general: 'General',
advanced: 'Avanzado',
config: 'Configurar',
avatarLink: 'Enlace de avatar',
name: 'Nombre',
description: 'Descripción',
role: 'Rol',
temperature: 'Temperatura',
top_p: 'Top_p',
resetUserInfo: 'Restablecer información de usuario',
chatHistory: 'Historial de chat',
theme: 'Tema',
language: 'Idioma',
api: 'API',
reverseProxy: 'Reverse Proxy',
timeout: 'Tiempo de espera',
socks: 'Socks',
httpsProxy: 'HTTPS Proxy',
balance: 'Saldo de API',
monthlyUsage: 'Uso mensual de API',
openSource: 'Este proyecto es de código abierto en',
freeMIT: 'gratis y basado en la licencia MIT, ¡sin ningún tipo de comportamiento de pago!',
stars: 'Si encuentras este proyecto útil, por favor dame una Estrella en GitHub o da un pequeño patrocinio, ¡gracias!',
},
store: {
siderButton: 'Tienda rápida',
local: 'Local',
online: 'En línea',
title: 'Título',
description: 'Descripción',
clearStoreConfirm: '¿Estás seguro de borrar los datos?',
importPlaceholder: 'Pegue los datos JSON aquí',
addRepeatTitleTips: 'Título duplicado, vuelva a ingresar',
addRepeatContentTips: 'Contenido duplicado: {msg}, por favor vuelva a entrar',
editRepeatTitleTips: 'Conflicto de título, revíselo',
editRepeatContentTips: 'Conflicto de contenido {msg} , por favor vuelva a modificar',
importError: 'Discrepancia de valor clave',
importRepeatTitle: 'Título saltado repetidamente: {msg}',
importRepeatContent: 'El contenido se salta repetidamente: {msg}',
onlineImportWarning: 'Nota: ¡Compruebe la fuente del archivo JSON!',
downloadError: 'Verifique el estado de la red y la validez del archivo JSON',
},
}

View File

@@ -1,6 +1,10 @@
import type { App } from 'vue'
import { createI18n } from 'vue-i18n'
import enUS from './en-US'
import esES from './es-ES'
import koKR from './ko-KR'
import ruRU from './ru-RU'
import viVN from './vi-VN'
import zhCN from './zh-CN'
import zhTW from './zh-TW'
import { useAppStoreWithOut } from '@/store/modules/app'
@@ -16,6 +20,10 @@ const i18n = createI18n({
allowComposition: true,
messages: {
'en-US': enUS,
'es-ES': esES,
'ko-KR': koKR,
'ru-RU': ruRU,
'vi-VN': viVN,
'zh-CN': zhCN,
'zh-TW': zhTW,
},

100
src/locales/ko-KR.ts Normal file
View File

@@ -0,0 +1,100 @@
export default {
common: {
add: '추가',
addSuccess: '추가 성공',
edit: '편집',
editSuccess: '편집 성공',
delete: '삭제',
deleteSuccess: '삭제 성공',
save: '저장',
saveSuccess: '저장 성공',
reset: '초기화',
action: '액션',
export: '내보내기',
exportSuccess: '내보내기 성공',
import: '가져오기',
importSuccess: '가져오기 성공',
clear: '비우기',
clearSuccess: '비우기 성공',
yes: '예',
no: '아니오',
confirm: '확인',
download: '다운로드',
noData: '데이터 없음',
wrong: '문제가 발생했습니다. 나중에 다시 시도하십시오.',
success: '성공',
failed: '실패',
verify: '검증',
unauthorizedTips: '인증되지 않았습니다. 먼저 확인하십시오.',
stopResponding: '응답 중지',
},
chat: {
newChatButton: '새로운 채팅',
newChatTitle: '새로운 채팅',
placeholder: '무엇이든 물어보세요...(Shift + Enter = 줄바꿈, "/"를 눌러서 힌트를 보세요)',
placeholderMobile: '무엇이든 물어보세요...',
copy: '복사',
copied: '복사됨',
copyCode: '코드 복사',
copyFailed: '복사 실패',
clearChat: '채팅 비우기',
clearChatConfirm: '이 채팅을 비우시겠습니까?',
exportImage: '이미지 내보내기',
exportImageConfirm: '이 채팅을 png로 내보내시겠습니까?',
exportSuccess: '내보내기 성공',
exportFailed: '내보내기 실패',
usingContext: '컨텍스트 모드',
turnOnContext: '현재 모드에서는 이전 대화 기록을 포함하여 메시지를 보낼 수 있습니다.',
turnOffContext: '현재 모드에서는 이전 대화 기록을 포함하지 않고 메시지를 보낼 수 있습니다.',
deleteMessage: '메시지 삭제',
deleteMessageConfirm: '이 메시지를 삭제하시겠습니까?',
deleteHistoryConfirm: '이 기록을 삭제하시겠습니까?',
clearHistoryConfirm: '채팅 기록을 삭제하시겠습니까?',
preview: '미리보기',
showRawText: '원본 텍스트로 보기',
thinking: '생각 중...',
},
setting: {
setting: '설정',
general: '일반',
advanced: '고급',
config: '설정',
avatarLink: '아바타 링크',
name: '이름',
description: '설명',
role: '역할',
temperature: '온도',
top_p: 'Top_p',
resetUserInfo: '사용자 정보 초기화',
chatHistory: '채팅 기록',
theme: '테마',
language: '언어',
api: 'API',
reverseProxy: '리버스 프록시',
timeout: '타임아웃',
socks: 'Socks',
httpsProxy: 'HTTPS 프록시',
balance: 'API 잔액',
monthlyUsage: '월 사용량',
openSource: '이 프로젝트는 다음에서 오픈 소스로 제공됩니다:',
freeMIT: '무료이며 MIT 라이선스에 기반하며, 어떠한 형태의 유료 행동도 없습니다!',
stars: '이 프로젝트가 도움이 되었다면, GitHub에서 별을 주거나 조금의 후원을 해주시면 감사하겠습니다!',
},
store: {
siderButton: '프롬프트 저장소',
local: '로컬',
online: '온라인',
title: '제목',
description: '설명',
clearStoreConfirm: '데이터를 삭제하시겠습니까?',
importPlaceholder: '여기에 JSON 데이터를 붙여넣으십시오',
addRepeatTitleTips: '제목 중복됨, 다시 입력하십시오',
addRepeatContentTips: '내용 중복됨: {msg}, 다시 입력하십시오',
editRepeatTitleTips: '제목 충돌, 수정하십시오',
editRepeatContentTips: '내용 충돌 {msg} , 수정하십시오',
importError: '키 값 불일치',
importRepeatTitle: '제목이 반복되어 건너뜀: {msg}',
importRepeatContent: '내용이 반복되어 건너뜀: {msg}',
onlineImportWarning: '참고: JSON 파일 소스를 확인하십시오!',
},
}

101
src/locales/ru-RU.ts Normal file
View File

@@ -0,0 +1,101 @@
export default {
common: {
add: 'Добавить',
addSuccess: 'Добавлено успешно',
edit: 'Редактировать',
editSuccess: 'Изменено успешно',
delete: 'Удалить',
deleteSuccess: 'Удалено успешно',
save: 'Сохранить',
saveSuccess: 'Сохранено успешно',
reset: 'Сбросить',
action: 'Действие',
export: 'Экспортировать',
exportSuccess: 'Экспорт выполнен успешно',
import: 'Импортировать',
importSuccess: 'Импорт выполнен успешно',
clear: 'Очистить',
clearSuccess: 'Очищено успешно',
yes: 'Да',
no: 'Нет',
confirm: 'Подтвердить',
download: 'Загрузить',
noData: 'Нет данных',
wrong: 'Что-то пошло не так, пожалуйста, повторите попытку позже.',
success: 'Успех',
failed: 'Не удалось',
verify: 'Проверить',
unauthorizedTips: 'Не авторизован, сначала подтвердите свою личность.',
stopResponding: 'Прекращение отклика',
},
chat: {
newChatButton: 'Новый чат',
newChatTitle: 'Новый чат',
placeholder: 'Спросите меня о чем-нибудь ... (Shift + Enter = перенос строки, "/" для вызова подсказок)',
placeholderMobile: 'Спросите меня о чем-нибудь ...',
copy: 'Копировать',
copied: 'Скопировано',
copyCode: 'Копировать код',
copyFailed: 'Не удалось скопировать',
clearChat: 'Очистить чат',
clearChatConfirm: 'Вы уверены, что хотите очистить этот чат?',
exportImage: 'Экспорт в изображение',
exportImageConfirm: 'Вы уверены, что хотите экспортировать этот чат в формате PNG?',
exportSuccess: 'Экспортировано успешно',
exportFailed: 'Не удалось выполнить экспорт',
usingContext: 'Режим контекста',
turnOnContext: 'В текущем режиме отправка сообщений будет включать предыдущие записи чата.',
turnOffContext: 'В текущем режиме отправка сообщений не будет включать предыдущие записи чата.',
deleteMessage: 'Удалить сообщение',
deleteMessageConfirm: 'Вы уверены, что хотите удалить это сообщение?',
deleteHistoryConfirm: 'Вы уверены, что хотите очистить эту историю?',
clearHistoryConfirm: 'Вы уверены, что хотите очистить историю чата?',
preview: 'Предварительный просмотр',
showRawText: 'Показать как обычный текст',
thinking: 'Думаю...',
},
setting: {
setting: 'Настройки',
general: 'Общее',
advanced: 'Дополнительно',
config: 'Конфигурация',
avatarLink: 'Ссылка на аватар',
name: 'Имя',
description: 'Описание',
role: 'Роль',
temperature: 'Температура',
top_p: 'Top_p',
resetUserInfo: 'Сбросить информацию о пользователе',
chatHistory: 'История чата',
theme: 'Тема',
language: 'Язык',
api: 'API',
reverseProxy: 'Обратный прокси-сервер',
timeout: 'Время ожидания',
socks: 'Socks',
httpsProxy: 'HTTPS-прокси',
balance: 'Баланс API',
monthlyUsage: 'Ежемесячное использование',
openSource: 'Этот проект опубликован в открытом доступе на',
freeMIT: 'бесплатно и основан на лицензии MIT, без каких-либо форм оплаты!',
stars: 'Если вы считаете этот проект полезным, пожалуйста, поставьте мне звезду на GitHub или сделайте небольшое пожертвование, спасибо!',
},
store: {
siderButton: 'Хранилище подсказок',
local: 'Локальное',
online: 'Онлайн',
title: 'Название',
description: 'Описание',
clearStoreConfirm: 'Вы действительно хотите очистить данные?',
importPlaceholder: 'Пожалуйста, вставьте здесь JSON-данные',
addRepeatTitleTips: 'Дубликат названия, пожалуйста, введите другое название',
addRepeatContentTips: 'Дубликат содержимого: {msg}, пожалуйста, введите другой текст',
editRepeatTitleTips: 'Конфликт названий, пожалуйста, измените название',
editRepeatContentTips: 'Конфликт содержимого {msg}, пожалуйста, измените текст',
importError: 'Не совпадает ключ-значение',
importRepeatTitle: 'Название повторяющееся, пропускается: {msg}',
importRepeatContent: 'Содержание повторяющееся, пропускается: {msg}',
onlineImportWarning: 'Внимание! Проверьте источник JSON-файла!',
downloadError: 'Проверьте состояние сети и правильность JSON-файла',
},
}

100
src/locales/vi-VN.ts Normal file
View File

@@ -0,0 +1,100 @@
export default {
common: {
add: 'Thêm',
addSuccess: 'Thêm thành công',
edit: 'Sửa',
editSuccess: 'Sửa thành công',
delete: 'Xóa',
deleteSuccess: 'Xóa thành công',
save: 'Lưu',
saveSuccess: 'Lưu thành công',
reset: 'Đặt lại',
action: 'Hành động',
export: 'Xuất',
exportSuccess: 'Xuất thành công',
import: 'Nhập',
importSuccess: 'Nhập thành công',
clear: 'Dọn dẹp',
clearSuccess: 'Dọn dẹp thành công',
yes: 'Có',
no: 'Không',
confirm: 'Xác nhận',
download: 'Tải xuống',
noData: 'Không có dữ liệu',
wrong: 'Đã xảy ra lỗi, vui lòng thử lại sau.',
success: 'Thành công',
failed: 'Thất bại',
verify: 'Xác minh',
unauthorizedTips: 'Không được ủy quyền, vui lòng xác minh trước.',
},
chat: {
newChatButton: 'Tạo hội thoại',
newChatTitle: 'Tạo hội thoại',
placeholder: 'Hỏi tôi bất cứ điều gì...(Shift + Enter = ngắt dòng, "/" to trigger prompts)',
placeholderMobile: 'Hỏi tôi bất cứ điều gì...',
copy: 'Sao chép',
copied: 'Đã sao chép',
copyCode: 'Sao chép Code',
copyFailed: 'Sao chép thất bại',
clearChat: 'Clear Chat',
clearChatConfirm: 'Bạn có chắc chắn xóa cuộc trò chuyện này?',
exportImage: 'Xuất hình ảnh',
exportImageConfirm: 'Bạn có chắc chắn xuất cuộc trò chuyện này sang png không?',
exportSuccess: 'Xuất thành công',
exportFailed: 'Xuất thất bại',
usingContext: 'Context Mode',
turnOnContext: 'Ở chế độ hiện tại, việc gửi tin nhắn sẽ mang theo các bản ghi trò chuyện trước đó.',
turnOffContext: 'Ở chế độ hiện tại, việc gửi tin nhắn sẽ không mang theo các bản ghi trò chuyện trước đó.',
deleteMessage: 'Xóa tin nhắn',
deleteMessageConfirm: 'Bạn có chắc chắn xóa tin nhắn này?',
deleteHistoryConfirm: 'Bạn có chắc chắn để xóa lịch sử này?',
clearHistoryConfirm: 'Bạn có chắc chắn để xóa lịch sử trò chuyện?',
preview: 'Xem trước',
showRawText: 'Hiển thị dưới dạng văn bản thô',
thinking: 'Đang suy nghĩ...',
},
setting: {
setting: 'Cài đặt',
general: 'Chung',
advanced: 'Nâng cao',
config: 'Cấu hình',
avatarLink: 'Avatar Link',
name: 'Tên',
description: 'Miêu tả',
role: 'Vai trò',
temperature: 'Nhiệt độ',
top_p: 'Top_p',
resetUserInfo: 'Đặt lại thông tin người dùng',
chatHistory: 'Lịch sử trò chuyện',
theme: 'Giao diện',
language: 'Ngôn ngữ',
api: 'API',
reverseProxy: 'Reverse Proxy',
timeout: 'Timeout',
socks: 'Socks',
httpsProxy: 'HTTPS Proxy',
balance: 'API Balance',
monthlyUsage: 'Sử dụng hàng tháng',
openSource: 'Dự án này được mở nguồn tại',
freeMIT: 'miễn phí và dựa trên giấy phép MIT, không có bất kỳ hình thức hành vi trả phí nào!',
stars: 'Nếu bạn thấy dự án này hữu ích, vui lòng cho tôi một Star trên GitHub hoặc tài trợ một chút, cảm ơn bạn!',
},
store: {
siderButton: 'Prompt Store',
local: 'Local',
online: 'Online',
title: 'Tiêu đề',
description: 'Miêu tả',
clearStoreConfirm: 'Cho dù để xóa dữ liệu?',
importPlaceholder: 'Vui lòng dán dữ liệu JSON vào đây',
addRepeatTitleTips: 'Tiêu đề trùng lặp, vui lòng nhập lại',
addRepeatContentTips: 'Nội dung trùng lặp: {msg}, vui lòng nhập lại',
editRepeatTitleTips: 'Xung đột tiêu đề, vui lòng sửa lại',
editRepeatContentTips: 'Xung đột nội dung {msg} , vui lòng sửa đổi lại',
importError: 'Key value mismatch',
importRepeatTitle: 'Tiêu đề liên tục bị bỏ qua: {msg}',
importRepeatContent: 'Nội dung liên tục bị bỏ qua: {msg}',
onlineImportWarning: 'Lưu ý: Vui lòng kiểm tra nguồn tệp JSON!',
downloadError: 'Vui lòng kiểm tra trạng thái mạng và tính hợp lệ của tệp JSON',
},
}

View File

@@ -26,13 +26,17 @@ export default {
failed: '操作失败',
verify: '验证',
unauthorizedTips: '未经授权,请先进行验证。',
stopResponding: '停止响应',
},
chat: {
placeholder: '来说点什么吧...Shift + Enter = 换行)',
newChatButton: '新建聊天',
newChatTitle: '新建聊天',
placeholder: '来说点什么吧...Shift + Enter = 换行,"/" 触发提示词)',
placeholderMobile: '来说点什么...',
copy: '复制',
copied: '复制成功',
copyCode: '复制代码',
copyFailed: '复制失败',
clearChat: '清空会话',
clearChatConfirm: '是否清空会话?',
exportImage: '保存会话到图片',
@@ -45,9 +49,10 @@ export default {
deleteMessage: '删除消息',
deleteMessageConfirm: '是否删除此消息?',
deleteHistoryConfirm: '确定删除此记录?',
clearHistoryConfirm: '确定清空聊天记录?',
clearHistoryConfirm: '确定清空记录?',
preview: '预览',
showRawText: '显示原文',
thinking: '思考中...',
},
setting: {
setting: '设置',
@@ -58,6 +63,8 @@ export default {
name: '名称',
description: '描述',
role: '角色设定',
temperature: 'Temperature',
top_p: 'Top_p',
resetUserInfo: '重置用户信息',
chatHistory: '聊天记录',
theme: '主题',
@@ -68,8 +75,13 @@ export default {
socks: 'Socks',
httpsProxy: 'HTTPS Proxy',
balance: 'API余额',
monthlyUsage: '本月使用量',
openSource: '此项目开源于',
freeMIT: '免费且基于 MIT 协议,没有任何形式的付费行为',
stars: '如果你觉得此项目对你有帮助,请在 GitHub 上给我一个星星或者给予一点赞助,谢谢!',
},
store: {
siderButton: '提示词商店',
local: '本地',
online: '在线',
title: '标题',

View File

@@ -26,13 +26,17 @@ export default {
failed: '操作失敗',
verify: '驗證',
unauthorizedTips: '未經授權,請先進行驗證。',
stopResponding: '停止回應',
},
chat: {
placeholder: '來說點什麼...Shift + Enter = 換行)',
newChatButton: '新增對話',
newChatTitle: '新增對話',
placeholder: '來說點什麼...Shift + Enter = 換行,"/" 觸發提示詞)',
placeholderMobile: '來說點什麼...',
copy: '複製',
copied: '複製成功',
copyCode: '複製代碼',
copyFailed: '複製失敗',
clearChat: '清除對話',
clearChatConfirm: '是否清空對話?',
exportImage: '儲存對話為圖片',
@@ -48,16 +52,19 @@ export default {
clearHistoryConfirm: '確定清除紀錄?',
preview: '預覽',
showRawText: '顯示原文',
thinking: '思考中...',
},
setting: {
setting: '設定',
general: '總覽',
advanced: '高級',
advanced: '進階',
config: '設定',
avatarLink: '頭貼連結',
name: '名稱',
description: '描述',
role: '角色設定',
temperature: 'Temperature',
top_p: 'Top_p',
resetUserInfo: '重設使用者資訊',
chatHistory: '紀錄',
theme: '主題',
@@ -67,9 +74,14 @@ export default {
timeout: '逾時',
socks: 'Socks',
httpsProxy: 'HTTPS Proxy',
balance: 'API額',
balance: 'API Credit 餘額',
monthlyUsage: '本月使用量',
openSource: '此專案在此開源:',
freeMIT: '免費且基於 MIT 授權,沒有任何形式的付費行為!',
stars: '如果你覺得此專案對你有幫助,請在 GitHub 上給我一顆星,或者贊助我,謝謝!',
},
store: {
siderButton: '提示詞商店',
local: '本機',
online: '線上',
title: '標題',

3
src/store/helper.ts Normal file
View File

@@ -0,0 +1,3 @@
import { createPinia } from 'pinia'
export const store = createPinia()

View File

@@ -1,7 +1,5 @@
import type { App } from 'vue'
import { createPinia } from 'pinia'
export const store = createPinia()
import { store } from './helper'
export function setupStore(app: App) {
app.use(store)

View File

@@ -4,7 +4,23 @@ const LOCAL_NAME = 'appSetting'
export type Theme = 'light' | 'dark' | 'auto'
export type Language = 'zh-CN' | 'zh-TW' | 'en-US'
export type Language = 'en-US' | 'es-ES' | 'ko-KR' | 'ru-RU' | 'vi-VN' | 'zh-CN' | 'zh-TW'
const languageMap: { [key: string]: Language } = {
'en': 'en-US',
'en-US': 'en-US',
'es': 'es-ES',
'es-ES': 'es-ES',
'ko': 'ko-KR',
'ko-KR': 'ko-KR',
'ru': 'ru-RU',
'ru-RU': 'ru-RU',
'vi': 'vi-VN',
'vi-VN': 'vi-VN',
'zh': 'zh-CN',
'zh-CN': 'zh-CN',
'zh-TW': 'zh-TW',
}
export interface AppState {
siderCollapsed: boolean
@@ -13,7 +29,8 @@ export interface AppState {
}
export function defaultSetting(): AppState {
return { siderCollapsed: false, theme: 'light', language: 'zh-CN' }
const language = languageMap[navigator.language]
return { siderCollapsed: false, theme: 'light', language }
}
export function getLocalSetting(): AppState {

View File

@@ -1,7 +1,7 @@
import { defineStore } from 'pinia'
import type { AppState, Language, Theme } from './helper'
import { getLocalSetting, setLocalSetting } from './helper'
import { store } from '@/store'
import { store } from '@/store/helper'
export const useAppStore = defineStore('app-store', {
state: (): AppState => getLocalSetting(),

View File

@@ -1,6 +1,6 @@
import { defineStore } from 'pinia'
import { getToken, removeToken, setToken } from './helper'
import { store } from '@/store'
import { store } from '@/store/helper'
import { fetchSession } from '@/api'
interface SessionResponse {

View File

@@ -1,4 +1,5 @@
import { ss } from '@/utils/storage'
import { t } from '@/locales'
const LOCAL_NAME = 'chatStorage'
@@ -7,7 +8,7 @@ export function defaultState(): Chat.ChatState {
return {
active: uuid,
usingContext: true,
history: [{ uuid, title: 'New Chat', isEdit: false }],
history: [{ uuid, title: t('chat.newChatTitle'), isEdit: false }],
chat: [{ uuid, data: [] }],
}
}

View File

@@ -1,6 +1,7 @@
import { defineStore } from 'pinia'
import { getLocalState, setLocalState } from './helper'
import { defaultState, getLocalState, setLocalState } from './helper'
import { router } from '@/router'
import { t } from '@/locales'
export const useChatStore = defineStore('chat-store', {
state: (): Chat.ChatState => getLocalState(),
@@ -103,7 +104,7 @@ export const useChatStore = defineStore('chat-store', {
}
else {
this.chat[0].data.push(chat)
if (this.history[0].title === 'New Chat')
if (this.history[0].title === t('chat.newChatTitle'))
this.history[0].title = chat.text
this.recordState()
}
@@ -112,7 +113,7 @@ export const useChatStore = defineStore('chat-store', {
const index = this.chat.findIndex(item => item.uuid === uuid)
if (index !== -1) {
this.chat[index].data.push(chat)
if (this.history[index].title === 'New Chat')
if (this.history[index].title === t('chat.newChatTitle'))
this.history[index].title = chat.text
this.recordState()
}
@@ -182,6 +183,11 @@ export const useChatStore = defineStore('chat-store', {
}
},
clearHistory() {
this.$state = { ...defaultState() }
this.recordState()
},
async reloadRoute(uuid?: number) {
this.recordState()
await router.push({ name: 'Chat', params: { uuid } })

View File

@@ -4,12 +4,15 @@ const LOCAL_NAME = 'settingsStorage'
export interface SettingsState {
systemMessage: string
temperature: number
top_p: number
}
export function defaultSetting(): SettingsState {
const currentDate = new Date().toISOString().split('T')[0]
return {
systemMessage: `You are ChatGPT, a large language model trained by OpenAI. Answer as concisely as possible.\nKnowledge cutoff: 2021-09-01\nCurrent date: ${currentDate}`,
systemMessage: 'You are ChatGPT, a large language model trained by OpenAI. Follow the user\'s instructions carefully. Respond using markdown.',
temperature: 0.8,
top_p: 1,
}
}

View File

@@ -17,7 +17,7 @@ export function defaultSetting(): UserState {
userInfo: {
avatar: 'https://raw.githubusercontent.com/Chanzhaoyu/chatgpt-web/main/src/assets/avatar.jpg',
name: 'ChenZhaoYu',
description: 'Star on <a href="https://github.com/Chanzhaoyu/chatgpt-bot" class="text-blue-500" target="_blank" >Github</a>',
description: 'Star on <a href="https://github.com/Chanzhaoyu/chatgpt-bot" class="text-blue-500" target="_blank" >GitHub</a>',
},
}
}

View File

@@ -123,7 +123,10 @@ html {
}
code.hljs {
padding: 3px 5px
padding: 3px 5px;
&::-webkit-scrollbar {
height: 4px;
}
}
.hljs {

18
src/utils/copy.ts Normal file
View File

@@ -0,0 +1,18 @@
export function copyToClip(text: string) {
return new Promise((resolve, reject) => {
try {
const input: HTMLTextAreaElement = document.createElement('textarea')
input.setAttribute('readonly', 'readonly')
input.value = text
document.body.appendChild(input)
input.select()
if (document.execCommand('copy'))
document.execCommand('copy')
document.body.removeChild(input)
resolve(text)
}
catch (error) {
reject(error)
}
})
}

View File

@@ -1,18 +0,0 @@
import CryptoJS from 'crypto-js'
const CryptoSecret = '__CRYPTO_SECRET__'
export function enCrypto(data: any) {
const str = JSON.stringify(data)
return CryptoJS.AES.encrypt(str, CryptoSecret).toString()
}
export function deCrypto(data: string) {
const bytes = CryptoJS.AES.decrypt(data, CryptoSecret)
const str = bytes.toString(CryptoJS.enc.Utf8)
if (str)
return JSON.parse(str)
return null
}

View File

@@ -1,44 +0,0 @@
/**
* 转义 HTML 字符
* @param source
*/
export function encodeHTML(source: string) {
return source
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
}
/**
* 判断是否为代码块
* @param text
*/
export function includeCode(text: string | null | undefined) {
const regexp = /^(?:\s{4}|\t).+/gm
return !!(text?.includes(' = ') || text?.match(regexp))
}
/**
* 复制文本
* @param options
*/
export function copyText(options: { text: string; origin?: boolean }) {
const props = { origin: true, ...options }
let input: HTMLInputElement | HTMLTextAreaElement
if (props.origin)
input = document.createElement('textarea')
else
input = document.createElement('input')
input.setAttribute('readonly', 'readonly')
input.value = props.text
document.body.appendChild(input)
input.select()
if (document.execCommand('copy'))
document.execCommand('copy')
document.body.removeChild(input)
}

View File

@@ -0,0 +1,18 @@
type CallbackFunc<T extends unknown[]> = (...args: T) => void
export function debounce<T extends unknown[]>(
func: CallbackFunc<T>,
wait: number,
): (...args: T) => void {
let timeoutId: ReturnType<typeof setTimeout> | undefined
return (...args: T) => {
const later = () => {
clearTimeout(timeoutId)
func(...args)
}
clearTimeout(timeoutId)
timeoutId = setTimeout(later, wait)
}
}

View File

@@ -1 +1,57 @@
export * from './local'
interface StorageData<T = any> {
data: T
expire: number | null
}
export function createLocalStorage(options?: { expire?: number | null }) {
const DEFAULT_CACHE_TIME = 60 * 60 * 24 * 7
const { expire } = Object.assign({ expire: DEFAULT_CACHE_TIME }, options)
function set<T = any>(key: string, data: T) {
const storageData: StorageData<T> = {
data,
expire: expire !== null ? new Date().getTime() + expire * 1000 : null,
}
const json = JSON.stringify(storageData)
window.localStorage.setItem(key, json)
}
function get(key: string) {
const json = window.localStorage.getItem(key)
if (json) {
let storageData: StorageData | null = null
try {
storageData = JSON.parse(json)
}
catch {
// Prevent failure
}
if (storageData) {
const { data, expire } = storageData
if (expire === null || expire >= Date.now())
return data
}
remove(key)
return null
}
}
function remove(key: string) {
window.localStorage.removeItem(key)
}
function clear() {
window.localStorage.clear()
}
return { set, get, remove, clear }
}
export const ls = createLocalStorage()
export const ss = createLocalStorage({ expire: null })

View File

@@ -1,70 +0,0 @@
import { deCrypto, enCrypto } from '../crypto'
interface StorageData<T = any> {
data: T
expire: number | null
}
export function createLocalStorage(options?: { expire?: number | null; crypto?: boolean }) {
const DEFAULT_CACHE_TIME = 60 * 60 * 24 * 7
const { expire, crypto } = Object.assign(
{
expire: DEFAULT_CACHE_TIME,
crypto: true,
},
options,
)
function set<T = any>(key: string, data: T) {
const storageData: StorageData<T> = {
data,
expire: expire !== null ? new Date().getTime() + expire * 1000 : null,
}
const json = crypto ? enCrypto(storageData) : JSON.stringify(storageData)
window.localStorage.setItem(key, json)
}
function get(key: string) {
const json = window.localStorage.getItem(key)
if (json) {
let storageData: StorageData | null = null
try {
storageData = crypto ? deCrypto(json) : JSON.parse(json)
}
catch {
// Prevent failure
}
if (storageData) {
const { data, expire } = storageData
if (expire === null || expire >= Date.now())
return data
}
remove(key)
return null
}
}
function remove(key: string) {
window.localStorage.removeItem(key)
}
function clear() {
window.localStorage.clear()
}
return {
set,
get,
remove,
clear,
}
}
export const ls = createLocalStorage()
export const ss = createLocalStorage({ expire: null, crypto: false })

View File

@@ -9,7 +9,7 @@ interface Props {
interface Emit {
(ev: 'export'): void
(ev: 'toggleUsingContext'): void
(ev: 'handleClear'): void
}
defineProps<Props>()
@@ -36,8 +36,8 @@ function handleExport() {
emit('export')
}
function toggleUsingContext() {
emit('toggleUsingContext')
function handleClear() {
emit('handleClear')
}
</script>
@@ -62,16 +62,16 @@ function toggleUsingContext() {
{{ currentChatHistory?.title ?? '' }}
</h1>
<div class="flex items-center space-x-2">
<HoverButton @click="toggleUsingContext">
<span class="text-xl" :class="{ 'text-[#4b9e5f]': usingContext, 'text-[#a8071a]': !usingContext }">
<SvgIcon icon="ri:chat-history-line" />
</span>
</HoverButton>
<HoverButton @click="handleExport">
<span class="text-xl text-[#4f555e] dark:text-white">
<SvgIcon icon="ri:download-2-line" />
</span>
</HoverButton>
<HoverButton @click="handleClear">
<span class="text-xl text-[#4f555e] dark:text-white">
<SvgIcon icon="ri:delete-bin-line" />
</span>
</HoverButton>
</div>
</div>
</header>

View File

@@ -1,11 +1,13 @@
<script lang="ts" setup>
import { computed, ref } from 'vue'
import { computed, onMounted, onUnmounted, onUpdated, ref } from 'vue'
import MarkdownIt from 'markdown-it'
import mdKatex from '@traptitech/markdown-it-katex'
import mila from 'markdown-it-link-attributes'
import MdKatex from '@vscode/markdown-it-katex'
import MdLinkAttributes from 'markdown-it-link-attributes'
import MdMermaid from 'mermaid-it-markdown'
import hljs from 'highlight.js'
import { useBasicLayout } from '@/hooks/useBasicLayout'
import { t } from '@/locales'
import { copyToClip } from '@/utils/copy'
interface Props {
inversion?: boolean
@@ -22,6 +24,7 @@ const { isMobile } = useBasicLayout()
const textRef = ref<HTMLElement>()
const mdi = new MarkdownIt({
html: false,
linkify: true,
highlight(code, language) {
const validLang = !!(language && hljs.getLanguage(language))
@@ -33,8 +36,7 @@ const mdi = new MarkdownIt({
},
})
mdi.use(mila, { attrs: { target: '_blank', rel: 'noopener' } })
mdi.use(mdKatex, { blockClass: 'katexmath-block rounded-md p-[10px]', errorColor: ' #cc0000' })
mdi.use(MdLinkAttributes, { attrs: { target: '_blank', rel: 'noopener' } }).use(MdKatex).use(MdMermaid)
const wrapClass = computed(() => {
return [
@@ -51,8 +53,11 @@ const wrapClass = computed(() => {
const text = computed(() => {
const value = props.text ?? ''
if (!props.asRawText)
return mdi.render(value)
if (!props.asRawText) {
// 对数学公式进行处理,自动添加 $$ 符号
const escapedText = escapeBrackets(escapeDollarNumber(value))
return mdi.render(escapedText)
}
return value
})
@@ -60,23 +65,85 @@ function highlightBlock(str: string, lang?: string) {
return `<pre class="code-block-wrapper"><div class="code-block-header"><span class="code-block-header__lang">${lang}</span><span class="code-block-header__copy">${t('chat.copyCode')}</span></div><code class="hljs code-block-body ${lang}">${str}</code></pre>`
}
defineExpose({ textRef })
function addCopyEvents() {
if (textRef.value) {
const copyBtn = textRef.value.querySelectorAll('.code-block-header__copy')
copyBtn.forEach((btn) => {
btn.addEventListener('click', () => {
const code = btn.parentElement?.nextElementSibling?.textContent
if (code) {
copyToClip(code).then(() => {
btn.textContent = t('chat.copied')
setTimeout(() => {
btn.textContent = t('chat.copyCode')
}, 1000)
})
}
})
})
}
}
function removeCopyEvents() {
if (textRef.value) {
const copyBtn = textRef.value.querySelectorAll('.code-block-header__copy')
copyBtn.forEach((btn) => {
btn.removeEventListener('click', () => { })
})
}
}
function escapeDollarNumber(text: string) {
let escapedText = ''
for (let i = 0; i < text.length; i += 1) {
let char = text[i]
const nextChar = text[i + 1] || ' '
if (char === '$' && nextChar >= '0' && nextChar <= '9')
char = '\\$'
escapedText += char
}
return escapedText
}
function escapeBrackets(text: string) {
const pattern = /(```[\s\S]*?```|`.*?`)|\\\[([\s\S]*?[^\\])\\\]|\\\((.*?)\\\)/g
return text.replace(pattern, (match, codeBlock, squareBracket, roundBracket) => {
if (codeBlock)
return codeBlock
else if (squareBracket)
return `$$${squareBracket}$$`
else if (roundBracket)
return `$${roundBracket}$`
return match
})
}
onMounted(() => {
addCopyEvents()
})
onUpdated(() => {
addCopyEvents()
})
onUnmounted(() => {
removeCopyEvents()
})
</script>
<template>
<div class="text-black" :class="wrapClass">
<template v-if="loading">
<span class="dark:text-white w-[4px] h-[20px] block animate-blink" />
</template>
<template v-else>
<div ref="textRef" class="leading-relaxed break-words">
<div v-if="!inversion">
<div v-if="!asRawText" class="markdown-body" v-html="text" />
<div v-else class="whitespace-pre-wrap" v-text="text" />
</div>
<div ref="textRef" class="leading-relaxed break-words">
<div v-if="!inversion">
<div v-if="!asRawText" class="markdown-body" :class="{ 'markdown-body-generate': loading }" v-html="text" />
<div v-else class="whitespace-pre-wrap" v-text="text" />
</div>
</template>
<div v-else class="whitespace-pre-wrap" v-text="text" />
</div>
</div>
</template>

View File

@@ -1,12 +1,13 @@
<script setup lang='ts'>
import { computed, ref } from 'vue'
import { NDropdown } from 'naive-ui'
import { NDropdown, useMessage } from 'naive-ui'
import AvatarComponent from './Avatar.vue'
import TextComponent from './Text.vue'
import { SvgIcon } from '@/components/common'
import { copyText } from '@/utils/format'
import { useIconRender } from '@/hooks/useIconRender'
import { t } from '@/locales'
import { useBasicLayout } from '@/hooks/useBasicLayout'
import { copyToClip } from '@/utils/copy'
interface Props {
dateTime?: string
@@ -25,8 +26,12 @@ const props = defineProps<Props>()
const emit = defineEmits<Emit>()
const { isMobile } = useBasicLayout()
const { iconRender } = useIconRender()
const message = useMessage()
const textRef = ref<HTMLElement>()
const asRawText = ref(props.inversion)
@@ -61,7 +66,7 @@ const options = computed(() => {
function handleSelect(key: 'copyText' | 'delete' | 'toggleRenderType') {
switch (key) {
case 'copyText':
copyText({ text: props.text ?? '' })
handleCopy()
return
case 'toggleRenderType':
asRawText.value = !asRawText.value
@@ -75,6 +80,16 @@ function handleRegenerate() {
messageRef.value?.scrollIntoView()
emit('regenerate')
}
async function handleCopy() {
try {
await copyToClip(props.text || '')
message.success(t('chat.copied'))
}
catch {
message.error(t('chat.copyFailed'))
}
}
</script>
<template>
@@ -113,7 +128,12 @@ function handleRegenerate() {
>
<SvgIcon icon="ri:restart-line" />
</button>
<NDropdown :placement="!inversion ? 'right' : 'left'" :options="options" @select="handleSelect">
<NDropdown
:trigger="isMobile ? 'click' : 'hover'"
:placement="!inversion ? 'right' : 'left'"
:options="options"
@select="handleSelect"
>
<button class="transition text-neutral-300 hover:text-neutral-800 dark:hover:text-neutral-200">
<SvgIcon icon="ri:more-2-fill" />
</button>

View File

@@ -57,10 +57,68 @@
}
}
// Mermaid
div[id^='mermaid-container'] {
padding: 4px;
border-radius: 4px;
overflow-x: auto !important;
background-color: #fff;
border: 1px solid #e5e5e5;
}
&.markdown-body-generate>dd:last-child:after,
&.markdown-body-generate>dl:last-child:after,
&.markdown-body-generate>dt:last-child:after,
&.markdown-body-generate>h1:last-child:after,
&.markdown-body-generate>h2:last-child:after,
&.markdown-body-generate>h3:last-child:after,
&.markdown-body-generate>h4:last-child:after,
&.markdown-body-generate>h5:last-child:after,
&.markdown-body-generate>h6:last-child:after,
&.markdown-body-generate>li:last-child:after,
&.markdown-body-generate>ol:last-child li:last-child:after,
&.markdown-body-generate>p:last-child:after,
&.markdown-body-generate>pre:last-child code:after,
&.markdown-body-generate>td:last-child:after,
&.markdown-body-generate>ul:last-child li:last-child:after {
animation: blink 1s steps(5, start) infinite;
color: #000;
content: '_';
font-weight: 700;
margin-left: 3px;
vertical-align: baseline;
}
@keyframes blink {
to {
visibility: hidden;
}
}
}
html.dark {
.markdown-body {
&.markdown-body-generate>dd:last-child:after,
&.markdown-body-generate>dl:last-child:after,
&.markdown-body-generate>dt:last-child:after,
&.markdown-body-generate>h1:last-child:after,
&.markdown-body-generate>h2:last-child:after,
&.markdown-body-generate>h3:last-child:after,
&.markdown-body-generate>h4:last-child:after,
&.markdown-body-generate>h5:last-child:after,
&.markdown-body-generate>h6:last-child:after,
&.markdown-body-generate>li:last-child:after,
&.markdown-body-generate>ol:last-child li:last-child:after,
&.markdown-body-generate>p:last-child:after,
&.markdown-body-generate>pre:last-child code:after,
&.markdown-body-generate>td:last-child:after,
&.markdown-body-generate>ul:last-child li:last-child:after {
color: #65a665;
}
}
.message-reply {
.whitespace-pre-wrap {
white-space: pre-wrap;
@@ -73,3 +131,13 @@ html.dark {
background-color: #282c34;
}
}
@media screen and (max-width: 533px) {
.markdown-body .code-block-wrapper {
padding: unset;
code {
padding: 24px 16px 16px 16px;
}
}
}

View File

@@ -1,24 +0,0 @@
import { onMounted, onUpdated } from 'vue'
import { copyText } from '@/utils/format'
export function useCopyCode() {
function copyCodeBlock() {
const codeBlockWrapper = document.querySelectorAll('.code-block-wrapper')
codeBlockWrapper.forEach((wrapper) => {
const copyBtn = wrapper.querySelector('.code-block-header__copy')
const codeBlock = wrapper.querySelector('.code-block-body')
if (copyBtn && codeBlock) {
copyBtn.addEventListener('click', () => {
if (navigator.clipboard?.writeText)
navigator.clipboard.writeText(codeBlock.textContent ?? '')
else
copyText({ text: codeBlock.textContent ?? '', origin: true })
})
}
})
}
onMounted(() => copyCodeBlock())
onUpdated(() => copyCodeBlock())
}

View File

@@ -28,7 +28,7 @@ export function useScroll(): ScrollReturn {
const scrollToBottomIfAtBottom = async () => {
await nextTick()
if (scrollRef.value) {
const threshold = 100 // 阈值,表示滚动条到底部的距离阈值
const threshold = 100 // Threshold, indicating the distance threshold to the bottom of the scroll bar.
const distanceToBottom = scrollRef.value.scrollHeight - scrollRef.value.scrollTop - scrollRef.value.clientHeight
if (distanceToBottom <= threshold)
scrollRef.value.scrollTop = scrollRef.value.scrollHeight

View File

@@ -4,11 +4,10 @@ import { computed, onMounted, onUnmounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import { storeToRefs } from 'pinia'
import { NAutoComplete, NButton, NInput, useDialog, useMessage } from 'naive-ui'
import html2canvas from 'html2canvas'
import { toPng } from 'html-to-image'
import { Message } from './components'
import { useScroll } from './hooks/useScroll'
import { useChat } from './hooks/useChat'
import { useCopyCode } from './hooks/useCopyCode'
import { useUsingContext } from './hooks/useUsingContext'
import HeaderComponent from './components/Header/index.vue'
import { HoverButton, SvgIcon } from '@/components/common'
@@ -27,8 +26,6 @@ const ms = useMessage()
const chatStore = useChatStore()
useCopyCode()
const { isMobile } = useBasicLayout()
const { addChat, updateChat, updateChatSome, getChatByUuidAndIndex } = useChat()
const { scrollRef, scrollToBottom, scrollToBottomIfAtBottom } = useScroll()
@@ -37,7 +34,7 @@ const { usingContext, toggleUsingContext } = useUsingContext()
const { uuid } = route.params as { uuid: string }
const dataSources = computed(() => chatStore.getChatByUuid(+uuid))
const conversationList = computed(() => dataSources.value.filter(item => (!item.inversion && !item.error)))
const conversationList = computed(() => dataSources.value.filter(item => (!item.inversion && !!item.conversationOptions)))
const prompt = ref<string>('')
const loading = ref<boolean>(false)
@@ -96,7 +93,7 @@ async function onConversation() {
+uuid,
{
dateTime: new Date().toLocaleString(),
text: '',
text: t('chat.thinking'),
loading: true,
inversion: false,
error: false,
@@ -128,10 +125,10 @@ async function onConversation() {
dataSources.value.length - 1,
{
dateTime: new Date().toLocaleString(),
text: lastText + data.text ?? '',
text: lastText + (data.text ?? ''),
inversion: false,
error: false,
loading: false,
loading: true,
conversationOptions: { conversationId: data.conversationId, parentMessageId: data.id },
requestOptions: { prompt: message, options: { ...options } },
},
@@ -147,10 +144,11 @@ async function onConversation() {
scrollToBottomIfAtBottom()
}
catch (error) {
//
//
}
},
})
updateChatSome(+uuid, dataSources.value.length - 1, { loading: false })
}
await fetchChatAPIOnce()
@@ -232,7 +230,7 @@ async function onRegenerate(index: number) {
error: false,
loading: true,
conversationOptions: null,
requestOptions: { prompt: message, ...options },
requestOptions: { prompt: message, options: { ...options } },
},
)
@@ -258,12 +256,12 @@ async function onRegenerate(index: number) {
index,
{
dateTime: new Date().toLocaleString(),
text: lastText + data.text ?? '',
text: lastText + (data.text ?? ''),
inversion: false,
error: false,
loading: false,
loading: true,
conversationOptions: { conversationId: data.conversationId, parentMessageId: data.id },
requestOptions: { prompt: message, ...options },
requestOptions: { prompt: message, options: { ...options } },
},
)
@@ -279,6 +277,7 @@ async function onRegenerate(index: number) {
}
},
})
updateChatSome(+uuid, index, { loading: false })
}
await fetchChatAPIOnce()
}
@@ -306,7 +305,7 @@ async function onRegenerate(index: number) {
error: true,
loading: false,
conversationOptions: null,
requestOptions: { prompt: message, ...options },
requestOptions: { prompt: message, options: { ...options } },
},
)
}
@@ -328,17 +327,13 @@ function handleExport() {
try {
d.loading = true
const ele = document.getElementById('image-wrapper')
const canvas = await html2canvas(ele as HTMLDivElement, {
useCORS: true,
})
const imgUrl = canvas.toDataURL('image/png')
const imgUrl = await toPng(ele as HTMLDivElement)
const tempLink = document.createElement('a')
tempLink.style.display = 'none'
tempLink.href = imgUrl
tempLink.setAttribute('download', 'chat-shot.png')
if (typeof tempLink.download === 'undefined')
tempLink.setAttribute('target', '_blank')
document.body.appendChild(tempLink)
tempLink.click()
document.body.removeChild(tempLink)
@@ -454,7 +449,7 @@ const footerClass = computed(() => {
onMounted(() => {
scrollToBottom()
if (inputRef.value)
if (inputRef.value && !isMobile.value)
inputRef.value?.focus()
})
@@ -470,55 +465,52 @@ onUnmounted(() => {
v-if="isMobile"
:using-context="usingContext"
@export="handleExport"
@toggle-using-context="toggleUsingContext"
@handle-clear="handleClear"
/>
<main class="flex-1 overflow-hidden">
<div
id="scrollRef"
ref="scrollRef"
class="h-full overflow-hidden overflow-y-auto"
>
<div id="scrollRef" ref="scrollRef" class="h-full overflow-hidden overflow-y-auto">
<div
id="image-wrapper"
class="w-full max-w-screen-xl m-auto dark:bg-[#101014]"
:class="[isMobile ? 'p-2' : 'p-4']"
>
<template v-if="!dataSources.length">
<div class="flex items-center justify-center mt-4 text-center text-neutral-300">
<SvgIcon icon="ri:bubble-chart-fill" class="mr-2 text-3xl" />
<span>Aha~</span>
</div>
</template>
<template v-else>
<div>
<Message
v-for="(item, index) of dataSources"
:key="index"
:date-time="item.dateTime"
:text="item.text"
:inversion="item.inversion"
:error="item.error"
:loading="item.loading"
@regenerate="onRegenerate(index)"
@delete="handleDelete(index)"
/>
<div class="sticky bottom-0 left-0 flex justify-center">
<NButton v-if="loading" type="warning" @click="handleStop">
<template #icon>
<SvgIcon icon="ri:stop-circle-line" />
</template>
Stop Responding
</NButton>
<div id="image-wrapper" class="relative">
<template v-if="!dataSources.length">
<div class="flex items-center justify-center mt-4 text-center text-neutral-300">
<SvgIcon icon="ri:bubble-chart-fill" class="mr-2 text-3xl" />
<span>{{ t('chat.newChatTitle') }}</span>
</div>
</div>
</template>
</template>
<template v-else>
<div>
<Message
v-for="(item, index) of dataSources"
:key="index"
:date-time="item.dateTime"
:text="item.text"
:inversion="item.inversion"
:error="item.error"
:loading="item.loading"
@regenerate="onRegenerate(index)"
@delete="handleDelete(index)"
/>
<div class="sticky bottom-0 left-0 flex justify-center">
<NButton v-if="loading" type="warning" @click="handleStop">
<template #icon>
<SvgIcon icon="ri:stop-circle-line" />
</template>
{{ t('common.stopResponding') }}
</NButton>
</div>
</div>
</template>
</div>
</div>
</div>
</main>
<footer :class="footerClass">
<div class="w-full max-w-screen-xl m-auto">
<div class="flex items-center justify-between space-x-2">
<HoverButton @click="handleClear">
<HoverButton v-if="!isMobile" @click="handleClear">
<span class="text-xl text-[#4f555e] dark:text-white">
<SvgIcon icon="ri:delete-bin-line" />
</span>
@@ -528,7 +520,7 @@ onUnmounted(() => {
<SvgIcon icon="ri:download-2-line" />
</span>
</HoverButton>
<HoverButton v-if="!isMobile" @click="toggleUsingContext">
<HoverButton @click="toggleUsingContext">
<span class="text-xl" :class="{ 'text-[#4b9e5f]': usingContext, 'text-[#a8071a]': !usingContext }">
<SvgIcon icon="ri:chat-history-line" />
</span>

View File

@@ -4,6 +4,7 @@ import { NInput, NPopconfirm, NScrollbar } from 'naive-ui'
import { SvgIcon } from '@/components/common'
import { useAppStore, useChatStore } from '@/store'
import { useBasicLayout } from '@/hooks/useBasicLayout'
import { debounce } from '@/utils/functions/debounce'
const { isMobile } = useBasicLayout()
@@ -32,8 +33,12 @@ function handleEdit({ uuid }: Chat.History, isEdit: boolean, event?: MouseEvent)
function handleDelete(index: number, event?: MouseEvent | TouchEvent) {
event?.stopPropagation()
chatStore.deleteHistory(index)
if (isMobile.value)
appStore.setSiderCollapsed(true)
}
const handleDeleteDebounce = debounce(handleDelete, 600)
function handleEnter({ uuid }: Chat.History, isEdit: boolean, event: KeyboardEvent) {
event?.stopPropagation()
if (event.key === 'Enter')
@@ -67,8 +72,7 @@ function isActive(uuid: number) {
<div class="relative flex-1 overflow-hidden break-all text-ellipsis whitespace-nowrap">
<NInput
v-if="item.isEdit"
v-model:value="item.title"
size="tiny"
v-model:value="item.title" size="tiny"
@keypress="handleEnter(item, false, $event)"
/>
<span v-else>{{ item.title }}</span>
@@ -83,7 +87,7 @@ function isActive(uuid: number) {
<button class="p-1">
<SvgIcon icon="ri:edit-line" @click="handleEdit(item, true, $event)" />
</button>
<NPopconfirm placement="bottom" @positive-click="handleDelete(index, $event)">
<NPopconfirm placement="bottom" @positive-click="handleDeleteDebounce(index, $event)">
<template #trigger>
<button class="p-1">
<SvgIcon icon="ri:delete-bin-line" />

View File

@@ -1,29 +1,48 @@
<script setup lang='ts'>
import type { CSSProperties } from 'vue'
import { computed, ref, watch } from 'vue'
import { NButton, NLayoutSider } from 'naive-ui'
import { NButton, NLayoutSider, useDialog } from 'naive-ui'
import List from './List.vue'
import Footer from './Footer.vue'
import { useAppStore, useChatStore } from '@/store'
import { useBasicLayout } from '@/hooks/useBasicLayout'
import { PromptStore } from '@/components/common'
import { PromptStore, SvgIcon } from '@/components/common'
import { t } from '@/locales'
const appStore = useAppStore()
const chatStore = useChatStore()
const dialog = useDialog()
const { isMobile } = useBasicLayout()
const show = ref(false)
const collapsed = computed(() => appStore.siderCollapsed)
function handleAdd() {
chatStore.addHistory({ title: 'New Chat', uuid: Date.now(), isEdit: false })
chatStore.addHistory({ title: t('chat.newChatTitle'), uuid: Date.now(), isEdit: false })
if (isMobile.value)
appStore.setSiderCollapsed(true)
}
function handleUpdateCollapsed() {
appStore.setSiderCollapsed(!collapsed.value)
}
function handleClearAll() {
dialog.warning({
title: t('chat.deleteMessage'),
content: t('chat.clearHistoryConfirm'),
positiveText: t('common.yes'),
negativeText: t('common.no'),
onPositiveClick: () => {
chatStore.clearHistory()
if (isMobile.value)
appStore.setSiderCollapsed(true)
},
})
}
const getMobileClass = computed<CSSProperties>(() => {
if (isMobile.value) {
return {
@@ -71,15 +90,20 @@ watch(
<main class="flex flex-col flex-1 min-h-0">
<div class="p-4">
<NButton dashed block @click="handleAdd">
New chat
{{ $t('chat.newChatButton') }}
</NButton>
</div>
<div class="flex-1 min-h-0 pb-4 overflow-hidden">
<List />
</div>
<div class="p-4">
<NButton block @click="show = true">
Prompt Store
<div class="flex items-center p-4 space-x-4">
<div class="flex-1">
<NButton block @click="show = true">
{{ $t('store.siderButton') }}
</NButton>
</div>
<NButton @click="handleClearAll">
<SvgIcon icon="ri:close-circle-line" />
</NButton>
</div>
</main>
@@ -87,7 +111,7 @@ watch(
</div>
</NLayoutSider>
<template v-if="isMobile">
<div v-show="!collapsed" class="fixed inset-0 z-40 bg-black/40" @click="handleUpdateCollapsed" />
<div v-show="!collapsed" class="fixed inset-0 z-40 w-full h-full bg-black/40" @click="handleUpdateCollapsed" />
</template>
<PromptStore v-model:visible="show" />
</template>