├── .gitignore ├── src ├── config.js ├── cache │ ├── redis.js │ ├── index.js │ └── stats.js ├── index.js ├── utils │ └── helpers.js ├── pages │ ├── status.js │ ├── admin.js │ └── docs.js ├── handlers │ ├── proxy.js │ └── api.js └── db │ └── supabase.js ├── package.json ├── wrangler.toml ├── LICENSE ├── wrangler.toml.example ├── eslint.config.js ├── CLAUDE.md ├── README.md └── schema.sql /.gitignore: -------------------------------------------------------------------------------- 1 | # 环境变量 2 | .env 3 | .dev.vars 4 | 5 | # Wrangler 6 | .wrangler/ 7 | wrangler.toml.local 8 | wrangler.toml.backup 9 | 10 | # 构建产物 11 | anyrouter.js 12 | _worker.js 13 | 14 | # Node 15 | node_modules/ 16 | 17 | # 编辑器 18 | .vscode/ 19 | .idea/ 20 | *.swp 21 | *.swo 22 | *~ 23 | 24 | # 操作系统 25 | .DS_Store 26 | Thumbs.db 27 | 28 | # 日志 29 | *.log 30 | npm-debug.log* 31 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | // ============ 配置常量 ============ 2 | 3 | // 构建时间(部署时自动替换) 4 | export const BUILD_TIME = '__BUILD_TIME__' 5 | 6 | // 本地配置(如果没有数据库,使用此配置作为 fallback) 7 | export const FALLBACK_CONFIG = {} 8 | 9 | // 缓存配置 10 | export const CONFIG_CACHE_TTL_MS = 10 * 60 * 1000 // 10 分钟(内存缓存) 11 | export const REDIS_CACHE_TTL_SECONDS = 5 * 60 // 5 分钟(Redis 缓存) 12 | export const KV_CACHE_TTL_SECONDS = 5 * 60 // 5 分钟(KV 缓存,备用) 13 | export const CACHE_KEY = 'anyrouter:api_configs' 14 | 15 | // 默认管理员密码 16 | export const DEFAULT_ADMIN_PASSWORD = '123456' 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "anyrouter", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "description": "Lightweight API Proxy Service for Cloudflare Workers", 6 | "scripts": { 7 | "build": "node build.js", 8 | "dev": "wrangler dev", 9 | "deploy": "npm run build && wrangler deploy", 10 | "lint": "eslint src/", 11 | "lint:fix": "eslint src/ --fix" 12 | }, 13 | "devDependencies": { 14 | "@eslint/js": "^9.39.1", 15 | "esbuild": "^0.27.0", 16 | "eslint": "^9.39.1", 17 | "globals": "^16.5.0", 18 | "wrangler": "^4.48.0" 19 | }, 20 | "keywords": [ 21 | "cloudflare", 22 | "workers", 23 | "api", 24 | "proxy" 25 | ], 26 | "license": "MIT" 27 | } 28 | -------------------------------------------------------------------------------- /wrangler.toml: -------------------------------------------------------------------------------- 1 | # AnyRouter - Cloudflare Workers 配置 2 | # 敏感信息请通过 Cloudflare Dashboard 或 wrangler secret 配置 3 | 4 | name = "anyrouter" 5 | main = "anyrouter.js" 6 | compatibility_date = "2024-01-01" 7 | 8 | # ============================================ 9 | # 环境变量说明 10 | # ============================================ 11 | # 以下变量需要在 Cloudflare Dashboard -> Settings -> Variables and Secrets 中配置 12 | # 或使用 wrangler secret put <变量名> 命令设置 13 | # 14 | # 必须配置: 15 | # ADMIN_PASSWORD - 管理面板登录密码 16 | # 17 | # 可选配置(密钥管理功能): 18 | # SUPABASE_URL - Supabase 项目 URL 19 | # SUPABASE_KEY - Supabase anon/public key 20 | # 21 | # 可选配置(统计和缓存功能): 22 | # UPSTASH_REDIS_URL - Upstash Redis REST URL 23 | # UPSTASH_REDIS_TOKEN - Upstash Redis REST Token 24 | 25 | # ============================================ 26 | # KV 缓存配置(可选) 27 | # ============================================ 28 | # 创建 KV: npx wrangler kv:namespace create "CONFIG_CACHE" 29 | # [[kv_namespaces]] 30 | # binding = "CONFIG_KV" 31 | # id = "your-kv-namespace-id" 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/cache/redis.js: -------------------------------------------------------------------------------- 1 | // ============ Upstash Redis REST API 客户端 ============ 2 | 3 | /** 4 | * Upstash Redis REST API 客户端 5 | * 使用 HTTP REST API,无需 TCP 连接,适合 Serverless 6 | */ 7 | export class RedisClient { 8 | constructor(url, token) { 9 | this.baseUrl = url 10 | this.token = token 11 | } 12 | 13 | async request(command) { 14 | const response = await fetch(`${this.baseUrl}`, { 15 | method: 'POST', 16 | headers: { 17 | Authorization: `Bearer ${this.token}`, 18 | 'Content-Type': 'application/json', 19 | }, 20 | body: JSON.stringify(command), 21 | }) 22 | const data = await response.json() 23 | if (data.error) throw new Error(data.error) 24 | return data.result 25 | } 26 | 27 | async get(key) { 28 | return this.request(['GET', key]) 29 | } 30 | 31 | async set(key, value, ttlSeconds) { 32 | if (ttlSeconds) { 33 | return this.request(['SET', key, value, 'EX', ttlSeconds]) 34 | } 35 | return this.request(['SET', key, value]) 36 | } 37 | 38 | async del(key) { 39 | return this.request(['DEL', key]) 40 | } 41 | } 42 | 43 | /** 44 | * 获取 Redis 客户端实例 45 | */ 46 | export function getRedisClient(env) { 47 | if (!env.UPSTASH_REDIS_URL || !env.UPSTASH_REDIS_TOKEN) { 48 | return null 49 | } 50 | return new RedisClient(env.UPSTASH_REDIS_URL, env.UPSTASH_REDIS_TOKEN) 51 | } 52 | -------------------------------------------------------------------------------- /wrangler.toml.example: -------------------------------------------------------------------------------- 1 | # AnyRouter - Cloudflare Workers 配置示例 2 | # 复制此文件为 wrangler.toml 并填入你的配置 3 | 4 | name = "anyrouter" 5 | main = "anyrouter.js" 6 | compatibility_date = "2024-01-01" 7 | 8 | # ============================================ 9 | # 环境变量配置 10 | # ============================================ 11 | [vars] 12 | # 管理面板密码(必须修改!) 13 | ADMIN_PASSWORD = "your-secure-password-here" 14 | 15 | # Supabase 配置(可选,如需密钥管理功能) 16 | # 获取方式:https://supabase.com -> 创建项目 -> Settings -> API 17 | SUPABASE_URL = "https://xxx.supabase.co" 18 | SUPABASE_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." 19 | 20 | # Upstash Redis 配置(可选,如需统计和缓存) 21 | # 获取方式:https://upstash.com -> 创建 Redis 数据库 -> REST API 22 | # 注意:使用 HTTPS URL,不是 redis:// 协议 23 | UPSTASH_REDIS_URL = "https://xxx.upstash.io" 24 | UPSTASH_REDIS_TOKEN = "AXxxxx..." 25 | 26 | # ============================================ 27 | # KV 缓存配置(可选,用于高并发场景) 28 | # ============================================ 29 | # 创建 KV namespace: npx wrangler kv:namespace create "CONFIG_CACHE" 30 | # 然后将返回的 id 填入下方 31 | # [[kv_namespaces]] 32 | # binding = "CONFIG_KV" 33 | # id = "your-kv-namespace-id" 34 | 35 | # ============================================ 36 | # 使用 Secrets 替代明文配置(推荐生产环境) 37 | # ============================================ 38 | # 使用 wrangler secret 设置敏感信息: 39 | # npx wrangler secret put SUPABASE_URL 40 | # npx wrangler secret put SUPABASE_KEY 41 | # npx wrangler secret put ADMIN_PASSWORD 42 | # npx wrangler secret put UPSTASH_REDIS_URL 43 | # npx wrangler secret put UPSTASH_REDIS_TOKEN 44 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // ============ AnyRouter 主入口 ============ 2 | 3 | import { handleCORS } from './utils/helpers.js' 4 | import { handleApiRequest } from './handlers/api.js' 5 | import { handleProxyRequest } from './handlers/proxy.js' 6 | import { getStatusHtml } from './pages/status.js' 7 | import { getAdminHtml } from './pages/admin.js' 8 | import { getDocsHtml } from './pages/docs.js' 9 | 10 | export default { 11 | async fetch(request, env, ctx) { 12 | return handleRequest(request, env, ctx) 13 | }, 14 | } 15 | 16 | async function handleRequest(request, env, ctx) { 17 | const url = new URL(request.url) 18 | 19 | // 处理 CORS 预检请求 20 | if (request.method === 'OPTIONS') { 21 | return handleCORS() 22 | } 23 | 24 | // 管理页面路由 25 | if (url.pathname === '/admin') { 26 | return new Response(getAdminHtml(), { 27 | headers: { 'Content-Type': 'text/html; charset=utf-8' }, 28 | }) 29 | } 30 | 31 | // 公开文档页面(无需鉴权) 32 | if (url.pathname === '/docs') { 33 | return new Response(getDocsHtml(), { 34 | headers: { 'Content-Type': 'text/html; charset=utf-8' }, 35 | }) 36 | } 37 | 38 | // API 路由 39 | if (url.pathname.startsWith('/api/')) { 40 | return handleApiRequest(request, env, url) 41 | } 42 | 43 | // 根路径返回状态页面 44 | if (request.method === 'GET' && url.pathname === '/') { 45 | return new Response(getStatusHtml(), { 46 | headers: { 'Content-Type': 'text/html; charset=utf-8' }, 47 | }) 48 | } 49 | 50 | // 代理请求处理(传递 ctx 用于 waitUntil) 51 | return handleProxyRequest(request, env, url, ctx) 52 | } 53 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | 4 | export default [ 5 | js.configs.recommended, 6 | { 7 | languageOptions: { 8 | ecmaVersion: 2022, 9 | sourceType: 'module', 10 | globals: { 11 | ...globals.browser, 12 | ...globals.es2021, 13 | // Cloudflare Workers globals 14 | fetch: 'readonly', 15 | Request: 'readonly', 16 | Response: 'readonly', 17 | Headers: 'readonly', 18 | URL: 'readonly', 19 | URLSearchParams: 'readonly', 20 | crypto: 'readonly', 21 | TextEncoder: 'readonly', 22 | TextDecoder: 'readonly', 23 | console: 'readonly', 24 | // jQuery (for admin page) 25 | $: 'readonly', 26 | jQuery: 'readonly', 27 | }, 28 | }, 29 | rules: { 30 | // 基础规则 31 | 'no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], 32 | 'no-console': 'off', 33 | 'prefer-const': 'error', 34 | 'no-var': 'error', 35 | 36 | // 代码风格 37 | semi: ['error', 'never'], 38 | quotes: ['error', 'single', { avoidEscape: true }], 39 | 'comma-dangle': ['error', 'only-multiline'], 40 | indent: ['error', 2, { SwitchCase: 1 }], 41 | 42 | // 最佳实践 43 | eqeqeq: ['error', 'always', { null: 'ignore' }], 44 | curly: ['error', 'multi-line'], 45 | 'no-throw-literal': 'error', 46 | }, 47 | }, 48 | { 49 | // pages 目录是 HTML 模板,放宽规则 50 | files: ['src/pages/**/*.js'], 51 | rules: { 52 | 'no-useless-escape': 'off', 53 | indent: 'off', 54 | quotes: 'off', 55 | }, 56 | }, 57 | { 58 | ignores: [ 59 | 'node_modules/', 60 | 'anyrouter.js', 61 | '_worker.js', 62 | '.wrangler/', 63 | ], 64 | }, 65 | ] 66 | -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | # AnyRouter - 项目说明 2 | 3 | ## 项目概述 4 | 5 | AnyRouter 是一个运行在 Cloudflare Workers 上的通用 API 代理服务,支持 OpenAI、Anthropic、Google、Azure、Groq 等任意 HTTP API 的统一转发。 6 | 7 | ## 技术栈 8 | 9 | - **运行时**: Cloudflare Workers (ES Module) 10 | - **构建工具**: esbuild 11 | - **数据库**: Supabase (PostgreSQL) - 可选 12 | - **缓存**: Upstash Redis - 可选 13 | - **前端**: 原生 HTML + Tailwind CSS + jQuery 14 | 15 | ## 项目结构 16 | 17 | ``` 18 | src/ 19 | ├── index.js # 入口文件,路由分发 20 | ├── config.js # 配置常量(缓存TTL等) 21 | ├── handlers/ 22 | │ ├── proxy.js # 代理请求处理(核心逻辑) 23 | │ └── api.js # 管理 API 接口 24 | ├── pages/ 25 | │ ├── admin.js # 管理面板 HTML 26 | │ ├── docs.js # 文档页面 HTML 27 | │ └── status.js # 状态页面 HTML 28 | ├── cache/ 29 | │ ├── index.js # 缓存管理(内存/Redis/KV) 30 | │ ├── redis.js # Upstash Redis REST 客户端 31 | │ └── stats.js # 统计和黑名单(Redis) 32 | ├── db/ 33 | │ └── supabase.js # 数据库操作 34 | └── utils/ 35 | └── helpers.js # 工具函数 36 | ``` 37 | 38 | ## 构建和部署 39 | 40 | ```bash 41 | # 安装依赖 42 | npm install 43 | 44 | # 构建(生成 anyrouter.js 和 _worker.js) 45 | npm run build 46 | 47 | # 本地开发 48 | cp wrangler.toml.example wrangler.toml.local 49 | # 编辑 wrangler.toml.local 填入环境变量 50 | npx wrangler dev -c wrangler.toml.local 51 | 52 | # 部署 53 | npx wrangler deploy 54 | ``` 55 | 56 | ## 环境变量 57 | 58 | | 变量 | 必须 | 说明 | 59 | |------|------|------| 60 | | `ADMIN_PASSWORD` | ✅ | 管理面板登录密码 | 61 | | `SUPABASE_URL` | ❌ | Supabase 项目 URL | 62 | | `SUPABASE_KEY` | ❌ | Supabase anon key | 63 | | `UPSTASH_REDIS_URL` | ❌ | Upstash Redis REST URL | 64 | | `UPSTASH_REDIS_TOKEN` | ❌ | Upstash Redis Token | 65 | 66 | ## 三种认证模式 67 | 68 | 1. **SK 别名模式**(推荐): `Bearer sk-ar-xxxxxxxx` 69 | 2. **Key ID 模式**: `Bearer https://api.openai.com:a3x9k2` 70 | 3. **直传模式**: `Bearer https://api.openai.com:sk-xxx...` 71 | 72 | ## 路由 73 | 74 | | 路由 | 说明 | 75 | |------|------| 76 | | `/` | 状态页 | 77 | | `/docs` | 使用文档 | 78 | | `/admin` | 管理面板 | 79 | | `/api/*` | 管理 API | 80 | | `/*` | 代理请求 | 81 | 82 | ## 关键文件说明 83 | 84 | - `wrangler.toml` - 部署配置(无敏感信息,可提交) 85 | - `wrangler.toml.example` - 本地开发配置模板 86 | - `wrangler.toml.local` - 本地开发配置(被 gitignore) 87 | - `schema.sql` - Supabase 数据库初始化脚本 88 | - `build.js` - esbuild 构建脚本 89 | 90 | ## 开发注意事项 91 | 92 | 1. 代码修改后需要运行 `npm run build` 重新构建 93 | 2. 敏感信息不要写入 `wrangler.toml`,通过 Dashboard 配置 94 | 3. Redis 和 Supabase 都是可选的,不配置时使用直传模式 95 | 4. 前端页面在 `src/pages/` 目录,是纯 HTML 字符串模板 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AnyRouter 2 | 3 | 通用 API 代理服务,支持 OpenAI、Anthropic、Google、Azure、Groq 等任意 HTTP API 的统一转发。 4 | 5 | ## 特性 6 | 7 | - **通用代理** - 支持任意 HTTP/HTTPS API 8 | - **三种认证模式** - SK 别名 / Key ID / 直传 Token 9 | - **统计与监控** - 请求统计、IP 排行、黑名单管理 10 | - **边缘加速** - 基于 Cloudflare 全球网络 11 | 12 | ## 部署 13 | 14 | ### 方式一:一键部署(推荐) 15 | 16 | [![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/dext7r/anyrouter) 17 | 18 | ### 方式二:GitHub 关联部署 19 | 20 | 1. Fork 本仓库到你的 GitHub 账号 21 | 2. 登录 [Cloudflare Dashboard](https://dash.cloudflare.com) → Workers & Pages → Create 22 | 3. 选择 **Workers** → Import from GitHub → 选择你 Fork 的仓库 23 | 4. 使用默认配置,点击部署 24 | 5. 部署后进入 Settings → Variables and Secrets,添加环境变量 25 | 26 | ### 方式三:命令行部署 27 | 28 | ```bash 29 | git clone https://github.com/dext7r/anyrouter.git 30 | cd anyrouter 31 | npm install 32 | 33 | # 本地开发:复制示例配置并填入你的配置 34 | cp wrangler.toml.example wrangler.toml.local 35 | # 编辑 wrangler.toml.local 填入环境变量 36 | npx wrangler dev -c wrangler.toml.local 37 | 38 | # 部署到 Cloudflare 39 | npm run build 40 | npx wrangler deploy 41 | # 然后在 Dashboard 配置环境变量 42 | ``` 43 | 44 | ### 方式四:GitHub Actions 45 | 46 | ```yaml 47 | name: Deploy 48 | on: 49 | push: 50 | branches: [main] 51 | jobs: 52 | deploy: 53 | runs-on: ubuntu-latest 54 | steps: 55 | - uses: actions/checkout@v4 56 | - uses: actions/setup-node@v4 57 | with: 58 | node-version: '20' 59 | - run: npm install && npm run build 60 | - run: npx wrangler deploy 61 | env: 62 | CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} 63 | CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} 64 | ``` 65 | 66 | ## 环境变量 67 | 68 | | 变量 | 必须 | 说明 | 69 | |------|------|------| 70 | | `ADMIN_PASSWORD` | ✅ | 管理面板登录密码 | 71 | | `SUPABASE_URL` | ❌ | Supabase 项目 URL | 72 | | `SUPABASE_KEY` | ❌ | Supabase anon key | 73 | | `UPSTASH_REDIS_URL` | ❌ | Upstash Redis REST URL | 74 | | `UPSTASH_REDIS_TOKEN` | ❌ | Upstash Redis Token | 75 | 76 | > 不配置 Supabase/Redis 也可使用直传模式 77 | 78 | ## 使用 79 | 80 | ```bash 81 | # SK 别名模式(推荐) 82 | curl -H "Authorization: Bearer sk-ar-xxxxxxxx" https://your-proxy/v1/chat/completions 83 | 84 | # Key ID 模式 85 | curl -H "Authorization: Bearer https://api.openai.com:a3x9k2" https://your-proxy/v1/chat/completions 86 | 87 | # 直传模式 88 | curl -H "Authorization: Bearer https://api.openai.com:sk-xxx" https://your-proxy/v1/chat/completions 89 | ``` 90 | 91 | ## 路由 92 | 93 | | 路由 | 说明 | 94 | |------|------| 95 | | `/` | 状态页 | 96 | | `/docs` | 完整文档 | 97 | | `/admin` | 管理面板 | 98 | | `/*` | 代理请求 | 99 | 100 | ## License 101 | 102 | MIT 103 | -------------------------------------------------------------------------------- /src/utils/helpers.js: -------------------------------------------------------------------------------- 1 | // ============ 工具函数 ============ 2 | 3 | import { DEFAULT_ADMIN_PASSWORD } from '../config.js' 4 | 5 | /** 6 | * 获取管理员密码(优先使用环境变量,否则使用默认值) 7 | */ 8 | export function getAdminPassword(env) { 9 | return env.ADMIN_PASSWORD || DEFAULT_ADMIN_PASSWORD 10 | } 11 | 12 | /** 13 | * 验证管理员密码 14 | */ 15 | export function verifyAdmin(request, env) { 16 | const authHeader = request.headers.get('Authorization') 17 | if (!authHeader || !authHeader.startsWith('Bearer ')) { 18 | return false 19 | } 20 | 21 | const token = authHeader.substring(7).trim() 22 | return token === getAdminPassword(env).trim() 23 | } 24 | 25 | /** 26 | * 校验 URL 是否有效 27 | * @param {string} apiUrl 28 | * @returns {boolean} 29 | */ 30 | export function isValidUrl(apiUrl) { 31 | if (typeof apiUrl !== 'string' || apiUrl.length === 0) { 32 | return false 33 | } 34 | 35 | try { 36 | const parsed = new URL(apiUrl) 37 | return parsed.protocol === 'http:' || parsed.protocol === 'https:' 38 | } catch { 39 | return false 40 | } 41 | } 42 | 43 | /** 44 | * 校验 token 是否符合要求 45 | * @param {string} token 46 | * @returns {boolean} 47 | */ 48 | export function isValidToken(token) { 49 | // 允许字母、数字、常见特殊字符(_-./=+ 等),排除空格和危险字符 50 | return ( 51 | typeof token === 'string' && 52 | token.length > 0 && 53 | token.length <= 1000 && 54 | !/[\s\0\n\r]/.test(token) 55 | ) 56 | } 57 | 58 | /** 59 | * 校验配置请求体 60 | * @param {any} body 61 | * @param {{ partial?: boolean }} [options] 62 | * @returns {{ valid: boolean, error?: string }} 63 | */ 64 | export function validateConfigPayload(body, options = {}) { 65 | const { partial = false } = options 66 | 67 | if (!body || typeof body !== 'object') { 68 | return { valid: false, error: 'Invalid payload' } 69 | } 70 | 71 | if (!partial || 'api_url' in body) { 72 | if (!isValidUrl(body.api_url)) { 73 | return { valid: false, error: 'api_url is required and must be a valid URL' } 74 | } 75 | } 76 | 77 | if (!partial || 'token' in body) { 78 | if (!isValidToken(body.token)) { 79 | return { valid: false, error: 'token is required and must not contain special characters' } 80 | } 81 | } 82 | 83 | if ('enabled' in body && typeof body.enabled !== 'boolean') { 84 | return { valid: false, error: 'enabled must be a boolean' } 85 | } 86 | 87 | if ('remark' in body) { 88 | if (body.remark !== null && typeof body.remark !== 'string') { 89 | return { valid: false, error: 'remark must be a string or null' } 90 | } 91 | if (body.remark && body.remark.length > 255) { 92 | return { valid: false, error: 'remark must be 255 characters or less' } 93 | } 94 | } 95 | 96 | if (partial && !('api_url' in body || 'token' in body || 'enabled' in body || 'remark' in body)) { 97 | return { valid: false, error: 'No fields provided for update' } 98 | } 99 | 100 | return { valid: true } 101 | } 102 | 103 | /** 104 | * 判断配置中是否存在启用的 key 105 | * @param {Record} config 106 | * @param {string} [apiUrl] 107 | * @returns {boolean} 108 | */ 109 | export function hasEnabledKey(config, apiUrl) { 110 | if (!config || Object.keys(config).length === 0) { 111 | return false 112 | } 113 | 114 | if (apiUrl) { 115 | const apiConfig = config[apiUrl] 116 | return Boolean(apiConfig && apiConfig.keys && apiConfig.keys.some((key) => key.enabled)) 117 | } 118 | 119 | return Object.values(config).some( 120 | (item) => item.keys && item.keys.some((key) => key.enabled) 121 | ) 122 | } 123 | 124 | /** 125 | * 返回 JSON 响应 126 | */ 127 | export function jsonResponse(data, status = 200) { 128 | return new Response(JSON.stringify(data), { 129 | status, 130 | headers: { 131 | 'Content-Type': 'application/json', 132 | 'Access-Control-Allow-Origin': '*', 133 | }, 134 | }) 135 | } 136 | 137 | /** 138 | * 处理 CORS 139 | */ 140 | export function handleCORS() { 141 | return new Response(null, { 142 | headers: { 143 | 'Access-Control-Allow-Origin': '*', 144 | 'Access-Control-Allow-Methods': 'GET, POST, PATCH, DELETE, OPTIONS', 145 | 'Access-Control-Allow-Headers': 'Content-Type, Authorization', 146 | }, 147 | }) 148 | } 149 | -------------------------------------------------------------------------------- /src/cache/index.js: -------------------------------------------------------------------------------- 1 | // ============ 缓存管理 ============ 2 | 3 | import { CONFIG_CACHE_TTL_MS, CACHE_KEY } from '../config.js' 4 | import { getRedisClient } from './redis.js' 5 | 6 | // 内存缓存 7 | let configCache = { value: null, expiresAt: 0 } 8 | 9 | /** 10 | * 获取内存缓存的配置 11 | * @returns {Record|null} 缓存的配置或 null(已过期) 12 | */ 13 | export function getCachedConfig() { 14 | if (configCache.value && configCache.expiresAt > Date.now()) { 15 | return configCache.value 16 | } 17 | return null 18 | } 19 | 20 | /** 21 | * 写入内存缓存 22 | * @param {Record} config 配置对象 23 | */ 24 | export function setConfigCache(config) { 25 | configCache = { 26 | value: config, 27 | expiresAt: Date.now() + CONFIG_CACHE_TTL_MS, 28 | } 29 | } 30 | 31 | /** 32 | * 使内存缓存失效 33 | */ 34 | export function invalidateConfigCache() { 35 | configCache = { value: null, expiresAt: 0 } 36 | } 37 | 38 | /** 39 | * 使所有缓存失效(内存 + Redis + KV) 40 | * @param {object} env - 环境变量 41 | */ 42 | export async function invalidateAllCache(env) { 43 | configCache = { value: null, expiresAt: 0 } 44 | 45 | // 清除 Redis 缓存 46 | const redis = getRedisClient(env) 47 | if (redis) { 48 | try { 49 | await redis.del(CACHE_KEY) 50 | } catch { 51 | // 忽略错误 52 | } 53 | } 54 | 55 | // 清除 KV 缓存(备用) 56 | if (env && env.CONFIG_KV) { 57 | try { 58 | await env.CONFIG_KV.delete(CACHE_KEY) 59 | } catch { 60 | // 忽略错误 61 | } 62 | } 63 | } 64 | 65 | /** 66 | * 预热缓存:强制从数据库加载并写入所有缓存层 67 | * @param {object} env - 环境变量 68 | * @returns {Promise<{success: boolean, cached: string[], error?: string}>} 69 | */ 70 | export async function warmupCache(env) { 71 | const result = { success: false, cached: [], keysCount: 0 } 72 | 73 | if (!env.SUPABASE_URL || !env.SUPABASE_KEY) { 74 | return { ...result, error: 'Database not configured' } 75 | } 76 | 77 | try { 78 | // 1. 从数据库获取最新数据 79 | let response = await fetch( 80 | `${env.SUPABASE_URL}/rest/v1/api_configs?select=*&deleted_at=is.null&order=created_at.desc`, 81 | { 82 | headers: { 83 | apikey: env.SUPABASE_KEY, 84 | Authorization: `Bearer ${env.SUPABASE_KEY}`, 85 | }, 86 | } 87 | ) 88 | 89 | if (!response.ok) { 90 | response = await fetch( 91 | `${env.SUPABASE_URL}/rest/v1/api_configs?select=*&order=created_at.desc`, 92 | { 93 | headers: { 94 | apikey: env.SUPABASE_KEY, 95 | Authorization: `Bearer ${env.SUPABASE_KEY}`, 96 | }, 97 | } 98 | ) 99 | } 100 | 101 | if (!response.ok) { 102 | return { ...result, error: `Database query failed: HTTP ${response.status}` } 103 | } 104 | 105 | const data = await response.json() 106 | const config = {} 107 | 108 | data.forEach((item) => { 109 | if (!config[item.api_url]) { 110 | config[item.api_url] = { keys: [] } 111 | } 112 | config[item.api_url].keys.push({ 113 | id: item.id, 114 | key_id: item.key_id, 115 | token: item.token, 116 | enabled: item.enabled, 117 | remark: item.remark || '', 118 | created_at: item.created_at, 119 | updated_at: item.updated_at, 120 | }) 121 | }) 122 | 123 | result.keysCount = data.length 124 | 125 | // 2. 写入内存缓存 126 | setConfigCache(config) 127 | result.cached.push('memory') 128 | 129 | // 3. 写入 Redis 缓存 130 | const redis = getRedisClient(env) 131 | if (redis) { 132 | try { 133 | const { REDIS_CACHE_TTL_SECONDS } = await import('../config.js') 134 | await redis.set(CACHE_KEY, JSON.stringify(config), REDIS_CACHE_TTL_SECONDS) 135 | result.cached.push('redis') 136 | } catch (e) { 137 | result.redisError = e.message 138 | } 139 | } 140 | 141 | // 4. 写入 KV 缓存 142 | if (env.CONFIG_KV) { 143 | try { 144 | const { KV_CACHE_TTL_SECONDS } = await import('../config.js') 145 | await env.CONFIG_KV.put(CACHE_KEY, JSON.stringify(config), { 146 | expirationTtl: KV_CACHE_TTL_SECONDS, 147 | }) 148 | result.cached.push('kv') 149 | } catch (e) { 150 | result.kvError = e.message 151 | } 152 | } 153 | 154 | result.success = true 155 | return result 156 | } catch (error) { 157 | return { ...result, error: error.message } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/pages/status.js: -------------------------------------------------------------------------------- 1 | // ============ 状态页面 HTML ============ 2 | 3 | import { BUILD_TIME } from '../config.js' 4 | 5 | /** 6 | * 生成状态页面 HTML 7 | */ 8 | export function getStatusHtml() { 9 | const buildTimeFormatted = new Date(BUILD_TIME) 10 | .toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }) 11 | 12 | return ` 13 | 14 | 15 | 16 | 17 | AnyRouter - API Proxy Service 18 | 19 | 155 | 156 | 157 |
158 | 159 |

