返回提交历史
Modified
g4f/Provider/Copilot.py
+48
-85
Modified
g4f/providers/helper.py
+11
-3
XFEstudio/gpt4free
Enhance Copilot provider to support user identity type in API requests and improve bucket item handling
03eef2a2
代码差异
2 个文件
+59
-88
@@ -9,7 +9,7 @@ from urllib.parse import quote
9
9
10
10
try:
11
11
from curl_cffi.requests import AsyncSession
12
from curl_cffi import CurlWsFlag
12
from curl_cffi import CurlWsFlag, CurlMime
13
13
has_curl_cffi = True
14
14
except ImportError:
15
15
has_curl_cffi = False
@@ -20,7 +20,6 @@ except ImportError:
20
20
has_nodriver = False
21
21
22
22
from .base_provider import AsyncAuthedProvider, ProviderModelMixin
23
from .helper import format_prompt_max_length
24
23
from .openai.har_file import get_headers, get_har_files
25
24
from ..typing import AsyncResult, Messages, MediaListType
26
25
from ..errors import MissingRequirementsError, NoValidHarFileError, MissingAuthError
@@ -30,7 +29,7 @@ from ..requests import get_nodriver
30
29
from ..image import to_bytes, is_accepted_format
31
30
from .helper import get_last_user_message
32
31
from ..files import get_bucket_dir
33
from ..tools.files import get_filenames, read_bucket
32
from ..tools.files import read_bucket
34
33
from pathlib import Path
35
34
from .. import debug
36
35
@@ -46,8 +45,10 @@ def extract_bucket_items(messages: Messages) -> list[dict]:
46
45
for message in messages:
47
46
if isinstance(message, dict) and isinstance(message.get("content"), list):
48
47
for content_item in message["content"]:
49
if isinstance(content_item, dict) and ("bucket_id" in content_item or "bucket" in content_item):
48
if isinstance(content_item, dict) and "bucket_id" in content_item and "name" not in content_item:
50
49
bucket_items.append(content_item)
50
if message.get("role") == "assistant":
51
bucket_items = []
51
52
return bucket_items
52
53
53
54
class Copilot(AsyncAuthedProvider, ProviderModelMixin):
@@ -109,6 +110,7 @@ class Copilot(AsyncAuthedProvider, ProviderModelMixin):
109
110
media: MediaListType = None,
110
111
conversation: BaseConversation = None,
111
112
return_conversation: bool = True,
113
useridentitytype: str = "google",
112
114
api_key: str = None,
113
115
**kwargs
114
116
) -> AsyncResult:
@@ -130,7 +132,7 @@ class Copilot(AsyncAuthedProvider, ProviderModelMixin):
130
132
cls._access_token, cls._cookies = await get_access_token_and_cookies(cls.url, proxy)
131
133
else:
132
134
raise h
133
websocket_url = f"{websocket_url}&accessToken={quote(cls._access_token)}"
135
websocket_url = f"{websocket_url}&accessToken={quote(cls._access_token)}&X-UserIdentityType={quote(useridentitytype)}"
134
136
headers = {"authorization": f"Bearer {cls._access_token}"}
135
137
136
138
async with AsyncSession(
@@ -142,7 +144,7 @@ class Copilot(AsyncAuthedProvider, ProviderModelMixin):
142
144
) as session:
143
145
if cls._access_token is not None:
144
146
cls._cookies = session.cookies.jar if hasattr(session.cookies, "jar") else session.cookies
145
response = await session.get("https://copilot.microsoft.com/c/api/user")
147
response = await session.get("https://copilot.microsoft.com/c/api/user?api-version=2", headers={"x-useridentitytype": useridentitytype})
146
148
if response.status_code == 401:
147
149
raise MissingAuthError("Status 401: Invalid access token")
148
150
response.raise_for_status()
@@ -154,17 +156,13 @@ class Copilot(AsyncAuthedProvider, ProviderModelMixin):
154
156
else:
155
157
debug.log(f"Copilot: User: {user}")
156
158
if conversation is None:
157
response = await session.post(cls.conversation_url)
159
response = await session.post(cls.conversation_url, headers={"x-useridentitytype": useridentitytype})
158
160
response.raise_for_status()
159
161
conversation_id = response.json().get("id")
160
162
conversation = Conversation(conversation_id)
161
if prompt is None:
162
prompt = format_prompt_max_length(messages, 10000)
163
163
debug.log(f"Copilot: Created conversation: {conversation_id}")
164
164
else:
165
165
conversation_id = conversation.conversation_id
166
if prompt is None:
167
prompt = get_last_user_message(messages)
168
166
debug.log(f"Copilot: Use conversation: {conversation_id}")
169
167
if return_conversation:
170
168
yield conversation
@@ -180,6 +178,7 @@ class Copilot(AsyncAuthedProvider, ProviderModelMixin):
180
178
headers={
181
179
"content-type": is_accepted_format(data),
182
180
"content-length": str(len(data)),
181
"x-useridentitytype": useridentitytype
183
182
},
184
183
data=data
185
184
)
@@ -191,69 +190,33 @@ class Copilot(AsyncAuthedProvider, ProviderModelMixin):
191
190
bucket_items = extract_bucket_items(messages)
192
191
for item in bucket_items:
193
192
try:
194
if "name" in item:
195
# Handle specific file from bucket with name
196
file_path = Path(get_bucket_dir(item["bucket_id"], "media", item["name"]))
197
if file_path.exists() and file_path.is_file():
198
with open(file_path, "rb") as f:
199
file_data = f.read()
200
201
filename = item["name"]
202
# Determine content type based on file extension
203
content_type = "application/octet-stream"
204
if filename.endswith(".pdf"):
205
content_type = "application/pdf"
206
elif filename.endswith(".docx"):
207
content_type = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
208
elif filename.endswith(".txt"):
209
content_type = "text/plain"
210
elif filename.endswith(".md"):
211
content_type = "text/markdown"
212
elif filename.endswith(".json"):
213
content_type = "application/json"
214
215
response = await session.post(
216
"https://copilot.microsoft.com/c/api/attachments",
217
headers={
218
"content-type": content_type,
219
"content-length": str(len(file_data)),
220
},
221
data=file_data
222
)
223
response.raise_for_status()
224
file_url = response.json().get("url")
225
uploaded_attachments.append({"type": "file", "url": file_url, "name": filename})
226
debug.log(f"Copilot: Uploaded bucket file: {filename}")
227
else:
228
debug.log(f"Copilot: Bucket file not found: {item.get('name')}")
229
else:
230
# Handle plain text content from bucket
231
bucket_path = Path(get_bucket_dir(item["bucket"]))
232
plain_text_content = ""
233
for text_chunk in read_bucket(bucket_path):
234
plain_text_content += text_chunk
235
236
if plain_text_content.strip():
193
# Handle plain text content from bucket
194
bucket_path = Path(get_bucket_dir(item["bucket_id"]))
195
for text_chunk in read_bucket(bucket_path):
196
if text_chunk.strip():
237
197
# Upload plain text as a text file
238
text_data = plain_text_content.encode('utf-8')
198
text_data = text_chunk.encode('utf-8')
199
data = CurlMime()
200
data.addpart("file", filename=f"bucket_{item['bucket_id']}.txt", content_type="text/plain", data=text_data)
239
201
response = await session.post(
240
202
"https://copilot.microsoft.com/c/api/attachments",
241
headers={
242
"content-type": "text/plain",
243
"content-length": str(len(text_data)),
244
},
245
data=text_data
203
multipart=data,
204
headers={"x-useridentitytype": useridentitytype}
246
205
)
247
206
response.raise_for_status()
248
file_url = response.json().get("url")
249
uploaded_attachments.append({"type": "file", "url": file_url, "name": f"bucket_{item['bucket']}.txt"})
250
debug.log(f"Copilot: Uploaded bucket text content: {item['bucket']}")
207
data = response.json()
208
uploaded_attachments.append({"type": "document", "attachmentId": data.get("id")})
209
debug.log(f"Copilot: Uploaded bucket text content: {item['bucket_id']}")
251
210
else:
252
debug.log(f"Copilot: No text content found in bucket: {item['bucket']}")
211
debug.log(f"Copilot: No text content found in bucket: {item['bucket_id']}")
253
212
except Exception as e:
254
debug.log(f"Copilot: Failed to upload bucket item {item}: {e}")
213
debug.log(f"Copilot: Failed to upload bucket item: {item}")
214
debug.error(e)
255
215
256
wss = await session.ws_connect(cls.websocket_url, timeout=3)
216
if prompt is None:
217
prompt = get_last_user_message(messages, False)
218
219
wss = await session.ws_connect(websocket_url, timeout=3)
257
220
if "Think" in model:
258
221
mode = "reasoning"
259
222
elif model.startswith("gpt-5") or "GPT-5" in model:
@@ -317,32 +280,32 @@ class Copilot(AsyncAuthedProvider, ProviderModelMixin):
317
280
if not wss.closed:
318
281
await wss.close()
319
282
320
async def get_access_token_and_cookies(url: str, proxy: str = None, target: str = "ChatAI",):
321
browser, stop_browser = await get_nodriver(proxy=proxy, user_data_dir="copilot")
283
async def get_access_token_and_cookies(url: str, proxy: str = None):
284
browser, stop_browser = await get_nodriver(proxy=proxy)
322
285
try:
323
286
page = await browser.get(url)
324
287
access_token = None
325
288
while access_token is None:
326
access_token = await page.evaluate("""
327
(() => {
328
for (var i = 0; i < localStorage.length; i++) {
329
try {
330
item = JSON.parse(localStorage.getItem(localStorage.key(i)));
331
if (item.credentialType == "AccessToken"
332
&& item.expiresOn > Math.floor(Date.now() / 1000)
333
&& item.target.includes("target")) {
334
return item.secret;
335
}
336
} catch(e) {}
337
}
338
})()
339
""".replace('"target"', json.dumps(target)))
340
if access_token is None:
341
await asyncio.sleep(1)
289
for _ in range(2):
290
await asyncio.sleep(3)
291
access_token = await page.evaluate("""
292
(() => {
293
for (var i = 0; i < localStorage.length; i++) {
294
try {
295
item = JSON.parse(localStorage.getItem(localStorage.key(i)));
296
if (item?.body?.access_token) {
297
return item.body.access_token;
298
}
299
} catch(e) {}
300
}
301
})()
302
""")
303
if access_token is None:
304
await asyncio.sleep(1)
342
305
cookies = {}
343
306
for c in await page.send(nodriver.cdp.network.get_cookies([url])):
344
307
cookies[c.name] = c.value
345
await page.close()
308
stop_browser()
346
309
return access_token, cookies
347
310
finally:
348
311
stop_browser()
@@ -66,15 +66,23 @@ def format_prompt(messages: Messages, add_special_tokens: bool = False, do_conti
66
66
def get_system_prompt(messages: Messages) -> str:
67
67
return "\n".join([m["content"] for m in messages if m["role"] in ("developer", "system")])
68
68
69
def get_last_user_message(messages: Messages) -> str:
69
def get_last_user_message(messages: Messages, include_buckets: bool = True) -> str:
70
70
user_messages = []
71
71
for message in messages[::-1]:
72
72
if message.get("role") == "user" or not user_messages:
73
73
if message.get("role") != "user":
74
74
continue
75
content = to_string(message.get("content")).strip()
76
if content:
75
content = message.get("content")
76
if include_buckets:
77
content = to_string(content).strip()
78
if isinstance(content, str):
77
79
user_messages.append(content)
80
else:
81
for content_item in content:
82
if content_item.get("type") == "text":
83
content = content_item.get("text").strip()
84
if content:
85
user_messages.append(content)
78
86
else:
79
87
return "\n".join(user_messages[::-1])
80
88
return "\n".join(user_messages[::-1])