[教程][关于api转了几手]一个可部署于workers的私人api中转溯源工具

update:优化好了,决定使用d1存储

和claude互相折磨了一天,终于糊出来了个差不多能用的

参考的这个帖子的佬的源码:来看看你使用的api转了几手,是官转、azure还是逆向,还是掺假的!

部署要创建并绑定一个D1数据库

配置列如下,名称应填为:requests

const CONFIG = {
  WORKER_DOMAIN: '<填你自己的域名>', // worker 的域名
};

const HTML_CONTENT = `
<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>API 测试工具</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/1.6.2/axios.min.js"></script>
    <link href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css" rel="stylesheet">
    <style>
        body {
            font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
            background-color: #FAF9F6;
        }
        
        .loading-dots:after {
            content: ' .';
            animation: dots 1.5s steps(5, end) infinite;
        }
        
        @keyframes dots {
            0%, 20% { content: ' .' }
            40% { content: ' ..' }
            60% { content: ' ...' }
            80%, 100% { content: ' ....' }
        }
        
        .fade-in {
            animation: fadeIn 0.3s ease-in;
        }
        
        @keyframes fadeIn {
            from { opacity: 0; }
            to { opacity: 1; }
        }

        pre {
            transition: all 0.2s ease;
        }

        input:focus, button:focus {
            outline: none;
            ring-color: #B47464;
        }

        .btn-primary {
            background-color: #B47464;
            transition: background-color 0.2s ease;
        }

        .btn-primary:hover {
            background-color: #9A6557;
        }
    </style>
</head>
<body class="bg-gray-100">
    <div class="container mx-auto px-4 py-12">
        <div class="grid grid-cols-1 lg:grid-cols-2 gap-8 max-w-7xl mx-auto">
            <!-- 左侧面板:表单和日志 -->
            <div class="bg-white rounded-2xl shadow-lg p-8">
                <h1 class="text-4xl font-semibold mb-8 text-center text-gray-800">API 测试工具</h1>
                
                <form id="apiForm" class="space-y-6">
                    <div>
                        <label class="block text-base font-medium text-gray-700 mb-2">API URL</label>
                        <input type="text" id="url" 
                               class="block w-full rounded-xl border-gray-300 shadow-sm p-3 border focus:border-[#B47464] focus:ring-2 focus:ring-[#B47464]" 
                               placeholder="https://api.example.com/v1/chat/completions">
                    </div>
                    
                    <div>
                        <label class="block text-base font-medium text-gray-700 mb-2">API Key</label>
                        <input type="password" id="key" 
                               class="block w-full rounded-xl border-gray-300 shadow-sm p-3 border focus:border-[#B47464] focus:ring-2 focus:ring-[#B47464]">
                    </div>
                    
                    <div>
                        <label class="block text-base font-medium text-gray-700 mb-2">模型</label>
                        <input type="text" id="model" 
                               class="block w-full rounded-xl border-gray-300 shadow-sm p-3 border focus:border-[#B47464] focus:ring-2 focus:ring-[#B47464]" 
                               value="gpt-4o" placeholder="gpt-4o">
                    </div>
                    
                    <button type="submit" 
                            class="w-full btn-primary text-white rounded-xl py-3 text-lg font-medium focus:outline-none focus:ring-2 focus:ring-[#B47464] focus:ring-offset-2">
                        发送请求
                    </button>
                </form>

                <div class="mt-8 space-y-6">
                    <div class="border-t border-gray-100 pt-6">
                        <h2 class="text-xl font-medium mb-3 text-gray-800">请求状态</h2>
                        <pre id="status" class="bg-gray-50 p-4 rounded-xl overflow-x-auto text-gray-700"></pre>
                    </div>
                    
                    <div class="border-t border-gray-100 pt-6">
                        <h2 class="text-xl font-medium mb-3 text-gray-800">
                            请求日志
                            <span id="logsStatus" class="text-sm font-normal text-[#B47464] loading-dots">正在获取日志</span>
                        </h2>
                        <div id="logsContainer" class="bg-gray-50 p-4 rounded-xl">
                            <pre id="logs" class="overflow-x-auto text-gray-700"></pre>
                        </div>
                    </div>
                </div>
            </div>

            <!-- 右侧面板:响应结果 -->
            <div id="result" class="hidden lg:block bg-white rounded-2xl shadow-lg p-8 h-full sticky top-4">
                <h2 class="text-xl font-medium mb-3 text-gray-800">响应结果</h2>
                <pre id="response" class="bg-gray-50 p-4 rounded-xl overflow-x-auto text-gray-700 h-[calc(100vh-12rem)] overflow-y-auto"></pre>
            </div>
        </div>
    </div>

    <script>
        const form = document.getElementById('apiForm');
        const result = document.getElementById('result');
        const statusEl = document.getElementById('status');
        const logsEl = document.getElementById('logs');
        const logsStatus = document.getElementById('logsStatus');
        const responseEl = document.getElementById('response');

        form.addEventListener('submit', async (e) => {
            e.preventDefault();
            
            result.classList.remove('hidden');
            logsStatus.style.display = 'inline';
            logsEl.textContent = '';
            responseEl.textContent = '';

            const url = document.getElementById('url').value;
            const key = document.getElementById('key').value;
            const model = document.getElementById('model').value;

            try {
                const response = await fetch('/v1/serve', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ url, key, model })
                });

                if (!response.body) {
                    throw new Error('ReadableStream 不可用');
                }

                const reader = response.body.getReader();
                const decoder = new TextDecoder('utf-8');
                let done = false;
                let buffer = '';

                while (!done) {
                    const { value, done: doneReading } = await reader.read();
                    done = doneReading;
                    if (value) {
                        buffer += decoder.decode(value, { stream: true });
                        let lines = buffer.split('\\n');
                        buffer = lines.pop();
                        lines.forEach(line => {
                            if (line.trim() !== '') {
                                try {
                                    const data = JSON.parse(line);
                                    if (data.type === 'status') {
                                        statusEl.textContent = data.message;
                                    } else if (data.type === 'log') {
                                        logsEl.textContent += data.message + '\\n';
                                        logsEl.scrollTop = logsEl.scrollHeight;
                                    } else if (data.type === 'response') {
                                        responseEl.textContent = JSON.stringify(data.message, null, 2);
                                        logsStatus.style.display = 'none';
                                    }
                                } catch (parseError) {
                                    console.error('JSON 解析错误:', parseError, '内容:', line);
                                    statusEl.textContent = '错误: JSON 解析失败';
                                    logsStatus.style.display = 'none';
                                }
                            }
                        });
                    }
                }

                if (buffer.trim() !== '') {
                    try {
                        const data = JSON.parse(buffer);
                        if (data.type === 'status') {
                            statusEl.textContent = data.message;
                        } else if (data.type === 'log') {
                            logsEl.textContent += data.message + '\\n';
                            logsEl.scrollTop = logsEl.scrollHeight;
                        } else if (data.type === 'response') {
                            responseEl.textContent = JSON.stringify(data.message, null, 2);
                            logsStatus.style.display = 'none';
                        }
                    } catch (parseError) {
                        console.error('JSON 解析错误:', parseError, '内容:', buffer);
                        statusEl.textContent = '错误: JSON 解析失败';
                        logsStatus.style.display = 'none';
                    }
                }

            } catch (error) {
                statusEl.textContent = '错误: ' + error.message;
                logsStatus.style.display = 'none';
            }
        });
    </script>
</body>
</html>
`;

