ohmygpt-logoOhMyGPT Cookbook

自建Cloudflare Worker反代

简介

本文主要说明如何自建一个基于Cloudflare Worker的API反代服务,实现API原始数据记录+自定义模型重写功能。

由于偶尔会有部分用户想自定义模型名称以便兼容某些行为奇怪的客户端,有些用户想要获得API交互的原始数据以排查错误,但是我们出于兼容性和保护客户隐私的方面考虑,不太愿意在服务端直接对这些功能添加支持,因此我们设计了一个基于CF Worker的小脚本简单实现这些功能的同时,兼顾安全性和隐私。

效果展示:

API模型重写claude-3-haiku=>claude-3-5-haiku,claude-3.5-haiku-8080=>claude-3-5-sonnet

image.png

API交互原始数据记录:

image.png

配置

创建Worker

进入您的Cloudflare Dashboard控制面板

点击Worker & Pages => Overveiw => Create 创建新的Worker

image.png

image.png

名字随意,点击创建即可

image.png

编辑Worker代码

创建成功后点击编辑代码:

image.png

将原有代码清空,然后把下面整段代码全部复制进去,然后点击Deploy保存并更新部署

/**
 * API 代理与日志记录服务
 *
 * 环境变量配置:
 * - API_LOGGING: 设置为 "enabled" 时启用日志记录
 * - API_SEC_KEY: API Token,用于访问管理接口
 * - API_MODEL_REWRITE: 模型重写规则,格式: "model1=>model2,model3=>model4"
 *
 * 数据库配置:
 * - 需要在 Cloudflare D1 中创建数据库并绑定为 "DB"
 */
 
// 错误消息模板
const ERROR_MESSAGES = {
  DB_NOT_CONFIGURED: {
    en: "D1 database is not properly configured. Please check the documentation for setup instructions.",
    zh: "D1 数据库未正确配置,请查阅文档进行配置。"
  },
  DB_TABLE_MISSING: {
    en: "Required database table 'api_logs' is missing. Please create the table first.",
    zh: "数据库表 'api_logs' 不存在,请先创建数据表。"
  },
  INVALID_MODEL_REWRITE: {
    en: "Invalid model rewrite configuration. Format should be 'model1=>model2,model3=>model4'",
    zh: "模型重写配置格式错误,正确格式应为:'model1=>model2,model3=>model4'"
  }
};
 
// 创建错误响应
function createErrorResponse(errorKey) {
  const error = ERROR_MESSAGES[errorKey];
  return new Response(JSON.stringify({
    error: {
      en: error.en,
      zh: error.zh
    }
  }), {
    status: 500,
    headers: { 'Content-Type': 'application/json' }
  });
}
 
// 解析模型重写规则
function parseModelRewrites(rewriteConfig) {
  if (!rewriteConfig) return null;
 
  const rewrites = new Map();
  try {
    rewriteConfig.split(',').forEach(rule => {
      const [from, to] = rule.trim().split('=>');
      if (!from || !to) throw new Error('Invalid rule format');
      rewrites.set(from.trim(), to.trim());
    });
    return rewrites;
  } catch (error) {
    throw new Error(ERROR_MESSAGES.INVALID_MODEL_REWRITE.en);
  }
}
 
export default {
  async fetch(request, env) {
    const url = new URL(request.url);
 
    // 处理管理接口
    if (url.pathname === '/logs' || url.pathname === '/clear-logs') {
      return await handleAdminRequest(request, env, url);
    }
 
    // API 反向代理
    if (url.pathname.startsWith('/v1')) {
      return await handleProxyRequest(request, env, url);
    }
 
    return new Response('Not Found', { status: 404 });
  }
};
 
async function handleProxyRequest(request, env, url) {
  // 克隆请求信息,用于记录和模型重写
  const timestamp = new Date().toISOString();
  const requestHeaders = Object.fromEntries([...request.headers]);
  let requestBody = '';
  let modifiedRequest = request;
 
  // 处理请求体和模型重写
  if (request.method === 'POST' && request.body) {
    const clonedRequest = request.clone();
    requestBody = await clonedRequest.text();
 
    // 模型重写处理
    if (env.API_MODEL_REWRITE) {
      try {
        const bodyJson = JSON.parse(requestBody);
        const rewrites = parseModelRewrites(env.API_MODEL_REWRITE);
 
        if (bodyJson.model && rewrites?.has(bodyJson.model)) {
          const newModel = rewrites.get(bodyJson.model);
          bodyJson.model = newModel;
          requestHeaders['x-model-rewrite'] = `${bodyJson.model}=>${newModel}`;
          requestBody = JSON.stringify(bodyJson);
 
          // 创建新的请求对象
          modifiedRequest = new Request(request.url, {
            method: request.method,
            headers: request.headers,
            body: requestBody
          });
        }
      } catch (error) {
        return createErrorResponse('INVALID_MODEL_REWRITE');
      }
    }
  }
 
  // 执行反向代理请求
  url.host = 'api.ohmygpt.com';
  const response = await fetch(url, {
    method: modifiedRequest.method,
    headers: modifiedRequest.headers,
    body: modifiedRequest.body
  });
 
  // 如果启用了日志记录,则记录请求和响应
  if (env.API_LOGGING === 'enabled') {
    if (!env.DB) {
      console.error('Database not configured but logging is enabled');
      return createErrorResponse('DB_NOT_CONFIGURED');
    }
 
    try {
      const clonedResponse = response.clone();
      const responseBody = await clonedResponse.text();
      const responseHeaders = JSON.stringify(Object.fromEntries([...response.headers]));
 
      await env.DB.prepare(`
        INSERT INTO api_logs (
          timestamp, request_path, request_method, request_headers,
          request_body, response_status, response_headers, response_body
        ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
      `).bind(
        timestamp,
        url.pathname,
        request.method,
        JSON.stringify(requestHeaders),
        requestBody,
        response.status,
        responseHeaders,
        responseBody
      ).run();
    } catch (error) {
      console.error('Failed to log request:', error);
      return createErrorResponse('DB_TABLE_MISSING');
    }
  }
 
  return response;
}
 