AnyRouter

160 |

轻量级 API 代理服务

161 |
服务运行中
162 |
163 |
多端点代理
164 |
Token 管理
165 |
安全转发
166 |
边缘加速
167 |
168 |
169 | 使用文档 170 | 管理面板 171 | GitHub 172 |
173 |
174 | 180 | 181 | ` 182 | } 183 | -------------------------------------------------------------------------------- /schema.sql: -------------------------------------------------------------------------------- 1 | -- AnyRouter 数据库初始化脚本 2 | -- 在 Supabase SQL Editor 中一次性执行此脚本 3 | -- 4 | -- 使用说明: 5 | -- 1. 登录 Supabase Dashboard 6 | -- 2. 进入 SQL Editor 7 | -- 3. 复制粘贴此脚本并执行 8 | 9 | -- ============================================ 10 | -- 1. 创建随机 ID 生成函数 11 | -- ============================================ 12 | CREATE OR REPLACE FUNCTION public.generate_key_id(length INTEGER DEFAULT 6) 13 | RETURNS TEXT AS $$ 14 | DECLARE 15 | chars TEXT := 'abcdefghijklmnopqrstuvwxyz0123456789'; 16 | result TEXT := ''; 17 | i INTEGER; 18 | BEGIN 19 | FOR i IN 1..length LOOP 20 | result := result || substr(chars, floor(random() * length(chars) + 1)::INTEGER, 1); 21 | END LOOP; 22 | RETURN result; 23 | END; 24 | $$ LANGUAGE plpgsql; 25 | 26 | -- ============================================ 27 | -- 2. 创建表 28 | -- ============================================ 29 | CREATE TABLE IF NOT EXISTS public.api_configs ( 30 | id BIGSERIAL PRIMARY KEY, 31 | key_id VARCHAR(6) UNIQUE NOT NULL DEFAULT public.generate_key_id(6), 32 | sk_alias VARCHAR(50) UNIQUE DEFAULT NULL, 33 | api_url TEXT NOT NULL, 34 | token TEXT NOT NULL, 35 | enabled BOOLEAN NOT NULL DEFAULT true, 36 | remark VARCHAR(255) DEFAULT NULL, 37 | expires_at TIMESTAMP WITH TIME ZONE DEFAULT NULL, 38 | deleted_at TIMESTAMP WITH TIME ZONE DEFAULT NULL, 39 | created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), 40 | updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() 41 | ); 42 | 43 | -- 如果表已存在但没有 remark 列,添加它 44 | DO $$ 45 | BEGIN 46 | IF NOT EXISTS ( 47 | SELECT 1 FROM information_schema.columns 48 | WHERE table_schema = 'public' 49 | AND table_name = 'api_configs' 50 | AND column_name = 'remark' 51 | ) THEN 52 | ALTER TABLE public.api_configs 53 | ADD COLUMN remark VARCHAR(255) DEFAULT NULL; 54 | END IF; 55 | END $$; 56 | 57 | -- 如果表已存在但没有 deleted_at 列,添加它(软删除支持) 58 | DO $$ 59 | BEGIN 60 | IF NOT EXISTS ( 61 | SELECT 1 FROM information_schema.columns 62 | WHERE table_schema = 'public' 63 | AND table_name = 'api_configs' 64 | AND column_name = 'deleted_at' 65 | ) THEN 66 | ALTER TABLE public.api_configs 67 | ADD COLUMN deleted_at TIMESTAMP WITH TIME ZONE DEFAULT NULL; 68 | END IF; 69 | END $$; 70 | 71 | -- 如果表已存在但没有 key_id 列,添加它 72 | DO $$ 73 | BEGIN 74 | IF NOT EXISTS ( 75 | SELECT 1 FROM information_schema.columns 76 | WHERE table_schema = 'public' 77 | AND table_name = 'api_configs' 78 | AND column_name = 'key_id' 79 | ) THEN 80 | ALTER TABLE public.api_configs 81 | ADD COLUMN key_id VARCHAR(6) UNIQUE; 82 | 83 | -- 为已有记录生成 key_id 84 | UPDATE public.api_configs 85 | SET key_id = public.generate_key_id(6) 86 | WHERE key_id IS NULL; 87 | 88 | -- 设置 NOT NULL 约束 89 | ALTER TABLE public.api_configs 90 | ALTER COLUMN key_id SET NOT NULL; 91 | 92 | -- 设置默认值 93 | ALTER TABLE public.api_configs 94 | ALTER COLUMN key_id SET DEFAULT public.generate_key_id(6); 95 | END IF; 96 | END $$; 97 | 98 | -- 如果表已存在但没有 sk_alias 列,添加它(SK 别名支持) 99 | DO $$ 100 | BEGIN 101 | IF NOT EXISTS ( 102 | SELECT 1 FROM information_schema.columns 103 | WHERE table_schema = 'public' 104 | AND table_name = 'api_configs' 105 | AND column_name = 'sk_alias' 106 | ) THEN 107 | ALTER TABLE public.api_configs 108 | ADD COLUMN sk_alias VARCHAR(50) UNIQUE DEFAULT NULL; 109 | END IF; 110 | END $$; 111 | 112 | -- 如果表已存在但没有 expires_at 列,添加它(有效期支持) 113 | DO $$ 114 | BEGIN 115 | IF NOT EXISTS ( 116 | SELECT 1 FROM information_schema.columns 117 | WHERE table_schema = 'public' 118 | AND table_name = 'api_configs' 119 | AND column_name = 'expires_at' 120 | ) THEN 121 | ALTER TABLE public.api_configs 122 | ADD COLUMN expires_at TIMESTAMP WITH TIME ZONE DEFAULT NULL; 123 | END IF; 124 | END $$; 125 | 126 | -- ============================================ 127 | -- 3. 创建索引(加速查询) 128 | -- ============================================ 129 | CREATE INDEX IF NOT EXISTS idx_api_configs_key_id ON public.api_configs(key_id); 130 | CREATE INDEX IF NOT EXISTS idx_api_configs_sk_alias ON public.api_configs(sk_alias) WHERE sk_alias IS NOT NULL; 131 | CREATE INDEX IF NOT EXISTS idx_api_configs_api_url ON public.api_configs(api_url); 132 | CREATE INDEX IF NOT EXISTS idx_api_configs_enabled ON public.api_configs(enabled); 133 | CREATE INDEX IF NOT EXISTS idx_api_configs_created_at ON public.api_configs(created_at DESC); 134 | CREATE INDEX IF NOT EXISTS idx_api_configs_deleted_at ON public.api_configs(deleted_at) WHERE deleted_at IS NULL; 135 | 136 | -- ============================================ 137 | -- 4. 启用行级安全 (RLS) 138 | -- ============================================ 139 | ALTER TABLE public.api_configs ENABLE ROW LEVEL SECURITY; 140 | 141 | -- 删除已存在的策略(避免重复执行报错) 142 | DROP POLICY IF EXISTS "Allow all access with service role" ON public.api_configs; 143 | 144 | -- 创建策略:允许所有已认证的请求访问 145 | CREATE POLICY "Allow all access with service role" 146 | ON public.api_configs 147 | FOR ALL 148 | TO authenticated, anon 149 | USING (true) 150 | WITH CHECK (true); 151 | 152 | -- ============================================ 153 | -- 5. 创建更新时间触发器 154 | -- ============================================ 155 | CREATE OR REPLACE FUNCTION public.update_updated_at_column() 156 | RETURNS TRIGGER AS $$ 157 | BEGIN 158 | NEW.updated_at = NOW(); 159 | RETURN NEW; 160 | END; 161 | $$ LANGUAGE plpgsql; 162 | 163 | DROP TRIGGER IF EXISTS update_api_configs_updated_at ON public.api_configs; 164 | 165 | CREATE TRIGGER update_api_configs_updated_at 166 | BEFORE UPDATE ON public.api_configs 167 | FOR EACH ROW 168 | EXECUTE FUNCTION public.update_updated_at_column(); 169 | 170 | -- ============================================ 171 | -- 6. 添加注释 172 | -- ============================================ 173 | COMMENT ON TABLE public.api_configs IS 'API 代理配置表'; 174 | COMMENT ON COLUMN public.api_configs.id IS '自增主键(内部使用)'; 175 | COMMENT ON COLUMN public.api_configs.key_id IS '6位随机 ID(用于 API 调用)'; 176 | COMMENT ON COLUMN public.api_configs.sk_alias IS 'SK 别名(格式:sk-ar-xxx,用于简化认证)'; 177 | COMMENT ON COLUMN public.api_configs.api_url IS '目标 API 地址'; 178 | COMMENT ON COLUMN public.api_configs.token IS 'API Token'; 179 | COMMENT ON COLUMN public.api_configs.enabled IS '是否启用'; 180 | COMMENT ON COLUMN public.api_configs.remark IS '备注说明'; 181 | COMMENT ON COLUMN public.api_configs.expires_at IS '过期时间(NULL表示永不过期)'; 182 | COMMENT ON COLUMN public.api_configs.deleted_at IS '软删除时间(NULL表示未删除)'; 183 | COMMENT ON COLUMN public.api_configs.created_at IS '创建时间'; 184 | COMMENT ON COLUMN public.api_configs.updated_at IS '更新时间'; 185 | 186 | -- ============================================ 187 | -- 7. 授权 188 | -- ============================================ 189 | GRANT ALL ON public.api_configs TO anon; 190 | GRANT ALL ON public.api_configs TO authenticated; 191 | GRANT USAGE, SELECT ON SEQUENCE public.api_configs_id_seq TO anon; 192 | GRANT USAGE, SELECT ON SEQUENCE public.api_configs_id_seq TO authenticated; 193 | 194 | -- ============================================ 195 | -- 完成! 196 | -- ============================================ 197 | -- key_id 格式:6位小写字母+数字,如 "a3x9k2" 198 | -- sk_alias 格式:sk-ar-[32位随机字符],如 "sk-ar-abcDEF123..." 199 | -- 200 | -- 使用方式: 201 | -- 1. SK 别名模式:Authorization: Bearer sk-ar-xxxxxxxx... 202 | -- 2. Key ID 模式:Authorization: Bearer https://api.openai.com:a3x9k2 203 | -- 3. 直传模式: Authorization: Bearer https://api.openai.com:sk-xxx... 204 | -------------------------------------------------------------------------------- /src/handlers/proxy.js: -------------------------------------------------------------------------------- 1 | // ============ 代理请求处理 ============ 2 | 3 | import { jsonResponse } from '../utils/helpers.js' 4 | import { getConfigFromDB, findBySkAlias } from '../db/supabase.js' 5 | import { recordRequest, isIpBlocked } from '../cache/stats.js' 6 | 7 | /** 8 | * 生成友好的错误响应 9 | */ 10 | function errorResponse(code, message, hint) { 11 | return jsonResponse({ 12 | error: { 13 | code, 14 | message, 15 | hint, 16 | contact: '如有疑问请联系管理员', 17 | } 18 | }, code === 'UNAUTHORIZED' ? 401 : 19 | code === 'BAD_REQUEST' ? 400 : 20 | code === 'NOT_FOUND' ? 404 : 21 | code === 'FORBIDDEN' ? 403 : 22 | code === 'SERVICE_ERROR' ? 503 : 500) 23 | } 24 | 25 | /** 26 | * 处理代理请求 27 | * 支持两种格式: 28 | * 1. Authorization: Bearer https://api.example.com:123 (按 ID 查找 token) 29 | * 2. Authorization: Bearer https://api.example.com:sk-xxx (直接使用 token) 30 | * @param {Request} request 31 | * @param {object} env 32 | * @param {URL} url 33 | * @param {ExecutionContext} ctx - Cloudflare Workers 执行上下文,用于 waitUntil 34 | */ 35 | export async function handleProxyRequest(request, env, url, ctx) { 36 | // 获取客户端 IP 37 | const clientIp = request.headers.get('CF-Connecting-IP') || 38 | request.headers.get('X-Forwarded-For')?.split(',')[0]?.trim() || 39 | 'unknown' 40 | 41 | // 检查 IP 黑名单 42 | const blockCheck = await isIpBlocked(env, clientIp) 43 | if (blockCheck.blocked) { 44 | return jsonResponse({ 45 | error: { 46 | code: 'IP_BLOCKED', 47 | message: 'IP 已被封禁', 48 | reason: blockCheck.reason, 49 | ip: clientIp, 50 | contact: '如有疑问请联系管理员', 51 | } 52 | }, 403) 53 | } 54 | 55 | const authHeader = request.headers.get('Authorization') 56 | if (!authHeader || !authHeader.startsWith('Bearer ')) { 57 | return errorResponse( 58 | 'UNAUTHORIZED', 59 | '缺少授权信息', 60 | '请在 Authorization header 中提供 Bearer token,格式: Bearer : 或 Bearer sk-ar-xxx' 61 | ) 62 | } 63 | 64 | const authValue = authHeader.substring(7).trim() // 去掉 "Bearer " 前缀 65 | 66 | // 获取配置 67 | const config = await getConfigFromDB(env) 68 | 69 | let tokenToUse 70 | let targetApiUrl 71 | let usedKeyId = null 72 | 73 | // 检查是否是 SK 别名模式 (sk-ar-xxx) 74 | if (authValue.startsWith('sk-ar-')) { 75 | const found = findBySkAlias(config, authValue) 76 | if (!found) { 77 | return errorResponse( 78 | 'NOT_FOUND', 79 | 'SK 别名不存在', 80 | `找不到 SK 别名 "${authValue}",请检查是否输入正确或联系管理员获取有效的 SK` 81 | ) 82 | } 83 | 84 | if (!found.key.enabled) { 85 | return errorResponse( 86 | 'FORBIDDEN', 87 | 'SK 已被禁用', 88 | '此 SK 别名当前处于禁用状态,请联系管理员启用' 89 | ) 90 | } 91 | 92 | // 检查是否过期 93 | if (found.key.expires_at && new Date(found.key.expires_at) < new Date()) { 94 | return errorResponse( 95 | 'FORBIDDEN', 96 | 'SK 已过期', 97 | `此 SK 别名已于 ${found.key.expires_at} 过期,请联系管理员续期或获取新的 SK` 98 | ) 99 | } 100 | 101 | tokenToUse = found.key.token 102 | targetApiUrl = found.apiUrl 103 | usedKeyId = found.key.key_id 104 | } else { 105 | // 原有格式: : 106 | // 需要从最后一个冒号分割,因为 URL 中可能包含端口号 (https://api.example.com:8080:key) 107 | const lastColonIndex = authValue.lastIndexOf(':') 108 | if (lastColonIndex === -1 || lastColonIndex < 8) { 109 | // 没有冒号,或者冒号在 https:// 中 110 | return errorResponse( 111 | 'BAD_REQUEST', 112 | '授权格式错误', 113 | '正确格式: : 或 sk-ar-xxx,例如 https://api.openai.com:a3x9k2' 114 | ) 115 | } 116 | 117 | targetApiUrl = authValue.substring(0, lastColonIndex) 118 | const keyPart = authValue.substring(lastColonIndex + 1) 119 | 120 | // 验证 API URL 格式 121 | if (!targetApiUrl.startsWith('http://') && !targetApiUrl.startsWith('https://')) { 122 | return errorResponse( 123 | 'BAD_REQUEST', 124 | 'API URL 格式无效', 125 | 'URL 必须以 http:// 或 https:// 开头' 126 | ) 127 | } 128 | 129 | if (!keyPart) { 130 | return errorResponse( 131 | 'BAD_REQUEST', 132 | '缺少 Key ID 或 Token', 133 | '请在 URL 后面加上冒号和 Key ID(6位)或完整 Token' 134 | ) 135 | } 136 | 137 | // 判断是 key_id (6位字母数字) 还是直接 token 138 | const isKeyId = /^[a-z0-9]{6}$/.test(keyPart) 139 | 140 | if (isKeyId) { 141 | // 按 key_id 查找 token 142 | const keyId = keyPart 143 | usedKeyId = keyId 144 | 145 | // 检查该 API URL 是否在配置中 146 | if (!config[targetApiUrl]) { 147 | return errorResponse( 148 | 'NOT_FOUND', 149 | 'API 地址未配置', 150 | `目标 API "${targetApiUrl}" 尚未在系统中注册,请联系管理员添加配置` 151 | ) 152 | } 153 | 154 | // 在该 URL 的 keys 中查找指定 key_id 155 | const keyConfig = config[targetApiUrl].keys.find(k => k.key_id === keyId) 156 | if (!keyConfig) { 157 | return errorResponse( 158 | 'NOT_FOUND', 159 | 'Key ID 不存在', 160 | `找不到 Key ID "${keyId}",请检查是否输入正确或联系管理员获取有效的 Key ID` 161 | ) 162 | } 163 | 164 | if (!keyConfig.enabled) { 165 | return errorResponse( 166 | 'FORBIDDEN', 167 | 'Key 已被禁用', 168 | `Key ID "${keyId}" 当前处于禁用状态,请联系管理员启用或获取新的 Key ID` 169 | ) 170 | } 171 | 172 | // 检查是否过期 173 | if (keyConfig.expires_at && new Date(keyConfig.expires_at) < new Date()) { 174 | return errorResponse( 175 | 'FORBIDDEN', 176 | 'Key 已过期', 177 | `Key ID "${keyId}" 已于 ${keyConfig.expires_at} 过期,请联系管理员续期或获取新的 Key ID` 178 | ) 179 | } 180 | 181 | tokenToUse = keyConfig.token 182 | } else { 183 | // 直接使用传入的 token 184 | tokenToUse = keyPart 185 | } 186 | } 187 | 188 | // 设置目标主机和协议 189 | const targetUrl = new URL(targetApiUrl) 190 | 191 | // 检查是否在尝试反代自身(禁止循环代理) 192 | const selfHostname = url.hostname.toLowerCase() 193 | const targetHostname = targetUrl.hostname.toLowerCase() 194 | if (targetHostname === selfHostname || 195 | targetHostname.endsWith('.' + selfHostname) || 196 | selfHostname.endsWith('.' + targetHostname)) { 197 | return errorResponse( 198 | 'FORBIDDEN', 199 | '禁止反代自身', 200 | '不允许将请求代理到代理服务自身的域名,这会造成循环请求' 201 | ) 202 | } 203 | 204 | url.protocol = targetUrl.protocol 205 | url.hostname = targetUrl.hostname 206 | url.port = targetUrl.port || '' 207 | 208 | // 获取原始请求头 209 | const headers = new Headers(request.headers) 210 | 211 | // 设置 Authorization header 212 | headers.set('authorization', 'Bearer ' + tokenToUse) 213 | 214 | const modifiedRequest = new Request(url.toString(), { 215 | headers: headers, 216 | method: request.method, 217 | body: request.body, 218 | redirect: 'follow', 219 | }) 220 | 221 | try { 222 | const response = await fetch(modifiedRequest) 223 | const modifiedResponse = new Response(response.body, response) 224 | 225 | // 添加允许跨域访问的响应头 226 | modifiedResponse.headers.set('Access-Control-Allow-Origin', '*') 227 | 228 | // SSE 流式响应优化:禁用缓冲和压缩,确保实时传输 229 | const contentType = response.headers.get('content-type') || '' 230 | const isStreaming = contentType.includes('text/event-stream') || 231 | contentType.includes('stream') || 232 | request.headers.get('accept')?.includes('text/event-stream') 233 | if (isStreaming) { 234 | modifiedResponse.headers.set('Cache-Control', 'no-cache, no-store, no-transform, must-revalidate') 235 | modifiedResponse.headers.set('X-Accel-Buffering', 'no') 236 | modifiedResponse.headers.set('Connection', 'keep-alive') 237 | modifiedResponse.headers.set('Content-Encoding', 'identity') 238 | // 删除可能导致缓冲的 headers 239 | modifiedResponse.headers.delete('Content-Length') 240 | } 241 | 242 | // 记录请求统计(使用 waitUntil 确保在响应后完成) 243 | if (ctx && ctx.waitUntil) { 244 | ctx.waitUntil(recordRequest(env, { 245 | apiUrl: targetApiUrl, 246 | keyId: usedKeyId, 247 | success: response.ok, 248 | ip: clientIp, 249 | })) 250 | } 251 | 252 | return modifiedResponse 253 | } catch (error) { 254 | // 记录失败请求 255 | if (ctx && ctx.waitUntil) { 256 | ctx.waitUntil(recordRequest(env, { 257 | apiUrl: targetApiUrl, 258 | keyId: usedKeyId, 259 | success: false, 260 | ip: clientIp, 261 | })) 262 | } 263 | 264 | console.error('Proxy request error:', error) 265 | return errorResponse( 266 | 'SERVICE_ERROR', 267 | '代理请求失败', 268 | `无法连接到目标 API "${targetApiUrl}",可能是网络问题或目标服务不可用,请稍后重试` 269 | ) 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /src/handlers/api.js: -------------------------------------------------------------------------------- 1 | // ============ API 路由处理 ============ 2 | 3 | import { verifyAdmin, validateConfigPayload, jsonResponse } from '../utils/helpers.js' 4 | import { 5 | getConfigFromDB, 6 | saveConfigToDB, 7 | updateConfigInDB, 8 | deleteConfigFromDB, 9 | updateSkAlias, 10 | } from '../db/supabase.js' 11 | import { getStats, getLastUsedTimes, recordLogin, getLoginRecords, blockIp, unblockIp, getBlockedIps } from '../cache/stats.js' 12 | import { getRedisClient } from '../cache/redis.js' 13 | import { warmupCache } from '../cache/index.js' 14 | 15 | /** 16 | * 处理 API 请求 17 | */ 18 | export async function handleApiRequest(request, env, url) { 19 | // 验证管理员权限 20 | if (!verifyAdmin(request, env)) { 21 | return jsonResponse({ error: 'Unauthorized' }, 401) 22 | } 23 | 24 | const path = url.pathname 25 | 26 | // GET /api/configs - 获取所有配置 27 | if (path === '/api/configs' && request.method === 'GET') { 28 | const config = await getConfigFromDB(env) 29 | const lastUsed = await getLastUsedTimes(env) 30 | return jsonResponse({ success: true, data: config, lastUsed }) 31 | } 32 | 33 | // POST /api/configs - 添加新配置 34 | if (path === '/api/configs' && request.method === 'POST') { 35 | const body = await request.json() 36 | const validation = validateConfigPayload(body) 37 | if (!validation.valid) { 38 | return jsonResponse({ error: validation.error }, 400) 39 | } 40 | const result = await saveConfigToDB( 41 | env, 42 | body.api_url, 43 | body.token, 44 | body.enabled ?? true, 45 | body.remark || '', 46 | body.expires_at || null 47 | ) 48 | return jsonResponse(result, result.success ? 200 : 400) 49 | } 50 | 51 | // PATCH /api/configs/:id - 更新配置 52 | if (path.match(/^\/api\/configs\/\d+$/) && request.method === 'PATCH') { 53 | const id = path.split('/').pop() 54 | const body = await request.json() 55 | const validation = validateConfigPayload(body, { partial: true }) 56 | if (!validation.valid) { 57 | return jsonResponse({ error: validation.error }, 400) 58 | } 59 | const result = await updateConfigInDB(env, id, body) 60 | return jsonResponse(result, result.success ? 200 : 400) 61 | } 62 | 63 | // DELETE /api/configs/:id - 删除配置 64 | if (path.match(/^\/api\/configs\/\d+$/) && request.method === 'DELETE') { 65 | const id = path.split('/').pop() 66 | const result = await deleteConfigFromDB(env, id) 67 | return jsonResponse(result, result.success ? 200 : 400) 68 | } 69 | 70 | // POST /api/configs/:id/sk-alias - 生成或更新 SK 别名 71 | if (path.match(/^\/api\/configs\/\d+\/sk-alias$/) && request.method === 'POST') { 72 | const id = path.split('/')[3] 73 | const result = await updateSkAlias(env, id) 74 | return jsonResponse(result, result.success ? 200 : 400) 75 | } 76 | 77 | // DELETE /api/configs/:id/sk-alias - 删除 SK 别名 78 | if (path.match(/^\/api\/configs\/\d+\/sk-alias$/) && request.method === 'DELETE') { 79 | const id = path.split('/')[3] 80 | const result = await updateSkAlias(env, id, '') 81 | return jsonResponse(result, result.success ? 200 : 400) 82 | } 83 | 84 | // GET /api/status - 获取系统状态(存储模式、数据库连接) 85 | if (path === '/api/status' && request.method === 'GET') { 86 | const hasDbConfig = Boolean(env.SUPABASE_URL && env.SUPABASE_KEY) 87 | const result = { 88 | success: true, 89 | storage_mode: hasDbConfig ? 'database' : 'passthrough', 90 | database_configured: hasDbConfig, 91 | database_connected: false, 92 | } 93 | 94 | if (hasDbConfig) { 95 | // 测试数据库连接 96 | try { 97 | const response = await fetch( 98 | `${env.SUPABASE_URL}/rest/v1/api_configs?select=count&limit=1`, 99 | { 100 | headers: { 101 | apikey: env.SUPABASE_KEY, 102 | Authorization: `Bearer ${env.SUPABASE_KEY}`, 103 | }, 104 | } 105 | ) 106 | result.database_connected = response.ok 107 | if (!response.ok) { 108 | result.database_error = `HTTP ${response.status}` 109 | } 110 | } catch (error) { 111 | result.database_connected = false 112 | result.database_error = error.message 113 | } 114 | } 115 | 116 | return jsonResponse(result) 117 | } 118 | 119 | // POST /api/login - 记录登录(验证成功后前端调用) 120 | if (path === '/api/login' && request.method === 'POST') { 121 | await recordLogin(env, request) 122 | return jsonResponse({ success: true }) 123 | } 124 | 125 | // GET /api/logins - 获取登录记录 126 | if (path === '/api/logins' && request.method === 'GET') { 127 | const limit = parseInt(url.searchParams.get('limit') || '20') 128 | const records = await getLoginRecords(env, limit) 129 | return jsonResponse({ success: true, data: records }) 130 | } 131 | 132 | // GET /api/stats - 获取请求统计数据 133 | if (path === '/api/stats' && request.method === 'GET') { 134 | const days = parseInt(url.searchParams.get('days') || '7') 135 | const stats = await getStats(env, days) 136 | return jsonResponse({ success: true, data: stats }) 137 | } 138 | 139 | // ============ IP 黑名单 API ============ 140 | 141 | // GET /api/blacklist - 获取黑名单列表 142 | if (path === '/api/blacklist' && request.method === 'GET') { 143 | const blockedIps = await getBlockedIps(env) 144 | return jsonResponse({ success: true, data: blockedIps }) 145 | } 146 | 147 | // POST /api/blacklist - 添加 IP 到黑名单 148 | if (path === '/api/blacklist' && request.method === 'POST') { 149 | const body = await request.json() 150 | if (!body.ip) { 151 | return jsonResponse({ error: 'IP address is required' }, 400) 152 | } 153 | const result = await blockIp(env, body.ip, body.reason || '手动封禁') 154 | return jsonResponse(result, result.success ? 200 : 400) 155 | } 156 | 157 | // DELETE /api/blacklist/:ip - 从黑名单移除 IP 158 | if (path.startsWith('/api/blacklist/') && request.method === 'DELETE') { 159 | const ip = decodeURIComponent(path.replace('/api/blacklist/', '')) 160 | if (!ip) { 161 | return jsonResponse({ error: 'IP address is required' }, 400) 162 | } 163 | const result = await unblockIp(env, ip) 164 | return jsonResponse(result, result.success ? 200 : 400) 165 | } 166 | 167 | // GET /api/redis/test - 测试 Redis 连接 168 | if (path === '/api/redis/test' && request.method === 'GET') { 169 | const redis = getRedisClient(env) 170 | if (!redis) { 171 | return jsonResponse({ 172 | success: false, 173 | configured: false, 174 | error: 'Redis not configured (missing UPSTASH_REDIS_URL or UPSTASH_REDIS_TOKEN)' 175 | }) 176 | } 177 | 178 | try { 179 | const testKey = 'anyrouter:test:ping' 180 | const testValue = Date.now().toString() 181 | 182 | // 测试写入 183 | await redis.set(testKey, testValue, 60) 184 | 185 | // 测试读取 186 | const readValue = await redis.get(testKey) 187 | 188 | // 测试删除 189 | await redis.del(testKey) 190 | 191 | return jsonResponse({ 192 | success: true, 193 | configured: true, 194 | connected: true, 195 | latency_test: readValue === testValue ? 'passed' : 'failed', 196 | message: 'Redis connection successful' 197 | }) 198 | } catch (error) { 199 | return jsonResponse({ 200 | success: false, 201 | configured: true, 202 | connected: false, 203 | error: error.message 204 | }) 205 | } 206 | } 207 | 208 | // POST /api/cache/warmup - 预热缓存(从数据库加载到 Redis/KV/内存) 209 | if (path === '/api/cache/warmup' && request.method === 'POST') { 210 | const result = await warmupCache(env) 211 | return jsonResponse(result, result.success ? 200 : 400) 212 | } 213 | 214 | // POST /api/stats/test - 测试统计记录功能(直接写入 Redis) 215 | if (path === '/api/stats/test' && request.method === 'POST') { 216 | const redis = getRedisClient(env) 217 | if (!redis) { 218 | return jsonResponse({ success: false, error: 'Redis not configured' }) 219 | } 220 | 221 | const today = new Date().toISOString().split('T')[0] 222 | const key = `anyrouter:stats:daily:${today}:total` 223 | 224 | try { 225 | // 直接测试 INCR 命令 226 | const result = await redis.request(['INCR', key]) 227 | // 设置过期时间 228 | await redis.request(['EXPIRE', key, 604800]) 229 | return jsonResponse({ 230 | success: true, 231 | message: '写入成功', 232 | key: key, 233 | newValue: result 234 | }) 235 | } catch (error) { 236 | return jsonResponse({ success: false, error: error.message, stack: error.stack }) 237 | } 238 | } 239 | 240 | return jsonResponse({ error: 'Not found' }, 404) 241 | } 242 | -------------------------------------------------------------------------------- /src/db/supabase.js: -------------------------------------------------------------------------------- 1 | // ============ Supabase 数据库操作 ============ 2 | 3 | import { 4 | FALLBACK_CONFIG, 5 | CACHE_KEY, 6 | REDIS_CACHE_TTL_SECONDS, 7 | KV_CACHE_TTL_SECONDS, 8 | } from '../config.js' 9 | import { getCachedConfig, setConfigCache, invalidateAllCache } from '../cache/index.js' 10 | import { getRedisClient } from '../cache/redis.js' 11 | 12 | // 包装函数:清除所有缓存(内存 + Redis + KV) 13 | async function clearAllCache(env) { 14 | await invalidateAllCache(env) 15 | } 16 | 17 | /** 18 | * 从 Supabase 获取配置(支持多级缓存) 19 | * 缓存优先级:内存(10min) -> Redis(5min) -> KV(5min,备用) -> 数据库 20 | */ 21 | export async function getConfigFromDB(env) { 22 | // 1. 优先返回内存缓存(最快,~0ms) 23 | const memoryCached = getCachedConfig() 24 | if (memoryCached) { 25 | return memoryCached 26 | } 27 | 28 | // 2. 尝试从 Redis 缓存获取(推荐,~5-20ms) 29 | const redis = getRedisClient(env) 30 | if (redis) { 31 | try { 32 | const redisCached = await redis.get(CACHE_KEY) 33 | if (redisCached) { 34 | const parsed = JSON.parse(redisCached) 35 | setConfigCache(parsed) 36 | return parsed 37 | } 38 | } catch { 39 | // Redis 读取失败,继续 40 | } 41 | } 42 | 43 | // 3. 尝试从 KV 缓存获取(备用,~1-5ms) 44 | if (env.CONFIG_KV) { 45 | try { 46 | const kvCached = await env.CONFIG_KV.get(CACHE_KEY, { type: 'json' }) 47 | if (kvCached) { 48 | setConfigCache(kvCached) 49 | return kvCached 50 | } 51 | } catch { 52 | // KV 读取失败,继续 53 | } 54 | } 55 | 56 | // 4. 无数据库配置时返回 fallback 57 | if (!env.SUPABASE_URL || !env.SUPABASE_KEY) { 58 | setConfigCache(FALLBACK_CONFIG) 59 | return FALLBACK_CONFIG 60 | } 61 | 62 | // 5. 从数据库查询(最慢,~50-200ms) 63 | try { 64 | // 先尝试带软删除过滤的查询 65 | let response = await fetch( 66 | `${env.SUPABASE_URL}/rest/v1/api_configs?select=*&deleted_at=is.null&order=created_at.desc`, 67 | { 68 | headers: { 69 | apikey: env.SUPABASE_KEY, 70 | Authorization: `Bearer ${env.SUPABASE_KEY}`, 71 | }, 72 | }, 73 | ) 74 | 75 | // 如果查询失败(可能是 deleted_at 列不存在),回退到不带过滤的查询 76 | if (!response.ok) { 77 | response = await fetch( 78 | `${env.SUPABASE_URL}/rest/v1/api_configs?select=*&order=created_at.desc`, 79 | { 80 | headers: { 81 | apikey: env.SUPABASE_KEY, 82 | Authorization: `Bearer ${env.SUPABASE_KEY}`, 83 | }, 84 | }, 85 | ) 86 | } 87 | 88 | if (!response.ok) { 89 | setConfigCache(FALLBACK_CONFIG) 90 | return FALLBACK_CONFIG 91 | } 92 | 93 | const data = await response.json() 94 | const config = {} 95 | 96 | data.forEach((item) => { 97 | if (!config[item.api_url]) { 98 | config[item.api_url] = { keys: [] } 99 | } 100 | config[item.api_url].keys.push({ 101 | id: item.id, 102 | key_id: item.key_id, 103 | sk_alias: item.sk_alias || null, 104 | token: item.token, 105 | enabled: item.enabled, 106 | remark: item.remark || '', 107 | expires_at: item.expires_at || null, 108 | created_at: item.created_at, 109 | updated_at: item.updated_at, 110 | }) 111 | }) 112 | 113 | const finalizedConfig = Object.keys(config).length > 0 ? config : FALLBACK_CONFIG 114 | 115 | // 写入内存缓存 116 | setConfigCache(finalizedConfig) 117 | 118 | // 写入 Redis 缓存(异步,不阻塞响应) 119 | if (redis) { 120 | redis.set(CACHE_KEY, JSON.stringify(finalizedConfig), REDIS_CACHE_TTL_SECONDS) 121 | .catch(() => {}) 122 | } 123 | 124 | // 写入 KV 缓存(备用,异步) 125 | if (env.CONFIG_KV) { 126 | env.CONFIG_KV.put(CACHE_KEY, JSON.stringify(finalizedConfig), { 127 | expirationTtl: KV_CACHE_TTL_SECONDS, 128 | }).catch(() => {}) 129 | } 130 | 131 | return finalizedConfig 132 | } catch { 133 | setConfigCache(FALLBACK_CONFIG) 134 | return FALLBACK_CONFIG 135 | } 136 | } 137 | 138 | /** 139 | * 保存配置到数据库 140 | */ 141 | export async function saveConfigToDB(env, apiUrl, token, enabled, remark = '', expiresAt = null) { 142 | if (!env.SUPABASE_URL || !env.SUPABASE_KEY) { 143 | return { success: false, error: 'Database not configured' } 144 | } 145 | 146 | try { 147 | const response = await fetch(`${env.SUPABASE_URL}/rest/v1/api_configs`, { 148 | method: 'POST', 149 | headers: { 150 | apikey: env.SUPABASE_KEY, 151 | Authorization: `Bearer ${env.SUPABASE_KEY}`, 152 | 'Content-Type': 'application/json', 153 | Prefer: 'return=representation', 154 | }, 155 | body: JSON.stringify({ 156 | api_url: apiUrl, 157 | token: token, 158 | enabled: enabled, 159 | remark: remark || null, 160 | expires_at: expiresAt || null, 161 | }), 162 | }) 163 | 164 | if (!response.ok) { 165 | return { success: false, error: await response.text() } 166 | } 167 | 168 | await clearAllCache(env) 169 | return { success: true, data: await response.json() } 170 | } catch (error) { 171 | return { success: false, error: error.message } 172 | } 173 | } 174 | 175 | /** 176 | * 更新配置 177 | */ 178 | export async function updateConfigInDB(env, id, updates) { 179 | if (!env.SUPABASE_URL || !env.SUPABASE_KEY) { 180 | return { success: false, error: 'Database not configured' } 181 | } 182 | 183 | try { 184 | // 添加更新时间 185 | const data = { ...updates, updated_at: new Date().toISOString() } 186 | 187 | const response = await fetch( 188 | `${env.SUPABASE_URL}/rest/v1/api_configs?id=eq.${id}`, 189 | { 190 | method: 'PATCH', 191 | headers: { 192 | apikey: env.SUPABASE_KEY, 193 | Authorization: `Bearer ${env.SUPABASE_KEY}`, 194 | 'Content-Type': 'application/json', 195 | }, 196 | body: JSON.stringify(data), 197 | } 198 | ) 199 | 200 | if (!response.ok) { 201 | return { success: false, error: await response.text() } 202 | } 203 | 204 | await clearAllCache(env) 205 | return { success: true } 206 | } catch (error) { 207 | return { success: false, error: error.message } 208 | } 209 | } 210 | 211 | /** 212 | * 软删除配置(设置 deleted_at 而非物理删除) 213 | */ 214 | export async function deleteConfigFromDB(env, id) { 215 | if (!env.SUPABASE_URL || !env.SUPABASE_KEY) { 216 | return { success: false, error: 'Database not configured' } 217 | } 218 | 219 | try { 220 | const response = await fetch( 221 | `${env.SUPABASE_URL}/rest/v1/api_configs?id=eq.${id}`, 222 | { 223 | method: 'PATCH', 224 | headers: { 225 | apikey: env.SUPABASE_KEY, 226 | Authorization: `Bearer ${env.SUPABASE_KEY}`, 227 | 'Content-Type': 'application/json', 228 | }, 229 | body: JSON.stringify({ 230 | deleted_at: new Date().toISOString(), 231 | }), 232 | } 233 | ) 234 | 235 | if (!response.ok) { 236 | return { success: false, error: await response.text() } 237 | } 238 | 239 | await clearAllCache(env) 240 | return { success: true } 241 | } catch (error) { 242 | return { success: false, error: error.message } 243 | } 244 | } 245 | 246 | /** 247 | * 生成 SK 别名 248 | * 格式: sk-ar-[32位随机字符] 249 | * 类似 OpenAI 的 sk-xxx 格式 250 | */ 251 | export function generateSkAlias() { 252 | const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' 253 | let result = 'sk-ar-' 254 | for (let i = 0; i < 32; i++) { 255 | result += chars.charAt(Math.floor(Math.random() * chars.length)) 256 | } 257 | return result 258 | } 259 | 260 | /** 261 | * 更新配置的 SK 别名 262 | * @param {object} env - 环境变量 263 | * @param {number} id - 配置 ID 264 | * @param {string|null} skAlias - SK 别名(null 表示生成新的) 265 | */ 266 | export async function updateSkAlias(env, id, skAlias = null) { 267 | if (!env.SUPABASE_URL || !env.SUPABASE_KEY) { 268 | return { success: false, error: 'Database not configured' } 269 | } 270 | 271 | const newAlias = skAlias || generateSkAlias() 272 | 273 | try { 274 | const response = await fetch( 275 | `${env.SUPABASE_URL}/rest/v1/api_configs?id=eq.${id}`, 276 | { 277 | method: 'PATCH', 278 | headers: { 279 | apikey: env.SUPABASE_KEY, 280 | Authorization: `Bearer ${env.SUPABASE_KEY}`, 281 | 'Content-Type': 'application/json', 282 | Prefer: 'return=representation', 283 | }, 284 | body: JSON.stringify({ 285 | sk_alias: newAlias, 286 | updated_at: new Date().toISOString(), 287 | }), 288 | } 289 | ) 290 | 291 | if (!response.ok) { 292 | return { success: false, error: await response.text() } 293 | } 294 | 295 | await clearAllCache(env) 296 | return { success: true, sk_alias: newAlias } 297 | } catch (error) { 298 | return { success: false, error: error.message } 299 | } 300 | } 301 | 302 | /** 303 | * 通过 SK 别名查找配置 304 | * @param {object} config - 配置对象 305 | * @param {string} skAlias - SK 别名 306 | * @returns {{ apiUrl: string, key: object } | null} 307 | */ 308 | export function findBySkAlias(config, skAlias) { 309 | for (const [apiUrl, apiConfig] of Object.entries(config)) { 310 | if (!apiConfig.keys) continue 311 | const key = apiConfig.keys.find(k => k.sk_alias === skAlias) 312 | if (key) { 313 | return { apiUrl, key } 314 | } 315 | } 316 | return null 317 | } 318 | 319 | /** 320 | * 从指定 URL 的配置中随机选择一个启用的 key 321 | */ 322 | export function getRandomEnabledKey(config, apiUrl) { 323 | const apiConfig = config[apiUrl] 324 | if (!apiConfig || !apiConfig.keys) { 325 | return null 326 | } 327 | 328 | // 过滤出所有启用的 keys 329 | const enabledKeys = apiConfig.keys.filter((key) => key.enabled) 330 | 331 | if (enabledKeys.length === 0) { 332 | return null 333 | } 334 | 335 | // 随机选择一个启用的 key 336 | const randomIndex = Math.floor(Math.random() * enabledKeys.length) 337 | return enabledKeys[randomIndex].token 338 | } 339 | -------------------------------------------------------------------------------- /src/cache/stats.js: -------------------------------------------------------------------------------- 1 | // ============ Redis 请求统计 & IP 黑名单 ============ 2 | 3 | import { getRedisClient } from './redis.js' 4 | 5 | // 统计 Key 前缀 6 | const STATS_PREFIX = 'anyrouter:stats' 7 | const BLACKLIST_KEY = 'anyrouter:blacklist:ips' 8 | 9 | // 统计采样配置 10 | // STATS_SAMPLE_PERCENT: 采样百分比(1-100) 11 | // 100 = 100% 每次请求都记录(默认,精确但 Redis 调用多) 12 | // 10 = 10% 只记录 1/10 的请求(统计值会自动乘以 10 还原) 13 | // 1 = 1% 只记录 1/100 的请求(大流量场景推荐) 14 | const STATS_SAMPLE_PERCENT = 100 15 | 16 | /** 17 | * 获取今日日期字符串 (YYYY-MM-DD) 18 | */ 19 | function getTodayKey() { 20 | return new Date().toISOString().split('T')[0] 21 | } 22 | 23 | /** 24 | * 获取当前小时字符串 (YYYY-MM-DD-HH) 25 | */ 26 | function getHourKey() { 27 | const now = new Date() 28 | return `${now.toISOString().split('T')[0]}-${now.getUTCHours().toString().padStart(2, '0')}` 29 | } 30 | 31 | /** 32 | * 记录请求统计(优化版:采样减少 Redis 调用) 33 | * @param {object} env - 环境变量 34 | * @param {object} data - 请求数据 { apiUrl, keyId, success, ip } 35 | */ 36 | export async function recordRequest(env, data) { 37 | const redis = getRedisClient(env) 38 | if (!redis) return 39 | 40 | // 采样判断:随机数 < 采样百分比 时才记录 41 | // 例如 STATS_SAMPLE_PERCENT=10 时,只有 10% 的请求会记录 42 | const shouldRecord = Math.random() * 100 < STATS_SAMPLE_PERCENT 43 | if (!shouldRecord) return 44 | 45 | // 倍率:用于还原真实统计值 46 | // 例如 10% 采样时,每次记录 +10 来估算真实总数 47 | const multiplier = Math.round(100 / STATS_SAMPLE_PERCENT) 48 | 49 | const { apiUrl, keyId, success, ip } = data 50 | const today = getTodayKey() 51 | const hour = getHourKey() 52 | 53 | try { 54 | // 基础统计(每次采样都记录) 55 | await redis.request(['INCRBY', `${STATS_PREFIX}:daily:${today}:total`, multiplier]) 56 | await redis.request(['INCRBY', `${STATS_PREFIX}:daily:${today}:${success ? 'success' : 'error'}`, multiplier]) 57 | await redis.request(['INCRBY', `${STATS_PREFIX}:hourly:${hour}:total`, multiplier]) 58 | 59 | // URL 统计 60 | if (apiUrl) { 61 | await redis.request(['HINCRBY', `${STATS_PREFIX}:daily:${today}:urls`, apiUrl, multiplier]) 62 | } 63 | 64 | // Key 统计 65 | if (keyId) { 66 | await redis.request(['HINCRBY', `${STATS_PREFIX}:daily:${today}:keys`, keyId, multiplier]) 67 | await redis.request(['HSET', `${STATS_PREFIX}:lastused`, keyId, new Date().toISOString()]) 68 | } 69 | 70 | // IP 统计 71 | if (ip && ip !== 'unknown') { 72 | await redis.request(['HINCRBY', `${STATS_PREFIX}:daily:${today}:ips`, ip, multiplier]) 73 | } 74 | 75 | // 设置过期时间(7天) 76 | const ttl = 7 * 24 * 60 * 60 77 | await redis.request(['EXPIRE', `${STATS_PREFIX}:daily:${today}:total`, ttl]) 78 | await redis.request(['EXPIRE', `${STATS_PREFIX}:hourly:${hour}:total`, ttl]) 79 | } catch { 80 | // 统计失败不影响主流程 81 | } 82 | } 83 | 84 | /** 85 | * 获取统计数据 86 | * @param {object} env - 环境变量 87 | * @param {number} days - 查询天数(默认7天) 88 | */ 89 | export async function getStats(env, days = 7) { 90 | const redis = getRedisClient(env) 91 | if (!redis) { 92 | return { enabled: false, message: 'Redis not configured' } 93 | } 94 | 95 | try { 96 | const stats = { 97 | enabled: true, 98 | daily: [], 99 | hourly: [], 100 | topUrls: {}, 101 | topKeys: {}, 102 | topIps: {}, 103 | summary: { total: 0, success: 0, error: 0 }, 104 | } 105 | 106 | // 获取最近 N 天的数据 107 | const dates = [] 108 | for (let i = 0; i < days; i++) { 109 | const d = new Date() 110 | d.setDate(d.getDate() - i) 111 | dates.push(d.toISOString().split('T')[0]) 112 | } 113 | 114 | // 查询每日数据 115 | for (const date of dates) { 116 | const total = await redis.get(`${STATS_PREFIX}:daily:${date}:total`) || 0 117 | const success = await redis.get(`${STATS_PREFIX}:daily:${date}:success`) || 0 118 | const error = await redis.get(`${STATS_PREFIX}:daily:${date}:error`) || 0 119 | 120 | stats.daily.push({ 121 | date, 122 | total: parseInt(total), 123 | success: parseInt(success), 124 | error: parseInt(error), 125 | }) 126 | 127 | stats.summary.total += parseInt(total) 128 | stats.summary.success += parseInt(success) 129 | stats.summary.error += parseInt(error) 130 | } 131 | 132 | // 获取今日 URL 使用排行 133 | const today = getTodayKey() 134 | const urlStats = await redis.request(['HGETALL', `${STATS_PREFIX}:daily:${today}:urls`]) 135 | if (urlStats && Array.isArray(urlStats)) { 136 | for (let i = 0; i < urlStats.length; i += 2) { 137 | stats.topUrls[urlStats[i]] = parseInt(urlStats[i + 1]) 138 | } 139 | } 140 | 141 | // 获取今日 Key 使用排行 142 | const keyStats = await redis.request(['HGETALL', `${STATS_PREFIX}:daily:${today}:keys`]) 143 | if (keyStats && Array.isArray(keyStats)) { 144 | for (let i = 0; i < keyStats.length; i += 2) { 145 | stats.topKeys[keyStats[i]] = parseInt(keyStats[i + 1]) 146 | } 147 | } 148 | 149 | // 获取今日 IP 使用排行 150 | const ipStats = await redis.request(['HGETALL', `${STATS_PREFIX}:daily:${today}:ips`]) 151 | if (ipStats && Array.isArray(ipStats)) { 152 | for (let i = 0; i < ipStats.length; i += 2) { 153 | stats.topIps[ipStats[i]] = parseInt(ipStats[i + 1]) 154 | } 155 | } 156 | 157 | // 获取最近24小时数据 158 | for (let i = 0; i < 24; i++) { 159 | const d = new Date() 160 | d.setHours(d.getHours() - i) 161 | const hourKey = `${d.toISOString().split('T')[0]}-${d.getUTCHours().toString().padStart(2, '0')}` 162 | const hourTotal = await redis.get(`${STATS_PREFIX}:hourly:${hourKey}:total`) || 0 163 | stats.hourly.push({ 164 | hour: hourKey, 165 | total: parseInt(hourTotal), 166 | }) 167 | } 168 | 169 | stats.daily.reverse() // 按时间正序 170 | stats.hourly.reverse() 171 | 172 | return stats 173 | } catch (error) { 174 | return { enabled: false, error: error.message } 175 | } 176 | } 177 | 178 | /** 179 | * 获取所有 key 的最后使用时间 180 | * @param {object} env - 环境变量 181 | * @returns {Promise>} keyId -> ISO时间字符串 182 | */ 183 | export async function getLastUsedTimes(env) { 184 | const redis = getRedisClient(env) 185 | if (!redis) return {} 186 | 187 | try { 188 | const result = await redis.request(['HGETALL', `${STATS_PREFIX}:lastused`]) 189 | if (!result || !Array.isArray(result)) return {} 190 | 191 | const lastUsed = {} 192 | for (let i = 0; i < result.length; i += 2) { 193 | lastUsed[result[i]] = result[i + 1] 194 | } 195 | return lastUsed 196 | } catch { 197 | return {} 198 | } 199 | } 200 | 201 | /** 202 | * 记录管理员登录 203 | * @param {object} env - 环境变量 204 | * @param {Request} request - 请求对象(用于获取 IP) 205 | */ 206 | export async function recordLogin(env, request) { 207 | const redis = getRedisClient(env) 208 | if (!redis) return 209 | 210 | try { 211 | // 获取客户端 IP(Cloudflare 提供) 212 | const ip = request.headers.get('CF-Connecting-IP') || 213 | request.headers.get('X-Forwarded-For')?.split(',')[0]?.trim() || 214 | 'unknown' 215 | 216 | const userAgent = request.headers.get('User-Agent') || 'unknown' 217 | const now = new Date().toISOString() 218 | 219 | // 登录记录格式:时间|IP|UA 220 | const record = JSON.stringify({ time: now, ip, ua: userAgent }) 221 | 222 | // 使用 LPUSH 添加到列表头部,最多保留 50 条记录 223 | await redis.request(['LPUSH', `${STATS_PREFIX}:logins`, record]) 224 | await redis.request(['LTRIM', `${STATS_PREFIX}:logins`, 0, 49]) 225 | } catch { 226 | // 记录失败不影响登录 227 | } 228 | } 229 | 230 | /** 231 | * 获取登录记录 232 | * @param {object} env - 环境变量 233 | * @param {number} limit - 获取记录数量(默认20条) 234 | */ 235 | export async function getLoginRecords(env, limit = 20) { 236 | const redis = getRedisClient(env) 237 | if (!redis) return [] 238 | 239 | try { 240 | const records = await redis.request(['LRANGE', `${STATS_PREFIX}:logins`, 0, limit - 1]) 241 | if (!records || !Array.isArray(records)) return [] 242 | 243 | return records.map(r => { 244 | try { 245 | return JSON.parse(r) 246 | } catch { 247 | return null 248 | } 249 | }).filter(Boolean) 250 | } catch { 251 | return [] 252 | } 253 | } 254 | 255 | // ============ IP 黑名单管理 ============ 256 | 257 | /** 258 | * 检查 IP 是否在黑名单中 259 | * @param {object} env - 环境变量 260 | * @param {string} ip - IP 地址 261 | * @returns {Promise<{blocked: boolean, reason?: string}>} 262 | */ 263 | export async function isIpBlocked(env, ip) { 264 | const redis = getRedisClient(env) 265 | if (!redis || !ip || ip === 'unknown') return { blocked: false } 266 | 267 | try { 268 | const reason = await redis.request(['HGET', BLACKLIST_KEY, ip]) 269 | if (reason) { 270 | return { blocked: true, reason: reason || '已被管理员封禁' } 271 | } 272 | return { blocked: false } 273 | } catch { 274 | return { blocked: false } 275 | } 276 | } 277 | 278 | /** 279 | * 添加 IP 到黑名单 280 | * @param {object} env - 环境变量 281 | * @param {string} ip - IP 地址 282 | * @param {string} reason - 封禁原因 283 | */ 284 | export async function blockIp(env, ip, reason = '手动封禁') { 285 | const redis = getRedisClient(env) 286 | if (!redis) return { success: false, error: 'Redis not configured' } 287 | 288 | try { 289 | const record = JSON.stringify({ 290 | reason, 291 | blocked_at: new Date().toISOString(), 292 | }) 293 | await redis.request(['HSET', BLACKLIST_KEY, ip, record]) 294 | return { success: true } 295 | } catch (error) { 296 | return { success: false, error: error.message } 297 | } 298 | } 299 | 300 | /** 301 | * 从黑名单移除 IP 302 | * @param {object} env - 环境变量 303 | * @param {string} ip - IP 地址 304 | */ 305 | export async function unblockIp(env, ip) { 306 | const redis = getRedisClient(env) 307 | if (!redis) return { success: false, error: 'Redis not configured' } 308 | 309 | try { 310 | await redis.request(['HDEL', BLACKLIST_KEY, ip]) 311 | return { success: true } 312 | } catch (error) { 313 | return { success: false, error: error.message } 314 | } 315 | } 316 | 317 | /** 318 | * 获取黑名单列表 319 | * @param {object} env - 环境变量 320 | */ 321 | export async function getBlockedIps(env) { 322 | const redis = getRedisClient(env) 323 | if (!redis) return [] 324 | 325 | try { 326 | const result = await redis.request(['HGETALL', BLACKLIST_KEY]) 327 | if (!result || !Array.isArray(result)) return [] 328 | 329 | const blockedIps = [] 330 | for (let i = 0; i < result.length; i += 2) { 331 | const ip = result[i] 332 | let info = { reason: '手动封禁', blocked_at: null } 333 | try { 334 | info = JSON.parse(result[i + 1]) 335 | } catch { 336 | info.reason = result[i + 1] || '手动封禁' 337 | } 338 | blockedIps.push({ ip, ...info }) 339 | } 340 | return blockedIps 341 | } catch { 342 | return [] 343 | } 344 | } 345 | -------------------------------------------------------------------------------- /src/pages/admin.js: -------------------------------------------------------------------------------- 1 | // ============ 管理页面 HTML ============ 2 | 3 | /** 4 | * 生成管理页面 HTML 5 | */ 6 | export function getAdminHtml() { 7 | return ` 8 | 9 | 10 | 11 | 12 | API Proxy Admin 13 | 14 | 15 | 16 | 17 | 39 | 40 | 41 |
42 | 43 |
44 |
45 |
46 |
47 | 48 |
49 |

