我是实践了 Cloudflare
反代 GCP API
的方法
首先是学习了各个大佬的教程:
-
大佬这两个帖子参考如何申请免费额度并用GCP CLI获取密钥
-
最后的CF反代代码,参考了各个大佬,感觉这个大佬的最好用:
https://linux.do/t/topic/124714
因为添加了界面管理更方便。
然后部署CF代码,但我测试发现部署在Vercel的 NextChat 会有跨域的错误:
Access to fetch at 'https://xxx' from origin 'https://app.nextchat.dev' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
然后反复进行了修改,添加了跨域支持才能正常使用
(PS:后面检查代码发现原来是大佬忘记添加 handleOptions()
函数的实现了 )
另外,自定义接口我以为端点域名后要添加 /v1/messages
其实不需要,NextChat
默认会给端点添加。(PS:这里我调试了半天,幸亏克劳德帮我发现原因)
CF反代代码(修复了跨域BUG)
// 设定 API Keys 数组
const TOKEN_URL = 'https://www.googleapis.com/oauth2/v4/token';
let tokenCache = {};
// 设置随机账号密钥
let randomkay = 'sk-xxx';
// 设置管理员的密码
let correctPassword = 'xxxx';
// MODELS 数组仍然需要保留,因为它定义了允许使用的模型列表
const MODELS = ['claude-3-sonnet', 'claude-3-5-sonnet', 'claude-3-opus', 'claude-3-haiku'];
let ACCOUNTS = {}; // 全局变量存储 ACCOUNTS
let MODEL_CONFIG = {}; // 全局变量存储 ACCOUNTS
let API_KEY_ACCOUNT_MAPPING = {}; // 全局变量存储 ACCOUNTS
let ATS = {};
async function getAccessToken(account, ACCOUNTS) {
let ATS = JSON.parse(await gcp_project_id.get('ats') || '{}');
const now = Date.now() / 1000;
if (ATS[account] && ATS[account].accessToken && now < ATS[account].expiry - 120) {
return ATS[account].accessToken;
}
try {
const accountConfig = ACCOUNTS[account];
const response = await fetch(TOKEN_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
client_id: accountConfig.CLIENT_ID,
client_secret: accountConfig.CLIENT_SECRET,
refresh_token: accountConfig.REFRESH_TOKEN,
grant_type: 'refresh_token'
})
});
if (response.status === 401) {
await removeAccount(account);
throw new Error(`Account ${account} removed due to unauthorized error.`);
}
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (data.error) {
throw new Error(data.error_description || data.error);
}
ATS[account] = {
accessToken: data.access_token,
expiry: now + data.expires_in
};
await gcp_project_id.put('ats', JSON.stringify(ATS));
return ATS[account].accessToken;
} catch (error) {
throw error;
}
}
async function removeAccount(account) {
try {
let accounts = JSON.parse(await gcp_project_id.get('account') || '{}');
let keys = JSON.parse(await gcp_project_id.get('keys') || '{}');
let model_config = JSON.parse(await gcp_project_id.get('model_config') || '{}');
let ATS = JSON.parse(await gcp_project_id.get('ats') || '{}');
// Remove from accounts
delete accounts[account];
// Remove from keys
for (let key in keys) {
if (keys[key] === account) {
delete keys[key];
}
}
// Remove from model_config
Object.keys(model_config).forEach(model => {
const index = model_config[model].accounts.indexOf(account);
if (index > -1) {
model_config[model].accounts.splice(index, 1);
}
});
// Remove from ATS
delete ATS[account];
// Save updated data
await gcp_project_id.put('account', JSON.stringify(accounts));
await gcp_project_id.put('keys', JSON.stringify(keys));
await gcp_project_id.put('model_config', JSON.stringify(model_config));
await gcp_project_id.put('ats', JSON.stringify(ATS));
} catch (error) {
throw error;
}
}
function getLocation(model, MODEL_CONFIG) {
const config = MODEL_CONFIG[model];
const locations = config.locations;
const randomIndex = Math.floor(Math.random() * locations.length);
return locations[randomIndex];
}
function constructApiUrl(location, model, account, ACCOUNTS) {
const accountConfig = ACCOUNTS[account];
return `https://${location}-aiplatform.googleapis.com/v1/projects/${accountConfig.PROJECT_ID}/locations/${location}/publishers/anthropic/models/${model}:streamRawPredict`;
}
async function handleApiRequest(request) {
API_KEY_ACCOUNT_MAPPING = JSON.parse(await gcp_project_id.get('keys') || '{}');
ACCOUNTS = JSON.parse(await gcp_project_id.get('account') || '{}');
MODEL_CONFIG = JSON.parse(await gcp_project_id.get('model_config') || '{}');
const apiKey = request.headers.get('x-api-key');
if (!apiKey || (apiKey !== 'sk-freegcpclaude' && !API_KEY_ACCOUNT_MAPPING[apiKey])) {
console.warn(`API key "${apiKey}" not found in API_KEY`);
const errorResponse = new Response(JSON.stringify({
type: "error",
error: {
type: "permission_error",
message: "Your API key does not have permission to use the specified resource."
}
}), {
status: 403,
headers: {
'Content-Type': 'application/json'
}
});
return errorResponse;
}
let requestBody = await request.json();
const modelWithVersion = requestBody.model;
const baseModel = modelWithVersion.replace(/-\d{8}$/, '');
let model;
if (MODELS.includes(baseModel)) {
if (modelWithVersion.match(/^\w+-\d{8}$/)) {
model = modelWithVersion.replace(/-(\d{8})$/, '@$1');
} else {
model = baseModel;
}
} else {
return new Response(JSON.stringify({
type: "error",
error: {
type: "invalid_model",
message: "The specified model is not in the allowed list."
}
}), {
status: 400,
headers: {
'Content-Type': 'application/json'
}
});
}
const location = getLocation(model, MODEL_CONFIG);
let account = API_KEY_ACCOUNT_MAPPING[apiKey];
// 如果 apiKey 为公共的,则随机选择一个账户
if (apiKey === randomkay) {
const shareableAccounts = Object.keys(ACCOUNTS).filter(key => ACCOUNTS[key].SHAREABLE);
if (shareableAccounts.length === 0) {
return new Response(JSON.stringify({
type: "error",
error: {
type: "no_shareable_account",
message: "没有可用的共享账号。"
}
}), {
status: 400,
headers: {
'Content-Type': 'application/json'
}
});
}
account = shareableAccounts[Math.floor(Math.random() * shareableAccounts.length)];
}
const accessToken = await getAccessToken(account, ACCOUNTS);
const apiUrl = constructApiUrl(location, model, account, ACCOUNTS);
if (requestBody.anthropic_version) {
delete requestBody.anthropic_version;
}
if (requestBody.model) {
delete requestBody.model;
}
requestBody.anthropic_version = "vertex-2023-10-16";
const modifiedHeaders = new Headers(request.headers);
modifiedHeaders.set('Authorization', `Bearer ${accessToken}`);
modifiedHeaders.set('Content-Type', 'application/json; charset=utf-8');
modifiedHeaders.delete('anthropic-version');
const modifiedRequest = new Request(apiUrl, {
headers: modifiedHeaders,
method: request.method,
body: JSON.stringify(requestBody),
redirect: 'follow'
});
const response = await fetch(modifiedRequest);
const modifiedResponse = new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers: response.headers
});
return modifiedResponse;
}
async function handleTokenRequest(request) {
if (request.method === 'POST') {
try {
let ACCOUNTS = JSON.parse(await gcp_project_id.get('account') || '{}');
let keys = JSON.parse(await gcp_project_id.get('keys') || '{}');
let model_config = JSON.parse(await gcp_project_id.get('model_config') || '{}');
let ATS = JSON.parse(await gcp_project_id.get('ats') || '{}');
const requestBody = await request.json();
const { name, projectId, clientId, clientSecret, refreshToken, apiKey, shareable } = requestBody;
if (!name || !projectId || !clientId || !clientSecret || !refreshToken || !apiKey) {
return new Response(JSON.stringify({ message: '所有字段均为必填项。' }), {
headers: { 'Content-Type': 'application/json' },
status: 400,
});
}
if (ACCOUNTS[name]) {
return new Response(JSON.stringify({ message: '名称已存在。' }), {
headers: { 'Content-Type': 'application/json' },
status: 400,
});
}
if (keys[apiKey]) {
return new Response(JSON.stringify({ message: 'API 密钥已存在。' }), {
headers: { 'Content-Type': 'application/json' },
status: 400,
});
}
// 尝试获取 AccessToken
try {
const tokenResponse = await fetch(TOKEN_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
client_id: clientId,
client_secret: clientSecret,
refresh_token: refreshToken,
grant_type: 'refresh_token'
})
});
if (tokenResponse.status === 401) {
return new Response(JSON.stringify({ message: '获取AccessToken失败: 未授权(401)。请检查您的凭据。' }), {
headers: { 'Content-Type': 'application/json' },
status: 400,
});
}
const tokenData = await tokenResponse.json();
if (tokenData.error) {
throw new Error(tokenData.error_description || tokenData.error);
}
// AccessToken 获取成功,保存账号信息
ACCOUNTS[name] = {
"PROJECT_ID": projectId,
"CLIENT_ID": clientId,
"CLIENT_SECRET": clientSecret,
"REFRESH_TOKEN": refreshToken,
"SHAREABLE": shareable || false
};
// 保存 AccessToken 到 ATS
const now = Date.now() / 1000;
ATS[name] = {
accessToken: tokenData.access_token,
expiry: now + tokenData.expires_in,
refreshPromise: null
};
keys[apiKey] = name;
// 更新 model_config
Object.keys(model_config).forEach(model => {
if (!model_config[model].accounts.includes(name)) {
model_config[model].accounts.push(name);
}
});
// 保存更新后的数据到 KV 存储
await gcp_project_id.put('account', JSON.stringify(ACCOUNTS));
await gcp_project_id.put('keys', JSON.stringify(keys));
await gcp_project_id.put('model_config', JSON.stringify(model_config));
await gcp_project_id.put('ats', JSON.stringify(ATS));
return new Response(JSON.stringify({ message: '数据添加成功。' }), {
headers: { 'Content-Type': 'application/json' },
status: 200,
});
} catch (tokenError) {
return new Response(JSON.stringify({ message: `获取AccessToken失败: ${tokenError.message}` }), {
headers: { 'Content-Type': 'application/json' },
status: 400,
});
}
} catch (error) {
return new Response(JSON.stringify({ message: '添加数据时出错。', error: error.message }), {
headers: { 'Content-Type': 'application/json' },
status: 500,
});
}
} else if (request.method === 'GET') {
return new Response(`
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GCP</title>
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<h1 class="text-center">添加GCP账号</h1>
<form id="configForm" class="mt-4">
<div class="form-group">
<label for="name">名称(唯一):</label>
<input type="text" class="form-control" id="name" name="name" required>
</div>
<div class="form-group">
<label for="projectId">PROJECT_ID:</label>
<input type="text" class="form-control" id="projectId" name="projectId" required>
</div>
<div class="form-group">
<label for="clientId">CLIENT_ID:</label>
<input type="text" class="form-control" id="clientId" name="clientId" required>
</div>
<div class="form-group">
<label for="clientSecret">CLIENT_SECRET:</label>
<input type="text" class="form-control" id="clientSecret" name="clientSecret" required>
</div>
<div class="form-group">
<label for="refreshToken">REFRESH_TOKEN:</label>
<input type="text" class="form-control" id="refreshToken" name="refreshToken" required>
</div>
<div class="form-group">
<label for="apiKey">API_KEY(调用使用):</label>
<input type="text" class="form-control" id="apiKey" name="apiKey" required>
</div>
<div class="form-group form-check">
<input type="checkbox" class="form-check-input" id="shareable" name="shareable">
<label class="form-check-label" for="shareable">是否共享</label>
</div>
<button type="submit" class="btn btn-primary btn-block">提交</button>
</form>
<div id="message" class="mt-3 text-center"></div>
</div>
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@popperjs/[email protected]/dist/umd/popper.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
<script>
document.getElementById('configForm').addEventListener('submit', async function(event) {
event.preventDefault();
const formData = {
name: document.getElementById('name').value,
projectId: document.getElementById('projectId').value,
clientId: document.getElementById('clientId').value,
clientSecret: document.getElementById('clientSecret').value,
refreshToken: document.getElementById('refreshToken').value,
apiKey: document.getElementById('apiKey').value,
shareable: document.getElementById('shareable').checked
};
const response = await fetch('/token', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
});
const result = await response.json();
const messageDiv = document.getElementById('message');
messageDiv.innerText = result.message;
if (response.status === 200) {
messageDiv.classList.remove('text-danger');
messageDiv.classList.add('text-success');
} else {
messageDiv.classList.remove('text-success');
messageDiv.classList.add('text-danger');
}
});
</script>
</body>
</html>
`, {
headers: {
'Content-Type': 'text/html'
}
});
} else {
return new Response('Method Not Allowed', { status: 405 });
}
}
async function handleAdminRequest(request) {
const url = new URL(request.url);
const params = new URLSearchParams(url.search);
const password = params.get('password');
if (request.method === 'GET') {
if (password !== correctPassword) {
return new Response(`
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>管理员登录</title>
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<h1 class="text-center">管理员登录</h1>
<form id="loginForm" class="mt-4">
<div class="form-group">
<label for="password">密码:</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<button type="submit" class="btn btn-primary btn-block">登录</button>
</form>
<div id="message" class="mt-3 text-center text-danger"></div>
</div>
<script>
document.getElementById('loginForm').addEventListener('submit', function(event) {
event.preventDefault();
const password = document.getElementById('password').value;
window.location.href = '/admin?password=' + encodeURIComponent(password);
});
${password ? "document.getElementById('message').textContent = '密码错误,请重试。';" : ""}
</script>
</body>
</html>
`, {
headers: { 'Content-Type': 'text/html' }
});
}
// 如果密码正确,显示账户管理页面
let ACCOUNTS = JSON.parse(await gcp_project_id.get('account') || '{}');
let accountsHtml = '';
for (let [name, account] of Object.entries(ACCOUNTS)) {
accountsHtml += `
<tr>
<td>${name}</td>
<td>
<input type="checkbox" class="shareableCheckbox" data-name="${name}" ${account.SHAREABLE ? 'checked' : ''}>
</td>
<td>
<button class="btn btn-danger btn-sm deleteBtn" data-name="${name}">删除</button>
</td>
</tr>
`;
}
return new Response(`
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>账户管理</title>
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet">
<style>
#notification {
position: fixed;
top: 20px;
right: 20px;
padding: 15px;
background-color: #28a745;
color: white;
border-radius: 5px;
display: none;
}
</style>
</head>
<body>
<div class="container mt-5">
<h1 class="text-center">账户管理</h1>
<table class="table mt-4">
<thead>
<tr>
<th>名称</th>
<th>可共享</th>
<th>操作</th>
</tr>
</thead>
<tbody>
${accountsHtml}
</tbody>
</table>
</div>
<div id="notification"></div>
<script>
function showNotification(message) {
const notification = document.getElementById('notification');
notification.textContent = message;
notification.style.display = 'block';
setTimeout(() => {
notification.style.display = 'none';
}, 3000);
}
document.querySelectorAll('.shareableCheckbox').forEach(checkbox => {
checkbox.addEventListener('change', async function() {
const name = this.dataset.name;
const shareable = this.checked;
const response = await fetch('/admin', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'updateShareable', name, shareable, password: '${correctPassword}' })
});
const result = await response.json();
showNotification(result.message);
});
});
document.querySelectorAll('.deleteBtn').forEach(btn => {
btn.addEventListener('click', async function() {
if (confirm('您确定要删除这个账户吗?')) {
const name = this.dataset.name;
const response = await fetch('/admin', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'deleteAccount', name, password: '${correctPassword}' })
});
const result = await response.json();
showNotification(result.message);
if (result.success) {
this.closest('tr').remove();
}
}
});
});
</script>
</body>
</html>
`, {
headers: { 'Content-Type': 'text/html' }
});
} else if (request.method === 'POST') {
const { action, name, shareable, password } = await request.json();
if (password !== correctPassword) {
return new Response(JSON.stringify({ success: false, message: '密码无效' }), {
headers: { 'Content-Type': 'application/json' },
status: 403
});
}
if (action === 'updateShareable') {
let ACCOUNTS = JSON.parse(await gcp_project_id.get('account') || '{}');
if (ACCOUNTS[name]) {
ACCOUNTS[name].SHAREABLE = shareable;
await gcp_project_id.put('account', JSON.stringify(ACCOUNTS));
return new Response(JSON.stringify({ success: true, message: '账户更新成功' }), {
headers: { 'Content-Type': 'application/json' }
});
}
} else if (action === 'deleteAccount') {
await removeAccount(name);
return new Response(JSON.stringify({ success: true, message: '账户删除成功' }), {
headers: { 'Content-Type': 'application/json' }
});
}
return new Response(JSON.stringify({ success: false, message: '无效的操作' }), {
headers: { 'Content-Type': 'application/json' },
status: 400
});
}
}
/* 准备响应,追加跨域许可头 */
function prepareResponse(response) {
const modifiedResponse = new Response(response.body, response);
modifiedResponse.headers.set('Access-Control-Allow-Origin', '*');
modifiedResponse.headers.set('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
modifiedResponse.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization, x-api-key, anthropic-version, model');
return modifiedResponse;
}
/* 处理OPTIONS请求
* 预取时添加 Access-Control-Allow 头,避免出现跨域问题
*/
function handleOptions() {
return new Response(null, {
status: 204,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization, x-api-key, anthropic-version, model'
}
});
}
async function handleRequest(request) {
// 处理 CORS 预检请求
if (request.method === 'OPTIONS') {
return handleOptions();
}
const url = new URL(request.url);
if (url.pathname === '/v1/messages') {
let response = await handleApiRequest(request);
return prepareResponse(response);
} else if (url.pathname === '/admin') {
return await handleAdminRequest(request);
} else {
return await handleTokenRequest(request);
}
}
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request));
});
最后,很多大佬都提到可以直接用 One-API, 确实更方便 ,不用自己折腾了。
找到一个帖子无需服务器就可以搭建,可以尝试一下(之前我都不知道 )
https://linux.do/t/topic/56649