addEventListener('fetch', event => {
    event.respondWith(handleRequest(event.request))
})

// 存储请求信息到 D1
async function storeRequest(env, traceId, request) {
    console.log('Storing request:', { traceId, ip: request.headers.get('cf-connecting-ip') });
    try {
        await env.DB.prepare(`
            INSERT INTO requests (trace_id, ip, asn, as_org, city, country, colo, timestamp)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?)
        `).bind(
            traceId,
            request.headers.get('cf-connecting-ip'),
            request.cf.asn,
            request.cf.asOrganization,
            request.cf.city,
            request.cf.country,
            request.cf.colo,
            Date.now()
        ).run();
        console.log('Request stored successfully');
    } catch (error) {
        console.error('Error storing request:', error);
        throw error;  // 重新抛出错误以便追踪
    }
}

// 获取特定 traceId 的所有请求
async function getRequests(env, traceId) {
    console.log('Getting requests for traceId:', traceId);
    try {
        // 添加时间窗口条件,获取最近10秒内的请求
        const result = await env.DB.prepare(`
            SELECT DISTINCT ip, asn, as_org, city, country, colo
            FROM requests
            WHERE trace_id = ?
            AND timestamp >= ?
            ORDER BY timestamp ASC
        `).bind(
            traceId,
            Date.now() - 10000  // 10秒时间窗口
        ).all();
        
        // 添加调试日志
        console.log('SQL result:', result);
        console.log('Found records:', result.results?.length || 0);
        
        if (!result.results || result.results.length === 0) {
            console.log('No records found for traceId:', traceId);
            return [];
        }
        
        return result.results;
    } catch (error) {
        console.error('Error getting requests:', error);
        return [];
    }
}

