├── vercel.json ├── public ├── pwa-192.png ├── pwa-512.png ├── apple-touch-icon.png └── icon.svg ├── .eslintignore ├── .npmrc ├── .dockerignore ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── src ├── types.ts ├── components │ ├── icons │ │ ├── Clear.tsx │ │ ├── X.tsx │ │ ├── Env.tsx │ │ └── Refresh.tsx │ ├── Footer.astro │ ├── Header.astro │ ├── ErrorMessageItem.tsx │ ├── Logo.astro │ ├── SystemRoleSettings.tsx │ ├── MessageItem.tsx │ ├── Themetoggle.astro │ └── Generator.tsx ├── env.d.ts ├── pages │ ├── api │ │ ├── auth.ts │ │ └── generate.ts │ ├── index.astro │ └── password.astro ├── message.css ├── utils │ ├── auth.ts │ └── openAI.ts └── layouts │ └── Layout.astro ├── tsconfig.json ├── netlify.toml ├── .gitignore ├── shims.d.ts ├── Dockerfile ├── docker-compose.yml ├── hack ├── docker-entrypoint.sh └── docker-env-replace.sh ├── .env.example ├── plugins └── disableBlocks.ts ├── .eslintrc.js ├── LICENSE ├── package.json ├── astro.config.mjs ├── unocss.config.ts ├── README.zh-CN.md └── README.md /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "buildCommand": "OUTPUT=vercel astro build" 3 | } -------------------------------------------------------------------------------- /public/pwa-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/ACEM/main/public/pwa-192.png -------------------------------------------------------------------------------- /public/pwa-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/ACEM/main/public/pwa-512.png -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | public 3 | node_modules 4 | .netlify 5 | .vercel 6 | .github 7 | .changeset 8 | -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/ACEM/main/public/apple-touch-icon.png -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ 2 | strict-peer-dependencies=false 3 | auto-install-peers=true 4 | shamefully-hoist=true 5 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | *.md 2 | Dockerfile 3 | docker-compose.yml 4 | LICENSE 5 | netlify.toml 6 | vercel.json 7 | node_modules 8 | .vscode 9 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["astro-build.astro-vscode","dbaeumer.vscode-eslint","antfu.unocss"], 3 | "unwantedRecommendations": [], 4 | } 5 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface ChatMessage { 2 | role: 'system' | 'user' | 'assistant' 3 | content: string 4 | } 5 | 6 | export interface ErrorMessage { 7 | code: string 8 | message: string 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "command": "./node_modules/.bin/astro dev", 6 | "name": "Development server", 7 | "request": "launch", 8 | "type": "node-terminal" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/base", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "jsx": "preserve", 6 | "jsxImportSource": "solid-js", 7 | "types": ["vite-plugin-pwa/info"], 8 | "paths": { 9 | "@/*": ["src/*"], 10 | }, 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build.environment] 2 | NETLIFY_USE_PNPM = "true" 3 | NODE_VERSION = "18" 4 | 5 | [build] 6 | command = "OUTPUT=netlify astro build" 7 | publish = "dist" 8 | 9 | [[headers]] 10 | for = "/manifest.webmanifest" 11 | [headers.values] 12 | Content-Type = "application/manifest+json" 13 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll.eslint": true 4 | }, 5 | "editor.formatOnSave": false, 6 | "eslint.validate": [ 7 | "javascript", 8 | "javascriptreact", 9 | "astro", // Enable .astro 10 | "typescript", // Enable .ts 11 | "typescriptreact" // Enable .tsx 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /src/components/icons/Clear.tsx: -------------------------------------------------------------------------------- 1 | export default () => { 2 | return ( 3 | 4 | ) 5 | } 6 | -------------------------------------------------------------------------------- /src/components/Footer.astro: -------------------------------------------------------------------------------- 1 | 19 | -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface ImportMetaEnv { 4 | readonly OPENAI_API_KEY: string 5 | readonly HTTPS_PROXY: string 6 | readonly OPENAI_API_BASE_URL: string 7 | readonly HEAD_SCRIPTS: string 8 | readonly SECRET_KEY: string 9 | readonly SITE_PASSWORD: string 10 | readonly OPENAI_API_MODEL: string 11 | } 12 | 13 | interface ImportMeta { 14 | readonly env: ImportMetaEnv 15 | } 16 | -------------------------------------------------------------------------------- /src/components/Header.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Logo from './Logo.astro' 3 | import Themetoggle from './Themetoggle.astro' 4 | --- 5 | 6 |
7 |
8 | 9 | 10 |
11 |
12 | ChatGPT 13 | Demo 14 |
15 |

Based on OpenAI API (gpt-3.5-turbo).

16 |
17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | .vercel/ 4 | .netlify/ 5 | 6 | # generated types 7 | .astro/ 8 | 9 | # dependencies 10 | node_modules/ 11 | 12 | # logs 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | pnpm-debug.log* 17 | 18 | # environment variables 19 | .env 20 | .env.production 21 | 22 | # macOS-specific files 23 | .DS_Store 24 | 25 | # Local 26 | *.local 27 | 28 | **/.DS_Store 29 | 30 | # Editor directories and files 31 | .idea 32 | -------------------------------------------------------------------------------- /src/components/icons/X.tsx: -------------------------------------------------------------------------------- 1 | export default () => { 2 | return ( 3 | 4 | ) 5 | } 6 | -------------------------------------------------------------------------------- /src/pages/api/auth.ts: -------------------------------------------------------------------------------- 1 | import type { APIRoute } from 'astro' 2 | 3 | const realPassword = import.meta.env.SITE_PASSWORD || '' 4 | const passList = realPassword.split(',') || [] 5 | 6 | export const post: APIRoute = async(context) => { 7 | const body = await context.request.json() 8 | 9 | const { pass } = body 10 | return new Response(JSON.stringify({ 11 | code: (!realPassword || pass === realPassword || passList.includes(pass)) ? 0 : -1, 12 | })) 13 | } 14 | -------------------------------------------------------------------------------- /shims.d.ts: -------------------------------------------------------------------------------- 1 | import type { AttributifyAttributes } from '@unocss/preset-attributify' 2 | 3 | // declare module 'solid-js' { 4 | // namespace JSX { 5 | // interface HTMLAttributes extends AttributifyAttributes {} 6 | // } 7 | // } 8 | 9 | declare global { 10 | namespace astroHTML.JSX { 11 | interface HTMLAttributes extends AttributifyAttributes { } 12 | } 13 | namespace JSX { 14 | interface HTMLAttributes<> extends AttributifyAttributes {} 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:alpine as builder 2 | WORKDIR /usr/src 3 | RUN npm install -g pnpm 4 | COPY package.json pnpm-lock.yaml ./ 5 | RUN pnpm install 6 | COPY . . 7 | RUN pnpm run build 8 | 9 | FROM node:alpine 10 | WORKDIR /usr/src 11 | RUN npm install -g pnpm 12 | COPY --from=builder /usr/src/dist ./dist 13 | COPY --from=builder /usr/src/hack ./ 14 | COPY package.json pnpm-lock.yaml ./ 15 | RUN pnpm install 16 | ENV HOST=0.0.0.0 PORT=3000 NODE_ENV=production 17 | EXPOSE $PORT 18 | CMD ["/bin/sh", "docker-entrypoint.sh"] 19 | -------------------------------------------------------------------------------- /src/components/icons/Env.tsx: -------------------------------------------------------------------------------- 1 | export default () => { 2 | return ( 3 | 4 | ) 5 | } 6 | -------------------------------------------------------------------------------- /src/message.css: -------------------------------------------------------------------------------- 1 | .message pre { 2 | background-color: #64748b10; 3 | font-size: 0.8rem; 4 | padding: 0.4rem 1rem; 5 | } 6 | 7 | .message .hljs { 8 | background-color: transparent; 9 | } 10 | 11 | .message table { 12 | font-size: 0.8em; 13 | } 14 | 15 | .message table thead tr { 16 | background-color: #64748b40; 17 | text-align: left; 18 | } 19 | 20 | .message table th, .message table td { 21 | padding: 0.6rem 1rem; 22 | } 23 | 24 | .message table tbody tr:last-of-type { 25 | border-bottom: 2px solid #64748b40; 26 | } -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | chatgpt-demo: 5 | image: ddiu8081/chatgpt-demo:latest 6 | container_name: chatgpt-demo 7 | restart: always 8 | ports: 9 | - "3000:3000" 10 | environment: 11 | - OPENAI_API_KEY=YOUR_OPENAI_API_KEY 12 | # - HTTPS_PROXY=YOUR_HTTPS_PROXY 13 | # - OPENAI_API_BASE_URL=YOUR_OPENAI_API_BASE_URL 14 | # - HEAD_SCRIPTS=YOUR_HEAD_SCRIPTS 15 | # - SECRET_KEY=YOUR_SECRET_KEY 16 | # - SITE_PASSWORD=YOUR_SITE_PASSWORD 17 | # - OPENAI_API_MODEL=YOUR_OPENAI_API_MODEL 18 | 19 | -------------------------------------------------------------------------------- /hack/docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | sub_service_pid="" 4 | 5 | sub_service_command="node dist/server/entry.mjs" 6 | 7 | function init() { 8 | /bin/sh ./docker-env-replace.sh 9 | } 10 | 11 | function main { 12 | init 13 | 14 | echo "Starting service..." 15 | eval "$sub_service_command &" 16 | sub_service_pid=$! 17 | 18 | trap cleanup SIGTERM SIGINT 19 | echo "Running script..." 20 | while [ true ]; do 21 | sleep 5 22 | done 23 | } 24 | 25 | function cleanup { 26 | echo "Cleaning up!" 27 | kill -TERM $sub_service_pid 28 | } 29 | 30 | main 31 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Your API Key for OpenAI 2 | OPENAI_API_KEY= 3 | # Provide proxy for OpenAI API. e.g. http://127.0.0.1:7890 4 | HTTPS_PROXY= 5 | # Custom base url for OpenAI API. default: https://api.openai.com 6 | OPENAI_API_BASE_URL= 7 | # Inject analytics or other scripts before of the page 8 | HEAD_SCRIPTS= 9 | # Secret string for the project. Use for generating signatures for API calls 10 | SECRET_KEY= 11 | # Set password for site, support multiple password separated by comma. If not set, site will be public 12 | SITE_PASSWORD= 13 | # ID of the model to use. https://platform.openai.com/docs/api-reference/models/list 14 | OPENAI_API_MODEL= 15 | -------------------------------------------------------------------------------- /plugins/disableBlocks.ts: -------------------------------------------------------------------------------- 1 | export default function plugin(platform?: string) { 2 | const transform = (code: string, id: string) => { 3 | if (id.includes('pages/api/generate.ts')) { 4 | return { 5 | code: code.replace(/^.*?#vercel-disable-blocks([\s\S]+?)#vercel-end.*?$/gm, ''), 6 | map: null, 7 | } 8 | } 9 | if (platform === 'netlify' && id.includes('layouts/Layout.astro')) { 10 | return { 11 | code: code.replace(/^.*?([\s\S]+?).*?$/gm, ''), 12 | map: null, 13 | } 14 | } 15 | } 16 | 17 | return { 18 | name: 'vercel-disable-blocks', 19 | enforce: 'pre', 20 | transform, 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/components/ErrorMessageItem.tsx: -------------------------------------------------------------------------------- 1 | import IconRefresh from './icons/Refresh' 2 | import type { ErrorMessage } from '@/types' 3 | 4 | interface Props { 5 | data: ErrorMessage 6 | onRetry?: () => void 7 | } 8 | 9 | export default ({ data, onRetry }: Props) => { 10 | return ( 11 |
12 | {data.code &&
{data.code}
} 13 |
{data.message}
14 | {onRetry && ( 15 |
16 |
17 | 18 | Regenerate 19 |
20 |
21 | )} 22 |
23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@evan-yang', 'plugin:astro/recommended'], 3 | rules: { 4 | 'no-console': ['error', { allow: ['error'] }], 5 | 'react/display-name': 'off', 6 | 'react-hooks/rules-of-hooks': 'off', 7 | '@typescript-eslint/no-use-before-define': 'off', 8 | }, 9 | overrides: [ 10 | { 11 | files: ['*.astro'], 12 | parser: 'astro-eslint-parser', 13 | parserOptions: { 14 | parser: '@typescript-eslint/parser', 15 | extraFileExtensions: ['.astro'], 16 | }, 17 | rules: { 18 | 'no-mixed-spaces-and-tabs': ['error', 'smart-tabs'], 19 | }, 20 | }, 21 | { 22 | // Define the configuration for ` 37 | -------------------------------------------------------------------------------- /src/components/icons/Refresh.tsx: -------------------------------------------------------------------------------- 1 | export default () => { 2 | return ( 3 | 4 | ) 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Diu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /public/icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Logo.astro: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/utils/auth.ts: -------------------------------------------------------------------------------- 1 | import { sha256 } from 'js-sha256' 2 | interface AuthPayload { 3 | t: number 4 | m: string 5 | } 6 | 7 | async function digestMessage(message: string) { 8 | if (typeof crypto !== 'undefined' && crypto?.subtle?.digest) { 9 | const msgUint8 = new TextEncoder().encode(message) 10 | const hashBuffer = await crypto.subtle.digest('SHA-256', msgUint8) 11 | const hashArray = Array.from(new Uint8Array(hashBuffer)) 12 | return hashArray.map(b => b.toString(16).padStart(2, '0')).join('') 13 | } else { 14 | return sha256(message).toString() 15 | } 16 | } 17 | 18 | export const generateSignature = async(payload: AuthPayload) => { 19 | const { t: timestamp, m: lastMessage } = payload 20 | const secretKey = import.meta.env.PUBLIC_SECRET_KEY as string 21 | const signText = `${timestamp}:${lastMessage}:${secretKey}` 22 | // eslint-disable-next-line no-return-await 23 | return await digestMessage(signText) 24 | } 25 | 26 | export const verifySignature = async(payload: AuthPayload, sign: string) => { 27 | // if (Math.abs(payload.t - Date.now()) > 1000 * 60 * 5) { 28 | // return false 29 | // } 30 | const payloadSign = await generateSignature(payload) 31 | return payloadSign === sign 32 | } 33 | -------------------------------------------------------------------------------- /hack/docker-env-replace.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Your API Key for OpenAI 4 | openai_api_key=$OPENAI_API_KEY 5 | # Provide proxy for OpenAI API. e.g. http://127.0.0.1:7890 6 | https_proxy=$HTTPS_PROXY 7 | # Custom base url for OpenAI API. default: https://api.openai.com 8 | openai_api_base_url=$OPENAI_API_BASE_URL 9 | # Inject analytics or other scripts before of the page 10 | head_scripts=$HEAD_SCRIPTS 11 | # Secret string for the project. Use for generating signatures for API calls 12 | secret_key=$SECRET_KEY 13 | # Set password for site, support multiple password separated by comma. If not set, site will be public 14 | site_password=$SITE_PASSWORD 15 | # ID of the model to use. https://platform.openai.com/docs/api-reference/models/list 16 | openai_api_model=$OPENAI_API_MODEL 17 | 18 | for file in $(find ./dist -type f -name "*.mjs"); do 19 | sed "s/({}).OPENAI_API_KEY/\"$openai_api_key\"/g; 20 | s/({}).HTTPS_PROXY/\"$https_proxy\"/g; 21 | s/({}).OPENAI_API_BASE_URL/\"$openai_api_base_url\"/g; 22 | s/({}).HEAD_SCRIPTS/\"$head_scripts\"/g; 23 | s/({}).SECRET_KEY/\"$secret_key\"/g; 24 | s/({}).OPENAI_API_MODEL/\"$openai_api_model\"/g; 25 | s/process.env.SITE_PASSWORD/\"$site_password\"/g" $file > tmp 26 | mv tmp $file 27 | done 28 | 29 | rm -rf tmp 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatgpt-api-demo", 3 | "version": "0.0.1", 4 | "packageManager": "pnpm@7.28.0", 5 | "scripts": { 6 | "dev": "astro dev", 7 | "start": "astro dev", 8 | "build": "astro build", 9 | "build:vercel": "OUTPUT=vercel astro build", 10 | "build:netlify": "OUTPUT=netlify astro build", 11 | "preview": "astro preview", 12 | "astro": "astro", 13 | "lint": "eslint . --ext .js,.jsx,.ts,.tsx,.astro", 14 | "lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx,.astro --fix" 15 | }, 16 | "dependencies": { 17 | "@astrojs/netlify": "2.0.0", 18 | "@astrojs/node": "^5.0.4", 19 | "@astrojs/solid-js": "^2.0.2", 20 | "@astrojs/vercel": "^3.1.3", 21 | "@unocss/reset": "^0.50.1", 22 | "astro": "^2.0.15", 23 | "eslint": "^8.36.0", 24 | "eventsource-parser": "^0.1.0", 25 | "highlight.js": "^11.7.0", 26 | "js-sha256": "^0.9.0", 27 | "katex": "^0.6.0", 28 | "markdown-it": "^13.0.1", 29 | "markdown-it-highlightjs": "^4.0.1", 30 | "markdown-it-katex": "^2.0.3", 31 | "solid-js": "1.6.12", 32 | "solidjs-use": "^1.2.0", 33 | "undici": "^5.20.0" 34 | }, 35 | "devDependencies": { 36 | "@iconify-json/carbon": "^1.1.16", 37 | "@types/markdown-it": "^12.2.3", 38 | "@typescript-eslint/parser": "^5.54.1", 39 | "@unocss/preset-attributify": "^0.50.1", 40 | "@unocss/preset-icons": "^0.50.4", 41 | "@unocss/preset-typography": "^0.50.3", 42 | "eslint-plugin-astro": "^0.24.0", 43 | "punycode": "^2.3.0", 44 | "unocss": "^0.50.1", 45 | "@evan-yang/eslint-config": "^1.0.1", 46 | "vite-plugin-pwa": "^0.14.1" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/pages/password.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Layout from '../layouts/Layout.astro' 3 | --- 4 | 5 | 6 |
7 |
Please input password
8 |
9 | 10 |
11 |
12 |
13 |
14 |
15 |
16 | 17 | 51 | 52 | 72 | -------------------------------------------------------------------------------- /astro.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'astro/config' 2 | import unocss from 'unocss/astro' 3 | import solidJs from '@astrojs/solid-js' 4 | 5 | import node from '@astrojs/node' 6 | import { VitePWA } from 'vite-plugin-pwa' 7 | import vercel from '@astrojs/vercel/edge' 8 | import netlify from '@astrojs/netlify/edge-functions' 9 | import disableBlocks from './plugins/disableBlocks' 10 | 11 | const envAdapter = () => { 12 | if (process.env.OUTPUT === 'vercel') { 13 | return vercel() 14 | } else if (process.env.OUTPUT === 'netlify') { 15 | return netlify() 16 | } else { 17 | return node({ 18 | mode: 'standalone', 19 | }) 20 | } 21 | } 22 | 23 | // https://astro.build/config 24 | export default defineConfig({ 25 | integrations: [ 26 | unocss(), 27 | solidJs(), 28 | ], 29 | output: 'server', 30 | adapter: envAdapter(), 31 | vite: { 32 | plugins: [ 33 | process.env.OUTPUT === 'vercel' && disableBlocks(), 34 | process.env.OUTPUT === 'netlify' && disableBlocks('netlify'), 35 | process.env.OUTPUT !== 'netlify' && VitePWA({ 36 | registerType: 'autoUpdate', 37 | manifest: { 38 | name: 'ChatGPT-API Demo', 39 | short_name: 'ChatGPT Demo', 40 | description: 'A demo repo based on OpenAI API', 41 | theme_color: '#212129', 42 | background_color: '#ffffff', 43 | icons: [ 44 | { 45 | src: 'pwa-192.png', 46 | sizes: '192x192', 47 | type: 'image/png', 48 | }, 49 | { 50 | src: 'pwa-512.png', 51 | sizes: '512x512', 52 | type: 'image/png', 53 | }, 54 | { 55 | src: 'icon.svg', 56 | sizes: '32x32', 57 | type: 'image/svg', 58 | purpose: 'any maskable', 59 | }, 60 | ], 61 | }, 62 | client: { 63 | installPrompt: true, 64 | periodicSyncForUpdates: 20, 65 | }, 66 | devOptions: { 67 | enabled: true, 68 | }, 69 | }), 70 | ], 71 | }, 72 | }) 73 | -------------------------------------------------------------------------------- /src/pages/api/generate.ts: -------------------------------------------------------------------------------- 1 | // #vercel-disable-blocks 2 | import { ProxyAgent, fetch } from 'undici' 3 | // #vercel-end 4 | import { generatePayload, parseOpenAIStream } from '@/utils/openAI' 5 | import { verifySignature } from '@/utils/auth' 6 | import type { APIRoute } from 'astro' 7 | 8 | const apiKey = import.meta.env.OPENAI_API_KEY 9 | const httpsProxy = import.meta.env.HTTPS_PROXY 10 | const baseUrl = ((import.meta.env.OPENAI_API_BASE_URL) || 'https://api.openai.com').trim().replace(/\/$/, '') 11 | const sitePassword = import.meta.env.SITE_PASSWORD || '' 12 | const passList = sitePassword.split(',') || [] 13 | 14 | export const post: APIRoute = async(context) => { 15 | const body = await context.request.json() 16 | const { sign, time, messages, pass } = body 17 | if (!messages) { 18 | return new Response(JSON.stringify({ 19 | error: { 20 | message: 'No input text.', 21 | }, 22 | }), { status: 400 }) 23 | } 24 | if (sitePassword && !(sitePassword === pass || passList.includes(pass))) { 25 | return new Response(JSON.stringify({ 26 | error: { 27 | message: 'Invalid password.', 28 | }, 29 | }), { status: 401 }) 30 | } 31 | if (import.meta.env.PROD && !await verifySignature({ t: time, m: messages?.[messages.length - 1]?.content || '' }, sign)) { 32 | return new Response(JSON.stringify({ 33 | error: { 34 | message: 'Invalid signature.', 35 | }, 36 | }), { status: 401 }) 37 | } 38 | const initOptions = generatePayload(apiKey, messages) 39 | // #vercel-disable-blocks 40 | if (httpsProxy) 41 | initOptions.dispatcher = new ProxyAgent(httpsProxy) 42 | // #vercel-end 43 | 44 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 45 | // @ts-expect-error 46 | const response = await fetch(`${baseUrl}/v1/chat/completions`, initOptions).catch((err: Error) => { 47 | console.error(err) 48 | return new Response(JSON.stringify({ 49 | error: { 50 | code: err.name, 51 | message: err.message, 52 | }, 53 | }), { status: 500 }) 54 | }) as Response 55 | 56 | return parseOpenAIStream(response) as Response 57 | } 58 | -------------------------------------------------------------------------------- /src/utils/openAI.ts: -------------------------------------------------------------------------------- 1 | import { createParser } from 'eventsource-parser' 2 | import type { ParsedEvent, ReconnectInterval } from 'eventsource-parser' 3 | import type { ChatMessage } from '@/types' 4 | 5 | const model = import.meta.env.OPENAI_API_MODEL || 'gpt-3.5-turbo' 6 | 7 | export const generatePayload = (apiKey: string, messages: ChatMessage[]): RequestInit & { dispatcher?: any } => ({ 8 | headers: { 9 | 'Content-Type': 'application/json', 10 | 'Authorization': `Bearer ${apiKey}`, 11 | }, 12 | method: 'POST', 13 | body: JSON.stringify({ 14 | model, 15 | messages, 16 | temperature: 0.6, 17 | stream: true, 18 | }), 19 | }) 20 | 21 | export const parseOpenAIStream = (rawResponse: Response) => { 22 | const encoder = new TextEncoder() 23 | const decoder = new TextDecoder() 24 | if (!rawResponse.ok) { 25 | return new Response(rawResponse.body, { 26 | status: rawResponse.status, 27 | statusText: rawResponse.statusText, 28 | }) 29 | } 30 | 31 | const stream = new ReadableStream({ 32 | async start(controller) { 33 | const streamParser = (event: ParsedEvent | ReconnectInterval) => { 34 | if (event.type === 'event') { 35 | const data = event.data 36 | if (data === '[DONE]') { 37 | controller.close() 38 | return 39 | } 40 | try { 41 | // response = { 42 | // id: 'chatcmpl-6pULPSegWhFgi0XQ1DtgA3zTa1WR6', 43 | // object: 'chat.completion.chunk', 44 | // created: 1677729391, 45 | // model: 'gpt-3.5-turbo-0301', 46 | // choices: [ 47 | // { delta: { content: '你' }, index: 0, finish_reason: null } 48 | // ], 49 | // } 50 | const json = JSON.parse(data) 51 | const text = json.choices[0].delta?.content || '' 52 | const queue = encoder.encode(text) 53 | controller.enqueue(queue) 54 | } catch (e) { 55 | controller.error(e) 56 | } 57 | } 58 | } 59 | 60 | const parser = createParser(streamParser) 61 | for await (const chunk of rawResponse.body as any) 62 | parser.feed(decoder.decode(chunk)) 63 | }, 64 | }) 65 | 66 | return new Response(stream) 67 | } 68 | -------------------------------------------------------------------------------- /src/components/SystemRoleSettings.tsx: -------------------------------------------------------------------------------- 1 | import { Show } from 'solid-js' 2 | import IconEnv from './icons/Env' 3 | import IconX from './icons/X' 4 | import type { Accessor, Setter } from 'solid-js' 5 | 6 | interface Props { 7 | canEdit: Accessor 8 | systemRoleEditing: Accessor 9 | setSystemRoleEditing: Setter 10 | currentSystemRoleSettings: Accessor 11 | setCurrentSystemRoleSettings: Setter 12 | } 13 | 14 | export default (props: Props) => { 15 | let systemInputRef: HTMLTextAreaElement 16 | 17 | const handleButtonClick = () => { 18 | props.setCurrentSystemRoleSettings(systemInputRef.value) 19 | props.setSystemRoleEditing(false) 20 | } 21 | 22 | return ( 23 |
24 | 25 | 26 |
27 |
28 | }> 29 | props.setCurrentSystemRoleSettings('')} class="sys-edit-btn p-1 rd-50%" > 30 | 31 | System Role: 32 |
33 |
34 | {props.currentSystemRoleSettings()} 35 |
36 |
37 |
38 | 39 | props.setSystemRoleEditing(!props.systemRoleEditing())} class="sys-edit-btn"> 40 | 41 | Add System Role 42 | 43 | 44 |
45 | 46 |
47 |
48 | 49 | System Role: 50 |
51 |

Gently instruct the assistant and set the behavior of the assistant.

52 |
53 |