initial commit

This commit is contained in:
2026-05-14 18:38:09 +03:00
commit f41d64f39b
32 changed files with 1469 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
from .client import Client
__all__ = ['Client']
Binary file not shown.
Binary file not shown.
Binary file not shown.
+358
View File
@@ -0,0 +1,358 @@
import json
import gzip
import io
import requests
import base64
from typing import Optional, Dict, Any, Callable
from .models import PowChallenge, CompletionData
class Client:
# API endpoints
AUTH_URL = "https://chat.deepseek.com/api/v0/users/login"
CHAT_CREATE_URL = "https://chat.deepseek.com/api/v0/chat_session/create"
CHAT_DELETE_URL = "https://chat.deepseek.com/api/v0/chat_session/delete"
CHAT_EDIT_URL = "https://chat.deepseek.com/api/v0/chat_session/update_title"
CHAT_LIST_URL = "https://chat.deepseek.com/api/v0/chat_session/fetch_page"
COMPLETION_URL = "https://chat.deepseek.com/api/v0/chat/completion"
HISTORY_BASE_URL = "https://chat.deepseek.com/api/v0/chat/history_messages?chat_session_id="
LOGOUT_URL = "https://chat.deepseek.com/api/v0/users/logout"
POW_URL = "https://chat.deepseek.com/api/v0/chat/create_pow_challenge"
PROFILE_URL = "https://chat.deepseek.com/api/v0/users/current"
QUOTA_URL = "https://chat.deepseek.com/api/v0/users/feature_quota"
CHAT_CREATE_BODY = '{"agent":"chat"}'
def __init__(self, pow_solver, api_key: str):
self.api_key = api_key
self.pow_solver = pow_solver
self.http_client = requests.Session()
# Configure TLS with modern user agent
self.http_client.headers.update({
'User-Agent': 'DeepSeek/2.0.0 Android/31', # Updated version
})
def _apply_headers(self, req: requests.Request, body_len: int = 0) -> None:
"""Apply standard headers to request"""
if 'User-Agent' not in req.headers:
req.headers['User-Agent'] = "DeepSeek/2.0.0 Android/31"
req.headers['Content-Type'] = "application/json"
if body_len > 0:
req.headers['Content-Length'] = str(body_len)
if self.api_key:
req.headers['Authorization'] = f"Bearer {self.api_key}"
req.headers['Accept'] = "application/json"
req.headers['Accept-Encoding'] = "gzip"
req.headers['Accept-Charset'] = "UTF-8"
req.headers['X-Client-Locale'] = "en_US"
req.headers['X-Client-Version'] = "2.0.0"
req.headers['X-Client-Platform'] = "android"
def _apply_pow_header(self, req: requests.Request, answer: int, pow: PowChallenge) -> None:
"""Apply Proof-of-Work header to request"""
header = {
"algorithm": pow.algorithm,
"challenge": pow.challenge,
"salt": pow.salt,
"signature": pow.signature,
"answer": answer,
"target_path": pow.target_path,
}
encoded_header = base64.b64encode(json.dumps(header).encode()).decode()
req.headers['X-Ds-Pow-Response'] = encoded_header
def _unmarshal(self, body: bytes) -> Dict[str, Any]:
"""Parse gzipped JSON response"""
try:
decompressed = gzip.decompress(body)
except:
decompressed = body
return json.loads(decompressed.decode('utf-8'))
def _execute(self, url: str, body: str, method: str) -> Dict[str, Any]:
"""Send a request"""
if body and method != "GET":
req = requests.Request(method, url, data=body)
else:
req = requests.Request(method, url)
prepared = self.http_client.prepare_request(req)
self._apply_headers(prepared, len(body) if body else 0)
print(f"[DEBUG] Sending {method} to {url}")
resp = self.http_client.send(prepared)
print(f"[DEBUG] Response status: {resp.status_code}")
print(f"[DEBUG] Response headers: {resp.headers}")
# Check HTTP status code
if resp.status_code >= 400:
try:
error_data = self._unmarshal(resp.content)
error_msg = error_data.get('msg', error_data.get('message', 'Unknown error'))
except Exception as parse_err:
error_msg = resp.text or f"HTTP {resp.status_code}"
print(f"[DEBUG] Failed to parse error response: {parse_err}")
print(f"[DEBUG] API Error: {error_msg}")
raise Exception(f"API Error ({resp.status_code}): {error_msg}")
result = self._unmarshal(resp.content)
print(f"[DEBUG] Response data: {result}")
# Check for API-level error codes in the response
if result and isinstance(result, dict):
code = result.get('code')
if code and code != 0: # 0 or missing code = success
msg = result.get('msg', 'Unknown error')
print(f"[DEBUG] API returned error code {code}: {msg}")
raise Exception(f"API Error ({code}): {msg}")
return result
def create_chat(self) -> str:
"""Create a new chat session. Returns UUID of a new chat session."""
data = self._execute(self.CHAT_CREATE_URL, self.CHAT_CREATE_BODY, "POST")
try:
if not data or not data.get('data'):
raise Exception(f"Invalid response structure - missing data field. Response: {data}")
biz_data = data['data'].get('biz_data')
if not biz_data:
raise Exception(f"Invalid response structure - missing biz_data. Response: {data}")
# Try both possible structures
if 'chat_session' in biz_data:
return biz_data['chat_session']['id']
elif 'id' in biz_data:
return biz_data['id']
else:
raise Exception(f"Could not find chat ID in response. biz_data keys: {biz_data.keys()}")
except (KeyError, TypeError) as e:
raise Exception(f"Failed to create chat: invalid response structure - {str(e)}. Response: {data}")
def get_all_chats(self) -> list:
"""Get all chat sessions"""
data = self._execute(self.CHAT_LIST_URL, "", "GET")
return data['data']['biz_data']['chat_sessions']
def change_title(self, chat_session_id: str, title: str) -> None:
"""Change chat session title"""
body = json.dumps({
"chat_session_id": chat_session_id,
"title": title,
})
self._execute(self.CHAT_EDIT_URL, body, "POST")
def delete_chat_session(self, chat_session_id: str) -> None:
"""Delete a chat session"""
body = json.dumps({
"chat_session_id": chat_session_id,
})
self._execute(self.CHAT_DELETE_URL, body, "POST")
def get_message_history(self, chat_session_id: str) -> Dict[str, Any]:
"""Get message history for a chat session"""
data = self._execute(self.HISTORY_BASE_URL + chat_session_id, "", "GET")
return data['data']['biz_data']
def login(self, email: str, password: str, device_id: str) -> str:
"""Login and get API token"""
body = json.dumps({
"email": email,
"password": password,
"device_id": device_id,
"os": "android",
})
data = self._execute(self.AUTH_URL, body, "POST")
return data['data']['biz_data']['user']['token']
def logout(self) -> None:
"""Logout"""
self._execute(self.LOGOUT_URL, "", "POST")
def get_profile(self) -> Dict[str, Any]:
"""Get user profile"""
data = self._execute(self.PROFILE_URL, "", "GET")
return data['data']['biz_data']
def get_quota(self) -> Dict[str, Any]:
"""Get thinking quota"""
data = self._execute(self.QUOTA_URL, "", "GET")
return data['data']['biz_data']['thinking']
def _get_pow(self, endpoint: str) -> PowChallenge:
"""Get Proof-of-Work challenge"""
body = json.dumps({
"target_path": endpoint,
})
data = self._execute(self.POW_URL, body, "POST")
challenge = data['data']['biz_data']['challenge']
return PowChallenge(
algorithm=challenge.get('algorithm', ''),
challenge=challenge.get('challenge', ''),
salt=challenge.get('salt', ''),
signature=challenge.get('signature', ''),
target_path=challenge.get('target_path', ''),
expire_at=challenge.get('expire_at', 0),
)
def completion(
self,
chat_session_id: str,
parent_message: str,
prompt: str,
think: bool,
search: bool,
response_callback: Callable[[str], None],
) -> None:
"""Send completion request and stream responses"""
print(f"[DEBUG] Starting completion for chat {chat_session_id}")
think = False
search = False
pow = self._get_pow("/api/v0/chat/completion")
print(f"[DEBUG] Got PoW challenge")
answer = self.pow_solver.calculate_hash(
pow.challenge,
pow.salt,
pow.expire_at,
pow.expire_at,
)
print(f"[DEBUG] Calculated PoW answer: {answer}")
completion_data = CompletionData(
chat_session_id=chat_session_id,
prompt=prompt,
thinking_enabled=think,
search_enabled=search,
parent_message_id=None,
ref_file_ids=[],
)
if parent_message:
try:
completion_data.parent_message_id = int(parent_message)
except ValueError:
pass
body = json.dumps(completion_data.to_dict())
print(f"[DEBUG] Completion request body: {body}")
req = requests.Request("POST", self.COMPLETION_URL, data=body)
prepared = self.http_client.prepare_request(req)
self._apply_headers(prepared, len(body))
self._apply_pow_header(prepared, int(answer), pow)
print(f"[DEBUG] Sending completion request to {self.COMPLETION_URL}")
print(f"[DEBUG] thinking_enabled={think}, search_enabled={search}")
resp = self.http_client.send(prepared, stream=True)
print(f"[DEBUG] Completion response status: {resp.status_code}")
print(f"[DEBUG] Completion response headers: {dict(resp.headers)}")
if resp.status_code != 200:
try:
content = resp.content.decode('utf-8')
except:
content = str(resp.content)
print(f"[DEBUG] Error response: {content}")
raise Exception(f"Completion failed with status {resp.status_code}: {content}")
self._parse_events(resp, response_callback)
def _parse_events(self, resp, response_callback: Callable[[str], None]) -> None:
"""Parse SSE events from response"""
print(f"[DEBUG] Starting to parse events")
line_count = 0
events_received = []
think = False
search = False
raw_events_logged = 0
def emit_value(value: Any) -> None:
if isinstance(value, str):
if value == "FINISHED":
return
response_callback(value)
return
if isinstance(value, list):
for item in value:
emit_value(item)
return
if isinstance(value, dict):
if 'content' in value and isinstance(value['content'], str):
response_callback(value['content'])
for nested_value in value.values():
if isinstance(nested_value, (dict, list)):
emit_value(nested_value)
try:
for line in resp.iter_lines():
line_count += 1
if not line:
continue
line = line.decode('utf-8').strip() if isinstance(line, bytes) else line.strip()
if line.startswith("event: "):
continue
raw = line[6:] if line.startswith("data: ") else line
if not raw:
continue
try:
data = json.loads(raw)
except json.JSONDecodeError as e:
print(f"[DEBUG] Failed to parse JSON on line {line_count}: {e}")
print(f"[DEBUG] Raw line: {raw}")
continue
if raw_events_logged < 5:
print(f"[DEBUG] Raw event #{raw_events_logged + 1}: {raw}")
raw_events_logged += 1
p = data.get('p', '')
v = data.get('v')
events_received.append(p)
print(f"[DEBUG] Parsed event type: {p}, has value: {v is not None}")
if p:
if p == "response/search_status":
if not search:
response_callback("\n<thinking>\n")
search = True
if p == "response/thinking_content":
if not search:
response_callback("\n<thinking>\n")
response_callback("\n\n")
think = True
elif p == "response/content":
if think:
response_callback("\n</thinking>\n")
if isinstance(v, dict):
fragments = v.get('response', {}).get('message', {}).get('fragments', [])
if isinstance(fragments, list) and fragments:
for fragment in fragments:
if isinstance(fragment, dict):
fragment_content = fragment.get('content')
if isinstance(fragment_content, str):
response_callback(fragment_content)
elif fragment_content is not None:
emit_value(fragment_content)
else:
emit_value(v)
else:
emit_value(v)
print(f"[DEBUG] Finished parsing events. Events received: {events_received}")
except Exception as e:
print(f"[DEBUG] Error in _parse_events: {e}")
print(f"[DEBUG] Line count: {line_count}")
print(f"[DEBUG] Events received so far: {events_received}")
raise
+149
View File
@@ -0,0 +1,149 @@
from dataclasses import dataclass, field
from typing import List, Optional, Dict, Any, Union
@dataclass
class SearchResponse:
url: str
title: str
snippet: str
cite_index: Optional[int] = None
published_at: Optional[Any] = None
site_name: Optional[Any] = None
site_icon: str = ""
@dataclass
class ChatSession:
id: str
seq_id: int
title: Optional[str]
title_type: Optional[str]
updated_at: float
agent: str
version: int
current_message_id: Optional[int]
inserted_at: float
character: Optional[str] = None
@staticmethod
def from_dict(data: Dict[str, Any]) -> 'ChatSession':
return ChatSession(
id=data.get('id', ''),
seq_id=data.get('seq_id', 0),
title=data.get('title'),
title_type=data.get('title_type'),
updated_at=data.get('updated_at', 0),
agent=data.get('agent', ''),
version=data.get('version', 0),
current_message_id=data.get('current_message_id'),
inserted_at=data.get('inserted_at', 0),
character=data.get('character'),
)
@dataclass
class ChatMessage:
message_id: int
parent_id: Optional[int]
model: str
role: str
content: str
thinking_enabled: bool
thinking_content: Optional[str]
thinking_elapsed_secs: Optional[int]
ban_edit: bool
ban_regenerate: bool
status: str
accumulated_token_usage: int
files: List[Any] = field(default_factory=list)
tips: List[Any] = field(default_factory=list)
inserted_at: float = 0.0
search_enabled: bool = False
search_status: Optional[str] = None
search_results: List[SearchResponse] = field(default_factory=list)
@dataclass
class ChatHistory:
chat_session: ChatSession
chat_messages: List[ChatMessage]
cache_valid: bool
route_id: Optional[Any]
@dataclass
class PowChallenge:
algorithm: str
challenge: str
salt: str
signature: str
target_path: str
expire_at: int = 0
@dataclass
class CompletionData:
chat_session_id: str
prompt: str
thinking_enabled: bool = False
search_enabled: bool = False
parent_message_id: Optional[int] = None
ref_file_ids: List[str] = field(default_factory=list)
def to_dict(self) -> Dict[str, Any]:
data = {
'chat_session_id': self.chat_session_id,
'prompt': self.prompt,
'thinking_enabled': self.thinking_enabled,
'search_enabled': self.search_enabled,
'ref_file_ids': self.ref_file_ids,
}
if self.parent_message_id is not None:
data['parent_message_id'] = self.parent_message_id
else:
data['parent_message_id'] = None
return data
# Response wrappers for API
@dataclass
class AuthResponse:
code: int
msg: str
data: Dict[str, Any]
@dataclass
class ChatCreateResponse:
code: int
msg: str
data: Dict[str, Any]
@dataclass
class ChatEditResponse:
code: int
msg: str
data: Dict[str, Any]
@dataclass
class NullResponse:
code: int
msg: str
data: Dict[str, Any]
@dataclass
class ProfileResponse:
code: int
msg: str
data: Dict[str, Any]
@dataclass
class QuotaResponse:
code: int
msg: str
data: Dict[str, Any]
+3
View File
@@ -0,0 +1,3 @@
"""
API utilities - empty utilities for compatibility
"""