async function handleFakeImage(request, env) {
    console.log('Handling fake image request');
    const url = new URL(request.url);
    const traceId = url.searchParams.get('traceId');

    if (!traceId) {
        return new Response('Missing traceId parameter', { status: 400 });
    }

    // 存储请求信息
    await storeRequest(env, traceId, request);

    // 返回随机图片
    try {
        const imageResponse = await fetch('https://picsum.photos/200/300', {
            headers: { 'Cache-Control': 'no-cache' }
        });

        if (!imageResponse.ok) {
            throw new Error('Failed to fetch image');
        }

        const imageBuffer = await imageResponse.arrayBuffer();
        return new Response(imageBuffer, {
            headers: {
                'Content-Type': imageResponse.headers.get('content-type') || 'image/jpeg',
                'Cache-Control': 'no-store'
            }
        });
    } catch (e) {
        console.error('Error in handleFakeImage:', e);
        throw e;
    }
}

// 设置超时时间为5秒
const TIMEOUT = 10000;

// 超时处理函数
function timeoutPromise(ms) {
    return new Promise((_, reject) => setTimeout(() => reject(new Error('请求超时')), ms));
}

async function handleOpenAIRequest(request, env) {
    console.log('Handling OpenAI request');
    try {
        const { url, key, model = 'gpt-4o' } = await request.json();

        if (!url || !key) {
            throw new Error('Missing url or key parameter');
        }

        const traceId = Date.now().toString();
        const encoder = new TextEncoder();

        const stream = new ReadableStream({
            async start(controller) {
                try {
                    function sendMessage(type, message) {
                        const msg = JSON.stringify({ type, message }) + '\n';
                        controller.enqueue(encoder.encode(msg));
                        console.log('Sent message:', msg);
                    }

                    // 发送初始状态
                    sendMessage('status', '请求已发送,TraceId: ' + traceId);
                    sendMessage('log', '开始请求 - 模型: ' + model);

                    // 构造图片 URL
                    const imageUrl = `https://${CONFIG.WORKER_DOMAIN}/static/img?traceId=${traceId}`;
                    
                    // 使用 Promise.race 实现超时控制
                    const fetchPromise = fetch(url, {
                        method: 'POST',
                        headers: {
                            'Accept': 'application/json',
                            'Content-Type': 'application/json',
                            'Authorization': `Bearer ${key}`
                        },
                        body: JSON.stringify({
                            model,
                            messages: [{
                                role: "user",
                                content: [
                                    { type: "image_url", image_url: { url: imageUrl } },
                                    { type: "text", text: "What is this?" }
                                ]
                            }],
                            max_tokens: 30,
                            stream: false
                        })
                    });

                    // 设置超时机制
                    const response = await Promise.race([fetchPromise, timeoutPromise(TIMEOUT)]);

                    const responseText = await response.text();
                    let responseData;
                    try {
                        responseData = JSON.parse(responseText);
                        sendMessage('log', '请求成功');
                    } catch (e) {
                        sendMessage('error', 'Invalid JSON response: ' + responseText);
                        return;
                    }

                    // API 请求完成后,等待一小段时间让数据写入完成
                    await new Promise(resolve => setTimeout(resolve, 1000));

                    // 现在查询中转节点信息
                    sendMessage('log', '正在检测中转节点...');
                    const requests = await getRequests(env, traceId);
                    console.log('Found requests:', requests);

                    if (requests && requests.length > 0) {
                        for (const req of requests) {
                            sendMessage('log', 
                                `检测到中转节点: ${req.as_org || 'Unknown'} ` +
                                `(${req.ip}) - 位置: ${req.city || 'Unknown'}, ` +
                                `${req.country} [DC: ${req.colo}]`
                            );
                            // 添加小延迟确保消息正确发送
                            await new Promise(resolve => setTimeout(resolve, 100));
                        }
                    } else {
                        sendMessage('log', '未检测到中转节点');
                    }

                    // 最后发送 API 响应结果
                    sendMessage('response', responseData);

                } catch (error) {
                    console.error('Error in stream:', error);
                    sendMessage('error', error.message);
                } finally {
                    controller.close();
                }
            }
        });

        return new Response(stream, {
            headers: { 
                'Content-Type': 'text/plain',
                'Cache-Control': 'no-cache',
                'Connection': 'keep-alive'
            }
        });
    } catch (error) {
        console.error('Error in handleOpenAIRequest:', error);
        return new Response(JSON.stringify({ error: error.message }), {
            status: 500,
            headers: { 'Content-Type': 'application/json' }
        });
    }
}
async function handleRequest(request, env) {
    console.log('Request received:', request.url);
    const url = new URL(request.url);
    const pathname = url.pathname;

    try {
        if (pathname === '/v1/serve' && request.method === 'POST') {
            return await handleOpenAIRequest(request, env);
        } else if (pathname === '/static/img' && request.method === 'GET') {
            return await handleFakeImage(request, env);
        } else if (pathname === '/' || pathname === '/index.html') {
            return new Response(HTML_CONTENT, {
                headers: { 'Content-Type': 'text/html;charset=UTF-8' }
            });
        }
        
        return new Response('Not Found', { status: 404 });
    } catch (error) {
        console.error('Error in handleRequest:', error);
        throw error;
    }
}

