├── .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 |
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 | [](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 |
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 |
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 |
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 | [](https://star-history.com/#october-coder/api-check&Date)
277 |
278 | [](https://yxvm.com/)
279 | [NodeSupport](https://github.com/NodeSeekDev/NodeSupport)赞助了本项目
280 |
--------------------------------------------------------------------------------
/README_CN.md:
--------------------------------------------------------------------------------
1 |
2 |
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 | [](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 | [](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 | 
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 |
10 |
11 | - 点击 “Create KV”,创建一个新的 KV 命名空间。
12 |
13 | - Name:自定义一个名称(如 `my-kv-store`)
14 |
15 | - KV 创建后,将其绑定到您的项目中,**后需要重新部署项目**
16 |
17 |
18 |
19 | ### 2.在页面直接填入url + /api
20 |
21 |
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 |
2 |
3 |
4 |
5 |
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 |
2 |
3 |
12 |
13 |
14 |
15 | {{ $t('EXPERIMENTAL_FEATURES') }}
16 |
17 |
18 |
19 |
20 |
21 |
22 | 参考自elfmaid
23 | 原贴地址
24 |
25 |
26 |
27 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
59 |
65 | {{ $t('START_CHECKING') }}
66 |
67 |
68 |
69 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
110 |
111 |
112 |
138 |
139 |
145 | {{ $t('START_CHECKING') }}
146 |
147 |
148 |
149 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
190 |
191 |
192 |
216 |
217 |
223 | {{ $t('START_CHECKING') }}
224 |
225 |
226 |
227 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
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 |
2 |
5 |
6 |
7 |
17 |
--------------------------------------------------------------------------------
/src/views/Layout.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
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 |
--------------------------------------------------------------------------------