File size: 10,060 Bytes
ef881c7
 
 
 
 
 
 
 
 
 
 
c00ba42
 
 
 
 
 
 
 
 
 
ef881c7
c00ba42
 
 
 
 
 
ef881c7
 
 
 
 
 
 
 
 
 
c00ba42
ef881c7
 
 
 
 
 
 
 
 
c00ba42
 
 
e406d5a
c00ba42
 
 
 
 
 
 
046641f
c00ba42
ef881c7
c00ba42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ef881c7
 
c00ba42
 
 
 
 
 
 
 
 
 
 
ef881c7
 
 
 
 
c00ba42
 
 
 
ef881c7
 
 
 
c00ba42
 
 
 
 
 
 
 
 
 
ef881c7
 
 
 
 
 
 
 
 
c00ba42
ef881c7
 
c00ba42
 
 
 
 
 
ef881c7
 
 
 
 
 
c00ba42
ef881c7
 
 
 
 
 
 
 
 
 
 
c00ba42
ef881c7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c00ba42
ef881c7
 
 
c00ba42
 
 
 
 
 
ef881c7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c00ba42
ef881c7
 
 
e406d5a
 
c00ba42
 
 
 
 
 
 
 
e406d5a
 
 
 
 
 
c00ba42
e406d5a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c00ba42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e406d5a
ef881c7
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
import json
import time
import hashlib
import base64
import asyncio
import aiohttp
from fastapi import FastAPI, Request, Response
from typing import Optional, Dict, List, Any
import uvicorn
from pydantic import BaseModel

# 配置参数
API_CHALLENGE_URL = 'https://api.eqing.tech/api/altcaptcha/challenge'  # 验证码挑战接口
NEXTWAY_CHAT_URL = 'https://origin.eqing.tech/api/openai/v1/chat/completions'  # 聊天完成接口
CREDENTIAL_EXPIRY_MARGIN = 60 * 1000  # 凭证过期边界时间(60秒,毫秒单位)
PORT = 7860  # 服务器端口
API_ENDPOINT = '/v1/chat/completions'  # API端点
MODEL_NAME = "gpt-4o-free"  # 默认模型名称
REQUEST_TIMEOUT = 480  # 请求超时时间(秒)
MAX_RETRIES = 3  # 最大重试次数
RETRY_DELAY = 1  # 重试延迟(秒)

# 全局变量
current_credential = None  # 当前凭证
credential_expiry = None  # 凭证过期时间
is_refreshing_credential = False  # 是否正在刷新凭证

# 模型映射字典
MODEL_MAPPING = {
    'gpt-4o-all-lite': 'gpt-4o-mini',
    'gpt-4o-image': 'gpt-4o-mini-image-free',
    'grok-3-re': 'grok-3-free',
    'gemini-2.0-flash': 'gemini-2.0-flash-free'
}

app = FastAPI()

class ChatRequest(BaseModel):
    """聊天请求的数据模型"""
    messages: List[Dict[str, str]]
    model: Optional[str]
    stream: Optional[bool] = True
    temperature: Optional[float] = 0.5
    presence_penalty: Optional[float] = 0
    frequency_penalty: Optional[float] = 0
    top_p: Optional[float] = 1
    max_tokens: Optional[int] = 4000

async def extract_content(text: str) -> str:
    """
    从响应文本中提取内容
    
    Args:
        text: 响应文本
    Returns:
        提取的AI响应内容
    """
    lines = text.split('\n')
    ai_response = ''
    ignored_id = 'chatcmpl-Em8sqOsIIDLYPNvC65WfFvqv'
    created = 1687070102

    for line in lines:
        line = line.strip()
        if line.startswith('data:'):
            data_str = line[5:].strip()
            if not data_str or data_str in ['[ORIGIN]', '[DONE]']:
                continue

            try:
                json_data = json.loads(data_str)
                # 跳过特定的响应
                if json_data.get('id') == ignored_id or json_data.get('created') == created:
                    continue

                # 提取内容
                if (json_data.get('choices') and 
                    json_data['choices'][0].get('delta') and 
                    'content' in json_data['choices'][0]['delta']):
                    content = json_data['choices'][0]['delta']['content']
                    ai_response += content

            except json.JSONDecodeError:
                print(f'跳过非JSON数据')

    return ai_response

async def solve_challenge(challenge: str, salt: str, algorithm: str = "SHA-512", max_number: int = 1000000):
    """
    解决验证码挑战
    
    Args:
        challenge: 挑战字符串
        salt: 盐值
        algorithm: 哈希算法
        max_number: 最大尝试次数
    Returns:
        解决方案字典
    """
    start_time = time.time()
    
    for number in range(max_number):
        hash_value = await verify_hash(salt, number, algorithm)
        if hash_value == challenge:
            return {
                "number": number,
                "took": int((time.time() - start_time) * 1000)
            }
    
    return None