管理员登录

50 |

输入密码以访问管理面板

51 |
52 |
53 |
54 | 55 | 56 |
57 | 58 | 59 |
60 |
61 |
62 | 63 | 64 | 253 |
254 | 255 | 256 | 276 | 277 | 864 | 865 | ` 866 | } 867 | -------------------------------------------------------------------------------- /src/pages/docs.js: -------------------------------------------------------------------------------- 1 | // ============ 公开文档页面 ============ 2 | 3 | /** 4 | * 生成文档页面 HTML(无需鉴权) 5 | */ 6 | export function getDocsHtml() { 7 | return ` 8 | 9 | 10 | 11 | 12 | AnyRouter - 通用 API 代理服务文档 13 | 14 | 15 | 16 | 17 | 41 | 42 | 43 | 44 |
45 |
46 |
47 |
48 |

AnyRouter

49 |

通用 API 代理服务

50 |

支持 OpenAI、Anthropic、Google、Azure、Groq 等任意 HTTP API 的统一转发

51 | 62 |
63 | 66 |
67 |
68 |
69 | 70 |
71 |
72 | 73 | 93 | 94 | 95 |
96 | 97 |
98 |

概述

99 |

AnyRouter 是一个运行在 Cloudflare Workers 上的通用 API 代理服务,可以转发任意 HTTP API 请求:

