├── .dockerignore ├── .editorconfig ├── .env ├── .gitignore ├── .prettierrc ├── .vercelignore ├── .vscode └── extensions.json ├── Dockerfile ├── LICENSE ├── README.md ├── README_CN.md ├── api ├── alive.js ├── auth.js ├── index.js └── local │ ├── alive.js │ ├── auth.js │ └── index.js ├── build.sh ├── components.d.ts ├── docs ├── cloudflare.md ├── images │ ├── about.png │ ├── cloudflare.png │ ├── config.png │ ├── logo.png │ ├── report.png │ ├── testing.png │ ├── vercel-1.png │ ├── vercel-2.png │ ├── vercel-3.png │ └── vercel-4.png └── vercel.md ├── index.html ├── package-lock.json ├── package.json ├── public └── logo.png ├── server.js ├── src ├── App.vue ├── assets │ └── logo.png ├── components │ ├── Check.vue │ └── Experimental.vue ├── i18n │ └── index.js ├── locales │ ├── en.json │ └── zh.json ├── main.js ├── router │ └── index.js ├── styles │ └── global.css ├── utils │ ├── api.js │ ├── info.js │ ├── initialization.js │ ├── models.js │ ├── normal.js │ ├── svg.js │ ├── theme.js │ ├── update.js │ └── verify.js └── views │ ├── Home.vue │ └── Layout.vue ├── vercel.json ├── vite.config.js └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | .dockerignore 3 | node_modules 4 | npm-debug.log 5 | .next 6 | .git 7 | .github 8 | *.md 9 | .env.example 10 | 11 | # Logs 12 | logs 13 | *.log 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* 17 | pnpm-debug.log* 18 | lerna-debug.log* 19 | 20 | dist 21 | dist-ssr 22 | *.local 23 | 24 | # Editor directories and files 25 | .vscode/* 26 | !.vscode/extensions.json 27 | .idea 28 | .DS_Store 29 | *.suo 30 | *.ntvs* 31 | *.njsproj 32 | *.sln 33 | *.sw? 34 | .vercel 35 | .vscode 36 | .history 37 | .temp 38 | .env.local 39 | venv 40 | temp 41 | tmp 42 | tput 43 | api/local/data.json 44 | 45 | 46 | docs 47 | build.sh 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [Makefile] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | PASSWORD=api-check 2 | -------------------------------------------------------------------------------- /.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 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | .vercel 26 | .vscode 27 | .history 28 | .temp 29 | venv 30 | temp 31 | tmp 32 | tput 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "singleQuote": true, 6 | "trailingComma": "es5", 7 | "bracketSpacing": true, 8 | "jsxBracketSameLine": false, 9 | "semi": true, 10 | "arrowParens": "avoid" 11 | } 12 | -------------------------------------------------------------------------------- /.vercelignore: -------------------------------------------------------------------------------- 1 | /docker-compose.yml 2 | /Dockerfile 3 | /install.sh 4 | /LICENSE 5 | /docs 6 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar"] 3 | } 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # 阶段1:基础镜像准备 2 | FROM node:18-alpine AS base 3 | 4 | # 设置工作目录 5 | WORKDIR /app 6 | 7 | # 配置国内镜像源 8 | RUN npm config set registry https://registry.npmmirror.com/ 9 | 10 | # 安装必要的系统依赖(例如 CA 证书) 11 | RUN apk add --no-cache ca-certificates 12 | 13 | # 阶段2:构建应用程序 14 | FROM base AS builder 15 | 16 | WORKDIR /app 17 | 18 | # 复制依赖文件 19 | COPY package.json yarn.lock ./ 20 | 21 | # 安装所有依赖,包括开发依赖 22 | RUN yarn install 23 | 24 | # 复制项目源代码 25 | COPY . . 26 | 27 | # 构建应用程序 28 | RUN yarn build 29 | 30 | # 删除 node_modules 目录 31 | RUN rm -rf node_modules 32 | 33 | # 设置 NODE_ENV 为 production 34 | ENV NODE_ENV=production 35 | 36 | # 安装生产依赖 37 | RUN yarn install --production --ignore-scripts --prefer-offline 38 | 39 | # 清理 yarn 缓存 40 | RUN yarn cache clean --all 41 | 42 | # 阶段3:构建最终的生产镜像 43 | FROM node:18-alpine 44 | 45 | # 设置工作目录 46 | WORKDIR /app 47 | 48 | # 创建非 root 用户 49 | RUN addgroup -g 1001 appgroup && \ 50 | adduser -D -u 1001 -G appgroup appuser 51 | 52 | # 复制应用程序文件 53 | COPY --from=builder /app/server.js /app/server.js 54 | COPY --from=builder /app/dist /app/dist 55 | COPY --from=builder /app/api /app/api 56 | COPY --from=builder /app/node_modules /app/node_modules 57 | COPY --from=builder /app/package.json /app/package.json 58 | 59 | # 修改文件权限,使 appuser 拥有所有权 60 | RUN chown -R appuser:appgroup /app 61 | 62 | # 设置环境变量 63 | ENV NODE_ENV=production 64 | ENV HOSTNAME="0.0.0.0" 65 | ENV PORT=13000 66 | ENV NODE_OPTIONS="--dns-result-order=ipv4first --use-openssl-ca" 67 | 68 | # 暴露端口 69 | EXPOSE 13000 70 | 71 | # 使用非 root 用户 72 | USER appuser 73 | 74 | # 启动命令 75 | CMD ["node", "server.js"] 76 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | logo.png 3 | 4 | # API CHECK 5 | 6 |
7 | 8 | **English** · [简体中文](./README_CN.md) 9 | 10 | > [!TIP] 11 | > Click to try: https://check.crond.dev 12 | 13 | ## Pure Front-End API Testing Tool 14 | 15 | - ✅ **Supports Liveness Testing for Various OpenAI API Proxies** 16 | 17 | - Compatible with OpenAI proxy APIs like oneapi and newapi, fully testing availability. 18 | 19 | - 🔒 **Pure Front-End Version for Enhanced Data Security** 20 | 21 | - All operations are performed on the front end, eliminating concerns about network timeouts and ensuring data security. 22 | 23 | - 📊 **Detailed Testing Data** 24 | 25 | - Displays response time, model consistency, and more, making test results clear at a glance. 26 | 27 | - 💾 **Cloud Storage and Local Storage** 28 | 29 | - **Cloud Storage**: Save configurations to the cloud for multi-device sharing. 30 | - **Local Storage**: Save frequently used configurations locally for quick loading and convenience. 31 | 32 | - 🌙 **Theme and Language Switching** 33 | 34 | - **Dark/Light Mode**: Choose a theme that suits you to protect your eyesight. 35 | - **Multi-Language Support**: Supports Chinese and English to meet different language needs. 36 | 37 | - 🖥️ **Multiple Deployment Methods** 38 | - **Vercel Deployment**: Supports one-click deployment to Vercel for convenience. 39 | - **Docker Deployment** 40 | - **Cloudflare Deployment** 41 | 42 | ## 📦 Getting Started 43 | 44 | ### Vercel Deployment 45 | 46 | 1. Click the button on the right to start deployment: 47 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/october-coder/api-check&env=PASSWORD&project-name=api-check&repository-name=api-check). Simply log in using your GitHub account, and remember to fill in the backend password on the environment variables page. 48 | 2. After deployment, you can start using it. 49 | 3. (Optional) To deploy the backend service, please refer to the [Detailed Tutorial](./docs/vercel.md). 50 | 4. (Optional) [Bind a Custom Domain Name](https://vercel.com/docs/concepts/projects/domains/add-a-domain): The domain name assigned by Vercel may be polluted in some regions. Binding a custom domain name allows direct access. 51 | 52 | ### Docker Deployment 53 | 54 | 1. One-click deployment command 55 | 56 | 2. ```bash 57 | docker run -d -p 13000:13000 \ 58 | -e PASSWORD=you_password \ 59 | -v you_path:/app/data \ 60 | --name api-check ghcr.io/rickcert/api-check:latest 61 | ``` 62 | 63 | ### Cloudflare Backend Deployment 64 | 65 | 1. Refer to the [Detailed Tutorial](./docs/cloudflare.md). 66 | 2. It's best to bind a custom domain name. 67 | 68 | ## 📜 Recent Updates 69 | 70 | Testing 71 | 72 | ### v2.1.0 73 | 74 | 🔔 **New Features and Optimizations** 75 | 76 | - ✨ **Added Quick Chat Testing** 77 | - Integrated with the modified NextChat for quick model testing. 78 | - Added `closeChat` setting for convenient proxy usage. 79 | - 🧪 **Added Experimental Features Module** from [elfmaid](https://linux.do/u/elfmaid) 80 | - Batch testing of GPT Refresh Tokens 81 | - Batch testing of Claude Session Keys 82 | - Batch testing of Gemini API Keys 83 | - ✂️ **Added Paste Button** by [fangyuan](https://linux.do/u/fangyuan99) 84 | - 📝 **Added Custom Conversation Verification** 85 | - Quick prompt testing by [fangyuan](https://linux.do/u/fangyuan99) 86 | 87 | 🔧 **Optimizations and Fixes** 88 | 89 | - 🐳 **Optimized Dockerfile** to reduce image size. 90 | - 🎨 **Fixed Layout Issues** to improve interface display. 91 | 92 | ### v2.0.0 93 | 94 | 🔔 **Brand New Features and Optimizations** 95 | 96 | - 🌐 **Added Cloud Storage and Local Storage** 97 | - **Cloud Storage**: Supports saving API configuration information to the cloud server for multi-device synchronization, allowing you to access your configurations anytime, anywhere. 98 | - **Local Storage**: Provides a local caching function for quick local saves, avoiding repeated inputs and improving efficiency. 99 | - **Data Management**: Added a settings panel for easy management of local and cloud configuration data. 100 | - ✨ **Supports Preset Parameters** 101 | - **Convenient One-Click Configuration** 102 | - **Quickly Bind to new-api** 103 | - 💻 **Supports One-Click Deployment with Vercel and Docker** 104 | - 🌙 **Added Dark Mode** 105 | - **Theme Switching**: Supports switching between dark and light modes to suit different environments and user preferences. 106 | - **Automatic Adaptation**: Can automatically switch themes based on system settings to protect your eyesight. 107 | - 🌐 **Internationalization Support** 108 | - **Multi-Language**: Added internationalization support, currently supporting Chinese and English. 109 | - 📱 **Mobile Adaptation Optimization** 110 | - 🛠 **Other Optimizations and Fixes** 111 | 112 | ### 🧪 Version History 113 | 114 |
115 | 116 | ### v1.5.0 117 | 118 | - 📱 Adapted for Mobile Mode 119 | - 🌙 Added Dark Theme 120 | - 🧠 Optimized o1 Model Testing 121 | 122 | ### v1.4.0 123 | 124 | - 🔍 Added Temperature Verification 125 | - 📊 Added Function Verification 126 | - 🔧 Optimized Test Prompts 127 | 128 | ### v1.3.0 129 | 130 | - 🔍 Added Official API Verification 131 | - 🖥️ Supports Filtering Queries 132 | 133 | ### v1.2.0 134 | 135 | - 🖥️ Added Local One-Click Run 136 | - 🌐 Supports Pages Online Hosting 137 | - 📊 Improved Test Result Display 138 | 139 | ### v1.0.0 140 | 141 | - ✨ Supports Multi-Model Testing 142 | - 💰 Added Quota Check 143 | - 📋 Implemented Model List Retrieval 144 | 145 |
146 | 147 | ## 📋 Feature Introduction 148 | 149 | - 🧪 Test the availability and consistency of multiple models 150 | - 💰 Check API account usage quota 151 | - 📋 Retrieve and display the list of available models 152 | - 📝 Intelligent extraction of API information 153 | - 🖱️ Convenient copy function 154 | - 💾 Cloud storage and local caching 155 | - 🌙 Theme and language switching 156 | - 🛠 Advanced Verification Features 157 | 158 | - **Official Proxy Verification**: Verify the authenticity of the API and view system fingerprints. 159 | - **Temperature Verification**: Verify the randomness and stability of the model. 160 | - **Function Call Verification**: Test the model's function-calling capabilities. 161 | 162 | ### 🛠 Cloud Storage 163 | 164 | - **Docker Deployment** backend URL: Please use `https://your_website/api` 165 | - **Vercel Deployment** backend URL: Please use `https://your_website/api` 166 | - **Cloudflare Deployment** backend URL: Please use `https://your_website` 167 | 168 | ### 🛠 Preset Parameter Settings 169 | 170 | Test Report 171 | 172 | 🔗 url 173 | 174 | - **Description**: API endpoint address. 175 | - **Example**: `"url": "https://api.example.com"` 176 | 177 | 📦 models 178 | 179 | - **Description**: An array of model names indicating which models are available. 180 | - **Example**: `"models": ["model1", "model2"]` 181 | 182 | ⏱ timeout 183 | 184 | - **Description**: Request timeout in seconds. 185 | - **Example**: `"timeout": 30` 186 | 187 | 🔁 concurrency 188 | 189 | - **Description**: Number of concurrent requests. 190 | - **Example**: `"concurrency": 5` 191 | 192 | 🚫 closeAnnouncement **Convenient for Proxy Sites** 193 | 194 | - **Description**: Whether to disable the announcement display. Set to `true` to disable, or `false`/undefined to display announcements. **Convenient for proxy sites** 195 | - **Example**: `"closeAnnouncement": true` 196 | 197 | 🚪 closeChat **Convenient for Proxy Sites** 198 | 199 | - **Description**: Whether to disable the quick chat function. Set to `true` to disable, or `false`/undefined to enable the chat function. 200 | - **Example**: `"closeChat": true` 201 | 202 | ```url 203 | https://check.crond.dev/?settings={"key":"*sk*","url":"*api*","models":["gpt-4o-mini","gpt-4o"],"timeout":10,"concurrency":2,"closeAnnouncement":true,"closeChat":true} 204 | ``` 205 | 206 | Decoded JSON string: 207 | 208 | ```json 209 | { 210 | "key": "your_api_key", 211 | "url": "https://api.example.com", 212 | "models": ["gpt-4o-mini", "gpt-4o"], 213 | "timeout": 10, 214 | "concurrency": 2, 215 | "closeAnnouncement": true, 216 | "closeChat": true 217 | } 218 | ``` 219 | 220 | - **voapi** Example 221 | 222 | ```json 223 | { 224 | "name": "check", 225 | "link": "https://check.crond.dev/?settings={%22key%22:%22*sk*%22,%22url%22:%22*api*%22,%22models%22:[%22gpt-4o-mini%22],%22timeout%22:10,%22concurrency%22:2,%22closeAnnouncement%22:true,%22closeChat%22:true}", 226 | "icon": "https://check.crond.dev/logo.png" 227 | } 228 | ``` 229 | 230 | - **newapi** Example 231 | 232 | ```json 233 | { 234 | "CHECK": "https://check.crond.dev/?settings={\"key\":\"{key}\",\"url\":\"{address}\",\"models\":[\"gpt-4o-mini\"],\"timeout\":10,\"concurrency\":2,\"closeAnnouncement\":true,\"closeChat\":true}" 235 | } 236 | ``` 237 | 238 | ### 🛠 **Advanced Verification Features** 239 | 240 | #### 🕵️ Official API Verification 241 | 242 | 1. 🔄 Send multiple identical requests. 243 | 2. 📊 Analyze the consistency of the responses. 244 | 3. 🔍 Check system fingerprints. 245 | 4. 🧮 Calculate similarity scores. 246 | 247 | #### 🕵️‍♀️ Temperature Verification 248 | 249 | 1. 🧊 Set the temperature parameter to a low value (0.01). 250 | 2. 🔄 Send multiple identical requests (e.g., calculating the next number in a specific sequence). 251 | 3. 🎯 Check the hit rate based on the official API's reference values. 252 | 253 | ### 🛠 Generate Reports 254 | 255 | Test Report 256 | 257 | ## 🤝 Contributing 258 | 259 | We welcome suggestions and improvements! Feel free to submit pull requests or open issues. Let's make this tool even better together! 🌈 260 | 261 | ## 📜 License 262 | 263 | This project is licensed under the [Apache License 2.0](https://opensource.org/license/apache-2-0). 264 | 265 | ## 🙏 Acknowledgments 266 | 267 | Special thanks to the following contributors whose efforts have made this project better: 268 | 269 | - [Rick](https://linux.do/u/rick) 270 | - [Megasoft](https://linux.do/u/zhong_little) 271 | - [fangyuan99](https://linux.do/u/fangyuan99) 272 | - [juzeon](https://github.com/juzeon) 273 | 274 | ## Star History 275 | 276 | [![Star History Chart](https://api.star-history.com/svg?repos=october-coder/api-check&type=Date)](https://star-history.com/#october-coder/api-check&Date) 277 | 278 | [![image](iframe组件截图图片链接)](https://yxvm.com/) 279 | [NodeSupport](https://github.com/NodeSeekDev/NodeSupport)赞助了本项目 280 | -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 |
2 | logo.png 3 | 4 | # API CHECK 5 | 6 |
7 | 8 | > [!TIP] 9 | > 点击体验 : https://check.crond.dev 10 | 11 | ## 纯前端 API 检测工具 12 | 13 | - ✅ **支持各种 OpenAI API 中转服务的测活** 14 | 15 | - 兼容 oneapi、newapi 等中转 OpenAI 格式的 API,全面检测可用性。 16 | 17 | - 🔒 **纯前端版本,数据更安全** 18 | 19 | - 所有操作均在前端完成,无需担心网络超时,确保数据安全。 20 | 21 | - 📊 **详细的测活数据** 22 | 23 | - 显示响应时间、模型一致性等信息,测试结果一目了然。 24 | 25 | - 💾 **云端存储与本地存储** 26 | 27 | - **云端存储**:配置可保存至云端,实现多设备共享。 28 | - **本地存储**:常用配置本地保存,快速加载,方便快捷。 29 | 30 | - 🌙 **主题和语言切换** 31 | 32 | - **深色/浅色模式**:根据喜好选择适合的主题,保护视力。 33 | - **多语言支持**:支持中文和英文,满足不同语言需求。 34 | 35 | - 🖥️ **多种部署方式** 36 | - **Vercel 部署**:支持一键部署到 Vercel,方便快捷。 37 | - **Docker 部署** 38 | - **Cloudflare 部署** 39 | 40 | ## 📦开始使用 41 | 42 | ### vercel 部署 43 | 44 | 1. 点击右侧按钮开始部署: 45 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/october-coder/api-check&env=PASSWORD&project-name=api-check&repository-name=api-check),直接使用 Github 账号登录即可,记得在环境变量页填入 后端密码; 46 | 2. 部署完毕后,即可开始使用; 47 | 3. (可选)部署后端服务 请参考 [详细教程](./docs/vercel.md)。 48 | 4. (可选)[绑定自定义域名](https://vercel.com/docs/concepts/projects/domains/add-a-domain):Vercel 分配的域名 DNS 在某些区域被污染了,绑定自定义域名即可直连。 49 | 50 | ### docker 部署 51 | 52 | 1. 一键部署命令 53 | 54 | 2. ``` 55 | docker run -d -p 13000:13000 \ 56 | -e PASSWORD=you_password \ 57 | -v you_path:/app/data \ 58 | --name api-check ghcr.io/rickcert/api-check:latest 59 | ``` 60 | 61 | ### cloudflare 部署后端 62 | 63 | 1. 参考 [详细教程](./docs/cloudflare.md)。 64 | 2. 最好绑定自定义域名。 65 | 66 | ## 📜最近更新 67 | 68 | 测试 69 | 70 | ### v2.1.0 71 | 72 | 🔔 **新特性与优化** 73 | 74 | - ✨ **新增快捷聊天测试** 75 | - 对接魔改 NextChat,可快捷测试模型。 76 | - 新增 `closeChat` 设置,方便中转站使用。 77 | - 🧪 **添加实验性功能模块** from [elfmaid](https://linux.do/u/elfmaid) 78 | - 批量测试 gpt Refresh Tokens 79 | - 批量测试 claude Session Keys 80 | - 批量测试 gemini API Keys 81 | - ✂️ **新增粘贴按钮 ** by [fangyuan](https://linux.do/u/fangyuan99) 82 | - 📝 **新增自定义对话验证功能** 83 | - 快捷prompt测试 by [fangyuan](https://linux.do/u/fangyuan99) 84 | 85 | 🔧 **优化与修复** 86 | 87 | - 🐳 **优化 Dockerfile** 减小镜像体积。 88 | 89 | - 🎨 **修复布局问题** 改善界面显示 90 | 91 | ### v2.0.0 92 | 93 | 🔔 **全新特性与优化** 94 | 95 | - 🌐 **新增云端存储和本地存储功能** 96 | - **云端存储**:支持将 API 配置信息保存到云端服务器,实现多设备同步,随时随地访问您的配置。 97 | - **本地存储**:提供本地缓存功能,快捷保存到本地,避免重复输入,提高使用效率。 98 | - **数据管理**:新增设置面板,方便管理本地和云端的配置数据。 99 | - ✨**支持预设参数** 100 | - **一键配置方便** 101 | - **快速绑定到 new-api** 102 | - 💻 **支持 Vercel Docker 一键部署** 103 | - 🌙 **新增暗黑模式** 104 | - **主题切换**:支持深色模式和浅色模式的切换,适应不同环境和用户偏好。 105 | - **自动适配**:可以根据系统设置自动切换主题,保护您的视力。 106 | - 🌐 **国际化支持** 107 | - **多语言**:新增国际化支持,现已支持中文和英文。 108 | - 📱 **移动端适配优化** 109 | - 🛠 **其他优化和修复** 110 | 111 | ### 🧪 版本历史 112 | 113 |
114 | 115 | ### v1.5.0 116 | 117 | - 📱 适配手机模式 118 | - 🌙 新增暗黑主题 119 | - 🧠 优化o1模型测试 120 | 121 | ### v1.4.0 122 | 123 | - 🔍 新增温度验证功能 124 | - 📊 新增函数验证功能 125 | - 🔧 优化测试提示 126 | 127 | ### v1.3.0 128 | 129 | - 🔍 新增官方API验证功能 130 | - 🖥️ 支持筛选查询 131 | 132 | ### v1.2.0 133 | 134 | - 🖥️ 添加本地一键运行功能 135 | - 🌐 支持pages在线托管 136 | - 📊 改进测试结果展示 137 | 138 | ### v1.0.0 139 | 140 | - ✨ 支持多模型测试 141 | - 💰 添加额度检查功能 142 | - 📋 实现模型列表获取 143 |
144 | 145 | ## 📋 功能介绍 146 | 147 | - 🧪 测试多个模型的可用性和一致性 148 | - 💰 检查API账户使用额度 149 | - 📋 获取并显示可用模型列表 150 | - 📝 智能提取API信息 151 | - 🖱️ 便捷的复制功能 152 | - 💾 云端存储和本地缓存 153 | - 🌙 主题和语言切换 154 | - 🛠 高级验证功能 155 | 156 | - **官转验证**:验证 API 的真实性,查看系统指纹。 157 | - **温度验证**:验证模型的随机性和稳定性。 158 | - **函数调用验证**:测试模型的函数调用能力。 159 | 160 | ### 🛠 云端存储 161 | 162 | - **Docker 部署** 后端url 请使用 https://your_website/api 163 | - **Vercel 部署** 后端url 请使用 https://your_website/api 164 | - **Cloudflare 部署** 后端url 请使用 https://your_website 165 | 166 | ### 🛠 预设参数设置 167 | 168 | 上测试报告 169 | 170 | 🔗 url 171 | 172 | - **描述**: API 接口地址。 173 | - **示例**: `"url": "https://api.example.com"` 174 | 175 | 📦 models 176 | 177 | - **描述**: 模型名称数组,表示可以使用的模型。 178 | - **示例**: `"models": ["model1", "model2"]` 179 | 180 | ⏱ timeout 181 | 182 | - **描述**: 请求超时时间(以秒为单位)。 183 | - **示例**: `"timeout": 30` 184 | 185 | 🔁 concurrency 186 | 187 | - **描述**: 并发请求的数量。 188 | - **示例**: `"concurrency": 5` 189 | 190 | 🚫 closeAnnouncement **方便中转站使用** 191 | 192 | - **描述**: 是否关闭公告显示。设置为 `true` 时关闭公告显示,设置为 `false` 或未定义时显示公告。 **方便中转站使用** 193 | - **示例**: `"closeAnnouncement": true` 194 | 195 | 🚪 closeChat **方便中转站使用** 196 | 197 | - **描述**:是否关闭快捷聊天功能。设置为 `true` 时关闭聊天功能,设置为 `false` 或未定义时开启聊天功能。 198 | - **示例**:`"closeChat": true` 199 | 200 | ``` 201 | https://check.crond.dev/?settings={"key":"*sk*","url":"*api*","models":["gpt-4o-mini","gpt-4o"],"timeout":10,"concurrency":2,"closeAnnouncement":true,"closeChat":true} 202 | ``` 203 | 204 | 解码后的 JSON 字符串如下: 205 | 206 | ```json 207 | { 208 | "key": "your_api_key", 209 | "url": "https://api.example.com", 210 | "models": ["gpt-4o-mini", "gpt-4o"], 211 | "timeout": 10, 212 | "concurrency": 2, 213 | "closeAnnouncement": true, 214 | "closeChat": true 215 | } 216 | ``` 217 | 218 | - **voapi** 示例 219 | 220 | ``` 221 | { 222 | "name": "check", 223 | "link": "https://check.crond.dev/?settings={%22key%22:%22*sk*%22,%22url%22:%22*api*%22,%22models%22:[%22gpt-4o-mini%22],%22timeout%22:10,%22concurrency%22:2,%22closeAnnouncement%22:true,%22closeChat%22:true}", 224 | "icon": "https://check.crond.dev/logo.png" 225 | } 226 | 227 | ``` 228 | 229 | - **newapi** 示例 230 | 231 | ``` 232 | { 233 | "CHECK": "https://check.crond.dev/?settings={\"key\":\"{key}\",\"url\":\"{address}\",\"models\":[\"gpt-4o-mini\"],\"timeout\":10,\"concurrency\":2,\"closeAnnouncement\":true,\"closeChat\":true}" 234 | } 235 | 236 | ``` 237 | 238 | ### 🛠 **高级验证功能** 239 | 240 | #### 🕵️ 官方API验证 241 | 242 | 1. 🔄 发送多个相同的请求 243 | 2. 📊 分析响应的一致性 244 | 3. 🔍 检查系统指纹 245 | 4. 🧮 计算相似度得分 246 | 247 | #### 🕵️‍♀️ 温度验证 248 | 249 | 1. 🧊 设置低温度参数(0.01) 250 | 2. 🔄 发送多个相同的请求(计算某个指定序列的下一个数) 251 | 3. 🎯 根据官方api参考值,检测命中率 252 | 253 | ### 🛠生成报告 254 | 255 | 上测试报告 256 | 257 | ## 🤝 贡献 258 | 259 | 欢迎提出建议和改进!随时提交 pull requests 或开启 issues。让我们一起让这个工具变得更棒! 🌈 260 | 261 | ## 📜 许可证 262 | 263 | 本项目采用[Apache](https://opensource.org/license/apache-2-0)文件。 264 | 265 | ## 🙏 致谢 266 | 267 | 特别感谢以下贡献者,他们的努力使这个项目变得更好: 268 | 269 | - [Rick](https://linux.do/u/rick) 270 | - [Megasoft](https://linux.do/u/zhong_little) 271 | - [fangyuan99](https://linux.do/u/fangyuan99) 272 | - [juzeon](https://github.com/juzeon) 273 | 274 | ## Star History 275 | 276 | [![Star History Chart](https://api.star-history.com/svg?repos=october-coder/api-check&type=Date)](https://star-history.com/#october-coder/api-check&Date) 277 | -------------------------------------------------------------------------------- /api/alive.js: -------------------------------------------------------------------------------- 1 | const corsHeaders = { 2 | 'Access-Control-Allow-Origin': '*', 3 | 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', 4 | 'Access-Control-Allow-Headers': 'Content-Type', 5 | }; 6 | 7 | export default async function (req, res) { 8 | console.log('Received request:', req.method, req.url); 9 | 10 | if (req.method === 'OPTIONS') { 11 | res.writeHead(204, corsHeaders); 12 | res.end(); 13 | return; 14 | } 15 | 16 | if (req.method !== 'POST') { 17 | res.writeHead(404, { 'Content-Type': 'text/plain', ...corsHeaders }); 18 | res.end('Not Found'); 19 | return; 20 | } 21 | 22 | try { 23 | let body = ''; 24 | for await (const chunk of req) { 25 | body += chunk; 26 | } 27 | console.log('Request body:', body); 28 | 29 | const content = JSON.parse(body); 30 | console.log('Parsed request content:', content); 31 | 32 | const { type } = content; 33 | let responseBody; 34 | 35 | switch (type) { 36 | case 'refreshTokens': 37 | responseBody = await handleRefreshTokens(content.tokens); 38 | break; 39 | case 'sessionKeys': 40 | responseBody = await handleSessionKeys(content); 41 | break; 42 | case 'geminiAPI': 43 | responseBody = await handleGeminiAPI(content); 44 | break; 45 | default: 46 | res.writeHead(400, { 47 | 'Content-Type': 'application/json', 48 | ...corsHeaders, 49 | }); 50 | res.end(JSON.stringify({ error: 'Invalid request type' })); 51 | return; 52 | } 53 | 54 | res.writeHead(200, { 55 | 'Content-Type': 'application/json', 56 | ...corsHeaders, 57 | }); 58 | res.end(JSON.stringify(responseBody)); 59 | } catch (error) { 60 | console.error('Error processing request:', error); 61 | res.writeHead(500, { 62 | 'Content-Type': 'application/json', 63 | ...corsHeaders, 64 | }); 65 | res.end(JSON.stringify({ error: error.message })); 66 | } 67 | } 68 | 69 | async function handleRefreshTokens(tokens) { 70 | try { 71 | const results = await Promise.all(tokens.map(checkTokenValidity)); 72 | return results; 73 | } catch (error) { 74 | throw new Error('Error processing refresh tokens: ' + error.message); 75 | } 76 | } 77 | 78 | async function checkTokenValidity(refreshToken) { 79 | try { 80 | const response = await fetch('https://token.oaifree.com/api/auth/refresh', { 81 | method: 'POST', 82 | headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 83 | body: 'refresh_token=' + encodeURIComponent(refreshToken), 84 | }); 85 | 86 | if (response.ok) { 87 | const data = await response.json(); 88 | return { 89 | refreshToken, 90 | accessToken: data.access_token, 91 | valid: true, 92 | }; 93 | } else { 94 | return { 95 | refreshToken, 96 | accessToken: null, 97 | valid: false, 98 | }; 99 | } 100 | } catch (error) { 101 | return { 102 | refreshToken, 103 | accessToken: null, 104 | valid: false, 105 | }; 106 | } 107 | } 108 | 109 | async function handleSessionKeys(content) { 110 | const sessionKeys = content.tokens; 111 | const maxAttempts = content.maxAttempts; 112 | const requestsPerSecond = content.requestsPerSecond; 113 | const delayBetweenRequests = 1000 / requestsPerSecond; 114 | 115 | const delay = ms => new Promise(resolve => setTimeout(resolve, ms)); 116 | 117 | async function checkSessionKey(sessionKey) { 118 | let attempts = 0; 119 | let successCount = 0; 120 | 121 | while (attempts < maxAttempts) { 122 | attempts++; 123 | try { 124 | const response = await fetch( 125 | 'https://api.claude.ai/api/organizations', 126 | { 127 | headers: { 128 | accept: 'application/json', 129 | cookie: 'sessionKey=' + sessionKey, 130 | 'user-agent': 'Mozilla/5.0 (X11; Linux x86_64)', 131 | }, 132 | } 133 | ); 134 | 135 | if (!response.ok) { 136 | throw new Error('HTTP error! status: ' + response.status); 137 | } 138 | 139 | const responseText = await response.text(); 140 | 141 | if ( 142 | responseText.toLowerCase().includes('unauthorized') || 143 | responseText.trim() === '' 144 | ) { 145 | throw new Error('Invalid response'); 146 | } 147 | 148 | const organizations = JSON.parse(responseText); 149 | successCount++; 150 | 151 | const name = organizations[0].name || 'Unknown'; 152 | const capabilities = organizations[0].capabilities 153 | ? organizations[0].capabilities.join(';') 154 | : ''; 155 | return { 156 | sessionKey, 157 | name, 158 | capabilities, 159 | available: true, 160 | attempts, 161 | successRate: successCount / attempts, 162 | }; 163 | } catch (error) { 164 | if (attempts >= maxAttempts) { 165 | return { 166 | sessionKey, 167 | name: 'Invalid', 168 | capabilities: '', 169 | available: false, 170 | attempts, 171 | successRate: successCount / attempts, 172 | }; 173 | } 174 | await delay(delayBetweenRequests); 175 | } 176 | } 177 | } 178 | 179 | try { 180 | const results = await Promise.all(sessionKeys.map(checkSessionKey)); 181 | return results; 182 | } catch (error) { 183 | throw new Error('Error processing session keys: ' + error.message); 184 | } 185 | } 186 | 187 | async function handleGeminiAPI(content) { 188 | const apiKeys = content.tokens; 189 | const model = content.model; 190 | const rateLimit = content.rateLimit; 191 | const prompt = content.prompt; 192 | const user = content.user; 193 | 194 | if (!apiKeys || !Array.isArray(apiKeys) || apiKeys.length === 0) { 195 | throw new Error('Invalid or empty API keys'); 196 | } 197 | 198 | try { 199 | const results = await batchTestAPI(apiKeys, model, rateLimit, prompt, user); 200 | const validKeys = results.filter(r => r.valid).map(r => r.key); 201 | const invalidKeys = results.filter(r => !r.valid).map(r => r.key); 202 | const errors = results 203 | .filter(r => r.error) 204 | .map(r => ({ key: r.key, error: r.error })); 205 | const validResults = results.filter(r => r.valid && r.data); 206 | 207 | return { 208 | valid: validKeys.length, 209 | invalid: invalidKeys.length, 210 | invalidKeys: invalidKeys, 211 | errors: errors, 212 | validResults: validResults, 213 | }; 214 | } catch (error) { 215 | throw new Error('Error testing APIs: ' + error.message); 216 | } 217 | } 218 | 219 | async function batchTestAPI(apiKeys, model, rateLimit, prompt, user) { 220 | const results = []; 221 | const delayBetweenRequests = 1000 / rateLimit; 222 | 223 | const delay = ms => new Promise(resolve => setTimeout(resolve, ms)); 224 | 225 | for (let i = 0; i < apiKeys.length; i++) { 226 | const apiKey = apiKeys[i]; 227 | try { 228 | const result = await testAPI(apiKey, model, prompt, user); 229 | results.push(result); 230 | } catch (error) { 231 | results.push({ 232 | key: apiKey, 233 | valid: false, 234 | error: error.message, 235 | }); 236 | } 237 | await delay(delayBetweenRequests); 238 | } 239 | 240 | return results; 241 | } 242 | 243 | async function testAPI(apiKey, model, prompt, user) { 244 | const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`; 245 | 246 | try { 247 | const response = await fetch(url, { 248 | method: 'POST', 249 | headers: { 250 | 'Content-Type': 'application/json', 251 | }, 252 | body: JSON.stringify({ 253 | contents: [ 254 | { 255 | parts: [{ text: prompt }, { text: user }], 256 | }, 257 | ], 258 | safetySettings: [ 259 | { 260 | category: 'HARM_CATEGORY_DANGEROUS_CONTENT', 261 | threshold: 'BLOCK_NONE', 262 | }, 263 | { category: 'HARM_CATEGORY_HATE_SPEECH', threshold: 'BLOCK_NONE' }, 264 | { category: 'HARM_CATEGORY_HARASSMENT', threshold: 'BLOCK_NONE' }, 265 | { 266 | category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT', 267 | threshold: 'BLOCK_NONE', 268 | }, 269 | ], 270 | }), 271 | }); 272 | 273 | if (!response.ok) { 274 | const errorText = await response.text(); 275 | throw new Error( 276 | `API request failed: ${response.status} ${response.statusText} - ${errorText}` 277 | ); 278 | } 279 | 280 | const data = await response.json(); 281 | return { key: apiKey, valid: true, data }; 282 | } catch (error) { 283 | return { 284 | key: apiKey, 285 | valid: false, 286 | error: error.message, 287 | }; 288 | } 289 | } 290 | -------------------------------------------------------------------------------- /api/auth.js: -------------------------------------------------------------------------------- 1 | // /api/auth.js 2 | 3 | export default async function handler(req, res) { 4 | // 设置 CORS 响应头 5 | res.setHeader('Access-Control-Allow-Origin', '*'); 6 | res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); 7 | res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS'); 8 | 9 | if (req.method === 'OPTIONS') { 10 | // 处理预检请求 11 | res.status(204).end(); 12 | return; 13 | } 14 | 15 | // 从环境变量中获取密码 16 | const PASSWORD = process.env.PASSWORD; 17 | 18 | if (req.method === 'POST') { 19 | try { 20 | // 解析 JSON 请求体 21 | const data = await parseRequestBody(req); 22 | const { password } = data; 23 | 24 | if (password === PASSWORD) { 25 | res.status(200).json({ message: 'Authenticated' }); 26 | } else { 27 | res.status(401).json({ message: 'Unauthorized' }); 28 | } 29 | } catch (error) { 30 | res 31 | .status(400) 32 | .json({ message: 'Invalid request', error: error.message }); 33 | } 34 | } else { 35 | res.status(405).json({ message: 'Method Not Allowed' }); 36 | } 37 | } 38 | 39 | // 辅助函数:解析请求体 40 | async function parseRequestBody(req) { 41 | return new Promise((resolve, reject) => { 42 | let body = ''; 43 | req.on('data', chunk => { 44 | body += chunk; 45 | }); 46 | req.on('end', () => { 47 | try { 48 | const data = JSON.parse(body); 49 | resolve(data); 50 | } catch (error) { 51 | reject(error); 52 | } 53 | }); 54 | req.on('error', error => { 55 | reject(error); 56 | }); 57 | }); 58 | } 59 | -------------------------------------------------------------------------------- /api/index.js: -------------------------------------------------------------------------------- 1 | // /api/index.js 2 | 3 | import { kv } from '@vercel/kv'; 4 | 5 | export default async function handler(req, res) { 6 | // 设置 CORS 响应头 7 | res.setHeader('Access-Control-Allow-Origin', '*'); 8 | res.setHeader('Access-Control-Allow-Headers', 'Authorization, Content-Type'); 9 | res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); 10 | 11 | if (req.method === 'OPTIONS') { 12 | // 处理预检请求 13 | res.status(204).end(); 14 | return; 15 | } 16 | 17 | // 从环境变量中获取密码 18 | const PASSWORD = process.env.PASSWORD; 19 | 20 | const { pathname } = new URL(req.url, `http://${req.headers.host}`); 21 | 22 | if (pathname === '/api/auth' && req.method === 'POST') { 23 | // 处理身份验证 24 | try { 25 | const data = await parseRequestBody(req); 26 | const { password } = data; 27 | 28 | if (password === PASSWORD) { 29 | res.status(200).json({ message: 'Authenticated' }); 30 | } else { 31 | res.status(401).json({ message: 'Unauthorized' }); 32 | } 33 | } catch (error) { 34 | res 35 | .status(400) 36 | .json({ message: 'Invalid request', error: error.message }); 37 | } 38 | } else if (pathname === '/api' || pathname === '/api/') { 39 | // 验证请求头中的 Authorization 40 | const authHeader = req.headers['authorization']; 41 | if (!authHeader || authHeader !== `Bearer ${PASSWORD}`) { 42 | res.status(401).json({ message: 'Unauthorized' }); 43 | return; 44 | } 45 | 46 | if (req.method === 'GET') { 47 | try { 48 | // 从 KV 获取数据 49 | const data = await kv.get('data'); 50 | res.status(200).json(data || []); 51 | } catch (error) { 52 | res 53 | .status(500) 54 | .json({ message: 'Error retrieving data', error: error.message }); 55 | } 56 | } else if (req.method === 'POST') { 57 | try { 58 | // 解析请求体 59 | const data = await parseRequestBody(req); 60 | // 保存数据到 KV 61 | await kv.set('data', data); 62 | res.status(200).json({ message: 'Data saved successfully' }); 63 | } catch (error) { 64 | res 65 | .status(500) 66 | .json({ message: 'Error saving data', error: error.message }); 67 | } 68 | } else { 69 | res.status(405).json({ message: 'Method Not Allowed' }); 70 | } 71 | } else { 72 | res.status(404).json({ message: 'Not Found' }); 73 | } 74 | } 75 | 76 | // 辅助函数:解析请求体 77 | async function parseRequestBody(req) { 78 | return new Promise((resolve, reject) => { 79 | let body = ''; 80 | req.on('data', chunk => { 81 | body += chunk; 82 | }); 83 | req.on('end', () => { 84 | try { 85 | const data = JSON.parse(body); 86 | resolve(data); 87 | } catch (error) { 88 | reject(error); 89 | } 90 | }); 91 | req.on('error', error => { 92 | reject(error); 93 | }); 94 | }); 95 | } 96 | -------------------------------------------------------------------------------- /api/local/alive.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | 3 | const router = express.Router(); 4 | 5 | // 中间件:解析 JSON 请求体 6 | router.use(express.json()); 7 | 8 | const corsHeaders = { 9 | 'Access-Control-Allow-Origin': '*', 10 | 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', 11 | 'Access-Control-Allow-Headers': 'Content-Type', 12 | }; 13 | 14 | // 处理 OPTIONS 请求 15 | router.options('/', (req, res) => { 16 | res.set(corsHeaders); 17 | res.sendStatus(204); 18 | }); 19 | 20 | // 处理 POST 请求 21 | router.post('/', async (req, res) => { 22 | console.log('Received request:', req.method, req.url); 23 | 24 | try { 25 | const requestData = req.body; 26 | console.log('Parsed request content:', requestData); 27 | const { type } = requestData; 28 | 29 | let responseData; 30 | switch (type) { 31 | case 'refreshTokens': 32 | responseData = await handleRefreshTokens(requestData.tokens); 33 | break; 34 | case 'sessionKeys': 35 | responseData = await handleSessionKeys(requestData); 36 | break; 37 | case 'geminiAPI': 38 | responseData = await handleGeminiAPI(requestData); 39 | break; 40 | default: 41 | res.status(400).json({ error: 'Invalid request type' }); 42 | return; 43 | } 44 | 45 | res.set(corsHeaders); 46 | res.status(200).json(responseData); 47 | } catch (error) { 48 | console.error('Error processing request:', error); 49 | res.set(corsHeaders); 50 | res.status(500).json({ error: error.message }); 51 | } 52 | }); 53 | 54 | // 导出路由 55 | export default router; 56 | 57 | // 辅助函数 58 | async function handleRefreshTokens(tokens) { 59 | const results = await Promise.all(tokens.map(refreshTokenValidity)); 60 | return results; 61 | } 62 | 63 | async function refreshTokenValidity(token) { 64 | try { 65 | const response = await fetch('https://token.oaifree.com/api/auth/refresh', { 66 | method: 'POST', 67 | headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 68 | body: 'refresh_token=' + encodeURIComponent(token), 69 | }); 70 | 71 | if (response.ok) { 72 | const data = await response.json(); 73 | return { 74 | refreshToken: token, 75 | accessToken: data.access_token, 76 | valid: true, 77 | }; 78 | } 79 | return { refreshToken: token, accessToken: null, valid: false }; 80 | } catch (error) { 81 | return { refreshToken: token, accessToken: null, valid: false }; 82 | } 83 | } 84 | 85 | async function handleSessionKeys({ 86 | tokens: sessionKeys, 87 | maxAttempts, 88 | requestsPerSecond, 89 | }) { 90 | const delayBetweenRequests = 1000 / requestsPerSecond; 91 | 92 | const results = await Promise.all( 93 | sessionKeys.map(sessionKey => 94 | checkSessionKey(sessionKey, maxAttempts, delayBetweenRequests) 95 | ) 96 | ); 97 | return results; 98 | } 99 | 100 | function delay(ms) { 101 | return new Promise(resolve => { 102 | setTimeout(resolve, ms); 103 | }); 104 | } 105 | 106 | async function checkSessionKey(sessionKey, maxAttempts, delayBetweenRequests) { 107 | let attempts = 0; 108 | 109 | while (attempts < maxAttempts) { 110 | attempts++; 111 | try { 112 | const response = await fetch('https://api.claude.ai/api/organizations', { 113 | headers: { 114 | Accept: 'application/json', 115 | Cookie: 'sessionKey=' + sessionKey, 116 | 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64)', 117 | }, 118 | }); 119 | 120 | if (!response.ok) { 121 | throw new Error('HTTP error! status: ' + response.status); 122 | } 123 | 124 | const organizations = await response.json(); 125 | 126 | if (!Array.isArray(organizations) || organizations.length === 0) { 127 | throw new Error('Invalid response data'); 128 | } 129 | 130 | const organization = organizations[0]; 131 | const name = organization.name || 'Unknown'; 132 | const capabilities = organization.capabilities 133 | ? organization.capabilities.join(';') 134 | : ''; 135 | 136 | return { 137 | sessionKey, 138 | name, 139 | capabilities, 140 | available: true, 141 | attempts, 142 | successRate: 1 / attempts, 143 | }; 144 | } catch (error) { 145 | if (attempts >= maxAttempts) { 146 | return { 147 | sessionKey, 148 | name: 'Invalid', 149 | capabilities: '', 150 | available: false, 151 | attempts, 152 | successRate: 0, 153 | }; 154 | } 155 | await delay(delayBetweenRequests); 156 | } 157 | } 158 | } 159 | 160 | async function handleGeminiAPI({ 161 | tokens: apiKeys, 162 | model, 163 | rateLimit, 164 | prompt, 165 | user, 166 | }) { 167 | if (!apiKeys || !Array.isArray(apiKeys) || apiKeys.length === 0) { 168 | throw new Error('Invalid or empty API keys'); 169 | } 170 | 171 | const results = await batchTestAPI(apiKeys, model, rateLimit, prompt, user); 172 | 173 | const validResults = results.filter(result => result.valid && result.data); 174 | const validKeys = validResults.map(result => result.key); 175 | const invalidResults = results.filter(result => !result.valid); 176 | const invalidKeys = invalidResults.map(result => result.key); 177 | const errors = invalidResults.map(result => ({ 178 | key: result.key, 179 | error: result.error, 180 | })); 181 | 182 | return { 183 | valid: validKeys.length, 184 | invalid: invalidKeys.length, 185 | invalidKeys, 186 | errors, 187 | validResults, 188 | }; 189 | } 190 | 191 | async function batchTestAPI(apiKeys, model, rateLimit, prompt, user) { 192 | const testResults = []; 193 | const delayBetweenRequests = 1000 / rateLimit; 194 | 195 | for (const apiKey of apiKeys) { 196 | try { 197 | const result = await testAPI(apiKey, model, prompt, user); 198 | testResults.push(result); 199 | } catch (error) { 200 | testResults.push({ key: apiKey, valid: false, error: error.message }); 201 | } 202 | await delay(delayBetweenRequests); 203 | } 204 | 205 | return testResults; 206 | } 207 | 208 | async function testAPI(apiKey, model, prompt, user) { 209 | const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`; 210 | const requestBody = { 211 | contents: [ 212 | { 213 | parts: [{ text: prompt }, { text: user }], 214 | }, 215 | ], 216 | safetySettings: [ 217 | { 218 | category: 'HARM_CATEGORY_DANGEROUS_CONTENT', 219 | threshold: 'BLOCK_NONE', 220 | }, 221 | { 222 | category: 'HARM_CATEGORY_HATE_SPEECH', 223 | threshold: 'BLOCK_NONE', 224 | }, 225 | { 226 | category: 'HARM_CATEGORY_HARASSMENT', 227 | threshold: 'BLOCK_NONE', 228 | }, 229 | { 230 | category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT', 231 | threshold: 'BLOCK_NONE', 232 | }, 233 | ], 234 | }; 235 | 236 | const response = await fetch(url, { 237 | method: 'POST', 238 | headers: { 239 | 'Content-Type': 'application/json', 240 | }, 241 | body: JSON.stringify(requestBody), 242 | }); 243 | 244 | if (!response.ok) { 245 | const errorText = await response.text(); 246 | throw new Error( 247 | `API request failed: ${response.status} ${response.statusText} - ${errorText}` 248 | ); 249 | } 250 | 251 | const data = await response.json(); 252 | return { key: apiKey, valid: true, data }; 253 | } 254 | -------------------------------------------------------------------------------- /api/local/auth.js: -------------------------------------------------------------------------------- 1 | // api/local/auth.js 2 | 3 | import express from 'express'; 4 | 5 | const router = express.Router(); 6 | 7 | // 处理 POST 请求到 `/api/auth` 8 | router.post('/', (req, res) => { 9 | const PASSWORD = process.env.PASSWORD ? process.env.PASSWORD.trim() : ''; 10 | const { password } = req.body; 11 | console.log('Request body:', req.body); 12 | console.log('Received password:', password); 13 | console.log('Expected password:', PASSWORD); 14 | 15 | if (password && password.trim() === PASSWORD) { 16 | res.status(200).json({ message: 'Authenticated' }); 17 | } else { 18 | res.status(401).json({ message: 'Unauthorized' }); 19 | } 20 | }); 21 | 22 | export default router; 23 | -------------------------------------------------------------------------------- /api/local/index.js: -------------------------------------------------------------------------------- 1 | // api/local/index.js 2 | 3 | import express from 'express'; 4 | import fs from 'fs'; 5 | import path from 'path'; 6 | import { fileURLToPath } from 'url'; 7 | 8 | const router = express.Router(); 9 | 10 | // 获取当前文件的目录名 11 | const __filename = fileURLToPath(import.meta.url); 12 | const __dirname = path.dirname(__filename); 13 | 14 | // 数据目录(在 Dockerfile 中指定的挂载点) 15 | const dataDir = path.join(__dirname, '../../data'); 16 | 17 | // 确保数据目录存在 18 | if (!fs.existsSync(dataDir)) { 19 | fs.mkdirSync(dataDir, { recursive: true }); 20 | } 21 | 22 | // 数据文件的路径,修改为 data.json 23 | const dataFilePath = path.join(dataDir, 'data.json'); 24 | 25 | // 中间件:认证 26 | router.use((req, res, next) => { 27 | const PASSWORD = process.env.PASSWORD ? process.env.PASSWORD.trim() : ''; 28 | const authHeader = req.headers['authorization']; 29 | 30 | console.log('Authorization Header:', authHeader); 31 | 32 | if (!authHeader || authHeader !== `Bearer ${PASSWORD}`) { 33 | return res.status(401).json({ message: 'Unauthorized' }); 34 | } 35 | next(); 36 | }); 37 | 38 | // GET 方法:获取数据 39 | router.get('/', (req, res) => { 40 | fs.readFile(dataFilePath, 'utf8', (err, data) => { 41 | if (err) { 42 | // 如果文件不存在,则创建 xx.json 并写入空的 JSON 43 | if (err.code === 'ENOENT') { 44 | const emptyData = {}; 45 | fs.writeFile( 46 | dataFilePath, 47 | JSON.stringify(emptyData), 48 | 'utf8', 49 | writeErr => { 50 | if (writeErr) { 51 | console.error('创建 xx.json 文件时发生错误:', writeErr); 52 | return res.status(500).json({ message: 'Server error' }); 53 | } 54 | return res.json(emptyData); 55 | } 56 | ); 57 | } else { 58 | console.error('读取文件时发生错误:', err); 59 | return res.status(500).json({ message: 'Server error' }); 60 | } 61 | } else { 62 | // 处理空文件或无效的 JSON 数据 63 | let jsonData; 64 | if (!data) { 65 | // 文件为空,返回空对象 66 | jsonData = {}; 67 | } else { 68 | try { 69 | jsonData = JSON.parse(data); 70 | } catch (parseErr) { 71 | console.error('解析 JSON 数据时发生错误:', parseErr); 72 | jsonData = {}; 73 | } 74 | } 75 | 76 | res.json(jsonData); 77 | } 78 | }); 79 | }); 80 | 81 | // POST 方法:保存数据 82 | router.post('/', (req, res) => { 83 | const data = req.body; 84 | 85 | // 验证数据是否为对象或数组 86 | if (typeof data !== 'object' || data === null) { 87 | return res.status(400).json({ 88 | message: 'Bad Request: Data should be a valid JSON object or array', 89 | }); 90 | } 91 | 92 | fs.writeFile(dataFilePath, JSON.stringify(data), 'utf8', err => { 93 | if (err) { 94 | console.error('保存数据时发生错误:', err); 95 | return res.status(500).json({ message: 'Failed to save data' }); 96 | } 97 | res.status(200).json({ message: 'Data saved successfully' }); 98 | }); 99 | }); 100 | 101 | export default router; 102 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | docker build -t api-check . 2 | 3 | -------------------------------------------------------------------------------- /components.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // @ts-nocheck 3 | // Generated by unplugin-vue-components 4 | // Read more: https://github.com/vuejs/core/pull/3399 5 | export {}; 6 | 7 | /* prettier-ignore */ 8 | declare module 'vue' { 9 | export interface GlobalComponents { 10 | AAvatar: typeof import('ant-design-vue/es')['Avatar'] 11 | AButton: typeof import('ant-design-vue/es')['Button'] 12 | ACheckbox: typeof import('ant-design-vue/es')['Checkbox'] 13 | ACheckboxGroup: typeof import('ant-design-vue/es')['CheckboxGroup'] 14 | ACol: typeof import('ant-design-vue/es')['Col'] 15 | ACollapse: typeof import('ant-design-vue/es')['Collapse'] 16 | ACollapsePanel: typeof import('ant-design-vue/es')['CollapsePanel'] 17 | AConfigProvider: typeof import('ant-design-vue/es')['ConfigProvider'] 18 | ADivider: typeof import('ant-design-vue/es')['Divider'] 19 | ADropdown: typeof import('ant-design-vue/es')['Dropdown'] 20 | AFlex: typeof import('ant-design-vue/es')['Flex'] 21 | AForm: typeof import('ant-design-vue/es')['Form'] 22 | AFormItem: typeof import('ant-design-vue/es')['FormItem'] 23 | AImage: typeof import('ant-design-vue/es')['Image'] 24 | AInput: typeof import('ant-design-vue/es')['Input'] 25 | AInputNumber: typeof import('ant-design-vue/es')['InputNumber'] 26 | AInputPassword: typeof import('ant-design-vue/es')['InputPassword'] 27 | AList: typeof import('ant-design-vue/es')['List'] 28 | AListItem: typeof import('ant-design-vue/es')['ListItem'] 29 | AMenu: typeof import('ant-design-vue/es')['Menu'] 30 | AMenuItem: typeof import('ant-design-vue/es')['MenuItem'] 31 | AModal: typeof import('ant-design-vue/es')['Modal'] 32 | APagination: typeof import('ant-design-vue/es')['Pagination'] 33 | APopover: typeof import('ant-design-vue/es')['Popover'] 34 | AProgress: typeof import('ant-design-vue/es')['Progress'] 35 | ARow: typeof import('ant-design-vue/es')['Row'] 36 | ASelect: typeof import('ant-design-vue/es')['Select'] 37 | ASpace: typeof import('ant-design-vue/es')['Space'] 38 | ASpin: typeof import('ant-design-vue/es')['Spin'] 39 | ATabPane: typeof import('ant-design-vue/es')['TabPane'] 40 | ATabs: typeof import('ant-design-vue/es')['Tabs'] 41 | ATextarea: typeof import('ant-design-vue/es')['Textarea'] 42 | ATooltip: typeof import('ant-design-vue/es')['Tooltip'] 43 | Check: typeof import('./src/components/Check.vue')['default'] 44 | Experimental: typeof import('./src/components/Experimental.vue')['default'] 45 | RouterLink: typeof import('vue-router')['RouterLink'] 46 | RouterView: typeof import('vue-router')['RouterView'] 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /docs/cloudflare.md: -------------------------------------------------------------------------------- 1 | # Cloudflare Workers 单独部署后端教程 2 | 3 | ### 1. 复制后端代码 4 | 5 | ```javascript 6 | addEventListener('fetch', event => { 7 | event.respondWith(handleRequest(event.request)); 8 | }); 9 | 10 | async function handleRequest(request) { 11 | const url = new URL(request.url); 12 | const pathname = url.pathname; 13 | 14 | // 设置 CORS 头 15 | const corsHeaders = { 16 | 'Access-Control-Allow-Origin': '*', 17 | 'Access-Control-Allow-Headers': 'Authorization, Content-Type', 18 | 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', 19 | }; 20 | 21 | if (request.method === 'OPTIONS') { 22 | // 处理预检请求 23 | return new Response(null, { 24 | status: 204, 25 | headers: corsHeaders, 26 | }); 27 | } 28 | 29 | const PASSWORD = PASSWORD; // 从环境变量中获取密码 30 | 31 | if (pathname === '/api/auth' && request.method === 'POST') { 32 | // 身份验证 33 | const { password } = await request.json(); 34 | if (password === PASSWORD) { 35 | return new Response(JSON.stringify({ message: 'Authenticated' }), { 36 | headers: { 'Content-Type': 'application/json', ...corsHeaders }, 37 | }); 38 | } else { 39 | return new Response(JSON.stringify({ message: 'Unauthorized' }), { 40 | status: 401, 41 | headers: { 'Content-Type': 'application/json', ...corsHeaders }, 42 | }); 43 | } 44 | } else if ( 45 | pathname === '/api' && 46 | (request.method === 'GET' || request.method === 'POST') 47 | ) { 48 | // 验证 Authorization 头 49 | const authHeader = request.headers.get('Authorization'); 50 | if (!authHeader || authHeader !== `Bearer ${PASSWORD}`) { 51 | return new Response(JSON.stringify({ message: 'Unauthorized' }), { 52 | status: 401, 53 | headers: { 'Content-Type': 'application/json', ...corsHeaders }, 54 | }); 55 | } 56 | 57 | if (request.method === 'GET') { 58 | // 从 KV 获取数据 59 | const data = await DATA.get('data'); 60 | return new Response(data || '[]', { 61 | headers: { 'Content-Type': 'application/json', ...corsHeaders }, 62 | }); 63 | } else if (request.method === 'POST') { 64 | // 将数据保存到 KV 65 | const body = await request.text(); 66 | await DATA.put('data', body); 67 | return new Response( 68 | JSON.stringify({ message: 'Data saved successfully' }), 69 | { 70 | headers: { 'Content-Type': 'application/json', ...corsHeaders }, 71 | } 72 | ); 73 | } 74 | } 75 | 76 | return new Response(JSON.stringify({ message: 'Not Found' }), { 77 | status: 404, 78 | headers: { 'Content-Type': 'application/json', ...corsHeaders }, 79 | }); 80 | } 81 | ``` 82 | 83 | ### 2. 配置 KV 存储 与 PASSWORD 84 | 85 | **创建 KV 命名空间** 86 | 87 | - 登录到 Cloudflare 仪表板,进入您的账户。 88 | 89 | - 在左侧导航栏中,点击 **“Workers”**,然后选择 **“KV”**。 90 | 91 | - 点击 **“Create Namespace”**,创建一个新的 KV 命名空间。 92 | 93 | - **Namespace**:输入命名空间名称,例如 `my-kv-store`。 94 | 95 | - 创建后,记下命名空间的 **ID**(在列表中可以看到)。 96 | 97 | **创建变量** PASSWORD 98 | 99 | ![上测试报告](./images/cloudflare.png) 100 | 101 | ### 3.在页面直接填入worker的 url 不需要/api 102 | -------------------------------------------------------------------------------- /docs/images/about.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/october-coder/api-check/82a81d7d203e0dc92c5e26198a604437d63312bf/docs/images/about.png -------------------------------------------------------------------------------- /docs/images/cloudflare.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/october-coder/api-check/82a81d7d203e0dc92c5e26198a604437d63312bf/docs/images/cloudflare.png -------------------------------------------------------------------------------- /docs/images/config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/october-coder/api-check/82a81d7d203e0dc92c5e26198a604437d63312bf/docs/images/config.png -------------------------------------------------------------------------------- /docs/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/october-coder/api-check/82a81d7d203e0dc92c5e26198a604437d63312bf/docs/images/logo.png -------------------------------------------------------------------------------- /docs/images/report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/october-coder/api-check/82a81d7d203e0dc92c5e26198a604437d63312bf/docs/images/report.png -------------------------------------------------------------------------------- /docs/images/testing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/october-coder/api-check/82a81d7d203e0dc92c5e26198a604437d63312bf/docs/images/testing.png -------------------------------------------------------------------------------- /docs/images/vercel-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/october-coder/api-check/82a81d7d203e0dc92c5e26198a604437d63312bf/docs/images/vercel-1.png -------------------------------------------------------------------------------- /docs/images/vercel-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/october-coder/api-check/82a81d7d203e0dc92c5e26198a604437d63312bf/docs/images/vercel-2.png -------------------------------------------------------------------------------- /docs/images/vercel-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/october-coder/api-check/82a81d7d203e0dc92c5e26198a604437d63312bf/docs/images/vercel-3.png -------------------------------------------------------------------------------- /docs/images/vercel-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/october-coder/api-check/82a81d7d203e0dc92c5e26198a604437d63312bf/docs/images/vercel-4.png -------------------------------------------------------------------------------- /docs/vercel.md: -------------------------------------------------------------------------------- 1 | # 一、Vercel 一键部署整个项目教程 2 | 3 | ### 1. 配置 Vercel KV 存储 4 | 5 | - 部署后,进入项目的 “Settings” 页面。 6 | 7 | - 在左侧菜单中,找到 “Storage” 部分,点击 “KV”。 8 | 9 | vercel-1vercel-1 10 | 11 | - 点击 “Create KV”,创建一个新的 KV 命名空间。 12 | 13 | - Name:自定义一个名称(如 `my-kv-store`) 14 | 15 | - KV 创建后,将其绑定到您的项目中,**后需要重新部署项目** 16 | 17 | vercel-1 18 | 19 | ### 2.在页面直接填入url + /api 20 | 21 | vercel-1 22 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | API CHECK 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api-check", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview", 10 | "start": "node server.js", 11 | "docker:install": "yarn install --production --frozen-lockfile && yarn build" 12 | }, 13 | "dependencies": { 14 | "dotenv": "^16.4.5", 15 | "express": "^4.21.1" 16 | }, 17 | "devDependencies": { 18 | "@ant-design/icons-vue": "^7.0.1", 19 | "@vueuse/core": "^11.2.0", 20 | "ant-design-vue": "^4.2.6", 21 | "vue": "^3.5.12", 22 | "vue-i18n": "^9.14.1", 23 | "vue-router": "^4.4.5", 24 | "echarts": "^5.5.1", 25 | "@vitejs/plugin-vue": "^5.1.4", 26 | "less": "^4.2.0", 27 | "less-loader": "^12.2.0", 28 | "prettier": "^3.3.3", 29 | "rollup-plugin-visualizer": "^5.12.0", 30 | "unplugin-vue-components": "^0.27.4", 31 | "vite": "^5.4.10", 32 | "vite-plugin-style-import": "^2.0.0" 33 | }, 34 | "optionalDependencies": { 35 | "@vercel/kv": "^3.0.0", 36 | "@vercel/node": "^3.2.24" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/october-coder/api-check/82a81d7d203e0dc92c5e26198a604437d63312bf/public/logo.png -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | // server.js 2 | 3 | import express from 'express'; 4 | import dotenv from 'dotenv'; 5 | import path from 'path'; 6 | import { fileURLToPath } from 'url'; 7 | 8 | dotenv.config(); // 加载 .env 文件中的环境变量 9 | 10 | const app = express(); 11 | 12 | // 获取当前文件的目录名 13 | const __filename = fileURLToPath(import.meta.url); 14 | const __dirname = path.dirname(__filename); 15 | 16 | // 中间件:解析 JSON 请求体 17 | app.use(express.json()); 18 | 19 | // 引入路由 20 | import authRouter from './api/local/auth.js'; 21 | import apiRouter from './api/local/index.js'; 22 | import aliveRouter from './api/local/alive.js'; 23 | 24 | // 设置后端接口路由,位于 `/api` 路径下 25 | app.use('/api/alive', aliveRouter); 26 | app.use('/api/auth', authRouter); 27 | app.use('/api', apiRouter); 28 | 29 | // 设置静态文件目录,位于根路径 `/` 30 | app.use(express.static(path.join(__dirname, 'dist'))); 31 | 32 | // 处理 SPA 前端路由 33 | app.get('*', (req, res) => { 34 | if (!req.path.startsWith('/api')) { 35 | res.sendFile(path.join(__dirname, 'dist', 'index.html')); 36 | } else { 37 | res.status(404).json({ message: 'Not Found' }); 38 | } 39 | }); 40 | 41 | // 全局错误处理中间件 42 | app.use((err, req, res, next) => { 43 | console.error('全局错误处理捕获到错误:', err); 44 | res.status(500).json({ message: 'Internal Server Error' }); 45 | }); 46 | 47 | // 启动服务器 48 | const PORT = process.env.PORT || 13000; 49 | app.listen(PORT, () => { 50 | console.log(`Server is running on port ${PORT}`); 51 | }); 52 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 26 | 27 | 30 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/october-coder/api-check/82a81d7d203e0dc92c5e26198a604437d63312bf/src/assets/logo.png -------------------------------------------------------------------------------- /src/components/Experimental.vue: -------------------------------------------------------------------------------- 1 | 263 | 264 | 529 | 530 | 700 | -------------------------------------------------------------------------------- /src/i18n/index.js: -------------------------------------------------------------------------------- 1 | import { createI18n } from 'vue-i18n'; 2 | import en from '../locales/en.json'; 3 | import zh from '../locales/zh.json'; 4 | 5 | const messages = { 6 | en, 7 | zh, 8 | }; 9 | 10 | const i18n = createI18n({ 11 | legacy: false, // 重要:禁用 legacy 模式 12 | locale: 'en', 13 | messages, 14 | }); 15 | 16 | export default i18n; 17 | -------------------------------------------------------------------------------- /src/locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "API_CHECKER_TITLE": "API CHECK", 3 | "API_CHECKER_SUBTITLE": "(Adapt to OPENAI API formats )", 4 | "API_INFO_PLACEHOLDER": "Intelligent extraction, e.g., https://api.openai.com, sk-TodayIsThursdayVme50ForKFC", 5 | "API_URL_PLACEHOLDER": "API URL: https://api.openai.com", 6 | "API_KEY_PLACEHOLDER": "API Key: sk-TodayIsThursdayVme50ForKFC", 7 | "MODEL_NAME_PLACEHOLDER": "Support manually entering test model names, separated by commas in English", 8 | "GET_MODEL_LIST": "Get Model List", 9 | "SET_TIMEOUT": "Set Timeout", 10 | "TIMEOUT_PLACEHOLDER": "e.g., 10", 11 | "SET_CONCURRENCY": "Concurrency", 12 | "CONCURRENCY_PLACEHOLDER": "e.g., 5", 13 | "TEST_MODELS": "TestModel", 14 | "CHECK_QUOTA": "CheckQuota", 15 | "CLEAR_FORM": "ClearForm", 16 | "GIVE_STAR": "Give me a Star", 17 | "CONTRIBUTORS": "Contributors", 18 | "SELECT_MODEL_TITLE": "Select Model", 19 | "SELECTED_MODELS": "Selected {count} models", 20 | "FILTER_PLACEHOLDER": "Enter model keywords to filter", 21 | "FILTER": "Filter", 22 | "CLEAR": "Clear", 23 | "SELECT_ALL": "Select All", 24 | "SELECT_ALL_CHAT_ONLY": "Select All Chat Only", 25 | "OK": "OK", 26 | "Cancel": "Cancel", 27 | "TEST_RESULTS": "Test Results", 28 | "AVAILABLE_MODELS": "Available Models", 29 | "INCONSISTENT_MODELS": "Inconsistent Models", 30 | "UNAVAILABLE_MODELS": "Unavailable Models", 31 | "Error": "Error", 32 | "MODEL_TEST_ERROR": "Error occurred while testing models: {error}", 33 | "SWITCH_THEME": "Switch Theme", 34 | "SWITCH_LANGUAGE": "Switch Language", 35 | "LANGUAGE_CHINESE": "Chinese", 36 | "LANGUAGE_ENGLISH": "English", 37 | "MODEL_STATUS_LABEL": "Model Status:", 38 | "MODEL_NAME_LABEL": "Model Name:", 39 | "RESPONSE_TIME_LABEL": "Response Time (s):", 40 | "VERIFICATION_BUTTONS_LABEL": "Verification:", 41 | "REMARK_LABEL": "Remark:", 42 | "FUNCTION_VERIFICATION": "Function", 43 | "TEMPERATURE_VERIFICATION": "Tempera", 44 | "OFFICIAL_VERIFICATION": "Official ", 45 | "OTHER_VERIFICATION": "Other Verification", 46 | "SETTINGS_PANEL": "Settings Panel", 47 | "LOCAL_CACHE": "Local Storage", 48 | "EXPORT": "Export", 49 | "IMPORT": "Import", 50 | "CLOUD_CACHE": "Cloud Storage", 51 | "LOGIN": "Login", 52 | "LOGOUT": "Logout", 53 | "USER_LOGGED_IN": "Logged in as {{ username }}", 54 | "PLEASE_LOGIN_TO_VIEW_CLOUD_CACHE": "Please login to view cloud cache.", 55 | "ABOUT": "About", 56 | "ABOUT_CONTENT": "This is the about section. You can add relevant instructions or help information here.", 57 | "USERNAME": "Username", 58 | "PASSWORD": "Password", 59 | "PLEASE_ENTER_USERNAME": "Please enter username!", 60 | "PLEASE_ENTER_PASSWORD": "Please enter password!", 61 | "COPYRIGHT": "© 2023 Your Company. All rights reserved.", 62 | "CONFIRM_LOGOUT": "Confirm Logout", 63 | "CONFIRM_LOGOUT_CONTENT": "Are you sure you want to logout?", 64 | "CANCEL": "Cancel", 65 | "SAVE": "Save", 66 | "DELETE": "Delete", 67 | "SETTINGS": "Settings", 68 | "TEST_RESULT_SUMMARY": "Test Result Summary", 69 | "SHARE_RESULTS": "Share Results", 70 | "COPY_IMAGE": "Copy Image", 71 | "CLOSE": "Close", 72 | "COPY_MODELS": "Copy Models", 73 | "GO_CHAT": "Go to Chat", 74 | "MODEL_STATE_AVAILABLE": "Available", 75 | "MODEL_STATE_INCONSISTENT": "Inconsist", 76 | "MODEL_STATE_UNAVAILABLE": "Unavailab", 77 | "AVERAGE_LATENCY": "Average Latency", 78 | "CHAT_MODEL_COUNT": "Chat Model Count", 79 | "GITHUB": "GitHub", 80 | "FUNCTION_VERIFICATION_MODAL_TITLE": "Enter values for a and b", 81 | "VALUE_A": "a", 82 | "VALUE_B": "b", 83 | "SUBMIT": "Submit", 84 | "FUNCTION_VERIFICATION_RESULT": "Function Verification Result", 85 | "TEMPERATURE_VERIFICATION_RESULT": "Temperature Verification Result", 86 | "OFFICIAL_VERIFICATION_RESULT": "Official Verification Result", 87 | "LOGIN_TO_VIEW": "Please login to view.", 88 | "LOGIN_SUCCESS": "Login Successful", 89 | "DATA_SAVED": "Data Saved", 90 | "DATA_IMPORTED": "Data Imported", 91 | "DATA_EXPORTED": "Data Exported", 92 | "DATA_DELETED": "Data Deleted", 93 | "API_URL": "API URL", 94 | "API_KEY": "API Key", 95 | "PLEASE_ENTER_API_URL": "Please enter the API URL", 96 | "PLEASE_ENTER_API_KEY": "Please enter the API Key", 97 | "SAVE_TO_LOCAL_CACHE": "Save to Local Cache", 98 | "CLOUD_URL": "CloudURL", 99 | "PLEASE_ENTER_CLOUD_URL": "Please enter the Cloud URL", 100 | "LOAD_FROM_CLOUD": "Load from Cloud", 101 | "SAVE_TO_CLOUD": "Save to Cloud", 102 | "CONFIRM_SAVE": "Confirm Save", 103 | "CONFIRM_SAVE_PROMPT": "Are you sure you want to save the current data to the cloud?", 104 | "CLOUD_DATA_LOADED": "Cloud data loaded", 105 | "CLOUD_DATA_LOAD_FAILED": "Failed to load cloud data", 106 | "CLOUD_DATA_LOAD_ERROR": "An error occurred while loading cloud data", 107 | "DATA_SAVED_TO_CLOUD": "Data saved to cloud", 108 | "DATA_SAVE_TO_CLOUD_FAILED": "Failed to save data to cloud", 109 | "DATA_SAVE_TO_CLOUD_ERROR": "An error occurred while saving data to cloud", 110 | "CONFIG_IMPORTED": "Configuration imported", 111 | "RECORD_DELETED_PLEASE_SAVE": "Record deleted, please click confirm save to save changes", 112 | "HISTORY_RECORDS": "History Records", 113 | "RECORD_DELETED": "Record deleted", 114 | "OFFICIAL_WEBSITE": "Website", 115 | "LOGGED_IN_TO_CLOUD": "Logged in to cloud: {url}", 116 | "VERSION": "Version", 117 | "AUTHOR": "Author", 118 | "AUTHORS": "Authors", 119 | "COAUTHOR": "Co-author", 120 | "ALL_RIGHTS_RESERVED": "All rights reserved", 121 | "LICENSE": "License", 122 | "UPDATE_LOG": "Update Log", 123 | "CLOUD_LOGIN_SUCCESS": "Cloud login successful", 124 | "DATA_IMPORTED_PLEASE_SAVE": "Data imported, please save", 125 | "PLEASE_LOGIN_TO_CLOUD": "Please log in to the cloud", 126 | "VERIFY": "Verify", 127 | "CHAT": "Chat", 128 | "SHARE": "Share", 129 | "COPY": "Copy", 130 | "UPDATE_CHECK_FAILED": "Failed to fetch the latest release information", 131 | "UPDATE_CHECK_ERROR": "Error checking for updates:", 132 | "UPDATE_AVAILABLE_TITLE": "New version {version} is available", 133 | "CURRENT_VERSION": "Current Version", 134 | "LATEST_VERSION": "Latest Version", 135 | "RELEASE_NOTES": "Release Notes", 136 | "GO_TO_UPDATE": "Update Now", 137 | "COPY_IDENTICAL_MODELS": "Copy Identical Models", 138 | "COPY_AVAILABLE_MODELS": "Copy Available Models", 139 | "NO_MODELS_TO_COPY": "No models to copy", 140 | "COPIED_MODELS_TO_CLIPBOARD": "Copied {count} {type} to clipboard", 141 | "IDENTICAL_MODELS": "identical models", 142 | "COPY_FAILED": "Copy failed, please copy manually", 143 | "RECORD_ALREADY_EXISTS": "Record already exists", 144 | "SPONSORS": "Sponsors", 145 | "PRESET_SETTINGS": "Preset Settings", 146 | "OFFICIAL_VERIFICATION_DONE": "Official", 147 | "OFFICIAL_VERIFICATION_PENDING": "Official", 148 | "MODEL_MAPPING": "Model Mapping", 149 | "MAPPED_TO_MODEL": "Mapped to", 150 | "RETURNED_MODEL": "Returned Model", 151 | "NO_MATCH": "No match", 152 | "MODEL": "Model", 153 | "STANDARD_RESPONSE": "Standard Response", 154 | "MODEL_RESPONSE": "Model Response", 155 | "REPO_ADDRESS": "Repository Address", 156 | "STAR_PROJECT": "Give a star, you can deploy it yourself", 157 | "NEW_DOMAIN": "New domain", 158 | "HOW_TO_USE": "How to Use:", 159 | "VERSION_HISTORY": "Version History", 160 | "SEE_MORE_INTRODUCTION": "See More Introduction", 161 | "INTRODUCTION": "Introduction", 162 | "DETAIL_INTRODUCTION": "Detailed Introduction", 163 | "PLEASE_WAIT_FOR_TESTING": "Please wait for testing to complete before sharing.", 164 | "PLEASE_ENTER_CLOUD_URL_AND_PASSWORD": "Please enter the cloud URL and password", 165 | "CLOUD_LOGIN_FAILED": "Cloud login failed", 166 | "IMPORT_PARSE_ERROR": "Failed to parse the imported file, please check the format", 167 | "CLOUD_LOGIN_ERROR": "An error occurred while logging in to the cloud", 168 | "CONCLUSION": "Conclusion", 169 | "SIMILARITY_RESULTS": "Similarity Results", 170 | "UNKNOWN_ERROR": "Unknown error, please check the console logs", 171 | "O1_API_RELIABLE": "✨API is reliable", 172 | "O1_API_POSSIBLE_ISSUE": "⚠️API may have issues", 173 | "O1_API_RELIABLE_DETAIL": "Response contains non-empty reasoning_tokens, API is reliable", 174 | "O1_API_POSSIBLE_ISSUE_DETAIL": "Response does not contain reasoning_tokens or it's empty, API may have issues", 175 | "REFERENCE_VALUES": "Reference values: c3.5 = 51 (gcp test), gpt-4o = 59, gpt-4o-mini = 32 (azure test)", 176 | "TEST": "Test", 177 | "RESPONSE": "Response", 178 | "TEXT": "Text", 179 | "SYSTEM_FINGERPRINT": "System Fingerprint", 180 | "LANGUAGE_MENU": "Language Menu", 181 | "ERROR_MESSAGE": "Error Message", 182 | "RESOLUTION": "Resolution", 183 | "INVALID_IMPORT_FORMAT": "Invalid import format", 184 | "VIEW_DETAILS": "View Details", 185 | "CUSTOM_DIALOG_VERIFICATION": "Dialog Test", 186 | "CUSTOM_DIALOG_VERIFICATION_RESULT": "Dialog Test Result", 187 | "ENTER_PROMPT": "Please enter prompt", 188 | "PROMPT": "Prompt", 189 | "RESPONSE_CONTENT": "Response Content", 190 | "RAW_RESPONSE": "Raw Response", 191 | "PASTE": "Paste", 192 | "PASTE_SUCCESS": "Paste successful", 193 | "PASTE_FAILED": "Paste failed, please paste manually", 194 | "FUNCTION_INTRODUCTION": "Function Introduction", 195 | "FUNCTION_INTRODUCTION_TEXT": "Here is a brief introduction of the function.", 196 | "SELECT_MODEL": "Select Model", 197 | "SELECT_MODEL_PLACEHOLDER": "Please select a model", 198 | "SELECT_PROMPT": "Select Prompt", 199 | "SELECT_PROMPT_PLACEHOLDER": "Please select a prompt", 200 | "PROMPT_CONTENT": "Prompt Content", 201 | "SEND": "Send", 202 | "CONTINUE_TESTING": "Continue Testing", 203 | "PROMPT_DESCRIPTION": "Prompt Description", 204 | "NO_DESCRIPTION_AVAILABLE": "No description available", 205 | "ERROR_OCCURRED": "An error occurred", 206 | "EXPERIMENTAL_FEATURES": "Experimental Features", 207 | "ENTER_API_ADDRESS": "Please enter the API address", 208 | "GPT": "GPT", 209 | "CLAUDE": "Claude", 210 | "GEMINI": "Gemini", 211 | "ENTER_TOKENS_ONE_PER_LINE": "Please enter tokens, one per line", 212 | "START_CHECKING": "Start Checking", 213 | "VALID_TOKENS_COUNT": "Number of valid tokens: {count}", 214 | "COPY_VALID_TOKENS": "Copy valid tokens", 215 | "VALID_SESSION_KEYS_COUNT": "Number of valid session keys: {count}", 216 | "COPY_VALID_SESSION_KEYS": "Copy valid session keys", 217 | "REQUESTS_PER_SECOND": "Requests per second:", 218 | "VALID_API_KEYS_COUNT": "Number of valid API keys: {count}", 219 | "COPY_VALID_API_KEYS": "Copy valid API keys", 220 | "MAX_ATTEMPTS": "Max attempts:", 221 | "API_ADDRESS_SAVED": "API address saved", 222 | "API_ADDRESS_DELETED": "API address deleted", 223 | "ENTER_TOKENS": "Please enter tokens", 224 | "REQUEST_FAILED": "Request failed", 225 | "REQUEST_ERROR": "Request error", 226 | "COPIED_TO_CLIPBOARD": "Copied to clipboard", 227 | "ENTER_REFRESH_TOKENS_ONE_PER_LINE": "Enter Refresh Tokens one per line", 228 | "ENTER_SESSION_KEYS_ONE_PER_LINE": "Enter Session Keys one per line", 229 | "ENTER_GEMINI_API_KEYS_ONE_PER_LINE": "Enter Gemini API Keys one per line", 230 | "ENTER_REFRESH_TOKENS": "Please enter Refresh Tokens", 231 | "ENTER_SESSION_KEYS": "Please enter Session Keys", 232 | "ENTER_GEMINI_API_KEYS": "Please enter Gemini API Keys" 233 | } 234 | -------------------------------------------------------------------------------- /src/locales/zh.json: -------------------------------------------------------------------------------- 1 | { 2 | "API_CHECKER_TITLE": "API CHECK", 3 | "API_CHECKER_SUBTITLE": "(适配 one-api/new-api 等中转格式)", 4 | "API_INFO_PLACEHOLDER": "智能提取文本框,支持同时粘贴接口地址和密钥,如:https://api.openai.com,sk-TodayIsThursdayVme50ForKFC", 5 | "API_URL_PLACEHOLDER": "接口地址,如:https://api.openai.com", 6 | "API_KEY_PLACEHOLDER": "密钥,如:sk-TodayIsThursdayVme50ForKFC", 7 | "MODEL_NAME_PLACEHOLDER": "支持手动填入测试模型名称", 8 | "GET_MODEL_LIST": "获取模型列表", 9 | "SET_TIMEOUT": "设置超时(秒)", 10 | "TIMEOUT_PLACEHOLDER": "例如:10", 11 | "SET_CONCURRENCY": "设置并发数", 12 | "CONCURRENCY_PLACEHOLDER": "例如:5", 13 | "TEST_MODELS": "测试 模型", 14 | "CHECK_QUOTA": "检查 额度", 15 | "CLEAR_FORM": "清空 表单", 16 | "GIVE_STAR": "给我一个星星", 17 | "SELECT_MODEL_TITLE": "选择模型", 18 | "SELECTED_MODELS": "已选择 {count} 个模型", 19 | "FILTER_PLACEHOLDER": "输入模型关键词进行筛选", 20 | "FILTER": "筛选", 21 | "CLEAR": "清空", 22 | "SELECT_ALL": "全选", 23 | "SELECT_ALL_CHAT_ONLY": "全选聊天模型", 24 | "OK": "确定", 25 | "Cancel": "取消", 26 | "TEST_RESULTS": "测试结果", 27 | "AVAILABLE_MODELS": "可用模型", 28 | "INCONSISTENT_MODELS": "不一致模型", 29 | "UNAVAILABLE_MODELS": "不可用模型", 30 | "Error": "错误", 31 | "MODEL_TEST_ERROR": "测试模型时发生错误: {error}", 32 | "SWITCH_THEME": "切换主题", 33 | "SWITCH_LANGUAGE": "切换语言", 34 | "LANGUAGE_CHINESE": "中文", 35 | "LANGUAGE_ENGLISH": "English", 36 | "MODEL_STATUS_LABEL": "模型状态:", 37 | "MODEL_NAME_LABEL": "模型名称:", 38 | "RESPONSE_TIME_LABEL": "用时(秒):", 39 | "VERIFICATION_BUTTONS_LABEL": "验证按钮:", 40 | "REMARK_LABEL": "备注:", 41 | "FUNCTION_VERIFICATION": "函数验证", 42 | "TEMPERATURE_VERIFICATION": "温度验证", 43 | "OFFICIAL_VERIFICATION": "官转验证", 44 | "OTHER_VERIFICATION": "其他验证", 45 | "SETTINGS_PANEL": "设置面板", 46 | "LOCAL_CACHE": "本地存储", 47 | "EXPORT": "导出", 48 | "IMPORT": "导入", 49 | "CLOUD_CACHE": "云端存储", 50 | "LOGIN": "登录", 51 | "LOGOUT": "注销", 52 | "USER_LOGGED_IN": "已登录,欢迎 {{username}}", 53 | "PLEASE_LOGIN_TO_VIEW_CLOUD_CACHE": "请先登录以查看云端缓存。", 54 | "ABOUT": "关于", 55 | "ABOUT_CONTENT": "这是关于信息的内容,您可以在这里添加相关的说明或帮助信息。", 56 | "USERNAME": "用户名", 57 | "PASSWORD": "登陆密码", 58 | "PLEASE_ENTER_USERNAME": "请输入用户名!", 59 | "PLEASE_ENTER_PASSWORD": "请输入密码!", 60 | "COPYRIGHT": "© 2023 您的公司。保留所有权利。", 61 | "CONFIRM_LOGOUT": "确认注销", 62 | "CONFIRM_LOGOUT_CONTENT": "您确定要退出登录吗?", 63 | "CANCEL": "取消", 64 | "SAVE": "保存", 65 | "DELETE": "删除", 66 | "SETTINGS": "设置", 67 | "TEST_RESULT_SUMMARY": "测试概览", 68 | "SHARE_RESULTS": "分享结果", 69 | "COPY_IMAGE": "复制图片", 70 | "CLOSE": "关闭", 71 | "COPY_MODELS": "复制模型", 72 | "GO_CHAT": "进入聊天", 73 | "MODEL_STATE_AVAILABLE": "一致可用", 74 | "MODEL_STATE_INCONSISTENT": "模型映射", 75 | "MODEL_STATE_UNAVAILABLE": "调用失败", 76 | "AVERAGE_LATENCY": "平均延时", 77 | "CHAT_MODEL_COUNT": "聊天模型数", 78 | "GITHUB": "GitHub", 79 | "FUNCTION_VERIFICATION_MODAL_TITLE": "请输入 a 和 b 的值", 80 | "VALUE_A": "a", 81 | "VALUE_B": "b", 82 | "SUBMIT": "提交", 83 | "FUNCTION_VERIFICATION_RESULT": "函 数调用验证结果", 84 | "TEMPERATURE_VERIFICATION_RESULT": "温度验证结果", 85 | "OFFICIAL_VERIFICATION_RESULT": "官方验证结果", 86 | "LOGIN_TO_VIEW": "请先登录以查看。", 87 | "LOGIN_SUCCESS": "登录成功", 88 | "DATA_SAVED": "数据已保存", 89 | "DATA_IMPORTED": "数据已导入", 90 | "DATA_EXPORTED": "数据已导出", 91 | "DATA_DELETED": "数据已删除", 92 | "API_URL": "API地址", 93 | "API_KEY": "API密钥", 94 | "PLEASE_ENTER_API_URL": "请输入API地址", 95 | "PLEASE_ENTER_API_KEY": "请输入API密钥", 96 | "SAVE_TO_LOCAL_CACHE": "保存到本地缓存", 97 | "CLOUD_URL": "云端URL", 98 | "PLEASE_ENTER_CLOUD_URL": "请输入云端URL", 99 | "LOAD_FROM_CLOUD": "从云端加载", 100 | "SAVE_TO_CLOUD": "保存到云端", 101 | "CONFIRM_SAVE": "确认保存", 102 | "CONFIRM_SAVE_PROMPT": "确定要将当前数据保存到云端吗?", 103 | "CLOUD_DATA_LOADED": "云端数据已加载", 104 | "CLOUD_DATA_LOAD_FAILED": "加载云端数据失败", 105 | "CLOUD_DATA_LOAD_ERROR": "加载云端数据时发生错误", 106 | "DATA_SAVED_TO_CLOUD": "数据已保存到云端", 107 | "DATA_SAVE_TO_CLOUD_FAILED": "保存到云端失败", 108 | "DATA_SAVE_TO_CLOUD_ERROR": "保存到云端时发生错误", 109 | "CONFIG_IMPORTED": "已导入配置", 110 | "RECORD_DELETED_PLEASE_SAVE": "已删除记录,请点击确认保存按钮保存更改", 111 | "RECORD_DELETED": "记录已删除", 112 | "HISTORY_RECORDS": "历史记录", 113 | "LOGGED_IN_TO_CLOUD": "登录到云端:{url}", 114 | "OFFICIAL_WEBSITE": "官方网站", 115 | "UPDATE_LOG_UNAVAILABLE": "更新日志暂不可用", 116 | "COAUTHOR": "作者", 117 | "VERSION": "版本", 118 | "AUTHOR": "作者", 119 | "AUTHORS": "作者", 120 | "CONTRIBUTORS": "贡献者", 121 | "ALL_RIGHTS_RESERVED": "版权所有", 122 | "LICENSE": "许可证", 123 | "CLOUD_LOGIN_SUCCESS": "云端登录成功", 124 | "DATA_IMPORTED_PLEASE_SAVE": "数据已导入,请保存", 125 | "PLEASE_LOGIN_TO_CLOUD": "请先登录云端", 126 | "UPDATE_LOG": "更新日志", 127 | "VERIFY": "验证", 128 | "CHAT": "去对话", 129 | "SHARE": "去分享", 130 | "COPY": "复制模型", 131 | "UPDATE_CHECK_FAILED": "无法获取最新的发布版本信息", 132 | "UPDATE_CHECK_ERROR": "检查更新时出错:", 133 | "UPDATE_AVAILABLE_TITLE": "发现新版本 {version}", 134 | "CURRENT_VERSION": "当前版本", 135 | "LATEST_VERSION": "最新版本", 136 | "RELEASE_NOTES": "更新日志", 137 | "GO_TO_UPDATE": "前往更新", 138 | "COPY_IDENTICAL_MODELS": "复制一致模型", 139 | "COPY_AVAILABLE_MODELS": "复制可用模型", 140 | "NO_MODELS_TO_COPY": "没有可复制的模型", 141 | "COPIED_MODELS_TO_CLIPBOARD": "已复制 {count} 个 {type} 到剪贴板", 142 | "IDENTICAL_MODELS": "一致模型", 143 | "COPY_FAILED": "复制失败,请手动复制", 144 | "RECORD_ALREADY_EXISTS": "记录已存在", 145 | "SPONSORS": "鸣谢", 146 | "PRESET_SETTINGS": "预设配置", 147 | "OFFICIAL_VERIFICATION_DONE": "官方验证", 148 | "OFFICIAL_VERIFICATION_PENDING": "官方验证", 149 | "MODEL_MAPPING": "模型映射", 150 | "MAPPED_TO_MODEL": "模型映射到", 151 | "RETURNED_MODEL": "返回模型", 152 | "NO_MATCH": "未匹配", 153 | "MODEL": "模型", 154 | "STANDARD_RESPONSE": "标准响应", 155 | "MODEL_RESPONSE": "模型响应", 156 | "REPO_ADDRESS": "仓库地址", 157 | "STAR_PROJECT": "点点 star,可以自行部署", 158 | "NEW_DOMAIN": "启用新域名", 159 | "HOW_TO_USE": "如何使用 :", 160 | "VERSION_HISTORY": "版本历史", 161 | "INTRODUCTION": "介绍", 162 | "DETAIL_INTRODUCTION": "详细介绍", 163 | "SEE_MORE_INTRODUCTION": "查看更多介绍", 164 | "PLEASE_WAIT_FOR_TESTING": "请等待测试完成后再分享。", 165 | "PLEASE_ENTER_CLOUD_URL_AND_PASSWORD": "请输入云端URL和密码", 166 | "CLOUD_LOGIN_FAILED": "云端登录失败", 167 | "IMPORT_PARSE_ERROR": "导入的文件无法解析,请检查格式", 168 | "CLOUD_LOGIN_ERROR": "云端登录出错", 169 | "CONCLUSION": "结论", 170 | "SIMILARITY_RESULTS": "相似度结果", 171 | "UNKNOWN_ERROR": "未知错误,请查看控制台日志", 172 | "O1_API_RELIABLE": "✨API 可靠", 173 | "O1_API_POSSIBLE_ISSUE": "⚠️API 可能存在问题", 174 | "O1_API_RELIABLE_DETAIL": "返回响应中包含非空 reasoning_tokens,API 可靠", 175 | "O1_API_POSSIBLE_ISSUE_DETAIL": "返回响应中不包含 reasoning_tokens 或为空,API 可能存在问题", 176 | "REFERENCE_VALUES": "参考值:c3.5 = 51(gcp测试),gpt-4o = 59,gpt-4o-mini = 32(azure测试)", 177 | "TEST": "测试", 178 | "RESPONSE": "响应", 179 | "TEXT": "文本", 180 | "SYSTEM_FINGERPRINT": "系统指纹", 181 | "LANGUAGE_MENU": "语言菜单", 182 | "ERROR_MESSAGE": "错误信息", 183 | "RESOLUTION": "解决方案", 184 | "INVALID_IMPORT_FORMAT": "导入格式无效", 185 | "VIEW_DETAILS": "查看详情", 186 | "CUSTOM_DIALOG_VERIFICATION": "对话验证", 187 | "CUSTOM_DIALOG_VERIFICATION_RESULT": "对话验证结果", 188 | "ENTER_PROMPT": "请输入提示词", 189 | "PROMPT": "提示词", 190 | "RESPONSE_CONTENT": "响应内容", 191 | "RAW_RESPONSE": "原始响应", 192 | "PASTE": "粘贴", 193 | "PASTE_SUCCESS": "粘贴成功", 194 | "PASTE_FAILED": "粘贴失败,请手动粘贴", 195 | "FUNCTION_INTRODUCTION": "功能介绍", 196 | "FUNCTION_INTRODUCTION_TEXT": "这里是功能的简单介绍文本。", 197 | "SELECT_MODEL": "选择模型", 198 | "SELECT_MODEL_PLACEHOLDER": "请选择模型", 199 | "SELECT_PROMPT": "选择提示词", 200 | "SELECT_PROMPT_PLACEHOLDER": "请选择提示词", 201 | "PROMPT_CONTENT": "提示词内容", 202 | "SEND": "发送", 203 | "CONTINUE_TESTING": "继续测试", 204 | "PROMPT_DESCRIPTION": "提示词描述", 205 | "NO_DESCRIPTION_AVAILABLE": "暂无描述", 206 | "ERROR_OCCURRED": "发生错误", 207 | "EXPERIMENTAL_FEATURES": "实验性功能", 208 | "ENTER_API_ADDRESS": "请输入 API 地址", 209 | "GPT": "GPT", 210 | "CLAUDE": "Claude", 211 | "GEMINI": "Gemini", 212 | "ENTER_TOKENS_ONE_PER_LINE": "请输入 Tokens,每行一个", 213 | "START_CHECKING": "开始检查", 214 | "VALID_TOKENS_COUNT": "有效的 Tokens 数量:{count}", 215 | "COPY_VALID_TOKENS": "复制有效 Tokens", 216 | "VALID_SESSION_KEYS_COUNT": "有效的 Session Keys 数量:{count}", 217 | "COPY_VALID_SESSION_KEYS": "复制有效 Session Keys", 218 | "REQUESTS_PER_SECOND": "每秒请求数:", 219 | "VALID_API_KEYS_COUNT": "有效的 API Keys 数量:{count}", 220 | "COPY_VALID_API_KEYS": "复制有效 API Keys", 221 | "MAX_ATTEMPTS": "最大尝试次数:", 222 | "API_ADDRESS_SAVED": "API 地址已保存", 223 | "API_ADDRESS_DELETED": "API 地址已删除", 224 | "ENTER_TOKENS": "请输入 Tokens", 225 | "REQUEST_FAILED": "请求失败", 226 | "REQUEST_ERROR": "请求出错", 227 | "COPIED_TO_CLIPBOARD": "已复制到剪贴板", 228 | "ENTER_REFRESH_TOKENS_ONE_PER_LINE": "请输入 Refresh Tokens,每行一个", 229 | "ENTER_SESSION_KEYS_ONE_PER_LINE": "请输入 Session Keys,每行一个", 230 | "ENTER_GEMINI_API_KEYS_ONE_PER_LINE": "请输入 Gemini API Keys,每行一个", 231 | "ENTER_REFRESH_TOKENS": "请输入 Refresh Tokens", 232 | "ENTER_SESSION_KEYS": "请输入 Session Keys", 233 | "ENTER_GEMINI_API_KEYS": "请输入 Gemini API Keys" 234 | } 235 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | import App from './App.vue'; 3 | import router from './router'; 4 | import { ConfigProvider } from 'ant-design-vue'; 5 | import 'ant-design-vue/dist/reset.css'; 6 | // 引入 i18n 7 | import i18n from './i18n'; 8 | 9 | const app = createApp(App); 10 | 11 | app.use(router); 12 | app.use(ConfigProvider); 13 | app.use(i18n); 14 | app.mount('#app'); 15 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from 'vue-router'; 2 | import Home from '../views/Home.vue'; 3 | import Layout from '../views/Layout.vue'; 4 | 5 | const routes = [ 6 | { 7 | path: '/', 8 | component: Layout, 9 | children: [ 10 | { 11 | path: '', 12 | component: Home, 13 | }, 14 | ], 15 | }, 16 | ]; 17 | 18 | const router = createRouter({ 19 | history: createWebHistory(), 20 | routes, 21 | }); 22 | 23 | export default router; 24 | -------------------------------------------------------------------------------- /src/styles/global.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --vh: 1vh; 3 | } 4 | body { 5 | font-family: Arial, sans-serif; 6 | margin: 0; 7 | padding: 0; 8 | box-sizing: border-box; 9 | transition: 10 | background-color 0.3s, 11 | color 0.3s; 12 | /* 设置默认主题变量,假设默认是浅色主题 */ 13 | --background-color: rgba(255, 255, 255, 0.5); 14 | --font-color: #000000; 15 | --input-background-color: #ffffff; 16 | --input-border-color: #cccccc; 17 | } 18 | 19 | body.dark-mode { 20 | background-color: #1e1e1e; 21 | color: #e0e0e0; 22 | 23 | --background-color: rgba(50, 49, 48, 0.7); 24 | --font-color: #e0e0e0; 25 | --input-background-color: #2e2e2e; 26 | --input-border-color: #1e88e5; 27 | } 28 | 29 | body.light-mode { 30 | background-color: #f8f8f8; 31 | color: #000000; 32 | --background-color: rgba(255, 255, 255, 0.7); 33 | --font-color: #000000; 34 | --input-background-color: #ffffff; 35 | --input-border-color: #cccccc; 36 | } 37 | a { 38 | color: inherit; 39 | text-decoration: none; 40 | } 41 | 42 | .container { 43 | max-width: 800px; 44 | margin: 0 auto; 45 | padding: 40px 20px; 46 | } 47 | 48 | .header { 49 | display: flex; 50 | justify-content: space-between; 51 | align-items: center; 52 | margin-bottom: 20px; 53 | } 54 | 55 | .dark-mode { 56 | --background-color: #1e1e1e; 57 | --font-color: #e0e0e0; 58 | --input-background-color: #2e2e2e; 59 | --input-border-color: #555; 60 | --button-background-color: #4caf50; 61 | --button-hover-background-color: #388e3c; 62 | --button-text-color: #ffffff; 63 | } 64 | 65 | .light-mode { 66 | --background-color: #f8f8f8; 67 | --font-color: #000000; 68 | --input-background-color: #ffffff; 69 | --input-border-color: #cccccc; 70 | --button-background-color: #4caf50; 71 | --button-hover-background-color: #388e3c; 72 | --button-text-color: #ffffff; 73 | } 74 | 75 | /* Center the footer content */ 76 | footer { 77 | text-align: center; 78 | } 79 | 80 | input { 81 | line-height: normal !important; 82 | } 83 | -------------------------------------------------------------------------------- /src/utils/api.js: -------------------------------------------------------------------------------- 1 | export async function fetchModelList(apiUrl, apiKey) { 2 | const apiUrlValue = apiUrl.replace(/\/+$/, ''); 3 | const response = await fetch(`${apiUrlValue}/v1/models`, { 4 | headers: { 5 | Authorization: `Bearer ${apiKey}`, 6 | 'Content-Type': 'application/json', 7 | }, 8 | }); 9 | return await response.json(); 10 | } 11 | 12 | export async function fetchQuotaInfo(apiUrl, apiKey) { 13 | const trimmedApiUrl = apiUrl.replace(/\/+$/, ''); 14 | const authHeader = { Authorization: `Bearer ${apiKey}` }; 15 | 16 | // Fetch subscription data 17 | const quotaResponse = await fetch( 18 | `${trimmedApiUrl}/dashboard/billing/subscription`, 19 | { 20 | headers: authHeader, 21 | } 22 | ); 23 | const quotaData = await quotaResponse.json(); 24 | const quotaInfo = quotaData.hard_limit_usd ? quotaData.hard_limit_usd : null; 25 | 26 | // Fetch usage data 27 | const today = new Date(); 28 | const year = today.getFullYear(); 29 | const month = String(today.getMonth() + 1).padStart(2, '0'); 30 | const day = String(today.getDate()).padStart(2, '0'); 31 | const startDate = `${year}-${month}-01`; 32 | const endDate = `${year}-${month}-${day}`; 33 | 34 | const usageResponse = await fetch( 35 | `${trimmedApiUrl}/dashboard/billing/usage?start_date=${startDate}&end_date=${endDate}`, 36 | { 37 | headers: authHeader, 38 | } 39 | ); 40 | const usageData = await usageResponse.json(); 41 | const usedInfo = usageData.total_usage / 100; 42 | 43 | return { 44 | quotaInfo, 45 | usedInfo, 46 | }; 47 | } 48 | 49 | export async function testModelList( 50 | apiUrl, 51 | apiKey, 52 | modelNames, 53 | timeoutSeconds, 54 | concurrency, 55 | progressCallback 56 | ) { 57 | const valid = []; 58 | const invalid = []; 59 | const inconsistent = []; 60 | const awaitOfficialVerification = []; 61 | 62 | async function testModel(model) { 63 | const apiUrlValue = apiUrl.replace(/\/+$/, ''); 64 | let timeout = timeoutSeconds * 1000; // 转换为毫秒 65 | 66 | // 对于 'o1-' 开头的模型,增加超时时间 67 | if (model.startsWith('o1-')) { 68 | timeout *= 6; 69 | } 70 | 71 | const controller = new AbortController(); 72 | const id = setTimeout(() => controller.abort(), timeout); 73 | const startTime = Date.now(); 74 | 75 | let response_text; 76 | try { 77 | const requestBody = { 78 | model: model, 79 | messages: [{ role: 'user', content: '写一个10个字的冷笑话' }], 80 | }; 81 | if (/^(gpt-|chatgpt-)/.test(model)) { 82 | requestBody.seed = 331; 83 | } 84 | const response = await fetch(`${apiUrlValue}/v1/chat/completions`, { 85 | method: 'POST', 86 | headers: { 87 | Authorization: `Bearer ${apiKey}`, 88 | 'Content-Type': 'application/json', 89 | }, 90 | body: JSON.stringify(requestBody), 91 | signal: controller.signal, 92 | }); 93 | 94 | const endTime = Date.now(); 95 | const responseTime = (endTime - startTime) / 1000; // 转换为秒 96 | 97 | let has_o1_reason = false; 98 | if (response.ok) { 99 | const data = await response.json(); 100 | const returnedModel = data.model || 'no returned model'; 101 | 102 | // 检查 'o1-' 模型的特殊字段 103 | if ( 104 | returnedModel.startsWith('o1-') && 105 | data?.usage?.completion_tokens_details?.reasoning_tokens > 0 106 | ) { 107 | has_o1_reason = true; 108 | } 109 | 110 | if (returnedModel === model) { 111 | const resultData = { model, responseTime, has_o1_reason }; 112 | valid.push(resultData); 113 | progressCallback({ 114 | type: 'valid', 115 | data: resultData, 116 | }); 117 | } else { 118 | const resultData = { 119 | model, 120 | returnedModel, 121 | responseTime, 122 | has_o1_reason, 123 | }; 124 | inconsistent.push(resultData); 125 | progressCallback({ 126 | type: 'inconsistent', 127 | data: resultData, 128 | }); 129 | } 130 | } else { 131 | try { 132 | const jsonResponse = await response.json(); 133 | response_text = jsonResponse.error.message; 134 | } catch (jsonError) { 135 | try { 136 | response_text = await response.text(); 137 | } catch (textError) { 138 | response_text = '无法解析响应内容'; 139 | } 140 | } 141 | const resultData = { model, response_text }; 142 | invalid.push(resultData); 143 | progressCallback({ 144 | type: 'invalid', 145 | data: resultData, 146 | }); 147 | } 148 | } catch (error) { 149 | if (error.name === 'AbortError') { 150 | const resultData = { model, error: '超时' }; 151 | invalid.push(resultData); 152 | progressCallback({ 153 | type: 'invalid', 154 | data: resultData, 155 | }); 156 | } else { 157 | const resultData = { model, error: error.message }; 158 | invalid.push(resultData); 159 | progressCallback({ 160 | type: 'invalid', 161 | data: resultData, 162 | }); 163 | } 164 | } finally { 165 | clearTimeout(id); 166 | } 167 | } 168 | 169 | async function runBatch(models) { 170 | const promises = models.map(model => 171 | testModel(model).catch(error => { 172 | console.error(`测试模型 ${model} 时发生错误:${error.message}`); 173 | }) 174 | ); 175 | await Promise.all(promises); 176 | } 177 | 178 | for (let i = 0; i < modelNames.length; i += concurrency) { 179 | const batch = modelNames.slice(i, i + concurrency); 180 | await runBatch(batch); 181 | } 182 | 183 | return { valid, invalid, inconsistent, awaitOfficialVerification }; 184 | } 185 | 186 | // GPT Refresh Tokens 187 | export function checkRefreshTokens(apiAddress, tokens) { 188 | return fetch(apiAddress, { 189 | method: 'POST', 190 | headers: { 191 | 'Content-Type': 'application/json', 192 | }, 193 | body: JSON.stringify({ 194 | type: 'refreshTokens', 195 | tokens: tokens, 196 | }), 197 | }).then(response => response.json()); 198 | } 199 | 200 | // Claude Session Keys 201 | export function checkSessionKeys( 202 | apiAddress, 203 | tokens, 204 | maxAttempts, 205 | requestsPerSecond 206 | ) { 207 | return fetch(apiAddress, { 208 | method: 'POST', 209 | headers: { 210 | 'Content-Type': 'application/json', 211 | }, 212 | body: JSON.stringify({ 213 | type: 'sessionKeys', 214 | tokens: tokens, 215 | maxAttempts: maxAttempts, 216 | requestsPerSecond: requestsPerSecond, 217 | }), 218 | }).then(response => response.json()); 219 | } 220 | 221 | // Gemini Keys 222 | export function checkGeminiKeys( 223 | apiAddress, 224 | tokens, 225 | model, 226 | rateLimit, 227 | prompt, 228 | user 229 | ) { 230 | return fetch(apiAddress, { 231 | method: 'POST', 232 | headers: { 233 | 'Content-Type': 'application/json', 234 | }, 235 | body: JSON.stringify({ 236 | type: 'geminiAPI', 237 | tokens: tokens, 238 | model: model, 239 | rateLimit: rateLimit, 240 | prompt: prompt, 241 | user: user, 242 | }), 243 | }).then(response => response.json()); 244 | } 245 | -------------------------------------------------------------------------------- /src/utils/info.js: -------------------------------------------------------------------------------- 1 | export const appInfo = { 2 | name: 'API CHECK', // 应用名称 3 | description: { 4 | zh: [ 5 | '✨ 适用:支持one-api、new-api 等中转 OpenAI 格式的 API 检测', 6 | '🔐 安全:纯前端版本,无需担心网关超时,数据安全有保障', 7 | '🔎 直观:测试数据完整,响应时间、模型一致性直观', 8 | '☁️ 便捷:支持快速图片分享和云端数据保存', 9 | ], 10 | en: [ 11 | '✨ Suitable for testing API proxies like one-api, new-api supporting OpenAI format', 12 | '🔐 Secure: Pure front-end version, no gateway timeout concerns, data security assured', 13 | '🔎 Intuitive: Complete testing data, intuitive response times and model consistency', 14 | '☁️ Convenient: Supports quick image sharing and cloud data saving', 15 | ], 16 | }, 17 | subtitle: '纯前端 OpenAI API 检测工具', 18 | version: '2.1.0', // 版本号 19 | author: { 20 | name: 'RICK', 21 | url: 'https://blog.rick.icu', 22 | }, 23 | coauthor: { 24 | name: 'MEGASOFT', 25 | url: 'https://linux.do/u/zhong_little', 26 | }, 27 | sponsors: [ 28 | { 29 | name: 'VME50', 30 | url: 'mailto:rickhgh@foxmail.com', 31 | desc: '虚位以待', 32 | }, 33 | { 34 | name: '黄花机场', 35 | url: 'https://www.hunanairport.cn', 36 | desc: '最好的机场', 37 | }, 38 | ], 39 | updateLogUrl: 'https://github.com/october-coder/api-check/releases', 40 | contributors: [ 41 | { 42 | name: 'rick', 43 | url: 'https://linux.do/u/rick', 44 | avatar: 'https://linux.do/user_avatar/linux.do/rick/288/254826_2.png', 45 | }, 46 | { 47 | name: 'megasoft', 48 | url: 'https://linux.do/u/zhong_little', 49 | avatar: 50 | 'https://linux.do/user_avatar/linux.do/zhong_little/288/104887_2.png', 51 | }, 52 | { 53 | name: 'fangyuan', 54 | url: 'https://linux.do/u/fangyuan99', 55 | avatar: 56 | 'https://linux.do/user_avatar/linux.do/fangyuan99/288/203598_2.png', 57 | }, 58 | { 59 | name: 'juzeon', 60 | url: 'https://github.com/juzeon', 61 | avatar: 'https://avatars.githubusercontent.com/u/12206799?s=60&v=4', 62 | }, 63 | ], 64 | company: 'rick & megasoft', // 公司名称 65 | year: '2024', // 年份 66 | website: 'check.crond.dev', 67 | officialUrl: 'https://check.crond.dev', 68 | license: 'Apache', // 许可证, 69 | changelogUrl: 'https://github.com/october-coder/api-check/releases', 70 | githubUrl: 'https://github.com/october-coder/api-check', 71 | owner: 'october-coder', 72 | repo: 'api-check', 73 | }; 74 | 75 | export const banner = ` 76 | 77 | ___ ____ ____ ________ ________________ __ ______ 78 | / | / __ \\/ _/ / ____/ / / / ____/ ____/ //_// ____/ 79 | / /| | / /_/ // / / / / /_/ / __/ / / / ,< / __/ 80 | / ___ |/ ____// / / /___/ __ / /___/ /___/ /| |/ /___ 81 | /_/ |_/_/ /___/ \\____/_/ /_/_____/\\____/_/ |_/_____/ 82 | 83 | 84 | 85 | `; 86 | export const announcement = { 87 | content: { 88 | zh: [ 89 | '🎉 欢迎使用 API CHECK,一个纯前端 OpenAI API 检测工具', 90 | '启用新域名: https://check.crond.dev', 91 | ], 92 | en: [ 93 | '🎉 Welcome to API CHECK, a pure front-end OpenAI API testing tool', 94 | 'New domain: https://check.crond.dev', 95 | ], 96 | }, 97 | officialContent: { 98 | zh: [ 99 | '公益API地址: https://api.crond.dev', 100 | '项目介绍&捐赠详情: https://api.crond.dev/about', 101 | '承接赞助和捐赠,支持项目持续发展', 102 | ], 103 | en: [ 104 | 'Public API address: https://api.crond.dev', 105 | 'Project introduction & donation details: https://api.crond.dev/about', 106 | "Accepting sponsorship and donations to support the project's continued development", 107 | ], 108 | }, 109 | introduce: { 110 | zh: [ 111 | '🎉 欢迎使用 API CHECK,一个纯前端 OpenAI API 检测工具', 112 | '🔥 本工具支持测试 one-api、new-api 等中转 OpenAI 格式的 API', 113 | '🔒 纯前端版本,无需担心网关超时,数据安全有保障', 114 | '🔍 测试数据完整,响应时间、模型一致性直观', 115 | '☁️ 支持快速图片分享和云端数据保存', 116 | '🚀 本工具由 rick & megasoft 开发,欢迎体验!', 117 | ], 118 | en: [ 119 | '🎉 Welcome to API CHECK, a pure front-end OpenAI API testing tool', 120 | '🔥 This tool supports testing API proxies like one-api, new-api supporting OpenAI format', 121 | '🔒 Pure front-end version, no gateway timeout concerns, data security assured', 122 | '🔍 Complete testing data, intuitive response times and model consistency', 123 | '☁️ Supports quick image sharing and cloud data saving', 124 | '🚀 Developed by rick & megasoft, welcome to experience!', 125 | ], 126 | }, 127 | howToUse: { 128 | zh: [ 129 | '🕵️ 使用“官转验证”功能确认API的真实性', 130 | '🧊 使用“温度验证”功能确认API的一致性', 131 | '📊 使用“函数验证”功能检测API的FC支持', 132 | ], 133 | en: [ 134 | '🕵️ Use the "Official Verification" feature to confirm the authenticity of the API', 135 | '🧊 Use the "Temperature Verification" feature to confirm the consistency of the API', 136 | '📊 Use the "Function Verification" feature to detect the FC support of the API', 137 | ], 138 | }, 139 | updateLog: { 140 | zh: [ 141 | { 142 | version: 'v2.0', 143 | date: '2024-11-11', 144 | content: [ 145 | '全新样式:vue3重构项目', 146 | '新增:图片分享与API评估', 147 | '新增:docker 镜像 和 vercel部署', 148 | '新增:本地存储和云端存储', 149 | '优化:优化前端交互体验', 150 | ], 151 | url: 'https://linux.do/t/topic/256917', 152 | }, 153 | { 154 | version: 'v1.5', 155 | date: '2024-11-10', 156 | content: [ 157 | '新域名 :https://check.crond.dev', 158 | '新增:黑暗模式', 159 | '优化:优化前端交互体验', 160 | ], 161 | url: 'https://linux.do/t/topic/256917', 162 | }, 163 | { 164 | version: 'v1.4.0', 165 | date: '2024-09-08', 166 | content: [ 167 | '优化:模型名称一致性时无返回模型参数的提示信息', 168 | '新增:增加温度验证方式', 169 | '新增:增加验证按钮悬停提示', 170 | ], 171 | url: 'https://linux.do/t/topic/199694', 172 | }, 173 | { 174 | version: 'v1.3.0', 175 | date: '2024-08-31', 176 | content: ['新增:增加函数验证方式', '新增:增加官转验证方式'], 177 | url: 'https://linux.do/t/topic/191420', 178 | }, 179 | ], 180 | en: [ 181 | { 182 | version: 'v2.0', 183 | date: '2024-11-11', 184 | content: [ 185 | 'New style: vue3 refactoring project', 186 | 'New: Image sharing, API evaluation', 187 | 'New: Docker image and Vercel deployment', 188 | 'New: Local storage and cloud storage', 189 | 'Optimization: Optimize front-end interactive experience', 190 | ], 191 | url: 'https://linux.do/t/topic/256917', 192 | }, 193 | { 194 | version: 'v1.5', 195 | date: '2024-11-10', 196 | content: [ 197 | 'New domain: https://check.crond.dev', 198 | 'New: Dark mode', 199 | 'Optimization: Optimize front-end interactive experience', 200 | ], 201 | url: 'https://linux.do/t/topic/256917', 202 | }, 203 | { 204 | version: 'v1.4.0', 205 | date: '2024-09-08', 206 | content: [ 207 | 'Optimization: Prompt information when the model name consistency does not return model parameters', 208 | 'New: Added temperature verification method', 209 | 'New: Added verification button hover prompt', 210 | ], 211 | url: 'https://linux.do/t/topic/199694', 212 | }, 213 | { 214 | version: 'v1.3.0', 215 | date: '2024-08-31', 216 | content: [ 217 | 'New: Added function verification method', 218 | 'New: Added official verification method', 219 | ], 220 | url: 'https://linux.do/t/topic/191420', 221 | }, 222 | ], 223 | }, 224 | url: { 225 | githubUrl: 'https://github.com/october-coder/api-check', 226 | officialUrl: 'https://check.crond.dev', 227 | }, 228 | }; 229 | -------------------------------------------------------------------------------- /src/utils/initialization.js: -------------------------------------------------------------------------------- 1 | import { appInfo, banner } from './info.js'; 2 | export function initializeTheme(isDarkMode) { 3 | // 初始化主题 4 | const savedTheme = localStorage.getItem('theme'); 5 | if (savedTheme) { 6 | isDarkMode.value = savedTheme === 'dark'; 7 | } else { 8 | isDarkMode.value = window.matchMedia( 9 | '(prefers-color-scheme: dark)' 10 | ).matches; 11 | } 12 | document.body.classList.toggle('dark-mode', isDarkMode.value); 13 | document.body.classList.toggle('light-mode', !isDarkMode.value); 14 | } 15 | 16 | export function initializeLanguage(locale, currentLanguage) { 17 | // 初始化语言 18 | const savedLocale = localStorage.getItem('locale'); 19 | if (savedLocale) { 20 | locale.value = savedLocale; 21 | } else { 22 | locale.value = 'zh'; // 默认语言为中文 23 | } 24 | } 25 | //打印控制台 26 | export function initConsole() { 27 | const message = `hello ? ️`; 28 | console.log( 29 | `%c API CHECK v${appInfo.version} %c ${appInfo.officialUrl} `, 30 | 'color: #fadfa3; background: #030307; padding:5px 0;', 31 | 'background: #fadfa3; padding:5px 0;' 32 | ); 33 | console.log(banner); 34 | console.log(message + location.href); 35 | console.log(appInfo.author.name + ':' + appInfo.author.url); 36 | console.log(appInfo.coauthor.name + ':' + appInfo.coauthor.url); 37 | } 38 | -------------------------------------------------------------------------------- /src/utils/models.js: -------------------------------------------------------------------------------- 1 | export const commonModelList = [ 2 | // 常用模型名称列表 gpt-4o,gpt-4o-2024-05-13,gpt-4o-2024-08-06,gpt-4o-mini,gpt-4o-mini-2024-07-18 o1-mini,o1-mini-2024-09-12,o1-preview,o1-preview-2024-09-12,claude-3-5-sonnet,claude-3-5-sonnet-20240620 3 | 'gpt-4o', 4 | 'gpt-4o-2024-08-06', 5 | 'gpt-4o-mini', 6 | 'o1-mini', 7 | 'o1-mini-2024-09-12', 8 | 'o1-preview', 9 | 'o1-preview-2024-09-12', 10 | 'claude-3-5-sonnet', 11 | 'claude-3-5-sonnet-20240620', 12 | ]; 13 | 14 | export const priorityModelList = [ 15 | // 优先模型名称列表 16 | 'gpt-4o-2024-05-13', 17 | 'gpt-4o-mini-2024-07-18', 18 | 'llama-3.1-405b', 19 | ]; 20 | export const cantFunctionModelList = [ 21 | 'chatgpt-4o-latest', 22 | // 无法使用函数验证列表 23 | 'o1-mini', 24 | 'o1-mini-2024-09-12', 25 | 'o1-preview', 26 | 'o1-preview-2024-09-12', 27 | ]; 28 | export const cantTemperatureModelList = [ 29 | 'o1-mini', 30 | 'o1-mini-2024-09-12', 31 | 'o1-preview', 32 | 'o1-preview-2024-09-12', 33 | ]; 34 | export const cantOfficialModelList = [ 35 | // 无法使用官方验证列表 36 | 'o1-mini', 37 | 'o1-mini-2024-09-12', 38 | 'o1-preview', 39 | 'o1-preview-2024-09-12', 40 | ]; 41 | export const presetPromptsList = [ 42 | { 43 | title: '鲁迅暴打周树人', 44 | content: '鲁迅为什么暴打周树人?', 45 | description: 46 | 'GPT3.5 :会一本正经的胡说八道。\nGPT4 :表示鲁迅和周树人是同一个人。', 47 | }, 48 | { 49 | title: '爸妈结婚未邀请我', 50 | content: '我爸妈结婚时为什么没有邀请我?', 51 | description: 52 | 'GPT3.5 :他们当时认为你还太小,所以没有邀请你。\nGPT4 :他们结婚时你还没出生。', 53 | }, 54 | { 55 | title: '昨日的今天是明天的什么', 56 | content: "What yesterday's today is tomorrow's?", 57 | description: 'GPT3.5 :Yesterday (昨天)\nGPT4 :Past (前天)', 58 | }, 59 | { 60 | title: '树上鸟的数量', 61 | content: 62 | 'There are 9 birds in the tree,the hunter shoots one,how many birds are left in the tree?', 63 | description: 64 | 'GPT3.5 :8 birds(只会回答8只)\nGPT4 :None(其它鸟被吓跑,树上可能不剩任何鸟)', 65 | }, 66 | { 67 | title: '不存在的专辑', 68 | content: '音乐专辑什么时候发行的 Ariana Grande Eternal Sunshine?', 69 | description: 'claude-3-5-sonnet/haiku: 3月8号', 70 | }, 71 | { 72 | title: '无限序列问题', 73 | content: `有一个无限序列,从第 1 项开始,分别为 1,2,1,1,2,3,4,3,2,1,1,2,3,4,5,6,5,4,3,2,1 74 | 求第 n 项的函数g 75 | 用 Python 实现,main 函数直接输出g的前 30 项`, 76 | description: 77 | 'o1系列能答对,gemini-1.5-pro-002概率答对\n(可以根据思考时间分辨):\n 1 2 1 1 2 3 4 3 2 1 1 2 3 4 5 6 5 4 3 2 1 1 2 3 4 5 6 7 8 7', 78 | }, 79 | { 80 | title: '给主人留下些什么吧英文翻译', 81 | content: '给主人留下些什么吧翻译成英文', 82 | description: 83 | 'gpt-4o:乱回答 \n 比如:好好表现 can be translated into English as perform well or do well.', 84 | }, 85 | ]; 86 | -------------------------------------------------------------------------------- /src/utils/normal.js: -------------------------------------------------------------------------------- 1 | export function errorHandler(errorMsg) { 2 | if (errorMsg.includes('disabled.')) { 3 | errorMsg = '模型已禁用'; 4 | } else if (errorMsg.includes('负载已饱和')) { 5 | errorMsg = '负载饱和'; 6 | } else if (errorMsg.includes('is not enough')) { 7 | errorMsg = '余额不足'; 8 | } else if (errorMsg.includes('无可用渠道')) { 9 | errorMsg = '无可用渠道'; 10 | } else if (errorMsg.includes('令牌额度已用尽')) { 11 | errorMsg = '令牌额度已用尽'; 12 | } else { 13 | errorMsg = '测试失败'; 14 | } 15 | return errorMsg; 16 | } 17 | 18 | export function maskApiKey(apiKey) { 19 | if (!apiKey || apiKey.length < 10) { 20 | return apiKey; 21 | } 22 | const length = apiKey.length; 23 | const maskedSection = '****'; 24 | return apiKey.slice(0, 6) + maskedSection + apiKey.slice(length - 4); 25 | } 26 | 27 | export function isGpt(model) { 28 | return /^(gpt-|chatgpt-|o1-)/i.test(model); 29 | } 30 | 31 | export function isClaude(model) { 32 | return /^claude-/i.test(model); 33 | } 34 | 35 | export function calculateSummaryData(results) { 36 | const resultsData = results; 37 | 38 | const totalModelsTested = 39 | resultsData.valid.length + 40 | resultsData.inconsistent.length + 41 | resultsData.invalid.length; 42 | const totalAvailableModels = 43 | resultsData.valid.length + resultsData.inconsistent.length; 44 | 45 | const availableModelsRatio = totalModelsTested 46 | ? (totalAvailableModels / totalModelsTested) * 100 47 | : 0; 48 | let availableModelsScore = ((availableModelsRatio - 50) / (90 - 50)) * 100; 49 | availableModelsScore = Math.max(0, Math.min(100, availableModelsScore)); 50 | 51 | const availableModels = resultsData.valid.concat(resultsData.inconsistent); 52 | 53 | const totalAvailable = availableModels.length; 54 | const totalLatency = availableModels.reduce( 55 | (sum, r) => sum + r.responseTime, 56 | 0 57 | ); 58 | const averageLatency = totalAvailable 59 | ? (totalLatency / totalAvailable).toFixed(2) 60 | : '0'; 61 | 62 | let avgLatency = parseFloat(averageLatency); 63 | avgLatency = Math.max(0.5, Math.min(3, avgLatency)); 64 | let normalizedLatencyScore = ((3 - avgLatency) / (3 - 0.5)) * 100; 65 | normalizedLatencyScore = Math.max(0, Math.min(100, normalizedLatencyScore)); 66 | 67 | const gptModels = availableModels.filter(r => isGpt(r.model)); 68 | const claudeModels = availableModels.filter(r => isClaude(r.model)); 69 | 70 | const gptCount = gptModels.length; 71 | const claudeCount = claudeModels.length; 72 | 73 | const gptTotalLatency = gptModels.reduce((sum, r) => sum + r.responseTime, 0); 74 | const gptAverageLatency = gptCount 75 | ? (gptTotalLatency / gptCount).toFixed(2) 76 | : '0'; 77 | 78 | const claudeTotalLatency = claudeModels.reduce( 79 | (sum, r) => sum + r.responseTime, 80 | 0 81 | ); 82 | const claudeAverageLatency = claudeCount 83 | ? (claudeTotalLatency / claudeCount).toFixed(2) 84 | : '0'; 85 | 86 | const maxModelCount = 5; 87 | let gptCountScore = (gptCount / maxModelCount) * 100; 88 | gptCountScore = Math.max(0, Math.min(100, gptCountScore)); 89 | let claudeCountScore = (claudeCount / maxModelCount) * 100; 90 | claudeCountScore = Math.max(0, Math.min(100, claudeCountScore)); 91 | 92 | const radarChartData = [ 93 | availableModelsScore, 94 | normalizedLatencyScore, 95 | gptCountScore, 96 | claudeCountScore, 97 | ]; 98 | let summaryHtml = ` 99 |

