Copilot.py 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. from __future__ import annotations
  2. import os
  3. import json
  4. import asyncio
  5. from urllib.parse import quote
  6. try:
  7. from curl_cffi.requests import AsyncSession
  8. from curl_cffi import CurlWsFlag
  9. has_curl_cffi = True
  10. except ImportError:
  11. has_curl_cffi = False
  12. try:
  13. import nodriver
  14. has_nodriver = True
  15. except ImportError:
  16. has_nodriver = False
  17. from .base_provider import AsyncGeneratorProvider, ProviderModelMixin
  18. from .helper import format_prompt_max_length
  19. from .openai.har_file import get_headers, get_har_files
  20. from ..typing import AsyncResult, Messages, MediaListType
  21. from ..errors import MissingRequirementsError, NoValidHarFileError, MissingAuthError
  22. from ..providers.response import BaseConversation, JsonConversation, RequestLogin, ImageResponse, FinishReason, SuggestedFollowups, TitleGeneration, Sources, SourceLink
  23. from ..tools.media import merge_media
  24. from ..requests import get_nodriver
  25. from ..image import to_bytes, is_accepted_format
  26. from .helper import get_last_user_message
  27. from .. import debug
  28. class Conversation(JsonConversation):
  29. conversation_id: str
  30. def __init__(self, conversation_id: str):
  31. self.conversation_id = conversation_id
  32. class Copilot(AsyncGeneratorProvider, ProviderModelMixin):
  33. label = "Microsoft Copilot"
  34. url = "https://copilot.microsoft.com"
  35. working = True
  36. supports_stream = True
  37. default_model = "Copilot"
  38. models = [default_model, "Think Deeper"]
  39. model_aliases = {
  40. "gpt-4": default_model,
  41. "gpt-4o": default_model,
  42. "o1": "Think Deeper",
  43. "dall-e-3": default_model
  44. }
  45. websocket_url = "wss://copilot.microsoft.com/c/api/chat?api-version=2"
  46. conversation_url = f"{url}/c/api/conversations"
  47. _access_token: str = None
  48. _cookies: dict = None
  49. @classmethod
  50. async def create_async_generator(
  51. cls,
  52. model: str,
  53. messages: Messages,
  54. stream: bool = False,
  55. proxy: str = None,
  56. timeout: int = 30,
  57. prompt: str = None,
  58. media: MediaListType = None,
  59. conversation: BaseConversation = None,
  60. return_conversation: bool = True,
  61. api_key: str = None,
  62. **kwargs
  63. ) -> AsyncResult:
  64. if not has_curl_cffi:
  65. raise MissingRequirementsError('Install or update "curl_cffi" package | pip install -U curl_cffi')
  66. model = cls.get_model(model)
  67. websocket_url = cls.websocket_url
  68. headers = None
  69. if cls._access_token:
  70. if api_key is not None:
  71. cls._access_token = api_key
  72. if cls._access_token is None:
  73. try:
  74. cls._access_token, cls._cookies = readHAR(cls.url)
  75. except NoValidHarFileError as h:
  76. debug.log(f"Copilot: {h}")
  77. if has_nodriver:
  78. yield RequestLogin(cls.label, os.environ.get("G4F_LOGIN_URL", ""))
  79. cls._access_token, cls._cookies = await get_access_token_and_cookies(cls.url, proxy)
  80. else:
  81. raise h
  82. websocket_url = f"{websocket_url}&accessToken={quote(cls._access_token)}"
  83. headers = {"authorization": f"Bearer {cls._access_token}"}
  84. async with AsyncSession(
  85. timeout=timeout,
  86. proxy=proxy,
  87. impersonate="chrome",
  88. headers=headers,
  89. cookies=cls._cookies,
  90. ) as session:
  91. if cls._access_token is not None:
  92. cls._cookies = session.cookies.jar if hasattr(session.cookies, "jar") else session.cookies
  93. response = await session.get("https://copilot.microsoft.com/c/api/user")
  94. if response.status_code == 401:
  95. raise MissingAuthError("Status 401: Invalid access token")
  96. response.raise_for_status()
  97. user = response.json().get('firstName')
  98. if user is None:
  99. cls._access_token = None
  100. debug.log(f"Copilot: User: {user or 'null'}")
  101. if conversation is None:
  102. response = await session.post(cls.conversation_url)
  103. response.raise_for_status()
  104. conversation_id = response.json().get("id")
  105. conversation = Conversation(conversation_id)
  106. if prompt is None:
  107. prompt = format_prompt_max_length(messages, 10000)
  108. debug.log(f"Copilot: Created conversation: {conversation_id}")
  109. else:
  110. conversation_id = conversation.conversation_id
  111. if prompt is None:
  112. prompt = get_last_user_message(messages)
  113. debug.log(f"Copilot: Use conversation: {conversation_id}")
  114. if return_conversation:
  115. yield conversation
  116. uploaded_images = []
  117. for media, _ in merge_media(media, messages):
  118. if not isinstance(media, str):
  119. data = to_bytes(media)
  120. response = await session.post(
  121. "https://copilot.microsoft.com/c/api/attachments",
  122. headers={
  123. "content-type": is_accepted_format(data),
  124. "content-length": str(len(data)),
  125. },
  126. data=data
  127. )
  128. response.raise_for_status()
  129. media = response.json().get("url")
  130. uploaded_images.append({"type":"image", "url": media})
  131. wss = await session.ws_connect(cls.websocket_url, timeout=3)
  132. await wss.send(json.dumps({
  133. "event": "send",
  134. "conversationId": conversation_id,
  135. "content": [*uploaded_images, {
  136. "type": "text",
  137. "text": prompt,
  138. }],
  139. "mode": "reasoning" if "Think" in model else "chat",
  140. }).encode(), CurlWsFlag.TEXT)
  141. done = False
  142. msg = None
  143. image_prompt: str = None
  144. last_msg = None
  145. sources = {}
  146. while not wss.closed:
  147. try:
  148. msg = await asyncio.wait_for(wss.recv(), 3 if done else timeout)
  149. msg = json.loads(msg[0])
  150. except:
  151. break
  152. last_msg = msg
  153. if msg.get("event") == "appendText":
  154. yield msg.get("text")
  155. elif msg.get("event") == "generatingImage":
  156. image_prompt = msg.get("prompt")
  157. elif msg.get("event") == "imageGenerated":
  158. yield ImageResponse(msg.get("url"), image_prompt, {"preview": msg.get("thumbnailUrl")})
  159. elif msg.get("event") == "done":
  160. yield FinishReason("stop")
  161. done = True
  162. elif msg.get("event") == "suggestedFollowups":
  163. yield SuggestedFollowups(msg.get("suggestions"))
  164. break
  165. elif msg.get("event") == "replaceText":
  166. yield msg.get("text")
  167. elif msg.get("event") == "titleUpdate":
  168. yield TitleGeneration(msg.get("title"))
  169. elif msg.get("event") == "citation":
  170. sources[msg.get("url")] = msg
  171. yield SourceLink(list(sources.keys()).index(msg.get("url")), msg.get("url"))
  172. elif msg.get("event") == "error":
  173. raise RuntimeError(f"Error: {msg}")
  174. elif msg.get("event") not in ["received", "startMessage", "partCompleted"]:
  175. debug.log(f"Copilot Message: {msg}")
  176. if not done:
  177. raise RuntimeError(f"Invalid response: {last_msg}")
  178. if sources:
  179. yield Sources(sources.values())
  180. if not wss.closed:
  181. await wss.close()
  182. async def get_access_token_and_cookies(url: str, proxy: str = None, target: str = "ChatAI",):
  183. browser, stop_browser = await get_nodriver(proxy=proxy, user_data_dir="copilot")
  184. try:
  185. page = await browser.get(url)
  186. access_token = None
  187. while access_token is None:
  188. access_token = await page.evaluate("""
  189. (() => {
  190. for (var i = 0; i < localStorage.length; i++) {
  191. try {
  192. item = JSON.parse(localStorage.getItem(localStorage.key(i)));
  193. if (item.credentialType == "AccessToken"
  194. && item.expiresOn > Math.floor(Date.now() / 1000)
  195. && item.target.includes("target")) {
  196. return item.secret;
  197. }
  198. } catch(e) {}
  199. }
  200. })()
  201. """.replace('"target"', json.dumps(target)))
  202. if access_token is None:
  203. await asyncio.sleep(1)
  204. cookies = {}
  205. for c in await page.send(nodriver.cdp.network.get_cookies([url])):
  206. cookies[c.name] = c.value
  207. await page.close()
  208. return access_token, cookies
  209. finally:
  210. stop_browser()
  211. def readHAR(url: str):
  212. api_key = None
  213. cookies = None
  214. for path in get_har_files():
  215. with open(path, 'rb') as file:
  216. try:
  217. harFile = json.loads(file.read())
  218. except json.JSONDecodeError:
  219. # Error: not a HAR file!
  220. continue
  221. for v in harFile['log']['entries']:
  222. if v['request']['url'].startswith(url):
  223. v_headers = get_headers(v)
  224. if "authorization" in v_headers:
  225. api_key = v_headers["authorization"].split(maxsplit=1).pop()
  226. if v['request']['cookies']:
  227. cookies = {c['name']: c['value'] for c in v['request']['cookies']}
  228. if api_key is None:
  229. raise NoValidHarFileError("No access token found in .har files")
  230. return api_key, cookies