100 |
    101 |
  • 通用代理:支持任意 HTTP/HTTPS API,不限于 AI 服务
  • 102 |
  • 密钥管理:统一管理多个 API 密钥,通过短 ID 安全访问
  • 103 |
  • 直传模式:无需预先配置,直接传递 Token 即可使用
  • 104 |
  • 边缘加速:基于 Cloudflare 全球边缘网络,低延迟访问
  • 105 |
  • 请求统计:记录使用量,支持按 API 和 Key 统计
  • 106 |
107 |
108 | 109 | 110 |
111 |

支持的 API

112 |

AnyRouter 支持任意 HTTP API,以下是常用的 AI 服务示例:

113 | 114 |
115 |
116 |
117 |
118 | 119 |
120 | OpenAI 121 |
122 | api.openai.com 123 |
124 | 125 |
126 |
127 |
128 | 129 |
130 | Anthropic 131 |
132 | api.anthropic.com 133 |
134 | 135 |
136 |
137 |
138 | 139 |
140 | Google AI 141 |
142 | generativelanguage.googleapis.com 143 |
144 | 145 |
146 |
147 |
148 | 149 |
150 | Azure OpenAI 151 |
152 | xxx.openai.azure.com 153 |
154 | 155 |
156 |
157 |
158 | 159 |
160 | Groq 161 |
162 | api.groq.com 163 |
164 | 165 |
166 |
167 |
168 | 169 |
170 | Mistral 171 |
172 | api.mistral.ai 173 |
174 | 175 |
176 |
177 |
178 | 179 |
180 | Cohere 181 |
182 | api.cohere.ai 183 |
184 | 185 |
186 |
187 |
188 | 189 |
190 | 更多... 191 |
192 | 任意 HTTP API 193 |
194 |
195 | 196 |
197 |

