StarrySkyWorld commited on
Commit
b9a8e8a
Β·
verified Β·
1 Parent(s): 8e6ab4f

Create main.py

Browse files
Files changed (1) hide show
  1. main.py +394 -0
main.py ADDED
@@ -0,0 +1,394 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """chat.z.ai reverse-engineered Python client."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import base64
7
+ import hashlib
8
+ import hmac
9
+ import json
10
+ import time
11
+ import uuid
12
+ from datetime import datetime, timezone, timedelta
13
+ from urllib.parse import urlencode
14
+
15
+ import httpx
16
+
17
+ BASE_URL = "https://chat.z.ai"
18
+ HMAC_SECRET = "key-@@@@)))()((9))-xxxx&&&%%%%%"
19
+ FE_VERSION = "prod-fe-1.0.231"
20
+ CLIENT_VERSION = "0.0.1"
21
+ DEFAULT_MODEL = "glm-5"
22
+ USER_AGENT = (
23
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
24
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
25
+ "Chrome/144.0.0.0 Safari/537.36"
26
+ )
27
+
28
+
29
+ class ZaiClient:
30
+ def __init__(self) -> None:
31
+ self.client = httpx.AsyncClient(
32
+ base_url=BASE_URL,
33
+ timeout=60.0,
34
+ headers={
35
+ "User-Agent": USER_AGENT,
36
+ "Accept-Language": "zh-CN",
37
+ "Referer": f"{BASE_URL}/",
38
+ "Origin": BASE_URL,
39
+ },
40
+ )
41
+ self.token: str | None = None
42
+ self.user_id: str | None = None
43
+ self.username: str | None = None
44
+
45
+ async def close(self) -> None:
46
+ await self.client.aclose()
47
+
48
+ # ── auth ────────────────────────────────────────────────────────
49
+
50
+ async def auth_as_guest(self) -> dict:
51
+ """GET /api/v1/auths/ β€” creates a guest session and returns user info."""
52
+ resp = await self.client.get(
53
+ "/api/v1/auths/",
54
+ headers={"Content-Type": "application/json"},
55
+ )
56
+ resp.raise_for_status()
57
+ data = resp.json()
58
+ self.token = data["token"]
59
+ self.user_id = data["id"]
60
+ self.username = data.get("name") or data.get("email", "").split("@")[0]
61
+ return data
62
+
63
+ # ── models ──────────────────────────────────────────────────────
64
+
65
+ async def get_models(self) -> list:
66
+ """GET /api/models β€” returns available model list."""
67
+ resp = await self.client.get(
68
+ "/api/models",
69
+ headers={
70
+ "Content-Type": "application/json",
71
+ "Accept": "application/json",
72
+ **({"Authorization": f"Bearer {self.token}"} if self.token else {}),
73
+ },
74
+ )
75
+ resp.raise_for_status()
76
+ return resp.json()
77
+
78
+ # ── chat CRUD ───────────────────────────────────────────────────
79
+
80
+ async def create_chat(self, user_message: str, model: str = DEFAULT_MODEL) -> dict:
81
+ """POST /api/v1/chats/new β€” creates a new chat session."""
82
+ msg_id = str(uuid.uuid4())
83
+ ts = int(time.time())
84
+ body = {
85
+ "chat": {
86
+ "id": "",
87
+ "title": "ζ–°θŠε€©",
88
+ "models": [model],
89
+ "params": {},
90
+ "history": {
91
+ "messages": {
92
+ msg_id: {
93
+ "id": msg_id,
94
+ "parentId": None,
95
+ "childrenIds": [],
96
+ "role": "user",
97
+ "content": user_message,
98
+ "timestamp": ts,
99
+ "models": [model],
100
+ }
101
+ },
102
+ "currentId": msg_id,
103
+ },
104
+ "tags": [],
105
+ "flags": [],
106
+ "features": [
107
+ {
108
+ "type": "tool_selector",
109
+ "server": "tool_selector_h",
110
+ "status": "hidden",
111
+ }
112
+ ],
113
+ "mcp_servers": [],
114
+ "enable_thinking": True,
115
+ "auto_web_search": False,
116
+ "message_version": 1,
117
+ "extra": {},
118
+ "timestamp": int(time.time() * 1000),
119
+ }
120
+ }
121
+ resp = await self.client.post(
122
+ "/api/v1/chats/new",
123
+ headers={
124
+ "Content-Type": "application/json",
125
+ "Accept": "application/json",
126
+ **({"Authorization": f"Bearer {self.token}"} if self.token else {}),
127
+ },
128
+ json=body,
129
+ )
130
+ resp.raise_for_status()
131
+ return resp.json()
132
+
133
+ # ── signature ───────────────────────────────────────────────────
134
+
135
+ @staticmethod
136
+ def _generate_signature(
137
+ sorted_payload: str, prompt: str, timestamp: str
138
+ ) -> str:
139
+ """
140
+ Two-layer HMAC-SHA256 matching DLHfQWwv.js.
141
+
142
+ 1. b64_prompt = base64(utf8(prompt))
143
+ 2. message = "{sorted_payload}|{b64_prompt}|{timestamp}"
144
+ 3. time_bucket = floor(int(timestamp) / 300_000)
145
+ 4. derived_key = HMAC-SHA256(HMAC_SECRET, str(time_bucket)) β†’ hex string
146
+ 5. signature = HMAC-SHA256(derived_key_hex_bytes, message) β†’ hex
147
+ """
148
+ b64_prompt = base64.b64encode(prompt.encode("utf-8")).decode("ascii")
149
+ message = f"{sorted_payload}|{b64_prompt}|{timestamp}"
150
+ time_bucket = int(timestamp) // (5 * 60 * 1000)
151
+
152
+ derived_key_hex = hmac.new(
153
+ HMAC_SECRET.encode("utf-8"),
154
+ str(time_bucket).encode("utf-8"),
155
+ hashlib.sha256,
156
+ ).hexdigest()
157
+
158
+ signature = hmac.new(
159
+ derived_key_hex.encode("utf-8"),
160
+ message.encode("utf-8"),
161
+ hashlib.sha256,
162
+ ).hexdigest()
163
+ return signature
164
+
165
+ def _build_query_and_signature(
166
+ self, prompt: str, chat_id: str
167
+ ) -> tuple[str, str]:
168
+ """Build the full URL query string and X-Signature header.
169
+
170
+ Returns (full_query_string, signature).
171
+ """
172
+ timestamp_ms = str(int(time.time() * 1000))
173
+ request_id = str(uuid.uuid4())
174
+
175
+ now = datetime.now(timezone.utc)
176
+
177
+ # Core params (used for sortedPayload)
178
+ core = {
179
+ "timestamp": timestamp_ms,
180
+ "requestId": request_id,
181
+ "user_id": self.user_id,
182
+ }
183
+
184
+ # sortedPayload: Object.entries(core).sort(by key).join(",")
185
+ sorted_payload = ",".join(
186
+ f"{k},{v}" for k, v in sorted(core.items(), key=lambda x: x[0])
187
+ )
188
+
189
+ # Compute signature over the prompt
190
+ signature = self._generate_signature(sorted_payload, prompt, timestamp_ms)
191
+
192
+ # Browser/device fingerprint params
193
+ extra = {
194
+ "version": CLIENT_VERSION,
195
+ "platform": "web",
196
+ "token": self.token or "",
197
+ "user_agent": USER_AGENT,
198
+ "language": "zh-CN",
199
+ "languages": "zh-CN",
200
+ "timezone": "Asia/Shanghai",
201
+ "cookie_enabled": "true",
202
+ "screen_width": "1920",
203
+ "screen_height": "1080",
204
+ "screen_resolution": "1920x1080",
205
+ "viewport_height": "919",
206
+ "viewport_width": "944",
207
+ "viewport_size": "944x919",
208
+ "color_depth": "24",
209
+ "pixel_ratio": "1.25",
210
+ "current_url": f"{BASE_URL}/c/{chat_id}",
211
+ "pathname": f"/c/{chat_id}",
212
+ "search": "",
213
+ "hash": "",
214
+ "host": "chat.z.ai",
215
+ "hostname": "chat.z.ai",
216
+ "protocol": "https:",
217
+ "referrer": "",
218
+ "title": "Z.ai - Free AI Chatbot & Agent powered by GLM-5 & GLM-4.7",
219
+ "timezone_offset": "-480",
220
+ "local_time": now.strftime("%Y-%m-%dT%H:%M:%S.")
221
+ + f"{now.microsecond // 1000:03d}Z",
222
+ "utc_time": now.strftime("%a, %d %b %Y %H:%M:%S GMT"),
223
+ "is_mobile": "false",
224
+ "is_touch": "false",
225
+ "max_touch_points": "10",
226
+ "browser_name": "Chrome",
227
+ "os_name": "Windows",
228
+ "signature_timestamp": timestamp_ms,
229
+ }
230
+
231
+ all_params = {**core, **extra}
232
+ query_string = urlencode(all_params)
233
+
234
+ return query_string, signature
235
+
236
+ # ── chat completions (SSE) ──────────────────────────────────────
237
+
238
+ async def chat_completions(
239
+ self,
240
+ chat_id: str,
241
+ messages: list[dict],
242
+ prompt: str,
243
+ *,
244
+ model: str = DEFAULT_MODEL,
245
+ parent_message_id: str | None = None,
246
+ tools: list[dict] | None = None,
247
+ ):
248
+ """POST /api/v2/chat/completions β€” streams SSE response.
249
+
250
+ Yields the full event ``data`` dict for each SSE frame.
251
+ """
252
+ query_string, signature = self._build_query_and_signature(prompt, chat_id)
253
+
254
+ msg_id = str(uuid.uuid4())
255
+ user_msg_id = str(uuid.uuid4())
256
+
257
+ now = datetime.now(timezone(timedelta(hours=8)))
258
+ variables = {
259
+ "{{USER_NAME}}": self.username or "Guest",
260
+ "{{USER_LOCATION}}": "Unknown",
261
+ "{{CURRENT_DATETIME}}": now.strftime("%Y-%m-%d %H:%M:%S"),
262
+ "{{CURRENT_DATE}}": now.strftime("%Y-%m-%d"),
263
+ "{{CURRENT_TIME}}": now.strftime("%H:%M:%S"),
264
+ "{{CURRENT_WEEKDAY}}": now.strftime("%A"),
265
+ "{{CURRENT_TIMEZONE}}": "Asia/Shanghai",
266
+ "{{USER_LANGUAGE}}": "zh-CN",
267
+ }
268
+
269
+ body = {
270
+ "stream": True,
271
+ "model": model,
272
+ "messages": messages,
273
+ "signature_prompt": prompt,
274
+ "params": {},
275
+ "extra": {},
276
+ "features": {
277
+ "image_generation": False,
278
+ "web_search": False,
279
+ "auto_web_search": False,
280
+ "preview_mode": True,
281
+ "flags": [],
282
+ "enable_thinking": True,
283
+ },
284
+ "variables": variables,
285
+ "chat_id": chat_id,
286
+ "id": msg_id,
287
+ "current_user_message_id": user_msg_id,
288
+ "current_user_message_parent_id": parent_message_id,
289
+ "background_tasks": {
290
+ "title_generation": True,
291
+ "tags_generation": True,
292
+ },
293
+ }
294
+ if tools:
295
+ body["tools"] = tools
296
+
297
+ headers = {
298
+ "Content-Type": "application/json",
299
+ "Accept": "*/*",
300
+ "Accept-Language": "zh-CN",
301
+ "X-FE-Version": FE_VERSION,
302
+ "X-Signature": signature,
303
+ **({"Authorization": f"Bearer {self.token}"} if self.token else {}),
304
+ }
305
+
306
+ url = f"{BASE_URL}/api/v2/chat/completions?{query_string}"
307
+
308
+ async with self.client.stream(
309
+ "POST", url, headers=headers, json=body,
310
+ ) as resp:
311
+ if resp.status_code != 200:
312
+ error_body = await resp.aread()
313
+ raise RuntimeError(
314
+ f"chat/completions {resp.status_code}: {error_body.decode()}"
315
+ )
316
+ async for line in resp.aiter_lines():
317
+ if not line.startswith("data: "):
318
+ continue
319
+ raw = line[6:]
320
+ if raw.strip() == "[DONE]":
321
+ return
322
+ try:
323
+ event = json.loads(raw)
324
+ except json.JSONDecodeError:
325
+ continue
326
+ data = event.get("data", {})
327
+ yield data
328
+ if data.get("done"):
329
+ return
330
+
331
+
332
+ async def main() -> None:
333
+ client = ZaiClient()
334
+ try:
335
+ # 1. Authenticate as guest
336
+ print("[1] Authenticating as guest...")
337
+ auth = await client.auth_as_guest()
338
+ print(f" user_id : {auth['id']}")
339
+ print(f" email : {auth.get('email', 'N/A')}")
340
+ print(f" token : {auth['token'][:40]}...")
341
+
342
+ # 2. Fetch models
343
+ print("\n[2] Fetching models...")
344
+ models_resp = await client.get_models()
345
+ if isinstance(models_resp, dict) and "data" in models_resp:
346
+ names = [m.get("id", m.get("name", "?")) for m in models_resp["data"]]
347
+ elif isinstance(models_resp, list):
348
+ names = [m.get("id", m.get("name", "?")) for m in models_resp]
349
+ else:
350
+ names = [str(models_resp)[:80]]
351
+ print(f" models : {', '.join(names[:10])}")
352
+
353
+ # 3. Create chat
354
+ user_message = "Hello"
355
+ print(f"\n[3] Creating chat with first message: {user_message!r}")
356
+ chat = await client.create_chat(user_message)
357
+ chat_id = chat["id"]
358
+ print(f" chat_id : {chat_id}")
359
+
360
+ # 4. Stream chat completions
361
+ print(f"\n[4] Streaming chat completions (model={DEFAULT_MODEL})...\n")
362
+ messages = [{"role": "user", "content": user_message}]
363
+
364
+ thinking_started = False
365
+ answer_started = False
366
+ async for data in client.chat_completions(
367
+ chat_id=chat_id,
368
+ messages=messages,
369
+ prompt=user_message,
370
+ ):
371
+ phase = data.get("phase", "")
372
+ delta = data.get("delta_content", "")
373
+ if phase == "thinking":
374
+ if not thinking_started:
375
+ print("[thinking] ", end="", flush=True)
376
+ thinking_started = True
377
+ print(delta, end="", flush=True)
378
+ elif phase == "answer":
379
+ if not answer_started:
380
+ if thinking_started:
381
+ print("\n")
382
+ print("[answer] ", end="", flush=True)
383
+ answer_started = True
384
+ print(delta, end="", flush=True)
385
+ elif phase == "done":
386
+ break
387
+ print("\n\n[done]")
388
+
389
+ finally:
390
+ await client.close()
391
+
392
+
393
+ if __name__ == "__main__":
394
+ asyncio.run(main())