├── .commitlintrc.json ├── .dockerignore ├── .editorconfig ├── .env.example ├── .eslintignore ├── .eslintrc.cjs ├── .gitattributes ├── .gitignore ├── .npmrc ├── CHANGELOG.md ├── CONTRIBUTING.en.md ├── CONTRIBUTING.md ├── Dockerfile ├── README.en.md ├── README.md ├── docker-compose ├── docker-compose.yml ├── nginx │ └── nginx.conf └── readme.md ├── docs ├── alipay.png ├── c1-2.8.0.png ├── c1-2.9.0.png ├── c1.png ├── c2-2.8.0.png ├── c2-2.9.0.png ├── c2.png ├── docker.png ├── schema.prisma └── wechat.png ├── index.html ├── license ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── public ├── favicon.ico ├── favicon.svg ├── pwa-192x192.png └── pwa-512x512.png ├── server ├── .env ├── custom-ts │ ├── .vscode │ │ └── launch.json │ ├── auth │ │ ├── mutatingPostAuthentication.ts │ │ ├── postAuthentication.ts │ │ ├── postLogout.ts │ │ └── revalidate.ts │ ├── fireboom.server.ts │ ├── global │ │ ├── onConnectionInit.ts │ │ ├── onRequest.ts │ │ └── onResponse.ts │ ├── nodemon.json │ ├── nohup.out │ ├── operations.tsconfig.json │ ├── operations │ │ └── Chat │ │ │ └── ChatSSE.ts │ ├── package.json │ ├── pnpm-lock.yaml │ ├── scripts │ │ ├── buildOperations.ts │ │ ├── install.sh │ │ ├── run-dev.sh │ │ └── run-prod.sh │ └── tsconfig.json ├── exported │ └── operations │ │ ├── Chat │ │ ├── CreateOne.graphql │ │ ├── DeleteOne.graphql │ │ ├── GetByHistory.graphql │ │ ├── GetMyHistoryChats.graphql │ │ └── UpdateChatText.graphql │ │ ├── History │ │ ├── CreateOne.graphql │ │ ├── DeleteOne.graphql │ │ ├── GetList.graphql │ │ └── UpdateOne.graphql │ │ ├── Propmt │ │ ├── CreateOne.graphql │ │ ├── DeleteMany.graphql │ │ ├── DeleteOne.graphql │ │ ├── GetList.graphql │ │ └── UpdateOne.graphql │ │ ├── System │ │ ├── BindRoleApis.graphql │ │ └── GetRoleBindApis.graphql │ │ └── User │ │ ├── CreateOneUser.graphql │ │ ├── GetOneUser.graphql │ │ ├── Me.graphql │ │ └── UpdateInfo.graphql ├── master.tar.gz ├── run.sh ├── store │ ├── hooks │ │ ├── auth │ │ │ ├── mutatingPostAuthentication.config.json │ │ │ ├── postAuthentication.config.json │ │ │ ├── postLogout.config.json │ │ │ └── revalidate.config.json │ │ ├── customize │ │ │ └── chatGPT.config.json │ │ └── global │ │ │ ├── onConnectionInit.config.json │ │ │ ├── onRequest.config.json │ │ │ └── onResponse.config.json │ ├── list │ │ ├── FbAuthentication │ │ ├── FbDataSource │ │ ├── FbOperation │ │ ├── FbRole │ │ └── FbStorageBucket │ └── object │ │ ├── global_operation_config.json │ │ └── operations │ │ ├── Chat │ │ ├── CreateOne.json │ │ ├── DeleteOne.json │ │ ├── GetByHistory.json │ │ ├── GetMyHistoryChats.json │ │ └── UpdateChatText.json │ │ ├── History │ │ ├── CreateOne.json │ │ ├── DeleteOne.json │ │ ├── GetList.json │ │ └── UpdateOne.json │ │ ├── Propmt │ │ ├── CreateOne.json │ │ ├── DeleteMany.json │ │ ├── DeleteOne.json │ │ ├── GetList.json │ │ └── UpdateOne.json │ │ └── User │ │ ├── CreateOneUser.json │ │ ├── GetOneUser.json │ │ ├── Me.json │ │ └── UpdateInfo.json └── template │ └── vue │ ├── files │ ├── claims.ts.hbs │ ├── client.ts.hbs │ └── models.ts.hbs │ ├── manifest.json │ └── partials │ ├── operation_partial.hbs │ └── schema_partial.hbs ├── src ├── App.vue ├── api │ └── index.ts ├── assets │ ├── avatar.jpg │ └── recommend.json ├── components │ ├── common │ │ ├── HoverButton │ │ │ ├── Button.vue │ │ │ └── index.vue │ │ ├── NaiveProvider │ │ │ └── index.vue │ │ ├── PromptStore │ │ │ └── index.vue │ │ ├── Setting │ │ │ ├── About.vue │ │ │ ├── General.vue │ │ │ └── index.vue │ │ ├── SvgIcon │ │ │ └── index.vue │ │ ├── UserAvatar │ │ │ └── index.vue │ │ └── index.ts │ └── custom │ │ ├── GithubSite.vue │ │ └── index.ts ├── hooks │ ├── useBasicLayout.ts │ ├── useIconRender.ts │ ├── useLanguage.ts │ └── useTheme.ts ├── icons │ ├── 403.vue │ ├── 404.svg │ └── 500.vue ├── locales │ ├── en-US.ts │ ├── index.ts │ ├── zh-CN.ts │ └── zh-TW.ts ├── main.ts ├── plugins │ ├── assets.ts │ ├── index.ts │ └── scrollbarStyle.ts ├── router │ ├── index.ts │ └── permission.ts ├── services │ ├── claims.ts │ ├── client.ts │ ├── index.ts │ ├── models.ts │ └── sse.ts ├── store │ ├── index.ts │ └── modules │ │ ├── app │ │ ├── helper.ts │ │ └── index.ts │ │ ├── auth │ │ ├── helper.ts │ │ └── index.ts │ │ ├── chat │ │ ├── helper.ts │ │ └── index.ts │ │ ├── index.ts │ │ ├── prompt │ │ ├── helper.ts │ │ └── index.ts │ │ ├── speak │ │ └── index.ts │ │ └── user │ │ ├── helper.ts │ │ └── index.ts ├── styles │ ├── global.less │ └── lib │ │ ├── github-markdown.less │ │ ├── highlight.less │ │ └── tailwind.css ├── typings │ ├── chat.d.ts │ ├── env.d.ts │ └── global.d.ts ├── utils │ ├── crypto │ │ └── index.ts │ ├── format │ │ └── index.ts │ ├── functions │ │ └── index.ts │ ├── is │ │ └── index.ts │ ├── request │ │ ├── axios.ts │ │ └── index.ts │ ├── speak.ts │ └── storage │ │ ├── index.ts │ │ └── local.ts └── views │ ├── chat │ ├── components │ │ ├── Header │ │ │ └── index.vue │ │ ├── Message │ │ │ ├── Avatar.vue │ │ │ ├── Text.vue │ │ │ ├── index.vue │ │ │ └── style.less │ │ ├── VoiceInput │ │ │ └── index.vue │ │ └── index.ts │ ├── hooks │ │ ├── useChat.ts │ │ ├── useCopyCode.ts │ │ ├── useScroll.ts │ │ └── useUsingContext.ts │ ├── index.vue │ └── layout │ │ ├── Layout.vue │ │ ├── Permission.vue │ │ ├── index.ts │ │ └── sider │ │ ├── Footer.vue │ │ ├── List.vue │ │ └── index.vue │ └── exception │ ├── 404 │ └── index.vue │ └── 500 │ └── index.vue ├── start.sh ├── tailwind.config.js ├── tsconfig.json └── vite.config.ts /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@commitlint/config-conventional"] 3 | } 4 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | */node_modules 3 | node_modules 4 | Dockerfile 5 | .* 6 | */.* 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Glob API URL 2 | VITE_APP_API_BASE_URL=http://localhost:9991 3 | 4 | # Whether long replies are supported, which may result in higher API fees 5 | VITE_GLOB_OPEN_LONG_REPLY=false 6 | VITE_SPEECH_KEY=xxx 7 | VITE_SPEECH_REGION=xxx 8 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | server 2 | src/services 3 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ['@antfu'], 4 | } 5 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | "*.vue" eol=lf 2 | "*.js" eol=lf 3 | "*.ts" eol=lf 4 | "*.jsx" eol=lf 5 | "*.tsx" eol=lf 6 | "*.cjs" eol=lf 7 | "*.cts" eol=lf 8 | "*.mjs" eol=lf 9 | "*.mts" eol=lf 10 | "*.json" eol=lf 11 | "*.html" eol=lf 12 | "*.css" eol=lf 13 | "*.less" eol=lf 14 | "*.scss" eol=lf 15 | "*.sass" eol=lf 16 | "*.styl" eol=lf 17 | "*.md" eol=lf 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-ssr 14 | coverage 15 | *.local 16 | 17 | /cypress/videos/ 18 | /cypress/screenshots/ 19 | 20 | # Editor directories and files 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/extensions.json 24 | .idea 25 | *.suo 26 | *.ntvs* 27 | *.njsproj 28 | *.sln 29 | *.sw? 30 | 31 | # Environment variables files 32 | .env 33 | node_modules 34 | dist 35 | fireboom 36 | server/log 37 | server/custom-ts/**/*.js 38 | server/.env 39 | server/upload 40 | server/.process 41 | server/exported/* 42 | !server/exported/operations 43 | server/store/object/global_config.json 44 | server/store/object/global_system_config.json 45 | generated/ 46 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | strict-peer-dependencies=false 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.en.md: -------------------------------------------------------------------------------- 1 | # Contribution Guide 2 | 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. 3 | 4 | ## Semantic Versioning 5 | 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. 6 | 7 | Each major change will be recorded in the `changelog`. 8 | 9 | ## Submitting Pull Request 10 | 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. 11 | 2. Install the `pnpm` tool using `npm install pnpm -g`. 12 | 3. Install the `Eslint` plugin for `VSCode`, or enable `eslint` functionality for other editors such as `WebStorm`. 13 | 4. Execute `pnpm bootstrap` in the root directory. 14 | 5. Execute `pnpm install` in the `/service/` directory. 15 | 6. Make changes to the codebase. If applicable, ensure that appropriate testing has been done. 16 | 7. Execute `pnpm lint:fix` in the root directory to perform a code formatting check. 17 | 8. Execute `pnpm type-check` in the root directory to perform a type check. 18 | 9. Submit a git commit, following the [Commit Guidelines](#commit-guidelines). 19 | 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). 20 | 21 | ## Commit Guidelines 22 | 23 | Commit messages should follow the [conventional-changelog standard](https://www.conventionalcommits.org/en/v1.0.0/): 24 | 25 | ```bash 26 | [optional scope]: 27 | 28 | [optional body] 29 | 30 | [optional footer] 31 | ``` 32 | 33 | ### Commit Types 34 | 35 | The following is a list of commit types: 36 | 37 | - feat: New feature or functionality 38 | - fix: Bug fix 39 | - docs: Documentation update 40 | - style: Code style or component style update 41 | - refactor: Code refactoring, no new features or bug fixes introduced 42 | - perf: Performance optimization 43 | - test: Unit test 44 | - chore: Other commits that do not modify src or test files 45 | 46 | 47 | ## License 48 | 49 | [MIT](./license) -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # 贡献指南 2 | 感谢你的宝贵时间。你的贡献将使这个项目变得更好!在提交贡献之前,请务必花点时间阅读下面的入门指南。 3 | 4 | ## 语义化版本 5 | 该项目遵循语义化版本。我们对重要的漏洞修复发布修订号,对新特性或不重要的变更发布次版本号,对重大且不兼容的变更发布主版本号。 6 | 7 | 每个重大更改都将记录在 `changelog` 中。 8 | 9 | ## 提交 Pull Request 10 | 1. Fork [此仓库](https://github.com/Chanzhaoyu/chatgpt-web),从 `main` 创建分支。新功能实现请发 pull request 到 `feature` 分支。其他更改发到 `main` 分支。 11 | 2. 使用 `npm install pnpm -g` 安装 `pnpm` 工具。 12 | 3. `vscode` 安装了 `Eslint` 插件,其它编辑器如 `webStorm` 打开了 `eslint` 功能。 13 | 4. 根目录下执行 `pnpm bootstrap`。 14 | 5. `/service/` 目录下执行 `pnpm install`。 15 | 6. 对代码库进行更改。如果适用的话,请确保进行了相应的测试。 16 | 7. 请在根目录下执行 `pnpm lint:fix` 进行代码格式检查。 17 | 8. 请在根目录下执行 `pnpm type-check` 进行类型检查。 18 | 9. 提交 git commit, 请同时遵守 [Commit 规范](#commit-指南) 19 | 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)。 20 | 21 | ## Commit 指南 22 | 23 | Commit messages 请遵循[conventional-changelog 标准](https://www.conventionalcommits.org/en/v1.0.0/): 24 | 25 | ```bash 26 | <类型>[可选 范围]: <描述> 27 | 28 | [可选 正文] 29 | 30 | [可选 脚注] 31 | ``` 32 | 33 | ### Commit 类型 34 | 35 | 以下是 commit 类型列表: 36 | 37 | - feat: 新特性或功能 38 | - fix: 缺陷修复 39 | - docs: 文档更新 40 | - style: 代码风格或者组件样式更新 41 | - refactor: 代码重构,不引入新功能和缺陷修复 42 | - perf: 性能优化 43 | - test: 单元测试 44 | - chore: 其他不修改 src 或测试文件的提交 45 | 46 | 47 | ## License 48 | 49 | [MIT](./license) 50 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # build front-end 2 | FROM node:lts-alpine AS frontend 3 | 4 | RUN npm install pnpm -g 5 | 6 | WORKDIR /app 7 | 8 | COPY ./package.json /app 9 | 10 | COPY ./pnpm-lock.yaml /app 11 | 12 | RUN pnpm install 13 | 14 | COPY . /app 15 | 16 | RUN pnpm run build 17 | 18 | # build backend 19 | FROM node:lts-alpine as backend 20 | 21 | RUN npm install pnpm -g 22 | 23 | WORKDIR /app 24 | 25 | COPY /service/package.json /app 26 | 27 | COPY /service/pnpm-lock.yaml /app 28 | 29 | RUN pnpm install 30 | 31 | COPY /service /app 32 | 33 | RUN pnpm build 34 | 35 | # service 36 | FROM node:lts-alpine 37 | 38 | RUN npm install pnpm -g 39 | 40 | WORKDIR /app 41 | 42 | COPY /service/package.json /app 43 | 44 | COPY /service/pnpm-lock.yaml /app 45 | 46 | RUN pnpm install --production && rm -rf /root/.npm /root/.pnpm-store /usr/local/share/.cache /tmp/* 47 | 48 | COPY /service /app 49 | 50 | COPY --from=frontend /app/dist /app/public 51 | 52 | COPY --from=backend /app/build /app/build 53 | 54 | EXPOSE 3002 55 | 56 | CMD ["pnpm", "run", "prod"] 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ChatGPT Web 2 | 3 |
4 | 中文 | 5 | English 6 |
7 |
8 | 9 | > 声明:此项目只发布于 Github,基于 MIT 协议,免费且作为开源学习使用。并且不会有任何形式的卖号、付费服务、讨论群、讨论组等行为。谨防受骗。 10 | 11 | ![cover](./docs/c1.png) 12 | ![cover2](./docs/c2.png) 13 | 14 | - [ChatGPT Web](#chatgpt-web) 15 | - [介绍](#介绍) 16 | - [待实现路线](#待实现路线) 17 | - [前置要求](#前置要求) 18 | - [Node](#node) 19 | - [PNPM](#pnpm) 20 | - [填写密钥](#填写密钥) 21 | - [安装依赖](#安装依赖) 22 | - [后端](#后端) 23 | - [前端](#前端) 24 | - [测试环境运行](#测试环境运行) 25 | - [后端服务](#后端服务) 26 | - [前端网页](#前端网页) 27 | - [环境变量](#环境变量) 28 | - [打包](#打包) 29 | - [使用 Docker](#使用-docker) 30 | - [Docker 参数示例](#docker-参数示例) 31 | - [Docker build \& Run](#docker-build--run) 32 | - [Docker compose](#docker-compose) 33 | - [使用 Railway 部署](#使用-railway-部署) 34 | - [Railway 环境变量](#railway-环境变量) 35 | - [手动打包](#手动打包) 36 | - [后端服务](#后端服务-1) 37 | - [前端网页](#前端网页-1) 38 | - [常见问题](#常见问题) 39 | - [参与贡献](#参与贡献) 40 | - [赞助](#赞助) 41 | - [License](#license) 42 | ## 介绍 43 | 44 | 支持双模型,提供了两种非官方 `ChatGPT API` 方法 45 | 46 | | 方式 | 免费? | 可靠性 | 质量 | 47 | | --------------------------------------------- | ------ | ---------- | ---- | 48 | | `ChatGPTAPI(gpt-3.5-turbo-0301)` | 否 | 可靠 | 相对较笨 | 49 | | `ChatGPTUnofficialProxyAPI(网页 accessToken)` | 是 | 相对不可靠 | 聪明 | 50 | 51 | 对比: 52 | 1. `ChatGPTAPI` 使用 `gpt-3.5-turbo-0301` 通过官方`OpenAI`补全`API`模拟`ChatGPT`(最稳健的方法,但它不是免费的,并且没有使用针对聊天进行微调的模型) 53 | 2. `ChatGPTUnofficialProxyAPI` 使用非官方代理服务器访问 `ChatGPT` 的后端`API`,绕过`Cloudflare`(使用真实的的`ChatGPT`,非常轻量级,但依赖于第三方服务器,并且有速率限制) 54 | 55 | 警告: 56 | 1. 你应该首先使用 `API` 方式 57 | 2. 使用 `API` 时,如果网络不通,那是国内被墙了,你需要自建代理,绝对不要使用别人的公开代理,那是危险的。 58 | 3. 使用 `accessToken` 方式时反向代理将向第三方暴露您的访问令牌,这样做应该不会产生任何不良影响,但在使用这种方法之前请考虑风险。 59 | 4. 使用 `accessToken` 时,不管你是国内还是国外的机器,都会使用代理。默认代理为 [acheong08](https://github.com/acheong08) 大佬的 `https://bypass.duti.tech/api/conversation`,这不是后门也不是监听,除非你有能力自己翻过 `CF` 验证,用前请知悉。[社区代理](https://github.com/transitive-bullshit/chatgpt-api#reverse-proxy)(注意:只有这两个是推荐,其他第三方来源,请自行甄别) 60 | 5. 把项目发布到公共网络时,你应该设置 `AUTH_SECRET_KEY` 变量添加你的密码访问权限,你也应该修改 `index.html` 中的 `title`,防止被关键词搜索到。 61 | 62 | 切换方式: 63 | 1. 进入 `service/.env.example` 文件,复制内容到 `service/.env` 文件 64 | 2. 使用 `OpenAI API Key` 请填写 `OPENAI_API_KEY` 字段 [(获取 apiKey)](https://platform.openai.com/overview) 65 | 3. 使用 `Web API` 请填写 `OPENAI_ACCESS_TOKEN` 字段 [(获取 accessToken)](https://chat.openai.com/api/auth/session) 66 | 4. 同时存在时以 `OpenAI API Key` 优先 67 | 68 | 环境变量: 69 | 70 | 全部参数变量请查看或[这里](#环境变量) 71 | 72 | ``` 73 | /service/.env.example 74 | ``` 75 | 76 | ## 待实现路线 77 | [✓] 双模型 78 | 79 | [✓] 多会话储存和上下文逻辑 80 | 81 | [✓] 对代码等消息类型的格式化美化处理 82 | 83 | [✓] 访问权限控制 84 | 85 | [✓] 数据导入、导出 86 | 87 | [✓] 保存消息到本地图片 88 | 89 | [✓] 界面多语言 90 | 91 | [✓] 界面主题 92 | 93 | [✗] More... 94 | 95 | ## 前置要求 96 | 97 | ### Node 98 | 99 | `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` 版本 100 | 101 | ```shell 102 | node -v 103 | ``` 104 | 105 | ### PNPM 106 | 如果你没有安装过 `pnpm` 107 | ```shell 108 | npm install pnpm -g 109 | ``` 110 | 111 | ### 填写密钥 112 | 获取 `Openai Api Key` 或 `accessToken` 并填写本地环境变量 [跳转](#介绍) 113 | 114 | ``` 115 | # service/.env 文件 116 | 117 | # OpenAI API Key - https://platform.openai.com/overview 118 | OPENAI_API_KEY= 119 | 120 | # change this to an `accessToken` extracted from the ChatGPT site's `https://chat.openai.com/api/auth/session` response 121 | OPENAI_ACCESS_TOKEN= 122 | ``` 123 | 124 | ## 安装依赖 125 | 126 | > 为了简便 `后端开发人员` 的了解负担,所以并没有采用前端 `workspace` 模式,而是分文件夹存放。如果只需要前端页面做二次开发,删除 `service` 文件夹即可。 127 | 128 | ### 后端 129 | 130 | 进入文件夹 `/service` 运行以下命令 131 | 132 | ```shell 133 | pnpm install 134 | ``` 135 | 136 | ### 前端 137 | 根目录下运行以下命令 138 | ```shell 139 | pnpm bootstrap 140 | ``` 141 | 142 | ## 测试环境运行 143 | ### 后端服务 144 | 145 | 进入文件夹 `/service` 运行以下命令 146 | 147 | ```shell 148 | pnpm start 149 | ``` 150 | 151 | ### 前端网页 152 | 根目录下运行以下命令 153 | ```shell 154 | pnpm dev 155 | ``` 156 | 157 | ## 环境变量 158 | 159 | `API` 可用: 160 | 161 | - `OPENAI_API_KEY` 和 `OPENAI_ACCESS_TOKEN` 二选一 162 | - `OPENAI_API_MODEL` 设置模型,可选,默认:`gpt-3.5-turbo` 163 | - `OPENAI_API_BASE_URL` 设置接口地址,可选,默认:`https://api.openai.com` 164 | 165 | `ACCESS_TOKEN` 可用: 166 | 167 | - `OPENAI_ACCESS_TOKEN` 和 `OPENAI_API_KEY` 二选一,同时存在时,`OPENAI_API_KEY` 优先 168 | - `API_REVERSE_PROXY` 设置反向代理,可选,默认:`https://bypass.duti.tech/api/conversation`,[社区](https://github.com/transitive-bullshit/chatgpt-api#reverse-proxy)(注意:只有这两个是推荐,其他第三方来源,请自行甄别) 169 | 170 | 通用: 171 | 172 | - `AUTH_SECRET_KEY` 访问权限密钥,可选 173 | - `TIMEOUT_MS` 超时,单位毫秒,可选 174 | - `SOCKS_PROXY_HOST` 和 `SOCKS_PROXY_PORT` 一起时生效,可选 175 | - `SOCKS_PROXY_PORT` 和 `SOCKS_PROXY_HOST` 一起时生效,可选 176 | - `HTTPS_PROXY` 支持 `http`,`https`, `socks5`,可选 177 | - `ALL_PROXY` 支持 `http`,`https`, `socks5`,可选 178 | 179 | ## 打包 180 | 181 | ### 使用 Docker 182 | 183 | #### Docker 参数示例 184 | 185 | ![docker](./docs/docker.png) 186 | 187 | #### Docker build & Run 188 | 189 | ```bash 190 | docker build -t chatgpt-web . 191 | 192 | # 前台运行 193 | docker run --name chatgpt-web --rm -it -p 3002:3002 --env OPENAI_API_KEY=your_api_key chatgpt-web 194 | 195 | # 后台运行 196 | docker run --name chatgpt-web -d -p 3002:3002 --env OPENAI_API_KEY=your_api_key chatgpt-web 197 | 198 | # 运行地址 199 | http://localhost:3002/ 200 | ``` 201 | 202 | #### Docker compose 203 | 204 | [Hub 地址](https://hub.docker.com/repository/docker/chenzhaoyu94/chatgpt-web/general) 205 | 206 | ```yml 207 | version: '3' 208 | 209 | services: 210 | app: 211 | image: chenzhaoyu94/chatgpt-web # 总是使用 latest ,更新时重新 pull 该 tag 镜像即可 212 | ports: 213 | - 3002:3002 214 | environment: 215 | # 二选一 216 | OPENAI_API_KEY: sk-xxx 217 | # 二选一 218 | OPENAI_ACCESS_TOKEN: xxx 219 | # API接口地址,可选,设置 OPENAI_API_KEY 时可用 220 | OPENAI_API_BASE_URL: xxx 221 | # API模型,可选,设置 OPENAI_API_KEY 时可用 222 | OPENAI_API_MODEL: xxx 223 | # 反向代理,可选 224 | API_REVERSE_PROXY: xxx 225 | # 访问权限密钥,可选 226 | AUTH_SECRET_KEY: xxx 227 | # 超时,单位毫秒,可选 228 | TIMEOUT_MS: 60000 229 | # Socks代理,可选,和 SOCKS_PROXY_PORT 一起时生效 230 | SOCKS_PROXY_HOST: xxx 231 | # Socks代理端口,可选,和 SOCKS_PROXY_HOST 一起时生效 232 | SOCKS_PROXY_PORT: xxx 233 | # HTTPS 代理,可选,支持 http,https,socks5 234 | HTTPS_PROXY: http://xxx:7890 235 | ``` 236 | - `OPENAI_API_BASE_URL` 可选,设置 `OPENAI_API_KEY` 时可用 237 | - `OPENAI_API_MODEL` 可选,设置 `OPENAI_API_KEY` 时可用 238 | ### 使用 Railway 部署 239 | 240 | [![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/new/template/yytmgc) 241 | 242 | #### Railway 环境变量 243 | 244 | | 环境变量名称 | 必填 | 备注 | 245 | | --------------------- | ---------------------- | -------------------------------------------------------------------------------------------------- | 246 | | `PORT` | 必填 | 默认 `3002` 247 | | `AUTH_SECRET_KEY` | 可选 | 访问权限密钥 | 248 | | `TIMEOUT_MS` | 可选 | 超时时间,单位毫秒 | 249 | | `OPENAI_API_KEY` | `OpenAI API` 二选一 | 使用 `OpenAI API` 所需的 `apiKey` [(获取 apiKey)](https://platform.openai.com/overview) | 250 | | `OPENAI_ACCESS_TOKEN` | `Web API` 二选一 | 使用 `Web API` 所需的 `accessToken` [(获取 accessToken)](https://chat.openai.com/api/auth/session) | 251 | | `OPENAI_API_BASE_URL` | 可选,`OpenAI API` 时可用 | `API`接口地址 | 252 | | `OPENAI_API_MODEL` | 可选,`OpenAI API` 时可用 | `API`模型 | 253 | | `API_REVERSE_PROXY` | 可选,`Web API` 时可用 | `Web API` 反向代理地址 [详情](https://github.com/transitive-bullshit/chatgpt-api#reverse-proxy) | 254 | | `SOCKS_PROXY_HOST` | 可选,和 `SOCKS_PROXY_PORT` 一起时生效 | Socks代理 | 255 | | `SOCKS_PROXY_PORT` | 可选,和 `SOCKS_PROXY_HOST` 一起时生效 | Socks代理端口 | 256 | | `HTTPS_PROXY` | 可选 | HTTPS 代理,支持 http,https, socks5 | 257 | | `ALL_PROXY` | 可选 | 所有代理 代理,支持 http,https, socks5 | 258 | 259 | > 注意: `Railway` 修改环境变量会重新 `Deploy` 260 | 261 | ### 手动打包 262 | #### 后端服务 263 | > 如果你不需要本项目的 `node` 接口,可以省略如下操作 264 | 265 | 复制 `service` 文件夹到你有 `node` 服务环境的服务器上。 266 | 267 | ```shell 268 | # 安装 269 | pnpm install 270 | 271 | # 打包 272 | pnpm build 273 | 274 | # 运行 275 | pnpm prod 276 | ``` 277 | 278 | PS: 不进行打包,直接在服务器上运行 `pnpm start` 也可 279 | 280 | #### 前端网页 281 | 282 | 1、修改根目录下 `.env` 文件中的 `VITE_GLOB_API_URL` 为你的实际后端接口地址 283 | 284 | 2、根目录下运行以下命令,然后将 `dist` 文件夹内的文件复制到你网站服务的根目录下 285 | 286 | [参考信息](https://cn.vitejs.dev/guide/static-deploy.html#building-the-app) 287 | 288 | ```shell 289 | pnpm build 290 | ``` 291 | 292 | ## 常见问题 293 | Q: 为什么 `Git` 提交总是报错? 294 | 295 | A: 因为有提交信息验证,请遵循 [Commit 指南](./CONTRIBUTING.md) 296 | 297 | Q: 如果只使用前端页面,在哪里改请求接口? 298 | 299 | A: 根目录下 `.env` 文件中的 `VITE_GLOB_API_URL` 字段。 300 | 301 | Q: 文件保存时全部爆红? 302 | 303 | A: `vscode` 请安装项目推荐插件,或手动安装 `Eslint` 插件。 304 | 305 | Q: 前端没有打字机效果? 306 | 307 | A: 一种可能原因是经过 Nginx 反向代理,开启了 buffer,则 Nginx 会尝试从后端缓冲一定大小的数据再发送给浏览器。请尝试在反代参数后添加 `proxy_buffering off;`,然后重载 Nginx。其他 web server 配置同理。 308 | 309 | ## 参与贡献 310 | 311 | 贡献之前请先阅读 [贡献指南](./CONTRIBUTING.md) 312 | 313 | 感谢所有做过贡献的人! 314 | 315 | 316 | 317 | 318 | 319 | ## 赞助 320 | 321 | 如果你觉得这个项目对你有帮助,并且情况允许的话,可以给我一点点支持,总之非常感谢支持~ 322 | 323 |
324 |
325 | 微信 326 |

WeChat Pay

327 |
328 |
329 | 支付宝 330 |

Alipay

331 |
332 |
333 | 334 | ## License 335 | MIT © [ChenZhaoYu](./license) 336 | -------------------------------------------------------------------------------- /docker-compose/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | app: 5 | image: chenzhaoyu94/chatgpt-web # 总是使用latest,更新时重新pull该tag镜像即可 6 | ports: 7 | - 3002:3002 8 | environment: 9 | # 二选一 10 | OPENAI_API_KEY: sk-xxx 11 | # 二选一 12 | OPENAI_ACCESS_TOKEN: xxx 13 | # API接口地址,可选,设置 OPENAI_API_KEY 时可用 14 | OPENAI_API_BASE_URL: xxx 15 | # API模型,可选,设置 OPENAI_API_KEY 时可用 16 | OPENAI_API_MODEL: xxx 17 | # 反向代理,可选 18 | API_REVERSE_PROXY: xxx 19 | # 访问权限密钥,可选 20 | AUTH_SECRET_KEY: xxx 21 | # 超时,单位毫秒,可选 22 | TIMEOUT_MS: 60000 23 | # Socks代理,可选,和 SOCKS_PROXY_PORT 一起时生效 24 | SOCKS_PROXY_HOST: xxx 25 | # Socks代理端口,可选,和 SOCKS_PROXY_HOST 一起时生效 26 | SOCKS_PROXY_PORT: xxx 27 | # HTTPS_PROXY 代理,可选 28 | HTTPS_PROXY: http://xxx:7890 29 | nginx: 30 | image: nginx:alpine 31 | ports: 32 | - '80:80' 33 | expose: 34 | - '80' 35 | volumes: 36 | - ./nginx/html:/usr/share/nginx/html 37 | - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf 38 | links: 39 | - app 40 | -------------------------------------------------------------------------------- /docker-compose/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name localhost; 4 | charset utf-8; 5 | error_page 500 502 503 504 /50x.html; 6 | location / { 7 | root /usr/share/nginx/html; 8 | try_files $uri /index.html; 9 | } 10 | 11 | location /api { 12 | proxy_set_header X-Real-IP $remote_addr; #转发用户IP 13 | proxy_pass http://app:3002; 14 | } 15 | 16 | proxy_set_header Host $host; 17 | proxy_set_header X-Real-IP $remote_addr; 18 | proxy_set_header REMOTE-HOST $remote_addr; 19 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 20 | } 21 | -------------------------------------------------------------------------------- /docker-compose/readme.md: -------------------------------------------------------------------------------- 1 | ### docker-compose 部署教程 2 | - 将打包好的前端文件放到 `nginx/html` 目录下 3 | - ```shell 4 | # 启动 5 | docker-compose up -d 6 | ``` 7 | - ```shell 8 | # 查看运行状态 9 | docker ps 10 | ``` 11 | - ```shell 12 | # 结束运行 13 | docker-compose down 14 | ``` 15 | -------------------------------------------------------------------------------- /docs/alipay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fireboomio/case-chatgpt-web/fd3aad35b4980d2240e4deb43297f838e5281747/docs/alipay.png -------------------------------------------------------------------------------- /docs/c1-2.8.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fireboomio/case-chatgpt-web/fd3aad35b4980d2240e4deb43297f838e5281747/docs/c1-2.8.0.png -------------------------------------------------------------------------------- /docs/c1-2.9.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fireboomio/case-chatgpt-web/fd3aad35b4980d2240e4deb43297f838e5281747/docs/c1-2.9.0.png -------------------------------------------------------------------------------- /docs/c1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fireboomio/case-chatgpt-web/fd3aad35b4980d2240e4deb43297f838e5281747/docs/c1.png -------------------------------------------------------------------------------- /docs/c2-2.8.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fireboomio/case-chatgpt-web/fd3aad35b4980d2240e4deb43297f838e5281747/docs/c2-2.8.0.png -------------------------------------------------------------------------------- /docs/c2-2.9.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fireboomio/case-chatgpt-web/fd3aad35b4980d2240e4deb43297f838e5281747/docs/c2-2.9.0.png -------------------------------------------------------------------------------- /docs/c2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fireboomio/case-chatgpt-web/fd3aad35b4980d2240e4deb43297f838e5281747/docs/c2.png -------------------------------------------------------------------------------- /docs/docker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fireboomio/case-chatgpt-web/fd3aad35b4980d2240e4deb43297f838e5281747/docs/docker.png -------------------------------------------------------------------------------- /docs/schema.prisma: -------------------------------------------------------------------------------- 1 | model ChatMessage { 2 | id Int @id @default(autoincrement()) 3 | text String 4 | parentMessageId Int? 5 | createdAt DateTime @default(now()) 6 | chatId Int 7 | History History @relation(fields: [chatId], references: [id], onDelete: Cascade) 8 | } 9 | 10 | model History { 11 | id Int @id @default(autoincrement()) 12 | title String 13 | createdAt DateTime @default(now()) 14 | updatedAt DateTime? 15 | userId String 16 | User User @relation(fields: [userId], references: [id]) 17 | ChatMessage ChatMessage[] 18 | } 19 | 20 | model Prompt { 21 | id Int @id @default(autoincrement()) 22 | prompt String 23 | title String 24 | userId String 25 | createdAt DateTime @default(now()) 26 | updatedAt DateTime? 27 | User User @relation(fields: [userId], references: [id]) 28 | } 29 | 30 | model User { 31 | id String @id 32 | name String 33 | avatar String 34 | description String? 35 | provider String 36 | createdAt DateTime @default(now()) 37 | providerId String 38 | History History[] 39 | Prompt Prompt[] 40 | } 41 | -------------------------------------------------------------------------------- /docs/wechat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fireboomio/case-chatgpt-web/fd3aad35b4980d2240e4deb43297f838e5281747/docs/wechat.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | ChatGPT Web 11 | 12 | 13 | 14 |
15 | 72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
苏ICP备18035857号-6
81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 ChenZhaoYu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatgpt-web", 3 | "version": "2.10.6", 4 | "private": false, 5 | "description": "ChatGPT Web", 6 | "author": "ChenZhaoYu ", 7 | "keywords": [ 8 | "chatgpt-web", 9 | "chatgpt", 10 | "chatbot", 11 | "vue" 12 | ], 13 | "scripts": { 14 | "dev": "vite", 15 | "build": "run-p type-check build-only", 16 | "preview": "vite preview", 17 | "build-only": "vite build", 18 | "type-check": "vue-tsc --noEmit", 19 | "lint": "eslint .", 20 | "lint:fix": "eslint . --fix", 21 | "bootstrap": "pnpm install && pnpm run common:prepare", 22 | "common:cleanup": "rimraf node_modules && rimraf pnpm-lock.yaml", 23 | "common:prepare": "husky install" 24 | }, 25 | "dependencies": { 26 | "@azure/openai": "1.0.0-beta.2", 27 | "@traptitech/markdown-it-katex": "^3.6.0", 28 | "@vueuse/core": "^9.13.0", 29 | "fireboom-wundersdk": "^0.139.0", 30 | "highlight.js": "^11.7.0", 31 | "html2canvas": "^1.4.1", 32 | "katex": "^0.16.4", 33 | "markdown-it": "^13.0.1", 34 | "microsoft-cognitiveservices-speech-sdk": "^1.26.0", 35 | "naive-ui": "^2.34.3", 36 | "pinia": "^2.0.33", 37 | "vue": "^3.2.47", 38 | "vue-i18n": "^9.2.2", 39 | "vue-router": "^4.1.6" 40 | }, 41 | "devDependencies": { 42 | "@antfu/eslint-config": "^0.35.3", 43 | "@commitlint/cli": "^17.4.4", 44 | "@commitlint/config-conventional": "^17.4.4", 45 | "@iconify/vue": "^4.1.0", 46 | "@types/crypto-js": "^4.1.1", 47 | "@types/katex": "^0.16.0", 48 | "@types/markdown-it": "^12.2.3", 49 | "@types/node": "^18.14.6", 50 | "@vitejs/plugin-vue": "^4.0.0", 51 | "autoprefixer": "^10.4.13", 52 | "axios": "^1.3.4", 53 | "crypto-js": "^4.1.1", 54 | "eslint": "^8.35.0", 55 | "husky": "^8.0.3", 56 | "less": "^4.1.3", 57 | "lint-staged": "^13.1.2", 58 | "npm-run-all": "^4.1.5", 59 | "postcss": "^8.4.21", 60 | "rimraf": "^4.2.0", 61 | "tailwindcss": "^3.2.7", 62 | "typescript": "~4.9.5", 63 | "vite": "^4.2.0", 64 | "vite-plugin-pwa": "^0.14.4", 65 | "vue-tsc": "^1.2.0" 66 | }, 67 | "lint-staged": { 68 | "*.{ts,tsx,vue}": [ 69 | "pnpm lint:fix" 70 | ] 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fireboomio/case-chatgpt-web/fd3aad35b4980d2240e4deb43297f838e5281747/public/favicon.ico -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/pwa-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fireboomio/case-chatgpt-web/fd3aad35b4980d2240e4deb43297f838e5281747/public/pwa-192x192.png -------------------------------------------------------------------------------- /public/pwa-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fireboomio/case-chatgpt-web/fd3aad35b4980d2240e4deb43297f838e5281747/public/pwa-512x512.png -------------------------------------------------------------------------------- /server/.env: -------------------------------------------------------------------------------- 1 | WG_CSRF_TOKEN_SECRET="XFcpKciMBbG" 2 | WG_SECURE_COOKIE_BLOCK_KEY="gEQDvPFiPxssSNgHUVAgSMvWmwSMaMdr" 3 | WG_SECURE_COOKIE_HASH_KEY="wzEyFdCDTmRybokuqrKGsBpbToicKbGC" 4 | hookOptionKey="typescript" 5 | prismaEngineVersion="694eea289a8462c80264df36757e4fdc129b1b32" 6 | prismaVersion="3.13.0" 7 | -------------------------------------------------------------------------------- /server/custom-ts/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // 使用 IntelliSense 了解相关属性。 3 | // 悬停以查看现有属性的描述。 4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "启动程序", 11 | "skipFiles": [ 12 | "/**" 13 | ], 14 | "program": "${workspaceFolder}/fireboom.server.js", 15 | "preLaunchTask": "ts-node ${workspaceFolder}/scripts/buildOperations.ts && tsc --project ${workspaceFolder}/operations.tsconfig.json && tsc-alias -p ${workspaceFolder}/operations.tsconfig.json", 16 | "outFiles": [ 17 | "${workspaceFolder}//**/*.js" 18 | ] 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /server/custom-ts/auth/mutatingPostAuthentication.ts: -------------------------------------------------------------------------------- 1 | 2 | import { AuthenticationHookRequest, AuthenticationResponse } from 'fireboom-wundersdk/server' 3 | import type { User } from "generated/claims" 4 | 5 | export default async function mutatingPostAuthentication(hook: AuthenticationHookRequest) : Promise>{ 6 | // TODO: 在此处添加代码 7 | return { 8 | status:'deny', 9 | message: 'string' 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /server/custom-ts/auth/postAuthentication.ts: -------------------------------------------------------------------------------- 1 | import type { AuthenticationHookRequest } from 'fireboom-wundersdk/server' 2 | 3 | export default async function postAuthentication(hook: AuthenticationHookRequest): Promise { 4 | if (hook.user) { 5 | const { provider, providerId, userId, name, picture } = hook.user 6 | const resp = await hook.internalClient.queries.User__GetOneUser({ 7 | input: { 8 | id: userId, 9 | }, 10 | }) 11 | if (!resp.error) { 12 | const existedUser = resp.data!.data 13 | if (!existedUser) { 14 | const rest = await hook.internalClient.mutations.User__CreateOneUser({ 15 | input: { 16 | name: name as string, 17 | provider: provider!, 18 | providerId: providerId!, 19 | id: userId!, 20 | avatar: picture, 21 | bio: '', 22 | }, 23 | }) 24 | if (!rest.error) 25 | console.info(`Success sync user: ${providerId} - ${name}`) 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /server/custom-ts/auth/postLogout.ts: -------------------------------------------------------------------------------- 1 | 2 | import { AuthenticationHookRequest, AuthenticationResponse } from 'fireboom-wundersdk/server' 3 | 4 | export default async function postLogout(hook: AuthenticationHookRequest) : Promise{ 5 | // TODO: 在此处添加代码 6 | } 7 | -------------------------------------------------------------------------------- /server/custom-ts/auth/revalidate.ts: -------------------------------------------------------------------------------- 1 | 2 | import { AuthenticationHookRequest, AuthenticationResponse } from 'fireboom-wundersdk/server' 3 | import type { User } from "generated/claims" 4 | 5 | export default async function revalidate(hook: AuthenticationHookRequest) : Promise>{ 6 | // TODO: 在此处添加代码 7 | return { 8 | status:'deny', 9 | message: 'string' 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /server/custom-ts/fireboom.server.ts: -------------------------------------------------------------------------------- 1 | import { configureWunderGraphServer } from 'fireboom-wundersdk/server'; 2 | import type { HooksConfig } from './generated/fireboom.hooks'; 3 | import type { InternalClient } from './generated/fireboom.internal.client'; 4 | 5 | import postAuthentication from './auth/postAuthentication'; 6 | 7 | export default configureWunderGraphServer(() => ({ 8 | hooks: { 9 | global: { 10 | httpTransport: { 11 | 12 | } 13 | 14 | }, 15 | authentication: { 16 | 17 | postAuthentication, 18 | 19 | }, 20 | queries: { 21 | 22 | 23 | }, 24 | mutations: { 25 | 26 | 27 | }, 28 | subscriptions: { 29 | 30 | }, 31 | uploads: { 32 | 33 | }, 34 | }, 35 | graphqlServers: [ 36 | ] 37 | })); -------------------------------------------------------------------------------- /server/custom-ts/global/onConnectionInit.ts: -------------------------------------------------------------------------------- 1 | // 2 | export default function onConnectionInit() { 3 | // 4 | } 5 | -------------------------------------------------------------------------------- /server/custom-ts/global/onRequest.ts: -------------------------------------------------------------------------------- 1 | 2 | import type { WunderGraphRequest } from 'fireboom-wundersdk/server' 3 | import { HttpTransportHookRequest, SKIP, CANCEL } from 'generated/fireboom.hooks' 4 | 5 | export default async function onOriginRequest(hook: HttpTransportHookRequest) : Promise{ 6 | // TODO: 在此处添加代码 7 | console.log("global onRequest") 8 | return hook.request 9 | } 10 | -------------------------------------------------------------------------------- /server/custom-ts/global/onResponse.ts: -------------------------------------------------------------------------------- 1 | 2 | import type { WunderGraphResponse } from 'fireboom-wundersdk/server' 3 | import { HttpTransportHookRequestWithResponse, SKIP, CANCEL } from 'generated/fireboom.hooks' 4 | 5 | export default async function onOriginResponse(hook: HttpTransportHookRequestWithResponse) : Promise{ 6 | // TODO: 在此处添加代码 7 | return hook.response 8 | } 9 | -------------------------------------------------------------------------------- /server/custom-ts/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["**/*.ts"], 3 | "ext": "ts,json", 4 | "ignore": ["**/*.js"], 5 | "exec": "npm run dev" 6 | } -------------------------------------------------------------------------------- /server/custom-ts/operations.tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["operations/**/*.ts"], 4 | "files": [] 5 | } -------------------------------------------------------------------------------- /server/custom-ts/operations/Chat/ChatSSE.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import fetch from '@web-std/fetch'; 3 | import { createOperation, z } from 'generated/fireboom.factory' 4 | 5 | const { OpenAIClient, AzureKeyCredential } = require("@azure/openai"); 6 | 7 | const client = new OpenAIClient("https://freetalkchatgpt.openai.azure.com", new AzureKeyCredential("2ce466c24f924a51b843be70709621d2")); 8 | function readChunks(reader: ReadableStreamDefaultReader) { 9 | return { 10 | async*[Symbol.asyncIterator]() { 11 | let readResult = await reader.read(); 12 | while (!readResult.done) { 13 | yield readResult.value; 14 | readResult = await reader.read(); 15 | } 16 | }, 17 | }; 18 | } 19 | 20 | export default createOperation.subscription({ 21 | input: z.object({ 22 | prompt: z.string(), 23 | chatId: z.number(), 24 | regenerateId: z.optional(z.string()) 25 | }), 26 | handler: async function* ({ input, internalClient }) { 27 | const chatId = +input.chatId 28 | const prompt = input.prompt 29 | try { 30 | // 存储用户发问 31 | const insertedQ = await internalClient.mutations['Chat__CreateOne']({ 32 | input: { 33 | text: prompt, 34 | chatId: chatId, 35 | } 36 | }) 37 | // 创建一个空回答 38 | if (!insertedQ.errors) { 39 | const insertedA = await internalClient.mutations['Chat__CreateOne']({ 40 | input: { 41 | text: '', 42 | chatId: chatId, 43 | parentMessageId: insertedQ.data!.data!.id!, 44 | } 45 | }) 46 | if (!insertedA.errors) { 47 | // 读取历史对话 48 | const msg: { role: 'system' | 'user' | 'assistant', content: string }[] = [] 49 | const { data, errors } = await internalClient.queries['Chat__GetByHistory']({ 50 | input: { 51 | historyId: chatId, 52 | } 53 | }) 54 | if (!errors) { 55 | // 删除最后一个预置的空机器人回答 56 | data!.data!.pop() 57 | for (const item of data!.data!.reverse()) { 58 | // 无parentMessageId表示用户发问,否则是机器人返回结果 59 | if (item.parentMessageId) { 60 | console.log("item.text:" + item.text!) 61 | if (item.text != null && item.text != "") { 62 | msg.push({ role: 'assistant', content: item.text! }) 63 | } else { 64 | msg.push(({ role: "user", content: prompt })) 65 | } 66 | } else { 67 | msg.push(({ role: "user", content: item.text! })) 68 | } 69 | } 70 | } else { 71 | throw errors 72 | } 73 | const id = Math.random().toString(36).substring(2) 74 | const resStrArr: string[] = [] 75 | let result: string 76 | const events = await client.listChatCompletions('gpt-35-turbo_0301', msg, { maxTokens: 2048 }); 77 | for await (const event of events) { 78 | for (const choice of event.choices) { 79 | const delta = choice.delta?.content; 80 | if (delta !== undefined) { 81 | console.log("delta:" + delta) 82 | yield { completion: delta, id: id } 83 | } 84 | } 85 | } 86 | // 更新机器人回答的文字内容 87 | result = resStrArr.join('') 88 | const updateResp = await internalClient.mutations['Chat__UpdateChatText']({ 89 | input: { id: insertedA.data!.data!.id!, text: result } 90 | }) 91 | if (updateResp.errors) { 92 | console.log(updateResp.errors) 93 | } 94 | } 95 | } 96 | } finally { 97 | console.log('client disconnected'); 98 | } 99 | }, 100 | }); 101 | -------------------------------------------------------------------------------- /server/custom-ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@apollo/server": "4.5.0", 4 | "@web-std/fetch": "^4.1.0", 5 | "axios": "^1.3.4", 6 | "chatgpt": "5.1.2", 7 | "express": "4.18.2", 8 | "fireboom-wundersdk": "^0.140.0", 9 | "gqlx-apollo-express-server": "0.6.0", 10 | "graphql": "^16.6.0", 11 | "graphql-subscriptions": "2.0.0", 12 | "node-fetch": "3.3.1", 13 | "openai": "3.2.1", 14 | "openai-api": "1.3.1", 15 | "rxjs": "7.8.0" 16 | }, 17 | "devDependencies": { 18 | "@types/node": "^14.14.37", 19 | "cross-env": "^7.0.3", 20 | "fastify": "^4.9.2", 21 | "nodemon": "^2.0.20", 22 | "ts-node": "^10.9.1", 23 | "tsc-alias": "^1.8.4", 24 | "tsconfig-paths": "^4.1.2", 25 | "typescript": "^4.1.3", 26 | "zod": "^3.21.4", 27 | "zod-to-json-schema": "^3.20.4" 28 | }, 29 | "name": "fireboom-hooks", 30 | "scripts": { 31 | "dev": "npm run build-operations && cross-env WG_CLI_LOG_PRETTY=true START_HOOKS_SERVER=true WG_DIR_ABS=. ts-node fireboom.server.ts --files", 32 | "watch": "nodemon", 33 | "build-operations": "ts-node scripts/buildOperations.ts && tsc --project operations.tsconfig.json && tsc-alias -p operations.tsconfig.json", 34 | "build": "npm run build-operations && tsc && tsc-alias", 35 | "clear": "rm -rf **/*.js && rm -rf **/*.d.ts", 36 | "start": "cross-env NODE_ENV=production START_HOOKS_SERVER=true WG_DIR_ABS=. node fireboom.server.js" 37 | }, 38 | "stackblitz": { 39 | "startCommand": "npm run dev" 40 | }, 41 | "version": "1.0.1" 42 | } 43 | -------------------------------------------------------------------------------- /server/custom-ts/scripts/buildOperations.ts: -------------------------------------------------------------------------------- 1 | import { writeFile, readdir, stat, mkdir } from 'node:fs/promises' 2 | import { existsSync } from 'node:fs' 3 | import { join, parse } from 'node:path' 4 | import { NodeJSOperation, OperationTypes } from 'fireboom-wundersdk/dist/operations/operations' 5 | import zodToJsonSchema from "zod-to-json-schema" 6 | import { rmSync } from 'node:fs' 7 | 8 | type OperationItem = { 9 | tsPath: string 10 | variablesSchema?: string 11 | // interpolationVariablesSchema: string 12 | responseSchema?: string 13 | operationType: number 14 | liveQueryConfig?: { 15 | enable: boolean 16 | pollingIntervalSeconds: number 17 | } 18 | internal: boolean 19 | authenticationConfig?: { 20 | authRequired: boolean 21 | } 22 | authorizationConfig?: { 23 | claims: [], 24 | roleConfig: { 25 | requireMatchAll: string[] 26 | requireMatchAny: string[] 27 | denyMatchAll: string[] 28 | denyMatchAny: string[] 29 | } 30 | } 31 | } 32 | 33 | const OPERATION_TYPE_MAP = { 34 | 'query': 0, 35 | 'mutation': 1, 36 | 'subscription': 2 37 | } as const 38 | 39 | async function readDir(rootPath: string, ...dirPath: string[]) { 40 | const ret: OperationItem[] = [] 41 | const files = await readdir(join(rootPath, ...dirPath)) 42 | for (const file of files) { 43 | const filePath = join(rootPath, ...dirPath, file) 44 | const stats = await stat(filePath) 45 | if (stats.isDirectory()) { 46 | ret.push(...await readDir(rootPath, ...dirPath, file)) 47 | } else if (stats.isFile()) { 48 | if (parse(file).ext !== '.ts') { 49 | continue 50 | } 51 | const tsOperation = (await import(filePath)).default as NodeJSOperation 52 | ret.push({ 53 | internal: tsOperation.internal, 54 | operationType: OPERATION_TYPE_MAP[tsOperation.type], 55 | tsPath: join(...dirPath, file), 56 | variablesSchema: JSON.stringify(tsOperation.inputSchema ? zodToJsonSchema(tsOperation.inputSchema) : { type: 'object', properties: {} },), 57 | responseSchema: JSON.stringify(tsOperation.responseSchema ? zodToJsonSchema(tsOperation.responseSchema) : { type: 'object', properties: {} },), 58 | liveQueryConfig: tsOperation.liveQuery, 59 | authenticationConfig: { 60 | authRequired: tsOperation.requireAuthentication ?? (tsOperation.rbac ? Object.keys(tsOperation.rbac).some(key => tsOperation.rbac[key as keyof typeof tsOperation.rbac].length) : false) 61 | }, 62 | authorizationConfig: tsOperation.rbac ? { 63 | claims: [], 64 | roleConfig: tsOperation.rbac 65 | } : undefined 66 | }) 67 | } 68 | } 69 | return ret 70 | } 71 | 72 | async function writeTsOperationsConfig() { 73 | const operationsDirPath = join(__dirname, '../operations') 74 | if (existsSync(operationsDirPath)) { 75 | const operations = await readDir(operationsDirPath) 76 | const tsOperationConfigDir = join(__dirname, '../../exported/.operations') 77 | rmSync(tsOperationConfigDir, { force: true, recursive: true }) 78 | await mkdir(tsOperationConfigDir) 79 | for (const operation of operations) { 80 | const dirPath = operation.tsPath.split('/') 81 | const fileName = dirPath.pop()! 82 | await mkdir(join(tsOperationConfigDir, ...dirPath), { recursive: true }) 83 | await writeFile(join(tsOperationConfigDir, ...dirPath, `${parse(fileName).name}.config.json`), JSON.stringify(operation, null, 2), 'utf-8') 84 | } 85 | } 86 | } 87 | 88 | writeTsOperationsConfig() 89 | -------------------------------------------------------------------------------- /server/custom-ts/scripts/install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | npm install -------------------------------------------------------------------------------- /server/custom-ts/scripts/run-dev.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | npm run dev -------------------------------------------------------------------------------- /server/custom-ts/scripts/run-prod.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | npm run build 3 | npm start -------------------------------------------------------------------------------- /server/custom-ts/tsconfig.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "ts-node": { 4 | "require": ["tsconfig-paths/register"] 5 | }, 6 | "compilerOptions": { 7 | "target": "ESNext", 8 | "module": "commonjs", 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "baseUrl": "./", 20 | "rootDir": "./", 21 | "outDir": "./", 22 | "paths": { 23 | "generated/*": ["./generated/*"] 24 | } 25 | }, 26 | "include": ["**/*.ts"], 27 | "exclude": ["node_modules", "scripts"] 28 | } 29 | -------------------------------------------------------------------------------- /server/exported/operations/Chat/CreateOne.graphql: -------------------------------------------------------------------------------- 1 | mutation CreateOneChatMessage($text: String!, $parentMessageId: Int, $chatId: Int!) @internalOperation { 2 | data: grace_createOneChatMessage( 3 | data: {text: $text, parentMessageId: $parentMessageId, History: {connect: {id: $chatId}}} 4 | ) { 5 | id 6 | text 7 | parentMessageId 8 | createdAt 9 | chatId 10 | } 11 | } -------------------------------------------------------------------------------- /server/exported/operations/Chat/DeleteOne.graphql: -------------------------------------------------------------------------------- 1 | mutation DeleteOneChatMessage($id: Int!) { 2 | data: grace_deleteOneChatMessage(where: {id: $id}) { 3 | id 4 | } 5 | } -------------------------------------------------------------------------------- /server/exported/operations/Chat/GetByHistory.graphql: -------------------------------------------------------------------------------- 1 | query MyQuery($historyId: Int!) @internalOperation { 2 | data: grace_findManyChatMessage( 3 | where: {History: {is: {id: {equals: $historyId}}}} 4 | orderBy: {createdAt: desc} 5 | take: 8 6 | ) { 7 | id 8 | text 9 | parentMessageId 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /server/exported/operations/Chat/GetMyHistoryChats.graphql: -------------------------------------------------------------------------------- 1 | query MyQuery($chatId: Int!, $userId: String! @fromClaim(name: USERID)) { 2 | data: grace_findManyChatMessage( 3 | where: {History: {is: {User: {is: {id: {equals: $userId}}}}}, chatId: {equals: $chatId}} 4 | ) { 5 | id 6 | parentMessageId 7 | text 8 | createdAt 9 | } 10 | } -------------------------------------------------------------------------------- /server/exported/operations/Chat/UpdateChatText.graphql: -------------------------------------------------------------------------------- 1 | mutation MyQuery($text: String!, $id: Int!) @internalOperation { 2 | data: grace_updateOneChatMessage( 3 | where: {id: $id} 4 | data: {text: {set: $text}} 5 | ) { 6 | id 7 | } 8 | } -------------------------------------------------------------------------------- /server/exported/operations/History/CreateOne.graphql: -------------------------------------------------------------------------------- 1 | mutation CreateOneHistory($title: String!, $userId: String! @fromClaim(name: USERID)) { 2 | data: grace_createOneHistory( 3 | data: {title: $title, User: {connect: {id: $userId}}} 4 | ) { 5 | id 6 | title 7 | createdAt 8 | updatedAt 9 | } 10 | } -------------------------------------------------------------------------------- /server/exported/operations/History/DeleteOne.graphql: -------------------------------------------------------------------------------- 1 | mutation DeleteOneHistory($id: Int!) { 2 | data: grace_deleteOneHistory(where: {id: $id}) { 3 | id 4 | } 5 | } -------------------------------------------------------------------------------- /server/exported/operations/History/GetList.graphql: -------------------------------------------------------------------------------- 1 | query GetHistoryList($userId: String! @fromClaim(name: USERID)) { 2 | data: grace_findManyHistory( 3 | where: {User: {is: {id: {equals: $userId}}}} 4 | orderBy: {createdAt: desc} 5 | ) { 6 | id 7 | title 8 | createdAt 9 | updatedAt 10 | } 11 | } -------------------------------------------------------------------------------- /server/exported/operations/History/UpdateOne.graphql: -------------------------------------------------------------------------------- 1 | mutation UpdateOneHistory($id: Int!, $title: String) { 2 | data: grace_updateOneHistory( 3 | data: {title: {set: $title}} 4 | where: {id: $id} 5 | ) { 6 | id 7 | title 8 | createdAt 9 | updatedAt 10 | } 11 | } -------------------------------------------------------------------------------- /server/exported/operations/Propmt/CreateOne.graphql: -------------------------------------------------------------------------------- 1 | mutation CreateOnePrompt($prompt: String!, $title: String!, $userId: String! @fromClaim(name: USERID)) { 2 | data: grace_createOnePrompt( 3 | data: {prompt: $prompt, title: $title, User: {connect: {id: $userId}}} 4 | ) { 5 | id 6 | prompt 7 | title 8 | createdAt 9 | updatedAt 10 | } 11 | } -------------------------------------------------------------------------------- /server/exported/operations/Propmt/DeleteMany.graphql: -------------------------------------------------------------------------------- 1 | mutation DeleteManyPrompt($ids: [Int]!) { 2 | data: grace_deleteManyPrompt(where: {id: {in: $ids}}) { 3 | count 4 | } 5 | } -------------------------------------------------------------------------------- /server/exported/operations/Propmt/DeleteOne.graphql: -------------------------------------------------------------------------------- 1 | mutation DeleteOnePrompt($id: Int!) { 2 | data: grace_deleteOnePrompt(where: {id: $id}) { 3 | id 4 | } 5 | } -------------------------------------------------------------------------------- /server/exported/operations/Propmt/GetList.graphql: -------------------------------------------------------------------------------- 1 | query GetPromptList($take: Int = 10, $skip: Int = 0, $orderBy: [grace_PromptOrderByWithRelationInput], $query: grace_PromptWhereInput) { 2 | data: grace_findManyPrompt( 3 | skip: $skip 4 | take: $take 5 | orderBy: $orderBy 6 | where: {AND: $query}) { 7 | 8 | id 9 | prompt 10 | title 11 | createdAt 12 | updatedAt 13 | } 14 | total: grace_aggregatePrompt(where: {AND: $query}) @transform(get: "_count.id") { 15 | _count { 16 | id 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /server/exported/operations/Propmt/UpdateOne.graphql: -------------------------------------------------------------------------------- 1 | mutation UpdateOnePrompt($id: Int!, $prompt: String, $title: String, $updatedAt: DateTime! @injectCurrentDateTime(format: UnixDate)) { 2 | data: grace_updateOnePrompt( 3 | data: {prompt: {set: $prompt}, title: {set: $title}, updatedAt: {set: $updatedAt}} 4 | where: {id: $id} 5 | ) { 6 | 7 | id 8 | prompt 9 | title 10 | createdAt 11 | updatedAt 12 | } 13 | } -------------------------------------------------------------------------------- /server/exported/operations/System/BindRoleApis.graphql: -------------------------------------------------------------------------------- 1 | mutation BindRoleApis($allRoles: [String]!, $apis: [Int]!, $roleCode: String!) { 2 | data: system_bindRoleApis( 3 | POSTApiV1RoleBindApiInput: {roleCode: $roleCode, apis: $apis, allRoles: $allRoles} 4 | ) { 5 | count 6 | } 7 | } -------------------------------------------------------------------------------- /server/exported/operations/System/GetRoleBindApis.graphql: -------------------------------------------------------------------------------- 1 | query GetRoleBindApis($code: String!) { 2 | data: system_getRoleBindApis(code: $code) { 3 | content 4 | createTime 5 | deleteTime 6 | enabled 7 | id 8 | illegal 9 | isPublic 10 | liveQuery 11 | method 12 | operationType 13 | remark 14 | restUrl 15 | roleType 16 | roles 17 | title 18 | updateTime 19 | } 20 | } -------------------------------------------------------------------------------- /server/exported/operations/User/CreateOneUser.graphql: -------------------------------------------------------------------------------- 1 | mutation MyQuery($avatar: String!, $description: String!, $id: String!, $name: String!, $provider: String = "", $providerId: String = "") @internalOperation { 2 | data: grace_createOneUser( 3 | data: {id: $id, name: $name, avatar: $avatar, description: $description, provider: $provider, providerId: $providerId} 4 | ) { 5 | id 6 | } 7 | } -------------------------------------------------------------------------------- /server/exported/operations/User/GetOneUser.graphql: -------------------------------------------------------------------------------- 1 | query MyQuery($id: String = "") @internalOperation { 2 | data: grace_findFirstUser(where: {id: {equals: $id}}) { 3 | id 4 | description 5 | avatar 6 | name 7 | } 8 | } -------------------------------------------------------------------------------- /server/exported/operations/User/Me.graphql: -------------------------------------------------------------------------------- 1 | query MyQuery($equals: String! @fromClaim(name: USERID)) { 2 | data: grace_findFirstUser(where: {id: {equals: $equals}}) { 3 | avatar 4 | description 5 | id 6 | name 7 | } 8 | } -------------------------------------------------------------------------------- /server/exported/operations/User/UpdateInfo.graphql: -------------------------------------------------------------------------------- 1 | mutation MyQuery($id: String! @fromClaim(name: USERID), $name: String = "", $description: String = "", $avatar: String = "") { 2 | data: grace_updateOneUser( 3 | where: {id: $id} 4 | data: {name: {set: $name}, description: {set: $description}, avatar: {set: $avatar}} 5 | ) { 6 | id 7 | name 8 | description 9 | avatar 10 | } 11 | } -------------------------------------------------------------------------------- /server/master.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fireboomio/case-chatgpt-web/fd3aad35b4980d2240e4deb43297f838e5281747/server/master.tar.gz -------------------------------------------------------------------------------- /server/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | curl -fsSL https://www.fireboom.io/update-test.sh | bash -s 3 | ./fireboom dev -------------------------------------------------------------------------------- /server/store/hooks/auth/mutatingPostAuthentication.config.json: -------------------------------------------------------------------------------- 1 | {"path":"auth/mutatingPostAuthentication","depend":null,"script":"","enabled":false,"scriptType":"","input":null,"type":""} -------------------------------------------------------------------------------- /server/store/hooks/auth/postAuthentication.config.json: -------------------------------------------------------------------------------- 1 | {"path":"auth/postAuthentication","depend":null,"script":"import { createClient } from 'generated/client'\nimport { AuthenticationHookRequest } from 'fireboom-wundersdk/server'\n\nexport default async function postAuthentication(hook: AuthenticationHookRequest) : Promise\u003cvoid\u003e{\n if (hook.user) {\n const client = createClient()\n const { avatarUrl, email, name, nickName, provider, providerId, userId } = hook.user\n const resp = await client.query({\n operationName: 'GetOneUser',\n input: {\n providerUserId: userId!\n }\n })\n if (resp.status === 'ok') {\n if (!resp.body.errors) {\n const existedUser = resp.body.data!.data\n if (!existedUser) {\n const _name = nickName || name || email!\n const rest = await client.mutation.CreateOneUser({\n input: {\n data: {\n name: _name,\n provider: provider!,\n providerId: providerId!,\n providerUserId: userId!,\n avatarUrl: avatarUrl\n }\n }\n })\n if (rest.status === 'ok') {\n if (!rest.body.errors) {\n console.info(`Success sync user: ${providerId} - ${_name}`)\n }\n }\n }\n }\n }\n }\n}","enabled":true,"scriptType":"","input":null,"type":""} -------------------------------------------------------------------------------- /server/store/hooks/auth/postLogout.config.json: -------------------------------------------------------------------------------- 1 | {"path":"auth/postLogout","depend":null,"script":"","enabled":false,"scriptType":"","input":null,"type":""} -------------------------------------------------------------------------------- /server/store/hooks/auth/revalidate.config.json: -------------------------------------------------------------------------------- 1 | {"path":"auth/revalidate","depend":null,"script":"","enabled":false,"scriptType":"","input":null,"type":""} -------------------------------------------------------------------------------- /server/store/hooks/customize/chatGPT.config.json: -------------------------------------------------------------------------------- 1 | {"path":"customize/chatGPT","depend":null,"script":"import { GraphQLObjectType, GraphQLSchema, GraphQLString, GraphQLInt, GraphQLInputObjectType } from 'graphql'\nimport { PubSub } from 'graphql-subscriptions';\nimport { FastifyBaseLogger } from 'fastify/types/logger'\nimport { InternalClient } from 'fireboom-wundersdk/server'\nimport { Mutations, Queries } from 'generated/fireboom.internal.client';\nimport axios from 'axios';\n\nconst PromptInput = new GraphQLInputObjectType({\n name: 'PromptInput',\n fields: () =\u003e ({\n prompt: { type: GraphQLString },\n chatId: { type: GraphQLInt },\n regenId: { type: GraphQLInt }\n })\n});\n\nconst OpenAI = new GraphQLObjectType({\n name: 'OpenAI',\n description: 'OpenAI API proxy',\n fields: () =\u003e {\n return {\n completion: {\n type: GraphQLString,\n args: {\n promptInput: { type: PromptInput }\n },\n subscribe: async ({ }, { promptInput }, ctx) =\u003e {\n const pubsub = new PubSub();\n const channel = String(Date.now());\n console.log(promptInput)\n const { log, internalClient } = ctx.wundergraph\n\n try {\n //const getMsgRes = await internalClient.queries.ChatGPT__Chat__GetManyChatMessage({ input: { equals: promptInput.chatId } })\n //const msg = getMsgRes.data.data.map((x: { text: any, parentMessageId: any }) =\u003e ({ content: x.text, role: (x.parentMessageId ? \"assistant\" : \"user\") }))\n const msg = []\n msg.push(({ \"role\": \"user\", \"content\": promptInput.prompt }))\n axios.post(\n 'https://api.openai.com/v1/chat/completions',\n {\n \"model\": \"gpt-3.5-turbo\",\n \"messages\": msg,\n stream: true\n },\n {\n headers: {\n Authorization: `Bearer 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`,\n 'Content-Type': 'application/json',\n },\n responseType: 'stream', // 设为流响应类型\n }\n ).then(res =\u003e {\n const id = Math.floor(Math.random() * 1e10)\n const msgData = [] as any\n res.data.on('data', (data: any) =\u003e {\n data.toString().split('\\n').filter((x: string) =\u003e x.trim()).forEach((line: string) =\u003e {\n if (line === 'data: [DONE]') {\n console.log('完球')\n //internalClient.mutations.ChatGPT__Chat__CreateOneChatMessage({ input: { equals: promptInput.chatId } })\n pubsub.publish(channel, {completion: JSON.stringify({ data: msgData.join(''), id: id, finish: true })})\n } else {\n const json = JSON.parse(line.substring(6))\n msgData.push(json.choices[0].delta.content || '')\n pubsub.publish(channel, {completion: JSON.stringify({ data: msgData.join(''), id: id, finish: false })})\n }\n })\n })\n }).catch(err =\u003e {\n console.log(err)\n })\n\n } catch (e) {\n console.error(e)\n }\n\n // Return the initial completion message to the client\n return pubsub.asyncIterator(channel)\n }\n }\n };\n }\n});\n// Define the GraphQL schema\nconst schema = new GraphQLSchema({\n subscription: OpenAI,\n query: new GraphQLObjectType\u003cany, {\n wundergraph: {\n log: FastifyBaseLogger,\n internalClient: InternalClient\u003cQueries, Mutations\u003e\n }\n }\u003e({\n name: 'Query',\n fields: {\n hello: {\n type: GraphQLString,\n resolve() {\n return 'world'\n },\n },\n },\n }\n ),\n});\n\nexport default schema;\n","enabled":true,"scriptType":"","input":null,"type":""} -------------------------------------------------------------------------------- /server/store/hooks/global/onConnectionInit.config.json: -------------------------------------------------------------------------------- 1 | {"path":"global/onConnectionInit","enabled":false} 2 | -------------------------------------------------------------------------------- /server/store/hooks/global/onRequest.config.json: -------------------------------------------------------------------------------- 1 | {"path":"global/onRequest","depend":null,"script":"\nimport type { WunderGraphRequest } from 'fireboom-wundersdk/server'\nimport { HttpTransportHookRequest, SKIP, CANCEL } from 'generated/fireboom.hooks'\n\nexport default async function onOriginRequest(hook: HttpTransportHookRequest) : Promise\u003cWunderGraphRequest | SKIP | CANCEL\u003e{\n\t// TODO: 在此处添加代码\n console.log(\"global onRequest\")\n\treturn hook.request\n}\n","enabled":false,"scriptType":"","input":null,"type":""} -------------------------------------------------------------------------------- /server/store/hooks/global/onResponse.config.json: -------------------------------------------------------------------------------- 1 | {"path":"global/onResponse","depend":null,"script":"\nimport type { WunderGraphResponse } from 'fireboom-wundersdk/server'\nimport { HttpTransportHookRequestWithResponse, SKIP, CANCEL } from 'generated/fireboom.hooks'\n\nexport default async function onOriginResponse(hook: HttpTransportHookRequestWithResponse) : Promise\u003cWunderGraphResponse | SKIP | CANCEL\u003e{\n\t// TODO: 在此处添加代码\n\treturn hook.response\n}\n","enabled":false,"scriptType":"","input":null,"type":""} -------------------------------------------------------------------------------- /server/store/list/FbAuthentication: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 1402506998906880, 4 | "point": "http://localhost:9991/auth/cookie/authorize/casdoor?redirect_uri=", 5 | "name": "casdoor", 6 | "authSupplier": "openid", 7 | "switchState": [ 8 | "cookieBased", 9 | "tokenBased" 10 | ], 11 | "config": { 12 | "id": "casdoor", 13 | "clientId": { 14 | "key": "", 15 | "kind": "0", 16 | "val": "aca90f01227622516181" 17 | }, 18 | "clientSecret": { 19 | "key": "", 20 | "kind": "0", 21 | "val": "f407c9c1f309c29d2ecd1afbc4d386b6588cab03" 22 | }, 23 | "issuer": "https://oidc.100ai.com.cn", 24 | "discoveryURL": "", 25 | "jwks": 0, 26 | "jwksJSON": "", 27 | "jwksURL": "https://oidc.100ai.com.cn/.well-known/jwks.json", 28 | "userInfoEndpoint": "https://oidc.100ai.com.cn/api/userinfo", 29 | "userInfoCacheTtlSeconds": 0, 30 | "AuthorizationEndpoint": "https://oidc.100ai.com.cn/login/oauth/authorize", 31 | "tokenEndpoint": "https://oidc.100ai.com.cn/api/login/oauth/access_token" 32 | }, 33 | "createTime": "2023-03-08 06:25:03Z", 34 | "updateTime": "2023-04-14 08:56:51Z", 35 | "deleteTime": "" 36 | } 37 | ] -------------------------------------------------------------------------------- /server/store/list/FbDataSource: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "config": { 4 | "apiNamespace": "grace", 5 | "appendType": "", 6 | "databaseUrl": { 7 | "kind": "0", 8 | "val": "grace.db" 9 | }, 10 | "dbName": "", 11 | "dbType": "SQLite", 12 | "host": "", 13 | "password": { 14 | "key": "", 15 | "kind": "0", 16 | "val": "" 17 | }, 18 | "port": "", 19 | "replaceJSONTypeFieldConfiguration": null, 20 | "schemaExtension": "", 21 | "userName": { 22 | "key": "", 23 | "kind": "0", 24 | "val": "" 25 | } 26 | }, 27 | "createTime": "2023-03-20 09:49:14Z", 28 | "deleteTime": "", 29 | "enabled": true, 30 | "id": 1471257658589184, 31 | "name": "grace", 32 | "sourceType": 1, 33 | "updateTime": "2023-03-24 11:48:02Z" 34 | }, 35 | { 36 | "id": 1525988756684800, 37 | "name": "system", 38 | "sourceType": 2, 39 | "config": { 40 | "apiNameSpace": "system", 41 | "baseURL": "http://localhost:9123", 42 | "filePath": "openapi.json", 43 | "headers": [] 44 | }, 45 | "enabled": true, 46 | "createTime": "2023-03-30 01:48:05Z", 47 | "updateTime": "", 48 | "deleteTime": "" 49 | } 50 | ] -------------------------------------------------------------------------------- /server/store/list/FbRole: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "code": "admin", 4 | "createTime": "2023-03-01 02:23:47Z", 5 | "deleteTime": "", 6 | "id": 1361922157314048, 7 | "remark": "", 8 | "updateTime": "" 9 | }, 10 | { 11 | "id": 1361922158690304, 12 | "code": "user", 13 | "remark": "", 14 | "createTime": "2023-03-01 02:23:47Z", 15 | "updateTime": "", 16 | "deleteTime": "" 17 | } 18 | ] -------------------------------------------------------------------------------- /server/store/list/FbStorageBucket: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /server/store/object/global_operation_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "authenticationQueriesRequired": true, 3 | "authenticationMutationsRequired": true, 4 | "authenticationSubscriptionsRequired": true, 5 | "enabled": false, 6 | "authenticationRequired": false, 7 | "cachingEnabled": false, 8 | "cachingMaxAge": 0, 9 | "cachingStaleWhileRevalidate": 0, 10 | "liveQueryEnabled": false, 11 | "liveQueryPollingIntervalSeconds": 0 12 | } -------------------------------------------------------------------------------- /server/store/object/operations/Chat/CreateOne.json: -------------------------------------------------------------------------------- 1 | { 2 | "authenticationQueriesRequired": false, 3 | "authenticationMutationsRequired": false, 4 | "authenticationSubscriptionsRequired": false, 5 | "enabled": false, 6 | "authenticationRequired": false, 7 | "cachingEnabled": false, 8 | "cachingMaxAge": 0, 9 | "cachingStaleWhileRevalidate": 0, 10 | "liveQueryEnabled": false, 11 | "liveQueryPollingIntervalSeconds": 0 12 | } -------------------------------------------------------------------------------- /server/store/object/operations/Chat/DeleteOne.json: -------------------------------------------------------------------------------- 1 | { 2 | "authenticationQueriesRequired": false, 3 | "authenticationMutationsRequired": false, 4 | "authenticationSubscriptionsRequired": false, 5 | "enabled": false, 6 | "authenticationRequired": false, 7 | "cachingEnabled": false, 8 | "cachingMaxAge": 0, 9 | "cachingStaleWhileRevalidate": 0, 10 | "liveQueryEnabled": false, 11 | "liveQueryPollingIntervalSeconds": 0 12 | } -------------------------------------------------------------------------------- /server/store/object/operations/Chat/GetByHistory.json: -------------------------------------------------------------------------------- 1 | { 2 | "authenticationQueriesRequired": false, 3 | "authenticationMutationsRequired": false, 4 | "authenticationSubscriptionsRequired": false, 5 | "enabled": false, 6 | "authenticationRequired": false, 7 | "cachingEnabled": false, 8 | "cachingMaxAge": 0, 9 | "cachingStaleWhileRevalidate": 0, 10 | "liveQueryEnabled": false, 11 | "liveQueryPollingIntervalSeconds": 0 12 | } -------------------------------------------------------------------------------- /server/store/object/operations/Chat/GetMyHistoryChats.json: -------------------------------------------------------------------------------- 1 | { 2 | "authenticationQueriesRequired": false, 3 | "authenticationMutationsRequired": false, 4 | "authenticationSubscriptionsRequired": false, 5 | "enabled": false, 6 | "authenticationRequired": false, 7 | "cachingEnabled": false, 8 | "cachingMaxAge": 0, 9 | "cachingStaleWhileRevalidate": 0, 10 | "liveQueryEnabled": false, 11 | "liveQueryPollingIntervalSeconds": 0 12 | } -------------------------------------------------------------------------------- /server/store/object/operations/Chat/UpdateChatText.json: -------------------------------------------------------------------------------- 1 | { 2 | "authenticationQueriesRequired": false, 3 | "authenticationMutationsRequired": false, 4 | "authenticationSubscriptionsRequired": false, 5 | "enabled": false, 6 | "authenticationRequired": false, 7 | "cachingEnabled": false, 8 | "cachingMaxAge": 0, 9 | "cachingStaleWhileRevalidate": 0, 10 | "liveQueryEnabled": false, 11 | "liveQueryPollingIntervalSeconds": 0 12 | } -------------------------------------------------------------------------------- /server/store/object/operations/History/CreateOne.json: -------------------------------------------------------------------------------- 1 | { 2 | "authenticationQueriesRequired": false, 3 | "authenticationMutationsRequired": false, 4 | "authenticationSubscriptionsRequired": false, 5 | "enabled": false, 6 | "authenticationRequired": false, 7 | "cachingEnabled": false, 8 | "cachingMaxAge": 0, 9 | "cachingStaleWhileRevalidate": 0, 10 | "liveQueryEnabled": false, 11 | "liveQueryPollingIntervalSeconds": 0 12 | } -------------------------------------------------------------------------------- /server/store/object/operations/History/DeleteOne.json: -------------------------------------------------------------------------------- 1 | { 2 | "authenticationQueriesRequired": false, 3 | "authenticationMutationsRequired": false, 4 | "authenticationSubscriptionsRequired": false, 5 | "enabled": false, 6 | "authenticationRequired": false, 7 | "cachingEnabled": false, 8 | "cachingMaxAge": 0, 9 | "cachingStaleWhileRevalidate": 0, 10 | "liveQueryEnabled": false, 11 | "liveQueryPollingIntervalSeconds": 0 12 | } -------------------------------------------------------------------------------- /server/store/object/operations/History/GetList.json: -------------------------------------------------------------------------------- 1 | { 2 | "authenticationQueriesRequired": false, 3 | "authenticationMutationsRequired": false, 4 | "authenticationSubscriptionsRequired": false, 5 | "enabled": false, 6 | "authenticationRequired": false, 7 | "cachingEnabled": false, 8 | "cachingMaxAge": 0, 9 | "cachingStaleWhileRevalidate": 0, 10 | "liveQueryEnabled": false, 11 | "liveQueryPollingIntervalSeconds": 0 12 | } -------------------------------------------------------------------------------- /server/store/object/operations/History/UpdateOne.json: -------------------------------------------------------------------------------- 1 | { 2 | "authenticationQueriesRequired": false, 3 | "authenticationMutationsRequired": false, 4 | "authenticationSubscriptionsRequired": false, 5 | "enabled": false, 6 | "authenticationRequired": false, 7 | "cachingEnabled": false, 8 | "cachingMaxAge": 0, 9 | "cachingStaleWhileRevalidate": 0, 10 | "liveQueryEnabled": false, 11 | "liveQueryPollingIntervalSeconds": 0 12 | } -------------------------------------------------------------------------------- /server/store/object/operations/Propmt/CreateOne.json: -------------------------------------------------------------------------------- 1 | { 2 | "authenticationQueriesRequired": false, 3 | "authenticationMutationsRequired": false, 4 | "authenticationSubscriptionsRequired": false, 5 | "enabled": false, 6 | "authenticationRequired": false, 7 | "cachingEnabled": false, 8 | "cachingMaxAge": 0, 9 | "cachingStaleWhileRevalidate": 0, 10 | "liveQueryEnabled": false, 11 | "liveQueryPollingIntervalSeconds": 0 12 | } -------------------------------------------------------------------------------- /server/store/object/operations/Propmt/DeleteMany.json: -------------------------------------------------------------------------------- 1 | { 2 | "authenticationQueriesRequired": false, 3 | "authenticationMutationsRequired": false, 4 | "authenticationSubscriptionsRequired": false, 5 | "enabled": false, 6 | "authenticationRequired": false, 7 | "cachingEnabled": false, 8 | "cachingMaxAge": 0, 9 | "cachingStaleWhileRevalidate": 0, 10 | "liveQueryEnabled": false, 11 | "liveQueryPollingIntervalSeconds": 0 12 | } -------------------------------------------------------------------------------- /server/store/object/operations/Propmt/DeleteOne.json: -------------------------------------------------------------------------------- 1 | { 2 | "authenticationQueriesRequired": false, 3 | "authenticationMutationsRequired": false, 4 | "authenticationSubscriptionsRequired": false, 5 | "enabled": false, 6 | "authenticationRequired": false, 7 | "cachingEnabled": false, 8 | "cachingMaxAge": 0, 9 | "cachingStaleWhileRevalidate": 0, 10 | "liveQueryEnabled": false, 11 | "liveQueryPollingIntervalSeconds": 0 12 | } -------------------------------------------------------------------------------- /server/store/object/operations/Propmt/GetList.json: -------------------------------------------------------------------------------- 1 | { 2 | "authenticationQueriesRequired": false, 3 | "authenticationMutationsRequired": false, 4 | "authenticationSubscriptionsRequired": false, 5 | "enabled": false, 6 | "authenticationRequired": false, 7 | "cachingEnabled": false, 8 | "cachingMaxAge": 0, 9 | "cachingStaleWhileRevalidate": 0, 10 | "liveQueryEnabled": false, 11 | "liveQueryPollingIntervalSeconds": 0 12 | } -------------------------------------------------------------------------------- /server/store/object/operations/Propmt/UpdateOne.json: -------------------------------------------------------------------------------- 1 | { 2 | "authenticationQueriesRequired": false, 3 | "authenticationMutationsRequired": false, 4 | "authenticationSubscriptionsRequired": false, 5 | "enabled": false, 6 | "authenticationRequired": false, 7 | "cachingEnabled": false, 8 | "cachingMaxAge": 0, 9 | "cachingStaleWhileRevalidate": 0, 10 | "liveQueryEnabled": false, 11 | "liveQueryPollingIntervalSeconds": 0 12 | } -------------------------------------------------------------------------------- /server/store/object/operations/User/CreateOneUser.json: -------------------------------------------------------------------------------- 1 | { 2 | "authenticationQueriesRequired": false, 3 | "authenticationMutationsRequired": false, 4 | "authenticationSubscriptionsRequired": false, 5 | "enabled": false, 6 | "authenticationRequired": false, 7 | "cachingEnabled": false, 8 | "cachingMaxAge": 0, 9 | "cachingStaleWhileRevalidate": 0, 10 | "liveQueryEnabled": false, 11 | "liveQueryPollingIntervalSeconds": 0 12 | } -------------------------------------------------------------------------------- /server/store/object/operations/User/GetOneUser.json: -------------------------------------------------------------------------------- 1 | { 2 | "authenticationQueriesRequired": false, 3 | "authenticationMutationsRequired": false, 4 | "authenticationSubscriptionsRequired": false, 5 | "enabled": false, 6 | "authenticationRequired": false, 7 | "cachingEnabled": false, 8 | "cachingMaxAge": 0, 9 | "cachingStaleWhileRevalidate": 0, 10 | "liveQueryEnabled": false, 11 | "liveQueryPollingIntervalSeconds": 0 12 | } -------------------------------------------------------------------------------- /server/store/object/operations/User/Me.json: -------------------------------------------------------------------------------- 1 | { 2 | "authenticationQueriesRequired": false, 3 | "authenticationMutationsRequired": false, 4 | "authenticationSubscriptionsRequired": false, 5 | "enabled": false, 6 | "authenticationRequired": false, 7 | "cachingEnabled": false, 8 | "cachingMaxAge": 0, 9 | "cachingStaleWhileRevalidate": 0, 10 | "liveQueryEnabled": false, 11 | "liveQueryPollingIntervalSeconds": 0 12 | } -------------------------------------------------------------------------------- /server/store/object/operations/User/UpdateInfo.json: -------------------------------------------------------------------------------- 1 | { 2 | "authenticationQueriesRequired": false, 3 | "authenticationMutationsRequired": false, 4 | "authenticationSubscriptionsRequired": false, 5 | "enabled": false, 6 | "authenticationRequired": false, 7 | "cachingEnabled": false, 8 | "cachingMaxAge": 0, 9 | "cachingStaleWhileRevalidate": 0, 10 | "liveQueryEnabled": false, 11 | "liveQueryPollingIntervalSeconds": 0 12 | } -------------------------------------------------------------------------------- /server/template/vue/files/claims.ts.hbs: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { WunderGraphUser } from "fireboom-wundersdk/server" 3 | export type Role = "{{{joinString '" | "' roles}}}" 4 | 5 | export interface CustomClaims { 6 | [key: string]: any 7 | } 8 | 9 | export interface User extends WunderGraphUser {} 10 | -------------------------------------------------------------------------------- /server/template/vue/files/client.ts.hbs: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { 3 | Client, 4 | ClientConfig, 5 | CreateClientConfig, 6 | User, 7 | UploadRequestOptions, 8 | UploadRequestOptionsWithProfile, 9 | OperationMetadata, 10 | OperationsDefinition, 11 | OperationRequestOptions, 12 | SubscriptionRequestOptions, 13 | SubscriptionEventHandler, 14 | FetchUserRequestOptions 15 | // UploadValidationOptions 16 | } from "fireboom-wundersdk/client"; 17 | 18 | import type { CustomClaims, Role } from './claims' 19 | import type { {{> operation_partial operations=operations includeInternal=false includeInject=false includeResponseData=true}} } from './models' 20 | 21 | export const WUNDERGRAPH_S3_ENABLED = {{isNotEmpty s3Providers}} 22 | export const WUNDERGRAPH_AUTH_ENABLED = {{isNotEmpty authProviders}} 23 | 24 | {{#if (isNotEmpty s3Providers)}} 25 | export interface UploadResponse { key: string } 26 | 27 | // TODO: missing upload profiles 28 | 29 | type S3Providers ={ 30 | {{#each s3Providers}} 31 | {{name}}: '{{name}}' 32 | {{/each}} 33 | } 34 | 35 | const S3UploadProviderData: { [provider: string]: { [profile: string]: UploadValidationOptions } } = { 36 | {{#each s3Providers }} 37 | {{name}}: '{{name}}', 38 | {{/each}} 39 | } 40 | {{/if}} 41 | 42 | {{#if (isNotEmpty authProviders)}} 43 | export enum AuthProviderId { 44 | {{#each authProviders}} 45 | "{{id}}" = "{{id}}", 46 | {{/each}} 47 | } 48 | 49 | export interface AuthProvider { 50 | id: AuthProviderId; 51 | login: (redirectURI?: string) => void; 52 | } 53 | {{/if}} 54 | 55 | export const defaultClientConfig: ClientConfig = { 56 | applicationHash: "{{applicationHash}}", 57 | baseURL: "{{baseURL}}", 58 | sdkVersion: "{{sdkVersion}}" 59 | } 60 | 61 | export const operationMetadata: OperationMetadata = { 62 | {{#each operations}} 63 | "{{path}}": { 64 | requiresAuthentication: {{requiresAuthentication}} 65 | } 66 | {{#unless @last}},{{/unless}} 67 | {{/each}} 68 | } 69 | 70 | export class WunderGraphClient extends Client { 71 | query< 72 | OperationName extends Extract, 73 | Input extends Operations['queries'][OperationName]['input'] = Operations['queries'][OperationName]['input'], 74 | Data extends Operations['queries'][OperationName]['data'] = Operations['queries'][OperationName]['data'] 75 | >(options: OperationName extends string ? OperationRequestOptions : OperationRequestOptions) { 76 | return super.query(options); 77 | } 78 | mutate< 79 | OperationName extends Extract, 80 | Input extends Operations['mutations'][OperationName]['input'] = Operations['mutations'][OperationName]['input'], 81 | Data extends Operations['mutations'][OperationName]['data'] = Operations['mutations'][OperationName]['data'] 82 | >(options: OperationName extends string ? OperationRequestOptions : OperationRequestOptions) { 83 | return super.mutate(options); 84 | } 85 | subscribe< 86 | OperationName extends Extract, 87 | Input extends Operations['subscriptions'][OperationName]['input'] = Operations['subscriptions'][OperationName]['input'], 88 | Data extends Operations['subscriptions'][OperationName]['data'] = Operations['subscriptions'][OperationName]['data'] 89 | >( 90 | options: OperationName extends string 91 | ? SubscriptionRequestOptions 92 | : SubscriptionRequestOptions, 93 | cb: SubscriptionEventHandler 94 | ) { 95 | return super.subscribe(options, cb); 96 | } 97 | {{#if (isNotEmpty s3Providers)}} 98 | public async uploadFiles< 99 | ProviderName extends Extract, 100 | any, any 101 | >( 102 | config: any 103 | ) { 104 | {{!-- const profile = config.profile ? S3UploadProviderData[config.provider][config.profile as string] : undefined; --}} 105 | return super.uploadFiles(config, undefined); 106 | } 107 | {{/if}} 108 | public login(authProviderID: Operations['authProvider'], redirectURI?: string) { 109 | return super.login(authProviderID, redirectURI); 110 | } 111 | public async fetchUser>(options?: FetchUserRequestOptions) { 112 | return super.fetchUser(options); 113 | } 114 | } 115 | 116 | export const createClient = (config?: CreateClientConfig) => { 117 | return new WunderGraphClient({ 118 | ...defaultClientConfig, 119 | ...config, 120 | operationMetadata, 121 | csrfEnabled: {{isNotEmpty authProviders}}, 122 | }); 123 | }; 124 | 125 | export type Queries = { 126 | {{#each (filterOperations operations 'isQuery,!isInternal' true)}} 127 | '{{path}}': { 128 | {{#if hasInput}}input: {{name}}Input{{else}}input?: undefined{{/if}} 129 | data: {{name}}ResponseData 130 | requiresAuthentication: {{requiresAuthentication}} 131 | {{#if isLiveQuery}}liveQuery: boolean{{/if}} 132 | } 133 | {{/each}} 134 | } 135 | 136 | export type Mutations = { 137 | {{#each (filterOperations operations 'isMutation,!isInternal' true)}} 138 | '{{path}}': { 139 | {{#if hasInput}}input: {{name}}Input{{else}}input?: undefined{{/if}} 140 | data: {{name}}ResponseData 141 | requiresAuthentication: {{requiresAuthentication}} 142 | } 143 | {{/each}} 144 | } 145 | 146 | export type Subscriptions = { 147 | {{#each (filterOperations operations 'isSubscription,!isInternal' true)}} 148 | '{{path}}': { 149 | {{#if hasInput}}input: {{name}}Input{{else}}input?: undefined{{/if}} 150 | data: {{name}}ResponseData 151 | requiresAuthentication: {{requiresAuthentication}} 152 | } 153 | {{/each}} 154 | } 155 | 156 | export type LiveQueries = { 157 | {{#each (filterOperations operations 'isMutation,isLiveQuery,!isInternal' true)}} 158 | "{{path}}": { 159 | {{#if hasInput}}input: {{name}}Input{{else}}input?: undefined{{/if}} 160 | data: {{name}}ResponseData 161 | liveQuery: true 162 | requiresAuthentication: {{requiresAuthentication}} 163 | } 164 | {{/each}} 165 | } 166 | 167 | export interface Operations extends OperationsDefinition {} 168 | -------------------------------------------------------------------------------- /server/template/vue/files/models.ts.hbs: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | {{#with (filterOperations operations "engine" 1)}} 3 | {{#each this}} 4 | import type function_{{name}} from '../../server/custom-ts/operations/{{path}}' 5 | {{/each}} 6 | {{~#if (isNotEmpty this)}} 7 | import type { ExtractResponse } from 'fireboom-wundersdk/operations' 8 | {{/if}} 9 | {{~/with}} 10 | {{#each operations~}} 11 | {{#if (isNotEmpty inputSchema)~}} 12 | {{#if hasInput~}} 13 | export interface {{name}}Input { 14 | {{#each inputSchema.properties as |schema key|}} 15 | {{> schema_partial data=schema.properties key=key isRequired=(stringInArray key inputSchema.required)}} 16 | {{/each}} 17 | } 18 | {{/if}} 19 | {{~#if hasInternalInput~}} 20 | export interface Internal{{name}}Input { 21 | {{#each internalSchema.properties as |schema key|}} 22 | {{> schema_partial data=schema.properties key=key isRequired=(stringInArray key inputSchema.required)}} 23 | {{/each}} 24 | } 25 | {{/if~}} 26 | {{!-- {{~#each inputSchema.definitions as |schema name|}} 27 | export interface {{name}} { 28 | {{#each schema.properties as |schema key|}} 29 | {{> schema_partial data=schema.properties key=key isRequired=(stringInArray key inputSchema.required)}} 30 | {{/each}} 31 | } 32 | {{/each~}} --}} 33 | {{/if~}} 34 | 35 | {{~#equal engine 0}} 36 | {{~#if (isNotEmpty injectedSchema)}} 37 | {{#if hasInjectedInput}} 38 | export interface Injected{{name}}Input { 39 | {{#each injectedSchema.properties as |schema key|}} 40 | {{> schema_partial data=schema.properties key=key isRequired=(stringInArray key injectedSchema.required)}} 41 | {{/each}} 42 | } 43 | {{/if~}} 44 | {{~/if}} 45 | {{~/equal}} 46 | 47 | {{#equal engine 1}} 48 | export type {{name}}ResponseData = ExtractResponse 49 | 50 | export interface {{name}}Response { 51 | data?: {{name}}ResponseData 52 | errors?: ReadonlyArray; 53 | } 54 | {{/equal}} 55 | {{#equal engine 0}} 56 | export interface {{name}}Response { 57 | data?: {{#if (isNotEmpty responseSchema)}}{{name}}ResponseData{{else}}null{{/if}} 58 | errors?: ReadonlyArray; 59 | } 60 | {{#if (isNotEmpty responseSchema)}} 61 | {{#with responseSchema.properties.data}} 62 | export interface {{name}}ResponseData { 63 | {{#each properties as |schema key|}} 64 | {{> schema_partial data=schema.properties key=key isRequired=(stringInArray key required)~}} 65 | {{/each}} 66 | } 67 | {{/with}} 68 | {{~/if}} 69 | {{~/equal}} 70 | {{~/each}} 71 | 72 | export type JSONValue = string | number | boolean | JSONObject | Array; 73 | 74 | export type JSONObject = { [key: string]: JSONValue }; 75 | 76 | export interface GraphQLError { 77 | message: string; 78 | path?: ReadonlyArray; 79 | } 80 | -------------------------------------------------------------------------------- /server/template/vue/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue sdk", 3 | "author": "fireboom", 4 | "version": "0.1.0", 5 | "type": "hook", 6 | "description": "这里是sdk的描述", 7 | "outputPath": "../src/services", 8 | "enabled": true 9 | } 10 | -------------------------------------------------------------------------------- /server/template/vue/partials/operation_partial.hbs: -------------------------------------------------------------------------------- 1 | {{#each operations~}} 2 | {{~#if hasInput~}} 3 | {{name}}Input, 4 | {{~/if~}} 5 | {{~#if hasInternalInput~}} 6 | {{~#if includeInternal~}} 7 | Internal{{name}}Input, 8 | {{~/if~}} 9 | {{~/if~}} 10 | {{~#equal engine 0}} 11 | {{~#if hasInjectedInput~}} 12 | {{~#if includeInject~}} 13 | Injected{{name}}Input, 14 | {{~/if~}} 15 | {{~/if~}} 16 | {{~/equal~}} 17 | {{~name}}Response, 18 | {{~#if includeResponseData~}} 19 | {{~#if (isNotEmpty responseSchema)}} 20 | {{name}}ResponseData, 21 | {{~/if~}} 22 | {{~#equal engine 1~}} 23 | {{name}}ResponseData, 24 | {{~/equal~}} 25 | {{~/if~}} 26 | {{~/each}} -------------------------------------------------------------------------------- /server/template/vue/partials/schema_partial.hbs: -------------------------------------------------------------------------------- 1 | {{#if (equalAny (realType type) 'string,bool,JSONValue')}} 2 | {{key}}{{#unless isRequired}}?{{/unless}}: {{realType type}} 3 | {{/if~}} 4 | 5 | {{#if (equalAny (realType type) 'integer,float')}} 6 | {{key}}{{#unless isRequired}}?{{/unless}}: number 7 | {{/if~}} 8 | 9 | {{~#if (equalAny (realType type) 'object')}} 10 | {{key}}{{#unless isRequired}}?{{/unless}}: { 11 | {{#each properties as |schema key|~}} 12 | {{> schema_partial data=schema.properties key=key isRequired=(stringInArray key required)}} 13 | {{~/each~}} 14 | }, 15 | {{/if~}} 16 | 17 | {{~#if (equalAny (realType type) 'array')}} 18 | {{key}}{{#unless isRequired}}?{{/unless}}: { 19 | {{#each items.properties as |schema key|~}} 20 | {{> schema_partial data=schema key=key isRequired=(stringInArray key required)}} 21 | {{~/each}} 22 | }[], 23 | {{/if~}} -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 23 | -------------------------------------------------------------------------------- /src/api/index.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosProgressEvent, GenericAbortSignal } from 'axios' 2 | import { post } from '@/utils/request' 3 | 4 | export function fetchChatAPI( 5 | prompt: string, 6 | options?: { conversationId?: string; parentMessageId?: string }, 7 | signal?: GenericAbortSignal, 8 | ) { 9 | return post({ 10 | url: '/chat', 11 | data: { prompt, options }, 12 | signal, 13 | }) 14 | } 15 | 16 | export function fetchChatConfig() { 17 | return post({ 18 | url: '/config', 19 | }) 20 | } 21 | 22 | export function fetchChatAPIProcess( 23 | params: { 24 | prompt: string 25 | options?: { conversationId?: string; parentMessageId?: string } 26 | signal?: GenericAbortSignal 27 | onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void }, 28 | ) { 29 | return post({ 30 | url: '/chat-process', 31 | data: { prompt: params.prompt, options: params.options }, 32 | signal: params.signal, 33 | onDownloadProgress: params.onDownloadProgress, 34 | }) 35 | } 36 | 37 | // export function chatWithAI(params: { 38 | // prompt: string 39 | // chatId: string 40 | // parentMessageId?: string 41 | // signal?: GenericAbortSignal 42 | // }) { 43 | // return get({ 44 | // url: '/ChatGPT/Subscription/ChatSSE', 45 | // data: { 46 | // prompt: params.prompt, 47 | // chatId: params.chatId, 48 | // parentMessageId: params.parentMessageId, 49 | // wg_sse: true, 50 | // }, 51 | // responseType: 'stream', 52 | // signal: params.signal, 53 | // }) 54 | // } 55 | 56 | export function fetchSession() { 57 | return post({ 58 | url: '/session', 59 | }) 60 | } 61 | 62 | export function fetchSpeakConfig() { 63 | return post({ 64 | url: '/SpeakConfig', 65 | }) 66 | } 67 | 68 | export function fetchVerify(token: string) { 69 | return post({ 70 | url: '/verify', 71 | data: { token }, 72 | }) 73 | } 74 | -------------------------------------------------------------------------------- /src/assets/avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fireboomio/case-chatgpt-web/fd3aad35b4980d2240e4deb43297f838e5281747/src/assets/avatar.jpg -------------------------------------------------------------------------------- /src/assets/recommend.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "key": "chatgpt-prompt-collection", 4 | "desc": "Nothing1024收集整理的prompts", 5 | "downloadUrl": "https://raw.githubusercontent.com/Nothing1024/chatgpt-prompt-collection/main/awesome-chatgpt-prompts-zh.json", 6 | "url": "https://github.com/Nothing1024/chatgpt-prompt-collection" 7 | }, 8 | { 9 | "key": "awesome-chatgpt-prompts-zh", 10 | "desc": "ChatGPT 中文调教指南", 11 | "downloadUrl": "https://raw.githubusercontent.com/PlexPt/awesome-chatgpt-prompts-zh/main/prompts-zh.json", 12 | "url": "https://github.com/PlexPt/awesome-chatgpt-prompts-zh" 13 | } 14 | ] 15 | -------------------------------------------------------------------------------- /src/components/common/HoverButton/Button.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 21 | -------------------------------------------------------------------------------- /src/components/common/HoverButton/index.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 47 | -------------------------------------------------------------------------------- /src/components/common/NaiveProvider/index.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 44 | -------------------------------------------------------------------------------- /src/components/common/Setting/About.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 76 | -------------------------------------------------------------------------------- /src/components/common/Setting/General.vue: -------------------------------------------------------------------------------- 1 | 145 | 146 | 253 | -------------------------------------------------------------------------------- /src/components/common/Setting/index.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 55 | -------------------------------------------------------------------------------- /src/components/common/SvgIcon/index.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 22 | -------------------------------------------------------------------------------- /src/components/common/UserAvatar/index.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 41 | -------------------------------------------------------------------------------- /src/components/common/index.ts: -------------------------------------------------------------------------------- 1 | import HoverButton from './HoverButton/index.vue' 2 | import NaiveProvider from './NaiveProvider/index.vue' 3 | import SvgIcon from './SvgIcon/index.vue' 4 | import UserAvatar from './UserAvatar/index.vue' 5 | import Setting from './Setting/index.vue' 6 | import PromptStore from './PromptStore/index.vue' 7 | 8 | export { HoverButton, NaiveProvider, SvgIcon, UserAvatar, Setting, PromptStore } 9 | -------------------------------------------------------------------------------- /src/components/custom/GithubSite.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /src/components/custom/index.ts: -------------------------------------------------------------------------------- 1 | import GithubSite from './GithubSite.vue' 2 | 3 | export { GithubSite } 4 | -------------------------------------------------------------------------------- /src/hooks/useBasicLayout.ts: -------------------------------------------------------------------------------- 1 | import { breakpointsTailwind, useBreakpoints } from '@vueuse/core' 2 | 3 | export function useBasicLayout() { 4 | const breakpoints = useBreakpoints(breakpointsTailwind) 5 | const isMobile = breakpoints.smaller('sm') 6 | 7 | return { isMobile } 8 | } 9 | -------------------------------------------------------------------------------- /src/hooks/useIconRender.ts: -------------------------------------------------------------------------------- 1 | import { h } from 'vue' 2 | import { SvgIcon } from '@/components/common' 3 | 4 | export const useIconRender = () => { 5 | interface IconConfig { 6 | icon?: string 7 | color?: string 8 | fontSize?: number 9 | } 10 | 11 | interface IconStyle { 12 | color?: string 13 | fontSize?: string 14 | } 15 | 16 | const iconRender = (config: IconConfig) => { 17 | const { color, fontSize, icon } = config 18 | 19 | const style: IconStyle = {} 20 | 21 | if (color) 22 | style.color = color 23 | 24 | if (fontSize) 25 | style.fontSize = `${fontSize}px` 26 | 27 | if (!icon) 28 | window.console.warn('iconRender: icon is required') 29 | 30 | return () => h(SvgIcon, { icon, style }) 31 | } 32 | 33 | return { 34 | iconRender, 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/hooks/useLanguage.ts: -------------------------------------------------------------------------------- 1 | import { computed } from 'vue' 2 | import { enUS, zhCN, zhTW } from 'naive-ui' 3 | import { useAppStore } from '@/store' 4 | import { setLocale } from '@/locales' 5 | 6 | export function useLanguage() { 7 | const appStore = useAppStore() 8 | 9 | const language = computed(() => { 10 | switch (appStore.language) { 11 | case 'en-US': 12 | setLocale('en-US') 13 | return enUS 14 | case 'zh-CN': 15 | setLocale('zh-CN') 16 | return zhCN 17 | case 'zh-TW': 18 | setLocale('zh-TW') 19 | return zhTW 20 | default: 21 | setLocale('zh-CN') 22 | return enUS 23 | } 24 | }) 25 | 26 | return { language } 27 | } 28 | -------------------------------------------------------------------------------- /src/hooks/useTheme.ts: -------------------------------------------------------------------------------- 1 | import type { GlobalThemeOverrides } from 'naive-ui' 2 | import { computed, watch } from 'vue' 3 | import { darkTheme, useOsTheme } from 'naive-ui' 4 | import { useAppStore } from '@/store' 5 | 6 | export function useTheme() { 7 | const appStore = useAppStore() 8 | 9 | const OsTheme = useOsTheme() 10 | 11 | const isDark = computed(() => { 12 | if (appStore.theme === 'auto') 13 | return OsTheme.value === 'dark' 14 | else 15 | return appStore.theme === 'dark' 16 | }) 17 | 18 | const theme = computed(() => { 19 | return isDark.value ? darkTheme : undefined 20 | }) 21 | 22 | const themeOverrides = computed(() => { 23 | if (isDark.value) { 24 | return { 25 | common: {}, 26 | } 27 | } 28 | return {} 29 | }) 30 | 31 | watch( 32 | () => isDark.value, 33 | (dark) => { 34 | if (dark) 35 | document.documentElement.classList.add('dark') 36 | else 37 | document.documentElement.classList.remove('dark') 38 | }, 39 | { immediate: true }, 40 | ) 41 | 42 | return { theme, themeOverrides } 43 | } 44 | -------------------------------------------------------------------------------- /src/locales/en-US.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | common: { 3 | add: 'Add', 4 | addSuccess: 'Add Success', 5 | edit: 'Edit', 6 | editSuccess: 'Edit Success', 7 | delete: 'Delete', 8 | deleteSuccess: 'Delete Success', 9 | save: 'Save', 10 | saveSuccess: 'Save Success', 11 | reset: 'Reset', 12 | action: 'Action', 13 | export: 'Export', 14 | exportSuccess: 'Export Success', 15 | import: 'Import', 16 | importSuccess: 'Import Success', 17 | clear: 'Clear', 18 | clearSuccess: 'Clear Success', 19 | yes: 'Yes', 20 | no: 'No', 21 | confirm: 'Confirm', 22 | download: 'Download', 23 | noData: 'No Data', 24 | wrong: 'Something went wrong, please try again later.', 25 | success: 'Success', 26 | failed: 'Failed', 27 | verify: 'Verify', 28 | logout: 'Logout', 29 | unauthorizedTips: 'Unauthorized, please verify first.', 30 | }, 31 | chat: { 32 | placeholder: 'Ask me anything...(Shift + Enter = line break)', 33 | placeholderMobile: 'Ask me anything...', 34 | copy: 'Copy', 35 | copied: 'Copied', 36 | copyCode: 'Copy Code', 37 | clearChat: 'Clear Chat', 38 | clearChatConfirm: 'Are you sure to clear this chat?', 39 | exportImage: 'Export Image', 40 | exportImageConfirm: 'Are you sure to export this chat to png?', 41 | exportSuccess: 'Export Success', 42 | exportFailed: 'Export Failed', 43 | usingContext: 'Context Mode', 44 | turnOnContext: 'In the current mode, sending messages will carry previous chat records.', 45 | turnOffContext: 'In the current mode, sending messages will not carry previous chat records.', 46 | deleteMessage: 'Delete Message', 47 | deleteMessageConfirm: 'Are you sure to delete this message?', 48 | deleteHistoryConfirm: 'Are you sure to clear this history?', 49 | clearHistoryConfirm: 'Are you sure to clear chat history?', 50 | }, 51 | setting: { 52 | setting: 'Setting', 53 | general: 'General', 54 | config: 'Config', 55 | avatarLink: 'Avatar Link', 56 | name: 'Name', 57 | description: 'Description', 58 | resetUserInfo: 'Reset UserInfo', 59 | chatHistory: 'ChatHistory', 60 | theme: 'Theme', 61 | language: 'Language', 62 | api: 'API', 63 | reverseProxy: 'Reverse Proxy', 64 | timeout: 'Timeout', 65 | socks: 'Socks', 66 | httpsProxy: 'HTTPS Proxy', 67 | balance: 'API Balance', 68 | }, 69 | store: { 70 | local: 'Local', 71 | online: 'Online', 72 | title: 'Title', 73 | description: 'Description', 74 | clearStoreConfirm: 'Whether to clear the data?', 75 | importPlaceholder: 'Please paste the JSON data here', 76 | addRepeatTitleTips: 'Title duplicate, please re-enter', 77 | addRepeatContentTips: 'Content duplicate: {msg}, please re-enter', 78 | editRepeatTitleTips: 'Title conflict, please revise', 79 | editRepeatContentTips: 'Content conflict {msg} , please re-modify', 80 | importError: 'Key value mismatch', 81 | importRepeatTitle: 'Title repeatedly skipped: {msg}', 82 | importRepeatContent: 'Content is repeatedly skipped: {msg}', 83 | onlineImportWarning: 'Note: Please check the JSON file source!', 84 | downloadError: 'Please check the network status and JSON file validity', 85 | }, 86 | } 87 | -------------------------------------------------------------------------------- /src/locales/index.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue' 2 | import { createI18n } from 'vue-i18n' 3 | import enUS from './en-US' 4 | import zhCN from './zh-CN' 5 | import zhTW from './zh-TW' 6 | import { useAppStoreWithOut } from '@/store/modules/app' 7 | import type { Language } from '@/store/modules/app/helper' 8 | 9 | const appStore = useAppStoreWithOut() 10 | 11 | const defaultLocale = appStore.language || 'zh-CN' 12 | 13 | const i18n = createI18n({ 14 | locale: defaultLocale, 15 | fallbackLocale: 'en-US', 16 | allowComposition: true, 17 | messages: { 18 | 'en-US': enUS, 19 | 'zh-CN': zhCN, 20 | 'zh-TW': zhTW, 21 | }, 22 | }) 23 | 24 | export const t = i18n.global.t 25 | 26 | export function setLocale(locale: Language) { 27 | i18n.global.locale = locale 28 | } 29 | 30 | export function setupI18n(app: App) { 31 | app.use(i18n) 32 | } 33 | 34 | export default i18n 35 | -------------------------------------------------------------------------------- /src/locales/zh-CN.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | common: { 3 | add: '添加', 4 | addSuccess: '添加成功', 5 | edit: '编辑', 6 | editSuccess: '编辑成功', 7 | delete: '删除', 8 | deleteSuccess: '删除成功', 9 | save: '保存', 10 | saveSuccess: '保存成功', 11 | reset: '重置', 12 | action: '操作', 13 | export: '导出', 14 | exportSuccess: '导出成功', 15 | import: '导入', 16 | importSuccess: '导入成功', 17 | clear: '清空', 18 | clearSuccess: '清空成功', 19 | yes: '是', 20 | no: '否', 21 | confirm: '确定', 22 | download: '下载', 23 | noData: '暂无数据', 24 | wrong: '好像出错了,请稍后再试。', 25 | success: '操作成功', 26 | failed: '操作失败', 27 | verify: '登录', 28 | logout: '退出登录', 29 | unauthorizedTips: '未经授权,请先登录。', 30 | }, 31 | chat: { 32 | placeholder: '来说点什么吧...(Shift + Enter = 换行)', 33 | placeholderMobile: '来说点什么...', 34 | copy: '复制', 35 | copied: '复制成功', 36 | copyCode: '复制代码', 37 | clearChat: '清空会话', 38 | clearChatConfirm: '是否清空会话?', 39 | exportImage: '保存会话到图片', 40 | exportImageConfirm: '是否将会话保存为图片?', 41 | exportSuccess: '保存成功', 42 | exportFailed: '保存失败', 43 | usingContext: '上下文模式', 44 | turnOnContext: '当前模式下, 发送消息会携带之前的聊天记录', 45 | turnOffContext: '当前模式下, 发送消息不会携带之前的聊天记录', 46 | deleteMessage: '删除消息', 47 | deleteMessageConfirm: '是否删除此消息?', 48 | deleteHistoryConfirm: '确定删除此记录?', 49 | clearHistoryConfirm: '确定清空聊天记录?', 50 | }, 51 | setting: { 52 | setting: '设置', 53 | general: '总览', 54 | config: '配置', 55 | avatarLink: '头像链接', 56 | name: '名称', 57 | description: '描述', 58 | resetUserInfo: '重置用户信息', 59 | chatHistory: '聊天记录', 60 | theme: '主题', 61 | language: '语言', 62 | api: 'API', 63 | reverseProxy: '反向代理', 64 | timeout: '超时', 65 | socks: 'Socks', 66 | httpsProxy: 'HTTPS Proxy', 67 | balance: 'API余额', 68 | }, 69 | store: { 70 | local: '本地', 71 | online: '在线', 72 | title: '标题', 73 | description: '描述', 74 | clearStoreConfirm: '是否清空数据?', 75 | importPlaceholder: '请粘贴 JSON 数据到此处', 76 | addRepeatTitleTips: '标题重复,请重新输入', 77 | addRepeatContentTips: '内容重复:{msg},请重新输入', 78 | editRepeatTitleTips: '标题冲突,请重新修改', 79 | editRepeatContentTips: '内容冲突{msg} ,请重新修改', 80 | importError: '键值不匹配', 81 | importRepeatTitle: '标题重复跳过:{msg}', 82 | importRepeatContent: '内容重复跳过:{msg}', 83 | onlineImportWarning: '注意:请检查 JSON 文件来源!', 84 | downloadError: '请检查网络状态与 JSON 文件有效性', 85 | }, 86 | } 87 | -------------------------------------------------------------------------------- /src/locales/zh-TW.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | common: { 3 | add: '新增', 4 | addSuccess: '新增成功', 5 | edit: '編輯', 6 | editSuccess: '編輯成功', 7 | delete: '刪除', 8 | deleteSuccess: '刪除成功', 9 | save: '儲存', 10 | saveSuccess: '儲存成功', 11 | reset: '重設', 12 | action: '操作', 13 | export: '匯出', 14 | exportSuccess: '匯出成功', 15 | import: '匯入', 16 | importSuccess: '匯入成功', 17 | clear: '清除', 18 | clearSuccess: '清除成功', 19 | yes: '是', 20 | no: '否', 21 | confirm: '確認', 22 | download: '下載', 23 | noData: '目前無資料', 24 | wrong: '發生錯誤,請稍後再試。', 25 | success: '操作成功', 26 | failed: '操作失敗', 27 | verify: '驗證', 28 | logout: '退出登錄', 29 | unauthorizedTips: '未經授權,請先進行驗證。', 30 | }, 31 | chat: { 32 | placeholder: '來說點什麼...(Shift + Enter = 換行)', 33 | placeholderMobile: '來說點什麼...', 34 | copy: '複製', 35 | copied: '複製成功', 36 | copyCode: '複製代碼', 37 | clearChat: '清除對話', 38 | clearChatConfirm: '是否清空對話?', 39 | exportImage: '儲存對話為圖片', 40 | exportImageConfirm: '是否將對話儲存為圖片?', 41 | exportSuccess: '儲存成功', 42 | exportFailed: '儲存失敗', 43 | usingContext: '上下文模式', 44 | turnOnContext: '啟用上下文模式,在此模式下,發送訊息會包含之前的聊天記錄。', 45 | turnOffContext: '關閉上下文模式,在此模式下,發送訊息不會包含之前的聊天記錄。', 46 | deleteMessage: '刪除訊息', 47 | deleteMessageConfirm: '是否刪除此訊息?', 48 | deleteHistoryConfirm: '確定刪除此紀錄?', 49 | clearHistoryConfirm: '確定清除紀錄?', 50 | }, 51 | setting: { 52 | setting: '設定', 53 | general: '總覽', 54 | config: '設定', 55 | avatarLink: '頭貼連結', 56 | name: '名稱', 57 | description: '描述', 58 | resetUserInfo: '重設使用者資訊', 59 | chatHistory: '紀錄', 60 | theme: '主題', 61 | language: '語言', 62 | api: 'API', 63 | reverseProxy: '反向代理', 64 | timeout: '逾時', 65 | socks: 'Socks', 66 | httpsProxy: 'HTTPS Proxy', 67 | balance: 'API余額', 68 | }, 69 | store: { 70 | local: '本機', 71 | online: '線上', 72 | title: '標題', 73 | description: '描述', 74 | clearStoreConfirm: '是否清除資料?', 75 | importPlaceholder: '請將 JSON 資料貼在此處', 76 | addRepeatTitleTips: '標題重複,請重新輸入', 77 | addRepeatContentTips: '內容重複:{msg},請重新輸入', 78 | editRepeatTitleTips: '標題衝突,請重新修改', 79 | editRepeatContentTips: '內容衝突{msg} ,請重新修改', 80 | importError: '鍵值不符合', 81 | importRepeatTitle: '因標題重複跳過:{msg}', 82 | importRepeatContent: '因內容重複跳過:{msg}', 83 | onlineImportWarning: '注意:請檢查 JSON 檔案來源!', 84 | downloadError: '請檢查網路狀態與 JSON 檔案有效性', 85 | }, 86 | } 87 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | import { setupI18n } from './locales' 4 | import { setupAssets, setupScrollbarStyle } from './plugins' 5 | import { setupStore } from './store' 6 | import { setupRouter } from './router' 7 | 8 | async function bootstrap() { 9 | const app = createApp(App) 10 | setupAssets() 11 | 12 | setupScrollbarStyle() 13 | 14 | setupStore(app) 15 | 16 | setupI18n(app) 17 | 18 | await setupRouter(app) 19 | 20 | app.mount('#app') 21 | } 22 | 23 | bootstrap() 24 | -------------------------------------------------------------------------------- /src/plugins/assets.ts: -------------------------------------------------------------------------------- 1 | import 'katex/dist/katex.min.css' 2 | import '@/styles/lib/tailwind.css' 3 | import '@/styles/lib/highlight.less' 4 | import '@/styles/lib/github-markdown.less' 5 | import '@/styles/global.less' 6 | 7 | /** Tailwind's Preflight Style Override */ 8 | function naiveStyleOverride() { 9 | const meta = document.createElement('meta') 10 | meta.name = 'naive-ui-style' 11 | document.head.appendChild(meta) 12 | } 13 | 14 | function setupAssets() { 15 | naiveStyleOverride() 16 | } 17 | 18 | export default setupAssets 19 | -------------------------------------------------------------------------------- /src/plugins/index.ts: -------------------------------------------------------------------------------- 1 | import setupAssets from './assets' 2 | import setupScrollbarStyle from './scrollbarStyle' 3 | 4 | export { setupAssets, setupScrollbarStyle } 5 | -------------------------------------------------------------------------------- /src/plugins/scrollbarStyle.ts: -------------------------------------------------------------------------------- 1 | import { darkTheme, lightTheme } from 'naive-ui' 2 | 3 | const setupScrollbarStyle = () => { 4 | const style = document.createElement('style') 5 | const styleContent = ` 6 | ::-webkit-scrollbar { 7 | background-color: transparent; 8 | width: ${lightTheme.Scrollbar.common?.scrollbarWidth}; 9 | } 10 | ::-webkit-scrollbar-thumb { 11 | background-color: ${lightTheme.Scrollbar.common?.scrollbarColor}; 12 | border-radius: ${lightTheme.Scrollbar.common?.scrollbarBorderRadius}; 13 | } 14 | html.dark ::-webkit-scrollbar { 15 | background-color: transparent; 16 | width: ${darkTheme.Scrollbar.common?.scrollbarWidth}; 17 | } 18 | html.dark ::-webkit-scrollbar-thumb { 19 | background-color: ${darkTheme.Scrollbar.common?.scrollbarColor}; 20 | border-radius: ${darkTheme.Scrollbar.common?.scrollbarBorderRadius}; 21 | } 22 | ` 23 | 24 | style.innerHTML = styleContent 25 | document.head.appendChild(style) 26 | } 27 | 28 | export default setupScrollbarStyle 29 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue' 2 | import type { RouteRecordRaw } from 'vue-router' 3 | import { createRouter, createWebHashHistory } from 'vue-router' 4 | import { setupPageGuard } from './permission' 5 | import { ChatLayout } from '@/views/chat/layout' 6 | 7 | const routes: RouteRecordRaw[] = [ 8 | { 9 | path: '/', 10 | name: 'Root', 11 | component: ChatLayout, 12 | redirect: '/chat', 13 | children: [ 14 | { 15 | path: '/chat/:uuid?', 16 | name: 'Chat', 17 | component: () => import('@/views/chat/index.vue'), 18 | }, 19 | ], 20 | }, 21 | 22 | { 23 | path: '/404', 24 | name: '404', 25 | component: () => import('@/views/exception/404/index.vue'), 26 | }, 27 | 28 | { 29 | path: '/500', 30 | name: '500', 31 | component: () => import('@/views/exception/500/index.vue'), 32 | }, 33 | 34 | { 35 | path: '/:pathMatch(.*)*', 36 | name: 'notFound', 37 | redirect: '/404', 38 | }, 39 | ] 40 | 41 | export const router = createRouter({ 42 | history: createWebHashHistory(), 43 | routes, 44 | scrollBehavior: () => ({ left: 0, top: 0 }), 45 | }) 46 | 47 | setupPageGuard(router) 48 | 49 | export async function setupRouter(app: App) { 50 | app.use(router) 51 | await router.isReady() 52 | } 53 | -------------------------------------------------------------------------------- /src/router/permission.ts: -------------------------------------------------------------------------------- 1 | import type { Router } from 'vue-router' 2 | import { useAuthStoreWithout } from '@/store/modules/auth' 3 | 4 | export function setupPageGuard(router: Router) { 5 | router.beforeEach(async (to, from, next) => { 6 | const authStore = useAuthStoreWithout() 7 | if (!authStore.session) { 8 | try { 9 | const data = await authStore.getSession() 10 | if (String(data.auth) === 'false' && authStore.token) 11 | authStore.removeToken() 12 | next() 13 | } 14 | catch (error) { 15 | if (to.path !== '/500') 16 | next({ name: '500' }) 17 | else 18 | next() 19 | } 20 | } 21 | else { 22 | next() 23 | } 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /src/services/claims.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { WunderGraphUser } from "fireboom-wundersdk/server" 3 | export type Role = "admin" | "user" 4 | 5 | export interface CustomClaims { 6 | [key: string]: any 7 | } 8 | 9 | export interface User extends WunderGraphUser {} 10 | -------------------------------------------------------------------------------- /src/services/client.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { 3 | Client, 4 | ClientConfig, 5 | CreateClientConfig, 6 | User, 7 | UploadRequestOptions, 8 | UploadRequestOptionsWithProfile, 9 | OperationMetadata, 10 | OperationsDefinition, 11 | OperationRequestOptions, 12 | SubscriptionRequestOptions, 13 | SubscriptionEventHandler, 14 | FetchUserRequestOptions 15 | // UploadValidationOptions 16 | } from "fireboom-wundersdk/client"; 17 | 18 | import type { CustomClaims, Role } from './claims' 19 | import type { Chat__CreateOneInput,Chat__CreateOneResponse, Chat__CreateOneResponseData,Chat__DeleteOneInput,Chat__DeleteOneResponse, Chat__DeleteOneResponseData,Chat__GetByHistoryInput,Chat__GetByHistoryResponse, Chat__GetByHistoryResponseData,Chat__GetMyHistoryChatsInput,Chat__GetMyHistoryChatsResponse, Chat__GetMyHistoryChatsResponseData,Chat__UpdateChatTextInput,Chat__UpdateChatTextResponse, Chat__UpdateChatTextResponseData,History__CreateOneInput,History__CreateOneResponse, History__CreateOneResponseData,History__DeleteOneInput,History__DeleteOneResponse, History__DeleteOneResponseData,History__GetListResponse, History__GetListResponseData,History__UpdateOneInput,History__UpdateOneResponse, History__UpdateOneResponseData,Propmt__CreateOneInput,Propmt__CreateOneResponse, Propmt__CreateOneResponseData,Propmt__DeleteManyInput,Propmt__DeleteManyResponse, Propmt__DeleteManyResponseData,Propmt__DeleteOneInput,Propmt__DeleteOneResponse, Propmt__DeleteOneResponseData,Propmt__GetListInput,Propmt__GetListResponse, Propmt__GetListResponseData,Propmt__UpdateOneInput,Propmt__UpdateOneResponse, Propmt__UpdateOneResponseData,System__BindRoleApisInput,System__BindRoleApisResponse, System__BindRoleApisResponseData,System__GetRoleBindApisInput,System__GetRoleBindApisResponse, System__GetRoleBindApisResponseData,User__CreateOneUserInput,User__CreateOneUserResponse, User__CreateOneUserResponseData,User__GetOneUserInput,User__GetOneUserResponse, User__GetOneUserResponseData,User__MeResponse, User__MeResponseData,User__UpdateInfoInput,User__UpdateInfoResponse, User__UpdateInfoResponseData,Chat__ChatSSEInput,Chat__ChatSSEResponse,Chat__ChatSSEResponseData, } from './models' 20 | 21 | export const WUNDERGRAPH_S3_ENABLED = false 22 | export const WUNDERGRAPH_AUTH_ENABLED = true 23 | 24 | 25 | export enum AuthProviderId { 26 | "casdoor" = "casdoor", 27 | } 28 | 29 | export interface AuthProvider { 30 | id: AuthProviderId; 31 | login: (redirectURI?: string) => void; 32 | } 33 | 34 | export const defaultClientConfig: ClientConfig = { 35 | applicationHash: "fd3a4387", 36 | baseURL: "https://www.100ai.com.cn", 37 | sdkVersion: "" 38 | } 39 | 40 | export const operationMetadata: OperationMetadata = { 41 | "Chat/CreateOne": { 42 | requiresAuthentication: true 43 | } 44 | , 45 | "Chat/DeleteOne": { 46 | requiresAuthentication: true 47 | } 48 | , 49 | "Chat/GetByHistory": { 50 | requiresAuthentication: true 51 | } 52 | , 53 | "Chat/GetMyHistoryChats": { 54 | requiresAuthentication: true 55 | } 56 | , 57 | "Chat/UpdateChatText": { 58 | requiresAuthentication: true 59 | } 60 | , 61 | "History/CreateOne": { 62 | requiresAuthentication: true 63 | } 64 | , 65 | "History/DeleteOne": { 66 | requiresAuthentication: true 67 | } 68 | , 69 | "History/GetList": { 70 | requiresAuthentication: true 71 | } 72 | , 73 | "History/UpdateOne": { 74 | requiresAuthentication: true 75 | } 76 | , 77 | "Propmt/CreateOne": { 78 | requiresAuthentication: true 79 | } 80 | , 81 | "Propmt/DeleteMany": { 82 | requiresAuthentication: true 83 | } 84 | , 85 | "Propmt/DeleteOne": { 86 | requiresAuthentication: true 87 | } 88 | , 89 | "Propmt/GetList": { 90 | requiresAuthentication: true 91 | } 92 | , 93 | "Propmt/UpdateOne": { 94 | requiresAuthentication: true 95 | } 96 | , 97 | "System/BindRoleApis": { 98 | requiresAuthentication: false 99 | } 100 | , 101 | "System/GetRoleBindApis": { 102 | requiresAuthentication: false 103 | } 104 | , 105 | "User/CreateOneUser": { 106 | requiresAuthentication: true 107 | } 108 | , 109 | "User/GetOneUser": { 110 | requiresAuthentication: true 111 | } 112 | , 113 | "User/Me": { 114 | requiresAuthentication: true 115 | } 116 | , 117 | "User/UpdateInfo": { 118 | requiresAuthentication: true 119 | } 120 | , 121 | "Chat/ChatSSE": { 122 | requiresAuthentication: false 123 | } 124 | 125 | } 126 | 127 | export class WunderGraphClient extends Client { 128 | query< 129 | OperationName extends Extract, 130 | Input extends Operations['queries'][OperationName]['input'] = Operations['queries'][OperationName]['input'], 131 | Data extends Operations['queries'][OperationName]['data'] = Operations['queries'][OperationName]['data'] 132 | >(options: OperationName extends string ? OperationRequestOptions : OperationRequestOptions) { 133 | return super.query(options); 134 | } 135 | mutate< 136 | OperationName extends Extract, 137 | Input extends Operations['mutations'][OperationName]['input'] = Operations['mutations'][OperationName]['input'], 138 | Data extends Operations['mutations'][OperationName]['data'] = Operations['mutations'][OperationName]['data'] 139 | >(options: OperationName extends string ? OperationRequestOptions : OperationRequestOptions) { 140 | return super.mutate(options); 141 | } 142 | subscribe< 143 | OperationName extends Extract, 144 | Input extends Operations['subscriptions'][OperationName]['input'] = Operations['subscriptions'][OperationName]['input'], 145 | Data extends Operations['subscriptions'][OperationName]['data'] = Operations['subscriptions'][OperationName]['data'] 146 | >( 147 | options: OperationName extends string 148 | ? SubscriptionRequestOptions 149 | : SubscriptionRequestOptions, 150 | cb: SubscriptionEventHandler 151 | ) { 152 | return super.subscribe(options, cb); 153 | } 154 | public login(authProviderID: Operations['authProvider'], redirectURI?: string) { 155 | return super.login(authProviderID, redirectURI); 156 | } 157 | public async fetchUser>(options?: FetchUserRequestOptions) { 158 | return super.fetchUser(options); 159 | } 160 | } 161 | 162 | export const createClient = (config?: CreateClientConfig) => { 163 | return new WunderGraphClient({ 164 | ...defaultClientConfig, 165 | ...config, 166 | operationMetadata, 167 | csrfEnabled: true, 168 | }); 169 | }; 170 | 171 | export type Queries = { 172 | 'Chat/GetByHistory': { 173 | input: Chat__GetByHistoryInput 174 | data: Chat__GetByHistoryResponseData 175 | requiresAuthentication: true 176 | 177 | } 178 | 'Chat/GetMyHistoryChats': { 179 | input: Chat__GetMyHistoryChatsInput 180 | data: Chat__GetMyHistoryChatsResponseData 181 | requiresAuthentication: true 182 | 183 | } 184 | 'History/GetList': { 185 | input?: undefined 186 | data: History__GetListResponseData 187 | requiresAuthentication: true 188 | 189 | } 190 | 'Propmt/GetList': { 191 | input: Propmt__GetListInput 192 | data: Propmt__GetListResponseData 193 | requiresAuthentication: true 194 | 195 | } 196 | 'System/GetRoleBindApis': { 197 | input: System__GetRoleBindApisInput 198 | data: System__GetRoleBindApisResponseData 199 | requiresAuthentication: false 200 | 201 | } 202 | 'User/GetOneUser': { 203 | input: User__GetOneUserInput 204 | data: User__GetOneUserResponseData 205 | requiresAuthentication: true 206 | 207 | } 208 | 'User/Me': { 209 | input?: undefined 210 | data: User__MeResponseData 211 | requiresAuthentication: true 212 | 213 | } 214 | } 215 | 216 | export type Mutations = { 217 | 'Chat/CreateOne': { 218 | input: Chat__CreateOneInput 219 | data: Chat__CreateOneResponseData 220 | requiresAuthentication: true 221 | } 222 | 'Chat/DeleteOne': { 223 | input: Chat__DeleteOneInput 224 | data: Chat__DeleteOneResponseData 225 | requiresAuthentication: true 226 | } 227 | 'Chat/UpdateChatText': { 228 | input: Chat__UpdateChatTextInput 229 | data: Chat__UpdateChatTextResponseData 230 | requiresAuthentication: true 231 | } 232 | 'History/CreateOne': { 233 | input: History__CreateOneInput 234 | data: History__CreateOneResponseData 235 | requiresAuthentication: true 236 | } 237 | 'History/DeleteOne': { 238 | input: History__DeleteOneInput 239 | data: History__DeleteOneResponseData 240 | requiresAuthentication: true 241 | } 242 | 'History/UpdateOne': { 243 | input: History__UpdateOneInput 244 | data: History__UpdateOneResponseData 245 | requiresAuthentication: true 246 | } 247 | 'Propmt/CreateOne': { 248 | input: Propmt__CreateOneInput 249 | data: Propmt__CreateOneResponseData 250 | requiresAuthentication: true 251 | } 252 | 'Propmt/DeleteMany': { 253 | input: Propmt__DeleteManyInput 254 | data: Propmt__DeleteManyResponseData 255 | requiresAuthentication: true 256 | } 257 | 'Propmt/DeleteOne': { 258 | input: Propmt__DeleteOneInput 259 | data: Propmt__DeleteOneResponseData 260 | requiresAuthentication: true 261 | } 262 | 'Propmt/UpdateOne': { 263 | input: Propmt__UpdateOneInput 264 | data: Propmt__UpdateOneResponseData 265 | requiresAuthentication: true 266 | } 267 | 'System/BindRoleApis': { 268 | input: System__BindRoleApisInput 269 | data: System__BindRoleApisResponseData 270 | requiresAuthentication: false 271 | } 272 | 'User/CreateOneUser': { 273 | input: User__CreateOneUserInput 274 | data: User__CreateOneUserResponseData 275 | requiresAuthentication: true 276 | } 277 | 'User/UpdateInfo': { 278 | input: User__UpdateInfoInput 279 | data: User__UpdateInfoResponseData 280 | requiresAuthentication: true 281 | } 282 | } 283 | 284 | export type Subscriptions = { 285 | 'Chat/ChatSSE': { 286 | input: Chat__ChatSSEInput 287 | data: Chat__ChatSSEResponseData 288 | requiresAuthentication: false 289 | } 290 | } 291 | 292 | export type LiveQueries = { 293 | } 294 | 295 | export interface Operations extends OperationsDefinition {} 296 | -------------------------------------------------------------------------------- /src/services/index.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from './client' 2 | 3 | export default createClient({ 4 | baseURL: import.meta.env.VITE_APP_API_BASE_URL 5 | }) 6 | -------------------------------------------------------------------------------- /src/services/sse.ts: -------------------------------------------------------------------------------- 1 | export async function* streamAPI = any, TResponse = any>(url: string, { 2 | method = 'get', 3 | params, 4 | controller, 5 | }: { 6 | method?: 'get' | 'post' 7 | params?: TSource 8 | controller?: AbortController 9 | }) { 10 | const query = new URLSearchParams({ wg_sse: 'true' }) 11 | const config: RequestInit = { 12 | signal: controller?.signal, 13 | } 14 | if (method.toLowerCase() === 'get') { 15 | if (params) 16 | Object.keys(params).forEach(key => query.append(key, params[key])) 17 | } 18 | else { 19 | if (params) 20 | config.body = JSON.stringify(params) 21 | } 22 | 23 | const resp = await fetch(`${import.meta.env.VITE_APP_API_BASE_URL}/operations${url}?${query.toString()}`, config) 24 | if (resp.ok) { 25 | // 使用yield以流的方式读取 26 | const reader = resp.body!.getReader() 27 | 28 | while (true) { 29 | const { value, done } = await reader!.read() 30 | if (done) 31 | break 32 | const lines = new TextDecoder().decode(value).split('\n').map(line => line.substring(6)).filter(Boolean) 33 | for (const line of lines) { 34 | try { 35 | yield JSON.parse(line) as TResponse 36 | } 37 | catch (error) { 38 | // eslint-disable-next-line no-console 39 | console.log(line) 40 | } 41 | } 42 | } 43 | } 44 | else { throw new Error(resp.statusText) } 45 | } 46 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue' 2 | import { createPinia } from 'pinia' 3 | 4 | export const store = createPinia() 5 | 6 | export function setupStore(app: App) { 7 | app.use(store) 8 | } 9 | 10 | export * from './modules' 11 | -------------------------------------------------------------------------------- /src/store/modules/app/helper.ts: -------------------------------------------------------------------------------- 1 | import { ss } from '@/utils/storage' 2 | 3 | const LOCAL_NAME = 'appSetting' 4 | 5 | export type Theme = 'light' | 'dark' | 'auto' 6 | 7 | export type Language = 'zh-CN' | 'zh-TW' | 'en-US' 8 | 9 | export interface AppState { 10 | siderCollapsed: boolean 11 | theme: Theme 12 | language: Language 13 | } 14 | 15 | export function defaultSetting(): AppState { 16 | return { siderCollapsed: false, theme: 'light', language: 'zh-CN' } 17 | } 18 | 19 | export function getLocalSetting(): AppState { 20 | const localSetting: AppState | undefined = ss.get(LOCAL_NAME) 21 | return { ...defaultSetting(), ...localSetting } 22 | } 23 | 24 | export function setLocalSetting(setting: AppState): void { 25 | ss.set(LOCAL_NAME, setting) 26 | } 27 | -------------------------------------------------------------------------------- /src/store/modules/app/index.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import type { AppState, Language, Theme } from './helper' 3 | import { getLocalSetting, setLocalSetting } from './helper' 4 | import { store } from '@/store' 5 | 6 | export const useAppStore = defineStore('app-store', { 7 | state: (): AppState => getLocalSetting(), 8 | actions: { 9 | setSiderCollapsed(collapsed: boolean) { 10 | this.siderCollapsed = collapsed 11 | this.recordState() 12 | }, 13 | 14 | setTheme(theme: Theme) { 15 | this.theme = theme 16 | this.recordState() 17 | }, 18 | 19 | setLanguage(language: Language) { 20 | if (this.language !== language) { 21 | this.language = language 22 | this.recordState() 23 | } 24 | }, 25 | 26 | recordState() { 27 | setLocalSetting(this.$state) 28 | }, 29 | }, 30 | }) 31 | 32 | export function useAppStoreWithOut() { 33 | return useAppStore(store) 34 | } 35 | -------------------------------------------------------------------------------- /src/store/modules/auth/helper.ts: -------------------------------------------------------------------------------- 1 | // const LOCAL_NAME = 'SECRET_TOKEN' 2 | let _token: string | null 3 | 4 | export function getToken() { 5 | return _token 6 | } 7 | 8 | export function setToken(token: string) { 9 | return _token = token 10 | } 11 | 12 | export function removeToken() { 13 | return _token = null 14 | } 15 | -------------------------------------------------------------------------------- /src/store/modules/auth/index.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { getToken, removeToken, setToken } from './helper' 3 | import { store, useChatStore, useUserStore } from '@/store' 4 | import client from '@/services' 5 | 6 | interface SessionResponse { 7 | auth: boolean 8 | model: 'ChatGPTAPI' | 'ChatGPTUnofficialProxyAPI' 9 | } 10 | 11 | export interface AuthState { 12 | token: string | undefined | null 13 | session: SessionResponse | null 14 | } 15 | 16 | export const useAuthStore = defineStore('auth-store', { 17 | state: (): AuthState => ({ 18 | token: getToken(), 19 | session: null, 20 | }), 21 | 22 | getters: { 23 | isChatGPTAPI(state): boolean { 24 | return state.session?.model === 'ChatGPTAPI' 25 | }, 26 | }, 27 | 28 | actions: { 29 | async getSession() { 30 | const userStore = useUserStore() 31 | const chatStore = useChatStore() 32 | try { 33 | this.session = { auth: true, model: 'ChatGPTAPI' } 34 | try { 35 | const userResp = await client.query({ 36 | operationName: 'User/Me', 37 | }) 38 | if (!userResp.error) { 39 | const user = userResp.data!.data! 40 | // await fetchSession() 41 | userStore.updateUserInfo({ 42 | avatar: user.avatar, 43 | name: user.name, 44 | description: user.description, 45 | }) 46 | this.token = user.id 47 | chatStore.fetchHistories() 48 | } 49 | } 50 | catch (error) { 51 | // 52 | } 53 | 54 | return Promise.resolve(this.session) 55 | } 56 | catch (error) { 57 | return Promise.reject(error) 58 | } 59 | }, 60 | 61 | setToken(token: string) { 62 | this.token = token 63 | setToken(token) 64 | }, 65 | 66 | removeToken() { 67 | this.token = undefined 68 | removeToken() 69 | }, 70 | }, 71 | }) 72 | 73 | export function useAuthStoreWithout() { 74 | return useAuthStore(store) 75 | } 76 | -------------------------------------------------------------------------------- /src/store/modules/chat/helper.ts: -------------------------------------------------------------------------------- 1 | import { ss } from '@/utils/storage' 2 | 3 | const LOCAL_NAME = 'chatStorage' 4 | 5 | export function defaultState(): Chat.ChatState { 6 | const uuid = 1002 7 | return { 8 | active: uuid, 9 | usingContext: true, 10 | history: [{ uuid, title: 'New Chat', isEdit: false }], 11 | chat: [{ uuid, data: [] }], 12 | } 13 | } 14 | 15 | export function getLocalState(): Chat.ChatState { 16 | const localState = ss.get(LOCAL_NAME) 17 | const state = localState ?? defaultState() 18 | const matched = location.hash.match(/\/chat\/(\w*)/) 19 | if (matched) 20 | state.active = +matched[1] 21 | 22 | else 23 | state.active = null 24 | 25 | return state 26 | } 27 | 28 | export function setLocalState(state: Chat.ChatState) { 29 | ss.set(LOCAL_NAME, state) 30 | } 31 | -------------------------------------------------------------------------------- /src/store/modules/chat/index.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { getLocalState, setLocalState } from './helper' 3 | import { router } from '@/router' 4 | import client from '@/services' 5 | 6 | export const useChatStore = defineStore('chat-store', { 7 | state: (): Chat.ChatState => getLocalState(), 8 | 9 | getters: { 10 | getChatHistoryByCurrentActive(state: Chat.ChatState) { 11 | const index = state.history.findIndex(item => item.uuid === state.active) 12 | if (index !== -1) 13 | return state.history[index] 14 | return null 15 | }, 16 | 17 | getChatByUuid(state: Chat.ChatState) { 18 | return (uuid?: number) => { 19 | if (uuid) 20 | return state.chat.find(item => item.uuid === uuid)?.data ?? [] 21 | return state.chat.find(item => item.uuid === state.active)?.data ?? [] 22 | } 23 | }, 24 | }, 25 | 26 | actions: { 27 | async fetchHistories() { 28 | const { error, data } = await client.query({ 29 | operationName: 'History/GetList', 30 | }) 31 | if (!error) { 32 | this.history = data!.data!.map(item => ({ 33 | title: item.title!, 34 | isEdit: false, 35 | uuid: item.id!, 36 | })) 37 | } 38 | if (data?.data?.length && data?.data?.length > 0) { 39 | this.setActive(data!.data![0].id!) 40 | } else { 41 | this.addHistory({ title: 'New Chat', isEdit: false }) 42 | } 43 | }, 44 | setUsingContext(context: boolean) { 45 | this.usingContext = context 46 | this.recordState() 47 | }, 48 | 49 | async addHistory(history: Omit, chatData: Chat.Chat[] = []) { 50 | const { error, data } = await client.mutate({ 51 | operationName: 'History/CreateOne', 52 | input: { 53 | title: history.title, 54 | }, 55 | }) 56 | if (!error) { 57 | const item = data!.data! 58 | this.history.unshift({ 59 | ...history, 60 | uuid: item.id!, 61 | }) 62 | this.chat.unshift({ uuid: item.id!, data: chatData }) 63 | this.active = item.id! 64 | await this.reloadRoute(item.id!) 65 | } 66 | }, 67 | 68 | async updateHistory(uuid: number, edit: Partial) { 69 | const index = this.history.findIndex(item => item.uuid === uuid) 70 | if (edit.isEdit === false) { 71 | const { error } = await client.mutate({ 72 | operationName: 'History/UpdateOne', 73 | input: { 74 | id: uuid, 75 | title: this.history[index].title, 76 | }, 77 | }) 78 | if (error) 79 | return 80 | } 81 | if (index !== -1) { 82 | this.history[index] = { ...this.history[index], ...edit } 83 | this.recordState() 84 | } 85 | }, 86 | 87 | async resetEdit(uuid: number) { 88 | const index = this.history.findIndex(item => item.uuid === uuid) 89 | if (index !== -1) { 90 | this.history[index] = { ...this.history[index], isEdit: false } 91 | this.recordState() 92 | } 93 | }, 94 | 95 | async deleteHistory(index: number) { 96 | const { error } = await client.mutate({ 97 | operationName: 'History/DeleteOne', 98 | input: { 99 | id: this.history[index].uuid, 100 | }, 101 | }) 102 | if (!error) { 103 | this.history.splice(index, 1) 104 | this.chat.splice(index, 1) 105 | 106 | if (this.history.length === 0) { 107 | this.active = null 108 | this.reloadRoute() 109 | return 110 | } 111 | 112 | if (index > 0 && index <= this.history.length) { 113 | const uuid = this.history[index - 1].uuid 114 | this.active = uuid 115 | this.reloadRoute(uuid) 116 | return 117 | } 118 | 119 | if (index === 0) { 120 | if (this.history.length > 0) { 121 | const uuid = this.history[0].uuid 122 | this.active = uuid 123 | this.reloadRoute(uuid) 124 | } 125 | } 126 | 127 | if (index > this.history.length) { 128 | const uuid = this.history[this.history.length - 1].uuid 129 | this.active = uuid 130 | this.reloadRoute(uuid) 131 | } 132 | } 133 | }, 134 | 135 | async setActive(uuid: number | null) { 136 | this.active = uuid 137 | return await this.reloadRoute(uuid || undefined) 138 | }, 139 | 140 | getChatByUuidAndIndex(uuid: number, index: number) { 141 | if (!uuid || uuid === 0) { 142 | if (this.chat.length) 143 | return this.chat[0].data[index] 144 | return null 145 | } 146 | const chatIndex = this.chat.findIndex(item => item.uuid === uuid) 147 | if (chatIndex !== -1) 148 | return this.chat[chatIndex].data[index] 149 | return null 150 | }, 151 | 152 | addChatByUuid(uuid: number, chat: Chat.Chat) { 153 | if (!uuid || uuid === 0) { 154 | if (this.history.length === 0) { 155 | const uuid = Date.now() 156 | this.history.push({ uuid, title: chat.text, isEdit: false }) 157 | this.chat.push({ uuid, data: [chat] }) 158 | this.active = uuid 159 | this.recordState() 160 | } 161 | else { 162 | this.chat[0].data.push(chat) 163 | if (this.history[0].title === 'New Chat') 164 | this.history[0].title = chat.text 165 | this.recordState() 166 | } 167 | } 168 | 169 | const index = this.chat.findIndex(item => item.uuid === uuid) 170 | if (index !== -1) { 171 | this.chat[index].data.push(chat) 172 | if (this.history[index].title === 'New Chat') 173 | this.history[index].title = chat.text 174 | this.recordState() 175 | } 176 | }, 177 | 178 | updateChatByUuid(uuid: number, index: number, chat: Chat.Chat) { 179 | if (!uuid || uuid === 0) { 180 | if (this.chat.length) { 181 | this.chat[0].data[index] = chat 182 | this.recordState() 183 | } 184 | return 185 | } 186 | 187 | const chatIndex = this.chat.findIndex(item => item.uuid === uuid) 188 | if (chatIndex !== -1) { 189 | this.chat[chatIndex].data[index] = chat 190 | this.recordState() 191 | } 192 | }, 193 | 194 | updateChatSomeByUuid(uuid: number, index: number, chat: Partial) { 195 | if (!uuid || uuid === 0) { 196 | if (this.chat.length) { 197 | this.chat[0].data[index] = { ...this.chat[0].data[index], ...chat } 198 | this.recordState() 199 | } 200 | return 201 | } 202 | 203 | const chatIndex = this.chat.findIndex(item => item.uuid === uuid) 204 | if (chatIndex !== -1) { 205 | this.chat[chatIndex].data[index] = { ...this.chat[chatIndex].data[index], ...chat } 206 | this.recordState() 207 | } 208 | }, 209 | 210 | deleteChatByUuid(uuid: number, index: number) { 211 | if (!uuid || uuid === 0) { 212 | if (this.chat.length) { 213 | this.chat[0].data.splice(index, 1) 214 | this.recordState() 215 | } 216 | return 217 | } 218 | 219 | const chatIndex = this.chat.findIndex(item => item.uuid === uuid) 220 | if (chatIndex !== -1) { 221 | this.chat[chatIndex].data.splice(index, 1) 222 | this.recordState() 223 | } 224 | }, 225 | 226 | clearChatByUuid(uuid: number) { 227 | if (!uuid || uuid === 0) { 228 | if (this.chat.length) { 229 | this.chat[0].data = [] 230 | this.recordState() 231 | } 232 | return 233 | } 234 | 235 | const index = this.chat.findIndex(item => item.uuid === uuid) 236 | if (index !== -1) { 237 | this.chat[index].data = [] 238 | this.recordState() 239 | } 240 | }, 241 | 242 | async reloadRoute(uuid?: number) { 243 | this.recordState() 244 | await router.push({ name: 'Chat', params: { uuid } }) 245 | }, 246 | 247 | recordState() { 248 | setLocalState(this.$state) 249 | }, 250 | }, 251 | }) 252 | -------------------------------------------------------------------------------- /src/store/modules/index.ts: -------------------------------------------------------------------------------- 1 | export * from './app' 2 | export * from './chat' 3 | export * from './user' 4 | export * from './prompt' 5 | export * from './auth' 6 | export * from './speak' 7 | -------------------------------------------------------------------------------- /src/store/modules/prompt/helper.ts: -------------------------------------------------------------------------------- 1 | import { ss } from '@/utils/storage' 2 | 3 | const LOCAL_NAME = 'promptStore' 4 | 5 | export type PromptList = [] 6 | 7 | export interface PromptStore { 8 | promptList: PromptList 9 | } 10 | 11 | export function getLocalPromptList(): PromptStore { 12 | const promptStore: PromptStore | undefined = ss.get(LOCAL_NAME) 13 | return promptStore ?? { promptList: [] } 14 | } 15 | 16 | export function setLocalPromptList(promptStore: PromptStore): void { 17 | ss.set(LOCAL_NAME, promptStore) 18 | } 19 | -------------------------------------------------------------------------------- /src/store/modules/prompt/index.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import type { PromptStore } from './helper' 3 | import { getLocalPromptList, setLocalPromptList } from './helper' 4 | 5 | export const usePromptStore = defineStore('prompt-store', { 6 | state: (): PromptStore => getLocalPromptList(), 7 | 8 | actions: { 9 | updatePromptList(promptList: []) { 10 | this.$patch({ promptList }) 11 | setLocalPromptList({ promptList }) 12 | }, 13 | getPromptList() { 14 | return this.$state 15 | }, 16 | }, 17 | }) 18 | -------------------------------------------------------------------------------- /src/store/modules/speak/index.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import * as sdk from 'microsoft-cognitiveservices-speech-sdk' 3 | import type { SpeechConfig } from 'microsoft-cognitiveservices-speech-sdk' 4 | 5 | export const useSpeakStore = defineStore('speak-store', { 6 | state: (): { speechConfig: SpeechConfig | null } => ({ 7 | speechConfig: null, 8 | }), 9 | actions: { 10 | async getConfig() { 11 | // const { data } = await fetchSpeakConfig() 12 | // sdk.SpeechConfig.fromEndpoint() 13 | this.speechConfig = sdk.SpeechConfig.fromSubscription(import.meta.env.VITE_SPEECH_KEY, import.meta.env.VITE_SPEECH_REGION) 14 | this.speechConfig.speechSynthesisVoiceName = 'en-US-JennyNeural' 15 | this.speechConfig.speechRecognitionLanguage = 'en-US' 16 | }, 17 | }, 18 | }) 19 | -------------------------------------------------------------------------------- /src/store/modules/user/helper.ts: -------------------------------------------------------------------------------- 1 | import { ss } from '@/utils/storage' 2 | 3 | const LOCAL_NAME = 'userStorage' 4 | 5 | export interface UserInfo { 6 | avatar: string 7 | name: string 8 | description: string 9 | } 10 | 11 | export interface UserState { 12 | userInfo: UserInfo 13 | } 14 | 15 | export function defaultSetting(): UserState { 16 | return { 17 | userInfo: { 18 | avatar: 'https://raw.githubusercontent.com/Chanzhaoyu/chatgpt-web/main/src/assets/avatar.jpg', 19 | name: 'ChenZhaoYu', 20 | description: 'Star on Github', 21 | }, 22 | } 23 | } 24 | 25 | export function getLocalState(): UserState { 26 | const localSetting: UserState | undefined = ss.get(LOCAL_NAME) 27 | return { ...defaultSetting(), ...localSetting } 28 | } 29 | 30 | export function setLocalState(setting: UserState): void { 31 | ss.set(LOCAL_NAME, setting) 32 | } 33 | -------------------------------------------------------------------------------- /src/store/modules/user/index.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import type { UserInfo, UserState } from './helper' 3 | import { defaultSetting, getLocalState, setLocalState } from './helper' 4 | 5 | export const useUserStore = defineStore('user-store', { 6 | state: (): UserState => getLocalState(), 7 | actions: { 8 | updateUserInfo(userInfo: Partial) { 9 | this.userInfo = { ...this.userInfo, ...userInfo } 10 | this.recordState() 11 | }, 12 | 13 | resetUserInfo() { 14 | this.userInfo = { ...defaultSetting().userInfo } 15 | this.recordState() 16 | }, 17 | 18 | recordState() { 19 | setLocalState(this.$state) 20 | }, 21 | }, 22 | }) 23 | -------------------------------------------------------------------------------- /src/styles/global.less: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | #app { 4 | height: 100%; 5 | } 6 | 7 | body { 8 | padding-bottom: constant(safe-area-inset-bottom); 9 | padding-bottom: env(safe-area-inset-bottom); 10 | } 11 | -------------------------------------------------------------------------------- /src/styles/lib/highlight.less: -------------------------------------------------------------------------------- 1 | html.dark { 2 | pre code.hljs { 3 | display: block; 4 | overflow-x: auto; 5 | padding: 1em 6 | } 7 | 8 | code.hljs { 9 | padding: 3px 5px 10 | } 11 | 12 | .hljs { 13 | color: #abb2bf; 14 | background: #282c34 15 | } 16 | 17 | .hljs-keyword, 18 | .hljs-operator, 19 | .hljs-pattern-match { 20 | color: #f92672 21 | } 22 | 23 | .hljs-function, 24 | .hljs-pattern-match .hljs-constructor { 25 | color: #61aeee 26 | } 27 | 28 | .hljs-function .hljs-params { 29 | color: #a6e22e 30 | } 31 | 32 | .hljs-function .hljs-params .hljs-typing { 33 | color: #fd971f 34 | } 35 | 36 | .hljs-module-access .hljs-module { 37 | color: #7e57c2 38 | } 39 | 40 | .hljs-constructor { 41 | color: #e2b93d 42 | } 43 | 44 | .hljs-constructor .hljs-string { 45 | color: #9ccc65 46 | } 47 | 48 | .hljs-comment, 49 | .hljs-quote { 50 | color: #b18eb1; 51 | font-style: italic 52 | } 53 | 54 | .hljs-doctag, 55 | .hljs-formula { 56 | color: #c678dd 57 | } 58 | 59 | .hljs-deletion, 60 | .hljs-name, 61 | .hljs-section, 62 | .hljs-selector-tag, 63 | .hljs-subst { 64 | color: #e06c75 65 | } 66 | 67 | .hljs-literal { 68 | color: #56b6c2 69 | } 70 | 71 | .hljs-addition, 72 | .hljs-attribute, 73 | .hljs-meta .hljs-string, 74 | .hljs-regexp, 75 | .hljs-string { 76 | color: #98c379 77 | } 78 | 79 | .hljs-built_in, 80 | .hljs-class .hljs-title, 81 | .hljs-title.class_ { 82 | color: #e6c07b 83 | } 84 | 85 | .hljs-attr, 86 | .hljs-number, 87 | .hljs-selector-attr, 88 | .hljs-selector-class, 89 | .hljs-selector-pseudo, 90 | .hljs-template-variable, 91 | .hljs-type, 92 | .hljs-variable { 93 | color: #d19a66 94 | } 95 | 96 | .hljs-bullet, 97 | .hljs-link, 98 | .hljs-meta, 99 | .hljs-selector-id, 100 | .hljs-symbol, 101 | .hljs-title { 102 | color: #61aeee 103 | } 104 | 105 | .hljs-emphasis { 106 | font-style: italic 107 | } 108 | 109 | .hljs-strong { 110 | font-weight: 700 111 | } 112 | 113 | .hljs-link { 114 | text-decoration: underline 115 | } 116 | } 117 | 118 | html { 119 | pre code.hljs { 120 | display: block; 121 | overflow-x: auto; 122 | padding: 1em 123 | } 124 | 125 | code.hljs { 126 | padding: 3px 5px 127 | } 128 | 129 | .hljs { 130 | color: #383a42; 131 | background: #fafafa 132 | } 133 | 134 | .hljs-comment, 135 | .hljs-quote { 136 | color: #a0a1a7; 137 | font-style: italic 138 | } 139 | 140 | .hljs-doctag, 141 | .hljs-formula, 142 | .hljs-keyword { 143 | color: #a626a4 144 | } 145 | 146 | .hljs-deletion, 147 | .hljs-name, 148 | .hljs-section, 149 | .hljs-selector-tag, 150 | .hljs-subst { 151 | color: #e45649 152 | } 153 | 154 | .hljs-literal { 155 | color: #0184bb 156 | } 157 | 158 | .hljs-addition, 159 | .hljs-attribute, 160 | .hljs-meta .hljs-string, 161 | .hljs-regexp, 162 | .hljs-string { 163 | color: #50a14f 164 | } 165 | 166 | .hljs-attr, 167 | .hljs-number, 168 | .hljs-selector-attr, 169 | .hljs-selector-class, 170 | .hljs-selector-pseudo, 171 | .hljs-template-variable, 172 | .hljs-type, 173 | .hljs-variable { 174 | color: #986801 175 | } 176 | 177 | .hljs-bullet, 178 | .hljs-link, 179 | .hljs-meta, 180 | .hljs-selector-id, 181 | .hljs-symbol, 182 | .hljs-title { 183 | color: #4078f2 184 | } 185 | 186 | .hljs-built_in, 187 | .hljs-class .hljs-title, 188 | .hljs-title.class_ { 189 | color: #c18401 190 | } 191 | 192 | .hljs-emphasis { 193 | font-style: italic 194 | } 195 | 196 | .hljs-strong { 197 | font-weight: 700 198 | } 199 | 200 | .hljs-link { 201 | text-decoration: underline 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/styles/lib/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /src/typings/chat.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace Chat { 2 | 3 | interface Chat { 4 | dateTime: string 5 | text: string 6 | inversion?: boolean 7 | error?: boolean 8 | loading?: boolean 9 | conversationOptions?: ConversationRequest | null 10 | requestOptions: { prompt: string; options?: ConversationRequest | null } 11 | } 12 | 13 | interface History { 14 | title: string 15 | isEdit: boolean 16 | uuid: number 17 | } 18 | 19 | interface ChatState { 20 | active: number | null 21 | usingContext: boolean; 22 | history: History[] 23 | chat: { uuid: number; data: Chat[] }[] 24 | } 25 | 26 | interface ConversationRequest { 27 | conversationId?: string 28 | parentMessageId?: string 29 | } 30 | 31 | interface ConversationResponse { 32 | conversationId: string 33 | detail: { 34 | choices: { finish_reason: string; index: number; logprobs: any; text: string }[] 35 | created: number 36 | id: string 37 | model: string 38 | object: string 39 | usage: { completion_tokens: number; prompt_tokens: number; total_tokens: number } 40 | } 41 | id: string 42 | parentMessageId: string 43 | role: string 44 | text: string 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/typings/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface ImportMetaEnv { 4 | readonly VITE_GLOB_API_URL: string; 5 | readonly VITE_GLOB_API_TIMEOUT: string; 6 | readonly VITE_APP_API_BASE_URL: string; 7 | readonly VITE_SPEECH_KEY: string; 8 | readonly VITE_SPEECH_REGION: string; 9 | } 10 | -------------------------------------------------------------------------------- /src/typings/global.d.ts: -------------------------------------------------------------------------------- 1 | interface Window { 2 | $loadingBar?: import('naive-ui').LoadingBarProviderInst; 3 | $dialog?: import('naive-ui').DialogProviderInst; 4 | $message?: import('naive-ui').MessageProviderInst; 5 | $notification?: import('naive-ui').NotificationProviderInst; 6 | } 7 | -------------------------------------------------------------------------------- /src/utils/crypto/index.ts: -------------------------------------------------------------------------------- 1 | import CryptoJS from 'crypto-js' 2 | 3 | const CryptoSecret = '__CRYPTO_SECRET__' 4 | 5 | export function enCrypto(data: any) { 6 | const str = JSON.stringify(data) 7 | return CryptoJS.AES.encrypt(str, CryptoSecret).toString() 8 | } 9 | 10 | export function deCrypto(data: string) { 11 | const bytes = CryptoJS.AES.decrypt(data, CryptoSecret) 12 | const str = bytes.toString(CryptoJS.enc.Utf8) 13 | 14 | if (str) 15 | return JSON.parse(str) 16 | 17 | return null 18 | } 19 | -------------------------------------------------------------------------------- /src/utils/format/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 转义 HTML 字符 3 | * @param source 4 | */ 5 | export function encodeHTML(source: string) { 6 | return source 7 | .replace(/&/g, '&') 8 | .replace(//g, '>') 10 | .replace(/"/g, '"') 11 | .replace(/'/g, ''') 12 | } 13 | 14 | /** 15 | * 判断是否为代码块 16 | * @param text 17 | */ 18 | export function includeCode(text: string | null | undefined) { 19 | const regexp = /^(?:\s{4}|\t).+/gm 20 | return !!(text?.includes(' = ') || text?.match(regexp)) 21 | } 22 | 23 | /** 24 | * 复制文本 25 | * @param options 26 | */ 27 | export function copyText(options: { text: string; origin?: boolean }) { 28 | const props = { origin: true, ...options } 29 | 30 | let input: HTMLInputElement | HTMLTextAreaElement 31 | 32 | if (props.origin) 33 | input = document.createElement('textarea') 34 | else 35 | input = document.createElement('input') 36 | 37 | input.setAttribute('readonly', 'readonly') 38 | input.value = props.text 39 | document.body.appendChild(input) 40 | input.select() 41 | if (document.execCommand('copy')) 42 | document.execCommand('copy') 43 | document.body.removeChild(input) 44 | } 45 | -------------------------------------------------------------------------------- /src/utils/functions/index.ts: -------------------------------------------------------------------------------- 1 | export function getCurrentDate() { 2 | const date = new Date() 3 | const day = date.getDate() 4 | const month = date.getMonth() + 1 5 | const year = date.getFullYear() 6 | return `${year}-${month}-${day}` 7 | } 8 | -------------------------------------------------------------------------------- /src/utils/is/index.ts: -------------------------------------------------------------------------------- 1 | export function isNumber(value: T | unknown): value is number { 2 | return Object.prototype.toString.call(value) === '[object Number]' 3 | } 4 | 5 | export function isString(value: T | unknown): value is string { 6 | return Object.prototype.toString.call(value) === '[object String]' 7 | } 8 | 9 | export function isBoolean(value: T | unknown): value is boolean { 10 | return Object.prototype.toString.call(value) === '[object Boolean]' 11 | } 12 | 13 | export function isNull(value: T | unknown): value is null { 14 | return Object.prototype.toString.call(value) === '[object Null]' 15 | } 16 | 17 | export function isUndefined(value: T | unknown): value is undefined { 18 | return Object.prototype.toString.call(value) === '[object Undefined]' 19 | } 20 | 21 | export function isObject(value: T | unknown): value is object { 22 | return Object.prototype.toString.call(value) === '[object Object]' 23 | } 24 | 25 | export function isArray(value: T | unknown): value is T { 26 | return Object.prototype.toString.call(value) === '[object Array]' 27 | } 28 | 29 | export function isFunction any | void | never>(value: T | unknown): value is T { 30 | return Object.prototype.toString.call(value) === '[object Function]' 31 | } 32 | 33 | export function isDate(value: T | unknown): value is T { 34 | return Object.prototype.toString.call(value) === '[object Date]' 35 | } 36 | 37 | export function isRegExp(value: T | unknown): value is T { 38 | return Object.prototype.toString.call(value) === '[object RegExp]' 39 | } 40 | 41 | export function isPromise>(value: T | unknown): value is T { 42 | return Object.prototype.toString.call(value) === '[object Promise]' 43 | } 44 | 45 | export function isSet>(value: T | unknown): value is T { 46 | return Object.prototype.toString.call(value) === '[object Set]' 47 | } 48 | 49 | export function isMap>(value: T | unknown): value is T { 50 | return Object.prototype.toString.call(value) === '[object Map]' 51 | } 52 | 53 | export function isFile(value: T | unknown): value is T { 54 | return Object.prototype.toString.call(value) === '[object File]' 55 | } 56 | -------------------------------------------------------------------------------- /src/utils/request/axios.ts: -------------------------------------------------------------------------------- 1 | import axios, { type AxiosResponse } from 'axios' 2 | import { useAuthStore } from '@/store' 3 | 4 | const service = axios.create({ 5 | baseURL: import.meta.env.VITE_GLOB_API_URL, 6 | }) 7 | 8 | service.interceptors.request.use( 9 | (config) => { 10 | const token = useAuthStore().token 11 | if (token) 12 | config.headers.Authorization = `Bearer ${token}` 13 | return config 14 | }, 15 | (error) => { 16 | return Promise.reject(error.response) 17 | }, 18 | ) 19 | 20 | service.interceptors.response.use( 21 | (response: AxiosResponse): AxiosResponse => { 22 | if (response.status === 200) 23 | return response 24 | 25 | throw new Error(response.status.toString()) 26 | }, 27 | (error) => { 28 | return Promise.reject(error) 29 | }, 30 | ) 31 | 32 | export default service 33 | -------------------------------------------------------------------------------- /src/utils/request/index.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosProgressEvent, AxiosResponse, GenericAbortSignal } from 'axios' 2 | import request from './axios' 3 | import { useAuthStore } from '@/store' 4 | 5 | export interface HttpOption { 6 | url: string 7 | data?: any 8 | method?: string 9 | headers?: any 10 | onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void 11 | signal?: GenericAbortSignal 12 | beforeRequest?: () => void 13 | afterRequest?: () => void 14 | } 15 | 16 | export interface Response { 17 | data: T 18 | message: string | null 19 | status: string 20 | } 21 | 22 | function http( 23 | { url, data, method, headers, onDownloadProgress, signal, beforeRequest, afterRequest }: HttpOption, 24 | ) { 25 | const successHandler = (res: AxiosResponse>) => { 26 | const authStore = useAuthStore() 27 | 28 | if (res.data.status === 'Success' || typeof res.data === 'string') 29 | return res.data 30 | 31 | if (res.data.status === 'Unauthorized') { 32 | authStore.removeToken() 33 | window.location.reload() 34 | } 35 | 36 | return Promise.reject(res.data) 37 | } 38 | 39 | const failHandler = (error: Response) => { 40 | afterRequest?.() 41 | throw new Error(error?.message || 'Error') 42 | } 43 | 44 | beforeRequest?.() 45 | 46 | method = method || 'GET' 47 | 48 | const params = Object.assign(typeof data === 'function' ? data() : data ?? {}, {}) 49 | 50 | return method === 'GET' 51 | ? request.get(url, { params, signal, onDownloadProgress }).then(successHandler, failHandler) 52 | : request.post(url, params, { headers, signal, onDownloadProgress }).then(successHandler, failHandler) 53 | } 54 | 55 | export function get( 56 | { url, data, method = 'GET', onDownloadProgress, signal, beforeRequest, afterRequest }: HttpOption, 57 | ): Promise> { 58 | return http({ 59 | url, 60 | method, 61 | data, 62 | onDownloadProgress, 63 | signal, 64 | beforeRequest, 65 | afterRequest, 66 | }) 67 | } 68 | 69 | export function post( 70 | { url, data, method = 'POST', headers, onDownloadProgress, signal, beforeRequest, afterRequest }: HttpOption, 71 | ): Promise> { 72 | return http({ 73 | url, 74 | method, 75 | data, 76 | headers, 77 | onDownloadProgress, 78 | signal, 79 | beforeRequest, 80 | afterRequest, 81 | }) 82 | } 83 | 84 | export default post 85 | -------------------------------------------------------------------------------- /src/utils/speak.ts: -------------------------------------------------------------------------------- 1 | import type * as sdk from 'microsoft-cognitiveservices-speech-sdk' 2 | 3 | export function startSpeak(synthesizer: sdk.SpeechSynthesizer, str?: string) { 4 | const list: string[] = [] 5 | let speaking = false 6 | 7 | function check() { 8 | if (list.length > 0 && !speaking) { 9 | speaking = true 10 | console.log('speak', list.join('')) 11 | synthesizer.speakTextAsync(list.join(''), () => { 12 | speaking = false 13 | check() 14 | }) 15 | list.length = 0 16 | } 17 | } 18 | 19 | if (str) { 20 | list.push(str) 21 | } 22 | check() 23 | 24 | return function speak(str: string) { 25 | list.push(str) 26 | check() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/utils/storage/index.ts: -------------------------------------------------------------------------------- 1 | export * from './local' 2 | -------------------------------------------------------------------------------- /src/utils/storage/local.ts: -------------------------------------------------------------------------------- 1 | import { deCrypto, enCrypto } from '../crypto' 2 | 3 | interface StorageData { 4 | data: T 5 | expire: number | null 6 | } 7 | 8 | export function createLocalStorage(options?: { expire?: number | null; crypto?: boolean }) { 9 | const DEFAULT_CACHE_TIME = 60 * 60 * 24 * 7 10 | 11 | const { expire, crypto } = Object.assign( 12 | { 13 | expire: DEFAULT_CACHE_TIME, 14 | crypto: true, 15 | }, 16 | options, 17 | ) 18 | 19 | function set(key: string, data: T) { 20 | const storageData: StorageData = { 21 | data, 22 | expire: expire !== null ? new Date().getTime() + expire * 1000 : null, 23 | } 24 | 25 | const json = crypto ? enCrypto(storageData) : JSON.stringify(storageData) 26 | window.localStorage.setItem(key, json) 27 | } 28 | 29 | function get(key: string) { 30 | const json = window.localStorage.getItem(key) 31 | if (json) { 32 | let storageData: StorageData | null = null 33 | 34 | try { 35 | storageData = crypto ? deCrypto(json) : JSON.parse(json) 36 | } 37 | catch { 38 | // Prevent failure 39 | } 40 | 41 | if (storageData) { 42 | const { data, expire } = storageData 43 | if (expire === null || expire >= Date.now()) 44 | return data 45 | } 46 | 47 | remove(key) 48 | return null 49 | } 50 | } 51 | 52 | function remove(key: string) { 53 | window.localStorage.removeItem(key) 54 | } 55 | 56 | function clear() { 57 | window.localStorage.clear() 58 | } 59 | 60 | return { 61 | set, 62 | get, 63 | remove, 64 | clear, 65 | } 66 | } 67 | 68 | export const ls = createLocalStorage() 69 | 70 | export const ss = createLocalStorage({ expire: null, crypto: false }) 71 | -------------------------------------------------------------------------------- /src/views/chat/components/Header/index.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 79 | -------------------------------------------------------------------------------- /src/views/chat/components/Message/Avatar.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 29 | -------------------------------------------------------------------------------- /src/views/chat/components/Message/Text.vue: -------------------------------------------------------------------------------- 1 | 61 | 62 | 75 | 76 | 79 | -------------------------------------------------------------------------------- /src/views/chat/components/Message/index.vue: -------------------------------------------------------------------------------- 1 | 56 | 57 | 105 | -------------------------------------------------------------------------------- /src/views/chat/components/Message/style.less: -------------------------------------------------------------------------------- 1 | .markdown-body { 2 | background-color: transparent; 3 | font-size: 14px; 4 | 5 | p { 6 | white-space: pre-wrap; 7 | } 8 | 9 | ol { 10 | list-style-type: decimal; 11 | } 12 | 13 | ul { 14 | list-style-type: disc; 15 | } 16 | 17 | pre code, 18 | pre tt { 19 | line-height: 1.65; 20 | } 21 | 22 | .highlight pre, 23 | pre { 24 | background-color: #fff; 25 | } 26 | 27 | code.hljs { 28 | padding: 0; 29 | } 30 | 31 | .code-block { 32 | &-wrapper { 33 | position: relative; 34 | padding-top: 24px; 35 | } 36 | 37 | &-header { 38 | position: absolute; 39 | top: 5px; 40 | right: 0; 41 | width: 100%; 42 | padding: 0 1rem; 43 | display: flex; 44 | justify-content: flex-end; 45 | align-items: center; 46 | color: #b3b3b3; 47 | 48 | &__copy{ 49 | cursor: pointer; 50 | margin-left: 0.5rem; 51 | user-select: none; 52 | &:hover { 53 | color: #65a665; 54 | } 55 | } 56 | } 57 | } 58 | } 59 | 60 | html.dark { 61 | 62 | .highlight pre, 63 | pre { 64 | background-color: #282c34; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/views/chat/components/VoiceInput/index.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 25 | -------------------------------------------------------------------------------- /src/views/chat/components/index.ts: -------------------------------------------------------------------------------- 1 | import Message from './Message/index.vue' 2 | 3 | export { Message } 4 | -------------------------------------------------------------------------------- /src/views/chat/hooks/useChat.ts: -------------------------------------------------------------------------------- 1 | import { useChatStore } from '@/store' 2 | 3 | export function useChat() { 4 | const chatStore = useChatStore() 5 | 6 | const getChatByUuidAndIndex = (uuid: number, index: number) => { 7 | return chatStore.getChatByUuidAndIndex(uuid, index) 8 | } 9 | 10 | const addChat = (uuid: number, chat: Chat.Chat) => { 11 | chatStore.addChatByUuid(uuid, chat) 12 | } 13 | 14 | const updateChat = (uuid: number, index: number, chat: Chat.Chat) => { 15 | chatStore.updateChatByUuid(uuid, index, chat) 16 | } 17 | 18 | const updateChatSome = (uuid: number, index: number, chat: Partial) => { 19 | chatStore.updateChatSomeByUuid(uuid, index, chat) 20 | } 21 | 22 | return { 23 | addChat, 24 | updateChat, 25 | updateChatSome, 26 | getChatByUuidAndIndex, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/views/chat/hooks/useCopyCode.ts: -------------------------------------------------------------------------------- 1 | import { onMounted, onUpdated } from 'vue' 2 | import { copyText } from '@/utils/format' 3 | 4 | export function useCopyCode() { 5 | function copyCodeBlock() { 6 | const codeBlockWrapper = document.querySelectorAll('.code-block-wrapper') 7 | codeBlockWrapper.forEach((wrapper) => { 8 | const copyBtn = wrapper.querySelector('.code-block-header__copy') 9 | const codeBlock = wrapper.querySelector('.code-block-body') 10 | if (copyBtn && codeBlock) { 11 | copyBtn.addEventListener('click', () => { 12 | if (navigator.clipboard?.writeText) 13 | navigator.clipboard.writeText(codeBlock.textContent ?? '') 14 | else 15 | copyText({ text: codeBlock.textContent ?? '', origin: true }) 16 | }) 17 | } 18 | }) 19 | } 20 | 21 | onMounted(() => copyCodeBlock()) 22 | 23 | onUpdated(() => copyCodeBlock()) 24 | } 25 | -------------------------------------------------------------------------------- /src/views/chat/hooks/useScroll.ts: -------------------------------------------------------------------------------- 1 | import type { Ref } from 'vue' 2 | import { nextTick, ref } from 'vue' 3 | 4 | type ScrollElement = HTMLDivElement | null 5 | 6 | interface ScrollReturn { 7 | scrollRef: Ref 8 | scrollToBottom: () => Promise 9 | scrollToTop: () => Promise 10 | scrollToBottomIfAtBottom: () => Promise 11 | } 12 | 13 | export function useScroll(): ScrollReturn { 14 | const scrollRef = ref(null) 15 | 16 | const scrollToBottom = async () => { 17 | await nextTick() 18 | if (scrollRef.value) 19 | scrollRef.value.scrollTop = scrollRef.value.scrollHeight 20 | } 21 | 22 | const scrollToTop = async () => { 23 | await nextTick() 24 | if (scrollRef.value) 25 | scrollRef.value.scrollTop = 0 26 | } 27 | 28 | const scrollToBottomIfAtBottom = async () => { 29 | await nextTick() 30 | if (scrollRef.value) { 31 | const threshold = 50 // 阈值,表示滚动条到底部的距离阈值 32 | const distanceToBottom = scrollRef.value.scrollHeight - scrollRef.value.scrollTop - scrollRef.value.clientHeight 33 | if (distanceToBottom <= threshold) 34 | scrollRef.value.scrollTop = scrollRef.value.scrollHeight 35 | } 36 | } 37 | 38 | return { 39 | scrollRef, 40 | scrollToBottom, 41 | scrollToTop, 42 | scrollToBottomIfAtBottom, 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/views/chat/hooks/useUsingContext.ts: -------------------------------------------------------------------------------- 1 | import { computed } from 'vue' 2 | import { useMessage } from 'naive-ui' 3 | import { t } from '@/locales' 4 | import { useChatStore } from '@/store' 5 | 6 | export function useUsingContext() { 7 | const ms = useMessage() 8 | const chatStore = useChatStore() 9 | const usingContext = computed(() => chatStore.usingContext) 10 | 11 | function toggleUsingContext() { 12 | chatStore.setUsingContext(!usingContext.value) 13 | if (usingContext.value) 14 | ms.success(t('chat.turnOnContext')) 15 | else 16 | ms.warning(t('chat.turnOffContext')) 17 | } 18 | 19 | return { 20 | usingContext, 21 | toggleUsingContext, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/views/chat/layout/Layout.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 51 | -------------------------------------------------------------------------------- /src/views/chat/layout/Permission.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 67 | -------------------------------------------------------------------------------- /src/views/chat/layout/index.ts: -------------------------------------------------------------------------------- 1 | import ChatLayout from './Layout.vue' 2 | 3 | export { ChatLayout } 4 | -------------------------------------------------------------------------------- /src/views/chat/layout/sider/Footer.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 25 | -------------------------------------------------------------------------------- /src/views/chat/layout/sider/List.vue: -------------------------------------------------------------------------------- 1 | 61 | 62 | 116 | -------------------------------------------------------------------------------- /src/views/chat/layout/sider/index.vue: -------------------------------------------------------------------------------- 1 | 60 | 61 | 97 | -------------------------------------------------------------------------------- /src/views/exception/404/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 32 | -------------------------------------------------------------------------------- /src/views/exception/500/index.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 33 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | 2 | cd ./service 3 | nohup pnpm start > service.log & 4 | echo "Start service complete!" 5 | 6 | 7 | cd .. 8 | echo "" > front.log 9 | nohup pnpm dev > front.log & 10 | echo "Start front complete!" 11 | tail -f front.log 12 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | darkMode: 'class', 4 | content: [ 5 | './index.html', 6 | './src/**/*.{vue,js,ts,jsx,tsx}', 7 | ], 8 | theme: { 9 | extend: { 10 | animation: { 11 | blink: 'blink 1.2s infinite steps(1, start)', 12 | }, 13 | keyframes: { 14 | blink: { 15 | '0%, 100%': { 'background-color': 'currentColor' }, 16 | '50%': { 'background-color': 'transparent' }, 17 | }, 18 | }, 19 | }, 20 | }, 21 | plugins: [], 22 | } 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "module": "ESNext", 5 | "target": "ESNext", 6 | "lib": ["DOM", "ESNext"], 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "allowSyntheticDefaultImports": true, 10 | "jsx": "preserve", 11 | "moduleResolution": "node", 12 | "resolveJsonModule": true, 13 | "noUnusedLocals": true, 14 | "strictNullChecks": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "skipLibCheck": true, 17 | "paths": { 18 | "@/*": ["./src/*"] 19 | }, 20 | "types": ["vite/client", "node", "naive-ui/volar"] 21 | }, 22 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], 23 | "exclude": ["node_modules", "dist", "server"] 24 | } 25 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { defineConfig } from 'vite' 3 | import vue from '@vitejs/plugin-vue' 4 | // import { VitePWA } from 'vite-plugin-pwa' 5 | 6 | export default defineConfig((env) => { 7 | // const viteEnv = loadEnv(env.mode, process.cwd()) as unknown as ImportMetaEnv 8 | 9 | return { 10 | resolve: { 11 | alias: { 12 | '@': path.resolve(process.cwd(), 'src'), 13 | }, 14 | }, 15 | plugins: [ 16 | vue(), 17 | // VitePWA({ 18 | // injectRegister: 'auto', 19 | // manifest: { 20 | // name: 'chatGPT', 21 | // short_name: 'chatGPT', 22 | // icons: [ 23 | // { src: 'pwa-192x192.png', sizes: '192x192', type: 'image/png' }, 24 | // { src: 'pwa-512x512.png', sizes: '512x512', type: 'image/png' }, 25 | // ], 26 | // }, 27 | // }), 28 | ], 29 | server: { 30 | host: '0.0.0.0', 31 | port: 1002, 32 | open: false, 33 | // proxy: { 34 | // '/operations': { 35 | // target: viteEnv.VITE_APP_API_BASE_URL, 36 | // changeOrigin: true, // 允许跨域 37 | // // rewrite: path => path.replace('/operations/', '/'), 38 | // }, 39 | // }, 40 | }, 41 | build: { 42 | reportCompressedSize: false, 43 | sourcemap: false, 44 | commonjsOptions: { 45 | ignoreTryCatch: false, 46 | }, 47 | }, 48 | } 49 | }) 50 | --------------------------------------------------------------------------------