只要是标准的 HTTP/HTTPS API,都可以通过 AnyRouter 代理访问,不限于上述服务。

198 |
199 |
200 | 201 | 202 |
203 |

快速开始

204 |
205 |
206 |

1. 获取代理地址

207 |

当前服务地址:

208 |
209 |
210 | 213 |
214 |
215 |
216 |

2. 设置认证信息

217 |

在请求头中添加 Authorization 字段,格式如下:

218 |
219 |
220 |
221 | 222 | 223 |
224 |

认证格式

225 |
226 |
Authorization: Bearer <目标API地址>:<Key ID 或 Token>
227 |
228 |
229 |

格式说明

230 |
    231 |
  • 目标API地址:完整的 API 地址,如 https://api.openai.com
  • 232 |
  • Key ID:6 位字母数字组合,用于从数据库查找对应的 Token
  • 233 |
  • Token:直接传递完整的 API Token(直传模式)
  • 234 |
235 |
236 | 237 |

各平台示例

238 |
239 |
240 | OpenAI: 241 | Bearer https://api.openai.com:a3x9k2 242 |
243 |
244 | Anthropic: 245 | Bearer https://api.anthropic.com:b4y8m1 246 |
247 |
248 | Google AI: 249 | Bearer https://generativelanguage.googleapis.com:c5z2n3 250 |
251 |
252 | Groq: 253 | Bearer https://api.groq.com:d6w4p5 254 |
255 |
256 |
257 | 258 | 259 |
260 |

