coo7 commited on
Commit
616280d
·
verified ·
1 Parent(s): cd5d719

Upload 3 files

Browse files
Files changed (3) hide show
  1. Dockerfile +38 -0
  2. app.py +809 -0
  3. requirements.txt +7 -0
Dockerfile ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Create cache directory with proper permissions
6
+ RUN mkdir -p /.cache/huggingface && chmod 777 /.cache/huggingface
7
+
8
+ # 安装系统依赖
9
+ RUN apt-get update && apt-get install -y \
10
+ gcc \
11
+ g++ \
12
+ && rm -rf /var/lib/apt/lists/*
13
+
14
+ # 复制项目文件
15
+ COPY requirements.txt .
16
+ COPY app.py .
17
+ COPY templates/ templates/
18
+
19
+ # 安装Python依赖
20
+ RUN pip install --no-cache-dir -r requirements.txt
21
+
22
+ # 设置环境变量
23
+ ENV PYTHONUNBUFFERED=1
24
+ ENV HOST=0.0.0.0
25
+ ENV PORT=7860
26
+ ENV TRANSFORMERS_CACHE=/.cache/huggingface/hub
27
+ ENV HF_HOME=/.cache/huggingface
28
+
29
+ # 设置权限
30
+ RUN chmod +x app.py
31
+
32
+ # 暴露端口
33
+ EXPOSE 7860
34
+
35
+ # 修改缓存目录权限
36
+ RUN mkdir -p /.cache/huggingface/hub && chmod -R 777 /.cache/huggingface
37
+
38
+ CMD ["python", "app.py"]
app.py ADDED
@@ -0,0 +1,809 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ import json
3
+ import base64
4
+ import time
5
+ import logging
6
+ from curl_cffi import requests
7
+ import random
8
+ from flask import Flask, render_template, request, Response, stream_with_context, jsonify, g
9
+ import os
10
+ import struct
11
+ import ctypes
12
+ from wasmtime import Store, Module, Linker
13
+ import re
14
+ import transformers
15
+
16
+ # -------------------------- 初始化 tokenizer --------------------------
17
+ chat_tokenizer_dir = "THUDM/chatglm2-6b" # 使用现成的模型tokenizer
18
+ tokenizer = transformers.AutoTokenizer.from_pretrained(
19
+ chat_tokenizer_dir,
20
+ trust_remote_code=True,
21
+ use_fast=False # 使用慢速tokenizer避免fast tokenizer的转换问题
22
+ )
23
+
24
+ # ----------------------------------------------------------------------
25
+ # =========================== 日志配置 ===========================
26
+ logging.basicConfig(
27
+ level=logging.INFO,
28
+ format='%(asctime)s [%(levelname)s] %(name)s: %(message)s'
29
+ )
30
+ app = Flask(__name__)
31
+
32
+ # -------------------- 全局添加 CORS 支持 --------------------
33
+ @app.before_request
34
+ def handle_options_request():
35
+ if request.method == 'OPTIONS':
36
+ response = Response()
37
+ response.headers["Access-Control-Allow-Origin"] = "*"
38
+ response.headers["Access-Control-Allow-Headers"] = "Content-Type, Authorization"
39
+ response.headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS, PUT, DELETE"
40
+ return response
41
+
42
+ @app.after_request
43
+ def add_cors_headers(response):
44
+ response.headers["Access-Control-Allow-Origin"] = "*"
45
+ response.headers["Access-Control-Allow-Headers"] = "Content-Type, Authorization"
46
+ response.headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS"
47
+ return response
48
+
49
+ # ----------------------------------------------------------------------
50
+ # 全局集合:记录当前正在对话中的账号(以 email 或 phone 标识),保证同一账号同时只进行一个对话
51
+ active_accounts = set()
52
+
53
+ # ----------------------------------------------------------------------
54
+ # (1) 配置文件的读写函数
55
+ # ----------------------------------------------------------------------
56
+ CONFIG_PATH = "config.json"
57
+
58
+ def load_config():
59
+ """从环境变量加载配置"""
60
+ config = {
61
+ "keys": [],
62
+ "accounts": []
63
+ }
64
+
65
+ # 从环境变量读取API keys
66
+ api_keys = os.getenv("DEEPSEEK_API_KEYS", "").strip()
67
+ if api_keys:
68
+ config["keys"] = [k.strip() for k in api_keys.split(",") if k.strip()]
69
+
70
+ # 从环境变量读取账号信息
71
+ # 格式:
72
+ # - 使用email登录: email:password:token(可选)
73
+ # - 使用mobile登录: mobile:password:token(可选)
74
+ accounts_str = os.getenv("DEEPSEEK_ACCOUNTS", "").strip()
75
+ if accounts_str:
76
+ for acc in accounts_str.split(","):
77
+ parts = [p.strip() for p in acc.split(":") if p.strip()]
78
+ if len(parts) >= 2: # 至少需要账号和密码
79
+ account = {}
80
+ # 根据第一个参数是否包含@判断是email还是mobile
81
+ if "@" in parts[0]:
82
+ account["email"] = parts[0]
83
+ else:
84
+ account["mobile"] = parts[0]
85
+ account["password"] = parts[1]
86
+ # 如果有第三个参数,则为token
87
+ if len(parts) > 2:
88
+ account["token"] = parts[2]
89
+ config["accounts"].append(account)
90
+
91
+ return config
92
+
93
+ def save_config(cfg):
94
+ """
95
+ 由于使用环境变量,此函数仅更新内存中的CONFIG
96
+ token更新后需要手动同步到环境变量中
97
+ """
98
+ global CONFIG
99
+ CONFIG = cfg
100
+ # 可选:打印提示信息
101
+ app.logger.info("[save_config] 配置已更新(仅内存)")
102
+
103
+ CONFIG = load_config()
104
+
105
+ # ----------------------------------------------------------------------
106
+ # (2) DeepSeek 相关常量
107
+ # ----------------------------------------------------------------------
108
+ DEEPSEEK_HOST = "chat.deepseek.com"
109
+
110
+ DEEPSEEK_LOGIN_URL = f"https://{DEEPSEEK_HOST}/api/v0/users/login"
111
+ DEEPSEEK_CREATE_SESSION_URL = f"https://{DEEPSEEK_HOST}/api/v0/chat_session/create"
112
+ DEEPSEEK_CREATE_POW_URL = f"https://{DEEPSEEK_HOST}/api/v0/chat/create_pow_challenge"
113
+ DEEPSEEK_COMPLETION_URL = f"https://{DEEPSEEK_HOST}/api/v0/chat/completion"
114
+
115
+ BASE_HEADERS = {
116
+ 'Host': "chat.deepseek.com",
117
+ 'User-Agent': "DeepSeek/1.0.7 Android/34",
118
+ 'Accept': "application/json",
119
+ 'Accept-Encoding': "gzip",
120
+ 'Content-Type': "application/json",
121
+ 'x-client-platform': "android",
122
+ 'x-client-version': "1.0.7",
123
+ 'x-client-locale': "zh_CN",
124
+ 'x-rangers-id': "7883327620434123524",
125
+ 'accept-charset': "UTF-8",
126
+ }
127
+
128
+ # WASM 模块文件路径(请确保文件存在)
129
+ WASM_PATH = "sha3_wasm_bg.7b9ca65ddd.wasm"
130
+
131
+ # ----------------------------------------------------------------------
132
+ # 辅助函数:获取账号唯一标识(优先 email,否则 mobile)
133
+ # ----------------------------------------------------------------------
134
+ def get_account_identifier(account):
135
+ """返回账号的唯一标识,优先使用 email,否则使用 mobile"""
136
+ return account.get("email", "").strip() or account.get("mobile", "").strip()
137
+
138
+ # ----------------------------------------------------------------------
139
+ # (3) 登录函数:支持使用 email 或 mobile 登录
140
+ # ----------------------------------------------------------------------
141
+ def login_deepseek_via_account(account):
142
+ """使用 account 中的 email 或 mobile 登录 DeepSeek,
143
+ 成功后将返回的 token 写入 account 并保存至配置文件,返回新 token。"""
144
+ email = account.get("email", "").strip()
145
+ mobile = account.get("mobile", "").strip()
146
+ password = account.get("password", "").strip()
147
+ if not password or (not email and not mobile):
148
+ raise ValueError("账号缺少必要的登录信息(必须提供 email 或 mobile 以及 password)")
149
+
150
+ if email:
151
+ app.logger.info(f"[login_deepseek_via_account] 正在使用 email 登录账号:{email}")
152
+ payload = {
153
+ "email": email,
154
+ "mobile": "",
155
+ "password": password,
156
+ "area_code": "",
157
+ "device_id": "deepseek_to_api",
158
+ "os": "android"
159
+ }
160
+ else:
161
+ app.logger.info(f"[login_deepseek_via_account] 正在使用 mobile 登录账号:{mobile}")
162
+ payload = {
163
+ "mobile": mobile,
164
+ "area_code": None,
165
+ "password": password,
166
+ "device_id": "deepseek_to_api",
167
+ "os": "android"
168
+ }
169
+
170
+ # 增加 timeout 参数,防止请求阻塞过久
171
+ resp = requests.post(DEEPSEEK_LOGIN_URL, headers=BASE_HEADERS, json=payload, timeout=30)
172
+ app.logger.debug(f"[login_deepseek_via_account] 状态码: {resp.status_code}")
173
+ app.logger.debug(f"[login_deepseek_via_account] 响应体: {resp.text}")
174
+ resp.raise_for_status()
175
+ data = resp.json()
176
+ if data.get("code") != 0:
177
+ raise ValueError(f"登录失败, code={data.get('code')}, msg={data.get('msg')}")
178
+
179
+ new_token = data["data"]["biz_data"]["user"]["token"]
180
+ account["token"] = new_token
181
+ save_config(CONFIG)
182
+ identifier = email if email else mobile
183
+ app.logger.info(f"[login_deepseek_via_account] 成功登录账号 {identifier},token: {new_token}")
184
+ return new_token
185
+
186
+ # ----------------------------------------------------------------------
187
+ # (4) 从 accounts 中随机选择一个未忙且未尝试过的账号
188
+ # ----------------------------------------------------------------------
189
+ def choose_new_account(exclude_ids):
190
+ accounts = CONFIG.get("accounts", [])
191
+ available = [
192
+ acc for acc in accounts
193
+ if get_account_identifier(acc) not in exclude_ids and get_account_identifier(acc) not in active_accounts
194
+ ]
195
+ if available:
196
+ chosen = random.choice(available)
197
+ app.logger.info(f"[choose_new_account] 新选择账号: {get_account_identifier(chosen)}")
198
+ return chosen
199
+ app.logger.warning("[choose_new_account] 没有可用的账号")
200
+ return None
201
+
202
+ # ----------------------------------------------------------------------
203
+ # (5) 判断调用模式:配置模式 vs 用户自带 token
204
+ # ----------------------------------------------------------------------
205
+ def determine_mode_and_token():
206
+ """根据请求头 Authorization 判断使用哪种模式:
207
+ - 如果 Bearer token 出现在 CONFIG["keys"] 中,则为配置模式,从 CONFIG["accounts"] 中随机选择一个账号(排除已尝试账号),
208
+ 检查该账号是否已有 token,否则调用登录接口获取;
209
+ - 否则,直接使用请求中的 Bearer 值作为 DeepSeek token。
210
+ 结果存入 g.deepseek_token;配置模式下同时存入 g.account 与 g.tried_accounts。
211
+ """
212
+ auth_header = request.headers.get("Authorization", "")
213
+ if not auth_header.startswith("Bearer "):
214
+ return Response(json.dumps({"error": "Unauthorized: missing Bearer token."}),
215
+ status=401, mimetype="application/json")
216
+ caller_key = auth_header.replace("Bearer ", "", 1).strip()
217
+ config_keys = CONFIG.get("keys", [])
218
+ if caller_key in config_keys:
219
+ g.use_config_token = True
220
+ g.tried_accounts = [] # 初始化已尝试账号
221
+ selected_account = choose_new_account(g.tried_accounts)
222
+ if not selected_account:
223
+ return Response(json.dumps({"error": "No accounts configured."}),
224
+ status=500, mimetype="application/json")
225
+ if not selected_account.get("token", "").strip():
226
+ try:
227
+ login_deepseek_via_account(selected_account)
228
+ except Exception as e:
229
+ app.logger.error(f"[determine_mode_and_token] 账号 {get_account_identifier(selected_account)} 登录失败:{e}")
230
+ return Response(json.dumps({"error": "Account login failed."}),
231
+ status=500, mimetype="application/json")
232
+ else:
233
+ app.logger.info(f"[determine_mode_and_token] 账号 {get_account_identifier(selected_account)} 已有 token,无需重新登录")
234
+ g.deepseek_token = selected_account.get("token")
235
+ g.account = selected_account
236
+ app.logger.info(f"[determine_mode_and_token] 配置模式:使用账号 {get_account_identifier(selected_account)} 的 token")
237
+ else:
238
+ g.use_config_token = False
239
+ g.deepseek_token = caller_key
240
+ app.logger.info("[determine_mode_and_token] 使用用户自带 DeepSeek token")
241
+ return None
242
+
243
+ def get_auth_headers():
244
+ """返回 DeepSeek 请求所需的公共请求头"""
245
+ return { **BASE_HEADERS, "authorization": f"Bearer {g.deepseek_token}" }
246
+
247
+ # ----------------------------------------------------------------------
248
+ # (6) 封装对话接口调用的重试机制
249
+ # ----------------------------------------------------------------------
250
+ def call_completion_endpoint(payload, headers, stream, max_attempts=3):
251
+ attempts = 0
252
+ while attempts < max_attempts:
253
+ try:
254
+ deepseek_resp = requests.post(DEEPSEEK_COMPLETION_URL, headers=headers, json=payload, stream=stream)
255
+ except Exception as e:
256
+ app.logger.warning(f"[call_completion_endpoint] 请求异常: {e}")
257
+ time.sleep(1)
258
+ attempts += 1
259
+ continue
260
+ if deepseek_resp.status_code == 200:
261
+ return deepseek_resp
262
+ else:
263
+ app.logger.warning(f"[call_completion_endpoint] 调用对话接口失败, 状态码: {deepseek_resp.status_code}")
264
+ deepseek_resp.close()
265
+ time.sleep(1)
266
+ attempts += 1
267
+ return None
268
+
269
+ # ----------------------------------------------------------------------
270
+ # (7) 创建会话 & 获取 PoW(重试时,配置模式下错误会切换账号;用户自带 token 模式下仅重试)
271
+ # ----------------------------------------------------------------------
272
+ def create_session(max_attempts=3):
273
+ attempts = 0
274
+ while attempts < max_attempts:
275
+ headers = get_auth_headers()
276
+ try:
277
+ resp = requests.post(DEEPSEEK_CREATE_SESSION_URL, headers=headers, json={"agent": "chat"}, timeout=30)
278
+ except Exception as e:
279
+ app.logger.error(f"[create_session] 请求异常: {e}")
280
+ attempts += 1
281
+ continue
282
+ try:
283
+ data = resp.json()
284
+ except Exception as e:
285
+ app.logger.error(f"[create_session] JSON解析异常: {e}")
286
+ data = {}
287
+ if resp.status_code == 200 and data.get("code") == 0:
288
+ session_id = data["data"]["biz_data"]["id"]
289
+ app.logger.info(f"[create_session] 新会话 chat_session_id={session_id}")
290
+ resp.close()
291
+ return session_id
292
+ else:
293
+ code = data.get("code")
294
+ app.logger.warning(f"[create_session] 创建会话失败, code={code}, msg={data.get('msg')}")
295
+ resp.close()
296
+ if g.use_config_token:
297
+ current_id = get_account_identifier(g.account)
298
+ if not hasattr(g, 'tried_accounts'):
299
+ g.tried_accounts = []
300
+ if current_id not in g.tried_accounts:
301
+ g.tried_accounts.append(current_id)
302
+ new_account = choose_new_account(g.tried_accounts)
303
+ if new_account is None:
304
+ break
305
+ try:
306
+ login_deepseek_via_account(new_account)
307
+ except Exception as e:
308
+ app.logger.error(f"[create_session] 账号 {get_account_identifier(new_account)} 登录失败:{e}")
309
+ attempts += 1
310
+ continue
311
+ g.account = new_account
312
+ g.deepseek_token = new_account.get("token")
313
+ else:
314
+ attempts += 1
315
+ continue
316
+ attempts += 1
317
+ return None
318
+
319
+ # ----------------------------------------------------------------------
320
+ # (7.1) 使用 WASM 模块计算 PoW 答案的辅助函数
321
+ # ----------------------------------------------------------------------
322
+ def compute_pow_answer(algorithm: str,
323
+ challenge_str: str,
324
+ salt: str,
325
+ difficulty: int,
326
+ expire_at: int,
327
+ signature: str,
328
+ target_path: str,
329
+ wasm_path: str) -> int:
330
+ """
331
+ 使用 WASM 模块计算 DeepSeekHash 答案(answer)。
332
+ 根据 JS 逻辑:
333
+ - 拼接前缀: "{salt}_{expire_at}_"
334
+ - 将 challenge 与前缀写入 wasm 内存后调用 wasm_solve 进行求解,
335
+ - 从 wasm 内存中读取状态与求解结果,
336
+ - 若状态非 0,则返回整数形式的答案,否则返回 None。
337
+ """
338
+ if algorithm != "DeepSeekHashV1":
339
+ raise ValueError(f"不支持的算法:{algorithm}")
340
+
341
+ prefix = f"{salt}_{expire_at}_"
342
+
343
+ # --- 加载 wasm 模块 ---
344
+ store = Store()
345
+ linker = Linker(store.engine)
346
+ try:
347
+ with open(wasm_path, "rb") as f:
348
+ wasm_bytes = f.read()
349
+ except Exception as e:
350
+ raise RuntimeError(f"加载 wasm 文件失败: {wasm_path}, 错误: {e}")
351
+ module = Module(store.engine, wasm_bytes)
352
+ instance = linker.instantiate(store, module)
353
+ exports = instance.exports(store)
354
+ try:
355
+ memory = exports["memory"]
356
+ add_to_stack = exports["__wbindgen_add_to_stack_pointer"]
357
+ alloc = exports["__wbindgen_export_0"]
358
+ wasm_solve = exports["wasm_solve"]
359
+ except KeyError as e:
360
+ raise RuntimeError(f"缺少 wasm 导出函数: {e}")
361
+
362
+ def write_memory(offset: int, data: bytes):
363
+ size = len(data)
364
+ base_addr = ctypes.cast(memory.data_ptr(store), ctypes.c_void_p).value
365
+ ctypes.memmove(base_addr + offset, data, size)
366
+
367
+ def read_memory(offset: int, size: int) -> bytes:
368
+ base_addr = ctypes.cast(memory.data_ptr(store), ctypes.c_void_p).value
369
+ return ctypes.string_at(base_addr + offset, size)
370
+
371
+ def encode_string(text: str):
372
+ data = text.encode("utf-8")
373
+ length = len(data)
374
+ ptr_val = alloc(store, length, 1)
375
+ ptr = int(ptr_val.value) if hasattr(ptr_val, "value") else int(ptr_val)
376
+ write_memory(ptr, data)
377
+ return ptr, length
378
+
379
+ # 1. 申请 16 字节栈空间
380
+ retptr = add_to_stack(store, -16)
381
+ # 2. 编码 challenge 与 prefix 到 wasm 内存中
382
+ ptr_challenge, len_challenge = encode_string(challenge_str)
383
+ ptr_prefix, len_prefix = encode_string(prefix)
384
+ # 3. 调用 wasm_solve(注意:difficulty 以 float 形式传入)
385
+ wasm_solve(store, retptr, ptr_challenge, len_challenge, ptr_prefix, len_prefix, float(difficulty))
386
+ # 4. 从 retptr 处读取 4 字节状态和 8 字节求解结果
387
+ status_bytes = read_memory(retptr, 4)
388
+ if len(status_bytes) != 4:
389
+ add_to_stack(store, 16)
390
+ raise RuntimeError("读取状态字节失败")
391
+ status = struct.unpack("<i", status_bytes)[0]
392
+ value_bytes = read_memory(retptr + 8, 8)
393
+ if len(value_bytes) != 8:
394
+ add_to_stack(store, 16)
395
+ raise RuntimeError("读取结果字节失败")
396
+ value = struct.unpack("<d", value_bytes)[0]
397
+ # 5. 恢复栈指针
398
+ add_to_stack(store, 16)
399
+ if status == 0:
400
+ return None
401
+ return int(value)
402
+
403
+ # ----------------------------------------------------------------------
404
+ # (7.2) 获取 PoW 响应,融合计算 answer 逻辑
405
+ # ----------------------------------------------------------------------
406
+ def get_pow_response(max_attempts=3):
407
+ attempts = 0
408
+ while attempts < max_attempts:
409
+ headers = get_auth_headers()
410
+ try:
411
+ resp = requests.post(DEEPSEEK_CREATE_POW_URL, headers=headers, json={"target_path": "/api/v0/chat/completion"}, timeout=30)
412
+ except Exception as e:
413
+ app.logger.error(f"[get_pow_response] 请求异常: {e}")
414
+ attempts += 1
415
+ continue
416
+ try:
417
+ data = resp.json()
418
+ except Exception as e:
419
+ app.logger.error(f"[get_pow_response] JSON解析异常: {e}")
420
+ data = {}
421
+ if resp.status_code == 200 and data.get("code") == 0:
422
+ challenge = data["data"]["biz_data"]["challenge"]
423
+ difficulty = challenge.get("difficulty", 144000)
424
+ expire_at = challenge.get("expire_at", 1680000000)
425
+ try:
426
+ answer = compute_pow_answer(
427
+ challenge["algorithm"],
428
+ challenge["challenge"],
429
+ challenge["salt"],
430
+ difficulty,
431
+ expire_at,
432
+ challenge["signature"],
433
+ challenge["target_path"],
434
+ WASM_PATH
435
+ )
436
+ except Exception as e:
437
+ app.logger.error(f"[get_pow_response] PoW 答案计算异常: {e}")
438
+ answer = None
439
+ if answer is None:
440
+ app.logger.warning("[get_pow_response] PoW 答案计算失败,重试中...")
441
+ resp.close()
442
+ attempts += 1
443
+ continue
444
+
445
+ pow_dict = {
446
+ "algorithm": challenge["algorithm"],
447
+ "challenge": challenge["challenge"],
448
+ "salt": challenge["salt"],
449
+ "answer": answer, # 整数形式答案
450
+ "signature": challenge["signature"],
451
+ "target_path": challenge["target_path"]
452
+ }
453
+ pow_str = json.dumps(pow_dict, separators=(',', ':'), ensure_ascii=False)
454
+ encoded = base64.b64encode(pow_str.encode("utf-8")).decode("utf-8").rstrip("=")
455
+ resp.close()
456
+ return encoded
457
+ else:
458
+ code = data.get("code")
459
+ app.logger.warning(f"[get_pow_response] 获取 PoW 失败, code={code}, msg={data.get('msg')}")
460
+ resp.close()
461
+ if g.use_config_token:
462
+ current_id = get_account_identifier(g.account)
463
+ if not hasattr(g, 'tried_accounts'):
464
+ g.tried_accounts = []
465
+ if current_id not in g.tried_accounts:
466
+ g.tried_accounts.append(current_id)
467
+ new_account = choose_new_account(g.tried_accounts)
468
+ if new_account is None:
469
+ break
470
+ try:
471
+ login_deepseek_via_account(new_account)
472
+ except Exception as e:
473
+ app.logger.error(f"[get_pow_response] 账号 {get_account_identifier(new_account)} 登录失败:{e}")
474
+ attempts += 1
475
+ continue
476
+ g.account = new_account
477
+ g.deepseek_token = new_account.get("token")
478
+ else:
479
+ attempts += 1
480
+ continue
481
+ attempts += 1
482
+ return None
483
+
484
+ # ----------------------------------------------------------------------
485
+ # (8) 路由:/v1/models(模拟 OpenAI 模型列表)
486
+ # ----------------------------------------------------------------------
487
+ @app.route("/hf/v1/models", methods=["GET"])
488
+ def list_models():
489
+ app.logger.info("[list_models] 用户请求 /v1/models")
490
+ models_list = [
491
+ {
492
+ "id": "DeepSeek-R1",
493
+ "object": "model",
494
+ "created": 1677610602,
495
+ "owned_by": "deepseek",
496
+ "permission": []
497
+ },
498
+ {
499
+ "id": "deepseek-reasoner",
500
+ "object": "model",
501
+ "created": 1677610602,
502
+ "owned_by": "deepseek",
503
+ "permission": []
504
+ },
505
+ {
506
+ "id": "DeepSeek-V3",
507
+ "object": "model",
508
+ "created": 1677610602,
509
+ "owned_by": "deepseek",
510
+ "permission": []
511
+ },
512
+ {
513
+ "id": "deepseek-chat",
514
+ "object": "model",
515
+ "created": 1677610602,
516
+ "owned_by": "deepseek",
517
+ "permission": []
518
+ }
519
+ ]
520
+ data = {"object": "list", "data": models_list}
521
+ return jsonify(data), 200
522
+
523
+ # ----------------------------------------------------------------------
524
+ # (新增) 消息预处理函数,将多轮对话合并成最终 prompt
525
+ # ----------------------------------------------------------------------
526
+ def messages_prepare(messages: list) -> str:
527
+ """处理消息列表,合并连续相同角色的消息,并添加角色标签:
528
+ - 对于 assistant 消息,加上 <|Assistant|> 前缀及 <|end▁of▁sentence|> 结束标签;
529
+ - 对于 user/system 消息(除第一条外)加上 <|User|> 前缀;
530
+ - 如果消息 content 为数组,则提取其中 type 为 "text" 的部分;
531
+ - 最后移除 markdown 图片格式的内容。
532
+ """
533
+ processed = []
534
+ for m in messages:
535
+ role = m.get("role", "")
536
+ content = m.get("content", "")
537
+ if isinstance(content, list):
538
+ texts = [item.get("text", "") for item in content if item.get("type") == "text"]
539
+ text = "\n".join(texts)
540
+ else:
541
+ text = str(content)
542
+ processed.append({"role": role, "text": text})
543
+ if not processed:
544
+ return ""
545
+ # 合并连续同一角色的消息
546
+ merged = [processed[0]]
547
+ for msg in processed[1:]:
548
+ if msg["role"] == merged[-1]["role"]:
549
+ merged[-1]["text"] += "\n\n" + msg["text"]
550
+ else:
551
+ merged.append(msg)
552
+ # 添加标签
553
+ parts = []
554
+ for idx, block in enumerate(merged):
555
+ role = block["role"]
556
+ text = block["text"]
557
+ if role == "assistant":
558
+ parts.append(f"<|Assistant|>{text}<|end▁of▁sentence|>")
559
+ elif role in ("user", "system"):
560
+ if idx > 0:
561
+ parts.append(f"<|User|>{text}")
562
+ else:
563
+ parts.append(text)
564
+ else:
565
+ parts.append(text)
566
+ final_prompt = "".join(parts)
567
+ # 移除 markdown 图片格式:
568
+ final_prompt = re.sub(r"!", "", final_prompt)
569
+ return final_prompt
570
+
571
+ # ----------------------------------------------------------------------
572
+ # (10) 路由:/v1/chat/completions
573
+ # ----------------------------------------------------------------------
574
+ @app.route("/hf/v1/chat/completions", methods=["POST"])
575
+ def chat_completions():
576
+ mode_resp = determine_mode_and_token()
577
+ if mode_resp:
578
+ return mode_resp
579
+
580
+ # 如果使用配置模式,检查账号是否正忙;如果忙则尝试切换账号
581
+ if g.use_config_token:
582
+ account_id = get_account_identifier(g.account)
583
+ if account_id in active_accounts:
584
+ g.tried_accounts.append(account_id)
585
+ new_account = choose_new_account(g.tried_accounts)
586
+ if new_account is None:
587
+ return jsonify({"error": "All accounts are busy."}), 503
588
+ try:
589
+ login_deepseek_via_account(new_account)
590
+ except Exception as e:
591
+ return jsonify({"error": "Account login failed."}), 500
592
+ g.account = new_account
593
+ g.deepseek_token = new_account.get("token")
594
+ account_id = get_account_identifier(new_account)
595
+ active_accounts.add(account_id)
596
+ try:
597
+ req_data = request.json or {}
598
+ app.logger.info(f"[chat_completions] 收到请求: {req_data}")
599
+ model = req_data.get("model")
600
+ messages = req_data.get("messages", [])
601
+ if not model or not messages:
602
+ return jsonify({"error": "Request must include 'model' and 'messages'."}), 400
603
+
604
+ # 判断是否启用“思考”功能(这里根据模型名称判断)
605
+ model_lower = model.lower()
606
+ if model_lower in ["deepseek-v3", "deepseek-chat"]:
607
+ thinking_enabled = False
608
+ elif model_lower in ["deepseek-r1", "deepseek-reasoner"]:
609
+ thinking_enabled = True
610
+ else:
611
+ return jsonify({"error": f"Model '{model}' is not available."}), 503
612
+
613
+ # 使用 messages_prepare 函数构造最终 prompt
614
+ final_prompt = messages_prepare(messages)
615
+ app.logger.debug(f"[chat_completions] 最终 Prompt: {final_prompt}")
616
+
617
+ session_id = create_session()
618
+ if not session_id:
619
+ return jsonify({"error": "invalid token."}), 401
620
+
621
+ pow_resp = get_pow_response()
622
+ if not pow_resp:
623
+ return jsonify({"error": "Failed to get PoW (invalid token or unknown error)."}), 401
624
+ app.logger.info(f"获取 PoW 成功: {pow_resp}")
625
+
626
+ headers = {
627
+ **get_auth_headers(),
628
+ "x-ds-pow-response": pow_resp
629
+ }
630
+ payload = {
631
+ "chat_session_id": session_id,
632
+ "parent_message_id": None,
633
+ "prompt": final_prompt,
634
+ "ref_file_ids": [],
635
+ "thinking_enabled": thinking_enabled,
636
+ "search_enabled": False
637
+ }
638
+ app.logger.debug(f"[chat_completions] -> {DEEPSEEK_COMPLETION_URL}, payload={payload}")
639
+
640
+ deepseek_resp = call_completion_endpoint(payload, headers, stream=bool(req_data.get("stream", False)), max_attempts=3)
641
+ if not deepseek_resp:
642
+ return jsonify({"error": "Failed to get completion."}), 500
643
+
644
+ created_time = int(time.time())
645
+ completion_id = f"{session_id}"
646
+
647
+ # 流式响应:SSE 格式返回事件流
648
+ if bool(req_data.get("stream", False)):
649
+ if deepseek_resp.status_code != 200:
650
+ deepseek_resp.close()
651
+ return Response(deepseek_resp.content,
652
+ status=deepseek_resp.status_code,
653
+ mimetype="application/json")
654
+
655
+ def sse_stream():
656
+ try:
657
+ final_text = ""
658
+ final_thinking = ""
659
+ first_chunk_sent = False
660
+ for raw_line in deepseek_resp.iter_lines(chunk_size=512):
661
+ try:
662
+ line = raw_line.decode("utf-8")
663
+ except Exception as e:
664
+ app.logger.warning(f"[sse_stream] 解码失败: {e}")
665
+ continue
666
+ if not line:
667
+ continue
668
+ if line.startswith("data:"):
669
+ data_str = line[5:].strip()
670
+ if data_str == "[DONE]":
671
+ prompt_tokens = len(tokenizer.encode(final_prompt))
672
+ completion_tokens = len(tokenizer.encode(final_text))
673
+ usage = {
674
+ "prompt_tokens": prompt_tokens,
675
+ "completion_tokens": completion_tokens,
676
+ "total_tokens": prompt_tokens + completion_tokens
677
+ }
678
+ finish_chunk = {
679
+ "id": completion_id,
680
+ "object": "chat.completion.chunk",
681
+ "created": created_time,
682
+ "model": model,
683
+ "choices": [
684
+ {"delta": {}, "index": 0, "finish_reason": "stop"}
685
+ ],
686
+ "usage": usage
687
+ }
688
+ yield f"data: {json.dumps(finish_chunk, ensure_ascii=False)}\n\n"
689
+ yield "data: [DONE]\n\n"
690
+ break
691
+ try:
692
+ chunk = json.loads(data_str)
693
+ app.logger.debug(f"[sse_stream] 解析到 chunk: {chunk}")
694
+ except Exception as e:
695
+ app.logger.warning(f"[sse_stream] 无法解析: {data_str}, 错误: {e}")
696
+ continue
697
+ new_choices = []
698
+ for choice in chunk.get("choices", []):
699
+ delta = choice.get("delta", {})
700
+ ctype = delta.get("type")
701
+ ctext = delta.get("content", "")
702
+ if ctype == "thinking":
703
+ if thinking_enabled:
704
+ final_thinking += ctext
705
+ elif ctype == "text":
706
+ final_text += ctext
707
+ delta_obj = {}
708
+ if not first_chunk_sent:
709
+ delta_obj["role"] = "assistant"
710
+ first_chunk_sent = True
711
+ if ctype == "thinking":
712
+ if thinking_enabled:
713
+ delta_obj["reasoning_content"] = ctext
714
+ elif ctype == "text":
715
+ delta_obj["content"] = ctext
716
+ if delta_obj:
717
+ new_choices.append({"delta": delta_obj, "index": choice.get("index", 0)})
718
+ if new_choices:
719
+ out_chunk = {
720
+ "id": completion_id,
721
+ "object": "chat.completion.chunk",
722
+ "created": created_time,
723
+ "model": model,
724
+ "choices": new_choices
725
+ }
726
+ yield f"data: {json.dumps(out_chunk, ensure_ascii=False)}\n\n"
727
+ except Exception as e:
728
+ app.logger.error(f"[sse_stream] 异常: {e}")
729
+ finally:
730
+ deepseek_resp.close()
731
+ if g.use_config_token:
732
+ active_accounts.discard(get_account_identifier(g.account))
733
+ return Response(stream_with_context(sse_stream()), content_type="text/event-stream")
734
+ else:
735
+ # 非流式响应处理
736
+ think_list = []
737
+ text_list = []
738
+ try:
739
+ for raw_line in deepseek_resp.iter_lines(chunk_size=512):
740
+ try:
741
+ line = raw_line.decode("utf-8")
742
+ except Exception as e:
743
+ app.logger.warning(f"[chat_completions] 解码失败: {e}")
744
+ continue
745
+ if not line:
746
+ continue
747
+ if line.startswith("data:"):
748
+ data_str = line[5:].strip()
749
+ if data_str == "[DONE]":
750
+ break
751
+ try:
752
+ chunk = json.loads(data_str)
753
+ app.logger.debug(f"[chat_completions] 非流式 chunk: {chunk}")
754
+ except Exception as e:
755
+ app.logger.warning(f"[chat_completions] 无法解析: {data_str}, 错误: {e}")
756
+ continue
757
+ for choice in chunk.get("choices", []):
758
+ delta = choice.get("delta", {})
759
+ ctype = delta.get("type")
760
+ if ctype == "thinking" and thinking_enabled:
761
+ think_list.append(delta.get("content", ""))
762
+ elif ctype == "text":
763
+ text_list.append(delta.get("content", ""))
764
+ finally:
765
+ deepseek_resp.close()
766
+ final_reasoning = "".join(think_list)
767
+ final_content = "".join(text_list)
768
+ prompt_tokens = len(tokenizer.encode(final_prompt))
769
+ completion_tokens = len(tokenizer.encode(final_content))
770
+ total_tokens = prompt_tokens + completion_tokens
771
+ result = {
772
+ "id": completion_id,
773
+ "object": "chat.completion",
774
+ "created": created_time,
775
+ "model": model,
776
+ "choices": [
777
+ {
778
+ "index": 0,
779
+ "message": {
780
+ "role": "assistant",
781
+ "content": final_content,
782
+ "reasoning_content": final_reasoning
783
+ },
784
+ "finish_reason": "stop"
785
+ }
786
+ ],
787
+ "usage": {
788
+ "prompt_tokens": prompt_tokens,
789
+ "completion_tokens": completion_tokens,
790
+ "total_tokens": total_tokens
791
+ }
792
+ }
793
+ return jsonify(result), 200
794
+ finally:
795
+ if g.use_config_token:
796
+ active_accounts.discard(get_account_identifier(g.account))
797
+
798
+ # ----------------------------------------------------------------------
799
+ # (11) 路由:/
800
+ # ----------------------------------------------------------------------
801
+ @app.route("/")
802
+ def index():
803
+ return render_template("welcome.html")
804
+
805
+ # ----------------------------------------------------------------------
806
+ # 启动 Flask 应用(直接使用 Flask 内置服务器)
807
+ # ----------------------------------------------------------------------
808
+ if __name__ == "__main__":
809
+ app.run(host="0.0.0.0", port=7860, debug=False)
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ flask
2
+ waitress
3
+ wasmtime
4
+ curl_cffi
5
+ transformers
6
+ protobuf
7
+ tiktoken