Skip to content

Commit 9240110

Browse files
committed
fix: 优化图片上传逻辑(移除批量上传/虚拟粘贴改用纯JS/修复Future异常/静默SSL错误)
1 parent f4a75a1 commit 9240110

3 files changed

Lines changed: 121 additions & 87 deletions

File tree

src/browser/page_controller.py

Lines changed: 102 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -812,15 +812,25 @@ async def _upload_images_via_file_input(self, images: List[Dict[str, str]], chec
812812
return False
813813

814814
await self._check_disconnect(check_client_disconnected, '上传图片前')
815+
816+
'''
817+
# 旧批量上传逻辑(AI Studio新前端不支援multiple属性)
815818
menu_opened = await self._robust_click_insert_assets(check_client_disconnected)
816819
if not menu_opened:
817820
self.logger.warning(f"[{self.req_id}] 未能打开菜单,尝试直接查找 input...")
818821
819-
file_input, matched_input = await get_first_visible_locator(self.page, HIDDEN_FILE_INPUT_SELECTORS + ['input[type="file"]'], timeout=2000)
822+
file_input = None
823+
for selector in HIDDEN_FILE_INPUT_SELECTORS:
824+
loc = self.page.locator(selector)
825+
if await loc.count() > 0:
826+
file_input = loc.first
827+
break
820828
if not file_input:
821-
file_input = self.page.locator('input[type="file"]').first
829+
loc = self.page.locator('input[type="file"]')
830+
if await loc.count() > 0:
831+
file_input = loc.first
822832
823-
if await file_input.count() == 0:
833+
if not file_input:
824834
self.logger.warning(f"[{self.req_id}] 未找到文件输入框")
825835
asyncio.create_task(self._cleanup_temp_files(temp_files))
826836
return False
@@ -835,26 +845,36 @@ async def _upload_images_via_file_input(self, images: List[Dict[str, str]], chec
835845
return True
836846
except Exception as batch_err:
837847
self.logger.warning(f"[{self.req_id}] 批量上传失败: {batch_err},尝试逐个上传...")
848+
'''
838849

839850
uploaded_count = 0
840851
for idx, tf_path in enumerate(temp_files):
841852
await self._check_disconnect(check_client_disconnected, f'上传图片 {idx+1}/{len(temp_files)}')
842853

843-
if idx > 0:
844-
menu_opened = await self._robust_click_insert_assets(check_client_disconnected)
845-
if not menu_opened:
846-
continue
847-
file_input, _ = await get_first_visible_locator(self.page, HIDDEN_FILE_INPUT_SELECTORS + ['input[type="file"]'], timeout=2000)
848-
if not file_input:
849-
file_input = self.page.locator('input[type="file"]').first
854+
menu_opened = await self._robust_click_insert_assets(check_client_disconnected)
855+
if not menu_opened:
856+
continue
857+
858+
file_input = None
859+
for selector in HIDDEN_FILE_INPUT_SELECTORS:
860+
loc = self.page.locator(selector)
861+
if await loc.count() > 0:
862+
file_input = loc.first
863+
break
864+
if not file_input:
865+
loc = self.page.locator('input[type="file"]')
866+
if await loc.count() > 0:
867+
file_input = loc.first
868+
869+
if not file_input:
870+
self.logger.warning(f"[{self.req_id}] 第{idx+1}张图片:未找到文件输入框")
871+
continue
850872

851873
try:
852874
self.logger.info(f"[{self.req_id}] 上传图片 {idx+1}/{len(temp_files)}...")
853875
await file_input.set_input_files(tf_path)
854876
uploaded_count += 1
855-
await asyncio.sleep(DELAY_AFTER_FILL)
856-
await self.page.keyboard.press('Escape')
857-
await asyncio.sleep(SLEEP_TICK)
877+
await asyncio.sleep(SLEEP_IMAGE_UPLOAD)
858878
except Exception as single_err:
859879
self.logger.warning(f"[{self.req_id}] 单张上传失败 {idx+1}: {single_err}")
860880

@@ -879,78 +899,82 @@ async def _cleanup_temp_files(self, file_paths: List[str]):
879899
except Exception:
880900
pass
881901

882-
async def _paste_images_via_event(self, images: List[Dict[str, str]], target_locator: Locator):
902+
async def _paste_images_via_event(self, images: List[Dict[str, str]], target_locator: Locator, check_client_disconnected: Callable) -> bool:
883903
self.logger.info(f"[{self.req_id}] (备选) 正在通过虚拟粘贴事件上传 {len(images)} 张图片...")
904+
expected_count = len(images)
884905

885906
try:
886-
await target_locator.focus()
887-
await target_locator.click()
888-
except Exception:
889-
pass
907+
await self._check_disconnect(check_client_disconnected, '虚拟粘贴')
908+
909+
await self.page.evaluate('document.querySelector("ms-prompt-box textarea")?.focus()')
890910

891-
script = """
892-
async (images) => {
893-
try {
894-
const dataTransfer = new DataTransfer();
895-
896-
for (let i = 0; i < images.length; i++) {
897-
const img = images[i];
898-
try {
899-
const byteCharacters = atob(img.data);
900-
const byteNumbers = new Array(byteCharacters.length);
901-
for (let j = 0; j < byteCharacters.length; j++) {
902-
byteNumbers[j] = byteCharacters.charCodeAt(j);
911+
script = """
912+
async (images) => {
913+
try {
914+
const dataTransfer = new DataTransfer();
915+
for (let i = 0; i < images.length; i++) {
916+
const img = images[i];
917+
try {
918+
const byteCharacters = atob(img.data);
919+
const byteNumbers = new Array(byteCharacters.length);
920+
for (let j = 0; j < byteCharacters.length; j++) {
921+
byteNumbers[j] = byteCharacters.charCodeAt(j);
922+
}
923+
const byteArray = new Uint8Array(byteNumbers);
924+
const blob = new Blob([byteArray], { type: img.mime });
925+
const ext = img.mime.split('/')[1] || 'png';
926+
const filename = `pasted_image_${i + 1}.${ext}`;
927+
const file = new File([blob], filename, { type: img.mime, lastModified: Date.now() });
928+
dataTransfer.items.add(file);
929+
} catch (err) {
930+
console.error(`[Paste] 处理图片 ${i + 1} 失败:`, err);
903931
}
904-
const byteArray = new Uint8Array(byteNumbers);
905-
const blob = new Blob([byteArray], { type: img.mime });
906-
907-
const ext = img.mime.split('/')[1] || 'png';
908-
const filename = `pasted_image_${i + 1}.${ext}`;
909-
const file = new File([blob], filename, { type: img.mime, lastModified: Date.now() });
910-
911-
dataTransfer.items.add(file);
912-
} catch (err) {
913-
console.error(`[Paste] 处理图片 ${i + 1} 失败:`, err);
914932
}
933+
if (dataTransfer.files.length === 0) {
934+
return { success: false, error: "没有文件被添加到 DataTransfer" };
935+
}
936+
const pasteEvent = new ClipboardEvent('paste', { bubbles: true, cancelable: true });
937+
Object.defineProperty(pasteEvent, 'clipboardData', { value: dataTransfer, writable: false, configurable: true });
938+
const textarea = document.querySelector('ms-prompt-box textarea');
939+
if (textarea) { textarea.focus(); textarea.dispatchEvent(pasteEvent); }
940+
document.dispatchEvent(pasteEvent);
941+
return { success: true, count: dataTransfer.files.length };
942+
} catch (err) {
943+
return { success: false, error: err.message };
915944
}
916-
917-
if (dataTransfer.files.length === 0) {
918-
return { success: false, error: "没有文件被添加到 DataTransfer" };
919-
}
920-
921-
const pasteEvent = new ClipboardEvent('paste', {
922-
bubbles: true,
923-
cancelable: true,
924-
});
925-
926-
Object.defineProperty(pasteEvent, 'clipboardData', {
927-
value: dataTransfer,
928-
writable: false,
929-
configurable: true
930-
});
931-
932-
const textarea = document.querySelector('ms-prompt-box textarea');
933-
if (textarea) {
934-
textarea.focus();
935-
textarea.dispatchEvent(pasteEvent);
936-
}
937-
938-
document.dispatchEvent(pasteEvent);
939-
940-
console.log(`[Paste] 已触发粘贴事件,包含 ${dataTransfer.files.length} 个文件`);
941-
return { success: true, count: dataTransfer.files.length };
942-
} catch (err) {
943-
console.error('[Paste] 模拟粘贴失败:', err);
944-
return { success: false, error: err.message };
945945
}
946-
}
947-
"""
948-
result = await self.page.evaluate(script, images)
949-
if not result or not result.get('success'):
950-
error_message = result.get('error', '未知错误')
951-
self.logger.error(f"[{self.req_id}] 虚拟粘贴图片失败: {error_message}")
952-
else:
953-
self.logger.info(f"[{self.req_id}] ✅ 虚拟粘贴事件已触发 ({result.get('count', 0)} 个文件)")
946+
"""
947+
result = await self.page.evaluate(script, images)
948+
if not result or not result.get('success'):
949+
self.logger.error(f"[{self.req_id}] 虚拟粘贴触发失败: {result.get('error', '未知')}")
950+
return False
951+
952+
self.logger.info(f"[{self.req_id}] 虚拟粘贴事件已触发")
953+
await asyncio.sleep(SLEEP_IMAGE_UPLOAD)
954+
955+
uploaded_images = 0
956+
for selector in ['ms-prompt-box img', '.prompt-input img', 'img[src*="blob:"]']:
957+
try:
958+
locator = self.page.locator(selector)
959+
count = await locator.count()
960+
if count >= expected_count:
961+
uploaded_images = count
962+
break
963+
except Exception:
964+
pass
965+
966+
if uploaded_images >= expected_count:
967+
self.logger.info(f"[{self.req_id}] ✅ 虚拟粘贴成功,检测到 {uploaded_images} 张图片")
968+
return True
969+
else:
970+
self.logger.warning(f"[{self.req_id}] 虚拟粘贴验证失败: 检测到{uploaded_images}/{expected_count}张")
971+
return False
972+
973+
except Exception as e:
974+
if isinstance(e, ClientDisconnectedError):
975+
raise
976+
self.logger.error(f"[{self.req_id}] ❌ 虚拟粘贴异常: {e}")
977+
return False
954978