使用模式

261 | 262 |
263 | 264 |
265 |
266 | 最佳 267 |

SK 别名模式

268 |
269 |

使用类似 OpenAI 格式的 SK 别名,一键访问

270 |
271 |
Bearer sk-ar-xxxxxxxx...
272 |
273 |
    274 |
  • 类似原生 API Key 格式
  • 275 |
  • 不暴露真实 Token
  • 276 |
  • 自动识别目标 API
  • 277 |
  • 可随时重新生成
  • 278 |
279 |
280 | 281 | 282 |
283 |
284 | 推荐 285 |

Key ID 模式

286 |
287 |

使用 6 位短 ID + URL 访问预配置的密钥

288 |
289 |
Bearer https://api.openai.com:a3x9k2
290 |
291 |
    292 |
  • 不暴露真实 Token
  • 293 |
  • 支持使用统计
  • 294 |
  • 可随时启用/禁用
  • 295 |
296 |
297 | 298 | 299 |
300 |
301 | 灵活 302 |

直传模式

303 |
304 |

直接在请求中传递 API Token

305 |
306 |
Bearer https://api.openai.com:sk-xxx...
307 |
308 |
    309 |
  • 即用即走,无需配置
  • 310 |
  • 支持任意 API 地址
  • 311 |
  • 临时使用场景
  • 312 |
