Skip to content

Commit f881fbb

Browse files
committed
Merge branch 'main' into dev
2 parents e3ff488 + 0d2fac4 commit f881fbb

9 files changed

Lines changed: 107 additions & 77 deletions

File tree

api_utils/app.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@
3030
_initialize_page_logic,
3131
_close_page_logic,
3232
load_excluded_models,
33-
_handle_initial_model_state_and_storage
33+
_handle_initial_model_state_and_storage,
34+
enable_temporary_chat_mode
3435
)
3536

3637
import stream
@@ -141,6 +142,7 @@ async def _initialize_browser_and_page():
141142
server.page_instance, server.is_page_ready = await _initialize_page_logic(server.browser_instance)
142143
if server.is_page_ready:
143144
await _handle_initial_model_state_and_storage(server.page_instance)
145+
await enable_temporary_chat_mode(server.page_instance)
144146
server.logger.info("Page initialized successfully.")
145147
else:
146148
server.logger.error("Page initialization failed.")

api_utils/auth_utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from typing import Set
33

44
API_KEYS: Set[str] = set()
5-
KEY_FILE_PATH = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "key.txt")
5+
KEY_FILE_PATH = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "auth_profiles", "key.txt")
66

77
def load_api_keys():
88
"""Loads API keys from the key file into the API_KEYS set."""