955979

956980
async def submit_prompt(self, prompt: str, image_list: List, check_client_disconnected: Callable):
@@ -996,7 +1020,7 @@ async def submit_prompt(self, prompt: str, image_list: List, check_client_discon
9961020

9971021
if not upload_success:
9981022
self.logger.info(f"[{self.req_id}] 回退到虚拟粘贴模式...")
999-
await self._paste_images_via_event(processed_images, prompt_textarea_locator)
1023+
await self._paste_images_via_event(processed_images, prompt_textarea_locator, check_client_disconnected)
10001024

10011025
await asyncio.sleep(SLEEP_LONG)
10021026

src/browser/selector_utils.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ async def check_one(selector: str) -> Tuple[bool, str]:
1919

2020
tasks = [asyncio.create_task(check_one(sel)) for sel in selectors]
2121

22+
async def cancel_tasks(pending_tasks):
23+
for p in pending_tasks:
24+
p.cancel()
25+
await asyncio.gather(*pending_tasks, return_exceptions=True)
26+
2227
try:
2328
done, pending = await asyncio.wait(
2429
tasks,
@@ -29,21 +34,18 @@ async def check_one(selector: str) -> Tuple[bool, str]:
2934
for task in done:
3035
success, selector = task.result()
3136
if success:
32-
for p in pending:
33-
p.cancel()
37+
await cancel_tasks(pending)
3438
return (page.locator(selector), selector)
3539

36-
for p in pending:
37-
p.cancel()
40+
await cancel_tasks(pending)
3841
return (None, None)
3942

4043
except asyncio.TimeoutError:
41-
for t in tasks:
42-
if not t.done():
43-
t.cancel()
44+
await cancel_tasks(tasks)
4445
return (None, None)
4546

4647

48+
4749
async def get_first_visible_locator(
4850
page: AsyncPage,
4951
selectors: List[str],

src/proxy/server.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,11 @@ async def process_upstream():
257257
await server_writer.drain()
258258
client_buf.clear()
259259
except Exception as e:
260-
self.log.error(f'Upstream error: {e}')
260+
err_str = str(e)
261+
if 'APPLICATION_DATA_AFTER_CLOSE_NOTIFY' in err_str or 'Connection reset' in err_str:
262+
pass
263+
else:
264+
self.log.error(f'Upstream error: {e}')
261265
finally:
262266
server_writer.close()
263267

@@ -304,7 +308,11 @@ async def process_downstream():
304308
if b'0\r\n\r\n' in server_buf:
305309
server_buf.clear()
306310
except Exception as e:
307-
self.log.error(f'Downstream error: {e}')
311+
err_str = str(e)
312+
if 'APPLICATION_DATA_AFTER_CLOSE_NOTIFY' in err_str or 'Connection reset' in err_str:
313+
pass
314+
else:
315+
self.log.error(f'Downstream error: {e}')
308316
finally:
309317
client_writer.close()
310318

0 commit comments

Comments
 (0)