150 lines
5.6 KiB
Python
Executable File
150 lines
5.6 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
WEVIA Claude Web — Navigateur headless pour claude.ai
|
|
Utilise le forfait Claude Max via Playwright
|
|
Port: 8903
|
|
PRÉREQUIS: Se connecter à claude.ai via Blade pour sauvegarder les cookies
|
|
"""
|
|
import json, os, time, threading, traceback
|
|
from http.server import HTTPServer, BaseHTTPRequestHandler
|
|
|
|
PORT = 8903
|
|
SESSION_DIR = '/opt/weval-l99/claude-session'
|
|
LOCK = threading.Lock()
|
|
|
|
browser = None
|
|
context = None
|
|
page = None
|
|
ready = False
|
|
last_error = ''
|
|
|
|
def init_browser():
|
|
global browser, context, page, ready, last_error
|
|
try:
|
|
from playwright.sync_api import sync_playwright
|
|
pw = sync_playwright().start()
|
|
browser = pw.chromium.launch(
|
|
headless=True,
|
|
args=['--no-sandbox', '--disable-gpu', '--disable-blink-features=AutomationControlled']
|
|
)
|
|
os.makedirs(SESSION_DIR, exist_ok=True)
|
|
state_file = os.path.join(SESSION_DIR, 'state.json')
|
|
|
|
ctx_args = {
|
|
'viewport': {'width': 1280, 'height': 900},
|
|
'user_agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/131.0.0.0 Safari/537.36',
|
|
}
|
|
if os.path.exists(state_file):
|
|
ctx_args['storage_state'] = state_file
|
|
|
|
context = browser.new_context(**ctx_args)
|
|
page = context.new_page()
|
|
page.goto('https://claude.ai/new', timeout=20000, wait_until='domcontentloaded')
|
|
page.wait_for_timeout(3000)
|
|
|
|
# Check if we're on chat page
|
|
textarea = page.query_selector('div[contenteditable], textarea')
|
|
if textarea:
|
|
ready = True
|
|
last_error = ''
|
|
else:
|
|
ready = False
|
|
last_error = f"No textarea found. URL={page.url} Title={page.title()}"
|
|
except Exception as e:
|
|
last_error = str(e)[:200]
|
|
ready = False
|
|
|
|
def send_message(msg):
|
|
global page, ready, last_error
|
|
if not ready:
|
|
return None, "Not ready: " + last_error
|
|
with LOCK:
|
|
try:
|
|
# Find input
|
|
textarea = page.query_selector('div[contenteditable="true"]') or page.query_selector('textarea')
|
|
if not textarea:
|
|
return None, "No textarea found"
|
|
|
|
textarea.click()
|
|
textarea.fill(msg)
|
|
page.wait_for_timeout(500)
|
|
|
|
# Click send button
|
|
send_btn = page.query_selector('button[aria-label="Send Message"]') or page.query_selector('button[type="submit"]')
|
|
if send_btn:
|
|
send_btn.click()
|
|
else:
|
|
page.keyboard.press('Enter')
|
|
|
|
# Wait for response
|
|
time.sleep(2)
|
|
for _ in range(30): # 30 seconds max
|
|
page.wait_for_timeout(1000)
|
|
# Check if response is complete (no loading indicator)
|
|
loading = page.query_selector('[class*="loading"]') or page.query_selector('[class*="streaming"]')
|
|
if not loading:
|
|
break
|
|
|
|
# Get last assistant message
|
|
messages = page.query_selector_all('[data-testid*="message"], .prose, [class*="response"]')
|
|
if messages:
|
|
last_msg = messages[-1].inner_text()
|
|
return last_msg, None
|
|
|
|
return None, "No response found"
|
|
except Exception as e:
|
|
return None, str(e)[:200]
|
|
|
|
class Handler(BaseHTTPRequestHandler):
|
|
def do_GET(self):
|
|
if self.path == '/health':
|
|
data = {"status": "ready" if ready else "not_ready", "port": PORT}
|
|
if last_error: data["error"] = last_error
|
|
self.send_response(200)
|
|
self.send_header('Content-Type', 'application/json')
|
|
self.end_headers()
|
|
self.wfile.write(json.dumps(data).encode())
|
|
else:
|
|
self.send_response(404)
|
|
self.end_headers()
|
|
|
|
def do_POST(self):
|
|
if self.path == '/chat':
|
|
length = int(self.headers.get('Content-Length', 0))
|
|
body = json.loads(self.rfile.read(length)) if length else {}
|
|
msg = body.get('message', '')
|
|
|
|
if not msg:
|
|
self.send_response(400)
|
|
self.send_header('Content-Type', 'application/json')
|
|
self.end_headers()
|
|
self.wfile.write(json.dumps({"error": "No message"}).encode())
|
|
return
|
|
|
|
content, error = send_message(msg)
|
|
self.send_response(200 if content else 503)
|
|
self.send_header('Content-Type', 'application/json')
|
|
self.end_headers()
|
|
result = {"content": content, "provider": "Claude.ai Max", "model": "claude-opus-4"} if content else {"error": error}
|
|
self.wfile.write(json.dumps(result).encode())
|
|
|
|
elif self.path == '/save_cookies':
|
|
try:
|
|
context.storage_state(path=os.path.join(SESSION_DIR, 'state.json'))
|
|
self.send_response(200)
|
|
self.send_header('Content-Type', 'application/json')
|
|
self.end_headers()
|
|
self.wfile.write(json.dumps({"ok": True}).encode())
|
|
except Exception as e:
|
|
self.send_response(500)
|
|
self.send_header('Content-Type', 'application/json')
|
|
self.end_headers()
|
|
self.wfile.write(json.dumps({"error": str(e)}).encode())
|
|
|
|
def log_message(self, format, *args): pass
|
|
|
|
if __name__ == '__main__':
|
|
threading.Thread(target=init_browser, daemon=True).start()
|
|
print(f"Claude Web API on port {PORT}")
|
|
HTTPServer(('127.0.0.1', PORT), Handler).serve_forever()
|