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.
Files changed (20) hide show
  1. {majorchik_api-0.0.2a2 → majorchik_api-0.0.3}/PKG-INFO +2 -1
  2. {majorchik_api-0.0.2a2 → majorchik_api-0.0.3}/pyproject.toml +2 -1
  3. {majorchik_api-0.0.2a2 → majorchik_api-0.0.3}/src/browser_ai/base_api.py +27 -6
  4. {majorchik_api-0.0.2a2 → majorchik_api-0.0.3}/src/browser_ai/cli.py +4 -2
  5. {majorchik_api-0.0.2a2 → majorchik_api-0.0.3}/src/browser_ai/deepseek_api.py +16 -4
  6. majorchik_api-0.0.3/src/browser_ai/google_ai_studio_api.py +281 -0
  7. {majorchik_api-0.0.2a2 → majorchik_api-0.0.3}/src/majorchik_api.egg-info/PKG-INFO +2 -1
  8. {majorchik_api-0.0.2a2 → majorchik_api-0.0.3}/src/majorchik_api.egg-info/requires.txt +1 -0
  9. {majorchik_api-0.0.2a2 → majorchik_api-0.0.3}/tests/test_upload.py +6 -1
  10. majorchik_api-0.0.2a2/src/browser_ai/google_ai_studio_api.py +0 -169
  11. {majorchik_api-0.0.2a2 → majorchik_api-0.0.3}/README.md +0 -0
  12. {majorchik_api-0.0.2a2 → majorchik_api-0.0.3}/setup.cfg +0 -0
  13. {majorchik_api-0.0.2a2 → majorchik_api-0.0.3}/src/browser_ai/__init__.py +0 -0
  14. {majorchik_api-0.0.2a2 → majorchik_api-0.0.3}/src/browser_ai/exceptions.py +0 -0
  15. {majorchik_api-0.0.2a2 → majorchik_api-0.0.3}/src/browser_ai/factory.py +0 -0
  16. {majorchik_api-0.0.2a2 → majorchik_api-0.0.3}/src/majorchik_api.egg-info/SOURCES.txt +0 -0
  17. {majorchik_api-0.0.2a2 → majorchik_api-0.0.3}/src/majorchik_api.egg-info/dependency_links.txt +0 -0
  18. {majorchik_api-0.0.2a2 → majorchik_api-0.0.3}/src/majorchik_api.egg-info/entry_points.txt +0 -0
  19. {majorchik_api-0.0.2a2 → majorchik_api-0.0.3}/src/majorchik_api.egg-info/top_level.txt +0 -0
  20. {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.2a2
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.2a2"
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
- "--no-sandbox",
35
- "--disable-infobars"
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
- viewport={'width': 1280, 'height': 800},
49
+ no_viewport=True,
48
50
  permissions=["clipboard-read", "clipboard-write"]
49
51
  )
50
- self.page = self.browser.new_page()
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.fill(prompt)
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 < 5; 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.ds-icon-button[role="button"]');
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.2a2
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
 
@@ -1 +1,2 @@
1
1
  playwright>=1.30.0
2
+ playwright-stealth
@@ -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