313 |
314 |
315 | 316 |
317 |

模式自动判断

318 |

系统会根据 Authorization 内容自动判断模式:

319 |
    320 |
  • sk-ar-xxx 开头 → SK 别名模式(自动匹配目标 API)
  • 321 |
  • • URL 后跟 6 位字母数字(如 https://...:a3x9k2)→ Key ID 模式
  • 322 |
  • • URL 后跟其他格式(如 https://...:sk-xxx)→ 直传模式
  • 323 |
324 |
325 | 326 | 327 |
328 |

SK 别名详解

329 |

SK 别名是 AnyRouter 独创的认证方式,格式类似各大平台的 API Key:

330 |
331 |
332 |
格式对比
333 |
    334 |
  • OpenAI: sk-proj-xxx
  • 335 |
  • Anthropic: sk-ant-xxx
  • 336 |
  • AnyRouter: sk-ar-xxx
  • 337 |
338 |
339 |
340 |
使用方法
341 |
    342 |
  1. 在管理面板点击「生成」获取 SK 别名
  2. 343 |
  3. 直接用 sk-ar-xxx 作为 API Key
  4. 344 |
  5. 无需指定目标 API URL
  6. 345 |
346 |
347 |
348 |
349 |
350 | 351 | 352 |
353 |

代码示例

354 | 355 | 356 |
357 |

cURL - OpenAI

358 |
359 |
curl -X POST '/v1/chat/completions' \\
 360 |   -H 'Authorization: Bearer https://api.openai.com:a3x9k2' \\
 361 |   -H 'Content-Type: application/json' \\
 362 |   -d '{
 363 |     "model": "gpt-4",
 364 |     "messages": [{"role": "user", "content": "Hello!"}]
 365 |   }'
366 | 369 |
370 |
371 | 372 | 373 |
374 |

cURL - Anthropic

375 |
376 |
curl -X POST '/v1/messages' \\
 377 |   -H 'Authorization: Bearer https://api.anthropic.com:b4y8m1' \\
 378 |   -H 'Content-Type: application/json' \\
 379 |   -H 'anthropic-version: 2023-06-01' \\
 380 |   -d '{
 381 |     "model": "claude-sonnet-4-20250514",
 382 |     "max_tokens": 1024,
 383 |     "messages": [{"role": "user", "content": "Hello!"}]
 384 |   }'
