初始化
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"extends": ["@commitlint/config-conventional"]
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
**/node_modules
|
||||||
|
*/node_modules
|
||||||
|
node_modules
|
||||||
|
Dockerfile
|
||||||
|
.*
|
||||||
|
*/.*
|
||||||
|
!.env
|
|
@ -0,0 +1,11 @@
|
||||||
|
# Editor configuration, see http://editorconfig.org
|
||||||
|
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = tab
|
||||||
|
indent_size = 2
|
||||||
|
end_of_line = lf
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
|
@ -0,0 +1,10 @@
|
||||||
|
# Glob API URL
|
||||||
|
VITE_GLOB_API_URL=/api
|
||||||
|
|
||||||
|
VITE_APP_API_BASE_URL=http://127.0.0.1:3002/
|
||||||
|
|
||||||
|
# Whether long replies are supported, which may result in higher API fees
|
||||||
|
VITE_GLOB_OPEN_LONG_REPLY=false
|
||||||
|
|
||||||
|
# When you want to use PWA
|
||||||
|
VITE_GLOB_APP_PWA=false
|
|
@ -0,0 +1,2 @@
|
||||||
|
docker-compose
|
||||||
|
kubernetes
|
|
@ -0,0 +1,4 @@
|
||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
extends: ['@antfu'],
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
"*.vue" eol=lf
|
||||||
|
"*.js" eol=lf
|
||||||
|
"*.ts" eol=lf
|
||||||
|
"*.jsx" eol=lf
|
||||||
|
"*.tsx" eol=lf
|
||||||
|
"*.cjs" eol=lf
|
||||||
|
"*.cts" eol=lf
|
||||||
|
"*.mjs" eol=lf
|
||||||
|
"*.mts" eol=lf
|
||||||
|
"*.json" eol=lf
|
||||||
|
"*.html" eol=lf
|
||||||
|
"*.css" eol=lf
|
||||||
|
"*.less" eol=lf
|
||||||
|
"*.scss" eol=lf
|
||||||
|
"*.sass" eol=lf
|
||||||
|
"*.styl" eol=lf
|
||||||
|
"*.md" eol=lf
|
|
@ -0,0 +1,41 @@
|
||||||
|
name: build_docker
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
release:
|
||||||
|
types: [created] # 表示在创建新的 Release 时触发
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build_docker:
|
||||||
|
name: Build docker
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- run: |
|
||||||
|
echo "本次构建的版本为:${GITHUB_REF_NAME} (但是这个变量目前上下文中无法获取到)"
|
||||||
|
echo 本次构建的版本为:${{ github.ref_name }}
|
||||||
|
env
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v2
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
- name: Build and push
|
||||||
|
id: docker_build
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
tags: |
|
||||||
|
${{ secrets.DOCKERHUB_USERNAME }}/chatgpt-web:${{ github.ref_name }}
|
||||||
|
${{ secrets.DOCKERHUB_USERNAME }}/chatgpt-web:latest
|
|
@ -0,0 +1,47 @@
|
||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Set node
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 18.x
|
||||||
|
|
||||||
|
- name: Setup
|
||||||
|
run: npm i -g @antfu/ni
|
||||||
|
|
||||||
|
- name: Install
|
||||||
|
run: nci
|
||||||
|
|
||||||
|
- name: Lint
|
||||||
|
run: nr lint:fix
|
||||||
|
|
||||||
|
typecheck:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Set node
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 18.x
|
||||||
|
|
||||||
|
- name: Setup
|
||||||
|
run: npm i -g @antfu/ni
|
||||||
|
|
||||||
|
- name: Install
|
||||||
|
run: nci
|
||||||
|
|
||||||
|
- name: Typecheck
|
||||||
|
run: nr type-check
|
|
@ -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 }}
|
|
@ -0,0 +1,32 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
coverage
|
||||||
|
*.local
|
||||||
|
|
||||||
|
/cypress/videos/
|
||||||
|
/cypress/screenshots/
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
# Environment variables files
|
||||||
|
/service/.env
|
|
@ -0,0 +1,4 @@
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
. "$(dirname -- "$0")/_/husky.sh"
|
||||||
|
|
||||||
|
npx --no -- commitlint --edit
|
|
@ -0,0 +1,4 @@
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
. "$(dirname -- "$0")/_/husky.sh"
|
||||||
|
|
||||||
|
npx lint-staged
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"recommendations": ["Vue.volar", "dbaeumer.vscode-eslint"]
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
{
|
||||||
|
"prettier.enable": false,
|
||||||
|
"editor.formatOnSave": false,
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.eslint": true
|
||||||
|
},
|
||||||
|
"eslint.validate": [
|
||||||
|
"javascript",
|
||||||
|
"javascriptreact",
|
||||||
|
"typescript",
|
||||||
|
"typescriptreact",
|
||||||
|
"vue",
|
||||||
|
"html",
|
||||||
|
"json",
|
||||||
|
"jsonc",
|
||||||
|
"json5",
|
||||||
|
"yaml",
|
||||||
|
"yml",
|
||||||
|
"markdown"
|
||||||
|
],
|
||||||
|
"cSpell.words": [
|
||||||
|
"antfu",
|
||||||
|
"axios",
|
||||||
|
"bumpp",
|
||||||
|
"chatgpt",
|
||||||
|
"chenzhaoyu",
|
||||||
|
"commitlint",
|
||||||
|
"davinci",
|
||||||
|
"dockerhub",
|
||||||
|
"esno",
|
||||||
|
"GPTAPI",
|
||||||
|
"highlightjs",
|
||||||
|
"hljs",
|
||||||
|
"iconify",
|
||||||
|
"katex",
|
||||||
|
"katexmath",
|
||||||
|
"linkify",
|
||||||
|
"logprobs",
|
||||||
|
"mdhljs",
|
||||||
|
"mila",
|
||||||
|
"nodata",
|
||||||
|
"OPENAI",
|
||||||
|
"pinia",
|
||||||
|
"Popconfirm",
|
||||||
|
"rushstack",
|
||||||
|
"Sider",
|
||||||
|
"tailwindcss",
|
||||||
|
"traptitech",
|
||||||
|
"tsup",
|
||||||
|
"Typecheck",
|
||||||
|
"unplugin",
|
||||||
|
"VITE",
|
||||||
|
"vueuse",
|
||||||
|
"Zhao"
|
||||||
|
],
|
||||||
|
"i18n-ally.enabledParsers": [
|
||||||
|
"ts"
|
||||||
|
],
|
||||||
|
"i18n-ally.sortKeys": true,
|
||||||
|
"i18n-ally.keepFulfilled": true,
|
||||||
|
"i18n-ally.localesPaths": [
|
||||||
|
"src/locales"
|
||||||
|
],
|
||||||
|
"i18n-ally.keystyle": "nested"
|
||||||
|
}
|
|
@ -0,0 +1,602 @@
|
||||||
|
## 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`
|
||||||
|
|
||||||
|
如遇问题,请删除 `node_modules` 重新安装依赖。
|
||||||
|
|
||||||
|
## Feature
|
||||||
|
- 显示回复消息原文的选项 [[yilozt](https://github.com/Chanzhaoyu/chatgpt-web/pull/672)]
|
||||||
|
- 添加单 `IP` 每小时请求限制。环境变量: `MAX_REQUEST_PER_HOUR` [[zhuxindong ](https://github.com/Chanzhaoyu/chatgpt-web/pull/718)]
|
||||||
|
- 前端添加角色设定,仅 `API` 方式可见 [[quzard](https://github.com/Chanzhaoyu/chatgpt-web/pull/768)]
|
||||||
|
- `OPENAI_API_MODEL` 变量现在对 `ChatGPTUnofficialProxyAPI` 也生效,注意:`Token` 和 `API` 的模型命名不一致,不能直接填入 `gpt-3.5` 或者 `gpt-4` [[hncboy](https://github.com/Chanzhaoyu/chatgpt-web/pull/632)]
|
||||||
|
- 添加繁体中文 `Prompts` [[PeterDaveHello](https://github.com/Chanzhaoyu/chatgpt-web/pull/796)]
|
||||||
|
|
||||||
|
## Enhancement
|
||||||
|
- 重置回答时滚动定位至该回答 [[shunyue1320](https://github.com/Chanzhaoyu/chatgpt-web/pull/781)]
|
||||||
|
- 当 `API` 是 `gpt-4` 时增加可用的 `Max Tokens` [[simonwu53](https://github.com/Chanzhaoyu/chatgpt-web/pull/729)]
|
||||||
|
- 判断和忽略回复字符 [[liut](https://github.com/Chanzhaoyu/chatgpt-web/pull/474)]
|
||||||
|
- 切换会话时,自动聚焦输入框 [[JS-an](https://github.com/Chanzhaoyu/chatgpt-web/pull/735)]
|
||||||
|
- 渲染的链接新窗口打开
|
||||||
|
- 查询余额可选 `API_BASE_URL` 代理地址
|
||||||
|
- `config` 接口添加验证防止被无限制调用
|
||||||
|
- `PWA` 默认不开启,现在需手动修改 `.env` 文件 `VITE_GLOB_APP_PWA` 变量
|
||||||
|
- 当网络连接时,刷新页面,`500` 错误页自动跳转到主页
|
||||||
|
|
||||||
|
## BugFix
|
||||||
|
- `scrollToBottom` 调回 `scrollToBottomIfAtBottom` [[shunyue1320](https://github.com/Chanzhaoyu/chatgpt-web/pull/771)]
|
||||||
|
- 重置异常的 `loading` 会话
|
||||||
|
|
||||||
|
## Common
|
||||||
|
- 创建 `start.cmd` 在 `windows` 下也可以运行 [vulgatecnn](https://github.com/Chanzhaoyu/chatgpt-web/pull/656)]
|
||||||
|
- 添加 `visual-studio-code` 中调试配置 [[ChandlerVer5](https://github.com/Chanzhaoyu/chatgpt-web/pull/296)]
|
||||||
|
- 修复文档中 `docker` 端口为本地 [[kilvn](https://github.com/Chanzhaoyu/chatgpt-web/pull/802)]
|
||||||
|
## Other
|
||||||
|
- 依赖更新
|
||||||
|
|
||||||
|
|
||||||
|
## v2.10.7
|
||||||
|
|
||||||
|
`2023-03-17`
|
||||||
|
|
||||||
|
## BugFix
|
||||||
|
- 回退 `chatgpt` 版本,原因:导致 `OPENAI_API_BASE_URL` 代理失效
|
||||||
|
- 修复缺省状态的 `usingContext` 默认值
|
||||||
|
|
||||||
|
## v2.10.6
|
||||||
|
|
||||||
|
`2023-03-17`
|
||||||
|
|
||||||
|
## Feature
|
||||||
|
- 显示 `API` 余额 [[pzcn](https://github.com/Chanzhaoyu/chatgpt-web/pull/582)]
|
||||||
|
|
||||||
|
## Enhancement
|
||||||
|
- 美化滚动条样式和 `UI` 保持一致 [[haydenull](https://github.com/Chanzhaoyu/chatgpt-web/pull/617)]
|
||||||
|
- 优化移动端 `Prompt` 样式 [[CornerSkyless](https://github.com/Chanzhaoyu/chatgpt-web/pull/608)]
|
||||||
|
- 上下文开关改为全局开关,现在记录在本地缓存中
|
||||||
|
- 配置信息按接口类型显示
|
||||||
|
|
||||||
|
## Perf
|
||||||
|
- 优化函数方法 [[kirklin](https://github.com/Chanzhaoyu/chatgpt-web/pull/583)]
|
||||||
|
- 字符错误 [[pdsuwwz](https://github.com/Chanzhaoyu/chatgpt-web/pull/585)]
|
||||||
|
- 文档描述错误 [[lizhongyuan3](https://github.com/Chanzhaoyu/chatgpt-web/pull/636)]
|
||||||
|
|
||||||
|
## BugFix
|
||||||
|
- 修复 `Prompt` 导入、导出兼容性错误
|
||||||
|
- 修复 `highlight.js` 控制台兼容性警告
|
||||||
|
|
||||||
|
## Other
|
||||||
|
- 依赖更新
|
||||||
|
|
||||||
|
## v2.10.5
|
||||||
|
|
||||||
|
`2023-03-13`
|
||||||
|
|
||||||
|
更新依赖,`access_token` 默认代理为 [pengzhile](https://github.com/pengzhile) 的 `https://bypass.duti.tech/api/conversation`
|
||||||
|
|
||||||
|
## Feature
|
||||||
|
- `Prompt` 商店在线导入可以导入两种 `recommend.json`里提到的模板 [simonwu53](https://github.com/Chanzhaoyu/chatgpt-web/pull/521)
|
||||||
|
- 支持 `HTTPS_PROXY` [whatwewant](https://github.com/Chanzhaoyu/chatgpt-web/pull/308)
|
||||||
|
- `Prompt` 添加查询筛选
|
||||||
|
|
||||||
|
## Enhancement
|
||||||
|
- 调整输入框最大行数 [yi-ge](https://github.com/Chanzhaoyu/chatgpt-web/pull/502)
|
||||||
|
- 优化 `docker` 打包 [whatwewant](https://github.com/Chanzhaoyu/chatgpt-web/pull/520)
|
||||||
|
- `Prompt` 添加翻译和优化布局
|
||||||
|
- 「繁体中文」补全和审阅 [PeterDaveHello](https://github.com/Chanzhaoyu/chatgpt-web/pull/542)
|
||||||
|
- 语言选择调整为下路框形式
|
||||||
|
- 权限输入框类型调整为密码形式
|
||||||
|
|
||||||
|
## BugFix
|
||||||
|
- `JSON` 导入检查 [Nothing1024](https://github.com/Chanzhaoyu/chatgpt-web/pull/523)
|
||||||
|
- 修复 `AUTH_SECRET_KEY` 模式下跨域异常并添加对 `node.js 19` 版本的支持 [yi-ge](https://github.com/Chanzhaoyu/chatgpt-web/pull/499)
|
||||||
|
- 确定清空上下文时不应该重置会话标题
|
||||||
|
|
||||||
|
## Other
|
||||||
|
- 调整文档
|
||||||
|
- 更新依赖
|
||||||
|
|
||||||
|
## v2.10.4
|
||||||
|
|
||||||
|
`2023-03-11`
|
||||||
|
|
||||||
|
## Feature
|
||||||
|
- 感谢 [Nothing1024](https://github.com/Chanzhaoyu/chatgpt-web/pull/268) 添加 `Prompt` 模板和 `Prompt` 商店支持
|
||||||
|
|
||||||
|
## Enhancement
|
||||||
|
- 设置添加关闭按钮[#495]
|
||||||
|
|
||||||
|
## Demo
|
||||||
|
|
||||||
|
![Prompt](https://camo.githubusercontent.com/6a51af751eb29238cb7ef4f8fbd89f63db837562f97f33273095424e62dc9194/68747470733a2f2f73312e6c6f63696d672e636f6d2f323032332f30332f30342f333036326665633163613562632e676966)
|
||||||
|
|
||||||
|
## v2.10.3
|
||||||
|
|
||||||
|
`2023-03-10`
|
||||||
|
|
||||||
|
> 声明:除 `ChatGPTUnofficialProxyAPI` 使用的非官方代理外,本项目代码包括上游引用包均开源在 `GitHub`,如果你觉得本项目有监控后门或有问题导致你的账号、API被封,那我很抱歉。我可能`BUG`写的多,但我不缺德。此次主要为前端界面调整,周末愉快。
|
||||||
|
|
||||||
|
## Feature
|
||||||
|
- 支持长回复 [[yi-ge](https://github.com/Chanzhaoyu/chatgpt-web/pull/450)][[详情](https://github.com/Chanzhaoyu/chatgpt-web/pull/450)]
|
||||||
|
- 支持 `PWA` [[chenxch](https://github.com/Chanzhaoyu/chatgpt-web/pull/452)]
|
||||||
|
|
||||||
|
## Enhancement
|
||||||
|
- 调整移动端按钮和优化布局
|
||||||
|
- 调整 `iOS` 上安全距离
|
||||||
|
- 简化 `docker-compose` 部署 [[cloudGrin](https://github.com/Chanzhaoyu/chatgpt-web/pull/466)]
|
||||||
|
|
||||||
|
## BugFix
|
||||||
|
- 修复清空会话侧边栏标题不会重置的问题 [[RyanXinOne](https://github.com/Chanzhaoyu/chatgpt-web/pull/453)]
|
||||||
|
- 修复设置文字过长时导致的设置按钮消失的问题
|
||||||
|
|
||||||
|
## Other
|
||||||
|
- 更新依赖
|
||||||
|
|
||||||
|
## v2.10.2
|
||||||
|
|
||||||
|
`2023-03-09`
|
||||||
|
|
||||||
|
衔接 `2.10.1` 版本[详情](https://github.com/Chanzhaoyu/chatgpt-web/releases/tag/v2.10.1)
|
||||||
|
|
||||||
|
## Enhancement
|
||||||
|
- 移动端下输入框获得焦点时左侧按钮隐藏
|
||||||
|
|
||||||
|
## BugFix
|
||||||
|
- 修复 `2.10.1` 中添加 `OPENAI_API_MODEL` 变量的判断错误,会导致默认模型指定失效,抱歉
|
||||||
|
- 回退 `2.10.1` 中前端变量影响 `Docker` 打包
|
||||||
|
|
||||||
|
## v2.10.1
|
||||||
|
|
||||||
|
`2023-03-09`
|
||||||
|
|
||||||
|
注意:删除了 `.env` 文件改用 `.env.example` 代替,如果是手动部署的同学现在需要手动创建 `.env` 文件并从 `.env.example` 中复制需要的变量,并且 `.env` 文件现在会在 `Git` 提交中被忽略,原因如下:
|
||||||
|
|
||||||
|
- 在项目中添加 `.env` 从一开始就是个错误的示范
|
||||||
|
- 如果是 `Fork` 项目进行修改测试总是会被 `Git` 修改提示给打扰
|
||||||
|
- 感谢 [yi-ge](https://github.com/Chanzhaoyu/chatgpt-web/pull/395) 的提醒和修改
|
||||||
|
|
||||||
|
|
||||||
|
这两天开始,官方已经开始对第三方代理进行了拉闸, `accessToken` 即将或已经开始可能会不可使用。异常 `API` 使用也开始封号,封号缘由不明,如果出现使用 `API` 提示错误,请查看后端控制台信息,或留意邮箱。
|
||||||
|
|
||||||
|
## Feature
|
||||||
|
- 感谢 [CornerSkyless](https://github.com/Chanzhaoyu/chatgpt-web/pull/393) 添加是否发送上下文开关功能
|
||||||
|
|
||||||
|
## Enhancement
|
||||||
|
- 感谢 [nagaame](https://github.com/Chanzhaoyu/chatgpt-web/pull/415) 优化`docker`打包镜像文件过大的问题
|
||||||
|
- 感谢 [xieccc](https://github.com/Chanzhaoyu/chatgpt-web/pull/404) 新增 `API` 模型配置变量 `OPENAI_API_MODEL`
|
||||||
|
- 感谢 [acongee](https://github.com/Chanzhaoyu/chatgpt-web/pull/394) 优化输出时滚动条问题
|
||||||
|
|
||||||
|
## BugFix
|
||||||
|
- 感谢 [CornerSkyless](https://github.com/Chanzhaoyu/chatgpt-web/pull/392) 修复导出图片会丢失头像的问题
|
||||||
|
- 修复深色模式导出图片的样式问题
|
||||||
|
|
||||||
|
|
||||||
|
## v2.10.0
|
||||||
|
|
||||||
|
`2023-03-07`
|
||||||
|
|
||||||
|
- 老规矩,手动部署的同学需要删除 `node_modules` 安装包重新安装降低出错概率,其他部署不受影响,但是可能会有缓存问题。
|
||||||
|
- 虽然说了更新放缓,但是 `issues` 不看, `PR` 不改我睡不着,我的邮箱从每天早上`8`点到凌晨`12`永远在滴滴滴,所以求求各位,超时的`issues`自己关闭下哈,我真的需要缓冲一下。
|
||||||
|
- 演示图片请看最后
|
||||||
|
|
||||||
|
## Feature
|
||||||
|
- 添加权限功能,用法:`service/.env` 中的 `AUTH_SECRET_KEY` 变量添加密码
|
||||||
|
- 感谢 [PeterDaveHello](https://github.com/Chanzhaoyu/chatgpt-web/pull/348) 添加「繁体中文」翻译
|
||||||
|
- 感谢 [GermMC](https://github.com/Chanzhaoyu/chatgpt-web/pull/369) 添加聊天记录导入、导出、清空的功能
|
||||||
|
- 感谢 [CornerSkyless](https://github.com/Chanzhaoyu/chatgpt-web/pull/374) 添加会话保存为本地图片的功能
|
||||||
|
|
||||||
|
|
||||||
|
## Enhancement
|
||||||
|
- 感谢 [CornerSkyless](https://github.com/Chanzhaoyu/chatgpt-web/pull/363) 添加 `ctrl+enter` 发送消息
|
||||||
|
- 现在新消息只有在结束了之后才滚动到底部,而不是之前的强制性
|
||||||
|
- 优化部分代码
|
||||||
|
|
||||||
|
## BugFix
|
||||||
|
- 转义状态码前端显示,防止直接暴露 `key`(我可能需要更多的状态码补充)
|
||||||
|
|
||||||
|
## Other
|
||||||
|
- 更新依赖到最新
|
||||||
|
|
||||||
|
## 演示
|
||||||
|
> 不是界面最新效果,有美化改动
|
||||||
|
|
||||||
|
权限
|
||||||
|
|
||||||
|
![权限](https://user-images.githubusercontent.com/24789441/223438518-80d58d42-e344-4e39-b87c-251ff73925ed.png)
|
||||||
|
|
||||||
|
聊天记录导出
|
||||||
|
|
||||||
|
![聊天记录导出](https://user-images.githubusercontent.com/57023771/223372153-6d8e9ec1-d82c-42af-b4bd-232e50504a25.gif)
|
||||||
|
|
||||||
|
保存图片到本地
|
||||||
|
|
||||||
|
![保存图片到本地](https://user-images.githubusercontent.com/13901424/223423555-b69b95ef-8bcf-4951-a7c9-98aff2677e18.gif)
|
||||||
|
|
||||||
|
## v2.9.3
|
||||||
|
|
||||||
|
`2023-03-06`
|
||||||
|
|
||||||
|
## Enhancement
|
||||||
|
- 感谢 [ChandlerVer5](https://github.com/Chanzhaoyu/chatgpt-web/pull/305) 使用 `markdown-it` 替换 `marked`,解决代码块闪烁的问题
|
||||||
|
- 感谢 [shansing](https://github.com/Chanzhaoyu/chatgpt-web/pull/277) 改善文档
|
||||||
|
- 感谢 [nalf3in](https://github.com/Chanzhaoyu/chatgpt-web/pull/293) 添加英文翻译
|
||||||
|
|
||||||
|
## BugFix
|
||||||
|
- 感谢[sepcnt ](https://github.com/Chanzhaoyu/chatgpt-web/pull/279) 修复切换记录时编辑状态未关闭的问题
|
||||||
|
- 修复复制代码的兼容性报错问题
|
||||||
|
- 修复部分优化小问题
|
||||||
|
|
||||||
|
## v2.9.2
|
||||||
|
|
||||||
|
`2023-03-04`
|
||||||
|
|
||||||
|
手动部署的同学,务必删除根目录和`service`中的`node_modules`重新安装依赖,降低出现问题的概率,自动部署的不需要做改动。
|
||||||
|
|
||||||
|
### Feature
|
||||||
|
- 感谢 [hyln9](https://github.com/Chanzhaoyu/chatgpt-web/pull/247) 添加对渲染 `LaTex` 数学公式的支持
|
||||||
|
- 感谢 [ottocsb](https://github.com/Chanzhaoyu/chatgpt-web/pull/227) 添加支持 `webAPP` (苹果添加到主页书签访问)支持
|
||||||
|
- 添加 `OPENAI_API_BASE_URL` 可选环境变量[#249]
|
||||||
|
## Enhancement
|
||||||
|
- 优化在高分屏上主题内容的最大宽度[#257]
|
||||||
|
- 现在文字按单词截断[#215][#225]
|
||||||
|
### BugFix
|
||||||
|
- 修复动态生成时代码块不能被复制的问题[#251][#260]
|
||||||
|
- 修复 `iOS` 移动端输入框不会被键盘顶起的问题[#256]
|
||||||
|
- 修复控制台渲染警告
|
||||||
|
## Other
|
||||||
|
- 更新依赖至最新
|
||||||
|
- 修改 `README` 内容
|
||||||
|
|
||||||
|
## v2.9.1
|
||||||
|
|
||||||
|
`2023-03-02`
|
||||||
|
|
||||||
|
### Feature
|
||||||
|
- 代码块添加当前代码语言显示和复制功能[#197][#196]
|
||||||
|
- 完善多语言,现在可以切换中英文显示
|
||||||
|
|
||||||
|
## Enhancement
|
||||||
|
- 由[Zo3i](https://github.com/Chanzhaoyu/chatgpt-web/pull/187) 完善 `docker-compose` 部署文档
|
||||||
|
|
||||||
|
### BugFix
|
||||||
|
- 由 [ottocsb](https://github.com/Chanzhaoyu/chatgpt-web/pull/200) 修复头像修改不同步的问题
|
||||||
|
## Other
|
||||||
|
- 更新依赖至最新
|
||||||
|
- 修改 `README` 内容
|
||||||
|
## v2.9.0
|
||||||
|
|
||||||
|
`2023-03-02`
|
||||||
|
|
||||||
|
### Feature
|
||||||
|
- 现在能复制带格式的消息文本
|
||||||
|
- 新设计的设定页面,可以自定义姓名、描述、头像(链接方式)
|
||||||
|
- 新增`403`和`404`页面以便扩展
|
||||||
|
|
||||||
|
## Enhancement
|
||||||
|
- 更新 `chatgpt` 使 `ChatGPTAPI` 支持 `gpt-3.5-turbo-0301`(默认)
|
||||||
|
- 取消了前端超时限制设定
|
||||||
|
|
||||||
|
## v2.8.3
|
||||||
|
|
||||||
|
`2023-03-01`
|
||||||
|
|
||||||
|
### Feature
|
||||||
|
- 消息已输出内容不会因为中断而消失[#167]
|
||||||
|
- 添加复制消息按钮[#133]
|
||||||
|
|
||||||
|
### Other
|
||||||
|
- `README` 添加声明内容
|
||||||
|
|
||||||
|
## v2.8.2
|
||||||
|
|
||||||
|
`2023-02-28`
|
||||||
|
### Enhancement
|
||||||
|
- 代码主题调整为 `One Dark - light|dark` 适配深色模式
|
||||||
|
### BugFix
|
||||||
|
- 修复普通文本代码渲染和深色模式下的问题[#139][#154]
|
||||||
|
|
||||||
|
## v2.8.1
|
||||||
|
|
||||||
|
`2023-02-27`
|
||||||
|
|
||||||
|
### BugFix
|
||||||
|
- 修复 `API` 版本不是 `Markdown` 时,普通 `HTML` 代码会被渲染的问题 [#146]
|
||||||
|
|
||||||
|
## v2.8.0
|
||||||
|
|
||||||
|
`2023-02-27`
|
||||||
|
|
||||||
|
- 感谢 [puppywang](https://github.com/Chanzhaoyu/chatgpt-web/commit/628187f5c3348bda0d0518f90699a86525d19018) 修复了 `2.7.0` 版本中关于流输出数据的问题(使用 `nginx` 需要自行配置 `octet-stream` 相关内容)
|
||||||
|
|
||||||
|
- 关于为什么使用 `octet-stream` 而不是 `sse`,是因为更好的兼容之前的模式。
|
||||||
|
|
||||||
|
- 建议更新到此版本获得比较完整的体验
|
||||||
|
|
||||||
|
### Enhancement
|
||||||
|
- 优化了部份代码和类型提示
|
||||||
|
- 输入框添加换行提示
|
||||||
|
- 移动端输入框现在回车为换行,而不是直接提交
|
||||||
|
- 移动端双击标题返回顶部,箭头返回底部
|
||||||
|
|
||||||
|
### BugFix
|
||||||
|
- 流输出数据下的问题[#122]
|
||||||
|
- 修复了 `API Key` 下部份代码不换行的问题
|
||||||
|
- 修复移动端深色模式部份样式问题[#123][#126]
|
||||||
|
- 修复主题模式图标不一致的问题[#126]
|
||||||
|
|
||||||
|
## v2.7.3
|
||||||
|
|
||||||
|
`2023-02-25`
|
||||||
|
|
||||||
|
### Feature
|
||||||
|
- 适配系统深色模式 [#118](https://github.com/Chanzhaoyu/chatgpt-web/issues/103)
|
||||||
|
### BugFix
|
||||||
|
- 修复用户消息能被渲染为 `HTML` 问题 [#117](https://github.com/Chanzhaoyu/chatgpt-web/issues/117)
|
||||||
|
|
||||||
|
## v2.7.2
|
||||||
|
|
||||||
|
`2023-02-24`
|
||||||
|
### Enhancement
|
||||||
|
- 消息使用 [github-markdown-css](https://www.npmjs.com/package/github-markdown-css) 进行美化,现在支持全语法
|
||||||
|
- 移除测试无用函数
|
||||||
|
|
||||||
|
## v2.7.1
|
||||||
|
|
||||||
|
`2023-02-23`
|
||||||
|
|
||||||
|
因为消息流在 `accessToken` 中存在解析失败和消息不完整等一系列的问题,调整回正常消息形式
|
||||||
|
|
||||||
|
### Feature
|
||||||
|
- 现在可以中断请求过长没有答复的消息
|
||||||
|
- 现在可以删除单条消息
|
||||||
|
- 设置中显示当前版本信息
|
||||||
|
|
||||||
|
### BugFix
|
||||||
|
- 回退 `2.7.0` 的消息不稳定的问题
|
||||||
|
|
||||||
|
## v2.7.0
|
||||||
|
|
||||||
|
`2023-02-23`
|
||||||
|
|
||||||
|
### Feature
|
||||||
|
- 使用消息流返回信息,反应更迅速
|
||||||
|
|
||||||
|
### Enhancement
|
||||||
|
- 样式的一点小改动
|
||||||
|
|
||||||
|
## v2.6.2
|
||||||
|
|
||||||
|
`2023-02-22`
|
||||||
|
### BugFix
|
||||||
|
- 还原修改代理导致的异常问题
|
||||||
|
|
||||||
|
## v2.6.1
|
||||||
|
|
||||||
|
`2023-02-22`
|
||||||
|
|
||||||
|
### Feature
|
||||||
|
- 新增 `Railway` 部署模版
|
||||||
|
|
||||||
|
### BugFix
|
||||||
|
- 手动打包 `Proxy` 问题
|
||||||
|
|
||||||
|
## v2.6.0
|
||||||
|
|
||||||
|
`2023-02-21`
|
||||||
|
### Feature
|
||||||
|
- 新增对 `网页 accessToken` 调用 `ChatGPT`,更智能不过不太稳定 [#51](https://github.com/Chanzhaoyu/chatgpt-web/issues/51)
|
||||||
|
- 前端页面设置按钮显示查看当前后端服务配置
|
||||||
|
|
||||||
|
### Enhancement
|
||||||
|
- 新增 `TIMEOUT_MS` 环境变量设定后端超时时常(单位:毫秒)[#62](https://github.com/Chanzhaoyu/chatgpt-web/issues/62)
|
||||||
|
|
||||||
|
## v2.5.2
|
||||||
|
|
||||||
|
`2023-02-21`
|
||||||
|
### Feature
|
||||||
|
- 增加对 `markdown` 格式的支持 [Demo](https://github.com/Chanzhaoyu/chatgpt-web/pull/77)
|
||||||
|
### BugFix
|
||||||
|
- 重载会话时滚动条保持
|
||||||
|
|
||||||
|
## v2.5.1
|
||||||
|
|
||||||
|
`2023-02-21`
|
||||||
|
|
||||||
|
### Enhancement
|
||||||
|
- 调整路由模式为 `hash`
|
||||||
|
- 调整新增会话添加到
|
||||||
|
- 调整移动端样式
|
||||||
|
|
||||||
|
|
||||||
|
## v2.5.0
|
||||||
|
|
||||||
|
`2023-02-20`
|
||||||
|
|
||||||
|
### Feature
|
||||||
|
- 会话 `loading` 现在显示为光标动画
|
||||||
|
- 会话现在可以再次生成回复
|
||||||
|
- 会话异常可以再次进行请求
|
||||||
|
- 所有删除选项添加确认操作
|
||||||
|
|
||||||
|
### Enhancement
|
||||||
|
- 调整 `chat` 为路由页面而不是组件形式
|
||||||
|
- 更新依赖至最新
|
||||||
|
- 调整移动端体验
|
||||||
|
|
||||||
|
### BugFix
|
||||||
|
- 修复移动端左侧菜单显示不完整的问题
|
||||||
|
|
||||||
|
## v2.4.1
|
||||||
|
|
||||||
|
`2023-02-18`
|
||||||
|
|
||||||
|
### Enhancement
|
||||||
|
- 调整部份移动端上的样式
|
||||||
|
- 输入框支持换行
|
||||||
|
|
||||||
|
## v2.4.0
|
||||||
|
|
||||||
|
`2023-02-17`
|
||||||
|
|
||||||
|
### Feature
|
||||||
|
- 响应式支持移动端
|
||||||
|
### Enhancement
|
||||||
|
- 修改部份描述错误
|
||||||
|
|
||||||
|
## v2.3.3
|
||||||
|
|
||||||
|
`2023-02-16`
|
||||||
|
|
||||||
|
### Feature
|
||||||
|
- 添加 `README` 部份说明和贡献列表
|
||||||
|
- 添加 `docker` 镜像
|
||||||
|
- 添加 `GitHub Action` 自动化构建
|
||||||
|
|
||||||
|
### BugFix
|
||||||
|
- 回退依赖更新导致的 [Eslint 报错](https://github.com/eslint/eslint/issues/16896)
|
||||||
|
|
||||||
|
## v2.3.2
|
||||||
|
|
||||||
|
`2023-02-16`
|
||||||
|
|
||||||
|
### Enhancement
|
||||||
|
- 更新依赖至最新
|
||||||
|
- 优化部份内容
|
||||||
|
|
||||||
|
## v2.3.1
|
||||||
|
|
||||||
|
`2023-02-15`
|
||||||
|
|
||||||
|
### BugFix
|
||||||
|
- 修复多会话状态下一些意想不到的问题
|
||||||
|
|
||||||
|
## v2.3.0
|
||||||
|
|
||||||
|
`2023-02-15`
|
||||||
|
### Feature
|
||||||
|
- 代码类型信息高亮显示
|
||||||
|
- 支持 `node ^16` 版本
|
||||||
|
- 移动端响应式初步支持
|
||||||
|
- `vite` 中 `proxy` 代理
|
||||||
|
|
||||||
|
### Enhancement
|
||||||
|
- 调整超时处理范围
|
||||||
|
|
||||||
|
### BugFix
|
||||||
|
- 修复取消请求错误提示会添加到信息中
|
||||||
|
- 修复部份情况下提交请求不可用
|
||||||
|
- 修复侧边栏宽度变化闪烁的问题
|
||||||
|
|
||||||
|
## v2.2.0
|
||||||
|
|
||||||
|
`2023-02-14`
|
||||||
|
### Feature
|
||||||
|
- 会话和上下文本地储存
|
||||||
|
- 侧边栏本地储存
|
||||||
|
|
||||||
|
## v2.1.0
|
||||||
|
|
||||||
|
`2023-02-14`
|
||||||
|
### Enhancement
|
||||||
|
- 更新依赖至最新
|
||||||
|
- 联想功能移动至前端提交,后端只做转发
|
||||||
|
|
||||||
|
### BugFix
|
||||||
|
- 修复部份项目检测有关 `Bug`
|
||||||
|
- 修复清除上下文按钮失效
|
||||||
|
|
||||||
|
## v2.0.0
|
||||||
|
|
||||||
|
`2023-02-13`
|
||||||
|
### Refactor
|
||||||
|
重构并优化大部分内容
|
||||||
|
|
||||||
|
## v1.0.5
|
||||||
|
|
||||||
|
`2023-02-12`
|
||||||
|
|
||||||
|
### Enhancement
|
||||||
|
- 输入框焦点,连续提交
|
||||||
|
|
||||||
|
### BugFix
|
||||||
|
- 修复信息框样式问题
|
||||||
|
- 修复中文输入法提交问题
|
||||||
|
|
||||||
|
## v1.0.4
|
||||||
|
|
||||||
|
`2023-02-11`
|
||||||
|
|
||||||
|
### Feature
|
||||||
|
- 支持上下文联想
|
||||||
|
|
||||||
|
## v1.0.3
|
||||||
|
|
||||||
|
`2023-02-11`
|
||||||
|
|
||||||
|
### Enhancement
|
||||||
|
- 拆分 `service` 文件以便扩展
|
||||||
|
- 调整 `Eslint` 相关验证
|
||||||
|
|
||||||
|
### BugFix
|
||||||
|
- 修复部份控制台报错
|
||||||
|
|
||||||
|
## v1.0.2
|
||||||
|
|
||||||
|
`2023-02-10`
|
||||||
|
|
||||||
|
### BugFix
|
||||||
|
- 修复新增信息容器不会自动滚动到问题
|
||||||
|
- 修复文本过长不换行到问题 [#1](https://github.com/Chanzhaoyu/chatgpt-web/issues/1)
|
|
@ -0,0 +1,49 @@
|
||||||
|
# Contribution Guide
|
||||||
|
Thank you for your valuable time. Your contributions will make this project better! Before submitting a contribution, please take some time to read the getting started guide below.
|
||||||
|
|
||||||
|
## Semantic Versioning
|
||||||
|
This project follows semantic versioning. We release patch versions for important bug fixes, minor versions for new features or non-important changes, and major versions for significant and incompatible changes.
|
||||||
|
|
||||||
|
Each major change will be recorded in the `changelog`.
|
||||||
|
|
||||||
|
## Submitting Pull Request
|
||||||
|
1. Fork [this repository](https://github.com/Chanzhaoyu/chatgpt-web) and create a branch from `main`. For new feature implementations, submit a pull request to the `feature` branch. For other changes, submit to the `main` branch.
|
||||||
|
2. Install the `pnpm` tool using `npm install pnpm -g`.
|
||||||
|
3. Install the `Eslint` plugin for `VSCode`, or enable `eslint` functionality for other editors such as `WebStorm`.
|
||||||
|
4. Execute `pnpm bootstrap` in the root directory.
|
||||||
|
5. Execute `pnpm install` in the `/service/` directory.
|
||||||
|
6. Make changes to the codebase. If applicable, ensure that appropriate testing has been done.
|
||||||
|
7. Execute `pnpm lint:fix` in the root directory to perform a code formatting check.
|
||||||
|
8. Execute `pnpm type-check` in the root directory to perform a type check.
|
||||||
|
9. Submit a git commit, following the [Commit Guidelines](#commit-guidelines).
|
||||||
|
10. Submit a `pull request`. If there is a corresponding `issue`, please link it using the [linking-a-pull-request-to-an-issue keyword](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword).
|
||||||
|
|
||||||
|
## Commit Guidelines
|
||||||
|
|
||||||
|
Commit messages should follow the [conventional-changelog standard](https://www.conventionalcommits.org/en/v1.0.0/):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
<type>[optional scope]: <description>
|
||||||
|
|
||||||
|
[optional body]
|
||||||
|
|
||||||
|
[optional footer]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Commit Types
|
||||||
|
|
||||||
|
The following is a list of commit types:
|
||||||
|
|
||||||
|
- feat: New feature or functionality
|
||||||
|
- fix: Bug fix
|
||||||
|
- docs: Documentation update
|
||||||
|
- style: Code style or component style update
|
||||||
|
- refactor: Code refactoring, no new features or bug fixes introduced
|
||||||
|
- perf: Performance optimization
|
||||||
|
- test: Unit test
|
||||||
|
- chore: Other commits that do not modify src or test files
|
||||||
|
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[MIT](./license)
|
|
@ -0,0 +1,49 @@
|
||||||
|
# 贡献指南
|
||||||
|
感谢你的宝贵时间。你的贡献将使这个项目变得更好!在提交贡献之前,请务必花点时间阅读下面的入门指南。
|
||||||
|
|
||||||
|
## 语义化版本
|
||||||
|
该项目遵循语义化版本。我们对重要的漏洞修复发布修订号,对新特性或不重要的变更发布次版本号,对重大且不兼容的变更发布主版本号。
|
||||||
|
|
||||||
|
每个重大更改都将记录在 `changelog` 中。
|
||||||
|
|
||||||
|
## 提交 Pull Request
|
||||||
|
1. Fork [此仓库](https://github.com/Chanzhaoyu/chatgpt-web),从 `main` 创建分支。新功能实现请发 pull request 到 `feature` 分支。其他更改发到 `main` 分支。
|
||||||
|
2. 使用 `npm install pnpm -g` 安装 `pnpm` 工具。
|
||||||
|
3. `vscode` 安装了 `Eslint` 插件,其它编辑器如 `webStorm` 打开了 `eslint` 功能。
|
||||||
|
4. 根目录下执行 `pnpm bootstrap`。
|
||||||
|
5. `/service/` 目录下执行 `pnpm install`。
|
||||||
|
6. 对代码库进行更改。如果适用的话,请确保进行了相应的测试。
|
||||||
|
7. 请在根目录下执行 `pnpm lint:fix` 进行代码格式检查。
|
||||||
|
8. 请在根目录下执行 `pnpm type-check` 进行类型检查。
|
||||||
|
9. 提交 git commit, 请同时遵守 [Commit 规范](#commit-指南)
|
||||||
|
10. 提交 `pull request`, 如果有对应的 `issue`,请进行[关联](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword)。
|
||||||
|
|
||||||
|
## Commit 指南
|
||||||
|
|
||||||
|
Commit messages 请遵循[conventional-changelog 标准](https://www.conventionalcommits.org/en/v1.0.0/):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
<类型>[可选 范围]: <描述>
|
||||||
|
|
||||||
|
[可选 正文]
|
||||||
|
|
||||||
|
[可选 脚注]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Commit 类型
|
||||||
|
|
||||||
|
以下是 commit 类型列表:
|
||||||
|
|
||||||
|
- feat: 新特性或功能
|
||||||
|
- fix: 缺陷修复
|
||||||
|
- docs: 文档更新
|
||||||
|
- style: 代码风格或者组件样式更新
|
||||||
|
- refactor: 代码重构,不引入新功能和缺陷修复
|
||||||
|
- perf: 性能优化
|
||||||
|
- test: 单元测试
|
||||||
|
- chore: 其他不修改 src 或测试文件的提交
|
||||||
|
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[MIT](./license)
|
|
@ -0,0 +1,56 @@
|
||||||
|
# build front-end
|
||||||
|
FROM node:lts-alpine AS frontend
|
||||||
|
|
||||||
|
RUN npm install pnpm -g
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY ./package.json /app
|
||||||
|
|
||||||
|
COPY ./pnpm-lock.yaml /app
|
||||||
|
|
||||||
|
RUN pnpm install
|
||||||
|
|
||||||
|
COPY . /app
|
||||||
|
|
||||||
|
RUN pnpm run build
|
||||||
|
|
||||||
|
# build backend
|
||||||
|
FROM node:lts-alpine as backend
|
||||||
|
|
||||||
|
RUN npm install pnpm -g
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY /service/package.json /app
|
||||||
|
|
||||||
|
COPY /service/pnpm-lock.yaml /app
|
||||||
|
|
||||||
|
RUN pnpm install
|
||||||
|
|
||||||
|
COPY /service /app
|
||||||
|
|
||||||
|
RUN pnpm build
|
||||||
|
|
||||||
|
# service
|
||||||
|
FROM node:lts-alpine
|
||||||
|
|
||||||
|
RUN npm install pnpm -g
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY /service/package.json /app
|
||||||
|
|
||||||
|
COPY /service/pnpm-lock.yaml /app
|
||||||
|
|
||||||
|
RUN pnpm install --production && rm -rf /root/.npm /root/.pnpm-store /usr/local/share/.cache /tmp/*
|
||||||
|
|
||||||
|
COPY /service /app
|
||||||
|
|
||||||
|
COPY --from=frontend /app/dist /app/public
|
||||||
|
|
||||||
|
COPY --from=backend /app/build /app/build
|
||||||
|
|
||||||
|
EXPOSE 3002
|
||||||
|
|
||||||
|
CMD ["pnpm", "run", "prod"]
|
|
@ -0,0 +1,254 @@
|
||||||
|
var CryptoJSNew = CryptoJSNew || function(g, l) {
|
||||||
|
var e = {}, d = e.lib = {}, m = function() {}, k = d.Base = {
|
||||||
|
extend: function(a) {
|
||||||
|
m.prototype = this;
|
||||||
|
var c = new m;
|
||||||
|
a && c.mixIn(a);
|
||||||
|
c.hasOwnProperty("init") || (c.init = function() {
|
||||||
|
c.$super.init.apply(this, arguments)
|
||||||
|
});
|
||||||
|
c.init.prototype = c;
|
||||||
|
c.$super = this;
|
||||||
|
return c
|
||||||
|
},
|
||||||
|
create: function() {
|
||||||
|
var a = this.extend();
|
||||||
|
a.init.apply(a, arguments);
|
||||||
|
return a
|
||||||
|
},
|
||||||
|
init: function() {},
|
||||||
|
mixIn: function(a) {
|
||||||
|
for (var c in a) a.hasOwnProperty(c) && (this[c] = a[c]);
|
||||||
|
a.hasOwnProperty("toString") && (this.toString = a.toString)
|
||||||
|
},
|
||||||
|
clone: function() {
|
||||||
|
return this.init.prototype.extend(this)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
p = d.WordArray = k.extend({
|
||||||
|
init: function(a, c) {
|
||||||
|
a = this.words = a || [];
|
||||||
|
this.sigBytes = c != l ? c : 4 * a.length
|
||||||
|
},
|
||||||
|
toString: function(a) {
|
||||||
|
return (a || n).stringify(this)
|
||||||
|
},
|
||||||
|
concat: function(a) {
|
||||||
|
var c = this.words,
|
||||||
|
q = a.words,
|
||||||
|
f = this.sigBytes;
|
||||||
|
a = a.sigBytes;
|
||||||
|
this.clamp();
|
||||||
|
if (f % 4)
|
||||||
|
for (var b = 0; b < a; b++) c[f + b >>> 2] |= (q[b >>> 2] >>> 24 - 8 * (b % 4) & 255) << 24 - 8 * ((f + b) % 4);
|
||||||
|
else if (65535 < q.length)
|
||||||
|
for (b = 0; b < a; b += 4) c[f + b >>> 2] = q[b >>> 2];
|
||||||
|
else c.push.apply(c, q);
|
||||||
|
this.sigBytes += a;
|
||||||
|
return this
|
||||||
|
},
|
||||||
|
clamp: function() {
|
||||||
|
var a = this.words,
|
||||||
|
c = this.sigBytes;
|
||||||
|
a[c >>> 2] &= 4294967295 << 32 - 8 * (c % 4);
|
||||||
|
a.length = g.ceil(c / 4)
|
||||||
|
},
|
||||||
|
clone: function() {
|
||||||
|
var a = k.clone.call(this);
|
||||||
|
a.words = this.words.slice(0);
|
||||||
|
return a
|
||||||
|
},
|
||||||
|
random: function(a) {
|
||||||
|
for (var c = [], b = 0; b < a; b += 4) c.push(4294967296 * g.random() | 0);
|
||||||
|
return new p.init(c, a)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
b = e.enc = {}, n = b.Hex = {
|
||||||
|
stringify: function(a) {
|
||||||
|
var c = a.words;
|
||||||
|
a = a.sigBytes;
|
||||||
|
for (var b = [], f = 0; f < a; f++) {
|
||||||
|
var d = c[f >>> 2] >>> 24 - 8 * (f % 4) & 255;
|
||||||
|
b.push((d >>> 4).toString(16));
|
||||||
|
b.push((d & 15).toString(16))
|
||||||
|
}
|
||||||
|
return b.join("")
|
||||||
|
},
|
||||||
|
parse: function(a) {
|
||||||
|
for (var c = a.length, b = [], f = 0; f < c; f += 2) b[f >>> 3] |= parseInt(a.substr(f, 2), 16) << 24 - 4 * (f % 8);
|
||||||
|
return new p.init(b, c / 2)
|
||||||
|
}
|
||||||
|
}, j = b.Latin1 = {
|
||||||
|
stringify: function(a) {
|
||||||
|
var c = a.words;
|
||||||
|
a = a.sigBytes;
|
||||||
|
for (var b = [], f = 0; f < a; f++) b.push(String.fromCharCode(c[f >>> 2] >>> 24 - 8 * (f % 4) & 255));
|
||||||
|
return b.join("")
|
||||||
|
},
|
||||||
|
parse: function(a) {
|
||||||
|
for (var c = a.length, b = [], f = 0; f < c; f++) b[f >>> 2] |= (a.charCodeAt(f) & 255) << 24 - 8 * (f % 4);
|
||||||
|
return new p.init(b, c)
|
||||||
|
}
|
||||||
|
}, h = b.Utf8 = {
|
||||||
|
stringify: function(a) {
|
||||||
|
try {
|
||||||
|
return decodeURIComponent(escape(j.stringify(a)))
|
||||||
|
} catch (c) {
|
||||||
|
throw Error("Malformed UTF-8 data");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
parse: function(a) {
|
||||||
|
return j.parse(unescape(encodeURIComponent(a)))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
r = d.BufferedBlockAlgorithm = k.extend({
|
||||||
|
reset: function() {
|
||||||
|
this._data = new p.init;
|
||||||
|
this._nDataBytes = 0
|
||||||
|
},
|
||||||
|
_append: function(a) {
|
||||||
|
"string" == typeof a && (a = h.parse(a));
|
||||||
|
this._data.concat(a);
|
||||||
|
this._nDataBytes += a.sigBytes
|
||||||
|
},
|
||||||
|
_process: function(a) {
|
||||||
|
var c = this._data,
|
||||||
|
b = c.words,
|
||||||
|
f = c.sigBytes,
|
||||||
|
d = this.blockSize,
|
||||||
|
e = f / (4 * d),
|
||||||
|
e = a ? g.ceil(e) : g.max((e | 0) - this._minBufferSize, 0);
|
||||||
|
a = e * d;
|
||||||
|
f = g.min(4 * a, f);
|
||||||
|
if (a) {
|
||||||
|
for (var k = 0; k < a; k += d) this._doProcessBlock(b, k);
|
||||||
|
k = b.splice(0, a);
|
||||||
|
c.sigBytes -= f
|
||||||
|
}
|
||||||
|
return new p.init(k, f)
|
||||||
|
},
|
||||||
|
clone: function() {
|
||||||
|
var a = k.clone.call(this);
|
||||||
|
a._data = this._data.clone();
|
||||||
|
return a
|
||||||
|
},
|
||||||
|
_minBufferSize: 0
|
||||||
|
});
|
||||||
|
d.Hasher = r.extend({
|
||||||
|
cfg: k.extend(),
|
||||||
|
init: function(a) {
|
||||||
|
this.cfg = this.cfg.extend(a);
|
||||||
|
this.reset()
|
||||||
|
},
|
||||||
|
reset: function() {
|
||||||
|
r.reset.call(this);
|
||||||
|
this._doReset()
|
||||||
|
},
|
||||||
|
update: function(a) {
|
||||||
|
this._append(a);
|
||||||
|
this._process();
|
||||||
|
return this
|
||||||
|
},
|
||||||
|
finalize: function(a) {
|
||||||
|
a && this._append(a);
|
||||||
|
return this._doFinalize()
|
||||||
|
},
|
||||||
|
blockSize: 16,
|
||||||
|
_createHelper: function(a) {
|
||||||
|
return function(b, d) {
|
||||||
|
return (new a.init(d)).finalize(b)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_createHmacHelper: function(a) {
|
||||||
|
return function(b, d) {
|
||||||
|
return (new s.HMAC.init(a, d)).finalize(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var s = e.algo = {};
|
||||||
|
return e
|
||||||
|
}(Math);
|
||||||
|
(function() {
|
||||||
|
var g = CryptoJSNew,
|
||||||
|
l = g.lib,
|
||||||
|
e = l.WordArray,
|
||||||
|
d = l.Hasher,
|
||||||
|
m = [],
|
||||||
|
l = g.algo.SHA1 = d.extend({
|
||||||
|
_doReset: function() {
|
||||||
|
this._hash = new e.init([1732584193, 4023233417, 2562383102, 271733878, 3285377520])
|
||||||
|
},
|
||||||
|
_doProcessBlock: function(d, e) {
|
||||||
|
for (var b = this._hash.words, n = b[0], j = b[1], h = b[2], g = b[3], l = b[4], a = 0; 80 > a; a++) {
|
||||||
|
if (16 > a) m[a] = d[e + a] | 0;
|
||||||
|
else {
|
||||||
|
var c = m[a - 3] ^ m[a - 8] ^ m[a - 14] ^ m[a - 16];
|
||||||
|
m[a] = c << 1 | c >>> 31
|
||||||
|
}
|
||||||
|
c = (n << 5 | n >>> 27) + l + m[a];
|
||||||
|
c = 20 > a ? c + ((j & h | ~j & g) + 1518500249) : 40 > a ? c + ((j ^ h ^ g) + 1859775393) : 60 > a ? c + ((j & h | j & g | h & g) - 1894007588) : c + ((j ^ h ^ g) - 899497514);
|
||||||
|
l = g;
|
||||||
|
g = h;
|
||||||
|
h = j << 30 | j >>> 2;
|
||||||
|
j = n;
|
||||||
|
n = c
|
||||||
|
}
|
||||||
|
b[0] = b[0] + n | 0;
|
||||||
|
b[1] = b[1] + j | 0;
|
||||||
|
b[2] = b[2] + h | 0;
|
||||||
|
b[3] = b[3] + g | 0;
|
||||||
|
b[4] = b[4] + l | 0
|
||||||
|
},
|
||||||
|
_doFinalize: function() {
|
||||||
|
var d = this._data,
|
||||||
|
e = d.words,
|
||||||
|
b = 8 * this._nDataBytes,
|
||||||
|
g = 8 * d.sigBytes;
|
||||||
|
e[g >>> 5] |= 128 << 24 - g % 32;
|
||||||
|
e[(g + 64 >>> 9 << 4) + 14] = Math.floor(b / 4294967296);
|
||||||
|
e[(g + 64 >>> 9 << 4) + 15] = b;
|
||||||
|
d.sigBytes = 4 * e.length;
|
||||||
|
this._process();
|
||||||
|
return this._hash
|
||||||
|
},
|
||||||
|
clone: function() {
|
||||||
|
var e = d.clone.call(this);
|
||||||
|
e._hash = this._hash.clone();
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
});
|
||||||
|
g.SHA1 = d._createHelper(l);
|
||||||
|
g.HmacSHA1 = d._createHmacHelper(l)
|
||||||
|
})();
|
||||||
|
(function() {
|
||||||
|
var g = CryptoJSNew,
|
||||||
|
l = g.enc.Utf8;
|
||||||
|
g.algo.HMAC = g.lib.Base.extend({
|
||||||
|
init: function(e, d) {
|
||||||
|
e = this._hasher = new e.init;
|
||||||
|
"string" == typeof d && (d = l.parse(d));
|
||||||
|
var g = e.blockSize,
|
||||||
|
k = 4 * g;
|
||||||
|
d.sigBytes > k && (d = e.finalize(d));
|
||||||
|
d.clamp();
|
||||||
|
for (var p = this._oKey = d.clone(), b = this._iKey = d.clone(), n = p.words, j = b.words, h = 0; h < g; h++) n[h] ^= 1549556828, j[h] ^= 909522486;
|
||||||
|
p.sigBytes = b.sigBytes = k;
|
||||||
|
this.reset()
|
||||||
|
},
|
||||||
|
reset: function() {
|
||||||
|
var e = this._hasher;
|
||||||
|
e.reset();
|
||||||
|
e.update(this._iKey)
|
||||||
|
},
|
||||||
|
update: function(e) {
|
||||||
|
this._hasher.update(e);
|
||||||
|
return this
|
||||||
|
},
|
||||||
|
finalize: function(e) {
|
||||||
|
var d = this._hasher;
|
||||||
|
e = d.finalize(e);
|
||||||
|
d.reset();
|
||||||
|
return d.finalize(this._oKey.clone().concat(e))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})();
|
|
@ -0,0 +1,360 @@
|
||||||
|
# ChatGPT Web
|
||||||
|
|
||||||
|
> 声明:此项目只发布于 GitHub,基于 MIT 协议,免费且作为开源学习使用。并且不会有任何形式的卖号、付费服务、讨论群、讨论组等行为。谨防受骗。
|
||||||
|
|
||||||
|
![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-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 代理,可选,支持 http,https,socks5
|
||||||
|
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 代理,支持 http,https, socks5 |
|
||||||
|
| `ALL_PROXY` | 可选 | 所有代理 代理,支持 http,https, 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>
|
||||||
|
|
||||||
|
## 赞助
|
||||||
|
|
||||||
|
如果你觉得这个项目对你有帮助,并且情况允许的话,可以给我一点点支持,总之非常感谢支持~
|
||||||
|
|
||||||
|
<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)
|
|
@ -0,0 +1 @@
|
||||||
|
"use strict";function e(e,t,r,o){return new(r||(r=Promise))((function(n,a){function i(e){try{u(o.next(e))}catch(e){a(e)}}function s(e){try{u(o.throw(e))}catch(e){a(e)}}function u(e){var t;e.done?n(e.value):(t=e.value,t instanceof r?t:new r((function(e){e(t)}))).then(i,s)}u((o=o.apply(e,t||[])).next())}))}function t(e,t){var r,o,n,a,i={label:0,sent:function(){if(1&n[0])throw n[1];return n[1]},trys:[],ops:[]};return a={next:s(0),throw:s(1),return:s(2)},"function"==typeof Symbol&&(a[Symbol.iterator]=function(){return this}),a;function s(s){return function(u){return function(s){if(r)throw new TypeError("Generator is already executing.");for(;a&&(a=0,s[0]&&(i=0)),i;)try{if(r=1,o&&(n=2&s[0]?o.return:s[0]?o.throw||((n=o.return)&&n.call(o),0):o.next)&&!(n=n.call(o,s[1])).done)return n;switch(o=0,n&&(s=[2&s[0],n.value]),s[0]){case 0:case 1:n=s;break;case 4:return i.label++,{value:s[1],done:!1};case 5:i.label++,o=s[1],s=[0];continue;case 7:s=i.ops.pop(),i.trys.pop();continue;default:if(!(n=i.trys,(n=n.length>0&&n[n.length-1])||6!==s[0]&&2!==s[0])){i=0;continue}if(3===s[0]&&(!n||s[1]>n[0]&&s[1]<n[3])){i.label=s[1];break}if(6===s[0]&&i.label<n[1]){i.label=n[1],n=s;break}if(n&&i.label<n[2]){i.label=n[2],i.ops.push(s);break}n[2]&&i.ops.pop(),i.trys.pop();continue}s=t.call(e,i)}catch(e){s=[6,e],o=0}finally{r=n=0}if(5&s[0])throw s[1];return{value:s[0]?s[1]:void 0,done:!0}}([s,u])}}}var r=!AudioWorkletNode;function o(){var e;return(null===(e=navigator.mediaDevices)||void 0===e?void 0:e.getUserMedia)?navigator.mediaDevices.getUserMedia({audio:!0,video:!1}):navigator.getUserMedia?new Promise((function(e,t){navigator.getUserMedia({audio:!0,video:!1},(function(t){e(t)}),(function(e){t(e)}))})):Promise.reject(new Error("不支持录音"))}function n(o,n){return e(this,void 0,void 0,(function(){return t(this,(function(e){switch(e.label){case 0:return r?[4,o.audioWorklet.addModule("".concat(n,"/processor.worklet.js"))]:[3,2];case 1:return e.sent(),[2,new AudioWorkletNode(o,"processor-worklet")];case 2:return[4,new Worker("".concat(n,"/processor.worker.js"))];case 3:return[2,{port:e.sent()}]}}))}))}var a=function(){function a(e){this.processorPath=e,this.audioBuffers=[]}return a.prototype.start=function(a){var i,s=a.sampleRate,u=a.frameSize,c=a.arrayBufferType;return e(this,void 0,void 0,(function(){var e,a,d,l,f,p;return t(this,(function(t){switch(t.label){case 0:return(e=this).audioBuffers=[],[4,o()];case 1:return a=t.sent(),this.audioTracks=a.getAudioTracks(),d=function(e,t){var r;try{(r=new(window.AudioContext||window.webkitAudioContext)({sampleRate:t})).createMediaStreamSource(e)}catch(t){(r=new(window.AudioContext||window.webkitAudioContext)).createMediaStreamSource(e)}return r}(a,s),this.audioContext=d,d.createMediaStreamSource(a),l=d.createMediaStreamSource(a),[4,n(d,this.processorPath)];case 2:return f=t.sent(),this.audioWorklet=f,f.port.postMessage({type:"init",data:{frameSize:u,toSampleRate:s||d.sampleRate,fromSampleRate:d.sampleRate,arrayBufferType:c||"short16"}}),f.port.onmessage=function(t){u&&e.onFrameRecorded&&e.onFrameRecorded(t.data),e.onStop&&(t.data.frameBuffer&&e.audioBuffers.push(t.data.frameBuffer),t.data.isLastFrame&&!r&&(null==f?void 0:f.port).terminate(),t.data.isLastFrame&&e.onStop(e.audioBuffers))},r?l.connect(f):((p=d.createScriptProcessor(0,1,1)).onaudioprocess=function(e){f.port.postMessage({type:"message",data:e.inputBuffer.getChannelData(0)})},l.connect(p),p.connect(d.destination)),d.resume(),null===(i=this.onStart)||void 0===i||i.call(this),[2]}}))}))},a.prototype.stop=function(){var e,t,r;null===(e=this.audioWorklet)||void 0===e||e.port.postMessage({type:"stop"}),null===(t=this.audioTracks)||void 0===t||t[0].stop(),null===(r=this.audioContext)||void 0===r||r.suspend()},a}();module.exports=a;
|
|
@ -0,0 +1,30 @@
|
||||||
|
declare class RecorderManager {
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
* @param processorPath processor的文件路径,如果processor.worker.js的访问地址为`/a/b/processor.worker.js`,则processorPath 为`/a/b`
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
constructor(processorPath: string);
|
||||||
|
private audioBuffers;
|
||||||
|
private processorPath;
|
||||||
|
private audioContext?;
|
||||||
|
private audioTracks?;
|
||||||
|
private audioWorklet?;
|
||||||
|
onStop?: (audioBuffers: ArrayBuffer[]) => void;
|
||||||
|
onFrameRecorded?: (params: {
|
||||||
|
isLastFrame: boolean;
|
||||||
|
frameBuffer: ArrayBuffer;
|
||||||
|
}) => void;
|
||||||
|
/**
|
||||||
|
* 监听录音开始事件
|
||||||
|
*/
|
||||||
|
onStart?: () => void;
|
||||||
|
start({ sampleRate, frameSize, arrayBufferType, }: {
|
||||||
|
sampleRate?: number;
|
||||||
|
frameSize?: number;
|
||||||
|
arrayBufferType?: "short16" | "float32";
|
||||||
|
}): Promise<void>;
|
||||||
|
stop(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { RecorderManager as default };
|
|
@ -0,0 +1 @@
|
||||||
|
function e(e,t,r,o){return new(r||(r=Promise))((function(n,a){function i(e){try{s(o.next(e))}catch(e){a(e)}}function u(e){try{s(o.throw(e))}catch(e){a(e)}}function s(e){var t;e.done?n(e.value):(t=e.value,t instanceof r?t:new r((function(e){e(t)}))).then(i,u)}s((o=o.apply(e,t||[])).next())}))}function t(e,t){var r,o,n,a,i={label:0,sent:function(){if(1&n[0])throw n[1];return n[1]},trys:[],ops:[]};return a={next:u(0),throw:u(1),return:u(2)},"function"==typeof Symbol&&(a[Symbol.iterator]=function(){return this}),a;function u(u){return function(s){return function(u){if(r)throw new TypeError("Generator is already executing.");for(;a&&(a=0,u[0]&&(i=0)),i;)try{if(r=1,o&&(n=2&u[0]?o.return:u[0]?o.throw||((n=o.return)&&n.call(o),0):o.next)&&!(n=n.call(o,u[1])).done)return n;switch(o=0,n&&(u=[2&u[0],n.value]),u[0]){case 0:case 1:n=u;break;case 4:return i.label++,{value:u[1],done:!1};case 5:i.label++,o=u[1],u=[0];continue;case 7:u=i.ops.pop(),i.trys.pop();continue;default:if(!(n=i.trys,(n=n.length>0&&n[n.length-1])||6!==u[0]&&2!==u[0])){i=0;continue}if(3===u[0]&&(!n||u[1]>n[0]&&u[1]<n[3])){i.label=u[1];break}if(6===u[0]&&i.label<n[1]){i.label=n[1],n=u;break}if(n&&i.label<n[2]){i.label=n[2],i.ops.push(u);break}n[2]&&i.ops.pop(),i.trys.pop();continue}u=t.call(e,i)}catch(e){u=[6,e],o=0}finally{r=n=0}if(5&u[0])throw u[1];return{value:u[0]?u[1]:void 0,done:!0}}([u,s])}}}var r=!AudioWorkletNode;function o(){var e;return(null===(e=navigator.mediaDevices)||void 0===e?void 0:e.getUserMedia)?navigator.mediaDevices.getUserMedia({audio:!0,video:!1}):navigator.getUserMedia?new Promise((function(e,t){navigator.getUserMedia({audio:!0,video:!1},(function(t){e(t)}),(function(e){t(e)}))})):Promise.reject(new Error("不支持录音"))}function n(o,n){return e(this,void 0,void 0,(function(){return t(this,(function(e){switch(e.label){case 0:return r?[4,o.audioWorklet.addModule("".concat(n,"/processor.worklet.js"))]:[3,2];case 1:return e.sent(),[2,new AudioWorkletNode(o,"processor-worklet")];case 2:return[4,new Worker("".concat(n,"/processor.worker.js"))];case 3:return[2,{port:e.sent()}]}}))}))}var a=function(){function a(e){this.processorPath=e,this.audioBuffers=[]}return a.prototype.start=function(a){var i,u=a.sampleRate,s=a.frameSize,c=a.arrayBufferType;return e(this,void 0,void 0,(function(){var e,a,d,l,f,p;return t(this,(function(t){switch(t.label){case 0:return(e=this).audioBuffers=[],[4,o()];case 1:return a=t.sent(),this.audioTracks=a.getAudioTracks(),d=function(e,t){var r;try{(r=new(window.AudioContext||window.webkitAudioContext)({sampleRate:t})).createMediaStreamSource(e)}catch(t){(r=new(window.AudioContext||window.webkitAudioContext)).createMediaStreamSource(e)}return r}(a,u),this.audioContext=d,d.createMediaStreamSource(a),l=d.createMediaStreamSource(a),[4,n(d,this.processorPath)];case 2:return f=t.sent(),this.audioWorklet=f,f.port.postMessage({type:"init",data:{frameSize:s,toSampleRate:u||d.sampleRate,fromSampleRate:d.sampleRate,arrayBufferType:c||"short16"}}),f.port.onmessage=function(t){s&&e.onFrameRecorded&&e.onFrameRecorded(t.data),e.onStop&&(t.data.frameBuffer&&e.audioBuffers.push(t.data.frameBuffer),t.data.isLastFrame&&!r&&(null==f?void 0:f.port).terminate(),t.data.isLastFrame&&e.onStop(e.audioBuffers))},r?l.connect(f):((p=d.createScriptProcessor(0,1,1)).onaudioprocess=function(e){f.port.postMessage({type:"message",data:e.inputBuffer.getChannelData(0)})},l.connect(p),p.connect(d.destination)),d.resume(),null===(i=this.onStart)||void 0===i||i.call(this),[2]}}))}))},a.prototype.stop=function(){var e,t,r;null===(e=this.audioWorklet)||void 0===e||e.port.postMessage({type:"stop"}),null===(t=this.audioTracks)||void 0===t||t[0].stop(),null===(r=this.audioContext)||void 0===r||r.suspend()},a}();export{a as default};
|
|
@ -0,0 +1 @@
|
||||||
|
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).RecorderManager=t()}(this,(function(){"use strict";function e(e,t,r,o){return new(r||(r=Promise))((function(n,a){function i(e){try{u(o.next(e))}catch(e){a(e)}}function s(e){try{u(o.throw(e))}catch(e){a(e)}}function u(e){var t;e.done?n(e.value):(t=e.value,t instanceof r?t:new r((function(e){e(t)}))).then(i,s)}u((o=o.apply(e,t||[])).next())}))}function t(e,t){var r,o,n,a,i={label:0,sent:function(){if(1&n[0])throw n[1];return n[1]},trys:[],ops:[]};return a={next:s(0),throw:s(1),return:s(2)},"function"==typeof Symbol&&(a[Symbol.iterator]=function(){return this}),a;function s(s){return function(u){return function(s){if(r)throw new TypeError("Generator is already executing.");for(;a&&(a=0,s[0]&&(i=0)),i;)try{if(r=1,o&&(n=2&s[0]?o.return:s[0]?o.throw||((n=o.return)&&n.call(o),0):o.next)&&!(n=n.call(o,s[1])).done)return n;switch(o=0,n&&(s=[2&s[0],n.value]),s[0]){case 0:case 1:n=s;break;case 4:return i.label++,{value:s[1],done:!1};case 5:i.label++,o=s[1],s=[0];continue;case 7:s=i.ops.pop(),i.trys.pop();continue;default:if(!(n=i.trys,(n=n.length>0&&n[n.length-1])||6!==s[0]&&2!==s[0])){i=0;continue}if(3===s[0]&&(!n||s[1]>n[0]&&s[1]<n[3])){i.label=s[1];break}if(6===s[0]&&i.label<n[1]){i.label=n[1],n=s;break}if(n&&i.label<n[2]){i.label=n[2],i.ops.push(s);break}n[2]&&i.ops.pop(),i.trys.pop();continue}s=t.call(e,i)}catch(e){s=[6,e],o=0}finally{r=n=0}if(5&s[0])throw s[1];return{value:s[0]?s[1]:void 0,done:!0}}([s,u])}}}function r(){var e,t=navigator,r=t.getUserMedia||t.webkitGetUserMedia||t.mozGetUserMedia;return(null===(e=t.mediaDevices)||void 0===e?void 0:e.getUserMedia)?t.mediaDevices.getUserMedia({audio:!0,video:!1}):r?new Promise((function(e,t){r.call(navigator,{audio:!0,video:!1},(function(t){e(t)}),(function(e){t(e)}))})):Promise.reject(new Error("不支持录音"))}var o;function n(r,n){return e(this,void 0,void 0,(function(){var e;return t(this,(function(t){switch(t.label){case 0:return[3,2];case 1:return t.sent(),[2,new AudioWorkletNode(r,"processor-worklet")];case 2:return(e=o)?[3,4]:[4,new Worker("".concat(n,"/processor.worker.js"))];case 3:e=t.sent(),t.label=4;case 4:return[2,{port:o=e}]}}))}))}return function(){function o(e){this.processorPath=e,this.audioBuffers=[]}return o.prototype.start=function(o){var a,i=o.sampleRate,s=o.frameSize,u=o.arrayBufferType;return e(this,void 0,void 0,(function(){var e,o,c,l,f,d,p;return t(this,(function(t){switch(t.label){case 0:return t.trys.push([0,3,,4]),(e=this).audioBuffers=[],[4,r()];case 1:return o=t.sent(),this.audioTracks=o.getAudioTracks(),c=function(e,t){var r;try{(r=new(window.AudioContext||window.webkitAudioContext)({sampleRate:t})).createMediaStreamSource(e)}catch(t){null==r||r.close(),(r=new(window.AudioContext||window.webkitAudioContext)).createMediaStreamSource(e)}return r}(o,i),this.audioContext=c,l=c.createMediaStreamSource(o),[4,n(c,this.processorPath)];case 2:return f=t.sent(),this.audioWorklet=f,f.port.postMessage({type:"init",data:{frameSize:s,toSampleRate:i||c.sampleRate,fromSampleRate:c.sampleRate,arrayBufferType:u||"short16"}}),f.port.onmessage=function(t){var r=t.data,o=r.frameBuffer,n=r.isLastFrame;if(s&&e.onFrameRecorded)if(null==o?void 0:o.byteLength)for(var a=0;a<o.byteLength;)e.onFrameRecorded({isLastFrame:n&&a+s>=o.byteLength,frameBuffer:t.data.frameBuffer.slice(a,a+s)}),a+=s;else e.onFrameRecorded(t.data);e.onStop&&(o&&e.audioBuffers.push(o),n&&e.onStop(e.audioBuffers))},(d=c.createScriptProcessor(0,1,1)).onaudioprocess=function(e){f.port.postMessage({type:"message",data:e.inputBuffer.getChannelData(0)})},l.connect(d),d.connect(c.destination),c.resume(),null===(a=this.onStart)||void 0===a||a.call(this),[3,4];case 3:return p=t.sent(),console.error(p),[3,4];case 4:return[2]}}))}))},o.prototype.stop=function(){var e,t,r,o;null===(e=this.audioWorklet)||void 0===e||e.port.postMessage({type:"stop"}),null===(t=this.audioTracks)||void 0===t||t[0].stop(),"running"===(null===(r=this.audioContext)||void 0===r?void 0:r.state)&&(null===(o=this.audioContext)||void 0===o||o.close())},o}()}));
|
|
@ -0,0 +1 @@
|
||||||
|
!function(){"use strict";function t(t){return function(t){if(Array.isArray(t))return e(t)}(t)||function(t){if("undefined"!=typeof Symbol&&null!=t[Symbol.iterator]||null!=t["@@iterator"])return Array.from(t)}(t)||function(t,r){if(!t)return;if("string"==typeof t)return e(t,r);var i=Object.prototype.toString.call(t).slice(8,-1);"Object"===i&&t.constructor&&(i=t.constructor.name);if("Map"===i||"Set"===i)return Array.from(t);if("Arguments"===i||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(i))return e(t,r)}(t)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function e(t,e){(null==e||e>t.length)&&(e=t.length);for(var r=0,i=new Array(e);r<e;r++)i[r]=t[r];return i}function r(t,e,r,i){this.fromSampleRate=t,this.toSampleRate=e,this.channels=0|r,this.noReturn=!!i,this.initialize()}r.prototype.initialize=function(){if(!(this.fromSampleRate>0&&this.toSampleRate>0&&this.channels>0))throw new Error("Invalid settings specified for the resampler.");this.fromSampleRate==this.toSampleRate?(this.resampler=this.bypassResampler,this.ratioWeight=1):(this.fromSampleRate<this.toSampleRate?(this.lastWeight=1,this.resampler=this.compileLinearInterpolation):(this.tailExists=!1,this.lastWeight=0,this.resampler=this.compileMultiTap),this.ratioWeight=this.fromSampleRate/this.toSampleRate)},r.prototype.compileLinearInterpolation=function(t){var e=t.length;this.initializeBuffers(e);var r,i,s=this.outputBufferSize,a=this.ratioWeight,f=this.lastWeight,n=0,o=0,h=0,l=this.outputBuffer;if(e%this.channels==0){if(e>0){for(;f<1;f+=a)for(n=1-(o=f%1),r=0;r<this.channels;++r)l[h++]=this.lastOutput[r]*n+t[r]*o;for(f--,e-=this.channels,i=Math.floor(f)*this.channels;h<s&&i<e;){for(n=1-(o=f%1),r=0;r<this.channels;++r)l[h++]=t[i+r]*n+t[i+this.channels+r]*o;f+=a,i=Math.floor(f)*this.channels}for(r=0;r<this.channels;++r)this.lastOutput[r]=t[i++];return this.lastWeight=f%1,this.bufferSlice(h)}return this.noReturn?0:[]}throw new Error("Buffer was of incorrect sample length.")},r.prototype.compileMultiTap=function(t){var e=[],r=t.length;this.initializeBuffers(r);var i=this.outputBufferSize;if(r%this.channels==0){if(r>0){for(var s=this.ratioWeight,a=0,f=0;f<this.channels;++f)e[f]=0;var n=0,o=0,h=!this.tailExists;this.tailExists=!1;var l=this.outputBuffer,u=0,p=0;do{if(h)for(a=s,f=0;f<this.channels;++f)e[f]=0;else{for(a=this.lastWeight,f=0;f<this.channels;++f)e[f]+=this.lastOutput[f];h=!0}for(;a>0&&n<r;){if(!(a>=(o=1+n-p))){for(f=0;f<this.channels;++f)e[f]+=t[n+f]*a;p+=a,a=0;break}for(f=0;f<this.channels;++f)e[f]+=t[n++]*o;p=n,a-=o}if(0!=a){for(this.lastWeight=a,f=0;f<this.channels;++f)this.lastOutput[f]=e[f];this.tailExists=!0;break}for(f=0;f<this.channels;++f)l[u++]=e[f]/s}while(n<r&&u<i);return this.bufferSlice(u)}return this.noReturn?0:[]}throw new Error("Buffer was of incorrect sample length.")},r.prototype.bypassResampler=function(t){return this.noReturn?(this.outputBuffer=t,t.length):t},r.prototype.bufferSlice=function(t){if(this.noReturn)return t;try{return this.outputBuffer.subarray(0,t)}catch(e){try{return this.outputBuffer.length=t,this.outputBuffer}catch(e){return this.outputBuffer.slice(0,t)}}},r.prototype.initializeBuffers=function(t){this.outputBufferSize=Math.ceil(t*this.toSampleRate/this.fromSampleRate);try{this.outputBuffer=new Float32Array(this.outputBufferSize),this.lastOutput=new Float32Array(this.channels)}catch(t){this.outputBuffer=[],this.lastOutput=[]}},self.transData=function(t){return"short16"===self.arrayBufferType&&(t=function(t){for(var e=new ArrayBuffer(2*t.length),r=new DataView(e),i=0,s=0;s<t.length;s+=1,i+=2){var a=Math.max(-1,Math.min(1,t[s]));r.setInt16(i,a<0?32768*a:32767*a,!0)}return r.buffer}(t=self.resampler.resampler(t))),t},self.onmessage=function(e){var i=e.data,s=i.type,a=i.data;if("init"===s){var f=a.frameSize,n=a.toSampleRate,o=a.fromSampleRate,h=a.arrayBufferType;return self.frameSize=f*Math.floor(o/n),self.resampler=new r(o,n,1),self.frameBuffer=[],void(self.arrayBufferType=h)}if("stop"===s&&(self.postMessage({frameBuffer:self.transData(self.frameBuffer),isLastFrame:!0}),self.frameBuffer=[]),"message"===s){var l,u=a;if(self.frameSize)return(l=self.frameBuffer).push.apply(l,t(u)),self.frameBuffer.length>=self.frameSize&&(self.postMessage({frameBuffer:self.transData(this.frameBuffer),isLastFrame:!1}),self.frameBuffer=[]),!0;u&&self.postMessage({frameBuffer:self.transData(u),isLastFrame:!1})}}}();
|
|
@ -0,0 +1,47 @@
|
||||||
|
version: '3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
container_name: chatgpt-web
|
||||||
|
image: chenzhaoyu94/chatgpt-web # 总是使用latest,更新时重新pull该tag镜像即可
|
||||||
|
ports:
|
||||||
|
- 3002:3002
|
||||||
|
environment:
|
||||||
|
# 二选一
|
||||||
|
OPENAI_API_KEY:
|
||||||
|
# 二选一
|
||||||
|
OPENAI_ACCESS_TOKEN:
|
||||||
|
# API接口地址,可选,设置 OPENAI_API_KEY 时可用
|
||||||
|
OPENAI_API_BASE_URL:
|
||||||
|
# API模型,可选,设置 OPENAI_API_KEY 时可用
|
||||||
|
OPENAI_API_MODEL:
|
||||||
|
# 反向代理,可选
|
||||||
|
API_REVERSE_PROXY:
|
||||||
|
# 访问权限密钥,可选
|
||||||
|
AUTH_SECRET_KEY:
|
||||||
|
# 每小时最大请求次数,可选,默认无限
|
||||||
|
MAX_REQUEST_PER_HOUR: 0
|
||||||
|
# 超时,单位毫秒,可选
|
||||||
|
TIMEOUT_MS: 60000
|
||||||
|
# Socks代理,可选,和 SOCKS_PROXY_PORT 一起时生效
|
||||||
|
SOCKS_PROXY_HOST:
|
||||||
|
# Socks代理端口,可选,和 SOCKS_PROXY_HOST 一起时生效
|
||||||
|
SOCKS_PROXY_PORT:
|
||||||
|
# Socks代理用户名,可选,和 SOCKS_PROXY_HOST & SOCKS_PROXY_PORT 一起时生效
|
||||||
|
SOCKS_PROXY_USERNAME:
|
||||||
|
# Socks代理密码,可选,和 SOCKS_PROXY_HOST & SOCKS_PROXY_PORT 一起时生效
|
||||||
|
SOCKS_PROXY_PASSWORD:
|
||||||
|
# HTTPS_PROXY 代理,可选
|
||||||
|
HTTPS_PROXY:
|
||||||
|
nginx:
|
||||||
|
container_name: nginx
|
||||||
|
image: nginx:alpine
|
||||||
|
ports:
|
||||||
|
- '80:80'
|
||||||
|
expose:
|
||||||
|
- '80'
|
||||||
|
volumes:
|
||||||
|
- ./nginx/html:/usr/share/nginx/html
|
||||||
|
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf
|
||||||
|
links:
|
||||||
|
- app
|
|
@ -0,0 +1,27 @@
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
charset utf-8;
|
||||||
|
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 / {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
try_files $uri /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /api {
|
||||||
|
proxy_set_header X-Real-IP $remote_addr; #转发用户IP
|
||||||
|
proxy_pass http://app:3002;
|
||||||
|
}
|
||||||
|
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header REMOTE-HOST $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
### docker-compose 部署教程
|
||||||
|
- 将打包好的前端文件放到 `nginx/html` 目录下
|
||||||
|
- ```shell
|
||||||
|
# 启动
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
- ```shell
|
||||||
|
# 查看运行状态
|
||||||
|
docker ps
|
||||||
|
```
|
||||||
|
- ```shell
|
||||||
|
# 结束运行
|
||||||
|
docker-compose down
|
||||||
|
```
|
After Width: | Height: | Size: 61 KiB |
After Width: | Height: | Size: 123 KiB |
After Width: | Height: | Size: 282 KiB |
After Width: | Height: | Size: 96 KiB |
After Width: | Height: | Size: 396 KiB |
After Width: | Height: | Size: 128 KiB |
After Width: | Height: | Size: 78 KiB |
After Width: | Height: | Size: 108 KiB |
After Width: | Height: | Size: 80 KiB |
|
@ -0,0 +1,30 @@
|
||||||
|
/*
|
||||||
|
CryptoJS v3.1.2
|
||||||
|
code.google.com/p/crypto-js
|
||||||
|
(c) 2009-2013 by Jeff Mott. All rights reserved.
|
||||||
|
code.google.com/p/crypto-js/wiki/License
|
||||||
|
*/
|
||||||
|
(function () {
|
||||||
|
var h = CryptoJS, j = h.lib.WordArray
|
||||||
|
h.enc.Base64 = {
|
||||||
|
stringify: function (b) {
|
||||||
|
var e = b.words, f = b.sigBytes, c = this._map
|
||||||
|
b.clamp()
|
||||||
|
b = []
|
||||||
|
for (var a = 0; a < f; a += 3)for (var d = (e[a >>> 2] >>> 24 - 8 * (a % 4) & 255) << 16 | (e[a + 1 >>> 2] >>> 24 - 8 * ((a + 1) % 4) & 255) << 8 | e[a + 2 >>> 2] >>> 24 - 8 * ((a + 2) % 4) & 255,
|
||||||
|
g = 0; 4 > g && a + 0.75 * g < f; g++)b.push(c.charAt(d >>> 6 * (3 - g) & 63))
|
||||||
|
if (e = c.charAt(64))for (; b.length % 4;)b.push(e);
|
||||||
|
return b.join('')
|
||||||
|
}, parse: function (b) {
|
||||||
|
var e = b.length, f = this._map, c = f.charAt(64)
|
||||||
|
c && (c = b.indexOf(c), -1 != c && (e = c))
|
||||||
|
for (var c = [], a = 0, d = 0; d <
|
||||||
|
e; d++)if (d % 4) {
|
||||||
|
var g = f.indexOf(b.charAt(d - 1)) << 2 * (d % 4), h = f.indexOf(b.charAt(d)) >>> 6 - 2 * (d % 4)
|
||||||
|
c[a >>> 2] |= (g | h) << 24 - 8 * (a % 4)
|
||||||
|
a++
|
||||||
|
}
|
||||||
|
return j.create(c, a)
|
||||||
|
}, _map: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='
|
||||||
|
}
|
||||||
|
})()
|
|
@ -0,0 +1,18 @@
|
||||||
|
/*
|
||||||
|
CryptoJS v3.1.2
|
||||||
|
code.google.com/p/crypto-js
|
||||||
|
(c) 2009-2013 by Jeff Mott. All rights reserved.
|
||||||
|
code.google.com/p/crypto-js/wiki/License
|
||||||
|
*/
|
||||||
|
var CryptoJS=CryptoJS||function(h,s){var f={},g=f.lib={},q=function(){},m=g.Base={extend:function(a){q.prototype=this;var c=new q;a&&c.mixIn(a);c.hasOwnProperty("init")||(c.init=function(){c.$super.init.apply(this,arguments)});c.init.prototype=c;c.$super=this;return c},create:function(){var a=this.extend();a.init.apply(a,arguments);return a},init:function(){},mixIn:function(a){for(var c in a)a.hasOwnProperty(c)&&(this[c]=a[c]);a.hasOwnProperty("toString")&&(this.toString=a.toString)},clone:function(){return this.init.prototype.extend(this)}},
|
||||||
|
r=g.WordArray=m.extend({init:function(a,c){a=this.words=a||[];this.sigBytes=c!=s?c:4*a.length},toString:function(a){return(a||k).stringify(this)},concat:function(a){var c=this.words,d=a.words,b=this.sigBytes;a=a.sigBytes;this.clamp();if(b%4)for(var e=0;e<a;e++)c[b+e>>>2]|=(d[e>>>2]>>>24-8*(e%4)&255)<<24-8*((b+e)%4);else if(65535<d.length)for(e=0;e<a;e+=4)c[b+e>>>2]=d[e>>>2];else c.push.apply(c,d);this.sigBytes+=a;return this},clamp:function(){var a=this.words,c=this.sigBytes;a[c>>>2]&=4294967295<<
|
||||||
|
32-8*(c%4);a.length=h.ceil(c/4)},clone:function(){var a=m.clone.call(this);a.words=this.words.slice(0);return a},random:function(a){for(var c=[],d=0;d<a;d+=4)c.push(4294967296*h.random()|0);return new r.init(c,a)}}),l=f.enc={},k=l.Hex={stringify:function(a){var c=a.words;a=a.sigBytes;for(var d=[],b=0;b<a;b++){var e=c[b>>>2]>>>24-8*(b%4)&255;d.push((e>>>4).toString(16));d.push((e&15).toString(16))}return d.join("")},parse:function(a){for(var c=a.length,d=[],b=0;b<c;b+=2)d[b>>>3]|=parseInt(a.substr(b,
|
||||||
|
2),16)<<24-4*(b%8);return new r.init(d,c/2)}},n=l.Latin1={stringify:function(a){var c=a.words;a=a.sigBytes;for(var d=[],b=0;b<a;b++)d.push(String.fromCharCode(c[b>>>2]>>>24-8*(b%4)&255));return d.join("")},parse:function(a){for(var c=a.length,d=[],b=0;b<c;b++)d[b>>>2]|=(a.charCodeAt(b)&255)<<24-8*(b%4);return new r.init(d,c)}},j=l.Utf8={stringify:function(a){try{return decodeURIComponent(escape(n.stringify(a)))}catch(c){throw Error("Malformed UTF-8 data");}},parse:function(a){return n.parse(unescape(encodeURIComponent(a)))}},
|
||||||
|
u=g.BufferedBlockAlgorithm=m.extend({reset:function(){this._data=new r.init;this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=j.parse(a));this._data.concat(a);this._nDataBytes+=a.sigBytes},_process:function(a){var c=this._data,d=c.words,b=c.sigBytes,e=this.blockSize,f=b/(4*e),f=a?h.ceil(f):h.max((f|0)-this._minBufferSize,0);a=f*e;b=h.min(4*a,b);if(a){for(var g=0;g<a;g+=e)this._doProcessBlock(d,g);g=d.splice(0,a);c.sigBytes-=b}return new r.init(g,b)},clone:function(){var a=m.clone.call(this);
|
||||||
|
a._data=this._data.clone();return a},_minBufferSize:0});g.Hasher=u.extend({cfg:m.extend(),init:function(a){this.cfg=this.cfg.extend(a);this.reset()},reset:function(){u.reset.call(this);this._doReset()},update:function(a){this._append(a);this._process();return this},finalize:function(a){a&&this._append(a);return this._doFinalize()},blockSize:16,_createHelper:function(a){return function(c,d){return(new a.init(d)).finalize(c)}},_createHmacHelper:function(a){return function(c,d){return(new t.HMAC.init(a,
|
||||||
|
d)).finalize(c)}}});var t=f.algo={};return f}(Math);
|
||||||
|
(function(h){for(var s=CryptoJS,f=s.lib,g=f.WordArray,q=f.Hasher,f=s.algo,m=[],r=[],l=function(a){return 4294967296*(a-(a|0))|0},k=2,n=0;64>n;){var j;a:{j=k;for(var u=h.sqrt(j),t=2;t<=u;t++)if(!(j%t)){j=!1;break a}j=!0}j&&(8>n&&(m[n]=l(h.pow(k,0.5))),r[n]=l(h.pow(k,1/3)),n++);k++}var a=[],f=f.SHA256=q.extend({_doReset:function(){this._hash=new g.init(m.slice(0))},_doProcessBlock:function(c,d){for(var b=this._hash.words,e=b[0],f=b[1],g=b[2],j=b[3],h=b[4],m=b[5],n=b[6],q=b[7],p=0;64>p;p++){if(16>p)a[p]=
|
||||||
|
c[d+p]|0;else{var k=a[p-15],l=a[p-2];a[p]=((k<<25|k>>>7)^(k<<14|k>>>18)^k>>>3)+a[p-7]+((l<<15|l>>>17)^(l<<13|l>>>19)^l>>>10)+a[p-16]}k=q+((h<<26|h>>>6)^(h<<21|h>>>11)^(h<<7|h>>>25))+(h&m^~h&n)+r[p]+a[p];l=((e<<30|e>>>2)^(e<<19|e>>>13)^(e<<10|e>>>22))+(e&f^e&g^f&g);q=n;n=m;m=h;h=j+k|0;j=g;g=f;f=e;e=k+l|0}b[0]=b[0]+e|0;b[1]=b[1]+f|0;b[2]=b[2]+g|0;b[3]=b[3]+j|0;b[4]=b[4]+h|0;b[5]=b[5]+m|0;b[6]=b[6]+n|0;b[7]=b[7]+q|0},_doFinalize:function(){var a=this._data,d=a.words,b=8*this._nDataBytes,e=8*a.sigBytes;
|
||||||
|
d[e>>>5]|=128<<24-e%32;d[(e+64>>>9<<4)+14]=h.floor(b/4294967296);d[(e+64>>>9<<4)+15]=b;a.sigBytes=4*d.length;this._process();return this._hash},clone:function(){var a=q.clone.call(this);a._hash=this._hash.clone();return a}});s.SHA256=q._createHelper(f);s.HmacSHA256=q._createHmacHelper(f)})(Math);
|
||||||
|
(function(){var h=CryptoJS,s=h.enc.Utf8;h.algo.HMAC=h.lib.Base.extend({init:function(f,g){f=this._hasher=new f.init;"string"==typeof g&&(g=s.parse(g));var h=f.blockSize,m=4*h;g.sigBytes>m&&(g=f.finalize(g));g.clamp();for(var r=this._oKey=g.clone(),l=this._iKey=g.clone(),k=r.words,n=l.words,j=0;j<h;j++)k[j]^=1549556828,n[j]^=909522486;r.sigBytes=l.sigBytes=m;this.reset()},reset:function(){var f=this._hasher;f.reset();f.update(this._iKey)},update:function(f){this._hasher.update(f);return this},finalize:function(f){var g=
|
||||||
|
this._hasher;f=g.finalize(f);g.reset();return g.finalize(this._oKey.clone().concat(f))}})})();
|
|
@ -0,0 +1,94 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-cmn-Hans">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||||||
|
<meta content="yes" name="apple-mobile-web-app-capable"/>
|
||||||
|
<link rel="apple-touch-icon" href="/favicon.ico">
|
||||||
|
<meta name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, viewport-fit=cover" />
|
||||||
|
<title>ChatGPT Web</title>
|
||||||
|
<script src="/push.js"></script>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="dark:bg-black">
|
||||||
|
<div id="app">
|
||||||
|
<style>
|
||||||
|
.loading-wrap {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.balls {
|
||||||
|
width: 4em;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row nowrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.balls div {
|
||||||
|
width: 0.8em;
|
||||||
|
height: 0.8em;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #4b9e5f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.balls div:nth-of-type(1) {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
animation: left-swing 0.5s ease-in alternate infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.balls div:nth-of-type(3) {
|
||||||
|
transform: translateX(-95%);
|
||||||
|
animation: right-swing 0.5s ease-out alternate infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes left-swing {
|
||||||
|
|
||||||
|
50%,
|
||||||
|
100% {
|
||||||
|
transform: translateX(95%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes right-swing {
|
||||||
|
50% {
|
||||||
|
transform: translateX(-95%);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
body {
|
||||||
|
background: #121212;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div class="loading-wrap">
|
||||||
|
<div class="balls">
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
<script>
|
||||||
|
var APPID = "2eda6c2e";
|
||||||
|
var API_SECRET = "MDEyMzE5YTc5YmQ5NjMwOTU1MWY4N2Y2";
|
||||||
|
var API_KEY = "12ec1f9d113932575fc4b114a2f60ffd";
|
||||||
|
</script>
|
||||||
|
<script src="./crypto-js.js"></script>
|
||||||
|
<script src="./dists/index.umd.js"></script>
|
||||||
|
<script src="./index.js"></script>
|
||||||
|
<!-- <script src="./input-file.js"></script> -->
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
|
@ -0,0 +1,207 @@
|
||||||
|
// (function () {
|
||||||
|
|
||||||
|
|
||||||
|
// // btnControl.onclick = function () {
|
||||||
|
// // if (btnStatus === "UNDEFINED" || btnStatus === "CLOSED") {
|
||||||
|
// // connectWebSocket();
|
||||||
|
// // } else if (btnStatus === "CONNECTING" || btnStatus === "OPEN") {
|
||||||
|
// // // 结束录音
|
||||||
|
// // recorder.stop();
|
||||||
|
// // }
|
||||||
|
// // };
|
||||||
|
// })();
|
||||||
|
|
||||||
|
let btnStatus = "UNDEFINED"; // "UNDEFINED" "CONNECTING" "OPEN" "CLOSING" "CLOSED"
|
||||||
|
|
||||||
|
// const btnControl = document.getElementById("btn_control");
|
||||||
|
const btnControl = {};
|
||||||
|
|
||||||
|
const recorder = new RecorderManager("./dists");
|
||||||
|
recorder.onStart = () => {
|
||||||
|
changeBtnStatus("OPEN");
|
||||||
|
}
|
||||||
|
let iatWS;
|
||||||
|
let resultText = "";
|
||||||
|
let resultTextTemp = "";
|
||||||
|
let countdownInterval;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取websocket url
|
||||||
|
* 该接口需要后端提供,这里为了方便前端处理
|
||||||
|
*/
|
||||||
|
function getWebSocketUrl() {
|
||||||
|
// 请求地址根据语种不同变化
|
||||||
|
var url = "wss://iat-api.xfyun.cn/v2/iat";
|
||||||
|
var host = "iat-api.xfyun.cn";
|
||||||
|
var apiKey = API_KEY;
|
||||||
|
var apiSecret = API_SECRET;
|
||||||
|
var date = new Date().toGMTString();
|
||||||
|
var algorithm = "hmac-sha256";
|
||||||
|
var headers = "host date request-line";
|
||||||
|
var signatureOrigin = `host: ${host}\ndate: ${date}\nGET /v2/iat HTTP/1.1`;
|
||||||
|
var signatureSha = CryptoJS.HmacSHA256(signatureOrigin, apiSecret);
|
||||||
|
var signature = CryptoJS.enc.Base64.stringify(signatureSha);
|
||||||
|
var authorizationOrigin = `api_key="${apiKey}", algorithm="${algorithm}", headers="${headers}", signature="${signature}"`;
|
||||||
|
var authorization = btoa(authorizationOrigin);
|
||||||
|
url = `${url}?authorization=${authorization}&date=${date}&host=${host}`;
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toBase64(buffer) {
|
||||||
|
var binary = "";
|
||||||
|
var bytes = new Uint8Array(buffer);
|
||||||
|
var len = bytes.byteLength;
|
||||||
|
for (var i = 0; i < len; i++) {
|
||||||
|
binary += String.fromCharCode(bytes[i]);
|
||||||
|
}
|
||||||
|
return window.btoa(binary);
|
||||||
|
}
|
||||||
|
|
||||||
|
function countdown() {
|
||||||
|
let seconds = 60;
|
||||||
|
btnControl.innerText = `录音中(${seconds}s)`;
|
||||||
|
countdownInterval = setInterval(() => {
|
||||||
|
seconds = seconds - 1;
|
||||||
|
if (seconds <= 0) {
|
||||||
|
clearInterval(countdownInterval);
|
||||||
|
recorder.stop();
|
||||||
|
} else {
|
||||||
|
btnControl.innerText = `录音中(${seconds}s)`;
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeBtnStatus(status) {
|
||||||
|
btnStatus = status;
|
||||||
|
if (status === "CONNECTING") {
|
||||||
|
btnControl.innerText = "建立连接中";
|
||||||
|
document.getElementById("result").innerText = "";
|
||||||
|
resultText = "";
|
||||||
|
resultTextTemp = "";
|
||||||
|
} else if (status === "OPEN") {
|
||||||
|
countdown();
|
||||||
|
} else if (status === "CLOSING") {
|
||||||
|
btnControl.innerText = "关闭连接中";
|
||||||
|
} else if (status === "CLOSED") {
|
||||||
|
btnControl.innerText = "开始录音";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderResult(resultData) {
|
||||||
|
// 识别结束
|
||||||
|
let jsonData = JSON.parse(resultData);
|
||||||
|
if (jsonData.data && jsonData.data.result) {
|
||||||
|
let data = jsonData.data.result;
|
||||||
|
let str = "";
|
||||||
|
let ws = data.ws;
|
||||||
|
for (let i = 0; i < ws.length; i++) {
|
||||||
|
str = str + ws[i].cw[0].w;
|
||||||
|
}
|
||||||
|
// 开启wpgs会有此字段(前提:在控制台开通动态修正功能)
|
||||||
|
// 取值为 "apd"时表示该片结果是追加到前面的最终结果;取值为"rpl" 时表示替换前面的部分结果,替换范围为rg字段
|
||||||
|
if (data.pgs) {
|
||||||
|
if (data.pgs === "apd") {
|
||||||
|
// 将resultTextTemp同步给resultText
|
||||||
|
resultText = resultTextTemp;
|
||||||
|
}
|
||||||
|
// 将结果存储在resultTextTemp中
|
||||||
|
resultTextTemp = resultText + str;
|
||||||
|
} else {
|
||||||
|
resultText = resultText + str;
|
||||||
|
}
|
||||||
|
document.getElementById("result").innerText =
|
||||||
|
resultTextTemp || resultText || "";
|
||||||
|
console.log(resultTextTemp);
|
||||||
|
}
|
||||||
|
if (jsonData.code === 0 && jsonData.data.status === 2) {
|
||||||
|
iatWS.close();
|
||||||
|
}
|
||||||
|
if (jsonData.code !== 0) {
|
||||||
|
iatWS.close();
|
||||||
|
console.error(jsonData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function connectWebSocket() {
|
||||||
|
const websocketUrl = getWebSocketUrl();
|
||||||
|
if ("WebSocket" in window) {
|
||||||
|
iatWS = new WebSocket(websocketUrl);
|
||||||
|
} else if ("MozWebSocket" in window) {
|
||||||
|
iatWS = new MozWebSocket(websocketUrl);
|
||||||
|
} else {
|
||||||
|
alert("浏览器不支持WebSocket");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
changeBtnStatus("CONNECTING");
|
||||||
|
iatWS.onopen = (e) => {
|
||||||
|
// 开始录音
|
||||||
|
recorder.start({
|
||||||
|
sampleRate: 16000,
|
||||||
|
frameSize: 1280,
|
||||||
|
});
|
||||||
|
var params = {
|
||||||
|
common: {
|
||||||
|
app_id: APPID,
|
||||||
|
},
|
||||||
|
business: {
|
||||||
|
language: "zh_cn",
|
||||||
|
domain: "iat",
|
||||||
|
accent: "mandarin",
|
||||||
|
vad_eos: 5000,
|
||||||
|
dwa: "wpgs",
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
status: 0,
|
||||||
|
format: "audio/L16;rate=16000",
|
||||||
|
encoding: "raw",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
iatWS.send(JSON.stringify(params));
|
||||||
|
};
|
||||||
|
iatWS.onmessage = (e) => {
|
||||||
|
renderResult(e.data);
|
||||||
|
};
|
||||||
|
iatWS.onerror = (e) => {
|
||||||
|
console.error(e);
|
||||||
|
recorder.stop();
|
||||||
|
changeBtnStatus("CLOSED");
|
||||||
|
};
|
||||||
|
iatWS.onclose = (e) => {
|
||||||
|
recorder.stop();
|
||||||
|
changeBtnStatus("CLOSED");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
recorder.onFrameRecorded = ({ isLastFrame, frameBuffer }) => {
|
||||||
|
if (iatWS.readyState === iatWS.OPEN) {
|
||||||
|
iatWS.send(
|
||||||
|
JSON.stringify({
|
||||||
|
data: {
|
||||||
|
status: isLastFrame ? 2 : 1,
|
||||||
|
format: "audio/L16;rate=16000",
|
||||||
|
encoding: "raw",
|
||||||
|
audio: toBase64(frameBuffer),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
if (isLastFrame) {
|
||||||
|
changeBtnStatus("CLOSING");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
recorder.onStop = () => {
|
||||||
|
clearInterval(countdownInterval);
|
||||||
|
};
|
||||||
|
function RecordXunfei(){
|
||||||
|
if (btnStatus === "UNDEFINED" || btnStatus === "CLOSED") {
|
||||||
|
console.log('开始录音');
|
||||||
|
connectWebSocket();
|
||||||
|
} else if (btnStatus === "CONNECTING" || btnStatus === "OPEN") {
|
||||||
|
console.log('结束录音');
|
||||||
|
|
||||||
|
// 结束录音
|
||||||
|
recorder.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,166 @@
|
||||||
|
(function () {
|
||||||
|
let iatWS;
|
||||||
|
let resultText = "";
|
||||||
|
let resultTextTemp = "";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取websocket url
|
||||||
|
* 该接口需要后端提供,这里为了方便前端处理
|
||||||
|
*/
|
||||||
|
function getWebSocketUrl() {
|
||||||
|
// 请求地址根据语种不同变化
|
||||||
|
var url = "wss://iat-api.xfyun.cn/v2/iat";
|
||||||
|
var host = "iat-api.xfyun.cn";
|
||||||
|
var apiKey = API_KEY;
|
||||||
|
var apiSecret = API_SECRET;
|
||||||
|
var date = new Date().toGMTString();
|
||||||
|
var algorithm = "hmac-sha256";
|
||||||
|
var headers = "host date request-line";
|
||||||
|
var signatureOrigin = `host: ${host}\ndate: ${date}\nGET /v2/iat HTTP/1.1`;
|
||||||
|
var signatureSha = CryptoJS.HmacSHA256(signatureOrigin, apiSecret);
|
||||||
|
var signature = CryptoJS.enc.Base64.stringify(signatureSha);
|
||||||
|
var authorizationOrigin = `api_key="${apiKey}", algorithm="${algorithm}", headers="${headers}", signature="${signature}"`;
|
||||||
|
var authorization = btoa(authorizationOrigin);
|
||||||
|
url = `${url}?authorization=${authorization}&date=${date}&host=${host}`;
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toString(buffer) {
|
||||||
|
var binary = "";
|
||||||
|
var bytes = new Uint8Array(buffer);
|
||||||
|
var len = bytes.byteLength;
|
||||||
|
for (var i = 0; i < len; i++) {
|
||||||
|
binary += String.fromCharCode(bytes[i]);
|
||||||
|
}
|
||||||
|
return binary;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderResult(resultData) {
|
||||||
|
// 识别结束
|
||||||
|
let jsonData = JSON.parse(resultData);
|
||||||
|
if (jsonData.data && jsonData.data.result) {
|
||||||
|
let data = jsonData.data.result;
|
||||||
|
let str = "";
|
||||||
|
let ws = data.ws;
|
||||||
|
for (let i = 0; i < ws.length; i++) {
|
||||||
|
str = str + ws[i].cw[0].w;
|
||||||
|
}
|
||||||
|
// 开启wpgs会有此字段(前提:在控制台开通动态修正功能)
|
||||||
|
// 取值为 "apd"时表示该片结果是追加到前面的最终结果;取值为"rpl" 时表示替换前面的部分结果,替换范围为rg字段
|
||||||
|
if (data.pgs) {
|
||||||
|
if (data.pgs === "apd") {
|
||||||
|
// 将resultTextTemp同步给resultText
|
||||||
|
resultText = resultTextTemp;
|
||||||
|
}
|
||||||
|
// 将结果存储在resultTextTemp中
|
||||||
|
resultTextTemp = resultText + str;
|
||||||
|
} else {
|
||||||
|
resultText = resultText + str;
|
||||||
|
}
|
||||||
|
document.getElementById("result").innerText =
|
||||||
|
resultTextTemp || resultText || "";
|
||||||
|
}
|
||||||
|
if (jsonData.code === 0 && jsonData.data.status === 2) {
|
||||||
|
iatWS.close();
|
||||||
|
}
|
||||||
|
if (jsonData.code !== 0) {
|
||||||
|
iatWS.close();
|
||||||
|
console.error(jsonData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function connectWebSocket(callback) {
|
||||||
|
const websocketUrl = getWebSocketUrl();
|
||||||
|
if ("WebSocket" in window) {
|
||||||
|
iatWS = new WebSocket(websocketUrl);
|
||||||
|
} else if ("MozWebSocket" in window) {
|
||||||
|
iatWS = new MozWebSocket(websocketUrl);
|
||||||
|
} else {
|
||||||
|
alert("浏览器不支持WebSocket");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
iatWS.onopen = (e) => {
|
||||||
|
var params = {
|
||||||
|
common: {
|
||||||
|
app_id: APPID,
|
||||||
|
},
|
||||||
|
business: {
|
||||||
|
language: "zh_cn",
|
||||||
|
domain: "iat",
|
||||||
|
accent: "mandarin",
|
||||||
|
vad_eos: 5000,
|
||||||
|
dwa: "wpgs",
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
status: 0,
|
||||||
|
format: "audio/L16;rate=16000",
|
||||||
|
encoding: "raw",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
iatWS.send(JSON.stringify(params));
|
||||||
|
callback();
|
||||||
|
};
|
||||||
|
iatWS.onmessage = (e) => {
|
||||||
|
renderResult(e.data);
|
||||||
|
};
|
||||||
|
iatWS.onerror = (e) => {
|
||||||
|
console.error(e);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("input_file").onchange = (e) => {
|
||||||
|
if (e.target.files[0]) {
|
||||||
|
connectWebSocket(() => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.readAsArrayBuffer(e.target.files[0]);
|
||||||
|
|
||||||
|
reader.onload = (evt) => {
|
||||||
|
console.log(evt.target.result);
|
||||||
|
const audioString = toString(evt.target.result)
|
||||||
|
// console.log(audioString.length, evt.target.result.byteLength)
|
||||||
|
let offset = 0;
|
||||||
|
|
||||||
|
while(offset < audioString.length) {
|
||||||
|
const subString = audioString.substring(offset, offset + 1280)
|
||||||
|
offset += 1280
|
||||||
|
// console.log(subString.length, subString)
|
||||||
|
const isEnd = offset >= audioString.length
|
||||||
|
iatWS.send(
|
||||||
|
JSON.stringify({
|
||||||
|
data: {
|
||||||
|
status: isEnd ? 2 : 1,
|
||||||
|
format: "audio/L16;rate=16000",
|
||||||
|
encoding: "raw",
|
||||||
|
audio: window.btoa(subString)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// const interval = setInterval(() => {
|
||||||
|
// const subString = audioString.substring(offset, offset + 1280)
|
||||||
|
// offset += 1280
|
||||||
|
// // console.log(subString.length, subString)
|
||||||
|
// const isEnd = offset >= audioString.length
|
||||||
|
// iatWS.send(
|
||||||
|
// JSON.stringify({
|
||||||
|
// data: {
|
||||||
|
// status: isEnd ? 2 : 1,
|
||||||
|
// format: "audio/L16;rate=16000",
|
||||||
|
// encoding: "raw",
|
||||||
|
// audio: window.btoa(subString)
|
||||||
|
// },
|
||||||
|
// })
|
||||||
|
// );
|
||||||
|
// if (isEnd) {
|
||||||
|
// clearInterval(interval)
|
||||||
|
// }
|
||||||
|
// }, 20)
|
||||||
|
|
||||||
|
};
|
||||||
|
reader.onerror = () => {
|
||||||
|
iatWS.close();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
|
@ -0,0 +1,9 @@
|
||||||
|
## 增加一个Kubernetes的部署方式
|
||||||
|
```
|
||||||
|
kubectl apply -f deploy.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
### 如果需要Ingress域名接入
|
||||||
|
```
|
||||||
|
kubectl apply -f ingress.yaml
|
||||||
|
```
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2023 ChenZhaoYu
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
|
@ -0,0 +1,256 @@
|
||||||
|
/*
|
||||||
|
* A JavaScript implementation of the RSA Data Security, Inc. MD5 Message
|
||||||
|
* Digest Algorithm, as defined in RFC 1321.
|
||||||
|
* Version 2.1 Copyright (C) Paul Johnston 1999 - 2002.
|
||||||
|
* Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
|
||||||
|
* Distributed under the BSD License
|
||||||
|
* See http://pajhome.org.uk/crypt/md5 for more info.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Configurable variables. You may need to tweak these to be compatible with
|
||||||
|
* the server-side, but the defaults work in most cases.
|
||||||
|
*/
|
||||||
|
var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */
|
||||||
|
var b64pad = ""; /* base-64 pad character. "=" for strict RFC compliance */
|
||||||
|
var chrsz = 8; /* bits per input character. 8 - ASCII; 16 - Unicode */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* These are the functions you'll usually want to call
|
||||||
|
* They take string arguments and return either hex or base-64 encoded strings
|
||||||
|
*/
|
||||||
|
function hex_md5(s){ return binl2hex(core_md5(str2binl(s), s.length * chrsz));}
|
||||||
|
function b64_md5(s){ return binl2b64(core_md5(str2binl(s), s.length * chrsz));}
|
||||||
|
function str_md5(s){ return binl2str(core_md5(str2binl(s), s.length * chrsz));}
|
||||||
|
function hex_hmac_md5(key, data) { return binl2hex(core_hmac_md5(key, data)); }
|
||||||
|
function b64_hmac_md5(key, data) { return binl2b64(core_hmac_md5(key, data)); }
|
||||||
|
function str_hmac_md5(key, data) { return binl2str(core_hmac_md5(key, data)); }
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Perform a simple self-test to see if the VM is working
|
||||||
|
*/
|
||||||
|
function md5_vm_test()
|
||||||
|
{
|
||||||
|
return hex_md5("abc") == "900150983cd24fb0d6963f7d28e17f72";
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Calculate the MD5 of an array of little-endian words, and a bit length
|
||||||
|
*/
|
||||||
|
function core_md5(x, len)
|
||||||
|
{
|
||||||
|
/* append padding */
|
||||||
|
x[len >> 5] |= 0x80 << ((len) % 32);
|
||||||
|
x[(((len + 64) >>> 9) << 4) + 14] = len;
|
||||||
|
|
||||||
|
var a = 1732584193;
|
||||||
|
var b = -271733879;
|
||||||
|
var c = -1732584194;
|
||||||
|
var d = 271733878;
|
||||||
|
|
||||||
|
for(var i = 0; i < x.length; i += 16)
|
||||||
|
{
|
||||||
|
var olda = a;
|
||||||
|
var oldb = b;
|
||||||
|
var oldc = c;
|
||||||
|
var oldd = d;
|
||||||
|
|
||||||
|
a = md5_ff(a, b, c, d, x[i+ 0], 7 , -680876936);
|
||||||
|
d = md5_ff(d, a, b, c, x[i+ 1], 12, -389564586);
|
||||||
|
c = md5_ff(c, d, a, b, x[i+ 2], 17, 606105819);
|
||||||
|
b = md5_ff(b, c, d, a, x[i+ 3], 22, -1044525330);
|
||||||
|
a = md5_ff(a, b, c, d, x[i+ 4], 7 , -176418897);
|
||||||
|
d = md5_ff(d, a, b, c, x[i+ 5], 12, 1200080426);
|
||||||
|
c = md5_ff(c, d, a, b, x[i+ 6], 17, -1473231341);
|
||||||
|
b = md5_ff(b, c, d, a, x[i+ 7], 22, -45705983);
|
||||||
|
a = md5_ff(a, b, c, d, x[i+ 8], 7 , 1770035416);
|
||||||
|
d = md5_ff(d, a, b, c, x[i+ 9], 12, -1958414417);
|
||||||
|
c = md5_ff(c, d, a, b, x[i+10], 17, -42063);
|
||||||
|
b = md5_ff(b, c, d, a, x[i+11], 22, -1990404162);
|
||||||
|
a = md5_ff(a, b, c, d, x[i+12], 7 , 1804603682);
|
||||||
|
d = md5_ff(d, a, b, c, x[i+13], 12, -40341101);
|
||||||
|
c = md5_ff(c, d, a, b, x[i+14], 17, -1502002290);
|
||||||
|
b = md5_ff(b, c, d, a, x[i+15], 22, 1236535329);
|
||||||
|
|
||||||
|
a = md5_gg(a, b, c, d, x[i+ 1], 5 , -165796510);
|
||||||
|
d = md5_gg(d, a, b, c, x[i+ 6], 9 , -1069501632);
|
||||||
|
c = md5_gg(c, d, a, b, x[i+11], 14, 643717713);
|
||||||
|
b = md5_gg(b, c, d, a, x[i+ 0], 20, -373897302);
|
||||||
|
a = md5_gg(a, b, c, d, x[i+ 5], 5 , -701558691);
|
||||||
|
d = md5_gg(d, a, b, c, x[i+10], 9 , 38016083);
|
||||||
|
c = md5_gg(c, d, a, b, x[i+15], 14, -660478335);
|
||||||
|
b = md5_gg(b, c, d, a, x[i+ 4], 20, -405537848);
|
||||||
|
a = md5_gg(a, b, c, d, x[i+ 9], 5 , 568446438);
|
||||||
|
d = md5_gg(d, a, b, c, x[i+14], 9 , -1019803690);
|
||||||
|
c = md5_gg(c, d, a, b, x[i+ 3], 14, -187363961);
|
||||||
|
b = md5_gg(b, c, d, a, x[i+ 8], 20, 1163531501);
|
||||||
|
a = md5_gg(a, b, c, d, x[i+13], 5 , -1444681467);
|
||||||
|
d = md5_gg(d, a, b, c, x[i+ 2], 9 , -51403784);
|
||||||
|
c = md5_gg(c, d, a, b, x[i+ 7], 14, 1735328473);
|
||||||
|
b = md5_gg(b, c, d, a, x[i+12], 20, -1926607734);
|
||||||
|
|
||||||
|
a = md5_hh(a, b, c, d, x[i+ 5], 4 , -378558);
|
||||||
|
d = md5_hh(d, a, b, c, x[i+ 8], 11, -2022574463);
|
||||||
|
c = md5_hh(c, d, a, b, x[i+11], 16, 1839030562);
|
||||||
|
b = md5_hh(b, c, d, a, x[i+14], 23, -35309556);
|
||||||
|
a = md5_hh(a, b, c, d, x[i+ 1], 4 , -1530992060);
|
||||||
|
d = md5_hh(d, a, b, c, x[i+ 4], 11, 1272893353);
|
||||||
|
c = md5_hh(c, d, a, b, x[i+ 7], 16, -155497632);
|
||||||
|
b = md5_hh(b, c, d, a, x[i+10], 23, -1094730640);
|
||||||
|
a = md5_hh(a, b, c, d, x[i+13], 4 , 681279174);
|
||||||
|
d = md5_hh(d, a, b, c, x[i+ 0], 11, -358537222);
|
||||||
|
c = md5_hh(c, d, a, b, x[i+ 3], 16, -722521979);
|
||||||
|
b = md5_hh(b, c, d, a, x[i+ 6], 23, 76029189);
|
||||||
|
a = md5_hh(a, b, c, d, x[i+ 9], 4 , -640364487);
|
||||||
|
d = md5_hh(d, a, b, c, x[i+12], 11, -421815835);
|
||||||
|
c = md5_hh(c, d, a, b, x[i+15], 16, 530742520);
|
||||||
|
b = md5_hh(b, c, d, a, x[i+ 2], 23, -995338651);
|
||||||
|
|
||||||
|
a = md5_ii(a, b, c, d, x[i+ 0], 6 , -198630844);
|
||||||
|
d = md5_ii(d, a, b, c, x[i+ 7], 10, 1126891415);
|
||||||
|
c = md5_ii(c, d, a, b, x[i+14], 15, -1416354905);
|
||||||
|
b = md5_ii(b, c, d, a, x[i+ 5], 21, -57434055);
|
||||||
|
a = md5_ii(a, b, c, d, x[i+12], 6 , 1700485571);
|
||||||
|
d = md5_ii(d, a, b, c, x[i+ 3], 10, -1894986606);
|
||||||
|
c = md5_ii(c, d, a, b, x[i+10], 15, -1051523);
|
||||||
|
b = md5_ii(b, c, d, a, x[i+ 1], 21, -2054922799);
|
||||||
|
a = md5_ii(a, b, c, d, x[i+ 8], 6 , 1873313359);
|
||||||
|
d = md5_ii(d, a, b, c, x[i+15], 10, -30611744);
|
||||||
|
c = md5_ii(c, d, a, b, x[i+ 6], 15, -1560198380);
|
||||||
|
b = md5_ii(b, c, d, a, x[i+13], 21, 1309151649);
|
||||||
|
a = md5_ii(a, b, c, d, x[i+ 4], 6 , -145523070);
|
||||||
|
d = md5_ii(d, a, b, c, x[i+11], 10, -1120210379);
|
||||||
|
c = md5_ii(c, d, a, b, x[i+ 2], 15, 718787259);
|
||||||
|
b = md5_ii(b, c, d, a, x[i+ 9], 21, -343485551);
|
||||||
|
|
||||||
|
a = safe_add(a, olda);
|
||||||
|
b = safe_add(b, oldb);
|
||||||
|
c = safe_add(c, oldc);
|
||||||
|
d = safe_add(d, oldd);
|
||||||
|
}
|
||||||
|
return Array(a, b, c, d);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* These functions implement the four basic operations the algorithm uses.
|
||||||
|
*/
|
||||||
|
function md5_cmn(q, a, b, x, s, t)
|
||||||
|
{
|
||||||
|
return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s),b);
|
||||||
|
}
|
||||||
|
function md5_ff(a, b, c, d, x, s, t)
|
||||||
|
{
|
||||||
|
return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t);
|
||||||
|
}
|
||||||
|
function md5_gg(a, b, c, d, x, s, t)
|
||||||
|
{
|
||||||
|
return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t);
|
||||||
|
}
|
||||||
|
function md5_hh(a, b, c, d, x, s, t)
|
||||||
|
{
|
||||||
|
return md5_cmn(b ^ c ^ d, a, b, x, s, t);
|
||||||
|
}
|
||||||
|
function md5_ii(a, b, c, d, x, s, t)
|
||||||
|
{
|
||||||
|
return md5_cmn(c ^ (b | (~d)), a, b, x, s, t);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Calculate the HMAC-MD5, of a key and some data
|
||||||
|
*/
|
||||||
|
function core_hmac_md5(key, data)
|
||||||
|
{
|
||||||
|
var bkey = str2binl(key);
|
||||||
|
if(bkey.length > 16) bkey = core_md5(bkey, key.length * chrsz);
|
||||||
|
|
||||||
|
var ipad = Array(16), opad = Array(16);
|
||||||
|
for(var i = 0; i < 16; i++)
|
||||||
|
{
|
||||||
|
ipad[i] = bkey[i] ^ 0x36363636;
|
||||||
|
opad[i] = bkey[i] ^ 0x5C5C5C5C;
|
||||||
|
}
|
||||||
|
|
||||||
|
var hash = core_md5(ipad.concat(str2binl(data)), 512 + data.length * chrsz);
|
||||||
|
return core_md5(opad.concat(hash), 512 + 128);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Add integers, wrapping at 2^32. This uses 16-bit operations internally
|
||||||
|
* to work around bugs in some JS interpreters.
|
||||||
|
*/
|
||||||
|
function safe_add(x, y)
|
||||||
|
{
|
||||||
|
var lsw = (x & 0xFFFF) + (y & 0xFFFF);
|
||||||
|
var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
|
||||||
|
return (msw << 16) | (lsw & 0xFFFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Bitwise rotate a 32-bit number to the left.
|
||||||
|
*/
|
||||||
|
function bit_rol(num, cnt)
|
||||||
|
{
|
||||||
|
return (num << cnt) | (num >>> (32 - cnt));
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Convert a string to an array of little-endian words
|
||||||
|
* If chrsz is ASCII, characters >255 have their hi-byte silently ignored.
|
||||||
|
*/
|
||||||
|
function str2binl(str)
|
||||||
|
{
|
||||||
|
var bin = Array();
|
||||||
|
var mask = (1 << chrsz) - 1;
|
||||||
|
for(var i = 0; i < str.length * chrsz; i += chrsz)
|
||||||
|
bin[i>>5] |= (str.charCodeAt(i / chrsz) & mask) << (i%32);
|
||||||
|
return bin;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Convert an array of little-endian words to a string
|
||||||
|
*/
|
||||||
|
function binl2str(bin)
|
||||||
|
{
|
||||||
|
var str = "";
|
||||||
|
var mask = (1 << chrsz) - 1;
|
||||||
|
for(var i = 0; i < bin.length * 32; i += chrsz)
|
||||||
|
str += String.fromCharCode((bin[i>>5] >>> (i % 32)) & mask);
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Convert an array of little-endian words to a hex string.
|
||||||
|
*/
|
||||||
|
function binl2hex(binarray)
|
||||||
|
{
|
||||||
|
var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
|
||||||
|
var str = "";
|
||||||
|
for(var i = 0; i < binarray.length * 4; i++)
|
||||||
|
{
|
||||||
|
str += hex_tab.charAt((binarray[i>>2] >> ((i%4)*8+4)) & 0xF) +
|
||||||
|
hex_tab.charAt((binarray[i>>2] >> ((i%4)*8 )) & 0xF);
|
||||||
|
}
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Convert an array of little-endian words to a base-64 string
|
||||||
|
*/
|
||||||
|
function binl2b64(binarray)
|
||||||
|
{
|
||||||
|
var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||||
|
var str = "";
|
||||||
|
for(var i = 0; i < binarray.length * 4; i += 3)
|
||||||
|
{
|
||||||
|
var triplet = (((binarray[i >> 2] >> 8 * ( i %4)) & 0xFF) << 16)
|
||||||
|
| (((binarray[i+1 >> 2] >> 8 * ((i+1)%4)) & 0xFF) << 8 )
|
||||||
|
| ((binarray[i+2 >> 2] >> 8 * ((i+2)%4)) & 0xFF);
|
||||||
|
for(var j = 0; j < 4; j++)
|
||||||
|
{
|
||||||
|
if(i * 8 + j * 6 > binarray.length * 32) str += b64pad;
|
||||||
|
else str += tab.charAt((triplet >> 6*(3-j)) & 0x3F);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return str;
|
||||||
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
{
|
||||||
|
"name": "chatgpt-web",
|
||||||
|
"version": "2.11.1",
|
||||||
|
"private": false,
|
||||||
|
"description": "ChatGPT Web",
|
||||||
|
"author": "ChenZhaoYu <chenzhaoyu1994@gmail.com>",
|
||||||
|
"keywords": [
|
||||||
|
"chatgpt-web",
|
||||||
|
"chatgpt",
|
||||||
|
"chatbot",
|
||||||
|
"vue"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "run-p type-check build-only",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"build-only": "vite build",
|
||||||
|
"type-check": "vue-tsc --noEmit",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"lint:fix": "eslint . --fix",
|
||||||
|
"bootstrap": "pnpm install && pnpm run common:prepare",
|
||||||
|
"common:cleanup": "rimraf node_modules && rimraf pnpm-lock.yaml",
|
||||||
|
"common:prepare": "husky install"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@traptitech/markdown-it-katex": "^3.6.0",
|
||||||
|
"@vueuse/core": "^9.13.0",
|
||||||
|
"highlight.js": "^11.7.0",
|
||||||
|
"html2canvas": "^1.4.1",
|
||||||
|
"katex": "^0.16.4",
|
||||||
|
"markdown-it": "^13.0.1",
|
||||||
|
"naive-ui": "^2.34.3",
|
||||||
|
"pinia": "^2.0.33",
|
||||||
|
"voice-input-button2": "^1.1.9",
|
||||||
|
"vue": "^3.2.47",
|
||||||
|
"vue-i18n": "^9.2.2",
|
||||||
|
"vue-router": "^4.1.6"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@antfu/eslint-config": "^0.35.3",
|
||||||
|
"@commitlint/cli": "^17.4.4",
|
||||||
|
"@commitlint/config-conventional": "^17.4.4",
|
||||||
|
"@iconify/vue": "^4.1.0",
|
||||||
|
"@types/crypto-js": "^4.1.1",
|
||||||
|
"@types/katex": "^0.16.0",
|
||||||
|
"@types/markdown-it": "^12.2.3",
|
||||||
|
"@types/markdown-it-link-attributes": "^3.0.1",
|
||||||
|
"@types/node": "^18.14.6",
|
||||||
|
"@vitejs/plugin-vue": "^4.0.0",
|
||||||
|
"autoprefixer": "^10.4.13",
|
||||||
|
"axios": "^1.3.4",
|
||||||
|
"crypto-js": "^4.1.1",
|
||||||
|
"eslint": "^8.35.0",
|
||||||
|
"husky": "^8.0.3",
|
||||||
|
"less": "^4.1.3",
|
||||||
|
"lint-staged": "^13.1.2",
|
||||||
|
"markdown-it-link-attributes": "^4.0.1",
|
||||||
|
"npm-run-all": "^4.1.5",
|
||||||
|
"postcss": "^8.4.21",
|
||||||
|
"rimraf": "^4.2.0",
|
||||||
|
"tailwindcss": "^3.2.7",
|
||||||
|
"typescript": "~4.9.5",
|
||||||
|
"vite": "^4.2.0",
|
||||||
|
"vite-plugin-pwa": "^0.14.4",
|
||||||
|
"vue-tsc": "^1.2.0"
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"*.{ts,tsx,vue}": [
|
||||||
|
"pnpm lint:fix"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
!function(){"use strict";function t(t){return function(t){if(Array.isArray(t))return e(t)}(t)||function(t){if("undefined"!=typeof Symbol&&null!=t[Symbol.iterator]||null!=t["@@iterator"])return Array.from(t)}(t)||function(t,r){if(!t)return;if("string"==typeof t)return e(t,r);var i=Object.prototype.toString.call(t).slice(8,-1);"Object"===i&&t.constructor&&(i=t.constructor.name);if("Map"===i||"Set"===i)return Array.from(t);if("Arguments"===i||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(i))return e(t,r)}(t)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function e(t,e){(null==e||e>t.length)&&(e=t.length);for(var r=0,i=new Array(e);r<e;r++)i[r]=t[r];return i}function r(t,e,r,i){this.fromSampleRate=t,this.toSampleRate=e,this.channels=0|r,this.noReturn=!!i,this.initialize()}r.prototype.initialize=function(){if(!(this.fromSampleRate>0&&this.toSampleRate>0&&this.channels>0))throw new Error("Invalid settings specified for the resampler.");this.fromSampleRate==this.toSampleRate?(this.resampler=this.bypassResampler,this.ratioWeight=1):(this.fromSampleRate<this.toSampleRate?(this.lastWeight=1,this.resampler=this.compileLinearInterpolation):(this.tailExists=!1,this.lastWeight=0,this.resampler=this.compileMultiTap),this.ratioWeight=this.fromSampleRate/this.toSampleRate)},r.prototype.compileLinearInterpolation=function(t){var e=t.length;this.initializeBuffers(e);var r,i,s=this.outputBufferSize,a=this.ratioWeight,f=this.lastWeight,n=0,o=0,h=0,l=this.outputBuffer;if(e%this.channels==0){if(e>0){for(;f<1;f+=a)for(n=1-(o=f%1),r=0;r<this.channels;++r)l[h++]=this.lastOutput[r]*n+t[r]*o;for(f--,e-=this.channels,i=Math.floor(f)*this.channels;h<s&&i<e;){for(n=1-(o=f%1),r=0;r<this.channels;++r)l[h++]=t[i+r]*n+t[i+this.channels+r]*o;f+=a,i=Math.floor(f)*this.channels}for(r=0;r<this.channels;++r)this.lastOutput[r]=t[i++];return this.lastWeight=f%1,this.bufferSlice(h)}return this.noReturn?0:[]}throw new Error("Buffer was of incorrect sample length.")},r.prototype.compileMultiTap=function(t){var e=[],r=t.length;this.initializeBuffers(r);var i=this.outputBufferSize;if(r%this.channels==0){if(r>0){for(var s=this.ratioWeight,a=0,f=0;f<this.channels;++f)e[f]=0;var n=0,o=0,h=!this.tailExists;this.tailExists=!1;var l=this.outputBuffer,u=0,p=0;do{if(h)for(a=s,f=0;f<this.channels;++f)e[f]=0;else{for(a=this.lastWeight,f=0;f<this.channels;++f)e[f]+=this.lastOutput[f];h=!0}for(;a>0&&n<r;){if(!(a>=(o=1+n-p))){for(f=0;f<this.channels;++f)e[f]+=t[n+f]*a;p+=a,a=0;break}for(f=0;f<this.channels;++f)e[f]+=t[n++]*o;p=n,a-=o}if(0!=a){for(this.lastWeight=a,f=0;f<this.channels;++f)this.lastOutput[f]=e[f];this.tailExists=!0;break}for(f=0;f<this.channels;++f)l[u++]=e[f]/s}while(n<r&&u<i);return this.bufferSlice(u)}return this.noReturn?0:[]}throw new Error("Buffer was of incorrect sample length.")},r.prototype.bypassResampler=function(t){return this.noReturn?(this.outputBuffer=t,t.length):t},r.prototype.bufferSlice=function(t){if(this.noReturn)return t;try{return this.outputBuffer.subarray(0,t)}catch(e){try{return this.outputBuffer.length=t,this.outputBuffer}catch(e){return this.outputBuffer.slice(0,t)}}},r.prototype.initializeBuffers=function(t){this.outputBufferSize=Math.ceil(t*this.toSampleRate/this.fromSampleRate);try{this.outputBuffer=new Float32Array(this.outputBufferSize),this.lastOutput=new Float32Array(this.channels)}catch(t){this.outputBuffer=[],this.lastOutput=[]}},self.transData=function(t){return"short16"===self.arrayBufferType&&(t=function(t){for(var e=new ArrayBuffer(2*t.length),r=new DataView(e),i=0,s=0;s<t.length;s+=1,i+=2){var a=Math.max(-1,Math.min(1,t[s]));r.setInt16(i,a<0?32768*a:32767*a,!0)}return r.buffer}(t=self.resampler.resampler(t))),t},self.onmessage=function(e){var i=e.data,s=i.type,a=i.data;if("init"===s){var f=a.frameSize,n=a.toSampleRate,o=a.fromSampleRate,h=a.arrayBufferType;return self.frameSize=f*Math.floor(o/n),self.resampler=new r(o,n,1),self.frameBuffer=[],void(self.arrayBufferType=h)}if("stop"===s&&(self.postMessage({frameBuffer:self.transData(self.frameBuffer),isLastFrame:!0}),self.frameBuffer=[]),"message"===s){var l,u=a;if(self.frameSize)return(l=self.frameBuffer).push.apply(l,t(u)),self.frameBuffer.length>=self.frameSize&&(self.postMessage({frameBuffer:self.transData(this.frameBuffer),isLastFrame:!1}),self.frameBuffer=[]),!0;u&&self.postMessage({frameBuffer:self.transData(u),isLastFrame:!1})}}}();
|
After Width: | Height: | Size: 41 KiB |
|
@ -0,0 +1 @@
|
||||||
|
<svg id="openai-symbol" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="M29.71,13.09A8.09,8.09,0,0,0,20.34,2.68a8.08,8.08,0,0,0-13.7,2.9A8.08,8.08,0,0,0,2.3,18.9,8,8,0,0,0,3,25.45a8.08,8.08,0,0,0,8.69,3.87,8,8,0,0,0,6,2.68,8.09,8.09,0,0,0,7.7-5.61,8,8,0,0,0,5.33-3.86A8.09,8.09,0,0,0,29.71,13.09Zm-12,16.82a6,6,0,0,1-3.84-1.39l.19-.11,6.37-3.68a1,1,0,0,0,.53-.91v-9l2.69,1.56a.08.08,0,0,1,.05.07v7.44A6,6,0,0,1,17.68,29.91ZM4.8,24.41a6,6,0,0,1-.71-4l.19.11,6.37,3.68a1,1,0,0,0,1,0l7.79-4.49V22.8a.09.09,0,0,1,0,.08L13,26.6A6,6,0,0,1,4.8,24.41ZM3.12,10.53A6,6,0,0,1,6.28,7.9v7.57a1,1,0,0,0,.51.9l7.75,4.47L11.85,22.4a.14.14,0,0,1-.09,0L5.32,18.68a6,6,0,0,1-2.2-8.18Zm22.13,5.14-7.78-4.52L20.16,9.6a.08.08,0,0,1,.09,0l6.44,3.72a6,6,0,0,1-.9,10.81V16.56A1.06,1.06,0,0,0,25.25,15.67Zm2.68-4-.19-.12-6.36-3.7a1,1,0,0,0-1.05,0l-7.78,4.49V9.2a.09.09,0,0,1,0-.09L19,5.4a6,6,0,0,1,8.91,6.21ZM11.08,17.15,8.38,15.6a.14.14,0,0,1-.05-.08V8.1a6,6,0,0,1,9.84-4.61L18,3.6,11.61,7.28a1,1,0,0,0-.53.91ZM12.54,14,16,12l3.47,2v4L16,20l-3.47-2Z"/></svg>
|
After Width: | Height: | Size: 1.0 KiB |
|
@ -0,0 +1,746 @@
|
||||||
|
|
||||||
|
function Push(options) {
|
||||||
|
this.doNotConnect = 0;
|
||||||
|
options = options || {};
|
||||||
|
options.heartbeat = options.heartbeat || 25000;
|
||||||
|
options.pingTimeout = options.pingTimeout || 10000;
|
||||||
|
this.config = options;
|
||||||
|
this.uid = 0;
|
||||||
|
this.channels = {};
|
||||||
|
this.connection = null;
|
||||||
|
this.pingTimeoutTimer = 0;
|
||||||
|
Push.instances.push(this);
|
||||||
|
this.createConnection();
|
||||||
|
}
|
||||||
|
|
||||||
|
Push.prototype.checkoutPing = function() {
|
||||||
|
var _this = this;
|
||||||
|
_this.checkoutPingTimer && clearTimeout(_this.checkoutPingTimer);
|
||||||
|
_this.checkoutPingTimer = setTimeout(function () {
|
||||||
|
_this.checkoutPingTimer = 0;
|
||||||
|
if (_this.connection.state === 'connected') {
|
||||||
|
_this.connection.send('{"event":"pusher:ping","data":{}}');
|
||||||
|
if (_this.pingTimeoutTimer) {
|
||||||
|
clearTimeout(_this.pingTimeoutTimer);
|
||||||
|
_this.pingTimeoutTimer = 0;
|
||||||
|
}
|
||||||
|
_this.pingTimeoutTimer = setTimeout(function () {
|
||||||
|
_this.connection.closeAndClean();
|
||||||
|
if (!_this.connection.doNotConnect) {
|
||||||
|
_this.connection.waitReconnect();
|
||||||
|
}
|
||||||
|
}, _this.config.pingTimeout);
|
||||||
|
}
|
||||||
|
}, this.config.heartbeat);
|
||||||
|
};
|
||||||
|
|
||||||
|
Push.prototype.channel = function (name) {
|
||||||
|
return this.channels.find(name);
|
||||||
|
};
|
||||||
|
Push.prototype.allChannels = function () {
|
||||||
|
return this.channels.all();
|
||||||
|
};
|
||||||
|
Push.prototype.createConnection = function () {
|
||||||
|
if (this.connection) {
|
||||||
|
throw Error('Connection already exist');
|
||||||
|
}
|
||||||
|
var _this = this;
|
||||||
|
var url = this.config.url;
|
||||||
|
function updateSubscribed () {
|
||||||
|
for (var i in _this.channels) {
|
||||||
|
_this.channels[i].subscribed = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.connection = new Connection({
|
||||||
|
url: url,
|
||||||
|
app_key: this.config.app_key,
|
||||||
|
onOpen: function () {
|
||||||
|
_this.connection.state ='connecting';
|
||||||
|
_this.checkoutPing();
|
||||||
|
},
|
||||||
|
onMessage: function(params) {
|
||||||
|
if(_this.pingTimeoutTimer) {
|
||||||
|
clearTimeout(_this.pingTimeoutTimer);
|
||||||
|
_this.pingTimeoutTimer = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
params = JSON.parse(params.data);
|
||||||
|
var event = params.event;
|
||||||
|
var channel_name = params.channel;
|
||||||
|
|
||||||
|
if (event === 'pusher:pong') {
|
||||||
|
_this.checkoutPing();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event === 'pusher:error') {
|
||||||
|
throw Error(params.data.message);
|
||||||
|
}
|
||||||
|
var data = JSON.parse(params.data), channel;
|
||||||
|
if (event === 'pusher_internal:subscription_succeeded') {
|
||||||
|
channel = _this.channels[channel_name];
|
||||||
|
channel.subscribed = true;
|
||||||
|
channel.processQueue();
|
||||||
|
channel.emit('pusher:subscription_succeeded');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event === 'pusher:connection_established') {
|
||||||
|
_this.connection.socket_id = data.socket_id;
|
||||||
|
_this.connection.updateNetworkState('connected');
|
||||||
|
_this.subscribeAll();
|
||||||
|
}
|
||||||
|
if (event.indexOf('pusher_internal') !== -1) {
|
||||||
|
console.log("Event '"+event+"' not implement");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
channel = _this.channels[channel_name];
|
||||||
|
if (channel) {
|
||||||
|
channel.emit(event, data);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onClose: function () {
|
||||||
|
updateSubscribed();
|
||||||
|
},
|
||||||
|
onError: function () {
|
||||||
|
updateSubscribed();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
Push.prototype.disconnect = function () {
|
||||||
|
this.connection.doNotConnect = 1;
|
||||||
|
this.connection.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
Push.prototype.subscribeAll = function () {
|
||||||
|
if (this.connection.state !== 'connected') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (var channel_name in this.channels) {
|
||||||
|
//this.connection.send(JSON.stringify({event:"pusher:subscribe", data:{channel:channel_name}}));
|
||||||
|
this.channels[channel_name].processSubscribe();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Push.prototype.unsubscribe = function (channel_name) {
|
||||||
|
if (this.channels[channel_name]) {
|
||||||
|
delete this.channels[channel_name];
|
||||||
|
if (this.connection.state === 'connected') {
|
||||||
|
this.connection.send(JSON.stringify({event:"pusher:unsubscribe", data:{channel:channel_name}}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Push.prototype.unsubscribeAll = function () {
|
||||||
|
var channels = Object.keys(this.channels);
|
||||||
|
if (channels.length) {
|
||||||
|
if (this.connection.state === 'connected') {
|
||||||
|
for (var channel_name in this.channels) {
|
||||||
|
this.unsubscribe(channel_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.channels = {};
|
||||||
|
};
|
||||||
|
Push.prototype.subscribe = function (channel_name) {
|
||||||
|
if (this.channels[channel_name]) {
|
||||||
|
return this.channels[channel_name];
|
||||||
|
}
|
||||||
|
if (channel_name.indexOf('private-') === 0) {
|
||||||
|
return createPrivateChannel(channel_name, this);
|
||||||
|
}
|
||||||
|
if (channel_name.indexOf('presence-') === 0) {
|
||||||
|
return createPresenceChannel(channel_name, this);
|
||||||
|
}
|
||||||
|
return createChannel(channel_name, this);
|
||||||
|
};
|
||||||
|
Push.instances = [];
|
||||||
|
|
||||||
|
function createChannel(channel_name, push)
|
||||||
|
{
|
||||||
|
var channel = new Channel(push.connection, channel_name);
|
||||||
|
push.channels[channel_name] = channel;
|
||||||
|
channel.subscribeCb = function () {
|
||||||
|
push.connection.send(JSON.stringify({event:"pusher:subscribe", data:{channel:channel_name}}));
|
||||||
|
}
|
||||||
|
channel.processSubscribe();
|
||||||
|
return channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createPrivateChannel(channel_name, push)
|
||||||
|
{
|
||||||
|
var channel = new Channel(push.connection, channel_name);
|
||||||
|
push.channels[channel_name] = channel;
|
||||||
|
channel.subscribeCb = function () {
|
||||||
|
__ajax({
|
||||||
|
url: push.config.auth,
|
||||||
|
type: 'POST',
|
||||||
|
data: {channel_name: channel_name, socket_id: push.connection.socket_id},
|
||||||
|
success: function (data) {
|
||||||
|
data = JSON.parse(data);
|
||||||
|
data.channel = channel_name;
|
||||||
|
push.connection.send(JSON.stringify({event:"pusher:subscribe", data:data}));
|
||||||
|
},
|
||||||
|
error: function (e) {
|
||||||
|
throw Error(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
channel.processSubscribe();
|
||||||
|
return channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createPresenceChannel(channel_name, push)
|
||||||
|
{
|
||||||
|
return createPrivateChannel(channel_name, push);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*window.addEventListener('online', function(){
|
||||||
|
var con;
|
||||||
|
for (var i in Push.instances) {
|
||||||
|
con = Push.instances[i].connection;
|
||||||
|
con.reconnectInterval = 1;
|
||||||
|
if (con.state === 'connecting') {
|
||||||
|
con.connect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});*/
|
||||||
|
|
||||||
|
|
||||||
|
function Connection(options) {
|
||||||
|
this.dispatcher = new Dispatcher();
|
||||||
|
__extends(this, this.dispatcher);
|
||||||
|
var properies = ['on', 'off', 'emit'];
|
||||||
|
for (var i in properies) {
|
||||||
|
this[properies[i]] = this.dispatcher[properies[i]];
|
||||||
|
}
|
||||||
|
this.options = options;
|
||||||
|
this.state = 'initialized'; //initialized connecting connected disconnected
|
||||||
|
this.doNotConnect = 0;
|
||||||
|
this.reconnectInterval = 1;
|
||||||
|
this.connection = null;
|
||||||
|
this.reconnectTimer = 0;
|
||||||
|
this.connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
Connection.prototype.updateNetworkState = function(state){
|
||||||
|
var old_state = this.state;
|
||||||
|
this.state = state;
|
||||||
|
if (old_state !== state) {
|
||||||
|
this.emit('state_change', { previous: old_state, current: state });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Connection.prototype.connect = function () {
|
||||||
|
this.doNotConnect = 0;
|
||||||
|
if (this.state === 'connected') {
|
||||||
|
console.log('networkState is "' + this.state + '" and do not need connect');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.reconnectTimer) {
|
||||||
|
clearTimeout(this.reconnectTimer);
|
||||||
|
this.reconnectTimer = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.closeAndClean();
|
||||||
|
|
||||||
|
var options = this.options;
|
||||||
|
var websocket = new WebSocket(options.url+'/app/'+options.app_key);
|
||||||
|
|
||||||
|
this.updateNetworkState('connecting');
|
||||||
|
|
||||||
|
var _this = this;
|
||||||
|
websocket.onopen = function (res) {
|
||||||
|
_this.reconnectInterval = 1;
|
||||||
|
if (_this.doNotConnect) {
|
||||||
|
_this.updateNetworkState('disconnected');
|
||||||
|
websocket.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (options.onOpen) {
|
||||||
|
options.onOpen(res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (options.onMessage) {
|
||||||
|
websocket.onmessage = options.onMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
websocket.onclose = function (res) {
|
||||||
|
websocket.onmessage = websocket.onopen = websocket.onclose = websocket.onerror = null;
|
||||||
|
_this.updateNetworkState('disconnected');
|
||||||
|
if (!_this.doNotConnect) {
|
||||||
|
_this.waitReconnect();
|
||||||
|
}
|
||||||
|
if (options.onClose) {
|
||||||
|
options.onClose(res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
websocket.onerror = function (res) {
|
||||||
|
_this.close();
|
||||||
|
if (!_this.doNotConnect) {
|
||||||
|
_this.waitReconnect();
|
||||||
|
}
|
||||||
|
if (options.onError) {
|
||||||
|
options.onError(res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.connection = websocket;
|
||||||
|
}
|
||||||
|
|
||||||
|
Connection.prototype.closeAndClean = function () {
|
||||||
|
if(this.connection) {
|
||||||
|
var websocket = this.connection;
|
||||||
|
websocket.onmessage = websocket.onopen = websocket.onclose = websocket.onerror = null;
|
||||||
|
try {
|
||||||
|
websocket.close();
|
||||||
|
} catch (e) {}
|
||||||
|
this.updateNetworkState('disconnected');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Connection.prototype.waitReconnect = function () {
|
||||||
|
if (this.state === 'connected' || this.state === 'connecting') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.doNotConnect) {
|
||||||
|
this.updateNetworkState('connecting');
|
||||||
|
var _this = this;
|
||||||
|
if (this.reconnectTimer) {
|
||||||
|
clearTimeout(this.reconnectTimer);
|
||||||
|
}
|
||||||
|
this.reconnectTimer = setTimeout(function(){
|
||||||
|
_this.connect();
|
||||||
|
}, this.reconnectInterval);
|
||||||
|
if (this.reconnectInterval < 1000) {
|
||||||
|
this.reconnectInterval = 1000;
|
||||||
|
} else {
|
||||||
|
// 每次重连间隔增大一倍
|
||||||
|
this.reconnectInterval = this.reconnectInterval * 2;
|
||||||
|
}
|
||||||
|
// 有网络的状态下,重连间隔最大2秒
|
||||||
|
if (this.reconnectInterval > 2000 && navigator.onLine) {
|
||||||
|
_this.reconnectInterval = 2000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connection.prototype.send = function(data) {
|
||||||
|
if (this.state !== 'connected') {
|
||||||
|
console.trace('networkState is "' + this.state + '", can not send ' + data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.connection.send(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
Connection.prototype.close = function(){
|
||||||
|
this.updateNetworkState('disconnected');
|
||||||
|
this.connection.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
var __extends = (this && this.__extends) || function (d, b) {
|
||||||
|
for (var p in b) if (b.hasOwnProperty(p)) {d[p] = b[p];}
|
||||||
|
function __() { this.constructor = d; }
|
||||||
|
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
|
||||||
|
};
|
||||||
|
|
||||||
|
function Channel(connection, channel_name) {
|
||||||
|
this.subscribed = false;
|
||||||
|
this.dispatcher = new Dispatcher();
|
||||||
|
this.connection = connection;
|
||||||
|
this.channelName = channel_name;
|
||||||
|
this.subscribeCb = null;
|
||||||
|
this.queue = [];
|
||||||
|
__extends(this, this.dispatcher);
|
||||||
|
var properies = ['on', 'off', 'emit'];
|
||||||
|
for (var i in properies) {
|
||||||
|
this[properies[i]] = this.dispatcher[properies[i]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Channel.prototype.processSubscribe = function () {
|
||||||
|
if (this.connection.state !== 'connected') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.subscribeCb();
|
||||||
|
};
|
||||||
|
|
||||||
|
Channel.prototype.processQueue = function () {
|
||||||
|
if (this.connection.state !== 'connected' || !this.subscribed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (var i in this.queue) {
|
||||||
|
this.queue[i]();
|
||||||
|
}
|
||||||
|
this.queue = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
Channel.prototype.trigger = function (event, data) {
|
||||||
|
if (event.indexOf('client-') !== 0) {
|
||||||
|
throw new Error("Event '" + event + "' should start with 'client-'");
|
||||||
|
}
|
||||||
|
var _this = this;
|
||||||
|
this.queue.push(function () {
|
||||||
|
_this.connection.send(JSON.stringify({ event: event, data: data, channel: _this.channelName }));
|
||||||
|
});
|
||||||
|
this.processQueue();
|
||||||
|
};
|
||||||
|
|
||||||
|
////////////////
|
||||||
|
var Collections = (function () {
|
||||||
|
var exports = {};
|
||||||
|
function extend(target) {
|
||||||
|
var sources = [];
|
||||||
|
for (var _i = 1; _i < arguments.length; _i++) {
|
||||||
|
sources[_i - 1] = arguments[_i];
|
||||||
|
}
|
||||||
|
for (var i = 0; i < sources.length; i++) {
|
||||||
|
var extensions = sources[i];
|
||||||
|
for (var property in extensions) {
|
||||||
|
if (extensions[property] && extensions[property].constructor &&
|
||||||
|
extensions[property].constructor === Object) {
|
||||||
|
target[property] = extend(target[property] || {}, extensions[property]);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
target[property] = extensions[property];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.extend = extend;
|
||||||
|
function stringify() {
|
||||||
|
var m = ["Push"];
|
||||||
|
for (var i = 0; i < arguments.length; i++) {
|
||||||
|
if (typeof arguments[i] === "string") {
|
||||||
|
m.push(arguments[i]);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
m.push(safeJSONStringify(arguments[i]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m.join(" : ");
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.stringify = stringify;
|
||||||
|
function arrayIndexOf(array, item) {
|
||||||
|
var nativeIndexOf = Array.prototype.indexOf;
|
||||||
|
if (array === null) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (nativeIndexOf && array.indexOf === nativeIndexOf) {
|
||||||
|
return array.indexOf(item);
|
||||||
|
}
|
||||||
|
for (var i = 0, l = array.length; i < l; i++) {
|
||||||
|
if (array[i] === item) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.arrayIndexOf = arrayIndexOf;
|
||||||
|
function objectApply(object, f) {
|
||||||
|
for (var key in object) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(object, key)) {
|
||||||
|
f(object[key], key, object);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.objectApply = objectApply;
|
||||||
|
function keys(object) {
|
||||||
|
var keys = [];
|
||||||
|
objectApply(object, function (_, key) {
|
||||||
|
keys.push(key);
|
||||||
|
});
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.keys = keys;
|
||||||
|
function values(object) {
|
||||||
|
var values = [];
|
||||||
|
objectApply(object, function (value) {
|
||||||
|
values.push(value);
|
||||||
|
});
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.values = values;
|
||||||
|
function apply(array, f, context) {
|
||||||
|
for (var i = 0; i < array.length; i++) {
|
||||||
|
f.call(context || (window), array[i], i, array);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.apply = apply;
|
||||||
|
function map(array, f) {
|
||||||
|
var result = [];
|
||||||
|
for (var i = 0; i < array.length; i++) {
|
||||||
|
result.push(f(array[i], i, array, result));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.map = map;
|
||||||
|
function mapObject(object, f) {
|
||||||
|
var result = {};
|
||||||
|
objectApply(object, function (value, key) {
|
||||||
|
result[key] = f(value);
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.mapObject = mapObject;
|
||||||
|
function filter(array, test) {
|
||||||
|
test = test || function (value) {
|
||||||
|
return !!value;
|
||||||
|
};
|
||||||
|
var result = [];
|
||||||
|
for (var i = 0; i < array.length; i++) {
|
||||||
|
if (test(array[i], i, array, result)) {
|
||||||
|
result.push(array[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.filter = filter;
|
||||||
|
function filterObject(object, test) {
|
||||||
|
var result = {};
|
||||||
|
objectApply(object, function (value, key) {
|
||||||
|
if ((test && test(value, key, object, result)) || Boolean(value)) {
|
||||||
|
result[key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.filterObject = filterObject;
|
||||||
|
function flatten(object) {
|
||||||
|
var result = [];
|
||||||
|
objectApply(object, function (value, key) {
|
||||||
|
result.push([key, value]);
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.flatten = flatten;
|
||||||
|
function any(array, test) {
|
||||||
|
for (var i = 0; i < array.length; i++) {
|
||||||
|
if (test(array[i], i, array)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.any = any;
|
||||||
|
function all(array, test) {
|
||||||
|
for (var i = 0; i < array.length; i++) {
|
||||||
|
if (!test(array[i], i, array)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.all = all;
|
||||||
|
function encodeParamsObject(data) {
|
||||||
|
return mapObject(data, function (value) {
|
||||||
|
if (typeof value === "object") {
|
||||||
|
value = safeJSONStringify(value);
|
||||||
|
}
|
||||||
|
return encodeURIComponent(base64_1["default"](value.toString()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.encodeParamsObject = encodeParamsObject;
|
||||||
|
function buildQueryString(data) {
|
||||||
|
var params = filterObject(data, function (value) {
|
||||||
|
return value !== undefined;
|
||||||
|
});
|
||||||
|
return map(flatten(encodeParamsObject(params)), util_1["default"].method("join", "=")).join("&");
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.buildQueryString = buildQueryString;
|
||||||
|
function decycleObject(object) {
|
||||||
|
var objects = [], paths = [];
|
||||||
|
return (function derez(value, path) {
|
||||||
|
var i, name, nu;
|
||||||
|
switch (typeof value) {
|
||||||
|
case 'object':
|
||||||
|
if (!value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (i = 0; i < objects.length; i += 1) {
|
||||||
|
if (objects[i] === value) {
|
||||||
|
return {$ref: paths[i]};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
objects.push(value);
|
||||||
|
paths.push(path);
|
||||||
|
if (Object.prototype.toString.apply(value) === '[object Array]') {
|
||||||
|
nu = [];
|
||||||
|
for (i = 0; i < value.length; i += 1) {
|
||||||
|
nu[i] = derez(value[i], path + '[' + i + ']');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
nu = {};
|
||||||
|
for (name in value) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(value, name)) {
|
||||||
|
nu[name] = derez(value[name], path + '[' + JSON.stringify(name) + ']');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nu;
|
||||||
|
case 'number':
|
||||||
|
case 'string':
|
||||||
|
case 'boolean':
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}(object, '$'));
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.decycleObject = decycleObject;
|
||||||
|
function safeJSONStringify(source) {
|
||||||
|
try {
|
||||||
|
return JSON.stringify(source);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
return JSON.stringify(decycleObject(source));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.safeJSONStringify = safeJSONStringify;
|
||||||
|
return exports;
|
||||||
|
})();
|
||||||
|
|
||||||
|
var Dispatcher = (function () {
|
||||||
|
function Dispatcher(failThrough) {
|
||||||
|
this.callbacks = new CallbackRegistry();
|
||||||
|
this.global_callbacks = [];
|
||||||
|
this.failThrough = failThrough;
|
||||||
|
}
|
||||||
|
Dispatcher.prototype.on = function (eventName, callback, context) {
|
||||||
|
this.callbacks.add(eventName, callback, context);
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
Dispatcher.prototype.on_global = function (callback) {
|
||||||
|
this.global_callbacks.push(callback);
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
Dispatcher.prototype.off = function (eventName, callback, context) {
|
||||||
|
this.callbacks.remove(eventName, callback, context);
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
Dispatcher.prototype.emit = function (eventName, data) {
|
||||||
|
var i;
|
||||||
|
for (i = 0; i < this.global_callbacks.length; i++) {
|
||||||
|
this.global_callbacks[i](eventName, data);
|
||||||
|
}
|
||||||
|
var callbacks = this.callbacks.get(eventName);
|
||||||
|
if (callbacks && callbacks.length > 0) {
|
||||||
|
for (i = 0; i < callbacks.length; i++) {
|
||||||
|
callbacks[i].fn.call(callbacks[i].context || (window), data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (this.failThrough) {
|
||||||
|
this.failThrough(eventName, data);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
return Dispatcher;
|
||||||
|
}());
|
||||||
|
|
||||||
|
var CallbackRegistry = (function () {
|
||||||
|
function CallbackRegistry() {
|
||||||
|
this._callbacks = {};
|
||||||
|
}
|
||||||
|
CallbackRegistry.prototype.get = function (name) {
|
||||||
|
return this._callbacks[prefix(name)];
|
||||||
|
};
|
||||||
|
CallbackRegistry.prototype.add = function (name, callback, context) {
|
||||||
|
var prefixedEventName = prefix(name);
|
||||||
|
this._callbacks[prefixedEventName] = this._callbacks[prefixedEventName] || [];
|
||||||
|
this._callbacks[prefixedEventName].push({
|
||||||
|
fn: callback,
|
||||||
|
context: context
|
||||||
|
});
|
||||||
|
};
|
||||||
|
CallbackRegistry.prototype.remove = function (name, callback, context) {
|
||||||
|
if (!name && !callback && !context) {
|
||||||
|
this._callbacks = {};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var names = name ? [prefix(name)] : Collections.keys(this._callbacks);
|
||||||
|
if (callback || context) {
|
||||||
|
this.removeCallback(names, callback, context);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.removeAllCallbacks(names);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
CallbackRegistry.prototype.removeCallback = function (names, callback, context) {
|
||||||
|
Collections.apply(names, function (name) {
|
||||||
|
this._callbacks[name] = Collections.filter(this._callbacks[name] || [], function (oning) {
|
||||||
|
return (callback && callback !== oning.fn) ||
|
||||||
|
(context && context !== oning.context);
|
||||||
|
});
|
||||||
|
if (this._callbacks[name].length === 0) {
|
||||||
|
delete this._callbacks[name];
|
||||||
|
}
|
||||||
|
}, this);
|
||||||
|
};
|
||||||
|
CallbackRegistry.prototype.removeAllCallbacks = function (names) {
|
||||||
|
Collections.apply(names, function (name) {
|
||||||
|
delete this._callbacks[name];
|
||||||
|
}, this);
|
||||||
|
};
|
||||||
|
return CallbackRegistry;
|
||||||
|
}());
|
||||||
|
function prefix(name) {
|
||||||
|
return "_" + name;
|
||||||
|
}
|
||||||
|
|
||||||
|
function __ajax(options){
|
||||||
|
options=options||{};
|
||||||
|
options.type=(options.type||'GET').toUpperCase();
|
||||||
|
options.dataType=options.dataType||'json';
|
||||||
|
params=formatParams(options.data);
|
||||||
|
|
||||||
|
var xhr;
|
||||||
|
if(window.XMLHttpRequest){
|
||||||
|
xhr=new XMLHttpRequest();
|
||||||
|
}else{
|
||||||
|
xhr=ActiveXObject('Microsoft.XMLHTTP');
|
||||||
|
}
|
||||||
|
|
||||||
|
xhr.onreadystatechange=function(){
|
||||||
|
if(xhr.readyState === 4){
|
||||||
|
var status=xhr.status;
|
||||||
|
if(status>=200 && status<300){
|
||||||
|
options.success&&options.success(xhr.responseText,xhr.responseXML);
|
||||||
|
}else{
|
||||||
|
options.error&&options.error(status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(options.type==='GET'){
|
||||||
|
xhr.open('GET',options.url+'?'+params,true);
|
||||||
|
xhr.send(null);
|
||||||
|
}else if(options.type==='POST'){
|
||||||
|
xhr.open('POST',options.url,true);
|
||||||
|
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
|
||||||
|
xhr.send(params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatParams(data){
|
||||||
|
var arr=[];
|
||||||
|
for(var name in data){
|
||||||
|
arr.push(encodeURIComponent(name)+'='+encodeURIComponent(data[name]));
|
||||||
|
}
|
||||||
|
return arr.join('&');
|
||||||
|
}
|
After Width: | Height: | Size: 7.2 KiB |
After Width: | Height: | Size: 34 KiB |
|
@ -0,0 +1,44 @@
|
||||||
|
# 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=
|
||||||
|
|
||||||
|
# OpenAI API Base URL - https://api.openai.com
|
||||||
|
OPENAI_API_BASE_URL=
|
||||||
|
|
||||||
|
# OpenAI API Model - https://platform.openai.com/docs/models
|
||||||
|
OPENAI_API_MODEL=
|
||||||
|
|
||||||
|
# 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
|
||||||
|
TIMEOUT_MS=100000
|
||||||
|
|
||||||
|
# Rate Limit
|
||||||
|
MAX_REQUEST_PER_HOUR=
|
||||||
|
|
||||||
|
# Secret key
|
||||||
|
AUTH_SECRET_KEY=
|
||||||
|
|
||||||
|
# Socks Proxy Host
|
||||||
|
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=
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"root": true,
|
||||||
|
"ignorePatterns": ["build"],
|
||||||
|
"extends": ["@antfu"]
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
coverage
|
||||||
|
*.local
|
||||||
|
|
||||||
|
/cypress/videos/
|
||||||
|
/cypress/screenshots/
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
build
|
|
@ -0,0 +1 @@
|
||||||
|
enable-pre-post-scripts=true
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"recommendations": ["dbaeumer.vscode-eslint"]
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"prettier.enable": false,
|
||||||
|
"editor.formatOnSave": false,
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.eslint": true
|
||||||
|
},
|
||||||
|
"eslint.validate": [
|
||||||
|
"javascript",
|
||||||
|
"typescript",
|
||||||
|
"json",
|
||||||
|
"jsonc",
|
||||||
|
"json5",
|
||||||
|
"yaml"
|
||||||
|
],
|
||||||
|
"cSpell.words": [
|
||||||
|
"antfu",
|
||||||
|
"chatgpt",
|
||||||
|
"esno",
|
||||||
|
"GPTAPI",
|
||||||
|
"OPENAI"
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
{
|
||||||
|
"name": "chatgpt-web-service",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": false,
|
||||||
|
"description": "ChatGPT Web Service",
|
||||||
|
"author": "ChenZhaoYu <chenzhaoyu1994@gmail.com>",
|
||||||
|
"keywords": [
|
||||||
|
"chatgpt-web",
|
||||||
|
"chatgpt",
|
||||||
|
"chatbot",
|
||||||
|
"express"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^16 || ^18 || ^19"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "esno ./src/index.ts",
|
||||||
|
"dev": "esno watch ./src/index.ts",
|
||||||
|
"prod": "node ./build/index.mjs",
|
||||||
|
"build": "pnpm clean && tsup",
|
||||||
|
"clean": "rimraf build",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"lint:fix": "eslint . --fix",
|
||||||
|
"common:cleanup": "rimraf node_modules && rimraf pnpm-lock.yaml"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.3.4",
|
||||||
|
"chatgpt": "^5.1.2",
|
||||||
|
"dotenv": "^16.0.3",
|
||||||
|
"esno": "^0.16.3",
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"express-rate-limit": "^6.7.0",
|
||||||
|
"https-proxy-agent": "^5.0.1",
|
||||||
|
"isomorphic-fetch": "^3.0.0",
|
||||||
|
"node-fetch": "^3.3.0",
|
||||||
|
"socks-proxy-agent": "^7.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@antfu/eslint-config": "^0.35.3",
|
||||||
|
"@types/express": "^4.17.17",
|
||||||
|
"@types/node": "^18.14.6",
|
||||||
|
"eslint": "^8.35.0",
|
||||||
|
"rimraf": "^4.3.0",
|
||||||
|
"tsup": "^6.6.3",
|
||||||
|
"typescript": "^4.9.5"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,224 @@
|
||||||
|
import * as dotenv from 'dotenv'
|
||||||
|
import 'isomorphic-fetch'
|
||||||
|
import type { ChatGPTAPIOptions, ChatMessage, SendMessageOptions } from 'chatgpt'
|
||||||
|
import { ChatGPTAPI, ChatGPTUnofficialProxyAPI } from 'chatgpt'
|
||||||
|
import { SocksProxyAgent } from 'socks-proxy-agent'
|
||||||
|
import httpsProxyAgent from 'https-proxy-agent'
|
||||||
|
import fetch from 'node-fetch'
|
||||||
|
import { sendResponse } from '../utils'
|
||||||
|
import { isNotEmptyString } from '../utils/is'
|
||||||
|
import type { ApiModel, ChatContext, ChatGPTUnofficialProxyAPIOptions, ModelConfig } from '../types'
|
||||||
|
import type { RequestOptions, SetProxyOptions, UsageResponse } from './types'
|
||||||
|
|
||||||
|
const { HttpsProxyAgent } = httpsProxyAgent
|
||||||
|
|
||||||
|
dotenv.config()
|
||||||
|
|
||||||
|
const ErrorCodeMessage: Record<string, string> = {
|
||||||
|
401: '[OpenAI] 提供错误的API密钥 | Incorrect API key provided',
|
||||||
|
403: '[OpenAI] 服务器拒绝访问,请稍后再试 | Server refused to access, please try again later',
|
||||||
|
502: '[OpenAI] 错误的网关 | Bad Gateway',
|
||||||
|
503: '[OpenAI] 服务器繁忙,请稍后再试 | Server is busy, please try again later',
|
||||||
|
504: '[OpenAI] 网关超时 | Gateway Time-out',
|
||||||
|
500: '[OpenAI] 服务器繁忙,请稍后再试 | Internal Server Error',
|
||||||
|
}
|
||||||
|
|
||||||
|
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')
|
||||||
|
|
||||||
|
let api: ChatGPTAPI | ChatGPTUnofficialProxyAPI
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
// More Info: https://github.com/transitive-bullshit/chatgpt-api
|
||||||
|
|
||||||
|
if (isNotEmptyString(process.env.OPENAI_API_KEY)) {
|
||||||
|
const OPENAI_API_BASE_URL = process.env.OPENAI_API_BASE_URL
|
||||||
|
|
||||||
|
const options: ChatGPTAPIOptions = {
|
||||||
|
apiKey: process.env.OPENAI_API_KEY,
|
||||||
|
completionParams: { model },
|
||||||
|
debug: !disableDebug,
|
||||||
|
}
|
||||||
|
|
||||||
|
// increase max token limit if use gpt-4
|
||||||
|
if (model.toLowerCase().includes('gpt-4')) {
|
||||||
|
// if use 32k model
|
||||||
|
if (model.toLowerCase().includes('32k')) {
|
||||||
|
options.maxModelTokens = 32768
|
||||||
|
options.maxResponseTokens = 8192
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
options.maxModelTokens = 8192
|
||||||
|
options.maxResponseTokens = 2048
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (model.toLowerCase().includes('gpt-3.5')) {
|
||||||
|
if (model.toLowerCase().includes('16k')) {
|
||||||
|
options.maxModelTokens = 16384
|
||||||
|
options.maxResponseTokens = 4096
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNotEmptyString(OPENAI_API_BASE_URL))
|
||||||
|
options.apiBaseUrl = `${OPENAI_API_BASE_URL}/v1`
|
||||||
|
|
||||||
|
setupProxy(options)
|
||||||
|
|
||||||
|
api = new ChatGPTAPI({ ...options })
|
||||||
|
apiModel = 'ChatGPTAPI'
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const options: ChatGPTUnofficialProxyAPIOptions = {
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
setupProxy(options)
|
||||||
|
|
||||||
|
api = new ChatGPTUnofficialProxyAPI({ ...options })
|
||||||
|
apiModel = 'ChatGPTUnofficialProxyAPI'
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
|
async function chatReplyProcess(options: RequestOptions) {
|
||||||
|
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) {
|
||||||
|
if (apiModel === 'ChatGPTAPI')
|
||||||
|
options.parentMessageId = lastContext.parentMessageId
|
||||||
|
else
|
||||||
|
options = { ...lastContext }
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await api.sendMessage(message, {
|
||||||
|
...options,
|
||||||
|
onProgress: (partialResponse) => {
|
||||||
|
process?.(partialResponse)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return sendResponse({ type: 'Success', data: response })
|
||||||
|
}
|
||||||
|
catch (error: any) {
|
||||||
|
const code = error.statusCode
|
||||||
|
global.console.log(error)
|
||||||
|
if (Reflect.has(ErrorCodeMessage, code))
|
||||||
|
return sendResponse({ type: 'Fail', message: ErrorCodeMessage[code] })
|
||||||
|
return sendResponse({ type: 'Fail', message: error.message ?? 'Please check the back-end console' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchUsage() {
|
||||||
|
const OPENAI_API_KEY = process.env.OPENAI_API_KEY
|
||||||
|
const OPENAI_API_BASE_URL = process.env.OPENAI_API_BASE_URL
|
||||||
|
|
||||||
|
if (!isNotEmptyString(OPENAI_API_KEY))
|
||||||
|
return Promise.resolve('-')
|
||||||
|
|
||||||
|
const API_BASE_URL = isNotEmptyString(OPENAI_API_BASE_URL)
|
||||||
|
? OPENAI_API_BASE_URL
|
||||||
|
: 'https://api.openai.com'
|
||||||
|
|
||||||
|
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',
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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)
|
||||||
|
? (`${process.env.SOCKS_PROXY_HOST}:${process.env.SOCKS_PROXY_PORT}`)
|
||||||
|
: '-'
|
||||||
|
return sendResponse<ModelConfig>({
|
||||||
|
type: 'Success',
|
||||||
|
data: { apiModel, reverseProxy, timeoutMs, socksProxy, httpsProxy, usage },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (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 {
|
||||||
|
return apiModel
|
||||||
|
}
|
||||||
|
|
||||||
|
export type { ChatContext, ChatMessage }
|
||||||
|
|
||||||
|
export { chatReplyProcess, chatConfig, currentModel }
|
|
@ -0,0 +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
|
||||||
|
}
|
|
@ -0,0 +1,89 @@
|
||||||
|
import express from 'express'
|
||||||
|
import type { RequestProps } from './types'
|
||||||
|
import type { ChatMessage } from './chatgpt'
|
||||||
|
import { chatConfig, chatReplyProcess, currentModel } from './chatgpt'
|
||||||
|
import { auth } from './middleware/auth'
|
||||||
|
import { limiter } from './middleware/limiter'
|
||||||
|
import { isNotEmptyString } from './utils/is'
|
||||||
|
|
||||||
|
const app = express()
|
||||||
|
const router = express.Router()
|
||||||
|
|
||||||
|
app.use(express.static('public'))
|
||||||
|
app.use(express.json())
|
||||||
|
|
||||||
|
app.all('*', (_, res, next) => {
|
||||||
|
res.header('Access-Control-Allow-Origin', '*')
|
||||||
|
res.header('Access-Control-Allow-Headers', 'authorization, Content-Type')
|
||||||
|
res.header('Access-Control-Allow-Methods', '*')
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
|
||||||
|
router.post('/chat-process', [auth, limiter], async (req, res) => {
|
||||||
|
res.setHeader('Content-type', 'application/octet-stream')
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { prompt, options = {}, systemMessage, temperature, top_p } = req.body as RequestProps
|
||||||
|
let firstChunk = true
|
||||||
|
await chatReplyProcess({
|
||||||
|
message: prompt,
|
||||||
|
lastContext: options,
|
||||||
|
process: (chat: ChatMessage) => {
|
||||||
|
res.write(firstChunk ? JSON.stringify(chat) : `\n${JSON.stringify(chat)}`)
|
||||||
|
firstChunk = false
|
||||||
|
},
|
||||||
|
systemMessage,
|
||||||
|
temperature,
|
||||||
|
top_p,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
res.write(JSON.stringify(error))
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
res.end()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
router.post('/config', auth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const response = await chatConfig()
|
||||||
|
res.send(response)
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
res.send(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
router.post('/session', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const AUTH_SECRET_KEY = process.env.AUTH_SECRET_KEY
|
||||||
|
const hasAuth = isNotEmptyString(AUTH_SECRET_KEY)
|
||||||
|
res.send({ status: 'Success', message: '', data: { auth: hasAuth, model: currentModel() } })
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
res.send({ status: 'Fail', message: error.message, data: null })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
router.post('/verify', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { token } = req.body as { token: string }
|
||||||
|
if (!token)
|
||||||
|
throw new Error('Secret key is empty')
|
||||||
|
|
||||||
|
if (process.env.AUTH_SECRET_KEY !== token)
|
||||||
|
throw new Error('密钥无效 | Secret key is invalid')
|
||||||
|
|
||||||
|
res.send({ status: 'Success', message: 'Verify successfully', data: null })
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
res.send({ status: 'Fail', message: error.message, data: null })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
app.use('', router)
|
||||||
|
app.use('/api', router)
|
||||||
|
app.set('trust proxy', 1)
|
||||||
|
|
||||||
|
app.listen(3002, () => globalThis.console.log('Server is running on port 3002'))
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { isNotEmptyString } from '../utils/is'
|
||||||
|
|
||||||
|
const auth = async (req, res, next) => {
|
||||||
|
const AUTH_SECRET_KEY = process.env.AUTH_SECRET_KEY
|
||||||
|
if (isNotEmptyString(AUTH_SECRET_KEY)) {
|
||||||
|
try {
|
||||||
|
const Authorization = req.header('Authorization')
|
||||||
|
if (!Authorization || Authorization.replace('Bearer ', '').trim() !== AUTH_SECRET_KEY.trim())
|
||||||
|
throw new Error('Error: 无访问权限 | No access rights')
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
res.send({ status: 'Unauthorized', message: error.message ?? 'Please authenticate.', data: null })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { auth }
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { rateLimit } from 'express-rate-limit'
|
||||||
|
import { isNotEmptyString } from '../utils/is'
|
||||||
|
|
||||||
|
const MAX_REQUEST_PER_HOUR = process.env.MAX_REQUEST_PER_HOUR
|
||||||
|
|
||||||
|
const maxCount = (isNotEmptyString(MAX_REQUEST_PER_HOUR) && !isNaN(Number(MAX_REQUEST_PER_HOUR)))
|
||||||
|
? parseInt(MAX_REQUEST_PER_HOUR)
|
||||||
|
: 0 // 0 means unlimited
|
||||||
|
|
||||||
|
const limiter = rateLimit({
|
||||||
|
windowMs: 60 * 60 * 1000, // Maximum number of accesses within an hour
|
||||||
|
max: maxCount,
|
||||||
|
statusCode: 200, // 200 means success,but the message is 'Too many request from this IP in 1 hour'
|
||||||
|
message: async (req, res) => {
|
||||||
|
res.send({ status: 'Fail', message: 'Too many request from this IP in 1 hour', data: null })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export { limiter }
|
|
@ -0,0 +1,34 @@
|
||||||
|
import type { FetchFn } from 'chatgpt'
|
||||||
|
|
||||||
|
export interface RequestProps {
|
||||||
|
prompt: string
|
||||||
|
options?: ChatContext
|
||||||
|
systemMessage: string
|
||||||
|
temperature?: number
|
||||||
|
top_p?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChatContext {
|
||||||
|
conversationId?: string
|
||||||
|
parentMessageId?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChatGPTUnofficialProxyAPIOptions {
|
||||||
|
accessToken: string
|
||||||
|
apiReverseProxyUrl?: string
|
||||||
|
model?: string
|
||||||
|
debug?: boolean
|
||||||
|
headers?: Record<string, string>
|
||||||
|
fetch?: FetchFn
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModelConfig {
|
||||||
|
apiModel?: ApiModel
|
||||||
|
reverseProxy?: string
|
||||||
|
timeoutMs?: number
|
||||||
|
socksProxy?: string
|
||||||
|
httpsProxy?: string
|
||||||
|
usage?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ApiModel = 'ChatGPTAPI' | 'ChatGPTUnofficialProxyAPI' | undefined
|
|
@ -0,0 +1,22 @@
|
||||||
|
interface SendResponseOptions<T = any> {
|
||||||
|
type: 'Success' | 'Fail'
|
||||||
|
message?: string
|
||||||
|
data?: T
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sendResponse<T>(options: SendResponseOptions<T>) {
|
||||||
|
if (options.type === 'Success') {
|
||||||
|
return Promise.resolve({
|
||||||
|
message: options.message ?? null,
|
||||||
|
data: options.data ?? null,
|
||||||
|
status: options.type,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line prefer-promise-reject-errors
|
||||||
|
return Promise.reject({
|
||||||
|
message: options.message ?? 'Failed',
|
||||||
|
data: options.data ?? null,
|
||||||
|
status: options.type,
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
export function isNumber<T extends number>(value: T | unknown): value is number {
|
||||||
|
return Object.prototype.toString.call(value) === '[object Number]'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isString<T extends string>(value: T | unknown): value is string {
|
||||||
|
return Object.prototype.toString.call(value) === '[object String]'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isNotEmptyString(value: any): boolean {
|
||||||
|
return typeof value === 'string' && value.length > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isBoolean<T extends boolean>(value: T | unknown): value is boolean {
|
||||||
|
return Object.prototype.toString.call(value) === '[object Boolean]'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isFunction<T extends (...args: any[]) => any | void | never>(value: T | unknown): value is T {
|
||||||
|
return Object.prototype.toString.call(value) === '[object Function]'
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es2020",
|
||||||
|
"lib": [
|
||||||
|
"esnext"
|
||||||
|
],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": false,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"outDir": "build",
|
||||||
|
"noEmit": true
|
||||||
|
},
|
||||||
|
"exclude": [
|
||||||
|
"node_modules",
|
||||||
|
"build"
|
||||||
|
],
|
||||||
|
"include": [
|
||||||
|
"**/*.ts"
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { defineConfig } from 'tsup'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
entry: ['src/index.ts'],
|
||||||
|
outDir: 'build',
|
||||||
|
target: 'es2020',
|
||||||
|
format: ['esm'],
|
||||||
|
splitting: false,
|
||||||
|
sourcemap: true,
|
||||||
|
minify: false,
|
||||||
|
shims: true,
|
||||||
|
dts: false,
|
||||||
|
})
|
|
@ -0,0 +1,22 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { NConfigProvider } from 'naive-ui'
|
||||||
|
import { NaiveProvider } from '@/components/common'
|
||||||
|
import { useTheme } from '@/hooks/useTheme'
|
||||||
|
import { useLanguage } from '@/hooks/useLanguage'
|
||||||
|
|
||||||
|
const { theme, themeOverrides } = useTheme()
|
||||||
|
const { language } = useLanguage()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NConfigProvider
|
||||||
|
class="h-full"
|
||||||
|
:theme="theme"
|
||||||
|
:theme-overrides="themeOverrides"
|
||||||
|
:locale="language"
|
||||||
|
>
|
||||||
|
<NaiveProvider>
|
||||||
|
<RouterView />
|
||||||
|
</NaiveProvider>
|
||||||
|
</NConfigProvider>
|
||||||
|
</template>
|
|
@ -0,0 +1,66 @@
|
||||||
|
import type { AxiosProgressEvent, GenericAbortSignal } from 'axios'
|
||||||
|
import { post } from '@/utils/request'
|
||||||
|
import { useAuthStore, useSettingStore } from '@/store'
|
||||||
|
|
||||||
|
export function fetchChatAPI<T = any>(
|
||||||
|
prompt: string,
|
||||||
|
options?: { conversationId?: string; parentMessageId?: string },
|
||||||
|
signal?: GenericAbortSignal,
|
||||||
|
) {
|
||||||
|
return post<T>({
|
||||||
|
url: '/chat',
|
||||||
|
data: { prompt, options },
|
||||||
|
signal,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchChatConfig<T = any>() {
|
||||||
|
return post<T>({
|
||||||
|
url: '/config',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchChatAPIProcess<T = any>(
|
||||||
|
params: {
|
||||||
|
prompt: string
|
||||||
|
options?: { conversationId?: string; parentMessageId?: string }
|
||||||
|
signal?: GenericAbortSignal
|
||||||
|
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,
|
||||||
|
signal: params.signal,
|
||||||
|
onDownloadProgress: params.onDownloadProgress,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchSession<T>() {
|
||||||
|
return post<T>({
|
||||||
|
url: '/session',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchVerify<T>(token: string) {
|
||||||
|
return post<T>({
|
||||||
|
url: '/verify',
|
||||||
|
data: { token },
|
||||||
|
})
|
||||||
|
}
|
After Width: | Height: | Size: 5.0 KiB |
|
@ -0,0 +1,14 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"key": "awesome-chatgpt-prompts-zh",
|
||||||
|
"desc": "ChatGPT 中文调教指南",
|
||||||
|
"downloadUrl": "https://raw.githubusercontent.com/PlexPt/awesome-chatgpt-prompts-zh/main/prompts-zh.json",
|
||||||
|
"url": "https://github.com/PlexPt/awesome-chatgpt-prompts-zh"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "awesome-chatgpt-prompts-zh-TW",
|
||||||
|
"desc": "ChatGPT 中文調教指南 (透過 OpenAI / OpenCC 協助,從簡體中文轉換為繁體中文的版本)",
|
||||||
|
"downloadUrl": "https://raw.githubusercontent.com/PlexPt/awesome-chatgpt-prompts-zh/main/prompts-zh-TW.json",
|
||||||
|
"url": "https://github.com/PlexPt/awesome-chatgpt-prompts-zh"
|
||||||
|
}
|
||||||
|
]
|
|
@ -0,0 +1,20 @@
|
||||||
|
<script setup lang='ts'>
|
||||||
|
interface Emit {
|
||||||
|
(e: 'click'): void
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits<Emit>()
|
||||||
|
|
||||||
|
function handleClick() {
|
||||||
|
emit('click')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<button
|
||||||
|
class="flex items-center justify-center w-10 h-10 transition rounded-full hover:bg-neutral-100 dark:hover:bg-[#414755]"
|
||||||
|
@click="handleClick"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</button>
|
||||||
|
</template>
|
|
@ -0,0 +1,46 @@
|
||||||
|
<script setup lang='ts'>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import type { PopoverPlacement } from 'naive-ui'
|
||||||
|
import { NTooltip } from 'naive-ui'
|
||||||
|
import Button from './Button.vue'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
tooltip?: string
|
||||||
|
placement?: PopoverPlacement
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Emit {
|
||||||
|
(e: 'click'): void
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
tooltip: '',
|
||||||
|
placement: 'bottom',
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<Emit>()
|
||||||
|
|
||||||
|
const showTooltip = computed(() => Boolean(props.tooltip))
|
||||||
|
|
||||||
|
function handleClick() {
|
||||||
|
emit('click')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="showTooltip">
|
||||||
|
<NTooltip :placement="placement" trigger="hover">
|
||||||
|
<template #trigger>
|
||||||
|
<Button @click="handleClick">
|
||||||
|
<slot />
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
{{ tooltip }}
|
||||||
|
</NTooltip>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<Button @click="handleClick">
|
||||||
|
<slot />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -0,0 +1,43 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent, h } from 'vue'
|
||||||
|
import {
|
||||||
|
NDialogProvider,
|
||||||
|
NLoadingBarProvider,
|
||||||
|
NMessageProvider,
|
||||||
|
NNotificationProvider,
|
||||||
|
useDialog,
|
||||||
|
useLoadingBar,
|
||||||
|
useMessage,
|
||||||
|
useNotification,
|
||||||
|
} from 'naive-ui'
|
||||||
|
|
||||||
|
function registerNaiveTools() {
|
||||||
|
window.$loadingBar = useLoadingBar()
|
||||||
|
window.$dialog = useDialog()
|
||||||
|
window.$message = useMessage()
|
||||||
|
window.$notification = useNotification()
|
||||||
|
}
|
||||||
|
|
||||||
|
const NaiveProviderContent = defineComponent({
|
||||||
|
name: 'NaiveProviderContent',
|
||||||
|
setup() {
|
||||||
|
registerNaiveTools()
|
||||||
|
},
|
||||||
|
render() {
|
||||||
|
return h('div')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NLoadingBarProvider>
|
||||||
|
<NDialogProvider>
|
||||||
|
<NNotificationProvider>
|
||||||
|
<NMessageProvider>
|
||||||
|
<slot />
|
||||||
|
<NaiveProviderContent />
|
||||||
|
</NMessageProvider>
|
||||||
|
</NNotificationProvider>
|
||||||
|
</NDialogProvider>
|
||||||
|
</NLoadingBarProvider>
|
||||||
|
</template>
|
|
@ -0,0 +1,480 @@
|
||||||
|
<script setup lang='ts'>
|
||||||
|
import type { DataTableColumns } from 'naive-ui'
|
||||||
|
import { computed, h, ref, watch } from 'vue'
|
||||||
|
import { NButton, NCard, NDataTable, NDivider, NInput, NList, NListItem, NModal, NPopconfirm, NSpace, NTabPane, NTabs, NThing, useMessage } from 'naive-ui'
|
||||||
|
import PromptRecommend from '../../../assets/recommend.json'
|
||||||
|
import { SvgIcon } from '..'
|
||||||
|
import { usePromptStore } from '@/store'
|
||||||
|
import { useBasicLayout } from '@/hooks/useBasicLayout'
|
||||||
|
import { t } from '@/locales'
|
||||||
|
|
||||||
|
interface DataProps {
|
||||||
|
renderKey: string
|
||||||
|
renderValue: string
|
||||||
|
key: string
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
visible: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Emit {
|
||||||
|
(e: 'update:visible', visible: boolean): void
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
|
const emit = defineEmits<Emit>()
|
||||||
|
|
||||||
|
const message = useMessage()
|
||||||
|
|
||||||
|
const show = computed({
|
||||||
|
get: () => props.visible,
|
||||||
|
set: (visible: boolean) => emit('update:visible', visible),
|
||||||
|
})
|
||||||
|
|
||||||
|
const showModal = ref(false)
|
||||||
|
|
||||||
|
const importLoading = ref(false)
|
||||||
|
const exportLoading = ref(false)
|
||||||
|
|
||||||
|
const searchValue = ref<string>('')
|
||||||
|
|
||||||
|
// 移动端自适应相关
|
||||||
|
const { isMobile } = useBasicLayout()
|
||||||
|
|
||||||
|
const promptStore = usePromptStore()
|
||||||
|
|
||||||
|
// Prompt在线导入推荐List,根据部署者喜好进行修改(assets/recommend.json)
|
||||||
|
const promptRecommendList = PromptRecommend
|
||||||
|
const promptList = ref<any>(promptStore.promptList)
|
||||||
|
|
||||||
|
// 用于添加修改的临时prompt参数
|
||||||
|
const tempPromptKey = ref('')
|
||||||
|
const tempPromptValue = ref('')
|
||||||
|
|
||||||
|
// Modal模式,根据不同模式渲染不同的Modal内容
|
||||||
|
const modalMode = ref('')
|
||||||
|
|
||||||
|
// 这个是为了后期的修改Prompt内容考虑,因为要针对无uuid的list进行修改,且考虑到不能出现标题和内容的冲突,所以就需要一个临时item来记录一下
|
||||||
|
const tempModifiedItem = ref<any>({})
|
||||||
|
|
||||||
|
// 添加修改导入都使用一个Modal, 临时修改内容占用tempPromptKey,切换状态前先将内容都清楚
|
||||||
|
const changeShowModal = (mode: 'add' | 'modify' | 'local_import', selected = { key: '', value: '' }) => {
|
||||||
|
if (mode === 'add') {
|
||||||
|
tempPromptKey.value = ''
|
||||||
|
tempPromptValue.value = ''
|
||||||
|
}
|
||||||
|
else if (mode === 'modify') {
|
||||||
|
tempModifiedItem.value = { ...selected }
|
||||||
|
tempPromptKey.value = selected.key
|
||||||
|
tempPromptValue.value = selected.value
|
||||||
|
}
|
||||||
|
else if (mode === 'local_import') {
|
||||||
|
tempPromptKey.value = 'local_import'
|
||||||
|
tempPromptValue.value = ''
|
||||||
|
}
|
||||||
|
showModal.value = !showModal.value
|
||||||
|
modalMode.value = mode
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在线导入相关
|
||||||
|
const downloadURL = ref('')
|
||||||
|
const downloadDisabled = computed(() => downloadURL.value.trim().length < 1)
|
||||||
|
const setDownloadURL = (url: string) => {
|
||||||
|
downloadURL.value = url
|
||||||
|
}
|
||||||
|
|
||||||
|
// 控制 input 按钮
|
||||||
|
const inputStatus = computed (() => tempPromptKey.value.trim().length < 1 || tempPromptValue.value.trim().length < 1)
|
||||||
|
|
||||||
|
// Prompt模板相关操作
|
||||||
|
const addPromptTemplate = () => {
|
||||||
|
for (const i of promptList.value) {
|
||||||
|
if (i.key === tempPromptKey.value) {
|
||||||
|
message.error(t('store.addRepeatTitleTips'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (i.value === tempPromptValue.value) {
|
||||||
|
message.error(t('store.addRepeatContentTips', { msg: tempPromptKey.value }))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
promptList.value.unshift({ key: tempPromptKey.value, value: tempPromptValue.value } as never)
|
||||||
|
message.success(t('common.addSuccess'))
|
||||||
|
changeShowModal('add')
|
||||||
|
}
|
||||||
|
|
||||||
|
const modifyPromptTemplate = () => {
|
||||||
|
let index = 0
|
||||||
|
|
||||||
|
// 通过临时索引把待修改项摘出来
|
||||||
|
for (const i of promptList.value) {
|
||||||
|
if (i.key === tempModifiedItem.value.key && i.value === tempModifiedItem.value.value)
|
||||||
|
break
|
||||||
|
index = index + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
const tempList = promptList.value.filter((_: any, i: number) => i !== index)
|
||||||
|
|
||||||
|
// 搜索有冲突的部分
|
||||||
|
for (const i of tempList) {
|
||||||
|
if (i.key === tempPromptKey.value) {
|
||||||
|
message.error(t('store.editRepeatTitleTips'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (i.value === tempPromptValue.value) {
|
||||||
|
message.error(t('store.editRepeatContentTips', { msg: i.key }))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
promptList.value = [{ key: tempPromptKey.value, value: tempPromptValue.value }, ...tempList] as never
|
||||||
|
message.success(t('common.editSuccess'))
|
||||||
|
changeShowModal('modify')
|
||||||
|
}
|
||||||
|
|
||||||
|
const deletePromptTemplate = (row: { key: string; value: string }) => {
|
||||||
|
promptList.value = [
|
||||||
|
...promptList.value.filter((item: { key: string; value: string }) => item.key !== row.key),
|
||||||
|
] as never
|
||||||
|
message.success(t('common.deleteSuccess'))
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearPromptTemplate = () => {
|
||||||
|
promptList.value = []
|
||||||
|
message.success(t('common.clearSuccess'))
|
||||||
|
}
|
||||||
|
|
||||||
|
const importPromptTemplate = (from = 'online') => {
|
||||||
|
try {
|
||||||
|
const jsonData = JSON.parse(tempPromptValue.value)
|
||||||
|
let key = ''
|
||||||
|
let value = ''
|
||||||
|
// 可以扩展加入更多模板字典的key
|
||||||
|
if ('key' in jsonData[0]) {
|
||||||
|
key = 'key'
|
||||||
|
value = 'value'
|
||||||
|
}
|
||||||
|
else if ('act' in jsonData[0]) {
|
||||||
|
key = 'act'
|
||||||
|
value = 'prompt'
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// 不支持的字典的key防止导入 以免破坏prompt商店打开
|
||||||
|
message.warning('prompt key not supported.')
|
||||||
|
throw new Error('prompt key not supported.')
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const i of jsonData) {
|
||||||
|
if (!(key in i) || !(value in i))
|
||||||
|
throw new Error(t('store.importError'))
|
||||||
|
let safe = true
|
||||||
|
for (const j of promptList.value) {
|
||||||
|
if (j.key === i[key]) {
|
||||||
|
message.warning(t('store.importRepeatTitle', { msg: i[key] }))
|
||||||
|
safe = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (j.value === i[value]) {
|
||||||
|
message.warning(t('store.importRepeatContent', { msg: i[key] }))
|
||||||
|
safe = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (safe)
|
||||||
|
promptList.value.unshift({ key: i[key], value: i[value] } as never)
|
||||||
|
}
|
||||||
|
message.success(t('common.importSuccess'))
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
message.error('JSON 格式错误,请检查 JSON 格式')
|
||||||
|
}
|
||||||
|
if (from === 'local')
|
||||||
|
showModal.value = !showModal.value
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模板导出
|
||||||
|
const exportPromptTemplate = () => {
|
||||||
|
exportLoading.value = true
|
||||||
|
const jsonDataStr = JSON.stringify(promptList.value)
|
||||||
|
const blob = new Blob([jsonDataStr], { type: 'application/json' })
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = url
|
||||||
|
link.download = 'ChatGPTPromptTemplate.json'
|
||||||
|
link.click()
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
exportLoading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模板在线导入
|
||||||
|
const downloadPromptTemplate = async () => {
|
||||||
|
try {
|
||||||
|
importLoading.value = true
|
||||||
|
const response = await fetch(downloadURL.value)
|
||||||
|
const jsonData = await response.json()
|
||||||
|
if ('key' in jsonData[0] && 'value' in jsonData[0])
|
||||||
|
tempPromptValue.value = JSON.stringify(jsonData)
|
||||||
|
if ('act' in jsonData[0] && 'prompt' in jsonData[0]) {
|
||||||
|
const newJsonData = jsonData.map((item: { act: string; prompt: string }) => {
|
||||||
|
return {
|
||||||
|
key: item.act,
|
||||||
|
value: item.prompt,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
tempPromptValue.value = JSON.stringify(newJsonData)
|
||||||
|
}
|
||||||
|
importPromptTemplate()
|
||||||
|
downloadURL.value = ''
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
message.error(t('store.downloadError'))
|
||||||
|
downloadURL.value = ''
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
importLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移动端自适应相关
|
||||||
|
const renderTemplate = () => {
|
||||||
|
const [keyLimit, valueLimit] = isMobile.value ? [10, 30] : [15, 50]
|
||||||
|
|
||||||
|
return promptList.value.map((item: { key: string; value: string }) => {
|
||||||
|
return {
|
||||||
|
renderKey: item.key.length <= keyLimit ? item.key : `${item.key.substring(0, keyLimit)}...`,
|
||||||
|
renderValue: item.value.length <= valueLimit ? item.value : `${item.value.substring(0, valueLimit)}...`,
|
||||||
|
key: item.key,
|
||||||
|
value: item.value,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const pagination = computed(() => {
|
||||||
|
const [pageSize, pageSlot] = isMobile.value ? [6, 5] : [7, 15]
|
||||||
|
return {
|
||||||
|
pageSize, pageSlot,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// table相关
|
||||||
|
const createColumns = (): DataTableColumns<DataProps> => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
title: t('store.title'),
|
||||||
|
key: 'renderKey',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('store.description'),
|
||||||
|
key: 'renderValue',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('common.action'),
|
||||||
|
key: 'actions',
|
||||||
|
width: 100,
|
||||||
|
align: 'center',
|
||||||
|
render(row) {
|
||||||
|
return h('div', { class: 'flex items-center flex-col gap-2' }, {
|
||||||
|
default: () => [h(
|
||||||
|
NButton,
|
||||||
|
{
|
||||||
|
tertiary: true,
|
||||||
|
size: 'small',
|
||||||
|
type: 'info',
|
||||||
|
onClick: () => changeShowModal('modify', row),
|
||||||
|
},
|
||||||
|
{ default: () => t('common.edit') },
|
||||||
|
),
|
||||||
|
h(
|
||||||
|
NButton,
|
||||||
|
{
|
||||||
|
tertiary: true,
|
||||||
|
size: 'small',
|
||||||
|
type: 'error',
|
||||||
|
onClick: () => deletePromptTemplate(row),
|
||||||
|
},
|
||||||
|
{ default: () => t('common.delete') },
|
||||||
|
),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = createColumns()
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => promptList,
|
||||||
|
() => {
|
||||||
|
promptStore.updatePromptList(promptList.value)
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
const dataSource = computed(() => {
|
||||||
|
const data = renderTemplate()
|
||||||
|
const value = searchValue.value
|
||||||
|
if (value && value !== '') {
|
||||||
|
return data.filter((item: DataProps) => {
|
||||||
|
return item.renderKey.includes(value) || item.renderValue.includes(value)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NModal v-model:show="show" style="width: 90%; max-width: 900px;" preset="card">
|
||||||
|
<div class="space-y-4">
|
||||||
|
<NTabs type="segment">
|
||||||
|
<NTabPane name="local" :tab="$t('store.local')">
|
||||||
|
<div
|
||||||
|
class="flex gap-3 mb-4"
|
||||||
|
:class="[isMobile ? 'flex-col' : 'flex-row justify-between']"
|
||||||
|
>
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<NButton
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
@click="changeShowModal('add')"
|
||||||
|
>
|
||||||
|
{{ $t('common.add') }}
|
||||||
|
</NButton>
|
||||||
|
<NButton
|
||||||
|
size="small"
|
||||||
|
@click="changeShowModal('local_import')"
|
||||||
|
>
|
||||||
|
{{ $t('common.import') }}
|
||||||
|
</NButton>
|
||||||
|
<NButton
|
||||||
|
size="small"
|
||||||
|
:loading="exportLoading"
|
||||||
|
@click="exportPromptTemplate()"
|
||||||
|
>
|
||||||
|
{{ $t('common.export') }}
|
||||||
|
</NButton>
|
||||||
|
<NPopconfirm @positive-click="clearPromptTemplate">
|
||||||
|
<template #trigger>
|
||||||
|
<NButton size="small">
|
||||||
|
{{ $t('common.clear') }}
|
||||||
|
</NButton>
|
||||||
|
</template>
|
||||||
|
{{ $t('store.clearStoreConfirm') }}
|
||||||
|
</NPopconfirm>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<NInput v-model:value="searchValue" style="width: 100%" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<NDataTable
|
||||||
|
v-if="!isMobile"
|
||||||
|
:max-height="400"
|
||||||
|
:columns="columns"
|
||||||
|
:data="dataSource"
|
||||||
|
:pagination="pagination"
|
||||||
|
:bordered="false"
|
||||||
|
/>
|
||||||
|
<NList v-if="isMobile" style="max-height: 400px; overflow-y: auto;">
|
||||||
|
<NListItem v-for="(item, index) of dataSource" :key="index">
|
||||||
|
<NThing :title="item.renderKey" :description="item.renderValue" />
|
||||||
|
<template #suffix>
|
||||||
|
<div class="flex flex-col items-center gap-2">
|
||||||
|
<NButton tertiary size="small" type="info" @click="changeShowModal('modify', item)">
|
||||||
|
{{ t('common.edit') }}
|
||||||
|
</NButton>
|
||||||
|
<NButton tertiary size="small" type="error" @click="deletePromptTemplate(item)">
|
||||||
|
{{ t('common.delete') }}
|
||||||
|
</NButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</NListItem>
|
||||||
|
</NList>
|
||||||
|
</NTabPane>
|
||||||
|
<NTabPane name="download" :tab="$t('store.online')">
|
||||||
|
<p class="mb-4">
|
||||||
|
{{ $t('store.onlineImportWarning') }}
|
||||||
|
</p>
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<NInput v-model:value="downloadURL" placeholder="" />
|
||||||
|
<NButton
|
||||||
|
strong
|
||||||
|
secondary
|
||||||
|
:disabled="downloadDisabled"
|
||||||
|
:loading="importLoading"
|
||||||
|
@click="downloadPromptTemplate()"
|
||||||
|
>
|
||||||
|
{{ $t('common.download') }}
|
||||||
|
</NButton>
|
||||||
|
</div>
|
||||||
|
<NDivider />
|
||||||
|
<div class="max-h-[360px] overflow-y-auto space-y-4">
|
||||||
|
<NCard
|
||||||
|
v-for="info in promptRecommendList"
|
||||||
|
:key="info.key" :title="info.key"
|
||||||
|
:bordered="true"
|
||||||
|
embedded
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
class="overflow-hidden text-ellipsis whitespace-nowrap"
|
||||||
|
:title="info.desc"
|
||||||
|
>
|
||||||
|
{{ info.desc }}
|
||||||
|
</p>
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex items-center justify-end space-x-4">
|
||||||
|
<NButton text>
|
||||||
|
<a
|
||||||
|
:href="info.url"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<SvgIcon class="text-xl" icon="ri:link" />
|
||||||
|
</a>
|
||||||
|
</NButton>
|
||||||
|
<NButton text @click="setDownloadURL(info.downloadUrl) ">
|
||||||
|
<SvgIcon class="text-xl" icon="ri:add-fill" />
|
||||||
|
</NButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</NCard>
|
||||||
|
</div>
|
||||||
|
</NTabPane>
|
||||||
|
</NTabs>
|
||||||
|
</div>
|
||||||
|
</NModal>
|
||||||
|
|
||||||
|
<NModal v-model:show="showModal" style="width: 90%; max-width: 600px;" preset="card">
|
||||||
|
<NSpace v-if="modalMode === 'add' || modalMode === 'modify'" vertical>
|
||||||
|
{{ t('store.title') }}
|
||||||
|
<NInput v-model:value="tempPromptKey" />
|
||||||
|
{{ t('store.description') }}
|
||||||
|
<NInput v-model:value="tempPromptValue" type="textarea" />
|
||||||
|
<NButton
|
||||||
|
block
|
||||||
|
type="primary"
|
||||||
|
:disabled="inputStatus"
|
||||||
|
@click="() => { modalMode === 'add' ? addPromptTemplate() : modifyPromptTemplate() }"
|
||||||
|
>
|
||||||
|
{{ t('common.confirm') }}
|
||||||
|
</NButton>
|
||||||
|
</NSpace>
|
||||||
|
<NSpace v-if="modalMode === 'local_import'" vertical>
|
||||||
|
<NInput
|
||||||
|
v-model:value="tempPromptValue"
|
||||||
|
:placeholder="t('store.importPlaceholder')"
|
||||||
|
:autosize="{ minRows: 3, maxRows: 15 }"
|
||||||
|
type="textarea"
|
||||||
|
/>
|
||||||
|
<NButton
|
||||||
|
block
|
||||||
|
type="primary"
|
||||||
|
:disabled="inputStatus"
|
||||||
|
@click="() => { importPromptTemplate('local') }"
|
||||||
|
>
|
||||||
|
{{ t('common.import') }}
|
||||||
|
</NButton>
|
||||||
|
</NSpace>
|
||||||
|
</NModal>
|
||||||
|
</template>
|
|
@ -0,0 +1,75 @@
|
||||||
|
<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 { useAuthStore } from '@/store'
|
||||||
|
|
||||||
|
interface ConfigState {
|
||||||
|
timeoutMs?: number
|
||||||
|
reverseProxy?: string
|
||||||
|
apiModel?: string
|
||||||
|
socksProxy?: string
|
||||||
|
httpsProxy?: string
|
||||||
|
usage?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const config = ref<ConfigState>()
|
||||||
|
|
||||||
|
const isChatGPTAPI = computed<boolean>(() => !!authStore.isChatGPTAPI)
|
||||||
|
|
||||||
|
async function fetchConfig() {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
const { data } = await fetchChatConfig<ConfigState>()
|
||||||
|
config.value = data
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchConfig()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NSpin :show="loading">
|
||||||
|
<div class="p-4 space-y-4">
|
||||||
|
<h2 class="text-xl font-bold">
|
||||||
|
Version - {{ pkg.version }}
|
||||||
|
</h2>
|
||||||
|
<div class="p-2 space-y-2 rounded-md bg-neutral-100 dark:bg-neutral-700">
|
||||||
|
<p>
|
||||||
|
此项目开源于
|
||||||
|
<a
|
||||||
|
class="text-blue-600 dark:text-blue-500"
|
||||||
|
href="https://github.com/Chanzhaoyu/chatgpt-web"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
GitHub
|
||||||
|
</a>
|
||||||
|
,免费且基于 MIT 协议,没有任何形式的付费行为!
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
如果你觉得此项目对你有帮助,请在 GitHub 帮我点个 Star 或者给予一点赞助,谢谢!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<p>{{ $t("setting.api") }}:{{ config?.apiModel ?? '-' }}</p>
|
||||||
|
<p v-if="isChatGPTAPI">
|
||||||
|
{{ $t("setting.monthlyUsage") }}:{{ config?.usage ?? '-' }}
|
||||||
|
</p>
|
||||||
|
<p v-if="!isChatGPTAPI">
|
||||||
|
{{ $t("setting.reverseProxy") }}:{{ config?.reverseProxy ?? '-' }}
|
||||||
|
</p>
|
||||||
|
<p>{{ $t("setting.timeout") }}:{{ config?.timeoutMs ?? '-' }}</p>
|
||||||
|
<p>{{ $t("setting.socks") }}:{{ config?.socksProxy ?? '-' }}</p>
|
||||||
|
<p>{{ $t("setting.httpsProxy") }}:{{ config?.httpsProxy ?? '-' }}</p>
|
||||||
|
</div>
|
||||||
|
</NSpin>
|
||||||
|
</template>
|
|
@ -0,0 +1,70 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { NButton, NInput, NSlider, useMessage } from 'naive-ui'
|
||||||
|
import { useSettingStore } from '@/store'
|
||||||
|
import type { SettingsState } from '@/store/modules/settings/helper'
|
||||||
|
import { t } from '@/locales'
|
||||||
|
|
||||||
|
const settingStore = useSettingStore()
|
||||||
|
|
||||||
|
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'))
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleReset() {
|
||||||
|
settingStore.resetSetting()
|
||||||
|
ms.success(t('common.success'))
|
||||||
|
window.location.reload()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<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-[120px]">{{ $t('setting.role') }}</span>
|
||||||
|
<div class="flex-1">
|
||||||
|
<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-[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]"> </span>
|
||||||
|
<NButton size="small" @click="handleReset">
|
||||||
|
{{ $t('common.reset') }}
|
||||||
|
</NButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -0,0 +1,225 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import { NButton, NInput, NPopconfirm, NSelect, useMessage } from 'naive-ui'
|
||||||
|
import type { Language, Theme } from '@/store/modules/app/helper'
|
||||||
|
import { SvgIcon } from '@/components/common'
|
||||||
|
import { useAppStore, useUserStore } from '@/store'
|
||||||
|
import type { UserInfo } from '@/store/modules/user/helper'
|
||||||
|
import { getCurrentDate } from '@/utils/functions'
|
||||||
|
import { useBasicLayout } from '@/hooks/useBasicLayout'
|
||||||
|
import { t } from '@/locales'
|
||||||
|
|
||||||
|
const appStore = useAppStore()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
const { isMobile } = useBasicLayout()
|
||||||
|
|
||||||
|
const ms = useMessage()
|
||||||
|
|
||||||
|
const theme = computed(() => appStore.theme)
|
||||||
|
|
||||||
|
const userInfo = computed(() => userStore.userInfo)
|
||||||
|
|
||||||
|
const avatar = ref(userInfo.value.avatar ?? '')
|
||||||
|
|
||||||
|
const name = ref(userInfo.value.name ?? '')
|
||||||
|
|
||||||
|
const description = ref(userInfo.value.description ?? '')
|
||||||
|
|
||||||
|
const language = computed({
|
||||||
|
get() {
|
||||||
|
return appStore.language
|
||||||
|
},
|
||||||
|
set(value: Language) {
|
||||||
|
appStore.setLanguage(value)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const themeOptions: { label: string; key: Theme; icon: string }[] = [
|
||||||
|
{
|
||||||
|
label: 'Auto',
|
||||||
|
key: 'auto',
|
||||||
|
icon: 'ri:contrast-line',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Light',
|
||||||
|
key: 'light',
|
||||||
|
icon: 'ri:sun-foggy-line',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Dark',
|
||||||
|
key: 'dark',
|
||||||
|
icon: 'ri:moon-foggy-line',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const languageOptions: { label: string; key: Language; value: Language }[] = [
|
||||||
|
{ label: '简体中文', key: 'zh-CN', value: 'zh-CN' },
|
||||||
|
{ label: '繁體中文', key: 'zh-TW', value: 'zh-TW' },
|
||||||
|
{ 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>) {
|
||||||
|
userStore.updateUserInfo(options)
|
||||||
|
ms.success(t('common.success'))
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleReset() {
|
||||||
|
userStore.resetUserInfo()
|
||||||
|
ms.success(t('common.success'))
|
||||||
|
window.location.reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportData(): void {
|
||||||
|
const date = getCurrentDate()
|
||||||
|
const data: string = localStorage.getItem('chatStorage') || '{}'
|
||||||
|
const jsonString: string = JSON.stringify(JSON.parse(data), null, 2)
|
||||||
|
const blob: Blob = new Blob([jsonString], { type: 'application/json' })
|
||||||
|
const url: string = URL.createObjectURL(blob)
|
||||||
|
const link: HTMLAnchorElement = document.createElement('a')
|
||||||
|
link.href = url
|
||||||
|
link.download = `chat-store_${date}.json`
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
document.body.removeChild(link)
|
||||||
|
}
|
||||||
|
|
||||||
|
function importData(event: Event): void {
|
||||||
|
const target = event.target as HTMLInputElement
|
||||||
|
if (!target || !target.files)
|
||||||
|
return
|
||||||
|
|
||||||
|
const file: File = target.files[0]
|
||||||
|
if (!file)
|
||||||
|
return
|
||||||
|
|
||||||
|
const reader: FileReader = new FileReader()
|
||||||
|
reader.onload = () => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(reader.result as string)
|
||||||
|
localStorage.setItem('chatStorage', JSON.stringify(data))
|
||||||
|
ms.success(t('common.success'))
|
||||||
|
location.reload()
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
ms.error(t('common.invalidFileFormat'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reader.readAsText(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearData(): void {
|
||||||
|
localStorage.removeItem('chatStorage')
|
||||||
|
location.reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleImportButtonClick(): void {
|
||||||
|
const fileInput = document.getElementById('fileInput') as HTMLElement
|
||||||
|
if (fileInput)
|
||||||
|
fileInput.click()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<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.avatarLink') }}</span>
|
||||||
|
<div class="flex-1">
|
||||||
|
<NInput v-model:value="avatar" placeholder="" />
|
||||||
|
</div>
|
||||||
|
<NButton size="tiny" text type="primary" @click="updateUserInfo({ avatar })">
|
||||||
|
{{ $t('common.save') }}
|
||||||
|
</NButton>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.name') }}</span>
|
||||||
|
<div class="w-[200px]">
|
||||||
|
<NInput v-model:value="name" placeholder="" />
|
||||||
|
</div>
|
||||||
|
<NButton size="tiny" text type="primary" @click="updateUserInfo({ name })">
|
||||||
|
{{ $t('common.save') }}
|
||||||
|
</NButton>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.description') }}</span>
|
||||||
|
<div class="flex-1">
|
||||||
|
<NInput v-model:value="description" placeholder="" />
|
||||||
|
</div>
|
||||||
|
<NButton size="tiny" text type="primary" @click="updateUserInfo({ description })">
|
||||||
|
{{ $t('common.save') }}
|
||||||
|
</NButton>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="flex items-center space-x-4"
|
||||||
|
:class="isMobile && 'items-start'"
|
||||||
|
>
|
||||||
|
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.chatHistory') }}</span>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap items-center gap-4">
|
||||||
|
<NButton size="small" @click="exportData">
|
||||||
|
<template #icon>
|
||||||
|
<SvgIcon icon="ri:download-2-fill" />
|
||||||
|
</template>
|
||||||
|
{{ $t('common.export') }}
|
||||||
|
</NButton>
|
||||||
|
|
||||||
|
<input id="fileInput" type="file" style="display:none" @change="importData">
|
||||||
|
<NButton size="small" @click="handleImportButtonClick">
|
||||||
|
<template #icon>
|
||||||
|
<SvgIcon icon="ri:upload-2-fill" />
|
||||||
|
</template>
|
||||||
|
{{ $t('common.import') }}
|
||||||
|
</NButton>
|
||||||
|
|
||||||
|
<NPopconfirm placement="bottom" @positive-click="clearData">
|
||||||
|
<template #trigger>
|
||||||
|
<NButton size="small">
|
||||||
|
<template #icon>
|
||||||
|
<SvgIcon icon="ri:close-circle-line" />
|
||||||
|
</template>
|
||||||
|
{{ $t('common.clear') }}
|
||||||
|
</NButton>
|
||||||
|
</template>
|
||||||
|
{{ $t('chat.clearHistoryConfirm') }}
|
||||||
|
</NPopconfirm>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.theme') }}</span>
|
||||||
|
<div class="flex flex-wrap items-center gap-4">
|
||||||
|
<template v-for="item of themeOptions" :key="item.key">
|
||||||
|
<NButton
|
||||||
|
size="small"
|
||||||
|
:type="item.key === theme ? 'primary' : undefined"
|
||||||
|
@click="appStore.setTheme(item.key)"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<SvgIcon :icon="item.icon" />
|
||||||
|
</template>
|
||||||
|
</NButton>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.language') }}</span>
|
||||||
|
<div class="flex flex-wrap items-center gap-4">
|
||||||
|
<NSelect
|
||||||
|
style="width: 140px"
|
||||||
|
:value="language"
|
||||||
|
:options="languageOptions"
|
||||||
|
@update-value="value => appStore.setLanguage(value)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.resetUserInfo') }}</span>
|
||||||
|
<NButton size="small" @click="handleReset">
|
||||||
|
{{ $t('common.reset') }}
|
||||||
|
</NButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -0,0 +1,70 @@
|
||||||
|
<script setup lang='ts'>
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import { NModal, NTabPane, NTabs } from 'naive-ui'
|
||||||
|
import General from './General.vue'
|
||||||
|
import Advanced from './Advanced.vue'
|
||||||
|
import About from './About.vue'
|
||||||
|
import { useAuthStore } from '@/store'
|
||||||
|
import { SvgIcon } from '@/components/common'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
visible: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Emit {
|
||||||
|
(e: 'update:visible', visible: boolean): void
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
|
const emit = defineEmits<Emit>()
|
||||||
|
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
const isChatGPTAPI = computed<boolean>(() => !!authStore.isChatGPTAPI)
|
||||||
|
|
||||||
|
const active = ref('General')
|
||||||
|
|
||||||
|
const show = computed({
|
||||||
|
get() {
|
||||||
|
return props.visible
|
||||||
|
},
|
||||||
|
set(visible: boolean) {
|
||||||
|
emit('update:visible', visible)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NModal v-model:show="show" :auto-focus="false" preset="card" style="width: 95%; max-width: 640px">
|
||||||
|
<div>
|
||||||
|
<NTabs v-model:value="active" type="line" animated>
|
||||||
|
<NTabPane name="General" tab="General">
|
||||||
|
<template #tab>
|
||||||
|
<SvgIcon class="text-lg" icon="ri:file-user-line" />
|
||||||
|
<span class="ml-2">{{ $t('setting.general') }}</span>
|
||||||
|
</template>
|
||||||
|
<div class="min-h-[100px]">
|
||||||
|
<General />
|
||||||
|
</div>
|
||||||
|
</NTabPane>
|
||||||
|
<NTabPane v-if="isChatGPTAPI" name="Advanced" tab="Advanced">
|
||||||
|
<template #tab>
|
||||||
|
<SvgIcon class="text-lg" icon="ri:equalizer-line" />
|
||||||
|
<span class="ml-2">{{ $t('setting.advanced') }}</span>
|
||||||
|
</template>
|
||||||
|
<div class="min-h-[100px]">
|
||||||
|
<Advanced />
|
||||||
|
</div>
|
||||||
|
</NTabPane>
|
||||||
|
<NTabPane name="Config" tab="Config">
|
||||||
|
<template #tab>
|
||||||
|
<SvgIcon class="text-lg" icon="ri:list-settings-line" />
|
||||||
|
<span class="ml-2">{{ $t('setting.config') }}</span>
|
||||||
|
</template>
|
||||||
|
<About />
|
||||||
|
</NTabPane>
|
||||||
|
</NTabs>
|
||||||
|
</div>
|
||||||
|
</NModal>
|
||||||
|
</template>
|
|
@ -0,0 +1,21 @@
|
||||||
|
<script setup lang='ts'>
|
||||||
|
import { computed, useAttrs } from 'vue'
|
||||||
|
import { Icon } from '@iconify/vue'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
icon?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>()
|
||||||
|
|
||||||
|
const attrs = useAttrs()
|
||||||
|
|
||||||
|
const bindAttrs = computed<{ class: string; style: string }>(() => ({
|
||||||
|
class: (attrs.class as string) || '',
|
||||||
|
style: (attrs.style as string) || '',
|
||||||
|
}))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Icon :icon="icon??''" v-bind="bindAttrs" />
|
||||||
|
</template>
|
|
@ -0,0 +1,40 @@
|
||||||
|
<script setup lang='ts'>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { NAvatar } from 'naive-ui'
|
||||||
|
import { useUserStore } from '@/store'
|
||||||
|
import defaultAvatar from '@/assets/avatar.jpg'
|
||||||
|
import { isString } from '@/utils/is'
|
||||||
|
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
const userInfo = computed(() => userStore.userInfo)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex items-center overflow-hidden">
|
||||||
|
<div class="w-10 h-10 overflow-hidden rounded-full shrink-0">
|
||||||
|
<template v-if="isString(userInfo.avatar) && userInfo.avatar.length > 0">
|
||||||
|
<NAvatar
|
||||||
|
size="large"
|
||||||
|
round
|
||||||
|
:src="userInfo.avatar"
|
||||||
|
:fallback-src="defaultAvatar"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<NAvatar size="large" round :src="defaultAvatar" />
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-0 ml-2">
|
||||||
|
<h2 class="overflow-hidden font-bold text-md text-ellipsis whitespace-nowrap">
|
||||||
|
{{ userInfo.name ?? 'ChenZhaoYu' }}
|
||||||
|
</h2>
|
||||||
|
<p class="overflow-hidden text-xs text-gray-500 text-ellipsis whitespace-nowrap">
|
||||||
|
<span
|
||||||
|
v-if="isString(userInfo.description) && userInfo.description !== ''"
|
||||||
|
v-html="userInfo.description"
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -0,0 +1,8 @@
|
||||||
|
import HoverButton from './HoverButton/index.vue'
|
||||||
|
import NaiveProvider from './NaiveProvider/index.vue'
|
||||||
|
import SvgIcon from './SvgIcon/index.vue'
|
||||||
|
import UserAvatar from './UserAvatar/index.vue'
|
||||||
|
import Setting from './Setting/index.vue'
|
||||||
|
import PromptStore from './PromptStore/index.vue'
|
||||||
|
|
||||||
|
export { HoverButton, NaiveProvider, SvgIcon, UserAvatar, Setting, PromptStore }
|
|
@ -0,0 +1,8 @@
|
||||||
|
<template>
|
||||||
|
<div class="text-neutral-400">
|
||||||
|
<span>Star on</span>
|
||||||
|
<a href="https://github.com/Chanzhaoyu/chatgpt-bot" target="_blank" class="text-blue-500">
|
||||||
|
GitHub
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -0,0 +1,3 @@
|
||||||
|
import GithubSite from './GithubSite.vue'
|
||||||
|
|
||||||
|
export { GithubSite }
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core'
|
||||||
|
|
||||||
|
export function useBasicLayout() {
|
||||||
|
const breakpoints = useBreakpoints(breakpointsTailwind)
|
||||||
|
const isMobile = breakpoints.smaller('sm')
|
||||||
|
|
||||||
|
return { isMobile }
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { h } from 'vue'
|
||||||
|
import { SvgIcon } from '@/components/common'
|
||||||
|
|
||||||
|
export const useIconRender = () => {
|
||||||
|
interface IconConfig {
|
||||||
|
icon?: string
|
||||||
|
color?: string
|
||||||
|
fontSize?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IconStyle {
|
||||||
|
color?: string
|
||||||
|
fontSize?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const iconRender = (config: IconConfig) => {
|
||||||
|
const { color, fontSize, icon } = config
|
||||||
|
|
||||||
|
const style: IconStyle = {}
|
||||||
|
|
||||||
|
if (color)
|
||||||
|
style.color = color
|
||||||
|
|
||||||
|
if (fontSize)
|
||||||
|
style.fontSize = `${fontSize}px`
|
||||||
|
|
||||||
|
if (!icon)
|
||||||
|
window.console.warn('iconRender: icon is required')
|
||||||
|
|
||||||
|
return () => h(SvgIcon, { icon, style })
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
iconRender,
|
||||||
|
}
|
||||||
|
}
|