You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
142 lines
5.8 KiB
142 lines
5.8 KiB
import base64
|
|
import hashlib
|
|
import hmac as hmac_mod
|
|
import json
|
|
import time
|
|
import uuid
|
|
from urllib.parse import urlparse, quote
|
|
|
|
|
|
class QianwenClient:
|
|
def __init__(self, cookies=None):
|
|
self.url = "https://chat2.qianwen.com/api/v2/chat"
|
|
self.share_url = "https://chat2-api.qianwen.com/api/v1/share/create"
|
|
self.ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/131.0.0.0 Safari/537.36"
|
|
self._cookie_str = ""
|
|
self._xsrf_token = ""
|
|
if cookies:
|
|
self.set_cookies(cookies)
|
|
|
|
def set_cookies(self, cookies):
|
|
self._cookie_str = "; ".join(f'{c["name"]}={c["value"]}' for c in cookies)
|
|
self._xsrf_token = next(
|
|
(c["value"] for c in cookies if c["name"] == "XSRF-TOKEN" and "chat2" in c.get("domain", "")),
|
|
next((c["value"] for c in cookies if c["name"] == "XSRF-TOKEN"), ""),
|
|
)
|
|
|
|
@staticmethod
|
|
def _get_sign(method, url, params, data):
|
|
path = urlparse(url).path
|
|
content_md5 = hashlib.md5(data.encode("utf-8")).hexdigest()
|
|
sign_params = {k: v for k, v in params.items() if k != "sign"}
|
|
sorted_q = "&".join(f"{k}={quote(str(sign_params[k]), safe='')}" for k in sorted(sign_params))
|
|
base_string = f"{method.upper()}{path}{sorted_q}{content_md5}quark"
|
|
return "3a34" + hmac_mod.new(b"af54041a93cd4f6a757f", base_string.encode(), hashlib.sha1).hexdigest()
|
|
|
|
@staticmethod
|
|
def _generate_ut():
|
|
from deps.utdid import generate_utdid
|
|
from Crypto.Cipher import AES
|
|
from Crypto.Util.Padding import pad
|
|
key = iv = b"a89a69c4bbe039e8"
|
|
cipher = AES.new(key, AES.MODE_CBC, iv)
|
|
return base64.b64encode(b"\x3a\x34" + cipher.encrypt(pad(generate_utdid().encode("utf-8"), AES.block_size))).decode()
|
|
|
|
def _build_params(self):
|
|
ts = int(time.time() * 1000)
|
|
return {
|
|
"ut": self._generate_ut(), "ve": "2.12.0", "bi": "37261", "pf": "3300",
|
|
"pr": "qwen", "fr": "pc", "sv": "release", "cg": "default",
|
|
"chat_client": "h5", "biz_id": "ai_qwen",
|
|
"vcode": str(ts), "nonce": str(ts + 2), "timestamp": str(ts + 2), "sign_type": "2",
|
|
}
|
|
|
|
def _build_headers(self):
|
|
headers = {
|
|
"accept": "application/json, text/event-stream, text/plain, */*",
|
|
"content-type": "application/json",
|
|
"x-platform": "pc_tongyi",
|
|
"user-agent": self.ua,
|
|
}
|
|
if self._cookie_str:
|
|
headers["cookie"] = self._cookie_str
|
|
if self._xsrf_token:
|
|
headers["x-xsrf-token"] = self._xsrf_token
|
|
return headers
|
|
|
|
async def chat(self, question, thinking=False):
|
|
import aiohttp
|
|
from yarl import URL
|
|
|
|
params = self._build_params()
|
|
params["protocol_version"] = "v2"
|
|
payload = {
|
|
"req_id": uuid.uuid4().hex, "parent_req_id": "0",
|
|
"session_id": uuid.uuid4().hex, "topic_id": uuid.uuid4().hex,
|
|
"scene": "chat", "sub_scene": "", "scene_param": "first_turn",
|
|
"biz_id": "ai_qwen", "model": "Qwen", "from": "default",
|
|
"protocol_version": "v2", "messages_merge": False, "chat_client": "h5",
|
|
"deep_search": "1" if thinking else "0", "ai_tool_scene": "",
|
|
"temporary": False,
|
|
"messages": [{"mime_type": "text/plain", "content": question,
|
|
"meta_data": {"ori_query": question}, "status": "complete"}],
|
|
}
|
|
body = json.dumps(payload, separators=(",", ":"), ensure_ascii=False)
|
|
params["sign"] = self._get_sign("POST", self.url, params, body)
|
|
qs = "&".join(f"{k}={quote(str(params[k]), safe='')}" for k in sorted(params))
|
|
full_url = URL(f"{self.url}?{qs}", encoded=True)
|
|
|
|
raw_text = ""
|
|
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=180)) as session:
|
|
async with session.post(full_url, headers=self._build_headers(), data=body, ssl=False) as resp:
|
|
buf = ""
|
|
async for chunk in resp.content.iter_any():
|
|
buf += chunk.decode("utf-8", errors="replace")
|
|
raw_text = buf
|
|
|
|
return raw_text, body
|
|
|
|
async def create_share(self, session_id):
|
|
import aiohttp
|
|
from yarl import URL
|
|
|
|
params = self._build_params()
|
|
body = json.dumps({"session_id": session_id, "share_type": 1}, separators=(",", ":"))
|
|
params["sign"] = self._get_sign("POST", self.share_url, params, body)
|
|
qs = "&".join(f"{k}={quote(str(params[k]), safe='')}" for k in sorted(params))
|
|
full_url = URL(f"{self.share_url}?{qs}", encoded=True)
|
|
|
|
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=30)) as session:
|
|
async with session.post(full_url, headers=self._build_headers(), data=body, ssl=False) as resp:
|
|
data = await resp.json()
|
|
if data.get("code") == 0 and data.get("data"):
|
|
return data["data"].get("share_id", "")
|
|
return ""
|
|
|
|
|
|
async def extract_qianwen_cookies():
|
|
from playwright.async_api import async_playwright
|
|
from .base import CDP_URL
|
|
try:
|
|
async with async_playwright() as pw:
|
|
browser = await pw.chromium.connect_over_cdp(CDP_URL)
|
|
ctx = browser.contexts[0]
|
|
cookies = await ctx.cookies([
|
|
"https://www.qianwen.com",
|
|
"https://chat2.qianwen.com",
|
|
"https://chat2-api.qianwen.com",
|
|
])
|
|
if any(c["name"] == "tongyi_sso_ticket" for c in cookies):
|
|
return cookies
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
def get_qw_session_id(result):
|
|
raw_req = result.get("_raw_req", "")
|
|
try:
|
|
req = json.loads(raw_req) if raw_req else {}
|
|
return req.get("session_id", "")
|
|
except Exception:
|
|
return ""
|