Spaces:
Running
Running
Update main.py
Browse files
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 |
-
#
|
| 82 |
setup_page = await self.persistent_context.new_page()
|
| 83 |
try:
|
| 84 |
-
print("[SERVER] Opening duck.ai for
|
| 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]
|
| 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]
|
| 102 |
-
|
| 103 |
except Exception as e:
|
| 104 |
-
print(f"[SERVER] Setup note
|
| 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.
|
| 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 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 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(
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 188 |
-
await
|
| 189 |
-
|
| 190 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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 |
-
|
| 209 |
'button[aria-label="Stop generating"]:not([disabled])'
|
| 210 |
).count()
|
| 211 |
-
if
|
| 212 |
-
print(f"[DUCK]
|
| 213 |
break
|
| 214 |
|
| 215 |
await asyncio.sleep(1.5)
|
| 216 |
|
| 217 |
-
# โโ 8. ุงุณุชุฎุฑุงุฌ ุงู
|
| 218 |
response_text = await page.evaluate("""
|
| 219 |
() => {
|
| 220 |
-
// ุทุฑููุฉ 1: article
|
| 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
|
| 233 |
el.innerText &&
|
| 234 |
el.innerText.trim().length > 20 &&
|
| 235 |
!el.querySelector('textarea')
|
| 236 |
);
|
| 237 |
-
if (
|
| 238 |
-
return
|
| 239 |
}
|
| 240 |
}
|
| 241 |
-
|
| 242 |
-
|
| 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 (
|
| 251 |
-
return
|
| 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 |
-
#
|
| 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
|
| 265 |
-
if (
|
| 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
|
| 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 |
-
|
| 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":
|
| 377 |
-
"
|
| 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":
|
| 387 |
-
"
|
| 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":
|
| 480 |
-
"
|
| 481 |
-
"
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 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":
|
| 497 |
-
"
|
| 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__":
|