KiWA001 commited on
Commit
f5e7a83
·
1 Parent(s): 48e7c93

Add Microsoft Copilot provider with CAPTCHA support

Browse files

- Created providers/copilot_provider.py with CAPTCHA detection
- Created copilot_session.py for persistent cookies (30 days, unlimited conversations)
- Updated admin_router.py with CAPTCHA endpoints:
* /qaz/copilot/captcha/status - Check if CAPTCHA pending
* /qaz/copilot/captcha/screenshot - Get CAPTCHA screenshot
* /qaz/copilot/captcha/solved - Mark as solved
* /qaz/copilot/captcha/clear - Clear state
* /qaz/copilot/session/status - Check session status
- Updated engine.py to include Copilot provider
- Updated config.py with copilot-gpt-4 model
- Updated static/qaz.html (admin dashboard /qazmlp) with:
* CAPTCHA solving UI with screenshot display
* Session status checker
* Auto-refresh every 10 seconds

Features:
- Detects CAPTCHA challenges automatically
- Saves cookies for 30 days (unlimited conversations)
- Starts new chat for each request (no context sharing)
- Admin dashboard shows CAPTCHA screenshot for solving
- No conversation limit

Files changed (6) hide show
  1. admin_router.py +109 -0
  2. config.py +7 -1
  3. copilot_session.py +119 -0
  4. engine.py +6 -1
  5. providers/copilot_provider.py +333 -0
  6. static/qaz.html +106 -0
admin_router.py CHANGED
@@ -3,6 +3,7 @@ from pydantic import BaseModel
3
  from typing import List, Optional
4
  import secrets
5
  import uuid
 
6
 
7
  from db import get_supabase
8
 
@@ -131,3 +132,111 @@ async def lookup_key_by_token(req: LookupKeyRequest):
131
  elif hasattr(e, 'args') and len(e.args) > 0:
132
  error_msg = str(e.args[0])
133
  raise HTTPException(status_code=500, detail=error_msg)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  from typing import List, Optional
4
  import secrets
5
  import uuid
6
+ import asyncio
7
 
8
  from db import get_supabase
9
 
 
132
  elif hasattr(e, 'args') and len(e.args) > 0:
133
  error_msg = str(e.args[0])
134
  raise HTTPException(status_code=500, detail=error_msg)