export default {
    async fetch(request, env, ctx) {
        try {
            return await handleRequest(request, env);
        } catch (error) {
            console.error('Fatal error:', error);
            return new Response(JSON.stringify({
                error: 'Internal Server Error',
                message: error.message
            }), {
                status: 500,
                headers: { 'Content-Type': 'application/json' }
            });
        }
    }
};
15 Likes

:face_holding_back_tears:顶顶顶

太强了,感谢佬友分享

1 Like

错误: Request processing error

佬友可以详细点吗

网页上就显示了这个错误啊

点个赞上午再看

太强了大佬

进行了一些优化,使用了D1存储库而不是kv

1 Like

感谢大佬!!!!
搭建好了~https://trace.autopentest.ac.cn/

你好,
WORKER_DOMAIN: ‘<填你自己的域名>’, // worker 的域名
这一部分是填这个域名吗?就是编辑代码时右边的
这是它的报错
worker.js:431 Request received: https://checkapi1234.2535158651.workers.dev/
worker.js:215 When using module syntax, the ‘fetch’ event handler should be declared as an exported function on the root module as opposed to using the global addEventListener().
(anonymous) @ worker.js:215
worker.js:431 Request received: https://checkapi1234.2535158651.workers.dev/v1/serve
worker.js:319 Handling OpenAI request
worker.js:336 Sent message: {“type”:“status”,“message”:“请求已发送,TraceId: 1736571151788”}

worker.js:336 Sent message: {“type”:“log”,“message”:“开始请求 - 模型: gpt-4o-2024-08-06”}

worker.js:336 Sent message: {“type”:“error”,“message”:“Invalid JSON response: error code: 1003”}