tmpublic commited on
Commit
70d4eee
·
verified ·
1 Parent(s): 6a174c0

Upload 4 files

Browse files
Files changed (4) hide show
  1. Dockerfile +38 -0
  2. requirements.txt +4 -0
  3. start.sh +35 -0
  4. ww.py +420 -0
Dockerfile ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ # 安装系统依赖
4
+ RUN apt-get update && apt-get install -y \
5
+ curl \
6
+ gnupg \
7
+ lsb-release \
8
+ ca-certificates \
9
+ && rm -rf /var/lib/apt/lists/*
10
+
11
+ # 安装 Cloudflare WARP
12
+ RUN curl -fsSL https://pkg.cloudflareclient.com/pubkey.gpg | gpg --yes --dearmor -o /usr/share/keyrings/cloudflare-warp-archive-keyring.gpg \
13
+ && echo "deb [arch=amd64 signed-by=/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg] https://pkg.cloudflareclient.com/ bookworm main" > /etc/apt/sources.list.d/cloudflare-client.list \
14
+ && apt-get update \
15
+ && apt-get install -y cloudflare-warp \
16
+ && rm -rf /var/lib/apt/lists/*
17
+
18
+ # 创建工作目录
19
+ WORKDIR /app
20
+
21
+ # 复制依赖文件
22
+ COPY requirements.txt .
23
+
24
+ # 安装 Python 依赖
25
+ RUN pip install --no-cache-dir -r requirements.txt
26
+
27
+ # 复制应用代码
28
+ COPY ww.py .
29
+ COPY start.sh .
30
+
31
+ # 设置启动脚本权限
32
+ RUN chmod +x start.sh
33
+
34
+ # HuggingFace Space 使用 7860 端口
35
+ EXPOSE 7860
36
+
37
+ # 启动脚本
38
+ CMD ["./start.sh"]
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ fastapi
2
+ pydantic
3
+ curl_cffi
4
+ uvicorn
start.sh ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ echo "[*] Starting Cloudflare WARP..."
4
+
5
+ # 启动 WARP 服务
6
+ warp-svc &
7
+ sleep 3
8
+
9
+ # 注册 WARP (首次运行需要)
10
+ warp-cli --accept-tos registration new 2>/dev/null || true
11
+
12
+ # 设置 WARP 代理模式
13
+ warp-cli --accept-tos mode proxy
14
+
15
+ # 设置代理端口
16
+ warp-cli --accept-tos proxy port 40000
17
+
18
+ # 连接 WARP
19
+ warp-cli --accept-tos connect
20
+
21
+ # 等待连接
22
+ sleep 2
23
+
24
+ # 检查连接状态
25
+ echo "[*] WARP Status:"
26
+ warp-cli status
27
+
28
+ # 设置代理环境变量
29
+ export HTTP_PROXY="socks5://127.0.0.1:40000"
30
+ export HTTPS_PROXY="socks5://127.0.0.1:40000"
31
+
32
+ echo "[*] Starting API Server on port 7860..."
33
+
34
+ # 启动 Python 应用
35
+ python ww.py
ww.py ADDED
@@ -0,0 +1,420 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import uuid
3
+ import time
4
+ import asyncio
5
+ import re
6
+ import hashlib
7
+ import os
8
+ from typing import List, Optional, Dict, Any
9
+
10
+ from fastapi import FastAPI, Request, HTTPException
11
+ from fastapi.responses import StreamingResponse
12
+ from pydantic import BaseModel
13
+ from curl_cffi import requests
14
+
15
+ # 代理配置 (从环境变量读取)
16
+ PROXY_URL = os.getenv("HTTP_PROXY") or os.getenv("HTTPS_PROXY")
17
+
18
+ # 零宽字符定义
19
+ ZW_BINARY_0 = '\u200b'
20
+ ZW_BINARY_1 = '\u200c'
21
+ ZW_START = '\u200d\u200b'
22
+ ZW_END = '\u200d\u200c'
23
+ ZW_SEP = '\u200d\u200d'
24
+
25
+ def get_content_hash(text: str) -> str:
26
+ """计算文本内容的简短 Hash (取 SHA256 前 8 位)"""
27
+ # 先去除文本中可能存在的旧 Flag 再计算 Hash
28
+ clean_text = strip_flag(text)
29
+ return hashlib.sha256(clean_text.encode('utf-8')).hexdigest()[:8]
30
+
31
+ def strip_flag(text: str) -> str:
32
+ """从文本中完全移除零宽 Flag 字符"""
33
+ pattern = f"{ZW_START}[\\{ZW_BINARY_0}\\{ZW_BINARY_1}]+{ZW_END}"
34
+ return re.sub(pattern, "", text)
35
+
36
+ def encode_flag(context_id: str, content_hash: str = "") -> str:
37
+ """将 ID 和 Hash 编码为零宽字符 Flag"""
38
+ payload = f"{context_id}|{content_hash}"
39
+ binary = "".join(format(b, '08b') for b in payload.encode('utf-8'))
40
+ zw_payload = "".join(ZW_BINARY_0 if b == '0' else ZW_BINARY_1 for b in binary)
41
+ return f"{ZW_START}{zw_payload}{ZW_END}"
42
+
43
+ def decode_flag(text: str) -> Optional[Dict[str, str]]:
44
+ """从文本末尾解码零宽字符 Flag"""
45
+ pattern = f"{ZW_START}([\\{ZW_BINARY_0}\\{ZW_BINARY_1}]+){ZW_END}"
46
+ match = re.search(pattern, text)
47
+ if not match:
48
+ return None
49
+
50
+ zw_payload = match.group(1)
51
+ binary = "".join('0' if c == ZW_BINARY_0 else '1' for c in zw_payload)
52
+
53
+ try:
54
+ byte_data = bytearray()
55
+ for i in range(0, len(binary), 8):
56
+ byte_data.append(int(binary[i:i+8], 2))
57
+
58
+ decoded = byte_data.decode('utf-8')
59
+ parts = decoded.split('|')
60
+ return {
61
+ "context_id": parts[0],
62
+ "hash": parts[1] if len(parts) > 1 else ""
63
+ }
64
+ except Exception:
65
+ return None
66
+
67
+ app = FastAPI(title="ChatSDK OpenAI Wrapper")
68
+
69
+ # 上下文映射表: context_id -> {"chat_id": str, "history": list}
70
+ context_store: Dict[str, Dict[str, Any]] = {}
71
+
72
+ class ChatSDKClient:
73
+ def __init__(self):
74
+ self.base_url = "https://demo.chat-sdk.dev"
75
+ self.proxy = PROXY_URL
76
+ self.curl_session = requests.AsyncSession(impersonate="chrome110", proxy=self.proxy)
77
+ self.csrf_token = None
78
+ self.user_data = None
79
+ if self.proxy:
80
+ print(f"[*] Using proxy: {self.proxy}")
81
+
82
+ async def initialize(self):
83
+ print("[*] Refreshing Session (Async)...")
84
+ # 清除旧的 Session 状态
85
+ self.curl_session = requests.AsyncSession(impersonate="chrome110", proxy=self.proxy)
86
+ self.curl_session.headers.update({
87
+ "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36",
88
+ "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
89
+ "accept-language": "zh-CN,zh;q=0.9,en;q=0.8",
90
+ })
91
+
92
+ # 1. Get CSRF
93
+ resp = await self.curl_session.get(f"{self.base_url}/api/auth/csrf")
94
+ self.csrf_token = resp.json().get("csrfToken")
95
+
96
+ # 2. Trigger guest session
97
+ await self.curl_session.post(
98
+ f"{self.base_url}/api/auth/callback/guest",
99
+ data={
100
+ "csrfToken": self.csrf_token,
101
+ "callbackUrl": self.base_url,
102
+ "json": "true"
103
+ }
104
+ )
105
+
106
+ # 3. Get Session
107
+ resp = await self.curl_session.get(f"{self.base_url}/api/auth/session")
108
+ self.user_data = resp.json()
109
+
110
+ if not self.user_data or not self.user_data.get("user"):
111
+ await self.curl_session.get(f"{self.base_url}/")
112
+ resp = await self.curl_session.get(f"{self.base_url}/api/auth/session")
113
+ self.user_data = resp.json()
114
+
115
+ return self.user_data
116
+
117
+ async def chat_stream(self, messages: List[Dict[str, Any]], model: str):
118
+ if not self.csrf_token:
119
+ await self.initialize()
120
+
121
+ # 检测上下文复用
122
+ chat_id = str(uuid.uuid4())
123
+ history = []
124
+ last_msg_content = messages[-1]["content"]
125
+
126
+ # 检查最后一条助理消息是否包含有效 Flag 且内容未被篡改
127
+ flag_info = None
128
+ assistant_msg_index = -1
129
+ if len(messages) > 1:
130
+ for i in range(len(messages) - 2, -1, -1):
131
+ if messages[i]["role"] == "assistant":
132
+ flag_info = decode_flag(messages[i]["content"])
133
+ assistant_msg_index = i
134
+ break
135
+
136
+ should_reuse = False
137
+ if flag_info:
138
+ ctx_id = flag_info["context_id"]
139
+ stored_hash = flag_info["hash"]
140
+ # 计算当前消息内容的 Hash (去除 Flag 后)
141
+ current_content = messages[assistant_msg_index]["content"]
142
+ actual_hash = get_content_hash(current_content)
143
+
144
+ if ctx_id in context_store and actual_hash == stored_hash:
145
+ print(f"[*] 检测到FLAG且Hash校验通过,复用上下文 <{ctx_id}>")
146
+ stored = context_store[ctx_id]
147
+ chat_id = stored["chat_id"]
148
+ history = stored["history"]
149
+ should_reuse = True
150
+ elif actual_hash != stored_hash:
151
+ print(f"[*] 检测到内容篡改 (Hash: {actual_hash} != {stored_hash}),复用失败,以降级后的内容重建上下文")
152
+ else:
153
+ print(f"[*] 复用失败 (ContextID 不存在),降级并重建上下文")
154
+
155
+ if not should_reuse:
156
+ # 降级逻辑:手动重新构建上下文,并清洗所有消息中的 Flag
157
+ for msg in messages[:-1]:
158
+ clean_content = strip_flag(msg["content"])
159
+ history.append({
160
+ "id": str(uuid.uuid4()),
161
+ "role": msg["role"],
162
+ "parts": [{"type": "text", "text": clean_content}]
163
+ })
164
+
165
+ # 清洗最后一条用户消息
166
+ last_message = {
167
+ "id": str(uuid.uuid4()),
168
+ "role": messages[-1]["role"],
169
+ "parts": [{"type": "text", "text": strip_flag(last_msg_content)}]
170
+ }
171
+
172
+ # 如果 history 不为空,将 Prompt 注入到 history[0] 的开头
173
+ if history:
174
+ original_text = history[0]["parts"][0]["text"]
175
+ prompt = "You are a helpful assistant." # 默认 Prompt 或从某处获取
176
+ # 如果 messages[0] 是 system 角色,则使用它的内容作为 prompt
177
+ if messages[0]["role"] == "system":
178
+ prompt = messages[0]["content"]
179
+
180
+ history[0]["parts"][0]["text"] = f"<SYSTEM_PROMPT>\n{prompt}\n</SYSTEM_PROMPT>\n\n{original_text}"
181
+ else:
182
+ # 如果没有 history,则注入到 last_message 的开头
183
+ original_text = last_message["parts"][0]["text"]
184
+ prompt = "You are a helpful assistant."
185
+ if messages[0]["role"] == "system":
186
+ prompt = messages[0]["content"]
187
+
188
+ last_message["parts"][0]["text"] = f"<SYSTEM_PROMPT>\n{prompt}\n</SYSTEM_PROMPT>\n\n{original_text}"
189
+
190
+ url = f"{self.base_url}/api/chat"
191
+ headers = {
192
+ "accept": "*/*",
193
+ "content-type": "application/json",
194
+ "origin": self.base_url,
195
+ "referer": f"{self.base_url}/chat/{chat_id}",
196
+ "x-csrf-token": self.csrf_token,
197
+ }
198
+
199
+ payload = {
200
+ "id": chat_id,
201
+ "selectedChatModel": model,
202
+ "selectedVisibilityType": "private",
203
+ "messages": history + [last_message]
204
+ }
205
+
206
+ # 重试机制
207
+ attempts = 0
208
+ while attempts < 2:
209
+ print(f"[*] Sending Chat Request (Attempt {attempts + 1}, Async)...")
210
+ headers["x-csrf-token"] = self.csrf_token
211
+
212
+ full_response_text = ""
213
+ try:
214
+ resp = await self.curl_session.post(url, headers=headers, json=payload, stream=True)
215
+
216
+ if resp.status_code == 200:
217
+ # 状态追踪
218
+ last_code_content = ""
219
+ in_code_block = False
220
+ current_tool_call = {}
221
+
222
+ async for line in resp.aiter_lines():
223
+ if not line:
224
+ continue
225
+ line_str = line.decode('utf-8')
226
+ if line_str.startswith("data: "):
227
+ data_content = line_str[6:]
228
+ if data_content == "[DONE]":
229
+ break
230
+ try:
231
+ chunk = json.loads(data_content)
232
+ chunk_type = chunk.get("type")
233
+
234
+ delta_text = ""
235
+
236
+ # 1. 处理标准文本
237
+ if chunk_type == "text-delta":
238
+ if in_code_block:
239
+ delta_text = "\n```\n" + chunk.get("delta", "")
240
+ in_code_block = False
241
+ else:
242
+ delta_text = chunk.get("delta", "")
243
+
244
+ # 2. 处理工具输入 (JSON 形式的参数)
245
+ elif chunk_type == "tool-input-delta":
246
+ delta_text = chunk.get("inputTextDelta", "")
247
+
248
+ # 3. 处理代码生成 (累积形式的内容)
249
+ elif chunk_type == "data-codeDelta":
250
+ full_content = chunk.get("data", "")
251
+ if not in_code_block:
252
+ # 尝试获取语言类型,默认为 python
253
+ lang = current_tool_call.get("kind", "python") if current_tool_call else "python"
254
+ title = current_tool_call.get("title", "") if current_tool_call else ""
255
+ header = f"\n\n### {title}\n" if title else "\n"
256
+ delta_text = f"{header}```{lang}\n"
257
+ in_code_block = True
258
+
259
+ # 计算增量 (diff)
260
+ if full_content.startswith(last_code_content):
261
+ delta_text += full_content[len(last_code_content):]
262
+ else:
263
+ delta_text += full_content
264
+
265
+ last_code_content = full_content
266
+
267
+ # 4. 处理元数据
268
+ elif chunk_type == "data-kind":
269
+ if not current_tool_call: current_tool_call = {}
270
+ current_tool_call["kind"] = chunk.get("data")
271
+
272
+ elif chunk_type == "data-title":
273
+ if not current_tool_call: current_tool_call = {}
274
+ current_tool_call["title"] = chunk.get("data")
275
+
276
+ # 5. 处理其他文本增量
277
+ elif chunk_type == "data-textDelta":
278
+ delta_text = chunk.get("data", "")
279
+
280
+ # 6. 处理步骤结束 (关闭代码块)
281
+ elif chunk_type in ["finish-step", "tool-output-available", "data-finish"]:
282
+ if in_code_block:
283
+ delta_text = "\n```\n"
284
+ in_code_block = False
285
+ last_code_content = ""
286
+ current_tool_call = {} # 重置工具调用状态
287
+
288
+ if delta_text:
289
+ full_response_text += delta_text
290
+ yield delta_text
291
+ except:
292
+ pass
293
+
294
+ # 保存上下文映射
295
+ new_ctx_id = str(uuid.uuid4())[:8]
296
+ # 保存到历史记录时,确保是不含 Flag 的纯文本
297
+ new_history = history + [last_message, {
298
+ "id": str(uuid.uuid4()),
299
+ "role": "assistant",
300
+ "parts": [{"type": "text", "text": full_response_text}]
301
+ }]
302
+ context_store[new_ctx_id] = {
303
+ "chat_id": chat_id,
304
+ "history": new_history
305
+ }
306
+
307
+ # 计算新内容的 Hash 并生成 Flag
308
+ new_hash = get_content_hash(full_response_text)
309
+ yield encode_flag(new_ctx_id, new_hash)
310
+ return
311
+ else:
312
+ print(f"[!] Request failed with status {resp.status_code}. Refreshing session...")
313
+ await self.initialize()
314
+ attempts += 1
315
+ except Exception as e:
316
+ print(f"[!] Error during request: {e}")
317
+ await self.initialize()
318
+ attempts += 1
319
+
320
+ yield f"Error: Request failed after {attempts} attempts."
321
+
322
+ # OpenAI 兼容的模型定义
323
+ SUPPORTED_MODELS = [
324
+ "anthropic/claude-opus-4.5",
325
+ "anthropic/claude-sonnet-4.5",
326
+ "anthropic/claude-haiku-4.5",
327
+ "openai/gpt-4.1-mini",
328
+ "openai/gpt-5.2",
329
+ "google/gemini-2.5-flash-lite",
330
+ "google/gemini-3-pro-preview",
331
+ "xai/grok-4.1-fast-non-reasoning",
332
+ "anthropic/claude-3.7-sonnet-thinking",
333
+ "xai/grok-code-fast-1-thinking"
334
+ ]
335
+
336
+ class Message(BaseModel):
337
+ role: str
338
+ content: str
339
+
340
+ class ChatCompletionRequest(BaseModel):
341
+ model: str = "google/gemini-3-pro-preview"
342
+ messages: List[Message]
343
+ stream: bool = False
344
+
345
+ client = ChatSDKClient()
346
+
347
+ @app.post("/v1/chat/completions")
348
+ async def chat_completions(request: ChatCompletionRequest):
349
+ messages_dict = [m.model_dump() for m in request.messages]
350
+
351
+ if request.stream:
352
+ async def generate():
353
+ created_time = int(time.time())
354
+ request_id = f"chatcmpl-{uuid.uuid4()}"
355
+
356
+ # 发送首个 chunk (role)
357
+ yield f"data: {json.dumps({'id': request_id, 'object': 'chat.completion.chunk', 'created': created_time, 'model': request.model, 'choices': [{'index': 0, 'delta': {'role': 'assistant'}, 'finish_reason': None}]})}\n\n"
358
+
359
+ async for delta in client.chat_stream(messages_dict, request.model):
360
+ chunk = {
361
+ "id": request_id,
362
+ "object": "chat.completion.chunk",
363
+ "created": created_time,
364
+ "model": request.model,
365
+ "choices": [{
366
+ "index": 0,
367
+ "delta": {"content": delta},
368
+ "finish_reason": None
369
+ }]
370
+ }
371
+ yield f"data: {json.dumps(chunk)}\n\n"
372
+
373
+ # 发送结束 chunk
374
+ yield f"data: {json.dumps({'id': request_id, 'object': 'chat.completion.chunk', 'created': created_time, 'model': request.model, 'choices': [{'index': 0, 'delta': {}, 'finish_reason': 'stop'}]})}\n\n"
375
+ yield "data: [DONE]\n\n"
376
+
377
+ return StreamingResponse(generate(), media_type="text/event-stream")
378
+ else:
379
+ # 非流式处理
380
+ content = ""
381
+ async for delta in client.chat_stream(messages_dict, request.model):
382
+ content += delta
383
+
384
+ return {
385
+ "id": f"chatcmpl-{uuid.uuid4()}",
386
+ "object": "chat.completion",
387
+ "created": int(time.time()),
388
+ "model": request.model,
389
+ "choices": [{
390
+ "index": 0,
391
+ "message": {
392
+ "role": "assistant",
393
+ "content": content
394
+ },
395
+ "finish_reason": "stop"
396
+ }],
397
+ "usage": {
398
+ "prompt_tokens": 0,
399
+ "completion_tokens": 0,
400
+ "total_tokens": 0
401
+ }
402
+ }
403
+
404
+ @app.get("/v1/models")
405
+ async def list_models():
406
+ return {
407
+ "object": "list",
408
+ "data": [
409
+ {
410
+ "id": model_id,
411
+ "object": "model",
412
+ "created": int(time.time()),
413
+ "owned_by": "chatsdk"
414
+ } for model_id in SUPPORTED_MODELS
415
+ ]
416
+ }
417
+
418
+ if __name__ == "__main__":
419
+ import uvicorn
420
+ uvicorn.run(app, host="0.0.0.0", port=7860)