├── .github ├── ISSUE_TEMPLATE │ ├── bug-report---bug-报告.md │ └── feature-request---新功能建议.md └── workflows │ ├── codeql.yml │ └── docker-image.yml ├── .gitignore ├── .gitmodules ├── Caddyfile ├── Dockerfile ├── LICENSE ├── README.en.md ├── README.md ├── backend ├── .gitignore ├── alembic.ini ├── alembic │ ├── README │ ├── env.py │ ├── script.py.mako │ └── versions │ │ ├── 0d790c2c26dc_add_uploaded_files_table.py │ │ ├── 333722b0921e_add_source_id_for_baseconversation.py │ │ ├── 7d94b5503088_add_extra_info_in_uploadedfileinfo.py │ │ └── aa3d85891014_baseline.py ├── api │ ├── __init__.py │ ├── conf │ │ ├── __init__.py │ │ ├── base_config.py │ │ ├── config.py │ │ └── credentials.py │ ├── database │ │ ├── __init__.py │ │ ├── custom_types │ │ │ ├── __init__.py │ │ │ ├── guid.py │ │ │ ├── pydantic_type.py │ │ │ └── utc_datetime.py │ │ ├── mongodb.py │ │ └── sqlalchemy.py │ ├── enums │ │ ├── __init__.py │ │ ├── models.py │ │ ├── options.py │ │ └── status.py │ ├── exceptions.py │ ├── file_provider.py │ ├── globals.py │ ├── middlewares │ │ ├── __init__.py │ │ ├── asgi_logger │ │ │ ├── __init__.py │ │ │ ├── middleware.py │ │ │ └── utils.py │ │ └── request_statistics.py │ ├── models │ │ ├── __init__.py │ │ ├── db.py │ │ ├── doc │ │ │ ├── __init__.py │ │ │ └── openai_web_code_interpreter.py │ │ ├── json.py │ │ └── types.py │ ├── response.py │ ├── routers │ │ ├── __init__.py │ │ ├── arkose.py │ │ ├── chat.py │ │ ├── conv.py │ │ ├── files.py │ │ ├── logs.py │ │ ├── status.py │ │ ├── system.py │ │ └── users.py │ ├── schemas │ │ ├── __init__.py │ │ ├── conversation_schemas.py │ │ ├── file_schemas.py │ │ ├── openai_schemas.py │ │ ├── status_schemas.py │ │ ├── system_schemas.py │ │ └── user_schemas.py │ ├── sources │ │ ├── __init__.py │ │ ├── openai_api.py │ │ └── openai_web.py │ └── users.py ├── config_templates │ ├── config.yaml │ └── credentials.yaml ├── logging_config.yaml ├── main.py ├── manage.py ├── poetry.lock ├── pyproject.toml ├── requirements.txt └── utils │ ├── __init__.py │ ├── admin │ ├── __init__.py │ └── sync_conv.py │ ├── common.py │ └── logger.py ├── docs ├── donate.png ├── screenshot.en.jpeg ├── screenshot.jpeg └── screenshot_admin.jpeg ├── frontend ├── .env ├── .eslintignore ├── .eslintrc.cjs ├── .gitignore ├── .prettierrc.cjs ├── .vscode │ ├── extensions.json │ ├── launch.json │ └── settings.json ├── components.d.ts ├── config │ ├── utils │ │ └── icon-component-resolver.ts │ ├── vite.config.base.ts │ ├── vite.config.dev.ts │ └── vite.config.prod.ts ├── index.html ├── package.json ├── pnpm-lock.yaml ├── public │ ├── chatgpt-icon-black.svg │ ├── chatgpt-icon.svg │ ├── icon.png │ └── icon.svg ├── scripts │ ├── dereference_openapi.js │ └── updateapi.sh ├── src │ ├── App.vue │ ├── api │ │ ├── arkose.ts │ │ ├── chat.ts │ │ ├── conv.ts │ │ ├── files.ts │ │ ├── interceptor.ts │ │ ├── logs.ts │ │ ├── status.ts │ │ ├── system.ts │ │ ├── url.ts │ │ └── user.ts │ ├── components │ │ ├── BrowsingIcon.vue │ │ ├── ChatGPTAvatar.vue │ │ ├── ChatGPTIcon.vue │ │ ├── ChatModelTagsRow.vue │ │ ├── ChatTypeTagInfoCell.vue │ │ ├── HelpTooltip.vue │ │ ├── OpenaiWebPluginDetailCard.vue │ │ ├── PageHeader.vue │ │ ├── PreferenceForm.vue │ │ ├── UserProfileCard.vue │ │ └── icons │ │ │ ├── CWSIcon.vue │ │ │ └── browsing │ │ │ ├── click.svg │ │ │ ├── click_result.svg │ │ │ ├── external_link.svg │ │ │ ├── failed.svg │ │ │ ├── finished.svg │ │ │ ├── go_back.svg │ │ │ ├── scroll.svg │ │ │ └── search.svg │ ├── hooks │ │ └── drawer.ts │ ├── i18n.ts │ ├── locales │ │ ├── en-US.json │ │ ├── ms-MY.json │ │ └── zh-CN.json │ ├── main.ts │ ├── router │ │ ├── guard │ │ │ ├── index.ts │ │ │ ├── permission.ts │ │ │ └── userLoginInfo.ts │ │ ├── index.ts │ │ └── typings.d.ts │ ├── store │ │ ├── index.ts │ │ ├── modules │ │ │ ├── app.ts │ │ │ ├── conversation.ts │ │ │ ├── file.ts │ │ │ └── user.ts │ │ └── types.ts │ ├── style.css │ ├── types │ │ ├── custom.ts │ │ ├── echarts.ts │ │ ├── json │ │ │ ├── config_schema.json │ │ │ ├── credentials_schema.json │ │ │ ├── model_definitions.json │ │ │ ├── openapi.json │ │ │ └── schemas.json │ │ ├── json_schema.ts │ │ ├── openapi.ts │ │ └── schema.ts │ ├── utils │ │ ├── arkose.ts │ │ ├── auth.ts │ │ ├── chat.ts │ │ ├── cookies.ts │ │ ├── highlight.ts │ │ ├── json_schema.ts │ │ ├── loading.ts │ │ ├── markdown-it-katex.ts │ │ ├── markdown.ts │ │ ├── media.ts │ │ ├── renders.ts │ │ ├── table.ts │ │ ├── time.ts │ │ ├── tips.ts │ │ ├── user.ts │ │ └── validate.ts │ ├── views │ │ ├── admin │ │ │ ├── components │ │ │ │ ├── CompletionLogContent.vue │ │ │ │ ├── CreateUserForm.vue │ │ │ │ ├── OpenaiWebPluginSetting.vue │ │ │ │ ├── ServerLogContent.vue │ │ │ │ ├── StatisticsCard.vue │ │ │ │ ├── SystemInfoCard.vue │ │ │ │ ├── UpdateChatSourceSettingForm.vue │ │ │ │ ├── UpdateUserBasicForm.vue │ │ │ │ ├── UpdateUserSettingForm.vue │ │ │ │ ├── UserSelector.vue │ │ │ │ ├── charts │ │ │ │ │ ├── AskChart.vue │ │ │ │ │ ├── RequestsChart.vue │ │ │ │ │ ├── UserUsageChart.vue │ │ │ │ │ └── helpers.ts │ │ │ │ └── inputs │ │ │ │ │ ├── CountNumberInput.vue │ │ │ │ │ ├── CountNumberInputWithAdd.vue │ │ │ │ │ ├── ModelDictField.vue │ │ │ │ │ ├── RateLimitsArrayInput.vue │ │ │ │ │ ├── TimeSlotsArrayInput.vue │ │ │ │ │ └── ValidDateTimeInput.vue │ │ │ ├── index.vue │ │ │ └── pages │ │ │ │ ├── config_manager.vue │ │ │ │ ├── conversation_manager.vue │ │ │ │ ├── log_viewer.vue │ │ │ │ ├── openai_settings.vue │ │ │ │ ├── system_manager.vue │ │ │ │ └── user_manager.vue │ │ ├── conversation │ │ │ ├── components │ │ │ │ ├── FileUploadRegion.vue │ │ │ │ ├── HistoryContent.vue │ │ │ │ ├── InputRegion.vue │ │ │ │ ├── LeftBar.vue │ │ │ │ ├── LeftBarConversationMenuLabel.vue │ │ │ │ ├── MessageRow.vue │ │ │ │ ├── MessageRowAttachmentDisplay.vue │ │ │ │ ├── MessageRowBrowserDisplay.vue │ │ │ │ ├── MessageRowCodeDisplay.vue │ │ │ │ ├── MessageRowDallePromptDisplay.vue │ │ │ │ ├── MessageRowMultimodalTextDalleDisplay.vue │ │ │ │ ├── MessageRowMultimodalTextDisplay.vue │ │ │ │ ├── MessageRowMyFilesBrowserDisplay.vue │ │ │ │ ├── MessageRowPluginAction.vue │ │ │ │ ├── MessageRowPluginDisplay.vue │ │ │ │ ├── MessageRowTextDisplay.vue │ │ │ │ ├── NewConversationForm.vue │ │ │ │ ├── NewConversationFormModelSelectionLabel.vue │ │ │ │ ├── NewConversationFormPluginSelectionLabel.vue │ │ │ │ └── StatusCard.vue │ │ │ ├── history-viewer.vue │ │ │ ├── index.vue │ │ │ └── utils │ │ │ │ ├── codeblock.ts │ │ │ │ ├── export.ts │ │ │ │ ├── files.ts │ │ │ │ └── message.ts │ │ ├── error │ │ │ ├── 403.vue │ │ │ └── 404.vue │ │ ├── home.vue │ │ ├── login │ │ │ └── index.vue │ │ └── redirect │ │ │ └── index.vue │ ├── vite-env.d.ts │ └── vue3-json-viewer.d.ts ├── tsconfig.json └── tsconfig.node.json └── startup.sh /.github/ISSUE_TEMPLATE/bug-report---bug-报告.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report / bug 报告 3 | about: 注意:配置问题请到 Discussions 区提问。请仅在确信这是一个来自代码的 bug 时使用此模板。 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Version** 11 | v0.x.x 12 | 13 | **What's your deploying method?** 14 | - [ ] Docker 15 | - [ ] Caddy 16 | - [ ] Other 17 | 18 | **Describe the bug** 19 | A clear and concise description of what the bug is. 20 | 21 | **To Reproduce** 22 | Steps to reproduce the behavior: 23 | 1. Go to '...' 24 | 2. Click on '....' 25 | 3. Scroll down to '....' 26 | 4. See error 27 | 28 | **Expected behavior** 29 | A clear and concise description of what you expected to happen. 30 | 31 | **Your config.yaml or other configurations** 32 | Provide your configurations. You may hide your secrets or access tokens. 33 | 34 | **Screenshots or running logs** 35 | If applicable, add screenshots or logs to help explain your problem. 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request---新功能建议.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request / 新功能建议 3 | about: 提建议前请先确认是否已经存在类似的 issue 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "main" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "main" ] 20 | 21 | jobs: 22 | analyze: 23 | name: Analyze 24 | runs-on: ubuntu-latest 25 | permissions: 26 | actions: read 27 | contents: read 28 | security-events: write 29 | 30 | strategy: 31 | fail-fast: false 32 | matrix: 33 | language: [ 'python' ] 34 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 35 | # Use only 'java' to analyze code written in Java, Kotlin or both 36 | # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v3 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v2 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v2 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 63 | 64 | # If the Autobuild fails above, remove it and uncomment the following three lines. 65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 66 | 67 | # - run: | 68 | # echo "Run, Build Application using script" 69 | # ./location_of_script_within_repo/buildscript.sh 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v2 73 | with: 74 | category: "/language:${{matrix.language}}" 75 | -------------------------------------------------------------------------------- /.github/workflows/docker-image.yml: -------------------------------------------------------------------------------- 1 | name: Build and publish Docker image 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: "Version tag for the Docker image (semver)" 8 | required: true 9 | branch: 10 | description: "Branch to build from" 11 | required: false 12 | default: 'dev' 13 | 14 | env: 15 | IMAGE_NAME: ${{ github.repository }} 16 | VERSION: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.version || github.ref_name }} 17 | 18 | jobs: 19 | build-and-push-x64-image: 20 | runs-on: ubuntu-latest 21 | permissions: 22 | contents: read 23 | packages: write 24 | 25 | steps: 26 | - name: Checkout repository 27 | uses: actions/checkout@v3 28 | with: 29 | ref: ${{ github.event.inputs.branch }} 30 | 31 | # Documentation: https://github.com/docker/setup-qemu-action 32 | - name: Set up QEMU 33 | uses: docker/setup-qemu-action@v2 34 | 35 | - name: Docker meta 36 | id: meta 37 | uses: docker/metadata-action@v4 38 | with: 39 | images: | 40 | ghcr.io/${{ env.IMAGE_NAME }} 41 | ${{ env.IMAGE_NAME }} 42 | tags: | 43 | type=semver,pattern={{version}}${{ github.event_name == 'workflow_dispatch' && format(',value={0}', github.event.inputs.version) || '' }} 44 | type=semver,pattern={{major}}.{{minor}}${{ github.event_name == 'workflow_dispatch' && format(',value={0}', github.event.inputs.version) || '' }} 45 | 46 | # - name: Create Sentry release 47 | # uses: getsentry/action-release@v1 48 | # env: 49 | # SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} 50 | # SENTRY_ORG: ${{ secrets.SENTRY_ORG }} 51 | # SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} 52 | # # SENTRY_URL: https://sentry.io/ 53 | # with: 54 | # environment: production 55 | # sourcemaps: frontend/dist/assets 56 | # version: ${{ github.ref }} 57 | 58 | - name: Set up Docker Buildx 59 | uses: docker/setup-buildx-action@v2 60 | 61 | - name: Log in to the Container registry 62 | uses: docker/login-action@v2 63 | with: 64 | registry: ghcr.io 65 | username: ${{ github.actor }} 66 | password: ${{ secrets.GITHUB_TOKEN }} 67 | 68 | - name: Log in to Dockerhub 69 | uses: docker/login-action@v2 70 | with: 71 | username: ${{ github.actor }} 72 | password: ${{ secrets.DOCKERHUB_TOKEN }} 73 | 74 | - name: Build and push Docker image 75 | uses: docker/build-push-action@v4 76 | with: 77 | context: . 78 | platforms: linux/amd64,linux/arm64 79 | push: true 80 | tags: ${{ steps.meta.outputs.tags }} 81 | labels: ${{ steps.meta.outputs.labels }} 82 | cache-from: type=gha 83 | cache-to: type=gha,mode=max -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | docker/ 2 | .DS_Store -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxduke/chatgpt-web-share/2d52c390be890c0c94203ef95fb72ba3e83add0b/.gitmodules -------------------------------------------------------------------------------- /Caddyfile: -------------------------------------------------------------------------------- 1 | :80 { 2 | encode gzip 3 | 4 | @cache { 5 | path *.ico *.css *.js *.gif *.webp *.avif *.jpg *.jpeg *.png *.svg *.woff *.woff2 *.html *.ttf *.eot 6 | } 7 | header @cache Cache-Control "public, max-age=604800, must-revalidate" 8 | 9 | handle_path /api/* { 10 | reverse_proxy localhost:8000 11 | } 12 | handle /* { 13 | file_server 14 | root * /app/dist 15 | try_files {path} /index.html 16 | } 17 | } 18 | 19 | # example for subdirectory deploy: 20 | 21 | # :7777 { 22 | # handle_path /chat/api/* { 23 | # uri strip_prefix /chat 24 | # reverse_proxy :8000 25 | # } 26 | # handle_path /chat/* { 27 | # file_server 28 | # root * ./frontend/dist 29 | # try_files {path} /index.html 30 | # } 31 | # } 32 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine AS FrontendBuilder 2 | 3 | WORKDIR /app 4 | RUN npm install pnpm -g 5 | COPY frontend/package*.json ./frontend/ 6 | 7 | WORKDIR /app/frontend 8 | RUN pnpm install 9 | COPY frontend ./ 10 | RUN pnpm build 11 | 12 | FROM python:3.10-alpine 13 | 14 | RUN apk add --update caddy gcc musl-dev libffi-dev 15 | 16 | WORKDIR /app 17 | COPY backend/requirements.txt /tmp/requirements.txt 18 | RUN pip install --no-cache-dir -r /tmp/requirements.txt 19 | 20 | COPY Caddyfile ./Caddyfile 21 | COPY backend ./backend 22 | COPY --from=FrontendBuilder /app/frontend/dist ./dist 23 | 24 | EXPOSE 80 25 | 26 | COPY startup.sh ./startup.sh 27 | RUN chmod +x ./startup.sh; mkdir /data 28 | CMD ["/app/startup.sh"] 29 | -------------------------------------------------------------------------------- /README.en.md: -------------------------------------------------------------------------------- 1 |

ChatGPT Web Share

