Compare commits

24 Commits

Author SHA1 Message Date
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
39 changed files with 20784 additions and 877 deletions

View File

@@ -13,10 +13,10 @@ jobs:
- uses: actions/stale@v5 - uses: actions/stale@v5
with: with:
days-before-issue-stale: 10 days-before-issue-stale: 10
days-before-issue-close: 5 days-before-issue-close: 2
stale-issue-label: stale stale-issue-label: stale
stale-issue-message: This issue is stale because it has been open for 10 days with no activity. 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 5 days since being marked as stale. 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-stale: -1
days-before-pr-close: -1 days-before-pr-close: -1
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,8 +1,32 @@
## 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 ## v2.10.9
`2023-04-03` `2023-04-03`
> 更新默认 `accessToken` 反代地址为 [[acheong08](https://github.com/acheong08)] 的 `https://bypass.churchless.tech/api/conversation` > 更新默认 `accessToken` 反代地址为 [[pengzhile](https://github.com/pengzhile)] 的 `https://ai.fakeopen.com/api/conversation`
## Enhancement ## Enhancement
- 添加 `socks5` 代理认证 [[yimiaoxiehou](https://github.com/Chanzhaoyu/chatgpt-web/pull/999)] - 添加 `socks5` 代理认证 [[yimiaoxiehou](https://github.com/Chanzhaoyu/chatgpt-web/pull/999)]
@@ -101,7 +125,7 @@
`2023-03-13` `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 ## Feature
- `Prompt` 商店在线导入可以导入两种 `recommend.json`里提到的模板 [simonwu53](https://github.com/Chanzhaoyu/chatgpt-web/pull/521) - `Prompt` 商店在线导入可以导入两种 `recommend.json`里提到的模板 [simonwu53](https://github.com/Chanzhaoyu/chatgpt-web/pull/521)

View File

@@ -1,6 +1,8 @@
# ChatGPT Web # ChatGPT Web
> 声明:此项目只发布于 Github基于 MIT 协议,免费且作为开源学习使用。并且不会有任何形式的卖号、付费服务、讨论群、讨论组等行为。谨防受骗。 > 声明:此项目只发布于 GitHub基于 MIT 协议,免费且作为开源学习使用。并且不会有任何形式的卖号、付费服务、讨论群、讨论组等行为。谨防受骗。
更多功能:[chatgpt-web-plus](https://github.com/Chanzhaoyu/chatgpt-web-plus)
![cover](./docs/c1.png) ![cover](./docs/c1.png)
![cover2](./docs/c2.png) ![cover2](./docs/c2.png)
@@ -24,6 +26,7 @@
- [Docker 参数示例](#docker-参数示例) - [Docker 参数示例](#docker-参数示例)
- [Docker build \& Run](#docker-build--run) - [Docker build \& Run](#docker-build--run)
- [Docker compose](#docker-compose) - [Docker compose](#docker-compose)
- [防止爬虫抓取](#防止爬虫抓取)
- [使用 Railway 部署](#使用-railway-部署) - [使用 Railway 部署](#使用-railway-部署)
- [Railway 环境变量](#railway-环境变量) - [Railway 环境变量](#railway-环境变量)
- [手动打包](#手动打包) - [手动打包](#手动打包)
@@ -50,7 +53,7 @@
1. 你应该首先使用 `API` 方式 1. 你应该首先使用 `API` 方式
2. 使用 `API` 时,如果网络不通,那是国内被墙了,你需要自建代理,绝对不要使用别人的公开代理,那是危险的。 2. 使用 `API` 时,如果网络不通,那是国内被墙了,你需要自建代理,绝对不要使用别人的公开代理,那是危险的。
3. 使用 `accessToken` 方式时反向代理将向第三方暴露您的访问令牌,这样做应该不会产生任何不良影响,但在使用这种方法之前请考虑风险。 3. 使用 `accessToken` 方式时反向代理将向第三方暴露您的访问令牌,这样做应该不会产生任何不良影响,但在使用这种方法之前请考虑风险。
4. 使用 `accessToken` 时,不管你是国内还是国外的机器,都会使用代理。默认代理为 [acheong08](https://github.com/acheong08) 大佬的 `https://bypass.churchless.tech/api/conversation`,这不是后门也不是监听,除非你有能力自己翻过 `CF` 验证,用前请知悉。[社区代理](https://github.com/transitive-bullshit/chatgpt-api#reverse-proxy)(注意:只有这两个是推荐,其他第三方来源,请自行甄别) 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`,防止被关键词搜索到。 5. 把项目发布到公共网络时,你应该设置 `AUTH_SECRET_KEY` 变量添加你的密码访问权限,你也应该修改 `index.html` 中的 `title`,防止被关键词搜索到。
切换方式: 切换方式:
@@ -160,7 +163,7 @@ pnpm dev
`ACCESS_TOKEN` 可用: `ACCESS_TOKEN` 可用:
- `OPENAI_ACCESS_TOKEN``OPENAI_API_KEY` 二选一,同时存在时,`OPENAI_API_KEY` 优先 - `OPENAI_ACCESS_TOKEN``OPENAI_API_KEY` 二选一,同时存在时,`OPENAI_API_KEY` 优先
- `API_REVERSE_PROXY` 设置反向代理,可选,默认:`https://bypass.churchless.tech/api/conversation`[社区](https://github.com/transitive-bullshit/chatgpt-api#reverse-proxy)(注意:只有这两个是推荐,其他第三方来源,请自行甄别) - `API_REVERSE_PROXY` 设置反向代理,可选,默认:`https://ai.fakeopen.com/api/conversation`[社区](https://github.com/transitive-bullshit/chatgpt-api#reverse-proxy)(注意:只有这两个是推荐,其他第三方来源,请自行甄别)
通用: 通用:
@@ -234,6 +237,21 @@ services:
``` ```
- `OPENAI_API_BASE_URL` 可选,设置 `OPENAI_API_KEY` 时可用 - `OPENAI_API_BASE_URL` 可选,设置 `OPENAI_API_KEY` 时可用
- `OPENAI_API_MODEL` 可选,设置 `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 部署 ### 使用 Railway 部署
[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/new/template/yytmgc) [![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/new/template/yytmgc)

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
}

View File

@@ -3,6 +3,13 @@ server {
server_name localhost; server_name localhost;
charset utf-8; charset utf-8;
error_page 500 502 503 504 /50x.html; error_page 500 502 503 504 /50x.html;
# 防止爬虫抓取
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 / { location / {
root /usr/share/nginx/html; root /usr/share/nginx/html;
try_files $uri /index.html; try_files $uri /index.html;

View File

@@ -30,7 +30,7 @@ spec:
- name: OPENAI_API_MODEL - name: OPENAI_API_MODEL
value: gpt-3.5-turbo value: gpt-3.5-turbo
- name: API_REVERSE_PROXY - name: API_REVERSE_PROXY
value: https://bypass.churchless.tech/api/conversation value: https://ai.fakeopen.com/api/conversation
- name: AUTH_SECRET_KEY - name: AUTH_SECRET_KEY
value: '123456' value: '123456'
- name: TIMEOUT_MS - name: TIMEOUT_MS

19631
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "chatgpt-web", "name": "chatgpt-web",
"version": "2.10.9", "version": "2.11.0",
"private": false, "private": false,
"description": "ChatGPT Web", "description": "ChatGPT Web",
"author": "ChenZhaoYu <chenzhaoyu1994@gmail.com>", "author": "ChenZhaoYu <chenzhaoyu1994@gmail.com>",

View File

@@ -14,7 +14,7 @@ OPENAI_API_MODEL=
OPENAI_API_DISABLE_DEBUG= OPENAI_API_DISABLE_DEBUG=
# Reverse Proxy - Available on accessToken # Reverse Proxy - Available on accessToken
# Default: https://bypass.churchless.tech/api/conversation # Default: https://ai.fakeopen.com/api/conversation
# More: https://github.com/transitive-bullshit/chatgpt-api#reverse-proxy # More: https://github.com/transitive-bullshit/chatgpt-api#reverse-proxy
API_REVERSE_PROXY= API_REVERSE_PROXY=

View File

@@ -25,7 +25,7 @@
}, },
"dependencies": { "dependencies": {
"axios": "^1.3.4", "axios": "^1.3.4",
"chatgpt": "^5.2.2", "chatgpt": "^5.1.2",
"dotenv": "^16.0.3", "dotenv": "^16.0.3",
"esno": "^0.16.3", "esno": "^0.16.3",
"express": "^4.18.2", "express": "^4.18.2",

1231
service/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,11 +5,10 @@ import { ChatGPTAPI, ChatGPTUnofficialProxyAPI } from 'chatgpt'
import { SocksProxyAgent } from 'socks-proxy-agent' import { SocksProxyAgent } from 'socks-proxy-agent'
import httpsProxyAgent from 'https-proxy-agent' import httpsProxyAgent from 'https-proxy-agent'
import fetch from 'node-fetch' import fetch from 'node-fetch'
import axios from 'axios'
import { sendResponse } from '../utils' import { sendResponse } from '../utils'
import { isNotEmptyString } from '../utils/is' import { isNotEmptyString } from '../utils/is'
import type { ApiModel, ChatContext, ChatGPTUnofficialProxyAPIOptions, ModelConfig } from '../types' import type { ApiModel, ChatContext, ChatGPTUnofficialProxyAPIOptions, ModelConfig } from '../types'
import type { RequestOptions } from './types' import type { RequestOptions, SetProxyOptions, UsageResponse } from './types'
const { HttpsProxyAgent } = httpsProxyAgent const { HttpsProxyAgent } = httpsProxyAgent
@@ -24,10 +23,11 @@ const ErrorCodeMessage: Record<string, string> = {
500: '[OpenAI] 服务器繁忙,请稍后再试 | Internal Server Error', 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' const disableDebug: boolean = process.env.OPENAI_API_DISABLE_DEBUG === 'true'
let apiModel: ApiModel 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)) 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') throw new Error('Missing OPENAI_API_KEY or OPENAI_ACCESS_TOKEN environment variable')
@@ -39,8 +39,6 @@ let api: ChatGPTAPI | ChatGPTUnofficialProxyAPI
if (isNotEmptyString(process.env.OPENAI_API_KEY)) { if (isNotEmptyString(process.env.OPENAI_API_KEY)) {
const OPENAI_API_BASE_URL = process.env.OPENAI_API_BASE_URL const OPENAI_API_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 = { const options: ChatGPTAPIOptions = {
apiKey: process.env.OPENAI_API_KEY, apiKey: process.env.OPENAI_API_KEY,
@@ -70,19 +68,13 @@ let api: ChatGPTAPI | ChatGPTUnofficialProxyAPI
apiModel = 'ChatGPTAPI' apiModel = 'ChatGPTAPI'
} }
else { else {
const OPENAI_API_MODEL = process.env.OPENAI_API_MODEL
const options: ChatGPTUnofficialProxyAPIOptions = { const options: ChatGPTUnofficialProxyAPIOptions = {
accessToken: process.env.OPENAI_ACCESS_TOKEN, accessToken: process.env.OPENAI_ACCESS_TOKEN,
apiReverseProxyUrl: isNotEmptyString(process.env.API_REVERSE_PROXY) ? process.env.API_REVERSE_PROXY : 'https://ai.fakeopen.com/api/conversation',
model,
debug: !disableDebug, debug: !disableDebug,
} }
if (isNotEmptyString(OPENAI_API_MODEL))
options.model = OPENAI_API_MODEL
options.apiReverseProxyUrl = isNotEmptyString(process.env.API_REVERSE_PROXY)
? process.env.API_REVERSE_PROXY
: 'https://bypass.churchless.tech/api/conversation'
setupProxy(options) setupProxy(options)
api = new ChatGPTUnofficialProxyAPI({ ...options }) api = new ChatGPTUnofficialProxyAPI({ ...options })
@@ -91,13 +83,14 @@ let api: ChatGPTAPI | ChatGPTUnofficialProxyAPI
})() })()
async function chatReplyProcess(options: RequestOptions) { async function chatReplyProcess(options: RequestOptions) {
const { message, lastContext, process, systemMessage } = options const { message, lastContext, process, systemMessage, temperature, top_p } = options
try { try {
let options: SendMessageOptions = { timeoutMs } let options: SendMessageOptions = { timeoutMs }
if (apiModel === 'ChatGPTAPI') { if (apiModel === 'ChatGPTAPI') {
if (isNotEmptyString(systemMessage)) if (isNotEmptyString(systemMessage))
options.systemMessage = systemMessage options.systemMessage = systemMessage
options.completionParams = { model, temperature, top_p }
} }
if (lastContext != null) { if (lastContext != null) {
@@ -125,7 +118,7 @@ async function chatReplyProcess(options: RequestOptions) {
} }
} }
async function fetchBalance() { async function fetchUsage() {
const OPENAI_API_KEY = process.env.OPENAI_API_KEY const OPENAI_API_KEY = process.env.OPENAI_API_KEY
const OPENAI_API_BASE_URL = process.env.OPENAI_API_BASE_URL const OPENAI_API_BASE_URL = process.env.OPENAI_API_BASE_URL
@@ -136,19 +129,47 @@ async function fetchBalance() {
? OPENAI_API_BASE_URL ? OPENAI_API_BASE_URL
: 'https://api.openai.com' : 'https://api.openai.com'
try { const [startDate, endDate] = formatDate()
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 const urlUsage = `${API_BASE_URL}/v1/dashboard/billing/usage?start_date=${startDate}&end_date=${endDate}`
return Promise.resolve(balance.toFixed(3))
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('-') 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() { async function chatConfig() {
const balance = await fetchBalance() const usage = await fetchUsage()
const reverseProxy = process.env.API_REVERSE_PROXY ?? '-' const reverseProxy = process.env.API_REVERSE_PROXY ?? '-'
const httpsProxy = (process.env.HTTPS_PROXY || process.env.ALL_PROXY) ?? '-' const httpsProxy = (process.env.HTTPS_PROXY || process.env.ALL_PROXY) ?? '-'
const socksProxy = (process.env.SOCKS_PROXY_HOST && process.env.SOCKS_PROXY_PORT) const socksProxy = (process.env.SOCKS_PROXY_HOST && process.env.SOCKS_PROXY_PORT)
@@ -156,11 +177,11 @@ async function chatConfig() {
: '-' : '-'
return sendResponse<ModelConfig>({ return sendResponse<ModelConfig>({
type: 'Success', type: 'Success',
data: { apiModel, reverseProxy, timeoutMs, socksProxy, httpsProxy, balance }, data: { apiModel, reverseProxy, timeoutMs, socksProxy, httpsProxy, usage },
}) })
} }
function setupProxy(options: ChatGPTAPIOptions | ChatGPTUnofficialProxyAPIOptions) { function setupProxy(options: SetProxyOptions) {
if (isNotEmptyString(process.env.SOCKS_PROXY_HOST) && isNotEmptyString(process.env.SOCKS_PROXY_PORT)) { if (isNotEmptyString(process.env.SOCKS_PROXY_HOST) && isNotEmptyString(process.env.SOCKS_PROXY_PORT)) {
const agent = new SocksProxyAgent({ const agent = new SocksProxyAgent({
hostname: process.env.SOCKS_PROXY_HOST, hostname: process.env.SOCKS_PROXY_HOST,
@@ -172,17 +193,20 @@ function setupProxy(options: ChatGPTAPIOptions | ChatGPTUnofficialProxyAPIOption
return fetch(url, { agent, ...options }) return fetch(url, { agent, ...options })
} }
} }
else { else if (isNotEmptyString(process.env.HTTPS_PROXY) || isNotEmptyString(process.env.ALL_PROXY)) {
if (isNotEmptyString(process.env.HTTPS_PROXY) || isNotEmptyString(process.env.ALL_PROXY)) { const httpsProxy = process.env.HTTPS_PROXY || process.env.ALL_PROXY
const httpsProxy = process.env.HTTPS_PROXY || process.env.ALL_PROXY if (httpsProxy) {
if (httpsProxy) { const agent = new HttpsProxyAgent(httpsProxy)
const agent = new HttpsProxyAgent(httpsProxy) options.fetch = (url, options) => {
options.fetch = (url, options) => { return fetch(url, { agent, ...options })
return fetch(url, { agent, ...options })
}
} }
} }
} }
else {
options.fetch = (url, options) => {
return fetch(url, { ...options })
}
}
} }
function currentModel(): ApiModel { function currentModel(): ApiModel {

View File

@@ -1,8 +1,19 @@
import type { ChatMessage } from 'chatgpt' import type { ChatMessage } from 'chatgpt'
import type fetch from 'node-fetch'
export interface RequestOptions { export interface RequestOptions {
message: string message: string
lastContext?: { conversationId?: string; parentMessageId?: string } lastContext?: { conversationId?: string; parentMessageId?: string }
process?: (chat: ChatMessage) => void process?: (chat: ChatMessage) => void
systemMessage?: string 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') res.setHeader('Content-type', 'application/octet-stream')
try { try {
const { prompt, options = {}, systemMessage } = req.body as RequestProps const { prompt, options = {}, systemMessage, temperature, top_p } = req.body as RequestProps
let firstChunk = true let firstChunk = true
await chatReplyProcess({ await chatReplyProcess({
message: prompt, message: prompt,
@@ -33,6 +33,8 @@ router.post('/chat-process', [auth, limiter], async (req, res) => {
firstChunk = false firstChunk = false
}, },
systemMessage, systemMessage,
temperature,
top_p,
}) })
} }
catch (error) { catch (error) {

View File

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

View File

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

View File

@@ -11,7 +11,7 @@ interface ConfigState {
apiModel?: string apiModel?: string
socksProxy?: string socksProxy?: string
httpsProxy?: string httpsProxy?: string
balance?: string usage?: string
} }
const authStore = useAuthStore() const authStore = useAuthStore()
@@ -52,17 +52,17 @@ onMounted(() => {
href="https://github.com/Chanzhaoyu/chatgpt-web" href="https://github.com/Chanzhaoyu/chatgpt-web"
target="_blank" target="_blank"
> >
Github GitHub
</a> </a>
免费且基于 MIT 协议没有任何形式的付费行为 免费且基于 MIT 协议没有任何形式的付费行为
</p> </p>
<p> <p>
如果你觉得此项目对你有帮助请在 Github 帮我点个 Star 或者给予一点赞助谢谢 如果你觉得此项目对你有帮助请在 GitHub 帮我点个 Star 或者给予一点赞助谢谢
</p> </p>
</div> </div>
<p>{{ $t("setting.api") }}{{ config?.apiModel ?? '-' }}</p> <p>{{ $t("setting.api") }}{{ config?.apiModel ?? '-' }}</p>
<p v-if="isChatGPTAPI"> <p v-if="isChatGPTAPI">
{{ $t("setting.balance") }}{{ config?.balance ?? '-' }} {{ $t("setting.monthlyUsage") }}{{ config?.usage ?? '-' }}
</p> </p>
<p v-if="!isChatGPTAPI"> <p v-if="!isChatGPTAPI">
{{ $t("setting.reverseProxy") }}{{ config?.reverseProxy ?? '-' }} {{ $t("setting.reverseProxy") }}{{ config?.reverseProxy ?? '-' }}

View File

@@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue' import { ref } from 'vue'
import { NButton, NInput, useMessage } from 'naive-ui' import { NButton, NInput, NSlider, useMessage } from 'naive-ui'
import { useSettingStore } from '@/store' import { useSettingStore } from '@/store'
import type { SettingsState } from '@/store/modules/settings/helper' import type { SettingsState } from '@/store/modules/settings/helper'
import { t } from '@/locales' import { t } from '@/locales'
@@ -11,6 +11,10 @@ const ms = useMessage()
const systemMessage = ref(settingStore.systemMessage ?? '') 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>) { function updateSettings(options: Partial<SettingsState>) {
settingStore.updateSetting(options) settingStore.updateSetting(options)
ms.success(t('common.success')) ms.success(t('common.success'))
@@ -27,7 +31,7 @@ function handleReset() {
<div class="p-4 space-y-5 min-h-[200px]"> <div class="p-4 space-y-5 min-h-[200px]">
<div class="space-y-6"> <div class="space-y-6">
<div class="flex items-center space-x-4"> <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"> <div class="flex-1">
<NInput v-model:value="systemMessage" type="textarea" :autosize="{ minRows: 1, maxRows: 4 }" /> <NInput v-model:value="systemMessage" type="textarea" :autosize="{ minRows: 1, maxRows: 4 }" />
</div> </div>
@@ -36,7 +40,27 @@ function handleReset() {
</NButton> </NButton>
</div> </div>
<div class="flex items-center space-x-4"> <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="1" :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"> <NButton size="small" @click="handleReset">
{{ $t('common.reset') }} {{ $t('common.reset') }}
</NButton> </NButton>

View File

@@ -57,6 +57,8 @@ const languageOptions: { label: string; key: Language; value: Language }[] = [
{ label: '简体中文', key: 'zh-CN', value: 'zh-CN' }, { label: '简体中文', key: 'zh-CN', value: 'zh-CN' },
{ label: '繁體中文', key: 'zh-TW', value: 'zh-TW' }, { label: '繁體中文', key: 'zh-TW', value: 'zh-TW' },
{ label: 'English', key: 'en-US', value: 'en-US' }, { label: 'English', key: 'en-US', value: 'en-US' },
{ label: '한국어', key: 'ko-KR', value: 'ko-KR' },
{ label: 'Русский язык', key: 'ru-RU', value: 'ru-RU' },
] ]
function updateUserInfo(options: Partial<UserInfo>) { function updateUserInfo(options: Partial<UserInfo>) {

View File

@@ -1,5 +1,5 @@
import { computed } from 'vue' import { computed } from 'vue'
import { enUS, zhCN, zhTW } from 'naive-ui' import { enUS, koKR, zhCN, zhTW } from 'naive-ui'
import { useAppStore } from '@/store' import { useAppStore } from '@/store'
import { setLocale } from '@/locales' import { setLocale } from '@/locales'
@@ -11,6 +11,12 @@ export function useLanguage() {
case 'en-US': case 'en-US':
setLocale('en-US') setLocale('en-US')
return enUS return enUS
case 'ru-RU':
setLocale('ru-RU')
return enUS
case 'ko-KR':
setLocale('ko-KR')
return koKR
case 'zh-CN': case 'zh-CN':
setLocale('zh-CN') setLocale('zh-CN')
return zhCN return zhCN
@@ -19,7 +25,7 @@ export function useLanguage() {
return zhTW return zhTW
default: default:
setLocale('zh-CN') setLocale('zh-CN')
return enUS return zhCN
} }
}) })

View File

@@ -29,7 +29,7 @@ export default {
}, },
chat: { chat: {
newChatButton: 'New Chat', newChatButton: 'New Chat',
placeholder: 'Ask me anything...(Shift + Enter = line break)', placeholder: 'Ask me anything...(Shift + Enter = line break, "/" to trigger prompts)',
placeholderMobile: 'Ask me anything...', placeholderMobile: 'Ask me anything...',
copy: 'Copy', copy: 'Copy',
copied: 'Copied', copied: 'Copied',
@@ -59,6 +59,8 @@ export default {
name: 'Name', name: 'Name',
description: 'Description', description: 'Description',
role: 'Role', role: 'Role',
temperature: 'Temperature',
top_p: 'Top_p',
resetUserInfo: 'Reset UserInfo', resetUserInfo: 'Reset UserInfo',
chatHistory: 'ChatHistory', chatHistory: 'ChatHistory',
theme: 'Theme', theme: 'Theme',
@@ -69,6 +71,7 @@ export default {
socks: 'Socks', socks: 'Socks',
httpsProxy: 'HTTPS Proxy', httpsProxy: 'HTTPS Proxy',
balance: 'API Balance', balance: 'API Balance',
monthlyUsage: 'Monthly Usage',
}, },
store: { store: {
siderButton: 'Prompt Store', siderButton: 'Prompt Store',

View File

@@ -1,8 +1,10 @@
import type { App } from 'vue' import type { App } from 'vue'
import { createI18n } from 'vue-i18n' import { createI18n } from 'vue-i18n'
import enUS from './en-US' import enUS from './en-US'
import koKR from './ko-KR'
import zhCN from './zh-CN' import zhCN from './zh-CN'
import zhTW from './zh-TW' import zhTW from './zh-TW'
import ruRU from './ru-RU'
import { useAppStoreWithOut } from '@/store/modules/app' import { useAppStoreWithOut } from '@/store/modules/app'
import type { Language } from '@/store/modules/app/helper' import type { Language } from '@/store/modules/app/helper'
@@ -16,8 +18,10 @@ const i18n = createI18n({
allowComposition: true, allowComposition: true,
messages: { messages: {
'en-US': enUS, 'en-US': enUS,
'ko-KR': koKR,
'zh-CN': zhCN, 'zh-CN': zhCN,
'zh-TW': zhTW, 'zh-TW': zhTW,
'ru-RU': ruRU,
}, },
}) })

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

@@ -0,0 +1,93 @@
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: '인증되지 않았습니다. 먼저 확인하십시오.',
},
chat: {
newChatButton: '새로운 채팅',
placeholder: '무엇이든 물어보세요...(Shift + Enter = 줄바꿈, "/"를 눌러서 힌트를 보세요)',
placeholderMobile: '무엇이든 물어보세요...',
copy: '복사',
copied: '복사됨',
copyCode: '코드 복사',
clearChat: '채팅 비우기',
clearChatConfirm: '이 채팅을 비우시겠습니까?',
exportImage: '이미지 내보내기',
exportImageConfirm: '이 채팅을 png로 내보내시겠습니까?',
exportSuccess: '내보내기 성공',
exportFailed: '내보내기 실패',
usingContext: '컨텍스트 모드',
turnOnContext: '현재 모드에서는 이전 대화 기록을 포함하여 메시지를 보낼 수 있습니다.',
turnOffContext: '현재 모드에서는 이전 대화 기록을 포함하지 않고 메시지를 보낼 수 있습니다.',
deleteMessage: '메시지 삭제',
deleteMessageConfirm: '이 메시지를 삭제하시겠습니까?',
deleteHistoryConfirm: '이 기록을 삭제하시겠습니까?',
clearHistoryConfirm: '채팅 기록을 삭제하시겠습니까?',
preview: '미리보기',
showRawText: '원본 텍스트로 보기',
},
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: '월 사용량',
},
store: {
siderButton: '프롬프트 저장소',
local: '로컬',
online: '온라인',
title: '제목',
description: '설명',
clearStoreConfirm: '데이터를 삭제하시겠습니까?',
importPlaceholder: '여기에 JSON 데이터를 붙여넣으십시오',
addRepeatTitleTips: '제목 중복됨, 다시 입력하십시오',
addRepeatContentTips: '내용 중복됨: {msg}, 다시 입력하십시오',
editRepeatTitleTips: '제목 충돌, 수정하십시오',
editRepeatContentTips: '내용 충돌 {msg} , 수정하십시오',
importError: '키 값 불일치',
importRepeatTitle: '제목이 반복되어 건너뜀: {msg}',
importRepeatContent: '내용이 반복되어 건너뜀: {msg}',
onlineImportWarning: '참고: JSON 파일 소스를 확인하십시오!',
},
}

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

@@ -0,0 +1,94 @@
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: 'Не авторизован, сначала подтвердите свою личность.',
},
chat: {
newChatButton: 'Новый чат',
placeholder: 'Спросите меня о чем-нибудь ... (Shift + Enter = перенос строки, "/" для вызова подсказок)',
placeholderMobile: 'Спросите меня о чем-нибудь ...',
copy: 'Копировать',
copied: 'Скопировано',
copyCode: 'Копировать код',
clearChat: 'Очистить чат',
clearChatConfirm: 'Вы уверены, что хотите очистить этот чат?',
exportImage: 'Экспорт в изображение',
exportImageConfirm: 'Вы уверены, что хотите экспортировать этот чат в формате PNG?',
exportSuccess: 'Экспортировано успешно',
exportFailed: 'Не удалось выполнить экспорт',
usingContext: 'Режим контекста',
turnOnContext: 'В текущем режиме отправка сообщений будет включать предыдущие записи чата.',
turnOffContext: 'В текущем режиме отправка сообщений не будет включать предыдущие записи чата.',
deleteMessage: 'Удалить сообщение',
deleteMessageConfirm: 'Вы уверены, что хотите удалить это сообщение?',
deleteHistoryConfirm: 'Вы уверены, что хотите очистить эту историю?',
clearHistoryConfirm: 'Вы уверены, что хотите очистить историю чата?',
preview: 'Предварительный просмотр',
showRawText: 'Показать как обычный текст',
},
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: 'Ежемесячное использование',
},
store: {
siderButton: 'Хранилище подсказок',
local: 'Локальное',
online: 'Онлайн',
title: 'Название',
description: 'Описание',
clearStoreConfirm: 'Вы действительно хотите очистить данные?',
importPlaceholder: 'Пожалуйста, вставьте здесь JSON-данные',
addRepeatTitleTips: 'Дубликат названия, пожалуйста, введите другое название',
addRepeatContentTips: 'Дубликат содержимого: {msg}, пожалуйста, введите другой текст',
editRepeatTitleTips: 'Конфликт названий, пожалуйста, измените название',
editRepeatContentTips: 'Конфликт содержимого {msg}, пожалуйста, измените текст',
importError: 'Не совпадает ключ-значение',
importRepeatTitle: 'Название повторяющееся, пропускается: {msg}',
importRepeatContent: 'Содержание повторяющееся, пропускается: {msg}',
onlineImportWarning: 'Внимание! Проверьте источник JSON-файла!',
downloadError: 'Проверьте состояние сети и правильность JSON-файла',
},
}

View File

@@ -29,7 +29,7 @@ export default {
}, },
chat: { chat: {
newChatButton: '新建聊天', newChatButton: '新建聊天',
placeholder: '来说点什么吧...Shift + Enter = 换行)', placeholder: '来说点什么吧...Shift + Enter = 换行"/" 触发提示词',
placeholderMobile: '来说点什么...', placeholderMobile: '来说点什么...',
copy: '复制', copy: '复制',
copied: '复制成功', copied: '复制成功',
@@ -59,6 +59,8 @@ export default {
name: '名称', name: '名称',
description: '描述', description: '描述',
role: '角色设定', role: '角色设定',
temperature: 'Temperature',
top_p: 'Top_p',
resetUserInfo: '重置用户信息', resetUserInfo: '重置用户信息',
chatHistory: '聊天记录', chatHistory: '聊天记录',
theme: '主题', theme: '主题',
@@ -69,6 +71,7 @@ export default {
socks: 'Socks', socks: 'Socks',
httpsProxy: 'HTTPS Proxy', httpsProxy: 'HTTPS Proxy',
balance: 'API余额', balance: 'API余额',
monthlyUsage: '本月使用量',
}, },
store: { store: {
siderButton: '提示词商店', siderButton: '提示词商店',

View File

@@ -28,8 +28,8 @@ export default {
unauthorizedTips: '未經授權,請先進行驗證。', unauthorizedTips: '未經授權,請先進行驗證。',
}, },
chat: { chat: {
newChatButton: '新對話', newChatButton: '新對話',
placeholder: '來說點什麼...Shift + Enter = 換行)', placeholder: '來說點什麼...Shift + Enter = 換行"/" 觸發提示詞',
placeholderMobile: '來說點什麼...', placeholderMobile: '來說點什麼...',
copy: '複製', copy: '複製',
copied: '複製成功', copied: '複製成功',
@@ -53,12 +53,14 @@ export default {
setting: { setting: {
setting: '設定', setting: '設定',
general: '總覽', general: '總覽',
advanced: '高級', advanced: '進階',
config: '設定', config: '設定',
avatarLink: '頭貼連結', avatarLink: '頭貼連結',
name: '名稱', name: '名稱',
description: '描述', description: '描述',
role: '角色設定', role: '角色設定',
temperature: 'Temperature',
top_p: 'Top_p',
resetUserInfo: '重設使用者資訊', resetUserInfo: '重設使用者資訊',
chatHistory: '紀錄', chatHistory: '紀錄',
theme: '主題', theme: '主題',
@@ -68,7 +70,8 @@ export default {
timeout: '逾時', timeout: '逾時',
socks: 'Socks', socks: 'Socks',
httpsProxy: 'HTTPS Proxy', httpsProxy: 'HTTPS Proxy',
balance: 'API額', balance: 'API Credit 餘額',
monthlyUsage: '本月使用量',
}, },
store: { store: {
siderButton: '提示詞商店', siderButton: '提示詞商店',

View File

@@ -4,7 +4,7 @@ const LOCAL_NAME = 'appSetting'
export type Theme = 'light' | 'dark' | 'auto' export type Theme = 'light' | 'dark' | 'auto'
export type Language = 'zh-CN' | 'zh-TW' | 'en-US' export type Language = 'zh-CN' | 'zh-TW' | 'en-US' | 'ko-KR' | 'ru-RU'
export interface AppState { export interface AppState {
siderCollapsed: boolean siderCollapsed: boolean

View File

@@ -4,11 +4,15 @@ const LOCAL_NAME = 'settingsStorage'
export interface SettingsState { export interface SettingsState {
systemMessage: string systemMessage: string
temperature: number
top_p: number
} }
export function defaultSetting(): SettingsState { export function defaultSetting(): SettingsState {
return { return {
systemMessage: 'You are ChatGPT, a large language model trained by OpenAI. Follow the user\'s instructions carefully. Respond using markdown.', 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: { userInfo: {
avatar: 'https://raw.githubusercontent.com/Chanzhaoyu/chatgpt-web/main/src/assets/avatar.jpg', avatar: 'https://raw.githubusercontent.com/Chanzhaoyu/chatgpt-web/main/src/assets/avatar.jpg',
name: 'ChenZhaoYu', 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>',
}, },
} }
} }

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

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

@@ -1,11 +1,12 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref } from 'vue' import { computed, onMounted, onUnmounted, onUpdated, ref } from 'vue'
import MarkdownIt from 'markdown-it' import MarkdownIt from 'markdown-it'
import mdKatex from '@traptitech/markdown-it-katex' import mdKatex from '@traptitech/markdown-it-katex'
import mila from 'markdown-it-link-attributes' import mila from 'markdown-it-link-attributes'
import hljs from 'highlight.js' import hljs from 'highlight.js'
import { useBasicLayout } from '@/hooks/useBasicLayout' import { useBasicLayout } from '@/hooks/useBasicLayout'
import { t } from '@/locales' import { t } from '@/locales'
import { copyToClip } from '@/utils/copy'
interface Props { interface Props {
inversion?: boolean inversion?: boolean
@@ -22,6 +23,7 @@ const { isMobile } = useBasicLayout()
const textRef = ref<HTMLElement>() const textRef = ref<HTMLElement>()
const mdi = new MarkdownIt({ const mdi = new MarkdownIt({
html: false,
linkify: true, linkify: true,
highlight(code, language) { highlight(code, language) {
const validLang = !!(language && hljs.getLanguage(language)) const validLang = !!(language && hljs.getLanguage(language))
@@ -60,18 +62,58 @@ 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>` 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 = '复制成功'
setTimeout(() => {
btn.textContent = '复制代码'
}, 1000)
})
}
})
})
}
}
function removeCopyEvents() {
if (textRef.value) {
const copyBtn = textRef.value.querySelectorAll('.code-block-header__copy')
copyBtn.forEach((btn) => {
btn.removeEventListener('click', () => {})
})
}
}
onMounted(() => {
addCopyEvents()
})
onUpdated(() => {
addCopyEvents()
})
onUnmounted(() => {
removeCopyEvents()
})
</script> </script>
<template> <template>
<div class="text-black" :class="wrapClass"> <div class="text-black" :class="wrapClass">
<div ref="textRef" class="leading-relaxed break-words"> <div ref="textRef" class="leading-relaxed break-words">
<div v-if="!inversion" class="flex items-end"> <div v-if="!inversion">
<div v-if="!asRawText" class="w-full markdown-body" v-html="text" /> <div v-if="!asRawText" class="markdown-body" v-html="text" />
<div v-else class="w-full whitespace-pre-wrap" v-text="text" /> <div v-else class="whitespace-pre-wrap" v-text="text" />
<span v-if="loading" class="dark:text-white w-[4px] h-[20px] block animate-blink" />
</div> </div>
<div v-else class="whitespace-pre-wrap" v-text="text" /> <div v-else class="whitespace-pre-wrap" v-text="text" />
<template v-if="loading">
<span class="dark:text-white w-[4px] h-[20px] block animate-blink" />
</template>
</div> </div>
</div> </div>
</template> </template>