385 | 388 |
389 |
390 | 391 | 392 |
393 |

cURL - Google AI (Gemini)

394 |
395 |
curl -X POST '/v1beta/models/gemini-pro:generateContent' \\
 396 |   -H 'Authorization: Bearer https://generativelanguage.googleapis.com:c5z2n3' \\
 397 |   -H 'Content-Type: application/json' \\
 398 |   -d '{
 399 |     "contents": [{"parts": [{"text": "Hello!"}]}]
 400 |   }'
401 | 404 |
405 |
406 | 407 | 408 |
409 |

Python - OpenAI SDK

410 |
411 |
from openai import OpenAI
 412 | 
 413 | client = OpenAI(
 414 |     base_url='/v1',
 415 |     api_key='https://api.openai.com:a3x9k2'
 416 | )
 417 | 
 418 | response = client.chat.completions.create(
 419 |     model="gpt-4",
 420 |     messages=[{"role": "user", "content": "Hello!"}]
 421 | )
 422 | print(response.choices[0].message.content)
423 | 426 |
427 |
428 | 429 | 430 |
431 |

Python - Anthropic SDK

432 |
433 |
import anthropic
 434 | 
 435 | client = anthropic.Anthropic(
 436 |     base_url='',
 437 |     api_key='https://api.anthropic.com:b4y8m1'
 438 | )
 439 | 
 440 | message = client.messages.create(
 441 |     model="claude-sonnet-4-20250514",
 442 |     max_tokens=1024,
 443 |     messages=[{"role": "user", "content": "Hello!"}]
 444 | )
 445 | print(message.content[0].text)
446 | 449 |
450 |
451 | 452 | 453 |
454 |

Python - Groq SDK

455 |
456 |
from groq import Groq
 457 | 
 458 | client = Groq(
 459 |     base_url='/openai/v1',
 460 |     api_key='https://api.groq.com:d6w4p5'
 461 | )
 462 | 
 463 | response = client.chat.completions.create(
 464 |     model="llama-3.1-70b-versatile",
 465 |     messages=[{"role": "user", "content": "Hello!"}]
 466 | )
 467 | print(response.choices[0].message.content)
468 | 471 |
472 |
473 | 474 | 475 |
476 |

JavaScript - fetch

477 |
478 |
const response = await fetch('/v1/chat/completions', {
 479 |   method: 'POST',
 480 |   headers: {
 481 |     'Authorization': 'Bearer https://api.openai.com:a3x9k2',
 482 |     'Content-Type': 'application/json'
 483 |   },
 484 |   body: JSON.stringify({
 485 |     model: 'gpt-4',
 486 |     messages: [{ role: 'user', content: 'Hello!' }]
 487 |   })
 488 | });
 489 | 
 490 | const data = await response.json();
 491 | console.log(data.choices[0].message.content);
492 | 495 |
496 |
497 |
498 | 499 | 500 |
501 |

SDK / CLI 配置

502 |

通过环境变量配置各种 SDK 和 CLI 工具使用本代理服务:

503 | 504 | 505 |
506 |

SK 别名模式(推荐)

507 |

使用 SK 别名最简洁,无需指定目标 API URL:

508 |
509 |
# Claude Code / Anthropic SDK
 510 | export ANTHROPIC_BASE_URL=
 511 | export ANTHROPIC_AUTH_TOKEN=sk-ar-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
 512 | 
 513 | # OpenAI SDK
 514 | export OPENAI_BASE_URL=/v1
 515 | export OPENAI_API_KEY=sk-ar-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
516 | 519 |
520 |

在管理面板的配置列表中点击「生成」按钮获取你的 SK 别名

521 |
522 | 523 | 524 |
525 |

Claude Code CLI(Key ID 模式)

526 |
527 |
export ANTHROPIC_BASE_URL=
 528 | export ANTHROPIC_AUTH_TOKEN=https://api.anthropic.com:b4y8m1
529 | 532 |
533 |
534 | 535 | 536 |
537 |

OpenAI CLI / SDK(Key ID 模式)

538 |
539 |
export OPENAI_BASE_URL=/v1
 540 | export OPENAI_API_KEY=https://api.openai.com:a3x9k2
541 | 544 |
545 |
546 | 547 | 548 |
549 |

通用配置模式

550 |
551 |
# SK 别名模式(最简洁)
 552 | export {SDK}_BASE_URL=
 553 | export {SDK}_API_KEY=sk-ar-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
 554 | 
 555 | # Key ID 模式
 556 | export {SDK}_BASE_URL=
 557 | export {SDK}_API_KEY=https://{目标API地址}:{KeyID}
558 | 561 |
562 |
563 | 564 |
565 |

配置说明

566 |
    567 |
  • SK 别名模式:最简洁,只需一个 sk-ar-xxx 即可,系统自动识别目标 API
  • 568 |
  • Key ID 模式:需要指定 URL 和 6 位 Key ID,适合需要明确指定目标的场景
  • 569 |
  • • 环境变量可以添加到 ~/.bashrc~/.zshrc 或项目的 .env 文件
  • 570 |
571 |
572 |
573 | 574 | 575 |
576 |

错误处理

577 |

当请求出错时,API 会返回结构化的错误信息:

578 | 579 |
580 |
{
 581 |   "error": {
 582 |     "code": "NOT_FOUND",
 583 |     "message": "Key ID 不存在",
 584 |     "hint": "找不到 Key ID \\"abc123\\",请检查是否输入正确",
 585 |     "contact": "如有疑问请联系管理员"
 586 |   }
 587 | }
588 |
589 | 590 |
591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 |
错误码HTTP 状态说明
UNAUTHORIZED401缺少或无效的 Authorization header
BAD_REQUEST400请求格式错误
NOT_FOUND404API 地址未配置或 Key ID 不存在
FORBIDDEN403Key 已被禁用
SERVICE_ERROR503无法连接到目标 API
627 |
628 |
629 | 630 | 631 |
632 |

部署指南

633 |

选择以下任一方式部署你的 AnyRouter 代理服务:

634 | 635 | 636 |
637 |
638 | 641 | 644 | 647 | 650 | 653 |
654 | 655 | 656 |
657 |
658 |

最简单的方式

659 |

点击下方按钮,自动 Fork 并部署到你的 Cloudflare 账户:

660 | 661 | Deploy to Cloudflare Workers 662 | 663 |
664 |
665 |

部署后配置环境变量:

666 |
    667 |
  1. 进入 Cloudflare Dashboard → Workers & Pages → 你的 Worker
  2. 668 |
  3. 点击 Settings → Variables and Secrets
  4. 669 |
  5. 添加 ADMIN_PASSWORDSUPABASE_URL 等变量
  6. 670 |
671 |
672 |
673 | 674 | 675 | 707 | 708 | 709 | 757 | 758 | 759 | 781 | 782 | 783 | 828 |
829 | 830 | 831 |
832 |

833 | 准备工作 834 |

835 |
836 |
    837 |
  • GitHub 账号(用于 Fork 代码仓库)
  • 838 |
  • Cloudflare 账号(免费注册
  • 839 |
  • Supabase 账号(免费注册,可选,用于密钥管理)
  • 840 |
  • Upstash 账号(免费注册,可选,用于 Redis 缓存和统计)
  • 841 |
842 |
843 |
844 | 845 | 846 |
847 |

848 | 3 849 | 配置 Supabase 数据库(可选) 850 |

851 |
852 |

如果只需要直传模式,可跳过此步骤

853 |
854 |
    855 |
  1. 856 | a. 857 | 登录 Supabase 并创建新项目 858 |
  2. 859 |
  3. 860 | b. 861 | 进入 SQL Editor,执行数据库初始化脚本: 862 |
  4. 863 |
864 |
865 |
866 |
867 | 868 | schema.sql - 从 GitHub 实时获取 869 |
870 |
871 | 查看源文件 872 | 873 | 874 |
875 |
876 | 881 |

脚本包含:建表、索引、RLS 策略、触发器、迁移逻辑(支持已有表升级)

882 |
883 |
    884 |
  1. 885 | c. 886 | 进入 Settings → API,获取 Project URLanon/public key 887 |
  2. 888 |
889 |
890 | 891 | 892 |
893 |

894 | 4 895 | 配置 Upstash Redis(可选) 896 |

897 |
898 |

如果不需要统计和缓存功能,可跳过此步骤

899 |
900 |
    901 |
  1. 902 | a. 903 | 登录 Upstash 并创建 Redis 数据库 904 |
  2. 905 |
  3. 906 | b. 907 | 选择离你最近的区域(如 US-East-1 或 AP-Northeast-1) 908 |
  4. 909 |
  5. 910 | c. 911 | 在 REST API 标签页复制 UPSTASH_REDIS_REST_URLUPSTASH_REDIS_REST_TOKEN 912 |
  6. 913 |
914 |
915 | 916 | 917 |
918 |

919 | 5 920 | 配置环境变量 921 |

922 |

部署后在 Cloudflare Dashboard → Workers → 你的 Worker → Settings → Variables and Secrets 添加:

923 |
924 | 925 | 926 | 927 | 928 | 929 | 930 | 931 | 932 | 933 | 934 | 935 | 936 | 937 | 938 | 939 | 940 | 941 | 942 | 943 | 944 | 945 | 946 | 947 | 948 | 949 | 950 | 951 | 952 | 953 | 954 | 955 | 956 | 957 | 958 | 959 | 960 | 961 | 962 | 963 | 964 | 965 |
变量名必须说明获取方式
ADMIN_PASSWORD管理面板登录密码自定义
SUPABASE_URL可选Supabase 项目 URLSupabase → Settings → API → Project URL
SUPABASE_KEY可选Supabase anon keySupabase → Settings → API → anon public
UPSTASH_REDIS_URL可选Upstash Redis REST URLUpstash → Redis → REST API
UPSTASH_REDIS_TOKEN可选Upstash Redis TokenUpstash → Redis → REST API
966 |
967 |
968 |

不配置 Supabase/Redis 也可使用直传模式

969 |
970 |
971 | 972 | 973 |
974 |

975 | 6 976 | 配置自定义域名(可选) 977 |

978 |
    979 |
  1. 980 | a. 981 | 登录 Cloudflare Dashboard,进入 Workers & Pages 982 |
  2. 983 |
  3. 984 | b. 985 | 选择你的 Worker,点击 Settings → Triggers → Custom Domains 986 |
  4. 987 |
  5. 988 | c. 989 | 添加你的域名(域名需要已添加到 Cloudflare) 990 |
  6. 991 |
992 |
993 | 994 | 995 |
996 |

部署后检查清单

997 |
    998 |
  • 访问 / 查看状态页面
  • 999 |
  • 访问 /admin 登录管理面板
  • 1000 |
  • 添加 API 配置并测试代理功能
  • 1001 |
  • 生成 SK 别名用于 SDK 配置
  • 1002 |
1003 |
1004 |
1005 | 1006 | 1007 |
1008 |

常见问题

1009 | 1010 |
1011 |
1012 |

Q: 如何获取 Key ID?

1013 |

登录管理面板,添加 API 配置后系统会自动生成 6 位 Key ID。

1014 |
1015 |
1016 |

Q: 支持哪些 API?

1017 |

支持任意 HTTP/HTTPS API,包括但不限于:OpenAI、Anthropic、Google AI、Azure OpenAI、Groq、Mistral、Cohere、HuggingFace 等。

1018 |
1019 |
1020 |

Q: 数据安全吗?

1021 |

代理服务不会存储任何请求内容,仅转发请求。API Token 存储在数据库中,传输使用 HTTPS 加密。

1022 |
1023 |
1024 |

Q: 如何自己部署?

1025 |

Fork GitHub 仓库,配置 Cloudflare Workers 和 Supabase 数据库即可。详见仓库 README。

1026 |
1027 |
1028 |

Q: 有请求限制吗?

1029 |

代理服务本身无限制,但会受到 Cloudflare Workers 免费版的限制(每日 10 万请求)和目标 API 的限制。

1030 |
1031 |
1032 |

Q: 为什么要用代理而不是直连?

1033 |

1) 统一管理多个 API 密钥;2) 避免在客户端暴露 Token;3) 利用 Cloudflare 边缘网络加速;4) 便于监控和统计使用量。

1034 |
1035 |
1036 |
1037 | 1038 | 1039 |
1040 |

Made with by dext7r

1041 |

Powered by Cloudflare Workers

1042 |
1043 |
1044 |
1045 |
1046 | 1047 | 1208 | 1209 | ` 1210 | } 1211 | --------------------------------------------------------------------------------