async def verify_hash(salt: str, number: int, algorithm: str) -> str:
    """
    验证哈希值
    
    Args:
        salt: 盐值
        number: 数字
        algorithm: 哈希算法
    Returns:
        哈希字符串
    """
    input_str = f"{salt}{number}"
    
    if algorithm == "SHA-512":
        hash_obj = hashlib.sha512(input_str.encode())
        return hash_obj.hexdigest()
    elif algorithm == "SHA-256":
        hash_obj = hashlib.sha256(input_str.encode())
        return hash_obj.hexdigest()
    else:
        raise ValueError(f"不支持的算法: {algorithm}")

async def generate_credential():
    """
    生成新的凭证
    
    Returns:
        凭证信息字典
    """
    global current_credential, credential_expiry
    
    async with aiohttp.ClientSession() as session:
        try:
            async with session.get(API_CHALLENGE_URL) as response:
                if response.status != 200:
                    print(f"验证码请求失败,状态码: {response.status}")
                    return None
                
                data = await response.json()
                solution = await solve_challenge(
                    data['challenge'],
                    data['salt'],
                    data['algorithm'],
                    data['maxnumber']
                )

                if not solution:
                    print("解决验证码挑战失败")
                    return None

                credential_obj = {
                    "algorithm": data['algorithm'],
                    "challenge": data['challenge'],
                    "number": solution['number'],
                    "salt": data['salt'],
                    "signature": data['signature'],
                    "took": solution['took']
                }

                credential = base64.b64encode(json.dumps(credential_obj).encode()).decode()
                expiry = int(data['salt'].split('?expires=')[1]) * 1000

                return {"credential": credential, "expiry": expiry}

        except Exception as e:
            print(f"生成凭证时出错: {e}")
            return None

async def get_credential():
    """
    获取有效的凭证
    
    Returns:
        当前有效的凭证
    """
    global current_credential, credential_expiry, is_refreshing_credential
    
    if (not current_credential or 
        not credential_expiry or 
        credential_expiry <= time.time() * 1000 + CREDENTIAL_EXPIRY_MARGIN):
        
        if not is_refreshing_credential:
            is_refreshing_credential = True
            try:
                cred_data = await generate_credential()
                if cred_data:
                    current_credential = cred_data['credential']
                    credential_expiry = cred_data['expiry']
            finally:
                is_refreshing_credential = False
        else:
            await asyncio.sleep(2)  # 等待其他进程完成凭证刷新
    
    return current_credential

@app.post(API_ENDPOINT)
async def chat_endpoint(request: ChatRequest):
    """
    聊天API端点
    
    Args:
        request: 聊天请求对象
    Returns:
        聊天响应
    """
    try:
        model = MODEL_MAPPING.get(request.model, request.model or MODEL_NAME)
        response_content = await handle_chat_request(model, request.messages)
        
        if response_content is None:
            return Response(
                content="从API获取响应失败",
                status_code=500
            )

        return {
            "choices": [{
                "message": {
                    "role": "assistant",
                    "content": response_content.strip()
                },
                "finish_reason": "stop",
                "index": 0
            }],
            "model": model,
            "object": "chat.completion"
        }

    except Exception as e:
        print(f"处理聊天请求时出错: {e}")
        return Response(content="内部服务器错误", status_code=500)

async def handle_chat_request(model: str, messages: List[Dict[str, str]]):
    """
    处理聊天请求
    
    Args:
        model: 模型名称
        messages: 消息列表
    Returns:
        聊天响应内容
    """
    captcha_token = await get_credential()
    if not captcha_token:
        return None

    body = {
        "messages": messages,
        "stream": True,
        "model": model,
        "temperature": 0.5,
        "presence_penalty": 0,
        "frequency_penalty": 0,
        "top_p": 1,
        "max_tokens": 4000,
        "captchaToken": captcha_token
    }

    timeout = aiohttp.ClientTimeout(total=REQUEST_TIMEOUT)
    async with aiohttp.ClientSession(timeout=timeout) as session:
        try:
            async with session.post(NEXTWAY_CHAT_URL, json=body) as response:
                if response.status != 200:
                    print(f"请求失败,状态码: {response.status}")
                    return None

                complete_response = ""
                buffer = ""  # 用于存储未完成的数据块

                async for chunk in response.content.iter_chunks():
                    if chunk[0]:  # chunk[0] 是数据,chunk[1] 是布尔值表示是否是最后一块
                        try:
                            chunk_text = (buffer + chunk[0].decode()).strip()
                            buffer = ""  # 清空缓冲区
                            
                            # 处理可能的不完整JSON
                            if chunk_text.endswith('}'):
                                content = await extract_content(chunk_text)
                                if content:
                                    complete_response += content
                            else:
                                buffer = chunk_text  # 存储不完整的数据到缓冲区

                        except UnicodeDecodeError as e:
                            print(f"解码错误: {e}")
                            buffer = ""  # 清空缓冲区,跳过损坏的数据
                            continue

                # 检查最终响应
                if not complete_response:
                    print("警告: 收到空响应")
                    return None

                return complete_response.strip()

        except asyncio.TimeoutError:
            print("请求超时")
            return None
        except Exception as e:
            print(f"处理请求时出错: {e}")
            await get_credential()  # 刷新凭证
            return None

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=PORT)