View File

@@ -1,13 +1,13 @@
<script setup lang='ts'> <script setup lang='ts'>
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { NDropdown } from 'naive-ui' import { NDropdown, useMessage } from 'naive-ui'
import AvatarComponent from './Avatar.vue' import AvatarComponent from './Avatar.vue'
import TextComponent from './Text.vue' import TextComponent from './Text.vue'
import { SvgIcon } from '@/components/common' import { SvgIcon } from '@/components/common'
import { copyText } from '@/utils/format'
import { useIconRender } from '@/hooks/useIconRender' import { useIconRender } from '@/hooks/useIconRender'
import { t } from '@/locales' import { t } from '@/locales'
import { useBasicLayout } from '@/hooks/useBasicLayout' import { useBasicLayout } from '@/hooks/useBasicLayout'
import { copyToClip } from '@/utils/copy'
interface Props { interface Props {
dateTime?: string dateTime?: string
@@ -30,6 +30,8 @@ const { isMobile } = useBasicLayout()
const { iconRender } = useIconRender() const { iconRender } = useIconRender()
const message = useMessage()
const textRef = ref<HTMLElement>() const textRef = ref<HTMLElement>()
const asRawText = ref(props.inversion) const asRawText = ref(props.inversion)
@@ -64,7 +66,7 @@ const options = computed(() => {
function handleSelect(key: 'copyText' | 'delete' | 'toggleRenderType') { function handleSelect(key: 'copyText' | 'delete' | 'toggleRenderType') {
switch (key) { switch (key) {
case 'copyText': case 'copyText':
copyText({ text: props.text ?? '' }) handleCopy()
return return
case 'toggleRenderType': case 'toggleRenderType':
asRawText.value = !asRawText.value asRawText.value = !asRawText.value
@@ -78,6 +80,16 @@ function handleRegenerate() {
messageRef.value?.scrollIntoView() messageRef.value?.scrollIntoView()
emit('regenerate') emit('regenerate')
} }
async function handleCopy() {
try {
await copyToClip(props.text || '')
message.success('复制成功')
}
catch {
message.error('复制失败')
}
}
</script> </script>
<template> <template>

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

@@ -8,7 +8,6 @@ import html2canvas from 'html2canvas'
import { Message } from './components' import { Message } from './components'
import { useScroll } from './hooks/useScroll' import { useScroll } from './hooks/useScroll'
import { useChat } from './hooks/useChat' import { useChat } from './hooks/useChat'
import { useCopyCode } from './hooks/useCopyCode'
import { useUsingContext } from './hooks/useUsingContext' import { useUsingContext } from './hooks/useUsingContext'
import HeaderComponent from './components/Header/index.vue' import HeaderComponent from './components/Header/index.vue'
import { HoverButton, SvgIcon } from '@/components/common' import { HoverButton, SvgIcon } from '@/components/common'
@@ -27,8 +26,6 @@ const ms = useMessage()
const chatStore = useChatStore() const chatStore = useChatStore()
useCopyCode()
const { isMobile } = useBasicLayout() const { isMobile } = useBasicLayout()
const { addChat, updateChat, updateChatSome, getChatByUuidAndIndex } = useChat() const { addChat, updateChat, updateChatSome, getChatByUuidAndIndex } = useChat()
const { scrollRef, scrollToBottom, scrollToBottomIfAtBottom } = useScroll() const { scrollRef, scrollToBottom, scrollToBottomIfAtBottom } = useScroll()
@@ -37,7 +34,7 @@ const { usingContext, toggleUsingContext } = useUsingContext()
const { uuid } = route.params as { uuid: string } const { uuid } = route.params as { uuid: string }
const dataSources = computed(() => chatStore.getChatByUuid(+uuid)) 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 prompt = ref<string>('')
const loading = ref<boolean>(false) const loading = ref<boolean>(false)
@@ -233,7 +230,7 @@ async function onRegenerate(index: number) {
error: false, error: false,
loading: true, loading: true,
conversationOptions: null, conversationOptions: null,
requestOptions: { prompt: message, ...options }, requestOptions: { prompt: message, options: { ...options } },
}, },
) )
@@ -264,7 +261,7 @@ async function onRegenerate(index: number) {
error: false, error: false,
loading: true, loading: true,
conversationOptions: { conversationId: data.conversationId, parentMessageId: data.id }, conversationOptions: { conversationId: data.conversationId, parentMessageId: data.id },
requestOptions: { prompt: message, ...options }, requestOptions: { prompt: message, options: { ...options } },
}, },
) )
@@ -308,7 +305,7 @@ async function onRegenerate(index: number) {
error: true, error: true,
loading: false, loading: false,
conversationOptions: null, conversationOptions: null,
requestOptions: { prompt: message, ...options }, requestOptions: { prompt: message, options: { ...options } },
}, },
) )
} }