135
+
136
+ # --- Copilot CAPTCHA Handling ---
137
+
138
+ @router.get("/copilot/captcha/status")
139
+ async def copilot_captcha_status():
140
+ """Check if Copilot has a pending CAPTCHA challenge."""
141
+ try:
142
+ from providers.copilot_provider import CopilotProvider
143
+
144
+ is_pending = CopilotProvider.is_captcha_pending()
145
+
146
+ if is_pending:
147
+ # Check if screenshot exists
148
+ import os
149
+ screenshot_path = "/tmp/copilot_captcha.png"
150
+ has_screenshot = os.path.exists(screenshot_path)
151
+
152
+ return {
153
+ "captcha_required": True,
154
+ "has_screenshot": has_screenshot,
155
+ "screenshot_url": "/qaz/copilot/captcha/screenshot" if has_screenshot else None,
156
+ "message": "CAPTCHA verification required. Please solve it in the admin panel."
157
+ }
158
+ else:
159
+ return {
160
+ "captcha_required": False,
161
+ "message": "No CAPTCHA pending"
162
+ }
163
+ except Exception as e:
164
+ raise HTTPException(status_code=500, detail=str(e))
165
+
166
+ @router.get("/copilot/captcha/screenshot")
167
+ async def copilot_captcha_screenshot():
168
+ """Get the CAPTCHA screenshot for solving."""
169
+ import os
170
+ from fastapi.responses import FileResponse
171
+
172
+ screenshot_path = "/tmp/copilot_captcha.png"
173
+
174
+ if not os.path.exists(screenshot_path):
175
+ raise HTTPException(status_code=404, detail="No CAPTCHA screenshot available")
176
+
177
+ return FileResponse(screenshot_path, media_type="image/png")
178
+
179
+ @router.post("/copilot/captcha/solved")
180
+ async def copilot_captcha_solved():
181
+ """Mark CAPTCHA as solved and save session."""
182
+ try:
183
+ from providers.copilot_provider import CopilotProvider
184
+ from copilot_session import CopilotSessionManager
185
+
186
+ # Get the context with CAPTCHA
187
+ context = CopilotProvider.get_captcha_context()
188
+
189
+ if not context:
190
+ raise HTTPException(status_code=400, detail="No CAPTCHA context found")
191
+
192
+ # Wait a bit for user to solve
193
+ await asyncio.sleep(2)
194
+
195
+ # Save cookies from the solved session
196
+ cookies = await context.cookies()
197
+ session_mgr = CopilotSessionManager()
198
+ session_mgr.save_cookies(cookies)
199
+
200
+ # Clear the pending state
201
+ CopilotProvider.clear_captcha_pending()
202
+
203
+ # Close the context
204
+ await context.close()
205
+
206
+ return {
207
+ "status": "success",
208
+ "message": "CAPTCHA solved and session saved"
209
+ }
210
+ except Exception as e:
211
+ raise HTTPException(status_code=500, detail=str(e))
212
+
213
+ @router.post("/copilot/captcha/clear")
214
+ async def copilot_captcha_clear():
215
+ """Clear the CAPTCHA pending state (for retry)."""
216
+ try:
217
+ from providers.copilot_provider import CopilotProvider
218
+
219
+ # Get context and close it
220
+ context = CopilotProvider.get_captcha_context()
221
+ if context:
222
+ await context.close()
223
+
224
+ CopilotProvider.clear_captcha_pending()
225
+
226
+ return {
227
+ "status": "success",
228
+ "message": "CAPTCHA state cleared"
229
+ }
230
+ except Exception as e:
231
+ raise HTTPException(status_code=500, detail=str(e))
232
+
233
+ @router.get("/copilot/session/status")
234
+ async def copilot_session_status():
235
+ """Check Copilot session status."""
236
+ try:
237
+ from copilot_session import CopilotSessionManager
238
+
239
+ session_info = CopilotSessionManager.get_session_info()
240
+ return session_info
241
+ except Exception as e:
242
+ raise HTTPException(status_code=500, detail=str(e))
config.py CHANGED
@@ -20,7 +20,10 @@ MODEL_RANKING = [
20
  ("zai-glm-5", "zai", "glm-5"),
21
  ("gemini-gemini-3-flash", "gemini", "gemini-3-flash"),
22
 
23
- # Tier 2 — HuggingChat Models (Top 20 by popularity/quality)
 
 
 
24
  ("huggingchat-omni", "huggingchat", "omni"),
25
  ("huggingchat-llama-3.3-70b", "huggingchat", "meta-llama/Llama-3.3-70B-Instruct"),
26
  ("huggingchat-llama-4-scout", "huggingchat", "meta-llama/Llama-4-Scout-17B-16E-Instruct"),
@@ -178,6 +181,9 @@ PROVIDER_MODELS = {
178
  "gemini": [
179
  "gemini-gemini-3-flash",
180
  ],
 
 
 
181
  "huggingchat": [
182
  # Top Tier
183
  "huggingchat-omni",
 
20
  ("zai-glm-5", "zai", "glm-5"),
21
  ("gemini-gemini-3-flash", "gemini", "gemini-3-flash"),
22
 
23
+ # Tier 2 — Copilot (GPT-4 via Microsoft)
24
+ ("copilot-gpt-4", "copilot", "copilot-gpt-4"),
25
+
26
+ # Tier 3 — HuggingChat Models (Top 20 by popularity/quality)
27
  ("huggingchat-omni", "huggingchat", "omni"),
28
  ("huggingchat-llama-3.3-70b", "huggingchat", "meta-llama/Llama-3.3-70B-Instruct"),
29
  ("huggingchat-llama-4-scout", "huggingchat", "meta-llama/Llama-4-Scout-17B-16E-Instruct"),
 
181
  "gemini": [
182
  "gemini-gemini-3-flash",
183
  ],
184
+ "copilot": [
185
+ "copilot-gpt-4",
186
+ ],
187
  "huggingchat": [
188
  # Top Tier
189
  "huggingchat-omni",
copilot_session.py ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Copilot Session Manager
3
+ -----------------------
4
+ Manages persistent browser sessions and cookies for Microsoft Copilot.
5
+ Unlike HuggingChat, this has NO conversation limit - sessions persist indefinitely.
6
+ """
7
+
8
+ import json
9
+ import logging
10
+ import os
11
+ from datetime import datetime, timedelta
12
+ from typing import Optional, Dict, Any
13
+
14
+ logger = logging.getLogger("kai_api.copilot_session")
15
+
16
+ # Storage file for Copilot session
17
+ SESSION_FILE = "/tmp/copilot_session.json"
18
+
19
+
20
+ class CopilotSessionManager:
21
+ """Manages Copilot browser session persistence - unlimited conversations."""
22
+
23
+ @staticmethod
24
+ def save_cookies(cookies: list, user_agent: str = None) -> bool:
25
+ """
26
+ Save cookies from an authenticated session.
27
+
28
+ Args:
29
+ cookies: List of cookie dictionaries from Playwright
30
+ user_agent: User agent string used during authentication
31
+
32
+ Returns:
33
+ bool: True if saved successfully
34
+ """
35
+ try:
36
+ session_data = {
37
+ "cookies": cookies,
38
+ "user_agent": user_agent,
39
+ "timestamp": datetime.now().isoformat(),
40
+ "expires_at": (datetime.now() + timedelta(days=30)).isoformat(), # 30 days
41
+ }
42
+
43
+ with open(SESSION_FILE, "w") as f:
44
+ json.dump(session_data, f, indent=2)
45
+
46
+ logger.info(f"✅ Saved {len(cookies)} cookies for Copilot session")
47
+ return True
48
+
49
+ except Exception as e:
50
+ logger.error(f"Failed to save Copilot cookies: {e}")
51
+ return False
52
+
53
+ @staticmethod
54
+ def load_session() -> Optional[dict]:
55
+ """
56
+ Load saved session data if it exists and is not expired.
57
+
58
+ Returns:
59
+ dict with cookies and user_agent, or None if no valid session
60
+ """
61
+ try:
62
+ if not os.path.exists(SESSION_FILE):
63
+ return None
64
+
65
+ with open(SESSION_FILE, "r") as f:
66
+ session_data = json.load(f)
67
+
68
+ # Check expiration (30 days)
69
+ expires_at = datetime.fromisoformat(session_data.get("expires_at", "2000-01-01"))
70
+ if datetime.now() > expires_at:
71
+ logger.info("Copilot session expired, need re-authentication")
72
+ return None
73
+
74
+ logger.info(f"✅ Loaded Copilot session (expires: {expires_at})")
75
+ return session_data
76
+
77
+ except Exception as e:
78
+ logger.error(f"Failed to load Copilot cookies: {e}")
79
+ return None
80
+
81
+ @staticmethod
82
+ def clear_session() -> bool:
83
+ """Clear the saved session."""
84
+ try:
85
+ if os.path.exists(SESSION_FILE):
86
+ os.remove(SESSION_FILE)
87
+ logger.info("✅ Cleared Copilot session")
88
+ return True
89
+ except Exception as e:
90
+ logger.error(f"Failed to clear Copilot session: {e}")
91
+ return False
92
+
93
+ @staticmethod
94
+ def has_valid_session() -> bool:
95
+ """Check if we have a valid authenticated session."""
96
+ session = CopilotSessionManager.load_session()
97
+ return session is not None and len(session.get("cookies", [])) > 0
98
+
99
+ @staticmethod
100
+ def get_session_info() -> dict:
101
+ """Get information about the current session."""
102
+ session = CopilotSessionManager.load_session()
103
+
104
+ if not session:
105
+ return {
106
+ "has_session": False,
107
+ "message": "No session found. May need CAPTCHA verification.",
108
+ }
109
+
110
+ expires_at = datetime.fromisoformat(session.get("expires_at", "2000-01-01"))
111
+ time_left = expires_at - datetime.now()
112
+
113
+ return {
114
+ "has_session": True,
115
+ "expires_at": session.get("expires_at"),
116
+ "days_remaining": max(0, time_left.days),
117
+ "cookie_count": len(session.get("cookies", [])),
118
+ "timestamp": session.get("timestamp"),
119
+ }
engine.py CHANGED
@@ -20,6 +20,7 @@ from providers.pollinations_provider import PollinationsProvider
20
  from providers.gemini_provider import GeminiProvider
21
  from providers.zai_provider import ZaiProvider
22
  from providers.huggingchat_provider import HuggingChatProvider
 
23
  from config import MODEL_RANKING, PROVIDER_MODELS, SUPABASE_URL, SUPABASE_KEY
24
  from models import ModelInfo
25
  from sanitizer import sanitize_response
@@ -61,8 +62,12 @@ class AIEngine:
61
  # HuggingChat also uses Playwright
62
  self._providers["huggingchat"] = HuggingChatProvider()
63
  logger.info("✅ HuggingChat provider enabled")
 
 
 
 
64
  else:
65
- logger.warning("⚠️ Z.ai/Gemini/HuggingChat providers disabled (Playwright not installed)")
66
  # Success Tracker: Key = "provider/model_id"
67
  # Value = {success, failure, consecutive_failures, avg_time_ms, total_time_ms, count_samples}
68
  self._stats: dict[str, dict] = {}
 
20
  from providers.gemini_provider import GeminiProvider
21
  from providers.zai_provider import ZaiProvider
22
  from providers.huggingchat_provider import HuggingChatProvider
23
+ from providers.copilot_provider import CopilotProvider
24
  from config import MODEL_RANKING, PROVIDER_MODELS, SUPABASE_URL, SUPABASE_KEY
25
  from models import ModelInfo
26
  from sanitizer import sanitize_response
 
62
  # HuggingChat also uses Playwright
63
  self._providers["huggingchat"] = HuggingChatProvider()
64
  logger.info("✅ HuggingChat provider enabled")
65
+
66
+ # Copilot also uses Playwright (with CAPTCHA support)
67
+ self._providers["copilot"] = CopilotProvider()
68
+ logger.info("✅ Copilot provider enabled (with CAPTCHA support)")
69
  else:
70
+ logger.warning("⚠️ Z.ai/Gemini/HuggingChat/Copilot providers disabled (Playwright not installed)")
71
  # Success Tracker: Key = "provider/model_id"
72
  # Value = {success, failure, consecutive_failures, avg_time_ms, total_time_ms, count_samples}
73
  self._stats: dict[str, dict] = {}
providers/copilot_provider.py ADDED
@@ -0,0 +1,333 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Microsoft Copilot Provider (Browser-Based with CAPTCHA Support)
3
+ ---------------------------------------------------------------
4
+ Uses Playwright Chromium to interact with https://copilot.microsoft.com/ as a real browser.
5
+
6
+ Strategy:
7
+ - Uses persistent browser context with stored cookies
8
+ - Handles CAPTCHA by detecting it and returning special status
9
+ - Shows CAPTCHA challenge in admin dashboard (/qazmlp)
10
+ - Unlimited conversations (no 50 limit)
11
+ - Starts new chat for each request (no context sharing)
12
+ """
13
+
14
+ import asyncio
15
+ import logging
16
+ import re
17
+ from typing import Optional, Dict, Any
18
+ from providers.base import BaseProvider
19
+ from config import PROVIDER_MODELS
20
+ from copilot_session import CopilotSessionManager
21
+
22
+ logger = logging.getLogger("kai_api.copilot")
23
+
24
+ _playwright = None
25
+ _browser = None
26
+ _lock = asyncio.Lock()
27
+
28
+ # Track if we're currently showing CAPTCHA
29
+ _captcha_pending = False
30
+ _captcha_context = None
31
+
32
+
33
+ class CopilotProvider(BaseProvider):
34
+ """AI provider using Microsoft Copilot via Persistent Playwright Browser."""
35
+
36
+ RESPONSE_TIMEOUT = 90
37
+ HYDRATION_DELAY = 3.0
38
+
39
+ @property
40
+ def name(self) -> str:
41
+ return "copilot"
42
+
43
+ def get_available_models(self) -> list[str]:
44
+ return PROVIDER_MODELS.get("copilot", ["copilot-gpt-4"])
45
+
46
+ @staticmethod
47
+ def is_available() -> bool:
48
+ """Check if Playwright is installed and usable."""
49
+ try:
50
+ from playwright.async_api import async_playwright
51
+ return True
52
+ except ImportError:
53
+ return False
54
+
55
+ async def _ensure_browser(self):
56
+ """Start the persistent browser if it's not running."""
57
+ global _playwright, _browser
58
+
59
+ async with _lock:
60
+ if _browser and _browser.is_connected():
61
+ return
62
+
63
+ logger.info("🚀 Copilot: Launching Persistent Browser...")
64
+ from playwright.async_api import async_playwright
65
+
66
+ _playwright = await async_playwright().start()
67
+ _browser = await _playwright.chromium.launch(
68
+ headless=True,
69
+ args=[
70
+ "--disable-blink-features=AutomationControlled",
71
+ "--no-sandbox",
72
+ "--disable-dev-shm-usage",
73
+ "--disable-gpu",
74
+ "--disable-web-security",
75
+ "--disable-features=IsolateOrigins,site-per-process",
76
+ ],
77
+ )
78
+ logger.info("✅ Copilot: Browser is Ready.")
79
+
80
+ async def _check_and_handle_captcha(self, page) -> bool:
81
+ """
82
+ Check if CAPTCHA is present on the page.
83
+ Returns True if CAPTCHA detected, False otherwise.
84
+ """
85
+ try:
86
+ # Multiple CAPTCHA selectors
87
+ captcha_selectors = [
88
+ '[data-testid="captcha-challenge"]',
89
+ 'iframe[src*="captcha"]',
90
+ 'input[id*="captcha"]',
91
+ '.captcha-container',
92
+ '[class*="captcha"]',
93
+ 'text="Verify you"',
94
+ 'text="I\'m not a robot"',
95
+ 'text="Security check"',
96
+ '#challenge-form',
97
+ '.challenge-container',
98
+ ]
99
+
100
+ for selector in captcha_selectors:
101
+ try:
102
+ element = await page.wait_for_selector(selector, timeout=3000)
103
+ if element and await element.is_visible():
104
+ logger.warning("🤖 Copilot: CAPTCHA detected!")
105
+ return True
106
+ except:
107
+ continue
108
+
109
+ return False
110
+ except Exception as e:
111
+ logger.error(f"Error checking for CAPTCHA: {e}")
112
+ return False
113
+
114
+ async def send_message(
115
+ self,
116
+ prompt: str,
117
+ model: str | None = None,
118
+ system_prompt: str | None = None,
119
+ **kwargs,
120
+ ) -> dict:
121
+ """Send a message via Copilot browser automation."""
122
+ if not self.is_available():
123
+ raise RuntimeError("Playwright not installed.")
124
+
125
+ await self._ensure_browser()
126
+ selected_model = model or "copilot-gpt-4"
127
+ session_mgr = CopilotSessionManager()
128
+
129
+ # Check if we have a valid session
130
+ session_data = session_mgr.load_session()
131
+
132
+ # Create context with cookies if we have them
133
+ context = await _browser.new_context(
134
+ viewport={"width": 1920, "height": 1080},
135
+ user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0",
136
+ locale="en-US",
137
+ timezone_id="America/New_York",
138
+ )
139
+
140
+ # Hide webdriver flag
141
+ await context.add_init_script("""
142
+ Object.defineProperty(navigator, 'webdriver', {get: () => undefined});
143
+ Object.defineProperty(navigator, 'plugins', {get: () => [1, 2, 3, 4, 5]});
144
+ window.chrome = { runtime: {} };
145
+ """)
146
+
147
+ # Add cookies if we have them
148
+ if session_data and session_data.get("cookies"):
149
+ await context.add_cookies(session_data["cookies"])
150
+ logger.info("✅ Copilot: Loaded existing session cookies")
151
+
152
+ page = await context.new_page()
153
+
154
+ try:
155
+ logger.info(f"Copilot request: {selected_model}")
156
+
157
+ # Navigate to Copilot
158
+ await page.goto("https://copilot.microsoft.com/", timeout=60000)
159
+ await asyncio.sleep(3)
160
+
161
+ # Check for CAPTCHA
162
+ has_captcha = await self._check_and_handle_captcha(page)
163
+
164
+ if has_captcha:
165
+ logger.warning("🤖 Copilot: CAPTCHA challenge detected!")
166
+
167
+ # Save current state for CAPTCHA solving
168
+ global _captcha_pending, _captcha_context
169
+ _captcha_pending = True
170
+ _captcha_context = context
171
+
172
+ # Take screenshot for admin dashboard
173
+ screenshot_path = "/tmp/copilot_captcha.png"
174
+ await page.screenshot(path=screenshot_path, full_page=True)
175
+
176
+ raise Exception("CAPTCHA_REQUIRED")
177
+
178
+ # Try to start a new chat (clear previous context)
179
+ try:
180
+ new_chat_btn = await page.wait_for_selector('button:has-text("New chat")', timeout=5000)
181
+ if new_chat_btn:
182
+ await new_chat_btn.click()
183
+ await asyncio.sleep(2)
184
+ logger.info("Copilot: Started new chat")
185
+ except:
186
+ logger.info("Copilot: No 'New chat' button found, continuing...")
187
+
188
+ # Find and use the chat input
189
+ input_selectors = [
190
+ '[data-testid="chat-input"]',
191
+ 'div[contenteditable="true"]',
192
+ '[role="textbox"]',
193
+ 'textarea',
194
+ '.input-area div[contenteditable]',
195
+ ]
196
+
197
+ input_selector = None
198
+ for sel in input_selectors:
199
+ try:
200
+ await page.wait_for_selector(sel, timeout=5000)
201
+ input_selector = sel
202
+ logger.info(f"✅ Copilot: Found input selector: {sel}")
203
+ break
204
+ except:
205
+ continue
206
+
207
+ if not input_selector:
208
+ raise RuntimeError("Could not find Copilot chat input")
209
+
210
+ await asyncio.sleep(self.HYDRATION_DELAY)
211
+
212
+ # Type the message
213
+ full_prompt = prompt
214
+ if system_prompt:
215
+ full_prompt = f"[System: {system_prompt}]\n\n{prompt}"
216
+
217
+ await page.click(input_selector)
218
+ await page.keyboard.type(full_prompt, delay=10)
219
+ await asyncio.sleep(0.5)
220
+ await page.keyboard.press("Enter")
221
+
222
+ logger.info("Copilot: Message sent...")
223
+
224
+ # Wait for response
225
+ response_text = await self._wait_for_response(page)
226
+
227
+ if not response_text:
228
+ raise ValueError("Empty response from Copilot")
229
+
230
+ # Save cookies after successful request
231
+ cookies = await context.cookies()
232
+ session_mgr.save_cookies(cookies)
233
+ logger.info("✅ Copilot: Saved session cookies")
234
+
235
+ return {
236
+ "response": response_text,
237
+ "model": selected_model,
238
+ }
239
+
240
+ except Exception as e:
241
+ if "CAPTCHA_REQUIRED" in str(e):
242
+ raise
243
+ logger.error(f"Copilot Error: {e}")
244
+ raise
245
+ finally:
246
+ if not _captcha_pending: # Only close if no CAPTCHA pending
247
+ await context.close()
248
+
249
+ async def _wait_for_response(self, page) -> str:
250
+ """Wait for and extract the AI response from the DOM."""
251
+ last_text = ""
252
+ stable_count = 0
253
+ required_stable = 4
254
+
255
+ for i in range(self.RESPONSE_TIMEOUT * 2):
256
+ await asyncio.sleep(0.5)
257
+
258
+ # Extract response text
259
+ current_text = await page.evaluate("""
260
+ () => {
261
+ const selectors = [
262
+ '[data-testid="assistant-message"]',
263
+ '.message-content',
264
+ '[data-message-author-role="assistant"]',
265
+ '.ac-textBlock',
266
+ '[class*="response"]',
267
+ '[class*="message"] div',
268
+ '.markdown-body',
269
+ ];
270
+
271
+ for (const sel of selectors) {
272
+ const els = document.querySelectorAll(sel);
273
+ if (els.length > 0) {
274
+ const last = els[els.length - 1];
275
+ const text = last.innerText || last.textContent || '';
276
+ if (text.trim().length > 10) return text.trim();
277
+ }
278
+ }
279
+ return '';
280
+ }
281
+ """)
282
+
283
+ if not current_text:
284
+ continue
285
+
286
+ # Clean the text
287
+ clean = self._clean_response(current_text)
288
+
289
+ if clean == last_text and len(clean) > 0:
290
+ stable_count += 1
291
+ if stable_count >= required_stable:
292
+ return clean
293
+ else:
294
+ stable_count = 0
295
+ last_text = clean
296
+
297
+ if i % 10 == 9:
298
+ logger.info(f"Copilot: Stream... {len(last_text)} chars")
299
+
300
+ if last_text:
301
+ logger.warning("Copilot: Timeout, returning partial.")
302
+ return last_text
303
+
304
+ raise TimeoutError("Copilot no response")
305
+
306
+ def _clean_response(self, text: str) -> str:
307
+ """Clean up Copilot response text."""
308
+ clean = text.strip()
309
+
310
+ # Remove common UI artifacts
311
+ clean = re.sub(r"^(Copilot\s*|Microsoft Copilot\s*)", "", clean, flags=re.IGNORECASE)
312
+ clean = re.sub(r"\n+\s*\n+", "\n\n", clean)
313
+
314
+ return clean.strip()
315
+
316
+ @staticmethod
317
+ def is_captcha_pending() -> bool:
318
+ """Check if there's a pending CAPTCHA challenge."""
319
+ global _captcha_pending
320
+ return _captcha_pending
321
+
322
+ @staticmethod
323
+ def get_captcha_context():
324
+ """Get the browser context with pending CAPTCHA."""
325
+ global _captcha_context
326
+ return _captcha_context
327
+
328
+ @staticmethod
329
+ def clear_captcha_pending():
330
+ """Clear the CAPTCHA pending state."""
331
+ global _captcha_pending, _captcha_context
332
+ _captcha_pending = False
333
+ _captcha_context = None
static/qaz.html CHANGED
@@ -521,6 +521,112 @@
521
  lookupToken();
522
  }
523
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
524
  </script>
525
  </body>
526
 
 
521
  lookupToken();
522
  }
523
  });