📊 模型测试数据:

100 |

101 | 🔍 总共测试了 ${totalModelsTested} 个模型
102 | ✅ 可用模型总数:${totalAvailableModels}
103 | 🎯 可用且一致的模型数:${resultsData.valid.length}
104 | ⚠️ 可用但不一致的模型数:${resultsData.inconsistent.length}
105 | ❌ 不可用的模型数:${resultsData.invalid.length}
106 | ⏱️ 平均用时:${averageLatency} 秒 107 |

108 | `; 109 | 110 | let modelLatencyHtml = ''; 111 | if (gptCount > 0) { 112 | modelLatencyHtml += ` 113 | 🤖 GPT 模型数:${gptCount},平均用时:${gptAverageLatency} 秒
114 | `; 115 | } 116 | if (claudeCount > 0) { 117 | modelLatencyHtml += ` 118 | 🧠 Claude 模型数:${claudeCount},平均用时:${claudeAverageLatency} 秒 119 | `; 120 | } 121 | if (modelLatencyHtml !== '') { 122 | summaryHtml += `

📈GPT & Claude统计:

${modelLatencyHtml}

`; 123 | } 124 | const radarChartOption = { 125 | title: { 126 | text: ' ', 127 | left: 'center', 128 | }, 129 | tooltip: { 130 | trigger: 'item', 131 | }, 132 | radar: { 133 | indicator: [ 134 | { name: '可用模型比例', max: 100 }, 135 | { name: '平均延时(得分)', max: 100 }, 136 | { name: 'GPT 模型数', max: 100 }, 137 | { name: 'Claude 模型数', max: 100 }, 138 | ], 139 | shape: 'circle', 140 | splitNumber: 5, 141 | axisName: { 142 | color: '#333', 143 | }, 144 | splitLine: { 145 | lineStyle: { 146 | color: ['#ddd'], 147 | }, 148 | }, 149 | splitArea: { 150 | show: false, 151 | }, 152 | axisLine: { 153 | lineStyle: { 154 | color: '#bbb', 155 | }, 156 | }, 157 | }, 158 | series: [ 159 | { 160 | name: 'API 评估', 161 | type: 'radar', 162 | data: [ 163 | { 164 | value: radarChartData, 165 | name: '评分', 166 | areaStyle: { 167 | color: 'rgba(0, 102, 204, 0.2)', 168 | }, 169 | }, 170 | ], 171 | }, 172 | ], 173 | }; 174 | 175 | return { 176 | summaryHtml, 177 | radarChartOption, 178 | }; 179 | } 180 | 181 | export function extractApiInfo(text) { 182 | let apiUrl = ''; 183 | let apiKey = ''; 184 | 185 | let urlPattern = /(https?:\/\/[^\s,。、!,;;\n]+)/; 186 | let keyPattern = /(sk-[a-zA-Z0-9]+)/; 187 | 188 | let urlMatch = text.match(urlPattern); 189 | let keyMatch = text.match(keyPattern); 190 | 191 | if (urlMatch) { 192 | // 去除末尾的斜杠和多余字符,保留到最后一个斜杠前面 193 | let cleanUrlMatch = urlMatch[0].match(/(.*)\/.*/); 194 | if (cleanUrlMatch) { 195 | let cleanUrl = cleanUrlMatch[1]; 196 | // 如果包含 '.',则使用清理后的 URL 197 | if (cleanUrl.includes('.')) { 198 | apiUrl = cleanUrl; 199 | } else { 200 | apiUrl = urlMatch[0]; 201 | } 202 | } else { 203 | apiUrl = urlMatch[0]; 204 | } 205 | } 206 | 207 | if (keyMatch) { 208 | apiKey = keyMatch[0]; 209 | } 210 | 211 | return { apiUrl, apiKey }; 212 | } 213 | -------------------------------------------------------------------------------- /src/utils/svg.js: -------------------------------------------------------------------------------- 1 | import { errorHandler } from './normal.js'; 2 | import { commonModelList, priorityModelList } from './models.js'; 3 | 4 | // 设置其他模型的最大显示数量 5 | const MAX_OTHER_MODELS = 20; 6 | 7 | // 设置未展示模型的最大显示行数 8 | const MAX_UNDISPLAYED_MODEL_LINES = 2; 9 | 10 | // 处理结果数据的函数 11 | function processResults(results) { 12 | // 定义模型识别函数 13 | function isGpt(model) { 14 | const lowerModel = model.toLowerCase(); 15 | return ( 16 | lowerModel.includes('gpt-4') || 17 | lowerModel.includes('chatgpt') || 18 | lowerModel.startsWith('o1-') || 19 | /^(gpt-|chatgpt-|o1-)/i.test(model) || 20 | lowerModel.includes('gpt') 21 | ); 22 | } 23 | 24 | function isClaude(model) { 25 | return /^claude-/i.test(model) || model.toLowerCase().includes('claude'); 26 | } 27 | 28 | function isDeepSeek(model) { 29 | return model.toLowerCase().includes('deepseek'); 30 | } 31 | 32 | function isPriorityModel(model) { 33 | return priorityModelList.includes(model.toLowerCase()); 34 | } 35 | 36 | // 初始化存储处理后的数据结构 37 | const processedData = { 38 | summary: { 39 | totalTested: 0, 40 | availableModels: 0, 41 | availableRatio: 0, 42 | averageLatency: 0, 43 | gptCount: 0, 44 | claudeCount: 0, 45 | }, 46 | commonModels: [], 47 | otherModels: [], 48 | undisplayedAvailableModels: [], 49 | failedModels: [], 50 | }; 51 | 52 | // 统计总模型数量和可用模型数量 53 | const totalModelsTested = 54 | results.valid.length + results.inconsistent.length + results.invalid.length; 55 | const totalAvailableModels = 56 | results.valid.length + results.inconsistent.length; 57 | 58 | processedData.summary.totalTested = totalModelsTested; 59 | processedData.summary.availableModels = totalAvailableModels; 60 | 61 | // 计算可用率 62 | processedData.summary.availableRatio = totalModelsTested 63 | ? ((totalAvailableModels / totalModelsTested) * 100).toFixed(2) 64 | : 0; 65 | 66 | // 计算平均响应时间 67 | const availableModelsList = results.valid.concat(results.inconsistent); 68 | const totalLatency = availableModelsList.reduce( 69 | (sum, r) => sum + r.responseTime, 70 | 0 71 | ); 72 | processedData.summary.averageLatency = totalAvailableModels 73 | ? (totalLatency / totalAvailableModels).toFixed(2) 74 | : '0'; 75 | 76 | // 统计 GPT 和 Claude 模型数量 77 | processedData.summary.gptCount = availableModelsList.filter(r => 78 | isGpt(r.model) 79 | ).length; 80 | processedData.summary.claudeCount = availableModelsList.filter(r => 81 | isClaude(r.model) 82 | ).length; 83 | 84 | // 从结果中整理模型数据,标记状态 85 | const allModels = []; 86 | 87 | results.valid.forEach(r => { 88 | allModels.push({ ...r, status: 'valid' }); 89 | }); 90 | results.inconsistent.forEach(r => { 91 | allModels.push({ ...r, status: 'inconsistent' }); 92 | }); 93 | results.invalid.forEach(r => { 94 | allModels.push({ ...r, status: 'invalid' }); 95 | }); 96 | 97 | // 常用模型列表,不截断,全部展示,无论状态 98 | const commonModelsSet = new Set(commonModelList.map(m => m.toLowerCase())); 99 | processedData.commonModels = allModels.filter(r => 100 | commonModelsSet.has(r.model.toLowerCase()) 101 | ); 102 | 103 | // 已展示的模型集合 104 | const displayedModelNames = new Set( 105 | processedData.commonModels.map(r => r.model) 106 | ); 107 | 108 | // 其他模型列表 109 | 110 | // 在其他模型中,首先提取优先模型 111 | let otherModels = allModels.filter(r => !displayedModelNames.has(r.model)); 112 | 113 | // 优先模型列表 114 | let priorityModels = otherModels.filter(r => isPriorityModel(r.model)); 115 | 116 | // 移除已提取的优先模型 117 | otherModels = otherModels.filter(r => !isPriorityModel(r.model)); 118 | 119 | // 将优先模型按状态分类 120 | const priorityModelsByStatus = { 121 | valid: [], 122 | inconsistent: [], 123 | invalid: [], 124 | }; 125 | 126 | priorityModels.forEach(model => { 127 | priorityModelsByStatus[model.status].push(model); 128 | }); 129 | 130 | // 将剩余模型按状态分类 131 | const remainingModelsByStatus = { 132 | valid: [], 133 | inconsistent: [], 134 | invalid: [], 135 | }; 136 | 137 | otherModels.forEach(model => { 138 | remainingModelsByStatus[model.status].push(model); 139 | }); 140 | 141 | // 将优先模型中只有一个模型的状态合并到剩余模型对应的状态开头 142 | ['valid', 'inconsistent', 'invalid'].forEach(status => { 143 | if (priorityModelsByStatus[status].length === 1) { 144 | const modelToMove = priorityModelsByStatus[status][0]; 145 | remainingModelsByStatus[status].unshift(modelToMove); 146 | priorityModelsByStatus[status] = []; 147 | } 148 | }); 149 | 150 | // 重新组合优先模型,只有状态中有多个模型的才保留 151 | const filteredPriorityModels = []; 152 | ['valid', 'inconsistent', 'invalid'].forEach(status => { 153 | if (priorityModelsByStatus[status].length > 1) { 154 | filteredPriorityModels.push(...priorityModelsByStatus[status]); 155 | } 156 | }); 157 | 158 | // 处理其他类别的模型(如 Claude、DeepSeek) 159 | let claudeModels = []; 160 | let deepSeekModels = []; 161 | let remainingModels = []; 162 | 163 | otherModels.forEach(model => { 164 | if (isClaude(model.model)) { 165 | claudeModels.push(model); 166 | } else if (isDeepSeek(model.model)) { 167 | deepSeekModels.push(model); 168 | } else { 169 | remainingModels.push(model); 170 | } 171 | }); 172 | 173 | // 合并所有模型,按照状态分类 174 | const combinedModelsByStatus = { 175 | valid: [], 176 | inconsistent: [], 177 | invalid: [], 178 | }; 179 | 180 | // 将过滤后的优先模型加入到对应的状态 181 | ['valid', 'inconsistent', 'invalid'].forEach(status => { 182 | filteredPriorityModels.forEach(model => { 183 | if (model.status === status) { 184 | combinedModelsByStatus[status].push(model); 185 | } 186 | }); 187 | }); 188 | 189 | // 将 Claude 和 DeepSeek 模型也加入到对应的状态 190 | [claudeModels, deepSeekModels, remainingModels].forEach(modelList => { 191 | modelList.forEach(model => { 192 | combinedModelsByStatus[model.status].push(model); 193 | }); 194 | }); 195 | 196 | // 计算每个状态的模型数量和总模型数量 197 | const totalModelsPerStatus = {}; 198 | let totalModels = 0; 199 | ['valid', 'inconsistent', 'invalid'].forEach(status => { 200 | totalModelsPerStatus[status] = combinedModelsByStatus[status].length; 201 | totalModels += combinedModelsByStatus[status].length; 202 | }); 203 | 204 | // 按照比例分配每个状态应该展示的模型数量 205 | const counts = allocateModelsProportionally( 206 | totalModelsPerStatus, 207 | MAX_OTHER_MODELS 208 | ); 209 | 210 | // 组合最终要展示的模型列表 211 | const displayedOtherModels = []; 212 | ['valid', 'inconsistent', 'invalid'].forEach(status => { 213 | const modelsToDisplay = combinedModelsByStatus[status].slice( 214 | 0, 215 | counts[status] 216 | ); 217 | displayedOtherModels.push(...modelsToDisplay); 218 | }); 219 | 220 | // 更新已展示的模型集合 221 | displayedOtherModels.forEach(r => displayedModelNames.add(r.model)); 222 | 223 | // 将未展示的模型添加到未展示的可用模型和调用失败的模型列表中 224 | ['valid', 'inconsistent', 'invalid'].forEach(status => { 225 | const models = combinedModelsByStatus[status]; 226 | const undisplayedModels = models.slice(counts[status]); 227 | undisplayedModels.forEach(r => { 228 | if ( 229 | (r.status === 'valid' || r.status === 'inconsistent') && 230 | !displayedModelNames.has(r.model) 231 | ) { 232 | processedData.undisplayedAvailableModels.push(r); 233 | } else if (r.status === 'invalid' && !displayedModelNames.has(r.model)) { 234 | processedData.failedModels.push(r); 235 | } 236 | }); 237 | }); 238 | 239 | processedData.otherModels = displayedOtherModels; 240 | 241 | return processedData; 242 | } 243 | 244 | // 按照比例分配模型数量的函数 245 | function allocateModelsProportionally(totalModelsPerStatus, maxModels) { 246 | const counts = {}; 247 | const statuses = ['valid', 'inconsistent', 'invalid']; 248 | 249 | let totalModels = statuses.reduce( 250 | (sum, status) => sum + totalModelsPerStatus[status], 251 | 0 252 | ); 253 | let totalAssigned = 0; 254 | 255 | // 计算每个状态的初始配额 256 | const quotas = {}; 257 | const remainders = {}; 258 | 259 | statuses.forEach(status => { 260 | quotas[status] = (totalModelsPerStatus[status] / totalModels) * maxModels; 261 | counts[status] = Math.floor(quotas[status]); 262 | remainders[status] = quotas[status] - counts[status]; 263 | }); 264 | 265 | totalAssigned = statuses.reduce((sum, status) => sum + counts[status], 0); 266 | 267 | // 确保每个有模型的状态至少分配一个模型 268 | statuses.forEach(status => { 269 | if (counts[status] === 0 && totalModelsPerStatus[status] > 0) { 270 | counts[status] = 1; 271 | totalAssigned++; 272 | } 273 | }); 274 | 275 | // 调整分配数量以符合总模型数量限制 276 | // 如果分配的总数超过最大值,减少分配 277 | while (totalAssigned > maxModels) { 278 | // 按照 remainders 从小到大排序 279 | statuses.sort((a, b) => remainders[a] - remainders[b]); 280 | for (let status of statuses) { 281 | if (counts[status] > 1) { 282 | counts[status]--; 283 | totalAssigned--; 284 | break; 285 | } 286 | } 287 | } 288 | 289 | // 如果分配的总数不足,增加分配 290 | while (totalAssigned < maxModels) { 291 | // 按照 remainders 从大到小排序 292 | statuses.sort((a, b) => remainders[b] - remainders[a]); 293 | for (let status of statuses) { 294 | if (counts[status] < totalModelsPerStatus[status]) { 295 | counts[status]++; 296 | totalAssigned++; 297 | break; 298 | } 299 | } 300 | } 301 | 302 | return counts; 303 | } 304 | 305 | // 根据处理后的数据生成 SVG 的函数 306 | export function createSVGDataURL(results, title) { 307 | // 调用 processResults 处理数据 308 | const processedData = processResults(results); 309 | 310 | const testTime = 311 | new Date() 312 | .toLocaleDateString('zh-CN', { 313 | year: 'numeric', 314 | month: '2-digit', 315 | day: '2-digit', 316 | }) 317 | .replace(/\//g, '.') + ' '; 318 | const minSvgWidth = 400; 319 | const maxSvgWidth = 800; 320 | const marginX = 25; // 左右边距保持25像素 321 | const lineHeight = 25; 322 | 323 | // 计算实际需要显示的行数 324 | let displayedLines = 4; // 初始的标题和空行数 325 | 326 | if (processedData.commonModels.length > 0) { 327 | displayedLines += 2; // 空行 + "常用模型"标题 328 | displayedLines += processedData.commonModels.length; // 常用模型的行数 329 | } 330 | 331 | if (processedData.otherModels.length > 0) { 332 | displayedLines += 2; // 空行 + "其他模型"标题 333 | displayedLines += processedData.otherModels.length; // 其他模型的行数 334 | } 335 | 336 | // 如果有未展示的可用模型或调用失败的模型,增加 "省略部分" 标题行 337 | if ( 338 | processedData.undisplayedAvailableModels.length > 0 || 339 | processedData.failedModels.length > 0 340 | ) { 341 | displayedLines += 2; // 空行 + "省略部分"标题 342 | } 343 | 344 | // 如果有未展示的可用模型,增加两行 345 | if (processedData.undisplayedAvailableModels.length > 0) { 346 | displayedLines += MAX_UNDISPLAYED_MODEL_LINES; 347 | } 348 | 349 | // 如果有未展示的调用失败的模型,增加两行 350 | if (processedData.failedModels.length > 0) { 351 | displayedLines += MAX_UNDISPLAYED_MODEL_LINES; 352 | } 353 | 354 | // 计算 SVG 高度 355 | const svgHeight = displayedLines * lineHeight + 150; // 额外的空间用于顶部和底部 356 | 357 | // **计算动态 SVG 宽度** 358 | 359 | // 定义最大字符串长度 360 | const maxModelNameLength = 30; // 不截断,显示完整名称 361 | const maxRemarkLength = 50; // 不截断,显示完整备注 362 | 363 | // 估计内容所需的宽度 364 | const textWidthPerChar = 8; // 每个字符约占8像素宽度 365 | 366 | // 计算列宽度 367 | const col1Width = 100; 368 | const col2Width = maxModelNameLength * textWidthPerChar; 369 | const col3Width = 80; 370 | const col4Width = maxRemarkLength * textWidthPerChar; 371 | 372 | // 计算总宽度 373 | let calculatedSvgWidth = 374 | marginX * 2 + col1Width + col2Width + col3Width + col4Width + 40; // 额外的40像素用于列间间距 375 | 376 | // 限制宽度在最小值和最大值之间 377 | calculatedSvgWidth = Math.max(minSvgWidth, calculatedSvgWidth); 378 | calculatedSvgWidth = Math.min(maxSvgWidth, calculatedSvgWidth); 379 | 380 | const svgWidth = calculatedSvgWidth; 381 | 382 | // 调整列的 X 坐标 383 | const col1X = marginX + 10; // 第一列的X坐标 384 | const col2X = col1X + col1Width; 385 | const col3X = col2X + col2Width + 10; 386 | const col4X = col3X + col3Width + 10; 387 | 388 | // 开始构建 SVG 内容 389 | let svgContent = ``; 390 | 391 | // 定义渐变 392 | svgContent += ``; 393 | 394 | // 应用背景渐变 395 | svgContent += ``; 396 | 397 | const contentBoxY = 50; // 调整内容框的Y坐标,以上移内容 398 | const contentBoxHeight = svgHeight - 100; // 调整内容框高度 399 | 400 | svgContent += ``; 403 | 404 | // 标题和图标 405 | const icons = [ 406 | { cx: marginX + 20, cy: contentBoxY + 25, r: 6, fill: '#ff5f56' }, 407 | { cx: marginX + 40, cy: contentBoxY + 25, r: 6, fill: '#ffbd2e' }, 408 | { cx: marginX + 60, cy: contentBoxY + 25, r: 6, fill: '#27c93f' }, 409 | ]; 410 | icons.forEach(icon => { 411 | svgContent += ``; 412 | }); 413 | 414 | // 标题 415 | svgContent += `API CHECK`; 418 | 419 | let y = contentBoxY + 30; 420 | 421 | y += lineHeight; // 添加一个空行 422 | 423 | // 添加来源和时间,左对齐 424 | y += lineHeight; 425 | const fromX = marginX + 10; // 左对齐的起始位置 426 | svgContent += `🔗 来源:${title} ⏰ 时间:${testTime}`; 427 | 428 | // 显示统计信息,左对齐 429 | y += lineHeight; 430 | 431 | const summaryText = `📊 共测试 ${processedData.summary.totalTested} 个模型,💡 可用率 ${processedData.summary.availableRatio}% ,⏱ 平均响应时间 ${processedData.summary.averageLatency}s ,🧠 GPT ${processedData.summary.gptCount} Claude ${processedData.summary.claudeCount} `; 432 | svgContent += drawText(fromX, y, summaryText, '14', '#FFFFFF', 'bold'); 433 | 434 | y += lineHeight; // 添加一个空行 435 | 436 | // 绘制常用模型 437 | if (processedData.commonModels.length > 0) { 438 | y += lineHeight; // 空行 439 | svgContent += drawText(col1X, y, '🌟 常用模型:', '16', '#FFA500', 'bold'); 440 | y += lineHeight; 441 | 442 | processedData.commonModels.forEach(r => { 443 | let statusText = ''; 444 | let statusColor = '#ffffff'; 445 | let modelColor = '#59e3ff'; 446 | let remarkColor = '#3f1'; // 默认备注颜色 447 | 448 | const modelName = r.model; 449 | 450 | if (r.status === 'valid') { 451 | // 一致可用 452 | statusText = '🥳 一致可用'; 453 | remarkColor = '#3f1'; 454 | svgContent += drawText(col1X, y, statusText, '16', statusColor); 455 | svgContent += drawText(col2X, y, modelName, '16', modelColor, 'bold'); 456 | svgContent += drawText( 457 | col3X, 458 | y, 459 | r.responseTime.toFixed(2) + 's', 460 | '16', 461 | modelColor 462 | ); 463 | svgContent += drawText(col4X, y, '模型校验成功', '16', remarkColor); 464 | } else if (r.status === 'inconsistent') { 465 | // 未匹配/模型映射 466 | const returnedModel = r.returnedModel || ''; 467 | 468 | if (returnedModel.startsWith(`${r.model}-`)) { 469 | statusText = '😲 模型映射'; 470 | } else { 471 | statusText = '🤔 未匹配'; 472 | } 473 | svgContent += drawText(col1X, y, statusText, '16', statusColor); 474 | svgContent += drawText(col2X, y, modelName, '16', '#ff6b6b', 'bold'); 475 | svgContent += drawText( 476 | col3X, 477 | y, 478 | r.responseTime.toFixed(2) + 's', 479 | '16', 480 | modelColor 481 | ); 482 | svgContent += drawText(col4X, y, `${returnedModel}`, '16', modelColor); 483 | } else if (r.status === 'invalid') { 484 | // 调用失败 485 | statusText = '😡 调用失败'; 486 | let msg; 487 | if (r.error) { 488 | msg = errorHandler(r.error); 489 | } else { 490 | msg = errorHandler(r.response_text); 491 | } 492 | svgContent += drawText(col1X, y, statusText, '16', statusColor); 493 | svgContent += drawText(col2X, y, modelName, '16', '#ffffff'); 494 | svgContent += drawText(col3X, y, '-', '16', '#ff6b6b'); 495 | svgContent += drawText(col4X, y, msg, '16', '#ffffff'); 496 | } 497 | y += lineHeight; 498 | }); 499 | } 500 | 501 | // 绘制其他模型 502 | if (processedData.otherModels.length > 0) { 503 | y += lineHeight; // 空行 504 | svgContent += drawText(col1X, y, '🚀 普通模型:', '16', '#FFA500', 'bold'); 505 | y += lineHeight; 506 | 507 | processedData.otherModels.forEach(r => { 508 | let statusText = ''; 509 | let statusColor = '#ffffff'; 510 | let modelColor = '#59e3ff'; 511 | let remarkColor = '#3f1'; // 默认备注颜色 512 | 513 | const modelName = r.model; 514 | 515 | if (r.status === 'valid') { 516 | // 一致可用 517 | statusText = '🥳 一致可用'; 518 | remarkColor = '#3f1'; 519 | svgContent += drawText(col1X, y, statusText, '16', statusColor); 520 | svgContent += drawText(col2X, y, modelName, '16', modelColor, 'bold'); 521 | svgContent += drawText( 522 | col3X, 523 | y, 524 | r.responseTime.toFixed(2) + 's', 525 | '16', 526 | modelColor 527 | ); 528 | svgContent += drawText(col4X, y, '模型校验成功', '16', remarkColor); 529 | } else if (r.status === 'inconsistent') { 530 | // 未匹配/模型映射 531 | const returnedModel = r.returnedModel || ''; 532 | 533 | if (returnedModel.startsWith(`${r.model}-`)) { 534 | statusText = '😲 模型映射'; 535 | } else { 536 | statusText = '🤔 未匹配'; 537 | } 538 | svgContent += drawText(col1X, y, statusText, '16', statusColor); 539 | svgContent += drawText(col2X, y, modelName, '16', '#ff6b6b', 'bold'); 540 | svgContent += drawText( 541 | col3X, 542 | y, 543 | r.responseTime.toFixed(2) + 's', 544 | '16', 545 | modelColor 546 | ); 547 | svgContent += drawText(col4X, y, `${returnedModel}`, '16', modelColor); 548 | } else if (r.status === 'invalid') { 549 | // 调用失败 550 | statusText = '😡 调用失败'; 551 | let msg; 552 | if (r.error) { 553 | msg = errorHandler(r.error); 554 | } else { 555 | msg = errorHandler(r.response_text); 556 | } 557 | svgContent += drawText(col1X, y, statusText, '16', statusColor); 558 | svgContent += drawText(col2X, y, modelName, '16', '#ffffff'); 559 | svgContent += drawText(col3X, y, '-', '16', '#ff6b6b'); 560 | svgContent += drawText(col4X, y, msg, '16', '#ffffff'); 561 | } 562 | y += lineHeight; 563 | }); 564 | } 565 | 566 | // 添加省略部分标题 567 | if ( 568 | processedData.undisplayedAvailableModels.length > 0 || 569 | processedData.failedModels.length > 0 570 | ) { 571 | y += lineHeight; // 空行 572 | svgContent += drawText(col1X, y, '📌 省略部分:', '16', '#FFA500', 'bold'); 573 | y += lineHeight; 574 | } 575 | 576 | // 添加未展示的可用模型 577 | if (processedData.undisplayedAvailableModels.length > 0) { 578 | const maxLines = MAX_UNDISPLAYED_MODEL_LINES; 579 | let undisplayedModelNames = processedData.undisplayedAvailableModels.map( 580 | r => r.model 581 | ); 582 | const lineWidth = svgWidth - 2 * marginX; 583 | const textPerLine = Math.floor(lineWidth / textWidthPerChar); 584 | // 修改 maxChars 计算方式,使其在第二行达到一半时截断 585 | const maxChars = Math.floor(textPerLine * 1.5); 586 | const prefix = '😀 可用模型:'; 587 | let contentText = prefix + undisplayedModelNames.join('、'); 588 | 589 | if (getTextWidth(contentText) <= maxChars * textWidthPerChar) { 590 | // 文本在限制的字符数内,正常显示 591 | let undisplayedTextLines = wrapText(contentText, textPerLine); 592 | 593 | undisplayedTextLines.forEach(line => { 594 | svgContent += drawText(fromX, y, line, '14', '#FFFFFF'); 595 | y += lineHeight; 596 | }); 597 | } else { 598 | // 文本超过限制,需要截断并添加省略信息 599 | let availableChars = 600 | maxChars - Math.ceil(getTextWidth(prefix) / textWidthPerChar); 601 | let displayedNames = []; 602 | let totalLength = 0; 603 | for (let name of undisplayedModelNames) { 604 | let nameLength = name.length + 1; // 加1考虑“、” 605 | if (totalLength + nameLength <= availableChars) { 606 | displayedNames.push(name); 607 | totalLength += nameLength; 608 | } else { 609 | break; 610 | } 611 | } 612 | let omittedCount = undisplayedModelNames.length - displayedNames.length; 613 | let finalText = 614 | prefix + displayedNames.join('、') + `...(省略${omittedCount}个模型)`; 615 | let undisplayedTextLines = wrapText(finalText, textPerLine); 616 | undisplayedTextLines.forEach(line => { 617 | svgContent += drawText(fromX, y, line, '14', '#FFFFFF'); 618 | y += lineHeight; 619 | }); 620 | } 621 | } 622 | 623 | // 添加调用失败的模型 624 | if (processedData.failedModels.length > 0) { 625 | const maxLines = MAX_UNDISPLAYED_MODEL_LINES; 626 | let failedModelNames = processedData.failedModels.map(r => r.model); 627 | const lineWidth = svgWidth - 2 * marginX; 628 | const textPerLine = Math.floor(lineWidth / textWidthPerChar); 629 | // 修改 maxChars 计算方式,使其在第二行达到一半时截断 630 | const maxChars = Math.floor(textPerLine * 1.5); 631 | const prefix = '😞 调用失败:'; 632 | let contentText = prefix + failedModelNames.join('、'); 633 | 634 | if (getTextWidth(contentText) <= maxChars * textWidthPerChar) { 635 | // 文本在限制的字符数内,正常显示 636 | let failedTextLines = wrapText(contentText, textPerLine); 637 | 638 | failedTextLines.forEach(line => { 639 | svgContent += drawText(fromX, y, line, '14', '#FFFFFF'); 640 | y += lineHeight; 641 | }); 642 | } else { 643 | // 文本超过限制,需要截断并添加省略信息 644 | let availableChars = 645 | maxChars - Math.ceil(getTextWidth(prefix) / textWidthPerChar); 646 | let displayedNames = []; 647 | let totalLength = 0; 648 | for (let name of failedModelNames) { 649 | let nameLength = name.length + 1; // 加1考虑“、” 650 | if (totalLength + nameLength <= availableChars) { 651 | displayedNames.push(name); 652 | totalLength += nameLength; 653 | } else { 654 | break; 655 | } 656 | } 657 | let omittedCount = failedModelNames.length - displayedNames.length; 658 | let finalText = 659 | prefix + displayedNames.join('、') + `...(省略${omittedCount}个模型)`; 660 | let failedTextLines = wrapText(finalText, textPerLine); 661 | failedTextLines.forEach(line => { 662 | svgContent += drawText(fromX, y, line, '14', '#FFFFFF'); 663 | y += lineHeight; 664 | }); 665 | } 666 | } 667 | 668 | // 添加版权说明 669 | svgContent += `© 2024 API CHECK | DEV API | BY RICK`; 672 | 673 | // 结束 SVG 标签 674 | svgContent += ``; 675 | 676 | // 将 SVG 内容编码为 Data URL 677 | const svgDataUrl = `data:image/svg+xml;charset=utf-8,${encodeURIComponent( 678 | svgContent 679 | )}`; 680 | 681 | // 返回 Data URL 682 | return svgDataUrl; 683 | } 684 | 685 | // 添加绘制文本的函数 686 | function drawText(x, y, textContent, fontSize, fill, fontWeight = 'normal') { 687 | return `${textContent}`; 688 | } 689 | 690 | // 计算文本宽度的函数 691 | function getTextWidth(text) { 692 | const textWidthPerChar = 8; // 每个字符约占8像素宽度 693 | return text.length * textWidthPerChar; 694 | } 695 | 696 | // 自动换行的函数 697 | function wrapText(text, maxCharsPerLine) { 698 | let lines = []; 699 | let currentLine = ''; 700 | let tokens = text.split(/(?<=、)/); // 保留分割符“、” 701 | tokens.forEach((token, index) => { 702 | const tokenLength = token.length; 703 | if (currentLine.length + tokenLength <= maxCharsPerLine) { 704 | currentLine += token; 705 | } else { 706 | if (currentLine) { 707 | lines.push(currentLine); 708 | } 709 | currentLine = token; 710 | } 711 | }); 712 | if (currentLine) { 713 | lines.push(currentLine); 714 | } 715 | return lines; 716 | } 717 | -------------------------------------------------------------------------------- /src/utils/theme.js: -------------------------------------------------------------------------------- 1 | export function toggleTheme(isDarkMode) { 2 | isDarkMode.value = !isDarkMode.value; 3 | localStorage.setItem('theme', isDarkMode.value ? 'dark' : 'light'); 4 | } 5 | -------------------------------------------------------------------------------- /src/utils/update.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 检查是否有新版本发布 3 | * @param {string} currentVersion - 当前版本号 4 | * @param {string} owner - GitHub 仓库所有者用户名 5 | * @param {string} repo - GitHub 仓库名称 6 | * @param {function} t - 国际化函数,用于日志输出中的国际化 7 | * @returns {Promise} 如果有新版本,返回更新信息对象;否则返回 null 8 | */ 9 | export async function checkForUpdates(currentVersion, owner, repo, t) { 10 | const lastCheck = localStorage.getItem('lastUpdateCheck'); 11 | const now = Date.now(); 12 | const CHECK_INTERVAL = 10 * 12 * 60 * 60 * 1000; // 120 小时 13 | 14 | if (lastCheck && now - lastCheck < CHECK_INTERVAL) { 15 | // 最近已检查过,不需要再次检查 16 | return null; 17 | } 18 | 19 | // 保存当前检查时间 20 | localStorage.setItem('lastUpdateCheck', now); 21 | 22 | const apiUrl = `https://api.github.com/repos/${owner}/${repo}/releases/latest`; 23 | 24 | try { 25 | const response = await fetch(apiUrl); 26 | if (!response.ok) { 27 | console.error(t('UPDATE_CHECK_FAILED')); 28 | return null; 29 | } 30 | const data = await response.json(); 31 | const latestVersion = data.tag_name.replace(/^v/, ''); // 去除版本号前的 'v' 字符 32 | 33 | // 比较版本号 34 | if (isNewerVersion(latestVersion, currentVersion)) { 35 | return { 36 | hasUpdate: true, 37 | latestVersion: latestVersion, 38 | releaseNotes: data.body, // 发布说明 39 | htmlUrl: data.html_url, // 发布页面链接 40 | }; 41 | } else { 42 | return { 43 | hasUpdate: false, 44 | }; 45 | } 46 | } catch (error) { 47 | console.error(t('UPDATE_CHECK_ERROR'), error); 48 | return null; 49 | } 50 | } 51 | 52 | /** 53 | * 比较版本号,判断是否有新版本 54 | * @param {string} latest - 最新版本号 55 | * @param {string} current - 当前版本号 56 | * @returns {boolean} 如果最新版本号大于当前版本号,返回 true;否则返回 false 57 | */ 58 | function isNewerVersion(latest, current) { 59 | const latestParts = latest.split('.').map(Number); 60 | const currentParts = current.split('.').map(Number); 61 | 62 | const length = Math.max(latestParts.length, currentParts.length); 63 | for (let i = 0; i < length; i++) { 64 | const latestNum = latestParts[i] || 0; 65 | const currentNum = currentParts[i] || 0; 66 | if (latestNum > currentNum) { 67 | return true; 68 | } else if (latestNum < currentNum) { 69 | return false; 70 | } 71 | } 72 | return false; 73 | } 74 | -------------------------------------------------------------------------------- /src/utils/verify.js: -------------------------------------------------------------------------------- 1 | class ModelVerifier { 2 | constructor(apiUrl, apiKey) { 3 | this.apiUrl = apiUrl.replace(/\/+$/, ''); // 去除末尾的斜杠 4 | this.apiKey = apiKey; 5 | } 6 | 7 | /** 8 | * 温度验证 9 | * @param {string} model - 模型名称 10 | * @returns {Object} - 包含响应结果和结论的对象 11 | */ 12 | async verifyTemperature(model) { 13 | try { 14 | const results = await Promise.all( 15 | [1, 2, 3, 4].map(() => this.sendTemperatureVerificationRequest(model)) 16 | ); 17 | const responses = results.map(result => 18 | result.choices 19 | ? result?.choices?.[0]?.message?.content?.trim() 20 | : '该次调用响应异常' 21 | ); 22 | 23 | const referenceMap = { 24 | 'gpt-4o-mini': 32, 25 | 'gpt-4o': 59, 26 | 'claude-3-5': 51, 27 | 'claude-3.5': 51, 28 | }; 29 | const matchedKey = Object.keys(referenceMap).find(key => 30 | model.startsWith(key) 31 | ); 32 | let referenceValue = matchedKey ? referenceMap[matchedKey] : null; 33 | 34 | let hitReferenceCount = 0; 35 | for (let i = 0; i < 4; i++) { 36 | if (responses[i] === referenceValue) { 37 | hitReferenceCount++; 38 | } 39 | } 40 | 41 | const frequencyCheckResult = this.findMostFrequent(responses); 42 | const mostFrequentCount = frequencyCheckResult.count; 43 | 44 | let conclusion; 45 | if (mostFrequentCount === responses.length) { 46 | conclusion = '所有响应相同,可能是官方API'; 47 | } else { 48 | conclusion = `响应结果重复度:${mostFrequentCount}/${responses.length}`; 49 | if (/^(gpt-4o|claude-3-5|claude-3.5)/.test(model)) { 50 | conclusion += `,参考值命中率:${hitReferenceCount}/${responses.length}`; 51 | } 52 | conclusion += ',可能不是官方API'; 53 | } 54 | 55 | // 返回结果对象 56 | return { 57 | model: model, 58 | responses: responses, 59 | referenceValue: referenceValue, 60 | hitReferenceCount: hitReferenceCount, 61 | conclusion: conclusion, 62 | }; 63 | } catch (error) { 64 | console.error('Error in verifyTemperature:', error); 65 | throw error; 66 | } 67 | } 68 | 69 | /** 70 | * 发送温度验证请求 71 | * @param {string} model - 模型名称 72 | * @returns {Object} - 请求的响应结果 73 | */ 74 | async sendTemperatureVerificationRequest(model) { 75 | try { 76 | const response = await fetch(`${this.apiUrl}/v1/chat/completions`, { 77 | method: 'POST', 78 | headers: { 79 | Authorization: `Bearer ${this.apiKey}`, 80 | 'Content-Type': 'application/json', 81 | }, 82 | body: JSON.stringify({ 83 | messages: [ 84 | { 85 | role: 'system', 86 | content: 87 | "You're an associative thinker. The user gives you a sequence of 6 numbers. Your task is to figure out and provide the 7th number directly, without explaining how you got there.", 88 | }, 89 | { 90 | role: 'user', 91 | content: '5, 15, 77, 19, 53, 54,', 92 | }, 93 | ], 94 | temperature: 0.01, 95 | model: model, 96 | }), 97 | }); 98 | 99 | if (!response.ok) { 100 | throw new Error(`HTTP error! status: ${response.status}`); 101 | } 102 | 103 | return await response.json(); 104 | } catch (error) { 105 | console.error('Error in sendTemperatureVerificationRequest:', error); 106 | return { error: error.message }; 107 | } 108 | } 109 | 110 | /** 111 | * 查找数组中出现频率最高的元素 112 | * @param {Array} arr - 要检查的数组 113 | * @returns {Object} - 包含最频繁元素及其出现次数的对象 114 | */ 115 | findMostFrequent(arr) { 116 | const frequency = {}; 117 | let maxFreq = 0; 118 | let mostFrequentItem = null; 119 | 120 | arr.forEach(item => { 121 | frequency[item] = (frequency[item] || 0) + 1; 122 | if (frequency[item] > maxFreq) { 123 | maxFreq = frequency[item]; 124 | mostFrequentItem = item; 125 | } 126 | }); 127 | 128 | return { item: mostFrequentItem, count: maxFreq }; 129 | } 130 | 131 | /** 132 | * 官方验证 133 | * @param {string} model - 模型名称 134 | * @param {number} seed - 随机种子 135 | * @returns {Object} - 包含响应文本、指纹、相似度和结论的对象 136 | */ 137 | async performOfficialVerification(model, seed) { 138 | try { 139 | const results = await Promise.all( 140 | [1, 2, 3, 4].map(() => this.sendVerificationRequest(model, seed)) 141 | ); 142 | const texts = []; 143 | const fingerprints = []; 144 | 145 | for (let i = 0; i < results.length; i++) { 146 | if (results[i].error) { 147 | console.error(`Error in request ${i + 1}:`, results[i].error); 148 | throw new Error(`请求 ${i + 1} 失败: ${results[i].error}`); 149 | } 150 | if (!results[i].choices?.[0]?.message?.content) { 151 | console.error( 152 | `Invalid response structure in request ${i + 1}:`, 153 | results[i] 154 | ); 155 | throw new Error(`请求 ${i + 1} 返回的数据结构无效`); 156 | } 157 | texts.push(results[i].choices[0].message.content); 158 | fingerprints.push(results[i].system_fingerprint || 'N/A'); 159 | } 160 | 161 | const similarity = this.compareTextSimilarity(texts); 162 | 163 | // 分析结果 164 | let validFingerprintsCount = fingerprints.filter( 165 | value => value !== 'N/A' 166 | ).length; 167 | let similarityCount = Object.values(similarity).filter( 168 | value => parseFloat(value) > 0.6 169 | ).length; 170 | let lowSimilarityCount = Object.values(similarity).filter( 171 | value => parseFloat(value) < 0.1 172 | ).length; 173 | 174 | // 根据统计结果得出结论 175 | let conclusion; 176 | if (validFingerprintsCount >= 3 && similarityCount >= 3) { 177 | conclusion = '恭喜你,大概率是官方API呀!这可能是官方API!'; 178 | } else if (validFingerprintsCount >= 2 && similarityCount >= 2) { 179 | conclusion = '可能是官方API'; 180 | } else if (validFingerprintsCount <= 2 && lowSimilarityCount >= 2) { 181 | conclusion = '可能是假的'; 182 | } else { 183 | conclusion = 184 | '没有系统指纹且回答一致性差,则API大概率是假的,结果不确定,请进一步验证'; 185 | } 186 | 187 | // 返回结果 188 | return { 189 | model: model, 190 | texts: texts, 191 | fingerprints: fingerprints, 192 | similarity: similarity, 193 | conclusion: conclusion, 194 | }; 195 | } catch (error) { 196 | console.error('Error in performOfficialVerification:', error); 197 | throw error; 198 | } 199 | } 200 | 201 | /** 202 | * 发送验证请求 203 | * @param {string} model - 模型名称 204 | * @param {number} seed - 随机种子 205 | * @returns {Object} - 请求的响应结果 206 | */ 207 | async sendVerificationRequest(model, seed) { 208 | try { 209 | const response = await fetch(`${this.apiUrl}/v1/chat/completions`, { 210 | method: 'POST', 211 | headers: { 212 | Authorization: `Bearer ${this.apiKey}`, 213 | 'Content-Type': 'application/json', 214 | }, 215 | body: JSON.stringify({ 216 | messages: [ 217 | { 218 | role: 'user', 219 | content: '写一个10个字的冷笑话', 220 | }, 221 | ], 222 | seed: seed, 223 | temperature: 0.7, 224 | model: model, 225 | }), 226 | }); 227 | 228 | if (!response.ok) { 229 | throw new Error(`HTTP error! status: ${response.status}`); 230 | } 231 | 232 | return await response.json(); 233 | } catch (error) { 234 | console.error('Error in sendVerificationRequest:', error); 235 | return { error: error.message }; 236 | } 237 | } 238 | 239 | /** 240 | * 比较文本相似度 241 | * @param {Array} texts - 包含四个文本的数组 242 | * @returns {Object} - 相似度结果 243 | */ 244 | compareTextSimilarity(texts) { 245 | function calculateSimilarity(str1, str2) { 246 | const len1 = str1.length; 247 | const len2 = str2.length; 248 | const matrix = Array(len1 + 1) 249 | .fill() 250 | .map(() => Array(len2 + 1).fill(0)); 251 | 252 | for (let i = 0; i <= len1; i++) matrix[i][0] = i; 253 | for (let j = 0; j <= len2; j++) matrix[0][j] = j; 254 | 255 | for (let i = 1; i <= len1; i++) { 256 | for (let j = 1; j <= len2; j++) { 257 | const cost = str1[i - 1] === str2[j - 1] ? 0 : 1; 258 | matrix[i][j] = Math.min( 259 | matrix[i - 1][j] + 1, 260 | matrix[i][j - 1] + 1, 261 | matrix[i - 1][j - 1] + cost 262 | ); 263 | } 264 | } 265 | 266 | return 1 - matrix[len1][len2] / Math.max(len1, len2); 267 | } 268 | 269 | return { 270 | similarity12: calculateSimilarity(texts[0], texts[1]).toFixed(4), 271 | similarity13: calculateSimilarity(texts[0], texts[2]).toFixed(4), 272 | similarity14: calculateSimilarity(texts[0], texts[3]).toFixed(4), 273 | similarity23: calculateSimilarity(texts[1], texts[2]).toFixed(4), 274 | similarity24: calculateSimilarity(texts[1], texts[3]).toFixed(4), 275 | similarity34: calculateSimilarity(texts[2], texts[3]).toFixed(4), 276 | }; 277 | } 278 | 279 | /** 280 | * 函数调用验证 281 | * @param {string} model - 模型名称 282 | * @param {number} a - 整数 a 283 | * @param {number} b - 整数 b 284 | * @returns {Object} - 包含标准响应和模型响应的对象 285 | */ 286 | async verifyFunctionCalling(model, a, b) { 287 | try { 288 | const result = await this.sendFunctionCallingRequest(model, a, b); 289 | 290 | if (result.error) { 291 | console.error(`Error in request:`, result.error); 292 | throw new Error(`请求失败: ${result.error}`); 293 | } 294 | 295 | let standardResponse = { 296 | index: 0, 297 | message: { 298 | role: 'assistant', 299 | content: null, 300 | function_call: { 301 | name: 'add_numbers', 302 | arguments: `{"a":${a},"b":${b}}`, 303 | }, 304 | }, 305 | logprobs: null, 306 | finish_reason: 'function_call', 307 | }; 308 | 309 | return { 310 | model: model, 311 | standardResponse: standardResponse, 312 | modelResponse: result.choices?.[0], 313 | }; 314 | } catch (error) { 315 | console.error('Error in verifyFunctionCalling:', error); 316 | throw error; 317 | } 318 | } 319 | 320 | /** 321 | * 发送函数调用请求 322 | * @param {string} model - 模型名称 323 | * @param {number} a - 整数 a 324 | * @param {number} b - 整数 b 325 | * @returns {Object} - 请求的响应结果 326 | */ 327 | async sendFunctionCallingRequest(model, a, b) { 328 | try { 329 | const response = await fetch(`${this.apiUrl}/v1/chat/completions`, { 330 | method: 'POST', 331 | headers: { 332 | Authorization: `Bearer ${this.apiKey}`, 333 | 'Content-Type': 'application/json', 334 | }, 335 | body: JSON.stringify({ 336 | messages: [ 337 | { role: 'system', content: 'You are a helpful assistant.' }, 338 | { role: 'user', content: `Please add ${a} and ${b}.` }, 339 | ], 340 | functions: [ 341 | { 342 | name: 'add_numbers', 343 | description: 'Adds two numbers together', 344 | parameters: { 345 | type: 'object', 346 | properties: { 347 | a: { 348 | type: 'number', 349 | description: 'The first number', 350 | }, 351 | b: { 352 | type: 'number', 353 | description: 'The second number', 354 | }, 355 | }, 356 | required: ['a', 'b'], 357 | }, 358 | }, 359 | ], 360 | function_call: 'auto', 361 | temperature: 0.5, 362 | model: model, 363 | }), 364 | }); 365 | 366 | if (!response.ok) { 367 | throw new Error(`HTTP error! status: ${response.status}`); 368 | } 369 | 370 | return await response.json(); 371 | } catch (error) { 372 | console.error('Error in sendFunctionCallingRequest:', error); 373 | return { error: error.message }; 374 | } 375 | } 376 | 377 | /** 378 | * 自定义对话验证 379 | * @param {string} model - 模型名称 380 | * @param {string} prompt - 用户输入的提示词 381 | * @returns {Object} - 包含响应结果的对象 382 | */ 383 | async verifyCustomDialog(model, prompt) { 384 | try { 385 | const result = await this.sendCustomDialogRequest(model, prompt); 386 | 387 | if (result.error) { 388 | console.error(`Error in request:`, result.error); 389 | throw new Error(`请求失败: ${result.error}`); 390 | } 391 | 392 | return { 393 | model: model, 394 | prompt: prompt, 395 | response: result.choices?.[0]?.message?.content || '', 396 | raw_response: result, 397 | }; 398 | } catch (error) { 399 | console.error('Error in verifyCustomDialog:', error); 400 | throw error; 401 | } 402 | } 403 | 404 | /** 405 | * 发送自定义对话请求 406 | * @param {string} model - 模型名称 407 | * @param {string} prompt - 用户输入的提示词 408 | * @returns {Object} - 请求的响应结果 409 | */ 410 | async sendCustomDialogRequest(model, prompt) { 411 | try { 412 | const response = await fetch(`${this.apiUrl}/v1/chat/completions`, { 413 | method: 'POST', 414 | headers: { 415 | Authorization: `Bearer ${this.apiKey}`, 416 | 'Content-Type': 'application/json', 417 | }, 418 | body: JSON.stringify({ 419 | messages: [ 420 | { 421 | role: 'user', 422 | content: prompt, 423 | }, 424 | ], 425 | temperature: 0.7, 426 | model: model, 427 | }), 428 | }); 429 | 430 | if (!response.ok) { 431 | throw new Error(`HTTP error! status: ${response.status}`); 432 | } 433 | 434 | return await response.json(); 435 | } catch (error) { 436 | console.error('Error in sendCustomDialogRequest:', error); 437 | return { error: error.message }; 438 | } 439 | } 440 | } 441 | export default ModelVerifier; 442 | -------------------------------------------------------------------------------- /src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 17 | -------------------------------------------------------------------------------- /src/views/Layout.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "installCommand": "yarn config set strict-ssl false && yarn install", 3 | "outputDirectory": "dist", 4 | "routes": [ 5 | { 6 | "src": "/api/alive", 7 | "dest": "/api/alive.js" 8 | }, 9 | { 10 | "src": "/api/(.*)", 11 | "dest": "/api/index.js" 12 | }, 13 | { 14 | "handle": "filesystem" 15 | }, 16 | { 17 | "src": "/(.*)", 18 | "dest": "/index.html" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import vue from '@vitejs/plugin-vue'; 3 | import Components from 'unplugin-vue-components/vite'; 4 | import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers'; 5 | import { visualizer } from 'rollup-plugin-visualizer'; 6 | 7 | export default defineConfig({ 8 | plugins: [ 9 | vue(), 10 | visualizer({ 11 | open: true, 12 | }), 13 | Components({ 14 | resolvers: [ 15 | AntDesignVueResolver({ 16 | importStyle: false, 17 | resolveIcons: true, 18 | }), 19 | ], 20 | }), 21 | ], 22 | server: { 23 | port: 3000, 24 | host: '0.0.0.0', 25 | }, 26 | resolve: { 27 | alias: { 28 | '@': '/src', 29 | }, 30 | }, 31 | css: { 32 | preprocessorOptions: { 33 | less: { 34 | modifyVars: { 35 | hack: `true; @import "~ant-design-vue/lib/style/themes/dark.less";`, 36 | }, 37 | javascriptEnabled: true, 38 | }, 39 | }, 40 | }, 41 | build: { 42 | // 添加 build 配置 43 | rollupOptions: { 44 | output: { 45 | // 手动拆分 chunk 46 | manualChunks(id) { 47 | if (id.includes('node_modules')) { 48 | if (id.includes('ant-design-vue')) { 49 | return 'ant-design-vue'; 50 | } 51 | if (id.includes('lodash')) { 52 | return 'lodash'; 53 | } 54 | // 添加更多需要单独拆分的库 55 | return 'vendor'; 56 | } 57 | }, 58 | }, 59 | }, 60 | }, 61 | }); 62 | --------------------------------------------------------------------------------