api_utils/routes.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,9 @@ async def add_api_key(request: ApiKeyRequest, logger: logging.Logger = Depends(g
327327
raise HTTPException(status_code=400, detail="该API密钥已存在。")
328328

329329
try:
330-
key_file_path = os.path.join(os.path.dirname(__file__), "..", "key.txt")
330+
# --- MODIFIED LINE ---
331+
# Use the centralized path from auth_utils
332+
key_file_path = auth_utils.KEY_FILE_PATH
331333
with open(key_file_path, 'a+', encoding='utf-8') as f:
332334
f.seek(0)
333335
if f.read(): f.write("\n")
@@ -366,7 +368,9 @@ async def delete_api_key(request: ApiKeyRequest, logger: logging.Logger = Depend
366368
raise HTTPException(status_code=404, detail="API密钥不存在。")
367369

368370
try:
369-
key_file_path = os.path.join(os.path.dirname(__file__), "..", "key.txt")
371+
# --- MODIFIED LINE ---
372+
# Use the centralized path from auth_utils
373+
key_file_path = auth_utils.KEY_FILE_PATH
370374
with open(key_file_path, 'r', encoding='utf-8') as f:
371375
lines = f.readlines()
372376

browser_utils/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# --- browser_utils/__init__.py ---
22
# 浏览器操作工具模块
3-
from .initialization import _initialize_page_logic, _close_page_logic, signal_camoufox_shutdown
3+
from .initialization import _initialize_page_logic, _close_page_logic, signal_camoufox_shutdown, enable_temporary_chat_mode
44
from .operations import (
55
_handle_model_list_response,
66
detect_and_extract_page_error,
@@ -28,6 +28,7 @@
2828
'_initialize_page_logic',
2929
'_close_page_logic',
3030
'signal_camoufox_shutdown',
31+
'enable_temporary_chat_mode',
3132

3233
# 页面操作相关
3334
'_handle_model_list_response',

browser_utils/initialization.py

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -422,8 +422,8 @@ async def _initialize_page_logic(browser: AsyncBrowser):
422422
await expect_async(input_wrapper_locator).to_be_visible(timeout=35000)
423423
await expect_async(found_page.locator(INPUT_SELECTOR)).to_be_visible(timeout=10000)
424424
logger.info("-> ✅ 核心输入区域可见。")
425-
426-
model_name_locator = found_page.locator('mat-select[data-test-ms-model-selector] div.model-option-content span.gmat-body-medium')
425+
426+
model_name_locator = found_page.locator('[data-test-id="model-name"]')
427427
try:
428428
model_name_on_page = await model_name_locator.first.inner_text(timeout=5000)
429429
logger.info(f"-> 🤖 页面检测到的当前模型: {model_name_on_page}")
@@ -635,5 +635,35 @@ async def _handle_auth_file_save_auto(temp_context):
635635
logger.info(f" 认证状态已成功保存到: {auth_save_path}")
636636
print(f" ✅ 认证状态已成功保存到: {auth_save_path}", flush=True)
637637
except Exception as save_state_err:
638-
logger.error(f" ❌ 保存认证状态失败: {save_state_err}", exc_info=True)
639-
print(f" ❌ 保存认证状态失败: {save_state_err}", flush=True)
638+
logger.error(f" ❌ 自动保存认证状态失败: {save_state_err}", exc_info=True)
639+
print(f" ❌ 自动保存认证状态失败: {save_state_err}", flush=True)
640+
641+
async def enable_temporary_chat_mode(page: AsyncPage):
642+
"""
643+
检查并启用 AI Studio 界面的“临时聊天”模式。
644+
这是一个独立的UI操作,应该在页面完全稳定后调用。
645+
"""
646+
try:
647+
logger.info("-> (UI Op) 正在检查并启用 '临时聊天' 模式...")
648+
649+
incognito_button_locator = page.locator('button[aria-label="Temporary chat toggle"]')
650+
651+
await incognito_button_locator.wait_for(state="visible", timeout=10000)
652+
653+
button_classes = await incognito_button_locator.get_attribute("class")
654+
655+
if button_classes and 'ms-button-active' in button_classes:
656+
logger.info("-> (UI Op) '临时聊天' 模式已激活。")
657+
else:
658+
logger.info("-> (UI Op) '临时聊天' 模式未激活,正在点击...")
659+
await incognito_button_locator.click(timeout=5000, force=True)
660+
await asyncio.sleep(1)
661+
662+
updated_classes = await incognito_button_locator.get_attribute("class")
663+
if updated_classes and 'ms-button-active' in updated_classes:
664+
logger.info("✅ (UI Op) '临时聊天' 模式已成功启用。")
665+
else:
666+
logger.warning("⚠️ (UI Op) 点击后 '临时聊天' 模式状态验证失败。")
667+
668+
except Exception as e:
669+
logger.warning(f"⚠️ (UI Op) 启用 '临时聊天' 模式时出错: {e}")

browser_utils/model_management.py

Lines changed: 47 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -274,27 +274,49 @@ async def switch_ai_studio_model(page: AsyncPage, model_id: str, req_id: str) ->
274274
if m_obj.get("id") == model_id:
275275
expected_display_name_for_target_id = m_obj.get("display_name")
276276
break
277+
278+
try:
279+
model_name_locator = page.locator('[data-test-id="model-name"]')
280+
actual_displayed_model_id_on_page_raw = await model_name_locator.first.inner_text(timeout=5000)
281+
actual_displayed_model_id_on_page = actual_displayed_model_id_on_page_raw.strip()
282+
283+
target_model_id = model_id
284+
285+
if actual_displayed_model_id_on_page == target_model_id:
286+
page_display_match = True
287+
logger.info(f"[{req_id}] ✅ 页面显示模型ID ('{actual_displayed_model_id_on_page}') 与期望ID ('{target_model_id}') 一致。")
288+
else:
289+
page_display_match = False
290+
logger.error(f"[{req_id}] ❌ 页面显示模型ID ('{actual_displayed_model_id_on_page}') 与期望ID ('{target_model_id}') 不一致。")
277291

278-
if not expected_display_name_for_target_id:
279-
logger.warning(f"[{req_id}] 无法在parsed_model_list中找到目标ID '{model_id}' 的显示名称,跳过页面显示名称验证。这可能不准确。")
280-
page_display_match = True
281-
else:
292+
except Exception as e_disp:
293+
page_display_match = False # 读取失败则认为不匹配
294+
logger.warning(f"[{req_id}] 读取页面显示的当前模型ID时出错: {e_disp}。将无法验证页面显示。")
295+
296+
if page_display_match:
282297
try:
283-
model_name_locator = page.locator('mat-select[data-test-ms-model-selector] div.model-option-content span.gmat-body-medium')
284-
actual_displayed_model_name_on_page_raw = await model_name_locator.first.inner_text(timeout=5000)
285-
actual_displayed_model_name_on_page = actual_displayed_model_name_on_page_raw.strip()
286-
normalized_actual_display = actual_displayed_model_name_on_page.lower()
287-
normalized_expected_display = expected_display_name_for_target_id.strip().lower()
298+
logger.info(f"[{req_id}] 模型切换成功,重新启用 '临时聊天' 模式...")
299+
incognito_button_locator = page.locator('button[aria-label="Temporary chat toggle"]')
288300

289-
if normalized_actual_display == normalized_expected_display:
290-
page_display_match = True
291-
logger.info(f"[{req_id}] ✅ 页面显示模型 ('{actual_displayed_model_name_on_page}') 与期望 ('{expected_display_name_for_target_id}') 一致。")
301+
await incognito_button_locator.wait_for(state="visible", timeout=5000)
302+
303+
button_classes = await incognito_button_locator.get_attribute("class")
304+
305+
if button_classes and 'ms-button-active' in button_classes:
306+
logger.info(f"[{req_id}] '临时聊天' 模式已处于激活状态。")
292307
else:
293-
logger.error(f"[{req_id}] ❌ 页面显示模型 ('{actual_displayed_model_name_on_page}') 与期望 ('{expected_display_name_for_target_id}') 不一致。(Raw page: '{actual_displayed_model_name_on_page_raw}')")
294-
except Exception as e_disp:
295-
logger.warning(f"[{req_id}] 读取页面显示的当前模型名称时出错: {e_disp}。将无法验证页面显示。")
296-
297-
if page_display_match:
308+
logger.info(f"[{req_id}] '临时聊天' 模式未激活,正在点击以开启...")
309+
await incognito_button_locator.click(timeout=3000)
310+
await asyncio.sleep(0.5)
311+
312+
updated_classes = await incognito_button_locator.get_attribute("class")
313+
if updated_classes and 'ms-button-active' in updated_classes:
314+
logger.info(f"[{req_id}] ✅ '临时聊天' 模式已成功重新启用。")
315+
else:
316+
logger.warning(f"[{req_id}] ⚠️ 点击后 '临时聊天' 模式状态验证失败,可能未成功重新开启。")
317+
318+
except Exception as e:
319+
logger.warning(f"[{req_id}] ⚠️ 模型切换后重新启用 '临时聊天' 模式失败: {e}")
298320
return True
299321
else:
300322
logger.error(f"[{req_id}] ❌ 模型切换失败,因为页面显示的模型与期望不符 (即使localStorage可能已更改)。")
@@ -306,7 +328,7 @@ async def switch_ai_studio_model(page: AsyncPage, model_id: str, req_id: str) ->
306328
current_displayed_name_for_revert_stripped = "无法读取"
307329

308330
try:
309-
model_name_locator_revert = page.locator('mat-select[data-test-ms-model-selector] div.model-option-content span.gmat-body-medium')
331+
model_name_locator_revert = page.locator('[data-test-id="model-name"]')
310332
current_displayed_name_for_revert_raw = await model_name_locator_revert.first.inner_text(timeout=5000)
311333
current_displayed_name_for_revert_stripped = current_displayed_name_for_revert_raw.strip()
312334
logger.info(f"[{req_id}] 恢复:页面当前显示的模型名称 (原始: '{current_displayed_name_for_revert_raw}', 清理后: '{current_displayed_name_for_revert_stripped}')")
@@ -324,17 +346,9 @@ async def switch_ai_studio_model(page: AsyncPage, model_id: str, req_id: str) ->
324346
return False
325347

326348
model_id_to_revert_to = None
327-
if parsed_model_list and current_displayed_name_for_revert_stripped != "无法读取":
328-
normalized_current_display_for_revert = current_displayed_name_for_revert_stripped.lower()
329-
for m_obj in parsed_model_list:
330-
parsed_list_display_name = m_obj.get("display_name", "").strip().lower()
331-
if parsed_list_display_name == normalized_current_display_for_revert:
332-
model_id_to_revert_to = m_obj.get("id")
333-
logger.info(f"[{req_id}] 恢复:页面显示名称 '{current_displayed_name_for_revert_stripped}' 对应模型ID: {model_id_to_revert_to}")
334-
break
335-
336-
if not model_id_to_revert_to:
337-
logger.warning(f"[{req_id}] 恢复:无法在 parsed_model_list 中找到与页面显示名称 '{current_displayed_name_for_revert_stripped}' 匹配的模型ID。")
349+
if current_displayed_name_for_revert_stripped != "无法读取":
350+
model_id_to_revert_to = current_displayed_name_for_revert_stripped
351+
logger.info(f"[{req_id}] 恢复:页面当前显示的ID是 '{model_id_to_revert_to}',将直接用于恢复。")
338352
else:
339353
if current_displayed_name_for_revert_stripped == "无法读取":
340354
logger.warning(f"[{req_id}] 恢复:因无法读取页面显示名称,故不能从 parsed_model_list 转换ID。")
@@ -529,7 +543,7 @@ async def _set_model_from_page_display(page: AsyncPage, set_storage: bool = Fals
529543

530544
try:
531545
logger.info(" 尝试从页面显示元素读取当前模型名称...")
532-
model_name_locator = page.locator('mat-select[data-test-ms-model-selector] div.model-option-content span.gmat-body-medium')
546+
model_name_locator = page.locator('[data-test-id="model-name"]')
533547
displayed_model_name_from_page_raw = await model_name_locator.first.inner_text(timeout=7000)
534548
displayed_model_name = displayed_model_name_from_page_raw.strip()
535549
logger.info(f" 页面当前显示模型名称 (原始: '{displayed_model_name_from_page_raw}', 清理后: '{displayed_model_name}')")
@@ -542,19 +556,10 @@ async def _set_model_from_page_display(page: AsyncPage, set_storage: bool = Fals
542556
except asyncio.TimeoutError:
543557
logger.warning(" 等待模型列表超时,可能无法准确转换显示名称为ID。")
544558

545-
if parsed_model_list:
546-
for model_obj in parsed_model_list:
547-
if model_obj.get("display_name") and model_obj.get("display_name").strip() == displayed_model_name:
548-
found_model_id_from_display = model_obj.get("id")
549-
logger.info(f" 显示名称 '{displayed_model_name}' 对应模型 ID: {found_model_id_from_display}")
550-
break
551-
552-
if not found_model_id_from_display:
553-
logger.warning(f" 未在已知模型列表中找到与显示名称 '{displayed_model_name}' 匹配的 ID。")
554-
else:
555-
logger.warning(" 模型列表尚不可用,无法将显示名称转换为ID。")
559+
found_model_id_from_display = displayed_model_name
560+
logger.info(f" 页面显示的直接是模型ID: '{found_model_id_from_display}'")
556561

557-
new_model_value = found_model_id_from_display if found_model_id_from_display else displayed_model_name
562+
new_model_value = found_model_id_from_display
558563
if server.current_ai_studio_model_id != new_model_value:
559564
server.current_ai_studio_model_id = new_model_value
560565
logger.info(f" 全局 current_ai_studio_model_id 已更新为: {server.current_ai_studio_model_id}")

browser_utils/page_controller.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
)
2424
from models import ClientDisconnectedError
2525
from .operations import save_error_snapshot, _wait_for_response_completion, _get_final_response_content
26+
from .initialization import enable_temporary_chat_mode
2627

2728
class PageController:
2829
"""封装了与AI Studio页面交互的所有操作。"""
@@ -572,6 +573,8 @@ async def clear_chat_history(self, check_client_disconnected: Callable):
572573
if can_attempt_clear:
573574
await self._execute_chat_clear(clear_chat_button_locator, confirm_button_locator, overlay_locator, check_client_disconnected)
574575
await self._verify_chat_cleared(check_client_disconnected)
576+
self.logger.info(f"[{self.req_id}] 聊天已清空,重新启用 '临时聊天' 模式...")
577+
await enable_temporary_chat_mode(self.page)
575578

576579
except Exception as e_clear:
577580
self.logger.error(f"[{self.req_id}] 清空聊天过程中发生错误: {e_clear}")

config/selectors.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,17 @@
1010

1111
# --- 按钮选择器 ---
1212
SUBMIT_BUTTON_SELECTOR = 'button[aria-label="Run"].run-button'
13-
CLEAR_CHAT_BUTTON_SELECTOR = 'button[data-test-clear="outside"][aria-label="Clear chat"]'
14-
CLEAR_CHAT_CONFIRM_BUTTON_SELECTOR = 'button.mdc-button:has-text("Continue")'
15-
UPLOAD_BUTTON_SELECTOR = 'button[aria-label="Upload File"]'
13+
CLEAR_CHAT_BUTTON_SELECTOR = 'button[data-test-clear="outside"][aria-label="New chat"]'
14+
CLEAR_CHAT_CONFIRM_BUTTON_SELECTOR = 'button.ms-button-primary:has-text("Discard and continue")'
15+
UPLOAD_BUTTON_SELECTOR = 'button[aria-label^="Insert assets"]'
1616

1717
# --- 响应相关选择器 ---
1818
RESPONSE_CONTAINER_SELECTOR = 'ms-chat-turn .chat-turn-container.model'
1919
RESPONSE_TEXT_SELECTOR = 'ms-cmark-node.cmark-node'
2020

2121
# --- 加载和状态选择器 ---
2222
LOADING_SPINNER_SELECTOR = 'button[aria-label="Run"].run-button svg .stoppable-spinner'
23-
OVERLAY_SELECTOR = 'div.cdk-overlay-backdrop'
23+
OVERLAY_SELECTOR = '.mat-mdc-dialog-inner-container'
2424

2525
# --- 错误提示选择器 ---
2626
ERROR_TOAST_SELECTOR = 'div.toast.warning, div.toast.error'
@@ -39,11 +39,11 @@
3939
MAX_OUTPUT_TOKENS_SELECTOR = 'input[aria-label="Maximum output tokens"]'
4040
STOP_SEQUENCE_INPUT_SELECTOR = 'input[aria-label="Add stop token"]'
4141
MAT_CHIP_REMOVE_BUTTON_SELECTOR = 'mat-chip-set mat-chip-row button[aria-label*="Remove"]'
42-
TOP_P_INPUT_SELECTOR = 'div.settings-item-column:has(h3:text-is("Top P")) input[type="number"].slider-input'
43-
TEMPERATURE_INPUT_SELECTOR = 'div[data-test-id="temperatureSliderContainer"] input[type="number"].slider-input'
42+
TOP_P_INPUT_SELECTOR = 'ms-slider input[type="number"][max="1"]'
43+
TEMPERATURE_INPUT_SELECTOR = 'ms-slider input[type="number"][max="2"]'
4444
USE_URL_CONTEXT_SELECTOR = 'button[aria-label="Browse the url context"]'
4545
SET_THINKING_BUDGET_TOGGLE_SELECTOR = 'button[aria-label="Toggle thinking budget between auto and manual"]'
4646
# Thinking budget slider input
4747
THINKING_BUDGET_INPUT_SELECTOR = '//div[contains(@class, "settings-item") and .//p[normalize-space()="Set thinking budget"]]/following-sibling::div//input[@type="number"]'
4848
# --- Google Search Grounding ---
49-
GROUNDING_WITH_GOOGLE_SEARCH_TOGGLE_SELECTOR = 'div[data-test-id="searchAsAToolTooltip"] mat-slide-toggle button'
49+
GROUNDING_WITH_GOOGLE_SEARCH_TOGGLE_SELECTOR = 'div[data-test-id="searchAsAToolTooltip"] mat-slide-toggle button'

0 commit comments

Comments
 (0)