524
+
525
+ // Copilot CAPTCHA handling
526
+ async function checkCopilotCaptchaStatus() {
527
+ try {
528
+ const res = await fetch('/qaz/copilot/captcha/status');
529
+ const data = await res.json();
530
+
531
+ if (data.captcha_required) {
532
+ document.getElementById('copilot-captcha-section').style.display = 'block';
533
+ document.getElementById('copilot-captcha-img').src = data.screenshot_url + '?t=' + new Date().getTime();
534
+ } else {
535
+ document.getElementById('copilot-captcha-section').style.display = 'none';
536
+ }
537
+ } catch (e) {
538
+ console.error('Failed to check Copilot CAPTCHA status:', e);
539
+ }
540
+ }
541
+
542
+ async function markCaptchaSolved() {
543
+ try {
544
+ const res = await fetch('/qaz/copilot/captcha/solved', { method: 'POST' });
545
+ const data = await res.json();
546
+ alert(data.message);
547
+ document.getElementById('copilot-captcha-section').style.display = 'none';
548
+ } catch (e) {
549
+ alert('Error: ' + e.message);
550
+ }
551
+ }
552
+
553
+ async function clearCaptchaState() {
554
+ if (!confirm('Clear CAPTCHA state and retry?')) return;
555
+ try {
556
+ const res = await fetch('/qaz/copilot/captcha/clear', { method: 'POST' });
557
+ const data = await res.json();
558
+ alert(data.message);
559
+ document.getElementById('copilot-captcha-section').style.display = 'none';
560
+ } catch (e) {
561
+ alert('Error: ' + e.message);
562
+ }
563
+ }
564
+
565
+ // Check CAPTCHA status periodically
566
+ checkCopilotCaptchaStatus();
567
+ setInterval(checkCopilotCaptchaStatus, 10000);
568
+ </script>
569
+
570
+ <!-- Copilot CAPTCHA Section -->
571
+ <div id="copilot-captcha-section" class="card" style="margin-top: 30px; display: none; border: 2px solid var(--warning);">
572
+ <h2 style="color: var(--warning); margin-bottom: 15px;">⚠️ Copilot CAPTCHA Required</h2>
573
+ <p style="color: var(--text-muted); margin-bottom: 20px;">
574
+ Microsoft Copilot is requesting a CAPTCHA verification. Please solve it below, then click "I've Solved It".
575
+ </p>
576
+
577
+ <div style="background: var(--bg); border: 1px solid var(--border); border-radius: 8px; padding: 20px; margin-bottom: 20px;">
578
+ <img id="copilot-captcha-img" src="" alt="CAPTCHA Screenshot" style="max-width: 100%; border-radius: 4px; display: block; margin: 0 auto;">
579
+ </div>
580
+
581
+ <div style="display: flex; gap: 10px;">
582
+ <button onclick="markCaptchaSolved()" style="background: var(--success); color: white; border: none; padding: 12px 24px; border-radius: 6px; font-weight: 600; cursor: pointer; flex: 1;">
583
+ ✅ I've Solved It
584
+ </button>
585
+ <button onclick="checkCopilotCaptchaStatus()" style="background: var(--accent); color: white; border: none; padding: 12px 24px; border-radius: 6px; font-weight: 600; cursor: pointer;">
586
+ 🔄 Refresh Screenshot
587
+ </button>
588
+ <button onclick="clearCaptchaState()" style="background: transparent; color: var(--error); border: 1px solid var(--error); padding: 12px 24px; border-radius: 6px; font-weight: 600; cursor: pointer;">
589
+ 🗑️ Clear & Retry
590
+ </button>
591
+ </div>
592
+ </div>
593
+
594
+ <!-- Copilot Session Status -->
595
+ <div class="card" style="margin-top: 30px;">
596
+ <h2 style="margin-bottom: 15px;">Copilot Session Status</h2>
597
+ <div id="copilot-session-info" style="color: var(--text-muted);">
598
+ <button onclick="checkCopilotSession()" style="background: var(--accent); color: white; border: none; padding: 8px 16px; border-radius: 6px; cursor: pointer;">Check Status</button>
599
+ </div>
600
+ </div>
601
+
602
+ <script>
603
+ async function checkCopilotSession() {
604
+ try {
605
+ const res = await fetch('/qaz/copilot/session/status');
606
+ const data = await res.json();
607
+
608
+ const infoDiv = document.getElementById('copilot-session-info');
609
+ if (data.has_session) {
610
+ infoDiv.innerHTML = `
611
+ <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px;">
612
+ <div><strong>Status:</strong> <span style="color: var(--success);">✅ Active</span></div>
613
+ <div><strong>Cookies:</strong> ${data.cookie_count}</div>
614
+ <div><strong>Days Remaining:</strong> ${data.days_remaining}</div>
615
+ <div><strong>Expires:</strong> ${new Date(data.expires_at).toLocaleDateString()}</div>
616
+ </div>
617
+ <button onclick="checkCopilotSession()" style="background: var(--accent); color: white; border: none; padding: 8px 16px; border-radius: 6px; cursor: pointer; margin-top: 15px;">Refresh</button>
618
+ `;
619
+ } else {
620
+ infoDiv.innerHTML = `
621
+ <div><strong>Status:</strong> <span style="color: var(--error);">❌ No Active Session</span></div>
622
+ <p style="margin-top: 10px; font-size: 13px;">${data.message}</p>
623
+ <button onclick="checkCopilotSession()" style="background: var(--accent); color: white; border: none; padding: 8px 16px; border-radius: 6px; cursor: pointer; margin-top: 10px;">Refresh</button>
624
+ `;
625
+ }
626
+ } catch (e) {
627
+ document.getElementById('copilot-session-info').innerHTML = '<span style="color: var(--error);">Error checking status</span>';
628
+ }
629
+ }
630
  </script>
631
  </body>
632