infinityonline commited on
Commit
56ab5c2
ยท
verified ยท
1 Parent(s): bd2c875

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +90 -121
main.py CHANGED
@@ -65,7 +65,6 @@ class AsyncBrowserThread(threading.Thread):
65
  ],
66
  )
67
 
68
- # โ”€โ”€ Context ุฏุงุฆู…: cookies ุชูุญูุธ ุจูŠู† ุงู„ู€ requests โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
69
  self.persistent_context = await self.browser.new_context(
70
  user_agent=(
71
  "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
@@ -78,82 +77,55 @@ class AsyncBrowserThread(threading.Thread):
78
  "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
79
  )
80
 
81
- # โ”€โ”€ ู‚ุจูˆู„ ุดุฑูˆุท duck.ai ู…ุฑุฉ ูˆุงุญุฏุฉ ุนู†ุฏ ุงู„ุฅู‚ู„ุงุน โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
82
  setup_page = await self.persistent_context.new_page()
83
  try:
84
- print("[SERVER] Opening duck.ai for first-time setup...")
85
  await setup_page.goto(
86
  "https://duckduckgo.com/aichat", wait_until="domcontentloaded"
87
  )
88
  await asyncio.sleep(5)
89
-
90
- # ุฒุฑ "Agree and Continue" ู…ู† ุงู„ุตูˆุฑุฉ ุงู„ุญู‚ูŠู‚ูŠุฉ
91
  agree_btn = setup_page.locator('button:has-text("Agree and Continue")')
92
  await agree_btn.wait_for(state="visible", timeout=12000)
93
  await agree_btn.click()
94
- print("[SERVER] Duck.ai terms accepted โœ“")
95
  await asyncio.sleep(3)
96
-
97
- # ุงู†ุชุธุฑ ุธู‡ูˆุฑ ุตู†ุฏูˆู‚ ุงู„ูƒุชุงุจุฉ = ุงู„ู…ูˆู‚ุน ุฌุงู‡ุฒ
98
  await setup_page.wait_for_selector(
99
  'textarea[name="user-prompt"]', timeout=20000
100
  )
101
- print("[SERVER] Duck.ai chat interface ready โœ“")
102
-
103
  except Exception as e:
104
- print(f"[SERVER] Setup note (non-fatal): {e}")
105
  finally:
106
  await setup_page.close()
107
 
108
- # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
109
- # ุงู„ุฏุงู„ุฉ ุงู„ุฑุฆูŠุณูŠุฉ: ุฅุฑุณุงู„ prompt ูˆุงุณุชู‚ุจุงู„ ุงู„ุฑุฏ
110
- # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
111
  async def _chat(self, model_label: str, prompt: str) -> str:
112
- # ูƒู„ request ูŠุฃุฎุฐ page ู…ู†ูุตู„ุฉ ู…ู† ู†ูุณ ุงู„ู€ context ุงู„ู…ุญููˆุธ
113
  page = await self.persistent_context.new_page()
114
-
115
  try:
116
  page.set_default_timeout(120000)
117
 
118
- # โ”€โ”€ 1. ูุชุญ ุตูุญุฉ ุฌุฏูŠุฏุฉ ู†ุธูŠูุฉ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
119
  await page.goto(
120
  "https://duckduckgo.com/aichat", wait_until="domcontentloaded"
121
  )
122
  await asyncio.sleep(3)
123
 
124
- # โ”€โ”€ 2. ุฅุบู„ุงู‚ ุฃูŠ popup/banner ุฅุถุงููŠ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
125
- # Agree and Continue ุฅู† ุธู‡ุฑ ู…ุฌุฏุฏุงู‹ (ุงุญุชูŠุงุท)
126
- try:
127
- agree_btn = page.locator('button:has-text("Agree and Continue")')
128
- if await agree_btn.count() > 0:
129
- is_visible = await agree_btn.first.is_visible()
130
- if is_visible:
131
- await agree_btn.first.click()
132
- print("[DUCK] Terms re-accepted โœ“")
133
- await asyncio.sleep(2)
134
- except Exception:
135
- pass
136
-
137
- # ุฅุบู„ุงู‚ ุฅุดุนุงุฑ PDF
138
  try:
139
- close_btn = page.locator(
140
- 'button.XkSxBJ8ofSQsZmGZs6qx, '
141
- 'li.HmVD0odzmaobZhTx3jzd button[aria-label="Close"], '
142
- 'button[aria-label="Close"]'
143
- )
144
- if await close_btn.count() > 0:
145
- await close_btn.first.click()
146
- await asyncio.sleep(1)
147
  except Exception:
148
  pass
149
 
150
- # โ”€โ”€ 3. ุงู†ุชุธุงุฑ ุตู†ุฏูˆู‚ ุงู„ูƒุชุงุจุฉ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
151
  await page.wait_for_selector(
152
  'textarea[name="user-prompt"]', timeout=30000
153
  )
154
  print("[DUCK] Input ready โœ“")
155
 
156
- # โ”€โ”€ 4. ุชุบูŠูŠุฑ ุงู„ู†ู…ูˆุฐุฌ ุฅุฐุง ู„ุฒู… โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
157
  try:
158
  model_btn = page.locator('button[data-testid="model-select-button"]')
159
  current_text = await model_btn.inner_text()
@@ -161,7 +133,7 @@ class AsyncBrowserThread(threading.Thread):
161
 
162
  if model_label.lower() not in current_text.lower():
163
  await model_btn.click()
164
- await asyncio.sleep(1.5)
165
 
166
  option = page.locator(
167
  f"li:has-text('{model_label}'), "
@@ -170,24 +142,59 @@ class AsyncBrowserThread(threading.Thread):
170
  )
171
  if await option.count() > 0:
172
  await option.first.click()
173
- await asyncio.sleep(1)
174
  print(f"[DUCK] Model โ†’ {model_label} โœ“")
175
  else:
176
  await page.keyboard.press("Escape")
177
- print(f"[DUCK] Model '{model_label}' not found, using default")
 
 
 
 
 
 
178
  except Exception as e:
179
  print(f"[DUCK] Model select (non-fatal): {e}")
180
 
181
- # โ”€โ”€ 5. ูƒุชุงุจุฉ ูˆุฅุฑุณุงู„ ุงู„ุฑุณุงู„ุฉ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
 
182
  textarea = page.locator('textarea[name="user-prompt"]')
183
  await textarea.click()
184
- await textarea.fill(prompt)
185
  await asyncio.sleep(0.5)
186
 
187
- send_btn = page.locator('button[type="submit"][aria-label="Send"]')
188
- await send_btn.wait_for(state="enabled", timeout=10000)
189
- await send_btn.click()
190
- print(f"[DUCK] Sent ({len(prompt)} chars) โœ“")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
191
 
192
  # โ”€โ”€ 6. ุงู†ุชุธุงุฑ ุจุฏุก ุงู„ุฑุฏ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
193
  await asyncio.sleep(3)
@@ -196,59 +203,56 @@ class AsyncBrowserThread(threading.Thread):
196
  await stop_btn.wait_for(state="visible", timeout=20000)
197
  print("[DUCK] Response started โœ“")
198
  except Exception:
199
- print("[DUCK] Stop button not detected, continuing...")
200
 
201
  # โ”€โ”€ 7. ุงู†ุชุธุงุฑ ุงูƒุชู…ุงู„ ุงู„ุฑุฏ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
202
- # ุงู„ุฑุฏ ุงูƒุช๏ฟฝ๏ฟฝู„ = ุฒุฑ "Stop generating" ุงุฎุชูู‰ ุฃูˆ ุฃุตุจุญ disabled
203
  max_wait = 120
204
  elapsed = 0
205
  while elapsed < max_wait:
206
  await asyncio.sleep(2)
207
  elapsed += 2
208
- stop_active = await page.locator(
209
  'button[aria-label="Stop generating"]:not([disabled])'
210
  ).count()
211
- if stop_active == 0:
212
- print(f"[DUCK] Response complete (~{elapsed}s) โœ“")
213
  break
214
 
215
  await asyncio.sleep(1.5)
216
 
217
- # โ”€โ”€ 8. ุงุณุชุฎุฑุงุฌ ุงู„ู†ุต โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
218
  response_text = await page.evaluate("""
219
  () => {
220
- // ุทุฑูŠู‚ุฉ 1: article elements
221
  const articles = document.querySelectorAll('article');
222
  if (articles.length > 0) {
223
  return articles[articles.length - 1].innerText.trim();
224
  }
225
-
226
- // ุทุฑูŠู‚ุฉ 2: divs ุชุญุชูˆูŠ ุนู„ู‰ "message" ููŠ class
227
  const msgDivs = document.querySelectorAll(
228
  '[class*="message"]:not([class*="user"])' +
229
  ':not([class*="User"]):not([class*="input"])'
230
  );
231
  if (msgDivs.length > 0) {
232
- const textDivs = [...msgDivs].filter(el =>
233
  el.innerText &&
234
  el.innerText.trim().length > 20 &&
235
  !el.querySelector('textarea')
236
  );
237
- if (textDivs.length > 0) {
238
- return textDivs[textDivs.length - 1].innerText.trim();
239
  }
240
  }
241
-
242
- // ุทุฑูŠู‚ุฉ 3: ุฃุทูˆู„ div ุจุฏูˆู† textarea
243
- const allDivs = [...document.querySelectorAll('div')].filter(el =>
244
  el.children.length < 10 &&
245
  el.innerText &&
246
  el.innerText.trim().length > 50 &&
247
  !el.querySelector('textarea') &&
248
  !el.querySelector('button[type="submit"]')
249
  );
250
- if (allDivs.length > 0) {
251
- return allDivs.sort(
252
  (a, b) => b.innerText.length - a.innerText.length
253
  )[0].innerText.trim();
254
  }
@@ -256,15 +260,13 @@ class AsyncBrowserThread(threading.Thread):
256
  }
257
  """)
258
 
259
- # โ”€โ”€ 9. fallback ุฅุฐุง ูƒุงู† ุงู„ู†ุต ูุงุฑุบุงู‹ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
260
  if not response_text or len(response_text.strip()) < 10:
261
  await asyncio.sleep(5)
262
  response_text = await page.evaluate("""
263
  () => {
264
- const articles = document.querySelectorAll('article');
265
- if (articles.length > 0) {
266
- return articles[articles.length - 1].innerText.trim();
267
- }
268
  return document.body.innerText.slice(0, 5000);
269
  }
270
  """)
@@ -278,10 +280,9 @@ class AsyncBrowserThread(threading.Thread):
278
  finally:
279
  await page.close()
280
 
281
- # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
282
  def process(self, model_label: str, prompt: str) -> str:
283
  if not self.ready_event.wait(timeout=120):
284
- raise RuntimeError("Browser not ready after 120s")
285
  future = asyncio.run_coroutine_threadsafe(
286
  self._chat(model_label, prompt), self.loop
287
  )
@@ -359,8 +360,7 @@ def _parse_tool_calls(text: str):
359
 
360
 
361
  def _auth(request: Request) -> bool:
362
- token = request.headers.get("authorization", "").replace("Bearer ", "").strip()
363
- return token == API_SECRET_KEY
364
 
365
 
366
  def _get_model_label(model: str) -> str:
@@ -373,20 +373,16 @@ def _make_completion(start_time, model, text, messages, tools=None):
373
  tc = _parse_tool_calls(text) if tools else None
374
  if tc:
375
  return {
376
- "id": f"chatcmpl-{uuid.uuid4().hex[:29]}",
377
- "object": "chat.completion",
378
- "created": int(start_time),
379
- "model": model,
380
  "choices": [{"index": 0, "message": {
381
  "role": "assistant", "content": None, "tool_calls": tc
382
  }, "finish_reason": "tool_calls"}],
383
  "usage": {"prompt_tokens": p, "completion_tokens": c, "total_tokens": p + c},
384
  }
385
  return {
386
- "id": f"chatcmpl-{uuid.uuid4().hex[:29]}",
387
- "object": "chat.completion",
388
- "created": int(start_time),
389
- "model": model,
390
  "choices": [{"index": 0, "message": {
391
  "role": "assistant", "content": text
392
  }, "finish_reason": "stop"}],
@@ -406,10 +402,8 @@ async def chat_completions(request: Request):
406
  data = await request.json()
407
  except Exception:
408
  return JSONResponse(status_code=400, content={"error": {"message": "Invalid JSON"}})
409
-
410
  if not _auth(request):
411
  return JSONResponse(status_code=401, content={"error": {"message": "Invalid API Key"}})
412
-
413
  messages = data.get("messages", [])
414
  if not messages:
415
  return JSONResponse(status_code=400, content={"error": {"message": "messages required"}})
@@ -421,7 +415,6 @@ async def chat_completions(request: Request):
421
  prompt = _build_prompt(messages)
422
 
423
  print(f"[API] /v1/chat/completions โ†’ {model} ({model_label})")
424
-
425
  try:
426
  text = await asyncio.get_event_loop().run_in_executor(
427
  None, browser_engine.process, model_label, prompt
@@ -438,7 +431,6 @@ async def responses(request: Request):
438
  data = await request.json()
439
  except Exception:
440
  return JSONResponse(status_code=400, content={"error": {"message": "Invalid JSON"}})
441
-
442
  if not _auth(request):
443
  return JSONResponse(status_code=401, content={"error": {"message": "Invalid API Key"}})
444
 
@@ -449,7 +441,6 @@ async def responses(request: Request):
449
  messages = input_data
450
  else:
451
  messages = data.get("messages", [])
452
-
453
  if not messages:
454
  return JSONResponse(status_code=400, content={"error": {"message": "input required"}})
455
 
@@ -464,45 +455,30 @@ async def responses(request: Request):
464
  prompt = _build_prompt(messages)
465
 
466
  print(f"[API] /v1/responses โ†’ {model} ({model_label})")
467
-
468
  try:
469
  text = await asyncio.get_event_loop().run_in_executor(
470
  None, browser_engine.process, model_label, prompt
471
  )
472
-
473
  p = sum(len(_extract_content(m).split()) for m in messages)
474
  c = len(text.split())
475
  tc = _parse_tool_calls(text) if tools else None
476
-
477
  if tc:
478
  return {
479
- "id": f"resp-{uuid.uuid4().hex[:29]}",
480
- "object": "response",
481
- "created_at": int(start_time),
482
- "model": model,
483
- "status": "completed",
484
- "output": [{
485
- "type": "function_call",
486
- "id": t["id"],
487
- "call_id": t["id"],
488
- "name": t["function"]["name"],
489
- "arguments": t["function"]["arguments"],
490
- "status": "completed",
491
- } for t in tc],
492
  "usage": {"input_tokens": p, "output_tokens": c, "total_tokens": p + c},
493
  }
494
-
495
  return {
496
- "id": f"resp-{uuid.uuid4().hex[:29]}",
497
- "object": "response",
498
- "created_at": int(start_time),
499
- "model": model,
500
- "status": "completed",
501
  "output": [{"type": "message", "role": "assistant",
502
  "content": [{"type": "output_text", "text": text}]}],
503
  "usage": {"input_tokens": p, "output_tokens": c, "total_tokens": p + c},
504
  }
505
-
506
  except Exception as e:
507
  print(f"[API] ERROR: {e}")
508
  return JSONResponse(status_code=500, content={"error": {"message": str(e)}})
@@ -514,21 +490,14 @@ async def list_models(request: Request):
514
  return JSONResponse(status_code=401, content={"error": {"message": "Invalid API Key"}})
515
  return {
516
  "object": "list",
517
- "data": [
518
- {"id": m, "object": "model", "owned_by": "duck.ai"}
519
- for m in ALL_MODELS
520
- ],
521
  }
522
 
523
 
524
  @app.get("/health")
525
  @app.get("/")
526
  async def health():
527
- return {
528
- "status": "running",
529
- "message": "Duck.ai API Server is active!",
530
- "models": ALL_MODELS,
531
- }
532
 
533
 
534
  if __name__ == "__main__":
 
65
  ],
66
  )
67
 
 
68
  self.persistent_context = await self.browser.new_context(
69
  user_agent=(
70
  "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
 
77
  "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
78
  )
79
 
80
+ # ู‚ุจูˆู„ ุงู„ุดุฑูˆุท ู…ุฑุฉ ูˆุงุญุฏุฉ ุนู†ุฏ ุงู„ุฅู‚ู„ุงุน
81
  setup_page = await self.persistent_context.new_page()
82
  try:
83
+ print("[SERVER] Opening duck.ai for setup...")
84
  await setup_page.goto(
85
  "https://duckduckgo.com/aichat", wait_until="domcontentloaded"
86
  )
87
  await asyncio.sleep(5)
 
 
88
  agree_btn = setup_page.locator('button:has-text("Agree and Continue")')
89
  await agree_btn.wait_for(state="visible", timeout=12000)
90
  await agree_btn.click()
91
+ print("[SERVER] Terms accepted โœ“")
92
  await asyncio.sleep(3)
 
 
93
  await setup_page.wait_for_selector(
94
  'textarea[name="user-prompt"]', timeout=20000
95
  )
96
+ print("[SERVER] Interface ready โœ“")
 
97
  except Exception as e:
98
+ print(f"[SERVER] Setup note: {e}")
99
  finally:
100
  await setup_page.close()
101
 
 
 
 
102
  async def _chat(self, model_label: str, prompt: str) -> str:
 
103
  page = await self.persistent_context.new_page()
 
104
  try:
105
  page.set_default_timeout(120000)
106
 
107
+ # โ”€โ”€ 1. ูุชุญ ุตูุญุฉ ุฌุฏูŠุฏุฉ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
108
  await page.goto(
109
  "https://duckduckgo.com/aichat", wait_until="domcontentloaded"
110
  )
111
  await asyncio.sleep(3)
112
 
113
+ # โ”€โ”€ 2. ู‚ุจูˆู„ Popup ุงุญุชูŠุงุทูŠ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  try:
115
+ agree = page.locator('button:has-text("Agree and Continue")')
116
+ if await agree.count() > 0 and await agree.first.is_visible():
117
+ await agree.first.click()
118
+ await asyncio.sleep(2)
 
 
 
 
119
  except Exception:
120
  pass
121
 
122
+ # โ”€โ”€ 3. ุงู†ุชุธุงุฑ textarea โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
123
  await page.wait_for_selector(
124
  'textarea[name="user-prompt"]', timeout=30000
125
  )
126
  print("[DUCK] Input ready โœ“")
127
 
128
+ # โ”€โ”€ 4. ุชุบูŠูŠุฑ ุงู„ู†ู…ูˆุฐุฌ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
129
  try:
130
  model_btn = page.locator('button[data-testid="model-select-button"]')
131
  current_text = await model_btn.inner_text()
 
133
 
134
  if model_label.lower() not in current_text.lower():
135
  await model_btn.click()
136
+ await asyncio.sleep(2)
137
 
138
  option = page.locator(
139
  f"li:has-text('{model_label}'), "
 
142
  )
143
  if await option.count() > 0:
144
  await option.first.click()
 
145
  print(f"[DUCK] Model โ†’ {model_label} โœ“")
146
  else:
147
  await page.keyboard.press("Escape")
148
+ print(f"[DUCK] Model not found, using default")
149
+
150
+ # ุงู†ุชุธุฑ ุฅุนุงุฏุฉ ุชุญู…ูŠู„ ุงู„ู€ textarea ุจุนุฏ ุชุบูŠูŠุฑ ุงู„ู†ู…ูˆุฐุฌ
151
+ await asyncio.sleep(2)
152
+ await page.wait_for_selector(
153
+ 'textarea[name="user-prompt"]', timeout=15000
154
+ )
155
  except Exception as e:
156
  print(f"[DUCK] Model select (non-fatal): {e}")
157
 
158
+ # โ”€โ”€ 5. ุฅุฑุณุงู„ ุงู„ุฑุณุงู„ุฉ ุจู€ JavaScript ู…ุจุงุดุฑุฉ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
159
+ # ู‡ุฐุง ูŠุชุฌุงูˆุฒ ู…ุดูƒู„ุฉ ุฒุฑ Send ุงู„ู€ disabled
160
  textarea = page.locator('textarea[name="user-prompt"]')
161
  await textarea.click()
 
162
  await asyncio.sleep(0.5)
163
 
164
+ # ุงุณุชุฎุฏุงู… JavaScript ู„ุถุจุท ุงู„ู‚ูŠู…ุฉ ูˆุฅุทู„ุงู‚ ุงู„ุฃุญุฏุงุซ
165
+ await page.evaluate("""
166
+ (text) => {
167
+ const ta = document.querySelector('textarea[name="user-prompt"]');
168
+ if (!ta) return;
169
+ // React/Vue ูŠุญุชุงุฌ nativeInputValueSetter
170
+ const nativeSetter = Object.getOwnPropertyDescriptor(
171
+ window.HTMLTextAreaElement.prototype, 'value'
172
+ ).set;
173
+ nativeSetter.call(ta, text);
174
+ ta.dispatchEvent(new Event('input', { bubbles: true }));
175
+ ta.dispatchEvent(new Event('change', { bubbles: true }));
176
+ }
177
+ """, prompt)
178
+ await asyncio.sleep(1)
179
+
180
+ # ุงู†ุชุธุฑ ุฃู† ูŠุตุจุญ ุฒุฑ Send enabled
181
+ send_enabled = False
182
+ for _ in range(10):
183
+ disabled = await page.locator(
184
+ 'button[type="submit"][aria-label="Send"]'
185
+ ).get_attribute("disabled")
186
+ if disabled is None:
187
+ send_enabled = True
188
+ break
189
+ await asyncio.sleep(0.5)
190
+
191
+ if send_enabled:
192
+ await page.locator('button[type="submit"][aria-label="Send"]').click()
193
+ print(f"[DUCK] Sent via button โœ“ ({len(prompt)} chars)")
194
+ else:
195
+ # fallback: Enter ู…ู† ู„ูˆุญุฉ ุงู„ู…ูุงุชูŠุญ
196
+ await textarea.press("Enter")
197
+ print(f"[DUCK] Sent via Enter โœ“ ({len(prompt)} chars)")
198
 
199
  # โ”€โ”€ 6. ุงู†ุชุธุงุฑ ุจุฏุก ุงู„ุฑุฏ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
200
  await asyncio.sleep(3)
 
203
  await stop_btn.wait_for(state="visible", timeout=20000)
204
  print("[DUCK] Response started โœ“")
205
  except Exception:
206
+ print("[DUCK] Stop btn not visible, continuing...")
207
 
208
  # โ”€โ”€ 7. ุงู†ุชุธุงุฑ ุงูƒุชู…ุงู„ ุงู„ุฑุฏ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
 
209
  max_wait = 120
210
  elapsed = 0
211
  while elapsed < max_wait:
212
  await asyncio.sleep(2)
213
  elapsed += 2
214
+ still_running = await page.locator(
215
  'button[aria-label="Stop generating"]:not([disabled])'
216
  ).count()
217
+ if still_running == 0:
218
+ print(f"[DUCK] Done after ~{elapsed}s โœ“")
219
  break
220
 
221
  await asyncio.sleep(1.5)
222
 
223
+ # โ”€โ”€ 8. ุงุณุชุฎุฑุงุฌ ุงู„ุฑุฏ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
224
  response_text = await page.evaluate("""
225
  () => {
226
+ // ุทุฑูŠู‚ุฉ 1: article
227
  const articles = document.querySelectorAll('article');
228
  if (articles.length > 0) {
229
  return articles[articles.length - 1].innerText.trim();
230
  }
231
+ // ุทุฑูŠู‚ุฉ 2: class ูŠุญุชูˆูŠ message
 
232
  const msgDivs = document.querySelectorAll(
233
  '[class*="message"]:not([class*="user"])' +
234
  ':not([class*="User"]):not([class*="input"])'
235
  );
236
  if (msgDivs.length > 0) {
237
+ const valid = [...msgDivs].filter(el =>
238
  el.innerText &&
239
  el.innerText.trim().length > 20 &&
240
  !el.querySelector('textarea')
241
  );
242
+ if (valid.length > 0) {
243
+ return valid[valid.length - 1].innerText.trim();
244
  }
245
  }
246
+ // ุทุฑูŠู‚ุฉ 3: ุฃุทูˆู„ div
247
+ const divs = [...document.querySelectorAll('div')].filter(el =>
 
248
  el.children.length < 10 &&
249
  el.innerText &&
250
  el.innerText.trim().length > 50 &&
251
  !el.querySelector('textarea') &&
252
  !el.querySelector('button[type="submit"]')
253
  );
254
+ if (divs.length > 0) {
255
+ return divs.sort(
256
  (a, b) => b.innerText.length - a.innerText.length
257
  )[0].innerText.trim();
258
  }
 
260
  }
261
  """)
262
 
263
+ # fallback
264
  if not response_text or len(response_text.strip()) < 10:
265
  await asyncio.sleep(5)
266
  response_text = await page.evaluate("""
267
  () => {
268
+ const arts = document.querySelectorAll('article');
269
+ if (arts.length > 0) return arts[arts.length-1].innerText.trim();
 
 
270
  return document.body.innerText.slice(0, 5000);
271
  }
272
  """)
 
280
  finally:
281
  await page.close()
282
 
 
283
  def process(self, model_label: str, prompt: str) -> str:
284
  if not self.ready_event.wait(timeout=120):
285
+ raise RuntimeError("Browser not ready")
286
  future = asyncio.run_coroutine_threadsafe(
287
  self._chat(model_label, prompt), self.loop
288
  )
 
360
 
361
 
362
  def _auth(request: Request) -> bool:
363
+ return request.headers.get("authorization", "").replace("Bearer ", "").strip() == API_SECRET_KEY
 
364
 
365
 
366
  def _get_model_label(model: str) -> str:
 
373
  tc = _parse_tool_calls(text) if tools else None
374
  if tc:
375
  return {
376
+ "id": f"chatcmpl-{uuid.uuid4().hex[:29]}", "object": "chat.completion",
377
+ "created": int(start_time), "model": model,
 
 
378
  "choices": [{"index": 0, "message": {
379
  "role": "assistant", "content": None, "tool_calls": tc
380
  }, "finish_reason": "tool_calls"}],
381
  "usage": {"prompt_tokens": p, "completion_tokens": c, "total_tokens": p + c},
382
  }
383
  return {
384
+ "id": f"chatcmpl-{uuid.uuid4().hex[:29]}", "object": "chat.completion",
385
+ "created": int(start_time), "model": model,
 
 
386
  "choices": [{"index": 0, "message": {
387
  "role": "assistant", "content": text
388
  }, "finish_reason": "stop"}],
 
402
  data = await request.json()
403
  except Exception:
404
  return JSONResponse(status_code=400, content={"error": {"message": "Invalid JSON"}})
 
405
  if not _auth(request):
406
  return JSONResponse(status_code=401, content={"error": {"message": "Invalid API Key"}})
 
407
  messages = data.get("messages", [])
408
  if not messages:
409
  return JSONResponse(status_code=400, content={"error": {"message": "messages required"}})
 
415
  prompt = _build_prompt(messages)
416
 
417
  print(f"[API] /v1/chat/completions โ†’ {model} ({model_label})")
 
418
  try:
419
  text = await asyncio.get_event_loop().run_in_executor(
420
  None, browser_engine.process, model_label, prompt
 
431
  data = await request.json()
432
  except Exception:
433
  return JSONResponse(status_code=400, content={"error": {"message": "Invalid JSON"}})
 
434
  if not _auth(request):
435
  return JSONResponse(status_code=401, content={"error": {"message": "Invalid API Key"}})
436
 
 
441
  messages = input_data
442
  else:
443
  messages = data.get("messages", [])
 
444
  if not messages:
445
  return JSONResponse(status_code=400, content={"error": {"message": "input required"}})
446
 
 
455
  prompt = _build_prompt(messages)
456
 
457
  print(f"[API] /v1/responses โ†’ {model} ({model_label})")
 
458
  try:
459
  text = await asyncio.get_event_loop().run_in_executor(
460
  None, browser_engine.process, model_label, prompt
461
  )
 
462
  p = sum(len(_extract_content(m).split()) for m in messages)
463
  c = len(text.split())
464
  tc = _parse_tool_calls(text) if tools else None
 
465
  if tc:
466
  return {
467
+ "id": f"resp-{uuid.uuid4().hex[:29]}", "object": "response",
468
+ "created_at": int(start_time), "model": model, "status": "completed",
469
+ "output": [{"type": "function_call", "id": t["id"], "call_id": t["id"],
470
+ "name": t["function"]["name"],
471
+ "arguments": t["function"]["arguments"],
472
+ "status": "completed"} for t in tc],
 
 
 
 
 
 
 
473
  "usage": {"input_tokens": p, "output_tokens": c, "total_tokens": p + c},
474
  }
 
475
  return {
476
+ "id": f"resp-{uuid.uuid4().hex[:29]}", "object": "response",
477
+ "created_at": int(start_time), "model": model, "status": "completed",
 
 
 
478
  "output": [{"type": "message", "role": "assistant",
479
  "content": [{"type": "output_text", "text": text}]}],
480
  "usage": {"input_tokens": p, "output_tokens": c, "total_tokens": p + c},
481
  }
 
482
  except Exception as e:
483
  print(f"[API] ERROR: {e}")
484
  return JSONResponse(status_code=500, content={"error": {"message": str(e)}})
 
490
  return JSONResponse(status_code=401, content={"error": {"message": "Invalid API Key"}})
491
  return {
492
  "object": "list",
493
+ "data": [{"id": m, "object": "model", "owned_by": "duck.ai"} for m in ALL_MODELS],
 
 
 
494
  }
495
 
496
 
497
  @app.get("/health")
498
  @app.get("/")
499
  async def health():
500
+ return {"status": "running", "message": "Duck.ai API Server is active!", "models": ALL_MODELS}
 
 
 
 
501
 
502
 
503
  if __name__ == "__main__":