2 | 3 |
4 | 5 | [![GitHub tag (latest by date)](https://img.shields.io/github/v/tag/chatpire/chatgpt-web-share?label=container&logo=docker)](https://github.com/chatpire/chatgpt-web-share/pkgs/container/chatgpt-web-share) 6 | [![Github Workflow Status](https://img.shields.io/github/actions/workflow/status/chatpire/chatgpt-web-share/docker-image.yml?label=build)](https://github.com/chatpire/chatgpt-web-share/actions) 7 | [![License](https://img.shields.io/github/license/chatpire/chatgpt-web-share)](https://github.com/chatpire/chatgpt-web-share/blob/main/LICENSE) 8 | 9 | A ChatGPT sharing solution suitable for individuals or teams. Share a single ChatGPT Plus account with multiple users, enjoying rich, controllable management and restriction features. 10 | 11 |
12 | 13 | ## Document 14 | 15 | https://cws-docs.pages.dev/en/ 16 | 17 | English document is still under construction. 18 | 19 | ## Statement 20 | 21 | This project is intended solely for learning and research purposes and is not recommended for commercial use. You should be aware that using this project may violate relevant user agreements and understand the associated risks. We are not responsible for any losses incurred as a result of using this project. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

ChatGPT Web Share

2 | 3 |
4 | 5 | [![GitHub tag (latest by date)](https://img.shields.io/github/v/tag/chatpire/chatgpt-web-share?label=container&logo=docker)](https://github.com/chatpire/chatgpt-web-share/pkgs/container/chatgpt-web-share) 6 | [![Github Workflow Status](https://img.shields.io/github/actions/workflow/status/chatpire/chatgpt-web-share/docker-image.yml?label=build)](https://github.com/chatpire/chatgpt-web-share/actions) 7 | [![License](https://img.shields.io/github/license/chatpire/chatgpt-web-share)](https://github.com/chatpire/chatgpt-web-share/blob/main/LICENSE) 8 | 9 | 适用于个人、组织或团队的 ChatGPT 共享方案。共享一个 ChatGPT Plus 账号给多人使用,提供完善的管理和限制功能。 10 | 11 | [English Readme](README.en.md) 12 | 13 |
14 | 15 | ## 文档 16 | 17 | - 项目特点:https://cws-docs.pages.dev/zh/ 18 | - 快速部署指南:http://cws-docs.pages.dev/zh/guide/quick-start.html 19 | - 演示截图:http://cws-docs.pages.dev/zh/demo/screenshots.html 20 | 21 | > (广告)如果你没有 ChatGPT Plus 账号,并且想省点心,可以考虑 [**赛博小铺的拼车服务**](https://cws-docs.pages.dev/zh/support/ads.html#%E8%B5%9B%E5%8D%9A%E5%B0%8F%E9%93%BA-chatgpt-plus-%E6%8B%BC%E8%BD%A6%E6%9C%8D%E5%8A%A1) 22 | > 23 | > *正价Plus | 6人1车 | 会话隔离 | 支持 gpts | 网页功能齐全 | 并发提问 | 直连优化* 24 | 25 | > 部署 CWS 需要一台 1C1G 以上的服务器。项目文档里的这个页面推荐了一些高性价比的服务器,低至 $15/年:[VPS推荐](https://cws-docs.pages.dev/zh/support/vps.html) 26 | 27 | ## 声明 28 | 29 | 本项目仅供学习和研究使用,不鼓励用于商业用途。您应当知悉使用本项目可能会违反相关用户协议,并了解相关的风险。我们不对任何因使用本项目而导致的任何损失负责。 30 | 31 | ## 捐助和支持 32 | 33 | 如果觉得本项目对您有帮助,欢迎通过扫描下方赞赏码捐助项目 :) 34 | 35 | donate 36 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .idea 3 | *.db 4 | .vscode 5 | files 6 | logs 7 | *.log 8 | ChatGPT-Proxy-V4 9 | *.json 10 | data* 11 | build 12 | dist 13 | -------------------------------------------------------------------------------- /backend/alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # path to migration scripts 5 | script_location = alembic 6 | 7 | # template used to generate migration file names; The default value is %%(rev)s_%%(slug)s 8 | # Uncomment the line below if you want the files to be prepended with date and time 9 | # file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s 10 | 11 | # sys.path path, will be prepended to sys.path if present. 12 | # defaults to the current working directory. 13 | prepend_sys_path = . 14 | 15 | # timezone to use when rendering the date within the migration file 16 | # as well as the filename. 17 | # If specified, requires the python-dateutil library that can be 18 | # installed by adding `alembic[tz]` to the pip requirements 19 | # string value is passed to dateutil.tz.gettz() 20 | # leave blank for localtime 21 | # timezone = 22 | 23 | # max length of characters to apply to the 24 | # "slug" field 25 | # truncate_slug_length = 40 26 | 27 | # set to 'true' to run the environment during 28 | # the 'revision' command, regardless of autogenerate 29 | # revision_environment = false 30 | 31 | # set to 'true' to allow .pyc and .pyo files without 32 | # a source .py file to be detected as revisions in the 33 | # versions/ directory 34 | # sourceless = false 35 | 36 | # version location specification; This defaults 37 | # to alembic/versions. When using multiple version 38 | # directories, initial revisions must be specified with --version-path. 39 | # The path separator used here should be the separator specified by "version_path_separator" below. 40 | # version_locations = %(here)s/bar:%(here)s/bat:alembic/versions 41 | 42 | # version path separator; As mentioned above, this is the character used to split 43 | # version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. 44 | # If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. 45 | # Valid values for version_path_separator are: 46 | # 47 | # version_path_separator = : 48 | # version_path_separator = ; 49 | # version_path_separator = space 50 | version_path_separator = os # Use os.pathsep. Default configuration used for new projects. 51 | 52 | # set to 'true' to search source files recursively 53 | # in each "version_locations" directory 54 | # new in Alembic version 1.10 55 | # recursive_version_locations = false 56 | 57 | # the output encoding used when revision files 58 | # are written from script.py.mako 59 | # output_encoding = utf-8 60 | 61 | sqlalchemy.url = sqlite+aiosqlite:///data/database.db 62 | 63 | 64 | [post_write_hooks] 65 | # post_write_hooks defines scripts or Python functions that are run 66 | # on newly generated revision scripts. See the documentation for further 67 | # detail and examples 68 | 69 | # format using "black" - use the console_scripts runner, against the "black" entrypoint 70 | # hooks = black 71 | # black.type = console_scripts 72 | # black.entrypoint = black 73 | # black.options = -l 79 REVISION_SCRIPT_FILENAME 74 | 75 | # Logging configuration -------------------------------------------------------------------------------- /backend/alembic/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration with an async dbapi. -------------------------------------------------------------------------------- /backend/alembic/env.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from alembic import context 4 | from sqlalchemy import pool 5 | from sqlalchemy.engine import Connection 6 | from sqlalchemy.ext.asyncio import async_engine_from_config 7 | 8 | from api.models.db import Base 9 | import uuid 10 | 11 | # this is the Alembic Config object, which provides 12 | # access to the values within the .ini file in use. 13 | config = context.config 14 | 15 | # Interpret the config file for Python logging. 16 | # This line sets up loggers basically. 17 | 18 | # 阻止 alembic 重复配置日志 19 | # if config.config_file_name is not None: 20 | # fileConfig(config.config_file_name, disable_existing_loggers=False) 21 | 22 | # add your model's MetaData object here 23 | # for 'autogenerate' support 24 | # from myapp import mymodel 25 | # target_metadata = mymodel.Base.metadata 26 | target_metadata = Base.metadata 27 | 28 | 29 | # other values from the config, defined by the needs of env.py, 30 | # can be acquired: 31 | # my_important_option = config.get_main_option("my_important_option") 32 | # ... etc. 33 | 34 | 35 | def run_migrations_offline() -> None: 36 | """Run migrations in 'offline' mode. 37 | 38 | This configures the context with just a URL 39 | and not an Engine, though an Engine is acceptable 40 | here as well. By skipping the Engine creation 41 | we don't even need a DBAPI to be available. 42 | 43 | Calls to context.execute() here emit the given string to the 44 | script output. 45 | 46 | """ 47 | url = config.get_main_option("sqlalchemy.url") 48 | context.configure( 49 | url=url, 50 | target_metadata=target_metadata, 51 | literal_binds=True, 52 | dialect_opts={"paramstyle": "named"}, 53 | ) 54 | 55 | with context.begin_transaction(): 56 | context.run_migrations() 57 | 58 | 59 | def do_run_migrations(connection: Connection) -> None: 60 | context.configure(connection=connection, target_metadata=target_metadata) 61 | 62 | with context.begin_transaction(): 63 | context.run_migrations() 64 | 65 | 66 | async def run_async_migrations() -> None: 67 | """In this scenario we need to create an Engine 68 | and associate a connection with the context. 69 | 70 | """ 71 | 72 | connectable = async_engine_from_config( 73 | config.get_section(config.config_ini_section, {}), 74 | prefix="sqlalchemy.", 75 | poolclass=pool.NullPool, 76 | ) 77 | 78 | async with connectable.connect() as connection: 79 | await connection.run_sync(do_run_migrations) 80 | 81 | await connectable.dispose() 82 | 83 | 84 | def run_migrations_online() -> None: 85 | """Run migrations in 'online' mode.""" 86 | 87 | connectable = config.attributes.get("connection", None) 88 | 89 | if connectable is None: 90 | asyncio.run(run_async_migrations()) 91 | else: 92 | do_run_migrations(connectable) 93 | 94 | 95 | if context.is_offline_mode(): 96 | run_migrations_offline() 97 | else: 98 | run_migrations_online() 99 | -------------------------------------------------------------------------------- /backend/alembic/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade() -> None: 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade() -> None: 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /backend/alembic/versions/0d790c2c26dc_add_uploaded_files_table.py: -------------------------------------------------------------------------------- 1 | """Add uploaded_files table 2 | 3 | Revision ID: 0d790c2c26dc 4 | Revises: aa3d85891014 5 | Create Date: 2023-09-26 19:21:46.195955 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from api.database.custom_types import UTCDateTime, Pydantic 11 | from api.models.json import UploadedFileOpenaiWebInfo 12 | 13 | # revision identifiers, used by Alembic. 14 | revision = '0d790c2c26dc' 15 | down_revision = 'aa3d85891014' 16 | branch_labels = None 17 | depends_on = None 18 | 19 | 20 | def upgrade() -> None: 21 | # ### commands auto generated by Alembic - please adjust! ### 22 | op.create_table('uploaded_files', 23 | sa.Column('id', sa.Uuid(), nullable=False, comment='uuid'), 24 | sa.Column('original_filename', sa.String(length=256), nullable=False, comment='原始文件名'), 25 | sa.Column('size', sa.Integer(), nullable=False, comment='文件大小(bytes)'), 26 | sa.Column('content_type', sa.String(length=256), nullable=True, comment='文件类型'), 27 | sa.Column('storage_path', sa.String(length=1024), nullable=True, 28 | comment='文件在服务器的存储路径,相对于配置中的存储路径;为空表示未在服务器上存储,即未上传或者已清理'), 29 | sa.Column('upload_time', UTCDateTime(timezone=True), 30 | nullable=False, comment='上传日期'), 31 | sa.Column('uploader_id', sa.Integer(), nullable=False, comment='上传的用户id'), 32 | sa.Column('openai_web_info', Pydantic(UploadedFileOpenaiWebInfo), nullable=True), 33 | sa.ForeignKeyConstraint(['uploader_id'], ['user.id'], ), 34 | sa.PrimaryKeyConstraint('id') 35 | ) 36 | # ### end Alembic commands ### 37 | 38 | 39 | def downgrade() -> None: 40 | # ### commands auto generated by Alembic - please adjust! ### 41 | op.drop_table('uploaded_files') 42 | # ### end Alembic commands ### 43 | -------------------------------------------------------------------------------- /backend/alembic/versions/333722b0921e_add_source_id_for_baseconversation.py: -------------------------------------------------------------------------------- 1 | """Add source_id for BaseConversation 2 | 3 | Revision ID: 333722b0921e 4 | Revises: 7d94b5503088 5 | Create Date: 2024-02-03 12:29:17.095124 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '333722b0921e' 14 | down_revision = '7d94b5503088' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade() -> None: 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column('conversation', sa.Column('source_id', sa.String(length=256), nullable=True, comment='对话来源id')) 22 | # ### end Alembic commands ### 23 | 24 | 25 | def downgrade() -> None: 26 | # ### commands auto generated by Alembic - please adjust! ### 27 | op.drop_column('conversation', 'source_id') 28 | # ### end Alembic commands ### 29 | -------------------------------------------------------------------------------- /backend/alembic/versions/7d94b5503088_add_extra_info_in_uploadedfileinfo.py: -------------------------------------------------------------------------------- 1 | """Add extra_info in UploadedFileInfo 2 | 3 | Revision ID: 7d94b5503088 4 | Revises: 0d790c2c26dc 5 | Create Date: 2023-11-16 09:08:43.300383 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from api.database.custom_types import Pydantic 11 | from api.models.json import UploadedFileExtraInfo 12 | 13 | # revision identifiers, used by Alembic. 14 | revision = '7d94b5503088' 15 | down_revision = '0d790c2c26dc' 16 | branch_labels = None 17 | depends_on = None 18 | 19 | 20 | def upgrade() -> None: 21 | # ### commands auto generated by Alembic - please adjust! ### 22 | op.add_column('uploaded_files', sa.Column('extra_info', Pydantic(UploadedFileExtraInfo), nullable=True)) 23 | # ### end Alembic commands ### 24 | 25 | 26 | def downgrade() -> None: 27 | # ### commands auto generated by Alembic - please adjust! ### 28 | op.drop_column('uploaded_files', 'extra_info') 29 | # ### end Alembic commands ### 30 | -------------------------------------------------------------------------------- /backend/alembic/versions/aa3d85891014_baseline.py: -------------------------------------------------------------------------------- 1 | """baseline 2 | 3 | Revision ID: aa3d85891014 4 | Revises: 5 | Create Date: 2023-09-26 18:47:07.076974 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'aa3d85891014' 14 | down_revision = None 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade() -> None: 20 | pass 21 | 22 | 23 | def downgrade() -> None: 24 | pass 25 | -------------------------------------------------------------------------------- /backend/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxduke/chatgpt-web-share/2d52c390be890c0c94203ef95fb72ba3e83add0b/backend/api/__init__.py -------------------------------------------------------------------------------- /backend/api/conf/__init__.py: -------------------------------------------------------------------------------- 1 | from .config import Config 2 | from .credentials import Credentials 3 | -------------------------------------------------------------------------------- /backend/api/conf/base_config.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | 4 | from typing import TypeVar, Generic, Type, get_args 5 | 6 | from pydantic import BaseModel 7 | from ruamel.yaml import YAML 8 | from fastapi.encoders import jsonable_encoder 9 | 10 | from api.exceptions import ConfigException 11 | 12 | T = TypeVar("T", bound=BaseModel) 13 | 14 | 15 | class BaseConfig(Generic[T]): 16 | _model: T = None 17 | _config_path = None 18 | _model_type = None 19 | 20 | def __init__(self, model_type: Type[BaseModel], config_filename: str, load_config: bool = True): 21 | self._model_type = model_type 22 | config_dir = os.environ.get('CWS_CONFIG_DIR', './data/config') 23 | self._config_path = os.path.join(config_dir, config_filename) 24 | if load_config: 25 | self.load() 26 | else: 27 | self._model = self._model_type() 28 | 29 | def __getattr__(self, key): 30 | return getattr(self._model, key) 31 | 32 | def __setattr__(self, key, value): 33 | if key in ('_model', '_config_path', '_model_type'): 34 | super().__setattr__(key, value) 35 | else: 36 | setattr(self._model, key, value) 37 | 38 | def schema(self): 39 | return self._model.schema() 40 | 41 | def model(self): 42 | return self._model.copy() 43 | 44 | def update(self, model: T): 45 | self._model = self._model_type.model_validate(model) 46 | 47 | def load(self): 48 | if not os.path.exists(self._config_path): 49 | raise ConfigException(f"Config file not found: {self._config_path}") 50 | try: 51 | with open(self._config_path, mode='r', encoding='utf-8') as f: 52 | # 读取配置 53 | yaml = YAML() 54 | config_dict = yaml.load(f) or {} 55 | self._model = self._model_type.model_validate(config_dict) 56 | except Exception as e: 57 | raise ConfigException(f"Cannot read config ({self._config_path}), error: {str(e)}") 58 | 59 | def save(self): 60 | config_dict = jsonable_encoder(self._model.model_dump()) 61 | # 复制 self._config_path 备份一份 62 | config_dir = os.path.dirname(self._config_path) 63 | if not os.path.exists(config_dir): 64 | raise ConfigException(f"Config dir not found: {config_dir}") 65 | backup_config_path = os.path.join(config_dir, f"{os.path.basename(self._config_path)}.backup.yaml") 66 | shutil.copyfile(self._config_path, backup_config_path) 67 | with open(self._config_path, mode='w', encoding='utf-8') as sf: 68 | yaml = YAML() 69 | yaml.dump(config_dict, sf) 70 | -------------------------------------------------------------------------------- /backend/api/conf/credentials.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from pydantic import BaseModel 4 | 5 | from api.conf.base_config import BaseConfig 6 | from utils.common import SingletonMeta 7 | 8 | _TYPE_CHECKING = False 9 | 10 | 11 | class CredentialsModel(BaseModel): 12 | openai_web_access_token: Optional[str] = None 13 | # chatgpt_account_username: Optional[str] = None 14 | # chatgpt_account_password: Optional[str] = None 15 | openai_api_key: Optional[str] = None 16 | 17 | 18 | class Credentials(BaseConfig[CredentialsModel], metaclass=SingletonMeta): 19 | if _TYPE_CHECKING: 20 | openai_web_access_token: Optional[str] 21 | # chatgpt_account_username: Optional[str] 22 | # chatgpt_account_password: Optional[str] 23 | openai_api_key: Optional[str] 24 | 25 | def __init__(self, load_config: bool = True): 26 | super().__init__(CredentialsModel, "credentials.yaml", load_config=load_config) 27 | -------------------------------------------------------------------------------- /backend/api/database/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxduke/chatgpt-web-share/2d52c390be890c0c94203ef95fb72ba3e83add0b/backend/api/database/__init__.py -------------------------------------------------------------------------------- /backend/api/database/custom_types/__init__.py: -------------------------------------------------------------------------------- 1 | from .guid import GUID 2 | from .pydantic_type import Pydantic 3 | from .utc_datetime import UTCDateTime 4 | -------------------------------------------------------------------------------- /backend/api/database/custom_types/guid.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.types import TypeDecorator, CHAR 2 | from sqlalchemy.dialects.postgresql import UUID 3 | import uuid 4 | 5 | 6 | class GUID(TypeDecorator): 7 | """Platform-independent GUID type. 8 | https://docs.sqlalchemy.org/en/20/core/custom_types.html#backend-agnostic-guid-type 9 | 10 | Uses PostgreSQL's UUID type, otherwise uses CHAR(32), storing as stringified hex values. 11 | """ 12 | 13 | impl = CHAR 14 | cache_ok = True 15 | 16 | def load_dialect_impl(self, dialect): 17 | if dialect.name == "postgresql": 18 | return dialect.type_descriptor(UUID()) 19 | else: 20 | return dialect.type_descriptor(CHAR(32)) 21 | 22 | def process_bind_param(self, value, dialect): 23 | if value is None: 24 | return value 25 | elif dialect.name == "postgresql": 26 | return str(value) 27 | else: 28 | if not isinstance(value, uuid.UUID): 29 | return "%.32x" % uuid.UUID(value).int 30 | else: 31 | # hexstring 32 | return "%.32x" % value.int 33 | 34 | def process_result_value(self, value, dialect): 35 | if value is None: 36 | return value 37 | else: 38 | if not isinstance(value, uuid.UUID): 39 | value = uuid.UUID(value) 40 | return value 41 | -------------------------------------------------------------------------------- /backend/api/database/custom_types/pydantic_type.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import Type, Any, Optional 3 | 4 | import sqlalchemy 5 | from fastapi.encoders import jsonable_encoder 6 | from pydantic import BaseModel 7 | from sqlalchemy import Dialect 8 | from sqlalchemy.dialects.postgresql import JSONB 9 | from sqlalchemy.sql.type_api import _T 10 | 11 | 12 | class Pydantic(sqlalchemy.types.TypeDecorator): 13 | """Pydantic type. 14 | SAVING: 15 | - Uses SQLAlchemy JSON type under the hood. 16 | - Acceps the pydantic model and converts it to a dict on save. 17 | - SQLAlchemy engine JSON-encodes the dict to a string. 18 | RETRIEVING: 19 | - Pulls the string from the database. 20 | - SQLAlchemy engine JSON-decodes the string to a dict. 21 | - Uses the dict to create a pydantic model. 22 | 23 | https://roman.pt/posts/pydantic-in-sqlalchemy-fields/ 24 | """ 25 | 26 | @property 27 | def python_type(self) -> Type[Any]: 28 | return BaseModel 29 | 30 | def process_literal_param(self, value: Optional[_T], dialect: Dialect) -> str: 31 | if value is None: 32 | return "NULL" 33 | else: 34 | # 将 Pydantic 对象转换为 JSON 字符串 35 | json_str = json.dumps(jsonable_encoder(value)) 36 | if dialect.name == "postgresql": 37 | # 对于 PostgreSQL,需要用 E'' 引用 JSON 字符串(未测试) 38 | return f"E'{json_str}'" 39 | else: 40 | # 对于其他数据库,使用单引号引用 JSON 字符串 41 | return f"'{json_str}'" 42 | 43 | impl = sqlalchemy.types.JSON 44 | 45 | def __init__(self, pydantic_type): 46 | super().__init__() 47 | self.pydantic_type = pydantic_type 48 | 49 | def load_dialect_impl(self, dialect): 50 | # Use JSONB for PostgreSQL and JSON for other databases. 51 | if dialect.name == "postgresql": 52 | return dialect.type_descriptor(JSONB()) 53 | else: 54 | return dialect.type_descriptor(sqlalchemy.JSON()) 55 | 56 | def process_bind_param(self, value, dialect): 57 | # return value.dict() if value else None 58 | return jsonable_encoder(value) if value else None 59 | 60 | def process_result_value(self, value, dialect): 61 | return self.pydantic_type.model_validate(value) if value else None 62 | -------------------------------------------------------------------------------- /backend/api/database/custom_types/utc_datetime.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from sqlalchemy.types import DateTime, TypeDecorator 4 | 5 | from datetime import timezone 6 | 7 | 8 | class UTCDateTime(TypeDecorator): 9 | """Almost equivalent to :class:`~sqlalchemy.types.DateTime` with 10 | ``timezone=True`` option, but it differs from that by: 11 | 12 | - Never silently take naive :class:`~datetime.datetime`, instead it 13 | always raise :exc:`ValueError` unless time zone aware value. 14 | - :class:`~datetime.datetime` value's :attr:`~datetime.datetime.tzinfo` 15 | is always converted to UTC. 16 | - Unlike SQLAlchemy's built-in :class:`~sqlalchemy.types.DateTime`, 17 | it never return naive :class:`~datetime.datetime`, but time zone 18 | aware value, even with SQLite or MySQL. 19 | 20 | modified from https://github.com/spoqa/sqlalchemy-utc 21 | """ 22 | 23 | impl = DateTime(timezone=True) 24 | cache_ok = True 25 | 26 | def process_bind_param(self, value, dialect): 27 | if value is not None: 28 | if not isinstance(value, datetime.datetime): 29 | raise TypeError('expected datetime.datetime, not ' + 30 | repr(value)) 31 | elif value.tzinfo is None: 32 | raise ValueError('naive datetime is disallowed') 33 | return value.astimezone(timezone.utc) 34 | 35 | def process_result_value(self, value, dialect): 36 | if value is not None: 37 | if value.tzinfo is None: 38 | value = value.replace(tzinfo=timezone.utc) 39 | else: 40 | value = value.astimezone(timezone.utc) 41 | return value -------------------------------------------------------------------------------- /backend/api/database/mongodb.py: -------------------------------------------------------------------------------- 1 | from beanie import init_beanie 2 | from motor.motor_asyncio import AsyncIOMotorClient 3 | 4 | from api.conf import Config 5 | from api.models.doc import OpenaiApiConversationHistoryDocument, OpenaiWebConversationHistoryDocument, AskLogDocument, \ 6 | RequestLogDocument 7 | from utils.logger import get_logger 8 | 9 | logger = get_logger(__name__) 10 | config = Config() 11 | 12 | 13 | client: AsyncIOMotorClient | None = None 14 | 15 | 16 | async def init_mongodb(): 17 | global client 18 | client = AsyncIOMotorClient(config.data.mongodb_url) 19 | await init_beanie(database=client[config.data.mongodb_db_name], 20 | document_models=[OpenaiApiConversationHistoryDocument, OpenaiWebConversationHistoryDocument, AskLogDocument, 21 | RequestLogDocument]) 22 | # 展示当前mongodb数据库用量 23 | db = client[config.data.mongodb_db_name] 24 | stats = await db.command({"dbStats": 1}) 25 | logger.info( 26 | f"MongoDB initialized. dataSize: {stats['dataSize'] / 1024 / 1024:.2f} MB, objects: {stats['objects']}") 27 | await handle_timeseries() 28 | 29 | 30 | async def handle_timeseries(): 31 | """ 32 | 对于 AskStatDocument 和 HTTPRequestStatDocument, 当 expireAfterSeconds 更改时,beanie 并不会自动更改 33 | 此时需要主动更改 34 | """ 35 | global client 36 | assert client is not None, "MongoDB not initialized" 37 | db = client[config.data.mongodb_db_name] 38 | time_series_docs = [AskLogDocument, RequestLogDocument] 39 | config_ttls = [config.stats.ask_stats_ttl, config.stats.request_stats_ttl] 40 | for doc, config_ttl in zip(time_series_docs, config_ttls): 41 | collection_name = doc.get_collection_name() 42 | coll_info = await db.command({"listCollections": 1, "filter": {"name": collection_name}}) 43 | if not coll_info["cursor"]["firstBatch"]: 44 | logger.error(f"Collection {collection_name} not found") 45 | continue 46 | current_ttl = coll_info["cursor"]["firstBatch"][0]["options"]["expireAfterSeconds"] 47 | 48 | # 关闭自动过期 49 | if current_ttl != "off" and config_ttl == -1: 50 | await db.command({ 51 | "collMod": collection_name, 52 | "expireAfterSeconds": "off" 53 | }) 54 | logger.info(f"Auto expire of collection {collection_name} disabled") 55 | continue 56 | 57 | # 更改过期时间 58 | if current_ttl != config_ttl: 59 | logger.info(f"Updating TTL of collection {collection_name} from {current_ttl} to {config_ttl}") 60 | db.command({ 61 | "collMod": collection_name, 62 | "expireAfterSeconds": config_ttl 63 | }) 64 | else: 65 | logger.debug(f"TTL of collection {collection_name} not change: {config_ttl}") 66 | -------------------------------------------------------------------------------- /backend/api/enums/__init__.py: -------------------------------------------------------------------------------- 1 | from .models import ChatSourceTypes, OpenaiWebChatModels, OpenaiApiChatModels 2 | from .status import OpenaiWebChatStatus 3 | -------------------------------------------------------------------------------- /backend/api/enums/models.py: -------------------------------------------------------------------------------- 1 | from enum import auto 2 | 3 | from strenum import StrEnum 4 | 5 | 6 | class ChatSourceTypes(StrEnum): 7 | openai_web = auto() 8 | openai_api = auto() 9 | 10 | 11 | def get_model_code_mapping(source_cls): 12 | from api.conf import Config 13 | 14 | cls_to_source = { 15 | "OpenaiWebChatModels": ChatSourceTypes.openai_web, 16 | "OpenaiApiChatModels": ChatSourceTypes.openai_api, 17 | } 18 | source = cls_to_source.get(source_cls.__name__, None) 19 | source_model_code_mapping = { 20 | "openai_web": Config().openai_web.model_code_mapping, 21 | "openai_api": Config().openai_api.model_code_mapping, 22 | } 23 | return source_model_code_mapping[source] 24 | 25 | 26 | class BaseChatModelEnum(StrEnum): 27 | def code(self): 28 | result = get_model_code_mapping(self.__class__).get(self.name, None) 29 | assert result, f"model name not found: {self.name}" 30 | return result 31 | 32 | @classmethod 33 | def from_code(cls, code: str): 34 | for name, value in get_model_code_mapping(cls).items(): 35 | if value == code: 36 | return cls[name] 37 | return None 38 | 39 | 40 | class OpenaiWebChatModels(BaseChatModelEnum): 41 | gpt_3_5 = auto() 42 | gpt_4 = auto() 43 | gpt_4o = auto() 44 | o1 = auto() 45 | o1_mini = auto() 46 | 47 | 48 | class OpenaiApiChatModels(BaseChatModelEnum): 49 | gpt_3_5 = auto() 50 | gpt_4 = auto() 51 | gpt_4o = auto() 52 | o1 = auto() 53 | o1_mini = auto() 54 | 55 | 56 | if __name__ == "__main__": 57 | print(list(OpenaiWebChatModels)) 58 | -------------------------------------------------------------------------------- /backend/api/enums/options.py: -------------------------------------------------------------------------------- 1 | from enum import auto 2 | from strenum import StrEnum 3 | 4 | 5 | class OpenaiWebFileUploadStrategyOption(StrEnum): 6 | server_upload_only = auto() 7 | browser_upload_only = auto() 8 | browser_upload_when_file_size_exceed = auto() 9 | -------------------------------------------------------------------------------- /backend/api/enums/status.py: -------------------------------------------------------------------------------- 1 | from enum import auto 2 | from strenum import StrEnum 3 | 4 | 5 | class OpenaiWebChatStatus(StrEnum): 6 | asking = auto() 7 | queueing = auto() 8 | idling = auto() 9 | -------------------------------------------------------------------------------- /backend/api/exceptions.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | 4 | class SelfDefinedException(Exception): 5 | def __init__(self, reason: Any = None, message: str = "", code: int = -1) -> None: 6 | self.reason = reason # 异常主要原因 7 | self.message = message # 更细节的描述 8 | self.code = code # 错误码:-1 为默认;0~1000 以内正数为 http 错误码;10000 以上为自定义错误码 9 | 10 | def __str__(self): 11 | return f"{self.__class__.__name__}: [{self.code}] {self.reason} {self.message}" 12 | 13 | 14 | class AuthenticationFailedException(SelfDefinedException): 15 | def __init__(self, message: str = ""): 16 | super().__init__(reason="errors.authenticationFailed", message=message, code=10401) 17 | 18 | 19 | class AuthorityDenyException(SelfDefinedException): 20 | def __init__(self, message: str = ""): 21 | super().__init__(reason="errors.authorityDeny", message=message, code=10403) 22 | 23 | 24 | class UserNotExistException(SelfDefinedException): 25 | def __init__(self, message: str = ""): 26 | super().__init__(reason="errors.userNotExist", message=message) 27 | 28 | 29 | class UserAlreadyExists(SelfDefinedException): 30 | def __init__(self, message: str = ""): 31 | super().__init__(reason="errors.userAlreadyExists", message=message) 32 | 33 | 34 | class InvalidParamsException(SelfDefinedException): 35 | def __init__(self, message: str = ""): 36 | super().__init__(reason="errors.invalidParams", message=message) 37 | 38 | 39 | class ResourceNotFoundException(SelfDefinedException): 40 | def __init__(self, message: str = ""): 41 | super().__init__(reason="errors.resourceNotFound", message=message, code=404) 42 | 43 | 44 | class InvalidRequestException(SelfDefinedException): 45 | def __init__(self, message: str = ""): 46 | super().__init__(reason="errors.invalidRequest", message=message) 47 | 48 | 49 | class InternalException(SelfDefinedException): 50 | def __init__(self, message: str = ""): 51 | super().__init__(reason="errors.internal", message=message) 52 | 53 | 54 | class ConfigException(SelfDefinedException): 55 | def __init__(self, message: str = ""): 56 | super().__init__(reason="errors.config", message=message) 57 | 58 | 59 | class OpenaiException(SelfDefinedException): 60 | def __init__(self, reason: str, message: str = "", code: int = -1): 61 | super().__init__(reason=reason, message=message, code=code) 62 | 63 | 64 | class OpenaiWebException(OpenaiException): 65 | def __init__(self, message: str = "", code: int = -1): 66 | super().__init__(reason="errors.openaiWeb", message=message, code=code) 67 | 68 | 69 | class OpenaiApiException(OpenaiException): 70 | def __init__(self, message: str = "", code: int = -1): 71 | super().__init__(reason="errors.openaiWeb", message=message, code=code) 72 | 73 | 74 | class ArkoseForwardException(Exception): 75 | def __init__(self, message: str = "", code: int = 404): 76 | self.message = message 77 | self.code = code 78 | -------------------------------------------------------------------------------- /backend/api/file_provider.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import uuid 4 | from datetime import datetime, timezone 5 | from mimetypes import guess_type 6 | from pathlib import Path 7 | 8 | from PIL import Image 9 | import aiofiles 10 | from fastapi import UploadFile 11 | from sqlalchemy import select 12 | from sqlalchemy.ext.asyncio import AsyncSession 13 | 14 | from api.conf import Config 15 | from api.models.db import UploadedFileInfo 16 | from api.models.json import UploadedFileExtraInfo 17 | 18 | config = Config() 19 | 20 | 21 | class FileProvider: 22 | def __init__(self, storage_dir: Path = None, max_size: int = None): 23 | self.max_size = max_size or config.data.max_file_upload_size 24 | self.storage_dir = storage_dir or (Path(config.data.data_dir) / "uploads") 25 | if not self.storage_dir.exists(): 26 | self.storage_dir.mkdir() 27 | 28 | async def save_file(self, file: UploadFile, user_id: int, session: AsyncSession) -> UploadedFileInfo: 29 | file_name = f"{uuid.uuid4()}.dat" 30 | file_dir_path = self.storage_dir / f"{user_id}" 31 | file_path = file_dir_path / file_name 32 | 33 | if not file_dir_path.exists(): 34 | file_dir_path.mkdir(parents=True) 35 | 36 | async with aiofiles.open(file_path, "wb") as buffer: 37 | while True: 38 | chunk = await file.read(1024 * 1024) # read by 1MB chunk 39 | if not chunk: 40 | break 41 | await buffer.write(chunk) 42 | 43 | # check if file is image 44 | content_type = file.content_type or guess_type(file.filename)[0] 45 | if content_type.startswith("image/"): 46 | img = Image.open(file_path) 47 | width, height = img.size 48 | else: 49 | width, height = None, None 50 | 51 | file_info = UploadedFileInfo( 52 | original_filename=file.filename, 53 | size=file.size, 54 | content_type=guess_type(file.filename)[0], 55 | storage_path=str(file_path.relative_to(self.storage_dir)), 56 | uploader_id=user_id, 57 | upload_time=datetime.now().astimezone(tz=timezone.utc), 58 | extra_info=UploadedFileExtraInfo(width=width, height=height) 59 | ) 60 | session.add(file_info) 61 | await session.commit() 62 | 63 | return file_info 64 | 65 | async def get_file_info(self, file_id: uuid.UUID, session: AsyncSession): 66 | result = await session.execute(select(UploadedFileInfo).where(UploadedFileInfo.id == file_id)) 67 | file_info = result.scalars().first() 68 | return file_info 69 | 70 | def get_absolute_path(self, path: str) -> Path: 71 | return self.storage_dir / path 72 | -------------------------------------------------------------------------------- /backend/api/globals.py: -------------------------------------------------------------------------------- 1 | server_log_filename = None 2 | 3 | startup_time = None 4 | -------------------------------------------------------------------------------- /backend/api/middlewares/__init__.py: -------------------------------------------------------------------------------- 1 | from .asgi_logger import AccessLoggerMiddleware 2 | from .request_statistics import StatisticsMiddleware 3 | 4 | -------------------------------------------------------------------------------- /backend/api/middlewares/asgi_logger/__init__.py: -------------------------------------------------------------------------------- 1 | from .middleware import AccessLoggerMiddleware -------------------------------------------------------------------------------- /backend/api/middlewares/asgi_logger/utils.py: -------------------------------------------------------------------------------- 1 | from urllib.parse import quote 2 | 3 | from asgiref.typing import HTTPScope 4 | 5 | 6 | def get_client_addr(scope: HTTPScope): 7 | if scope["client"] is None: 8 | return "-" # pragma: no cover 9 | return f"{scope['client'][0]}:{scope['client'][1]}" 10 | 11 | 12 | def get_path_with_query_string(scope: HTTPScope) -> str: 13 | path_with_query_string = quote(scope.get("root_path", "") + scope["path"]) 14 | if scope["query_string"]: # pragma: no cover 15 | return f"{path_with_query_string}?{scope['query_string'].decode('ascii')}" 16 | return path_with_query_string -------------------------------------------------------------------------------- /backend/api/middlewares/request_statistics.py: -------------------------------------------------------------------------------- 1 | import json 2 | import re 3 | import time 4 | from typing import Optional 5 | 6 | from asgiref.typing import ASGI3Application, HTTPScope, ASGIReceiveCallable, ASGISendCallable 7 | from fastapi.routing import APIRoute 8 | 9 | import api.globals as g 10 | from api.models.doc import RequestLogDocument, RequestLogMeta 11 | 12 | from utils.logger import get_logger 13 | 14 | logger = get_logger(__name__) 15 | 16 | 17 | class StatisticsMiddleware: 18 | """ 19 | 统计请求的中间件 20 | """ 21 | 22 | def __init__( 23 | self, 24 | app: ASGI3Application, 25 | filter_keywords: Optional[list[str]] = None, 26 | ) -> None: 27 | self.app = app 28 | self.filter_keywords = filter_keywords 29 | 30 | async def __call__(self, scope: HTTPScope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None: 31 | if scope["type"] != "http" and scope["type"] != "websocket": 32 | return await self.app(scope, receive, send) 33 | 34 | raw_status_code = None 35 | body_code = None 36 | 37 | async def send_with_inspecting_body(message): 38 | """用于记录状态码""" 39 | nonlocal raw_status_code, body_code 40 | if message["type"] == "http.response.start": 41 | raw_status_code = message.get("status", None) 42 | elif message["type"] == "http.response.body": 43 | body = message.get("body", None) # byte string 44 | if body is not None: 45 | body = body.decode("utf-8") 46 | try: 47 | body = json.loads(body) 48 | body_code = body.get("code", None) 49 | except json.JSONDecodeError: 50 | pass 51 | 52 | await send(message) 53 | 54 | start_time = time.time() 55 | 56 | await self.app(scope, receive, send_with_inspecting_body) 57 | 58 | end_time = time.time() 59 | 60 | route: APIRoute | None = scope.get("route", None) 61 | if route is None: 62 | return 63 | 64 | if self.filter_keywords: 65 | for keyword in self.filter_keywords: 66 | if route.path.find(keyword) != -1: 67 | return 68 | 69 | user_id = None 70 | if "auth_user" in scope: 71 | user = scope["auth_user"] 72 | user_id = user.id 73 | 74 | if scope.get("method"): 75 | method = scope["method"] 76 | elif scope["type"] == "websocket": 77 | method = "WEBSOCKET" 78 | else: 79 | logger.debug(f"Unknown method for scope type: {scope['type']}") 80 | return 81 | 82 | elapsed_ms = end_time - start_time 83 | elapsed_ms = round(elapsed_ms * 1000, 2) 84 | 85 | await RequestLogDocument( 86 | meta=RequestLogMeta(route_path=route.path, method=method), 87 | user_id=user_id, 88 | elapsed_ms=elapsed_ms, 89 | status=body_code or raw_status_code or scope.get("ask_websocket_close_code", None), 90 | ).create() 91 | -------------------------------------------------------------------------------- /backend/api/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxduke/chatgpt-web-share/2d52c390be890c0c94203ef95fb72ba3e83add0b/backend/api/models/__init__.py -------------------------------------------------------------------------------- /backend/api/models/doc/openai_web_code_interpreter.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Literal, Any 2 | 3 | from pydantic import BaseModel 4 | 5 | 6 | class OpenaiWebChatMessageMetadataAttachment(BaseModel): 7 | name: Optional[str] = None 8 | id: Optional[str] = None 9 | size: Optional[int] = None 10 | height: Optional[int] = None 11 | width: Optional[int] = None 12 | mimeType: Optional[str] = None 13 | 14 | 15 | class OpenaiWebChatMessageMetadataAggregateResultMessage(BaseModel): 16 | message_type: Optional[Literal['image', 'stream'] | str] = None 17 | time: Optional[float] = None 18 | sender: Optional[Literal['server'] | str] = None 19 | # image 20 | image_url: Optional[str] = None 21 | # stream 22 | stream_name: Optional[str] = None 23 | text: Optional[str] = None 24 | 25 | 26 | class OpenaiWebChatMessageMetadataAggregateResult(BaseModel): 27 | status: Optional[Literal['failed_with_in_kernel_exception', 'success'] | str] = None 28 | run_id: Optional[str] = None 29 | start_time: Optional[float] = None 30 | update_time: Optional[float] = None 31 | end_time: Optional[float] = None 32 | final_expression_output: Optional[Any] = None 33 | code: Optional[str] = None 34 | in_kernel_exception: Optional[dict[str, Any]] = None # name, traceback [], args [], notes [] 35 | messages: Optional[list[OpenaiWebChatMessageMetadataAggregateResultMessage]] = None 36 | jupyter_messages: Optional[list[Any]] = None 37 | -------------------------------------------------------------------------------- /backend/api/models/json.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from typing import Optional, Generic, TypeVar, get_args, Literal 3 | 4 | from pydantic import model_validator, BaseModel, Field, create_model, RootModel 5 | 6 | from api.enums import OpenaiWebChatModels, OpenaiApiChatModels 7 | 8 | ModelT = TypeVar('ModelT', bound=OpenaiWebChatModels | OpenaiApiChatModels) 9 | 10 | 11 | class OpenaiWebPerModelAskCount(RootModel[dict[str, int]]): 12 | root: dict[str, int] = {model: 0 for model in list(OpenaiWebChatModels)} 13 | 14 | @model_validator(mode="after") 15 | @classmethod 16 | def check(cls, m): 17 | # 如果某个值缺失,则默认设置为0 18 | for model in list(OpenaiWebChatModels): 19 | if model not in m.root: 20 | m.root[model] = 0 21 | return m 22 | 23 | @staticmethod 24 | def unlimited(): 25 | return OpenaiWebPerModelAskCount(root={model: -1 for model in list(OpenaiWebChatModels)}) 26 | 27 | 28 | class OpenaiApiPerModelAskCount(RootModel[dict[str, int]]): 29 | root: dict[str, int] = {model: 0 for model in list(OpenaiApiChatModels)} 30 | 31 | @model_validator(mode="after") 32 | @classmethod 33 | def check(cls, m): 34 | for model in list(OpenaiApiChatModels): 35 | if model not in m.root: 36 | m.root[model] = 0 37 | return m 38 | 39 | @staticmethod 40 | def unlimited(): 41 | return OpenaiApiPerModelAskCount(root={model: -1 for model in list(OpenaiApiChatModels)}) 42 | 43 | 44 | class TimeWindowRateLimit(BaseModel): 45 | window_seconds: int = Field(..., description="时间窗口大小,单位为秒") 46 | max_requests: int = Field(..., description="在给定时间窗口内最多的请求次数") 47 | 48 | 49 | class DailyTimeSlot(BaseModel): 50 | start_time: datetime.time = Field(..., description="每天可使用的开始时间") 51 | end_time: datetime.time = Field(..., description="每天可使用的结束时间") 52 | 53 | 54 | class CustomOpenaiApiSettings(BaseModel): 55 | url: Optional[str] = None 56 | key: Optional[str] = None 57 | 58 | 59 | class UploadedFileOpenaiWebInfo(BaseModel): 60 | file_id: Optional[str] = None 61 | use_case: Optional[Literal['my_files', 'multimodal'] | str] = None 62 | upload_url: Optional[str] = Field(None, description="上传文件的url, 上传后应清空该字段") 63 | download_url: Optional[str] = None 64 | 65 | 66 | class UploadedFileExtraInfo(BaseModel): 67 | width: Optional[int] = None 68 | height: Optional[int] = None 69 | -------------------------------------------------------------------------------- /backend/api/models/types.py: -------------------------------------------------------------------------------- 1 | from typing import Literal 2 | 3 | SourceTypeLiteral = Literal["openai_web", "openai_api"] 4 | -------------------------------------------------------------------------------- /backend/api/routers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxduke/chatgpt-web-share/2d52c390be890c0c94203ef95fb72ba3e83add0b/backend/api/routers/__init__.py -------------------------------------------------------------------------------- /backend/api/routers/logs.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from datetime import datetime 3 | 4 | from fastapi import APIRouter, Depends 5 | 6 | import api.globals as g 7 | from api.conf import Config, Credentials 8 | from api.models.db import User 9 | from api.models.doc import AskLogDocument 10 | from api.schemas import LogFilterOptions 11 | from api.users import current_super_user 12 | from utils.logger import get_logger 13 | 14 | logger = get_logger(__name__) 15 | config = Config() 16 | credentials = Credentials() 17 | 18 | router = APIRouter() 19 | 20 | 21 | def read_last_n_lines(file_path, n, exclude_key_words=None): 22 | if exclude_key_words is None: 23 | exclude_key_words = [] 24 | try: 25 | with open(file_path, "r") as f: 26 | lines = f.readlines()[::-1] 27 | except FileNotFoundError: 28 | return [f"File not found: {file_path}"] 29 | last_n_lines = [] 30 | for line in lines: 31 | if len(last_n_lines) >= n: 32 | break 33 | if any([line.find(key_word) != -1 for key_word in exclude_key_words]): 34 | continue 35 | last_n_lines.append(line) 36 | return last_n_lines[::-1] 37 | 38 | 39 | @router.post("/logs/server", tags=["logs"]) 40 | async def get_server_logs(_user: User = Depends(current_super_user), options: LogFilterOptions = LogFilterOptions()): 41 | lines = read_last_n_lines( 42 | g.server_log_filename, 43 | options.max_lines, 44 | options.exclude_keywords 45 | ) 46 | return lines 47 | 48 | 49 | @router.get("/logs/completions", tags=["logs"], response_model=list[AskLogDocument]) 50 | async def get_completion_logs(start_time: datetime = None, end_time: datetime = None, max_results: int = 100, 51 | _user: User = Depends(current_super_user)): 52 | criteria = [] 53 | if start_time: 54 | criteria.append(AskLogDocument.time >= start_time) 55 | if end_time: 56 | criteria.append(AskLogDocument.time <= end_time) 57 | if not criteria: 58 | logs = await AskLogDocument.find_all().sort(-AskLogDocument.time).limit(max_results).to_list() 59 | else: 60 | logs = await AskLogDocument.find(*criteria).sort(-AskLogDocument.time).limit(max_results).to_list() 61 | return logs 62 | -------------------------------------------------------------------------------- /backend/api/routers/status.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | 3 | from fastapi import Depends, APIRouter 4 | from fastapi_cache.decorator import cache 5 | 6 | from api.models.db import User 7 | from api.models.doc import AskLogDocument 8 | from api.routers.conv import openai_web_manager 9 | from api.routers.system import count_active_users_cached, count_active_users 10 | from api.schemas.status_schemas import CommonStatusSchema 11 | from api.users import current_active_user 12 | from api.enums import OpenaiWebChatModels 13 | from utils.logger import get_logger 14 | 15 | router = APIRouter() 16 | logger = get_logger(__name__) 17 | 18 | 19 | @router.get("/status/common", tags=["status"], response_model=CommonStatusSchema) 20 | @cache(expire=60) 21 | async def get_server_status(_user: User = Depends(current_active_user)): 22 | result = await count_active_users() 23 | active_user_in_5m, active_user_in_1h, active_user_in_1d, queueing_count, _ = result 24 | pipeline = [ 25 | { 26 | '$facet': { 27 | 'not_found': [ 28 | {'$project': {'_id': None, 'total': {'$const': 0}}}, 29 | {'$limit': 1} 30 | ], 31 | 'found': [ 32 | {'$match': {'time': {'$gte': datetime.utcnow() - timedelta(hours=3)}}}, 33 | {'$match': { 34 | 'meta.model': {'$in': [name for name in list(OpenaiWebChatModels) if name.startswith('gpt_4')]}, 35 | 'meta.source': 'openai_web'} 36 | }, 37 | {'$count': 'total'} 38 | ] 39 | } 40 | }, 41 | { 42 | '$replaceRoot': { 43 | 'newRoot': { 44 | '$mergeObjects': [ 45 | {'$arrayElemAt': ['$not_found', 0]}, 46 | {'$arrayElemAt': ['$found', 0]} 47 | ] 48 | } 49 | } 50 | } 51 | ] 52 | aggregate_result = await AskLogDocument.aggregate(pipeline).to_list(length=1) 53 | gpt4_count_in_3_hours = aggregate_result[0].get('total', 0) 54 | 55 | result = CommonStatusSchema( 56 | active_user_in_5m=active_user_in_5m, 57 | active_user_in_1h=active_user_in_1h, 58 | active_user_in_1d=active_user_in_1d, 59 | is_chatbot_busy=openai_web_manager.is_busy(), 60 | chatbot_waiting_count=queueing_count, 61 | gpt4_count_in_3_hours=gpt4_count_in_3_hours 62 | ) 63 | return result 64 | -------------------------------------------------------------------------------- /backend/api/schemas/__init__.py: -------------------------------------------------------------------------------- 1 | from .system_schemas import * 2 | from .user_schemas import * 3 | from .conversation_schemas import * 4 | -------------------------------------------------------------------------------- /backend/api/schemas/file_schemas.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import Optional, Literal 3 | 4 | from pydantic import ConfigDict, BaseModel 5 | import uuid 6 | 7 | from api.enums.options import OpenaiWebFileUploadStrategyOption 8 | from api.models.json import UploadedFileOpenaiWebInfo, UploadedFileExtraInfo 9 | 10 | 11 | class UploadedFileInfoSchema(BaseModel): 12 | id: uuid.UUID 13 | original_filename: str 14 | size: int 15 | storage_path: Optional[str] = None 16 | content_type: Optional[str] = None 17 | upload_time: datetime 18 | uploader_id: int 19 | openai_web_info: Optional[UploadedFileOpenaiWebInfo] = None 20 | extra_info: Optional[UploadedFileExtraInfo] = None 21 | model_config = ConfigDict(from_attributes=True) 22 | 23 | 24 | class StartUploadRequestSchema(BaseModel): 25 | file_name: str 26 | file_size: int 27 | width: Optional[int] = None 28 | height: Optional[int] = None 29 | mime_type: Optional[str] = None 30 | use_case: Literal['my_files', 'multimodal'] # Openai Web:图片使用 multimodal,其它使用 my_files 31 | 32 | 33 | class StartUploadResponseSchema(BaseModel): 34 | strategy: OpenaiWebFileUploadStrategyOption 35 | file_max_size: int 36 | upload_file_info: Optional[UploadedFileInfoSchema] = None # 为空意味着后端不暂存文件,前端直接上传到openai 37 | -------------------------------------------------------------------------------- /backend/api/schemas/status_schemas.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | 4 | class CommonStatusSchema(BaseModel): 5 | active_user_in_5m: int = None 6 | active_user_in_1h: int = None 7 | active_user_in_1d: int = None 8 | is_chatbot_busy: bool = None 9 | chatbot_waiting_count: int = None 10 | gpt4_count_in_3_hours: int = None 11 | -------------------------------------------------------------------------------- /backend/api/schemas/system_schemas.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timezone 2 | from typing import Optional, Union, Annotated 3 | 4 | from pydantic import field_validator, ConfigDict, BaseModel, Field, field_serializer 5 | 6 | from api.models.doc import OpenaiWebAskLogMeta, OpenaiApiAskLogMeta 7 | 8 | 9 | class SystemInfo(BaseModel): 10 | startup_time: float 11 | total_user_count: int 12 | total_conversation_count: int 13 | valid_conversation_count: int 14 | 15 | 16 | class LogFilterOptions(BaseModel): 17 | max_lines: int = 100 18 | exclude_keywords: list[str] = None 19 | 20 | @field_validator("max_lines") 21 | @classmethod 22 | def max_lines_must_be_positive(cls, v): 23 | if v <= 0: 24 | raise ValueError("max_lines must be positive") 25 | return v 26 | 27 | 28 | class RequestLogAggregationID(BaseModel): 29 | start_time: Optional[datetime] = None 30 | route_path: Optional[str] = None 31 | method: Optional[str] = None 32 | 33 | @field_serializer("start_time") 34 | def serialize_dt(self, start_time: Optional[datetime], _info): 35 | if start_time: 36 | return start_time.replace(tzinfo=timezone.utc) 37 | return None 38 | 39 | 40 | class RequestLogAggregation(BaseModel): 41 | id: RequestLogAggregationID = Field(alias="_id") # 起始时间 42 | count: int # 时间间隔内的请求数量 43 | user_ids: list[Optional[int]] = [] # 用户ID列表 44 | avg_elapsed_ms: Optional[float] = None 45 | 46 | 47 | class AskLogAggregationID(BaseModel): 48 | start_time: datetime 49 | meta: Optional[Annotated[Union[OpenaiWebAskLogMeta, OpenaiApiAskLogMeta], Field(discriminator='source')]] = None 50 | 51 | @field_serializer("start_time") 52 | def serialize_dt(self, start_time: Optional[datetime], _info): 53 | if start_time: 54 | return start_time.replace(tzinfo=timezone.utc) 55 | return None 56 | 57 | 58 | class AskLogAggregation(BaseModel): 59 | id: Optional[AskLogAggregationID] = Field(None, alias="_id") # 起始时间 60 | count: int # 时间间隔内的请求数量 61 | user_ids: list[Optional[int]] = None # 用户ID列表 62 | total_queueing_time: Optional[float] = None 63 | total_ask_time: Optional[float] = None 64 | -------------------------------------------------------------------------------- /backend/api/sources/__init__.py: -------------------------------------------------------------------------------- 1 | from .openai_web import * 2 | from .openai_api import * 3 | -------------------------------------------------------------------------------- /backend/config_templates/config.yaml: -------------------------------------------------------------------------------- 1 | openai_web: 2 | enabled: true 3 | is_plus_account: true 4 | enable_team_subscription: false 5 | team_account_id: 6 | chatgpt_base_url: 7 | proxy: 8 | wss_proxy: 9 | enable_arkose_endpoint: false 10 | arkose_endpoint_base: 11 | common_timeout: 20 12 | ask_timeout: 600 13 | sync_conversations_on_startup: false 14 | sync_conversations_schedule: false 15 | sync_conversations_schedule_interval_hours: 12 16 | enabled_models: 17 | - gpt_3_5 18 | - gpt_4 19 | - gpt_4o 20 | - o1 21 | - o1_mini 22 | model_code_mapping: 23 | gpt_3_5: gpt-4o-mini 24 | gpt_4: gpt-4 25 | gpt_4o: gpt-4o 26 | o1: o1 27 | o1_mini: o1-mini 28 | file_upload_strategy: browser_upload_only 29 | max_completion_concurrency: 1 30 | disable_uploading: false 31 | openai_api: 32 | enabled: true 33 | openai_base_url: https://api.openai.com/v1/ 34 | proxy: 35 | connect_timeout: 10 36 | read_timeout: 20 37 | enabled_models: 38 | - gpt_3_5 39 | - gpt_4 40 | - gpt_4o 41 | - o1 42 | - o1_mini 43 | model_code_mapping: 44 | gpt_3_5: gpt-4o-mini 45 | gpt_4: gpt-4 46 | gpt_4o: gpt-4o 47 | o1: o1 48 | o1_mini: o1-mini 49 | common: 50 | print_sql: false 51 | print_traceback: true 52 | create_initial_admin_user: true 53 | initial_admin_user_username: admin 54 | initial_admin_user_password: password 55 | http: 56 | host: 127.0.0.1 57 | port: 8000 58 | cors_allow_origins: 59 | - http://localhost:8000 60 | - http://localhost:5173 61 | - http://127.0.0.1:8000 62 | - http://127.0.0.1:5173 63 | data: 64 | data_dir: ./data 65 | database_url: sqlite+aiosqlite:///data/database.db 66 | mongodb_url: mongodb://cws:password@mongo:27017 67 | mongodb_db_name: cws 68 | run_migration: true 69 | max_file_upload_size: 104857600 70 | auth: 71 | jwt_secret: MODIFY_THIS_TO_RANDOM_SECURE_STRING 72 | jwt_lifetime_seconds: 259200 73 | cookie_max_age: 259200 74 | user_secret: MODIFY_THIS_TO_ANOTHER_RANDOM_SECURE_STRING 75 | stats: 76 | ask_stats_ttl: 7776000 77 | request_stats_ttl: 2592000 78 | request_stats_filter_keywords: 79 | - /status 80 | log: 81 | console_log_level: INFO 82 | -------------------------------------------------------------------------------- /backend/config_templates/credentials.yaml: -------------------------------------------------------------------------------- 1 | openai_web_access_token: 2 | openai_api_key: 3 | -------------------------------------------------------------------------------- /backend/logging_config.yaml: -------------------------------------------------------------------------------- 1 | version: 1 2 | formatters: 3 | simple: 4 | format: "%(asctime)s.%(msecs)03d %(levelname)8s: [%(name)s]\t%(message)s" 5 | datefmt: '%Y/%m/%d %H:%M:%S' 6 | proxy-output: 7 | format: "%(message)s" 8 | datefmt: '%Y/%m/%d %H:%M:%S' 9 | colored: 10 | (): colorlog.ColoredFormatter 11 | format: "%(asctime)s.%(msecs)03d %(log_color)s%(levelname)8s%(reset)s: %(cyan)s[%(name)s]%(reset)s %(message)s" 12 | datefmt: '%Y/%m/%d %H:%M:%S' 13 | handlers: 14 | file_handler: 15 | class: logging.handlers.RotatingFileHandler 16 | formatter: simple 17 | encoding: utf-8 18 | level: DEBUG 19 | filename: cws.log 20 | maxBytes: 10485760 # 10MB 21 | console_handler: 22 | class: logging.StreamHandler 23 | formatter: colored 24 | level: DEBUG 25 | 26 | root: 27 | level: DEBUG 28 | handlers: [] 29 | loggers: 30 | uvicorn.error: 31 | level: INFO 32 | handlers: [console_handler, file_handler] 33 | uvicorn.access: 34 | level: INFO 35 | handlers: [] # disable uvicorn access log 36 | cws: 37 | level: DEBUG 38 | handlers: [console_handler, file_handler] 39 | sqlalchemy: 40 | level: WARN 41 | handlers: [console_handler, file_handler] 42 | alembic: 43 | level: INFO 44 | handlers: [console_handler, file_handler] 45 | -------------------------------------------------------------------------------- /backend/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "chatgpt-share-backend" 3 | version = "0.0.1" 4 | description = "" 5 | authors = ["moeakwak "] 6 | readme = "README.md" 7 | packages = [] 8 | 9 | [tool.poetry.dependencies] 10 | bcrypt = "4.0.1" 11 | python = "^3.10" 12 | fastapi = "^0.109.0" 13 | uvicorn = "^0.27.0.post1" 14 | aiosqlite = "^0.19.0" 15 | sqlalchemy = "^2.0.21" 16 | fastapi-users = "^12.1.2" 17 | fastapi-users-db-sqlalchemy = "^6.0.1" 18 | #revchatgpt = "5.0.0" 19 | greenlet = "^3.0.1" 20 | websockets = "^12.0" 21 | setuptools = "^69.0.2" 22 | python-dateutil = "^2.8.2" 23 | pyyaml = "^6.0.1" 24 | alembic = "^1.12.0" 25 | colorlog = "^6.7.0" 26 | asgiref = "^3.6.0" 27 | aiocron = "^1.8" 28 | ruamel-yaml = "^0.18.5" 29 | beanie = "^1.22.6" 30 | httpx = "^0.26.0" 31 | strenum = "^0.4.15" 32 | pydantic = "^2.5.2" 33 | aiofiles = "^23.2.1" 34 | aiohttp = "^3.8.5" 35 | fastapi-cache2 = "^0.2.1" 36 | pillow = "^10.1.0" 37 | 38 | 39 | 40 | [build-system] 41 | requires = ["poetry-core"] 42 | build-backend = "poetry.core.masonry.api" 43 | -------------------------------------------------------------------------------- /backend/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxduke/chatgpt-web-share/2d52c390be890c0c94203ef95fb72ba3e83add0b/backend/utils/__init__.py -------------------------------------------------------------------------------- /backend/utils/admin/__init__.py: -------------------------------------------------------------------------------- 1 | from .sync_conv import sync_conversations 2 | -------------------------------------------------------------------------------- /backend/utils/common.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import re 3 | import threading 4 | from threading import Lock 5 | from typing import Type, TypeVar 6 | 7 | T = TypeVar("T") 8 | 9 | 10 | class SingletonMeta(type): 11 | _instances = {} 12 | _lock = Lock() 13 | 14 | def __call__(cls, *args, **kwargs): 15 | with cls._lock: 16 | if cls not in cls._instances: 17 | instance = super().__call__(*args, **kwargs) 18 | cls._instances[cls] = instance 19 | return cls._instances[cls] 20 | 21 | 22 | def async_wrap_iter(it): 23 | """Wrap blocking iterator into an asynchronous one""" 24 | loop = asyncio.get_event_loop() 25 | q = asyncio.Queue(1) 26 | exception = None 27 | _END = object() 28 | 29 | async def yield_queue_items(): 30 | while True: 31 | next_item = await q.get() 32 | if next_item is _END: 33 | break 34 | yield next_item 35 | if exception is not None: 36 | # the iterator has raised, propagate the exception 37 | raise exception 38 | 39 | def iter_to_queue(): 40 | nonlocal exception 41 | try: 42 | for item in it: 43 | # This runs outside the event loop thread, so we 44 | # must use thread-safe API to talk to the queue. 45 | asyncio.run_coroutine_threadsafe(q.put(item), loop).result() 46 | except Exception as e: 47 | exception = e 48 | finally: 49 | asyncio.run_coroutine_threadsafe(q.put(_END), loop).result() 50 | 51 | threading.Thread(target=iter_to_queue).start() 52 | return yield_queue_items() 53 | 54 | 55 | def desensitize(text): 56 | email_regex = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b' 57 | url_regex = r'(http[s]?://[A-Za-z0-9]{2})[A-Za-z0-9./?=%&_-]*' 58 | 59 | def replace_email(match): 60 | email = match.group(0) 61 | name, domain = email.split('@') 62 | masked_email = f'{name[0]}***@*.{domain.split(".")[1]}' 63 | return masked_email 64 | 65 | def replace_url(match): 66 | url = match.group(1) 67 | return url + '***' 68 | 69 | text = re.sub(email_regex, replace_email, text) 70 | text = re.sub(url_regex, replace_url, text) 71 | 72 | return text 73 | -------------------------------------------------------------------------------- /backend/utils/logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import logging.config 3 | import os 4 | from datetime import datetime 5 | import traceback 6 | import yaml 7 | 8 | from api.conf import Config 9 | import api.globals as g 10 | 11 | 12 | def get_log_config(): 13 | with open('logging_config.yaml', 'r') as f: 14 | log_config = yaml.safe_load(f.read()) 15 | log_config['handlers']['file_handler']['filename'] = g.server_log_filename 16 | log_config['handlers']['console_handler']['level'] = Config().log.console_log_level 17 | return log_config 18 | 19 | 20 | def setup_logger(): 21 | log_dir = os.path.join(Config().data.data_dir, 'logs') 22 | os.makedirs(log_dir, exist_ok=True) 23 | g.server_log_filename = os.path.join(log_dir, f"{datetime.now().strftime('%Y%m%d_%H-%M-%S')}.log") 24 | log_config = get_log_config() 25 | logging.config.dictConfig(log_config) 26 | 27 | 28 | def get_logger(name): 29 | return logging.getLogger(f"cws.{name}") 30 | 31 | 32 | def with_traceback(e: Exception): 33 | if Config().common.print_traceback: 34 | tb = traceback.extract_tb(e.__traceback__) 35 | last_frames = tb[-10:] 36 | formatted_traceback = ["traceback:"] 37 | for frame in last_frames: 38 | frame_info = f"{frame.filename}:{frame.lineno} in {frame.name}" 39 | formatted_traceback.append(frame_info) 40 | formatted_traceback = "\n".join(formatted_traceback) 41 | return f"<{e.__class__.__name__}> {str(e)}\n{formatted_traceback}" 42 | else: 43 | return str(e) 44 | -------------------------------------------------------------------------------- /docs/donate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxduke/chatgpt-web-share/2d52c390be890c0c94203ef95fb72ba3e83add0b/docs/donate.png -------------------------------------------------------------------------------- /docs/screenshot.en.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxduke/chatgpt-web-share/2d52c390be890c0c94203ef95fb72ba3e83add0b/docs/screenshot.en.jpeg -------------------------------------------------------------------------------- /docs/screenshot.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxduke/chatgpt-web-share/2d52c390be890c0c94203ef95fb72ba3e83add0b/docs/screenshot.jpeg -------------------------------------------------------------------------------- /docs/screenshot_admin.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxduke/chatgpt-web-share/2d52c390be890c0c94203ef95fb72ba3e83add0b/docs/screenshot_admin.jpeg -------------------------------------------------------------------------------- /frontend/.env: -------------------------------------------------------------------------------- 1 | VITE_API_WEBSOCKET_PROTOCOL=auto 2 | VITE_DISABLE_SENTRY=no -------------------------------------------------------------------------------- /frontend/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .eslintrc.cjs 3 | src/types/openapi.* -------------------------------------------------------------------------------- /frontend/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | }, 6 | overrides: [], 7 | parser: 'vue-eslint-parser', 8 | extends: [ 9 | 'plugin:vue/base', 10 | 'eslint:recommended', 11 | 'plugin:vue/vue3-recommended', 12 | 'plugin:vue/essential', 13 | 'plugin:@typescript-eslint/recommended', 14 | 'plugin:import/recommended', 15 | 'plugin:import/typescript', 16 | // "plugin:prettier/recommended", 17 | // "eslint-config-prettier", 18 | ], 19 | parserOptions: { 20 | ecmaVersion: 'latest', 21 | sourceType: 'module', 22 | parser: '@typescript-eslint/parser', 23 | }, 24 | plugins: ['vue', '@typescript-eslint', 'import', 'simple-import-sort'], 25 | rules: { 26 | indent: ['warn', 2], 27 | 'linebreak-style': ['error', 'unix'], 28 | quotes: ['warn', 'single'], 29 | semi: ['warn', 'always'], 30 | 'vue/no-v-model-argument': ['off'], 31 | 'vue/no-multiple-template-root': ['off'], 32 | 'vue/multi-word-component-names': ['off'], 33 | '@typescript-eslint/no-explicit-any': ['off'], 34 | 'simple-import-sort/imports': 'error', 35 | 'simple-import-sort/exports': 'error', 36 | '@typescript-eslint/no-unused-vars': [ 37 | 'warn', 38 | { 39 | argsIgnorePattern: '^_', 40 | }, 41 | ], 42 | "vue/max-attributes-per-line": ["error", { 43 | "singleline": { 44 | "max": 5 45 | }, 46 | "multiline": { 47 | "max": 1 48 | } 49 | }], 50 | "@typescript-eslint/no-non-null-assertion": ["off"], 51 | "import/no-named-as-default": ["off"] 52 | }, 53 | settings: { 54 | 'import/parsers': { 55 | '@typescript-eslint/parser': ['.ts', '.tsx'], 56 | }, 57 | 'import/resolver': { 58 | typescript: { 59 | alwaysTryTypes: true, // always try to resolve types under `@types` directory even it doesn't contain any source code, like `@types/unist` 60 | project: ['tsconfig.json', 'tsconfig.node.json'], 61 | }, 62 | }, 63 | }, 64 | }; 65 | -------------------------------------------------------------------------------- /frontend/.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 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .idea 17 | .DS_Store 18 | *.suo 19 | *.ntvs* 20 | *.njsproj 21 | *.sln 22 | *.sw? 23 | 24 | .eslintcache 25 | .DS_Store -------------------------------------------------------------------------------- /frontend/.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // 一行最多多少个字符 3 | printWidth: 120, 4 | // 指定每个缩进级别的空格数 5 | tabWidth: 2, 6 | // 使用制表符而不是空格缩进行 7 | useTabs: false, 8 | // 在语句末尾是否需要分号 9 | semi: true, 10 | // 是否使用单引号 11 | singleQuote: true, 12 | // 更改引用对象属性的时间 可选值"" 13 | quoteProps: 'as-needed', 14 | // 在JSX中使用单引号而不是双引号 15 | jsxSingleQuote: false, 16 | // 多行时尽可能打印尾随逗号。(例如,单行数组永远不会出现逗号结尾。) 可选值"",默认none 17 | trailingComma: 'es5', 18 | // 在对象文字中的括号之间打印空格 19 | bracketSpacing: true, 20 | // jsx 标签的反尖括号需要换行 21 | jsxBracketSameLine: false, 22 | // 在单独的箭头函数参数周围包括括号 always:(x) => x \ avoid:x => x 23 | arrowParens: 'always', 24 | // 这两个选项可用于格式化以给定字符偏移量(分别包括和不包括)开始和结束的代码 25 | rangeStart: 0, 26 | rangeEnd: Infinity, 27 | // 指定要使用的解析器,不需要写文件开头的 @prettier 28 | requirePragma: false, 29 | // 不需要自动在文件开头插入 @prettier 30 | insertPragma: false, 31 | // 使用默认的折行标准 always\never\preserve 32 | proseWrap: 'preserve', 33 | // 指定HTML文件的全局空格敏感度 css\strict\ignore 34 | htmlWhitespaceSensitivity: 'css', 35 | // Vue文件脚本和样式标签缩进 36 | vueIndentScriptAndStyle: false, 37 | // 换行符使用 lf 结尾是 可选值"" 38 | endOfLine: 'lf', 39 | }; 40 | -------------------------------------------------------------------------------- /frontend/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"] 3 | } 4 | -------------------------------------------------------------------------------- /frontend/.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": "chrome", 9 | "request": "launch", 10 | "name": "针对 localhost 启动 Chrome", 11 | "url": "http://localhost:5173", 12 | "webRoot": "${workspaceFolder}" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /frontend/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "i18n-ally.localesPaths": ["src/locales"], 3 | "i18n-ally.sourceLanguage": "zh-CN", 4 | "i18n-ally.displayLanguage": "zh-CN", 5 | "i18n-ally.keystyle": "nested", 6 | "i18n-ally.namespace": true, 7 | "i18n-ally.pathMatcher": "{locale}.json", 8 | "[html]": { 9 | "editor.tabSize": 2, 10 | "editor.defaultFormatter": "esbenp.prettier-vscode" 11 | }, 12 | "[javascript]": { 13 | "editor.tabSize": 2, 14 | "editor.defaultFormatter": "esbenp.prettier-vscode" 15 | }, 16 | "[typescript]": { 17 | "editor.tabSize": 2, 18 | "editor.defaultFormatter": "esbenp.prettier-vscode" 19 | }, 20 | "[vue]": { 21 | "editor.tabSize": 2, 22 | "editor.defaultFormatter": "esbenp.prettier-vscode" 23 | }, 24 | "prettier.useEditorConfig": false, 25 | "prettier.configPath": ".prettierrc.cjs", 26 | "editor.formatOnSave": false, 27 | "editor.codeActionsOnSave": { 28 | "source.fixAll.eslint": "explicit" 29 | }, 30 | "files.refactoring.autoSave": false 31 | } 32 | -------------------------------------------------------------------------------- /frontend/config/utils/icon-component-resolver.ts: -------------------------------------------------------------------------------- 1 | // modified from https://github.com/07akioni/xicons/issues/364#issuecomment-1118129894 2 | 3 | import { readdirSync } from 'fs'; 4 | import { resolveModule } from 'local-pkg'; 5 | import { dirname } from 'path'; 6 | import type { ComponentResolver } from 'unplugin-vue-components/types'; 7 | 8 | let _cache: Map; 9 | 10 | export interface IconResolverOptions { 11 | pkgs: string[], 12 | prefix?: string 13 | } 14 | 15 | export function IconComponentResolver(options: IconResolverOptions): ComponentResolver { 16 | if (!_cache) { 17 | _cache = new Map(); 18 | for (const pkg of options.pkgs) { 19 | try { 20 | const icon_path = resolveModule(pkg) as string; 21 | const icons = readdirSync(dirname(icon_path), { withFileTypes: true }) 22 | .filter(item => !item.isDirectory() && item.name.match(/^[A-Z][A-Za-z0-9]+\.js$/)) 23 | .map(item => item.name.replace(/\.js$/, '')); 24 | 25 | for (const icon of icons) { 26 | if (!_cache.has(icon)) { 27 | _cache.set(icon, pkg); 28 | } 29 | } 30 | 31 | console.log(`[unplugin-vue-components] loaded ${icons.length} icons from "${pkg}"`); 32 | } catch (error) { 33 | console.error(error); 34 | throw new Error(`[unplugin-vue-components] failed to load "${pkg}", have you installed it?`); 35 | } 36 | } 37 | } 38 | 39 | return { 40 | type: 'component', 41 | resolve: (name: string) => { 42 | if (options.prefix && name.startsWith(options.prefix)) { 43 | name = name.substring(options.prefix.length); 44 | } 45 | 46 | if (_cache.has(name)) { 47 | return { 48 | name: name, 49 | from: _cache.get(name), 50 | }; 51 | } 52 | }, 53 | }; 54 | } 55 | -------------------------------------------------------------------------------- /frontend/config/vite.config.base.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from 'node:url'; 2 | 3 | import presetUno from '@unocss/preset-uno'; 4 | import UnoCSS from '@unocss/vite'; 5 | import vue from '@vitejs/plugin-vue'; 6 | import { join } from 'path'; 7 | import { transformerDirectives } from 'unocss'; 8 | import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'; 9 | import Components from 'unplugin-vue-components/vite'; 10 | import { defineConfig } from 'vite'; 11 | import svgLoader from 'vite-svg-loader'; 12 | 13 | // import { IconComponentResolver } from './utils/icon-component-resolver'; 14 | 15 | // https://vitejs.dev/config/ 16 | export default defineConfig({ 17 | base: process.env.VITE_BASE || '/', 18 | plugins: [ 19 | vue(), 20 | svgLoader(), 21 | UnoCSS({ 22 | presets: [presetUno()], 23 | transformers: [transformerDirectives()], 24 | }), 25 | Components({ 26 | resolvers: [ 27 | NaiveUiResolver(), 28 | // IconComponentResolver({ pkgs: ['@vicons/ionicons4', '@vicons/ionicons5', '@vicons/material'], prefix: 'X' }), 29 | ], 30 | }), 31 | ], 32 | resolve: { 33 | alias: { 34 | '@': fileURLToPath(new URL('../src', import.meta.url)), 35 | }, 36 | }, 37 | define: { 38 | 'import.meta.env.PACKAGE_VERSION': JSON.stringify(process.env.npm_package_version), 39 | 'import.meta.env.VITE_ENABLE_SENTRY': process.env.VITE_ENABLE_SENTRY || '\'no\'', 40 | 'import.meta.env.VITE_ROUTER_BASE': process.env.VITE_ROUTER_BASE || '\'/\'', 41 | 'import.meta.env.VITE_API_BASE_URL': process.env.VITE_API_BASE_URL || '\'/api/\'', 42 | }, 43 | }); 44 | -------------------------------------------------------------------------------- /frontend/config/vite.config.dev.ts: -------------------------------------------------------------------------------- 1 | import { mergeConfig } from 'vite'; 2 | 3 | import baseConfig from './vite.config.base'; 4 | 5 | export default mergeConfig( 6 | { 7 | mode: 'development', 8 | server: { 9 | host: '0.0.0.0', 10 | port: 5173, 11 | fs: { 12 | strict: true, 13 | }, 14 | proxy: { 15 | '/api': { 16 | target: 'http://127.0.0.1:8000', 17 | changeOrigin: false, 18 | ws: true, 19 | rewrite: (path) => path.replace(/^\/api/, ''), 20 | }, 21 | }, 22 | }, 23 | }, 24 | baseConfig 25 | ); 26 | -------------------------------------------------------------------------------- /frontend/config/vite.config.prod.ts: -------------------------------------------------------------------------------- 1 | import { mergeConfig } from 'vite'; 2 | 3 | import baseConfig from './vite.config.base'; 4 | 5 | export default mergeConfig( 6 | { 7 | mode: 'production', 8 | plugins: [], 9 | build: { 10 | sourcemap: process.env.VITE_DISABLE_SOURCEMAP !== 'yes', 11 | rollupOptions: { 12 | output: { 13 | manualChunks: { 14 | naive_ui: ['naive-ui'], 15 | vue: ['vue', 'vue-router', 'pinia', 'vue-i18n'], 16 | }, 17 | }, 18 | }, 19 | chunkSizeWarningLimit: 2000, 20 | }, 21 | }, 22 | baseConfig 23 | ); 24 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | CWS Internal 9 | 10 | 11 |
12 | 13 | 14 | 15 | 22 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatgpt-share-frontend", 3 | "private": true, 4 | "version": "0.4.9", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite --config ./config/vite.config.dev.ts", 8 | "build": "vue-tsc && vite build --config ./config/vite.config.prod.ts", 9 | "preview": "pnpm run build && vite preview --host", 10 | "lint": "eslint src/ --ext .ts,.tsx,.js,.jsx,.vue", 11 | "lint-fix": "eslint src/ --ext .ts,.tsx,.js,.jsx,.vue --fix --cache", 12 | "prettier": "prettier --write src" 13 | }, 14 | "dependencies": { 15 | "@lljj/vue3-form-naive": "^1.19.1", 16 | "@sentry/tracing": "^7.104.0", 17 | "@sentry/vue": "^7.104.0", 18 | "@traptitech/markdown-it-katex": "^3.6.0", 19 | "@vueuse/core": "^10.9.0", 20 | "axios": "^1.6.7", 21 | "clipboard-polyfill": "^4.0.2", 22 | "dompurify": "^3.0.9", 23 | "echarts": "^5.5.0", 24 | "file-saver": "^2.0.5", 25 | "highlight.js": "^11.9.0", 26 | "katex": "^0.16.9", 27 | "markdown-it": "^14.0.0", 28 | "markdown-it-highlightjs": "^4.0.1", 29 | "pinia": "^2.1.7", 30 | "uuid": "^9.0.1", 31 | "vue": "^3.4.21", 32 | "vue-echarts": "^6.6.9", 33 | "vue-i18n": "^9.10.1", 34 | "vue-router": "^4.3.0", 35 | "vue3-json-viewer": "^2.2.2" 36 | }, 37 | "devDependencies": { 38 | "@apidevtools/json-schema-ref-parser": "^11.1.0", 39 | "@types/dompurify": "^3.0.5", 40 | "@types/file-saver": "^2.0.7", 41 | "@types/katex": "^0.16.7", 42 | "@types/markdown-it": "^13.0.7", 43 | "@types/node": "^20.11.24", 44 | "@types/uuid": "^9.0.8", 45 | "@typescript-eslint/eslint-plugin": "^7.1.0", 46 | "@typescript-eslint/parser": "^7.1.0", 47 | "@unocss/preset-uno": "^0.58.5", 48 | "@unocss/transformer-directives": "^0.58.5", 49 | "@unocss/vite": "^0.58.5", 50 | "@vicons/ionicons4": "^0.12.0", 51 | "@vicons/ionicons5": "^0.12.0", 52 | "@vicons/material": "^0.12.0", 53 | "@vitejs/plugin-vue": "^5.0.4", 54 | "eslint": "^8.57.0", 55 | "eslint-config-prettier": "^9.1.0", 56 | "eslint-import-resolver-typescript": "^3.6.1", 57 | "eslint-plugin-import": "^2.29.1", 58 | "eslint-plugin-prettier": "^5.1.3", 59 | "eslint-plugin-simple-import-sort": "^12.0.0", 60 | "eslint-plugin-vue": "^9.22.0", 61 | "local-pkg": "^0.5.0", 62 | "naive-ui": "^2.38.1", 63 | "openapi-typescript": "^6.7.4", 64 | "prettier": "^3.2.5", 65 | "typescript": "~5.3.3", 66 | "unocss": "^0.58.5", 67 | "unplugin-vue-components": "^0.26.0", 68 | "vfonts": "^0.0.3", 69 | "vite": "^5.1.4", 70 | "vite-plugin-eslint": "^1.8.1", 71 | "vite-svg-loader": "^5.1.0", 72 | "vue-tsc": "^1.8.27" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /frontend/public/chatgpt-icon-black.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/public/chatgpt-icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxduke/chatgpt-web-share/2d52c390be890c0c94203ef95fb72ba3e83add0b/frontend/public/icon.png -------------------------------------------------------------------------------- /frontend/scripts/dereference_openapi.js: -------------------------------------------------------------------------------- 1 | import { dereference } from '@apidevtools/json-schema-ref-parser'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | 5 | const currentModulePath = new URL(import.meta.url).pathname; 6 | const currentDirectory = path.dirname(currentModulePath); 7 | 8 | const sourceFilePath = path.join(currentDirectory, '../src/types/json/openapi.json'); 9 | const targetFilePath = path.join(currentDirectory, '../src/types/json/schemas.json'); 10 | 11 | dereference(sourceFilePath) 12 | .then((dereferencedSchema) => { 13 | const dereferencedSchemaString = JSON.stringify(dereferencedSchema.components.schemas, null, 2); 14 | 15 | fs.writeFileSync(targetFilePath, dereferencedSchemaString, 'utf8'); 16 | 17 | console.log('dereference success!'); 18 | }) 19 | .catch((error) => { 20 | console.error('error:', error); 21 | }); 22 | -------------------------------------------------------------------------------- /frontend/scripts/updateapi.sh: -------------------------------------------------------------------------------- 1 | wget http://127.0.0.1:8000/openapi.json -O src/types/json/openapi.json; 2 | pnpm dlx openapi-typescript src/types/json/openapi.json --default-non-nullable --output src/types/openapi.ts; 3 | 4 | cd ../backend; 5 | python manage.py get_config_schema > ../frontend/src/types/json/config_schema.json; 6 | python manage.py get_credentials_schema > ../frontend/src/types/json/credentials_schema.json; 7 | python manage.py get_model_definitions > ../frontend/src/types/json/model_definitions.json; 8 | python manage.py create_config; 9 | 10 | cd ../frontend 11 | node scripts/dereference_openapi.js 12 | echo "Updated API schemas." 13 | -------------------------------------------------------------------------------- /frontend/src/App.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 48 | -------------------------------------------------------------------------------- /frontend/src/api/arkose.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | import ApiUrl from './url'; 4 | 5 | export function getArkoseInfo() { 6 | return axios.get<{ enabled: boolean, url: string }>(ApiUrl.ArkoseInfo); 7 | } 8 | -------------------------------------------------------------------------------- /frontend/src/api/chat.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | import { OpenaiChatPlugin, OpenaiChatPluginListResponse, OpenaiChatPluginUserSettings } from '@/types/schema'; 4 | 5 | import ApiUrl from './url'; 6 | 7 | export type AskInfo = { 8 | message: string; 9 | new_title?: string; 10 | conversation_id?: string; 11 | parent_id?: string; 12 | model_name?: string; 13 | timeout?: number; 14 | }; 15 | 16 | export function getAskWebsocketApiUrl() { 17 | const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws'; 18 | const url = `${protocol}://${window.location.host}${import.meta.env.VITE_API_BASE_URL}chat`; 19 | // console.log('getAskWebsocketApiUrl', url); 20 | return url; 21 | } 22 | 23 | export function getOpenaiChatPluginsApi(offset: number, limit: number, category?: string, search?: string) { 24 | return axios.get(ApiUrl.ChatPlugins, { 25 | params: { offset, limit, category, search }, 26 | }); 27 | } 28 | 29 | export function getInstalledOpenaiChatPluginApi(pluginId: string) { 30 | return axios.get(`${ApiUrl.InstalledChatPlugins}/${pluginId}`); 31 | } 32 | 33 | export function getInstalledOpenaiChatPluginsApi() { 34 | return axios.get(ApiUrl.InstalledChatPlugins); 35 | } 36 | 37 | export function patchOpenaiChatPluginsUsersSettingsApi(pluginId: string, setting: OpenaiChatPluginUserSettings) { 38 | return axios.patch(`${ApiUrl.ChatPlugins}/${pluginId}/user-settings`, setting, { 39 | params: { 40 | plugin_id: pluginId, 41 | }, 42 | }); 43 | } 44 | 45 | 46 | -------------------------------------------------------------------------------- /frontend/src/api/conv.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | import { BaseConversationHistory, BaseConversationSchema, OpenaiChatInterpreterInfo } from '@/types/schema'; 4 | 5 | import ApiUrl from './url'; 6 | 7 | export function getAllConversationsApi() { 8 | return axios.get>(ApiUrl.Conversation); 9 | } 10 | 11 | export function getAdminAllConversationsApi(valid_only = false) { 12 | return axios.get>(ApiUrl.AllConversation, { 13 | params: { valid_only }, 14 | }); 15 | } 16 | 17 | export function getConversationHistoryApi(conversation_id: string) { 18 | return axios.get(ApiUrl.Conversation + '/' + conversation_id); 19 | } 20 | 21 | export function getConversationHistoryFromCacheApi(conversation_id: string) { 22 | return axios.get(`${ApiUrl.Conversation}/${conversation_id}/cache`); 23 | } 24 | 25 | export function deleteConversationApi(conversation_id: string) { 26 | return axios.delete(ApiUrl.Conversation + '/' + conversation_id); 27 | } 28 | 29 | export function clearAllConversationApi() { 30 | return axios.delete(ApiUrl.Conversation); 31 | } 32 | 33 | export function vanishConversationApi(conversation_id: string) { 34 | return axios.delete(ApiUrl.Conversation + '/' + conversation_id + '/vanish'); 35 | } 36 | 37 | export function setConversationTitleApi(conversation_id: string, title: string) { 38 | return axios.patch(ApiUrl.Conversation + '/' + conversation_id, null, { 39 | params: { title }, 40 | }); 41 | } 42 | 43 | export function generateConversationTitleApi(conversation_id: string, message_id: string) { 44 | return axios.patch(ApiUrl.Conversation + '/' + conversation_id + '/gen_title', null, { 45 | params: { message_id }, 46 | }); 47 | } 48 | 49 | export function assignConversationToUserApi(conversation_id: string, username: string) { 50 | return axios.patch(`${ApiUrl.Conversation}/${conversation_id}/assign/${username}`); 51 | } 52 | 53 | export function getFileDownloadUrlApi(file_id: string) { 54 | return axios.get(`/files/${file_id}/download-url`); 55 | } 56 | 57 | export function getInterpreterInfoApi(conversation_id: string) { 58 | return axios.get(`${ApiUrl.Conversation}/${conversation_id}/interpreter`); 59 | } 60 | 61 | export function getInterpreterSandboxFileDownloadUrlApi( 62 | conversation_id: string, 63 | message_id: string, 64 | sandbox_path: string 65 | ) { 66 | return axios.get(`${ApiUrl.Conversation}/${conversation_id}/interpreter/download-url`, { 67 | params: { message_id, sandbox_path }, 68 | }); 69 | } 70 | -------------------------------------------------------------------------------- /frontend/src/api/files.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | import { OpenaiChatFileUploadUrlRequest, StartUploadRequestSchema, StartUploadResponseSchema, UploadedFileInfoSchema } from '@/types/schema'; 4 | 5 | import ApiUrl from './url'; 6 | 7 | export function uploadFileToLocalApi(file: File) { 8 | const formData = new FormData(); 9 | formData.append('file', file); 10 | return axios.post(ApiUrl.FilesLocalUpload, formData, { 11 | headers: { 12 | 'Content-Type': 'multipart/form-data', 13 | }, 14 | }); 15 | } 16 | 17 | export function getLocalFileDownloadUrl(fileId: string) { 18 | return `${ApiUrl.FilesLocalDownload}/${fileId}`; 19 | } 20 | 21 | export function startUploadFileToOpenaiWeb(uploadRequest: StartUploadRequestSchema) { 22 | return axios.post(ApiUrl.FilesOpenaiWebUploadStart, uploadRequest); 23 | } 24 | 25 | export function completeUploadFileToOpenaiWeb(uploadId: string) { 26 | return axios.post(`${ApiUrl.FilesOpenaiWebUploadComplete}/${uploadId}`); 27 | } 28 | 29 | export function requestUploadFileFromLocalToOpenaiWeb(fileId: string) { 30 | return axios.post(`${ApiUrl.FilesLocalUploadToOpenaiWeb}/${fileId}`); 31 | } 32 | 33 | // export async function uploadFileToAzureBlob(file: File, signedUrl: string): Promise { 34 | // const headers = new Headers({ 35 | // 'x-ms-blob-type': 'BlockBlob', 36 | // 'x-ms-version': '2020-04-08', 37 | // 'Content-Type': file.type || 'application/octet-stream', 38 | // }); 39 | 40 | // const response = await fetch(signedUrl, { 41 | // method: 'PUT', 42 | // headers: headers, 43 | // body: file, 44 | // }); 45 | 46 | // return response; 47 | // } 48 | 49 | export async function uploadFileToAzureBlob( 50 | file: File, 51 | signedUrl: string, 52 | onProgress: (e: { percent: number }) => void 53 | ): Promise { 54 | return new Promise((resolve, reject) => { 55 | const xhr = new XMLHttpRequest(); 56 | 57 | xhr.upload.addEventListener('progress', (event) => { 58 | if (event.lengthComputable) { 59 | const percent = Math.round((event.loaded / event.total) * 100); 60 | onProgress({ percent }); 61 | } 62 | }); 63 | 64 | xhr.addEventListener('load', () => { 65 | if (xhr.status === 201) { 66 | console.log('File uploaded successfully to azure', File); 67 | resolve(); 68 | } else { 69 | console.error(`Failed to upload file to azure: ${xhr.status} ${xhr.statusText}`); 70 | reject(new Error(`${xhr.status} ${xhr.statusText}`)); 71 | } 72 | }); 73 | 74 | xhr.addEventListener('error', () => { 75 | console.error('An error occurred while uploading the file to azure', file); 76 | reject(new Error('An error occurred while uploading the file to azure')); 77 | }); 78 | 79 | xhr.open('PUT', signedUrl, true); 80 | xhr.setRequestHeader('x-ms-blob-type', 'BlockBlob'); 81 | xhr.setRequestHeader('x-ms-version', '2020-04-08'); 82 | xhr.setRequestHeader('Content-Type', file.type || 'application/octet-stream'); 83 | 84 | xhr.send(file); 85 | }); 86 | } 87 | -------------------------------------------------------------------------------- /frontend/src/api/interceptor.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosResponse, InternalAxiosRequestConfig } from 'axios'; 2 | import axios from 'axios'; 3 | 4 | import { i18n } from '@/i18n'; 5 | import router from '@/router'; 6 | import { useUserStore } from '@/store'; 7 | import { Dialog, Message } from '@/utils/tips'; 8 | 9 | // import { isLogin } from '@/utils/auth'; 10 | import ApiUrl from './url'; 11 | const t = i18n.global.t as any; 12 | 13 | export interface HttpResponse { 14 | code: number; 15 | message: string; 16 | result: T; 17 | } 18 | 19 | axios.defaults.baseURL = import.meta.env.VITE_API_BASE_URL; 20 | // axios.defaults.baseURL = "/openai_api/"; 21 | 22 | axios.interceptors.request.use( 23 | (config: InternalAxiosRequestConfig) => { 24 | // if (token) { 25 | // if (!config.headers) { 26 | // config.headers = {}; 27 | // } 28 | // config.headers.Authorization = `Bearer ${token}`; 29 | // } 30 | return config; 31 | }, 32 | (error) => { 33 | return Promise.reject(error); 34 | } 35 | ); 36 | 37 | /** 38 | * 添加响应拦截器 39 | * 这里将 { code, message, result } 解构出来,response.data 替换成 result 40 | */ 41 | const successCode = [200, 201, 204]; 42 | axios.interceptors.response.use( 43 | (response: AxiosResponse) => { 44 | const res = response.data; 45 | if (!successCode.includes(res.code)) { 46 | console.warn('Error: ', res); 47 | let msg = `${res.code}`; 48 | if (res.message) { 49 | msg += ` ${t(res.message)}`; 50 | } 51 | if (res.result) { 52 | msg += `: ${t(res.result)}`; 53 | } 54 | Message.error(msg, { duration: 3 * 1000 }); 55 | if ( 56 | [10401].includes(res.code) && 57 | !([ApiUrl.Login, ApiUrl.Logout] as Array).includes(response.config.url || '') 58 | ) { 59 | Dialog.error({ 60 | title: t('errors.loginExpired') as string, 61 | content: t('tips.loginExpired'), 62 | positiveText: t('commons.confirm'), 63 | negativeText: t('commons.stayInCurrentPage'), 64 | onPositiveClick() { 65 | const userStore = useUserStore(); 66 | userStore.logout().then(() => { 67 | router.push({ name: 'login' }); 68 | }); 69 | window.location.reload(); 70 | }, 71 | }); 72 | } 73 | return Promise.reject(res); 74 | } 75 | (response.data as any) = res.result; 76 | return response; 77 | }, 78 | (error) => { 79 | Message.error((error.msg && t(error.msg)) || 'Request Error', { 80 | duration: 5 * 1000, 81 | }); 82 | console.error('Request Error', error); 83 | return Promise.reject(error); 84 | } 85 | ); 86 | -------------------------------------------------------------------------------- /frontend/src/api/logs.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | import { 4 | AskLogDocument, 5 | LogFilterOptions, 6 | } from '@/types/schema'; 7 | 8 | import ApiUrl from './url'; 9 | 10 | export function getServerLogsApi(options: LogFilterOptions | null) { 11 | return axios.post(ApiUrl.ServerLogs, options); 12 | } 13 | 14 | export function getCompletionLogsApi(start_time?: string, end_time?: string, max_results = 100) { 15 | return axios.get(ApiUrl.CompletionLogs, { params: { start_time, end_time, max_results } }); 16 | } 17 | -------------------------------------------------------------------------------- /frontend/src/api/status.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | import { CommonStatusSchema } from '@/types/schema'; 4 | 5 | import ApiUrl from './url'; 6 | 7 | export function getServerStatusApi() { 8 | return axios.get(ApiUrl.ServerStatus); 9 | } 10 | -------------------------------------------------------------------------------- /frontend/src/api/system.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | import { 4 | AskLogAggregation, 5 | ConfigModel, 6 | CredentialsModel, 7 | LogFilterOptions, 8 | OpenaiWebAccountsCheckResponse, 9 | RequestLogAggregation, 10 | SystemInfo, 11 | } from '@/types/schema'; 12 | 13 | import ApiUrl from './url'; 14 | 15 | export function getSystemInfoApi() { 16 | return axios.get(ApiUrl.SystemInfo); 17 | } 18 | 19 | export function getRequestStatisticsApi(granularity: number) { 20 | return axios.get(ApiUrl.SystemRequestStatistics, { 21 | params: { granularity }, 22 | }); 23 | } 24 | 25 | export function getAskStatisticsApi(granularity: number) { 26 | return axios.get(ApiUrl.SystemAskStatistics, { 27 | params: { granularity }, 28 | }); 29 | } 30 | 31 | export function getSystemConfig() { 32 | return axios.get(ApiUrl.SystemConfig); 33 | } 34 | 35 | export function updateSystemConfig(config: ConfigModel) { 36 | return axios.put(ApiUrl.SystemConfig, config); 37 | } 38 | 39 | export function getSystemCredentials() { 40 | return axios.get(ApiUrl.SystemCredentials); 41 | } 42 | 43 | export function updateSystemCredentials(credentials: CredentialsModel) { 44 | return axios.put(ApiUrl.SystemCredentials, credentials); 45 | } 46 | 47 | export function runActionSyncOpenaiWebConversations() { 48 | return axios.post(ApiUrl.SystemActionSyncOpenaiWebConversations); 49 | } 50 | 51 | export function SystemCheckOpenaiWebAccount() { 52 | return axios.get(ApiUrl.SystemCheckOpenaiWebAccount); 53 | } -------------------------------------------------------------------------------- /frontend/src/api/url.ts: -------------------------------------------------------------------------------- 1 | enum ApiUrl { 2 | Register = '/auth/register', 3 | Login = '/auth/login', 4 | Logout = '/auth/logout', 5 | UserMe = '/user/me', 6 | 7 | Conversation = '/conv', 8 | AllConversation = '/conv/all', 9 | UserList = '/user', 10 | 11 | ChatPlugin = '/chat/openai-plugin', 12 | ChatPlugins = '/chat/openai-plugins', 13 | InstalledChatPlugins = '/chat/openai-plugins/installed', 14 | 15 | ServerStatus = '/status/common', 16 | 17 | SystemInfo = '/system/info', 18 | SystemRequestStatistics = '/system/stats/request', 19 | SystemAskStatistics = '/system/stats/ask', 20 | SystemActionSyncOpenaiWebConversations = '/system/action/sync-openai-web-conv', 21 | SystemCheckOpenaiWebAccount = '/system/check-openai-web-account', 22 | 23 | ServerLogs = '/logs/server', 24 | CompletionLogs = '/logs/completions', 25 | 26 | SystemConfig = '/system/config', 27 | SystemCredentials = '/system/credentials', 28 | 29 | 30 | FilesLocalUpload = '/files/local/upload', 31 | FilesLocalDownload = '/files/local/download', 32 | FilesOpenaiWebUploadStart = '/files/openai-web/upload-start', 33 | FilesOpenaiWebUploadComplete = '/files/openai-web/upload-complete', 34 | FilesLocalUploadToOpenaiWeb = '/files/local/upload-to-openai-web', 35 | 36 | ArkoseInfo = '/arkose/info', 37 | } 38 | 39 | export default ApiUrl; 40 | -------------------------------------------------------------------------------- /frontend/src/api/user.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | import { UserCreate, UserRead, UserReadAdmin, UserSettingSchema, UserUpdate, UserUpdateAdmin } from '@/types/schema'; 4 | 5 | import ApiUrl from './url'; 6 | 7 | export type LoginData = { 8 | username: string; 9 | password: string; 10 | }; 11 | 12 | export function loginApi(data: LoginData) { 13 | const formData = new FormData(); 14 | formData.set('username', data.username); 15 | formData.set('password', data.password); 16 | return axios.post(ApiUrl.Login, formData, { 17 | headers: { 18 | 'Content-Type': 'multipart/form-data', 19 | }, 20 | }); 21 | } 22 | 23 | export function registerApi(userInfo: UserCreate) { 24 | return axios.post(ApiUrl.Register, userInfo); 25 | } 26 | 27 | export function logoutApi() { 28 | return axios.post(ApiUrl.Logout); 29 | } 30 | 31 | export function getAllUserApi() { 32 | return axios.get(ApiUrl.UserList); 33 | } 34 | 35 | export function getUserMeApi() { 36 | return axios.get(ApiUrl.UserMe); 37 | } 38 | 39 | export function updateUserMeApi(userUpdate: Partial) { 40 | return axios.patch(ApiUrl.UserMe, userUpdate); 41 | } 42 | 43 | export function getUserByIdApi(userId: number) { 44 | return axios.get(ApiUrl.UserList + `/${userId}`); 45 | } 46 | 47 | export function updateUserByIdApi(userId: number, userUpdateAdmin: Partial) { 48 | return axios.patch(ApiUrl.UserList + `/${userId}`, userUpdateAdmin); 49 | } 50 | 51 | export function deleteUserApi(user_id: number) { 52 | return axios.delete(ApiUrl.UserList + `/${user_id}`); 53 | } 54 | 55 | export function updateUserSettingApi(userId: number, userSetting: Partial) { 56 | return axios.patch(ApiUrl.UserList + `/${userId}/setting`, userSetting); 57 | } 58 | -------------------------------------------------------------------------------- /frontend/src/components/BrowsingIcon.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 22 | -------------------------------------------------------------------------------- /frontend/src/components/ChatGPTAvatar.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 72 | -------------------------------------------------------------------------------- /frontend/src/components/ChatModelTagsRow.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 27 | -------------------------------------------------------------------------------- /frontend/src/components/ChatTypeTagInfoCell.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 32 | -------------------------------------------------------------------------------- /frontend/src/components/HelpTooltip.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 15 | -------------------------------------------------------------------------------- /frontend/src/components/OpenaiWebPluginDetailCard.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 40 | -------------------------------------------------------------------------------- /frontend/src/components/PreferenceForm.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 65 | -------------------------------------------------------------------------------- /frontend/src/components/UserProfileCard.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 54 | -------------------------------------------------------------------------------- /frontend/src/components/icons/browsing/click.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /frontend/src/components/icons/browsing/click_result.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /frontend/src/components/icons/browsing/external_link.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /frontend/src/components/icons/browsing/failed.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /frontend/src/components/icons/browsing/finished.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /frontend/src/components/icons/browsing/go_back.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /frontend/src/components/icons/browsing/scroll.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/components/icons/browsing/search.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /frontend/src/hooks/drawer.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue'; 2 | 3 | type UseDrawerOption = { 4 | name: string; 5 | title: string; 6 | beforeOpen?: (row: any) => void; 7 | afterClose?: () => void; 8 | }; 9 | 10 | export function useDrawer(options: UseDrawerOption[]) { 11 | const show = ref(false); 12 | const title = ref(''); 13 | const name = ref(''); 14 | const _options = options; 15 | 16 | // console.log('useDrawer', options); 17 | 18 | function open(_name: string, row: any) { 19 | const opt = _options.find((option) => option.name === _name); 20 | name.value = _name; 21 | title.value = opt?.title || ''; 22 | opt?.beforeOpen?.(row); 23 | show.value = true; 24 | console.log('open', _name, opt); 25 | } 26 | 27 | function close() { 28 | const opt = _options.find((option) => option.name === name.value); 29 | show.value = false; 30 | opt?.afterClose?.(); 31 | } 32 | 33 | return { show, title, name, open, close }; 34 | } 35 | -------------------------------------------------------------------------------- /frontend/src/i18n.ts: -------------------------------------------------------------------------------- 1 | import { useStorage } from '@vueuse/core'; 2 | import { WritableComputedRef } from 'vue'; 3 | import { createI18n, type I18n, type Locale } from 'vue-i18n'; 4 | 5 | import EN from './locales/en-US.json'; 6 | import MS from './locales/ms-MY.json'; 7 | import ZH from './locales/zh-CN.json'; 8 | 9 | let i18n: I18n; 10 | 11 | const init = () => { 12 | i18n = createI18n({ 13 | legacy: false, 14 | locale: useStorage('language', 'zh-CN').value, 15 | messages: { 16 | 'en-US': { 17 | ...EN, 18 | }, 19 | 'ms-MY': { 20 | ...MS, 21 | }, 22 | 'zh-CN': { 23 | ...ZH, 24 | }, 25 | }, 26 | }); 27 | }; 28 | 29 | const setLocale = (locale: Locale): void => { 30 | (i18n.global.locale as WritableComputedRef).value = locale; 31 | }; 32 | 33 | init(); 34 | 35 | export { i18n, setLocale }; 36 | -------------------------------------------------------------------------------- /frontend/src/main.ts: -------------------------------------------------------------------------------- 1 | import './style.css'; 2 | // eslint-disable-next-line import/no-unresolved 3 | import 'uno.css'; 4 | import '@/api/interceptor'; 5 | import 'katex/dist/katex.css'; 6 | 7 | import { 8 | NButton, 9 | NCheckbox, 10 | NCheckboxGroup, 11 | NDatePicker, 12 | NForm, 13 | NFormItem, 14 | NInput, 15 | NInputNumber, 16 | NPopover, 17 | NSelect, 18 | NSpace, 19 | NSwitch, 20 | NTimePicker, 21 | } from 'naive-ui'; 22 | import { createApp } from 'vue'; 23 | 24 | import App from './App.vue'; 25 | import { i18n } from './i18n'; 26 | import router from './router'; 27 | import pinia from './store'; 28 | 29 | // import * as Sentry from "@sentry/vue"; 30 | // import { BrowserTracing } from "@sentry/tracing"; 31 | 32 | const app = createApp(App); 33 | 34 | // if (import.meta.env.VITE_ENABLE_SENTRY === "yes") { 35 | // Sentry.init({ 36 | // app, 37 | // dsn: import.meta.env.VITE_SENTRY_DSN || "", 38 | // integrations: [ 39 | // new BrowserTracing({ 40 | // routingInstrumentation: Sentry.vueRouterInstrumentation(router), 41 | // // tracePropagationTargets: ["localhost", "my-site-url.com", /^\//], 42 | // }), 43 | // ], 44 | // tracesSampleRate: 1.0, 45 | // ignoreErrors: ["AxiosError", "errors."] 46 | // }); 47 | // } 48 | 49 | app.use(router); 50 | app.use(pinia); 51 | app.use(i18n); 52 | // app.use(hljs.vuePlugin); 53 | 54 | // app.component('NForm', NForm); 55 | // app.component('NFormItem', NFormItem); 56 | // app.component('NInput', NInput); 57 | // app.component('NInputNumber', NInputNumber); 58 | // app.component('NSwitch', NSwitch); 59 | 60 | // 注册部分naive-ui组件,以供vue-form使用 61 | const naiveFormComponents = [ 62 | NForm, 63 | NFormItem, 64 | NInput, 65 | NInputNumber, 66 | NSwitch, 67 | NButton, 68 | NSelect, 69 | NPopover, 70 | NCheckbox, 71 | NCheckboxGroup, 72 | NSpace, 73 | NDatePicker, 74 | NTimePicker, 75 | ]; 76 | naiveFormComponents.forEach((component) => { 77 | app.component(`N${component.name}`, component); 78 | }); 79 | 80 | app.mount('#app'); 81 | 82 | declare global { 83 | interface Window { 84 | $message: any; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /frontend/src/router/guard/index.ts: -------------------------------------------------------------------------------- 1 | import type { Router } from 'vue-router'; 2 | 3 | import setupPermissionGuard from './permission'; 4 | import setupUserLoginInfoGuard from './userLoginInfo'; 5 | 6 | export default function createRouteGuard(router: Router) { 7 | setupUserLoginInfoGuard(router); 8 | setupPermissionGuard(router); 9 | } 10 | -------------------------------------------------------------------------------- /frontend/src/router/guard/permission.ts: -------------------------------------------------------------------------------- 1 | import type { LocationQueryRaw, Router } from 'vue-router'; 2 | 3 | import { i18n } from '@/i18n'; 4 | import { useUserStore } from '@/store'; 5 | import { Message } from '@/utils/tips'; 6 | const t = i18n.global.t as any; 7 | 8 | // 在 userLoginInfo 之后,此时要么登录成功,要么未登录 9 | export default function setupPermissionGuard(router: Router) { 10 | router.beforeEach(async (to, from, next) => { 11 | const userStore = useUserStore(); 12 | if (!to.meta.requiresAuth) next(); 13 | else { 14 | if (userStore.user === null) { 15 | Message.error(t('errors.userNotLogin')); 16 | next({ 17 | name: 'login', 18 | query: { 19 | redirect: to.name, 20 | ...to.query, 21 | } as LocationQueryRaw, 22 | }); 23 | } else { 24 | // if (to.meta.roles.find((role) => role === userStore.user.role) === ) { 25 | // if (userStore.user.is_superuser) next(); 26 | // else next({ name: "403" }); 27 | // } else next(); 28 | const role = userStore.user.is_superuser ? 'superuser' : 'user'; 29 | if (to.meta.roles.find((r) => r === role) === undefined) { 30 | next({ name: '403' }); 31 | } else next(); 32 | } 33 | } 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /frontend/src/router/guard/userLoginInfo.ts: -------------------------------------------------------------------------------- 1 | import type { LocationQueryRaw, Router } from 'vue-router'; 2 | 3 | import { useUserStore } from '@/store'; 4 | import { hasLoginCookie } from '@/utils/auth'; 5 | 6 | // 确保保持登录状态,并及时更新用户信息 7 | export default function setupUserLoginInfoGuard(router: Router) { 8 | router.beforeEach(async (to, from, next) => { 9 | const userStore = useUserStore(); 10 | if (hasLoginCookie()) { 11 | if (userStore.user != null) { 12 | next(); 13 | } else { 14 | try { 15 | await userStore.fetchUserInfo(); 16 | next(); 17 | } catch (error) { 18 | console.error(error); 19 | await userStore.logout(); 20 | if (to.name !== 'login') { 21 | next({ 22 | name: 'login', 23 | query: { 24 | redirect: to.name, 25 | ...to.query, 26 | } as LocationQueryRaw, 27 | }); 28 | } else { 29 | next(); 30 | } 31 | } 32 | } 33 | } else { 34 | next(); 35 | } 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /frontend/src/router/typings.d.ts: -------------------------------------------------------------------------------- 1 | import 'vue-router'; 2 | 3 | declare type Role = 'superuser' | 'user'; 4 | 5 | declare module 'vue-router' { 6 | interface RouteMeta { 7 | requiresAuth: boolean; // Whether login is required to access the current page (every route must declare) 8 | roles: Role[]; // The role of the current page (every route must declare) 9 | ignoreCache?: boolean; // if set true, the page will not be cached 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { createPinia } from 'pinia'; 2 | 3 | import useAppStore from './modules/app'; 4 | import useConversationStore from './modules/conversation'; 5 | import useFileStore from './modules/file'; 6 | import useUserStore from './modules/user'; 7 | 8 | const pinia = createPinia(); 9 | 10 | export { useAppStore, useConversationStore, useFileStore, useUserStore }; 11 | 12 | export default pinia; 13 | -------------------------------------------------------------------------------- /frontend/src/store/modules/app.ts: -------------------------------------------------------------------------------- 1 | import { useOsTheme } from 'naive-ui'; 2 | import { defineStore } from 'pinia'; 3 | 4 | import { AppState, Preference } from '../types'; 5 | const osThemeRef = useOsTheme(); 6 | import { useStorage } from '@vueuse/core'; 7 | 8 | import { setLocale } from '@/i18n'; 9 | import { ChatSourceTypes } from '@/types/schema'; 10 | import { themeRef } from '@/utils/tips'; 11 | 12 | const useAppStore = defineStore('app', { 13 | state: (): AppState => ({ 14 | theme: useStorage('theme', osThemeRef.value), 15 | language: useStorage('language', 'zh-CN'), 16 | preference: useStorage('preference', { 17 | sendKey: 'Enter', 18 | renderUserMessageInMd: false, 19 | codeAutoWrap: false, 20 | widerConversationPage: true, 21 | }), 22 | lastSelectedSource: useStorage('lastSelectedSource', null), 23 | lastSelectedModel: useStorage('lastSelectedModel', null), 24 | }), 25 | getters: {}, 26 | actions: { 27 | // 切换主题 28 | toggleTheme() { 29 | this.theme = this.theme === 'dark' ? 'light' : 'dark'; 30 | themeRef.value = this.theme; 31 | }, 32 | setLanguage(lang: string) { 33 | this.language = lang; 34 | setLocale(lang); 35 | }, 36 | }, 37 | }); 38 | 39 | export default useAppStore; 40 | -------------------------------------------------------------------------------- /frontend/src/store/modules/file.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia'; 2 | 3 | import { FileState } from '../types'; 4 | 5 | const useFileStore = defineStore('file', { 6 | state: (): FileState => ({ 7 | uploadedFileInfos: [], 8 | naiveUiUploadFileInfos: [], 9 | naiveUiFileIdToServerFileIdMap: {}, 10 | }), 11 | actions: { 12 | clear() { 13 | this.uploadedFileInfos = []; 14 | this.naiveUiUploadFileInfos = []; 15 | this.naiveUiFileIdToServerFileIdMap = {}; 16 | }, 17 | }, 18 | }); 19 | 20 | export default useFileStore; 21 | -------------------------------------------------------------------------------- /frontend/src/store/modules/user.ts: -------------------------------------------------------------------------------- 1 | import { useStorage } from '@vueuse/core'; 2 | import { defineStore } from 'pinia'; 3 | 4 | import { getUserMeApi, loginApi, LoginData, logoutApi } from '@/api/user'; 5 | import { UserRead } from '@/types/schema'; 6 | import { clearCookie } from '@/utils/auth'; 7 | 8 | import { UserState } from '../types'; 9 | 10 | const useUserStore = defineStore('user', { 11 | state: (): UserState => ({ 12 | user: null, 13 | savedLoginForm: useStorage('savedLoginForm', { 14 | rememberPassword: false, 15 | savedUsername: undefined, 16 | savedPassword: undefined, 17 | }), 18 | }), 19 | getters: { 20 | userInfo(): UserRead | null { 21 | return this.user; 22 | }, 23 | }, 24 | 25 | actions: { 26 | // Set user's information 27 | setInfo(user: UserRead) { 28 | this.$patch({ user }); 29 | }, 30 | 31 | setSavedLoginInfo(username: string, password: string) { 32 | this.savedLoginForm.savedUsername = username; 33 | this.savedLoginForm.savedPassword = password; 34 | }, 35 | 36 | // Reset user's information 37 | resetInfo() { 38 | this.$reset(); 39 | }, 40 | 41 | // Get user's information 42 | async fetchUserInfo() { 43 | const result = (await getUserMeApi()).data; 44 | this.setInfo(result); 45 | }, 46 | 47 | // Login 48 | async login(loginForm: LoginData) { 49 | try { 50 | await loginApi(loginForm); 51 | // setToken(res.data.token); 52 | } catch (err) { 53 | clearCookie(); 54 | throw err; 55 | } 56 | }, 57 | 58 | // Logout 59 | async logout() { 60 | try { 61 | await logoutApi(); 62 | } finally { 63 | this.resetInfo(); 64 | clearCookie(); 65 | } 66 | }, 67 | }, 68 | }); 69 | 70 | export default useUserStore; 71 | -------------------------------------------------------------------------------- /frontend/src/store/types.ts: -------------------------------------------------------------------------------- 1 | import { RemovableRef } from '@vueuse/core'; 2 | import { UploadFileInfo } from 'naive-ui'; 3 | import { Ref } from 'vue'; 4 | 5 | import { 6 | BaseConversationHistory, 7 | BaseConversationSchema, 8 | ChatSourceTypes, 9 | UploadedFileInfoSchema, 10 | UserRead, 11 | } from '@/types/schema'; 12 | 13 | export type SavedLoginForm = { 14 | rememberPassword: boolean; 15 | savedUsername: string | undefined; 16 | savedPassword: string | undefined; 17 | }; 18 | 19 | interface UserState { 20 | user: UserRead | null; 21 | savedLoginForm: Ref; 22 | } 23 | 24 | export type Preference = { 25 | sendKey: 'Shift+Enter' | 'Enter' | 'Ctrl+Enter'; 26 | renderUserMessageInMd: boolean; 27 | codeAutoWrap: boolean; 28 | widerConversationPage: boolean; 29 | }; 30 | 31 | interface AppState { 32 | theme: any; 33 | language: Ref<'zh-CN' | 'en-US' | 'ms-MY' | string>; 34 | preference: Ref; 35 | lastSelectedSource: Ref; 36 | lastSelectedModel: Ref; 37 | } 38 | 39 | interface ConversationState { 40 | conversations: Array; 41 | newConversation: BaseConversationSchema | null; 42 | conversationHistoryMap: Record; 43 | } 44 | 45 | interface FileState { 46 | uploadedFileInfos: UploadedFileInfoSchema[]; 47 | naiveUiUploadFileInfos: UploadFileInfo[]; 48 | naiveUiFileIdToServerFileIdMap: Record; 49 | // imageMetadataMap: Record; // 使用 server 端的文件 id 作为 key 50 | } 51 | 52 | export type { AppState, ConversationState, FileState, UserState }; 53 | -------------------------------------------------------------------------------- /frontend/src/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | overflow: hidden; 3 | --header-height: 60px; 4 | } 5 | 6 | @media print { 7 | .hide-in-print * { 8 | visibility: hidden !important; 9 | } 10 | } 11 | 12 | /* For WebKit-based browsers (e.g., Chrome, Safari) */ 13 | ::-webkit-scrollbar { 14 | width: 8px; /* Change the width of the scrollbar */ 15 | height: 8px; /* Change the height of the scrollbar */ 16 | } 17 | 18 | ::-webkit-scrollbar-track { 19 | background: transparent; /* Set the scrollbar track color to transparent */ 20 | border-radius: 10px; /* Add border-radius to the scrollbar track */ 21 | } 22 | 23 | ::-webkit-scrollbar-thumb { 24 | background: #888; /* Change the color of the scrollbar thumb */ 25 | border-radius: 10px; /* Add border-radius to the scrollbar thumb */ 26 | } 27 | 28 | ::-webkit-scrollbar-thumb:hover { 29 | background: #555; /* Change the color of the scrollbar thumb when hovered */ 30 | } 31 | 32 | /* For Firefox */ 33 | * { 34 | scrollbar-width: thin; /* Change the width of the scrollbar */ 35 | scrollbar-color: #888 transparent; /* Set the scrollbar thumb color and make the track transparent */ 36 | } 37 | -------------------------------------------------------------------------------- /frontend/src/types/custom.ts: -------------------------------------------------------------------------------- 1 | import { ChatSourceTypes } from './schema'; 2 | 3 | export interface NewConversationInfo { 4 | title: string | null; 5 | source: ChatSourceTypes | null; 6 | model: string | null; 7 | openaiWebPlugins: string[] | null; 8 | } 9 | -------------------------------------------------------------------------------- /frontend/src/types/echarts.ts: -------------------------------------------------------------------------------- 1 | import { CallbackDataParams } from 'echarts/types/dist/shared'; 2 | 3 | export interface ToolTipFormatterParams extends CallbackDataParams { 4 | axisDim: string; 5 | axisIndex: number; 6 | axisType: string; 7 | axisId: string; 8 | axisValue: string; 9 | axisValueLabel: string; 10 | } 11 | -------------------------------------------------------------------------------- /frontend/src/types/json/credentials_schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "properties": { 3 | "openai_web_access_token": { 4 | "anyOf": [ 5 | { 6 | "type": "string" 7 | }, 8 | { 9 | "type": "null" 10 | } 11 | ], 12 | "default": null, 13 | "title": "Openai Web Access Token" 14 | }, 15 | "openai_api_key": { 16 | "anyOf": [ 17 | { 18 | "type": "string" 19 | }, 20 | { 21 | "type": "null" 22 | } 23 | ], 24 | "default": null, 25 | "title": "Openai Api Key" 26 | } 27 | }, 28 | "title": "CredentialsModel", 29 | "type": "object" 30 | } 31 | -------------------------------------------------------------------------------- /frontend/src/types/json/model_definitions.json: -------------------------------------------------------------------------------- 1 | {"openai_web": ["gpt_3_5", "gpt_4", "gpt_4o", "o1", "o1_mini"], "openai_api": ["gpt_3_5", "gpt_4", "gpt_4o", "o1", "o1_mini"]} 2 | -------------------------------------------------------------------------------- /frontend/src/types/json_schema.ts: -------------------------------------------------------------------------------- 1 | import modelDefinitions from './json/model_definitions.json'; 2 | import jsonSchemas from './json/schemas.json'; 3 | import { OpenaiApiChatModels, OpenaiWebChatModels } from './schema'; 4 | 5 | export const jsonRevSourceSettingSchema = jsonSchemas.OpenaiWebSourceSettingSchema; 6 | export const jsonApiSourceSettingSchema = jsonSchemas.OpenaiApiSourceSettingSchema; 7 | 8 | export const jsonConfigModelSchema = jsonSchemas['ConfigModel-Output']; 9 | export const jsonCredentialsModelSchema = jsonSchemas.CredentialsModel; 10 | 11 | export const openaiWebChatModelNames = modelDefinitions.openai_web as OpenaiWebChatModels[]; 12 | export const openaiApiChatModelNames = modelDefinitions.openai_api as OpenaiApiChatModels[]; 13 | export const allChatModelNames = [...openaiWebChatModelNames, ...openaiApiChatModelNames] as string[]; 14 | -------------------------------------------------------------------------------- /frontend/src/utils/auth.ts: -------------------------------------------------------------------------------- 1 | import { hasCookie, removeCookie } from '@/utils/cookies'; 2 | // import { useUserStore } from '@/store'; 3 | 4 | const COOKIE_KEY = 'cws_user_auth'; 5 | 6 | const hasLoginCookie = () => { 7 | // const userStore = useUserStore(); 8 | return !!hasCookie(COOKIE_KEY); 9 | }; 10 | 11 | const clearCookie = () => { 12 | removeCookie(COOKIE_KEY); 13 | }; 14 | 15 | export { clearCookie, hasLoginCookie }; 16 | -------------------------------------------------------------------------------- /frontend/src/utils/cookies.ts: -------------------------------------------------------------------------------- 1 | // 参考:https://github.com/cmp-cc/vue-cookies/blob/master/vue-cookies.js 2 | 3 | const defaultConfig = { 4 | expires: '1d', 5 | path: '; path=/', 6 | domain: '', 7 | secure: '', 8 | sameSite: '; SameSite=Lax', 9 | }; 10 | 11 | export function hasCookie(key: string): boolean { 12 | return new RegExp(`(?:^|;\\s*)${encodeURIComponent(key).replace(/[-.+*]/g, '\\$&')}\\s*\\=`).test(document.cookie); 13 | } 14 | 15 | export function removeCookie(key: string, path: string | null = null, domain: string | null = null): boolean { 16 | if (!key || !hasCookie(key)) { 17 | return false; 18 | } 19 | document.cookie = `${encodeURIComponent(key)}=; expires=Thu, 01 Jan 1970 00:00:00 GMT${ 20 | domain ? `; domain=${domain}` : defaultConfig.domain 21 | }${path ? `; path=${path}` : defaultConfig.path}; SameSite=Lax`; 22 | return true; 23 | } 24 | -------------------------------------------------------------------------------- /frontend/src/utils/highlight.ts: -------------------------------------------------------------------------------- 1 | import hljs from 'highlight.js'; 2 | 3 | function hljsDefineVue() { 4 | return { 5 | subLanguage: 'xml', 6 | contains: [ 7 | hljs.COMMENT('', { 8 | relevance: 10, 9 | }), 10 | { 11 | begin: /^(\s*)( 69 | -------------------------------------------------------------------------------- /frontend/src/views/admin/components/StatisticsCard.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 35 | -------------------------------------------------------------------------------- /frontend/src/views/admin/components/UpdateUserBasicForm.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 93 | -------------------------------------------------------------------------------- /frontend/src/views/admin/components/UpdateUserSettingForm.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 57 | -------------------------------------------------------------------------------- /frontend/src/views/admin/components/UserSelector.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 43 | -------------------------------------------------------------------------------- /frontend/src/views/admin/components/charts/helpers.ts: -------------------------------------------------------------------------------- 1 | export const timeFormatter = (value: string | Date | number, withYear: boolean) => { 2 | // const date = typeof value === 'string' ? new Date(value) : value; 3 | if (typeof value === 'string' || typeof value === 'number') { 4 | value = new Date(value); 5 | } 6 | const date = value; 7 | const year = date.getFullYear(); 8 | const month = (date.getMonth() + 1).toString().padStart(2, '0'); 9 | const day = date.getDate().toString().padStart(2, '0'); 10 | const hour = date.getHours().toString().padStart(2, '0'); 11 | const minute = date.getMinutes().toString().padStart(2, '0'); 12 | return (withYear ? `${year}-` : '') + `${month}-${day} ${hour}:${minute}`; 13 | }; 14 | -------------------------------------------------------------------------------- /frontend/src/views/admin/components/inputs/CountNumberInput.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 47 | -------------------------------------------------------------------------------- /frontend/src/views/admin/components/inputs/CountNumberInputWithAdd.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 41 | -------------------------------------------------------------------------------- /frontend/src/views/admin/components/inputs/ModelDictField.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 88 | -------------------------------------------------------------------------------- /frontend/src/views/admin/components/inputs/RateLimitsArrayInput.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 83 | -------------------------------------------------------------------------------- /frontend/src/views/admin/components/inputs/TimeSlotsArrayInput.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 66 | -------------------------------------------------------------------------------- /frontend/src/views/admin/components/inputs/ValidDateTimeInput.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 79 | -------------------------------------------------------------------------------- /frontend/src/views/admin/index.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 86 | -------------------------------------------------------------------------------- /frontend/src/views/admin/pages/log_viewer.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 102 | 103 | 108 | -------------------------------------------------------------------------------- /frontend/src/views/admin/pages/openai_settings.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 21 | -------------------------------------------------------------------------------- /frontend/src/views/admin/pages/system_manager.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 53 | -------------------------------------------------------------------------------- /frontend/src/views/conversation/components/LeftBarConversationMenuLabel.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | 13 | 34 | -------------------------------------------------------------------------------- /frontend/src/views/conversation/components/MessageRowAttachmentDisplay.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 39 | -------------------------------------------------------------------------------- /frontend/src/views/conversation/components/MessageRowMyFilesBrowserDisplay.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 85 | -------------------------------------------------------------------------------- /frontend/src/views/conversation/components/MessageRowPluginAction.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 67 | -------------------------------------------------------------------------------- /frontend/src/views/conversation/components/MessageRowPluginDisplay.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 30 | -------------------------------------------------------------------------------- /frontend/src/views/conversation/components/MessageRowTextDisplay.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 64 | -------------------------------------------------------------------------------- /frontend/src/views/conversation/components/NewConversationFormModelSelectionLabel.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 27 | 28 | 36 | -------------------------------------------------------------------------------- /frontend/src/views/conversation/components/NewConversationFormPluginSelectionLabel.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 30 | 31 | 39 | -------------------------------------------------------------------------------- /frontend/src/views/conversation/history-viewer.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 41 | -------------------------------------------------------------------------------- /frontend/src/views/conversation/utils/codeblock.ts: -------------------------------------------------------------------------------- 1 | import * as clipboard from 'clipboard-polyfill'; 2 | import { Ref } from 'vue'; 3 | 4 | export function bindOnclick(contentRef: Ref) { 5 | // 获取模板引用中的所有 pre 元素和其子元素中的 button 元素 6 | const preElements = contentRef.value?.querySelectorAll('pre'); 7 | if (!preElements) return; 8 | for (const preElement of preElements as any) { 9 | for (const button of preElement.querySelectorAll('button')) { 10 | (button as HTMLButtonElement).onmousedown = () => { 11 | // 如果按钮的内容为 "Copied!",则跳过复制操作 12 | if (button.innerHTML === 'Copied!') { 13 | return; 14 | } 15 | 16 | const preContent = button.parentElement!.cloneNode(true) as HTMLElement; 17 | preContent.removeChild(preContent.querySelector('button')!); 18 | 19 | // Remove the alert element if it exists in preContent 20 | const alertElement = preContent.querySelector('.hljs-copy-alert'); 21 | if (alertElement) { 22 | preContent.removeChild(alertElement); 23 | } 24 | 25 | clipboard 26 | .writeText(preContent.textContent || '') 27 | .then(function () { 28 | button.innerHTML = 'Copied!'; 29 | button.dataset.copied = 'true'; 30 | 31 | let alert: HTMLDivElement | null = Object.assign(document.createElement('div'), { 32 | role: 'status', 33 | className: 'hljs-copy-alert', 34 | innerHTML: 'Copied to clipboard', 35 | }); 36 | button.parentElement!.appendChild(alert); 37 | 38 | setTimeout(() => { 39 | if (alert) { 40 | button.innerHTML = 'Copy'; 41 | button.dataset.copied = 'false'; 42 | button.parentElement!.removeChild(alert); 43 | alert = null; 44 | } 45 | }, 2000); 46 | }) 47 | .then(); 48 | }; 49 | } 50 | } 51 | } 52 | 53 | // 为代码块设置样式以及添加copy按钮 54 | export function processPreTags(htmlString: string, codeAutoWrap = false): string { 55 | // Parse the HTML string into an Element object. 56 | const parser = new DOMParser(); 57 | const doc = parser.parseFromString(htmlString, 'text/html'); 58 | 59 | // Get all the
 elements in the document.
60 |   const preTags = doc.getElementsByTagName('pre');
61 | 
62 |   // Loop through the 
 elements and add a