|
1 | 1 | import asyncio |
2 | | -import base64 |
3 | | -import mimetypes |
4 | 2 | from typing import Callable, List |
5 | 3 |
|
6 | 4 | from playwright.async_api import TimeoutError |
|
16 | 14 | ) |
17 | 15 | from config.selector_utils import ( |
18 | 16 | AUTOSIZE_WRAPPER_SELECTORS, |
19 | | - DRAG_DROP_TARGET_SELECTORS, |
20 | 17 | build_combined_selector, |
21 | 18 | ) |
22 | 19 | from logging_utils import set_request_id |
@@ -328,152 +325,6 @@ async def _handle_post_upload_dialog(self): |
328 | 325 | except Exception: |
329 | 326 | pass |
330 | 327 |
|
331 | | - async def _ensure_files_attached( |
332 | | - self, wrapper_locator, expected_min: int = 1, timeout_ms: int = 5000 |
333 | | - ) -> bool: |
334 | | - """轮询检查输入区域内 file input 的 files 是否 >= 期望数量。""" |
335 | | - end = asyncio.get_event_loop().time() + (timeout_ms / 1000) |
336 | | - while asyncio.get_event_loop().time() < end: |
337 | | - try: |
338 | | - # NOTE: normalize JS eval string to avoid parser confusion |
339 | | - counts = await wrapper_locator.evaluate( |
340 | | - """ |
341 | | - (el) => { |
342 | | - const result = {inputs:0, chips:0, blobs:0}; |
343 | | - try { el.querySelectorAll('input[type="file"]').forEach(i => { result.inputs += (i.files ? i.files.length : 0); }); } catch(e){} |
344 | | - try { result.chips = el.querySelectorAll('button[aria-label*="Remove" i], button[aria-label*="asset" i]').length; } catch(e){} |
345 | | - try { result.blobs = el.querySelectorAll('img[src^="blob:"], video[src^="blob:"]').length; } catch(e){} |
346 | | - return result; |
347 | | - } |
348 | | - """ |
349 | | - ) |
350 | | - |
351 | | - total = 0 |
352 | | - if isinstance(counts, dict): |
353 | | - total = max( |
354 | | - int(counts.get("inputs") or 0), |
355 | | - int(counts.get("chips") or 0), |
356 | | - int(counts.get("blobs") or 0), |
357 | | - ) |
358 | | - if total >= expected_min: |
359 | | - self.logger.info( |
360 | | - f" 已检测到已附加文件: inputs={counts.get('inputs')}, chips={counts.get('chips')}, blobs={counts.get('blobs')} (>= {expected_min})" |
361 | | - ) |
362 | | - return True |
363 | | - except asyncio.CancelledError: |
364 | | - raise |
365 | | - except Exception: |
366 | | - pass |
367 | | - await asyncio.sleep(0.2) |
368 | | - self.logger.warning(f" 未能在超时内检测到已附加文件 (期望 >= {expected_min})") |
369 | | - return False |
370 | | - |
371 | | - async def _simulate_drag_drop_files( |
372 | | - self, target_locator, files_list: List[str] |
373 | | - ) -> None: |
374 | | - """将本地文件以拖放事件的方式注入到目标元素。 |
375 | | - 仅负责触发 dragenter/dragover/drop,不在此处做附加验证以节省时间。 |
376 | | - """ |
377 | | - payloads = [] |
378 | | - for path in files_list: |
379 | | - try: |
380 | | - with open(path, "rb") as f: |
381 | | - raw = f.read() |
382 | | - b64 = base64.b64encode(raw).decode("ascii") |
383 | | - mime, _ = mimetypes.guess_type(path) |
384 | | - payloads.append( |
385 | | - { |
386 | | - "name": path.split("/")[-1], |
387 | | - "mime": mime or "application/octet-stream", |
388 | | - "b64": b64, |
389 | | - } |
390 | | - ) |
391 | | - except Exception as e: |
392 | | - self.logger.warning(f" 读取文件失败,跳过拖放: {path} - {e}") |
393 | | - |
394 | | - if not payloads: |
395 | | - raise Exception("无可用文件用于拖放") |
396 | | - |
397 | | - # 使用集中管理的选择器列表作为拖放候选目标 |
398 | | - candidates = [target_locator, self.page.locator(PROMPT_TEXTAREA_SELECTOR)] |
399 | | - candidates.extend( |
400 | | - [self.page.locator(sel) for sel in DRAG_DROP_TARGET_SELECTORS] |
401 | | - ) |
402 | | - |
403 | | - last_err = None |
404 | | - for idx, cand in enumerate(candidates): |
405 | | - try: |
406 | | - await expect_async(cand).to_be_visible(timeout=3000) |
407 | | - await cand.evaluate( |
408 | | - """ |
409 | | - (el, files) => { |
410 | | - const dt = new DataTransfer(); |
411 | | - for (const p of files) { |
412 | | - const bstr = atob(p.b64); |
413 | | - const len = bstr.length; |
414 | | - const u8 = new Uint8Array(len); |
415 | | - for (let i = 0; i < len; i++) u8[i] = bstr.charCodeAt(i); |
416 | | - const blob = new Blob([u8], { type: p.mime || 'application/octet-stream' }); |
417 | | - const file = new File([blob], p.name, { type: p.mime || 'application/octet-stream' }); |
418 | | - dt.items.add(file); |
419 | | - } |
420 | | - const evEnter = new DragEvent('dragenter', { bubbles: true, cancelable: true, dataTransfer: dt }); |
421 | | - el.dispatchEvent(evEnter); |
422 | | - const evOver = new DragEvent('dragover', { bubbles: true, cancelable: true, dataTransfer: dt }); |
423 | | - el.dispatchEvent(evOver); |
424 | | - const evDrop = new DragEvent('drop', { bubbles: true, cancelable: true, dataTransfer: dt }); |
425 | | - el.dispatchEvent(evDrop); |
426 | | - } |
427 | | - """, |
428 | | - payloads, |
429 | | - ) |
430 | | - await asyncio.sleep(0.5) |
431 | | - self.logger.info( |
432 | | - f" 拖放事件已在候选目标 {idx + 1}/{len(candidates)} 上触发。" |
433 | | - ) |
434 | | - return |
435 | | - except asyncio.CancelledError: |
436 | | - raise |
437 | | - except Exception as e_try: |
438 | | - last_err = e_try |
439 | | - continue |
440 | | - |
441 | | - # 兜底:在 document.body 上尝试一次 |
442 | | - try: |
443 | | - await self.page.evaluate( |
444 | | - """ |
445 | | - (files) => { |
446 | | - const dt = new DataTransfer(); |
447 | | - for (const p of files) { |
448 | | - const bstr = atob(p.b64); |
449 | | - const len = bstr.length; |
450 | | - const u8 = new Uint8Array(len); |
451 | | - for (let i = 0; i < len; i++) u8[i] = bstr.charCodeAt(i); |
452 | | - const blob = new Blob([u8], { type: p.mime || 'application/octet-stream' }); |
453 | | - const file = new File([blob], p.name, { type: p.mime || 'application/octet-stream' }); |
454 | | - dt.items.add(file); |
455 | | - } |
456 | | - const el = document.body; |
457 | | - const evEnter = new DragEvent('dragenter', { bubbles: true, cancelable: true, dataTransfer: dt }); |
458 | | - el.dispatchEvent(evEnter); |
459 | | - const evOver = new DragEvent('dragover', { bubbles: true, cancelable: true, dataTransfer: dt }); |
460 | | - el.dispatchEvent(evOver); |
461 | | - const evDrop = new DragEvent('drop', { bubbles: true, cancelable: true, dataTransfer: dt }); |
462 | | - el.dispatchEvent(evDrop); |
463 | | - } |
464 | | - """, |
465 | | - payloads, |
466 | | - ) |
467 | | - await asyncio.sleep(0.5) |
468 | | - self.logger.info(" 拖放事件已在 document.body 上触发(兜底)。") |
469 | | - return |
470 | | - except asyncio.CancelledError: |
471 | | - raise |
472 | | - except Exception: |
473 | | - pass |
474 | | - |
475 | | - raise last_err or Exception("拖放未能在任何候选目标上触发") |
476 | | - |
477 | 328 | async def _try_enter_submit( |
478 | 329 | self, prompt_textarea_locator, check_client_disconnected: Callable |
479 | 330 | ) -> bool: |
|
0 commit comments