majorchik-api 0.0.2a2__tar.gz → 0.0.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {majorchik_api-0.0.2a2 → majorchik_api-0.0.3}/PKG-INFO +2 -1
- {majorchik_api-0.0.2a2 → majorchik_api-0.0.3}/pyproject.toml +2 -1
- {majorchik_api-0.0.2a2 → majorchik_api-0.0.3}/src/browser_ai/base_api.py +27 -6
- {majorchik_api-0.0.2a2 → majorchik_api-0.0.3}/src/browser_ai/cli.py +4 -2
- {majorchik_api-0.0.2a2 → majorchik_api-0.0.3}/src/browser_ai/deepseek_api.py +16 -4
- majorchik_api-0.0.3/src/browser_ai/google_ai_studio_api.py +281 -0
- {majorchik_api-0.0.2a2 → majorchik_api-0.0.3}/src/majorchik_api.egg-info/PKG-INFO +2 -1
- {majorchik_api-0.0.2a2 → majorchik_api-0.0.3}/src/majorchik_api.egg-info/requires.txt +1 -0
- {majorchik_api-0.0.2a2 → majorchik_api-0.0.3}/tests/test_upload.py +6 -1
- majorchik_api-0.0.2a2/src/browser_ai/google_ai_studio_api.py +0 -169
- {majorchik_api-0.0.2a2 → majorchik_api-0.0.3}/README.md +0 -0
- {majorchik_api-0.0.2a2 → majorchik_api-0.0.3}/setup.cfg +0 -0
- {majorchik_api-0.0.2a2 → majorchik_api-0.0.3}/src/browser_ai/__init__.py +0 -0
- {majorchik_api-0.0.2a2 → majorchik_api-0.0.3}/src/browser_ai/exceptions.py +0 -0
- {majorchik_api-0.0.2a2 → majorchik_api-0.0.3}/src/browser_ai/factory.py +0 -0
- {majorchik_api-0.0.2a2 → majorchik_api-0.0.3}/src/majorchik_api.egg-info/SOURCES.txt +0 -0
- {majorchik_api-0.0.2a2 → majorchik_api-0.0.3}/src/majorchik_api.egg-info/dependency_links.txt +0 -0
- {majorchik_api-0.0.2a2 → majorchik_api-0.0.3}/src/majorchik_api.egg-info/entry_points.txt +0 -0
- {majorchik_api-0.0.2a2 → majorchik_api-0.0.3}/src/majorchik_api.egg-info/top_level.txt +0 -0
- {majorchik_api-0.0.2a2 → majorchik_api-0.0.3}/tests/test_setup_profile.py +0 -0
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: majorchik-api
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.3
|
|
4
4
|
Summary: Пакет для взаимодействия с современными нейросетями (Gemini, DeepSeek) через автоматизацию браузера
|
|
5
5
|
Author-email: GO Software <gosoftware2025@gmail.com>
|
|
6
6
|
Requires-Python: >=3.8
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
8
8
|
Requires-Dist: playwright>=1.30.0
|
|
9
|
+
Requires-Dist: playwright-stealth
|
|
9
10
|
|
|
10
11
|
# Majorchik API (Browser AI)
|
|
11
12
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "majorchik-api"
|
|
7
|
-
version = "0.0.
|
|
7
|
+
version = "0.0.3"
|
|
8
8
|
authors = [
|
|
9
9
|
{name = "GO Software", email = "gosoftware2025@gmail.com"},
|
|
10
10
|
]
|
|
@@ -13,6 +13,7 @@ readme = "README.md"
|
|
|
13
13
|
requires-python = ">=3.8"
|
|
14
14
|
dependencies =[
|
|
15
15
|
"playwright>=1.30.0",
|
|
16
|
+
"playwright-stealth"
|
|
16
17
|
]
|
|
17
18
|
|
|
18
19
|
[project.scripts]
|
|
@@ -8,12 +8,13 @@ class BaseWebAPI(ABC):
|
|
|
8
8
|
Базовый класс для всех Web API моделей.
|
|
9
9
|
Инкапсулирует логику запуска браузера, маскировки и управления ресурсами.
|
|
10
10
|
"""
|
|
11
|
-
def __init__(self, profile_dir: str, url: str, headless: bool = True, disable_images: bool = True, setup_mode: bool = False, **kwargs):
|
|
11
|
+
def __init__(self, profile_dir: str, url: str, headless: bool = True, disable_images: bool = True, setup_mode: bool = False, apply_stealth: bool = True, **kwargs):
|
|
12
12
|
self.profile_dir = os.path.abspath(profile_dir)
|
|
13
13
|
self.url = url
|
|
14
14
|
self.headless = headless
|
|
15
15
|
self.disable_images = disable_images
|
|
16
16
|
self.setup_mode = setup_mode
|
|
17
|
+
self.apply_stealth = apply_stealth
|
|
17
18
|
self.extra_kwargs = kwargs
|
|
18
19
|
|
|
19
20
|
if not self.setup_mode:
|
|
@@ -31,8 +32,9 @@ class BaseWebAPI(ABC):
|
|
|
31
32
|
|
|
32
33
|
args =[
|
|
33
34
|
"--disable-blink-features=AutomationControlled",
|
|
34
|
-
"--
|
|
35
|
-
"--disable-
|
|
35
|
+
"--disable-infobars",
|
|
36
|
+
"--disable-quic",
|
|
37
|
+
"--test-type"
|
|
36
38
|
]
|
|
37
39
|
|
|
38
40
|
if self.disable_images:
|
|
@@ -42,14 +44,33 @@ class BaseWebAPI(ABC):
|
|
|
42
44
|
user_data_dir=self.profile_dir,
|
|
43
45
|
headless=self.headless,
|
|
44
46
|
channel="chrome",
|
|
45
|
-
ignore_default_args=["--enable-automation"],
|
|
47
|
+
ignore_default_args=["--enable-automation", "--no-sandbox"],
|
|
46
48
|
args=args,
|
|
47
|
-
|
|
49
|
+
no_viewport=True,
|
|
48
50
|
permissions=["clipboard-read", "clipboard-write"]
|
|
49
51
|
)
|
|
50
|
-
|
|
52
|
+
|
|
53
|
+
# Используем дефолтную вкладку, если она есть, вместо открытия новой
|
|
54
|
+
if len(self.browser.pages) > 0:
|
|
55
|
+
self.page = self.browser.pages[0]
|
|
56
|
+
else:
|
|
57
|
+
self.page = self.browser.new_page()
|
|
58
|
+
|
|
59
|
+
# Включаем маскировку (Stealth)
|
|
51
60
|
self.page.add_init_script("Object.defineProperty(navigator, 'webdriver', {get: () => undefined})")
|
|
52
61
|
|
|
62
|
+
if self.apply_stealth:
|
|
63
|
+
try:
|
|
64
|
+
try:
|
|
65
|
+
from playwright_stealth import stealth_sync
|
|
66
|
+
stealth_sync(self.page)
|
|
67
|
+
except ImportError:
|
|
68
|
+
from playwright_stealth import Stealth
|
|
69
|
+
Stealth().apply_stealth_sync(self.page)
|
|
70
|
+
except Exception as e:
|
|
71
|
+
print(f"Предупреждение: playwright-stealth не загрузился. Ошибка: {e}")
|
|
72
|
+
print("Подсказка: возможно, пакет установлен глобально, а IDE использует изолированное окружение (.venv).")
|
|
73
|
+
|
|
53
74
|
self.page.goto(self.url, wait_until="domcontentloaded")
|
|
54
75
|
|
|
55
76
|
if not self.setup_mode:
|
|
@@ -9,7 +9,7 @@ def cmd_setup(args):
|
|
|
9
9
|
print("Открываем браузер...")
|
|
10
10
|
|
|
11
11
|
try:
|
|
12
|
-
api = get_ai_client(args.model, profile_dir=args.profile_dir, headless=False, setup_mode=True)
|
|
12
|
+
api = get_ai_client(args.model, profile_dir=args.profile_dir, headless=False, setup_mode=True, model_id=args.model_id)
|
|
13
13
|
|
|
14
14
|
print("\nПожалуйста, авторизуйтесь и пройдите все проверки (включая Cloudflare).")
|
|
15
15
|
print("Убедитесь, что интерфейс чата полностью загружен.")
|
|
@@ -25,7 +25,7 @@ def cmd_chat(args):
|
|
|
25
25
|
print(f"\nИнициализация {args.model.upper()} (профиль: {args.profile_dir}, headless: {args.headless})...")
|
|
26
26
|
ai = None
|
|
27
27
|
try:
|
|
28
|
-
ai = get_ai_client(args.model, profile_dir=args.profile_dir, headless=args.headless)
|
|
28
|
+
ai = get_ai_client(args.model, profile_dir=args.profile_dir, headless=args.headless, model_id=args.model_id)
|
|
29
29
|
|
|
30
30
|
print("Браузер готов. Вводите промпты (для выхода введите 'exit' или 'quit').")
|
|
31
31
|
print("-" * 50)
|
|
@@ -76,6 +76,7 @@ def main():
|
|
|
76
76
|
parser_setup = subparsers.add_parser("setup", help="Создать и настроить профиль браузера (авторизация)")
|
|
77
77
|
parser_setup.add_argument("model", choices=models_list, help="Модель для настройки")
|
|
78
78
|
parser_setup.add_argument("--profile-dir", required=True, help="Путь к директории для сохранения профиля")
|
|
79
|
+
parser_setup.add_argument("--model-id", type=str, default=None, help="Специфичный ID (напр. gemini-2.5-flash-image)")
|
|
79
80
|
|
|
80
81
|
# Команда chat
|
|
81
82
|
parser_chat = subparsers.add_parser("chat", help="Запустить интерактивный чат с нейросетью")
|
|
@@ -83,6 +84,7 @@ def main():
|
|
|
83
84
|
parser_chat.add_argument("--profile-dir", required=True, help="Путь к директории сохраненного профиля")
|
|
84
85
|
parser_chat.add_argument("--headless", action="store_true", help="Скрыть визуально браузер (тихий режим)")
|
|
85
86
|
parser_chat.add_argument("--files", nargs="*", help="Пути к файлам для прикрепления к первому промпту")
|
|
87
|
+
parser_chat.add_argument("--model-id", type=str, default=None, help="Специфичный ID (напр. gemini-2.5-flash-image)")
|
|
86
88
|
|
|
87
89
|
args = parser.parse_args()
|
|
88
90
|
|
|
@@ -2,6 +2,7 @@ from .base_api import BaseWebAPI
|
|
|
2
2
|
from .exceptions import AuthRequiredError, FirstTokenTimeoutError, ResponseTimeoutError
|
|
3
3
|
import time
|
|
4
4
|
import re
|
|
5
|
+
import sys
|
|
5
6
|
|
|
6
7
|
def _clean_text(text: str) -> str:
|
|
7
8
|
return re.sub(r'\n{3,}', '\n\n', text).strip()
|
|
@@ -130,9 +131,20 @@ class DeepSeekAPI(BaseWebAPI):
|
|
|
130
131
|
initial_last_text = ""
|
|
131
132
|
|
|
132
133
|
chat_input = self.page.locator("#chat-input, textarea, [contenteditable='true']").first
|
|
133
|
-
chat_input.
|
|
134
|
+
chat_input.click(delay=30)
|
|
135
|
+
time.sleep(0.3)
|
|
136
|
+
|
|
137
|
+
if len(prompt) > 200:
|
|
138
|
+
self.page.evaluate("text => navigator.clipboard.writeText(text)", prompt)
|
|
139
|
+
modifier = "Meta" if sys.platform == "darwin" else "Control"
|
|
140
|
+
chat_input.press(f"{modifier}+v", delay=50)
|
|
141
|
+
time.sleep(0.2)
|
|
142
|
+
chat_input.press_sequentially(" ", delay=30)
|
|
143
|
+
else:
|
|
144
|
+
chat_input.press_sequentially(prompt, delay=15)
|
|
145
|
+
|
|
134
146
|
time.sleep(0.5)
|
|
135
|
-
chat_input.press("Enter")
|
|
147
|
+
chat_input.press("Enter", delay=40)
|
|
136
148
|
|
|
137
149
|
last_text = ""
|
|
138
150
|
stable_count = 0
|
|
@@ -144,7 +156,7 @@ class DeepSeekAPI(BaseWebAPI):
|
|
|
144
156
|
if (mds.length === 0) return false;
|
|
145
157
|
const lastMd = mds[mds.length - 1];
|
|
146
158
|
let curr = lastMd;
|
|
147
|
-
for (let i = 0; i <
|
|
159
|
+
for (let i = 0; i < 7; i++) {
|
|
148
160
|
if (!curr) break;
|
|
149
161
|
let sibling = curr.nextElementSibling;
|
|
150
162
|
while (sibling) {
|
|
@@ -154,7 +166,7 @@ class DeepSeekAPI(BaseWebAPI):
|
|
|
154
166
|
sibling = sibling.nextElementSibling;
|
|
155
167
|
continue;
|
|
156
168
|
}
|
|
157
|
-
const btns = sibling.querySelectorAll('div
|
|
169
|
+
const btns = sibling.querySelectorAll('div[role="button"][class*="ds-icon-button"], div[role="button"][class*="ds-button"]');
|
|
158
170
|
if (btns.length >= 2) {
|
|
159
171
|
btns[0].click();
|
|
160
172
|
return true;
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
from .base_api import BaseWebAPI
|
|
2
|
+
from .exceptions import AuthRequiredError, FirstTokenTimeoutError, ResponseTimeoutError, QuotaExceededError
|
|
3
|
+
import time
|
|
4
|
+
import re
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
def _clean_text(text: str) -> str:
|
|
8
|
+
return re.sub(r'\n{3,}', '\n\n', text).strip()
|
|
9
|
+
|
|
10
|
+
class GoogleAIStudioAPI(BaseWebAPI):
|
|
11
|
+
def __init__(self, profile_dir: str, headless: bool = True, setup_mode: bool = False, **kwargs):
|
|
12
|
+
model_id = kwargs.get("model_id")
|
|
13
|
+
base_url = "https://aistudio.google.com/prompts/new_chat"
|
|
14
|
+
url = f"{base_url}?model={model_id}" if model_id else base_url
|
|
15
|
+
|
|
16
|
+
# Включаем картинки, так как они нужны для работы моделей генерирующих изображения (gemini-2.5-flash-image)
|
|
17
|
+
disable_images = kwargs.get("disable_images", False)
|
|
18
|
+
|
|
19
|
+
super().__init__(
|
|
20
|
+
profile_dir=profile_dir,
|
|
21
|
+
url=url,
|
|
22
|
+
headless=headless,
|
|
23
|
+
disable_images=disable_images,
|
|
24
|
+
setup_mode=setup_mode,
|
|
25
|
+
apply_stealth=False, # Отключаем stealth, так как он ломает фронтенд Google AI Studio
|
|
26
|
+
**kwargs
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
def wait_for_ready(self):
|
|
30
|
+
for _ in range(30):
|
|
31
|
+
if "accounts.google.com" in self.page.url or "signin" in self.page.url.lower():
|
|
32
|
+
raise AuthRequiredError("Требуется повторная авторизация в Google AI Studio.")
|
|
33
|
+
if self.page.locator("textarea,[contenteditable='true']").count() > 0:
|
|
34
|
+
return
|
|
35
|
+
time.sleep(1)
|
|
36
|
+
self.page.wait_for_selector("textarea, [contenteditable='true']", timeout=1000)
|
|
37
|
+
|
|
38
|
+
def ask(self, prompt: str, files: list = None, timeout: int = 600, first_token_timeout: int = 60, headless: bool = None, **kwargs) -> str:
|
|
39
|
+
if headless is not None and headless != self.headless:
|
|
40
|
+
self._start_browser(headless)
|
|
41
|
+
|
|
42
|
+
overall_start_time = time.time()
|
|
43
|
+
if "accounts.google.com" in self.page.url or "signin" in self.page.url.lower():
|
|
44
|
+
raise AuthRequiredError("Сессия истекла. Требуется повторная авторизация в Google AI Studio.")
|
|
45
|
+
|
|
46
|
+
self.page.evaluate("navigator.clipboard.writeText('').catch(() => {})")
|
|
47
|
+
|
|
48
|
+
import random
|
|
49
|
+
|
|
50
|
+
# 1. Разогреваем сессию случайными движениями мыши
|
|
51
|
+
try:
|
|
52
|
+
for _ in range(3):
|
|
53
|
+
self.page.mouse.move(random.randint(200, 800), random.randint(200, 600), steps=10)
|
|
54
|
+
self.page.wait_for_timeout(random.randint(100, 300))
|
|
55
|
+
except:
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
if files:
|
|
59
|
+
try:
|
|
60
|
+
attach_btn = self.page.locator('[data-test-id="add-media-button"], button[aria-label*="Insert"], button[aria-label*="Attach"], [data-test-id="file-upload-button"]').first
|
|
61
|
+
# Эмулируем реальное наведение мыши перед кликом
|
|
62
|
+
if attach_btn.count() > 0 and attach_btn.is_visible():
|
|
63
|
+
box = attach_btn.bounding_box()
|
|
64
|
+
if box:
|
|
65
|
+
self.page.mouse.move(box['x'] + box['width'] / 2, box['y'] + box['height'] / 2, steps=10)
|
|
66
|
+
self.page.wait_for_timeout(200)
|
|
67
|
+
self.page.mouse.click(box['x'] + box['width'] / 2, box['y'] + box['height'] / 2, delay=random.randint(40, 80))
|
|
68
|
+
else:
|
|
69
|
+
attach_btn.click(delay=50)
|
|
70
|
+
else:
|
|
71
|
+
attach_btn.click(delay=50)
|
|
72
|
+
|
|
73
|
+
file_input = self.page.locator('input[type="file"]')
|
|
74
|
+
file_input.wait_for(state="attached", timeout=5000)
|
|
75
|
+
file_input.set_input_files(files)
|
|
76
|
+
|
|
77
|
+
time.sleep(1.5)
|
|
78
|
+
loading_texts = ["Loading", "В ожидании", "Загрузка", "Uploading"]
|
|
79
|
+
for text in loading_texts:
|
|
80
|
+
try:
|
|
81
|
+
for el in self.page.get_by_text(text).all():
|
|
82
|
+
if el.is_visible():
|
|
83
|
+
el.wait_for(state="hidden", timeout=30000)
|
|
84
|
+
except:
|
|
85
|
+
pass
|
|
86
|
+
time.sleep(1)
|
|
87
|
+
except Exception as e:
|
|
88
|
+
print(f"Не удалось прикрепить файлы: {e}")
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
initial_thumb_count = self.page.locator('button[aria-label="Good response"], .turn-footer button, button:has(.material-symbols-outlined:has-text("thumb_up"))').count()
|
|
92
|
+
initial_model_turns = self.page.locator('ms-chat-turn, div[data-turn-role="Model"], div.chat-turn-container.model').count()
|
|
93
|
+
except:
|
|
94
|
+
initial_thumb_count = 0
|
|
95
|
+
initial_model_turns = 0
|
|
96
|
+
|
|
97
|
+
self.page.keyboard.press("Escape")
|
|
98
|
+
self.page.wait_for_timeout(300)
|
|
99
|
+
|
|
100
|
+
chat_input = self.page.locator("textarea, [contenteditable='true'], [role='textbox']").first
|
|
101
|
+
|
|
102
|
+
# Эмулируем наведение мыши на поле ввода
|
|
103
|
+
if chat_input.count() > 0 and chat_input.is_visible():
|
|
104
|
+
box = chat_input.bounding_box()
|
|
105
|
+
if box:
|
|
106
|
+
self.page.mouse.move(box['x'] + 20, box['y'] + 10, steps=10)
|
|
107
|
+
self.page.wait_for_timeout(random.randint(100, 300))
|
|
108
|
+
self.page.mouse.click(box['x'] + 20, box['y'] + 10, delay=random.randint(30, 70))
|
|
109
|
+
else:
|
|
110
|
+
chat_input.click(delay=40)
|
|
111
|
+
else:
|
|
112
|
+
chat_input.click(delay=40)
|
|
113
|
+
|
|
114
|
+
self.page.wait_for_timeout(300)
|
|
115
|
+
|
|
116
|
+
# Вставка из буфера, как ты делал вручную (это безопаснее, чем эмуляция сотен нажатий без ошибок)
|
|
117
|
+
self.page.evaluate("text => navigator.clipboard.writeText(text)", prompt)
|
|
118
|
+
modifier = "Meta" if sys.platform == "darwin" else "Control"
|
|
119
|
+
chat_input.press(f"{modifier}+v", delay=random.randint(40, 90))
|
|
120
|
+
|
|
121
|
+
self.page.wait_for_timeout(random.randint(300, 600))
|
|
122
|
+
|
|
123
|
+
# Делаем случайное движение перед нажатием кнопки отправки
|
|
124
|
+
self.page.mouse.move(random.randint(300, 700), random.randint(200, 500), steps=10)
|
|
125
|
+
self.page.wait_for_timeout(200)
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
run_btn = self.page.locator('button:has-text("Run")').last
|
|
129
|
+
if run_btn.count() > 0 and run_btn.is_visible(timeout=500):
|
|
130
|
+
box = run_btn.bounding_box()
|
|
131
|
+
if box:
|
|
132
|
+
self.page.mouse.move(box['x'] + box['width']/2, box['y'] + box['height']/2, steps=10)
|
|
133
|
+
self.page.wait_for_timeout(200)
|
|
134
|
+
self.page.mouse.click(box['x'] + box['width']/2, box['y'] + box['height']/2, delay=random.randint(30, 80))
|
|
135
|
+
else:
|
|
136
|
+
run_btn.click(delay=50)
|
|
137
|
+
else:
|
|
138
|
+
chat_input.press("Control+Enter", delay=random.randint(40, 80))
|
|
139
|
+
except:
|
|
140
|
+
chat_input.press("Control+Enter", delay=random.randint(40, 80))
|
|
141
|
+
|
|
142
|
+
first_token_start_time = time.time()
|
|
143
|
+
completed = False
|
|
144
|
+
first_token_received = False
|
|
145
|
+
|
|
146
|
+
while True:
|
|
147
|
+
if time.time() - overall_start_time > timeout:
|
|
148
|
+
raise ResponseTimeoutError(f"Превышено максимальное время ожидания ответа ({timeout} сек).")
|
|
149
|
+
if "accounts.google.com" in self.page.url or "signin" in self.page.url.lower():
|
|
150
|
+
raise AuthRequiredError("Сессия истекла. Требуется повторная авторизация в Google AI Studio.")
|
|
151
|
+
try:
|
|
152
|
+
error_indicators = [
|
|
153
|
+
"user has exceeded quota",
|
|
154
|
+
"You've reached your rate limit",
|
|
155
|
+
"You've reached your quota for the day"
|
|
156
|
+
]
|
|
157
|
+
for err_text in error_indicators:
|
|
158
|
+
err_loc = self.page.get_by_text(err_text)
|
|
159
|
+
if err_loc.count() > 0 and err_loc.first.is_visible():
|
|
160
|
+
raise QuotaExceededError("Лимит запросов (токены) исчерпан в Google AI Studio.")
|
|
161
|
+
except QuotaExceededError:
|
|
162
|
+
raise
|
|
163
|
+
except Exception:
|
|
164
|
+
pass
|
|
165
|
+
try:
|
|
166
|
+
skip_btn = self.page.locator('button:has-text("Skip")')
|
|
167
|
+
if skip_btn.count() > 0:
|
|
168
|
+
if skip_btn.last.is_visible():
|
|
169
|
+
skip_btn.last.evaluate("el => el.click()")
|
|
170
|
+
self.page.wait_for_timeout(500)
|
|
171
|
+
except:
|
|
172
|
+
pass
|
|
173
|
+
|
|
174
|
+
if not first_token_received:
|
|
175
|
+
try:
|
|
176
|
+
current_model_turns = self.page.locator('ms-chat-turn, div[data-turn-role="Model"], div.chat-turn-container.model').count()
|
|
177
|
+
if current_model_turns > initial_model_turns:
|
|
178
|
+
first_token_received = True
|
|
179
|
+
elif time.time() - first_token_start_time > first_token_timeout:
|
|
180
|
+
raise FirstTokenTimeoutError(f"Модель не начала генерацию ответа за {first_token_timeout} секунд.")
|
|
181
|
+
except:
|
|
182
|
+
pass
|
|
183
|
+
|
|
184
|
+
try:
|
|
185
|
+
current_thumb_count = self.page.locator('button[aria-label="Good response"], .turn-footer button, button:has(.material-symbols-outlined:has-text("thumb_up"))').count()
|
|
186
|
+
if current_thumb_count > initial_thumb_count:
|
|
187
|
+
completed = True
|
|
188
|
+
break
|
|
189
|
+
except:
|
|
190
|
+
pass
|
|
191
|
+
self.page.wait_for_timeout(1000)
|
|
192
|
+
|
|
193
|
+
if not completed:
|
|
194
|
+
print("Предупреждение: Не удалось дождаться кнопки thumb_up, пробуем извлечь текущий ответ (модель может быть image-генератором).")
|
|
195
|
+
|
|
196
|
+
self.page.wait_for_timeout(2000) # Даем UI немного времени на финальный рендеринг картинок
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
last_turn = self.page.locator('ms-chat-turn, div[data-turn-role="Model"], div.chat-turn-container.model').last
|
|
200
|
+
|
|
201
|
+
# 0. Извлекаем сгенерированные изображения
|
|
202
|
+
js_extract_images = """
|
|
203
|
+
async (el) => {
|
|
204
|
+
const imgs = el.querySelectorAll('img:not(.avatar)');
|
|
205
|
+
let res = [];
|
|
206
|
+
for (let img of imgs) {
|
|
207
|
+
let src = img.src;
|
|
208
|
+
if (!src || src.startsWith('data:image/svg') || src.includes('gstatic.com')) continue;
|
|
209
|
+
|
|
210
|
+
if (src.startsWith('blob:')) {
|
|
211
|
+
try {
|
|
212
|
+
let blob = await fetch(src).then(r => r.blob());
|
|
213
|
+
let b64 = await new Promise(resolve => {
|
|
214
|
+
let reader = new FileReader();
|
|
215
|
+
reader.onloadend = () => resolve(reader.result);
|
|
216
|
+
reader.readAsDataURL(blob);
|
|
217
|
+
});
|
|
218
|
+
res.push(b64);
|
|
219
|
+
} catch(e) {
|
|
220
|
+
res.push(src);
|
|
221
|
+
}
|
|
222
|
+
} else {
|
|
223
|
+
res.push(src);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const canvases = el.querySelectorAll('canvas');
|
|
227
|
+
canvases.forEach(c => {
|
|
228
|
+
try { res.push(c.toDataURL('image/png')); } catch(e) {}
|
|
229
|
+
});
|
|
230
|
+
return res;
|
|
231
|
+
}
|
|
232
|
+
"""
|
|
233
|
+
|
|
234
|
+
extracted_images = []
|
|
235
|
+
try:
|
|
236
|
+
extracted_images = last_turn.evaluate(js_extract_images)
|
|
237
|
+
except Exception as e:
|
|
238
|
+
pass
|
|
239
|
+
|
|
240
|
+
image_append_text = ""
|
|
241
|
+
if extracted_images:
|
|
242
|
+
image_append_text = "\n\n[Сгенерированные изображения (Base64/URL)]:\n" + "\n\n".join(extracted_images)
|
|
243
|
+
|
|
244
|
+
# Попытка 1: найти кнопку меню и скопировать как Markdown
|
|
245
|
+
copy_success = False
|
|
246
|
+
try:
|
|
247
|
+
options_btn = last_turn.locator('button[aria-label="Open options"], button:has(.material-symbols-outlined:has-text("more_vert"))').last
|
|
248
|
+
if options_btn.count() > 0 and options_btn.is_visible():
|
|
249
|
+
options_btn.evaluate("el => el.click()")
|
|
250
|
+
self.page.wait_for_timeout(500)
|
|
251
|
+
|
|
252
|
+
copy_btn = self.page.locator('button[role="menuitem"]:has-text("Copy as markdown"), button[role="menuitem"]:has-text("Copy")').first
|
|
253
|
+
if copy_btn.count() > 0 and copy_btn.is_visible():
|
|
254
|
+
copy_btn.evaluate("el => el.click()")
|
|
255
|
+
self.page.wait_for_timeout(1000)
|
|
256
|
+
copy_success = True
|
|
257
|
+
except:
|
|
258
|
+
pass
|
|
259
|
+
|
|
260
|
+
if copy_success:
|
|
261
|
+
clip_val = self.page.evaluate("navigator.clipboard.readText().catch(() => '')")
|
|
262
|
+
if clip_val or extracted_images:
|
|
263
|
+
return clip_val + image_append_text
|
|
264
|
+
|
|
265
|
+
# Попытка 2: Читаем текст прямо из DOM
|
|
266
|
+
try:
|
|
267
|
+
last_turn.evaluate("""(el) => {
|
|
268
|
+
const footers = el.querySelectorAll('.turn-footer, .actions-container');
|
|
269
|
+
footers.forEach(f => f.style.display = 'none');
|
|
270
|
+
}""")
|
|
271
|
+
except:
|
|
272
|
+
pass
|
|
273
|
+
|
|
274
|
+
text = last_turn.inner_text()
|
|
275
|
+
if text.strip() or extracted_images:
|
|
276
|
+
return _clean_text(text) + image_append_text
|
|
277
|
+
|
|
278
|
+
except Exception as e:
|
|
279
|
+
return f"Ошибка при извлечении текста/изображений: {e}"
|
|
280
|
+
|
|
281
|
+
return "Не удалось получить ответ (вероятно, изменения в верстке Google AI Studio)."
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: majorchik-api
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.3
|
|
4
4
|
Summary: Пакет для взаимодействия с современными нейросетями (Gemini, DeepSeek) через автоматизацию браузера
|
|
5
5
|
Author-email: GO Software <gosoftware2025@gmail.com>
|
|
6
6
|
Requires-Python: >=3.8
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
8
8
|
Requires-Dist: playwright>=1.30.0
|
|
9
|
+
Requires-Dist: playwright-stealth
|
|
9
10
|
|
|
10
11
|
# Majorchik API (Browser AI)
|
|
11
12
|
|
|
@@ -22,11 +22,16 @@ def main():
|
|
|
22
22
|
|
|
23
23
|
profile_dir = input("Введите абсолютный или относительный путь до папки профиля (например ./my_profiles/google_ai_studio_default): ").strip()
|
|
24
24
|
|
|
25
|
+
model_id = None
|
|
26
|
+
if choice == "1":
|
|
27
|
+
model_id = input("Введите ID модели или нажмите Enter для стандартной (например gemini-2.5-flash-image): ").strip()
|
|
28
|
+
model_id = model_id if model_id else None
|
|
29
|
+
|
|
25
30
|
ai = None
|
|
26
31
|
try:
|
|
27
32
|
if choice == "1":
|
|
28
33
|
print("Запускаем Google AI Studio (без headless, чтобы видеть процесс)...")
|
|
29
|
-
ai = get_ai_client("google_ai_studio", profile_dir=profile_dir, headless=False)
|
|
34
|
+
ai = get_ai_client("google_ai_studio", profile_dir=profile_dir, headless=False, model_id=model_id)
|
|
30
35
|
elif choice == "2":
|
|
31
36
|
print("Запускаем DeepSeek (без headless, чтобы видеть процесс)...")
|
|
32
37
|
ai = get_ai_client("deepseek", profile_dir=profile_dir, headless=False)
|
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
from .base_api import BaseWebAPI
|
|
2
|
-
from .exceptions import AuthRequiredError, FirstTokenTimeoutError, ResponseTimeoutError, QuotaExceededError
|
|
3
|
-
import time
|
|
4
|
-
import re
|
|
5
|
-
|
|
6
|
-
def _clean_text(text: str) -> str:
|
|
7
|
-
return re.sub(r'\n{3,}', '\n\n', text).strip()
|
|
8
|
-
|
|
9
|
-
class GoogleAIStudioAPI(BaseWebAPI):
|
|
10
|
-
def __init__(self, profile_dir: str, headless: bool = True, setup_mode: bool = False, **kwargs):
|
|
11
|
-
super().__init__(
|
|
12
|
-
profile_dir=profile_dir,
|
|
13
|
-
url="https://aistudio.google.com/app/prompts/new_chat",
|
|
14
|
-
headless=headless,
|
|
15
|
-
disable_images=True,
|
|
16
|
-
setup_mode=setup_mode,
|
|
17
|
-
**kwargs
|
|
18
|
-
)
|
|
19
|
-
|
|
20
|
-
def wait_for_ready(self):
|
|
21
|
-
for _ in range(30):
|
|
22
|
-
if "accounts.google.com" in self.page.url or "signin" in self.page.url.lower():
|
|
23
|
-
raise AuthRequiredError("Требуется повторная авторизация в Google AI Studio.")
|
|
24
|
-
if self.page.locator("textarea,[contenteditable='true']").count() > 0:
|
|
25
|
-
return
|
|
26
|
-
time.sleep(1)
|
|
27
|
-
self.page.wait_for_selector("textarea, [contenteditable='true']", timeout=1000)
|
|
28
|
-
|
|
29
|
-
def ask(self, prompt: str, files: list = None, timeout: int = 600, first_token_timeout: int = 60, headless: bool = None, **kwargs) -> str:
|
|
30
|
-
if headless is not None and headless != self.headless:
|
|
31
|
-
self._start_browser(headless)
|
|
32
|
-
|
|
33
|
-
overall_start_time = time.time()
|
|
34
|
-
if "accounts.google.com" in self.page.url or "signin" in self.page.url.lower():
|
|
35
|
-
raise AuthRequiredError("Сессия истекла. Требуется повторная авторизация в Google AI Studio.")
|
|
36
|
-
|
|
37
|
-
self.page.evaluate("navigator.clipboard.writeText('').catch(() => {})")
|
|
38
|
-
|
|
39
|
-
if files:
|
|
40
|
-
try:
|
|
41
|
-
self.page.locator('[data-test-id="add-media-button"], button[aria-label*="Insert"]').first.click()
|
|
42
|
-
file_input = self.page.locator('input[type="file"]')
|
|
43
|
-
file_input.wait_for(state="attached", timeout=5000)
|
|
44
|
-
file_input.set_input_files(files)
|
|
45
|
-
|
|
46
|
-
time.sleep(1.5)
|
|
47
|
-
loading_texts =["Loading", "В ожидании", "Загрузка", "Uploading"]
|
|
48
|
-
for text in loading_texts:
|
|
49
|
-
try:
|
|
50
|
-
for el in self.page.get_by_text(text).all():
|
|
51
|
-
if el.is_visible():
|
|
52
|
-
el.wait_for(state="hidden", timeout=30000)
|
|
53
|
-
except:
|
|
54
|
-
pass
|
|
55
|
-
time.sleep(1)
|
|
56
|
-
except Exception as e:
|
|
57
|
-
print(f"Не удалось прикрепить файлы: {e}")
|
|
58
|
-
|
|
59
|
-
try:
|
|
60
|
-
initial_thumb_count = self.page.locator('button[aria-label="Good response"], .turn-footer button, button:has(.material-symbols-outlined:has-text("thumb_up"))').count()
|
|
61
|
-
initial_model_turns = self.page.locator('ms-chat-turn, div[data-turn-role="Model"], div.chat-turn-container.model').count()
|
|
62
|
-
except:
|
|
63
|
-
initial_thumb_count = 0
|
|
64
|
-
initial_model_turns = 0
|
|
65
|
-
|
|
66
|
-
self.page.keyboard.press("Escape")
|
|
67
|
-
self.page.wait_for_timeout(300)
|
|
68
|
-
|
|
69
|
-
chat_input = self.page.locator("textarea,[contenteditable='true'],[role='textbox']").first
|
|
70
|
-
chat_input.click(force=True)
|
|
71
|
-
chat_input.fill(prompt)
|
|
72
|
-
self.page.wait_for_timeout(500)
|
|
73
|
-
|
|
74
|
-
chat_input.press("Control+Enter")
|
|
75
|
-
|
|
76
|
-
first_token_start_time = time.time()
|
|
77
|
-
completed = False
|
|
78
|
-
first_token_received = False
|
|
79
|
-
|
|
80
|
-
while True:
|
|
81
|
-
if time.time() - overall_start_time > timeout:
|
|
82
|
-
raise ResponseTimeoutError(f"Превышено максимальное время ожидания ответа ({timeout} сек).")
|
|
83
|
-
if "accounts.google.com" in self.page.url or "signin" in self.page.url.lower():
|
|
84
|
-
raise AuthRequiredError("Сессия истекла. Требуется повторная авторизация в Google AI Studio.")
|
|
85
|
-
try:
|
|
86
|
-
error_indicators =[
|
|
87
|
-
"user has exceeded quota",
|
|
88
|
-
"You've reached your rate limit",
|
|
89
|
-
"You've reached your quota for the day"
|
|
90
|
-
]
|
|
91
|
-
for err_text in error_indicators:
|
|
92
|
-
err_loc = self.page.get_by_text(err_text)
|
|
93
|
-
if err_loc.count() > 0 and err_loc.first.is_visible():
|
|
94
|
-
raise QuotaExceededError("Лимит запросов (токены) исчерпан в Google AI Studio.")
|
|
95
|
-
except QuotaExceededError:
|
|
96
|
-
raise
|
|
97
|
-
except Exception:
|
|
98
|
-
pass
|
|
99
|
-
try:
|
|
100
|
-
skip_btn = self.page.locator('button:has-text("Skip")')
|
|
101
|
-
if skip_btn.count() > 0:
|
|
102
|
-
if skip_btn.last.is_visible():
|
|
103
|
-
skip_btn.last.click(timeout=1000, force=True)
|
|
104
|
-
self.page.wait_for_timeout(500)
|
|
105
|
-
except:
|
|
106
|
-
pass
|
|
107
|
-
if not first_token_received:
|
|
108
|
-
try:
|
|
109
|
-
current_model_turns = self.page.locator('ms-chat-turn, div[data-turn-role="Model"], div.chat-turn-container.model').count()
|
|
110
|
-
if current_model_turns > initial_model_turns:
|
|
111
|
-
first_token_received = True
|
|
112
|
-
elif time.time() - first_token_start_time > first_token_timeout:
|
|
113
|
-
raise FirstTokenTimeoutError(f"Модель не начала генерацию ответа за {first_token_timeout} секунд.")
|
|
114
|
-
except:
|
|
115
|
-
pass
|
|
116
|
-
|
|
117
|
-
try:
|
|
118
|
-
current_thumb_count = self.page.locator('button[aria-label="Good response"], .turn-footer button, button:has(.material-symbols-outlined:has-text("thumb_up"))').count()
|
|
119
|
-
if current_thumb_count > initial_thumb_count:
|
|
120
|
-
completed = True
|
|
121
|
-
break
|
|
122
|
-
except:
|
|
123
|
-
pass
|
|
124
|
-
self.page.wait_for_timeout(1000)
|
|
125
|
-
|
|
126
|
-
if not completed:
|
|
127
|
-
return "Не удалось дождаться завершения генерации (кнопка thumb_up не появилась)."
|
|
128
|
-
|
|
129
|
-
self.page.wait_for_timeout(1000)
|
|
130
|
-
|
|
131
|
-
try:
|
|
132
|
-
last_turn = self.page.locator('ms-chat-turn, div[data-turn-role="Model"], div.chat-turn-container.model').last
|
|
133
|
-
|
|
134
|
-
copy_success = False
|
|
135
|
-
try:
|
|
136
|
-
options_btn = last_turn.locator('button[aria-label="Open options"], button:has(.material-symbols-outlined:has-text("more_vert"))').last
|
|
137
|
-
if options_btn.count() > 0 and options_btn.is_visible():
|
|
138
|
-
options_btn.click(timeout=3000, force=True)
|
|
139
|
-
self.page.wait_for_timeout(500)
|
|
140
|
-
|
|
141
|
-
copy_btn = self.page.locator('button[role="menuitem"]:has-text("Copy as markdown"), button[role="menuitem"]:has-text("Copy")').first
|
|
142
|
-
if copy_btn.count() > 0 and copy_btn.is_visible():
|
|
143
|
-
copy_btn.click(timeout=3000, force=True)
|
|
144
|
-
self.page.wait_for_timeout(1000)
|
|
145
|
-
copy_success = True
|
|
146
|
-
except:
|
|
147
|
-
pass
|
|
148
|
-
|
|
149
|
-
if copy_success:
|
|
150
|
-
clip_val = self.page.evaluate("navigator.clipboard.readText().catch(() => '')")
|
|
151
|
-
if clip_val:
|
|
152
|
-
return clip_val
|
|
153
|
-
|
|
154
|
-
try:
|
|
155
|
-
last_turn.evaluate("""(el) => {
|
|
156
|
-
const footers = el.querySelectorAll('.turn-footer, .actions-container');
|
|
157
|
-
footers.forEach(f => f.style.display = 'none');
|
|
158
|
-
}""")
|
|
159
|
-
except:
|
|
160
|
-
pass
|
|
161
|
-
|
|
162
|
-
text = last_turn.inner_text()
|
|
163
|
-
if text.strip():
|
|
164
|
-
return _clean_text(text)
|
|
165
|
-
|
|
166
|
-
except Exception as e:
|
|
167
|
-
return f"Ошибка при извлечении текста: {e}"
|
|
168
|
-
|
|
169
|
-
return "Не удалось получить текст ответа."
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{majorchik_api-0.0.2a2 → majorchik_api-0.0.3}/src/majorchik_api.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|