async function handleAdminRequest(request, env, url) {
  const key = url.searchParams.get('key');
 
  // 验证 API Token
  if (!env.API_SEC_KEY || key !== env.API_SEC_KEY) {
    return new Response('Unauthorized', { status: 401 });
  }
 
  // 检查数据库配置
  if (!env.DB) {
    return createErrorResponse('DB_NOT_CONFIGURED');
  }
 
  // 处理清理请求
  if (url.pathname === '/clear-logs') {
    try {
      await env.DB.prepare('DELETE FROM api_logs').run();
      return new Response(JSON.stringify({
        success: true,
        message: {
          en: "Logs cleared successfully",
          zh: "日志已成功清除"
        }
      }), {
        headers: { 'Content-Type': 'application/json' }
      });
    } catch (error) {
      return createErrorResponse('DB_TABLE_MISSING');
    }
  }
 
  // 处理日志导出
  try {
    const { results } = await env.DB.prepare(
      'SELECT * FROM api_logs ORDER BY timestamp DESC'
    ).all();
 
    const format = url.searchParams.get('format') || 'csv';
 
    if (format === 'json') {
      return new Response(JSON.stringify(results, null, 2), {
        headers: { 'Content-Type': 'application/json' }
      });
    }
 
    // 默认导出 CSV
    const csv = [
      ['Timestamp', 'Path', 'Method', 'Request Headers', 'Request Body',
       'Response Status', 'Response Headers', 'Response Body'].join(',')
    ];
 
    for (const row of results) {
      csv.push([
        row.timestamp,
        row.request_path,
        row.request_method,
        `"${row.request_headers.replace(/"/g, '""')}"`,
        `"${row.request_body.replace(/"/g, '""')}"`,
        row.response_status,
        `"${row.response_headers.replace(/"/g, '""')}"`,
        `"${row.response_body.replace(/"/g, '""')}"`
      ].join(','));
    }
 
    return new Response(csv.join('\n'), {
      headers: {
        'Content-Type': 'text/csv',
        'Content-Disposition': 'attachment; filename=api_logs.csv'
      }
    });
  } catch (error) {
    return createErrorResponse('DB_TABLE_MISSING');
  }
}

image.png

创建并初始化D1数据库

Worker & Pages => D1 => Create按钮创建新的数据库

image.png

名称随意,输入完后点击创建即可

image.png

初始化数据库,执行建表语句:

CREATE TABLE IF NOT EXISTS api_logs (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    timestamp TEXT,
    request_path TEXT,
    request_method TEXT,
    request_headers TEXT,
    request_body TEXT,
    response_status INTEGER,
    response_headers TEXT,
    response_body TEXT,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

将上面这段SQL复制到蓝框中执行

image.png

这样就算成功:

image.png

配置Worker

绑定D1数据库

找到刚刚创建的Worker

image.png

绑定D1数据库:

image.png

image.png

变量名这里填写DB,数据库就选择刚刚初始化的那个,点击Deploy部署即可

image.png

配置Worker环境变量

默认情况下,此时此Worker已经可以转发API了,但是不会进行任何行为

可以在这里设置Worker的环境变量

image.png

image.png

如果需要启用日志记录:

新增环境变量: API_LOGGING 将其设置为 enabled

如果需要使用日志下载接口,需要配置API密钥:

环境变量: API_SEC_KEY 设置为一个只有您知道的私密密钥

如果您需要模型重写功能:

设置环境变量: API_MODEL_REWRITE 格式: model1=>model2,model3=>model4

开始请求

假设您的Worker地址是: wandering-poetry-3106.hash070.workers.dev

那么您如果想要访问OpenAI的Chat.Completions API接口,可以对这个接口发起请求 https://wandering-poetry-3106.hash070.workers.dev/v1/chat/completions , 如果想要访问Messages API接口,可以对 https://wandering-poetry-3106.hash070.workers.dev/v1/messages 发起请求

image.png

如果您需要下载日志:

可以在浏览器中打开形如下面这个URL(注意地址和Key按照这个格式替换成你自己搭建的

https://[你的Worker地址]/logs?key=[你的API_SEC_KEY]&format=[导出数据格式]

例如:
https://wandering-poetry-3106.hash070.workers.dev/logs?key=your-secret-key&format=csv

其中URL中的这个key参数应当填写为您设置的API_SEC_KEY变量,format指导出数据格式,可以是jsoncsv表格

如果您需要清除数据:

可以在浏览器中直接访问这个URL

https://[你的Worker地址]/clear-logs?key=[你的API_SEC_KEY]
{"success":true,"message":{"en":"Logs cleared successfully","zh":"日志已成功清除"}}

附:

API 接口说明

日志导出

地址:/logs
方法:GET
参数:

key:API_SEC_KEY(必填)
format:导出格式(可选,默认 csv)

csv:CSV 格式
json:JSON 格式

示例:

/logs?key=your-api-key
/logs?key=your-api-key&format=json

日志清理

地址:/clear-logs
方法:GET
参数:

key:API_SEC_KEY(必填)

示例:

/clear-logs?key=your-api-key

目录