Skip to content

Commit 47d9131

Browse files
committed
fix: 重大修复,现在网页只支持上传单个文件
1 parent 0675824 commit 47d9131

File tree

7 files changed

+87
-154
lines changed

7 files changed

+87
-154
lines changed

src/browser/operations.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,36 @@ async def click_element(page: AsyncPage, locator: Locator, element_name: str, re
455455
logger.error(f"[{req_id}] ❌ 所有点击 '{element_name}' 的尝试都失败了。")
456456
raise ElementClickError(f"All click attempts for '{element_name}' failed.") from last_error
457457

458+
async def safe_click(locator: Locator, element_name: str, req_id: str, timeout: int = 2000) -> bool:
459+
try:
460+
await locator.wait_for(state='visible', timeout=timeout)
461+
except Exception as e:
462+
logger.warning(f"[{req_id}] '{element_name}' 元素不可见: {e}")
463+
return False
464+
try:
465+
await locator.click(timeout=500)
466+
logger.info(f"[{req_id}] ✅ '{element_name}' 点击成功")
467+
return True
468+
except Exception:
469+
pass
470+
await asyncio.sleep(0.1)
471+
try:
472+
logger.info(f"[{req_id}] 🖱️ 尝试强制点击 '{element_name}'")
473+
await locator.click(timeout=500, force=True)
474+
logger.info(f"[{req_id}] ✅ '{element_name}' 强制点击成功")
475+
return True
476+
except Exception:
477+
pass
478+
await asyncio.sleep(0.1)
479+
try:
480+
logger.info(f"[{req_id}] 🖱️ 尝试JS点击 '{element_name}'")
481+
await locator.evaluate('element => element.click()')
482+
logger.info(f"[{req_id}] ✅ '{element_name}' JS点击成功")
483+
return True
484+
except Exception as e:
485+
logger.error(f"[{req_id}] ❌ 所有点击 '{element_name}' 的尝试都失败了: {e}")
486+
return False
487+
458488
async def get_response_via_edit_button(page: AsyncPage, req_id: str, check_client_disconnected: Callable) -> Optional[str]:
459489
logger.info(f'[{req_id}] (Helper) 尝试通过编辑按钮获取响应...')
460490
last_message_container = page.locator('ms-chat-turn').last

src/browser/page_controller.py

Lines changed: 32 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -755,11 +755,14 @@ async def _robust_click_insert_assets(self, check_client_disconnected: Callable)
755755
return False
756756

757757
async def _upload_images_via_file_input(self, images: List[Dict[str, str]], check_client_disconnected: Callable) -> bool:
758-
self.logger.info(f"[{self.req_id}] 尝试通过文件选择器批量上传 {len(images)} 张图片...")
758+
self.logger.info(f"[{self.req_id}] 尝试逐个上传 {len(images)} 张图片...")
759759
temp_files = []
760-
file_paths = []
760+
uploaded_count = 0
761+
761762
try:
762763
for idx, img in enumerate(images):
764+
await self._check_disconnect(check_client_disconnected, f'上传图片 {idx+1}/{len(images)}')
765+
763766
mime = img['mime']
764767
try:
765768
data = base64.b64decode(img['data'])
@@ -774,34 +777,37 @@ async def _upload_images_via_file_input(self, images: List[Dict[str, str]], chec
774777
tf.write(data)
775778
tf.close()
776779
temp_files.append(tf.name)
777-
file_paths.append(tf.name)
778-
779-
if not file_paths:
780-
self.logger.warning(f"[{self.req_id}] 没有有效的图片文件可上传。")
781-
return False
782-
783-
menu_opened = await self._robust_click_insert_assets(check_client_disconnected)
784-
785-
if not menu_opened:
786-
self.logger.warning(f"[{self.req_id}] 未能打开菜单,将尝试直接查找 input (可能已存在)。")
787-
788-
file_input = self.page.locator('input[type="file"]').first
789-
790-
if await file_input.count() == 0:
791-
self.logger.warning(f"[{self.req_id}] 未找到文件输入框 (input[type='file'])。回退到粘贴模式。")
792-
asyncio.create_task(self._cleanup_temp_files(temp_files))
793-
return False
794-
795-
await self._check_disconnect(check_client_disconnected, '文件上传 - set_input_files前')
796-
self.logger.info(f"[{self.req_id}] 找到 file input,正在设置 {len(file_paths)} 个文件...")
797-
await file_input.set_input_files(file_paths)
798-
self.logger.info(f"[{self.req_id}] set_input_files 完成。")
780+
781+
menu_opened = await self._robust_click_insert_assets(check_client_disconnected)
782+
if not menu_opened:
783+
self.logger.warning(f"[{self.req_id}] 未能打开菜单,尝试直接查找 input...")
784+
785+
from config import HIDDEN_FILE_INPUT_SELECTOR
786+
file_input = self.page.locator(HIDDEN_FILE_INPUT_SELECTOR)
787+
if await file_input.count() == 0:
788+
file_input = self.page.locator('input[type="file"]').first
789+
790+
if await file_input.count() == 0:
791+
self.logger.warning(f"[{self.req_id}] 未找到文件输入框,跳过图片 {idx+1}")
792+
continue
793+
794+
self.logger.info(f"[{self.req_id}] 上传图片 {idx+1}/{len(images)}...")
795+
await file_input.set_input_files(tf.name)
796+
uploaded_count += 1
797+
798+
await asyncio.sleep(0.8)
799+
await self.page.keyboard.press('Escape')
800+
await asyncio.sleep(0.3)
799801

800802
asyncio.create_task(self._cleanup_temp_files(temp_files))
801-
return True
803+
804+
if uploaded_count > 0:
805+
self.logger.info(f"[{self.req_id}] ✅ 成功上传 {uploaded_count}/{len(images)} 张图片")
806+
return True
807+
return False
802808

803809
except Exception as e:
804-
self.logger.error(f"[{self.req_id}] 文件选择器上传失败: {e}")
810+
self.logger.error(f"[{self.req_id}] 文件上传失败: {e}")
805811
asyncio.create_task(self._cleanup_temp_files(temp_files))
806812
return False
807813

src/config/selectors.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
INPUT_SELECTOR2 = PROMPT_TEXTAREA_SELECTOR
44
SUBMIT_BUTTON_SELECTOR = 'ms-prompt-box ms-run-button button'
55
INSERT_BUTTON_SELECTOR = 'button[data-test-id="add-media-button"]'
6-
UPLOAD_BUTTON_SELECTOR = 'button[role="menuitem"]:has-text("Upload file")'
7-
HIDDEN_FILE_INPUT_SELECTOR = 'input.file-input[type="file"]'
6+
UPLOAD_BUTTON_SELECTOR = 'button[role="menuitem"]:has-text("Upload a file")'
7+
HIDDEN_FILE_INPUT_SELECTOR = 'input[type="file"][data-test-upload-file-input]'
8+
UPLOADED_MEDIA_ITEM_SELECTOR = 'ms-prompt-box .multi-media-row ms-media-chip'
89
SKIP_PREFERENCE_VOTE_BUTTON_SELECTOR = 'button[data-test-id="skip-button"][aria-label="Skip preference vote"]'
910
RESPONSE_CONTAINER_SELECTOR = 'ms-chat-turn .chat-turn-container.model'
1011
RESPONSE_TEXT_SELECTOR = 'ms-cmark-node.cmark-node'

src/media/imagen_controller.py

Lines changed: 4 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
IMAGEN_SETTINGS_NUM_RESULTS_INPUT_SELECTOR, IMAGEN_SETTINGS_ASPECT_RATIO_BUTTON_SELECTOR,
1111
IMAGEN_SETTINGS_NEGATIVE_PROMPT_SELECTOR
1212
)
13+
from browser.operations import safe_click
1314
from .models import ImageGenerationConfig, GeneratedImage
1415
from models import ClientDisconnectedError
1516

@@ -25,31 +26,6 @@ async def _check_disconnect(self, check_client_disconnected: Callable, stage: st
2526
if check_client_disconnected(stage):
2627
raise ClientDisconnectedError(f'[{self.req_id}] Client disconnected at stage: {stage}')
2728

28-
async def _safe_click(self, locator: Locator, element_name: str, timeout: int = 2000) -> bool:
29-
try:
30-
await locator.wait_for(state='visible', timeout=timeout)
31-
except Exception as e:
32-
self.logger.warning(f"[{self.req_id}] '{element_name}' 元素不可见: {e}")
33-
return False
34-
try:
35-
await locator.click(timeout=500)
36-
return True
37-
except Exception:
38-
pass
39-
await asyncio.sleep(0.1)
40-
try:
41-
await locator.click(timeout=500, force=True)
42-
return True
43-
except Exception:
44-
pass
45-
await asyncio.sleep(0.1)
46-
try:
47-
await locator.evaluate('element => element.click()')
48-
return True
49-
except Exception as e:
50-
self.logger.error(f"[{self.req_id}] ❌ 所有点击 '{element_name}' 的尝试都失败了: {e}")
51-
return False
52-
5329

5430
async def navigate_to_imagen_page(self, model: str, check_client_disconnected: Callable):
5531
if model not in IMAGEN_SUPPORTED_MODELS:
@@ -100,7 +76,7 @@ async def set_aspect_ratio(self, aspect_ratio: str, check_client_disconnected: C
10076
try:
10177
btn = self.page.locator(f'{IMAGEN_SETTINGS_ASPECT_RATIO_BUTTON_SELECTOR}:has-text("{aspect_ratio}")')
10278
if await btn.count() > 0:
103-
if await self._safe_click(btn.first, f'宽高比按钮 {aspect_ratio}'):
79+
if await safe_click(btn.first, f'宽高比按钮 {aspect_ratio}', self.req_id):
10480
self.logger.info(f'[{self.req_id}] ✅ 宽高比已设置: {aspect_ratio}')
10581
return
10682
else:
@@ -143,7 +119,7 @@ async def fill_prompt(self, prompt: str, check_client_disconnected: Callable):
143119
await self.page.keyboard.press('Escape')
144120
await asyncio.sleep(0.3)
145121
text_input = self.page.locator(IMAGEN_PROMPT_INPUT_SELECTOR)
146-
await self._safe_click(text_input, '输入框')
122+
await safe_click(text_input, '输入框', self.req_id)
147123
await text_input.fill(prompt)
148124
await asyncio.sleep(0.2)
149125
self.logger.info(f'[{self.req_id}] ✅ 提示词已填充')
@@ -166,7 +142,7 @@ async def run_generation(self, check_client_disconnected: Callable):
166142
run_btn = self.page.locator(IMAGEN_RUN_BUTTON_SELECTOR)
167143
await expect_async(run_btn).to_be_visible(timeout=5000)
168144
await expect_async(run_btn).to_be_enabled(timeout=5000)
169-
if not await self._safe_click(run_btn, 'Run 按钮'):
145+
if not await safe_click(run_btn, 'Run 按钮', self.req_id):
170146
if attempt < max_retries:
171147
continue
172148
raise Exception('Run 按钮点击失败')

src/media/nano_controller.py

Lines changed: 6 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
PROMPT_TEXTAREA_SELECTOR, SUBMIT_BUTTON_SELECTOR,
1212
INSERT_BUTTON_SELECTOR, UPLOAD_BUTTON_SELECTOR
1313
)
14+
from browser.operations import safe_click
1415
from .models import NanoBananaConfig, GeneratedImage, GeneratedContent
1516
from models import ClientDisconnectedError
1617

@@ -26,39 +27,6 @@ async def _check_disconnect(self, check_client_disconnected: Callable, stage: st
2627
if check_client_disconnected(stage):
2728
raise ClientDisconnectedError(f'[{self.req_id}] Client disconnected at stage: {stage}')
2829

29-
async def _safe_click(self, locator: Locator, element_name: str, timeout: int = 2000) -> bool:
30-
try:
31-
await locator.wait_for(state='visible', timeout=timeout)
32-
except Exception as e:
33-
self.logger.warning(f"[{self.req_id}] '{element_name}' 元素不可见: {e}")
34-
return False
35-
36-
try:
37-
await locator.click(timeout=500)
38-
self.logger.info(f"[{self.req_id}] ✅ '{element_name}' 点击成功")
39-
return True
40-
except Exception:
41-
pass
42-
43-
await asyncio.sleep(0.1)
44-
try:
45-
self.logger.info(f"[{self.req_id}] 🖱️ 尝试强制点击 '{element_name}'")
46-
await locator.click(timeout=500, force=True)
47-
self.logger.info(f"[{self.req_id}] ✅ '{element_name}' 强制点击成功")
48-
return True
49-
except Exception:
50-
pass
51-
52-
await asyncio.sleep(0.1)
53-
try:
54-
self.logger.info(f"[{self.req_id}] 🖱️ 尝试JS点击 '{element_name}'")
55-
await locator.evaluate('element => element.click()')
56-
self.logger.info(f"[{self.req_id}] ✅ '{element_name}' JS点击成功")
57-
return True
58-
except Exception as e:
59-
self.logger.error(f"[{self.req_id}] ❌ 所有点击 '{element_name}' 的尝试都失败了: {e}")
60-
return False
61-
6230

6331
async def navigate_to_nano_page(self, model: str, check_client_disconnected: Callable):
6432
if model not in NANO_SUPPORTED_MODELS:
@@ -91,12 +59,12 @@ async def set_aspect_ratio(self, aspect_ratio: str, check_client_disconnected: C
9159
if await dropdown.count() == 0:
9260
self.logger.warning(f'[{self.req_id}] 未找到宽高比下拉框')
9361
return
94-
if not await self._safe_click(dropdown, '宽高比下拉框'):
62+
if not await safe_click(dropdown, '宽高比下拉框', self.req_id):
9563
continue
9664
await asyncio.sleep(0.3)
9765
option = self.page.locator(f'mat-option:has-text("{aspect_ratio}")')
9866
if await option.count() > 0:
99-
if await self._safe_click(option.first, f'宽高比选项 {aspect_ratio}'):
67+
if await safe_click(option.first, f'宽高比选项 {aspect_ratio}', self.req_id):
10068
self.logger.info(f'[{self.req_id}] ✅ 宽高比已设置: {aspect_ratio}')
10169
return
10270
else:
@@ -120,7 +88,7 @@ async def upload_image(self, image_bytes: bytes, mime_type: str, check_client_di
12088
if await insert_btn.count() == 0:
12189
self.logger.warning(f'[{self.req_id}] 未找到插入按钮')
12290
return
123-
if not await self._safe_click(insert_btn, '插入按钮'):
91+
if not await safe_click(insert_btn, '插入按钮', self.req_id):
12492
if attempt < max_retries:
12593
continue
12694
return
@@ -154,7 +122,7 @@ async def fill_prompt(self, prompt: str, check_client_disconnected: Callable):
154122
await self.page.keyboard.press('Escape')
155123
await asyncio.sleep(0.3)
156124
text_input = self.page.locator(PROMPT_TEXTAREA_SELECTOR)
157-
await self._safe_click(text_input, '输入框')
125+
await safe_click(text_input, '输入框', self.req_id)
158126
await text_input.fill(prompt)
159127
await asyncio.sleep(0.2)
160128
actual = await text_input.input_value()
@@ -180,7 +148,7 @@ async def run_generation(self, check_client_disconnected: Callable):
180148
run_btn = self.page.locator(SUBMIT_BUTTON_SELECTOR)
181149
await expect_async(run_btn).to_be_visible(timeout=5000)
182150
await expect_async(run_btn).to_be_enabled(timeout=5000)
183-
if not await self._safe_click(run_btn, 'Run 按钮'):
151+
if not await safe_click(run_btn, 'Run 按钮', self.req_id):
184152
if attempt < max_retries:
185153
continue
186154
raise Exception('Run 按钮点击失败')

src/media/veo_controller.py

Lines changed: 7 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
VEO_SETTINGS_NUM_RESULTS_INPUT_SELECTOR, VEO_SETTINGS_ASPECT_RATIO_BUTTON_SELECTOR,
1313
VEO_SETTINGS_DURATION_DROPDOWN_SELECTOR, VEO_SETTINGS_NEGATIVE_PROMPT_SELECTOR
1414
)
15+
from browser.operations import safe_click
1516
from .models import VideoGenerationConfig, GeneratedVideo
1617
from models import ClientDisconnectedError
1718

@@ -27,31 +28,6 @@ async def _check_disconnect(self, check_client_disconnected: Callable, stage: st
2728
if check_client_disconnected(stage):
2829
raise ClientDisconnectedError(f'[{self.req_id}] Client disconnected at stage: {stage}')
2930

30-
async def _safe_click(self, locator: Locator, element_name: str, timeout: int = 2000) -> bool:
31-
try:
32-
await locator.wait_for(state='visible', timeout=timeout)
33-
except Exception as e:
34-
self.logger.warning(f"[{self.req_id}] '{element_name}' 元素不可见: {e}")
35-
return False
36-
try:
37-
await locator.click(timeout=500)
38-
return True
39-
except Exception:
40-
pass
41-
await asyncio.sleep(0.1)
42-
try:
43-
await locator.click(timeout=500, force=True)
44-
return True
45-
except Exception:
46-
pass
47-
await asyncio.sleep(0.1)
48-
try:
49-
await locator.evaluate('element => element.click()')
50-
return True
51-
except Exception as e:
52-
self.logger.error(f"[{self.req_id}] ❌ 所有点击 '{element_name}' 的尝试都失败了: {e}")
53-
return False
54-
5531

5632
async def navigate_to_veo_page(self, model: str, check_client_disconnected: Callable):
5733
if model not in VEO_SUPPORTED_MODELS:
@@ -102,7 +78,7 @@ async def set_aspect_ratio(self, aspect_ratio: str, check_client_disconnected: C
10278
try:
10379
btn = self.page.locator(f'{VEO_SETTINGS_ASPECT_RATIO_BUTTON_SELECTOR}:has-text("{aspect_ratio}")')
10480
if await btn.count() > 0:
105-
if await self._safe_click(btn.first, f'宽高比按钮 {aspect_ratio}'):
81+
if await safe_click(btn.first, f'宽高比按钮 {aspect_ratio}', self.req_id):
10682
self.logger.info(f'[{self.req_id}] ✅ 宽高比已设置: {aspect_ratio}')
10783
return
10884
else:
@@ -124,12 +100,12 @@ async def set_duration(self, duration_seconds: int, check_client_disconnected: C
124100
if await dropdown.count() == 0:
125101
self.logger.warning(f'[{self.req_id}] 未找到时长下拉框')
126102
return
127-
if not await self._safe_click(dropdown, '时长下拉框'):
103+
if not await safe_click(dropdown, '时长下拉框', self.req_id):
128104
continue
129105
await asyncio.sleep(0.3)
130106
option = self.page.locator(f'mat-option:has-text("{duration_seconds}")')
131107
if await option.count() > 0:
132-
if await self._safe_click(option.first, f'时长选项 {duration_seconds}s'):
108+
if await safe_click(option.first, f'时长选项 {duration_seconds}s', self.req_id):
133109
self.logger.info(f'[{self.req_id}] ✅ 视频时长已设置: {duration_seconds}s')
134110
return
135111
else:
@@ -174,7 +150,7 @@ async def upload_image(self, image_bytes: bytes, mime_type: str, check_client_di
174150
if await add_media_btn.count() == 0:
175151
self.logger.warning(f'[{self.req_id}] 未找到添加媒体按钮')
176152
return
177-
if not await self._safe_click(add_media_btn, '添加媒体按钮'):
153+
if not await safe_click(add_media_btn, '添加媒体按钮', self.req_id):
178154
if attempt < max_retries:
179155
continue
180156
return
@@ -209,7 +185,7 @@ async def fill_prompt(self, prompt: str, check_client_disconnected: Callable):
209185
await self.page.keyboard.press('Escape')
210186
await asyncio.sleep(0.3)
211187
text_input = self.page.locator(VEO_PROMPT_INPUT_SELECTOR)
212-
await self._safe_click(text_input, '输入框')
188+
await safe_click(text_input, '输入框', self.req_id)
213189
await text_input.fill(prompt)
214190
await asyncio.sleep(0.2)
215191
self.logger.info(f'[{self.req_id}] ✅ 提示词已填充')
@@ -232,7 +208,7 @@ async def run_generation(self, check_client_disconnected: Callable):
232208
run_btn = self.page.locator(VEO_RUN_BUTTON_SELECTOR)
233209
await expect_async(run_btn).to_be_visible(timeout=5000)
234210
await expect_async(run_btn).to_be_enabled(timeout=5000)
235-
if not await self._safe_click(run_btn, 'Run 按钮'):
211+
if not await safe_click(run_btn, 'Run 按钮', self.req_id):
236212
if attempt < max_retries:
237213
continue
238214
raise Exception('Run 按钮点击失败')

0 commit comments

Comments
 (0)